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
|
@@ -4,28 +4,29 @@ 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';
|
|
8
|
+
import { z } from 'zod';
|
|
9
9
|
import { ActionResult } from "../../../src/action-result.js";
|
|
10
|
+
import { getPreviousResearch } from "../../../src/ai/researcher/cache.js";
|
|
10
11
|
import { actionRule, locatorRule } from "../../../src/ai/rules.js";
|
|
11
|
-
import { createAgentTools, createCodeceptJSTools } from "../../../src/ai/tools.js";
|
|
12
|
+
import { createAgentTools, createCodeceptJSTools, createRefTools } from "../../../src/ai/tools.js";
|
|
12
13
|
import { getAliveEndpoint, launchServer, listInstances, stopServer } from "../../../src/browser-server.js";
|
|
13
|
-
import {
|
|
14
|
-
import { ConfigMissingError, ConfigParser, outputPath } from "../../../src/config.js";
|
|
14
|
+
import { ConfigCommand } from "../../../src/commands/config-command.js";
|
|
15
|
+
import { ConfigMissingError, ConfigParser, EXPLORBOT_ENV_VARS, outputPath } from "../../../src/config.js";
|
|
15
16
|
import { ExplorBot } from "../../../src/explorbot.js";
|
|
17
|
+
import { listSites } from "../../../src/global-config.js";
|
|
16
18
|
import { Reporter } from "../../../src/reporter.js";
|
|
17
19
|
import { Stats } from "../../../src/stats.js";
|
|
18
20
|
import { Task, Test, TestResult } from "../../../src/test-plan.js";
|
|
19
|
-
import { getPreviousResearch } from "../../../src/ai/researcher/cache.js";
|
|
20
21
|
import { compactAriaSnapshot } from "../../../src/utils/aria.js";
|
|
21
|
-
import { mdq } from "../../../src/utils/markdown-query.js";
|
|
22
22
|
import { browserErrorMessage } from "../../../src/utils/browser-errors.js";
|
|
23
23
|
import { pluralize } from "../../../src/utils/logger.js";
|
|
24
|
+
import { mdq } from "../../../src/utils/markdown-query.js";
|
|
24
25
|
import { safeFilename } from "../../../src/utils/strings.js";
|
|
25
26
|
import { writeArtifacts } from "./envelope.js";
|
|
26
27
|
import { isFunctionExpression, takePwValue, toCodeceptWrapper } from "./pw-parser.js";
|
|
27
|
-
import { latestSessionFile, readSession, recordCommand, sessionFile, sessionsDir } from "./session-log.js";
|
|
28
28
|
import { readDescriptors, selectDescriptor } from "./pw-registry.js";
|
|
29
|
+
import { latestSessionFile, readSession, recordCommand, sessionFile, sessionsDir } from "./session-log.js";
|
|
29
30
|
const TESTER_ONLY_TOOLS = ['learnExperience', 'askUser'];
|
|
30
31
|
const ITERATIONS_PER_INSTRUCTION = 2;
|
|
31
32
|
const MAX_INSTRUCTION_ITERATIONS = 24;
|
|
@@ -36,7 +37,7 @@ const AI_AGENT_NAME = 'prima';
|
|
|
36
37
|
const CONNECT_TIMEOUT = 3000;
|
|
37
38
|
const requireLib = createRequire(import.meta.url);
|
|
38
39
|
const VOLATILE_COLUMNS = ['CSS', 'XPath', 'Coordinates', 'eidx'];
|
|
39
|
-
const UNACCOUNTED = { open: '
|
|
40
|
+
const UNACCOUNTED = { open: 'the run ended without confirming this one — the actions above are everything that ran' };
|
|
40
41
|
function dropVolatileColumns(markdown) {
|
|
41
42
|
return mdq(markdown)
|
|
42
43
|
.query('table')
|
|
@@ -67,6 +68,7 @@ export class Prima {
|
|
|
67
68
|
server = null;
|
|
68
69
|
attached = null;
|
|
69
70
|
session = null;
|
|
71
|
+
artifacts;
|
|
70
72
|
constructor(options = {}) {
|
|
71
73
|
this.options = options;
|
|
72
74
|
this.bot = new ExplorBot({
|
|
@@ -80,6 +82,14 @@ export class Prima {
|
|
|
80
82
|
reporter: { enabled: false },
|
|
81
83
|
});
|
|
82
84
|
}
|
|
85
|
+
static applyEnv() {
|
|
86
|
+
for (const { name } of EXPLORBOT_ENV_VARS) {
|
|
87
|
+
const value = process.env[name.replace('EXPLORBOT_', 'PRIMA_CLI_')];
|
|
88
|
+
if (!value)
|
|
89
|
+
continue;
|
|
90
|
+
process.env[name] = value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
83
93
|
async start() {
|
|
84
94
|
let discovery;
|
|
85
95
|
if (!this.options.endpoint) {
|
|
@@ -134,7 +144,7 @@ export class Prima {
|
|
|
134
144
|
const deps = { explorer: this.bot.getExplorer(), stateManager: this.bot.stateManager(), ai: provider };
|
|
135
145
|
const ledger = instructions.map((text) => ({ text, status: 'open', proof: '' }));
|
|
136
146
|
const descent = { markup: false };
|
|
137
|
-
const tools = { ...createCodeceptJSTools(deps, task), ...this.testerTools(deps), context: this.contextTool(descent), completed: this.completedTool(), blocked: this.blockedTool() };
|
|
147
|
+
const tools = { ...createCodeceptJSTools(deps, task), ...createRefTools(deps, task), ...this.testerTools(deps), context: this.contextTool(descent), completed: this.completedTool(), blocked: this.blockedTool() };
|
|
138
148
|
conversation.addUserText(await this.instructionPrompt(instructions, await this.capturedResult(previousState)));
|
|
139
149
|
const used = [];
|
|
140
150
|
const trace = [];
|
|
@@ -208,18 +218,18 @@ export class Prima {
|
|
|
208
218
|
}
|
|
209
219
|
if (aiError)
|
|
210
220
|
return this.failureEnvelope(command, aiError, previousState);
|
|
211
|
-
if (
|
|
221
|
+
if (trace.length && ledger.some((entry) => entry.status === 'open')) {
|
|
212
222
|
await this.settleLedger(conversation, provider, ledger, trace);
|
|
213
223
|
}
|
|
214
224
|
const unfinished = ledger.filter((entry) => entry.status !== 'done');
|
|
215
|
-
const steps = [...trace, ...ledger.filter((entry) => entry.status === 'open').map((entry) => ({ label:
|
|
225
|
+
const steps = [...trace, ...ledger.filter((entry) => entry.status === 'open').map((entry) => ({ label: entry.text, ok: false, unconfirmed: true, proof: UNACCOUNTED.open }))];
|
|
216
226
|
if (failure && unfinished.length) {
|
|
217
227
|
const envelope = await this.failureEnvelope(command, failure.message, previousState);
|
|
218
228
|
envelope.steps = steps;
|
|
219
229
|
envelope.stepFiles = this.statusDir();
|
|
220
230
|
return envelope;
|
|
221
231
|
}
|
|
222
|
-
if (!
|
|
232
|
+
if (!trace.length && unfinished.length === ledger.length) {
|
|
223
233
|
const reason = ['No action was performed for these instructions on the current page.', narration].filter(Boolean).join(' ');
|
|
224
234
|
return this.failureEnvelope(command, reason, previousState);
|
|
225
235
|
}
|
|
@@ -230,10 +240,10 @@ export class Prima {
|
|
|
230
240
|
// the step log already reports every action and what it changed
|
|
231
241
|
envelope.used = undefined;
|
|
232
242
|
envelope.changes = undefined;
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
243
|
+
const blocked = ledger.filter((entry) => entry.status === 'blocked');
|
|
244
|
+
if (blocked.length) {
|
|
235
245
|
envelope.ok = false;
|
|
236
|
-
envelope.failure = { error:
|
|
246
|
+
envelope.failure = { error: blocked.map((entry) => `blocked: ${entry.text}${entry.proof ? ` — ${entry.proof}` : ''}`).join('\n') };
|
|
237
247
|
}
|
|
238
248
|
return envelope;
|
|
239
249
|
}
|
|
@@ -281,7 +291,13 @@ export class Prima {
|
|
|
281
291
|
instructions have moved it on, and something you confirmed earlier stays confirmed even if it is gone.
|
|
282
292
|
completed() for those, blocked() for the ones the page could not do. Report every one — nothing else runs after this.
|
|
283
293
|
`);
|
|
284
|
-
|
|
294
|
+
let settleError = null;
|
|
295
|
+
const invoked = await provider.invokeConversation(conversation, { completed: this.completedTool(), blocked: this.blockedTool() }, { maxToolRoundtrips: 2, toolChoice: 'required', agentName: AI_AGENT_NAME }).catch((error) => {
|
|
296
|
+
settleError = error;
|
|
297
|
+
return null;
|
|
298
|
+
});
|
|
299
|
+
if (settleError)
|
|
300
|
+
trace.push({ label: 'settling which instructions were satisfied', ok: false, proof: browserErrorMessage(settleError) });
|
|
285
301
|
for (const execution of invoked?.toolExecutions || []) {
|
|
286
302
|
this.applyLedgerReport(execution, ledger, trace);
|
|
287
303
|
}
|
|
@@ -305,17 +321,32 @@ export class Prima {
|
|
|
305
321
|
const outcomes = expected.length ? expected : [scenario];
|
|
306
322
|
const test = new Test(scenario, 'normal', outcomes, previousState?.url || this.options.url || '');
|
|
307
323
|
const tester = this.bot.agentTester();
|
|
308
|
-
|
|
324
|
+
await tester.test(test, { startOnCurrentPage: true });
|
|
309
325
|
const notes = Object.values(test.notes || {});
|
|
310
|
-
const result = await this.capturedResult(this.bot.stateManager().getCurrentState());
|
|
311
|
-
const envelope = await this.reportEnvelope(command, result, previousState, {
|
|
326
|
+
const result = await this.capturedResult(this.bot.stateManager().getCurrentState(), { screenshot: this.visionEnabled() });
|
|
327
|
+
const envelope = await this.reportEnvelope(command, result, previousState, {});
|
|
312
328
|
const recorded = notes.filter((note) => !note.observation && !outcomes.includes(note.message));
|
|
313
329
|
const failed = recorded.filter((note) => note.status === TestResult.FAILED);
|
|
314
330
|
envelope.steps = failed.map((note) => ({ label: note.message, ok: false, proof: note.log || '' }));
|
|
315
331
|
const routine = recorded.length - failed.length;
|
|
316
332
|
if (routine)
|
|
317
333
|
envelope.steps.push({ label: `${routine} further ${pluralize(routine, 'step')} ran without failing — prima status ${envelope.status} for the full log`, ok: true, proof: '' });
|
|
318
|
-
envelope.expectations = await this.bot.agentPilot().settleExpectations(test);
|
|
334
|
+
envelope.expectations = await this.bot.agentPilot().settleExpectations(test, result);
|
|
335
|
+
if (!result.screenshot || !this.visionEnabled()) {
|
|
336
|
+
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.';
|
|
337
|
+
}
|
|
338
|
+
const unreached = envelope.expectations.filter((expectation) => expectation.status === 'failed');
|
|
339
|
+
const contradicted = envelope.expectations.filter((expectation) => expectation.status === 'contradiction');
|
|
340
|
+
envelope.ok = !unreached.length && !contradicted.length;
|
|
341
|
+
const problems = [...unreached.map((expectation) => `not reached: ${expectation.text}`), ...contradicted.map((expectation) => `the picture and the run disagree about: ${expectation.text}`)];
|
|
342
|
+
if (problems.length)
|
|
343
|
+
envelope.failure = { error: problems.join('\n') };
|
|
344
|
+
if (contradicted.length)
|
|
345
|
+
envelope.artifacts = this.artifacts;
|
|
346
|
+
if (!test.hasFinished || test.isSkipped) {
|
|
347
|
+
envelope.ok = false;
|
|
348
|
+
envelope.failure = { error: `the run did not complete, so it established nothing about the app: ${notes.at(-1)?.message || 'no steps were recorded'}` };
|
|
349
|
+
}
|
|
319
350
|
const observations = notes.filter((note) => note.observation).map((note) => note.message);
|
|
320
351
|
if (observations.length)
|
|
321
352
|
envelope.answer = ['Page problems noticed while running, not step failures:', ...observations.map((line) => `- ${line}`)].join('\n');
|
|
@@ -338,7 +369,14 @@ export class Prima {
|
|
|
338
369
|
const previousState = this.bot.stateManager().getCurrentState();
|
|
339
370
|
const result = await this.capturedResult(previousState);
|
|
340
371
|
const verification = await this.bot.agentNavigator().verifyState(assertion, result);
|
|
341
|
-
|
|
372
|
+
const outcome = { assertions: verification.results || [] };
|
|
373
|
+
if (verification.inexpressible) {
|
|
374
|
+
const question = `Judging only from the screenshot, is this true of the page: "${assertion}"? Answer true, false or undetermined, and say what settles it.`;
|
|
375
|
+
const seen = await this.visionAnswer(question, await this.capturedResult(previousState, { screenshot: this.visionEnabled() }));
|
|
376
|
+
if (seen)
|
|
377
|
+
outcome.answer = `No assertion could express this claim, so it was judged from a screenshot instead.\n\n${seen}`;
|
|
378
|
+
}
|
|
379
|
+
return this.reportEnvelope(command, result, previousState, outcome);
|
|
342
380
|
}
|
|
343
381
|
async research(opts = {}) {
|
|
344
382
|
const flags = [opts.data && '--data', opts.deep && '--deep', opts.fresh && '--fresh'].filter(Boolean);
|
|
@@ -398,27 +436,13 @@ export class Prima {
|
|
|
398
436
|
}
|
|
399
437
|
return stopped;
|
|
400
438
|
}
|
|
401
|
-
async config() {
|
|
439
|
+
async config(json) {
|
|
402
440
|
const [site] = listSites();
|
|
403
441
|
if (site && !this.configBaseUrl())
|
|
404
442
|
this.sessionUrl = site.url;
|
|
405
443
|
const config = await this.loadConfig();
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
return model;
|
|
409
|
-
return model?.modelId || model?.model || 'unknown';
|
|
410
|
-
};
|
|
411
|
-
const ai = config.ai || {};
|
|
412
|
-
const roles = [
|
|
413
|
-
['model', ai.model],
|
|
414
|
-
['agenticModel', ai.agenticModel],
|
|
415
|
-
['visionModel', ai.visionModel],
|
|
416
|
-
];
|
|
417
|
-
const lines = roles.filter(([, model]) => model).map(([role, model]) => `${role.padEnd(14)} ${named(model)}`);
|
|
418
|
-
lines.push(`config ${ConfigParser.getInstance().getConfigPath() || 'built-in defaults'}`);
|
|
419
|
-
if (ai.langfuse?.enabled)
|
|
420
|
-
lines.push('telemetry langfuse');
|
|
421
|
-
return lines.join('\n');
|
|
444
|
+
const parser = ConfigParser.getInstance();
|
|
445
|
+
return ConfigCommand.render(config, { configPath: parser.getConfigPath(), root: parser.getProjectRoot(), json });
|
|
422
446
|
}
|
|
423
447
|
record(envelope, durationMs) {
|
|
424
448
|
if (!this.session)
|
|
@@ -706,6 +730,7 @@ export class Prima {
|
|
|
706
730
|
<proof>
|
|
707
731
|
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.
|
|
708
732
|
That part is what completed() takes as its proof. Do not restate the action as if it were the outcome.
|
|
733
|
+
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.
|
|
709
734
|
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.
|
|
710
735
|
</proof>
|
|
711
736
|
|
|
@@ -856,6 +881,8 @@ export class Prima {
|
|
|
856
881
|
visionEnabled() {
|
|
857
882
|
if (this.options.noVision)
|
|
858
883
|
return false;
|
|
884
|
+
if (Stats.visionDisabled)
|
|
885
|
+
return false;
|
|
859
886
|
return this.bot.getProvider().hasVision?.() === true;
|
|
860
887
|
}
|
|
861
888
|
async answer(question, result) {
|
|
@@ -964,7 +991,13 @@ export class Prima {
|
|
|
964
991
|
if (!previousState)
|
|
965
992
|
return 'no snapshot was captured before this command, so nothing could be compared';
|
|
966
993
|
const toolResult = await result.toToolResult(ActionResult.fromState(previousState), code);
|
|
967
|
-
|
|
994
|
+
const pageDiff = toolResult.pageDiff;
|
|
995
|
+
if (!pageDiff?.urlChanged)
|
|
996
|
+
return pageDiff?.ariaChanges || 'no change';
|
|
997
|
+
const lines = [`left ${previousState.url} for ${result.url}`];
|
|
998
|
+
for (const message of pageDiff.messages ?? [])
|
|
999
|
+
lines.push(`- ${message}`);
|
|
1000
|
+
return lines.join('\n');
|
|
968
1001
|
}
|
|
969
1002
|
async status(hash) {
|
|
970
1003
|
const dir = this.statusDir(hash);
|
|
@@ -976,7 +1009,6 @@ export class Prima {
|
|
|
976
1009
|
ok: true,
|
|
977
1010
|
command: `status ${hash}`,
|
|
978
1011
|
page: saved.page,
|
|
979
|
-
changes: saved.changes,
|
|
980
1012
|
instance: await this.instanceInfo(),
|
|
981
1013
|
artifacts: { aria: path.join(dir, 'aria.yml'), html: path.join(dir, 'page.html') },
|
|
982
1014
|
};
|
|
@@ -984,7 +1016,7 @@ export class Prima {
|
|
|
984
1016
|
async saveStatus(result) {
|
|
985
1017
|
const hash = this.statusHash();
|
|
986
1018
|
await this.writeSnapshot(result);
|
|
987
|
-
writeFileSync(path.join(this.statusDir(hash), 'status.json'), JSON.stringify({ page: this.pageBlock(result, null)
|
|
1019
|
+
writeFileSync(path.join(this.statusDir(hash), 'status.json'), JSON.stringify({ page: this.pageBlock(result, null) }), 'utf-8');
|
|
988
1020
|
return hash;
|
|
989
1021
|
}
|
|
990
1022
|
async writeStepFiles(index, label, diff) {
|
|
@@ -1001,12 +1033,12 @@ export class Prima {
|
|
|
1001
1033
|
writeFileSync(`${stem}.diff.yaml`, diff, 'utf-8');
|
|
1002
1034
|
}
|
|
1003
1035
|
async writeSnapshot(result) {
|
|
1004
|
-
writeArtifacts(this.statusDir(), {
|
|
1036
|
+
this.artifacts = writeArtifacts(this.statusDir(), {
|
|
1005
1037
|
aria: result.ariaSnapshot,
|
|
1006
1038
|
html: await result.combinedHtml(),
|
|
1039
|
+
screenshot: result.screenshot,
|
|
1007
1040
|
requests: this.bot.requestStore().getRequests(),
|
|
1008
1041
|
});
|
|
1009
|
-
return undefined;
|
|
1010
1042
|
}
|
|
1011
1043
|
statusHash() {
|
|
1012
1044
|
this.hash ||= createHash('sha1')
|
package/dist/models.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"openrouter": {
|
|
3
3
|
"model": "openai/gpt-oss-20b:nitro",
|
|
4
|
-
"visionModel": "
|
|
5
|
-
"agenticModel": "
|
|
4
|
+
"visionModel": "openai/gpt-5.6-luna",
|
|
5
|
+
"agenticModel": "openai/gpt-5.6-luna"
|
|
6
6
|
},
|
|
7
7
|
"poolside": {
|
|
8
8
|
"model": "poolside/laguna-xs-2.1"
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"agenticModel": "qwen/qwen3.6-27b"
|
|
14
14
|
},
|
|
15
15
|
"openai": {
|
|
16
|
-
"model": "gpt-5
|
|
17
|
-
"visionModel": "gpt-5.
|
|
16
|
+
"model": "gpt-5-nano",
|
|
17
|
+
"visionModel": "gpt-5.6-luna",
|
|
18
18
|
"agenticModel": "gpt-5.6-luna"
|
|
19
19
|
},
|
|
20
20
|
"anthropic": {
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"boat/prima/src/**/*.ts",
|
|
31
31
|
"boat/prima/bin/**/*.ts",
|
|
32
32
|
"boat/prima/package.json",
|
|
33
|
+
"boat/prima/README.md",
|
|
33
34
|
"rules/",
|
|
34
35
|
"assets/sample-files/",
|
|
35
36
|
"models.json"
|
|
@@ -58,7 +59,8 @@
|
|
|
58
59
|
"lint:fix": "biome lint --write .",
|
|
59
60
|
"check": "biome check .",
|
|
60
61
|
"check:fix": "biome check --write .",
|
|
61
|
-
"langfuse:export": "bun run .claude/skills/explorbot-debug/langfuse-export.ts"
|
|
62
|
+
"langfuse:export": "bun run .claude/skills/explorbot-debug/langfuse-export.ts",
|
|
63
|
+
"build:prima": "bun run scripts/build-prima-npm.ts"
|
|
62
64
|
},
|
|
63
65
|
"keywords": [
|
|
64
66
|
"cli",
|
|
@@ -97,6 +99,7 @@
|
|
|
97
99
|
"ai": "^7.0.2",
|
|
98
100
|
"axe-core": "^4.11.1",
|
|
99
101
|
"bash-tool": "^1.3.15",
|
|
102
|
+
"chalk": "^5.6.2",
|
|
100
103
|
"cli-highlight": "^2.1.11",
|
|
101
104
|
"codeceptjs": "4.0.0-rc.16",
|
|
102
105
|
"commander": "^14.0.1",
|
|
@@ -121,6 +124,7 @@
|
|
|
121
124
|
"parse5": "^8.0.0",
|
|
122
125
|
"pixelmatch": "^7.2.0",
|
|
123
126
|
"playwright": "^1.62",
|
|
127
|
+
"playwright-core": "^1.62",
|
|
124
128
|
"pngjs": "^7.0.0",
|
|
125
129
|
"react": "^19.1.1",
|
|
126
130
|
"sambanova-ai-provider": "^1.2.2",
|
|
@@ -17,6 +17,7 @@ interface ActionResultData extends WebPageState {
|
|
|
17
17
|
h3?: string | undefined;
|
|
18
18
|
h4?: string | undefined;
|
|
19
19
|
browserLogs?: any[];
|
|
20
|
+
networkRequests?: NetworkCall[];
|
|
20
21
|
iframeSnapshots?: Array<{
|
|
21
22
|
src: string;
|
|
22
23
|
html: string;
|
|
@@ -34,6 +35,9 @@ export interface PageDiff {
|
|
|
34
35
|
currentUrl: string;
|
|
35
36
|
ariaChanges?: string | null;
|
|
36
37
|
ariaChangeCount?: number;
|
|
38
|
+
messages?: string[];
|
|
39
|
+
requests?: NetworkCall[];
|
|
40
|
+
consoleErrors?: string[];
|
|
37
41
|
htmlParts?: HtmlDiffPart[];
|
|
38
42
|
iframes?: string;
|
|
39
43
|
}
|
|
@@ -57,6 +61,7 @@ export declare class ActionResult implements ActionResultData {
|
|
|
57
61
|
url: string;
|
|
58
62
|
fullUrl: string | undefined;
|
|
59
63
|
browserLogs: any[];
|
|
64
|
+
networkRequests: NetworkCall[];
|
|
60
65
|
iframeSnapshots: Array<{
|
|
61
66
|
src: string;
|
|
62
67
|
html: string;
|
|
@@ -111,11 +116,13 @@ export declare class ActionResult implements ActionResultData {
|
|
|
111
116
|
getStateHash(): string;
|
|
112
117
|
diff(previousState: ActionResult | null): Promise<Diff>;
|
|
113
118
|
toToolResult(previousState: ActionResult | null, locator: string): Promise<ToolResultMetadata>;
|
|
119
|
+
consoleErrors(): string[];
|
|
114
120
|
}
|
|
115
121
|
export declare class Diff {
|
|
116
122
|
current: ActionResult;
|
|
117
123
|
previous: ActionResult | null;
|
|
118
124
|
_htmlDiffResult: HtmlDiffResult | null;
|
|
125
|
+
_messages: string[];
|
|
119
126
|
_ariaDiffResult: string | null;
|
|
120
127
|
_ariaChangeCount: number;
|
|
121
128
|
_isSameUrl: boolean;
|
|
@@ -128,8 +135,14 @@ export declare class Diff {
|
|
|
128
135
|
get ariaChanged(): string | null;
|
|
129
136
|
get ariaChangeCount(): number;
|
|
130
137
|
get htmlDiff(): HtmlDiffResult | null;
|
|
138
|
+
get messages(): string[];
|
|
131
139
|
calculate(): Promise<void>;
|
|
132
140
|
}
|
|
141
|
+
export interface NetworkCall {
|
|
142
|
+
method: string;
|
|
143
|
+
path: string;
|
|
144
|
+
status: number;
|
|
145
|
+
}
|
|
133
146
|
export interface FocusedElement {
|
|
134
147
|
role: string;
|
|
135
148
|
name: string;
|
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import { ConfigParser, outputPath } from "./config.js";
|
|
3
3
|
import { LARGE_ARIA_CHANGE_THRESHOLD, compactAriaSnapshot, diffAriaSnapshots } from "./utils/aria.js";
|
|
4
4
|
import { TTLCache } from "./utils/cache.js";
|
|
5
|
-
import { htmlDiff } from "./utils/html-diff.js";
|
|
5
|
+
import { htmlDiff, liveRegionMessages } from "./utils/html-diff.js";
|
|
6
6
|
import { extractHeadings, extractLinks, extractTargetedHtml, htmlCombinedSnapshot, htmlMinimalUISnapshot, htmlTextSnapshot, minifyHtml } from "./utils/html.js";
|
|
7
7
|
import { createDebug } from "./utils/logger.js";
|
|
8
8
|
import { slugify } from "./utils/strings.js";
|
|
@@ -21,6 +21,7 @@ export class ActionResult {
|
|
|
21
21
|
url = '';
|
|
22
22
|
fullUrl = undefined;
|
|
23
23
|
browserLogs = [];
|
|
24
|
+
networkRequests = [];
|
|
24
25
|
iframeSnapshots = [];
|
|
25
26
|
iframeURL = undefined;
|
|
26
27
|
screenshotFile = undefined;
|
|
@@ -45,6 +46,7 @@ export class ActionResult {
|
|
|
45
46
|
this.httpStatus = data.httpStatus;
|
|
46
47
|
this.error = data.error ?? null;
|
|
47
48
|
this.browserLogs = data.browserLogs ?? [];
|
|
49
|
+
this.networkRequests = data.networkRequests ?? [];
|
|
48
50
|
this.iframeSnapshots = data.iframeSnapshots ?? [];
|
|
49
51
|
this.iframeURL = data.iframeURL;
|
|
50
52
|
this.notes = data.notes ?? [];
|
|
@@ -415,20 +417,25 @@ export class ActionResult {
|
|
|
415
417
|
if (previousState?.id !== undefined && this.id === previousState.id) {
|
|
416
418
|
return result;
|
|
417
419
|
}
|
|
418
|
-
const urlChanged = previousState ? !this.isSameUrl({ url: previousState.url }) : true;
|
|
419
|
-
if (!previousState) {
|
|
420
|
-
result.pageDiff = {
|
|
421
|
-
urlChanged: true,
|
|
422
|
-
currentUrl: this.url,
|
|
423
|
-
};
|
|
424
|
-
return result;
|
|
425
|
-
}
|
|
426
|
-
const diff = await this.diff(previousState);
|
|
427
420
|
const pageDiff = {
|
|
428
|
-
urlChanged,
|
|
429
|
-
previousUrl: previousState.url,
|
|
421
|
+
urlChanged: previousState ? !this.isSameUrl({ url: previousState.url }) : true,
|
|
430
422
|
currentUrl: this.url,
|
|
431
423
|
};
|
|
424
|
+
result.pageDiff = pageDiff;
|
|
425
|
+
if (this.networkRequests.length > 0) {
|
|
426
|
+
pageDiff.requests = this.networkRequests;
|
|
427
|
+
}
|
|
428
|
+
const consoleErrors = this.consoleErrors();
|
|
429
|
+
if (consoleErrors.length > 0) {
|
|
430
|
+
pageDiff.consoleErrors = consoleErrors;
|
|
431
|
+
}
|
|
432
|
+
if (!previousState)
|
|
433
|
+
return result;
|
|
434
|
+
pageDiff.previousUrl = previousState.url;
|
|
435
|
+
const diff = await this.diff(previousState);
|
|
436
|
+
if (diff.messages.length > 0) {
|
|
437
|
+
pageDiff.messages = diff.messages;
|
|
438
|
+
}
|
|
432
439
|
if (diff.ariaChanged) {
|
|
433
440
|
pageDiff.ariaChanges = diff.ariaChanged;
|
|
434
441
|
pageDiff.ariaChangeCount = diff.ariaChangeCount;
|
|
@@ -452,10 +459,27 @@ export class ActionResult {
|
|
|
452
459
|
pageDiff.iframes = this.iframeSnapshots.map((snap) => `iframe src="${snap.src}":\n${snap.html}`).join('\n\n');
|
|
453
460
|
}
|
|
454
461
|
}
|
|
455
|
-
result.pageDiff = pageDiff;
|
|
456
462
|
return result;
|
|
457
463
|
}
|
|
464
|
+
consoleErrors() {
|
|
465
|
+
const errors = [];
|
|
466
|
+
for (const log of this.browserLogs) {
|
|
467
|
+
if ((log.type || log.level) !== 'error')
|
|
468
|
+
continue;
|
|
469
|
+
const text = String(log.text || log.message || log).trim();
|
|
470
|
+
if (!text)
|
|
471
|
+
continue;
|
|
472
|
+
if (errors.includes(text))
|
|
473
|
+
continue;
|
|
474
|
+
errors.push(text.slice(0, CONSOLE_ERROR_MAX_LENGTH));
|
|
475
|
+
if (errors.length === CONSOLE_ERROR_LIMIT)
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
return errors;
|
|
479
|
+
}
|
|
458
480
|
}
|
|
481
|
+
const CONSOLE_ERROR_MAX_LENGTH = 300;
|
|
482
|
+
const CONSOLE_ERROR_LIMIT = 3;
|
|
459
483
|
const HTML_PARTS_TOTAL_BUDGET = 8000;
|
|
460
484
|
const HTML_PARTS_COUNT_LIMIT = 8;
|
|
461
485
|
const HTML_PART_SUBTREE_BUDGET = 2000;
|
|
@@ -482,6 +506,7 @@ export class Diff {
|
|
|
482
506
|
current;
|
|
483
507
|
previous;
|
|
484
508
|
_htmlDiffResult = null;
|
|
509
|
+
_messages = [];
|
|
485
510
|
_ariaDiffResult = null;
|
|
486
511
|
_ariaChangeCount = 0;
|
|
487
512
|
_isSameUrl;
|
|
@@ -524,12 +549,18 @@ export class Diff {
|
|
|
524
549
|
get htmlDiff() {
|
|
525
550
|
return this._htmlDiffResult;
|
|
526
551
|
}
|
|
552
|
+
get messages() {
|
|
553
|
+
return this._messages;
|
|
554
|
+
}
|
|
527
555
|
async calculate() {
|
|
528
556
|
if (!this.previous)
|
|
529
557
|
return;
|
|
530
|
-
if (this._isSameUrl) {
|
|
531
|
-
this.
|
|
558
|
+
if (!this._isSameUrl) {
|
|
559
|
+
this._messages = liveRegionMessages(this.previous.html, this.current.html);
|
|
560
|
+
return;
|
|
532
561
|
}
|
|
562
|
+
this._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html, ConfigParser.getInstance().getConfig().html);
|
|
563
|
+
this._messages = this._htmlDiffResult.messages;
|
|
533
564
|
const ariaDiff = diffAriaSnapshots(this.previous.ariaSnapshot, this.current.ariaSnapshot);
|
|
534
565
|
this._ariaDiffResult = ariaDiff.text;
|
|
535
566
|
this._ariaChangeCount = ariaDiff.count;
|
package/dist/src/action.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionResult } from './action-result.js';
|
|
1
|
+
import { ActionResult, type NetworkCall } from './action-result.js';
|
|
2
2
|
import type { ExplorbotConfig } from './config.js';
|
|
3
3
|
import type { PlaywrightRecorder } from './playwright-recorder.js';
|
|
4
4
|
import type { StateManager } from './state-manager.js';
|
|
@@ -19,6 +19,8 @@ declare class Action {
|
|
|
19
19
|
recorder?: PlaywrightRecorder;
|
|
20
20
|
recovery: RecoveryRunner;
|
|
21
21
|
mainDocumentStatus: number | undefined;
|
|
22
|
+
networkRequests: NetworkCall[];
|
|
23
|
+
baseOrigin: string;
|
|
22
24
|
constructor(actor: CodeceptJS.I, stateManager: StateManager, recorder?: PlaywrightRecorder, recovery?: RecoveryRunner);
|
|
23
25
|
saveScreenshot(): Promise<string | undefined>;
|
|
24
26
|
capturePageState(opts?: {
|
|
@@ -31,7 +33,8 @@ declare class Action {
|
|
|
31
33
|
codeBlock?: string;
|
|
32
34
|
}): Promise<ActionResult>;
|
|
33
35
|
captureMainDocumentStatus(): Promise<number | undefined>;
|
|
34
|
-
|
|
36
|
+
captureResponses(): () => void;
|
|
37
|
+
recordNetworkCall(request: any, status: number): void;
|
|
35
38
|
/**
|
|
36
39
|
* Capture HTML snapshots of all iframes on the page
|
|
37
40
|
*/
|