explorbot 0.2.3 → 0.2.5
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 +1 -1
- package/bin/explorbot-cli.ts +26 -8
- package/boat/api-tester/src/cli.ts +17 -0
- package/boat/api-tester/src/config.ts +4 -2
- package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
- package/boat/doc-collector/src/ai/documentarian.ts +61 -31
- package/boat/doc-collector/src/cli.ts +14 -1
- package/boat/doc-collector/src/config.ts +4 -2
- package/boat/prima/bin/prima-cli.ts +0 -0
- package/boat/prima/src/activity-line.ts +33 -0
- package/boat/prima/src/cli.ts +127 -86
- package/boat/prima/src/envelope.ts +102 -52
- package/boat/prima/src/prima.ts +567 -128
- package/boat/prima/src/pw-parser.ts +11 -1
- package/boat/prima/src/pw-registry.ts +4 -5
- package/boat/prima/src/session-log.ts +126 -0
- package/dist/bin/explorbot-cli.js +26 -8
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
- package/dist/boat/api-tester/src/cli.js +17 -0
- package/dist/boat/api-tester/src/config.js +4 -2
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
- package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
- package/dist/boat/doc-collector/src/cli.js +14 -1
- package/dist/boat/doc-collector/src/config.js +4 -2
- package/dist/boat/prima/src/activity-line.js +30 -0
- package/dist/boat/prima/src/cli.js +109 -77
- package/dist/boat/prima/src/envelope.js +94 -44
- package/dist/boat/prima/src/prima.js +533 -119
- package/dist/boat/prima/src/pw-parser.js +13 -1
- package/dist/boat/prima/src/pw-registry.js +4 -5
- package/dist/boat/prima/src/session-log.js +108 -0
- package/dist/package.json +3 -2
- package/dist/rules/navigator/verification-actions.md +20 -0
- package/dist/src/action-result.d.ts +7 -0
- package/dist/src/action-result.js +4 -0
- package/dist/src/action.d.ts +2 -0
- package/dist/src/action.js +41 -2
- package/dist/src/ai/captain/web-mode.js +6 -3
- package/dist/src/ai/captain.js +2 -0
- package/dist/src/ai/navigator.d.ts +34 -0
- package/dist/src/ai/navigator.js +237 -181
- package/dist/src/ai/pilot.d.ts +7 -0
- package/dist/src/ai/pilot.js +90 -2
- package/dist/src/ai/provider.d.ts +2 -2
- package/dist/src/ai/provider.js +14 -23
- package/dist/src/ai/rerunner.js +2 -1
- package/dist/src/ai/researcher/cache.d.ts +2 -0
- package/dist/src/ai/researcher/cache.js +10 -2
- package/dist/src/ai/researcher.js +3 -2
- package/dist/src/ai/rules.js +17 -10
- package/dist/src/ai/session-analyst.js +2 -0
- package/dist/src/ai/task-agent.js +4 -1
- package/dist/src/ai/tester.d.ts +6 -3
- package/dist/src/ai/tester.js +50 -46
- package/dist/src/ai/tools.d.ts +14 -0
- package/dist/src/ai/tools.js +117 -37
- 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/config.d.ts +9 -1
- package/dist/src/config.js +53 -4
- package/dist/src/execution-controller.d.ts +2 -0
- package/dist/src/execution-controller.js +6 -0
- package/dist/src/explorbot.d.ts +2 -1
- package/dist/src/explorbot.js +7 -2
- package/dist/src/explorer.js +2 -3
- package/dist/src/playwright-recorder.js +30 -0
- package/dist/src/remote.d.ts +55 -0
- package/dist/src/remote.js +235 -0
- package/dist/src/reporter.d.ts +1 -0
- package/dist/src/reporter.js +7 -1
- package/dist/src/state-manager.d.ts +2 -1
- package/dist/src/state-manager.js +3 -1
- package/dist/src/stats.d.ts +1 -0
- package/dist/src/stats.js +1 -0
- package/dist/src/test-plan.d.ts +3 -0
- package/dist/src/test-plan.js +26 -0
- package/dist/src/utils/aria.d.ts +2 -8
- package/dist/src/utils/aria.js +69 -40
- package/dist/src/utils/html.js +1 -0
- package/dist/src/utils/logger.d.ts +7 -1
- package/dist/src/utils/logger.js +32 -0
- package/dist/src/utils/page-readiness.js +18 -1
- package/dist/src/utils/url-matcher.js +3 -0
- package/dist/src/utils/web-element.d.ts +2 -0
- package/dist/src/utils/web-element.js +8 -0
- package/dist/src/utils/web-sandbox.d.ts +1 -1
- package/dist/src/utils/web-sandbox.js +2 -3
- package/docs/api-testing/basics.md +90 -0
- package/docs/api-testing/planning.md +57 -0
- package/docs/api-testing/running-tests.md +55 -0
- package/docs/assets/cloud-report.png +0 -0
- package/docs/assets/html-report.png +0 -0
- package/docs/assets/langfuse-trace.png +0 -0
- package/docs/assets/successful-explore-run.png +0 -0
- package/docs/basics/getting-started.md +140 -0
- package/docs/basics/prerequisites.md +63 -0
- package/docs/basics/providers.md +362 -0
- package/docs/basics/running.md +78 -0
- package/docs/contributing/ai-integration-tests.md +57 -0
- package/docs/contributing/contributing.md +90 -0
- package/docs/contributing/demo-videos.md +36 -0
- package/docs/contributing/npm-package.md +138 -0
- package/docs/contributing/observability.md +227 -0
- package/docs/contributing/regression-tests.md +103 -0
- package/docs/contributing/testing.md +95 -0
- package/docs/doc-collection/basics.md +128 -0
- package/docs/doc-collection/crawling.md +67 -0
- package/docs/doc-collection/interactive-mode.md +99 -0
- package/docs/index.json +87 -0
- package/docs/reference/commands.md +997 -0
- package/docs/reference/configuration.md +569 -0
- package/docs/reference/scripting.md +303 -0
- package/docs/reference/websocket.md +50 -0
- package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
- package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
- package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
- package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
- package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
- package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
- package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
- package/docs/superpowers/specs/2026-08-18-prima-false-verdicts.md +159 -0
- package/docs/web-testing/agents.md +158 -0
- package/docs/web-testing/automated-tests.md +134 -0
- package/docs/web-testing/basics.md +91 -0
- package/docs/web-testing/customization.md +131 -0
- package/docs/web-testing/hooks.md +238 -0
- package/docs/web-testing/page-interaction.md +84 -0
- package/docs/web-testing/planner.md +122 -0
- package/docs/web-testing/rerun.md +164 -0
- package/docs/web-testing/researcher.md +380 -0
- package/docs/workflow/agentic-usage.md +233 -0
- package/docs/workflow/application-spec.md +73 -0
- package/docs/workflow/ci.md +202 -0
- package/docs/workflow/knowledge.md +310 -0
- package/docs/workflow/planning-styles.md +67 -0
- package/docs/workflow/reporting.md +133 -0
- package/docs/workflow/test-plans.md +90 -0
- package/package.json +3 -2
- package/rules/navigator/verification-actions.md +20 -0
- package/src/action-result.ts +11 -0
- package/src/action.ts +43 -3
- package/src/ai/captain/web-mode.ts +6 -3
- package/src/ai/captain.ts +3 -0
- package/src/ai/navigator.ts +255 -186
- package/src/ai/pilot.ts +104 -2
- package/src/ai/provider.ts +14 -24
- package/src/ai/rerunner.ts +2 -1
- package/src/ai/researcher/cache.ts +12 -2
- package/src/ai/researcher.ts +3 -2
- package/src/ai/rules.ts +17 -10
- package/src/ai/session-analyst.ts +2 -0
- package/src/ai/task-agent.ts +3 -1
- package/src/ai/tester.ts +52 -45
- package/src/ai/tools.ts +136 -37
- package/src/commands/config-command.ts +146 -0
- package/src/commands/index.ts +2 -0
- package/src/config.ts +60 -5
- package/src/execution-controller.ts +8 -0
- package/src/explorbot.ts +7 -3
- package/src/explorer.ts +2 -2
- package/src/playwright-recorder.ts +23 -0
- package/src/remote.ts +244 -0
- package/src/reporter.ts +7 -1
- package/src/state-manager.ts +6 -2
- package/src/stats.ts +1 -0
- package/src/test-plan.ts +29 -0
- package/src/utils/aria.ts +65 -45
- package/src/utils/html.ts +1 -0
- package/src/utils/logger.ts +33 -2
- package/src/utils/page-readiness.ts +24 -1
- package/src/utils/url-matcher.ts +3 -0
- package/src/utils/web-element.ts +9 -0
- package/src/utils/web-sandbox.ts +3 -4
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { ConfigParser, EXPLORBOT_ENV_VARS, 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
|
+
export class ConfigCommand extends BaseCommand {
|
|
11
|
+
name = 'config';
|
|
12
|
+
description = 'Show models, config file and paths used by this run';
|
|
13
|
+
async execute() {
|
|
14
|
+
const parser = ConfigParser.getInstance();
|
|
15
|
+
tag('info').log(ConfigCommand.render(this.explorBot.getConfig(), { configPath: parser.getConfigPath(), root: parser.getProjectRoot() }));
|
|
16
|
+
}
|
|
17
|
+
static async summary(options = {}) {
|
|
18
|
+
const parser = ConfigParser.getInstance();
|
|
19
|
+
const [site] = listSites();
|
|
20
|
+
const load = (baseUrl) => parser.loadConfig({ config: options.config, path: options.path, baseUrl });
|
|
21
|
+
const config = await load(options.url).catch((error) => {
|
|
22
|
+
if (!site)
|
|
23
|
+
throw error;
|
|
24
|
+
return load(site.url);
|
|
25
|
+
});
|
|
26
|
+
return ConfigCommand.render(config, { configPath: parser.getConfigPath(), root: parser.getProjectRoot(), json: options.json });
|
|
27
|
+
}
|
|
28
|
+
static data(config, options = {}) {
|
|
29
|
+
let configPath = '';
|
|
30
|
+
if (options.configPath && existsSync(options.configPath))
|
|
31
|
+
configPath = options.configPath;
|
|
32
|
+
const dirs = {};
|
|
33
|
+
if (options.root) {
|
|
34
|
+
for (const [name, dir] of Object.entries({ output: 'output', ...config.dirs })) {
|
|
35
|
+
dirs[name] = path.join(options.root, dir);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const env = {};
|
|
39
|
+
for (const variable of EXPLORBOT_ENV_VARS) {
|
|
40
|
+
const value = process.env[variable.name];
|
|
41
|
+
if (value)
|
|
42
|
+
env[variable.name] = value;
|
|
43
|
+
}
|
|
44
|
+
const models = {};
|
|
45
|
+
const providers = {};
|
|
46
|
+
for (const [role, model] of Object.entries(configuredModels(config.ai))) {
|
|
47
|
+
models[role] = model.name;
|
|
48
|
+
if (model.provider)
|
|
49
|
+
providers[role] = model.provider;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
config: configPath,
|
|
53
|
+
url: config.playwright?.url || config.web?.url || config.api?.baseEndpoint || '',
|
|
54
|
+
browser: config.playwright?.browser || '',
|
|
55
|
+
headless: !config.playwright?.show,
|
|
56
|
+
dirs,
|
|
57
|
+
models,
|
|
58
|
+
providers,
|
|
59
|
+
integrations: {
|
|
60
|
+
langfuse: !!config.ai?.langfuse?.enabled,
|
|
61
|
+
testomatio: Reporter.resolveEnabled(config.reporter),
|
|
62
|
+
},
|
|
63
|
+
env,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
static render(config, options = {}) {
|
|
67
|
+
const data = ConfigCommand.data(config, options);
|
|
68
|
+
if (options.json)
|
|
69
|
+
return JSON.stringify(data, null, 2);
|
|
70
|
+
const lines = [];
|
|
71
|
+
const section = (title, entries) => {
|
|
72
|
+
if (!entries.length)
|
|
73
|
+
return;
|
|
74
|
+
const width = Math.max(...entries.map(([label]) => label.length));
|
|
75
|
+
lines.push(chalk.bold(title));
|
|
76
|
+
for (const [label, value] of entries)
|
|
77
|
+
lines.push(` ${chalk.dim(label.padEnd(width))} ${value}`);
|
|
78
|
+
lines.push('');
|
|
79
|
+
};
|
|
80
|
+
const general = [['config', data.config || 'EXPLORBOT_* environment variables']];
|
|
81
|
+
if (data.url)
|
|
82
|
+
general.push(['url', data.url]);
|
|
83
|
+
if (data.browser) {
|
|
84
|
+
let window = 'visible';
|
|
85
|
+
if (data.headless)
|
|
86
|
+
window = 'headless';
|
|
87
|
+
general.push(['browser', `${data.browser}, ${window}`]);
|
|
88
|
+
}
|
|
89
|
+
for (const [name, dir] of Object.entries(data.dirs))
|
|
90
|
+
general.push([name, dir]);
|
|
91
|
+
section('Config', general);
|
|
92
|
+
const providerWidth = Math.max(0, ...Object.values(data.providers).map((provider) => provider.length));
|
|
93
|
+
const models = Object.entries(data.models).map(([role, model]) => {
|
|
94
|
+
if (!providerWidth)
|
|
95
|
+
return [role, model];
|
|
96
|
+
return [role, `${chalk.dim((data.providers[role] || '').padEnd(providerWidth))} ${model}`];
|
|
97
|
+
});
|
|
98
|
+
if (!models.length)
|
|
99
|
+
models.push(['model', chalk.red(`not configured — run ${getCliName()} init`)]);
|
|
100
|
+
section('Models', models);
|
|
101
|
+
const integrations = [];
|
|
102
|
+
if (data.integrations.langfuse)
|
|
103
|
+
integrations.push(['langfuse', 'traces sent']);
|
|
104
|
+
if (data.integrations.testomatio)
|
|
105
|
+
integrations.push(['testomatio', 'runs reported']);
|
|
106
|
+
section('Integrations', integrations);
|
|
107
|
+
const env = Object.entries(data.env).map(([name, value]) => {
|
|
108
|
+
let shown = value;
|
|
109
|
+
if (shown.length > 60)
|
|
110
|
+
shown = `${shown.slice(0, 57)}...`;
|
|
111
|
+
return [name, shown];
|
|
112
|
+
});
|
|
113
|
+
section('Environment', env);
|
|
114
|
+
lines.push(chalk.dim(`Every EXPLORBOT_* variable: ${getCliName()} --help`));
|
|
115
|
+
return lines.join('\n');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AddRuleCommand } from './add-rule-command.js';
|
|
2
2
|
import { CleanCommand } from './clean-command.js';
|
|
3
3
|
import { CompactCommand } from './compact-command.js';
|
|
4
|
+
import { ConfigCommand } from './config-command.js';
|
|
4
5
|
import { ContextAriaCommand } from './context-aria-command.js';
|
|
5
6
|
import { ContextCommand } from './context-command.js';
|
|
6
7
|
import { ContextDataCommand } from './context-data-command.js';
|
|
@@ -64,6 +65,7 @@ const commandClasses = [
|
|
|
64
65
|
RunsCommand,
|
|
65
66
|
RerunCommand,
|
|
66
67
|
StatusCommand,
|
|
68
|
+
ConfigCommand,
|
|
67
69
|
DebugCommand,
|
|
68
70
|
ExitCommand,
|
|
69
71
|
];
|
package/dist/src/config.d.ts
CHANGED
|
@@ -172,6 +172,7 @@ interface HtmlConfig {
|
|
|
172
172
|
interface ActionConfig {
|
|
173
173
|
delay?: number;
|
|
174
174
|
retries?: number;
|
|
175
|
+
timeout?: number;
|
|
175
176
|
}
|
|
176
177
|
interface ReporterConfig {
|
|
177
178
|
enabled?: boolean;
|
|
@@ -271,12 +272,19 @@ export declare class ConfigMissingError extends Error {
|
|
|
271
272
|
}
|
|
272
273
|
export declare function envConfigRequested(): boolean;
|
|
273
274
|
export declare function missingConfigMessage(configFile?: string): string;
|
|
275
|
+
export declare function modelName(model: unknown): string;
|
|
276
|
+
export declare function modelProvider(model: unknown): string;
|
|
277
|
+
export declare function configuredModels(ai?: AIConfig): Record<string, ConfiguredModel>;
|
|
274
278
|
export declare function resolveConfigModels(ai?: AIConfig): Promise<void>;
|
|
275
279
|
export declare function resolveOutputRoot(baseUrl?: string): string;
|
|
276
280
|
export declare function resolveStateRoot(baseUrl: string, ephemeral?: boolean): string;
|
|
277
281
|
export declare function materializeKnowledge(outputRoot: string): void;
|
|
278
282
|
export declare function createModel(provider: string, modelId: string): Promise<any>;
|
|
279
283
|
type ModelRole = 'model' | 'visionModel' | 'agenticModel';
|
|
284
|
+
interface ConfiguredModel {
|
|
285
|
+
name: string;
|
|
286
|
+
provider: string;
|
|
287
|
+
}
|
|
280
288
|
interface ProviderInfo {
|
|
281
289
|
envKey: string;
|
|
282
290
|
load: () => Promise<(modelId: string) => any>;
|
|
@@ -286,4 +294,4 @@ interface EnvVar {
|
|
|
286
294
|
description: string;
|
|
287
295
|
required?: boolean;
|
|
288
296
|
}
|
|
289
|
-
export type { ModelRole, EnvVar, ProviderInfo };
|
|
297
|
+
export type { ModelRole, EnvVar, ProviderInfo, ConfiguredModel };
|
package/dist/src/config.js
CHANGED
|
@@ -9,12 +9,13 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
9
9
|
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import { tmpdir } from 'node:os';
|
|
11
11
|
import path, { basename, dirname, join, resolve } from 'node:path';
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
12
13
|
import { parseEnv } from 'node:util';
|
|
13
14
|
import dedent from 'dedent';
|
|
14
15
|
import matter from 'gray-matter';
|
|
15
16
|
import { findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from './global-config.js';
|
|
16
17
|
import { getCliName } from './utils/cli-name.js';
|
|
17
|
-
import { log } from './utils/logger.js';
|
|
18
|
+
import { log, tag } from './utils/logger.js';
|
|
18
19
|
export const PROVIDERS = {
|
|
19
20
|
openai: { envKey: 'OPENAI_API_KEY', load: async () => (await import('@ai-sdk/openai')).createOpenAI() },
|
|
20
21
|
anthropic: { envKey: 'ANTHROPIC_API_KEY', load: async () => (await import('@ai-sdk/anthropic')).createAnthropic() },
|
|
@@ -178,8 +179,15 @@ export class ConfigParser {
|
|
|
178
179
|
return this.site;
|
|
179
180
|
}
|
|
180
181
|
resolveTargetPath(target) {
|
|
181
|
-
if (!this.site)
|
|
182
|
+
if (!this.site) {
|
|
183
|
+
const configured = this.config?.playwright?.url || this.config?.web?.url;
|
|
184
|
+
const targetOrigin = target ? URL.parse(target)?.origin : null;
|
|
185
|
+
const baseOrigin = configured ? URL.parse(configured)?.origin : null;
|
|
186
|
+
if (targetOrigin && baseOrigin && targetOrigin !== baseOrigin) {
|
|
187
|
+
tag('warning').log(`Exploring ${targetOrigin} but base URL is ${baseOrigin}. Relative navigation resolves against the base URL — set web.url to ${targetOrigin} to avoid it.`);
|
|
188
|
+
}
|
|
182
189
|
return target || '/';
|
|
190
|
+
}
|
|
183
191
|
if (!target)
|
|
184
192
|
return this.siteStartPath;
|
|
185
193
|
const resolved = resolveSiteTarget(target, this.site.url);
|
|
@@ -223,6 +231,7 @@ export class ConfigParser {
|
|
|
223
231
|
model: { modelId: 'test-model', provider: 'test' },
|
|
224
232
|
config: {},
|
|
225
233
|
vision: false,
|
|
234
|
+
langfuse: { enabled: false },
|
|
226
235
|
},
|
|
227
236
|
dirs: {
|
|
228
237
|
knowledge: join(testBaseDir, 'knowledge'),
|
|
@@ -321,9 +330,10 @@ export class ConfigParser {
|
|
|
321
330
|
}
|
|
322
331
|
async loadConfigModule(configPath) {
|
|
323
332
|
const ext = configPath.split('.').pop();
|
|
333
|
+
const moduleUrl = pathToFileURL(resolve(configPath)).href;
|
|
324
334
|
if (ext === 'ts') {
|
|
325
335
|
try {
|
|
326
|
-
const module = await import(__rewriteRelativeImportExtension(
|
|
336
|
+
const module = await import(__rewriteRelativeImportExtension(moduleUrl));
|
|
327
337
|
return module;
|
|
328
338
|
}
|
|
329
339
|
catch (error) {
|
|
@@ -332,7 +342,7 @@ export class ConfigParser {
|
|
|
332
342
|
}
|
|
333
343
|
}
|
|
334
344
|
else if (ext === 'js' || ext === 'mjs') {
|
|
335
|
-
const module = await import(__rewriteRelativeImportExtension(
|
|
345
|
+
const module = await import(__rewriteRelativeImportExtension(moduleUrl));
|
|
336
346
|
return module;
|
|
337
347
|
}
|
|
338
348
|
else {
|
|
@@ -349,6 +359,17 @@ export class ConfigParser {
|
|
|
349
359
|
config.playwright = config.playwright || { browser: 'chromium', url: '' };
|
|
350
360
|
config.playwright.url = options.baseUrl;
|
|
351
361
|
}
|
|
362
|
+
if (config.ai) {
|
|
363
|
+
const langfuse = config.ai.langfuse;
|
|
364
|
+
const publicKey = langfuse?.publicKey || process.env.LANGFUSE_PUBLIC_KEY;
|
|
365
|
+
const secretKey = langfuse?.secretKey || process.env.LANGFUSE_SECRET_KEY;
|
|
366
|
+
config.ai.langfuse = {
|
|
367
|
+
enabled: langfuse?.enabled ?? Boolean(publicKey && secretKey),
|
|
368
|
+
publicKey,
|
|
369
|
+
secretKey,
|
|
370
|
+
baseUrl: langfuse?.baseUrl || process.env.LANGFUSE_BASE_URL || process.env.LANGFUSE_HOST,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
352
373
|
return config;
|
|
353
374
|
}
|
|
354
375
|
validateConfig(config) {
|
|
@@ -445,6 +466,34 @@ export function missingConfigMessage(configFile = 'explorbot.config.js') {
|
|
|
445
466
|
Providers: ${Object.keys(PROVIDERS).join(', ')}
|
|
446
467
|
`;
|
|
447
468
|
}
|
|
469
|
+
export function modelName(model) {
|
|
470
|
+
if (typeof model === 'string')
|
|
471
|
+
return model;
|
|
472
|
+
return model?.modelId || model?.model || 'unknown';
|
|
473
|
+
}
|
|
474
|
+
export function modelProvider(model) {
|
|
475
|
+
const provider = model?.provider;
|
|
476
|
+
if (typeof provider === 'string')
|
|
477
|
+
return provider.split('.')[0];
|
|
478
|
+
if (typeof model === 'string')
|
|
479
|
+
return model.split('/')[0];
|
|
480
|
+
return '';
|
|
481
|
+
}
|
|
482
|
+
export function configuredModels(ai) {
|
|
483
|
+
if (!ai?.model)
|
|
484
|
+
return {};
|
|
485
|
+
const describe = (model) => ({ name: modelName(model), provider: modelProvider(model) });
|
|
486
|
+
const models = { model: describe(ai.model) };
|
|
487
|
+
if (ai.agenticModel)
|
|
488
|
+
models.agenticModel = describe(ai.agenticModel);
|
|
489
|
+
if (ai.visionModel)
|
|
490
|
+
models.visionModel = describe(ai.visionModel);
|
|
491
|
+
for (const [agent, agentConfig] of Object.entries(ai.agents || {})) {
|
|
492
|
+
if (agentConfig?.model)
|
|
493
|
+
models[agent] = describe(agentConfig.model);
|
|
494
|
+
}
|
|
495
|
+
return models;
|
|
496
|
+
}
|
|
448
497
|
export async function resolveConfigModels(ai) {
|
|
449
498
|
if (!ai)
|
|
450
499
|
return;
|
|
@@ -10,6 +10,8 @@ export declare class ExecutionController extends EventEmitter {
|
|
|
10
10
|
constructor();
|
|
11
11
|
static getInstance(): ExecutionController;
|
|
12
12
|
setInputCallback(callback: InputCallback): void;
|
|
13
|
+
hasInputCallback(): boolean;
|
|
14
|
+
clearInputCallback(): void;
|
|
13
15
|
startExecution(): void;
|
|
14
16
|
getAbortSignal(): AbortSignal | undefined;
|
|
15
17
|
interrupt(): void;
|
|
@@ -20,6 +20,12 @@ export class ExecutionController extends EventEmitter {
|
|
|
20
20
|
setInputCallback(callback) {
|
|
21
21
|
this.inputCallback = callback;
|
|
22
22
|
}
|
|
23
|
+
hasInputCallback() {
|
|
24
|
+
return !!this.inputCallback;
|
|
25
|
+
}
|
|
26
|
+
clearInputCallback() {
|
|
27
|
+
this.inputCallback = null;
|
|
28
|
+
}
|
|
23
29
|
startExecution() {
|
|
24
30
|
this.interrupted = false;
|
|
25
31
|
this.abortController = new AbortController();
|
package/dist/src/explorbot.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { Researcher } from './ai/researcher.js';
|
|
|
15
15
|
import { SessionAnalyst } from './ai/session-analyst.js';
|
|
16
16
|
import { Tester } from './ai/tester.js';
|
|
17
17
|
import { RequestStore } from './api/request-store.js';
|
|
18
|
-
import type { ExplorbotConfig } from './config.js';
|
|
18
|
+
import type { ExplorbotConfig, ReporterConfig } from './config.js';
|
|
19
19
|
import { ConfigParser } from './config.js';
|
|
20
20
|
import { ExperienceTracker } from './experience-tracker.js';
|
|
21
21
|
import Explorer from './explorer.js';
|
|
@@ -37,6 +37,7 @@ export interface ExplorBotOptions {
|
|
|
37
37
|
session?: string | boolean;
|
|
38
38
|
instance?: string;
|
|
39
39
|
optionalAi?: boolean;
|
|
40
|
+
reporter?: ReporterConfig;
|
|
40
41
|
attachedBrowser?: Browser;
|
|
41
42
|
applicationSpec?: string;
|
|
42
43
|
}
|
package/dist/src/explorbot.js
CHANGED
|
@@ -101,6 +101,8 @@ export class ExplorBot {
|
|
|
101
101
|
this.config = await this.configParser.loadConfig(this.options);
|
|
102
102
|
if (this.options.session === true)
|
|
103
103
|
this.options.session = path.join(this.configParser.getOutputDir(), 'session.json');
|
|
104
|
+
if (typeof this.options.session === 'string')
|
|
105
|
+
this.options.session = path.resolve(this.options.path || '.', this.options.session);
|
|
104
106
|
if (this.options.optionalAi)
|
|
105
107
|
return this.bootstrapOptionalProvider();
|
|
106
108
|
this.provider = new AIProvider(this.config.ai);
|
|
@@ -138,7 +140,7 @@ export class ExplorBot {
|
|
|
138
140
|
return (this._stateManager ||= new StateManager(this.experienceTracker(), this.knowledgeTracker()));
|
|
139
141
|
}
|
|
140
142
|
reporter() {
|
|
141
|
-
return (this._reporter ||= new Reporter(this.config.reporter, this.stateManager()));
|
|
143
|
+
return (this._reporter ||= new Reporter(this.options.reporter || this.config.reporter, this.stateManager()));
|
|
142
144
|
}
|
|
143
145
|
requestStore() {
|
|
144
146
|
return (this._requestStore ||= new RequestStore(this.configParser.getOutputDir()));
|
|
@@ -416,9 +418,12 @@ export class ExplorBot {
|
|
|
416
418
|
}
|
|
417
419
|
setCurrentPlan(plan) {
|
|
418
420
|
this.currentPlan = plan;
|
|
419
|
-
if (
|
|
421
|
+
if (!plan)
|
|
422
|
+
return;
|
|
423
|
+
if (!this.sessionPlans.includes(plan)) {
|
|
420
424
|
this.sessionPlans.push(plan);
|
|
421
425
|
}
|
|
426
|
+
plan.notifyChange();
|
|
422
427
|
}
|
|
423
428
|
getSessionTests() {
|
|
424
429
|
return this.sessionPlans.flatMap((p) => p.tests.filter((t) => t.startTime != null));
|
package/dist/src/explorer.js
CHANGED
|
@@ -10,7 +10,6 @@ import { ActionResult } from "./action-result.js";
|
|
|
10
10
|
import Action from './action.js';
|
|
11
11
|
import { XhrCapture } from "./api/xhr-capture.js";
|
|
12
12
|
import { ConfigParser } from './config.js';
|
|
13
|
-
import { TestResult } from "./test-plan.js";
|
|
14
13
|
import { BrowserRecoveryError, browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from "./utils/browser-errors.js";
|
|
15
14
|
import { createDebug, log, tag } from './utils/logger.js';
|
|
16
15
|
import { sleep, waitForPageReadiness } from "./utils/page-readiness.js";
|
|
@@ -624,12 +623,12 @@ class Explorer {
|
|
|
624
623
|
if (this.observedTestPages.has(page))
|
|
625
624
|
return;
|
|
626
625
|
this.testPageErrorHandler ||= (err) => {
|
|
627
|
-
this._activeTest?.
|
|
626
|
+
this._activeTest?.addObservation(`Console error: ${err.message}`);
|
|
628
627
|
};
|
|
629
628
|
this.testConsoleHandler ||= (msg) => {
|
|
630
629
|
if (msg.type() !== 'error')
|
|
631
630
|
return;
|
|
632
|
-
this._activeTest?.
|
|
631
|
+
this._activeTest?.addObservation(`Console error: ${msg.text()}`);
|
|
633
632
|
};
|
|
634
633
|
this.testDialogHandler ||= (dialog) => {
|
|
635
634
|
const dialogType = dialog.type();
|
|
@@ -292,8 +292,38 @@ function formatSelectOption(options) {
|
|
|
292
292
|
return quote(values[0]);
|
|
293
293
|
return `[${values.map((v) => quote(v)).join(', ')}]`;
|
|
294
294
|
}
|
|
295
|
+
function assertionLocator(target) {
|
|
296
|
+
if (typeof target === 'string')
|
|
297
|
+
return `page.locator(${JSON.stringify(target)})`;
|
|
298
|
+
if (!target || typeof target !== 'object')
|
|
299
|
+
return null;
|
|
300
|
+
const role = target.role || target.aria;
|
|
301
|
+
const name = target.text ?? target.name ?? target.label;
|
|
302
|
+
if (role && name)
|
|
303
|
+
return `page.getByRole(${JSON.stringify(String(role))}, { name: ${JSON.stringify(String(name))} })`;
|
|
304
|
+
if (role)
|
|
305
|
+
return `page.getByRole(${JSON.stringify(String(role))})`;
|
|
306
|
+
if (name)
|
|
307
|
+
return `page.getByText(${JSON.stringify(String(name))})`;
|
|
308
|
+
if (target.css)
|
|
309
|
+
return `page.locator(${JSON.stringify(String(target.css))})`;
|
|
310
|
+
if (target.xpath)
|
|
311
|
+
return `page.locator(${JSON.stringify(`xpath=${target.xpath}`)})`;
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
295
314
|
export function renderAssertion(assertion) {
|
|
296
315
|
const args = assertion.args;
|
|
316
|
+
const target = assertionLocator(args[0]);
|
|
317
|
+
if (target) {
|
|
318
|
+
if (assertion.name === 'seeElement')
|
|
319
|
+
return `await expect(${target}).toBeVisible();`;
|
|
320
|
+
if (assertion.name === 'dontSeeElement')
|
|
321
|
+
return `await expect(${target}).toBeHidden();`;
|
|
322
|
+
if (assertion.name === 'seeInField' && args[1] !== undefined)
|
|
323
|
+
return `await expect(${target}).toHaveValue(${JSON.stringify(String(args[1]))});`;
|
|
324
|
+
if (assertion.name === 'dontSeeInField' && args[1] !== undefined)
|
|
325
|
+
return `await expect(${target}).not.toHaveValue(${JSON.stringify(String(args[1]))});`;
|
|
326
|
+
}
|
|
297
327
|
if (assertion.name === 'see' && typeof args[0] === 'string') {
|
|
298
328
|
return `await expect(page).toContainText(${JSON.stringify(args[0])});`;
|
|
299
329
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { type ActivityEntry } from './activity.js';
|
|
3
|
+
import { type LogDestination, type TaggedLogEntry } from './utils/logger.js';
|
|
4
|
+
/**
|
|
5
|
+
* Streams a run to a remote UI over one WebSocket, dialling out so a child
|
|
6
|
+
* process and a CI bot are the same case.
|
|
7
|
+
*
|
|
8
|
+
* It **is** a LogDestination — that is the whole integration on the logger's
|
|
9
|
+
* side, messages and `data` alike — and it answers asks by installing itself
|
|
10
|
+
* as the execution controller's input callback. Nothing else in explorbot
|
|
11
|
+
* knows it exists.
|
|
12
|
+
*/
|
|
13
|
+
export declare class Remote implements LogDestination {
|
|
14
|
+
url: string | null;
|
|
15
|
+
socket: WebSocket | null;
|
|
16
|
+
queue: Frame[];
|
|
17
|
+
reconnectDelay: number;
|
|
18
|
+
reconnectTimer: ReturnType<typeof setTimeout> | null;
|
|
19
|
+
asks: Map<string, (value: string | null) => void>;
|
|
20
|
+
askCounter: number;
|
|
21
|
+
lastActivity: string | null;
|
|
22
|
+
registerOption(program: Command): void;
|
|
23
|
+
attach(url: string, command: string): void;
|
|
24
|
+
isAttached(): boolean;
|
|
25
|
+
send(type: string, data?: Record<string, unknown>): void;
|
|
26
|
+
ask(prompt: string): Promise<string | null>;
|
|
27
|
+
close(exitCode: number): Promise<void>;
|
|
28
|
+
isEnabled(): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The entry as the logger already built it, so the `LogType` reaches the UI
|
|
31
|
+
* as the frame's level and each kind is styled there instead of being scraped
|
|
32
|
+
* back out of flattened text.
|
|
33
|
+
*/
|
|
34
|
+
write(entry: TaggedLogEntry): void;
|
|
35
|
+
connect(): void;
|
|
36
|
+
reconnect(): void;
|
|
37
|
+
flush(): Promise<void>;
|
|
38
|
+
drain(): void;
|
|
39
|
+
push(frame: Frame): void;
|
|
40
|
+
receive(raw: string): void;
|
|
41
|
+
/** A new listener is primed with the current activity, which at attach time is
|
|
42
|
+
* nothing — so skip repeats, the priming null included. */
|
|
43
|
+
reportActivity(activity: ActivityEntry | null): void;
|
|
44
|
+
errorOf(args: any[] | undefined): string | undefined;
|
|
45
|
+
commandPath(command: Command): string;
|
|
46
|
+
}
|
|
47
|
+
export declare const remote: Remote;
|
|
48
|
+
/** Whatever the run wants to say. Not a schema — the UI renders what it knows
|
|
49
|
+
* and ignores the rest, so either side can start sending more at any time. */
|
|
50
|
+
type Frame = {
|
|
51
|
+
type: string;
|
|
52
|
+
ts: number;
|
|
53
|
+
[key: string]: unknown;
|
|
54
|
+
};
|
|
55
|
+
export {};
|