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
|
@@ -55,13 +55,22 @@ class Documentarian {
|
|
|
55
55
|
const message = error instanceof Error ? error.message : String(error);
|
|
56
56
|
tag('warning').log(`Interactive documentation failed: ${message}.`);
|
|
57
57
|
if (meaningfulInteractions.length > 0) {
|
|
58
|
-
tag('info').log(`
|
|
59
|
-
return this.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
might: [],
|
|
58
|
+
tag('info').log(`Retrying static documentation while preserving ${meaningfulInteractions.length} observed interaction(s).`);
|
|
59
|
+
return this.documentStatic(state, research)
|
|
60
|
+
.then((documentation) => this.normalizeDocumentation({
|
|
61
|
+
...documentation,
|
|
63
62
|
interactions: meaningfulInteractions,
|
|
64
|
-
}, state, research)
|
|
63
|
+
}, state, research))
|
|
64
|
+
.catch((fallbackError) => {
|
|
65
|
+
const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
|
|
66
|
+
tag('warning').log(`Static documentation fallback failed: ${fallbackMessage}. Preserving observed interactions without AI summary.`);
|
|
67
|
+
return this.normalizeDocumentation({
|
|
68
|
+
summary: `Observed ${meaningfulInteractions.length} interaction(s); AI-generated summary was unavailable.`,
|
|
69
|
+
can: [],
|
|
70
|
+
might: [],
|
|
71
|
+
interactions: meaningfulInteractions,
|
|
72
|
+
}, state, research);
|
|
73
|
+
});
|
|
65
74
|
}
|
|
66
75
|
return this.documentStatic(state, research);
|
|
67
76
|
}
|
|
@@ -94,7 +103,7 @@ class Documentarian {
|
|
|
94
103
|
content: this.buildPrompt(state, `${research}${this.buildInteractionContext(interactions)}`),
|
|
95
104
|
},
|
|
96
105
|
];
|
|
97
|
-
const response = await this.provider.generateObject(messages,
|
|
106
|
+
const response = await this.provider.generateObject(messages, generatedPageDocumentationSchema, undefined, {
|
|
98
107
|
agentName: 'documentarian',
|
|
99
108
|
});
|
|
100
109
|
return this.normalizeDocumentation({
|
|
@@ -113,7 +122,7 @@ class Documentarian {
|
|
|
113
122
|
content: this.buildPrompt(state, research, simplified),
|
|
114
123
|
},
|
|
115
124
|
];
|
|
116
|
-
const response = await this.provider.generateObject(messages,
|
|
125
|
+
const response = await this.provider.generateObject(messages, generatedPageDocumentationSchema, undefined, {
|
|
117
126
|
agentName: 'documentarian',
|
|
118
127
|
});
|
|
119
128
|
return this.normalizeDocumentation(response.object, state, research);
|
|
@@ -230,10 +239,10 @@ class Documentarian {
|
|
|
230
239
|
normalized.interactions = undefined;
|
|
231
240
|
}
|
|
232
241
|
const qualityNotes = this.evaluateDocumentationQuality(normalized);
|
|
233
|
-
return {
|
|
242
|
+
return pageDocumentationSchema.parse({
|
|
234
243
|
...normalized,
|
|
235
244
|
qualityNotes,
|
|
236
|
-
};
|
|
245
|
+
});
|
|
237
246
|
}
|
|
238
247
|
evaluateDocumentationQuality(documentation) {
|
|
239
248
|
const notes = [];
|
|
@@ -296,30 +305,46 @@ const stateTransitionSchema = z.object({
|
|
|
296
305
|
action: z.string(),
|
|
297
306
|
before: z.string(),
|
|
298
307
|
after: z.string(),
|
|
299
|
-
targetUrl: z.string().
|
|
300
|
-
discoveredUrls: z.array(z.string()).
|
|
301
|
-
newCapabilities: z.array(z.string()).
|
|
308
|
+
targetUrl: z.string().optional(),
|
|
309
|
+
discoveredUrls: z.array(z.string()).optional(),
|
|
310
|
+
newCapabilities: z.array(z.string()).optional(),
|
|
302
311
|
element: z
|
|
303
312
|
.object({
|
|
304
313
|
role: z.string(),
|
|
305
314
|
name: z.string(),
|
|
306
315
|
section: z.string(),
|
|
307
|
-
container: z.string().
|
|
308
|
-
locator: z.string().
|
|
316
|
+
container: z.string().optional(),
|
|
317
|
+
locator: z.string().optional(),
|
|
309
318
|
})
|
|
310
|
-
.
|
|
319
|
+
.optional(),
|
|
311
320
|
changes: z
|
|
312
321
|
.object({
|
|
313
322
|
urlChanged: z.boolean(),
|
|
314
323
|
newElements: z.number(),
|
|
315
324
|
removedElements: z.number(),
|
|
316
325
|
})
|
|
317
|
-
.
|
|
326
|
+
.optional(),
|
|
327
|
+
targetState: z
|
|
328
|
+
.object({
|
|
329
|
+
kind: z.enum(['page', 'dialog', 'modal', 'section']),
|
|
330
|
+
label: z.string(),
|
|
331
|
+
url: z.string(),
|
|
332
|
+
})
|
|
333
|
+
.optional(),
|
|
334
|
+
screenshot: z
|
|
335
|
+
.object({
|
|
336
|
+
title: z.string(),
|
|
337
|
+
relativePath: z.string(),
|
|
338
|
+
})
|
|
339
|
+
.optional(),
|
|
318
340
|
});
|
|
319
|
-
const
|
|
341
|
+
const generatedPageDocumentationSchema = z.object({
|
|
320
342
|
summary: z.string(),
|
|
321
343
|
can: z.array(capabilitySchema),
|
|
322
344
|
might: z.array(capabilitySchema),
|
|
323
|
-
|
|
345
|
+
});
|
|
346
|
+
const pageDocumentationSchema = generatedPageDocumentationSchema.extend({
|
|
347
|
+
interactions: z.array(stateTransitionSchema).optional(),
|
|
348
|
+
qualityNotes: z.array(z.string()).optional(),
|
|
324
349
|
});
|
|
325
350
|
export { Documentarian };
|
|
@@ -47,6 +47,7 @@ export function createDocsCommands(name = 'docs') {
|
|
|
47
47
|
console.log(`Skipped ${result.skipped.length} page(s)`);
|
|
48
48
|
console.log(`Spec index: ${result.indexPath}`);
|
|
49
49
|
console.log(`Pages dir: ${path.join(result.outputDir, 'pages')}`);
|
|
50
|
+
console.log(`Use in Explorbot: npx explorbot start ${startPath} --spec "${result.outputDir}"`);
|
|
50
51
|
await bot.stop();
|
|
51
52
|
process.exit(0);
|
|
52
53
|
}
|
|
@@ -8,6 +8,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
};
|
|
9
9
|
import { existsSync, 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
13
|
import { ConfigParser } from "../../../src/config.js";
|
|
13
14
|
class DocbotConfigParser {
|
|
@@ -84,9 +85,10 @@ class DocbotConfigParser {
|
|
|
84
85
|
}
|
|
85
86
|
async loadConfigModule(configPath) {
|
|
86
87
|
const ext = configPath.split('.').pop();
|
|
88
|
+
const moduleUrl = pathToFileURL(resolve(configPath)).href;
|
|
87
89
|
if (ext === 'ts') {
|
|
88
90
|
try {
|
|
89
|
-
return await import(__rewriteRelativeImportExtension(
|
|
91
|
+
return await import(__rewriteRelativeImportExtension(moduleUrl));
|
|
90
92
|
}
|
|
91
93
|
catch {
|
|
92
94
|
const require = (await import('node:module')).createRequire(import.meta.url);
|
|
@@ -94,7 +96,7 @@ class DocbotConfigParser {
|
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
if (ext === 'js' || ext === 'mjs') {
|
|
97
|
-
return await import(__rewriteRelativeImportExtension(
|
|
99
|
+
return await import(__rewriteRelativeImportExtension(moduleUrl));
|
|
98
100
|
}
|
|
99
101
|
return JSON.parse(readFileSync(configPath, 'utf8'));
|
|
100
102
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import
|
|
2
|
+
import matter from 'gray-matter';
|
|
3
|
+
import { APPLICATION_SPEC_FORMAT, APPLICATION_SPEC_VERSION } from "../../../src/application-spec-contract.js";
|
|
3
4
|
import { normalizeInlineText } from "../../../src/utils/strings.js";
|
|
5
|
+
import { buildStateGraph, renderMermaidFromGraph, renderPageStateDiagram, renderStateMapFromGraph } from "./state-diagram.js";
|
|
4
6
|
function renderPageDocumentation(state, documentation, screenshots = []) {
|
|
5
7
|
const lines = [];
|
|
6
8
|
lines.push(`# ${state.url}`);
|
|
@@ -25,6 +27,13 @@ function renderPageDocumentation(state, documentation, screenshots = []) {
|
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
const interactions = documentation.interactions;
|
|
30
|
+
const pageStateDiagram = renderPageStateDiagram(state.title || state.url || 'page', state.url || '', interactions || []);
|
|
31
|
+
if (pageStateDiagram) {
|
|
32
|
+
lines.push('## State Map');
|
|
33
|
+
lines.push('');
|
|
34
|
+
lines.push(`\`\`\`mermaid\n${pageStateDiagram}\n\`\`\``);
|
|
35
|
+
lines.push('');
|
|
36
|
+
}
|
|
28
37
|
if (interactions && interactions.length > 0) {
|
|
29
38
|
lines.push('## State Transitions');
|
|
30
39
|
lines.push('');
|
|
@@ -83,7 +92,11 @@ function renderPageDocumentation(state, documentation, screenshots = []) {
|
|
|
83
92
|
}
|
|
84
93
|
lines.push('');
|
|
85
94
|
}
|
|
86
|
-
return `${lines.join('\n').trimEnd()}\n
|
|
95
|
+
return matter.stringify(`${lines.join('\n').trimEnd()}\n`, {
|
|
96
|
+
url: state.url,
|
|
97
|
+
format: APPLICATION_SPEC_FORMAT,
|
|
98
|
+
version: APPLICATION_SPEC_VERSION,
|
|
99
|
+
});
|
|
87
100
|
}
|
|
88
101
|
function renderSpecIndex(outputDir, startPath, pages, skipped, maxPages) {
|
|
89
102
|
const lines = [];
|
|
@@ -97,9 +110,10 @@ function renderSpecIndex(outputDir, startPath, pages, skipped, maxPages) {
|
|
|
97
110
|
lines.push(`Max pages: ${maxPages}`);
|
|
98
111
|
lines.push('');
|
|
99
112
|
const graph = buildStateGraph(outputDir, pages);
|
|
113
|
+
const mermaid = renderMermaidFromGraph(graph, true);
|
|
100
114
|
lines.push('## State Transitions');
|
|
101
115
|
lines.push('');
|
|
102
|
-
lines.push(`\`\`\`mermaid\n${
|
|
116
|
+
lines.push(`\`\`\`mermaid\n${mermaid}\n\`\`\``);
|
|
103
117
|
lines.push('');
|
|
104
118
|
const stateMap = renderStateMapFromGraph(graph);
|
|
105
119
|
if (stateMap) {
|
|
@@ -60,8 +60,11 @@ function buildStateGraph(outputDir, pages) {
|
|
|
60
60
|
if (!targetId) {
|
|
61
61
|
continue;
|
|
62
62
|
}
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
if (adjacency.get(sourceId)?.has(targetId)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (createsCycle(sourceId, targetId, adjacency)) {
|
|
67
|
+
const pairKey = `${sourceId}>${targetId}`;
|
|
65
68
|
if (drawnBack.has(pairKey)) {
|
|
66
69
|
continue;
|
|
67
70
|
}
|
|
@@ -69,9 +72,6 @@ function buildStateGraph(outputDir, pages) {
|
|
|
69
72
|
edges.push({ source: sourceId, target: targetId, action: transition.action, isBack: true });
|
|
70
73
|
continue;
|
|
71
74
|
}
|
|
72
|
-
if (adjacency.get(sourceId)?.has(targetId) || createsCycle(sourceId, targetId, adjacency)) {
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
75
|
adjacency.get(sourceId)?.add(targetId);
|
|
76
76
|
edges.push({ source: sourceId, target: targetId, action: transition.action, isBack: false });
|
|
77
77
|
}
|
|
@@ -90,8 +90,8 @@ function buildStateGraph(outputDir, pages) {
|
|
|
90
90
|
function renderMermaidBody(outputDir, pages) {
|
|
91
91
|
return renderMermaidFromGraph(buildStateGraph(outputDir, pages));
|
|
92
92
|
}
|
|
93
|
-
function renderMermaidFromGraph(graph) {
|
|
94
|
-
const lines = ['flowchart TD'];
|
|
93
|
+
function renderMermaidFromGraph(graph, compact = false) {
|
|
94
|
+
const lines = [compact ? 'flowchart LR' : 'flowchart TD'];
|
|
95
95
|
if (graph.pages.length === 0) {
|
|
96
96
|
lines.push(' empty["No documented states"]');
|
|
97
97
|
return lines.join('\n');
|
|
@@ -102,6 +102,12 @@ function renderMermaidFromGraph(graph) {
|
|
|
102
102
|
if (!children || children.length === 0) {
|
|
103
103
|
continue;
|
|
104
104
|
}
|
|
105
|
+
if (compact) {
|
|
106
|
+
for (const child of children) {
|
|
107
|
+
lines.push(` ${renderNodeLine(child)}`);
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
105
111
|
lines.push(` subgraph sg_${page.id} ["${escapeMermaidLabel(page.label)} — transient states"]`);
|
|
106
112
|
for (const child of children) {
|
|
107
113
|
lines.push(` ${renderNodeLine(child)}`);
|
|
@@ -109,11 +115,13 @@ function renderMermaidFromGraph(graph) {
|
|
|
109
115
|
lines.push(' end');
|
|
110
116
|
}
|
|
111
117
|
for (const edge of graph.edges) {
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
arrow
|
|
118
|
+
const arrow = edge.isBack ? '-.->' : '-->';
|
|
119
|
+
if (compact) {
|
|
120
|
+
lines.push(` ${edge.source} ${arrow} ${edge.target}`);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
lines.push(` ${edge.source} ${arrow}|"${escapeMermaidLabel(edge.action)}"| ${edge.target}`);
|
|
115
124
|
}
|
|
116
|
-
lines.push(` ${edge.source} ${arrow}|"${escapeMermaidLabel(edge.action)}"| ${edge.target}`);
|
|
117
125
|
}
|
|
118
126
|
lines.push(' classDef page fill:#dbeafe,stroke:#2563eb,color:#0f172a;');
|
|
119
127
|
lines.push(' classDef dialog fill:#ffedd5,stroke:#ea580c,color:#0f172a;');
|
|
@@ -153,6 +161,42 @@ function renderStateMapFromGraph(graph) {
|
|
|
153
161
|
}
|
|
154
162
|
return rows.join('\n');
|
|
155
163
|
}
|
|
164
|
+
function renderPageStateDiagram(label, url, interactions) {
|
|
165
|
+
const targets = new Map();
|
|
166
|
+
let index = 0;
|
|
167
|
+
for (const interaction of interactions) {
|
|
168
|
+
const targetState = interaction.targetState;
|
|
169
|
+
if (!targetState) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const key = `${targetState.kind}:${targetState.label}:${normalizeUrl(targetState.url)}`;
|
|
173
|
+
if (targets.has(key)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
targets.set(key, {
|
|
177
|
+
node: { id: `target${index++}`, kind: targetState.kind, label: targetState.label, subLabel: targetState.kind },
|
|
178
|
+
action: interaction.action,
|
|
179
|
+
screenshot: interaction.screenshot,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (targets.size === 0) {
|
|
183
|
+
return '';
|
|
184
|
+
}
|
|
185
|
+
const lines = ['flowchart LR'];
|
|
186
|
+
lines.push(` ${renderNodeLine({ id: 'self', kind: 'page', label, subLabel: url })}`);
|
|
187
|
+
for (const target of targets.values()) {
|
|
188
|
+
lines.push(` ${renderNodeLine(target.node)}`);
|
|
189
|
+
}
|
|
190
|
+
for (const target of targets.values()) {
|
|
191
|
+
lines.push(` self -->|"${escapeMermaidLabel(target.action)}"| ${target.node.id}`);
|
|
192
|
+
}
|
|
193
|
+
for (const target of targets.values()) {
|
|
194
|
+
if (target.screenshot) {
|
|
195
|
+
lines.push(` click ${target.node.id} "${target.screenshot.relativePath}" "${escapeMermaidLabel(target.screenshot.title)}"`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return lines.join('\n');
|
|
199
|
+
}
|
|
156
200
|
function renderNodeLine(node) {
|
|
157
201
|
const label = `${escapeMermaidLabel(node.label)}<br/>${escapeMermaidLabel(node.subLabel)}`;
|
|
158
202
|
if (node.kind === 'dialog' || node.kind === 'modal') {
|
|
@@ -183,7 +227,7 @@ function createsCycle(sourceId, targetId, adjacency) {
|
|
|
183
227
|
return false;
|
|
184
228
|
}
|
|
185
229
|
function escapeMermaidLabel(value) {
|
|
186
|
-
return normalizeInlineText(value).replaceAll('&', '&').replaceAll('"', '"').replaceAll('|', '|');
|
|
230
|
+
return normalizeInlineText(value).replaceAll('&', '&').replaceAll('"', '"').replaceAll('#', '#').replaceAll('<', '<').replaceAll('>', '>').replaceAll('|', '|');
|
|
187
231
|
}
|
|
188
232
|
function escapeTable(value) {
|
|
189
233
|
return normalizeInlineText(value).replaceAll('|', '\\|');
|
|
@@ -197,4 +241,4 @@ function classForKind(kind) {
|
|
|
197
241
|
}
|
|
198
242
|
return 'dialog';
|
|
199
243
|
}
|
|
200
|
-
export { buildStateGraph, renderMermaidBody, renderMermaidFromGraph, renderStateMapFromGraph };
|
|
244
|
+
export { buildStateGraph, renderMermaidBody, renderMermaidFromGraph, renderPageStateDiagram, renderStateMapFromGraph };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { addActivityListener, removeActivityListener } from "../../../src/activity.js";
|
|
3
|
+
import { isVerboseMode } from "../../../src/utils/logger.js";
|
|
4
|
+
const RESET_LINE = '\r\u001b[2K';
|
|
5
|
+
const stream = process.stderr;
|
|
6
|
+
let tracking = false;
|
|
7
|
+
export function trackActivityLine() {
|
|
8
|
+
if (tracking)
|
|
9
|
+
return;
|
|
10
|
+
if (!stream.isTTY)
|
|
11
|
+
return;
|
|
12
|
+
if (isVerboseMode())
|
|
13
|
+
return;
|
|
14
|
+
tracking = true;
|
|
15
|
+
addActivityListener(writeActivityLine);
|
|
16
|
+
}
|
|
17
|
+
export function clearActivityLine() {
|
|
18
|
+
if (!tracking)
|
|
19
|
+
return;
|
|
20
|
+
tracking = false;
|
|
21
|
+
removeActivityListener(writeActivityLine);
|
|
22
|
+
stream.write(RESET_LINE);
|
|
23
|
+
}
|
|
24
|
+
function writeActivityLine(activity) {
|
|
25
|
+
if (!activity)
|
|
26
|
+
return;
|
|
27
|
+
const width = (stream.columns || 80) - 2;
|
|
28
|
+
const message = Array.from(activity.message.replace(/\s+/g, ' ').trim()).slice(0, width).join('');
|
|
29
|
+
stream.write(`${RESET_LINE}${chalk.gray(message)}`);
|
|
30
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import dedent from 'dedent';
|
|
3
|
+
import { keepServerRunning } from "../../../src/browser-server.js";
|
|
4
|
+
import { browserErrorMessage } from "../../../src/utils/browser-errors.js";
|
|
5
|
+
import { isVerboseMode, setQuietMode } from "../../../src/utils/logger.js";
|
|
6
|
+
import { clearActivityLine, trackActivityLine } from "./activity-line.js";
|
|
7
|
+
import { renderEnvelope } from "./envelope.js";
|
|
8
|
+
import { Prima } from "./prima.js";
|
|
9
|
+
const helpContract = dedent `
|
|
10
|
+
Prima is a high-level AI extension to playwright-cli, driving the browser it has open.
|
|
11
|
+
|
|
12
|
+
playwright-cli open <url> starts the session
|
|
13
|
+
prima <command> ... drives it
|
|
14
|
+
playwright-cli close ends it
|
|
15
|
+
|
|
16
|
+
One call takes a whole job:
|
|
17
|
+
|
|
18
|
+
prima check "a workflow can be created and appears in the list" --expected "the new workflow is listed"
|
|
19
|
+
prima do "open the account menu" "choose the settings entry" "switch the theme to dark" "check it took effect"
|
|
20
|
+
prima pw "({ page }) => page.click('[data-test=submit]')"
|
|
21
|
+
`;
|
|
22
|
+
const checkHelp = dedent `
|
|
23
|
+
check takes an outcome rather than a click path, and works out how to reach it.
|
|
24
|
+
--expected one outcome the run must reach, repeatable for several. Without it the
|
|
25
|
+
scenario text is the single expected outcome. Each comes back under
|
|
26
|
+
### Expected outcomes as PASSED, FAILED or not verified - "not verified"
|
|
27
|
+
means the run never checked it, which is not the same as false.
|
|
28
|
+
Page problems seen on the way appear under ### Answer, not as step failures.
|
|
29
|
+
`;
|
|
30
|
+
const doHelp = dedent `
|
|
31
|
+
Each instruction is numbered and accounted for: ### Steps reports each as ok or FAIL
|
|
32
|
+
with what proved it. One that could not be carried out fails the command and says why.
|
|
33
|
+
Nothing runs past the last instruction given. A whole remaining sequence in one call is
|
|
34
|
+
what makes this tier cheap.
|
|
35
|
+
`;
|
|
36
|
+
const verifyHelp = dedent `
|
|
37
|
+
Reports each assertion it could express as PASSED or FAILED with its playwright form,
|
|
38
|
+
and gives no overall verdict - read the lines and decide. "none ran" means the claim
|
|
39
|
+
could not be expressed, which is not the same as false.
|
|
40
|
+
`;
|
|
41
|
+
const reportHelp = dedent `
|
|
42
|
+
Commands are logged as they run, so the report needs no browser and outlives the session.
|
|
43
|
+
The most recent session is reported unless --pw-session names another.
|
|
44
|
+
`;
|
|
45
|
+
const sessionHelp = dedent `
|
|
46
|
+
--endpoint <ep> attach to a browser server endpoint directly, skipping discovery
|
|
47
|
+
--instance <name> which prima-owned browser you talk to; parallel work needs one each
|
|
48
|
+
--session [file] cookies and storage persisted across processes; ignored while
|
|
49
|
+
attached, since the attached session keeps its own
|
|
50
|
+
--framework parsed but not active yet; reported code is CodeceptJS either way
|
|
51
|
+
DEBUG='explorbot:*' in front of a command prints the log of everything it does.
|
|
52
|
+
When no AI model is usable pw still works; for everything else drive playwright-cli.
|
|
53
|
+
`;
|
|
54
|
+
let rootOptions = () => ({});
|
|
55
|
+
function buildOptions(subcommand) {
|
|
56
|
+
const options = { ...rootOptions(), ...stripEmpty(subcommand) };
|
|
57
|
+
return {
|
|
58
|
+
config: options.config,
|
|
59
|
+
path: options.path,
|
|
60
|
+
instance: options.instance,
|
|
61
|
+
session: options.session,
|
|
62
|
+
ephemeral: options.ephemeral,
|
|
63
|
+
framework: options.framework,
|
|
64
|
+
noVision: options.vision === false,
|
|
65
|
+
url: options.url,
|
|
66
|
+
baseUrl: options.baseUrl,
|
|
67
|
+
show: options.show,
|
|
68
|
+
headless: options.headless,
|
|
69
|
+
endpoint: options.endpoint,
|
|
70
|
+
pwSession: options.pwSession,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function stripEmpty(options) {
|
|
74
|
+
const present = {};
|
|
75
|
+
for (const [key, value] of Object.entries(options || {})) {
|
|
76
|
+
if (value === undefined)
|
|
77
|
+
continue;
|
|
78
|
+
present[key] = value;
|
|
79
|
+
}
|
|
80
|
+
return present;
|
|
81
|
+
}
|
|
82
|
+
function addCommonOptions(cmd) {
|
|
83
|
+
return cmd
|
|
84
|
+
.option('-c, --config <path>', 'Path to explorbot configuration file')
|
|
85
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
86
|
+
.option('-i, --instance <name>', 'Browser instance to drive')
|
|
87
|
+
.option('--session [file]', 'Persist cookies and storage to a session file')
|
|
88
|
+
.option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
|
|
89
|
+
.option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
|
|
90
|
+
.option('--url <url>', 'Page to open when the session has no page yet')
|
|
91
|
+
.option('--endpoint <ep>', 'Websocket endpoint of a browser server to attach to, skipping discovery')
|
|
92
|
+
.option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
|
|
93
|
+
.addHelpText('after', `\n${sessionHelp}`);
|
|
94
|
+
}
|
|
95
|
+
function primaFor(options) {
|
|
96
|
+
if (options.ephemeral)
|
|
97
|
+
process.env.EXPLORBOT_EPHEMERAL = '1';
|
|
98
|
+
return new Prima(buildOptions(options));
|
|
99
|
+
}
|
|
100
|
+
async function runPrima(options, command, run, record = true) {
|
|
101
|
+
setQuietMode(!isVerboseMode());
|
|
102
|
+
trackActivityLine();
|
|
103
|
+
const prima = primaFor(options);
|
|
104
|
+
const startedAt = Date.now();
|
|
105
|
+
let envelope;
|
|
106
|
+
try {
|
|
107
|
+
await prima.start();
|
|
108
|
+
envelope = await run(prima);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
envelope = await prima.toolFailureEnvelope(command, error);
|
|
112
|
+
}
|
|
113
|
+
if (record)
|
|
114
|
+
prima.record(envelope, Date.now() - startedAt);
|
|
115
|
+
clearActivityLine();
|
|
116
|
+
console.log(renderEnvelope(envelope));
|
|
117
|
+
await prima.stop().catch(() => { });
|
|
118
|
+
process.exit(envelope.ok ? 0 : 1);
|
|
119
|
+
}
|
|
120
|
+
async function runBrowser(options, run) {
|
|
121
|
+
let ok = false;
|
|
122
|
+
try {
|
|
123
|
+
ok = await run(primaFor(options));
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
console.error(browserErrorMessage(error));
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
process.exit(ok ? 0 : 1);
|
|
130
|
+
}
|
|
131
|
+
export function createPrimaCommands(name = 'prima') {
|
|
132
|
+
const cmd = new Command(name);
|
|
133
|
+
cmd.description('Tests and drives a web app through described behaviour instead of locators: one command carries a whole scenario, verifies it, and reports the proof');
|
|
134
|
+
cmd.option('--pw-session <title>', 'Title of the playwright-cli session to attach to');
|
|
135
|
+
cmd.option('--url <url>', 'Page to open when the session has no page yet');
|
|
136
|
+
cmd.addHelpText('after', `\n${helpContract}`);
|
|
137
|
+
rootOptions = () => cmd.opts();
|
|
138
|
+
addCommonOptions(cmd.command('pw <fn>').description('Run a Playwright function expression against the open page')).action(async (fn, options) => {
|
|
139
|
+
await runPrima(options, `pw ${fn}`, (prima) => prima.pw(fn));
|
|
140
|
+
});
|
|
141
|
+
addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction'))
|
|
142
|
+
.addHelpText('after', `\n${doHelp}`)
|
|
143
|
+
.action(async (instructions, options) => {
|
|
144
|
+
await runPrima(options, `do ${instructions.join(' ')}`, (prima) => prima.do(instructions));
|
|
145
|
+
});
|
|
146
|
+
addCommonOptions(cmd.command('check <scenario>').description('Run a scenario end to end as a test, with its own verification, and report the steps it took'))
|
|
147
|
+
.option('--expected <outcome>', 'An outcome the run must reach; repeat the flag for several', (value, all) => [...all, value], [])
|
|
148
|
+
.addHelpText('after', `\n${checkHelp}`)
|
|
149
|
+
.action(async (scenario, options) => {
|
|
150
|
+
await runPrima(options, `check ${scenario}`, (prima) => prima.check(scenario, options.expected));
|
|
151
|
+
});
|
|
152
|
+
addCommonOptions(cmd.command('ask <question>').description('Answer a question about the current page').option('--no-vision', 'Answer from page structure only, without a screenshot')).action(async (question, options) => {
|
|
153
|
+
await runPrima(options, `ask ${question}`, (prima) => prima.ask(question));
|
|
154
|
+
});
|
|
155
|
+
addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page'))
|
|
156
|
+
.addHelpText('after', `\n${verifyHelp}`)
|
|
157
|
+
.action(async (assertion, options) => {
|
|
158
|
+
await runPrima(options, `verify ${assertion}`, (prima) => prima.verify(assertion));
|
|
159
|
+
});
|
|
160
|
+
addCommonOptions(cmd.command('research').description('Map the current page and return verified locators').option('--data', 'Include data extraction in the map').option('--deep', 'Expand hidden elements for a deeper map').option('--fresh', 'Ignore the cached map and research the page again')).action(async (options) => {
|
|
161
|
+
await runPrima(options, 'research', (prima) => prima.research({ data: options.data, deep: options.deep, fresh: options.fresh }));
|
|
162
|
+
});
|
|
163
|
+
addCommonOptions(cmd.command('go <target>').description('Navigate to a url, a path, or a page described in plain words')).action(async (target, options) => {
|
|
164
|
+
if (URL.canParse(target))
|
|
165
|
+
options.baseUrl = target;
|
|
166
|
+
await runPrima(options, `go ${target}`, (prima) => prima.go(target));
|
|
167
|
+
});
|
|
168
|
+
addCommonOptions(cmd.command('config').description('Show the AI models prima runs on and the config file they come from')).action(async (options) => {
|
|
169
|
+
setQuietMode(!isVerboseMode());
|
|
170
|
+
const prima = primaFor(options);
|
|
171
|
+
console.log(await prima.config().catch((error) => browserErrorMessage(error)));
|
|
172
|
+
await prima.stop().catch(() => { });
|
|
173
|
+
process.exit(0);
|
|
174
|
+
});
|
|
175
|
+
addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command')).action(async (hash, options) => {
|
|
176
|
+
await runPrima(options, `status ${hash}`, (prima) => prima.status(hash), false);
|
|
177
|
+
});
|
|
178
|
+
addCommonOptions(cmd.command('report').description('Turn every command of a session into one html and markdown report'))
|
|
179
|
+
.addHelpText('after', `\n${reportHelp}`)
|
|
180
|
+
.action(async (options) => {
|
|
181
|
+
setQuietMode(!isVerboseMode());
|
|
182
|
+
console.log(await primaFor(options)
|
|
183
|
+
.report()
|
|
184
|
+
.catch((error) => browserErrorMessage(error)));
|
|
185
|
+
process.exit(0);
|
|
186
|
+
});
|
|
187
|
+
const browser = cmd.command('browser').description('Manage the browsers prima drives');
|
|
188
|
+
addCommonOptions(browser.command('start').description('Start a prima-owned browser and hold it open until Ctrl+C'))
|
|
189
|
+
.option('-s, --show', 'Launch the browser in a visible window')
|
|
190
|
+
.option('--headless', 'Launch the browser without a window')
|
|
191
|
+
.action(async (options) => {
|
|
192
|
+
await runBrowser(options, async (prima) => {
|
|
193
|
+
await prima.browserStart();
|
|
194
|
+
console.log(await prima.browserStatus());
|
|
195
|
+
return keepServerRunning(() => prima.browserStop());
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
addCommonOptions(browser.command('stop').description('Stop the browser of this instance'))
|
|
199
|
+
.option('--all', 'Stop every running instance')
|
|
200
|
+
.action(async (options) => {
|
|
201
|
+
await runBrowser(options, async (prima) => {
|
|
202
|
+
const stopped = await prima.browserStop(options.all);
|
|
203
|
+
console.log(await prima.browserStatus());
|
|
204
|
+
return stopped;
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
addCommonOptions(browser.command('status').description('Report the browser of this instance')).action(async (options) => {
|
|
208
|
+
await runBrowser(options, async (prima) => {
|
|
209
|
+
console.log(await prima.browserStatus());
|
|
210
|
+
return true;
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
addCommonOptions(browser.command('list').description('List every browser instance that is running')).action(async (options) => {
|
|
214
|
+
await runBrowser(options, async (prima) => {
|
|
215
|
+
console.log(await prima.browserList());
|
|
216
|
+
return true;
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
return cmd;
|
|
220
|
+
}
|