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.
Files changed (113) hide show
  1. package/bin/explorbot-cli.ts +19 -7
  2. package/boat/api-tester/src/cli.ts +17 -0
  3. package/boat/doc-collector/src/cli.ts +14 -1
  4. package/boat/prima/README.md +96 -0
  5. package/boat/prima/package.json +14 -10
  6. package/boat/prima/src/cli.ts +29 -12
  7. package/boat/prima/src/envelope.ts +35 -13
  8. package/boat/prima/src/prima.ts +78 -45
  9. package/dist/bin/explorbot-cli.js +19 -7
  10. package/dist/boat/api-tester/src/cli.js +17 -0
  11. package/dist/boat/doc-collector/src/cli.js +14 -1
  12. package/dist/boat/prima/src/cli.js +26 -7
  13. package/dist/boat/prima/src/envelope.js +32 -8
  14. package/dist/boat/prima/src/prima.js +75 -43
  15. package/dist/models.json +4 -4
  16. package/dist/package.json +6 -2
  17. package/dist/src/action-result.d.ts +13 -0
  18. package/dist/src/action-result.js +46 -15
  19. package/dist/src/action.d.ts +5 -2
  20. package/dist/src/action.js +53 -18
  21. package/dist/src/ai/captain/web-mode.js +1 -2
  22. package/dist/src/ai/captain.d.ts +20 -0
  23. package/dist/src/ai/captain.js +10 -1
  24. package/dist/src/ai/driller.js +6 -2
  25. package/dist/src/ai/fisherman-tools.d.ts +40 -1
  26. package/dist/src/ai/fisherman-tools.js +39 -0
  27. package/dist/src/ai/fisherman.js +2 -1
  28. package/dist/src/ai/navigator.d.ts +28 -0
  29. package/dist/src/ai/navigator.js +223 -175
  30. package/dist/src/ai/pilot.d.ts +7 -4
  31. package/dist/src/ai/pilot.js +89 -30
  32. package/dist/src/ai/planner/subpages.js +2 -16
  33. package/dist/src/ai/planner.js +1 -1
  34. package/dist/src/ai/provider.d.ts +2 -2
  35. package/dist/src/ai/provider.js +28 -22
  36. package/dist/src/ai/researcher/cache.d.ts +10 -3
  37. package/dist/src/ai/researcher/cache.js +23 -10
  38. package/dist/src/ai/researcher/deep-analysis.js +1 -1
  39. package/dist/src/ai/researcher/fingerprint-worker.js +21 -4
  40. package/dist/src/ai/researcher.js +6 -4
  41. package/dist/src/ai/rules.js +1 -5
  42. package/dist/src/ai/session-analyst.js +2 -0
  43. package/dist/src/ai/tester.d.ts +6 -3
  44. package/dist/src/ai/tester.js +30 -35
  45. package/dist/src/ai/tools.d.ts +8 -5
  46. package/dist/src/ai/tools.js +83 -57
  47. package/dist/src/commands/config-command.d.ts +51 -0
  48. package/dist/src/commands/config-command.js +117 -0
  49. package/dist/src/commands/index.js +2 -0
  50. package/dist/src/commands/init-command.js +13 -20
  51. package/dist/src/config.d.ts +8 -1
  52. package/dist/src/config.js +43 -1
  53. package/dist/src/experience-tracker.d.ts +2 -0
  54. package/dist/src/experience-tracker.js +12 -0
  55. package/dist/src/explorbot.js +5 -2
  56. package/dist/src/playwright-recorder.js +6 -12
  57. package/dist/src/remote.d.ts +3 -2
  58. package/dist/src/remote.js +8 -2
  59. package/dist/src/state-manager.d.ts +1 -1
  60. package/dist/src/state-manager.js +3 -1
  61. package/dist/src/test-plan.d.ts +9 -0
  62. package/dist/src/test-plan.js +30 -0
  63. package/dist/src/utils/html-diff.d.ts +5 -0
  64. package/dist/src/utils/html-diff.js +65 -6
  65. package/dist/src/utils/logger.d.ts +1 -1
  66. package/dist/src/utils/logger.js +8 -0
  67. package/dist/src/utils/strings.d.ts +2 -0
  68. package/dist/src/utils/strings.js +32 -0
  69. package/dist/src/utils/url-matcher.d.ts +1 -0
  70. package/dist/src/utils/url-matcher.js +31 -2
  71. package/docs/basics/getting-started.md +33 -10
  72. package/docs/basics/providers.md +6 -4
  73. package/docs/contributing/npm-package.md +73 -4
  74. package/docs/index.json +2 -1
  75. package/docs/reference/commands.md +3 -0
  76. package/docs/reference/websocket.md +50 -0
  77. package/docs/superpowers/specs/2026-08-18-prima-false-verdicts.md +159 -0
  78. package/models.json +4 -4
  79. package/package.json +6 -2
  80. package/src/action-result.ts +61 -16
  81. package/src/action.ts +56 -18
  82. package/src/ai/captain/web-mode.ts +1 -2
  83. package/src/ai/captain.ts +9 -1
  84. package/src/ai/driller.ts +6 -2
  85. package/src/ai/fisherman-tools.ts +35 -0
  86. package/src/ai/fisherman.ts +2 -1
  87. package/src/ai/navigator.ts +238 -179
  88. package/src/ai/pilot.ts +104 -36
  89. package/src/ai/planner/subpages.ts +2 -13
  90. package/src/ai/planner.ts +1 -1
  91. package/src/ai/provider.ts +29 -21
  92. package/src/ai/researcher/cache.ts +29 -11
  93. package/src/ai/researcher/deep-analysis.ts +1 -1
  94. package/src/ai/researcher/fingerprint-worker.ts +23 -5
  95. package/src/ai/researcher.ts +6 -4
  96. package/src/ai/rules.ts +1 -5
  97. package/src/ai/session-analyst.ts +2 -0
  98. package/src/ai/tester.ts +33 -34
  99. package/src/ai/tools.ts +88 -61
  100. package/src/commands/config-command.ts +146 -0
  101. package/src/commands/index.ts +2 -0
  102. package/src/commands/init-command.ts +14 -20
  103. package/src/config.ts +47 -2
  104. package/src/experience-tracker.ts +13 -0
  105. package/src/explorbot.ts +4 -2
  106. package/src/playwright-recorder.ts +6 -11
  107. package/src/remote.ts +8 -2
  108. package/src/state-manager.ts +5 -2
  109. package/src/test-plan.ts +38 -0
  110. package/src/utils/html-diff.ts +72 -7
  111. package/src/utils/logger.ts +9 -1
  112. package/src/utils/strings.ts +36 -0
  113. 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 { listSites } from "../../../src/global-config.js";
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: 'never reported — the run ended with this instruction still 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 (used.length && ledger.some((entry) => entry.status === 'open')) {
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: `unreported: ${entry.text}`, ok: false, proof: UNACCOUNTED.open }))];
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 (!used.length && unfinished.length === ledger.length) {
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 unmet = unfinished.map((entry) => `${entry.status}: ${entry.text}${entry.proof ? ` — ${entry.proof}` : ''}`);
234
- if (unmet.length) {
243
+ const blocked = ledger.filter((entry) => entry.status === 'blocked');
244
+ if (blocked.length) {
235
245
  envelope.ok = false;
236
- envelope.failure = { error: unmet.join('\n') };
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
- const invoked = await provider.invokeConversation(conversation, { completed: this.completedTool(), blocked: this.blockedTool() }, { maxToolRoundtrips: 2, toolChoice: 'required', agentName: AI_AGENT_NAME }).catch(() => null);
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
- const outcome = await tester.test(test);
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, { ok: outcome.success });
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
- return this.reportEnvelope(command, result, previousState, { assertions: verification.results || [] });
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 named = (model) => {
407
- if (typeof model === 'string')
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
- return toolResult.pageDiff?.ariaChanges || 'no change';
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), changes: compactAriaSnapshot(result.ariaSnapshot, true) }), 'utf-8');
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": "google/gemma-4-31b-it:nitro",
5
- "agenticModel": "google/gemma-4-31b-it:nitro"
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.4-nano",
17
- "visionModel": "gpt-5.4-nano",
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.2.4",
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._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html, ConfigParser.getInstance().getConfig().html);
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;
@@ -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
- captureMainDocumentResponse(): () => void;
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
  */