neuron-inspector 0.8.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +345 -42
  2. package/dist/automations/executor.d.ts +21 -0
  3. package/dist/automations/executor.js +835 -0
  4. package/dist/automations/executor.js.map +1 -0
  5. package/dist/automations/index.d.ts +7 -0
  6. package/dist/automations/index.js +21 -0
  7. package/dist/automations/index.js.map +1 -0
  8. package/dist/automations/store.d.ts +15 -0
  9. package/dist/automations/store.js +193 -0
  10. package/dist/automations/store.js.map +1 -0
  11. package/dist/automations/tools.d.ts +14 -0
  12. package/dist/automations/tools.js +735 -0
  13. package/dist/automations/tools.js.map +1 -0
  14. package/dist/automations/types.d.ts +211 -0
  15. package/dist/automations/types.js +4 -0
  16. package/dist/automations/types.js.map +1 -0
  17. package/dist/business-tools.d.ts +17 -0
  18. package/dist/business-tools.js +3065 -0
  19. package/dist/business-tools.js.map +1 -0
  20. package/dist/collections/__tests__/assertions.test.js +203 -0
  21. package/dist/collections/__tests__/assertions.test.js.map +1 -0
  22. package/dist/collections/__tests__/dynamic-vars.test.d.ts +1 -0
  23. package/dist/collections/__tests__/dynamic-vars.test.js +169 -0
  24. package/dist/collections/__tests__/dynamic-vars.test.js.map +1 -0
  25. package/dist/collections/__tests__/execution.test.d.ts +1 -0
  26. package/dist/collections/__tests__/execution.test.js +271 -0
  27. package/dist/collections/__tests__/execution.test.js.map +1 -0
  28. package/dist/collections/__tests__/integration.test.d.ts +1 -0
  29. package/dist/collections/__tests__/integration.test.js +478 -0
  30. package/dist/collections/__tests__/integration.test.js.map +1 -0
  31. package/dist/collections/__tests__/resolver.test.d.ts +1 -0
  32. package/dist/collections/__tests__/resolver.test.js +282 -0
  33. package/dist/collections/__tests__/resolver.test.js.map +1 -0
  34. package/dist/collections/__tests__/scenario.test.d.ts +1 -0
  35. package/dist/collections/__tests__/scenario.test.js +367 -0
  36. package/dist/collections/__tests__/scenario.test.js.map +1 -0
  37. package/dist/collections/__tests__/scripts.test.d.ts +1 -0
  38. package/dist/collections/__tests__/scripts.test.js +245 -0
  39. package/dist/collections/__tests__/scripts.test.js.map +1 -0
  40. package/dist/collections/__tests__/store.test.d.ts +1 -0
  41. package/dist/collections/__tests__/store.test.js +313 -0
  42. package/dist/collections/__tests__/store.test.js.map +1 -0
  43. package/dist/collections/assertions.d.ts +16 -0
  44. package/dist/collections/assertions.js +107 -0
  45. package/dist/collections/assertions.js.map +1 -0
  46. package/dist/collections/dynamic-vars.d.ts +75 -0
  47. package/dist/collections/dynamic-vars.js +187 -0
  48. package/dist/collections/dynamic-vars.js.map +1 -0
  49. package/dist/collections/executor.d.ts +100 -0
  50. package/dist/collections/executor.js +301 -0
  51. package/dist/collections/executor.js.map +1 -0
  52. package/dist/collections/http.d.ts +9 -0
  53. package/dist/collections/http.js +194 -0
  54. package/dist/collections/http.js.map +1 -0
  55. package/dist/collections/index.d.ts +12 -0
  56. package/dist/collections/index.js +27 -0
  57. package/dist/collections/index.js.map +1 -0
  58. package/dist/collections/postman.d.ts +10 -0
  59. package/dist/collections/postman.js +288 -0
  60. package/dist/collections/postman.js.map +1 -0
  61. package/dist/collections/resolver.d.ts +74 -0
  62. package/dist/collections/resolver.js +330 -0
  63. package/dist/collections/resolver.js.map +1 -0
  64. package/dist/collections/scripts.d.ts +20 -0
  65. package/dist/collections/scripts.js +225 -0
  66. package/dist/collections/scripts.js.map +1 -0
  67. package/dist/collections/store.d.ts +68 -0
  68. package/dist/collections/store.js +507 -0
  69. package/dist/collections/store.js.map +1 -0
  70. package/dist/collections/tools.d.ts +8 -0
  71. package/dist/collections/tools.js +1276 -0
  72. package/dist/collections/tools.js.map +1 -0
  73. package/dist/collections/types.d.ts +226 -0
  74. package/dist/collections/types.js +3 -0
  75. package/dist/collections/types.js.map +1 -0
  76. package/dist/compound-tools.js +136 -2
  77. package/dist/compound-tools.js.map +1 -1
  78. package/dist/killswitch.d.ts +8 -0
  79. package/dist/killswitch.js +304 -0
  80. package/dist/killswitch.js.map +1 -0
  81. package/dist/monitor.d.ts +36 -2
  82. package/dist/monitor.js +213 -38
  83. package/dist/monitor.js.map +1 -1
  84. package/dist/paths.d.ts +16 -0
  85. package/dist/paths.js +39 -0
  86. package/dist/paths.js.map +1 -0
  87. package/dist/recipe-tools.js +67 -9
  88. package/dist/recipe-tools.js.map +1 -1
  89. package/dist/recipes.d.ts +6 -0
  90. package/dist/recipes.js +83 -8
  91. package/dist/recipes.js.map +1 -1
  92. package/dist/resilience.d.ts +112 -0
  93. package/dist/resilience.js +319 -0
  94. package/dist/resilience.js.map +1 -0
  95. package/dist/scheduler.js +4 -8
  96. package/dist/scheduler.js.map +1 -1
  97. package/dist/server.js +221 -60
  98. package/dist/server.js.map +1 -1
  99. package/dist/session-state.js +2 -2
  100. package/dist/session-state.js.map +1 -1
  101. package/dist/tools.js +54 -0
  102. package/dist/tools.js.map +1 -1
  103. package/package.json +4 -3
  104. package/recipes/autopilot/agent.md +316 -0
  105. package/recipes/autopilot/learnings.md +33 -0
  106. package/recipes/autopilot/recipe.yaml +96 -0
  107. package/recipes/inbox-responder/agent.md +268 -0
  108. package/recipes/inbox-responder/learnings.md +25 -0
  109. package/recipes/inbox-responder/recipe.yaml +82 -0
  110. package/recipes/linkedin-job-scraper/agent.md +144 -0
  111. package/recipes/linkedin-job-scraper/learnings.md +3 -0
  112. package/recipes/linkedin-job-scraper/recipe.yaml +71 -0
  113. package/recipes/linkedin-outreach/learnings.md +17 -1
  114. package/recipes/planner/agent.md +66 -1
  115. package/recipes/planner/learnings.md +49 -1
  116. package/recipes/sheets-data-entry/agent.md +166 -0
  117. package/recipes/sheets-data-entry/learnings.md +3 -0
  118. package/recipes/sheets-data-entry/recipe.yaml +78 -0
  119. package/recipes/social-ops/learnings.md +33 -13
  120. package/recipes/x-job-scraper/agent.md +156 -0
  121. package/recipes/x-job-scraper/learnings.md +3 -0
  122. package/recipes/x-job-scraper/recipe.yaml +72 -0
  123. package/dist/__tests__/correlation.test.js +0 -53
  124. package/dist/__tests__/correlation.test.js.map +0 -1
  125. /package/dist/{__tests__/correlation.test.d.ts → collections/__tests__/assertions.test.d.ts} +0 -0
