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.
- package/bin/explorbot-cli.ts +6 -1
- package/dist/bin/explorbot-cli.js +5 -1
- package/dist/package.json +1 -1
- package/dist/src/ai/fisherman/request-haul.d.ts +1 -0
- package/dist/src/ai/fisherman/request-haul.js +3 -0
- package/dist/src/ai/fisherman/tools.d.ts +50 -0
- package/dist/src/ai/{fisherman-tools.js → fisherman/tools.js} +78 -13
- package/dist/src/ai/fisherman.d.ts +12 -3
- package/dist/src/ai/fisherman.js +89 -13
- package/dist/src/ai/pilot.d.ts +13 -1
- package/dist/src/ai/pilot.js +20 -7
- package/dist/src/ai/rules.js +2 -0
- package/dist/src/api/request-result.js +3 -1
- package/dist/src/api/request-store.d.ts +6 -1
- package/dist/src/api/request-store.js +55 -17
- package/dist/src/api/xhr-capture.d.ts +2 -0
- package/dist/src/api/xhr-capture.js +35 -10
- package/dist/src/commands/help-json-command.d.ts +31 -0
- package/dist/src/commands/help-json-command.js +58 -0
- package/docs/reference/commands.md +1 -0
- package/docs/reference/configuration.md +4 -0
- package/docs/superpowers/plans/2026-09-03-fisherman-query-api.md +1361 -0
- package/docs/workflow/agentic-usage.md +12 -0
- package/package.json +1 -1
- package/src/ai/fisherman/request-haul.ts +4 -0
- package/src/ai/{fisherman-tools.ts → fisherman/tools.ts} +93 -20
- package/src/ai/fisherman.ts +104 -15
- package/src/ai/pilot.ts +20 -7
- package/src/ai/rules.ts +2 -0
- package/src/api/request-result.ts +2 -1
- package/src/api/request-store.ts +58 -18
- package/src/api/xhr-capture.ts +39 -11
- package/src/commands/help-json-command.ts +74 -0
- 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.
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
166
|
-
return
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|