explorbot 0.4.0 → 0.4.2

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 (177) hide show
  1. package/bin/explorbot-cli.ts +13 -6
  2. package/boat/api-tester/src/ai/chief.ts +7 -1
  3. package/boat/api-tester/src/ai/curler-tools.ts +1 -1
  4. package/boat/api-tester/src/ai/curler.ts +7 -1
  5. package/boat/api-tester/src/apibot.ts +10 -4
  6. package/boat/api-tester/src/cli.ts +15 -2
  7. package/boat/api-tester/src/config.ts +28 -8
  8. package/boat/doc-collector/bin/doc-collector-cli.ts +3 -2
  9. package/boat/doc-collector/src/ai/documentarian.ts +57 -5
  10. package/boat/doc-collector/src/ai/tools.ts +1 -1
  11. package/boat/doc-collector/src/cli.ts +21 -3
  12. package/boat/doc-collector/src/config.ts +3 -0
  13. package/boat/doc-collector/src/docbot.ts +57 -12
  14. package/boat/doc-collector/src/docs-renderer.ts +79 -55
  15. package/boat/doc-collector/src/interaction-screenshots.ts +19 -10
  16. package/boat/doc-collector/src/screenshots.ts +106 -3
  17. package/boat/doc-collector/src/template-dedup.ts +31 -0
  18. package/boat/prima/bin/prima-cli.ts +2 -0
  19. package/boat/prima/src/cli.ts +5 -0
  20. package/boat/prima/src/prima.ts +3 -4
  21. package/dist/bin/explorbot-cli.js +14 -6
  22. package/dist/boat/api-tester/bin/apibot-cli.js +3 -2
  23. package/dist/boat/api-tester/src/ai/chief.js +6 -1
  24. package/dist/boat/api-tester/src/ai/curler-tools.js +1 -1
  25. package/dist/boat/api-tester/src/ai/curler.js +6 -1
  26. package/dist/boat/api-tester/src/apibot.js +7 -3
  27. package/dist/boat/api-tester/src/cli.js +14 -2
  28. package/dist/boat/api-tester/src/config.js +31 -8
  29. package/dist/boat/doc-collector/bin/doc-collector-cli.js +3 -2
  30. package/dist/boat/doc-collector/src/ai/documentarian.js +62 -4
  31. package/dist/boat/doc-collector/src/ai/tools.js +1 -1
  32. package/dist/boat/doc-collector/src/cli.js +18 -3
  33. package/dist/boat/doc-collector/src/config.js +1 -0
  34. package/dist/boat/doc-collector/src/docbot.js +53 -11
  35. package/dist/boat/doc-collector/src/docs-renderer.js +69 -46
  36. package/dist/boat/doc-collector/src/interaction-screenshots.js +18 -11
  37. package/dist/boat/doc-collector/src/screenshots.js +104 -1
  38. package/dist/boat/doc-collector/src/template-dedup.js +27 -0
  39. package/dist/boat/prima/bin/prima-cli.js +2 -0
  40. package/dist/boat/prima/src/cli.js +5 -0
  41. package/dist/boat/prima/src/prima.js +3 -4
  42. package/dist/package.json +1 -1
  43. package/dist/rules/planner/styles/normal.md +1 -1
  44. package/dist/src/action-result.d.ts +2 -2
  45. package/dist/src/action-result.js +15 -17
  46. package/dist/src/action.d.ts +1 -1
  47. package/dist/src/action.js +15 -15
  48. package/dist/src/ai/captain.js +1 -1
  49. package/dist/src/ai/fisherman/request-haul.d.ts +11 -0
  50. package/dist/src/ai/fisherman/request-haul.js +28 -0
  51. package/dist/src/ai/fisherman-tools.d.ts +10 -1
  52. package/dist/src/ai/fisherman-tools.js +35 -31
  53. package/dist/src/ai/fisherman.d.ts +3 -2
  54. package/dist/src/ai/fisherman.js +8 -6
  55. package/dist/src/ai/navigator.js +1 -1
  56. package/dist/src/ai/pilot.js +21 -16
  57. package/dist/src/ai/planner.js +9 -7
  58. package/dist/src/ai/provider.js +8 -14
  59. package/dist/src/ai/researcher/coordinates.d.ts +2 -0
  60. package/dist/src/ai/researcher/coordinates.js +13 -1
  61. package/dist/src/ai/researcher/deep-analysis.js +9 -9
  62. package/dist/src/ai/researcher/fingerprint-worker.js +1 -11
  63. package/dist/src/ai/researcher.js +3 -1
  64. package/dist/src/ai/rules.js +11 -3
  65. package/dist/src/ai/tester.js +20 -20
  66. package/dist/src/ai/tools.d.ts +1 -1
  67. package/dist/src/ai/tools.js +18 -74
  68. package/dist/src/api/request-store.d.ts +1 -5
  69. package/dist/src/api/request-store.js +12 -22
  70. package/dist/src/api/spec-reader.js +1 -1
  71. package/dist/src/commands/config-command.js +3 -10
  72. package/dist/src/commands/drill-command.js +1 -1
  73. package/dist/src/commands/explore-command.js +12 -1
  74. package/dist/src/commands/index.js +2 -0
  75. package/dist/src/commands/init-command.js +3 -7
  76. package/dist/src/commands/options/base-option.d.ts +8 -0
  77. package/dist/src/commands/options/base-option.js +12 -0
  78. package/dist/src/commands/options/index.d.ts +5 -0
  79. package/dist/src/commands/options/index.js +5 -0
  80. package/dist/src/commands/options/knowledge-option.d.ts +7 -0
  81. package/dist/src/commands/options/knowledge-option.js +12 -0
  82. package/dist/src/commands/options/ws-option.d.ts +7 -0
  83. package/dist/src/commands/options/ws-option.js +21 -0
  84. package/dist/src/commands/plans-command.d.ts +4 -1
  85. package/dist/src/commands/plans-command.js +4 -4
  86. package/dist/src/commands/recommended-models-command.d.ts +9 -0
  87. package/dist/src/commands/recommended-models-command.js +85 -0
  88. package/dist/src/config.d.ts +3 -0
  89. package/dist/src/config.js +18 -2
  90. package/dist/src/experience-tracker.js +1 -1
  91. package/dist/src/explorbot.js +4 -4
  92. package/dist/src/global-config.d.ts +2 -0
  93. package/dist/src/global-config.js +6 -0
  94. package/dist/src/knowledge-tracker.d.ts +20 -7
  95. package/dist/src/knowledge-tracker.js +69 -31
  96. package/dist/src/remote.d.ts +0 -3
  97. package/dist/src/remote.js +0 -18
  98. package/dist/src/state-manager.d.ts +3 -3
  99. package/dist/src/state-manager.js +8 -8
  100. package/dist/src/test-plan.d.ts +1 -0
  101. package/dist/src/test-plan.js +13 -9
  102. package/dist/src/utils/aria.d.ts +1 -1
  103. package/dist/src/utils/aria.js +23 -2
  104. package/dist/src/utils/cli-section.d.ts +1 -0
  105. package/dist/src/utils/cli-section.js +12 -0
  106. package/dist/src/utils/html-diff.d.ts +1 -0
  107. package/dist/src/utils/html-diff.js +5 -5
  108. package/dist/src/utils/html.d.ts +2 -0
  109. package/dist/src/utils/html.js +5 -4
  110. package/dist/src/utils/overlay.d.ts +19 -55
  111. package/dist/src/utils/overlay.js +94 -148
  112. package/dist/src/utils/region.d.ts +41 -0
  113. package/dist/src/utils/region.js +99 -0
  114. package/dist/src/utils/similarity.d.ts +1 -0
  115. package/dist/src/utils/similarity.js +11 -0
  116. package/dist/src/utils/url-matcher.d.ts +1 -1
  117. package/dist/src/utils/url-matcher.js +4 -4
  118. package/docs/api-testing/basics.md +15 -0
  119. package/docs/api-testing/planning.md +10 -1
  120. package/docs/doc-collection/basics.md +11 -5
  121. package/docs/doc-collection/crawling.md +21 -1
  122. package/docs/reference/commands.md +27 -4
  123. package/docs/superpowers/plans/2026-09-03-model-resolves-click-ambiguity.md +509 -0
  124. package/docs/workflow/agentic-usage.md +11 -2
  125. package/docs/workflow/application-spec.md +4 -0
  126. package/docs/workflow/knowledge.md +46 -2
  127. package/package.json +1 -1
  128. package/rules/planner/styles/normal.md +1 -1
  129. package/src/action-result.ts +15 -16
  130. package/src/action.ts +16 -15
  131. package/src/ai/captain.ts +1 -1
  132. package/src/ai/fisherman/request-haul.ts +32 -0
  133. package/src/ai/fisherman-tools.ts +38 -32
  134. package/src/ai/fisherman.ts +8 -7
  135. package/src/ai/navigator.ts +1 -1
  136. package/src/ai/pilot.ts +19 -14
  137. package/src/ai/planner.ts +9 -8
  138. package/src/ai/provider.ts +8 -12
  139. package/src/ai/researcher/coordinates.ts +13 -1
  140. package/src/ai/researcher/deep-analysis.ts +9 -9
  141. package/src/ai/researcher/fingerprint-worker.ts +1 -10
  142. package/src/ai/researcher.ts +2 -1
  143. package/src/ai/rules.ts +11 -3
  144. package/src/ai/tester.ts +18 -18
  145. package/src/ai/tools.ts +18 -86
  146. package/src/api/request-store.ts +13 -27
  147. package/src/api/spec-reader.ts +1 -1
  148. package/src/commands/config-command.ts +3 -8
  149. package/src/commands/drill-command.ts +1 -1
  150. package/src/commands/explore-command.ts +12 -1
  151. package/src/commands/index.ts +2 -0
  152. package/src/commands/init-command.ts +4 -11
  153. package/src/commands/options/base-option.ts +18 -0
  154. package/src/commands/options/index.ts +7 -0
  155. package/src/commands/options/knowledge-option.ts +14 -0
  156. package/src/commands/options/ws-option.ts +24 -0
  157. package/src/commands/plans-command.ts +5 -5
  158. package/src/commands/recommended-models-command.ts +92 -0
  159. package/src/config.ts +20 -2
  160. package/src/experience-tracker.ts +1 -1
  161. package/src/explorbot.ts +4 -4
  162. package/src/global-config.ts +8 -0
  163. package/src/knowledge-tracker.ts +94 -36
  164. package/src/remote.ts +0 -20
  165. package/src/state-manager.ts +10 -9
  166. package/src/test-plan.ts +12 -10
  167. package/src/utils/aria.ts +26 -3
  168. package/src/utils/cli-section.ts +12 -0
  169. package/src/utils/html-diff.ts +6 -6
  170. package/src/utils/html.ts +6 -4
  171. package/src/utils/overlay.ts +105 -192
  172. package/src/utils/region.ts +132 -0
  173. package/src/utils/similarity.ts +9 -0
  174. package/src/utils/url-matcher.ts +3 -3
  175. package/dist/src/utils/request-map.d.ts +0 -7
  176. package/dist/src/utils/request-map.js +0 -16
  177. package/src/utils/request-map.ts +0 -19
