explorbot 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/explorbot-cli.ts +13 -6
- package/boat/api-tester/src/ai/chief.ts +7 -1
- package/boat/api-tester/src/ai/curler-tools.ts +1 -1
- package/boat/api-tester/src/ai/curler.ts +7 -1
- package/boat/api-tester/src/apibot.ts +10 -4
- package/boat/api-tester/src/cli.ts +15 -2
- package/boat/api-tester/src/config.ts +28 -8
- package/boat/doc-collector/bin/doc-collector-cli.ts +3 -2
- package/boat/doc-collector/src/ai/documentarian.ts +57 -5
- package/boat/doc-collector/src/ai/tools.ts +1 -1
- package/boat/doc-collector/src/cli.ts +21 -3
- package/boat/doc-collector/src/config.ts +3 -0
- package/boat/doc-collector/src/docbot.ts +57 -12
- package/boat/doc-collector/src/docs-renderer.ts +79 -55
- package/boat/doc-collector/src/interaction-screenshots.ts +19 -10
- package/boat/doc-collector/src/screenshots.ts +106 -3
- package/boat/doc-collector/src/template-dedup.ts +31 -0
- package/boat/prima/bin/prima-cli.ts +2 -0
- package/boat/prima/src/cli.ts +5 -0
- package/boat/prima/src/prima.ts +3 -4
- package/dist/bin/explorbot-cli.js +14 -6
- package/dist/boat/api-tester/bin/apibot-cli.js +3 -2
- package/dist/boat/api-tester/src/ai/chief.js +6 -1
- package/dist/boat/api-tester/src/ai/curler-tools.js +1 -1
- package/dist/boat/api-tester/src/ai/curler.js +6 -1
- package/dist/boat/api-tester/src/apibot.js +7 -3
- package/dist/boat/api-tester/src/cli.js +14 -2
- package/dist/boat/api-tester/src/config.js +31 -8
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +3 -2
- package/dist/boat/doc-collector/src/ai/documentarian.js +62 -4
- package/dist/boat/doc-collector/src/ai/tools.js +1 -1
- package/dist/boat/doc-collector/src/cli.js +18 -3
- package/dist/boat/doc-collector/src/config.js +1 -0
- package/dist/boat/doc-collector/src/docbot.js +53 -11
- package/dist/boat/doc-collector/src/docs-renderer.js +69 -46
- package/dist/boat/doc-collector/src/interaction-screenshots.js +18 -11
- package/dist/boat/doc-collector/src/screenshots.js +104 -1
- package/dist/boat/doc-collector/src/template-dedup.js +27 -0
- package/dist/boat/prima/bin/prima-cli.js +2 -0
- package/dist/boat/prima/src/cli.js +5 -0
- package/dist/boat/prima/src/prima.js +3 -4
- package/dist/package.json +1 -1
- package/dist/rules/planner/styles/normal.md +1 -1
- package/dist/src/action-result.d.ts +2 -2
- package/dist/src/action-result.js +15 -17
- package/dist/src/action.d.ts +1 -1
- package/dist/src/action.js +15 -15
- package/dist/src/ai/captain.js +1 -1
- package/dist/src/ai/fisherman/request-haul.d.ts +11 -0
- package/dist/src/ai/fisherman/request-haul.js +28 -0
- package/dist/src/ai/fisherman-tools.d.ts +10 -1
- package/dist/src/ai/fisherman-tools.js +35 -31
- package/dist/src/ai/fisherman.d.ts +3 -2
- package/dist/src/ai/fisherman.js +8 -6
- package/dist/src/ai/navigator.js +1 -1
- package/dist/src/ai/pilot.js +21 -16
- package/dist/src/ai/planner.js +9 -7
- package/dist/src/ai/provider.js +8 -14
- package/dist/src/ai/researcher/coordinates.d.ts +2 -0
- package/dist/src/ai/researcher/coordinates.js +13 -1
- package/dist/src/ai/researcher/deep-analysis.js +9 -9
- package/dist/src/ai/researcher/fingerprint-worker.js +1 -11
- package/dist/src/ai/researcher.js +3 -1
- package/dist/src/ai/rules.js +11 -3
- package/dist/src/ai/tester.js +20 -20
- package/dist/src/ai/tools.d.ts +1 -1
- package/dist/src/ai/tools.js +18 -74
- package/dist/src/api/request-store.d.ts +1 -5
- package/dist/src/api/request-store.js +12 -22
- package/dist/src/api/spec-reader.js +1 -1
- package/dist/src/commands/config-command.js +3 -10
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/explore-command.js +12 -1
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/init-command.js +3 -7
- package/dist/src/commands/options/base-option.d.ts +8 -0
- package/dist/src/commands/options/base-option.js +12 -0
- package/dist/src/commands/options/index.d.ts +5 -0
- package/dist/src/commands/options/index.js +5 -0
- package/dist/src/commands/options/knowledge-option.d.ts +7 -0
- package/dist/src/commands/options/knowledge-option.js +12 -0
- package/dist/src/commands/options/ws-option.d.ts +7 -0
- package/dist/src/commands/options/ws-option.js +21 -0
- package/dist/src/commands/plans-command.d.ts +4 -1
- package/dist/src/commands/plans-command.js +4 -4
- package/dist/src/commands/recommended-models-command.d.ts +9 -0
- package/dist/src/commands/recommended-models-command.js +85 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +18 -2
- package/dist/src/experience-tracker.js +1 -1
- package/dist/src/explorbot.js +4 -4
- package/dist/src/global-config.d.ts +2 -0
- package/dist/src/global-config.js +6 -0
- package/dist/src/knowledge-tracker.d.ts +20 -7
- package/dist/src/knowledge-tracker.js +69 -31
- package/dist/src/remote.d.ts +0 -3
- package/dist/src/remote.js +0 -18
- package/dist/src/state-manager.d.ts +3 -3
- package/dist/src/state-manager.js +8 -8
- package/dist/src/test-plan.d.ts +1 -0
- package/dist/src/test-plan.js +13 -9
- package/dist/src/utils/aria.d.ts +1 -1
- package/dist/src/utils/aria.js +23 -2
- package/dist/src/utils/cli-section.d.ts +1 -0
- package/dist/src/utils/cli-section.js +12 -0
- package/dist/src/utils/html-diff.d.ts +1 -0
- package/dist/src/utils/html-diff.js +5 -5
- package/dist/src/utils/html.d.ts +2 -0
- package/dist/src/utils/html.js +5 -4
- package/dist/src/utils/overlay.d.ts +19 -55
- package/dist/src/utils/overlay.js +94 -148
- package/dist/src/utils/region.d.ts +41 -0
- package/dist/src/utils/region.js +99 -0
- package/dist/src/utils/similarity.d.ts +1 -0
- package/dist/src/utils/similarity.js +11 -0
- package/dist/src/utils/url-matcher.d.ts +1 -1
- package/dist/src/utils/url-matcher.js +4 -4
- package/docs/api-testing/basics.md +15 -0
- package/docs/api-testing/planning.md +10 -1
- package/docs/doc-collection/basics.md +11 -5
- package/docs/doc-collection/crawling.md +21 -1
- package/docs/reference/commands.md +27 -4
- package/docs/superpowers/plans/2026-09-03-model-resolves-click-ambiguity.md +509 -0
- package/docs/workflow/agentic-usage.md +11 -2
- package/docs/workflow/application-spec.md +4 -0
- package/docs/workflow/knowledge.md +46 -2
- package/package.json +1 -1
- package/rules/planner/styles/normal.md +1 -1
- package/src/action-result.ts +15 -16
- package/src/action.ts +16 -15
- package/src/ai/captain.ts +1 -1
- package/src/ai/fisherman/request-haul.ts +32 -0
- package/src/ai/fisherman-tools.ts +38 -32
- package/src/ai/fisherman.ts +8 -7
- package/src/ai/navigator.ts +1 -1
- package/src/ai/pilot.ts +19 -14
- package/src/ai/planner.ts +9 -8
- package/src/ai/provider.ts +8 -12
- package/src/ai/researcher/coordinates.ts +13 -1
- package/src/ai/researcher/deep-analysis.ts +9 -9
- package/src/ai/researcher/fingerprint-worker.ts +1 -10
- package/src/ai/researcher.ts +2 -1
- package/src/ai/rules.ts +11 -3
- package/src/ai/tester.ts +18 -18
- package/src/ai/tools.ts +18 -86
- package/src/api/request-store.ts +13 -27
- package/src/api/spec-reader.ts +1 -1
- package/src/commands/config-command.ts +3 -8
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/explore-command.ts +12 -1
- package/src/commands/index.ts +2 -0
- package/src/commands/init-command.ts +4 -11
- package/src/commands/options/base-option.ts +18 -0
- package/src/commands/options/index.ts +7 -0
- package/src/commands/options/knowledge-option.ts +14 -0
- package/src/commands/options/ws-option.ts +24 -0
- package/src/commands/plans-command.ts +5 -5
- package/src/commands/recommended-models-command.ts +92 -0
- package/src/config.ts +20 -2
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +4 -4
- package/src/global-config.ts +8 -0
- package/src/knowledge-tracker.ts +94 -36
- package/src/remote.ts +0 -20
- package/src/state-manager.ts +10 -9
- package/src/test-plan.ts +12 -10
- package/src/utils/aria.ts +26 -3
- package/src/utils/cli-section.ts +12 -0
- package/src/utils/html-diff.ts +6 -6
- package/src/utils/html.ts +6 -4
- package/src/utils/overlay.ts +105 -192
- package/src/utils/region.ts +132 -0
- package/src/utils/similarity.ts +9 -0
- package/src/utils/url-matcher.ts +3 -3
- package/dist/src/utils/request-map.d.ts +0 -7
- package/dist/src/utils/request-map.js +0 -16
- package/src/utils/request-map.ts +0 -19
|
@@ -261,9 +261,15 @@ export class ExploreCommand extends BaseCommand {
|
|
|
261
261
|
tag('info').log(`Exploring sub-page: ${pick.url} (${pick.reason})`);
|
|
262
262
|
try {
|
|
263
263
|
await this.explorBot.visit(pick.url);
|
|
264
|
+
const errorPage = getStateErrorPageError(this.explorBot.stateManager().getCurrentState());
|
|
265
|
+
if (errorPage) {
|
|
266
|
+
tag('warning').log(`Skipping sub-page: ${errorPage.message}`);
|
|
267
|
+
this.failedSubPages.add(normalizeUrl(pick.url));
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
264
270
|
await this.runAllStyles(pick.url, undefined, mainPlan, this.completedPlans, styles);
|
|
265
271
|
const subPlan = this.explorBot.getCurrentPlan();
|
|
266
|
-
if (subPlan && !this.completedPlans.includes(subPlan)) {
|
|
272
|
+
if (subPlan?.tests.length && !this.completedPlans.includes(subPlan)) {
|
|
267
273
|
this.completedPlans.push(subPlan);
|
|
268
274
|
}
|
|
269
275
|
knownUrls.add(normalizeUrl(pick.url));
|
|
@@ -298,6 +304,11 @@ export class ExploreCommand extends BaseCommand {
|
|
|
298
304
|
if (fresh && parentPlan) opts.extend = parentPlan;
|
|
299
305
|
if (this.dryRun) opts.noSave = true;
|
|
300
306
|
await this.planWithRetry(feature, opts, pageUrl);
|
|
307
|
+
const plan = this.explorBot.getCurrentPlan();
|
|
308
|
+
if (plan && plan.tests.length === 0) {
|
|
309
|
+
tag('warning').log('Nothing to test on this page, moving on');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
301
312
|
await this.runPendingTests();
|
|
302
313
|
this.rememberCurrentPlan();
|
|
303
314
|
fresh = false;
|
package/src/commands/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { PlanLoadCommand } from './plan-load-command.js';
|
|
|
28
28
|
import { PlanReloadCommand } from './plan-reload-command.js';
|
|
29
29
|
import { PlanSaveCommand } from './plan-save-command.js';
|
|
30
30
|
import { PlansCommand } from './plans-command.js';
|
|
31
|
+
import { RecommendedModelsCommand } from './recommended-models-command.js';
|
|
31
32
|
import { RerunCommand } from './rerun-command.js';
|
|
32
33
|
import { ResearchCommand } from './research-command.js';
|
|
33
34
|
import { RunsCommand } from './runs-command.js';
|
|
@@ -72,6 +73,7 @@ const commandClasses: CommandClass[] = [
|
|
|
72
73
|
RerunCommand,
|
|
73
74
|
StatusCommand,
|
|
74
75
|
ConfigCommand,
|
|
76
|
+
RecommendedModelsCommand,
|
|
75
77
|
DebugCommand,
|
|
76
78
|
ExitCommand,
|
|
77
79
|
];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, extname, join, resolve } from 'node:path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import { ConfigParser, PROVIDERS } from '../config.ts';
|
|
4
|
+
import { ConfigParser, type ModelRole, PROVIDERS, missingModelRoles } from '../config.ts';
|
|
5
5
|
import { findGlobalConfig, globalConfigPath, globalDir, globalEnvPath } from '../global-config.ts';
|
|
6
6
|
import { getCliName } from '../utils/cli-name.ts';
|
|
7
7
|
import { log, tag } from '../utils/logger.js';
|
|
@@ -91,7 +91,7 @@ export function writeGlobalConfig(provider: string, apiKey?: string): void {
|
|
|
91
91
|
writeEnvKey(envKey, apiKey || '');
|
|
92
92
|
log(`Stored ${envKey} in ${globalEnvPath()}`);
|
|
93
93
|
|
|
94
|
-
const missing =
|
|
94
|
+
const missing = missingModelRoles(provider);
|
|
95
95
|
if (missing.length) {
|
|
96
96
|
tag('warning').log(`No recommended ${missing.join(' and ')} for ${provider} — set the model ids in ${globalConfigPath()}`);
|
|
97
97
|
}
|
|
@@ -156,7 +156,7 @@ export function runInitCommand(options: InitCommandOptions): void {
|
|
|
156
156
|
log(`Env file already exists: ${relativeToCwd(envPath)}`);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
const missing =
|
|
159
|
+
const missing = missingModelRoles(provider);
|
|
160
160
|
if (missing.length) {
|
|
161
161
|
tag('warning').log(`No recommended ${missing.join(' and ')} for ${provider} — set the model ids in ${relativeToCwd(outPath)}`);
|
|
162
162
|
}
|
|
@@ -255,7 +255,7 @@ async function renderLocalProviderWizard(): Promise<string | null> {
|
|
|
255
255
|
|
|
256
256
|
function modelLines(provider: string): string {
|
|
257
257
|
const recommended = ConfigParser.recommendedModels()[provider] || {};
|
|
258
|
-
const roles: Array<[
|
|
258
|
+
const roles: Array<[ModelRole, string]> = [
|
|
259
259
|
['model', 'fast model with tool calling capabilities'],
|
|
260
260
|
['visionModel', 'vision model for screenshot analysis'],
|
|
261
261
|
['agenticModel', 'agentic model for decision making'],
|
|
@@ -308,11 +308,6 @@ function isModuleProject(configDir: string): boolean {
|
|
|
308
308
|
}
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
function missingRoles(provider: string): string[] {
|
|
312
|
-
const recommended = ConfigParser.recommendedModels()[provider] || {};
|
|
313
|
-
return ['model', 'visionModel', 'agenticModel'].filter((role) => !recommended[role]);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
311
|
function writeEnvKey(key: string, value: string): void {
|
|
317
312
|
const envPath = globalEnvPath();
|
|
318
313
|
let content = '# AI provider API keys';
|
|
@@ -327,8 +322,6 @@ function writeEnvKey(key: string, value: string): void {
|
|
|
327
322
|
writeFileSync(envPath, `${lines.join('\n').trimEnd()}\n`, 'utf8');
|
|
328
323
|
}
|
|
329
324
|
|
|
330
|
-
type ModelRoleName = 'model' | 'visionModel' | 'agenticModel';
|
|
331
|
-
|
|
332
325
|
type InitCommandOptions = {
|
|
333
326
|
configPath?: string;
|
|
334
327
|
force?: boolean;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
|
|
3
|
+
export abstract class BaseOption {
|
|
4
|
+
abstract flags: string;
|
|
5
|
+
abstract description: string;
|
|
6
|
+
collect?: (value: string, previous: any) => any;
|
|
7
|
+
|
|
8
|
+
register(program: Command): void {
|
|
9
|
+
if (this.collect) program.option(this.flags, this.description, this.collect);
|
|
10
|
+
if (!this.collect) program.option(this.flags, this.description);
|
|
11
|
+
|
|
12
|
+
program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
13
|
+
this.apply(actionCommand.optsWithGlobals(), actionCommand);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
protected abstract apply(options: Record<string, any>, command: Command): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { KnowledgeTracker } from '../../knowledge-tracker.js';
|
|
2
|
+
import { BaseOption } from './base-option.js';
|
|
3
|
+
|
|
4
|
+
export class KnowledgeOption extends BaseOption {
|
|
5
|
+
flags = '--knowledge <text>';
|
|
6
|
+
description = 'Knowledge for this run only, not saved to disk. Markdown text; add url: or endpoint: frontmatter to scope it, otherwise it applies everywhere. Repeatable';
|
|
7
|
+
collect = (value: string, previous: string[] = []) => [...previous, value];
|
|
8
|
+
|
|
9
|
+
protected apply(options: Record<string, any>): void {
|
|
10
|
+
for (const text of options.knowledge || []) {
|
|
11
|
+
KnowledgeTracker.appendSessionKnowledge(text);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { remote } from '../../remote.js';
|
|
3
|
+
import { BaseOption } from './base-option.js';
|
|
4
|
+
|
|
5
|
+
export class WsOption extends BaseOption {
|
|
6
|
+
flags = '--ws <url>';
|
|
7
|
+
description = 'Stream this run to a remote UI over WebSocket';
|
|
8
|
+
|
|
9
|
+
protected apply(options: Record<string, any>, command: Command): void {
|
|
10
|
+
const url = options.ws || process.env.EXPLORBOT_WS_URL;
|
|
11
|
+
if (!url) return;
|
|
12
|
+
remote.attach(String(url), commandPath(command));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function commandPath(command: Command): string {
|
|
17
|
+
const parts: string[] = [];
|
|
18
|
+
let node: Command | null = command;
|
|
19
|
+
while (node) {
|
|
20
|
+
parts.unshift(node.name());
|
|
21
|
+
node = node.parent;
|
|
22
|
+
}
|
|
23
|
+
return parts.slice(1).join(' ') || parts.join(' ');
|
|
24
|
+
}
|
|
@@ -21,8 +21,7 @@ export class PlansCommand extends BaseCommand {
|
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const file = this.resolvePlanFile(target, files);
|
|
25
|
-
const plan = Plan.fromMarkdown(file.path);
|
|
24
|
+
const { plan, file } = this.resolvePlanFile(target, files);
|
|
26
25
|
this.printPlanDetails(plan, file);
|
|
27
26
|
}
|
|
28
27
|
|
|
@@ -71,12 +70,12 @@ export class PlansCommand extends BaseCommand {
|
|
|
71
70
|
tag('info').log(`${getCliName()} test 1 --from-plan ${file.name}`);
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
private resolvePlanFile(target: string, files: PlanFile[]): PlanFile {
|
|
73
|
+
private resolvePlanFile(target: string, files: PlanFile[]): { plan: Plan; file: PlanFile } {
|
|
75
74
|
const index = Number.parseInt(target, 10);
|
|
76
75
|
if (!Number.isNaN(index) && String(index) === target) {
|
|
77
76
|
const file = files[index - 1];
|
|
78
77
|
if (!file) throw new Error(`Plan #${target} not found. Available: 1-${files.length}`);
|
|
79
|
-
return file;
|
|
78
|
+
return { plan: Plan.fromMarkdown(file.path), file };
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
const plan = Plan.loadFromFile(target, this.explorBot.getPlansDir());
|
|
@@ -84,11 +83,12 @@ export class PlansCommand extends BaseCommand {
|
|
|
84
83
|
throw new Error(`Plan file not found: ${target}`);
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
|
|
86
|
+
const file = {
|
|
88
87
|
name: path.basename(plan.filePath),
|
|
89
88
|
path: plan.filePath,
|
|
90
89
|
modifiedAt: statSync(plan.filePath).mtimeMs,
|
|
91
90
|
};
|
|
91
|
+
return { plan, file };
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import type { Command } from 'commander';
|
|
3
|
+
import { ConfigParser, EXPLORBOT_ENV_VARS, MODEL_ROLES, PROVIDERS } from '../config.js';
|
|
4
|
+
import { getCliName } from '../utils/cli-name.js';
|
|
5
|
+
import { renderSection } from '../utils/cli-section.js';
|
|
6
|
+
import { tag } from '../utils/logger.js';
|
|
7
|
+
import { BaseCommand } from './base-command.js';
|
|
8
|
+
|
|
9
|
+
const DESCRIPTION = 'Show the models recommended for every AI provider';
|
|
10
|
+
|
|
11
|
+
const ROLES: Record<string, { hint: string; env: string }> = {
|
|
12
|
+
model: { hint: 'fast', env: 'EXPLORBOT_AI_MODEL' },
|
|
13
|
+
visionModel: { hint: 'screenshots', env: 'EXPLORBOT_VISION_MODEL' },
|
|
14
|
+
agenticModel: { hint: 'smart', env: 'EXPLORBOT_AGENTIC_MODEL' },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const MODEL_ENV_VARS = MODEL_ROLES.map((role) => ROLES[role].env);
|
|
18
|
+
|
|
19
|
+
export class RecommendedModelsCommand extends BaseCommand {
|
|
20
|
+
name = 'recommended-models';
|
|
21
|
+
description = DESCRIPTION;
|
|
22
|
+
|
|
23
|
+
async execute(): Promise<void> {
|
|
24
|
+
tag('info').log(RecommendedModelsCommand.render());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static register(program: Command): void {
|
|
28
|
+
program
|
|
29
|
+
.command('recommended-models')
|
|
30
|
+
.description(DESCRIPTION)
|
|
31
|
+
.option('--json', 'Print the recommendations as JSON')
|
|
32
|
+
.action((options) => console.log(RecommendedModelsCommand.render(options.json)));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static render(json = false): string {
|
|
36
|
+
const recommended = ConfigParser.recommendedModels();
|
|
37
|
+
if (json) return JSON.stringify(recommended, null, 2);
|
|
38
|
+
|
|
39
|
+
const lines = [
|
|
40
|
+
chalk.bold('Export the provider key, then pick one of two ways to select models:'),
|
|
41
|
+
` ${chalk.bold('EXPLORBOT_AI_PROVIDER')} — every role takes that provider's recommendation below`,
|
|
42
|
+
` ${chalk.bold(MODEL_ENV_VARS.join(' + '))} — no provider variable, pin every role yourself as ${chalk.dim('provider/model-id')}`,
|
|
43
|
+
'',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
for (const [provider, roles] of Object.entries(recommended)) {
|
|
47
|
+
lines.push(chalk.bold.yellow(`${provider}:`));
|
|
48
|
+
|
|
49
|
+
const models: [string, string][] = [];
|
|
50
|
+
for (const role of MODEL_ROLES) {
|
|
51
|
+
const label = `${role} (${ROLES[role].hint})`;
|
|
52
|
+
if (roles[role]) models.push([label, roles[role]]);
|
|
53
|
+
if (!roles[role]) models.push([label, chalk.dim('not served, pair with another provider')]);
|
|
54
|
+
}
|
|
55
|
+
lines.push(...renderSection('models', models, 2));
|
|
56
|
+
|
|
57
|
+
const envKey = PROVIDERS[provider]?.envKey;
|
|
58
|
+
if (!envKey) {
|
|
59
|
+
lines.push(` ${chalk.dim('not bundled — import the provider in your config')}`, '');
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const env: [string, string][] = [
|
|
64
|
+
['key', envKey],
|
|
65
|
+
['provider', `EXPLORBOT_AI_PROVIDER=${provider}`],
|
|
66
|
+
];
|
|
67
|
+
let label = 'or models';
|
|
68
|
+
for (const role of MODEL_ROLES) {
|
|
69
|
+
if (!roles[role]) continue;
|
|
70
|
+
env.push([label, `${ROLES[role].env}=${provider}/${roles[role]}`]);
|
|
71
|
+
label = '';
|
|
72
|
+
}
|
|
73
|
+
lines.push(...renderSection('env variables', env, 2));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const modelVars = ['EXPLORBOT_AI_PROVIDER', ...MODEL_ENV_VARS];
|
|
77
|
+
const current: [string, string][] = [];
|
|
78
|
+
for (const variable of EXPLORBOT_ENV_VARS) {
|
|
79
|
+
if (!modelVars.includes(variable.name)) continue;
|
|
80
|
+
if (process.env[variable.name]) current.push([variable.name, process.env[variable.name]!]);
|
|
81
|
+
}
|
|
82
|
+
for (const [provider, { envKey }] of Object.entries(PROVIDERS)) {
|
|
83
|
+
if (process.env[envKey]) current.push([envKey, chalk.dim(`set, ${provider} usable`)]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!current.length) lines.push(chalk.dim('Nothing set — export one of the variables above, or write models into your config'), '');
|
|
87
|
+
if (current.length) lines.push(...renderSection('Currently set', current));
|
|
88
|
+
|
|
89
|
+
lines.push(chalk.bold('Example using OpenRouter:'), ` OPENROUTER_API_KEY=sk-... EXPLORBOT_AI_PROVIDER=openrouter ${getCliName()} <command>`);
|
|
90
|
+
return lines.join('\n');
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -19,6 +19,8 @@ export const PROVIDERS: Record<string, ProviderInfo> = {
|
|
|
19
19
|
sambanova: { envKey: 'SAMBANOVA_API_KEY', load: async () => (await import('sambanova-ai-provider')).createSambaNova() },
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
export const MODEL_ROLES: ModelRole[] = ['model', 'visionModel', 'agenticModel'];
|
|
23
|
+
|
|
22
24
|
let cachedOutputRoot: string | null = null;
|
|
23
25
|
|
|
24
26
|
interface PlaywrightConfig {
|
|
@@ -271,6 +273,7 @@ export const EXPLORBOT_ENV_VARS: EnvVar[] = [
|
|
|
271
273
|
{ name: 'EXPLORBOT_EPHEMERAL', description: 'Keep no state between runs — output goes to a fresh temp directory instead of the site dir' },
|
|
272
274
|
{ name: 'EXPLORBOT_KNOWLEDGE', description: 'Inline knowledge text, applied to every page' },
|
|
273
275
|
{ name: 'EXPLORBOT_KNOWLEDGE_FILE', description: 'Path to a knowledge markdown file' },
|
|
276
|
+
{ name: 'EXPLORBOT_SPEC', description: 'Docbot application spec directory or index.md, used as page knowledge' },
|
|
274
277
|
{ name: 'EXPLORBOT_API_SPEC', description: 'OpenAPI spec path for the API boat' },
|
|
275
278
|
{ name: 'EXPLORBOT_NO_BANNER', description: 'Suppress the startup banner, for machine-readable output' },
|
|
276
279
|
{ name: 'EXPLORBOT_MAX_DURATION', description: 'Wall-clock budget in minutes for an explore run; same as --max-duration' },
|
|
@@ -405,6 +408,8 @@ export class ConfigParser {
|
|
|
405
408
|
this.enterGlobalMode(this.config, target);
|
|
406
409
|
}
|
|
407
410
|
|
|
411
|
+
this.applyEnvSpec(this.config);
|
|
412
|
+
|
|
408
413
|
// Restore original directory after successful config load
|
|
409
414
|
if (options?.path && originalCwd !== process.cwd()) {
|
|
410
415
|
process.chdir(originalCwd);
|
|
@@ -555,10 +560,18 @@ export class ConfigParser {
|
|
|
555
560
|
|
|
556
561
|
config.dirs = { knowledge: 'knowledge', experience: 'experience', output: 'output' };
|
|
557
562
|
config.playwright = { ...config.playwright, browser: config.playwright?.browser || 'chromium', url: site.baseUrl };
|
|
563
|
+
materializeKnowledge(this.site.dir);
|
|
558
564
|
|
|
559
565
|
log(`Global mode: ${site.baseUrl} stored in ${this.site.dir}`);
|
|
560
566
|
}
|
|
561
567
|
|
|
568
|
+
private applyEnvSpec(config: ExplorbotConfig): void {
|
|
569
|
+
const spec = process.env.EXPLORBOT_SPEC;
|
|
570
|
+
if (!spec) return;
|
|
571
|
+
if (!config.dirs) config.dirs = { knowledge: 'knowledge', experience: 'experience', output: 'output' };
|
|
572
|
+
config.dirs.spec = spec;
|
|
573
|
+
}
|
|
574
|
+
|
|
562
575
|
private async buildEnvConfig(baseUrl: string | undefined, outputRoot: string): Promise<ExplorbotConfig> {
|
|
563
576
|
const provider = process.env.EXPLORBOT_AI_PROVIDER;
|
|
564
577
|
const modelSpec = process.env.EXPLORBOT_AI_MODEL;
|
|
@@ -754,6 +767,11 @@ export async function resolveModel(spec: string, role: ModelRole = 'model'): Pro
|
|
|
754
767
|
return createModel(spec, modelId);
|
|
755
768
|
}
|
|
756
769
|
|
|
770
|
+
export function missingModelRoles(provider: string): ModelRole[] {
|
|
771
|
+
const recommended = ConfigParser.recommendedModels()[provider] || {};
|
|
772
|
+
return MODEL_ROLES.filter((role) => !recommended[role]);
|
|
773
|
+
}
|
|
774
|
+
|
|
757
775
|
export class ConfigMissingError extends Error {}
|
|
758
776
|
|
|
759
777
|
export function envConfigRequested(): boolean {
|
|
@@ -775,6 +793,7 @@ export function missingConfigMessage(configFile = 'explorbot.config.js'): string
|
|
|
775
793
|
EXPLORBOT_AI_PROVIDER=openrouter EXPLORBOT_URL=https://your-app.example.com ${cli} ...
|
|
776
794
|
|
|
777
795
|
Providers: ${Object.keys(PROVIDERS).join(', ')}
|
|
796
|
+
See the models each one recommends: ${cli} recommended-models
|
|
778
797
|
`;
|
|
779
798
|
}
|
|
780
799
|
|
|
@@ -807,8 +826,7 @@ export function configuredModels(ai?: AIConfig): Record<string, ConfiguredModel>
|
|
|
807
826
|
export async function resolveConfigModels(ai?: AIConfig): Promise<void> {
|
|
808
827
|
if (!ai) return;
|
|
809
828
|
|
|
810
|
-
const
|
|
811
|
-
for (const role of roles) {
|
|
829
|
+
for (const role of MODEL_ROLES) {
|
|
812
830
|
if (typeof ai[role] === 'string') ai[role] = await resolveModel(ai[role], role);
|
|
813
831
|
}
|
|
814
832
|
|
|
@@ -127,7 +127,7 @@ export class ExperienceTracker {
|
|
|
127
127
|
url: state.url ? extractStatePath(state.url) : '',
|
|
128
128
|
title: state.title,
|
|
129
129
|
};
|
|
130
|
-
if (state.overlay.
|
|
130
|
+
if (state.overlay.isOpen && state.overlay.name) {
|
|
131
131
|
frontmatter.region = state.overlay.name;
|
|
132
132
|
if (state.overlay.root) frontmatter.root = state.overlay.root;
|
|
133
133
|
}
|
package/src/explorbot.ts
CHANGED
|
@@ -166,7 +166,7 @@ export class ExplorBot {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
knowledgeTracker(): KnowledgeTracker {
|
|
169
|
-
return (this._knowledgeTracker ||= new KnowledgeTracker(this.options.applicationSpec));
|
|
169
|
+
return (this._knowledgeTracker ||= new KnowledgeTracker({ applicationSpec: this.options.applicationSpec }));
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
experienceTracker(): ExperienceTracker {
|
|
@@ -465,9 +465,9 @@ export class ExplorBot {
|
|
|
465
465
|
}
|
|
466
466
|
|
|
467
467
|
loadPlans(filename: string): Plan[] {
|
|
468
|
-
const
|
|
469
|
-
if (!
|
|
470
|
-
return parsePlansFromMarkdown(
|
|
468
|
+
const filePath = Plan.resolveFile(filename, this.getPlansDir());
|
|
469
|
+
if (!filePath) throw new Error(`Plan file not found: ${filename}`);
|
|
470
|
+
return parsePlansFromMarkdown(filePath);
|
|
471
471
|
}
|
|
472
472
|
|
|
473
473
|
setCurrentPlan(plan?: Plan): void {
|
package/src/global-config.ts
CHANGED
|
@@ -47,6 +47,14 @@ export function listSites(): SiteRecord[] {
|
|
|
47
47
|
.sort((a, b) => b.lastRunAt.localeCompare(a.lastRunAt));
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export function findSiteWith(subpath: string): SiteRecord | undefined {
|
|
51
|
+
return listSites().find((site) => existsSync(join(site.dir, subpath)));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function listSitePlanDirs(): string[] {
|
|
55
|
+
return listSites().map((site) => join(site.dir, 'output', 'plans'));
|
|
56
|
+
}
|
|
57
|
+
|
|
50
58
|
export function registerSite(baseUrl: string): SiteRecord {
|
|
51
59
|
const folder = siteFolderName(baseUrl);
|
|
52
60
|
const dir = join(sitesDir(), folder);
|
package/src/knowledge-tracker.ts
CHANGED
|
@@ -11,36 +11,54 @@ import { loadMarkdownFiles } from './utils/markdown-files.js';
|
|
|
11
11
|
import { mdq } from './utils/markdown-query.js';
|
|
12
12
|
import { isSecretName, registerSecret } from './utils/secrets.js';
|
|
13
13
|
import { slugify } from './utils/strings.js';
|
|
14
|
+
import { extractStatePath, matchesUrl } from './utils/url-matcher.js';
|
|
14
15
|
|
|
15
16
|
const debugLog = createDebug('explorbot:knowledge-tracker');
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
filePath: string;
|
|
19
|
-
url: string;
|
|
20
|
-
content: string;
|
|
21
|
-
[key: string]: any;
|
|
22
|
-
}
|
|
18
|
+
const sessionEntries: string[] = [];
|
|
23
19
|
|
|
24
20
|
export class KnowledgeTracker {
|
|
25
21
|
private knowledgeDir: string;
|
|
26
22
|
private knowledgeFiles: Knowledge[] = [];
|
|
23
|
+
private sessionKnowledge: Knowledge[] = [];
|
|
27
24
|
private isLoaded = false;
|
|
28
25
|
private applicationSpec?: ApplicationSpec;
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
static appendSessionKnowledge(text: string): void {
|
|
28
|
+
sessionEntries.push(text);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static resetSessionKnowledge(): void {
|
|
32
|
+
sessionEntries.length = 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
constructor(options: KnowledgeTrackerOptions = {}) {
|
|
36
|
+
let knowledgeDir = options.knowledgeDir;
|
|
37
|
+
let specPath = options.applicationSpec;
|
|
38
|
+
|
|
39
|
+
if (!knowledgeDir) {
|
|
40
|
+
const configParser = ConfigParser.getInstance();
|
|
41
|
+
const config = configParser.getConfig();
|
|
42
|
+
knowledgeDir = configParser.resolveProjectDir(config.dirs?.knowledge || 'knowledge');
|
|
43
|
+
specPath ||= config.dirs?.spec;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.knowledgeDir = knowledgeDir;
|
|
34
47
|
|
|
35
48
|
if (!existsSync(this.knowledgeDir)) {
|
|
36
49
|
mkdirSync(this.knowledgeDir, { recursive: true });
|
|
37
50
|
}
|
|
38
51
|
|
|
39
|
-
const specPath = applicationSpecPath || config.dirs?.spec;
|
|
40
52
|
if (specPath) {
|
|
41
53
|
this.applicationSpec = new ApplicationSpec(specPath);
|
|
42
54
|
tag('info').log(`Loaded application spec with ${this.applicationSpec.pageCount} documented pages`);
|
|
43
55
|
}
|
|
56
|
+
|
|
57
|
+
this.sessionKnowledge = sessionEntries.map((entry, index) => {
|
|
58
|
+
const parsed = matter(entry);
|
|
59
|
+
debugLog(`Session knowledge #${index + 1}`);
|
|
60
|
+
return this.toKnowledge(`--knowledge #${index + 1}`, parsed.data, parsed.content.trim());
|
|
61
|
+
});
|
|
44
62
|
}
|
|
45
63
|
|
|
46
64
|
private loadKnowledgeFiles(): void {
|
|
@@ -49,12 +67,7 @@ export class KnowledgeTracker {
|
|
|
49
67
|
this.knowledgeFiles = [];
|
|
50
68
|
|
|
51
69
|
for (const entry of loadMarkdownFiles(this.knowledgeDir, { recursive: true })) {
|
|
52
|
-
this.knowledgeFiles.push(
|
|
53
|
-
filePath: entry.filePath,
|
|
54
|
-
url: entry.data.url || entry.data.path || '*',
|
|
55
|
-
content: this.interpolateVars(entry.content),
|
|
56
|
-
...entry.data,
|
|
57
|
-
});
|
|
70
|
+
this.knowledgeFiles.push(this.toKnowledge(entry.filePath, entry.data, entry.content));
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
this.isLoaded = true;
|
|
@@ -63,28 +76,22 @@ export class KnowledgeTracker {
|
|
|
63
76
|
getRelevantKnowledge(state: ActionResult): Knowledge[] {
|
|
64
77
|
this.loadKnowledgeFiles();
|
|
65
78
|
|
|
66
|
-
return this.
|
|
67
|
-
return state.isMatchedBy(knowledge);
|
|
68
|
-
});
|
|
79
|
+
return this.allKnowledge().filter((knowledge) => knowledge.url && state.isMatchedBy(knowledge));
|
|
69
80
|
}
|
|
70
81
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
getEndpointKnowledge(endpoint: string): Knowledge[] {
|
|
83
|
+
this.loadKnowledgeFiles();
|
|
84
|
+
const path = extractStatePath(endpoint);
|
|
74
85
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.filter((k) => !!k)
|
|
78
|
-
.join('\n\n');
|
|
86
|
+
return this.allKnowledge().filter((knowledge) => knowledge.endpoint && matchesUrl(knowledge.endpoint, path));
|
|
87
|
+
}
|
|
79
88
|
|
|
80
|
-
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
Here is relevant knowledge for this page:
|
|
89
|
+
renderRelevantKnowledge(state: ActionResult): string {
|
|
90
|
+
return this.renderKnowledge(this.getRelevantKnowledge(state), 'page');
|
|
91
|
+
}
|
|
84
92
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
`;
|
|
93
|
+
renderEndpointKnowledge(endpoint: string): string {
|
|
94
|
+
return this.renderKnowledge(this.getEndpointKnowledge(endpoint), 'endpoint');
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
renderRelevantContext(state: ActionResult): string {
|
|
@@ -188,7 +195,7 @@ export class KnowledgeTracker {
|
|
|
188
195
|
getExistingUrls(): string[] {
|
|
189
196
|
this.loadKnowledgeFiles();
|
|
190
197
|
|
|
191
|
-
return this.knowledgeFiles.map((knowledge) => knowledge.url).filter((url) => url && url !== '*');
|
|
198
|
+
return this.knowledgeFiles.map((knowledge) => knowledge.url || '').filter((url) => url && url !== '*');
|
|
192
199
|
}
|
|
193
200
|
|
|
194
201
|
getKnowledgeForUrl(urlPattern: string): string[] {
|
|
@@ -205,7 +212,7 @@ export class KnowledgeTracker {
|
|
|
205
212
|
const content = knowledge.content.trim();
|
|
206
213
|
const firstLine = mdq(content).meta()[0]?.text.split('\n')[0]?.trim() || '';
|
|
207
214
|
return {
|
|
208
|
-
url: knowledge.url,
|
|
215
|
+
url: knowledge.url || knowledge.endpoint || '',
|
|
209
216
|
firstLine,
|
|
210
217
|
filePath: knowledge.filePath,
|
|
211
218
|
};
|
|
@@ -242,4 +249,55 @@ export class KnowledgeTracker {
|
|
|
242
249
|
|
|
243
250
|
return result;
|
|
244
251
|
}
|
|
252
|
+
|
|
253
|
+
private allKnowledge(): Knowledge[] {
|
|
254
|
+
return [...this.knowledgeFiles, ...this.sessionKnowledge];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private toKnowledge(filePath: string, data: Record<string, any>, content: string): Knowledge {
|
|
258
|
+
const knowledge: Knowledge = {
|
|
259
|
+
...data,
|
|
260
|
+
filePath,
|
|
261
|
+
url: data.url || data.path,
|
|
262
|
+
content: this.interpolateVars(content),
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
if (!data.url && !data.path && !data.endpoint) {
|
|
266
|
+
knowledge.url = '*';
|
|
267
|
+
knowledge.endpoint = '*';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return knowledge;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private renderKnowledge(knowledgeFiles: Knowledge[], scope: string): string {
|
|
274
|
+
if (knowledgeFiles.length === 0) return '';
|
|
275
|
+
|
|
276
|
+
const knowledgeContent = knowledgeFiles
|
|
277
|
+
.map((k) => k.content)
|
|
278
|
+
.filter((k) => !!k)
|
|
279
|
+
.join('\n\n');
|
|
280
|
+
|
|
281
|
+
tag('operation').log(`Found ${knowledgeFiles.length} relevant knowledge ${pluralize(knowledgeFiles.length, 'file')}`);
|
|
282
|
+
return dedent`
|
|
283
|
+
<knowledge>
|
|
284
|
+
Here is relevant knowledge for this ${scope}:
|
|
285
|
+
|
|
286
|
+
${knowledgeContent}
|
|
287
|
+
</knowledge>
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export interface Knowledge {
|
|
293
|
+
filePath: string;
|
|
294
|
+
url?: string;
|
|
295
|
+
endpoint?: string;
|
|
296
|
+
content: string;
|
|
297
|
+
[key: string]: any;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export interface KnowledgeTrackerOptions {
|
|
301
|
+
applicationSpec?: string;
|
|
302
|
+
knowledgeDir?: string;
|
|
245
303
|
}
|
package/src/remote.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Command } from 'commander';
|
|
2
1
|
import stripAnsi from 'strip-ansi';
|
|
3
2
|
import { type ActivityEntry, addActivityListener } from './activity.ts';
|
|
4
3
|
import { executionController } from './execution-controller.ts';
|
|
@@ -30,15 +29,6 @@ export class Remote implements LogDestination {
|
|
|
30
29
|
private askCounter = 0;
|
|
31
30
|
private lastActivity: string | null = null;
|
|
32
31
|
|
|
33
|
-
registerOption(program: Command): void {
|
|
34
|
-
program.option('--ws <url>', 'Stream this run to a remote UI over WebSocket');
|
|
35
|
-
program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
36
|
-
const url = actionCommand.optsWithGlobals().ws || process.env.EXPLORBOT_WS_URL;
|
|
37
|
-
if (!url) return;
|
|
38
|
-
this.attach(String(url), this.commandPath(actionCommand));
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
32
|
attach(url: string, command: string): void {
|
|
43
33
|
if (this.url) return;
|
|
44
34
|
this.url = url;
|
|
@@ -225,16 +215,6 @@ export class Remote implements LogDestination {
|
|
|
225
215
|
if (typeof failure.message === 'string') return failure.message;
|
|
226
216
|
return undefined;
|
|
227
217
|
}
|
|
228
|
-
|
|
229
|
-
private commandPath(command: Command): string {
|
|
230
|
-
const parts: string[] = [];
|
|
231
|
-
let node: Command | null = command;
|
|
232
|
-
while (node) {
|
|
233
|
-
parts.unshift(node.name());
|
|
234
|
-
node = node.parent;
|
|
235
|
-
}
|
|
236
|
-
return parts.slice(1).join(' ') || parts.join(' ');
|
|
237
|
-
}
|
|
238
218
|
}
|
|
239
219
|
|
|
240
220
|
export const remote = new Remote();
|