surf-cli 2.2.0 → 2.4.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.
@@ -150,22 +150,23 @@ async function selectModel(cdp, desiredModel, timeoutMs = 8000) {
150
150
  })()`
151
151
  );
152
152
  await delay(300);
153
+ // Select from menu - loop in Node.js to avoid CDP timeout issues
153
154
  const normalizedModel = desiredModel.toLowerCase().replace(/[^a-z0-9]/g, "");
154
- const result = await evaluate(
155
- cdp,
156
- `(async () => {
157
- ${buildClickDispatcher()}
158
- const TIMEOUT_MS = ${timeoutMs};
159
- const targetModel = ${JSON.stringify(normalizedModel)};
160
- const menuSelector = '${SELECTORS.menuContainer}';
161
- const itemSelector = '${SELECTORS.menuItem}';
162
- const normalize = (text) => (text || '').toLowerCase().replace(/[^a-z0-9]/g, '');
163
- const deadline = Date.now() + TIMEOUT_MS;
164
- while (Date.now() < deadline) {
155
+ const deadline = Date.now() + timeoutMs;
156
+
157
+ while (Date.now() < deadline) {
158
+ const result = await evaluate(
159
+ cdp,
160
+ `(() => {
161
+ ${buildClickDispatcher()}
162
+ const targetModel = ${JSON.stringify(normalizedModel)};
163
+ const menuSelector = '${SELECTORS.menuContainer}';
164
+ const itemSelector = '${SELECTORS.menuItem}';
165
+ const normalize = (text) => (text || '').toLowerCase().replace(/[^a-z0-9]/g, '');
166
+
165
167
  const menu = document.querySelector(menuSelector);
166
168
  if (!menu) {
167
- await new Promise(r => setTimeout(r, 100));
168
- continue;
169
+ return { found: false, waiting: true };
169
170
  }
170
171
  const items = Array.from(menu.querySelectorAll(itemSelector));
171
172
  let bestMatch = null;
@@ -183,18 +184,24 @@ async function selectModel(cdp, desiredModel, timeoutMs = 8000) {
183
184
  }
184
185
  if (bestMatch) {
185
186
  dispatchClickSequence(bestMatch);
186
- await new Promise(r => setTimeout(r, 200));
187
- return { success: true, label: bestMatch.textContent?.trim() };
187
+ return { found: true, success: true, label: bestMatch.textContent?.trim() };
188
188
  }
189
- await new Promise(r => setTimeout(r, 100));
189
+ return { found: true, success: false, error: 'No matching model in menu' };
190
+ })()`
191
+ );
192
+
193
+ if (result && result.found) {
194
+ if (result.success) {
195
+ await delay(200);
196
+ return result.label;
190
197
  }
191
- return { success: false, error: 'Model option not found' };
192
- })()`
193
- );
194
- if (!result || !result.success) {
195
- throw new Error(`Model not found: ${desiredModel}`);
198
+ throw new Error(`Model not found: ${desiredModel}`);
199
+ }
200
+
201
+ await delay(100);
196
202
  }
197
- return result.label;
203
+
204
+ throw new Error(`Model not found: ${desiredModel} (timeout)`);
198
205
  }
199
206
 
200
207
  async function typePrompt(cdp, inputCdp, prompt) {
package/native/cli.cjs CHANGED
@@ -5,6 +5,8 @@ const { execSync } = require("child_process");
5
5
  const { loadConfig, getConfigPath, createStarterConfig } = require("./config.cjs");
6
6
  const networkFormatters = require("./formatters/network.cjs");
7
7
  const networkStore = require("./network-store.cjs");
8
+ const { parseDoCommands } = require("./do-parser.cjs");
9
+ const { executeDoSteps } = require("./do-executor.cjs");
8
10
 
9
11
  const SOCKET_PATH = "/tmp/surf.sock";
10
12
 
@@ -146,6 +148,27 @@ const TOOLS = {
146
148
  { cmd: 'perplexity "latest AI news" --model sonar', desc: "Specify model (Pro)" },
147
149
  ]
148
150
  },
151
+ "grok": {
152
+ desc: "Query Grok AI with real-time X/Twitter data access (uses browser session)",
153
+ args: ["query"],
154
+ opts: {
155
+ "with-page": "Include current page context",
156
+ model: "Model: auto, fast, expert, thinking (default)",
157
+ "deep-search": "Enable DeepSearch for X post searching",
158
+ timeout: "Timeout in seconds (default: 300 for thinking models)",
159
+ validate: "Check Grok UI and scrape available models (no query sent)",
160
+ "save-models": "Save discovered models to surf.json config"
161
+ },
162
+ examples: [
163
+ { cmd: 'grok "what are the latest AI agent trends on X"', desc: "Search X posts" },
164
+ { cmd: 'grok "analyze @username recent activity"', desc: "Profile analysis" },
165
+ { cmd: 'grok "summarize this page" --with-page', desc: "With page context" },
166
+ { cmd: 'grok "find viral AI posts" --deep-search', desc: "DeepSearch mode" },
167
+ { cmd: 'grok "quick question" --model fast', desc: "Faster model" },
168
+ { cmd: 'grok --validate', desc: "Check UI and list available models" },
169
+ { cmd: 'grok --validate --save-models', desc: "Save discovered models to settings" },
170
+ ]
171
+ },
149
172
  "ai": {
150
173
  desc: "Analyze page with AI (requires GOOGLE_API_KEY)",
151
174
  args: ["query"],
@@ -780,6 +803,27 @@ const TOOLS = {
780
803
  },
781
804
  }
782
805
  },
806
+ workflow: {
807
+ desc: "Workflow execution",
808
+ commands: {
809
+ "do": {
810
+ desc: "Execute multiple commands as a single workflow",
811
+ args: ["commands"],
812
+ opts: {
813
+ file: "Load workflow from JSON file",
814
+ "on-error": "stop (default) | continue",
815
+ "no-auto-wait": "Disable automatic waits between steps",
816
+ "step-delay": "Delay between steps in ms (default: 100)",
817
+ "dry-run": "Parse and validate without executing"
818
+ },
819
+ examples: [
820
+ { cmd: 'do \'go "https://example.com"\\nclick e5\\nscreenshot\'', desc: "Inline workflow" },
821
+ { cmd: 'do -f login.json', desc: "From JSON file" },
822
+ { cmd: 'do \'go "url"\\nclick e5\' --dry-run', desc: "Validate without running" },
823
+ ]
824
+ },
825
+ }
826
+ },
783
827
  zoom: {
784
828
  desc: "Zoom control",
785
829
  commands: {
@@ -1704,7 +1748,139 @@ if (args.includes("--script")) {
1704
1748
  return;
1705
1749
  }
1706
1750
 
1707
- const BOOLEAN_FLAGS = ["auto-capture", "json", "stream", "dry-run", "stop-on-error", "fail-fast", "clear", "submit", "all", "case-sensitive", "hard", "annotate", "fullpage", "reset", "no-screenshot", "full", "soft-fail", "has-body", "exclude-static", "v", "vv", "request", "by-tab", "har", "jsonl", "no-save"];
1751
+ // Handle `surf do` workflow command
1752
+ // Must be parsed before general parseArgs since it uses its own arg handling
1753
+ if (args[0] === "do") {
1754
+ const doArgs = args.slice(1);
1755
+ let commandsInput = null;
1756
+ let fileInput = null;
1757
+ let dryRun = false;
1758
+ let onError = "stop";
1759
+ let noAutoWait = false;
1760
+ let stepDelay = 100;
1761
+ let wantJson = false;
1762
+ let tabId = undefined;
1763
+ let windowId = undefined;
1764
+
1765
+ // Parse do-specific arguments
1766
+ for (let i = 0; i < doArgs.length; i++) {
1767
+ const arg = doArgs[i];
1768
+ if (arg === "--file" || arg === "-f") {
1769
+ fileInput = doArgs[i + 1];
1770
+ i++;
1771
+ } else if (arg === "--dry-run") {
1772
+ dryRun = true;
1773
+ } else if (arg === "--on-error") {
1774
+ onError = doArgs[i + 1] || "stop";
1775
+ i++;
1776
+ } else if (arg === "--no-auto-wait") {
1777
+ noAutoWait = true;
1778
+ } else if (arg === "--step-delay") {
1779
+ const parsed = parseInt(doArgs[i + 1], 10);
1780
+ stepDelay = isNaN(parsed) ? 100 : parsed;
1781
+ i++;
1782
+ } else if (arg === "--json") {
1783
+ wantJson = true;
1784
+ } else if (arg === "--tab-id") {
1785
+ tabId = parseInt(doArgs[i + 1], 10);
1786
+ i++;
1787
+ } else if (arg === "--window-id") {
1788
+ windowId = parseInt(doArgs[i + 1], 10);
1789
+ i++;
1790
+ } else if (!arg.startsWith("-")) {
1791
+ commandsInput = arg;
1792
+ }
1793
+ }
1794
+
1795
+ if (!commandsInput && !fileInput) {
1796
+ console.error("Error: commands string or --file required");
1797
+ console.error('Usage: surf do \'go "url"\\nclick e5\'');
1798
+ console.error(" surf do --file workflow.json");
1799
+ process.exit(1);
1800
+ }
1801
+
1802
+ let steps;
1803
+ try {
1804
+ if (fileInput) {
1805
+ if (!fs.existsSync(fileInput)) {
1806
+ console.error(`Error: File not found: ${fileInput}`);
1807
+ process.exit(1);
1808
+ }
1809
+ const content = fs.readFileSync(fileInput, "utf8");
1810
+ // JSON file format (same as --script)
1811
+ const script = JSON.parse(content);
1812
+ if (!script.steps || !Array.isArray(script.steps)) {
1813
+ throw new Error("JSON must have a 'steps' array");
1814
+ }
1815
+ // Convert --script format { tool, args } to do format { cmd, args }
1816
+ steps = script.steps.map(s => ({ cmd: s.tool, args: s.args || {} }));
1817
+ } else {
1818
+ // Inline string parsing
1819
+ steps = parseDoCommands(commandsInput);
1820
+ }
1821
+ } catch (e) {
1822
+ console.error(`Error: Failed to parse workflow: ${e.message}`);
1823
+ process.exit(1);
1824
+ }
1825
+
1826
+ if (steps.length === 0) {
1827
+ console.error("Error: No commands found in workflow");
1828
+ process.exit(1);
1829
+ }
1830
+
1831
+ // Validate with --dry-run
1832
+ if (dryRun) {
1833
+ console.log(`Would execute ${steps.length} steps:`);
1834
+ steps.forEach((s, i) => {
1835
+ const argStr = Object.entries(s.args || {})
1836
+ .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
1837
+ .join(" ");
1838
+ console.log(` ${i + 1}. ${s.cmd} ${argStr}`);
1839
+ });
1840
+ process.exit(0);
1841
+ }
1842
+
1843
+ if (!wantJson) {
1844
+ console.log(`Running workflow (${steps.length} steps)...\n`);
1845
+ }
1846
+
1847
+ const runWorkflow = async () => {
1848
+ const result = await executeDoSteps(steps, {
1849
+ onError,
1850
+ autoWait: !noAutoWait,
1851
+ stepDelay,
1852
+ quiet: wantJson,
1853
+ context: {
1854
+ tabId,
1855
+ windowId,
1856
+ },
1857
+ });
1858
+
1859
+ // Print summary
1860
+ if (wantJson) {
1861
+ console.log(JSON.stringify(result, null, 2));
1862
+ process.exit(result.status === "completed" ? 0 : 1);
1863
+ }
1864
+
1865
+ console.log("");
1866
+ if (result.status === "completed") {
1867
+ console.log(`Completed: ${result.completedSteps}/${result.totalSteps} steps (${result.totalMs}ms)`);
1868
+ process.exit(0);
1869
+ } else if (result.status === "partial") {
1870
+ console.log(`Partial: ${result.completedSteps}/${result.totalSteps} steps completed, ${result.failed} failed`);
1871
+ process.exit(1);
1872
+ } else {
1873
+ console.error(`Failed: ${result.completedSteps}/${result.totalSteps} steps completed`);
1874
+ if (result.error) console.error(`Error: ${result.error}`);
1875
+ process.exit(1);
1876
+ }
1877
+ };
1878
+
1879
+ runWorkflow();
1880
+ return;
1881
+ }
1882
+
1883
+ const BOOLEAN_FLAGS = ["auto-capture", "json", "stream", "dry-run", "stop-on-error", "fail-fast", "clear", "submit", "all", "case-sensitive", "hard", "annotate", "fullpage", "reset", "no-screenshot", "full", "soft-fail", "has-body", "exclude-static", "v", "vv", "request", "by-tab", "har", "jsonl", "no-save", "no-auto-wait"];
1708
1884
 
1709
1885
  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"];
1710
1886
 
@@ -1734,8 +1910,12 @@ const parseArgs = (rawArgs) => {
1734
1910
  result.options.v = true;
1735
1911
  } else if (arg === "-vv") {
1736
1912
  result.options.vv = true;
1913
+ } else if (arg === "-f" && rawArgs[i + 1] && !rawArgs[i + 1].startsWith("-")) {
1914
+ // -f takes a file path argument (for surf do -f <file>)
1915
+ result.options.file = rawArgs[i + 1];
1916
+ i++;
1737
1917
  } else if (arg.startsWith("-") && arg.length === 2) {
1738
- // Short flag like -n, -f
1918
+ // Short flag like -n
1739
1919
  result.options[arg.slice(1)] = true;
1740
1920
  } else {
1741
1921
  result.positional.push(arg);
@@ -1802,6 +1982,7 @@ const PRIMARY_ARG_MAP = {
1802
1982
  gemini: "query",
1803
1983
  chatgpt: "query",
1804
1984
  perplexity: "query",
1985
+ grok: "query",
1805
1986
  navigate: "url",
1806
1987
  go: "url",
1807
1988
  js: "code",
@@ -2168,7 +2349,7 @@ const socket = net.createConnection(SOCKET_PATH, () => {
2168
2349
  socket.write(JSON.stringify(request) + "\n");
2169
2350
  });
2170
2351
 
2171
- const AI_TOOLS = ["smoke", "chatgpt", "gemini", "perplexity", "ai"];
2352
+ const AI_TOOLS = ["smoke", "chatgpt", "gemini", "perplexity", "grok", "ai"];
2172
2353
  const requestTimeout = AI_TOOLS.includes(tool) ? 300000 : 30000;
2173
2354
  const timeout = setTimeout(() => {
2174
2355
  console.error(`Error: Request timed out (${requestTimeout / 1000}s)`);
package/native/config.cjs CHANGED
@@ -21,6 +21,18 @@ const STARTER_CONFIG = {
21
21
  }
22
22
  };
23
23
 
24
+ // Grok models can be customized in surf.json if X.com UI changes:
25
+ // {
26
+ // "grok": {
27
+ // "models": {
28
+ // "thinking": { "id": "thinking", "name": "Grok 4.1 Thinking" },
29
+ // "auto": { "id": "auto", "name": "Auto" },
30
+ // "fast": { "id": "fast", "name": "Fast" },
31
+ // "expert": { "id": "expert", "name": "Expert" }
32
+ // }
33
+ // }
34
+ // }
35
+
24
36
  function findConfigPath() {
25
37
  const cwdPath = path.join(process.cwd(), CONFIG_NAME);
26
38
  if (fs.existsSync(cwdPath)) {
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Executor for surf `do` workflow commands
3
+ *
4
+ * Executes steps sequentially with auto-waits and streaming progress output.
5
+ * Follows the same socket communication pattern as --script mode in cli.cjs.
6
+ */
7
+
8
+ const net = require("net");
9
+
10
+ const SOCKET_PATH = "/tmp/surf.sock";
11
+
12
+ // Commands that trigger auto-wait after execution
13
+ // Note: 'type' is intentionally excluded - typing doesn't trigger navigation or DOM changes
14
+ const AUTO_WAIT_COMMANDS = [
15
+ 'go', 'navigate', 'click', 'key', 'form.fill', 'submit',
16
+ 'tab.switch', 'tab.new', 'back', 'forward'
17
+ ];
18
+
19
+ // Auto-wait strategies per command type
20
+ const AUTO_WAIT_MAP = {
21
+ 'navigate': 'wait.load',
22
+ 'go': 'wait.load',
23
+ 'click': 'wait.dom',
24
+ 'key': 'wait.dom',
25
+ 'form.fill': 'wait.dom',
26
+ 'submit': 'wait.load', // Form submission typically triggers navigation
27
+ 'tab.switch': 'wait.load',
28
+ 'tab.new': 'wait.load',
29
+ 'back': 'wait.load',
30
+ 'forward': 'wait.load',
31
+ };
32
+
33
+ /**
34
+ * Check if a command should trigger an auto-wait
35
+ * @param {string} cmd - Command name
36
+ * @returns {boolean}
37
+ */
38
+ function shouldAutoWait(cmd) {
39
+ return AUTO_WAIT_COMMANDS.some(c => cmd === c || cmd.startsWith(c + '.'));
40
+ }
41
+
42
+ /**
43
+ * Get the appropriate auto-wait command for a given command
44
+ * @param {string} cmd - Command name
45
+ * @returns {string|null} - Wait command to execute, or null
46
+ */
47
+ function getAutoWaitCommand(cmd) {
48
+ // Check exact match first
49
+ if (AUTO_WAIT_MAP[cmd] !== undefined) return AUTO_WAIT_MAP[cmd];
50
+
51
+ // Check prefix match
52
+ for (const [prefix, waitCmd] of Object.entries(AUTO_WAIT_MAP)) {
53
+ if (cmd.startsWith(prefix + '.')) return waitCmd;
54
+ }
55
+
56
+ return null;
57
+ }
58
+
59
+ /**
60
+ * Send a single tool request over socket
61
+ * @param {string} toolName - Tool/command name
62
+ * @param {object} toolArgs - Tool arguments
63
+ * @param {object} context - Execution context (tabId, windowId)
64
+ * @returns {Promise<object>} - Response from host
65
+ */
66
+ function sendDoRequest(toolName, toolArgs, context = {}) {
67
+ return new Promise((resolve, reject) => {
68
+ const sock = net.createConnection(SOCKET_PATH, () => {
69
+ const req = {
70
+ type: "tool_request",
71
+ method: "execute_tool",
72
+ params: { tool: toolName, args: toolArgs },
73
+ id: "do-" + Date.now() + "-" + Math.random(),
74
+ };
75
+ if (context.tabId) req.tabId = context.tabId;
76
+ if (context.windowId) req.windowId = context.windowId;
77
+ sock.write(JSON.stringify(req) + "\n");
78
+ });
79
+
80
+ let buf = "";
81
+ sock.on("data", (d) => {
82
+ buf += d.toString();
83
+ const lines = buf.split("\n");
84
+ buf = lines.pop();
85
+ for (const line of lines) {
86
+ if (!line.trim()) continue;
87
+ try {
88
+ const resp = JSON.parse(line);
89
+ sock.end();
90
+ resolve(resp);
91
+ } catch {
92
+ sock.end();
93
+ reject(new Error("Invalid JSON response"));
94
+ }
95
+ }
96
+ });
97
+
98
+ sock.on("error", (e) => {
99
+ if (e.code === "ENOENT") {
100
+ reject(new Error("Socket not found. Is Chrome running with the extension?"));
101
+ } else if (e.code === "ECONNREFUSED") {
102
+ reject(new Error("Connection refused. Native host not running."));
103
+ } else {
104
+ reject(e);
105
+ }
106
+ });
107
+
108
+ const timeoutId = setTimeout(() => {
109
+ sock.destroy();
110
+ reject(new Error("Request timeout"));
111
+ }, 30000);
112
+
113
+ sock.on("close", () => clearTimeout(timeoutId));
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Substitute variables in arguments using %{varname} syntax
119
+ * @param {object} args - Arguments object
120
+ * @param {object} vars - Variables map
121
+ * @returns {object} - Arguments with variables substituted
122
+ */
123
+ function substituteVars(args, vars) {
124
+ if (!args || typeof args !== 'object') return args;
125
+
126
+ const result = {};
127
+ for (const [key, val] of Object.entries(args)) {
128
+ if (typeof val === 'string') {
129
+ result[key] = val.replace(/%\{(\w+)\}/g, (_, name) => vars[name] ?? `%{${name}}`);
130
+ } else {
131
+ result[key] = val;
132
+ }
133
+ }
134
+ return result;
135
+ }
136
+
137
+ /**
138
+ * Execute all workflow steps sequentially
139
+ * @param {Array<{ cmd: string, args: object }>} steps - Steps to execute
140
+ * @param {object} options - Execution options
141
+ * @returns {Promise<object>} - Execution result
142
+ */
143
+ async function executeDoSteps(steps, options = {}) {
144
+ const {
145
+ onError = 'stop',
146
+ autoWait = true,
147
+ stepDelay = 100,
148
+ context = {},
149
+ quiet = false, // For --json mode, suppress streaming output
150
+ } = options;
151
+
152
+ const results = [];
153
+ const vars = context.vars || {};
154
+ const total = steps.length;
155
+ let failed = 0;
156
+ const startTotal = Date.now();
157
+
158
+ for (let i = 0; i < total; i++) {
159
+ const step = steps[i];
160
+ const startTime = Date.now();
161
+ const stepNum = `[${i + 1}/${total}]`;
162
+
163
+ // Build description (matches --script output format)
164
+ const argSummary = Object.entries(step.args || {})
165
+ .map(([k, v]) => typeof v === "string" && v.length > 40
166
+ ? `${k}="${v.slice(0, 37)}..."`
167
+ : `${k}=${JSON.stringify(v)}`)
168
+ .join(" ");
169
+ const desc = argSummary ? `${step.cmd} ${argSummary}` : step.cmd;
170
+
171
+ // Print step prefix (streaming output)
172
+ if (!quiet) {
173
+ process.stdout.write(`${stepNum} ${desc} ... `);
174
+ }
175
+
176
+ try {
177
+ // Substitute variables in args
178
+ const resolvedArgs = substituteVars(step.args, vars);
179
+
180
+ const resp = await sendDoRequest(step.cmd, resolvedArgs, context);
181
+ const ms = Date.now() - startTime;
182
+
183
+ if (resp.error) {
184
+ const errText = resp.error.content?.[0]?.text || JSON.stringify(resp.error);
185
+
186
+ if (!quiet) {
187
+ console.log(`FAIL`);
188
+ console.log(` Error: ${errText}`);
189
+ }
190
+
191
+ results.push({ step: i + 1, cmd: step.cmd, status: 'error', error: errText, ms });
192
+ failed++;
193
+
194
+ if (onError === 'stop') {
195
+ return {
196
+ status: 'failed',
197
+ completedSteps: i,
198
+ totalSteps: total,
199
+ results,
200
+ error: errText,
201
+ totalMs: Date.now() - startTotal
202
+ };
203
+ }
204
+ } else {
205
+ if (!quiet) {
206
+ console.log(`OK (${ms}ms)`);
207
+ }
208
+
209
+ results.push({ step: i + 1, cmd: step.cmd, status: 'ok', ms });
210
+
211
+ // Command-specific auto-wait
212
+ if (autoWait) {
213
+ const waitCmd = getAutoWaitCommand(step.cmd);
214
+ if (waitCmd) {
215
+ const waitArgs = waitCmd === 'wait.load'
216
+ ? { timeout: 10000 }
217
+ : { stable: 100, timeout: 5000 };
218
+ try {
219
+ await sendDoRequest(waitCmd, waitArgs, context);
220
+ } catch {
221
+ // Ignore auto-wait failures silently
222
+ }
223
+ }
224
+ }
225
+ }
226
+
227
+ // Fixed delay between steps
228
+ if (stepDelay > 0 && i < total - 1) {
229
+ await new Promise(r => setTimeout(r, stepDelay));
230
+ }
231
+ } catch (err) {
232
+ const ms = Date.now() - startTime;
233
+
234
+ if (!quiet) {
235
+ console.log(`FAIL`);
236
+ console.log(` Error: ${err.message}`);
237
+ }
238
+
239
+ results.push({ step: i + 1, cmd: step.cmd, status: 'error', error: err.message, ms });
240
+ failed++;
241
+
242
+ if (onError === 'stop') {
243
+ return {
244
+ status: 'failed',
245
+ completedSteps: i,
246
+ totalSteps: total,
247
+ results,
248
+ error: err.message,
249
+ totalMs: Date.now() - startTotal
250
+ };
251
+ }
252
+ }
253
+ }
254
+
255
+ return {
256
+ status: failed > 0 ? 'partial' : 'completed',
257
+ completedSteps: total - failed,
258
+ totalSteps: total,
259
+ results,
260
+ failed,
261
+ totalMs: Date.now() - startTotal
262
+ };
263
+ }
264
+
265
+ module.exports = {
266
+ executeDoSteps,
267
+ sendDoRequest,
268
+ shouldAutoWait,
269
+ getAutoWaitCommand,
270
+ substituteVars,
271
+ AUTO_WAIT_COMMANDS,
272
+ AUTO_WAIT_MAP
273
+ };