surf-cli 2.9.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.
Files changed (37) hide show
  1. package/README.md +48 -4
  2. package/dist/content/accessibility-tree.js +11 -0
  3. package/dist/content/accessibility-tree.js.map +1 -0
  4. package/dist/content/visual-indicator.js +111 -0
  5. package/dist/content/visual-indicator.js.map +1 -0
  6. package/dist/manifest.json +11 -2
  7. package/dist/options/options.js +3 -3
  8. package/dist/options/options.js.map +1 -1
  9. package/dist/service-worker/index.js +61 -261
  10. package/dist/service-worker/index.js.map +1 -1
  11. package/native/activity-journal.cjs +55 -0
  12. package/native/chatgpt-client.cjs +2 -0
  13. package/native/cli.cjs +52 -278
  14. package/native/do-executor.cjs +52 -475
  15. package/native/do-parser.cjs +8 -249
  16. package/native/host-helpers.cjs +6 -14
  17. package/native/host-sessions.cjs +5 -1
  18. package/native/host.cjs +199 -1
  19. package/native/network-export.cjs +20 -17
  20. package/native/network-store.cjs +38 -58
  21. package/native/playbook-authoring.cjs +44 -0
  22. package/native/playbook-cli.cjs +157 -0
  23. package/native/playbook-client.cjs +259 -0
  24. package/native/playbook-receipts.cjs +109 -0
  25. package/native/playbook-records.cjs +208 -0
  26. package/native/playbook-runtime.cjs +177 -0
  27. package/native/playbooks.cjs +235 -0
  28. package/native/private-state.cjs +156 -0
  29. package/native/redaction.cjs +104 -0
  30. package/native/workflow-definition.cjs +368 -0
  31. package/native/workflow-runtime.cjs +225 -0
  32. package/package.json +2 -1
  33. package/playbooks/page/ops/read.json +22 -0
  34. package/playbooks/page/playbook.json +7 -0
  35. package/skills/surf/SKILL.md +41 -1
  36. package/dist/content/index.js +0 -116
  37. package/dist/content/index.js.map +0 -1
@@ -1,507 +1,84 @@
1
- /**
2
- * Executor for surf `do` workflow commands
3
- *
4
- * Executes steps sequentially with auto-waits and streaming progress output.
5
- * Supports:
6
- * - Step outputs: capture results with `as` field
7
- * - Loops: `repeat` for fixed iterations, `each` for array iteration
8
- * - Variable substitution: %{varname} syntax
9
- *
10
- * Follows the same socket communication pattern as --script mode in cli.cjs.
11
- */
12
-
13
- const { selectEndpoint } = require("./endpoint.cjs");
14
1
  const { openClientTransport } = require("./client-transport.cjs");
15
- const { resolveRequestDeadlineMs } = require("./host-sessions.cjs");
2
+ const { selectEndpoint } = require("./endpoint.cjs");
16
3
  const { prepareRemoteTool, validateLocalToolPaths } = require("./file-transfer.cjs");
4
+ const { resolveRequestDeadlineMs } = require("./host-sessions.cjs");
5
+ const runtime = require("./workflow-runtime.cjs");
17
6
 
