explorbot 0.2.3 → 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 (156) hide show
  1. package/README.md +1 -1
  2. package/bin/explorbot-cli.ts +7 -1
  3. package/boat/api-tester/src/config.ts +4 -2
  4. package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
  5. package/boat/doc-collector/src/ai/documentarian.ts +61 -31
  6. package/boat/doc-collector/src/config.ts +4 -2
  7. package/boat/prima/bin/prima-cli.ts +0 -0
  8. package/boat/prima/src/activity-line.ts +33 -0
  9. package/boat/prima/src/cli.ts +115 -86
  10. package/boat/prima/src/envelope.ts +80 -52
  11. package/boat/prima/src/prima.ts +547 -128
  12. package/boat/prima/src/pw-parser.ts +11 -1
  13. package/boat/prima/src/pw-registry.ts +4 -5
  14. package/boat/prima/src/session-log.ts +126 -0
  15. package/dist/bin/explorbot-cli.js +7 -1
  16. package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
  17. package/dist/boat/api-tester/src/config.js +4 -2
  18. package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
  19. package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
  20. package/dist/boat/doc-collector/src/config.js +4 -2
  21. package/dist/boat/prima/src/activity-line.js +30 -0
  22. package/dist/boat/prima/src/cli.js +97 -77
  23. package/dist/boat/prima/src/envelope.js +70 -44
  24. package/dist/boat/prima/src/prima.js +515 -119
  25. package/dist/boat/prima/src/pw-parser.js +13 -1
  26. package/dist/boat/prima/src/pw-registry.js +4 -5
  27. package/dist/boat/prima/src/session-log.js +108 -0
  28. package/dist/package.json +3 -2
  29. package/dist/rules/navigator/verification-actions.md +20 -0
  30. package/dist/src/action-result.d.ts +7 -0
  31. package/dist/src/action-result.js +4 -0
  32. package/dist/src/action.d.ts +2 -0
  33. package/dist/src/action.js +36 -1
  34. package/dist/src/ai/captain/web-mode.js +6 -3
  35. package/dist/src/ai/captain.js +2 -0
  36. package/dist/src/ai/navigator.d.ts +7 -0
  37. package/dist/src/ai/navigator.js +10 -6
  38. package/dist/src/ai/pilot.d.ts +4 -0
  39. package/dist/src/ai/pilot.js +48 -2
  40. package/dist/src/ai/provider.js +2 -2
  41. package/dist/src/ai/rerunner.js +2 -1
  42. package/dist/src/ai/researcher.js +1 -1
  43. package/dist/src/ai/rules.js +17 -10
  44. package/dist/src/ai/task-agent.js +4 -1
  45. package/dist/src/ai/tester.d.ts +1 -1
  46. package/dist/src/ai/tester.js +33 -33
  47. package/dist/src/ai/tools.d.ts +14 -0
  48. package/dist/src/ai/tools.js +113 -36
  49. package/dist/src/config.d.ts +1 -0
  50. package/dist/src/config.js +13 -4
  51. package/dist/src/execution-controller.d.ts +2 -0
  52. package/dist/src/execution-controller.js +6 -0
  53. package/dist/src/explorbot.d.ts +2 -1
  54. package/dist/src/explorbot.js +3 -1
  55. package/dist/src/explorer.js +2 -3
  56. package/dist/src/playwright-recorder.js +30 -0
  57. package/dist/src/remote.d.ts +54 -0
  58. package/dist/src/remote.js +229 -0
  59. package/dist/src/reporter.d.ts +1 -0
  60. package/dist/src/reporter.js +7 -1
  61. package/dist/src/state-manager.d.ts +2 -1
  62. package/dist/src/stats.d.ts +1 -0
  63. package/dist/src/stats.js +1 -0
  64. package/dist/src/test-plan.d.ts +2 -0
  65. package/dist/src/test-plan.js +7 -0
  66. package/dist/src/utils/aria.d.ts +2 -8
  67. package/dist/src/utils/aria.js +69 -40
  68. package/dist/src/utils/html.js +1 -0
  69. package/dist/src/utils/logger.d.ts +6 -0
  70. package/dist/src/utils/logger.js +24 -0
  71. package/dist/src/utils/page-readiness.js +18 -1
  72. package/dist/src/utils/url-matcher.js +3 -0
  73. package/dist/src/utils/web-element.d.ts +2 -0
  74. package/dist/src/utils/web-element.js +8 -0
  75. package/dist/src/utils/web-sandbox.d.ts +1 -1
  76. package/dist/src/utils/web-sandbox.js +2 -3
  77. package/docs/api-testing/basics.md +90 -0
  78. package/docs/api-testing/planning.md +57 -0
  79. package/docs/api-testing/running-tests.md +55 -0
  80. package/docs/assets/cloud-report.png +0 -0
  81. package/docs/assets/html-report.png +0 -0
  82. package/docs/assets/langfuse-trace.png +0 -0
  83. package/docs/assets/successful-explore-run.png +0 -0
  84. package/docs/basics/getting-started.md +140 -0
  85. package/docs/basics/prerequisites.md +63 -0
  86. package/docs/basics/providers.md +362 -0
  87. package/docs/basics/running.md +78 -0
  88. package/docs/contributing/ai-integration-tests.md +57 -0
  89. package/docs/contributing/contributing.md +90 -0
  90. package/docs/contributing/demo-videos.md +36 -0
  91. package/docs/contributing/npm-package.md +138 -0
  92. package/docs/contributing/observability.md +227 -0
  93. package/docs/contributing/regression-tests.md +103 -0
  94. package/docs/contributing/testing.md +95 -0
  95. package/docs/doc-collection/basics.md +128 -0
  96. package/docs/doc-collection/crawling.md +67 -0
  97. package/docs/doc-collection/interactive-mode.md +99 -0
  98. package/docs/index.json +86 -0
  99. package/docs/reference/commands.md +994 -0
  100. package/docs/reference/configuration.md +569 -0
  101. package/docs/reference/scripting.md +303 -0
  102. package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
  103. package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
  104. package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
  105. package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
  106. package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
  107. package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
  108. package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
  109. package/docs/web-testing/agents.md +158 -0
  110. package/docs/web-testing/automated-tests.md +134 -0
  111. package/docs/web-testing/basics.md +91 -0
  112. package/docs/web-testing/customization.md +131 -0
  113. package/docs/web-testing/hooks.md +238 -0
  114. package/docs/web-testing/page-interaction.md +84 -0
  115. package/docs/web-testing/planner.md +122 -0
  116. package/docs/web-testing/rerun.md +164 -0
  117. package/docs/web-testing/researcher.md +380 -0
  118. package/docs/workflow/agentic-usage.md +233 -0
  119. package/docs/workflow/application-spec.md +73 -0
  120. package/docs/workflow/ci.md +202 -0
  121. package/docs/workflow/knowledge.md +310 -0
  122. package/docs/workflow/planning-styles.md +67 -0
  123. package/docs/workflow/reporting.md +133 -0
  124. package/docs/workflow/test-plans.md +90 -0
  125. package/package.json +3 -2
  126. package/rules/navigator/verification-actions.md +20 -0
  127. package/src/action-result.ts +11 -0
  128. package/src/action.ts +38 -2
  129. package/src/ai/captain/web-mode.ts +6 -3
  130. package/src/ai/captain.ts +3 -0
  131. package/src/ai/navigator.ts +14 -8
  132. package/src/ai/pilot.ts +53 -2
  133. package/src/ai/provider.ts +2 -4
  134. package/src/ai/rerunner.ts +2 -1
  135. package/src/ai/researcher.ts +1 -1
  136. package/src/ai/rules.ts +17 -10
  137. package/src/ai/task-agent.ts +3 -1
  138. package/src/ai/tester.ts +32 -33
  139. package/src/ai/tools.ts +132 -36
  140. package/src/config.ts +15 -4
  141. package/src/execution-controller.ts +8 -0
  142. package/src/explorbot.ts +4 -2
  143. package/src/explorer.ts +2 -2
  144. package/src/playwright-recorder.ts +23 -0
  145. package/src/remote.ts +238 -0
  146. package/src/reporter.ts +7 -1
  147. package/src/state-manager.ts +2 -1
  148. package/src/stats.ts +1 -0
  149. package/src/test-plan.ts +9 -0
  150. package/src/utils/aria.ts +65 -45
  151. package/src/utils/html.ts +1 -0
  152. package/src/utils/logger.ts +24 -1
  153. package/src/utils/page-readiness.ts +24 -1
  154. package/src/utils/url-matcher.ts +3 -0
  155. package/src/utils/web-element.ts +9 -0
  156. package/src/utils/web-sandbox.ts +3 -4
