surf-cli 2.9.0 → 2.11.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 (44) hide show
  1. package/README.md +61 -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-response.cjs +336 -0
  13. package/native/chatgpt-client-selection.cjs +119 -0
  14. package/native/chatgpt-client-ui.cjs +481 -0
  15. package/native/chatgpt-client.cjs +254 -664
  16. package/native/cli.cjs +100 -273
  17. package/native/do-executor.cjs +52 -475
  18. package/native/do-parser.cjs +8 -249
  19. package/native/host-helpers.cjs +32 -15
  20. package/native/host-sessions.cjs +6 -1
  21. package/native/host.cjs +228 -6
  22. package/native/network-export.cjs +20 -17
  23. package/native/network-store.cjs +38 -58
  24. package/native/oracle-cli.cjs +434 -0
  25. package/native/oracle-context.cjs +311 -0
  26. package/native/oracle-host.cjs +301 -0
  27. package/native/oracle-jobs.cjs +253 -0
  28. package/native/playbook-authoring.cjs +44 -0
  29. package/native/playbook-cli.cjs +157 -0
  30. package/native/playbook-client.cjs +259 -0
  31. package/native/playbook-receipts.cjs +109 -0
  32. package/native/playbook-records.cjs +208 -0
  33. package/native/playbook-runtime.cjs +177 -0
  34. package/native/playbooks.cjs +235 -0
  35. package/native/private-state.cjs +156 -0
  36. package/native/redaction.cjs +104 -0
  37. package/native/workflow-definition.cjs +369 -0
  38. package/native/workflow-runtime.cjs +225 -0
  39. package/package.json +2 -1
  40. package/playbooks/page/ops/read.json +22 -0
  41. package/playbooks/page/playbook.json +7 -0
  42. package/skills/surf/SKILL.md +72 -1
  43. package/dist/content/index.js +0 -116
  44. package/dist/content/index.js.map +0 -1