@@ -8,8 +8,10 @@ import figureSet from 'figures';
8
8
  import { render } from 'ink';
9
9
  import React from 'react';
10
10
  import { flushTelemetry } from '../src/ai/provider.js';
11
+ import { RecommendedModelsCommand } from '../src/commands/recommended-models-command.js';
11
12
  import { App } from '../src/components/App.js';
12
13
  import { StatusPane } from '../src/components/StatusPane.js';
14
+ import { knowledgeOption, wsOption } from '../src/commands/options/index.js';
13
15
  import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
14
16
  import { ExplorBot } from '../src/explorbot.js';
15
17
  import { remote } from '../src/remote.js';
@@ -25,7 +27,8 @@ const cli = getCliName();
25
27
  const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../package.json');
26
28
  const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
27
29
  program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
28
- remote.registerOption(program);
30
+ wsOption.register(program);
31
+ knowledgeOption.register(program);
29
32
  process.on('uncaughtException', async (error) => {
30
33
  tag('error').log(`Uncaught exception: ${error instanceof Error ? `${error.message}\n${error.stack}` : String(error)}`);
31
34
  await flushTelemetry();
@@ -294,10 +297,14 @@ addCommonOptions(program.command('test <planfile> [index]').description('Execute
294
297
  planfileArg = options.fromPlan;
295
298
  indexArg = planfile;
296
299
  }
297
- const planTarget = Plan.loadFromFile(planfileArg)?.startUrl;
298
- const explorBot = new ExplorBot(buildExplorBotOptions(planTarget, options));
300
+ const peeked = Plan.loadFromFile(planfileArg);
301
+ const explorBot = new ExplorBot(buildExplorBotOptions(peeked?.startUrl, options));
299
302
  await explorBot.start();
300
- const plan = explorBot.loadPlan(planfileArg);
303
+ let plan = peeked;
304
+ if (plan)
305
+ explorBot.setCurrentPlan(plan);
306
+ if (!plan)
307
+ plan = explorBot.loadPlan(planfileArg);
301
308
  const pending = plan.getPendingTests();
302
309
  log(`Plan loaded: "${plan.title}" (${plan.tests.length} tests, ${pending.length} pending)`);
303
310
  const startUrl = plan.startUrl;
@@ -418,6 +425,7 @@ program
418
425
  process.exit(1);
419
426
  }
420
427
  });
