explorbot 0.4.2 → 0.4.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 (74) hide show
  1. package/bin/explorbot-cli.ts +6 -1
  2. package/boat/api-tester/src/apibot.ts +8 -13
  3. package/boat/api-tester/src/cli.ts +7 -3
  4. package/boat/api-tester/src/config.ts +45 -9
  5. package/boat/prima/src/cli.ts +33 -99
  6. package/boat/prima/src/envelope.ts +3 -1
  7. package/boat/prima/src/help.ts +72 -0
  8. package/boat/prima/src/prima.ts +33 -43
  9. package/dist/bin/explorbot-cli.js +5 -1
  10. package/dist/boat/api-tester/src/apibot.js +7 -6
  11. package/dist/boat/api-tester/src/cli.js +9 -3
  12. package/dist/boat/api-tester/src/config.js +32 -6
  13. package/dist/boat/prima/src/cli.js +30 -86
  14. package/dist/boat/prima/src/envelope.js +2 -1
  15. package/dist/boat/prima/src/help.js +63 -0
  16. package/dist/boat/prima/src/prima.js +29 -41
  17. package/dist/package.json +1 -1
  18. package/dist/src/action-result.d.ts +3 -0
  19. package/dist/src/action-result.js +5 -0
  20. package/dist/src/action.js +12 -1
  21. package/dist/src/ai/fisherman/request-haul.d.ts +1 -0
  22. package/dist/src/ai/fisherman/request-haul.js +3 -0
  23. package/dist/src/ai/fisherman/tools.d.ts +50 -0
  24. package/dist/src/ai/{fisherman-tools.js → fisherman/tools.js} +78 -13
  25. package/dist/src/ai/fisherman.d.ts +12 -3
  26. package/dist/src/ai/fisherman.js +89 -13
  27. package/dist/src/ai/pilot.d.ts +13 -1
  28. package/dist/src/ai/pilot.js +20 -7
  29. package/dist/src/ai/researcher/deep-analysis.d.ts +1 -1
  30. package/dist/src/ai/researcher/deep-analysis.js +4 -1
  31. package/dist/src/ai/researcher/sections.d.ts +1 -1
  32. package/dist/src/ai/researcher/sections.js +2 -1
  33. package/dist/src/ai/researcher.js +25 -11
  34. package/dist/src/ai/rules.js +2 -0
  35. package/dist/src/ai/tester.d.ts +1 -0
  36. package/dist/src/ai/tester.js +27 -33
  37. package/dist/src/ai/tools.js +5 -0
  38. package/dist/src/api/request-result.js +3 -1
  39. package/dist/src/api/request-store.d.ts +6 -1
  40. package/dist/src/api/request-store.js +55 -17
  41. package/dist/src/api/xhr-capture.d.ts +2 -0
  42. package/dist/src/api/xhr-capture.js +35 -10
  43. package/dist/src/commands/config-command.js +6 -2
  44. package/dist/src/commands/help-json-command.d.ts +31 -0
  45. package/dist/src/commands/help-json-command.js +58 -0
  46. package/dist/src/config.d.ts +3 -0
  47. package/dist/src/config.js +14 -0
  48. package/dist/src/state-manager.js +5 -1
  49. package/docs/api-testing/basics.md +12 -4
  50. package/docs/reference/commands.md +2 -0
  51. package/docs/reference/configuration.md +4 -0
  52. package/docs/superpowers/plans/2026-09-03-fisherman-query-api.md +1361 -0
  53. package/docs/workflow/agentic-usage.md +15 -1
  54. package/package.json +1 -1
  55. package/src/action-result.ts +7 -0
  56. package/src/action.ts +14 -2
  57. package/src/ai/fisherman/request-haul.ts +4 -0
  58. package/src/ai/{fisherman-tools.ts → fisherman/tools.ts} +93 -20
  59. package/src/ai/fisherman.ts +104 -15
  60. package/src/ai/pilot.ts +20 -7
  61. package/src/ai/researcher/deep-analysis.ts +4 -2
  62. package/src/ai/researcher/sections.ts +2 -2
  63. package/src/ai/researcher.ts +28 -11
  64. package/src/ai/rules.ts +2 -0
  65. package/src/ai/tester.ts +25 -30
  66. package/src/ai/tools.ts +6 -0
  67. package/src/api/request-result.ts +2 -1
  68. package/src/api/request-store.ts +58 -18
  69. package/src/api/xhr-capture.ts +39 -11
  70. package/src/commands/config-command.ts +4 -1
  71. package/src/commands/help-json-command.ts +74 -0
  72. package/src/config.ts +16 -0
  73. package/src/state-manager.ts +6 -1
  74. package/dist/src/ai/fisherman-tools.d.ts +0 -147
