explorbot 0.2.2 → 0.2.4

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 (214) hide show
  1. package/README.md +1 -1
  2. package/bin/explorbot-cli.ts +59 -38
  3. package/boat/api-tester/src/apibot.ts +4 -2
  4. package/boat/api-tester/src/cli.ts +2 -2
  5. package/boat/api-tester/src/config.ts +43 -10
  6. package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
  7. package/boat/doc-collector/src/ai/documentarian.ts +61 -31
  8. package/boat/doc-collector/src/cli.ts +1 -0
  9. package/boat/doc-collector/src/config.ts +4 -2
  10. package/boat/doc-collector/src/docs-renderer.ts +18 -4
  11. package/boat/doc-collector/src/state-diagram.ts +61 -14
  12. package/boat/prima/bin/prima-cli.ts +5 -0
  13. package/boat/prima/package.json +16 -0
  14. package/boat/prima/src/activity-line.ts +33 -0
  15. package/boat/prima/src/cli.ts +251 -0
  16. package/boat/prima/src/envelope.ts +169 -0
  17. package/boat/prima/src/prima.ts +1124 -0
  18. package/boat/prima/src/pw-parser.ts +27 -0
  19. package/boat/prima/src/pw-registry.ts +74 -0
  20. package/boat/prima/src/session-log.ts +126 -0
  21. package/dist/bin/explorbot-cli.js +51 -32
  22. package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
  23. package/dist/boat/api-tester/src/apibot.js +3 -2
  24. package/dist/boat/api-tester/src/cli.js +2 -2
  25. package/dist/boat/api-tester/src/config.js +40 -10
  26. package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
  27. package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
  28. package/dist/boat/doc-collector/src/cli.js +1 -0
  29. package/dist/boat/doc-collector/src/config.js +4 -2
  30. package/dist/boat/doc-collector/src/docs-renderer.js +17 -3
  31. package/dist/boat/doc-collector/src/state-diagram.js +57 -13
  32. package/dist/boat/prima/bin/prima-cli.js +4 -0
  33. package/dist/boat/prima/src/activity-line.js +30 -0
  34. package/dist/boat/prima/src/cli.js +220 -0
  35. package/dist/boat/prima/src/envelope.js +142 -0
  36. package/dist/boat/prima/src/prima.js +1031 -0
  37. package/dist/boat/prima/src/pw-parser.js +30 -0
  38. package/dist/boat/prima/src/pw-registry.js +65 -0
  39. package/dist/boat/prima/src/session-log.js +108 -0
  40. package/dist/models.json +3 -0
  41. package/dist/package.json +8 -3
  42. package/dist/rules/navigator/verification-actions.md +20 -0
  43. package/dist/src/action-result.d.ts +7 -0
  44. package/dist/src/action-result.js +4 -0
  45. package/dist/src/action.d.ts +7 -2
  46. package/dist/src/action.js +41 -6
  47. package/dist/src/ai/captain/mixin.js +3 -4
  48. package/dist/src/ai/captain/web-mode.js +7 -4
  49. package/dist/src/ai/captain.js +2 -0
  50. package/dist/src/ai/navigator.d.ts +11 -0
  51. package/dist/src/ai/navigator.js +21 -12
  52. package/dist/src/ai/pilot.d.ts +4 -0
  53. package/dist/src/ai/pilot.js +48 -2
  54. package/dist/src/ai/planner.d.ts +1 -0
  55. package/dist/src/ai/planner.js +6 -0
  56. package/dist/src/ai/provider.js +2 -2
  57. package/dist/src/ai/rerunner.js +2 -1
  58. package/dist/src/ai/researcher.js +2 -2
  59. package/dist/src/ai/rules.js +17 -10
  60. package/dist/src/ai/task-agent.js +5 -2
  61. package/dist/src/ai/tester.d.ts +1 -0
  62. package/dist/src/ai/tester.js +33 -20
  63. package/dist/src/ai/tools.d.ts +14 -0
  64. package/dist/src/ai/tools.js +113 -36
  65. package/dist/src/application-spec-contract.d.ts +8 -0
  66. package/dist/src/application-spec-contract.js +8 -0
  67. package/dist/src/application-spec.d.ts +15 -0
  68. package/dist/src/application-spec.js +71 -0
  69. package/dist/src/browser-server.d.ts +12 -6
  70. package/dist/src/browser-server.js +74 -19
  71. package/dist/src/commands/clean-command.js +2 -7
  72. package/dist/src/commands/init-command.d.ts +5 -0
  73. package/dist/src/commands/init-command.js +119 -1
  74. package/dist/src/commands/navigate-command.js +1 -1
  75. package/dist/src/commands/research-command.js +1 -1
  76. package/dist/src/commands/sites-command.d.ts +6 -0
  77. package/dist/src/commands/sites-command.js +23 -0
  78. package/dist/src/components/InitWizard.d.ts +10 -0
  79. package/dist/src/components/InitWizard.js +133 -0
  80. package/dist/src/components/InputReadline.d.ts +1 -0
  81. package/dist/src/components/InputReadline.js +7 -4
  82. package/dist/src/config.d.ts +25 -5
  83. package/dist/src/config.js +158 -40
  84. package/dist/src/execution-controller.d.ts +2 -0
  85. package/dist/src/execution-controller.js +6 -0
  86. package/dist/src/explorbot.d.ts +11 -1
  87. package/dist/src/explorbot.js +27 -6
  88. package/dist/src/explorer.d.ts +4 -1
  89. package/dist/src/explorer.js +42 -9
  90. package/dist/src/global-config.d.ts +22 -0
  91. package/dist/src/global-config.js +117 -0
  92. package/dist/src/knowledge-tracker.d.ts +5 -1
  93. package/dist/src/knowledge-tracker.js +14 -1
  94. package/dist/src/playwright-recorder.js +30 -0
  95. package/dist/src/remote.d.ts +54 -0
  96. package/dist/src/remote.js +229 -0
  97. package/dist/src/reporter.d.ts +1 -0
  98. package/dist/src/reporter.js +7 -1
  99. package/dist/src/state-manager.d.ts +2 -1
  100. package/dist/src/stats.d.ts +1 -0
  101. package/dist/src/stats.js +1 -0
  102. package/dist/src/test-plan.d.ts +2 -0
  103. package/dist/src/test-plan.js +7 -0
  104. package/dist/src/utils/aria.d.ts +2 -8
  105. package/dist/src/utils/aria.js +69 -40
  106. package/dist/src/utils/cli-name.js +6 -2
  107. package/dist/src/utils/html.js +1 -0
  108. package/dist/src/utils/logger.d.ts +6 -0
  109. package/dist/src/utils/logger.js +24 -0
  110. package/dist/src/utils/page-readiness.js +18 -1
  111. package/dist/src/utils/test-files.js +1 -2
  112. package/dist/src/utils/url-matcher.d.ts +1 -0
  113. package/dist/src/utils/url-matcher.js +12 -0
  114. package/dist/src/utils/web-element.d.ts +2 -0
  115. package/dist/src/utils/web-element.js +8 -0
  116. package/dist/src/utils/web-sandbox.d.ts +1 -1
  117. package/dist/src/utils/web-sandbox.js +2 -3
  118. package/docs/api-testing/basics.md +90 -0
  119. package/docs/api-testing/planning.md +57 -0
  120. package/docs/api-testing/running-tests.md +55 -0
  121. package/docs/assets/cloud-report.png +0 -0
  122. package/docs/assets/html-report.png +0 -0
  123. package/docs/assets/langfuse-trace.png +0 -0
  124. package/docs/assets/successful-explore-run.png +0 -0
  125. package/docs/basics/getting-started.md +140 -0
  126. package/docs/basics/prerequisites.md +63 -0
  127. package/docs/basics/providers.md +362 -0
  128. package/docs/basics/running.md +78 -0
  129. package/docs/contributing/ai-integration-tests.md +57 -0
  130. package/docs/contributing/contributing.md +90 -0
  131. package/docs/contributing/demo-videos.md +36 -0
  132. package/docs/contributing/npm-package.md +138 -0
  133. package/docs/contributing/observability.md +227 -0
  134. package/docs/contributing/regression-tests.md +103 -0
  135. package/docs/contributing/testing.md +95 -0
  136. package/docs/doc-collection/basics.md +128 -0
  137. package/docs/doc-collection/crawling.md +67 -0
  138. package/docs/doc-collection/interactive-mode.md +99 -0
  139. package/docs/index.json +86 -0
  140. package/docs/reference/commands.md +994 -0
  141. package/docs/reference/configuration.md +569 -0
  142. package/docs/reference/scripting.md +303 -0
  143. package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
  144. package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
  145. package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
  146. package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
  147. package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
  148. package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
  149. package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
  150. package/docs/web-testing/agents.md +158 -0
  151. package/docs/web-testing/automated-tests.md +134 -0
  152. package/docs/web-testing/basics.md +91 -0
  153. package/docs/web-testing/customization.md +131 -0
  154. package/docs/web-testing/hooks.md +238 -0
  155. package/docs/web-testing/page-interaction.md +84 -0
  156. package/docs/web-testing/planner.md +122 -0
  157. package/docs/web-testing/rerun.md +164 -0
  158. package/docs/web-testing/researcher.md +380 -0
  159. package/docs/workflow/agentic-usage.md +233 -0
  160. package/docs/workflow/application-spec.md +73 -0
  161. package/docs/workflow/ci.md +202 -0
  162. package/docs/workflow/knowledge.md +310 -0
  163. package/docs/workflow/planning-styles.md +67 -0
  164. package/docs/workflow/reporting.md +133 -0
  165. package/docs/workflow/test-plans.md +90 -0
  166. package/models.json +3 -0
  167. package/package.json +8 -3
  168. package/rules/navigator/verification-actions.md +20 -0
  169. package/src/action-result.ts +11 -0
  170. package/src/action.ts +47 -7
  171. package/src/ai/captain/mixin.ts +3 -3
  172. package/src/ai/captain/web-mode.ts +7 -4
  173. package/src/ai/captain.ts +3 -0
  174. package/src/ai/navigator.ts +26 -15
  175. package/src/ai/pilot.ts +53 -2
  176. package/src/ai/planner.ts +7 -0
  177. package/src/ai/provider.ts +2 -4
  178. package/src/ai/rerunner.ts +2 -1
  179. package/src/ai/researcher.ts +2 -2
  180. package/src/ai/rules.ts +17 -10
  181. package/src/ai/task-agent.ts +4 -2
  182. package/src/ai/tester.ts +32 -18
  183. package/src/ai/tools.ts +132 -36
  184. package/src/application-spec-contract.ts +10 -0
  185. package/src/application-spec.ts +87 -0
  186. package/src/browser-server.ts +74 -19
  187. package/src/commands/clean-command.ts +1 -6
  188. package/src/commands/init-command.ts +146 -1
  189. package/src/commands/navigate-command.ts +1 -1
  190. package/src/commands/research-command.ts +1 -1
  191. package/src/commands/sites-command.ts +27 -0
  192. package/src/components/InitWizard.tsx +166 -0
  193. package/src/components/InputReadline.tsx +8 -4
  194. package/src/config.ts +177 -43
  195. package/src/execution-controller.ts +8 -0
  196. package/src/explorbot.ts +34 -7
  197. package/src/explorer.ts +47 -9
  198. package/src/global-config.ts +148 -0
  199. package/src/knowledge-tracker.ts +17 -1
  200. package/src/playwright-recorder.ts +23 -0
  201. package/src/remote.ts +238 -0
  202. package/src/reporter.ts +7 -1
  203. package/src/state-manager.ts +2 -1
  204. package/src/stats.ts +1 -0
  205. package/src/test-plan.ts +9 -0
  206. package/src/utils/aria.ts +65 -45
  207. package/src/utils/cli-name.ts +5 -2
  208. package/src/utils/html.ts +1 -0
  209. package/src/utils/logger.ts +24 -1
  210. package/src/utils/page-readiness.ts +24 -1
  211. package/src/utils/test-files.ts +1 -2
  212. package/src/utils/url-matcher.ts +13 -0
  213. package/src/utils/web-element.ts +9 -0
  214. package/src/utils/web-sandbox.ts +3 -4
