surf-cli 2.7.1 → 2.8.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 +110 -9
- package/native/browser-lock.cjs +169 -0
- package/native/chatgpt-client.cjs +394 -105
- package/native/cli.cjs +670 -279
- package/native/config.cjs +2 -2
- package/native/do-executor.cjs +2 -9
- package/native/do-parser.cjs +12 -0
- package/native/doctor.cjs +583 -0
- package/native/gemini-client.cjs +91 -20
- package/native/grok-client.cjs +270 -170
- package/native/host-helpers.cjs +51 -4
- package/native/host.cjs +22 -7
- package/native/mcp-server.cjs +10 -7
- package/native/socket-path.cjs +46 -0
- package/package.json +5 -5
- package/scripts/install-native-host.cjs +155 -53
- package/scripts/uninstall-native-host.cjs +93 -15
- package/skills/surf/SKILL.md +46 -18
- package/native/edited.png +0 -0
package/native/cli.cjs
CHANGED
|
@@ -3,7 +3,7 @@ const net = require("net");
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const os = require("os");
|
|
6
|
-
const { execSync } = require("child_process");
|
|
6
|
+
const { execFileSync, execSync } = require("child_process");
|
|
7
7
|
const { loadConfig, getConfigPath, createStarterConfig } = require("./config.cjs");
|
|
8
8
|
const networkFormatters = require("./formatters/network.cjs");
|
|
9
9
|
const networkStore = require("./network-store.cjs");
|
|
@@ -12,10 +12,52 @@ const { executeDoSteps } = require("./do-executor.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 { SOCKET_PATH, SURF_TMP, formatSocketError } = require("./socket-path.cjs");
|
|
16
|
+
const { acquireBrowserLock } = require("./browser-lock.cjs");
|
|
17
17
|
if (IS_WIN) { try { fs.mkdirSync(SURF_TMP, { recursive: true }); } catch {} }
|
|
18
18
|
|
|
19
|
+
function parseBrowserLockOptions(noLockFlag) {
|
|
20
|
+
const noLock = noLockFlag || process.env.SURF_NO_LOCK === "1" || process.env.SURF_NO_LOCK === "true";
|
|
21
|
+
let timeoutMs;
|
|
22
|
+
if (process.env.SURF_LOCK_TIMEOUT_MS !== undefined) {
|
|
23
|
+
timeoutMs = Number(process.env.SURF_LOCK_TIMEOUT_MS);
|
|
24
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
25
|
+
console.error("Error: SURF_LOCK_TIMEOUT_MS must be a non-negative number");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { noLock, timeoutMs };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function installBrowserLock({ noLock, timeoutMs }) {
|
|
33
|
+
let releaseBrowserLock = () => {};
|
|
34
|
+
if (!noLock) {
|
|
35
|
+
try {
|
|
36
|
+
const lock = acquireBrowserLock(SOCKET_PATH, SURF_TMP, { timeoutMs });
|
|
37
|
+
releaseBrowserLock = lock.release;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error("Error:", error && error.message ? error.message : String(error));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const release = () => {
|
|
45
|
+
const releaseCurrent = releaseBrowserLock;
|
|
46
|
+
releaseBrowserLock = () => {};
|
|
47
|
+
releaseCurrent();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
process.once("exit", release);
|
|
51
|
+
process.once("SIGINT", () => {
|
|
52
|
+
release();
|
|
53
|
+
process.exit(130);
|
|
54
|
+
});
|
|
55
|
+
process.once("SIGTERM", () => {
|
|
56
|
+
release();
|
|
57
|
+
process.exit(143);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
19
61
|
// ============================================================================
|
|
20
62
|
// Workflow Resolution and Management
|
|
21
63
|
// ============================================================================
|
|
@@ -41,7 +83,7 @@ function resolveWorkflow(nameOrPath) {
|
|
|
41
83
|
if (nameOrPath.includes('|')) {
|
|
42
84
|
return { type: 'inline', content: nameOrPath };
|
|
43
85
|
}
|
|
44
|
-
|
|
86
|
+
|
|
45
87
|
// Check if it's a direct file path (with extension or path separator)
|
|
46
88
|
if (nameOrPath.includes('/') || nameOrPath.includes('\\') || nameOrPath.endsWith('.json')) {
|
|
47
89
|
if (fs.existsSync(nameOrPath)) {
|
|
@@ -49,17 +91,17 @@ function resolveWorkflow(nameOrPath) {
|
|
|
49
91
|
}
|
|
50
92
|
return { type: 'not_found', name: nameOrPath };
|
|
51
93
|
}
|
|
52
|
-
|
|
94
|
+
|
|
53
95
|
// Look up by name in workflow directories
|
|
54
96
|
const searchDirs = getWorkflowDirs();
|
|
55
|
-
|
|
97
|
+
|
|
56
98
|
for (const { path: dir } of searchDirs) {
|
|
57
99
|
const filePath = path.join(dir, `${nameOrPath}.json`);
|
|
58
100
|
if (fs.existsSync(filePath)) {
|
|
59
101
|
return { type: 'file', path: filePath };
|
|
60
102
|
}
|
|
61
103
|
}
|
|
62
|
-
|
|
104
|
+
|
|
63
105
|
return { type: 'not_found', name: nameOrPath };
|
|
64
106
|
}
|
|
65
107
|
|
|
@@ -70,7 +112,7 @@ function resolveWorkflow(nameOrPath) {
|
|
|
70
112
|
function listWorkflows() {
|
|
71
113
|
const workflows = [];
|
|
72
114
|
const searchDirs = getWorkflowDirs();
|
|
73
|
-
|
|
115
|
+
|
|
74
116
|
for (const { path: dir, scope } of searchDirs) {
|
|
75
117
|
if (fs.existsSync(dir)) {
|
|
76
118
|
try {
|
|
@@ -96,7 +138,7 @@ function listWorkflows() {
|
|
|
96
138
|
}
|
|
97
139
|
}
|
|
98
140
|
}
|
|
99
|
-
|
|
141
|
+
|
|
100
142
|
return workflows;
|
|
101
143
|
}
|
|
102
144
|
|
|
@@ -107,15 +149,15 @@ function listWorkflows() {
|
|
|
107
149
|
*/
|
|
108
150
|
function getWorkflowInfo(name) {
|
|
109
151
|
const resolved = resolveWorkflow(name);
|
|
110
|
-
|
|
152
|
+
|
|
111
153
|
if (resolved.type === 'not_found') {
|
|
112
154
|
return { error: `Workflow not found: ${name}` };
|
|
113
155
|
}
|
|
114
|
-
|
|
156
|
+
|
|
115
157
|
if (resolved.type === 'inline') {
|
|
116
158
|
return { error: 'Cannot get info for inline workflows' };
|
|
117
159
|
}
|
|
118
|
-
|
|
160
|
+
|
|
119
161
|
try {
|
|
120
162
|
const content = JSON.parse(fs.readFileSync(resolved.path, 'utf8'));
|
|
121
163
|
return {
|
|
@@ -175,24 +217,24 @@ function validateWorkflowFile(filePath) {
|
|
|
175
217
|
if (!fs.existsSync(filePath)) {
|
|
176
218
|
return { valid: false, error: `File not found: ${filePath}` };
|
|
177
219
|
}
|
|
178
|
-
|
|
220
|
+
|
|
179
221
|
try {
|
|
180
222
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
181
223
|
const workflow = JSON.parse(content);
|
|
182
|
-
|
|
224
|
+
|
|
183
225
|
// Basic structure validation
|
|
184
226
|
if (!workflow.steps || !Array.isArray(workflow.steps)) {
|
|
185
227
|
return { valid: false, error: "Workflow must have a 'steps' array" };
|
|
186
228
|
}
|
|
187
|
-
|
|
229
|
+
|
|
188
230
|
if (workflow.steps.length === 0) {
|
|
189
231
|
return { valid: false, error: "Workflow has no steps" };
|
|
190
232
|
}
|
|
191
|
-
|
|
233
|
+
|
|
192
234
|
// Validate each step
|
|
193
235
|
for (let i = 0; i < workflow.steps.length; i++) {
|
|
194
236
|
const step = workflow.steps[i];
|
|
195
|
-
|
|
237
|
+
|
|
196
238
|
// Check for loops
|
|
197
239
|
if (step.repeat !== undefined || step.each !== undefined) {
|
|
198
240
|
if (!step.steps || !Array.isArray(step.steps)) {
|
|
@@ -200,18 +242,18 @@ function validateWorkflowFile(filePath) {
|
|
|
200
242
|
}
|
|
201
243
|
continue;
|
|
202
244
|
}
|
|
203
|
-
|
|
245
|
+
|
|
204
246
|
// Regular step must have tool/cmd
|
|
205
247
|
if (!step.tool && !step.cmd) {
|
|
206
248
|
return { valid: false, error: `Step ${i + 1}: must have 'tool' field` };
|
|
207
249
|
}
|
|
208
250
|
}
|
|
209
|
-
|
|
251
|
+
|
|
210
252
|
// Validate args schema if present
|
|
211
253
|
if (workflow.args && typeof workflow.args !== 'object') {
|
|
212
254
|
return { valid: false, error: "'args' must be an object" };
|
|
213
255
|
}
|
|
214
|
-
|
|
256
|
+
|
|
215
257
|
return { valid: true, workflow };
|
|
216
258
|
} catch (e) {
|
|
217
259
|
return { valid: false, error: `Invalid JSON: ${e.message}` };
|
|
@@ -226,7 +268,7 @@ function validateWorkflowFile(filePath) {
|
|
|
226
268
|
*/
|
|
227
269
|
function formatStep(step, indent = 0) {
|
|
228
270
|
const pad = ' '.repeat(indent);
|
|
229
|
-
|
|
271
|
+
|
|
230
272
|
if (step.repeat !== undefined) {
|
|
231
273
|
const lines = [`${pad}repeat ${step.repeat} times:`];
|
|
232
274
|
for (const s of step.steps || []) {
|
|
@@ -237,7 +279,7 @@ function formatStep(step, indent = 0) {
|
|
|
237
279
|
}
|
|
238
280
|
return lines.join('\n');
|
|
239
281
|
}
|
|
240
|
-
|
|
282
|
+
|
|
241
283
|
if (step.each !== undefined) {
|
|
242
284
|
const lines = [`${pad}each ${step.each} as ${step.as || 'item'}:`];
|
|
243
285
|
for (const s of step.steps || []) {
|
|
@@ -245,24 +287,24 @@ function formatStep(step, indent = 0) {
|
|
|
245
287
|
}
|
|
246
288
|
return lines.join('\n');
|
|
247
289
|
}
|
|
248
|
-
|
|
290
|
+
|
|
249
291
|
const tool = step.tool || step.cmd;
|
|
250
292
|
const args = step.args || {};
|
|
251
293
|
const argStr = Object.entries(args)
|
|
252
294
|
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
253
295
|
.join(' ');
|
|
254
|
-
|
|
296
|
+
|
|
255
297
|
let line = `${pad}${tool}`;
|
|
256
298
|
if (argStr) line += ` ${argStr}`;
|
|
257
299
|
if (step.as) line += ` → ${step.as}`;
|
|
258
|
-
|
|
300
|
+
|
|
259
301
|
return line;
|
|
260
302
|
}
|
|
261
303
|
|
|
262
304
|
// Cross-platform image resize (macOS: sips, Linux: ImageMagick)
|
|
263
305
|
function resizeImage(filePath, maxSize) {
|
|
264
306
|
const platform = process.platform;
|
|
265
|
-
|
|
307
|
+
|
|
266
308
|
try {
|
|
267
309
|
if (platform === "darwin") {
|
|
268
310
|
// macOS: use sips
|
|
@@ -342,10 +384,10 @@ const TOOLS = {
|
|
|
342
384
|
ai: {
|
|
343
385
|
desc: "AI assistants (ChatGPT, Gemini)",
|
|
344
386
|
commands: {
|
|
345
|
-
"chatgpt": {
|
|
346
|
-
desc: "Send prompt to ChatGPT (uses browser cookies)",
|
|
347
|
-
args: ["query"],
|
|
348
|
-
opts: {
|
|
387
|
+
"chatgpt": {
|
|
388
|
+
desc: "Send prompt to ChatGPT (uses browser cookies)",
|
|
389
|
+
args: ["query"],
|
|
390
|
+
opts: {
|
|
349
391
|
"with-page": "Include current page context",
|
|
350
392
|
model: "Model: gpt-4o, o1, etc.",
|
|
351
393
|
file: "Attach file",
|
|
@@ -358,10 +400,10 @@ const TOOLS = {
|
|
|
358
400
|
{ cmd: 'chatgpt "analyze" --model gpt-4o', desc: "Specify model" },
|
|
359
401
|
]
|
|
360
402
|
},
|
|
361
|
-
"gemini": {
|
|
362
|
-
desc: "Send prompt to Gemini (uses browser cookies)",
|
|
363
|
-
args: ["query"],
|
|
364
|
-
opts: {
|
|
403
|
+
"gemini": {
|
|
404
|
+
desc: "Send prompt to Gemini (uses browser cookies)",
|
|
405
|
+
args: ["query"],
|
|
406
|
+
opts: {
|
|
365
407
|
"with-page": "Include current page context",
|
|
366
408
|
model: "Model: gemini-3-pro (default), gemini-2.5-pro, gemini-2.5-flash",
|
|
367
409
|
file: "Attach file to analyze",
|
|
@@ -402,9 +444,9 @@ const TOOLS = {
|
|
|
402
444
|
args: ["query"],
|
|
403
445
|
opts: {
|
|
404
446
|
"with-page": "Include current page context",
|
|
405
|
-
model: "Model: auto, fast, expert,
|
|
447
|
+
model: "Model: auto, fast (default), expert, grok-4.20-beta",
|
|
406
448
|
"deep-search": "Enable DeepSearch for X post searching",
|
|
407
|
-
timeout: "Timeout in seconds (default: 300
|
|
449
|
+
timeout: "Timeout in seconds (default: 300)",
|
|
408
450
|
validate: "Check Grok UI and scrape available models (no query sent)",
|
|
409
451
|
"save-models": "Save discovered models to surf.json config"
|
|
410
452
|
},
|
|
@@ -447,9 +489,9 @@ const TOOLS = {
|
|
|
447
489
|
{ cmd: 'aistudio.build "crm dashboard" --output ./out', desc: "Build and extract to directory" },
|
|
448
490
|
]
|
|
449
491
|
},
|
|
450
|
-
"ai": {
|
|
451
|
-
desc: "Analyze page with AI (requires GOOGLE_API_KEY)",
|
|
452
|
-
args: ["query"],
|
|
492
|
+
"ai": {
|
|
493
|
+
desc: "Analyze page with AI (requires GOOGLE_API_KEY)",
|
|
494
|
+
args: ["query"],
|
|
453
495
|
opts: { mode: "Query mode: find|summary|extract (auto-detected)" },
|
|
454
496
|
examples: [
|
|
455
497
|
{ cmd: 'ai "find the login button"', desc: "Find element" },
|
|
@@ -463,39 +505,39 @@ const TOOLS = {
|
|
|
463
505
|
desc: "Tab management",
|
|
464
506
|
commands: {
|
|
465
507
|
"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"],
|
|
508
|
+
"tab.new": {
|
|
509
|
+
desc: "Open new tab",
|
|
510
|
+
args: ["url"],
|
|
469
511
|
opts: { urls: "Open multiple URLs" },
|
|
470
512
|
examples: [
|
|
471
513
|
{ cmd: 'tab.new "https://google.com"', desc: "Open single tab" },
|
|
472
514
|
{ cmd: 'tab.new --urls "https://a.com" "https://b.com"', desc: "Open multiple" },
|
|
473
515
|
]
|
|
474
516
|
},
|
|
475
|
-
"tab.switch": {
|
|
476
|
-
desc: "Switch to tab by ID or name",
|
|
517
|
+
"tab.switch": {
|
|
518
|
+
desc: "Switch to tab by ID or name",
|
|
477
519
|
args: ["id"],
|
|
478
520
|
examples: [
|
|
479
521
|
{ cmd: "tab.switch 123", desc: "Switch by ID" },
|
|
480
522
|
{ cmd: 'tab.switch "myTab"', desc: "Switch by name" },
|
|
481
523
|
]
|
|
482
524
|
},
|
|
483
|
-
"tab.close": {
|
|
484
|
-
desc: "Close tab by ID or name",
|
|
485
|
-
args: ["id"],
|
|
525
|
+
"tab.close": {
|
|
526
|
+
desc: "Close tab by ID or name",
|
|
527
|
+
args: ["id"],
|
|
486
528
|
opts: { ids: "Close multiple tabs" },
|
|
487
529
|
examples: [{ cmd: "tab.close 123", desc: "Close tab" }]
|
|
488
530
|
},
|
|
489
|
-
"tab.name": {
|
|
490
|
-
desc: "Register current tab with a name",
|
|
531
|
+
"tab.name": {
|
|
532
|
+
desc: "Register current tab with a name",
|
|
491
533
|
args: ["name"],
|
|
492
534
|
examples: [{ cmd: 'tab.name "dashboard"', desc: "Name current tab" }]
|
|
493
535
|
},
|
|
494
536
|
"tab.unname": { desc: "Unregister a named tab", args: ["name"] },
|
|
495
537
|
"tab.named": { desc: "List all named tabs", args: [] },
|
|
496
|
-
"tab.group": {
|
|
497
|
-
desc: "Create/add to tab group",
|
|
498
|
-
args: [],
|
|
538
|
+
"tab.group": {
|
|
539
|
+
desc: "Create/add to tab group",
|
|
540
|
+
args: [],
|
|
499
541
|
opts: { name: "Group name", tabs: "Tab IDs (comma-separated)", color: "Group color" },
|
|
500
542
|
examples: [
|
|
501
543
|
{ cmd: 'tab.group --name "Work" --color blue', desc: "Group current tab" },
|
|
@@ -504,9 +546,9 @@ const TOOLS = {
|
|
|
504
546
|
},
|
|
505
547
|
"tab.ungroup": { desc: "Remove tabs from group", args: [], opts: { tabs: "Tab IDs (comma-separated)" } },
|
|
506
548
|
"tab.groups": { desc: "List all tab groups", args: [] },
|
|
507
|
-
"tab.reload": {
|
|
508
|
-
desc: "Reload current tab",
|
|
509
|
-
args: [],
|
|
549
|
+
"tab.reload": {
|
|
550
|
+
desc: "Reload current tab",
|
|
551
|
+
args: [],
|
|
510
552
|
opts: { hard: "Bypass cache" },
|
|
511
553
|
examples: [
|
|
512
554
|
{ cmd: "tab.reload", desc: "Soft reload" },
|
|
@@ -518,30 +560,31 @@ const TOOLS = {
|
|
|
518
560
|
nav: {
|
|
519
561
|
desc: "Navigation",
|
|
520
562
|
commands: {
|
|
521
|
-
"navigate": {
|
|
522
|
-
desc: "Go to URL",
|
|
563
|
+
"navigate": {
|
|
564
|
+
desc: "Go to URL",
|
|
523
565
|
args: ["url"],
|
|
524
566
|
examples: [{ cmd: 'navigate "https://example.com"', desc: "Go to URL" }]
|
|
525
567
|
},
|
|
526
568
|
"go": { desc: "Alias for navigate", args: ["url"], alias: "navigate" },
|
|
527
|
-
"back": {
|
|
528
|
-
desc: "Go back in history",
|
|
569
|
+
"back": {
|
|
570
|
+
desc: "Go back in history",
|
|
529
571
|
args: [],
|
|
530
572
|
examples: [{ cmd: "back", desc: "Browser back" }]
|
|
531
573
|
},
|
|
532
|
-
"forward": {
|
|
533
|
-
desc: "Go forward in history",
|
|
574
|
+
"forward": {
|
|
575
|
+
desc: "Go forward in history",
|
|
534
576
|
args: [],
|
|
535
577
|
examples: [{ cmd: "forward", desc: "Browser forward" }]
|
|
536
578
|
},
|
|
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",
|
|
579
|
+
"screenshot": {
|
|
580
|
+
desc: "Capture screenshot (auto-saves to /tmp by default)",
|
|
581
|
+
args: [],
|
|
582
|
+
opts: {
|
|
583
|
+
output: "Save to file",
|
|
584
|
+
selector: "Capture specific element",
|
|
585
|
+
annotate: "Draw element labels",
|
|
586
|
+
fullpage: "Capture full page",
|
|
587
|
+
"full-page": "Capture full page (alias for --fullpage)",
|
|
545
588
|
"max-height": "Max height for fullpage (default: 4000)",
|
|
546
589
|
full: "Skip resize, save at full resolution",
|
|
547
590
|
"max-size": "Max dimension in px (default: 1200)",
|
|
@@ -555,23 +598,65 @@ const TOOLS = {
|
|
|
555
598
|
{ cmd: "snap", desc: "Alias for screenshot" },
|
|
556
599
|
]
|
|
557
600
|
},
|
|
601
|
+
"record": {
|
|
602
|
+
desc: "Capture screenshot frames over time and assemble an animated GIF",
|
|
603
|
+
args: [],
|
|
604
|
+
opts: {
|
|
605
|
+
output: "GIF output path (default: /tmp/surf-record-*.gif)",
|
|
606
|
+
duration: "Capture duration in ms (default: 2000, max: 10000)",
|
|
607
|
+
fps: "Frames per second (default: 10, max: 30)",
|
|
608
|
+
trigger: "Optional action before capture: click:<selector> or scroll:<target>",
|
|
609
|
+
rect: "Crop rectangle x,y,width,height"
|
|
610
|
+
},
|
|
611
|
+
examples: [
|
|
612
|
+
{ cmd: "record --duration 2000 --fps 10 --output /tmp/anim.gif", desc: "Record a 2s GIF" },
|
|
613
|
+
{ cmd: 'record --trigger "click:#btn" --output /tmp/click.gif', desc: "Click, then record" },
|
|
614
|
+
]
|
|
615
|
+
},
|
|
616
|
+
"animate-audit": {
|
|
617
|
+
desc: "Sample matching elements over time and return a JSON animation timeline",
|
|
618
|
+
args: [],
|
|
619
|
+
opts: {
|
|
620
|
+
selector: "CSS selector to sample (required)",
|
|
621
|
+
duration: "Capture duration in ms (default: 2000, max: 10000)",
|
|
622
|
+
fps: "Samples per second (default: 10, max: 30)"
|
|
623
|
+
},
|
|
624
|
+
examples: [
|
|
625
|
+
{ cmd: 'animate-audit --selector ".thing" --duration 2000 --fps 10', desc: "Capture a bounded JSON timeline" },
|
|
626
|
+
]
|
|
627
|
+
},
|
|
628
|
+
"perf-audit": {
|
|
629
|
+
desc: "Capture layout shift, event, long task, and animation-frame performance entries",
|
|
630
|
+
args: [],
|
|
631
|
+
opts: {
|
|
632
|
+
duration: "Capture duration in ms (default: 3000, max: 10000)",
|
|
633
|
+
trigger: "Optional action before capture: click:<selector> or scroll:<target>",
|
|
634
|
+
output: "Save JSON to file"
|
|
635
|
+
},
|
|
636
|
+
examples: [
|
|
637
|
+
{ cmd: 'perf-audit --duration 3000 --trigger "click:.cta" --output /tmp/perf.json', desc: "Capture a performance snapshot" },
|
|
638
|
+
]
|
|
639
|
+
},
|
|
558
640
|
"snap": { desc: "Alias for screenshot (auto-saves to /tmp)", args: [], alias: "screenshot" },
|
|
559
641
|
}
|
|
560
642
|
},
|
|
561
643
|
scroll: {
|
|
562
644
|
desc: "Scrolling",
|
|
563
645
|
commands: {
|
|
564
|
-
"scroll": {
|
|
565
|
-
desc: "Scroll in direction",
|
|
566
|
-
args: [],
|
|
646
|
+
"scroll": {
|
|
647
|
+
desc: "Scroll in direction",
|
|
648
|
+
args: ["direction", "pixels"],
|
|
567
649
|
opts: { direction: "up|down|left|right", amount: "Scroll amount (1-10)" },
|
|
568
|
-
examples: [
|
|
650
|
+
examples: [
|
|
651
|
+
{ cmd: "scroll down 800", desc: "Scroll down 800px" },
|
|
652
|
+
{ cmd: "scroll --direction down --amount 3", desc: "Scroll down" },
|
|
653
|
+
]
|
|
569
654
|
},
|
|
570
655
|
"scroll.top": { desc: "Scroll to top of page", args: [], opts: { selector: "Target specific container" } },
|
|
571
656
|
"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: [],
|
|
657
|
+
"scroll.to": {
|
|
658
|
+
desc: "Scroll element into view",
|
|
659
|
+
args: [],
|
|
575
660
|
opts: { ref: "Element ref" },
|
|
576
661
|
examples: [{ cmd: "scroll.to --ref e5", desc: "Scroll to element" }]
|
|
577
662
|
},
|
|
@@ -581,12 +666,12 @@ const TOOLS = {
|
|
|
581
666
|
page: {
|
|
582
667
|
desc: "Page inspection",
|
|
583
668
|
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",
|
|
669
|
+
"page.read": {
|
|
670
|
+
desc: "Get accessibility tree + visible text",
|
|
671
|
+
args: [],
|
|
672
|
+
opts: {
|
|
673
|
+
all: "Include all elements",
|
|
674
|
+
ref: "Get specific element",
|
|
590
675
|
"no-text": "Exclude visible text content",
|
|
591
676
|
depth: "Maximum tree depth (default: unlimited)",
|
|
592
677
|
compact: "Remove empty structural elements",
|
|
@@ -612,7 +697,7 @@ const TOOLS = {
|
|
|
612
697
|
"locate.role": {
|
|
613
698
|
desc: "Find element by ARIA role",
|
|
614
699
|
args: ["role"],
|
|
615
|
-
opts: {
|
|
700
|
+
opts: {
|
|
616
701
|
name: "Element name/text",
|
|
617
702
|
action: "Action to perform (click|fill|hover|text)",
|
|
618
703
|
value: "Value for fill action",
|
|
@@ -684,14 +769,14 @@ const TOOLS = {
|
|
|
684
769
|
wait: {
|
|
685
770
|
desc: "Waiting",
|
|
686
771
|
commands: {
|
|
687
|
-
"wait": {
|
|
688
|
-
desc: "Wait N seconds",
|
|
772
|
+
"wait": {
|
|
773
|
+
desc: "Wait N seconds",
|
|
689
774
|
args: ["duration"],
|
|
690
775
|
examples: [{ cmd: "wait 2", desc: "Wait 2 seconds" }]
|
|
691
776
|
},
|
|
692
|
-
"wait.element": {
|
|
693
|
-
desc: "Wait for element to appear",
|
|
694
|
-
args: ["selector"],
|
|
777
|
+
"wait.element": {
|
|
778
|
+
desc: "Wait for element to appear",
|
|
779
|
+
args: ["selector"],
|
|
695
780
|
opts: { timeout: "Timeout in ms" },
|
|
696
781
|
examples: [
|
|
697
782
|
{ cmd: 'wait.element ".loading"', desc: "Wait for element" },
|
|
@@ -699,9 +784,9 @@ const TOOLS = {
|
|
|
699
784
|
]
|
|
700
785
|
},
|
|
701
786
|
"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"],
|
|
787
|
+
"wait.url": {
|
|
788
|
+
desc: "Wait for URL to match",
|
|
789
|
+
args: ["pattern"],
|
|
705
790
|
opts: { timeout: "Timeout in ms" },
|
|
706
791
|
examples: [{ cmd: 'wait.url "/dashboard"', desc: "Wait for URL pattern" }]
|
|
707
792
|
},
|
|
@@ -712,15 +797,15 @@ const TOOLS = {
|
|
|
712
797
|
input: {
|
|
713
798
|
desc: "Input actions",
|
|
714
799
|
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",
|
|
800
|
+
"click": {
|
|
801
|
+
desc: "Click element or coordinates",
|
|
802
|
+
args: ["ref"],
|
|
803
|
+
opts: {
|
|
804
|
+
ref: "Element ref",
|
|
805
|
+
x: "X coordinate",
|
|
806
|
+
y: "Y coordinate",
|
|
807
|
+
button: "left|right|double|triple",
|
|
808
|
+
selector: "CSS selector",
|
|
724
809
|
index: "Which match (0-indexed) for selector",
|
|
725
810
|
},
|
|
726
811
|
examples: [
|
|
@@ -730,15 +815,15 @@ const TOOLS = {
|
|
|
730
815
|
{ cmd: "click --x 100 --y 200", desc: "Click coordinates" },
|
|
731
816
|
]
|
|
732
817
|
},
|
|
733
|
-
"type": {
|
|
734
|
-
desc: "Type text (uses form.fill when --ref provided for better modal/form support)",
|
|
735
|
-
args: ["text"],
|
|
736
|
-
opts: {
|
|
818
|
+
"type": {
|
|
819
|
+
desc: "Type text (uses form.fill when --ref provided for better modal/form support)",
|
|
820
|
+
args: ["text"],
|
|
821
|
+
opts: {
|
|
737
822
|
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 (default: cdp, but ref uses JS automatically)"
|
|
823
|
+
ref: "Element ref (uses JS DOM method, more reliable for modals)",
|
|
824
|
+
submit: "Press enter after",
|
|
825
|
+
clear: "Clear first",
|
|
826
|
+
method: "cdp|js (default: cdp, but ref uses JS automatically)"
|
|
742
827
|
},
|
|
743
828
|
examples: [
|
|
744
829
|
{ cmd: 'type "hello world"', desc: "Type at cursor (CDP events)" },
|
|
@@ -747,9 +832,9 @@ const TOOLS = {
|
|
|
747
832
|
]
|
|
748
833
|
},
|
|
749
834
|
"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"],
|
|
835
|
+
"key": {
|
|
836
|
+
desc: "Press key",
|
|
837
|
+
args: ["key"],
|
|
753
838
|
examples: [
|
|
754
839
|
{ cmd: "key Enter", desc: "Press Enter" },
|
|
755
840
|
{ cmd: "key Escape", desc: "Press Escape" },
|
|
@@ -764,9 +849,9 @@ const TOOLS = {
|
|
|
764
849
|
js: {
|
|
765
850
|
desc: "JavaScript execution",
|
|
766
851
|
commands: {
|
|
767
|
-
"js": {
|
|
768
|
-
desc: "Execute JavaScript (use 'return' for values)",
|
|
769
|
-
args: ["code"],
|
|
852
|
+
"js": {
|
|
853
|
+
desc: "Execute JavaScript (use 'return' for values)",
|
|
854
|
+
args: ["code"],
|
|
770
855
|
opts: { file: "Run JS from file" },
|
|
771
856
|
examples: [
|
|
772
857
|
{ cmd: 'js "return document.title"', desc: "Get title" },
|
|
@@ -779,9 +864,9 @@ const TOOLS = {
|
|
|
779
864
|
dev: {
|
|
780
865
|
desc: "Dev tools",
|
|
781
866
|
commands: {
|
|
782
|
-
"console": {
|
|
783
|
-
desc: "Read console messages",
|
|
784
|
-
args: [],
|
|
867
|
+
"console": {
|
|
868
|
+
desc: "Read console messages",
|
|
869
|
+
args: [],
|
|
785
870
|
opts: { clear: "Clear after reading", stream: "Continuous output", level: "Filter by level (log,warn,error)", limit: "Max messages" },
|
|
786
871
|
examples: [
|
|
787
872
|
{ cmd: "console", desc: "Get recent messages" },
|
|
@@ -794,10 +879,10 @@ const TOOLS = {
|
|
|
794
879
|
network: {
|
|
795
880
|
desc: "Network capture",
|
|
796
881
|
commands: {
|
|
797
|
-
"network": {
|
|
798
|
-
desc: "List captured network requests",
|
|
799
|
-
args: [],
|
|
800
|
-
opts: {
|
|
882
|
+
"network": {
|
|
883
|
+
desc: "List captured network requests",
|
|
884
|
+
args: [],
|
|
885
|
+
opts: {
|
|
801
886
|
origin: "Filter by origin (domain)",
|
|
802
887
|
method: "Filter by method (GET,POST,...)",
|
|
803
888
|
status: "Filter by status (200, 4xx, 5xx)",
|
|
@@ -822,16 +907,16 @@ const TOOLS = {
|
|
|
822
907
|
{ cmd: "network -v", desc: "Verbose with headers" },
|
|
823
908
|
]
|
|
824
909
|
},
|
|
825
|
-
"network.get": {
|
|
826
|
-
desc: "Get full details for a request",
|
|
910
|
+
"network.get": {
|
|
911
|
+
desc: "Get full details for a request",
|
|
827
912
|
args: ["id"],
|
|
828
913
|
opts: {},
|
|
829
914
|
examples: [
|
|
830
915
|
{ cmd: "network.get r_001", desc: "Get request details" }
|
|
831
916
|
]
|
|
832
917
|
},
|
|
833
|
-
"network.body": {
|
|
834
|
-
desc: "Get response body (for piping)",
|
|
918
|
+
"network.body": {
|
|
919
|
+
desc: "Get response body (for piping)",
|
|
835
920
|
args: ["id"],
|
|
836
921
|
opts: { request: "Get request body instead" },
|
|
837
922
|
examples: [
|
|
@@ -839,24 +924,24 @@ const TOOLS = {
|
|
|
839
924
|
{ cmd: "network.body r_001 | jq .", desc: "Pipe JSON to jq" }
|
|
840
925
|
]
|
|
841
926
|
},
|
|
842
|
-
"network.curl": {
|
|
843
|
-
desc: "Generate curl command for request",
|
|
927
|
+
"network.curl": {
|
|
928
|
+
desc: "Generate curl command for request",
|
|
844
929
|
args: ["id"],
|
|
845
930
|
opts: {},
|
|
846
931
|
examples: [
|
|
847
932
|
{ cmd: "network.curl r_001", desc: "Generate curl" }
|
|
848
933
|
]
|
|
849
934
|
},
|
|
850
|
-
"network.origins": {
|
|
851
|
-
desc: "List captured origins with stats",
|
|
935
|
+
"network.origins": {
|
|
936
|
+
desc: "List captured origins with stats",
|
|
852
937
|
args: [],
|
|
853
938
|
opts: { "by-tab": "Group by tab" },
|
|
854
939
|
examples: [
|
|
855
940
|
{ cmd: "network.origins", desc: "List origins" }
|
|
856
941
|
]
|
|
857
942
|
},
|
|
858
|
-
"network.clear": {
|
|
859
|
-
desc: "Clear captured requests",
|
|
943
|
+
"network.clear": {
|
|
944
|
+
desc: "Clear captured requests",
|
|
860
945
|
args: [],
|
|
861
946
|
opts: { before: "Clear before timestamp/duration", origin: "Clear specific origin" },
|
|
862
947
|
examples: [
|
|
@@ -864,24 +949,24 @@ const TOOLS = {
|
|
|
864
949
|
{ cmd: "network.clear --before 1h", desc: "Clear older than 1 hour" }
|
|
865
950
|
]
|
|
866
951
|
},
|
|
867
|
-
"network.stats": {
|
|
868
|
-
desc: "Show capture statistics",
|
|
952
|
+
"network.stats": {
|
|
953
|
+
desc: "Show capture statistics",
|
|
869
954
|
args: [],
|
|
870
955
|
opts: {},
|
|
871
956
|
examples: [
|
|
872
957
|
{ cmd: "network.stats", desc: "Show stats" }
|
|
873
958
|
]
|
|
874
959
|
},
|
|
875
|
-
"network.export": {
|
|
876
|
-
desc: "Export captured requests",
|
|
960
|
+
"network.export": {
|
|
961
|
+
desc: "Export captured requests",
|
|
877
962
|
args: [],
|
|
878
963
|
opts: { jsonl: "Export as JSONL", output: "Output file path" },
|
|
879
964
|
examples: [
|
|
880
965
|
{ cmd: "network.export --jsonl --output /tmp/requests.jsonl", desc: "Export as JSONL" }
|
|
881
966
|
]
|
|
882
967
|
},
|
|
883
|
-
"network.path": {
|
|
884
|
-
desc: "Get file paths for request data",
|
|
968
|
+
"network.path": {
|
|
969
|
+
desc: "Get file paths for request data",
|
|
885
970
|
args: ["id"],
|
|
886
971
|
opts: {},
|
|
887
972
|
examples: [
|
|
@@ -893,9 +978,19 @@ const TOOLS = {
|
|
|
893
978
|
health: {
|
|
894
979
|
desc: "Health checks",
|
|
895
980
|
commands: {
|
|
896
|
-
"
|
|
897
|
-
desc: "
|
|
898
|
-
args: [],
|
|
981
|
+
"doctor": {
|
|
982
|
+
desc: "Diagnose native host manifests and socket connectivity",
|
|
983
|
+
args: [],
|
|
984
|
+
opts: { browser: "Browser to inspect (default: chrome)", target: "auto|linux|windows", socket: "Socket path to check", json: "Raw diagnostic JSON" },
|
|
985
|
+
examples: [
|
|
986
|
+
{ cmd: "doctor", desc: "Check default Chrome setup" },
|
|
987
|
+
{ cmd: "doctor --browser all", desc: "Check all supported browsers" },
|
|
988
|
+
{ cmd: "doctor --json", desc: "Machine-readable diagnostics" },
|
|
989
|
+
]
|
|
990
|
+
},
|
|
991
|
+
"health": {
|
|
992
|
+
desc: "Wait for URL or element",
|
|
993
|
+
args: [],
|
|
899
994
|
opts: { url: "URL to check (expects 200)", selector: "CSS selector to wait for", expect: "Expected status code (default: 200)", timeout: "Timeout in ms" },
|
|
900
995
|
examples: [
|
|
901
996
|
{ cmd: 'health --url "https://api.example.com"', desc: "Check URL" },
|
|
@@ -914,9 +1009,9 @@ const TOOLS = {
|
|
|
914
1009
|
desc: "Browser dialog handling",
|
|
915
1010
|
commands: {
|
|
916
1011
|
"dialog.accept": { desc: "Accept current dialog", args: [], opts: { text: "Text for prompt input" } },
|
|
917
|
-
"dialog.dismiss": {
|
|
918
|
-
desc: "Dismiss current dialog",
|
|
919
|
-
args: [],
|
|
1012
|
+
"dialog.dismiss": {
|
|
1013
|
+
desc: "Dismiss current dialog",
|
|
1014
|
+
args: [],
|
|
920
1015
|
opts: { all: "Dismiss all dialogs repeatedly" },
|
|
921
1016
|
examples: [
|
|
922
1017
|
{ cmd: "dialog.dismiss", desc: "Dismiss once" },
|
|
@@ -980,9 +1075,9 @@ const TOOLS = {
|
|
|
980
1075
|
upload: {
|
|
981
1076
|
desc: "File upload",
|
|
982
1077
|
commands: {
|
|
983
|
-
"upload": {
|
|
984
|
-
desc: "Upload file(s) to input",
|
|
985
|
-
args: [],
|
|
1078
|
+
"upload": {
|
|
1079
|
+
desc: "Upload file(s) to input",
|
|
1080
|
+
args: [],
|
|
986
1081
|
opts: { ref: "Element ref", files: "File path(s) comma-separated" },
|
|
987
1082
|
examples: [{ cmd: 'upload --ref e5 --files "/path/to/file.pdf"', desc: "Upload file" }]
|
|
988
1083
|
},
|
|
@@ -991,8 +1086,8 @@ const TOOLS = {
|
|
|
991
1086
|
frame: {
|
|
992
1087
|
desc: "Iframe handling",
|
|
993
1088
|
commands: {
|
|
994
|
-
"frame.list": {
|
|
995
|
-
desc: "List all frames in page",
|
|
1089
|
+
"frame.list": {
|
|
1090
|
+
desc: "List all frames in page",
|
|
996
1091
|
args: [],
|
|
997
1092
|
examples: [{ cmd: "frame.list", desc: "Show frame tree" }]
|
|
998
1093
|
},
|
|
@@ -1015,9 +1110,9 @@ const TOOLS = {
|
|
|
1015
1110
|
args: [],
|
|
1016
1111
|
examples: [{ cmd: "frame.main", desc: "Exit iframe context" }]
|
|
1017
1112
|
},
|
|
1018
|
-
"frame.js": {
|
|
1019
|
-
desc: "Execute JS in specific frame",
|
|
1020
|
-
args: ["code"],
|
|
1113
|
+
"frame.js": {
|
|
1114
|
+
desc: "Execute JS in specific frame",
|
|
1115
|
+
args: ["code"],
|
|
1021
1116
|
opts: { id: "Frame ID from frame.list", file: "Run JS from file" },
|
|
1022
1117
|
examples: [
|
|
1023
1118
|
{ cmd: 'frame.js "return document.title" --id frame1', desc: "JS in specific frame" },
|
|
@@ -1028,25 +1123,37 @@ const TOOLS = {
|
|
|
1028
1123
|
cookie: {
|
|
1029
1124
|
desc: "Cookie management",
|
|
1030
1125
|
commands: {
|
|
1031
|
-
"cookie.list": {
|
|
1032
|
-
desc: "List all cookies for current tab's domain",
|
|
1126
|
+
"cookie.list": {
|
|
1127
|
+
desc: "List all cookies for current tab's domain",
|
|
1128
|
+
args: [],
|
|
1129
|
+
examples: [
|
|
1130
|
+
{ cmd: "cookie list", desc: "Show all cookies" },
|
|
1131
|
+
{ cmd: "cookie.list", desc: "Dot command form" },
|
|
1132
|
+
]
|
|
1133
|
+
},
|
|
1134
|
+
"cookie.get": {
|
|
1135
|
+
desc: "Get specific cookie",
|
|
1033
1136
|
args: [],
|
|
1034
|
-
|
|
1137
|
+
opts: { name: "Cookie name" },
|
|
1138
|
+
examples: [{ cmd: "cookie get session", desc: "Get cookie" }]
|
|
1035
1139
|
},
|
|
1036
|
-
"cookie.
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
args: [],
|
|
1140
|
+
"cookie.set": {
|
|
1141
|
+
desc: "Set a cookie",
|
|
1142
|
+
args: [],
|
|
1040
1143
|
opts: { name: "Cookie name", value: "Cookie value", expires: "Expiry date (optional)" },
|
|
1041
|
-
examples: [
|
|
1144
|
+
examples: [
|
|
1145
|
+
{ cmd: 'cookie set --name "session" --value "abc123"', desc: "Set cookie" },
|
|
1146
|
+
{ cmd: 'cookie.set --name "session" --value "abc123"', desc: "Dot command form" },
|
|
1147
|
+
]
|
|
1042
1148
|
},
|
|
1043
|
-
"cookie.clear": {
|
|
1044
|
-
desc: "Clear cookies",
|
|
1045
|
-
args: [],
|
|
1149
|
+
"cookie.clear": {
|
|
1150
|
+
desc: "Clear cookies",
|
|
1151
|
+
args: [],
|
|
1046
1152
|
opts: { name: "Specific cookie (optional)", all: "Clear all for domain" },
|
|
1047
1153
|
examples: [
|
|
1048
|
-
{ cmd: 'cookie
|
|
1049
|
-
{ cmd: "cookie
|
|
1154
|
+
{ cmd: 'cookie delete "session"', desc: "Clear one" },
|
|
1155
|
+
{ cmd: "cookie clear --all", desc: "Clear all" },
|
|
1156
|
+
{ cmd: 'cookie.clear --name "session"', desc: "Dot command form" },
|
|
1050
1157
|
]
|
|
1051
1158
|
},
|
|
1052
1159
|
}
|
|
@@ -1054,9 +1161,9 @@ const TOOLS = {
|
|
|
1054
1161
|
search: {
|
|
1055
1162
|
desc: "Text search",
|
|
1056
1163
|
commands: {
|
|
1057
|
-
"search": {
|
|
1058
|
-
desc: "Search for text in page",
|
|
1059
|
-
args: ["term"],
|
|
1164
|
+
"search": {
|
|
1165
|
+
desc: "Search for text in page",
|
|
1166
|
+
args: ["term"],
|
|
1060
1167
|
opts: { "case-sensitive": "Case-sensitive match", limit: "Max results" },
|
|
1061
1168
|
examples: [
|
|
1062
1169
|
{ cmd: 'search "login"', desc: "Find text" },
|
|
@@ -1070,9 +1177,9 @@ const TOOLS = {
|
|
|
1070
1177
|
batch: {
|
|
1071
1178
|
desc: "Batch execution",
|
|
1072
1179
|
commands: {
|
|
1073
|
-
"batch": {
|
|
1074
|
-
desc: "Execute multiple actions",
|
|
1075
|
-
args: [],
|
|
1180
|
+
"batch": {
|
|
1181
|
+
desc: "Execute multiple actions",
|
|
1182
|
+
args: [],
|
|
1076
1183
|
opts: { actions: "JSON array of actions", file: "Path to actions JSON file" },
|
|
1077
1184
|
examples: [
|
|
1078
1185
|
{ cmd: 'batch --actions \'[{"type":"click","ref":"e1"},{"type":"wait","ms":500}]\'', desc: "Inline actions" },
|
|
@@ -1130,9 +1237,9 @@ const TOOLS = {
|
|
|
1130
1237
|
zoom: {
|
|
1131
1238
|
desc: "Zoom control",
|
|
1132
1239
|
commands: {
|
|
1133
|
-
"zoom": {
|
|
1134
|
-
desc: "Get or set zoom level",
|
|
1135
|
-
args: [],
|
|
1240
|
+
"zoom": {
|
|
1241
|
+
desc: "Get or set zoom level",
|
|
1242
|
+
args: [],
|
|
1136
1243
|
opts: { level: "Zoom level (e.g., 1.5 for 150%)", reset: "Reset to default zoom" },
|
|
1137
1244
|
examples: [
|
|
1138
1245
|
{ cmd: "zoom", desc: "Get current zoom" },
|
|
@@ -1145,11 +1252,14 @@ const TOOLS = {
|
|
|
1145
1252
|
resize: {
|
|
1146
1253
|
desc: "Window management",
|
|
1147
1254
|
commands: {
|
|
1148
|
-
"resize": {
|
|
1149
|
-
desc: "Resize browser window",
|
|
1150
|
-
args: [],
|
|
1255
|
+
"resize": {
|
|
1256
|
+
desc: "Resize browser window",
|
|
1257
|
+
args: ["width", "height"],
|
|
1151
1258
|
opts: { width: "Window width", height: "Window height" },
|
|
1152
|
-
examples: [
|
|
1259
|
+
examples: [
|
|
1260
|
+
{ cmd: "resize 1280 720", desc: "Set size" },
|
|
1261
|
+
{ cmd: "resize --width 1280 --height 720", desc: "Set size with flags" },
|
|
1262
|
+
]
|
|
1153
1263
|
},
|
|
1154
1264
|
}
|
|
1155
1265
|
},
|
|
@@ -1164,14 +1274,14 @@ const TOOLS = {
|
|
|
1164
1274
|
history: {
|
|
1165
1275
|
desc: "Browser history",
|
|
1166
1276
|
commands: {
|
|
1167
|
-
"history.list": {
|
|
1168
|
-
desc: "Recent history",
|
|
1169
|
-
args: [],
|
|
1277
|
+
"history.list": {
|
|
1278
|
+
desc: "Recent history",
|
|
1279
|
+
args: [],
|
|
1170
1280
|
opts: { limit: "Max results" },
|
|
1171
1281
|
examples: [{ cmd: "history.list --limit 20", desc: "Last 20 items" }]
|
|
1172
1282
|
},
|
|
1173
|
-
"history.search": {
|
|
1174
|
-
desc: "Search history",
|
|
1283
|
+
"history.search": {
|
|
1284
|
+
desc: "Search history",
|
|
1175
1285
|
args: ["query"],
|
|
1176
1286
|
examples: [{ cmd: 'history.search "github"', desc: "Search history" }]
|
|
1177
1287
|
},
|
|
@@ -1180,10 +1290,10 @@ const TOOLS = {
|
|
|
1180
1290
|
window: {
|
|
1181
1291
|
desc: "Window management (isolate agent from your browsing)",
|
|
1182
1292
|
commands: {
|
|
1183
|
-
"window.new": {
|
|
1184
|
-
desc: "Create new browser window",
|
|
1185
|
-
args: ["url"],
|
|
1186
|
-
opts: {
|
|
1293
|
+
"window.new": {
|
|
1294
|
+
desc: "Create new browser window",
|
|
1295
|
+
args: ["url"],
|
|
1296
|
+
opts: {
|
|
1187
1297
|
width: "Window width",
|
|
1188
1298
|
height: "Window height",
|
|
1189
1299
|
incognito: "Open incognito window",
|
|
@@ -1195,28 +1305,28 @@ const TOOLS = {
|
|
|
1195
1305
|
{ cmd: 'window.new --incognito', desc: "Incognito window" },
|
|
1196
1306
|
]
|
|
1197
1307
|
},
|
|
1198
|
-
"window.list": {
|
|
1199
|
-
desc: "List all browser windows",
|
|
1308
|
+
"window.list": {
|
|
1309
|
+
desc: "List all browser windows",
|
|
1200
1310
|
args: [],
|
|
1201
1311
|
opts: { tabs: "Include tab details" },
|
|
1202
1312
|
examples: [{ cmd: "window.list", desc: "Show all windows" }]
|
|
1203
1313
|
},
|
|
1204
|
-
"window.focus": {
|
|
1205
|
-
desc: "Focus a window by ID",
|
|
1314
|
+
"window.focus": {
|
|
1315
|
+
desc: "Focus a window by ID",
|
|
1206
1316
|
args: ["id"],
|
|
1207
1317
|
examples: [{ cmd: "window.focus 123", desc: "Focus window" }]
|
|
1208
1318
|
},
|
|
1209
|
-
"window.close": {
|
|
1210
|
-
desc: "Close a window by ID",
|
|
1319
|
+
"window.close": {
|
|
1320
|
+
desc: "Close a window by ID",
|
|
1211
1321
|
args: ["id"],
|
|
1212
1322
|
examples: [{ cmd: "window.close 123", desc: "Close window" }]
|
|
1213
1323
|
},
|
|
1214
|
-
"window.resize": {
|
|
1215
|
-
desc: "Resize or reposition a window",
|
|
1216
|
-
args: [],
|
|
1217
|
-
opts: {
|
|
1218
|
-
id: "Window ID (required)",
|
|
1219
|
-
width: "Window width",
|
|
1324
|
+
"window.resize": {
|
|
1325
|
+
desc: "Resize or reposition a window",
|
|
1326
|
+
args: [],
|
|
1327
|
+
opts: {
|
|
1328
|
+
id: "Window ID (required)",
|
|
1329
|
+
width: "Window width",
|
|
1220
1330
|
height: "Window height",
|
|
1221
1331
|
left: "Window X position",
|
|
1222
1332
|
top: "Window Y position",
|
|
@@ -1263,10 +1373,17 @@ Use --index to select from multiple matches:
|
|
|
1263
1373
|
content: `Cookies are scoped to the current tab's domain.
|
|
1264
1374
|
|
|
1265
1375
|
Commands:
|
|
1266
|
-
cookie
|
|
1267
|
-
cookie
|
|
1268
|
-
cookie
|
|
1269
|
-
cookie
|
|
1376
|
+
cookie list List all cookies
|
|
1377
|
+
cookie get X Get specific cookie
|
|
1378
|
+
cookie set Set a cookie
|
|
1379
|
+
cookie clear --all Clear all cookies
|
|
1380
|
+
cookie delete X Clear one cookie
|
|
1381
|
+
|
|
1382
|
+
Dot commands remain supported:
|
|
1383
|
+
cookie.list
|
|
1384
|
+
cookie.get --name X
|
|
1385
|
+
cookie.set
|
|
1386
|
+
cookie.clear
|
|
1270
1387
|
|
|
1271
1388
|
Notes:
|
|
1272
1389
|
- HttpOnly cookies are accessible
|
|
@@ -1298,6 +1415,7 @@ Commands:
|
|
|
1298
1415
|
screenshot --output file.png Basic screenshot
|
|
1299
1416
|
screenshot --annotate --output file.png With element labels
|
|
1300
1417
|
screenshot --fullpage --output file.png Full page capture
|
|
1418
|
+
screenshot --full-page --output file.png Full page capture (alias)
|
|
1301
1419
|
screenshot --annotate --fullpage --output file.png Full page with labels
|
|
1302
1420
|
snap Auto-save to /tmp
|
|
1303
1421
|
|
|
@@ -1305,6 +1423,7 @@ Options:
|
|
|
1305
1423
|
--output Save path
|
|
1306
1424
|
--annotate Draw element refs
|
|
1307
1425
|
--fullpage Capture entire page
|
|
1426
|
+
--full-page Capture entire page (alias)
|
|
1308
1427
|
--max-height Max height for fullpage (default: 4000)`
|
|
1309
1428
|
},
|
|
1310
1429
|
automation: {
|
|
@@ -1327,7 +1446,7 @@ Wait for dynamic content:
|
|
|
1327
1446
|
|
|
1328
1447
|
Scroll and capture:
|
|
1329
1448
|
scroll.bottom
|
|
1330
|
-
screenshot --
|
|
1449
|
+
screenshot --full-page --output full.png`
|
|
1331
1450
|
},
|
|
1332
1451
|
windows: {
|
|
1333
1452
|
title: "Window Isolation",
|
|
@@ -1346,7 +1465,7 @@ All commands in that window:
|
|
|
1346
1465
|
|
|
1347
1466
|
Manage windows:
|
|
1348
1467
|
surf window.list # List all windows
|
|
1349
|
-
surf window.list --tabs # Include tab details
|
|
1468
|
+
surf window.list --tabs # Include tab details
|
|
1350
1469
|
surf window.focus 123 # Bring window to front
|
|
1351
1470
|
surf window.close 123 # Close when done
|
|
1352
1471
|
|
|
@@ -1439,7 +1558,7 @@ Exclude text content:
|
|
|
1439
1558
|
};
|
|
1440
1559
|
|
|
1441
1560
|
const ALL_SOCKET_TOOLS = [
|
|
1442
|
-
"ai", "screenshot", "navigate",
|
|
1561
|
+
"ai", "screenshot", "record", "animate-audit", "perf-audit", "navigate",
|
|
1443
1562
|
"form_input", "find_and_type", "autocomplete", "set_value", "smart_type",
|
|
1444
1563
|
"scroll_to_position", "get_scroll_info", "close_dialogs", "page_state",
|
|
1445
1564
|
"javascript_tool", "health", "smoke",
|
|
@@ -1453,8 +1572,8 @@ const ALL_SOCKET_TOOLS = [
|
|
|
1453
1572
|
"scroll.top", "scroll.bottom", "scroll.to", "scroll.info",
|
|
1454
1573
|
"wait.element", "wait.network", "wait.url", "wait.dom", "wait.load",
|
|
1455
1574
|
"click", "hover", "drag",
|
|
1456
|
-
"js", "console", "network",
|
|
1457
|
-
"network.get", "network.body", "network.curl", "network.origins",
|
|
1575
|
+
"js", "console", "network",
|
|
1576
|
+
"network.get", "network.body", "network.curl", "network.origins",
|
|
1458
1577
|
"network.clear", "network.stats", "network.export", "network.path",
|
|
1459
1578
|
"dialog.accept", "dialog.dismiss", "dialog.info",
|
|
1460
1579
|
"emulate.network", "emulate.cpu", "emulate.geo", "emulate.device", "emulate.viewport", "emulate.touch",
|
|
@@ -1497,6 +1616,9 @@ const SEE_ALSO = {
|
|
|
1497
1616
|
"perf.metrics": ["perf.start", "console", "network"],
|
|
1498
1617
|
"navigate": ["wait.load", "page.read"],
|
|
1499
1618
|
"screenshot": ["page.read", "scroll.bottom for fullpage"],
|
|
1619
|
+
"record": ["screenshot", "animate-audit", "perf-audit"],
|
|
1620
|
+
"animate-audit": ["screenshot", "record", "perf-audit", "js"],
|
|
1621
|
+
"perf-audit": ["record", "animate-audit", "perf.metrics", "console"],
|
|
1500
1622
|
"search": ["locate.text", "page.read"],
|
|
1501
1623
|
"wait.element": ["wait.load", "wait.network"],
|
|
1502
1624
|
"wait.load": ["wait.element", "wait.network"],
|
|
@@ -1516,10 +1638,14 @@ Common Commands:
|
|
|
1516
1638
|
click <ref> Click element by ref or selector
|
|
1517
1639
|
type <text> Type text at cursor or into element
|
|
1518
1640
|
screenshot Capture screenshot (alias: snap)
|
|
1641
|
+
record Capture screenshot frames into an animated GIF
|
|
1642
|
+
animate-audit JSON timeline of element animation/style samples
|
|
1643
|
+
perf-audit PerformanceObserver snapshot for motion/jank debugging
|
|
1519
1644
|
page.read Get page accessibility tree (alias: read)
|
|
1520
1645
|
locate.role <role> Find element by ARIA role
|
|
1521
1646
|
search <term> Search for text in page (alias: find)
|
|
1522
1647
|
window.new <url> Create isolated browser window
|
|
1648
|
+
doctor Diagnose native host/socket setup
|
|
1523
1649
|
wait <seconds> Wait N seconds
|
|
1524
1650
|
|
|
1525
1651
|
Quick Examples:
|
|
@@ -1534,6 +1660,7 @@ Quick Examples:
|
|
|
1534
1660
|
|
|
1535
1661
|
More Help:
|
|
1536
1662
|
surf --help-full All commands
|
|
1663
|
+
surf --llm-context Compact reference for AI agents
|
|
1537
1664
|
surf --help-topic <topic> Topic guide (refs, semantic, frames, devices...)
|
|
1538
1665
|
surf <command> --help Command details
|
|
1539
1666
|
surf --find <query> Search for commands
|
|
@@ -1541,6 +1668,34 @@ More Help:
|
|
|
1541
1668
|
`);
|
|
1542
1669
|
};
|
|
1543
1670
|
|
|
1671
|
+
const showLlmContext = () => {
|
|
1672
|
+
console.log(`SURF CLI LLM CONTEXT
|
|
1673
|
+
Purpose: control Chrome from shell. Commands are \`surf <command> [args] [options]\`.
|
|
1674
|
+
Core loop: navigate -> wait/read -> act -> screenshot/read.
|
|
1675
|
+
Navigate: surf navigate "https://example.com" # alias: surf go "..."
|
|
1676
|
+
Wait after navigation: surf wait 2 # or wait.load for load complete
|
|
1677
|
+
Read DOM/refs: surf page.read --depth 3 --compact # alias: surf read
|
|
1678
|
+
Refs: use e1/e2 refs from page.read; prefer refs over CSS when available.
|
|
1679
|
+
Click ref: surf click e5
|
|
1680
|
+
Click selector/coords: surf click --selector ".btn" | surf click 100 200
|
|
1681
|
+
Type: surf type "text" --submit # use --ref e5 to target a field
|
|
1682
|
+
Screenshot: surf screenshot /tmp/shot.png # auto-saves to /tmp if no path
|
|
1683
|
+
Full page screenshot: surf screenshot --full-page /tmp/full.png
|
|
1684
|
+
Record animation: surf record --duration 2000 --fps 10 --output /tmp/anim.gif
|
|
1685
|
+
Animation audit: surf animate-audit --selector ".thing" --duration 2000 --fps 10
|
|
1686
|
+
Performance audit: surf perf-audit --duration 3000 --trigger "click:.cta" --output /tmp/perf.json
|
|
1687
|
+
JavaScript: surf js "return document.title"
|
|
1688
|
+
Scroll: surf scroll down 800 | surf scroll up 400 | surf scroll bottom | surf scroll top
|
|
1689
|
+
Find by semantics: surf locate.role button --name "Submit" --action click
|
|
1690
|
+
Device/viewport: surf emulate.device "iPhone 14" | surf resize 375 812
|
|
1691
|
+
Cookies: surf cookie list | surf cookie get "name" | surf cookie delete "name"
|
|
1692
|
+
Window isolation: surf window.new "https://example.com" then pass --window-id <id>
|
|
1693
|
+
Concurrency: surf serializes commands per socket; use --no-lock only for intentional bypass
|
|
1694
|
+
Doctor: surf doctor --browser all # native host/socket diagnostics
|
|
1695
|
+
Workflow: surf do 'go "https://example.com" | wait 2 | read | click e5 | screenshot'
|
|
1696
|
+
More help: surf --help-full | surf <command> --help | surf --help-topic refs | surf --find <query>`);
|
|
1697
|
+
};
|
|
1698
|
+
|
|
1544
1699
|
const showFullHelp = () => {
|
|
1545
1700
|
console.log(`surf v${VERSION} - Browser automation CLI
|
|
1546
1701
|
|
|
@@ -1565,6 +1720,7 @@ Options:
|
|
|
1565
1720
|
--json Output raw JSON
|
|
1566
1721
|
--auto-capture On error: capture screenshot + console to /tmp
|
|
1567
1722
|
--soft-fail On error: warn and exit 0 (for non-critical commands)
|
|
1723
|
+
--no-lock Bypass the per-socket browser request lock
|
|
1568
1724
|
|
|
1569
1725
|
Script Mode:
|
|
1570
1726
|
surf --script <file> Run workflow from JSON
|
|
@@ -1673,7 +1829,7 @@ const showToolHelp = (toolName) => {
|
|
|
1673
1829
|
const fuzzyFind = (query) => {
|
|
1674
1830
|
const terms = query.toLowerCase().split(/\s+/);
|
|
1675
1831
|
const results = [];
|
|
1676
|
-
|
|
1832
|
+
|
|
1677
1833
|
for (const [groupName, group] of Object.entries(TOOLS)) {
|
|
1678
1834
|
for (const [cmd, info] of Object.entries(group.commands)) {
|
|
1679
1835
|
if (info.alias) continue;
|
|
@@ -1684,7 +1840,7 @@ const fuzzyFind = (query) => {
|
|
|
1684
1840
|
}
|
|
1685
1841
|
}
|
|
1686
1842
|
}
|
|
1687
|
-
|
|
1843
|
+
|
|
1688
1844
|
return results.sort((a, b) => b.score - a.score);
|
|
1689
1845
|
};
|
|
1690
1846
|
|
|
@@ -1732,6 +1888,11 @@ const showAllTools = () => {
|
|
|
1732
1888
|
console.log(`\n Total: ${ALL_SOCKET_TOOLS.length} commands\n`);
|
|
1733
1889
|
};
|
|
1734
1890
|
|
|
1891
|
+
if (args[0] === "--llm-context") {
|
|
1892
|
+
showLlmContext();
|
|
1893
|
+
process.exit(0);
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1735
1896
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
1736
1897
|
showBasicHelp();
|
|
1737
1898
|
process.exit(0);
|
|
@@ -1790,11 +1951,17 @@ if (args[0] === "extension-path" || args[0] === "path") {
|
|
|
1790
1951
|
process.exit(0);
|
|
1791
1952
|
}
|
|
1792
1953
|
|
|
1954
|
+
if (args[0] === "doctor") {
|
|
1955
|
+
const { runDoctorCli } = require("./doctor.cjs");
|
|
1956
|
+
runDoctorCli(args.slice(1)).then((code) => process.exit(code));
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1793
1960
|
if (args[0] === "install") {
|
|
1794
1961
|
const { spawnSync } = require("child_process");
|
|
1795
1962
|
const scriptPath = require("path").resolve(__dirname, "../scripts/install-native-host.cjs");
|
|
1796
1963
|
const installArgs = args.slice(1);
|
|
1797
|
-
|
|
1964
|
+
|
|
1798
1965
|
if (installArgs.length === 0 || installArgs[0] === "--help" || installArgs[0] === "-h") {
|
|
1799
1966
|
console.log(`
|
|
1800
1967
|
Usage: surf install <extension-id> [options]
|
|
@@ -1809,11 +1976,14 @@ Options:
|
|
|
1809
1976
|
-b, --browser Browser(s) to install for (default: chrome)
|
|
1810
1977
|
Values: chrome, chromium, brave, edge, arc, helium, all
|
|
1811
1978
|
Multiple: --browser chrome,brave
|
|
1979
|
+
--target Install target: auto, linux, windows
|
|
1980
|
+
On WSL2, auto installs for Windows Chrome. Use linux for WSLg/Linux browsers.
|
|
1812
1981
|
|
|
1813
1982
|
Examples:
|
|
1814
1983
|
surf install hnfbepgmaoklhekckbpjnleifhahkcpl
|
|
1815
1984
|
surf install hnfbepgmaoklhekckbpjnleifhahkcpl --browser brave
|
|
1816
1985
|
surf install hnfbepgmaoklhekckbpjnleifhahkcpl --browser all
|
|
1986
|
+
surf install hnfbepgmaoklhekckbpjnleifhahkcpl --target linux
|
|
1817
1987
|
`);
|
|
1818
1988
|
process.exit(0);
|
|
1819
1989
|
}
|
|
@@ -1828,7 +1998,7 @@ if (args[0] === "uninstall") {
|
|
|
1828
1998
|
const { spawnSync } = require("child_process");
|
|
1829
1999
|
const scriptPath = require("path").resolve(__dirname, "../scripts/uninstall-native-host.cjs");
|
|
1830
2000
|
const uninstallArgs = args.slice(1);
|
|
1831
|
-
|
|
2001
|
+
|
|
1832
2002
|
if (uninstallArgs.includes("--help") || uninstallArgs.includes("-h")) {
|
|
1833
2003
|
console.log(`
|
|
1834
2004
|
Usage: surf uninstall [options]
|
|
@@ -1839,11 +2009,14 @@ Options:
|
|
|
1839
2009
|
-b, --browser Browser(s) to uninstall from (default: chrome)
|
|
1840
2010
|
Values: chrome, chromium, brave, edge, arc, helium, all
|
|
1841
2011
|
-a, --all Uninstall from all browsers and remove wrapper
|
|
2012
|
+
--target Install target to remove: auto, linux, windows
|
|
2013
|
+
On WSL2, auto removes Windows-browser manifests. Use linux for WSLg/Linux browsers.
|
|
1842
2014
|
|
|
1843
2015
|
Examples:
|
|
1844
2016
|
surf uninstall
|
|
1845
2017
|
surf uninstall --browser brave
|
|
1846
2018
|
surf uninstall --all
|
|
2019
|
+
surf uninstall --target linux
|
|
1847
2020
|
`);
|
|
1848
2021
|
process.exit(0);
|
|
1849
2022
|
}
|
|
@@ -1977,7 +2150,7 @@ if (args.includes("--script")) {
|
|
|
1977
2150
|
}
|
|
1978
2151
|
}
|
|
1979
2152
|
});
|
|
1980
|
-
sock.on("error", (e) => reject(e));
|
|
2153
|
+
sock.on("error", (e) => reject(new Error(formatSocketError(e))));
|
|
1981
2154
|
let timeoutId;
|
|
1982
2155
|
timeoutId = setTimeout(() => { sock.destroy(); reject(new Error("Timeout")); }, 30000);
|
|
1983
2156
|
sock.on("close", () => clearTimeout(timeoutId));
|
|
@@ -2046,6 +2219,10 @@ if (args.includes("--script")) {
|
|
|
2046
2219
|
process.exit(failed > 0 ? 1 : 0);
|
|
2047
2220
|
};
|
|
2048
2221
|
|
|
2222
|
+
if (!dryRun) {
|
|
2223
|
+
installBrowserLock(parseBrowserLockOptions(args.includes("--no-lock")));
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2049
2226
|
runScript();
|
|
2050
2227
|
return;
|
|
2051
2228
|
}
|
|
@@ -2063,13 +2240,13 @@ if (args[0] === "do") {
|
|
|
2063
2240
|
let wantJson = false;
|
|
2064
2241
|
let tabId = undefined;
|
|
2065
2242
|
let windowId = undefined;
|
|
2066
|
-
|
|
2243
|
+
|
|
2067
2244
|
// 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
|
-
|
|
2245
|
+
const reservedFlags = ['file', 'f', 'dry-run', 'on-error', 'no-auto-wait', 'step-delay', 'json', 'tab-id', 'window-id', 'no-lock'];
|
|
2246
|
+
|
|
2070
2247
|
// Workflow-specific args (collected for variable substitution)
|
|
2071
2248
|
const workflowArgs = {};
|
|
2072
|
-
|
|
2249
|
+
|
|
2073
2250
|
// Parse do-specific arguments
|
|
2074
2251
|
for (let i = 0; i < doArgs.length; i++) {
|
|
2075
2252
|
const arg = doArgs[i];
|
|
@@ -2117,7 +2294,7 @@ if (args[0] === "do") {
|
|
|
2117
2294
|
commandsInput = arg;
|
|
2118
2295
|
}
|
|
2119
2296
|
}
|
|
2120
|
-
|
|
2297
|
+
|
|
2121
2298
|
if (!commandsInput && !fileInput) {
|
|
2122
2299
|
console.error("Error: commands string, workflow name, or --file required");
|
|
2123
2300
|
console.error('Usage: surf do \'go "url" | click e5\'');
|
|
@@ -2125,11 +2302,11 @@ if (args[0] === "do") {
|
|
|
2125
2302
|
console.error(" surf do my-workflow --arg1 value1 --arg2 value2");
|
|
2126
2303
|
process.exit(1);
|
|
2127
2304
|
}
|
|
2128
|
-
|
|
2305
|
+
|
|
2129
2306
|
let steps;
|
|
2130
2307
|
let workflow = null; // Full workflow object (for arg validation)
|
|
2131
2308
|
let workflowName = null;
|
|
2132
|
-
|
|
2309
|
+
|
|
2133
2310
|
try {
|
|
2134
2311
|
if (fileInput) {
|
|
2135
2312
|
// Explicit file path via --file
|
|
@@ -2143,7 +2320,7 @@ if (args[0] === "do") {
|
|
|
2143
2320
|
} else {
|
|
2144
2321
|
// Resolve: inline | file path | named workflow
|
|
2145
2322
|
const resolved = resolveWorkflow(commandsInput);
|
|
2146
|
-
|
|
2323
|
+
|
|
2147
2324
|
if (resolved.type === 'inline') {
|
|
2148
2325
|
// Inline pipe syntax
|
|
2149
2326
|
steps = parseDoCommands(resolved.content);
|
|
@@ -2166,13 +2343,13 @@ if (args[0] === "do") {
|
|
|
2166
2343
|
}
|
|
2167
2344
|
}
|
|
2168
2345
|
}
|
|
2169
|
-
|
|
2346
|
+
|
|
2170
2347
|
// Process workflow file if loaded
|
|
2171
2348
|
if (workflow) {
|
|
2172
2349
|
if (!workflow.steps || !Array.isArray(workflow.steps)) {
|
|
2173
2350
|
throw new Error("Workflow must have a 'steps' array");
|
|
2174
2351
|
}
|
|
2175
|
-
|
|
2352
|
+
|
|
2176
2353
|
// Validate required args
|
|
2177
2354
|
const argErrors = validateWorkflowArgs(workflow, workflowArgs);
|
|
2178
2355
|
if (argErrors.length > 0) {
|
|
@@ -2190,7 +2367,7 @@ if (args[0] === "do") {
|
|
|
2190
2367
|
console.error(`\nRun 'surf workflow.info ${workflowName}' for details.`);
|
|
2191
2368
|
process.exit(1);
|
|
2192
2369
|
}
|
|
2193
|
-
|
|
2370
|
+
|
|
2194
2371
|
// Convert steps: support both { tool, args } and { cmd, args } formats
|
|
2195
2372
|
// Also preserve loop steps as-is
|
|
2196
2373
|
steps = workflow.steps.map(s => {
|
|
@@ -2199,16 +2376,16 @@ if (args[0] === "do") {
|
|
|
2199
2376
|
const convertSteps = (stepsArr) => stepsArr.map(ns => {
|
|
2200
2377
|
if (ns.repeat !== undefined || ns.each !== undefined) {
|
|
2201
2378
|
// Recursively convert nested loop steps and until condition
|
|
2202
|
-
return {
|
|
2203
|
-
...ns,
|
|
2379
|
+
return {
|
|
2380
|
+
...ns,
|
|
2204
2381
|
steps: convertSteps(ns.steps || []),
|
|
2205
2382
|
until: ns.until ? { cmd: ns.until.tool || ns.until.cmd, args: ns.until.args || {} } : undefined
|
|
2206
2383
|
};
|
|
2207
2384
|
}
|
|
2208
2385
|
return { cmd: ns.tool || ns.cmd, args: ns.args || {}, as: ns.as };
|
|
2209
2386
|
});
|
|
2210
|
-
return {
|
|
2211
|
-
...s,
|
|
2387
|
+
return {
|
|
2388
|
+
...s,
|
|
2212
2389
|
steps: convertSteps(s.steps || []),
|
|
2213
2390
|
until: s.until ? { cmd: s.until.tool || s.until.cmd, args: s.until.args || {} } : undefined
|
|
2214
2391
|
};
|
|
@@ -2220,15 +2397,15 @@ if (args[0] === "do") {
|
|
|
2220
2397
|
console.error(`Error: Failed to parse workflow: ${e.message}`);
|
|
2221
2398
|
process.exit(1);
|
|
2222
2399
|
}
|
|
2223
|
-
|
|
2400
|
+
|
|
2224
2401
|
if (!steps || steps.length === 0) {
|
|
2225
2402
|
console.error("Error: No commands found in workflow");
|
|
2226
2403
|
process.exit(1);
|
|
2227
2404
|
}
|
|
2228
|
-
|
|
2405
|
+
|
|
2229
2406
|
// Apply arg defaults
|
|
2230
2407
|
const vars = workflow ? applyArgDefaults(workflow, workflowArgs) : workflowArgs;
|
|
2231
|
-
|
|
2408
|
+
|
|
2232
2409
|
// Validate with --dry-run
|
|
2233
2410
|
if (dryRun) {
|
|
2234
2411
|
if (workflowName) {
|
|
@@ -2247,7 +2424,9 @@ if (args[0] === "do") {
|
|
|
2247
2424
|
}
|
|
2248
2425
|
process.exit(0);
|
|
2249
2426
|
}
|
|
2250
|
-
|
|
2427
|
+
|
|
2428
|
+
installBrowserLock(parseBrowserLockOptions(doArgs.includes("--no-lock")));
|
|
2429
|
+
|
|
2251
2430
|
if (!wantJson) {
|
|
2252
2431
|
if (workflowName) {
|
|
2253
2432
|
console.log(`Running workflow: ${workflowName} (${steps.length} steps)...\n`);
|
|
@@ -2255,7 +2434,7 @@ if (args[0] === "do") {
|
|
|
2255
2434
|
console.log(`Running workflow (${steps.length} steps)...\n`);
|
|
2256
2435
|
}
|
|
2257
2436
|
}
|
|
2258
|
-
|
|
2437
|
+
|
|
2259
2438
|
const runWorkflow = async () => {
|
|
2260
2439
|
const result = await executeDoSteps(steps, {
|
|
2261
2440
|
onError,
|
|
@@ -2268,13 +2447,13 @@ if (args[0] === "do") {
|
|
|
2268
2447
|
windowId,
|
|
2269
2448
|
},
|
|
2270
2449
|
});
|
|
2271
|
-
|
|
2450
|
+
|
|
2272
2451
|
// Print summary
|
|
2273
2452
|
if (wantJson) {
|
|
2274
2453
|
console.log(JSON.stringify(result, null, 2));
|
|
2275
2454
|
process.exit(result.status === "completed" ? 0 : 1);
|
|
2276
2455
|
}
|
|
2277
|
-
|
|
2456
|
+
|
|
2278
2457
|
console.log("");
|
|
2279
2458
|
if (result.status === "completed") {
|
|
2280
2459
|
console.log(`Completed: ${result.completedSteps}/${result.totalSteps} steps (${result.totalMs}ms)`);
|
|
@@ -2288,7 +2467,7 @@ if (args[0] === "do") {
|
|
|
2288
2467
|
process.exit(1);
|
|
2289
2468
|
}
|
|
2290
2469
|
};
|
|
2291
|
-
|
|
2470
|
+
|
|
2292
2471
|
runWorkflow();
|
|
2293
2472
|
return;
|
|
2294
2473
|
}
|
|
@@ -2296,7 +2475,7 @@ if (args[0] === "do") {
|
|
|
2296
2475
|
// Handle workflow management commands
|
|
2297
2476
|
if (args[0] === "workflow.list") {
|
|
2298
2477
|
const workflows = listWorkflows();
|
|
2299
|
-
|
|
2478
|
+
|
|
2300
2479
|
if (workflows.length === 0) {
|
|
2301
2480
|
console.log("No workflows found.");
|
|
2302
2481
|
console.log(`\nWorkflow directories:`);
|
|
@@ -2306,13 +2485,13 @@ if (args[0] === "workflow.list") {
|
|
|
2306
2485
|
console.log(`\nCreate a workflow JSON file in one of these directories.`);
|
|
2307
2486
|
process.exit(0);
|
|
2308
2487
|
}
|
|
2309
|
-
|
|
2488
|
+
|
|
2310
2489
|
// Group by scope
|
|
2311
2490
|
const byScope = { project: [], user: [] };
|
|
2312
2491
|
for (const w of workflows) {
|
|
2313
2492
|
byScope[w.scope].push(w);
|
|
2314
2493
|
}
|
|
2315
|
-
|
|
2494
|
+
|
|
2316
2495
|
if (byScope.user.length > 0) {
|
|
2317
2496
|
console.log(`User Workflows (~/.surf/workflows/):`);
|
|
2318
2497
|
for (const w of byScope.user) {
|
|
@@ -2321,7 +2500,7 @@ if (args[0] === "workflow.list") {
|
|
|
2321
2500
|
}
|
|
2322
2501
|
console.log("");
|
|
2323
2502
|
}
|
|
2324
|
-
|
|
2503
|
+
|
|
2325
2504
|
if (byScope.project.length > 0) {
|
|
2326
2505
|
console.log(`Project Workflows (./.surf/workflows/):`);
|
|
2327
2506
|
for (const w of byScope.project) {
|
|
@@ -2330,7 +2509,7 @@ if (args[0] === "workflow.list") {
|
|
|
2330
2509
|
}
|
|
2331
2510
|
console.log("");
|
|
2332
2511
|
}
|
|
2333
|
-
|
|
2512
|
+
|
|
2334
2513
|
console.log(`Run 'surf workflow.info <name>' for details.`);
|
|
2335
2514
|
process.exit(0);
|
|
2336
2515
|
}
|
|
@@ -2342,16 +2521,16 @@ if (args[0] === "workflow.info") {
|
|
|
2342
2521
|
console.error("Usage: surf workflow.info <name>");
|
|
2343
2522
|
process.exit(1);
|
|
2344
2523
|
}
|
|
2345
|
-
|
|
2524
|
+
|
|
2346
2525
|
const info = getWorkflowInfo(name);
|
|
2347
2526
|
if (info.error) {
|
|
2348
2527
|
console.error(`Error: ${info.error}`);
|
|
2349
2528
|
process.exit(1);
|
|
2350
2529
|
}
|
|
2351
|
-
|
|
2530
|
+
|
|
2352
2531
|
console.log(`${info.name}${info.description ? ` - ${info.description}` : ''}`);
|
|
2353
2532
|
console.log("");
|
|
2354
|
-
|
|
2533
|
+
|
|
2355
2534
|
// Arguments
|
|
2356
2535
|
if (info.args && Object.keys(info.args).length > 0) {
|
|
2357
2536
|
console.log("Arguments:");
|
|
@@ -2364,18 +2543,18 @@ if (args[0] === "workflow.info") {
|
|
|
2364
2543
|
}
|
|
2365
2544
|
console.log("");
|
|
2366
2545
|
}
|
|
2367
|
-
|
|
2546
|
+
|
|
2368
2547
|
// Steps
|
|
2369
2548
|
console.log(`Steps (${info.steps.length}):`);
|
|
2370
2549
|
info.steps.forEach((step, i) => {
|
|
2371
2550
|
console.log(` ${i + 1}. ${formatStep(step)}`);
|
|
2372
2551
|
});
|
|
2373
2552
|
console.log("");
|
|
2374
|
-
|
|
2553
|
+
|
|
2375
2554
|
// Location
|
|
2376
2555
|
console.log(`Location: ${info.path}`);
|
|
2377
2556
|
console.log("");
|
|
2378
|
-
|
|
2557
|
+
|
|
2379
2558
|
// Example run command
|
|
2380
2559
|
const argExample = Object.entries(info.args || {})
|
|
2381
2560
|
.filter(([_, spec]) => spec.required)
|
|
@@ -2383,7 +2562,7 @@ if (args[0] === "workflow.info") {
|
|
|
2383
2562
|
.join(' ');
|
|
2384
2563
|
console.log(`Run:`);
|
|
2385
2564
|
console.log(` surf do ${name}${argExample ? ' ' + argExample : ''}`);
|
|
2386
|
-
|
|
2565
|
+
|
|
2387
2566
|
process.exit(0);
|
|
2388
2567
|
}
|
|
2389
2568
|
|
|
@@ -2394,9 +2573,9 @@ if (args[0] === "workflow.validate") {
|
|
|
2394
2573
|
console.error("Usage: surf workflow.validate <file>");
|
|
2395
2574
|
process.exit(1);
|
|
2396
2575
|
}
|
|
2397
|
-
|
|
2576
|
+
|
|
2398
2577
|
const result = validateWorkflowFile(filePath);
|
|
2399
|
-
|
|
2578
|
+
|
|
2400
2579
|
if (result.valid) {
|
|
2401
2580
|
console.log(`✓ Valid workflow: ${filePath}`);
|
|
2402
2581
|
console.log(` Name: ${result.workflow.name || '(unnamed)'}`);
|
|
@@ -2414,7 +2593,7 @@ if (args[0] === "workflow.validate") {
|
|
|
2414
2593
|
}
|
|
2415
2594
|
}
|
|
2416
2595
|
|
|
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"];
|
|
2596
|
+
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"];
|
|
2418
2597
|
|
|
2419
2598
|
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"];
|
|
2420
2599
|
|
|
@@ -2462,6 +2641,22 @@ let { positional, options } = parseArgs(args);
|
|
|
2462
2641
|
let tool = positional[0];
|
|
2463
2642
|
let firstArg = positional[1];
|
|
2464
2643
|
|
|
2644
|
+
if (tool === "cookie" && firstArg) {
|
|
2645
|
+
const cookieSubcommands = {
|
|
2646
|
+
list: "cookie.list",
|
|
2647
|
+
get: "cookie.get",
|
|
2648
|
+
set: "cookie.set",
|
|
2649
|
+
clear: "cookie.clear",
|
|
2650
|
+
delete: "cookie.clear",
|
|
2651
|
+
};
|
|
2652
|
+
const cookieTool = cookieSubcommands[firstArg];
|
|
2653
|
+
if (cookieTool) {
|
|
2654
|
+
tool = cookieTool;
|
|
2655
|
+
positional = [tool, ...positional.slice(2)];
|
|
2656
|
+
firstArg = positional[1];
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2465
2660
|
if (!tool) {
|
|
2466
2661
|
console.error("Error: No command specified");
|
|
2467
2662
|
process.exit(1);
|
|
@@ -2478,9 +2673,14 @@ tool = ALIASES[tool] || tool;
|
|
|
2478
2673
|
// Auto-save screenshots to temp file when no --output specified
|
|
2479
2674
|
// This ensures agents always get a usable file path, not just an in-memory ID
|
|
2480
2675
|
// Can be disabled with --no-save flag or autoSaveScreenshots: false in surf.json
|
|
2676
|
+
if (options["full-page"] === true) {
|
|
2677
|
+
options.fullpage = true;
|
|
2678
|
+
delete options["full-page"];
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2481
2681
|
const config = loadConfig();
|
|
2482
2682
|
const autoSaveEnabled = config.autoSaveScreenshots !== false && !options["no-save"];
|
|
2483
|
-
if (tool === "screenshot" && !options.output && !options.savePath && autoSaveEnabled) {
|
|
2683
|
+
if (tool === "screenshot" && !options.output && !options.savePath && firstArg === undefined && autoSaveEnabled) {
|
|
2484
2684
|
options.savePath = path.join(SURF_TMP, `surf-snap-${Date.now()}.png`);
|
|
2485
2685
|
}
|
|
2486
2686
|
|
|
@@ -2541,6 +2741,8 @@ const PRIMARY_ARG_MAP = {
|
|
|
2541
2741
|
"emulate.cpu": "rate",
|
|
2542
2742
|
search: "term",
|
|
2543
2743
|
find: "term",
|
|
2744
|
+
"cookie.get": "name",
|
|
2745
|
+
"cookie.clear": "name",
|
|
2544
2746
|
"wait.element": "selector",
|
|
2545
2747
|
"wait.url": "pattern",
|
|
2546
2748
|
zoom: "level",
|
|
@@ -2563,6 +2765,19 @@ const PRIMARY_ARG_MAP = {
|
|
|
2563
2765
|
|
|
2564
2766
|
const toolArgs = { ...options };
|
|
2565
2767
|
|
|
2768
|
+
if (tool === "scroll" && firstArg) {
|
|
2769
|
+
if (firstArg === "top" || firstArg === "bottom") {
|
|
2770
|
+
tool = `scroll.${firstArg}`;
|
|
2771
|
+
firstArg = undefined;
|
|
2772
|
+
} else if (["up", "down", "left", "right"].includes(firstArg)) {
|
|
2773
|
+
if (toolArgs.direction === undefined) toolArgs.direction = firstArg;
|
|
2774
|
+
if (positional[2] !== undefined && /^-?\d+$/.test(positional[2]) && toolArgs.amount === undefined && toolArgs.scroll_amount === undefined) {
|
|
2775
|
+
toolArgs.scroll_pixels = parseInt(positional[2], 10);
|
|
2776
|
+
}
|
|
2777
|
+
firstArg = undefined;
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2566
2781
|
if (tool === "click" && firstArg) {
|
|
2567
2782
|
if (/^e\d+$/.test(firstArg)) {
|
|
2568
2783
|
toolArgs.ref = firstArg;
|
|
@@ -2574,6 +2789,30 @@ if (tool === "click" && firstArg) {
|
|
|
2574
2789
|
}
|
|
2575
2790
|
}
|
|
2576
2791
|
|
|
2792
|
+
if (tool === "resize") {
|
|
2793
|
+
if (firstArg !== undefined && toolArgs.width === undefined) {
|
|
2794
|
+
let val = firstArg;
|
|
2795
|
+
if (/^-?\d+$/.test(val)) val = parseInt(val, 10);
|
|
2796
|
+
toolArgs.width = val;
|
|
2797
|
+
}
|
|
2798
|
+
if (positional[2] !== undefined && toolArgs.height === undefined) {
|
|
2799
|
+
let val = positional[2];
|
|
2800
|
+
if (/^-?\d+$/.test(val)) val = parseInt(val, 10);
|
|
2801
|
+
toolArgs.height = val;
|
|
2802
|
+
}
|
|
2803
|
+
firstArg = undefined;
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
if (tool === "screenshot" && firstArg !== undefined && toolArgs.output === undefined && toolArgs.savePath === undefined) {
|
|
2807
|
+
toolArgs.savePath = firstArg;
|
|
2808
|
+
firstArg = undefined;
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
if (tool === "record" && firstArg !== undefined && toolArgs.output === undefined) {
|
|
2812
|
+
toolArgs.output = firstArg;
|
|
2813
|
+
firstArg = undefined;
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2577
2816
|
if (firstArg !== undefined) {
|
|
2578
2817
|
const primaryKey = PRIMARY_ARG_MAP[tool];
|
|
2579
2818
|
if (primaryKey && toolArgs[primaryKey] === undefined) {
|
|
@@ -2646,6 +2885,9 @@ delete toolArgs["no-screenshot"];
|
|
|
2646
2885
|
const softFail = toolArgs["soft-fail"] === true;
|
|
2647
2886
|
delete toolArgs["soft-fail"];
|
|
2648
2887
|
|
|
2888
|
+
const lockOptions = parseBrowserLockOptions(toolArgs["no-lock"] === true);
|
|
2889
|
+
delete toolArgs["no-lock"];
|
|
2890
|
+
|
|
2649
2891
|
if (!noScreenshot && AUTO_SCREENSHOT_TOOLS.includes(tool)) {
|
|
2650
2892
|
toolArgs.autoScreenshot = true;
|
|
2651
2893
|
}
|
|
@@ -2667,12 +2909,20 @@ if (tool === "gemini") {
|
|
|
2667
2909
|
toolArgs.file = path.resolve(toolArgs.file);
|
|
2668
2910
|
}
|
|
2669
2911
|
}
|
|
2912
|
+
if (tool === "chatgpt" && toolArgs.file) {
|
|
2913
|
+
if (Array.isArray(toolArgs.file)) {
|
|
2914
|
+
toolArgs.file = toolArgs.file.map((filePath) => path.resolve(filePath));
|
|
2915
|
+
} else if (typeof toolArgs.file === "string") {
|
|
2916
|
+
toolArgs.file = path.resolve(toolArgs.file);
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
if ((tool === "screenshot" || tool === "record" || tool === "perf-audit") && outputPath && typeof outputPath !== "string") {
|
|
2921
|
+
console.error("Error: --output requires a file path");
|
|
2922
|
+
process.exit(1);
|
|
2923
|
+
}
|
|
2670
2924
|
|
|
2671
2925
|
if (tool === "screenshot" && outputPath) {
|
|
2672
|
-
if (typeof outputPath !== "string") {
|
|
2673
|
-
console.error("Error: --output requires a file path");
|
|
2674
|
-
process.exit(1);
|
|
2675
|
-
}
|
|
2676
2926
|
toolArgs.savePath = outputPath;
|
|
2677
2927
|
if (options.full) toolArgs.full = true;
|
|
2678
2928
|
if (options["max-size"]) toolArgs["max-size"] = options["max-size"];
|
|
@@ -2794,11 +3044,7 @@ if (streamMode && (tool === "console" || tool === "network")) {
|
|
|
2794
3044
|
});
|
|
2795
3045
|
|
|
2796
3046
|
sock.on("error", (e) => {
|
|
2797
|
-
|
|
2798
|
-
console.error("Error: Socket not found. Is Chrome running with the extension?");
|
|
2799
|
-
} else {
|
|
2800
|
-
console.error("Error:", e.message);
|
|
2801
|
-
}
|
|
3047
|
+
console.error("Error:", formatSocketError(e));
|
|
2802
3048
|
process.exit(1);
|
|
2803
3049
|
});
|
|
2804
3050
|
|
|
@@ -2819,7 +3065,7 @@ const request = {
|
|
|
2819
3065
|
...globalOpts,
|
|
2820
3066
|
};
|
|
2821
3067
|
|
|
2822
|
-
const sendRequest = (toolName, toolArgs = {}) => {
|
|
3068
|
+
const sendRequest = (toolName, toolArgs = {}, timeoutMs = 5000) => {
|
|
2823
3069
|
return new Promise((resolve, reject) => {
|
|
2824
3070
|
const sock = net.createConnection(SOCKET_PATH, () => {
|
|
2825
3071
|
const req = {
|
|
@@ -2853,13 +3099,140 @@ const sendRequest = (toolName, toolArgs = {}) => {
|
|
|
2853
3099
|
}
|
|
2854
3100
|
}
|
|
2855
3101
|
});
|
|
2856
|
-
sock.on("error", (e) => reject(e));
|
|
3102
|
+
sock.on("error", (e) => reject(new Error(formatSocketError(e))));
|
|
2857
3103
|
let timeoutId;
|
|
2858
|
-
timeoutId = setTimeout(() => { sock.destroy(); reject(new Error("Timeout")); },
|
|
3104
|
+
timeoutId = setTimeout(() => { sock.destroy(); reject(new Error("Timeout")); }, timeoutMs);
|
|
2859
3105
|
sock.on("close", () => clearTimeout(timeoutId));
|
|
2860
3106
|
});
|
|
2861
3107
|
};
|
|
2862
3108
|
|
|
3109
|
+
function parseRecordNumber(value, fallback, name, min, max) {
|
|
3110
|
+
if (value === undefined) return fallback;
|
|
3111
|
+
if (typeof value === "boolean") throw new Error(`${name} must be a number`);
|
|
3112
|
+
const parsed = Number(value);
|
|
3113
|
+
if (!Number.isFinite(parsed) || parsed < min || parsed > max) {
|
|
3114
|
+
throw new Error(`${name} must be between ${min} and ${max}`);
|
|
3115
|
+
}
|
|
3116
|
+
return parsed;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
function parseRecordRect(value) {
|
|
3120
|
+
if (value === undefined) return null;
|
|
3121
|
+
if (typeof value !== "string") throw new Error("rect must be x,y,width,height");
|
|
3122
|
+
const parts = value.split(",").map((part) => Number(part.trim()));
|
|
3123
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isFinite(part))) {
|
|
3124
|
+
throw new Error("rect must be x,y,width,height");
|
|
3125
|
+
}
|
|
3126
|
+
const [x, y, width, height] = parts;
|
|
3127
|
+
if (x < 0 || y < 0 || width <= 0 || height <= 0) {
|
|
3128
|
+
throw new Error("rect must use non-negative x/y and positive width/height");
|
|
3129
|
+
}
|
|
3130
|
+
return { x, y, width, height, crop: `${width}x${height}+${x}+${y}` };
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
function assertToolOk(response, context) {
|
|
3134
|
+
if (!response?.error) return;
|
|
3135
|
+
const message = response.error.content?.[0]?.text || response.error.message || JSON.stringify(response.error);
|
|
3136
|
+
throw new Error(`${context}: ${message}`);
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
function assembleRecordGif(framePaths, output, fps, rect) {
|
|
3140
|
+
const delay = Math.max(1, Math.round(100 / fps));
|
|
3141
|
+
const args = ["-delay", String(delay), "-loop", "0", ...framePaths];
|
|
3142
|
+
if (rect) args.push("-crop", rect.crop, "+repage");
|
|
3143
|
+
args.push(output);
|
|
3144
|
+
|
|
3145
|
+
try {
|
|
3146
|
+
execFileSync("magick", args, { stdio: "pipe" });
|
|
3147
|
+
return "magick";
|
|
3148
|
+
} catch (magickError) {
|
|
3149
|
+
try {
|
|
3150
|
+
execFileSync("convert", args, { stdio: "pipe" });
|
|
3151
|
+
return "convert";
|
|
3152
|
+
} catch (convertError) {
|
|
3153
|
+
const detail = convertError && convertError.message ? convertError.message : String(convertError);
|
|
3154
|
+
throw new Error(`Failed to assemble GIF with ImageMagick. Install ImageMagick (magick or convert). Last error: ${detail}`);
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
async function runRecord() {
|
|
3160
|
+
const durationMs = parseRecordNumber(toolArgs.duration, 2000, "duration", 100, 10000);
|
|
3161
|
+
const fps = parseRecordNumber(toolArgs.fps, 10, "fps", 1, 30);
|
|
3162
|
+
const rect = parseRecordRect(toolArgs.rect);
|
|
3163
|
+
const output = path.resolve(outputPath || path.join(SURF_TMP, `surf-record-${Date.now()}.gif`));
|
|
3164
|
+
const frameCount = Math.max(1, Math.ceil((durationMs / 1000) * fps));
|
|
3165
|
+
const frameDir = fs.mkdtempSync(path.join(SURF_TMP, "surf-record-"));
|
|
3166
|
+
const framePaths = [];
|
|
3167
|
+
let trigger = null;
|
|
3168
|
+
|
|
3169
|
+
try {
|
|
3170
|
+
if (toolArgs.trigger !== undefined) {
|
|
3171
|
+
trigger = await runRecordTrigger(toolArgs.trigger);
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
const startedAt = Date.now();
|
|
3175
|
+
for (let i = 0; i < frameCount; i++) {
|
|
3176
|
+
const framePath = path.join(frameDir, `frame-${String(i).padStart(4, "0")}.png`);
|
|
3177
|
+
const response = await sendRequest("screenshot", {
|
|
3178
|
+
savePath: framePath,
|
|
3179
|
+
full: toolArgs.full,
|
|
3180
|
+
"max-size": toolArgs["max-size"],
|
|
3181
|
+
}, 30000);
|
|
3182
|
+
assertToolOk(response, `record frame ${i + 1}`);
|
|
3183
|
+
framePaths.push(framePath);
|
|
3184
|
+
|
|
3185
|
+
if (i < frameCount - 1) {
|
|
3186
|
+
const nextFrameAt = startedAt + Math.round(((i + 1) * durationMs) / frameCount);
|
|
3187
|
+
const waitMs = nextFrameAt - Date.now();
|
|
3188
|
+
if (waitMs > 0) await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
3193
|
+
const imageMagick = assembleRecordGif(framePaths, output, fps, rect);
|
|
3194
|
+
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 } }) };
|
|
3195
|
+
|
|
3196
|
+
if (wantJson) {
|
|
3197
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3198
|
+
} else {
|
|
3199
|
+
console.log(`Saved recording to ${output} (${result.frames} frames, ${durationMs}ms @ ${fps}fps)`);
|
|
3200
|
+
}
|
|
3201
|
+
} finally {
|
|
3202
|
+
fs.rmSync(frameDir, { recursive: true, force: true });
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
async function runRecordTrigger(trigger) {
|
|
3207
|
+
if (typeof trigger !== "string") throw new Error("trigger must be action:target");
|
|
3208
|
+
const separator = trigger.indexOf(":");
|
|
3209
|
+
if (separator === -1) throw new Error("trigger must be action:target");
|
|
3210
|
+
const action = trigger.slice(0, separator).trim();
|
|
3211
|
+
const target = trigger.slice(separator + 1).trim();
|
|
3212
|
+
if (!action || !target) throw new Error("trigger must be action:target");
|
|
3213
|
+
|
|
3214
|
+
if (action === "click") {
|
|
3215
|
+
const response = await sendRequest("click", { selector: target }, 30000);
|
|
3216
|
+
assertToolOk(response, "record trigger");
|
|
3217
|
+
return { action, selector: target };
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
if (action === "scroll") {
|
|
3221
|
+
let response;
|
|
3222
|
+
if (["up", "down", "left", "right"].includes(target)) {
|
|
3223
|
+
response = await sendRequest("scroll", { direction: target }, 30000);
|
|
3224
|
+
} else if (target === "top" || target === "bottom") {
|
|
3225
|
+
response = await sendRequest(`scroll.${target}`, {}, 30000);
|
|
3226
|
+
} else {
|
|
3227
|
+
response = await sendRequest("scroll.bottom", { selector: target }, 30000);
|
|
3228
|
+
}
|
|
3229
|
+
assertToolOk(response, "record trigger");
|
|
3230
|
+
return { action, target };
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
throw new Error("trigger action must be click or scroll");
|
|
3234
|
+
}
|
|
3235
|
+
|
|
2863
3236
|
const performAutoCapture = async () => {
|
|
2864
3237
|
const timestamp = Date.now();
|
|
2865
3238
|
const screenshotPath = path.join(SURF_TMP, `surf-error-${timestamp}.png`);
|
|
@@ -2896,6 +3269,19 @@ const performAutoCapture = async () => {
|
|
|
2896
3269
|
}
|
|
2897
3270
|
};
|
|
2898
3271
|
|
|
3272
|
+
if (finalTool === "record") {
|
|
3273
|
+
installBrowserLock(lockOptions);
|
|
3274
|
+
runRecord()
|
|
3275
|
+
.then(() => process.exit(0))
|
|
3276
|
+
.catch((error) => {
|
|
3277
|
+
console.error("Error:", error && error.message ? error.message : String(error));
|
|
3278
|
+
process.exit(1);
|
|
3279
|
+
});
|
|
3280
|
+
return;
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
installBrowserLock(lockOptions);
|
|
3284
|
+
|
|
2899
3285
|
const socket = net.createConnection(SOCKET_PATH, () => {
|
|
2900
3286
|
socket.write(JSON.stringify(request) + "\n");
|
|
2901
3287
|
});
|
|
@@ -2923,14 +3309,14 @@ socket.on("data", (data) => {
|
|
|
2923
3309
|
if (!line.trim()) continue;
|
|
2924
3310
|
try {
|
|
2925
3311
|
const msg = JSON.parse(line);
|
|
2926
|
-
|
|
3312
|
+
|
|
2927
3313
|
if (msg.type === "extension_disconnected") {
|
|
2928
3314
|
clearTimeout(timeout);
|
|
2929
3315
|
console.error(msg.message);
|
|
2930
3316
|
socket.end();
|
|
2931
3317
|
process.exit(1);
|
|
2932
3318
|
}
|
|
2933
|
-
|
|
3319
|
+
|
|
2934
3320
|
handleResponse(msg).catch((err) => {
|
|
2935
3321
|
console.error("Handler error:", err.message);
|
|
2936
3322
|
process.exit(1);
|
|
@@ -2944,13 +3330,7 @@ socket.on("data", (data) => {
|
|
|
2944
3330
|
|
|
2945
3331
|
socket.on("error", (err) => {
|
|
2946
3332
|
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
|
-
}
|
|
3333
|
+
console.error("Error:", formatSocketError(err));
|
|
2954
3334
|
process.exit(1);
|
|
2955
3335
|
});
|
|
2956
3336
|
|
|
@@ -2979,7 +3359,7 @@ async function handleResponse(response) {
|
|
|
2979
3359
|
}
|
|
2980
3360
|
|
|
2981
3361
|
const result = response.result?.content?.[0]?.text;
|
|
2982
|
-
|
|
3362
|
+
|
|
2983
3363
|
let data;
|
|
2984
3364
|
try {
|
|
2985
3365
|
data = result ? JSON.parse(result) : response.result;
|
|
@@ -2991,6 +3371,17 @@ async function handleResponse(response) {
|
|
|
2991
3371
|
data = { response: data };
|
|
2992
3372
|
}
|
|
2993
3373
|
|
|
3374
|
+
if (tool === "perf-audit" && outputPath) {
|
|
3375
|
+
const saveTo = path.resolve(outputPath);
|
|
3376
|
+
fs.mkdirSync(path.dirname(saveTo), { recursive: true });
|
|
3377
|
+
fs.writeFileSync(saveTo, JSON.stringify(data ?? null, null, 2));
|
|
3378
|
+
if (!wantJson) {
|
|
3379
|
+
console.log(`Saved perf audit to ${saveTo}`);
|
|
3380
|
+
socket.end();
|
|
3381
|
+
process.exit(0);
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
|
|
2994
3385
|
if (wantJson) {
|
|
2995
3386
|
console.log(JSON.stringify(data ?? null, null, 2));
|
|
2996
3387
|
socket.end();
|
|
@@ -3000,12 +3391,12 @@ async function handleResponse(response) {
|
|
|
3000
3391
|
if (tool === "screenshot" && data?.base64 && (outputPath || toolArgs.savePath)) {
|
|
3001
3392
|
const saveTo = outputPath || toolArgs.savePath;
|
|
3002
3393
|
fs.writeFileSync(saveTo, Buffer.from(data.base64, "base64"));
|
|
3003
|
-
|
|
3394
|
+
|
|
3004
3395
|
const skipResize = options.full || toolArgs.full;
|
|
3005
3396
|
const maxSize = parseInt(options["max-size"] || toolArgs["max-size"] || "1200", 10);
|
|
3006
3397
|
const origWidth = data.width || 0;
|
|
3007
3398
|
const origHeight = data.height || 0;
|
|
3008
|
-
|
|
3399
|
+
|
|
3009
3400
|
if (!skipResize && (origWidth > maxSize || origHeight > maxSize)) {
|
|
3010
3401
|
const result = resizeImage(saveTo, maxSize);
|
|
3011
3402
|
if (result.success) {
|
|
@@ -3092,7 +3483,7 @@ async function handleResponse(response) {
|
|
|
3092
3483
|
} else if (tool === "smoke" && data?.results) {
|
|
3093
3484
|
const results = data.results;
|
|
3094
3485
|
const summary = data.summary || { pass: 0, fail: 0, total: results.length };
|
|
3095
|
-
|
|
3486
|
+
|
|
3096
3487
|
for (const r of results) {
|
|
3097
3488
|
const status = r.status === "pass" ? "PASS" : "FAIL";
|
|
3098
3489
|
const timeStr = r.time ? ` (${r.time}ms)` : "";
|
|
@@ -3104,10 +3495,10 @@ async function handleResponse(response) {
|
|
|
3104
3495
|
}
|
|
3105
3496
|
}
|
|
3106
3497
|
}
|
|
3107
|
-
|
|
3498
|
+
|
|
3108
3499
|
console.log("");
|
|
3109
3500
|
console.log(`Summary: ${summary.pass} passed, ${summary.fail} failed, ${summary.total} total`);
|
|
3110
|
-
|
|
3501
|
+
|
|
3111
3502
|
if (summary.fail > 0) {
|
|
3112
3503
|
socket.end();
|
|
3113
3504
|
process.exit(1);
|
|
@@ -3119,7 +3510,7 @@ async function handleResponse(response) {
|
|
|
3119
3510
|
} else if (tool === "network" && (data?.entries || data?.requests)) {
|
|
3120
3511
|
// Network list - handle both new (entries) and old (requests) formats
|
|
3121
3512
|
const items = data.entries || data.requests || [];
|
|
3122
|
-
|
|
3513
|
+
|
|
3123
3514
|
if (items.length === 0) {
|
|
3124
3515
|
console.log("No network requests captured");
|
|
3125
3516
|
} else if (data._format === 'raw') {
|