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.
Files changed (128) hide show
  1. package/bin/explorbot-cli.ts +8 -3
  2. package/boat/api-tester/src/ai/curler-tools.ts +1 -1
  3. package/boat/api-tester/src/cli.ts +3 -0
  4. package/boat/doc-collector/src/ai/documentarian.ts +57 -5
  5. package/boat/doc-collector/src/ai/tools.ts +1 -1
  6. package/boat/doc-collector/src/cli.ts +18 -2
  7. package/boat/doc-collector/src/config.ts +3 -0
  8. package/boat/doc-collector/src/docbot.ts +56 -11
  9. package/boat/doc-collector/src/docs-renderer.ts +79 -55
  10. package/boat/doc-collector/src/interaction-screenshots.ts +19 -10
  11. package/boat/doc-collector/src/screenshots.ts +106 -3
  12. package/boat/doc-collector/src/template-dedup.ts +31 -0
  13. package/boat/prima/src/cli.ts +3 -0
  14. package/boat/prima/src/prima.ts +3 -4
  15. package/dist/bin/explorbot-cli.js +9 -3
  16. package/dist/boat/api-tester/src/ai/curler-tools.js +1 -1
  17. package/dist/boat/api-tester/src/cli.js +2 -0
  18. package/dist/boat/doc-collector/src/ai/documentarian.js +62 -4
  19. package/dist/boat/doc-collector/src/ai/tools.js +1 -1
  20. package/dist/boat/doc-collector/src/cli.js +15 -2
  21. package/dist/boat/doc-collector/src/config.js +1 -0
  22. package/dist/boat/doc-collector/src/docbot.js +52 -10
  23. package/dist/boat/doc-collector/src/docs-renderer.js +69 -46
  24. package/dist/boat/doc-collector/src/interaction-screenshots.js +18 -11
  25. package/dist/boat/doc-collector/src/screenshots.js +104 -1
  26. package/dist/boat/doc-collector/src/template-dedup.js +27 -0
  27. package/dist/boat/prima/src/cli.js +2 -0
  28. package/dist/boat/prima/src/prima.js +3 -4
  29. package/dist/package.json +1 -1
  30. package/dist/src/action-result.d.ts +2 -2
  31. package/dist/src/action-result.js +15 -17
  32. package/dist/src/action.d.ts +1 -1
  33. package/dist/src/action.js +15 -15
  34. package/dist/src/ai/fisherman/request-haul.d.ts +11 -0
  35. package/dist/src/ai/fisherman/request-haul.js +28 -0
  36. package/dist/src/ai/fisherman-tools.d.ts +10 -1
  37. package/dist/src/ai/fisherman-tools.js +35 -31
  38. package/dist/src/ai/fisherman.d.ts +3 -2
  39. package/dist/src/ai/fisherman.js +8 -6
  40. package/dist/src/ai/pilot.js +21 -16
  41. package/dist/src/ai/provider.js +8 -14
  42. package/dist/src/ai/researcher/coordinates.d.ts +2 -0
  43. package/dist/src/ai/researcher/coordinates.js +13 -1
  44. package/dist/src/ai/researcher/deep-analysis.js +9 -9
  45. package/dist/src/ai/researcher/fingerprint-worker.js +1 -11
  46. package/dist/src/ai/researcher.js +1 -1
  47. package/dist/src/ai/rules.js +8 -0
  48. package/dist/src/ai/tester.js +20 -20
  49. package/dist/src/ai/tools.d.ts +1 -1
  50. package/dist/src/ai/tools.js +18 -74
  51. package/dist/src/api/request-store.d.ts +1 -5
  52. package/dist/src/api/request-store.js +12 -22
  53. package/dist/src/commands/config-command.js +2 -9
  54. package/dist/src/commands/index.js +2 -0
  55. package/dist/src/commands/init-command.js +3 -7
  56. package/dist/src/commands/plans-command.d.ts +4 -1
  57. package/dist/src/commands/plans-command.js +4 -4
  58. package/dist/src/commands/recommended-models-command.d.ts +9 -0
  59. package/dist/src/commands/recommended-models-command.js +85 -0
  60. package/dist/src/config.d.ts +2 -0
  61. package/dist/src/config.js +7 -2
  62. package/dist/src/experience-tracker.js +1 -1
  63. package/dist/src/explorbot.js +3 -3
  64. package/dist/src/global-config.d.ts +2 -0
  65. package/dist/src/global-config.js +6 -0
  66. package/dist/src/state-manager.d.ts +3 -3
  67. package/dist/src/state-manager.js +8 -8
  68. package/dist/src/test-plan.d.ts +1 -0
  69. package/dist/src/test-plan.js +13 -9
  70. package/dist/src/utils/aria.d.ts +1 -1
  71. package/dist/src/utils/aria.js +23 -2
  72. package/dist/src/utils/cli-section.d.ts +1 -0
  73. package/dist/src/utils/cli-section.js +12 -0
  74. package/dist/src/utils/html-diff.d.ts +1 -0
  75. package/dist/src/utils/html-diff.js +5 -5
  76. package/dist/src/utils/html.d.ts +2 -0
  77. package/dist/src/utils/html.js +5 -4
  78. package/dist/src/utils/overlay.d.ts +19 -55
  79. package/dist/src/utils/overlay.js +94 -148
  80. package/dist/src/utils/region.d.ts +41 -0
  81. package/dist/src/utils/region.js +99 -0
  82. package/dist/src/utils/similarity.d.ts +1 -0
  83. package/dist/src/utils/similarity.js +11 -0
  84. package/dist/src/utils/url-matcher.d.ts +1 -1
  85. package/dist/src/utils/url-matcher.js +4 -4
  86. package/docs/doc-collection/basics.md +11 -5
  87. package/docs/doc-collection/crawling.md +21 -1
  88. package/docs/reference/commands.md +3 -0
  89. package/docs/superpowers/plans/2026-09-03-model-resolves-click-ambiguity.md +509 -0
  90. package/docs/workflow/application-spec.md +4 -0
  91. package/package.json +1 -1
  92. package/src/action-result.ts +15 -16
  93. package/src/action.ts +16 -15
  94. package/src/ai/fisherman/request-haul.ts +32 -0
  95. package/src/ai/fisherman-tools.ts +38 -32
  96. package/src/ai/fisherman.ts +8 -7
  97. package/src/ai/pilot.ts +19 -14
  98. package/src/ai/provider.ts +8 -12
  99. package/src/ai/researcher/coordinates.ts +13 -1
  100. package/src/ai/researcher/deep-analysis.ts +9 -9
  101. package/src/ai/researcher/fingerprint-worker.ts +1 -10
  102. package/src/ai/researcher.ts +1 -1
  103. package/src/ai/rules.ts +8 -0
  104. package/src/ai/tester.ts +18 -18
  105. package/src/ai/tools.ts +18 -86
  106. package/src/api/request-store.ts +13 -27
  107. package/src/commands/config-command.ts +2 -7
  108. package/src/commands/index.ts +2 -0
  109. package/src/commands/init-command.ts +4 -11
  110. package/src/commands/plans-command.ts +5 -5
  111. package/src/commands/recommended-models-command.ts +92 -0
  112. package/src/config.ts +9 -2
  113. package/src/experience-tracker.ts +1 -1
  114. package/src/explorbot.ts +3 -3
  115. package/src/global-config.ts +8 -0
  116. package/src/state-manager.ts +10 -9
  117. package/src/test-plan.ts +12 -10
  118. package/src/utils/aria.ts +26 -3
  119. package/src/utils/cli-section.ts +12 -0
  120. package/src/utils/html-diff.ts +6 -6
  121. package/src/utils/html.ts +6 -4
  122. package/src/utils/overlay.ts +105 -192
  123. package/src/utils/region.ts +132 -0
  124. package/src/utils/similarity.ts +9 -0
  125. package/src/utils/url-matcher.ts +3 -3
  126. package/dist/src/utils/request-map.d.ts +0 -7
  127. package/dist/src/utils/request-map.js +0 -16
  128. 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(` ![${normalizeInlineText(item.action)}](${shot.relativePath})`);
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 addPadding(region, imageWidth, imageHeight, padding = REGION_PADDING) {
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 sites = listSites();
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().getRequests(),
1051
+ requests: this.bot.requestStore().getMadeRequests(),
1053
1052
  });
