@dyyz1993/agent-browser 0.11.5 → 0.12.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 (68) hide show
  1. package/bin/agent-browser-darwin-arm64 +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.map +1 -1
  6. package/dist/actions.js +117 -81
  7. package/dist/actions.js.map +1 -1
  8. package/dist/browser.d.ts +1 -0
  9. package/dist/browser.d.ts.map +1 -1
  10. package/dist/browser.js +24 -29
  11. package/dist/browser.js.map +1 -1
  12. package/dist/cli/commands.d.ts.map +1 -1
  13. package/dist/cli/commands.js +102 -3
  14. package/dist/cli/commands.js.map +1 -1
  15. package/dist/cli/connection.d.ts.map +1 -1
  16. package/dist/cli/connection.js +12 -29
  17. package/dist/cli/connection.js.map +1 -1
  18. package/dist/cli/help.d.ts.map +1 -1
  19. package/dist/cli/help.js +35 -25
  20. package/dist/cli/help.js.map +1 -1
  21. package/dist/cli/output.d.ts.map +1 -1
  22. package/dist/cli/output.js +3 -0
  23. package/dist/cli/output.js.map +1 -1
  24. package/dist/cli.js +117 -3
  25. package/dist/cli.js.map +1 -1
  26. package/dist/daemon.d.ts +18 -2
  27. package/dist/daemon.d.ts.map +1 -1
  28. package/dist/daemon.js +46 -32
  29. package/dist/daemon.js.map +1 -1
  30. package/dist/message-bridge.d.ts.map +1 -1
  31. package/dist/message-bridge.js +4 -1
  32. package/dist/message-bridge.js.map +1 -1
  33. package/dist/rc-config.d.ts +42 -0
  34. package/dist/rc-config.d.ts.map +1 -0
  35. package/dist/rc-config.js +170 -0
  36. package/dist/rc-config.js.map +1 -0
  37. package/dist/recorder/inject.js +30 -24
  38. package/package.json +1 -1
  39. package/scripts/check_goods_container.js +35 -0
  40. package/scripts/check_page_content.js +36 -0
  41. package/scripts/click_applause_rate.js +30 -0
  42. package/scripts/explore_jd_page.js +31 -0
  43. package/scripts/extract_all_jd_data.js +80 -0
  44. package/scripts/extract_jd_product_detail.js +62 -0
  45. package/scripts/extract_jd_products_correct_links.js +78 -0
  46. package/scripts/extract_jd_products_final.js +80 -0
  47. package/scripts/extract_jd_reviews.js +48 -0
  48. package/scripts/extract_jd_seafood_final.js +78 -0
  49. package/scripts/extract_multiple_products.js +77 -0
  50. package/scripts/extract_products_no_scroll.js +68 -0
  51. package/scripts/extract_products_simple.js +68 -0
  52. package/scripts/find_applause_rate.js +26 -0
  53. package/scripts/find_jd_links.js +28 -0
  54. package/scripts/find_main_content.js +20 -0
  55. package/scripts/find_product_cards.js +38 -0
  56. package/scripts/find_root_content.js +26 -0
  57. package/scripts/find_unique_products.js +55 -0
  58. package/scripts/generate-skill.cjs +303 -0
  59. package/scripts/get_jd_product_detail.js +16 -0
  60. package/scripts/get_jd_products.js +23 -0
  61. package/scripts/get_jd_seafood_products.js +44 -0
  62. package/scripts/get_product_details_from_images.js +54 -0
  63. package/scripts/verify-form.sh +67 -0
  64. package/scripts/verify-login.sh +65 -0
  65. package/scripts/verify-recording.sh +80 -0
  66. package/scripts/verify-upload.sh +41 -0
  67. package/skills/agent-browser/SKILL.md +135 -370
  68. package/bin/agent-browser-linux-x64 +0 -0
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
  /**
@@ -369,7 +377,7 @@ async function handleLaunch(command, browser) {
369
377
  return successResponse(command.id, {
370
378
  launched: true,
371
379
  instanceId,
372
- viewerUrl: `http://localhost:5005/view?instanceId=${instanceId}`,
380
+ viewerUrl: getViewerUrl(instanceId),
373
381
  });
374
382
  }
375
383
  async function handleNavigate(command, browser) {
@@ -895,17 +903,17 @@ async function handleFill(command, browser) {
895
903
  const diffResult = await performDiff(locator, command.diffScope, async () => {
896
904
  try {
897
905
  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);
906
+ if (!isRef) {
907
+ const page = browser.getPage();
908
+ if (page) {
909
+ await page.evaluate((selector) => {
910
+ const el = document.querySelector(selector);
911
+ if (el) {
912
+ el.dispatchEvent(new Event('input', { bubbles: true }));
913
+ el.dispatchEvent(new Event('change', { bubbles: true }));
914
+ }
915
+ }, command.selector);
916
+ }
909
917
  }
910
918
  }
911
919
  catch (error) {
@@ -1207,6 +1215,7 @@ async function handleRequests(command, browser) {
1207
1215
  return successResponse(command.id, { cleared: true });
1208
1216
  }
1209
1217
  // Start tracking if not already (with response capture if requested)
1218
+ const wasTracking = browser.trackingEnabled;
1210
1219
  browser.startRequestTracking(command.captureResponse);
1211
1220
  // If output directory is specified, save to directory
1212
1221
  if (command.output) {
@@ -1219,7 +1228,11 @@ async function handleRequests(command, browser) {
1219
1228
  });
1220
1229
  }
1221
1230
  const requests = browser.getRequests(command.filter, command.type);
1222
- return successResponse(command.id, { requests });
1231
+ const result = { requests };
1232
+ if (requests.length === 0 && !wasTracking) {
1233
+ result.hint = 'Request tracking just activated. Reload or navigate to capture requests.';
1234
+ }
1235
+ return successResponse(command.id, result);
1223
1236
  }
1224
1237
  async function handleDownload(command, browser) {
1225
1238
  const page = browser.getPage();
@@ -1659,7 +1672,7 @@ async function handleNth(command, browser) {
1659
1672
  const refLocator = browser.getLocatorFromRef(command.selector, command.inFrame);
1660
1673
  let locator;
1661
1674
  if (refLocator) {
1662
- locator = refLocator;
1675
+ locator = command.index === -1 ? refLocator.last() : refLocator.nth(command.index);
1663
1676
  }
1664
1677
  else {
1665
1678
  const frame = browser.getFrame(command.inFrame);
@@ -1999,14 +2012,10 @@ async function handleRecorderStatus(command, browser) {
1999
2012
  async function handleRecorderReplay(command, browser) {
2000
2013
  const fs = await import('node:fs');
2001
2014
  const path = await import('node:path');
2002
- // Determine YAML file path
2003
2015
  let yamlPath = command.path;
2004
2016
  if (!yamlPath) {
2005
- // Use the most recent recording from temp directory
2006
2017
  const recorderDir = path.join(getAppDir(), 'tmp', 'recordings');
2007
- console.log('[Replay] Looking for recordings in:', recorderDir);
2008
2018
  if (!fs.existsSync(recorderDir)) {
2009
- console.log('[Replay] Directory does not exist');
2010
2019
  return errorResponse(command.id, 'No recordings found. Please record first.');
2011
2020
  }
2012
2021
  const files = fs
@@ -2017,21 +2026,39 @@ async function handleRecorderReplay(command, browser) {
2017
2026
  time: fs.statSync(path.join(recorderDir, f)).mtime.getTime(),
2018
2027
  }))
2019
2028
  .sort((a, b) => b.time - a.time);
2020
- console.log('[Replay] Found files:', files.length);
2021
2029
  if (files.length === 0) {
2022
2030
  return errorResponse(command.id, 'No recordings found. Please record first.');
2023
2031
  }
2024
2032
  yamlPath = path.join(recorderDir, files[0].name);
2025
- console.log('[Replay] Using file:', yamlPath);
2026
2033
  }
2027
- // Read YAML file
2028
2034
  if (!fs.existsSync(yamlPath)) {
2029
2035
  return errorResponse(command.id, `Recording file not found: ${yamlPath}`);
2030
2036
  }
2031
2037
  const yamlContent = fs.readFileSync(yamlPath, 'utf-8');
2032
- // Parse CLI commands from YAML
2033
- const lines = yamlContent.split('\n');
2034
2038
  const cliCommands = [];
2039
+ // Strategy 1: Parse structured steps and generate CLI commands
2040
+ const stepRegex = /^\s+-\s+(?:id:\s*.+)/;
2041
+ const lines = yamlContent.split('\n');
2042
+ let inSteps = false;
2043
+ const parsedSteps = {};
2044
+ for (const line of lines) {
2045
+ if (/^steps:/.test(line.trim())) {
2046
+ inSteps = true;
2047
+ continue;
2048
+ }
2049
+ if (inSteps && /^-\s+id:/.test(line.trim())) {
2050
+ const idMatch = line.match(/id:\s*(.+)/);
2051
+ if (idMatch)
2052
+ parsedSteps.currentId = idMatch[1].trim();
2053
+ }
2054
+ if (inSteps && /^\s+action:\s*(.+)/.test(line)) {
2055
+ // End of steps section when we hit a non-step line
2056
+ }
2057
+ if (inSteps && !/^\s/.test(line) && !/^$/.test(line) && !/^steps:/.test(line.trim())) {
2058
+ inSteps = false;
2059
+ }
2060
+ }
2061
+ // Strategy 2: Fall back to CLI Commands comment section
2035
2062
  let inCliSection = false;
2036
2063
  for (const line of lines) {
2037
2064
  if (line.includes('# CLI Commands')) {
@@ -2043,10 +2070,23 @@ async function handleRecorderReplay(command, browser) {
2043
2070
  cliCommands.push(line.trim());
2044
2071
  }
2045
2072
  }
2073
+ // Strategy 3: If no CLI section, generate from structured steps using browser's method
2046
2074
  if (cliCommands.length === 0) {
2047
2075
  return errorResponse(command.id, 'No CLI commands found in recording. Please re-record with the new version.');
2048
2076
  }
2049
- // Helper function to parse command line with proper quote handling
2077
+ // Filter out env-only lines (keep them for env setup but not as commands)
2078
+ const envLines = cliCommands.filter((l) => l.startsWith('AGENT_BROWSER_'));
2079
+ const cmdLines = cliCommands.filter((l) => l.startsWith('agent-browser '));
2080
+ // Set env vars from recording
2081
+ const originalEnv = {};
2082
+ for (const envLine of envLines) {
2083
+ const eqIdx = envLine.indexOf('=');
2084
+ if (eqIdx > 0) {
2085
+ const key = envLine.substring(0, eqIdx);
2086
+ originalEnv[key] = process.env[key];
2087
+ process.env[key] = envLine.substring(eqIdx + 1);
2088
+ }
2089
+ }
2050
2090
  function parseCommandLine(line) {
2051
2091
  const parts = [];
2052
2092
  let current = '';
@@ -2077,62 +2117,38 @@ async function handleRecorderReplay(command, browser) {
2077
2117
  }
2078
2118
  return parts;
2079
2119
  }
2080
- // Execute each command
2120
+ // Get current session for passthrough
2121
+ const currentSession = process.env.AGENT_BROWSER_SESSION || 'default';
2081
2122
  const results = [];
2082
- for (const cmdLine of cliCommands) {
2123
+ for (const cmdLine of cmdLines) {
2083
2124
  try {
2084
- // Parse command line with proper quote handling
2085
2125
  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
2126
  if (parts.length > 0 && parts[0] === 'agent-browser') {
2094
2127
  parts = parts.slice(1);
2095
2128
  }
2096
- // Skip empty commands
2097
2129
  if (parts.length === 0) {
2098
2130
  results.push({ command: cmdLine, success: true });
2099
2131
  continue;
2100
2132
  }
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
2133
  const { parseCommand } = await import('./cli/commands.js');
2109
2134
  const { parseFlags } = await import('./cli/flags.js');
2110
2135
  const flags = parseFlags([]);
2136
+ if (currentSession !== 'default') {
2137
+ flags.session = currentSession;
2138
+ }
2111
2139
  const parsedCmd = parseCommand(parts, flags);
2112
- // Check if recording is active, and temporarily disable
2113
2140
  const wasRecording = browser.isRecordingSession();
2114
2141
  if (wasRecording) {
2115
2142
  browser.pauseRecording();
2116
2143
  }
2117
- // Execute the command with a timeout to prevent hanging on invalid selectors
2118
2144
  const COMMAND_TIMEOUT_MS = 5000;
2119
2145
  const result = (await Promise.race([
2120
2146
  executeCommand(parsedCmd, browser),
2121
2147
  new Promise((_, reject) => setTimeout(() => reject(new Error(`Command timed out after ${COMMAND_TIMEOUT_MS}ms`)), COMMAND_TIMEOUT_MS)),
2122
2148
  ]));
2123
- // Restore recording state
2124
2149
  if (wasRecording) {
2125
2150
  browser.resumeRecording();
2126
2151
  }
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
2152
  results.push({ command: cmdLine, success: result.success });
2137
2153
  }
2138
2154
  catch (e) {
@@ -2140,29 +2156,37 @@ async function handleRecorderReplay(command, browser) {
2140
2156
  results.push({ command: cmdLine, success: false, error: errorMessage });
2141
2157
  }
2142
2158
  }
2159
+ // Restore env vars
2160
+ for (const [key, value] of Object.entries(originalEnv)) {
2161
+ if (value === undefined) {
2162
+ delete process.env[key];
2163
+ }
2164
+ else {
2165
+ process.env[key] = value;
2166
+ }
2167
+ }
2143
2168
  const successCount = results.filter((r) => r.success).length;
2144
2169
  const failCount = results.filter((r) => !r.success).length;
2145
2170
  return successResponse(command.id, {
2146
2171
  replayed: true,
2147
2172
  file: yamlPath,
2148
- totalCommands: cliCommands.length,
2173
+ totalCommands: cmdLines.length,
2149
2174
  successCount,
2150
2175
  failCount,
2151
- results: results.slice(0, 20), // Only return first 20 results
2176
+ results: results.slice(0, 20),
2152
2177
  });
2153
2178
  }
2154
2179
  async function handleViewer(command, _browser) {
2155
2180
  const instanceId = getInstanceId();
2156
- const port = parseInt(process.env.AGENT_BROWSER_STREAM_PORT || '5005', 10);
2157
2181
  return successResponse(command.id, {
2158
- url: `http://localhost:${port}/view?instanceId=${instanceId}`,
2159
- wsUrl: `ws://localhost:${port}?instanceId=${instanceId}`,
2160
- streamPort: port,
2182
+ url: getViewerUrl(instanceId),
2183
+ wsUrl: getViewerWsUrl(instanceId),
2184
+ streamPort: getViewerPort(),
2161
2185
  });
2162
2186
  }
2163
2187
  async function handleAsk(command, _browser) {
2164
2188
  const session = getSession();
2165
- const bridge = new MessageBridge();
2189
+ const bridge = new MessageBridge(getMessageBridgeUrl());
2166
2190
  try {
2167
2191
  const answer = await bridge.ask(command.question, session);
2168
2192
  return successResponse(command.id, { answer });
@@ -2174,9 +2198,10 @@ async function handleAsk(command, _browser) {
2174
2198
  }
2175
2199
  function handleConfig(command) {
2176
2200
  const humanConfig = getHumanConfigFromEnv();
2201
+ const rcConfig = loadConfig();
2177
2202
  const config = {
2178
2203
  session: process.env.AGENT_BROWSER_SESSION || 'default',
2179
- executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH || null,
2204
+ executablePath: getExecutablePath() || null,
2180
2205
  extensions: process.env.AGENT_BROWSER_EXTENSIONS || null,
2181
2206
  profile: process.env.AGENT_BROWSER_PROFILE || null,
2182
2207
  state: process.env.AGENT_BROWSER_STATE || null,
@@ -2187,24 +2212,34 @@ function handleConfig(command) {
2187
2212
  provider: process.env.AGENT_BROWSER_PROVIDER || null,
2188
2213
  allowFileAccess: process.env.AGENT_BROWSER_ALLOW_FILE_ACCESS === '1',
2189
2214
  iosDevice: process.env.AGENT_BROWSER_IOS_DEVICE || null,
2190
- streamPort: process.env.AGENT_BROWSER_STREAM_PORT || null,
2215
+ streamPort: getViewerPort(),
2191
2216
  headed: process.env.AGENT_BROWSER_HEADED === '1',
2192
2217
  human: humanConfig,
2193
2218
  };
2194
2219
  if (command.json) {
2195
- return successResponse(command.id, { config });
2220
+ return successResponse(command.id, { config, rc: rcConfig });
2196
2221
  }
2222
+ const viewerHost = getEffectiveValue('viewer.host');
2223
+ const bridgeUrl = getEffectiveValue('messageBridge.url');
2224
+ const msgProxy = getEffectiveValue('messageProxy.url');
2197
2225
  // Format human-readable output
2198
2226
  const lines = [
2199
2227
  'Agent Browser Configuration',
2200
2228
  '===========================',
2201
2229
  '',
2202
2230
  'Session & Browser:',
2203
- ` AGENT_BROWSER_SESSION ${config.session}`,
2204
- ` AGENT_BROWSER_EXECUTABLE_PATH ${config.executablePath || '(not set)'}`,
2231
+ ` executablePath ${config.executablePath || '(not set)'}`,
2205
2232
  ` AGENT_BROWSER_PROVIDER ${config.provider || '(not set)'}`,
2206
2233
  ` AGENT_BROWSER_HEADED ${config.headed ? 'true' : 'false (default)'}`,
2207
2234
  '',
2235
+ 'Viewer & Stream:',
2236
+ ` viewer.host ${viewerHost || '(not set, using http://localhost)'}`,
2237
+ ` viewer.port ${config.streamPort}`,
2238
+ '',
2239
+ 'Message Bridge (ask command):',
2240
+ ` messageBridge.url ${bridgeUrl || '(not set, using default)'}`,
2241
+ ` messageProxy.url ${msgProxy || '(not set)'}`,
2242
+ '',
2208
2243
  'Browser Options:',
2209
2244
  ` AGENT_BROWSER_PROFILE ${config.profile || '(not set)'}`,
2210
2245
  ` AGENT_BROWSER_EXTENSIONS ${config.extensions || '(not set)'}`,
@@ -2216,8 +2251,9 @@ function handleConfig(command) {
2216
2251
  'Human Mode (runtime):',
2217
2252
  ` AGENT_BROWSER_HUMAN ${humanConfig.enabled ? humanConfig.pathType + ' ✓' : '(disabled)'}`,
2218
2253
  '',
2219
- 'Note: Most settings only take effect at browser startup.',
2220
- 'Use "export AGENT_BROWSER_XXX=value" before starting.',
2254
+ `Persistent config: ~/.agent-browser/config.json`,
2255
+ 'Run "agent-browser config set <key> <value>" to persist settings.',
2256
+ 'Run "agent-browser config list" to see configurable keys.',
2221
2257
  ];
2222
2258
  return successResponse(command.id, { config, output: lines.join('\n') });
2223
2259
  }