@@ -8,6 +8,7 @@ import figureSet from 'figures';
8
8
  import { render } from 'ink';
9
9
  import React from 'react';
10
10
  import { flushTelemetry } from '../src/ai/provider.js';
11
+ import { HelpJsonCommand } from '../src/commands/help-json-command.js';
11
12
  import { RecommendedModelsCommand } from '../src/commands/recommended-models-command.js';
12
13
  import { App } from '../src/components/App.js';
13
14
  import { StatusPane } from '../src/components/StatusPane.js';
@@ -43,7 +44,9 @@ process.on('unhandledRejection', (reason) => {
43
44
  tag('error').log(`Unhandled rejection: ${reason instanceof Error ? `${reason.message}\n${reason.stack}` : String(reason)}`);
44
45
  });
45
46
 
46
- if (!process.env.EXPLORBOT_NO_BANNER && !process.argv.includes('prima')) {
47
+ const printsJson = process.argv.includes('--json') || process.argv.includes('help-json');
48
+
49
+ if (!process.env.EXPLORBOT_NO_BANNER && !process.argv.includes('prima') && !printsJson) {
47
50
  console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
48
51
  }
49
52
 
@@ -952,6 +955,8 @@ ${rows}
952
955
  `;
953
956
  };
954
957
 
958
+ program.addHelpText('after', `\nFor agents and tools:\n ${cli} help-json [command...] the same definitions as JSON — commands, arguments, options, defaults\n`);
955
959
  program.addHelpText('after', envHelp);
960
+ HelpJsonCommand.register(program);
956
961
 
957
962
  program.parse();
@@ -10,7 +10,7 @@ import { setVerboseMode, tag } from '../../../src/utils/logger.ts';
10
10
  import { Chief } from './ai/chief.ts';
11
11
  import { Curler } from './ai/curler.ts';
12
12
  import { ApiClient } from './api-client.ts';
13
- import { type ApibotConfig, ApibotConfigParser } from './config.ts';
13
+ import { type ApibotConfig, ApibotConfigParser, type ApibotRunOptions } from './config.ts';
14
14
 
15
15
  export class ApiBot {
16
16
  private configParser: ApibotConfigParser;
@@ -141,7 +141,7 @@ export class ApiBot {
141
141
  return this.currentPlan;
142
142
  }
143
143
 
144
- savePlan(filename?: string): string | null {
144
+ savePlan(suffix?: string): string | null {
145
145
  if (!this.currentPlan) return null;
146
146
 
147
147
  const plansDir = this.configParser.getPlansDir();
@@ -149,8 +149,7 @@ export class ApiBot {
149
149
  mkdirSync(plansDir, { recursive: true });
150
150
  }
151
151
 
152
- const planFilename = filename || this.generatePlanFilename();
153
- const planPath = path.join(plansDir, planFilename);
152
+ const planPath = path.join(plansDir, this.generatePlanFilename(suffix));
154
153
  this.currentPlan.saveToMarkdown(planPath);
155
154
  return planPath;
156
155
  }
@@ -192,20 +191,16 @@ export class ApiBot {
192
191
  }
193
192
  }
194
193
 
195
- private generatePlanFilename(): string {
194
+ private generatePlanFilename(suffix?: string): string {
196
195
  const endpoint = this.currentPlan?.url || '/';
197
- const sanitized = endpoint.replace(/^\//, '').replace(/[^a-zA-Z0-9]/g, '_') || 'root';
198
- return `${sanitized.slice(0, 200)}.md`;
196
+ let name = endpoint.replace(/^\//, '').replace(/[^a-zA-Z0-9]/g, '_') || 'root';
197
+ if (suffix) name = `${name}_${suffix}`;
198
+ return `${name.slice(0, 200)}.md`;
199
199
  }
200
200
  }
201
201
 
202
- interface ApibotOptions {
202
+ interface ApibotOptions extends ApibotRunOptions {
203
203
  verbose?: boolean;
204
- config?: string;
205
- path?: string;
206
- endpoint?: string;
207
- baseEndpoint?: string;
208
- spec?: string;
209
204
  }
210
205
 
211
206
  export type { ApibotOptions };
@@ -16,6 +16,7 @@ function buildOptions(options: any): ApibotOptions {
16
16
  path: options.path,
17
17
  baseEndpoint: options.endpoint,
18
18
  spec: options.spec,
19
+ header: options.header,
19
20
  };
20
21
  }
21
22
 
@@ -26,7 +27,8 @@ function addCommonOptions(cmd: Command): Command {
26
27
  .option('-c, --config <path>', 'Path to configuration file')
27
28
  .option('-p, --path <path>', 'Working directory path')
28
29
  .option('--endpoint <url>', 'Base API endpoint to test (env: EXPLORBOT_URL)')
29
- .option('--spec <path>', 'OpenAPI spec file or URL (env: EXPLORBOT_API_SPEC)');
30
+ .option('--spec <path>', 'OpenAPI spec file or URL (env: EXPLORBOT_API_SPEC)')
31
+ .option('-H, --header <header>', 'Header sent with every request, as "Name: value". Repeatable (env: EXPLORBOT_API_HEADERS)', (value: string, previous: string[] = []) => [...previous, value]);
30
32
  }
31
33
 
32
34
  function selectTests(tests: any[], index?: string): any[] {
@@ -101,6 +103,7 @@ export function createApiCommands(name = 'api'): Command {
101
103
  const [site] = listSites();
102
104
  const runOptions = buildOptions(options);
103
105
  runOptions.endpoint = endpoint || site?.url;
106
+ if (runOptions.endpoint && URL.canParse(runOptions.endpoint)) runOptions.baseEndpoint ||= runOptions.endpoint;
104
107
  try {
105
108
  const config = await parser.loadConfig(runOptions);
106
109
  console.log(ConfigCommand.render(config, { configPath: parser.getConfigPath(), root: parser.getProjectRoot(), json: options.json }));
@@ -149,9 +152,10 @@ export function createApiCommands(name = 'api'): Command {
149
152
  }
150
153
  });
151
154
 
152
- addCommonOptions(cmd.command('explore <endpoint>').description('Full cycle: plan all styles, execute tests, re-plan')).action(async (endpoint, options) => {
155
+ addCommonOptions(cmd.command('explore <endpoint>').description('Full cycle: plan all styles, execute tests, re-plan. The endpoint may be the base endpoint itself')).action(async (endpoint, options) => {
153
156
  setPreserveConsoleLogs(true);
154
157
  try {
158
+ if (URL.canParse(endpoint)) options.endpoint ||= endpoint;
155
159
  const bot = new ApiBot({ ...buildOptions(options), endpoint });
156
160
  await bot.start();
157
161
 
@@ -182,7 +186,7 @@ export function createApiCommands(name = 'api'): Command {
182
186
  else totalFailed++;
183
187
  }
184
188
 
185
- bot.savePlan(`${endpoint.replace(/^\//, '').replace(/[^a-zA-Z0-9]/g, '_')}_${style}.md`);
189
+ bot.savePlan(style);
186
190
  }
187
191
 
188
192
  console.log('\n=== Final Results ===');
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
2
2
  import path, { resolve } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { parseEnv } from 'node:util';
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
+ import { type AIConfig, type ApiHookFn, type ApiConfig as BaseApiConfig, ConfigMissingError, EXPLORBOT_CONFIG_PATHS, createModel, envConfigRequested, materializeKnowledge, missingConfigMessage, resolveConfigModels, resolveModel, resolveOutputRoot, setOutputDir } from '../../../src/config.ts';
6
6
  import { type SiteRecord, findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from '../../../src/global-config.ts';
7
7
 
8
8
  export type { AIConfig };
@@ -24,6 +24,20 @@ interface ApibotConfig {
24
24
  };
25
25
  }
26
26
 
27
+ function isAbsoluteEndpoint(value?: string): boolean {
28
+ return !!value && (value.startsWith('http://') || value.startsWith('https://'));
29
+ }
30
+
31
+ function parseHeaders(raw: string): Record<string, string> {
32
+ const headers: Record<string, string> = {};
33
+ for (const line of raw.split('\n')) {
34
+ const separator = line.indexOf(':');
35
+ if (separator < 1) continue;
36
+ headers[line.slice(0, separator).trim()] = line.slice(separator + 1).trim();
37
+ }
38
+ return headers;
39
+ }
40
+
27
41
  export class ApibotConfigParser {
28
42
  private static instance: ApibotConfigParser;
29
43
  private config: ApibotConfig | null = null;
@@ -45,7 +59,7 @@ export class ApibotConfigParser {
45
59
  Object.assign(process.env, parseEnv(readFileSync(resolved, 'utf8')));
46
60
  }
47
61
 
48
- async loadConfig(options?: { config?: string; path?: string; endpoint?: string; baseEndpoint?: string; spec?: string }): Promise<ApibotConfig> {
62
+ async loadConfig(options?: ApibotRunOptions): Promise<ApibotConfig> {
49
63
  if (this.config && !options?.config && !options?.path) return this.config;
50
64
 
51
65
  const originalCwd = process.cwd();
@@ -84,6 +98,7 @@ export class ApibotConfigParser {
84
98
 
85
99
  this.config = this.mergeWithDefaults(loadedConfig);
86
100
  this.applyEnvSpec(this.config.api);
101
+ this.applyEnvHeaders(this.config.api);
87
102
  if (options?.baseEndpoint) this.config.api.baseEndpoint = options.baseEndpoint.replace(/\/$/, '');
88
103
  await resolveConfigModels(this.config.ai);
89
104
  this.configPath = resolvedPath;
@@ -94,6 +109,7 @@ export class ApibotConfigParser {
94
109
  }
95
110
 
96
111
  this.validateConfig(this.config);
112
+ setOutputDir(this.getOutputDir());
97
113
 
98
114
  return this.config;
99
115
  } finally {
@@ -125,12 +141,15 @@ export class ApibotConfigParser {
125
141
  }
126
142
 
127
143
  resolveEndpointPath(endpoint: string): string {
128
- if (!this.site) return endpoint;
144
+ if (!this.site && !isAbsoluteEndpoint(endpoint)) return endpoint;
129
145
 
130
- const resolved = resolveSiteTarget(endpoint, this.site.url);
131
- if (resolved.baseUrl !== this.site.url) return endpoint;
146
+ const base = new URL(this.getConfig().api.baseEndpoint);
147
+ const origin = this.site?.url || base.origin;
132
148
 
133
- const basePath = new URL(this.getConfig().api.baseEndpoint).pathname.replace(/\/$/, '');
149
+ const resolved = resolveSiteTarget(endpoint, origin);
150
+ if (resolved.baseUrl !== origin) return endpoint;
151
+
152
+ const basePath = base.pathname.replace(/\/$/, '');
134
153
  if (!basePath) return resolved.path;
135
154
  if (resolved.path === basePath) return '/';
136
155
  if (resolved.path.startsWith(`${basePath}/`)) return resolved.path.slice(basePath.length);
@@ -156,9 +175,10 @@ export class ApibotConfigParser {
156
175
  }
157
176
  }
158
177
 
159
- private applyRunOptions(options?: { baseEndpoint?: string; spec?: string }): void {
178
+ private applyRunOptions(options?: ApibotRunOptions): void {
160
179
  if (options?.baseEndpoint) process.env.EXPLORBOT_URL = options.baseEndpoint;
161
180
  if (options?.spec) process.env.EXPLORBOT_API_SPEC = options.spec;
181
+ if (options?.header?.length) process.env.EXPLORBOT_API_HEADERS = options.header.join('\n');
162
182
  }
163
183
 
164
184
  private applyEnvSpec(api: ApiConfig): void {
@@ -166,6 +186,11 @@ export class ApibotConfigParser {
166
186
  api.spec = [process.env.EXPLORBOT_API_SPEC];
167
187
  }
168
188
 
189
+ private applyEnvHeaders(api: ApiConfig): void {
190
+ if (!process.env.EXPLORBOT_API_HEADERS) return;
191
+ api.headers = { ...api.headers, ...parseHeaders(process.env.EXPLORBOT_API_HEADERS) };
192
+ }
193
+
169
194
  private enterGlobalMode(config: ApibotConfig, endpoint?: string): void {
170
195
  const site = resolveSiteTarget(endpoint);
171
196
  this.site = registerSite(site.baseUrl);
@@ -189,7 +214,7 @@ export class ApibotConfigParser {
189
214
  throw new Error('EXPLORBOT_AI_MODEL needs a provider — set EXPLORBOT_AI_PROVIDER, or write it as "provider/model-id"');
190
215
  }
191
216
 
192
- const baseEndpoint = process.env.EXPLORBOT_URL;
217
+ const baseEndpoint = process.env.EXPLORBOT_URL?.replace(/\/$/, '');
193
218
  if (!baseEndpoint) {
194
219
  throw new Error('No API endpoint to test. Pass --endpoint or set EXPLORBOT_URL to the API base endpoint');
195
220
  }
@@ -199,6 +224,7 @@ export class ApibotConfigParser {
199
224
 
200
225
  const api: ApiConfig = { baseEndpoint };
201
226
  this.applyEnvSpec(api);
227
+ this.applyEnvHeaders(api);
202
228
 
203
229
  let model: any;
204
230
  if (provider && modelSpec) model = await createModel(provider, modelSpec);
@@ -212,6 +238,7 @@ export class ApibotConfigParser {
212
238
  };
213
239
  this.configPath = path.join(outputRoot, 'apibot.config.js');
214
240
  this.validateConfig(this.config);
241
+ setOutputDir(this.getOutputDir());
215
242
 
216
243
  return this.config;
217
244
  }
@@ -282,4 +309,13 @@ export class ApibotConfigParser {
282
309
  }
283
310
  }
284
311
 
285
- export type { ApibotConfig, ApiConfig, HookFn };
312
+ interface ApibotRunOptions {
313
+ config?: string;
314
+ path?: string;
315
+ endpoint?: string;
316
+ baseEndpoint?: string;
317
+ spec?: string;
318
+ header?: string[];
319
+ }
320
+
321
+ export type { ApibotConfig, ApiConfig, HookFn, ApibotRunOptions };
@@ -1,81 +1,13 @@
1
1
  import { Command } from 'commander';
2
- import dedent from 'dedent';
3
2
  import { keepServerRunning } from '../../../src/browser-server.ts';
4
3
  import { RecommendedModelsCommand } from '../../../src/commands/recommended-models-command.ts';
5
4
  import { browserErrorMessage } from '../../../src/utils/browser-errors.ts';
6
5
  import { isVerboseMode, setQuietMode } from '../../../src/utils/logger.ts';
7
6
  import { clearActivityLine, trackActivityLine } from './activity-line.ts';
8
7
  import { type EnvelopeData, renderEnvelope } from './envelope.ts';
8
+ import { askHelp, checkHelp, doHelp, helpContract, reportHelp, researchHelp, sessionHelp, statusHelp, verifyHelp } from './help.ts';
9
9
  import { Prima, type PrimaOptions } from './prima.ts';
10
10
 
11
- const helpContract = dedent`
12
- Prima is a high-level AI extension to playwright-cli, driving the browser it has open.
13
-
14
- playwright-cli open <url> starts the session
15
- prima <command> ... drives it
16
- playwright-cli close ends it
17
-
18
- One call takes a whole job:
19
-
20
- prima check "a workflow can be created and appears in the list" --expected "the new workflow is listed"
21
- prima do "open the account menu" "choose the settings entry" "switch the theme to dark" "check it took effect"
22
- prima pw "({ page }) => page.click('[data-test=submit]')"
23
- `;
24
-
25
- const checkHelp = dedent`
26
- check takes an outcome rather than a click path, and works out how to reach it. It runs
27
- on the page you are already on and never reloads it, so an open dialog survives the check.
28
- --expected one outcome the run must reach, repeatable for several. Without it the
29
- scenario text is the single expected outcome. Each comes back under
30
- ### Expected outcomes as PASSED, FAILED, CONTRADICTION or not verified.
31
- "not verified" means the run never checked it, which is not the same
32
- as false.
33
- Outcomes are settled against a screenshot of the whole page: what a user can see is
34
- the proof, and the run log only says what was done. CONTRADICTION means the two
35
- disagree - reported with both sides rather than settled one way, so read the html,
36
- aria and screenshot named under ### Artifacts and judge it yourself. Not finding
37
- something in the picture is not enough on its own; that is "not verified".
38
- ok: follows those outcomes - false when one FAILED or CONTRADICTED, or when the run
39
- could not complete, which is reported as such rather than as an app failure.
40
- Page problems seen on the way appear under ### Answer, not as step failures.
41
- `;
42
-
43
- const doHelp = dedent`
44
- Each instruction is numbered and accounted for: ### Steps reports each as ok, FAIL or ??.
45
- ?? means the action ran but the run ended without confirming that instruction - read the
46
- steps above it. Only FAIL and an instruction the page could not carry out fail the command.
47
- Nothing runs past the last instruction given. A whole remaining sequence in one call is
48
- what makes this tier cheap.
49
- `;
50
-
51
- const verifyHelp = dedent`
52
- Reports each assertion it could express as PASSED or FAILED with its playwright form,
53
- and gives no overall verdict - read the lines and decide. "none ran" means the claim
54
- could not be expressed, which is not the same as false.
55
- `;
56
-
57
- const statusHelp = dedent`
58
- Reads the files a command recorded, so it needs no browser and outlives the session.
59
- The hash is looked up across every recorded site. ### Artifacts names every file kept
60
- under it: the aria tree, the html, the screenshot and network log when they were
61
- captured, and the per-step captures of a do run.
62
- `;
63
-
64
- const reportHelp = dedent`
65
- Commands are logged as they run, so the report needs no browser and outlives the session.
66
- The most recent session is reported unless --pw-session names another.
67
- `;
68
-
69
- const sessionHelp = dedent`
70
- --endpoint <ep> attach to a browser server endpoint directly, skipping discovery
71
- --instance <name> which prima-owned browser you talk to; parallel work needs one each
72
- --session [file] cookies and storage persisted across processes; ignored while
73
- attached, since the attached session keeps its own
74
- --framework parsed but not active yet; reported code is CodeceptJS either way
75
- DEBUG='explorbot:*' in front of a command prints the log of everything it does.
76
- When no AI model is usable pw still works; for everything else drive playwright-cli.
77
- `;
78
-
79
11
  let rootOptions: () => any = () => ({});
80
12
 
81
13
  function buildOptions(subcommand: any): PrimaOptions {
@@ -114,13 +46,16 @@ function addCommonOptions(cmd: Command): Command {
114
46
  .option('--session [file]', 'Persist cookies and storage to a session file')
115
47
  .option('--model <model>', 'Main model, as provider/model-id')
116
48
  .option('--vision-model <model>', 'Model for screenshot analysis, as provider/model-id')
117
- .option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
118
- .option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
49
+ .option('--ephemeral', 'Keep no state; config-free runs use a temp directory')
50
+ .option('--framework <name>', 'Inactive: reported code targets codeceptjs or playwright')
119
51
  .option('--url <url>', 'Page to open when the session has no page yet')
120
52
  .option('--spec <path>', 'Docbot application spec directory or index.md to read as page knowledge')
121
53
  .option('--endpoint <ep>', 'Websocket endpoint of a browser server to attach to, skipping discovery')
122
- .option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
123
- .addHelpText('after', `\n${sessionHelp}`);
54
+ .option('--pw-session <title>', 'Title of the playwright-cli session to attach to');
55
+ }
56
+
57
+ function addBrowserOptions(cmd: Command): Command {
58
+ return addCommonOptions(cmd).addHelpText('after', `\n${sessionHelp}`);
124
59
  }
125
60
 
126
61
  function primaFor(options: any): Prima {
@@ -132,7 +67,8 @@ function primaFor(options: any): Prima {
132
67
  return new Prima(buildOptions(options));
133
68
  }
134
69
 
135
- async function runPrima(options: any, command: string, run: (prima: Prima) => Promise<EnvelopeData>): Promise<void> {
70
+ async function runPrima(options: any, command: string, run: (prima: Prima) => Promise<EnvelopeData>, opts: { browser?: boolean; record?: boolean } = {}): Promise<void> {
71
+ const { browser = true, record = true } = opts;
136
72
  setQuietMode(!isVerboseMode());
137
73
  trackActivityLine();
138
74
  const prima = primaFor(options);
@@ -140,13 +76,13 @@ async function runPrima(options: any, command: string, run: (prima: Prima) => Pr
140
76
 
141
77
  let envelope: EnvelopeData;
142
78
  try {
143
- await prima.start();
79
+ if (browser) await prima.start();
144
80
  envelope = await run(prima);
145
81
  } catch (error) {
146
82
  envelope = await prima.toolFailureEnvelope(command, error);
147
83
  }
148
84
 
149
- prima.record(envelope, Date.now() - startedAt);
85
+ if (record) prima.record(envelope, Date.now() - startedAt);
150
86
  clearActivityLine();
151
87
  console.log(renderEnvelope(envelope));
152
88
  await prima.stop().catch(() => {});
@@ -167,46 +103,48 @@ async function runBrowser(options: any, run: (prima: Prima) => Promise<boolean>)
167
103
 
168
104
  export function createPrimaCommands(name = 'prima'): Command {
169
105
  const cmd = new Command(name);
170
- 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');
106
+ cmd.description('Drives a web app through described behaviour, not locators');
171
107
  cmd.option('--pw-session <title>', 'Title of the playwright-cli session to attach to');
172
108
  cmd.option('--url <url>', 'Page to open when the session has no page yet');
173
109
  cmd.addHelpText('after', `\n${helpContract}`);
174
110
  rootOptions = () => cmd.opts();
175
111
 
176
- addCommonOptions(cmd.command('pw <fn>').description('Run a Playwright function expression against the open page')).action(async (fn, options) => {
112
+ addBrowserOptions(cmd.command('pw <fn>').description('Run a Playwright function expression against the open page')).action(async (fn, options) => {
177
113
  await runPrima(options, `pw ${fn}`, (prima) => prima.pw(fn));
178
114
  });
179
115
 
180
- addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction'))
116
+ addBrowserOptions(cmd.command('do <instructions...>').description('Run instructions, one argument each'))
181
117
  .addHelpText('after', `\n${doHelp}`)
182
118
  .action(async (instructions, options) => {
183
119
  await runPrima(options, `do ${instructions.join(' ')}`, (prima) => prima.do(instructions));
184
120
  });
185
121
 
186
- 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'))
122
+ addBrowserOptions(cmd.command('check <scenario>').description('Run a scenario as a test, with verification'))
187
123
  .option('--expected <outcome>', 'An outcome the run must reach; repeat the flag for several', (value: string, all: string[]) => [...all, value], [])
188
124
  .addHelpText('after', `\n${checkHelp}`)
189
125
  .action(async (scenario, options) => {
190
126
  await runPrima(options, `check ${scenario}`, (prima) => prima.check(scenario, options.expected));
191
127
  });
192
128
 
193
- 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) => {
194
- await runPrima(options, `ask ${question}`, (prima) => prima.ask(question));
195
- });
129
+ addBrowserOptions(cmd.command('ask <question>').description('Answer a question about the current page').option('--no-vision', 'Answer from page structure only, without a screenshot'))
130
+ .addHelpText('after', `\n${askHelp}`)
131
+ .action(async (question, options) => {
132
+ await runPrima(options, `ask ${question}`, (prima) => prima.ask(question));
133
+ });
196
134
 
197
- addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page'))
135
+ addBrowserOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page'))
198
136
  .addHelpText('after', `\n${verifyHelp}`)
199
137
  .action(async (assertion, options) => {
200
138
  await runPrima(options, `verify ${assertion}`, (prima) => prima.verify(assertion));
201
139
  });
202
140
 
203
- addCommonOptions(
204
- 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')
205
- ).action(async (options) => {
206
- await runPrima(options, 'research', (prima) => prima.research({ data: options.data, deep: options.deep, fresh: options.fresh }));
207
- });
141
+ addBrowserOptions(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'))
142
+ .addHelpText('after', `\n${researchHelp}`)
143
+ .action(async (options) => {
144
+ await runPrima(options, 'research', (prima) => prima.research({ data: options.data, deep: options.deep, fresh: options.fresh }));
145
+ });
208
146
 
209
- addCommonOptions(cmd.command('go <target>').description('Navigate to a url, a path, or a page described in plain words')).action(async (target, options) => {
147
+ addBrowserOptions(cmd.command('go <target>').description('Navigate to a url, a path, or a page described in plain words')).action(async (target, options) => {
210
148
  if (URL.canParse(target)) options.baseUrl = target;
211
149
  await runPrima(options, `go ${target}`, (prima) => prima.go(target));
212
150
  });
@@ -226,11 +164,7 @@ export function createPrimaCommands(name = 'prima'): Command {
226
164
  addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command'))
227
165
  .addHelpText('after', `\n${statusHelp}`)
228
166
  .action(async (hash, options) => {
229
- setQuietMode(!isVerboseMode());
230
- const prima = primaFor(options);
231
- const envelope = await prima.status(hash).catch((error: unknown) => prima.toolFailureEnvelope(`status ${hash}`, error));
232
- console.log(renderEnvelope(envelope));
233
- process.exit(envelope.ok ? 0 : 1);
167
+ await runPrima(options, `status ${hash}`, (prima) => prima.status(hash), { browser: false, record: false });
234
168
  });
235
169
 
236
170
  addCommonOptions(cmd.command('report').description('Turn every command of a session into one html and markdown report'))
@@ -247,7 +181,7 @@ export function createPrimaCommands(name = 'prima'): Command {
247
181
 
248
182
  const browser = cmd.command('browser').description('Manage the browsers prima drives');
249
183
 
250
- addCommonOptions(browser.command('start').description('Start a prima-owned browser and hold it open until Ctrl+C'))
184
+ addBrowserOptions(browser.command('start').description('Start a prima-owned browser and hold it open until Ctrl+C'))
251
185
  .option('-s, --show', 'Launch the browser in a visible window')
252
186
  .option('--headless', 'Launch the browser without a window')
253
187
  .action(async (options) => {
@@ -258,7 +192,7 @@ export function createPrimaCommands(name = 'prima'): Command {
258
192
  });
259
193
  });
260
194
 
261
- addCommonOptions(browser.command('stop').description('Stop the browser of this instance'))
195
+ addBrowserOptions(browser.command('stop').description('Stop the browser of this instance'))
262
196
  .option('--all', 'Stop every running instance')
263
197
  .action(async (options) => {
264
198
  await runBrowser(options, async (prima) => {
@@ -268,14 +202,14 @@ export function createPrimaCommands(name = 'prima'): Command {
268
202
  });
269
203
  });
270
204
 
271
- addCommonOptions(browser.command('status').description('Report the browser of this instance')).action(async (options) => {
205
+ addBrowserOptions(browser.command('status').description('Report the browser of this instance')).action(async (options) => {
272
206
  await runBrowser(options, async (prima) => {
273
207
  console.log(await prima.browserStatus());
274
208
  return true;
275
209
  });
276
210
  });
277
211
 
278
- addCommonOptions(browser.command('list').description('List every browser instance that is running')).action(async (options) => {
212
+ addBrowserOptions(browser.command('list').description('List every browser instance that is running')).action(async (options) => {
279
213
  await runBrowser(options, async (prima) => {
280
214
  console.log(await prima.browserList());
281
215
  return true;
@@ -5,6 +5,8 @@ export const STATUS_FILE = 'status.json';
5
5
 
6
6
  const ARTIFACT_FILES = { aria: 'aria.yml', html: 'page.html', screenshot: 'page.png', network: 'network.jsonl' };
7
7
 
8
+ export const STEP_FILES = { aria: 'aria.yaml', html: 'html', diff: 'diff.yaml' };
9
+
8
10
  const EXPECTATION_LABELS = {
9
11
  passed: 'PASSED ',
10
12
  failed: 'FAILED ',
@@ -117,7 +119,7 @@ function renderSteps(data: EnvelopeData): string | null {
117
119
  lines.push(`${index + 1}. ${mark} ${step.label}`);
118
120
  for (const line of (step.proof || '').split('\n').filter(Boolean)) lines.push(` ${line}`);
119
121
  });
120
- if (data.stepFiles) lines.push('', `page after each step: ${data.stepFiles}/<n>-<step>.{aria.yaml,html,diff.yaml}`);
122
+ if (data.stepFiles) lines.push('', `page after each step: ${data.stepFiles}/<n>-<step>.{${Object.values(STEP_FILES).join(',')}}`);
121
123
  return section('Steps', lines.join('\n'));
122
124
  }
123
125
 
@@ -0,0 +1,72 @@
1
+ import dedent from 'dedent';
2
+
3
+ export const helpContract = dedent`
4
+ Prima drives the browser opened by playwright-cli.
5
+
6
+ playwright-cli open <url> starts the session
7
+ prima <command> ... drives it
8
+ playwright-cli close ends it
9
+
10
+ One call runs a whole job:
11
+
12
+ prima check "a workflow can be created and appears in the list" --expected "the new workflow is listed"
13
+ prima do "open the account menu" "choose the settings entry" "switch the theme to dark" "check it took effect"
14
+ prima pw "({ page }) => page.click('[data-test=submit]')"
15
+
16
+ Only research maps a page. Other commands read the accessibility tree; a cached
17
+ research map joins when present. On large or unclear pages, run prima research first.
18
+ Without a usable AI model only pw works; otherwise use playwright-cli.
19
+ DEBUG='explorbot:*' in front of a command logs everything it does.
20
+ `;
21
+
22
+ export const checkHelp = dedent`
23
+ check states the outcome, not the clicks; it finds the path itself. It stays
24
+ on the current page and never reloads it, so an open dialog survives.
25
+ --expected one required outcome, repeatable for several. Without it the
26
+ scenario text is the outcome. Each returns under
27
+ ### Expected outcomes as PASSED, FAILED, CONTRADICTION or not verified.
28
+ "not verified" means never checked, not false.
29
+ Proof is a full-page screenshot: what a user sees counts, the log only shows actions.
30
+ CONTRADICTION means screenshot and log disagree; judge the html, aria and
31
+ screenshot under ### Artifacts yourself.
32
+ ok is false when any outcome FAILED or CONTRADICTED, or the run could not finish,
33
+ reported as such rather than as an app failure.
34
+ Side issues found on the way go under ### Answer, not as step failures.
35
+ `;
36
+
37
+ export const doHelp = dedent`
38
+ ### Steps marks each instruction ok, FAIL or ??. ?? means it ran but the run ended
39
+ without confirming it - read the steps above. Only FAIL fails the command.
40
+ Nothing runs past the last instruction. Batch the whole sequence in one call;
41
+ that is what keeps this tier cheap.
42
+ `;
43
+
44
+ export const askHelp = dedent`
45
+ Answers from a page screenshot, or from its structure with --no-vision.
46
+ `;
47
+
48
+ export const verifyHelp = dedent`
49
+ Reports each expressible assertion as PASSED or FAILED with its playwright form;
50
+ no overall verdict, read the lines. "none ran" means unexpressible, not false.
51
+ `;
52
+
53
+ export const researchHelp = dedent`
54
+ The map is saved per page state and joins later commands there, so one research run pays for all that follow it.
55
+ `;
56
+
57
+ export const statusHelp = dedent`
58
+ Reads recorded files, so it needs no browser and outlives the session.
59
+ The hash is matched across all recorded sites. ### Artifacts lists every kept
60
+ file: aria, html, screenshot and network log when captured, plus per-step captures of a do run.
61
+ `;
62
+
63
+ export const reportHelp = dedent`
64
+ Built from the command log, so it needs no browser and outlives the session.
65
+ Reports the latest session unless --pw-session names another.
66
+ `;
67
+
68
+ export const sessionHelp = dedent`
69
+ Parallel jobs need one --instance each. --session is ignored when attached,
70
+ since the attached session keeps its own. --framework is parsed but inactive;
71
+ reported code is CodeceptJS either way.
72
+ `;