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
|
@@ -9,8 +9,9 @@ import { DocbotConfigParser } from "./config.js";
|
|
|
9
9
|
import { renderPageDocumentation, renderSpecIndex } from "./docs-renderer.js";
|
|
10
10
|
import { getDocPageKey, shouldCrawlDocPath } from "./path-filter.js";
|
|
11
11
|
import { extractResearchNavigationTargets } from "./research-navigation.js";
|
|
12
|
-
import { captureBeforeInteraction, captureDocumentationScreenshots, captureInteractionScreenshot } from "./screenshots.js";
|
|
12
|
+
import { captureBeforeInteraction, captureDocumentationScreenshots, captureEvidenceScreenshots, captureInteractionScreenshot } from "./screenshots.js";
|
|
13
13
|
import { renderMermaidBody } from "./state-diagram.js";
|
|
14
|
+
import { buildTemplateRecord, findTemplateMatch } from "./template-dedup.js";
|
|
14
15
|
class DocBot {
|
|
15
16
|
explorBot;
|
|
16
17
|
configParser;
|
|
@@ -20,7 +21,7 @@ class DocBot {
|
|
|
20
21
|
scopeRoot = '/';
|
|
21
22
|
constructor(options = {}) {
|
|
22
23
|
this.options = options;
|
|
23
|
-
const baseUrl = this.extractAbsoluteBaseUrl(options.startUrl || '/');
|
|
24
|
+
const baseUrl = this.extractAbsoluteBaseUrl(options.startUrl || '/') || options.baseUrl;
|
|
24
25
|
this.explorBot = new ExplorBot({
|
|
25
26
|
baseUrl,
|
|
26
27
|
verbose: options.verbose,
|
|
@@ -53,6 +54,7 @@ class DocBot {
|
|
|
53
54
|
const queue = [];
|
|
54
55
|
const queued = new Set();
|
|
55
56
|
const documented = new Set();
|
|
57
|
+
const templates = [];
|
|
56
58
|
const pages = [];
|
|
57
59
|
const skipped = [];
|
|
58
60
|
const baseUrl = this.explorBot.getConfig().playwright.url;
|
|
@@ -73,14 +75,6 @@ class DocBot {
|
|
|
73
75
|
try {
|
|
74
76
|
tag('info').log(`Collecting docs for ${this.toDisplayUrl(target, baseUrl)}`);
|
|
75
77
|
await this.explorBot.visit(target);
|
|
76
|
-
if (stateManager.isInDeadLoop()) {
|
|
77
|
-
tag('warning').log('Dead loop detected during docs crawl, stopping collection');
|
|
78
|
-
skipped.push({
|
|
79
|
-
url: target,
|
|
80
|
-
reason: 'dead loop detected during crawl',
|
|
81
|
-
});
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
84
78
|
const state = this.explorBot.getCurrentState();
|
|
85
79
|
if (!state) {
|
|
86
80
|
skipped.push({
|
|
@@ -93,6 +87,22 @@ class DocBot {
|
|
|
93
87
|
if (documented.has(pageKey)) {
|
|
94
88
|
continue;
|
|
95
89
|
}
|
|
90
|
+
if (this.shouldCollapseTemplates(opts.collapseTemplatePages)) {
|
|
91
|
+
const templateUrl = findTemplateMatch(state.ariaSnapshot ?? null, templates, this.getTemplateSimilarity(opts.templateSimilarity));
|
|
92
|
+
if (templateUrl) {
|
|
93
|
+
skipped.push({
|
|
94
|
+
url: state.url,
|
|
95
|
+
reason: `same layout as ${templateUrl} (only content differs)`,
|
|
96
|
+
});
|
|
97
|
+
documented.add(pageKey);
|
|
98
|
+
for (const nextPath of this.extractNextPaths(state, baseUrl, '')) {
|
|
99
|
+
if (documented.has(this.getPageKey(nextPath)) || stateManager.hasVisitedState(nextPath))
|
|
100
|
+
continue;
|
|
101
|
+
this.enqueuePath(nextPath, queue, queued);
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
96
106
|
const research = await this.explorBot.agentResearcher().research(state, {
|
|
97
107
|
screenshot: this.shouldUseScreenshots(),
|
|
98
108
|
force: true,
|
|
@@ -134,6 +144,9 @@ class DocBot {
|
|
|
134
144
|
filePath,
|
|
135
145
|
});
|
|
136
146
|
documented.add(pageKey);
|
|
147
|
+
const templateRecord = buildTemplateRecord(state.url, state.ariaSnapshot ?? null);
|
|
148
|
+
if (templateRecord)
|
|
149
|
+
templates.push(templateRecord);
|
|
137
150
|
const nextPaths = this.extractNextPaths(state, baseUrl, research, documentation);
|
|
138
151
|
const interactionPriorityPaths = new Set(this.extractInteractionPaths(baseUrl, documentation));
|
|
139
152
|
for (const nextPath of nextPaths) {
|
|
@@ -184,6 +197,23 @@ class DocBot {
|
|
|
184
197
|
}
|
|
185
198
|
return true;
|
|
186
199
|
}
|
|
200
|
+
shouldCollapseTemplates(override) {
|
|
201
|
+
if (override === false) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
if (this.config.docs?.collapseTemplatePages === false) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
getTemplateSimilarity(override) {
|
|
210
|
+
const value = override ?? this.config.docs?.templateSimilarity;
|
|
211
|
+
if (value == null)
|
|
212
|
+
return undefined;
|
|
213
|
+
if (!Number.isFinite(value) || value <= 0 || value > 100)
|
|
214
|
+
return undefined;
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
187
217
|
extractNextPaths(state, baseUrl, research, documentation) {
|
|
188
218
|
const paths = [];
|
|
189
219
|
const seen = new Set();
|
|
@@ -393,7 +423,8 @@ class DocBot {
|
|
|
393
423
|
async savePageDocumentation(state, documentation, research) {
|
|
394
424
|
const pagePath = this.getPageFilePath(state.url);
|
|
395
425
|
const screenshots = await this.captureScreenshots(state, research, pagePath);
|
|
396
|
-
|
|
426
|
+
const evidence = await this.captureEvidence(state, documentation, research, pagePath);
|
|
427
|
+
writeFileSync(pagePath, renderPageDocumentation(state, documentation, screenshots, evidence), 'utf8');
|
|
397
428
|
return pagePath;
|
|
398
429
|
}
|
|
399
430
|
async captureScreenshots(state, research, pagePath) {
|
|
@@ -406,6 +437,17 @@ class DocBot {
|
|
|
406
437
|
config: this.config,
|
|
407
438
|
});
|
|
408
439
|
}
|
|
440
|
+
async captureEvidence(state, documentation, research, pagePath) {
|
|
441
|
+
if (!this.shouldUseScreenshots()) {
|
|
442
|
+
return documentation.can.map(() => null);
|
|
443
|
+
}
|
|
444
|
+
return captureEvidenceScreenshots(this.explorBot.getExplorer(), state, documentation, {
|
|
445
|
+
pageFilePath: pagePath,
|
|
446
|
+
screenshotsDir: this.getScreenshotsDir(),
|
|
447
|
+
config: this.config,
|
|
448
|
+
research,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
409
451
|
saveIndex(startPath, pages, skipped, maxPages) {
|
|
410
452
|
const outputDir = this.configParser.getOutputDir();
|
|
411
453
|
const indexPath = path.join(outputDir, 'index.md');
|
|
@@ -3,7 +3,7 @@ import matter from 'gray-matter';
|
|
|
3
3
|
import { APPLICATION_SPEC_FORMAT, APPLICATION_SPEC_VERSION } from "../../../src/application-spec-contract.js";
|
|
4
4
|
import { normalizeInlineText } from "../../../src/utils/strings.js";
|
|
5
5
|
import { buildStateGraph, renderMermaidFromGraph, renderPageStateDiagram, renderStateMapFromGraph } from "./state-diagram.js";
|
|
6
|
-
function renderPageDocumentation(state, documentation, screenshots = []) {
|
|
6
|
+
function renderPageDocumentation(state, documentation, screenshots = [], evidence = []) {
|
|
7
7
|
const lines = [];
|
|
8
8
|
lines.push(`# ${state.url}`);
|
|
9
9
|
lines.push('');
|
|
@@ -15,6 +15,35 @@ function renderPageDocumentation(state, documentation, screenshots = []) {
|
|
|
15
15
|
lines.push('');
|
|
16
16
|
lines.push(ensureSentence(documentation.summary));
|
|
17
17
|
lines.push('');
|
|
18
|
+
lines.push('## User Can');
|
|
19
|
+
lines.push('');
|
|
20
|
+
if (documentation.can.length === 0) {
|
|
21
|
+
lines.push('- No proven actions were identified from the collected research.');
|
|
22
|
+
lines.push('');
|
|
23
|
+
}
|
|
24
|
+
for (const [index, item] of documentation.can.entries()) {
|
|
25
|
+
lines.push(`- ${normalizeAction(item.action)} -> ${item.scope}`);
|
|
26
|
+
lines.push(` Proof: ${ensureSentence(item.evidence)}`);
|
|
27
|
+
const shot = evidence[index];
|
|
28
|
+
if (shot) {
|
|
29
|
+
lines.push('');
|
|
30
|
+
lines.push(` `);
|
|
31
|
+
}
|
|
32
|
+
lines.push('');
|
|
33
|
+
}
|
|
34
|
+
lines.push('## User Might');
|
|
35
|
+
lines.push('');
|
|
36
|
+
if (documentation.might.length === 0) {
|
|
37
|
+
lines.push('- No assumption-based actions were identified.');
|
|
38
|
+
lines.push('');
|
|
39
|
+
}
|
|
40
|
+
for (const item of documentation.might) {
|
|
41
|
+
lines.push(`- ${normalizeAction(item.action, 'might')} -> ${item.scope}`);
|
|
42
|
+
lines.push(` Signal: ${ensureSentence(item.evidence)}`);
|
|
43
|
+
}
|
|
44
|
+
if (documentation.might.length > 0) {
|
|
45
|
+
lines.push('');
|
|
46
|
+
}
|
|
18
47
|
if (screenshots.length > 0) {
|
|
19
48
|
lines.push('## Screenshots');
|
|
20
49
|
lines.push('');
|
|
@@ -26,6 +55,24 @@ function renderPageDocumentation(state, documentation, screenshots = []) {
|
|
|
26
55
|
lines.push('');
|
|
27
56
|
}
|
|
28
57
|
}
|
|
58
|
+
const navigationLinks = renderNavigationLinks(state);
|
|
59
|
+
if (navigationLinks.length > 0) {
|
|
60
|
+
lines.push('## Navigation');
|
|
61
|
+
lines.push('');
|
|
62
|
+
for (const link of navigationLinks) {
|
|
63
|
+
lines.push(`- ${link.title}: ${link.url}`);
|
|
64
|
+
}
|
|
65
|
+
lines.push('');
|
|
66
|
+
}
|
|
67
|
+
const qualityNotes = documentation.qualityNotes;
|
|
68
|
+
if (qualityNotes && qualityNotes.length > 0) {
|
|
69
|
+
lines.push('## Coverage Notes');
|
|
70
|
+
lines.push('');
|
|
71
|
+
for (const note of qualityNotes) {
|
|
72
|
+
lines.push(`- ${ensureSentence(note)}`);
|
|
73
|
+
}
|
|
74
|
+
lines.push('');
|
|
75
|
+
}
|
|
29
76
|
const interactions = documentation.interactions;
|
|
30
77
|
const pageStateDiagram = renderPageStateDiagram(state.title || state.url || 'page', state.url || '', interactions || []);
|
|
31
78
|
if (pageStateDiagram) {
|
|
@@ -57,41 +104,6 @@ function renderPageDocumentation(state, documentation, screenshots = []) {
|
|
|
57
104
|
}
|
|
58
105
|
}
|
|
59
106
|
}
|
|
60
|
-
lines.push('## User Can');
|
|
61
|
-
lines.push('');
|
|
62
|
-
if (documentation.can.length === 0) {
|
|
63
|
-
lines.push('- No proven actions were identified from the collected research.');
|
|
64
|
-
lines.push('');
|
|
65
|
-
}
|
|
66
|
-
for (const item of documentation.can) {
|
|
67
|
-
lines.push(`- ${normalizeAction(item.action)} -> ${item.scope}`);
|
|
68
|
-
lines.push(` Proof: ${ensureSentence(item.evidence)}`);
|
|
69
|
-
}
|
|
70
|
-
if (documentation.can.length > 0) {
|
|
71
|
-
lines.push('');
|
|
72
|
-
}
|
|
73
|
-
lines.push('## User Might');
|
|
74
|
-
lines.push('');
|
|
75
|
-
if (documentation.might.length === 0) {
|
|
76
|
-
lines.push('- No assumption-based actions were identified.');
|
|
77
|
-
lines.push('');
|
|
78
|
-
}
|
|
79
|
-
for (const item of documentation.might) {
|
|
80
|
-
lines.push(`- ${normalizeAction(item.action, 'might')} -> ${item.scope}`);
|
|
81
|
-
lines.push(` Signal: ${ensureSentence(item.evidence)}`);
|
|
82
|
-
}
|
|
83
|
-
if (documentation.might.length > 0) {
|
|
84
|
-
lines.push('');
|
|
85
|
-
}
|
|
86
|
-
const qualityNotes = documentation.qualityNotes;
|
|
87
|
-
if (qualityNotes && qualityNotes.length > 0) {
|
|
88
|
-
lines.push('## Coverage Notes');
|
|
89
|
-
lines.push('');
|
|
90
|
-
for (const note of qualityNotes) {
|
|
91
|
-
lines.push(`- ${ensureSentence(note)}`);
|
|
92
|
-
}
|
|
93
|
-
lines.push('');
|
|
94
|
-
}
|
|
95
107
|
return matter.stringify(`${lines.join('\n').trimEnd()}\n`, {
|
|
96
108
|
url: state.url,
|
|
97
109
|
format: APPLICATION_SPEC_FORMAT,
|
|
@@ -111,17 +123,7 @@ function renderSpecIndex(outputDir, startPath, pages, skipped, maxPages) {
|
|
|
111
123
|
lines.push('');
|
|
112
124
|
const graph = buildStateGraph(outputDir, pages);
|
|
113
125
|
const mermaid = renderMermaidFromGraph(graph, true);
|
|
114
|
-
lines.push('## State Transitions');
|
|
115
|
-
lines.push('');
|
|
116
|
-
lines.push(`\`\`\`mermaid\n${mermaid}\n\`\`\``);
|
|
117
|
-
lines.push('');
|
|
118
126
|
const stateMap = renderStateMapFromGraph(graph);
|
|
119
|
-
if (stateMap) {
|
|
120
|
-
lines.push('### State Index');
|
|
121
|
-
lines.push('');
|
|
122
|
-
lines.push(stateMap);
|
|
123
|
-
lines.push('');
|
|
124
|
-
}
|
|
125
127
|
lines.push('## Pages');
|
|
126
128
|
lines.push('');
|
|
127
129
|
if (pages.length === 0) {
|
|
@@ -179,8 +181,29 @@ function renderSpecIndex(outputDir, startPath, pages, skipped, maxPages) {
|
|
|
179
181
|
}
|
|
180
182
|
lines.push('');
|
|
181
183
|
}
|
|
184
|
+
lines.push('## State Transitions');
|
|
185
|
+
lines.push('');
|
|
186
|
+
lines.push(`\`\`\`mermaid\n${mermaid}\n\`\`\``);
|
|
187
|
+
lines.push('');
|
|
188
|
+
if (stateMap) {
|
|
189
|
+
lines.push('### State Index');
|
|
190
|
+
lines.push('');
|
|
191
|
+
lines.push(stateMap);
|
|
192
|
+
lines.push('');
|
|
193
|
+
}
|
|
182
194
|
return `${lines.join('\n').trimEnd()}\n`;
|
|
183
195
|
}
|
|
196
|
+
function renderNavigationLinks(state) {
|
|
197
|
+
const links = [];
|
|
198
|
+
const seen = new Set();
|
|
199
|
+
for (const link of state.links || []) {
|
|
200
|
+
if (!link.url || seen.has(link.url))
|
|
201
|
+
continue;
|
|
202
|
+
seen.add(link.url);
|
|
203
|
+
links.push({ title: normalizeInlineText(link.title) || link.url, url: link.url });
|
|
204
|
+
}
|
|
205
|
+
return links;
|
|
206
|
+
}
|
|
184
207
|
function normalizeAction(action, kind = 'can') {
|
|
185
208
|
const trimmed = normalizeInlineText(action);
|
|
186
209
|
if (!trimmed) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFileSync } from 'node:fs';
|
|
2
2
|
import pixelmatch from 'pixelmatch';
|
|
3
3
|
import { PNG } from 'pngjs';
|
|
4
|
+
import { removeVisualAnnotations } from "../../../src/ai/researcher/coordinates.js";
|
|
4
5
|
const REGION_PADDING = 30;
|
|
5
6
|
const SCREENSHOT_OPTIONS = { animations: 'disabled', caret: 'hide' };
|
|
6
7
|
export async function captureInteractionBefore(page) {
|
|
@@ -43,7 +44,7 @@ export function findChangedRegion(beforeScreenshot, afterScreenshot, padding = R
|
|
|
43
44
|
const changedPixels = findChangedPixelBounds(before, after);
|
|
44
45
|
return changedPixels ? addPadding(changedPixels, before.width, before.height, padding) : null;
|
|
45
46
|
}
|
|
46
|
-
function saveRegion(after, region, filePath) {
|
|
47
|
+
export function saveRegion(after, region, filePath) {
|
|
47
48
|
const cropped = new PNG({ width: region.width, height: region.height });
|
|
48
49
|
PNG.bitblt(after, cropped, region.x, region.y, region.width, region.height, 0, 0);
|
|
49
50
|
writeFileSync(filePath, PNG.sync.write(cropped));
|
|
@@ -69,22 +70,28 @@ function findChangedPixelBounds(before, after) {
|
|
|
69
70
|
}
|
|
70
71
|
return { x: left, y: top, width: right - left + 1, height: bottom - top + 1 };
|
|
71
72
|
}
|
|
72
|
-
function
|
|
73
|
+
export function regionAround(png, box, viewport, padding) {
|
|
74
|
+
let scaleX = 1;
|
|
75
|
+
let scaleY = 1;
|
|
76
|
+
if (viewport) {
|
|
77
|
+
scaleX = png.width / viewport.width;
|
|
78
|
+
scaleY = png.height / viewport.height;
|
|
79
|
+
}
|
|
80
|
+
const scaled = {
|
|
81
|
+
x: Math.round(box.x * scaleX),
|
|
82
|
+
y: Math.round(box.y * scaleY),
|
|
83
|
+
width: Math.round(box.width * scaleX),
|
|
84
|
+
height: Math.round(box.height * scaleY),
|
|
85
|
+
};
|
|
86
|
+
return addPadding(scaled, png.width, png.height, Math.round(padding * scaleX));
|
|
87
|
+
}
|
|
88
|
+
export function addPadding(region, imageWidth, imageHeight, padding = REGION_PADDING) {
|
|
73
89
|
const x = Math.max(0, region.x - padding);
|
|
74
90
|
const y = Math.max(0, region.y - padding);
|
|
75
91
|
const maxX = Math.min(imageWidth, region.x + region.width + padding);
|
|
76
92
|
const maxY = Math.min(imageHeight, region.y + region.height + padding);
|
|
77
93
|
return { x, y, width: maxX - x, height: maxY - y };
|
|
78
94
|
}
|
|
79
|
-
async function removeVisualAnnotations(page) {
|
|
80
|
-
try {
|
|
81
|
-
await page.locator('[data-explorbot-annotation]').evaluateAll((elements) => {
|
|
82
|
-
for (const element of elements)
|
|
83
|
-
element.remove();
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
catch { }
|
|
87
|
-
}
|
|
88
95
|
async function findOverlayRegion(page, image, detectUnmarkedOverlay) {
|
|
89
96
|
let box = null;
|
|
90
97
|
try {
|
|
@@ -1,14 +1,21 @@
|
|
|
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.js";
|
|
3
5
|
import { parseResearchSections } from "../../../src/ai/researcher/parser.js";
|
|
6
|
+
import { parseAriaLocator } from "../../../src/utils/aria.js";
|
|
7
|
+
import { normalizeUrl } from "../../../src/state-manager.js";
|
|
4
8
|
import { safeFilename, sanitizeFilename } from "../../../src/utils/strings.js";
|
|
5
|
-
import { captureInteractionAfter, captureInteractionBefore } from "./interaction-screenshots.js";
|
|
9
|
+
import { captureInteractionAfter, captureInteractionBefore, regionAround, saveRegion } from "./interaction-screenshots.js";
|
|
6
10
|
const DEFAULT_MAX_SECTION_SCREENSHOTS = 8;
|
|
11
|
+
const EVIDENCE_PADDING = 250;
|
|
7
12
|
export async function captureDocumentationScreenshots(explorer, state, research, options) {
|
|
8
13
|
const page = explorer.page;
|
|
9
14
|
if (!page) {
|
|
10
15
|
return [];
|
|
11
16
|
}
|
|
17
|
+
await dismissTransientOverlay(page);
|
|
18
|
+
await removeVisualAnnotations(page);
|
|
12
19
|
mkdirSync(options.screenshotsDir, { recursive: true });
|
|
13
20
|
const screenshots = [];
|
|
14
21
|
const pageName = sanitizeFilename(state.url || 'page') || 'page';
|
|
@@ -26,6 +33,102 @@ export async function captureDocumentationScreenshots(explorer, state, research,
|
|
|
26
33
|
}
|
|
27
34
|
return screenshots;
|
|
28
35
|
}
|
|
36
|
+
export async function captureEvidenceScreenshots(explorer, state, documentation, options) {
|
|
37
|
+
const page = explorer.page;
|
|
38
|
+
if (!page) {
|
|
39
|
+
return documentation.can.map(() => null);
|
|
40
|
+
}
|
|
41
|
+
await dismissTransientOverlay(page);
|
|
42
|
+
await removeVisualAnnotations(page);
|
|
43
|
+
if (!isOnDocumentedPage(page, state)) {
|
|
44
|
+
return documentation.can.map(() => null);
|
|
45
|
+
}
|
|
46
|
+
mkdirSync(options.screenshotsDir, { recursive: true });
|
|
47
|
+
const pageName = sanitizeFilename(state.url || 'page') || 'page';
|
|
48
|
+
const shots = [];
|
|
49
|
+
for (const [index, item] of documentation.can.entries()) {
|
|
50
|
+
shots.push(await captureEvidenceScreenshot(page, pageName, index, item, options));
|
|
51
|
+
}
|
|
52
|
+
return shots;
|
|
53
|
+
}
|
|
54
|
+
async function captureEvidenceScreenshot(page, pageName, index, item, options) {
|
|
55
|
+
if (!item.element)
|
|
56
|
+
return null;
|
|
57
|
+
const element = resolveResearchLocator(item.element, options.research);
|
|
58
|
+
if (!element)
|
|
59
|
+
return null;
|
|
60
|
+
const locatorInfo = parseAriaLocator(element);
|
|
61
|
+
try {
|
|
62
|
+
const locator = resolveEvidenceLocator(page, element, locatorInfo);
|
|
63
|
+
if ((await locator.count()) !== 1)
|
|
64
|
+
return null;
|
|
65
|
+
await locator.scrollIntoViewIfNeeded({ timeout: 2000 });
|
|
66
|
+
const box = await locator.boundingBox();
|
|
67
|
+
if (!box || box.width <= 0 || box.height <= 0)
|
|
68
|
+
return null;
|
|
69
|
+
const image = await page.screenshot();
|
|
70
|
+
const png = PNG.sync.read(image);
|
|
71
|
+
const region = regionAround(png, box, await page.viewportSize(), EVIDENCE_PADDING);
|
|
72
|
+
const filePath = path.join(options.screenshotsDir, safeFilename(`${pageName}_can_${index + 1}`, '.png'));
|
|
73
|
+
saveRegion(png, region, filePath);
|
|
74
|
+
return {
|
|
75
|
+
title: item.action,
|
|
76
|
+
path: filePath,
|
|
77
|
+
relativePath: toMarkdownPath(options.pageFilePath, filePath),
|
|
78
|
+
kind: 'evidence',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function resolveResearchLocator(requested, research) {
|
|
86
|
+
if (!research)
|
|
87
|
+
return requested;
|
|
88
|
+
const requestedAria = parseAriaLocator(requested);
|
|
89
|
+
const requestedCss = unwrapLocator(requested);
|
|
90
|
+
for (const section of parseResearchSections(research)) {
|
|
91
|
+
for (const element of section.elements) {
|
|
92
|
+
if (requestedAria && element.aria?.role === requestedAria.role && element.aria.text === requestedAria.text) {
|
|
93
|
+
return `{ role: '${element.aria.role}', text: '${element.aria.text}' }`;
|
|
94
|
+
}
|
|
95
|
+
if (element.css && requestedCss === unwrapLocator(element.css))
|
|
96
|
+
return element.css;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
function unwrapLocator(locator) {
|
|
102
|
+
const trimmed = locator.trim();
|
|
103
|
+
const first = trimmed.at(0);
|
|
104
|
+
const last = trimmed.at(-1);
|
|
105
|
+
if (first === last && (first === '`' || first === '"' || first === "'")) {
|
|
106
|
+
return trimmed.slice(1, -1).trim();
|
|
107
|
+
}
|
|
108
|
+
return trimmed;
|
|
109
|
+
}
|
|
110
|
+
function isOnDocumentedPage(page, state) {
|
|
111
|
+
try {
|
|
112
|
+
const current = new URL(page.url());
|
|
113
|
+
const expected = new URL(state.url || '', current.origin);
|
|
114
|
+
return current.origin === expected.origin && normalizeUrl(current.href) === normalizeUrl(expected.href);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function resolveEvidenceLocator(page, element, locatorInfo) {
|
|
121
|
+
if (locatorInfo?.text) {
|
|
122
|
+
return page.getByRole(locatorInfo.role, { name: locatorInfo.text, exact: true });
|
|
123
|
+
}
|
|
124
|
+
return page.locator(element);
|
|
125
|
+
}
|
|
126
|
+
async function dismissTransientOverlay(page) {
|
|
127
|
+
try {
|
|
128
|
+
await page.keyboard.press('Escape');
|
|
129
|
+
}
|
|
130
|
+
catch { }
|
|
131
|
+
}
|
|
29
132
|
export function getScreenshotSections(research) {
|
|
30
133
|
const sections = [];
|
|
31
134
|
const seen = new Set();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ariaTemplateSignature } from "../../../src/utils/aria.js";
|
|
2
|
+
import { diceSimilarity } from "../../../src/utils/similarity.js";
|
|
3
|
+
const TEMPLATE_MIN_SIGNATURE_SIZE = 8;
|
|
4
|
+
const TEMPLATE_SIMILARITY_THRESHOLD = 90;
|
|
5
|
+
export function buildTemplateRecord(url, snapshot) {
|
|
6
|
+
const signature = ariaTemplateSignature(snapshot);
|
|
7
|
+
if (signature.size < TEMPLATE_MIN_SIGNATURE_SIZE)
|
|
8
|
+
return null;
|
|
9
|
+
return { url, signature };
|
|
10
|
+
}
|
|
11
|
+
export function findTemplateMatch(snapshot, known, threshold = TEMPLATE_SIMILARITY_THRESHOLD) {
|
|
12
|
+
const signature = ariaTemplateSignature(snapshot);
|
|
13
|
+
if (signature.size < TEMPLATE_MIN_SIGNATURE_SIZE)
|
|
14
|
+
return null;
|
|
15
|
+
let bestUrl = null;
|
|
16
|
+
let bestScore = 0;
|
|
17
|
+
for (const record of known) {
|
|
18
|
+
const score = diceSimilarity(signature, record.signature);
|
|
19
|
+
if (score < bestScore)
|
|
20
|
+
continue;
|
|
21
|
+
bestScore = score;
|
|
22
|
+
bestUrl = record.url;
|
|
23
|
+
}
|
|
24
|
+
if (bestScore < threshold)
|
|
25
|
+
return null;
|
|
26
|
+
return bestUrl;
|
|
27
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import dedent from 'dedent';
|
|
3
3
|
import { keepServerRunning } from "../../../src/browser-server.js";
|
|
4
|
+
import { RecommendedModelsCommand } from "../../../src/commands/recommended-models-command.js";
|
|
4
5
|
import { browserErrorMessage } from "../../../src/utils/browser-errors.js";
|
|
5
6
|
import { isVerboseMode, setQuietMode } from "../../../src/utils/logger.js";
|
|
6
7
|
import { clearActivityLine, trackActivityLine } from "./activity-line.js";
|
|
@@ -106,6 +107,7 @@ function addCommonOptions(cmd) {
|
|
|
106
107
|
.option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
|
|
107
108
|
.option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
|
|
108
109
|
.option('--url <url>', 'Page to open when the session has no page yet')
|
|
110
|
+
.option('--spec <path>', 'Docbot application spec directory or index.md to read as page knowledge')
|
|
109
111
|
.option('--endpoint <ep>', 'Websocket endpoint of a browser server to attach to, skipping discovery')
|
|
110
112
|
.option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
|
|
111
113
|
.addHelpText('after', `\n${sessionHelp}`);
|
|
@@ -118,6 +120,8 @@ function primaFor(options) {
|
|
|
118
120
|
process.env.EXPLORBOT_AI_MODEL = options.model;
|
|
119
121
|
if (options.visionModel)
|
|
120
122
|
process.env.EXPLORBOT_VISION_MODEL = options.visionModel;
|
|
123
|
+
if (options.spec)
|
|
124
|
+
process.env.EXPLORBOT_SPEC = options.spec;
|
|
121
125
|
return new Prima(buildOptions(options));
|
|
122
126
|
}
|
|
123
127
|
async function runPrima(options, command, run) {
|
|
@@ -196,6 +200,7 @@ export function createPrimaCommands(name = 'prima') {
|
|
|
196
200
|
await prima.stop().catch(() => { });
|
|
197
201
|
process.exit(0);
|
|
198
202
|
});
|
|
203
|
+
RecommendedModelsCommand.register(cmd);
|
|
199
204
|
addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command'))
|
|
200
205
|
.addHelpText('after', `\n${statusHelp}`)
|
|
201
206
|
.action(async (hash, options) => {
|
|
@@ -14,7 +14,7 @@ import { getAliveEndpoint, launchServer, listInstances, stopServer } from "../..
|
|
|
14
14
|
import { ConfigCommand } from "../../../src/commands/config-command.js";
|
|
15
15
|
import { ConfigMissingError, ConfigParser, EXPLORBOT_ENV_VARS, outputPath } from "../../../src/config.js";
|
|
16
16
|
import { ExplorBot } from "../../../src/explorbot.js";
|
|
17
|
-
import { listSites } from "../../../src/global-config.js";
|
|
17
|
+
import { findSiteWith, listSites } from "../../../src/global-config.js";
|
|
18
18
|
import { Reporter } from "../../../src/reporter.js";
|
|
19
19
|
import { Stats } from "../../../src/stats.js";
|
|
20
20
|
import { Task, Test, TestResult } from "../../../src/test-plan.js";
|
|
@@ -1006,8 +1006,7 @@ export class Prima {
|
|
|
1006
1006
|
}
|
|
1007
1007
|
async status(hash) {
|
|
1008
1008
|
if (!this.artifactsDir) {
|
|
1009
|
-
const
|
|
1010
|
-
const site = sites.find((candidate) => existsSync(path.join(candidate.dir, 'output', 'prima', hash))) || sites[0];
|
|
1009
|
+
const site = findSiteWith(path.join('output', 'prima', hash)) || listSites()[0];
|
|
1011
1010
|
if (site && !this.configBaseUrl())
|
|
1012
1011
|
this.sessionUrl = site.url;
|
|
1013
1012
|
await this.loadConfig();
|
|
@@ -1049,7 +1048,7 @@ export class Prima {
|
|
|
1049
1048
|
aria: result.ariaSnapshot,
|
|
1050
1049
|
html: await result.combinedHtml(),
|
|
1051
1050
|
screenshot: result.screenshot,
|
|
1052
|
-
requests: this.bot.requestStore().
|
|
1051
|
+
requests: this.bot.requestStore().getMadeRequests(),
|
|
1053
1052
|
});
|
|
1054
1053
|
}
|
|
1055
1054
|
statusHash() {
|
package/dist/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Study the page and figure out its business purpose. What is this page FOR? What would a user come here to do?
|
|
2
2
|
|
|
3
3
|
Based on the page type, propose tests for COMPLETE user workflows:
|
|
4
|
-
- If this is a data page (lists, tables): test
|
|
4
|
+
- If this is a data page (lists, tables): test create, edit and delete as separate tests, each preparing the item it acts on
|
|
5
5
|
- If the page has inputs to fill in: test the full commit flow, not just that the controls render
|
|
6
6
|
- If this has filters and search: test filtering AND verify results change, not just "filter tab clicked"
|
|
7
7
|
- If this has modals/dropdowns: test the ACTION inside them, not just opening/closing them
|
|
@@ -2,7 +2,7 @@ import { type HtmlConfig } from './config.js';
|
|
|
2
2
|
import type { Link, WebPageState } from './state-manager.js';
|
|
3
3
|
import { TTLCache } from './utils/cache.js';
|
|
4
4
|
import { type HtmlDiffPart, type HtmlDiffResult } from './utils/html-diff.js';
|
|
5
|
-
import {
|
|
5
|
+
import { Region } from './utils/region.js';
|
|
6
6
|
interface ActionResultData extends WebPageState {
|
|
7
7
|
html?: string;
|
|
8
8
|
fullUrl?: string | undefined;
|
|
@@ -83,7 +83,7 @@ export declare class ActionResult implements ActionResultData {
|
|
|
83
83
|
notes: string[];
|
|
84
84
|
links: Link[];
|
|
85
85
|
verifications?: Record<string, boolean>;
|
|
86
|
-
overlay:
|
|
86
|
+
overlay: Region;
|
|
87
87
|
_diffCache: {
|
|
88
88
|
previousId: number | undefined;
|
|
89
89
|
diff: Diff;
|