18
- // Maximum iterations for loops (safety cap)
19
- const MAX_LOOP_ITERATIONS = 100;
20
-
21
- // Commands that trigger auto-wait after execution
22
- // Note: 'type' is intentionally excluded - typing doesn't trigger navigation or DOM changes
23
- const AUTO_WAIT_COMMANDS = [
24
- 'go', 'navigate', 'click', 'key', 'form.fill', 'submit',
25
- 'tab.switch', 'tab.new', 'back', 'forward'
26
- ];
27
-
28
- // Auto-wait strategies per command type
29
- const AUTO_WAIT_MAP = {
30
- 'navigate': 'wait.load',
31
- 'go': 'wait.load',
32
- 'click': 'wait.dom',
33
- 'key': 'wait.dom',
34
- 'form.fill': 'wait.dom',
35
- 'submit': 'wait.load', // Form submission typically triggers navigation
36
- 'tab.switch': 'wait.load',
37
- 'tab.new': 'wait.load',
38
- 'back': 'wait.load',
39
- 'forward': 'wait.load',
40
- };
41
-
42
- /**
43
- * Check if a command should trigger an auto-wait
44
- * @param {string} cmd - Command name
45
- * @returns {boolean}
46
- */
47
- function shouldAutoWait(cmd) {
48
- return AUTO_WAIT_COMMANDS.some(c => cmd === c || cmd.startsWith(c + '.'));
49
- }
50
-
51
- /**
52
- * Get the appropriate auto-wait command for a given command
53
- * @param {string} cmd - Command name
54
- * @returns {string|null} - Wait command to execute, or null
55
- */
56
- function getAutoWaitCommand(cmd) {
57
- // Check exact match first
58
- if (AUTO_WAIT_MAP[cmd] !== undefined) return AUTO_WAIT_MAP[cmd];
59
-
60
- // Check prefix match
61
- for (const [prefix, waitCmd] of Object.entries(AUTO_WAIT_MAP)) {
62
- if (cmd.startsWith(prefix + '.')) return waitCmd;
63
- }
64
-
65
- return null;
66
- }
67
-
68
- /**
69
- * Send a single tool request over socket
70
- * @param {string} toolName - Tool/command name
71
- * @param {object} toolArgs - Tool arguments
72
- * @param {object} context - Execution context (tabId, windowId)
73
- * @returns {Promise<object>} - Response from host
74
- */
75
7
  function sendDoRequest(toolName, toolArgs, context = {}) {
76
8
  const request = {
77
9
  type: "tool_request",
78
10
  method: "execute_tool",
79
11
  params: { tool: toolName, args: toolArgs },
80
- id: "do-" + Date.now() + "-" + Math.random(),
12
+ id: `do-${Date.now()}-${Math.random()}`,
81
13
  };
82
14
  if (context.tabId) request.tabId = context.tabId;
83
15
  if (context.windowId) request.windowId = context.windowId;
84
- const requestTimeoutMs = context.timeoutMs || resolveRequestDeadlineMs(toolName, toolArgs);
16
+ const timeoutMs = context.timeoutMs || resolveRequestDeadlineMs(toolName, toolArgs);
85
17
  const endpoint = context.endpoint || selectEndpoint([]).endpoint;
86
- const prepared = endpoint.kind === "remote" ? prepareRemoteTool(toolName, toolArgs) : (() => { const args = validateLocalToolPaths(toolName, toolArgs); return { args, uploads: [], downloads: [] }; })();
18
+ const prepared = endpoint.kind === "remote"
19
+ ? prepareRemoteTool(toolName, toolArgs)
20
+ : { args: validateLocalToolPaths(toolName, toolArgs), uploads: [], downloads: [] };
87
21
  request.params.args = prepared.args;
88
- if (context.transport) return context.transport.request(request, requestTimeoutMs, prepared);
22
+ if (context.transport) return context.transport.request(request, timeoutMs, prepared);
89
23
  return (async () => {
90
24
  const transport = await openClientTransport(endpoint);
91
25
  try {
92
- return await transport.request(request, requestTimeoutMs, prepared);
26
+ return await transport.request(request, timeoutMs, prepared);
93
27
  } finally {
94
28
  await transport.close();
95
29
  }
96
30
  })();
97
31
  }
98
32
 
