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
@@ -10,6 +10,7 @@ export class RequestStore {
10
10
  onFailedListeners = [];
11
11
  outputDir;
12
12
  sessionStartedAt = new Date();
13
+ readEndpointKeys = new Set();
13
14
  constructor(outputDir) {
14
15
  this.outputDir = outputDir;
15
16
  }
@@ -17,6 +18,14 @@ export class RequestStore {
17
18
  this.capturedRequests.push(result);
18
19
  result.save(this.outputDir);
19
20
  }
21
+ addReadRequest(result) {
22
+ const key = readEndpointKey(result);
23
+ if (this.readEndpointKeys.has(key))
24
+ return;
25
+ this.readEndpointKeys.add(key);
26
+ this.capturedRequests.push(result);
27
+ result.save(this.outputDir);
28
+ }
20
29
  addFailedRequest(result) {
21
30
  this.failedRequests.push(result);
22
31
  for (const cb of this.onFailedListeners) {
@@ -47,14 +56,14 @@ export class RequestStore {
47
56
  getLastRequest() {
48
57
  return this.madeRequests[this.madeRequests.length - 1];
49
58
  }
50
- toEndpointList(scopePath) {
51
- let requests = this.capturedRequests;
59
+ toEndpointList(scopePath, methods = 'write') {
60
+ let requests = this.capturedRequests.filter((r) => matchesFamily(r, methods));
52
61
  if (scopePath)
53
- requests = this.getWriteRequestsForScope(scopePath);
62
+ requests = this.getRequestsForScope(scopePath, methods);
54
63
  const seen = new Set();
55
64
  const lines = [];
56
65
  for (const req of requests) {
57
- const key = `${req.method} ${generalizeUrl(req.path, () => '{id}')}`;
66
+ const key = `${req.method} ${generalizeUrl(req.path, () => '{id}')}${queryParamHint(req)}`;
58
67
  if (seen.has(key))
59
68
  continue;
60
69
  seen.add(key);
@@ -119,6 +128,12 @@ export class RequestStore {
119
128
  const result = RequestResult.load(path.join(requestsDir, file));
120
129
  if (existingIds.has(result.id))
121
130
  continue;
131
+ if (!result.isWrite) {
132
+ const key = readEndpointKey(result);
133
+ if (this.readEndpointKeys.has(key))
134
+ continue;
135
+ this.readEndpointKeys.add(key);
136
+ }
122
137
  this.capturedRequests.push(result);
123
138
  }
124
139
  catch {
@@ -127,17 +142,29 @@ export class RequestStore {
127
142
  }
128
143
  }
129
144
  getWriteRequestsForScope(scopePath) {
130
- const writes = this.capturedRequests.filter((r) => r.isWrite);
145
+ return this.getRequestsForScope(scopePath, 'write');
146
+ }
147
+ getReadRequestsForScope(scopePath) {
148
+ return this.getRequestsForScope(scopePath, 'read');
149
+ }
150
+ clear() {
151
+ this.capturedRequests = [];
152
+ this.madeRequests = [];
153
+ this.failedRequests = [];
154
+ this.readEndpointKeys.clear();
155
+ }
156
+ getRequestsForScope(scopePath, methods) {
157
+ const candidates = this.capturedRequests.filter((r) => matchesFamily(r, methods));
131
158
  const scopeSegments = scopePath.split('/').filter(Boolean);
132
159
  if (scopeSegments.length === 0)
133
- return writes;
160
+ return candidates;
134
161
  let scoped = [];
135
162
  let fewest = Number.POSITIVE_INFINITY;
136
163
  let ambiguous = false;
137
164
  for (const segment of scopeSegments) {
138
165
  if (isDynamicSegment(segment))
139
166
  continue;
140
- const matches = writes.filter((r) => r.path.split('/').includes(segment));
167
+ const matches = candidates.filter((r) => r.path.split('/').includes(segment));
141
168
  if (matches.length === 0 || matches.length > fewest)
142
169
  continue;
143
170
  if (matches.length === fewest) {
@@ -153,18 +180,29 @@ export class RequestStore {
153
180
  return [];
154
181
  return scoped;
155
182
  }
156
- clear() {
157
- this.capturedRequests = [];
158
- this.madeRequests = [];
159
- this.failedRequests = [];
160
- }
161
183
  }
162
184
  export function isFailedRequest(request) {
163
185
  return request.status >= 400 || Boolean(request.error);
164
186
  }
165
- function normalizePathPattern(urlPath) {
166
- return urlPath
167
- .split('/')
168
- .map((segment) => (segment && isDynamicSegment(segment) ? '{id}' : segment))
169
- .join('/');
187
+ function readEndpointKey(result) {
188
+ return `${result.method} ${generalizeUrl(result.path, () => '{id}')}?${queryParamNames(result).join(',')}`;
189
+ }
190
+ function matchesFamily(result, methods) {
191
+ if (methods === 'write')
192
+ return result.isWrite;
193
+ return result.method === 'GET';
194
+ }
195
+ function queryParamHint(result) {
196
+ if (result.isWrite)
197
+ return '';
198
+ const names = queryParamNames(result);
199
+ if (names.length === 0)
200
+ return '';
201
+ return ` ?${names.join(',')}`;
202
+ }
203
+ function queryParamNames(result) {
204
+ const query = result.fullUrl.split('?')[1];
205
+ if (!query)
206
+ return [];
207
+ return [...new Set(new URLSearchParams(query).keys())].sort();
170
208
  }
@@ -7,4 +7,6 @@ export declare class XhrCapture {
7
7
  attach(page: any): void;
8
8
  detach(page: any): void;
9
9
  captureResponse(response: any): Promise<void>;
10
+ captureReadEndpoint(request: any, response: any): void;
11
+ toHeaderMap(headers: Record<string, unknown>): Record<string, string>;
10
12
  }
@@ -52,24 +52,24 @@ export class XhrCapture {
52
52
  });
53
53
  this.store.addFailedRequest(failure);
54
54
  }
55
- if (!WRITE_METHODS.has(method))
56
- return;
57
55
  const contentType = response.headers()['content-type'] || '';
58
56
  if (!JSON_CONTENT_TYPES.test(contentType))
59
57
  return;
58
+ if (method === 'GET') {
59
+ if (status !== 200)
60
+ return;
61
+ this.captureReadEndpoint(request, response);
62
+ return;
63
+ }
64
+ if (!WRITE_METHODS.has(method))
65
+ return;
60
66
  if (status === 304)
61
67
  return;
62
68
  const parsedUrl = new URL(url);
63
69
  const origin = parsedUrl.pathname + parsedUrl.search;
64
70
  const id = generateRequestId(method, parsedUrl.pathname, 'xhr_');
65
- const requestHeaders = {};
66
- for (const [k, v] of Object.entries(request.headers())) {
67
- requestHeaders[k] = String(v);
68
- }
69
- const responseHeaders = {};
70
- for (const [k, v] of Object.entries(response.headers())) {
71
- responseHeaders[k] = String(v);
72
- }
71
+ const requestHeaders = this.toHeaderMap(request.headers());
72
+ const responseHeaders = this.toHeaderMap(response.headers());
73
73
  let rawBody = '';
74
74
  try {
75
75
  rawBody = await response.text();
@@ -103,4 +103,29 @@ export class XhrCapture {
103
103
  result.rawResponseBodyValue = rawBody;
104
104
  this.store.addCapturedRequest(result);
105
105
  }
106
+ captureReadEndpoint(request, response) {
107
+ const parsedUrl = new URL(request.url());
108
+ const requestHeaders = this.toHeaderMap(request.headers());
109
+ const result = new RequestResult({
110
+ id: generateRequestId('GET', parsedUrl.pathname, 'xhr_'),
111
+ method: 'GET',
112
+ path: parsedUrl.pathname,
113
+ fullUrl: parsedUrl.pathname + parsedUrl.search,
114
+ requestHeaders,
115
+ status: response.status(),
116
+ statusText: response.statusText(),
117
+ responseHeaders: {},
118
+ timing: 0,
119
+ timestamp: new Date(),
120
+ });
121
+ result.rawResponseBodyValue = '';
122
+ this.store.addReadRequest(result);
123
+ }
124
+ toHeaderMap(headers) {
125
+ const map = {};
126
+ for (const [k, v] of Object.entries(headers)) {
127
+ map[k] = String(v);
128
+ }
129
+ return map;
130
+ }
106
131
  }
@@ -39,8 +39,12 @@ export class ConfigCommand extends BaseCommand {
39
39
  const env = {};
40
40
  for (const variable of EXPLORBOT_ENV_VARS) {
41
41
  const value = process.env[variable.name];
42
- if (value)
43
- env[variable.name] = value;
42
+ if (!value)
43
+ continue;
44
+ let shown = value;
45
+ if (variable.secret)
46
+ shown = 'set';
47
+ env[variable.name] = shown;
44
48
  }
45
49
  const models = {};
46
50
  const providers = {};
@@ -0,0 +1,31 @@
1
+ import type { Command } from 'commander';
2
+ export declare class HelpJsonCommand {
3
+ static register(program: Command): void;
4
+ static data(cmd: Command, root?: boolean): CommandDefinition;
5
+ static find(cmd: Command, names: string[]): Command | undefined;
6
+ }
7
+ interface CommandDefinition {
8
+ name: string;
9
+ description: string;
10
+ aliases: string[];
11
+ version?: string;
12
+ arguments: {
13
+ name: string;
14
+ description: string;
15
+ required: boolean;
16
+ variadic: boolean;
17
+ }[];
18
+ options: {
19
+ flags: string;
20
+ description: string;
21
+ default?: unknown;
22
+ choices?: string[];
23
+ }[];
24
+ commands: CommandDefinition[];
25
+ env?: {
26
+ name: string;
27
+ description: string;
28
+ required: boolean;
29
+ }[];
30
+ }
31
+ export {};
@@ -0,0 +1,58 @@
1
+ import { EXPLORBOT_ENV_VARS } from '../config.js';
2
+ const DESCRIPTION = 'Print command definitions as JSON for agents and tools';
3
+ export class HelpJsonCommand {
4
+ static register(program) {
5
+ program
6
+ .command('help-json [command...]')
7
+ .description(DESCRIPTION)
8
+ .action(async (names) => {
9
+ const target = HelpJsonCommand.find(program, names);
10
+ if (!target) {
11
+ console.error(`Unknown command: ${names.join(' ')}`);
12
+ process.exit(1);
13
+ }
14
+ const json = JSON.stringify(HelpJsonCommand.data(target, target === program), null, 2);
15
+ await new Promise((resolve) => process.stdout.write(`${json}\n`, () => resolve()));
16
+ });
17
+ }
18
+ static data(cmd, root = false) {
19
+ const helper = cmd.createHelp();
20
+ const definition = {
21
+ name: cmd.name(),
22
+ description: cmd.description(),
23
+ aliases: cmd.aliases(),
24
+ arguments: cmd.registeredArguments.map((argument) => ({
25
+ name: argument.name(),
26
+ description: argument.description,
27
+ required: argument.required,
28
+ variadic: argument.variadic,
29
+ })),
30
+ options: helper.visibleOptions(cmd).map((option) => ({
31
+ flags: option.flags,
32
+ description: option.description,
33
+ default: option.defaultValue,
34
+ choices: option.argChoices,
35
+ })),
36
+ commands: helper.visibleCommands(cmd).map((sub) => HelpJsonCommand.data(sub)),
37
+ };
38
+ if (!root)
39
+ return definition;
40
+ definition.version = cmd.version();
41
+ definition.env = EXPLORBOT_ENV_VARS.map((variable) => ({
42
+ name: variable.name,
43
+ description: variable.description,
44
+ required: !!variable.required,
45
+ }));
46
+ return definition;
47
+ }
48
+ static find(cmd, names) {
49
+ let target = cmd;
50
+ for (const name of names) {
51
+ const sub = target.commands.find((candidate) => candidate.name() === name || candidate.aliases().includes(name));
52
+ if (!sub)
53
+ return undefined;
54
+ target = sub;
55
+ }
56
+ return target;
57
+ }
58
+ }
@@ -269,7 +269,9 @@ export declare class ConfigParser {
269
269
  deepMerge(target: any, source: any): any;
270
270
  ensureDirectory(path: string): void;
271
271
  }
272
+ export declare function setOutputDir(dir: string): void;
272
273
  export declare function outputPath(...segments: string[]): string;
274
+ export declare function agentSettings<K extends keyof AgentsConfig>(config: ExplorbotConfig, agent: K): NonNullable<AgentsConfig[K]>;
273
275
  export declare function resolveModel(spec: string, role?: ModelRole): Promise<any>;
274
276
  export declare function missingModelRoles(provider: string): ModelRole[];
275
277
  export declare class ConfigMissingError extends Error {
@@ -297,5 +299,6 @@ interface EnvVar {
297
299
  name: string;
298
300
  description: string;
299
301
  required?: boolean;
302
+ secret?: boolean;
300
303
  }
301
304
  export type { ModelRole, EnvVar, ProviderInfo, ConfiguredModel };
@@ -27,6 +27,7 @@ export const PROVIDERS = {
27
27
  };
28
28
  export const MODEL_ROLES = ['model', 'visionModel', 'agenticModel'];
29
29
  let cachedOutputRoot = null;
30
+ let runOutputDir = null;
30
31
  const config = {
31
32
  playwright: {
32
33
  browser: 'chromium',
@@ -49,6 +50,7 @@ export const EXPLORBOT_ENV_VARS = [
49
50
  { name: 'EXPLORBOT_KNOWLEDGE_FILE', description: 'Path to a knowledge markdown file' },
50
51
  { name: 'EXPLORBOT_SPEC', description: 'Docbot application spec directory or index.md, used as page knowledge' },
51
52
  { name: 'EXPLORBOT_API_SPEC', description: 'OpenAPI spec path for the API boat' },
53
+ { name: 'EXPLORBOT_API_HEADERS', description: 'Headers sent with every API request, one "Name: value" per line', secret: true },
52
54
  { name: 'EXPLORBOT_NO_BANNER', description: 'Suppress the startup banner, for machine-readable output' },
53
55
  { name: 'EXPLORBOT_MAX_DURATION', description: 'Wall-clock budget in minutes for an explore run; same as --max-duration' },
54
56
  ];
@@ -213,6 +215,7 @@ export class ConfigParser {
213
215
  // For testing purposes only
214
216
  static resetForTesting() {
215
217
  cachedOutputRoot = null;
218
+ runOutputDir = null;
216
219
  if (ConfigParser.instance) {
217
220
  ConfigParser.instance.config = null;
218
221
  ConfigParser.instance.configPath = null;
@@ -441,9 +444,20 @@ export class ConfigParser {
441
444
  }
442
445
  }
443
446
  }
447
+ export function setOutputDir(dir) {
448
+ runOutputDir = dir;
449
+ }
444
450
  export function outputPath(...segments) {
451
+ if (runOutputDir)
452
+ return path.join(runOutputDir, ...segments);
445
453
  return path.join(ConfigParser.getInstance().getOutputDir(), ...segments);
446
454
  }
455
+ export function agentSettings(config, agent) {
456
+ const ai = (config.ai ??= { model: null });
457
+ const agents = (ai.agents ??= {});
458
+ agents[agent] ??= {};
459
+ return agents[agent];
460
+ }
447
461
  export async function resolveModel(spec, role = 'model') {
448
462
  const separator = spec.indexOf('/');
449
463
  if (separator > 0) {
@@ -60,12 +60,16 @@ export class StateManager {
60
60
  updateState(actionResult, codeBlock, trigger = 'manual') {
61
61
  const previousState = this.currentState;
62
62
  const previousHash = previousState?.hash;
63
+ const hashChanged = actionResult.hash !== previousHash;
64
+ if (!hashChanged && previousState?.verifications) {
65
+ const stillTrue = Object.entries(previousState.verifications).filter(([, passed]) => passed);
66
+ actionResult.verifications = { ...Object.fromEntries(stillTrue), ...actionResult.verifications };
67
+ }
63
68
  const newState = actionResult;
64
69
  this.currentState = newState;
65
70
  this.currentState.id = this.nextStateId++;
66
71
  if (newState.url)
67
72
  this.allVisitedUrls.add(normalizeUrl(newState.url));
68
- const hashChanged = actionResult.hash !== previousHash;
69
73
  const regionOpened = !hashChanged && this.regionOpened(previousState, newState);
70
74
  if (hashChanged || regionOpened) {
71
75
  const transition = {
@@ -30,7 +30,7 @@ export default {
30
30
 
31
31
  - **`baseEndpoint`** (required) — the base URL prepended to every request. Test steps use relative paths like `/users`; Curler adds the base for you.
32
32
  - **`spec`** (required) — one or more OpenAPI specs, given as HTTP(S) URLs or local file paths, in YAML or JSON. Chief uses the spec to plan; Curler uses it to look up schemas. Both agents refuse to run without one.
33
- - **`headers`** — sent with every request. This is where API keys and auth tokens go.
33
+ - **`headers`** — sent with every request. This is where API keys and auth tokens go. `-H "Name: value"` on the command line and `EXPLORBOT_API_HEADERS` add to them without a config file.
34
34
 
35
35
  See the [full configuration reference](../reference/configuration.md) for every option and [providers](../basics/providers.md) for choosing an AI model.
36
36
 
@@ -60,16 +60,24 @@ A matching `teardown` hook runs after all tests finish — use it to clean up da
60
60
 
61
61
  Chief and Curler need three things: where the API is, what its spec says, and how to authenticate. Pass all three on the command line and no config file is needed:
62
62
 
63
+ ```bash
64
+ npx explorbot api explore https://api.example.com/v1 \
65
+ --spec ./openapi.yaml \
66
+ -H "Authorization: Bearer $TOKEN"
67
+ ```
68
+
69
+ `api explore` takes the base endpoint as its argument, so one line covers the whole run: it plans in every style, executes each plan, and reports the totals. The other commands take a path within the API and read the base from `--endpoint`:
70
+
63
71
  ```bash
64
72
  npx explorbot api plan /users \
65
73
  --endpoint https://api.example.com/v1 \
66
74
  --spec ./openapi.yaml \
67
- --knowledge 'Send X-Api-Key: ${env.API_KEY} on every request'
75
+ -H "Authorization: Bearer $TOKEN"
68
76
  ```
69
77
 
70
- `--endpoint` and `--spec` each have an environment twin — `EXPLORBOT_URL` and `EXPLORBOT_API_SPEC` — and the flag wins when both are set. `--knowledge` adds to the facts `EXPLORBOT_KNOWLEDGE` and `EXPLORBOT_KNOWLEDGE_FILE` bring in rather than replacing them. Configure your models once with `npx explorbot init --global` and every run stores its plans and requests per host under `~/.explorbot/sites/<host>/`, so a later `api test` against the same API picks up where the last one left off. Knowledge given on the command line lasts for the run; `api know` is what writes it down.
78
+ Each flag has an environment twin — `EXPLORBOT_URL`, `EXPLORBOT_API_SPEC` and `EXPLORBOT_API_HEADERS` — and the flag wins when both are set. `-H` is repeatable and takes one `Name: value` per use; the variable takes one per line. Headers land on every request, the startup health check included, and merge over any `headers` a config file sets. `--knowledge` adds to the facts `EXPLORBOT_KNOWLEDGE` and `EXPLORBOT_KNOWLEDGE_FILE` bring in rather than replacing them. Configure your models once with `npx explorbot init --global` and every run stores its plans and requests per host under `~/.explorbot/sites/<host>/`, so a later `api test` against the same API picks up where the last one left off. Knowledge given on the command line lasts for the run; `api know` is what writes it down.
71
79
 
72
- `--endpoint` keeps its path prefix: given `https://api.example.com/v1`, steps stay relative (`/users`) and Curler sends them to `https://api.example.com/v1/users`. `api test`, which takes a plan file rather than an endpoint, reads it from the flag or the variable.
80
+ The base endpoint keeps its path prefix: given `https://api.example.com/v1`, steps stay relative (`/users`) and Curler sends them to `https://api.example.com/v1/users`. `api test`, which takes a plan file rather than an endpoint, reads the base from the flag or the variable.
73
81
 
74
82
  ### A dedicated API project
75
83
 
@@ -53,6 +53,7 @@ Inside the TUI, use the matching slash command: `/explore`, `/research`, `/plan`
53
53
  | List registered sites | `npx explorbot sites` | — | Sites stored in the global installation |
54
54
  | Show resolved configuration | `npx explorbot config [url] [--json]` | `/config` | Models, config file, paths and `EXPLORBOT_*` in effect |
55
55
  | Show recommended models | `npx explorbot recommended-models [--json]` | `/recommended-models` | Models this version recommends per provider |
56
+ | Describe commands | `npx explorbot help-json [command...]` | — | The command tree as JSON: arguments, options, defaults |
56
57
  | Clean generated files | `npx explorbot clean [target]` | `/clean [target]` | Same targets both ways |
57
58
 
58
59
  ## Common CLI Options
@@ -117,6 +118,7 @@ EXPLORBOT_AI_PROVIDER=openrouter \
117
118
  | `EXPLORBOT_KNOWLEDGE_FILE` | Path to a knowledge markdown file |
118
119
  | `EXPLORBOT_SPEC` | Docbot application spec directory or index.md, used as page knowledge |
119
120
  | `EXPLORBOT_API_SPEC` | OpenAPI spec path for the API boat |
121
+ | `EXPLORBOT_API_HEADERS` | Headers sent with every API request, one "Name: value" per line |
120
122
  | `EXPLORBOT_NO_BANNER` | Suppress the startup banner, for machine-readable output |
121
123
  | `EXPLORBOT_MAX_DURATION` | Wall-clock budget in minutes for an explore run; same as --max-duration |
122
124
  <!-- END env -->
@@ -253,6 +253,10 @@ With `'playwright'`, runs are saved as `@playwright/test` `.spec.ts` files using
253
253
 
254
254
  See [AI providers](../basics/providers.md) for recommended models and provider setup.
255
255
 
256
+ ### Fisherman agent
257
+
258
+ Fisherman prepares test data over the API before a scenario runs, and can also answer questions about data that already exists without creating or changing anything. Pilot reaches this read-only capability through its `askApi(question)` tool, calling it to check whether suitable data is already there — or to get the exact name or id of an existing record — before deciding whether to create anything through `precondition()`. In replicate mode, where Fisherman learns the API by watching browser traffic instead of reading a spec, the read endpoints it can query come from successful GET requests observed in the browser, alongside the write endpoints already captured from XHR traffic. The endpoint list shown to the model names only the path and its query-parameter names, never their values; the underlying capture on disk holds the full request URL and headers — what write captures already hold — but no response body.
259
+
256
260
  ## Playwright settings
257
261
 
258
262
  ### Browser selection