explorbot 0.4.2 → 0.4.3

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 (34) hide show
  1. package/bin/explorbot-cli.ts +6 -1
  2. package/dist/bin/explorbot-cli.js +5 -1
  3. package/dist/package.json +1 -1
  4. package/dist/src/ai/fisherman/request-haul.d.ts +1 -0
  5. package/dist/src/ai/fisherman/request-haul.js +3 -0
  6. package/dist/src/ai/fisherman/tools.d.ts +50 -0
  7. package/dist/src/ai/{fisherman-tools.js → fisherman/tools.js} +78 -13
  8. package/dist/src/ai/fisherman.d.ts +12 -3
  9. package/dist/src/ai/fisherman.js +89 -13
  10. package/dist/src/ai/pilot.d.ts +13 -1
  11. package/dist/src/ai/pilot.js +20 -7
  12. package/dist/src/ai/rules.js +2 -0
  13. package/dist/src/api/request-result.js +3 -1
  14. package/dist/src/api/request-store.d.ts +6 -1
  15. package/dist/src/api/request-store.js +55 -17
  16. package/dist/src/api/xhr-capture.d.ts +2 -0
  17. package/dist/src/api/xhr-capture.js +35 -10
  18. package/dist/src/commands/help-json-command.d.ts +31 -0
  19. package/dist/src/commands/help-json-command.js +58 -0
  20. package/docs/reference/commands.md +1 -0
  21. package/docs/reference/configuration.md +4 -0
  22. package/docs/superpowers/plans/2026-09-03-fisherman-query-api.md +1361 -0
  23. package/docs/workflow/agentic-usage.md +12 -0
  24. package/package.json +1 -1
  25. package/src/ai/fisherman/request-haul.ts +4 -0
  26. package/src/ai/{fisherman-tools.ts → fisherman/tools.ts} +93 -20
  27. package/src/ai/fisherman.ts +104 -15
  28. package/src/ai/pilot.ts +20 -7
  29. package/src/ai/rules.ts +2 -0
  30. package/src/api/request-result.ts +2 -1
  31. package/src/api/request-store.ts +58 -18
  32. package/src/api/xhr-capture.ts +39 -11
  33. package/src/commands/help-json-command.ts +74 -0
  34. 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
  }
@@ -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
+ }
@@ -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
@@ -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