explorbot 0.2.5 → 0.3.1

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 (89) hide show
  1. package/boat/prima/README.md +96 -0
  2. package/boat/prima/package.json +14 -10
  3. package/boat/prima/src/cli.ts +5 -0
  4. package/boat/prima/src/prima.ts +17 -4
  5. package/dist/boat/prima/src/cli.js +7 -0
  6. package/dist/boat/prima/src/prima.js +18 -4
  7. package/dist/models.json +4 -4
  8. package/dist/package.json +6 -2
  9. package/dist/src/action-result.d.ts +13 -0
  10. package/dist/src/action-result.js +46 -15
  11. package/dist/src/action.d.ts +5 -2
  12. package/dist/src/action.js +48 -17
  13. package/dist/src/ai/captain/web-mode.js +1 -2
  14. package/dist/src/ai/captain.d.ts +20 -0
  15. package/dist/src/ai/captain.js +10 -1
  16. package/dist/src/ai/conversation.d.ts +1 -0
  17. package/dist/src/ai/conversation.js +3 -0
  18. package/dist/src/ai/driller.js +6 -2
  19. package/dist/src/ai/fisherman-tools.d.ts +40 -1
  20. package/dist/src/ai/fisherman-tools.js +39 -0
  21. package/dist/src/ai/fisherman.js +3 -2
  22. package/dist/src/ai/navigator.d.ts +2 -1
  23. package/dist/src/ai/navigator.js +5 -9
  24. package/dist/src/ai/pilot.js +51 -29
  25. package/dist/src/ai/planner/subpages.js +2 -16
  26. package/dist/src/ai/planner.js +1 -1
  27. package/dist/src/ai/provider.d.ts +3 -0
  28. package/dist/src/ai/provider.js +80 -17
  29. package/dist/src/ai/researcher/cache.d.ts +8 -3
  30. package/dist/src/ai/researcher/cache.js +13 -8
  31. package/dist/src/ai/researcher/deep-analysis.js +1 -1
  32. package/dist/src/ai/researcher/fingerprint-worker.js +21 -4
  33. package/dist/src/ai/researcher.js +4 -3
  34. package/dist/src/ai/rules.js +1 -5
  35. package/dist/src/ai/tester.d.ts +1 -1
  36. package/dist/src/ai/tester.js +34 -26
  37. package/dist/src/ai/tools.d.ts +8 -5
  38. package/dist/src/ai/tools.js +79 -56
  39. package/dist/src/commands/explore-command.js +22 -17
  40. package/dist/src/commands/init-command.js +13 -20
  41. package/dist/src/config.d.ts +2 -1
  42. package/dist/src/config.js +3 -1
  43. package/dist/src/experience-tracker.d.ts +2 -0
  44. package/dist/src/experience-tracker.js +12 -0
  45. package/dist/src/explorbot.js +1 -1
  46. package/dist/src/playwright-recorder.js +6 -12
  47. package/dist/src/test-plan.d.ts +8 -0
  48. package/dist/src/test-plan.js +11 -0
  49. package/dist/src/utils/html-diff.d.ts +5 -0
  50. package/dist/src/utils/html-diff.js +65 -6
  51. package/dist/src/utils/strings.d.ts +2 -0
  52. package/dist/src/utils/strings.js +32 -0
  53. package/dist/src/utils/url-matcher.d.ts +1 -0
  54. package/dist/src/utils/url-matcher.js +31 -2
  55. package/docs/basics/getting-started.md +33 -10
  56. package/docs/basics/providers.md +6 -4
  57. package/docs/contributing/npm-package.md +73 -4
  58. package/models.json +4 -4
  59. package/package.json +6 -2
  60. package/src/action-result.ts +61 -16
  61. package/src/action.ts +51 -17
  62. package/src/ai/captain/web-mode.ts +1 -2
  63. package/src/ai/captain.ts +9 -1
  64. package/src/ai/conversation.ts +3 -0
  65. package/src/ai/driller.ts +6 -2
  66. package/src/ai/fisherman-tools.ts +35 -0
  67. package/src/ai/fisherman.ts +3 -2
  68. package/src/ai/navigator.ts +6 -10
  69. package/src/ai/pilot.ts +54 -32
  70. package/src/ai/planner/subpages.ts +2 -13
  71. package/src/ai/planner.ts +1 -1
  72. package/src/ai/provider.ts +111 -41
  73. package/src/ai/researcher/cache.ts +17 -9
  74. package/src/ai/researcher/deep-analysis.ts +1 -1
  75. package/src/ai/researcher/fingerprint-worker.ts +23 -5
  76. package/src/ai/researcher.ts +4 -3
  77. package/src/ai/rules.ts +1 -5
  78. package/src/ai/tester.ts +32 -27
  79. package/src/ai/tools.ts +84 -60
  80. package/src/commands/explore-command.ts +17 -14
  81. package/src/commands/init-command.ts +14 -20
  82. package/src/config.ts +4 -2
  83. package/src/experience-tracker.ts +13 -0
  84. package/src/explorbot.ts +1 -1
  85. package/src/playwright-recorder.ts +6 -11
  86. package/src/test-plan.ts +18 -0
  87. package/src/utils/html-diff.ts +72 -7
  88. package/src/utils/strings.ts +36 -0
  89. package/src/utils/url-matcher.ts +27 -2
