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
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, extname, join, resolve } from 'node:path';
3
3
  import chalk from 'chalk';
4
- import { ConfigParser, PROVIDERS } from "../config.js";
4
+ import { ConfigParser, PROVIDERS, missingModelRoles } from "../config.js";
5
5
  import { findGlobalConfig, globalConfigPath, globalDir, globalEnvPath } from "../global-config.js";
6
6
  import { getCliName } from "../utils/cli-name.js";
7
7
  import { log, tag } from '../utils/logger.js';
@@ -82,7 +82,7 @@ export function writeGlobalConfig(provider, apiKey) {
82
82
  const envKey = PROVIDERS[provider].envKey;
83
83
  writeEnvKey(envKey, apiKey || '');
84
84
  log(`Stored ${envKey} in ${globalEnvPath()}`);
85
- const missing = missingRoles(provider);
85
+ const missing = missingModelRoles(provider);
86
86
  if (missing.length) {
87
87
  tag('warning').log(`No recommended ${missing.join(' and ')} for ${provider} — set the model ids in ${globalConfigPath()}`);
88
88
  }
@@ -139,7 +139,7 @@ export function runInitCommand(options) {
139
139
  else {
140
140
  log(`Env file already exists: ${relativeToCwd(envPath)}`);
141
141
  }
142
- const missing = missingRoles(provider);
142
+ const missing = missingModelRoles(provider);
143
143
  if (missing.length) {
144
144
  tag('warning').log(`No recommended ${missing.join(' and ')} for ${provider} — set the model ids in ${relativeToCwd(outPath)}`);
145
145
  }
@@ -269,10 +269,6 @@ function isModuleProject(configDir) {
269
269
  currentDir = parentDir;
270
270
  }
271
271
  }
272
- function missingRoles(provider) {
273
- const recommended = ConfigParser.recommendedModels()[provider] || {};
274
- return ['model', 'visionModel', 'agenticModel'].filter((role) => !recommended[role]);
275
- }
276
272
  function writeEnvKey(key, value) {
277
273
  const envPath = globalEnvPath();
278
274
  let content = '# AI provider API keys';
@@ -0,0 +1,8 @@
1
+ import type { Command } from 'commander';
2
+ export declare abstract class BaseOption {
3
+ abstract flags: string;
4
+ abstract description: string;
5
+ collect?: (value: string, previous: any) => any;
6
+ register(program: Command): void;
7
+ abstract apply(options: Record<string, any>, command: Command): void;
8
+ }
@@ -0,0 +1,12 @@
1
+ export class BaseOption {
2
+ collect;
3
+ register(program) {
4
+ if (this.collect)
5
+ program.option(this.flags, this.description, this.collect);
6
+ if (!this.collect)
7
+ program.option(this.flags, this.description);
8
+ program.hook('preAction', (_thisCommand, actionCommand) => {
9
+ this.apply(actionCommand.optsWithGlobals(), actionCommand);
10
+ });
11
+ }
12
+ }
@@ -0,0 +1,5 @@
1
+ import { KnowledgeOption } from './knowledge-option.js';
2
+ import { WsOption } from './ws-option.js';
3
+ export { BaseOption } from './base-option.js';
4
+ export declare const knowledgeOption: KnowledgeOption;
5
+ export declare const wsOption: WsOption;
@@ -0,0 +1,5 @@
1
+ import { KnowledgeOption } from './knowledge-option.js';
2
+ import { WsOption } from './ws-option.js';
3
+ export { BaseOption } from './base-option.js';
4
+ export const knowledgeOption = new KnowledgeOption();
5
+ export const wsOption = new WsOption();
@@ -0,0 +1,7 @@
1
+ import { BaseOption } from './base-option.js';
2
+ export declare class KnowledgeOption extends BaseOption {
3
+ flags: string;
4
+ description: string;
5
+ collect: (value: string, previous?: string[]) => string[];
6
+ apply(options: Record<string, any>): void;
7
+ }
@@ -0,0 +1,12 @@
1
+ import { KnowledgeTracker } from '../../knowledge-tracker.js';
2
+ import { BaseOption } from './base-option.js';
3
+ export class KnowledgeOption extends BaseOption {
4
+ flags = '--knowledge <text>';
5
+ description = 'Knowledge for this run only, not saved to disk. Markdown text; add url: or endpoint: frontmatter to scope it, otherwise it applies everywhere. Repeatable';
6
+ collect = (value, previous = []) => [...previous, value];
7
+ apply(options) {
8
+ for (const text of options.knowledge || []) {
9
+ KnowledgeTracker.appendSessionKnowledge(text);
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,7 @@
1
+ import type { Command } from 'commander';
2
+ import { BaseOption } from './base-option.js';
3
+ export declare class WsOption extends BaseOption {
4
+ flags: string;
5
+ description: string;
6
+ apply(options: Record<string, any>, command: Command): void;
7
+ }
@@ -0,0 +1,21 @@
1
+ import { remote } from '../../remote.js';
2
+ import { BaseOption } from './base-option.js';
3
+ export class WsOption extends BaseOption {
4
+ flags = '--ws <url>';
5
+ description = 'Stream this run to a remote UI over WebSocket';
6
+ apply(options, command) {
7
+ const url = options.ws || process.env.EXPLORBOT_WS_URL;
8
+ if (!url)
9
+ return;
10
+ remote.attach(String(url), commandPath(command));
11
+ }
12
+ }
13
+ function commandPath(command) {
14
+ const parts = [];
15
+ let node = command;
16
+ while (node) {
17
+ parts.unshift(node.name());
18
+ node = node.parent;
19
+ }
20
+ return parts.slice(1).join(' ') || parts.join(' ');
21
+ }
@@ -11,7 +11,10 @@ export declare class PlansCommand extends BaseCommand {
11
11
  getPlanFiles(): PlanFile[];
12
12
  printPlans(files: PlanFile[]): void;
13
13
  printPlanDetails(plan: Plan, file: PlanFile): void;
14
- resolvePlanFile(target: string, files: PlanFile[]): PlanFile;
14
+ resolvePlanFile(target: string, files: PlanFile[]): {
15
+ plan: Plan;
16
+ file: PlanFile;
17
+ };
15
18
  }
16
19
  interface PlanFile {
17
20
  name: string;
@@ -17,8 +17,7 @@ export class PlansCommand extends BaseCommand {
17
17
  this.printPlans(files);
18
18
  return;
19
19
  }
20
- const file = this.resolvePlanFile(target, files);
21
- const plan = Plan.fromMarkdown(file.path);
20
+ const { plan, file } = this.resolvePlanFile(target, files);
22
21
  this.printPlanDetails(plan, file);
23
22
  }
24
23
  getPlanFiles() {
@@ -68,16 +67,17 @@ export class PlansCommand extends BaseCommand {
68
67
  const file = files[index - 1];
69
68
  if (!file)
70
69
  throw new Error(`Plan #${target} not found. Available: 1-${files.length}`);
71
- return file;
70
+ return { plan: Plan.fromMarkdown(file.path), file };
72
71
  }
73
72
  const plan = Plan.loadFromFile(target, this.explorBot.getPlansDir());
74
73
  if (!plan?.filePath) {
75
74
  throw new Error(`Plan file not found: ${target}`);
76
75
  }
77
- return {
76
+ const file = {
78
77
  name: path.basename(plan.filePath),
79
78
  path: plan.filePath,
80
79
  modifiedAt: statSync(plan.filePath).mtimeMs,
81
80
  };
81
+ return { plan, file };
82
82
  }
83
83
  }
@@ -0,0 +1,9 @@
1
+ import type { Command } from 'commander';
2
+ import { BaseCommand } from './base-command.js';
3
+ export declare class RecommendedModelsCommand extends BaseCommand {
4
+ name: string;
5
+ description: string;
6
+ execute(): Promise<void>;
7
+ static register(program: Command): void;
8
+ static render(json?: boolean): string;
9
+ }
@@ -0,0 +1,85 @@
1
+ import chalk from 'chalk';
2
+ import { ConfigParser, EXPLORBOT_ENV_VARS, MODEL_ROLES, PROVIDERS } from '../config.js';
3
+ import { getCliName } from '../utils/cli-name.js';
4
+ import { renderSection } from '../utils/cli-section.js';
5
+ import { tag } from '../utils/logger.js';
6
+ import { BaseCommand } from './base-command.js';
7
+ const DESCRIPTION = 'Show the models recommended for every AI provider';
8
+ const ROLES = {
9
+ model: { hint: 'fast', env: 'EXPLORBOT_AI_MODEL' },
10
+ visionModel: { hint: 'screenshots', env: 'EXPLORBOT_VISION_MODEL' },
11
+ agenticModel: { hint: 'smart', env: 'EXPLORBOT_AGENTIC_MODEL' },
12
+ };
13
+ const MODEL_ENV_VARS = MODEL_ROLES.map((role) => ROLES[role].env);
14
+ export class RecommendedModelsCommand extends BaseCommand {
15
+ name = 'recommended-models';
16
+ description = DESCRIPTION;
17
+ async execute() {
18
+ tag('info').log(RecommendedModelsCommand.render());
19
+ }
20
+ static register(program) {
21
+ program
22
+ .command('recommended-models')
23
+ .description(DESCRIPTION)
24
+ .option('--json', 'Print the recommendations as JSON')
25
+ .action((options) => console.log(RecommendedModelsCommand.render(options.json)));
26
+ }
27
+ static render(json = false) {
28
+ const recommended = ConfigParser.recommendedModels();
29
+ if (json)
30
+ return JSON.stringify(recommended, null, 2);
31
+ const lines = [
32
+ chalk.bold('Export the provider key, then pick one of two ways to select models:'),
33
+ ` ${chalk.bold('EXPLORBOT_AI_PROVIDER')} — every role takes that provider's recommendation below`,
34
+ ` ${chalk.bold(MODEL_ENV_VARS.join(' + '))} — no provider variable, pin every role yourself as ${chalk.dim('provider/model-id')}`,
35
+ '',
36
+ ];
37
+ for (const [provider, roles] of Object.entries(recommended)) {
38
+ lines.push(chalk.bold.yellow(`${provider}:`));
39
+ const models = [];
40
+ for (const role of MODEL_ROLES) {
41
+ const label = `${role} (${ROLES[role].hint})`;
42
+ if (roles[role])
43
+ models.push([label, roles[role]]);
44
+ if (!roles[role])
45
+ models.push([label, chalk.dim('not served, pair with another provider')]);
46
+ }
47
+ lines.push(...renderSection('models', models, 2));
48
+ const envKey = PROVIDERS[provider]?.envKey;
49
+ if (!envKey) {
50
+ lines.push(` ${chalk.dim('not bundled — import the provider in your config')}`, '');
51
+ continue;
52
+ }
53
+ const env = [
54
+ ['key', envKey],
55
+ ['provider', `EXPLORBOT_AI_PROVIDER=${provider}`],
56
+ ];
57
+ let label = 'or models';
58
+ for (const role of MODEL_ROLES) {
59
+ if (!roles[role])
60
+ continue;
61
+ env.push([label, `${ROLES[role].env}=${provider}/${roles[role]}`]);
62
+ label = '';
63
+ }
64
+ lines.push(...renderSection('env variables', env, 2));
65
+ }
66
+ const modelVars = ['EXPLORBOT_AI_PROVIDER', ...MODEL_ENV_VARS];
67
+ const current = [];
68
+ for (const variable of EXPLORBOT_ENV_VARS) {
69
+ if (!modelVars.includes(variable.name))
70
+ continue;
71
+ if (process.env[variable.name])
72
+ current.push([variable.name, process.env[variable.name]]);
73
+ }
74
+ for (const [provider, { envKey }] of Object.entries(PROVIDERS)) {
75
+ if (process.env[envKey])
76
+ current.push([envKey, chalk.dim(`set, ${provider} usable`)]);
77
+ }
78
+ if (!current.length)
79
+ lines.push(chalk.dim('Nothing set — export one of the variables above, or write models into your config'), '');
80
+ if (current.length)
81
+ lines.push(...renderSection('Currently set', current));
82
+ lines.push(chalk.bold('Example using OpenRouter:'), ` OPENROUTER_API_KEY=sk-... EXPLORBOT_AI_PROVIDER=openrouter ${getCliName()} <command>`);
83
+ return lines.join('\n');
84
+ }
85
+ }
@@ -1,5 +1,6 @@
1
1
  import { type SiteRecord } from './global-config.js';
2
2
  export declare const PROVIDERS: Record<string, ProviderInfo>;
3
+ export declare const MODEL_ROLES: ModelRole[];
3
4
  interface PlaywrightConfig {
4
5
  browser: 'chromium' | 'firefox' | 'webkit';
5
6
  url: string;
@@ -255,6 +256,7 @@ export declare class ConfigParser {
255
256
  static getTestDirectories(): string[];
256
257
  static cleanupAllTestDirectories(): void;
257
258
  enterGlobalMode(config: ExplorbotConfig, target: string | null): void;
259
+ applyEnvSpec(config: ExplorbotConfig): void;
258
260
  buildEnvConfig(baseUrl: string | undefined, outputRoot: string): Promise<ExplorbotConfig>;
259
261
  findConfigFile(): string | null;
260
262
  loadConfigModule(configPath: string): Promise<any>;
@@ -269,6 +271,7 @@ export declare class ConfigParser {
269
271
  }
270
272
  export declare function outputPath(...segments: string[]): string;
271
273
  export declare function resolveModel(spec: string, role?: ModelRole): Promise<any>;
274
+ export declare function missingModelRoles(provider: string): ModelRole[];
272
275
  export declare class ConfigMissingError extends Error {
273
276
  }
274
277
  export declare function envConfigRequested(): boolean;
@@ -25,6 +25,7 @@ export const PROVIDERS = {
25
25
  openrouter: { envKey: 'OPENROUTER_API_KEY', load: async () => (await import('@openrouter/ai-sdk-provider')).createOpenRouter() },
26
26
  sambanova: { envKey: 'SAMBANOVA_API_KEY', load: async () => (await import('sambanova-ai-provider')).createSambaNova() },
27
27
  };
28
+ export const MODEL_ROLES = ['model', 'visionModel', 'agenticModel'];
28
29
  let cachedOutputRoot = null;
29
30
  const config = {
30
31
  playwright: {
@@ -46,6 +47,7 @@ export const EXPLORBOT_ENV_VARS = [
46
47
  { name: 'EXPLORBOT_EPHEMERAL', description: 'Keep no state between runs — output goes to a fresh temp directory instead of the site dir' },
47
48
  { name: 'EXPLORBOT_KNOWLEDGE', description: 'Inline knowledge text, applied to every page' },
48
49
  { name: 'EXPLORBOT_KNOWLEDGE_FILE', description: 'Path to a knowledge markdown file' },
50
+ { name: 'EXPLORBOT_SPEC', description: 'Docbot application spec directory or index.md, used as page knowledge' },
49
51
  { name: 'EXPLORBOT_API_SPEC', description: 'OpenAPI spec path for the API boat' },
50
52
  { name: 'EXPLORBOT_NO_BANNER', description: 'Suppress the startup banner, for machine-readable output' },
51
53
  { name: 'EXPLORBOT_MAX_DURATION', description: 'Wall-clock budget in minutes for an explore run; same as --max-duration' },
@@ -129,6 +131,7 @@ export class ConfigParser {
129
131
  if (resolvedPath && isGlobalConfigPath(resolvedPath)) {
130
132
  this.enterGlobalMode(this.config, target);
131
133
  }
134
+ this.applyEnvSpec(this.config);
132
135
  // Restore original directory after successful config load
133
136
  if (options?.path && originalCwd !== process.cwd()) {
134
137
  process.chdir(originalCwd);
@@ -269,8 +272,17 @@ export class ConfigParser {
269
272
  this.siteStartPath = site.path;
270
273
  config.dirs = { knowledge: 'knowledge', experience: 'experience', output: 'output' };
271
274
  config.playwright = { ...config.playwright, browser: config.playwright?.browser || 'chromium', url: site.baseUrl };
275
+ materializeKnowledge(this.site.dir);
272
276
  log(`Global mode: ${site.baseUrl} stored in ${this.site.dir}`);
273
277
  }
278
+ applyEnvSpec(config) {
279
+ const spec = process.env.EXPLORBOT_SPEC;
280
+ if (!spec)
281
+ return;
282
+ if (!config.dirs)
283
+ config.dirs = { knowledge: 'knowledge', experience: 'experience', output: 'output' };
284
+ config.dirs.spec = spec;
285
+ }
274
286
  async buildEnvConfig(baseUrl, outputRoot) {
275
287
  const provider = process.env.EXPLORBOT_AI_PROVIDER;
276
288
  const modelSpec = process.env.EXPLORBOT_AI_MODEL;
@@ -447,6 +459,10 @@ export async function resolveModel(spec, role = 'model') {
447
459
  }
448
460
  return createModel(spec, modelId);
449
461
  }
462
+ export function missingModelRoles(provider) {
463
+ const recommended = ConfigParser.recommendedModels()[provider] || {};
464
+ return MODEL_ROLES.filter((role) => !recommended[role]);
465
+ }
450
466
  export class ConfigMissingError extends Error {
451
467
  }
452
468
  export function envConfigRequested() {
@@ -467,6 +483,7 @@ export function missingConfigMessage(configFile = 'explorbot.config.js') {
467
483
  EXPLORBOT_AI_PROVIDER=openrouter EXPLORBOT_URL=https://your-app.example.com ${cli} ...
468
484
 
469
485
  Providers: ${Object.keys(PROVIDERS).join(', ')}
486
+ See the models each one recommends: ${cli} recommended-models
470
487
  `;
471
488
  }
472
489
  export function modelName(model) {
@@ -500,8 +517,7 @@ export function configuredModels(ai) {
500
517
  export async function resolveConfigModels(ai) {
501
518
  if (!ai)
502
519
  return;
503
- const roles = ['model', 'visionModel', 'agenticModel'];
504
- for (const role of roles) {
520
+ for (const role of MODEL_ROLES) {
505
521
  if (typeof ai[role] === 'string')
506
522
  ai[role] = await resolveModel(ai[role], role);
507
523
  }
@@ -105,7 +105,7 @@ export class ExperienceTracker {
105
105
  url: state.url ? extractStatePath(state.url) : '',
106
106
  title: state.title,
107
107
  };
108
- if (state.overlay.present && state.overlay.name) {
108
+ if (state.overlay.isOpen && state.overlay.name) {
109
109
  frontmatter.region = state.overlay.name;
110
110
  if (state.overlay.root)
111
111
  frontmatter.root = state.overlay.root;
@@ -132,7 +132,7 @@ export class ExplorBot {
132
132
  return this.explorer;
133
133
  }
134
134
  knowledgeTracker() {
135
- return (this._knowledgeTracker ||= new KnowledgeTracker(this.options.applicationSpec));
135
+ return (this._knowledgeTracker ||= new KnowledgeTracker({ applicationSpec: this.options.applicationSpec }));
136
136
  }
137
137
  experienceTracker() {
138
138
  return (this._experienceTracker ||= new ExperienceTracker(this.knowledgeTracker()));
@@ -399,10 +399,10 @@ export class ExplorBot {
399
399
  return plan;
400
400
  }
401
401
  loadPlans(filename) {
402
- const plan = Plan.loadFromFile(filename, this.getPlansDir());
403
- if (!plan?.filePath)
402
+ const filePath = Plan.resolveFile(filename, this.getPlansDir());
403
+ if (!filePath)
404
404
  throw new Error(`Plan file not found: ${filename}`);
405
- return parsePlansFromMarkdown(plan.filePath);
405
+ return parsePlansFromMarkdown(filePath);
406
406
  }
407
407
  setCurrentPlan(plan) {
408
408
  this.currentPlan = plan;
@@ -6,6 +6,8 @@ export declare function isGlobalConfigPath(configPath: string): boolean;
6
6
  export declare function sitesDir(): string;
7
7
  export declare function siteFolderName(url: string): string;
8
8
  export declare function listSites(): SiteRecord[];
9
+ export declare function findSiteWith(subpath: string): SiteRecord | undefined;
10
+ export declare function listSitePlanDirs(): string[];
9
11
  export declare function registerSite(baseUrl: string): SiteRecord;
10
12
  export declare function resolveSiteTarget(target?: string, defaultBaseUrl?: string): SiteTarget;
11
13
  interface SiteRecord {
@@ -38,6 +38,12 @@ export function listSites() {
38
38
  .filter((site) => !!site)
39
39
  .sort((a, b) => b.lastRunAt.localeCompare(a.lastRunAt));
40
40
  }
41
+ export function findSiteWith(subpath) {
42
+ return listSites().find((site) => existsSync(join(site.dir, subpath)));
43
+ }
44
+ export function listSitePlanDirs() {
45
+ return listSites().map((site) => join(site.dir, 'output', 'plans'));
46
+ }
41
47
  export function registerSite(baseUrl) {
42
48
  const folder = siteFolderName(baseUrl);
43
49
  const dir = join(sitesDir(), folder);
@@ -1,20 +1,19 @@
1
1
  import { ActionResult } from './action-result.js';
2
2
  import { ApplicationSpec } from './application-spec.js';
3
- export interface Knowledge {
4
- filePath: string;
5
- url: string;
6
- content: string;
7
- [key: string]: any;
8
- }
9
3
  export declare class KnowledgeTracker {
10
4
  knowledgeDir: string;
11
5
  knowledgeFiles: Knowledge[];
6
+ sessionKnowledge: Knowledge[];
12
7
  isLoaded: boolean;
13
8
  applicationSpec?: ApplicationSpec;
14
- constructor(applicationSpecPath?: string);
9
+ static appendSessionKnowledge(text: string): void;
10
+ static resetSessionKnowledge(): void;
11
+ constructor(options?: KnowledgeTrackerOptions);
15
12
  loadKnowledgeFiles(): void;
16
13
  getRelevantKnowledge(state: ActionResult): Knowledge[];
14
+ getEndpointKnowledge(endpoint: string): Knowledge[];
17
15
  renderRelevantKnowledge(state: ActionResult): string;
16
+ renderEndpointKnowledge(endpoint: string): string;
18
17
  renderRelevantContext(state: ActionResult): string;
19
18
  renderApplicationSpec(state: ActionResult): string;
20
19
  addKnowledge(urlPattern: string, description: string, opts?: {
@@ -36,4 +35,18 @@ export declare class KnowledgeTracker {
36
35
  getMatchingKnowledge(url: string): Knowledge[];
37
36
  normalizeUrl(url: string): string;
38
37
  getStateParameters(state: ActionResult, keys: string[]): Record<string, any>;
38
+ allKnowledge(): Knowledge[];
39
+ toKnowledge(filePath: string, data: Record<string, any>, content: string): Knowledge;
40
+ renderKnowledge(knowledgeFiles: Knowledge[], scope: string): string;
41
+ }
42
+ export interface Knowledge {
43
+ filePath: string;
44
+ url?: string;
45
+ endpoint?: string;
46
+ content: string;
47
+ [key: string]: any;
48
+ }
49
+ export interface KnowledgeTrackerOptions {
50
+ applicationSpec?: string;
51
+ knowledgeDir?: string;
39
52
  }
@@ -11,61 +11,67 @@ import { loadMarkdownFiles } from './utils/markdown-files.js';
11
11
  import { mdq } from './utils/markdown-query.js';
12
12
  import { isSecretName, registerSecret } from './utils/secrets.js';
13
13
  import { slugify } from './utils/strings.js';
14
+ import { extractStatePath, matchesUrl } from './utils/url-matcher.js';
14
15
  const debugLog = createDebug('explorbot:knowledge-tracker');
16
+ const sessionEntries = [];
15
17
  export class KnowledgeTracker {
16
18
  knowledgeDir;
17
19
  knowledgeFiles = [];
20
+ sessionKnowledge = [];
18
21
  isLoaded = false;
19
22
  applicationSpec;
20
- constructor(applicationSpecPath) {
21
- const configParser = ConfigParser.getInstance();
22
- const config = configParser.getConfig();
23
- this.knowledgeDir = configParser.resolveProjectDir(config.dirs?.knowledge || 'knowledge');
23
+ static appendSessionKnowledge(text) {
24
+ sessionEntries.push(text);
25
+ }
26
+ static resetSessionKnowledge() {
27
+ sessionEntries.length = 0;
28
+ }
29
+ constructor(options = {}) {
30
+ let knowledgeDir = options.knowledgeDir;
31
+ let specPath = options.applicationSpec;
32
+ if (!knowledgeDir) {
33
+ const configParser = ConfigParser.getInstance();
34
+ const config = configParser.getConfig();
35
+ knowledgeDir = configParser.resolveProjectDir(config.dirs?.knowledge || 'knowledge');
36
+ specPath ||= config.dirs?.spec;
37
+ }
38
+ this.knowledgeDir = knowledgeDir;
24
39
  if (!existsSync(this.knowledgeDir)) {
25
40
  mkdirSync(this.knowledgeDir, { recursive: true });
26
41
  }
27
- const specPath = applicationSpecPath || config.dirs?.spec;
28
42
  if (specPath) {
29
43
  this.applicationSpec = new ApplicationSpec(specPath);
30
44
  tag('info').log(`Loaded application spec with ${this.applicationSpec.pageCount} documented pages`);
31
45
  }
46
+ this.sessionKnowledge = sessionEntries.map((entry, index) => {
47
+ const parsed = matter(entry);
48
+ debugLog(`Session knowledge #${index + 1}`);
49
+ return this.toKnowledge(`--knowledge #${index + 1}`, parsed.data, parsed.content.trim());
50
+ });
32
51
  }
33
52
  loadKnowledgeFiles() {
34
53
  if (this.isLoaded)
35
54
  return;
36
55
  this.knowledgeFiles = [];
37
56
  for (const entry of loadMarkdownFiles(this.knowledgeDir, { recursive: true })) {
38
- this.knowledgeFiles.push({
39
- filePath: entry.filePath,
40
- url: entry.data.url || entry.data.path || '*',
41
- content: this.interpolateVars(entry.content),
42
- ...entry.data,
43
- });
57
+ this.knowledgeFiles.push(this.toKnowledge(entry.filePath, entry.data, entry.content));
44
58
  }
45
59
  this.isLoaded = true;
46
60
  }
47
61
  getRelevantKnowledge(state) {
48
62
  this.loadKnowledgeFiles();
49
- return this.knowledgeFiles.filter((knowledge) => {
50
- return state.isMatchedBy(knowledge);
51
- });
63
+ return this.allKnowledge().filter((knowledge) => knowledge.url && state.isMatchedBy(knowledge));
64
+ }
65
+ getEndpointKnowledge(endpoint) {
66
+ this.loadKnowledgeFiles();
67
+ const path = extractStatePath(endpoint);
68
+ return this.allKnowledge().filter((knowledge) => knowledge.endpoint && matchesUrl(knowledge.endpoint, path));
52
69
  }
53
70
  renderRelevantKnowledge(state) {
54
- const knowledgeFiles = this.getRelevantKnowledge(state);
55
- if (knowledgeFiles.length === 0)
56
- return '';
57
- const knowledgeContent = knowledgeFiles
58
- .map((k) => k.content)
59
- .filter((k) => !!k)
60
- .join('\n\n');
61
- tag('operation').log(`Found ${knowledgeFiles.length} relevant knowledge ${pluralize(knowledgeFiles.length, 'file')}`);
62
- return dedent `
63
- <knowledge>
64
- Here is relevant knowledge for this page:
65
-
66
- ${knowledgeContent}
67
- </knowledge>
68
- `;
71
+ return this.renderKnowledge(this.getRelevantKnowledge(state), 'page');
72
+ }
73
+ renderEndpointKnowledge(endpoint) {
74
+ return this.renderKnowledge(this.getEndpointKnowledge(endpoint), 'endpoint');
69
75
  }
70
76
  renderRelevantContext(state) {
71
77
  return [this.renderRelevantKnowledge(state), this.renderApplicationSpec(state)].filter(Boolean).join('\n\n');
@@ -151,7 +157,7 @@ export class KnowledgeTracker {
151
157
  }
152
158
  getExistingUrls() {
153
159
  this.loadKnowledgeFiles();
154
- return this.knowledgeFiles.map((knowledge) => knowledge.url).filter((url) => url && url !== '*');
160
+ return this.knowledgeFiles.map((knowledge) => knowledge.url || '').filter((url) => url && url !== '*');
155
161
  }
156
162
  getKnowledgeForUrl(urlPattern) {
157
163
  this.loadKnowledgeFiles();
@@ -164,7 +170,7 @@ export class KnowledgeTracker {
164
170
  const content = knowledge.content.trim();
165
171
  const firstLine = mdq(content).meta()[0]?.text.split('\n')[0]?.trim() || '';
166
172
  return {
167
- url: knowledge.url,
173
+ url: knowledge.url || knowledge.endpoint || '',
168
174
  firstLine,
169
175
  filePath: knowledge.filePath,
170
176
  };
@@ -194,4 +200,36 @@ export class KnowledgeTracker {
194
200
  }
195
201
  return result;
196
202
  }
203
+ allKnowledge() {
204
+ return [...this.knowledgeFiles, ...this.sessionKnowledge];
205
+ }
206
+ toKnowledge(filePath, data, content) {
207
+ const knowledge = {
208
+ ...data,
209
+ filePath,
210
+ url: data.url || data.path,
211
+ content: this.interpolateVars(content),
212
+ };
213
+ if (!data.url && !data.path && !data.endpoint) {
214
+ knowledge.url = '*';
215
+ knowledge.endpoint = '*';
216
+ }
217
+ return knowledge;
218
+ }
219
+ renderKnowledge(knowledgeFiles, scope) {
220
+ if (knowledgeFiles.length === 0)
221
+ return '';
222
+ const knowledgeContent = knowledgeFiles
223
+ .map((k) => k.content)
224
+ .filter((k) => !!k)
225
+ .join('\n\n');
226
+ tag('operation').log(`Found ${knowledgeFiles.length} relevant knowledge ${pluralize(knowledgeFiles.length, 'file')}`);
227
+ return dedent `
228
+ <knowledge>
229
+ Here is relevant knowledge for this ${scope}:
230
+
231
+ ${knowledgeContent}
232
+ </knowledge>
233
+ `;
234
+ }
197
235
  }
@@ -1,4 +1,3 @@
1
- import type { Command } from 'commander';
2
1
  import { type ActivityEntry } from './activity.js';
3
2
  import { type LogDestination, type TaggedLogEntry } from './utils/logger.js';
4
3
  /**
@@ -19,7 +18,6 @@ export declare class Remote implements LogDestination {
19
18
  asks: Map<string, (value: string | null) => void>;
20
19
  askCounter: number;
21
20
  lastActivity: string | null;
22
- registerOption(program: Command): void;
23
21
  attach(url: string, command: string): void;
24
22
  isAttached(): boolean;
25
23
  send(type: string, data?: Record<string, unknown>): void;
@@ -42,7 +40,6 @@ export declare class Remote implements LogDestination {
42
40
  * nothing — so skip repeats, the priming null included. */
43
41
  reportActivity(activity: ActivityEntry | null): void;
44
42
  errorOf(args: any[] | undefined): string | undefined;
45
- commandPath(command: Command): string;
46
43
  }
47
44
  export declare const remote: Remote;
48
45
  /** Whatever the run wants to say. Not a schema — the UI renders what it knows