surf-cli 2.8.0 → 2.9.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 +98 -4
- package/dist/content/index.js +116 -0
- package/dist/content/index.js.map +1 -0
- package/dist/manifest.json +2 -11
- package/dist/options/options.js +3 -3
- package/dist/options/options.js.map +1 -1
- package/dist/service-worker/index.js +261 -61
- package/dist/service-worker/index.js.map +1 -1
- package/native/abort.cjs +65 -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 +47 -31
- package/native/cli.cjs +300 -204
- package/native/client-transport.cjs +168 -0
- package/native/do-executor.cjs +25 -44
- 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 +37 -12
- package/native/host-sessions.cjs +283 -0
- package/native/host.cjs +800 -620
- package/native/listener.cjs +20 -0
- package/native/mcp-server.cjs +60 -65
- package/native/network-export.cjs +113 -0
- package/native/perplexity-client.cjs +46 -17
- 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/package.json +8 -6
- 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 +31 -4
- package/dist/content/accessibility-tree.js +0 -11
- package/dist/content/accessibility-tree.js.map +0 -1
- package/dist/content/visual-indicator.js +0 -111
- package/dist/content/visual-indicator.js.map +0 -1
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,80 @@ 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, 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 MAX_CLIENT_FRAME_BYTES = MAX_FRAME_BYTES;
|
|
31
|
+
const TEST_REQUEST_DEADLINE_MS = process.env.SURF_TEST_MODE === "1" && Number.isFinite(Number(process.env.SURF_TEST_REQUEST_DEADLINE_MS))
|
|
32
|
+
? Number(process.env.SURF_TEST_REQUEST_DEADLINE_MS)
|
|
33
|
+
: null;
|
|
19
34
|
if (IS_WIN) { try { fs.mkdirSync(SURF_TMP, { recursive: true }); } catch {} }
|
|
20
35
|
|
|
36
|
+
// The endpoint passed here is already validated by the caller. Keeping this
|
|
37
|
+
// lifecycle separate lets tests use an ephemeral loopback port without adding
|
|
38
|
+
// a localhost escape hatch to SURF_LISTEN parsing.
|
|
39
|
+
function createListenerLifecycle({ localPath, tcpEndpoint, handler, onReady, onFatal }) {
|
|
40
|
+
const localServer = net.createServer(handler);
|
|
41
|
+
const tcpServer = tcpEndpoint ? net.createServer(handler) : null;
|
|
42
|
+
let shuttingDown = false;
|
|
43
|
+
let startPromise = null;
|
|
44
|
+
const close = (server) => {
|
|
45
|
+
if (!server) return;
|
|
46
|
+
try { server.close(); } catch (error) {
|
|
47
|
+
if (error.code !== "ERR_SERVER_NOT_RUNNING") throw error;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const unlink = () => { if (!IS_WIN) { try { fs.unlinkSync(localPath); } catch {} } };
|
|
51
|
+
const listen = (server, options) => new Promise((resolve, reject) => {
|
|
52
|
+
server.once("error", reject);
|
|
53
|
+
server.listen(options, () => {
|
|
54
|
+
server.removeListener("error", reject);
|
|
55
|
+
if (shuttingDown) { close(server); unlink(); }
|
|
56
|
+
resolve();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
const start = () => {
|
|
60
|
+
if (startPromise) return startPromise;
|
|
61
|
+
startPromise = (async () => {
|
|
62
|
+
try {
|
|
63
|
+
await listen(localServer, localPath);
|
|
64
|
+
if (shuttingDown) return false;
|
|
65
|
+
if (!IS_WIN) { try { fs.chmodSync(localPath, 0o600); } catch {} }
|
|
66
|
+
if (tcpServer) {
|
|
67
|
+
await listen(tcpServer, tcpEndpoint);
|
|
68
|
+
if (shuttingDown) return false;
|
|
69
|
+
}
|
|
70
|
+
onReady();
|
|
71
|
+
return true;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (!shuttingDown) onFatal(error);
|
|
74
|
+
close(localServer); close(tcpServer); unlink();
|
|
75
|
+
return false;
|
|
76
|
+
} finally {
|
|
77
|
+
if (shuttingDown) { close(localServer); close(tcpServer); unlink(); }
|
|
78
|
+
}
|
|
79
|
+
})();
|
|
80
|
+
return startPromise;
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
localServer,
|
|
84
|
+
tcpServer,
|
|
85
|
+
start,
|
|
86
|
+
async shutdown() {
|
|
87
|
+
shuttingDown = true;
|
|
88
|
+
close(localServer); close(tcpServer); unlink();
|
|
89
|
+
if (startPromise) await startPromise;
|
|
90
|
+
unlink();
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
21
95
|
// Cross-platform image resize (macOS: sips, Linux: ImageMagick)
|
|
22
96
|
function resizeImage(filePath, maxSize) {
|
|
23
97
|
const platform = process.platform;
|
|
@@ -54,29 +128,9 @@ function resizeImage(filePath, maxSize) {
|
|
|
54
128
|
}
|
|
55
129
|
}
|
|
56
130
|
|
|
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
|
-
}
|
|
131
|
+
let aiQueue;
|
|
132
|
+
function queueAiRequest(handler, request = requestStorage.getStore()) {
|
|
133
|
+
return aiQueue.enqueue(handler, request);
|
|
80
134
|
}
|
|
81
135
|
const LOG_FILE = path.join(SURF_TMP, "surf-host.log");
|
|
82
136
|
const AUTH_FILE = path.join(os.homedir(), ".pi", "agent", "auth.json");
|
|
@@ -89,7 +143,8 @@ const DEFAULT_RETRY_OPTIONS = {
|
|
|
89
143
|
retryableStatusCodes: [429, 500, 502, 503, 504]
|
|
90
144
|
};
|
|
91
145
|
|
|
92
|
-
async function withRetry(fn, retryOptions = DEFAULT_RETRY_OPTIONS, retryCount = 0) {
|
|
146
|
+
async function withRetry(fn, retryOptions = DEFAULT_RETRY_OPTIONS, retryCount = 0, signal) {
|
|
147
|
+
throwIfAborted(signal);
|
|
93
148
|
try {
|
|
94
149
|
return await fn();
|
|
95
150
|
} catch (error) {
|
|
@@ -125,8 +180,8 @@ async function withRetry(fn, retryOptions = DEFAULT_RETRY_OPTIONS, retryCount =
|
|
|
125
180
|
const jitter = 0.8 + Math.random() * 0.4;
|
|
126
181
|
const delayWithJitter = Math.floor(delay * jitter);
|
|
127
182
|
|
|
128
|
-
await
|
|
129
|
-
return withRetry(fn, retryOptions, retryCount + 1);
|
|
183
|
+
await require("./abort.cjs").abortableDelay(delayWithJitter, signal);
|
|
184
|
+
return withRetry(fn, retryOptions, retryCount + 1, signal);
|
|
130
185
|
}
|
|
131
186
|
}
|
|
132
187
|
|
|
@@ -194,13 +249,14 @@ class GeminiClient {
|
|
|
194
249
|
|
|
195
250
|
async analyze(query, pageContext, options = {}) {
|
|
196
251
|
const mode = options.mode || detectQueryMode(query);
|
|
252
|
+
throwIfAborted(options.signal);
|
|
197
253
|
const promptFn = AI_PROMPTS[mode];
|
|
198
254
|
const prompt = promptFn(query, pageContext);
|
|
199
255
|
|
|
200
256
|
const result = await withRetry(async () => {
|
|
201
257
|
const response = await this.model.generateContent(prompt);
|
|
202
258
|
return response.response.text();
|
|
203
|
-
});
|
|
259
|
+
}, DEFAULT_RETRY_OPTIONS, 0, options.signal);
|
|
204
260
|
|
|
205
261
|
let content = result.trim();
|
|
206
262
|
|
|
@@ -291,57 +347,238 @@ const log = (msg) => {
|
|
|
291
347
|
fs.appendFileSync(LOG_FILE, `${new Date().toISOString()} ${msg}\n`);
|
|
292
348
|
};
|
|
293
349
|
|
|
350
|
+
if (require.main === module) {
|
|
294
351
|
log("Host starting...");
|
|
295
352
|
|
|
296
353
|
if (!IS_WIN) { try { fs.unlinkSync(SOCKET_PATH); } catch {} }
|
|
297
354
|
|
|
298
355
|
const pendingRequests = new Map();
|
|
299
|
-
const pendingToolRequests = new
|
|
356
|
+
const pendingToolRequests = new RequestPendingMap({ getRequest: () => requestStorage.getStore() });
|
|
300
357
|
const activeStreams = new Map();
|
|
358
|
+
const socketContexts = new WeakMap();
|
|
359
|
+
const socketWriters = new WeakMap();
|
|
301
360
|
let requestCounter = 0;
|
|
302
361
|
|
|
303
|
-
function
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
362
|
+
function auditSession(event) {
|
|
363
|
+
const context = event.context;
|
|
364
|
+
const principal = context?.principal;
|
|
365
|
+
const request = event.request;
|
|
366
|
+
log(`SESSION ${JSON.stringify({
|
|
367
|
+
event: event.event,
|
|
368
|
+
outcome: event.outcome,
|
|
369
|
+
principalId: principal?.clientId || "local",
|
|
370
|
+
principalLabel: principal?.label || "local",
|
|
371
|
+
peer: context?.socket?.remoteAddress || "local",
|
|
372
|
+
requestId: request?.id,
|
|
373
|
+
tool: request?.tool,
|
|
374
|
+
elapsedMs: event.elapsedMs,
|
|
375
|
+
})}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
aiQueue = new BoundedAiQueue({
|
|
379
|
+
maxQueued: 8,
|
|
380
|
+
audit: (event) => auditSession(event),
|
|
381
|
+
run: (handler, request) => request
|
|
382
|
+
? requestStorage.run(request, () => handler())
|
|
383
|
+
: handler(),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
function sendSocket(socket, value, options = {}) {
|
|
387
|
+
const writer = socketWriters.get(socket);
|
|
388
|
+
return writer ? writer.send(value, options) : writeFrame(socket, value);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function sendOwnedExtensionMessage(request, message) {
|
|
392
|
+
const cleanupMessage = typeof message?.type === "string" && /(?:CLOSE_TAB|TAB_CLOSE)$/.test(message.type);
|
|
393
|
+
if (request?.hardBoundary) throwIfAborted(request.signal, "Request timed out");
|
|
394
|
+
if (!cleanupMessage) throwIfAborted(request?.signal, "Request cancelled");
|
|
395
|
+
writeMessage(message);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function requestCallExtension(request, tool, message, timeoutMs = 30000, cleanup = false) {
|
|
399
|
+
cleanup = cleanup || tool === "close_tab";
|
|
400
|
+
if (request?.hardBoundary) throwIfAborted(request.signal, "Request timed out");
|
|
401
|
+
if (!cleanup) throwIfAborted(request?.signal, "Request cancelled");
|
|
402
|
+
return new Promise((resolve, reject) => {
|
|
403
|
+
const id = ++requestCounter;
|
|
404
|
+
const timer = setTimeout(() => {
|
|
405
|
+
pendingToolRequests.expire(id, new Error(`Timeout waiting for extension: ${tool}`));
|
|
406
|
+
}, timeoutMs);
|
|
407
|
+
const pending = {
|
|
408
|
+
request,
|
|
409
|
+
cleanup,
|
|
410
|
+
tool,
|
|
411
|
+
resolve: (result) => {
|
|
412
|
+
clearTimeout(timer);
|
|
413
|
+
resolve(result);
|
|
414
|
+
},
|
|
415
|
+
reject: (error) => {
|
|
416
|
+
clearTimeout(timer);
|
|
417
|
+
reject(error);
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
pendingToolRequests.set(id, pending);
|
|
421
|
+
try {
|
|
422
|
+
if (cleanup) writeMessage({ ...message, id });
|
|
423
|
+
else sendOwnedExtensionMessage(request, { ...message, id });
|
|
424
|
+
} catch (error) {
|
|
425
|
+
pendingToolRequests.delete(id);
|
|
426
|
+
reject(error);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const sessionManager = new HostSessionManager({
|
|
432
|
+
audit: auditSession,
|
|
433
|
+
onTimeout(context, request) {
|
|
434
|
+
pendingToolRequests.hardDeadline(request);
|
|
435
|
+
const response = {
|
|
436
|
+
type: "tool_response",
|
|
437
|
+
id: request.id,
|
|
438
|
+
error: { content: [{ type: "text", text: "Request timed out" }] },
|
|
439
|
+
};
|
|
440
|
+
cleanupRequestTransfers(request)
|
|
441
|
+
.then(() => {
|
|
442
|
+
sessionManager.complete(context, request.id, "hard-timeout");
|
|
443
|
+
if (!context.closed) return sendSocket(context.socket, response);
|
|
444
|
+
})
|
|
445
|
+
.catch((error) => log(`Error settling timed-out request: ${error.message}`));
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
async function discardRequestTransfers(message, state) {
|
|
450
|
+
if (!state) return;
|
|
451
|
+
const ids = [];
|
|
452
|
+
for (const entry of message?._surfTransfers?.uploads || []) if (typeof entry?.transferId === "string") ids.push(entry.transferId);
|
|
453
|
+
for (const entry of message?._surfTransfers?.downloads || []) if (typeof entry?.transferId === "string") ids.push(entry.transferId);
|
|
454
|
+
await Promise.all([...new Set(ids)].map((id) => state.discardCompleted(id)));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function applyRequestTransfers(msg, request, transferState, getTransferState) {
|
|
458
|
+
if (!request.context?.isRemote) return;
|
|
459
|
+
const materialized = await materializeRemoteTool({
|
|
460
|
+
tool: request.tool,
|
|
461
|
+
args: msg.params?.args || {},
|
|
462
|
+
metadata: msg._surfTransfers,
|
|
463
|
+
pathRefs: msg._surfPaths || [],
|
|
464
|
+
transferState,
|
|
465
|
+
getTransferState,
|
|
466
|
+
});
|
|
467
|
+
request.transferState = materialized.transferState;
|
|
468
|
+
request.outputTransfers = materialized.outputTransfers;
|
|
469
|
+
request.pathRewrites = materialized.pathRewrites;
|
|
470
|
+
request.transferCleanup = materialized.transferCleanup;
|
|
471
|
+
msg.params = { ...msg.params, args: materialized.args };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function cleanupRequestTransfers(request) {
|
|
475
|
+
if (!request || request.transferCleanupStarted) return request?.transferCleanupPromise;
|
|
476
|
+
request.transferCleanupStarted = true;
|
|
477
|
+
const paths = request.transferCleanup || [];
|
|
478
|
+
request.transferCleanup = [];
|
|
479
|
+
request.transferCleanupPromise = cleanupFilePaths(paths);
|
|
480
|
+
await request.transferCleanupPromise;
|
|
481
|
+
return request.transferCleanupPromise;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function completeOwnedRequest(context, id, outcome) {
|
|
485
|
+
const request = context?.activeRequest;
|
|
486
|
+
if (!request || request.id !== id) return Promise.resolve();
|
|
487
|
+
if (request.pendingEntries?.size && !request.hardBoundary) {
|
|
488
|
+
if (request.completionRequested) return request.completionPromise;
|
|
489
|
+
request.completionRequested = true;
|
|
490
|
+
request.completionOutcome = outcome;
|
|
491
|
+
request.completionPromise = new Promise((resolve) => {
|
|
492
|
+
pendingToolRequests.onDrain(request, () => {
|
|
493
|
+
sessionManager.complete(context, id, request.completionOutcome);
|
|
494
|
+
resolve();
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
return request.completionPromise;
|
|
310
498
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
499
|
+
sessionManager.complete(context, id, outcome);
|
|
500
|
+
return Promise.resolve();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function sendRequestDownloads(context, request, result) {
|
|
504
|
+
if (!request) return result;
|
|
505
|
+
let rewritten = result;
|
|
506
|
+
for (const output of request.outputTransfers || []) {
|
|
507
|
+
await streamFileDownload({
|
|
508
|
+
writer: { send: (frame) => sendSocket(context.socket, frame) },
|
|
509
|
+
state: request.transferState,
|
|
510
|
+
filePath: output.path,
|
|
511
|
+
transferId: output.transferId,
|
|
512
|
+
original: output.original,
|
|
513
|
+
});
|
|
316
514
|
}
|
|
515
|
+
rewritten = rewriteTransferPaths(rewritten, request.pathRewrites || []);
|
|
516
|
+
return rewritten;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function sendToolResponse(socket, id, result, error) {
|
|
520
|
+
const context = socketContexts.get(socket);
|
|
521
|
+
if (context && !sessionManager.canRespond(context, id)) return;
|
|
522
|
+
const request = context?.activeRequest;
|
|
523
|
+
let finalError = error;
|
|
524
|
+
(async () => {
|
|
525
|
+
let output = result;
|
|
526
|
+
try {
|
|
527
|
+
if (!error) output = await sendRequestDownloads(context, request, result);
|
|
528
|
+
} catch (transferFailure) {
|
|
529
|
+
finalError = transferFailure.message;
|
|
530
|
+
}
|
|
531
|
+
if (finalError && request) {
|
|
532
|
+
finalError = rewriteTransferPaths(finalError, request.pathRewrites || []);
|
|
533
|
+
}
|
|
534
|
+
await cleanupRequestTransfers(request);
|
|
535
|
+
if (request?.settled) return;
|
|
536
|
+
const outcome = request?.signal.aborted
|
|
537
|
+
? (request.tombstoned ? "cleanup-settled" : "cancelled")
|
|
538
|
+
: finalError ? "error" : "completed";
|
|
539
|
+
await completeOwnedRequest(context, id, outcome);
|
|
540
|
+
const response = { type: "tool_response", id };
|
|
541
|
+
if (finalError) response.error = { content: [{ type: "text", text: finalError }] };
|
|
542
|
+
else response.result = { content: formatToolContent(output, log, { suppressImages: Boolean(context?.isRemote) }) };
|
|
543
|
+
if (!context?.closed) await sendSocket(socket, response);
|
|
544
|
+
})().catch((sendError) => log(`Error sending tool_response: ${sendError.message}`));
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function stopActiveStream(streamId, { notifyExtension = true } = {}) {
|
|
548
|
+
const stream = activeStreams.get(streamId);
|
|
549
|
+
if (!stream) return;
|
|
550
|
+
activeStreams.delete(streamId);
|
|
551
|
+
sessionManager.stopStream(socketContexts.get(stream.socket));
|
|
552
|
+
if (notifyExtension) writeMessage({ type: "STREAM_STOP", streamId });
|
|
317
553
|
}
|
|
318
554
|
|
|
319
555
|
function handleStreamRequest(msg, socket) {
|
|
320
556
|
const { streamType, options, id: originalId } = msg;
|
|
321
557
|
const tabId = msg.tabId;
|
|
322
558
|
const streamId = ++requestCounter;
|
|
323
|
-
|
|
559
|
+
|
|
324
560
|
activeStreams.set(streamId, {
|
|
325
561
|
socket,
|
|
326
562
|
originalId,
|
|
327
563
|
streamType,
|
|
328
564
|
});
|
|
329
|
-
|
|
565
|
+
|
|
330
566
|
writeMessage({
|
|
331
567
|
type: streamType,
|
|
332
568
|
streamId,
|
|
333
569
|
options: options || {},
|
|
334
570
|
tabId,
|
|
335
571
|
});
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
572
|
+
|
|
573
|
+
sendSocket(socket, { type: "stream_started", streamId }, { stream: true }).catch((error) => {
|
|
574
|
+
log(`Error sending stream_started: ${error.message}`);
|
|
575
|
+
stopActiveStream(streamId);
|
|
576
|
+
socket.destroy(error);
|
|
577
|
+
});
|
|
342
578
|
}
|
|
343
579
|
|
|
344
|
-
function handleToolRequest(msg, socket) {
|
|
580
|
+
function handleToolRequest(msg, socket, requestContext = requestStorage.getStore()) {
|
|
581
|
+
const writeMessage = (message) => sendOwnedExtensionMessage(requestContext, message);
|
|
345
582
|
const { method, params } = msg;
|
|
346
583
|
const originalId = msg.id || null;
|
|
347
584
|
|
|
@@ -383,14 +620,14 @@ function handleToolRequest(msg, socket) {
|
|
|
383
620
|
}
|
|
384
621
|
|
|
385
622
|
if (extensionMsg.type === "LOCAL_WAIT") {
|
|
386
|
-
|
|
387
|
-
sendToolResponse(socket, originalId, { success: true }, null)
|
|
388
|
-
|
|
623
|
+
require("./abort.cjs").abortableDelay(extensionMsg.seconds * 1000, requestContext.signal)
|
|
624
|
+
.then(() => sendToolResponse(socket, originalId, { success: true }, null))
|
|
625
|
+
.catch((error) => sendToolResponse(socket, originalId, null, error.message));
|
|
389
626
|
return;
|
|
390
627
|
}
|
|
391
628
|
|
|
392
629
|
if (extensionMsg.type === "BATCH_EXECUTE") {
|
|
393
|
-
executeBatch(extensionMsg.actions, extensionMsg.tabId, socket, originalId);
|
|
630
|
+
executeBatch(extensionMsg.actions, extensionMsg.tabId, socket, originalId, requestContext);
|
|
394
631
|
return;
|
|
395
632
|
}
|
|
396
633
|
|
|
@@ -406,46 +643,23 @@ function handleToolRequest(msg, socket) {
|
|
|
406
643
|
return;
|
|
407
644
|
}
|
|
408
645
|
|
|
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
|
-
}
|
|
646
|
+
requestCallExtension(
|
|
647
|
+
requestContext,
|
|
648
|
+
"read_page",
|
|
649
|
+
{ type: "READ_PAGE", options: { filter: "interactive" }, tabId: extensionMsg.tabId },
|
|
650
|
+
45000,
|
|
651
|
+
).then(async (pageResult) => {
|
|
652
|
+
if (pageResult.error) throw new Error(`Failed to read page: ${pageResult.error}`);
|
|
653
|
+
const pageContent = pageResult.pageContent || "";
|
|
654
|
+
if (!pageContent) throw new Error("No page content available");
|
|
655
|
+
const gemini = getGeminiClient(apiKey);
|
|
656
|
+
const result = await gemini.analyze(extensionMsg.query, pageContent, { mode: extensionMsg.mode, signal: requestContext.signal });
|
|
657
|
+
return result.mode === "find"
|
|
658
|
+
? { ref: result.content === "NOT_FOUND" ? null : result.content, mode: result.mode, aiResult: true }
|
|
659
|
+
: { content: result.content, mode: result.mode, aiResult: true };
|
|
660
|
+
}).then((result) => sendToolResponse(socket, originalId, result, null)).catch((err) => {
|
|
661
|
+
sendToolResponse(socket, originalId, null, err.message);
|
|
447
662
|
});
|
|
448
|
-
writeMessage({ type: "READ_PAGE", options: { filter: "interactive" }, tabId: extensionMsg.tabId, id: pageRequestId });
|
|
449
663
|
return;
|
|
450
664
|
}
|
|
451
665
|
|
|
@@ -455,16 +669,12 @@ function handleToolRequest(msg, socket) {
|
|
|
455
669
|
queueAiRequest(async () => {
|
|
456
670
|
let pageContext = null;
|
|
457
671
|
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
|
-
});
|
|
672
|
+
const pageResult = await requestCallExtension(
|
|
673
|
+
requestContext,
|
|
674
|
+
"read_page",
|
|
675
|
+
{ type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId },
|
|
676
|
+
45000,
|
|
677
|
+
);
|
|
468
678
|
if (pageResult && !pageResult.error) {
|
|
469
679
|
pageContext = {
|
|
470
680
|
url: pageResult.url,
|
|
@@ -480,69 +690,36 @@ function handleToolRequest(msg, socket) {
|
|
|
480
690
|
|
|
481
691
|
const result = await chatgptClient.query({
|
|
482
692
|
prompt: fullPrompt,
|
|
693
|
+
signal: requestContext.signal,
|
|
483
694
|
model,
|
|
484
695
|
file,
|
|
485
696
|
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
|
-
}),
|
|
697
|
+
getCookies: () => requestCallExtension(
|
|
698
|
+
requestContext,
|
|
699
|
+
"get_cookies",
|
|
700
|
+
{ type: "GET_CHATGPT_COOKIES" },
|
|
701
|
+
),
|
|
702
|
+
createTab: () => requestCallExtension(
|
|
703
|
+
requestContext,
|
|
704
|
+
"create_tab",
|
|
705
|
+
{ type: "CHATGPT_NEW_TAB" },
|
|
706
|
+
),
|
|
707
|
+
closeTab: (tabIdToClose) => requestCallExtension(requestContext, "close_tab", { type: "CHATGPT_CLOSE_TAB", tabId: tabIdToClose }, 45000, true),
|
|
708
|
+
cdpEvaluate: (tabId, expression) => requestCallExtension(
|
|
709
|
+
requestContext,
|
|
710
|
+
"cdp_evaluate",
|
|
711
|
+
{ type: "CHATGPT_EVALUATE", tabId, expression },
|
|
712
|
+
),
|
|
713
|
+
cdpCommand: (tabId, method, params) => requestCallExtension(
|
|
714
|
+
requestContext,
|
|
715
|
+
"cdp_command",
|
|
716
|
+
{ type: "CHATGPT_CDP_COMMAND", tabId, method, params },
|
|
717
|
+
),
|
|
718
|
+
uploadFile: (tabId, filePaths) => requestCallExtension(
|
|
719
|
+
requestContext,
|
|
720
|
+
"upload_file",
|
|
721
|
+
buildProviderUploadMessage("chatgpt", tabId, filePaths),
|
|
722
|
+
),
|
|
546
723
|
log: (msg) => log(`[chatgpt] ${msg}`)
|
|
547
724
|
});
|
|
548
725
|
|
|
@@ -566,16 +743,12 @@ function handleToolRequest(msg, socket) {
|
|
|
566
743
|
queueAiRequest(async () => {
|
|
567
744
|
let pageContext = null;
|
|
568
745
|
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
|
-
});
|
|
746
|
+
const pageResult = await requestCallExtension(
|
|
747
|
+
requestContext,
|
|
748
|
+
"read_page",
|
|
749
|
+
{ type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId },
|
|
750
|
+
45000,
|
|
751
|
+
);
|
|
579
752
|
if (pageResult && !pageResult.error) {
|
|
580
753
|
pageContext = {
|
|
581
754
|
url: pageResult.url,
|
|
@@ -591,49 +764,26 @@ function handleToolRequest(msg, socket) {
|
|
|
591
764
|
|
|
592
765
|
const result = await perplexityClient.query({
|
|
593
766
|
prompt: fullPrompt,
|
|
767
|
+
signal: requestContext.signal,
|
|
594
768
|
mode: mode || 'search',
|
|
595
769
|
model,
|
|
596
770
|
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
|
-
}),
|
|
771
|
+
createTab: () => requestCallExtension(
|
|
772
|
+
requestContext,
|
|
773
|
+
"create_tab",
|
|
774
|
+
{ type: "PERPLEXITY_NEW_TAB" },
|
|
775
|
+
),
|
|
776
|
+
closeTab: (tabIdToClose) => requestCallExtension(requestContext, "close_tab", { type: "PERPLEXITY_CLOSE_TAB", tabId: tabIdToClose }, 45000, true),
|
|
777
|
+
cdpEvaluate: (tabId, expression) => requestCallExtension(
|
|
778
|
+
requestContext,
|
|
779
|
+
"cdp_evaluate",
|
|
780
|
+
{ type: "PERPLEXITY_EVALUATE", tabId, expression },
|
|
781
|
+
),
|
|
782
|
+
cdpCommand: (tabId, method, params) => requestCallExtension(
|
|
783
|
+
requestContext,
|
|
784
|
+
"cdp_command",
|
|
785
|
+
{ type: "PERPLEXITY_CDP_COMMAND", tabId, method, params },
|
|
786
|
+
),
|
|
637
787
|
log: (msg) => log(`[perplexity] ${msg}`)
|
|
638
788
|
});
|
|
639
789
|
|
|
@@ -661,16 +811,12 @@ function handleToolRequest(msg, socket) {
|
|
|
661
811
|
// 1. Get page context if requested
|
|
662
812
|
let pageContext = null;
|
|
663
813
|
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
|
-
});
|
|
814
|
+
const pageResult = await requestCallExtension(
|
|
815
|
+
requestContext,
|
|
816
|
+
"get_page_text",
|
|
817
|
+
{ type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId },
|
|
818
|
+
45000,
|
|
819
|
+
);
|
|
674
820
|
if (pageResult && !pageResult.error) {
|
|
675
821
|
pageContext = {
|
|
676
822
|
url: pageResult.url,
|
|
@@ -688,7 +834,8 @@ function handleToolRequest(msg, socket) {
|
|
|
688
834
|
// 3. Call Gemini client
|
|
689
835
|
const result = await geminiClient.query({
|
|
690
836
|
prompt: fullPrompt,
|
|
691
|
-
|
|
837
|
+
signal: requestContext.signal,
|
|
838
|
+
model: model || "gemini-3.1-pro",
|
|
692
839
|
file,
|
|
693
840
|
generateImage,
|
|
694
841
|
editImage,
|
|
@@ -696,67 +843,32 @@ function handleToolRequest(msg, socket) {
|
|
|
696
843
|
youtube,
|
|
697
844
|
aspectRatio,
|
|
698
845
|
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
|
-
}),
|
|
846
|
+
getCookies: () => requestCallExtension(
|
|
847
|
+
requestContext,
|
|
848
|
+
"get_cookies",
|
|
849
|
+
{ type: "GET_GOOGLE_COOKIES" },
|
|
850
|
+
),
|
|
851
|
+
createTab: () => requestCallExtension(
|
|
852
|
+
requestContext,
|
|
853
|
+
"create_tab",
|
|
854
|
+
{ type: "GEMINI_NEW_TAB" },
|
|
855
|
+
),
|
|
856
|
+
closeTab: (tabIdToClose) => requestCallExtension(requestContext, "close_tab", { type: "GEMINI_CLOSE_TAB", tabId: tabIdToClose }, 45000, true),
|
|
857
|
+
jsEval: (tabId, code) => requestCallExtension(
|
|
858
|
+
requestContext,
|
|
859
|
+
"js_eval",
|
|
860
|
+
{ type: "EXECUTE_JAVASCRIPT", tabId, code },
|
|
861
|
+
),
|
|
862
|
+
uploadFile: (tabId, filePaths) => requestCallExtension(
|
|
863
|
+
requestContext,
|
|
864
|
+
"upload_file",
|
|
865
|
+
buildProviderUploadMessage("gemini", tabId, filePaths),
|
|
866
|
+
),
|
|
867
|
+
fetchUrl: (url) => requestCallExtension(
|
|
868
|
+
requestContext,
|
|
869
|
+
"fetch_url",
|
|
870
|
+
{ type: "GEMINI_FETCH_URL", url },
|
|
871
|
+
),
|
|
760
872
|
log: (msg) => log(`[gemini] ${msg}`)
|
|
761
873
|
});
|
|
762
874
|
|
|
@@ -785,16 +897,12 @@ function handleToolRequest(msg, socket) {
|
|
|
785
897
|
// 1. Get page context if requested
|
|
786
898
|
let pageContext = null;
|
|
787
899
|
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
|
-
});
|
|
900
|
+
const pageResult = await requestCallExtension(
|
|
901
|
+
requestContext,
|
|
902
|
+
"get_page_text",
|
|
903
|
+
{ type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId },
|
|
904
|
+
45000,
|
|
905
|
+
);
|
|
798
906
|
if (pageResult && !pageResult.error) {
|
|
799
907
|
pageContext = {
|
|
800
908
|
url: pageResult.url,
|
|
@@ -812,59 +920,31 @@ function handleToolRequest(msg, socket) {
|
|
|
812
920
|
// 3. Call Grok client
|
|
813
921
|
const result = await grokClient.query({
|
|
814
922
|
prompt: fullPrompt,
|
|
923
|
+
signal: requestContext.signal,
|
|
815
924
|
model: model,
|
|
816
925
|
deepSearch: deepSearch || false,
|
|
817
926
|
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
|
-
}),
|
|
927
|
+
getCookies: () => requestCallExtension(
|
|
928
|
+
requestContext,
|
|
929
|
+
"get_cookies",
|
|
930
|
+
{ type: "GET_TWITTER_COOKIES" },
|
|
931
|
+
),
|
|
932
|
+
createTab: () => requestCallExtension(
|
|
933
|
+
requestContext,
|
|
934
|
+
"create_tab",
|
|
935
|
+
{ type: "GROK_NEW_TAB" },
|
|
936
|
+
),
|
|
937
|
+
closeTab: (tabIdToClose) => requestCallExtension(requestContext, "close_tab", { type: "GROK_CLOSE_TAB", tabId: tabIdToClose }, 45000, true),
|
|
938
|
+
cdpEvaluate: (tabId, expression) => requestCallExtension(
|
|
939
|
+
requestContext,
|
|
940
|
+
"cdp_evaluate",
|
|
941
|
+
{ type: "GROK_EVALUATE", tabId, expression },
|
|
942
|
+
),
|
|
943
|
+
cdpCommand: (tabId, method, params) => requestCallExtension(
|
|
944
|
+
requestContext,
|
|
945
|
+
"cdp_command",
|
|
946
|
+
{ type: "GROK_CDP_COMMAND", tabId, method, params },
|
|
947
|
+
),
|
|
868
948
|
log: (msg) => log(`[grok] ${msg}`)
|
|
869
949
|
});
|
|
870
950
|
|
|
@@ -903,46 +983,29 @@ function handleToolRequest(msg, socket) {
|
|
|
903
983
|
|
|
904
984
|
queueAiRequest(async () => {
|
|
905
985
|
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
|
-
}),
|
|
986
|
+
signal: requestContext.signal,
|
|
987
|
+
getCookies: () => requestCallExtension(
|
|
988
|
+
requestContext,
|
|
989
|
+
"get_cookies",
|
|
990
|
+
{ type: "GET_TWITTER_COOKIES" },
|
|
991
|
+
),
|
|
992
|
+
createTab: () => requestCallExtension(
|
|
993
|
+
requestContext,
|
|
994
|
+
"create_tab",
|
|
995
|
+
{ type: "GROK_NEW_TAB" },
|
|
996
|
+
),
|
|
997
|
+
closeTab: (tabIdToClose) => requestCallExtension(
|
|
998
|
+
requestContext,
|
|
999
|
+
"close_tab",
|
|
1000
|
+
{ type: "GROK_CLOSE_TAB", tabId: tabIdToClose },
|
|
1001
|
+
45000,
|
|
1002
|
+
true,
|
|
1003
|
+
),
|
|
1004
|
+
cdpEvaluate: (tabId, expression) => requestCallExtension(
|
|
1005
|
+
requestContext,
|
|
1006
|
+
"cdp_evaluate",
|
|
1007
|
+
{ type: "GROK_EVALUATE", tabId, expression },
|
|
1008
|
+
),
|
|
946
1009
|
log: (msg) => log(`[grok:validate] ${msg}`)
|
|
947
1010
|
});
|
|
948
1011
|
|
|
@@ -987,30 +1050,12 @@ function handleToolRequest(msg, socket) {
|
|
|
987
1050
|
queueAiRequest(async () => {
|
|
988
1051
|
const EXT_CALL_TIMEOUT_MS = 30000;
|
|
989
1052
|
|
|
990
|
-
const callExtension = (toolName, msg, timeoutMs = EXT_CALL_TIMEOUT_MS) =>
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
if (msg && msg.type === "AISTUDIO_NEW_TAB") {
|
|
1053
|
+
const callExtension = (toolName, msg, timeoutMs = EXT_CALL_TIMEOUT_MS) => {
|
|
1054
|
+
if (msg?.type === "AISTUDIO_NEW_TAB") {
|
|
994
1055
|
log(`[aistudio] Opening tab: ${(msg.url || "https://aistudio.google.com/prompts/new_chat")}`);
|
|
995
1056
|
}
|
|
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
|
-
});
|
|
1057
|
+
return requestCallExtension(requestContext, toolName, msg, timeoutMs);
|
|
1058
|
+
};
|
|
1014
1059
|
|
|
1015
1060
|
// 1. Get page context if requested
|
|
1016
1061
|
let pageContext = null;
|
|
@@ -1044,6 +1089,7 @@ function handleToolRequest(msg, socket) {
|
|
|
1044
1089
|
// 3. Call AI Studio client
|
|
1045
1090
|
const result = await aistudioClient.query({
|
|
1046
1091
|
prompt: fullPrompt,
|
|
1092
|
+
signal: requestContext.signal,
|
|
1047
1093
|
model: model || undefined,
|
|
1048
1094
|
timeout: timeout || 300000,
|
|
1049
1095
|
getCookies: () => callExtension("get_cookies", { type: "GET_GOOGLE_COOKIES" }, 45000),
|
|
@@ -1102,33 +1148,16 @@ function handleToolRequest(msg, socket) {
|
|
|
1102
1148
|
queueAiRequest(async () => {
|
|
1103
1149
|
const EXT_CALL_TIMEOUT_MS = 30000;
|
|
1104
1150
|
|
|
1105
|
-
const callExtension = (toolName, msg, timeoutMs = EXT_CALL_TIMEOUT_MS) =>
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
if (msg && msg.type === "AISTUDIO_NEW_TAB") {
|
|
1151
|
+
const callExtension = (toolName, msg, timeoutMs = EXT_CALL_TIMEOUT_MS) => {
|
|
1152
|
+
if (msg?.type === "AISTUDIO_NEW_TAB") {
|
|
1109
1153
|
log(`[aistudio] Opening tab: ${(msg.url || "https://aistudio.google.com/apps")}`);
|
|
1110
1154
|
}
|
|
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
|
-
});
|
|
1155
|
+
return requestCallExtension(requestContext, toolName, msg, timeoutMs);
|
|
1156
|
+
};
|
|
1129
1157
|
|
|
1130
1158
|
const result = await aistudioBuild.build({
|
|
1131
1159
|
prompt: query,
|
|
1160
|
+
signal: requestContext.signal,
|
|
1132
1161
|
model: model || undefined,
|
|
1133
1162
|
output,
|
|
1134
1163
|
keepOpen,
|
|
@@ -1179,6 +1208,7 @@ function handleToolRequest(msg, socket) {
|
|
|
1179
1208
|
let lastError = null;
|
|
1180
1209
|
|
|
1181
1210
|
const sendNextKey = () => {
|
|
1211
|
+
if (requestContext.signal.aborted) return;
|
|
1182
1212
|
if (completed >= repeat) {
|
|
1183
1213
|
if (lastError) {
|
|
1184
1214
|
sendToolResponse(socket, originalId, null, `Key repeat failed: ${lastError}`);
|
|
@@ -1187,18 +1217,15 @@ function handleToolRequest(msg, socket) {
|
|
|
1187
1217
|
}
|
|
1188
1218
|
return;
|
|
1189
1219
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
socket: null,
|
|
1193
|
-
originalId: null,
|
|
1220
|
+
requestCallExtension(
|
|
1221
|
+
requestContext,
|
|
1194
1222
|
tool,
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
});
|
|
1201
|
-
writeMessage({ type: "EXECUTE_KEY", key, tabId: tid, id });
|
|
1223
|
+
{ type: "EXECUTE_KEY", key, tabId: tid },
|
|
1224
|
+
).then((result) => {
|
|
1225
|
+
if (result.error) lastError = result.error;
|
|
1226
|
+
completed++;
|
|
1227
|
+
return require("./abort.cjs").abortableDelay(50, requestContext.signal);
|
|
1228
|
+
}).then(sendNextKey).catch((error) => sendToolResponse(socket, originalId, null, error.message));
|
|
1202
1229
|
};
|
|
1203
1230
|
sendNextKey();
|
|
1204
1231
|
return;
|
|
@@ -1206,23 +1233,25 @@ function handleToolRequest(msg, socket) {
|
|
|
1206
1233
|
|
|
1207
1234
|
if (extensionMsg.type === "NAMED_TAB_SWITCH" || extensionMsg.type === "NAMED_TAB_CLOSE") {
|
|
1208
1235
|
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 });
|
|
1236
|
+
requestCallExtension(
|
|
1237
|
+
requestContext,
|
|
1238
|
+
"tabs_get_by_name",
|
|
1239
|
+
{ type: "TABS_GET_BY_NAME", name },
|
|
1240
|
+
).then((result) => {
|
|
1241
|
+
if (result.error || !result.tabId) {
|
|
1242
|
+
throw new Error(result.error || `No tab found with name "${name}"`);
|
|
1223
1243
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1244
|
+
const actionType = opType === "NAMED_TAB_SWITCH" ? "SWITCH_TAB" : "CLOSE_TAB";
|
|
1245
|
+
const actionTool = opType === "NAMED_TAB_SWITCH" ? "switch_tab" : "close_tab";
|
|
1246
|
+
return requestCallExtension(
|
|
1247
|
+
requestContext,
|
|
1248
|
+
actionTool,
|
|
1249
|
+
{ type: actionType, tabId: result.tabId },
|
|
1250
|
+
30000,
|
|
1251
|
+
actionTool === "close_tab",
|
|
1252
|
+
);
|
|
1253
|
+
}).then((result) => sendToolResponse(socket, originalId, result, result?.error || null))
|
|
1254
|
+
.catch((error) => sendToolResponse(socket, originalId, null, error.message));
|
|
1226
1255
|
return;
|
|
1227
1256
|
}
|
|
1228
1257
|
|
|
@@ -1232,7 +1261,11 @@ function handleToolRequest(msg, socket) {
|
|
|
1232
1261
|
originalId,
|
|
1233
1262
|
tool,
|
|
1234
1263
|
savePath: extensionMsg.savePath || args?.savePath,
|
|
1235
|
-
autoScreenshot: args?.autoScreenshot,
|
|
1264
|
+
autoScreenshot: args?.autoScreenshot === true,
|
|
1265
|
+
autoScreenshotOutput: args?.autoScreenshotOutput,
|
|
1266
|
+
networkExport: extensionMsg.type === "EXPORT_NETWORK_REQUESTS",
|
|
1267
|
+
networkExportPath: args?.output,
|
|
1268
|
+
networkExportFormat: extensionMsg.har ? "har" : extensionMsg.jsonl ? "jsonl" : "json",
|
|
1236
1269
|
fullRes: extensionMsg.fullRes || args?.fullRes,
|
|
1237
1270
|
maxSize: extensionMsg.maxSize || args?.maxSize,
|
|
1238
1271
|
tabId: extensionMsg.tabId || tabId
|
|
@@ -1245,12 +1278,14 @@ function handleToolRequest(msg, socket) {
|
|
|
1245
1278
|
writeMessage(finalMsg);
|
|
1246
1279
|
}
|
|
1247
1280
|
|
|
1248
|
-
function executeBatch(actions, tabId, socket, originalId) {
|
|
1281
|
+
function executeBatch(actions, tabId, socket, originalId, requestContext = requestStorage.getStore()) {
|
|
1282
|
+
const writeMessage = (message) => sendOwnedExtensionMessage(requestContext, message);
|
|
1249
1283
|
const results = [];
|
|
1250
1284
|
const DELAY_MS = 100;
|
|
1251
1285
|
let currentIndex = 0;
|
|
1252
1286
|
|
|
1253
1287
|
function executeNextAction() {
|
|
1288
|
+
if (requestContext.signal.aborted) return;
|
|
1254
1289
|
if (currentIndex >= actions.length) {
|
|
1255
1290
|
sendToolResponse(socket, originalId, {
|
|
1256
1291
|
success: true,
|
|
@@ -1281,16 +1316,14 @@ function executeBatch(actions, tabId, socket, originalId) {
|
|
|
1281
1316
|
if (extensionMsg.type === "LOCAL_WAIT") {
|
|
1282
1317
|
results.push({ index: currentIndex, type: action.type, success: true });
|
|
1283
1318
|
currentIndex++;
|
|
1284
|
-
|
|
1319
|
+
require("./abort.cjs").abortableDelay(extensionMsg.seconds * 1000, requestContext.signal)
|
|
1320
|
+
.then(executeNextAction)
|
|
1321
|
+
.catch((error) => sendToolResponse(socket, originalId, null, error.message));
|
|
1285
1322
|
return;
|
|
1286
1323
|
}
|
|
1287
1324
|
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
socket: null,
|
|
1291
|
-
originalId: null,
|
|
1292
|
-
tool: toolName,
|
|
1293
|
-
onComplete: (result) => {
|
|
1325
|
+
requestCallExtension(requestContext, toolName, extensionMsg, 30000)
|
|
1326
|
+
.then((result) => {
|
|
1294
1327
|
if (result.error) {
|
|
1295
1328
|
results.push({ index: currentIndex, type: action.type, success: false, error: result.error });
|
|
1296
1329
|
sendToolResponse(socket, originalId, {
|
|
@@ -1302,15 +1335,12 @@ function executeBatch(actions, tabId, socket, originalId) {
|
|
|
1302
1335
|
}, null);
|
|
1303
1336
|
return;
|
|
1304
1337
|
}
|
|
1305
|
-
|
|
1306
1338
|
results.push({ index: currentIndex, type: action.type, success: true });
|
|
1307
1339
|
currentIndex++;
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
writeMessage({ ...extensionMsg, id });
|
|
1340
|
+
return require("./abort.cjs").abortableDelay(DELAY_MS, requestContext.signal)
|
|
1341
|
+
.then(executeNextAction);
|
|
1342
|
+
})
|
|
1343
|
+
.catch((error) => sendToolResponse(socket, originalId, null, error.message));
|
|
1314
1344
|
}
|
|
1315
1345
|
|
|
1316
1346
|
executeNextAction();
|
|
@@ -1371,7 +1401,7 @@ function processInput() {
|
|
|
1371
1401
|
|
|
1372
1402
|
try {
|
|
1373
1403
|
const msg = JSON.parse(jsonStr);
|
|
1374
|
-
log(`Received from extension: ${
|
|
1404
|
+
log(`Received from extension: ${msg.type || "unknown"}${msg.id !== undefined ? ` id=${msg.id}` : ""}`);
|
|
1375
1405
|
|
|
1376
1406
|
if (msg.type === "GET_AUTH") {
|
|
1377
1407
|
log("Handling GET_AUTH from extension");
|
|
@@ -1405,24 +1435,24 @@ function processInput() {
|
|
|
1405
1435
|
if (msg.type === "STREAM_EVENT") {
|
|
1406
1436
|
const stream = activeStreams.get(msg.streamId);
|
|
1407
1437
|
if (stream) {
|
|
1408
|
-
|
|
1409
|
-
stream
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
writeMessage({ type: "STREAM_STOP", streamId: msg.streamId });
|
|
1414
|
-
}
|
|
1438
|
+
sendSocket(stream.socket, msg.event, { stream: true }).catch((error) => {
|
|
1439
|
+
log(`Error forwarding stream event: ${error.message}`);
|
|
1440
|
+
stopActiveStream(msg.streamId);
|
|
1441
|
+
stream.socket.destroy(error);
|
|
1442
|
+
});
|
|
1415
1443
|
}
|
|
1416
1444
|
return;
|
|
1417
1445
|
}
|
|
1418
|
-
|
|
1446
|
+
|
|
1419
1447
|
if (msg.type === "STREAM_ERROR") {
|
|
1420
1448
|
const stream = activeStreams.get(msg.streamId);
|
|
1421
1449
|
if (stream) {
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1450
|
+
sendSocket(stream.socket, { error: msg.error }, { stream: true })
|
|
1451
|
+
.catch((error) => {
|
|
1452
|
+
log(`Error forwarding stream error: ${error.message}`);
|
|
1453
|
+
stream.socket.destroy(error);
|
|
1454
|
+
})
|
|
1455
|
+
.finally(() => stopActiveStream(msg.streamId));
|
|
1426
1456
|
}
|
|
1427
1457
|
return;
|
|
1428
1458
|
}
|
|
@@ -1430,19 +1460,41 @@ function processInput() {
|
|
|
1430
1460
|
|
|
1431
1461
|
if (msg.id && pendingToolRequests.has(msg.id)) {
|
|
1432
1462
|
const pending = pendingToolRequests.get(msg.id);
|
|
1463
|
+
if (pending.request?.signal.aborted || pending.request?.tombstoned) {
|
|
1464
|
+
const request = pending.request;
|
|
1465
|
+
const topLevelResponse = !pending.resolve && !pending.onComplete;
|
|
1466
|
+
pendingToolRequests.resolve(msg.id, msg);
|
|
1467
|
+
if (topLevelResponse && request?.context) {
|
|
1468
|
+
completeOwnedRequest(request.context, request.id, "cleanup-settled");
|
|
1469
|
+
}
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
if (pending.resolve || pending.onComplete) {
|
|
1473
|
+
pendingToolRequests.resolve(msg.id, msg);
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1433
1476
|
pendingToolRequests.delete(msg.id);
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
pending.onComplete(msg);
|
|
1437
|
-
} else {
|
|
1477
|
+
{
|
|
1478
|
+
|
|
1438
1479
|
const { socket, originalId, savePath, autoScreenshot, tabId: storedTabId } = pending;
|
|
1439
1480
|
const tabId = storedTabId || msg._resolvedTabId;
|
|
1481
|
+
const failAutoScreenshot = (message) => pending.autoScreenshotOutput
|
|
1482
|
+
? sendToolResponse(socket, originalId, null, `Auto-screenshot failed: ${message}`)
|
|
1483
|
+
: sendToolResponse(socket, originalId, { ...msg, autoScreenshotError: message }, null);
|
|
1440
1484
|
|
|
1441
|
-
if (
|
|
1485
|
+
if (pending.networkExport && Array.isArray(msg.entries)) {
|
|
1486
|
+
try {
|
|
1487
|
+
const exportResult = writeNetworkExport(pending.networkExportPath, msg.entries, pending.networkExportFormat);
|
|
1488
|
+
sendToolResponse(socket, originalId, exportResult, null);
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
sendToolResponse(socket, originalId, null, `Failed to export network requests: ${error.message}`);
|
|
1491
|
+
}
|
|
1492
|
+
} else if (savePath && msg.base64) {
|
|
1442
1493
|
try {
|
|
1443
1494
|
const dir = path.dirname(savePath);
|
|
1444
1495
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1445
|
-
fs.writeFileSync(savePath, Buffer.from(msg.base64, "base64"));
|
|
1496
|
+
fs.writeFileSync(savePath, Buffer.from(msg.base64, "base64"), { mode: 0o600 });
|
|
1497
|
+
try { fs.chmodSync(savePath, 0o600); } catch {}
|
|
1446
1498
|
const origWidth = msg.width || 0;
|
|
1447
1499
|
const origHeight = msg.height || 0;
|
|
1448
1500
|
const maxSize = pending.maxSize || 1200;
|
|
@@ -1465,8 +1517,7 @@ function processInput() {
|
|
|
1465
1517
|
}
|
|
1466
1518
|
} else if (autoScreenshot && tabId && !msg.error && !msg.base64) {
|
|
1467
1519
|
|
|
1468
|
-
const
|
|
1469
|
-
const screenshotPath = path.join(SURF_TMP, `pi-auto-${Date.now()}.png`);
|
|
1520
|
+
const screenshotPath = pending.autoScreenshotOutput || path.join(SURF_TMP, `pi-auto-${Date.now()}.png`);
|
|
1470
1521
|
|
|
1471
1522
|
const autoFiles = fs.readdirSync(SURF_TMP)
|
|
1472
1523
|
.filter(f => f.startsWith("pi-auto-") && f.endsWith(".png"))
|
|
@@ -1477,14 +1528,17 @@ function processInput() {
|
|
|
1477
1528
|
try { fs.unlinkSync(path.join(SURF_TMP, f.name)); } catch (e) {}
|
|
1478
1529
|
});
|
|
1479
1530
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1531
|
+
require("./abort.cjs").abortableDelay(500, pending.request?.signal)
|
|
1532
|
+
.then(() => requestCallExtension(
|
|
1533
|
+
pending.request,
|
|
1534
|
+
"screenshot",
|
|
1535
|
+
{ type: "EXECUTE_SCREENSHOT", tabId },
|
|
1536
|
+
))
|
|
1537
|
+
.then((screenshotMsg) => {
|
|
1485
1538
|
if (screenshotMsg.base64) {
|
|
1486
1539
|
try {
|
|
1487
|
-
fs.writeFileSync(screenshotPath, Buffer.from(screenshotMsg.base64, "base64"));
|
|
1540
|
+
fs.writeFileSync(screenshotPath, Buffer.from(screenshotMsg.base64, "base64"), { mode: 0o600 });
|
|
1541
|
+
try { fs.chmodSync(screenshotPath, 0o600); } catch {}
|
|
1488
1542
|
const origW = screenshotMsg.width || 0;
|
|
1489
1543
|
const origH = screenshotMsg.height || 0;
|
|
1490
1544
|
let finalW = origW, finalH = origH;
|
|
@@ -1501,16 +1555,17 @@ function processInput() {
|
|
|
1501
1555
|
autoScreenshot: { path: screenshotPath, width: finalW, height: finalH, originalWidth: origW, originalHeight: origH }
|
|
1502
1556
|
}, null);
|
|
1503
1557
|
} catch (e) {
|
|
1504
|
-
|
|
1558
|
+
failAutoScreenshot(e.message);
|
|
1505
1559
|
}
|
|
1506
1560
|
} else {
|
|
1507
1561
|
const errMsg = screenshotMsg.error || "Failed to capture";
|
|
1508
|
-
|
|
1562
|
+
failAutoScreenshot(errMsg);
|
|
1509
1563
|
}
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
setTimeout(() => writeMessage({ type: "EXECUTE_SCREENSHOT", tabId, id: screenshotId }), 500);
|
|
1564
|
+
})
|
|
1565
|
+
.catch((error) => failAutoScreenshot(error.message));
|
|
1513
1566
|
return;
|
|
1567
|
+
} else if (autoScreenshot && pending.autoScreenshotOutput && !msg.error) {
|
|
1568
|
+
failAutoScreenshot(tabId ? "screenshot response was invalid" : "no tab available");
|
|
1514
1569
|
} else if (msg.results && msg.savePath) {
|
|
1515
1570
|
try {
|
|
1516
1571
|
const dir = msg.savePath;
|
|
@@ -1544,11 +1599,7 @@ function processInput() {
|
|
|
1544
1599
|
}
|
|
1545
1600
|
} else if (msg.id && pendingRequests.has(msg.id)) {
|
|
1546
1601
|
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
|
-
}
|
|
1602
|
+
sendSocket(socket, msg).catch((error) => log(`Error writing to CLI socket: ${error.message}`));
|
|
1552
1603
|
pendingRequests.delete(msg.id);
|
|
1553
1604
|
}
|
|
1554
1605
|
} catch (e) {
|
|
@@ -1571,17 +1622,12 @@ const connectedSockets = new Set();
|
|
|
1571
1622
|
process.stdin.on("end", () => {
|
|
1572
1623
|
log("stdin ended (extension disconnected), notifying clients");
|
|
1573
1624
|
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
|
-
}
|
|
1625
|
+
sendSocket(socket, {
|
|
1626
|
+
type: "extension_disconnected",
|
|
1627
|
+
message: "Surf extension was reloaded. Restart your command."
|
|
1628
|
+
}).finally(() => socket.end()).catch(() => socket.end());
|
|
1583
1629
|
}
|
|
1584
|
-
|
|
1630
|
+
shutdown(0);
|
|
1585
1631
|
});
|
|
1586
1632
|
|
|
1587
1633
|
process.stdin.on("error", (err) => {
|
|
@@ -1592,142 +1638,276 @@ process.stdout.on("error", (err) => {
|
|
|
1592
1638
|
log(`stdout error: ${err.message}`);
|
|
1593
1639
|
});
|
|
1594
1640
|
|
|
1595
|
-
const
|
|
1641
|
+
const handleClient = (socket) => {
|
|
1642
|
+
const isRemote = Boolean(socket.remoteAddress && socket.remotePort);
|
|
1643
|
+
let transferState;
|
|
1644
|
+
let transferReady;
|
|
1645
|
+
let transferCleanupPromise;
|
|
1646
|
+
const cleanupTransfers = () => {
|
|
1647
|
+
if (!transferCleanupPromise) {
|
|
1648
|
+
transferCleanupPromise = transferReady
|
|
1649
|
+
? transferReady.then((state) => state?.cleanup())
|
|
1650
|
+
: Promise.resolve();
|
|
1651
|
+
}
|
|
1652
|
+
return transferCleanupPromise;
|
|
1653
|
+
};
|
|
1654
|
+
socket.transferCleanup = cleanupTransfers;
|
|
1655
|
+
const ensureTransferState = () => {
|
|
1656
|
+
if (context?.closed || socket.destroyed) throw transferError("transfer connection is closed", "SURF_TRANSFER_CLOSED");
|
|
1657
|
+
if (!transferReady) {
|
|
1658
|
+
transferCleanupPromise = undefined;
|
|
1659
|
+
transferReady = createStagingDirectory(SURF_TMP)
|
|
1660
|
+
.then(async (directory) => {
|
|
1661
|
+
if (context?.closed || socket.destroyed) {
|
|
1662
|
+
await fs.promises.rm(directory, { recursive: true, force: true }).catch(() => {});
|
|
1663
|
+
throw transferError("transfer connection is closed", "SURF_TRANSFER_CLOSED");
|
|
1664
|
+
}
|
|
1665
|
+
try {
|
|
1666
|
+
return createTransferState({ directory, writer: { send: (frame) => sendSocket(socket, frame) }, onActivity: () => sessionManager.touch(socketContexts.get(socket)) });
|
|
1667
|
+
} catch (error) {
|
|
1668
|
+
return fs.promises.rm(directory, { recursive: true, force: true }).catch(() => {}).then(() => { throw error; });
|
|
1669
|
+
}
|
|
1670
|
+
})
|
|
1671
|
+
.catch((error) => { transferReady = undefined; throw error; });
|
|
1672
|
+
}
|
|
1673
|
+
return transferReady;
|
|
1674
|
+
};
|
|
1675
|
+
let context;
|
|
1676
|
+
try {
|
|
1677
|
+
context = sessionManager.admit(socket, isRemote);
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
sendSocket(socket, { error: error.message }).finally(() => socket.destroy()).catch(() => socket.destroy());
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
const writer = createSocketWriter(socket, {
|
|
1683
|
+
maxPendingBytes: 4 * 1024 * 1024,
|
|
1684
|
+
onOverflow: ({ stream, error }) => {
|
|
1685
|
+
auditSession({ event: stream ? "stream" : "writer", context, outcome: "overflow", request: context.activeRequest });
|
|
1686
|
+
sessionManager.stopStream(context);
|
|
1687
|
+
socket.destroy(error);
|
|
1688
|
+
},
|
|
1689
|
+
});
|
|
1690
|
+
socketContexts.set(socket, context);
|
|
1691
|
+
socketWriters.set(socket, writer);
|
|
1596
1692
|
log("CLI client connected");
|
|
1597
1693
|
connectedSockets.add(socket);
|
|
1598
1694
|
socket.on("close", () => connectedSockets.delete(socket));
|
|
1599
1695
|
|
|
1600
|
-
|
|
1696
|
+
const stateDir = getStateDir();
|
|
1697
|
+
let principal = null;
|
|
1698
|
+
const authSession = isRemote ? createServerAuthSession({
|
|
1699
|
+
socket,
|
|
1700
|
+
stateDir,
|
|
1701
|
+
send: (value) => sendSocket(socket, value),
|
|
1702
|
+
async onAuthenticated(authenticatedPrincipal) {
|
|
1703
|
+
sessionManager.authenticate(context, authenticatedPrincipal);
|
|
1704
|
+
principal = authenticatedPrincipal;
|
|
1705
|
+
log(`Remote client authenticated: ${authenticatedPrincipal.label} (${authenticatedPrincipal.clientId})`);
|
|
1706
|
+
},
|
|
1707
|
+
onError(error) {
|
|
1708
|
+
log(`Remote authentication rejected: ${error.message}`);
|
|
1709
|
+
sendSocket(socket, { type: "auth_error", message: error.message }).finally(() => socket.destroy()).catch(() => socket.destroy());
|
|
1710
|
+
},
|
|
1711
|
+
}) : null;
|
|
1712
|
+
let messageChain = Promise.resolve();
|
|
1713
|
+
const processMessage = async (msg) => {
|
|
1714
|
+
if (context.closed) return;
|
|
1715
|
+
if (isRemote && !authSession.authenticated) {
|
|
1716
|
+
await authSession.handle(msg);
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
if (isRemote) {
|
|
1720
|
+
let authorized = false;
|
|
1721
|
+
try {
|
|
1722
|
+
authorized = Boolean(principal && isClientAuthorized(stateDir, principal.clientId));
|
|
1723
|
+
} catch (error) {
|
|
1724
|
+
log(`Remote authorization registry check failed: ${error.message}`);
|
|
1725
|
+
}
|
|
1726
|
+
if (!authorized) {
|
|
1727
|
+
await sendSocket(socket, { error: "remote client authorization is unavailable or revoked" }).catch(() => {});
|
|
1728
|
+
socket.destroy();
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1601
1732
|
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1733
|
+
if (isRemote && msg.type && msg.type.startsWith("transfer_")) {
|
|
1734
|
+
transferState ||= await ensureTransferState();
|
|
1735
|
+
await transferState.handle(msg);
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1606
1738
|
|
|
1607
|
-
|
|
1608
|
-
|
|
1739
|
+
if (msg.type === "tool_request") {
|
|
1740
|
+
const tool = msg.params?.tool || "unknown";
|
|
1741
|
+
let request;
|
|
1609
1742
|
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;
|
|
1743
|
+
const deadlineMs = TEST_REQUEST_DEADLINE_MS || resolveRequestDeadlineMs(tool, msg.params?.args);
|
|
1744
|
+
request = await sessionManager.beginRequest(context, { id: msg.id, tool, deadlineMs });
|
|
1745
|
+
request.context = context;
|
|
1746
|
+
} catch (error) {
|
|
1747
|
+
if (transferState) await discardRequestTransfers(msg, transferState);
|
|
1748
|
+
await sendSocket(socket, { type: "tool_response", id: msg.id || null, error: { content: [{ type: "text", text: error.message }] } }).catch(() => {});
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
log(`Handling tool_request: ${msg.method} ${tool}${principal ? ` for ${principal.label}` : ""}`);
|
|
1752
|
+
try {
|
|
1753
|
+
if (isRemote) {
|
|
1754
|
+
await applyRequestTransfers(msg, request, transferState, ensureTransferState);
|
|
1665
1755
|
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
log(`Forwarding to extension: id=${id} type=${msg.type}`);
|
|
1669
|
-
pendingRequests.set(id, { socket });
|
|
1670
|
-
writeMessage({ ...msg, id });
|
|
1756
|
+
throwIfAborted(request.signal, "Request cancelled");
|
|
1757
|
+
requestStorage.run(request, () => handleToolRequest(msg, socket, request));
|
|
1671
1758
|
} catch (e) {
|
|
1672
|
-
|
|
1673
|
-
socket.
|
|
1759
|
+
await discardRequestTransfers(msg, transferState);
|
|
1760
|
+
sendToolResponse(socket, msg.id || null, null, e.message || "Request failed");
|
|
1674
1761
|
}
|
|
1762
|
+
return;
|
|
1675
1763
|
}
|
|
1764
|
+
|
|
1765
|
+
if (msg.type === "stream_request") {
|
|
1766
|
+
if (msg.streamType !== "STREAM_CONSOLE" && msg.streamType !== "STREAM_NETWORK") {
|
|
1767
|
+
log(`Rejecting unsupported stream type: ${msg.streamType}`);
|
|
1768
|
+
await sendSocket(socket, { error: `Unsupported stream type: ${msg.streamType}` }).catch(() => {});
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
if (!sessionManager.canStartStream(context)) {
|
|
1772
|
+
await sendSocket(socket, { error: "stream limit reached or connection is not stream-only" }).catch(() => {});
|
|
1773
|
+
socket.destroy();
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
log(`Handling stream_request: ${msg.streamType}`);
|
|
1777
|
+
handleStreamRequest(msg, socket);
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
if (msg.type === "stream_stop") {
|
|
1782
|
+
log("Handling stream_stop");
|
|
1783
|
+
for (const [streamId, stream] of activeStreams.entries()) {
|
|
1784
|
+
if (stream.socket === socket) stopActiveStream(streamId);
|
|
1785
|
+
}
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
log(`Rejecting unsupported socket request type: ${msg.type}`);
|
|
1790
|
+
await sendSocket(socket, { error: `Unsupported request type: ${msg.type}` }).catch(() => {});
|
|
1791
|
+
};
|
|
1792
|
+
|
|
1793
|
+
const parser = createFrameParser({
|
|
1794
|
+
onFrame(msg) {
|
|
1795
|
+
messageChain = messageChain.then(() => processMessage(msg)).catch((error) => {
|
|
1796
|
+
log(`Error handling CLI request: ${error.message}`);
|
|
1797
|
+
if (isRemote && msg.type && msg.type.startsWith("transfer_")) {
|
|
1798
|
+
sendSocket(socket, { type: "transfer_error", version: 1, transferId: msg.transferId, error: error.message || "Transfer failed" })
|
|
1799
|
+
.finally(() => socket.destroy()).catch(() => socket.destroy());
|
|
1800
|
+
} else {
|
|
1801
|
+
sendSocket(socket, { error: error.message || "Request failed" }).catch(() => {});
|
|
1802
|
+
}
|
|
1803
|
+
});
|
|
1804
|
+
},
|
|
1805
|
+
onError(error) {
|
|
1806
|
+
log(`CLI frame rejected: ${error.message}`);
|
|
1807
|
+
if (isRemote && !authSession.authenticated) {
|
|
1808
|
+
sendSocket(socket, { type: "auth_error", message: error.message }).finally(() => socket.destroy()).catch(() => socket.destroy());
|
|
1809
|
+
} else {
|
|
1810
|
+
socket.destroy();
|
|
1811
|
+
}
|
|
1812
|
+
},
|
|
1813
|
+
maxFrameBytes: MAX_CLIENT_FRAME_BYTES,
|
|
1676
1814
|
});
|
|
1677
1815
|
|
|
1816
|
+
socket.on("data", (data) => parser.push(data));
|
|
1817
|
+
|
|
1678
1818
|
socket.on("error", (err) => {
|
|
1679
1819
|
log(`CLI socket error: ${err.message}`);
|
|
1680
1820
|
});
|
|
1681
1821
|
|
|
1682
1822
|
socket.on("close", () => {
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1823
|
+
parser.close();
|
|
1824
|
+
authSession?.close();
|
|
1825
|
+
const activeRequest = context.activeRequest;
|
|
1826
|
+
let cleanupPendingId;
|
|
1827
|
+
if (activeRequest && !activeRequest.queued) {
|
|
1828
|
+
cleanupPendingId = `transfer-cleanup-${++requestCounter}`;
|
|
1829
|
+
pendingToolRequests.set(cleanupPendingId, {
|
|
1830
|
+
request: activeRequest,
|
|
1831
|
+
cleanup: true,
|
|
1832
|
+
tool: "transfer_cleanup",
|
|
1833
|
+
resolve: () => {},
|
|
1834
|
+
reject: () => {},
|
|
1835
|
+
});
|
|
1836
|
+
completeOwnedRequest(context, activeRequest.id, "cleanup-settled");
|
|
1693
1837
|
}
|
|
1838
|
+
const cleanupPromise = cleanupTransfers();
|
|
1839
|
+
cleanupPromise.finally(() => {
|
|
1840
|
+
if (cleanupPendingId) pendingToolRequests.delete(cleanupPendingId);
|
|
1841
|
+
}).catch(() => {});
|
|
1842
|
+
writer.close();
|
|
1843
|
+
sessionManager.close(context);
|
|
1844
|
+
if (activeRequest) pendingToolRequests.tombstoneAfterAbort(activeRequest);
|
|
1845
|
+
log("CLI client disconnected");
|
|
1694
1846
|
for (const [streamId, stream] of activeStreams.entries()) {
|
|
1695
|
-
if (stream.socket === socket)
|
|
1696
|
-
writeMessage({ type: "STREAM_STOP", streamId });
|
|
1697
|
-
activeStreams.delete(streamId);
|
|
1698
|
-
}
|
|
1847
|
+
if (stream.socket === socket) stopActiveStream(streamId);
|
|
1699
1848
|
}
|
|
1700
1849
|
});
|
|
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
|
-
});
|
|
1850
|
+
};
|
|
1709
1851
|
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1852
|
+
let listenerLifecycle = null;
|
|
1853
|
+
let shuttingDown = false;
|
|
1854
|
+
let exitCode = 0;
|
|
1855
|
+
let exitScheduled = false;
|
|
1856
|
+
function scheduleExit() {
|
|
1857
|
+
if (exitScheduled) return;
|
|
1858
|
+
exitScheduled = true;
|
|
1859
|
+
setTimeout(() => process.exit(exitCode), 50);
|
|
1860
|
+
}
|
|
1861
|
+
function shutdown(code = 0) {
|
|
1862
|
+
exitCode = Math.max(exitCode, code);
|
|
1863
|
+
if (shuttingDown) return;
|
|
1864
|
+
shuttingDown = true;
|
|
1865
|
+
const cleanupPromises = [...connectedSockets].map((socket) => socket.transferCleanup?.() || Promise.resolve());
|
|
1866
|
+
for (const socket of connectedSockets) socket.destroy();
|
|
1867
|
+
pendingRequests.clear(); pendingToolRequests.clear(); activeStreams.clear();
|
|
1868
|
+
Promise.allSettled([...cleanupPromises, Promise.resolve(listenerLifecycle?.shutdown())]).finally(scheduleExit);
|
|
1869
|
+
}
|
|
1870
|
+
function failStartup(error, endpoint) {
|
|
1871
|
+
log(`Listener startup failed (${endpoint}): ${error.message}`);
|
|
1872
|
+
shutdown(1);
|
|
1873
|
+
}
|
|
1874
|
+
async function startListeners() {
|
|
1875
|
+
let endpoint;
|
|
1876
|
+
try {
|
|
1877
|
+
endpoint = process.env.SURF_LISTEN ? parseListenEndpoint(process.env.SURF_LISTEN) : null;
|
|
1878
|
+
listenerLifecycle = createListenerLifecycle({
|
|
1879
|
+
localPath: SOCKET_PATH,
|
|
1880
|
+
tcpEndpoint: endpoint && { host: endpoint.host, port: endpoint.port },
|
|
1881
|
+
handler: handleClient,
|
|
1882
|
+
onReady: () => {
|
|
1883
|
+
if (endpoint) log(`TCP listener listening on ${endpoint.display}`);
|
|
1884
|
+
writeMessage({ type: "HOST_READY" });
|
|
1885
|
+
log("Sent HOST_READY to extension");
|
|
1886
|
+
},
|
|
1887
|
+
onFatal: (error) => failStartup(error, endpoint?.display || process.env.SURF_LISTEN || SOCKET_PATH),
|
|
1888
|
+
});
|
|
1889
|
+
if (shuttingDown) await listenerLifecycle.shutdown();
|
|
1890
|
+
await listenerLifecycle.start();
|
|
1891
|
+
} catch (error) { failStartup(error, endpoint?.display || process.env.SURF_LISTEN || SOCKET_PATH); }
|
|
1892
|
+
}
|
|
1893
|
+
startListeners();
|
|
1713
1894
|
|
|
1714
1895
|
process.on("SIGTERM", () => {
|
|
1715
1896
|
log("SIGTERM received");
|
|
1716
|
-
|
|
1717
|
-
if (!IS_WIN) { try { fs.unlinkSync(SOCKET_PATH); } catch {} }
|
|
1718
|
-
process.exit(0);
|
|
1897
|
+
shutdown();
|
|
1719
1898
|
});
|
|
1720
1899
|
|
|
1721
1900
|
process.on("SIGINT", () => {
|
|
1722
1901
|
log("SIGINT received");
|
|
1723
|
-
|
|
1724
|
-
if (!IS_WIN) { try { fs.unlinkSync(SOCKET_PATH); } catch {} }
|
|
1725
|
-
process.exit(0);
|
|
1902
|
+
shutdown();
|
|
1726
1903
|
});
|
|
1727
1904
|
|
|
1728
1905
|
process.on("uncaughtException", (err) => {
|
|
1729
1906
|
log(`Uncaught exception: ${err.message}\n${err.stack}`);
|
|
1730
|
-
|
|
1907
|
+
shutdown(1);
|
|
1731
1908
|
});
|
|
1732
1909
|
|
|
1733
1910
|
log("Host initialization complete, waiting for connections...");
|
|
1911
|
+
} else {
|
|
1912
|
+
module.exports = { createListenerLifecycle, MAX_CLIENT_FRAME_BYTES };
|
|
1913
|
+
}
|