@@ -1,9 +1,11 @@
1
1
  import path from 'node:path';
2
+ import matter from 'gray-matter';
3
+ import { APPLICATION_SPEC_FORMAT, APPLICATION_SPEC_VERSION } from '../../../src/application-spec-contract.ts';
2
4
  import { type WebPageState } from '../../../src/state-manager.ts';
5
+ import { normalizeInlineText } from '../../../src/utils/strings.ts';
3
6
  import type { PageDocumentation, StateTransition } from './ai/documentarian.ts';
4
7
  import type { DocumentationScreenshot } from './screenshots.ts';
5
- import { buildStateGraph, renderMermaidFromGraph, renderStateMapFromGraph, type DocumentedPage, type SkippedPage } from './state-diagram.ts';
6
- import { normalizeInlineText } from '../../../src/utils/strings.ts';
8
+ import { type DocumentedPage, type SkippedPage, buildStateGraph, renderMermaidFromGraph, renderPageStateDiagram, renderStateMapFromGraph } from './state-diagram.ts';
7
9
 
8
10
  function renderPageDocumentation(state: WebPageState, documentation: PageDocumentation, screenshots: DocumentationScreenshot[] = []): string {
9
11
  const lines: string[] = [];
@@ -33,6 +35,13 @@ function renderPageDocumentation(state: WebPageState, documentation: PageDocumen
33
35
  }
34
36
 
35
37
  const interactions = documentation.interactions;
38
+ const pageStateDiagram = renderPageStateDiagram(state.title || state.url || 'page', state.url || '', interactions || []);
39
+ if (pageStateDiagram) {
40
+ lines.push('## State Map');
41
+ lines.push('');
42
+ lines.push(`\`\`\`mermaid\n${pageStateDiagram}\n\`\`\``);
43
+ lines.push('');
44
+ }
36
45
  if (interactions && interactions.length > 0) {
37
46
  lines.push('## State Transitions');
38
47
  lines.push('');
@@ -101,7 +110,11 @@ function renderPageDocumentation(state: WebPageState, documentation: PageDocumen
101
110
  lines.push('');
102
111
  }
103
112
 
104
- return `${lines.join('\n').trimEnd()}\n`;
113
+ return matter.stringify(`${lines.join('\n').trimEnd()}\n`, {
114
+ url: state.url,
115
+ format: APPLICATION_SPEC_FORMAT,
116
+ version: APPLICATION_SPEC_VERSION,
117
+ });
105
118
  }
106
119
 
107
120
  function renderSpecIndex(outputDir: string, startPath: string, pages: DocumentedPage[], skipped: SkippedPage[], maxPages: number): string {
@@ -116,9 +129,10 @@ function renderSpecIndex(outputDir: string, startPath: string, pages: Documented
116
129
  lines.push(`Max pages: ${maxPages}`);
117
130
  lines.push('');
118
131
  const graph = buildStateGraph(outputDir, pages);
132
+ const mermaid = renderMermaidFromGraph(graph, true);
119
133
  lines.push('## State Transitions');
120
134
  lines.push('');
121
- lines.push(`\`\`\`mermaid\n${renderMermaidFromGraph(graph)}\n\`\`\``);
135
+ lines.push(`\`\`\`mermaid\n${mermaid}\n\`\`\``);
122
136
  lines.push('');
123
137
  const stateMap = renderStateMapFromGraph(graph);
124
138
  if (stateMap) {
@@ -69,8 +69,12 @@ function buildStateGraph(outputDir: string, pages: DocumentedPage[]): StateGraph
69
69
  continue;
70
70
  }
71
71
 
72
- const pairKey = `${sourceId}>${targetId}`;
73
- if (adjacency.get(targetId)?.has(sourceId)) {
72
+ if (adjacency.get(sourceId)?.has(targetId)) {
73
+ continue;
74
+ }
75
+
76
+ if (createsCycle(sourceId, targetId, adjacency)) {
77
+ const pairKey = `${sourceId}>${targetId}`;
74
78
  if (drawnBack.has(pairKey)) {
75
79
  continue;
76
80
  }
@@ -78,10 +82,6 @@ function buildStateGraph(outputDir: string, pages: DocumentedPage[]): StateGraph
78
82
  edges.push({ source: sourceId, target: targetId, action: transition.action, isBack: true });
79
83
  continue;
80
84
  }
81
-
82
- if (adjacency.get(sourceId)?.has(targetId) || createsCycle(sourceId, targetId, adjacency)) {
83
- continue;
84
- }
85
85
  adjacency.get(sourceId)?.add(targetId);
86
86
  edges.push({ source: sourceId, target: targetId, action: transition.action, isBack: false });
87
87
  }
@@ -104,8 +104,8 @@ function renderMermaidBody(outputDir: string, pages: DocumentedPage[]): string {
104
104
  return renderMermaidFromGraph(buildStateGraph(outputDir, pages));
105
105
  }
106
106
 
107
- function renderMermaidFromGraph(graph: StateGraph): string {
108
- const lines: string[] = ['flowchart TD'];
107
+ function renderMermaidFromGraph(graph: StateGraph, compact = false): string {
108
+ const lines: string[] = [compact ? 'flowchart LR' : 'flowchart TD'];
109
109
  if (graph.pages.length === 0) {
110
110
  lines.push(' empty["No documented states"]');
111
111
  return lines.join('\n');
@@ -117,6 +117,12 @@ function renderMermaidFromGraph(graph: StateGraph): string {
117
117
  if (!children || children.length === 0) {
118
118
  continue;
119
119
  }
120
+ if (compact) {
121
+ for (const child of children) {
122
+ lines.push(` ${renderNodeLine(child)}`);
123
+ }
124
+ continue;
125
+ }
120
126
  lines.push(` subgraph sg_${page.id} ["${escapeMermaidLabel(page.label)} — transient states"]`);
121
127
  for (const child of children) {
122
128
  lines.push(` ${renderNodeLine(child)}`);
@@ -125,11 +131,12 @@ function renderMermaidFromGraph(graph: StateGraph): string {
125
131
  }
126
132
 
127
133
  for (const edge of graph.edges) {
128
- let arrow = '-->';
129
- if (edge.isBack) {
130
- arrow = '-.->';
134
+ const arrow = edge.isBack ? '-.->' : '-->';
135
+ if (compact) {
136
+ lines.push(` ${edge.source} ${arrow} ${edge.target}`);
137
+ } else {
138
+ lines.push(` ${edge.source} ${arrow}|"${escapeMermaidLabel(edge.action)}"| ${edge.target}`);
131
139
  }
132
- lines.push(` ${edge.source} ${arrow}|"${escapeMermaidLabel(edge.action)}"| ${edge.target}`);
133
140
  }
134
141
 
135
142
  lines.push(' classDef page fill:#dbeafe,stroke:#2563eb,color:#0f172a;');
@@ -174,6 +181,46 @@ function renderStateMapFromGraph(graph: StateGraph): string {
174
181
  return rows.join('\n');
175
182
  }
176
183
 
184
+ function renderPageStateDiagram(label: string, url: string, interactions: StateTransition[]): string {
185
+ const targets = new Map<string, { node: StateNode; action: string; screenshot?: { title: string; relativePath: string } }>();
186
+ let index = 0;
187
+ for (const interaction of interactions) {
188
+ const targetState = interaction.targetState;
189
+ if (!targetState) {
190
+ continue;
191
+ }
192
+ const key = `${targetState.kind}:${targetState.label}:${normalizeUrl(targetState.url)}`;
193
+ if (targets.has(key)) {
194
+ continue;
195
+ }
196
+ targets.set(key, {
197
+ node: { id: `target${index++}`, kind: targetState.kind, label: targetState.label, subLabel: targetState.kind },
198
+ action: interaction.action,
199
+ screenshot: interaction.screenshot,
200
+ });
201
+ }
202
+
203
+ if (targets.size === 0) {
204
+ return '';
205
+ }
206
+
207
+ const lines: string[] = ['flowchart LR'];
208
+ lines.push(` ${renderNodeLine({ id: 'self', kind: 'page', label, subLabel: url })}`);
209
+ for (const target of targets.values()) {
210
+ lines.push(` ${renderNodeLine(target.node)}`);
211
+ }
212
+ for (const target of targets.values()) {
213
+ lines.push(` self -->|"${escapeMermaidLabel(target.action)}"| ${target.node.id}`);
214
+ }
215
+ for (const target of targets.values()) {
216
+ if (target.screenshot) {
217
+ lines.push(` click ${target.node.id} "${target.screenshot.relativePath}" "${escapeMermaidLabel(target.screenshot.title)}"`);
218
+ }
219
+ }
220
+
221
+ return lines.join('\n');
222
+ }
223
+
177
224
  function renderNodeLine(node: StateNode): string {
178
225
  const label = `${escapeMermaidLabel(node.label)}<br/>${escapeMermaidLabel(node.subLabel)}`;
179
226
  if (node.kind === 'dialog' || node.kind === 'modal') {
@@ -207,7 +254,7 @@ function createsCycle(sourceId: string, targetId: string, adjacency: Map<string,
207
254
  }
208
255
 
209
256
  function escapeMermaidLabel(value: string): string {
210
- return normalizeInlineText(value).replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('|', '&#124;');
257
+ return normalizeInlineText(value).replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('#', '&#35;').replaceAll('<', '&#60;').replaceAll('>', '&#62;').replaceAll('|', '&#124;');
211
258
  }
212
259
 
213
260
  function escapeTable(value: string): string {
@@ -277,5 +324,5 @@ interface StateGraph {
277
324
  classAssignment: Map<StateClass, string[]>;
278
325
  }
279
326
 
280
- export { buildStateGraph, renderMermaidBody, renderMermaidFromGraph, renderStateMapFromGraph };
327
+ export { buildStateGraph, renderMermaidBody, renderMermaidFromGraph, renderPageStateDiagram, renderStateMapFromGraph };
281
328
  export type { DocumentedPage, SkippedPage, StateGraph, StateNode, StateEdge, StateClick };
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bun
2
+ import { createPrimaCommands } from '../src/cli.ts';
3
+
4
+ const program = createPrimaCommands('prima');
5
+ program.parse();
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "prima",
3
+ "version": "1.0.0",
4
+ "description": "High-level browser driver CLI for orchestrating agents",
5
+ "type": "module",
6
+ "bin": { "prima": "./bin/prima-cli.ts" },
7
+ "scripts": {
8
+ "format": "biome format --write .",
9
+ "lint:fix": "biome lint --write .",
10
+ "check:fix": "biome check --write ."
11
+ },
12
+ "dependencies": {
13
+ "commander": "^14.0.1",
14
+ "dedent": "^1.6.0"
15
+ }
16
+ }
@@ -0,0 +1,33 @@
1
+ import chalk from 'chalk';
2
+ import { type ActivityEntry, addActivityListener, removeActivityListener } from '../../../src/activity.ts';
3
+ import { isVerboseMode } from '../../../src/utils/logger.ts';
4
+
5
+ const RESET_LINE = '\r\u001b[2K';
6
+
7
+ const stream = process.stderr;
8
+ let tracking = false;
9
+
10
+ export function trackActivityLine(): void {
11
+ if (tracking) return;
12
+ if (!stream.isTTY) return;
13
+ if (isVerboseMode()) return;
14
+
15
+ tracking = true;
16
+ addActivityListener(writeActivityLine);
17
+ }
18
+
19
+ export function clearActivityLine(): void {
20
+ if (!tracking) return;
21
+
22
+ tracking = false;
23
+ removeActivityListener(writeActivityLine);
24
+ stream.write(RESET_LINE);
25
+ }
26
+
27
+ function writeActivityLine(activity: ActivityEntry | null): void {
28
+ if (!activity) return;
29
+
30
+ const width = (stream.columns || 80) - 2;
31
+ const message = Array.from(activity.message.replace(/\s+/g, ' ').trim()).slice(0, width).join('');
32
+ stream.write(`${RESET_LINE}${chalk.gray(message)}`);
33
+ }
@@ -0,0 +1,251 @@
1
+ import { Command } from 'commander';
2
+ import dedent from 'dedent';
3
+ import { keepServerRunning } from '../../../src/browser-server.ts';
4
+ import { browserErrorMessage } from '../../../src/utils/browser-errors.ts';
5
+ import { isVerboseMode, setQuietMode } from '../../../src/utils/logger.ts';
6
+ import { clearActivityLine, trackActivityLine } from './activity-line.ts';
7
+ import { type EnvelopeData, renderEnvelope } from './envelope.ts';
8
+ import { Prima, type PrimaOptions } from './prima.ts';
9
+
10
+ const helpContract = dedent`
11
+ Prima is a high-level AI extension to playwright-cli, driving the browser it has open.
12
+
13
+ playwright-cli open <url> starts the session
14
+ prima <command> ... drives it
15
+ playwright-cli close ends it
16
+
17
+ One call takes a whole job:
18
+
19
+ prima check "a workflow can be created and appears in the list" --expected "the new workflow is listed"
20
+ prima do "open the account menu" "choose the settings entry" "switch the theme to dark" "check it took effect"
21
+ prima pw "({ page }) => page.click('[data-test=submit]')"
22
+ `;
23
+
24
+ const checkHelp = dedent`
25
+ check takes an outcome rather than a click path, and works out how to reach it.
26
+ --expected one outcome the run must reach, repeatable for several. Without it the
27
+ scenario text is the single expected outcome. Each comes back under
28
+ ### Expected outcomes as PASSED, FAILED or not verified - "not verified"
29
+ means the run never checked it, which is not the same as false.
30
+ Page problems seen on the way appear under ### Answer, not as step failures.
31
+ `;
32
+
33
+ const doHelp = dedent`
34
+ Each instruction is numbered and accounted for: ### Steps reports each as ok or FAIL
35
+ with what proved it. One that could not be carried out fails the command and says why.
36
+ Nothing runs past the last instruction given. A whole remaining sequence in one call is
37
+ what makes this tier cheap.
38
+ `;
39
+
40
+ const verifyHelp = dedent`
41
+ Reports each assertion it could express as PASSED or FAILED with its playwright form,
42
+ and gives no overall verdict - read the lines and decide. "none ran" means the claim
43
+ could not be expressed, which is not the same as false.
44
+ `;
45
+
46
+ const reportHelp = dedent`
47
+ Commands are logged as they run, so the report needs no browser and outlives the session.
48
+ The most recent session is reported unless --pw-session names another.
49
+ `;
50
+
51
+ const sessionHelp = dedent`
52
+ --endpoint <ep> attach to a browser server endpoint directly, skipping discovery
53
+ --instance <name> which prima-owned browser you talk to; parallel work needs one each
54
+ --session [file] cookies and storage persisted across processes; ignored while
55
+ attached, since the attached session keeps its own
56
+ --framework parsed but not active yet; reported code is CodeceptJS either way
57
+ DEBUG='explorbot:*' in front of a command prints the log of everything it does.
58
+ When no AI model is usable pw still works; for everything else drive playwright-cli.
59
+ `;
60
+
61
+ let rootOptions: () => any = () => ({});
62
+
63
+ function buildOptions(subcommand: any): PrimaOptions {
64
+ const options = { ...rootOptions(), ...stripEmpty(subcommand) };
65
+ return {
66
+ config: options.config,
67
+ path: options.path,
68
+ instance: options.instance,
69
+ session: options.session,
70
+ ephemeral: options.ephemeral,
71
+ framework: options.framework,
72
+ noVision: options.vision === false,
73
+ url: options.url,
74
+ baseUrl: options.baseUrl,
75
+ show: options.show,
76
+ headless: options.headless,
77
+ endpoint: options.endpoint,
78
+ pwSession: options.pwSession,
79
+ };
80
+ }
81
+
82
+ function stripEmpty(options: any): any {
83
+ const present: any = {};
84
+ for (const [key, value] of Object.entries(options || {})) {
85
+ if (value === undefined) continue;
86
+ present[key] = value;
87
+ }
88
+ return present;
89
+ }
90
+
91
+ function addCommonOptions(cmd: Command): Command {
92
+ return cmd
93
+ .option('-c, --config <path>', 'Path to explorbot configuration file')
94
+ .option('-p, --path <path>', 'Working directory path')
95
+ .option('-i, --instance <name>', 'Browser instance to drive')
96
+ .option('--session [file]', 'Persist cookies and storage to a session file')
97
+ .option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
98
+ .option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
99
+ .option('--url <url>', 'Page to open when the session has no page yet')
100
+ .option('--endpoint <ep>', 'Websocket endpoint of a browser server to attach to, skipping discovery')
101
+ .option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
102
+ .addHelpText('after', `\n${sessionHelp}`);
103
+ }
104
+
105
+ function primaFor(options: any): Prima {
106
+ if (options.ephemeral) process.env.EXPLORBOT_EPHEMERAL = '1';
107
+ return new Prima(buildOptions(options));
108
+ }
109
+
110
+ async function runPrima(options: any, command: string, run: (prima: Prima) => Promise<EnvelopeData>, record = true): Promise<void> {
111
+ setQuietMode(!isVerboseMode());
112
+ trackActivityLine();
113
+ const prima = primaFor(options);
114
+ const startedAt = Date.now();
115
+
116
+ let envelope: EnvelopeData;
117
+ try {
118
+ await prima.start();
119
+ envelope = await run(prima);
120
+ } catch (error) {
121
+ envelope = await prima.toolFailureEnvelope(command, error);
122
+ }
123
+
124
+ if (record) prima.record(envelope, Date.now() - startedAt);
125
+ clearActivityLine();
126
+ console.log(renderEnvelope(envelope));
127
+ await prima.stop().catch(() => {});
128
+ process.exit(envelope.ok ? 0 : 1);
129
+ }
130
+
131
+ async function runBrowser(options: any, run: (prima: Prima) => Promise<boolean>): Promise<void> {
132
+ let ok = false;
133
+ try {
134
+ ok = await run(primaFor(options));
135
+ } catch (error) {
136
+ console.error(browserErrorMessage(error));
137
+ process.exit(1);
138
+ }
139
+
140
+ process.exit(ok ? 0 : 1);
141
+ }
142
+
143
+ export function createPrimaCommands(name = 'prima'): Command {
144
+ const cmd = new Command(name);
145
+ cmd.description('Tests and drives a web app through described behaviour instead of locators: one command carries a whole scenario, verifies it, and reports the proof');
146
+ cmd.option('--pw-session <title>', 'Title of the playwright-cli session to attach to');
147
+ cmd.option('--url <url>', 'Page to open when the session has no page yet');
148
+ cmd.addHelpText('after', `\n${helpContract}`);
149
+ rootOptions = () => cmd.opts();
150
+
151
+ addCommonOptions(cmd.command('pw <fn>').description('Run a Playwright function expression against the open page')).action(async (fn, options) => {
152
+ await runPrima(options, `pw ${fn}`, (prima) => prima.pw(fn));
153
+ });
154
+
155
+ addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction'))
156
+ .addHelpText('after', `\n${doHelp}`)
157
+ .action(async (instructions, options) => {
158
+ await runPrima(options, `do ${instructions.join(' ')}`, (prima) => prima.do(instructions));
159
+ });
160
+
161
+ addCommonOptions(cmd.command('check <scenario>').description('Run a scenario end to end as a test, with its own verification, and report the steps it took'))
162
+ .option('--expected <outcome>', 'An outcome the run must reach; repeat the flag for several', (value: string, all: string[]) => [...all, value], [])
163
+ .addHelpText('after', `\n${checkHelp}`)
164
+ .action(async (scenario, options) => {
165
+ await runPrima(options, `check ${scenario}`, (prima) => prima.check(scenario, options.expected));
166
+ });
167
+
168
+ addCommonOptions(cmd.command('ask <question>').description('Answer a question about the current page').option('--no-vision', 'Answer from page structure only, without a screenshot')).action(async (question, options) => {
169
+ await runPrima(options, `ask ${question}`, (prima) => prima.ask(question));
170
+ });
171
+
172
+ addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page'))
173
+ .addHelpText('after', `\n${verifyHelp}`)
174
+ .action(async (assertion, options) => {
175
+ await runPrima(options, `verify ${assertion}`, (prima) => prima.verify(assertion));
176
+ });
177
+
178
+ addCommonOptions(
179
+ cmd.command('research').description('Map the current page and return verified locators').option('--data', 'Include data extraction in the map').option('--deep', 'Expand hidden elements for a deeper map').option('--fresh', 'Ignore the cached map and research the page again')
180
+ ).action(async (options) => {
181
+ await runPrima(options, 'research', (prima) => prima.research({ data: options.data, deep: options.deep, fresh: options.fresh }));
182
+ });
183
+
184
+ addCommonOptions(cmd.command('go <target>').description('Navigate to a url, a path, or a page described in plain words')).action(async (target, options) => {
185
+ if (URL.canParse(target)) options.baseUrl = target;
186
+ await runPrima(options, `go ${target}`, (prima) => prima.go(target));
187
+ });
188
+
189
+ addCommonOptions(cmd.command('config').description('Show the AI models prima runs on and the config file they come from')).action(async (options) => {
190
+ setQuietMode(!isVerboseMode());
191
+ const prima = primaFor(options);
192
+ console.log(await prima.config().catch((error: unknown) => browserErrorMessage(error)));
193
+ await prima.stop().catch(() => {});
194
+ process.exit(0);
195
+ });
196
+
197
+ addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command')).action(async (hash, options) => {
198
+ await runPrima(options, `status ${hash}`, (prima) => prima.status(hash), false);
199
+ });
200
+
201
+ addCommonOptions(cmd.command('report').description('Turn every command of a session into one html and markdown report'))
202
+ .addHelpText('after', `\n${reportHelp}`)
203
+ .action(async (options) => {
204
+ setQuietMode(!isVerboseMode());
205
+ console.log(
206
+ await primaFor(options)
207
+ .report()
208
+ .catch((error: unknown) => browserErrorMessage(error))
209
+ );
210
+ process.exit(0);
211
+ });
212
+
213
+ const browser = cmd.command('browser').description('Manage the browsers prima drives');
214
+
215
+ addCommonOptions(browser.command('start').description('Start a prima-owned browser and hold it open until Ctrl+C'))
216
+ .option('-s, --show', 'Launch the browser in a visible window')
217
+ .option('--headless', 'Launch the browser without a window')
218
+ .action(async (options) => {
219
+ await runBrowser(options, async (prima) => {
220
+ await prima.browserStart();
221
+ console.log(await prima.browserStatus());
222
+ return keepServerRunning(() => prima.browserStop());
223
+ });
224
+ });
225
+
226
+ addCommonOptions(browser.command('stop').description('Stop the browser of this instance'))
227
+ .option('--all', 'Stop every running instance')
228
+ .action(async (options) => {
229
+ await runBrowser(options, async (prima) => {
230
+ const stopped = await prima.browserStop(options.all);
231
+ console.log(await prima.browserStatus());
232
+ return stopped;
233
+ });
234
+ });
235
+
236
+ addCommonOptions(browser.command('status').description('Report the browser of this instance')).action(async (options) => {
237
+ await runBrowser(options, async (prima) => {
238
+ console.log(await prima.browserStatus());
239
+ return true;
240
+ });
241
+ });
242
+
243
+ addCommonOptions(browser.command('list').description('List every browser instance that is running')).action(async (options) => {
244
+ await runBrowser(options, async (prima) => {
245
+ console.log(await prima.browserList());
246
+ return true;
247
+ });
248
+ });
249
+
250
+ return cmd;
251
+ }
@@ -0,0 +1,169 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const EXPECTATION_LABELS = {
5
+ passed: 'PASSED ',
6
+ failed: 'FAILED ',
7
+ unverified: 'not verified',
8
+ };
9
+
10
+ export interface InstanceInfo {
11
+ name: string;
12
+ tabs: number;
13
+ startedAgo?: string;
14
+ attached?: string;
15
+ others: Array<{ name: string; tabs: number }>;
16
+ }
17
+
18
+ export interface EnvelopeData {
19
+ ok: boolean;
20
+ command: string;
21
+ used?: string[];
22
+ page: { url: string; previousUrl?: string; title: string; state: string; visits: number };
23
+ changes?: string | null;
24
+ steps?: Array<{ label: string; ok: boolean; proof: string }>;
25
+ expectations?: Array<{ text: string; status: 'passed' | 'failed' | 'unverified' }>;
26
+ stepFiles?: string;
27
+ value?: string;
28
+ answer?: string;
29
+ research?: string;
30
+ assertions?: Array<{ code: string; passed: boolean; proof: string[] }>;
31
+ failure?: { error: string; compactAria?: string };
32
+ instance: InstanceInfo;
33
+ status?: string;
34
+ artifacts?: { aria: string; html: string; network?: string };
35
+ }
36
+
37
+ export function renderEnvelope(data: EnvelopeData): string {
38
+ const sections = [renderResult(data), renderPage(data), renderValue(data), renderChanges(data), renderSteps(data), renderExpectations(data), renderOutcome(data), ...renderFailure(data), renderInstance(data), renderArtifacts(data)];
39
+ return sections.filter((section) => section).join('\n\n');
40
+ }
41
+
42
+ export function writeArtifacts(dir: string, snapshot: { aria: string | null; html: string | null; requests: unknown[] }): { aria: string; html: string; network?: string } {
43
+ mkdirSync(dir, { recursive: true });
44
+ const paths: { aria: string; html: string; network?: string } = {
45
+ aria: path.resolve(dir, 'aria.yml'),
46
+ html: path.resolve(dir, 'page.html'),
47
+ };
48
+ writeFileSync(paths.aria, snapshot.aria ?? '', 'utf-8');
49
+ writeFileSync(paths.html, snapshot.html ?? '', 'utf-8');
50
+
51
+ if (!snapshot.requests.length) return paths;
52
+
53
+ paths.network = path.resolve(dir, 'network.jsonl');
54
+ writeFileSync(paths.network, snapshot.requests.map((request) => `${JSON.stringify(request)}\n`).join(''), 'utf-8');
55
+ return paths;
56
+ }
57
+
58
+ function renderResult(data: EnvelopeData): string {
59
+ const lines = [`ok: ${data.ok}`, `command: ${data.command}`];
60
+ if (data.used?.length) lines.push(`used: ${data.used.join('; ')}`);
61
+ return section('Result', lines.join('\n'));
62
+ }
63
+
64
+ function renderPage(data: EnvelopeData): string {
65
+ const { url, previousUrl, title, state, visits } = data.page;
66
+ const urlLabel = `url: ${url}`;
67
+ const stateLabel = `state: ${state}`;
68
+ const width = Math.max(urlLabel.length, stateLabel.length) + 3;
69
+ let changedMarker = '';
70
+ if (previousUrl && previousUrl !== url) changedMarker = `(changed: ${previousUrl} → ${url})`;
71
+ const lines = [align(urlLabel, changedMarker, width), `title: ${title}`, align(stateLabel, `(visit #${visits})`, width)];
72
+ return section('Page', lines.join('\n'));
73
+ }
74
+
75
+ function renderValue(data: EnvelopeData): string | null {
76
+ if (data.value === undefined) return null;
77
+ return section('Value', data.value);
78
+ }
79
+
80
+ function renderChanges(data: EnvelopeData): string | null {
81
+ if (data.changes === undefined || data.changes === null) return null;
82
+ return section('Changes', data.changes);
83
+ }
84
+
85
+ function renderSteps(data: EnvelopeData): string | null {
86
+ if (!data.steps?.length) return null;
87
+
88
+ const lines: string[] = [];
89
+ data.steps.forEach((step, index) => {
90
+ lines.push(`${index + 1}. ${step.ok ? 'ok ' : 'FAIL'} ${step.label}`);
91
+ for (const line of (step.proof || '').split('\n').filter(Boolean)) lines.push(` ${line}`);
92
+ });
93
+ if (data.stepFiles) lines.push('', `page after each step: ${data.stepFiles}`);
94
+ return section('Steps', lines.join('\n'));
95
+ }
96
+
97
+ function renderExpectations(data: EnvelopeData): string | null {
98
+ if (!data.expectations?.length) return null;
99
+ const lines = data.expectations.map((expectation, index) => `${index + 1}. ${EXPECTATION_LABELS[expectation.status]} ${expectation.text}`);
100
+ return section('Expected outcomes', lines.join('\n'));
101
+ }
102
+
103
+ function renderOutcome(data: EnvelopeData): string | null {
104
+ if (data.answer) return section('Answer', data.answer);
105
+ if (data.research) return section('Research', data.research);
106
+ if (!data.assertions) return null;
107
+
108
+ if (!data.assertions.length) return section('Assertions', 'none ran — no assertion could express this claim, so nothing was checked against the page');
109
+
110
+ const lines = data.assertions.map((assertion) => {
111
+ const code = assertion.code
112
+ .split('\n')
113
+ .map((line) => line.trim())
114
+ .filter((line) => line && !line.startsWith('//'))
115
+ .join(' ');
116
+ return `${code} => ${assertion.passed ? 'PASSED' : 'FAILED'}`;
117
+ });
118
+
119
+ const proof = data.assertions.flatMap((assertion) => assertion.proof);
120
+ if (proof.length) lines.push('', 'playwright:', ...proof);
121
+
122
+ return section('Assertions', lines.join('\n'));
123
+ }
124
+
125
+ function renderFailure(data: EnvelopeData): Array<string | null> {
126
+ if (!data.failure) return [];
127
+ return [section('Failure', `error: ${data.failure.error}`), renderCompactAria(data.failure.compactAria)];
128
+ }
129
+
130
+ function renderCompactAria(compactAria?: string): string | null {
131
+ if (!compactAria) return null;
132
+ return section('Current page (compact ARIA)', compactAria);
133
+ }
134
+
135
+ function renderInstance(data: EnvelopeData): string | null {
136
+ const instance = data.instance;
137
+ const parts = [`${instance.name} (${tabsLabel(instance.tabs)})`, browserLine(instance)];
138
+ if (instance.others.length) parts.push(`other instances: ${instance.others.map((other) => `${other.name} (${tabsLabel(other.tabs)})`).join(', ')}`);
139
+ if (data.status) parts.push(`details: prima status ${data.status}`);
140
+ return section('Instance', parts.join(' | '));
141
+ }
142
+
143
+ function browserLine(instance: InstanceInfo): string {
144
+ if (instance.attached) return `attached to ${instance.attached}`;
145
+ if (instance.startedAgo) return `running, started ${instance.startedAgo} ago`;
146
+ if (instance.tabs > 0) return 'running';
147
+ return 'not running';
148
+ }
149
+
150
+ function tabsLabel(tabs: number): string {
151
+ if (tabs === 1) return '1 tab';
152
+ return `${tabs} tabs`;
153
+ }
154
+
155
+ export function renderArtifacts(data: EnvelopeData): string | null {
156
+ if (!data.artifacts) return null;
157
+ const lines = [`aria: ${data.artifacts.aria}`, `html: ${data.artifacts.html}`];
158
+ if (data.artifacts.network) lines.push(`network: ${data.artifacts.network}`);
159
+ return section('Artifacts', lines.join('\n'));
160
+ }
161
+
162
+ function align(label: string, marker: string, width: number): string {
163
+ if (!marker) return label;
164
+ return `${label.padEnd(width)}${marker}`;
165
+ }
166
+
167
+ function section(title: string, body: string): string {
168
+ return `### ${title}\n${body}`;
169
+ }