explorbot 0.2.2 → 0.2.4

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 (214) hide show
  1. package/README.md +1 -1
  2. package/bin/explorbot-cli.ts +59 -38
  3. package/boat/api-tester/src/apibot.ts +4 -2
  4. package/boat/api-tester/src/cli.ts +2 -2
  5. package/boat/api-tester/src/config.ts +43 -10
  6. package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
  7. package/boat/doc-collector/src/ai/documentarian.ts +61 -31
  8. package/boat/doc-collector/src/cli.ts +1 -0
  9. package/boat/doc-collector/src/config.ts +4 -2
  10. package/boat/doc-collector/src/docs-renderer.ts +18 -4
  11. package/boat/doc-collector/src/state-diagram.ts +61 -14
  12. package/boat/prima/bin/prima-cli.ts +5 -0
  13. package/boat/prima/package.json +16 -0
  14. package/boat/prima/src/activity-line.ts +33 -0
  15. package/boat/prima/src/cli.ts +251 -0
  16. package/boat/prima/src/envelope.ts +169 -0
  17. package/boat/prima/src/prima.ts +1124 -0
  18. package/boat/prima/src/pw-parser.ts +27 -0
  19. package/boat/prima/src/pw-registry.ts +74 -0
  20. package/boat/prima/src/session-log.ts +126 -0
  21. package/dist/bin/explorbot-cli.js +51 -32
  22. package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
  23. package/dist/boat/api-tester/src/apibot.js +3 -2
  24. package/dist/boat/api-tester/src/cli.js +2 -2
  25. package/dist/boat/api-tester/src/config.js +40 -10
  26. package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
  27. package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
  28. package/dist/boat/doc-collector/src/cli.js +1 -0
  29. package/dist/boat/doc-collector/src/config.js +4 -2
  30. package/dist/boat/doc-collector/src/docs-renderer.js +17 -3
  31. package/dist/boat/doc-collector/src/state-diagram.js +57 -13
  32. package/dist/boat/prima/bin/prima-cli.js +4 -0
  33. package/dist/boat/prima/src/activity-line.js +30 -0
  34. package/dist/boat/prima/src/cli.js +220 -0
  35. package/dist/boat/prima/src/envelope.js +142 -0
  36. package/dist/boat/prima/src/prima.js +1031 -0
  37. package/dist/boat/prima/src/pw-parser.js +30 -0
  38. package/dist/boat/prima/src/pw-registry.js +65 -0
  39. package/dist/boat/prima/src/session-log.js +108 -0
  40. package/dist/models.json +3 -0
  41. package/dist/package.json +8 -3
  42. package/dist/rules/navigator/verification-actions.md +20 -0
  43. package/dist/src/action-result.d.ts +7 -0
  44. package/dist/src/action-result.js +4 -0
  45. package/dist/src/action.d.ts +7 -2
  46. package/dist/src/action.js +41 -6
  47. package/dist/src/ai/captain/mixin.js +3 -4
  48. package/dist/src/ai/captain/web-mode.js +7 -4
  49. package/dist/src/ai/captain.js +2 -0
  50. package/dist/src/ai/navigator.d.ts +11 -0
  51. package/dist/src/ai/navigator.js +21 -12
  52. package/dist/src/ai/pilot.d.ts +4 -0
  53. package/dist/src/ai/pilot.js +48 -2
  54. package/dist/src/ai/planner.d.ts +1 -0
  55. package/dist/src/ai/planner.js +6 -0
  56. package/dist/src/ai/provider.js +2 -2
  57. package/dist/src/ai/rerunner.js +2 -1
  58. package/dist/src/ai/researcher.js +2 -2
  59. package/dist/src/ai/rules.js +17 -10
  60. package/dist/src/ai/task-agent.js +5 -2
  61. package/dist/src/ai/tester.d.ts +1 -0
  62. package/dist/src/ai/tester.js +33 -20
  63. package/dist/src/ai/tools.d.ts +14 -0
  64. package/dist/src/ai/tools.js +113 -36
  65. package/dist/src/application-spec-contract.d.ts +8 -0
  66. package/dist/src/application-spec-contract.js +8 -0
  67. package/dist/src/application-spec.d.ts +15 -0
  68. package/dist/src/application-spec.js +71 -0
  69. package/dist/src/browser-server.d.ts +12 -6
  70. package/dist/src/browser-server.js +74 -19
  71. package/dist/src/commands/clean-command.js +2 -7
  72. package/dist/src/commands/init-command.d.ts +5 -0
  73. package/dist/src/commands/init-command.js +119 -1
  74. package/dist/src/commands/navigate-command.js +1 -1
  75. package/dist/src/commands/research-command.js +1 -1
  76. package/dist/src/commands/sites-command.d.ts +6 -0
  77. package/dist/src/commands/sites-command.js +23 -0
  78. package/dist/src/components/InitWizard.d.ts +10 -0
  79. package/dist/src/components/InitWizard.js +133 -0
  80. package/dist/src/components/InputReadline.d.ts +1 -0
  81. package/dist/src/components/InputReadline.js +7 -4
  82. package/dist/src/config.d.ts +25 -5
  83. package/dist/src/config.js +158 -40
  84. package/dist/src/execution-controller.d.ts +2 -0
  85. package/dist/src/execution-controller.js +6 -0
  86. package/dist/src/explorbot.d.ts +11 -1
  87. package/dist/src/explorbot.js +27 -6
  88. package/dist/src/explorer.d.ts +4 -1
  89. package/dist/src/explorer.js +42 -9
  90. package/dist/src/global-config.d.ts +22 -0
  91. package/dist/src/global-config.js +117 -0
  92. package/dist/src/knowledge-tracker.d.ts +5 -1
  93. package/dist/src/knowledge-tracker.js +14 -1
  94. package/dist/src/playwright-recorder.js +30 -0
  95. package/dist/src/remote.d.ts +54 -0
  96. package/dist/src/remote.js +229 -0
  97. package/dist/src/reporter.d.ts +1 -0
  98. package/dist/src/reporter.js +7 -1
  99. package/dist/src/state-manager.d.ts +2 -1
  100. package/dist/src/stats.d.ts +1 -0
  101. package/dist/src/stats.js +1 -0
  102. package/dist/src/test-plan.d.ts +2 -0
  103. package/dist/src/test-plan.js +7 -0
  104. package/dist/src/utils/aria.d.ts +2 -8
  105. package/dist/src/utils/aria.js +69 -40
  106. package/dist/src/utils/cli-name.js +6 -2
  107. package/dist/src/utils/html.js +1 -0
  108. package/dist/src/utils/logger.d.ts +6 -0
  109. package/dist/src/utils/logger.js +24 -0
  110. package/dist/src/utils/page-readiness.js +18 -1
  111. package/dist/src/utils/test-files.js +1 -2
  112. package/dist/src/utils/url-matcher.d.ts +1 -0
  113. package/dist/src/utils/url-matcher.js +12 -0
  114. package/dist/src/utils/web-element.d.ts +2 -0
  115. package/dist/src/utils/web-element.js +8 -0
  116. package/dist/src/utils/web-sandbox.d.ts +1 -1
  117. package/dist/src/utils/web-sandbox.js +2 -3
  118. package/docs/api-testing/basics.md +90 -0
  119. package/docs/api-testing/planning.md +57 -0
  120. package/docs/api-testing/running-tests.md +55 -0
  121. package/docs/assets/cloud-report.png +0 -0
  122. package/docs/assets/html-report.png +0 -0
  123. package/docs/assets/langfuse-trace.png +0 -0
  124. package/docs/assets/successful-explore-run.png +0 -0
  125. package/docs/basics/getting-started.md +140 -0
  126. package/docs/basics/prerequisites.md +63 -0
  127. package/docs/basics/providers.md +362 -0
  128. package/docs/basics/running.md +78 -0
  129. package/docs/contributing/ai-integration-tests.md +57 -0
  130. package/docs/contributing/contributing.md +90 -0
  131. package/docs/contributing/demo-videos.md +36 -0
  132. package/docs/contributing/npm-package.md +138 -0
  133. package/docs/contributing/observability.md +227 -0
  134. package/docs/contributing/regression-tests.md +103 -0
  135. package/docs/contributing/testing.md +95 -0
  136. package/docs/doc-collection/basics.md +128 -0
  137. package/docs/doc-collection/crawling.md +67 -0
  138. package/docs/doc-collection/interactive-mode.md +99 -0
  139. package/docs/index.json +86 -0
  140. package/docs/reference/commands.md +994 -0
  141. package/docs/reference/configuration.md +569 -0
  142. package/docs/reference/scripting.md +303 -0
  143. package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
  144. package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
  145. package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
  146. package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
  147. package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
  148. package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
  149. package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
  150. package/docs/web-testing/agents.md +158 -0
  151. package/docs/web-testing/automated-tests.md +134 -0
  152. package/docs/web-testing/basics.md +91 -0
  153. package/docs/web-testing/customization.md +131 -0
  154. package/docs/web-testing/hooks.md +238 -0
  155. package/docs/web-testing/page-interaction.md +84 -0
  156. package/docs/web-testing/planner.md +122 -0
  157. package/docs/web-testing/rerun.md +164 -0
  158. package/docs/web-testing/researcher.md +380 -0
  159. package/docs/workflow/agentic-usage.md +233 -0
  160. package/docs/workflow/application-spec.md +73 -0
  161. package/docs/workflow/ci.md +202 -0
  162. package/docs/workflow/knowledge.md +310 -0
  163. package/docs/workflow/planning-styles.md +67 -0
  164. package/docs/workflow/reporting.md +133 -0
  165. package/docs/workflow/test-plans.md +90 -0
  166. package/models.json +3 -0
  167. package/package.json +8 -3
  168. package/rules/navigator/verification-actions.md +20 -0
  169. package/src/action-result.ts +11 -0
  170. package/src/action.ts +47 -7
  171. package/src/ai/captain/mixin.ts +3 -3
  172. package/src/ai/captain/web-mode.ts +7 -4
  173. package/src/ai/captain.ts +3 -0
  174. package/src/ai/navigator.ts +26 -15
  175. package/src/ai/pilot.ts +53 -2
  176. package/src/ai/planner.ts +7 -0
  177. package/src/ai/provider.ts +2 -4
  178. package/src/ai/rerunner.ts +2 -1
  179. package/src/ai/researcher.ts +2 -2
  180. package/src/ai/rules.ts +17 -10
  181. package/src/ai/task-agent.ts +4 -2
  182. package/src/ai/tester.ts +32 -18
  183. package/src/ai/tools.ts +132 -36
  184. package/src/application-spec-contract.ts +10 -0
  185. package/src/application-spec.ts +87 -0
  186. package/src/browser-server.ts +74 -19
  187. package/src/commands/clean-command.ts +1 -6
  188. package/src/commands/init-command.ts +146 -1
  189. package/src/commands/navigate-command.ts +1 -1
  190. package/src/commands/research-command.ts +1 -1
  191. package/src/commands/sites-command.ts +27 -0
  192. package/src/components/InitWizard.tsx +166 -0
  193. package/src/components/InputReadline.tsx +8 -4
  194. package/src/config.ts +177 -43
  195. package/src/execution-controller.ts +8 -0
  196. package/src/explorbot.ts +34 -7
  197. package/src/explorer.ts +47 -9
  198. package/src/global-config.ts +148 -0
  199. package/src/knowledge-tracker.ts +17 -1
  200. package/src/playwright-recorder.ts +23 -0
  201. package/src/remote.ts +238 -0
  202. package/src/reporter.ts +7 -1
  203. package/src/state-manager.ts +2 -1
  204. package/src/stats.ts +1 -0
  205. package/src/test-plan.ts +9 -0
  206. package/src/utils/aria.ts +65 -45
  207. package/src/utils/cli-name.ts +5 -2
  208. package/src/utils/html.ts +1 -0
  209. package/src/utils/logger.ts +24 -1
  210. package/src/utils/page-readiness.ts +24 -1
  211. package/src/utils/test-files.ts +1 -2
  212. package/src/utils/url-matcher.ts +13 -0
  213. package/src/utils/web-element.ts +9 -0
  214. package/src/utils/web-sandbox.ts +3 -4