package/README.md CHANGED
@@ -138,7 +138,7 @@ EXPLORBOT_KNOWLEDGE="Log in as admin@example.com / secret123" \
138
138
  npx explorbot explore /admin/users --max-tests 3
139
139
  ```
140
140
 
141
- Output lands in a per-host state directory, `~/.explorbot/state/<host>/`, so runs against the same app collect in one place and nothing is written to your project. Set `EXPLORBOT_EPHEMERAL=1` to keep nothing between runs. See [Agentic Usage](docs/workflow/agentic-usage.md).
141
+ Output lands in a per-host site directory, `~/.explorbot/sites/<host>/`, so runs against the same app collect in one place and nothing is written to your project. Set `EXPLORBOT_EPHEMERAL=1` to keep nothing between runs. See [Agentic Usage](docs/workflow/agentic-usage.md).
142
142
 
143
143
  ## Teaching Explorbot
144
144
 
@@ -11,6 +11,7 @@ import { App } from '../src/components/App.js';
11
11
  import { StatusPane } from '../src/components/StatusPane.js';
12
12
  import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
13
13
  import { ExplorBot, type ExplorBotOptions } from '../src/explorbot.js';
14
+ import { remote } from '../src/remote.js';
14
15
  import { Stats } from '../src/stats.js';
15
16
  import { Plan } from '../src/test-plan.js';
16
17
  import { getCliName } from '../src/utils/cli-name.ts';
@@ -26,8 +27,9 @@ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../p
26
27
  const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version as string;
27
28
 
28
29
  program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
30
+ remote.registerOption(program);
29
31
 
30
- if (!process.env.EXPLORBOT_NO_BANNER) {
32
+ if (!process.env.EXPLORBOT_NO_BANNER && !process.argv.includes('prima')) {
31
33
  console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
32
34
  }
33
35
 
@@ -99,6 +101,10 @@ async function startTUI(explorBot: ExplorBot): Promise<void> {
99
101
  }
100
102
 
101
103
  async function showStatsAndExit(code: number): Promise<never> {
104
+ if (remote.isAttached()) {
105
+ await remote.close(code);
106
+ process.exit(code);
107
+ }
102
108
  if (Stats.hasActivity()) {
103
109
  await new Promise<void>((resolve) => {
104
110
  const { unmount } = render(
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync } from 'node:fs';
2
2
  import path, { resolve } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
3
4
  import { parseEnv } from 'node:util';
4
5
  import { type AIConfig, type ApiHookFn, type ApiConfig as BaseApiConfig, ConfigMissingError, EXPLORBOT_CONFIG_PATHS, createModel, envConfigRequested, materializeKnowledge, missingConfigMessage, resolveConfigModels, resolveModel, resolveOutputRoot } from '../../../src/config.ts';
5
6
  import { type SiteRecord, findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from '../../../src/global-config.ts';
@@ -213,10 +214,11 @@ export class ApibotConfigParser {
213
214
 
214
215
  private async loadConfigModule(configPath: string): Promise<any> {
215
216
  const ext = configPath.split('.').pop();
217
+ const moduleUrl = pathToFileURL(resolve(configPath)).href;
216
218
 
217
219
  if (ext === 'ts') {
218
220
  try {
219
- return await import(configPath);
221
+ return await import(moduleUrl);
220
222
  } catch {
221
223
  const require = (await import('node:module')).createRequire(import.meta.url);
222
224
  return require(configPath);
@@ -224,7 +226,7 @@ export class ApibotConfigParser {
224
226
  }
225
227
 
226
228
  if (ext === 'js' || ext === 'mjs') {
227
- return await import(configPath);
229
+ return await import(moduleUrl);
228
230
  }
229
231
 
230
232
  const content = readFileSync(configPath, 'utf8');
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env bun
2
+ import { remote } from '../../../src/remote.ts';
2
3
  import { createDocsCommands } from '../src/cli.ts';
3
4
 
4
5
  const program = createDocsCommands('doc-collector');
6
+ remote.registerOption(program);
5
7
  program.parse();
@@ -67,17 +67,32 @@ class Documentarian {
67
67
  const message = error instanceof Error ? error.message : String(error);
68
68
  tag('warning').log(`Interactive documentation failed: ${message}.`);
69
69
  if (meaningfulInteractions.length > 0) {
70
- tag('info').log(`Preserving ${meaningfulInteractions.length} observed interaction(s) without AI summary.`);
71
- return this.normalizeDocumentation(
72
- {
73
- summary: `Observed ${meaningfulInteractions.length} interaction(s); AI-generated summary was unavailable.`,
74
- can: [],
75
- might: [],
76
- interactions: meaningfulInteractions,
77
- },
78
- state,
79
- research
80
- );
70
+ tag('info').log(`Retrying static documentation while preserving ${meaningfulInteractions.length} observed interaction(s).`);
71
+ return this.documentStatic(state, research)
72
+ .then((documentation) =>
73
+ this.normalizeDocumentation(
74
+ {
75
+ ...documentation,
76
+ interactions: meaningfulInteractions,
77
+ },
78
+ state,
79
+ research
80
+ )
81
+ )
82
+ .catch((fallbackError) => {
83
+ const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
84
+ tag('warning').log(`Static documentation fallback failed: ${fallbackMessage}. Preserving observed interactions without AI summary.`);
85
+ return this.normalizeDocumentation(
86
+ {
87
+ summary: `Observed ${meaningfulInteractions.length} interaction(s); AI-generated summary was unavailable.`,
88
+ can: [],
89
+ might: [],
90
+ interactions: meaningfulInteractions,
91
+ },
92
+ state,
93
+ research
94
+ );
95
+ });
81
96
  }
82
97
  return this.documentStatic(state, research);
83
98
  }
@@ -113,13 +128,13 @@ class Documentarian {
113
128
  },
114
129
  ];
115
130
 
116
- const response = await this.provider.generateObject(messages, pageDocumentationSchema, undefined, {
131
+ const response = await this.provider.generateObject(messages, generatedPageDocumentationSchema, undefined, {
117
132
  agentName: 'documentarian',
118
133
  });
119
134
 
120
135
  return this.normalizeDocumentation(
121
136
  {
122
- ...(response.object as PageDocumentation),
137
+ ...(response.object as GeneratedPageDocumentation),
123
138
  interactions,
124
139
  },
125
140
  state,
@@ -139,11 +154,11 @@ class Documentarian {
139
154
  },
140
155
  ];
141
156
 
142
- const response = await this.provider.generateObject(messages, pageDocumentationSchema, undefined, {
157
+ const response = await this.provider.generateObject(messages, generatedPageDocumentationSchema, undefined, {
143
158
  agentName: 'documentarian',
144
159
  });
145
160
 
146
- return this.normalizeDocumentation(response.object as PageDocumentation, state, research);
161
+ return this.normalizeDocumentation(response.object as GeneratedPageDocumentation, state, research);
147
162
  }
148
163
 
149
164
  private getSystemPrompt(): string {
@@ -259,7 +274,7 @@ class Documentarian {
259
274
  return message.includes('Failed to generate JSON') || message.includes('Failed to validate JSON') || message.includes('failed_generation') || message.includes('No object generated') || message.includes('response did not match schema');
260
275
  }
261
276
 
262
- private normalizeDocumentation(documentation: PageDocumentation, _state: WebPageState, _research: string): PageDocumentation {
277
+ private normalizeDocumentation(documentation: GeneratedPageDocumentation & Partial<Pick<PageDocumentation, 'interactions'>>, _state: WebPageState, _research: string): PageDocumentation {
263
278
  const normalized = { ...documentation };
264
279
  if (!normalized.interactions) {
265
280
  normalized.interactions = undefined;
@@ -267,10 +282,10 @@ class Documentarian {
267
282
 
268
283
  const qualityNotes = this.evaluateDocumentationQuality(normalized);
269
284
 
270
- return {
285
+ return pageDocumentationSchema.parse({
271
286
  ...normalized,
272
287
  qualityNotes,
273
- };
288
+ });
274
289
  }
275
290
 
276
291
  private evaluateDocumentationQuality(documentation: PageDocumentation): string[] {
@@ -346,39 +361,54 @@ const stateTransitionSchema = z.object({
346
361
  action: z.string(),
347
362
  before: z.string(),
348
363
  after: z.string(),
349
- targetUrl: z.string().nullable(),
350
- discoveredUrls: z.array(z.string()).nullable(),
351
- newCapabilities: z.array(z.string()).nullable(),
364
+ targetUrl: z.string().optional(),
365
+ discoveredUrls: z.array(z.string()).optional(),
366
+ newCapabilities: z.array(z.string()).optional(),
352
367
  element: z
353
368
  .object({
354
369
  role: z.string(),
355
370
  name: z.string(),
356
371
  section: z.string(),
357
- container: z.string().nullable(),
358
- locator: z.string().nullable(),
372
+ container: z.string().optional(),
373
+ locator: z.string().optional(),
359
374
  })
360
- .nullable(),
375
+ .optional(),
361
376
  changes: z
362
377
  .object({
363
378
  urlChanged: z.boolean(),
364
379
  newElements: z.number(),
365
380
  removedElements: z.number(),
366
381
  })
367
- .nullable(),
382
+ .optional(),
383
+ targetState: z
384
+ .object({
385
+ kind: z.enum(['page', 'dialog', 'modal', 'section']),
386
+ label: z.string(),
387
+ url: z.string(),
388
+ })
389
+ .optional(),
390
+ screenshot: z
391
+ .object({
392
+ title: z.string(),
393
+ relativePath: z.string(),
394
+ })
395
+ .optional(),
368
396
  });
369
397
 
370
- const pageDocumentationSchema = z.object({
398
+ const generatedPageDocumentationSchema = z.object({
371
399
  summary: z.string(),
372
400
  can: z.array(capabilitySchema),
373
401
  might: z.array(capabilitySchema),
374
- interactions: z.array(stateTransitionSchema).nullable(),
402
+ });
403
+
404
+ const pageDocumentationSchema = generatedPageDocumentationSchema.extend({
405
+ interactions: z.array(stateTransitionSchema).optional(),
406
+ qualityNotes: z.array(z.string()).optional(),
375
407
  });
376
408
 
377
409
  type StateTransition = DocStateTransition;
378
- type PageDocumentation = Omit<z.infer<typeof pageDocumentationSchema>, 'interactions'> & {
379
- interactions?: StateTransition[];
380
- qualityNotes?: string[];
381
- };
410
+ type GeneratedPageDocumentation = z.infer<typeof generatedPageDocumentationSchema>;
411
+ type PageDocumentation = z.infer<typeof pageDocumentationSchema>;
382
412
 
383
413
  export { Documentarian };
384
414
  export type { PageDocumentation, StateTransition };
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import path, { resolve } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
3
4
  import { parseEnv } from 'node:util';
4
5
  import { ConfigParser } from '../../../src/config.ts';
5
6
 
@@ -90,10 +91,11 @@ class DocbotConfigParser {
90
91
 
91
92
  private async loadConfigModule(configPath: string): Promise<any> {
92
93
  const ext = configPath.split('.').pop();
94
+ const moduleUrl = pathToFileURL(resolve(configPath)).href;
93
95
 
94
96
  if (ext === 'ts') {
95
97
  try {
96
- return await import(configPath);
98
+ return await import(moduleUrl);
97
99
  } catch {
98
100
  const require = (await import('node:module')).createRequire(import.meta.url);
99
101
  return require(configPath);
@@ -101,7 +103,7 @@ class DocbotConfigParser {
101
103
  }
102
104
 
103
105
  if (ext === 'js' || ext === 'mjs') {
104
- return await import(configPath);
106
+ return await import(moduleUrl);
105
107
  }
106
108
 
107
109
  return JSON.parse(readFileSync(configPath, 'utf8'));
File without changes
@@ -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
+ }
@@ -2,83 +2,71 @@ import { Command } from 'commander';
2
2
  import dedent from 'dedent';
3
3
  import { keepServerRunning } from '../../../src/browser-server.ts';
4
4
  import { browserErrorMessage } from '../../../src/utils/browser-errors.ts';
5
- import { setPreserveConsoleLogs } from '../../../src/utils/logger.ts';
5
+ import { isVerboseMode, setQuietMode } from '../../../src/utils/logger.ts';
6
+ import { clearActivityLine, trackActivityLine } from './activity-line.ts';
6
7
  import { type EnvelopeData, renderEnvelope } from './envelope.ts';
7
8
  import { Prima, type PrimaOptions } from './prima.ts';
8
9
 
9
10
  const helpContract = dedent`
