explorbot 0.2.5 → 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/boat/prima/README.md +96 -0
- package/boat/prima/package.json +14 -10
- package/boat/prima/src/cli.ts +5 -0
- package/boat/prima/src/prima.ts +17 -4
- package/dist/boat/prima/src/cli.js +7 -0
- package/dist/boat/prima/src/prima.js +18 -4
- 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 +48 -17
- 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 +2 -1
- package/dist/src/ai/navigator.js +5 -9
- package/dist/src/ai/pilot.js +39 -22
- package/dist/src/ai/planner/subpages.js +2 -16
- package/dist/src/ai/planner.js +1 -1
- package/dist/src/ai/provider.js +16 -1
- package/dist/src/ai/researcher/cache.d.ts +8 -3
- package/dist/src/ai/researcher/cache.js +13 -8
- 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 +4 -3
- package/dist/src/ai/rules.js +1 -5
- package/dist/src/ai/tester.d.ts +1 -1
- package/dist/src/ai/tester.js +13 -22
- package/dist/src/ai/tools.d.ts +8 -5
- package/dist/src/ai/tools.js +79 -56
- package/dist/src/commands/init-command.js +13 -20
- package/dist/src/config.js +3 -1
- package/dist/src/experience-tracker.d.ts +2 -0
- package/dist/src/experience-tracker.js +12 -0
- package/dist/src/explorbot.js +1 -1
- package/dist/src/playwright-recorder.js +6 -12
- package/dist/src/test-plan.d.ts +8 -0
- package/dist/src/test-plan.js +11 -0
- package/dist/src/utils/html-diff.d.ts +5 -0
- package/dist/src/utils/html-diff.js +65 -6
- 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/models.json +4 -4
- package/package.json +6 -2
- package/src/action-result.ts +61 -16
- package/src/action.ts +51 -17
- 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 +6 -10
- package/src/ai/pilot.ts +41 -24
- package/src/ai/planner/subpages.ts +2 -13
- package/src/ai/planner.ts +1 -1
- package/src/ai/provider.ts +17 -1
- package/src/ai/researcher/cache.ts +17 -9
- package/src/ai/researcher/deep-analysis.ts +1 -1
- package/src/ai/researcher/fingerprint-worker.ts +23 -5
- package/src/ai/researcher.ts +4 -3
- package/src/ai/rules.ts +1 -5
- package/src/ai/tester.ts +13 -22
- package/src/ai/tools.ts +84 -60
- package/src/commands/init-command.ts +14 -20
- package/src/config.ts +2 -1
- package/src/experience-tracker.ts +13 -0
- package/src/explorbot.ts +1 -1
- package/src/playwright-recorder.ts +6 -11
- package/src/test-plan.ts +18 -0
- package/src/utils/html-diff.ts +72 -7
- package/src/utils/strings.ts +36 -0
- 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).
|
package/boat/prima/package.json
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "prima",
|
|
3
|
-
"version": "
|
|
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": {
|
|
7
|
-
|
|
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
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
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
|
}
|
package/boat/prima/src/cli.ts
CHANGED
|
@@ -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
|
|
package/boat/prima/src/prima.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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": "
|
|
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
|
*/
|