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.
- package/bin/explorbot-cli.ts +6 -1
- package/boat/api-tester/src/apibot.ts +8 -13
- package/boat/api-tester/src/cli.ts +7 -3
- package/boat/api-tester/src/config.ts +45 -9
- package/boat/prima/src/cli.ts +33 -99
- package/boat/prima/src/envelope.ts +3 -1
- package/boat/prima/src/help.ts +72 -0
- package/boat/prima/src/prima.ts +33 -43
- package/dist/bin/explorbot-cli.js +5 -1
- package/dist/boat/api-tester/src/apibot.js +7 -6
- package/dist/boat/api-tester/src/cli.js +9 -3
- package/dist/boat/api-tester/src/config.js +32 -6
- package/dist/boat/prima/src/cli.js +30 -86
- package/dist/boat/prima/src/envelope.js +2 -1
- package/dist/boat/prima/src/help.js +63 -0
- package/dist/boat/prima/src/prima.js +29 -41
- package/dist/package.json +1 -1
- package/dist/src/action-result.d.ts +3 -0
- package/dist/src/action-result.js +5 -0
- package/dist/src/action.js +12 -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/researcher/deep-analysis.d.ts +1 -1
- package/dist/src/ai/researcher/deep-analysis.js +4 -1
- package/dist/src/ai/researcher/sections.d.ts +1 -1
- package/dist/src/ai/researcher/sections.js +2 -1
- package/dist/src/ai/researcher.js +25 -11
- package/dist/src/ai/rules.js +2 -0
- package/dist/src/ai/tester.d.ts +1 -0
- package/dist/src/ai/tester.js +27 -33
- package/dist/src/ai/tools.js +5 -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/config-command.js +6 -2
- package/dist/src/commands/help-json-command.d.ts +31 -0
- package/dist/src/commands/help-json-command.js +58 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +14 -0
- package/dist/src/state-manager.js +5 -1
- package/docs/api-testing/basics.md +12 -4
- package/docs/reference/commands.md +2 -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 +15 -1
- package/package.json +1 -1
- package/src/action-result.ts +7 -0
- package/src/action.ts +14 -2
- 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/researcher/deep-analysis.ts +4 -2
- package/src/ai/researcher/sections.ts +2 -2
- package/src/ai/researcher.ts +28 -11
- package/src/ai/rules.ts +2 -0
- package/src/ai/tester.ts +25 -30
- package/src/ai/tools.ts +6 -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/config-command.ts +4 -1
- package/src/commands/help-json-command.ts +74 -0
- package/src/config.ts +16 -0
- package/src/state-manager.ts +6 -1
- 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
|
}
|
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/src/config.d.ts
CHANGED
|
@@ -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 };
|
package/dist/src/config.js
CHANGED
|
@@ -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
|
-
|
|
75
|
+
-H "Authorization: Bearer $TOKEN"
|
|
68
76
|
```
|
|
69
77
|
|
|
70
|
-
|
|
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
|
-
|
|
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
|