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.
Files changed (177) hide show
  1. package/bin/explorbot-cli.ts +13 -6
  2. package/boat/api-tester/src/ai/chief.ts +7 -1
  3. package/boat/api-tester/src/ai/curler-tools.ts +1 -1
  4. package/boat/api-tester/src/ai/curler.ts +7 -1
  5. package/boat/api-tester/src/apibot.ts +10 -4
  6. package/boat/api-tester/src/cli.ts +15 -2
  7. package/boat/api-tester/src/config.ts +28 -8
  8. package/boat/doc-collector/bin/doc-collector-cli.ts +3 -2
  9. package/boat/doc-collector/src/ai/documentarian.ts +57 -5
  10. package/boat/doc-collector/src/ai/tools.ts +1 -1
  11. package/boat/doc-collector/src/cli.ts +21 -3
  12. package/boat/doc-collector/src/config.ts +3 -0
  13. package/boat/doc-collector/src/docbot.ts +57 -12
  14. package/boat/doc-collector/src/docs-renderer.ts +79 -55
  15. package/boat/doc-collector/src/interaction-screenshots.ts +19 -10
  16. package/boat/doc-collector/src/screenshots.ts +106 -3
  17. package/boat/doc-collector/src/template-dedup.ts +31 -0
  18. package/boat/prima/bin/prima-cli.ts +2 -0
  19. package/boat/prima/src/cli.ts +5 -0
  20. package/boat/prima/src/prima.ts +3 -4
  21. package/dist/bin/explorbot-cli.js +14 -6
  22. package/dist/boat/api-tester/bin/apibot-cli.js +3 -2
  23. package/dist/boat/api-tester/src/ai/chief.js +6 -1
  24. package/dist/boat/api-tester/src/ai/curler-tools.js +1 -1
  25. package/dist/boat/api-tester/src/ai/curler.js +6 -1
  26. package/dist/boat/api-tester/src/apibot.js +7 -3
  27. package/dist/boat/api-tester/src/cli.js +14 -2
  28. package/dist/boat/api-tester/src/config.js +31 -8
  29. package/dist/boat/doc-collector/bin/doc-collector-cli.js +3 -2
  30. package/dist/boat/doc-collector/src/ai/documentarian.js +62 -4
  31. package/dist/boat/doc-collector/src/ai/tools.js +1 -1
  32. package/dist/boat/doc-collector/src/cli.js +18 -3
  33. package/dist/boat/doc-collector/src/config.js +1 -0
  34. package/dist/boat/doc-collector/src/docbot.js +53 -11
  35. package/dist/boat/doc-collector/src/docs-renderer.js +69 -46
  36. package/dist/boat/doc-collector/src/interaction-screenshots.js +18 -11
  37. package/dist/boat/doc-collector/src/screenshots.js +104 -1
  38. package/dist/boat/doc-collector/src/template-dedup.js +27 -0
  39. package/dist/boat/prima/bin/prima-cli.js +2 -0
  40. package/dist/boat/prima/src/cli.js +5 -0
  41. package/dist/boat/prima/src/prima.js +3 -4
  42. package/dist/package.json +1 -1
  43. package/dist/rules/planner/styles/normal.md +1 -1
  44. package/dist/src/action-result.d.ts +2 -2
  45. package/dist/src/action-result.js +15 -17
  46. package/dist/src/action.d.ts +1 -1
  47. package/dist/src/action.js +15 -15
  48. package/dist/src/ai/captain.js +1 -1
  49. package/dist/src/ai/fisherman/request-haul.d.ts +11 -0
  50. package/dist/src/ai/fisherman/request-haul.js +28 -0
  51. package/dist/src/ai/fisherman-tools.d.ts +10 -1
  52. package/dist/src/ai/fisherman-tools.js +35 -31
  53. package/dist/src/ai/fisherman.d.ts +3 -2
  54. package/dist/src/ai/fisherman.js +8 -6
  55. package/dist/src/ai/navigator.js +1 -1
  56. package/dist/src/ai/pilot.js +21 -16
  57. package/dist/src/ai/planner.js +9 -7
  58. package/dist/src/ai/provider.js +8 -14
  59. package/dist/src/ai/researcher/coordinates.d.ts +2 -0
  60. package/dist/src/ai/researcher/coordinates.js +13 -1
  61. package/dist/src/ai/researcher/deep-analysis.js +9 -9
  62. package/dist/src/ai/researcher/fingerprint-worker.js +1 -11
  63. package/dist/src/ai/researcher.js +3 -1
  64. package/dist/src/ai/rules.js +11 -3
  65. package/dist/src/ai/tester.js +20 -20
  66. package/dist/src/ai/tools.d.ts +1 -1
  67. package/dist/src/ai/tools.js +18 -74
  68. package/dist/src/api/request-store.d.ts +1 -5
  69. package/dist/src/api/request-store.js +12 -22
  70. package/dist/src/api/spec-reader.js +1 -1
  71. package/dist/src/commands/config-command.js +3 -10
  72. package/dist/src/commands/drill-command.js +1 -1
  73. package/dist/src/commands/explore-command.js +12 -1
  74. package/dist/src/commands/index.js +2 -0
  75. package/dist/src/commands/init-command.js +3 -7
  76. package/dist/src/commands/options/base-option.d.ts +8 -0
  77. package/dist/src/commands/options/base-option.js +12 -0
  78. package/dist/src/commands/options/index.d.ts +5 -0
  79. package/dist/src/commands/options/index.js +5 -0
  80. package/dist/src/commands/options/knowledge-option.d.ts +7 -0
  81. package/dist/src/commands/options/knowledge-option.js +12 -0
  82. package/dist/src/commands/options/ws-option.d.ts +7 -0
  83. package/dist/src/commands/options/ws-option.js +21 -0
  84. package/dist/src/commands/plans-command.d.ts +4 -1
  85. package/dist/src/commands/plans-command.js +4 -4
  86. package/dist/src/commands/recommended-models-command.d.ts +9 -0
  87. package/dist/src/commands/recommended-models-command.js +85 -0
  88. package/dist/src/config.d.ts +3 -0
  89. package/dist/src/config.js +18 -2
  90. package/dist/src/experience-tracker.js +1 -1
  91. package/dist/src/explorbot.js +4 -4
  92. package/dist/src/global-config.d.ts +2 -0
  93. package/dist/src/global-config.js +6 -0
  94. package/dist/src/knowledge-tracker.d.ts +20 -7
  95. package/dist/src/knowledge-tracker.js +69 -31
  96. package/dist/src/remote.d.ts +0 -3
  97. package/dist/src/remote.js +0 -18
  98. package/dist/src/state-manager.d.ts +3 -3
  99. package/dist/src/state-manager.js +8 -8
  100. package/dist/src/test-plan.d.ts +1 -0
  101. package/dist/src/test-plan.js +13 -9
  102. package/dist/src/utils/aria.d.ts +1 -1
  103. package/dist/src/utils/aria.js +23 -2
  104. package/dist/src/utils/cli-section.d.ts +1 -0
  105. package/dist/src/utils/cli-section.js +12 -0
  106. package/dist/src/utils/html-diff.d.ts +1 -0
  107. package/dist/src/utils/html-diff.js +5 -5
  108. package/dist/src/utils/html.d.ts +2 -0
  109. package/dist/src/utils/html.js +5 -4
  110. package/dist/src/utils/overlay.d.ts +19 -55
  111. package/dist/src/utils/overlay.js +94 -148
  112. package/dist/src/utils/region.d.ts +41 -0
  113. package/dist/src/utils/region.js +99 -0
  114. package/dist/src/utils/similarity.d.ts +1 -0
  115. package/dist/src/utils/similarity.js +11 -0
  116. package/dist/src/utils/url-matcher.d.ts +1 -1
  117. package/dist/src/utils/url-matcher.js +4 -4
  118. package/docs/api-testing/basics.md +15 -0
  119. package/docs/api-testing/planning.md +10 -1
  120. package/docs/doc-collection/basics.md +11 -5
  121. package/docs/doc-collection/crawling.md +21 -1
  122. package/docs/reference/commands.md +27 -4
  123. package/docs/superpowers/plans/2026-09-03-model-resolves-click-ambiguity.md +509 -0
  124. package/docs/workflow/agentic-usage.md +11 -2
  125. package/docs/workflow/application-spec.md +4 -0
  126. package/docs/workflow/knowledge.md +46 -2
  127. package/package.json +1 -1
  128. package/rules/planner/styles/normal.md +1 -1
  129. package/src/action-result.ts +15 -16
  130. package/src/action.ts +16 -15
  131. package/src/ai/captain.ts +1 -1
  132. package/src/ai/fisherman/request-haul.ts +32 -0
  133. package/src/ai/fisherman-tools.ts +38 -32
  134. package/src/ai/fisherman.ts +8 -7
  135. package/src/ai/navigator.ts +1 -1
  136. package/src/ai/pilot.ts +19 -14
  137. package/src/ai/planner.ts +9 -8
  138. package/src/ai/provider.ts +8 -12
  139. package/src/ai/researcher/coordinates.ts +13 -1
  140. package/src/ai/researcher/deep-analysis.ts +9 -9
  141. package/src/ai/researcher/fingerprint-worker.ts +1 -10
  142. package/src/ai/researcher.ts +2 -1
  143. package/src/ai/rules.ts +11 -3
  144. package/src/ai/tester.ts +18 -18
  145. package/src/ai/tools.ts +18 -86
  146. package/src/api/request-store.ts +13 -27
  147. package/src/api/spec-reader.ts +1 -1
  148. package/src/commands/config-command.ts +3 -8
  149. package/src/commands/drill-command.ts +1 -1
  150. package/src/commands/explore-command.ts +12 -1
  151. package/src/commands/index.ts +2 -0
  152. package/src/commands/init-command.ts +4 -11
  153. package/src/commands/options/base-option.ts +18 -0
  154. package/src/commands/options/index.ts +7 -0
  155. package/src/commands/options/knowledge-option.ts +14 -0
  156. package/src/commands/options/ws-option.ts +24 -0
  157. package/src/commands/plans-command.ts +5 -5
  158. package/src/commands/recommended-models-command.ts +92 -0
  159. package/src/config.ts +20 -2
  160. package/src/experience-tracker.ts +1 -1
  161. package/src/explorbot.ts +4 -4
  162. package/src/global-config.ts +8 -0
  163. package/src/knowledge-tracker.ts +94 -36
  164. package/src/remote.ts +0 -20
  165. package/src/state-manager.ts +10 -9
  166. package/src/test-plan.ts +12 -10
  167. package/src/utils/aria.ts +26 -3
  168. package/src/utils/cli-section.ts +12 -0
  169. package/src/utils/html-diff.ts +6 -6
  170. package/src/utils/html.ts +6 -4
  171. package/src/utils/overlay.ts +105 -192
  172. package/src/utils/region.ts +132 -0
  173. package/src/utils/similarity.ts +9 -0
  174. package/src/utils/url-matcher.ts +3 -3
  175. package/dist/src/utils/request-map.d.ts +0 -7
  176. package/dist/src/utils/request-map.js +0 -16
  177. 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
- writeFileSync(pagePath, renderPageDocumentation(state, documentation, screenshots), 'utf8');
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(` ![${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,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import { knowledgeOption } from "../../../src/commands/options/index.js";
2
3
  import { createPrimaCommands } from "../src/cli.js";
3
4
  const program = createPrimaCommands('prima');
5
+ knowledgeOption.register(program);
4
6
  program.parse();
@@ -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 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.0",
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",
@@ -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 CRUD operations end-to-end (create item → verify in list, edit item verify changes saved, delete item verify removed)
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 { 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;