@dyyz1993/agent-browser 0.11.5 → 0.13.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 (47) hide show
  1. package/bin/agent-browser-linux-x64 +0 -0
  2. package/dist/__tests__/utils/parseCli.d.ts.map +1 -1
  3. package/dist/__tests__/utils/parseCli.js +97 -2
  4. package/dist/__tests__/utils/parseCli.js.map +1 -1
  5. package/dist/actions.d.ts +2 -1
  6. package/dist/actions.d.ts.map +1 -1
  7. package/dist/actions.js +254 -105
  8. package/dist/actions.js.map +1 -1
  9. package/dist/browser.d.ts +41 -0
  10. package/dist/browser.d.ts.map +1 -1
  11. package/dist/browser.js +125 -30
  12. package/dist/browser.js.map +1 -1
  13. package/dist/cli/commands.d.ts.map +1 -1
  14. package/dist/cli/commands.js +129 -5
  15. package/dist/cli/commands.js.map +1 -1
  16. package/dist/cli/connection.d.ts.map +1 -1
  17. package/dist/cli/connection.js +12 -29
  18. package/dist/cli/connection.js.map +1 -1
  19. package/dist/cli/help.d.ts.map +1 -1
  20. package/dist/cli/help.js +35 -25
  21. package/dist/cli/help.js.map +1 -1
  22. package/dist/cli/output.d.ts.map +1 -1
  23. package/dist/cli/output.js +27 -0
  24. package/dist/cli/output.js.map +1 -1
  25. package/dist/cli.js +117 -3
  26. package/dist/cli.js.map +1 -1
  27. package/dist/daemon.d.ts +18 -2
  28. package/dist/daemon.d.ts.map +1 -1
  29. package/dist/daemon.js +71 -33
  30. package/dist/daemon.js.map +1 -1
  31. package/dist/message-bridge.d.ts.map +1 -1
  32. package/dist/message-bridge.js +4 -1
  33. package/dist/message-bridge.js.map +1 -1
  34. package/dist/protocol.d.ts.map +1 -1
  35. package/dist/protocol.js +18 -0
  36. package/dist/protocol.js.map +1 -1
  37. package/dist/rc-config.d.ts +42 -0
  38. package/dist/rc-config.d.ts.map +1 -0
  39. package/dist/rc-config.js +171 -0
  40. package/dist/rc-config.js.map +1 -0
  41. package/dist/recorder/inject.js +30 -24
  42. package/dist/types.d.ts +16 -1
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js.map +1 -1
  45. package/package.json +2 -1
  46. package/scripts/generate-skill.cjs +303 -0
  47. package/skills/agent-browser/SKILL.md +135 -370
package/dist/actions.js CHANGED
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { getAppDir, getSession, getInstanceId } from './daemon.js';
4
4
  import { performDiff } from './diff.js';
5
5
  import { MessageBridge } from './message-bridge.js';
6
+ import { getViewerUrl, getViewerWsUrl, getViewerPort, getMessageBridgeUrl, getExecutablePath, getEffectiveValue, loadConfig, } from './rc-config.js';
6
7
  import { detectMainContent, generateContentTips } from './content-detection.js';
7
8
  import { humanClick, humanType, humanMoveTo, humanWander, getHumanConfigFromEnv, } from './human-mouse.js';
8
9
  import { successResponse, errorResponse } from './protocol.js';
@@ -52,37 +53,44 @@ async function assertElementExists(locator, selector, isRef) {
52
53
  */
53
54
  export function toAIFriendlyError(error, selector) {
54
55
  const message = error instanceof Error ? error.message : String(error);
55
- // Handle strict mode violation (multiple elements match)
56
56
  if (message.includes('strict mode violation')) {
57
- // Extract count if available
58
57
  const countMatch = message.match(/resolved to (\d+) elements/);
59
58
  const count = countMatch ? countMatch[1] : 'multiple';
60
59
  return new Error(`Selector "${selector}" matched ${count} elements. ` +
61
- `Run 'snapshot' to get updated refs, or use a more specific CSS selector.`);
60
+ `Run 'snapshot' to get updated refs, or use a more specific CSS selector. ` +
61
+ `Tip: Use 'find nth <index> ${selector} --click' to target a specific match.`);
62
62
  }
63
- // Handle element not interactable (must be checked BEFORE timeout case)
64
- // This includes cases where an overlay/modal blocks the element
65
63
  if (message.includes('intercepts pointer events')) {
66
64
  return new Error(`Element "${selector}" is blocked by another element (likely a modal or overlay). ` +
67
- `Try dismissing any modals/cookie banners first.`);
65
+ `Try dismissing any modals/cookie banners first. ` +
66
+ `Tip: Run 'snapshot -i' to see all visible elements and identify what's blocking.`);
68
67
  }
