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
package/boat/prima/src/prima.ts
CHANGED
|
@@ -4,30 +4,31 @@ import { createRequire } from 'node:module';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { tool } from 'ai';
|
|
6
6
|
import dedent from 'dedent';
|
|
7
|
-
import { z } from 'zod';
|
|
8
7
|
import * as playwright from 'playwright';
|
|
9
8
|
import type { Browser } from 'playwright';
|
|
9
|
+
import { z } from 'zod';
|
|
10
10
|
import { ActionResult } from '../../../src/action-result.ts';
|
|
11
|
+
import { getPreviousResearch } from '../../../src/ai/researcher/cache.ts';
|
|
11
12
|
import { actionRule, locatorRule } from '../../../src/ai/rules.ts';
|
|
12
|
-
import { createAgentTools, createCodeceptJSTools } from '../../../src/ai/tools.ts';
|
|
13
|
+
import { createAgentTools, createCodeceptJSTools, createRefTools } from '../../../src/ai/tools.ts';
|
|
13
14
|
import { getAliveEndpoint, launchServer, listInstances, stopServer } from '../../../src/browser-server.ts';
|
|
14
|
-
import {
|
|
15
|
-
import { ConfigMissingError, ConfigParser, type ExplorbotConfig, outputPath } from '../../../src/config.ts';
|
|
15
|
+
import { ConfigCommand } from '../../../src/commands/config-command.ts';
|
|
16
|
+
import { ConfigMissingError, ConfigParser, EXPLORBOT_ENV_VARS, type ExplorbotConfig, outputPath } from '../../../src/config.ts';
|
|
16
17
|
import { ExplorBot } from '../../../src/explorbot.ts';
|
|
18
|
+
import { listSites } from '../../../src/global-config.ts';
|
|
17
19
|
import { Reporter } from '../../../src/reporter.ts';
|
|
18
|
-
import { Stats } from '../../../src/stats.ts';
|
|
19
20
|
import type { WebPageState } from '../../../src/state-manager.ts';
|
|
21
|
+
import { Stats } from '../../../src/stats.ts';
|
|
20
22
|
import { Task, Test, TestResult } from '../../../src/test-plan.ts';
|
|
21
|
-
import { getPreviousResearch } from '../../../src/ai/researcher/cache.ts';
|
|
22
23
|
import { compactAriaSnapshot } from '../../../src/utils/aria.ts';
|
|
23
|
-
import { mdq } from '../../../src/utils/markdown-query.ts';
|
|
24
24
|
import { browserErrorMessage } from '../../../src/utils/browser-errors.ts';
|
|
25
25
|
import { pluralize } from '../../../src/utils/logger.ts';
|
|
26
|
+
import { mdq } from '../../../src/utils/markdown-query.ts';
|
|
26
27
|
import { safeFilename } from '../../../src/utils/strings.ts';
|
|
27
28
|
import { type EnvelopeData, type InstanceInfo, writeArtifacts } from './envelope.ts';
|
|
28
29
|
import { isFunctionExpression, takePwValue, toCodeceptWrapper } from './pw-parser.ts';
|
|
29
|
-
import { type SessionRun, latestSessionFile, readSession, recordCommand, sessionFile, sessionsDir } from './session-log.ts';
|
|
30
30
|
import { type PwServerDescriptor, readDescriptors, selectDescriptor } from './pw-registry.ts';
|
|
31
|
+
import { type SessionRun, latestSessionFile, readSession, recordCommand, sessionFile, sessionsDir } from './session-log.ts';
|
|
31
32
|
|
|
32
33
|
const TESTER_ONLY_TOOLS = ['learnExperience', 'askUser'];
|
|
33
34
|
const ITERATIONS_PER_INSTRUCTION = 2;
|
|
@@ -40,7 +41,7 @@ const CONNECT_TIMEOUT = 3000;
|
|
|
40
41
|
const requireLib = createRequire(import.meta.url);
|
|
41
42
|
|
|
42
43
|
const VOLATILE_COLUMNS = ['CSS', 'XPath', 'Coordinates', 'eidx'];
|
|
43
|
-
const UNACCOUNTED: Record<string, string> = { open: '
|
|
44
|
+
const UNACCOUNTED: Record<string, string> = { open: 'the run ended without confirming this one — the actions above are everything that ran' };
|
|
44
45
|
|
|
45
46
|
function dropVolatileColumns(markdown: string): string {
|
|
46
47
|
return mdq(markdown)
|
|
@@ -73,6 +74,7 @@ export class Prima {
|
|
|
73
74
|
private server: { close: () => Promise<void> } | null = null;
|
|
74
75
|
private attached: string | null = null;
|
|
75
76
|
private session: SessionRun | null = null;
|
|
77
|
+
private artifacts?: EnvelopeData['artifacts'];
|
|
76
78
|
|
|
77
79
|
constructor(options: PrimaOptions = {}) {
|
|
78
80
|
this.options = options;
|
|
@@ -88,6 +90,14 @@ export class Prima {
|
|
|
88
90
|
});
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
static applyEnv(): void {
|
|
94
|
+
for (const { name } of EXPLORBOT_ENV_VARS) {
|
|
95
|
+
const value = process.env[name.replace('EXPLORBOT_', 'PRIMA_CLI_')];
|
|
96
|
+
if (!value) continue;
|
|
97
|
+
process.env[name] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
91
101
|
async start(): Promise<void> {
|
|
92
102
|
let discovery: Discovery | undefined;
|
|
93
103
|
if (!this.options.endpoint) {
|
|
@@ -146,7 +156,7 @@ export class Prima {
|
|
|
146
156
|
const deps = { explorer: this.bot.getExplorer(), stateManager: this.bot.stateManager(), ai: provider };
|
|
147
157
|
const ledger: LedgerEntry[] = instructions.map((text) => ({ text, status: 'open', proof: '' }));
|
|
148
158
|
const descent = { markup: false };
|
|
149
|
-
const tools = { ...createCodeceptJSTools(deps, task), ...this.testerTools(deps), context: this.contextTool(descent), completed: this.completedTool(), blocked: this.blockedTool() };
|
|
159
|
+
const tools = { ...createCodeceptJSTools(deps, task), ...createRefTools(deps, task), ...this.testerTools(deps), context: this.contextTool(descent), completed: this.completedTool(), blocked: this.blockedTool() };
|
|
150
160
|
conversation.addUserText(await this.instructionPrompt(instructions, await this.capturedResult(previousState)));
|
|
151
161
|
|
|
152
162
|
const used: string[] = [];
|
|
@@ -227,12 +237,12 @@ export class Prima {
|
|
|
227
237
|
|
|
228
238
|
if (aiError) return this.failureEnvelope(command, aiError, previousState);
|
|
229
239
|
|
|
230
|
-
if (
|
|
240
|
+
if (trace.length && ledger.some((entry) => entry.status === 'open')) {
|
|
231
241
|
await this.settleLedger(conversation, provider, ledger, trace);
|
|
232
242
|
}
|
|
233
243
|
|
|
234
244
|
const unfinished = ledger.filter((entry) => entry.status !== 'done');
|
|
235
|
-
const steps = [...trace, ...ledger.filter((entry) => entry.status === 'open').map((entry) => ({ label:
|
|
245
|
+
const steps = [...trace, ...ledger.filter((entry) => entry.status === 'open').map((entry) => ({ label: entry.text, ok: false, unconfirmed: true, proof: UNACCOUNTED.open }))];
|
|
236
246
|
|
|
237
247
|
if (failure && unfinished.length) {
|
|
238
248
|
const envelope = await this.failureEnvelope(command, failure.message, previousState);
|
|
@@ -241,7 +251,7 @@ export class Prima {
|
|
|
241
251
|
return envelope;
|
|
242
252
|
}
|
|
243
253
|
|
|
244
|
-
if (!
|
|
254
|
+
if (!trace.length && unfinished.length === ledger.length) {
|
|
245
255
|
const reason = ['No action was performed for these instructions on the current page.', narration].filter(Boolean).join(' ');
|
|
246
256
|
return this.failureEnvelope(command, reason, previousState);
|
|
247
257
|
}
|
|
@@ -254,10 +264,10 @@ export class Prima {
|
|
|
254
264
|
envelope.used = undefined;
|
|
255
265
|
envelope.changes = undefined;
|
|
256
266
|
|
|
257
|
-
const
|
|
258
|
-
if (
|
|
267
|
+
const blocked = ledger.filter((entry) => entry.status === 'blocked');
|
|
268
|
+
if (blocked.length) {
|
|
259
269
|
envelope.ok = false;
|
|
260
|
-
envelope.failure = { error:
|
|
270
|
+
envelope.failure = { error: blocked.map((entry) => `blocked: ${entry.text}${entry.proof ? ` — ${entry.proof}` : ''}`).join('\n') };
|
|
261
271
|
}
|
|
262
272
|
return envelope;
|
|
263
273
|
}
|
|
@@ -309,7 +319,13 @@ export class Prima {
|
|
|
309
319
|
completed() for those, blocked() for the ones the page could not do. Report every one — nothing else runs after this.
|
|
310
320
|
`);
|
|
311
321
|
|
|
312
|
-
|
|
322
|
+
let settleError: unknown = null;
|
|
323
|
+
const invoked = await provider.invokeConversation(conversation, { completed: this.completedTool(), blocked: this.blockedTool() }, { maxToolRoundtrips: 2, toolChoice: 'required', agentName: AI_AGENT_NAME }).catch((error: unknown) => {
|
|
324
|
+
settleError = error;
|
|
325
|
+
return null;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (settleError) trace.push({ label: 'settling which instructions were satisfied', ok: false, proof: browserErrorMessage(settleError) });
|
|
313
329
|
|
|
314
330
|
for (const execution of invoked?.toolExecutions || []) {
|
|
315
331
|
this.applyLedgerReport(execution, ledger, trace);
|
|
@@ -336,11 +352,11 @@ export class Prima {
|
|
|
336
352
|
const test = new Test(scenario, 'normal', outcomes, previousState?.url || this.options.url || '');
|
|
337
353
|
const tester = this.bot.agentTester();
|
|
338
354
|
|
|
339
|
-
|
|
355
|
+
await tester.test(test, { startOnCurrentPage: true });
|
|
340
356
|
|
|
341
357
|
const notes = Object.values(test.notes || {}) as Array<{ message: string; status?: string; log?: string; observation?: boolean }>;
|
|
342
|
-
const result = await this.capturedResult(this.bot.stateManager().getCurrentState());
|
|
343
|
-
const envelope = await this.reportEnvelope(command, result, previousState, {
|
|
358
|
+
const result = await this.capturedResult(this.bot.stateManager().getCurrentState(), { screenshot: this.visionEnabled() });
|
|
359
|
+
const envelope = await this.reportEnvelope(command, result, previousState, {});
|
|
344
360
|
const recorded = notes.filter((note) => !note.observation && !outcomes.includes(note.message));
|
|
345
361
|
const failed = recorded.filter((note) => note.status === TestResult.FAILED);
|
|
346
362
|
envelope.steps = failed.map((note) => ({ label: note.message, ok: false, proof: note.log || '' }));
|
|
@@ -348,7 +364,24 @@ export class Prima {
|
|
|
348
364
|
const routine = recorded.length - failed.length;
|
|
349
365
|
if (routine) envelope.steps.push({ label: `${routine} further ${pluralize(routine, 'step')} ran without failing — prima status ${envelope.status} for the full log`, ok: true, proof: '' });
|
|
350
366
|
|
|
351
|
-
envelope.expectations = await this.bot.agentPilot().settleExpectations(test);
|
|
367
|
+
envelope.expectations = await this.bot.agentPilot().settleExpectations(test, result);
|
|
368
|
+
|
|
369
|
+
if (!result.screenshot || !this.visionEnabled()) {
|
|
370
|
+
envelope.warning = 'These outcomes were settled from the run log alone — no screenshot backed them. Set ai.visionModel, or check anything visual with prima ask.';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const unreached = envelope.expectations.filter((expectation) => expectation.status === 'failed');
|
|
374
|
+
const contradicted = envelope.expectations.filter((expectation) => expectation.status === 'contradiction');
|
|
375
|
+
envelope.ok = !unreached.length && !contradicted.length;
|
|
376
|
+
|
|
377
|
+
const problems = [...unreached.map((expectation) => `not reached: ${expectation.text}`), ...contradicted.map((expectation) => `the picture and the run disagree about: ${expectation.text}`)];
|
|
378
|
+
if (problems.length) envelope.failure = { error: problems.join('\n') };
|
|
379
|
+
if (contradicted.length) envelope.artifacts = this.artifacts;
|
|
380
|
+
|
|
381
|
+
if (!test.hasFinished || test.isSkipped) {
|
|
382
|
+
envelope.ok = false;
|
|
383
|
+
envelope.failure = { error: `the run did not complete, so it established nothing about the app: ${notes.at(-1)?.message || 'no steps were recorded'}` };
|
|
384
|
+
}
|
|
352
385
|
|
|
353
386
|
const observations = notes.filter((note) => note.observation).map((note) => note.message);
|
|
354
387
|
if (observations.length) envelope.answer = ['Page problems noticed while running, not step failures:', ...observations.map((line) => `- ${line}`)].join('\n');
|
|
@@ -373,7 +406,15 @@ export class Prima {
|
|
|
373
406
|
const previousState = this.bot.stateManager().getCurrentState();
|
|
374
407
|
const result = await this.capturedResult(previousState);
|
|
375
408
|
const verification = await this.bot.agentNavigator().verifyState(assertion, result);
|
|
376
|
-
|
|
409
|
+
const outcome: Partial<EnvelopeData> = { assertions: verification.results || [] };
|
|
410
|
+
|
|
411
|
+
if (verification.inexpressible) {
|
|
412
|
+
const question = `Judging only from the screenshot, is this true of the page: "${assertion}"? Answer true, false or undetermined, and say what settles it.`;
|
|
413
|
+
const seen = await this.visionAnswer(question, await this.capturedResult(previousState, { screenshot: this.visionEnabled() }));
|
|
414
|
+
if (seen) outcome.answer = `No assertion could express this claim, so it was judged from a screenshot instead.\n\n${seen}`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return this.reportEnvelope(command, result, previousState, outcome);
|
|
377
418
|
}
|
|
378
419
|
|
|
379
420
|
async research(opts: { data?: boolean; deep?: boolean; fresh?: boolean } = {}): Promise<EnvelopeData> {
|
|
@@ -436,27 +477,13 @@ export class Prima {
|
|
|
436
477
|
return stopped;
|
|
437
478
|
}
|
|
438
479
|
|
|
439
|
-
async config(): Promise<string> {
|
|
480
|
+
async config(json?: boolean): Promise<string> {
|
|
440
481
|
const [site] = listSites();
|
|
441
482
|
if (site && !this.configBaseUrl()) this.sessionUrl = site.url;
|
|
442
483
|
const config = await this.loadConfig();
|
|
484
|
+
const parser = ConfigParser.getInstance();
|
|
443
485
|
|
|
444
|
-
|
|
445
|
-
if (typeof model === 'string') return model;
|
|
446
|
-
return (model as any)?.modelId || (model as any)?.model || 'unknown';
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
const ai = config.ai || ({} as any);
|
|
450
|
-
const roles: Array<[string, unknown]> = [
|
|
451
|
-
['model', ai.model],
|
|
452
|
-
['agenticModel', ai.agenticModel],
|
|
453
|
-
['visionModel', ai.visionModel],
|
|
454
|
-
];
|
|
455
|
-
|
|
456
|
-
const lines = roles.filter(([, model]) => model).map(([role, model]) => `${role.padEnd(14)} ${named(model)}`);
|
|
457
|
-
lines.push(`config ${ConfigParser.getInstance().getConfigPath() || 'built-in defaults'}`);
|
|
458
|
-
if (ai.langfuse?.enabled) lines.push('telemetry langfuse');
|
|
459
|
-
return lines.join('\n');
|
|
486
|
+
return ConfigCommand.render(config, { configPath: parser.getConfigPath(), root: parser.getProjectRoot(), json });
|
|
460
487
|
}
|
|
461
488
|
|
|
462
489
|
record(envelope: EnvelopeData, durationMs: number): void {
|
|
@@ -752,6 +779,7 @@ export class Prima {
|
|
|
752
779
|
<proof>
|
|
753
780
|
An instruction is done only when a change on the page shows it. After each action read the reported change and decide which part of it proves the instruction.
|
|
754
781
|
That part is what completed() takes as its proof. Do not restate the action as if it were the outcome.
|
|
782
|
+
How much of the page moved is not evidence of whether it happened — a change confined to one region proves an instruction as well as one that redraws everything.
|
|
755
783
|
An instruction that only inspects the page is satisfied by what you can see, including seeing that something is absent — those need no action at all.
|
|
756
784
|
</proof>
|
|
757
785
|
|
|
@@ -909,6 +937,7 @@ export class Prima {
|
|
|
909
937
|
|
|
910
938
|
private visionEnabled(): boolean {
|
|
911
939
|
if (this.options.noVision) return false;
|
|
940
|
+
if (Stats.visionDisabled) return false;
|
|
912
941
|
return this.bot.getProvider().hasVision?.() === true;
|
|
913
942
|
}
|
|
914
943
|
|
|
@@ -1022,7 +1051,12 @@ export class Prima {
|
|
|
1022
1051
|
private async pageChanges(result: ActionResult, previousState: WebPageState | null, code: string): Promise<string> {
|
|
1023
1052
|
if (!previousState) return 'no snapshot was captured before this command, so nothing could be compared';
|
|
1024
1053
|
const toolResult = await result.toToolResult(ActionResult.fromState(previousState), code);
|
|
1025
|
-
|
|
1054
|
+
const pageDiff = toolResult.pageDiff;
|
|
1055
|
+
if (!pageDiff?.urlChanged) return pageDiff?.ariaChanges || 'no change';
|
|
1056
|
+
|
|
1057
|
+
const lines = [`left ${previousState.url} for ${result.url}`];
|
|
1058
|
+
for (const message of pageDiff.messages ?? []) lines.push(`- ${message}`);
|
|
1059
|
+
return lines.join('\n');
|
|
1026
1060
|
}
|
|
1027
1061
|
|
|
1028
1062
|
async status(hash: string): Promise<EnvelopeData> {
|
|
@@ -1035,7 +1069,6 @@ export class Prima {
|
|
|
1035
1069
|
ok: true,
|
|
1036
1070
|
command: `status ${hash}`,
|
|
1037
1071
|
page: saved.page,
|
|
1038
|
-
changes: saved.changes,
|
|
1039
1072
|
instance: await this.instanceInfo(),
|
|
1040
1073
|
artifacts: { aria: path.join(dir, 'aria.yml'), html: path.join(dir, 'page.html') },
|
|
1041
1074
|
};
|
|
@@ -1044,7 +1077,7 @@ export class Prima {
|
|
|
1044
1077
|
private async saveStatus(result: ActionResult): Promise<string> {
|
|
1045
1078
|
const hash = this.statusHash();
|
|
1046
1079
|
await this.writeSnapshot(result);
|
|
1047
|
-
writeFileSync(path.join(this.statusDir(hash), 'status.json'), JSON.stringify({ page: this.pageBlock(result, null)
|
|
1080
|
+
writeFileSync(path.join(this.statusDir(hash), 'status.json'), JSON.stringify({ page: this.pageBlock(result, null) }), 'utf-8');
|
|
1048
1081
|
return hash;
|
|
1049
1082
|
}
|
|
1050
1083
|
|
|
@@ -1062,13 +1095,13 @@ export class Prima {
|
|
|
1062
1095
|
if (diff) writeFileSync(`${stem}.diff.yaml`, diff, 'utf-8');
|
|
1063
1096
|
}
|
|
1064
1097
|
|
|
1065
|
-
private async writeSnapshot(result: ActionResult): Promise<
|
|
1066
|
-
writeArtifacts(this.statusDir(), {
|
|
1098
|
+
private async writeSnapshot(result: ActionResult): Promise<void> {
|
|
1099
|
+
this.artifacts = writeArtifacts(this.statusDir(), {
|
|
1067
1100
|
aria: result.ariaSnapshot,
|
|
1068
1101
|
html: await result.combinedHtml(),
|
|
1102
|
+
screenshot: result.screenshot,
|
|
1069
1103
|
requests: this.bot.requestStore().getRequests(),
|
|
1070
1104
|
});
|
|
1071
|
-
return undefined;
|
|
1072
1105
|
}
|
|
1073
1106
|
|
|
1074
1107
|
private statusHash(): string {
|
|
@@ -15,7 +15,7 @@ import { remote } from '../src/remote.js';
|
|
|
15
15
|
import { Stats } from '../src/stats.js';
|
|
16
16
|
import { Plan } from '../src/test-plan.js';
|
|
17
17
|
import { getCliName } from "../src/utils/cli-name.js";
|
|
18
|
-
import { log, setPreserveConsoleLogs } from '../src/utils/logger.js';
|
|
18
|
+
import { isVerboseMode, log, setPreserveConsoleLogs, setQuietMode } from '../src/utils/logger.js';
|
|
19
19
|
import { jsonToTable } from '../src/utils/markdown-parser.js';
|
|
20
20
|
import { parseMarkdownToTerminal } from '../src/utils/markdown-terminal.js';
|
|
21
21
|
import { printNextSteps, relativeToCwd } from "../src/utils/next-steps.js";
|
|
@@ -386,6 +386,23 @@ addCommonOptions(program
|
|
|
386
386
|
const cmd = new FreesailCommand(explorBot);
|
|
387
387
|
await cmd.execute(args);
|
|
388
388
|
});
|
|
389
|
+
program
|
|
390
|
+
.command('config [url]')
|
|
391
|
+
.description('Show models, config file and paths used by this run')
|
|
392
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
393
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
394
|
+
.option('--json', 'Print the resolved config as JSON')
|
|
395
|
+
.action(async (url, options) => {
|
|
396
|
+
setQuietMode(!isVerboseMode());
|
|
397
|
+
const { ConfigCommand } = await import('../src/commands/config-command.js');
|
|
398
|
+
try {
|
|
399
|
+
console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
389
406
|
program
|
|
390
407
|
.command('init')
|
|
391
408
|
.description('Initialize configuration for a project or for this machine')
|
|
@@ -826,10 +843,5 @@ ${rows}
|
|
|
826
843
|
${cli} explore /login --max-tests 3
|
|
827
844
|
`;
|
|
828
845
|
};
|
|
829
|
-
|
|
830
|
-
cmd.addHelpText('after', envHelp);
|
|
831
|
-
for (const sub of cmd.commands)
|
|
832
|
-
addEnvHelp(sub);
|
|
833
|
-
};
|
|
834
|
-
addEnvHelp(program);
|
|
846
|
+
program.addHelpText('after', envHelp);
|
|
835
847
|
program.parse();
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
+
import { ConfigCommand } from "../../../src/commands/config-command.js";
|
|
5
|
+
import { listSites } from "../../../src/global-config.js";
|
|
4
6
|
import { setPreserveConsoleLogs } from "../../../src/utils/logger.js";
|
|
5
7
|
import { getStyles } from "./ai/chief/styles.js";
|
|
6
8
|
import { ApiBot } from "./apibot.js";
|
|
9
|
+
import { ApibotConfigParser } from "./config.js";
|
|
7
10
|
function buildOptions(options) {
|
|
8
11
|
return {
|
|
9
12
|
verbose: options.verbose || options.debug,
|
|
@@ -68,6 +71,20 @@ export function createApiCommands(name = 'api') {
|
|
|
68
71
|
process.exit(1);
|
|
69
72
|
}
|
|
70
73
|
});
|
|
74
|
+
addCommonOptions(cmd.command('config [endpoint]').description('Show models, config file and paths used by this run'))
|
|
75
|
+
.option('--json', 'Print the resolved config as JSON')
|
|
76
|
+
.action(async (endpoint, options) => {
|
|
77
|
+
const parser = ApibotConfigParser.getInstance();
|
|
78
|
+
const [site] = listSites();
|
|
79
|
+
try {
|
|
80
|
+
const config = await parser.loadConfig({ config: options.config, path: options.path, endpoint: endpoint || site?.url });
|
|
81
|
+
console.log(ConfigCommand.render(config, { configPath: parser.getConfigPath(), root: parser.getProjectRoot(), json: options.json }));
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
71
88
|
addCommonOptions(cmd.command('test <planfile> [index]').description('Execute tests from a plan file. Index: 1, 1-3, *')).action(async (planfile, index, options) => {
|
|
72
89
|
setPreserveConsoleLogs(true);
|
|
73
90
|
try {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
-
import {
|
|
4
|
+
import { ConfigCommand } from "../../../src/commands/config-command.js";
|
|
5
|
+
import { isVerboseMode, setPreserveConsoleLogs, setQuietMode } from "../../../src/utils/logger.js";
|
|
5
6
|
import { DocBot } from "./docbot.js";
|
|
6
7
|
function buildOptions(options) {
|
|
7
8
|
return {
|
|
@@ -56,6 +57,18 @@ export function createDocsCommands(name = 'docs') {
|
|
|
56
57
|
process.exit(1);
|
|
57
58
|
}
|
|
58
59
|
});
|
|
60
|
+
addCommonOptions(cmd.command('config [url]').description('Show models, config file and paths used by this run'))
|
|
61
|
+
.option('--json', 'Print the resolved config as JSON')
|
|
62
|
+
.action(async (url, options) => {
|
|
63
|
+
setQuietMode(!isVerboseMode());
|
|
64
|
+
try {
|
|
65
|
+
console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
59
72
|
cmd
|
|
60
73
|
.command('init')
|
|
61
74
|
.description('Initialize doc collector configuration')
|
|
@@ -20,16 +20,26 @@ const helpContract = dedent `
|
|
|
20
20
|
prima pw "({ page }) => page.click('[data-test=submit]')"
|
|
21
21
|
`;
|
|
22
22
|
const checkHelp = dedent `
|
|
23
|
-
check takes an outcome rather than a click path, and works out how to reach it.
|
|
23
|
+
check takes an outcome rather than a click path, and works out how to reach it. It runs
|
|
24
|
+
on the page you are already on and never reloads it, so an open dialog survives the check.
|
|
24
25
|
--expected one outcome the run must reach, repeatable for several. Without it the
|
|
25
26
|
scenario text is the single expected outcome. Each comes back under
|
|
26
|
-
### Expected outcomes as PASSED, FAILED or not verified
|
|
27
|
-
means the run never checked it, which is not the same
|
|
27
|
+
### Expected outcomes as PASSED, FAILED, CONTRADICTION or not verified.
|
|
28
|
+
"not verified" means the run never checked it, which is not the same
|
|
29
|
+
as false.
|
|
30
|
+
Outcomes are settled against a screenshot of the whole page: what a user can see is
|
|
31
|
+
the proof, and the run log only says what was done. CONTRADICTION means the two
|
|
32
|
+
disagree - reported with both sides rather than settled one way, and ### Artifacts
|
|
33
|
+
then names the html, aria and screenshot on disk so you can judge it yourself. Not
|
|
34
|
+
finding something in the picture is not enough on its own; that is "not verified".
|
|
35
|
+
ok: follows those outcomes - false when one FAILED or CONTRADICTED, or when the run
|
|
36
|
+
could not complete, which is reported as such rather than as an app failure.
|
|
28
37
|
Page problems seen on the way appear under ### Answer, not as step failures.
|
|
29
38
|
`;
|
|
30
39
|
const doHelp = dedent `
|
|
31
|
-
Each instruction is numbered and accounted for: ### Steps reports each as ok or
|
|
32
|
-
|
|
40
|
+
Each instruction is numbered and accounted for: ### Steps reports each as ok, FAIL or ??.
|
|
41
|
+
?? means the action ran but the run ended without confirming that instruction - read the
|
|
42
|
+
steps above it. Only FAIL and an instruction the page could not carry out fail the command.
|
|
33
43
|
Nothing runs past the last instruction given. A whole remaining sequence in one call is
|
|
34
44
|
what makes this tier cheap.
|
|
35
45
|
`;
|
|
@@ -85,6 +95,8 @@ function addCommonOptions(cmd) {
|
|
|
85
95
|
.option('-p, --path <path>', 'Working directory path')
|
|
86
96
|
.option('-i, --instance <name>', 'Browser instance to drive')
|
|
87
97
|
.option('--session [file]', 'Persist cookies and storage to a session file')
|
|
98
|
+
.option('--model <model>', 'Main model, as provider/model-id')
|
|
99
|
+
.option('--vision-model <model>', 'Model for screenshot analysis, as provider/model-id')
|
|
88
100
|
.option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
|
|
89
101
|
.option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
|
|
90
102
|
.option('--url <url>', 'Page to open when the session has no page yet')
|
|
@@ -93,8 +105,13 @@ function addCommonOptions(cmd) {
|
|
|
93
105
|
.addHelpText('after', `\n${sessionHelp}`);
|
|
94
106
|
}
|
|
95
107
|
function primaFor(options) {
|
|
108
|
+
Prima.applyEnv();
|
|
96
109
|
if (options.ephemeral)
|
|
97
110
|
process.env.EXPLORBOT_EPHEMERAL = '1';
|
|
111
|
+
if (options.model)
|
|
112
|
+
process.env.EXPLORBOT_AI_MODEL = options.model;
|
|
113
|
+
if (options.visionModel)
|
|
114
|
+
process.env.EXPLORBOT_VISION_MODEL = options.visionModel;
|
|
98
115
|
return new Prima(buildOptions(options));
|
|
99
116
|
}
|
|
100
117
|
async function runPrima(options, command, run, record = true) {
|
|
@@ -165,10 +182,12 @@ export function createPrimaCommands(name = 'prima') {
|
|
|
165
182
|
options.baseUrl = target;
|
|
166
183
|
await runPrima(options, `go ${target}`, (prima) => prima.go(target));
|
|
167
184
|
});
|
|
168
|
-
addCommonOptions(cmd.command('config').description('Show
|
|
185
|
+
addCommonOptions(cmd.command('config').description('Show models, config file and paths used by this run'))
|
|
186
|
+
.option('--json', 'Print the resolved config as JSON')
|
|
187
|
+
.action(async (options) => {
|
|
169
188
|
setQuietMode(!isVerboseMode());
|
|
170
189
|
const prima = primaFor(options);
|
|
171
|
-
console.log(await prima.config().catch((error) => browserErrorMessage(error)));
|
|
190
|
+
console.log(await prima.config(options.json).catch((error) => browserErrorMessage(error)));
|
|
172
191
|
await prima.stop().catch(() => { });
|
|
173
192
|
process.exit(0);
|
|
174
193
|
});
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
const EXPECTATION_LABELS = {
|
|
4
|
-
passed: 'PASSED
|
|
5
|
-
failed: 'FAILED
|
|
6
|
-
unverified: 'not verified',
|
|
4
|
+
passed: 'PASSED ',
|
|
5
|
+
failed: 'FAILED ',
|
|
6
|
+
unverified: 'not verified ',
|
|
7
|
+
contradiction: 'CONTRADICTION',
|
|
7
8
|
};
|
|
8
9
|
export function renderEnvelope(data) {
|
|
9
|
-
const sections = [renderResult(data), renderPage(data), renderValue(data), renderChanges(data), renderSteps(data), renderExpectations(data), renderOutcome(data), ...renderFailure(data), renderInstance(data), renderArtifacts(data)];
|
|
10
|
+
const sections = [renderResult(data), renderPage(data), renderValue(data), renderChanges(data), renderSteps(data), renderExpectations(data), renderWarning(data), renderOutcome(data), ...renderFailure(data), renderInstance(data), renderArtifacts(data)];
|
|
10
11
|
return sections.filter((section) => section).join('\n\n');
|
|
11
12
|
}
|
|
12
13
|
export function writeArtifacts(dir, snapshot) {
|
|
@@ -17,6 +18,10 @@ export function writeArtifacts(dir, snapshot) {
|
|
|
17
18
|
};
|
|
18
19
|
writeFileSync(paths.aria, snapshot.aria ?? '', 'utf-8');
|
|
19
20
|
writeFileSync(paths.html, snapshot.html ?? '', 'utf-8');
|
|
21
|
+
if (snapshot.screenshot) {
|
|
22
|
+
paths.screenshot = path.resolve(dir, 'page.png');
|
|
23
|
+
writeFileSync(paths.screenshot, snapshot.screenshot);
|
|
24
|
+
}
|
|
20
25
|
if (!snapshot.requests.length)
|
|
21
26
|
return paths;
|
|
22
27
|
paths.network = path.resolve(dir, 'network.jsonl');
|
|
@@ -55,7 +60,12 @@ function renderSteps(data) {
|
|
|
55
60
|
return null;
|
|
56
61
|
const lines = [];
|
|
57
62
|
data.steps.forEach((step, index) => {
|
|
58
|
-
|
|
63
|
+
let mark = 'FAIL';
|
|
64
|
+
if (step.ok)
|
|
65
|
+
mark = 'ok ';
|
|
66
|
+
if (step.unconfirmed)
|
|
67
|
+
mark = '?? ';
|
|
68
|
+
lines.push(`${index + 1}. ${mark} ${step.label}`);
|
|
59
69
|
for (const line of (step.proof || '').split('\n').filter(Boolean))
|
|
60
70
|
lines.push(` ${line}`);
|
|
61
71
|
});
|
|
@@ -66,9 +76,21 @@ function renderSteps(data) {
|
|
|
66
76
|
function renderExpectations(data) {
|
|
67
77
|
if (!data.expectations?.length)
|
|
68
78
|
return null;
|
|
69
|
-
const lines =
|
|
79
|
+
const lines = [];
|
|
80
|
+
data.expectations.forEach((expectation, index) => {
|
|
81
|
+
lines.push(`${index + 1}. ${EXPECTATION_LABELS[expectation.status]} ${expectation.text}`);
|
|
82
|
+
if (expectation.status !== 'contradiction' && expectation.status !== 'failed')
|
|
83
|
+
return;
|
|
84
|
+
for (const line of (expectation.evidence || '').split('\n').filter(Boolean))
|
|
85
|
+
lines.push(` ${line}`);
|
|
86
|
+
});
|
|
70
87
|
return section('Expected outcomes', lines.join('\n'));
|
|
71
88
|
}
|
|
89
|
+
function renderWarning(data) {
|
|
90
|
+
if (!data.warning)
|
|
91
|
+
return null;
|
|
92
|
+
return section('Warning', data.warning);
|
|
93
|
+
}
|
|
72
94
|
function renderOutcome(data) {
|
|
73
95
|
if (data.answer)
|
|
74
96
|
return section('Answer', data.answer);
|
|
@@ -127,9 +149,11 @@ function tabsLabel(tabs) {
|
|
|
127
149
|
export function renderArtifacts(data) {
|
|
128
150
|
if (!data.artifacts)
|
|
129
151
|
return null;
|
|
130
|
-
const lines = [`aria:
|
|
152
|
+
const lines = [`aria: ${data.artifacts.aria}`, `html: ${data.artifacts.html}`];
|
|
153
|
+
if (data.artifacts.screenshot)
|
|
154
|
+
lines.push(`screenshot: ${data.artifacts.screenshot}`);
|
|
131
155
|
if (data.artifacts.network)
|
|
132
|
-
lines.push(`network:
|
|
156
|
+
lines.push(`network: ${data.artifacts.network}`);
|
|
133
157
|
return section('Artifacts', lines.join('\n'));
|
|
134
158
|
}
|
|
135
159
|
function align(label, marker, width) {
|