explorbot 0.1.32-beta.1 → 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 +35 -19
- package/bin/explorbot-cli.ts +33 -10
- package/boat/api-tester/src/config.ts +45 -3
- package/boat/doc-collector/src/ai/documentarian.ts +26 -28
- package/boat/doc-collector/src/ai/tools.ts +74 -16
- package/boat/doc-collector/src/cli.ts +1 -6
- package/boat/doc-collector/src/docbot.ts +30 -9
- package/boat/doc-collector/src/docs-renderer.ts +19 -24
- package/boat/doc-collector/src/screenshots.ts +34 -2
- package/boat/doc-collector/src/state-diagram.ts +281 -0
- package/dist/bin/explorbot-cli.js +29 -9
- package/dist/boat/api-tester/src/config.js +43 -4
- package/dist/boat/doc-collector/src/ai/documentarian.js +19 -7
- package/dist/boat/doc-collector/src/ai/tools.js +53 -16
- package/dist/boat/doc-collector/src/cli.js +1 -5
- package/dist/boat/doc-collector/src/docbot.js +27 -8
- package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
- package/dist/boat/doc-collector/src/screenshots.js +30 -1
- package/dist/boat/doc-collector/src/state-diagram.js +200 -0
- package/dist/models.json +30 -0
- package/dist/package.json +23 -3
- package/dist/src/action-result.d.ts +8 -10
- package/dist/src/action-result.js +72 -164
- package/dist/src/action.d.ts +13 -12
- package/dist/src/action.js +37 -113
- 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 -29
- package/dist/src/ai/captain.d.ts +1 -3
- package/dist/src/ai/captain.js +17 -45
- package/dist/src/ai/conversation.d.ts +2 -5
- package/dist/src/ai/conversation.js +9 -28
- package/dist/src/ai/driller.d.ts +2 -17
- package/dist/src/ai/driller.js +26 -50
- package/dist/src/ai/experience-compactor.d.ts +2 -2
- package/dist/src/ai/experience-compactor.js +16 -33
- package/dist/src/ai/fisherman-tools.js +1 -1
- package/dist/src/ai/fisherman.d.ts +0 -1
- package/dist/src/ai/fisherman.js +0 -3
- package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
- package/dist/src/ai/historian/codeceptjs.js +4 -3
- package/dist/src/ai/historian/playwright.d.ts +4 -1
- package/dist/src/ai/historian/playwright.js +4 -3
- package/dist/src/ai/historian/screencast.d.ts +2 -1
- package/dist/src/ai/historian/screencast.js +2 -2
- package/dist/src/ai/historian/utils.d.ts +0 -1
- package/dist/src/ai/historian/utils.js +0 -1
- package/dist/src/ai/historian.d.ts +10 -8
- package/dist/src/ai/historian.js +1 -5
- package/dist/src/ai/navigator.d.ts +8 -6
- package/dist/src/ai/navigator.js +34 -65
- package/dist/src/ai/pilot.d.ts +13 -8
- package/dist/src/ai/pilot.js +53 -59
- package/dist/src/ai/planner.d.ts +5 -4
- package/dist/src/ai/planner.js +66 -52
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +77 -137
- package/dist/src/ai/quartermaster.d.ts +22 -22
- package/dist/src/ai/quartermaster.js +8 -14
- package/dist/src/ai/rerunner.d.ts +3 -13
- package/dist/src/ai/rerunner.js +23 -44
- package/dist/src/ai/researcher/cache.js +11 -18
- 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 +8 -11
- package/dist/src/ai/researcher/locators.d.ts +9 -2
- package/dist/src/ai/researcher/locators.js +65 -8
- package/dist/src/ai/researcher/parser.js +1 -1
- package/dist/src/ai/researcher/research-result.d.ts +0 -1
- package/dist/src/ai/researcher/research-result.js +0 -19
- package/dist/src/ai/researcher/sections.d.ts +2 -0
- package/dist/src/ai/researcher/sections.js +7 -3
- package/dist/src/ai/researcher.js +40 -106
- package/dist/src/ai/rules.d.ts +1 -0
- package/dist/src/ai/rules.js +22 -15
- package/dist/src/ai/session-analyst.js +8 -5
- package/dist/src/ai/task-agent.d.ts +19 -6
- package/dist/src/ai/task-agent.js +37 -33
- package/dist/src/ai/tester.d.ts +6 -14
- package/dist/src/ai/tester.js +46 -111
- package/dist/src/ai/tools.d.ts +26 -14
- package/dist/src/ai/tools.js +138 -132
- package/dist/src/command-handler.js +1 -1
- package/dist/src/commands/compact-command.d.ts +1 -1
- package/dist/src/commands/compact-command.js +6 -6
- package/dist/src/commands/context-aria-command.js +1 -1
- package/dist/src/commands/context-command.js +8 -6
- 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 +2 -2
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/experience-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/knows-command.js +1 -1
- package/dist/src/commands/learn-command.js +5 -3
- 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/AddKnowledge.d.ts +2 -0
- package/dist/src/components/AddKnowledge.js +3 -7
- package/dist/src/components/App.js +3 -3
- package/dist/src/config.d.ts +19 -0
- package/dist/src/config.js +156 -10
- package/dist/src/experience-tracker.d.ts +11 -6
- package/dist/src/experience-tracker.js +82 -155
- package/dist/src/explorbot.d.ts +21 -13
- package/dist/src/explorbot.js +84 -80
- package/dist/src/explorer.d.ts +66 -102
- package/dist/src/explorer.js +332 -623
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +38 -45
- package/dist/src/state-manager.d.ts +10 -30
- package/dist/src/state-manager.js +11 -129
- package/dist/src/utils/aria.d.ts +6 -1
- package/dist/src/utils/aria.js +4 -1
- package/dist/src/utils/cache.d.ts +15 -0
- package/dist/src/utils/cache.js +34 -0
- package/dist/src/utils/context-formatter.d.ts +6 -0
- package/dist/src/utils/context-formatter.js +15 -27
- package/dist/src/utils/hooks-runner.js +2 -4
- package/dist/src/utils/html.d.ts +5 -0
- package/dist/src/utils/html.js +71 -0
- package/dist/src/utils/markdown-files.d.ts +10 -0
- package/dist/src/utils/markdown-files.js +21 -0
- package/dist/src/utils/markdown-query.d.ts +6 -0
- package/dist/src/utils/markdown-query.js +14 -1
- package/dist/src/utils/page-readiness.d.ts +1 -0
- package/dist/src/utils/page-readiness.js +1 -1
- package/dist/src/utils/secrets.js +2 -2
- package/dist/src/utils/strings.d.ts +2 -0
- package/dist/src/utils/strings.js +10 -0
- 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 +3 -1
- package/dist/src/utils/web-element.js +32 -0
- package/dist/src/utils/web-sandbox.d.ts +4 -0
- package/dist/src/utils/web-sandbox.js +37 -0
- package/models.json +30 -0
- package/package.json +23 -3
- package/src/action-result.ts +70 -173
- package/src/action.ts +38 -132
- 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 -30
- package/src/ai/captain.ts +17 -42
- package/src/ai/conversation.ts +9 -32
- package/src/ai/driller.ts +53 -83
- package/src/ai/experience-compactor.ts +16 -35
- package/src/ai/fisherman-tools.ts +1 -1
- package/src/ai/fisherman.ts +0 -4
- package/src/ai/historian/codeceptjs.ts +5 -3
- package/src/ai/historian/playwright.ts +7 -4
- package/src/ai/historian/screencast.ts +4 -3
- package/src/ai/historian/utils.ts +0 -1
- package/src/ai/historian.ts +6 -8
- package/src/ai/navigator.ts +39 -70
- package/src/ai/pilot.ts +59 -61
- package/src/ai/planner.ts +76 -62
- package/src/ai/provider.ts +85 -146
- package/src/ai/quartermaster.ts +39 -45
- package/src/ai/rerunner.ts +27 -50
- package/src/ai/researcher/cache.ts +10 -16
- package/src/ai/researcher/coordinates.ts +4 -3
- package/src/ai/researcher/deep-analysis.ts +10 -11
- package/src/ai/researcher/locators.ts +66 -9
- package/src/ai/researcher/parser.ts +1 -1
- package/src/ai/researcher/research-result.ts +0 -18
- package/src/ai/researcher/sections.ts +9 -3
- package/src/ai/researcher.ts +42 -119
- package/src/ai/rules.ts +24 -13
- package/src/ai/session-analyst.ts +8 -5
- package/src/ai/task-agent.ts +45 -38
- package/src/ai/tester.ts +51 -129
- package/src/ai/tools.ts +145 -153
- package/src/command-handler.ts +1 -1
- package/src/commands/compact-command.ts +7 -8
- package/src/commands/context-aria-command.ts +1 -1
- package/src/commands/context-command.ts +8 -6
- 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 +2 -2
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/experience-command.ts +1 -1
- package/src/commands/explore-command.ts +4 -3
- package/src/commands/freesail-command.ts +2 -2
- package/src/commands/knows-command.ts +1 -1
- package/src/commands/learn-command.ts +5 -3
- 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/AddKnowledge.tsx +5 -7
- package/src/components/App.tsx +3 -3
- package/src/config.ts +185 -11
- package/src/experience-tracker.ts +89 -152
- package/src/explorbot.ts +88 -83
- package/src/explorer.ts +402 -693
- package/src/knowledge-tracker.ts +42 -49
- package/src/state-manager.ts +20 -155
- package/src/utils/aria.ts +11 -2
- package/src/utils/cache.ts +40 -0
- package/src/utils/context-formatter.ts +12 -15
- package/src/utils/hooks-runner.ts +2 -4
- package/src/utils/html.ts +79 -0
- package/src/utils/markdown-files.ts +30 -0
- package/src/utils/markdown-query.ts +15 -1
- package/src/utils/page-readiness.ts +1 -1
- package/src/utils/secrets.ts +2 -3
- package/src/utils/strings.ts +12 -0
- 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 +34 -0
- package/src/utils/web-sandbox.ts +43 -0
- package/dist/boat/api-tester/example/apibot.config.js +0 -30
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()) {
|
|
@@ -362,6 +405,18 @@ export class ConfigParser {
|
|
|
362
405
|
return path.join(path.dirname(configPath), config.dirs?.output || 'output');
|
|
363
406
|
}
|
|
364
407
|
|
|
408
|
+
public getProjectRoot(): string {
|
|
409
|
+
const configPath = this.getConfigPath();
|
|
410
|
+
if (configPath) return path.dirname(configPath);
|
|
411
|
+
return process.cwd();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
public resolveProjectDir(relativeDir: string): string {
|
|
415
|
+
const configPath = this.getConfigPath();
|
|
416
|
+
if (!configPath) return relativeDir;
|
|
417
|
+
return path.join(path.dirname(configPath), relativeDir);
|
|
418
|
+
}
|
|
419
|
+
|
|
365
420
|
public getStatesDir(): string {
|
|
366
421
|
return outputPath('states');
|
|
367
422
|
}
|
|
@@ -376,6 +431,7 @@ export class ConfigParser {
|
|
|
376
431
|
|
|
377
432
|
// For testing purposes only
|
|
378
433
|
public static resetForTesting(): void {
|
|
434
|
+
cachedOutputRoot = null;
|
|
379
435
|
if (ConfigParser.instance) {
|
|
380
436
|
ConfigParser.instance.config = null;
|
|
381
437
|
ConfigParser.instance.configPath = null;
|
|
@@ -430,6 +486,52 @@ export class ConfigParser {
|
|
|
430
486
|
}
|
|
431
487
|
}
|
|
432
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
|
+
|
|
433
535
|
private findConfigFile(): string | null {
|
|
434
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'];
|
|
435
537
|
|
|
@@ -542,3 +644,75 @@ export class ConfigParser {
|
|
|
542
644
|
export function outputPath(...segments: string[]): string {
|
|
543
645
|
return path.join(ConfigParser.getInstance().getOutputDir(), ...segments);
|
|
544
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 };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
2
|
-
import { basename,
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
3
|
import matter from 'gray-matter';
|
|
4
|
-
import { type Tokens, marked } from 'marked';
|
|
5
4
|
import type { ActionResult } from './action-result.js';
|
|
6
5
|
import { ConfigParser } from './config.js';
|
|
7
6
|
import { KnowledgeTracker } from './knowledge-tracker.js';
|
|
8
7
|
import type { WebPageState } from './state-manager.js';
|
|
9
|
-
import { createDebug, tag } from './utils/logger.js';
|
|
8
|
+
import { createDebug, pluralize, tag } from './utils/logger.js';
|
|
9
|
+
import { loadMarkdownFiles } from './utils/markdown-files.js';
|
|
10
10
|
import { mdq } from './utils/markdown-query.js';
|
|
11
11
|
import { redactSecrets } from './utils/secrets.js';
|
|
12
12
|
import { isNonReusableCode } from './utils/step-analyzer.ts';
|
|
@@ -39,20 +39,14 @@ export class ExperienceTracker {
|
|
|
39
39
|
private disabled: boolean;
|
|
40
40
|
private knowledgeTracker: KnowledgeTracker;
|
|
41
41
|
|
|
42
|
-
constructor(options: { disabled?: boolean } = {}) {
|
|
42
|
+
constructor(knowledgeTracker: KnowledgeTracker, options: { disabled?: boolean } = {}) {
|
|
43
43
|
const configParser = ConfigParser.getInstance();
|
|
44
44
|
const config = configParser.getConfig();
|
|
45
|
-
|
|
46
|
-
this.
|
|
47
|
-
this.knowledgeTracker = new KnowledgeTracker();
|
|
45
|
+
this.disabled = options.disabled ?? config.experience?.disabled ?? false;
|
|
46
|
+
this.knowledgeTracker = knowledgeTracker;
|
|
48
47
|
|
|
49
48
|
// Resolve experience directory relative to the config file location (project root)
|
|
50
|
-
|
|
51
|
-
const projectRoot = dirname(configPath);
|
|
52
|
-
this.experienceDir = join(projectRoot, config.dirs?.experience || 'experience');
|
|
53
|
-
} else {
|
|
54
|
-
this.experienceDir = config.dirs?.experience || 'experience';
|
|
55
|
-
}
|
|
49
|
+
this.experienceDir = configParser.resolveProjectDir(config.dirs?.experience || 'experience');
|
|
56
50
|
|
|
57
51
|
if (!this.disabled) {
|
|
58
52
|
this.ensureDirectory(this.experienceDir);
|
|
@@ -99,7 +93,10 @@ export class ExperienceTracker {
|
|
|
99
93
|
}
|
|
100
94
|
|
|
101
95
|
readExperienceFile(stateHash: string): { content: string; data: any } {
|
|
102
|
-
|
|
96
|
+
return this.readExperienceFileAt(this.getExperienceFilePath(stateHash));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private readExperienceFileAt(filePath: string): { content: string; data: any } {
|
|
103
100
|
const fileContent = readFileSync(filePath, 'utf8');
|
|
104
101
|
const { content, data } = matter(fileContent);
|
|
105
102
|
return { content, data };
|
|
@@ -114,21 +111,6 @@ export class ExperienceTracker {
|
|
|
114
111
|
writeFileSync(filePath, fileContent, 'utf8');
|
|
115
112
|
}
|
|
116
113
|
|
|
117
|
-
hasRecentExperience(stateHash: string, prefix = ''): boolean {
|
|
118
|
-
if (this.disabled) {
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
if (prefix) {
|
|
122
|
-
stateHash = `${prefix}_${stateHash}`;
|
|
123
|
-
}
|
|
124
|
-
const filePath = this.getExperienceFilePath(stateHash);
|
|
125
|
-
if (!existsSync(filePath)) {
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
const stats = statSync(filePath);
|
|
129
|
-
return stats.mtime.getTime() > Date.now() - 1000 * 60 * 60 * 24;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
114
|
private getExperienceFilePath(stateHash: string): string {
|
|
133
115
|
return join(this.experienceDir, `${stateHash}.md`);
|
|
134
116
|
}
|
|
@@ -176,7 +158,12 @@ export class ExperienceTracker {
|
|
|
176
158
|
this.ensureExperienceFile(state);
|
|
177
159
|
const stateHash = state.getStateHash();
|
|
178
160
|
const { content, data } = this.readExperienceFile(stateHash);
|
|
179
|
-
if (
|
|
161
|
+
if (
|
|
162
|
+
mdq(content)
|
|
163
|
+
.query('code')
|
|
164
|
+
.meta()
|
|
165
|
+
.some((m) => m.text.trim() === action.code.trim())
|
|
166
|
+
) {
|
|
180
167
|
debugLog('Skipping duplicate action', action.code);
|
|
181
168
|
return;
|
|
182
169
|
}
|
|
@@ -204,7 +191,12 @@ export class ExperienceTracker {
|
|
|
204
191
|
const stateHash = state.getStateHash();
|
|
205
192
|
const { content, data } = this.readExperienceFile(stateHash);
|
|
206
193
|
|
|
207
|
-
if (
|
|
194
|
+
if (
|
|
195
|
+
mdq(content)
|
|
196
|
+
.query('section2')
|
|
197
|
+
.each()
|
|
198
|
+
.some((s) => s.text().trim() === body.trim())
|
|
199
|
+
) {
|
|
208
200
|
debugLog('Skipping duplicate flow body');
|
|
209
201
|
return;
|
|
210
202
|
}
|
|
@@ -231,25 +223,11 @@ export class ExperienceTracker {
|
|
|
231
223
|
}
|
|
232
224
|
|
|
233
225
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
const content = readFileSync(file, 'utf8');
|
|
241
|
-
const parsed = matter(content);
|
|
242
|
-
const mtime = statSync(file).mtime;
|
|
243
|
-
allFiles.push({
|
|
244
|
-
filePath: file,
|
|
245
|
-
data: parsed.data,
|
|
246
|
-
content: parsed.content,
|
|
247
|
-
mtime,
|
|
248
|
-
});
|
|
249
|
-
} catch (error) {
|
|
250
|
-
debugLog(`Failed to read experience file ${file}:`, error);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
226
|
+
allFiles.push(
|
|
227
|
+
...loadMarkdownFiles(experienceDir, {
|
|
228
|
+
onError: (filePath, error) => debugLog(`Failed to read experience file ${filePath}:`, error),
|
|
229
|
+
})
|
|
230
|
+
);
|
|
253
231
|
} catch (error) {
|
|
254
232
|
debugLog(`Failed to read experience directory ${experienceDir}:`, error);
|
|
255
233
|
}
|
|
@@ -275,20 +253,17 @@ export class ExperienceTracker {
|
|
|
275
253
|
});
|
|
276
254
|
})
|
|
277
255
|
.map((experience) => {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
256
|
+
if (experience.content.split('\n').length <= maxLines) return experience;
|
|
257
|
+
let content = experience.content;
|
|
258
|
+
while (content.split('\n').length > maxLines) {
|
|
259
|
+
const sections = mdq(content).query('section2').each();
|
|
260
|
+
if (sections.length <= 1) break;
|
|
261
|
+
content = sections[sections.length - 1].replace('');
|
|
262
|
+
}
|
|
263
|
+
return { ...experience, content };
|
|
281
264
|
});
|
|
282
265
|
}
|
|
283
266
|
|
|
284
|
-
/**
|
|
285
|
-
* Clean up experience tracker (for testing)
|
|
286
|
-
*/
|
|
287
|
-
cleanup(): void {
|
|
288
|
-
// Clear any in-memory state if needed
|
|
289
|
-
// The actual files will be cleaned up by test cleanup
|
|
290
|
-
}
|
|
291
|
-
|
|
292
267
|
getSuccessfulExperience(state: ActionResult, options?: { includeDescendants?: boolean; stripCode?: boolean }): string[] {
|
|
293
268
|
const records = this.getRelevantExperience(state, {
|
|
294
269
|
includeDescendantExperience: options?.includeDescendants,
|
|
@@ -326,36 +301,50 @@ export class ExperienceTracker {
|
|
|
326
301
|
return aHash.localeCompare(bHash);
|
|
327
302
|
});
|
|
328
303
|
|
|
304
|
+
return this.buildToc(sorted);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
renderExperienceTocFor(state: ActionResult): string {
|
|
308
|
+
const toc = this.getExperienceTableOfContents(state);
|
|
309
|
+
if (toc.length === 0) return '';
|
|
310
|
+
|
|
311
|
+
const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
|
|
312
|
+
debugLog(`injecting experience TOC (${toc.length} files, ${totalSections} sections)`);
|
|
313
|
+
tag('operation').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections)`);
|
|
314
|
+
return renderExperienceToc(toc);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
getExperienceSection(fileTag: string, sectionIndex: number, state: ActionResult, options?: { includeDescendantExperience?: boolean }): { title: string; url: string; content: string } | null {
|
|
318
|
+
const toc = this.getExperienceTableOfContents(state, options);
|
|
319
|
+
const entry = toc.find((e) => e.fileTag === fileTag);
|
|
320
|
+
if (!entry) return null;
|
|
321
|
+
|
|
322
|
+
const extracted = this.readSectionByHash(entry.fileHash, sectionIndex);
|
|
323
|
+
if (!extracted) return null;
|
|
324
|
+
|
|
325
|
+
return { title: extracted.title, url: entry.url, content: extracted.body };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private buildToc(records: ExperienceFile[]): ExperienceTocEntry[] {
|
|
329
329
|
const toc: ExperienceTocEntry[] = [];
|
|
330
|
-
for (
|
|
331
|
-
const record = sorted[i];
|
|
332
|
-
const fileHash = basename(record.filePath, '.md');
|
|
333
|
-
const url = (record.data as WebPageState)?.url || '';
|
|
330
|
+
for (const record of records) {
|
|
334
331
|
const sections = listTocHeadings(record.content);
|
|
335
332
|
if (sections.length === 0) continue;
|
|
336
333
|
toc.push({
|
|
337
|
-
fileTag: indexToLetters(
|
|
338
|
-
fileHash,
|
|
339
|
-
url,
|
|
334
|
+
fileTag: indexToLetters(toc.length),
|
|
335
|
+
fileHash: basename(record.filePath, '.md'),
|
|
336
|
+
url: (record.data as WebPageState)?.url || '',
|
|
340
337
|
sections,
|
|
341
338
|
});
|
|
342
339
|
}
|
|
343
340
|
return toc;
|
|
344
341
|
}
|
|
345
342
|
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
const entry = toc.find((e) => e.fileTag === fileTag);
|
|
349
|
-
if (!entry) return null;
|
|
350
|
-
|
|
351
|
-
const filePath = this.findExperienceFileByHash(entry.fileHash);
|
|
343
|
+
private readSectionByHash(fileHash: string, sectionIndex: number): { title: string; body: string } | null {
|
|
344
|
+
const filePath = this.findExperienceFileByHash(fileHash);
|
|
352
345
|
if (!filePath) return null;
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const extracted = extractHeadingSection(content, sectionIndex);
|
|
356
|
-
if (!extracted) return null;
|
|
357
|
-
|
|
358
|
-
return { title: extracted.title, url: entry.url, content: extracted.body };
|
|
346
|
+
const { content } = this.readExperienceFileAt(filePath);
|
|
347
|
+
return extractHeadingSection(content, sectionIndex);
|
|
359
348
|
}
|
|
360
349
|
|
|
361
350
|
private findExperienceFileByHash(fileHash: string): string | null {
|
|
@@ -402,19 +391,7 @@ export class ExperienceTracker {
|
|
|
402
391
|
return aUrl.localeCompare(bUrl);
|
|
403
392
|
});
|
|
404
393
|
|
|
405
|
-
|
|
406
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
407
|
-
const record = sorted[i];
|
|
408
|
-
const sections = listTocHeadings(record.content);
|
|
409
|
-
if (sections.length === 0) continue;
|
|
410
|
-
toc.push({
|
|
411
|
-
fileTag: indexToLetters(toc.length),
|
|
412
|
-
fileHash: basename(record.filePath, '.md'),
|
|
413
|
-
url: (record.data as WebPageState)?.url || '',
|
|
414
|
-
sections,
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
return toc;
|
|
394
|
+
return this.buildToc(sorted);
|
|
418
395
|
}
|
|
419
396
|
|
|
420
397
|
getExperienceSectionByTag(fileTag: string, sectionIndex: number, filter?: string): { title: string; url: string; content: string; fileHash: string } | null {
|
|
@@ -422,11 +399,7 @@ export class ExperienceTracker {
|
|
|
422
399
|
const entry = toc.find((e) => e.fileTag === fileTag);
|
|
423
400
|
if (!entry) return null;
|
|
424
401
|
|
|
425
|
-
const
|
|
426
|
-
if (!filePath) return null;
|
|
427
|
-
|
|
428
|
-
const { content } = this.readExperienceFile(entry.fileHash);
|
|
429
|
-
const extracted = extractHeadingSection(content, sectionIndex);
|
|
402
|
+
const extracted = this.readSectionByHash(entry.fileHash, sectionIndex);
|
|
430
403
|
if (!extracted) return null;
|
|
431
404
|
|
|
432
405
|
return { title: extracted.title, url: entry.url, content: extracted.body, fileHash: entry.fileHash };
|
|
@@ -434,49 +407,22 @@ export class ExperienceTracker {
|
|
|
434
407
|
}
|
|
435
408
|
|
|
436
409
|
function listTocHeadings(content: string): { index: number; level: 2 | 3; title: string }[] {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const heading = token as Tokens.Heading;
|
|
443
|
-
if (heading.depth !== 2 && heading.depth !== 3) continue;
|
|
444
|
-
index++;
|
|
445
|
-
result.push({ index, level: heading.depth as 2 | 3, title: heading.text });
|
|
446
|
-
}
|
|
447
|
-
return result;
|
|
410
|
+
return mdq(content)
|
|
411
|
+
.query('heading')
|
|
412
|
+
.meta()
|
|
413
|
+
.filter((m) => m.depth === 2 || m.depth === 3)
|
|
414
|
+
.map((m, i) => ({ index: i + 1, level: m.depth as 2 | 3, title: m.text }));
|
|
448
415
|
}
|
|
449
416
|
|
|
450
417
|
function extractHeadingSection(content: string, sectionIndex: number): { title: string; body: string } | null {
|
|
451
|
-
const
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
455
|
-
const token = tokens[i];
|
|
456
|
-
if (token.type !== 'heading') continue;
|
|
457
|
-
const heading = token as Tokens.Heading;
|
|
458
|
-
if (heading.depth !== 2 && heading.depth !== 3) continue;
|
|
459
|
-
matching.push({ tokenIdx: i, depth: heading.depth, text: heading.text });
|
|
460
|
-
}
|
|
418
|
+
const sections = mdq(content).query('section').each();
|
|
419
|
+
const metas = mdq(content).query('section').meta();
|
|
420
|
+
const matching = metas.map((meta, i) => ({ meta, section: sections[i] })).filter((entry) => entry.meta.depth === 2 || entry.meta.depth === 3);
|
|
461
421
|
|
|
462
422
|
if (sectionIndex < 1 || sectionIndex > matching.length) return null;
|
|
463
423
|
|
|
464
424
|
const target = matching[sectionIndex - 1];
|
|
465
|
-
|
|
466
|
-
for (let j = target.tokenIdx + 1; j < tokens.length; j++) {
|
|
467
|
-
const token = tokens[j];
|
|
468
|
-
if (token.type !== 'heading') continue;
|
|
469
|
-
if ((token as Tokens.Heading).depth <= target.depth) {
|
|
470
|
-
endTokenIdx = j;
|
|
471
|
-
break;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const body = tokens
|
|
476
|
-
.slice(target.tokenIdx, endTokenIdx)
|
|
477
|
-
.map((t) => (t as any).raw || '')
|
|
478
|
-
.join('');
|
|
479
|
-
return { title: target.text, body };
|
|
425
|
+
return { title: target.meta.text, body: target.section.text() };
|
|
480
426
|
}
|
|
481
427
|
|
|
482
428
|
function indexToLetters(index: number): string {
|
|
@@ -545,23 +491,14 @@ function generateActionContent(title: string, code: string, explanation?: string
|
|
|
545
491
|
}
|
|
546
492
|
|
|
547
493
|
function renderAsHowTo(content: string): string {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
if (text.startsWith('
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
if (text.startsWith('ACTION:')) {
|
|
558
|
-
result += `## HOW to ${text.slice(7).trim()} (single-step)\n\n`;
|
|
559
|
-
continue;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
result += (token as any).raw || '';
|
|
563
|
-
}
|
|
564
|
-
return result;
|
|
494
|
+
return mdq(content)
|
|
495
|
+
.query('h2')
|
|
496
|
+
.replaceEach((heading) => {
|
|
497
|
+
const text = heading.meta()[0]?.text.trim() || '';
|
|
498
|
+
if (text.startsWith('FLOW:')) return `## HOW to ${text.slice(5).trim()} (multi-step)\n\n`;
|
|
499
|
+
if (text.startsWith('ACTION:')) return `## HOW to ${text.slice(7).trim()} (single-step)\n\n`;
|
|
500
|
+
return heading.text();
|
|
501
|
+
});
|
|
565
502
|
}
|
|
566
503
|
|
|
567
504
|
export interface ExperienceFile {
|