explorbot 0.3.5 → 0.4.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.
- package/bin/explorbot-cli.ts +23 -16
- package/boat/api-tester/src/ai/chief.ts +7 -1
- package/boat/api-tester/src/ai/curler.ts +7 -1
- package/boat/api-tester/src/apibot.ts +10 -4
- package/boat/api-tester/src/cli.ts +12 -2
- package/boat/api-tester/src/config.ts +28 -8
- package/boat/doc-collector/bin/doc-collector-cli.ts +3 -2
- package/boat/doc-collector/src/cli.ts +6 -1
- package/boat/doc-collector/src/docbot.ts +4 -2
- package/boat/prima/bin/prima-cli.ts +2 -0
- package/boat/prima/src/cli.ts +23 -8
- package/boat/prima/src/envelope.ts +35 -9
- package/boat/prima/src/prima.ts +23 -10
- package/dist/bin/explorbot-cli.js +24 -16
- package/dist/boat/api-tester/bin/apibot-cli.js +3 -2
- package/dist/boat/api-tester/src/ai/chief.js +6 -1
- package/dist/boat/api-tester/src/ai/curler.js +6 -1
- package/dist/boat/api-tester/src/apibot.js +7 -3
- package/dist/boat/api-tester/src/cli.js +12 -2
- package/dist/boat/api-tester/src/config.js +31 -8
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +3 -2
- package/dist/boat/doc-collector/src/cli.js +6 -1
- package/dist/boat/doc-collector/src/docbot.js +4 -2
- package/dist/boat/prima/bin/prima-cli.js +2 -0
- package/dist/boat/prima/src/cli.js +22 -8
- package/dist/boat/prima/src/envelope.js +24 -6
- package/dist/boat/prima/src/prima.js +23 -11
- package/dist/package.json +2 -2
- package/dist/rules/planner/styles/normal.md +1 -1
- package/dist/src/action-result.d.ts +9 -1
- package/dist/src/action-result.js +57 -18
- package/dist/src/action.d.ts +1 -1
- package/dist/src/action.js +87 -12
- package/dist/src/ai/captain.js +1 -1
- package/dist/src/ai/driller.d.ts +0 -1
- package/dist/src/ai/driller.js +8 -20
- package/dist/src/ai/fisherman-tools.d.ts +9 -0
- package/dist/src/ai/fisherman-tools.js +52 -6
- package/dist/src/ai/fisherman.d.ts +4 -2
- package/dist/src/ai/fisherman.js +48 -27
- package/dist/src/ai/historian/codeceptjs.js +1 -1
- package/dist/src/ai/historian/playwright.js +1 -1
- package/dist/src/ai/navigator.js +1 -1
- package/dist/src/ai/pilot.d.ts +1 -0
- package/dist/src/ai/pilot.js +13 -1
- package/dist/src/ai/planner.js +9 -7
- package/dist/src/ai/provider.js +20 -3
- package/dist/src/ai/researcher/deep-analysis.js +1 -3
- package/dist/src/ai/researcher.js +5 -3
- package/dist/src/ai/rules.js +3 -3
- package/dist/src/ai/tester.d.ts +3 -0
- package/dist/src/ai/tester.js +40 -3
- package/dist/src/ai/tools.d.ts +1 -0
- package/dist/src/ai/tools.js +13 -6
- package/dist/src/api/request-result.d.ts +2 -0
- package/dist/src/api/request-result.js +8 -2
- package/dist/src/api/request-store.d.ts +3 -2
- package/dist/src/api/request-store.js +66 -14
- package/dist/src/api/spec-reader.js +1 -1
- package/dist/src/commands/config-command.js +1 -1
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/explore-command.d.ts +6 -0
- package/dist/src/commands/explore-command.js +39 -3
- package/dist/src/commands/freesail-command.js +10 -1
- package/dist/src/commands/options/base-option.d.ts +8 -0
- package/dist/src/commands/options/base-option.js +12 -0
- package/dist/src/commands/options/index.d.ts +5 -0
- package/dist/src/commands/options/index.js +5 -0
- package/dist/src/commands/options/knowledge-option.d.ts +7 -0
- package/dist/src/commands/options/knowledge-option.js +12 -0
- package/dist/src/commands/options/ws-option.d.ts +7 -0
- package/dist/src/commands/options/ws-option.js +21 -0
- package/dist/src/commands/plans-command.js +6 -6
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +12 -0
- package/dist/src/experience-tracker.js +5 -0
- package/dist/src/explorbot.d.ts +0 -1
- package/dist/src/explorbot.js +24 -37
- package/dist/src/knowledge-tracker.d.ts +20 -7
- package/dist/src/knowledge-tracker.js +69 -31
- package/dist/src/remote.d.ts +0 -3
- package/dist/src/remote.js +0 -18
- package/dist/src/state-manager.d.ts +5 -1
- package/dist/src/state-manager.js +10 -7
- package/dist/src/test-plan.d.ts +3 -0
- package/dist/src/test-plan.js +27 -0
- package/dist/src/utils/aria.d.ts +1 -1
- package/dist/src/utils/aria.js +6 -42
- package/dist/src/utils/html-diff.d.ts +4 -0
- package/dist/src/utils/html-diff.js +62 -7
- package/dist/src/utils/html.d.ts +5 -15
- package/dist/src/utils/html.js +14 -85
- package/dist/src/utils/overlay.d.ts +56 -11
- package/dist/src/utils/overlay.js +191 -21
- package/dist/src/utils/request-map.d.ts +7 -0
- package/dist/src/utils/request-map.js +16 -0
- package/dist/src/utils/url-matcher.js +4 -2
- package/docs/api-testing/basics.md +15 -0
- package/docs/api-testing/planning.md +10 -1
- package/docs/reference/commands.md +32 -5
- package/docs/reference/websocket.md +1 -0
- package/docs/superpowers/plans/2026-08-29-fisherman-reliability.md +953 -0
- package/docs/superpowers/plans/2026-08-29-region-states.md +1292 -0
- package/docs/superpowers/plans/2026-08-30-fisherman-live-session-auth.md +457 -0
- package/docs/superpowers/specs/2026-08-29-fisherman-reliability-design.md +45 -0
- package/docs/superpowers/specs/2026-08-29-region-states-design.md +262 -0
- package/docs/superpowers/specs/2026-08-29-region-states-fixes-design.md +269 -0
- package/docs/superpowers/specs/2026-08-30-fisherman-live-session-auth-design.md +37 -0
- package/docs/workflow/agentic-usage.md +12 -2
- package/docs/workflow/ci.md +1 -0
- package/docs/workflow/knowledge.md +46 -2
- package/package.json +2 -2
- package/rules/planner/styles/normal.md +1 -1
- package/src/action-result.ts +61 -22
- package/src/action.ts +87 -14
- package/src/ai/captain.ts +1 -1
- package/src/ai/driller.ts +7 -39
- package/src/ai/fisherman-tools.ts +56 -7
- package/src/ai/fisherman.ts +48 -28
- package/src/ai/historian/codeceptjs.ts +1 -1
- package/src/ai/historian/playwright.ts +1 -1
- package/src/ai/navigator.ts +1 -1
- package/src/ai/pilot.ts +9 -1
- package/src/ai/planner.ts +9 -8
- package/src/ai/provider.ts +21 -3
- package/src/ai/researcher/deep-analysis.ts +1 -2
- package/src/ai/researcher.ts +4 -3
- package/src/ai/rules.ts +3 -3
- package/src/ai/tester.ts +40 -3
- package/src/ai/tools.ts +17 -9
- package/src/api/request-result.ts +10 -2
- package/src/api/request-store.ts +60 -13
- package/src/api/spec-reader.ts +1 -1
- package/src/commands/config-command.ts +1 -1
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/explore-command.ts +37 -3
- package/src/commands/freesail-command.ts +7 -1
- package/src/commands/options/base-option.ts +18 -0
- package/src/commands/options/index.ts +7 -0
- package/src/commands/options/knowledge-option.ts +14 -0
- package/src/commands/options/ws-option.ts +24 -0
- package/src/commands/plans-command.ts +6 -6
- package/src/config.ts +12 -0
- package/src/experience-tracker.ts +5 -1
- package/src/explorbot.ts +21 -37
- package/src/knowledge-tracker.ts +94 -36
- package/src/remote.ts +0 -20
- package/src/state-manager.ts +13 -7
- package/src/test-plan.ts +29 -0
- package/src/utils/aria.ts +7 -44
- package/src/utils/html-diff.ts +62 -7
- package/src/utils/html.ts +14 -91
- package/src/utils/overlay.ts +226 -23
- package/src/utils/request-map.ts +19 -0
- package/src/utils/url-matcher.ts +3 -2
|
@@ -29,9 +29,9 @@ const checkHelp = dedent `
|
|
|
29
29
|
as false.
|
|
30
30
|
Outcomes are settled against a screenshot of the whole page: what a user can see is
|
|
31
31
|
the proof, and the run log only says what was done. CONTRADICTION means the two
|
|
32
|
-
disagree - reported with both sides rather than settled one way,
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
disagree - reported with both sides rather than settled one way, so read the html,
|
|
33
|
+
aria and screenshot named under ### Artifacts and judge it yourself. Not finding
|
|
34
|
+
something in the picture is not enough on its own; that is "not verified".
|
|
35
35
|
ok: follows those outcomes - false when one FAILED or CONTRADICTED, or when the run
|
|
36
36
|
could not complete, which is reported as such rather than as an app failure.
|
|
37
37
|
Page problems seen on the way appear under ### Answer, not as step failures.
|
|
@@ -48,6 +48,12 @@ const verifyHelp = dedent `
|
|
|
48
48
|
and gives no overall verdict - read the lines and decide. "none ran" means the claim
|
|
49
49
|
could not be expressed, which is not the same as false.
|
|
50
50
|
`;
|
|
51
|
+
const statusHelp = dedent `
|
|
52
|
+
Reads the files a command recorded, so it needs no browser and outlives the session.
|
|
53
|
+
The hash is looked up across every recorded site. ### Artifacts names every file kept
|
|
54
|
+
under it: the aria tree, the html, the screenshot and network log when they were
|
|
55
|
+
captured, and the per-step captures of a do run.
|
|
56
|
+
`;
|
|
51
57
|
const reportHelp = dedent `
|
|
52
58
|
Commands are logged as they run, so the report needs no browser and outlives the session.
|
|
53
59
|
The most recent session is reported unless --pw-session names another.
|
|
@@ -100,6 +106,7 @@ function addCommonOptions(cmd) {
|
|
|
100
106
|
.option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
|
|
101
107
|
.option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
|
|
102
108
|
.option('--url <url>', 'Page to open when the session has no page yet')
|
|
109
|
+
.option('--spec <path>', 'Docbot application spec directory or index.md to read as page knowledge')
|
|
103
110
|
.option('--endpoint <ep>', 'Websocket endpoint of a browser server to attach to, skipping discovery')
|
|
104
111
|
.option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
|
|
105
112
|
.addHelpText('after', `\n${sessionHelp}`);
|
|
@@ -112,9 +119,11 @@ function primaFor(options) {
|
|
|
112
119
|
process.env.EXPLORBOT_AI_MODEL = options.model;
|
|
113
120
|
if (options.visionModel)
|
|
114
121
|
process.env.EXPLORBOT_VISION_MODEL = options.visionModel;
|
|
122
|
+
if (options.spec)
|
|
123
|
+
process.env.EXPLORBOT_SPEC = options.spec;
|
|
115
124
|
return new Prima(buildOptions(options));
|
|
116
125
|
}
|
|
117
|
-
async function runPrima(options, command, run
|
|
126
|
+
async function runPrima(options, command, run) {
|
|
118
127
|
setQuietMode(!isVerboseMode());
|
|
119
128
|
trackActivityLine();
|
|
120
129
|
const prima = primaFor(options);
|
|
@@ -127,8 +136,7 @@ async function runPrima(options, command, run, record = true) {
|
|
|
127
136
|
catch (error) {
|
|
128
137
|
envelope = await prima.toolFailureEnvelope(command, error);
|
|
129
138
|
}
|
|
130
|
-
|
|
131
|
-
prima.record(envelope, Date.now() - startedAt);
|
|
139
|
+
prima.record(envelope, Date.now() - startedAt);
|
|
132
140
|
clearActivityLine();
|
|
133
141
|
console.log(renderEnvelope(envelope));
|
|
134
142
|
await prima.stop().catch(() => { });
|
|
@@ -191,8 +199,14 @@ export function createPrimaCommands(name = 'prima') {
|
|
|
191
199
|
await prima.stop().catch(() => { });
|
|
192
200
|
process.exit(0);
|
|
193
201
|
});
|
|
194
|
-
addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command'))
|
|
195
|
-
|
|
202
|
+
addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command'))
|
|
203
|
+
.addHelpText('after', `\n${statusHelp}`)
|
|
204
|
+
.action(async (hash, options) => {
|
|
205
|
+
setQuietMode(!isVerboseMode());
|
|
206
|
+
const prima = primaFor(options);
|
|
207
|
+
const envelope = await prima.status(hash).catch((error) => prima.toolFailureEnvelope(`status ${hash}`, error));
|
|
208
|
+
console.log(renderEnvelope(envelope));
|
|
209
|
+
process.exit(envelope.ok ? 0 : 1);
|
|
196
210
|
});
|
|
197
211
|
addCommonOptions(cmd.command('report').description('Turn every command of a session into one html and markdown report'))
|
|
198
212
|
.addHelpText('after', `\n${reportHelp}`)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
export const STATUS_FILE = 'status.json';
|
|
4
|
+
const ARTIFACT_FILES = { aria: 'aria.yml', html: 'page.html', screenshot: 'page.png', network: 'network.jsonl' };
|
|
3
5
|
const EXPECTATION_LABELS = {
|
|
4
6
|
passed: 'PASSED ',
|
|
5
7
|
failed: 'FAILED ',
|
|
@@ -13,21 +15,35 @@ export function renderEnvelope(data) {
|
|
|
13
15
|
export function writeArtifacts(dir, snapshot) {
|
|
14
16
|
mkdirSync(dir, { recursive: true });
|
|
15
17
|
const paths = {
|
|
16
|
-
aria: path.resolve(dir,
|
|
17
|
-
html: path.resolve(dir,
|
|
18
|
+
aria: path.resolve(dir, ARTIFACT_FILES.aria),
|
|
19
|
+
html: path.resolve(dir, ARTIFACT_FILES.html),
|
|
18
20
|
};
|
|
19
21
|
writeFileSync(paths.aria, snapshot.aria ?? '', 'utf-8');
|
|
20
22
|
writeFileSync(paths.html, snapshot.html ?? '', 'utf-8');
|
|
21
23
|
if (snapshot.screenshot) {
|
|
22
|
-
paths.screenshot = path.resolve(dir,
|
|
24
|
+
paths.screenshot = path.resolve(dir, ARTIFACT_FILES.screenshot);
|
|
23
25
|
writeFileSync(paths.screenshot, snapshot.screenshot);
|
|
24
26
|
}
|
|
25
27
|
if (!snapshot.requests.length)
|
|
26
28
|
return paths;
|
|
27
|
-
paths.network = path.resolve(dir,
|
|
29
|
+
paths.network = path.resolve(dir, ARTIFACT_FILES.network);
|
|
28
30
|
writeFileSync(paths.network, snapshot.requests.map((request) => `${JSON.stringify(request)}\n`).join(''), 'utf-8');
|
|
29
31
|
return paths;
|
|
30
32
|
}
|
|
33
|
+
export function readArtifacts(dir) {
|
|
34
|
+
const paths = { aria: path.resolve(dir, ARTIFACT_FILES.aria), html: path.resolve(dir, ARTIFACT_FILES.html) };
|
|
35
|
+
if (existsSync(path.resolve(dir, ARTIFACT_FILES.screenshot)))
|
|
36
|
+
paths.screenshot = path.resolve(dir, ARTIFACT_FILES.screenshot);
|
|
37
|
+
if (existsSync(path.resolve(dir, ARTIFACT_FILES.network)))
|
|
38
|
+
paths.network = path.resolve(dir, ARTIFACT_FILES.network);
|
|
39
|
+
const recorded = [...Object.values(ARTIFACT_FILES), STATUS_FILE];
|
|
40
|
+
const files = readdirSync(dir)
|
|
41
|
+
.filter((entry) => !recorded.includes(entry))
|
|
42
|
+
.sort();
|
|
43
|
+
if (files.length)
|
|
44
|
+
paths.files = files;
|
|
45
|
+
return paths;
|
|
46
|
+
}
|
|
31
47
|
function renderResult(data) {
|
|
32
48
|
const lines = [`ok: ${data.ok}`, `command: ${data.command}`];
|
|
33
49
|
if (data.used?.length)
|
|
@@ -70,7 +86,7 @@ function renderSteps(data) {
|
|
|
70
86
|
lines.push(` ${line}`);
|
|
71
87
|
});
|
|
72
88
|
if (data.stepFiles)
|
|
73
|
-
lines.push('', `page after each step: ${data.stepFiles}`);
|
|
89
|
+
lines.push('', `page after each step: ${data.stepFiles}/<n>-<step>.{aria.yaml,html,diff.yaml}`);
|
|
74
90
|
return section('Steps', lines.join('\n'));
|
|
75
91
|
}
|
|
76
92
|
function renderExpectations(data) {
|
|
@@ -154,6 +170,8 @@ export function renderArtifacts(data) {
|
|
|
154
170
|
lines.push(`screenshot: ${data.artifacts.screenshot}`);
|
|
155
171
|
if (data.artifacts.network)
|
|
156
172
|
lines.push(`network: ${data.artifacts.network}`);
|
|
173
|
+
if (data.artifacts.files?.length)
|
|
174
|
+
lines.push(`also here: ${data.artifacts.files.join(', ')}`);
|
|
157
175
|
return section('Artifacts', lines.join('\n'));
|
|
158
176
|
}
|
|
159
177
|
function align(label, marker, width) {
|
|
@@ -23,7 +23,7 @@ import { browserErrorMessage } from "../../../src/utils/browser-errors.js";
|
|
|
23
23
|
import { pluralize } from "../../../src/utils/logger.js";
|
|
24
24
|
import { mdq } from "../../../src/utils/markdown-query.js";
|
|
25
25
|
import { safeFilename } from "../../../src/utils/strings.js";
|
|
26
|
-
import { writeArtifacts } from "./envelope.js";
|
|
26
|
+
import { STATUS_FILE, readArtifacts, writeArtifacts } from "./envelope.js";
|
|
27
27
|
import { isFunctionExpression, takePwValue, toCodeceptWrapper } from "./pw-parser.js";
|
|
28
28
|
import { readDescriptors, selectDescriptor } from "./pw-registry.js";
|
|
29
29
|
import { latestSessionFile, readSession, recordCommand, sessionFile, sessionsDir } from "./session-log.js";
|
|
@@ -341,8 +341,6 @@ export class Prima {
|
|
|
341
341
|
const problems = [...unreached.map((expectation) => `not reached: ${expectation.text}`), ...contradicted.map((expectation) => `the picture and the run disagree about: ${expectation.text}`)];
|
|
342
342
|
if (problems.length)
|
|
343
343
|
envelope.failure = { error: problems.join('\n') };
|
|
344
|
-
if (contradicted.length)
|
|
345
|
-
envelope.artifacts = this.artifacts;
|
|
346
344
|
if (!test.hasFinished || test.isSkipped) {
|
|
347
345
|
envelope.ok = false;
|
|
348
346
|
envelope.failure = { error: `the run did not complete, so it established nothing about the app: ${notes.at(-1)?.message || 'no steps were recorded'}` };
|
|
@@ -924,14 +922,17 @@ export class Prima {
|
|
|
924
922
|
return response?.text || '';
|
|
925
923
|
}
|
|
926
924
|
async successEnvelope(command, used, result, previousState) {
|
|
925
|
+
const changes = await this.pageChanges(result, previousState, used[0]);
|
|
926
|
+
const status = await this.saveStatus(result);
|
|
927
927
|
return {
|
|
928
928
|
ok: true,
|
|
929
929
|
command,
|
|
930
930
|
used,
|
|
931
931
|
page: this.pageBlock(result, previousState),
|
|
932
|
-
changes
|
|
932
|
+
changes,
|
|
933
933
|
instance: await this.instanceInfo(),
|
|
934
|
-
status
|
|
934
|
+
status,
|
|
935
|
+
artifacts: this.artifacts,
|
|
935
936
|
};
|
|
936
937
|
}
|
|
937
938
|
async failureEnvelope(command, error, previousState) {
|
|
@@ -939,23 +940,27 @@ export class Prima {
|
|
|
939
940
|
const failure = { error: browserErrorMessage(error) };
|
|
940
941
|
if (result.ariaSnapshot)
|
|
941
942
|
failure.compactAria = compactAriaSnapshot(result.ariaSnapshot, true);
|
|
943
|
+
const status = await this.saveStatus(result);
|
|
942
944
|
return {
|
|
943
945
|
ok: false,
|
|
944
946
|
command,
|
|
945
947
|
page: this.pageBlock(result, previousState),
|
|
946
948
|
failure,
|
|
947
949
|
instance: await this.instanceInfo(),
|
|
948
|
-
status
|
|
950
|
+
status,
|
|
951
|
+
artifacts: this.artifacts,
|
|
949
952
|
};
|
|
950
953
|
}
|
|
951
954
|
async reportEnvelope(command, result, previousState, outcome) {
|
|
955
|
+
const status = await this.saveStatus(result);
|
|
952
956
|
return {
|
|
953
957
|
ok: true,
|
|
954
958
|
command,
|
|
955
959
|
page: this.pageBlock(result, previousState),
|
|
956
960
|
...outcome,
|
|
957
961
|
instance: await this.instanceInfo(),
|
|
958
|
-
status
|
|
962
|
+
status,
|
|
963
|
+
artifacts: this.artifacts,
|
|
959
964
|
};
|
|
960
965
|
}
|
|
961
966
|
async capturedResult(previousState, opts = {}) {
|
|
@@ -1000,23 +1005,30 @@ export class Prima {
|
|
|
1000
1005
|
return lines.join('\n');
|
|
1001
1006
|
}
|
|
1002
1007
|
async status(hash) {
|
|
1008
|
+
if (!this.artifactsDir) {
|
|
1009
|
+
const sites = listSites();
|
|
1010
|
+
const site = sites.find((candidate) => existsSync(path.join(candidate.dir, 'output', 'prima', hash))) || sites[0];
|
|
1011
|
+
if (site && !this.configBaseUrl())
|
|
1012
|
+
this.sessionUrl = site.url;
|
|
1013
|
+
await this.loadConfig();
|
|
1014
|
+
}
|
|
1003
1015
|
const dir = this.statusDir(hash);
|
|
1004
|
-
const statusFile = path.join(dir,
|
|
1016
|
+
const statusFile = path.join(dir, STATUS_FILE);
|
|
1005
1017
|
if (!existsSync(statusFile))
|
|
1006
|
-
return this.toolFailureEnvelope(`status ${hash}`, `No command was recorded under ${
|
|
1018
|
+
return this.toolFailureEnvelope(`status ${hash}`, `No command was recorded under ${dir}. Every envelope prints its own hash on the Instance line.`);
|
|
1007
1019
|
const saved = JSON.parse(readFileSync(statusFile, 'utf-8'));
|
|
1008
1020
|
return {
|
|
1009
1021
|
ok: true,
|
|
1010
1022
|
command: `status ${hash}`,
|
|
1011
1023
|
page: saved.page,
|
|
1012
1024
|
instance: await this.instanceInfo(),
|
|
1013
|
-
artifacts:
|
|
1025
|
+
artifacts: readArtifacts(dir),
|
|
1014
1026
|
};
|
|
1015
1027
|
}
|
|
1016
1028
|
async saveStatus(result) {
|
|
1017
1029
|
const hash = this.statusHash();
|
|
1018
1030
|
await this.writeSnapshot(result);
|
|
1019
|
-
writeFileSync(path.join(this.statusDir(hash),
|
|
1031
|
+
writeFileSync(path.join(this.statusDir(hash), STATUS_FILE), JSON.stringify({ page: this.pageBlock(result, null) }), 'utf-8');
|
|
1020
1032
|
return hash;
|
|
1021
1033
|
}
|
|
1022
1034
|
async writeStepFiles(index, label, diff) {
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"dependencies": {
|
|
78
78
|
"@ai-sdk/anthropic": "^4.0",
|
|
79
79
|
"@ai-sdk/google": "^4.0.18",
|
|
80
|
-
"@ai-sdk/groq": "^4.0",
|
|
80
|
+
"@ai-sdk/groq": "^4.0.34",
|
|
81
81
|
"@ai-sdk/mistral": "^4.0.13",
|
|
82
82
|
"@ai-sdk/openai": "^4.0",
|
|
83
83
|
"@ai-sdk/otel": "^1.0.2",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Study the page and figure out its business purpose. What is this page FOR? What would a user come here to do?
|
|
2
2
|
|
|
3
3
|
Based on the page type, propose tests for COMPLETE user workflows:
|
|
4
|
-
- If this is a data page (lists, tables): test
|
|
4
|
+
- If this is a data page (lists, tables): test create, edit and delete as separate tests, each preparing the item it acts on
|
|
5
5
|
- If the page has inputs to fill in: test the full commit flow, not just that the controls render
|
|
6
6
|
- If this has filters and search: test filtering AND verify results change, not just "filter tab clicked"
|
|
7
7
|
- If this has modals/dropdowns: test the ACTION inside them, not just opening/closing them
|
|
@@ -29,7 +29,6 @@ interface ActionResultData extends WebPageState {
|
|
|
29
29
|
focusedElement?: FocusedElement | null;
|
|
30
30
|
iframeURL?: string;
|
|
31
31
|
links?: Link[];
|
|
32
|
-
overlayHtml?: string;
|
|
33
32
|
}
|
|
34
33
|
export interface PageDiff {
|
|
35
34
|
urlChanged: boolean;
|
|
@@ -42,6 +41,7 @@ export interface PageDiff {
|
|
|
42
41
|
consoleErrors?: string[];
|
|
43
42
|
htmlParts?: HtmlDiffPart[];
|
|
44
43
|
iframes?: string;
|
|
44
|
+
areaOfInterest?: string;
|
|
45
45
|
}
|
|
46
46
|
export interface ToolResultMetadata {
|
|
47
47
|
url: string;
|
|
@@ -84,6 +84,10 @@ export declare class ActionResult implements ActionResultData {
|
|
|
84
84
|
links: Link[];
|
|
85
85
|
verifications?: Record<string, boolean>;
|
|
86
86
|
overlay: Overlay;
|
|
87
|
+
_diffCache: {
|
|
88
|
+
previousId: number | undefined;
|
|
89
|
+
diff: Diff;
|
|
90
|
+
} | null;
|
|
87
91
|
constructor(data: ActionResultData);
|
|
88
92
|
get hash(): string;
|
|
89
93
|
get html(): string;
|
|
@@ -117,8 +121,10 @@ export declare class ActionResult implements ActionResultData {
|
|
|
117
121
|
get relativeUrl(): string | null;
|
|
118
122
|
get isInsideIframe(): boolean;
|
|
119
123
|
getStateHash(): string;
|
|
124
|
+
get baseHash(): string;
|
|
120
125
|
diff(previousState: ActionResult | null): Promise<Diff>;
|
|
121
126
|
toToolResult(previousState: ActionResult | null, locator: string): Promise<ToolResultMetadata>;
|
|
127
|
+
computeStateHash(includeRegion: boolean): string;
|
|
122
128
|
consoleErrors(): string[];
|
|
123
129
|
}
|
|
124
130
|
export declare class Diff {
|
|
@@ -140,6 +146,8 @@ export declare class Diff {
|
|
|
140
146
|
get ariaChangeCount(): number;
|
|
141
147
|
get htmlDiff(): HtmlDiffResult | null;
|
|
142
148
|
get messages(): string[];
|
|
149
|
+
get similarity(): number;
|
|
150
|
+
get pageSize(): number;
|
|
143
151
|
calculate(): Promise<void>;
|
|
144
152
|
}
|
|
145
153
|
export interface NetworkCall {
|
|
@@ -39,6 +39,7 @@ export class ActionResult {
|
|
|
39
39
|
links = [];
|
|
40
40
|
verifications;
|
|
41
41
|
overlay = new Overlay();
|
|
42
|
+
_diffCache = null;
|
|
42
43
|
constructor(data) {
|
|
43
44
|
this.id = data.id;
|
|
44
45
|
this.timestamp = data.timestamp ?? new Date();
|
|
@@ -108,6 +109,7 @@ export class ActionResult {
|
|
|
108
109
|
set html(value) {
|
|
109
110
|
this._html = value;
|
|
110
111
|
this.snapshotCache.clear();
|
|
112
|
+
this._diffCache = null;
|
|
111
113
|
}
|
|
112
114
|
get screenshot() {
|
|
113
115
|
if (this._screenshot) {
|
|
@@ -193,6 +195,14 @@ export class ActionResult {
|
|
|
193
195
|
isRelevantExperienceRecord(record, options) {
|
|
194
196
|
if (!record.url || !this.url)
|
|
195
197
|
return false;
|
|
198
|
+
if (record.region && this.overlay.name !== record.region)
|
|
199
|
+
return false;
|
|
200
|
+
if (record.root) {
|
|
201
|
+
if (!this.overlay.present)
|
|
202
|
+
return false;
|
|
203
|
+
if (this.overlay.root && this.overlay.root !== record.root)
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
196
206
|
if (this.isMatchedBy(record))
|
|
197
207
|
return true;
|
|
198
208
|
if (!options?.includeDescendantExperience)
|
|
@@ -386,24 +396,17 @@ export class ActionResult {
|
|
|
386
396
|
return !!this.iframeURL;
|
|
387
397
|
}
|
|
388
398
|
getStateHash() {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
parts.push(`h1_${this.h1}`);
|
|
394
|
-
if (this.h2)
|
|
395
|
-
parts.push(`h2_${this.h2}`);
|
|
396
|
-
let stateString = slugify(parts.map((part) => part.substring(0, 100)).join('_'));
|
|
397
|
-
if (stateString.length > 200) {
|
|
398
|
-
stateString = stateString.substring(0, 200);
|
|
399
|
-
if (stateString.endsWith('_')) {
|
|
400
|
-
stateString = stateString.slice(0, -1);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
return stateString;
|
|
399
|
+
return this.computeStateHash(true);
|
|
400
|
+
}
|
|
401
|
+
get baseHash() {
|
|
402
|
+
return this.computeStateHash(false);
|
|
404
403
|
}
|
|
405
404
|
async diff(previousState) {
|
|
406
|
-
|
|
405
|
+
if (this._diffCache && this._diffCache.previousId === previousState?.id)
|
|
406
|
+
return this._diffCache.diff;
|
|
407
|
+
const diff = await Diff.create(this, previousState);
|
|
408
|
+
this._diffCache = { previousId: previousState?.id, diff };
|
|
409
|
+
return diff;
|
|
407
410
|
}
|
|
408
411
|
async toToolResult(previousState, locator) {
|
|
409
412
|
const result = {
|
|
@@ -443,7 +446,18 @@ export class ActionResult {
|
|
|
443
446
|
pageDiff.ariaChanges = diff.ariaChanged;
|
|
444
447
|
pageDiff.ariaChangeCount = diff.ariaChangeCount;
|
|
445
448
|
}
|
|
446
|
-
if (
|
|
449
|
+
if (this.overlay.present && (!previousState.overlay.present || previousState.overlay.name !== this.overlay.name)) {
|
|
450
|
+
pageDiff.areaOfInterest = this.overlay.describe();
|
|
451
|
+
}
|
|
452
|
+
if (pageDiff.areaOfInterest && this.overlay.html && this.overlay.root) {
|
|
453
|
+
const htmlConfig = ConfigParser.getInstance().getConfig().html;
|
|
454
|
+
let subtree = await minifyHtml(htmlCombinedSnapshot(this.overlay.html, htmlConfig?.combined));
|
|
455
|
+
if (subtree.length > HTML_PART_SUBTREE_BUDGET) {
|
|
456
|
+
subtree = `${subtree.slice(0, HTML_PART_SUBTREE_BUDGET)}...<!-- truncated -->`;
|
|
457
|
+
}
|
|
458
|
+
pageDiff.htmlParts = [{ container: this.overlay.root, subtree, rawSize: subtree.length, added: [], removed: [] }];
|
|
459
|
+
}
|
|
460
|
+
else if (diff.isSameUrl() && diff.htmlParts.length > 0) {
|
|
447
461
|
const collapsed = collapseHtmlParts(await diff.cleanedHtmlParts());
|
|
448
462
|
if (collapsed.length > 0) {
|
|
449
463
|
pageDiff.htmlParts = collapsed;
|
|
@@ -456,6 +470,25 @@ export class ActionResult {
|
|
|
456
470
|
}
|
|
457
471
|
return result;
|
|
458
472
|
}
|
|
473
|
+
computeStateHash(includeRegion) {
|
|
474
|
+
const parts = [];
|
|
475
|
+
parts.push(this.relativeUrl || this.url || '/');
|
|
476
|
+
this.extractHeadings(this.html);
|
|
477
|
+
if (this.h1)
|
|
478
|
+
parts.push(`h1_${this.h1}`);
|
|
479
|
+
if (this.h2)
|
|
480
|
+
parts.push(`h2_${this.h2}`);
|
|
481
|
+
if (includeRegion && this.overlay.present && this.overlay.name)
|
|
482
|
+
parts.push(`region_${this.overlay.name}`);
|
|
483
|
+
let stateString = slugify(parts.map((part) => part.substring(0, 100)).join('_'));
|
|
484
|
+
if (stateString.length > 200) {
|
|
485
|
+
stateString = stateString.substring(0, 200);
|
|
486
|
+
if (stateString.endsWith('_')) {
|
|
487
|
+
stateString = stateString.slice(0, -1);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return stateString;
|
|
491
|
+
}
|
|
459
492
|
consoleErrors() {
|
|
460
493
|
const errors = [];
|
|
461
494
|
for (const log of this.browserLogs) {
|
|
@@ -560,14 +593,20 @@ export class Diff {
|
|
|
560
593
|
get messages() {
|
|
561
594
|
return this._messages;
|
|
562
595
|
}
|
|
596
|
+
get similarity() {
|
|
597
|
+
return this._htmlDiffResult?.similarity ?? 0;
|
|
598
|
+
}
|
|
599
|
+
get pageSize() {
|
|
600
|
+
return this._htmlDiffResult?.pageSize ?? 0;
|
|
601
|
+
}
|
|
563
602
|
async calculate() {
|
|
564
603
|
if (!this.previous)
|
|
565
604
|
return;
|
|
605
|
+
this._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html, ConfigParser.getInstance().getConfig().html);
|
|
566
606
|
if (!this._isSameUrl) {
|
|
567
607
|
this._messages = liveRegionMessages(this.previous.html, this.current.html);
|
|
568
608
|
return;
|
|
569
609
|
}
|
|
570
|
-
this._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html, ConfigParser.getInstance().getConfig().html);
|
|
571
610
|
this._messages = this._htmlDiffResult.messages;
|
|
572
611
|
const ariaDiff = diffAriaSnapshots(this.previous.ariaSnapshot, this.current.ariaSnapshot);
|
|
573
612
|
this._ariaDiffResult = ariaDiff.text;
|
package/dist/src/action.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ declare class Action {
|
|
|
33
33
|
includeScreenshot?: boolean;
|
|
34
34
|
codeBlock?: string;
|
|
35
35
|
}): Promise<ActionResult>;
|
|
36
|
-
|
|
36
|
+
detectRegionOfInterest(result: ActionResult): Promise<void>;
|
|
37
37
|
captureMainDocumentStatus(): Promise<number | undefined>;
|
|
38
38
|
captureResponses(): () => void;
|
|
39
39
|
recordNetworkCall(request: any, status: number): void;
|
package/dist/src/action.js
CHANGED
|
@@ -8,9 +8,9 @@ import { clearActivity, setActivity } from "./activity.js";
|
|
|
8
8
|
import { ConfigParser, outputPath } from './config.js';
|
|
9
9
|
import { Observability } from "./observability.js";
|
|
10
10
|
import { browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from "./utils/browser-errors.js";
|
|
11
|
-
import { captureHtmlForSnapshot,
|
|
11
|
+
import { captureHtmlForSnapshot, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
|
|
12
12
|
import { createDebug, setStepSpanParent, tag } from './utils/logger.js';
|
|
13
|
-
import { Overlay } from './utils/overlay.js';
|
|
13
|
+
import { Overlay, OverlayPage } from './utils/overlay.js';
|
|
14
14
|
import { sleep, waitForPageReadiness } from "./utils/page-readiness.js";
|
|
15
15
|
import { safeFilename } from "./utils/strings.js";
|
|
16
16
|
import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from "./utils/web-sandbox.js";
|
|
@@ -133,13 +133,10 @@ class Action {
|
|
|
133
133
|
let ariaSnapshot = null;
|
|
134
134
|
let ariaSnapshotFile = undefined;
|
|
135
135
|
let focusedElement = null;
|
|
136
|
-
let overlayHtml = '';
|
|
137
136
|
try {
|
|
138
137
|
const page = this.playwrightHelper.page;
|
|
139
138
|
ariaSnapshot = await page.locator('body').ariaSnapshot();
|
|
140
139
|
focusedElement = await page.evaluate(readFocusedElement);
|
|
141
|
-
if (!frame)
|
|
142
|
-
overlayHtml = await this.captureOverlayHtml();
|
|
143
140
|
}
|
|
144
141
|
catch (err) {
|
|
145
142
|
debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
|
|
@@ -166,9 +163,10 @@ class Action {
|
|
|
166
163
|
ariaSnapshot,
|
|
167
164
|
ariaSnapshotFile,
|
|
168
165
|
focusedElement,
|
|
169
|
-
overlayHtml: overlayHtml || undefined,
|
|
170
166
|
iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
|
|
171
167
|
});
|
|
168
|
+
if (!frame)
|
|
169
|
+
await this.detectRegionOfInterest(result).catch((err) => debugLog('Region detection failed:', err.message));
|
|
172
170
|
this.stateManager.updateState(result, codeBlock);
|
|
173
171
|
return result;
|
|
174
172
|
}
|
|
@@ -181,11 +179,64 @@ class Action {
|
|
|
181
179
|
return new ActionResult({ url, error: msg });
|
|
182
180
|
}
|
|
183
181
|
}
|
|
184
|
-
async
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return
|
|
188
|
-
|
|
182
|
+
async detectRegionOfInterest(result) {
|
|
183
|
+
const previousState = this.stateManager.getCurrentState();
|
|
184
|
+
if (!previousState)
|
|
185
|
+
return;
|
|
186
|
+
const previous = ActionResult.fromState(previousState);
|
|
187
|
+
const previousOverlay = previous.overlay;
|
|
188
|
+
const sameUrl = !!previous.url && result.isSameUrl({ url: previous.url });
|
|
189
|
+
const overlayPage = new OverlayPage(this.playwrightHelper.page);
|
|
190
|
+
if (result.overlay.detected && previousOverlay.detected && previousOverlay.root && previousOverlay.type === result.overlay.type && previousOverlay.name === result.overlay.name) {
|
|
191
|
+
result.overlay = previousOverlay;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (!previous.html)
|
|
195
|
+
return;
|
|
196
|
+
if (previous.html === result.html) {
|
|
197
|
+
if (sameUrl && previousOverlay.present && previousOverlay.xpath && !result.overlay.detected)
|
|
198
|
+
result.overlay = previousOverlay;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
let carried = null;
|
|
202
|
+
if (sameUrl && previousOverlay.present && previousOverlay.xpath) {
|
|
203
|
+
if (await overlayPage.isStillOpen(previousOverlay)) {
|
|
204
|
+
carried = previousOverlay;
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
debugLog(`Region closed: ${previousOverlay.name || previousOverlay.type}`);
|
|
208
|
+
if (!result.overlay.detected) {
|
|
209
|
+
const parent = previousOverlay.parent;
|
|
210
|
+
if (parent?.xpath) {
|
|
211
|
+
const restored = new Overlay(parent);
|
|
212
|
+
if (await overlayPage.isStillOpen(restored))
|
|
213
|
+
result.overlay = restored;
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const diff = await result.diff(previous);
|
|
220
|
+
const detected = await overlayPage.detectRegion({
|
|
221
|
+
parts: diff.htmlParts,
|
|
222
|
+
pageSize: diff.pageSize,
|
|
223
|
+
similarity: diff.similarity,
|
|
224
|
+
sameUrl,
|
|
225
|
+
previousHtml: previous.html,
|
|
226
|
+
});
|
|
227
|
+
if (result.overlay.detected) {
|
|
228
|
+
if (detected)
|
|
229
|
+
result.overlay = result.overlay.withGeometry(detected);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (detected) {
|
|
233
|
+
result.overlay = detected;
|
|
234
|
+
if (carried)
|
|
235
|
+
result.overlay = detected.withParent(carried);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (carried)
|
|
239
|
+
result.overlay = carried;
|
|
189
240
|
}
|
|
190
241
|
async captureMainDocumentStatus() {
|
|
191
242
|
if (this.mainDocumentStatus)
|
|
@@ -349,6 +400,14 @@ class Action {
|
|
|
349
400
|
await recorder.reset();
|
|
350
401
|
await recorder.start();
|
|
351
402
|
}
|
|
403
|
+
if (executedSteps.length > 0) {
|
|
404
|
+
codeString = executedSteps.map((step) => step.command).join('\n');
|
|
405
|
+
}
|
|
406
|
+
if (!isFatalBrowserError(err)) {
|
|
407
|
+
const captured = await this.captureOnce({ codeBlock: codeString }).catch(() => null);
|
|
408
|
+
if (captured && !captured.error)
|
|
409
|
+
this.actionResult = captured;
|
|
410
|
+
}
|
|
352
411
|
this.assertionSteps = [];
|
|
353
412
|
throw err;
|
|
354
413
|
}
|
|
@@ -448,14 +507,30 @@ async function captureTitle(page, actor) {
|
|
|
448
507
|
}
|
|
449
508
|
const ASSERTION_STEP_NAMES = new Set(['see', 'dontSee', 'seeElement', 'dontSeeElement', 'seeInField', 'dontSeeInField', 'seeInCurrentUrl', 'dontSeeInCurrentUrl']);
|
|
450
509
|
export const attachStepLogger = (target, assertionsTarget) => {
|
|
510
|
+
const recorded = new WeakMap();
|
|
511
|
+
let batchFailed = false;
|
|
451
512
|
const listener = (step, error) => {
|
|
452
513
|
if (!step?.toCode)
|
|
453
514
|
return;
|
|
454
515
|
if (step.name?.startsWith('grab'))
|
|
455
516
|
return;
|
|
517
|
+
const existing = recorded.get(step);
|
|
518
|
+
if (existing) {
|
|
519
|
+
if (!error && !existing.success) {
|
|
520
|
+
existing.success = true;
|
|
521
|
+
existing.error = undefined;
|
|
522
|
+
batchFailed = target.some((entry) => !entry.success);
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (batchFailed)
|
|
527
|
+
return;
|
|
456
528
|
const executed = { command: step.toCode(), success: !error };
|
|
457
|
-
if (error)
|
|
529
|
+
if (error) {
|
|
458
530
|
executed.error = errorToString(error);
|
|
531
|
+
batchFailed = true;
|
|
532
|
+
}
|
|
533
|
+
recorded.set(step, executed);
|
|
459
534
|
target.push(executed);
|
|
460
535
|
if (assertionsTarget && ASSERTION_STEP_NAMES.has(step.name)) {
|
|
461
536
|
assertionsTarget.push({ name: step.name, args: step.args || [] });
|
package/dist/src/ai/captain.js
CHANGED
|
@@ -137,7 +137,7 @@ export class Captain extends CaptainBase {
|
|
|
137
137
|
const headingLines = formatHeadings(state);
|
|
138
138
|
const headingsBlock = headingLines.join('\n');
|
|
139
139
|
let pageSummary = '';
|
|
140
|
-
const cachedResearch = Researcher.getCachedResearch(
|
|
140
|
+
const cachedResearch = Researcher.getCachedResearch(actionResult);
|
|
141
141
|
if (cachedResearch) {
|
|
142
142
|
pageSummary = `<page_summary>\n${this.explorBot.agentResearcher().extractBrief(cachedResearch)}\n</page_summary>`;
|
|
143
143
|
}
|
package/dist/src/ai/driller.d.ts
CHANGED
|
@@ -135,7 +135,6 @@ export declare class Driller extends TaskAgent implements Agent {
|
|
|
135
135
|
createVerifiedActionTools(baseTools: Record<string, any>, component: ComponentInfo): Record<string, any>;
|
|
136
136
|
hasVerifiedAction(componentId: string): boolean;
|
|
137
137
|
detectNestedOverlayContext(component: ComponentInfo, result: any): Promise<string | null>;
|
|
138
|
-
getVisibleOverlayHtml(): Promise<string>;
|
|
139
138
|
getComponentScopeHtml(component: ComponentInfo, originalState: ActionResult): Promise<string>;
|
|
140
139
|
saveToKnowledge(knowledgePath: string, state: ActionResult, results: InteractionResult[]): Promise<void>;
|
|
141
140
|
generateKnowledgeContent(state: ActionResult, interactions: InteractionResult[]): string;
|