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,27 @@
|
|
|
1
|
+
const FUNCTION_SHAPE = /^(async\s+)?(function\b|\()/;
|
|
2
|
+
|
|
3
|
+
export function isFunctionExpression(expr: string): { valid: boolean; error?: string } {
|
|
4
|
+
const trimmed = expr.trim();
|
|
5
|
+
if (!trimmed) return { valid: false, error: 'empty expression; pass a function like ({ page }) => ...' };
|
|
6
|
+
if (!FUNCTION_SHAPE.test(trimmed)) return { valid: false, error: 'expression must be a function like ({ page }) => ... destructuring the playwright objects it needs' };
|
|
7
|
+
try {
|
|
8
|
+
new Function(`return (${trimmed})`);
|
|
9
|
+
} catch (e) {
|
|
10
|
+
return { valid: false, error: `not a valid function expression: ${(e as Error).message}` };
|
|
11
|
+
}
|
|
12
|
+
return { valid: true };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function toCodeceptWrapper(expr: string): string {
|
|
16
|
+
return `return I.usePlaywrightTo('pw', async (playwright) => (${expr.trim()})(playwright))`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function takePwValue(held: unknown): string | undefined {
|
|
20
|
+
if (held === undefined || held === null) return undefined;
|
|
21
|
+
if (typeof held === 'string') return held;
|
|
22
|
+
try {
|
|
23
|
+
return JSON.stringify(held, null, 2);
|
|
24
|
+
} catch {
|
|
25
|
+
return String(held);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TITLE = 'default';
|
|
6
|
+
const DEFAULT_BROWSER = 'chromium';
|
|
7
|
+
|
|
8
|
+
export function registryDir(): string {
|
|
9
|
+
if (process.platform === 'darwin') return path.join(os.homedir(), 'Library', 'Caches', 'ms-playwright', 'b');
|
|
10
|
+
if (process.platform === 'win32') 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
|
+
|
|
14
|
+
export function readDescriptors(dir = registryDir()): PwServerDescriptor[] {
|
|
15
|
+
const descriptors: PwServerDescriptor[] = [];
|
|
16
|
+
for (const fileName of listFiles(dir)) {
|
|
17
|
+
const descriptor = parseDescriptor(path.join(dir, fileName));
|
|
18
|
+
if (descriptor) descriptors.push(descriptor);
|
|
19
|
+
}
|
|
20
|
+
return descriptors;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function selectDescriptor(descriptors: PwServerDescriptor[], opts: { title?: string } = {}): { match?: PwServerDescriptor; candidates: PwServerDescriptor[] } {
|
|
24
|
+
const candidates = descriptors;
|
|
25
|
+
if (!candidates.length) return { candidates };
|
|
26
|
+
|
|
27
|
+
if (opts.title) {
|
|
28
|
+
const titled = candidates.find((descriptor) => descriptor.title === opts.title);
|
|
29
|
+
if (titled) return { match: titled, candidates };
|
|
30
|
+
return { candidates };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const preferred = candidates.find((descriptor) => descriptor.title === DEFAULT_TITLE);
|
|
34
|
+
if (preferred) return { match: preferred, candidates };
|
|
35
|
+
if (candidates.length === 1) return { match: candidates[0], candidates };
|
|
36
|
+
return { candidates };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function listFiles(dir: string): string[] {
|
|
40
|
+
try {
|
|
41
|
+
return readdirSync(dir);
|
|
42
|
+
} catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseDescriptor(file: string): PwServerDescriptor | null {
|
|
48
|
+
let data: any;
|
|
49
|
+
try {
|
|
50
|
+
data = JSON.parse(readFileSync(file, 'utf8'));
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!data?.endpoint || !data?.title) return null;
|
|
56
|
+
|
|
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
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface PwServerDescriptor {
|
|
68
|
+
file: string;
|
|
69
|
+
title: string;
|
|
70
|
+
endpoint: string;
|
|
71
|
+
workspaceDir: string;
|
|
72
|
+
browserName: string;
|
|
73
|
+
playwrightLib: string;
|
|
74
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { outputPath } from '../../../src/config.ts';
|
|
4
|
+
import { safeFilename } from '../../../src/utils/strings.ts';
|
|
5
|
+
import { type EnvelopeData, renderEnvelope } from './envelope.ts';
|
|
6
|
+
|
|
7
|
+
const EXPECTATION_STATUS = { passed: 'passed', failed: 'failed', unverified: 'skipped' };
|
|
8
|
+
|
|
9
|
+
export function sessionsDir(): string {
|
|
10
|
+
return outputPath('prima', 'sessions');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function sessionFile(key: string): string {
|
|
14
|
+
return path.join(sessionsDir(), `${safeFilename(key)}.jsonl`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function latestSessionFile(): string | null {
|
|
18
|
+
const dir = sessionsDir();
|
|
19
|
+
if (!existsSync(dir)) return null;
|
|
20
|
+
|
|
21
|
+
const files = readdirSync(dir)
|
|
22
|
+
.filter((name) => name.endsWith('.jsonl'))
|
|
23
|
+
.map((name) => path.join(dir, name))
|
|
24
|
+
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
|
25
|
+
|
|
26
|
+
return files[0] || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function recordCommand(file: string, session: SessionRun, envelope: EnvelopeData, durationMs: number): void {
|
|
30
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
31
|
+
if (recordedEndpoint(file) !== session.endpoint) writeFileSync(file, jsonLine({ action: 'createRun', endpoint: session.endpoint, params: { title: session.title } }), 'utf-8');
|
|
32
|
+
appendFileSync(file, jsonLine({ action: 'addTest', testId: commandTest(envelope, durationMs) }), 'utf-8');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function readSession(file: string): { title: string; tests: any[] } {
|
|
36
|
+
const entries = readEntries(file);
|
|
37
|
+
const run = entries.find((entry) => entry.action === 'createRun');
|
|
38
|
+
const tests = entries.filter((entry) => entry.action === 'addTest').map((entry) => entry.testId);
|
|
39
|
+
return { title: run?.params?.title || 'prima session', tests };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function commandTest(envelope: EnvelopeData, durationMs: number): any {
|
|
43
|
+
let status = 'failed';
|
|
44
|
+
if (envelope.ok) status = 'passed';
|
|
45
|
+
|
|
46
|
+
const test: any = {
|
|
47
|
+
rid: envelope.status,
|
|
48
|
+
title: envelope.command,
|
|
49
|
+
suite_title: 'Prima session',
|
|
50
|
+
status,
|
|
51
|
+
file: '<prima>',
|
|
52
|
+
description: [`page: ${envelope.page.url}`, envelope.stepFiles && `artifacts: ${envelope.stepFiles}`].filter(Boolean).join('\n'),
|
|
53
|
+
code: (envelope.used || []).join('\n'),
|
|
54
|
+
steps: commandSteps(envelope),
|
|
55
|
+
logs: renderEnvelope(envelope),
|
|
56
|
+
time: Math.round(durationMs),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (envelope.failure) test.error = { message: envelope.failure.error };
|
|
60
|
+
return test;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function commandSteps(envelope: EnvelopeData): any[] {
|
|
64
|
+
const steps: any[] = [];
|
|
65
|
+
|
|
66
|
+
for (const step of envelope.steps || []) {
|
|
67
|
+
const entry: any = { category: 'user', title: step.label, status: 'passed', duration: 0, log: step.proof };
|
|
68
|
+
if (!step.ok) {
|
|
69
|
+
entry.status = 'failed';
|
|
70
|
+
entry.error = step.proof;
|
|
71
|
+
}
|
|
72
|
+
steps.push(entry);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const expectation of envelope.expectations || []) {
|
|
76
|
+
const entry: any = { category: 'user', title: `expected: ${expectation.text}`, status: EXPECTATION_STATUS[expectation.status], duration: 0 };
|
|
77
|
+
if (expectation.status === 'failed') entry.error = 'the run did not reach this outcome';
|
|
78
|
+
steps.push(entry);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const assertion of envelope.assertions || []) {
|
|
82
|
+
const entry: any = { category: 'user', title: assertion.code, status: 'passed', duration: 0, log: assertion.proof.join('\n') };
|
|
83
|
+
if (!assertion.passed) {
|
|
84
|
+
entry.status = 'failed';
|
|
85
|
+
entry.error = 'the assertion did not hold on the page';
|
|
86
|
+
}
|
|
87
|
+
steps.push(entry);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return steps;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function recordedEndpoint(file: string): string | null {
|
|
94
|
+
const [first] = readEntries(file);
|
|
95
|
+
return first?.endpoint || null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readEntries(file: string): any[] {
|
|
99
|
+
if (!existsSync(file)) return [];
|
|
100
|
+
|
|
101
|
+
const entries: any[] = [];
|
|
102
|
+
for (const line of readFileSync(file, 'utf-8').split('\n')) {
|
|
103
|
+
if (!line.trim()) continue;
|
|
104
|
+
const entry = parseLine(line);
|
|
105
|
+
if (entry) entries.push(entry);
|
|
106
|
+
}
|
|
107
|
+
return entries;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseLine(line: string): any {
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(line);
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function jsonLine(entry: unknown): string {
|
|
119
|
+
return `${JSON.stringify(entry)}\n`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface SessionRun {
|
|
123
|
+
key: string;
|
|
124
|
+
endpoint: string;
|
|
125
|
+
title: string;
|
|
126
|
+
}
|
|
@@ -11,6 +11,7 @@ import { App } from '../src/components/App.js';
|
|
|
11
11
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
12
12
|
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
13
13
|
import { ExplorBot } from '../src/explorbot.js';
|
|
14
|
+
import { remote } from '../src/remote.js';
|
|
14
15
|
import { Stats } from '../src/stats.js';
|
|
15
16
|
import { Plan } from '../src/test-plan.js';
|
|
16
17
|
import { getCliName } from "../src/utils/cli-name.js";
|
|
@@ -23,7 +24,8 @@ const cli = getCliName();
|
|
|
23
24
|
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../package.json');
|
|
24
25
|
const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
|
|
25
26
|
program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
|
|
26
|
-
|
|
27
|
+
remote.registerOption(program);
|
|
28
|
+
if (!process.env.EXPLORBOT_NO_BANNER && !process.argv.includes('prima')) {
|
|
27
29
|
console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
|
|
28
30
|
}
|
|
29
31
|
function buildExplorBotOptions(from, options) {
|
|
@@ -36,6 +38,7 @@ function buildExplorBotOptions(from, options) {
|
|
|
36
38
|
headless: options.headless,
|
|
37
39
|
incognito: options.incognito,
|
|
38
40
|
session: options.session,
|
|
41
|
+
applicationSpec: options.spec,
|
|
39
42
|
};
|
|
40
43
|
}
|
|
41
44
|
function addCommonOptions(cmd) {
|
|
@@ -47,6 +50,7 @@ function addCommonOptions(cmd) {
|
|
|
47
50
|
.option('-s, --show', 'Show browser window')
|
|
48
51
|
.option('--headless', 'Run browser in headless mode')
|
|
49
52
|
.option('--incognito', 'Run without recording experiences')
|
|
53
|
+
.option('--spec <path>', 'Use a Docbot application spec directory or index.md')
|
|
50
54
|
.option('--session [file]', 'Save/restore browser session from file');
|
|
51
55
|
}
|
|
52
56
|
async function startTUI(explorBot) {
|
|
@@ -72,6 +76,10 @@ async function startTUI(explorBot) {
|
|
|
72
76
|
});
|
|
73
77
|
}
|
|
74
78
|
async function showStatsAndExit(code) {
|
|
79
|
+
if (remote.isAttached()) {
|
|
80
|
+
await remote.close(code);
|
|
81
|
+
process.exit(code);
|
|
82
|
+
}
|
|
75
83
|
if (Stats.hasActivity()) {
|
|
76
84
|
await new Promise((resolve) => {
|
|
77
85
|
const { unmount } = render(React.createElement(StatusPane, {
|
|
@@ -341,6 +349,13 @@ program
|
|
|
341
349
|
process.exit(1);
|
|
342
350
|
}
|
|
343
351
|
});
|
|
352
|
+
program
|
|
353
|
+
.command('sites')
|
|
354
|
+
.description('List sites registered in the global installation')
|
|
355
|
+
.action(async () => {
|
|
356
|
+
const { SitesCommand } = await import('../src/commands/sites-command.js');
|
|
357
|
+
await new SitesCommand(new ExplorBot()).execute('');
|
|
358
|
+
});
|
|
344
359
|
addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests with AI auto-healing')).action(async (filename, index, options) => {
|
|
345
360
|
try {
|
|
346
361
|
const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
|
|
@@ -373,17 +388,29 @@ addCommonOptions(program
|
|
|
373
388
|
});
|
|
374
389
|
program
|
|
375
390
|
.command('init')
|
|
376
|
-
.description('Initialize a
|
|
377
|
-
.option('-c, --config-path <path>', 'Path for the config file'
|
|
391
|
+
.description('Initialize configuration for a project or for this machine')
|
|
392
|
+
.option('-c, --config-path <path>', 'Path for the config file')
|
|
378
393
|
.option('-f, --force', 'Overwrite existing config file')
|
|
379
394
|
.option('-p, --path <path>', 'Working directory for initialization')
|
|
395
|
+
.option('-g, --global', 'Configure explorbot in ~/.explorbot to run from anywhere')
|
|
396
|
+
.option('--provider <name>', `AI provider for the global config: ${Object.keys(PROVIDERS).join(', ')}`)
|
|
397
|
+
.option('--api-key <key>', 'API key stored in ~/.explorbot/.env')
|
|
380
398
|
.action(async (options) => {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
399
|
+
try {
|
|
400
|
+
const { runInit } = await import('../src/commands/init-command.js');
|
|
401
|
+
await runInit({
|
|
402
|
+
configPath: options.configPath,
|
|
403
|
+
force: options.force,
|
|
404
|
+
path: options.path,
|
|
405
|
+
global: options.global,
|
|
406
|
+
provider: options.provider,
|
|
407
|
+
apiKey: options.apiKey,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
387
414
|
});
|
|
388
415
|
program
|
|
389
416
|
.command('clean [target]')
|
|
@@ -626,7 +653,7 @@ program
|
|
|
626
653
|
};
|
|
627
654
|
const explorBot = new ExplorBot(mainOptions);
|
|
628
655
|
await explorBot.start();
|
|
629
|
-
await explorBot.
|
|
656
|
+
await explorBot.visit(url);
|
|
630
657
|
const { ContextCommand } = await import('../src/commands/context-command.js');
|
|
631
658
|
const argParts = [];
|
|
632
659
|
if (options.full)
|
|
@@ -650,7 +677,7 @@ addCommonOptions(program.command('shell <url> <command>').description('Execute a
|
|
|
650
677
|
try {
|
|
651
678
|
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
652
679
|
await explorBot.start();
|
|
653
|
-
await explorBot.
|
|
680
|
+
await explorBot.visit(url);
|
|
654
681
|
const action = explorBot.getExplorer().action();
|
|
655
682
|
await action.execute(command);
|
|
656
683
|
log('Command executed successfully');
|
|
@@ -671,10 +698,11 @@ browserCmd
|
|
|
671
698
|
.description('Launch a persistent browser server')
|
|
672
699
|
.option('-s, --show', 'Launch browser in headed mode (visible window)')
|
|
673
700
|
.option('--headless', 'Launch browser in headless mode')
|
|
701
|
+
.option('--instance <name>', 'Named browser instance (lowercase letters, digits, dashes)')
|
|
674
702
|
.option('-c, --config <path>', 'Path to configuration file')
|
|
675
703
|
.option('-p, --path <path>', 'Working directory path')
|
|
676
704
|
.action(async (options) => {
|
|
677
|
-
const { launchServer, removeEndpointFile } = await import('../src/browser-server.js');
|
|
705
|
+
const { launchServer, removeEndpointFile, keepServerRunning } = await import('../src/browser-server.js');
|
|
678
706
|
await ConfigParser.getInstance().loadConfig({
|
|
679
707
|
config: options.config,
|
|
680
708
|
path: options.path,
|
|
@@ -688,45 +716,34 @@ browserCmd
|
|
|
688
716
|
const server = await launchServer({
|
|
689
717
|
browser: config.playwright.browser,
|
|
690
718
|
show,
|
|
719
|
+
}, options.instance);
|
|
720
|
+
await keepServerRunning(async () => {
|
|
721
|
+
await server.close();
|
|
722
|
+
removeEndpointFile(options.instance);
|
|
691
723
|
});
|
|
692
|
-
console.log('Browser server is running. Press Ctrl+C to stop.');
|
|
693
|
-
const cleanup = () => {
|
|
694
|
-
console.log('\nStopping browser server...');
|
|
695
|
-
server.close();
|
|
696
|
-
removeEndpointFile();
|
|
697
|
-
process.exit(0);
|
|
698
|
-
};
|
|
699
|
-
process.on('SIGINT', cleanup);
|
|
700
|
-
process.on('SIGTERM', cleanup);
|
|
701
724
|
});
|
|
702
725
|
browserCmd
|
|
703
726
|
.command('stop')
|
|
704
727
|
.description('Stop a running browser server')
|
|
728
|
+
.option('--instance <name>', 'Named browser instance (lowercase letters, digits, dashes)')
|
|
705
729
|
.option('-c, --config <path>', 'Path to configuration file')
|
|
706
730
|
.option('-p, --path <path>', 'Working directory path')
|
|
707
731
|
.action(async (options) => {
|
|
708
|
-
const {
|
|
732
|
+
const { stopServer } = await import('../src/browser-server.js');
|
|
709
733
|
await ConfigParser.getInstance().loadConfig({
|
|
710
734
|
config: options.config,
|
|
711
735
|
path: options.path,
|
|
712
736
|
});
|
|
713
|
-
|
|
714
|
-
if (!endpoint) {
|
|
737
|
+
if (!(await stopServer(options.instance))) {
|
|
715
738
|
console.log('No running browser server found.');
|
|
716
739
|
process.exit(0);
|
|
717
740
|
}
|
|
718
|
-
try {
|
|
719
|
-
const { chromium } = await import('playwright-core');
|
|
720
|
-
const browser = await chromium.connect(endpoint, { timeout: 3000 });
|
|
721
|
-
await browser.close();
|
|
722
|
-
}
|
|
723
|
-
catch { }
|
|
724
|
-
removeEndpointFile();
|
|
725
741
|
console.log('Browser server stopped.');
|
|
726
742
|
});
|
|
727
743
|
browserCmd
|
|
728
744
|
.command('status')
|
|
729
745
|
.description('Check if a browser server is running')
|
|
746
|
+
.option('--instance <name>', 'Named browser instance (lowercase letters, digits, dashes)')
|
|
730
747
|
.option('-c, --config <path>', 'Path to configuration file')
|
|
731
748
|
.option('-p, --path <path>', 'Working directory path')
|
|
732
749
|
.action(async (options) => {
|
|
@@ -735,7 +752,7 @@ browserCmd
|
|
|
735
752
|
config: options.config,
|
|
736
753
|
path: options.path,
|
|
737
754
|
});
|
|
738
|
-
const endpoint = await getAliveEndpoint();
|
|
755
|
+
const endpoint = await getAliveEndpoint(options.instance);
|
|
739
756
|
if (endpoint) {
|
|
740
757
|
console.log(`Browser server is running at: ${endpoint}`);
|
|
741
758
|
}
|
|
@@ -790,8 +807,10 @@ program
|
|
|
790
807
|
});
|
|
791
808
|
import { createApiCommands } from "../boat/api-tester/src/cli.js";
|
|
792
809
|
import { createDocsCommands } from "../boat/doc-collector/src/cli.js";
|
|
810
|
+
import { createPrimaCommands } from "../boat/prima/src/cli.js";
|
|
793
811
|
program.addCommand(createApiCommands('api'));
|
|
794
812
|
program.addCommand(createDocsCommands('docs'));
|
|
813
|
+
program.addCommand(createPrimaCommands('prima'));
|
|
795
814
|
const envHelp = () => {
|
|
796
815
|
const width = Math.max(...EXPLORBOT_ENV_VARS.map((v) => v.name.length));
|
|
797
816
|
const rows = EXPLORBOT_ENV_VARS.map((v) => ` ${v.name.padEnd(width)} ${v.description}`).join('\n');
|
|
@@ -30,7 +30,7 @@ export class ApiBot {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
async start() {
|
|
33
|
-
this.config = await this.configParser.loadConfig({ config: this.options.config, path: this.options.path });
|
|
33
|
+
this.config = await this.configParser.loadConfig({ config: this.options.config, path: this.options.path, endpoint: this.options.endpoint });
|
|
34
34
|
this.provider = new AIProvider(this.config.ai);
|
|
35
35
|
await this.provider.validateConnection();
|
|
36
36
|
this.apiClient = new ApiClient(this.config.api.baseEndpoint, this.config.api.headers || {}, {
|
|
@@ -86,11 +86,12 @@ export class ApiBot {
|
|
|
86
86
|
agentCurler() {
|
|
87
87
|
return (this.agents.curler ||= this.createAgent(({ ai, apiClient, requestState }) => new Curler(ai, apiClient, requestState, this.reporter)));
|
|
88
88
|
}
|
|
89
|
-
async plan(
|
|
89
|
+
async plan(target, opts = {}) {
|
|
90
90
|
if (opts.fresh) {
|
|
91
91
|
this.currentPlan = undefined;
|
|
92
92
|
this.agents.chief = undefined;
|
|
93
93
|
}
|
|
94
|
+
const endpoint = this.configParser.resolveEndpointPath(target);
|
|
94
95
|
const chief = this.agentChief();
|
|
95
96
|
const specDefinition = this.getEndpointDefinition(endpoint);
|
|
96
97
|
this.currentPlan = await chief.plan(endpoint, { style: opts.style, specDefinition });
|
|
@@ -40,7 +40,7 @@ export function createApiCommands(name = 'api') {
|
|
|
40
40
|
addCommonOptions(cmd.command('plan <endpoint>').description('Generate test plan for an API endpoint').option('--style <style>', 'Planning style: basename of a file in rules/chief/styles/').option('--fresh', 'Start planning from scratch')).action(async (endpoint, options) => {
|
|
41
41
|
setPreserveConsoleLogs(true);
|
|
42
42
|
try {
|
|
43
|
-
const bot = new ApiBot(buildOptions(options));
|
|
43
|
+
const bot = new ApiBot({ ...buildOptions(options), endpoint });
|
|
44
44
|
await bot.start();
|
|
45
45
|
await bot.plan(endpoint, { style: options.style, fresh: options.fresh });
|
|
46
46
|
const plan = bot.getCurrentPlan();
|
|
@@ -104,7 +104,7 @@ export function createApiCommands(name = 'api') {
|
|
|
104
104
|
addCommonOptions(cmd.command('explore <endpoint>').description('Full cycle: plan all styles, execute tests, re-plan')).action(async (endpoint, options) => {
|
|
105
105
|
setPreserveConsoleLogs(true);
|
|
106
106
|
try {
|
|
107
|
-
const bot = new ApiBot(buildOptions(options));
|
|
107
|
+
const bot = new ApiBot({ ...buildOptions(options), endpoint });
|
|
108
108
|
await bot.start();
|
|
109
109
|
const styles = Object.keys(getStyles());
|
|
110
110
|
let totalPassed = 0;
|
|
@@ -8,12 +8,15 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
};
|
|
9
9
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
10
10
|
import path, { resolve } from 'node:path';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
11
12
|
import { parseEnv } from 'node:util';
|
|
12
|
-
import { EXPLORBOT_CONFIG_PATHS, createModel, materializeKnowledge, resolveModel, resolveOutputRoot } from "../../../src/config.js";
|
|
13
|
+
import { ConfigMissingError, EXPLORBOT_CONFIG_PATHS, createModel, envConfigRequested, materializeKnowledge, missingConfigMessage, resolveConfigModels, resolveModel, resolveOutputRoot } from "../../../src/config.js";
|
|
14
|
+
import { findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from "../../../src/global-config.js";
|
|
13
15
|
export class ApibotConfigParser {
|
|
14
16
|
static instance;
|
|
15
17
|
config = null;
|
|
16
18
|
configPath = null;
|
|
19
|
+
site = null;
|
|
17
20
|
constructor() { }
|
|
18
21
|
static getInstance() {
|
|
19
22
|
if (!ApibotConfigParser.instance) {
|
|
@@ -34,6 +37,7 @@ export class ApibotConfigParser {
|
|
|
34
37
|
if (options?.path) {
|
|
35
38
|
process.chdir(resolve(options.path));
|
|
36
39
|
}
|
|
40
|
+
ApibotConfigParser.loadEnv(globalEnvPath());
|
|
37
41
|
ApibotConfigParser.loadEnv('.env');
|
|
38
42
|
const resolvedPath = options?.config || this.findConfigFile();
|
|
39
43
|
if (!resolvedPath) {
|
|
@@ -59,7 +63,12 @@ export class ApibotConfigParser {
|
|
|
59
63
|
};
|
|
60
64
|
}
|
|
61
65
|
this.config = this.mergeWithDefaults(loadedConfig);
|
|
66
|
+
await resolveConfigModels(this.config.ai);
|
|
62
67
|
this.configPath = resolvedPath;
|
|
68
|
+
this.site = null;
|
|
69
|
+
if (isGlobalConfigPath(resolvedPath)) {
|
|
70
|
+
this.enterGlobalMode(this.config, options?.endpoint);
|
|
71
|
+
}
|
|
63
72
|
this.validateConfig(this.config);
|
|
64
73
|
return this.config;
|
|
65
74
|
}
|
|
@@ -79,10 +88,23 @@ export class ApibotConfigParser {
|
|
|
79
88
|
}
|
|
80
89
|
getOutputDir() {
|
|
81
90
|
const config = this.getConfig();
|
|
91
|
+
return path.join(this.getProjectRoot(), config.dirs?.output || 'output');
|
|
92
|
+
}
|
|
93
|
+
getProjectRoot() {
|
|
94
|
+
if (this.site)
|
|
95
|
+
return this.site.dir;
|
|
82
96
|
const configPath = this.getConfigPath();
|
|
83
97
|
if (!configPath)
|
|
84
98
|
throw new Error('Config path not found');
|
|
85
|
-
return path.
|
|
99
|
+
return path.dirname(configPath);
|
|
100
|
+
}
|
|
101
|
+
resolveEndpointPath(endpoint) {
|
|
102
|
+
if (!this.site)
|
|
103
|
+
return endpoint;
|
|
104
|
+
const resolved = resolveSiteTarget(endpoint, this.site.url);
|
|
105
|
+
if (resolved.baseUrl !== this.site.url)
|
|
106
|
+
return endpoint;
|
|
107
|
+
return resolved.path;
|
|
86
108
|
}
|
|
87
109
|
getPlansDir() {
|
|
88
110
|
return path.join(this.getOutputDir(), 'plans');
|
|
@@ -92,21 +114,26 @@ export class ApibotConfigParser {
|
|
|
92
114
|
}
|
|
93
115
|
getKnowledgeDir() {
|
|
94
116
|
const config = this.getConfig();
|
|
95
|
-
|
|
96
|
-
if (!configPath)
|
|
97
|
-
throw new Error('Config path not found');
|
|
98
|
-
return path.join(path.dirname(configPath), config.dirs?.knowledge || 'knowledge');
|
|
117
|
+
return path.join(this.getProjectRoot(), config.dirs?.knowledge || 'knowledge');
|
|
99
118
|
}
|
|
100
119
|
ensureDirectory(dirPath) {
|
|
101
120
|
if (!existsSync(dirPath)) {
|
|
102
121
|
mkdirSync(dirPath, { recursive: true });
|
|
103
122
|
}
|
|
104
123
|
}
|
|
124
|
+
enterGlobalMode(config, endpoint) {
|
|
125
|
+
const site = resolveSiteTarget(endpoint);
|
|
126
|
+
this.site = registerSite(site.baseUrl);
|
|
127
|
+
config.dirs = { output: 'output', knowledge: 'knowledge' };
|
|
128
|
+
config.api = { ...config.api, baseEndpoint: site.baseUrl };
|
|
129
|
+
if (process.env.EXPLORBOT_API_SPEC)
|
|
130
|
+
config.api.spec = [process.env.EXPLORBOT_API_SPEC];
|
|
131
|
+
}
|
|
105
132
|
async loadEnvConfig() {
|
|
106
133
|
const provider = process.env.EXPLORBOT_AI_PROVIDER;
|
|
107
134
|
const modelSpec = process.env.EXPLORBOT_AI_MODEL;
|
|
108
135
|
if (!provider && !modelSpec) {
|
|
109
|
-
throw new
|
|
136
|
+
throw new ConfigMissingError(missingConfigMessage('apibot.config.js'));
|
|
110
137
|
}
|
|
111
138
|
if (modelSpec && !provider && !modelSpec.includes('/')) {
|
|
112
139
|
throw new Error('EXPLORBOT_AI_MODEL needs a provider — set EXPLORBOT_AI_PROVIDER, or write it as "provider/model-id"');
|
|
@@ -149,13 +176,16 @@ export class ApibotConfigParser {
|
|
|
149
176
|
if (existsSync(fullPath))
|
|
150
177
|
return fullPath;
|
|
151
178
|
}
|
|
152
|
-
|
|
179
|
+
if (envConfigRequested())
|
|
180
|
+
return null;
|
|
181
|
+
return findGlobalConfig();
|
|
153
182
|
}
|
|
154
183
|
async loadConfigModule(configPath) {
|
|
155
184
|
const ext = configPath.split('.').pop();
|
|
185
|
+
const moduleUrl = pathToFileURL(resolve(configPath)).href;
|
|
156
186
|
if (ext === 'ts') {
|
|
157
187
|
try {
|
|
158
|
-
return await import(__rewriteRelativeImportExtension(
|
|
188
|
+
return await import(__rewriteRelativeImportExtension(moduleUrl));
|
|
159
189
|
}
|
|
160
190
|
catch {
|
|
161
191
|
const require = (await import('node:module')).createRequire(import.meta.url);
|
|
@@ -163,7 +193,7 @@ export class ApibotConfigParser {
|
|
|
163
193
|
}
|
|
164
194
|
}
|
|
165
195
|
if (ext === 'js' || ext === 'mjs') {
|
|
166
|
-
return await import(__rewriteRelativeImportExtension(
|
|
196
|
+
return await import(__rewriteRelativeImportExtension(moduleUrl));
|
|
167
197
|
}
|
|
168
198
|
const content = readFileSync(configPath, 'utf8');
|
|
169
199
|
return JSON.parse(content);
|