explorbot 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/explorbot-cli.ts +13 -6
- package/boat/api-tester/src/ai/chief.ts +7 -1
- package/boat/api-tester/src/ai/curler-tools.ts +1 -1
- package/boat/api-tester/src/ai/curler.ts +7 -1
- package/boat/api-tester/src/apibot.ts +10 -4
- package/boat/api-tester/src/cli.ts +15 -2
- package/boat/api-tester/src/config.ts +28 -8
- package/boat/doc-collector/bin/doc-collector-cli.ts +3 -2
- package/boat/doc-collector/src/ai/documentarian.ts +57 -5
- package/boat/doc-collector/src/ai/tools.ts +1 -1
- package/boat/doc-collector/src/cli.ts +21 -3
- package/boat/doc-collector/src/config.ts +3 -0
- package/boat/doc-collector/src/docbot.ts +57 -12
- package/boat/doc-collector/src/docs-renderer.ts +79 -55
- package/boat/doc-collector/src/interaction-screenshots.ts +19 -10
- package/boat/doc-collector/src/screenshots.ts +106 -3
- package/boat/doc-collector/src/template-dedup.ts +31 -0
- package/boat/prima/bin/prima-cli.ts +2 -0
- package/boat/prima/src/cli.ts +5 -0
- package/boat/prima/src/prima.ts +3 -4
- package/dist/bin/explorbot-cli.js +14 -6
- package/dist/boat/api-tester/bin/apibot-cli.js +3 -2
- package/dist/boat/api-tester/src/ai/chief.js +6 -1
- package/dist/boat/api-tester/src/ai/curler-tools.js +1 -1
- package/dist/boat/api-tester/src/ai/curler.js +6 -1
- package/dist/boat/api-tester/src/apibot.js +7 -3
- package/dist/boat/api-tester/src/cli.js +14 -2
- package/dist/boat/api-tester/src/config.js +31 -8
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +3 -2
- package/dist/boat/doc-collector/src/ai/documentarian.js +62 -4
- package/dist/boat/doc-collector/src/ai/tools.js +1 -1
- package/dist/boat/doc-collector/src/cli.js +18 -3
- package/dist/boat/doc-collector/src/config.js +1 -0
- package/dist/boat/doc-collector/src/docbot.js +53 -11
- package/dist/boat/doc-collector/src/docs-renderer.js +69 -46
- package/dist/boat/doc-collector/src/interaction-screenshots.js +18 -11
- package/dist/boat/doc-collector/src/screenshots.js +104 -1
- package/dist/boat/doc-collector/src/template-dedup.js +27 -0
- package/dist/boat/prima/bin/prima-cli.js +2 -0
- package/dist/boat/prima/src/cli.js +5 -0
- package/dist/boat/prima/src/prima.js +3 -4
- package/dist/package.json +1 -1
- package/dist/rules/planner/styles/normal.md +1 -1
- package/dist/src/action-result.d.ts +2 -2
- package/dist/src/action-result.js +15 -17
- package/dist/src/action.d.ts +1 -1
- package/dist/src/action.js +15 -15
- package/dist/src/ai/captain.js +1 -1
- package/dist/src/ai/fisherman/request-haul.d.ts +11 -0
- package/dist/src/ai/fisherman/request-haul.js +28 -0
- package/dist/src/ai/fisherman-tools.d.ts +10 -1
- package/dist/src/ai/fisherman-tools.js +35 -31
- package/dist/src/ai/fisherman.d.ts +3 -2
- package/dist/src/ai/fisherman.js +8 -6
- package/dist/src/ai/navigator.js +1 -1
- package/dist/src/ai/pilot.js +21 -16
- package/dist/src/ai/planner.js +9 -7
- package/dist/src/ai/provider.js +8 -14
- package/dist/src/ai/researcher/coordinates.d.ts +2 -0
- package/dist/src/ai/researcher/coordinates.js +13 -1
- package/dist/src/ai/researcher/deep-analysis.js +9 -9
- package/dist/src/ai/researcher/fingerprint-worker.js +1 -11
- package/dist/src/ai/researcher.js +3 -1
- package/dist/src/ai/rules.js +11 -3
- package/dist/src/ai/tester.js +20 -20
- package/dist/src/ai/tools.d.ts +1 -1
- package/dist/src/ai/tools.js +18 -74
- package/dist/src/api/request-store.d.ts +1 -5
- package/dist/src/api/request-store.js +12 -22
- package/dist/src/api/spec-reader.js +1 -1
- package/dist/src/commands/config-command.js +3 -10
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/explore-command.js +12 -1
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/init-command.js +3 -7
- package/dist/src/commands/options/base-option.d.ts +8 -0
- package/dist/src/commands/options/base-option.js +12 -0
- package/dist/src/commands/options/index.d.ts +5 -0
- package/dist/src/commands/options/index.js +5 -0
- package/dist/src/commands/options/knowledge-option.d.ts +7 -0
- package/dist/src/commands/options/knowledge-option.js +12 -0
- package/dist/src/commands/options/ws-option.d.ts +7 -0
- package/dist/src/commands/options/ws-option.js +21 -0
- package/dist/src/commands/plans-command.d.ts +4 -1
- package/dist/src/commands/plans-command.js +4 -4
- package/dist/src/commands/recommended-models-command.d.ts +9 -0
- package/dist/src/commands/recommended-models-command.js +85 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +18 -2
- package/dist/src/experience-tracker.js +1 -1
- package/dist/src/explorbot.js +4 -4
- package/dist/src/global-config.d.ts +2 -0
- package/dist/src/global-config.js +6 -0
- package/dist/src/knowledge-tracker.d.ts +20 -7
- package/dist/src/knowledge-tracker.js +69 -31
- package/dist/src/remote.d.ts +0 -3
- package/dist/src/remote.js +0 -18
- package/dist/src/state-manager.d.ts +3 -3
- package/dist/src/state-manager.js +8 -8
- package/dist/src/test-plan.d.ts +1 -0
- package/dist/src/test-plan.js +13 -9
- package/dist/src/utils/aria.d.ts +1 -1
- package/dist/src/utils/aria.js +23 -2
- package/dist/src/utils/cli-section.d.ts +1 -0
- package/dist/src/utils/cli-section.js +12 -0
- package/dist/src/utils/html-diff.d.ts +1 -0
- package/dist/src/utils/html-diff.js +5 -5
- package/dist/src/utils/html.d.ts +2 -0
- package/dist/src/utils/html.js +5 -4
- package/dist/src/utils/overlay.d.ts +19 -55
- package/dist/src/utils/overlay.js +94 -148
- package/dist/src/utils/region.d.ts +41 -0
- package/dist/src/utils/region.js +99 -0
- package/dist/src/utils/similarity.d.ts +1 -0
- package/dist/src/utils/similarity.js +11 -0
- package/dist/src/utils/url-matcher.d.ts +1 -1
- package/dist/src/utils/url-matcher.js +4 -4
- package/docs/api-testing/basics.md +15 -0
- package/docs/api-testing/planning.md +10 -1
- package/docs/doc-collection/basics.md +11 -5
- package/docs/doc-collection/crawling.md +21 -1
- package/docs/reference/commands.md +27 -4
- package/docs/superpowers/plans/2026-09-03-model-resolves-click-ambiguity.md +509 -0
- package/docs/workflow/agentic-usage.md +11 -2
- package/docs/workflow/application-spec.md +4 -0
- package/docs/workflow/knowledge.md +46 -2
- package/package.json +1 -1
- package/rules/planner/styles/normal.md +1 -1
- package/src/action-result.ts +15 -16
- package/src/action.ts +16 -15
- package/src/ai/captain.ts +1 -1
- package/src/ai/fisherman/request-haul.ts +32 -0
- package/src/ai/fisherman-tools.ts +38 -32
- package/src/ai/fisherman.ts +8 -7
- package/src/ai/navigator.ts +1 -1
- package/src/ai/pilot.ts +19 -14
- package/src/ai/planner.ts +9 -8
- package/src/ai/provider.ts +8 -12
- package/src/ai/researcher/coordinates.ts +13 -1
- package/src/ai/researcher/deep-analysis.ts +9 -9
- package/src/ai/researcher/fingerprint-worker.ts +1 -10
- package/src/ai/researcher.ts +2 -1
- package/src/ai/rules.ts +11 -3
- package/src/ai/tester.ts +18 -18
- package/src/ai/tools.ts +18 -86
- package/src/api/request-store.ts +13 -27
- package/src/api/spec-reader.ts +1 -1
- package/src/commands/config-command.ts +3 -8
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/explore-command.ts +12 -1
- package/src/commands/index.ts +2 -0
- package/src/commands/init-command.ts +4 -11
- package/src/commands/options/base-option.ts +18 -0
- package/src/commands/options/index.ts +7 -0
- package/src/commands/options/knowledge-option.ts +14 -0
- package/src/commands/options/ws-option.ts +24 -0
- package/src/commands/plans-command.ts +5 -5
- package/src/commands/recommended-models-command.ts +92 -0
- package/src/config.ts +20 -2
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +4 -4
- package/src/global-config.ts +8 -0
- package/src/knowledge-tracker.ts +94 -36
- package/src/remote.ts +0 -20
- package/src/state-manager.ts +10 -9
- package/src/test-plan.ts +12 -10
- package/src/utils/aria.ts +26 -3
- package/src/utils/cli-section.ts +12 -0
- package/src/utils/html-diff.ts +6 -6
- package/src/utils/html.ts +6 -4
- package/src/utils/overlay.ts +105 -192
- package/src/utils/region.ts +132 -0
- package/src/utils/similarity.ts +9 -0
- package/src/utils/url-matcher.ts +3 -3
- package/dist/src/utils/request-map.d.ts +0 -7
- package/dist/src/utils/request-map.js +0 -16
- package/src/utils/request-map.ts +0 -19
|
@@ -11,8 +11,9 @@ import { type DocbotConfig, DocbotConfigParser } from './config.ts';
|
|
|
11
11
|
import { type DocumentedPage, type SkippedPage, renderPageDocumentation, renderSpecIndex } from './docs-renderer.ts';
|
|
12
12
|
import { getDocPageKey, shouldCrawlDocPath } from './path-filter.ts';
|
|
13
13
|
import { extractResearchNavigationTargets } from './research-navigation.ts';
|
|
14
|
-
import { type DocumentationScreenshot, captureBeforeInteraction, captureDocumentationScreenshots, captureInteractionScreenshot } from './screenshots.ts';
|
|
14
|
+
import { type DocumentationScreenshot, captureBeforeInteraction, captureDocumentationScreenshots, captureEvidenceScreenshots, captureInteractionScreenshot } from './screenshots.ts';
|
|
15
15
|
import { renderMermaidBody } from './state-diagram.ts';
|
|
16
|
+
import { type TemplateRecord, buildTemplateRecord, findTemplateMatch } from './template-dedup.ts';
|
|
16
17
|
|
|
17
18
|
class DocBot {
|
|
18
19
|
private explorBot: ExplorBot;
|
|
@@ -24,7 +25,7 @@ class DocBot {
|
|
|
24
25
|
|
|
25
26
|
constructor(options: DocbotOptions = {}) {
|
|
26
27
|
this.options = options;
|
|
27
|
-
const baseUrl = this.extractAbsoluteBaseUrl(options.startUrl || '/');
|
|
28
|
+
const baseUrl = this.extractAbsoluteBaseUrl(options.startUrl || '/') || options.baseUrl;
|
|
28
29
|
this.explorBot = new ExplorBot({
|
|
29
30
|
baseUrl,
|
|
30
31
|
verbose: options.verbose,
|
|
@@ -60,6 +61,7 @@ class DocBot {
|
|
|
60
61
|
const queue: string[] = [];
|
|
61
62
|
const queued = new Set<string>();
|
|
62
63
|
const documented = new Set<string>();
|
|
64
|
+
const templates: TemplateRecord[] = [];
|
|
63
65
|
const pages: DocumentedPage[] = [];
|
|
64
66
|
const skipped: SkippedPage[] = [];
|
|
65
67
|
const baseUrl = this.explorBot.getConfig().playwright.url;
|
|
@@ -86,15 +88,6 @@ class DocBot {
|
|
|
86
88
|
tag('info').log(`Collecting docs for ${this.toDisplayUrl(target, baseUrl)}`);
|
|
87
89
|
await this.explorBot.visit(target);
|
|
88
90
|
|
|
89
|
-
if (stateManager.isInDeadLoop()) {
|
|
90
|
-
tag('warning').log('Dead loop detected during docs crawl, stopping collection');
|
|
91
|
-
skipped.push({
|
|
92
|
-
url: target,
|
|
93
|
-
reason: 'dead loop detected during crawl',
|
|
94
|
-
});
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
91
|
const state = this.explorBot.getCurrentState();
|
|
99
92
|
if (!state) {
|
|
100
93
|
skipped.push({
|
|
@@ -109,6 +102,22 @@ class DocBot {
|
|
|
109
102
|
continue;
|
|
110
103
|
}
|
|
111
104
|
|
|
105
|
+
if (this.shouldCollapseTemplates(opts.collapseTemplatePages)) {
|
|
106
|
+
const templateUrl = findTemplateMatch(state.ariaSnapshot ?? null, templates, this.getTemplateSimilarity(opts.templateSimilarity));
|
|
107
|
+
if (templateUrl) {
|
|
108
|
+
skipped.push({
|
|
109
|
+
url: state.url,
|
|
110
|
+
reason: `same layout as ${templateUrl} (only content differs)`,
|
|
111
|
+
});
|
|
112
|
+
documented.add(pageKey);
|
|
113
|
+
for (const nextPath of this.extractNextPaths(state, baseUrl, '')) {
|
|
114
|
+
if (documented.has(this.getPageKey(nextPath)) || stateManager.hasVisitedState(nextPath)) continue;
|
|
115
|
+
this.enqueuePath(nextPath, queue, queued);
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
112
121
|
const research = await this.explorBot.agentResearcher().research(state, {
|
|
113
122
|
screenshot: this.shouldUseScreenshots(),
|
|
114
123
|
force: true,
|
|
@@ -153,6 +162,9 @@ class DocBot {
|
|
|
153
162
|
});
|
|
154
163
|
documented.add(pageKey);
|
|
155
164
|
|
|
165
|
+
const templateRecord = buildTemplateRecord(state.url, state.ariaSnapshot ?? null);
|
|
166
|
+
if (templateRecord) templates.push(templateRecord);
|
|
167
|
+
|
|
156
168
|
const nextPaths = this.extractNextPaths(state, baseUrl, research, documentation);
|
|
157
169
|
const interactionPriorityPaths = new Set(this.extractInteractionPaths(baseUrl, documentation));
|
|
158
170
|
for (const nextPath of nextPaths) {
|
|
@@ -209,6 +221,23 @@ class DocBot {
|
|
|
209
221
|
return true;
|
|
210
222
|
}
|
|
211
223
|
|
|
224
|
+
private shouldCollapseTemplates(override?: boolean): boolean {
|
|
225
|
+
if (override === false) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
if (this.config.docs?.collapseTemplatePages === false) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private getTemplateSimilarity(override?: number): number | undefined {
|
|
235
|
+
const value = override ?? this.config.docs?.templateSimilarity;
|
|
236
|
+
if (value == null) return undefined;
|
|
237
|
+
if (!Number.isFinite(value) || value <= 0 || value > 100) return undefined;
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
|
|
212
241
|
private extractNextPaths(state: WebPageState, baseUrl: string, research: string, documentation?: PageDocumentation): string[] {
|
|
213
242
|
const paths: string[] = [];
|
|
214
243
|
const seen = new Set<string>();
|
|
@@ -446,7 +475,8 @@ class DocBot {
|
|
|
446
475
|
private async savePageDocumentation(state: WebPageState, documentation: PageDocumentation, research: string): Promise<string> {
|
|
447
476
|
const pagePath = this.getPageFilePath(state.url);
|
|
448
477
|
const screenshots = await this.captureScreenshots(state, research, pagePath);
|
|
449
|
-
|
|
478
|
+
const evidence = await this.captureEvidence(state, documentation, research, pagePath);
|
|
479
|
+
writeFileSync(pagePath, renderPageDocumentation(state, documentation, screenshots, evidence), 'utf8');
|
|
450
480
|
return pagePath;
|
|
451
481
|
}
|
|
452
482
|
|
|
@@ -462,6 +492,19 @@ class DocBot {
|
|
|
462
492
|
});
|
|
463
493
|
}
|
|
464
494
|
|
|
495
|
+
private async captureEvidence(state: WebPageState, documentation: PageDocumentation, research: string, pagePath: string): Promise<Array<DocumentationScreenshot | null>> {
|
|
496
|
+
if (!this.shouldUseScreenshots()) {
|
|
497
|
+
return documentation.can.map(() => null);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return captureEvidenceScreenshots(this.explorBot.getExplorer(), state, documentation, {
|
|
501
|
+
pageFilePath: pagePath,
|
|
502
|
+
screenshotsDir: this.getScreenshotsDir(),
|
|
503
|
+
config: this.config,
|
|
504
|
+
research,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
465
508
|
private saveIndex(startPath: string, pages: DocumentedPage[], skipped: SkippedPage[], maxPages: number): { indexPath: string; diagramPath: string } {
|
|
466
509
|
const outputDir = this.configParser.getOutputDir();
|
|
467
510
|
const indexPath = path.join(outputDir, 'index.md');
|
|
@@ -505,6 +548,8 @@ interface DocbotOptions extends ExplorBotOptions {
|
|
|
505
548
|
|
|
506
549
|
interface CollectOptions {
|
|
507
550
|
maxPages?: number;
|
|
551
|
+
collapseTemplatePages?: boolean;
|
|
552
|
+
templateSimilarity?: number;
|
|
508
553
|
}
|
|
509
554
|
|
|
510
555
|
interface CollectionResult {
|
|
@@ -7,7 +7,7 @@ import type { PageDocumentation, StateTransition } from './ai/documentarian.ts';
|
|
|
7
7
|
import type { DocumentationScreenshot } from './screenshots.ts';
|
|
8
8
|
import { type DocumentedPage, type SkippedPage, buildStateGraph, renderMermaidFromGraph, renderPageStateDiagram, renderStateMapFromGraph } from './state-diagram.ts';
|
|
9
9
|
|
|
10
|
-
function renderPageDocumentation(state: WebPageState, documentation: PageDocumentation, screenshots: DocumentationScreenshot[] = []): string {
|
|
10
|
+
function renderPageDocumentation(state: WebPageState, documentation: PageDocumentation, screenshots: DocumentationScreenshot[] = [], evidence: Array<DocumentationScreenshot | null> = []): string {
|
|
11
11
|
const lines: string[] = [];
|
|
12
12
|
lines.push(`# ${state.url}`);
|
|
13
13
|
lines.push('');
|
|
@@ -22,6 +22,42 @@ function renderPageDocumentation(state: WebPageState, documentation: PageDocumen
|
|
|
22
22
|
lines.push(ensureSentence(documentation.summary));
|
|
23
23
|
lines.push('');
|
|
24
24
|
|
|
25
|
+
lines.push('## User Can');
|
|
26
|
+
lines.push('');
|
|
27
|
+
|
|
28
|
+
if (documentation.can.length === 0) {
|
|
29
|
+
lines.push('- No proven actions were identified from the collected research.');
|
|
30
|
+
lines.push('');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const [index, item] of documentation.can.entries()) {
|
|
34
|
+
lines.push(`- ${normalizeAction(item.action)} -> ${item.scope}`);
|
|
35
|
+
lines.push(` Proof: ${ensureSentence(item.evidence)}`);
|
|
36
|
+
const shot = evidence[index];
|
|
37
|
+
if (shot) {
|
|
38
|
+
lines.push('');
|
|
39
|
+
lines.push(` `);
|
|
40
|
+
}
|
|
41
|
+
lines.push('');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
lines.push('## User Might');
|
|
45
|
+
lines.push('');
|
|
46
|
+
|
|
47
|
+
if (documentation.might.length === 0) {
|
|
48
|
+
lines.push('- No assumption-based actions were identified.');
|
|
49
|
+
lines.push('');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const item of documentation.might) {
|
|
53
|
+
lines.push(`- ${normalizeAction(item.action, 'might')} -> ${item.scope}`);
|
|
54
|
+
lines.push(` Signal: ${ensureSentence(item.evidence)}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (documentation.might.length > 0) {
|
|
58
|
+
lines.push('');
|
|
59
|
+
}
|
|
60
|
+
|
|
25
61
|
if (screenshots.length > 0) {
|
|
26
62
|
lines.push('## Screenshots');
|
|
27
63
|
lines.push('');
|
|
@@ -34,6 +70,26 @@ function renderPageDocumentation(state: WebPageState, documentation: PageDocumen
|
|
|
34
70
|
}
|
|
35
71
|
}
|
|
36
72
|
|
|
73
|
+
const navigationLinks = renderNavigationLinks(state);
|
|
74
|
+
if (navigationLinks.length > 0) {
|
|
75
|
+
lines.push('## Navigation');
|
|
76
|
+
lines.push('');
|
|
77
|
+
for (const link of navigationLinks) {
|
|
78
|
+
lines.push(`- ${link.title}: ${link.url}`);
|
|
79
|
+
}
|
|
80
|
+
lines.push('');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const qualityNotes = documentation.qualityNotes;
|
|
84
|
+
if (qualityNotes && qualityNotes.length > 0) {
|
|
85
|
+
lines.push('## Coverage Notes');
|
|
86
|
+
lines.push('');
|
|
87
|
+
for (const note of qualityNotes) {
|
|
88
|
+
lines.push(`- ${ensureSentence(note)}`);
|
|
89
|
+
}
|
|
90
|
+
lines.push('');
|
|
91
|
+
}
|
|
92
|
+
|
|
37
93
|
const interactions = documentation.interactions;
|
|
38
94
|
const pageStateDiagram = renderPageStateDiagram(state.title || state.url || 'page', state.url || '', interactions || []);
|
|
39
95
|
if (pageStateDiagram) {
|
|
@@ -66,50 +122,6 @@ function renderPageDocumentation(state: WebPageState, documentation: PageDocumen
|
|
|
66
122
|
}
|
|
67
123
|
}
|
|
68
124
|
|
|
69
|
-
lines.push('## User Can');
|
|
70
|
-
lines.push('');
|
|
71
|
-
|
|
72
|
-
if (documentation.can.length === 0) {
|
|
73
|
-
lines.push('- No proven actions were identified from the collected research.');
|
|
74
|
-
lines.push('');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
for (const item of documentation.can) {
|
|
78
|
-
lines.push(`- ${normalizeAction(item.action)} -> ${item.scope}`);
|
|
79
|
-
lines.push(` Proof: ${ensureSentence(item.evidence)}`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (documentation.can.length > 0) {
|
|
83
|
-
lines.push('');
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
lines.push('## User Might');
|
|
87
|
-
lines.push('');
|
|
88
|
-
|
|
89
|
-
if (documentation.might.length === 0) {
|
|
90
|
-
lines.push('- No assumption-based actions were identified.');
|
|
91
|
-
lines.push('');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
for (const item of documentation.might) {
|
|
95
|
-
lines.push(`- ${normalizeAction(item.action, 'might')} -> ${item.scope}`);
|
|
96
|
-
lines.push(` Signal: ${ensureSentence(item.evidence)}`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (documentation.might.length > 0) {
|
|
100
|
-
lines.push('');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const qualityNotes = documentation.qualityNotes;
|
|
104
|
-
if (qualityNotes && qualityNotes.length > 0) {
|
|
105
|
-
lines.push('## Coverage Notes');
|
|
106
|
-
lines.push('');
|
|
107
|
-
for (const note of qualityNotes) {
|
|
108
|
-
lines.push(`- ${ensureSentence(note)}`);
|
|
109
|
-
}
|
|
110
|
-
lines.push('');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
125
|
return matter.stringify(`${lines.join('\n').trimEnd()}\n`, {
|
|
114
126
|
url: state.url,
|
|
115
127
|
format: APPLICATION_SPEC_FORMAT,
|
|
@@ -130,17 +142,7 @@ function renderSpecIndex(outputDir: string, startPath: string, pages: Documented
|
|
|
130
142
|
lines.push('');
|
|
131
143
|
const graph = buildStateGraph(outputDir, pages);
|
|
132
144
|
const mermaid = renderMermaidFromGraph(graph, true);
|
|
133
|
-
lines.push('## State Transitions');
|
|
134
|
-
lines.push('');
|
|
135
|
-
lines.push(`\`\`\`mermaid\n${mermaid}\n\`\`\``);
|
|
136
|
-
lines.push('');
|
|
137
145
|
const stateMap = renderStateMapFromGraph(graph);
|
|
138
|
-
if (stateMap) {
|
|
139
|
-
lines.push('### State Index');
|
|
140
|
-
lines.push('');
|
|
141
|
-
lines.push(stateMap);
|
|
142
|
-
lines.push('');
|
|
143
|
-
}
|
|
144
146
|
lines.push('## Pages');
|
|
145
147
|
lines.push('');
|
|
146
148
|
|
|
@@ -208,9 +210,31 @@ function renderSpecIndex(outputDir: string, startPath: string, pages: Documented
|
|
|
208
210
|
lines.push('');
|
|
209
211
|
}
|
|
210
212
|
|
|
213
|
+
lines.push('## State Transitions');
|
|
214
|
+
lines.push('');
|
|
215
|
+
lines.push(`\`\`\`mermaid\n${mermaid}\n\`\`\``);
|
|
216
|
+
lines.push('');
|
|
217
|
+
if (stateMap) {
|
|
218
|
+
lines.push('### State Index');
|
|
219
|
+
lines.push('');
|
|
220
|
+
lines.push(stateMap);
|
|
221
|
+
lines.push('');
|
|
222
|
+
}
|
|
223
|
+
|
|
211
224
|
return `${lines.join('\n').trimEnd()}\n`;
|
|
212
225
|
}
|
|
213
226
|
|
|
227
|
+
function renderNavigationLinks(state: WebPageState): Array<{ title: string; url: string }> {
|
|
228
|
+
const links: Array<{ title: string; url: string }> = [];
|
|
229
|
+
const seen = new Set<string>();
|
|
230
|
+
for (const link of state.links || []) {
|
|
231
|
+
if (!link.url || seen.has(link.url)) continue;
|
|
232
|
+
seen.add(link.url);
|
|
233
|
+
links.push({ title: normalizeInlineText(link.title) || link.url, url: link.url });
|
|
234
|
+
}
|
|
235
|
+
return links;
|
|
236
|
+
}
|
|
237
|
+
|
|
214
238
|
function normalizeAction(action: string, kind: 'can' | 'might' = 'can'): string {
|
|
215
239
|
const trimmed = normalizeInlineText(action);
|
|
216
240
|
if (!trimmed) {
|
|
@@ -2,6 +2,7 @@ import { writeFileSync } from 'node:fs';
|
|
|
2
2
|
import pixelmatch from 'pixelmatch';
|
|
3
3
|
import type { Page } from 'playwright';
|
|
4
4
|
import { PNG } from 'pngjs';
|
|
5
|
+
import { removeVisualAnnotations } from '../../../src/ai/researcher/coordinates.ts';
|
|
5
6
|
|
|
6
7
|
const REGION_PADDING = 30;
|
|
7
8
|
const SCREENSHOT_OPTIONS = { animations: 'disabled', caret: 'hide' } as const;
|
|
@@ -44,7 +45,7 @@ export function findChangedRegion(beforeScreenshot: Buffer, afterScreenshot: Buf
|
|
|
44
45
|
return changedPixels ? addPadding(changedPixels, before.width, before.height, padding) : null;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
function saveRegion(after: PNG, region: ScreenshotRegion, filePath: string): void {
|
|
48
|
+
export function saveRegion(after: PNG, region: ScreenshotRegion, filePath: string): void {
|
|
48
49
|
const cropped = new PNG({ width: region.width, height: region.height });
|
|
49
50
|
PNG.bitblt(after, cropped, region.x, region.y, region.width, region.height, 0, 0);
|
|
50
51
|
writeFileSync(filePath, PNG.sync.write(cropped));
|
|
@@ -73,7 +74,23 @@ function findChangedPixelBounds(before: PNG, after: PNG): ScreenshotRegion | nul
|
|
|
73
74
|
return { x: left, y: top, width: right - left + 1, height: bottom - top + 1 };
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
function
|
|
77
|
+
export function regionAround(png: PNG, box: { x: number; y: number; width: number; height: number }, viewport: { width: number; height: number } | null, padding: number): ScreenshotRegion {
|
|
78
|
+
let scaleX = 1;
|
|
79
|
+
let scaleY = 1;
|
|
80
|
+
if (viewport) {
|
|
81
|
+
scaleX = png.width / viewport.width;
|
|
82
|
+
scaleY = png.height / viewport.height;
|
|
83
|
+
}
|
|
84
|
+
const scaled: ScreenshotRegion = {
|
|
85
|
+
x: Math.round(box.x * scaleX),
|
|
86
|
+
y: Math.round(box.y * scaleY),
|
|
87
|
+
width: Math.round(box.width * scaleX),
|
|
88
|
+
height: Math.round(box.height * scaleY),
|
|
89
|
+
};
|
|
90
|
+
return addPadding(scaled, png.width, png.height, Math.round(padding * scaleX));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function addPadding(region: ScreenshotRegion, imageWidth: number, imageHeight: number, padding = REGION_PADDING): ScreenshotRegion {
|
|
77
94
|
const x = Math.max(0, region.x - padding);
|
|
78
95
|
const y = Math.max(0, region.y - padding);
|
|
79
96
|
const maxX = Math.min(imageWidth, region.x + region.width + padding);
|
|
@@ -81,14 +98,6 @@ function addPadding(region: ScreenshotRegion, imageWidth: number, imageHeight: n
|
|
|
81
98
|
return { x, y, width: maxX - x, height: maxY - y };
|
|
82
99
|
}
|
|
83
100
|
|
|
84
|
-
async function removeVisualAnnotations(page: Page): Promise<void> {
|
|
85
|
-
try {
|
|
86
|
-
await page.locator('[data-explorbot-annotation]').evaluateAll((elements) => {
|
|
87
|
-
for (const element of elements) element.remove();
|
|
88
|
-
});
|
|
89
|
-
} catch {}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
101
|
async function findOverlayRegion(page: Page, image: PNG, detectUnmarkedOverlay: boolean): Promise<ScreenshotRegion | null> {
|
|
93
102
|
let box: { x: number; y: number; width: number; height: number } | null = null;
|
|
94
103
|
try {
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { mkdirSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { PNG } from 'pngjs';
|
|
4
|
+
import { removeVisualAnnotations } from '../../../src/ai/researcher/coordinates.ts';
|
|
3
5
|
import { parseResearchSections } from '../../../src/ai/researcher/parser.ts';
|
|
6
|
+
import { parseAriaLocator } from '../../../src/utils/aria.ts';
|
|
4
7
|
import type Explorer from '../../../src/explorer.ts';
|
|
5
|
-
import type
|
|
8
|
+
import { type WebPageState, normalizeUrl } from '../../../src/state-manager.ts';
|
|
6
9
|
import { safeFilename, sanitizeFilename } from '../../../src/utils/strings.ts';
|
|
10
|
+
import type { PageDocumentation } from './ai/documentarian.ts';
|
|
7
11
|
import type { DocStateTransition } from './ai/tools.ts';
|
|
8
12
|
import type { DocbotConfig } from './config.ts';
|
|
9
|
-
import { captureInteractionAfter, captureInteractionBefore } from './interaction-screenshots.ts';
|
|
13
|
+
import { addPadding, captureInteractionAfter, captureInteractionBefore, regionAround, saveRegion } from './interaction-screenshots.ts';
|
|
10
14
|
|
|
11
15
|
const DEFAULT_MAX_SECTION_SCREENSHOTS = 8;
|
|
16
|
+
const EVIDENCE_PADDING = 250;
|
|
12
17
|
|
|
13
18
|
export async function captureDocumentationScreenshots(explorer: Explorer, state: WebPageState, research: string, options: DocumentationScreenshotOptions): Promise<DocumentationScreenshot[]> {
|
|
14
19
|
const page = explorer.page;
|
|
@@ -16,6 +21,8 @@ export async function captureDocumentationScreenshots(explorer: Explorer, state:
|
|
|
16
21
|
return [];
|
|
17
22
|
}
|
|
18
23
|
|
|
24
|
+
await dismissTransientOverlay(page);
|
|
25
|
+
await removeVisualAnnotations(page);
|
|
19
26
|
mkdirSync(options.screenshotsDir, { recursive: true });
|
|
20
27
|
|
|
21
28
|
const screenshots: DocumentationScreenshot[] = [];
|
|
@@ -37,6 +44,101 @@ export async function captureDocumentationScreenshots(explorer: Explorer, state:
|
|
|
37
44
|
return screenshots;
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
export async function captureEvidenceScreenshots(explorer: Explorer, state: WebPageState, documentation: PageDocumentation, options: DocumentationScreenshotOptions): Promise<Array<DocumentationScreenshot | null>> {
|
|
48
|
+
const page = explorer.page;
|
|
49
|
+
if (!page) {
|
|
50
|
+
return documentation.can.map(() => null);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await dismissTransientOverlay(page);
|
|
54
|
+
await removeVisualAnnotations(page);
|
|
55
|
+
if (!isOnDocumentedPage(page, state)) {
|
|
56
|
+
return documentation.can.map(() => null);
|
|
57
|
+
}
|
|
58
|
+
mkdirSync(options.screenshotsDir, { recursive: true });
|
|
59
|
+
const pageName = sanitizeFilename(state.url || 'page') || 'page';
|
|
60
|
+
const shots: Array<DocumentationScreenshot | null> = [];
|
|
61
|
+
for (const [index, item] of documentation.can.entries()) {
|
|
62
|
+
shots.push(await captureEvidenceScreenshot(page, pageName, index, item, options));
|
|
63
|
+
}
|
|
64
|
+
return shots;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function captureEvidenceScreenshot(page: any, pageName: string, index: number, item: { action: string; element?: string | null }, options: DocumentationScreenshotOptions): Promise<DocumentationScreenshot | null> {
|
|
68
|
+
if (!item.element) return null;
|
|
69
|
+
const element = resolveResearchLocator(item.element, options.research);
|
|
70
|
+
if (!element) return null;
|
|
71
|
+
const locatorInfo = parseAriaLocator(element);
|
|
72
|
+
try {
|
|
73
|
+
const locator = resolveEvidenceLocator(page, element, locatorInfo);
|
|
74
|
+
if ((await locator.count()) !== 1) return null;
|
|
75
|
+
await locator.scrollIntoViewIfNeeded({ timeout: 2000 });
|
|
76
|
+
const box = await locator.boundingBox();
|
|
77
|
+
if (!box || box.width <= 0 || box.height <= 0) return null;
|
|
78
|
+
const image = await page.screenshot();
|
|
79
|
+
const png = PNG.sync.read(image);
|
|
80
|
+
const region = regionAround(png, box, await page.viewportSize(), EVIDENCE_PADDING);
|
|
81
|
+
const filePath = path.join(options.screenshotsDir, safeFilename(`${pageName}_can_${index + 1}`, '.png'));
|
|
82
|
+
saveRegion(png, region, filePath);
|
|
83
|
+
return {
|
|
84
|
+
title: item.action,
|
|
85
|
+
path: filePath,
|
|
86
|
+
relativePath: toMarkdownPath(options.pageFilePath, filePath),
|
|
87
|
+
kind: 'evidence',
|
|
88
|
+
};
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveResearchLocator(requested: string, research?: string): string | null {
|
|
95
|
+
if (!research) return requested;
|
|
96
|
+
const requestedAria = parseAriaLocator(requested);
|
|
97
|
+
const requestedCss = unwrapLocator(requested);
|
|
98
|
+
for (const section of parseResearchSections(research)) {
|
|
99
|
+
for (const element of section.elements) {
|
|
100
|
+
if (requestedAria && element.aria?.role === requestedAria.role && element.aria.text === requestedAria.text) {
|
|
101
|
+
return `{ role: '${element.aria.role}', text: '${element.aria.text}' }`;
|
|
102
|
+
}
|
|
103
|
+
if (element.css && requestedCss === unwrapLocator(element.css)) return element.css;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function unwrapLocator(locator: string): string {
|
|
110
|
+
const trimmed = locator.trim();
|
|
111
|
+
const first = trimmed.at(0);
|
|
112
|
+
const last = trimmed.at(-1);
|
|
113
|
+
if (first === last && (first === '`' || first === '"' || first === "'")) {
|
|
114
|
+
return trimmed.slice(1, -1).trim();
|
|
115
|
+
}
|
|
116
|
+
return trimmed;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isOnDocumentedPage(page: any, state: WebPageState): boolean {
|
|
120
|
+
try {
|
|
121
|
+
const current = new URL(page.url());
|
|
122
|
+
const expected = new URL(state.url || '', current.origin);
|
|
123
|
+
return current.origin === expected.origin && normalizeUrl(current.href) === normalizeUrl(expected.href);
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveEvidenceLocator(page: any, element: string, locatorInfo: { role: string; text: string } | null): any {
|
|
130
|
+
if (locatorInfo?.text) {
|
|
131
|
+
return page.getByRole(locatorInfo.role, { name: locatorInfo.text, exact: true });
|
|
132
|
+
}
|
|
133
|
+
return page.locator(element);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function dismissTransientOverlay(page: any): Promise<void> {
|
|
137
|
+
try {
|
|
138
|
+
await page.keyboard.press('Escape');
|
|
139
|
+
} catch {}
|
|
140
|
+
}
|
|
141
|
+
|
|
40
142
|
export function getScreenshotSections(research: string): ScreenshotSection[] {
|
|
41
143
|
const sections: ScreenshotSection[] = [];
|
|
42
144
|
const seen = new Set<string>();
|
|
@@ -150,7 +252,7 @@ export interface DocumentationScreenshot {
|
|
|
150
252
|
title: string;
|
|
151
253
|
path: string;
|
|
152
254
|
relativePath: string;
|
|
153
|
-
kind: 'page' | 'section' | 'state';
|
|
255
|
+
kind: 'page' | 'section' | 'state' | 'evidence';
|
|
154
256
|
selector?: string;
|
|
155
257
|
}
|
|
156
258
|
|
|
@@ -158,6 +260,7 @@ interface DocumentationScreenshotOptions {
|
|
|
158
260
|
pageFilePath: string;
|
|
159
261
|
screenshotsDir: string;
|
|
160
262
|
config: DocbotConfig;
|
|
263
|
+
research?: string;
|
|
161
264
|
}
|
|
162
265
|
|
|
163
266
|
interface ScreenshotSection {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ariaTemplateSignature } from '../../../src/utils/aria.ts';
|
|
2
|
+
import { diceSimilarity } from '../../../src/utils/similarity.ts';
|
|
3
|
+
|
|
4
|
+
const TEMPLATE_MIN_SIGNATURE_SIZE = 8;
|
|
5
|
+
const TEMPLATE_SIMILARITY_THRESHOLD = 90;
|
|
6
|
+
|
|
7
|
+
export function buildTemplateRecord(url: string, snapshot: string | null): TemplateRecord | null {
|
|
8
|
+
const signature = ariaTemplateSignature(snapshot);
|
|
9
|
+
if (signature.size < TEMPLATE_MIN_SIGNATURE_SIZE) return null;
|
|
10
|
+
return { url, signature };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function findTemplateMatch(snapshot: string | null, known: TemplateRecord[], threshold: number = TEMPLATE_SIMILARITY_THRESHOLD): string | null {
|
|
14
|
+
const signature = ariaTemplateSignature(snapshot);
|
|
15
|
+
if (signature.size < TEMPLATE_MIN_SIGNATURE_SIZE) return null;
|
|
16
|
+
let bestUrl: string | null = null;
|
|
17
|
+
let bestScore = 0;
|
|
18
|
+
for (const record of known) {
|
|
19
|
+
const score = diceSimilarity(signature, record.signature);
|
|
20
|
+
if (score < bestScore) continue;
|
|
21
|
+
bestScore = score;
|
|
22
|
+
bestUrl = record.url;
|
|
23
|
+
}
|
|
24
|
+
if (bestScore < threshold) return null;
|
|
25
|
+
return bestUrl;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TemplateRecord {
|
|
29
|
+
url: string;
|
|
30
|
+
signature: Set<string>;
|
|
31
|
+
}
|
package/boat/prima/src/cli.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import dedent from 'dedent';
|
|
3
3
|
import { keepServerRunning } from '../../../src/browser-server.ts';
|
|
4
|
+
import { RecommendedModelsCommand } from '../../../src/commands/recommended-models-command.ts';
|
|
4
5
|
import { browserErrorMessage } from '../../../src/utils/browser-errors.ts';
|
|
5
6
|
import { isVerboseMode, setQuietMode } from '../../../src/utils/logger.ts';
|
|
6
7
|
import { clearActivityLine, trackActivityLine } from './activity-line.ts';
|
|
@@ -116,6 +117,7 @@ function addCommonOptions(cmd: Command): Command {
|
|
|
116
117
|
.option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
|
|
117
118
|
.option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
|
|
118
119
|
.option('--url <url>', 'Page to open when the session has no page yet')
|
|
120
|
+
.option('--spec <path>', 'Docbot application spec directory or index.md to read as page knowledge')
|
|
119
121
|
.option('--endpoint <ep>', 'Websocket endpoint of a browser server to attach to, skipping discovery')
|
|
120
122
|
.option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
|
|
121
123
|
.addHelpText('after', `\n${sessionHelp}`);
|
|
@@ -126,6 +128,7 @@ function primaFor(options: any): Prima {
|
|
|
126
128
|
if (options.ephemeral) process.env.EXPLORBOT_EPHEMERAL = '1';
|
|
127
129
|
if (options.model) process.env.EXPLORBOT_AI_MODEL = options.model;
|
|
128
130
|
if (options.visionModel) process.env.EXPLORBOT_VISION_MODEL = options.visionModel;
|
|
131
|
+
if (options.spec) process.env.EXPLORBOT_SPEC = options.spec;
|
|
129
132
|
return new Prima(buildOptions(options));
|
|
130
133
|
}
|
|
131
134
|
|
|
@@ -218,6 +221,8 @@ export function createPrimaCommands(name = 'prima'): Command {
|
|
|
218
221
|
process.exit(0);
|
|
219
222
|
});
|
|
220
223
|
|
|
224
|
+
RecommendedModelsCommand.register(cmd);
|
|
225
|
+
|
|
221
226
|
addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command'))
|
|
222
227
|
.addHelpText('after', `\n${statusHelp}`)
|
|
223
228
|
.action(async (hash, options) => {
|
package/boat/prima/src/prima.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { getAliveEndpoint, launchServer, listInstances, stopServer } from '../..
|
|
|
15
15
|
import { ConfigCommand } from '../../../src/commands/config-command.ts';
|
|
16
16
|
import { ConfigMissingError, ConfigParser, EXPLORBOT_ENV_VARS, type ExplorbotConfig, outputPath } from '../../../src/config.ts';
|
|
17
17
|
import { ExplorBot } from '../../../src/explorbot.ts';
|
|
18
|
-
import { listSites } from '../../../src/global-config.ts';
|
|
18
|
+
import { findSiteWith, listSites } from '../../../src/global-config.ts';
|
|
19
19
|
import { Reporter } from '../../../src/reporter.ts';
|
|
20
20
|
import type { WebPageState } from '../../../src/state-manager.ts';
|
|
21
21
|
import { Stats } from '../../../src/stats.ts';
|
|
@@ -1067,8 +1067,7 @@ export class Prima {
|
|
|
1067
1067
|
|
|
1068
1068
|
async status(hash: string): Promise<EnvelopeData> {
|
|
1069
1069
|
if (!this.artifactsDir) {
|
|
1070
|
-
const
|
|
1071
|
-
const site = sites.find((candidate) => existsSync(path.join(candidate.dir, 'output', 'prima', hash))) || sites[0];
|
|
1070
|
+
const site = findSiteWith(path.join('output', 'prima', hash)) || listSites()[0];
|
|
1072
1071
|
if (site && !this.configBaseUrl()) this.sessionUrl = site.url;
|
|
1073
1072
|
await this.loadConfig();
|
|
1074
1073
|
}
|
|
@@ -1113,7 +1112,7 @@ export class Prima {
|
|
|
1113
1112
|
aria: result.ariaSnapshot,
|
|
1114
1113
|
html: await result.combinedHtml(),
|
|
1115
1114
|
screenshot: result.screenshot,
|
|
1116
|
-
requests: this.bot.requestStore().
|
|
1115
|
+
requests: this.bot.requestStore().getMadeRequests(),
|
|
1117
1116
|
});
|
|
1118
1117
|
}
|
|
1119
1118
|
|