@@ -0,0 +1,835 @@
1
+ /**
2
+ * Step Execution Engine — Browser Automation
3
+ *
4
+ * Dispatches automation steps to bridge primitives via WebSocket.
5
+ * Handles variable substitution, tab tracking, assertions, extraction,
6
+ * retry logic, and error handling.
7
+ */
8
+ import { handleRecipeTool } from "../recipe-tools.js";
9
+ // ============================================================================
10
+ // Helpers
11
+ // ============================================================================
12
+ async function call(ctx, primitive, args, timeoutMs = 15000) {
13
+ return ctx.pending.call(ctx.ws, primitive, args, timeoutMs);
14
+ }
15
+ function sleep(ms) {
16
+ return new Promise((r) => setTimeout(r, ms));
17
+ }
18
+ /**
19
+ * Substitute {{variable}} placeholders in a string
20
+ */
21
+ function substituteVars(text, vars) {
22
+ return text.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
23
+ }
24
+ /**
25
+ * Deep substitute variables in all string fields of a config object
26
+ */
27
+ function substituteInConfig(config, vars) {
28
+ if (typeof config === "string") {
29
+ return substituteVars(config, vars);
30
+ }
31
+ if (Array.isArray(config)) {
32
+ return config.map((item) => substituteInConfig(item, vars));
33
+ }
34
+ if (config && typeof config === "object") {
35
+ const result = {};
36
+ for (const [key, value] of Object.entries(config)) {
37
+ result[key] = substituteInConfig(value, vars);
38
+ }
39
+ return result;
40
+ }
41
+ return config;
42
+ }
43
+ // ============================================================================
44
+ // Recipe Loading
45
+ // ============================================================================
46
+ async function loadRecipe(slug, variables) {
47
+ const args = { slug };
48
+ if (variables)
49
+ args.variables = variables;
50
+ return handleRecipeTool("neuron_recipe_run", args);
51
+ }
52
+ // ============================================================================
53
+ // Assertion Checking
54
+ // ============================================================================
55
+ async function checkAssertion(ctx, tabId, assertion, vars) {
56
+ const check = assertion.check;
57
+ const value = substituteVars(assertion.value, vars);
58
+ const expected = assertion.expected;
59
+ try {
60
+ switch (check) {
61
+ case "selector_exists": {
62
+ const result = (await call(ctx, "queryDom", { tabId, selector: value }));
63
+ const exists = !!(result.elements && result.elements.length > 0);
64
+ return { check, passed: exists, actual: exists, expected: true };
65
+ }
66
+ case "selector_not_exists": {
67
+ const result = (await call(ctx, "queryDom", { tabId, selector: value }));
68
+ const exists = !!(result.elements && result.elements.length > 0);
69
+ return { check, passed: !exists, actual: exists, expected: false };
70
+ }
71
+ case "text_contains": {
72
+ const text = (await call(ctx, "evaluateJS", {
73
+ tabId,
74
+ expression: "document.body.innerText",
75
+ }));
76
+ const contains = text.includes(value);
77
+ return { check, passed: contains, actual: contains, expected: true };
78
+ }
79
+ case "text_not_contains": {
80
+ const text = (await call(ctx, "evaluateJS", {
81
+ tabId,
82
+ expression: "document.body.innerText",
83
+ }));
84
+ const contains = text.includes(value);
85
+ return { check, passed: !contains, actual: contains, expected: false };
86
+ }
87
+ case "url_matches": {
88
+ const url = (await call(ctx, "evaluateJS", {
89
+ tabId,
90
+ expression: "location.href",
91
+ }));
92
+ const matches = new RegExp(value).test(url);
93
+ return { check, passed: matches, actual: url, expected: value };
94
+ }
95
+ case "title_matches": {
96
+ const title = (await call(ctx, "evaluateJS", {
97
+ tabId,
98
+ expression: "document.title",
99
+ }));
100
+ const matches = new RegExp(value).test(title);
101
+ return { check, passed: matches, actual: title, expected: value };
102
+ }
103
+ case "element_visible": {
104
+ const visible = (await call(ctx, "evaluateJS", {
105
+ tabId,
106
+ expression: `
107
+ (() => {
108
+ const el = document.querySelector(${JSON.stringify(value)});
109
+ if (!el) return false;
110
+ const rect = el.getBoundingClientRect();
111
+ const style = window.getComputedStyle(el);
112
+ return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
113
+ })()
114
+ `,
115
+ }));
116
+ return { check, passed: visible, actual: visible, expected: true };
117
+ }
118
+ case "element_count": {
119
+ const result = (await call(ctx, "queryDom", { tabId, selector: value }));
120
+ const count = result.elements?.length ?? 0;
121
+ const expectedCount = Number(expected ?? 0);
122
+ const op = assertion.op ?? "eq";
123
+ let passed = false;
124
+ if (op === "eq")
125
+ passed = count === expectedCount;
126
+ else if (op === "gt")
127
+ passed = count > expectedCount;
128
+ else if (op === "lt")
129
+ passed = count < expectedCount;
130
+ return { check, passed, actual: count, expected: expectedCount };
131
+ }
132
+ case "js_truthy": {
133
+ const result = await call(ctx, "evaluateJS", { tabId, expression: value });
134
+ const truthy = Boolean(result);
135
+ return { check, passed: truthy, actual: result, expected: true };
136
+ }
137
+ default:
138
+ return { check, passed: false, actual: null, expected: null };
139
+ }
140
+ }
141
+ catch (err) {
142
+ return {
143
+ check,
144
+ passed: false,
145
+ actual: err.message,
146
+ expected,
147
+ };
148
+ }
149
+ }
150
+ // ============================================================================
151
+ // Action Dispatchers
152
+ // ============================================================================
153
+ async function executeNavigate(ctx, config, tabId, vars) {
154
+ const url = substituteVars(config.url, vars);
155
+ if (tabId === null) {
156
+ // Open new tab
157
+ const result = (await call(ctx, "openTab", { url }));
158
+ const newTabId = result.tabId;
159
+ if (!newTabId)
160
+ throw new Error("Failed to open tab");
161
+ await sleep(2000); // Wait for initial load
162
+ return { tabId: newTabId, result };
163
+ }
164
+ else {
165
+ // Navigate existing tab
166
+ const result = await call(ctx, "navigate", { tabId, url });
167
+ await sleep(1500);
168
+ return { tabId, result };
169
+ }
170
+ }
171
+ async function executeClick(ctx, config, tabId, vars) {
172
+ const selector = substituteVars(config.selector, vars);
173
+ const text = config.text ? substituteVars(config.text, vars) : undefined;
174
+ return call(ctx, "smartClick", {
175
+ tabId,
176
+ selectors: [selector],
177
+ texts: text ? [text] : undefined,
178
+ scrollToFind: true,
179
+ });
180
+ }
181
+ async function executeType(ctx, config, tabId, vars) {
182
+ const selector = substituteVars(config.selector, vars);
183
+ const text = substituteVars(config.text, vars);
184
+ if (config.clear) {
185
+ await call(ctx, "evaluateJS", {
186
+ tabId,
187
+ expression: `
188
+ (() => {
189
+ const el = document.querySelector(${JSON.stringify(selector)});
190
+ if (el) el.value = '';
191
+ })()
192
+ `,
193
+ });
194
+ }
195
+ const result = await call(ctx, "smartType", {
196
+ tabId,
197
+ selectors: [selector],
198
+ value: text,
199
+ pressEnter: config.press_enter ?? false,
200
+ });
201
+ return result;
202
+ }
203
+ async function executeSelect(ctx, config, tabId, vars) {
204
+ const selector = substituteVars(config.selector, vars);
205
+ const value = config.value ? substituteVars(config.value, vars) : undefined;
206
+ const text = config.text ? substituteVars(config.text, vars) : undefined;
207
+ let expression = "";
208
+ if (value !== undefined) {
209
+ expression = `
210
+ (() => {
211
+ const sel = document.querySelector(${JSON.stringify(selector)});
212
+ if (sel) {
213
+ sel.value = ${JSON.stringify(value)};
214
+ sel.dispatchEvent(new Event('change', { bubbles: true }));
215
+ return true;
216
+ }
217
+ return false;
218
+ })()
219
+ `;
220
+ }
221
+ else if (text !== undefined) {
222
+ expression = `
223
+ (() => {
224
+ const sel = document.querySelector(${JSON.stringify(selector)});
225
+ if (sel) {
226
+ for (const opt of sel.options) {
227
+ if (opt.text === ${JSON.stringify(text)}) {
228
+ sel.value = opt.value;
229
+ sel.dispatchEvent(new Event('change', { bubbles: true }));
230
+ return true;
231
+ }
232
+ }
233
+ }
234
+ return false;
235
+ })()
236
+ `;
237
+ }
238
+ else if (config.index !== undefined) {
239
+ expression = `
240
+ (() => {
241
+ const sel = document.querySelector(${JSON.stringify(selector)});
242
+ if (sel && sel.options[${config.index}]) {
243
+ sel.selectedIndex = ${config.index};
244
+ sel.dispatchEvent(new Event('change', { bubbles: true }));
245
+ return true;
246
+ }
247
+ return false;
248
+ })()
249
+ `;
250
+ }
251
+ return call(ctx, "evaluateJS", { tabId, expression });
252
+ }
253
+ async function executeScroll(ctx, config, tabId, vars) {
254
+ const selector = config.selector ? substituteVars(config.selector, vars) : undefined;
255
+ if (selector) {
256
+ // Scroll to element
257
+ await call(ctx, "evaluateJS", {
258
+ tabId,
259
+ expression: `
260
+ (() => {
261
+ const el = document.querySelector(${JSON.stringify(selector)});
262
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
263
+ })()
264
+ `,
265
+ });
266
+ return { scrolled_to: selector };
267
+ }
268
+ const deltaY = config.y ?? (config.direction === "up" ? -600 : 600);
269
+ const pages = config.pages ?? 1;
270
+ for (let i = 0; i < pages; i++) {
271
+ await call(ctx, "scrollPage", { tabId, deltaY, smooth: true });
272
+ await sleep(400);
273
+ }
274
+ return { scrolled: pages };
275
+ }
276
+ async function executeWait(ctx, config, tabId, vars, timeoutMs) {
277
+ const value = substituteVars(config.value, vars);
278
+ const timeout = config.timeout_ms ?? timeoutMs;
279
+ switch (config.type) {
280
+ case "selector":
281
+ return call(ctx, "waitFor", {
282
+ tabId,
283
+ condition: "element",
284
+ selectors: [value],
285
+ timeoutMs: timeout,
286
+ });
287
+ case "text":
288
+ return call(ctx, "waitFor", {
289
+ tabId,
290
+ condition: "text",
291
+ text: value,
292
+ timeoutMs: timeout,
293
+ });
294
+ case "url":
295
+ return call(ctx, "waitFor", {
296
+ tabId,
297
+ condition: "url",
298
+ urlContains: value,
299
+ timeoutMs: timeout,
300
+ });
301
+ case "time":
302
+ await sleep(Number(value));
303
+ return { waited_ms: Number(value) };
304
+ case "network_idle":
305
+ await sleep(2000); // Simple network idle approximation
306
+ return { network_idle: true };
307
+ default:
308
+ throw new Error(`Unknown wait type: ${config.type}`);
309
+ }
310
+ }
311
+ async function executeExtract(ctx, config, tabId, vars) {
312
+ if (config.expression) {
313
+ const expr = substituteVars(config.expression, vars);
314
+ return call(ctx, "evaluateJS", { tabId, expression: expr });
315
+ }
316
+ if (config.selector) {
317
+ const selector = substituteVars(config.selector, vars);
318
+ const result = (await call(ctx, "queryDom", { tabId, selector }));
319
+ if (config.attribute) {
320
+ const attr = config.attribute;
321
+ const extracted = await call(ctx, "evaluateJS", {
322
+ tabId,
323
+ expression: `
324
+ Array.from(document.querySelectorAll(${JSON.stringify(selector)}))
325
+ .map(el => el.getAttribute(${JSON.stringify(attr)}))
326
+ `,
327
+ });
328
+ return config.all ? extracted : extracted[0];
329
+ }
330
+ if (config.property) {
331
+ const prop = config.property;
332
+ const extracted = await call(ctx, "evaluateJS", {
333
+ tabId,
334
+ expression: `
335
+ Array.from(document.querySelectorAll(${JSON.stringify(selector)}))
336
+ .map(el => el[${JSON.stringify(prop)}])
337
+ `,
338
+ });
339
+ return config.all ? extracted : extracted[0];
340
+ }
341
+ return result;
342
+ }
343
+ return call(ctx, "extractData", { tabId });
344
+ }
345
+ async function executeScreenshot(ctx, config, tabId) {
346
+ return call(ctx, "screenshot", { tabId });
347
+ }
348
+ async function executeEvaluate(ctx, config, tabId, vars) {
349
+ const expression = substituteVars(config.expression, vars);
350
+ return call(ctx, "evaluateJS", { tabId, expression });
351
+ }
352
+ async function executeAssert(ctx, config, tabId, vars) {
353
+ return checkAssertion(ctx, tabId, config, vars);
354
+ }
355
+ async function executeKeyboard(ctx, config, tabId, vars) {
356
+ const key = substituteVars(config.key, vars);
357
+ return call(ctx, "pressKey", {
358
+ tabId,
359
+ key,
360
+ modifiers: config.modifiers ?? [],
361
+ });
362
+ }
363
+ async function executeHover(ctx, config, tabId, vars) {
364
+ const selector = substituteVars(config.selector, vars);
365
+ return call(ctx, "evaluateJS", {
366
+ tabId,
367
+ expression: `
368
+ (() => {
369
+ const el = document.querySelector(${JSON.stringify(selector)});
370
+ if (el) {
371
+ el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true }));
372
+ el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, cancelable: true }));
373
+ return true;
374
+ }
375
+ return false;
376
+ })()
377
+ `,
378
+ });
379
+ }
380
+ async function executeTab(ctx, config, vars) {
381
+ switch (config.action) {
382
+ case "new": {
383
+ const url = config.url ? substituteVars(config.url, vars) : "about:blank";
384
+ const result = (await call(ctx, "openTab", { url }));
385
+ return { tabId: result.tabId ?? null, result };
386
+ }
387
+ case "close": {
388
+ if (config.index !== undefined) {
389
+ // Close by index - we don't have direct tab list access, skip
390
+ return { tabId: null, result: { closed: false } };
391
+ }
392
+ return { tabId: null, result: { closed: true } };
393
+ }
394
+ case "switch": {
395
+ if (config.index !== undefined) {
396
+ // Switch by index - call focusTab with the index
397
+ await call(ctx, "focusTab", { tabId: config.index });
398
+ return { tabId: config.index, result: { switched: true } };
399
+ }
400
+ return { tabId: null, result: { switched: false } };
401
+ }
402
+ default:
403
+ throw new Error(`Unknown tab action: ${config.action}`);
404
+ }
405
+ }
406
+ async function executeGroup(ctx, config, tabId, vars, suiteDefaults) {
407
+ const results = [];
408
+ let currentTabId = tabId;
409
+ let currentVars = { ...vars };
410
+ // Handle condition
411
+ if (config.condition) {
412
+ const condExpr = substituteVars(config.condition, currentVars);
413
+ let condResult = false;
414
+ if (currentTabId !== null) {
415
+ try {
416
+ condResult = Boolean(await call(ctx, "evaluateJS", { tabId: currentTabId, expression: condExpr }));
417
+ }
418
+ catch {
419
+ condResult = false;
420
+ }
421
+ }
422
+ if (!condResult) {
423
+ return { results, tabId: currentTabId, variables: currentVars };
424
+ }
425
+ }
426
+ // Handle loop
427
+ if (config.loop) {
428
+ const loop = config.loop;
429
+ let iterations = 0;
430
+ const maxIterations = loop.count ?? 1000; // Safety limit
431
+ while (iterations < maxIterations) {
432
+ if (loop.while) {
433
+ const whileExpr = substituteVars(loop.while, currentVars);
434
+ if (currentTabId === null)
435
+ break;
436
+ const condResult = Boolean(await call(ctx, "evaluateJS", { tabId: currentTabId, expression: whileExpr }));
437
+ if (!condResult)
438
+ break;
439
+ }
440
+ // Execute steps
441
+ for (const step of config.steps) {
442
+ const stepResult = await executeStep(ctx, step, currentVars, currentTabId, suiteDefaults);
443
+ results.push(stepResult.result);
444
+ currentTabId = stepResult.tabId;
445
+ currentVars = stepResult.variables;
446
+ if (stepResult.result.status === "failed" || stepResult.result.status === "error") {
447
+ return { results, tabId: currentTabId, variables: currentVars };
448
+ }
449
+ }
450
+ iterations++;
451
+ if (loop.count !== undefined && iterations >= loop.count)
452
+ break;
453
+ }
454
+ }
455
+ else {
456
+ // Execute steps once
457
+ for (const step of config.steps) {
458
+ const stepResult = await executeStep(ctx, step, currentVars, currentTabId, suiteDefaults);
459
+ results.push(stepResult.result);
460
+ currentTabId = stepResult.tabId;
461
+ currentVars = stepResult.variables;
462
+ if (stepResult.result.status === "failed" || stepResult.result.status === "error") {
463
+ break;
464
+ }
465
+ }
466
+ }
467
+ return { results, tabId: currentTabId, variables: currentVars };
468
+ }
469
+ // ============================================================================
470
+ // Step Execution
471
+ // ============================================================================
472
+ export async function executeStep(ctx, step, variables, tabId, suiteDefaults) {
473
+ const startTime = Date.now();
474
+ const logs = [];
475
+ let currentTabId = tabId;
476
+ let currentVars = { ...variables };
477
+ // Handle disabled steps
478
+ if (step.disabled) {
479
+ return {
480
+ result: {
481
+ step_id: step.id,
482
+ step_name: step.name,
483
+ action: step.action,
484
+ status: "skipped",
485
+ duration_ms: 0,
486
+ },
487
+ tabId: currentTabId,
488
+ variables: currentVars,
489
+ };
490
+ }
491
+ // Wait before execution
492
+ if (step.wait_before_ms) {
493
+ await sleep(step.wait_before_ms);
494
+ }
495
+ const timeout = step.timeout_ms ?? suiteDefaults?.timeout_ms ?? 15000;
496
+ const onError = step.on_error ?? suiteDefaults?.on_error ?? "stop";
497
+ const retryConfig = step.retry ?? { max: 0, delay_ms: 1000 };
498
+ let attempts = 0;
499
+ let lastError = null;
500
+ let actionResult = null;
501
+ // Retry loop
502
+ while (attempts <= retryConfig.max) {
503
+ try {
504
+ // Substitute variables in config
505
+ const config = substituteInConfig(step.config, currentVars);
506
+ // Execute action
507
+ switch (step.action) {
508
+ case "navigate": {
509
+ const navResult = await executeNavigate(ctx, config, currentTabId, currentVars);
510
+ currentTabId = navResult.tabId;
511
+ actionResult = navResult.result;
512
+ break;
513
+ }
514
+ case "click":
515
+ if (currentTabId === null)
516
+ throw new Error("No active tab for click action");
517
+ actionResult = await executeClick(ctx, config, currentTabId, currentVars);
518
+ break;
519
+ case "type":
520
+ if (currentTabId === null)
521
+ throw new Error("No active tab for type action");
522
+ actionResult = await executeType(ctx, config, currentTabId, currentVars);
523
+ break;
524
+ case "select":
525
+ if (currentTabId === null)
526
+ throw new Error("No active tab for select action");
527
+ actionResult = await executeSelect(ctx, config, currentTabId, currentVars);
528
+ break;
529
+ case "scroll":
530
+ if (currentTabId === null)
531
+ throw new Error("No active tab for scroll action");
532
+ actionResult = await executeScroll(ctx, config, currentTabId, currentVars);
533
+ break;
534
+ case "wait":
535
+ if (currentTabId === null)
536
+ throw new Error("No active tab for wait action");
537
+ actionResult = await executeWait(ctx, config, currentTabId, currentVars, timeout);
538
+ break;
539
+ case "extract":
540
+ if (currentTabId === null)
541
+ throw new Error("No active tab for extract action");
542
+ actionResult = await executeExtract(ctx, config, currentTabId, currentVars);
543
+ break;
544
+ case "screenshot":
545
+ if (currentTabId === null)
546
+ throw new Error("No active tab for screenshot action");
547
+ actionResult = await executeScreenshot(ctx, config, currentTabId);
548
+ break;
549
+ case "evaluate":
550
+ if (currentTabId === null)
551
+ throw new Error("No active tab for evaluate action");
552
+ actionResult = await executeEvaluate(ctx, config, currentTabId, currentVars);
553
+ break;
554
+ case "assert":
555
+ if (currentTabId === null)
556
+ throw new Error("No active tab for assert action");
557
+ actionResult = await executeAssert(ctx, config, currentTabId, currentVars);
558
+ break;
559
+ case "keyboard":
560
+ if (currentTabId === null)
561
+ throw new Error("No active tab for keyboard action");
562
+ actionResult = await executeKeyboard(ctx, config, currentTabId, currentVars);
563
+ break;
564
+ case "hover":
565
+ if (currentTabId === null)
566
+ throw new Error("No active tab for hover action");
567
+ actionResult = await executeHover(ctx, config, currentTabId, currentVars);
568
+ break;
569
+ case "tab": {
570
+ const tabResult = await executeTab(ctx, config, currentVars);
571
+ if (tabResult.tabId !== null)
572
+ currentTabId = tabResult.tabId;
573
+ actionResult = tabResult.result;
574
+ break;
575
+ }
576
+ case "group": {
577
+ const groupResult = await executeGroup(ctx, config, currentTabId, currentVars, suiteDefaults);
578
+ currentTabId = groupResult.tabId;
579
+ currentVars = groupResult.variables;
580
+ actionResult = { steps: groupResult.results };
581
+ break;
582
+ }
583
+ case "recipe": {
584
+ const cfg = config;
585
+ const recipeSlug = substituteVars(cfg.slug, currentVars);
586
+ const recipeVars = cfg.variables
587
+ ? Object.fromEntries(Object.entries(cfg.variables).map(([k, v]) => [k, substituteVars(v, currentVars)]))
588
+ : undefined;
589
+ actionResult = await loadRecipe(recipeSlug, recipeVars);
590
+ break;
591
+ }
592
+ default:
593
+ throw new Error(`Unknown action: ${step.action}`);
594
+ }
595
+ // Success - break retry loop
596
+ lastError = null;
597
+ break;
598
+ }
599
+ catch (err) {
600
+ lastError = err;
601
+ attempts++;
602
+ if (onError === "retry" && attempts <= retryConfig.max) {
603
+ logs.push(`Attempt ${attempts} failed: ${lastError.message}. Retrying...`);
604
+ await sleep(retryConfig.delay_ms);
605
+ }
606
+ else {
607
+ break;
608
+ }
609
+ }
610
+ }
611
+ // Handle final error
612
+ if (lastError) {
613
+ const duration = Date.now() - startTime;
614
+ if (onError === "continue") {
615
+ logs.push(`Error (continuing): ${lastError.message}`);
616
+ return {
617
+ result: {
618
+ step_id: step.id,
619
+ step_name: step.name,
620
+ action: step.action,
621
+ status: "error",
622
+ duration_ms: duration,
623
+ error: lastError.message,
624
+ logs,
625
+ },
626
+ tabId: currentTabId,
627
+ variables: currentVars,
628
+ };
629
+ }
630
+ return {
631
+ result: {
632
+ step_id: step.id,
633
+ step_name: step.name,
634
+ action: step.action,
635
+ status: "failed",
636
+ duration_ms: duration,
637
+ error: lastError.message,
638
+ logs,
639
+ },
640
+ tabId: currentTabId,
641
+ variables: currentVars,
642
+ };
643
+ }
644
+ // Run assertions
645
+ const assertionResults = [];
646
+ if (step.assertions && currentTabId !== null) {
647
+ for (const assertion of step.assertions) {
648
+ const result = await checkAssertion(ctx, currentTabId, assertion, currentVars);
649
+ assertionResults.push(result);
650
+ if (!result.passed) {
651
+ logs.push(`Assertion failed: ${assertion.check} - expected ${assertion.expected}, got ${result.actual}`);
652
+ }
653
+ }
654
+ }
655
+ // Extract variables
656
+ const extracted = {};
657
+ if (step.extract && currentTabId !== null) {
658
+ for (const [varName, expression] of Object.entries(step.extract)) {
659
+ try {
660
+ const expr = substituteVars(expression, currentVars);
661
+ const value = await call(ctx, "evaluateJS", { tabId: currentTabId, expression: expr });
662
+ extracted[varName] = String(value ?? "");
663
+ currentVars[varName] = extracted[varName];
664
+ }
665
+ catch (err) {
666
+ logs.push(`Failed to extract ${varName}: ${err.message}`);
667
+ }
668
+ }
669
+ }
670
+ // Wait after execution
671
+ if (step.wait_after_ms) {
672
+ await sleep(step.wait_after_ms);
673
+ }
674
+ const duration = Date.now() - startTime;
675
+ const allAssertionsPassed = assertionResults.length === 0 || assertionResults.every((a) => a.passed);
676
+ return {
677
+ result: {
678
+ step_id: step.id,
679
+ step_name: step.name,
680
+ action: step.action,
681
+ status: allAssertionsPassed ? "passed" : "failed",
682
+ duration_ms: duration,
683
+ assertions: assertionResults.length > 0 ? assertionResults : undefined,
684
+ extracted: Object.keys(extracted).length > 0 ? extracted : undefined,
685
+ logs: logs.length > 0 ? logs : undefined,
686
+ },
687
+ tabId: currentTabId,
688
+ variables: currentVars,
689
+ };
690
+ }
691
+ // ============================================================================
692
+ // Suite Execution
693
+ // ============================================================================
694
+ export async function executeSuite(ctx, suiteId, suite) {
695
+ const startTime = new Date();
696
+ const steps = [];
697
+ let currentTabId = null;
698
+ let variables = { ...suite.variables };
699
+ let status = "passed";
700
+ try {
701
+ // Execute setup recipe (before setup steps)
702
+ if (suite.setup_recipe) {
703
+ const recipeStart = Date.now();
704
+ try {
705
+ const recipeResult = await loadRecipe(suite.setup_recipe.slug, suite.setup_recipe.variables);
706
+ steps.push({
707
+ step_id: "__setup_recipe",
708
+ step_name: `Recipe: ${suite.setup_recipe.slug}`,
709
+ action: "recipe",
710
+ status: "passed",
711
+ duration_ms: Date.now() - recipeStart,
712
+ logs: [JSON.stringify(recipeResult)],
713
+ });
714
+ }
715
+ catch (err) {
716
+ steps.push({
717
+ step_id: "__setup_recipe",
718
+ step_name: `Recipe: ${suite.setup_recipe.slug}`,
719
+ action: "recipe",
720
+ status: "failed",
721
+ duration_ms: Date.now() - recipeStart,
722
+ error: err.message,
723
+ });
724
+ status = "failed";
725
+ }
726
+ }
727
+ // Execute setup steps
728
+ if (suite.setup && status === "passed") {
729
+ for (const step of suite.setup) {
730
+ const result = await executeStep(ctx, step, variables, currentTabId, suite.defaults);
731
+ steps.push(result.result);
732
+ currentTabId = result.tabId;
733
+ variables = result.variables;
734
+ if (result.result.status === "failed" || result.result.status === "error") {
735
+ status = "failed";
736
+ break;
737
+ }
738
+ }
739
+ }
740
+ // Execute main steps (only if setup succeeded)
741
+ if (status === "passed") {
742
+ for (const step of suite.steps) {
743
+ const result = await executeStep(ctx, step, variables, currentTabId, suite.defaults);
744
+ steps.push(result.result);
745
+ currentTabId = result.tabId;
746
+ variables = result.variables;
747
+ if (result.result.status === "failed") {
748
+ status = "failed";
749
+ // Continue or stop based on on_error
750
+ if (step.on_error !== "continue")
751
+ break;
752
+ }
753
+ else if (result.result.status === "error") {
754
+ status = "error";
755
+ if (step.on_error !== "continue")
756
+ break;
757
+ }
758
+ // Wait between steps
759
+ if (suite.defaults?.wait_between_ms) {
760
+ await sleep(suite.defaults.wait_between_ms);
761
+ }
762
+ }
763
+ }
764
+ }
765
+ catch (err) {
766
+ status = "error";
767
+ steps.push({
768
+ step_id: "suite_error",
769
+ action: "error",
770
+ status: "error",
771
+ duration_ms: 0,
772
+ error: err.message,
773
+ });
774
+ }
775
+ finally {
776
+ // Always execute teardown steps
777
+ if (suite.teardown) {
778
+ for (const step of suite.teardown) {
779
+ try {
780
+ const result = await executeStep(ctx, step, variables, currentTabId, suite.defaults);
781
+ steps.push(result.result);
782
+ currentTabId = result.tabId;
783
+ variables = result.variables;
784
+ }
785
+ catch (err) {
786
+ steps.push({
787
+ step_id: step.id,
788
+ step_name: step.name,
789
+ action: step.action,
790
+ status: "error",
791
+ duration_ms: 0,
792
+ error: err.message,
793
+ });
794
+ }
795
+ }
796
+ }
797
+ // Always execute teardown recipe (after teardown steps)
798
+ if (suite.teardown_recipe) {
799
+ const recipeStart = Date.now();
800
+ try {
801
+ const recipeResult = await loadRecipe(suite.teardown_recipe.slug, suite.teardown_recipe.variables);
802
+ steps.push({
803
+ step_id: "__teardown_recipe",
804
+ step_name: `Recipe: ${suite.teardown_recipe.slug}`,
805
+ action: "recipe",
806
+ status: "passed",
807
+ duration_ms: Date.now() - recipeStart,
808
+ logs: [JSON.stringify(recipeResult)],
809
+ });
810
+ }
811
+ catch (err) {
812
+ steps.push({
813
+ step_id: "__teardown_recipe",
814
+ step_name: `Recipe: ${suite.teardown_recipe.slug}`,
815
+ action: "recipe",
816
+ status: "error",
817
+ duration_ms: Date.now() - recipeStart,
818
+ error: err.message,
819
+ });
820
+ }
821
+ }
822
+ }
823
+ const endTime = new Date();
824
+ return {
825
+ suite_id: suiteId,
826
+ suite_name: suite.name,
827
+ status,
828
+ started_at: startTime.toISOString(),
829
+ completed_at: endTime.toISOString(),
830
+ duration_ms: endTime.getTime() - startTime.getTime(),
831
+ steps,
832
+ extracted: variables,
833
+ };
834
+ }
835
+ //# sourceMappingURL=executor.js.map