explorbot 0.2.4 → 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/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/src/cli.ts +24 -12
- package/boat/prima/src/envelope.ts +35 -13
- package/boat/prima/src/prima.ts +61 -41
- 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 +19 -7
- package/dist/boat/prima/src/envelope.js +32 -8
- package/dist/boat/prima/src/prima.js +57 -39
- package/dist/package.json +1 -1
- package/dist/src/action.js +5 -1
- package/dist/src/ai/navigator.d.ts +27 -0
- package/dist/src/ai/navigator.js +227 -175
- package/dist/src/ai/pilot.d.ts +7 -4
- package/dist/src/ai/pilot.js +50 -8
- package/dist/src/ai/provider.d.ts +2 -2
- package/dist/src/ai/provider.js +12 -21
- 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 +2 -1
- package/dist/src/ai/session-analyst.js +2 -0
- package/dist/src/ai/tester.d.ts +5 -2
- package/dist/src/ai/tester.js +17 -13
- package/dist/src/ai/tools.js +4 -1
- 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 +8 -1
- package/dist/src/config.js +40 -0
- package/dist/src/explorbot.js +4 -1
- 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 +1 -0
- package/dist/src/test-plan.js +19 -0
- package/dist/src/utils/logger.d.ts +1 -1
- package/dist/src/utils/logger.js +8 -0
- 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/package.json +1 -1
- package/src/action.ts +5 -1
- package/src/ai/navigator.ts +241 -178
- package/src/ai/pilot.ts +63 -12
- package/src/ai/provider.ts +12 -20
- package/src/ai/researcher/cache.ts +12 -2
- package/src/ai/researcher.ts +2 -1
- package/src/ai/session-analyst.ts +2 -0
- package/src/ai/tester.ts +20 -12
- package/src/ai/tools.ts +4 -1
- package/src/commands/config-command.ts +146 -0
- package/src/commands/index.ts +2 -0
- package/src/config.ts +45 -1
- package/src/explorbot.ts +3 -1
- package/src/remote.ts +8 -2
- package/src/state-manager.ts +5 -2
- package/src/test-plan.ts +20 -0
- package/src/utils/logger.ts +9 -1
package/dist/src/ai/provider.js
CHANGED
|
@@ -3,6 +3,7 @@ import { LangfuseSpanProcessor } from '@langfuse/otel';
|
|
|
3
3
|
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
4
4
|
import { generateObject, generateText, isStepCount, registerTelemetry } from 'ai';
|
|
5
5
|
import { clearActivity, setActivity } from "../activity.js";
|
|
6
|
+
import { configuredModels, modelName as getModelName } from '../config.js';
|
|
6
7
|
import { executionController } from "../execution-controller.js";
|
|
7
8
|
import { Observability } from "../observability.js";
|
|
8
9
|
import { Stats } from "../stats.js";
|
|
@@ -79,9 +80,6 @@ export class Provider {
|
|
|
79
80
|
this.config = config;
|
|
80
81
|
this.initLangfuse();
|
|
81
82
|
}
|
|
82
|
-
getModelName(model) {
|
|
83
|
-
return model?.modelId || model?.model || 'unknown';
|
|
84
|
-
}
|
|
85
83
|
async validateConnection() {
|
|
86
84
|
try {
|
|
87
85
|
await generateText({
|
|
@@ -108,16 +106,13 @@ export class Provider {
|
|
|
108
106
|
}
|
|
109
107
|
return this.config.agenticModel || this.config.model;
|
|
110
108
|
}
|
|
109
|
+
getVisionModel() {
|
|
110
|
+
return this.config.visionModel;
|
|
111
|
+
}
|
|
111
112
|
getConfiguredModels() {
|
|
112
|
-
const models = {
|
|
113
|
-
|
|
114
|
-
models
|
|
115
|
-
if (this.config.visionModel)
|
|
116
|
-
models.visionModel = this.getModelName(this.config.visionModel);
|
|
117
|
-
for (const [agent, agentConfig] of Object.entries(this.config.agents || {})) {
|
|
118
|
-
if (agentConfig?.model)
|
|
119
|
-
models[agent] = this.getModelName(agentConfig.model);
|
|
120
|
-
}
|
|
113
|
+
const models = {};
|
|
114
|
+
for (const [role, model] of Object.entries(configuredModels(this.config)))
|
|
115
|
+
models[role] = model.name;
|
|
121
116
|
return models;
|
|
122
117
|
}
|
|
123
118
|
getSystemPromptForAgent(agentName, currentUrl) {
|
|
@@ -207,11 +202,7 @@ export class Provider {
|
|
|
207
202
|
return retry(reduced.messages, { ...options, _contextRetryLevel: reduced.nextLevel });
|
|
208
203
|
}
|
|
209
204
|
initLangfuse() {
|
|
210
|
-
const
|
|
211
|
-
const publicKey = langfuseConfig?.publicKey || process.env.LANGFUSE_PUBLIC_KEY;
|
|
212
|
-
const secretKey = langfuseConfig?.secretKey || process.env.LANGFUSE_SECRET_KEY;
|
|
213
|
-
const baseUrl = langfuseConfig?.baseUrl || process.env.LANGFUSE_BASE_URL || process.env.LANGFUSE_HOST;
|
|
214
|
-
const enabled = langfuseConfig?.enabled ?? Boolean(publicKey && secretKey);
|
|
205
|
+
const { enabled, publicKey, secretKey, baseUrl } = this.config.langfuse || {};
|
|
215
206
|
if (!enabled || !publicKey || !secretKey) {
|
|
216
207
|
return;
|
|
217
208
|
}
|
|
@@ -282,7 +273,7 @@ export class Provider {
|
|
|
282
273
|
return { conversation, response, toolExecutions };
|
|
283
274
|
}
|
|
284
275
|
async chat(messages, model, options = {}) {
|
|
285
|
-
const modelName =
|
|
276
|
+
const modelName = getModelName(model);
|
|
286
277
|
setActivity(`🤖 Asking ${modelName}`, 'ai');
|
|
287
278
|
promptLog(`Using model: ${modelName}`);
|
|
288
279
|
const config = this.buildGenerateConfig({ maxOutputTokens: 16384 }, { model, abortSignal: executionController.getAbortSignal() }, options);
|
|
@@ -324,7 +315,7 @@ export class Provider {
|
|
|
324
315
|
}
|
|
325
316
|
}
|
|
326
317
|
async generateWithTools(messages, model, tools, options = {}) {
|
|
327
|
-
const modelName =
|
|
318
|
+
const modelName = getModelName(model);
|
|
328
319
|
setActivity(`🤖 Asking ${modelName} with dynamic tools`, 'ai');
|
|
329
320
|
promptLog(`Using model: ${modelName}`);
|
|
330
321
|
const toolNames = Object.keys(tools || {});
|
|
@@ -379,7 +370,7 @@ export class Provider {
|
|
|
379
370
|
}
|
|
380
371
|
async generateObject(messages, schema, model, options = {}) {
|
|
381
372
|
const modelToUse = model || this.config.model;
|
|
382
|
-
const modelName =
|
|
373
|
+
const modelName = getModelName(modelToUse);
|
|
383
374
|
setActivity(`🤖 Asking ${modelName} for structured output`, 'ai');
|
|
384
375
|
promptLog(`Using model: ${modelName}`);
|
|
385
376
|
const config = this.buildGenerateConfig({ schema }, { model: modelToUse }, options);
|
|
@@ -557,7 +548,7 @@ export class Provider {
|
|
|
557
548
|
}, this.getRetryOptions());
|
|
558
549
|
clearActivity();
|
|
559
550
|
responseLog(response.text);
|
|
560
|
-
this.recordUsage('vision',
|
|
551
|
+
this.recordUsage('vision', getModelName(this.config.visionModel), response.usage);
|
|
561
552
|
return response;
|
|
562
553
|
}
|
|
563
554
|
catch (error) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export declare function researchPath(hash: string): string;
|
|
2
|
+
export declare function reportResearch(hash: string, text: string): void;
|
|
1
3
|
export declare function clearResearchCache(): void;
|
|
2
4
|
export declare function getCachedResearch(hash: string): string;
|
|
3
5
|
export declare function getPreviousResearch(hash: string): string;
|
|
@@ -4,6 +4,7 @@ import { Worker } from 'node:worker_threads';
|
|
|
4
4
|
import { outputPath } from "../../config.js";
|
|
5
5
|
import { TTLCache } from "../../utils/cache.js";
|
|
6
6
|
import { computeHtmlFingerprint } from "../../utils/html-diff.js";
|
|
7
|
+
import { tag } from "../../utils/logger.js";
|
|
7
8
|
import { debugLog } from "./mixin.js";
|
|
8
9
|
const CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
9
10
|
const FINGERPRINT_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
|
@@ -11,6 +12,12 @@ const FINGERPRINT_WORKER_TIMEOUT_MS = 10_000;
|
|
|
11
12
|
const SIMILARITY_THRESHOLD = 90;
|
|
12
13
|
const memoryCache = new TTLCache(CACHE_TTL_MS);
|
|
13
14
|
let fingerprintWorker = null;
|
|
15
|
+
export function researchPath(hash) {
|
|
16
|
+
return outputPath('research', `${hash}.md`);
|
|
17
|
+
}
|
|
18
|
+
export function reportResearch(hash, text) {
|
|
19
|
+
tag('data').log('research', { path: researchPath(hash), hash, content: text });
|
|
20
|
+
}
|
|
14
21
|
function getStatesDir() {
|
|
15
22
|
return outputPath('states');
|
|
16
23
|
}
|
|
@@ -30,7 +37,7 @@ export function getCachedResearch(hash) {
|
|
|
30
37
|
const cached = memoryCache.get(hash);
|
|
31
38
|
if (cached !== undefined)
|
|
32
39
|
return cached;
|
|
33
|
-
const researchFile =
|
|
40
|
+
const researchFile = researchPath(hash);
|
|
34
41
|
if (!existsSync(researchFile))
|
|
35
42
|
return '';
|
|
36
43
|
const stats = statSync(researchFile);
|
|
@@ -43,7 +50,7 @@ export function getCachedResearch(hash) {
|
|
|
43
50
|
export function getPreviousResearch(hash) {
|
|
44
51
|
if (!hash)
|
|
45
52
|
return '';
|
|
46
|
-
const researchFile =
|
|
53
|
+
const researchFile = researchPath(hash);
|
|
47
54
|
if (!existsSync(researchFile))
|
|
48
55
|
return '';
|
|
49
56
|
return readFileSync(researchFile, 'utf8');
|
|
@@ -55,6 +62,7 @@ export function saveResearch(hash, text, combinedHtml) {
|
|
|
55
62
|
mkdirSync(researchDir, { recursive: true });
|
|
56
63
|
writeFileSync(researchFile, text);
|
|
57
64
|
memoryCache.set(hash, text);
|
|
65
|
+
reportResearch(hash, text);
|
|
58
66
|
debugLog(`Research saved to ${researchFile}`);
|
|
59
67
|
if (combinedHtml) {
|
|
60
68
|
const statesDir = getStatesDir();
|
|
@@ -13,7 +13,7 @@ import { mdq } from "../utils/markdown-query.js";
|
|
|
13
13
|
import { RulesLoader } from "../utils/rules-loader.js";
|
|
14
14
|
import { annotatePageElements } from "../utils/web-annotate.js";
|
|
15
15
|
import { ContextLengthError } from './provider.js';
|
|
16
|
-
import { findSimilarResearch, getCachedResearch, saveResearch } from "./researcher/cache.js";
|
|
16
|
+
import { findSimilarResearch, getCachedResearch, reportResearch, saveResearch } from "./researcher/cache.js";
|
|
17
17
|
import { WithCoordinates } from "./researcher/coordinates.js";
|
|
18
18
|
import { WithDeepAnalysis } from "./researcher/deep-analysis.js";
|
|
19
19
|
import { detectFocusedSection, hasFocusedSection, markSectionAsFocused, pickDefaultFocusedSection } from "./researcher/focus.js";
|
|
@@ -76,6 +76,7 @@ export class Researcher extends ResearcherBase {
|
|
|
76
76
|
const cached = getCachedResearch(stateHash);
|
|
77
77
|
if (cached) {
|
|
78
78
|
debugLog('Previous research result found');
|
|
79
|
+
reportResearch(stateHash, cached);
|
|
79
80
|
return cached;
|
|
80
81
|
}
|
|
81
82
|
}
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import dedent from 'dedent';
|
|
4
4
|
import { outputPath } from "../config.js";
|
|
5
5
|
import { Stats } from "../stats.js";
|
|
6
|
+
import { tag } from "../utils/logger.js";
|
|
6
7
|
export class SessionAnalyst {
|
|
7
8
|
emoji = '🧐';
|
|
8
9
|
provider;
|
|
@@ -103,6 +104,7 @@ export class SessionAnalyst {
|
|
|
103
104
|
if (!existsSync(dir))
|
|
104
105
|
mkdirSync(dir, { recursive: true });
|
|
105
106
|
writeFileSync(filePath, markdown);
|
|
107
|
+
tag('data').log('report', { path: filePath, content: markdown });
|
|
106
108
|
return filePath;
|
|
107
109
|
}
|
|
108
110
|
serializeTest(test, ref) {
|
package/dist/src/ai/tester.d.ts
CHANGED
|
@@ -39,10 +39,10 @@ export declare class Tester extends TaskAgent implements Agent {
|
|
|
39
39
|
getCurrentState(): ActionResult;
|
|
40
40
|
get progressCheckInterval(): number;
|
|
41
41
|
getConversation(): Conversation | null;
|
|
42
|
-
test(task: Test): Promise<{
|
|
42
|
+
test(task: Test, opts?: TestOptions): Promise<{
|
|
43
43
|
success: boolean;
|
|
44
44
|
}>;
|
|
45
|
-
runTestSession(task: Test, initialState: ActionResult, conversation: Conversation, handlers: TestSessionHandlers): Promise<{
|
|
45
|
+
runTestSession(task: Test, initialState: ActionResult, conversation: Conversation, handlers: TestSessionHandlers, opts: TestOptions): Promise<{
|
|
46
46
|
success: boolean;
|
|
47
47
|
}>;
|
|
48
48
|
shouldAnalyzeProgress(iteration: number, currentState: ActionResult): boolean;
|
|
@@ -93,4 +93,7 @@ export declare class Tester extends TaskAgent implements Agent {
|
|
|
93
93
|
interface TestSessionHandlers {
|
|
94
94
|
offFailedRequest?: () => void;
|
|
95
95
|
}
|
|
96
|
+
export interface TestOptions {
|
|
97
|
+
startOnCurrentPage?: boolean;
|
|
98
|
+
}
|
|
96
99
|
export {};
|
package/dist/src/ai/tester.js
CHANGED
|
@@ -74,7 +74,7 @@ export class Tester extends TaskAgent {
|
|
|
74
74
|
getConversation() {
|
|
75
75
|
return this.currentConversation;
|
|
76
76
|
}
|
|
77
|
-
async test(task) {
|
|
77
|
+
async test(task, opts = {}) {
|
|
78
78
|
Stats.tests++;
|
|
79
79
|
const state = this.stateManager.getCurrentState();
|
|
80
80
|
if (!state)
|
|
@@ -120,9 +120,9 @@ export class Tester extends TaskAgent {
|
|
|
120
120
|
startUrl: task.startUrl,
|
|
121
121
|
expected: task.expected,
|
|
122
122
|
},
|
|
123
|
-
}, async () => this.runTestSession(task, initialState, conversation, { offFailedRequest }));
|
|
123
|
+
}, async () => this.runTestSession(task, initialState, conversation, { offFailedRequest }, opts));
|
|
124
124
|
}
|
|
125
|
-
async runTestSession(task, initialState, conversation, handlers) {
|
|
125
|
+
async runTestSession(task, initialState, conversation, handlers, opts) {
|
|
126
126
|
const { offFailedRequest } = handlers;
|
|
127
127
|
if (this.pilot) {
|
|
128
128
|
try {
|
|
@@ -151,16 +151,20 @@ export class Tester extends TaskAgent {
|
|
|
151
151
|
await this.cleanupStartedTest(task);
|
|
152
152
|
return { success: task.isSuccessful };
|
|
153
153
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
await this.
|
|
163
|
-
|
|
154
|
+
if (opts.startOnCurrentPage)
|
|
155
|
+
debugLog(`Starting on the page already open at ${task.startUrl}`);
|
|
156
|
+
if (!opts.startOnCurrentPage) {
|
|
157
|
+
debugLog(`Navigating to ${task.startUrl}`);
|
|
158
|
+
try {
|
|
159
|
+
await this.explorer.visit(task.startUrl);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
const result = await this.handleLoopError(task, error);
|
|
163
|
+
if (result === 'stop') {
|
|
164
|
+
offFailedRequest?.();
|
|
165
|
+
await this.cleanupStartedTest(task);
|
|
166
|
+
return { success: task.isSuccessful };
|
|
167
|
+
}
|
|
164
168
|
}
|
|
165
169
|
}
|
|
166
170
|
const startState = this.stateManager.getCurrentState();
|
package/dist/src/ai/tools.js
CHANGED
|
@@ -698,7 +698,10 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
698
698
|
message: `Successfully executed: ${instruction}`,
|
|
699
699
|
});
|
|
700
700
|
}
|
|
701
|
-
|
|
701
|
+
let reason = '';
|
|
702
|
+
if (navigator.lastFailureReason)
|
|
703
|
+
reason = `: ${navigator.lastFailureReason}`;
|
|
704
|
+
return failedToolResult('interact', `Failed to execute: ${instruction}${reason}`, {
|
|
702
705
|
...toolResult,
|
|
703
706
|
suggestion: 'The action could not be completed. Try a different instruction or use more specific element descriptions.',
|
|
704
707
|
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type AIConfig, type ReporterConfig } from '../config.js';
|
|
2
|
+
import { BaseCommand } from './base-command.js';
|
|
3
|
+
export declare class ConfigCommand extends BaseCommand {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
execute(): Promise<void>;
|
|
7
|
+
static summary(options?: {
|
|
8
|
+
config?: string;
|
|
9
|
+
path?: string;
|
|
10
|
+
url?: string;
|
|
11
|
+
json?: boolean;
|
|
12
|
+
}): Promise<string>;
|
|
13
|
+
static data(config: SummarizedConfig, options?: ConfigSummaryOptions): ConfigData;
|
|
14
|
+
static render(config: SummarizedConfig, options?: ConfigSummaryOptions): string;
|
|
15
|
+
}
|
|
16
|
+
interface ConfigSummaryOptions {
|
|
17
|
+
configPath?: string | null;
|
|
18
|
+
root?: string;
|
|
19
|
+
json?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface ConfigData {
|
|
22
|
+
config: string;
|
|
23
|
+
url: string;
|
|
24
|
+
browser: string;
|
|
25
|
+
headless: boolean;
|
|
26
|
+
dirs: Record<string, string>;
|
|
27
|
+
models: Record<string, string>;
|
|
28
|
+
providers: Record<string, string>;
|
|
29
|
+
integrations: {
|
|
30
|
+
langfuse: boolean;
|
|
31
|
+
testomatio: boolean;
|
|
32
|
+
};
|
|
33
|
+
env: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
interface SummarizedConfig {
|
|
36
|
+
ai?: AIConfig;
|
|
37
|
+
playwright?: {
|
|
38
|
+
url?: string;
|
|
39
|
+
browser?: string;
|
|
40
|
+
show?: boolean;
|
|
41
|
+
};
|
|
42
|
+
web?: {
|
|
43
|
+
url?: string;
|
|
44
|
+
};
|
|
45
|
+
api?: {
|
|
46
|
+
baseEndpoint?: string;
|
|
47
|
+
};
|
|
48
|
+
dirs?: Record<string, string>;
|
|
49
|
+
reporter?: ReporterConfig;
|
|
50
|
+
}
|
|
51
|
+
export {};
|
|
@@ -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
|
@@ -272,12 +272,19 @@ export declare class ConfigMissingError extends Error {
|
|
|
272
272
|
}
|
|
273
273
|
export declare function envConfigRequested(): boolean;
|
|
274
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>;
|
|
275
278
|
export declare function resolveConfigModels(ai?: AIConfig): Promise<void>;
|
|
276
279
|
export declare function resolveOutputRoot(baseUrl?: string): string;
|
|
277
280
|
export declare function resolveStateRoot(baseUrl: string, ephemeral?: boolean): string;
|
|
278
281
|
export declare function materializeKnowledge(outputRoot: string): void;
|
|
279
282
|
export declare function createModel(provider: string, modelId: string): Promise<any>;
|
|
280
283
|
type ModelRole = 'model' | 'visionModel' | 'agenticModel';
|
|
284
|
+
interface ConfiguredModel {
|
|
285
|
+
name: string;
|
|
286
|
+
provider: string;
|
|
287
|
+
}
|
|
281
288
|
interface ProviderInfo {
|
|
282
289
|
envKey: string;
|
|
283
290
|
load: () => Promise<(modelId: string) => any>;
|
|
@@ -287,4 +294,4 @@ interface EnvVar {
|
|
|
287
294
|
description: string;
|
|
288
295
|
required?: boolean;
|
|
289
296
|
}
|
|
290
|
-
export type { ModelRole, EnvVar, ProviderInfo };
|
|
297
|
+
export type { ModelRole, EnvVar, ProviderInfo, ConfiguredModel };
|
package/dist/src/config.js
CHANGED
|
@@ -231,6 +231,7 @@ export class ConfigParser {
|
|
|
231
231
|
model: { modelId: 'test-model', provider: 'test' },
|
|
232
232
|
config: {},
|
|
233
233
|
vision: false,
|
|
234
|
+
langfuse: { enabled: false },
|
|
234
235
|
},
|
|
235
236
|
dirs: {
|
|
236
237
|
knowledge: join(testBaseDir, 'knowledge'),
|
|
@@ -358,6 +359,17 @@ export class ConfigParser {
|
|
|
358
359
|
config.playwright = config.playwright || { browser: 'chromium', url: '' };
|
|
359
360
|
config.playwright.url = options.baseUrl;
|
|
360
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
|
+
}
|
|
361
373
|
return config;
|
|
362
374
|
}
|
|
363
375
|
validateConfig(config) {
|
|
@@ -454,6 +466,34 @@ export function missingConfigMessage(configFile = 'explorbot.config.js') {
|
|
|
454
466
|
Providers: ${Object.keys(PROVIDERS).join(', ')}
|
|
455
467
|
`;
|
|
456
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
|
+
}
|
|
457
497
|
export async function resolveConfigModels(ai) {
|
|
458
498
|
if (!ai)
|
|
459
499
|
return;
|
package/dist/src/explorbot.js
CHANGED
|
@@ -418,9 +418,12 @@ export class ExplorBot {
|
|
|
418
418
|
}
|
|
419
419
|
setCurrentPlan(plan) {
|
|
420
420
|
this.currentPlan = plan;
|
|
421
|
-
if (
|
|
421
|
+
if (!plan)
|
|
422
|
+
return;
|
|
423
|
+
if (!this.sessionPlans.includes(plan)) {
|
|
422
424
|
this.sessionPlans.push(plan);
|
|
423
425
|
}
|
|
426
|
+
plan.notifyChange();
|
|
424
427
|
}
|
|
425
428
|
getSessionTests() {
|
|
426
429
|
return this.sessionPlans.flatMap((p) => p.tests.filter((t) => t.startTime != null));
|
package/dist/src/remote.d.ts
CHANGED
|
@@ -6,8 +6,9 @@ import { type LogDestination, type TaggedLogEntry } from './utils/logger.js';
|
|
|
6
6
|
* process and a CI bot are the same case.
|
|
7
7
|
*
|
|
8
8
|
* It **is** a LogDestination — that is the whole integration on the logger's
|
|
9
|
-
* side — and it answers asks by installing itself
|
|
10
|
-
* controller's input callback. Nothing else in explorbot
|
|
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.
|
|
11
12
|
*/
|
|
12
13
|
export declare class Remote implements LogDestination {
|
|
13
14
|
url: string | null;
|
package/dist/src/remote.js
CHANGED
|
@@ -13,8 +13,9 @@ const FLUSH_TIMEOUT_MS = 3000;
|
|
|
13
13
|
* process and a CI bot are the same case.
|
|
14
14
|
*
|
|
15
15
|
* It **is** a LogDestination — that is the whole integration on the logger's
|
|
16
|
-
* side — and it answers asks by installing itself
|
|
17
|
-
* controller's input callback. Nothing else in explorbot
|
|
16
|
+
* side, messages and `data` alike — and it answers asks by installing itself
|
|
17
|
+
* as the execution controller's input callback. Nothing else in explorbot
|
|
18
|
+
* knows it exists.
|
|
18
19
|
*/
|
|
19
20
|
export class Remote {
|
|
20
21
|
url = null;
|
|
@@ -105,6 +106,11 @@ export class Remote {
|
|
|
105
106
|
write(entry) {
|
|
106
107
|
if (entry.type === 'html')
|
|
107
108
|
return;
|
|
109
|
+
if (entry.type === 'data') {
|
|
110
|
+
const [kind, payload] = entry.originalArgs || [];
|
|
111
|
+
this.send(String(kind), payload);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
108
114
|
let content = stripAnsi(entry.content ?? '');
|
|
109
115
|
if (content.length > CONTENT_CAP)
|
|
110
116
|
content = `${content.slice(0, CONTENT_CAP)}… (${content.length} chars)`;
|
|
@@ -1,4 +1,4 @@
|
|
|
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
|
export interface Link {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ActionResult } from './action-result.js';
|
|
2
2
|
import { detectFocusArea } from './utils/aria.js';
|
|
3
|
-
import { createDebug } from './utils/logger.js';
|
|
3
|
+
import { createDebug, tag } from './utils/logger.js';
|
|
4
4
|
import { slugify } from './utils/strings.js';
|
|
5
5
|
import { extractStatePath } from './utils/url-matcher.js';
|
|
6
6
|
const debugLog = createDebug('explorbot:state');
|
|
@@ -40,6 +40,8 @@ export class StateManager {
|
|
|
40
40
|
* Emit state change event to all listeners
|
|
41
41
|
*/
|
|
42
42
|
emitStateChange(event) {
|
|
43
|
+
const state = event.toState;
|
|
44
|
+
tag('data').log('state', { url: state.fullUrl || state.url, path: state.url, title: state.title, h1: state.h1 });
|
|
43
45
|
this.stateChangeListeners.forEach((listener) => {
|
|
44
46
|
try {
|
|
45
47
|
listener(event);
|
package/dist/src/test-plan.d.ts
CHANGED
|
@@ -113,6 +113,7 @@ export declare class Test extends Task {
|
|
|
113
113
|
finish(result?: TestResultType): void;
|
|
114
114
|
getDurationMs(): number | null;
|
|
115
115
|
getRemainingExpectations(): string[];
|
|
116
|
+
reportStatus(): void;
|
|
116
117
|
getLog(): Array<{
|
|
117
118
|
type: 'step' | 'note' | 'artifact';
|
|
118
119
|
content: string;
|