10
- Prima drives a browser that is already open. One command per process; every command
11
- prints a plain-text envelope on stdout and exits 0 when ok, 1 when not.
12
-
13
- TIERS - choose by what you hold, not by how hard the step looks
14
- pw <fn> Precise. A Playwright function expression built from a locator you
15
- already verified. No AI on the happy path.
16
- prima pw "({ page }) => page.click('[data-test=submit]')"
17
- click / fill One action described in words; AI resolves it on the current page.
18
- prima click "the primary action button in the header"
19
- prima fill "the search box" "a search term"
20
- do <steps...> Several described steps, run tester-style in one process.
21
- prima do "open the account menu" "choose the settings entry"
22
- Never pass a locator or a function expression to click/fill/do - describe the target.
23
- Never pass a description to pw - it takes executable code only.
24
-
25
- LOOP
26
- prima go <url|path|words> reach the page you want to work on
27
- prima research once per new page; returns verified locators
28
- prima pw "..." drive the page with those locators
29
- prima verify "..." assert the outcome (prima ask "..." to inspect instead)
30
- Fall back to click/fill/do whenever research left you no locator to hold.
31
-
32
- ENVELOPE
33
- ### Result ok, command, healed, used
34
- ### Page url, title, state hash, visit count
35
- ### Changes what the accessibility tree gained or lost
36
- ### Answer | ### Research | ### Verdict output of ask, research, verify
37
- ### Failure error, reasoning, healing attempts, compact ARIA of the page
38
- ### Instance the browser you are on and the other instances running
39
- ### Artifacts paths to the full aria.yml, page.html and network.jsonl
40
- used: is code that already executed - CodeceptJS steps to copy as they are, except
41
- for pw, whose Playwright expression a test needs inside I.usePlaywrightTo(...).
42
- Log lines can precede the envelope; start parsing at the first ### line.
43
-
44
- HEALING AND FAILURE
45
- A failed action is retried by AI along a different route; healed: true means the
46
- outcome was reached another way and used: holds the code that worked.
47
- --no-heal skips that and fails fast.
48
- Failures print compact ARIA inline, so retarget from the envelope itself and open
49
- the artifact files only when the inline snapshot is not enough.
50
-
51
- SESSIONS
52
- By default prima attaches to the playwright-cli browser of this workspace and works
53
- on the tabs it already has open; driving the same session from both tools is the
54
- intended usage.
55
- playwright-cli open <url> the session prima attaches to
56
- --pw-session <title> which playwright-cli session, when several are open
57
- --endpoint <ep> attach to a browser server endpoint directly
58
- prima browser start a prima-owned browser instead, when no session is open
59
- --instance <name> which prima-owned browser you talk to; parallel work
60
- needs one each
61
- --session [file] cookies and storage persisted across processes; ignored
62
- while attached, the attached session keeps its own
63
- Prima never launches a browser implicitly and never closes an attached one - it
64
- disconnects. browser list shows both kinds; ### Instance names the one you are on.
65
- Every browser is reached over a Playwright browser-server endpoint, which needs the
66
- Node build - run prima as "npx explorbot prima ..." or through the published prima
67
- bin; from source under Bun the connection does not open.
68
- When no AI model is usable pw still works; for everything else drive
69
- playwright-cli directly.
70
- Parsed but not active yet: --framework, so reported code is CodeceptJS whatever
71
- you pass.
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.
72
59
  `;
73
60
 
74
- function buildOptions(options: any): PrimaOptions {
61
+ let rootOptions: () => any = () => ({});
62
+
63
+ function buildOptions(subcommand: any): PrimaOptions {
64
+ const options = { ...rootOptions(), ...stripEmpty(subcommand) };
75
65
  return {
76
- verbose: options.verbose || options.debug,
77
66
  config: options.config,
78
67
  path: options.path,
79
68
  instance: options.instance,
80
69
  session: options.session,
81
- heal: options.heal,
82
70
  ephemeral: options.ephemeral,
83
71
  framework: options.framework,
84
72
  noVision: options.vision === false,
@@ -91,30 +79,39 @@ function buildOptions(options: any): PrimaOptions {
91
79
  };
92
80
  }
93
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
+
94
91
  function addCommonOptions(cmd: Command): Command {
95
92
  return cmd
96
- .option('-v, --verbose', 'Enable verbose logging')
97
- .option('--debug', 'Enable debug logging (same as --verbose)')
98
93
  .option('-c, --config <path>', 'Path to explorbot configuration file')
99
94
  .option('-p, --path <path>', 'Working directory path')
100
95
  .option('-i, --instance <name>', 'Browser instance to drive')
101
96
  .option('--session [file]', 'Persist cookies and storage to a session file')
102
- .option('--no-heal', 'Fail immediately instead of letting AI retry a failed action')
103
97
  .option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
104
98
  .option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
105
99
  .option('--url <url>', 'Page to open when the session has no page yet')
106
100
  .option('--endpoint <ep>', 'Websocket endpoint of a browser server to attach to, skipping discovery')
107
- .option('--pw-session <title>', 'Title of the playwright-cli session to attach to');
101
+ .option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
102
+ .addHelpText('after', `\n${sessionHelp}`);
108
103
  }
109
104
 
110
105
  function primaFor(options: any): Prima {
111
- setPreserveConsoleLogs(true);
112
106
  if (options.ephemeral) process.env.EXPLORBOT_EPHEMERAL = '1';
113
107
  return new Prima(buildOptions(options));
114
108
  }
115
109
 
116
- async function runPrima(options: any, command: string, run: (prima: Prima) => Promise<EnvelopeData>): Promise<void> {
110
+ async function runPrima(options: any, command: string, run: (prima: Prima) => Promise<EnvelopeData>, record = true): Promise<void> {
111
+ setQuietMode(!isVerboseMode());
112
+ trackActivityLine();
117
113
  const prima = primaFor(options);
114
+ const startedAt = Date.now();
118
115
 
119
116
  let envelope: EnvelopeData;
120
117
  try {
@@ -124,6 +121,8 @@ async function runPrima(options: any, command: string, run: (prima: Prima) => Pr
124
121
  envelope = await prima.toolFailureEnvelope(command, error);
125
122
  }
126
123
 
124
+ if (record) prima.record(envelope, Date.now() - startedAt);
125
+ clearActivityLine();
127
126
  console.log(renderEnvelope(envelope));
128
127
  await prima.stop().catch(() => {});
129
128
  process.exit(envelope.ok ? 0 : 1);
@@ -143,32 +142,38 @@ async function runBrowser(options: any, run: (prima: Prima) => Promise<boolean>)
143
142
 
144
143
  export function createPrimaCommands(name = 'prima'): Command {
145
144
  const cmd = new Command(name);
146
- cmd.description('Drive an already-open browser one command at a time and report back in a plain-text envelope');
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');
147
148
  cmd.addHelpText('after', `\n${helpContract}`);
149
+ rootOptions = () => cmd.opts();
148
150
 
149
151
  addCommonOptions(cmd.command('pw <fn>').description('Run a Playwright function expression against the open page')).action(async (fn, options) => {
150
152
  await runPrima(options, `pw ${fn}`, (prima) => prima.pw(fn));
151
153
  });
152
154
 
153
- addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction')).action(async (instructions, options) => {
154
- await runPrima(options, `do ${instructions.join(' ')}`, (prima) => prima.do(instructions));
155
- });
156
-
157
- addCommonOptions(cmd.command('click <target>').description('Click an element described in plain words')).action(async (target, options) => {
158
- await runPrima(options, `click ${target}`, (prima) => prima.click(target));
159
- });
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
160
 
161
- addCommonOptions(cmd.command('fill <field> <value>').description('Fill a field described in plain words')).action(async (field, value, options) => {
162
- await runPrima(options, `fill ${field} ${value}`, (prima) => prima.fill(field, value));
163
- });
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
+ });
164
167
 
165
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) => {
166
169
  await runPrima(options, `ask ${question}`, (prima) => prima.ask(question));
167
170
  });
168
171
 
169
- addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page')).action(async (assertion, options) => {
170
- await runPrima(options, `verify ${assertion}`, (prima) => prima.verify(assertion));
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
+ });
172
177
 
173
178
  addCommonOptions(
174
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')
@@ -181,6 +186,30 @@ export function createPrimaCommands(name = 'prima'): Command {
181
186
  await runPrima(options, `go ${target}`, (prima) => prima.go(target));
182
187
  });
183
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
+
184
213
  const browser = cmd.command('browser').description('Manage the browsers prima drives');
185
214
 
186
215
  addCommonOptions(browser.command('start').description('Start a prima-owned browser and hold it open until Ctrl+C'))