explorbot 0.4.1 → 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 +8 -3
- package/boat/api-tester/src/ai/curler-tools.ts +1 -1
- package/boat/api-tester/src/cli.ts +3 -0
- 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 +18 -2
- package/boat/doc-collector/src/config.ts +3 -0
- package/boat/doc-collector/src/docbot.ts +56 -11
- 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/src/cli.ts +3 -0
- package/boat/prima/src/prima.ts +3 -4
- package/dist/bin/explorbot-cli.js +9 -3
- package/dist/boat/api-tester/src/ai/curler-tools.js +1 -1
- package/dist/boat/api-tester/src/cli.js +2 -0
- 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 +15 -2
- package/dist/boat/doc-collector/src/config.js +1 -0
- package/dist/boat/doc-collector/src/docbot.js +52 -10
- 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/src/cli.js +2 -0
- package/dist/boat/prima/src/prima.js +3 -4
- package/dist/package.json +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/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/pilot.js +21 -16
- 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 +1 -1
- package/dist/src/ai/rules.js +8 -0
- 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/commands/config-command.js +2 -9
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/init-command.js +3 -7
- 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 +2 -0
- package/dist/src/config.js +7 -2
- package/dist/src/experience-tracker.js +1 -1
- package/dist/src/explorbot.js +3 -3
- package/dist/src/global-config.d.ts +2 -0
- package/dist/src/global-config.js +6 -0
- 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/doc-collection/basics.md +11 -5
- package/docs/doc-collection/crawling.md +21 -1
- package/docs/reference/commands.md +3 -0
- package/docs/superpowers/plans/2026-09-03-model-resolves-click-ambiguity.md +509 -0
- package/docs/workflow/application-spec.md +4 -0
- package/package.json +1 -1
- package/src/action-result.ts +15 -16
- package/src/action.ts +16 -15
- 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/pilot.ts +19 -14
- 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 +1 -1
- package/src/ai/rules.ts +8 -0
- package/src/ai/tester.ts +18 -18
- package/src/ai/tools.ts +18 -86
- package/src/api/request-store.ts +13 -27
- package/src/commands/config-command.ts +2 -7
- package/src/commands/index.ts +2 -0
- package/src/commands/init-command.ts +4 -11
- package/src/commands/plans-command.ts +5 -5
- package/src/commands/recommended-models-command.ts +92 -0
- package/src/config.ts +9 -2
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +3 -3
- package/src/global-config.ts +8 -0
- 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
|
@@ -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";
|
|
@@ -199,6 +200,7 @@ export function createPrimaCommands(name = 'prima') {
|
|
|
199
200
|
await prima.stop().catch(() => { });
|
|
200
201
|
process.exit(0);
|
|
201
202
|
});
|
|
203
|
+
RecommendedModelsCommand.register(cmd);
|
|
202
204
|
addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command'))
|
|
203
205
|
.addHelpText('after', `\n${statusHelp}`)
|
|
204
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
|
@@ -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;
|
|
@@ -6,6 +6,7 @@ import { htmlDiff, liveRegionMessages } from "./utils/html-diff.js";
|
|
|
6
6
|
import { extractHeadings, extractLinks, extractTargetedHtml, htmlCombinedSnapshot, htmlMinimalUISnapshot, htmlTextSnapshot, minifyHtml } from "./utils/html.js";
|
|
7
7
|
import { createDebug } from "./utils/logger.js";
|
|
8
8
|
import { Overlay } from "./utils/overlay.js";
|
|
9
|
+
import { Region } from "./utils/region.js";
|
|
9
10
|
import { slugify } from "./utils/strings.js";
|
|
10
11
|
import { extractStatePath, matchesUrl } from "./utils/url-matcher.js";
|
|
11
12
|
const debugLog = createDebug('explorbot:state');
|
|
@@ -38,7 +39,7 @@ export class ActionResult {
|
|
|
38
39
|
notes = [];
|
|
39
40
|
links = [];
|
|
40
41
|
verifications;
|
|
41
|
-
overlay = new
|
|
42
|
+
overlay = new Region();
|
|
42
43
|
_diffCache = null;
|
|
43
44
|
constructor(data) {
|
|
44
45
|
this.id = data.id;
|
|
@@ -198,7 +199,7 @@ export class ActionResult {
|
|
|
198
199
|
if (record.region && this.overlay.name !== record.region)
|
|
199
200
|
return false;
|
|
200
201
|
if (record.root) {
|
|
201
|
-
if (!this.overlay.
|
|
202
|
+
if (!this.overlay.isOpen)
|
|
202
203
|
return false;
|
|
203
204
|
if (this.overlay.root && this.overlay.root !== record.root)
|
|
204
205
|
return false;
|
|
@@ -446,15 +447,12 @@ export class ActionResult {
|
|
|
446
447
|
pageDiff.ariaChanges = diff.ariaChanged;
|
|
447
448
|
pageDiff.ariaChangeCount = diff.ariaChangeCount;
|
|
448
449
|
}
|
|
449
|
-
if (this.overlay.
|
|
450
|
+
if (this.overlay.isOpen && (!previousState.overlay.isOpen || previousState.overlay.name !== this.overlay.name)) {
|
|
450
451
|
pageDiff.areaOfInterest = this.overlay.describe();
|
|
451
452
|
}
|
|
452
453
|
if (pageDiff.areaOfInterest && this.overlay.html && this.overlay.root) {
|
|
453
454
|
const htmlConfig = ConfigParser.getInstance().getConfig().html;
|
|
454
|
-
|
|
455
|
-
if (subtree.length > HTML_PART_SUBTREE_BUDGET) {
|
|
456
|
-
subtree = `${subtree.slice(0, HTML_PART_SUBTREE_BUDGET)}...<!-- truncated -->`;
|
|
457
|
-
}
|
|
455
|
+
const subtree = trimSubtree(await minifyHtml(htmlCombinedSnapshot(this.overlay.html, htmlConfig?.combined)));
|
|
458
456
|
pageDiff.htmlParts = [{ container: this.overlay.root, subtree, rawSize: subtree.length, added: [], removed: [] }];
|
|
459
457
|
}
|
|
460
458
|
else if (diff.isSameUrl() && diff.htmlParts.length > 0) {
|
|
@@ -478,7 +476,7 @@ export class ActionResult {
|
|
|
478
476
|
parts.push(`h1_${this.h1}`);
|
|
479
477
|
if (this.h2)
|
|
480
478
|
parts.push(`h2_${this.h2}`);
|
|
481
|
-
if (includeRegion && this.overlay.
|
|
479
|
+
if (includeRegion && this.overlay.isOpen && this.overlay.name)
|
|
482
480
|
parts.push(`region_${this.overlay.name}`);
|
|
483
481
|
let stateString = slugify(parts.map((part) => part.substring(0, 100)).join('_'));
|
|
484
482
|
if (stateString.length > 200) {
|
|
@@ -511,6 +509,7 @@ const CONSOLE_ERROR_LIMIT = 3;
|
|
|
511
509
|
const HTML_PARTS_TOTAL_BUDGET = 8000;
|
|
512
510
|
const HTML_PARTS_COUNT_LIMIT = 8;
|
|
513
511
|
const HTML_PART_SUBTREE_BUDGET = 2000;
|
|
512
|
+
const SUBTREE_TAIL_BUDGET = 600;
|
|
514
513
|
function collapseHtmlParts(parts) {
|
|
515
514
|
const total = parts.reduce((sum, p) => sum + p.subtree.length, 0);
|
|
516
515
|
const fullPageReRender = total > HTML_PARTS_TOTAL_BUDGET || parts.length > HTML_PARTS_COUNT_LIMIT;
|
|
@@ -522,15 +521,14 @@ function collapseHtmlParts(parts) {
|
|
|
522
521
|
subtree: `<html><head></head><body>...collapsed (${part.subtree.length} chars, ${part.added.length} added, ${part.removed.length} removed)...</body></html>`,
|
|
523
522
|
}));
|
|
524
523
|
}
|
|
525
|
-
return parts.map((part) => {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
return
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
});
|
|
524
|
+
return parts.map((part) => ({ ...part, subtree: trimSubtree(part.subtree) }));
|
|
525
|
+
}
|
|
526
|
+
function trimSubtree(subtree) {
|
|
527
|
+
if (subtree.length <= HTML_PART_SUBTREE_BUDGET)
|
|
528
|
+
return subtree;
|
|
529
|
+
const head = subtree.slice(0, HTML_PART_SUBTREE_BUDGET - SUBTREE_TAIL_BUDGET);
|
|
530
|
+
const tail = subtree.slice(-SUBTREE_TAIL_BUDGET);
|
|
531
|
+
return `${head}...<!-- truncated ${subtree.length - HTML_PART_SUBTREE_BUDGET} chars -->${tail}`;
|
|
534
532
|
}
|
|
535
533
|
export class Diff {
|
|
536
534
|
current;
|
package/dist/src/action.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ declare class Action {
|
|
|
33
33
|
includeScreenshot?: boolean;
|
|
34
34
|
codeBlock?: string;
|
|
35
35
|
}): Promise<ActionResult>;
|
|
36
|
-
|
|
36
|
+
detectRegion(result: ActionResult): Promise<void>;
|
|
37
37
|
captureMainDocumentStatus(): Promise<number | undefined>;
|
|
38
38
|
captureResponses(): () => void;
|
|
39
39
|
recordNetworkCall(request: any, status: number): void;
|
package/dist/src/action.js
CHANGED
|
@@ -166,7 +166,7 @@ class Action {
|
|
|
166
166
|
iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
|
|
167
167
|
});
|
|
168
168
|
if (!frame)
|
|
169
|
-
await this.
|
|
169
|
+
await this.detectRegion(result).catch((err) => debugLog('Region detection failed:', err.message));
|
|
170
170
|
this.stateManager.updateState(result, codeBlock);
|
|
171
171
|
return result;
|
|
172
172
|
}
|
|
@@ -179,36 +179,36 @@ class Action {
|
|
|
179
179
|
return new ActionResult({ url, error: msg });
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
|
-
async
|
|
182
|
+
async detectRegion(result) {
|
|
183
183
|
const previousState = this.stateManager.getCurrentState();
|
|
184
184
|
if (!previousState)
|
|
185
185
|
return;
|
|
186
186
|
const previous = ActionResult.fromState(previousState);
|
|
187
|
-
const
|
|
187
|
+
const previousRegion = previous.overlay;
|
|
188
188
|
const sameUrl = !!previous.url && result.isSameUrl({ url: previous.url });
|
|
189
189
|
const overlayPage = new OverlayPage(this.playwrightHelper.page);
|
|
190
|
-
if (result.overlay.
|
|
191
|
-
result.overlay =
|
|
190
|
+
if (result.overlay.isModal && previousRegion.isModal && previousRegion.root && previousRegion.type === result.overlay.type && previousRegion.name === result.overlay.name) {
|
|
191
|
+
result.overlay = previousRegion;
|
|
192
192
|
return;
|
|
193
193
|
}
|
|
194
194
|
if (!previous.html)
|
|
195
195
|
return;
|
|
196
196
|
if (previous.html === result.html) {
|
|
197
|
-
if (sameUrl &&
|
|
198
|
-
result.overlay =
|
|
197
|
+
if (sameUrl && previousRegion.isOpen && previousRegion.xpath && !result.overlay.isModal)
|
|
198
|
+
result.overlay = previousRegion;
|
|
199
199
|
return;
|
|
200
200
|
}
|
|
201
201
|
let carried = null;
|
|
202
|
-
if (sameUrl &&
|
|
203
|
-
if (await overlayPage.isStillOpen(
|
|
204
|
-
carried =
|
|
202
|
+
if (sameUrl && previousRegion.isOpen && previousRegion.xpath) {
|
|
203
|
+
if (await overlayPage.isStillOpen(previousRegion)) {
|
|
204
|
+
carried = previousRegion;
|
|
205
205
|
}
|
|
206
206
|
else {
|
|
207
|
-
debugLog(`Region closed: ${
|
|
208
|
-
if (!result.overlay.
|
|
209
|
-
const parent =
|
|
207
|
+
debugLog(`Region closed: ${previousRegion.name || previousRegion.type}`);
|
|
208
|
+
if (!result.overlay.isModal) {
|
|
209
|
+
const parent = previousRegion.parent;
|
|
210
210
|
if (parent?.xpath) {
|
|
211
|
-
const restored =
|
|
211
|
+
const restored = Overlay.resolve({ overlay: parent });
|
|
212
212
|
if (await overlayPage.isStillOpen(restored))
|
|
213
213
|
result.overlay = restored;
|
|
214
214
|
}
|
|
@@ -224,7 +224,7 @@ class Action {
|
|
|
224
224
|
sameUrl,
|
|
225
225
|
previousHtml: previous.html,
|
|
226
226
|
});
|
|
227
|
-
if (result.overlay
|
|
227
|
+
if (result.overlay instanceof Overlay) {
|
|
228
228
|
if (detected)
|
|
229
229
|
result.overlay = result.overlay.withGeometry(detected);
|
|
230
230
|
return;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RequestResult } from '../../api/request-result.js';
|
|
2
|
+
import { type RequestStore } from '../../api/request-store.js';
|
|
3
|
+
export declare class RequestHaul {
|
|
4
|
+
store: RequestStore;
|
|
5
|
+
start: number;
|
|
6
|
+
constructor(store: RequestStore);
|
|
7
|
+
requests(): RequestResult[];
|
|
8
|
+
failed(): RequestResult[];
|
|
9
|
+
successfulWrites(): RequestResult[];
|
|
10
|
+
byId(): Map<string, RequestResult>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { isFailedRequest } from "../../api/request-store.js";
|
|
2
|
+
export class RequestHaul {
|
|
3
|
+
store;
|
|
4
|
+
start;
|
|
5
|
+
constructor(store) {
|
|
6
|
+
this.store = store;
|
|
7
|
+
this.start = store.getMadeRequests().length;
|
|
8
|
+
}
|
|
9
|
+
requests() {
|
|
10
|
+
return this.store.getMadeRequests().slice(this.start);
|
|
11
|
+
}
|
|
12
|
+
failed() {
|
|
13
|
+
return this.requests().filter(isFailedRequest);
|
|
14
|
+
}
|
|
15
|
+
successfulWrites() {
|
|
16
|
+
return this.requests().filter((r) => r.isWrite && !r.error && r.status >= 200 && r.status < 400);
|
|
17
|
+
}
|
|
18
|
+
byId() {
|
|
19
|
+
const map = new Map();
|
|
20
|
+
for (const request of this.successfulWrites()) {
|
|
21
|
+
const { id } = request.extractIdAndTitle();
|
|
22
|
+
if (id === undefined)
|
|
23
|
+
continue;
|
|
24
|
+
map.set(String(id), request);
|
|
25
|
+
}
|
|
26
|
+
return map;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ApiClient } from '../api/api-client.js';
|
|
2
2
|
import type { RequestStore } from '../api/request-store.js';
|
|
3
|
-
|
|
3
|
+
import type { RequestHaul } from './fisherman/request-haul.js';
|
|
4
|
+
export declare function createFishermanTools(apiClient: ApiClient, requestStore: RequestStore, haul: RequestHaul, opts: {
|
|
4
5
|
spec?: any;
|
|
5
6
|
baseEndpoint?: string;
|
|
6
7
|
}): {
|
|
@@ -120,6 +121,14 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
|
|
|
120
121
|
isFinished: () => boolean;
|
|
121
122
|
finishFromText: (text?: string) => void;
|
|
122
123
|
};
|
|
124
|
+
export declare function verifyFinish(haul: RequestHaul, input: {
|
|
125
|
+
summary: string;
|
|
126
|
+
created: FishermanResult['created'];
|
|
127
|
+
failed?: FishermanResult['failed'];
|
|
128
|
+
}): {
|
|
129
|
+
result: FishermanResult | null;
|
|
130
|
+
error?: string;
|
|
131
|
+
};
|
|
123
132
|
export interface FishermanResult {
|
|
124
133
|
success: boolean;
|
|
125
134
|
summary: string;
|