explorbot 0.2.0 → 0.2.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 (145) hide show
  1. package/README.md +21 -5
  2. package/bin/explorbot-cli.ts +29 -7
  3. package/boat/api-tester/src/config.ts +45 -3
  4. package/boat/doc-collector/src/ai/documentarian.ts +8 -2
  5. package/boat/doc-collector/src/ai/tools.ts +29 -16
  6. package/boat/doc-collector/src/cli.ts +2 -6
  7. package/boat/doc-collector/src/config.ts +2 -0
  8. package/boat/doc-collector/src/docbot.ts +42 -14
  9. package/boat/doc-collector/src/interaction-screenshots.ts +160 -0
  10. package/boat/doc-collector/src/screenshots.ts +24 -16
  11. package/dist/bin/explorbot-cli.js +26 -6
  12. package/dist/boat/api-tester/src/config.js +43 -4
  13. package/dist/boat/doc-collector/src/ai/documentarian.js +7 -2
  14. package/dist/boat/doc-collector/src/ai/tools.js +21 -15
  15. package/dist/boat/doc-collector/src/cli.js +2 -5
  16. package/dist/boat/doc-collector/src/config.js +1 -0
  17. package/dist/boat/doc-collector/src/docbot.js +39 -13
  18. package/dist/boat/doc-collector/src/interaction-screenshots.js +156 -0
  19. package/dist/boat/doc-collector/src/screenshots.js +25 -16
  20. package/dist/models.json +30 -0
  21. package/dist/package.json +8 -2
  22. package/dist/src/action.d.ts +11 -3
  23. package/dist/src/action.js +22 -5
  24. package/dist/src/ai/agent.d.ts +17 -0
  25. package/dist/src/ai/captain/idle-mode.js +1 -1
  26. package/dist/src/ai/captain/test-mode.js +1 -1
  27. package/dist/src/ai/captain/web-mode.js +18 -22
  28. package/dist/src/ai/captain.js +13 -26
  29. package/dist/src/ai/driller.d.ts +2 -13
  30. package/dist/src/ai/driller.js +23 -38
  31. package/dist/src/ai/historian/codeceptjs.js +1 -1
  32. package/dist/src/ai/historian/playwright.d.ts +3 -2
  33. package/dist/src/ai/historian/playwright.js +1 -1
  34. package/dist/src/ai/historian/screencast.d.ts +2 -1
  35. package/dist/src/ai/historian/screencast.js +2 -2
  36. package/dist/src/ai/historian.d.ts +5 -4
  37. package/dist/src/ai/navigator.d.ts +6 -2
  38. package/dist/src/ai/navigator.js +26 -22
  39. package/dist/src/ai/pilot.d.ts +13 -6
  40. package/dist/src/ai/pilot.js +49 -44
  41. package/dist/src/ai/planner.d.ts +3 -2
  42. package/dist/src/ai/planner.js +19 -17
  43. package/dist/src/ai/provider.js +0 -1
  44. package/dist/src/ai/quartermaster.d.ts +3 -2
  45. package/dist/src/ai/quartermaster.js +4 -4
  46. package/dist/src/ai/rerunner.d.ts +2 -11
  47. package/dist/src/ai/rerunner.js +9 -22
  48. package/dist/src/ai/researcher/coordinates.js +4 -3
  49. package/dist/src/ai/researcher/deep-analysis.d.ts +2 -0
  50. package/dist/src/ai/researcher/deep-analysis.js +6 -6
  51. package/dist/src/ai/researcher/locators.d.ts +9 -2
  52. package/dist/src/ai/researcher/locators.js +65 -8
  53. package/dist/src/ai/researcher/sections.d.ts +2 -0
  54. package/dist/src/ai/researcher/sections.js +2 -2
  55. package/dist/src/ai/researcher.js +29 -54
  56. package/dist/src/ai/task-agent.d.ts +18 -5
  57. package/dist/src/ai/task-agent.js +35 -6
  58. package/dist/src/ai/tester.d.ts +6 -12
  59. package/dist/src/ai/tester.js +37 -51
  60. package/dist/src/ai/tools.d.ts +11 -10
  61. package/dist/src/ai/tools.js +24 -36
  62. package/dist/src/command-handler.js +1 -1
  63. package/dist/src/commands/context-aria-command.js +1 -1
  64. package/dist/src/commands/context-command.js +7 -5
  65. package/dist/src/commands/context-data-command.js +2 -2
  66. package/dist/src/commands/context-experience-command.js +2 -2
  67. package/dist/src/commands/context-html-command.js +2 -2
  68. package/dist/src/commands/context-knowledge-command.js +1 -1
  69. package/dist/src/commands/drill-command.js +1 -1
  70. package/dist/src/commands/explore-command.js +4 -3
  71. package/dist/src/commands/freesail-command.js +2 -2
  72. package/dist/src/commands/learn-command.js +4 -4
  73. package/dist/src/commands/path-command.js +1 -1
  74. package/dist/src/commands/research-command.js +1 -1
  75. package/dist/src/commands/test-command.js +1 -1
  76. package/dist/src/components/App.js +1 -1
  77. package/dist/src/config.d.ts +17 -0
  78. package/dist/src/config.js +144 -10
  79. package/dist/src/experience-tracker.js +1 -1
  80. package/dist/src/explorbot.d.ts +17 -8
  81. package/dist/src/explorbot.js +74 -36
  82. package/dist/src/explorer.d.ts +67 -91
  83. package/dist/src/explorer.js +327 -567
  84. package/dist/src/state-manager.d.ts +6 -1
  85. package/dist/src/state-manager.js +1 -0
  86. package/dist/src/utils/hooks-runner.js +2 -4
  87. package/dist/src/utils/test-files.js +1 -1
  88. package/dist/src/utils/web-annotate.d.ts +5 -0
  89. package/dist/src/utils/web-annotate.js +58 -0
  90. package/dist/src/utils/web-eidx.d.ts +2 -0
  91. package/dist/src/utils/web-eidx.js +20 -0
  92. package/dist/src/utils/web-element.d.ts +1 -0
  93. package/dist/src/utils/web-element.js +29 -0
  94. package/models.json +30 -0
  95. package/package.json +8 -2
  96. package/src/action.ts +25 -5
  97. package/src/ai/agent.ts +20 -0
  98. package/src/ai/captain/idle-mode.ts +1 -1
  99. package/src/ai/captain/test-mode.ts +1 -1
  100. package/src/ai/captain/web-mode.ts +18 -23
  101. package/src/ai/captain.ts +12 -25
  102. package/src/ai/driller.ts +50 -67
  103. package/src/ai/historian/codeceptjs.ts +1 -1
  104. package/src/ai/historian/playwright.ts +4 -3
  105. package/src/ai/historian/screencast.ts +4 -3
  106. package/src/ai/historian.ts +3 -2
  107. package/src/ai/navigator.ts +30 -25
  108. package/src/ai/pilot.ts +55 -47
  109. package/src/ai/planner.ts +22 -20
  110. package/src/ai/provider.ts +0 -1
  111. package/src/ai/quartermaster.ts +5 -4
  112. package/src/ai/rerunner.ts +10 -29
  113. package/src/ai/researcher/coordinates.ts +4 -3
  114. package/src/ai/researcher/deep-analysis.ts +8 -6
  115. package/src/ai/researcher/locators.ts +66 -9
  116. package/src/ai/researcher/sections.ts +4 -2
  117. package/src/ai/researcher.ts +32 -61
  118. package/src/ai/task-agent.ts +45 -11
  119. package/src/ai/tester.ts +40 -57
  120. package/src/ai/tools.ts +32 -53
  121. package/src/command-handler.ts +1 -1
  122. package/src/commands/context-aria-command.ts +1 -1
  123. package/src/commands/context-command.ts +7 -5
  124. package/src/commands/context-data-command.ts +2 -2
  125. package/src/commands/context-experience-command.ts +2 -2
  126. package/src/commands/context-html-command.ts +2 -2
  127. package/src/commands/context-knowledge-command.ts +1 -1
  128. package/src/commands/drill-command.ts +1 -1
  129. package/src/commands/explore-command.ts +4 -3
  130. package/src/commands/freesail-command.ts +2 -2
  131. package/src/commands/learn-command.ts +4 -4
  132. package/src/commands/path-command.ts +1 -1
  133. package/src/commands/research-command.ts +1 -1
  134. package/src/commands/test-command.ts +1 -1
  135. package/src/components/App.tsx +1 -1
  136. package/src/config.ts +173 -11
  137. package/src/experience-tracker.ts +1 -1
  138. package/src/explorbot.ts +78 -39
  139. package/src/explorer.ts +388 -626
  140. package/src/state-manager.ts +7 -1
  141. package/src/utils/hooks-runner.ts +2 -4
  142. package/src/utils/test-files.ts +1 -1
  143. package/src/utils/web-annotate.ts +64 -0
  144. package/src/utils/web-eidx.ts +21 -0
  145. package/src/utils/web-element.ts +30 -0
