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/abort.cjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
class RequestAbortError extends Error {
|
|
2
|
+
constructor(message = "Request cancelled") {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "RequestAbortError";
|
|
5
|
+
this.code = "SURF_REQUEST_ABORTED";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function abortError(signal, fallback = "Request cancelled") {
|
|
10
|
+
if (signal?.reason instanceof Error && signal.reason.name !== "AbortError") return signal.reason;
|
|
11
|
+
return new RequestAbortError(fallback);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function throwIfAborted(signal, fallback) {
|
|
15
|
+
if (signal?.aborted) throw abortError(signal, fallback);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function raceAbort(promiseOrFactory, signal, fallback) {
|
|
19
|
+
if (!signal) return typeof promiseOrFactory === "function" ? promiseOrFactory() : promiseOrFactory;
|
|
20
|
+
throwIfAborted(signal, fallback);
|
|
21
|
+
const promise = typeof promiseOrFactory === "function" ? promiseOrFactory() : promiseOrFactory;
|
|
22
|
+
if (signal.aborted) return Promise.reject(abortError(signal, fallback));
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
let settled = false;
|
|
25
|
+
const onAbort = () => {
|
|
26
|
+
if (settled) return;
|
|
27
|
+
settled = true;
|
|
28
|
+
signal.removeEventListener("abort", onAbort);
|
|
29
|
+
reject(abortError(signal, fallback));
|
|
30
|
+
};
|
|
31
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
32
|
+
Promise.resolve(promise).then(
|
|
33
|
+
(value) => {
|
|
34
|
+
if (settled) return;
|
|
35
|
+
settled = true;
|
|
36
|
+
signal.removeEventListener("abort", onAbort);
|
|
37
|
+
resolve(value);
|
|
38
|
+
},
|
|
39
|
+
(error) => {
|
|
40
|
+
if (settled) return;
|
|
41
|
+
settled = true;
|
|
42
|
+
signal.removeEventListener("abort", onAbort);
|
|
43
|
+
reject(error);
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function abortableDelay(ms, signal, fallback) {
|
|
50
|
+
throwIfAborted(signal, fallback);
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const onAbort = () => {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
signal.removeEventListener("abort", onAbort);
|
|
55
|
+
reject(abortError(signal, fallback));
|
|
56
|
+
};
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
signal?.removeEventListener("abort", onAbort);
|
|
59
|
+
resolve();
|
|
60
|
+
}, ms);
|
|
61
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { RequestAbortError, abortError, abortableDelay, raceAbort, throwIfAborted };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const { abortError } = require("./abort.cjs");
|
|
2
|
+
|
|
3
|
+
class BoundedAiQueue {
|
|
4
|
+
constructor({ maxQueued = 8, spacingMs = 2000, audit = () => {}, run = (handler) => handler() } = {}) {
|
|
5
|
+
this.maxQueued = maxQueued;
|
|
6
|
+
this.spacingMs = spacingMs;
|
|
7
|
+
this.audit = audit;
|
|
8
|
+
this.run = run;
|
|
9
|
+
this.items = [];
|
|
10
|
+
this.active = false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
enqueue(handler, request) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
if (request?.signal.aborted) {
|
|
16
|
+
reject(abortError(request.signal));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (this.items.length >= this.maxQueued) {
|
|
20
|
+
reject(new Error("AI request queue is full"));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const item = { handler, request, resolve, reject, abortCleanup: null };
|
|
24
|
+
const onAbort = () => {
|
|
25
|
+
const index = this.items.indexOf(item);
|
|
26
|
+
if (index === -1) return;
|
|
27
|
+
this.items.splice(index, 1);
|
|
28
|
+
item.abortCleanup = null;
|
|
29
|
+
reject(abortError(request.signal));
|
|
30
|
+
this.audit({ event: "request", context: request.context, request, outcome: "queued-cancel" });
|
|
31
|
+
};
|
|
32
|
+
if (request?.signal) {
|
|
33
|
+
request.signal.addEventListener("abort", onAbort, { once: true });
|
|
34
|
+
item.abortCleanup = () => request.signal.removeEventListener("abort", onAbort);
|
|
35
|
+
}
|
|
36
|
+
this.items.push(item);
|
|
37
|
+
this.#process();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get queued() {
|
|
42
|
+
return this.items.length;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async #process() {
|
|
46
|
+
if (this.active || this.items.length === 0) return;
|
|
47
|
+
this.active = true;
|
|
48
|
+
const item = this.items.shift();
|
|
49
|
+
item.abortCleanup?.();
|
|
50
|
+
try {
|
|
51
|
+
if (item.request?.signal.aborted) throw abortError(item.request.signal);
|
|
52
|
+
const result = await this.run(item.handler, item.request);
|
|
53
|
+
if (item.request?.signal.aborted) throw abortError(item.request.signal);
|
|
54
|
+
item.resolve(result);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
item.reject(error);
|
|
57
|
+
} finally {
|
|
58
|
+
this.active = false;
|
|
59
|
+
setTimeout(() => this.#process(), this.spacingMs);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { BoundedAiQueue };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
|
+
const { raceAbort, throwIfAborted } = require("./abort.cjs");
|
|
2
3
|
const { execFileSync } = require("child_process");
|
|
3
4
|
const {
|
|
4
5
|
delay,
|
|
@@ -483,26 +484,29 @@ async function build({
|
|
|
483
484
|
cdpCommand,
|
|
484
485
|
searchDownloads,
|
|
485
486
|
log = () => {},
|
|
487
|
+
signal,
|
|
486
488
|
}) {
|
|
489
|
+
throwIfAborted(signal);
|
|
490
|
+
const guardedSearchDownloads = (...args) => raceAbort(() => searchDownloads(...args), signal);
|
|
487
491
|
const startedAt = Date.now();
|
|
488
492
|
const timeoutMs = Number.isFinite(timeout) ? timeout : 600000;
|
|
489
493
|
const requestedModel = normalizeModelString(model || "");
|
|
490
494
|
const resolvedModel = requestedModel || DEFAULT_MODEL;
|
|
491
495
|
|
|
492
|
-
const cookieResult = await getCookies
|
|
496
|
+
const cookieResult = await raceAbort(getCookies, signal);
|
|
493
497
|
const cookies = cookieResult?.cookies || [];
|
|
494
498
|
if (!hasRequiredCookies(cookies)) {
|
|
495
499
|
throw new Error("Sign into Google in Chrome first.");
|
|
496
500
|
}
|
|
497
501
|
|
|
498
|
-
const tabInfo = await createTab(AISTUDIO_APPS_URL);
|
|
502
|
+
const tabInfo = await raceAbort(() => createTab(AISTUDIO_APPS_URL), signal);
|
|
499
503
|
const tabId = tabInfo?.tabId;
|
|
500
504
|
if (!tabId) {
|
|
501
505
|
throw new Error(`Failed to create AI Studio tab: ${JSON.stringify(tabInfo)}`);
|
|
502
506
|
}
|
|
503
507
|
|
|
504
|
-
const cdp = (expression) => cdpEvaluate(tabId, expression);
|
|
505
|
-
const inputCdp = (method, params) => cdpCommand(tabId, method, params);
|
|
508
|
+
const cdp = (expression) => raceAbort(() => cdpEvaluate(tabId, expression), signal);
|
|
509
|
+
const inputCdp = (method, params) => raceAbort(() => cdpCommand(tabId, method, params), signal);
|
|
506
510
|
|
|
507
511
|
let buildDuration = null;
|
|
508
512
|
let zipPath = null;
|
|
@@ -510,7 +514,7 @@ async function build({
|
|
|
510
514
|
let modelUsed = resolvedModel;
|
|
511
515
|
|
|
512
516
|
try {
|
|
513
|
-
await waitForBuildPageReady(cdp, 30000);
|
|
517
|
+
await raceAbort(waitForBuildPageReady(cdp, 30000), signal);
|
|
514
518
|
|
|
515
519
|
if (requestedModel) {
|
|
516
520
|
const applied = await selectModelInAdvancedSettings(cdp, inputCdp, requestedModel, log);
|
|
@@ -518,20 +522,20 @@ async function build({
|
|
|
518
522
|
}
|
|
519
523
|
|
|
520
524
|
await typePromptAndBuild(cdp, inputCdp, prompt);
|
|
521
|
-
const completion = await waitForBuildCompletion(cdp, timeoutMs, log);
|
|
525
|
+
const completion = await raceAbort(waitForBuildCompletion(cdp, timeoutMs, log), signal);
|
|
522
526
|
buildDuration = completion.buildDuration;
|
|
523
527
|
|
|
524
|
-
await activateCodeTab(cdp, inputCdp);
|
|
525
|
-
await waitForDownloadButton(cdp, 5000);
|
|
528
|
+
await raceAbort(activateCodeTab(cdp, inputCdp), signal);
|
|
529
|
+
await raceAbort(waitForDownloadButton(cdp, 5000), signal);
|
|
526
530
|
|
|
527
|
-
const beforeDownloads = await
|
|
531
|
+
const beforeDownloads = await guardedSearchDownloads({
|
|
528
532
|
limit: 1,
|
|
529
533
|
orderBy: ["-startTime"],
|
|
530
534
|
});
|
|
531
535
|
const latestIdBefore = beforeDownloads?.[0]?.id || 0;
|
|
532
536
|
|
|
533
|
-
await clickDownloadButton(cdp);
|
|
534
|
-
zipPath = await waitForDownloadComplete(
|
|
537
|
+
await raceAbort(clickDownloadButton(cdp), signal);
|
|
538
|
+
zipPath = await waitForDownloadComplete(guardedSearchDownloads, latestIdBefore, 30000);
|
|
535
539
|
|
|
536
540
|
if (output) {
|
|
537
541
|
const outputDir = String(output);
|
|
@@ -553,8 +557,12 @@ async function build({
|
|
|
553
557
|
tookMs: Date.now() - startedAt,
|
|
554
558
|
};
|
|
555
559
|
} finally {
|
|
556
|
-
if (!keepOpen) {
|
|
557
|
-
|
|
560
|
+
if (!keepOpen || signal?.aborted) {
|
|
561
|
+
try {
|
|
562
|
+
await closeTab(tabId);
|
|
563
|
+
} catch (error) {
|
|
564
|
+
log(`Failed to close AI Studio Build tab ${tabId}: ${error?.message || error}`);
|
|
565
|
+
}
|
|
558
566
|
}
|
|
559
567
|
}
|
|
560
568
|
}
|
|
@@ -33,6 +33,7 @@ const {
|
|
|
33
33
|
waitForGenerateResponseFromNetwork,
|
|
34
34
|
waitForResponse,
|
|
35
35
|
} = require("./aistudio-response.cjs");
|
|
36
|
+
const { raceAbort, throwIfAborted } = require("./abort.cjs");
|
|
36
37
|
|
|
37
38
|
const DEFAULT_MODEL = "gemini-3.1-pro-preview";
|
|
38
39
|
|
|
@@ -310,22 +311,27 @@ async function query(options) {
|
|
|
310
311
|
cdpCommand,
|
|
311
312
|
readNetworkEntries,
|
|
312
313
|
log = () => {},
|
|
314
|
+
signal,
|
|
313
315
|
} = options;
|
|
316
|
+
throwIfAborted(signal);
|
|
314
317
|
|
|
318
|
+
const guardedReadNetworkEntries = readNetworkEntries
|
|
319
|
+
? (...args) => raceAbort(() => readNetworkEntries(...args), signal)
|
|
320
|
+
: readNetworkEntries;
|
|
315
321
|
const startTime = Date.now();
|
|
316
322
|
log("Starting AI Studio query");
|
|
317
323
|
|
|
318
324
|
const resolvedModel = normalizeModelString(model) || DEFAULT_MODEL;
|
|
319
325
|
log(`Requested model: ${resolvedModel}`);
|
|
320
326
|
|
|
321
|
-
const { cookies } = await getCookies
|
|
327
|
+
const { cookies } = await raceAbort(getCookies, signal);
|
|
322
328
|
if (!hasRequiredCookies(cookies)) {
|
|
323
329
|
throw new Error("Google login required - sign into Google in Chrome first");
|
|
324
330
|
}
|
|
325
331
|
log(`Got ${cookies.length} cookies`);
|
|
326
332
|
|
|
327
333
|
const createdUrl = buildAiStudioUrl(resolvedModel);
|
|
328
|
-
const tabInfo = await createTab(createdUrl);
|
|
334
|
+
const tabInfo = await raceAbort(() => createTab(createdUrl), signal);
|
|
329
335
|
const { tabId } = tabInfo || {};
|
|
330
336
|
|
|
331
337
|
if (!tabId) {
|
|
@@ -333,21 +339,21 @@ async function query(options) {
|
|
|
333
339
|
}
|
|
334
340
|
log(`Created tab ${tabId}`);
|
|
335
341
|
|
|
336
|
-
const cdp = (expr) => cdpEvaluate(tabId, expr);
|
|
337
|
-
const inputCdp = (method, params) => cdpCommand(tabId, method, params);
|
|
342
|
+
const cdp = (expr) => raceAbort(() => cdpEvaluate(tabId, expr), signal);
|
|
343
|
+
const inputCdp = (method, params) => raceAbort(() => cdpCommand(tabId, method, params), signal);
|
|
338
344
|
|
|
339
345
|
let baselineGenerateEntryIds = new Set();
|
|
340
346
|
|
|
341
347
|
try {
|
|
342
|
-
await waitForPageLoad(cdp);
|
|
348
|
+
await raceAbort(waitForPageLoad(cdp), signal);
|
|
343
349
|
log("Page loaded");
|
|
344
350
|
|
|
345
|
-
await waitForStudioReady(cdp);
|
|
351
|
+
await raceAbort(waitForStudioReady(cdp), signal);
|
|
346
352
|
log("AI Studio ready");
|
|
347
353
|
|
|
348
354
|
if (typeof readNetworkEntries === 'function') {
|
|
349
355
|
try {
|
|
350
|
-
const baselineNetwork = await
|
|
356
|
+
const baselineNetwork = await guardedReadNetworkEntries(tabId);
|
|
351
357
|
const baselineEntries = Array.isArray(baselineNetwork?.entries)
|
|
352
358
|
? baselineNetwork.entries
|
|
353
359
|
: Array.isArray(baselineNetwork?.requests)
|
|
@@ -362,6 +368,7 @@ async function query(options) {
|
|
|
362
368
|
|
|
363
369
|
log(`Network baseline ready (${baselineGenerateEntryIds.size} GenerateContent entries)`);
|
|
364
370
|
} catch (e) {
|
|
371
|
+
if (signal?.aborted) throw e;
|
|
365
372
|
log(`Network baseline failed: ${e.message || e}`);
|
|
366
373
|
}
|
|
367
374
|
}
|
|
@@ -369,6 +376,7 @@ async function query(options) {
|
|
|
369
376
|
try {
|
|
370
377
|
await enableUnformattedMarkdownView(cdp, log);
|
|
371
378
|
} catch (e) {
|
|
379
|
+
if (signal?.aborted) throw e;
|
|
372
380
|
log(`Markdown toggle failed: ${e.message}`);
|
|
373
381
|
}
|
|
374
382
|
|
|
@@ -387,8 +395,8 @@ async function query(options) {
|
|
|
387
395
|
try {
|
|
388
396
|
log(`Runtime model param missing; retrying direct navigation: ${createdUrl}`);
|
|
389
397
|
await inputCdp('Page.navigate', { url: createdUrl });
|
|
390
|
-
await waitForPageLoad(cdp);
|
|
391
|
-
await waitForStudioReady(cdp);
|
|
398
|
+
await raceAbort(waitForPageLoad(cdp), signal);
|
|
399
|
+
await raceAbort(waitForStudioReady(cdp), signal);
|
|
392
400
|
runtimeUrl = await evaluate(cdp, 'location.href');
|
|
393
401
|
runtimeModelParam = await evaluate(cdp, `(() => {
|
|
394
402
|
try {
|
|
@@ -398,6 +406,7 @@ async function query(options) {
|
|
|
398
406
|
}
|
|
399
407
|
})()`);
|
|
400
408
|
} catch (e) {
|
|
409
|
+
if (signal?.aborted) throw e;
|
|
401
410
|
log(`Direct model URL navigation retry failed: ${e.message || e}`);
|
|
402
411
|
}
|
|
403
412
|
}
|
|
@@ -408,7 +417,10 @@ async function query(options) {
|
|
|
408
417
|
log(`Model via UI (no URL param): requested="${resolvedModel}"`);
|
|
409
418
|
}
|
|
410
419
|
|
|
411
|
-
const currentModelInfo = await readCurrentModelInfo(cdp).catch(() =>
|
|
420
|
+
const currentModelInfo = await readCurrentModelInfo(cdp).catch((error) => {
|
|
421
|
+
if (signal?.aborted) throw error;
|
|
422
|
+
return { found: false, label: '', modelId: '' };
|
|
423
|
+
});
|
|
412
424
|
|
|
413
425
|
log(`AI Studio URL: ${runtimeUrl}`);
|
|
414
426
|
if (currentModelInfo?.label) {
|
|
@@ -427,8 +439,9 @@ async function query(options) {
|
|
|
427
439
|
|
|
428
440
|
if (!modelApplied && usedUrlParam) {
|
|
429
441
|
try {
|
|
430
|
-
modelApplied = await waitForModelToApply(cdp, resolvedModel, log);
|
|
442
|
+
modelApplied = await raceAbort(waitForModelToApply(cdp, resolvedModel, log), signal);
|
|
431
443
|
} catch (e) {
|
|
444
|
+
if (signal?.aborted) throw e;
|
|
432
445
|
log(`Model apply wait failed: ${e.message}`);
|
|
433
446
|
}
|
|
434
447
|
}
|
|
@@ -436,19 +449,20 @@ async function query(options) {
|
|
|
436
449
|
if (!modelApplied) {
|
|
437
450
|
try {
|
|
438
451
|
log(`Attempting UI model selection fallback: ${resolvedModel}`);
|
|
439
|
-
await selectModel(cdp, resolvedModel, log);
|
|
440
|
-
modelApplied = await waitForModelToApply(cdp, resolvedModel, log, 10000);
|
|
452
|
+
await raceAbort(selectModel(cdp, resolvedModel, log), signal);
|
|
453
|
+
modelApplied = await raceAbort(waitForModelToApply(cdp, resolvedModel, log, 10000), signal);
|
|
441
454
|
} catch (e) {
|
|
455
|
+
if (signal?.aborted) throw e;
|
|
442
456
|
log(`UI model selection fallback failed: ${e.message}`);
|
|
443
457
|
}
|
|
444
458
|
}
|
|
445
459
|
|
|
446
|
-
await closeModelSelectorIfOpen(cdp, log);
|
|
460
|
+
await raceAbort(closeModelSelectorIfOpen(cdp, log), signal);
|
|
447
461
|
|
|
448
|
-
await typePrompt(cdp, inputCdp, prompt);
|
|
462
|
+
await raceAbort(typePrompt(cdp, inputCdp, prompt), signal);
|
|
449
463
|
log("Prompt typed");
|
|
450
464
|
|
|
451
|
-
await submitPrompt(cdp, inputCdp);
|
|
465
|
+
await raceAbort(submitPrompt(cdp, inputCdp), signal);
|
|
452
466
|
log("Submitted, waiting for response...");
|
|
453
467
|
|
|
454
468
|
let response;
|
|
@@ -457,10 +471,11 @@ async function query(options) {
|
|
|
457
471
|
try {
|
|
458
472
|
const networkResult = await waitForGenerateResponseFromNetwork({
|
|
459
473
|
tabId,
|
|
460
|
-
readNetworkEntries,
|
|
474
|
+
readNetworkEntries: guardedReadNetworkEntries,
|
|
461
475
|
timeoutMs: timeout,
|
|
462
476
|
baselineEntryIds: baselineGenerateEntryIds,
|
|
463
477
|
prompt,
|
|
478
|
+
signal,
|
|
464
479
|
log,
|
|
465
480
|
});
|
|
466
481
|
|
|
@@ -474,13 +489,14 @@ async function query(options) {
|
|
|
474
489
|
`${networkResult.requestId ? ` (request ${networkResult.requestId})` : ''}`
|
|
475
490
|
);
|
|
476
491
|
} catch (networkErr) {
|
|
492
|
+
if (signal?.aborted) throw networkErr;
|
|
477
493
|
log(`Network extraction failed, falling back to DOM: ${networkErr.message || networkErr}`);
|
|
478
494
|
|
|
479
495
|
const remainingTimeoutMs = Math.max(timeout - (Date.now() - startTime), 10000);
|
|
480
|
-
response = await waitForResponse(cdp, remainingTimeoutMs, prompt, log);
|
|
496
|
+
response = await raceAbort(waitForResponse(cdp, remainingTimeoutMs, prompt, log), signal);
|
|
481
497
|
}
|
|
482
498
|
} else {
|
|
483
|
-
response = await waitForResponse(cdp, timeout, prompt, log);
|
|
499
|
+
response = await raceAbort(waitForResponse(cdp, timeout, prompt, log), signal);
|
|
484
500
|
}
|
|
485
501
|
|
|
486
502
|
const thinkingInfo = response.thinkingTime ? ` (thought for ${response.thinkingTime}s)` : '';
|
|
@@ -493,7 +509,11 @@ async function query(options) {
|
|
|
493
509
|
tookMs: Date.now() - startTime,
|
|
494
510
|
};
|
|
495
511
|
} finally {
|
|
496
|
-
|
|
512
|
+
try {
|
|
513
|
+
await closeTab(tabId);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
log(`Failed to close AI Studio tab ${tabId}: ${error?.message || error}`);
|
|
516
|
+
}
|
|
497
517
|
}
|
|
498
518
|
}
|
|
499
519
|
|
package/native/browser-lock.cjs
CHANGED
|
@@ -11,8 +11,8 @@ function sleepSync(ms) {
|
|
|
11
11
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function getBrowserLockDir(
|
|
15
|
-
const hash = crypto.createHash("sha256").update(
|
|
14
|
+
function getBrowserLockDir(endpointKey, tempDir) {
|
|
15
|
+
const hash = crypto.createHash("sha256").update(endpointKey).digest("hex").slice(0, 16);
|
|
16
16
|
return path.join(tempDir, `surf-lock-${hash}`);
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
|
+
const { abortableDelay, raceAbort, throwIfAborted } = require("./abort.cjs");
|
|
2
3
|
|
|
3
4
|
const CHATGPT_URL = "https://chatgpt.com/";
|
|
4
5
|
|
|
@@ -14,8 +15,8 @@ const SELECTORS = {
|
|
|
14
15
|
cloudflareScript: 'script[src*="/challenge-platform/"]',
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
function delay(ms) {
|
|
18
|
-
return
|
|
18
|
+
function delay(ms, signal) {
|
|
19
|
+
return abortableDelay(ms, signal);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
function buildClickDispatcher() {
|
|
@@ -189,8 +190,10 @@ function isChatGPTResponseComplete(snapshot, stableCycles, stableMs) {
|
|
|
189
190
|
return stableCycles >= 6 && stableMs >= 1200;
|
|
190
191
|
}
|
|
191
192
|
|
|
192
|
-
async function evaluate(cdp, expression) {
|
|
193
|
+
async function evaluate(cdp, expression, signal) {
|
|
194
|
+
throwIfAborted(signal);
|
|
193
195
|
const result = await cdp(expression);
|
|
196
|
+
throwIfAborted(signal);
|
|
194
197
|
if (result.exceptionDetails) {
|
|
195
198
|
const desc = result.exceptionDetails.exception?.description ||
|
|
196
199
|
result.exceptionDetails.text ||
|
|
@@ -203,14 +206,15 @@ async function evaluate(cdp, expression) {
|
|
|
203
206
|
return result.result?.value;
|
|
204
207
|
}
|
|
205
208
|
|
|
206
|
-
async function waitForPageLoad(cdp, timeoutMs = 45000) {
|
|
209
|
+
async function waitForPageLoad(cdp, timeoutMs = 45000, signal) {
|
|
210
|
+
throwIfAborted(signal);
|
|
207
211
|
const deadline = Date.now() + timeoutMs;
|
|
208
212
|
while (Date.now() < deadline) {
|
|
209
213
|
const ready = await evaluate(cdp, "document.readyState");
|
|
210
214
|
if (ready === "complete" || ready === "interactive") {
|
|
211
215
|
return;
|
|
212
216
|
}
|
|
213
|
-
await delay(100);
|
|
217
|
+
await delay(100, signal);
|
|
214
218
|
}
|
|
215
219
|
throw new Error("Page did not load in time");
|
|
216
220
|
}
|
|
@@ -252,7 +256,8 @@ async function checkLoginStatus(cdp) {
|
|
|
252
256
|
return result || { status: 0 };
|
|
253
257
|
}
|
|
254
258
|
|
|
255
|
-
async function waitForPromptReady(cdp, timeoutMs = 30000) {
|
|
259
|
+
async function waitForPromptReady(cdp, timeoutMs = 30000, signal) {
|
|
260
|
+
throwIfAborted(signal);
|
|
256
261
|
const deadline = Date.now() + timeoutMs;
|
|
257
262
|
const selectors = JSON.stringify(SELECTORS.promptTextarea.split(", "));
|
|
258
263
|
while (Date.now() < deadline) {
|
|
@@ -270,7 +275,7 @@ async function waitForPromptReady(cdp, timeoutMs = 30000) {
|
|
|
270
275
|
})()`
|
|
271
276
|
);
|
|
272
277
|
if (found) return true;
|
|
273
|
-
await delay(200);
|
|
278
|
+
await delay(200, signal);
|
|
274
279
|
}
|
|
275
280
|
return false;
|
|
276
281
|
}
|
|
@@ -302,7 +307,8 @@ function resolveChatGPTModelMenuOption(items, desiredModel) {
|
|
|
302
307
|
}) || null;
|
|
303
308
|
}
|
|
304
309
|
|
|
305
|
-
async function selectModel(cdp, desiredModel, timeoutMs = 8000) {
|
|
310
|
+
async function selectModel(cdp, desiredModel, timeoutMs = 8000, signal) {
|
|
311
|
+
throwIfAborted(signal);
|
|
306
312
|
const modelButton = await evaluate(
|
|
307
313
|
cdp,
|
|
308
314
|
`(() => {
|
|
@@ -321,7 +327,7 @@ async function selectModel(cdp, desiredModel, timeoutMs = 8000) {
|
|
|
321
327
|
if (btn) dispatchClickSequence(btn);
|
|
322
328
|
})()`
|
|
323
329
|
);
|
|
324
|
-
await delay(300);
|
|
330
|
+
await delay(300, signal);
|
|
325
331
|
|
|
326
332
|
const normalizedModel = normalizeChatGPTModelChoice(desiredModel);
|
|
327
333
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -361,7 +367,7 @@ async function selectModel(cdp, desiredModel, timeoutMs = 8000) {
|
|
|
361
367
|
if (item) dispatchClickSequence(item);
|
|
362
368
|
})()`
|
|
363
369
|
);
|
|
364
|
-
await delay(200);
|
|
370
|
+
await delay(200, signal);
|
|
365
371
|
return match.label;
|
|
366
372
|
}
|
|
367
373
|
|
|
@@ -379,13 +385,14 @@ async function selectModel(cdp, desiredModel, timeoutMs = 8000) {
|
|
|
379
385
|
);
|
|
380
386
|
}
|
|
381
387
|
|
|
382
|
-
await delay(100);
|
|
388
|
+
await delay(100, signal);
|
|
383
389
|
}
|
|
384
390
|
|
|
385
391
|
throw new Error(`Model not found: ${desiredModel} (timeout)`);
|
|
386
392
|
}
|
|
387
393
|
|
|
388
|
-
async function typePrompt(cdp, inputCdp, prompt) {
|
|
394
|
+
async function typePrompt(cdp, inputCdp, prompt, signal) {
|
|
395
|
+
throwIfAborted(signal);
|
|
389
396
|
const selectors = JSON.stringify(SELECTORS.promptTextarea.split(", "));
|
|
390
397
|
const encodedPrompt = JSON.stringify(prompt);
|
|
391
398
|
const focused = await evaluate(
|
|
@@ -416,7 +423,7 @@ async function typePrompt(cdp, inputCdp, prompt) {
|
|
|
416
423
|
throw new Error("Failed to focus prompt textarea");
|
|
417
424
|
}
|
|
418
425
|
await inputCdp("Input.insertText", { text: prompt });
|
|
419
|
-
await delay(300);
|
|
426
|
+
await delay(300, signal);
|
|
420
427
|
const verified = await evaluate(
|
|
421
428
|
cdp,
|
|
422
429
|
`(() => {
|
|
@@ -449,7 +456,8 @@ async function typePrompt(cdp, inputCdp, prompt) {
|
|
|
449
456
|
}
|
|
450
457
|
}
|
|
451
458
|
|
|
452
|
-
async function clickSend(cdp, inputCdp) {
|
|
459
|
+
async function clickSend(cdp, inputCdp, signal) {
|
|
460
|
+
throwIfAborted(signal);
|
|
453
461
|
const selectors = SELECTORS.sendButton.split(", ");
|
|
454
462
|
const selectorsJson = JSON.stringify(selectors);
|
|
455
463
|
const deadline = Date.now() + 8000;
|
|
@@ -475,7 +483,7 @@ async function clickSend(cdp, inputCdp) {
|
|
|
475
483
|
);
|
|
476
484
|
if (result === "clicked") return true;
|
|
477
485
|
if (result === "missing") break;
|
|
478
|
-
await delay(100);
|
|
486
|
+
await delay(100, signal);
|
|
479
487
|
}
|
|
480
488
|
await inputCdp("Input.dispatchKeyEvent", {
|
|
481
489
|
type: "keyDown",
|
|
@@ -574,8 +582,10 @@ async function waitForResponse(
|
|
|
574
582
|
cdp,
|
|
575
583
|
timeoutMs = 2700000,
|
|
576
584
|
baselineAssistant,
|
|
577
|
-
baselineAssistantCount
|
|
585
|
+
baselineAssistantCount,
|
|
586
|
+
signal
|
|
578
587
|
) {
|
|
588
|
+
throwIfAborted(signal);
|
|
579
589
|
const deadline = Date.now() + timeoutMs;
|
|
580
590
|
let previousText = "";
|
|
581
591
|
let stableCycles = 0;
|
|
@@ -588,7 +598,7 @@ async function waitForResponse(
|
|
|
588
598
|
const snapshot = await readChatGPTResponseSnapshot(cdp);
|
|
589
599
|
|
|
590
600
|
if (!snapshot) {
|
|
591
|
-
await delay(400);
|
|
601
|
+
await delay(400, signal);
|
|
592
602
|
continue;
|
|
593
603
|
}
|
|
594
604
|
|
|
@@ -602,7 +612,7 @@ async function waitForResponse(
|
|
|
602
612
|
);
|
|
603
613
|
|
|
604
614
|
if (!hasNewAssistantContent) {
|
|
605
|
-
await delay(400);
|
|
615
|
+
await delay(400, signal);
|
|
606
616
|
continue;
|
|
607
617
|
}
|
|
608
618
|
|
|
@@ -630,7 +640,7 @@ async function waitForResponse(
|
|
|
630
640
|
};
|
|
631
641
|
}
|
|
632
642
|
|
|
633
|
-
await delay(400);
|
|
643
|
+
await delay(400, signal);
|
|
634
644
|
}
|
|
635
645
|
|
|
636
646
|
throw new Error("Response timeout");
|
|
@@ -649,26 +659,31 @@ async function query(options) {
|
|
|
649
659
|
cdpCommand,
|
|
650
660
|
uploadFile,
|
|
651
661
|
log = () => {},
|
|
662
|
+
signal,
|
|
652
663
|
} = options;
|
|
664
|
+
throwIfAborted(signal);
|
|
665
|
+
const guardedUploadFile = uploadFile
|
|
666
|
+
? (...args) => raceAbort(() => uploadFile(...args), signal)
|
|
667
|
+
: uploadFile;
|
|
653
668
|
const startTime = Date.now();
|
|
654
669
|
log("Starting ChatGPT query");
|
|
655
|
-
const { cookies } = await getCookies
|
|
670
|
+
const { cookies } = await raceAbort(getCookies, signal);
|
|
656
671
|
if (!hasRequiredCookies(cookies)) {
|
|
657
672
|
throw new Error("ChatGPT login required");
|
|
658
673
|
}
|
|
659
674
|
log(`Got ${cookies.length} cookies`);
|
|
660
|
-
const tabInfo = await createTab
|
|
675
|
+
const tabInfo = await raceAbort(createTab, signal);
|
|
661
676
|
const { tabId } = tabInfo;
|
|
662
677
|
if (!tabId) {
|
|
663
678
|
throw new Error("Failed to create ChatGPT tab");
|
|
664
679
|
}
|
|
665
680
|
log(`Created tab ${tabId}`);
|
|
666
681
|
|
|
667
|
-
const cdp = (expr) => cdpEvaluate(tabId, expr);
|
|
668
|
-
const inputCdp = (method, params) => cdpCommand(tabId, method, params);
|
|
682
|
+
const cdp = (expr) => raceAbort(() => cdpEvaluate(tabId, expr), signal);
|
|
683
|
+
const inputCdp = (method, params) => raceAbort(() => cdpCommand(tabId, method, params), signal);
|
|
669
684
|
|
|
670
685
|
try {
|
|
671
|
-
await waitForPageLoad(cdp);
|
|
686
|
+
await waitForPageLoad(cdp, 45000, signal);
|
|
672
687
|
log("Page loaded");
|
|
673
688
|
if (await isCloudflareBlocked(cdp)) {
|
|
674
689
|
throw new Error("Cloudflare challenge detected - complete in browser");
|
|
@@ -685,13 +700,13 @@ async function query(options) {
|
|
|
685
700
|
throw new Error("ChatGPT login required");
|
|
686
701
|
}
|
|
687
702
|
log("Login verified");
|
|
688
|
-
const promptReady = await waitForPromptReady(cdp);
|
|
703
|
+
const promptReady = await waitForPromptReady(cdp, 30000, signal);
|
|
689
704
|
if (!promptReady) {
|
|
690
705
|
throw new Error("Prompt textarea not ready");
|
|
691
706
|
}
|
|
692
707
|
log("Prompt ready");
|
|
693
708
|
if (model) {
|
|
694
|
-
const selectedLabel = await selectModel(cdp, model);
|
|
709
|
+
const selectedLabel = await selectModel(cdp, model, 8000, signal);
|
|
695
710
|
log(`Selected model: ${selectedLabel}`);
|
|
696
711
|
}
|
|
697
712
|
if (file) {
|
|
@@ -701,7 +716,7 @@ async function query(options) {
|
|
|
701
716
|
const files = Array.isArray(file) ? file : [file];
|
|
702
717
|
const absFiles = files.map((filePath) => path.resolve(process.cwd(), filePath));
|
|
703
718
|
log(`Uploading ${absFiles.length} file(s) to ChatGPT...`);
|
|
704
|
-
const uploadResult = await
|
|
719
|
+
const uploadResult = await guardedUploadFile(tabId, absFiles);
|
|
705
720
|
if (uploadResult?.error) {
|
|
706
721
|
throw new Error(`ChatGPT file upload failed: ${uploadResult.error}`);
|
|
707
722
|
}
|
|
@@ -709,18 +724,19 @@ async function query(options) {
|
|
|
709
724
|
throw new Error("ChatGPT file upload failed: upload did not report success");
|
|
710
725
|
}
|
|
711
726
|
log("File uploaded, waiting for ChatGPT attachment processing...");
|
|
712
|
-
await delay(1500);
|
|
727
|
+
await delay(1500, signal);
|
|
713
728
|
}
|
|
714
|
-
await typePrompt(cdp, inputCdp, prompt);
|
|
729
|
+
await typePrompt(cdp, inputCdp, prompt, signal);
|
|
715
730
|
log("Prompt typed");
|
|
716
731
|
const baseline = normalizeResponseSnapshot(await readChatGPTResponseSnapshot(cdp));
|
|
717
|
-
await clickSend(cdp, inputCdp);
|
|
732
|
+
await clickSend(cdp, inputCdp, signal);
|
|
718
733
|
log("Prompt sent, waiting for response...");
|
|
719
734
|
const response = await waitForResponse(
|
|
720
735
|
cdp,
|
|
721
736
|
timeout,
|
|
722
737
|
baseline.latestAssistant,
|
|
723
|
-
baseline.assistantCount
|
|
738
|
+
baseline.assistantCount,
|
|
739
|
+
signal
|
|
724
740
|
);
|
|
725
741
|
log(`Response received (${response.text.length} chars)`);
|
|
726
742
|
return {
|