@@ -1,250 +1,9 @@
1
- /**
2
- * Parser for surf `do` workflow commands
3
- *
4
- * Parses newline-separated commands into structured step arrays:
5
- *
6
- * Input:
7
- * 'go "https://example.com"
8
- * click e5
9
- * screenshot'
10
- *
11
- * Output:
12
- * [
13
- * { cmd: 'navigate', args: { url: 'https://example.com' } },
14
- * { cmd: 'click', args: { ref: 'e5' } },
15
- * { cmd: 'screenshot', args: {} }
16
- * ]
17
- */
18
-
19
- // Aliases mapping (matches cli.cjs)
20
- const ALIASES = {
21
- snap: "screenshot",
22
- read: "page.read",
23
- find: "search",
24
- go: "navigate",
25
- net: "network",
26
- "network.dump": "network.get",
27
- };
28
-
29
- // Primary argument mapping for positional args (matches cli.cjs)
30
- const PRIMARY_ARG_MAP = {
31
- ai: "query",
32
- gemini: "query",
33
- chatgpt: "query",
34
- perplexity: "query",
35
- grok: "query",
36
- navigate: "url",
37
- go: "url",
38
- js: "code",
39
- javascript_tool: "code",
40
- key: "key",
41
- wait: "duration",
42
- health: "url",
43
- new_tab: "url",
44
- "tab.new": "url",
45
- switch_tab: "tab_id",
46
- "tab.switch": "id",
47
- close_tab: "tab_id",
48
- "tab.close": "id",
49
- "tab.name": "name",
50
- "tab.unname": "name",
51
- scroll_to_position: "position",
52
- type: "text",
53
- smart_type: "text",
54
- "emulate.network": "preset",
55
- "emulate.cpu": "rate",
56
- search: "term",
57
- find: "term",
58
- "wait.element": "selector",
59
- "wait.url": "pattern",
60
- zoom: "level",
61
- "history.search": "query",
62
- "network.get": "id",
63
- "network.body": "id",
64
- "network.curl": "id",
65
- "network.path": "id",
66
- "window.new": "url",
67
- "window.focus": "id",
68
- "window.close": "id",
69
- "locate.role": "role",
70
- "locate.text": "text",
71
- "locate.label": "label",
72
- "emulate.device": "device",
73
- "frame.js": "code",
74
- "element.styles": "selector",
75
- "select": "selector",
76
- };
77
-
78
- /**
79
- * Tokenize a command line, respecting single and double quotes
80
- * @param {string} line - Single line to tokenize
81
- * @returns {string[]} - Array of tokens
82
- */
83
- function tokenize(line) {
84
- const tokens = [];
85
- let current = '';
86
- let inQuote = null;
87
-
88
- for (let i = 0; i < line.length; i++) {
89
- const ch = line[i];
90
-
91
- if (inQuote) {
92
- if (ch === inQuote) {
93
- // End of quoted string
94
- inQuote = null;
95
- } else {
96
- current += ch;
97
- }
98
- } else if (ch === '"' || ch === "'") {
99
- // Start of quoted string
100
- inQuote = ch;
101
- } else if (ch === ' ' || ch === '\t') {
102
- // Whitespace separator
103
- if (current) {
104
- tokens.push(current);
105
- current = '';
106
- }
107
- } else {
108
- current += ch;
109
- }
110
- }
111
-
112
- // Don't forget last token
113
- if (current) {
114
- tokens.push(current);
115
- }
116
-
117
- return tokens;
118
- }
119
-
120
- /**
121
- * Parse a single command line into a step object
122
- * @param {string} line - Single command line
123
- * @returns {{ cmd: string, args: object } | null}
124
- */
125
- function parseCommandLine(line) {
126
- const tokens = tokenize(line);
127
- if (tokens.length === 0) return null;
128
-
129
- // Get command and apply alias
130
- let cmd = tokens[0];
131
- cmd = ALIASES[cmd] || cmd;
132
-
133
- const args = {};
134
- let i = 1;
135
-
136
- // Handle first positional argument based on command type
137
- if (i < tokens.length && !tokens[i].startsWith('--')) {
138
- const firstArg = tokens[i];
139
-
140
- // Special handling for click command
141
- if (cmd === 'click') {
142
- if (/^e\d+$/.test(firstArg)) {
143
- // Element reference: e5 -> ref
144
- args.ref = firstArg;
145
- i++;
146
- } else if (/^\d+$/.test(firstArg) && tokens[i + 1] && /^\d+$/.test(tokens[i + 1])) {
147
- // Coordinates: 100 200 -> x, y
148
- args.x = parseInt(firstArg, 10);
149
- args.y = parseInt(tokens[i + 1], 10);
150
- i += 2;
151
- }
152
- } else if (cmd === 'select') {
153
- // Select takes selector + one or more values: select e5 "US" or select e5 "opt1" "opt2"
154
- args.selector = firstArg;
155
- i++;
156
- // Collect remaining positional args as values
157
- const values = [];
158
- while (i < tokens.length && !tokens[i].startsWith('--')) {
159
- values.push(tokens[i]);
160
- i++;
161
- }
162
- // Host expects 'values' (always), matching CLI behavior
163
- if (values.length === 1) {
164
- args.values = values[0]; // Single value as string (host will wrap in array)
165
- } else if (values.length > 1) {
166
- args.values = values; // Multiple values as array
167
- }
168
- } else if (cmd === 'scroll') {
169
- if (firstArg === 'top' || firstArg === 'bottom') {
170
- cmd = `scroll.${firstArg}`;
171
- i++;
172
- } else if (['up', 'down', 'left', 'right'].includes(firstArg)) {
173
- args.direction = firstArg;
174
- i++;
175
- if (i < tokens.length && /^-?\d+$/.test(tokens[i])) {
176
- args.scroll_pixels = parseInt(tokens[i], 10);
177
- i++;
178
- }
179
- }
180
- } else {
181
- // Use PRIMARY_ARG_MAP for other commands
182
- const primaryKey = PRIMARY_ARG_MAP[cmd];
183
- if (primaryKey) {
184
- args[primaryKey] = firstArg;
185
- i++;
186
- }
187
- }
188
- }
189
-
190
- // Parse --flag value pairs
191
- while (i < tokens.length) {
192
- const token = tokens[i];
193
- if (token.startsWith('--')) {
194
- const key = token.slice(2);
195
- const next = tokens[i + 1];
196
- if (next && !next.startsWith('--')) {
197
- // Flag with value
198
- let val = next;
199
- // Type coercion
200
- if (val === "true") val = true;
201
- else if (val === "false") val = false;
202
- else if (/^-?\d+$/.test(val)) val = parseInt(val, 10);
203
- else if (/^-?\d+\.\d+$/.test(val)) val = parseFloat(val);
204
- args[key] = val;
205
- i += 2;
206
- } else {
207
- // Boolean flag
208
- args[key] = true;
209
- i++;
210
- }
211
- } else {
212
- // Skip unrecognized positional (shouldn't happen normally)
213
- i++;
214
- }
215
- }
216
-
217
- return { cmd, args };
218
- }
219
-
220
- /**
221
- * Parse a workflow string into step array
222
- * Supports pipe-separated (inline) or newline-separated (file) commands
223
- * @param {string} input - Workflow string
224
- * @returns {Array<{ cmd: string, args: object }>}
225
- */
226
- function parseDoCommands(input) {
227
- // Determine separator: use pipe if present, otherwise newlines
228
- // Pipe is preferred for inline: 'go "url" | click e5 | screenshot'
229
- // Newlines for files or heredocs
230
- const hasPipe = input.includes('|');
231
- const separator = hasPipe ? '|' : '\n';
232
-
233
- // Also handle literal \n for backwards compatibility
234
- const normalized = hasPipe ? input : input.replace(/\\n/g, '\n');
235
-
236
- return normalized
237
- .split(separator)
238
- .map(line => line.trim())
239
- .filter(line => line && !line.startsWith('#'))
240
- .map(line => parseCommandLine(line))
241
- .filter(step => step !== null);
242
- }
243
-
244
- module.exports = {
245
- parseDoCommands,
246
- parseCommandLine,
247
- tokenize,
248
- ALIASES,
249
- PRIMARY_ARG_MAP
1
+ const definition = require("./workflow-definition.cjs");
2
+
3
+ module.exports = {
4
+ ALIASES: definition.ALIASES,
5
+ PRIMARY_ARG_MAP: definition.PRIMARY_ARG_MAP,
6
+ parseCommandLine: definition.parseCommandLine,
7
+ parseDoCommands: definition.parseDoCommands,
8
+ tokenize: definition.tokenize,
250
9
  };
@@ -1,6 +1,5 @@
1
1
  const fs = require("fs");
2
2
  const networkFormatters = require("./formatters/network.cjs");
3
- const networkStore = require("./network-store.cjs");
4
3
 
5
4
  function buildProviderUploadMessage(provider, tabId, filePaths, id) {
6
5
  const normalizedProvider = String(provider || "").toLowerCase();
@@ -14,6 +13,21 @@ function normalizeModelString(model) {
14
13
  return String(model || "").trim().toLowerCase();
15
14
  }
16
15
 
16
+ function formatToolError(error) {
17
+ const message = error instanceof Error
18
+ ? error.message
19
+ : typeof error === "string"
20
+ ? error
21
+ : error?.message || String(error);
22
+ const result = { content: [{ type: "text", text: message }] };
23
+ if (error && typeof error === "object") {
24
+ result.message = message;
25
+ if (typeof error.code === "string") result.code = error.code;
26
+ if (typeof error.jobId === "string") result.jobId = error.jobId;
27
+ }
28
+ return result;
29
+ }
30
+
17
31
  /**
18
32
  * Format tool result content for MCP response
19
33
  * @param {*} result - The result object from the extension
@@ -123,19 +137,6 @@ function formatToolContent(result, log = () => {}, options = {}) {
123
137
  // Handle both requests (basic) and entries (full) formats
124
138
  const items = result.requests || result.entries;
125
139
  if (items && Array.isArray(items)) {
126
- // Persist entries with full data to disk
127
- if (result.entries && items.length > 0) {
128
- (async () => {
129
- for (const entry of items) {
130
- try {
131
- await networkStore.appendEntry(entry);
132
- } catch (err) {
133
- log(`Failed to persist network entry: ${err.message}`);
134
- }
135
- }
136
- })();
137
- }
138
-
139
140
  if (items.length === 0) {
140
141
  return text("No network requests captured");
141
142
  }
@@ -679,6 +680,9 @@ function mapToolToMessage(tool, args, tabId) {
679
680
  limit: a.limit || a.last,
680
681
  format: a.format,
681
682
  verbose: a.v ? 1 : (a.vv ? 2 : 0),
683
+ bodyMode: a["body-mode"] || a.bodyMode,
684
+ perBodyBytes: a["per-body-bytes"] || a.perBodyBytes,
685
+ totalBodyBytes: a["total-body-bytes"] || a.totalBodyBytes,
682
686
  ...baseMsg
683
687
  };
684
688
 
@@ -732,6 +736,9 @@ function mapToolToMessage(tool, args, tabId) {
732
736
  type: "EXPORT_NETWORK_REQUESTS",
733
737
  har: a.har,
734
738
  jsonl: a.jsonl,
739
+ bodyMode: a["body-mode"] || a.bodyMode,
740
+ perBodyBytes: a["per-body-bytes"] || a.perBodyBytes,
741
+ totalBodyBytes: a["total-body-bytes"] || a.totalBodyBytes,
735
742
  ...baseMsg
736
743
  };
737
744
 
@@ -1090,6 +1097,16 @@ function mapToolToMessage(tool, args, tabId) {
1090
1097
  case "history.search":
1091
1098
  if (!a.query) throw new Error("query required");
1092
1099
  return { type: "HISTORY_SEARCH", query: a.query, limit: a.limit !== undefined ? parseInt(a.limit, 10) : 20 };
1100
+ case "oracle.ask":
1101
+ if (!a.prompt) throw new Error("prompt required");
1102
+ return { ...a, type: "ORACLE_ASK" };
1103
+ case "oracle.status":
1104
+ return { ...a, type: "ORACLE_STATUS" };
1105
+ case "oracle.result":
1106
+ if (!a.id) throw new Error("id required");
1107
+ return { ...a, type: "ORACLE_RESULT" };
1108
+ case "oracle.list":
1109
+ return { type: "ORACLE_LIST" };
1093
1110
  case "chatgpt":
1094
1111
  if (!a.query) throw new Error("query required");
1095
1112
  return {
@@ -1204,4 +1221,4 @@ function mapToolToMessage(tool, args, tabId) {
1204
1221
  }
1205
1222
  }
1206
1223
 
1207
- module.exports = { mapToolToMessage, mapComputerAction, formatToolContent, buildProviderUploadMessage };
1224
+ module.exports = { mapToolToMessage, mapComputerAction, formatToolContent, formatToolError, buildProviderUploadMessage };
@@ -23,13 +23,18 @@ const PROVIDER_DEFAULT_TIMEOUT_SECONDS = {
23
23
  gemini: 300,
24
24
  grok: 300,
25
25
  perplexity: 120,
26
+ "oracle.result": 300,
27
+ "playbook.run": 600,
26
28
  };
27
29
 
28
30
  function resolveRequestDeadlineMs(tool, args = {}) {
29
31
  const defaultSeconds = PROVIDER_DEFAULT_TIMEOUT_SECONDS[tool];
30
32
  if (defaultSeconds === undefined) return DEFAULT_DEADLINE_MS;
33
+ const rawTimeout = tool === "playbook.run" && args && typeof args === "object" && !Array.isArray(args)
34
+ ? args.timeout ?? args.args?.timeout
35
+ : args?.timeout;
31
36
  const requestedSeconds = Number(
32
- args && typeof args === "object" && !Array.isArray(args) ? args.timeout : undefined,
37
+ rawTimeout,
33
38
  );
34
39
  const seconds = Number.isFinite(requestedSeconds) && requestedSeconds > 0
35
40
  ? requestedSeconds