69
- // Handle element not visible
70
68
  if (message.includes('not visible') && !message.includes('Timeout')) {
71
69
  return new Error(`Element "${selector}" is not visible. ` +
72
- `Try scrolling it into view or check if it's hidden.`);
70
+ `Try 'scrollintoview ${selector}' or check if it's hidden. ` +
71
+ `Tip: Run 'is visible ${selector}' to confirm visibility state.`);
73
72
  }
74
- // Handle general timeout (element exists but action couldn't complete)
75
73
  if (message.includes('Timeout') && message.includes('exceeded')) {
76
74
  return new Error(`Action on "${selector}" timed out. The element may be blocked, still loading, or not interactable. ` +
77
- `Run 'snapshot' to check the current page state.`);
75
+ `Run 'snapshot' to check the current page state. ` +
76
+ `Tip: If the page is still loading, try 'wait --load networkidle' first.`);
78
77
  }
79
- // Handle element not found (timeout waiting for element)
80
78
  if (message.includes('waiting for') &&
81
79
  (message.includes('to be visible') || message.includes('Timeout'))) {
82
80
  return new Error(`Element "${selector}" not found or not visible. ` +
83
- `Run 'snapshot' to see current page elements.`);
81
+ `Run 'snapshot -i' to see current page elements and their refs. ` +
82
+ `Tip: If using @ref, the page may have changed. Re-run 'snapshot -i' to get fresh refs.`);
83
+ }
84
+ if (message.includes('Execution context was destroyed') || message.includes('Target closed')) {
85
+ return new Error(`Browser context was lost (page navigated or closed). ` +
86
+ `Re-open the page with 'open <url>' and start fresh. ` +
87
+ `Tip: This usually happens after a form submission triggers navigation.`);
88
+ }
89
+ if (message.includes('querySelector') || message.includes('is not a valid selector')) {
90
+ return new Error(`Invalid selector "${selector}". ` +
91
+ `CSS selectors like '#id', '.class', or 'tag' are supported. ` +
92
+ `Tip: Use 'snapshot -i' to get @ref selectors (e.g., @e1) that are always valid.`);
84
93
  }
85
- // Return original error for unknown cases
86
94
  return error instanceof Error ? error : new Error(message);
87
95
  }
