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.
package/host/host.js ADDED
@@ -0,0 +1,368 @@
1
+ import net from "node:net";
2
+ import fs from "node:fs";
3
+ import { execFileSync } from "node:child_process";
4
+ import { STATE_DIR, LOG_PATH } from "../lib/paths.js";
5
+ import { browserForExecutable, socketPath, LEGACY_SOCKET } from "../lib/browsers.js";
6
+ import { rotateLog, pruneScreenshots, pruneTempFiles } from "../lib/housekeeping.js";
7
+ import { blockedBy, readBlocklist } from "../lib/blocklist.js";
8
+
9
+ fs.mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
10
+ await rotateLog().catch(() => {});
11
+ pruneScreenshots().catch(() => {});
12
+ pruneTempFiles().catch(() => {});
13
+
14
+ function log(...parts) {
15
+ fs.appendFileSync(LOG_PATH, `${new Date().toISOString()} ${parts.join(" ")}\n`);
16
+ }
17
+
18
+ let extensionInfo = null;
19
+ let nextId = 1;
20
+ const pending = new Map();
21
+ const clients = new Set();
22
+ const attached = new Set();
23
+ const dialogs = new Map();
24
+ const autoDialogTabs = new Set();
25
+ const handledDialogs = new Map();
26
+ const sessions = new Map();
27
+ const shims = new Map();
28
+ const autoAttached = new Set();
29
+ const shimScripts = new Map();
30
+
31
+ const debuggee = (tabId, sessionId) => (sessionId ? { tabId, sessionId } : tabId);
32
+ const send = (tabId, sessionId, method, params = {}) => callExtension("debugger.send", [debuggee(tabId, sessionId), method, params]);
33
+
34
+ const popupLinks = new Map();
35
+
36
+ const POPUP_SHIM = `(() => {
37
+ if (Object.prototype.hasOwnProperty.call(window, "__arcOpenerLink")) return;
38
+ Object.defineProperty(window, "__arcOpenerLink", { value: true });
39
+ const send = (payload) => {
40
+ try {
41
+ window.__arcOpener(JSON.stringify(payload));
42
+ } catch {}
43
+ };
44
+ const opener = {
45
+ closed: false,
46
+ focus() {},
47
+ blur() {},
48
+ postMessage(data, targetOrigin) {
49
+ send({ type: "message", data, targetOrigin: String(targetOrigin ?? "*"), origin: location.origin });
50
+ },
51
+ };
52
+ Object.defineProperty(window, "opener", { get: () => opener, set() {}, configurable: true });
53
+ window.close = function close() {
54
+ send({ type: "close" });
55
+ };
56
+ })();`;
57
+
58
+ const OPENER_CALL = `function (action, id, data, origin) {
59
+ const shim = globalThis.__arcShim;
60
+ if (!shim) return false;
61
+ if (action === "deliver") return shim.deliver(id, data, origin);
62
+ if (action === "close") return shim.closeStub(id);
63
+ if (action === "url") return shim.setStubUrl(id, data);
64
+ return false;
65
+ }`;
66
+
67
+ async function linkPopup(popupTabId, link) {
68
+ await attach(popupTabId);
69
+ popupLinks.set(popupTabId, link);
70
+ await send(popupTabId, null, "Page.enable").catch(() => {});
71
+ await send(popupTabId, null, "Runtime.enable").catch(() => {});
72
+ await send(popupTabId, null, "Runtime.addBinding", { name: "__arcOpener" });
73
+ await send(popupTabId, null, "Page.addScriptToEvaluateOnNewDocument", { source: POPUP_SHIM, runImmediately: true });
74
+ return true;
75
+ }
76
+
77
+ async function callOpener(link, action, data = null, origin = null) {
78
+ const global = await send(link.tabId, null, "Runtime.evaluate", { expression: "globalThis" }).catch(() => null);
79
+ const objectId = global?.result?.objectId;
80
+ if (!objectId) return;
81
+ await send(link.tabId, null, "Runtime.callFunctionOn", {
82
+ objectId,
83
+ functionDeclaration: OPENER_CALL,
84
+ arguments: [{ value: action }, { value: link.stubId }, { value: data }, { value: origin }],
85
+ returnByValue: true,
86
+ }).catch(() => {});
87
+ await send(link.tabId, null, "Runtime.releaseObject", { objectId }).catch(() => {});
88
+ }
89
+
90
+ function onPopupEvent(message) {
91
+ const link = popupLinks.get(message.tabId);
92
+ if (!link || message.sessionId) return;
93
+ if (message.method === "Runtime.bindingCalled" && message.params.name === "__arcOpener") {
94
+ let payload;
95
+ try {
96
+ payload = JSON.parse(message.params.payload);
97
+ } catch {
98
+ return;
99
+ }
100
+ if (payload.type === "message") callOpener(link, "deliver", payload.data, payload.origin);
101
+ if (payload.type === "close") {
102
+ callOpener(link, "close");
103
+ callExtension("tabs.remove", [message.tabId]).catch(() => {});
104
+ }
105
+ }
106
+ if (message.method === "Page.frameNavigated" && !message.params.frame?.parentId) callOpener(link, "url", message.params.frame.url);
107
+ }
108
+
109
+ async function injectShim(tabId, sessionId) {
110
+ const shim = shims.get(tabId);
111
+ if (!shim) return;
112
+ if (!sessionId) {
113
+ await send(tabId, null, "Page.setInterceptFileChooserDialog", { enabled: true }).catch(() => {});
114
+ await send(tabId, null, "Emulation.setFocusEmulationEnabled", { enabled: true }).catch(() => {});
115
+ }
116
+ const key = `${tabId}:${sessionId ?? "root"}`;
117
+ const previous = shimScripts.get(key);
118
+ if (previous?.source === shim) return;
119
+ if (previous) await send(tabId, sessionId, "Page.removeScriptToEvaluateOnNewDocument", { identifier: previous.identifier }).catch(() => {});
120
+ await send(tabId, sessionId, "Page.enable").catch(() => {});
121
+ const result = await send(tabId, sessionId, "Page.addScriptToEvaluateOnNewDocument", { source: shim, runImmediately: true }).catch(() => null);
122
+ if (result?.identifier) shimScripts.set(key, { source: shim, identifier: result.identifier });
123
+ }
124
+
125
+ async function prepareSession(tabId, sessionId) {
126
+ await send(tabId, sessionId, "Target.setAutoAttach", { autoAttach: true, waitForDebuggerOnStart: false, flatten: true }).catch(() => {});
127
+ await injectShim(tabId, sessionId);
128
+ }
129
+
130
+ async function enableAutoAttach(tabId, refresh) {
131
+ if (autoAttached.has(tabId) && !refresh) return;
132
+ autoAttached.add(tabId);
133
+ if (refresh) {
134
+ sessions.delete(tabId);
135
+ await send(tabId, null, "Target.setAutoAttach", { autoAttach: false, waitForDebuggerOnStart: false, flatten: true }).catch(() => {});
136
+ }
137
+ await send(tabId, null, "Target.setAutoAttach", { autoAttach: true, waitForDebuggerOnStart: false, flatten: true }).catch(() => {});
138
+ }
139
+
140
+ function forgetTab(tabId) {
141
+ attached.delete(tabId);
142
+ dialogs.delete(tabId);
143
+ autoDialogTabs.delete(tabId);
144
+ handledDialogs.delete(tabId);
145
+ sessions.delete(tabId);
146
+ shims.delete(tabId);
147
+ autoAttached.delete(tabId);
148
+ for (const key of shimScripts.keys()) if (key.startsWith(`${tabId}:`)) shimScripts.delete(key);
149
+ popupLinks.delete(tabId);
150
+ }
151
+
152
+ function sendToExtension(message) {
153
+ const body = Buffer.from(JSON.stringify(message));
154
+ const header = Buffer.alloc(4);
155
+ header.writeUInt32LE(body.length, 0);
156
+ process.stdout.write(Buffer.concat([header, body]));
157
+ }
158
+
159
+ function sendToClient(client, message) {
160
+ if (!client.destroyed) client.write(JSON.stringify(message) + "\n");
161
+ }
162
+
163
+ function callExtension(api, args) {
164
+ return new Promise((resolve, reject) => {
165
+ const id = nextId++;
166
+ pending.set(id, { resolve, reject });
167
+ sendToExtension({ id, api, args });
168
+ });
169
+ }
170
+
171
+ function onExtensionMessage(message) {
172
+ if (message.type === "hello") {
173
+ extensionInfo = { version: message.version, connectedAt: new Date().toISOString() };
174
+ log("extension hello", message.version);
175
+ return;
176
+ }
177
+ if (message.type) {
178
+ if (message.type === "tabRemoved" && popupLinks.has(message.tabId)) callOpener(popupLinks.get(message.tabId), "close");
179
+ if (message.type === "detached" || message.type === "tabRemoved") forgetTab(message.tabId);
180
+ if (message.type === "event") onPopupEvent(message);
181
+ if (message.method === "Page.frameNavigated" && !message.sessionId && !message.params.frame?.parentId && autoDialogTabs.has(message.tabId)) {
182
+ const pattern = blockedBy(message.params.frame.url, readBlocklist());
183
+ if (pattern) {
184
+ log("blocked", message.tabId, pattern);
185
+ send(message.tabId, null, "Page.navigate", { url: "about:blank" }).catch(() => {});
186
+ }
187
+ }
188
+ if (message.method === "Target.attachedToTarget") {
189
+ const info = message.params.targetInfo;
190
+ const tabSessions = sessions.get(message.tabId) ?? new Map();
191
+ for (const [sessionId, entry] of tabSessions) if (entry.targetId === info.targetId) tabSessions.delete(sessionId);
192
+ tabSessions.set(message.params.sessionId, { sessionId: message.params.sessionId, targetId: info.targetId, type: info.type, url: info.url, parent: message.sessionId ?? null });
193
+ sessions.set(message.tabId, tabSessions);
194
+ if (info.type === "iframe") prepareSession(message.tabId, message.params.sessionId);
195
+ }
196
+ if (message.method === "Target.detachedFromTarget") {
197
+ sessions.get(message.tabId)?.delete(message.params.sessionId);
198
+ shimScripts.delete(`${message.tabId}:${message.params.sessionId}`);
199
+ }
200
+ if (message.method === "Page.javascriptDialogOpening") {
201
+ if (autoDialogTabs.has(message.tabId)) {
202
+ const accept = message.params.type === "beforeunload";
203
+ send(message.tabId, message.sessionId, "Page.handleJavaScriptDialog", { accept }).catch(() => {});
204
+ const log = handledDialogs.get(message.tabId) ?? [];
205
+ log.push({ type: message.params.type, message: message.params.message, accepted: accept, native: true });
206
+ handledDialogs.set(message.tabId, log.slice(-20));
207
+ } else {
208
+ dialogs.set(message.tabId, message.params);
209
+ }
210
+ }
211
+ if (message.method === "Page.javascriptDialogClosed") dialogs.delete(message.tabId);
212
+ if (message.method === "Page.fileChooserOpened" && autoDialogTabs.has(message.tabId)) {
213
+ const log = handledDialogs.get(message.tabId) ?? [];
214
+ log.push({ type: "filechooser", message: "", accepted: false, native: true });
215
+ handledDialogs.set(message.tabId, log.slice(-20));
216
+ }
217
+ for (const client of clients) sendToClient(client, message);
218
+ return;
219
+ }
220
+ const entry = pending.get(message.id);
221
+ if (!entry) return;
222
+ pending.delete(message.id);
223
+ if (message.error) entry.reject(new Error(message.error));
224
+ else entry.resolve(message.result);
225
+ }
226
+
227
+ let buffer = Buffer.alloc(0);
228
+ process.stdin.on("data", (chunk) => {
229
+ buffer = Buffer.concat([buffer, chunk]);
230
+ while (buffer.length >= 4) {
231
+ const length = buffer.readUInt32LE(0);
232
+ if (buffer.length < 4 + length) break;
233
+ const body = buffer.subarray(4, 4 + length).toString("utf8");
234
+ buffer = buffer.subarray(4 + length);
235
+ try {
236
+ onExtensionMessage(JSON.parse(body));
237
+ } catch (error) {
238
+ log("bad message from extension", error.message);
239
+ }
240
+ }
241
+ });
242
+
243
+ process.stdin.on("end", () => {
244
+ log("extension disconnected");
245
+ shutdown();
246
+ });
247
+
248
+ async function attach(tabId) {
249
+ if (attached.has(tabId)) {
250
+ await enableAutoAttach(tabId, false);
251
+ return { already: true };
252
+ }
253
+ try {
254
+ await callExtension("debugger.attach", [tabId]);
255
+ attached.add(tabId);
256
+ await enableAutoAttach(tabId, false);
257
+ return { already: false };
258
+ } catch (error) {
259
+ if (!/already attached/i.test(error.message)) throw error;
260
+ attached.add(tabId);
261
+ await enableAutoAttach(tabId, true);
262
+ return { already: true };
263
+ }
264
+ }
265
+
266
+ async function onClientRequest(client, request) {
267
+ const { id, api, args = [] } = request;
268
+ try {
269
+ let result;
270
+ if (api === "host.status") {
271
+ result = { host: { pid: process.pid, node: process.version, browser: BROWSER }, extension: extensionInfo, attached: [...attached] };
272
+ } else if (api === "host.dialog") {
273
+ result = dialogs.get(args[0]) ?? null;
274
+ } else if (api === "host.autoDialogs") {
275
+ if (args[1]) {
276
+ autoDialogTabs.add(args[0]);
277
+ if (args[2]) shims.set(args[0], args[2]);
278
+ await injectShim(args[0], null);
279
+ for (const sessionId of sessions.get(args[0])?.keys() ?? []) await injectShim(args[0], sessionId);
280
+ } else {
281
+ autoDialogTabs.delete(args[0]);
282
+ shims.delete(args[0]);
283
+ }
284
+ result = true;
285
+ } else if (api === "host.linkPopup") {
286
+ result = await linkPopup(args[0], args[1]);
287
+ } else if (api === "host.sessions") {
288
+ result = [...(sessions.get(args[0])?.values() ?? [])];
289
+ } else if (api === "host.handledDialogs") {
290
+ result = handledDialogs.get(args[0]) ?? [];
291
+ handledDialogs.delete(args[0]);
292
+ } else if (api === "debugger.attach") {
293
+ result = await attach(args[0]);
294
+ } else {
295
+ if (api === "debugger.detach") forgetTab(args[0]);
296
+ result = await callExtension(api, args);
297
+ if (api === "debugger.send" && args[1] === "Page.handleJavaScriptDialog") dialogs.delete(args[0]);
298
+ }
299
+ sendToClient(client, { id, result });
300
+ } catch (error) {
301
+ sendToClient(client, { id, error: error.message });
302
+ }
303
+ }
304
+
305
+ function onClientClose(client) {
306
+ clients.delete(client);
307
+ }
308
+
309
+ function launchingBrowser() {
310
+ if (process.env.CLAUDE4ARC_HOST_BROWSER) return process.env.CLAUDE4ARC_HOST_BROWSER;
311
+ try {
312
+ const executable = execFileSync("ps", ["-o", "comm=", "-p", String(process.ppid)], { encoding: "utf8" }).trim();
313
+ return browserForExecutable(executable)?.key ?? "browser";
314
+ } catch {
315
+ return "browser";
316
+ }
317
+ }
318
+
319
+ const BROWSER = launchingBrowser();
320
+ const SOCKET_PATH = socketPath(BROWSER);
321
+
322
+ try {
323
+ fs.unlinkSync(SOCKET_PATH);
324
+ } catch {}
325
+ try {
326
+ if (BROWSER === "arc" && fs.lstatSync(LEGACY_SOCKET).isSocket()) fs.unlinkSync(LEGACY_SOCKET);
327
+ } catch {}
328
+
329
+ const server = net.createServer((client) => {
330
+ clients.add(client);
331
+ let text = "";
332
+ client.setEncoding("utf8");
333
+ client.on("data", (chunk) => {
334
+ text += chunk;
335
+ let index;
336
+ while ((index = text.indexOf("\n")) >= 0) {
337
+ const line = text.slice(0, index);
338
+ text = text.slice(index + 1);
339
+ if (!line.trim()) continue;
340
+ try {
341
+ onClientRequest(client, JSON.parse(line));
342
+ } catch (error) {
343
+ log("bad message from client", error.message);
344
+ }
345
+ }
346
+ });
347
+ client.on("close", () => onClientClose(client));
348
+ client.on("error", () => {});
349
+ });
350
+
351
+ let socketInode = null;
352
+
353
+ server.listen(SOCKET_PATH, () => {
354
+ fs.chmodSync(SOCKET_PATH, 0o600);
355
+ socketInode = fs.statSync(SOCKET_PATH).ino;
356
+ log("host listening", process.pid, BROWSER);
357
+ });
358
+
359
+ function shutdown() {
360
+ server.close();
361
+ try {
362
+ if (fs.statSync(SOCKET_PATH).ino === socketInode) fs.unlinkSync(SOCKET_PATH);
363
+ } catch {}
364
+ process.exit(0);
365
+ }
366
+
367
+ process.on("SIGTERM", shutdown);
368
+ process.on("SIGINT", shutdown);
@@ -0,0 +1,52 @@
1
+ import { readConfig, updateConfig, CONFIG_PATH } from "./config.js";
2
+
3
+ export { CONFIG_PATH };
4
+
5
+ export function normalizePattern(raw) {
6
+ const value = String(raw ?? "")
7
+ .trim()
8
+ .toLowerCase()
9
+ .replace(/^[a-z][a-z0-9+.-]*:\/\//, "")
10
+ .replace(/^\*\./, "")
11
+ .replace(/^www\./, "")
12
+ .replace(/\/+$/, "");
13
+ if (!value || /\s/.test(value) || value.startsWith("/")) throw new Error(`Not a site pattern: "${raw}". Use a domain such as chase.com or mail.google.com/mail.`);
14
+ return value;
15
+ }
16
+
17
+ export function blockedBy(url, patterns) {
18
+ if (!patterns?.length) return null;
19
+ let parsed;
20
+ try {
21
+ parsed = new URL(url);
22
+ } catch {
23
+ return null;
24
+ }
25
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
26
+ const host = parsed.hostname.toLowerCase();
27
+ const pathname = parsed.pathname.toLowerCase();
28
+ for (const pattern of patterns) {
29
+ const slash = pattern.indexOf("/");
30
+ const domain = slash < 0 ? pattern : pattern.slice(0, slash);
31
+ const prefix = slash < 0 ? "" : pattern.slice(slash);
32
+ const hostMatches = host === domain || host.endsWith(`.${domain}`);
33
+ if (hostMatches && (!prefix || pathname === prefix || pathname.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`))) return pattern;
34
+ }
35
+ return null;
36
+ }
37
+
38
+ export function readBlocklist() {
39
+ const list = readConfig().blockedSites;
40
+ return Array.isArray(list) ? list.filter((item) => typeof item === "string") : [];
41
+ }
42
+
43
+ export function writeBlocklist(list) {
44
+ return updateConfig({ blockedSites: [...new Set(list)].sort() }).blockedSites;
45
+ }
46
+
47
+ export function blockedError(url, pattern) {
48
+ const error = new Error(`${url} is on the claude4arc blocklist (${pattern}). Ask the user to do this themselves.`);
49
+ error.fatal = true;
50
+ error.blocked = true;
51
+ return error;
52
+ }
@@ -0,0 +1,56 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { STATE_DIR } from "./paths.js";
5
+
6
+ const SUPPORT = path.join(os.homedir(), "Library", "Application Support");
7
+
8
+ export const BROWSERS = [
9
+ { key: "arc", name: "Arc", bundle: "Arc.app", process: "Arc", hostDirs: ["Google/Chrome"], extensionsPage: "arc://extensions" },
10
+ { key: "dia", name: "Dia", bundle: "Dia.app", process: "Dia", hostDirs: ["Google/Chrome", "Dia/User Data"], extensionsPage: "chrome://extensions" },
11
+ { key: "chrome", name: "Google Chrome", bundle: "Google Chrome.app", process: "Google Chrome", hostDirs: ["Google/Chrome"], extensionsPage: "chrome://extensions" },
12
+ { key: "brave", name: "Brave", bundle: "Brave Browser.app", process: "Brave Browser", hostDirs: ["BraveSoftware/Brave-Browser"], extensionsPage: "brave://extensions" },
13
+ { key: "edge", name: "Microsoft Edge", bundle: "Microsoft Edge.app", process: "Microsoft Edge", hostDirs: ["Microsoft Edge"], extensionsPage: "edge://extensions" },
14
+ { key: "chromium", name: "Chromium", bundle: "Chromium.app", process: "Chromium", hostDirs: ["Chromium"], extensionsPage: "chrome://extensions" },
15
+ ];
16
+
17
+ export const LEGACY_SOCKET = path.join(STATE_DIR, "bridge.sock");
18
+
19
+ export function browserByKey(key) {
20
+ const browser = BROWSERS.find((item) => item.key === String(key ?? "").toLowerCase());
21
+ if (!browser) throw new Error(`Unknown browser "${key}". Supported: ${BROWSERS.map((item) => item.key).join(", ")}.`);
22
+ return browser;
23
+ }
24
+
25
+ export function browserForExecutable(executable) {
26
+ return BROWSERS.find((browser) => String(executable ?? "").includes(`/${browser.bundle}/`)) ?? null;
27
+ }
28
+
29
+ export function hostManifestDirs(browser) {
30
+ return browser.hostDirs.map((dir) => path.join(SUPPORT, dir, "NativeMessagingHosts"));
31
+ }
32
+
33
+ export function isInstalled(browser) {
34
+ return [path.join("/Applications", browser.bundle), path.join(os.homedir(), "Applications", browser.bundle)].some((app) => fs.existsSync(app));
35
+ }
36
+
37
+ export function installedBrowsers() {
38
+ return BROWSERS.filter(isInstalled);
39
+ }
40
+
41
+ export function socketPath(key) {
42
+ return path.join(STATE_DIR, `${key}.sock`);
43
+ }
44
+
45
+ export function socketCandidates(preferred) {
46
+ if (preferred) {
47
+ const browser = browserByKey(preferred);
48
+ return browser.key === "arc" ? [socketPath("arc"), LEGACY_SOCKET] : [socketPath(browser.key)];
49
+ }
50
+ return [...BROWSERS.map((browser) => socketPath(browser.key)), LEGACY_SOCKET];
51
+ }
52
+
53
+ export function browserOfSocket(file) {
54
+ if (file === LEGACY_SOCKET) return "arc";
55
+ return path.basename(file, ".sock");
56
+ }
package/lib/client.js ADDED
@@ -0,0 +1,89 @@
1
+ import fs from "node:fs";
2
+ import net from "node:net";
3
+ import { browserByKey, browserOfSocket, socketCandidates } from "./browsers.js";
4
+
5
+ export class BridgeError extends Error {}
6
+
7
+ function connectTo(file) {
8
+ return new Promise((resolve, reject) => {
9
+ const socket = net.createConnection(file);
10
+ socket.once("connect", () => resolve(socket));
11
+ socket.once("error", reject);
12
+ });
13
+ }
14
+
15
+ export class Bridge {
16
+ static async connect({ browser } = {}) {
17
+ const preferred = browser ?? process.env.CLAUDE4ARC_BROWSER ?? null;
18
+ const candidates = socketCandidates(preferred).filter((file) => fs.existsSync(file));
19
+ let lastError = null;
20
+ for (const file of candidates) {
21
+ try {
22
+ return new Bridge(await connectTo(file), browserOfSocket(file));
23
+ } catch (error) {
24
+ lastError = error;
25
+ }
26
+ }
27
+ const name = preferred ? browserByKey(preferred).name : "Arc or another supported browser";
28
+ if (!lastError || lastError.code === "ENOENT" || lastError.code === "ECONNREFUSED") {
29
+ throw new BridgeError(
30
+ `The claude4arc bridge is not running. Make sure ${name} is open and the "claude4arc" extension is enabled. Run \`claude4arc doctor\` for details.`,
31
+ );
32
+ }
33
+ throw new BridgeError(`Cannot connect to the claude4arc bridge: ${lastError.message}`);
34
+ }
35
+
36
+ constructor(socket, browser = "arc") {
37
+ this.socket = socket;
38
+ this.browser = browser;
39
+ this.nextId = 1;
40
+ this.pending = new Map();
41
+ this.listeners = new Set();
42
+ let text = "";
43
+ socket.setEncoding("utf8");
44
+ socket.on("data", (chunk) => {
45
+ text += chunk;
46
+ let index;
47
+ while ((index = text.indexOf("\n")) >= 0) {
48
+ const line = text.slice(0, index);
49
+ text = text.slice(index + 1);
50
+ if (line.trim()) this.#dispatch(JSON.parse(line));
51
+ }
52
+ });
53
+ socket.on("close", () => {
54
+ for (const { reject } of this.pending.values()) {
55
+ reject(new BridgeError("The claude4arc bridge connection closed"));
56
+ }
57
+ this.pending.clear();
58
+ });
59
+ }
60
+
61
+ #dispatch(message) {
62
+ if (message.type) {
63
+ for (const listener of this.listeners) listener(message);
64
+ return;
65
+ }
66
+ const entry = this.pending.get(message.id);
67
+ if (!entry) return;
68
+ this.pending.delete(message.id);
69
+ if (message.error) entry.reject(new BridgeError(message.error));
70
+ else entry.resolve(message.result);
71
+ }
72
+
73
+ call(api, ...args) {
74
+ return new Promise((resolve, reject) => {
75
+ const id = this.nextId++;
76
+ this.pending.set(id, { resolve, reject });
77
+ this.socket.write(JSON.stringify({ id, api, args }) + "\n");
78
+ });
79
+ }
80
+
81
+ on(listener) {
82
+ this.listeners.add(listener);
83
+ return () => this.listeners.delete(listener);
84
+ }
85
+
86
+ close() {
87
+ this.socket.end();
88
+ }
89
+ }