explorbot 0.2.0 → 0.2.1
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.
- package/README.md +21 -5
- package/bin/explorbot-cli.ts +29 -7
- package/boat/api-tester/src/config.ts +45 -3
- package/boat/doc-collector/src/ai/documentarian.ts +5 -2
- package/boat/doc-collector/src/ai/tools.ts +12 -11
- package/boat/doc-collector/src/cli.ts +1 -6
- package/boat/doc-collector/src/docbot.ts +3 -3
- package/boat/doc-collector/src/screenshots.ts +2 -2
- package/dist/bin/explorbot-cli.js +26 -6
- package/dist/boat/api-tester/src/config.js +43 -4
- package/dist/boat/doc-collector/src/ai/documentarian.js +4 -2
- package/dist/boat/doc-collector/src/ai/tools.js +11 -11
- package/dist/boat/doc-collector/src/cli.js +1 -5
- package/dist/boat/doc-collector/src/docbot.js +3 -3
- package/dist/boat/doc-collector/src/screenshots.js +2 -2
- package/dist/models.json +30 -0
- package/dist/package.json +5 -2
- package/dist/src/action.d.ts +11 -3
- package/dist/src/action.js +22 -5
- package/dist/src/ai/agent.d.ts +17 -0
- package/dist/src/ai/captain/idle-mode.js +1 -1
- package/dist/src/ai/captain/test-mode.js +1 -1
- package/dist/src/ai/captain/web-mode.js +18 -22
- package/dist/src/ai/captain.js +13 -26
- package/dist/src/ai/driller.d.ts +2 -13
- package/dist/src/ai/driller.js +23 -38
- package/dist/src/ai/historian/codeceptjs.js +1 -1
- package/dist/src/ai/historian/playwright.d.ts +3 -2
- package/dist/src/ai/historian/playwright.js +1 -1
- package/dist/src/ai/historian/screencast.d.ts +2 -1
- package/dist/src/ai/historian/screencast.js +2 -2
- package/dist/src/ai/historian.d.ts +5 -4
- package/dist/src/ai/navigator.d.ts +6 -2
- package/dist/src/ai/navigator.js +26 -22
- package/dist/src/ai/pilot.d.ts +13 -6
- package/dist/src/ai/pilot.js +49 -44
- package/dist/src/ai/planner.d.ts +3 -2
- package/dist/src/ai/planner.js +19 -17
- package/dist/src/ai/provider.js +0 -1
- package/dist/src/ai/quartermaster.d.ts +3 -2
- package/dist/src/ai/quartermaster.js +4 -4
- package/dist/src/ai/rerunner.d.ts +2 -11
- package/dist/src/ai/rerunner.js +9 -22
- package/dist/src/ai/researcher/coordinates.js +4 -3
- package/dist/src/ai/researcher/deep-analysis.d.ts +2 -0
- package/dist/src/ai/researcher/deep-analysis.js +6 -6
- package/dist/src/ai/researcher/locators.d.ts +9 -2
- package/dist/src/ai/researcher/locators.js +65 -8
- package/dist/src/ai/researcher/sections.d.ts +2 -0
- package/dist/src/ai/researcher/sections.js +2 -2
- package/dist/src/ai/researcher.js +29 -54
- package/dist/src/ai/task-agent.d.ts +18 -5
- package/dist/src/ai/task-agent.js +35 -6
- package/dist/src/ai/tester.d.ts +6 -12
- package/dist/src/ai/tester.js +37 -51
- package/dist/src/ai/tools.d.ts +11 -10
- package/dist/src/ai/tools.js +24 -36
- package/dist/src/command-handler.js +1 -1
- package/dist/src/commands/context-aria-command.js +1 -1
- package/dist/src/commands/context-command.js +7 -5
- package/dist/src/commands/context-data-command.js +2 -2
- package/dist/src/commands/context-experience-command.js +2 -2
- package/dist/src/commands/context-html-command.js +2 -2
- package/dist/src/commands/context-knowledge-command.js +1 -1
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/explore-command.js +4 -3
- package/dist/src/commands/freesail-command.js +2 -2
- package/dist/src/commands/learn-command.js +4 -4
- package/dist/src/commands/path-command.js +1 -1
- package/dist/src/commands/research-command.js +1 -1
- package/dist/src/commands/test-command.js +1 -1
- package/dist/src/components/App.js +1 -1
- package/dist/src/config.d.ts +17 -0
- package/dist/src/config.js +144 -10
- package/dist/src/experience-tracker.js +1 -1
- package/dist/src/explorbot.d.ts +17 -8
- package/dist/src/explorbot.js +74 -36
- package/dist/src/explorer.d.ts +66 -91
- package/dist/src/explorer.js +326 -567
- package/dist/src/state-manager.d.ts +6 -1
- package/dist/src/state-manager.js +1 -0
- package/dist/src/utils/hooks-runner.js +2 -4
- package/dist/src/utils/test-files.js +1 -1
- package/dist/src/utils/web-annotate.d.ts +5 -0
- package/dist/src/utils/web-annotate.js +58 -0
- package/dist/src/utils/web-eidx.d.ts +2 -0
- package/dist/src/utils/web-eidx.js +20 -0
- package/dist/src/utils/web-element.d.ts +1 -0
- package/dist/src/utils/web-element.js +29 -0
- package/models.json +30 -0
- package/package.json +5 -2
- package/src/action.ts +25 -5
- package/src/ai/agent.ts +20 -0
- package/src/ai/captain/idle-mode.ts +1 -1
- package/src/ai/captain/test-mode.ts +1 -1
- package/src/ai/captain/web-mode.ts +18 -23
- package/src/ai/captain.ts +12 -25
- package/src/ai/driller.ts +50 -67
- package/src/ai/historian/codeceptjs.ts +1 -1
- package/src/ai/historian/playwright.ts +4 -3
- package/src/ai/historian/screencast.ts +4 -3
- package/src/ai/historian.ts +3 -2
- package/src/ai/navigator.ts +30 -25
- package/src/ai/pilot.ts +55 -47
- package/src/ai/planner.ts +22 -20
- package/src/ai/provider.ts +0 -1
- package/src/ai/quartermaster.ts +5 -4
- package/src/ai/rerunner.ts +10 -29
- package/src/ai/researcher/coordinates.ts +4 -3
- package/src/ai/researcher/deep-analysis.ts +8 -6
- package/src/ai/researcher/locators.ts +66 -9
- package/src/ai/researcher/sections.ts +4 -2
- package/src/ai/researcher.ts +32 -61
- package/src/ai/task-agent.ts +45 -11
- package/src/ai/tester.ts +40 -57
- package/src/ai/tools.ts +32 -53
- package/src/command-handler.ts +1 -1
- package/src/commands/context-aria-command.ts +1 -1
- package/src/commands/context-command.ts +7 -5
- package/src/commands/context-data-command.ts +2 -2
- package/src/commands/context-experience-command.ts +2 -2
- package/src/commands/context-html-command.ts +2 -2
- package/src/commands/context-knowledge-command.ts +1 -1
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/explore-command.ts +4 -3
- package/src/commands/freesail-command.ts +2 -2
- package/src/commands/learn-command.ts +4 -4
- package/src/commands/path-command.ts +1 -1
- package/src/commands/research-command.ts +1 -1
- package/src/commands/test-command.ts +1 -1
- package/src/components/App.tsx +1 -1
- package/src/config.ts +173 -11
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +78 -39
- package/src/explorer.ts +389 -628
- package/src/state-manager.ts +7 -1
- package/src/utils/hooks-runner.ts +2 -4
- package/src/utils/test-files.ts +1 -1
- package/src/utils/web-annotate.ts +64 -0
- package/src/utils/web-eidx.ts +21 -0
- 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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
}
|
package/src/components/App.tsx
CHANGED
|
@@ -68,7 +68,7 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
const manager = explorBot.
|
|
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
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
|
|
320
|
-
|
|
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
|
-
|
|
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 =
|
|
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)
|