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
package/native/cli.cjs
CHANGED
|
@@ -1,21 +1,68 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const net = require("net");
|
|
3
2
|
const fs = require("fs");
|
|
4
3
|
const path = require("path");
|
|
5
4
|
const os = require("os");
|
|
6
|
-
const { execSync } = require("child_process");
|
|
5
|
+
const { execFileSync, execSync } = require("child_process");
|
|
7
6
|
const { loadConfig, getConfigPath, createStarterConfig } = require("./config.cjs");
|
|
8
7
|
const networkFormatters = require("./formatters/network.cjs");
|
|
9
8
|
const networkStore = require("./network-store.cjs");
|
|
10
9
|
const { parseDoCommands } = require("./do-parser.cjs");
|
|
11
10
|
const { executeDoSteps } = require("./do-executor.cjs");
|
|
11
|
+
const { openClientTransport } = require("./client-transport.cjs");
|
|
12
12
|
const { version: VERSION } = require("../package.json");
|
|
13
13
|
|
|
14
14
|
const IS_WIN = process.platform === "win32";
|
|
15
|
-
const SURF_TMP
|
|
16
|
-
const
|
|
15
|
+
const { SURF_TMP, formatSocketError } = require("./socket-path.cjs");
|
|
16
|
+
const { acquireBrowserLock } = require("./browser-lock.cjs");
|
|
17
|
+
const { selectEndpoint, connectEndpoint, formatEndpointError } = require("./endpoint.cjs");
|
|
18
|
+
const { createFrameParser, createSocketWriter, writeFrame } = require("./remote-transport.cjs");
|
|
19
|
+
const { resolveRequestDeadlineMs } = require("./host-sessions.cjs");
|
|
20
|
+
const { AUTO_SCREENSHOT_TOOLS, prepareRemoteTool, validateLocalToolPaths } = require("./file-transfer.cjs");
|
|
21
|
+
const { authorizeClient, listClients, revokeClient, getStateDir } = require("./remote-auth.cjs");
|
|
17
22
|
if (IS_WIN) { try { fs.mkdirSync(SURF_TMP, { recursive: true }); } catch {} }
|
|
18
23
|
|
|
24
|
+
function parseBrowserLockOptions(noLockFlag) {
|
|
25
|
+
const noLock = noLockFlag || process.env.SURF_NO_LOCK === "1" || process.env.SURF_NO_LOCK === "true";
|
|
26
|
+
let timeoutMs;
|
|
27
|
+
if (process.env.SURF_LOCK_TIMEOUT_MS !== undefined) {
|
|
28
|
+
timeoutMs = Number(process.env.SURF_LOCK_TIMEOUT_MS);
|
|
29
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
30
|
+
console.error("Error: SURF_LOCK_TIMEOUT_MS must be a non-negative number");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { noLock, timeoutMs };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function installBrowserLock({ noLock, timeoutMs }, endpoint) {
|
|
38
|
+
let releaseBrowserLock = () => {};
|
|
39
|
+
if (!noLock) {
|
|
40
|
+
try {
|
|
41
|
+
const lock = acquireBrowserLock(endpoint.key, SURF_TMP, { timeoutMs });
|
|
42
|
+
releaseBrowserLock = lock.release;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error("Error:", error && error.message ? error.message : String(error));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const release = () => {
|
|
50
|
+
const releaseCurrent = releaseBrowserLock;
|
|
51
|
+
releaseBrowserLock = () => {};
|
|
52
|
+
releaseCurrent();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
process.once("exit", release);
|
|
56
|
+
process.once("SIGINT", () => {
|
|
57
|
+
release();
|
|
58
|
+
process.exit(130);
|
|
59
|
+
});
|
|
60
|
+
process.once("SIGTERM", () => {
|
|
61
|
+
release();
|
|
62
|
+
process.exit(143);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
19
66
|
// ============================================================================
|
|
20
67
|
// Workflow Resolution and Management
|
|
21
68
|
// ============================================================================
|
|
@@ -41,7 +88,7 @@ function resolveWorkflow(nameOrPath) {
|
|
|
41
88
|
if (nameOrPath.includes('|')) {
|
|
42
89
|
return { type: 'inline', content: nameOrPath };
|
|
43
90
|
}
|
|
44
|
-
|
|
91
|
+
|
|
45
92
|
// Check if it's a direct file path (with extension or path separator)
|
|
46
93
|
if (nameOrPath.includes('/') || nameOrPath.includes('\\') || nameOrPath.endsWith('.json')) {
|
|
47
94
|
if (fs.existsSync(nameOrPath)) {
|
|
@@ -49,17 +96,17 @@ function resolveWorkflow(nameOrPath) {
|
|
|
49
96
|
}
|
|
50
97
|
return { type: 'not_found', name: nameOrPath };
|
|
51
98
|
}
|
|
52
|
-
|
|
99
|
+
|
|
53
100
|
// Look up by name in workflow directories
|
|
54
101
|
const searchDirs = getWorkflowDirs();
|
|
55
|
-
|
|
102
|
+
|
|
56
103
|
for (const { path: dir } of searchDirs) {
|
|
57
104
|
const filePath = path.join(dir, `${nameOrPath}.json`);
|
|
58
105
|
if (fs.existsSync(filePath)) {
|
|
59
106
|
return { type: 'file', path: filePath };
|
|
60
107
|
}
|
|
61
108
|
}
|
|
62
|
-
|
|
109
|
+
|
|
63
110
|
return { type: 'not_found', name: nameOrPath };
|
|
64
111
|
}
|
|
65
112
|
|
|
@@ -70,7 +117,7 @@ function resolveWorkflow(nameOrPath) {
|
|
|
70
117
|
function listWorkflows() {
|
|
71
118
|
const workflows = [];
|
|
72
119
|
const searchDirs = getWorkflowDirs();
|
|
73
|
-
|
|
120
|
+
|
|
74
121
|
for (const { path: dir, scope } of searchDirs) {
|
|
75
122
|
if (fs.existsSync(dir)) {
|
|
76
123
|
try {
|
|
@@ -96,7 +143,7 @@ function listWorkflows() {
|
|
|
96
143
|
}
|
|
97
144
|
}
|
|
98
145
|
}
|
|
99
|
-
|
|
146
|
+
|
|
100
147
|
return workflows;
|
|
101
148
|
}
|
|
102
149
|
|
|
@@ -107,15 +154,15 @@ function listWorkflows() {
|
|
|
107
154
|
*/
|
|
108
155
|
function getWorkflowInfo(name) {
|
|
109
156
|
const resolved = resolveWorkflow(name);
|
|
110
|
-
|
|
157
|
+
|
|
111
158
|
if (resolved.type === 'not_found') {
|
|
112
159
|
return { error: `Workflow not found: ${name}` };
|
|
113
160
|
}
|
|
114
|
-
|
|
161
|
+
|
|
115
162
|
if (resolved.type === 'inline') {
|
|
116
163
|
return { error: 'Cannot get info for inline workflows' };
|
|
117
164
|
}
|
|
118
|
-
|
|
165
|
+
|
|
119
166
|
try {
|
|
120
167
|
const content = JSON.parse(fs.readFileSync(resolved.path, 'utf8'));
|
|
121
168
|
return {
|
|
@@ -175,24 +222,24 @@ function validateWorkflowFile(filePath) {
|
|
|
175
222
|
if (!fs.existsSync(filePath)) {
|
|
176
223
|
return { valid: false, error: `File not found: ${filePath}` };
|
|
177
224
|
}
|
|
178
|
-
|
|
225
|
+
|
|
179
226
|
try {
|
|
180
227
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
181
228
|
const workflow = JSON.parse(content);
|
|
182
|
-
|
|
229
|
+
|
|
183
230
|
// Basic structure validation
|
|
184
231
|
if (!workflow.steps || !Array.isArray(workflow.steps)) {
|
|
185
232
|
return { valid: false, error: "Workflow must have a 'steps' array" };
|
|
186
233
|
}
|
|
187
|
-
|
|
234
|
+
|
|
188
235
|
if (workflow.steps.length === 0) {
|
|
189
236
|
return { valid: false, error: "Workflow has no steps" };
|
|
190
237
|
}
|
|
191
|
-
|
|
238
|
+
|
|
192
239
|
// Validate each step
|
|
193
240
|
for (let i = 0; i < workflow.steps.length; i++) {
|
|
194
241
|
const step = workflow.steps[i];
|
|
195
|
-
|
|
242
|
+
|
|
196
243
|
// Check for loops
|
|
197
244
|
if (step.repeat !== undefined || step.each !== undefined) {
|
|
198
245
|
if (!step.steps || !Array.isArray(step.steps)) {
|
|
@@ -200,18 +247,18 @@ function validateWorkflowFile(filePath) {
|
|
|
200
247
|
}
|
|
201
248
|
continue;
|
|
202
249
|
}
|
|
203
|
-
|
|
250
|
+
|
|
204
251
|
// Regular step must have tool/cmd
|
|
205
252
|
if (!step.tool && !step.cmd) {
|
|
206
253
|
return { valid: false, error: `Step ${i + 1}: must have 'tool' field` };
|
|
207
254
|
}
|
|
208
255
|
}
|
|
209
|
-
|
|
256
|
+
|
|
210
257
|
// Validate args schema if present
|
|
211
258
|
if (workflow.args && typeof workflow.args !== 'object') {
|
|
212
259
|
return { valid: false, error: "'args' must be an object" };
|
|
213
260
|
}
|
|
214
|
-
|
|
261
|
+
|
|
215
262
|
return { valid: true, workflow };
|
|
216
263
|
} catch (e) {
|
|
217
264
|
return { valid: false, error: `Invalid JSON: ${e.message}` };
|
|
@@ -226,7 +273,7 @@ function validateWorkflowFile(filePath) {
|
|
|
226
273
|
*/
|
|
227
274
|
function formatStep(step, indent = 0) {
|
|
228
275
|
const pad = ' '.repeat(indent);
|
|
229
|
-
|
|
276
|
+
|
|
230
277
|
if (step.repeat !== undefined) {
|
|
231
278
|
const lines = [`${pad}repeat ${step.repeat} times:`];
|
|
232
279
|
for (const s of step.steps || []) {
|
|
@@ -237,7 +284,7 @@ function formatStep(step, indent = 0) {
|
|
|
237
284
|
}
|
|
238
285
|
return lines.join('\n');
|
|
239
286
|
}
|
|
240
|
-
|
|
287
|
+
|
|
241
288
|
if (step.each !== undefined) {
|
|
242
289
|
const lines = [`${pad}each ${step.each} as ${step.as || 'item'}:`];
|
|
243
290
|
for (const s of step.steps || []) {
|
|
@@ -245,24 +292,24 @@ function formatStep(step, indent = 0) {
|
|
|
245
292
|
}
|
|
246
293
|
return lines.join('\n');
|
|
247
294
|
}
|
|
248
|
-
|
|
295
|
+
|
|
249
296
|
const tool = step.tool || step.cmd;
|
|
250
297
|
const args = step.args || {};
|
|
251
298
|
const argStr = Object.entries(args)
|
|
252
299
|
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
253
300
|
.join(' ');
|
|
254
|
-
|
|
301
|
+
|
|
255
302
|
let line = `${pad}${tool}`;
|
|
256
303
|
if (argStr) line += ` ${argStr}`;
|
|
257
304
|
if (step.as) line += ` → ${step.as}`;
|
|
258
|
-
|
|
305
|
+
|
|
259
306
|
return line;
|
|
260
307
|
}
|
|
261
308
|
|
|
262
309
|
// Cross-platform image resize (macOS: sips, Linux: ImageMagick)
|
|
263
310
|
function resizeImage(filePath, maxSize) {
|
|
264
311
|
const platform = process.platform;
|
|
265
|
-
|
|
312
|
+
|
|
266
313
|
try {
|
|
267
314
|
if (platform === "darwin") {
|
|
268
315
|
// macOS: use sips
|
|
@@ -294,7 +341,50 @@ function resizeImage(filePath, maxSize) {
|
|
|
294
341
|
return { success: false, error: e.message };
|
|
295
342
|
}
|
|
296
343
|
}
|
|
297
|
-
|
|
344
|
+
let args = process.argv.slice(2);
|
|
345
|
+
if (args[0] === "remote") {
|
|
346
|
+
const remoteArgs = args.slice(1);
|
|
347
|
+
const subcommand = remoteArgs[0];
|
|
348
|
+
const stateDir = getStateDir();
|
|
349
|
+
try {
|
|
350
|
+
if (subcommand === "authorize") {
|
|
351
|
+
const label = remoteArgs[1];
|
|
352
|
+
const outputIndex = remoteArgs.indexOf("--output");
|
|
353
|
+
const output = outputIndex === -1 ? undefined : remoteArgs[outputIndex + 1];
|
|
354
|
+
if (!label || !output || output.startsWith("--")) throw new Error("Usage: surf remote authorize <label> --output <credential-file>");
|
|
355
|
+
const client = authorizeClient(label, output, stateDir);
|
|
356
|
+
console.log(`Authorized remote client: ${client.label}`);
|
|
357
|
+
console.log(`Credential: ${client.output}`);
|
|
358
|
+
process.exit(0);
|
|
359
|
+
}
|
|
360
|
+
if (subcommand === "list") {
|
|
361
|
+
const clients = listClients(stateDir);
|
|
362
|
+
if (clients.length === 0) console.log("No authorized remote clients.");
|
|
363
|
+
else for (const client of clients) console.log(`${client.label}\t${client.id}\t${client.createdAt}`);
|
|
364
|
+
process.exit(0);
|
|
365
|
+
}
|
|
366
|
+
if (subcommand === "revoke") {
|
|
367
|
+
const label = remoteArgs[1];
|
|
368
|
+
if (!label || label.startsWith("--")) throw new Error("Usage: surf remote revoke <label>");
|
|
369
|
+
revokeClient(label, stateDir);
|
|
370
|
+
console.log(`Revoked remote client: ${label}`);
|
|
371
|
+
process.exit(0);
|
|
372
|
+
}
|
|
373
|
+
console.error("Usage: surf remote authorize <label> --output <credential-file> | list | revoke <label>");
|
|
374
|
+
process.exit(1);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
console.error(`Error: ${error.message}`);
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let endpoint;
|
|
382
|
+
try {
|
|
383
|
+
({ args, endpoint } = selectEndpoint(args));
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.error(`Error: ${error.message}`);
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
298
388
|
|
|
299
389
|
const ALIASES = {
|
|
300
390
|
snap: "screenshot",
|
|
@@ -342,10 +432,10 @@ const TOOLS = {
|
|
|
342
432
|
ai: {
|
|
343
433
|
desc: "AI assistants (ChatGPT, Gemini)",
|
|
344
434
|
commands: {
|
|
345
|
-
"chatgpt": {
|
|
346
|
-
desc: "Send prompt to ChatGPT (uses browser cookies)",
|
|
347
|
-
args: ["query"],
|
|
348
|
-
opts: {
|
|
435
|
+
"chatgpt": {
|
|
436
|
+
desc: "Send prompt to ChatGPT (uses browser cookies)",
|
|
437
|
+
args: ["query"],
|
|
438
|
+
opts: {
|
|
349
439
|
"with-page": "Include current page context",
|
|
350
440
|
model: "Model: gpt-4o, o1, etc.",
|
|
351
441
|
file: "Attach file",
|
|
@@ -358,12 +448,12 @@ const TOOLS = {
|
|
|
358
448
|
{ cmd: 'chatgpt "analyze" --model gpt-4o', desc: "Specify model" },
|
|
359
449
|
]
|
|
360
450
|
},
|
|
361
|
-
"gemini": {
|
|
362
|
-
desc: "Send prompt to Gemini (uses browser cookies)",
|
|
363
|
-
args: ["query"],
|
|
364
|
-
opts: {
|
|
451
|
+
"gemini": {
|
|
452
|
+
desc: "Send prompt to Gemini (uses browser cookies)",
|
|
453
|
+
args: ["query"],
|
|
454
|
+
opts: {
|
|
365
455
|
"with-page": "Include current page context",
|
|
366
|
-
model: "Model: gemini-3-pro (default), gemini-
|
|
456
|
+
model: "Model: gemini-3.1-pro (default), gemini-3.5-flash, gemini-3.1-flash-lite",
|
|
367
457
|
file: "Attach file to analyze",
|
|
368
458
|
"generate-image": "Generate image and save to path",
|
|
369
459
|
"edit-image": "Edit existing image (use with --output)",
|
|
@@ -402,9 +492,9 @@ const TOOLS = {
|
|
|
402
492
|
args: ["query"],
|
|
403
493
|
opts: {
|
|
404
494
|
"with-page": "Include current page context",
|
|
405
|
-
model: "Model: auto, fast, expert,
|
|
495
|
+
model: "Model: auto, fast (default), expert, grok-4.20-beta",
|
|
406
496
|
"deep-search": "Enable DeepSearch for X post searching",
|
|
407
|
-
timeout: "Timeout in seconds (default: 300
|
|
497
|
+
timeout: "Timeout in seconds (default: 300)",
|
|
408
498
|
validate: "Check Grok UI and scrape available models (no query sent)",
|
|
409
499
|
"save-models": "Save discovered models to surf.json config"
|
|
410
500
|
},
|
|
@@ -447,9 +537,9 @@ const TOOLS = {
|
|
|
447
537
|
{ cmd: 'aistudio.build "crm dashboard" --output ./out', desc: "Build and extract to directory" },
|
|
448
538
|
]
|
|
449
539
|
},
|
|
450
|
-
"ai": {
|
|
451
|
-
desc: "Analyze page with AI (requires GOOGLE_API_KEY)",
|
|
452
|
-
args: ["query"],
|
|
540
|
+
"ai": {
|
|
541
|
+
desc: "Analyze page with AI (requires GOOGLE_API_KEY)",
|
|
542
|
+
args: ["query"],
|
|
453
543
|
opts: { mode: "Query mode: find|summary|extract (auto-detected)" },
|
|
454
544
|
examples: [
|
|
455
545
|
{ cmd: 'ai "find the login button"', desc: "Find element" },
|
|
@@ -463,39 +553,45 @@ const TOOLS = {
|
|
|
463
553
|
desc: "Tab management",
|
|
464
554
|
commands: {
|
|
465
555
|
"tab.list": { desc: "List all open tabs", args: [], examples: [{ cmd: "tab.list", desc: "Show all tabs" }] },
|
|
466
|
-
"tab.new": {
|
|
467
|
-
desc: "Open new tab",
|
|
468
|
-
args: ["url"],
|
|
556
|
+
"tab.new": {
|
|
557
|
+
desc: "Open new tab",
|
|
558
|
+
args: ["url"],
|
|
469
559
|
opts: { urls: "Open multiple URLs" },
|
|
470
560
|
examples: [
|
|
471
561
|
{ cmd: 'tab.new "https://google.com"', desc: "Open single tab" },
|
|
472
562
|
{ cmd: 'tab.new --urls "https://a.com" "https://b.com"', desc: "Open multiple" },
|
|
473
563
|
]
|
|
474
564
|
},
|
|
475
|
-
"tab.switch": {
|
|
476
|
-
desc: "Switch to tab by ID or name",
|
|
565
|
+
"tab.switch": {
|
|
566
|
+
desc: "Switch to tab by ID or name",
|
|
477
567
|
args: ["id"],
|
|
478
568
|
examples: [
|
|
479
569
|
{ cmd: "tab.switch 123", desc: "Switch by ID" },
|
|
480
570
|
{ cmd: 'tab.switch "myTab"', desc: "Switch by name" },
|
|
481
571
|
]
|
|
482
572
|
},
|
|
483
|
-
"tab.close": {
|
|
484
|
-
desc: "Close tab by ID or name",
|
|
485
|
-
args: ["id"],
|
|
573
|
+
"tab.close": {
|
|
574
|
+
desc: "Close tab by ID or name",
|
|
575
|
+
args: ["id"],
|
|
486
576
|
opts: { ids: "Close multiple tabs" },
|
|
487
577
|
examples: [{ cmd: "tab.close 123", desc: "Close tab" }]
|
|
488
578
|
},
|
|
489
|
-
"tab.
|
|
490
|
-
desc: "
|
|
579
|
+
"tab.move": {
|
|
580
|
+
desc: "Move tab to another window",
|
|
581
|
+
args: ["id"],
|
|
582
|
+
opts: { ids: "Move multiple tabs", "to-window": "Destination window ID", index: "Destination index" },
|
|
583
|
+
examples: [{ cmd: "tab.move 123 --to-window 456", desc: "Move tab to window" }]
|
|
584
|
+
},
|
|
585
|
+
"tab.name": {
|
|
586
|
+
desc: "Register current tab with a name",
|
|
491
587
|
args: ["name"],
|
|
492
588
|
examples: [{ cmd: 'tab.name "dashboard"', desc: "Name current tab" }]
|
|
493
589
|
},
|
|
494
590
|
"tab.unname": { desc: "Unregister a named tab", args: ["name"] },
|
|
495
591
|
"tab.named": { desc: "List all named tabs", args: [] },
|
|
496
|
-
"tab.group": {
|
|
497
|
-
desc: "Create/add to tab group",
|
|
498
|
-
args: [],
|
|
592
|
+
"tab.group": {
|
|
593
|
+
desc: "Create/add to tab group",
|
|
594
|
+
args: [],
|
|
499
595
|
opts: { name: "Group name", tabs: "Tab IDs (comma-separated)", color: "Group color" },
|
|
500
596
|
examples: [
|
|
501
597
|
{ cmd: 'tab.group --name "Work" --color blue', desc: "Group current tab" },
|
|
@@ -504,9 +600,9 @@ const TOOLS = {
|
|
|
504
600
|
},
|
|
505
601
|
"tab.ungroup": { desc: "Remove tabs from group", args: [], opts: { tabs: "Tab IDs (comma-separated)" } },
|
|
506
602
|
"tab.groups": { desc: "List all tab groups", args: [] },
|
|
507
|
-
"tab.reload": {
|
|
508
|
-
desc: "Reload current tab",
|
|
509
|
-
args: [],
|
|
603
|
+
"tab.reload": {
|
|
604
|
+
desc: "Reload current tab",
|
|
605
|
+
args: [],
|
|
510
606
|
opts: { hard: "Bypass cache" },
|
|
511
607
|
examples: [
|
|
512
608
|
{ cmd: "tab.reload", desc: "Soft reload" },
|
|
@@ -518,30 +614,31 @@ const TOOLS = {
|
|
|
518
614
|
nav: {
|
|
519
615
|
desc: "Navigation",
|
|
520
616
|
commands: {
|
|
521
|
-
"navigate": {
|
|
522
|
-
desc: "Go to URL",
|
|
617
|
+
"navigate": {
|
|
618
|
+
desc: "Go to URL",
|
|
523
619
|
args: ["url"],
|
|
524
620
|
examples: [{ cmd: 'navigate "https://example.com"', desc: "Go to URL" }]
|
|
525
621
|
},
|
|
526
622
|
"go": { desc: "Alias for navigate", args: ["url"], alias: "navigate" },
|
|
527
|
-
"back": {
|
|
528
|
-
desc: "Go back in history",
|
|
623
|
+
"back": {
|
|
624
|
+
desc: "Go back in history",
|
|
529
625
|
args: [],
|
|
530
626
|
examples: [{ cmd: "back", desc: "Browser back" }]
|
|
531
627
|
},
|
|
532
|
-
"forward": {
|
|
533
|
-
desc: "Go forward in history",
|
|
628
|
+
"forward": {
|
|
629
|
+
desc: "Go forward in history",
|
|
534
630
|
args: [],
|
|
535
631
|
examples: [{ cmd: "forward", desc: "Browser forward" }]
|
|
536
632
|
},
|
|
537
|
-
"screenshot": {
|
|
538
|
-
desc: "Capture screenshot (auto-saves to /tmp by default)",
|
|
539
|
-
args: [],
|
|
540
|
-
opts: {
|
|
541
|
-
output: "Save to file",
|
|
542
|
-
selector: "Capture specific element",
|
|
543
|
-
annotate: "Draw element labels",
|
|
544
|
-
fullpage: "Capture full page",
|
|
633
|
+
"screenshot": {
|
|
634
|
+
desc: "Capture screenshot (auto-saves to /tmp by default)",
|
|
635
|
+
args: [],
|
|
636
|
+
opts: {
|
|
637
|
+
output: "Save to file",
|
|
638
|
+
selector: "Capture specific element",
|
|
639
|
+
annotate: "Draw element labels",
|
|
640
|
+
fullpage: "Capture full page",
|
|
641
|
+
"full-page": "Capture full page (alias for --fullpage)",
|
|
545
642
|
"max-height": "Max height for fullpage (default: 4000)",
|
|
546
643
|
full: "Skip resize, save at full resolution",
|
|
547
644
|
"max-size": "Max dimension in px (default: 1200)",
|
|
@@ -555,23 +652,65 @@ const TOOLS = {
|
|
|
555
652
|
{ cmd: "snap", desc: "Alias for screenshot" },
|
|
556
653
|
]
|
|
557
654
|
},
|
|
655
|
+
"record": {
|
|
656
|
+
desc: "Capture screenshot frames over time and assemble an animated GIF",
|
|
657
|
+
args: [],
|
|
658
|
+
opts: {
|
|
659
|
+
output: "GIF output path (default: /tmp/surf-record-*.gif)",
|
|
660
|
+
duration: "Capture duration in ms (default: 2000, max: 10000)",
|
|
661
|
+
fps: "Frames per second (default: 10, max: 30)",
|
|
662
|
+
trigger: "Optional action before capture: click:<selector> or scroll:<target>",
|
|
663
|
+
rect: "Crop rectangle x,y,width,height"
|
|
664
|
+
},
|
|
665
|
+
examples: [
|
|
666
|
+
{ cmd: "record --duration 2000 --fps 10 --output /tmp/anim.gif", desc: "Record a 2s GIF" },
|
|
667
|
+
{ cmd: 'record --trigger "click:#btn" --output /tmp/click.gif', desc: "Click, then record" },
|
|
668
|
+
]
|
|
669
|
+
},
|
|
670
|
+
"animate-audit": {
|
|
671
|
+
desc: "Sample matching elements over time and return a JSON animation timeline",
|
|
672
|
+
args: [],
|
|
673
|
+
opts: {
|
|
674
|
+
selector: "CSS selector to sample (required)",
|
|
675
|
+
duration: "Capture duration in ms (default: 2000, max: 10000)",
|
|
676
|
+
fps: "Samples per second (default: 10, max: 30)"
|
|
677
|
+
},
|
|
678
|
+
examples: [
|
|
679
|
+
{ cmd: 'animate-audit --selector ".thing" --duration 2000 --fps 10', desc: "Capture a bounded JSON timeline" },
|
|
680
|
+
]
|
|
681
|
+
},
|
|
682
|
+
"perf-audit": {
|
|
683
|
+
desc: "Capture layout shift, event, long task, and animation-frame performance entries",
|
|
684
|
+
args: [],
|
|
685
|
+
opts: {
|
|
686
|
+
duration: "Capture duration in ms (default: 3000, max: 10000)",
|
|
687
|
+
trigger: "Optional action before capture: click:<selector> or scroll:<target>",
|
|
688
|
+
output: "Save JSON to file"
|
|
689
|
+
},
|
|
690
|
+
examples: [
|
|
691
|
+
{ cmd: 'perf-audit --duration 3000 --trigger "click:.cta" --output /tmp/perf.json', desc: "Capture a performance snapshot" },
|
|
692
|
+
]
|
|
693
|
+
},
|
|
558
694
|
"snap": { desc: "Alias for screenshot (auto-saves to /tmp)", args: [], alias: "screenshot" },
|
|
559
695
|
}
|
|
560
696
|
},
|
|
561
697
|
scroll: {
|
|
562
698
|
desc: "Scrolling",
|
|
563
699
|
commands: {
|
|
564
|
-
"scroll": {
|
|
565
|
-
desc: "Scroll in direction",
|
|
566
|
-
args: [],
|
|
700
|
+
"scroll": {
|
|
701
|
+
desc: "Scroll in direction",
|
|
702
|
+
args: ["direction", "pixels"],
|
|
567
703
|
opts: { direction: "up|down|left|right", amount: "Scroll amount (1-10)" },
|
|
568
|
-
examples: [
|
|
704
|
+
examples: [
|
|
705
|
+
{ cmd: "scroll down 800", desc: "Scroll down 800px" },
|
|
706
|
+
{ cmd: "scroll --direction down --amount 3", desc: "Scroll down" },
|
|
707
|
+
]
|
|
569
708
|
},
|
|
570
709
|
"scroll.top": { desc: "Scroll to top of page", args: [], opts: { selector: "Target specific container" } },
|
|
571
710
|
"scroll.bottom": { desc: "Scroll to bottom of page", args: [], opts: { selector: "Target specific container" } },
|
|
572
|
-
"scroll.to": {
|
|
573
|
-
desc: "Scroll element into view",
|
|
574
|
-
args: [],
|
|
711
|
+
"scroll.to": {
|
|
712
|
+
desc: "Scroll element into view",
|
|
713
|
+
args: [],
|
|
575
714
|
opts: { ref: "Element ref" },
|
|
576
715
|
examples: [{ cmd: "scroll.to --ref e5", desc: "Scroll to element" }]
|
|
577
716
|
},
|
|
@@ -581,15 +720,16 @@ const TOOLS = {
|
|
|
581
720
|
page: {
|
|
582
721
|
desc: "Page inspection",
|
|
583
722
|
commands: {
|
|
584
|
-
"page.read": {
|
|
585
|
-
desc: "Get accessibility tree + visible text",
|
|
586
|
-
args: [],
|
|
587
|
-
opts: {
|
|
588
|
-
all: "Include all elements",
|
|
589
|
-
ref: "Get specific element",
|
|
723
|
+
"page.read": {
|
|
724
|
+
desc: "Get accessibility tree + visible text",
|
|
725
|
+
args: [],
|
|
726
|
+
opts: {
|
|
727
|
+
all: "Include all elements",
|
|
728
|
+
ref: "Get specific element",
|
|
590
729
|
"no-text": "Exclude visible text content",
|
|
591
730
|
depth: "Maximum tree depth (default: unlimited)",
|
|
592
731
|
compact: "Remove empty structural elements",
|
|
732
|
+
"max-bytes": "Maximum visible text bytes",
|
|
593
733
|
},
|
|
594
734
|
examples: [
|
|
595
735
|
{ cmd: "page.read", desc: "Interactive elements + text content" },
|
|
@@ -597,7 +737,7 @@ const TOOLS = {
|
|
|
597
737
|
{ cmd: "page.read --no-text", desc: "Interactive elements only (no text)" },
|
|
598
738
|
{ cmd: "page.read --depth 3", desc: "Limit to 3 levels deep" },
|
|
599
739
|
{ cmd: "page.read --compact", desc: "Skip empty containers" },
|
|
600
|
-
{ cmd: "page.read --depth 3 --compact", desc: "Shallow + compact
|
|
740
|
+
{ cmd: "page.read --depth 3 --compact --max-bytes 2000", desc: "Shallow + compact output" },
|
|
601
741
|
{ cmd: "read", desc: "Alias" },
|
|
602
742
|
]
|
|
603
743
|
},
|
|
@@ -612,7 +752,7 @@ const TOOLS = {
|
|
|
612
752
|
"locate.role": {
|
|
613
753
|
desc: "Find element by ARIA role",
|
|
614
754
|
args: ["role"],
|
|
615
|
-
opts: {
|
|
755
|
+
opts: {
|
|
616
756
|
name: "Element name/text",
|
|
617
757
|
action: "Action to perform (click|fill|hover|text)",
|
|
618
758
|
value: "Value for fill action",
|
|
@@ -684,14 +824,14 @@ const TOOLS = {
|
|
|
684
824
|
wait: {
|
|
685
825
|
desc: "Waiting",
|
|
686
826
|
commands: {
|
|
687
|
-
"wait": {
|
|
688
|
-
desc: "Wait N seconds",
|
|
827
|
+
"wait": {
|
|
828
|
+
desc: "Wait N seconds",
|
|
689
829
|
args: ["duration"],
|
|
690
830
|
examples: [{ cmd: "wait 2", desc: "Wait 2 seconds" }]
|
|
691
831
|
},
|
|
692
|
-
"wait.element": {
|
|
693
|
-
desc: "Wait for element to appear",
|
|
694
|
-
args: ["selector"],
|
|
832
|
+
"wait.element": {
|
|
833
|
+
desc: "Wait for element to appear",
|
|
834
|
+
args: ["selector"],
|
|
695
835
|
opts: { timeout: "Timeout in ms" },
|
|
696
836
|
examples: [
|
|
697
837
|
{ cmd: 'wait.element ".loading"', desc: "Wait for element" },
|
|
@@ -699,9 +839,9 @@ const TOOLS = {
|
|
|
699
839
|
]
|
|
700
840
|
},
|
|
701
841
|
"wait.network": { desc: "Wait for network idle", args: [], opts: { timeout: "Timeout in ms" } },
|
|
702
|
-
"wait.url": {
|
|
703
|
-
desc: "Wait for URL to match",
|
|
704
|
-
args: ["pattern"],
|
|
842
|
+
"wait.url": {
|
|
843
|
+
desc: "Wait for URL to match",
|
|
844
|
+
args: ["pattern"],
|
|
705
845
|
opts: { timeout: "Timeout in ms" },
|
|
706
846
|
examples: [{ cmd: 'wait.url "/dashboard"', desc: "Wait for URL pattern" }]
|
|
707
847
|
},
|
|
@@ -712,15 +852,15 @@ const TOOLS = {
|
|
|
712
852
|
input: {
|
|
713
853
|
desc: "Input actions",
|
|
714
854
|
commands: {
|
|
715
|
-
"click": {
|
|
716
|
-
desc: "Click element or coordinates",
|
|
717
|
-
args: ["ref"],
|
|
718
|
-
opts: {
|
|
719
|
-
ref: "Element ref",
|
|
720
|
-
x: "X coordinate",
|
|
721
|
-
y: "Y coordinate",
|
|
722
|
-
button: "left|right|double|triple",
|
|
723
|
-
selector: "CSS selector",
|
|
855
|
+
"click": {
|
|
856
|
+
desc: "Click element or coordinates",
|
|
857
|
+
args: ["ref"],
|
|
858
|
+
opts: {
|
|
859
|
+
ref: "Element ref",
|
|
860
|
+
x: "X coordinate",
|
|
861
|
+
y: "Y coordinate",
|
|
862
|
+
button: "left|right|double|triple",
|
|
863
|
+
selector: "CSS selector",
|
|
724
864
|
index: "Which match (0-indexed) for selector",
|
|
725
865
|
},
|
|
726
866
|
examples: [
|
|
@@ -730,15 +870,15 @@ const TOOLS = {
|
|
|
730
870
|
{ cmd: "click --x 100 --y 200", desc: "Click coordinates" },
|
|
731
871
|
]
|
|
732
872
|
},
|
|
733
|
-
"type": {
|
|
734
|
-
desc: "Type text (uses form.fill when --ref provided for better modal/form support)",
|
|
735
|
-
args: ["text"],
|
|
736
|
-
opts: {
|
|
873
|
+
"type": {
|
|
874
|
+
desc: "Type text (uses form.fill when --ref provided for better modal/form support)",
|
|
875
|
+
args: ["text"],
|
|
876
|
+
opts: {
|
|
737
877
|
into: "Target selector",
|
|
738
|
-
ref: "Element ref (uses JS DOM method, more reliable for modals)",
|
|
739
|
-
submit: "Press enter after",
|
|
740
|
-
clear: "Clear first",
|
|
741
|
-
method: "cdp|js (
|
|
878
|
+
ref: "Element ref (uses JS DOM method, more reliable for modals)",
|
|
879
|
+
submit: "Press enter after",
|
|
880
|
+
clear: "Clear first",
|
|
881
|
+
method: "cdp|js (cursor typing uses CDP; selector/ref targets use JS)"
|
|
742
882
|
},
|
|
743
883
|
examples: [
|
|
744
884
|
{ cmd: 'type "hello world"', desc: "Type at cursor (CDP events)" },
|
|
@@ -747,9 +887,9 @@ const TOOLS = {
|
|
|
747
887
|
]
|
|
748
888
|
},
|
|
749
889
|
"smart_type": { desc: "Type into specific element (js method)", args: [], opts: { selector: "CSS selector", text: "Text to type", clear: "Clear first (default: true)", submit: "Submit after" } },
|
|
750
|
-
"key": {
|
|
751
|
-
desc: "Press key",
|
|
752
|
-
args: ["key"],
|
|
890
|
+
"key": {
|
|
891
|
+
desc: "Press key",
|
|
892
|
+
args: ["key"],
|
|
753
893
|
examples: [
|
|
754
894
|
{ cmd: "key Enter", desc: "Press Enter" },
|
|
755
895
|
{ cmd: "key Escape", desc: "Press Escape" },
|
|
@@ -764,9 +904,9 @@ const TOOLS = {
|
|
|
764
904
|
js: {
|
|
765
905
|
desc: "JavaScript execution",
|
|
766
906
|
commands: {
|
|
767
|
-
"js": {
|
|
768
|
-
desc: "Execute JavaScript (use 'return' for values)",
|
|
769
|
-
args: ["code"],
|
|
907
|
+
"js": {
|
|
908
|
+
desc: "Execute JavaScript (use 'return' for values)",
|
|
909
|
+
args: ["code"],
|
|
770
910
|
opts: { file: "Run JS from file" },
|
|
771
911
|
examples: [
|
|
772
912
|
{ cmd: 'js "return document.title"', desc: "Get title" },
|
|
@@ -779,9 +919,9 @@ const TOOLS = {
|
|
|
779
919
|
dev: {
|
|
780
920
|
desc: "Dev tools",
|
|
781
921
|
commands: {
|
|
782
|
-
"console": {
|
|
783
|
-
desc: "Read console messages",
|
|
784
|
-
args: [],
|
|
922
|
+
"console": {
|
|
923
|
+
desc: "Read console messages",
|
|
924
|
+
args: [],
|
|
785
925
|
opts: { clear: "Clear after reading", stream: "Continuous output", level: "Filter by level (log,warn,error)", limit: "Max messages" },
|
|
786
926
|
examples: [
|
|
787
927
|
{ cmd: "console", desc: "Get recent messages" },
|
|
@@ -794,10 +934,10 @@ const TOOLS = {
|
|
|
794
934
|
network: {
|
|
795
935
|
desc: "Network capture",
|
|
796
936
|
commands: {
|
|
797
|
-
"network": {
|
|
798
|
-
desc: "List captured network requests",
|
|
799
|
-
args: [],
|
|
800
|
-
opts: {
|
|
937
|
+
"network": {
|
|
938
|
+
desc: "List captured network requests",
|
|
939
|
+
args: [],
|
|
940
|
+
opts: {
|
|
801
941
|
origin: "Filter by origin (domain)",
|
|
802
942
|
method: "Filter by method (GET,POST,...)",
|
|
803
943
|
status: "Filter by status (200, 4xx, 5xx)",
|
|
@@ -822,16 +962,16 @@ const TOOLS = {
|
|
|
822
962
|
{ cmd: "network -v", desc: "Verbose with headers" },
|
|
823
963
|
]
|
|
824
964
|
},
|
|
825
|
-
"network.get": {
|
|
826
|
-
desc: "Get full details for a request",
|
|
965
|
+
"network.get": {
|
|
966
|
+
desc: "Get full details for a request",
|
|
827
967
|
args: ["id"],
|
|
828
968
|
opts: {},
|
|
829
969
|
examples: [
|
|
830
970
|
{ cmd: "network.get r_001", desc: "Get request details" }
|
|
831
971
|
]
|
|
832
972
|
},
|
|
833
|
-
"network.body": {
|
|
834
|
-
desc: "Get response body (for piping)",
|
|
973
|
+
"network.body": {
|
|
974
|
+
desc: "Get response body (for piping)",
|
|
835
975
|
args: ["id"],
|
|
836
976
|
opts: { request: "Get request body instead" },
|
|
837
977
|
examples: [
|
|
@@ -839,24 +979,24 @@ const TOOLS = {
|
|
|
839
979
|
{ cmd: "network.body r_001 | jq .", desc: "Pipe JSON to jq" }
|
|
840
980
|
]
|
|
841
981
|
},
|
|
842
|
-
"network.curl": {
|
|
843
|
-
desc: "Generate curl command for request",
|
|
982
|
+
"network.curl": {
|
|
983
|
+
desc: "Generate curl command for request",
|
|
844
984
|
args: ["id"],
|
|
845
985
|
opts: {},
|
|
846
986
|
examples: [
|
|
847
987
|
{ cmd: "network.curl r_001", desc: "Generate curl" }
|
|
848
988
|
]
|
|
849
989
|
},
|
|
850
|
-
"network.origins": {
|
|
851
|
-
desc: "List captured origins with stats",
|
|
990
|
+
"network.origins": {
|
|
991
|
+
desc: "List captured origins with stats",
|
|
852
992
|
args: [],
|
|
853
993
|
opts: { "by-tab": "Group by tab" },
|
|
854
994
|
examples: [
|
|
855
995
|
{ cmd: "network.origins", desc: "List origins" }
|
|
856
996
|
]
|
|
857
997
|
},
|
|
858
|
-
"network.clear": {
|
|
859
|
-
desc: "Clear captured requests",
|
|
998
|
+
"network.clear": {
|
|
999
|
+
desc: "Clear captured requests",
|
|
860
1000
|
args: [],
|
|
861
1001
|
opts: { before: "Clear before timestamp/duration", origin: "Clear specific origin" },
|
|
862
1002
|
examples: [
|
|
@@ -864,24 +1004,24 @@ const TOOLS = {
|
|
|
864
1004
|
{ cmd: "network.clear --before 1h", desc: "Clear older than 1 hour" }
|
|
865
1005
|
]
|
|
866
1006
|
},
|
|
867
|
-
"network.stats": {
|
|
868
|
-
desc: "Show capture statistics",
|
|
1007
|
+
"network.stats": {
|
|
1008
|
+
desc: "Show capture statistics",
|
|
869
1009
|
args: [],
|
|
870
1010
|
opts: {},
|
|
871
1011
|
examples: [
|
|
872
1012
|
{ cmd: "network.stats", desc: "Show stats" }
|
|
873
1013
|
]
|
|
874
1014
|
},
|
|
875
|
-
"network.export": {
|
|
876
|
-
desc: "Export captured requests",
|
|
1015
|
+
"network.export": {
|
|
1016
|
+
desc: "Export captured requests",
|
|
877
1017
|
args: [],
|
|
878
1018
|
opts: { jsonl: "Export as JSONL", output: "Output file path" },
|
|
879
1019
|
examples: [
|
|
880
1020
|
{ cmd: "network.export --jsonl --output /tmp/requests.jsonl", desc: "Export as JSONL" }
|
|
881
1021
|
]
|
|
882
1022
|
},
|
|
883
|
-
"network.path": {
|
|
884
|
-
desc: "Get file paths for request data",
|
|
1023
|
+
"network.path": {
|
|
1024
|
+
desc: "Get file paths for request data",
|
|
885
1025
|
args: ["id"],
|
|
886
1026
|
opts: {},
|
|
887
1027
|
examples: [
|
|
@@ -893,9 +1033,19 @@ const TOOLS = {
|
|
|
893
1033
|
health: {
|
|
894
1034
|
desc: "Health checks",
|
|
895
1035
|
commands: {
|
|
896
|
-
"
|
|
897
|
-
desc: "
|
|
898
|
-
args: [],
|
|
1036
|
+
"doctor": {
|
|
1037
|
+
desc: "Diagnose native host manifests and socket connectivity",
|
|
1038
|
+
args: [],
|
|
1039
|
+
opts: { browser: "Browser to inspect (default: chrome)", target: "auto|linux|windows", socket: "Socket path to check", json: "Raw diagnostic JSON" },
|
|
1040
|
+
examples: [
|
|
1041
|
+
{ cmd: "doctor", desc: "Check default Chrome setup" },
|
|
1042
|
+
{ cmd: "doctor --browser all", desc: "Check all supported browsers" },
|
|
1043
|
+
{ cmd: "doctor --json", desc: "Machine-readable diagnostics" },
|
|
1044
|
+
]
|
|
1045
|
+
},
|
|
1046
|
+
"health": {
|
|
1047
|
+
desc: "Wait for URL or element",
|
|
1048
|
+
args: [],
|
|
899
1049
|
opts: { url: "URL to check (expects 200)", selector: "CSS selector to wait for", expect: "Expected status code (default: 200)", timeout: "Timeout in ms" },
|
|
900
1050
|
examples: [
|
|
901
1051
|
{ cmd: 'health --url "https://api.example.com"', desc: "Check URL" },
|
|
@@ -914,9 +1064,9 @@ const TOOLS = {
|
|
|
914
1064
|
desc: "Browser dialog handling",
|
|
915
1065
|
commands: {
|
|
916
1066
|
"dialog.accept": { desc: "Accept current dialog", args: [], opts: { text: "Text for prompt input" } },
|
|
917
|
-
"dialog.dismiss": {
|
|
918
|
-
desc: "Dismiss current dialog",
|
|
919
|
-
args: [],
|
|
1067
|
+
"dialog.dismiss": {
|
|
1068
|
+
desc: "Dismiss current dialog",
|
|
1069
|
+
args: [],
|
|
920
1070
|
opts: { all: "Dismiss all dialogs repeatedly" },
|
|
921
1071
|
examples: [
|
|
922
1072
|
{ cmd: "dialog.dismiss", desc: "Dismiss once" },
|
|
@@ -980,9 +1130,9 @@ const TOOLS = {
|
|
|
980
1130
|
upload: {
|
|
981
1131
|
desc: "File upload",
|
|
982
1132
|
commands: {
|
|
983
|
-
"upload": {
|
|
984
|
-
desc: "Upload file(s) to input",
|
|
985
|
-
args: [],
|
|
1133
|
+
"upload": {
|
|
1134
|
+
desc: "Upload file(s) to input",
|
|
1135
|
+
args: [],
|
|
986
1136
|
opts: { ref: "Element ref", files: "File path(s) comma-separated" },
|
|
987
1137
|
examples: [{ cmd: 'upload --ref e5 --files "/path/to/file.pdf"', desc: "Upload file" }]
|
|
988
1138
|
},
|
|
@@ -991,8 +1141,8 @@ const TOOLS = {
|
|
|
991
1141
|
frame: {
|
|
992
1142
|
desc: "Iframe handling",
|
|
993
1143
|
commands: {
|
|
994
|
-
"frame.list": {
|
|
995
|
-
desc: "List all frames in page",
|
|
1144
|
+
"frame.list": {
|
|
1145
|
+
desc: "List all frames in page",
|
|
996
1146
|
args: [],
|
|
997
1147
|
examples: [{ cmd: "frame.list", desc: "Show frame tree" }]
|
|
998
1148
|
},
|
|
@@ -1015,9 +1165,9 @@ const TOOLS = {
|
|
|
1015
1165
|
args: [],
|
|
1016
1166
|
examples: [{ cmd: "frame.main", desc: "Exit iframe context" }]
|
|
1017
1167
|
},
|
|
1018
|
-
"frame.js": {
|
|
1019
|
-
desc: "Execute JS in specific frame",
|
|
1020
|
-
args: ["code"],
|
|
1168
|
+
"frame.js": {
|
|
1169
|
+
desc: "Execute JS in specific frame",
|
|
1170
|
+
args: ["code"],
|
|
1021
1171
|
opts: { id: "Frame ID from frame.list", file: "Run JS from file" },
|
|
1022
1172
|
examples: [
|
|
1023
1173
|
{ cmd: 'frame.js "return document.title" --id frame1', desc: "JS in specific frame" },
|
|
@@ -1028,25 +1178,37 @@ const TOOLS = {
|
|
|
1028
1178
|
cookie: {
|
|
1029
1179
|
desc: "Cookie management",
|
|
1030
1180
|
commands: {
|
|
1031
|
-
"cookie.list": {
|
|
1032
|
-
desc: "List all cookies for current tab's domain",
|
|
1181
|
+
"cookie.list": {
|
|
1182
|
+
desc: "List all cookies for current tab's domain",
|
|
1183
|
+
args: [],
|
|
1184
|
+
examples: [
|
|
1185
|
+
{ cmd: "cookie list", desc: "Show all cookies" },
|
|
1186
|
+
{ cmd: "cookie.list", desc: "Dot command form" },
|
|
1187
|
+
]
|
|
1188
|
+
},
|
|
1189
|
+
"cookie.get": {
|
|
1190
|
+
desc: "Get specific cookie",
|
|
1033
1191
|
args: [],
|
|
1034
|
-
|
|
1192
|
+
opts: { name: "Cookie name" },
|
|
1193
|
+
examples: [{ cmd: "cookie get session", desc: "Get cookie" }]
|
|
1035
1194
|
},
|
|
1036
|
-
"cookie.
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
args: [],
|
|
1195
|
+
"cookie.set": {
|
|
1196
|
+
desc: "Set a cookie",
|
|
1197
|
+
args: [],
|
|
1040
1198
|
opts: { name: "Cookie name", value: "Cookie value", expires: "Expiry date (optional)" },
|
|
1041
|
-
examples: [
|
|
1199
|
+
examples: [
|
|
1200
|
+
{ cmd: 'cookie set --name "session" --value "abc123"', desc: "Set cookie" },
|
|
1201
|
+
{ cmd: 'cookie.set --name "session" --value "abc123"', desc: "Dot command form" },
|
|
1202
|
+
]
|
|
1042
1203
|
},
|
|
1043
|
-
"cookie.clear": {
|
|
1044
|
-
desc: "Clear cookies",
|
|
1045
|
-
args: [],
|
|
1204
|
+
"cookie.clear": {
|
|
1205
|
+
desc: "Clear cookies",
|
|
1206
|
+
args: [],
|
|
1046
1207
|
opts: { name: "Specific cookie (optional)", all: "Clear all for domain" },
|
|
1047
1208
|
examples: [
|
|
1048
|
-
{ cmd: 'cookie
|
|
1049
|
-
{ cmd: "cookie
|
|
1209
|
+
{ cmd: 'cookie delete "session"', desc: "Clear one" },
|
|
1210
|
+
{ cmd: "cookie clear --all", desc: "Clear all" },
|
|
1211
|
+
{ cmd: 'cookie.clear --name "session"', desc: "Dot command form" },
|
|
1050
1212
|
]
|
|
1051
1213
|
},
|
|
1052
1214
|
}
|
|
@@ -1054,9 +1216,9 @@ const TOOLS = {
|
|
|
1054
1216
|
search: {
|
|
1055
1217
|
desc: "Text search",
|
|
1056
1218
|
commands: {
|
|
1057
|
-
"search": {
|
|
1058
|
-
desc: "Search for text in page",
|
|
1059
|
-
args: ["term"],
|
|
1219
|
+
"search": {
|
|
1220
|
+
desc: "Search for text in page",
|
|
1221
|
+
args: ["term"],
|
|
1060
1222
|
opts: { "case-sensitive": "Case-sensitive match", limit: "Max results" },
|
|
1061
1223
|
examples: [
|
|
1062
1224
|
{ cmd: 'search "login"', desc: "Find text" },
|
|
@@ -1070,9 +1232,9 @@ const TOOLS = {
|
|
|
1070
1232
|
batch: {
|
|
1071
1233
|
desc: "Batch execution",
|
|
1072
1234
|
commands: {
|
|
1073
|
-
"batch": {
|
|
1074
|
-
desc: "Execute multiple actions",
|
|
1075
|
-
args: [],
|
|
1235
|
+
"batch": {
|
|
1236
|
+
desc: "Execute multiple actions",
|
|
1237
|
+
args: [],
|
|
1076
1238
|
opts: { actions: "JSON array of actions", file: "Path to actions JSON file" },
|
|
1077
1239
|
examples: [
|
|
1078
1240
|
{ cmd: 'batch --actions \'[{"type":"click","ref":"e1"},{"type":"wait","ms":500}]\'', desc: "Inline actions" },
|
|
@@ -1130,9 +1292,9 @@ const TOOLS = {
|
|
|
1130
1292
|
zoom: {
|
|
1131
1293
|
desc: "Zoom control",
|
|
1132
1294
|
commands: {
|
|
1133
|
-
"zoom": {
|
|
1134
|
-
desc: "Get or set zoom level",
|
|
1135
|
-
args: [],
|
|
1295
|
+
"zoom": {
|
|
1296
|
+
desc: "Get or set zoom level",
|
|
1297
|
+
args: [],
|
|
1136
1298
|
opts: { level: "Zoom level (e.g., 1.5 for 150%)", reset: "Reset to default zoom" },
|
|
1137
1299
|
examples: [
|
|
1138
1300
|
{ cmd: "zoom", desc: "Get current zoom" },
|
|
@@ -1145,11 +1307,14 @@ const TOOLS = {
|
|
|
1145
1307
|
resize: {
|
|
1146
1308
|
desc: "Window management",
|
|
1147
1309
|
commands: {
|
|
1148
|
-
"resize": {
|
|
1149
|
-
desc: "Resize browser window",
|
|
1150
|
-
args: [],
|
|
1310
|
+
"resize": {
|
|
1311
|
+
desc: "Resize browser window",
|
|
1312
|
+
args: ["width", "height"],
|
|
1151
1313
|
opts: { width: "Window width", height: "Window height" },
|
|
1152
|
-
examples: [
|
|
1314
|
+
examples: [
|
|
1315
|
+
{ cmd: "resize 1280 720", desc: "Set size" },
|
|
1316
|
+
{ cmd: "resize --width 1280 --height 720", desc: "Set size with flags" },
|
|
1317
|
+
]
|
|
1153
1318
|
},
|
|
1154
1319
|
}
|
|
1155
1320
|
},
|
|
@@ -1164,14 +1329,14 @@ const TOOLS = {
|
|
|
1164
1329
|
history: {
|
|
1165
1330
|
desc: "Browser history",
|
|
1166
1331
|
commands: {
|
|
1167
|
-
"history.list": {
|
|
1168
|
-
desc: "Recent history",
|
|
1169
|
-
args: [],
|
|
1332
|
+
"history.list": {
|
|
1333
|
+
desc: "Recent history",
|
|
1334
|
+
args: [],
|
|
1170
1335
|
opts: { limit: "Max results" },
|
|
1171
1336
|
examples: [{ cmd: "history.list --limit 20", desc: "Last 20 items" }]
|
|
1172
1337
|
},
|
|
1173
|
-
"history.search": {
|
|
1174
|
-
desc: "Search history",
|
|
1338
|
+
"history.search": {
|
|
1339
|
+
desc: "Search history",
|
|
1175
1340
|
args: ["query"],
|
|
1176
1341
|
examples: [{ cmd: 'history.search "github"', desc: "Search history" }]
|
|
1177
1342
|
},
|
|
@@ -1180,10 +1345,10 @@ const TOOLS = {
|
|
|
1180
1345
|
window: {
|
|
1181
1346
|
desc: "Window management (isolate agent from your browsing)",
|
|
1182
1347
|
commands: {
|
|
1183
|
-
"window.new": {
|
|
1184
|
-
desc: "Create new browser window",
|
|
1185
|
-
args: ["url"],
|
|
1186
|
-
opts: {
|
|
1348
|
+
"window.new": {
|
|
1349
|
+
desc: "Create new browser window",
|
|
1350
|
+
args: ["url"],
|
|
1351
|
+
opts: {
|
|
1187
1352
|
width: "Window width",
|
|
1188
1353
|
height: "Window height",
|
|
1189
1354
|
incognito: "Open incognito window",
|
|
@@ -1195,28 +1360,28 @@ const TOOLS = {
|
|
|
1195
1360
|
{ cmd: 'window.new --incognito', desc: "Incognito window" },
|
|
1196
1361
|
]
|
|
1197
1362
|
},
|
|
1198
|
-
"window.list": {
|
|
1199
|
-
desc: "List all browser windows",
|
|
1363
|
+
"window.list": {
|
|
1364
|
+
desc: "List all browser windows",
|
|
1200
1365
|
args: [],
|
|
1201
1366
|
opts: { tabs: "Include tab details" },
|
|
1202
1367
|
examples: [{ cmd: "window.list", desc: "Show all windows" }]
|
|
1203
1368
|
},
|
|
1204
|
-
"window.focus": {
|
|
1205
|
-
desc: "Focus a window by ID",
|
|
1369
|
+
"window.focus": {
|
|
1370
|
+
desc: "Focus a window by ID",
|
|
1206
1371
|
args: ["id"],
|
|
1207
1372
|
examples: [{ cmd: "window.focus 123", desc: "Focus window" }]
|
|
1208
1373
|
},
|
|
1209
|
-
"window.close": {
|
|
1210
|
-
desc: "Close a window by ID",
|
|
1374
|
+
"window.close": {
|
|
1375
|
+
desc: "Close a window by ID",
|
|
1211
1376
|
args: ["id"],
|
|
1212
1377
|
examples: [{ cmd: "window.close 123", desc: "Close window" }]
|
|
1213
1378
|
},
|
|
1214
|
-
"window.resize": {
|
|
1215
|
-
desc: "Resize or reposition a window",
|
|
1216
|
-
args: [],
|
|
1217
|
-
opts: {
|
|
1218
|
-
id: "Window ID (required)",
|
|
1219
|
-
width: "Window width",
|
|
1379
|
+
"window.resize": {
|
|
1380
|
+
desc: "Resize or reposition a window",
|
|
1381
|
+
args: [],
|
|
1382
|
+
opts: {
|
|
1383
|
+
id: "Window ID (required)",
|
|
1384
|
+
width: "Window width",
|
|
1220
1385
|
height: "Window height",
|
|
1221
1386
|
left: "Window X position",
|
|
1222
1387
|
top: "Window Y position",
|
|
@@ -1263,10 +1428,17 @@ Use --index to select from multiple matches:
|
|
|
1263
1428
|
content: `Cookies are scoped to the current tab's domain.
|
|
1264
1429
|
|
|
1265
1430
|
Commands:
|
|
1266
|
-
cookie
|
|
1267
|
-
cookie
|
|
1268
|
-
cookie
|
|
1269
|
-
cookie
|
|
1431
|
+
cookie list List all cookies
|
|
1432
|
+
cookie get X Get specific cookie
|
|
1433
|
+
cookie set Set a cookie
|
|
1434
|
+
cookie clear --all Clear all cookies
|
|
1435
|
+
cookie delete X Clear one cookie
|
|
1436
|
+
|
|
1437
|
+
Dot commands remain supported:
|
|
1438
|
+
cookie.list
|
|
1439
|
+
cookie.get --name X
|
|
1440
|
+
cookie.set
|
|
1441
|
+
cookie.clear
|
|
1270
1442
|
|
|
1271
1443
|
Notes:
|
|
1272
1444
|
- HttpOnly cookies are accessible
|
|
@@ -1298,6 +1470,7 @@ Commands:
|
|
|
1298
1470
|
screenshot --output file.png Basic screenshot
|
|
1299
1471
|
screenshot --annotate --output file.png With element labels
|
|
1300
1472
|
screenshot --fullpage --output file.png Full page capture
|
|
1473
|
+
screenshot --full-page --output file.png Full page capture (alias)
|
|
1301
1474
|
screenshot --annotate --fullpage --output file.png Full page with labels
|
|
1302
1475
|
snap Auto-save to /tmp
|
|
1303
1476
|
|
|
@@ -1305,6 +1478,7 @@ Options:
|
|
|
1305
1478
|
--output Save path
|
|
1306
1479
|
--annotate Draw element refs
|
|
1307
1480
|
--fullpage Capture entire page
|
|
1481
|
+
--full-page Capture entire page (alias)
|
|
1308
1482
|
--max-height Max height for fullpage (default: 4000)`
|
|
1309
1483
|
},
|
|
1310
1484
|
automation: {
|
|
@@ -1327,7 +1501,7 @@ Wait for dynamic content:
|
|
|
1327
1501
|
|
|
1328
1502
|
Scroll and capture:
|
|
1329
1503
|
scroll.bottom
|
|
1330
|
-
screenshot --
|
|
1504
|
+
screenshot --full-page --output full.png`
|
|
1331
1505
|
},
|
|
1332
1506
|
windows: {
|
|
1333
1507
|
title: "Window Isolation",
|
|
@@ -1346,7 +1520,7 @@ All commands in that window:
|
|
|
1346
1520
|
|
|
1347
1521
|
Manage windows:
|
|
1348
1522
|
surf window.list # List all windows
|
|
1349
|
-
surf window.list --tabs # Include tab details
|
|
1523
|
+
surf window.list --tabs # Include tab details
|
|
1350
1524
|
surf window.focus 123 # Bring window to front
|
|
1351
1525
|
surf window.close 123 # Close when done
|
|
1352
1526
|
|
|
@@ -1439,7 +1613,7 @@ Exclude text content:
|
|
|
1439
1613
|
};
|
|
1440
1614
|
|
|
1441
1615
|
const ALL_SOCKET_TOOLS = [
|
|
1442
|
-
"ai", "screenshot", "navigate",
|
|
1616
|
+
"ai", "screenshot", "record", "animate-audit", "perf-audit", "navigate",
|
|
1443
1617
|
"form_input", "find_and_type", "autocomplete", "set_value", "smart_type",
|
|
1444
1618
|
"scroll_to_position", "get_scroll_info", "close_dialogs", "page_state",
|
|
1445
1619
|
"javascript_tool", "health", "smoke",
|
|
@@ -1448,13 +1622,13 @@ const ALL_SOCKET_TOOLS = [
|
|
|
1448
1622
|
"computer",
|
|
1449
1623
|
"page.read", "page.text", "page.state",
|
|
1450
1624
|
"locate.role", "locate.text", "locate.label",
|
|
1451
|
-
"tab.list", "tab.new", "tab.switch", "tab.close", "tab.name", "tab.unname", "tab.named",
|
|
1625
|
+
"tab.list", "tab.new", "tab.switch", "tab.close", "tab.move", "tab.name", "tab.unname", "tab.named",
|
|
1452
1626
|
"tab.group", "tab.ungroup", "tab.groups", "tab.reload",
|
|
1453
1627
|
"scroll.top", "scroll.bottom", "scroll.to", "scroll.info",
|
|
1454
1628
|
"wait.element", "wait.network", "wait.url", "wait.dom", "wait.load",
|
|
1455
1629
|
"click", "hover", "drag",
|
|
1456
|
-
"js", "console", "network",
|
|
1457
|
-
"network.get", "network.body", "network.curl", "network.origins",
|
|
1630
|
+
"js", "console", "network",
|
|
1631
|
+
"network.get", "network.body", "network.curl", "network.origins",
|
|
1458
1632
|
"network.clear", "network.stats", "network.export", "network.path",
|
|
1459
1633
|
"dialog.accept", "dialog.dismiss", "dialog.info",
|
|
1460
1634
|
"emulate.network", "emulate.cpu", "emulate.geo", "emulate.device", "emulate.viewport", "emulate.touch",
|
|
@@ -1497,6 +1671,9 @@ const SEE_ALSO = {
|
|
|
1497
1671
|
"perf.metrics": ["perf.start", "console", "network"],
|
|
1498
1672
|
"navigate": ["wait.load", "page.read"],
|
|
1499
1673
|
"screenshot": ["page.read", "scroll.bottom for fullpage"],
|
|
1674
|
+
"record": ["screenshot", "animate-audit", "perf-audit"],
|
|
1675
|
+
"animate-audit": ["screenshot", "record", "perf-audit", "js"],
|
|
1676
|
+
"perf-audit": ["record", "animate-audit", "perf.metrics", "console"],
|
|
1500
1677
|
"search": ["locate.text", "page.read"],
|
|
1501
1678
|
"wait.element": ["wait.load", "wait.network"],
|
|
1502
1679
|
"wait.load": ["wait.element", "wait.network"],
|
|
@@ -1516,10 +1693,14 @@ Common Commands:
|
|
|
1516
1693
|
click <ref> Click element by ref or selector
|
|
1517
1694
|
type <text> Type text at cursor or into element
|
|
1518
1695
|
screenshot Capture screenshot (alias: snap)
|
|
1696
|
+
record Capture screenshot frames into an animated GIF
|
|
1697
|
+
animate-audit JSON timeline of element animation/style samples
|
|
1698
|
+
perf-audit PerformanceObserver snapshot for motion/jank debugging
|
|
1519
1699
|
page.read Get page accessibility tree (alias: read)
|
|
1520
1700
|
locate.role <role> Find element by ARIA role
|
|
1521
1701
|
search <term> Search for text in page (alias: find)
|
|
1522
1702
|
window.new <url> Create isolated browser window
|
|
1703
|
+
doctor Diagnose native host/socket setup
|
|
1523
1704
|
wait <seconds> Wait N seconds
|
|
1524
1705
|
|
|
1525
1706
|
Quick Examples:
|
|
@@ -1533,7 +1714,12 @@ Quick Examples:
|
|
|
1533
1714
|
surf window.new "https://example.com" && surf --window-id 123 go "https://other.com"
|
|
1534
1715
|
|
|
1535
1716
|
More Help:
|
|
1717
|
+
--remote <host>:<port> Route requests to a remote native host
|
|
1718
|
+
--remote-credential <path> Use a mode-0600 Ed25519 remote credential file
|
|
1719
|
+
surf remote authorize <label> --output <path>
|
|
1720
|
+
surf remote list | surf remote revoke <label>
|
|
1536
1721
|
surf --help-full All commands
|
|
1722
|
+
surf --llm-context Compact reference for AI agents
|
|
1537
1723
|
surf --help-topic <topic> Topic guide (refs, semantic, frames, devices...)
|
|
1538
1724
|
surf <command> --help Command details
|
|
1539
1725
|
surf --find <query> Search for commands
|
|
@@ -1541,6 +1727,34 @@ More Help:
|
|
|
1541
1727
|
`);
|
|
1542
1728
|
};
|
|
1543
1729
|
|
|
1730
|
+
const showLlmContext = () => {
|
|
1731
|
+
console.log(`SURF CLI LLM CONTEXT
|
|
1732
|
+
Purpose: control Chrome from shell. Commands are \`surf <command> [args] [options]\`.
|
|
1733
|
+
Core loop: navigate -> wait/read -> act -> screenshot/read.
|
|
1734
|
+
Navigate: surf navigate "https://example.com" # alias: surf go "..."
|
|
1735
|
+
Wait after navigation: surf wait 2 # or wait.load for load complete
|
|
1736
|
+
Read DOM/refs: surf page.read --depth 3 --compact # alias: surf read
|
|
1737
|
+
Refs: use e1/e2 refs from page.read; prefer refs over CSS when available.
|
|
1738
|
+
Click ref: surf click e5
|
|
1739
|
+
Click selector/coords: surf click --selector ".btn" | surf click 100 200
|
|
1740
|
+
Type: surf type "text" --submit # use --ref e5 to target a field
|
|
1741
|
+
Screenshot: surf screenshot /tmp/shot.png # auto-saves to /tmp if no path
|
|
1742
|
+
Full page screenshot: surf screenshot --full-page /tmp/full.png
|
|
1743
|
+
Record animation: surf record --duration 2000 --fps 10 --output /tmp/anim.gif
|
|
1744
|
+
Animation audit: surf animate-audit --selector ".thing" --duration 2000 --fps 10
|
|
1745
|
+
Performance audit: surf perf-audit --duration 3000 --trigger "click:.cta" --output /tmp/perf.json
|
|
1746
|
+
JavaScript: surf js "return document.title"
|
|
1747
|
+
Scroll: surf scroll down 800 | surf scroll up 400 | surf scroll bottom | surf scroll top
|
|
1748
|
+
Find by semantics: surf locate.role button --name "Submit" --action click
|
|
1749
|
+
Device/viewport: surf emulate.device "iPhone 14" | surf resize 375 812
|
|
1750
|
+
Cookies: surf cookie list | surf cookie get "name" | surf cookie delete "name"
|
|
1751
|
+
Window isolation: surf window.new "https://example.com" then pass --window-id <id>
|
|
1752
|
+
Concurrency: surf serializes commands per socket; use --no-lock only for intentional bypass
|
|
1753
|
+
Doctor: surf doctor --browser all # native host/socket diagnostics
|
|
1754
|
+
Workflow: surf do 'go "https://example.com" | wait 2 | read | click e5 | screenshot'
|
|
1755
|
+
More help: surf --help-full | surf <command> --help | surf --help-topic refs | surf --find <query>`);
|
|
1756
|
+
};
|
|
1757
|
+
|
|
1544
1758
|
const showFullHelp = () => {
|
|
1545
1759
|
console.log(`surf v${VERSION} - Browser automation CLI
|
|
1546
1760
|
|
|
@@ -1560,11 +1774,19 @@ Usage: surf <command> [args] [options]
|
|
|
1560
1774
|
console.log(`Aliases: snap -> screenshot, read -> page.read, find -> search, go -> navigate
|
|
1561
1775
|
|
|
1562
1776
|
Options:
|
|
1777
|
+
--remote <host>:<port> Route requests to a remote native host
|
|
1778
|
+
--remote-credential <path> Use a mode-0600 Ed25519 remote credential file
|
|
1563
1779
|
--tab-id <id> Target specific tab
|
|
1564
1780
|
--window-id <id> Target specific window (isolate from your browsing)
|
|
1565
1781
|
--json Output raw JSON
|
|
1566
1782
|
--auto-capture On error: capture screenshot + console to /tmp
|
|
1567
1783
|
--soft-fail On error: warn and exit 0 (for non-critical commands)
|
|
1784
|
+
--no-lock Bypass the per-socket browser request lock
|
|
1785
|
+
|
|
1786
|
+
Remote Credentials (run on the browser host):
|
|
1787
|
+
surf remote authorize <label> --output <credential-file>
|
|
1788
|
+
surf remote list
|
|
1789
|
+
surf remote revoke <label>
|
|
1568
1790
|
|
|
1569
1791
|
Script Mode:
|
|
1570
1792
|
surf --script <file> Run workflow from JSON
|
|
@@ -1673,7 +1895,7 @@ const showToolHelp = (toolName) => {
|
|
|
1673
1895
|
const fuzzyFind = (query) => {
|
|
1674
1896
|
const terms = query.toLowerCase().split(/\s+/);
|
|
1675
1897
|
const results = [];
|
|
1676
|
-
|
|
1898
|
+
|
|
1677
1899
|
for (const [groupName, group] of Object.entries(TOOLS)) {
|
|
1678
1900
|
for (const [cmd, info] of Object.entries(group.commands)) {
|
|
1679
1901
|
if (info.alias) continue;
|
|
@@ -1684,7 +1906,7 @@ const fuzzyFind = (query) => {
|
|
|
1684
1906
|
}
|
|
1685
1907
|
}
|
|
1686
1908
|
}
|
|
1687
|
-
|
|
1909
|
+
|
|
1688
1910
|
return results.sort((a, b) => b.score - a.score);
|
|
1689
1911
|
};
|
|
1690
1912
|
|
|
@@ -1732,6 +1954,11 @@ const showAllTools = () => {
|
|
|
1732
1954
|
console.log(`\n Total: ${ALL_SOCKET_TOOLS.length} commands\n`);
|
|
1733
1955
|
};
|
|
1734
1956
|
|
|
1957
|
+
if (args[0] === "--llm-context") {
|
|
1958
|
+
showLlmContext();
|
|
1959
|
+
process.exit(0);
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1735
1962
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
1736
1963
|
showBasicHelp();
|
|
1737
1964
|
process.exit(0);
|
|
@@ -1776,7 +2003,7 @@ if (args[0] === "server") {
|
|
|
1776
2003
|
process.exit(0);
|
|
1777
2004
|
}
|
|
1778
2005
|
const { PiChromeMcpServer } = require("./mcp-server.cjs");
|
|
1779
|
-
const server = new PiChromeMcpServer();
|
|
2006
|
+
const server = new PiChromeMcpServer(endpoint);
|
|
1780
2007
|
server.start().catch((err) => {
|
|
1781
2008
|
console.error("MCP Server error:", err.message);
|
|
1782
2009
|
process.exit(1);
|
|
@@ -1790,11 +2017,17 @@ if (args[0] === "extension-path" || args[0] === "path") {
|
|
|
1790
2017
|
process.exit(0);
|
|
1791
2018
|
}
|
|
1792
2019
|
|
|
2020
|
+
if (args[0] === "doctor") {
|
|
2021
|
+
const { runDoctorCli } = require("./doctor.cjs");
|
|
2022
|
+
runDoctorCli(args.slice(1), endpoint).then((code) => process.exit(code));
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1793
2026
|
if (args[0] === "install") {
|
|
1794
2027
|
const { spawnSync } = require("child_process");
|
|
1795
2028
|
const scriptPath = require("path").resolve(__dirname, "../scripts/install-native-host.cjs");
|
|
1796
2029
|
const installArgs = args.slice(1);
|
|
1797
|
-
|
|
2030
|
+
|
|
1798
2031
|
if (installArgs.length === 0 || installArgs[0] === "--help" || installArgs[0] === "-h") {
|
|
1799
2032
|
console.log(`
|
|
1800
2033
|
Usage: surf install <extension-id> [options]
|
|
@@ -1809,11 +2042,16 @@ Options:
|
|
|
1809
2042
|
-b, --browser Browser(s) to install for (default: chrome)
|
|
1810
2043
|
Values: chrome, chromium, brave, edge, arc, helium, all
|
|
1811
2044
|
Multiple: --browser chrome,brave
|
|
2045
|
+
--target Install target: auto, linux, windows
|
|
2046
|
+
On WSL2, auto installs for Windows Chrome. Use linux for WSLg/Linux browsers.
|
|
2047
|
+
--listen <tailscale-ip>:<port>
|
|
2048
|
+
Requires surf remote authorize <label> --output <path> first.
|
|
1812
2049
|
|
|
1813
2050
|
Examples:
|
|
1814
2051
|
surf install hnfbepgmaoklhekckbpjnleifhahkcpl
|
|
1815
2052
|
surf install hnfbepgmaoklhekckbpjnleifhahkcpl --browser brave
|
|
1816
2053
|
surf install hnfbepgmaoklhekckbpjnleifhahkcpl --browser all
|
|
2054
|
+
surf install hnfbepgmaoklhekckbpjnleifhahkcpl --target linux
|
|
1817
2055
|
`);
|
|
1818
2056
|
process.exit(0);
|
|
1819
2057
|
}
|
|
@@ -1828,7 +2066,7 @@ if (args[0] === "uninstall") {
|
|
|
1828
2066
|
const { spawnSync } = require("child_process");
|
|
1829
2067
|
const scriptPath = require("path").resolve(__dirname, "../scripts/uninstall-native-host.cjs");
|
|
1830
2068
|
const uninstallArgs = args.slice(1);
|
|
1831
|
-
|
|
2069
|
+
|
|
1832
2070
|
if (uninstallArgs.includes("--help") || uninstallArgs.includes("-h")) {
|
|
1833
2071
|
console.log(`
|
|
1834
2072
|
Usage: surf uninstall [options]
|
|
@@ -1839,11 +2077,14 @@ Options:
|
|
|
1839
2077
|
-b, --browser Browser(s) to uninstall from (default: chrome)
|
|
1840
2078
|
Values: chrome, chromium, brave, edge, arc, helium, all
|
|
1841
2079
|
-a, --all Uninstall from all browsers and remove wrapper
|
|
2080
|
+
--target Install target to remove: auto, linux, windows
|
|
2081
|
+
On WSL2, auto removes Windows-browser manifests. Use linux for WSLg/Linux browsers.
|
|
1842
2082
|
|
|
1843
2083
|
Examples:
|
|
1844
2084
|
surf uninstall
|
|
1845
2085
|
surf uninstall --browser brave
|
|
1846
2086
|
surf uninstall --all
|
|
2087
|
+
surf uninstall --target linux
|
|
1847
2088
|
`);
|
|
1848
2089
|
process.exit(0);
|
|
1849
2090
|
}
|
|
@@ -1948,44 +2189,24 @@ if (args.includes("--script")) {
|
|
|
1948
2189
|
process.exit(1);
|
|
1949
2190
|
}
|
|
1950
2191
|
|
|
2192
|
+
let scriptTransport;
|
|
1951
2193
|
const sendScriptRequest = (toolName, toolArgs = {}) => {
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
});
|
|
1963
|
-
let buf = "";
|
|
1964
|
-
sock.on("data", (d) => {
|
|
1965
|
-
buf += d.toString();
|
|
1966
|
-
const lines = buf.split("\n");
|
|
1967
|
-
buf = lines.pop();
|
|
1968
|
-
for (const line of lines) {
|
|
1969
|
-
if (!line.trim()) continue;
|
|
1970
|
-
try {
|
|
1971
|
-
const resp = JSON.parse(line);
|
|
1972
|
-
sock.end();
|
|
1973
|
-
resolve(resp);
|
|
1974
|
-
} catch {
|
|
1975
|
-
sock.end();
|
|
1976
|
-
reject(new Error("Invalid JSON"));
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
1979
|
-
});
|
|
1980
|
-
sock.on("error", (e) => reject(e));
|
|
1981
|
-
let timeoutId;
|
|
1982
|
-
timeoutId = setTimeout(() => { sock.destroy(); reject(new Error("Timeout")); }, 30000);
|
|
1983
|
-
sock.on("close", () => clearTimeout(timeoutId));
|
|
1984
|
-
});
|
|
2194
|
+
const req = {
|
|
2195
|
+
type: "tool_request",
|
|
2196
|
+
method: "execute_tool",
|
|
2197
|
+
params: { tool: toolName, args: toolArgs },
|
|
2198
|
+
id: "cli-" + Date.now() + "-" + Math.random(),
|
|
2199
|
+
};
|
|
2200
|
+
if (scriptTabId) req.tabId = parseInt(scriptTabId, 10);
|
|
2201
|
+
const prepared = endpoint.kind === "remote" ? prepareRemoteTool(toolName, toolArgs) : (() => { const args = validateLocalToolPaths(toolName, toolArgs); return { args, uploads: [], downloads: [] }; })();
|
|
2202
|
+
req.params.args = prepared.args;
|
|
2203
|
+
return scriptTransport.request(req, resolveRequestDeadlineMs(toolName, prepared.args), prepared);
|
|
1985
2204
|
};
|
|
1986
2205
|
|
|
1987
2206
|
const runScript = async () => {
|
|
1988
|
-
|
|
2207
|
+
try {
|
|
2208
|
+
if (!dryRun) scriptTransport = await openClientTransport(endpoint);
|
|
2209
|
+
const total = script.steps.length;
|
|
1989
2210
|
const results = [];
|
|
1990
2211
|
let failed = 0;
|
|
1991
2212
|
|
|
@@ -2043,10 +2264,22 @@ if (args.includes("--script")) {
|
|
|
2043
2264
|
console.log(`Summary: ${passed} passed, ${failed} failed, ${total} total`);
|
|
2044
2265
|
}
|
|
2045
2266
|
|
|
2046
|
-
|
|
2267
|
+
return failed > 0 ? 1 : 0;
|
|
2268
|
+
} finally {
|
|
2269
|
+
await scriptTransport?.close();
|
|
2270
|
+
}
|
|
2047
2271
|
};
|
|
2048
2272
|
|
|
2049
|
-
|
|
2273
|
+
if (!dryRun) {
|
|
2274
|
+
installBrowserLock(parseBrowserLockOptions(args.includes("--no-lock")), endpoint);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
runScript()
|
|
2278
|
+
.then((code) => process.exit(code))
|
|
2279
|
+
.catch((error) => {
|
|
2280
|
+
console.error(`Error: ${error.message}`);
|
|
2281
|
+
process.exit(1);
|
|
2282
|
+
});
|
|
2050
2283
|
return;
|
|
2051
2284
|
}
|
|
2052
2285
|
|
|
@@ -2063,13 +2296,13 @@ if (args[0] === "do") {
|
|
|
2063
2296
|
let wantJson = false;
|
|
2064
2297
|
let tabId = undefined;
|
|
2065
2298
|
let windowId = undefined;
|
|
2066
|
-
|
|
2299
|
+
|
|
2067
2300
|
// Reserved flags that aren't workflow args
|
|
2068
|
-
const reservedFlags = ['file', 'f', 'dry-run', 'on-error', 'no-auto-wait', 'step-delay', 'json', 'tab-id', 'window-id'];
|
|
2069
|
-
|
|
2301
|
+
const reservedFlags = ['file', 'f', 'dry-run', 'on-error', 'no-auto-wait', 'step-delay', 'json', 'tab-id', 'window-id', 'no-lock'];
|
|
2302
|
+
|
|
2070
2303
|
// Workflow-specific args (collected for variable substitution)
|
|
2071
2304
|
const workflowArgs = {};
|
|
2072
|
-
|
|
2305
|
+
|
|
2073
2306
|
// Parse do-specific arguments
|
|
2074
2307
|
for (let i = 0; i < doArgs.length; i++) {
|
|
2075
2308
|
const arg = doArgs[i];
|
|
@@ -2117,7 +2350,7 @@ if (args[0] === "do") {
|
|
|
2117
2350
|
commandsInput = arg;
|
|
2118
2351
|
}
|
|
2119
2352
|
}
|
|
2120
|
-
|
|
2353
|
+
|
|
2121
2354
|
if (!commandsInput && !fileInput) {
|
|
2122
2355
|
console.error("Error: commands string, workflow name, or --file required");
|
|
2123
2356
|
console.error('Usage: surf do \'go "url" | click e5\'');
|
|
@@ -2125,11 +2358,11 @@ if (args[0] === "do") {
|
|
|
2125
2358
|
console.error(" surf do my-workflow --arg1 value1 --arg2 value2");
|
|
2126
2359
|
process.exit(1);
|
|
2127
2360
|
}
|
|
2128
|
-
|
|
2361
|
+
|
|
2129
2362
|
let steps;
|
|
2130
2363
|
let workflow = null; // Full workflow object (for arg validation)
|
|
2131
2364
|
let workflowName = null;
|
|
2132
|
-
|
|
2365
|
+
|
|
2133
2366
|
try {
|
|
2134
2367
|
if (fileInput) {
|
|
2135
2368
|
// Explicit file path via --file
|
|
@@ -2143,7 +2376,7 @@ if (args[0] === "do") {
|
|
|
2143
2376
|
} else {
|
|
2144
2377
|
// Resolve: inline | file path | named workflow
|
|
2145
2378
|
const resolved = resolveWorkflow(commandsInput);
|
|
2146
|
-
|
|
2379
|
+
|
|
2147
2380
|
if (resolved.type === 'inline') {
|
|
2148
2381
|
// Inline pipe syntax
|
|
2149
2382
|
steps = parseDoCommands(resolved.content);
|
|
@@ -2166,13 +2399,13 @@ if (args[0] === "do") {
|
|
|
2166
2399
|
}
|
|
2167
2400
|
}
|
|
2168
2401
|
}
|
|
2169
|
-
|
|
2402
|
+
|
|
2170
2403
|
// Process workflow file if loaded
|
|
2171
2404
|
if (workflow) {
|
|
2172
2405
|
if (!workflow.steps || !Array.isArray(workflow.steps)) {
|
|
2173
2406
|
throw new Error("Workflow must have a 'steps' array");
|
|
2174
2407
|
}
|
|
2175
|
-
|
|
2408
|
+
|
|
2176
2409
|
// Validate required args
|
|
2177
2410
|
const argErrors = validateWorkflowArgs(workflow, workflowArgs);
|
|
2178
2411
|
if (argErrors.length > 0) {
|
|
@@ -2190,7 +2423,7 @@ if (args[0] === "do") {
|
|
|
2190
2423
|
console.error(`\nRun 'surf workflow.info ${workflowName}' for details.`);
|
|
2191
2424
|
process.exit(1);
|
|
2192
2425
|
}
|
|
2193
|
-
|
|
2426
|
+
|
|
2194
2427
|
// Convert steps: support both { tool, args } and { cmd, args } formats
|
|
2195
2428
|
// Also preserve loop steps as-is
|
|
2196
2429
|
steps = workflow.steps.map(s => {
|
|
@@ -2199,16 +2432,16 @@ if (args[0] === "do") {
|
|
|
2199
2432
|
const convertSteps = (stepsArr) => stepsArr.map(ns => {
|
|
2200
2433
|
if (ns.repeat !== undefined || ns.each !== undefined) {
|
|
2201
2434
|
// Recursively convert nested loop steps and until condition
|
|
2202
|
-
return {
|
|
2203
|
-
...ns,
|
|
2435
|
+
return {
|
|
2436
|
+
...ns,
|
|
2204
2437
|
steps: convertSteps(ns.steps || []),
|
|
2205
2438
|
until: ns.until ? { cmd: ns.until.tool || ns.until.cmd, args: ns.until.args || {} } : undefined
|
|
2206
2439
|
};
|
|
2207
2440
|
}
|
|
2208
2441
|
return { cmd: ns.tool || ns.cmd, args: ns.args || {}, as: ns.as };
|
|
2209
2442
|
});
|
|
2210
|
-
return {
|
|
2211
|
-
...s,
|
|
2443
|
+
return {
|
|
2444
|
+
...s,
|
|
2212
2445
|
steps: convertSteps(s.steps || []),
|
|
2213
2446
|
until: s.until ? { cmd: s.until.tool || s.until.cmd, args: s.until.args || {} } : undefined
|
|
2214
2447
|
};
|
|
@@ -2220,15 +2453,15 @@ if (args[0] === "do") {
|
|
|
2220
2453
|
console.error(`Error: Failed to parse workflow: ${e.message}`);
|
|
2221
2454
|
process.exit(1);
|
|
2222
2455
|
}
|
|
2223
|
-
|
|
2456
|
+
|
|
2224
2457
|
if (!steps || steps.length === 0) {
|
|
2225
2458
|
console.error("Error: No commands found in workflow");
|
|
2226
2459
|
process.exit(1);
|
|
2227
2460
|
}
|
|
2228
|
-
|
|
2461
|
+
|
|
2229
2462
|
// Apply arg defaults
|
|
2230
2463
|
const vars = workflow ? applyArgDefaults(workflow, workflowArgs) : workflowArgs;
|
|
2231
|
-
|
|
2464
|
+
|
|
2232
2465
|
// Validate with --dry-run
|
|
2233
2466
|
if (dryRun) {
|
|
2234
2467
|
if (workflowName) {
|
|
@@ -2247,7 +2480,9 @@ if (args[0] === "do") {
|
|
|
2247
2480
|
}
|
|
2248
2481
|
process.exit(0);
|
|
2249
2482
|
}
|
|
2250
|
-
|
|
2483
|
+
|
|
2484
|
+
installBrowserLock(parseBrowserLockOptions(doArgs.includes("--no-lock")), endpoint);
|
|
2485
|
+
|
|
2251
2486
|
if (!wantJson) {
|
|
2252
2487
|
if (workflowName) {
|
|
2253
2488
|
console.log(`Running workflow: ${workflowName} (${steps.length} steps)...\n`);
|
|
@@ -2255,9 +2490,12 @@ if (args[0] === "do") {
|
|
|
2255
2490
|
console.log(`Running workflow (${steps.length} steps)...\n`);
|
|
2256
2491
|
}
|
|
2257
2492
|
}
|
|
2258
|
-
|
|
2493
|
+
|
|
2259
2494
|
const runWorkflow = async () => {
|
|
2260
|
-
|
|
2495
|
+
let transport;
|
|
2496
|
+
try {
|
|
2497
|
+
transport = await openClientTransport(endpoint);
|
|
2498
|
+
const result = await executeDoSteps(steps, {
|
|
2261
2499
|
onError,
|
|
2262
2500
|
autoWait: !noAutoWait,
|
|
2263
2501
|
stepDelay,
|
|
@@ -2266,37 +2504,47 @@ if (args[0] === "do") {
|
|
|
2266
2504
|
context: {
|
|
2267
2505
|
tabId,
|
|
2268
2506
|
windowId,
|
|
2507
|
+
endpoint,
|
|
2508
|
+
transport,
|
|
2269
2509
|
},
|
|
2270
|
-
|
|
2271
|
-
|
|
2510
|
+
});
|
|
2511
|
+
|
|
2272
2512
|
// Print summary
|
|
2273
2513
|
if (wantJson) {
|
|
2274
2514
|
console.log(JSON.stringify(result, null, 2));
|
|
2275
|
-
|
|
2515
|
+
return result.status === "completed" ? 0 : 1;
|
|
2276
2516
|
}
|
|
2277
|
-
|
|
2517
|
+
|
|
2278
2518
|
console.log("");
|
|
2279
2519
|
if (result.status === "completed") {
|
|
2280
2520
|
console.log(`Completed: ${result.completedSteps}/${result.totalSteps} steps (${result.totalMs}ms)`);
|
|
2281
|
-
|
|
2521
|
+
return 0;
|
|
2282
2522
|
} else if (result.status === "partial") {
|
|
2283
2523
|
console.log(`Partial: ${result.completedSteps}/${result.totalSteps} steps completed, ${result.failed} failed`);
|
|
2284
|
-
|
|
2524
|
+
return 1;
|
|
2285
2525
|
} else {
|
|
2286
2526
|
console.error(`Failed: ${result.completedSteps}/${result.totalSteps} steps completed`);
|
|
2287
2527
|
if (result.error) console.error(`Error: ${result.error}`);
|
|
2288
|
-
|
|
2528
|
+
return 1;
|
|
2529
|
+
}
|
|
2530
|
+
} finally {
|
|
2531
|
+
transport?.close();
|
|
2289
2532
|
}
|
|
2290
2533
|
};
|
|
2291
|
-
|
|
2292
|
-
runWorkflow()
|
|
2534
|
+
|
|
2535
|
+
runWorkflow()
|
|
2536
|
+
.then((code) => process.exit(code))
|
|
2537
|
+
.catch((error) => {
|
|
2538
|
+
console.error(`Error: ${error.message}`);
|
|
2539
|
+
process.exit(1);
|
|
2540
|
+
});
|
|
2293
2541
|
return;
|
|
2294
2542
|
}
|
|
2295
2543
|
|
|
2296
2544
|
// Handle workflow management commands
|
|
2297
2545
|
if (args[0] === "workflow.list") {
|
|
2298
2546
|
const workflows = listWorkflows();
|
|
2299
|
-
|
|
2547
|
+
|
|
2300
2548
|
if (workflows.length === 0) {
|
|
2301
2549
|
console.log("No workflows found.");
|
|
2302
2550
|
console.log(`\nWorkflow directories:`);
|
|
@@ -2306,13 +2554,13 @@ if (args[0] === "workflow.list") {
|
|
|
2306
2554
|
console.log(`\nCreate a workflow JSON file in one of these directories.`);
|
|
2307
2555
|
process.exit(0);
|
|
2308
2556
|
}
|
|
2309
|
-
|
|
2557
|
+
|
|
2310
2558
|
// Group by scope
|
|
2311
2559
|
const byScope = { project: [], user: [] };
|
|
2312
2560
|
for (const w of workflows) {
|
|
2313
2561
|
byScope[w.scope].push(w);
|
|
2314
2562
|
}
|
|
2315
|
-
|
|
2563
|
+
|
|
2316
2564
|
if (byScope.user.length > 0) {
|
|
2317
2565
|
console.log(`User Workflows (~/.surf/workflows/):`);
|
|
2318
2566
|
for (const w of byScope.user) {
|
|
@@ -2321,7 +2569,7 @@ if (args[0] === "workflow.list") {
|
|
|
2321
2569
|
}
|
|
2322
2570
|
console.log("");
|
|
2323
2571
|
}
|
|
2324
|
-
|
|
2572
|
+
|
|
2325
2573
|
if (byScope.project.length > 0) {
|
|
2326
2574
|
console.log(`Project Workflows (./.surf/workflows/):`);
|
|
2327
2575
|
for (const w of byScope.project) {
|
|
@@ -2330,7 +2578,7 @@ if (args[0] === "workflow.list") {
|
|
|
2330
2578
|
}
|
|
2331
2579
|
console.log("");
|
|
2332
2580
|
}
|
|
2333
|
-
|
|
2581
|
+
|
|
2334
2582
|
console.log(`Run 'surf workflow.info <name>' for details.`);
|
|
2335
2583
|
process.exit(0);
|
|
2336
2584
|
}
|
|
@@ -2342,16 +2590,16 @@ if (args[0] === "workflow.info") {
|
|
|
2342
2590
|
console.error("Usage: surf workflow.info <name>");
|
|
2343
2591
|
process.exit(1);
|
|
2344
2592
|
}
|
|
2345
|
-
|
|
2593
|
+
|
|
2346
2594
|
const info = getWorkflowInfo(name);
|
|
2347
2595
|
if (info.error) {
|
|
2348
2596
|
console.error(`Error: ${info.error}`);
|
|
2349
2597
|
process.exit(1);
|
|
2350
2598
|
}
|
|
2351
|
-
|
|
2599
|
+
|
|
2352
2600
|
console.log(`${info.name}${info.description ? ` - ${info.description}` : ''}`);
|
|
2353
2601
|
console.log("");
|
|
2354
|
-
|
|
2602
|
+
|
|
2355
2603
|
// Arguments
|
|
2356
2604
|
if (info.args && Object.keys(info.args).length > 0) {
|
|
2357
2605
|
console.log("Arguments:");
|
|
@@ -2364,18 +2612,18 @@ if (args[0] === "workflow.info") {
|
|
|
2364
2612
|
}
|
|
2365
2613
|
console.log("");
|
|
2366
2614
|
}
|
|
2367
|
-
|
|
2615
|
+
|
|
2368
2616
|
// Steps
|
|
2369
2617
|
console.log(`Steps (${info.steps.length}):`);
|
|
2370
2618
|
info.steps.forEach((step, i) => {
|
|
2371
2619
|
console.log(` ${i + 1}. ${formatStep(step)}`);
|
|
2372
2620
|
});
|
|
2373
2621
|
console.log("");
|
|
2374
|
-
|
|
2622
|
+
|
|
2375
2623
|
// Location
|
|
2376
2624
|
console.log(`Location: ${info.path}`);
|
|
2377
2625
|
console.log("");
|
|
2378
|
-
|
|
2626
|
+
|
|
2379
2627
|
// Example run command
|
|
2380
2628
|
const argExample = Object.entries(info.args || {})
|
|
2381
2629
|
.filter(([_, spec]) => spec.required)
|
|
@@ -2383,7 +2631,7 @@ if (args[0] === "workflow.info") {
|
|
|
2383
2631
|
.join(' ');
|
|
2384
2632
|
console.log(`Run:`);
|
|
2385
2633
|
console.log(` surf do ${name}${argExample ? ' ' + argExample : ''}`);
|
|
2386
|
-
|
|
2634
|
+
|
|
2387
2635
|
process.exit(0);
|
|
2388
2636
|
}
|
|
2389
2637
|
|
|
@@ -2394,9 +2642,9 @@ if (args[0] === "workflow.validate") {
|
|
|
2394
2642
|
console.error("Usage: surf workflow.validate <file>");
|
|
2395
2643
|
process.exit(1);
|
|
2396
2644
|
}
|
|
2397
|
-
|
|
2645
|
+
|
|
2398
2646
|
const result = validateWorkflowFile(filePath);
|
|
2399
|
-
|
|
2647
|
+
|
|
2400
2648
|
if (result.valid) {
|
|
2401
2649
|
console.log(`✓ Valid workflow: ${filePath}`);
|
|
2402
2650
|
console.log(` Name: ${result.workflow.name || '(unnamed)'}`);
|
|
@@ -2414,9 +2662,7 @@ if (args[0] === "workflow.validate") {
|
|
|
2414
2662
|
}
|
|
2415
2663
|
}
|
|
2416
2664
|
|
|
2417
|
-
const BOOLEAN_FLAGS = ["auto-capture", "json", "stream", "dry-run", "stop-on-error", "fail-fast", "clear", "submit", "all", "case-sensitive", "hard", "annotate", "fullpage", "reset", "no-screenshot", "full", "soft-fail", "has-body", "exclude-static", "v", "vv", "request", "by-tab", "har", "jsonl", "no-save", "no-auto-wait"];
|
|
2418
|
-
|
|
2419
|
-
const AUTO_SCREENSHOT_TOOLS = ["click", "type", "key", "smart_type", "form.fill", "form_input", "drag", "hover", "scroll", "scroll.top", "scroll.bottom", "scroll.to", "dialog.accept", "dialog.dismiss", "js", "eval"];
|
|
2665
|
+
const BOOLEAN_FLAGS = ["auto-capture", "json", "stream", "dry-run", "stop-on-error", "fail-fast", "clear", "submit", "all", "case-sensitive", "hard", "annotate", "fullpage", "full-page", "reset", "no-screenshot", "full", "soft-fail", "has-body", "exclude-static", "v", "vv", "request", "by-tab", "har", "jsonl", "no-save", "no-auto-wait", "no-lock"];
|
|
2420
2666
|
|
|
2421
2667
|
const parseArgs = (rawArgs) => {
|
|
2422
2668
|
const result = { positional: [], options: {} };
|
|
@@ -2462,6 +2708,22 @@ let { positional, options } = parseArgs(args);
|
|
|
2462
2708
|
let tool = positional[0];
|
|
2463
2709
|
let firstArg = positional[1];
|
|
2464
2710
|
|
|
2711
|
+
if (tool === "cookie" && firstArg) {
|
|
2712
|
+
const cookieSubcommands = {
|
|
2713
|
+
list: "cookie.list",
|
|
2714
|
+
get: "cookie.get",
|
|
2715
|
+
set: "cookie.set",
|
|
2716
|
+
clear: "cookie.clear",
|
|
2717
|
+
delete: "cookie.clear",
|
|
2718
|
+
};
|
|
2719
|
+
const cookieTool = cookieSubcommands[firstArg];
|
|
2720
|
+
if (cookieTool) {
|
|
2721
|
+
tool = cookieTool;
|
|
2722
|
+
positional = [tool, ...positional.slice(2)];
|
|
2723
|
+
firstArg = positional[1];
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2465
2727
|
if (!tool) {
|
|
2466
2728
|
console.error("Error: No command specified");
|
|
2467
2729
|
process.exit(1);
|
|
@@ -2478,9 +2740,14 @@ tool = ALIASES[tool] || tool;
|
|
|
2478
2740
|
// Auto-save screenshots to temp file when no --output specified
|
|
2479
2741
|
// This ensures agents always get a usable file path, not just an in-memory ID
|
|
2480
2742
|
// Can be disabled with --no-save flag or autoSaveScreenshots: false in surf.json
|
|
2743
|
+
if (options["full-page"] === true) {
|
|
2744
|
+
options.fullpage = true;
|
|
2745
|
+
delete options["full-page"];
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2481
2748
|
const config = loadConfig();
|
|
2482
2749
|
const autoSaveEnabled = config.autoSaveScreenshots !== false && !options["no-save"];
|
|
2483
|
-
if (tool === "screenshot" && !options.output && !options.savePath && autoSaveEnabled) {
|
|
2750
|
+
if (tool === "screenshot" && !options.output && !options.savePath && firstArg === undefined && autoSaveEnabled) {
|
|
2484
2751
|
options.savePath = path.join(SURF_TMP, `surf-snap-${Date.now()}.png`);
|
|
2485
2752
|
}
|
|
2486
2753
|
|
|
@@ -2532,6 +2799,7 @@ const PRIMARY_ARG_MAP = {
|
|
|
2532
2799
|
"tab.switch": "id",
|
|
2533
2800
|
close_tab: "tab_id",
|
|
2534
2801
|
"tab.close": "id",
|
|
2802
|
+
"tab.move": "id",
|
|
2535
2803
|
"tab.name": "name",
|
|
2536
2804
|
"tab.unname": "name",
|
|
2537
2805
|
scroll_to_position: "position",
|
|
@@ -2541,6 +2809,8 @@ const PRIMARY_ARG_MAP = {
|
|
|
2541
2809
|
"emulate.cpu": "rate",
|
|
2542
2810
|
search: "term",
|
|
2543
2811
|
find: "term",
|
|
2812
|
+
"cookie.get": "name",
|
|
2813
|
+
"cookie.clear": "name",
|
|
2544
2814
|
"wait.element": "selector",
|
|
2545
2815
|
"wait.url": "pattern",
|
|
2546
2816
|
zoom: "level",
|
|
@@ -2561,7 +2831,20 @@ const PRIMARY_ARG_MAP = {
|
|
|
2561
2831
|
"select": "selector",
|
|
2562
2832
|
};
|
|
2563
2833
|
|
|
2564
|
-
|
|
2834
|
+
let toolArgs = { ...options };
|
|
2835
|
+
|
|
2836
|
+
if (tool === "scroll" && firstArg) {
|
|
2837
|
+
if (firstArg === "top" || firstArg === "bottom") {
|
|
2838
|
+
tool = `scroll.${firstArg}`;
|
|
2839
|
+
firstArg = undefined;
|
|
2840
|
+
} else if (["up", "down", "left", "right"].includes(firstArg)) {
|
|
2841
|
+
if (toolArgs.direction === undefined) toolArgs.direction = firstArg;
|
|
2842
|
+
if (positional[2] !== undefined && /^-?\d+$/.test(positional[2]) && toolArgs.amount === undefined && toolArgs.scroll_amount === undefined) {
|
|
2843
|
+
toolArgs.scroll_pixels = parseInt(positional[2], 10);
|
|
2844
|
+
}
|
|
2845
|
+
firstArg = undefined;
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2565
2848
|
|
|
2566
2849
|
if (tool === "click" && firstArg) {
|
|
2567
2850
|
if (/^e\d+$/.test(firstArg)) {
|
|
@@ -2574,6 +2857,30 @@ if (tool === "click" && firstArg) {
|
|
|
2574
2857
|
}
|
|
2575
2858
|
}
|
|
2576
2859
|
|
|
2860
|
+
if (tool === "resize") {
|
|
2861
|
+
if (firstArg !== undefined && toolArgs.width === undefined) {
|
|
2862
|
+
let val = firstArg;
|
|
2863
|
+
if (/^-?\d+$/.test(val)) val = parseInt(val, 10);
|
|
2864
|
+
toolArgs.width = val;
|
|
2865
|
+
}
|
|
2866
|
+
if (positional[2] !== undefined && toolArgs.height === undefined) {
|
|
2867
|
+
let val = positional[2];
|
|
2868
|
+
if (/^-?\d+$/.test(val)) val = parseInt(val, 10);
|
|
2869
|
+
toolArgs.height = val;
|
|
2870
|
+
}
|
|
2871
|
+
firstArg = undefined;
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
if (tool === "screenshot" && firstArg !== undefined && toolArgs.output === undefined && toolArgs.savePath === undefined) {
|
|
2875
|
+
toolArgs.savePath = firstArg;
|
|
2876
|
+
firstArg = undefined;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
if (tool === "record" && firstArg !== undefined && toolArgs.output === undefined) {
|
|
2880
|
+
toolArgs.output = firstArg;
|
|
2881
|
+
firstArg = undefined;
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2577
2884
|
if (firstArg !== undefined) {
|
|
2578
2885
|
const primaryKey = PRIMARY_ARG_MAP[tool];
|
|
2579
2886
|
if (primaryKey && toolArgs[primaryKey] === undefined) {
|
|
@@ -2585,7 +2892,7 @@ if (firstArg !== undefined) {
|
|
|
2585
2892
|
}
|
|
2586
2893
|
}
|
|
2587
2894
|
|
|
2588
|
-
if (tool === "js" && toolArgs.file) {
|
|
2895
|
+
if ((tool === "js" || tool === "frame.js") && toolArgs.file) {
|
|
2589
2896
|
try {
|
|
2590
2897
|
toolArgs.code = fs.readFileSync(toolArgs.file, "utf8");
|
|
2591
2898
|
delete toolArgs.file;
|
|
@@ -2595,6 +2902,17 @@ if (tool === "js" && toolArgs.file) {
|
|
|
2595
2902
|
}
|
|
2596
2903
|
}
|
|
2597
2904
|
|
|
2905
|
+
if (tool === "batch" && toolArgs.file) {
|
|
2906
|
+
try {
|
|
2907
|
+
const parsed = JSON.parse(fs.readFileSync(toolArgs.file, "utf8"));
|
|
2908
|
+
toolArgs.actions = parsed;
|
|
2909
|
+
delete toolArgs.file;
|
|
2910
|
+
} catch (e) {
|
|
2911
|
+
console.error(`Error: Failed to read batch file: ${e.message}`);
|
|
2912
|
+
process.exit(1);
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2598
2916
|
// Handle select command: capture multiple values after selector
|
|
2599
2917
|
if (tool === "select" && positional.length > 2) {
|
|
2600
2918
|
const values = positional.slice(2); // All args after "select <selector>"
|
|
@@ -2646,6 +2964,9 @@ delete toolArgs["no-screenshot"];
|
|
|
2646
2964
|
const softFail = toolArgs["soft-fail"] === true;
|
|
2647
2965
|
delete toolArgs["soft-fail"];
|
|
2648
2966
|
|
|
2967
|
+
const lockOptions = parseBrowserLockOptions(toolArgs["no-lock"] === true);
|
|
2968
|
+
delete toolArgs["no-lock"];
|
|
2969
|
+
|
|
2649
2970
|
if (!noScreenshot && AUTO_SCREENSHOT_TOOLS.includes(tool)) {
|
|
2650
2971
|
toolArgs.autoScreenshot = true;
|
|
2651
2972
|
}
|
|
@@ -2656,23 +2977,26 @@ if (tool === "aistudio.build" && outputPath) {
|
|
|
2656
2977
|
toolArgs.output = path.resolve(outputPath);
|
|
2657
2978
|
}
|
|
2658
2979
|
if (tool === "gemini") {
|
|
2659
|
-
if (outputPath) toolArgs.output =
|
|
2660
|
-
if (toolArgs
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
toolArgs.file = path.resolve(toolArgs.file);
|
|
2980
|
+
if (outputPath !== undefined) toolArgs.output = outputPath;
|
|
2981
|
+
if (toolArgs.model) {
|
|
2982
|
+
const known = ["gemini-3.1-pro", "gemini-3.5-flash", "gemini-3.1-flash-lite"];
|
|
2983
|
+
if (!known.includes(toolArgs.model)) {
|
|
2984
|
+
process.stderr.write(
|
|
2985
|
+
`warning: unknown Gemini model "${toolArgs.model}"; using "gemini-3.1-pro". Available: ${known.join(", ")}\n`,
|
|
2986
|
+
);
|
|
2987
|
+
}
|
|
2668
2988
|
}
|
|
2669
2989
|
}
|
|
2990
|
+
if (tool === "network.export" && outputPath !== undefined) {
|
|
2991
|
+
toolArgs.output = outputPath;
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
if ((tool === "screenshot" || tool === "record" || tool === "perf-audit") && outputPath && typeof outputPath !== "string") {
|
|
2995
|
+
console.error("Error: --output requires a file path");
|
|
2996
|
+
process.exit(1);
|
|
2997
|
+
}
|
|
2670
2998
|
|
|
2671
2999
|
if (tool === "screenshot" && outputPath) {
|
|
2672
|
-
if (typeof outputPath !== "string") {
|
|
2673
|
-
console.error("Error: --output requires a file path");
|
|
2674
|
-
process.exit(1);
|
|
2675
|
-
}
|
|
2676
3000
|
toolArgs.savePath = outputPath;
|
|
2677
3001
|
if (options.full) toolArgs.full = true;
|
|
2678
3002
|
if (options["max-size"]) toolArgs["max-size"] = options["max-size"];
|
|
@@ -2688,7 +3012,9 @@ const streamMode = toolArgs.stream === true;
|
|
|
2688
3012
|
delete toolArgs.stream;
|
|
2689
3013
|
|
|
2690
3014
|
const streamLevel = toolArgs.level;
|
|
2691
|
-
|
|
3015
|
+
if (tool === "console" || tool === "network") {
|
|
3016
|
+
delete toolArgs.level;
|
|
3017
|
+
}
|
|
2692
3018
|
|
|
2693
3019
|
const streamFilter = toolArgs.filter;
|
|
2694
3020
|
delete toolArgs.filter;
|
|
@@ -2696,11 +3022,15 @@ delete toolArgs.filter;
|
|
|
2696
3022
|
let finalTool = tool;
|
|
2697
3023
|
if (methodFlag === "js") {
|
|
2698
3024
|
if (tool === "type") {
|
|
2699
|
-
if (
|
|
2700
|
-
|
|
2701
|
-
|
|
3025
|
+
if (toolArgs.ref) {
|
|
3026
|
+
finalTool = "type";
|
|
3027
|
+
} else {
|
|
3028
|
+
if (!toolArgs.selector) {
|
|
3029
|
+
console.error("Error: --selector, --into, or --ref required for type with --method js");
|
|
3030
|
+
process.exit(1);
|
|
3031
|
+
}
|
|
3032
|
+
finalTool = "smart_type";
|
|
2702
3033
|
}
|
|
2703
|
-
finalTool = "smart_type";
|
|
2704
3034
|
} else if (tool === "click") {
|
|
2705
3035
|
if (!toolArgs.selector) {
|
|
2706
3036
|
console.error("Error: --selector required for click with --method js");
|
|
@@ -2711,8 +3041,13 @@ if (methodFlag === "js") {
|
|
|
2711
3041
|
finalTool = "js";
|
|
2712
3042
|
}
|
|
2713
3043
|
} else if (methodFlag === "cdp") {
|
|
3044
|
+
if (tool === "type" && (toolArgs.selector || toolArgs.ref)) {
|
|
3045
|
+
console.error("Error: --method cdp types at the current focus and cannot be combined with --into, --selector, or --ref");
|
|
3046
|
+
process.exit(1);
|
|
3047
|
+
}
|
|
2714
3048
|
if (tool === "smart_type") {
|
|
2715
|
-
|
|
3049
|
+
console.error("Error: smart_type uses the JS input path and cannot be combined with --method cdp");
|
|
3050
|
+
process.exit(1);
|
|
2716
3051
|
}
|
|
2717
3052
|
}
|
|
2718
3053
|
|
|
@@ -2730,8 +3065,10 @@ if (streamMode && (tool === "console" || tool === "network")) {
|
|
|
2730
3065
|
|
|
2731
3066
|
let connectionTimeout = null;
|
|
2732
3067
|
let receivedData = false;
|
|
3068
|
+
let streamWriter;
|
|
2733
3069
|
|
|
2734
|
-
const sock =
|
|
3070
|
+
const sock = connectEndpoint(endpoint, () => {
|
|
3071
|
+
streamWriter = createSocketWriter(sock, { onOverflow: ({ error }) => sock.destroy(error) });
|
|
2735
3072
|
const req = {
|
|
2736
3073
|
type: "stream_request",
|
|
2737
3074
|
streamType,
|
|
@@ -2739,7 +3076,11 @@ if (streamMode && (tool === "console" || tool === "network")) {
|
|
|
2739
3076
|
id: "cli-stream-" + Date.now(),
|
|
2740
3077
|
...globalOpts,
|
|
2741
3078
|
};
|
|
2742
|
-
|
|
3079
|
+
streamWriter.send(req).catch((error) => sock.destroy(error));
|
|
3080
|
+
if (connectionTimeout) {
|
|
3081
|
+
clearTimeout(connectionTimeout);
|
|
3082
|
+
connectionTimeout = null;
|
|
3083
|
+
}
|
|
2743
3084
|
connectionTimeout = setTimeout(() => {
|
|
2744
3085
|
if (!receivedData) {
|
|
2745
3086
|
console.error("Error: Stream connection timeout (10s) - no data received");
|
|
@@ -2749,61 +3090,62 @@ if (streamMode && (tool === "console" || tool === "network")) {
|
|
|
2749
3090
|
}, 10000);
|
|
2750
3091
|
});
|
|
2751
3092
|
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
if (!line.trim()) continue;
|
|
2766
|
-
try {
|
|
2767
|
-
const msg = JSON.parse(line);
|
|
2768
|
-
if (msg.error) {
|
|
2769
|
-
console.error("Error:", msg.error);
|
|
2770
|
-
sock.end();
|
|
2771
|
-
process.exit(1);
|
|
2772
|
-
}
|
|
2773
|
-
if (msg.type === "extension_disconnected") {
|
|
2774
|
-
console.error(msg.message);
|
|
2775
|
-
sock.end();
|
|
2776
|
-
process.exit(1);
|
|
2777
|
-
}
|
|
2778
|
-
if (msg.type === "stream_started") {
|
|
2779
|
-
continue;
|
|
2780
|
-
}
|
|
2781
|
-
if (msg.type === "console_event") {
|
|
2782
|
-
const { level, text, timestamp } = msg;
|
|
2783
|
-
if (streamLevel && level !== streamLevel) continue;
|
|
2784
|
-
console.log(`[console] [${level}] ${formatTime(timestamp)} ${text}`);
|
|
2785
|
-
} else if (msg.type === "network_event") {
|
|
2786
|
-
const { method, url, status, duration } = msg;
|
|
2787
|
-
if (streamFilter && !url.includes(streamFilter)) continue;
|
|
2788
|
-
const statusStr = status !== undefined ? status : "...";
|
|
2789
|
-
const durationStr = duration !== undefined ? ` (${duration}ms)` : "";
|
|
2790
|
-
console.log(`[network] ${method} ${url} ${statusStr}${durationStr}`);
|
|
3093
|
+
connectionTimeout = setTimeout(() => {
|
|
3094
|
+
console.error(`Error: Stream connection timeout (10s) - could not connect to ${endpoint.display}`);
|
|
3095
|
+
sock.destroy();
|
|
3096
|
+
process.exit(1);
|
|
3097
|
+
}, 10000);
|
|
3098
|
+
|
|
3099
|
+
const parser = createFrameParser({
|
|
3100
|
+
onFrame(msg) {
|
|
3101
|
+
if (!receivedData) {
|
|
3102
|
+
receivedData = true;
|
|
3103
|
+
if (connectionTimeout) {
|
|
3104
|
+
clearTimeout(connectionTimeout);
|
|
3105
|
+
connectionTimeout = null;
|
|
2791
3106
|
}
|
|
2792
|
-
}
|
|
2793
|
-
|
|
3107
|
+
}
|
|
3108
|
+
if (msg.error) {
|
|
3109
|
+
console.error("Error:", msg.error);
|
|
3110
|
+
sock.end();
|
|
3111
|
+
process.exit(1);
|
|
3112
|
+
}
|
|
3113
|
+
if (msg.type === "extension_disconnected") {
|
|
3114
|
+
console.error(msg.message);
|
|
3115
|
+
sock.end();
|
|
3116
|
+
process.exit(1);
|
|
3117
|
+
}
|
|
3118
|
+
if (msg.type === "stream_started") return;
|
|
3119
|
+
if (msg.type === "console_event") {
|
|
3120
|
+
const { level, text, timestamp } = msg;
|
|
3121
|
+
if (streamLevel && level !== streamLevel) return;
|
|
3122
|
+
console.log(`[console] [${level}] ${formatTime(timestamp)} ${text}`);
|
|
3123
|
+
} else if (msg.type === "network_event") {
|
|
3124
|
+
const { method, url, status, duration } = msg;
|
|
3125
|
+
if (streamFilter && !url.includes(streamFilter)) return;
|
|
3126
|
+
const statusStr = status !== undefined ? status : "...";
|
|
3127
|
+
const durationStr = duration !== undefined ? ` (${duration}ms)` : "";
|
|
3128
|
+
console.log(`[network] ${method} ${url} ${statusStr}${durationStr}`);
|
|
3129
|
+
}
|
|
3130
|
+
},
|
|
3131
|
+
onError(error) {
|
|
3132
|
+
if (connectionTimeout) clearTimeout(connectionTimeout);
|
|
3133
|
+
console.error("Error:", error.message);
|
|
3134
|
+
sock.destroy();
|
|
3135
|
+
process.exit(1);
|
|
3136
|
+
},
|
|
2794
3137
|
});
|
|
3138
|
+
sock.on("data", (data) => parser.push(data));
|
|
2795
3139
|
|
|
2796
3140
|
sock.on("error", (e) => {
|
|
2797
|
-
if (
|
|
2798
|
-
|
|
2799
|
-
} else {
|
|
2800
|
-
console.error("Error:", e.message);
|
|
2801
|
-
}
|
|
3141
|
+
if (connectionTimeout) clearTimeout(connectionTimeout);
|
|
3142
|
+
console.error("Error:", formatEndpointError(e, endpoint, formatSocketError));
|
|
2802
3143
|
process.exit(1);
|
|
2803
3144
|
});
|
|
2804
3145
|
|
|
2805
3146
|
process.on("SIGINT", () => {
|
|
2806
|
-
|
|
3147
|
+
if (connectionTimeout) clearTimeout(connectionTimeout);
|
|
3148
|
+
streamWriter?.send({ type: "stream_stop" }).catch(() => {});
|
|
2807
3149
|
sock.end();
|
|
2808
3150
|
process.exit(0);
|
|
2809
3151
|
});
|
|
@@ -2811,6 +3153,17 @@ if (streamMode && (tool === "console" || tool === "network")) {
|
|
|
2811
3153
|
return;
|
|
2812
3154
|
}
|
|
2813
3155
|
|
|
3156
|
+
let transferPlan;
|
|
3157
|
+
try {
|
|
3158
|
+
transferPlan = endpoint.kind === "remote" ? prepareRemoteTool(finalTool, toolArgs) : (() => { const args = validateLocalToolPaths(finalTool, toolArgs); return { args, uploads: [], downloads: [] }; })();
|
|
3159
|
+
} catch (error) {
|
|
3160
|
+
const message = finalTool === "record" && endpoint.kind === "remote"
|
|
3161
|
+
? `record is not supported with remote endpoint ${endpoint.display}`
|
|
3162
|
+
: error.message;
|
|
3163
|
+
console.error(`Error: ${message}`);
|
|
3164
|
+
process.exit(1);
|
|
3165
|
+
}
|
|
3166
|
+
toolArgs = transferPlan.args;
|
|
2814
3167
|
const request = {
|
|
2815
3168
|
type: "tool_request",
|
|
2816
3169
|
method: "execute_tool",
|
|
@@ -2819,47 +3172,149 @@ const request = {
|
|
|
2819
3172
|
...globalOpts,
|
|
2820
3173
|
};
|
|
2821
3174
|
|
|
2822
|
-
const sendRequest = (toolName, toolArgs = {}) => {
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
buf += d.toString();
|
|
2837
|
-
const lines = buf.split("\n");
|
|
2838
|
-
buf = lines.pop();
|
|
2839
|
-
for (const line of lines) {
|
|
2840
|
-
if (!line.trim()) continue;
|
|
2841
|
-
try {
|
|
2842
|
-
const resp = JSON.parse(line);
|
|
2843
|
-
if (resp.type === "extension_disconnected") {
|
|
2844
|
-
sock.end();
|
|
2845
|
-
reject(new Error(resp.message));
|
|
2846
|
-
return;
|
|
2847
|
-
}
|
|
2848
|
-
sock.end();
|
|
2849
|
-
resolve(resp);
|
|
2850
|
-
} catch {
|
|
2851
|
-
sock.end();
|
|
2852
|
-
reject(new Error("Invalid JSON"));
|
|
2853
|
-
}
|
|
2854
|
-
}
|
|
2855
|
-
});
|
|
2856
|
-
sock.on("error", (e) => reject(e));
|
|
2857
|
-
let timeoutId;
|
|
2858
|
-
timeoutId = setTimeout(() => { sock.destroy(); reject(new Error("Timeout")); }, 5000);
|
|
2859
|
-
sock.on("close", () => clearTimeout(timeoutId));
|
|
2860
|
-
});
|
|
3175
|
+
const sendRequest = async (toolName, toolArgs = {}, timeoutMs = 5000) => {
|
|
3176
|
+
const transport = await openClientTransport(endpoint, { requestTimeoutMs: timeoutMs });
|
|
3177
|
+
try {
|
|
3178
|
+
const prepared = endpoint.kind === "remote" ? prepareRemoteTool(toolName, toolArgs) : (() => { const args = validateLocalToolPaths(toolName, toolArgs); return { args, uploads: [], downloads: [] }; })();
|
|
3179
|
+
return await transport.request({
|
|
3180
|
+
type: "tool_request",
|
|
3181
|
+
method: "execute_tool",
|
|
3182
|
+
params: { tool: toolName, args: prepared.args },
|
|
3183
|
+
id: "cli-" + Date.now() + "-" + Math.random(),
|
|
3184
|
+
...globalOpts,
|
|
3185
|
+
}, timeoutMs, prepared);
|
|
3186
|
+
} finally {
|
|
3187
|
+
await transport.close();
|
|
3188
|
+
}
|
|
2861
3189
|
};
|
|
2862
3190
|
|
|
3191
|
+
function parseRecordNumber(value, fallback, name, min, max) {
|
|
3192
|
+
if (value === undefined) return fallback;
|
|
3193
|
+
if (typeof value === "boolean") throw new Error(`${name} must be a number`);
|
|
3194
|
+
const parsed = Number(value);
|
|
3195
|
+
if (!Number.isFinite(parsed) || parsed < min || parsed > max) {
|
|
3196
|
+
throw new Error(`${name} must be between ${min} and ${max}`);
|
|
3197
|
+
}
|
|
3198
|
+
return parsed;
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
function parseRecordRect(value) {
|
|
3202
|
+
if (value === undefined) return null;
|
|
3203
|
+
if (typeof value !== "string") throw new Error("rect must be x,y,width,height");
|
|
3204
|
+
const parts = value.split(",").map((part) => Number(part.trim()));
|
|
3205
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isFinite(part))) {
|
|
3206
|
+
throw new Error("rect must be x,y,width,height");
|
|
3207
|
+
}
|
|
3208
|
+
const [x, y, width, height] = parts;
|
|
3209
|
+
if (x < 0 || y < 0 || width <= 0 || height <= 0) {
|
|
3210
|
+
throw new Error("rect must use non-negative x/y and positive width/height");
|
|
3211
|
+
}
|
|
3212
|
+
return { x, y, width, height, crop: `${width}x${height}+${x}+${y}` };
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
function assertToolOk(response, context) {
|
|
3216
|
+
if (!response?.error) return;
|
|
3217
|
+
const message = response.error.content?.[0]?.text || response.error.message || JSON.stringify(response.error);
|
|
3218
|
+
throw new Error(`${context}: ${message}`);
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
function assembleRecordGif(framePaths, output, fps, rect) {
|
|
3222
|
+
const delay = Math.max(1, Math.round(100 / fps));
|
|
3223
|
+
const args = ["-delay", String(delay), "-loop", "0", ...framePaths];
|
|
3224
|
+
if (rect) args.push("-crop", rect.crop, "+repage");
|
|
3225
|
+
args.push(output);
|
|
3226
|
+
|
|
3227
|
+
try {
|
|
3228
|
+
execFileSync("magick", args, { stdio: "pipe" });
|
|
3229
|
+
return "magick";
|
|
3230
|
+
} catch (magickError) {
|
|
3231
|
+
try {
|
|
3232
|
+
execFileSync("convert", args, { stdio: "pipe" });
|
|
3233
|
+
return "convert";
|
|
3234
|
+
} catch (convertError) {
|
|
3235
|
+
const detail = convertError && convertError.message ? convertError.message : String(convertError);
|
|
3236
|
+
throw new Error(`Failed to assemble GIF with ImageMagick. Install ImageMagick (magick or convert). Last error: ${detail}`);
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
async function runRecord() {
|
|
3242
|
+
const durationMs = parseRecordNumber(toolArgs.duration, 2000, "duration", 100, 10000);
|
|
3243
|
+
const fps = parseRecordNumber(toolArgs.fps, 10, "fps", 1, 30);
|
|
3244
|
+
const rect = parseRecordRect(toolArgs.rect);
|
|
3245
|
+
const output = path.resolve(outputPath || path.join(SURF_TMP, `surf-record-${Date.now()}.gif`));
|
|
3246
|
+
const frameCount = Math.max(1, Math.ceil((durationMs / 1000) * fps));
|
|
3247
|
+
const frameDir = fs.mkdtempSync(path.join(SURF_TMP, "surf-record-"));
|
|
3248
|
+
const framePaths = [];
|
|
3249
|
+
let trigger = null;
|
|
3250
|
+
|
|
3251
|
+
try {
|
|
3252
|
+
if (toolArgs.trigger !== undefined) {
|
|
3253
|
+
trigger = await runRecordTrigger(toolArgs.trigger);
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
const startedAt = Date.now();
|
|
3257
|
+
for (let i = 0; i < frameCount; i++) {
|
|
3258
|
+
const framePath = path.join(frameDir, `frame-${String(i).padStart(4, "0")}.png`);
|
|
3259
|
+
const response = await sendRequest("screenshot", {
|
|
3260
|
+
savePath: framePath,
|
|
3261
|
+
full: toolArgs.full,
|
|
3262
|
+
"max-size": toolArgs["max-size"],
|
|
3263
|
+
}, 30000);
|
|
3264
|
+
assertToolOk(response, `record frame ${i + 1}`);
|
|
3265
|
+
framePaths.push(framePath);
|
|
3266
|
+
|
|
3267
|
+
if (i < frameCount - 1) {
|
|
3268
|
+
const nextFrameAt = startedAt + Math.round(((i + 1) * durationMs) / frameCount);
|
|
3269
|
+
const waitMs = nextFrameAt - Date.now();
|
|
3270
|
+
if (waitMs > 0) await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
3275
|
+
const imageMagick = assembleRecordGif(framePaths, output, fps, rect);
|
|
3276
|
+
const result = { output, frames: framePaths.length, durationMs, fps, imageMagick, ...(trigger && { trigger }), ...(rect && { rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } }) };
|
|
3277
|
+
|
|
3278
|
+
if (wantJson) {
|
|
3279
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3280
|
+
} else {
|
|
3281
|
+
console.log(`Saved recording to ${output} (${result.frames} frames, ${durationMs}ms @ ${fps}fps)`);
|
|
3282
|
+
}
|
|
3283
|
+
} finally {
|
|
3284
|
+
fs.rmSync(frameDir, { recursive: true, force: true });
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
async function runRecordTrigger(trigger) {
|
|
3289
|
+
if (typeof trigger !== "string") throw new Error("trigger must be action:target");
|
|
3290
|
+
const separator = trigger.indexOf(":");
|
|
3291
|
+
if (separator === -1) throw new Error("trigger must be action:target");
|
|
3292
|
+
const action = trigger.slice(0, separator).trim();
|
|
3293
|
+
const target = trigger.slice(separator + 1).trim();
|
|
3294
|
+
if (!action || !target) throw new Error("trigger must be action:target");
|
|
3295
|
+
|
|
3296
|
+
if (action === "click") {
|
|
3297
|
+
const response = await sendRequest("click", { selector: target }, 30000);
|
|
3298
|
+
assertToolOk(response, "record trigger");
|
|
3299
|
+
return { action, selector: target };
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
if (action === "scroll") {
|
|
3303
|
+
let response;
|
|
3304
|
+
if (["up", "down", "left", "right"].includes(target)) {
|
|
3305
|
+
response = await sendRequest("scroll", { direction: target }, 30000);
|
|
3306
|
+
} else if (target === "top" || target === "bottom") {
|
|
3307
|
+
response = await sendRequest(`scroll.${target}`, {}, 30000);
|
|
3308
|
+
} else {
|
|
3309
|
+
response = await sendRequest("scroll.bottom", { selector: target }, 30000);
|
|
3310
|
+
}
|
|
3311
|
+
assertToolOk(response, "record trigger");
|
|
3312
|
+
return { action, target };
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
throw new Error("trigger action must be click or scroll");
|
|
3316
|
+
}
|
|
3317
|
+
|
|
2863
3318
|
const performAutoCapture = async () => {
|
|
2864
3319
|
const timestamp = Date.now();
|
|
2865
3320
|
const screenshotPath = path.join(SURF_TMP, `surf-error-${timestamp}.png`);
|
|
@@ -2896,61 +3351,82 @@ const performAutoCapture = async () => {
|
|
|
2896
3351
|
}
|
|
2897
3352
|
};
|
|
2898
3353
|
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
});
|
|
3354
|
+
if (finalTool === "record") {
|
|
3355
|
+
if (endpoint.kind === "remote") {
|
|
3356
|
+
console.error(`Error: record is not supported with remote endpoint ${endpoint.display}`);
|
|
3357
|
+
process.exit(1);
|
|
3358
|
+
}
|
|
3359
|
+
installBrowserLock(lockOptions, endpoint);
|
|
3360
|
+
runRecord()
|
|
3361
|
+
.then(() => process.exit(0))
|
|
3362
|
+
.catch((error) => {
|
|
3363
|
+
console.error("Error:", error && error.message ? error.message : String(error));
|
|
3364
|
+
process.exit(1);
|
|
3365
|
+
});
|
|
3366
|
+
return;
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
installBrowserLock(lockOptions, endpoint);
|
|
3370
|
+
let socket;
|
|
3371
|
+
let timeout;
|
|
2902
3372
|
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
3373
|
+
if (endpoint.kind === "remote") {
|
|
3374
|
+
socket = { end() {}, destroy() {} };
|
|
3375
|
+
const requestTimeout = resolveRequestDeadlineMs(tool, toolArgs);
|
|
3376
|
+
openClientTransport(endpoint, { requestTimeoutMs: requestTimeout })
|
|
3377
|
+
.then(async (transport) => {
|
|
3378
|
+
try {
|
|
3379
|
+
const response = await transport.request(request, requestTimeout, transferPlan);
|
|
3380
|
+
await handleResponse(response);
|
|
3381
|
+
} finally {
|
|
3382
|
+
await transport.close();
|
|
3383
|
+
}
|
|
3384
|
+
})
|
|
3385
|
+
.catch((error) => {
|
|
3386
|
+
console.error(`Error: ${error.message}`);
|
|
3387
|
+
process.exit(1);
|
|
3388
|
+
});
|
|
3389
|
+
return;
|
|
2908
3390
|
}
|
|
2909
|
-
|
|
3391
|
+
|
|
3392
|
+
socket = connectEndpoint(endpoint, () => {
|
|
3393
|
+
writeFrame(socket, request).catch((error) => socket.destroy(error));
|
|
3394
|
+
});
|
|
3395
|
+
|
|
3396
|
+
const requestTimeout = resolveRequestDeadlineMs(tool, options);
|
|
3397
|
+
timeout = setTimeout(() => {
|
|
2910
3398
|
console.error(`Error: Request timed out (${requestTimeout / 1000}s)`);
|
|
2911
3399
|
socket.destroy();
|
|
2912
3400
|
process.exit(1);
|
|
2913
3401
|
}, requestTimeout);
|
|
2914
3402
|
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
for (const line of lines) {
|
|
2923
|
-
if (!line.trim()) continue;
|
|
2924
|
-
try {
|
|
2925
|
-
const msg = JSON.parse(line);
|
|
2926
|
-
|
|
2927
|
-
if (msg.type === "extension_disconnected") {
|
|
2928
|
-
clearTimeout(timeout);
|
|
2929
|
-
console.error(msg.message);
|
|
2930
|
-
socket.end();
|
|
2931
|
-
process.exit(1);
|
|
2932
|
-
}
|
|
2933
|
-
|
|
2934
|
-
handleResponse(msg).catch((err) => {
|
|
2935
|
-
console.error("Handler error:", err.message);
|
|
2936
|
-
process.exit(1);
|
|
2937
|
-
});
|
|
2938
|
-
} catch (e) {
|
|
2939
|
-
console.error("Invalid JSON response:", line);
|
|
3403
|
+
const responseParser = createFrameParser({
|
|
3404
|
+
onFrame(msg) {
|
|
3405
|
+
if (msg.type === "extension_disconnected") {
|
|
3406
|
+
clearTimeout(timeout);
|
|
3407
|
+
console.error(msg.message);
|
|
3408
|
+
socket.end();
|
|
2940
3409
|
process.exit(1);
|
|
2941
3410
|
}
|
|
2942
|
-
|
|
3411
|
+
if (msg.id !== request.id) return;
|
|
3412
|
+
handleResponse(msg).catch((err) => {
|
|
3413
|
+
console.error("Handler error:", err.message);
|
|
3414
|
+
process.exit(1);
|
|
3415
|
+
});
|
|
3416
|
+
},
|
|
3417
|
+
onError(error) {
|
|
3418
|
+
clearTimeout(timeout);
|
|
3419
|
+
console.error("Invalid response frame:", error.message);
|
|
3420
|
+
socket.destroy();
|
|
3421
|
+
process.exit(1);
|
|
3422
|
+
},
|
|
2943
3423
|
});
|
|
2944
3424
|
|
|
3425
|
+
socket.on("data", (data) => responseParser.push(data));
|
|
3426
|
+
|
|
2945
3427
|
socket.on("error", (err) => {
|
|
2946
3428
|
clearTimeout(timeout);
|
|
2947
|
-
|
|
2948
|
-
console.error("Error: Socket not found. Is Chrome running with the extension?");
|
|
2949
|
-
} else if (err.code === "ECONNREFUSED") {
|
|
2950
|
-
console.error("Error: Connection refused. Native host not running.");
|
|
2951
|
-
} else {
|
|
2952
|
-
console.error("Error:", err.message);
|
|
2953
|
-
}
|
|
3429
|
+
console.error("Error:", formatEndpointError(err, endpoint, formatSocketError));
|
|
2954
3430
|
process.exit(1);
|
|
2955
3431
|
});
|
|
2956
3432
|
|
|
@@ -2979,7 +3455,7 @@ async function handleResponse(response) {
|
|
|
2979
3455
|
}
|
|
2980
3456
|
|
|
2981
3457
|
const result = response.result?.content?.[0]?.text;
|
|
2982
|
-
|
|
3458
|
+
|
|
2983
3459
|
let data;
|
|
2984
3460
|
try {
|
|
2985
3461
|
data = result ? JSON.parse(result) : response.result;
|
|
@@ -2991,6 +3467,17 @@ async function handleResponse(response) {
|
|
|
2991
3467
|
data = { response: data };
|
|
2992
3468
|
}
|
|
2993
3469
|
|
|
3470
|
+
if (tool === "perf-audit" && outputPath) {
|
|
3471
|
+
const saveTo = path.resolve(outputPath);
|
|
3472
|
+
fs.mkdirSync(path.dirname(saveTo), { recursive: true });
|
|
3473
|
+
fs.writeFileSync(saveTo, JSON.stringify(data ?? null, null, 2));
|
|
3474
|
+
if (!wantJson) {
|
|
3475
|
+
console.log(`Saved perf audit to ${saveTo}`);
|
|
3476
|
+
socket.end();
|
|
3477
|
+
process.exit(0);
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
|
|
2994
3481
|
if (wantJson) {
|
|
2995
3482
|
console.log(JSON.stringify(data ?? null, null, 2));
|
|
2996
3483
|
socket.end();
|
|
@@ -2998,14 +3485,14 @@ async function handleResponse(response) {
|
|
|
2998
3485
|
}
|
|
2999
3486
|
|
|
3000
3487
|
if (tool === "screenshot" && data?.base64 && (outputPath || toolArgs.savePath)) {
|
|
3001
|
-
const saveTo =
|
|
3488
|
+
const saveTo = transferPlan.downloads?.[0]?.destination || toolArgs.savePath || outputPath;
|
|
3002
3489
|
fs.writeFileSync(saveTo, Buffer.from(data.base64, "base64"));
|
|
3003
|
-
|
|
3490
|
+
|
|
3004
3491
|
const skipResize = options.full || toolArgs.full;
|
|
3005
3492
|
const maxSize = parseInt(options["max-size"] || toolArgs["max-size"] || "1200", 10);
|
|
3006
3493
|
const origWidth = data.width || 0;
|
|
3007
3494
|
const origHeight = data.height || 0;
|
|
3008
|
-
|
|
3495
|
+
|
|
3009
3496
|
if (!skipResize && (origWidth > maxSize || origHeight > maxSize)) {
|
|
3010
3497
|
const result = resizeImage(saveTo, maxSize);
|
|
3011
3498
|
if (result.success) {
|
|
@@ -3092,7 +3579,7 @@ async function handleResponse(response) {
|
|
|
3092
3579
|
} else if (tool === "smoke" && data?.results) {
|
|
3093
3580
|
const results = data.results;
|
|
3094
3581
|
const summary = data.summary || { pass: 0, fail: 0, total: results.length };
|
|
3095
|
-
|
|
3582
|
+
|
|
3096
3583
|
for (const r of results) {
|
|
3097
3584
|
const status = r.status === "pass" ? "PASS" : "FAIL";
|
|
3098
3585
|
const timeStr = r.time ? ` (${r.time}ms)` : "";
|
|
@@ -3104,10 +3591,10 @@ async function handleResponse(response) {
|
|
|
3104
3591
|
}
|
|
3105
3592
|
}
|
|
3106
3593
|
}
|
|
3107
|
-
|
|
3594
|
+
|
|
3108
3595
|
console.log("");
|
|
3109
3596
|
console.log(`Summary: ${summary.pass} passed, ${summary.fail} failed, ${summary.total} total`);
|
|
3110
|
-
|
|
3597
|
+
|
|
3111
3598
|
if (summary.fail > 0) {
|
|
3112
3599
|
socket.end();
|
|
3113
3600
|
process.exit(1);
|
|
@@ -3119,7 +3606,7 @@ async function handleResponse(response) {
|
|
|
3119
3606
|
} else if (tool === "network" && (data?.entries || data?.requests)) {
|
|
3120
3607
|
// Network list - handle both new (entries) and old (requests) formats
|
|
3121
3608
|
const items = data.entries || data.requests || [];
|
|
3122
|
-
|
|
3609
|
+
|
|
3123
3610
|
if (items.length === 0) {
|
|
3124
3611
|
console.log("No network requests captured");
|
|
3125
3612
|
} else if (data._format === 'raw') {
|