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
@@ -55,13 +55,22 @@ class Documentarian {
55
55
  const message = error instanceof Error ? error.message : String(error);
56
56
  tag('warning').log(`Interactive documentation failed: ${message}.`);
57
57
  if (meaningfulInteractions.length > 0) {
58
- tag('info').log(`Preserving ${meaningfulInteractions.length} observed interaction(s) without AI summary.`);
59
- return this.normalizeDocumentation({
60
- summary: `Observed ${meaningfulInteractions.length} interaction(s); AI-generated summary was unavailable.`,
61
- can: [],
62
- might: [],
58
+ tag('info').log(`Retrying static documentation while preserving ${meaningfulInteractions.length} observed interaction(s).`);
59
+ return this.documentStatic(state, research)
60
+ .then((documentation) => this.normalizeDocumentation({
61
+ ...documentation,
63
62
  interactions: meaningfulInteractions,
64
- }, state, research);
63
+ }, state, research))
64
+ .catch((fallbackError) => {
65
+ const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
66
+ tag('warning').log(`Static documentation fallback failed: ${fallbackMessage}. Preserving observed interactions without AI summary.`);
67
+ return this.normalizeDocumentation({
68
+ summary: `Observed ${meaningfulInteractions.length} interaction(s); AI-generated summary was unavailable.`,
69
+ can: [],
70
+ might: [],
71
+ interactions: meaningfulInteractions,
72
+ }, state, research);
73
+ });
65
74
  }
66
75
  return this.documentStatic(state, research);
67
76
  }
@@ -94,7 +103,7 @@ class Documentarian {
94
103
  content: this.buildPrompt(state, `${research}${this.buildInteractionContext(interactions)}`),
95
104
  },
96
105
  ];
