surf-cli 2.8.0 → 2.10.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/README.md +146 -8
- package/native/abort.cjs +65 -0
- package/native/activity-journal.cjs +55 -0
- package/native/ai-queue.cjs +64 -0
- package/native/aistudio-build.cjs +21 -13
- package/native/aistudio-client.cjs +40 -20
- package/native/browser-lock.cjs +2 -2
- package/native/chatgpt-client.cjs +49 -31
- package/native/cli.cjs +352 -482
- package/native/client-transport.cjs +168 -0
- package/native/do-executor.cjs +68 -510
- package/native/do-parser.cjs +8 -249
- package/native/doctor.cjs +55 -5
- package/native/endpoint.cjs +174 -0
- package/native/file-transfer.cjs +734 -0
- package/native/gemini-client.cjs +156 -71
- package/native/grok-client.cjs +98 -89
- package/native/host-helpers.cjs +43 -26
- package/native/host-sessions.cjs +287 -0
- package/native/host.cjs +998 -620
- package/native/listener.cjs +20 -0
- package/native/mcp-server.cjs +60 -65
- package/native/network-export.cjs +116 -0
- package/native/network-store.cjs +38 -58
- package/native/perplexity-client.cjs +46 -17
- package/native/playbook-authoring.cjs +44 -0
- package/native/playbook-cli.cjs +157 -0
- package/native/playbook-client.cjs +259 -0
- package/native/playbook-receipts.cjs +109 -0
- package/native/playbook-records.cjs +208 -0
- package/native/playbook-runtime.cjs +177 -0
- package/native/playbooks.cjs +235 -0
- package/native/private-state.cjs +156 -0
- package/native/redaction.cjs +104 -0
- package/native/remote-auth.cjs +279 -0
- package/native/remote-transport.cjs +337 -0
- package/native/request-pending.cjs +148 -0
- package/native/socket-path.cjs +1 -1
- package/native/workflow-definition.cjs +368 -0
- package/native/workflow-runtime.cjs +225 -0
- package/package.json +9 -6
- package/playbooks/page/ops/read.json +22 -0
- package/playbooks/page/playbook.json +7 -0
- package/scripts/install-native-host.cjs +36 -5
- package/skills/README.md +11 -5
- package/skills/deep-x-research/SKILL.md +106 -0
- package/skills/surf/SKILL.md +72 -5
package/native/host.cjs
CHANGED
|
@@ -3,6 +3,8 @@ const net = require("net");
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const os = require("os");
|
|
6
|
+
const { AsyncLocalStorage } = require("async_hooks");
|
|
7
|
+
const requestStorage = new AsyncLocalStorage();
|
|
6
8
|
const https = require("https");
|
|
7
9
|
const { execSync } = require("child_process");
|
|
8
10
|
const { GoogleGenerativeAI } = require("@google/generative-ai");
|
|
@@ -16,8 +18,99 @@ const { mapToolToMessage, mapComputerAction, formatToolContent, buildProviderUpl
|
|
|
16
18
|
|
|
17
19
|
const IS_WIN = process.platform === "win32";
|
|
18
20
|
const { SOCKET_PATH, SURF_TMP } = require("./socket-path.cjs");
|
|
21
|
+
const { parseListenEndpoint } = require("./listener.cjs");
|
|
22
|
+
const { getStateDir } = require("./remote-auth.cjs");
|
|
23
|
+
const { createFrameParser, createServerAuthSession, createSocketWriter, isClientAuthorized, writeFrame, MAX_FRAME_BYTES } = require("./remote-transport.cjs");
|
|
24
|
+
const { HostSessionManager, resolveRequestDeadlineMs } = require("./host-sessions.cjs");
|
|
25
|
+
const { abortError, abortableDelay, throwIfAborted } = require("./abort.cjs");
|
|
26
|
+
const { BoundedAiQueue } = require("./ai-queue.cjs");
|
|
27
|
+
const { RequestPendingMap } = require("./request-pending.cjs");
|
|
28
|
+
const { cleanupFilePaths, createStagingDirectory, createTransferState, materializeRemoteTool, rewriteTransferPaths, streamFileDownload, transferError } = require("./file-transfer.cjs");
|
|
29
|
+
const { writeNetworkExport } = require("./network-export.cjs");
|
|
30
|
+
const networkStore = require("./network-store.cjs");
|
|
31
|
+
const { redactUrlSecrets } = require("./redaction.cjs");
|
|
32
|
+
const { appendActivity, journalCommand } = require("./activity-journal.cjs");
|
|
33
|
+
const { reserveReceipt, updateReceipt } = require("./playbook-receipts.cjs");
|
|
34
|
+
const {
|
|
35
|
+
activeRecord,
|
|
36
|
+
appendRecordEvent,
|
|
37
|
+
attachNetworkTrace,
|
|
38
|
+
discardRecord,
|
|
39
|
+
markRecord,
|
|
40
|
+
pauseRecord,
|
|
41
|
+
resumeRecord,
|
|
42
|
+
startRecord,
|
|
43
|
+
stopRecord,
|
|
44
|
+
updateRecordContext,
|
|
45
|
+
} = require("./playbook-records.cjs");
|
|
46
|
+
const { resolveArgs, runPlaybookOp } = require("./playbook-runtime.cjs");
|
|
47
|
+
const { resolveOp } = require("./playbooks.cjs");
|
|
48
|
+
const { commandMetadata, redactCommandArgs } = require("./workflow-definition.cjs");
|
|
49
|
+
const MAX_CLIENT_FRAME_BYTES = MAX_FRAME_BYTES;
|
|
50
|
+
const TEST_REQUEST_DEADLINE_MS = process.env.SURF_TEST_MODE === "1" && Number.isFinite(Number(process.env.SURF_TEST_REQUEST_DEADLINE_MS))
|
|
51
|
+
? Number(process.env.SURF_TEST_REQUEST_DEADLINE_MS)
|
|
52
|
+
: null;
|
|
19
53
|
if (IS_WIN) { try { fs.mkdirSync(SURF_TMP, { recursive: true }); } catch {} }
|
|
20
54
|
|
|
55
|
+
// The endpoint passed here is already validated by the caller. Keeping this
|
|
56
|
+
// lifecycle separate lets tests use an ephemeral loopback port without adding
|
|
57
|
+
// a localhost escape hatch to SURF_LISTEN parsing.
|
|
58
|
+
function createListenerLifecycle({ localPath, tcpEndpoint, handler, onReady, onFatal }) {
|
|
59
|
+
const localServer = net.createServer(handler);
|
|
60
|
+
const tcpServer = tcpEndpoint ? net.createServer(handler) : null;
|
|
61
|
+
let shuttingDown = false;
|
|
62
|
+
let startPromise = null;
|
|
63
|
+
const close = (server) => {
|
|
64
|
+
if (!server) return;
|
|
65
|
+
try { server.close(); } catch (error) {
|
|
66
|
+
if (error.code !== "ERR_SERVER_NOT_RUNNING") throw error;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const unlink = () => { if (!IS_WIN) { try { fs.unlinkSync(localPath); } catch {} } };
|
|
70
|
+
const listen = (server, options) => new Promise((resolve, reject) => {
|
|
71
|
+
server.once("error", reject);
|
|
72
|
+
server.listen(options, () => {
|
|
73
|
+
server.removeListener("error", reject);
|
|
74
|
+
if (shuttingDown) { close(server); unlink(); }
|
|
75
|
+
resolve();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
const start = () => {
|
|
79
|
+
if (startPromise) return startPromise;
|
|
80
|
+
startPromise = (async () => {
|
|
81
|
+
try {
|
|
82
|
+
await listen(localServer, localPath);
|
|
83
|
+
if (shuttingDown) return false;
|
|
84
|
+
if (!IS_WIN) { try { fs.chmodSync(localPath, 0o600); } catch {} }
|
|
85
|
+
if (tcpServer) {
|
|
86
|
+
await listen(tcpServer, tcpEndpoint);
|
|
87
|
+
if (shuttingDown) return false;
|
|
88
|
+
}
|
|
89
|
+
onReady();
|
|
90
|
+
return true;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (!shuttingDown) onFatal(error);
|
|
93
|
+
close(localServer); close(tcpServer); unlink();
|
|
94
|
+
return false;
|
|
95
|
+
} finally {
|
|
96
|
+
if (shuttingDown) { close(localServer); close(tcpServer); unlink(); }
|
|
97
|
+
}
|
|
98
|
+
})();
|
|
99
|
+
return startPromise;
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
localServer,
|
|
103
|
+
tcpServer,
|
|
104
|
+
start,
|
|
105
|
+
async shutdown() {
|
|
106
|
+
shuttingDown = true;
|
|
107
|
+
close(localServer); close(tcpServer); unlink();
|
|
108
|
+
if (startPromise) await startPromise;
|
|
109
|
+
unlink();
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
21
114
|
// Cross-platform image resize (macOS: sips, Linux: ImageMagick)
|
|
22
115
|
function resizeImage(filePath, maxSize) {
|
|
23
116
|
const platform = process.platform;
|
|
@@ -54,29 +147,9 @@ function resizeImage(filePath, maxSize) {
|
|
|
54
147
|
}
|
|
55
148
|
}
|
|
56
149
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
function queueAiRequest(handler) {
|
|
61
|
-
return new Promise((resolve, reject) => {
|
|
62
|
-
aiRequestQueue.push({ handler, resolve, reject });
|
|
63
|
-
processAiQueue();
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function processAiQueue() {
|
|
68
|
-
if (aiRequestInProgress || aiRequestQueue.length === 0) return;
|
|
69
|
-
aiRequestInProgress = true;
|
|
70
|
-
const { handler, resolve, reject } = aiRequestQueue.shift();
|
|
71
|
-
try {
|
|
72
|
-
const result = await handler();
|
|
73
|
-
resolve(result);
|
|
74
|
-
} catch (err) {
|
|
75
|
-
reject(err);
|
|
76
|
-
} finally {
|
|
77
|
-
aiRequestInProgress = false;
|
|
78
|
-
setTimeout(processAiQueue, 2000);
|
|
79
|
-
}
|
|
150
|
+
let aiQueue;
|
|
151
|
+
function queueAiRequest(handler, request = requestStorage.getStore()) {
|
|
152
|
+
return aiQueue.enqueue(handler, request);
|
|
80
153
|
}
|
|
81
154
|
const LOG_FILE = path.join(SURF_TMP, "surf-host.log");
|
|
82
155
|
const AUTH_FILE = path.join(os.homedir(), ".pi", "agent", "auth.json");
|
|
@@ -89,7 +162,8 @@ const DEFAULT_RETRY_OPTIONS = {
|
|
|
89
162
|
retryableStatusCodes: [429, 500, 502, 503, 504]
|
|
90
163
|
};
|
|
91
164
|
|
|
92
|
-
async function withRetry(fn, retryOptions = DEFAULT_RETRY_OPTIONS, retryCount = 0) {
|
|
165
|
+
async function withRetry(fn, retryOptions = DEFAULT_RETRY_OPTIONS, retryCount = 0, signal) {
|
|
166
|
+
throwIfAborted(signal);
|
|
93
167
|
try {
|
|
94
168
|
return await fn();
|
|
95
169
|
} catch (error) {
|
|
@@ -125,8 +199,8 @@ async function withRetry(fn, retryOptions = DEFAULT_RETRY_OPTIONS, retryCount =
|
|
|
125
199
|
const jitter = 0.8 + Math.random() * 0.4;
|
|
126
200
|
const delayWithJitter = Math.floor(delay * jitter);
|
|
127
201
|
|
|
128
|
-
await
|
|
129
|
-
return withRetry(fn, retryOptions, retryCount + 1);
|
|
202
|
+
await require("./abort.cjs").abortableDelay(delayWithJitter, signal);
|
|
203
|
+
return withRetry(fn, retryOptions, retryCount + 1, signal);
|
|
130
204
|
}
|
|
131
205
|
}
|
|
132
206
|
|
|
@@ -194,13 +268,14 @@ class GeminiClient {
|
|
|
194
268
|
|
|
195
269
|
async analyze(query, pageContext, options = {}) {
|
|
196
270
|
const mode = options.mode || detectQueryMode(query);
|
|
271
|
+
throwIfAborted(options.signal);
|
|
197
272
|
const promptFn = AI_PROMPTS[mode];
|
|
198
273
|
const prompt = promptFn(query, pageContext);
|
|
199
274
|
|
|
200
275
|
const result = await withRetry(async () => {
|
|
201
276
|
const response = await this.model.generateContent(prompt);
|
|
202
277
|
return response.response.text();
|
|
203
|
-
});
|
|
278
|
+
}, DEFAULT_RETRY_OPTIONS, 0, options.signal);
|
|
204
279
|
|
|
205
280
|
let content = result.trim();
|
|
206
281
|
|
|
@@ -291,57 +366,378 @@ const log = (msg) => {
|
|
|
291
366
|
fs.appendFileSync(LOG_FILE, `${new Date().toISOString()} ${msg}\n`);
|
|
292
367
|
};
|
|
293
368
|
|
|
369
|
+
if (require.main === module) {
|
|
294
370
|
log("Host starting...");
|
|
295
371
|
|
|
296
372
|
if (!IS_WIN) { try { fs.unlinkSync(SOCKET_PATH); } catch {} }
|
|
297
373
|
|
|
298
374
|
const pendingRequests = new Map();
|
|
299
|
-
const pendingToolRequests = new
|
|
375
|
+
const pendingToolRequests = new RequestPendingMap({ getRequest: () => requestStorage.getStore() });
|
|
300
376
|
const activeStreams = new Map();
|
|
377
|
+
const socketContexts = new WeakMap();
|
|
378
|
+
const socketWriters = new WeakMap();
|
|
301
379
|
let requestCounter = 0;
|
|
302
380
|
|
|
303
|
-
function
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
381
|
+
function auditSession(event) {
|
|
382
|
+
const context = event.context;
|
|
383
|
+
const principal = context?.principal;
|
|
384
|
+
const request = event.request;
|
|
385
|
+
log(`SESSION ${JSON.stringify({
|
|
386
|
+
event: event.event,
|
|
387
|
+
outcome: event.outcome,
|
|
388
|
+
principalId: principal?.clientId || "local",
|
|
389
|
+
principalLabel: principal?.label || "local",
|
|
390
|
+
peer: context?.socket?.remoteAddress || "local",
|
|
391
|
+
requestId: request?.id,
|
|
392
|
+
tool: request?.tool,
|
|
393
|
+
elapsedMs: event.elapsedMs,
|
|
394
|
+
})}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
aiQueue = new BoundedAiQueue({
|
|
398
|
+
maxQueued: 8,
|
|
399
|
+
audit: (event) => auditSession(event),
|
|
400
|
+
run: (handler, request) => request
|
|
401
|
+
? requestStorage.run(request, () => handler())
|
|
402
|
+
: handler(),
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
function sendSocket(socket, value, options = {}) {
|
|
406
|
+
const writer = socketWriters.get(socket);
|
|
407
|
+
return writer ? writer.send(value, options) : writeFrame(socket, value);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function sendOwnedExtensionMessage(request, message) {
|
|
411
|
+
const cleanupMessage = typeof message?.type === "string" && /(?:CLOSE_TAB|TAB_CLOSE)$/.test(message.type);
|
|
412
|
+
if (request?.hardBoundary) throwIfAborted(request.signal, "Request timed out");
|
|
413
|
+
if (!cleanupMessage) throwIfAborted(request?.signal, "Request cancelled");
|
|
414
|
+
writeMessage(message);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function requestCallExtension(request, tool, message, timeoutMs = 30000, cleanup = false) {
|
|
418
|
+
cleanup = cleanup || tool === "close_tab";
|
|
419
|
+
if (request?.hardBoundary) throwIfAborted(request.signal, "Request timed out");
|
|
420
|
+
if (!cleanup) throwIfAborted(request?.signal, "Request cancelled");
|
|
421
|
+
return new Promise((resolve, reject) => {
|
|
422
|
+
const id = ++requestCounter;
|
|
423
|
+
const timer = setTimeout(() => {
|
|
424
|
+
pendingToolRequests.expire(id, new Error(`Timeout waiting for extension: ${tool}`));
|
|
425
|
+
}, timeoutMs);
|
|
426
|
+
const pending = {
|
|
427
|
+
request,
|
|
428
|
+
cleanup,
|
|
429
|
+
tool,
|
|
430
|
+
resolve: (result) => {
|
|
431
|
+
clearTimeout(timer);
|
|
432
|
+
resolve(result);
|
|
433
|
+
},
|
|
434
|
+
reject: (error) => {
|
|
435
|
+
clearTimeout(timer);
|
|
436
|
+
reject(error);
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
pendingToolRequests.set(id, pending);
|
|
440
|
+
try {
|
|
441
|
+
if (cleanup) writeMessage({ ...message, id });
|
|
442
|
+
else sendOwnedExtensionMessage(request, { ...message, id });
|
|
443
|
+
} catch (error) {
|
|
444
|
+
pendingToolRequests.delete(id);
|
|
445
|
+
reject(error);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function executeMappedHostTool(request, tool, args, tabId) {
|
|
451
|
+
const extensionMsg = mapToolToMessage(tool, args, tabId);
|
|
452
|
+
if (!extensionMsg) throw new Error(`Unknown tool: ${tool}`);
|
|
453
|
+
if (extensionMsg.type === "UNSUPPORTED_ACTION") throw new Error(extensionMsg.message);
|
|
454
|
+
if (extensionMsg.type === "LOCAL_WAIT") {
|
|
455
|
+
await abortableDelay(extensionMsg.seconds * 1000, request.signal);
|
|
456
|
+
return { success: true };
|
|
310
457
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
458
|
+
if (extensionMsg.type === "BATCH_EXECUTE" || extensionMsg.type.endsWith("_QUERY")) {
|
|
459
|
+
throw new Error(`tool ${tool} is not available inside a host-owned workflow`);
|
|
460
|
+
}
|
|
461
|
+
return requestCallExtension(request, tool, extensionMsg, resolveRequestDeadlineMs(tool, args));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function executeNativePlaybook(request, handler, args, options = {}) {
|
|
465
|
+
if (handler !== "chatgpt.ask") throw new Error(`unknown native playbook handler: ${handler}`);
|
|
466
|
+
const result = await chatgptClient.query({
|
|
467
|
+
prompt: args.prompt,
|
|
468
|
+
signal: request.signal,
|
|
469
|
+
model: args.model,
|
|
470
|
+
timeout: args.timeout ? Number(args.timeout) * 1000 : undefined,
|
|
471
|
+
getCookies: () => requestCallExtension(request, "get_cookies", { type: "GET_CHATGPT_COOKIES" }),
|
|
472
|
+
createTab: () => requestCallExtension(request, "create_tab", { type: "CHATGPT_NEW_TAB" }),
|
|
473
|
+
closeTab: (tabId) => requestCallExtension(request, "close_tab", { type: "CHATGPT_CLOSE_TAB", tabId }, 45000, true),
|
|
474
|
+
cdpEvaluate: (tabId, expression) => requestCallExtension(request, "cdp_evaluate", { type: "CHATGPT_EVALUATE", tabId, expression }),
|
|
475
|
+
cdpCommand: (tabId, method, params) => requestCallExtension(request, "cdp_command", { type: "CHATGPT_CDP_COMMAND", tabId, method, params }),
|
|
476
|
+
beforeSubmit: options.markDispatched,
|
|
477
|
+
log: (message) => log(`[playbook:chatgpt] ${message}`),
|
|
478
|
+
});
|
|
479
|
+
return { response: result.response, model: result.model, tookMs: result.tookMs };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function runHostPlaybook(msg, request) {
|
|
483
|
+
const params = msg.params?.args || {};
|
|
484
|
+
const { playbook, op } = resolveOp(params.playbook, params.op, {
|
|
485
|
+
cwd: request.context?.isRemote ? process.cwd() : params.projectDir || process.cwd(),
|
|
486
|
+
pinBuiltIn: params.pinBuiltIn === true,
|
|
487
|
+
});
|
|
488
|
+
const runArgs = resolveArgs(op, params.args || {});
|
|
489
|
+
if (op.effect === "write" && op.safety.authorization === "explicit" && params.write !== true) {
|
|
490
|
+
throw new Error(`write op ${playbook.id} ${op.id} requires --write`);
|
|
491
|
+
}
|
|
492
|
+
const receipt = reserveReceipt({
|
|
493
|
+
playbookId: playbook.id,
|
|
494
|
+
op,
|
|
495
|
+
args: runArgs,
|
|
496
|
+
repeat: params.repeat === true,
|
|
497
|
+
retryAttempt: params.retryAttempt,
|
|
498
|
+
overrideInDoubt: params.overrideInDoubt === true,
|
|
499
|
+
});
|
|
500
|
+
const report = (event) => {
|
|
501
|
+
appendActivity(event);
|
|
502
|
+
appendRecordEvent(event);
|
|
503
|
+
};
|
|
504
|
+
return runPlaybookOp({
|
|
505
|
+
playbook,
|
|
506
|
+
op,
|
|
507
|
+
args: runArgs,
|
|
508
|
+
attemptId: receipt?.attemptId,
|
|
509
|
+
signal: request.signal,
|
|
510
|
+
executeTool: (tool, args) => executeMappedHostTool(request, tool, args, msg.tabId),
|
|
511
|
+
executeNative: (handler, args, options) => executeNativePlaybook(request, handler, args, options),
|
|
512
|
+
sleep: (ms) => abortableDelay(ms, request.signal),
|
|
513
|
+
onEvent: report,
|
|
514
|
+
beforeDispatch: async () => updateReceipt(receipt, "dispatched"),
|
|
515
|
+
afterDispatch: async ({ status, error }) => updateReceipt(receipt, status, { error }),
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function handleRecordRequest(tool, args, msg, request) {
|
|
520
|
+
if (tool === "playbook.record.start") {
|
|
521
|
+
let record = startRecord({ ...args, tabId: msg.tabId });
|
|
522
|
+
try {
|
|
523
|
+
const context = await requestCallExtension(request, tool, {
|
|
524
|
+
type: "GET_PLAYBOOK_RECORD_CONTEXT",
|
|
525
|
+
tabId: msg.tabId,
|
|
526
|
+
});
|
|
527
|
+
record = updateRecordContext({
|
|
528
|
+
tabId: context._resolvedTabId || msg.tabId,
|
|
529
|
+
origin: context.origin,
|
|
530
|
+
});
|
|
531
|
+
if (record.capture.network) {
|
|
532
|
+
const result = await requestCallExtension(request, tool, { type: "START_NETWORK_CAPTURE", tabId: record.tabId, bodyMode: "text" });
|
|
533
|
+
record = updateRecordContext({ tabId: result._resolvedTabId || record.tabId });
|
|
534
|
+
}
|
|
535
|
+
if (record.capture.watch) {
|
|
536
|
+
const result = await requestCallExtension(request, tool, { type: "START_PLAYBOOK_WATCH", tabId: record.tabId || msg.tabId, includeInputValues: record.redaction.includeInputValues });
|
|
537
|
+
record = updateRecordContext({ tabId: result._resolvedTabId || record.tabId || msg.tabId });
|
|
538
|
+
}
|
|
539
|
+
return record;
|
|
540
|
+
} catch (error) {
|
|
541
|
+
if (record?.capture.network) await requestCallExtension(request, tool, { type: "STOP_NETWORK_CAPTURE", tabId: record.tabId }, 30000, true).catch(() => {});
|
|
542
|
+
if (record?.capture.watch) await requestCallExtension(request, tool, { type: "STOP_PLAYBOOK_WATCH", tabId: record.tabId }, 30000, true).catch(() => {});
|
|
543
|
+
discardRecord();
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (tool === "playbook.record.status") return activeRecord() || { status: "idle" };
|
|
548
|
+
if (tool === "playbook.record.mark") return markRecord(args.label);
|
|
549
|
+
if (tool === "playbook.record.pause") return pauseRecord();
|
|
550
|
+
if (tool === "playbook.record.resume") return resumeRecord();
|
|
551
|
+
if (tool === "playbook.record.discard") {
|
|
552
|
+
const record = activeRecord();
|
|
553
|
+
if (record?.capture.network) await requestCallExtension(request, tool, { type: "STOP_NETWORK_CAPTURE", tabId: record.tabId }, 30000, true).catch(() => {});
|
|
554
|
+
if (record?.capture.watch) await requestCallExtension(request, tool, { type: "STOP_PLAYBOOK_WATCH", tabId: record.tabId }, 30000, true).catch(() => {});
|
|
555
|
+
return discardRecord();
|
|
556
|
+
}
|
|
557
|
+
if (tool === "playbook.record.stop") {
|
|
558
|
+
const record = activeRecord();
|
|
559
|
+
if (!record) throw new Error("no active playbook record");
|
|
560
|
+
if (record.capture.network) {
|
|
561
|
+
try {
|
|
562
|
+
const result = await requestCallExtension(request, tool, { type: "READ_NETWORK_REQUESTS", tabId: record.tabId, full: true, limit: 500 });
|
|
563
|
+
const cutoff = Date.parse(record.startedAt);
|
|
564
|
+
attachNetworkTrace(record.id, (result.entries || []).filter((entry) => entry.ts >= cutoff));
|
|
565
|
+
} finally {
|
|
566
|
+
await requestCallExtension(request, tool, { type: "STOP_NETWORK_CAPTURE", tabId: record.tabId }, 30000, true).catch(() => {});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (record.capture.watch) await requestCallExtension(request, tool, { type: "STOP_PLAYBOOK_WATCH", tabId: record.tabId }, 30000, true).catch(() => {});
|
|
570
|
+
return stopRecord({ draft: args.draft === true });
|
|
571
|
+
}
|
|
572
|
+
throw new Error(`Unknown record command: ${tool}`);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const sessionManager = new HostSessionManager({
|
|
576
|
+
audit: auditSession,
|
|
577
|
+
onTimeout(context, request) {
|
|
578
|
+
pendingToolRequests.hardDeadline(request);
|
|
579
|
+
const response = {
|
|
580
|
+
type: "tool_response",
|
|
581
|
+
id: request.id,
|
|
582
|
+
error: { content: [{ type: "text", text: "Request timed out" }] },
|
|
583
|
+
};
|
|
584
|
+
cleanupRequestTransfers(request)
|
|
585
|
+
.then(() => {
|
|
586
|
+
sessionManager.complete(context, request.id, "hard-timeout");
|
|
587
|
+
if (!context.closed) return sendSocket(context.socket, response);
|
|
588
|
+
})
|
|
589
|
+
.catch((error) => log(`Error settling timed-out request: ${error.message}`));
|
|
590
|
+
},
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
async function discardRequestTransfers(message, state) {
|
|
594
|
+
if (!state) return;
|
|
595
|
+
const ids = [];
|
|
596
|
+
for (const entry of message?._surfTransfers?.uploads || []) if (typeof entry?.transferId === "string") ids.push(entry.transferId);
|
|
597
|
+
for (const entry of message?._surfTransfers?.downloads || []) if (typeof entry?.transferId === "string") ids.push(entry.transferId);
|
|
598
|
+
await Promise.all([...new Set(ids)].map((id) => state.discardCompleted(id)));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function applyRequestTransfers(msg, request, transferState, getTransferState) {
|
|
602
|
+
if (!request.context?.isRemote) return;
|
|
603
|
+
const materialized = await materializeRemoteTool({
|
|
604
|
+
tool: request.tool,
|
|
605
|
+
args: msg.params?.args || {},
|
|
606
|
+
metadata: msg._surfTransfers,
|
|
607
|
+
pathRefs: msg._surfPaths || [],
|
|
608
|
+
transferState,
|
|
609
|
+
getTransferState,
|
|
610
|
+
});
|
|
611
|
+
request.transferState = materialized.transferState;
|
|
612
|
+
request.outputTransfers = materialized.outputTransfers;
|
|
613
|
+
request.pathRewrites = materialized.pathRewrites;
|
|
614
|
+
request.transferCleanup = materialized.transferCleanup;
|
|
615
|
+
msg.params = { ...msg.params, args: materialized.args };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function cleanupRequestTransfers(request) {
|
|
619
|
+
if (!request || request.transferCleanupStarted) return request?.transferCleanupPromise;
|
|
620
|
+
request.transferCleanupStarted = true;
|
|
621
|
+
const paths = request.transferCleanup || [];
|
|
622
|
+
request.transferCleanup = [];
|
|
623
|
+
request.transferCleanupPromise = cleanupFilePaths(paths);
|
|
624
|
+
await request.transferCleanupPromise;
|
|
625
|
+
return request.transferCleanupPromise;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function completeOwnedRequest(context, id, outcome) {
|
|
629
|
+
const request = context?.activeRequest;
|
|
630
|
+
if (!request || request.id !== id) return Promise.resolve();
|
|
631
|
+
if (request.pendingEntries?.size && !request.hardBoundary) {
|
|
632
|
+
if (request.completionRequested) return request.completionPromise;
|
|
633
|
+
request.completionRequested = true;
|
|
634
|
+
request.completionOutcome = outcome;
|
|
635
|
+
request.completionPromise = new Promise((resolve) => {
|
|
636
|
+
pendingToolRequests.onDrain(request, () => {
|
|
637
|
+
sessionManager.complete(context, id, request.completionOutcome);
|
|
638
|
+
resolve();
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
return request.completionPromise;
|
|
316
642
|
}
|
|
643
|
+
sessionManager.complete(context, id, outcome);
|
|
644
|
+
return Promise.resolve();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async function sendRequestDownloads(context, request, result) {
|
|
648
|
+
if (!request) return result;
|
|
649
|
+
let rewritten = result;
|
|
650
|
+
for (const output of request.outputTransfers || []) {
|
|
651
|
+
await streamFileDownload({
|
|
652
|
+
writer: { send: (frame) => sendSocket(context.socket, frame) },
|
|
653
|
+
state: request.transferState,
|
|
654
|
+
filePath: output.path,
|
|
655
|
+
transferId: output.transferId,
|
|
656
|
+
original: output.original,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
rewritten = rewriteTransferPaths(rewritten, request.pathRewrites || []);
|
|
660
|
+
return rewritten;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function sendToolResponse(socket, id, result, error) {
|
|
664
|
+
const context = socketContexts.get(socket);
|
|
665
|
+
if (context && !sessionManager.canRespond(context, id)) return;
|
|
666
|
+
const request = context?.activeRequest;
|
|
667
|
+
let finalError = error;
|
|
668
|
+
(async () => {
|
|
669
|
+
let output = result;
|
|
670
|
+
try {
|
|
671
|
+
if (!error) output = await sendRequestDownloads(context, request, result);
|
|
672
|
+
} catch (transferFailure) {
|
|
673
|
+
finalError = transferFailure.message;
|
|
674
|
+
}
|
|
675
|
+
if (finalError && request) {
|
|
676
|
+
finalError = rewriteTransferPaths(finalError, request.pathRewrites || []);
|
|
677
|
+
}
|
|
678
|
+
if (request?.tool && !request.tool.startsWith("playbook.")) {
|
|
679
|
+
const metadata = commandMetadata(request.tool);
|
|
680
|
+
if (metadata.recordable) {
|
|
681
|
+
const event = {
|
|
682
|
+
type: finalError ? "tool.failed" : "tool.completed",
|
|
683
|
+
command: metadata.name,
|
|
684
|
+
argsRedacted: redactCommandArgs(request.tool, request.args || {}),
|
|
685
|
+
startedAt: request.activityStartedAt || new Date().toISOString(),
|
|
686
|
+
endedAt: new Date().toISOString(),
|
|
687
|
+
resultSummary: finalError ? "failed" : "success",
|
|
688
|
+
};
|
|
689
|
+
appendActivity(event);
|
|
690
|
+
appendRecordEvent(event);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
await cleanupRequestTransfers(request);
|
|
694
|
+
if (request?.settled) return;
|
|
695
|
+
const outcome = request?.signal.aborted
|
|
696
|
+
? (request.tombstoned ? "cleanup-settled" : "cancelled")
|
|
697
|
+
: finalError ? "error" : "completed";
|
|
698
|
+
await completeOwnedRequest(context, id, outcome);
|
|
699
|
+
const response = { type: "tool_response", id };
|
|
700
|
+
if (finalError) response.error = { content: [{ type: "text", text: finalError }] };
|
|
701
|
+
else response.result = { content: formatToolContent(output, log, { suppressImages: Boolean(context?.isRemote) }) };
|
|
702
|
+
if (!context?.closed) await sendSocket(socket, response);
|
|
703
|
+
})().catch((sendError) => log(`Error sending tool_response: ${sendError.message}`));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function stopActiveStream(streamId, { notifyExtension = true } = {}) {
|
|
707
|
+
const stream = activeStreams.get(streamId);
|
|
708
|
+
if (!stream) return;
|
|
709
|
+
activeStreams.delete(streamId);
|
|
710
|
+
sessionManager.stopStream(socketContexts.get(stream.socket));
|
|
711
|
+
if (notifyExtension) writeMessage({ type: "STREAM_STOP", streamId });
|
|
317
712
|
}
|
|
318
713
|
|
|
319
714
|
function handleStreamRequest(msg, socket) {
|
|
320
715
|
const { streamType, options, id: originalId } = msg;
|
|
321
716
|
const tabId = msg.tabId;
|
|
322
717
|
const streamId = ++requestCounter;
|
|
323
|
-
|
|
718
|
+
|
|
324
719
|
activeStreams.set(streamId, {
|
|
325
720
|
socket,
|
|
326
721
|
originalId,
|
|
327
722
|
streamType,
|
|
328
723
|
});
|
|
329
|
-
|
|
724
|
+
|
|
330
725
|
writeMessage({
|
|
331
726
|
type: streamType,
|
|
332
727
|
streamId,
|
|
333
728
|
options: options || {},
|
|
334
729
|
tabId,
|
|
335
730
|
});
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
731
|
+
|
|
732
|
+
sendSocket(socket, { type: "stream_started", streamId }, { stream: true }).catch((error) => {
|
|
733
|
+
log(`Error sending stream_started: ${error.message}`);
|
|
734
|
+
stopActiveStream(streamId);
|
|
735
|
+
socket.destroy(error);
|
|
736
|
+
});
|
|
342
737
|
}
|
|
343
738
|
|
|
344
|
-
function handleToolRequest(msg, socket) {
|
|
739
|
+
function handleToolRequest(msg, socket, requestContext = requestStorage.getStore()) {
|
|
740
|
+
const writeMessage = (message) => sendOwnedExtensionMessage(requestContext, message);
|
|
345
741
|
const { method, params } = msg;
|
|
346
742
|
const originalId = msg.id || null;
|
|
347
743
|
|
|
@@ -370,6 +766,22 @@ function handleToolRequest(msg, socket) {
|
|
|
370
766
|
sendToolResponse(socket, originalId, null, "No tool specified");
|
|
371
767
|
return;
|
|
372
768
|
}
|
|
769
|
+
|
|
770
|
+
requestContext.args = args || {};
|
|
771
|
+
requestContext.activityStartedAt = new Date().toISOString();
|
|
772
|
+
if (!tool.startsWith("playbook.")) journalCommand(tool, args || {}, { tabId });
|
|
773
|
+
if (tool === "playbook.run") {
|
|
774
|
+
runHostPlaybook(msg, requestContext)
|
|
775
|
+
.then((result) => sendToolResponse(socket, originalId, { output: JSON.stringify(result) }, null))
|
|
776
|
+
.catch((error) => sendToolResponse(socket, originalId, null, error.message));
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (tool.startsWith("playbook.record.")) {
|
|
780
|
+
handleRecordRequest(tool, args || {}, msg, requestContext)
|
|
781
|
+
.then((result) => sendToolResponse(socket, originalId, result, null))
|
|
782
|
+
.catch((error) => sendToolResponse(socket, originalId, null, error.message));
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
373
785
|
|
|
374
786
|
const extensionMsg = mapToolToMessage(tool, args, tabId);
|
|
375
787
|
if (!extensionMsg) {
|
|
@@ -383,14 +795,14 @@ function handleToolRequest(msg, socket) {
|
|
|
383
795
|
}
|
|
384
796
|
|
|
385
797
|
if (extensionMsg.type === "LOCAL_WAIT") {
|
|
386
|
-
|
|
387
|
-
sendToolResponse(socket, originalId, { success: true }, null)
|
|
388
|
-
|
|
798
|
+
require("./abort.cjs").abortableDelay(extensionMsg.seconds * 1000, requestContext.signal)
|
|
799
|
+
.then(() => sendToolResponse(socket, originalId, { success: true }, null))
|
|
800
|
+
.catch((error) => sendToolResponse(socket, originalId, null, error.message));
|
|
389
801
|
return;
|
|
390
802
|
}
|
|
391
803
|
|
|
392
804
|
if (extensionMsg.type === "BATCH_EXECUTE") {
|
|
393
|
-
executeBatch(extensionMsg.actions, extensionMsg.tabId, socket, originalId);
|
|
805
|
+
executeBatch(extensionMsg.actions, extensionMsg.tabId, socket, originalId, requestContext);
|
|
394
806
|
return;
|
|
395
807
|
}
|
|
396
808
|
|
|
@@ -406,46 +818,23 @@ function handleToolRequest(msg, socket) {
|
|
|
406
818
|
return;
|
|
407
819
|
}
|
|
408
820
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
try {
|
|
427
|
-
const gemini = getGeminiClient(apiKey);
|
|
428
|
-
const result = await gemini.analyze(extensionMsg.query, pageContent, { mode: extensionMsg.mode });
|
|
429
|
-
|
|
430
|
-
if (result.mode === "find") {
|
|
431
|
-
sendToolResponse(socket, originalId, {
|
|
432
|
-
ref: result.content === "NOT_FOUND" ? null : result.content,
|
|
433
|
-
mode: result.mode,
|
|
434
|
-
aiResult: true
|
|
435
|
-
}, null);
|
|
436
|
-
} else {
|
|
437
|
-
sendToolResponse(socket, originalId, {
|
|
438
|
-
content: result.content,
|
|
439
|
-
mode: result.mode,
|
|
440
|
-
aiResult: true
|
|
441
|
-
}, null);
|
|
442
|
-
}
|
|
443
|
-
} catch (err) {
|
|
444
|
-
sendToolResponse(socket, originalId, null, `AI analysis failed: ${err.message}`);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
821
|
+
requestCallExtension(
|
|
822
|
+
requestContext,
|
|
823
|
+
"read_page",
|
|
824
|
+
{ type: "READ_PAGE", options: { filter: "interactive" }, tabId: extensionMsg.tabId },
|
|
825
|
+
45000,
|
|
826
|
+
).then(async (pageResult) => {
|
|
827
|
+
if (pageResult.error) throw new Error(`Failed to read page: ${pageResult.error}`);
|
|
828
|
+
const pageContent = pageResult.pageContent || "";
|
|
829
|
+
if (!pageContent) throw new Error("No page content available");
|
|
830
|
+
const gemini = getGeminiClient(apiKey);
|
|
831
|
+
const result = await gemini.analyze(extensionMsg.query, pageContent, { mode: extensionMsg.mode, signal: requestContext.signal });
|
|
832
|
+
return result.mode === "find"
|
|
833
|
+
? { ref: result.content === "NOT_FOUND" ? null : result.content, mode: result.mode, aiResult: true }
|
|
834
|
+
: { content: result.content, mode: result.mode, aiResult: true };
|
|
835
|
+
}).then((result) => sendToolResponse(socket, originalId, result, null)).catch((err) => {
|
|
836
|
+
sendToolResponse(socket, originalId, null, err.message);
|
|
447
837
|
});
|
|
448
|
-
writeMessage({ type: "READ_PAGE", options: { filter: "interactive" }, tabId: extensionMsg.tabId, id: pageRequestId });
|
|
449
838
|
return;
|
|
450
839
|
}
|
|
451
840
|
|
|
@@ -455,16 +844,12 @@ function handleToolRequest(msg, socket) {
|
|
|
455
844
|
queueAiRequest(async () => {
|
|
456
845
|
let pageContext = null;
|
|
457
846
|
if (withPage) {
|
|
458
|
-
const pageResult = await
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
onComplete: resolve
|
|
465
|
-
});
|
|
466
|
-
writeMessage({ type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId, id: pageId });
|
|
467
|
-
});
|
|
847
|
+
const pageResult = await requestCallExtension(
|
|
848
|
+
requestContext,
|
|
849
|
+
"read_page",
|
|
850
|
+
{ type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId },
|
|
851
|
+
45000,
|
|
852
|
+
);
|
|
468
853
|
if (pageResult && !pageResult.error) {
|
|
469
854
|
pageContext = {
|
|
470
855
|
url: pageResult.url,
|
|
@@ -480,69 +865,36 @@ function handleToolRequest(msg, socket) {
|
|
|
480
865
|
|
|
481
866
|
const result = await chatgptClient.query({
|
|
482
867
|
prompt: fullPrompt,
|
|
868
|
+
signal: requestContext.signal,
|
|
483
869
|
model,
|
|
484
870
|
file,
|
|
485
871
|
timeout,
|
|
486
|
-
getCookies: () =>
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
onComplete: (r) => resolve(r)
|
|
513
|
-
});
|
|
514
|
-
writeMessage({ type: "CHATGPT_CLOSE_TAB", tabId: tabIdToClose, id: tabCloseId });
|
|
515
|
-
}),
|
|
516
|
-
cdpEvaluate: (tabId, expression) => new Promise((resolve) => {
|
|
517
|
-
const evalId = ++requestCounter;
|
|
518
|
-
pendingToolRequests.set(evalId, {
|
|
519
|
-
socket: null,
|
|
520
|
-
originalId: null,
|
|
521
|
-
tool: "cdp_evaluate",
|
|
522
|
-
onComplete: (r) => resolve(r)
|
|
523
|
-
});
|
|
524
|
-
writeMessage({ type: "CHATGPT_EVALUATE", tabId, expression, id: evalId });
|
|
525
|
-
}),
|
|
526
|
-
cdpCommand: (tabId, method, params) => new Promise((resolve) => {
|
|
527
|
-
const cmdId = ++requestCounter;
|
|
528
|
-
pendingToolRequests.set(cmdId, {
|
|
529
|
-
socket: null,
|
|
530
|
-
originalId: null,
|
|
531
|
-
tool: "cdp_command",
|
|
532
|
-
onComplete: (r) => resolve(r)
|
|
533
|
-
});
|
|
534
|
-
writeMessage({ type: "CHATGPT_CDP_COMMAND", tabId, method, params, id: cmdId });
|
|
535
|
-
}),
|
|
536
|
-
uploadFile: (tabId, filePaths) => new Promise((resolve) => {
|
|
537
|
-
const uploadId = ++requestCounter;
|
|
538
|
-
pendingToolRequests.set(uploadId, {
|
|
539
|
-
socket: null,
|
|
540
|
-
originalId: null,
|
|
541
|
-
tool: "upload_file",
|
|
542
|
-
onComplete: (r) => resolve(r)
|
|
543
|
-
});
|
|
544
|
-
writeMessage(buildProviderUploadMessage("chatgpt", tabId, filePaths, uploadId));
|
|
545
|
-
}),
|
|
872
|
+
getCookies: () => requestCallExtension(
|
|
873
|
+
requestContext,
|
|
874
|
+
"get_cookies",
|
|
875
|
+
{ type: "GET_CHATGPT_COOKIES" },
|
|
876
|
+
),
|
|
877
|
+
createTab: () => requestCallExtension(
|
|
878
|
+
requestContext,
|
|
879
|
+
"create_tab",
|
|
880
|
+
{ type: "CHATGPT_NEW_TAB" },
|
|
881
|
+
),
|
|
882
|
+
closeTab: (tabIdToClose) => requestCallExtension(requestContext, "close_tab", { type: "CHATGPT_CLOSE_TAB", tabId: tabIdToClose }, 45000, true),
|
|
883
|
+
cdpEvaluate: (tabId, expression) => requestCallExtension(
|
|
884
|
+
requestContext,
|
|
885
|
+
"cdp_evaluate",
|
|
886
|
+
{ type: "CHATGPT_EVALUATE", tabId, expression },
|
|
887
|
+
),
|
|
888
|
+
cdpCommand: (tabId, method, params) => requestCallExtension(
|
|
889
|
+
requestContext,
|
|
890
|
+
"cdp_command",
|
|
891
|
+
{ type: "CHATGPT_CDP_COMMAND", tabId, method, params },
|
|
892
|
+
),
|
|
893
|
+
uploadFile: (tabId, filePaths) => requestCallExtension(
|
|
894
|
+
requestContext,
|
|
895
|
+
"upload_file",
|
|
896
|
+
buildProviderUploadMessage("chatgpt", tabId, filePaths),
|
|
897
|
+
),
|
|
546
898
|
log: (msg) => log(`[chatgpt] ${msg}`)
|
|
547
899
|
});
|
|
548
900
|
|
|
@@ -566,16 +918,12 @@ function handleToolRequest(msg, socket) {
|
|
|
566
918
|
queueAiRequest(async () => {
|
|
567
919
|
let pageContext = null;
|
|
568
920
|
if (withPage) {
|
|
569
|
-
const pageResult = await
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
onComplete: resolve
|
|
576
|
-
});
|
|
577
|
-
writeMessage({ type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId, id: pageId });
|
|
578
|
-
});
|
|
921
|
+
const pageResult = await requestCallExtension(
|
|
922
|
+
requestContext,
|
|
923
|
+
"read_page",
|
|
924
|
+
{ type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId },
|
|
925
|
+
45000,
|
|
926
|
+
);
|
|
579
927
|
if (pageResult && !pageResult.error) {
|
|
580
928
|
pageContext = {
|
|
581
929
|
url: pageResult.url,
|
|
@@ -591,49 +939,26 @@ function handleToolRequest(msg, socket) {
|
|
|
591
939
|
|
|
592
940
|
const result = await perplexityClient.query({
|
|
593
941
|
prompt: fullPrompt,
|
|
942
|
+
signal: requestContext.signal,
|
|
594
943
|
mode: mode || 'search',
|
|
595
944
|
model,
|
|
596
945
|
timeout: timeout || 120000,
|
|
597
|
-
createTab: () =>
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
onComplete: (r) => resolve(r)
|
|
614
|
-
});
|
|
615
|
-
writeMessage({ type: "PERPLEXITY_CLOSE_TAB", tabId: tabIdToClose, id: tabCloseId });
|
|
616
|
-
}),
|
|
617
|
-
cdpEvaluate: (tabId, expression) => new Promise((resolve) => {
|
|
618
|
-
const evalId = ++requestCounter;
|
|
619
|
-
pendingToolRequests.set(evalId, {
|
|
620
|
-
socket: null,
|
|
621
|
-
originalId: null,
|
|
622
|
-
tool: "cdp_evaluate",
|
|
623
|
-
onComplete: (r) => resolve(r)
|
|
624
|
-
});
|
|
625
|
-
writeMessage({ type: "PERPLEXITY_EVALUATE", tabId, expression, id: evalId });
|
|
626
|
-
}),
|
|
627
|
-
cdpCommand: (tabId, method, params) => new Promise((resolve) => {
|
|
628
|
-
const cmdId = ++requestCounter;
|
|
629
|
-
pendingToolRequests.set(cmdId, {
|
|
630
|
-
socket: null,
|
|
631
|
-
originalId: null,
|
|
632
|
-
tool: "cdp_command",
|
|
633
|
-
onComplete: (r) => resolve(r)
|
|
634
|
-
});
|
|
635
|
-
writeMessage({ type: "PERPLEXITY_CDP_COMMAND", tabId, method, params, id: cmdId });
|
|
636
|
-
}),
|
|
946
|
+
createTab: () => requestCallExtension(
|
|
947
|
+
requestContext,
|
|
948
|
+
"create_tab",
|
|
949
|
+
{ type: "PERPLEXITY_NEW_TAB" },
|
|
950
|
+
),
|
|
951
|
+
closeTab: (tabIdToClose) => requestCallExtension(requestContext, "close_tab", { type: "PERPLEXITY_CLOSE_TAB", tabId: tabIdToClose }, 45000, true),
|
|
952
|
+
cdpEvaluate: (tabId, expression) => requestCallExtension(
|
|
953
|
+
requestContext,
|
|
954
|
+
"cdp_evaluate",
|
|
955
|
+
{ type: "PERPLEXITY_EVALUATE", tabId, expression },
|
|
956
|
+
),
|
|
957
|
+
cdpCommand: (tabId, method, params) => requestCallExtension(
|
|
958
|
+
requestContext,
|
|
959
|
+
"cdp_command",
|
|
960
|
+
{ type: "PERPLEXITY_CDP_COMMAND", tabId, method, params },
|
|
961
|
+
),
|
|
637
962
|
log: (msg) => log(`[perplexity] ${msg}`)
|
|
638
963
|
});
|
|
639
964
|
|
|
@@ -661,16 +986,12 @@ function handleToolRequest(msg, socket) {
|
|
|
661
986
|
// 1. Get page context if requested
|
|
662
987
|
let pageContext = null;
|
|
663
988
|
if (withPage) {
|
|
664
|
-
const pageResult = await
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
onComplete: resolve
|
|
671
|
-
});
|
|
672
|
-
writeMessage({ type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId, id: pageId });
|
|
673
|
-
});
|
|
989
|
+
const pageResult = await requestCallExtension(
|
|
990
|
+
requestContext,
|
|
991
|
+
"get_page_text",
|
|
992
|
+
{ type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId },
|
|
993
|
+
45000,
|
|
994
|
+
);
|
|
674
995
|
if (pageResult && !pageResult.error) {
|
|
675
996
|
pageContext = {
|
|
676
997
|
url: pageResult.url,
|
|
@@ -688,7 +1009,8 @@ function handleToolRequest(msg, socket) {
|
|
|
688
1009
|
// 3. Call Gemini client
|
|
689
1010
|
const result = await geminiClient.query({
|
|
690
1011
|
prompt: fullPrompt,
|
|
691
|
-
|
|
1012
|
+
signal: requestContext.signal,
|
|
1013
|
+
model: model || "gemini-3.1-pro",
|
|
692
1014
|
file,
|
|
693
1015
|
generateImage,
|
|
694
1016
|
editImage,
|
|
@@ -696,67 +1018,32 @@ function handleToolRequest(msg, socket) {
|
|
|
696
1018
|
youtube,
|
|
697
1019
|
aspectRatio,
|
|
698
1020
|
timeout: timeout || 300000,
|
|
699
|
-
getCookies: () =>
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
onComplete: (r) => resolve(r)
|
|
726
|
-
});
|
|
727
|
-
writeMessage({ type: "GEMINI_CLOSE_TAB", tabId: tabIdToClose, id: tabCloseId });
|
|
728
|
-
}),
|
|
729
|
-
jsEval: (tabId, code) => new Promise((resolve) => {
|
|
730
|
-
const jsId = ++requestCounter;
|
|
731
|
-
pendingToolRequests.set(jsId, {
|
|
732
|
-
socket: null,
|
|
733
|
-
originalId: null,
|
|
734
|
-
tool: "js_eval",
|
|
735
|
-
onComplete: (r) => resolve(r)
|
|
736
|
-
});
|
|
737
|
-
log(`[gemini] Sending EXECUTE_JAVASCRIPT id=${jsId} tabId=${tabId} code=${code.length} chars`);
|
|
738
|
-
writeMessage({ type: "EXECUTE_JAVASCRIPT", tabId, code, id: jsId });
|
|
739
|
-
}),
|
|
740
|
-
uploadFile: (tabId, filePaths) => new Promise((resolve) => {
|
|
741
|
-
const uploadId = ++requestCounter;
|
|
742
|
-
pendingToolRequests.set(uploadId, {
|
|
743
|
-
socket: null,
|
|
744
|
-
originalId: null,
|
|
745
|
-
tool: "upload_file",
|
|
746
|
-
onComplete: (r) => resolve(r)
|
|
747
|
-
});
|
|
748
|
-
writeMessage(buildProviderUploadMessage("gemini", tabId, filePaths, uploadId));
|
|
749
|
-
}),
|
|
750
|
-
fetchUrl: (url) => new Promise((resolve) => {
|
|
751
|
-
const fetchId = ++requestCounter;
|
|
752
|
-
pendingToolRequests.set(fetchId, {
|
|
753
|
-
socket: null,
|
|
754
|
-
originalId: null,
|
|
755
|
-
tool: "fetch_url",
|
|
756
|
-
onComplete: (r) => resolve(r)
|
|
757
|
-
});
|
|
758
|
-
writeMessage({ type: "GEMINI_FETCH_URL", url, id: fetchId });
|
|
759
|
-
}),
|
|
1021
|
+
getCookies: () => requestCallExtension(
|
|
1022
|
+
requestContext,
|
|
1023
|
+
"get_cookies",
|
|
1024
|
+
{ type: "GET_GOOGLE_COOKIES" },
|
|
1025
|
+
),
|
|
1026
|
+
createTab: () => requestCallExtension(
|
|
1027
|
+
requestContext,
|
|
1028
|
+
"create_tab",
|
|
1029
|
+
{ type: "GEMINI_NEW_TAB" },
|
|
1030
|
+
),
|
|
1031
|
+
closeTab: (tabIdToClose) => requestCallExtension(requestContext, "close_tab", { type: "GEMINI_CLOSE_TAB", tabId: tabIdToClose }, 45000, true),
|
|
1032
|
+
jsEval: (tabId, code) => requestCallExtension(
|
|
1033
|
+
requestContext,
|
|
1034
|
+
"js_eval",
|
|
1035
|
+
{ type: "EXECUTE_JAVASCRIPT", tabId, code },
|
|
1036
|
+
),
|
|
1037
|
+
uploadFile: (tabId, filePaths) => requestCallExtension(
|
|
1038
|
+
requestContext,
|
|
1039
|
+
"upload_file",
|
|
1040
|
+
buildProviderUploadMessage("gemini", tabId, filePaths),
|
|
1041
|
+
),
|
|
1042
|
+
fetchUrl: (url) => requestCallExtension(
|
|
1043
|
+
requestContext,
|
|
1044
|
+
"fetch_url",
|
|
1045
|
+
{ type: "GEMINI_FETCH_URL", url },
|
|
1046
|
+
),
|
|
760
1047
|
log: (msg) => log(`[gemini] ${msg}`)
|
|
761
1048
|
});
|
|
762
1049
|
|
|
@@ -785,16 +1072,12 @@ function handleToolRequest(msg, socket) {
|
|
|
785
1072
|
// 1. Get page context if requested
|
|
786
1073
|
let pageContext = null;
|
|
787
1074
|
if (withPage) {
|
|
788
|
-
const pageResult = await
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
onComplete: resolve
|
|
795
|
-
});
|
|
796
|
-
writeMessage({ type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId, id: pageId });
|
|
797
|
-
});
|
|
1075
|
+
const pageResult = await requestCallExtension(
|
|
1076
|
+
requestContext,
|
|
1077
|
+
"get_page_text",
|
|
1078
|
+
{ type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId },
|
|
1079
|
+
45000,
|
|
1080
|
+
);
|
|
798
1081
|
if (pageResult && !pageResult.error) {
|
|
799
1082
|
pageContext = {
|
|
800
1083
|
url: pageResult.url,
|
|
@@ -812,59 +1095,31 @@ function handleToolRequest(msg, socket) {
|
|
|
812
1095
|
// 3. Call Grok client
|
|
813
1096
|
const result = await grokClient.query({
|
|
814
1097
|
prompt: fullPrompt,
|
|
1098
|
+
signal: requestContext.signal,
|
|
815
1099
|
model: model,
|
|
816
1100
|
deepSearch: deepSearch || false,
|
|
817
1101
|
timeout: timeout || 300000,
|
|
818
|
-
getCookies: () =>
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
const tabCloseId = ++requestCounter;
|
|
840
|
-
pendingToolRequests.set(tabCloseId, {
|
|
841
|
-
socket: null,
|
|
842
|
-
originalId: null,
|
|
843
|
-
tool: "close_tab",
|
|
844
|
-
onComplete: (r) => resolve(r)
|
|
845
|
-
});
|
|
846
|
-
writeMessage({ type: "GROK_CLOSE_TAB", tabId: tabIdToClose, id: tabCloseId });
|
|
847
|
-
}),
|
|
848
|
-
cdpEvaluate: (tabId, expression) => new Promise((resolve) => {
|
|
849
|
-
const evalId = ++requestCounter;
|
|
850
|
-
pendingToolRequests.set(evalId, {
|
|
851
|
-
socket: null,
|
|
852
|
-
originalId: null,
|
|
853
|
-
tool: "cdp_evaluate",
|
|
854
|
-
onComplete: (r) => resolve(r)
|
|
855
|
-
});
|
|
856
|
-
writeMessage({ type: "GROK_EVALUATE", tabId, expression, id: evalId });
|
|
857
|
-
}),
|
|
858
|
-
cdpCommand: (tabId, method, params) => new Promise((resolve) => {
|
|
859
|
-
const cmdId = ++requestCounter;
|
|
860
|
-
pendingToolRequests.set(cmdId, {
|
|
861
|
-
socket: null,
|
|
862
|
-
originalId: null,
|
|
863
|
-
tool: "cdp_command",
|
|
864
|
-
onComplete: (r) => resolve(r)
|
|
865
|
-
});
|
|
866
|
-
writeMessage({ type: "GROK_CDP_COMMAND", tabId, method, params, id: cmdId });
|
|
867
|
-
}),
|
|
1102
|
+
getCookies: () => requestCallExtension(
|
|
1103
|
+
requestContext,
|
|
1104
|
+
"get_cookies",
|
|
1105
|
+
{ type: "GET_TWITTER_COOKIES" },
|
|
1106
|
+
),
|
|
1107
|
+
createTab: () => requestCallExtension(
|
|
1108
|
+
requestContext,
|
|
1109
|
+
"create_tab",
|
|
1110
|
+
{ type: "GROK_NEW_TAB" },
|
|
1111
|
+
),
|
|
1112
|
+
closeTab: (tabIdToClose) => requestCallExtension(requestContext, "close_tab", { type: "GROK_CLOSE_TAB", tabId: tabIdToClose }, 45000, true),
|
|
1113
|
+
cdpEvaluate: (tabId, expression) => requestCallExtension(
|
|
1114
|
+
requestContext,
|
|
1115
|
+
"cdp_evaluate",
|
|
1116
|
+
{ type: "GROK_EVALUATE", tabId, expression },
|
|
1117
|
+
),
|
|
1118
|
+
cdpCommand: (tabId, method, params) => requestCallExtension(
|
|
1119
|
+
requestContext,
|
|
1120
|
+
"cdp_command",
|
|
1121
|
+
{ type: "GROK_CDP_COMMAND", tabId, method, params },
|
|
1122
|
+
),
|
|
868
1123
|
log: (msg) => log(`[grok] ${msg}`)
|
|
869
1124
|
});
|
|
870
1125
|
|
|
@@ -903,46 +1158,29 @@ function handleToolRequest(msg, socket) {
|
|
|
903
1158
|
|
|
904
1159
|
queueAiRequest(async () => {
|
|
905
1160
|
const result = await grokClient.validate({
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
socket: null,
|
|
930
|
-
originalId: null,
|
|
931
|
-
tool: "close_tab",
|
|
932
|
-
onComplete: (r) => resolve(r)
|
|
933
|
-
});
|
|
934
|
-
writeMessage({ type: "GROK_CLOSE_TAB", tabId: tabIdToClose, id: tabCloseId });
|
|
935
|
-
}),
|
|
936
|
-
cdpEvaluate: (tabId, expression) => new Promise((resolve) => {
|
|
937
|
-
const evalId = ++requestCounter;
|
|
938
|
-
pendingToolRequests.set(evalId, {
|
|
939
|
-
socket: null,
|
|
940
|
-
originalId: null,
|
|
941
|
-
tool: "cdp_evaluate",
|
|
942
|
-
onComplete: (r) => resolve(r)
|
|
943
|
-
});
|
|
944
|
-
writeMessage({ type: "GROK_EVALUATE", tabId, expression, id: evalId });
|
|
945
|
-
}),
|
|
1161
|
+
signal: requestContext.signal,
|
|
1162
|
+
getCookies: () => requestCallExtension(
|
|
1163
|
+
requestContext,
|
|
1164
|
+
"get_cookies",
|
|
1165
|
+
{ type: "GET_TWITTER_COOKIES" },
|
|
1166
|
+
),
|
|
1167
|
+
createTab: () => requestCallExtension(
|
|
1168
|
+
requestContext,
|
|
1169
|
+
"create_tab",
|
|
1170
|
+
{ type: "GROK_NEW_TAB" },
|
|
1171
|
+
),
|
|
1172
|
+
closeTab: (tabIdToClose) => requestCallExtension(
|
|
1173
|
+
requestContext,
|
|
1174
|
+
"close_tab",
|
|
1175
|
+
{ type: "GROK_CLOSE_TAB", tabId: tabIdToClose },
|
|
1176
|
+
45000,
|
|
1177
|
+
true,
|
|
1178
|
+
),
|
|
1179
|
+
cdpEvaluate: (tabId, expression) => requestCallExtension(
|
|
1180
|
+
requestContext,
|
|
1181
|
+
"cdp_evaluate",
|
|
1182
|
+
{ type: "GROK_EVALUATE", tabId, expression },
|
|
1183
|
+
),
|
|
946
1184
|
log: (msg) => log(`[grok:validate] ${msg}`)
|
|
947
1185
|
});
|
|
948
1186
|
|
|
@@ -987,30 +1225,12 @@ function handleToolRequest(msg, socket) {
|
|
|
987
1225
|
queueAiRequest(async () => {
|
|
988
1226
|
const EXT_CALL_TIMEOUT_MS = 30000;
|
|
989
1227
|
|
|
990
|
-
const callExtension = (toolName, msg, timeoutMs = EXT_CALL_TIMEOUT_MS) =>
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
if (msg && msg.type === "AISTUDIO_NEW_TAB") {
|
|
1228
|
+
const callExtension = (toolName, msg, timeoutMs = EXT_CALL_TIMEOUT_MS) => {
|
|
1229
|
+
if (msg?.type === "AISTUDIO_NEW_TAB") {
|
|
994
1230
|
log(`[aistudio] Opening tab: ${(msg.url || "https://aistudio.google.com/prompts/new_chat")}`);
|
|
995
1231
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
pendingToolRequests.delete(id);
|
|
999
|
-
reject(new Error(`Timeout waiting for extension: ${toolName}`));
|
|
1000
|
-
}, timeoutMs);
|
|
1001
|
-
|
|
1002
|
-
pendingToolRequests.set(id, {
|
|
1003
|
-
socket: null,
|
|
1004
|
-
originalId: null,
|
|
1005
|
-
tool: toolName,
|
|
1006
|
-
onComplete: (r) => {
|
|
1007
|
-
clearTimeout(timeoutId);
|
|
1008
|
-
resolve(r);
|
|
1009
|
-
}
|
|
1010
|
-
});
|
|
1011
|
-
|
|
1012
|
-
writeMessage({ ...msg, id });
|
|
1013
|
-
});
|
|
1232
|
+
return requestCallExtension(requestContext, toolName, msg, timeoutMs);
|
|
1233
|
+
};
|
|
1014
1234
|
|
|
1015
1235
|
// 1. Get page context if requested
|
|
1016
1236
|
let pageContext = null;
|
|
@@ -1044,6 +1264,7 @@ function handleToolRequest(msg, socket) {
|
|
|
1044
1264
|
// 3. Call AI Studio client
|
|
1045
1265
|
const result = await aistudioClient.query({
|
|
1046
1266
|
prompt: fullPrompt,
|
|
1267
|
+
signal: requestContext.signal,
|
|
1047
1268
|
model: model || undefined,
|
|
1048
1269
|
timeout: timeout || 300000,
|
|
1049
1270
|
getCookies: () => callExtension("get_cookies", { type: "GET_GOOGLE_COOKIES" }, 45000),
|
|
@@ -1102,33 +1323,16 @@ function handleToolRequest(msg, socket) {
|
|
|
1102
1323
|
queueAiRequest(async () => {
|
|
1103
1324
|
const EXT_CALL_TIMEOUT_MS = 30000;
|
|
1104
1325
|
|
|
1105
|
-
const callExtension = (toolName, msg, timeoutMs = EXT_CALL_TIMEOUT_MS) =>
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
if (msg && msg.type === "AISTUDIO_NEW_TAB") {
|
|
1326
|
+
const callExtension = (toolName, msg, timeoutMs = EXT_CALL_TIMEOUT_MS) => {
|
|
1327
|
+
if (msg?.type === "AISTUDIO_NEW_TAB") {
|
|
1109
1328
|
log(`[aistudio] Opening tab: ${(msg.url || "https://aistudio.google.com/apps")}`);
|
|
1110
1329
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
pendingToolRequests.delete(id);
|
|
1114
|
-
reject(new Error(`Timeout waiting for extension: ${toolName}`));
|
|
1115
|
-
}, timeoutMs);
|
|
1116
|
-
|
|
1117
|
-
pendingToolRequests.set(id, {
|
|
1118
|
-
socket: null,
|
|
1119
|
-
originalId: null,
|
|
1120
|
-
tool: toolName,
|
|
1121
|
-
onComplete: (r) => {
|
|
1122
|
-
clearTimeout(timeoutId);
|
|
1123
|
-
resolve(r);
|
|
1124
|
-
}
|
|
1125
|
-
});
|
|
1126
|
-
|
|
1127
|
-
writeMessage({ ...msg, id });
|
|
1128
|
-
});
|
|
1330
|
+
return requestCallExtension(requestContext, toolName, msg, timeoutMs);
|
|
1331
|
+
};
|
|
1129
1332
|
|
|
1130
1333
|
const result = await aistudioBuild.build({
|
|
1131
1334
|
prompt: query,
|
|
1335
|
+
signal: requestContext.signal,
|
|
1132
1336
|
model: model || undefined,
|
|
1133
1337
|
output,
|
|
1134
1338
|
keepOpen,
|
|
@@ -1179,6 +1383,7 @@ function handleToolRequest(msg, socket) {
|
|
|
1179
1383
|
let lastError = null;
|
|
1180
1384
|
|
|
1181
1385
|
const sendNextKey = () => {
|
|
1386
|
+
if (requestContext.signal.aborted) return;
|
|
1182
1387
|
if (completed >= repeat) {
|
|
1183
1388
|
if (lastError) {
|
|
1184
1389
|
sendToolResponse(socket, originalId, null, `Key repeat failed: ${lastError}`);
|
|
@@ -1187,18 +1392,15 @@ function handleToolRequest(msg, socket) {
|
|
|
1187
1392
|
}
|
|
1188
1393
|
return;
|
|
1189
1394
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
socket: null,
|
|
1193
|
-
originalId: null,
|
|
1395
|
+
requestCallExtension(
|
|
1396
|
+
requestContext,
|
|
1194
1397
|
tool,
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
});
|
|
1201
|
-
writeMessage({ type: "EXECUTE_KEY", key, tabId: tid, id });
|
|
1398
|
+
{ type: "EXECUTE_KEY", key, tabId: tid },
|
|
1399
|
+
).then((result) => {
|
|
1400
|
+
if (result.error) lastError = result.error;
|
|
1401
|
+
completed++;
|
|
1402
|
+
return require("./abort.cjs").abortableDelay(50, requestContext.signal);
|
|
1403
|
+
}).then(sendNextKey).catch((error) => sendToolResponse(socket, originalId, null, error.message));
|
|
1202
1404
|
};
|
|
1203
1405
|
sendNextKey();
|
|
1204
1406
|
return;
|
|
@@ -1206,23 +1408,25 @@ function handleToolRequest(msg, socket) {
|
|
|
1206
1408
|
|
|
1207
1409
|
if (extensionMsg.type === "NAMED_TAB_SWITCH" || extensionMsg.type === "NAMED_TAB_CLOSE") {
|
|
1208
1410
|
const { name, type: opType } = extensionMsg;
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
sendToolResponse(socket, originalId, null, result.error || `No tab found with name "${name}"`);
|
|
1217
|
-
return;
|
|
1218
|
-
}
|
|
1219
|
-
const actionId = ++requestCounter;
|
|
1220
|
-
const actionType = opType === "NAMED_TAB_SWITCH" ? "SWITCH_TAB" : "CLOSE_TAB";
|
|
1221
|
-
pendingToolRequests.set(actionId, { socket, originalId, tool, tabId: result.tabId });
|
|
1222
|
-
writeMessage({ type: actionType, tabId: result.tabId, id: actionId });
|
|
1411
|
+
requestCallExtension(
|
|
1412
|
+
requestContext,
|
|
1413
|
+
"tabs_get_by_name",
|
|
1414
|
+
{ type: "TABS_GET_BY_NAME", name },
|
|
1415
|
+
).then((result) => {
|
|
1416
|
+
if (result.error || !result.tabId) {
|
|
1417
|
+
throw new Error(result.error || `No tab found with name "${name}"`);
|
|
1223
1418
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1419
|
+
const actionType = opType === "NAMED_TAB_SWITCH" ? "SWITCH_TAB" : "CLOSE_TAB";
|
|
1420
|
+
const actionTool = opType === "NAMED_TAB_SWITCH" ? "switch_tab" : "close_tab";
|
|
1421
|
+
return requestCallExtension(
|
|
1422
|
+
requestContext,
|
|
1423
|
+
actionTool,
|
|
1424
|
+
{ type: actionType, tabId: result.tabId },
|
|
1425
|
+
30000,
|
|
1426
|
+
actionTool === "close_tab",
|
|
1427
|
+
);
|
|
1428
|
+
}).then((result) => sendToolResponse(socket, originalId, result, result?.error || null))
|
|
1429
|
+
.catch((error) => sendToolResponse(socket, originalId, null, error.message));
|
|
1226
1430
|
return;
|
|
1227
1431
|
}
|
|
1228
1432
|
|
|
@@ -1232,7 +1436,13 @@ function handleToolRequest(msg, socket) {
|
|
|
1232
1436
|
originalId,
|
|
1233
1437
|
tool,
|
|
1234
1438
|
savePath: extensionMsg.savePath || args?.savePath,
|
|
1235
|
-
autoScreenshot: args?.autoScreenshot,
|
|
1439
|
+
autoScreenshot: args?.autoScreenshot === true,
|
|
1440
|
+
autoScreenshotOutput: args?.autoScreenshotOutput,
|
|
1441
|
+
networkExport: extensionMsg.type === "EXPORT_NETWORK_REQUESTS",
|
|
1442
|
+
persistNetwork: extensionMsg.type === "READ_NETWORK_REQUESTS" && extensionMsg.full && args?.["no-save"] !== true,
|
|
1443
|
+
networkExportPath: args?.output,
|
|
1444
|
+
networkPath: args?.["network-path"],
|
|
1445
|
+
networkExportFormat: extensionMsg.har ? "har" : extensionMsg.jsonl ? "jsonl" : "json",
|
|
1236
1446
|
fullRes: extensionMsg.fullRes || args?.fullRes,
|
|
1237
1447
|
maxSize: extensionMsg.maxSize || args?.maxSize,
|
|
1238
1448
|
tabId: extensionMsg.tabId || tabId
|
|
@@ -1245,12 +1455,14 @@ function handleToolRequest(msg, socket) {
|
|
|
1245
1455
|
writeMessage(finalMsg);
|
|
1246
1456
|
}
|
|
1247
1457
|
|
|
1248
|
-
function executeBatch(actions, tabId, socket, originalId) {
|
|
1458
|
+
function executeBatch(actions, tabId, socket, originalId, requestContext = requestStorage.getStore()) {
|
|
1459
|
+
const writeMessage = (message) => sendOwnedExtensionMessage(requestContext, message);
|
|
1249
1460
|
const results = [];
|
|
1250
1461
|
const DELAY_MS = 100;
|
|
1251
1462
|
let currentIndex = 0;
|
|
1252
1463
|
|
|
1253
1464
|
function executeNextAction() {
|
|
1465
|
+
if (requestContext.signal.aborted) return;
|
|
1254
1466
|
if (currentIndex >= actions.length) {
|
|
1255
1467
|
sendToolResponse(socket, originalId, {
|
|
1256
1468
|
success: true,
|
|
@@ -1281,16 +1493,14 @@ function executeBatch(actions, tabId, socket, originalId) {
|
|
|
1281
1493
|
if (extensionMsg.type === "LOCAL_WAIT") {
|
|
1282
1494
|
results.push({ index: currentIndex, type: action.type, success: true });
|
|
1283
1495
|
currentIndex++;
|
|
1284
|
-
|
|
1496
|
+
require("./abort.cjs").abortableDelay(extensionMsg.seconds * 1000, requestContext.signal)
|
|
1497
|
+
.then(executeNextAction)
|
|
1498
|
+
.catch((error) => sendToolResponse(socket, originalId, null, error.message));
|
|
1285
1499
|
return;
|
|
1286
1500
|
}
|
|
1287
1501
|
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
socket: null,
|
|
1291
|
-
originalId: null,
|
|
1292
|
-
tool: toolName,
|
|
1293
|
-
onComplete: (result) => {
|
|
1502
|
+
requestCallExtension(requestContext, toolName, extensionMsg, 30000)
|
|
1503
|
+
.then((result) => {
|
|
1294
1504
|
if (result.error) {
|
|
1295
1505
|
results.push({ index: currentIndex, type: action.type, success: false, error: result.error });
|
|
1296
1506
|
sendToolResponse(socket, originalId, {
|
|
@@ -1302,15 +1512,12 @@ function executeBatch(actions, tabId, socket, originalId) {
|
|
|
1302
1512
|
}, null);
|
|
1303
1513
|
return;
|
|
1304
1514
|
}
|
|
1305
|
-
|
|
1306
1515
|
results.push({ index: currentIndex, type: action.type, success: true });
|
|
1307
1516
|
currentIndex++;
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
writeMessage({ ...extensionMsg, id });
|
|
1517
|
+
return require("./abort.cjs").abortableDelay(DELAY_MS, requestContext.signal)
|
|
1518
|
+
.then(executeNextAction);
|
|
1519
|
+
})
|
|
1520
|
+
.catch((error) => sendToolResponse(socket, originalId, null, error.message));
|
|
1314
1521
|
}
|
|
1315
1522
|
|
|
1316
1523
|
executeNextAction();
|
|
@@ -1371,7 +1578,7 @@ function processInput() {
|
|
|
1371
1578
|
|
|
1372
1579
|
try {
|
|
1373
1580
|
const msg = JSON.parse(jsonStr);
|
|
1374
|
-
log(`Received from extension: ${
|
|
1581
|
+
log(`Received from extension: ${msg.type || "unknown"}${msg.id !== undefined ? ` id=${msg.id}` : ""}`);
|
|
1375
1582
|
|
|
1376
1583
|
if (msg.type === "GET_AUTH") {
|
|
1377
1584
|
log("Handling GET_AUTH from extension");
|
|
@@ -1401,28 +1608,41 @@ function processInput() {
|
|
|
1401
1608
|
handleApiRequest(msg, writeMessage);
|
|
1402
1609
|
return;
|
|
1403
1610
|
}
|
|
1611
|
+
|
|
1612
|
+
if (msg.type === "PLAYBOOK_WATCH_EVENT") {
|
|
1613
|
+
appendRecordEvent({
|
|
1614
|
+
type: "browser.event",
|
|
1615
|
+
event: msg.event,
|
|
1616
|
+
selector: msg.selector,
|
|
1617
|
+
value: msg.value,
|
|
1618
|
+
url: redactUrlSecrets(msg.url),
|
|
1619
|
+
tabId: msg.tabId,
|
|
1620
|
+
timestamp: msg.timestamp || new Date().toISOString(),
|
|
1621
|
+
});
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1404
1624
|
|
|
1405
1625
|
if (msg.type === "STREAM_EVENT") {
|
|
1406
1626
|
const stream = activeStreams.get(msg.streamId);
|
|
1407
1627
|
if (stream) {
|
|
1408
|
-
|
|
1409
|
-
stream
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
writeMessage({ type: "STREAM_STOP", streamId: msg.streamId });
|
|
1414
|
-
}
|
|
1628
|
+
sendSocket(stream.socket, msg.event, { stream: true }).catch((error) => {
|
|
1629
|
+
log(`Error forwarding stream event: ${error.message}`);
|
|
1630
|
+
stopActiveStream(msg.streamId);
|
|
1631
|
+
stream.socket.destroy(error);
|
|
1632
|
+
});
|
|
1415
1633
|
}
|
|
1416
1634
|
return;
|
|
1417
1635
|
}
|
|
1418
|
-
|
|
1636
|
+
|
|
1419
1637
|
if (msg.type === "STREAM_ERROR") {
|
|
1420
1638
|
const stream = activeStreams.get(msg.streamId);
|
|
1421
1639
|
if (stream) {
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1640
|
+
sendSocket(stream.socket, { error: msg.error }, { stream: true })
|
|
1641
|
+
.catch((error) => {
|
|
1642
|
+
log(`Error forwarding stream error: ${error.message}`);
|
|
1643
|
+
stream.socket.destroy(error);
|
|
1644
|
+
})
|
|
1645
|
+
.finally(() => stopActiveStream(msg.streamId));
|
|
1426
1646
|
}
|
|
1427
1647
|
return;
|
|
1428
1648
|
}
|
|
@@ -1430,19 +1650,49 @@ function processInput() {
|
|
|
1430
1650
|
|
|
1431
1651
|
if (msg.id && pendingToolRequests.has(msg.id)) {
|
|
1432
1652
|
const pending = pendingToolRequests.get(msg.id);
|
|
1653
|
+
if (pending.request?.signal.aborted || pending.request?.tombstoned) {
|
|
1654
|
+
const request = pending.request;
|
|
1655
|
+
const topLevelResponse = !pending.resolve && !pending.onComplete;
|
|
1656
|
+
pendingToolRequests.resolve(msg.id, msg);
|
|
1657
|
+
if (topLevelResponse && request?.context) {
|
|
1658
|
+
completeOwnedRequest(request.context, request.id, "cleanup-settled");
|
|
1659
|
+
}
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
if (pending.resolve || pending.onComplete) {
|
|
1663
|
+
pendingToolRequests.resolve(msg.id, msg);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1433
1666
|
pendingToolRequests.delete(msg.id);
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
pending.onComplete(msg);
|
|
1437
|
-
} else {
|
|
1667
|
+
{
|
|
1668
|
+
|
|
1438
1669
|
const { socket, originalId, savePath, autoScreenshot, tabId: storedTabId } = pending;
|
|
1439
1670
|
const tabId = storedTabId || msg._resolvedTabId;
|
|
1671
|
+
const failAutoScreenshot = (message) => pending.autoScreenshotOutput
|
|
1672
|
+
? sendToolResponse(socket, originalId, null, `Auto-screenshot failed: ${message}`)
|
|
1673
|
+
: sendToolResponse(socket, originalId, { ...msg, autoScreenshotError: message }, null);
|
|
1440
1674
|
|
|
1441
|
-
if (
|
|
1675
|
+
if (pending.networkExport && Array.isArray(msg.entries)) {
|
|
1676
|
+
try {
|
|
1677
|
+
const exportResult = writeNetworkExport(pending.networkExportPath, msg.entries, pending.networkExportFormat);
|
|
1678
|
+
sendToolResponse(socket, originalId, exportResult, null);
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
sendToolResponse(socket, originalId, null, `Failed to export network requests: ${error.message}`);
|
|
1681
|
+
}
|
|
1682
|
+
} else if (pending.persistNetwork && Array.isArray(msg.entries)) {
|
|
1683
|
+
try {
|
|
1684
|
+
for (const entry of msg.entries) networkStore.appendEntrySync(entry, pending.networkPath);
|
|
1685
|
+
networkStore.maybeAutoCleanup();
|
|
1686
|
+
sendToolResponse(socket, originalId, msg, null);
|
|
1687
|
+
} catch (error) {
|
|
1688
|
+
sendToolResponse(socket, originalId, null, `Failed to persist network requests: ${error.message}`);
|
|
1689
|
+
}
|
|
1690
|
+
} else if (savePath && msg.base64) {
|
|
1442
1691
|
try {
|
|
1443
1692
|
const dir = path.dirname(savePath);
|
|
1444
1693
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1445
|
-
fs.writeFileSync(savePath, Buffer.from(msg.base64, "base64"));
|
|
1694
|
+
fs.writeFileSync(savePath, Buffer.from(msg.base64, "base64"), { mode: 0o600 });
|
|
1695
|
+
try { fs.chmodSync(savePath, 0o600); } catch {}
|
|
1446
1696
|
const origWidth = msg.width || 0;
|
|
1447
1697
|
const origHeight = msg.height || 0;
|
|
1448
1698
|
const maxSize = pending.maxSize || 1200;
|
|
@@ -1465,8 +1715,7 @@ function processInput() {
|
|
|
1465
1715
|
}
|
|
1466
1716
|
} else if (autoScreenshot && tabId && !msg.error && !msg.base64) {
|
|
1467
1717
|
|
|
1468
|
-
const
|
|
1469
|
-
const screenshotPath = path.join(SURF_TMP, `pi-auto-${Date.now()}.png`);
|
|
1718
|
+
const screenshotPath = pending.autoScreenshotOutput || path.join(SURF_TMP, `pi-auto-${Date.now()}.png`);
|
|
1470
1719
|
|
|
1471
1720
|
const autoFiles = fs.readdirSync(SURF_TMP)
|
|
1472
1721
|
.filter(f => f.startsWith("pi-auto-") && f.endsWith(".png"))
|
|
@@ -1477,14 +1726,17 @@ function processInput() {
|
|
|
1477
1726
|
try { fs.unlinkSync(path.join(SURF_TMP, f.name)); } catch (e) {}
|
|
1478
1727
|
});
|
|
1479
1728
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1729
|
+
require("./abort.cjs").abortableDelay(500, pending.request?.signal)
|
|
1730
|
+
.then(() => requestCallExtension(
|
|
1731
|
+
pending.request,
|
|
1732
|
+
"screenshot",
|
|
1733
|
+
{ type: "EXECUTE_SCREENSHOT", tabId },
|
|
1734
|
+
))
|
|
1735
|
+
.then((screenshotMsg) => {
|
|
1485
1736
|
if (screenshotMsg.base64) {
|
|
1486
1737
|
try {
|
|
1487
|
-
fs.writeFileSync(screenshotPath, Buffer.from(screenshotMsg.base64, "base64"));
|
|
1738
|
+
fs.writeFileSync(screenshotPath, Buffer.from(screenshotMsg.base64, "base64"), { mode: 0o600 });
|
|
1739
|
+
try { fs.chmodSync(screenshotPath, 0o600); } catch {}
|
|
1488
1740
|
const origW = screenshotMsg.width || 0;
|
|
1489
1741
|
const origH = screenshotMsg.height || 0;
|
|
1490
1742
|
let finalW = origW, finalH = origH;
|
|
@@ -1501,16 +1753,17 @@ function processInput() {
|
|
|
1501
1753
|
autoScreenshot: { path: screenshotPath, width: finalW, height: finalH, originalWidth: origW, originalHeight: origH }
|
|
1502
1754
|
}, null);
|
|
1503
1755
|
} catch (e) {
|
|
1504
|
-
|
|
1756
|
+
failAutoScreenshot(e.message);
|
|
1505
1757
|
}
|
|
1506
1758
|
} else {
|
|
1507
1759
|
const errMsg = screenshotMsg.error || "Failed to capture";
|
|
1508
|
-
|
|
1760
|
+
failAutoScreenshot(errMsg);
|
|
1509
1761
|
}
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
setTimeout(() => writeMessage({ type: "EXECUTE_SCREENSHOT", tabId, id: screenshotId }), 500);
|
|
1762
|
+
})
|
|
1763
|
+
.catch((error) => failAutoScreenshot(error.message));
|
|
1513
1764
|
return;
|
|
1765
|
+
} else if (autoScreenshot && pending.autoScreenshotOutput && !msg.error) {
|
|
1766
|
+
failAutoScreenshot(tabId ? "screenshot response was invalid" : "no tab available");
|
|
1514
1767
|
} else if (msg.results && msg.savePath) {
|
|
1515
1768
|
try {
|
|
1516
1769
|
const dir = msg.savePath;
|
|
@@ -1544,11 +1797,7 @@ function processInput() {
|
|
|
1544
1797
|
}
|
|
1545
1798
|
} else if (msg.id && pendingRequests.has(msg.id)) {
|
|
1546
1799
|
const { socket } = pendingRequests.get(msg.id);
|
|
1547
|
-
|
|
1548
|
-
socket.write(JSON.stringify(msg) + "\n");
|
|
1549
|
-
} catch (e) {
|
|
1550
|
-
log(`Error writing to CLI socket: ${e.message}`);
|
|
1551
|
-
}
|
|
1800
|
+
sendSocket(socket, msg).catch((error) => log(`Error writing to CLI socket: ${error.message}`));
|
|
1552
1801
|
pendingRequests.delete(msg.id);
|
|
1553
1802
|
}
|
|
1554
1803
|
} catch (e) {
|
|
@@ -1571,17 +1820,12 @@ const connectedSockets = new Set();
|
|
|
1571
1820
|
process.stdin.on("end", () => {
|
|
1572
1821
|
log("stdin ended (extension disconnected), notifying clients");
|
|
1573
1822
|
for (const socket of Array.from(connectedSockets)) {
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
}) + "\n");
|
|
1579
|
-
socket.end();
|
|
1580
|
-
} catch (e) {
|
|
1581
|
-
// Socket may already be closed
|
|
1582
|
-
}
|
|
1823
|
+
sendSocket(socket, {
|
|
1824
|
+
type: "extension_disconnected",
|
|
1825
|
+
message: "Surf extension was reloaded. Restart your command."
|
|
1826
|
+
}).finally(() => socket.end()).catch(() => socket.end());
|
|
1583
1827
|
}
|
|
1584
|
-
|
|
1828
|
+
shutdown(0);
|
|
1585
1829
|
});
|
|
1586
1830
|
|
|
1587
1831
|
process.stdin.on("error", (err) => {
|
|
@@ -1592,142 +1836,276 @@ process.stdout.on("error", (err) => {
|
|
|
1592
1836
|
log(`stdout error: ${err.message}`);
|
|
1593
1837
|
});
|
|
1594
1838
|
|
|
1595
|
-
const
|
|
1839
|
+
const handleClient = (socket) => {
|
|
1840
|
+
const isRemote = Boolean(socket.remoteAddress && socket.remotePort);
|
|
1841
|
+
let transferState;
|
|
1842
|
+
let transferReady;
|
|
1843
|
+
let transferCleanupPromise;
|
|
1844
|
+
const cleanupTransfers = () => {
|
|
1845
|
+
if (!transferCleanupPromise) {
|
|
1846
|
+
transferCleanupPromise = transferReady
|
|
1847
|
+
? transferReady.then((state) => state?.cleanup())
|
|
1848
|
+
: Promise.resolve();
|
|
1849
|
+
}
|
|
1850
|
+
return transferCleanupPromise;
|
|
1851
|
+
};
|
|
1852
|
+
socket.transferCleanup = cleanupTransfers;
|
|
1853
|
+
const ensureTransferState = () => {
|
|
1854
|
+
if (context?.closed || socket.destroyed) throw transferError("transfer connection is closed", "SURF_TRANSFER_CLOSED");
|
|
1855
|
+
if (!transferReady) {
|
|
1856
|
+
transferCleanupPromise = undefined;
|
|
1857
|
+
transferReady = createStagingDirectory(SURF_TMP)
|
|
1858
|
+
.then(async (directory) => {
|
|
1859
|
+
if (context?.closed || socket.destroyed) {
|
|
1860
|
+
await fs.promises.rm(directory, { recursive: true, force: true }).catch(() => {});
|
|
1861
|
+
throw transferError("transfer connection is closed", "SURF_TRANSFER_CLOSED");
|
|
1862
|
+
}
|
|
1863
|
+
try {
|
|
1864
|
+
return createTransferState({ directory, writer: { send: (frame) => sendSocket(socket, frame) }, onActivity: () => sessionManager.touch(socketContexts.get(socket)) });
|
|
1865
|
+
} catch (error) {
|
|
1866
|
+
return fs.promises.rm(directory, { recursive: true, force: true }).catch(() => {}).then(() => { throw error; });
|
|
1867
|
+
}
|
|
1868
|
+
})
|
|
1869
|
+
.catch((error) => { transferReady = undefined; throw error; });
|
|
1870
|
+
}
|
|
1871
|
+
return transferReady;
|
|
1872
|
+
};
|
|
1873
|
+
let context;
|
|
1874
|
+
try {
|
|
1875
|
+
context = sessionManager.admit(socket, isRemote);
|
|
1876
|
+
} catch (error) {
|
|
1877
|
+
sendSocket(socket, { error: error.message }).finally(() => socket.destroy()).catch(() => socket.destroy());
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
const writer = createSocketWriter(socket, {
|
|
1881
|
+
maxPendingBytes: 4 * 1024 * 1024,
|
|
1882
|
+
onOverflow: ({ stream, error }) => {
|
|
1883
|
+
auditSession({ event: stream ? "stream" : "writer", context, outcome: "overflow", request: context.activeRequest });
|
|
1884
|
+
sessionManager.stopStream(context);
|
|
1885
|
+
socket.destroy(error);
|
|
1886
|
+
},
|
|
1887
|
+
});
|
|
1888
|
+
socketContexts.set(socket, context);
|
|
1889
|
+
socketWriters.set(socket, writer);
|
|
1596
1890
|
log("CLI client connected");
|
|
1597
1891
|
connectedSockets.add(socket);
|
|
1598
1892
|
socket.on("close", () => connectedSockets.delete(socket));
|
|
1599
1893
|
|
|
1600
|
-
|
|
1894
|
+
const stateDir = getStateDir();
|
|
1895
|
+
let principal = null;
|
|
1896
|
+
const authSession = isRemote ? createServerAuthSession({
|
|
1897
|
+
socket,
|
|
1898
|
+
stateDir,
|
|
1899
|
+
send: (value) => sendSocket(socket, value),
|
|
1900
|
+
async onAuthenticated(authenticatedPrincipal) {
|
|
1901
|
+
sessionManager.authenticate(context, authenticatedPrincipal);
|
|
1902
|
+
principal = authenticatedPrincipal;
|
|
1903
|
+
log(`Remote client authenticated: ${authenticatedPrincipal.label} (${authenticatedPrincipal.clientId})`);
|
|
1904
|
+
},
|
|
1905
|
+
onError(error) {
|
|
1906
|
+
log(`Remote authentication rejected: ${error.message}`);
|
|
1907
|
+
sendSocket(socket, { type: "auth_error", message: error.message }).finally(() => socket.destroy()).catch(() => socket.destroy());
|
|
1908
|
+
},
|
|
1909
|
+
}) : null;
|
|
1910
|
+
let messageChain = Promise.resolve();
|
|
1911
|
+
const processMessage = async (msg) => {
|
|
1912
|
+
if (context.closed) return;
|
|
1913
|
+
if (isRemote && !authSession.authenticated) {
|
|
1914
|
+
await authSession.handle(msg);
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
if (isRemote) {
|
|
1918
|
+
let authorized = false;
|
|
1919
|
+
try {
|
|
1920
|
+
authorized = Boolean(principal && isClientAuthorized(stateDir, principal.clientId));
|
|
1921
|
+
} catch (error) {
|
|
1922
|
+
log(`Remote authorization registry check failed: ${error.message}`);
|
|
1923
|
+
}
|
|
1924
|
+
if (!authorized) {
|
|
1925
|
+
await sendSocket(socket, { error: "remote client authorization is unavailable or revoked" }).catch(() => {});
|
|
1926
|
+
socket.destroy();
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1601
1930
|
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1931
|
+
if (isRemote && msg.type && msg.type.startsWith("transfer_")) {
|
|
1932
|
+
transferState ||= await ensureTransferState();
|
|
1933
|
+
await transferState.handle(msg);
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1606
1936
|
|
|
1607
|
-
|
|
1608
|
-
|
|
1937
|
+
if (msg.type === "tool_request") {
|
|
1938
|
+
const tool = msg.params?.tool || "unknown";
|
|
1939
|
+
let request;
|
|
1609
1940
|
try {
|
|
1610
|
-
const
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
} else {
|
|
1623
|
-
socket.write(JSON.stringify({
|
|
1624
|
-
id: msg.id || 0,
|
|
1625
|
-
auth: null,
|
|
1626
|
-
hint: "No OAuth credentials found. Run 'pi --login anthropic' in terminal to authenticate with Claude Max."
|
|
1627
|
-
}) + "\n");
|
|
1628
|
-
}
|
|
1629
|
-
} catch (e) {
|
|
1630
|
-
log(`Error reading auth file: ${e.message}`);
|
|
1631
|
-
socket.write(JSON.stringify({
|
|
1632
|
-
id: msg.id || 0,
|
|
1633
|
-
auth: null,
|
|
1634
|
-
hint: "Failed to read auth credentials. Run 'pi --login anthropic' in terminal to authenticate."
|
|
1635
|
-
}) + "\n");
|
|
1636
|
-
}
|
|
1637
|
-
continue;
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
if (msg.type === "tool_request") {
|
|
1641
|
-
log("Handling tool_request: " + msg.method + " " + (msg.params?.tool || ""));
|
|
1642
|
-
try {
|
|
1643
|
-
handleToolRequest(msg, socket);
|
|
1644
|
-
} catch (e) {
|
|
1645
|
-
socket.write(JSON.stringify({ error: e.message || "Request failed" }) + "\n");
|
|
1646
|
-
}
|
|
1647
|
-
continue;
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
if (msg.type === "stream_request") {
|
|
1651
|
-
log("Handling stream_request: " + msg.streamType);
|
|
1652
|
-
handleStreamRequest(msg, socket);
|
|
1653
|
-
continue;
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
if (msg.type === "stream_stop") {
|
|
1657
|
-
log("Handling stream_stop");
|
|
1658
|
-
for (const [streamId, stream] of activeStreams.entries()) {
|
|
1659
|
-
if (stream.socket === socket) {
|
|
1660
|
-
writeMessage({ type: "STREAM_STOP", streamId });
|
|
1661
|
-
activeStreams.delete(streamId);
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
continue;
|
|
1941
|
+
const deadlineMs = TEST_REQUEST_DEADLINE_MS || resolveRequestDeadlineMs(tool, msg.params?.args);
|
|
1942
|
+
request = await sessionManager.beginRequest(context, { id: msg.id, tool, deadlineMs });
|
|
1943
|
+
request.context = context;
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
if (transferState) await discardRequestTransfers(msg, transferState);
|
|
1946
|
+
await sendSocket(socket, { type: "tool_response", id: msg.id || null, error: { content: [{ type: "text", text: error.message }] } }).catch(() => {});
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
log(`Handling tool_request: ${msg.method} ${tool}${principal ? ` for ${principal.label}` : ""}`);
|
|
1950
|
+
try {
|
|
1951
|
+
if (isRemote) {
|
|
1952
|
+
await applyRequestTransfers(msg, request, transferState, ensureTransferState);
|
|
1665
1953
|
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
log(`Forwarding to extension: id=${id} type=${msg.type}`);
|
|
1669
|
-
pendingRequests.set(id, { socket });
|
|
1670
|
-
writeMessage({ ...msg, id });
|
|
1954
|
+
throwIfAborted(request.signal, "Request cancelled");
|
|
1955
|
+
requestStorage.run(request, () => handleToolRequest(msg, socket, request));
|
|
1671
1956
|
} catch (e) {
|
|
1672
|
-
|
|
1673
|
-
socket.
|
|
1957
|
+
await discardRequestTransfers(msg, transferState);
|
|
1958
|
+
sendToolResponse(socket, msg.id || null, null, e.message || "Request failed");
|
|
1674
1959
|
}
|
|
1960
|
+
return;
|
|
1675
1961
|
}
|
|
1962
|
+
|
|
1963
|
+
if (msg.type === "stream_request") {
|
|
1964
|
+
if (msg.streamType !== "STREAM_CONSOLE" && msg.streamType !== "STREAM_NETWORK") {
|
|
1965
|
+
log(`Rejecting unsupported stream type: ${msg.streamType}`);
|
|
1966
|
+
await sendSocket(socket, { error: `Unsupported stream type: ${msg.streamType}` }).catch(() => {});
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
if (!sessionManager.canStartStream(context)) {
|
|
1970
|
+
await sendSocket(socket, { error: "stream limit reached or connection is not stream-only" }).catch(() => {});
|
|
1971
|
+
socket.destroy();
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
log(`Handling stream_request: ${msg.streamType}`);
|
|
1975
|
+
handleStreamRequest(msg, socket);
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
if (msg.type === "stream_stop") {
|
|
1980
|
+
log("Handling stream_stop");
|
|
1981
|
+
for (const [streamId, stream] of activeStreams.entries()) {
|
|
1982
|
+
if (stream.socket === socket) stopActiveStream(streamId);
|
|
1983
|
+
}
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
log(`Rejecting unsupported socket request type: ${msg.type}`);
|
|
1988
|
+
await sendSocket(socket, { error: `Unsupported request type: ${msg.type}` }).catch(() => {});
|
|
1989
|
+
};
|
|
1990
|
+
|
|
1991
|
+
const parser = createFrameParser({
|
|
1992
|
+
onFrame(msg) {
|
|
1993
|
+
messageChain = messageChain.then(() => processMessage(msg)).catch((error) => {
|
|
1994
|
+
log(`Error handling CLI request: ${error.message}`);
|
|
1995
|
+
if (isRemote && msg.type && msg.type.startsWith("transfer_")) {
|
|
1996
|
+
sendSocket(socket, { type: "transfer_error", version: 1, transferId: msg.transferId, error: error.message || "Transfer failed" })
|
|
1997
|
+
.finally(() => socket.destroy()).catch(() => socket.destroy());
|
|
1998
|
+
} else {
|
|
1999
|
+
sendSocket(socket, { error: error.message || "Request failed" }).catch(() => {});
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
},
|
|
2003
|
+
onError(error) {
|
|
2004
|
+
log(`CLI frame rejected: ${error.message}`);
|
|
2005
|
+
if (isRemote && !authSession.authenticated) {
|
|
2006
|
+
sendSocket(socket, { type: "auth_error", message: error.message }).finally(() => socket.destroy()).catch(() => socket.destroy());
|
|
2007
|
+
} else {
|
|
2008
|
+
socket.destroy();
|
|
2009
|
+
}
|
|
2010
|
+
},
|
|
2011
|
+
maxFrameBytes: MAX_CLIENT_FRAME_BYTES,
|
|
1676
2012
|
});
|
|
1677
2013
|
|
|
2014
|
+
socket.on("data", (data) => parser.push(data));
|
|
2015
|
+
|
|
1678
2016
|
socket.on("error", (err) => {
|
|
1679
2017
|
log(`CLI socket error: ${err.message}`);
|
|
1680
2018
|
});
|
|
1681
2019
|
|
|
1682
2020
|
socket.on("close", () => {
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
2021
|
+
parser.close();
|
|
2022
|
+
authSession?.close();
|
|
2023
|
+
const activeRequest = context.activeRequest;
|
|
2024
|
+
let cleanupPendingId;
|
|
2025
|
+
if (activeRequest && !activeRequest.queued) {
|
|
2026
|
+
cleanupPendingId = `transfer-cleanup-${++requestCounter}`;
|
|
2027
|
+
pendingToolRequests.set(cleanupPendingId, {
|
|
2028
|
+
request: activeRequest,
|
|
2029
|
+
cleanup: true,
|
|
2030
|
+
tool: "transfer_cleanup",
|
|
2031
|
+
resolve: () => {},
|
|
2032
|
+
reject: () => {},
|
|
2033
|
+
});
|
|
2034
|
+
completeOwnedRequest(context, activeRequest.id, "cleanup-settled");
|
|
1693
2035
|
}
|
|
2036
|
+
const cleanupPromise = cleanupTransfers();
|
|
2037
|
+
cleanupPromise.finally(() => {
|
|
2038
|
+
if (cleanupPendingId) pendingToolRequests.delete(cleanupPendingId);
|
|
2039
|
+
}).catch(() => {});
|
|
2040
|
+
writer.close();
|
|
2041
|
+
sessionManager.close(context);
|
|
2042
|
+
if (activeRequest) pendingToolRequests.tombstoneAfterAbort(activeRequest);
|
|
2043
|
+
log("CLI client disconnected");
|
|
1694
2044
|
for (const [streamId, stream] of activeStreams.entries()) {
|
|
1695
|
-
if (stream.socket === socket)
|
|
1696
|
-
writeMessage({ type: "STREAM_STOP", streamId });
|
|
1697
|
-
activeStreams.delete(streamId);
|
|
1698
|
-
}
|
|
2045
|
+
if (stream.socket === socket) stopActiveStream(streamId);
|
|
1699
2046
|
}
|
|
1700
2047
|
});
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
server.listen(SOCKET_PATH, () => {
|
|
1704
|
-
log("Socket server listening on " + SOCKET_PATH);
|
|
1705
|
-
if (!IS_WIN) { try { fs.chmodSync(SOCKET_PATH, 0o600); } catch {} }
|
|
1706
|
-
writeMessage({ type: "HOST_READY" });
|
|
1707
|
-
log("Sent HOST_READY to extension");
|
|
1708
|
-
});
|
|
2048
|
+
};
|
|
1709
2049
|
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
2050
|
+
let listenerLifecycle = null;
|
|
2051
|
+
let shuttingDown = false;
|
|
2052
|
+
let exitCode = 0;
|
|
2053
|
+
let exitScheduled = false;
|
|
2054
|
+
function scheduleExit() {
|
|
2055
|
+
if (exitScheduled) return;
|
|
2056
|
+
exitScheduled = true;
|
|
2057
|
+
setTimeout(() => process.exit(exitCode), 50);
|
|
2058
|
+
}
|
|
2059
|
+
function shutdown(code = 0) {
|
|
2060
|
+
exitCode = Math.max(exitCode, code);
|
|
2061
|
+
if (shuttingDown) return;
|
|
2062
|
+
shuttingDown = true;
|
|
2063
|
+
const cleanupPromises = [...connectedSockets].map((socket) => socket.transferCleanup?.() || Promise.resolve());
|
|
2064
|
+
for (const socket of connectedSockets) socket.destroy();
|
|
2065
|
+
pendingRequests.clear(); pendingToolRequests.clear(); activeStreams.clear();
|
|
2066
|
+
Promise.allSettled([...cleanupPromises, Promise.resolve(listenerLifecycle?.shutdown())]).finally(scheduleExit);
|
|
2067
|
+
}
|
|
2068
|
+
function failStartup(error, endpoint) {
|
|
2069
|
+
log(`Listener startup failed (${endpoint}): ${error.message}`);
|
|
2070
|
+
shutdown(1);
|
|
2071
|
+
}
|
|
2072
|
+
async function startListeners() {
|
|
2073
|
+
let endpoint;
|
|
2074
|
+
try {
|
|
2075
|
+
endpoint = process.env.SURF_LISTEN ? parseListenEndpoint(process.env.SURF_LISTEN) : null;
|
|
2076
|
+
listenerLifecycle = createListenerLifecycle({
|
|
2077
|
+
localPath: SOCKET_PATH,
|
|
2078
|
+
tcpEndpoint: endpoint && { host: endpoint.host, port: endpoint.port },
|
|
2079
|
+
handler: handleClient,
|
|
2080
|
+
onReady: () => {
|
|
2081
|
+
if (endpoint) log(`TCP listener listening on ${endpoint.display}`);
|
|
2082
|
+
writeMessage({ type: "HOST_READY" });
|
|
2083
|
+
log("Sent HOST_READY to extension");
|
|
2084
|
+
},
|
|
2085
|
+
onFatal: (error) => failStartup(error, endpoint?.display || process.env.SURF_LISTEN || SOCKET_PATH),
|
|
2086
|
+
});
|
|
2087
|
+
if (shuttingDown) await listenerLifecycle.shutdown();
|
|
2088
|
+
await listenerLifecycle.start();
|
|
2089
|
+
} catch (error) { failStartup(error, endpoint?.display || process.env.SURF_LISTEN || SOCKET_PATH); }
|
|
2090
|
+
}
|
|
2091
|
+
startListeners();
|
|
1713
2092
|
|
|
1714
2093
|
process.on("SIGTERM", () => {
|
|
1715
2094
|
log("SIGTERM received");
|
|
1716
|
-
|
|
1717
|
-
if (!IS_WIN) { try { fs.unlinkSync(SOCKET_PATH); } catch {} }
|
|
1718
|
-
process.exit(0);
|
|
2095
|
+
shutdown();
|
|
1719
2096
|
});
|
|
1720
2097
|
|
|
1721
2098
|
process.on("SIGINT", () => {
|
|
1722
2099
|
log("SIGINT received");
|
|
1723
|
-
|
|
1724
|
-
if (!IS_WIN) { try { fs.unlinkSync(SOCKET_PATH); } catch {} }
|
|
1725
|
-
process.exit(0);
|
|
2100
|
+
shutdown();
|
|
1726
2101
|
});
|
|
1727
2102
|
|
|
1728
2103
|
process.on("uncaughtException", (err) => {
|
|
1729
2104
|
log(`Uncaught exception: ${err.message}\n${err.stack}`);
|
|
1730
|
-
|
|
2105
|
+
shutdown(1);
|
|
1731
2106
|
});
|
|
1732
2107
|
|
|
1733
2108
|
log("Host initialization complete, waiting for connections...");
|
|
2109
|
+
} else {
|
|
2110
|
+
module.exports = { createListenerLifecycle, MAX_CLIENT_FRAME_BYTES };
|
|
2111
|
+
}
|