88
96
  /**
@@ -146,6 +154,8 @@ export async function executeCommand(command, browser) {
146
154
  return await handleTabNew(cmd, browser);
147
155
  case 'tab_list':
148
156
  return await handleTabList(cmd, browser);
157
+ case 'frames':
158
+ return await handleFrames(cmd, browser);
149
159
  case 'tab_switch':
150
160
  return await handleTabSwitch(cmd, browser);
151
161
  case 'tab_close':
@@ -174,6 +184,8 @@ export async function executeCommand(command, browser) {
174
184
  return await handleUnroute(cmd, browser);
175
185
  case 'requests':
176
186
  return await handleRequests(cmd, browser);
187
+ case 'websockets':
188
+ return await handleWebSockets(cmd, browser);
177
189
  case 'download':
178
190
  return await handleDownload(cmd, browser);
179
191
  case 'geolocation':
@@ -352,6 +364,8 @@ export async function executeCommand(command, browser) {
352
364
  return await handleAsk(cmd, browser);
353
365
  case 'config':
354
366
  return handleConfig(cmd);
367
+ case 'history':
368
+ return await handleHistory(cmd, browser);
355
369
  default: {
356
370
  const unknownCommand = cmd;
357
371
  return errorResponse(unknownCommand.id, `Unknown action: ${unknownCommand.action}`);
@@ -369,7 +383,7 @@ async function handleLaunch(command, browser) {
369
383
  return successResponse(command.id, {
370
384
  launched: true,
371
385
  instanceId,
372
- viewerUrl: `http://localhost:5005/view?instanceId=${instanceId}`,
386
+ viewerUrl: getViewerUrl(instanceId),
373
387
  });
374
388
  }
375
389
  async function handleNavigate(command, browser) {
@@ -418,6 +432,7 @@ async function handleClick(command, browser) {
418
432
  result.diff = diffResult.output;
419
433
  result.diffScope = diffResult.diff.scope;
420
434
  }
435
+ browser.recordCommand('click', command.selector, undefined, true);
421
436
  return successResponse(command.id, result);
422
437
  }
423
438
  const diffResult = await performDiff(locator, command.diffScope, async () => {
@@ -438,6 +453,7 @@ async function handleClick(command, browser) {
438
453
  result.diff = diffResult.output;
439
454
  result.diffScope = diffResult.diff.scope;
440
455
  }
456
+ browser.recordCommand('click', command.selector, undefined, true);
441
457
  return successResponse(command.id, result);
442
458
  }
443
459
  async function handleType(command, browser) {
@@ -468,6 +484,7 @@ async function handleType(command, browser) {
468
484
  result.diff = diffResult.output;
469
485
  result.diffScope = diffResult.diff.scope;
470
486
  }
487
+ browser.recordCommand('type', command.selector, command.text, true);
471
488
  return successResponse(command.id, result);
472
489
  }
473
490
  const diffResult = await performDiff(locator, command.diffScope, async () => {
@@ -491,6 +508,7 @@ async function handleType(command, browser) {
491
508
  result.diff = diffResult.output;
492
509
  result.diffScope = diffResult.diff.scope;
493
510
  }
511
+ browser.recordCommand('type', command.selector, command.text, true);
494
512
  return successResponse(command.id, result);
495
513
  }
496
514
  async function handlePress(command, browser) {
@@ -604,7 +622,6 @@ async function handleScreenshot(command, browser) {
604
622
  async function handleSnapshot(command, browser) {
605
623
  let effectiveSelector = command.selector;
606
624
  let detectionResult = null;
607
- // 如果未指定 selector,自动检测主体区域
608
625
  if (!command.selector) {
609
626
  const page = browser.getPage();
610
627
  detectionResult = await detectMainContent(page);
@@ -619,6 +636,8 @@ async function handleSnapshot(command, browser) {
619
636
  framePath: command.inFrame,
620
637
  path: command.path,
621
638
  attrs: command.attrs,
639
+ selectors: command.selectors,
640
+ all: command.all,
622
641
  });
623
642
  const simpleRefs = {};
624
643
  const refs = snapshot.refs || {};
@@ -662,9 +681,12 @@ async function handleEvaluate(command, browser) {
662
681
  const page = browser.getPage();
663
682
  result = await page.evaluate(script);
664
683
  }
684
+ browser.recordCommand('eval', 'javascript', script.length > 200 ? script.substring(0, 200) + '...' : script, true);
665
685
  return successResponse(command.id, { result });
666
686
  }
667
687
  catch (error) {
688
+ const script = command.script || command.file || '';
689
+ browser.recordCommand('eval', 'javascript', script.length > 200 ? script.substring(0, 200) + '...' : script, false);
668
690
  console.error('Error in handleEvaluate:', error);
669
691
  return errorResponse(command.id, error instanceof Error ? error.message : String(error));
670
692
  }
@@ -766,6 +788,7 @@ async function handleSelect(command, browser) {
766
788
  result.diff = diffResult.output;
767
789
  result.diffScope = diffResult.diff.scope;
768
790
  }
791
+ browser.recordCommand('select', command.selector, values.join(','), true);
769
792
  return successResponse(command.id, result);
770
793
  }
771
794
  async function handleHover(command, browser) {
@@ -842,6 +865,16 @@ async function handleTabList(command, browser) {
842
865
  active: browser.getActiveIndex(),
843
866
  });
844
867
  }
868
+ async function handleFrames(command, browser) {
869
+ const frames = browser.listFrames();
870
+ if (frames.length === 0) {
871
+ return successResponse(command.id, {
872
+ frames: [],
873
+ tip: 'No iframes found on this page.',
874
+ });
875
+ }
876
+ return successResponse(command.id, { frames });
877
+ }
845
878
  async function handleTabSwitch(command, browser) {
846
879
  const result = await browser.switchTo(command.index);
847
880
  const page = browser.getPage();
@@ -890,22 +923,23 @@ async function handleFill(command, browser) {
890
923
  result.diff = diffResult.output;
891
924
  result.diffScope = diffResult.diff.scope;
892
925
  }
926
+ browser.recordCommand('fill', command.selector, command.value, true);
893
927
  return successResponse(command.id, result);
894
928
  }
895
929
  const diffResult = await performDiff(locator, command.diffScope, async () => {
896
930
  try {
897
931
  await locator.fill(command.value);
898
- // Trigger input event for recorder to capture
899
- // Use page.evaluate to dispatch events in the browser context
900
- const page = browser.getPage();
901
- if (page) {
902
- await page.evaluate((selector) => {
903
- const el = document.querySelector(selector);
904
- if (el) {
905
- el.dispatchEvent(new Event('input', { bubbles: true }));
906
- el.dispatchEvent(new Event('change', { bubbles: true }));
907
- }
908
- }, command.selector);
932
+ if (!isRef) {
933
+ const page = browser.getPage();
934
+ if (page) {
935
+ await page.evaluate((selector) => {
936
+ const el = document.querySelector(selector);
937
+ if (el) {
938
+ el.dispatchEvent(new Event('input', { bubbles: true }));
939
+ el.dispatchEvent(new Event('change', { bubbles: true }));
940
+ }
941
+ }, command.selector);
942
+ }
909
943
  }
910
944
  }
911
945
  catch (error) {
@@ -917,6 +951,7 @@ async function handleFill(command, browser) {
917
951
  result.diff = diffResult.output;
918
952
  result.diffScope = diffResult.diff.scope;
919
953
  }
954
+ browser.recordCommand('fill', command.selector, command.value, true);
920
955
  return successResponse(command.id, result);
921
956
  }
922
957
  async function handleCheck(command, browser) {
@@ -940,6 +975,7 @@ async function handleCheck(command, browser) {
940
975
  result.diff = diffResult.output;
941
976
  result.diffScope = diffResult.diff.scope;
942
977
  }
978
+ browser.recordCommand('check', command.selector, undefined, true);
943
979
  return successResponse(command.id, result);
944
980
  }
945
981
  async function handleUncheck(command, browser) {
@@ -963,6 +999,7 @@ async function handleUncheck(command, browser) {
963
999
  result.diff = diffResult.output;
964
1000
  result.diffScope = diffResult.diff.scope;
965
1001
  }
1002
+ browser.recordCommand('uncheck', command.selector, undefined, true);
966
1003
  return successResponse(command.id, result);
967
1004
  }
968
1005
  async function handleUpload(command, browser) {
@@ -1061,32 +1098,85 @@ async function handleGetByRole(command, browser) {
1061
1098
  name: command.name,
1062
1099
  exact: command.exact,
1063
1100
  });
1064
- switch (command.subaction) {
1065
- case 'click':
1066
- await locator.click();
1067
- return successResponse(command.id, { clicked: true });
1068
- case 'fill':
1069
- await locator.fill(command.value ?? '');
1070
- return successResponse(command.id, { filled: true });
1071
- case 'check':
1072
- await locator.check();
1073
- return successResponse(command.id, { checked: true });
1074
- case 'hover':
1075
- await locator.hover();
1076
- return successResponse(command.id, { hovered: true });
1101
+ try {
1102
+ switch (command.subaction) {
1103
+ case 'click':
1104
+ await locator.click();
1105
+ return successResponse(command.id, { clicked: true });
1106
+ case 'fill':
1107
+ await locator.fill(command.value ?? '');
1108
+ return successResponse(command.id, { filled: true });
1109
+ case 'check':
1110
+ await locator.check();
1111
+ return successResponse(command.id, { checked: true });
1112
+ case 'hover':
1113
+ await locator.hover();
1114
+ return successResponse(command.id, { hovered: true });
1115
+ }
1116
+ }
1117
+ catch (error) {
1118
+ const msg = error instanceof Error ? error.message : String(error);
1119
+ if (msg.includes('strict mode violation')) {
1120
+ const countMatch = msg.match(/resolved to (\d+) elements/);
1121
+ const count = countMatch ? countMatch[1] : 'multiple';
1122
+ const first = locator.first();
1123
+ const warning = `Matched ${count} elements, used first match. Use 'find nth <index> role "${command.role}" --click' for a specific match.`;
1124
+ switch (command.subaction) {
1125
+ case 'click':
1126
+ await first.click();
1127
+ return successResponse(command.id, { clicked: true, warning });
1128
+ case 'fill':
1129
+ await first.fill(command.value ?? '');
1130
+ return successResponse(command.id, { filled: true, warning });
1131
+ case 'check':
1132
+ await first.check();
1133
+ return successResponse(command.id, { checked: true, warning });
1134
+ case 'hover':
1135
+ await first.hover();
1136
+ return successResponse(command.id, { hovered: true, warning });
1137
+ }
1138
+ }
1139
+ throw error;
1077
1140
  }
1141
+ return successResponse(command.id, {});
1078
1142
  }
1079
1143
  async function handleGetByText(command, browser) {
1080
1144
  const frame = browser.getFrame(command.inFrame);
1081
1145
  const locator = frame.getByText(command.text, { exact: command.exact });
1082
- switch (command.subaction) {
1083
- case 'click':
1084
- await locator.click();
1085
- return successResponse(command.id, { clicked: true });
1086
- case 'hover':
1087
- await locator.hover();
1088
- return successResponse(command.id, { hovered: true });
1146
+ try {
1147
+ switch (command.subaction) {
1148
+ case 'click':
1149
+ await locator.click();
1150
+ return successResponse(command.id, { clicked: true });
1151
+ case 'hover':
1152
+ await locator.hover();
1153
+ return successResponse(command.id, { hovered: true });
1154
+ }
1089
1155
  }
1156
+ catch (error) {
1157
+ const msg = error instanceof Error ? error.message : String(error);
1158
+ if (msg.includes('strict mode violation')) {
1159
+ const countMatch = msg.match(/resolved to (\d+) elements/);
1160
+ const count = countMatch ? countMatch[1] : 'multiple';
1161
+ const first = locator.first();
1162
+ switch (command.subaction) {
1163
+ case 'click':
1164
+ await first.click();
1165
+ return successResponse(command.id, {
1166
+ clicked: true,
1167
+ warning: `Matched ${count} elements, used first match. Use 'find nth <index> text "${command.text}" --click' for a specific match.`,
1168
+ });
1169
+ case 'hover':
1170
+ await first.hover();
1171
+ return successResponse(command.id, {
1172
+ hovered: true,
1173
+ warning: `Matched ${count} elements, used first match. Use 'find nth <index> text "${command.text}" --hover' for a specific match.`,
1174
+ });
1175
+ }
1176
+ }
1177
+ throw error;
1178
+ }
1179
+ return successResponse(command.id, {});
1090
1180
  }
1091
1181
  async function handleGetByLabel(command, browser) {
1092
1182
  const frame = browser.getFrame(command.inFrame);
@@ -1207,6 +1297,7 @@ async function handleRequests(command, browser) {
1207
1297
  return successResponse(command.id, { cleared: true });
1208
1298
  }
1209
1299
  // Start tracking if not already (with response capture if requested)
1300
+ const wasTracking = browser.trackingEnabled;
1210
1301
  browser.startRequestTracking(command.captureResponse);
1211
1302
  // If output directory is specified, save to directory
1212
1303
  if (command.output) {
@@ -1219,7 +1310,25 @@ async function handleRequests(command, browser) {
1219
1310
  });
1220
1311
  }
1221
1312
  const requests = browser.getRequests(command.filter, command.type);
1222
- return successResponse(command.id, { requests });
1313
+ const result = { requests };
1314
+ if (requests.length === 0 && !wasTracking) {
1315
+ result.hint = 'Request tracking just activated. Reload or navigate to capture requests.';
1316
+ }
1317
+ return successResponse(command.id, result);
1318
+ }
1319
+ async function handleWebSockets(command, browser) {
1320
+ if (command.clear) {
1321
+ browser.clearWebSockets();
1322
+ return successResponse(command.id, { cleared: true });
1323
+ }
1324
+ const wasTracking = browser.wsTrackingEnabled;
1325
+ browser.startWebSocketTracking();
1326
+ const sockets = browser.getWebSockets(command.filter);
1327
+ const result = { websockets: sockets };
1328
+ if (sockets.length === 0 && !wasTracking) {
1329
+ result.hint = 'WebSocket tracking just activated. Reload or navigate to capture connections.';
1330
+ }
1331
+ return successResponse(command.id, result);
1223
1332
  }
1224
1333
  async function handleDownload(command, browser) {
1225
1334
  const page = browser.getPage();
@@ -1659,7 +1768,7 @@ async function handleNth(command, browser) {
1659
1768
  const refLocator = browser.getLocatorFromRef(command.selector, command.inFrame);
1660
1769
  let locator;
1661
1770
  if (refLocator) {
1662
- locator = refLocator;
1771
+ locator = command.index === -1 ? refLocator.last() : refLocator.nth(command.index);
1663
1772
  }
1664
1773
  else {
1665
1774
  const frame = browser.getFrame(command.inFrame);
@@ -1794,10 +1903,19 @@ async function handleScrollIntoView(command, browser) {
1794
1903
  await page.locator(command.selector).scrollIntoViewIfNeeded();
1795
1904
  return successResponse(command.id, { scrolled: true });
1796
1905
  }
1797
- async function handleAddInitScript(command, browser) {
1798
- const context = browser.getPage().context();
1906
+ export async function handleAddInitScript(command, browser) {
1907
+ const page = browser.getPage();
1908
+ const context = page.context();
1799
1909
  await context.addInitScript(command.script);
1800
- return successResponse(command.id, { added: true });
1910
+ const tips = [];
1911
+ try {
1912
+ await page.evaluate(command.script);
1913
+ }
1914
+ catch (e) {
1915
+ const msg = e instanceof Error ? e.message : String(e);
1916
+ tips.push(`Init script error on current page: ${msg}. Script will work on next navigation.`);
1917
+ }
1918
+ return successResponse(command.id, { added: true }, tips.length ? tips : undefined);
1801
1919
  }
1802
1920
  async function handleKeyDown(command, browser) {
1803
1921
  const page = browser.getPage();
@@ -1999,14 +2117,10 @@ async function handleRecorderStatus(command, browser) {
1999
2117
  async function handleRecorderReplay(command, browser) {
2000
2118
  const fs = await import('node:fs');
2001
2119
  const path = await import('node:path');
2002
- // Determine YAML file path
2003
2120
  let yamlPath = command.path;
2004
2121
  if (!yamlPath) {
2005
- // Use the most recent recording from temp directory
2006
2122
  const recorderDir = path.join(getAppDir(), 'tmp', 'recordings');
2007
- console.log('[Replay] Looking for recordings in:', recorderDir);
2008
2123
  if (!fs.existsSync(recorderDir)) {
2009
- console.log('[Replay] Directory does not exist');
2010
2124
  return errorResponse(command.id, 'No recordings found. Please record first.');
2011
2125
  }
2012
2126
  const files = fs
@@ -2017,21 +2131,39 @@ async function handleRecorderReplay(command, browser) {
2017
2131
  time: fs.statSync(path.join(recorderDir, f)).mtime.getTime(),
2018
2132
  }))
2019
2133
  .sort((a, b) => b.time - a.time);
2020
- console.log('[Replay] Found files:', files.length);
2021
2134
  if (files.length === 0) {
2022
2135
  return errorResponse(command.id, 'No recordings found. Please record first.');
2023
2136
  }
2024
2137
  yamlPath = path.join(recorderDir, files[0].name);
2025
- console.log('[Replay] Using file:', yamlPath);
2026
2138
  }
2027
- // Read YAML file
2028
2139
  if (!fs.existsSync(yamlPath)) {
2029
2140
  return errorResponse(command.id, `Recording file not found: ${yamlPath}`);
2030
2141
  }
2031
2142
  const yamlContent = fs.readFileSync(yamlPath, 'utf-8');
2032
- // Parse CLI commands from YAML
2033
- const lines = yamlContent.split('\n');
2034
2143
  const cliCommands = [];
2144
+ // Strategy 1: Parse structured steps and generate CLI commands
2145
+ const stepRegex = /^\s+-\s+(?:id:\s*.+)/;
2146
+ const lines = yamlContent.split('\n');
2147
+ let inSteps = false;
2148
+ const parsedSteps = {};
2149
+ for (const line of lines) {
2150
+ if (/^steps:/.test(line.trim())) {
2151
+ inSteps = true;
2152
+ continue;
2153
+ }
2154
+ if (inSteps && /^-\s+id:/.test(line.trim())) {
2155
+ const idMatch = line.match(/id:\s*(.+)/);
2156
+ if (idMatch)
2157
+ parsedSteps.currentId = idMatch[1].trim();
2158
+ }
2159
+ if (inSteps && /^\s+action:\s*(.+)/.test(line)) {
2160
+ // End of steps section when we hit a non-step line
2161
+ }
2162
+ if (inSteps && !/^\s/.test(line) && !/^$/.test(line) && !/^steps:/.test(line.trim())) {
2163
+ inSteps = false;
2164
+ }
2165
+ }
2166
+ // Strategy 2: Fall back to CLI Commands comment section
2035
2167
  let inCliSection = false;
2036
2168
  for (const line of lines) {
2037
2169
  if (line.includes('# CLI Commands')) {
@@ -2043,10 +2175,23 @@ async function handleRecorderReplay(command, browser) {
2043
2175
  cliCommands.push(line.trim());
2044
2176
  }
2045
2177
  }
2178
+ // Strategy 3: If no CLI section, generate from structured steps using browser's method
2046
2179
  if (cliCommands.length === 0) {
2047
2180
  return errorResponse(command.id, 'No CLI commands found in recording. Please re-record with the new version.');
2048
2181
  }
2049
- // Helper function to parse command line with proper quote handling
2182
+ // Filter out env-only lines (keep them for env setup but not as commands)
2183
+ const envLines = cliCommands.filter((l) => l.startsWith('AGENT_BROWSER_'));
2184
+ const cmdLines = cliCommands.filter((l) => l.startsWith('agent-browser '));
2185
+ // Set env vars from recording
2186
+ const originalEnv = {};
2187
+ for (const envLine of envLines) {
2188
+ const eqIdx = envLine.indexOf('=');
2189
+ if (eqIdx > 0) {
2190
+ const key = envLine.substring(0, eqIdx);
2191
+ originalEnv[key] = process.env[key];
2192
+ process.env[key] = envLine.substring(eqIdx + 1);
2193
+ }
2194
+ }
2050
2195
  function parseCommandLine(line) {
2051
2196
  const parts = [];
2052
2197
  let current = '';
@@ -2077,62 +2222,38 @@ async function handleRecorderReplay(command, browser) {
2077
2222
  }
2078
2223
  return parts;
2079
2224
  }
2080
- // Execute each command
2225
+ // Get current session for passthrough
2226
+ const currentSession = process.env.AGENT_BROWSER_SESSION || 'default';
2081
2227
  const results = [];
2082
- for (const cmdLine of cliCommands) {
2228
+ for (const cmdLine of cmdLines) {
2083
2229
  try {
2084
- // Parse command line with proper quote handling
2085
2230
  let parts = parseCommandLine(cmdLine);
2086
- const envVars = {};
2087
- // Extract environment variables (format: KEY=value)
2088
- while (parts.length > 0 && parts[0].includes('=')) {
2089
- const [key, ...valueParts] = parts.shift().split('=');
2090
- envVars[key] = valueParts.join('=');
2091
- }
2092
- // Remove 'agent-browser' prefix if present
2093
2231
  if (parts.length > 0 && parts[0] === 'agent-browser') {
2094
2232
  parts = parts.slice(1);
2095
2233
  }
2096
- // Skip empty commands
2097
2234
  if (parts.length === 0) {
2098
2235
  results.push({ command: cmdLine, success: true });
2099
2236
  continue;
2100
2237
  }
2101
- // Set environment variables temporarily
2102
- const originalEnv = {};
2103
- for (const [key, value] of Object.entries(envVars)) {
2104
- originalEnv[key] = process.env[key];
2105
- process.env[key] = value;
2106
- }
2107
- // Execute command using the existing executeCommand flow
2108
2238
  const { parseCommand } = await import('./cli/commands.js');
2109
2239
  const { parseFlags } = await import('./cli/flags.js');
2110
2240
  const flags = parseFlags([]);
2241
+ if (currentSession !== 'default') {
2242
+ flags.session = currentSession;
2243
+ }
2111
2244
  const parsedCmd = parseCommand(parts, flags);
2112
- // Check if recording is active, and temporarily disable
2113
2245
  const wasRecording = browser.isRecordingSession();
2114
2246
  if (wasRecording) {
2115
2247
  browser.pauseRecording();
2116
2248
  }
2117
- // Execute the command with a timeout to prevent hanging on invalid selectors
2118
2249
  const COMMAND_TIMEOUT_MS = 5000;
2119
2250
  const result = (await Promise.race([
2120
2251
  executeCommand(parsedCmd, browser),
2121
2252
  new Promise((_, reject) => setTimeout(() => reject(new Error(`Command timed out after ${COMMAND_TIMEOUT_MS}ms`)), COMMAND_TIMEOUT_MS)),
2122
2253
  ]));
2123
- // Restore recording state
2124
2254
  if (wasRecording) {
2125
2255
  browser.resumeRecording();
2126
2256
  }
2127
- // Restore environment variables
2128
- for (const [key, value] of Object.entries(originalEnv)) {
2129
- if (value === undefined) {
2130
- delete process.env[key];
2131
- }
2132
- else {
2133
- process.env[key] = value;
2134
- }
2135
- }
2136
2257
  results.push({ command: cmdLine, success: result.success });
2137
2258
  }
2138
2259
  catch (e) {
@@ -2140,29 +2261,37 @@ async function handleRecorderReplay(command, browser) {
2140
2261
  results.push({ command: cmdLine, success: false, error: errorMessage });
2141
2262
  }
2142
2263
  }
2264
+ // Restore env vars
2265
+ for (const [key, value] of Object.entries(originalEnv)) {
2266
+ if (value === undefined) {
2267
+ delete process.env[key];
2268
+ }
2269
+ else {
2270
+ process.env[key] = value;
2271
+ }
2272
+ }
2143
2273
  const successCount = results.filter((r) => r.success).length;
2144
2274
  const failCount = results.filter((r) => !r.success).length;
2145
2275
  return successResponse(command.id, {
2146
2276
  replayed: true,
2147
2277
  file: yamlPath,
2148
- totalCommands: cliCommands.length,
2278
+ totalCommands: cmdLines.length,
2149
2279
  successCount,
2150
2280
  failCount,
2151
- results: results.slice(0, 20), // Only return first 20 results
2281
+ results: results.slice(0, 20),
2152
2282
  });
2153
2283
  }
2154
2284
  async function handleViewer(command, _browser) {
2155
2285
  const instanceId = getInstanceId();
2156
- const port = parseInt(process.env.AGENT_BROWSER_STREAM_PORT || '5005', 10);
2157
2286
  return successResponse(command.id, {
2158
- url: `http://localhost:${port}/view?instanceId=${instanceId}`,
2159
- wsUrl: `ws://localhost:${port}?instanceId=${instanceId}`,
2160
- streamPort: port,
2287
+ url: getViewerUrl(instanceId),
2288
+ wsUrl: getViewerWsUrl(instanceId),
2289
+ streamPort: getViewerPort(),
2161
2290
  });
2162
2291
  }
2163
2292
  async function handleAsk(command, _browser) {
2164
2293
  const session = getSession();
2165
- const bridge = new MessageBridge();
2294
+ const bridge = new MessageBridge(getMessageBridgeUrl());
2166
2295
  try {
2167
2296
  const answer = await bridge.ask(command.question, session);
2168
2297
  return successResponse(command.id, { answer });
@@ -2174,9 +2303,10 @@ async function handleAsk(command, _browser) {
2174
2303
  }
2175
2304
  function handleConfig(command) {
2176
2305
  const humanConfig = getHumanConfigFromEnv();
2306
+ const rcConfig = loadConfig();
2177
2307
  const config = {
2178
2308
  session: process.env.AGENT_BROWSER_SESSION || 'default',
2179
- executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH || null,
2309
+ executablePath: getExecutablePath() || null,
2180
2310
  extensions: process.env.AGENT_BROWSER_EXTENSIONS || null,
2181
2311
  profile: process.env.AGENT_BROWSER_PROFILE || null,
2182
2312
  state: process.env.AGENT_BROWSER_STATE || null,
@@ -2187,24 +2317,34 @@ function handleConfig(command) {
2187
2317
  provider: process.env.AGENT_BROWSER_PROVIDER || null,
2188
2318
  allowFileAccess: process.env.AGENT_BROWSER_ALLOW_FILE_ACCESS === '1',
2189
2319
  iosDevice: process.env.AGENT_BROWSER_IOS_DEVICE || null,
2190
- streamPort: process.env.AGENT_BROWSER_STREAM_PORT || null,
2320
+ streamPort: getViewerPort(),
2191
2321
  headed: process.env.AGENT_BROWSER_HEADED === '1',
2192
2322
  human: humanConfig,
2193
2323
  };
2194
2324
  if (command.json) {
2195
- return successResponse(command.id, { config });
2325
+ return successResponse(command.id, { config, rc: rcConfig });
2196
2326
  }
2327
+ const viewerHost = getEffectiveValue('viewer.host');
2328
+ const bridgeUrl = getEffectiveValue('messageBridge.url');
2329
+ const msgProxy = getEffectiveValue('messageProxy.url');
2197
2330
  // Format human-readable output
2198
2331
  const lines = [
2199
2332
  'Agent Browser Configuration',
2200
2333
  '===========================',
2201
2334
  '',
2202
2335
  'Session & Browser:',
2203
- ` AGENT_BROWSER_SESSION ${config.session}`,
2204
- ` AGENT_BROWSER_EXECUTABLE_PATH ${config.executablePath || '(not set)'}`,
2336
+ ` executablePath ${config.executablePath || '(not set)'}`,
2205
2337
  ` AGENT_BROWSER_PROVIDER ${config.provider || '(not set)'}`,
2206
2338
  ` AGENT_BROWSER_HEADED ${config.headed ? 'true' : 'false (default)'}`,
2207
2339
  '',
2340
+ 'Viewer & Stream:',
2341
+ ` viewer.host ${viewerHost || '(not set, using http://localhost)'}`,
2342
+ ` viewer.port ${config.streamPort}`,
2343
+ '',
2344
+ 'Message Bridge (ask command):',
2345
+ ` messageBridge.url ${bridgeUrl || '(not set, using default)'}`,
2346
+ ` messageProxy.url ${msgProxy || '(not set)'}`,
2347
+ '',
2208
2348
  'Browser Options:',
2209
2349
  ` AGENT_BROWSER_PROFILE ${config.profile || '(not set)'}`,
2210
2350
  ` AGENT_BROWSER_EXTENSIONS ${config.extensions || '(not set)'}`,
@@ -2216,9 +2356,18 @@ function handleConfig(command) {
2216
2356
  'Human Mode (runtime):',
2217
2357
  ` AGENT_BROWSER_HUMAN ${humanConfig.enabled ? humanConfig.pathType + ' ✓' : '(disabled)'}`,
2218
2358
  '',
2219
- 'Note: Most settings only take effect at browser startup.',
2220
- 'Use "export AGENT_BROWSER_XXX=value" before starting.',
2359
+ `Persistent config: ~/.agent-browser/config.json`,
2360
+ 'Run "agent-browser config set <key> <value>" to persist settings.',
2361
+ 'Run "agent-browser config list" to see configurable keys.',
2221
2362
  ];
2222
2363
  return successResponse(command.id, { config, output: lines.join('\n') });
2223
2364
  }
2365
+ async function handleHistory(command, browser) {
2366
+ if (command.clear) {
2367
+ browser.clearHistory();
2368
+ return successResponse(command.id, { cleared: true });
2369
+ }
2370
+ const history = browser.getHistory(command.filter);
2371
+ return successResponse(command.id, { history });
2372
+ }
2224
2373
  //# sourceMappingURL=actions.js.map