theseus/bundled-addons/claude-connector/lib/action-log.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

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 };