99
- /**
100
- * Resolve a variable reference or perform string substitution
101
- * @param {*} template - Value to resolve (may contain %{var} references)
102
- * @param {object} vars - Variables map
103
- * @returns {*} - Resolved value
104
- */
105
- function resolveVar(template, vars) {
106
- if (typeof template !== 'string') return template;
107
-
108
- // Check if it's a simple variable reference like %{urls}
109
- const match = template.match(/^%\{(\w+)\}$/);
110
- if (match) {
111
- const value = vars[match[1]];
112
- return value !== undefined ? value : template;
113
- }
114
-
115
- // Otherwise do string substitution
116
- return template.replace(/%\{(\w+)\}/g, (_, name) => {
117
- const val = vars[name];
118
- if (val === undefined) return `%{${name}}`;
119
- // Convert objects/arrays to string for interpolation
120
- if (typeof val === 'object') return JSON.stringify(val);
121
- return String(val);
122
- });
123
- }
124
-
125
- /**
126
- * Substitute variables in arguments using %{varname} syntax
127
- * @param {object} args - Arguments object
128
- * @param {object} vars - Variables map
129
- * @returns {object} - Arguments with variables substituted
130
- */
131
- function substituteVars(args, vars) {
132
- if (!args || typeof args !== 'object') return args;
133
-
134
- // Handle arrays specially to preserve array type
135
- if (Array.isArray(args)) {
136
- return args.map(item => {
137
- if (typeof item === 'string') {
138
- return resolveVar(item, vars);
139
- } else if (typeof item === 'object' && item !== null) {
140
- return substituteVars(item, vars);
141
- } else {
142
- return item;
143
- }
144
- });
145
- }
146
-
147
- // Handle plain objects
148
- const result = {};
149
- for (const [key, val] of Object.entries(args)) {
150
- if (typeof val === 'string') {
151
- result[key] = resolveVar(val, vars);
152
- } else if (Array.isArray(val)) {
153
- result[key] = substituteVars(val, vars);
154
- } else if (typeof val === 'object' && val !== null) {
155
- result[key] = substituteVars(val, vars);
156
- } else {
157
- result[key] = val;
158
- }
159
- }
160
- return result;
161
- }
162
-
163
- /**
164
- * Extract usable output from a step response for the `as` capture
165
- * @param {object} resp - Response from sendDoRequest
166
- * @returns {*} - Extracted value
167
- */
168
- function extractStepOutput(resp) {
169
- // MCP format: resp.result.content[0].text
170
- if (resp.result?.content?.[0]?.text) {
171
- const text = resp.result.content[0].text;
172
- // Try to parse as JSON, otherwise return raw text
173
- try {
174
- return JSON.parse(text);
175
- } catch {
176
- return text;
177
- }
178
- }
179
-
180
- // Direct value (some tools return this)
181
- if (resp.value !== undefined) return resp.value;
182
-
183
- // Direct result object
184
- if (resp.result !== undefined) return resp.result;
185
-
186
- // Fallback to the whole response
187
- return resp;
188
- }
189
-
190
- /**
191
- * Execute a single tool step (non-loop)
192
- * @param {object} step - Step to execute { cmd, args, as? }
193
- * @param {object} vars - Variables map (mutated if step has `as`)
194
- * @param {object} context - Execution context
195
- * @param {object} options - Execution options
196
- * @returns {Promise<object>} - Result { success, error?, output? }
197
- */
198
- async function executeSingleStep(step, vars, context, options) {
199
- const { autoWait = true, stepDelay = 100 } = options;
200
-
201
- // Substitute variables in args
202
- const resolvedArgs = substituteVars(step.args || {}, vars);
203
-
204
- try {
205
- const resp = await sendDoRequest(step.cmd, resolvedArgs, context);
206
-
207
- if (resp.error) {
208
- const errText = resp.error.content?.[0]?.text || JSON.stringify(resp.error);
209
- return { success: false, error: errText };
210
- }
211
-
212
- // Capture output if step has `as` field
213
- if (step.as) {
214
- const output = extractStepOutput(resp);
215
- vars[step.as] = output;
216
- }
217
-
218
- // Command-specific auto-wait
219
- if (autoWait) {
220
- const waitCmd = getAutoWaitCommand(step.cmd);
221
- if (waitCmd) {
222
- const waitArgs = waitCmd === 'wait.load'
223
- ? { timeout: 10000 }
224
- : { stable: 100, timeout: 5000 };
225
- try {
226
- await sendDoRequest(waitCmd, waitArgs, context);
227
- } catch {
228
- // Ignore auto-wait failures silently
229
- }
230
- }
231
- }
232
-
233
- // Delay between steps
234
- if (stepDelay > 0) {
235
- await new Promise(r => setTimeout(r, stepDelay));
236
- }
237
-
238
- return { success: true, output: step.as ? vars[step.as] : undefined };
239
- } catch (err) {
240
- return { success: false, error: err.message };
241
- }
33
+ function summarizeArgs(step) {
34
+ return Object.entries(step.args || {})
35
+ .map(([key, value]) => typeof value === "string" && value.length > 40 ? `${key}="${value.slice(0, 37)}..."` : `${key}=${JSON.stringify(value)}`)
36
+ .join(" ");
242
37
  }
243
38
 
244
- /**
245
- * Execute a single step, handling loops recursively
246
- * @param {object} step - Step to execute (may be a loop or regular step)
247
- * @param {object} vars - Variables map
248
- * @param {object} context - Execution context
249
- * @param {object} options - Execution options
250
- * @param {function} onProgress - Progress callback for streaming output
251
- * @returns {Promise<object>} - Result { success, error?, stepsExecuted }
252
- */
253
- async function executeStep(step, vars, context, options, onProgress) {
254
- const { onError = 'stop' } = options;
255
-
256
- // Handle `repeat` loop
257
- if (step.repeat !== undefined) {
258
- // Resolve repeat count (may be a variable)
259
- let max = resolveVar(step.repeat, vars);
260
- if (typeof max === 'string') max = parseInt(max, 10);
261
- if (typeof max !== 'number' || isNaN(max)) max = 1;
262
-
263
- // Safety cap
264
- max = Math.min(max, MAX_LOOP_ITERATIONS);
265
-
266
- if (!Array.isArray(step.steps) || step.steps.length === 0) {
267
- return { success: false, error: 'repeat: steps array required', stepsExecuted: 0 };
268
- }
269
-
270
- let totalExecuted = 0;
271
-
272
- for (let i = 0; i < max; i++) {
273
- // Create loop-scoped variables
274
- const loopVars = { ...vars, _index: i, _iteration: i + 1 };
275
-
276
- // Execute nested steps
277
- for (const nestedStep of step.steps) {
278
- const result = await executeStep(nestedStep, loopVars, context, options, onProgress);
279
- totalExecuted += result.stepsExecuted || 1;
280
-
281
- if (!result.success && onError === 'stop') {
282
- return { success: false, error: result.error, stepsExecuted: totalExecuted };
283
- }
284
- }
285
-
286
- // Copy captured variables back to parent scope (only from regular steps, not loops)
287
- for (const nestedStep of step.steps) {
288
- // Skip loop steps - their 'as' is the loop variable, not an output capture
289
- const isNestedLoop = nestedStep.repeat !== undefined || nestedStep.each !== undefined;
290
- if (!isNestedLoop && nestedStep.as && loopVars[nestedStep.as] !== undefined) {
291
- vars[nestedStep.as] = loopVars[nestedStep.as];
292
- }
293
- }
294
-
295
- // Check `until` condition
296
- if (step.until) {
297
- const untilResult = await executeSingleStep(step.until, loopVars, context, options);
298
- totalExecuted++;
299
-
300
- // Exit loop if until condition is truthy
301
- const exitValue = untilResult.output;
302
- if (exitValue === true || exitValue === 'true' || exitValue) {
303
- break;
304
- }
305
- }
306
- }
307
-
308
- return { success: true, stepsExecuted: totalExecuted };
309
- }
310
-
311
- // Handle `each` loop
312
- if (step.each !== undefined) {
313
- const items = resolveVar(step.each, vars);
314
-
315
- if (!Array.isArray(items)) {
316
- return {
317
- success: false,
318
- error: `each: expected array, got ${typeof items}${items === undefined ? ' (undefined)' : ''}`,
319
- stepsExecuted: 0
320
- };
321
- }
322
-
323
- if (!Array.isArray(step.steps) || step.steps.length === 0) {
324
- return { success: false, error: 'each: steps array required', stepsExecuted: 0 };
325
- }
326
-
327
- // Safety cap
328
- const maxItems = Math.min(items.length, MAX_LOOP_ITERATIONS);
329
- const itemVar = step.as || 'item';
330
- let totalExecuted = 0;
331
-
332
- for (let i = 0; i < maxItems; i++) {
333
- // Create loop-scoped variables
334
- const loopVars = { ...vars, [itemVar]: items[i], _index: i, _iteration: i + 1 };
335
-
336
- // Execute nested steps
337
- for (const nestedStep of step.steps) {
338
- const result = await executeStep(nestedStep, loopVars, context, options, onProgress);
339
- totalExecuted += result.stepsExecuted || 1;
340
-
341
- if (!result.success && onError === 'stop') {
342
- return { success: false, error: result.error, stepsExecuted: totalExecuted };
343
- }
344
- }
345
-
346
- // Copy captured variables back to parent scope (only from regular steps, not loops)
347
- for (const nestedStep of step.steps) {
348
- // Skip loop steps - their 'as' is the loop variable, not an output capture
349
- const isNestedLoop = nestedStep.repeat !== undefined || nestedStep.each !== undefined;
350
- if (!isNestedLoop && nestedStep.as && loopVars[nestedStep.as] !== undefined) {
351
- vars[nestedStep.as] = loopVars[nestedStep.as];
352
- }
353
- }
354
- }
355
-
356
- return { success: true, stepsExecuted: totalExecuted };
39
+ function printProgress(event) {
40
+ const stepNum = `[${event.index + 1}/${event.total}]`;
41
+ if (event.type === "loop") {
42
+ if (event.phase === "start") {
43
+ const step = event.step;
44
+ const loopType = step.repeat !== undefined ? `repeat ${step.repeat}` : `each ${step.each}`;
45
+ console.log(`${stepNum} Loop: ${loopType} (${step.steps?.length || 0} nested steps)`);
46
+ } else if (event.phase === "ok") console.log(` Loop completed: ${event.stepsExecuted} steps (${event.ms}ms)`);
47
+ return;
357
48
  }
358
-
359
- // Regular step (non-loop)
360
- if (onProgress) {
361
- onProgress(step, 'start');
49
+ if (event.phase === "start") {
50
+ const args = summarizeArgs(event.step);
51
+ process.stdout.write(`${stepNum} ${event.step.cmd}${args ? ` ${args}` : ""} ... `);
52
+ } else if (event.phase === "ok") console.log(`OK (${event.ms}ms)`);
53
+ else {
54
+ console.log("FAIL");
55
+ console.log(` Error: ${event.error}`);
362
56
  }
363
-
364
- const result = await executeSingleStep(step, vars, context, options);
365
-
366
- if (onProgress) {
367
- onProgress(step, result.success ? 'ok' : 'fail', result.error);
368
- }
369
-
370
- return { ...result, stepsExecuted: 1 };
371
57
  }
372
58
 
373
- /**
374
- * Execute all workflow steps sequentially
375
- * @param {Array<object>} steps - Steps to execute
376
- * @param {object} options - Execution options
377
- * @returns {Promise<object>} - Execution result
378
- */
379
59
  async function executeDoSteps(steps, options = {}) {
380
- const {
381
- onError = 'stop',
382
- autoWait = true,
383
- stepDelay = 100,
384
- context = {},
385
- quiet = false, // For --json mode, suppress streaming output
386
- vars: initialVars = {},
387
- } = options;
388
-
389
- const results = [];
390
- const vars = { ...initialVars, ...(context.vars || {}) };
391
- const total = steps.length;
392
- let failed = 0;
393
- let stepsExecuted = 0;
394
- const startTotal = Date.now();
395
-
396
- for (let i = 0; i < total; i++) {
397
- const step = steps[i];
398
- const startTime = Date.now();
399
-
400
- // Check if this is a loop step
401
- const isLoop = step.repeat !== undefined || step.each !== undefined;
402
-
403
- if (isLoop) {
404
- // Loops handle their own progress output
405
- if (!quiet) {
406
- const loopType = step.repeat !== undefined ? `repeat ${step.repeat}` : `each ${step.each}`;
407
- console.log(`[${i + 1}/${total}] Loop: ${loopType} (${step.steps?.length || 0} nested steps)`);
408
- }
409
-
410
- const result = await executeStep(step, vars, context, { onError, autoWait, stepDelay }, null);
411
- const ms = Date.now() - startTime;
412
-
413
- stepsExecuted += result.stepsExecuted || 0;
414
-
415
- if (!result.success) {
416
- results.push({ step: i + 1, type: 'loop', status: 'error', error: result.error, ms });
417
- failed++;
418
-
419
- if (onError === 'stop') {
420
- return {
421
- status: 'failed',
422
- completedSteps: stepsExecuted,
423
- totalSteps: total,
424
- results,
425
- error: result.error,
426
- totalMs: Date.now() - startTotal,
427
- vars
428
- };
429
- }
430
- } else {
431
- results.push({ step: i + 1, type: 'loop', status: 'ok', stepsExecuted: result.stepsExecuted, ms });
432
- if (!quiet) {
433
- console.log(` Loop completed: ${result.stepsExecuted} steps (${ms}ms)`);
434
- }
435
- }
436
- } else {
437
- // Regular step
438
- const stepNum = `[${i + 1}/${total}]`;
439
- const argSummary = Object.entries(step.args || {})
440
- .map(([k, v]) => typeof v === "string" && v.length > 40
441
- ? `${k}="${v.slice(0, 37)}..."`
442
- : `${k}=${JSON.stringify(v)}`)
443
- .join(" ");
444
- const desc = argSummary ? `${step.cmd} ${argSummary}` : step.cmd;
445
-
446
- if (!quiet) {
447
- process.stdout.write(`${stepNum} ${desc} ... `);
448
- }
449
-
450
- const result = await executeSingleStep(step, vars, context, { onError, autoWait, stepDelay });
451
- const ms = Date.now() - startTime;
452
- stepsExecuted++;
453
-
454
- if (!result.success) {
455
- if (!quiet) {
456
- console.log('FAIL');
457
- console.log(` Error: ${result.error}`);
458
- }
459
-
460
- results.push({ step: i + 1, cmd: step.cmd, status: 'error', error: result.error, ms });
461
- failed++;
462
-
463
- if (onError === 'stop') {
464
- return {
465
- status: 'failed',
466
- completedSteps: stepsExecuted - 1,
467
- totalSteps: total,
468
- results,
469
- error: result.error,
470
- totalMs: Date.now() - startTotal,
471
- vars
472
- };
473
- }
474
- } else {
475
- if (!quiet) {
476
- console.log(`OK (${ms}ms)`);
477
- }
478
- results.push({ step: i + 1, cmd: step.cmd, status: 'ok', ms });
479
- }
480
- }
481
- }
482
-
483
- return {
484
- status: failed > 0 ? 'partial' : 'completed',
485
- completedSteps: stepsExecuted,
486
- totalSteps: total,
487
- results,
488
- failed,
489
- totalMs: Date.now() - startTotal,
490
- vars
491
- };
60
+ const context = options.context || {};
61
+ return runtime.executeWorkflow(steps, {
62
+ ...options,
63
+ executeTool: options.executeTool || ((tool, args) => sendDoRequest(tool, args, context)),
64
+ onProgress: options.quiet ? options.onProgress : (event) => {
65
+ printProgress(event);
66
+ options.onProgress?.(event);
67
+ },
68
+ });
492
69
  }
493
70
 
494
- module.exports = {
495
- executeDoSteps,
496
- sendDoRequest,
497
- shouldAutoWait,
498
- getAutoWaitCommand,
499
- substituteVars,
500
- resolveVar,
501
- extractStepOutput,
502
- executeStep,
503
- executeSingleStep,
504
- AUTO_WAIT_COMMANDS,
505
- AUTO_WAIT_MAP,
506
- MAX_LOOP_ITERATIONS
71
+ module.exports = {
72
+ AUTO_WAIT_COMMANDS: runtime.AUTO_WAIT_COMMANDS,
73
+ AUTO_WAIT_MAP: runtime.AUTO_WAIT_MAP,
74
+ MAX_LOOP_ITERATIONS: runtime.MAX_LOOP_ITERATIONS,
75
+ executeSingleStep: runtime.executeSingleStep,
76
+ executeStep: runtime.executeStep,
77
+ executeDoSteps,
78
+ extractStepOutput: runtime.extractStepOutput,
79
+ getAutoWaitCommand: runtime.getAutoWaitCommand,
80
+ resolveVar: runtime.resolveVar,
81
+ sendDoRequest,
82
+ shouldAutoWait: runtime.shouldAutoWait,
83
+ substituteVars: runtime.substituteVars,
507
84
  };