surf-cli 2.7.2 → 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 +208 -13
- 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 +169 -0
- package/native/chatgpt-client.cjs +63 -30
- package/native/cli.cjs +947 -460
- package/native/client-transport.cjs +168 -0
- package/native/config.cjs +2 -2
- package/native/do-executor.cjs +25 -51
- package/native/do-parser.cjs +12 -0
- package/native/doctor.cjs +633 -0
- package/native/endpoint.cjs +174 -0
- package/native/file-transfer.cjs +734 -0
- package/native/gemini-client.cjs +244 -88
- package/native/grok-client.cjs +321 -212
- package/native/host-helpers.cjs +88 -16
- package/native/host-sessions.cjs +283 -0
- package/native/host.cjs +811 -616
- package/native/listener.cjs +20 -0
- package/native/mcp-server.cjs +60 -62
- 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 +46 -0
- package/package.json +11 -9
- package/scripts/install-native-host.cjs +184 -51
- package/scripts/uninstall-native-host.cjs +93 -15
- package/skills/README.md +11 -5
- package/skills/deep-x-research/SKILL.md +106 -0
- package/skills/surf/SKILL.md +77 -22
- 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
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const net = require("net");
|
|
2
|
+
|
|
3
|
+
function parseListenEndpoint(value) {
|
|
4
|
+
const v6 = typeof value === "string" && value.match(/^\[([^\]]+)\]:(\d+)$/);
|
|
5
|
+
const v4 = typeof value === "string" && value.match(/^([^:]+):(\d+)$/);
|
|
6
|
+
const host = v6 ? v6[1] : v4?.[1];
|
|
7
|
+
const port = Number(v6 ? v6[2] : v4?.[2]);
|
|
8
|
+
if (!host || !Number.isInteger(port) || port < 1 || port > 65535) throw new Error("SURF_LISTEN must be a Tailnet IP and port 1..65535");
|
|
9
|
+
if (v6) {
|
|
10
|
+
if (net.isIP(host) !== 6) throw new Error("SURF_LISTEN must use a Tailscale IPv6 address");
|
|
11
|
+
const canonical = new URL(`http://[${host}]`).hostname.slice(1, -1);
|
|
12
|
+
if (!canonical.startsWith("fd7a:115c:a1e0:")) throw new Error("SURF_LISTEN must use a Tailscale IPv6 address");
|
|
13
|
+
return { host: canonical, port, display: `[${canonical}]:${port}` };
|
|
14
|
+
}
|
|
15
|
+
const parts = host.split(".").map(Number);
|
|
16
|
+
if (net.isIP(host) !== 4 || parts[0] !== 100 || parts[1] < 64 || parts[1] > 127) throw new Error("SURF_LISTEN must use a Tailscale IPv4 address");
|
|
17
|
+
return { host, port, display: `${host}:${port}` };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { parseListenEndpoint };
|
package/native/mcp-server.cjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const net = require("net");
|
|
3
2
|
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
4
3
|
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
5
4
|
const { z } = require("zod");
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
5
|
+
const { formatSocketError } = require("./socket-path.cjs");
|
|
6
|
+
const { selectEndpoint } = require("./endpoint.cjs");
|
|
7
|
+
const { openClientTransport } = require("./client-transport.cjs");
|
|
8
|
+
const { resolveRequestDeadlineMs } = require("./host-sessions.cjs");
|
|
9
|
+
const { prepareRemoteTool, validateLocalToolPaths } = require("./file-transfer.cjs");
|
|
9
10
|
|
|
10
11
|
const TOOL_SCHEMAS = {
|
|
11
12
|
navigate: {
|
|
@@ -263,60 +264,56 @@ const TOOL_SCHEMAS = {
|
|
|
263
264
|
query: z.string().describe("Question about the page"),
|
|
264
265
|
mode: z.enum(["find", "summary", "extract"]).optional().describe("Query mode")
|
|
265
266
|
}
|
|
267
|
+
},
|
|
268
|
+
chatgpt: {
|
|
269
|
+
desc: "Ask ChatGPT through the browser session",
|
|
270
|
+
schema: {
|
|
271
|
+
query: z.string().describe("Question or prompt"),
|
|
272
|
+
model: z.string().optional().describe("ChatGPT model"),
|
|
273
|
+
"with-page": z.boolean().optional().describe("Include current page context"),
|
|
274
|
+
file: z.string().optional().describe("One attachment path"),
|
|
275
|
+
timeout: z.number().optional().describe("Timeout in seconds")
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
gemini: {
|
|
279
|
+
desc: "Ask Gemini or generate/edit one image",
|
|
280
|
+
schema: {
|
|
281
|
+
query: z.string().optional().describe("Question or image prompt"),
|
|
282
|
+
model: z.string().optional().describe("Gemini model"),
|
|
283
|
+
"with-page": z.boolean().optional().describe("Include current page context"),
|
|
284
|
+
file: z.string().optional().describe("One attachment path"),
|
|
285
|
+
"edit-image": z.string().optional().describe("One image input path"),
|
|
286
|
+
"generate-image": z.string().optional().describe("One generated image output path"),
|
|
287
|
+
output: z.string().optional().describe("Edited image output path"),
|
|
288
|
+
youtube: z.string().optional().describe("YouTube URL"),
|
|
289
|
+
"aspect-ratio": z.string().optional().describe("Image aspect ratio"),
|
|
290
|
+
timeout: z.number().optional().describe("Timeout in seconds")
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
"network.export": {
|
|
294
|
+
desc: "Export captured network requests",
|
|
295
|
+
schema: {
|
|
296
|
+
output: z.string().optional().describe("Output file path"),
|
|
297
|
+
jsonl: z.boolean().optional().describe("Write JSONL"),
|
|
298
|
+
har: z.boolean().optional().describe("Write HAR 1.2")
|
|
299
|
+
}
|
|
266
300
|
}
|
|
267
301
|
};
|
|
268
302
|
|
|
269
|
-
function sendSocketRequest(tool, args = {}) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
sock.destroy();
|
|
284
|
-
reject(new Error("Request timeout"));
|
|
285
|
-
}, REQUEST_TIMEOUT);
|
|
286
|
-
|
|
287
|
-
sock.on("data", (d) => {
|
|
288
|
-
buf += d.toString();
|
|
289
|
-
const lines = buf.split("\n");
|
|
290
|
-
buf = lines.pop();
|
|
291
|
-
for (const line of lines) {
|
|
292
|
-
if (!line.trim()) continue;
|
|
293
|
-
try {
|
|
294
|
-
clearTimeout(timeout);
|
|
295
|
-
const resp = JSON.parse(line);
|
|
296
|
-
sock.end();
|
|
297
|
-
resolve(resp);
|
|
298
|
-
} catch {
|
|
299
|
-
clearTimeout(timeout);
|
|
300
|
-
sock.end();
|
|
301
|
-
reject(new Error("Invalid JSON response"));
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
sock.on("error", (e) => {
|
|
307
|
-
clearTimeout(timeout);
|
|
308
|
-
if (e.code === "ENOENT") {
|
|
309
|
-
reject(new Error("Socket not found. Is Chrome running with the surf extension?"));
|
|
310
|
-
} else {
|
|
311
|
-
reject(e);
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
sock.on("close", () => {
|
|
316
|
-
clearTimeout(timeout);
|
|
317
|
-
reject(new Error("Socket closed unexpectedly"));
|
|
318
|
-
});
|
|
319
|
-
});
|
|
303
|
+
async function sendSocketRequest(tool, args = {}, endpoint = selectEndpoint([]).endpoint) {
|
|
304
|
+
const requestTimeoutMs = resolveRequestDeadlineMs(tool, args);
|
|
305
|
+
const transport = await openClientTransport(endpoint, { requestTimeoutMs });
|
|
306
|
+
try {
|
|
307
|
+
const prepared = endpoint.kind === "remote" ? prepareRemoteTool(tool, args) : (() => { const normalized = validateLocalToolPaths(tool, args); return { args: normalized, uploads: [], downloads: [] }; })();
|
|
308
|
+
return await transport.request({
|
|
309
|
+
type: "tool_request",
|
|
310
|
+
method: "execute_tool",
|
|
311
|
+
params: { tool, args: prepared.args },
|
|
312
|
+
id: "mcp-" + Date.now() + "-" + Math.random(),
|
|
313
|
+
}, requestTimeoutMs, prepared);
|
|
314
|
+
} finally {
|
|
315
|
+
await transport.close();
|
|
316
|
+
}
|
|
320
317
|
}
|
|
321
318
|
|
|
322
319
|
function formatResult(resp) {
|
|
@@ -348,7 +345,8 @@ function formatResult(resp) {
|
|
|
348
345
|
}
|
|
349
346
|
|
|
350
347
|
class PiChromeMcpServer {
|
|
351
|
-
constructor() {
|
|
348
|
+
constructor(endpoint = selectEndpoint([]).endpoint) {
|
|
349
|
+
this.endpoint = endpoint;
|
|
352
350
|
this.server = new McpServer({
|
|
353
351
|
name: "surf",
|
|
354
352
|
version: "1.0.0"
|
|
@@ -370,7 +368,7 @@ class PiChromeMcpServer {
|
|
|
370
368
|
schemaObj,
|
|
371
369
|
async (args) => {
|
|
372
370
|
try {
|
|
373
|
-
const resp = await sendSocketRequest(name, args);
|
|
371
|
+
const resp = await sendSocketRequest(name, args, this.endpoint);
|
|
374
372
|
return formatResult(resp);
|
|
375
373
|
} catch (err) {
|
|
376
374
|
return {
|
|
@@ -389,7 +387,7 @@ class PiChromeMcpServer {
|
|
|
389
387
|
"page://current",
|
|
390
388
|
async (uri) => {
|
|
391
389
|
try {
|
|
392
|
-
const resp = await sendSocketRequest("page.read", {});
|
|
390
|
+
const resp = await sendSocketRequest("page.read", {}, this.endpoint);
|
|
393
391
|
const text = resp.result?.content?.[0]?.text || "No content";
|
|
394
392
|
return {
|
|
395
393
|
contents: [{
|
|
@@ -415,7 +413,7 @@ class PiChromeMcpServer {
|
|
|
415
413
|
"tabs://list",
|
|
416
414
|
async (uri) => {
|
|
417
415
|
try {
|
|
418
|
-
const resp = await sendSocketRequest("tab.list", {});
|
|
416
|
+
const resp = await sendSocketRequest("tab.list", {}, this.endpoint);
|
|
419
417
|
const text = resp.result?.content?.[0]?.text || "[]";
|
|
420
418
|
return {
|
|
421
419
|
contents: [{
|
|
@@ -441,7 +439,7 @@ class PiChromeMcpServer {
|
|
|
441
439
|
"console://messages",
|
|
442
440
|
async (uri) => {
|
|
443
441
|
try {
|
|
444
|
-
const resp = await sendSocketRequest("console", {});
|
|
442
|
+
const resp = await sendSocketRequest("console", {}, this.endpoint);
|
|
445
443
|
const text = resp.result?.content?.[0]?.text || "No messages";
|
|
446
444
|
return {
|
|
447
445
|
contents: [{
|
|
@@ -467,7 +465,7 @@ class PiChromeMcpServer {
|
|
|
467
465
|
"network://requests",
|
|
468
466
|
async (uri) => {
|
|
469
467
|
try {
|
|
470
|
-
const resp = await sendSocketRequest("network", {});
|
|
468
|
+
const resp = await sendSocketRequest("network", {}, this.endpoint);
|
|
471
469
|
const text = resp.result?.content?.[0]?.text || "No requests";
|
|
472
470
|
return {
|
|
473
471
|
contents: [{
|
|
@@ -508,4 +506,4 @@ if (require.main === module) {
|
|
|
508
506
|
});
|
|
509
507
|
}
|
|
510
508
|
|
|
511
|
-
module.exports = { PiChromeMcpServer };
|
|
509
|
+
module.exports = { PiChromeMcpServer, TOOL_SCHEMAS };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { version: PACKAGE_VERSION } = require("../package.json");
|
|
5
|
+
|
|
6
|
+
const MAX_NETWORK_EXPORT_FILE_BYTES = 256 * 1024 * 1024;
|
|
7
|
+
const INTERNAL_FIELDS = new Set(["_requestId", "_responseReceived", "_loadingFinished"]);
|
|
8
|
+
|
|
9
|
+
function publicEntry(entry) {
|
|
10
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
11
|
+
throw new Error("network export entries must be objects");
|
|
12
|
+
}
|
|
13
|
+
const result = Object.create(null);
|
|
14
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
15
|
+
if (!INTERNAL_FIELDS.has(key)) result[key] = value;
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function headerList(headers) {
|
|
21
|
+
if (!headers || typeof headers !== "object" || Array.isArray(headers)) return [];
|
|
22
|
+
return Object.entries(headers).map(([name, value]) => ({ name, value: String(value) }));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function harEntry(entry) {
|
|
26
|
+
const requestBody = entry.requestBody;
|
|
27
|
+
const responseBody = entry.responseBody;
|
|
28
|
+
const requestHeaders = entry.requestHeaders;
|
|
29
|
+
const responseHeaders = entry.responseHeaders;
|
|
30
|
+
const duration = Number.isFinite(entry.duration) ? Math.max(0, entry.duration) : 0;
|
|
31
|
+
const ttfb = Number.isFinite(entry.ttfb) ? Math.max(0, entry.ttfb) : duration;
|
|
32
|
+
return {
|
|
33
|
+
startedDateTime: new Date(Number(entry.ts) || Date.now()).toISOString(),
|
|
34
|
+
time: duration,
|
|
35
|
+
request: {
|
|
36
|
+
method: entry.method || "GET",
|
|
37
|
+
url: entry.url || "",
|
|
38
|
+
httpVersion: "HTTP/1.1",
|
|
39
|
+
headers: headerList(requestHeaders),
|
|
40
|
+
queryString: [],
|
|
41
|
+
cookies: [],
|
|
42
|
+
headersSize: -1,
|
|
43
|
+
bodySize: Number.isFinite(entry.requestBodySize) ? entry.requestBodySize : requestBody ? Buffer.byteLength(String(requestBody)) : -1,
|
|
44
|
+
...(requestBody !== undefined ? { postData: { mimeType: "application/octet-stream", text: String(requestBody) } } : {}),
|
|
45
|
+
},
|
|
46
|
+
response: {
|
|
47
|
+
status: Number.isFinite(entry.status) ? entry.status : 0,
|
|
48
|
+
statusText: entry.statusText || "",
|
|
49
|
+
httpVersion: "HTTP/1.1",
|
|
50
|
+
headers: headerList(responseHeaders),
|
|
51
|
+
cookies: [],
|
|
52
|
+
content: {
|
|
53
|
+
size: Number.isFinite(entry.responseBodySize) ? entry.responseBodySize : responseBody ? Buffer.byteLength(String(responseBody)) : 0,
|
|
54
|
+
mimeType: entry.mimeType || "",
|
|
55
|
+
...(responseBody !== undefined ? { text: String(responseBody) } : {}),
|
|
56
|
+
},
|
|
57
|
+
redirectURL: "",
|
|
58
|
+
headersSize: -1,
|
|
59
|
+
bodySize: Number.isFinite(entry.responseBodySize) ? entry.responseBodySize : responseBody ? Buffer.byteLength(String(responseBody)) : -1,
|
|
60
|
+
},
|
|
61
|
+
cache: {},
|
|
62
|
+
timings: { send: 0, wait: ttfb, receive: Math.max(0, duration - ttfb) },
|
|
63
|
+
...(entry.comment ? { comment: String(entry.comment) } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function serializeNetworkExport(entries, format = "json") {
|
|
68
|
+
if (!Array.isArray(entries)) throw new Error("network export entries must be an array");
|
|
69
|
+
if (format !== "json" && format !== "jsonl" && format !== "har") throw new Error(`unsupported network export format: ${format}`);
|
|
70
|
+
const publicEntries = entries.map(publicEntry);
|
|
71
|
+
if (format === "jsonl") return `${publicEntries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
|
|
72
|
+
if (format === "har") {
|
|
73
|
+
return JSON.stringify({
|
|
74
|
+
log: {
|
|
75
|
+
version: "1.2",
|
|
76
|
+
creator: { name: "surf-cli", version: PACKAGE_VERSION },
|
|
77
|
+
entries: publicEntries.map(harEntry),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return JSON.stringify(publicEntries, null, 2);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writeNetworkExport(outputPath, entries, format = "json") {
|
|
85
|
+
if (typeof outputPath !== "string" || !path.isAbsolute(outputPath)) throw new Error("network export output must be an absolute path");
|
|
86
|
+
const content = serializeNetworkExport(entries, format);
|
|
87
|
+
const bytes = Buffer.byteLength(content);
|
|
88
|
+
if (bytes > MAX_NETWORK_EXPORT_FILE_BYTES) throw new Error("network export exceeds the 256 MiB file limit");
|
|
89
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
90
|
+
const temporaryPath = path.join(path.dirname(outputPath), `.${path.basename(outputPath)}.surf-${crypto.randomBytes(12).toString("hex")}.tmp`);
|
|
91
|
+
try {
|
|
92
|
+
const fd = fs.openSync(temporaryPath, "wx", 0o600);
|
|
93
|
+
try {
|
|
94
|
+
fs.writeFileSync(fd, content, "utf8");
|
|
95
|
+
fs.fchmodSync(fd, 0o600);
|
|
96
|
+
} finally {
|
|
97
|
+
fs.closeSync(fd);
|
|
98
|
+
}
|
|
99
|
+
fs.renameSync(temporaryPath, outputPath);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
try { fs.rmSync(temporaryPath, { force: true }); } catch {}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
return { path: outputPath, format, count: entries.length, bytes };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
INTERNAL_FIELDS,
|
|
109
|
+
MAX_NETWORK_EXPORT_FILE_BYTES,
|
|
110
|
+
publicEntry,
|
|
111
|
+
serializeNetworkExport,
|
|
112
|
+
writeNetworkExport,
|
|
113
|
+
};
|
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
* Similar approach to the ChatGPT client.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
const { abortableDelay, raceAbort, throwIfAborted } = require("./abort.cjs");
|
|
9
|
+
|
|
8
10
|
const PERPLEXITY_URL = "https://www.perplexity.ai/";
|
|
9
11
|
|
|
10
12
|
// ============================================================================
|
|
11
13
|
// Helpers
|
|
12
14
|
// ============================================================================
|
|
13
15
|
|
|
14
|
-
function delay(ms) {
|
|
15
|
-
return
|
|
16
|
+
function delay(ms, signal) {
|
|
17
|
+
return abortableDelay(ms, signal);
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
function buildClickDispatcher() {
|
|
@@ -371,7 +373,27 @@ async function submitPrompt(cdp, inputCdp) {
|
|
|
371
373
|
// Response Handling
|
|
372
374
|
// ============================================================================
|
|
373
375
|
|
|
374
|
-
|
|
376
|
+
function extractPerplexityResponseText() {
|
|
377
|
+
const selectors = [
|
|
378
|
+
'[id^="markdown-content"]',
|
|
379
|
+
'[data-testid="answer"]',
|
|
380
|
+
'article',
|
|
381
|
+
'.prose',
|
|
382
|
+
];
|
|
383
|
+
|
|
384
|
+
for (const selector of selectors) {
|
|
385
|
+
const elements = Array.from(document.querySelectorAll(selector));
|
|
386
|
+
for (let i = elements.length - 1; i >= 0; i--) {
|
|
387
|
+
const text = elements[i].innerText?.trim() || '';
|
|
388
|
+
if (text) return text;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return '';
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function waitForResponse(cdp, timeoutMs = 120000, signal) {
|
|
396
|
+
throwIfAborted(signal);
|
|
375
397
|
const deadline = Date.now() + timeoutMs;
|
|
376
398
|
let previousText = '';
|
|
377
399
|
let stableCycles = 0;
|
|
@@ -386,17 +408,16 @@ async function waitForResponse(cdp, timeoutMs = 120000) {
|
|
|
386
408
|
if (url && url.includes('/search/')) {
|
|
387
409
|
break;
|
|
388
410
|
}
|
|
389
|
-
await delay(200);
|
|
411
|
+
await delay(200, signal);
|
|
390
412
|
}
|
|
391
413
|
|
|
392
414
|
// Wait a bit for the response area to render
|
|
393
|
-
await delay(1000);
|
|
415
|
+
await delay(1000, signal);
|
|
394
416
|
|
|
395
417
|
// Now poll for response completion
|
|
396
418
|
while (Date.now() < deadline) {
|
|
397
419
|
const snapshot = await evaluate(cdp, `(function() {
|
|
398
|
-
const
|
|
399
|
-
const text = prose ? prose.innerText : '';
|
|
420
|
+
const text = (${extractPerplexityResponseText.toString()})();
|
|
400
421
|
const hasStop = !!document.querySelector('button[aria-label*=stop], button[aria-label*=Stop]');
|
|
401
422
|
const hasCopy = !!document.querySelector('button[aria-label*=copy], button[aria-label*=Copy]');
|
|
402
423
|
const hasRelated = document.body.innerText.indexOf('Related') > -1;
|
|
@@ -412,7 +433,7 @@ async function waitForResponse(cdp, timeoutMs = 120000) {
|
|
|
412
433
|
})()`);
|
|
413
434
|
|
|
414
435
|
if (!snapshot) {
|
|
415
|
-
await delay(300);
|
|
436
|
+
await delay(300, signal);
|
|
416
437
|
continue;
|
|
417
438
|
}
|
|
418
439
|
|
|
@@ -437,7 +458,7 @@ async function waitForResponse(cdp, timeoutMs = 120000) {
|
|
|
437
458
|
const hasCompletionIndicators = snapshot.hasActions || snapshot.hasRelated || snapshot.hasFollowUp;
|
|
438
459
|
const isDone = !snapshot.generating && (hasCompletionIndicators || isStable);
|
|
439
460
|
|
|
440
|
-
if (isDone && currentText.length >
|
|
461
|
+
if (isDone && currentText.trim().length > 0) {
|
|
441
462
|
// Clean up the response text
|
|
442
463
|
let cleanText = currentText;
|
|
443
464
|
|
|
@@ -454,11 +475,11 @@ async function waitForResponse(cdp, timeoutMs = 120000) {
|
|
|
454
475
|
};
|
|
455
476
|
}
|
|
456
477
|
|
|
457
|
-
await delay(300);
|
|
478
|
+
await delay(300, signal);
|
|
458
479
|
}
|
|
459
480
|
|
|
460
481
|
// Timeout - return whatever we have
|
|
461
|
-
if (previousText.length >
|
|
482
|
+
if (previousText.trim().length > 0) {
|
|
462
483
|
return {
|
|
463
484
|
text: previousText,
|
|
464
485
|
sources: 0,
|
|
@@ -485,13 +506,15 @@ async function query(options) {
|
|
|
485
506
|
cdpEvaluate,
|
|
486
507
|
cdpCommand,
|
|
487
508
|
log = () => {},
|
|
509
|
+
signal,
|
|
488
510
|
} = options;
|
|
511
|
+
throwIfAborted(signal);
|
|
489
512
|
|
|
490
513
|
const startTime = Date.now();
|
|
491
514
|
log("Starting Perplexity query");
|
|
492
515
|
|
|
493
516
|
// Create tab
|
|
494
|
-
const tabInfo = await createTab
|
|
517
|
+
const tabInfo = await raceAbort(createTab, signal);
|
|
495
518
|
log(`createTab returned: ${JSON.stringify(tabInfo)}`);
|
|
496
519
|
const { tabId } = tabInfo || {};
|
|
497
520
|
|
|
@@ -500,8 +523,8 @@ async function query(options) {
|
|
|
500
523
|
}
|
|
501
524
|
log(`Created tab ${tabId}`);
|
|
502
525
|
|
|
503
|
-
const cdp = (expr) => cdpEvaluate(tabId, expr);
|
|
504
|
-
const inputCdp = (method, params) => cdpCommand(tabId, method, params);
|
|
526
|
+
const cdp = (expr) => raceAbort(() => cdpEvaluate(tabId, expr), signal);
|
|
527
|
+
const inputCdp = (method, params) => raceAbort(() => cdpCommand(tabId, method, params), signal);
|
|
505
528
|
|
|
506
529
|
try {
|
|
507
530
|
// Wait for page load
|
|
@@ -522,6 +545,7 @@ async function query(options) {
|
|
|
522
545
|
const selectedMode = await selectMode(cdp, mode);
|
|
523
546
|
log(`Mode: ${selectedMode}`);
|
|
524
547
|
} catch (e) {
|
|
548
|
+
if (signal?.aborted) throw e;
|
|
525
549
|
log(`Mode selection failed: ${e.message}`);
|
|
526
550
|
}
|
|
527
551
|
}
|
|
@@ -532,6 +556,7 @@ async function query(options) {
|
|
|
532
556
|
const selectedModel = await selectModel(cdp, model);
|
|
533
557
|
log(`Model: ${selectedModel}`);
|
|
534
558
|
} catch (e) {
|
|
559
|
+
if (signal?.aborted) throw e;
|
|
535
560
|
log(`Model selection failed: ${e.message}`);
|
|
536
561
|
}
|
|
537
562
|
}
|
|
@@ -545,7 +570,7 @@ async function query(options) {
|
|
|
545
570
|
log("Submitted, waiting for response...");
|
|
546
571
|
|
|
547
572
|
// Wait for response
|
|
548
|
-
const response = await waitForResponse(cdp, timeout);
|
|
573
|
+
const response = await waitForResponse(cdp, timeout, signal);
|
|
549
574
|
log(`Response: ${response.text.length} chars, ${response.sources} sources${response.partial ? ' (partial)' : ''}`);
|
|
550
575
|
|
|
551
576
|
return {
|
|
@@ -558,8 +583,12 @@ async function query(options) {
|
|
|
558
583
|
tookMs: Date.now() - startTime,
|
|
559
584
|
};
|
|
560
585
|
} finally {
|
|
561
|
-
|
|
586
|
+
try {
|
|
587
|
+
await closeTab(tabId);
|
|
588
|
+
} catch (error) {
|
|
589
|
+
log(`Failed to close Perplexity tab ${tabId}: ${error?.message || error}`);
|
|
590
|
+
}
|
|
562
591
|
}
|
|
563
592
|
}
|
|
564
593
|
|
|
565
|
-
module.exports = { query, PERPLEXITY_URL };
|
|
594
|
+
module.exports = { query, PERPLEXITY_URL, waitForResponse, extractPerplexityResponseText };
|