surf-cli 2.0.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/LICENSE +21 -0
- package/README.md +426 -0
- package/dist/content/accessibility-tree.js +11 -0
- package/dist/content/accessibility-tree.js.map +1 -0
- package/dist/content/visual-indicator.js +111 -0
- package/dist/content/visual-indicator.js.map +1 -0
- package/dist/icons/icon-128.png +0 -0
- package/dist/icons/icon-16.png +0 -0
- package/dist/icons/icon-48.png +0 -0
- package/dist/manifest.json +70 -0
- package/dist/options/options.html +30 -0
- package/dist/options/options.js +30 -0
- package/dist/options/options.js.map +1 -0
- package/dist/service-worker/index.js +156 -0
- package/dist/service-worker/index.js.map +1 -0
- package/dist/service-worker-loader.js +1 -0
- package/native/CHANGELOG.md +136 -0
- package/native/README.md +141 -0
- package/native/chatgpt-client.cjs +455 -0
- package/native/cli.cjs +2424 -0
- package/native/config.cjs +87 -0
- package/native/device-presets.cjs +211 -0
- package/native/formatters/network.cjs +402 -0
- package/native/gemini-client.cjs +637 -0
- package/native/host-helpers.cjs +989 -0
- package/native/host-wrapper.py +15 -0
- package/native/host.cjs +1271 -0
- package/native/host.sh +2 -0
- package/native/mcp-server.cjs +511 -0
- package/native/network-store.cjs +851 -0
- package/native/perplexity-client.cjs +561 -0
- package/native/protocol.cjs +27 -0
- package/native/test-host.py +41 -0
- package/native/tests/cli-tests.sh +115 -0
- package/package.json +70 -0
- package/scripts/install-native-host.cjs +308 -0
- package/scripts/uninstall-native-host.cjs +194 -0
package/native/cli.cjs
ADDED
|
@@ -0,0 +1,2424 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const net = require("net");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const { execSync } = require("child_process");
|
|
5
|
+
const { loadConfig, getConfigPath, createStarterConfig } = require("./config.cjs");
|
|
6
|
+
const networkFormatters = require("./formatters/network.cjs");
|
|
7
|
+
const networkStore = require("./network-store.cjs");
|
|
8
|
+
|
|
9
|
+
const SOCKET_PATH = "/tmp/surf.sock";
|
|
10
|
+
|
|
11
|
+
// Cross-platform image resize (macOS: sips, Linux: ImageMagick)
|
|
12
|
+
function resizeImage(filePath, maxSize) {
|
|
13
|
+
const platform = process.platform;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
if (platform === "darwin") {
|
|
17
|
+
// macOS: use sips
|
|
18
|
+
execSync(`sips --resampleHeightWidthMax ${maxSize} "${filePath}" --out "${filePath}" 2>/dev/null`, { stdio: "pipe" });
|
|
19
|
+
const sizeInfo = execSync(`sips -g pixelWidth -g pixelHeight "${filePath}" 2>/dev/null`, { encoding: "utf8" });
|
|
20
|
+
const width = parseInt(sizeInfo.match(/pixelWidth:\s*(\d+)/)?.[1] || "0", 10);
|
|
21
|
+
const height = parseInt(sizeInfo.match(/pixelHeight:\s*(\d+)/)?.[1] || "0", 10);
|
|
22
|
+
return { success: true, width, height };
|
|
23
|
+
} else {
|
|
24
|
+
// Linux/other: use ImageMagick (try IM6 first, then IM7)
|
|
25
|
+
try {
|
|
26
|
+
execSync(`convert "${filePath}" -resize ${maxSize}x${maxSize}\\> "${filePath}"`, { stdio: "pipe" });
|
|
27
|
+
} catch {
|
|
28
|
+
// IM7 uses 'magick' as main command
|
|
29
|
+
execSync(`magick "${filePath}" -resize ${maxSize}x${maxSize}\\> "${filePath}"`, { stdio: "pipe" });
|
|
30
|
+
}
|
|
31
|
+
// Get dimensions (IM7 may need 'magick identify' instead of just 'identify')
|
|
32
|
+
let sizeInfo;
|
|
33
|
+
try {
|
|
34
|
+
sizeInfo = execSync(`identify -format "%w %h" "${filePath}"`, { encoding: "utf8" });
|
|
35
|
+
} catch {
|
|
36
|
+
sizeInfo = execSync(`magick identify -format "%w %h" "${filePath}"`, { encoding: "utf8" });
|
|
37
|
+
}
|
|
38
|
+
const [width, height] = sizeInfo.trim().split(" ").map(Number);
|
|
39
|
+
return { success: true, width, height };
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return { success: false, error: e.message };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const args = process.argv.slice(2);
|
|
46
|
+
const VERSION = "2.0.0";
|
|
47
|
+
|
|
48
|
+
const ALIASES = {
|
|
49
|
+
snap: "screenshot",
|
|
50
|
+
read: "page.read",
|
|
51
|
+
find: "search",
|
|
52
|
+
go: "navigate",
|
|
53
|
+
net: "network",
|
|
54
|
+
"network.dump": "network.get",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const REMOVED_COMMANDS = {
|
|
58
|
+
read_page: "page.read",
|
|
59
|
+
get_page_text: "page.text",
|
|
60
|
+
page_state: "page.state",
|
|
61
|
+
list_tabs: "tab.list",
|
|
62
|
+
new_tab: "tab.new",
|
|
63
|
+
switch_tab: "tab.switch",
|
|
64
|
+
close_tab: "tab.close",
|
|
65
|
+
scroll_to: "scroll.to",
|
|
66
|
+
scroll_to_position: "scroll.to",
|
|
67
|
+
get_scroll_info: "scroll.info",
|
|
68
|
+
wait_for_element: "wait.element",
|
|
69
|
+
wait_for_url: "wait.url",
|
|
70
|
+
wait_for_network_idle: "wait.network",
|
|
71
|
+
javascript_tool: "js",
|
|
72
|
+
read_console_messages: "console",
|
|
73
|
+
read_network_requests: "network",
|
|
74
|
+
tabs_context: "tab.list",
|
|
75
|
+
tabs_create: "tab.new",
|
|
76
|
+
tabs_register: "tab.name",
|
|
77
|
+
tabs_unregister: "tab.unname",
|
|
78
|
+
tabs_get_by_name: "tab.switch",
|
|
79
|
+
tabs_list_named: "tab.named",
|
|
80
|
+
upload_image: "upload",
|
|
81
|
+
resize_window: "resize",
|
|
82
|
+
type_submit: "type --submit",
|
|
83
|
+
left_click: "click",
|
|
84
|
+
right_click: "click --button right",
|
|
85
|
+
double_click: "click --button double",
|
|
86
|
+
triple_click: "click --button triple",
|
|
87
|
+
left_click_drag: "drag",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const TOOLS = {
|
|
91
|
+
ai: {
|
|
92
|
+
desc: "AI assistants (ChatGPT, Gemini)",
|
|
93
|
+
commands: {
|
|
94
|
+
"chatgpt": {
|
|
95
|
+
desc: "Send prompt to ChatGPT (uses browser cookies)",
|
|
96
|
+
args: ["query"],
|
|
97
|
+
opts: {
|
|
98
|
+
"with-page": "Include current page context",
|
|
99
|
+
model: "Model: gpt-4o, o1, etc.",
|
|
100
|
+
file: "Attach file",
|
|
101
|
+
timeout: "Timeout in seconds (default: 2700 = 45min)"
|
|
102
|
+
},
|
|
103
|
+
examples: [
|
|
104
|
+
{ cmd: 'chatgpt "explain this code"', desc: "Basic query" },
|
|
105
|
+
{ cmd: 'chatgpt "summarize" --with-page', desc: "With page context" },
|
|
106
|
+
{ cmd: 'chatgpt "review" --file code.ts', desc: "With file" },
|
|
107
|
+
{ cmd: 'chatgpt "analyze" --model gpt-4o', desc: "Specify model" },
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
"gemini": {
|
|
111
|
+
desc: "Send prompt to Gemini (uses browser cookies)",
|
|
112
|
+
args: ["query"],
|
|
113
|
+
opts: {
|
|
114
|
+
"with-page": "Include current page context",
|
|
115
|
+
model: "Model: gemini-3-pro (default), gemini-2.5-pro, gemini-2.5-flash",
|
|
116
|
+
file: "Attach file to analyze",
|
|
117
|
+
"generate-image": "Generate image and save to path",
|
|
118
|
+
"edit-image": "Edit existing image (use with --output)",
|
|
119
|
+
output: "Output file path for image operations",
|
|
120
|
+
youtube: "YouTube video URL to analyze",
|
|
121
|
+
"aspect-ratio": "Aspect ratio for image generation (e.g., 1:1, 16:9)",
|
|
122
|
+
timeout: "Timeout in seconds (default: 300)"
|
|
123
|
+
},
|
|
124
|
+
examples: [
|
|
125
|
+
{ cmd: 'gemini "explain quantum computing"', desc: "Basic query" },
|
|
126
|
+
{ cmd: 'gemini "summarize" --with-page', desc: "With page context" },
|
|
127
|
+
{ cmd: 'gemini "analyze" --file data.csv', desc: "With file attachment" },
|
|
128
|
+
{ cmd: 'gemini "a robot surfing" --generate-image /tmp/robot.png', desc: "Generate image" },
|
|
129
|
+
{ cmd: 'gemini "add sunglasses" --edit-image photo.jpg --output out.jpg', desc: "Edit image" },
|
|
130
|
+
{ cmd: 'gemini "summarize this video" --youtube "https://youtube.com/..."', desc: "YouTube analysis" },
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
"perplexity": {
|
|
134
|
+
desc: "Search with Perplexity AI (uses browser session)",
|
|
135
|
+
args: ["query"],
|
|
136
|
+
opts: {
|
|
137
|
+
"with-page": "Include current page context",
|
|
138
|
+
mode: "Mode: search (default), research",
|
|
139
|
+
model: "Model (Pro users): sonar, gpt-4o, claude, etc.",
|
|
140
|
+
timeout: "Timeout in seconds (default: 120)"
|
|
141
|
+
},
|
|
142
|
+
examples: [
|
|
143
|
+
{ cmd: 'perplexity "what is quantum computing"', desc: "Basic search" },
|
|
144
|
+
{ cmd: 'perplexity "explain this page" --with-page', desc: "With page context" },
|
|
145
|
+
{ cmd: 'perplexity "deep dive into transformers" --mode research', desc: "Research mode" },
|
|
146
|
+
{ cmd: 'perplexity "latest AI news" --model sonar', desc: "Specify model (Pro)" },
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
"ai": {
|
|
150
|
+
desc: "Analyze page with AI (requires GOOGLE_API_KEY)",
|
|
151
|
+
args: ["query"],
|
|
152
|
+
opts: { mode: "Query mode: find|summary|extract (auto-detected)" },
|
|
153
|
+
examples: [
|
|
154
|
+
{ cmd: 'ai "find the login button"', desc: "Find element" },
|
|
155
|
+
{ cmd: 'ai "summarize this page"', desc: "Get summary" },
|
|
156
|
+
{ cmd: 'ai "extract all links as json"', desc: "Extract data" },
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
tab: {
|
|
162
|
+
desc: "Tab management",
|
|
163
|
+
commands: {
|
|
164
|
+
"tab.list": { desc: "List all open tabs", args: [], examples: [{ cmd: "tab.list", desc: "Show all tabs" }] },
|
|
165
|
+
"tab.new": {
|
|
166
|
+
desc: "Open new tab",
|
|
167
|
+
args: ["url"],
|
|
168
|
+
opts: { urls: "Open multiple URLs" },
|
|
169
|
+
examples: [
|
|
170
|
+
{ cmd: 'tab.new "https://google.com"', desc: "Open single tab" },
|
|
171
|
+
{ cmd: 'tab.new --urls "https://a.com" "https://b.com"', desc: "Open multiple" },
|
|
172
|
+
]
|
|
173
|
+
},
|
|
174
|
+
"tab.switch": {
|
|
175
|
+
desc: "Switch to tab by ID or name",
|
|
176
|
+
args: ["id"],
|
|
177
|
+
examples: [
|
|
178
|
+
{ cmd: "tab.switch 123", desc: "Switch by ID" },
|
|
179
|
+
{ cmd: 'tab.switch "myTab"', desc: "Switch by name" },
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
"tab.close": {
|
|
183
|
+
desc: "Close tab by ID or name",
|
|
184
|
+
args: ["id"],
|
|
185
|
+
opts: { ids: "Close multiple tabs" },
|
|
186
|
+
examples: [{ cmd: "tab.close 123", desc: "Close tab" }]
|
|
187
|
+
},
|
|
188
|
+
"tab.name": {
|
|
189
|
+
desc: "Register current tab with a name",
|
|
190
|
+
args: ["name"],
|
|
191
|
+
examples: [{ cmd: 'tab.name "dashboard"', desc: "Name current tab" }]
|
|
192
|
+
},
|
|
193
|
+
"tab.unname": { desc: "Unregister a named tab", args: ["name"] },
|
|
194
|
+
"tab.named": { desc: "List all named tabs", args: [] },
|
|
195
|
+
"tab.group": {
|
|
196
|
+
desc: "Create/add to tab group",
|
|
197
|
+
args: [],
|
|
198
|
+
opts: { name: "Group name", tabs: "Tab IDs (comma-separated)", color: "Group color" },
|
|
199
|
+
examples: [
|
|
200
|
+
{ cmd: 'tab.group --name "Work" --color blue', desc: "Group current tab" },
|
|
201
|
+
{ cmd: 'tab.group --name "Research" --tabs 1,2,3', desc: "Group multiple" },
|
|
202
|
+
]
|
|
203
|
+
},
|
|
204
|
+
"tab.ungroup": { desc: "Remove tabs from group", args: [], opts: { tabs: "Tab IDs (comma-separated)" } },
|
|
205
|
+
"tab.groups": { desc: "List all tab groups", args: [] },
|
|
206
|
+
"tab.reload": {
|
|
207
|
+
desc: "Reload current tab",
|
|
208
|
+
args: [],
|
|
209
|
+
opts: { hard: "Bypass cache" },
|
|
210
|
+
examples: [
|
|
211
|
+
{ cmd: "tab.reload", desc: "Soft reload" },
|
|
212
|
+
{ cmd: "tab.reload --hard", desc: "Hard reload (bypass cache)" },
|
|
213
|
+
]
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
nav: {
|
|
218
|
+
desc: "Navigation",
|
|
219
|
+
commands: {
|
|
220
|
+
"navigate": {
|
|
221
|
+
desc: "Go to URL",
|
|
222
|
+
args: ["url"],
|
|
223
|
+
examples: [{ cmd: 'navigate "https://example.com"', desc: "Go to URL" }]
|
|
224
|
+
},
|
|
225
|
+
"go": { desc: "Alias for navigate", args: ["url"], alias: "navigate" },
|
|
226
|
+
"back": {
|
|
227
|
+
desc: "Go back in history",
|
|
228
|
+
args: [],
|
|
229
|
+
examples: [{ cmd: "back", desc: "Browser back" }]
|
|
230
|
+
},
|
|
231
|
+
"forward": {
|
|
232
|
+
desc: "Go forward in history",
|
|
233
|
+
args: [],
|
|
234
|
+
examples: [{ cmd: "forward", desc: "Browser forward" }]
|
|
235
|
+
},
|
|
236
|
+
"screenshot": {
|
|
237
|
+
desc: "Capture screenshot (auto-resized for LLM by default)",
|
|
238
|
+
args: [],
|
|
239
|
+
opts: {
|
|
240
|
+
output: "Save to file",
|
|
241
|
+
selector: "Capture specific element",
|
|
242
|
+
annotate: "Draw element labels",
|
|
243
|
+
fullpage: "Capture full page",
|
|
244
|
+
"max-height": "Max height for fullpage (default: 4000)",
|
|
245
|
+
full: "Skip resize, save at full resolution",
|
|
246
|
+
"max-size": "Max dimension in px (default: 1200)"
|
|
247
|
+
},
|
|
248
|
+
examples: [
|
|
249
|
+
{ cmd: "screenshot --output /tmp/shot.png", desc: "Save to file (auto-resized)" },
|
|
250
|
+
{ cmd: "screenshot --full --output /tmp/shot.png", desc: "Full resolution" },
|
|
251
|
+
{ cmd: "screenshot --max-size 800 --output /tmp/small.png", desc: "Custom max size" },
|
|
252
|
+
{ cmd: "screenshot --annotate --output /tmp/annotated.png", desc: "With element labels" },
|
|
253
|
+
{ cmd: "snap", desc: "Auto-save to /tmp (resized)" },
|
|
254
|
+
]
|
|
255
|
+
},
|
|
256
|
+
"snap": { desc: "Alias for screenshot (auto-saves to /tmp)", args: [], alias: "screenshot" },
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
scroll: {
|
|
260
|
+
desc: "Scrolling",
|
|
261
|
+
commands: {
|
|
262
|
+
"scroll": {
|
|
263
|
+
desc: "Scroll in direction",
|
|
264
|
+
args: [],
|
|
265
|
+
opts: { direction: "up|down|left|right", amount: "Scroll amount (1-10)" },
|
|
266
|
+
examples: [{ cmd: "scroll --direction down --amount 3", desc: "Scroll down" }]
|
|
267
|
+
},
|
|
268
|
+
"scroll.top": { desc: "Scroll to top of page", args: [], opts: { selector: "Target specific container" } },
|
|
269
|
+
"scroll.bottom": { desc: "Scroll to bottom of page", args: [], opts: { selector: "Target specific container" } },
|
|
270
|
+
"scroll.to": {
|
|
271
|
+
desc: "Scroll element into view",
|
|
272
|
+
args: [],
|
|
273
|
+
opts: { ref: "Element ref" },
|
|
274
|
+
examples: [{ cmd: "scroll.to --ref e5", desc: "Scroll to element" }]
|
|
275
|
+
},
|
|
276
|
+
"scroll.info": { desc: "Get scroll position info", args: [], opts: { selector: "Target specific container" } },
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
page: {
|
|
280
|
+
desc: "Page inspection",
|
|
281
|
+
commands: {
|
|
282
|
+
"page.read": {
|
|
283
|
+
desc: "Get accessibility tree + visible text",
|
|
284
|
+
args: [],
|
|
285
|
+
opts: {
|
|
286
|
+
all: "Include all elements",
|
|
287
|
+
ref: "Get specific element",
|
|
288
|
+
"no-text": "Exclude visible text content",
|
|
289
|
+
depth: "Maximum tree depth (default: unlimited)",
|
|
290
|
+
compact: "Remove empty structural elements",
|
|
291
|
+
},
|
|
292
|
+
examples: [
|
|
293
|
+
{ cmd: "page.read", desc: "Interactive elements + text content" },
|
|
294
|
+
{ cmd: "page.read --all", desc: "All elements + text" },
|
|
295
|
+
{ cmd: "page.read --no-text", desc: "Interactive elements only (no text)" },
|
|
296
|
+
{ cmd: "page.read --depth 3", desc: "Limit to 3 levels deep" },
|
|
297
|
+
{ cmd: "page.read --compact", desc: "Skip empty containers" },
|
|
298
|
+
{ cmd: "page.read --depth 3 --compact", desc: "Shallow + compact (60% smaller)" },
|
|
299
|
+
{ cmd: "read", desc: "Alias" },
|
|
300
|
+
]
|
|
301
|
+
},
|
|
302
|
+
"read": { desc: "Alias for page.read", args: [], alias: "page.read" },
|
|
303
|
+
"page.text": { desc: "Extract all text from page", args: [] },
|
|
304
|
+
"page.state": { desc: "Get page state (modals, loading, etc.)", args: [] },
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
locate: {
|
|
308
|
+
desc: "Semantic element location",
|
|
309
|
+
commands: {
|
|
310
|
+
"locate.role": {
|
|
311
|
+
desc: "Find element by ARIA role",
|
|
312
|
+
args: ["role"],
|
|
313
|
+
opts: {
|
|
314
|
+
name: "Element name/text",
|
|
315
|
+
action: "Action to perform (click|fill|hover|text)",
|
|
316
|
+
value: "Value for fill action",
|
|
317
|
+
all: "Return all matches"
|
|
318
|
+
},
|
|
319
|
+
examples: [
|
|
320
|
+
{ cmd: 'locate.role button --name "Submit" --action click', desc: "Click button by name" },
|
|
321
|
+
{ cmd: 'locate.role textbox --name "Email" --action fill --value "test@test.com"', desc: "Fill input" },
|
|
322
|
+
{ cmd: 'locate.role link --all', desc: "List all links with refs" },
|
|
323
|
+
]
|
|
324
|
+
},
|
|
325
|
+
"locate.text": {
|
|
326
|
+
desc: "Find element by text content",
|
|
327
|
+
args: ["text"],
|
|
328
|
+
opts: {
|
|
329
|
+
exact: "Exact match",
|
|
330
|
+
action: "Action to perform",
|
|
331
|
+
value: "Value for fill action"
|
|
332
|
+
},
|
|
333
|
+
examples: [
|
|
334
|
+
{ cmd: 'locate.text "Sign In" --action click', desc: "Click by text" },
|
|
335
|
+
{ cmd: 'locate.text "Accept" --exact --action click', desc: "Exact text match" },
|
|
336
|
+
]
|
|
337
|
+
},
|
|
338
|
+
"locate.label": {
|
|
339
|
+
desc: "Find form field by label",
|
|
340
|
+
args: ["label"],
|
|
341
|
+
opts: {
|
|
342
|
+
action: "Action to perform",
|
|
343
|
+
value: "Value for fill action"
|
|
344
|
+
},
|
|
345
|
+
examples: [
|
|
346
|
+
{ cmd: 'locate.label "Username" --action fill --value "john"', desc: "Fill by label" },
|
|
347
|
+
]
|
|
348
|
+
},
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
wait: {
|
|
352
|
+
desc: "Waiting",
|
|
353
|
+
commands: {
|
|
354
|
+
"wait": {
|
|
355
|
+
desc: "Wait N seconds",
|
|
356
|
+
args: ["duration"],
|
|
357
|
+
examples: [{ cmd: "wait 2", desc: "Wait 2 seconds" }]
|
|
358
|
+
},
|
|
359
|
+
"wait.element": {
|
|
360
|
+
desc: "Wait for element to appear",
|
|
361
|
+
args: ["selector"],
|
|
362
|
+
opts: { timeout: "Timeout in ms" },
|
|
363
|
+
examples: [
|
|
364
|
+
{ cmd: 'wait.element ".loading"', desc: "Wait for element" },
|
|
365
|
+
{ cmd: 'wait.element "#result" --timeout 10000', desc: "With timeout" },
|
|
366
|
+
]
|
|
367
|
+
},
|
|
368
|
+
"wait.network": { desc: "Wait for network idle", args: [], opts: { timeout: "Timeout in ms" } },
|
|
369
|
+
"wait.url": {
|
|
370
|
+
desc: "Wait for URL to match",
|
|
371
|
+
args: ["pattern"],
|
|
372
|
+
opts: { timeout: "Timeout in ms" },
|
|
373
|
+
examples: [{ cmd: 'wait.url "/dashboard"', desc: "Wait for URL pattern" }]
|
|
374
|
+
},
|
|
375
|
+
"wait.dom": { desc: "Wait for DOM to stabilize", args: [], opts: { stable: "Stability window in ms (default: 100)", timeout: "Max wait time in ms" } },
|
|
376
|
+
"wait.load": { desc: "Wait for page to fully load", args: [], opts: { timeout: "Max wait time in ms (default: 30000)" } },
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
input: {
|
|
380
|
+
desc: "Input actions",
|
|
381
|
+
commands: {
|
|
382
|
+
"click": {
|
|
383
|
+
desc: "Click element or coordinates",
|
|
384
|
+
args: ["ref"],
|
|
385
|
+
opts: {
|
|
386
|
+
ref: "Element ref",
|
|
387
|
+
x: "X coordinate",
|
|
388
|
+
y: "Y coordinate",
|
|
389
|
+
button: "left|right|double|triple",
|
|
390
|
+
selector: "CSS selector",
|
|
391
|
+
index: "Which match (0-indexed) for selector",
|
|
392
|
+
},
|
|
393
|
+
examples: [
|
|
394
|
+
{ cmd: "click e5", desc: "Click by ref" },
|
|
395
|
+
{ cmd: 'click --selector ".btn"', desc: "Click by selector" },
|
|
396
|
+
{ cmd: 'click --selector ".item" --index 2', desc: "Click 3rd match" },
|
|
397
|
+
{ cmd: "click --x 100 --y 200", desc: "Click coordinates" },
|
|
398
|
+
]
|
|
399
|
+
},
|
|
400
|
+
"type": {
|
|
401
|
+
desc: "Type text (uses form.fill when --ref provided for better modal/form support)",
|
|
402
|
+
args: ["text"],
|
|
403
|
+
opts: {
|
|
404
|
+
into: "Target selector",
|
|
405
|
+
ref: "Element ref (uses JS DOM method, more reliable for modals)",
|
|
406
|
+
submit: "Press enter after",
|
|
407
|
+
clear: "Clear first",
|
|
408
|
+
method: "cdp|js (default: cdp, but ref uses JS automatically)"
|
|
409
|
+
},
|
|
410
|
+
examples: [
|
|
411
|
+
{ cmd: 'type "hello world"', desc: "Type at cursor (CDP events)" },
|
|
412
|
+
{ cmd: 'type "user@example.com" --ref e5', desc: "Type into element by ref (JS DOM)" },
|
|
413
|
+
{ cmd: 'type "search query" --submit', desc: "Type and press Enter" },
|
|
414
|
+
]
|
|
415
|
+
},
|
|
416
|
+
"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" } },
|
|
417
|
+
"key": {
|
|
418
|
+
desc: "Press key",
|
|
419
|
+
args: ["key"],
|
|
420
|
+
examples: [
|
|
421
|
+
{ cmd: "key Enter", desc: "Press Enter" },
|
|
422
|
+
{ cmd: "key Escape", desc: "Press Escape" },
|
|
423
|
+
{ cmd: "key cmd+a", desc: "Select all (Mac)" },
|
|
424
|
+
{ cmd: "key ctrl+shift+p", desc: "Key combo" },
|
|
425
|
+
]
|
|
426
|
+
},
|
|
427
|
+
"hover": { desc: "Hover over element", args: [], opts: { ref: "Element ref", x: "X coordinate", y: "Y coordinate" } },
|
|
428
|
+
"drag": { desc: "Drag between points", args: [], opts: { from: "Start x,y", to: "End x,y" } },
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
js: {
|
|
432
|
+
desc: "JavaScript execution",
|
|
433
|
+
commands: {
|
|
434
|
+
"js": {
|
|
435
|
+
desc: "Execute JavaScript (use 'return' for values)",
|
|
436
|
+
args: ["code"],
|
|
437
|
+
opts: { file: "Run JS from file" },
|
|
438
|
+
examples: [
|
|
439
|
+
{ cmd: 'js "return document.title"', desc: "Get title" },
|
|
440
|
+
{ cmd: 'js "document.body.style.background = \'red\'"', desc: "Run code" },
|
|
441
|
+
{ cmd: "js --file script.js", desc: "Run file" },
|
|
442
|
+
]
|
|
443
|
+
},
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
dev: {
|
|
447
|
+
desc: "Dev tools",
|
|
448
|
+
commands: {
|
|
449
|
+
"console": {
|
|
450
|
+
desc: "Read console messages",
|
|
451
|
+
args: [],
|
|
452
|
+
opts: { clear: "Clear after reading", stream: "Continuous output", level: "Filter by level (log,warn,error)", limit: "Max messages" },
|
|
453
|
+
examples: [
|
|
454
|
+
{ cmd: "console", desc: "Get recent messages" },
|
|
455
|
+
{ cmd: "console --level error", desc: "Only errors" },
|
|
456
|
+
{ cmd: "console --stream", desc: "Stream live" },
|
|
457
|
+
]
|
|
458
|
+
},
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
network: {
|
|
462
|
+
desc: "Network capture",
|
|
463
|
+
commands: {
|
|
464
|
+
"network": {
|
|
465
|
+
desc: "List captured network requests",
|
|
466
|
+
args: [],
|
|
467
|
+
opts: {
|
|
468
|
+
origin: "Filter by origin (domain)",
|
|
469
|
+
method: "Filter by method (GET,POST,...)",
|
|
470
|
+
status: "Filter by status (200, 4xx, 5xx)",
|
|
471
|
+
type: "Filter by content type (json, html, proto)",
|
|
472
|
+
since: "Show requests since (5m, 1h, timestamp)",
|
|
473
|
+
last: "Show last N requests",
|
|
474
|
+
"has-body": "Only requests with body",
|
|
475
|
+
"exclude-static": "Exclude images/fonts/css/js",
|
|
476
|
+
filter: "URL pattern filter",
|
|
477
|
+
format: "Output format: compact, urls, curl, raw",
|
|
478
|
+
all: "Show all (no limit)",
|
|
479
|
+
v: "Verbose output",
|
|
480
|
+
vv: "Very verbose output",
|
|
481
|
+
clear: "Clear after reading",
|
|
482
|
+
stream: "Continuous output"
|
|
483
|
+
},
|
|
484
|
+
examples: [
|
|
485
|
+
{ cmd: "network", desc: "Show recent requests" },
|
|
486
|
+
{ cmd: "network --origin api.github.com", desc: "Filter by origin" },
|
|
487
|
+
{ cmd: "network --method POST --type json", desc: "POST JSON requests" },
|
|
488
|
+
{ cmd: "network --format curl", desc: "Output as curl commands" },
|
|
489
|
+
{ cmd: "network -v", desc: "Verbose with headers" },
|
|
490
|
+
]
|
|
491
|
+
},
|
|
492
|
+
"network.get": {
|
|
493
|
+
desc: "Get full details for a request",
|
|
494
|
+
args: ["id"],
|
|
495
|
+
opts: {},
|
|
496
|
+
examples: [
|
|
497
|
+
{ cmd: "network.get r_001", desc: "Get request details" }
|
|
498
|
+
]
|
|
499
|
+
},
|
|
500
|
+
"network.body": {
|
|
501
|
+
desc: "Get response body (for piping)",
|
|
502
|
+
args: ["id"],
|
|
503
|
+
opts: { request: "Get request body instead" },
|
|
504
|
+
examples: [
|
|
505
|
+
{ cmd: "network.body r_001", desc: "Get response body" },
|
|
506
|
+
{ cmd: "network.body r_001 | jq .", desc: "Pipe JSON to jq" }
|
|
507
|
+
]
|
|
508
|
+
},
|
|
509
|
+
"network.curl": {
|
|
510
|
+
desc: "Generate curl command for request",
|
|
511
|
+
args: ["id"],
|
|
512
|
+
opts: {},
|
|
513
|
+
examples: [
|
|
514
|
+
{ cmd: "network.curl r_001", desc: "Generate curl" }
|
|
515
|
+
]
|
|
516
|
+
},
|
|
517
|
+
"network.origins": {
|
|
518
|
+
desc: "List captured origins with stats",
|
|
519
|
+
args: [],
|
|
520
|
+
opts: { "by-tab": "Group by tab" },
|
|
521
|
+
examples: [
|
|
522
|
+
{ cmd: "network.origins", desc: "List origins" }
|
|
523
|
+
]
|
|
524
|
+
},
|
|
525
|
+
"network.clear": {
|
|
526
|
+
desc: "Clear captured requests",
|
|
527
|
+
args: [],
|
|
528
|
+
opts: { before: "Clear before timestamp/duration", origin: "Clear specific origin" },
|
|
529
|
+
examples: [
|
|
530
|
+
{ cmd: "network.clear", desc: "Clear all" },
|
|
531
|
+
{ cmd: "network.clear --before 1h", desc: "Clear older than 1 hour" }
|
|
532
|
+
]
|
|
533
|
+
},
|
|
534
|
+
"network.stats": {
|
|
535
|
+
desc: "Show capture statistics",
|
|
536
|
+
args: [],
|
|
537
|
+
opts: {},
|
|
538
|
+
examples: [
|
|
539
|
+
{ cmd: "network.stats", desc: "Show stats" }
|
|
540
|
+
]
|
|
541
|
+
},
|
|
542
|
+
"network.export": {
|
|
543
|
+
desc: "Export captured requests",
|
|
544
|
+
args: [],
|
|
545
|
+
opts: { jsonl: "Export as JSONL", output: "Output file path" },
|
|
546
|
+
examples: [
|
|
547
|
+
{ cmd: "network.export --jsonl --output /tmp/requests.jsonl", desc: "Export as JSONL" }
|
|
548
|
+
]
|
|
549
|
+
},
|
|
550
|
+
"network.path": {
|
|
551
|
+
desc: "Get file paths for request data",
|
|
552
|
+
args: ["id"],
|
|
553
|
+
opts: {},
|
|
554
|
+
examples: [
|
|
555
|
+
{ cmd: "network.path r_001", desc: "Get file paths" }
|
|
556
|
+
]
|
|
557
|
+
},
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
health: {
|
|
561
|
+
desc: "Health checks",
|
|
562
|
+
commands: {
|
|
563
|
+
"health": {
|
|
564
|
+
desc: "Wait for URL or element",
|
|
565
|
+
args: [],
|
|
566
|
+
opts: { url: "URL to check (expects 200)", selector: "CSS selector to wait for", expect: "Expected status code (default: 200)", timeout: "Timeout in ms" },
|
|
567
|
+
examples: [
|
|
568
|
+
{ cmd: 'health --url "https://api.example.com"', desc: "Check URL" },
|
|
569
|
+
{ cmd: 'health --selector ".loaded"', desc: "Wait for element" },
|
|
570
|
+
]
|
|
571
|
+
},
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
smoke: {
|
|
575
|
+
desc: "Smoke testing",
|
|
576
|
+
commands: {
|
|
577
|
+
"smoke": { desc: "Run smoke tests on URLs", args: [], opts: { urls: "URLs to test (space-separated)", routes: "Route group from config", screenshot: "Directory to save screenshots", "fail-fast": "Stop on first error" } },
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
dialog: {
|
|
581
|
+
desc: "Browser dialog handling",
|
|
582
|
+
commands: {
|
|
583
|
+
"dialog.accept": { desc: "Accept current dialog", args: [], opts: { text: "Text for prompt input" } },
|
|
584
|
+
"dialog.dismiss": {
|
|
585
|
+
desc: "Dismiss current dialog",
|
|
586
|
+
args: [],
|
|
587
|
+
opts: { all: "Dismiss all dialogs repeatedly" },
|
|
588
|
+
examples: [
|
|
589
|
+
{ cmd: "dialog.dismiss", desc: "Dismiss once" },
|
|
590
|
+
{ cmd: "dialog.dismiss --all", desc: "Dismiss all" },
|
|
591
|
+
]
|
|
592
|
+
},
|
|
593
|
+
"dialog.info": { desc: "Get current dialog info", args: [] },
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
emulate: {
|
|
597
|
+
desc: "Device/network emulation",
|
|
598
|
+
commands: {
|
|
599
|
+
"emulate.network": { desc: "Emulate network conditions", args: ["preset"], opts: {} },
|
|
600
|
+
"emulate.cpu": { desc: "CPU throttling (rate >= 1)", args: ["rate"], opts: {} },
|
|
601
|
+
"emulate.geo": { desc: "Override geolocation", args: [], opts: { lat: "Latitude", lon: "Longitude", accuracy: "Accuracy in meters (default: 100)", clear: "Clear override" } },
|
|
602
|
+
"emulate.device": {
|
|
603
|
+
desc: "Emulate mobile device",
|
|
604
|
+
args: ["device"],
|
|
605
|
+
opts: { list: "List available devices" },
|
|
606
|
+
examples: [
|
|
607
|
+
{ cmd: 'emulate.device "iPhone 14"', desc: "Emulate iPhone" },
|
|
608
|
+
{ cmd: 'emulate.device "Pixel 7"', desc: "Emulate Pixel" },
|
|
609
|
+
{ cmd: "emulate.device --list", desc: "Show all devices" },
|
|
610
|
+
{ cmd: 'emulate.device "reset"', desc: "Return to desktop" },
|
|
611
|
+
]
|
|
612
|
+
},
|
|
613
|
+
"emulate.viewport": {
|
|
614
|
+
desc: "Set custom viewport",
|
|
615
|
+
args: [],
|
|
616
|
+
opts: { width: "Viewport width", height: "Viewport height", scale: "Device scale factor", mobile: "Enable mobile mode" },
|
|
617
|
+
examples: [
|
|
618
|
+
{ cmd: "emulate.viewport --width 375 --height 812", desc: "iPhone size" },
|
|
619
|
+
{ cmd: "emulate.viewport --width 1920 --height 1080 --scale 2", desc: "Retina display" },
|
|
620
|
+
]
|
|
621
|
+
},
|
|
622
|
+
"emulate.touch": {
|
|
623
|
+
desc: "Enable/disable touch emulation",
|
|
624
|
+
args: [],
|
|
625
|
+
opts: { enabled: "Enable touch (default: true)" },
|
|
626
|
+
examples: [
|
|
627
|
+
{ cmd: "emulate.touch", desc: "Enable touch" },
|
|
628
|
+
{ cmd: "emulate.touch --enabled false", desc: "Disable touch" },
|
|
629
|
+
]
|
|
630
|
+
},
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
form: {
|
|
634
|
+
desc: "Form automation",
|
|
635
|
+
commands: {
|
|
636
|
+
"form.fill": { desc: "Batch fill form fields", args: [], opts: { data: "JSON array of {ref, value}" } },
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
perf: {
|
|
640
|
+
desc: "Performance tracing",
|
|
641
|
+
commands: {
|
|
642
|
+
"perf.start": { desc: "Start performance trace", args: [], opts: { categories: "Trace categories (comma-separated)" } },
|
|
643
|
+
"perf.stop": { desc: "Stop trace and get metrics", args: [] },
|
|
644
|
+
"perf.metrics": { desc: "Get current performance metrics", args: [] },
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
upload: {
|
|
648
|
+
desc: "File upload",
|
|
649
|
+
commands: {
|
|
650
|
+
"upload": {
|
|
651
|
+
desc: "Upload file(s) to input",
|
|
652
|
+
args: [],
|
|
653
|
+
opts: { ref: "Element ref", files: "File path(s) comma-separated" },
|
|
654
|
+
examples: [{ cmd: 'upload --ref e5 --files "/path/to/file.pdf"', desc: "Upload file" }]
|
|
655
|
+
},
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
frame: {
|
|
659
|
+
desc: "Iframe handling",
|
|
660
|
+
commands: {
|
|
661
|
+
"frame.list": {
|
|
662
|
+
desc: "List all frames in page",
|
|
663
|
+
args: [],
|
|
664
|
+
examples: [{ cmd: "frame.list", desc: "Show frame tree" }]
|
|
665
|
+
},
|
|
666
|
+
"frame.switch": {
|
|
667
|
+
desc: "Switch to iframe context",
|
|
668
|
+
args: [],
|
|
669
|
+
opts: {
|
|
670
|
+
selector: "Frame CSS selector",
|
|
671
|
+
name: "Frame name attribute",
|
|
672
|
+
index: "Frame index (0-based)"
|
|
673
|
+
},
|
|
674
|
+
examples: [
|
|
675
|
+
{ cmd: 'frame.switch --selector "#payment-iframe"', desc: "Switch by selector" },
|
|
676
|
+
{ cmd: 'frame.switch --name "payment"', desc: "Switch by name" },
|
|
677
|
+
{ cmd: "frame.switch --index 0", desc: "Switch to first frame" },
|
|
678
|
+
]
|
|
679
|
+
},
|
|
680
|
+
"frame.main": {
|
|
681
|
+
desc: "Return to main frame",
|
|
682
|
+
args: [],
|
|
683
|
+
examples: [{ cmd: "frame.main", desc: "Exit iframe context" }]
|
|
684
|
+
},
|
|
685
|
+
"frame.js": {
|
|
686
|
+
desc: "Execute JS in specific frame",
|
|
687
|
+
args: ["code"],
|
|
688
|
+
opts: { id: "Frame ID from frame.list", file: "Run JS from file" },
|
|
689
|
+
examples: [
|
|
690
|
+
{ cmd: 'frame.js "return document.title" --id frame1', desc: "JS in specific frame" },
|
|
691
|
+
]
|
|
692
|
+
},
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
cookie: {
|
|
696
|
+
desc: "Cookie management",
|
|
697
|
+
commands: {
|
|
698
|
+
"cookie.list": {
|
|
699
|
+
desc: "List all cookies for current tab's domain",
|
|
700
|
+
args: [],
|
|
701
|
+
examples: [{ cmd: "cookie.list", desc: "Show all cookies" }]
|
|
702
|
+
},
|
|
703
|
+
"cookie.get": { desc: "Get specific cookie", args: [], opts: { name: "Cookie name" } },
|
|
704
|
+
"cookie.set": {
|
|
705
|
+
desc: "Set a cookie",
|
|
706
|
+
args: [],
|
|
707
|
+
opts: { name: "Cookie name", value: "Cookie value", expires: "Expiry date (optional)" },
|
|
708
|
+
examples: [{ cmd: 'cookie.set --name "session" --value "abc123"', desc: "Set cookie" }]
|
|
709
|
+
},
|
|
710
|
+
"cookie.clear": {
|
|
711
|
+
desc: "Clear cookies",
|
|
712
|
+
args: [],
|
|
713
|
+
opts: { name: "Specific cookie (optional)", all: "Clear all for domain" },
|
|
714
|
+
examples: [
|
|
715
|
+
{ cmd: 'cookie.clear --name "session"', desc: "Clear one" },
|
|
716
|
+
{ cmd: "cookie.clear --all", desc: "Clear all" },
|
|
717
|
+
]
|
|
718
|
+
},
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
search: {
|
|
722
|
+
desc: "Text search",
|
|
723
|
+
commands: {
|
|
724
|
+
"search": {
|
|
725
|
+
desc: "Search for text in page",
|
|
726
|
+
args: ["term"],
|
|
727
|
+
opts: { "case-sensitive": "Case-sensitive match", limit: "Max results" },
|
|
728
|
+
examples: [
|
|
729
|
+
{ cmd: 'search "login"', desc: "Find text" },
|
|
730
|
+
{ cmd: 'search "Error" --case-sensitive', desc: "Case sensitive" },
|
|
731
|
+
{ cmd: 'find "button"', desc: "Using alias" },
|
|
732
|
+
]
|
|
733
|
+
},
|
|
734
|
+
"find": { desc: "Alias for search", args: ["term"], alias: "search" },
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
batch: {
|
|
738
|
+
desc: "Batch execution",
|
|
739
|
+
commands: {
|
|
740
|
+
"batch": {
|
|
741
|
+
desc: "Execute multiple actions",
|
|
742
|
+
args: [],
|
|
743
|
+
opts: { actions: "JSON array of actions", file: "Path to actions JSON file" },
|
|
744
|
+
examples: [
|
|
745
|
+
{ cmd: 'batch --actions \'[{"type":"click","ref":"e1"},{"type":"wait","ms":500}]\'', desc: "Inline actions" },
|
|
746
|
+
{ cmd: "batch --file workflow.json", desc: "From file" },
|
|
747
|
+
]
|
|
748
|
+
},
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
zoom: {
|
|
752
|
+
desc: "Zoom control",
|
|
753
|
+
commands: {
|
|
754
|
+
"zoom": {
|
|
755
|
+
desc: "Get or set zoom level",
|
|
756
|
+
args: [],
|
|
757
|
+
opts: { level: "Zoom level (e.g., 1.5 for 150%)", reset: "Reset to default zoom" },
|
|
758
|
+
examples: [
|
|
759
|
+
{ cmd: "zoom", desc: "Get current zoom" },
|
|
760
|
+
{ cmd: "zoom 1.5", desc: "Set to 150%" },
|
|
761
|
+
{ cmd: "zoom --reset", desc: "Reset to 100%" },
|
|
762
|
+
]
|
|
763
|
+
},
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
resize: {
|
|
767
|
+
desc: "Window management",
|
|
768
|
+
commands: {
|
|
769
|
+
"resize": {
|
|
770
|
+
desc: "Resize browser window",
|
|
771
|
+
args: [],
|
|
772
|
+
opts: { width: "Window width", height: "Window height" },
|
|
773
|
+
examples: [{ cmd: "resize --width 1280 --height 720", desc: "Set size" }]
|
|
774
|
+
},
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
bookmark: {
|
|
778
|
+
desc: "Bookmark management",
|
|
779
|
+
commands: {
|
|
780
|
+
"bookmark.add": { desc: "Bookmark current page", args: [], opts: { folder: "Folder name" } },
|
|
781
|
+
"bookmark.remove": { desc: "Remove bookmark for current page", args: [] },
|
|
782
|
+
"bookmark.list": { desc: "List bookmarks", args: [], opts: { folder: "Folder name", limit: "Max results" } },
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
history: {
|
|
786
|
+
desc: "Browser history",
|
|
787
|
+
commands: {
|
|
788
|
+
"history.list": {
|
|
789
|
+
desc: "Recent history",
|
|
790
|
+
args: [],
|
|
791
|
+
opts: { limit: "Max results" },
|
|
792
|
+
examples: [{ cmd: "history.list --limit 20", desc: "Last 20 items" }]
|
|
793
|
+
},
|
|
794
|
+
"history.search": {
|
|
795
|
+
desc: "Search history",
|
|
796
|
+
args: ["query"],
|
|
797
|
+
examples: [{ cmd: 'history.search "github"', desc: "Search history" }]
|
|
798
|
+
},
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
window: {
|
|
802
|
+
desc: "Window management (isolate agent from your browsing)",
|
|
803
|
+
commands: {
|
|
804
|
+
"window.new": {
|
|
805
|
+
desc: "Create new browser window",
|
|
806
|
+
args: ["url"],
|
|
807
|
+
opts: {
|
|
808
|
+
width: "Window width",
|
|
809
|
+
height: "Window height",
|
|
810
|
+
incognito: "Open incognito window",
|
|
811
|
+
unfocused: "Don't focus the new window"
|
|
812
|
+
},
|
|
813
|
+
examples: [
|
|
814
|
+
{ cmd: 'window.new "https://example.com"', desc: "New window with URL" },
|
|
815
|
+
{ cmd: 'window.new --width 1280 --height 720', desc: "Sized window" },
|
|
816
|
+
{ cmd: 'window.new --incognito', desc: "Incognito window" },
|
|
817
|
+
]
|
|
818
|
+
},
|
|
819
|
+
"window.list": {
|
|
820
|
+
desc: "List all browser windows",
|
|
821
|
+
args: [],
|
|
822
|
+
opts: { tabs: "Include tab details" },
|
|
823
|
+
examples: [{ cmd: "window.list", desc: "Show all windows" }]
|
|
824
|
+
},
|
|
825
|
+
"window.focus": {
|
|
826
|
+
desc: "Focus a window by ID",
|
|
827
|
+
args: ["id"],
|
|
828
|
+
examples: [{ cmd: "window.focus 123", desc: "Focus window" }]
|
|
829
|
+
},
|
|
830
|
+
"window.close": {
|
|
831
|
+
desc: "Close a window by ID",
|
|
832
|
+
args: ["id"],
|
|
833
|
+
examples: [{ cmd: "window.close 123", desc: "Close window" }]
|
|
834
|
+
},
|
|
835
|
+
"window.resize": {
|
|
836
|
+
desc: "Resize or reposition a window",
|
|
837
|
+
args: [],
|
|
838
|
+
opts: {
|
|
839
|
+
id: "Window ID (required)",
|
|
840
|
+
width: "Window width",
|
|
841
|
+
height: "Window height",
|
|
842
|
+
left: "Window X position",
|
|
843
|
+
top: "Window Y position",
|
|
844
|
+
state: "Window state: normal, minimized, maximized, fullscreen"
|
|
845
|
+
},
|
|
846
|
+
examples: [
|
|
847
|
+
{ cmd: "window.resize --id 123 --width 1920 --height 1080", desc: "Resize" },
|
|
848
|
+
{ cmd: "window.resize --id 123 --left 0 --top 0", desc: "Move to corner" },
|
|
849
|
+
{ cmd: "window.resize --id 123 --state maximized", desc: "Maximize" },
|
|
850
|
+
]
|
|
851
|
+
},
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
const HELP_TOPICS = {
|
|
857
|
+
refs: {
|
|
858
|
+
title: "Element References",
|
|
859
|
+
content: `Element refs (e1, e2, e3...) are stable identifiers from page.read.
|
|
860
|
+
|
|
861
|
+
Usage:
|
|
862
|
+
1. Run page.read to get the accessibility tree
|
|
863
|
+
2. Find elements with refs like [e5] button "Submit"
|
|
864
|
+
3. Use the ref: click e5, scroll.to --ref e5, type "text" --ref e5
|
|
865
|
+
|
|
866
|
+
Refs are more reliable than selectors for dynamic pages.`
|
|
867
|
+
},
|
|
868
|
+
selectors: {
|
|
869
|
+
title: "CSS Selectors",
|
|
870
|
+
content: `Use CSS selectors when you know the element's structure.
|
|
871
|
+
|
|
872
|
+
Examples:
|
|
873
|
+
click --selector "#submit-btn"
|
|
874
|
+
click --selector ".btn-primary"
|
|
875
|
+
click --selector "[data-testid='login']"
|
|
876
|
+
click --selector "button:contains('Submit')"
|
|
877
|
+
wait.element ".loading-spinner"
|
|
878
|
+
|
|
879
|
+
Use --index to select from multiple matches:
|
|
880
|
+
click --selector ".item" --index 2 # 3rd match (0-indexed)`
|
|
881
|
+
},
|
|
882
|
+
cookies: {
|
|
883
|
+
title: "Cookie Management",
|
|
884
|
+
content: `Cookies are scoped to the current tab's domain.
|
|
885
|
+
|
|
886
|
+
Commands:
|
|
887
|
+
cookie.list List all cookies
|
|
888
|
+
cookie.get --name X Get specific cookie
|
|
889
|
+
cookie.set Set a cookie
|
|
890
|
+
cookie.clear Clear cookies
|
|
891
|
+
|
|
892
|
+
Notes:
|
|
893
|
+
- HttpOnly cookies are accessible
|
|
894
|
+
- Use --expires with ISO date: "2025-12-31T00:00:00Z"`
|
|
895
|
+
},
|
|
896
|
+
batch: {
|
|
897
|
+
title: "Batch Execution",
|
|
898
|
+
content: `Run multiple actions in sequence.
|
|
899
|
+
|
|
900
|
+
JSON format:
|
|
901
|
+
[
|
|
902
|
+
{"type": "click", "ref": "e1"},
|
|
903
|
+
{"type": "wait", "ms": 500},
|
|
904
|
+
{"type": "type", "text": "hello"},
|
|
905
|
+
{"type": "key", "key": "Enter"}
|
|
906
|
+
]
|
|
907
|
+
|
|
908
|
+
Supported types: click, type, key, wait, scroll, screenshot, navigate
|
|
909
|
+
|
|
910
|
+
Options:
|
|
911
|
+
--actions '[...]' Inline JSON
|
|
912
|
+
--file workflow.json Load from file`
|
|
913
|
+
},
|
|
914
|
+
screenshots: {
|
|
915
|
+
title: "Screenshots",
|
|
916
|
+
content: `Capture screenshots with various options.
|
|
917
|
+
|
|
918
|
+
Commands:
|
|
919
|
+
screenshot --output file.png Basic screenshot
|
|
920
|
+
screenshot --annotate --output file.png With element labels
|
|
921
|
+
screenshot --fullpage --output file.png Full page capture
|
|
922
|
+
screenshot --annotate --fullpage --output file.png Full page with labels
|
|
923
|
+
snap Auto-save to /tmp
|
|
924
|
+
|
|
925
|
+
Options:
|
|
926
|
+
--output Save path
|
|
927
|
+
--annotate Draw element refs
|
|
928
|
+
--fullpage Capture entire page
|
|
929
|
+
--max-height Max height for fullpage (default: 4000)`
|
|
930
|
+
},
|
|
931
|
+
automation: {
|
|
932
|
+
title: "Automation Patterns",
|
|
933
|
+
content: `Common automation patterns:
|
|
934
|
+
|
|
935
|
+
Wait for page load:
|
|
936
|
+
navigate "https://example.com"
|
|
937
|
+
wait.load
|
|
938
|
+
|
|
939
|
+
Fill a form:
|
|
940
|
+
type "user@email.com" --into "#email"
|
|
941
|
+
type "password123" --into "#password"
|
|
942
|
+
click --selector "button[type=submit]"
|
|
943
|
+
|
|
944
|
+
Wait for dynamic content:
|
|
945
|
+
click e5
|
|
946
|
+
wait.element ".results"
|
|
947
|
+
page.read
|
|
948
|
+
|
|
949
|
+
Scroll and capture:
|
|
950
|
+
scroll.bottom
|
|
951
|
+
screenshot --fullpage --output full.png`
|
|
952
|
+
},
|
|
953
|
+
windows: {
|
|
954
|
+
title: "Window Isolation",
|
|
955
|
+
content: `Keep agent work separate from your browsing.
|
|
956
|
+
|
|
957
|
+
Create a dedicated window:
|
|
958
|
+
surf window.new "https://example.com"
|
|
959
|
+
# Returns: Window 123 (tab 456)
|
|
960
|
+
# Use --window-id 123 to target this window
|
|
961
|
+
|
|
962
|
+
All commands in that window:
|
|
963
|
+
surf navigate "https://other.com" --window-id 123
|
|
964
|
+
surf read --window-id 123
|
|
965
|
+
surf click e5 --window-id 123
|
|
966
|
+
surf screenshot --output /tmp/shot.png --window-id 123
|
|
967
|
+
|
|
968
|
+
Manage windows:
|
|
969
|
+
surf window.list # List all windows
|
|
970
|
+
surf window.list --tabs # Include tab details
|
|
971
|
+
surf window.focus 123 # Bring window to front
|
|
972
|
+
surf window.close 123 # Close when done
|
|
973
|
+
|
|
974
|
+
Tips:
|
|
975
|
+
- Agent commands won't affect your active browser window
|
|
976
|
+
- If window has no usable tabs, one is auto-created
|
|
977
|
+
- Use window.new --incognito for isolated cookies`
|
|
978
|
+
},
|
|
979
|
+
semantic: {
|
|
980
|
+
title: "Semantic Locators",
|
|
981
|
+
content: `Find elements by role, text, or label instead of refs or selectors.
|
|
982
|
+
|
|
983
|
+
By ARIA role:
|
|
984
|
+
locate.role button --name "Submit" --action click
|
|
985
|
+
locate.role textbox --name "Email" --action fill --value "test@test.com"
|
|
986
|
+
locate.role link --all # List all links
|
|
987
|
+
|
|
988
|
+
By text content:
|
|
989
|
+
locate.text "Sign In" --action click
|
|
990
|
+
locate.text "Accept" --exact --action click # Exact match
|
|
991
|
+
|
|
992
|
+
By form label:
|
|
993
|
+
locate.label "Username" --action fill --value "john"
|
|
994
|
+
locate.label "Password" --action fill --value "secret"
|
|
995
|
+
|
|
996
|
+
Available actions: click, fill, hover, text
|
|
997
|
+
Without --action, returns the ref for later use.`
|
|
998
|
+
},
|
|
999
|
+
frames: {
|
|
1000
|
+
title: "Iframe Navigation",
|
|
1001
|
+
content: `Work with embedded iframes.
|
|
1002
|
+
|
|
1003
|
+
List frames:
|
|
1004
|
+
frame.list # Show frame tree with IDs
|
|
1005
|
+
|
|
1006
|
+
Switch context:
|
|
1007
|
+
frame.switch --selector "#payment-iframe"
|
|
1008
|
+
frame.switch --name "checkout"
|
|
1009
|
+
frame.switch --index 0 # First iframe
|
|
1010
|
+
|
|
1011
|
+
Return to main:
|
|
1012
|
+
frame.main
|
|
1013
|
+
|
|
1014
|
+
Execute JS in frame:
|
|
1015
|
+
frame.js "return document.title" --id frame1
|
|
1016
|
+
|
|
1017
|
+
After frame.switch, subsequent commands target that frame context.`
|
|
1018
|
+
},
|
|
1019
|
+
devices: {
|
|
1020
|
+
title: "Device Emulation",
|
|
1021
|
+
content: `Test responsive designs and mobile views.
|
|
1022
|
+
|
|
1023
|
+
Emulate a device:
|
|
1024
|
+
emulate.device "iPhone 14"
|
|
1025
|
+
emulate.device "Pixel 7"
|
|
1026
|
+
emulate.device --list # Show all devices
|
|
1027
|
+
emulate.device "reset" # Return to desktop
|
|
1028
|
+
|
|
1029
|
+
Custom viewport:
|
|
1030
|
+
emulate.viewport --width 375 --height 812
|
|
1031
|
+
emulate.viewport --width 1920 --height 1080 --scale 2
|
|
1032
|
+
|
|
1033
|
+
Touch events:
|
|
1034
|
+
emulate.touch # Enable touch
|
|
1035
|
+
emulate.touch --enabled false # Disable
|
|
1036
|
+
|
|
1037
|
+
Popular devices: iPhone 14, iPhone SE, iPad, iPad Pro,
|
|
1038
|
+
Pixel 7, Galaxy S23, Nest Hub`
|
|
1039
|
+
},
|
|
1040
|
+
optimization: {
|
|
1041
|
+
title: "Token Optimization",
|
|
1042
|
+
content: `Reduce output size for LLM efficiency.
|
|
1043
|
+
|
|
1044
|
+
Limit tree depth:
|
|
1045
|
+
page.read --depth 3 # Max 3 levels deep
|
|
1046
|
+
|
|
1047
|
+
Skip empty containers:
|
|
1048
|
+
page.read --compact # Remove empty structural elements
|
|
1049
|
+
|
|
1050
|
+
Combine for best results:
|
|
1051
|
+
page.read --depth 3 --compact # ~60% smaller output
|
|
1052
|
+
|
|
1053
|
+
Filter to interactive only:
|
|
1054
|
+
page.read # Default: interactive elements only
|
|
1055
|
+
page.read --all # Include all elements
|
|
1056
|
+
|
|
1057
|
+
Exclude text content:
|
|
1058
|
+
page.read --no-text # Skip visible text section`
|
|
1059
|
+
},
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
const ALL_SOCKET_TOOLS = [
|
|
1063
|
+
"ai", "screenshot", "navigate",
|
|
1064
|
+
"form_input", "find_and_type", "autocomplete", "set_value", "smart_type",
|
|
1065
|
+
"scroll_to_position", "get_scroll_info", "close_dialogs", "page_state",
|
|
1066
|
+
"javascript_tool", "health", "smoke",
|
|
1067
|
+
"click_type", "click_type_submit", "type", "key", "type_submit",
|
|
1068
|
+
"scroll", "scroll_to", "hover", "left_click_drag", "drag", "wait",
|
|
1069
|
+
"computer",
|
|
1070
|
+
"page.read", "page.text", "page.state",
|
|
1071
|
+
"locate.role", "locate.text", "locate.label",
|
|
1072
|
+
"tab.list", "tab.new", "tab.switch", "tab.close", "tab.name", "tab.unname", "tab.named",
|
|
1073
|
+
"tab.group", "tab.ungroup", "tab.groups", "tab.reload",
|
|
1074
|
+
"scroll.top", "scroll.bottom", "scroll.to", "scroll.info",
|
|
1075
|
+
"wait.element", "wait.network", "wait.url", "wait.dom", "wait.load",
|
|
1076
|
+
"click", "hover", "drag",
|
|
1077
|
+
"js", "console", "network",
|
|
1078
|
+
"network.get", "network.body", "network.curl", "network.origins",
|
|
1079
|
+
"network.clear", "network.stats", "network.export", "network.path",
|
|
1080
|
+
"dialog.accept", "dialog.dismiss", "dialog.info",
|
|
1081
|
+
"emulate.network", "emulate.cpu", "emulate.geo", "emulate.device", "emulate.viewport", "emulate.touch",
|
|
1082
|
+
"form.fill",
|
|
1083
|
+
"perf.start", "perf.stop", "perf.metrics",
|
|
1084
|
+
"upload",
|
|
1085
|
+
"frame.list", "frame.switch", "frame.main", "frame.js",
|
|
1086
|
+
"cookie.list", "cookie.get", "cookie.set", "cookie.clear",
|
|
1087
|
+
"search", "batch",
|
|
1088
|
+
"zoom", "resize",
|
|
1089
|
+
"back", "forward",
|
|
1090
|
+
"bookmark.add", "bookmark.remove", "bookmark.list",
|
|
1091
|
+
"history.list", "history.search",
|
|
1092
|
+
"window.new", "window.list", "window.focus", "window.close", "window.resize",
|
|
1093
|
+
];
|
|
1094
|
+
|
|
1095
|
+
// See also suggestions for related commands
|
|
1096
|
+
const SEE_ALSO = {
|
|
1097
|
+
"click": ["locate.role", "locate.text", "page.read"],
|
|
1098
|
+
"type": ["locate.label", "form.fill", "smart_type"],
|
|
1099
|
+
"page.read": ["--depth for smaller output", "--compact to skip empty containers", "page.text"],
|
|
1100
|
+
"locate.role": ["locate.text", "locate.label", "click --selector"],
|
|
1101
|
+
"locate.text": ["locate.role", "locate.label", "search"],
|
|
1102
|
+
"locate.label": ["locate.role", "form.fill"],
|
|
1103
|
+
"tab.list": ["window.list"],
|
|
1104
|
+
"tab.new": ["window.new for isolation"],
|
|
1105
|
+
"window.new": ["window.list"],
|
|
1106
|
+
"window.list": ["tab.list"],
|
|
1107
|
+
"frame.list": ["frame.switch", "frame.main"],
|
|
1108
|
+
"frame.switch": ["frame.list", "frame.main", "frame.js"],
|
|
1109
|
+
"frame.main": ["frame.list", "frame.switch"],
|
|
1110
|
+
"frame.js": ["frame.switch", "js"],
|
|
1111
|
+
"emulate.network": ["emulate.device", "emulate.cpu"],
|
|
1112
|
+
"emulate.device": ["emulate.viewport", "emulate.touch"],
|
|
1113
|
+
"emulate.viewport": ["emulate.device", "emulate.touch"],
|
|
1114
|
+
"emulate.touch": ["emulate.device", "emulate.viewport"],
|
|
1115
|
+
"emulate.cpu": ["emulate.network", "perf.metrics"],
|
|
1116
|
+
"perf.start": ["perf.stop", "perf.metrics"],
|
|
1117
|
+
"perf.stop": ["perf.start", "perf.metrics"],
|
|
1118
|
+
"perf.metrics": ["perf.start", "console", "network"],
|
|
1119
|
+
"navigate": ["wait.load", "page.read"],
|
|
1120
|
+
"screenshot": ["page.read", "scroll.bottom for fullpage"],
|
|
1121
|
+
"search": ["locate.text", "page.read"],
|
|
1122
|
+
"wait.element": ["wait.load", "wait.network"],
|
|
1123
|
+
"wait.load": ["wait.element", "wait.network"],
|
|
1124
|
+
"wait.network": ["wait.load", "wait.element"],
|
|
1125
|
+
"scroll.to": ["click", "page.read"],
|
|
1126
|
+
"console": ["network", "perf.metrics"],
|
|
1127
|
+
"network": ["console", "network.get"],
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
const showBasicHelp = () => {
|
|
1131
|
+
console.log(`surf v${VERSION} - Browser automation CLI
|
|
1132
|
+
|
|
1133
|
+
Usage: surf <command> [args] [options]
|
|
1134
|
+
|
|
1135
|
+
Common Commands:
|
|
1136
|
+
navigate <url> Go to URL (alias: go)
|
|
1137
|
+
click <ref> Click element by ref or selector
|
|
1138
|
+
type <text> Type text at cursor or into element
|
|
1139
|
+
screenshot Capture screenshot (alias: snap)
|
|
1140
|
+
page.read Get page accessibility tree (alias: read)
|
|
1141
|
+
locate.role <role> Find element by ARIA role
|
|
1142
|
+
search <term> Search for text in page (alias: find)
|
|
1143
|
+
window.new <url> Create isolated browser window
|
|
1144
|
+
wait <seconds> Wait N seconds
|
|
1145
|
+
|
|
1146
|
+
Quick Examples:
|
|
1147
|
+
surf go "https://example.com"
|
|
1148
|
+
surf read
|
|
1149
|
+
surf click e5
|
|
1150
|
+
surf type "hello" --submit
|
|
1151
|
+
surf locate.role button --name "Submit" --action click
|
|
1152
|
+
surf read --depth 3 --compact
|
|
1153
|
+
surf emulate.device "iPhone 14"
|
|
1154
|
+
surf window.new "https://example.com" && surf --window-id 123 go "https://other.com"
|
|
1155
|
+
|
|
1156
|
+
More Help:
|
|
1157
|
+
surf --help-full All commands
|
|
1158
|
+
surf --help-topic <topic> Topic guide (refs, semantic, frames, devices...)
|
|
1159
|
+
surf <command> --help Command details
|
|
1160
|
+
surf --find <query> Search for commands
|
|
1161
|
+
surf --about <topic> Learn about a topic
|
|
1162
|
+
`);
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
const showFullHelp = () => {
|
|
1166
|
+
console.log(`surf v${VERSION} - Browser automation CLI
|
|
1167
|
+
|
|
1168
|
+
Usage: surf <command> [args] [options]
|
|
1169
|
+
|
|
1170
|
+
`);
|
|
1171
|
+
for (const [groupName, group] of Object.entries(TOOLS)) {
|
|
1172
|
+
console.log(`${groupName.toUpperCase()} - ${group.desc}`);
|
|
1173
|
+
for (const [cmd, info] of Object.entries(group.commands)) {
|
|
1174
|
+
if (info.alias) continue;
|
|
1175
|
+
const argStr = info.args?.length ? `<${info.args.join("> <")}>` : "";
|
|
1176
|
+
const line = ` ${cmd} ${argStr}`.padEnd(32);
|
|
1177
|
+
console.log(`${line}${info.desc}`);
|
|
1178
|
+
}
|
|
1179
|
+
console.log();
|
|
1180
|
+
}
|
|
1181
|
+
console.log(`Aliases: snap -> screenshot, read -> page.read, find -> search, go -> navigate
|
|
1182
|
+
|
|
1183
|
+
Options:
|
|
1184
|
+
--tab-id <id> Target specific tab
|
|
1185
|
+
--window-id <id> Target specific window (isolate from your browsing)
|
|
1186
|
+
--json Output raw JSON
|
|
1187
|
+
--auto-capture On error: capture screenshot + console to /tmp
|
|
1188
|
+
--soft-fail On error: warn and exit 0 (for non-critical commands)
|
|
1189
|
+
|
|
1190
|
+
Script Mode:
|
|
1191
|
+
surf --script <file> Run workflow from JSON
|
|
1192
|
+
surf --script <file> --dry-run
|
|
1193
|
+
`);
|
|
1194
|
+
};
|
|
1195
|
+
|
|
1196
|
+
const showHelpTopic = (topic) => {
|
|
1197
|
+
const t = HELP_TOPICS[topic];
|
|
1198
|
+
if (!t) {
|
|
1199
|
+
console.error(`Unknown topic: ${topic}`);
|
|
1200
|
+
console.error(`Available topics: ${Object.keys(HELP_TOPICS).join(", ")}`);
|
|
1201
|
+
process.exit(1);
|
|
1202
|
+
}
|
|
1203
|
+
console.log(`\n${t.title}\n${"=".repeat(t.title.length)}\n\n${t.content}\n`);
|
|
1204
|
+
};
|
|
1205
|
+
|
|
1206
|
+
const showGroupHelp = (groupName) => {
|
|
1207
|
+
const group = TOOLS[groupName];
|
|
1208
|
+
if (!group) {
|
|
1209
|
+
console.error(`Unknown group: ${groupName}`);
|
|
1210
|
+
console.error(`Available groups: ${Object.keys(TOOLS).join(", ")}`);
|
|
1211
|
+
process.exit(1);
|
|
1212
|
+
}
|
|
1213
|
+
console.log(`\n${groupName} - ${group.desc}\n`);
|
|
1214
|
+
for (const [cmd, info] of Object.entries(group.commands)) {
|
|
1215
|
+
if (info.alias) {
|
|
1216
|
+
console.log(` ${cmd} -> ${info.alias}\n`);
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
const argStr = info.args?.length ? `<${info.args.join("> <")}>` : "";
|
|
1220
|
+
console.log(` ${cmd} ${argStr}`);
|
|
1221
|
+
console.log(` ${info.desc}`);
|
|
1222
|
+
if (info.opts) {
|
|
1223
|
+
for (const [opt, desc] of Object.entries(info.opts)) {
|
|
1224
|
+
console.log(` --${opt.padEnd(14)} ${desc}`);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
if (info.examples?.length) {
|
|
1228
|
+
console.log(" Examples:");
|
|
1229
|
+
for (const ex of info.examples) {
|
|
1230
|
+
console.log(` surf ${ex.cmd}`);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
console.log();
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
const showToolHelp = (toolName) => {
|
|
1238
|
+
for (const [groupName, group] of Object.entries(TOOLS)) {
|
|
1239
|
+
const info = group.commands[toolName];
|
|
1240
|
+
if (info) {
|
|
1241
|
+
if (info.alias) {
|
|
1242
|
+
console.log(`\n ${toolName} -> ${info.alias}\n`);
|
|
1243
|
+
showToolHelp(info.alias);
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
const argStr = info.args?.length ? `<${info.args.join("> <")}>` : "";
|
|
1247
|
+
console.log(`\n${toolName} - ${info.desc}\n`);
|
|
1248
|
+
console.log(`Usage: surf ${toolName} ${argStr}\n`);
|
|
1249
|
+
if (info.args?.length) {
|
|
1250
|
+
console.log("Arguments:");
|
|
1251
|
+
for (const arg of info.args) {
|
|
1252
|
+
console.log(` <${arg}>`);
|
|
1253
|
+
}
|
|
1254
|
+
console.log();
|
|
1255
|
+
}
|
|
1256
|
+
if (info.opts) {
|
|
1257
|
+
console.log("Options:");
|
|
1258
|
+
for (const [opt, desc] of Object.entries(info.opts)) {
|
|
1259
|
+
console.log(` --${opt.padEnd(18)} ${desc}`);
|
|
1260
|
+
}
|
|
1261
|
+
console.log();
|
|
1262
|
+
}
|
|
1263
|
+
if (info.examples?.length) {
|
|
1264
|
+
console.log("Examples:");
|
|
1265
|
+
for (const ex of info.examples) {
|
|
1266
|
+
console.log(` surf ${ex.cmd.padEnd(40)} ${ex.desc}`);
|
|
1267
|
+
}
|
|
1268
|
+
console.log();
|
|
1269
|
+
}
|
|
1270
|
+
// Show related commands
|
|
1271
|
+
const related = SEE_ALSO[toolName];
|
|
1272
|
+
if (related && related.length > 0) {
|
|
1273
|
+
console.log(`See also: ${related.join(", ")}`);
|
|
1274
|
+
console.log();
|
|
1275
|
+
}
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
if (ALL_SOCKET_TOOLS.includes(toolName)) {
|
|
1280
|
+
console.log(`\n ${toolName}\n`);
|
|
1281
|
+
console.log(" Socket API tool. Use --json to see response format.\n");
|
|
1282
|
+
// Show related commands for socket tools too
|
|
1283
|
+
const related = SEE_ALSO[toolName];
|
|
1284
|
+
if (related && related.length > 0) {
|
|
1285
|
+
console.log(`See also: ${related.join(", ")}`);
|
|
1286
|
+
console.log();
|
|
1287
|
+
}
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
console.error(`Unknown command: ${toolName}`);
|
|
1291
|
+
process.exit(1);
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1294
|
+
const fuzzyFind = (query) => {
|
|
1295
|
+
const terms = query.toLowerCase().split(/\s+/);
|
|
1296
|
+
const results = [];
|
|
1297
|
+
|
|
1298
|
+
for (const [groupName, group] of Object.entries(TOOLS)) {
|
|
1299
|
+
for (const [cmd, info] of Object.entries(group.commands)) {
|
|
1300
|
+
if (info.alias) continue;
|
|
1301
|
+
const searchText = `${cmd} ${info.desc} ${groupName}`.toLowerCase();
|
|
1302
|
+
const score = terms.filter(t => searchText.includes(t)).length;
|
|
1303
|
+
if (score > 0) {
|
|
1304
|
+
results.push({ cmd, desc: info.desc, group: groupName, score });
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
return results.sort((a, b) => b.score - a.score);
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1312
|
+
const showFindResults = (query) => {
|
|
1313
|
+
const results = fuzzyFind(query);
|
|
1314
|
+
if (results.length === 0) {
|
|
1315
|
+
console.log(`No commands found for: "${query}"`);
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
console.log(`\nSearch results for "${query}":\n`);
|
|
1319
|
+
for (const r of results.slice(0, 10)) {
|
|
1320
|
+
console.log(` ${r.cmd.padEnd(24)} ${r.desc}`);
|
|
1321
|
+
}
|
|
1322
|
+
console.log();
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
const showAbout = (topic) => {
|
|
1326
|
+
const t = HELP_TOPICS[topic];
|
|
1327
|
+
if (t) {
|
|
1328
|
+
showHelpTopic(topic);
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
const topicLower = topic.toLowerCase();
|
|
1332
|
+
for (const [groupName, group] of Object.entries(TOOLS)) {
|
|
1333
|
+
if (groupName === topicLower || group.desc.toLowerCase().includes(topicLower)) {
|
|
1334
|
+
showGroupHelp(groupName);
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
console.error(`Unknown topic: ${topic}`);
|
|
1339
|
+
console.error(`Available topics: ${Object.keys(HELP_TOPICS).join(", ")}`);
|
|
1340
|
+
console.error(`Or use a group name: ${Object.keys(TOOLS).join(", ")}`);
|
|
1341
|
+
process.exit(1);
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
const showAllTools = () => {
|
|
1345
|
+
console.log("\n All available commands:\n");
|
|
1346
|
+
const sorted = [...ALL_SOCKET_TOOLS].sort();
|
|
1347
|
+
const cols = 4;
|
|
1348
|
+
const width = 22;
|
|
1349
|
+
for (let i = 0; i < sorted.length; i += cols) {
|
|
1350
|
+
const row = sorted.slice(i, i + cols).map(t => t.padEnd(width)).join("");
|
|
1351
|
+
console.log(" " + row);
|
|
1352
|
+
}
|
|
1353
|
+
console.log(`\n Total: ${ALL_SOCKET_TOOLS.length} commands\n`);
|
|
1354
|
+
};
|
|
1355
|
+
|
|
1356
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
1357
|
+
showBasicHelp();
|
|
1358
|
+
process.exit(0);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
if (args[0] === "--help-full") {
|
|
1362
|
+
showFullHelp();
|
|
1363
|
+
process.exit(0);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
if (args[0] === "--help-topic" && args[1]) {
|
|
1367
|
+
showHelpTopic(args[1]);
|
|
1368
|
+
process.exit(0);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
1372
|
+
console.log(`surf version ${VERSION}`);
|
|
1373
|
+
process.exit(0);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
if (args[0] === "--list") {
|
|
1377
|
+
showAllTools();
|
|
1378
|
+
process.exit(0);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
if (args[0] === "--find" && args[1]) {
|
|
1382
|
+
showFindResults(args.slice(1).join(" "));
|
|
1383
|
+
process.exit(0);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
if (args[0] === "--about" && args[1]) {
|
|
1387
|
+
showAbout(args[1]);
|
|
1388
|
+
process.exit(0);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
if (args[0] === "server") {
|
|
1392
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
1393
|
+
console.log("Usage: surf server");
|
|
1394
|
+
console.log("");
|
|
1395
|
+
console.log("Start MCP server for Claude Desktop/Cursor integration.");
|
|
1396
|
+
console.log("Communicates via stdio using the Model Context Protocol.");
|
|
1397
|
+
process.exit(0);
|
|
1398
|
+
}
|
|
1399
|
+
const { PiChromeMcpServer } = require("./mcp-server.cjs");
|
|
1400
|
+
const server = new PiChromeMcpServer();
|
|
1401
|
+
server.start().catch((err) => {
|
|
1402
|
+
console.error("MCP Server error:", err.message);
|
|
1403
|
+
process.exit(1);
|
|
1404
|
+
});
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
if (args[0] === "extension-path" || args[0] === "path") {
|
|
1409
|
+
const path = require("path");
|
|
1410
|
+
const distPath = path.resolve(__dirname, "../dist");
|
|
1411
|
+
console.log(distPath);
|
|
1412
|
+
process.exit(0);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
if (args[0] === "install") {
|
|
1416
|
+
const { spawnSync } = require("child_process");
|
|
1417
|
+
const scriptPath = require("path").resolve(__dirname, "../scripts/install-native-host.cjs");
|
|
1418
|
+
const installArgs = args.slice(1);
|
|
1419
|
+
|
|
1420
|
+
if (installArgs.length === 0 || installArgs[0] === "--help" || installArgs[0] === "-h") {
|
|
1421
|
+
console.log(`
|
|
1422
|
+
Usage: surf install <extension-id> [options]
|
|
1423
|
+
|
|
1424
|
+
Install native messaging host for browser communication.
|
|
1425
|
+
|
|
1426
|
+
Arguments:
|
|
1427
|
+
extension-id Chrome extension ID (32 lowercase letters a-p)
|
|
1428
|
+
Find at chrome://extensions with Developer Mode enabled
|
|
1429
|
+
|
|
1430
|
+
Options:
|
|
1431
|
+
-b, --browser Browser(s) to install for (default: chrome)
|
|
1432
|
+
Values: chrome, chromium, brave, edge, arc, all
|
|
1433
|
+
Multiple: --browser chrome,brave
|
|
1434
|
+
|
|
1435
|
+
Examples:
|
|
1436
|
+
surf install hnfbepgmaoklhekckbpjnleifhahkcpl
|
|
1437
|
+
surf install hnfbepgmaoklhekckbpjnleifhahkcpl --browser brave
|
|
1438
|
+
surf install hnfbepgmaoklhekckbpjnleifhahkcpl --browser all
|
|
1439
|
+
`);
|
|
1440
|
+
process.exit(0);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
const result = spawnSync(process.execPath, [scriptPath, ...installArgs], {
|
|
1444
|
+
stdio: "inherit",
|
|
1445
|
+
});
|
|
1446
|
+
process.exit(result.status || 0);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
if (args[0] === "uninstall") {
|
|
1450
|
+
const { spawnSync } = require("child_process");
|
|
1451
|
+
const scriptPath = require("path").resolve(__dirname, "../scripts/uninstall-native-host.cjs");
|
|
1452
|
+
const uninstallArgs = args.slice(1);
|
|
1453
|
+
|
|
1454
|
+
if (uninstallArgs.includes("--help") || uninstallArgs.includes("-h")) {
|
|
1455
|
+
console.log(`
|
|
1456
|
+
Usage: surf uninstall [options]
|
|
1457
|
+
|
|
1458
|
+
Remove native messaging host configuration.
|
|
1459
|
+
|
|
1460
|
+
Options:
|
|
1461
|
+
-b, --browser Browser(s) to uninstall from (default: chrome)
|
|
1462
|
+
Values: chrome, chromium, brave, edge, arc, all
|
|
1463
|
+
-a, --all Uninstall from all browsers and remove wrapper
|
|
1464
|
+
|
|
1465
|
+
Examples:
|
|
1466
|
+
surf uninstall
|
|
1467
|
+
surf uninstall --browser brave
|
|
1468
|
+
surf uninstall --all
|
|
1469
|
+
`);
|
|
1470
|
+
process.exit(0);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
const result = spawnSync(process.execPath, [scriptPath, ...uninstallArgs], {
|
|
1474
|
+
stdio: "inherit",
|
|
1475
|
+
});
|
|
1476
|
+
process.exit(result.status || 0);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
1480
|
+
const tool = args[0];
|
|
1481
|
+
if (TOOLS[tool]) {
|
|
1482
|
+
showGroupHelp(tool);
|
|
1483
|
+
} else {
|
|
1484
|
+
showToolHelp(tool);
|
|
1485
|
+
}
|
|
1486
|
+
process.exit(0);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
if (TOOLS[args[0]] && args.length === 1) {
|
|
1490
|
+
const group = TOOLS[args[0]];
|
|
1491
|
+
const sameNameCmd = group.commands[args[0]];
|
|
1492
|
+
const executableAlone = ["zoom"];
|
|
1493
|
+
if (sameNameCmd && executableAlone.includes(args[0])) {
|
|
1494
|
+
// Command that works without args - execute it
|
|
1495
|
+
} else {
|
|
1496
|
+
showGroupHelp(args[0]);
|
|
1497
|
+
process.exit(0);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
if (args[0] === "config") {
|
|
1502
|
+
const configArgs = args.slice(1);
|
|
1503
|
+
const hasInit = configArgs.includes("--init");
|
|
1504
|
+
const hasPath = configArgs.includes("--path");
|
|
1505
|
+
|
|
1506
|
+
if (hasInit) {
|
|
1507
|
+
const result = createStarterConfig();
|
|
1508
|
+
if (result.success) {
|
|
1509
|
+
console.log(`Created: ${result.path}`);
|
|
1510
|
+
} else {
|
|
1511
|
+
console.error(`Error: ${result.error}`);
|
|
1512
|
+
console.error(`Path: ${result.path}`);
|
|
1513
|
+
process.exit(1);
|
|
1514
|
+
}
|
|
1515
|
+
process.exit(0);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
if (hasPath) {
|
|
1519
|
+
loadConfig();
|
|
1520
|
+
const configPath = getConfigPath();
|
|
1521
|
+
if (configPath) {
|
|
1522
|
+
console.log(configPath);
|
|
1523
|
+
} else {
|
|
1524
|
+
console.log("No config found");
|
|
1525
|
+
}
|
|
1526
|
+
process.exit(0);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
const config = loadConfig();
|
|
1530
|
+
const configPath = getConfigPath();
|
|
1531
|
+
if (configPath) {
|
|
1532
|
+
console.log(JSON.stringify(config, null, 2));
|
|
1533
|
+
} else {
|
|
1534
|
+
console.log("No config found");
|
|
1535
|
+
console.log("Create one with: surf config --init");
|
|
1536
|
+
}
|
|
1537
|
+
process.exit(0);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
if (args.includes("--script")) {
|
|
1541
|
+
const scriptIdx = args.indexOf("--script");
|
|
1542
|
+
const scriptPath = args[scriptIdx + 1];
|
|
1543
|
+
const dryRun = args.includes("--dry-run");
|
|
1544
|
+
const stopOnError = args.includes("--stop-on-error");
|
|
1545
|
+
|
|
1546
|
+
const tabIdIdx = args.indexOf("--tab-id");
|
|
1547
|
+
const scriptTabId = tabIdIdx !== -1 ? args[tabIdIdx + 1] : undefined;
|
|
1548
|
+
|
|
1549
|
+
if (!scriptPath || scriptPath.startsWith("--")) {
|
|
1550
|
+
console.error("Error: --script requires a file path");
|
|
1551
|
+
process.exit(1);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
if (!fs.existsSync(scriptPath)) {
|
|
1555
|
+
console.error(`Error: Script file not found: ${scriptPath}`);
|
|
1556
|
+
process.exit(1);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
let script;
|
|
1560
|
+
try {
|
|
1561
|
+
const content = fs.readFileSync(scriptPath, "utf8");
|
|
1562
|
+
script = JSON.parse(content);
|
|
1563
|
+
} catch (e) {
|
|
1564
|
+
console.error(`Error: Failed to parse script: ${e.message}`);
|
|
1565
|
+
process.exit(1);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
if (!script.steps || !Array.isArray(script.steps)) {
|
|
1569
|
+
console.error("Error: Script must have a 'steps' array");
|
|
1570
|
+
process.exit(1);
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
const sendScriptRequest = (toolName, toolArgs = {}) => {
|
|
1574
|
+
return new Promise((resolve, reject) => {
|
|
1575
|
+
const sock = net.createConnection(SOCKET_PATH, () => {
|
|
1576
|
+
const req = {
|
|
1577
|
+
type: "tool_request",
|
|
1578
|
+
method: "execute_tool",
|
|
1579
|
+
params: { tool: toolName, args: toolArgs },
|
|
1580
|
+
id: "cli-" + Date.now() + "-" + Math.random(),
|
|
1581
|
+
};
|
|
1582
|
+
if (scriptTabId) req.tabId = parseInt(scriptTabId, 10);
|
|
1583
|
+
sock.write(JSON.stringify(req) + "\n");
|
|
1584
|
+
});
|
|
1585
|
+
let buf = "";
|
|
1586
|
+
sock.on("data", (d) => {
|
|
1587
|
+
buf += d.toString();
|
|
1588
|
+
const lines = buf.split("\n");
|
|
1589
|
+
buf = lines.pop();
|
|
1590
|
+
for (const line of lines) {
|
|
1591
|
+
if (!line.trim()) continue;
|
|
1592
|
+
try {
|
|
1593
|
+
const resp = JSON.parse(line);
|
|
1594
|
+
sock.end();
|
|
1595
|
+
resolve(resp);
|
|
1596
|
+
} catch {
|
|
1597
|
+
sock.end();
|
|
1598
|
+
reject(new Error("Invalid JSON"));
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
sock.on("error", (e) => reject(e));
|
|
1603
|
+
let timeoutId;
|
|
1604
|
+
timeoutId = setTimeout(() => { sock.destroy(); reject(new Error("Timeout")); }, 30000);
|
|
1605
|
+
sock.on("close", () => clearTimeout(timeoutId));
|
|
1606
|
+
});
|
|
1607
|
+
};
|
|
1608
|
+
|
|
1609
|
+
const runScript = async () => {
|
|
1610
|
+
const total = script.steps.length;
|
|
1611
|
+
const results = [];
|
|
1612
|
+
let failed = 0;
|
|
1613
|
+
|
|
1614
|
+
console.log(`Running: ${script.name || scriptPath} (${total} steps)`);
|
|
1615
|
+
if (dryRun) console.log("(dry-run mode)\n");
|
|
1616
|
+
else console.log("");
|
|
1617
|
+
|
|
1618
|
+
for (let i = 0; i < total; i++) {
|
|
1619
|
+
const step = script.steps[i];
|
|
1620
|
+
const stepNum = `[${i + 1}/${total}]`;
|
|
1621
|
+
const toolName = step.tool;
|
|
1622
|
+
const toolArgs = step.args || {};
|
|
1623
|
+
|
|
1624
|
+
const argSummary = Object.entries(toolArgs)
|
|
1625
|
+
.map(([k, v]) => typeof v === "string" && v.length > 40 ? `${k}="${v.slice(0, 37)}..."` : `${k}=${JSON.stringify(v)}`)
|
|
1626
|
+
.join(" ");
|
|
1627
|
+
const desc = argSummary ? `${toolName} ${argSummary}` : toolName;
|
|
1628
|
+
|
|
1629
|
+
if (dryRun) {
|
|
1630
|
+
console.log(`${stepNum} ${desc}`);
|
|
1631
|
+
results.push({ step: i + 1, tool: toolName, status: "skipped" });
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
process.stdout.write(`${stepNum} ${desc} ... `);
|
|
1636
|
+
|
|
1637
|
+
try {
|
|
1638
|
+
const resp = await sendScriptRequest(toolName, toolArgs);
|
|
1639
|
+
if (resp.error) {
|
|
1640
|
+
const errText = resp.error.content?.[0]?.text || JSON.stringify(resp.error);
|
|
1641
|
+
console.log(`FAIL`);
|
|
1642
|
+
console.log(` Error: ${errText}`);
|
|
1643
|
+
results.push({ step: i + 1, tool: toolName, status: "fail", error: errText });
|
|
1644
|
+
failed++;
|
|
1645
|
+
if (stopOnError) break;
|
|
1646
|
+
} else {
|
|
1647
|
+
console.log("OK");
|
|
1648
|
+
results.push({ step: i + 1, tool: toolName, status: "ok" });
|
|
1649
|
+
}
|
|
1650
|
+
} catch (e) {
|
|
1651
|
+
console.log(`FAIL`);
|
|
1652
|
+
console.log(` Error: ${e.message}`);
|
|
1653
|
+
results.push({ step: i + 1, tool: toolName, status: "fail", error: e.message });
|
|
1654
|
+
failed++;
|
|
1655
|
+
if (stopOnError) break;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
console.log("");
|
|
1660
|
+
const passed = results.filter(r => r.status === "ok").length;
|
|
1661
|
+
const skipped = results.filter(r => r.status === "skipped").length;
|
|
1662
|
+
if (dryRun) {
|
|
1663
|
+
console.log(`Summary: ${skipped} steps would run`);
|
|
1664
|
+
} else {
|
|
1665
|
+
console.log(`Summary: ${passed} passed, ${failed} failed, ${total} total`);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
1669
|
+
};
|
|
1670
|
+
|
|
1671
|
+
runScript();
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
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"];
|
|
1676
|
+
|
|
1677
|
+
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"];
|
|
1678
|
+
|
|
1679
|
+
const parseArgs = (rawArgs) => {
|
|
1680
|
+
const result = { positional: [], options: {} };
|
|
1681
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
1682
|
+
const arg = rawArgs[i];
|
|
1683
|
+
if (arg.startsWith("--")) {
|
|
1684
|
+
const key = arg.slice(2);
|
|
1685
|
+
if (BOOLEAN_FLAGS.includes(key)) {
|
|
1686
|
+
result.options[key] = true;
|
|
1687
|
+
} else {
|
|
1688
|
+
const next = rawArgs[i + 1];
|
|
1689
|
+
if (next !== undefined && !next.startsWith("--") && !next.startsWith("-")) {
|
|
1690
|
+
let val = next;
|
|
1691
|
+
if (val === "true") val = true;
|
|
1692
|
+
else if (val === "false") val = false;
|
|
1693
|
+
else if (/^-?\d+$/.test(val)) val = parseInt(val, 10);
|
|
1694
|
+
else if (/^-?\d+\.\d+$/.test(val)) val = parseFloat(val);
|
|
1695
|
+
result.options[key] = val;
|
|
1696
|
+
i++;
|
|
1697
|
+
} else {
|
|
1698
|
+
result.options[key] = true;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
} else if (arg === "-v") {
|
|
1702
|
+
result.options.v = true;
|
|
1703
|
+
} else if (arg === "-vv") {
|
|
1704
|
+
result.options.vv = true;
|
|
1705
|
+
} else if (arg.startsWith("-") && arg.length === 2) {
|
|
1706
|
+
// Short flag like -n, -f
|
|
1707
|
+
result.options[arg.slice(1)] = true;
|
|
1708
|
+
} else {
|
|
1709
|
+
result.positional.push(arg);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
return result;
|
|
1713
|
+
};
|
|
1714
|
+
|
|
1715
|
+
let { positional, options } = parseArgs(args);
|
|
1716
|
+
let tool = positional[0];
|
|
1717
|
+
let firstArg = positional[1];
|
|
1718
|
+
|
|
1719
|
+
if (!tool) {
|
|
1720
|
+
console.error("Error: No command specified");
|
|
1721
|
+
process.exit(1);
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
if (REMOVED_COMMANDS[tool]) {
|
|
1725
|
+
console.error(`Error: Unknown command: ${tool}`);
|
|
1726
|
+
console.error(`This command was renamed. Use: ${REMOVED_COMMANDS[tool]}`);
|
|
1727
|
+
process.exit(1);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
const wasSnap = tool === "snap";
|
|
1731
|
+
tool = ALIASES[tool] || tool;
|
|
1732
|
+
|
|
1733
|
+
if (wasSnap && !options.output && !options.savePath) {
|
|
1734
|
+
options.savePath = `/tmp/surf-snap-${Date.now()}.png`;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
if (tool === "smoke") {
|
|
1738
|
+
const smokeUrls = [];
|
|
1739
|
+
const smokeArgs = args.slice(1);
|
|
1740
|
+
for (let i = 0; i < smokeArgs.length; i++) {
|
|
1741
|
+
const arg = smokeArgs[i];
|
|
1742
|
+
if (arg === "--urls") {
|
|
1743
|
+
i++;
|
|
1744
|
+
while (i < smokeArgs.length && !smokeArgs[i].startsWith("--")) {
|
|
1745
|
+
smokeUrls.push(smokeArgs[i]);
|
|
1746
|
+
i++;
|
|
1747
|
+
}
|
|
1748
|
+
i--;
|
|
1749
|
+
} else if (arg === "--routes") {
|
|
1750
|
+
options.routes = smokeArgs[i + 1];
|
|
1751
|
+
i++;
|
|
1752
|
+
} else if (arg === "--screenshot") {
|
|
1753
|
+
options.screenshot = smokeArgs[i + 1];
|
|
1754
|
+
i++;
|
|
1755
|
+
} else if (arg === "--fail-fast") {
|
|
1756
|
+
options["fail-fast"] = true;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
if (smokeUrls.length > 0) {
|
|
1760
|
+
options.urls = smokeUrls;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
const PRIMARY_ARG_MAP = {
|
|
1765
|
+
ai: "query",
|
|
1766
|
+
gemini: "query",
|
|
1767
|
+
chatgpt: "query",
|
|
1768
|
+
perplexity: "query",
|
|
1769
|
+
navigate: "url",
|
|
1770
|
+
go: "url",
|
|
1771
|
+
js: "code",
|
|
1772
|
+
javascript_tool: "code",
|
|
1773
|
+
key: "key",
|
|
1774
|
+
wait: "duration",
|
|
1775
|
+
health: "url",
|
|
1776
|
+
new_tab: "url",
|
|
1777
|
+
"tab.new": "url",
|
|
1778
|
+
switch_tab: "tab_id",
|
|
1779
|
+
"tab.switch": "id",
|
|
1780
|
+
close_tab: "tab_id",
|
|
1781
|
+
"tab.close": "id",
|
|
1782
|
+
"tab.name": "name",
|
|
1783
|
+
"tab.unname": "name",
|
|
1784
|
+
scroll_to_position: "position",
|
|
1785
|
+
type: "text",
|
|
1786
|
+
smart_type: "text",
|
|
1787
|
+
"emulate.network": "preset",
|
|
1788
|
+
"emulate.cpu": "rate",
|
|
1789
|
+
search: "term",
|
|
1790
|
+
find: "term",
|
|
1791
|
+
"wait.element": "selector",
|
|
1792
|
+
"wait.url": "pattern",
|
|
1793
|
+
zoom: "level",
|
|
1794
|
+
"history.search": "query",
|
|
1795
|
+
"network.get": "id",
|
|
1796
|
+
"network.body": "id",
|
|
1797
|
+
"network.curl": "id",
|
|
1798
|
+
"network.path": "id",
|
|
1799
|
+
"window.new": "url",
|
|
1800
|
+
"window.focus": "id",
|
|
1801
|
+
"window.close": "id",
|
|
1802
|
+
"locate.role": "role",
|
|
1803
|
+
"locate.text": "text",
|
|
1804
|
+
"locate.label": "label",
|
|
1805
|
+
"emulate.device": "device",
|
|
1806
|
+
"frame.js": "code",
|
|
1807
|
+
};
|
|
1808
|
+
|
|
1809
|
+
const toolArgs = { ...options };
|
|
1810
|
+
|
|
1811
|
+
if (tool === "click" && firstArg) {
|
|
1812
|
+
if (/^e\d+$/.test(firstArg)) {
|
|
1813
|
+
toolArgs.ref = firstArg;
|
|
1814
|
+
firstArg = undefined;
|
|
1815
|
+
} else if (/^\d+$/.test(firstArg) && positional[2] && /^\d+$/.test(positional[2])) {
|
|
1816
|
+
toolArgs.x = parseInt(firstArg, 10);
|
|
1817
|
+
toolArgs.y = parseInt(positional[2], 10);
|
|
1818
|
+
firstArg = undefined;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
if (firstArg !== undefined) {
|
|
1823
|
+
const primaryKey = PRIMARY_ARG_MAP[tool];
|
|
1824
|
+
if (primaryKey && toolArgs[primaryKey] === undefined) {
|
|
1825
|
+
let val = firstArg;
|
|
1826
|
+
if (val === "true") val = true;
|
|
1827
|
+
else if (val === "false") val = false;
|
|
1828
|
+
else if (/^-?\d+$/.test(val)) val = parseInt(val, 10);
|
|
1829
|
+
toolArgs[primaryKey] = val;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
if (tool === "js" && toolArgs.file) {
|
|
1834
|
+
try {
|
|
1835
|
+
toolArgs.code = fs.readFileSync(toolArgs.file, "utf8");
|
|
1836
|
+
delete toolArgs.file;
|
|
1837
|
+
} catch (e) {
|
|
1838
|
+
console.error(`Error: Failed to read file: ${e.message}`);
|
|
1839
|
+
process.exit(1);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
if (toolArgs.into && !toolArgs.selector) {
|
|
1844
|
+
toolArgs.selector = toolArgs.into;
|
|
1845
|
+
delete toolArgs.into;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
const globalOpts = {};
|
|
1849
|
+
if (toolArgs["tab-id"] !== undefined) {
|
|
1850
|
+
const tid = parseInt(toolArgs["tab-id"], 10);
|
|
1851
|
+
if (isNaN(tid)) {
|
|
1852
|
+
console.error("Error: --tab-id must be a number");
|
|
1853
|
+
process.exit(1);
|
|
1854
|
+
}
|
|
1855
|
+
globalOpts.tabId = tid;
|
|
1856
|
+
delete toolArgs["tab-id"];
|
|
1857
|
+
}
|
|
1858
|
+
if (toolArgs["window-id"] !== undefined) {
|
|
1859
|
+
const wid = parseInt(toolArgs["window-id"], 10);
|
|
1860
|
+
if (isNaN(wid)) {
|
|
1861
|
+
console.error("Error: --window-id must be a number");
|
|
1862
|
+
process.exit(1);
|
|
1863
|
+
}
|
|
1864
|
+
globalOpts.windowId = wid;
|
|
1865
|
+
delete toolArgs["window-id"];
|
|
1866
|
+
}
|
|
1867
|
+
if (toolArgs["network-path"] !== undefined) {
|
|
1868
|
+
networkStore.setBasePath(toolArgs["network-path"]);
|
|
1869
|
+
delete toolArgs["network-path"];
|
|
1870
|
+
}
|
|
1871
|
+
const wantJson = toolArgs.json === true;
|
|
1872
|
+
delete toolArgs.json;
|
|
1873
|
+
|
|
1874
|
+
const autoCapture = toolArgs["auto-capture"] === true;
|
|
1875
|
+
delete toolArgs["auto-capture"];
|
|
1876
|
+
|
|
1877
|
+
const noScreenshot = toolArgs["no-screenshot"] === true;
|
|
1878
|
+
delete toolArgs["no-screenshot"];
|
|
1879
|
+
|
|
1880
|
+
const softFail = toolArgs["soft-fail"] === true;
|
|
1881
|
+
delete toolArgs["soft-fail"];
|
|
1882
|
+
|
|
1883
|
+
if (!noScreenshot && AUTO_SCREENSHOT_TOOLS.includes(tool)) {
|
|
1884
|
+
toolArgs.autoScreenshot = true;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
const outputPath = toolArgs.output;
|
|
1888
|
+
delete toolArgs.output;
|
|
1889
|
+
|
|
1890
|
+
if ((tool === "screenshot" || tool === "snap") && outputPath) {
|
|
1891
|
+
if (typeof outputPath !== "string") {
|
|
1892
|
+
console.error("Error: --output requires a file path");
|
|
1893
|
+
process.exit(1);
|
|
1894
|
+
}
|
|
1895
|
+
toolArgs.savePath = outputPath;
|
|
1896
|
+
if (options.full) toolArgs.full = true;
|
|
1897
|
+
if (options["max-size"]) toolArgs["max-size"] = options["max-size"];
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
const methodFlag = toolArgs.method;
|
|
1901
|
+
// Keep method for network filtering, only delete for other tools
|
|
1902
|
+
if (tool !== 'network' && tool !== 'get_network_entries') {
|
|
1903
|
+
delete toolArgs.method;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
const streamMode = toolArgs.stream === true;
|
|
1907
|
+
delete toolArgs.stream;
|
|
1908
|
+
|
|
1909
|
+
const streamLevel = toolArgs.level;
|
|
1910
|
+
delete toolArgs.level;
|
|
1911
|
+
|
|
1912
|
+
const streamFilter = toolArgs.filter;
|
|
1913
|
+
delete toolArgs.filter;
|
|
1914
|
+
|
|
1915
|
+
let finalTool = tool;
|
|
1916
|
+
if (methodFlag === "js") {
|
|
1917
|
+
if (tool === "type") {
|
|
1918
|
+
if (!toolArgs.selector) {
|
|
1919
|
+
console.error("Error: --selector or --into required for type with --method js");
|
|
1920
|
+
process.exit(1);
|
|
1921
|
+
}
|
|
1922
|
+
finalTool = "smart_type";
|
|
1923
|
+
} else if (tool === "click") {
|
|
1924
|
+
if (!toolArgs.selector) {
|
|
1925
|
+
console.error("Error: --selector required for click with --method js");
|
|
1926
|
+
process.exit(1);
|
|
1927
|
+
}
|
|
1928
|
+
toolArgs.code = `document.querySelector(${JSON.stringify(toolArgs.selector)})?.click()`;
|
|
1929
|
+
delete toolArgs.selector;
|
|
1930
|
+
finalTool = "js";
|
|
1931
|
+
}
|
|
1932
|
+
} else if (methodFlag === "cdp") {
|
|
1933
|
+
if (tool === "smart_type") {
|
|
1934
|
+
finalTool = "type";
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
if (streamMode && (tool === "console" || tool === "network")) {
|
|
1939
|
+
const streamType = tool === "console" ? "STREAM_CONSOLE" : "STREAM_NETWORK";
|
|
1940
|
+
const streamOpts = {
|
|
1941
|
+
level: streamLevel,
|
|
1942
|
+
filter: streamFilter,
|
|
1943
|
+
};
|
|
1944
|
+
|
|
1945
|
+
const formatTime = (ts) => {
|
|
1946
|
+
const d = new Date(ts);
|
|
1947
|
+
return d.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit", fractionalSecondDigits: 3 });
|
|
1948
|
+
};
|
|
1949
|
+
|
|
1950
|
+
let connectionTimeout = null;
|
|
1951
|
+
let receivedData = false;
|
|
1952
|
+
|
|
1953
|
+
const sock = net.createConnection(SOCKET_PATH, () => {
|
|
1954
|
+
const req = {
|
|
1955
|
+
type: "stream_request",
|
|
1956
|
+
streamType,
|
|
1957
|
+
options: streamOpts,
|
|
1958
|
+
id: "cli-stream-" + Date.now(),
|
|
1959
|
+
...globalOpts,
|
|
1960
|
+
};
|
|
1961
|
+
sock.write(JSON.stringify(req) + "\n");
|
|
1962
|
+
connectionTimeout = setTimeout(() => {
|
|
1963
|
+
if (!receivedData) {
|
|
1964
|
+
console.error("Error: Stream connection timeout (10s) - no data received");
|
|
1965
|
+
sock.destroy();
|
|
1966
|
+
process.exit(1);
|
|
1967
|
+
}
|
|
1968
|
+
}, 10000);
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
let buf = "";
|
|
1972
|
+
sock.on("data", (d) => {
|
|
1973
|
+
if (!receivedData) {
|
|
1974
|
+
receivedData = true;
|
|
1975
|
+
if (connectionTimeout) {
|
|
1976
|
+
clearTimeout(connectionTimeout);
|
|
1977
|
+
connectionTimeout = null;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
buf += d.toString();
|
|
1981
|
+
const lines = buf.split("\n");
|
|
1982
|
+
buf = lines.pop();
|
|
1983
|
+
for (const line of lines) {
|
|
1984
|
+
if (!line.trim()) continue;
|
|
1985
|
+
try {
|
|
1986
|
+
const msg = JSON.parse(line);
|
|
1987
|
+
if (msg.error) {
|
|
1988
|
+
console.error("Error:", msg.error);
|
|
1989
|
+
sock.end();
|
|
1990
|
+
process.exit(1);
|
|
1991
|
+
}
|
|
1992
|
+
if (msg.type === "extension_disconnected") {
|
|
1993
|
+
console.error(msg.message);
|
|
1994
|
+
sock.end();
|
|
1995
|
+
process.exit(1);
|
|
1996
|
+
}
|
|
1997
|
+
if (msg.type === "stream_started") {
|
|
1998
|
+
continue;
|
|
1999
|
+
}
|
|
2000
|
+
if (msg.type === "console_event") {
|
|
2001
|
+
const { level, text, timestamp } = msg;
|
|
2002
|
+
if (streamLevel && level !== streamLevel) continue;
|
|
2003
|
+
console.log(`[console] [${level}] ${formatTime(timestamp)} ${text}`);
|
|
2004
|
+
} else if (msg.type === "network_event") {
|
|
2005
|
+
const { method, url, status, duration } = msg;
|
|
2006
|
+
if (streamFilter && !url.includes(streamFilter)) continue;
|
|
2007
|
+
const statusStr = status !== undefined ? status : "...";
|
|
2008
|
+
const durationStr = duration !== undefined ? ` (${duration}ms)` : "";
|
|
2009
|
+
console.log(`[network] ${method} ${url} ${statusStr}${durationStr}`);
|
|
2010
|
+
}
|
|
2011
|
+
} catch {}
|
|
2012
|
+
}
|
|
2013
|
+
});
|
|
2014
|
+
|
|
2015
|
+
sock.on("error", (e) => {
|
|
2016
|
+
if (e.code === "ENOENT") {
|
|
2017
|
+
console.error("Error: Socket not found. Is Chrome running with the extension?");
|
|
2018
|
+
} else {
|
|
2019
|
+
console.error("Error:", e.message);
|
|
2020
|
+
}
|
|
2021
|
+
process.exit(1);
|
|
2022
|
+
});
|
|
2023
|
+
|
|
2024
|
+
process.on("SIGINT", () => {
|
|
2025
|
+
sock.write(JSON.stringify({ type: "stream_stop" }) + "\n");
|
|
2026
|
+
sock.end();
|
|
2027
|
+
process.exit(0);
|
|
2028
|
+
});
|
|
2029
|
+
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
const request = {
|
|
2034
|
+
type: "tool_request",
|
|
2035
|
+
method: "execute_tool",
|
|
2036
|
+
params: { tool: finalTool, args: toolArgs },
|
|
2037
|
+
id: "cli-" + Date.now(),
|
|
2038
|
+
...globalOpts,
|
|
2039
|
+
};
|
|
2040
|
+
|
|
2041
|
+
const sendRequest = (toolName, toolArgs = {}) => {
|
|
2042
|
+
return new Promise((resolve, reject) => {
|
|
2043
|
+
const sock = net.createConnection(SOCKET_PATH, () => {
|
|
2044
|
+
const req = {
|
|
2045
|
+
type: "tool_request",
|
|
2046
|
+
method: "execute_tool",
|
|
2047
|
+
params: { tool: toolName, args: toolArgs },
|
|
2048
|
+
id: "cli-" + Date.now() + "-" + Math.random(),
|
|
2049
|
+
...globalOpts,
|
|
2050
|
+
};
|
|
2051
|
+
sock.write(JSON.stringify(req) + "\n");
|
|
2052
|
+
});
|
|
2053
|
+
let buf = "";
|
|
2054
|
+
sock.on("data", (d) => {
|
|
2055
|
+
buf += d.toString();
|
|
2056
|
+
const lines = buf.split("\n");
|
|
2057
|
+
buf = lines.pop();
|
|
2058
|
+
for (const line of lines) {
|
|
2059
|
+
if (!line.trim()) continue;
|
|
2060
|
+
try {
|
|
2061
|
+
const resp = JSON.parse(line);
|
|
2062
|
+
if (resp.type === "extension_disconnected") {
|
|
2063
|
+
sock.end();
|
|
2064
|
+
reject(new Error(resp.message));
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
sock.end();
|
|
2068
|
+
resolve(resp);
|
|
2069
|
+
} catch {
|
|
2070
|
+
sock.end();
|
|
2071
|
+
reject(new Error("Invalid JSON"));
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
});
|
|
2075
|
+
sock.on("error", (e) => reject(e));
|
|
2076
|
+
let timeoutId;
|
|
2077
|
+
timeoutId = setTimeout(() => { sock.destroy(); reject(new Error("Timeout")); }, 5000);
|
|
2078
|
+
sock.on("close", () => clearTimeout(timeoutId));
|
|
2079
|
+
});
|
|
2080
|
+
};
|
|
2081
|
+
|
|
2082
|
+
const performAutoCapture = async () => {
|
|
2083
|
+
const timestamp = Date.now();
|
|
2084
|
+
const screenshotPath = `/tmp/surf-error-${timestamp}.png`;
|
|
2085
|
+
|
|
2086
|
+
try {
|
|
2087
|
+
const [screenshotResp, consoleResp] = await Promise.all([
|
|
2088
|
+
sendRequest("screenshot", { savePath: screenshotPath }),
|
|
2089
|
+
sendRequest("console", {}),
|
|
2090
|
+
]);
|
|
2091
|
+
|
|
2092
|
+
if (screenshotResp.result) {
|
|
2093
|
+
console.error(`Auto-captured: ${screenshotPath}`);
|
|
2094
|
+
} else {
|
|
2095
|
+
console.error("Auto-captured: (screenshot failed)");
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
let consoleErrors = "(none)";
|
|
2099
|
+
const consoleText = consoleResp.result?.content?.[0]?.text;
|
|
2100
|
+
if (consoleText) {
|
|
2101
|
+
try {
|
|
2102
|
+
const parsed = JSON.parse(consoleText);
|
|
2103
|
+
const msgs = parsed.messages || parsed || [];
|
|
2104
|
+
const errors = msgs.filter(m => m.level === "error" || m.type === "error");
|
|
2105
|
+
if (errors.length > 0) {
|
|
2106
|
+
consoleErrors = errors.map(e => e.text || e.message || JSON.stringify(e)).join("\n ");
|
|
2107
|
+
}
|
|
2108
|
+
} catch {
|
|
2109
|
+
consoleErrors = consoleText;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
console.error(`Console errors: ${consoleErrors}`);
|
|
2113
|
+
} catch (captureErr) {
|
|
2114
|
+
console.error(`Auto-capture failed: ${captureErr.message}`);
|
|
2115
|
+
}
|
|
2116
|
+
};
|
|
2117
|
+
|
|
2118
|
+
const socket = net.createConnection(SOCKET_PATH, () => {
|
|
2119
|
+
socket.write(JSON.stringify(request) + "\n");
|
|
2120
|
+
});
|
|
2121
|
+
|
|
2122
|
+
const AI_TOOLS = ["smoke", "chatgpt", "gemini", "perplexity", "ai"];
|
|
2123
|
+
const requestTimeout = AI_TOOLS.includes(tool) ? 300000 : 30000;
|
|
2124
|
+
const timeout = setTimeout(() => {
|
|
2125
|
+
console.error(`Error: Request timed out (${requestTimeout / 1000}s)`);
|
|
2126
|
+
socket.destroy();
|
|
2127
|
+
process.exit(1);
|
|
2128
|
+
}, requestTimeout);
|
|
2129
|
+
|
|
2130
|
+
let buffer = "";
|
|
2131
|
+
|
|
2132
|
+
socket.on("data", (data) => {
|
|
2133
|
+
buffer += data.toString();
|
|
2134
|
+
const lines = buffer.split("\n");
|
|
2135
|
+
buffer = lines.pop();
|
|
2136
|
+
|
|
2137
|
+
for (const line of lines) {
|
|
2138
|
+
if (!line.trim()) continue;
|
|
2139
|
+
try {
|
|
2140
|
+
const msg = JSON.parse(line);
|
|
2141
|
+
|
|
2142
|
+
if (msg.type === "extension_disconnected") {
|
|
2143
|
+
clearTimeout(timeout);
|
|
2144
|
+
console.error(msg.message);
|
|
2145
|
+
socket.end();
|
|
2146
|
+
process.exit(1);
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
handleResponse(msg).catch((err) => {
|
|
2150
|
+
console.error("Handler error:", err.message);
|
|
2151
|
+
process.exit(1);
|
|
2152
|
+
});
|
|
2153
|
+
} catch (e) {
|
|
2154
|
+
console.error("Invalid JSON response:", line);
|
|
2155
|
+
process.exit(1);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
});
|
|
2159
|
+
|
|
2160
|
+
socket.on("error", (err) => {
|
|
2161
|
+
clearTimeout(timeout);
|
|
2162
|
+
if (err.code === "ENOENT") {
|
|
2163
|
+
console.error("Error: Socket not found. Is Chrome running with the extension?");
|
|
2164
|
+
} else if (err.code === "ECONNREFUSED") {
|
|
2165
|
+
console.error("Error: Connection refused. Native host not running.");
|
|
2166
|
+
} else {
|
|
2167
|
+
console.error("Error:", err.message);
|
|
2168
|
+
}
|
|
2169
|
+
process.exit(1);
|
|
2170
|
+
});
|
|
2171
|
+
|
|
2172
|
+
socket.on("close", () => {
|
|
2173
|
+
clearTimeout(timeout);
|
|
2174
|
+
});
|
|
2175
|
+
|
|
2176
|
+
async function handleResponse(response) {
|
|
2177
|
+
clearTimeout(timeout);
|
|
2178
|
+
|
|
2179
|
+
if (response.error) {
|
|
2180
|
+
const errContent = response.error.content?.[0]?.text || JSON.stringify(response.error);
|
|
2181
|
+
if (softFail) {
|
|
2182
|
+
console.warn("Warning:", errContent);
|
|
2183
|
+
socket.end();
|
|
2184
|
+
process.exit(0);
|
|
2185
|
+
}
|
|
2186
|
+
console.error("Error:", errContent);
|
|
2187
|
+
|
|
2188
|
+
if (autoCapture) {
|
|
2189
|
+
await performAutoCapture();
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
socket.end();
|
|
2193
|
+
process.exit(1);
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
const result = response.result?.content?.[0]?.text;
|
|
2197
|
+
|
|
2198
|
+
let data;
|
|
2199
|
+
try {
|
|
2200
|
+
data = result ? JSON.parse(result) : response.result;
|
|
2201
|
+
} catch {
|
|
2202
|
+
data = result || response.result;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
if (wantJson) {
|
|
2206
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2207
|
+
socket.end();
|
|
2208
|
+
process.exit(0);
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
if ((tool === "screenshot" || tool === "snap") && data?.base64 && (outputPath || toolArgs.savePath)) {
|
|
2212
|
+
const saveTo = outputPath || toolArgs.savePath;
|
|
2213
|
+
fs.writeFileSync(saveTo, Buffer.from(data.base64, "base64"));
|
|
2214
|
+
|
|
2215
|
+
const skipResize = options.full || toolArgs.full;
|
|
2216
|
+
const maxSize = parseInt(options["max-size"] || toolArgs["max-size"] || "1200", 10);
|
|
2217
|
+
const origWidth = data.width || 0;
|
|
2218
|
+
const origHeight = data.height || 0;
|
|
2219
|
+
|
|
2220
|
+
if (!skipResize && (origWidth > maxSize || origHeight > maxSize)) {
|
|
2221
|
+
const result = resizeImage(saveTo, maxSize);
|
|
2222
|
+
if (result.success) {
|
|
2223
|
+
console.log(`Saved to ${saveTo} (${result.width}x${result.height}, resized from ${origWidth}x${origHeight})`);
|
|
2224
|
+
} else {
|
|
2225
|
+
console.log(`Saved to ${saveTo} (${origWidth}x${origHeight}, resize failed: ${result.error})`);
|
|
2226
|
+
}
|
|
2227
|
+
} else {
|
|
2228
|
+
console.log(`Saved to ${saveTo} (${origWidth}x${origHeight})`);
|
|
2229
|
+
}
|
|
2230
|
+
} else if ((tool === "screenshot" || tool === "snap") && data?.message) {
|
|
2231
|
+
console.log(data.message);
|
|
2232
|
+
} else if (tool === "tab.list") {
|
|
2233
|
+
const tabs = data?.tabs || data || [];
|
|
2234
|
+
if (Array.isArray(tabs)) {
|
|
2235
|
+
if (tabs.length === 0) {
|
|
2236
|
+
if (globalOpts.windowId) {
|
|
2237
|
+
console.log(`No tabs in window ${globalOpts.windowId}. Window may not exist - use 'surf window.list' to verify.`);
|
|
2238
|
+
} else {
|
|
2239
|
+
console.log("No tabs found.");
|
|
2240
|
+
}
|
|
2241
|
+
} else {
|
|
2242
|
+
for (const t of tabs) {
|
|
2243
|
+
console.log(`${t.id}\t${t.title}\t${t.url}`);
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
} else {
|
|
2247
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2248
|
+
}
|
|
2249
|
+
} else if (tool === "tab.named") {
|
|
2250
|
+
const named = data?.tabs || data?.namedTabs || data || [];
|
|
2251
|
+
if (Array.isArray(named)) {
|
|
2252
|
+
if (named.length === 0) {
|
|
2253
|
+
console.log("No named tabs");
|
|
2254
|
+
} else {
|
|
2255
|
+
for (const t of named) {
|
|
2256
|
+
console.log(`${t.name}\t${t.tabId}\t${t.title || ""}\t${t.url || ""}`);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
} else {
|
|
2260
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2261
|
+
}
|
|
2262
|
+
} else if (tool === "ai" && data?.aiResult) {
|
|
2263
|
+
if (data.mode === "find") {
|
|
2264
|
+
console.log(data.ref || "NOT_FOUND");
|
|
2265
|
+
} else {
|
|
2266
|
+
console.log(data.content);
|
|
2267
|
+
}
|
|
2268
|
+
} else if (tool === "page.read" && data?.pageContent) {
|
|
2269
|
+
console.log(data.pageContent);
|
|
2270
|
+
} else if (tool === "page.text" && data?.text) {
|
|
2271
|
+
console.log(data.text);
|
|
2272
|
+
} else if (tool === "emulate.device" && data?.devices) {
|
|
2273
|
+
console.log("Available devices:\n");
|
|
2274
|
+
const devices = data.devices;
|
|
2275
|
+
for (const d of devices) {
|
|
2276
|
+
console.log(` ${d}`);
|
|
2277
|
+
}
|
|
2278
|
+
console.log("\nUsage: surf emulate.device \"<device name>\"");
|
|
2279
|
+
console.log('Reset: surf emulate.device "reset"');
|
|
2280
|
+
} else if (tool === "js") {
|
|
2281
|
+
if (data?.result !== undefined) {
|
|
2282
|
+
const val = data.result.value ?? data.result;
|
|
2283
|
+
console.log(typeof val === "string" ? val : JSON.stringify(val, null, 2));
|
|
2284
|
+
} else {
|
|
2285
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2286
|
+
}
|
|
2287
|
+
} else if (tool === "health") {
|
|
2288
|
+
if (data?.success) {
|
|
2289
|
+
const timeStr = data.time ? ` (${data.time}ms)` : "";
|
|
2290
|
+
if (data.status) {
|
|
2291
|
+
console.log(`OK: ${data.status}${timeStr}`);
|
|
2292
|
+
} else if (data.found) {
|
|
2293
|
+
console.log(`OK: element found${timeStr}`);
|
|
2294
|
+
} else {
|
|
2295
|
+
console.log(`OK${timeStr}`);
|
|
2296
|
+
}
|
|
2297
|
+
} else {
|
|
2298
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2299
|
+
}
|
|
2300
|
+
} else if (tool === "smoke" && data?.results) {
|
|
2301
|
+
const results = data.results;
|
|
2302
|
+
const summary = data.summary || { pass: 0, fail: 0, total: results.length };
|
|
2303
|
+
|
|
2304
|
+
for (const r of results) {
|
|
2305
|
+
const status = r.status === "pass" ? "PASS" : "FAIL";
|
|
2306
|
+
const timeStr = r.time ? ` (${r.time}ms)` : "";
|
|
2307
|
+
const ssStr = r.screenshot ? ` [${r.screenshot}]` : "";
|
|
2308
|
+
console.log(`[${status}] ${r.url}${timeStr}${ssStr}`);
|
|
2309
|
+
if (r.errors && r.errors.length > 0) {
|
|
2310
|
+
for (const err of r.errors) {
|
|
2311
|
+
console.log(` - ${err}`);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
console.log("");
|
|
2317
|
+
console.log(`Summary: ${summary.pass} passed, ${summary.fail} failed, ${summary.total} total`);
|
|
2318
|
+
|
|
2319
|
+
if (summary.fail > 0) {
|
|
2320
|
+
socket.end();
|
|
2321
|
+
process.exit(1);
|
|
2322
|
+
}
|
|
2323
|
+
} else if (tool === "zoom" && data?.zoom !== undefined) {
|
|
2324
|
+
console.log(`Zoom: ${Math.round(data.zoom * 100)}%`);
|
|
2325
|
+
} else if (tool === "back" || tool === "forward") {
|
|
2326
|
+
console.log("OK");
|
|
2327
|
+
} else if (tool === "network" && (data?.entries || data?.requests)) {
|
|
2328
|
+
// Network list - handle both new (entries) and old (requests) formats
|
|
2329
|
+
const items = data.entries || data.requests || [];
|
|
2330
|
+
|
|
2331
|
+
if (items.length === 0) {
|
|
2332
|
+
console.log("No network requests captured");
|
|
2333
|
+
} else if (data._format === 'raw') {
|
|
2334
|
+
// Raw JSON output - print entries array directly
|
|
2335
|
+
console.log(JSON.stringify(items, null, 2));
|
|
2336
|
+
} else {
|
|
2337
|
+
// Simple compact format for now
|
|
2338
|
+
for (const req of items) {
|
|
2339
|
+
const status = req.status || '-';
|
|
2340
|
+
const method = (req.method || 'GET').padEnd(6);
|
|
2341
|
+
const type = (req.type || '').padEnd(10);
|
|
2342
|
+
const url = req.url || '';
|
|
2343
|
+
console.log(`${status} ${method} ${type} ${url}`);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
} else if (tool === "network.get" && data?.entry) {
|
|
2347
|
+
console.log(networkFormatters.formatEntry(data.entry));
|
|
2348
|
+
} else if (tool === "network.body" && data?.body !== undefined) {
|
|
2349
|
+
// Raw body for piping
|
|
2350
|
+
process.stdout.write(data.body);
|
|
2351
|
+
} else if (tool === "network.curl" && data?.curl) {
|
|
2352
|
+
console.log(data.curl);
|
|
2353
|
+
} else if (tool === "network.curl" && data?.entry) {
|
|
2354
|
+
console.log(networkFormatters.formatCurl(data.entry));
|
|
2355
|
+
} else if (tool === "network.origins" && data?.origins) {
|
|
2356
|
+
console.log(networkFormatters.formatOrigins(data.origins));
|
|
2357
|
+
} else if (tool === "network.stats" && data?.stats) {
|
|
2358
|
+
console.log(networkFormatters.formatStats(data.stats));
|
|
2359
|
+
} else if (tool === "network.clear" && data?.cleared !== undefined) {
|
|
2360
|
+
console.log(`Cleared ${data.cleared} requests`);
|
|
2361
|
+
} else if (tool === "network.export" && data?.path) {
|
|
2362
|
+
console.log(`Exported to: ${data.path}`);
|
|
2363
|
+
} else if (tool === "network.path" && data?.paths) {
|
|
2364
|
+
for (const [key, val] of Object.entries(data.paths)) {
|
|
2365
|
+
console.log(`${key}: ${val}`);
|
|
2366
|
+
}
|
|
2367
|
+
} else if ((tool === "chatgpt" || tool === "gemini") && data?.response) {
|
|
2368
|
+
console.log(data.response);
|
|
2369
|
+
if (data.imagePath) {
|
|
2370
|
+
console.log(`\nImage saved: ${data.imagePath}`);
|
|
2371
|
+
}
|
|
2372
|
+
console.error(`\n[${data.model || 'unknown'} | ${((data.tookMs || 0) / 1000).toFixed(1)}s]`);
|
|
2373
|
+
} else if (tool === "perplexity" && data?.response) {
|
|
2374
|
+
console.log(data.response);
|
|
2375
|
+
const meta = [];
|
|
2376
|
+
if (data.sources) meta.push(`${data.sources} sources`);
|
|
2377
|
+
if (data.mode) meta.push(data.mode);
|
|
2378
|
+
if (data.model && data.model !== 'default') meta.push(data.model);
|
|
2379
|
+
meta.push(`${((data.tookMs || 0) / 1000).toFixed(1)}s`);
|
|
2380
|
+
console.error(`\n[${meta.join(' | ')}]`);
|
|
2381
|
+
if (data.url) console.error(`URL: ${data.url}`);
|
|
2382
|
+
} else if (tool === "window.list" && data?.windows) {
|
|
2383
|
+
if (data.windows.length === 0) {
|
|
2384
|
+
console.log("No windows. Use 'surf window.new' to create one.");
|
|
2385
|
+
} else {
|
|
2386
|
+
for (const w of data.windows) {
|
|
2387
|
+
const focused = w.focused ? " [focused]" : "";
|
|
2388
|
+
const state = w.state !== "normal" ? ` (${w.state})` : "";
|
|
2389
|
+
console.log(`${w.id}\t${w.tabCount} tabs\t${w.width}x${w.height}${focused}${state}`);
|
|
2390
|
+
if (w.tabs) {
|
|
2391
|
+
for (const t of w.tabs) {
|
|
2392
|
+
const active = t.active ? "*" : " ";
|
|
2393
|
+
console.log(` ${active} ${t.id}\t${t.title || "(no title)"}\t${t.url || ""}`);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
// Hint for agents
|
|
2398
|
+
if (data.windows.length > 0 && !globalOpts.windowId) {
|
|
2399
|
+
console.log("\n[hint] Use --window-id <id> to isolate commands to a specific window");
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
} else if (typeof data === "string") {
|
|
2403
|
+
console.log(data);
|
|
2404
|
+
} else if (data?.success === true) {
|
|
2405
|
+
console.log("OK");
|
|
2406
|
+
} else if (data?.error) {
|
|
2407
|
+
if (softFail) {
|
|
2408
|
+
console.warn("Warning:", data.error);
|
|
2409
|
+
socket.end();
|
|
2410
|
+
process.exit(0);
|
|
2411
|
+
}
|
|
2412
|
+
console.error("Error:", data.error);
|
|
2413
|
+
if (autoCapture) {
|
|
2414
|
+
await performAutoCapture();
|
|
2415
|
+
}
|
|
2416
|
+
socket.end();
|
|
2417
|
+
process.exit(1);
|
|
2418
|
+
} else {
|
|
2419
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
socket.end();
|
|
2423
|
+
process.exit(0);
|
|
2424
|
+
}
|