package/README.md CHANGED
@@ -138,7 +138,7 @@ EXPLORBOT_KNOWLEDGE="Log in as admin@example.com / secret123" \
138
138
  npx explorbot explore /admin/users --max-tests 3
139
139
  ```
140
140
 
141
- Output lands in a temp directory and nothing is written to your project. See [Agentic Usage](docs/workflow/agentic-usage.md).
141
+ Output lands in a per-host site directory, `~/.explorbot/sites/<host>/`, so runs against the same app collect in one place and nothing is written to your project. Set `EXPLORBOT_EPHEMERAL=1` to keep nothing between runs. See [Agentic Usage](docs/workflow/agentic-usage.md).
142
142
 
143
143
  ## Teaching Explorbot
144
144
 
@@ -11,6 +11,7 @@ import { App } from '../src/components/App.js';
11
11
  import { StatusPane } from '../src/components/StatusPane.js';
12
12
  import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
13
13
  import { ExplorBot, type ExplorBotOptions } from '../src/explorbot.js';
14
+ import { remote } from '../src/remote.js';
14
15
  import { Stats } from '../src/stats.js';
15
16
  import { Plan } from '../src/test-plan.js';
16
17
  import { getCliName } from '../src/utils/cli-name.ts';
@@ -26,8 +27,9 @@ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../p
26
27
  const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version as string;
27
28
 
28
29
  program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
30
+ remote.registerOption(program);
29
31
 
30
- if (!process.env.EXPLORBOT_NO_BANNER) {
32
+ if (!process.env.EXPLORBOT_NO_BANNER && !process.argv.includes('prima')) {
31
33
  console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
32
34
  }
33
35
 
@@ -40,6 +42,7 @@ interface CLIOptions {
40
42
  headless?: boolean;
41
43
  incognito?: boolean;
42
44
  session?: string | boolean;
45
+ spec?: string;
43
46
  }
44
47
 
45
48
  function buildExplorBotOptions(from: string | undefined, options: CLIOptions): ExplorBotOptions {
@@ -52,6 +55,7 @@ function buildExplorBotOptions(from: string | undefined, options: CLIOptions): E
52
55
  headless: options.headless,
53
56
  incognito: options.incognito,
54
57
  session: options.session,
58
+ applicationSpec: options.spec,
55
59
  } as ExplorBotOptions;
56
60
  }
57
61
 
@@ -64,6 +68,7 @@ function addCommonOptions(cmd: Command): Command {
64
68
  .option('-s, --show', 'Show browser window')
65
69
  .option('--headless', 'Run browser in headless mode')
66
70
  .option('--incognito', 'Run without recording experiences')
71
+ .option('--spec <path>', 'Use a Docbot application spec directory or index.md')
67
72
  .option('--session [file]', 'Save/restore browser session from file');
68
73
  }
69
74
 
@@ -96,6 +101,10 @@ async function startTUI(explorBot: ExplorBot): Promise<void> {
96
101
  }
97
102
 
98
103
  async function showStatsAndExit(code: number): Promise<never> {
104
+ if (remote.isAttached()) {
105
+ await remote.close(code);
106
+ process.exit(code);
107
+ }
99
108
  if (Stats.hasActivity()) {
100
109
  await new Promise<void>((resolve) => {
101
110
  const { unmount } = render(
@@ -386,6 +395,14 @@ program
386
395
  }
387
396
  });
388
397
 
398
+ program
399
+ .command('sites')
400
+ .description('List sites registered in the global installation')
401
+ .action(async () => {
402
+ const { SitesCommand } = await import('../src/commands/sites-command.js');
403
+ await new SitesCommand(new ExplorBot()).execute('');
404
+ });
405
+
389
406
  addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests with AI auto-healing')).action(async (filename, index, options) => {
390
407
  try {
391
408
  const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
@@ -421,17 +438,28 @@ addCommonOptions(
421
438
 
422
439
  program
423
440
  .command('init')
424
- .description('Initialize a new project with configuration')
425
- .option('-c, --config-path <path>', 'Path for the config file', './explorbot.config.js')
441
+ .description('Initialize configuration for a project or for this machine')
442
+ .option('-c, --config-path <path>', 'Path for the config file')
426
443
  .option('-f, --force', 'Overwrite existing config file')
427
444
  .option('-p, --path <path>', 'Working directory for initialization')
445
+ .option('-g, --global', 'Configure explorbot in ~/.explorbot to run from anywhere')
446
+ .option('--provider <name>', `AI provider for the global config: ${Object.keys(PROVIDERS).join(', ')}`)
447
+ .option('--api-key <key>', 'API key stored in ~/.explorbot/.env')
428
448
  .action(async (options) => {
429
- const { runInitCommand } = await import('../src/commands/init-command.js');
430
- runInitCommand({
431
- configPath: options.configPath,
432
- force: options.force,
433
- path: options.path,
434
- });
449
+ try {
450
+ const { runInit } = await import('../src/commands/init-command.js');
451
+ await runInit({
452
+ configPath: options.configPath,
453
+ force: options.force,
454
+ path: options.path,
455
+ global: options.global,
456
+ provider: options.provider,
457
+ apiKey: options.apiKey,
458
+ });
459
+ } catch (error) {
460
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
461
+ process.exit(1);
462
+ }
435
463
  });
436
464
 
437
465
  program
@@ -694,7 +722,7 @@ program
694
722
  const explorBot = new ExplorBot(mainOptions);
695
723
  await explorBot.start();
696
724
 
697
- await explorBot.agentNavigator().visit(url);
725
+ await explorBot.visit(url);
698
726
 
699
727
  const { ContextCommand } = await import('../src/commands/context-command.js');
700
728
  const argParts: string[] = [];
@@ -716,7 +744,7 @@ addCommonOptions(program.command('shell <url> <command>').description('Execute a
716
744
  try {
717
745
  const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
718
746
  await explorBot.start();
719
- await explorBot.agentNavigator().visit(url);
747
+ await explorBot.visit(url);
720
748
 
721
749
  const action = explorBot.getExplorer().action();
722
750
  await action.execute(command);
@@ -740,10 +768,11 @@ browserCmd
740
768
  .description('Launch a persistent browser server')
741
769
  .option('-s, --show', 'Launch browser in headed mode (visible window)')
742
770
  .option('--headless', 'Launch browser in headless mode')
771
+ .option('--instance <name>', 'Named browser instance (lowercase letters, digits, dashes)')
743
772
  .option('-c, --config <path>', 'Path to configuration file')
744
773
  .option('-p, --path <path>', 'Working directory path')
745
774
  .action(async (options) => {
746
- const { launchServer, removeEndpointFile } = await import('../src/browser-server.js');
775
+ const { launchServer, removeEndpointFile, keepServerRunning } = await import('../src/browser-server.js');
747
776
  await ConfigParser.getInstance().loadConfig({
748
777
  config: options.config,
749
778
  path: options.path,
@@ -754,55 +783,45 @@ browserCmd
754
783
  if (options.show !== undefined) show = true;
755
784
  if (options.headless !== undefined) show = false;
756
785
 
757
- const server = await launchServer({
758
- browser: config.playwright.browser,
759
- show,
760
- });
761
-
762
- console.log('Browser server is running. Press Ctrl+C to stop.');
763
-
764
- const cleanup = () => {
765
- console.log('\nStopping browser server...');
766
- server.close();
767
- removeEndpointFile();
768
- process.exit(0);
769
- };
786
+ const server = await launchServer(
787
+ {
788
+ browser: config.playwright.browser,
789
+ show,
790
+ },
791
+ options.instance
792
+ );
770
793
 
771
- process.on('SIGINT', cleanup);
772
- process.on('SIGTERM', cleanup);
794
+ await keepServerRunning(async () => {
795
+ await server.close();
796
+ removeEndpointFile(options.instance);
797
+ });
773
798
  });
774
799
 
775
800
  browserCmd
776
801
  .command('stop')
777
802
  .description('Stop a running browser server')
803
+ .option('--instance <name>', 'Named browser instance (lowercase letters, digits, dashes)')
778
804
  .option('-c, --config <path>', 'Path to configuration file')
779
805
  .option('-p, --path <path>', 'Working directory path')
780
806
  .action(async (options) => {
781
- const { getAliveEndpoint, removeEndpointFile } = await import('../src/browser-server.js');
807
+ const { stopServer } = await import('../src/browser-server.js');
782
808
  await ConfigParser.getInstance().loadConfig({
783
809
  config: options.config,
784
810
  path: options.path,
785
811
  });
786
812
 
787
- const endpoint = await getAliveEndpoint();
788
- if (!endpoint) {
813
+ if (!(await stopServer(options.instance))) {
789
814
  console.log('No running browser server found.');
790
815
  process.exit(0);
791
816
  }
792
817
 
793
- try {
794
- const { chromium } = await import('playwright-core');
795
- const browser = await chromium.connect(endpoint, { timeout: 3000 });
796
- await browser.close();
797
- } catch {}
798
-
799
- removeEndpointFile();
800
818
  console.log('Browser server stopped.');
801
819
  });
802
820
 
803
821
  browserCmd
804
822
  .command('status')
805
823
  .description('Check if a browser server is running')
824
+ .option('--instance <name>', 'Named browser instance (lowercase letters, digits, dashes)')
806
825
  .option('-c, --config <path>', 'Path to configuration file')
807
826
  .option('-p, --path <path>', 'Working directory path')
808
827
  .action(async (options) => {
@@ -812,7 +831,7 @@ browserCmd
812
831
  path: options.path,
813
832
  });
814
833
 
815
- const endpoint = await getAliveEndpoint();
834
+ const endpoint = await getAliveEndpoint(options.instance);
816
835
  if (endpoint) {
817
836
  console.log(`Browser server is running at: ${endpoint}`);
818
837
  } else {
@@ -871,8 +890,10 @@ program
871
890
 
872
891
  import { createApiCommands } from '../boat/api-tester/src/cli.ts';
873
892
  import { createDocsCommands } from '../boat/doc-collector/src/cli.ts';
893
+ import { createPrimaCommands } from '../boat/prima/src/cli.ts';
874
894
  program.addCommand(createApiCommands('api'));
875
895
  program.addCommand(createDocsCommands('docs'));
896
+ program.addCommand(createPrimaCommands('prima'));
876
897
 
877
898
  const envHelp = () => {
878
899
  const width = Math.max(...EXPLORBOT_ENV_VARS.map((v) => v.name.length));
@@ -33,7 +33,7 @@ export class ApiBot {
33
33
  }
34
34
 
35
35
  async start(): Promise<void> {
36
- this.config = await this.configParser.loadConfig({ config: this.options.config, path: this.options.path });
36
+ this.config = await this.configParser.loadConfig({ config: this.options.config, path: this.options.path, endpoint: this.options.endpoint });
37
37
  this.provider = new AIProvider(this.config.ai);
38
38
  await this.provider.validateConnection();
39
39
 
@@ -101,12 +101,13 @@ export class ApiBot {
101
101
  return (this.agents.curler ||= this.createAgent(({ ai, apiClient, requestState }) => new Curler(ai, apiClient, requestState, this.reporter)));
102
102
  }
103
103
 
104
- async plan(endpoint: string, opts: { style?: string; fresh?: boolean } = {}): Promise<Plan> {
104
+ async plan(target: string, opts: { style?: string; fresh?: boolean } = {}): Promise<Plan> {
105
105
  if (opts.fresh) {
106
106
  this.currentPlan = undefined;
107
107
  this.agents.chief = undefined;
108
108
  }
109
109
 
110
+ const endpoint = this.configParser.resolveEndpointPath(target);
110
111
  const chief = this.agentChief();
111
112
  const specDefinition = this.getEndpointDefinition(endpoint);
112
113
  this.currentPlan = await chief.plan(endpoint, { style: opts.style, specDefinition });
@@ -198,6 +199,7 @@ interface ApibotOptions {
198
199
  verbose?: boolean;
199
200
  config?: string;
200
201
  path?: string;
202
+ endpoint?: string;
201
203
  }
202
204
 
203
205
  export type { ApibotOptions };
@@ -49,7 +49,7 @@ export function createApiCommands(name = 'api'): Command {
49
49
  addCommonOptions(cmd.command('plan <endpoint>').description('Generate test plan for an API endpoint').option('--style <style>', 'Planning style: basename of a file in rules/chief/styles/').option('--fresh', 'Start planning from scratch')).action(async (endpoint, options) => {
50
50
  setPreserveConsoleLogs(true);
51
51
  try {
52
- const bot = new ApiBot(buildOptions(options));
52
+ const bot = new ApiBot({ ...buildOptions(options), endpoint });
53
53
  await bot.start();
54
54
 
55
55
  await bot.plan(endpoint, { style: options.style, fresh: options.fresh });
@@ -122,7 +122,7 @@ export function createApiCommands(name = 'api'): Command {
122
122
  addCommonOptions(cmd.command('explore <endpoint>').description('Full cycle: plan all styles, execute tests, re-plan')).action(async (endpoint, options) => {
123
123
  setPreserveConsoleLogs(true);
124
124
  try {
125
- const bot = new ApiBot(buildOptions(options));
125
+ const bot = new ApiBot({ ...buildOptions(options), endpoint });
126
126
  await bot.start();
127
127
 
128
128
  const styles = Object.keys(getStyles());
@@ -1,7 +1,9 @@
1
1
  import { existsSync, mkdirSync, readFileSync } from 'node:fs';
2
2
  import path, { resolve } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
3
4
  import { parseEnv } from 'node:util';
4
- import { type AIConfig, type ApiHookFn, type ApiConfig as BaseApiConfig, EXPLORBOT_CONFIG_PATHS, createModel, materializeKnowledge, resolveModel, resolveOutputRoot } from '../../../src/config.ts';
5
+ import { type AIConfig, type ApiHookFn, type ApiConfig as BaseApiConfig, ConfigMissingError, EXPLORBOT_CONFIG_PATHS, createModel, envConfigRequested, materializeKnowledge, missingConfigMessage, resolveConfigModels, resolveModel, resolveOutputRoot } from '../../../src/config.ts';
6
+ import { type SiteRecord, findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from '../../../src/global-config.ts';
5
7
 
6
8
  export type { AIConfig };
7
9
 
@@ -26,6 +28,7 @@ export class ApibotConfigParser {
26
28
  private static instance: ApibotConfigParser;
27
29
  private config: ApibotConfig | null = null;
28
30
  private configPath: string | null = null;
31
+ private site: SiteRecord | null = null;
29
32
 
30
33
  private constructor() {}
31
34
 
@@ -42,7 +45,7 @@ export class ApibotConfigParser {
42
45
  Object.assign(process.env, parseEnv(readFileSync(resolved, 'utf8')));
43
46
  }
44
47
 
45
- async loadConfig(options?: { config?: string; path?: string }): Promise<ApibotConfig> {
48
+ async loadConfig(options?: { config?: string; path?: string; endpoint?: string }): Promise<ApibotConfig> {
46
49
  if (this.config && !options?.config && !options?.path) return this.config;
47
50
 
48
51
  const originalCwd = process.cwd();
@@ -50,6 +53,7 @@ export class ApibotConfigParser {
50
53
  process.chdir(resolve(options.path));
51
54
  }
52
55
 
56
+ ApibotConfigParser.loadEnv(globalEnvPath());
53
57
  ApibotConfigParser.loadEnv('.env');
54
58
 
55
59
  const resolvedPath = options?.config || this.findConfigFile();
@@ -78,7 +82,14 @@ export class ApibotConfigParser {
78
82
  }
79
83
 
80
84
  this.config = this.mergeWithDefaults(loadedConfig);
85
+ await resolveConfigModels(this.config.ai);
81
86
  this.configPath = resolvedPath;
87
+ this.site = null;
88
+
89
+ if (isGlobalConfigPath(resolvedPath)) {
90
+ this.enterGlobalMode(this.config, options?.endpoint);
91
+ }
92
+
82
93
  this.validateConfig(this.config);
83
94
 
84
95
  return this.config;
@@ -100,9 +111,22 @@ export class ApibotConfigParser {
100
111
 
101
112
  getOutputDir(): string {
102
113
  const config = this.getConfig();
114
+ return path.join(this.getProjectRoot(), config.dirs?.output || 'output');
115
+ }
116
+
117
+ getProjectRoot(): string {
118
+ if (this.site) return this.site.dir;
103
119
  const configPath = this.getConfigPath();
104
120
  if (!configPath) throw new Error('Config path not found');
105
- return path.join(path.dirname(configPath), config.dirs?.output || 'output');
121
+ return path.dirname(configPath);
122
+ }
123
+
124
+ resolveEndpointPath(endpoint: string): string {
125
+ if (!this.site) return endpoint;
126
+
127
+ const resolved = resolveSiteTarget(endpoint, this.site.url);
128
+ if (resolved.baseUrl !== this.site.url) return endpoint;
129
+ return resolved.path;
106
130
  }
107
131
 
108
132
  getPlansDir(): string {
@@ -115,9 +139,7 @@ export class ApibotConfigParser {
115
139
 
116
140
  getKnowledgeDir(): string {
117
141
  const config = this.getConfig();
118
- const configPath = this.getConfigPath();
119
- if (!configPath) throw new Error('Config path not found');
120
- return path.join(path.dirname(configPath), config.dirs?.knowledge || 'knowledge');
142
+ return path.join(this.getProjectRoot(), config.dirs?.knowledge || 'knowledge');
121
143
  }
122
144
 
123
145
  ensureDirectory(dirPath: string): void {
@@ -126,11 +148,20 @@ export class ApibotConfigParser {
126
148
  }
127
149
  }
128
150
 
151
+ private enterGlobalMode(config: ApibotConfig, endpoint?: string): void {
152
+ const site = resolveSiteTarget(endpoint);
153
+ this.site = registerSite(site.baseUrl);
154
+
155
+ config.dirs = { output: 'output', knowledge: 'knowledge' };
156
+ config.api = { ...config.api, baseEndpoint: site.baseUrl };
157
+ if (process.env.EXPLORBOT_API_SPEC) config.api.spec = [process.env.EXPLORBOT_API_SPEC];
158
+ }
159
+
129
160
  private async loadEnvConfig(): Promise<ApibotConfig> {
130
161
  const provider = process.env.EXPLORBOT_AI_PROVIDER;
131
162
  const modelSpec = process.env.EXPLORBOT_AI_MODEL;
132
163
  if (!provider && !modelSpec) {
133
- throw new Error('No configuration file found. Create apibot.config.js or set EXPLORBOT_URL and EXPLORBOT_AI_PROVIDER environment variables');
164
+ throw new ConfigMissingError(missingConfigMessage('apibot.config.js'));
134
165
  }
135
166
  if (modelSpec && !provider && !modelSpec.includes('/')) {
136
167
  throw new Error('EXPLORBOT_AI_MODEL needs a provider — set EXPLORBOT_AI_PROVIDER, or write it as "provider/model-id"');
@@ -177,15 +208,17 @@ export class ApibotConfigParser {
177
208
  if (existsSync(fullPath)) return fullPath;
178
209
  }
179
210
 
180
- return null;
211
+ if (envConfigRequested()) return null;
212
+ return findGlobalConfig();
181
213
  }
182
214
 
183
215
  private async loadConfigModule(configPath: string): Promise<any> {
184
216
  const ext = configPath.split('.').pop();
217
+ const moduleUrl = pathToFileURL(resolve(configPath)).href;
185
218
 
186
219
  if (ext === 'ts') {
187
220
  try {
188
- return await import(configPath);
221
+ return await import(moduleUrl);
189
222
  } catch {
190
223
  const require = (await import('node:module')).createRequire(import.meta.url);
191
224
  return require(configPath);
@@ -193,7 +226,7 @@ export class ApibotConfigParser {
193
226
  }
194
227
 
195
228
  if (ext === 'js' || ext === 'mjs') {
196
- return await import(configPath);
229
+ return await import(moduleUrl);
197
230
  }
198
231
 
199
232
  const content = readFileSync(configPath, 'utf8');
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env bun
2
+ import { remote } from '../../../src/remote.ts';
2
3
  import { createDocsCommands } from '../src/cli.ts';
3
4
 
4
5
  const program = createDocsCommands('doc-collector');
6
+ remote.registerOption(program);
5
7
  program.parse();
@@ -67,17 +67,32 @@ class Documentarian {
67
67
  const message = error instanceof Error ? error.message : String(error);
68
68
  tag('warning').log(`Interactive documentation failed: ${message}.`);
69
69
  if (meaningfulInteractions.length > 0) {
70
- tag('info').log(`Preserving ${meaningfulInteractions.length} observed interaction(s) without AI summary.`);
71
- return this.normalizeDocumentation(
72
- {
73
- summary: `Observed ${meaningfulInteractions.length} interaction(s); AI-generated summary was unavailable.`,
74
- can: [],
75
- might: [],
76
- interactions: meaningfulInteractions,
77
- },
78
- state,
79
- research
80
- );
70
+ tag('info').log(`Retrying static documentation while preserving ${meaningfulInteractions.length} observed interaction(s).`);
71
+ return this.documentStatic(state, research)
72
+ .then((documentation) =>
73
+ this.normalizeDocumentation(
74
+ {
75
+ ...documentation,
76
+ interactions: meaningfulInteractions,
77
+ },
78
+ state,
79
+ research
80
+ )
81
+ )
82
+ .catch((fallbackError) => {
83
+ const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
84
+ tag('warning').log(`Static documentation fallback failed: ${fallbackMessage}. Preserving observed interactions without AI summary.`);
85
+ return this.normalizeDocumentation(
86
+ {
87
+ summary: `Observed ${meaningfulInteractions.length} interaction(s); AI-generated summary was unavailable.`,
88
+ can: [],
89
+ might: [],
90
+ interactions: meaningfulInteractions,
91
+ },
92
+ state,
93
+ research
94
+ );
95
+ });
81
96
  }
82
97
  return this.documentStatic(state, research);
83
98
  }
@@ -113,13 +128,13 @@ class Documentarian {
113
128
  },
114
129
  ];
115
130
 
116
- const response = await this.provider.generateObject(messages, pageDocumentationSchema, undefined, {
131
+ const response = await this.provider.generateObject(messages, generatedPageDocumentationSchema, undefined, {
117
132
  agentName: 'documentarian',
118
133
  });
119
134
 
120
135
  return this.normalizeDocumentation(
121
136
  {
122
- ...(response.object as PageDocumentation),
137
+ ...(response.object as GeneratedPageDocumentation),
123
138
  interactions,
124
139
  },
125
140
  state,
@@ -139,11 +154,11 @@ class Documentarian {
139
154
  },
140
155
  ];
141
156
 
142
- const response = await this.provider.generateObject(messages, pageDocumentationSchema, undefined, {
157
+ const response = await this.provider.generateObject(messages, generatedPageDocumentationSchema, undefined, {
143
158
  agentName: 'documentarian',
144
159
  });
145
160
 
146
- return this.normalizeDocumentation(response.object as PageDocumentation, state, research);
161
+ return this.normalizeDocumentation(response.object as GeneratedPageDocumentation, state, research);
147
162
  }
148
163
 
149
164
  private getSystemPrompt(): string {
@@ -259,7 +274,7 @@ class Documentarian {
259
274
  return message.includes('Failed to generate JSON') || message.includes('Failed to validate JSON') || message.includes('failed_generation') || message.includes('No object generated') || message.includes('response did not match schema');
260
275
  }
261
276
 
262
- private normalizeDocumentation(documentation: PageDocumentation, _state: WebPageState, _research: string): PageDocumentation {
277
+ private normalizeDocumentation(documentation: GeneratedPageDocumentation & Partial<Pick<PageDocumentation, 'interactions'>>, _state: WebPageState, _research: string): PageDocumentation {
263
278
  const normalized = { ...documentation };
264
279
  if (!normalized.interactions) {
265
280
  normalized.interactions = undefined;
@@ -267,10 +282,10 @@ class Documentarian {
267
282
 
268
283
  const qualityNotes = this.evaluateDocumentationQuality(normalized);
269
284
 
270
- return {
285
+ return pageDocumentationSchema.parse({
271
286
  ...normalized,
272
287
  qualityNotes,
273
- };
288
+ });
274
289
  }
275
290
 
276
291
  private evaluateDocumentationQuality(documentation: PageDocumentation): string[] {
@@ -346,39 +361,54 @@ const stateTransitionSchema = z.object({
346
361
  action: z.string(),
347
362
  before: z.string(),
348
363
  after: z.string(),
349
- targetUrl: z.string().nullable(),
350
- discoveredUrls: z.array(z.string()).nullable(),
351
- newCapabilities: z.array(z.string()).nullable(),
364
+ targetUrl: z.string().optional(),
365
+ discoveredUrls: z.array(z.string()).optional(),
366
+ newCapabilities: z.array(z.string()).optional(),
352
367
  element: z
353
368
  .object({
354
369
  role: z.string(),
355
370
  name: z.string(),
356
371
  section: z.string(),
357
- container: z.string().nullable(),
358
- locator: z.string().nullable(),
372
+ container: z.string().optional(),
373
+ locator: z.string().optional(),
359
374
  })
360
- .nullable(),
375
+ .optional(),
361
376
  changes: z
362
377
  .object({
363
378
  urlChanged: z.boolean(),
364
379
  newElements: z.number(),
365
380
  removedElements: z.number(),
366
381
  })
367
- .nullable(),
382
+ .optional(),
383
+ targetState: z
384
+ .object({
385
+ kind: z.enum(['page', 'dialog', 'modal', 'section']),
386
+ label: z.string(),
387
+ url: z.string(),
388
+ })
389
+ .optional(),
390
+ screenshot: z
391
+ .object({
392
+ title: z.string(),
393
+ relativePath: z.string(),
394
+ })
395
+ .optional(),
368
396
  });
369
397
 
370
- const pageDocumentationSchema = z.object({
398
+ const generatedPageDocumentationSchema = z.object({
371
399
  summary: z.string(),
372
400
  can: z.array(capabilitySchema),
373
401
  might: z.array(capabilitySchema),
374
- interactions: z.array(stateTransitionSchema).nullable(),
402
+ });
403
+
404
+ const pageDocumentationSchema = generatedPageDocumentationSchema.extend({
405
+ interactions: z.array(stateTransitionSchema).optional(),
406
+ qualityNotes: z.array(z.string()).optional(),
375
407
  });
376
408
 
377
409
  type StateTransition = DocStateTransition;
378
- type PageDocumentation = Omit<z.infer<typeof pageDocumentationSchema>, 'interactions'> & {
379
- interactions?: StateTransition[];
380
- qualityNotes?: string[];
381
- };
410
+ type GeneratedPageDocumentation = z.infer<typeof generatedPageDocumentationSchema>;
411
+ type PageDocumentation = z.infer<typeof pageDocumentationSchema>;
382
412
 
383
413
  export { Documentarian };
384
414
  export type { PageDocumentation, StateTransition };
@@ -55,6 +55,7 @@ export function createDocsCommands(name = 'docs'): Command {
55
55
  console.log(`Skipped ${result.skipped.length} page(s)`);
56
56
  console.log(`Spec index: ${result.indexPath}`);
57
57
  console.log(`Pages dir: ${path.join(result.outputDir, 'pages')}`);
58
+ console.log(`Use in Explorbot: npx explorbot start ${startPath} --spec "${result.outputDir}"`);
58
59
 
59
60
  await bot.stop();
60
61
  process.exit(0);
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import path, { resolve } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
3
4
  import { parseEnv } from 'node:util';
4
5
  import { ConfigParser } from '../../../src/config.ts';
5
6
 
@@ -90,10 +91,11 @@ class DocbotConfigParser {
90
91
 
91
92
  private async loadConfigModule(configPath: string): Promise<any> {
92
93
  const ext = configPath.split('.').pop();
94
+ const moduleUrl = pathToFileURL(resolve(configPath)).href;
93
95
 
94
96
  if (ext === 'ts') {
95
97
  try {
96
- return await import(configPath);
98
+ return await import(moduleUrl);
97
99
  } catch {
98
100
  const require = (await import('node:module')).createRequire(import.meta.url);
99
101
  return require(configPath);
@@ -101,7 +103,7 @@ class DocbotConfigParser {
101
103
  }
102
104
 
103
105
  if (ext === 'js' || ext === 'mjs') {
104
- return await import(configPath);
106
+ return await import(moduleUrl);
105
107
  }
106
108
 
107
109
  return JSON.parse(readFileSync(configPath, 'utf8'));