explorbot 0.2.2 → 0.2.4
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 +59 -38
- package/boat/api-tester/src/apibot.ts +4 -2
- package/boat/api-tester/src/cli.ts +2 -2
- package/boat/api-tester/src/config.ts +43 -10
- 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 +1 -0
- package/boat/doc-collector/src/config.ts +4 -2
- package/boat/doc-collector/src/docs-renderer.ts +18 -4
- package/boat/doc-collector/src/state-diagram.ts +61 -14
- package/boat/prima/bin/prima-cli.ts +5 -0
- package/boat/prima/package.json +16 -0
- package/boat/prima/src/activity-line.ts +33 -0
- package/boat/prima/src/cli.ts +251 -0
- package/boat/prima/src/envelope.ts +169 -0
- package/boat/prima/src/prima.ts +1124 -0
- package/boat/prima/src/pw-parser.ts +27 -0
- package/boat/prima/src/pw-registry.ts +74 -0
- package/boat/prima/src/session-log.ts +126 -0
- package/dist/bin/explorbot-cli.js +51 -32
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
- package/dist/boat/api-tester/src/apibot.js +3 -2
- package/dist/boat/api-tester/src/cli.js +2 -2
- package/dist/boat/api-tester/src/config.js +40 -10
- 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 +1 -0
- package/dist/boat/doc-collector/src/config.js +4 -2
- package/dist/boat/doc-collector/src/docs-renderer.js +17 -3
- package/dist/boat/doc-collector/src/state-diagram.js +57 -13
- package/dist/boat/prima/bin/prima-cli.js +4 -0
- package/dist/boat/prima/src/activity-line.js +30 -0
- package/dist/boat/prima/src/cli.js +220 -0
- package/dist/boat/prima/src/envelope.js +142 -0
- package/dist/boat/prima/src/prima.js +1031 -0
- package/dist/boat/prima/src/pw-parser.js +30 -0
- package/dist/boat/prima/src/pw-registry.js +65 -0
- package/dist/boat/prima/src/session-log.js +108 -0
- package/dist/models.json +3 -0
- package/dist/package.json +8 -3
- 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 +7 -2
- package/dist/src/action.js +41 -6
- package/dist/src/ai/captain/mixin.js +3 -4
- package/dist/src/ai/captain/web-mode.js +7 -4
- package/dist/src/ai/captain.js +2 -0
- package/dist/src/ai/navigator.d.ts +11 -0
- package/dist/src/ai/navigator.js +21 -12
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +48 -2
- package/dist/src/ai/planner.d.ts +1 -0
- package/dist/src/ai/planner.js +6 -0
- package/dist/src/ai/provider.js +2 -2
- package/dist/src/ai/rerunner.js +2 -1
- package/dist/src/ai/researcher.js +2 -2
- package/dist/src/ai/rules.js +17 -10
- package/dist/src/ai/task-agent.js +5 -2
- package/dist/src/ai/tester.d.ts +1 -0
- package/dist/src/ai/tester.js +33 -20
- package/dist/src/ai/tools.d.ts +14 -0
- package/dist/src/ai/tools.js +113 -36
- package/dist/src/application-spec-contract.d.ts +8 -0
- package/dist/src/application-spec-contract.js +8 -0
- package/dist/src/application-spec.d.ts +15 -0
- package/dist/src/application-spec.js +71 -0
- package/dist/src/browser-server.d.ts +12 -6
- package/dist/src/browser-server.js +74 -19
- package/dist/src/commands/clean-command.js +2 -7
- package/dist/src/commands/init-command.d.ts +5 -0
- package/dist/src/commands/init-command.js +119 -1
- package/dist/src/commands/navigate-command.js +1 -1
- package/dist/src/commands/research-command.js +1 -1
- package/dist/src/commands/sites-command.d.ts +6 -0
- package/dist/src/commands/sites-command.js +23 -0
- package/dist/src/components/InitWizard.d.ts +10 -0
- package/dist/src/components/InitWizard.js +133 -0
- package/dist/src/components/InputReadline.d.ts +1 -0
- package/dist/src/components/InputReadline.js +7 -4
- package/dist/src/config.d.ts +25 -5
- package/dist/src/config.js +158 -40
- package/dist/src/execution-controller.d.ts +2 -0
- package/dist/src/execution-controller.js +6 -0
- package/dist/src/explorbot.d.ts +11 -1
- package/dist/src/explorbot.js +27 -6
- package/dist/src/explorer.d.ts +4 -1
- package/dist/src/explorer.js +42 -9
- package/dist/src/global-config.d.ts +22 -0
- package/dist/src/global-config.js +117 -0
- package/dist/src/knowledge-tracker.d.ts +5 -1
- package/dist/src/knowledge-tracker.js +14 -1
- package/dist/src/playwright-recorder.js +30 -0
- package/dist/src/remote.d.ts +54 -0
- package/dist/src/remote.js +229 -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/stats.d.ts +1 -0
- package/dist/src/stats.js +1 -0
- package/dist/src/test-plan.d.ts +2 -0
- package/dist/src/test-plan.js +7 -0
- package/dist/src/utils/aria.d.ts +2 -8
- package/dist/src/utils/aria.js +69 -40
- package/dist/src/utils/cli-name.js +6 -2
- package/dist/src/utils/html.js +1 -0
- package/dist/src/utils/logger.d.ts +6 -0
- package/dist/src/utils/logger.js +24 -0
- package/dist/src/utils/page-readiness.js +18 -1
- package/dist/src/utils/test-files.js +1 -2
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +12 -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 +86 -0
- package/docs/reference/commands.md +994 -0
- package/docs/reference/configuration.md +569 -0
- package/docs/reference/scripting.md +303 -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/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/models.json +3 -0
- package/package.json +8 -3
- package/rules/navigator/verification-actions.md +20 -0
- package/src/action-result.ts +11 -0
- package/src/action.ts +47 -7
- package/src/ai/captain/mixin.ts +3 -3
- package/src/ai/captain/web-mode.ts +7 -4
- package/src/ai/captain.ts +3 -0
- package/src/ai/navigator.ts +26 -15
- package/src/ai/pilot.ts +53 -2
- package/src/ai/planner.ts +7 -0
- package/src/ai/provider.ts +2 -4
- package/src/ai/rerunner.ts +2 -1
- package/src/ai/researcher.ts +2 -2
- package/src/ai/rules.ts +17 -10
- package/src/ai/task-agent.ts +4 -2
- package/src/ai/tester.ts +32 -18
- package/src/ai/tools.ts +132 -36
- package/src/application-spec-contract.ts +10 -0
- package/src/application-spec.ts +87 -0
- package/src/browser-server.ts +74 -19
- package/src/commands/clean-command.ts +1 -6
- package/src/commands/init-command.ts +146 -1
- package/src/commands/navigate-command.ts +1 -1
- package/src/commands/research-command.ts +1 -1
- package/src/commands/sites-command.ts +27 -0
- package/src/components/InitWizard.tsx +166 -0
- package/src/components/InputReadline.tsx +8 -4
- package/src/config.ts +177 -43
- package/src/execution-controller.ts +8 -0
- package/src/explorbot.ts +34 -7
- package/src/explorer.ts +47 -9
- package/src/global-config.ts +148 -0
- package/src/knowledge-tracker.ts +17 -1
- package/src/playwright-recorder.ts +23 -0
- package/src/remote.ts +238 -0
- package/src/reporter.ts +7 -1
- package/src/state-manager.ts +2 -1
- package/src/stats.ts +1 -0
- package/src/test-plan.ts +9 -0
- package/src/utils/aria.ts +65 -45
- package/src/utils/cli-name.ts +5 -2
- package/src/utils/html.ts +1 -0
- package/src/utils/logger.ts +24 -1
- package/src/utils/page-readiness.ts +24 -1
- package/src/utils/test-files.ts +1 -2
- package/src/utils/url-matcher.ts +13 -0
- package/src/utils/web-element.ts +9 -0
- package/src/utils/web-sandbox.ts +3 -4
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const FUNCTION_SHAPE = /^(async\s+)?(function\b|\()/;
|
|
2
|
+
export function isFunctionExpression(expr) {
|
|
3
|
+
const trimmed = expr.trim();
|
|
4
|
+
if (!trimmed)
|
|
5
|
+
return { valid: false, error: 'empty expression; pass a function like ({ page }) => ...' };
|
|
6
|
+
if (!FUNCTION_SHAPE.test(trimmed))
|
|
7
|
+
return { valid: false, error: 'expression must be a function like ({ page }) => ... destructuring the playwright objects it needs' };
|
|
8
|
+
try {
|
|
9
|
+
new Function(`return (${trimmed})`);
|
|
10
|
+
}
|
|
11
|
+
catch (e) {
|
|
12
|
+
return { valid: false, error: `not a valid function expression: ${e.message}` };
|
|
13
|
+
}
|
|
14
|
+
return { valid: true };
|
|
15
|
+
}
|
|
16
|
+
export function toCodeceptWrapper(expr) {
|
|
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
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const DEFAULT_TITLE = 'default';
|
|
5
|
+
const DEFAULT_BROWSER = 'chromium';
|
|
6
|
+
export function registryDir() {
|
|
7
|
+
if (process.platform === 'darwin')
|
|
8
|
+
return path.join(os.homedir(), 'Library', 'Caches', 'ms-playwright', 'b');
|
|
9
|
+
if (process.platform === 'win32')
|
|
10
|
+
return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'ms-playwright', 'b');
|
|
11
|
+
return path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'), 'ms-playwright', 'b');
|
|
12
|
+
}
|
|
13
|
+
export function readDescriptors(dir = registryDir()) {
|
|
14
|
+
const descriptors = [];
|
|
15
|
+
for (const fileName of listFiles(dir)) {
|
|
16
|
+
const descriptor = parseDescriptor(path.join(dir, fileName));
|
|
17
|
+
if (descriptor)
|
|
18
|
+
descriptors.push(descriptor);
|
|
19
|
+
}
|
|
20
|
+
return descriptors;
|
|
21
|
+
}
|
|
22
|
+
export function selectDescriptor(descriptors, opts = {}) {
|
|
23
|
+
const candidates = descriptors;
|
|
24
|
+
if (!candidates.length)
|
|
25
|
+
return { candidates };
|
|
26
|
+
if (opts.title) {
|
|
27
|
+
const titled = candidates.find((descriptor) => descriptor.title === opts.title);
|
|
28
|
+
if (titled)
|
|
29
|
+
return { match: titled, candidates };
|
|
30
|
+
return { candidates };
|
|
31
|
+
}
|
|
32
|
+
const preferred = candidates.find((descriptor) => descriptor.title === DEFAULT_TITLE);
|
|
33
|
+
if (preferred)
|
|
34
|
+
return { match: preferred, candidates };
|
|
35
|
+
if (candidates.length === 1)
|
|
36
|
+
return { match: candidates[0], candidates };
|
|
37
|
+
return { candidates };
|
|
38
|
+
}
|
|
39
|
+
function listFiles(dir) {
|
|
40
|
+
try {
|
|
41
|
+
return readdirSync(dir);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function parseDescriptor(file) {
|
|
48
|
+
let data;
|
|
49
|
+
try {
|
|
50
|
+
data = JSON.parse(readFileSync(file, 'utf8'));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (!data?.endpoint || !data?.title)
|
|
56
|
+
return null;
|
|
57
|
+
return {
|
|
58
|
+
file,
|
|
59
|
+
title: data.title,
|
|
60
|
+
endpoint: data.endpoint,
|
|
61
|
+
workspaceDir: data.workspaceDir || '',
|
|
62
|
+
browserName: data.browser?.browserName || DEFAULT_BROWSER,
|
|
63
|
+
playwrightLib: data.playwrightLib || '',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -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/models.json
CHANGED
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"bin": {
|
|
17
|
-
"explorbot": "./dist/bin/explorbot-cli.js"
|
|
17
|
+
"explorbot": "./dist/bin/explorbot-cli.js",
|
|
18
|
+
"prima": "./dist/boat/prima/bin/prima-cli.js"
|
|
18
19
|
},
|
|
19
20
|
"files": [
|
|
20
21
|
"dist/",
|
|
22
|
+
"docs/",
|
|
21
23
|
"src/**/*.ts",
|
|
22
24
|
"src/**/*.tsx",
|
|
23
25
|
"bin/**/*.ts",
|
|
@@ -25,6 +27,9 @@
|
|
|
25
27
|
"boat/doc-collector/src/**/*.ts",
|
|
26
28
|
"boat/doc-collector/bin/**/*.ts",
|
|
27
29
|
"boat/doc-collector/package.json",
|
|
30
|
+
"boat/prima/src/**/*.ts",
|
|
31
|
+
"boat/prima/bin/**/*.ts",
|
|
32
|
+
"boat/prima/package.json",
|
|
28
33
|
"rules/",
|
|
29
34
|
"assets/sample-files/",
|
|
30
35
|
"models.json"
|
|
@@ -115,7 +120,7 @@
|
|
|
115
120
|
"ora-classic": "^5.4.2",
|
|
116
121
|
"parse5": "^8.0.0",
|
|
117
122
|
"pixelmatch": "^7.2.0",
|
|
118
|
-
"playwright": "^1.
|
|
123
|
+
"playwright": "^1.62",
|
|
119
124
|
"pngjs": "^7.0.0",
|
|
120
125
|
"react": "^19.1.1",
|
|
121
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;
|
|
@@ -24,7 +25,7 @@ declare class Action {
|
|
|
24
25
|
includeScreenshot?: boolean;
|
|
25
26
|
codeBlock?: string;
|
|
26
27
|
}): Promise<ActionResult>;
|
|
27
|
-
execute(code: string): Promise<Action>;
|
|
28
|
+
execute(code: string, opts?: ExecuteOptions): Promise<Action>;
|
|
28
29
|
captureOnce({ includeScreenshot, codeBlock }?: {
|
|
29
30
|
includeScreenshot?: boolean;
|
|
30
31
|
codeBlock?: string;
|
|
@@ -40,12 +41,16 @@ declare class Action {
|
|
|
40
41
|
id?: string;
|
|
41
42
|
}>>;
|
|
42
43
|
captureBrowserLogs(): Promise<any[]>;
|
|
43
|
-
executeOnce(code: string): Promise<Action>;
|
|
44
|
+
executeOnce(code: string, verbatim?: boolean): Promise<Action>;
|
|
44
45
|
attempt(codeBlock: string, originalMessage?: string): Promise<boolean>;
|
|
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;
|
|
51
53
|
export type RecoveryRunner = <T>(fn: () => Promise<T>) => Promise<T>;
|
|
54
|
+
export interface ExecuteOptions {
|
|
55
|
+
verbatim?: boolean;
|
|
56
|
+
}
|
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;
|
|
@@ -57,8 +60,8 @@ class Action {
|
|
|
57
60
|
async capturePageState(opts = {}) {
|
|
58
61
|
return this.recovery(() => this.captureOnce(opts));
|
|
59
62
|
}
|
|
60
|
-
async execute(code) {
|
|
61
|
-
return this.recovery(() => this.executeOnce(code));
|
|
63
|
+
async execute(code, opts = {}) {
|
|
64
|
+
return this.recovery(() => this.executeOnce(code, opts.verbatim));
|
|
62
65
|
}
|
|
63
66
|
async captureOnce({ includeScreenshot = false, codeBlock } = {}) {
|
|
64
67
|
try {
|
|
@@ -120,9 +123,11 @@ class Action {
|
|
|
120
123
|
const iframeSnapshots = await this.captureIframeSnapshots(html);
|
|
121
124
|
let ariaSnapshot = null;
|
|
122
125
|
let ariaSnapshotFile = undefined;
|
|
126
|
+
let focusedElement = null;
|
|
123
127
|
try {
|
|
124
128
|
const page = this.playwrightHelper.page;
|
|
125
129
|
ariaSnapshot = await page.locator('body').ariaSnapshot();
|
|
130
|
+
focusedElement = await page.evaluate(readFocusedElement);
|
|
126
131
|
}
|
|
127
132
|
catch (err) {
|
|
128
133
|
debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
|
|
@@ -145,6 +150,7 @@ class Action {
|
|
|
145
150
|
iframeSnapshots,
|
|
146
151
|
ariaSnapshot,
|
|
147
152
|
ariaSnapshotFile,
|
|
153
|
+
focusedElement,
|
|
148
154
|
iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
|
|
149
155
|
});
|
|
150
156
|
this.stateManager.updateState(result, codeBlock);
|
|
@@ -251,7 +257,7 @@ class Action {
|
|
|
251
257
|
return [];
|
|
252
258
|
}
|
|
253
259
|
}
|
|
254
|
-
async executeOnce(code) {
|
|
260
|
+
async executeOnce(code, verbatim = false) {
|
|
255
261
|
let error = null;
|
|
256
262
|
setActivity('🔎 Browsing...', 'action');
|
|
257
263
|
let codeString = code.replace(/^\(I\) => /, '').trim();
|
|
@@ -265,23 +271,26 @@ class Action {
|
|
|
265
271
|
const tracer = trace.getTracer('ai');
|
|
266
272
|
const stepSpan = activeSpan ? tracer.startSpan('codeceptjs.step', undefined, trace.setSpan(context.active(), activeSpan)) : null;
|
|
267
273
|
setStepSpanParent(stepSpan);
|
|
268
|
-
const sanitizedCode = sanitizeCodeBlock(codeString);
|
|
269
|
-
const isPlaywright = hasPlaywrightCommands(sanitizedCode);
|
|
274
|
+
const sanitizedCode = verbatim ? codeString : sanitizeCodeBlock(codeString);
|
|
275
|
+
const isPlaywright = !verbatim && hasPlaywrightCommands(sanitizedCode);
|
|
270
276
|
try {
|
|
271
277
|
debugLog('Executing action:', codeString);
|
|
272
278
|
if (!sanitizedCode) {
|
|
273
279
|
throw new Error('No valid I.* or page.* commands found in code block');
|
|
274
280
|
}
|
|
281
|
+
this.playwrightHelper?.page?.setDefaultTimeout(this.config.action?.timeout ?? DEFAULT_ACTION_TIMEOUT);
|
|
275
282
|
if (isPlaywright) {
|
|
276
283
|
const page = this.playwrightHelper.page;
|
|
277
284
|
await playwrightSandbox(page, sanitizedCode);
|
|
278
285
|
await sleep(this.config.action?.delay || 500);
|
|
279
286
|
}
|
|
280
287
|
else {
|
|
281
|
-
codeceptJSSandbox(this.actor, sanitizedCode);
|
|
288
|
+
const returned = codeceptJSSandbox(this.actor, sanitizedCode);
|
|
282
289
|
await recorder.add(() => sleep(this.config.action?.delay || 500));
|
|
283
290
|
await recorder.promise();
|
|
291
|
+
this.lastValue = await returned;
|
|
284
292
|
}
|
|
293
|
+
this.restorePageTimeout();
|
|
285
294
|
if (executedSteps.length > 0) {
|
|
286
295
|
codeString = executedSteps.join('\n');
|
|
287
296
|
}
|
|
@@ -300,6 +309,7 @@ class Action {
|
|
|
300
309
|
throw err;
|
|
301
310
|
}
|
|
302
311
|
finally {
|
|
312
|
+
this.restorePageTimeout();
|
|
303
313
|
detachMainDocumentResponse();
|
|
304
314
|
if (groupId)
|
|
305
315
|
await this.recorder.endAction();
|
|
@@ -346,6 +356,9 @@ class Action {
|
|
|
346
356
|
getActionResult() {
|
|
347
357
|
return this.actionResult;
|
|
348
358
|
}
|
|
359
|
+
restorePageTimeout() {
|
|
360
|
+
this.playwrightHelper?.page?.setDefaultTimeout(this.config.playwright.timeout ?? DEFAULT_PAGE_TIMEOUT);
|
|
361
|
+
}
|
|
349
362
|
async waitForPageReadiness(page) {
|
|
350
363
|
await waitForPageReadiness(page, {
|
|
351
364
|
timeout: this.config.playwright.waitForTimeout,
|
|
@@ -408,3 +421,25 @@ const detachStepLogger = (listener) => {
|
|
|
408
421
|
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, listener);
|
|
409
422
|
codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, listener);
|
|
410
423
|
};
|
|
424
|
+
const readFocusedElement = () => {
|
|
425
|
+
const el = document.activeElement;
|
|
426
|
+
if (!el || el === document.body)
|
|
427
|
+
return null;
|
|
428
|
+
const tag = el.tagName.toLowerCase();
|
|
429
|
+
const textish = new Set(['text', 'search', 'email', 'password', 'url', 'tel', 'number']);
|
|
430
|
+
let role = el.getAttribute('role') || tag;
|
|
431
|
+
if (tag === 'textarea' || el.isContentEditable)
|
|
432
|
+
role = 'textbox';
|
|
433
|
+
if (tag === 'input' && textish.has(el.type))
|
|
434
|
+
role = 'textbox';
|
|
435
|
+
if (tag === 'select')
|
|
436
|
+
role = 'combobox';
|
|
437
|
+
if (tag === 'a')
|
|
438
|
+
role = 'link';
|
|
439
|
+
const label = el.getAttribute('aria-label') || el.getAttribute('placeholder') || el.labels?.[0]?.textContent || el.textContent || '';
|
|
440
|
+
const focused = { role, name: label.trim().slice(0, 80) };
|
|
441
|
+
const value = el.value ?? el.textContent;
|
|
442
|
+
if (typeof value === 'string' && value)
|
|
443
|
+
focused.value = value.slice(0, 200);
|
|
444
|
+
return focused;
|
|
445
|
+
};
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { dirname } from 'node:path';
|
|
2
1
|
import { ConfigParser } from "../../config.js";
|
|
3
2
|
import { createDebug } from '../../utils/logger.js';
|
|
4
3
|
export const debugLog = createDebug('explorbot:captain');
|
|
5
4
|
export function resolveProjectRoot() {
|
|
6
|
-
const
|
|
7
|
-
if (!
|
|
5
|
+
const configParser = ConfigParser.getInstance();
|
|
6
|
+
if (!configParser.getConfigPath())
|
|
8
7
|
return null;
|
|
9
|
-
return
|
|
8
|
+
return configParser.getProjectRoot();
|
|
10
9
|
}
|
|
@@ -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({
|
|
@@ -25,7 +25,7 @@ export function WithWebMode(Base) {
|
|
|
25
25
|
execute: async ({ destination }) => {
|
|
26
26
|
try {
|
|
27
27
|
debugLog('navigate', destination);
|
|
28
|
-
await ctx.explorBot.
|
|
28
|
+
await ctx.explorBot.visit(destination);
|
|
29
29
|
const stateManager = ctx.explorBot.stateManager();
|
|
30
30
|
const state = stateManager.getCurrentState();
|
|
31
31
|
return { success: true, url: state?.url, title: state?.title };
|
|
@@ -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();
|
|
@@ -32,6 +32,10 @@ declare class Navigator implements Agent {
|
|
|
32
32
|
resolveState(message: string, actionResult: ActionResult, opts?: {
|
|
33
33
|
action?: Action;
|
|
34
34
|
expectedUrl?: string;
|
|
35
|
+
onAttempt?: (attempt: {
|
|
36
|
+
code: string;
|
|
37
|
+
error?: string;
|
|
38
|
+
}) => void;
|
|
35
39
|
}): Promise<boolean>;
|
|
36
40
|
buildExperienceTools(): {
|
|
37
41
|
learnExperience: unknown;
|
|
@@ -46,6 +50,8 @@ declare class Navigator implements Agent {
|
|
|
46
50
|
} | null>;
|
|
47
51
|
verifyState(message: string, actionResult: ActionResult): Promise<{
|
|
48
52
|
verified: boolean;
|
|
53
|
+
inexpressible: boolean;
|
|
54
|
+
results: AssertionResult[];
|
|
49
55
|
successfulCodes: string[];
|
|
50
56
|
assertionSteps: Array<{
|
|
51
57
|
name: string;
|
|
@@ -55,4 +61,9 @@ declare class Navigator implements Agent {
|
|
|
55
61
|
}>;
|
|
56
62
|
checkAlreadyVerified(aiResponse: string, actionResult: ActionResult): boolean;
|
|
57
63
|
}
|
|
64
|
+
export type AssertionResult = {
|
|
65
|
+
code: string;
|
|
66
|
+
passed: boolean;
|
|
67
|
+
proof: string[];
|
|
68
|
+
};
|
|
58
69
|
export { Navigator };
|