claude4arc 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,405 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { execFileSync } from "node:child_process";
6
+ import { Console } from "node:console";
7
+
8
+ globalThis.console = new Console({ stdout: process.stdout, stderr: process.stderr, colorMode: false });
9
+ import { Bridge } from "../lib/client.js";
10
+ import { Task, listTasks, browserOfTask } from "../lib/task.js";
11
+ import { runCommands, createTask, runBatch, COMMAND_NAMES } from "../lib/commands.js";
12
+ import { pruneScreenshots, pruneTempFiles } from "../lib/housekeeping.js";
13
+ import { normalizePattern, readBlocklist, writeBlocklist, CONFIG_PATH } from "../lib/blocklist.js";
14
+ import { readConfig, updateConfig } from "../lib/config.js";
15
+ import { ROOT, STATE_DIR, LOG_PATH, HOST_NAME, EXTENSION_ID } from "../lib/paths.js";
16
+ import { BROWSERS, installedBrowsers, hostManifestDirs, socketCandidates, browserOfSocket } from "../lib/browsers.js";
17
+
18
+ const manifestPathIn = (dir) => path.join(dir, `${HOST_NAME}.json`);
19
+ const manifestDirsFor = (browsers) => [...new Set(browsers.flatMap(hostManifestDirs))];
20
+ const LAUNCHER_PATH = path.join(STATE_DIR, "host-launcher.sh");
21
+ const SKILL_LINK = path.join(os.homedir(), ".claude", "skills", "claude4arc");
22
+ const LEGACY_SKILL_LINK = path.join(os.homedir(), ".claude", "skills", "arc-browser");
23
+
24
+ const USAGE = `claude4arc: let Claude Code drive Arc (or Dia, Chrome, Brave, Edge, Chromium)
25
+
26
+ Usage:
27
+ claude4arc new [url] [name] [-s] Create a task (page p1), -s prints a snapshot
28
+ claude4arc <id>[:page] <cmd> [args] [-- <cmd> ...] [-s]
29
+ Run one or more commands; -s appends a diff snapshot
30
+ claude4arc batch [id] [--keep] Run one command chain per stdin line ("B1: goto … -- click …");
31
+ prints each line's start/end ms; finishes the task unless --keep or an id
32
+ claude4arc run [id] [-e <code>] Run a script (stdin when -e is absent); with id, t and page are set
33
+ claude4arc status Show bridge, extension, and task state
34
+ claude4arc clean [--all] Delete screenshots older than a day (or all) and stale temp files
35
+ claude4arc block <site...> Never let Claude open or act on these sites (chase.com, mail.google.com/mail)
36
+ claude4arc unblock <site...> Remove sites from the blocklist
37
+ claude4arc blocked List blocked sites
38
+ claude4arc tabs List open Arc tabs
39
+ claude4arc doctor Diagnose installation problems
40
+ claude4arc reload-extension Reload the Arc extension after changing extension/
41
+ claude4arc install [--no-skill] [--extension-id <id>]
42
+ Register the native host and link the Claude skill
43
+ claude4arc uninstall Remove the native host registration and skill link
44
+ claude4arc help [topic] Print API help (topics: api, selectors, keys)`;
45
+
46
+ const API_HELP = `Globals inside \`claude4arc run\`:
47
+ await task(name, { active }) create a task; opens page p1 (background tab unless active: true)
48
+ await task(id) resume an existing task by numeric id
49
+ await listTasks() list tasks saved on disk
50
+ help(topic) print this help
51
+
52
+ Task:
53
+ spaceId, name, page(label), await pages(), await tabs(), await userTab()
54
+ await newPage({ url, active }), await adopt(tabOrTabId, { as }), await release(label)
55
+ await finish({ keep: [] })
56
+
57
+ Page:
58
+ label, tabId, openedBy, url(), title(), info()
59
+ goto(url, { waitUntil: "load"|"domcontentloaded"|"networkidle"|"none", timeout })
60
+ reload(), goBack(), goForward(), bringToFront(), close()
61
+ snapshot({ scope: "viewport"|"full_page", root: "@N", diff, maxLines }), find(query, { limit })
62
+ seek(selOrText, { container, max, timeout, direction: "down"|"up" })
63
+ text({ maxChars, all }), screenshot({ path, fullPage, selector, scale: "css"|"device" })
64
+ click(sel, { button, clickCount, force, modifiers, label }), dblclick(sel), hover(sel)
65
+ fill(sel, value), press(sel, key) | press(key), focus(sel), check(sel, bool), uncheck(sel)
66
+ selectOption(sel, value | { label } | { index } | array | null)
67
+ setInputFiles(sel, paths), waitForFileChooser() -> { setFiles(paths) }
68
+ dragAndDrop(source, target, { steps }) -> { mode: "html5"|"pointer" }, scroll(deltaY, { deltaX, x, y })
69
+ waitForURL(match), waitForSelector(sel, { state }), waitForLoadState(state)
70
+ waitForFunction(fn, arg, { timeout }), waitForTimeout(ms), waitForEvent("popup"|"dialog"|"load")
71
+ acceptDialog(promptText), dismissDialog()
72
+ evaluate(fnOrString, arg), fetch(url, { method, headers, body, saveAs }), cdp(method, params)
73
+ mouse.move(x, y, { steps }), mouse.click(x, y), mouse.down(), mouse.up(), mouse.wheel(dx, dy)
74
+ keyboard.press("Meta+a"), keyboard.type(text), keyboard.insertText(text), keyboard.paste(text)`;
75
+
76
+ const SELECTOR_HELP = `Selectors (every element action needs exactly one visible match):
77
+ @12 or ref=12 ref from the latest snapshot
78
+ text=Sign in case-insensitive substring of visible text
79
+ text="Sign in" exact, case-sensitive text
80
+ role=button[name="Sign in"] role + exact accessible name (also loc=role:...)
81
+ role=link[name*="docs"] role + name substring
82
+ loc=href:/pricing link whose href contains the value
83
+ xpath=//main//h2
84
+ css=.card:has-text("Pro") CSS with a terminal :has-text() or :text-is()
85
+ button.primary >> nth=0 any selector + index
86
+ button.primary raw CSS (searches open and closed shadow roots and same-origin iframes)`;
87
+
88
+ const KEY_HELP = `Keys use Playwright names: Enter, Tab, Escape, Backspace, Delete, ArrowUp, Home, PageDown, F1, a, A, 1, ...
89
+ Chords join with "+": "Meta+a", "Shift+Tab", "ControlOrMeta+Enter". Mac editing shortcuts (Meta+a/c/v/x/z) run the native editing command.`;
90
+
91
+ function help(topic) {
92
+ const text = topic === "selectors" ? SELECTOR_HELP : topic === "keys" ? KEY_HELP : `${API_HELP}\n\n${SELECTOR_HELP}\n\n${KEY_HELP}`;
93
+ console.log(text);
94
+ }
95
+
96
+ async function readStdin() {
97
+ if (process.stdin.isTTY) return "";
98
+ const chunks = [];
99
+ for await (const chunk of process.stdin) chunks.push(chunk);
100
+ return Buffer.concat(chunks).toString("utf8");
101
+ }
102
+
103
+ async function withBridge(work, { taskId, browser: requested } = {}) {
104
+ const browser = requested ?? process.env.CLAUDE4ARC_BROWSER ?? (taskId ? await browserOfTask(taskId) : null) ?? undefined;
105
+ const bridge = await Bridge.connect({ browser });
106
+ try {
107
+ return await work(bridge);
108
+ } finally {
109
+ bridge.close();
110
+ }
111
+ }
112
+
113
+ async function runScript(code, taskId) {
114
+ if (!code.trim()) throw new Error("No script given. Pipe code on stdin or pass -e '<code>'.");
115
+ const bridge = await Bridge.connect({ browser: process.env.CLAUDE4ARC_BROWSER ?? (taskId ? await browserOfTask(taskId) : null) ?? undefined });
116
+ const tasks = [];
117
+ const openTask = async (nameOrId, options) => {
118
+ const task = await Task.open(bridge, nameOrId, options);
119
+ tasks.push(task);
120
+ return task;
121
+ };
122
+ const cleanup = async () => {
123
+ for (const task of tasks) {
124
+ for (const note of await task._settleDialogs().catch(() => [])) console.log(note);
125
+ }
126
+ bridge.close();
127
+ };
128
+ process.once("SIGINT", async () => {
129
+ await cleanup();
130
+ process.exit(130);
131
+ });
132
+ const AsyncFunction = (async () => {}).constructor;
133
+ const names = ["task", "taskSpace", "listTasks", "help", ...(taskId ? ["t", "page"] : [])];
134
+ const script = new AsyncFunction(...names, code);
135
+ try {
136
+ const t = taskId ? await openTask(Number(taskId)) : undefined;
137
+ await script(openTask, openTask, listTasks, help, ...(t ? [t, t.page()] : []));
138
+ } finally {
139
+ await cleanup();
140
+ }
141
+ }
142
+
143
+ async function status() {
144
+ const tasks = await listTasks();
145
+ const bridges = await liveBridges();
146
+ const browsers = [];
147
+ for (const bridge of bridges.filter((entry) => !entry.error)) {
148
+ await withBridge(
149
+ async (connection) => {
150
+ const host = await connection.call("host.status");
151
+ const tabs = await connection.call("tabs.query", {});
152
+ browsers.push({ browser: bridge.browser, host: host.host, extension: bridge.info, tabCount: tabs.length });
153
+ },
154
+ { browser: bridge.browser },
155
+ );
156
+ }
157
+ console.log(JSON.stringify({ connected: browsers.length > 0, browsers, tasks }, null, 2));
158
+ if (!browsers.length) process.exitCode = 1;
159
+ }
160
+
161
+ async function tabs() {
162
+ await withBridge(async (bridge) => {
163
+ const list = await bridge.call("tabs.query", {});
164
+ for (const tab of list) {
165
+ console.log(`${tab.active ? "*" : " "} ${String(tab.id).padEnd(10)} ${tab.title?.slice(0, 60).padEnd(60)} ${tab.url}`);
166
+ }
167
+ });
168
+ }
169
+
170
+ function resolveNode() {
171
+ try {
172
+ const found = execFileSync("/bin/sh", ["-lc", "command -v node"], { encoding: "utf8" }).trim();
173
+ if (found) return found;
174
+ } catch {}
175
+ return process.execPath;
176
+ }
177
+
178
+ async function removeOwnLink(link) {
179
+ try {
180
+ if ((await fs.readlink(link)) === path.join(ROOT, "skill")) await fs.rm(link);
181
+ } catch {}
182
+ }
183
+
184
+ async function linkSkillFolder() {
185
+ await fs.mkdir(path.dirname(SKILL_LINK), { recursive: true });
186
+ try {
187
+ const current = await fs.readlink(SKILL_LINK);
188
+ return current === path.join(ROOT, "skill") ? "linked" : `left alone (points to ${current})`;
189
+ } catch (error) {
190
+ if (error.code !== "ENOENT") return `left alone (${SKILL_LINK} exists and is not a symlink)`;
191
+ await fs.symlink(path.join(ROOT, "skill"), SKILL_LINK);
192
+ return "linked";
193
+ }
194
+ }
195
+
196
+ function allowedExtensionIds(extra = []) {
197
+ const saved = readConfig().extensionIds;
198
+ return [...new Set([EXTENSION_ID, ...(Array.isArray(saved) ? saved : []), ...extra])];
199
+ }
200
+
201
+ async function install(args = []) {
202
+ const extra = [];
203
+ for (let index = 0; index < args.length; index++) {
204
+ if (args[index] === "--extension-id") {
205
+ const id = args[++index] ?? "";
206
+ if (!/^[a-p]{32}$/.test(id)) throw new Error(`Not an extension ID: "${id}". It is 32 letters from a to p, shown on the extensions page.`);
207
+ extra.push(id);
208
+ }
209
+ }
210
+ const linkSkill = !args.includes("--no-skill");
211
+ const extensionIds = allowedExtensionIds(extra);
212
+ if (extra.length) updateConfig({ extensionIds: extensionIds.filter((id) => id !== EXTENSION_ID) });
213
+ await fs.mkdir(STATE_DIR, { recursive: true, mode: 0o700 });
214
+ await removeOwnLink(LEGACY_SKILL_LINK);
215
+ const node = resolveNode();
216
+ const hostScript = path.join(ROOT, "host", "host.js");
217
+ await fs.writeFile(LAUNCHER_PATH, `#!/bin/sh\nexec "${node}" "${hostScript}" "$@"\n`, { mode: 0o755 });
218
+ await fs.chmod(LAUNCHER_PATH, 0o755);
219
+ const browsers = installedBrowsers();
220
+ if (!browsers.length) throw new Error(`No supported browser found in /Applications. Supported: ${BROWSERS.map((browser) => browser.name).join(", ")}.`);
221
+ const manifest = {
222
+ name: HOST_NAME,
223
+ description: "claude4arc native bridge",
224
+ path: LAUNCHER_PATH,
225
+ type: "stdio",
226
+ allowed_origins: extensionIds.map((id) => `chrome-extension://${id}/`),
227
+ };
228
+ const written = [];
229
+ for (const dir of manifestDirsFor(browsers)) {
230
+ await fs.mkdir(dir, { recursive: true });
231
+ await fs.writeFile(manifestPathIn(dir), JSON.stringify(manifest, null, 2) + "\n");
232
+ written.push(manifestPathIn(dir));
233
+ }
234
+ const linked = linkSkill ? await linkSkillFolder() : (await removeOwnLink(SKILL_LINK), "not linked (--no-skill; use the Claude Code plugin instead)");
235
+ const pages = browsers.map((browser) => `${browser.extensionsPage} (${browser.name})`).join(", ");
236
+ console.log(`Browsers found: ${browsers.map((browser) => browser.name).join(", ")}
237
+ Native host registered: ${written.join("\n ")}
238
+ Host launcher: ${LAUNCHER_PATH} (node: ${node})
239
+ Claude skill: ${SKILL_LINK} ${linked}
240
+
241
+ Last step, once in each browser you want Claude to use:
242
+ 1. Open the extensions page: ${pages}
243
+ 2. Turn on "Developer mode"
244
+ 3. Click "Load unpacked" and choose: ${path.join(ROOT, "extension")}
245
+ 4. Run: claude4arc doctor`);
246
+ }
247
+
248
+ async function uninstall() {
249
+ for (const dir of manifestDirsFor(BROWSERS)) await fs.rm(manifestPathIn(dir), { force: true });
250
+ await fs.rm(LAUNCHER_PATH, { force: true });
251
+ await removeOwnLink(SKILL_LINK);
252
+ await removeOwnLink(LEGACY_SKILL_LINK);
253
+ console.log("Removed the native host registration and the skill link. Remove the extension from each browser's extensions page.");
254
+ }
255
+
256
+ async function liveBridges() {
257
+ const found = [];
258
+ for (const file of [...new Set(socketCandidates())]) {
259
+ let isSocket = false;
260
+ try {
261
+ isSocket = (await fs.stat(file)).isSocket();
262
+ } catch {}
263
+ if (!isSocket) continue;
264
+ const browser = browserOfSocket(file);
265
+ if (found.some((entry) => entry.browser === browser)) continue;
266
+ try {
267
+ const info = await withBridge((bridge) => bridge.call("extension.info"), { browser });
268
+ found.push({ browser, file, info });
269
+ } catch (error) {
270
+ found.push({ browser, file, error: error.message });
271
+ }
272
+ }
273
+ return found;
274
+ }
275
+
276
+ async function doctor() {
277
+ const checks = [];
278
+ const check = (name, ok, detail) => checks.push(`${ok ? "ok " : "FAIL"} ${name}${detail ? ` — ${detail}` : ""}`);
279
+ const browsers = installedBrowsers();
280
+ check("supported browser installed", browsers.length > 0, browsers.map((browser) => browser.name).join(", ") || BROWSERS.map((browser) => browser.name).join(", "));
281
+ const running = browsers.filter((browser) => {
282
+ try {
283
+ execFileSync("pgrep", ["-x", browser.process]);
284
+ return true;
285
+ } catch {
286
+ return false;
287
+ }
288
+ });
289
+ check("browser running", running.length > 0, running.map((browser) => browser.name).join(", ") || "open Arc or another supported browser");
290
+ for (const dir of manifestDirsFor(browsers)) {
291
+ let manifest = null;
292
+ try {
293
+ manifest = JSON.parse(await fs.readFile(manifestPathIn(dir), "utf8"));
294
+ } catch {}
295
+ const ok = Boolean(manifest?.allowed_origins?.includes(`chrome-extension://${EXTENSION_ID}/`));
296
+ check("native host manifest", ok, ok ? manifestPathIn(dir) : `${manifestPathIn(dir)} missing, run \`claude4arc install\``);
297
+ }
298
+ let launcherOk = false;
299
+ try {
300
+ await fs.access(LAUNCHER_PATH, fs.constants.X_OK);
301
+ launcherOk = true;
302
+ } catch {}
303
+ check("host launcher is executable", launcherOk, LAUNCHER_PATH);
304
+ const bridges = await liveBridges();
305
+ for (const bridge of bridges) {
306
+ check(`extension responds in ${bridge.browser}`, !bridge.error, bridge.error ?? `v${bridge.info.version} (${bridge.info.id})`);
307
+ }
308
+ if (!bridges.length) {
309
+ check("bridge socket", false, `none. Load the extension from ${path.join(ROOT, "extension")} on your browser's extensions page, or reload it there`);
310
+ }
311
+ try {
312
+ const log = (await fs.readFile(LOG_PATH, "utf8")).trim().split("\n").slice(-3).join("\n ");
313
+ checks.push(` host log tail:\n ${log}`);
314
+ } catch {}
315
+ console.log(checks.join("\n"));
316
+ if (checks.some((line) => line.startsWith("FAIL"))) process.exitCode = 1;
317
+ }
318
+
319
+ async function main() {
320
+ const [command = "help", ...rest] = process.argv.slice(2);
321
+ if (/^\d+(:[\w-]+)?$/.test(command)) {
322
+ if (!rest.length) throw new Error(`Give a command after the task id. Commands: ${COMMAND_NAMES.join(", ")}`);
323
+ console.log(await withBridge((bridge) => runCommands(bridge, command, rest), { taskId: command.split(":")[0] }));
324
+ return;
325
+ }
326
+ switch (command) {
327
+ case "new":
328
+ console.log(await withBridge((bridge) => createTask(bridge, rest)));
329
+ break;
330
+ case "batch": {
331
+ const taskId = rest.find((arg) => /^\d+$/.test(arg));
332
+ const keep = rest.includes("--keep") || Boolean(taskId);
333
+ const script = await readStdin();
334
+ const failures = await withBridge((bridge) => runBatch(bridge, script, { taskId, keep, write: (text) => console.log(text) }), { taskId });
335
+ if (failures) process.exitCode = 1;
336
+ break;
337
+ }
338
+ case "run":
339
+ case "nodejs": {
340
+ const taskId = /^\d+$/.test(rest[0] ?? "") ? rest.shift() : undefined;
341
+ const index = rest.indexOf("-e");
342
+ const code = index >= 0 ? rest[index + 1] ?? "" : await readStdin();
343
+ await runScript(code, taskId);
344
+ break;
345
+ }
346
+ case "status":
347
+ await status();
348
+ break;
349
+ case "block":
350
+ case "unblock": {
351
+ if (!rest.length) throw new Error(`Usage: claude4arc ${command} <site> [site...], for example: claude4arc ${command} chase.com mail.google.com`);
352
+ const patterns = rest.map(normalizePattern);
353
+ const current = new Set(readBlocklist());
354
+ for (const pattern of patterns) command === "block" ? current.add(pattern) : current.delete(pattern);
355
+ const list = writeBlocklist([...current]);
356
+ console.log(list.length ? `Blocked sites (${CONFIG_PATH}):\n ${list.join("\n ")}` : "The blocklist is empty.");
357
+ break;
358
+ }
359
+ case "blocked": {
360
+ const list = readBlocklist();
361
+ console.log(list.length ? `Blocked sites (${CONFIG_PATH}):\n ${list.join("\n ")}` : "The blocklist is empty. Add sites with: claude4arc block <site>");
362
+ break;
363
+ }
364
+ case "clean": {
365
+ const all = rest.includes("--all");
366
+ const screenshots = await pruneScreenshots({ all });
367
+ const temp = await pruneTempFiles();
368
+ console.log(`Removed ${screenshots} screenshot${screenshots === 1 ? "" : "s"} and ${temp} temporary file${temp === 1 ? "" : "s"}.`);
369
+ break;
370
+ }
371
+ case "reload-extension":
372
+ await withBridge((bridge) => bridge.call("extension.reload"));
373
+ console.log("Extension reloading. Run `claude4arc status` in a few seconds.");
374
+ break;
375
+ case "tabs":
376
+ await tabs();
377
+ break;
378
+ case "doctor":
379
+ await doctor();
380
+ break;
381
+ case "install":
382
+ await install(rest);
383
+ break;
384
+ case "uninstall":
385
+ await uninstall();
386
+ break;
387
+ case "help":
388
+ case "--help":
389
+ case "-h":
390
+ if (rest[0]) help(rest[0]);
391
+ else console.log(`${USAGE}\n\n${API_HELP}`);
392
+ break;
393
+ default:
394
+ console.error(`Unknown command: ${command}\n\n${USAGE}`);
395
+ process.exitCode = 2;
396
+ }
397
+ }
398
+
399
+ main().then(
400
+ () => process.exit(process.exitCode ?? 0),
401
+ (error) => {
402
+ console.error(`Error: ${error.message}`);
403
+ process.exit(1);
404
+ },
405
+ );
@@ -0,0 +1,153 @@
1
+ const HOST_NAME = "com.arcforclaude.bridge";
2
+ const PROTOCOL_VERSION = "1.3";
3
+
4
+ let port = null;
5
+ let reconnectTimer = null;
6
+ let agentTabs = null;
7
+
8
+ async function loadAgentTabs() {
9
+ if (!agentTabs) {
10
+ const stored = await chrome.storage.session.get("agentTabs");
11
+ agentTabs = new Set(stored.agentTabs ?? []);
12
+ }
13
+ return agentTabs;
14
+ }
15
+
16
+ async function setAgentTab(tabId, enabled) {
17
+ const tabs = await loadAgentTabs();
18
+ if (enabled) tabs.add(tabId);
19
+ else tabs.delete(tabId);
20
+ await chrome.storage.session.set({ agentTabs: [...tabs] });
21
+ }
22
+
23
+ async function messageTab(tabId, message) {
24
+ try {
25
+ await chrome.tabs.sendMessage(tabId, message);
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ const debuggee = (target) => (typeof target === "number" ? { tabId: target } : target);
33
+
34
+ const handlers = {
35
+ "tabs.query": (query) => chrome.tabs.query(query ?? {}),
36
+ "tabs.get": (tabId) => chrome.tabs.get(tabId),
37
+ "tabs.create": (props) => chrome.tabs.create(props ?? {}),
38
+ "tabs.update": (tabId, props) => chrome.tabs.update(tabId, props),
39
+ "tabs.remove": (tabId) => chrome.tabs.remove(tabId),
40
+ "tabs.goBack": (tabId) => chrome.tabs.goBack(tabId),
41
+ "tabs.goForward": (tabId) => chrome.tabs.goForward(tabId),
42
+ "windows.getAll": (query) => chrome.windows.getAll(query ?? {}),
43
+ "windows.getLastFocused": () => chrome.windows.getLastFocused(),
44
+ "windows.update": (windowId, props) => chrome.windows.update(windowId, props),
45
+ "debugger.attach": (target) => chrome.debugger.attach(debuggee(target), PROTOCOL_VERSION),
46
+ "debugger.detach": (target) => chrome.debugger.detach(debuggee(target)),
47
+ "debugger.send": (target, method, params) => chrome.debugger.sendCommand(debuggee(target), method, params ?? {}),
48
+ "debugger.getTargets": () => chrome.debugger.getTargets(),
49
+ "extension.info": () => ({
50
+ version: chrome.runtime.getManifest().version,
51
+ id: chrome.runtime.id,
52
+ userAgent: navigator.userAgent,
53
+ features: ["sessions", "reload", "guard", "downloads"],
54
+ }),
55
+ "guard.enable": async (tabId) => {
56
+ await setAgentTab(tabId, true);
57
+ return messageTab(tabId, { type: "arc-guard" });
58
+ },
59
+ "guard.disable": (tabId) => setAgentTab(tabId, false).then(() => true),
60
+ "guard.sweep": async (tabId) => {
61
+ await setAgentTab(tabId, true);
62
+ return messageTab(tabId, { type: "arc-sweep" });
63
+ },
64
+ "downloads.search": (query) => chrome.downloads.search(query ?? {}),
65
+ "downloads.erase": (query) => chrome.downloads.erase(query ?? {}),
66
+ "extension.reload": () => {
67
+ setTimeout(() => chrome.runtime.reload(), 50);
68
+ return true;
69
+ },
70
+ };
71
+
72
+ function post(message) {
73
+ try {
74
+ port?.postMessage(message);
75
+ } catch {
76
+ port = null;
77
+ }
78
+ }
79
+
80
+ async function handle({ id, api, args = [] }) {
81
+ const handler = handlers[api];
82
+ if (!handler) {
83
+ post({ id, error: `Unknown api: ${api}` });
84
+ return;
85
+ }
86
+ try {
87
+ const result = await handler(...args);
88
+ post({ id, result: result ?? null });
89
+ } catch (error) {
90
+ post({ id, error: String(error?.message ?? error) });
91
+ }
92
+ }
93
+
94
+ function scheduleReconnect() {
95
+ clearTimeout(reconnectTimer);
96
+ reconnectTimer = setTimeout(connect, 1000);
97
+ }
98
+
99
+ function connect() {
100
+ if (port) return;
101
+ try {
102
+ port = chrome.runtime.connectNative(HOST_NAME);
103
+ } catch {
104
+ port = null;
105
+ scheduleReconnect();
106
+ return;
107
+ }
108
+ port.onMessage.addListener(handle);
109
+ port.onDisconnect.addListener(() => {
110
+ void chrome.runtime.lastError;
111
+ port = null;
112
+ scheduleReconnect();
113
+ });
114
+ post({ type: "hello", version: chrome.runtime.getManifest().version });
115
+ }
116
+
117
+ chrome.debugger.onEvent.addListener((source, method, params) => {
118
+ post({ type: "event", tabId: source.tabId, sessionId: source.sessionId, method, params: params ?? {} });
119
+ });
120
+
121
+ chrome.debugger.onDetach.addListener((source, reason) => {
122
+ post({ type: "detached", tabId: source.tabId, reason });
123
+ });
124
+
125
+ chrome.tabs.onCreated.addListener((tab) => {
126
+ post({ type: "tabCreated", tab });
127
+ });
128
+
129
+ chrome.tabs.onRemoved.addListener((tabId) => {
130
+ post({ type: "tabRemoved", tabId });
131
+ setAgentTab(tabId, false).catch(() => {});
132
+ });
133
+
134
+ chrome.downloads.onCreated.addListener((item) => {
135
+ post({ type: "download", event: "created", item });
136
+ });
137
+
138
+ chrome.downloads.onChanged.addListener((delta) => {
139
+ post({ type: "download", event: "changed", delta });
140
+ });
141
+
142
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
143
+ if (message?.type !== "arc-guard?" || !sender.tab) return false;
144
+ loadAgentTabs().then((tabs) => sendResponse(tabs.has(sender.tab.id)));
145
+ return true;
146
+ });
147
+
148
+ chrome.alarms.create("keepalive", { periodInMinutes: 0.5 });
149
+ chrome.alarms.onAlarm.addListener(connect);
150
+ chrome.runtime.onStartup.addListener(connect);
151
+ chrome.runtime.onInstalled.addListener(connect);
152
+
153
+ connect();
@@ -0,0 +1,66 @@
1
+ const OWN_PREFIX = `chrome-extension://${chrome.runtime.id}/`;
2
+ const watched = new WeakSet();
3
+ let enabled = false;
4
+
5
+ const isForeign = (frame) => {
6
+ const source = frame.getAttribute("src") || "";
7
+ return /^chrome-extension:\/\//i.test(source) && !source.startsWith(OWN_PREFIX);
8
+ };
9
+
10
+ const isFrame = (node) => node.tagName === "IFRAME" || node.tagName === "FRAME";
11
+
12
+ function inspect(node) {
13
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
14
+ if (isFrame(node)) {
15
+ if (isForeign(node)) node.remove();
16
+ return;
17
+ }
18
+ const shadow = chrome.dom.openOrClosedShadowRoot(node);
19
+ if (shadow) watch(shadow);
20
+ for (const child of node.children) inspect(child);
21
+ }
22
+
23
+ function onMutations(records) {
24
+ if (!enabled) return;
25
+ for (const record of records) {
26
+ if (record.type === "attributes") {
27
+ if (isFrame(record.target) && isForeign(record.target)) record.target.remove();
28
+ continue;
29
+ }
30
+ for (const node of record.addedNodes) {
31
+ inspect(node);
32
+ queueMicrotask(() => inspect(node));
33
+ }
34
+ }
35
+ }
36
+
37
+ function watch(root) {
38
+ if (watched.has(root)) return;
39
+ watched.add(root);
40
+ new MutationObserver(onMutations).observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ["src"] });
41
+ if (enabled) for (const child of root.children) inspect(child);
42
+ }
43
+
44
+ function enable() {
45
+ if (enabled) return;
46
+ enabled = true;
47
+ sweep();
48
+ }
49
+
50
+ function sweep() {
51
+ if (document.documentElement) inspect(document.documentElement);
52
+ }
53
+
54
+ watch(document);
55
+
56
+ chrome.runtime.onMessage.addListener((message) => {
57
+ if (message?.type === "arc-guard") enable();
58
+ if (message?.type === "arc-sweep") {
59
+ enable();
60
+ sweep();
61
+ }
62
+ });
63
+
64
+ chrome.runtime.sendMessage({ type: "arc-guard?" }).then((agent) => {
65
+ if (agent) enable();
66
+ }).catch(() => {});
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,41 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "claude4arc",
4
+ "version": "0.5.0",
5
+ "description": "Lets Claude Code control Arc through a local native messaging bridge.",
6
+ "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtVt9tTARyz+OoLW9OAZRFWdqNcTTaFGqNd84PFLq0N+HpDEjRc5RHzeBkw1rjSCPL6Wvlm48MgtWSDCWwBtsmvYhCsbAodaEyRBngPGcRYN0j/BK3iaQT9CQu/EuVugSaIehizrp5JdEAyV9FUjlm3N72KnZngxnt6/07/zwI1uSTUgWrUYwgKjStS9UVNGqlGT6TFde25Q1RyysU8AtbW003QqbqsgAhXI+WQ8+OE3F26nQp4aNYk7LUn0iXhRHjKpSaJo3xbC9gHxgzGlkiVpafHdmC5sxH9DVSN45OqaxXTPtzTakoWwkVFl+MKn16t6tzzSoWRZNddyY7CPInwIDAQAB",
7
+ "permissions": [
8
+ "alarms",
9
+ "debugger",
10
+ "downloads",
11
+ "nativeMessaging",
12
+ "storage",
13
+ "tabs"
14
+ ],
15
+ "host_permissions": [
16
+ "<all_urls>"
17
+ ],
18
+ "background": {
19
+ "service_worker": "background.js"
20
+ },
21
+ "minimum_chrome_version": "125",
22
+ "content_scripts": [
23
+ {
24
+ "matches": [
25
+ "<all_urls>"
26
+ ],
27
+ "js": [
28
+ "guard.js"
29
+ ],
30
+ "run_at": "document_start",
31
+ "all_frames": true,
32
+ "match_about_blank": true
33
+ }
34
+ ],
35
+ "icons": {
36
+ "16": "icons/icon-16.png",
37
+ "32": "icons/icon-32.png",
38
+ "48": "icons/icon-48.png",
39
+ "128": "icons/icon-128.png"
40
+ }
41
+ }