explorbot 0.2.4 → 0.3.0
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 +19 -7
- package/boat/api-tester/src/cli.ts +17 -0
- package/boat/doc-collector/src/cli.ts +14 -1
- package/boat/prima/README.md +96 -0
- package/boat/prima/package.json +14 -10
- package/boat/prima/src/cli.ts +29 -12
- package/boat/prima/src/envelope.ts +35 -13
- package/boat/prima/src/prima.ts +78 -45
- package/dist/bin/explorbot-cli.js +19 -7
- package/dist/boat/api-tester/src/cli.js +17 -0
- package/dist/boat/doc-collector/src/cli.js +14 -1
- package/dist/boat/prima/src/cli.js +26 -7
- package/dist/boat/prima/src/envelope.js +32 -8
- package/dist/boat/prima/src/prima.js +75 -43
- package/dist/models.json +4 -4
- package/dist/package.json +6 -2
- package/dist/src/action-result.d.ts +13 -0
- package/dist/src/action-result.js +46 -15
- package/dist/src/action.d.ts +5 -2
- package/dist/src/action.js +53 -18
- package/dist/src/ai/captain/web-mode.js +1 -2
- package/dist/src/ai/captain.d.ts +20 -0
- package/dist/src/ai/captain.js +10 -1
- package/dist/src/ai/driller.js +6 -2
- package/dist/src/ai/fisherman-tools.d.ts +40 -1
- package/dist/src/ai/fisherman-tools.js +39 -0
- package/dist/src/ai/fisherman.js +2 -1
- package/dist/src/ai/navigator.d.ts +28 -0
- package/dist/src/ai/navigator.js +223 -175
- package/dist/src/ai/pilot.d.ts +7 -4
- package/dist/src/ai/pilot.js +89 -30
- package/dist/src/ai/planner/subpages.js +2 -16
- package/dist/src/ai/planner.js +1 -1
- package/dist/src/ai/provider.d.ts +2 -2
- package/dist/src/ai/provider.js +28 -22
- package/dist/src/ai/researcher/cache.d.ts +10 -3
- package/dist/src/ai/researcher/cache.js +23 -10
- package/dist/src/ai/researcher/deep-analysis.js +1 -1
- package/dist/src/ai/researcher/fingerprint-worker.js +21 -4
- package/dist/src/ai/researcher.js +6 -4
- package/dist/src/ai/rules.js +1 -5
- package/dist/src/ai/session-analyst.js +2 -0
- package/dist/src/ai/tester.d.ts +6 -3
- package/dist/src/ai/tester.js +30 -35
- package/dist/src/ai/tools.d.ts +8 -5
- package/dist/src/ai/tools.js +83 -57
- package/dist/src/commands/config-command.d.ts +51 -0
- package/dist/src/commands/config-command.js +117 -0
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/init-command.js +13 -20
- package/dist/src/config.d.ts +8 -1
- package/dist/src/config.js +43 -1
- package/dist/src/experience-tracker.d.ts +2 -0
- package/dist/src/experience-tracker.js +12 -0
- package/dist/src/explorbot.js +5 -2
- package/dist/src/playwright-recorder.js +6 -12
- package/dist/src/remote.d.ts +3 -2
- package/dist/src/remote.js +8 -2
- package/dist/src/state-manager.d.ts +1 -1
- package/dist/src/state-manager.js +3 -1
- package/dist/src/test-plan.d.ts +9 -0
- package/dist/src/test-plan.js +30 -0
- package/dist/src/utils/html-diff.d.ts +5 -0
- package/dist/src/utils/html-diff.js +65 -6
- package/dist/src/utils/logger.d.ts +1 -1
- package/dist/src/utils/logger.js +8 -0
- package/dist/src/utils/strings.d.ts +2 -0
- package/dist/src/utils/strings.js +32 -0
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +31 -2
- package/docs/basics/getting-started.md +33 -10
- package/docs/basics/providers.md +6 -4
- package/docs/contributing/npm-package.md +73 -4
- package/docs/index.json +2 -1
- package/docs/reference/commands.md +3 -0
- package/docs/reference/websocket.md +50 -0
- package/docs/superpowers/specs/2026-08-18-prima-false-verdicts.md +159 -0
- package/models.json +4 -4
- package/package.json +6 -2
- package/src/action-result.ts +61 -16
- package/src/action.ts +56 -18
- package/src/ai/captain/web-mode.ts +1 -2
- package/src/ai/captain.ts +9 -1
- package/src/ai/driller.ts +6 -2
- package/src/ai/fisherman-tools.ts +35 -0
- package/src/ai/fisherman.ts +2 -1
- package/src/ai/navigator.ts +238 -179
- package/src/ai/pilot.ts +104 -36
- package/src/ai/planner/subpages.ts +2 -13
- package/src/ai/planner.ts +1 -1
- package/src/ai/provider.ts +29 -21
- package/src/ai/researcher/cache.ts +29 -11
- package/src/ai/researcher/deep-analysis.ts +1 -1
- package/src/ai/researcher/fingerprint-worker.ts +23 -5
- package/src/ai/researcher.ts +6 -4
- package/src/ai/rules.ts +1 -5
- package/src/ai/session-analyst.ts +2 -0
- package/src/ai/tester.ts +33 -34
- package/src/ai/tools.ts +88 -61
- package/src/commands/config-command.ts +146 -0
- package/src/commands/index.ts +2 -0
- package/src/commands/init-command.ts +14 -20
- package/src/config.ts +47 -2
- package/src/experience-tracker.ts +13 -0
- package/src/explorbot.ts +4 -2
- package/src/playwright-recorder.ts +6 -11
- package/src/remote.ts +8 -2
- package/src/state-manager.ts +5 -2
- package/src/test-plan.ts +38 -0
- package/src/utils/html-diff.ts +72 -7
- package/src/utils/logger.ts +9 -1
- package/src/utils/strings.ts +36 -0
- package/src/utils/url-matcher.ts +27 -2
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { type AIConfig, ConfigParser, EXPLORBOT_ENV_VARS, type ReporterConfig, configuredModels } from '../config.js';
|
|
5
|
+
import { listSites } from '../global-config.js';
|
|
6
|
+
import { Reporter } from '../reporter.js';
|
|
7
|
+
import { getCliName } from '../utils/cli-name.js';
|
|
8
|
+
import { tag } from '../utils/logger.js';
|
|
9
|
+
import { BaseCommand } from './base-command.js';
|
|
10
|
+
|
|
11
|
+
export class ConfigCommand extends BaseCommand {
|
|
12
|
+
name = 'config';
|
|
13
|
+
description = 'Show models, config file and paths used by this run';
|
|
14
|
+
|
|
15
|
+
async execute(): Promise<void> {
|
|
16
|
+
const parser = ConfigParser.getInstance();
|
|
17
|
+
tag('info').log(ConfigCommand.render(this.explorBot.getConfig(), { configPath: parser.getConfigPath(), root: parser.getProjectRoot() }));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static async summary(options: { config?: string; path?: string; url?: string; json?: boolean } = {}): Promise<string> {
|
|
21
|
+
const parser = ConfigParser.getInstance();
|
|
22
|
+
const [site] = listSites();
|
|
23
|
+
const load = (baseUrl?: string) => parser.loadConfig({ config: options.config, path: options.path, baseUrl });
|
|
24
|
+
|
|
25
|
+
const config = await load(options.url).catch((error) => {
|
|
26
|
+
if (!site) throw error;
|
|
27
|
+
return load(site.url);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return ConfigCommand.render(config, { configPath: parser.getConfigPath(), root: parser.getProjectRoot(), json: options.json });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static data(config: SummarizedConfig, options: ConfigSummaryOptions = {}): ConfigData {
|
|
34
|
+
let configPath = '';
|
|
35
|
+
if (options.configPath && existsSync(options.configPath)) configPath = options.configPath;
|
|
36
|
+
|
|
37
|
+
const dirs: Record<string, string> = {};
|
|
38
|
+
if (options.root) {
|
|
39
|
+
for (const [name, dir] of Object.entries({ output: 'output', ...config.dirs })) {
|
|
40
|
+
dirs[name] = path.join(options.root, dir);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const env: Record<string, string> = {};
|
|
45
|
+
for (const variable of EXPLORBOT_ENV_VARS) {
|
|
46
|
+
const value = process.env[variable.name];
|
|
47
|
+
if (value) env[variable.name] = value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const models: Record<string, string> = {};
|
|
51
|
+
const providers: Record<string, string> = {};
|
|
52
|
+
for (const [role, model] of Object.entries(configuredModels(config.ai))) {
|
|
53
|
+
models[role] = model.name;
|
|
54
|
+
if (model.provider) providers[role] = model.provider;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
config: configPath,
|
|
59
|
+
url: config.playwright?.url || config.web?.url || config.api?.baseEndpoint || '',
|
|
60
|
+
browser: config.playwright?.browser || '',
|
|
61
|
+
headless: !config.playwright?.show,
|
|
62
|
+
dirs,
|
|
63
|
+
models,
|
|
64
|
+
providers,
|
|
65
|
+
integrations: {
|
|
66
|
+
langfuse: !!config.ai?.langfuse?.enabled,
|
|
67
|
+
testomatio: Reporter.resolveEnabled(config.reporter),
|
|
68
|
+
},
|
|
69
|
+
env,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static render(config: SummarizedConfig, options: ConfigSummaryOptions = {}): string {
|
|
74
|
+
const data = ConfigCommand.data(config, options);
|
|
75
|
+
if (options.json) return JSON.stringify(data, null, 2);
|
|
76
|
+
|
|
77
|
+
const lines: string[] = [];
|
|
78
|
+
const section = (title: string, entries: [string, string][]) => {
|
|
79
|
+
if (!entries.length) return;
|
|
80
|
+
const width = Math.max(...entries.map(([label]) => label.length));
|
|
81
|
+
lines.push(chalk.bold(title));
|
|
82
|
+
for (const [label, value] of entries) lines.push(` ${chalk.dim(label.padEnd(width))} ${value}`);
|
|
83
|
+
lines.push('');
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const general: [string, string][] = [['config', data.config || 'EXPLORBOT_* environment variables']];
|
|
87
|
+
if (data.url) general.push(['url', data.url]);
|
|
88
|
+
if (data.browser) {
|
|
89
|
+
let window = 'visible';
|
|
90
|
+
if (data.headless) window = 'headless';
|
|
91
|
+
general.push(['browser', `${data.browser}, ${window}`]);
|
|
92
|
+
}
|
|
93
|
+
for (const [name, dir] of Object.entries(data.dirs)) general.push([name, dir]);
|
|
94
|
+
section('Config', general);
|
|
95
|
+
|
|
96
|
+
const providerWidth = Math.max(0, ...Object.values(data.providers).map((provider) => provider.length));
|
|
97
|
+
const models: [string, string][] = Object.entries(data.models).map(([role, model]) => {
|
|
98
|
+
if (!providerWidth) return [role, model];
|
|
99
|
+
return [role, `${chalk.dim((data.providers[role] || '').padEnd(providerWidth))} ${model}`];
|
|
100
|
+
});
|
|
101
|
+
if (!models.length) models.push(['model', chalk.red(`not configured — run ${getCliName()} init`)]);
|
|
102
|
+
section('Models', models);
|
|
103
|
+
|
|
104
|
+
const integrations: [string, string][] = [];
|
|
105
|
+
if (data.integrations.langfuse) integrations.push(['langfuse', 'traces sent']);
|
|
106
|
+
if (data.integrations.testomatio) integrations.push(['testomatio', 'runs reported']);
|
|
107
|
+
section('Integrations', integrations);
|
|
108
|
+
|
|
109
|
+
const env: [string, string][] = Object.entries(data.env).map(([name, value]) => {
|
|
110
|
+
let shown = value;
|
|
111
|
+
if (shown.length > 60) shown = `${shown.slice(0, 57)}...`;
|
|
112
|
+
return [name, shown];
|
|
113
|
+
});
|
|
114
|
+
section('Environment', env);
|
|
115
|
+
|
|
116
|
+
lines.push(chalk.dim(`Every EXPLORBOT_* variable: ${getCliName()} --help`));
|
|
117
|
+
return lines.join('\n');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface ConfigSummaryOptions {
|
|
122
|
+
configPath?: string | null;
|
|
123
|
+
root?: string;
|
|
124
|
+
json?: boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface ConfigData {
|
|
128
|
+
config: string;
|
|
129
|
+
url: string;
|
|
130
|
+
browser: string;
|
|
131
|
+
headless: boolean;
|
|
132
|
+
dirs: Record<string, string>;
|
|
133
|
+
models: Record<string, string>;
|
|
134
|
+
providers: Record<string, string>;
|
|
135
|
+
integrations: { langfuse: boolean; testomatio: boolean };
|
|
136
|
+
env: Record<string, string>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface SummarizedConfig {
|
|
140
|
+
ai?: AIConfig;
|
|
141
|
+
playwright?: { url?: string; browser?: string; show?: boolean };
|
|
142
|
+
web?: { url?: string };
|
|
143
|
+
api?: { baseEndpoint?: string };
|
|
144
|
+
dirs?: Record<string, string>;
|
|
145
|
+
reporter?: ReporterConfig;
|
|
146
|
+
}
|
package/src/commands/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { AddRuleCommand } from './add-rule-command.js';
|
|
|
3
3
|
import type { BaseCommand } from './base-command.js';
|
|
4
4
|
import { CleanCommand } from './clean-command.js';
|
|
5
5
|
import { CompactCommand } from './compact-command.js';
|
|
6
|
+
import { ConfigCommand } from './config-command.js';
|
|
6
7
|
import { ContextAriaCommand } from './context-aria-command.js';
|
|
7
8
|
import { ContextCommand } from './context-command.js';
|
|
8
9
|
import { ContextDataCommand } from './context-data-command.js';
|
|
@@ -70,6 +71,7 @@ const commandClasses: CommandClass[] = [
|
|
|
70
71
|
RunsCommand,
|
|
71
72
|
RerunCommand,
|
|
72
73
|
StatusCommand,
|
|
74
|
+
ConfigCommand,
|
|
73
75
|
DebugCommand,
|
|
74
76
|
ExitCommand,
|
|
75
77
|
];
|
|
@@ -8,15 +8,10 @@ import { getCliName } from '../utils/cli-name.ts';
|
|
|
8
8
|
import { log, tag } from '../utils/logger.js';
|
|
9
9
|
import { relativeToCwd } from '../utils/next-steps.ts';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
// Bring your own provider or use OpenRouter (one API key, many providers).
|
|
16
|
-
// https://github.com/testomatio/explorbot/blob/main/docs/providers.md
|
|
17
|
-
const openrouter = createOpenRouter({
|
|
18
|
-
apiKey: process.env.OPENROUTER_API_KEY,
|
|
19
|
-
});
|
|
11
|
+
function defaultConfigTemplate(): string {
|
|
12
|
+
return `// 'provider/model-id' uses a bundled provider.
|
|
13
|
+
// It is also possible to import provider as a module from Vercel AI SDK.
|
|
14
|
+
// https://github.com/testomatio/explorbot/blob/main/docs/basics/providers.md
|
|
20
15
|
|
|
21
16
|
const config = {
|
|
22
17
|
web: {
|
|
@@ -25,12 +20,7 @@ const config = {
|
|
|
25
20
|
},
|
|
26
21
|
|
|
27
22
|
ai: {
|
|
28
|
-
|
|
29
|
-
model: openrouter('openai/gpt-oss-20b:nitro'),
|
|
30
|
-
// vision model for screenshot analysis
|
|
31
|
-
visionModel: openrouter('meta-llama/llama-4-scout-17b-16e-instruct'),
|
|
32
|
-
// agentic model for decision making
|
|
33
|
-
agenticModel: openrouter('minimax/minimax-m2.5:nitro'),
|
|
23
|
+
${modelLines('openrouter')}
|
|
34
24
|
},
|
|
35
25
|
|
|
36
26
|
reporter: {
|
|
@@ -45,6 +35,7 @@ const config = {
|
|
|
45
35
|
|
|
46
36
|
export default config;
|
|
47
37
|
`;
|
|
38
|
+
}
|
|
48
39
|
|
|
49
40
|
const DEFAULT_ENV_TEMPLATE = dedent`
|
|
50
41
|
# AI provider API keys
|
|
@@ -141,7 +132,7 @@ export function runInitCommand(options: InitCommandOptions): void {
|
|
|
141
132
|
process.exit(1);
|
|
142
133
|
}
|
|
143
134
|
|
|
144
|
-
writeFileSync(outPath,
|
|
135
|
+
writeFileSync(outPath, defaultConfigTemplate(), 'utf8');
|
|
145
136
|
log(`Created config file: ${relativeToCwd(outPath)}`);
|
|
146
137
|
|
|
147
138
|
const envPath = resolve(process.cwd(), '.env');
|
|
@@ -222,8 +213,7 @@ async function renderInitWizard(mode: 'choose' | 'global'): Promise<'local' | 'g
|
|
|
222
213
|
});
|
|
223
214
|
}
|
|
224
215
|
|
|
225
|
-
function
|
|
226
|
-
const { envKey } = PROVIDERS[provider];
|
|
216
|
+
function modelLines(provider: string): string {
|
|
227
217
|
const recommended = ConfigParser.recommendedModels()[provider] || {};
|
|
228
218
|
const roles: Array<[ModelRoleName, string]> = [
|
|
229
219
|
['model', 'fast model with tool calling capabilities'],
|
|
@@ -231,7 +221,11 @@ function globalConfigTemplate(provider: string): string {
|
|
|
231
221
|
['agenticModel', 'agentic model for decision making'],
|
|
232
222
|
];
|
|
233
223
|
|
|
234
|
-
|
|
224
|
+
return roles.map(([role, comment]) => ` // ${comment}\n ${role}: '${provider}/${recommended[role] || '<model-id>'}',`).join('\n');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function globalConfigTemplate(provider: string): string {
|
|
228
|
+
const { envKey } = PROVIDERS[provider];
|
|
235
229
|
|
|
236
230
|
return `// Global Explorbot configuration — used by every directory without its own explorbot.config.js.
|
|
237
231
|
// Models are written as 'provider/model-id' so they resolve without a local node_modules.
|
|
@@ -240,7 +234,7 @@ function globalConfigTemplate(provider: string): string {
|
|
|
240
234
|
// https://github.com/testomatio/explorbot/blob/main/docs/basics/providers.md
|
|
241
235
|
const config = {
|
|
242
236
|
ai: {
|
|
243
|
-
${
|
|
237
|
+
${modelLines(provider)}
|
|
244
238
|
},
|
|
245
239
|
|
|
246
240
|
reporter: {
|
package/src/config.ts
CHANGED
|
@@ -433,7 +433,7 @@ export class ConfigParser {
|
|
|
433
433
|
public getOutputDir(): string {
|
|
434
434
|
const config = this.getConfig();
|
|
435
435
|
if (!this.configPath) throw new Error('Config path not found');
|
|
436
|
-
return
|
|
436
|
+
return this.resolveProjectDir(config.dirs?.output || 'output');
|
|
437
437
|
}
|
|
438
438
|
|
|
439
439
|
public getProjectRoot(): string {
|
|
@@ -444,6 +444,7 @@ export class ConfigParser {
|
|
|
444
444
|
}
|
|
445
445
|
|
|
446
446
|
public resolveProjectDir(relativeDir: string): string {
|
|
447
|
+
if (path.isAbsolute(relativeDir)) return relativeDir;
|
|
447
448
|
if (!this.configPath) return relativeDir;
|
|
448
449
|
return path.join(this.getProjectRoot(), relativeDir);
|
|
449
450
|
}
|
|
@@ -514,6 +515,7 @@ export class ConfigParser {
|
|
|
514
515
|
model: { modelId: 'test-model', provider: 'test' },
|
|
515
516
|
config: {},
|
|
516
517
|
vision: false,
|
|
518
|
+
langfuse: { enabled: false },
|
|
517
519
|
},
|
|
518
520
|
dirs: {
|
|
519
521
|
knowledge: join(testBaseDir, 'knowledge'),
|
|
@@ -650,6 +652,18 @@ export class ConfigParser {
|
|
|
650
652
|
config.playwright.url = options.baseUrl;
|
|
651
653
|
}
|
|
652
654
|
|
|
655
|
+
if (config.ai) {
|
|
656
|
+
const langfuse = config.ai.langfuse;
|
|
657
|
+
const publicKey = langfuse?.publicKey || process.env.LANGFUSE_PUBLIC_KEY;
|
|
658
|
+
const secretKey = langfuse?.secretKey || process.env.LANGFUSE_SECRET_KEY;
|
|
659
|
+
config.ai.langfuse = {
|
|
660
|
+
enabled: langfuse?.enabled ?? Boolean(publicKey && secretKey),
|
|
661
|
+
publicKey,
|
|
662
|
+
secretKey,
|
|
663
|
+
baseUrl: langfuse?.baseUrl || process.env.LANGFUSE_BASE_URL || process.env.LANGFUSE_HOST,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
653
667
|
return config;
|
|
654
668
|
}
|
|
655
669
|
|
|
@@ -762,6 +776,32 @@ export function missingConfigMessage(configFile = 'explorbot.config.js'): string
|
|
|
762
776
|
`;
|
|
763
777
|
}
|
|
764
778
|
|
|
779
|
+
export function modelName(model: unknown): string {
|
|
780
|
+
if (typeof model === 'string') return model;
|
|
781
|
+
return (model as any)?.modelId || (model as any)?.model || 'unknown';
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
export function modelProvider(model: unknown): string {
|
|
785
|
+
const provider = (model as any)?.provider;
|
|
786
|
+
if (typeof provider === 'string') return provider.split('.')[0];
|
|
787
|
+
if (typeof model === 'string') return model.split('/')[0];
|
|
788
|
+
return '';
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export function configuredModels(ai?: AIConfig): Record<string, ConfiguredModel> {
|
|
792
|
+
if (!ai?.model) return {};
|
|
793
|
+
|
|
794
|
+
const describe = (model: unknown): ConfiguredModel => ({ name: modelName(model), provider: modelProvider(model) });
|
|
795
|
+
|
|
796
|
+
const models: Record<string, ConfiguredModel> = { model: describe(ai.model) };
|
|
797
|
+
if (ai.agenticModel) models.agenticModel = describe(ai.agenticModel);
|
|
798
|
+
if (ai.visionModel) models.visionModel = describe(ai.visionModel);
|
|
799
|
+
for (const [agent, agentConfig] of Object.entries(ai.agents || {})) {
|
|
800
|
+
if (agentConfig?.model) models[agent] = describe(agentConfig.model);
|
|
801
|
+
}
|
|
802
|
+
return models;
|
|
803
|
+
}
|
|
804
|
+
|
|
765
805
|
export async function resolveConfigModels(ai?: AIConfig): Promise<void> {
|
|
766
806
|
if (!ai) return;
|
|
767
807
|
|
|
@@ -838,6 +878,11 @@ export async function createModel(provider: string, modelId: string): Promise<an
|
|
|
838
878
|
|
|
839
879
|
type ModelRole = 'model' | 'visionModel' | 'agenticModel';
|
|
840
880
|
|
|
881
|
+
interface ConfiguredModel {
|
|
882
|
+
name: string;
|
|
883
|
+
provider: string;
|
|
884
|
+
}
|
|
885
|
+
|
|
841
886
|
interface ProviderInfo {
|
|
842
887
|
envKey: string;
|
|
843
888
|
load: () => Promise<(modelId: string) => any>;
|
|
@@ -849,4 +894,4 @@ interface EnvVar {
|
|
|
849
894
|
required?: boolean;
|
|
850
895
|
}
|
|
851
896
|
|
|
852
|
-
export type { ModelRole, EnvVar, ProviderInfo };
|
|
897
|
+
export type { ModelRole, EnvVar, ProviderInfo, ConfiguredModel };
|
|
@@ -304,6 +304,14 @@ export class ExperienceTracker {
|
|
|
304
304
|
return this.buildToc(sorted);
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
renderExperienceFor(state: ActionResult): string {
|
|
308
|
+
const successful = this.getSuccessfulExperience(state);
|
|
309
|
+
if (!successful.length) return '';
|
|
310
|
+
|
|
311
|
+
tag('operation').log(`Found ${successful.length} experience ${pluralize(successful.length, 'file')} for: ${state.url}`);
|
|
312
|
+
return renderExperienceRecipes(successful);
|
|
313
|
+
}
|
|
314
|
+
|
|
307
315
|
renderExperienceTocFor(state: ActionResult): string {
|
|
308
316
|
const toc = this.getExperienceTableOfContents(state);
|
|
309
317
|
if (toc.length === 0) return '';
|
|
@@ -437,6 +445,11 @@ function indexToLetters(index: number): string {
|
|
|
437
445
|
return result;
|
|
438
446
|
}
|
|
439
447
|
|
|
448
|
+
export function renderExperienceRecipes(recipes: string[]): string {
|
|
449
|
+
if (recipes.length === 0) return '';
|
|
450
|
+
return `<experience>\nPast successful recipes recorded from prior runs for this page. Prefer these solutions first if they match the goal.\n\n${recipes.join('\n\n')}\n</experience>`;
|
|
451
|
+
}
|
|
452
|
+
|
|
440
453
|
export function renderExperienceToc(toc: ExperienceTocEntry[]): string {
|
|
441
454
|
if (toc.length === 0) return '';
|
|
442
455
|
|
package/src/explorbot.ts
CHANGED
|
@@ -248,7 +248,7 @@ export class ExplorBot {
|
|
|
248
248
|
this.agents.tester = this.createAgent((deps) => {
|
|
249
249
|
const researcher = this.agentResearcher();
|
|
250
250
|
const navigator = this.agentNavigator();
|
|
251
|
-
const tools = createAgentTools({ ...deps, researcher, navigator });
|
|
251
|
+
const tools = createAgentTools({ ...deps, researcher, navigator, withExperience: false });
|
|
252
252
|
return new Tester(deps, researcher, navigator, tools);
|
|
253
253
|
});
|
|
254
254
|
|
|
@@ -487,9 +487,11 @@ export class ExplorBot {
|
|
|
487
487
|
|
|
488
488
|
setCurrentPlan(plan?: Plan): void {
|
|
489
489
|
this.currentPlan = plan;
|
|
490
|
-
if (
|
|
490
|
+
if (!plan) return;
|
|
491
|
+
if (!this.sessionPlans.includes(plan)) {
|
|
491
492
|
this.sessionPlans.push(plan);
|
|
492
493
|
}
|
|
494
|
+
plan.notifyChange();
|
|
493
495
|
}
|
|
494
496
|
|
|
495
497
|
getSessionTests(): Test[] {
|
|
@@ -9,7 +9,7 @@ const RECORDABLE: Record<string, Set<string>> = {
|
|
|
9
9
|
Page: new Set(['goBack', 'goForward', 'reload', 'keyboardPress', 'keyboardType', 'keyboardDown', 'keyboardUp', 'keyboardInsertText', 'mouseClick', 'mouseDblclick', 'mouseMove', 'mouseDown', 'mouseUp', 'mouseWheel']),
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
const PLAYWRIGHT_INCOMPATIBLE = "Playwright output
|
|
12
|
+
const PLAYWRIGHT_INCOMPATIBLE = "Playwright output requires playwright-core 1.62 or newer (lib/coreBundle does not expose iso.asLocator). Use output.framework: 'codeceptjs' instead.";
|
|
13
13
|
|
|
14
14
|
let cachedAsLocator: ((lang: string, selector: string) => string) | null = null;
|
|
15
15
|
let asLocatorLoadAttempted = false;
|
|
@@ -20,16 +20,11 @@ function getAsLocator(): (lang: string, selector: string) => string {
|
|
|
20
20
|
if (asLocatorLoadAttempted) throw new Error(PLAYWRIGHT_INCOMPATIBLE);
|
|
21
21
|
|
|
22
22
|
asLocatorLoadAttempted = true;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
} catch {
|
|
30
|
-
// Module not exported or not found
|
|
31
|
-
}
|
|
32
|
-
throw new Error(PLAYWRIGHT_INCOMPATIBLE);
|
|
23
|
+
const asLocator = nodeRequire('playwright-core/lib/coreBundle')?.iso?.asLocator;
|
|
24
|
+
if (typeof asLocator !== 'function') throw new Error(PLAYWRIGHT_INCOMPATIBLE);
|
|
25
|
+
|
|
26
|
+
cachedAsLocator = asLocator;
|
|
27
|
+
return cachedAsLocator;
|
|
33
28
|
}
|
|
34
29
|
|
|
35
30
|
export interface TraceCall {
|
package/src/remote.ts
CHANGED
|
@@ -16,8 +16,9 @@ const FLUSH_TIMEOUT_MS = 3000;
|
|
|
16
16
|
* process and a CI bot are the same case.
|
|
17
17
|
*
|
|
18
18
|
* It **is** a LogDestination — that is the whole integration on the logger's
|
|
19
|
-
* side — and it answers asks by installing itself
|
|
20
|
-
* controller's input callback. Nothing else in explorbot
|
|
19
|
+
* side, messages and `data` alike — and it answers asks by installing itself
|
|
20
|
+
* as the execution controller's input callback. Nothing else in explorbot
|
|
21
|
+
* knows it exists.
|
|
21
22
|
*/
|
|
22
23
|
export class Remote implements LogDestination {
|
|
23
24
|
private url: string | null = null;
|
|
@@ -110,6 +111,11 @@ export class Remote implements LogDestination {
|
|
|
110
111
|
*/
|
|
111
112
|
write(entry: TaggedLogEntry): void {
|
|
112
113
|
if (entry.type === 'html') return;
|
|
114
|
+
if (entry.type === 'data') {
|
|
115
|
+
const [kind, payload] = entry.originalArgs || [];
|
|
116
|
+
this.send(String(kind), payload);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
113
119
|
let content = stripAnsi(entry.content ?? '');
|
|
114
120
|
if (content.length > CONTENT_CAP) content = `${content.slice(0, CONTENT_CAP)}… (${content.length} chars)`;
|
|
115
121
|
this.send('log', {
|
package/src/state-manager.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type FocusedElement
|
|
1
|
+
import { ActionResult, type FocusedElement } from './action-result.js';
|
|
2
2
|
import type { ExperienceTracker } from './experience-tracker.js';
|
|
3
3
|
import type { Knowledge, KnowledgeTracker } from './knowledge-tracker.js';
|
|
4
4
|
import { detectFocusArea } from './utils/aria.js';
|
|
5
|
-
import { createDebug } from './utils/logger.js';
|
|
5
|
+
import { createDebug, tag } from './utils/logger.js';
|
|
6
6
|
import { slugify } from './utils/strings.js';
|
|
7
7
|
import { extractStatePath } from './utils/url-matcher.js';
|
|
8
8
|
|
|
@@ -117,6 +117,9 @@ export class StateManager {
|
|
|
117
117
|
* Emit state change event to all listeners
|
|
118
118
|
*/
|
|
119
119
|
private emitStateChange(event: StateTransition): void {
|
|
120
|
+
const state = event.toState;
|
|
121
|
+
tag('data').log('state', { url: state.fullUrl || state.url, path: state.url, title: state.title, h1: state.h1 });
|
|
122
|
+
|
|
120
123
|
this.stateChangeListeners.forEach((listener) => {
|
|
121
124
|
try {
|
|
122
125
|
listener(event);
|
package/src/test-plan.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import figures from 'figures';
|
|
3
|
+
import type { ActionResult } from './action-result.ts';
|
|
3
4
|
import { WebPageState } from './state-manager.ts';
|
|
5
|
+
import { tag } from './utils/logger.ts';
|
|
4
6
|
import { parsePlanFromMarkdown, planToAiContext, savePlanToMarkdown, savePlansToMarkdown } from './utils/test-plan-markdown.ts';
|
|
5
7
|
import { uniqSessionName } from './utils/unique-names.ts';
|
|
6
8
|
|
|
@@ -235,6 +237,7 @@ export class Test extends Task {
|
|
|
235
237
|
startTime?: number;
|
|
236
238
|
endTime?: number;
|
|
237
239
|
resetCount = 0;
|
|
240
|
+
appliedExperience: AppliedExperience[] = [];
|
|
238
241
|
|
|
239
242
|
constructor(scenario: string, priority: 'critical' | 'important' | 'high' | 'normal' | 'low', expectedOutcome: string | string[], startUrl: string, plannedSteps: string[] = []) {
|
|
240
243
|
super(scenario, startUrl);
|
|
@@ -256,6 +259,17 @@ export class Test extends Task {
|
|
|
256
259
|
return [...new Set([this.startUrl, ...this.states.map((s) => s.url)].filter((value): value is string => Boolean(value) && value.trim() !== ''))];
|
|
257
260
|
}
|
|
258
261
|
|
|
262
|
+
applyExperience(recipes: AppliedExperience[]): void {
|
|
263
|
+
for (const recipe of recipes) {
|
|
264
|
+
if (this.appliedExperience.some((applied) => applied.content === recipe.content)) continue;
|
|
265
|
+
this.appliedExperience.push(recipe);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
getAppliedExperience(state: ActionResult): string[] {
|
|
270
|
+
return this.appliedExperience.filter((recipe) => state.isRelevantExperienceRecord({ url: recipe.url })).map((recipe) => recipe.content);
|
|
271
|
+
}
|
|
272
|
+
|
|
259
273
|
addArtifact(artifact?: string): void {
|
|
260
274
|
if (!artifact) return;
|
|
261
275
|
const timestamp = `${performance.now()}_${this.timestampCounter++}`;
|
|
@@ -317,6 +331,7 @@ export class Test extends Task {
|
|
|
317
331
|
this.startTime = performance.now();
|
|
318
332
|
this.addNote(`Test started. Session name: ${this.sessionName}`);
|
|
319
333
|
this.plan?.notifyChange();
|
|
334
|
+
this.reportStatus();
|
|
320
335
|
}
|
|
321
336
|
|
|
322
337
|
finish(result: TestResultType = TestResult.FAILED): void {
|
|
@@ -324,6 +339,7 @@ export class Test extends Task {
|
|
|
324
339
|
this.result = result;
|
|
325
340
|
this.endTime = performance.now();
|
|
326
341
|
this.plan?.notifyChange();
|
|
342
|
+
this.reportStatus();
|
|
327
343
|
}
|
|
328
344
|
|
|
329
345
|
getDurationMs(): number | null {
|
|
@@ -336,6 +352,18 @@ export class Test extends Task {
|
|
|
336
352
|
return this.expected.filter((e) => !achieved.includes(e));
|
|
337
353
|
}
|
|
338
354
|
|
|
355
|
+
private reportStatus(): void {
|
|
356
|
+
tag('data').log('test', {
|
|
357
|
+
scenario: this.scenario,
|
|
358
|
+
status: this.status,
|
|
359
|
+
result: this.result,
|
|
360
|
+
priority: this.priority,
|
|
361
|
+
sessionName: this.sessionName,
|
|
362
|
+
url: this.startUrl,
|
|
363
|
+
plan: this.plan?.title,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
339
367
|
override getLog(): Array<{ type: 'step' | 'note' | 'artifact'; content: string; timestamp: number }> {
|
|
340
368
|
const merged: Record<string, { type: 'step' | 'note' | 'artifact'; content: string }> = {};
|
|
341
369
|
|
|
@@ -407,6 +435,11 @@ export class Plan {
|
|
|
407
435
|
for (const listener of this.changeListeners) {
|
|
408
436
|
listener(this.tests);
|
|
409
437
|
}
|
|
438
|
+
tag('data').log('plan', {
|
|
439
|
+
title: this.title,
|
|
440
|
+
url: this.url,
|
|
441
|
+
tests: this.tests.map((test) => ({ scenario: test.scenario, status: test.status, result: test.result, priority: test.priority })),
|
|
442
|
+
});
|
|
410
443
|
}
|
|
411
444
|
|
|
412
445
|
getAllTests(): Test[] {
|
|
@@ -510,3 +543,8 @@ interface UrlNoteState {
|
|
|
510
543
|
h2?: string;
|
|
511
544
|
screenshotFile?: string;
|
|
512
545
|
}
|
|
546
|
+
|
|
547
|
+
interface AppliedExperience {
|
|
548
|
+
url: string;
|
|
549
|
+
content: string;
|
|
550
|
+
}
|