@@ -0,0 +1,96 @@
1
+ # prima-cli
2
+
3
+ Prima is a high-level AI browser driver. You describe the behaviour you want checked, and it works out how to reach it — instead of writing selectors and step-by-step click paths.
4
+
5
+ It drives a browser opened by [playwright-cli](https://www.npmjs.com/package/playwright-cli), or one of its own.
6
+
7
+ ```bash
8
+ npx prima-cli check "a workflow can be created and appears in the list" --expected "the new workflow is listed"
9
+ ```
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npx prima-cli --help # no install
15
+ npm install -g prima-cli # or install it
16
+ ```
17
+
18
+ Prima also ships inside [explorbot](https://www.npmjs.com/package/explorbot), so `npx explorbot prima <command>` runs the same tool if you already have it.
19
+
20
+ Requires Node.js 24+. Playwright browsers come from `npx playwright install chromium`.
21
+
22
+ ## Session
23
+
24
+ Prima needs a browser to drive. Either attach to a `playwright-cli` session:
25
+
26
+ ```bash
27
+ playwright-cli open https://app.example.com
28
+ prima-cli check "the settings page saves a changed theme"
29
+ playwright-cli close
30
+ ```
31
+
32
+ Or let prima own the browser:
33
+
34
+ ```bash
35
+ prima-cli browser start --url https://app.example.com
36
+ prima-cli do "open the account menu" "choose the settings entry"
37
+ prima-cli browser stop
38
+ ```
39
+
40
+ ## Commands
41
+
42
+ | Command | What it does |
43
+ | --- | --- |
44
+ | `check <scenario>` | Run a scenario end to end as a test, verify it, and report the steps it took |
45
+ | `do <instructions...>` | Run high-level instructions tester-style, one argument per instruction |
46
+ | `verify <assertion>` | Assert a statement about the current page |
47
+ | `ask <question>` | Answer a question about the current page |
48
+ | `research` | Map the current page and return verified locators |
49
+ | `go <target>` | Navigate to a url, a path, or a page described in plain words |
50
+ | `pw <fn>` | Run a Playwright function expression against the open page |
51
+ | `status <hash>` | Show the artifacts and page detail recorded for an earlier command |
52
+ | `report` | Turn every command of a session into one html and markdown report |
53
+ | `browser start\|stop\|status\|list` | Manage the browsers prima drives |
54
+ | `config` | Show models, config file and paths used by this run |
55
+
56
+ `check` takes an outcome rather than a click path. It runs on the page you are already on and never reloads it, so an open dialog survives the check. Each `--expected` outcome comes back as PASSED, FAILED, CONTRADICTION or not verified — settled against a screenshot of the whole page, because what a user can see is the proof.
57
+
58
+ `do` accounts for every instruction you gave: each is reported as ok, FAIL or ??. Nothing runs past the last instruction.
59
+
60
+ ## AI model
61
+
62
+ Prima needs a model, and takes it from the environment. There is no `init` to run and nothing is written for you.
63
+
64
+ ```bash
65
+ export PRIMA_CLI_AI_MODEL=openrouter/openai/gpt-oss-120b
66
+ export OPENROUTER_API_KEY=your-key
67
+ ```
68
+
69
+ The provider comes from the model name, so that is the whole setup. Fish uses `set -gx` instead of `export`. Since prima is usually run by a coding agent, the better home is the agent's own config — in `~/.claude/settings.json`, an `env` block reaches every command the agent runs:
70
+
71
+ ```json
72
+ {
73
+ "env": {
74
+ "PRIMA_CLI_AI_MODEL": "openrouter/openai/gpt-oss-120b",
75
+ "OPENROUTER_API_KEY": "your-key"
76
+ }
77
+ }
78
+ ```
79
+
80
+ Screenshot analysis needs its own model — one is never guessed from the main model. Set `PRIMA_CLI_VISION_MODEL`, or pass `--vision-model` for a single run, as `--model` does for the main one. Without it prima still runs, but settles `check` outcomes from the run log rather than from the page as seen.
81
+
82
+ Every `PRIMA_CLI_*` variable mirrors the `EXPLORBOT_*` one of the same name and wins over it, so prima can be pointed at a different provider, model or URL without disturbing an explorbot setup on the same machine. `PRIMA_CLI_AI_MODEL`, `PRIMA_CLI_URL`, `PRIMA_CLI_VISION_MODEL` and the rest all work this way.
83
+
84
+ Where [explorbot](https://www.npmjs.com/package/explorbot) is already configured, prima reads its config too — a project's `explorbot.config.js` first, then `~/.explorbot/config.js`. Variables win over both. `prima config` prints what a directory resolves to, and `--ephemeral` keeps no state between runs, writing output to a temp directory.
85
+
86
+ Providers: openai, anthropic, google, groq, mistral, openrouter, sambanova.
87
+
88
+ Without a model `pw` still works, since it runs Playwright directly.
89
+
90
+ ## Agents
91
+
92
+ Prima is built to be called by a coding agent: one call takes a whole job, and the reply is a compact report rather than a browser transcript. Point your agent at `prima-cli --help` and it will find its way.
93
+
94
+ ## License
95
+
96
+ Elastic-2.0. Part of [explorbot](https://github.com/testomatio/explorbot).
@@ -1,16 +1,20 @@
1
1
  {
2
- "name": "prima",
3
- "version": "1.0.0",
2
+ "name": "prima-cli",
3
+ "version": "0.0.0",
4
4
  "description": "High-level browser driver CLI for orchestrating agents",
5
+ "license": "Elastic-2.0",
5
6
  "type": "module",
6
- "bin": { "prima": "./bin/prima-cli.ts" },
7
- "scripts": {
8
- "format": "biome format --write .",
9
- "lint:fix": "biome lint --write .",
10
- "check:fix": "biome check --write ."
7
+ "bin": {
8
+ "prima-cli": "./dist/boat/prima/bin/prima-cli.js"
11
9
  },
12
- "dependencies": {
13
- "commander": "^14.0.1",
14
- "dedent": "^1.6.0"
10
+ "files": ["dist/"],
11
+ "keywords": ["cli", "playwright", "browser", "ai", "agent", "testing"],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/testomatio/explorbot",
15
+ "directory": "boat/prima"
16
+ },
17
+ "engines": {
18
+ "node": ">=24.0.0"
15
19
  }
16
20
  }
@@ -104,6 +104,8 @@ function addCommonOptions(cmd: Command): Command {
104
104
  .option('-p, --path <path>', 'Working directory path')
105
105
  .option('-i, --instance <name>', 'Browser instance to drive')
106
106
  .option('--session [file]', 'Persist cookies and storage to a session file')
107
+ .option('--model <model>', 'Main model, as provider/model-id')
108
+ .option('--vision-model <model>', 'Model for screenshot analysis, as provider/model-id')
107
109
  .option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
108
110
  .option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
109
111
  .option('--url <url>', 'Page to open when the session has no page yet')
@@ -113,7 +115,10 @@ function addCommonOptions(cmd: Command): Command {
113
115
  }
114
116
 
115
117
  function primaFor(options: any): Prima {
118
+ Prima.applyEnv();
116
119
  if (options.ephemeral) process.env.EXPLORBOT_EPHEMERAL = '1';
120
+ if (options.model) process.env.EXPLORBOT_AI_MODEL = options.model;
121
+ if (options.visionModel) process.env.EXPLORBOT_VISION_MODEL = options.visionModel;
117
122
  return new Prima(buildOptions(options));
118
123
  }
119
124
 
@@ -10,10 +10,10 @@ import { z } from 'zod';
10
10
  import { ActionResult } from '../../../src/action-result.ts';
11
11
  import { getPreviousResearch } from '../../../src/ai/researcher/cache.ts';
12
12
  import { actionRule, locatorRule } from '../../../src/ai/rules.ts';
13
- import { createAgentTools, createCodeceptJSTools } from '../../../src/ai/tools.ts';
13
+ import { createAgentTools, createCodeceptJSTools, createRefTools } from '../../../src/ai/tools.ts';
14
14
  import { getAliveEndpoint, launchServer, listInstances, stopServer } from '../../../src/browser-server.ts';
15
15
  import { ConfigCommand } from '../../../src/commands/config-command.ts';
16
- import { ConfigMissingError, ConfigParser, type ExplorbotConfig, outputPath } from '../../../src/config.ts';
16
+ import { ConfigMissingError, ConfigParser, EXPLORBOT_ENV_VARS, type ExplorbotConfig, outputPath } from '../../../src/config.ts';
17
17
  import { ExplorBot } from '../../../src/explorbot.ts';
18
18
  import { listSites } from '../../../src/global-config.ts';
19
19
  import { Reporter } from '../../../src/reporter.ts';
@@ -90,6 +90,14 @@ export class Prima {
90
90
  });
91
91
  }
92
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
+
93
101
  async start(): Promise<void> {
94
102
  let discovery: Discovery | undefined;
95
103
  if (!this.options.endpoint) {
@@ -148,7 +156,7 @@ export class Prima {
148
156
  const deps = { explorer: this.bot.getExplorer(), stateManager: this.bot.stateManager(), ai: provider };
149
157
  const ledger: LedgerEntry[] = instructions.map((text) => ({ text, status: 'open', proof: '' }));
150
158
  const descent = { markup: false };
151
- 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() };
152
160
  conversation.addUserText(await this.instructionPrompt(instructions, await this.capturedResult(previousState)));
153
161
 
154
162
  const used: string[] = [];
@@ -1043,7 +1051,12 @@ export class Prima {
1043
1051
  private async pageChanges(result: ActionResult, previousState: WebPageState | null, code: string): Promise<string> {
1044
1052
  if (!previousState) return 'no snapshot was captured before this command, so nothing could be compared';
1045
1053
  const toolResult = await result.toToolResult(ActionResult.fromState(previousState), code);
1046
- return toolResult.pageDiff?.ariaChanges || 'no change';
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');
1047
1060
  }
1048
1061
 
1049
1062
  async status(hash: string): Promise<EnvelopeData> {
@@ -95,6 +95,8 @@ function addCommonOptions(cmd) {
95
95
  .option('-p, --path <path>', 'Working directory path')
96
96
  .option('-i, --instance <name>', 'Browser instance to drive')
97
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')
98
100
  .option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
99
101
  .option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
100
102
  .option('--url <url>', 'Page to open when the session has no page yet')
@@ -103,8 +105,13 @@ function addCommonOptions(cmd) {
103
105
  .addHelpText('after', `\n${sessionHelp}`);
104
106
  }
105
107
  function primaFor(options) {
108
+ Prima.applyEnv();
106
109
  if (options.ephemeral)
107
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;
108
115
  return new Prima(buildOptions(options));
109
116
  }
110
117
  async function runPrima(options, command, run, record = true) {
@@ -9,10 +9,10 @@ import { z } from 'zod';
9
9
  import { ActionResult } from "../../../src/action-result.js";
10
10
  import { getPreviousResearch } from "../../../src/ai/researcher/cache.js";
11
11
  import { actionRule, locatorRule } from "../../../src/ai/rules.js";
12
- import { createAgentTools, createCodeceptJSTools } from "../../../src/ai/tools.js";
12
+ import { createAgentTools, createCodeceptJSTools, createRefTools } from "../../../src/ai/tools.js";
13
13
  import { getAliveEndpoint, launchServer, listInstances, stopServer } from "../../../src/browser-server.js";
14
14
  import { ConfigCommand } from "../../../src/commands/config-command.js";
15
- import { ConfigMissingError, ConfigParser, outputPath } from "../../../src/config.js";
15
+ import { ConfigMissingError, ConfigParser, EXPLORBOT_ENV_VARS, outputPath } from "../../../src/config.js";
16
16
  import { ExplorBot } from "../../../src/explorbot.js";
17
17
  import { listSites } from "../../../src/global-config.js";
18
18
  import { Reporter } from "../../../src/reporter.js";
@@ -82,6 +82,14 @@ export class Prima {
82
82
  reporter: { enabled: false },
83
83
  });
84
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
+ }
85
93
  async start() {
86
94
  let discovery;
87
95
  if (!this.options.endpoint) {
@@ -136,7 +144,7 @@ export class Prima {
136
144
  const deps = { explorer: this.bot.getExplorer(), stateManager: this.bot.stateManager(), ai: provider };
137
145
  const ledger = instructions.map((text) => ({ text, status: 'open', proof: '' }));
138
146
  const descent = { markup: false };
139
- 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() };
140
148
  conversation.addUserText(await this.instructionPrompt(instructions, await this.capturedResult(previousState)));
141
149
  const used = [];
142
150
  const trace = [];
@@ -983,7 +991,13 @@ export class Prima {
983
991
  if (!previousState)
984
992
  return 'no snapshot was captured before this command, so nothing could be compared';
985
993
  const toolResult = await result.toToolResult(ActionResult.fromState(previousState), code);
986
- 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');
987
1001
  }
988
1002
  async status(hash) {
989
1003
  const dir = this.statusDir(hash);
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.5",
3
+ "version": "0.3.1",
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
  */