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
@@ -6,7 +6,7 @@ export class ContextHtmlCommand extends BaseCommand {
6
6
  description = 'Print combined HTML snapshot for current page';
7
7
  async execute(_args) {
8
8
  const explorer = this.explorBot.getExplorer();
9
- const manager = explorer.getStateManager();
9
+ const manager = this.explorBot.stateManager();
10
10
  const state = manager.getCurrentState();
11
11
  if (!state) {
12
12
  throw new Error('No active page to snapshot');
@@ -14,7 +14,7 @@ export class ContextHtmlCommand extends BaseCommand {
14
14
  let actionResult = ActionResult.fromState(state);
15
15
  if (!actionResult.html || actionResult.html.trim().length < 100) {
16
16
  tag('info').log('Capturing fresh page content...');
17
- actionResult = await explorer.createAction().capturePageState();
17
+ actionResult = await explorer.capture();
18
18
  }
19
19
  const html = await actionResult.combinedHtml();
20
20
  if (!html) {
@@ -8,7 +8,7 @@ export class ContextKnowledgeCommand extends BaseCommand {
8
8
  description = 'Print all matching knowledge for current page';
9
9
  async execute(_args) {
10
10
  const explorer = this.explorBot.getExplorer();
11
- const state = explorer.getStateManager().getCurrentState();
11
+ const state = this.explorBot.stateManager().getCurrentState();
12
12
  if (!state) {
13
13
  throw new Error('No active page');
14
14
  }
@@ -10,7 +10,7 @@ export class DrillCommand extends BaseCommand {
10
10
  async execute(args) {
11
11
  const knowledgePath = this.parseKnowledgeArg(args);
12
12
  const maxComponents = this.parseMaxArg(args);
13
- const state = this.explorBot.getExplorer().getStateManager().getCurrentState();
13
+ const state = this.explorBot.stateManager().getCurrentState();
14
14
  if (!state) {
15
15
  throw new Error('No active page to drill');
16
16
  }
@@ -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 { TestResult } from '../test-plan.js';
7
+ import { browserErrorMessage } from "../utils/browser-errors.js";
7
8
  import { getCliName } from "../utils/cli-name.js";
8
9
  import { ErrorPageError, getStateErrorPageError } from "../utils/error-page.js";
9
10
  import { tag } from '../utils/logger.js';
@@ -34,7 +35,7 @@ export class ExploreCommand extends BaseCommand {
34
35
  oldTestRefs = new Set();
35
36
  priorityFilter;
36
37
  getCurrentPageUrl() {
37
- const state = this.explorBot.getExplorer().getStateManager().getCurrentState();
38
+ const state = this.explorBot.stateManager().getCurrentState();
38
39
  return state?.fullUrl || state?.url;
39
40
  }
40
41
  async execute(args) {
@@ -53,7 +54,7 @@ export class ExploreCommand extends BaseCommand {
53
54
  Stats.mode ??= 'explore';
54
55
  Stats.focus ??= feature;
55
56
  const mainUrl = this.getCurrentPageUrl();
56
- const error = getStateErrorPageError(this.explorBot.getExplorer().getStateManager().getCurrentState());
57
+ const error = getStateErrorPageError(this.explorBot.stateManager().getCurrentState());
57
58
  if (error) {
58
59
  tag('warning').log(error.message);
59
60
  return;
@@ -72,7 +73,7 @@ export class ExploreCommand extends BaseCommand {
72
73
  return;
73
74
  }
74
75
  if (mainUrl)
75
- await this.explorBot.visit(mainUrl);
76
+ await this.explorBot.visit(mainUrl).catch((err) => tag('warning').log(`Could not return to ${mainUrl}: ${browserErrorMessage(err)}`));
76
77
  const savedPath = this.explorBot.savePlans(this.completedPlans);
77
78
  this.printResults();
78
79
  await this.explorBot.printSessionAnalysis();
@@ -31,7 +31,7 @@ export class FreesailCommand extends BaseCommand {
31
31
  await loop(async (ctx) => {
32
32
  if (maxTests != null && testsRun >= maxTests)
33
33
  ctx.stop();
34
- const stateManager = this.explorBot.getExplorer().getStateManager();
34
+ const stateManager = this.explorBot.stateManager();
35
35
  const state = stateManager.getCurrentState();
36
36
  if (state && !Researcher.getCachedResearch(state)) {
37
37
  await this.explorBot.agentResearcher().research(state, { deep: true, screenshot: true });
@@ -63,7 +63,7 @@ export class FreesailCommand extends BaseCommand {
63
63
  return;
64
64
  }
65
65
  tag('info').log(`Navigating to: ${suggestion.target} - ${suggestion.reason}`);
66
- await this.explorBot.openFreshTab();
66
+ await this.explorBot.openTab();
67
67
  await this.explorBot.visit(suggestion.target);
68
68
  this.explorBot.clearPlan();
69
69
  }, { maxAttempts: Number.POSITIVE_INFINITY });
@@ -11,11 +11,11 @@ export class LearnCommand extends BaseCommand {
11
11
  if (!note) {
12
12
  const AddKnowledge = (await import('../components/AddKnowledge.js')).default;
13
13
  const explorer = this.explorBot.getExplorer();
14
- const state = explorer.getStateManager().getCurrentState();
14
+ const state = this.explorBot.stateManager().getCurrentState();
15
15
  const initialUrl = state?.url || '';
16
16
  const { unmount } = render(React.createElement(AddKnowledge, {
17
17
  initialUrl,
18
- knowledgeTracker: explorer.getKnowledgeTracker(),
18
+ knowledgeTracker: this.explorBot.knowledgeTracker(),
19
19
  onComplete: () => unmount(),
20
20
  onCancel: () => unmount(),
21
21
  }), {
@@ -25,12 +25,12 @@ export class LearnCommand extends BaseCommand {
25
25
  return;
26
26
  }
27
27
  const explorer = this.explorBot.getExplorer();
28
- const state = explorer.getStateManager().getCurrentState();
28
+ const state = this.explorBot.stateManager().getCurrentState();
29
29
  if (!state) {
30
30
  throw new Error('No active page to attach knowledge');
31
31
  }
32
32
  const targetUrl = state.url || state.fullUrl || '/';
33
- explorer.getKnowledgeTracker().addKnowledge(targetUrl, note);
33
+ this.explorBot.knowledgeTracker().addKnowledge(targetUrl, note);
34
34
  tag('success').log('Knowledge saved for current page');
35
35
  }
36
36
  }
@@ -7,7 +7,7 @@ export class PathCommand extends BaseCommand {
7
7
  async execute(args) {
8
8
  const { opts } = this.parseArgs(args);
9
9
  const showLinks = !!opts.links;
10
- const stateManager = this.explorBot.getExplorer().getStateManager();
10
+ const stateManager = this.explorBot.stateManager();
11
11
  const history = stateManager.getStateHistory();
12
12
  if (history.length === 0) {
13
13
  tag('multiline').log('No navigation history yet.');
@@ -23,7 +23,7 @@ export class ResearchCommand extends BaseCommand {
23
23
  if (target) {
24
24
  await this.explorBot.agentNavigator().visit(target);
25
25
  }
26
- const state = this.explorBot.getExplorer().getStateManager().getCurrentState();
26
+ const state = this.explorBot.stateManager().getCurrentState();
27
27
  if (!state) {
28
28
  throw new Error('No active page to research');
29
29
  }
@@ -48,7 +48,7 @@ export class TestCommand extends BaseCommand {
48
48
  toExecute.push(...matching);
49
49
  }
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
  }
@@ -47,7 +47,7 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
47
47
  userInputPromiseRef.current = { resolve, reject };
48
48
  });
49
49
  });
50
- const manager = explorBot.getExplorer().getStateManager();
50
+ const manager = explorBot.stateManager();
51
51
  unsubscribe = manager.onStateChange((transition) => {
52
52
  if (mounted) {
53
53
  setLastTransition(transition);
@@ -1,3 +1,4 @@
1
+ export declare const PROVIDERS: Record<string, () => Promise<(modelId: string) => any>>;
1
2
  interface PlaywrightConfig {
2
3
  browser: 'chromium' | 'firefox' | 'webkit';
3
4
  url: string;
@@ -204,6 +205,7 @@ interface ExplorbotConfig {
204
205
  };
205
206
  experience?: {
206
207
  maxReadLines?: number;
208
+ disabled?: boolean;
207
209
  };
208
210
  reporter?: ReporterConfig;
209
211
  api?: ApiConfig;
@@ -213,14 +215,17 @@ interface ExplorbotConfig {
213
215
  }
214
216
  type RuleEntry = string | Record<string, string>;
215
217
  export declare const EXPLORBOT_CONFIG_PATHS: string[];
218
+ export declare const EXPLORBOT_ENV_VARS: EnvVar[];
216
219
  export type { ExplorbotConfig, PlaywrightConfig, AIConfig, HtmlConfig, ActionConfig, AgentConfig, AgentsConfig, HistorianAgentConfig, ResearcherAgentConfig, NavigatorAgentConfig, PlannerAgentConfig, RerunnerAgentConfig, HealRecipe, Hook, HookConfig, HooksConfig, PlaywrightHook, CodeceptJSHook, HookPatternMap, RuleEntry, ReporterConfig, ApiConfig, WebConfig, ApiHookFn, };
217
220
  export declare class ConfigParser {
218
221
  static instance: ConfigParser;
222
+ static recommended: Record<string, Record<string, string>> | null;
219
223
  config: ExplorbotConfig | null;
220
224
  configPath: string | null;
221
225
  runtimeBaseUrlOverride: string | null;
222
226
  constructor();
223
227
  static loadEnv(filePath: string): void;
228
+ static recommendedModels(): Record<string, Record<string, string>>;
224
229
  static getInstance(): ConfigParser;
225
230
  loadConfig(options?: {
226
231
  config?: string;
@@ -239,6 +244,7 @@ export declare class ConfigParser {
239
244
  static setupTestConfig(): void;
240
245
  static getTestDirectories(): string[];
241
246
  static cleanupAllTestDirectories(): void;
247
+ buildEnvConfig(baseUrl: string | undefined, outputRoot: string): Promise<ExplorbotConfig>;
242
248
  findConfigFile(): string | null;
243
249
  loadConfigModule(configPath: string): Promise<any>;
244
250
  resolveConfig(config: ExplorbotConfig, options?: {
@@ -251,3 +257,14 @@ export declare class ConfigParser {
251
257
  ensureDirectory(path: string): void;
252
258
  }
253
259
  export declare function outputPath(...segments: string[]): string;
260
+ export declare function resolveModel(spec: string, role?: ModelRole): Promise<any>;
261
+ export declare function resolveOutputRoot(): string;
262
+ export declare function materializeKnowledge(outputRoot: string): void;
263
+ export declare function createModel(provider: string, modelId: string): Promise<any>;
264
+ type ModelRole = 'model' | 'visionModel' | 'agenticModel';
265
+ interface EnvVar {
266
+ name: string;
267
+ description: string;
268
+ required?: boolean;
269
+ }
270
+ export type { ModelRole, EnvVar };
@@ -6,10 +6,22 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
- import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
10
- import path, { dirname, join, resolve } from 'node:path';
9
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import path, { basename, dirname, join, resolve } from 'node:path';
11
12
  import { parseEnv } from 'node:util';
13
+ import matter from 'gray-matter';
12
14
  import { log } from './utils/logger.js';
15
+ export const PROVIDERS = {
16
+ openai: async () => (await import('@ai-sdk/openai')).createOpenAI(),
17
+ anthropic: async () => (await import('@ai-sdk/anthropic')).createAnthropic(),
18
+ google: async () => (await import('@ai-sdk/google')).createGoogleGenerativeAI(),
19
+ groq: async () => (await import('@ai-sdk/groq')).createGroq(),
20
+ mistral: async () => (await import('@ai-sdk/mistral')).createMistral(),
21
+ openrouter: async () => (await import('@openrouter/ai-sdk-provider')).createOpenRouter(),
22
+ sambanova: async () => (await import('sambanova-ai-provider')).createSambaNova(),
23
+ };
24
+ let cachedOutputRoot = null;
13
25
  const config = {
14
26
  playwright: {
15
27
  browser: 'chromium',
@@ -20,8 +32,21 @@ const config = {
20
32
  },
21
33
  };
22
34
  export const EXPLORBOT_CONFIG_PATHS = ['explorbot.config.js', 'explorbot.config.mjs', 'explorbot.config.ts'];
35
+ export const EXPLORBOT_ENV_VARS = [
36
+ { name: 'EXPLORBOT_AI_PROVIDER', required: true, description: 'Provider name; fills every model role from its recommended models. Turns on config-free mode' },
37
+ { name: 'EXPLORBOT_AI_MODEL', description: 'Pins the main model — a model id for the provider, or a standalone provider/model-id' },
38
+ { name: 'EXPLORBOT_URL', required: true, description: 'Base URL to test; the API boat reads it as the base endpoint' },
39
+ { name: 'EXPLORBOT_VISION_MODEL', description: 'Screenshot analysis; overrides the provider recommendation' },
40
+ { name: 'EXPLORBOT_AGENTIC_MODEL', description: 'Captain and Pilot decisions; overrides the provider recommendation' },
41
+ { name: 'EXPLORBOT_OUTPUT', description: 'Output root for states, plans, research, and reports. Defaults to a fresh temp directory' },
42
+ { name: 'EXPLORBOT_KNOWLEDGE', description: 'Inline knowledge text, applied to every page' },
43
+ { name: 'EXPLORBOT_KNOWLEDGE_FILE', description: 'Path to a knowledge markdown file' },
44
+ { name: 'EXPLORBOT_API_SPEC', description: 'OpenAPI spec path for the API boat' },
45
+ { name: 'EXPLORBOT_NO_BANNER', description: 'Suppress the startup banner, for machine-readable output' },
46
+ ];
23
47
  export class ConfigParser {
24
48
  static instance;
49
+ static recommended = null;
25
50
  config = null;
26
51
  configPath = null;
27
52
  runtimeBaseUrlOverride = null;
@@ -32,6 +57,10 @@ export class ConfigParser {
32
57
  return;
33
58
  Object.assign(process.env, parseEnv(readFileSync(resolved, 'utf8')));
34
59
  }
60
+ static recommendedModels() {
61
+ ConfigParser.recommended ||= JSON.parse(readFileSync(new URL('../models.json', import.meta.url), 'utf8'));
62
+ return ConfigParser.recommended;
63
+ }
35
64
  static getInstance() {
36
65
  if (!ConfigParser.instance) {
37
66
  ConfigParser.instance = new ConfigParser();
@@ -54,18 +83,25 @@ export class ConfigParser {
54
83
  ConfigParser.loadEnv('.env');
55
84
  try {
56
85
  const resolvedPath = options?.config || this.findConfigFile();
57
- if (!resolvedPath) {
58
- throw new Error('No configuration file found. Please create explorbot.config.js or explorbot.config.ts');
86
+ let loadedConfig = null;
87
+ let sourcePath = resolvedPath;
88
+ if (resolvedPath) {
89
+ const configModule = await this.loadConfigModule(resolvedPath);
90
+ loadedConfig = configModule.default || configModule;
91
+ if (!loadedConfig) {
92
+ throw new Error('Configuration file is empty or invalid');
93
+ }
94
+ log(`Configuration loaded from: ${resolvedPath}`);
59
95
  }
60
- const configModule = await this.loadConfigModule(resolvedPath);
61
- const loadedConfig = configModule.default || configModule;
62
- if (!loadedConfig) {
63
- throw new Error('Configuration file is empty or invalid');
96
+ if (!resolvedPath) {
97
+ const outputRoot = resolveOutputRoot();
98
+ loadedConfig = await this.buildEnvConfig(options?.baseUrl, outputRoot);
99
+ sourcePath = join(outputRoot, 'explorbot.config.js');
100
+ log(`Configuration built from EXPLORBOT_* environment variables. Output: ${outputRoot}`);
64
101
  }
65
102
  this.config = this.resolveConfig(loadedConfig, options);
66
103
  this.runtimeBaseUrlOverride = options?.baseUrl || null;
67
- this.configPath = resolvedPath;
68
- log(`Configuration loaded from: ${resolvedPath}`);
104
+ this.configPath = sourcePath;
69
105
  // Restore original directory after successful config load
70
106
  if (options?.path && originalCwd !== process.cwd()) {
71
107
  process.chdir(originalCwd);
@@ -119,6 +155,7 @@ export class ConfigParser {
119
155
  }
120
156
  // For testing purposes only
121
157
  static resetForTesting() {
158
+ cachedOutputRoot = null;
122
159
  if (ConfigParser.instance) {
123
160
  ConfigParser.instance.config = null;
124
161
  ConfigParser.instance.configPath = null;
@@ -169,6 +206,51 @@ export class ConfigParser {
169
206
  // Ignore cleanup errors
170
207
  }
171
208
  }
209
+ async buildEnvConfig(baseUrl, outputRoot) {
210
+ const provider = process.env.EXPLORBOT_AI_PROVIDER;
211
+ const modelSpec = process.env.EXPLORBOT_AI_MODEL;
212
+ if (!provider && !modelSpec) {
213
+ throw new Error('No configuration file found. Please create explorbot.config.js or set EXPLORBOT_URL and EXPLORBOT_AI_PROVIDER environment variables');
214
+ }
215
+ if (modelSpec && !provider && !modelSpec.includes('/')) {
216
+ throw new Error('EXPLORBOT_AI_MODEL needs a provider — set EXPLORBOT_AI_PROVIDER, or write it as "provider/model-id"');
217
+ }
218
+ const url = process.env.EXPLORBOT_URL || baseUrl;
219
+ if (!url) {
220
+ throw new Error('No URL to explore. Set EXPLORBOT_URL or pass a URL to the command');
221
+ }
222
+ materializeKnowledge(outputRoot);
223
+ let model;
224
+ if (provider && modelSpec)
225
+ model = await createModel(provider, modelSpec);
226
+ if (provider && !modelSpec)
227
+ model = await resolveModel(provider, 'model');
228
+ if (!provider)
229
+ model = await resolveModel(modelSpec, 'model');
230
+ const ai = {
231
+ model,
232
+ agents: { historian: { enabled: false } },
233
+ };
234
+ let recommended = {};
235
+ if (provider)
236
+ recommended = ConfigParser.recommendedModels()[provider] || {};
237
+ const visionSpec = process.env.EXPLORBOT_VISION_MODEL;
238
+ if (visionSpec)
239
+ ai.visionModel = await resolveModel(visionSpec, 'visionModel');
240
+ if (!visionSpec && recommended.visionModel)
241
+ ai.visionModel = await resolveModel(provider, 'visionModel');
242
+ const agenticSpec = process.env.EXPLORBOT_AGENTIC_MODEL;
243
+ if (agenticSpec)
244
+ ai.agenticModel = await resolveModel(agenticSpec, 'agenticModel');
245
+ if (!agenticSpec && recommended.agenticModel)
246
+ ai.agenticModel = await resolveModel(provider, 'agenticModel');
247
+ return {
248
+ playwright: { browser: 'chromium', url, show: false },
249
+ ai,
250
+ dirs: { knowledge: 'knowledge', experience: 'experience', output: '.' },
251
+ experience: { disabled: true },
252
+ };
253
+ }
172
254
  findConfigFile() {
173
255
  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'];
174
256
  for (const path of possiblePaths) {
@@ -268,3 +350,55 @@ export class ConfigParser {
268
350
  export function outputPath(...segments) {
269
351
  return path.join(ConfigParser.getInstance().getOutputDir(), ...segments);
270
352
  }
353
+ export async function resolveModel(spec, role = 'model') {
354
+ const separator = spec.indexOf('/');
355
+ if (separator > 0) {
356
+ return createModel(spec.slice(0, separator), spec.slice(separator + 1));
357
+ }
358
+ const recommended = ConfigParser.recommendedModels()[spec];
359
+ if (!recommended) {
360
+ 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(', ')}`);
361
+ }
362
+ const modelId = recommended[role];
363
+ if (!modelId) {
364
+ throw new Error(`Provider "${spec}" has no recommended ${role}. Set it explicitly as "provider/model-id".`);
365
+ }
366
+ return createModel(spec, modelId);
367
+ }
368
+ export function resolveOutputRoot() {
369
+ if (cachedOutputRoot)
370
+ return cachedOutputRoot;
371
+ const configured = process.env.EXPLORBOT_OUTPUT;
372
+ if (!configured) {
373
+ cachedOutputRoot = mkdtempSync(join(tmpdir(), 'explorbot-'));
374
+ return cachedOutputRoot;
375
+ }
376
+ cachedOutputRoot = resolve(configured);
377
+ mkdirSync(cachedOutputRoot, { recursive: true });
378
+ return cachedOutputRoot;
379
+ }
380
+ export function materializeKnowledge(outputRoot) {
381
+ const inline = process.env.EXPLORBOT_KNOWLEDGE;
382
+ const knowledgeFile = process.env.EXPLORBOT_KNOWLEDGE_FILE;
383
+ if (!inline && !knowledgeFile)
384
+ return;
385
+ const knowledgeDir = join(outputRoot, 'knowledge');
386
+ mkdirSync(knowledgeDir, { recursive: true });
387
+ if (inline) {
388
+ writeFileSync(join(knowledgeDir, 'global.md'), matter.stringify(inline, { url: '*', endpoint: '*' }));
389
+ }
390
+ if (!knowledgeFile)
391
+ return;
392
+ const source = resolve(knowledgeFile);
393
+ if (!existsSync(source)) {
394
+ throw new Error(`Knowledge file from EXPLORBOT_KNOWLEDGE_FILE not found: ${source}`);
395
+ }
396
+ copyFileSync(source, join(knowledgeDir, basename(source)));
397
+ }
398
+ export async function createModel(provider, modelId) {
399
+ const factory = PROVIDERS[provider];
400
+ if (!factory) {
401
+ throw new Error(`Unknown AI provider "${provider}". Supported providers: ${Object.keys(PROVIDERS).join(', ')}`);
402
+ }
403
+ return (await factory())(modelId);
404
+ }
@@ -35,7 +35,7 @@ export class ExperienceTracker {
35
35
  constructor(knowledgeTracker, options = {}) {
36
36
  const configParser = ConfigParser.getInstance();
37
37
  const config = configParser.getConfig();
38
- this.disabled = options.disabled ?? false;
38
+ this.disabled = options.disabled ?? config.experience?.disabled ?? false;
39
39
  this.knowledgeTracker = knowledgeTracker;
40
40
  // Resolve experience directory relative to the config file location (project root)
41
41
  this.experienceDir = configParser.resolveProjectDir(config.dirs?.experience || 'experience');
@@ -1,3 +1,4 @@
1
+ import type { AgentDeps } from './ai/agent.js';
1
2
  import { Captain } from './ai/captain.js';
2
3
  import { Driller } from './ai/driller.js';
3
4
  import { ExperienceCompactor } from './ai/experience-compactor.js';
@@ -12,12 +13,15 @@ import { Rerunner } from './ai/rerunner.js';
12
13
  import { Researcher } from './ai/researcher.js';
13
14
  import { SessionAnalyst } from './ai/session-analyst.js';
14
15
  import { Tester } from './ai/tester.js';
16
+ import { RequestStore } from './api/request-store.js';
15
17
  import type { ExplorbotConfig } from './config.js';
16
18
  import { ConfigParser } from './config.js';
17
19
  import { ExperienceTracker } from './experience-tracker.js';
18
20
  import Explorer from './explorer.js';
19
21
  import { KnowledgeTracker } from './knowledge-tracker.js';
20
- import { WebPageState } from './state-manager.js';
22
+ import { PlaywrightRecorder } from './playwright-recorder.js';
23
+ import { Reporter } from './reporter.js';
24
+ import { StateManager, WebPageState } from './state-manager.js';
21
25
  import type { Suite } from './suite.js';
22
26
  import { Plan, type Test } from './test-plan.js';
23
27
  export interface ExplorBotOptions {
@@ -29,7 +33,7 @@ export interface ExplorBotOptions {
29
33
  show?: boolean;
30
34
  headless?: boolean;
31
35
  incognito?: boolean;
32
- session?: string;
36
+ session?: string | boolean;
33
37
  }
34
38
  export type UserResolveFunction = (error?: Error, showWelcome?: boolean) => Promise<string | null>;
35
39
  export declare class ExplorBot {
@@ -48,6 +52,10 @@ export declare class ExplorBot {
48
52
  lastReportedTestCount: number;
49
53
  _experienceTracker?: ExperienceTracker;
50
54
  _knowledgeTracker?: KnowledgeTracker;
55
+ _stateManager?: StateManager;
56
+ _reporter?: Reporter;
57
+ _requestStore?: RequestStore;
58
+ _playwrightRecorder?: PlaywrightRecorder;
51
59
  constructor(options?: ExplorBotOptions);
52
60
  get isExploring(): boolean;
53
61
  setUserResolve(fn: UserResolveFunction): void;
@@ -56,19 +64,19 @@ export declare class ExplorBot {
56
64
  stop(): Promise<void>;
57
65
  visitInitialState(): Promise<void>;
58
66
  visit(url: string): Promise<void>;
59
- openFreshTab(): Promise<void>;
67
+ openTab(): Promise<void>;
60
68
  getCurrentState(): WebPageState | null;
61
69
  getExplorer(): Explorer;
62
70
  knowledgeTracker(): KnowledgeTracker;
63
71
  experienceTracker(): ExperienceTracker;
72
+ stateManager(): StateManager;
73
+ reporter(): Reporter;
74
+ requestStore(): RequestStore;
75
+ playwrightRecorder(): PlaywrightRecorder;
64
76
  getConfig(): ExplorbotConfig;
65
77
  getOptions(): ExplorBotOptions;
66
78
  getProvider(): AIProvider;
67
- createAgent<T>(factory: (deps: {
68
- explorer: Explorer;
69
- ai: AIProvider;
70
- config: ExplorbotConfig;
71
- }) => T): T;
79
+ createAgent<T>(factory: (deps: AgentDeps) => T): T;
72
80
  agentResearcher(): Researcher;
73
81
  agentNavigator(): Navigator;
74
82
  agentPlanner(): Planner;
@@ -103,4 +111,5 @@ export declare class ExplorBot {
103
111
  setCurrentPlan(plan?: Plan): void;
104
112
  getSessionTests(): Test[];
105
113
  printSessionAnalysis(): Promise<void>;
114
+ isHistorianEnabled(): boolean;
106
115
  }