surf-cli 2.8.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -8
- package/native/abort.cjs +65 -0
- package/native/activity-journal.cjs +55 -0
- package/native/ai-queue.cjs +64 -0
- package/native/aistudio-build.cjs +21 -13
- package/native/aistudio-client.cjs +40 -20
- package/native/browser-lock.cjs +2 -2
- package/native/chatgpt-client.cjs +49 -31
- package/native/cli.cjs +352 -482
- package/native/client-transport.cjs +168 -0
- package/native/do-executor.cjs +68 -510
- package/native/do-parser.cjs +8 -249
- package/native/doctor.cjs +55 -5
- package/native/endpoint.cjs +174 -0
- package/native/file-transfer.cjs +734 -0
- package/native/gemini-client.cjs +156 -71
- package/native/grok-client.cjs +98 -89
- package/native/host-helpers.cjs +43 -26
- package/native/host-sessions.cjs +287 -0
- package/native/host.cjs +998 -620
- package/native/listener.cjs +20 -0
- package/native/mcp-server.cjs +60 -65
- package/native/network-export.cjs +116 -0
- package/native/network-store.cjs +38 -58
- package/native/perplexity-client.cjs +46 -17
- package/native/playbook-authoring.cjs +44 -0
- package/native/playbook-cli.cjs +157 -0
- package/native/playbook-client.cjs +259 -0
- package/native/playbook-receipts.cjs +109 -0
- package/native/playbook-records.cjs +208 -0
- package/native/playbook-runtime.cjs +177 -0
- package/native/playbooks.cjs +235 -0
- package/native/private-state.cjs +156 -0
- package/native/redaction.cjs +104 -0
- package/native/remote-auth.cjs +279 -0
- package/native/remote-transport.cjs +337 -0
- package/native/request-pending.cjs +148 -0
- package/native/socket-path.cjs +1 -1
- package/native/workflow-definition.cjs +368 -0
- package/native/workflow-runtime.cjs +225 -0
- package/package.json +9 -6
- package/playbooks/page/ops/read.json +22 -0
- package/playbooks/page/playbook.json +7 -0
- package/scripts/install-native-host.cjs +36 -5
- package/skills/README.md +11 -5
- package/skills/deep-x-research/SKILL.md +106 -0
- package/skills/surf/SKILL.md +72 -5
package/native/cli.cjs
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const net = require("net");
|
|
3
2
|
const fs = require("fs");
|
|
4
3
|
const path = require("path");
|
|
5
|
-
const os = require("os");
|
|
6
4
|
const { execFileSync, execSync } = require("child_process");
|
|
7
5
|
const { loadConfig, getConfigPath, createStarterConfig } = require("./config.cjs");
|
|
8
6
|
const networkFormatters = require("./formatters/network.cjs");
|
|
9
|
-
const
|
|
10
|
-
|
|
7
|
+
const {
|
|
8
|
+
applyArgDefaults,
|
|
9
|
+
formatStep,
|
|
10
|
+
getWorkflowDirs,
|
|
11
|
+
getWorkflowInfo,
|
|
12
|
+
listWorkflows,
|
|
13
|
+
normalizeWorkflow,
|
|
14
|
+
parseDoCommands,
|
|
15
|
+
resolveWorkflow,
|
|
16
|
+
validateWorkflowArgs,
|
|
17
|
+
validateWorkflowFile,
|
|
18
|
+
} = require("./workflow-definition.cjs");
|
|
11
19
|
const { executeDoSteps } = require("./do-executor.cjs");
|
|
20
|
+
const { openClientTransport } = require("./client-transport.cjs");
|
|
12
21
|
const { version: VERSION } = require("../package.json");
|
|
22
|
+
const { formatPlaybookOutput, handlePlaybookCli, playbookCommandNeedsBrowser } = require("./playbook-cli.cjs");
|
|
13
23
|
|
|
14
24
|
const IS_WIN = process.platform === "win32";
|
|
15
|
-
const {
|
|
25
|
+
const { SURF_TMP, formatSocketError } = require("./socket-path.cjs");
|
|
16
26
|
const { acquireBrowserLock } = require("./browser-lock.cjs");
|
|
27
|
+
const { selectEndpoint, connectEndpoint, formatEndpointError } = require("./endpoint.cjs");
|
|
28
|
+
const { createFrameParser, createSocketWriter, writeFrame } = require("./remote-transport.cjs");
|
|
29
|
+
const { resolveRequestDeadlineMs } = require("./host-sessions.cjs");
|
|
30
|
+
const { AUTO_SCREENSHOT_TOOLS, prepareRemoteTool, validateLocalToolPaths } = require("./file-transfer.cjs");
|
|
31
|
+
const { authorizeClient, listClients, revokeClient, getStateDir } = require("./remote-auth.cjs");
|
|
17
32
|
if (IS_WIN) { try { fs.mkdirSync(SURF_TMP, { recursive: true }); } catch {} }
|
|
18
33
|
|
|
19
34
|
function parseBrowserLockOptions(noLockFlag) {
|
|
@@ -29,11 +44,11 @@ function parseBrowserLockOptions(noLockFlag) {
|
|
|
29
44
|
return { noLock, timeoutMs };
|
|
30
45
|
}
|
|
31
46
|
|
|
32
|
-
function installBrowserLock({ noLock, timeoutMs }) {
|
|
47
|
+
function installBrowserLock({ noLock, timeoutMs }, endpoint) {
|
|
33
48
|
let releaseBrowserLock = () => {};
|
|
34
49
|
if (!noLock) {
|
|
35
50
|
try {
|
|
36
|
-
const lock = acquireBrowserLock(
|
|
51
|
+
const lock = acquireBrowserLock(endpoint.key, SURF_TMP, { timeoutMs });
|
|
37
52
|
releaseBrowserLock = lock.release;
|
|
38
53
|
} catch (error) {
|
|
39
54
|
console.error("Error:", error && error.message ? error.message : String(error));
|
|
@@ -58,249 +73,6 @@ function installBrowserLock({ noLock, timeoutMs }) {
|
|
|
58
73
|
});
|
|
59
74
|
}
|
|
60
75
|
|
|
61
|
-
// ============================================================================
|
|
62
|
-
// Workflow Resolution and Management
|
|
63
|
-
// ============================================================================
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Get workflow search directories
|
|
67
|
-
* @returns {Array<{path: string, scope: string}>}
|
|
68
|
-
*/
|
|
69
|
-
function getWorkflowDirs() {
|
|
70
|
-
return [
|
|
71
|
-
{ path: path.join(process.cwd(), '.surf', 'workflows'), scope: 'project' },
|
|
72
|
-
{ path: path.join(os.homedir(), '.surf', 'workflows'), scope: 'user' },
|
|
73
|
-
];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Resolve a workflow by name or path
|
|
78
|
-
* @param {string} nameOrPath - Workflow name or file path
|
|
79
|
-
* @returns {{ type: 'inline'|'file'|'not_found', content?: string, path?: string, name?: string }}
|
|
80
|
-
*/
|
|
81
|
-
function resolveWorkflow(nameOrPath) {
|
|
82
|
-
// Check if it's an inline workflow (contains pipe)
|
|
83
|
-
if (nameOrPath.includes('|')) {
|
|
84
|
-
return { type: 'inline', content: nameOrPath };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Check if it's a direct file path (with extension or path separator)
|
|
88
|
-
if (nameOrPath.includes('/') || nameOrPath.includes('\\') || nameOrPath.endsWith('.json')) {
|
|
89
|
-
if (fs.existsSync(nameOrPath)) {
|
|
90
|
-
return { type: 'file', path: nameOrPath };
|
|
91
|
-
}
|
|
92
|
-
return { type: 'not_found', name: nameOrPath };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Look up by name in workflow directories
|
|
96
|
-
const searchDirs = getWorkflowDirs();
|
|
97
|
-
|
|
98
|
-
for (const { path: dir } of searchDirs) {
|
|
99
|
-
const filePath = path.join(dir, `${nameOrPath}.json`);
|
|
100
|
-
if (fs.existsSync(filePath)) {
|
|
101
|
-
return { type: 'file', path: filePath };
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return { type: 'not_found', name: nameOrPath };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* List all available workflows
|
|
110
|
-
* @returns {Array<{name: string, description: string, scope: string, path: string, args?: object}>}
|
|
111
|
-
*/
|
|
112
|
-
function listWorkflows() {
|
|
113
|
-
const workflows = [];
|
|
114
|
-
const searchDirs = getWorkflowDirs();
|
|
115
|
-
|
|
116
|
-
for (const { path: dir, scope } of searchDirs) {
|
|
117
|
-
if (fs.existsSync(dir)) {
|
|
118
|
-
try {
|
|
119
|
-
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
120
|
-
for (const file of files) {
|
|
121
|
-
const filePath = path.join(dir, file);
|
|
122
|
-
try {
|
|
123
|
-
const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
124
|
-
workflows.push({
|
|
125
|
-
name: content.name || file.replace('.json', ''),
|
|
126
|
-
description: content.description || '',
|
|
127
|
-
scope,
|
|
128
|
-
path: filePath,
|
|
129
|
-
args: content.args,
|
|
130
|
-
stepCount: content.steps?.length || 0,
|
|
131
|
-
});
|
|
132
|
-
} catch {
|
|
133
|
-
// Skip invalid JSON files
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
} catch {
|
|
137
|
-
// Skip inaccessible directories
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return workflows;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Get detailed info about a workflow
|
|
147
|
-
* @param {string} name - Workflow name
|
|
148
|
-
* @returns {{ error?: string, name?: string, description?: string, args?: object, steps?: Array, path?: string }}
|
|
149
|
-
*/
|
|
150
|
-
function getWorkflowInfo(name) {
|
|
151
|
-
const resolved = resolveWorkflow(name);
|
|
152
|
-
|
|
153
|
-
if (resolved.type === 'not_found') {
|
|
154
|
-
return { error: `Workflow not found: ${name}` };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (resolved.type === 'inline') {
|
|
158
|
-
return { error: 'Cannot get info for inline workflows' };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
const content = JSON.parse(fs.readFileSync(resolved.path, 'utf8'));
|
|
163
|
-
return {
|
|
164
|
-
name: content.name || name,
|
|
165
|
-
description: content.description || '',
|
|
166
|
-
args: content.args || {},
|
|
167
|
-
steps: content.steps || [],
|
|
168
|
-
path: resolved.path,
|
|
169
|
-
};
|
|
170
|
-
} catch (e) {
|
|
171
|
-
return { error: `Failed to parse workflow: ${e.message}` };
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Validate workflow args against schema
|
|
177
|
-
* @param {object} workflow - Workflow with args schema
|
|
178
|
-
* @param {object} providedArgs - User-provided args
|
|
179
|
-
* @returns {string[]} - Array of error messages
|
|
180
|
-
*/
|
|
181
|
-
function validateWorkflowArgs(workflow, providedArgs) {
|
|
182
|
-
const errors = [];
|
|
183
|
-
if (workflow.args) {
|
|
184
|
-
for (const [name, spec] of Object.entries(workflow.args)) {
|
|
185
|
-
if (spec.required && providedArgs[name] === undefined) {
|
|
186
|
-
errors.push(`Missing required argument: --${name}`);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return errors;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Apply default values to workflow args
|
|
195
|
-
* @param {object} workflow - Workflow with args schema
|
|
196
|
-
* @param {object} providedArgs - User-provided args
|
|
197
|
-
* @returns {object} - Args with defaults applied
|
|
198
|
-
*/
|
|
199
|
-
function applyArgDefaults(workflow, providedArgs) {
|
|
200
|
-
const vars = { ...providedArgs };
|
|
201
|
-
if (workflow.args) {
|
|
202
|
-
for (const [name, spec] of Object.entries(workflow.args)) {
|
|
203
|
-
if (vars[name] === undefined && spec.default !== undefined) {
|
|
204
|
-
vars[name] = spec.default;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
return vars;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Validate a workflow JSON file
|
|
213
|
-
* @param {string} filePath - Path to workflow file
|
|
214
|
-
* @returns {{ valid: boolean, error?: string, workflow?: object }}
|
|
215
|
-
*/
|
|
216
|
-
function validateWorkflowFile(filePath) {
|
|
217
|
-
if (!fs.existsSync(filePath)) {
|
|
218
|
-
return { valid: false, error: `File not found: ${filePath}` };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
223
|
-
const workflow = JSON.parse(content);
|
|
224
|
-
|
|
225
|
-
// Basic structure validation
|
|
226
|
-
if (!workflow.steps || !Array.isArray(workflow.steps)) {
|
|
227
|
-
return { valid: false, error: "Workflow must have a 'steps' array" };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (workflow.steps.length === 0) {
|
|
231
|
-
return { valid: false, error: "Workflow has no steps" };
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Validate each step
|
|
235
|
-
for (let i = 0; i < workflow.steps.length; i++) {
|
|
236
|
-
const step = workflow.steps[i];
|
|
237
|
-
|
|
238
|
-
// Check for loops
|
|
239
|
-
if (step.repeat !== undefined || step.each !== undefined) {
|
|
240
|
-
if (!step.steps || !Array.isArray(step.steps)) {
|
|
241
|
-
return { valid: false, error: `Step ${i + 1}: loop must have a 'steps' array` };
|
|
242
|
-
}
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Regular step must have tool/cmd
|
|
247
|
-
if (!step.tool && !step.cmd) {
|
|
248
|
-
return { valid: false, error: `Step ${i + 1}: must have 'tool' field` };
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Validate args schema if present
|
|
253
|
-
if (workflow.args && typeof workflow.args !== 'object') {
|
|
254
|
-
return { valid: false, error: "'args' must be an object" };
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return { valid: true, workflow };
|
|
258
|
-
} catch (e) {
|
|
259
|
-
return { valid: false, error: `Invalid JSON: ${e.message}` };
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Format a step for display
|
|
265
|
-
* @param {object} step - Workflow step
|
|
266
|
-
* @param {number} indent - Indentation level
|
|
267
|
-
* @returns {string}
|
|
268
|
-
*/
|
|
269
|
-
function formatStep(step, indent = 0) {
|
|
270
|
-
const pad = ' '.repeat(indent);
|
|
271
|
-
|
|
272
|
-
if (step.repeat !== undefined) {
|
|
273
|
-
const lines = [`${pad}repeat ${step.repeat} times:`];
|
|
274
|
-
for (const s of step.steps || []) {
|
|
275
|
-
lines.push(formatStep(s, indent + 1));
|
|
276
|
-
}
|
|
277
|
-
if (step.until) {
|
|
278
|
-
lines.push(`${pad} until: ${step.until.tool || step.until.cmd}`);
|
|
279
|
-
}
|
|
280
|
-
return lines.join('\n');
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (step.each !== undefined) {
|
|
284
|
-
const lines = [`${pad}each ${step.each} as ${step.as || 'item'}:`];
|
|
285
|
-
for (const s of step.steps || []) {
|
|
286
|
-
lines.push(formatStep(s, indent + 1));
|
|
287
|
-
}
|
|
288
|
-
return lines.join('\n');
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const tool = step.tool || step.cmd;
|
|
292
|
-
const args = step.args || {};
|
|
293
|
-
const argStr = Object.entries(args)
|
|
294
|
-
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
295
|
-
.join(' ');
|
|
296
|
-
|
|
297
|
-
let line = `${pad}${tool}`;
|
|
298
|
-
if (argStr) line += ` ${argStr}`;
|
|
299
|
-
if (step.as) line += ` → ${step.as}`;
|
|
300
|
-
|
|
301
|
-
return line;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
76
|
// Cross-platform image resize (macOS: sips, Linux: ImageMagick)
|
|
305
77
|
function resizeImage(filePath, maxSize) {
|
|
306
78
|
const platform = process.platform;
|
|
@@ -336,7 +108,67 @@ function resizeImage(filePath, maxSize) {
|
|
|
336
108
|
return { success: false, error: e.message };
|
|
337
109
|
}
|
|
338
110
|
}
|
|
339
|
-
|
|
111
|
+
let args = process.argv.slice(2);
|
|
112
|
+
if (args[0] === "remote") {
|
|
113
|
+
const remoteArgs = args.slice(1);
|
|
114
|
+
const subcommand = remoteArgs[0];
|
|
115
|
+
const stateDir = getStateDir();
|
|
116
|
+
try {
|
|
117
|
+
if (subcommand === "authorize") {
|
|
118
|
+
const label = remoteArgs[1];
|
|
119
|
+
const outputIndex = remoteArgs.indexOf("--output");
|
|
120
|
+
const output = outputIndex === -1 ? undefined : remoteArgs[outputIndex + 1];
|
|
121
|
+
if (!label || !output || output.startsWith("--")) throw new Error("Usage: surf remote authorize <label> --output <credential-file>");
|
|
122
|
+
const client = authorizeClient(label, output, stateDir);
|
|
123
|
+
console.log(`Authorized remote client: ${client.label}`);
|
|
124
|
+
console.log(`Credential: ${client.output}`);
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
if (subcommand === "list") {
|
|
128
|
+
const clients = listClients(stateDir);
|
|
129
|
+
if (clients.length === 0) console.log("No authorized remote clients.");
|
|
130
|
+
else for (const client of clients) console.log(`${client.label}\t${client.id}\t${client.createdAt}`);
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
if (subcommand === "revoke") {
|
|
134
|
+
const label = remoteArgs[1];
|
|
135
|
+
if (!label || label.startsWith("--")) throw new Error("Usage: surf remote revoke <label>");
|
|
136
|
+
revokeClient(label, stateDir);
|
|
137
|
+
console.log(`Revoked remote client: ${label}`);
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
console.error("Usage: surf remote authorize <label> --output <credential-file> | list | revoke <label>");
|
|
141
|
+
process.exit(1);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error(`Error: ${error.message}`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let endpoint;
|
|
149
|
+
try {
|
|
150
|
+
({ args, endpoint } = selectEndpoint(args));
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error(`Error: ${error.message}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (["playbook", "pb", "use"].includes(args[0])) {
|
|
157
|
+
if (playbookCommandNeedsBrowser(args)) {
|
|
158
|
+
installBrowserLock(parseBrowserLockOptions(args.includes("--no-lock")), endpoint);
|
|
159
|
+
}
|
|
160
|
+
handlePlaybookCli(args, { endpoint, cwd: process.cwd() })
|
|
161
|
+
.then((result) => {
|
|
162
|
+
if (!result.handled) throw new Error("Playbook command was not handled");
|
|
163
|
+
if (result.value !== undefined) console.log(formatPlaybookOutput(result.value, result.json));
|
|
164
|
+
process.exit(0);
|
|
165
|
+
})
|
|
166
|
+
.catch((error) => {
|
|
167
|
+
console.error(`Error: ${error.message}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
340
172
|
|
|
341
173
|
const ALIASES = {
|
|
342
174
|
snap: "screenshot",
|
|
@@ -405,7 +237,7 @@ const TOOLS = {
|
|
|
405
237
|
args: ["query"],
|
|
406
238
|
opts: {
|
|
407
239
|
"with-page": "Include current page context",
|
|
408
|
-
model: "Model: gemini-3-pro (default), gemini-
|
|
240
|
+
model: "Model: gemini-3.1-pro (default), gemini-3.5-flash, gemini-3.1-flash-lite",
|
|
409
241
|
file: "Attach file to analyze",
|
|
410
242
|
"generate-image": "Generate image and save to path",
|
|
411
243
|
"edit-image": "Edit existing image (use with --output)",
|
|
@@ -528,6 +360,12 @@ const TOOLS = {
|
|
|
528
360
|
opts: { ids: "Close multiple tabs" },
|
|
529
361
|
examples: [{ cmd: "tab.close 123", desc: "Close tab" }]
|
|
530
362
|
},
|
|
363
|
+
"tab.move": {
|
|
364
|
+
desc: "Move tab to another window",
|
|
365
|
+
args: ["id"],
|
|
366
|
+
opts: { ids: "Move multiple tabs", "to-window": "Destination window ID", index: "Destination index" },
|
|
367
|
+
examples: [{ cmd: "tab.move 123 --to-window 456", desc: "Move tab to window" }]
|
|
368
|
+
},
|
|
531
369
|
"tab.name": {
|
|
532
370
|
desc: "Register current tab with a name",
|
|
533
371
|
args: ["name"],
|
|
@@ -675,6 +513,7 @@ const TOOLS = {
|
|
|
675
513
|
"no-text": "Exclude visible text content",
|
|
676
514
|
depth: "Maximum tree depth (default: unlimited)",
|
|
677
515
|
compact: "Remove empty structural elements",
|
|
516
|
+
"max-bytes": "Maximum visible text bytes",
|
|
678
517
|
},
|
|
679
518
|
examples: [
|
|
680
519
|
{ cmd: "page.read", desc: "Interactive elements + text content" },
|
|
@@ -682,7 +521,7 @@ const TOOLS = {
|
|
|
682
521
|
{ cmd: "page.read --no-text", desc: "Interactive elements only (no text)" },
|
|
683
522
|
{ cmd: "page.read --depth 3", desc: "Limit to 3 levels deep" },
|
|
684
523
|
{ cmd: "page.read --compact", desc: "Skip empty containers" },
|
|
685
|
-
{ cmd: "page.read --depth 3 --compact", desc: "Shallow + compact
|
|
524
|
+
{ cmd: "page.read --depth 3 --compact --max-bytes 2000", desc: "Shallow + compact output" },
|
|
686
525
|
{ cmd: "read", desc: "Alias" },
|
|
687
526
|
]
|
|
688
527
|
},
|
|
@@ -823,7 +662,7 @@ const TOOLS = {
|
|
|
823
662
|
ref: "Element ref (uses JS DOM method, more reliable for modals)",
|
|
824
663
|
submit: "Press enter after",
|
|
825
664
|
clear: "Clear first",
|
|
826
|
-
method: "cdp|js (
|
|
665
|
+
method: "cdp|js (cursor typing uses CDP; selector/ref targets use JS)"
|
|
827
666
|
},
|
|
828
667
|
examples: [
|
|
829
668
|
{ cmd: 'type "hello world"', desc: "Type at cursor (CDP events)" },
|
|
@@ -896,6 +735,9 @@ const TOOLS = {
|
|
|
896
735
|
all: "Show all (no limit)",
|
|
897
736
|
v: "Verbose output",
|
|
898
737
|
vv: "Very verbose output",
|
|
738
|
+
"body-mode": "Response bodies: none, text, or all (default: text)",
|
|
739
|
+
"per-body-bytes": "Maximum captured bytes per response body",
|
|
740
|
+
"total-body-bytes": "Maximum captured response-body bytes per tab session",
|
|
899
741
|
clear: "Clear after reading",
|
|
900
742
|
stream: "Continuous output"
|
|
901
743
|
},
|
|
@@ -960,9 +802,17 @@ const TOOLS = {
|
|
|
960
802
|
"network.export": {
|
|
961
803
|
desc: "Export captured requests",
|
|
962
804
|
args: [],
|
|
963
|
-
opts: {
|
|
805
|
+
opts: {
|
|
806
|
+
har: "Export as HAR 1.2",
|
|
807
|
+
jsonl: "Export as JSONL",
|
|
808
|
+
output: "Output file path",
|
|
809
|
+
"body-mode": "Response bodies: none, text, or all (default: text)",
|
|
810
|
+
"per-body-bytes": "Maximum captured bytes per response body",
|
|
811
|
+
"total-body-bytes": "Maximum captured response-body bytes per tab session",
|
|
812
|
+
},
|
|
964
813
|
examples: [
|
|
965
|
-
{ cmd: "network.export --jsonl --output /tmp/requests.jsonl", desc: "Export as JSONL" }
|
|
814
|
+
{ cmd: "network.export --jsonl --output /tmp/requests.jsonl", desc: "Export as JSONL" },
|
|
815
|
+
{ cmd: "network.export --har --output /tmp/requests.har", desc: "Export as HAR" },
|
|
966
816
|
]
|
|
967
817
|
},
|
|
968
818
|
"network.path": {
|
|
@@ -1567,7 +1417,7 @@ const ALL_SOCKET_TOOLS = [
|
|
|
1567
1417
|
"computer",
|
|
1568
1418
|
"page.read", "page.text", "page.state",
|
|
1569
1419
|
"locate.role", "locate.text", "locate.label",
|
|
1570
|
-
"tab.list", "tab.new", "tab.switch", "tab.close", "tab.name", "tab.unname", "tab.named",
|
|
1420
|
+
"tab.list", "tab.new", "tab.switch", "tab.close", "tab.move", "tab.name", "tab.unname", "tab.named",
|
|
1571
1421
|
"tab.group", "tab.ungroup", "tab.groups", "tab.reload",
|
|
1572
1422
|
"scroll.top", "scroll.bottom", "scroll.to", "scroll.info",
|
|
1573
1423
|
"wait.element", "wait.network", "wait.url", "wait.dom", "wait.load",
|
|
@@ -1659,6 +1509,10 @@ Quick Examples:
|
|
|
1659
1509
|
surf window.new "https://example.com" && surf --window-id 123 go "https://other.com"
|
|
1660
1510
|
|
|
1661
1511
|
More Help:
|
|
1512
|
+
--remote <host>:<port> Route requests to a remote native host
|
|
1513
|
+
--remote-credential <path> Use a mode-0600 Ed25519 remote credential file
|
|
1514
|
+
surf remote authorize <label> --output <path>
|
|
1515
|
+
surf remote list | surf remote revoke <label>
|
|
1662
1516
|
surf --help-full All commands
|
|
1663
1517
|
surf --llm-context Compact reference for AI agents
|
|
1664
1518
|
surf --help-topic <topic> Topic guide (refs, semantic, frames, devices...)
|
|
@@ -1701,6 +1555,10 @@ const showFullHelp = () => {
|
|
|
1701
1555
|
|
|
1702
1556
|
Usage: surf <command> [args] [options]
|
|
1703
1557
|
|
|
1558
|
+
Playbooks:
|
|
1559
|
+
surf playbook|pb <list|show|ops|run|record|suggest|save|client|trace|export|import>
|
|
1560
|
+
surf use <playbook> <op> [--arg value]
|
|
1561
|
+
|
|
1704
1562
|
`);
|
|
1705
1563
|
for (const [groupName, group] of Object.entries(TOOLS)) {
|
|
1706
1564
|
console.log(`${groupName.toUpperCase()} - ${group.desc}`);
|
|
@@ -1715,6 +1573,8 @@ Usage: surf <command> [args] [options]
|
|
|
1715
1573
|
console.log(`Aliases: snap -> screenshot, read -> page.read, find -> search, go -> navigate
|
|
1716
1574
|
|
|
1717
1575
|
Options:
|
|
1576
|
+
--remote <host>:<port> Route requests to a remote native host
|
|
1577
|
+
--remote-credential <path> Use a mode-0600 Ed25519 remote credential file
|
|
1718
1578
|
--tab-id <id> Target specific tab
|
|
1719
1579
|
--window-id <id> Target specific window (isolate from your browsing)
|
|
1720
1580
|
--json Output raw JSON
|
|
@@ -1722,6 +1582,11 @@ Options:
|
|
|
1722
1582
|
--soft-fail On error: warn and exit 0 (for non-critical commands)
|
|
1723
1583
|
--no-lock Bypass the per-socket browser request lock
|
|
1724
1584
|
|
|
1585
|
+
Remote Credentials (run on the browser host):
|
|
1586
|
+
surf remote authorize <label> --output <credential-file>
|
|
1587
|
+
surf remote list
|
|
1588
|
+
surf remote revoke <label>
|
|
1589
|
+
|
|
1725
1590
|
Script Mode:
|
|
1726
1591
|
surf --script <file> Run workflow from JSON
|
|
1727
1592
|
surf --script <file> --dry-run
|
|
@@ -1937,7 +1802,7 @@ if (args[0] === "server") {
|
|
|
1937
1802
|
process.exit(0);
|
|
1938
1803
|
}
|
|
1939
1804
|
const { PiChromeMcpServer } = require("./mcp-server.cjs");
|
|
1940
|
-
const server = new PiChromeMcpServer();
|
|
1805
|
+
const server = new PiChromeMcpServer(endpoint);
|
|
1941
1806
|
server.start().catch((err) => {
|
|
1942
1807
|
console.error("MCP Server error:", err.message);
|
|
1943
1808
|
process.exit(1);
|
|
@@ -1953,7 +1818,7 @@ if (args[0] === "extension-path" || args[0] === "path") {
|
|
|
1953
1818
|
|
|
1954
1819
|
if (args[0] === "doctor") {
|
|
1955
1820
|
const { runDoctorCli } = require("./doctor.cjs");
|
|
1956
|
-
runDoctorCli(args.slice(1)).then((code) => process.exit(code));
|
|
1821
|
+
runDoctorCli(args.slice(1), endpoint).then((code) => process.exit(code));
|
|
1957
1822
|
return;
|
|
1958
1823
|
}
|
|
1959
1824
|
|
|
@@ -1978,6 +1843,8 @@ Options:
|
|
|
1978
1843
|
Multiple: --browser chrome,brave
|
|
1979
1844
|
--target Install target: auto, linux, windows
|
|
1980
1845
|
On WSL2, auto installs for Windows Chrome. Use linux for WSLg/Linux browsers.
|
|
1846
|
+
--listen <tailscale-ip>:<port>
|
|
1847
|
+
Requires surf remote authorize <label> --output <path> first.
|
|
1981
1848
|
|
|
1982
1849
|
Examples:
|
|
1983
1850
|
surf install hnfbepgmaoklhekckbpjnleifhahkcpl
|
|
@@ -2121,44 +1988,24 @@ if (args.includes("--script")) {
|
|
|
2121
1988
|
process.exit(1);
|
|
2122
1989
|
}
|
|
2123
1990
|
|
|
1991
|
+
let scriptTransport;
|
|
2124
1992
|
const sendScriptRequest = (toolName, toolArgs = {}) => {
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
});
|
|
2136
|
-
let buf = "";
|
|
2137
|
-
sock.on("data", (d) => {
|
|
2138
|
-
buf += d.toString();
|
|
2139
|
-
const lines = buf.split("\n");
|
|
2140
|
-
buf = lines.pop();
|
|
2141
|
-
for (const line of lines) {
|
|
2142
|
-
if (!line.trim()) continue;
|
|
2143
|
-
try {
|
|
2144
|
-
const resp = JSON.parse(line);
|
|
2145
|
-
sock.end();
|
|
2146
|
-
resolve(resp);
|
|
2147
|
-
} catch {
|
|
2148
|
-
sock.end();
|
|
2149
|
-
reject(new Error("Invalid JSON"));
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
});
|
|
2153
|
-
sock.on("error", (e) => reject(new Error(formatSocketError(e))));
|
|
2154
|
-
let timeoutId;
|
|
2155
|
-
timeoutId = setTimeout(() => { sock.destroy(); reject(new Error("Timeout")); }, 30000);
|
|
2156
|
-
sock.on("close", () => clearTimeout(timeoutId));
|
|
2157
|
-
});
|
|
1993
|
+
const req = {
|
|
1994
|
+
type: "tool_request",
|
|
1995
|
+
method: "execute_tool",
|
|
1996
|
+
params: { tool: toolName, args: toolArgs },
|
|
1997
|
+
id: "cli-" + Date.now() + "-" + Math.random(),
|
|
1998
|
+
};
|
|
1999
|
+
if (scriptTabId) req.tabId = parseInt(scriptTabId, 10);
|
|
2000
|
+
const prepared = endpoint.kind === "remote" ? prepareRemoteTool(toolName, toolArgs) : (() => { const args = validateLocalToolPaths(toolName, toolArgs); return { args, uploads: [], downloads: [] }; })();
|
|
2001
|
+
req.params.args = prepared.args;
|
|
2002
|
+
return scriptTransport.request(req, resolveRequestDeadlineMs(toolName, prepared.args), prepared);
|
|
2158
2003
|
};
|
|
2159
2004
|
|
|
2160
2005
|
const runScript = async () => {
|
|
2161
|
-
|
|
2006
|
+
try {
|
|
2007
|
+
if (!dryRun) scriptTransport = await openClientTransport(endpoint);
|
|
2008
|
+
const total = script.steps.length;
|
|
2162
2009
|
const results = [];
|
|
2163
2010
|
let failed = 0;
|
|
2164
2011
|
|
|
@@ -2216,14 +2063,22 @@ if (args.includes("--script")) {
|
|
|
2216
2063
|
console.log(`Summary: ${passed} passed, ${failed} failed, ${total} total`);
|
|
2217
2064
|
}
|
|
2218
2065
|
|
|
2219
|
-
|
|
2066
|
+
return failed > 0 ? 1 : 0;
|
|
2067
|
+
} finally {
|
|
2068
|
+
await scriptTransport?.close();
|
|
2069
|
+
}
|
|
2220
2070
|
};
|
|
2221
2071
|
|
|
2222
2072
|
if (!dryRun) {
|
|
2223
|
-
installBrowserLock(parseBrowserLockOptions(args.includes("--no-lock")));
|
|
2073
|
+
installBrowserLock(parseBrowserLockOptions(args.includes("--no-lock")), endpoint);
|
|
2224
2074
|
}
|
|
2225
2075
|
|
|
2226
|
-
runScript()
|
|
2076
|
+
runScript()
|
|
2077
|
+
.then((code) => process.exit(code))
|
|
2078
|
+
.catch((error) => {
|
|
2079
|
+
console.error(`Error: ${error.message}`);
|
|
2080
|
+
process.exit(1);
|
|
2081
|
+
});
|
|
2227
2082
|
return;
|
|
2228
2083
|
}
|
|
2229
2084
|
|
|
@@ -2346,9 +2201,7 @@ if (args[0] === "do") {
|
|
|
2346
2201
|
|
|
2347
2202
|
// Process workflow file if loaded
|
|
2348
2203
|
if (workflow) {
|
|
2349
|
-
|
|
2350
|
-
throw new Error("Workflow must have a 'steps' array");
|
|
2351
|
-
}
|
|
2204
|
+
workflow = normalizeWorkflow(workflow);
|
|
2352
2205
|
|
|
2353
2206
|
// Validate required args
|
|
2354
2207
|
const argErrors = validateWorkflowArgs(workflow, workflowArgs);
|
|
@@ -2368,30 +2221,7 @@ if (args[0] === "do") {
|
|
|
2368
2221
|
process.exit(1);
|
|
2369
2222
|
}
|
|
2370
2223
|
|
|
2371
|
-
|
|
2372
|
-
// Also preserve loop steps as-is
|
|
2373
|
-
steps = workflow.steps.map(s => {
|
|
2374
|
-
if (s.repeat !== undefined || s.each !== undefined) {
|
|
2375
|
-
// Loop step - convert nested steps recursively
|
|
2376
|
-
const convertSteps = (stepsArr) => stepsArr.map(ns => {
|
|
2377
|
-
if (ns.repeat !== undefined || ns.each !== undefined) {
|
|
2378
|
-
// Recursively convert nested loop steps and until condition
|
|
2379
|
-
return {
|
|
2380
|
-
...ns,
|
|
2381
|
-
steps: convertSteps(ns.steps || []),
|
|
2382
|
-
until: ns.until ? { cmd: ns.until.tool || ns.until.cmd, args: ns.until.args || {} } : undefined
|
|
2383
|
-
};
|
|
2384
|
-
}
|
|
2385
|
-
return { cmd: ns.tool || ns.cmd, args: ns.args || {}, as: ns.as };
|
|
2386
|
-
});
|
|
2387
|
-
return {
|
|
2388
|
-
...s,
|
|
2389
|
-
steps: convertSteps(s.steps || []),
|
|
2390
|
-
until: s.until ? { cmd: s.until.tool || s.until.cmd, args: s.until.args || {} } : undefined
|
|
2391
|
-
};
|
|
2392
|
-
}
|
|
2393
|
-
return { cmd: s.tool || s.cmd, args: s.args || {}, as: s.as };
|
|
2394
|
-
});
|
|
2224
|
+
steps = workflow.steps;
|
|
2395
2225
|
}
|
|
2396
2226
|
} catch (e) {
|
|
2397
2227
|
console.error(`Error: Failed to parse workflow: ${e.message}`);
|
|
@@ -2425,7 +2255,7 @@ if (args[0] === "do") {
|
|
|
2425
2255
|
process.exit(0);
|
|
2426
2256
|
}
|
|
2427
2257
|
|
|
2428
|
-
installBrowserLock(parseBrowserLockOptions(doArgs.includes("--no-lock")));
|
|
2258
|
+
installBrowserLock(parseBrowserLockOptions(doArgs.includes("--no-lock")), endpoint);
|
|
2429
2259
|
|
|
2430
2260
|
if (!wantJson) {
|
|
2431
2261
|
if (workflowName) {
|
|
@@ -2436,7 +2266,10 @@ if (args[0] === "do") {
|
|
|
2436
2266
|
}
|
|
2437
2267
|
|
|
2438
2268
|
const runWorkflow = async () => {
|
|
2439
|
-
|
|
2269
|
+
let transport;
|
|
2270
|
+
try {
|
|
2271
|
+
transport = await openClientTransport(endpoint);
|
|
2272
|
+
const result = await executeDoSteps(steps, {
|
|
2440
2273
|
onError,
|
|
2441
2274
|
autoWait: !noAutoWait,
|
|
2442
2275
|
stepDelay,
|
|
@@ -2445,30 +2278,40 @@ if (args[0] === "do") {
|
|
|
2445
2278
|
context: {
|
|
2446
2279
|
tabId,
|
|
2447
2280
|
windowId,
|
|
2281
|
+
endpoint,
|
|
2282
|
+
transport,
|
|
2448
2283
|
},
|
|
2449
|
-
|
|
2284
|
+
});
|
|
2450
2285
|
|
|
2451
2286
|
// Print summary
|
|
2452
2287
|
if (wantJson) {
|
|
2453
2288
|
console.log(JSON.stringify(result, null, 2));
|
|
2454
|
-
|
|
2289
|
+
return result.status === "completed" ? 0 : 1;
|
|
2455
2290
|
}
|
|
2456
2291
|
|
|
2457
2292
|
console.log("");
|
|
2458
2293
|
if (result.status === "completed") {
|
|
2459
2294
|
console.log(`Completed: ${result.completedSteps}/${result.totalSteps} steps (${result.totalMs}ms)`);
|
|
2460
|
-
|
|
2295
|
+
return 0;
|
|
2461
2296
|
} else if (result.status === "partial") {
|
|
2462
2297
|
console.log(`Partial: ${result.completedSteps}/${result.totalSteps} steps completed, ${result.failed} failed`);
|
|
2463
|
-
|
|
2298
|
+
return 1;
|
|
2464
2299
|
} else {
|
|
2465
2300
|
console.error(`Failed: ${result.completedSteps}/${result.totalSteps} steps completed`);
|
|
2466
2301
|
if (result.error) console.error(`Error: ${result.error}`);
|
|
2467
|
-
|
|
2302
|
+
return 1;
|
|
2303
|
+
}
|
|
2304
|
+
} finally {
|
|
2305
|
+
transport?.close();
|
|
2468
2306
|
}
|
|
2469
2307
|
};
|
|
2470
2308
|
|
|
2471
|
-
runWorkflow()
|
|
2309
|
+
runWorkflow()
|
|
2310
|
+
.then((code) => process.exit(code))
|
|
2311
|
+
.catch((error) => {
|
|
2312
|
+
console.error(`Error: ${error.message}`);
|
|
2313
|
+
process.exit(1);
|
|
2314
|
+
});
|
|
2472
2315
|
return;
|
|
2473
2316
|
}
|
|
2474
2317
|
|
|
@@ -2595,8 +2438,6 @@ if (args[0] === "workflow.validate") {
|
|
|
2595
2438
|
|
|
2596
2439
|
const BOOLEAN_FLAGS = ["auto-capture", "json", "stream", "dry-run", "stop-on-error", "fail-fast", "clear", "submit", "all", "case-sensitive", "hard", "annotate", "fullpage", "full-page", "reset", "no-screenshot", "full", "soft-fail", "has-body", "exclude-static", "v", "vv", "request", "by-tab", "har", "jsonl", "no-save", "no-auto-wait", "no-lock"];
|
|
2597
2440
|
|
|
2598
|
-
const AUTO_SCREENSHOT_TOOLS = ["click", "type", "key", "smart_type", "form.fill", "form_input", "drag", "hover", "scroll", "scroll.top", "scroll.bottom", "scroll.to", "dialog.accept", "dialog.dismiss", "js", "eval"];
|
|
2599
|
-
|
|
2600
2441
|
const parseArgs = (rawArgs) => {
|
|
2601
2442
|
const result = { positional: [], options: {} };
|
|
2602
2443
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
@@ -2732,6 +2573,7 @@ const PRIMARY_ARG_MAP = {
|
|
|
2732
2573
|
"tab.switch": "id",
|
|
2733
2574
|
close_tab: "tab_id",
|
|
2734
2575
|
"tab.close": "id",
|
|
2576
|
+
"tab.move": "id",
|
|
2735
2577
|
"tab.name": "name",
|
|
2736
2578
|
"tab.unname": "name",
|
|
2737
2579
|
scroll_to_position: "position",
|
|
@@ -2763,7 +2605,7 @@ const PRIMARY_ARG_MAP = {
|
|
|
2763
2605
|
"select": "selector",
|
|
2764
2606
|
};
|
|
2765
2607
|
|
|
2766
|
-
|
|
2608
|
+
let toolArgs = { ...options };
|
|
2767
2609
|
|
|
2768
2610
|
if (tool === "scroll" && firstArg) {
|
|
2769
2611
|
if (firstArg === "top" || firstArg === "bottom") {
|
|
@@ -2824,7 +2666,7 @@ if (firstArg !== undefined) {
|
|
|
2824
2666
|
}
|
|
2825
2667
|
}
|
|
2826
2668
|
|
|
2827
|
-
if (tool === "js" && toolArgs.file) {
|
|
2669
|
+
if ((tool === "js" || tool === "frame.js") && toolArgs.file) {
|
|
2828
2670
|
try {
|
|
2829
2671
|
toolArgs.code = fs.readFileSync(toolArgs.file, "utf8");
|
|
2830
2672
|
delete toolArgs.file;
|
|
@@ -2834,6 +2676,17 @@ if (tool === "js" && toolArgs.file) {
|
|
|
2834
2676
|
}
|
|
2835
2677
|
}
|
|
2836
2678
|
|
|
2679
|
+
if (tool === "batch" && toolArgs.file) {
|
|
2680
|
+
try {
|
|
2681
|
+
const parsed = JSON.parse(fs.readFileSync(toolArgs.file, "utf8"));
|
|
2682
|
+
toolArgs.actions = parsed;
|
|
2683
|
+
delete toolArgs.file;
|
|
2684
|
+
} catch (e) {
|
|
2685
|
+
console.error(`Error: Failed to read batch file: ${e.message}`);
|
|
2686
|
+
process.exit(1);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2837
2690
|
// Handle select command: capture multiple values after selector
|
|
2838
2691
|
if (tool === "select" && positional.length > 2) {
|
|
2839
2692
|
const values = positional.slice(2); // All args after "select <selector>"
|
|
@@ -2869,9 +2722,9 @@ if (toolArgs["window-id"] !== undefined) {
|
|
|
2869
2722
|
globalOpts.windowId = wid;
|
|
2870
2723
|
delete toolArgs["window-id"];
|
|
2871
2724
|
}
|
|
2872
|
-
if (toolArgs["network-path"] !== undefined) {
|
|
2873
|
-
|
|
2874
|
-
|
|
2725
|
+
if (toolArgs["network-path"] !== undefined && typeof toolArgs["network-path"] !== "string") {
|
|
2726
|
+
console.error("Error: --network-path requires a directory");
|
|
2727
|
+
process.exit(1);
|
|
2875
2728
|
}
|
|
2876
2729
|
const wantJson = toolArgs.json === true;
|
|
2877
2730
|
delete toolArgs.json;
|
|
@@ -2898,23 +2751,18 @@ if (tool === "aistudio.build" && outputPath) {
|
|
|
2898
2751
|
toolArgs.output = path.resolve(outputPath);
|
|
2899
2752
|
}
|
|
2900
2753
|
if (tool === "gemini") {
|
|
2901
|
-
if (outputPath) toolArgs.output =
|
|
2902
|
-
if (toolArgs
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
toolArgs.file = path.resolve(toolArgs.file);
|
|
2754
|
+
if (outputPath !== undefined) toolArgs.output = outputPath;
|
|
2755
|
+
if (toolArgs.model) {
|
|
2756
|
+
const known = ["gemini-3.1-pro", "gemini-3.5-flash", "gemini-3.1-flash-lite"];
|
|
2757
|
+
if (!known.includes(toolArgs.model)) {
|
|
2758
|
+
process.stderr.write(
|
|
2759
|
+
`warning: unknown Gemini model "${toolArgs.model}"; using "gemini-3.1-pro". Available: ${known.join(", ")}\n`,
|
|
2760
|
+
);
|
|
2761
|
+
}
|
|
2910
2762
|
}
|
|
2911
2763
|
}
|
|
2912
|
-
if (tool === "
|
|
2913
|
-
|
|
2914
|
-
toolArgs.file = toolArgs.file.map((filePath) => path.resolve(filePath));
|
|
2915
|
-
} else if (typeof toolArgs.file === "string") {
|
|
2916
|
-
toolArgs.file = path.resolve(toolArgs.file);
|
|
2917
|
-
}
|
|
2764
|
+
if (tool === "network.export" && outputPath !== undefined) {
|
|
2765
|
+
toolArgs.output = outputPath;
|
|
2918
2766
|
}
|
|
2919
2767
|
|
|
2920
2768
|
if ((tool === "screenshot" || tool === "record" || tool === "perf-audit") && outputPath && typeof outputPath !== "string") {
|
|
@@ -2938,7 +2786,9 @@ const streamMode = toolArgs.stream === true;
|
|
|
2938
2786
|
delete toolArgs.stream;
|
|
2939
2787
|
|
|
2940
2788
|
const streamLevel = toolArgs.level;
|
|
2941
|
-
|
|
2789
|
+
if (tool === "console" || tool === "network") {
|
|
2790
|
+
delete toolArgs.level;
|
|
2791
|
+
}
|
|
2942
2792
|
|
|
2943
2793
|
const streamFilter = toolArgs.filter;
|
|
2944
2794
|
delete toolArgs.filter;
|
|
@@ -2946,11 +2796,15 @@ delete toolArgs.filter;
|
|
|
2946
2796
|
let finalTool = tool;
|
|
2947
2797
|
if (methodFlag === "js") {
|
|
2948
2798
|
if (tool === "type") {
|
|
2949
|
-
if (
|
|
2950
|
-
|
|
2951
|
-
|
|
2799
|
+
if (toolArgs.ref) {
|
|
2800
|
+
finalTool = "type";
|
|
2801
|
+
} else {
|
|
2802
|
+
if (!toolArgs.selector) {
|
|
2803
|
+
console.error("Error: --selector, --into, or --ref required for type with --method js");
|
|
2804
|
+
process.exit(1);
|
|
2805
|
+
}
|
|
2806
|
+
finalTool = "smart_type";
|
|
2952
2807
|
}
|
|
2953
|
-
finalTool = "smart_type";
|
|
2954
2808
|
} else if (tool === "click") {
|
|
2955
2809
|
if (!toolArgs.selector) {
|
|
2956
2810
|
console.error("Error: --selector required for click with --method js");
|
|
@@ -2961,8 +2815,13 @@ if (methodFlag === "js") {
|
|
|
2961
2815
|
finalTool = "js";
|
|
2962
2816
|
}
|
|
2963
2817
|
} else if (methodFlag === "cdp") {
|
|
2818
|
+
if (tool === "type" && (toolArgs.selector || toolArgs.ref)) {
|
|
2819
|
+
console.error("Error: --method cdp types at the current focus and cannot be combined with --into, --selector, or --ref");
|
|
2820
|
+
process.exit(1);
|
|
2821
|
+
}
|
|
2964
2822
|
if (tool === "smart_type") {
|
|
2965
|
-
|
|
2823
|
+
console.error("Error: smart_type uses the JS input path and cannot be combined with --method cdp");
|
|
2824
|
+
process.exit(1);
|
|
2966
2825
|
}
|
|
2967
2826
|
}
|
|
2968
2827
|
|
|
@@ -2980,8 +2839,10 @@ if (streamMode && (tool === "console" || tool === "network")) {
|
|
|
2980
2839
|
|
|
2981
2840
|
let connectionTimeout = null;
|
|
2982
2841
|
let receivedData = false;
|
|
2842
|
+
let streamWriter;
|
|
2983
2843
|
|
|
2984
|
-
const sock =
|
|
2844
|
+
const sock = connectEndpoint(endpoint, () => {
|
|
2845
|
+
streamWriter = createSocketWriter(sock, { onOverflow: ({ error }) => sock.destroy(error) });
|
|
2985
2846
|
const req = {
|
|
2986
2847
|
type: "stream_request",
|
|
2987
2848
|
streamType,
|
|
@@ -2989,7 +2850,11 @@ if (streamMode && (tool === "console" || tool === "network")) {
|
|
|
2989
2850
|
id: "cli-stream-" + Date.now(),
|
|
2990
2851
|
...globalOpts,
|
|
2991
2852
|
};
|
|
2992
|
-
|
|
2853
|
+
streamWriter.send(req).catch((error) => sock.destroy(error));
|
|
2854
|
+
if (connectionTimeout) {
|
|
2855
|
+
clearTimeout(connectionTimeout);
|
|
2856
|
+
connectionTimeout = null;
|
|
2857
|
+
}
|
|
2993
2858
|
connectionTimeout = setTimeout(() => {
|
|
2994
2859
|
if (!receivedData) {
|
|
2995
2860
|
console.error("Error: Stream connection timeout (10s) - no data received");
|
|
@@ -2999,57 +2864,62 @@ if (streamMode && (tool === "console" || tool === "network")) {
|
|
|
2999
2864
|
}, 10000);
|
|
3000
2865
|
});
|
|
3001
2866
|
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
if (!line.trim()) continue;
|
|
3016
|
-
try {
|
|
3017
|
-
const msg = JSON.parse(line);
|
|
3018
|
-
if (msg.error) {
|
|
3019
|
-
console.error("Error:", msg.error);
|
|
3020
|
-
sock.end();
|
|
3021
|
-
process.exit(1);
|
|
3022
|
-
}
|
|
3023
|
-
if (msg.type === "extension_disconnected") {
|
|
3024
|
-
console.error(msg.message);
|
|
3025
|
-
sock.end();
|
|
3026
|
-
process.exit(1);
|
|
3027
|
-
}
|
|
3028
|
-
if (msg.type === "stream_started") {
|
|
3029
|
-
continue;
|
|
3030
|
-
}
|
|
3031
|
-
if (msg.type === "console_event") {
|
|
3032
|
-
const { level, text, timestamp } = msg;
|
|
3033
|
-
if (streamLevel && level !== streamLevel) continue;
|
|
3034
|
-
console.log(`[console] [${level}] ${formatTime(timestamp)} ${text}`);
|
|
3035
|
-
} else if (msg.type === "network_event") {
|
|
3036
|
-
const { method, url, status, duration } = msg;
|
|
3037
|
-
if (streamFilter && !url.includes(streamFilter)) continue;
|
|
3038
|
-
const statusStr = status !== undefined ? status : "...";
|
|
3039
|
-
const durationStr = duration !== undefined ? ` (${duration}ms)` : "";
|
|
3040
|
-
console.log(`[network] ${method} ${url} ${statusStr}${durationStr}`);
|
|
2867
|
+
connectionTimeout = setTimeout(() => {
|
|
2868
|
+
console.error(`Error: Stream connection timeout (10s) - could not connect to ${endpoint.display}`);
|
|
2869
|
+
sock.destroy();
|
|
2870
|
+
process.exit(1);
|
|
2871
|
+
}, 10000);
|
|
2872
|
+
|
|
2873
|
+
const parser = createFrameParser({
|
|
2874
|
+
onFrame(msg) {
|
|
2875
|
+
if (!receivedData) {
|
|
2876
|
+
receivedData = true;
|
|
2877
|
+
if (connectionTimeout) {
|
|
2878
|
+
clearTimeout(connectionTimeout);
|
|
2879
|
+
connectionTimeout = null;
|
|
3041
2880
|
}
|
|
3042
|
-
}
|
|
3043
|
-
|
|
2881
|
+
}
|
|
2882
|
+
if (msg.error) {
|
|
2883
|
+
console.error("Error:", msg.error);
|
|
2884
|
+
sock.end();
|
|
2885
|
+
process.exit(1);
|
|
2886
|
+
}
|
|
2887
|
+
if (msg.type === "extension_disconnected") {
|
|
2888
|
+
console.error(msg.message);
|
|
2889
|
+
sock.end();
|
|
2890
|
+
process.exit(1);
|
|
2891
|
+
}
|
|
2892
|
+
if (msg.type === "stream_started") return;
|
|
2893
|
+
if (msg.type === "console_event") {
|
|
2894
|
+
const { level, text, timestamp } = msg;
|
|
2895
|
+
if (streamLevel && level !== streamLevel) return;
|
|
2896
|
+
console.log(`[console] [${level}] ${formatTime(timestamp)} ${text}`);
|
|
2897
|
+
} else if (msg.type === "network_event") {
|
|
2898
|
+
const { method, url, status, duration } = msg;
|
|
2899
|
+
if (streamFilter && !url.includes(streamFilter)) return;
|
|
2900
|
+
const statusStr = status !== undefined ? status : "...";
|
|
2901
|
+
const durationStr = duration !== undefined ? ` (${duration}ms)` : "";
|
|
2902
|
+
console.log(`[network] ${method} ${url} ${statusStr}${durationStr}`);
|
|
2903
|
+
}
|
|
2904
|
+
},
|
|
2905
|
+
onError(error) {
|
|
2906
|
+
if (connectionTimeout) clearTimeout(connectionTimeout);
|
|
2907
|
+
console.error("Error:", error.message);
|
|
2908
|
+
sock.destroy();
|
|
2909
|
+
process.exit(1);
|
|
2910
|
+
},
|
|
3044
2911
|
});
|
|
2912
|
+
sock.on("data", (data) => parser.push(data));
|
|
3045
2913
|
|
|
3046
2914
|
sock.on("error", (e) => {
|
|
3047
|
-
|
|
2915
|
+
if (connectionTimeout) clearTimeout(connectionTimeout);
|
|
2916
|
+
console.error("Error:", formatEndpointError(e, endpoint, formatSocketError));
|
|
3048
2917
|
process.exit(1);
|
|
3049
2918
|
});
|
|
3050
2919
|
|
|
3051
2920
|
process.on("SIGINT", () => {
|
|
3052
|
-
|
|
2921
|
+
if (connectionTimeout) clearTimeout(connectionTimeout);
|
|
2922
|
+
streamWriter?.send({ type: "stream_stop" }).catch(() => {});
|
|
3053
2923
|
sock.end();
|
|
3054
2924
|
process.exit(0);
|
|
3055
2925
|
});
|
|
@@ -3057,6 +2927,17 @@ if (streamMode && (tool === "console" || tool === "network")) {
|
|
|
3057
2927
|
return;
|
|
3058
2928
|
}
|
|
3059
2929
|
|
|
2930
|
+
let transferPlan;
|
|
2931
|
+
try {
|
|
2932
|
+
transferPlan = endpoint.kind === "remote" ? prepareRemoteTool(finalTool, toolArgs) : (() => { const args = validateLocalToolPaths(finalTool, toolArgs); return { args, uploads: [], downloads: [] }; })();
|
|
2933
|
+
} catch (error) {
|
|
2934
|
+
const message = finalTool === "record" && endpoint.kind === "remote"
|
|
2935
|
+
? `record is not supported with remote endpoint ${endpoint.display}`
|
|
2936
|
+
: error.message;
|
|
2937
|
+
console.error(`Error: ${message}`);
|
|
2938
|
+
process.exit(1);
|
|
2939
|
+
}
|
|
2940
|
+
toolArgs = transferPlan.args;
|
|
3060
2941
|
const request = {
|
|
3061
2942
|
type: "tool_request",
|
|
3062
2943
|
method: "execute_tool",
|
|
@@ -3065,45 +2946,20 @@ const request = {
|
|
|
3065
2946
|
...globalOpts,
|
|
3066
2947
|
};
|
|
3067
2948
|
|
|
3068
|
-
const sendRequest = (toolName, toolArgs = {}, timeoutMs = 5000) => {
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
buf += d.toString();
|
|
3083
|
-
const lines = buf.split("\n");
|
|
3084
|
-
buf = lines.pop();
|
|
3085
|
-
for (const line of lines) {
|
|
3086
|
-
if (!line.trim()) continue;
|
|
3087
|
-
try {
|
|
3088
|
-
const resp = JSON.parse(line);
|
|
3089
|
-
if (resp.type === "extension_disconnected") {
|
|
3090
|
-
sock.end();
|
|
3091
|
-
reject(new Error(resp.message));
|
|
3092
|
-
return;
|
|
3093
|
-
}
|
|
3094
|
-
sock.end();
|
|
3095
|
-
resolve(resp);
|
|
3096
|
-
} catch {
|
|
3097
|
-
sock.end();
|
|
3098
|
-
reject(new Error("Invalid JSON"));
|
|
3099
|
-
}
|
|
3100
|
-
}
|
|
3101
|
-
});
|
|
3102
|
-
sock.on("error", (e) => reject(new Error(formatSocketError(e))));
|
|
3103
|
-
let timeoutId;
|
|
3104
|
-
timeoutId = setTimeout(() => { sock.destroy(); reject(new Error("Timeout")); }, timeoutMs);
|
|
3105
|
-
sock.on("close", () => clearTimeout(timeoutId));
|
|
3106
|
-
});
|
|
2949
|
+
const sendRequest = async (toolName, toolArgs = {}, timeoutMs = 5000) => {
|
|
2950
|
+
const transport = await openClientTransport(endpoint, { requestTimeoutMs: timeoutMs });
|
|
2951
|
+
try {
|
|
2952
|
+
const prepared = endpoint.kind === "remote" ? prepareRemoteTool(toolName, toolArgs) : (() => { const args = validateLocalToolPaths(toolName, toolArgs); return { args, uploads: [], downloads: [] }; })();
|
|
2953
|
+
return await transport.request({
|
|
2954
|
+
type: "tool_request",
|
|
2955
|
+
method: "execute_tool",
|
|
2956
|
+
params: { tool: toolName, args: prepared.args },
|
|
2957
|
+
id: "cli-" + Date.now() + "-" + Math.random(),
|
|
2958
|
+
...globalOpts,
|
|
2959
|
+
}, timeoutMs, prepared);
|
|
2960
|
+
} finally {
|
|
2961
|
+
await transport.close();
|
|
2962
|
+
}
|
|
3107
2963
|
};
|
|
3108
2964
|
|
|
3109
2965
|
function parseRecordNumber(value, fallback, name, min, max) {
|
|
@@ -3270,7 +3126,11 @@ const performAutoCapture = async () => {
|
|
|
3270
3126
|
};
|
|
3271
3127
|
|
|
3272
3128
|
if (finalTool === "record") {
|
|
3273
|
-
|
|
3129
|
+
if (endpoint.kind === "remote") {
|
|
3130
|
+
console.error(`Error: record is not supported with remote endpoint ${endpoint.display}`);
|
|
3131
|
+
process.exit(1);
|
|
3132
|
+
}
|
|
3133
|
+
installBrowserLock(lockOptions, endpoint);
|
|
3274
3134
|
runRecord()
|
|
3275
3135
|
.then(() => process.exit(0))
|
|
3276
3136
|
.catch((error) => {
|
|
@@ -3280,57 +3140,67 @@ if (finalTool === "record") {
|
|
|
3280
3140
|
return;
|
|
3281
3141
|
}
|
|
3282
3142
|
|
|
3283
|
-
installBrowserLock(lockOptions);
|
|
3143
|
+
installBrowserLock(lockOptions, endpoint);
|
|
3144
|
+
let socket;
|
|
3145
|
+
let timeout;
|
|
3146
|
+
|
|
3147
|
+
if (endpoint.kind === "remote") {
|
|
3148
|
+
socket = { end() {}, destroy() {} };
|
|
3149
|
+
const requestTimeout = resolveRequestDeadlineMs(tool, toolArgs);
|
|
3150
|
+
openClientTransport(endpoint, { requestTimeoutMs: requestTimeout })
|
|
3151
|
+
.then(async (transport) => {
|
|
3152
|
+
try {
|
|
3153
|
+
const response = await transport.request(request, requestTimeout, transferPlan);
|
|
3154
|
+
await handleResponse(response);
|
|
3155
|
+
} finally {
|
|
3156
|
+
await transport.close();
|
|
3157
|
+
}
|
|
3158
|
+
})
|
|
3159
|
+
.catch((error) => {
|
|
3160
|
+
console.error(`Error: ${error.message}`);
|
|
3161
|
+
process.exit(1);
|
|
3162
|
+
});
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3284
3165
|
|
|
3285
|
-
|
|
3286
|
-
socket.
|
|
3166
|
+
socket = connectEndpoint(endpoint, () => {
|
|
3167
|
+
writeFrame(socket, request).catch((error) => socket.destroy(error));
|
|
3287
3168
|
});
|
|
3288
3169
|
|
|
3289
|
-
const
|
|
3290
|
-
|
|
3291
|
-
if (tool === "aistudio.build") {
|
|
3292
|
-
const userTimeoutSec = parseInt(options.timeout || "600", 10);
|
|
3293
|
-
requestTimeout = (userTimeoutSec * 1000) + 60000;
|
|
3294
|
-
}
|
|
3295
|
-
const timeout = setTimeout(() => {
|
|
3170
|
+
const requestTimeout = resolveRequestDeadlineMs(tool, options);
|
|
3171
|
+
timeout = setTimeout(() => {
|
|
3296
3172
|
console.error(`Error: Request timed out (${requestTimeout / 1000}s)`);
|
|
3297
3173
|
socket.destroy();
|
|
3298
3174
|
process.exit(1);
|
|
3299
3175
|
}, requestTimeout);
|
|
3300
3176
|
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
for (const line of lines) {
|
|
3309
|
-
if (!line.trim()) continue;
|
|
3310
|
-
try {
|
|
3311
|
-
const msg = JSON.parse(line);
|
|
3312
|
-
|
|
3313
|
-
if (msg.type === "extension_disconnected") {
|
|
3314
|
-
clearTimeout(timeout);
|
|
3315
|
-
console.error(msg.message);
|
|
3316
|
-
socket.end();
|
|
3317
|
-
process.exit(1);
|
|
3318
|
-
}
|
|
3319
|
-
|
|
3320
|
-
handleResponse(msg).catch((err) => {
|
|
3321
|
-
console.error("Handler error:", err.message);
|
|
3322
|
-
process.exit(1);
|
|
3323
|
-
});
|
|
3324
|
-
} catch (e) {
|
|
3325
|
-
console.error("Invalid JSON response:", line);
|
|
3177
|
+
const responseParser = createFrameParser({
|
|
3178
|
+
onFrame(msg) {
|
|
3179
|
+
if (msg.type === "extension_disconnected") {
|
|
3180
|
+
clearTimeout(timeout);
|
|
3181
|
+
console.error(msg.message);
|
|
3182
|
+
socket.end();
|
|
3326
3183
|
process.exit(1);
|
|
3327
3184
|
}
|
|
3328
|
-
|
|
3185
|
+
if (msg.id !== request.id) return;
|
|
3186
|
+
handleResponse(msg).catch((err) => {
|
|
3187
|
+
console.error("Handler error:", err.message);
|
|
3188
|
+
process.exit(1);
|
|
3189
|
+
});
|
|
3190
|
+
},
|
|
3191
|
+
onError(error) {
|
|
3192
|
+
clearTimeout(timeout);
|
|
3193
|
+
console.error("Invalid response frame:", error.message);
|
|
3194
|
+
socket.destroy();
|
|
3195
|
+
process.exit(1);
|
|
3196
|
+
},
|
|
3329
3197
|
});
|
|
3330
3198
|
|
|
3199
|
+
socket.on("data", (data) => responseParser.push(data));
|
|
3200
|
+
|
|
3331
3201
|
socket.on("error", (err) => {
|
|
3332
3202
|
clearTimeout(timeout);
|
|
3333
|
-
console.error("Error:",
|
|
3203
|
+
console.error("Error:", formatEndpointError(err, endpoint, formatSocketError));
|
|
3334
3204
|
process.exit(1);
|
|
3335
3205
|
});
|
|
3336
3206
|
|
|
@@ -3389,7 +3259,7 @@ async function handleResponse(response) {
|
|
|
3389
3259
|
}
|
|
3390
3260
|
|
|
3391
3261
|
if (tool === "screenshot" && data?.base64 && (outputPath || toolArgs.savePath)) {
|
|
3392
|
-
const saveTo =
|
|
3262
|
+
const saveTo = transferPlan.downloads?.[0]?.destination || toolArgs.savePath || outputPath;
|
|
3393
3263
|
fs.writeFileSync(saveTo, Buffer.from(data.base64, "base64"));
|
|
3394
3264
|
|
|
3395
3265
|
const skipResize = options.full || toolArgs.full;
|