explorbot 0.2.3 → 0.2.5
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/README.md +1 -1
- package/bin/explorbot-cli.ts +26 -8
- package/boat/api-tester/src/cli.ts +17 -0
- package/boat/api-tester/src/config.ts +4 -2
- package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
- package/boat/doc-collector/src/ai/documentarian.ts +61 -31
- package/boat/doc-collector/src/cli.ts +14 -1
- package/boat/doc-collector/src/config.ts +4 -2
- package/boat/prima/bin/prima-cli.ts +0 -0
- package/boat/prima/src/activity-line.ts +33 -0
- package/boat/prima/src/cli.ts +127 -86
- package/boat/prima/src/envelope.ts +102 -52
- package/boat/prima/src/prima.ts +567 -128
- package/boat/prima/src/pw-parser.ts +11 -1
- package/boat/prima/src/pw-registry.ts +4 -5
- package/boat/prima/src/session-log.ts +126 -0
- package/dist/bin/explorbot-cli.js +26 -8
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
- package/dist/boat/api-tester/src/cli.js +17 -0
- package/dist/boat/api-tester/src/config.js +4 -2
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
- package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
- package/dist/boat/doc-collector/src/cli.js +14 -1
- package/dist/boat/doc-collector/src/config.js +4 -2
- package/dist/boat/prima/src/activity-line.js +30 -0
- package/dist/boat/prima/src/cli.js +109 -77
- package/dist/boat/prima/src/envelope.js +94 -44
- package/dist/boat/prima/src/prima.js +533 -119
- package/dist/boat/prima/src/pw-parser.js +13 -1
- package/dist/boat/prima/src/pw-registry.js +4 -5
- package/dist/boat/prima/src/session-log.js +108 -0
- package/dist/package.json +3 -2
- package/dist/rules/navigator/verification-actions.md +20 -0
- package/dist/src/action-result.d.ts +7 -0
- package/dist/src/action-result.js +4 -0
- package/dist/src/action.d.ts +2 -0
- package/dist/src/action.js +41 -2
- package/dist/src/ai/captain/web-mode.js +6 -3
- package/dist/src/ai/captain.js +2 -0
- package/dist/src/ai/navigator.d.ts +34 -0
- package/dist/src/ai/navigator.js +237 -181
- package/dist/src/ai/pilot.d.ts +7 -0
- package/dist/src/ai/pilot.js +90 -2
- package/dist/src/ai/provider.d.ts +2 -2
- package/dist/src/ai/provider.js +14 -23
- package/dist/src/ai/rerunner.js +2 -1
- package/dist/src/ai/researcher/cache.d.ts +2 -0
- package/dist/src/ai/researcher/cache.js +10 -2
- package/dist/src/ai/researcher.js +3 -2
- package/dist/src/ai/rules.js +17 -10
- package/dist/src/ai/session-analyst.js +2 -0
- package/dist/src/ai/task-agent.js +4 -1
- package/dist/src/ai/tester.d.ts +6 -3
- package/dist/src/ai/tester.js +50 -46
- package/dist/src/ai/tools.d.ts +14 -0
- package/dist/src/ai/tools.js +117 -37
- package/dist/src/commands/config-command.d.ts +51 -0
- package/dist/src/commands/config-command.js +117 -0
- package/dist/src/commands/index.js +2 -0
- package/dist/src/config.d.ts +9 -1
- package/dist/src/config.js +53 -4
- package/dist/src/execution-controller.d.ts +2 -0
- package/dist/src/execution-controller.js +6 -0
- package/dist/src/explorbot.d.ts +2 -1
- package/dist/src/explorbot.js +7 -2
- package/dist/src/explorer.js +2 -3
- package/dist/src/playwright-recorder.js +30 -0
- package/dist/src/remote.d.ts +55 -0
- package/dist/src/remote.js +235 -0
- package/dist/src/reporter.d.ts +1 -0
- package/dist/src/reporter.js +7 -1
- package/dist/src/state-manager.d.ts +2 -1
- package/dist/src/state-manager.js +3 -1
- package/dist/src/stats.d.ts +1 -0
- package/dist/src/stats.js +1 -0
- package/dist/src/test-plan.d.ts +3 -0
- package/dist/src/test-plan.js +26 -0
- package/dist/src/utils/aria.d.ts +2 -8
- package/dist/src/utils/aria.js +69 -40
- package/dist/src/utils/html.js +1 -0
- package/dist/src/utils/logger.d.ts +7 -1
- package/dist/src/utils/logger.js +32 -0
- package/dist/src/utils/page-readiness.js +18 -1
- package/dist/src/utils/url-matcher.js +3 -0
- package/dist/src/utils/web-element.d.ts +2 -0
- package/dist/src/utils/web-element.js +8 -0
- package/dist/src/utils/web-sandbox.d.ts +1 -1
- package/dist/src/utils/web-sandbox.js +2 -3
- package/docs/api-testing/basics.md +90 -0
- package/docs/api-testing/planning.md +57 -0
- package/docs/api-testing/running-tests.md +55 -0
- package/docs/assets/cloud-report.png +0 -0
- package/docs/assets/html-report.png +0 -0
- package/docs/assets/langfuse-trace.png +0 -0
- package/docs/assets/successful-explore-run.png +0 -0
- package/docs/basics/getting-started.md +140 -0
- package/docs/basics/prerequisites.md +63 -0
- package/docs/basics/providers.md +362 -0
- package/docs/basics/running.md +78 -0
- package/docs/contributing/ai-integration-tests.md +57 -0
- package/docs/contributing/contributing.md +90 -0
- package/docs/contributing/demo-videos.md +36 -0
- package/docs/contributing/npm-package.md +138 -0
- package/docs/contributing/observability.md +227 -0
- package/docs/contributing/regression-tests.md +103 -0
- package/docs/contributing/testing.md +95 -0
- package/docs/doc-collection/basics.md +128 -0
- package/docs/doc-collection/crawling.md +67 -0
- package/docs/doc-collection/interactive-mode.md +99 -0
- package/docs/index.json +87 -0
- package/docs/reference/commands.md +997 -0
- package/docs/reference/configuration.md +569 -0
- package/docs/reference/scripting.md +303 -0
- package/docs/reference/websocket.md +50 -0
- package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
- package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
- package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
- package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
- package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
- package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
- package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
- package/docs/superpowers/specs/2026-08-18-prima-false-verdicts.md +159 -0
- package/docs/web-testing/agents.md +158 -0
- package/docs/web-testing/automated-tests.md +134 -0
- package/docs/web-testing/basics.md +91 -0
- package/docs/web-testing/customization.md +131 -0
- package/docs/web-testing/hooks.md +238 -0
- package/docs/web-testing/page-interaction.md +84 -0
- package/docs/web-testing/planner.md +122 -0
- package/docs/web-testing/rerun.md +164 -0
- package/docs/web-testing/researcher.md +380 -0
- package/docs/workflow/agentic-usage.md +233 -0
- package/docs/workflow/application-spec.md +73 -0
- package/docs/workflow/ci.md +202 -0
- package/docs/workflow/knowledge.md +310 -0
- package/docs/workflow/planning-styles.md +67 -0
- package/docs/workflow/reporting.md +133 -0
- package/docs/workflow/test-plans.md +90 -0
- package/package.json +3 -2
- package/rules/navigator/verification-actions.md +20 -0
- package/src/action-result.ts +11 -0
- package/src/action.ts +43 -3
- package/src/ai/captain/web-mode.ts +6 -3
- package/src/ai/captain.ts +3 -0
- package/src/ai/navigator.ts +255 -186
- package/src/ai/pilot.ts +104 -2
- package/src/ai/provider.ts +14 -24
- package/src/ai/rerunner.ts +2 -1
- package/src/ai/researcher/cache.ts +12 -2
- package/src/ai/researcher.ts +3 -2
- package/src/ai/rules.ts +17 -10
- package/src/ai/session-analyst.ts +2 -0
- package/src/ai/task-agent.ts +3 -1
- package/src/ai/tester.ts +52 -45
- package/src/ai/tools.ts +136 -37
- package/src/commands/config-command.ts +146 -0
- package/src/commands/index.ts +2 -0
- package/src/config.ts +60 -5
- package/src/execution-controller.ts +8 -0
- package/src/explorbot.ts +7 -3
- package/src/explorer.ts +2 -2
- package/src/playwright-recorder.ts +23 -0
- package/src/remote.ts +244 -0
- package/src/reporter.ts +7 -1
- package/src/state-manager.ts +6 -2
- package/src/stats.ts +1 -0
- package/src/test-plan.ts +29 -0
- package/src/utils/aria.ts +65 -45
- package/src/utils/html.ts +1 -0
- package/src/utils/logger.ts +33 -2
- package/src/utils/page-readiness.ts +24 -1
- package/src/utils/url-matcher.ts +3 -0
- package/src/utils/web-element.ts +9 -0
- package/src/utils/web-sandbox.ts +3 -4
|
@@ -14,5 +14,17 @@ export function isFunctionExpression(expr) {
|
|
|
14
14
|
return { valid: true };
|
|
15
15
|
}
|
|
16
16
|
export function toCodeceptWrapper(expr) {
|
|
17
|
-
return `I.usePlaywrightTo('pw', async (playwright) => (${expr.trim()})(playwright))`;
|
|
17
|
+
return `return I.usePlaywrightTo('pw', async (playwright) => (${expr.trim()})(playwright))`;
|
|
18
|
+
}
|
|
19
|
+
export function takePwValue(held) {
|
|
20
|
+
if (held === undefined || held === null)
|
|
21
|
+
return undefined;
|
|
22
|
+
if (typeof held === 'string')
|
|
23
|
+
return held;
|
|
24
|
+
try {
|
|
25
|
+
return JSON.stringify(held, null, 2);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return String(held);
|
|
29
|
+
}
|
|
18
30
|
}
|
|
@@ -19,9 +19,8 @@ export function readDescriptors(dir = registryDir()) {
|
|
|
19
19
|
}
|
|
20
20
|
return descriptors;
|
|
21
21
|
}
|
|
22
|
-
export function selectDescriptor(descriptors, opts) {
|
|
23
|
-
const
|
|
24
|
-
const candidates = descriptors.filter((descriptor) => path.resolve(descriptor.workspaceDir) === workspaceDir);
|
|
22
|
+
export function selectDescriptor(descriptors, opts = {}) {
|
|
23
|
+
const candidates = descriptors;
|
|
25
24
|
if (!candidates.length)
|
|
26
25
|
return { candidates };
|
|
27
26
|
if (opts.title) {
|
|
@@ -53,13 +52,13 @@ function parseDescriptor(file) {
|
|
|
53
52
|
catch {
|
|
54
53
|
return null;
|
|
55
54
|
}
|
|
56
|
-
if (!data?.endpoint || !data?.title
|
|
55
|
+
if (!data?.endpoint || !data?.title)
|
|
57
56
|
return null;
|
|
58
57
|
return {
|
|
59
58
|
file,
|
|
60
59
|
title: data.title,
|
|
61
60
|
endpoint: data.endpoint,
|
|
62
|
-
workspaceDir: data.workspaceDir,
|
|
61
|
+
workspaceDir: data.workspaceDir || '',
|
|
63
62
|
browserName: data.browser?.browserName || DEFAULT_BROWSER,
|
|
64
63
|
playwrightLib: data.playwrightLib || '',
|
|
65
64
|
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { outputPath } from "../../../src/config.js";
|
|
4
|
+
import { safeFilename } from "../../../src/utils/strings.js";
|
|
5
|
+
import { renderEnvelope } from "./envelope.js";
|
|
6
|
+
const EXPECTATION_STATUS = { passed: 'passed', failed: 'failed', unverified: 'skipped' };
|
|
7
|
+
export function sessionsDir() {
|
|
8
|
+
return outputPath('prima', 'sessions');
|
|
9
|
+
}
|
|
10
|
+
export function sessionFile(key) {
|
|
11
|
+
return path.join(sessionsDir(), `${safeFilename(key)}.jsonl`);
|
|
12
|
+
}
|
|
13
|
+
export function latestSessionFile() {
|
|
14
|
+
const dir = sessionsDir();
|
|
15
|
+
if (!existsSync(dir))
|
|
16
|
+
return null;
|
|
17
|
+
const files = readdirSync(dir)
|
|
18
|
+
.filter((name) => name.endsWith('.jsonl'))
|
|
19
|
+
.map((name) => path.join(dir, name))
|
|
20
|
+
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
|
21
|
+
return files[0] || null;
|
|
22
|
+
}
|
|
23
|
+
export function recordCommand(file, session, envelope, durationMs) {
|
|
24
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
25
|
+
if (recordedEndpoint(file) !== session.endpoint)
|
|
26
|
+
writeFileSync(file, jsonLine({ action: 'createRun', endpoint: session.endpoint, params: { title: session.title } }), 'utf-8');
|
|
27
|
+
appendFileSync(file, jsonLine({ action: 'addTest', testId: commandTest(envelope, durationMs) }), 'utf-8');
|
|
28
|
+
}
|
|
29
|
+
export function readSession(file) {
|
|
30
|
+
const entries = readEntries(file);
|
|
31
|
+
const run = entries.find((entry) => entry.action === 'createRun');
|
|
32
|
+
const tests = entries.filter((entry) => entry.action === 'addTest').map((entry) => entry.testId);
|
|
33
|
+
return { title: run?.params?.title || 'prima session', tests };
|
|
34
|
+
}
|
|
35
|
+
function commandTest(envelope, durationMs) {
|
|
36
|
+
let status = 'failed';
|
|
37
|
+
if (envelope.ok)
|
|
38
|
+
status = 'passed';
|
|
39
|
+
const test = {
|
|
40
|
+
rid: envelope.status,
|
|
41
|
+
title: envelope.command,
|
|
42
|
+
suite_title: 'Prima session',
|
|
43
|
+
status,
|
|
44
|
+
file: '<prima>',
|
|
45
|
+
description: [`page: ${envelope.page.url}`, envelope.stepFiles && `artifacts: ${envelope.stepFiles}`].filter(Boolean).join('\n'),
|
|
46
|
+
code: (envelope.used || []).join('\n'),
|
|
47
|
+
steps: commandSteps(envelope),
|
|
48
|
+
logs: renderEnvelope(envelope),
|
|
49
|
+
time: Math.round(durationMs),
|
|
50
|
+
};
|
|
51
|
+
if (envelope.failure)
|
|
52
|
+
test.error = { message: envelope.failure.error };
|
|
53
|
+
return test;
|
|
54
|
+
}
|
|
55
|
+
function commandSteps(envelope) {
|
|
56
|
+
const steps = [];
|
|
57
|
+
for (const step of envelope.steps || []) {
|
|
58
|
+
const entry = { category: 'user', title: step.label, status: 'passed', duration: 0, log: step.proof };
|
|
59
|
+
if (!step.ok) {
|
|
60
|
+
entry.status = 'failed';
|
|
61
|
+
entry.error = step.proof;
|
|
62
|
+
}
|
|
63
|
+
steps.push(entry);
|
|
64
|
+
}
|
|
65
|
+
for (const expectation of envelope.expectations || []) {
|
|
66
|
+
const entry = { category: 'user', title: `expected: ${expectation.text}`, status: EXPECTATION_STATUS[expectation.status], duration: 0 };
|
|
67
|
+
if (expectation.status === 'failed')
|
|
68
|
+
entry.error = 'the run did not reach this outcome';
|
|
69
|
+
steps.push(entry);
|
|
70
|
+
}
|
|
71
|
+
for (const assertion of envelope.assertions || []) {
|
|
72
|
+
const entry = { category: 'user', title: assertion.code, status: 'passed', duration: 0, log: assertion.proof.join('\n') };
|
|
73
|
+
if (!assertion.passed) {
|
|
74
|
+
entry.status = 'failed';
|
|
75
|
+
entry.error = 'the assertion did not hold on the page';
|
|
76
|
+
}
|
|
77
|
+
steps.push(entry);
|
|
78
|
+
}
|
|
79
|
+
return steps;
|
|
80
|
+
}
|
|
81
|
+
function recordedEndpoint(file) {
|
|
82
|
+
const [first] = readEntries(file);
|
|
83
|
+
return first?.endpoint || null;
|
|
84
|
+
}
|
|
85
|
+
function readEntries(file) {
|
|
86
|
+
if (!existsSync(file))
|
|
87
|
+
return [];
|
|
88
|
+
const entries = [];
|
|
89
|
+
for (const line of readFileSync(file, 'utf-8').split('\n')) {
|
|
90
|
+
if (!line.trim())
|
|
91
|
+
continue;
|
|
92
|
+
const entry = parseLine(line);
|
|
93
|
+
if (entry)
|
|
94
|
+
entries.push(entry);
|
|
95
|
+
}
|
|
96
|
+
return entries;
|
|
97
|
+
}
|
|
98
|
+
function parseLine(line) {
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(line);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function jsonLine(entry) {
|
|
107
|
+
return `${JSON.stringify(entry)}\n`;
|
|
108
|
+
}
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist/",
|
|
22
|
+
"docs/",
|
|
22
23
|
"src/**/*.ts",
|
|
23
24
|
"src/**/*.tsx",
|
|
24
25
|
"bin/**/*.ts",
|
|
@@ -119,7 +120,7 @@
|
|
|
119
120
|
"ora-classic": "^5.4.2",
|
|
120
121
|
"parse5": "^8.0.0",
|
|
121
122
|
"pixelmatch": "^7.2.0",
|
|
122
|
-
"playwright": "^1.
|
|
123
|
+
"playwright": "^1.62",
|
|
123
124
|
"pngjs": "^7.0.0",
|
|
124
125
|
"react": "^19.1.1",
|
|
125
126
|
"sambanova-ai-provider": "^1.2.2",
|
|
@@ -105,9 +105,29 @@ Checks that page source does NOT contain expected text.
|
|
|
105
105
|
I.dontSeeInSource('error-class');
|
|
106
106
|
</example>
|
|
107
107
|
|
|
108
|
+
### Asserting the state of a control
|
|
109
|
+
|
|
110
|
+
State means disabled, checked, readonly, required, selected, expanded. A presence assertion says
|
|
111
|
+
nothing about it, so assert it with an attribute selector:
|
|
112
|
+
|
|
113
|
+
<example>
|
|
114
|
+
I.seeElement('button[aria-label="Submit"][disabled]');
|
|
115
|
+
I.dontSeeElement('button[aria-label="Submit"][disabled]');
|
|
116
|
+
I.seeElement('input[name="accept"][checked]');
|
|
117
|
+
I.seeElement('button[aria-label="Details"][aria-expanded="true"]');
|
|
118
|
+
</example>
|
|
119
|
+
|
|
120
|
+
Use I.seeElement for the state you expect and I.dontSeeElement for the state you expect to be
|
|
121
|
+
absent — that pair expresses both directions.
|
|
122
|
+
|
|
123
|
+
I.seeAttributesOnElements(<locator>, { disabled: true }) also exists, but only takes a state that
|
|
124
|
+
must be PRESENT and does not resolve reliably against a role/text locator. Prefer the selector form above.
|
|
125
|
+
|
|
108
126
|
<verification_rules>
|
|
109
127
|
Be strict in assertions to avoid false positives.
|
|
110
128
|
Prefer I.seeElement() with ARIA locators - most reliable.
|
|
129
|
+
For a claim about a control's state, assert it with an attribute selector — presence of the element is not evidence of its state.
|
|
130
|
+
If no assertion above can express the claim, say so instead of proposing an assertion that checks something weaker.
|
|
111
131
|
I.see() and I.dontSee() MUST include context parameter.
|
|
112
132
|
For input field values, ALWAYS use I.seeInField() — never check value via CSS attribute selectors or I.seeInSource.
|
|
113
133
|
Prefer text locators (label, name, placeholder) for form fields: I.seeInField('Search', 'value') over I.seeInField('input[name="search"]', 'value').
|
|
@@ -24,6 +24,7 @@ interface ActionResultData extends WebPageState {
|
|
|
24
24
|
}>;
|
|
25
25
|
ariaSnapshot?: string | null;
|
|
26
26
|
ariaSnapshotFile?: string;
|
|
27
|
+
focusedElement?: FocusedElement | null;
|
|
27
28
|
iframeURL?: string;
|
|
28
29
|
links?: Link[];
|
|
29
30
|
}
|
|
@@ -69,6 +70,7 @@ export declare class ActionResult implements ActionResultData {
|
|
|
69
70
|
snapshotCache: TTLCache<string>;
|
|
70
71
|
readonly logFile: string | undefined;
|
|
71
72
|
readonly ariaSnapshotFile: string | undefined;
|
|
73
|
+
readonly focusedElement: FocusedElement | null;
|
|
72
74
|
_ariaSnapshot: string | null | undefined;
|
|
73
75
|
_lastExtractedHtml: string | undefined;
|
|
74
76
|
notes: string[];
|
|
@@ -128,4 +130,9 @@ export declare class Diff {
|
|
|
128
130
|
get htmlDiff(): HtmlDiffResult | null;
|
|
129
131
|
calculate(): Promise<void>;
|
|
130
132
|
}
|
|
133
|
+
export interface FocusedElement {
|
|
134
|
+
role: string;
|
|
135
|
+
name: string;
|
|
136
|
+
value?: string;
|
|
137
|
+
}
|
|
131
138
|
export {};
|
|
@@ -30,6 +30,7 @@ export class ActionResult {
|
|
|
30
30
|
snapshotCache = new TTLCache();
|
|
31
31
|
logFile = undefined;
|
|
32
32
|
ariaSnapshotFile = undefined;
|
|
33
|
+
focusedElement = null;
|
|
33
34
|
_ariaSnapshot = undefined;
|
|
34
35
|
_lastExtractedHtml = undefined;
|
|
35
36
|
notes = [];
|
|
@@ -61,6 +62,9 @@ export class ActionResult {
|
|
|
61
62
|
if (data.ariaSnapshotFile !== undefined) {
|
|
62
63
|
this.ariaSnapshotFile = data.ariaSnapshotFile;
|
|
63
64
|
}
|
|
65
|
+
if (data.focusedElement !== undefined) {
|
|
66
|
+
this.focusedElement = data.focusedElement;
|
|
67
|
+
}
|
|
64
68
|
// Store HTML in a private property if provided
|
|
65
69
|
if (data.html !== undefined) {
|
|
66
70
|
this._html = data.html;
|
package/dist/src/action.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ declare class Action {
|
|
|
15
15
|
name: string;
|
|
16
16
|
args: any[];
|
|
17
17
|
}>;
|
|
18
|
+
lastValue: unknown;
|
|
18
19
|
recorder?: PlaywrightRecorder;
|
|
19
20
|
recovery: RecoveryRunner;
|
|
20
21
|
mainDocumentStatus: number | undefined;
|
|
@@ -45,6 +46,7 @@ declare class Action {
|
|
|
45
46
|
exitIframe(): Promise<void>;
|
|
46
47
|
getActor(): CodeceptJS.I;
|
|
47
48
|
getActionResult(): ActionResult | null;
|
|
49
|
+
restorePageTimeout(): void;
|
|
48
50
|
waitForPageReadiness(page: any): Promise<void>;
|
|
49
51
|
}
|
|
50
52
|
export default Action;
|
package/dist/src/action.js
CHANGED
|
@@ -15,6 +15,8 @@ import { safeFilename } from "./utils/strings.js";
|
|
|
15
15
|
import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from "./utils/web-sandbox.js";
|
|
16
16
|
const debugLog = createDebug('explorbot:action');
|
|
17
17
|
const CAPTURE_NAVIGATION_TRANSITION_ATTEMPTS = 3;
|
|
18
|
+
const DEFAULT_ACTION_TIMEOUT = 3000;
|
|
19
|
+
const DEFAULT_PAGE_TIMEOUT = 3000;
|
|
18
20
|
class Action {
|
|
19
21
|
actor;
|
|
20
22
|
stateManager;
|
|
@@ -26,6 +28,7 @@ class Action {
|
|
|
26
28
|
playwrightHelper;
|
|
27
29
|
playwrightGroupId = null;
|
|
28
30
|
assertionSteps = [];
|
|
31
|
+
lastValue;
|
|
29
32
|
recorder;
|
|
30
33
|
recovery;
|
|
31
34
|
mainDocumentStatus = undefined;
|
|
@@ -47,6 +50,7 @@ class Action {
|
|
|
47
50
|
await this.actor.saveScreenshot(filename);
|
|
48
51
|
if (currentState)
|
|
49
52
|
currentState.screenshotFile = filename;
|
|
53
|
+
tag('data').log('screenshot', { path: outputPath('states', filename) });
|
|
50
54
|
return filename;
|
|
51
55
|
}
|
|
52
56
|
catch (err) {
|
|
@@ -94,7 +98,10 @@ class Action {
|
|
|
94
98
|
const screenshotPath = join(statesDir, filename);
|
|
95
99
|
screenshotFile = await page
|
|
96
100
|
?.screenshot({ path: screenshotPath, fullPage: true })
|
|
97
|
-
.then(() =>
|
|
101
|
+
.then(() => {
|
|
102
|
+
tag('data').log('screenshot', { path: screenshotPath });
|
|
103
|
+
return filename;
|
|
104
|
+
})
|
|
98
105
|
.catch((err) => {
|
|
99
106
|
debugLog('Screenshot failed, continuing without it:', err);
|
|
100
107
|
return undefined;
|
|
@@ -120,9 +127,11 @@ class Action {
|
|
|
120
127
|
const iframeSnapshots = await this.captureIframeSnapshots(html);
|
|
121
128
|
let ariaSnapshot = null;
|
|
122
129
|
let ariaSnapshotFile = undefined;
|
|
130
|
+
let focusedElement = null;
|
|
123
131
|
try {
|
|
124
132
|
const page = this.playwrightHelper.page;
|
|
125
133
|
ariaSnapshot = await page.locator('body').ariaSnapshot();
|
|
134
|
+
focusedElement = await page.evaluate(readFocusedElement);
|
|
126
135
|
}
|
|
127
136
|
catch (err) {
|
|
128
137
|
debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
|
|
@@ -145,6 +154,7 @@ class Action {
|
|
|
145
154
|
iframeSnapshots,
|
|
146
155
|
ariaSnapshot,
|
|
147
156
|
ariaSnapshotFile,
|
|
157
|
+
focusedElement,
|
|
148
158
|
iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
|
|
149
159
|
});
|
|
150
160
|
this.stateManager.updateState(result, codeBlock);
|
|
@@ -272,16 +282,19 @@ class Action {
|
|
|
272
282
|
if (!sanitizedCode) {
|
|
273
283
|
throw new Error('No valid I.* or page.* commands found in code block');
|
|
274
284
|
}
|
|
285
|
+
this.playwrightHelper?.page?.setDefaultTimeout(this.config.action?.timeout ?? DEFAULT_ACTION_TIMEOUT);
|
|
275
286
|
if (isPlaywright) {
|
|
276
287
|
const page = this.playwrightHelper.page;
|
|
277
288
|
await playwrightSandbox(page, sanitizedCode);
|
|
278
289
|
await sleep(this.config.action?.delay || 500);
|
|
279
290
|
}
|
|
280
291
|
else {
|
|
281
|
-
codeceptJSSandbox(this.actor, sanitizedCode);
|
|
292
|
+
const returned = codeceptJSSandbox(this.actor, sanitizedCode);
|
|
282
293
|
await recorder.add(() => sleep(this.config.action?.delay || 500));
|
|
283
294
|
await recorder.promise();
|
|
295
|
+
this.lastValue = await returned;
|
|
284
296
|
}
|
|
297
|
+
this.restorePageTimeout();
|
|
285
298
|
if (executedSteps.length > 0) {
|
|
286
299
|
codeString = executedSteps.join('\n');
|
|
287
300
|
}
|
|
@@ -300,6 +313,7 @@ class Action {
|
|
|
300
313
|
throw err;
|
|
301
314
|
}
|
|
302
315
|
finally {
|
|
316
|
+
this.restorePageTimeout();
|
|
303
317
|
detachMainDocumentResponse();
|
|
304
318
|
if (groupId)
|
|
305
319
|
await this.recorder.endAction();
|
|
@@ -346,6 +360,9 @@ class Action {
|
|
|
346
360
|
getActionResult() {
|
|
347
361
|
return this.actionResult;
|
|
348
362
|
}
|
|
363
|
+
restorePageTimeout() {
|
|
364
|
+
this.playwrightHelper?.page?.setDefaultTimeout(this.config.playwright.timeout ?? DEFAULT_PAGE_TIMEOUT);
|
|
365
|
+
}
|
|
349
366
|
async waitForPageReadiness(page) {
|
|
350
367
|
await waitForPageReadiness(page, {
|
|
351
368
|
timeout: this.config.playwright.waitForTimeout,
|
|
@@ -408,3 +425,25 @@ const detachStepLogger = (listener) => {
|
|
|
408
425
|
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, listener);
|
|
409
426
|
codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, listener);
|
|
410
427
|
};
|
|
428
|
+
const readFocusedElement = () => {
|
|
429
|
+
const el = document.activeElement;
|
|
430
|
+
if (!el || el === document.body)
|
|
431
|
+
return null;
|
|
432
|
+
const tag = el.tagName.toLowerCase();
|
|
433
|
+
const textish = new Set(['text', 'search', 'email', 'password', 'url', 'tel', 'number']);
|
|
434
|
+
let role = el.getAttribute('role') || tag;
|
|
435
|
+
if (tag === 'textarea' || el.isContentEditable)
|
|
436
|
+
role = 'textbox';
|
|
437
|
+
if (tag === 'input' && textish.has(el.type))
|
|
438
|
+
role = 'textbox';
|
|
439
|
+
if (tag === 'select')
|
|
440
|
+
role = 'combobox';
|
|
441
|
+
if (tag === 'a')
|
|
442
|
+
role = 'link';
|
|
443
|
+
const label = el.getAttribute('aria-label') || el.getAttribute('placeholder') || el.labels?.[0]?.textContent || el.textContent || '';
|
|
444
|
+
const focused = { role, name: label.trim().slice(0, 80) };
|
|
445
|
+
const value = el.value ?? el.textContent;
|
|
446
|
+
if (typeof value === 'string' && value)
|
|
447
|
+
focused.value = value.slice(0, 200);
|
|
448
|
+
return focused;
|
|
449
|
+
};
|
|
@@ -16,7 +16,7 @@ export function WithWebMode(Base) {
|
|
|
16
16
|
navigator: ctx.explorBot.agentNavigator(),
|
|
17
17
|
});
|
|
18
18
|
const { see, context, visualClick, learnExperience } = agentTools;
|
|
19
|
-
|
|
19
|
+
const tools = {
|
|
20
20
|
navigate: tool({
|
|
21
21
|
description: 'Navigate to a URL or page description using AI-powered navigation.',
|
|
22
22
|
inputSchema: z.object({
|
|
@@ -114,11 +114,14 @@ export function WithWebMode(Base) {
|
|
|
114
114
|
},
|
|
115
115
|
}),
|
|
116
116
|
...codeceptTools,
|
|
117
|
-
see,
|
|
118
117
|
context,
|
|
119
|
-
visualClick,
|
|
120
118
|
learnExperience,
|
|
121
119
|
};
|
|
120
|
+
if (see)
|
|
121
|
+
tools.see = see;
|
|
122
|
+
if (visualClick)
|
|
123
|
+
tools.visualClick = visualClick;
|
|
124
|
+
return tools;
|
|
122
125
|
}
|
|
123
126
|
webModePrompt() {
|
|
124
127
|
return dedent `
|
package/dist/src/ai/captain.js
CHANGED
|
@@ -15,6 +15,7 @@ import { WithWebMode } from "./captain/web-mode.js";
|
|
|
15
15
|
import { toolExecutionLabel } from './conversation.js';
|
|
16
16
|
import { Researcher } from "./researcher.js";
|
|
17
17
|
import { TaskAgent } from "./task-agent.js";
|
|
18
|
+
import { withdrawVisionTools } from "./tools.js";
|
|
18
19
|
const MAX_STEPS = 15;
|
|
19
20
|
const CaptainBase = WithTestMode(WithWebMode(WithIdleMode(TaskAgent)));
|
|
20
21
|
export class Captain extends CaptainBase {
|
|
@@ -389,6 +390,7 @@ export class Captain extends CaptainBase {
|
|
|
389
390
|
stop();
|
|
390
391
|
return;
|
|
391
392
|
}
|
|
393
|
+
withdrawVisionTools(tools);
|
|
392
394
|
const currentState = stateManager.getCurrentState();
|
|
393
395
|
if (!currentState && this.getMode() !== 'idle') {
|
|
394
396
|
stop();
|
|
@@ -15,6 +15,7 @@ declare class Navigator implements Agent {
|
|
|
15
15
|
experienceTracker: ExperienceTracker;
|
|
16
16
|
hooksRunner: HooksRunner;
|
|
17
17
|
MAX_ATTEMPTS: number;
|
|
18
|
+
lastFailureReason: string | null;
|
|
18
19
|
systemPrompt: string;
|
|
19
20
|
freeSailSystemPrompt: string;
|
|
20
21
|
explorer: Explorer;
|
|
@@ -25,10 +26,16 @@ declare class Navigator implements Agent {
|
|
|
25
26
|
get verifyTimeout(): number;
|
|
26
27
|
getBaseOrigin(): string | null;
|
|
27
28
|
getComparableCurrentUrl(stateManager: any, expectedUrl: string): string;
|
|
29
|
+
comparableUrl(state: {
|
|
30
|
+
url?: string;
|
|
31
|
+
fullUrl?: string;
|
|
32
|
+
}, expectedUrl: string): string;
|
|
28
33
|
isSameExpectedOrigin(expectedUrl: string, stateManager: any): boolean;
|
|
29
34
|
isOnExpectedPage(expectedUrl: string, stateManager: any): boolean;
|
|
30
35
|
visit(url: string): Promise<void>;
|
|
31
36
|
visitOnce(url: string): Promise<void>;
|
|
37
|
+
navigationError(url: string, fallback: string): Error;
|
|
38
|
+
failureReason(stopReason: string | null, knowledge: string, url: string): string | null;
|
|
32
39
|
resolveState(message: string, actionResult: ActionResult, opts?: {
|
|
33
40
|
action?: Action;
|
|
34
41
|
expectedUrl?: string;
|
|
@@ -37,6 +44,20 @@ declare class Navigator implements Agent {
|
|
|
37
44
|
error?: string;
|
|
38
45
|
}) => void;
|
|
39
46
|
}): Promise<boolean>;
|
|
47
|
+
buildResolutionPrompt(message: string, actionResult: ActionResult): Promise<string>;
|
|
48
|
+
buildRetryFeedback(failures: BatchFailure[], includeHtml: boolean, actionResult: ActionResult): Promise<string>;
|
|
49
|
+
executeAttempt(action: Action, codeBlock: string, message: string): Promise<{
|
|
50
|
+
ok: boolean;
|
|
51
|
+
error?: string;
|
|
52
|
+
}>;
|
|
53
|
+
verifyNavigation(action: Action, expectedUrl: string): Promise<{
|
|
54
|
+
freshState: ActionResult;
|
|
55
|
+
urlMatches: boolean;
|
|
56
|
+
}>;
|
|
57
|
+
ariaDiff(freshState: ActionResult, previous: ActionResult): Promise<string | null>;
|
|
58
|
+
saveFlow(message: string, expectedUrl: string | undefined, actionResult: ActionResult, progressBlocks: string[]): void;
|
|
59
|
+
rescueDelayedRedirect(action: Action, expectedUrl: string): Promise<boolean>;
|
|
60
|
+
askUserToResolve(action: Action, message: string, expectedUrl: string | undefined, stopReason: string | null): Promise<boolean>;
|
|
40
61
|
buildExperienceTools(): {
|
|
41
62
|
learnExperience: unknown;
|
|
42
63
|
} | undefined;
|
|
@@ -50,6 +71,8 @@ declare class Navigator implements Agent {
|
|
|
50
71
|
} | null>;
|
|
51
72
|
verifyState(message: string, actionResult: ActionResult): Promise<{
|
|
52
73
|
verified: boolean;
|
|
74
|
+
inexpressible: boolean;
|
|
75
|
+
results: AssertionResult[];
|
|
53
76
|
successfulCodes: string[];
|
|
54
77
|
assertionSteps: Array<{
|
|
55
78
|
name: string;
|
|
@@ -59,4 +82,15 @@ declare class Navigator implements Agent {
|
|
|
59
82
|
}>;
|
|
60
83
|
checkAlreadyVerified(aiResponse: string, actionResult: ActionResult): boolean;
|
|
61
84
|
}
|
|
85
|
+
type BatchFailure = {
|
|
86
|
+
code: string;
|
|
87
|
+
error: string;
|
|
88
|
+
ariaChanges?: string | null;
|
|
89
|
+
urlAfter?: string;
|
|
90
|
+
};
|
|
91
|
+
export type AssertionResult = {
|
|
92
|
+
code: string;
|
|
93
|
+
passed: boolean;
|
|
94
|
+
proof: string[];
|
|
95
|
+
};
|
|
62
96
|
export { Navigator };
|