@@ -10,14 +10,14 @@ export class ContextExperienceCommand extends BaseCommand {
10
10
 
11
11
  async execute(_args: string): Promise<void> {
12
12
  const explorer = this.explorBot.getExplorer();
13
- const state = explorer.getStateManager().getCurrentState();
13
+ const state = this.explorBot.stateManager().getCurrentState();
14
14
 
15
15
  if (!state) {
16
16
  throw new Error('No active page');
17
17
  }
18
18
 
19
19
  const actionResult = ActionResult.fromState(state);
20
- const experienceTracker = explorer.getStateManager().getExperienceTracker();
20
+ const experienceTracker = this.explorBot.experienceTracker();
21
21
  const experience = experienceTracker.getRelevantExperience(actionResult);
22
22
 
23
23
  if (experience.length === 0) {
@@ -8,7 +8,7 @@ export class ContextHtmlCommand extends BaseCommand {
8
8
 
9
9
  async execute(_args: string): Promise<void> {
10
10
  const explorer = this.explorBot.getExplorer();
11
- const manager = explorer.getStateManager();
11
+ const manager = this.explorBot.stateManager();
12
12
  const state = manager.getCurrentState();
13
13
 
14
14
  if (!state) {
@@ -19,7 +19,7 @@ export class ContextHtmlCommand extends BaseCommand {
19
19
 
20
20
  if (!actionResult.html || actionResult.html.trim().length < 100) {
21
21
  tag('info').log('Capturing fresh page content...');
22
- actionResult = await explorer.createAction().capturePageState();
22
+ actionResult = await explorer.capture();
23
23
  }
24
24
 
25
25
  const html = await actionResult.combinedHtml();
@@ -10,7 +10,7 @@ export class ContextKnowledgeCommand extends BaseCommand {
10
10
 
11
11
  async execute(_args: string): Promise<void> {
12
12
  const explorer = this.explorBot.getExplorer();
13
- const state = explorer.getStateManager().getCurrentState();
13
+ const state = this.explorBot.stateManager().getCurrentState();
14
14
 
15
15
  if (!state) {
16
16
  throw new Error('No active page');
@@ -13,7 +13,7 @@ export class DrillCommand extends BaseCommand {
13
13
  const knowledgePath = this.parseKnowledgeArg(args);
14
14
  const maxComponents = this.parseMaxArg(args);
15
15
 
16
- const state = this.explorBot.getExplorer().getStateManager().getCurrentState();
16
+ const state = this.explorBot.stateManager().getCurrentState();
17
17
  if (!state) {
18
18
  throw new Error('No active page to drill');
19
19
  }
@@ -4,6 +4,7 @@ import { outputPath } from '../config.js';
4
4
  import { normalizeUrl } from '../state-manager.js';
5
5
  import { Stats } from '../stats.js';
6
6
  import { type Plan, type Test, TestResult } from '../test-plan.js';
7
+ import { browserErrorMessage } from '../utils/browser-errors.ts';
7
8
  import { getCliName } from '../utils/cli-name.ts';
8
9
  import { ErrorPageError, getStateErrorPageError } from '../utils/error-page.ts';
9
10
  import { tag } from '../utils/logger.js';
@@ -38,7 +39,7 @@ export class ExploreCommand extends BaseCommand {
38
39
  private priorityFilter?: Set<string>;
39
40
 
40
41
  private getCurrentPageUrl(): string | undefined {
41
- const state = this.explorBot.getExplorer().getStateManager().getCurrentState();
42
+ const state = this.explorBot.stateManager().getCurrentState();
42
43
  return state?.fullUrl || state?.url;
43
44
  }
44
45
 
@@ -56,7 +57,7 @@ export class ExploreCommand extends BaseCommand {
56
57
  Stats.mode ??= 'explore';
57
58
  Stats.focus ??= feature;
58
59
  const mainUrl = this.getCurrentPageUrl();
59
- const error = getStateErrorPageError(this.explorBot.getExplorer().getStateManager().getCurrentState());
60
+ const error = getStateErrorPageError(this.explorBot.stateManager().getCurrentState());
60
61
  if (error) {
61
62
  tag('warning').log(error.message);
62
63
  return;
@@ -74,7 +75,7 @@ export class ExploreCommand extends BaseCommand {
74
75
  this.printResults();
75
76
  return;
76
77
  }
77
- if (mainUrl) await this.explorBot.visit(mainUrl);
78
+ if (mainUrl) await this.explorBot.visit(mainUrl).catch((err) => tag('warning').log(`Could not return to ${mainUrl}: ${browserErrorMessage(err)}`));
78
79
  const savedPath = this.explorBot.savePlans(this.completedPlans);
79
80
  this.printResults();
80
81
  await this.explorBot.printSessionAnalysis();
@@ -35,7 +35,7 @@ export class FreesailCommand extends BaseCommand {
35
35
  async (ctx) => {
36
36
  if (maxTests != null && testsRun >= maxTests) ctx.stop();
37
37
 
38
- const stateManager = this.explorBot.getExplorer().getStateManager();
38
+ const stateManager = this.explorBot.stateManager();
39
39
  const state = stateManager.getCurrentState();
40
40
 
41
41
  if (state && !Researcher.getCachedResearch(state)) {
@@ -70,7 +70,7 @@ export class FreesailCommand extends BaseCommand {
70
70
  }
71
71
 
72
72
  tag('info').log(`Navigating to: ${suggestion.target} - ${suggestion.reason}`);
73
- await this.explorBot.openFreshTab();
73
+ await this.explorBot.openTab();
74
74
  await this.explorBot.visit(suggestion.target);
75
75
  this.explorBot.clearPlan();
76
76
  },
@@ -14,13 +14,13 @@ export class LearnCommand extends BaseCommand {
14
14
  if (!note) {
15
15
  const AddKnowledge = (await import('../components/AddKnowledge.js')).default;
16
16
  const explorer = this.explorBot.getExplorer();
17
- const state = explorer.getStateManager().getCurrentState();
17
+ const state = this.explorBot.stateManager().getCurrentState();
18
18
  const initialUrl = state?.url || '';
19
19
 
20
20
  const { unmount } = render(
21
21
  React.createElement(AddKnowledge, {
22
22
  initialUrl,
23
- knowledgeTracker: explorer.getKnowledgeTracker(),
23
+ knowledgeTracker: this.explorBot.knowledgeTracker(),
24
24
  onComplete: () => unmount(),
25
25
  onCancel: () => unmount(),
26
26
  }),
@@ -33,14 +33,14 @@ export class LearnCommand extends BaseCommand {
33
33
  }
34
34
 
35
35
  const explorer = this.explorBot.getExplorer();
36
- const state = explorer.getStateManager().getCurrentState();
36
+ const state = this.explorBot.stateManager().getCurrentState();
37
37
 
38
38
  if (!state) {
39
39
  throw new Error('No active page to attach knowledge');
40
40
  }
41
41
 
42
42
  const targetUrl = state.url || state.fullUrl || '/';
43
- explorer.getKnowledgeTracker().addKnowledge(targetUrl, note);
43
+ this.explorBot.knowledgeTracker().addKnowledge(targetUrl, note);
44
44
  tag('success').log('Knowledge saved for current page');
45
45
  }
46
46
  }
@@ -16,7 +16,7 @@ export class PathCommand extends BaseCommand {
16
16
  async execute(args: string): Promise<void> {
17
17
  const { opts } = this.parseArgs(args);
18
18
  const showLinks = !!opts.links;
19
- const stateManager = this.explorBot.getExplorer().getStateManager();
19
+ const stateManager = this.explorBot.stateManager();
20
20
  const history = stateManager.getStateHistory();
21
21
 
22
22
  if (history.length === 0) {
@@ -27,7 +27,7 @@ export class ResearchCommand extends BaseCommand {
27
27
  await this.explorBot.agentNavigator().visit(target);
28
28
  }
29
29
 
30
- const state = this.explorBot.getExplorer().getStateManager().getCurrentState();
30
+ const state = this.explorBot.stateManager().getCurrentState();
31
31
  if (!state) {
32
32
  throw new Error('No active page to research');
33
33
  }
@@ -48,7 +48,7 @@ export class TestCommand extends BaseCommand {
48
48
  if (matching.length > 0) {
49
49
  toExecute.push(...matching);
50
50
  } else {
51
- const state = this.explorBot.getExplorer().getStateManager().getCurrentState();
51
+ const state = this.explorBot.stateManager().getCurrentState();
52
52
  if (!state) {
53
53
  throw new Error('No page loaded. Please navigate to a page first.');
54
54
  }
@@ -68,7 +68,7 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
68
68
  });
69
69
  });
70
70
 
71
- const manager = explorBot.getExplorer().getStateManager();
71
+ const manager = explorBot.stateManager();
72
72
 
73
73
  unsubscribe = manager.onStateChange((transition: StateTransition) => {
74
74
  if (mounted) {
package/src/config.ts CHANGED
@@ -1,8 +1,22 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
2
- import path, { dirname, join, resolve } from 'node:path';
1
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import path, { basename, dirname, join, resolve } from 'node:path';
3
4
  import { parseEnv } from 'node:util';
5
+ import matter from 'gray-matter';
4
6
  import { log } from './utils/logger.js';
5
7
 
8
+ export const PROVIDERS: Record<string, () => Promise<(modelId: string) => any>> = {
9
+ openai: async () => (await import('@ai-sdk/openai')).createOpenAI(),
10
+ anthropic: async () => (await import('@ai-sdk/anthropic')).createAnthropic(),
11
+ google: async () => (await import('@ai-sdk/google')).createGoogleGenerativeAI(),
12
+ groq: async () => (await import('@ai-sdk/groq')).createGroq(),
13
+ mistral: async () => (await import('@ai-sdk/mistral')).createMistral(),
14
+ openrouter: async () => (await import('@openrouter/ai-sdk-provider')).createOpenRouter(),
15
+ sambanova: async () => (await import('sambanova-ai-provider')).createSambaNova(),
16
+ };
17
+
18
+ let cachedOutputRoot: string | null = null;
19
+
6
20
  interface PlaywrightConfig {
7
21
  browser: 'chromium' | 'firefox' | 'webkit';
8
22
  url: string;
@@ -216,6 +230,7 @@ interface ExplorbotConfig {
216
230
  };
217
231
  experience?: {
218
232
  maxReadLines?: number;
233
+ disabled?: boolean;
219
234
  };
220
235
  reporter?: ReporterConfig;
221
236
  api?: ApiConfig;
@@ -239,6 +254,19 @@ type RuleEntry = string | Record<string, string>;
239
254
 
240
255
  export const EXPLORBOT_CONFIG_PATHS = ['explorbot.config.js', 'explorbot.config.mjs', 'explorbot.config.ts'];
241
256
 
257
+ export const EXPLORBOT_ENV_VARS: EnvVar[] = [
258
+ { name: 'EXPLORBOT_AI_PROVIDER', required: true, description: 'Provider name; fills every model role from its recommended models. Turns on config-free mode' },
259
+ { name: 'EXPLORBOT_AI_MODEL', description: 'Pins the main model — a model id for the provider, or a standalone provider/model-id' },
260
+ { name: 'EXPLORBOT_URL', required: true, description: 'Base URL to test; the API boat reads it as the base endpoint' },
261
+ { name: 'EXPLORBOT_VISION_MODEL', description: 'Screenshot analysis; overrides the provider recommendation' },
262
+ { name: 'EXPLORBOT_AGENTIC_MODEL', description: 'Captain and Pilot decisions; overrides the provider recommendation' },
263
+ { name: 'EXPLORBOT_OUTPUT', description: 'Output root for states, plans, research, and reports. Defaults to a fresh temp directory' },
264
+ { name: 'EXPLORBOT_KNOWLEDGE', description: 'Inline knowledge text, applied to every page' },
265
+ { name: 'EXPLORBOT_KNOWLEDGE_FILE', description: 'Path to a knowledge markdown file' },
266
+ { name: 'EXPLORBOT_API_SPEC', description: 'OpenAPI spec path for the API boat' },
267
+ { name: 'EXPLORBOT_NO_BANNER', description: 'Suppress the startup banner, for machine-readable output' },
268
+ ];
269
+
242
270
  export type {
243
271
  ExplorbotConfig,
244
272
  PlaywrightConfig,
@@ -268,6 +296,7 @@ export type {
268
296
 
269
297
  export class ConfigParser {
270
298
  private static instance: ConfigParser;
299
+ private static recommended: Record<string, Record<string, string>> | null = null;
271
300
  private config: ExplorbotConfig | null = null;
272
301
  private configPath: string | null = null;
273
302
  private runtimeBaseUrlOverride: string | null = null;
@@ -280,6 +309,11 @@ export class ConfigParser {
280
309
  Object.assign(process.env, parseEnv(readFileSync(resolved, 'utf8')));
281
310
  }
282
311
 
312
+ public static recommendedModels(): Record<string, Record<string, string>> {
313
+ ConfigParser.recommended ||= JSON.parse(readFileSync(new URL('../models.json', import.meta.url), 'utf8'));
314
+ return ConfigParser.recommended!;
315
+ }
316
+
283
317
  public static getInstance(): ConfigParser {
284
318
  if (!ConfigParser.instance) {
285
319
  ConfigParser.instance = new ConfigParser();
@@ -312,22 +346,31 @@ export class ConfigParser {
312
346
  try {
313
347
  const resolvedPath = options?.config || this.findConfigFile();
314
348
 
315
- if (!resolvedPath) {
316
- throw new Error('No configuration file found. Please create explorbot.config.js or explorbot.config.ts');
349
+ let loadedConfig: ExplorbotConfig | null = null;
350
+ let sourcePath = resolvedPath;
351
+
352
+ if (resolvedPath) {
353
+ const configModule = await this.loadConfigModule(resolvedPath);
354
+ loadedConfig = configModule.default || configModule;
355
+
356
+ if (!loadedConfig) {
357
+ throw new Error('Configuration file is empty or invalid');
358
+ }
359
+
360
+ log(`Configuration loaded from: ${resolvedPath}`);
317
361
  }
318
362
 
319
- const configModule = await this.loadConfigModule(resolvedPath);
320
- const loadedConfig = configModule.default || configModule;
363
+ if (!resolvedPath) {
364
+ const outputRoot = resolveOutputRoot();
365
+ loadedConfig = await this.buildEnvConfig(options?.baseUrl, outputRoot);
366
+ sourcePath = join(outputRoot, 'explorbot.config.js');
321
367
 
322
- if (!loadedConfig) {
323
- throw new Error('Configuration file is empty or invalid');
368
+ log(`Configuration built from EXPLORBOT_* environment variables. Output: ${outputRoot}`);
324
369
  }
325
370
 
326
371
  this.config = this.resolveConfig(loadedConfig as ExplorbotConfig, options);
327
372
  this.runtimeBaseUrlOverride = options?.baseUrl || null;
328
- this.configPath = resolvedPath;
329
-
330
- log(`Configuration loaded from: ${resolvedPath}`);
373
+ this.configPath = sourcePath;
331
374
 
332
375
  // Restore original directory after successful config load
333
376
  if (options?.path && originalCwd !== process.cwd()) {
@@ -388,6 +431,7 @@ export class ConfigParser {
388
431
 
389
432
  // For testing purposes only
390
433
  public static resetForTesting(): void {
434
+ cachedOutputRoot = null;
391
435
  if (ConfigParser.instance) {
392
436
  ConfigParser.instance.config = null;
393
437
  ConfigParser.instance.configPath = null;
@@ -442,6 +486,52 @@ export class ConfigParser {
442
486
  }
443
487
  }
444
488
 
489
+ private async buildEnvConfig(baseUrl: string | undefined, outputRoot: string): Promise<ExplorbotConfig> {
490
+ const provider = process.env.EXPLORBOT_AI_PROVIDER;
491
+ const modelSpec = process.env.EXPLORBOT_AI_MODEL;
492
+ if (!provider && !modelSpec) {
493
+ throw new Error('No configuration file found. Please create explorbot.config.js or set EXPLORBOT_URL and EXPLORBOT_AI_PROVIDER environment variables');
494
+ }
495
+ if (modelSpec && !provider && !modelSpec.includes('/')) {
496
+ throw new Error('EXPLORBOT_AI_MODEL needs a provider — set EXPLORBOT_AI_PROVIDER, or write it as "provider/model-id"');
497
+ }
498
+
499
+ const url = process.env.EXPLORBOT_URL || baseUrl;
500
+ if (!url) {
501
+ throw new Error('No URL to explore. Set EXPLORBOT_URL or pass a URL to the command');
502
+ }
503
+
504
+ materializeKnowledge(outputRoot);
505
+
506
+ let model: any;
507
+ if (provider && modelSpec) model = await createModel(provider, modelSpec);
508
+ if (provider && !modelSpec) model = await resolveModel(provider, 'model');
509
+ if (!provider) model = await resolveModel(modelSpec!, 'model');
510
+
511
+ const ai: AIConfig = {
512
+ model,
513
+ agents: { historian: { enabled: false } },
514
+ };
515
+
516
+ let recommended: Record<string, string> = {};
517
+ if (provider) recommended = ConfigParser.recommendedModels()[provider] || {};
518
+
519
+ const visionSpec = process.env.EXPLORBOT_VISION_MODEL;
520
+ if (visionSpec) ai.visionModel = await resolveModel(visionSpec, 'visionModel');
521
+ if (!visionSpec && recommended.visionModel) ai.visionModel = await resolveModel(provider!, 'visionModel');
522
+
523
+ const agenticSpec = process.env.EXPLORBOT_AGENTIC_MODEL;
524
+ if (agenticSpec) ai.agenticModel = await resolveModel(agenticSpec, 'agenticModel');
525
+ if (!agenticSpec && recommended.agenticModel) ai.agenticModel = await resolveModel(provider!, 'agenticModel');
526
+
527
+ return {
528
+ playwright: { browser: 'chromium', url, show: false },
529
+ ai,
530
+ dirs: { knowledge: 'knowledge', experience: 'experience', output: '.' },
531
+ experience: { disabled: true },
532
+ };
533
+ }
534
+
445
535
  private findConfigFile(): string | null {
446
536
  const possiblePaths = [...EXPLORBOT_CONFIG_PATHS, 'config/explorbot.config.js', 'config/explorbot.config.mjs', 'config/explorbot.config.ts', 'src/config/explorbot.config.js', 'src/config/explorbot.config.mjs', 'src/config/explorbot.config.ts'];
447
537
 
@@ -554,3 +644,75 @@ export class ConfigParser {
554
644
  export function outputPath(...segments: string[]): string {
555
645
  return path.join(ConfigParser.getInstance().getOutputDir(), ...segments);
556
646
  }
647
+
648
+ export async function resolveModel(spec: string, role: ModelRole = 'model'): Promise<any> {
649
+ const separator = spec.indexOf('/');
650
+ if (separator > 0) {
651
+ return createModel(spec.slice(0, separator), spec.slice(separator + 1));
652
+ }
653
+
654
+ const recommended = ConfigParser.recommendedModels()[spec];
655
+ if (!recommended) {
656
+ throw new Error(`No recommended models for "${spec}". Write it as "provider/model-id", or use a provider with recommendations: ${Object.keys(ConfigParser.recommendedModels()).join(', ')}`);
657
+ }
658
+
659
+ const modelId = recommended[role];
660
+ if (!modelId) {
661
+ throw new Error(`Provider "${spec}" has no recommended ${role}. Set it explicitly as "provider/model-id".`);
662
+ }
663
+
664
+ return createModel(spec, modelId);
665
+ }
666
+
667
+ export function resolveOutputRoot(): string {
668
+ if (cachedOutputRoot) return cachedOutputRoot;
669
+
670
+ const configured = process.env.EXPLORBOT_OUTPUT;
671
+ if (!configured) {
672
+ cachedOutputRoot = mkdtempSync(join(tmpdir(), 'explorbot-'));
673
+ return cachedOutputRoot;
674
+ }
675
+
676
+ cachedOutputRoot = resolve(configured);
677
+ mkdirSync(cachedOutputRoot, { recursive: true });
678
+ return cachedOutputRoot;
679
+ }
680
+
681
+ export function materializeKnowledge(outputRoot: string): void {
682
+ const inline = process.env.EXPLORBOT_KNOWLEDGE;
683
+ const knowledgeFile = process.env.EXPLORBOT_KNOWLEDGE_FILE;
684
+ if (!inline && !knowledgeFile) return;
685
+
686
+ const knowledgeDir = join(outputRoot, 'knowledge');
687
+ mkdirSync(knowledgeDir, { recursive: true });
688
+
689
+ if (inline) {
690
+ writeFileSync(join(knowledgeDir, 'global.md'), matter.stringify(inline, { url: '*', endpoint: '*' }));
691
+ }
692
+
693
+ if (!knowledgeFile) return;
694
+
695
+ const source = resolve(knowledgeFile);
696
+ if (!existsSync(source)) {
697
+ throw new Error(`Knowledge file from EXPLORBOT_KNOWLEDGE_FILE not found: ${source}`);
698
+ }
699
+ copyFileSync(source, join(knowledgeDir, basename(source)));
700
+ }
701
+
702
+ export async function createModel(provider: string, modelId: string): Promise<any> {
703
+ const factory = PROVIDERS[provider];
704
+ if (!factory) {
705
+ throw new Error(`Unknown AI provider "${provider}". Supported providers: ${Object.keys(PROVIDERS).join(', ')}`);
706
+ }
707
+ return (await factory())(modelId);
708
+ }
709
+
710
+ type ModelRole = 'model' | 'visionModel' | 'agenticModel';
711
+
712
+ interface EnvVar {
713
+ name: string;
714
+ description: string;
715
+ required?: boolean;
716
+ }
717
+
718
+ export type { ModelRole, EnvVar };
@@ -42,7 +42,7 @@ export class ExperienceTracker {
42
42
  constructor(knowledgeTracker: KnowledgeTracker, options: { disabled?: boolean } = {}) {
43
43
  const configParser = ConfigParser.getInstance();
44
44
  const config = configParser.getConfig();
45
- this.disabled = options.disabled ?? false;
45
+ this.disabled = options.disabled ?? config.experience?.disabled ?? false;
46
46
  this.knowledgeTracker = knowledgeTracker;
47
47
 
48
48
  // Resolve experience directory relative to the config file location (project root)