97
- const response = await this.provider.generateObject(messages, pageDocumentationSchema, undefined, {
106
+ const response = await this.provider.generateObject(messages, generatedPageDocumentationSchema, undefined, {
98
107
  agentName: 'documentarian',
99
108
  });
100
109
  return this.normalizeDocumentation({
@@ -113,7 +122,7 @@ class Documentarian {
113
122
  content: this.buildPrompt(state, research, simplified),
114
123
  },
115
124
  ];
116
- const response = await this.provider.generateObject(messages, pageDocumentationSchema, undefined, {
125
+ const response = await this.provider.generateObject(messages, generatedPageDocumentationSchema, undefined, {
117
126
  agentName: 'documentarian',
118
127
  });
119
128
  return this.normalizeDocumentation(response.object, state, research);
@@ -230,10 +239,10 @@ class Documentarian {
230
239
  normalized.interactions = undefined;
231
240
  }
232
241
  const qualityNotes = this.evaluateDocumentationQuality(normalized);
233
- return {
242
+ return pageDocumentationSchema.parse({
234
243
  ...normalized,
235
244
  qualityNotes,
236
- };
245
+ });
237
246
  }
238
247
  evaluateDocumentationQuality(documentation) {
239
248
  const notes = [];
@@ -296,30 +305,46 @@ const stateTransitionSchema = z.object({
296
305
  action: z.string(),
297
306
  before: z.string(),
298
307
  after: z.string(),
299
- targetUrl: z.string().nullable(),
300
- discoveredUrls: z.array(z.string()).nullable(),
301
- newCapabilities: z.array(z.string()).nullable(),
308
+ targetUrl: z.string().optional(),
309
+ discoveredUrls: z.array(z.string()).optional(),
310
+ newCapabilities: z.array(z.string()).optional(),
302
311
  element: z
303
312
  .object({
304
313
  role: z.string(),
305
314
  name: z.string(),
306
315
  section: z.string(),
307
- container: z.string().nullable(),
308
- locator: z.string().nullable(),
316
+ container: z.string().optional(),
317
+ locator: z.string().optional(),
309
318
  })
310
- .nullable(),
319
+ .optional(),
311
320
  changes: z
312
321
  .object({
313
322
  urlChanged: z.boolean(),
314
323
  newElements: z.number(),
315
324
  removedElements: z.number(),
316
325
  })
317
- .nullable(),
326
+ .optional(),
327
+ targetState: z
328
+ .object({
329
+ kind: z.enum(['page', 'dialog', 'modal', 'section']),
330
+ label: z.string(),
331
+ url: z.string(),
332
+ })
333
+ .optional(),
334
+ screenshot: z
335
+ .object({
336
+ title: z.string(),
337
+ relativePath: z.string(),
338
+ })
339
+ .optional(),
318
340
  });
319
- const pageDocumentationSchema = z.object({
341
+ const generatedPageDocumentationSchema = z.object({
320
342
  summary: z.string(),
321
343
  can: z.array(capabilitySchema),
322
344
  might: z.array(capabilitySchema),
323
- interactions: z.array(stateTransitionSchema).nullable(),
345
+ });
346
+ const pageDocumentationSchema = generatedPageDocumentationSchema.extend({
347
+ interactions: z.array(stateTransitionSchema).optional(),
348
+ qualityNotes: z.array(z.string()).optional(),
324
349
  });
325
350
  export { Documentarian };
@@ -47,6 +47,7 @@ export function createDocsCommands(name = 'docs') {
47
47
  console.log(`Skipped ${result.skipped.length} page(s)`);
48
48
  console.log(`Spec index: ${result.indexPath}`);
49
49
  console.log(`Pages dir: ${path.join(result.outputDir, 'pages')}`);
50
+ console.log(`Use in Explorbot: npx explorbot start ${startPath} --spec "${result.outputDir}"`);
50
51
  await bot.stop();
51
52
  process.exit(0);
52
53
  }
@@ -8,6 +8,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
8
8
  };
9
9
  import { existsSync, readFileSync } from 'node:fs';
10
10
  import path, { resolve } from 'node:path';
11
+ import { pathToFileURL } from 'node:url';
11
12
  import { parseEnv } from 'node:util';
12
13
  import { ConfigParser } from "../../../src/config.js";
13
14
  class DocbotConfigParser {
@@ -84,9 +85,10 @@ class DocbotConfigParser {
84
85
  }
85
86
  async loadConfigModule(configPath) {
86
87
  const ext = configPath.split('.').pop();
88
+ const moduleUrl = pathToFileURL(resolve(configPath)).href;
87
89
  if (ext === 'ts') {
88
90
  try {
89
- return await import(__rewriteRelativeImportExtension(configPath));
91
+ return await import(__rewriteRelativeImportExtension(moduleUrl));
90
92
  }
91
93
  catch {
92
94
  const require = (await import('node:module')).createRequire(import.meta.url);
@@ -94,7 +96,7 @@ class DocbotConfigParser {
94
96
  }
95
97
  }
96
98
  if (ext === 'js' || ext === 'mjs') {
97
- return await import(__rewriteRelativeImportExtension(configPath));
99
+ return await import(__rewriteRelativeImportExtension(moduleUrl));
98
100
  }
99
101
  return JSON.parse(readFileSync(configPath, 'utf8'));
100
102
  }
@@ -1,6 +1,8 @@
1
1
  import path from 'node:path';
2
- import { buildStateGraph, renderMermaidFromGraph, renderStateMapFromGraph } from "./state-diagram.js";
2
+ import matter from 'gray-matter';
3
+ import { APPLICATION_SPEC_FORMAT, APPLICATION_SPEC_VERSION } from "../../../src/application-spec-contract.js";
3
4
  import { normalizeInlineText } from "../../../src/utils/strings.js";
5
+ import { buildStateGraph, renderMermaidFromGraph, renderPageStateDiagram, renderStateMapFromGraph } from "./state-diagram.js";
4
6
  function renderPageDocumentation(state, documentation, screenshots = []) {
5
7
  const lines = [];
6
8
  lines.push(`# ${state.url}`);
@@ -25,6 +27,13 @@ function renderPageDocumentation(state, documentation, screenshots = []) {
25
27
  }
26
28
  }
27
29
  const interactions = documentation.interactions;
30
+ const pageStateDiagram = renderPageStateDiagram(state.title || state.url || 'page', state.url || '', interactions || []);
31
+ if (pageStateDiagram) {
32
+ lines.push('## State Map');
33
+ lines.push('');
34
+ lines.push(`\`\`\`mermaid\n${pageStateDiagram}\n\`\`\``);
35
+ lines.push('');
36
+ }
28
37
  if (interactions && interactions.length > 0) {
29
38
  lines.push('## State Transitions');
30
39
  lines.push('');
@@ -83,7 +92,11 @@ function renderPageDocumentation(state, documentation, screenshots = []) {
83
92
  }
84
93
  lines.push('');
85
94
  }
86
- return `${lines.join('\n').trimEnd()}\n`;
95
+ return matter.stringify(`${lines.join('\n').trimEnd()}\n`, {
96
+ url: state.url,
97
+ format: APPLICATION_SPEC_FORMAT,
98
+ version: APPLICATION_SPEC_VERSION,
99
+ });
87
100
  }
88
101
  function renderSpecIndex(outputDir, startPath, pages, skipped, maxPages) {
89
102
  const lines = [];
@@ -97,9 +110,10 @@ function renderSpecIndex(outputDir, startPath, pages, skipped, maxPages) {
97
110
  lines.push(`Max pages: ${maxPages}`);
98
111
  lines.push('');
99
112
  const graph = buildStateGraph(outputDir, pages);
113
+ const mermaid = renderMermaidFromGraph(graph, true);
100
114
  lines.push('## State Transitions');
101
115
  lines.push('');
102
- lines.push(`\`\`\`mermaid\n${renderMermaidFromGraph(graph)}\n\`\`\``);
116
+ lines.push(`\`\`\`mermaid\n${mermaid}\n\`\`\``);
103
117
  lines.push('');
104
118
  const stateMap = renderStateMapFromGraph(graph);
105
119
  if (stateMap) {
@@ -60,8 +60,11 @@ function buildStateGraph(outputDir, pages) {
60
60
  if (!targetId) {
61
61
  continue;
62
62
  }
63
- const pairKey = `${sourceId}>${targetId}`;
64
- if (adjacency.get(targetId)?.has(sourceId)) {
63
+ if (adjacency.get(sourceId)?.has(targetId)) {
64
+ continue;
65
+ }
66
+ if (createsCycle(sourceId, targetId, adjacency)) {
67
+ const pairKey = `${sourceId}>${targetId}`;
65
68
  if (drawnBack.has(pairKey)) {
66
69
  continue;
67
70
  }
@@ -69,9 +72,6 @@ function buildStateGraph(outputDir, pages) {
69
72
  edges.push({ source: sourceId, target: targetId, action: transition.action, isBack: true });
70
73
  continue;
71
74
  }
72
- if (adjacency.get(sourceId)?.has(targetId) || createsCycle(sourceId, targetId, adjacency)) {
73
- continue;
74
- }
75
75
  adjacency.get(sourceId)?.add(targetId);
76
76
  edges.push({ source: sourceId, target: targetId, action: transition.action, isBack: false });
77
77
  }
@@ -90,8 +90,8 @@ function buildStateGraph(outputDir, pages) {
90
90
  function renderMermaidBody(outputDir, pages) {
91
91
  return renderMermaidFromGraph(buildStateGraph(outputDir, pages));
92
92
  }
93
- function renderMermaidFromGraph(graph) {
94
- const lines = ['flowchart TD'];
93
+ function renderMermaidFromGraph(graph, compact = false) {
94
+ const lines = [compact ? 'flowchart LR' : 'flowchart TD'];
95
95
  if (graph.pages.length === 0) {
96
96
  lines.push(' empty["No documented states"]');
97
97
  return lines.join('\n');
@@ -102,6 +102,12 @@ function renderMermaidFromGraph(graph) {
102
102
  if (!children || children.length === 0) {
103
103
  continue;
104
104
  }
105
+ if (compact) {
106
+ for (const child of children) {
107
+ lines.push(` ${renderNodeLine(child)}`);
108
+ }
109
+ continue;
110
+ }
105
111
  lines.push(` subgraph sg_${page.id} ["${escapeMermaidLabel(page.label)} — transient states"]`);
106
112
  for (const child of children) {
107
113
  lines.push(` ${renderNodeLine(child)}`);
@@ -109,11 +115,13 @@ function renderMermaidFromGraph(graph) {
109
115
  lines.push(' end');
110
116
  }
111
117
  for (const edge of graph.edges) {
112
- let arrow = '-->';
113
- if (edge.isBack) {
114
- arrow = '-.->';
118
+ const arrow = edge.isBack ? '-.->' : '-->';
119
+ if (compact) {
120
+ lines.push(` ${edge.source} ${arrow} ${edge.target}`);
121
+ }
122
+ else {
123
+ lines.push(` ${edge.source} ${arrow}|"${escapeMermaidLabel(edge.action)}"| ${edge.target}`);
115
124
  }
116
- lines.push(` ${edge.source} ${arrow}|"${escapeMermaidLabel(edge.action)}"| ${edge.target}`);
117
125
  }
118
126
  lines.push(' classDef page fill:#dbeafe,stroke:#2563eb,color:#0f172a;');
119
127
  lines.push(' classDef dialog fill:#ffedd5,stroke:#ea580c,color:#0f172a;');
@@ -153,6 +161,42 @@ function renderStateMapFromGraph(graph) {
153
161
  }
154
162
  return rows.join('\n');
155
163
  }
164
+ function renderPageStateDiagram(label, url, interactions) {
165
+ const targets = new Map();
166
+ let index = 0;
167
+ for (const interaction of interactions) {
168
+ const targetState = interaction.targetState;
169
+ if (!targetState) {
170
+ continue;
171
+ }
172
+ const key = `${targetState.kind}:${targetState.label}:${normalizeUrl(targetState.url)}`;
173
+ if (targets.has(key)) {
174
+ continue;
175
+ }
176
+ targets.set(key, {
177
+ node: { id: `target${index++}`, kind: targetState.kind, label: targetState.label, subLabel: targetState.kind },
178
+ action: interaction.action,
179
+ screenshot: interaction.screenshot,
180
+ });
181
+ }
182
+ if (targets.size === 0) {
183
+ return '';
184
+ }
185
+ const lines = ['flowchart LR'];
186
+ lines.push(` ${renderNodeLine({ id: 'self', kind: 'page', label, subLabel: url })}`);
187
+ for (const target of targets.values()) {
188
+ lines.push(` ${renderNodeLine(target.node)}`);
189
+ }
190
+ for (const target of targets.values()) {
191
+ lines.push(` self -->|"${escapeMermaidLabel(target.action)}"| ${target.node.id}`);
192
+ }
193
+ for (const target of targets.values()) {
194
+ if (target.screenshot) {
195
+ lines.push(` click ${target.node.id} "${target.screenshot.relativePath}" "${escapeMermaidLabel(target.screenshot.title)}"`);
196
+ }
197
+ }
198
+ return lines.join('\n');
199
+ }
156
200
  function renderNodeLine(node) {
157
201
  const label = `${escapeMermaidLabel(node.label)}<br/>${escapeMermaidLabel(node.subLabel)}`;
158
202
  if (node.kind === 'dialog' || node.kind === 'modal') {
@@ -183,7 +227,7 @@ function createsCycle(sourceId, targetId, adjacency) {
183
227
  return false;
184
228
  }
185
229
  function escapeMermaidLabel(value) {
186
- return normalizeInlineText(value).replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('|', '&#124;');
230
+ return normalizeInlineText(value).replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('#', '&#35;').replaceAll('<', '&#60;').replaceAll('>', '&#62;').replaceAll('|', '&#124;');
187
231
  }
188
232
  function escapeTable(value) {
189
233
  return normalizeInlineText(value).replaceAll('|', '\\|');
@@ -197,4 +241,4 @@ function classForKind(kind) {
197
241
  }
198
242
  return 'dialog';
199
243
  }
200
- export { buildStateGraph, renderMermaidBody, renderMermaidFromGraph, renderStateMapFromGraph };
244
+ export { buildStateGraph, renderMermaidBody, renderMermaidFromGraph, renderPageStateDiagram, renderStateMapFromGraph };
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { createPrimaCommands } from "../src/cli.js";
3
+ const program = createPrimaCommands('prima');
4
+ program.parse();
@@ -0,0 +1,30 @@
1
+ import chalk from 'chalk';
2
+ import { addActivityListener, removeActivityListener } from "../../../src/activity.js";
3
+ import { isVerboseMode } from "../../../src/utils/logger.js";
4
+ const RESET_LINE = '\r\u001b[2K';
5
+ const stream = process.stderr;
6
+ let tracking = false;
7
+ export function trackActivityLine() {
8
+ if (tracking)
9
+ return;
10
+ if (!stream.isTTY)
11
+ return;
12
+ if (isVerboseMode())
13
+ return;
14
+ tracking = true;
15
+ addActivityListener(writeActivityLine);
16
+ }
17
+ export function clearActivityLine() {
18
+ if (!tracking)
19
+ return;
20
+ tracking = false;
21
+ removeActivityListener(writeActivityLine);
22
+ stream.write(RESET_LINE);
23
+ }
24
+ function writeActivityLine(activity) {
25
+ if (!activity)
26
+ return;
27
+ const width = (stream.columns || 80) - 2;
28
+ const message = Array.from(activity.message.replace(/\s+/g, ' ').trim()).slice(0, width).join('');
29
+ stream.write(`${RESET_LINE}${chalk.gray(message)}`);
30
+ }
@@ -0,0 +1,220 @@
1
+ import { Command } from 'commander';
2
+ import dedent from 'dedent';
3
+ import { keepServerRunning } from "../../../src/browser-server.js";
4
+ import { browserErrorMessage } from "../../../src/utils/browser-errors.js";
5
+ import { isVerboseMode, setQuietMode } from "../../../src/utils/logger.js";
6
+ import { clearActivityLine, trackActivityLine } from "./activity-line.js";
7
+ import { renderEnvelope } from "./envelope.js";
8
+ import { Prima } from "./prima.js";
9
+ const helpContract = dedent `
10
+ Prima is a high-level AI extension to playwright-cli, driving the browser it has open.
11
+
12
+ playwright-cli open <url> starts the session
13
+ prima <command> ... drives it
14
+ playwright-cli close ends it
15
+
16
+ One call takes a whole job:
17
+
18
+ prima check "a workflow can be created and appears in the list" --expected "the new workflow is listed"
19
+ prima do "open the account menu" "choose the settings entry" "switch the theme to dark" "check it took effect"
20
+ prima pw "({ page }) => page.click('[data-test=submit]')"
21
+ `;
22
+ const checkHelp = dedent `
23
+ check takes an outcome rather than a click path, and works out how to reach it.
24
+ --expected one outcome the run must reach, repeatable for several. Without it the
25
+ scenario text is the single expected outcome. Each comes back under
26
+ ### Expected outcomes as PASSED, FAILED or not verified - "not verified"
27
+ means the run never checked it, which is not the same as false.
28
+ Page problems seen on the way appear under ### Answer, not as step failures.
29
+ `;
30
+ const doHelp = dedent `
31
+ Each instruction is numbered and accounted for: ### Steps reports each as ok or FAIL
32
+ with what proved it. One that could not be carried out fails the command and says why.
33
+ Nothing runs past the last instruction given. A whole remaining sequence in one call is
34
+ what makes this tier cheap.
35
+ `;
36
+ const verifyHelp = dedent `
37
+ Reports each assertion it could express as PASSED or FAILED with its playwright form,
38
+ and gives no overall verdict - read the lines and decide. "none ran" means the claim
39
+ could not be expressed, which is not the same as false.
40
+ `;
41
+ const reportHelp = dedent `
42
+ Commands are logged as they run, so the report needs no browser and outlives the session.
43
+ The most recent session is reported unless --pw-session names another.
44
+ `;
45
+ const sessionHelp = dedent `
46
+ --endpoint <ep> attach to a browser server endpoint directly, skipping discovery
47
+ --instance <name> which prima-owned browser you talk to; parallel work needs one each
48
+ --session [file] cookies and storage persisted across processes; ignored while
49
+ attached, since the attached session keeps its own
50
+ --framework parsed but not active yet; reported code is CodeceptJS either way
51
+ DEBUG='explorbot:*' in front of a command prints the log of everything it does.
52
+ When no AI model is usable pw still works; for everything else drive playwright-cli.
53
+ `;
54
+ let rootOptions = () => ({});
55
+ function buildOptions(subcommand) {
56
+ const options = { ...rootOptions(), ...stripEmpty(subcommand) };
57
+ return {
58
+ config: options.config,
59
+ path: options.path,
60
+ instance: options.instance,
61
+ session: options.session,
62
+ ephemeral: options.ephemeral,
63
+ framework: options.framework,
64
+ noVision: options.vision === false,
65
+ url: options.url,
66
+ baseUrl: options.baseUrl,
67
+ show: options.show,
68
+ headless: options.headless,
69
+ endpoint: options.endpoint,
70
+ pwSession: options.pwSession,
71
+ };
72
+ }
73
+ function stripEmpty(options) {
74
+ const present = {};
75
+ for (const [key, value] of Object.entries(options || {})) {
76
+ if (value === undefined)
77
+ continue;
78
+ present[key] = value;
79
+ }
80
+ return present;
81
+ }
82
+ function addCommonOptions(cmd) {
83
+ return cmd
84
+ .option('-c, --config <path>', 'Path to explorbot configuration file')
85
+ .option('-p, --path <path>', 'Working directory path')
86
+ .option('-i, --instance <name>', 'Browser instance to drive')
87
+ .option('--session [file]', 'Persist cookies and storage to a session file')
88
+ .option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
89
+ .option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
90
+ .option('--url <url>', 'Page to open when the session has no page yet')
91
+ .option('--endpoint <ep>', 'Websocket endpoint of a browser server to attach to, skipping discovery')
92
+ .option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
93
+ .addHelpText('after', `\n${sessionHelp}`);
94
+ }
95
+ function primaFor(options) {
96
+ if (options.ephemeral)
97
+ process.env.EXPLORBOT_EPHEMERAL = '1';
98
+ return new Prima(buildOptions(options));
99
+ }
100
+ async function runPrima(options, command, run, record = true) {
101
+ setQuietMode(!isVerboseMode());
102
+ trackActivityLine();
103
+ const prima = primaFor(options);
104
+ const startedAt = Date.now();
105
+ let envelope;
106
+ try {
107
+ await prima.start();
108
+ envelope = await run(prima);
109
+ }
110
+ catch (error) {
111
+ envelope = await prima.toolFailureEnvelope(command, error);
112
+ }
113
+ if (record)
114
+ prima.record(envelope, Date.now() - startedAt);
115
+ clearActivityLine();
116
+ console.log(renderEnvelope(envelope));
117
+ await prima.stop().catch(() => { });
118
+ process.exit(envelope.ok ? 0 : 1);
119
+ }
120
+ async function runBrowser(options, run) {
121
+ let ok = false;
122
+ try {
123
+ ok = await run(primaFor(options));
124
+ }
125
+ catch (error) {
126
+ console.error(browserErrorMessage(error));
127
+ process.exit(1);
128
+ }
129
+ process.exit(ok ? 0 : 1);
130
+ }
131
+ export function createPrimaCommands(name = 'prima') {
132
+ const cmd = new Command(name);
133
+ 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');
134
+ cmd.option('--pw-session <title>', 'Title of the playwright-cli session to attach to');
135
+ cmd.option('--url <url>', 'Page to open when the session has no page yet');
136
+ cmd.addHelpText('after', `\n${helpContract}`);
137
+ rootOptions = () => cmd.opts();
138
+ addCommonOptions(cmd.command('pw <fn>').description('Run a Playwright function expression against the open page')).action(async (fn, options) => {
139
+ await runPrima(options, `pw ${fn}`, (prima) => prima.pw(fn));
140
+ });
141
+ addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction'))
142
+ .addHelpText('after', `\n${doHelp}`)
143
+ .action(async (instructions, options) => {
144
+ await runPrima(options, `do ${instructions.join(' ')}`, (prima) => prima.do(instructions));
145
+ });
146
+ 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'))
147
+ .option('--expected <outcome>', 'An outcome the run must reach; repeat the flag for several', (value, all) => [...all, value], [])
148
+ .addHelpText('after', `\n${checkHelp}`)
149
+ .action(async (scenario, options) => {
150
+ await runPrima(options, `check ${scenario}`, (prima) => prima.check(scenario, options.expected));
151
+ });
152
+ 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) => {
153
+ await runPrima(options, `ask ${question}`, (prima) => prima.ask(question));
154
+ });
155
+ addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page'))
156
+ .addHelpText('after', `\n${verifyHelp}`)
157
+ .action(async (assertion, options) => {
158
+ await runPrima(options, `verify ${assertion}`, (prima) => prima.verify(assertion));
159
+ });
160
+ addCommonOptions(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')).action(async (options) => {
161
+ await runPrima(options, 'research', (prima) => prima.research({ data: options.data, deep: options.deep, fresh: options.fresh }));
162
+ });
163
+ addCommonOptions(cmd.command('go <target>').description('Navigate to a url, a path, or a page described in plain words')).action(async (target, options) => {
164
+ if (URL.canParse(target))
165
+ options.baseUrl = target;
166
+ await runPrima(options, `go ${target}`, (prima) => prima.go(target));
167
+ });
168
+ addCommonOptions(cmd.command('config').description('Show the AI models prima runs on and the config file they come from')).action(async (options) => {
169
+ setQuietMode(!isVerboseMode());
170
+ const prima = primaFor(options);
171
+ console.log(await prima.config().catch((error) => browserErrorMessage(error)));
172
+ await prima.stop().catch(() => { });
173
+ process.exit(0);
174
+ });
175
+ addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command')).action(async (hash, options) => {
176
+ await runPrima(options, `status ${hash}`, (prima) => prima.status(hash), false);
177
+ });
178
+ addCommonOptions(cmd.command('report').description('Turn every command of a session into one html and markdown report'))
179
+ .addHelpText('after', `\n${reportHelp}`)
180
+ .action(async (options) => {
181
+ setQuietMode(!isVerboseMode());
182
+ console.log(await primaFor(options)
183
+ .report()
184
+ .catch((error) => browserErrorMessage(error)));
185
+ process.exit(0);
186
+ });
187
+ const browser = cmd.command('browser').description('Manage the browsers prima drives');
188
+ addCommonOptions(browser.command('start').description('Start a prima-owned browser and hold it open until Ctrl+C'))
189
+ .option('-s, --show', 'Launch the browser in a visible window')
190
+ .option('--headless', 'Launch the browser without a window')
191
+ .action(async (options) => {
192
+ await runBrowser(options, async (prima) => {
193
+ await prima.browserStart();
194
+ console.log(await prima.browserStatus());
195
+ return keepServerRunning(() => prima.browserStop());
196
+ });
197
+ });
198
+ addCommonOptions(browser.command('stop').description('Stop the browser of this instance'))
199
+ .option('--all', 'Stop every running instance')
200
+ .action(async (options) => {
201
+ await runBrowser(options, async (prima) => {
202
+ const stopped = await prima.browserStop(options.all);
203
+ console.log(await prima.browserStatus());
204
+ return stopped;
205
+ });
206
+ });
207
+ addCommonOptions(browser.command('status').description('Report the browser of this instance')).action(async (options) => {
208
+ await runBrowser(options, async (prima) => {
209
+ console.log(await prima.browserStatus());
210
+ return true;
211
+ });
212
+ });
213
+ addCommonOptions(browser.command('list').description('List every browser instance that is running')).action(async (options) => {
214
+ await runBrowser(options, async (prima) => {
215
+ console.log(await prima.browserList());
216
+ return true;
217
+ });
218
+ });
219
+ return cmd;
220
+ }