theseus/bundled-addons/claude-connector/lib/clipboard.cjs
Dev c06557e957 Theseus: Claude connector, an opt-in MCP server for the signed-in profile
Claude sessions could not work inside the user's own profile: the desktop
app's browser lost synthetic pastes and typing in Medium's editor, and a
browser-wide --remote-debugging-port would hand every session to any local
process. The connector is a built-in extension, off until turned on in
Settings > Extensions, that serves MCP on 127.0.0.1 behind a per-install
token and drives one tab at a time through that tab's webContents.debugger.

- Host: a "tab-control" capability (built-in add-ons only) for listing,
  opening, navigating, closing and focusing tabs, handing out web tabs'
  webContents (never Settings, extension or home tabs), and a toolbar chip
  plus tab outline while a client is connected; the chip's X stops it.
  Built-in add-ons can ship "defaultEnabled": false.
- Tools: list_tabs, open_tab, navigate, activate_tab, close_tab, screenshot,
  read_page_text, snapshot (accessibility tree with refs), click, type
  (insertText + real Enter), press_key, paste_html / paste_text (system
  clipboard + webContents.paste, clipboard restored), upload_file
  (DOM.setFileInputFiles), wait_for, and evaluate behind its own switch.
- Safety: first use of each site asks the user; password and one-time-code
  fields are refused; uploads only from folders the user added; requests
  from web pages or foreign Host headers are refused; every action is
  logged with a timestamp in the panel and actions.log.
- Claude Desktop runs stdio-shim.cjs through the Theseus executable in Node
  mode.
- Tests: unit (protocol, auth, policy, shim) and an end-to-end run on a
  throwaway profile that types, pastes HTML with a heading and a link, and
  uploads a PNG on a local page.
2026-10-23 17:45:02 +03:00

59 lines
2.3 KiB
JavaScript

// The system clipboard, for paste_html / paste_text: put our content on it,
// and save and restore what the user had there.
//
// Electron 44 replaced the synchronous clipboard (readHTML, readImage, …)
// with the W3C-style async one (read / write of ClipboardItem). Both are
// handled; a build with the old API gets text, HTML, RTF and image back.
function makeClipboard(electron) {
const cb = electron.clipboard;
const Item = electron.ClipboardItem;
if (typeof cb.read === "function" && typeof Item === "function") {
return {
async put({ html, text }) {
const data = { "text/plain": String(text ?? "") };
if (html != null) data["text/html"] = String(html);
await cb.write([new Item(data)]);
},
// Every format the platform offers, read out now (the items' getType
// would otherwise read the clipboard again later, after our write).
async save() {
const items = await cb.read();
const out = [];
for (const it of items) {
const entry = {};
for (const type of it.types) { try { entry[type] = await it.getType(type); } catch {} }
if (Object.keys(entry).length) out.push(entry);
}
return out;
},
async restore(saved) {
if (!saved || !saved.length) { cb.clear(); return; }
try { await cb.write(saved.map((e) => new Item(e))); return; } catch {}
// A platform format that cannot be written back: keep the text at least.
for (const e of saved) {
const t = e["text/plain"];
if (t) { try { await cb.writeText(typeof t === "string" ? t : await t.text()); return; } catch {} }
}
cb.clear();
},
};
}
return {
async put({ html, text }) { cb.write(html != null ? { html: String(html), text: String(text ?? "") } : { text: String(text ?? "") }); },
async save() {
const s = { text: cb.readText(), html: cb.readHTML(), rtf: cb.readRTF() };
try { const img = cb.readImage(); if (img && !img.isEmpty()) s.image = img; } catch {}
return s;
},
async restore(s) {
const back = {};
for (const k of ["text", "html", "rtf", "image"]) if (s && s[k]) back[k] = s[k];
if (Object.keys(back).length) cb.write(back); else cb.clear();
},
};
}
module.exports = { makeClipboard };