428
+ RecommendedModelsCommand.register(program);
421
429
  program
422
430
  .command('init')
423
431
  .description('Initialize configuration for a project or for this machine')
@@ -644,13 +652,13 @@ addCommonOptions(program.command('navigate <url>').description('Navigate to a UR
644
652
  await showStatsAndExit(1);
645
653
  }
646
654
  });
647
- addCommonOptions(program.command('drill <url>').alias('driller').description('Drill all components on a page to learn interactions').option('--knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max-components <count>', 'Maximum number of components to drill')).action(async (url, options) => {
655
+ addCommonOptions(program.command('drill <url>').alias('driller').description('Drill all components on a page to learn interactions').option('--save-knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max-components <count>', 'Maximum number of components to drill')).action(async (url, options) => {
648
656
  try {
649
657
  const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
650
658
  await explorBot.start();
651
659
  await explorBot.visit(url);
652
660
  const plan = await explorBot.agentDriller().drill({
653
- knowledgePath: options.knowledge,
661
+ knowledgePath: options.saveKnowledge,
654
662
  maxComponents: Number.parseInt(options.maxComponents || '30', 10),
655
663
  interactive: false,
656
664
  });
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
- import { remote } from "../../../src/remote.js";
2
+ import { knowledgeOption, wsOption } from "../../../src/commands/options/index.js";
3
3
  import { createApiCommands } from "../src/cli.js";
4
4
  const program = createApiCommands('apibot');
5
- remote.registerOption(program);
5
+ wsOption.register(program);
6
+ knowledgeOption.register(program);
6
7
  program.parse();
@@ -24,15 +24,17 @@ export class Chief extends ChiefBase {
24
24
  provider;
25
25
  config;
26
26
  apiClient;
27
+ knowledgeTracker;
27
28
  currentPlan = null;
28
29
  lastStyleName = '';
29
30
  MIN_TASKS = 3;
30
31
  MAX_TASKS = 10;
31
- constructor(provider, config, apiClient) {
32
+ constructor(provider, config, apiClient, knowledgeTracker) {
32
33
  super();
33
34
  this.provider = provider;
34
35
  this.config = config;
35
36
  this.apiClient = apiClient || null;
37
+ this.knowledgeTracker = knowledgeTracker;
36
38
  }
37
39
  async plan(endpoint, opts) {
38
40
  tag('info').log(`Planning API tests for ${endpoint}`);
@@ -42,6 +44,9 @@ export class Chief extends ChiefBase {
42
44
  await Observability.run(`chief: ${endpoint}`, { tags: ['chief'], sessionId: endpoint }, async () => {
43
45
  const sampleData = await this.collectSampleData(endpoint);
44
46
  const conversation = this.buildConversation(endpoint, opts?.style, sampleData);
47
+ const knowledge = this.knowledgeTracker?.renderEndpointKnowledge(endpoint);
48
+ if (knowledge)
49
+ conversation.addUserText(knowledge);
45
50
  if (opts?.specDefinition) {
46
51
  conversation.addUserText(dedent `
47
52
  <api_spec>
@@ -61,7 +61,7 @@ export function createCurlerTools(apiClient, requestState, test, searchSpec) {
61
61
  body: input.body,
62
62
  queryParams: input.queryParams,
63
63
  });
64
- requestState.addRequest(result);
64
+ requestState.addMadeRequest(result);
65
65
  if (result.error) {
66
66
  tag('error').log(`${input.method} ${input.path} > Network error: ${result.error}`);
67
67
  activeNote.commit(TestResult.FAILED);
@@ -11,11 +11,13 @@ export class Curler {
11
11
  apiClient;
12
12
  requestState;
13
13
  reporter;
14
- constructor(provider, apiClient, requestState, reporter) {
14
+ knowledgeTracker;
15
+ constructor(provider, apiClient, requestState, reporter, knowledgeTracker) {
15
16
  this.provider = provider;
16
17
  this.apiClient = apiClient;
17
18
  this.requestState = requestState;
18
19
  this.reporter = reporter;
20
+ this.knowledgeTracker = knowledgeTracker;
19
21
  }
20
22
  async test(test, opts) {
21
23
  tag('info').log(`Testing: ${test.scenario}`);
@@ -25,6 +27,9 @@ export class Curler {
25
27
  await this.reporter.reportTestStart(test);
26
28
  const conversation = this.provider.startConversation(this.buildSystemPrompt(), 'curler', this.provider.getAgenticModel('curler'));
27
29
  const tools = createCurlerTools(this.apiClient, this.requestState, test, opts?.searchSpec);
30
+ const knowledge = test.startUrl && this.knowledgeTracker?.renderEndpointKnowledge(test.startUrl);
31
+ if (knowledge)
32
+ conversation.addUserText(knowledge);
28
33
  const initialPrompt = this.buildTestPrompt(test, opts?.specDefinition, opts?.baseEndpoint);
29
34
  conversation.addUserText(initialPrompt);
30
35
  await loop(async ({ stop, iteration }) => {
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { AIProvider } from "../../../src/ai/provider.js";
4
4
  import { RequestStore } from "../../../src/api/request-store.js";
5
5
  import { extractEndpointDefinition, loadSpec, searchEndpoints, validateSpecs } from "../../../src/api/spec-reader.js";
6
+ import { KnowledgeTracker } from "../../../src/knowledge-tracker.js";
6
7
  import { Reporter } from "../../../src/reporter.js";
7
8
  import { Plan } from "../../../src/test-plan.js";
8
9
  import { setVerboseMode, tag } from "../../../src/utils/logger.js";
@@ -19,6 +20,7 @@ export class ApiBot {
19
20
  apiClient;
20
21
  requestState;
21
22
  reporter;
23
+ knowledgeTracker;
22
24
  options;
23
25
  apiSpec;
24
26
  constructor(options = {}) {
@@ -30,7 +32,7 @@ export class ApiBot {
30
32
  }
31
33
  }
32
34
  async start() {
33
- this.config = await this.configParser.loadConfig({ config: this.options.config, path: this.options.path, endpoint: this.options.endpoint });
35
+ this.config = await this.configParser.loadConfig(this.options);
34
36
  this.provider = new AIProvider(this.config.ai);
35
37
  await this.provider.validateConnection();
36
38
  this.apiClient = new ApiClient(this.config.api.baseEndpoint, this.config.api.headers || {}, {
@@ -42,6 +44,7 @@ export class ApiBot {
42
44
  this.configParser.ensureDirectory(outputDir);
43
45
  this.requestState = new RequestStore(outputDir);
44
46
  this.reporter = new Reporter(this.config.reporter);
47
+ this.knowledgeTracker = new KnowledgeTracker({ knowledgeDir: this.configParser.getKnowledgeDir() });
45
48
  validateSpecs(this.config.api.spec);
46
49
  this.apiSpec = await loadSpec(this.config.api.spec, outputDir);
47
50
  tag('info').log('OpenAPI spec loaded');
@@ -78,13 +81,14 @@ export class ApiBot {
78
81
  config: this.config,
79
82
  apiClient: this.apiClient,
80
83
  requestState: this.requestState,
84
+ knowledge: this.knowledgeTracker,
81
85
  });
82
86
  }
83
87
  agentChief() {
84
- return (this.agents.chief ||= this.createAgent(({ ai, config, apiClient }) => new Chief(ai, config, apiClient)));
88
+ return (this.agents.chief ||= this.createAgent(({ ai, config, apiClient, knowledge }) => new Chief(ai, config, apiClient, knowledge)));
85
89
  }
86
90
  agentCurler() {
87
- return (this.agents.curler ||= this.createAgent(({ ai, apiClient, requestState }) => new Curler(ai, apiClient, requestState, this.reporter)));
91
+ return (this.agents.curler ||= this.createAgent(({ ai, apiClient, requestState, knowledge }) => new Curler(ai, apiClient, requestState, this.reporter, knowledge)));
88
92
  }
89
93
  async plan(target, opts = {}) {
90
94
  if (opts.fresh) {
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { Command } from 'commander';
4
4
  import { ConfigCommand } from "../../../src/commands/config-command.js";
5
+ import { RecommendedModelsCommand } from "../../../src/commands/recommended-models-command.js";
5
6
  import { listSites } from "../../../src/global-config.js";
6
7
  import { setPreserveConsoleLogs } from "../../../src/utils/logger.js";
7
8
  import { getStyles } from "./ai/chief/styles.js";
@@ -12,10 +13,18 @@ function buildOptions(options) {
12
13
  verbose: options.verbose || options.debug,
13
14
  config: options.config,
14
15
  path: options.path,
16
+ baseEndpoint: options.endpoint,
17
+ spec: options.spec,
15
18
  };
16
19
  }
17
20
  function addCommonOptions(cmd) {
18
- return cmd.option('-v, --verbose', 'Enable verbose logging').option('--debug', 'Enable debug logging').option('-c, --config <path>', 'Path to configuration file').option('-p, --path <path>', 'Working directory path');
21
+ return cmd
22
+ .option('-v, --verbose', 'Enable verbose logging')
23
+ .option('--debug', 'Enable debug logging')
24
+ .option('-c, --config <path>', 'Path to configuration file')
25
+ .option('-p, --path <path>', 'Working directory path')
26
+ .option('--endpoint <url>', 'Base API endpoint to test (env: EXPLORBOT_URL)')
27
+ .option('--spec <path>', 'OpenAPI spec file or URL (env: EXPLORBOT_API_SPEC)');
19
28
  }
20
29
  function selectTests(tests, index) {
21
30
  if (!index || index === '*' || index === 'all') {
@@ -76,8 +85,10 @@ export function createApiCommands(name = 'api') {
76
85
  .action(async (endpoint, options) => {
77
86
  const parser = ApibotConfigParser.getInstance();
78
87
  const [site] = listSites();
88
+ const runOptions = buildOptions(options);
89
+ runOptions.endpoint = endpoint || site?.url;
79
90
  try {
80
- const config = await parser.loadConfig({ config: options.config, path: options.path, endpoint: endpoint || site?.url });
91
+ const config = await parser.loadConfig(runOptions);
81
92
  console.log(ConfigCommand.render(config, { configPath: parser.getConfigPath(), root: parser.getProjectRoot(), json: options.json }));
82
93
  }
83
94
  catch (error) {
@@ -85,6 +96,7 @@ export function createApiCommands(name = 'api') {
85
96
  process.exit(1);
86
97
  }
87
98
  });
99
+ RecommendedModelsCommand.register(cmd);
88
100
  addCommonOptions(cmd.command('test <planfile> [index]').description('Execute tests from a plan file. Index: 1, 1-3, *')).action(async (planfile, index, options) => {
89
101
  setPreserveConsoleLogs(true);
90
102
  try {
@@ -39,6 +39,7 @@ export class ApibotConfigParser {
39
39
  }
40
40
  ApibotConfigParser.loadEnv(globalEnvPath());
41
41
  ApibotConfigParser.loadEnv('.env');
42
+ this.applyRunOptions(options);
42
43
  const resolvedPath = options?.config || this.findConfigFile();
43
44
  if (!resolvedPath) {
44
45
  try {
@@ -63,6 +64,9 @@ export class ApibotConfigParser {
63
64
  };
64
65
  }
65
66
  this.config = this.mergeWithDefaults(loadedConfig);
67
+ this.applyEnvSpec(this.config.api);
68
+ if (options?.baseEndpoint)
69
+ this.config.api.baseEndpoint = options.baseEndpoint.replace(/\/$/, '');
66
70
  await resolveConfigModels(this.config.ai);
67
71
  this.configPath = resolvedPath;
68
72
  this.site = null;
@@ -104,6 +108,13 @@ export class ApibotConfigParser {
104
108
  const resolved = resolveSiteTarget(endpoint, this.site.url);
105
109
  if (resolved.baseUrl !== this.site.url)
106
110
  return endpoint;
111
+ const basePath = new URL(this.getConfig().api.baseEndpoint).pathname.replace(/\/$/, '');
112
+ if (!basePath)
113
+ return resolved.path;
114
+ if (resolved.path === basePath)
115
+ return '/';
116
+ if (resolved.path.startsWith(`${basePath}/`))
117
+ return resolved.path.slice(basePath.length);
107
118
  return resolved.path;
108
119
  }
109
120
  getPlansDir() {
@@ -121,13 +132,27 @@ export class ApibotConfigParser {
121
132
  mkdirSync(dirPath, { recursive: true });
122
133
  }
123
134
  }
135
+ applyRunOptions(options) {
136
+ if (options?.baseEndpoint)
137
+ process.env.EXPLORBOT_URL = options.baseEndpoint;
138
+ if (options?.spec)
139
+ process.env.EXPLORBOT_API_SPEC = options.spec;
140
+ }
141
+ applyEnvSpec(api) {
142
+ if (!process.env.EXPLORBOT_API_SPEC)
143
+ return;
144
+ api.spec = [process.env.EXPLORBOT_API_SPEC];
145
+ }
124
146
  enterGlobalMode(config, endpoint) {
125
147
  const site = resolveSiteTarget(endpoint);
126
148
  this.site = registerSite(site.baseUrl);
149
+ let baseEndpoint = site.baseUrl;
150
+ const envUrl = process.env.EXPLORBOT_URL;
151
+ if (envUrl && URL.parse(envUrl)?.origin === site.baseUrl)
152
+ baseEndpoint = envUrl.replace(/\/$/, '');
127
153
  config.dirs = { output: 'output', knowledge: 'knowledge' };
128
- config.api = { ...config.api, baseEndpoint: site.baseUrl };
129
- if (process.env.EXPLORBOT_API_SPEC)
130
- config.api.spec = [process.env.EXPLORBOT_API_SPEC];
154
+ config.api = { ...config.api, baseEndpoint };
155
+ materializeKnowledge(this.site.dir);
131
156
  }
132
157
  async loadEnvConfig() {
133
158
  const provider = process.env.EXPLORBOT_AI_PROVIDER;
@@ -140,14 +165,12 @@ export class ApibotConfigParser {
140
165
  }
141
166
  const baseEndpoint = process.env.EXPLORBOT_URL;
142
167
  if (!baseEndpoint) {
143
- throw new Error('No API endpoint to test. Set EXPLORBOT_URL to the API base endpoint');
168
+ throw new Error('No API endpoint to test. Pass --endpoint or set EXPLORBOT_URL to the API base endpoint');
144
169
  }
145
170
  const outputRoot = resolveOutputRoot();
146
171
  materializeKnowledge(outputRoot);
147
172
  const api = { baseEndpoint };
148
- if (process.env.EXPLORBOT_API_SPEC) {
149
- api.spec = [process.env.EXPLORBOT_API_SPEC];
150
- }
173
+ this.applyEnvSpec(api);
151
174
  let model;
152
175
  if (provider && modelSpec)
153
176
  model = await createModel(provider, modelSpec);
@@ -210,7 +233,7 @@ export class ApibotConfigParser {
210
233
  }
211
234
  }
212
235
  mergeWithDefaults(config) {
213
- return this.deepMerge({ dirs: { output: 'output' } }, config);
236
+ return this.deepMerge({ dirs: { output: 'output' }, api: {} }, config);
214
237
  }
215
238
  deepMerge(target, source) {
216
239
  const result = { ...target };
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
- import { remote } from "../../../src/remote.js";
2
+ import { knowledgeOption, wsOption } from "../../../src/commands/options/index.js";
3
3
  import { createDocsCommands } from "../src/cli.js";
4
4
  const program = createDocsCommands('doc-collector');
5
- remote.registerOption(program);
5
+ wsOption.register(program);
6
+ knowledgeOption.register(program);
6
7
  program.parse();
@@ -1,6 +1,8 @@
1
1
  import dedent from 'dedent';
2
2
  import { z } from 'zod';
3
+ import { parseAriaLocator } from "../../../../src/utils/aria.js";
3
4
  import { tag } from "../../../../src/utils/logger.js";
5
+ import { parseResearchSections } from "../../../../src/ai/researcher/parser.js";
4
6
  import { collectDocInteractions } from "./tools.js";
5
7
  class Documentarian {
6
8
  provider;
@@ -147,6 +149,9 @@ class Documentarian {
147
149
  Only list capabilities that are grounded in the provided page research.
148
150
  Put actions into "can" only when there is direct evidence in the page context.
149
151
  Put actions into "might" only when the UI strongly suggests a capability but proof is incomplete.
152
+ Going to another page is navigation, not a capability. "can" and "might" list only actions the user performs on this page; links and menus are documented in a separate navigation section, never here.
153
+ Raw interaction observations that change the URL are navigation evidence only. Never turn them into "can" or "might" actions.
154
+ Exclude capabilities supplied by unrelated embedded support, marketing, consent, or feedback widgets. Include an embedded interface only when it is part of the current page's documented purpose.
150
155
  Describe each action from the end-user perspective.
151
156
  Be explicit about scope:
152
157
  - one item
@@ -154,7 +159,7 @@ class Documentarian {
154
159
  - bulk operations
155
160
  - all items
156
161
  - page-level
157
- Avoid implementation details, selectors, and QA wording.
162
+ Avoid implementation details, selectors, and QA wording — except the machine-consumed element field, which is a locator, not prose.
158
163
  Avoid duplicate actions with different phrasing.
159
164
  </rules>
160
165
 
@@ -203,6 +208,8 @@ class Documentarian {
203
208
  - action: concise user-facing capability phrased as "user can ..."
204
209
  - scope: one of one item, list of items, bulk operations, all items, page-level
205
210
  - evidence: short reason based on visible UI or research
211
+ - element: required for every "can" and "might" action. Return the proving or suggesting control's locator copied verbatim from the page research table — the value from the ARIA column, or from the CSS column when the ARIA column is empty. Return null only when no single element supports the action. It is machine metadata used for validation and never shown as prose.
212
+ Never copy the human-readable Element description from interaction_observations into element. It is not a locator.
206
213
  </output_requirements>
207
214
  `;
208
215
  }
@@ -233,8 +240,37 @@ class Documentarian {
233
240
  const message = error instanceof Error ? error.message : String(error);
234
241
  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');
235
242
  }
236
- normalizeDocumentation(documentation, _state, _research) {
243
+ normalizeDocumentation(documentation, _state, research) {
237
244
  const normalized = { ...documentation };
245
+ const navigationLocators = new Set();
246
+ const researchLocators = new Set();
247
+ for (const section of parseResearchSections(research)) {
248
+ for (const element of section.elements) {
249
+ const aria = element.aria ? `{ role: '${element.aria.role}', text: '${element.aria.text}' }` : null;
250
+ if (aria)
251
+ researchLocators.add(this.locatorKey(aria));
252
+ if (element.css)
253
+ researchLocators.add(this.locatorKey(element.css));
254
+ if (element.aria?.role !== 'link')
255
+ continue;
256
+ if (aria)
257
+ navigationLocators.add(this.locatorKey(aria));
258
+ if (element.css)
259
+ navigationLocators.add(this.locatorKey(element.css));
260
+ }
261
+ }
262
+ normalized.can = normalized.can.filter((capability) => {
263
+ if (!capability.element)
264
+ return true;
265
+ const key = this.locatorKey(capability.element);
266
+ return researchLocators.has(key) && !navigationLocators.has(key);
267
+ });
268
+ normalized.might = normalized.might.filter((capability) => {
269
+ if (!capability.element)
270
+ return false;
271
+ const key = this.locatorKey(capability.element);
272
+ return researchLocators.has(key) && !navigationLocators.has(key);
273
+ });
238
274
  if (!normalized.interactions) {
239
275
  normalized.interactions = undefined;
240
276
  }
@@ -244,6 +280,18 @@ class Documentarian {
244
280
  qualityNotes,
245
281
  });
246
282
  }
283
+ locatorKey(locator) {
284
+ const aria = parseAriaLocator(locator);
285
+ if (aria)
286
+ return `aria:${aria.role}:${aria.text}`;
287
+ const trimmed = locator.trim();
288
+ const first = trimmed.at(0);
289
+ const last = trimmed.at(-1);
290
+ if (first === last && (first === '`' || first === '"' || first === "'")) {
291
+ return `css:${trimmed.slice(1, -1).trim()}`;
292
+ }
293
+ return `css:${trimmed}`;
294
+ }
247
295
  evaluateDocumentationQuality(documentation) {
248
296
  const notes = [];
249
297
  if ((documentation.interactions || []).length === 0 && this.config.docs?.interactive) {
@@ -300,6 +348,13 @@ const capabilitySchema = z.object({
300
348
  action: z.string(),
301
349
  scope: z.enum(['one item', 'list of items', 'bulk operations', 'all items', 'page-level']),
302
350
  evidence: z.string(),
351
+ element: z.string().nullable().optional(),
352
+ });
353
+ const generatedCapabilitySchema = z.object({
354
+ action: z.string(),
355
+ scope: z.enum(['one item', 'list of items', 'bulk operations', 'all items', 'page-level']),
356
+ evidence: z.string(),
357
+ element: z.string().nullable(),
303
358
  });
304
359
  const stateTransitionSchema = z.object({
305
360
  action: z.string(),
@@ -339,11 +394,14 @@ const stateTransitionSchema = z.object({
339
394
  .optional(),
340
395
  });
341
396
  const generatedPageDocumentationSchema = z.object({
397
+ summary: z.string(),
398
+ can: z.array(generatedCapabilitySchema),
399
+ might: z.array(generatedCapabilitySchema),
400
+ });
401
+ const pageDocumentationSchema = z.object({
342
402
  summary: z.string(),
343
403
  can: z.array(capabilitySchema),
344
404
  might: z.array(capabilitySchema),
345
- });
346
- const pageDocumentationSchema = generatedPageDocumentationSchema.extend({
347
405
  interactions: z.array(stateTransitionSchema).optional(),
348
406
  qualityNotes: z.array(z.string()).optional(),
349
407
  });
@@ -132,7 +132,7 @@ function buildTransition(candidate, beforeState, afterState, changes) {
132
132
  function describeTargetState(beforeState, afterState, candidate) {
133
133
  const beforeFocus = detectFocusArea(beforeState.ariaSnapshot || null);
134
134
  const afterFocus = detectFocusArea(afterState.ariaSnapshot || null);
135
- if (afterFocus.detected && (!beforeFocus.detected || beforeFocus.name !== afterFocus.name)) {
135
+ if (afterFocus.type && (!beforeFocus.type || beforeFocus.name !== afterFocus.name)) {
136
136
  return {
137
137
  kind: afterFocus.type || 'dialog',
138
138
  label: afterFocus.name || candidate.element.name.trim(),
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { Command } from 'commander';
4
4
  import { ConfigCommand } from "../../../src/commands/config-command.js";
5
+ import { RecommendedModelsCommand } from "../../../src/commands/recommended-models-command.js";
5
6
  import { remote } from "../../../src/remote.js";
6
7
  import { isVerboseMode, setPreserveConsoleLogs, setQuietMode } from "../../../src/utils/logger.js";
7
8
  import { DocBot } from "./docbot.js";
@@ -15,6 +16,7 @@ function buildOptions(options) {
15
16
  incognito: options.incognito,
16
17
  session: options.session,
17
18
  docsConfig: options.docsConfig,
19
+ baseUrl: options.url,
18
20
  };
19
21
  }
20
22
  function addCommonOptions(cmd) {
@@ -24,6 +26,7 @@ function addCommonOptions(cmd) {
24
26
  .option('-c, --config <path>', 'Path to explorbot configuration file')
25
27
  .option('--docs-config <path>', 'Path to doc collector configuration file')
26
28
  .option('-p, --path <path>', 'Working directory path')
29
+ .option('--url <url>', 'Base URL of the site, when the path argument is relative (env: EXPLORBOT_URL)')
27
30
  .option('-s, --show', 'Show browser window')
28
31
  .option('--headless', 'Run browser in headless mode')
29
32
  .option('--incognito', 'Run without recording experiences')
@@ -32,7 +35,12 @@ function addCommonOptions(cmd) {
32
35
  export function createDocsCommands(name = 'docs') {
33
36
  const cmd = new Command(name);
34
37
  cmd.description('AI-powered website documentation collector');
35
- addCommonOptions(cmd.command('collect <path>').description('Crawl pages and generate documentation spec').option('--max-pages <count>', 'Maximum number of pages to document')).action(async (startPath, options) => {
38
+ addCommonOptions(cmd
39
+ .command('collect <path>')
40
+ .description('Crawl pages and generate documentation spec')
41
+ .option('--max-pages <count>', 'Maximum number of pages to document')
42
+ .option('--no-collapse-template-pages', 'Visit every page even when its layout matches a documented page')
43
+ .option('--template-similarity <percent>', 'Structural similarity percent that counts pages as the same layout (default 90)')).action(async (startPath, options) => {
36
44
  setPreserveConsoleLogs(true);
37
45
  try {
38
46
  const bot = new DocBot({
@@ -44,7 +52,11 @@ export function createDocsCommands(name = 'docs') {
44
52
  if (options.maxPages) {
45
53
  maxPages = Number.parseInt(options.maxPages, 10);
46
54
  }
47
- const result = await bot.collect(startPath, { maxPages });
55
+ let templateSimilarity;
56
+ if (options.templateSimilarity) {
57
+ templateSimilarity = Number.parseInt(options.templateSimilarity, 10);
58
+ }
59
+ const result = await bot.collect(startPath, { maxPages, collapseTemplatePages: options.collapseTemplatePages, templateSimilarity });
48
60
  console.log(`\nDocumented ${result.pages.length} page(s)`);
49
61
  console.log(`Skipped ${result.skipped.length} page(s)`);
50
62
  console.log(`Spec index: ${result.indexPath}`);
@@ -65,13 +77,14 @@ export function createDocsCommands(name = 'docs') {
65
77
  .action(async (url, options) => {
66
78
  setQuietMode(!isVerboseMode());
67
79
  try {
68
- console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
80
+ console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url: url || options.url, json: options.json }));
69
81
  }
70
82
  catch (error) {
71
83
  console.error(error instanceof Error ? error.message : 'Unknown error');
72
84
  process.exit(1);
73
85
  }
74
86
  });
87
+ RecommendedModelsCommand.register(cmd);
75
88
  cmd
76
89
  .command('init')
77
90
  .description('Initialize doc collector configuration')
@@ -99,6 +112,8 @@ export function createDocsCommands(name = 'docs') {
99
112
  interactive: false,
100
113
  ignoreErrors: true,
101
114
  collapseDynamicPages: true,
115
+ collapseTemplatePages: true,
116
+ templateSimilarity: 90,
102
117
  scope: 'site',
103
118
  includePaths: [],
104
119
  excludePaths: [],
@@ -109,6 +109,7 @@ class DocbotConfigParser {
109
109
  interactive: false,
110
110
  ignoreErrors: true,
111
111
  collapseDynamicPages: true,
112
+ collapseTemplatePages: true,
112
113
  scope: 'site',
113
114
  includePaths: [],
114
115
  excludePaths: [],