1054
1053
  }
1055
1054
  statusHash() {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -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 { Overlay } from './utils/overlay.js';
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: 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 Overlay();
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.present)
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.present && (!previousState.overlay.present || previousState.overlay.name !== this.overlay.name)) {
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
- let subtree = await minifyHtml(htmlCombinedSnapshot(this.overlay.html, htmlConfig?.combined));
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.present && this.overlay.name)
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
- if (part.subtree.length <= HTML_PART_SUBTREE_BUDGET)
527
- return part;
528
- const head = part.subtree.slice(0, HTML_PART_SUBTREE_BUDGET);
529
- return {
530
- ...part,
531
- subtree: `${head}...<!-- truncated ${part.subtree.length - HTML_PART_SUBTREE_BUDGET} chars -->`,
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;
@@ -33,7 +33,7 @@ declare class Action {
33
33
  includeScreenshot?: boolean;
34
34
  codeBlock?: string;
35
35
  }): Promise<ActionResult>;
36
- detectRegionOfInterest(result: ActionResult): Promise<void>;
36
+ detectRegion(result: ActionResult): Promise<void>;
37
37
  captureMainDocumentStatus(): Promise<number | undefined>;
38
38
  captureResponses(): () => void;
39
39
  recordNetworkCall(request: any, status: number): void;
@@ -166,7 +166,7 @@ class Action {
166
166
  iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
167
167
  });
168
168
  if (!frame)
169
- await this.detectRegionOfInterest(result).catch((err) => debugLog('Region detection failed:', err.message));
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 detectRegionOfInterest(result) {
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 previousOverlay = previous.overlay;
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.detected && previousOverlay.detected && previousOverlay.root && previousOverlay.type === result.overlay.type && previousOverlay.name === result.overlay.name) {
191
- result.overlay = previousOverlay;
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 && previousOverlay.present && previousOverlay.xpath && !result.overlay.detected)
198
- result.overlay = previousOverlay;
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 && previousOverlay.present && previousOverlay.xpath) {
203
- if (await overlayPage.isStillOpen(previousOverlay)) {
204
- carried = previousOverlay;
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: ${previousOverlay.name || previousOverlay.type}`);
208
- if (!result.overlay.detected) {
209
- const parent = previousOverlay.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 = new Overlay(parent);
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.detected) {
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
- export declare function createFishermanTools(apiClient: ApiClient, requestStore: RequestStore, opts: {
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;