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.
61 lines
2.3 KiB
JavaScript
61 lines
2.3 KiB
JavaScript
// Every connector action, timestamped: kept in memory for the panel and
|
|
// appended to <dataDir>/actions.log as JSON lines, rotated at 1 MB (one old
|
|
// file kept as actions.log.1).
|
|
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const KEEP = 500;
|
|
const ROTATE_BYTES = 1024 * 1024;
|
|
|
|
class ActionLog {
|
|
constructor(dir, { onEntry } = {}) {
|
|
this.file = path.join(dir, "actions.log");
|
|
this.onEntry = typeof onEntry === "function" ? onEntry : null;
|
|
this.entries = [];
|
|
this.seq = 0;
|
|
try {
|
|
const lines = fs.readFileSync(this.file, "utf8").trim().split("\n").slice(-KEEP);
|
|
for (const l of lines) { try { this.entries.push(JSON.parse(l)); } catch {} }
|
|
this.seq = this.entries.reduce((m, e) => Math.max(m, Number(e.seq) || 0), 0);
|
|
} catch {}
|
|
}
|
|
|
|
add(fields) {
|
|
const entry = { seq: ++this.seq, ts: new Date().toISOString(), ...fields };
|
|
this.entries.push(entry);
|
|
if (this.entries.length > KEEP) this.entries.splice(0, this.entries.length - KEEP);
|
|
try {
|
|
try { if (fs.statSync(this.file).size > ROTATE_BYTES) fs.renameSync(this.file, this.file + ".1"); } catch {}
|
|
fs.appendFileSync(this.file, JSON.stringify(entry) + "\n");
|
|
} catch {}
|
|
if (this.onEntry) { try { this.onEntry(entry); } catch {} }
|
|
return entry;
|
|
}
|
|
|
|
recent(n = 200) { return this.entries.slice(-Math.max(1, Math.min(KEEP, n))); }
|
|
|
|
clear() {
|
|
this.entries = [];
|
|
try { fs.rmSync(this.file, { force: true }); fs.rmSync(this.file + ".1", { force: true }); } catch {}
|
|
}
|
|
}
|
|
|
|
// A short, safe description of a tool's arguments for the log. Typed and
|
|
// pasted content is logged by length and its first characters only.
|
|
function summarizeArgs(args) {
|
|
const a = args && typeof args === "object" ? args : {};
|
|
const out = [];
|
|
const clip = (s, n) => { s = String(s); return s.length > n ? s.slice(0, n) + "…" : s; };
|
|
for (const [k, v] of Object.entries(a)) {
|
|
if (v == null || k === "tab_id") continue;
|
|
if (k === "text" || k === "html" || k === "expression") {
|
|
const s = String(v);
|
|
out.push(`${k}: ${s.length} chars "${clip(s.replace(/\s+/g, " "), 60)}"`);
|
|
} else if (typeof v === "object") out.push(`${k}: ${clip(JSON.stringify(v), 80)}`);
|
|
else out.push(`${k}: ${clip(v, 120)}`);
|
|
}
|
|
return out.join(", ");
|
|
}
|
|
|
|
module.exports = { ActionLog, summarizeArgs };
|