explorbot 0.4.1 → 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 +14 -4
- package/boat/api-tester/src/ai/curler-tools.ts +1 -1
- package/boat/api-tester/src/cli.ts +3 -0
- package/boat/doc-collector/src/ai/documentarian.ts +57 -5
- package/boat/doc-collector/src/ai/tools.ts +1 -1
- package/boat/doc-collector/src/cli.ts +18 -2
- package/boat/doc-collector/src/config.ts +3 -0
- package/boat/doc-collector/src/docbot.ts +56 -11
- package/boat/doc-collector/src/docs-renderer.ts +79 -55
- package/boat/doc-collector/src/interaction-screenshots.ts +19 -10
- package/boat/doc-collector/src/screenshots.ts +106 -3
- package/boat/doc-collector/src/template-dedup.ts +31 -0
- package/boat/prima/src/cli.ts +3 -0
- package/boat/prima/src/prima.ts +3 -4
- package/dist/bin/explorbot-cli.js +14 -4
- package/dist/boat/api-tester/src/ai/curler-tools.js +1 -1
- package/dist/boat/api-tester/src/cli.js +2 -0
- package/dist/boat/doc-collector/src/ai/documentarian.js +62 -4
- package/dist/boat/doc-collector/src/ai/tools.js +1 -1
- package/dist/boat/doc-collector/src/cli.js +15 -2
- package/dist/boat/doc-collector/src/config.js +1 -0
- package/dist/boat/doc-collector/src/docbot.js +52 -10
- package/dist/boat/doc-collector/src/docs-renderer.js +69 -46
- package/dist/boat/doc-collector/src/interaction-screenshots.js +18 -11
- package/dist/boat/doc-collector/src/screenshots.js +104 -1
- package/dist/boat/doc-collector/src/template-dedup.js +27 -0
- package/dist/boat/prima/src/cli.js +2 -0
- package/dist/boat/prima/src/prima.js +3 -4
- package/dist/package.json +1 -1
- package/dist/src/action-result.d.ts +2 -2
- package/dist/src/action-result.js +15 -17
- package/dist/src/action.d.ts +1 -1
- package/dist/src/action.js +15 -15
- package/dist/src/ai/fisherman/request-haul.d.ts +12 -0
- package/dist/src/ai/fisherman/request-haul.js +31 -0
- package/dist/src/ai/fisherman/tools.d.ts +50 -0
- package/dist/src/ai/{fisherman-tools.js → fisherman/tools.js} +109 -40
- package/dist/src/ai/fisherman.d.ts +14 -4
- package/dist/src/ai/fisherman.js +96 -18
- package/dist/src/ai/pilot.d.ts +13 -1
- package/dist/src/ai/pilot.js +40 -22
- package/dist/src/ai/provider.js +8 -14
- package/dist/src/ai/researcher/coordinates.d.ts +2 -0
- package/dist/src/ai/researcher/coordinates.js +13 -1
- package/dist/src/ai/researcher/deep-analysis.js +9 -9
- package/dist/src/ai/researcher/fingerprint-worker.js +1 -11
- package/dist/src/ai/researcher.js +1 -1
- package/dist/src/ai/rules.js +10 -0
- package/dist/src/ai/tester.js +20 -20
- package/dist/src/ai/tools.d.ts +1 -1
- package/dist/src/ai/tools.js +18 -74
- package/dist/src/api/request-result.js +3 -1
- package/dist/src/api/request-store.d.ts +7 -6
- package/dist/src/api/request-store.js +65 -37
- 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 +2 -9
- package/dist/src/commands/help-json-command.d.ts +31 -0
- package/dist/src/commands/help-json-command.js +58 -0
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/init-command.js +3 -7
- package/dist/src/commands/plans-command.d.ts +4 -1
- package/dist/src/commands/plans-command.js +4 -4
- package/dist/src/commands/recommended-models-command.d.ts +9 -0
- package/dist/src/commands/recommended-models-command.js +85 -0
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +7 -2
- package/dist/src/experience-tracker.js +1 -1
- package/dist/src/explorbot.js +3 -3
- package/dist/src/global-config.d.ts +2 -0
- package/dist/src/global-config.js +6 -0
- package/dist/src/state-manager.d.ts +3 -3
- package/dist/src/state-manager.js +8 -8
- package/dist/src/test-plan.d.ts +1 -0
- package/dist/src/test-plan.js +13 -9
- package/dist/src/utils/aria.d.ts +1 -1
- package/dist/src/utils/aria.js +23 -2
- package/dist/src/utils/cli-section.d.ts +1 -0
- package/dist/src/utils/cli-section.js +12 -0
- package/dist/src/utils/html-diff.d.ts +1 -0
- package/dist/src/utils/html-diff.js +5 -5
- package/dist/src/utils/html.d.ts +2 -0
- package/dist/src/utils/html.js +5 -4
- package/dist/src/utils/overlay.d.ts +19 -55
- package/dist/src/utils/overlay.js +94 -148
- package/dist/src/utils/region.d.ts +41 -0
- package/dist/src/utils/region.js +99 -0
- package/dist/src/utils/similarity.d.ts +1 -0
- package/dist/src/utils/similarity.js +11 -0
- package/dist/src/utils/url-matcher.d.ts +1 -1
- package/dist/src/utils/url-matcher.js +4 -4
- package/docs/doc-collection/basics.md +11 -5
- package/docs/doc-collection/crawling.md +21 -1
- package/docs/reference/commands.md +4 -0
- package/docs/reference/configuration.md +4 -0
- package/docs/superpowers/plans/2026-09-03-fisherman-query-api.md +1361 -0
- package/docs/superpowers/plans/2026-09-03-model-resolves-click-ambiguity.md +509 -0
- package/docs/workflow/agentic-usage.md +12 -0
- package/docs/workflow/application-spec.md +4 -0
- package/package.json +1 -1
- package/src/action-result.ts +15 -16
- package/src/action.ts +16 -15
- package/src/ai/fisherman/request-haul.ts +36 -0
- package/src/ai/{fisherman-tools.ts → fisherman/tools.ts} +125 -46
- package/src/ai/fisherman.ts +110 -20
- package/src/ai/pilot.ts +38 -20
- package/src/ai/provider.ts +8 -12
- package/src/ai/researcher/coordinates.ts +13 -1
- package/src/ai/researcher/deep-analysis.ts +9 -9
- package/src/ai/researcher/fingerprint-worker.ts +1 -10
- package/src/ai/researcher.ts +1 -1
- package/src/ai/rules.ts +10 -0
- package/src/ai/tester.ts +18 -18
- package/src/ai/tools.ts +18 -86
- package/src/api/request-result.ts +2 -1
- package/src/api/request-store.ts +68 -42
- package/src/api/xhr-capture.ts +39 -11
- package/src/commands/config-command.ts +2 -7
- package/src/commands/help-json-command.ts +74 -0
- package/src/commands/index.ts +2 -0
- package/src/commands/init-command.ts +4 -11
- package/src/commands/plans-command.ts +5 -5
- package/src/commands/recommended-models-command.ts +92 -0
- package/src/config.ts +9 -2
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +3 -3
- package/src/global-config.ts +8 -0
- package/src/state-manager.ts +10 -9
- package/src/test-plan.ts +12 -10
- package/src/utils/aria.ts +26 -3
- package/src/utils/cli-section.ts +12 -0
- package/src/utils/html-diff.ts +6 -6
- package/src/utils/html.ts +6 -4
- package/src/utils/overlay.ts +105 -192
- package/src/utils/region.ts +132 -0
- package/src/utils/similarity.ts +9 -0
- package/src/utils/url-matcher.ts +3 -3
- package/dist/src/ai/fisherman-tools.d.ts +0 -138
- package/dist/src/utils/request-map.d.ts +0 -7
- package/dist/src/utils/request-map.js +0 -16
- package/src/utils/request-map.ts +0 -19
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { isDynamicSegment } from "../utils/url-matcher.js";
|
|
3
|
+
import { generalizeUrl, isDynamicSegment } from "../utils/url-matcher.js";
|
|
4
4
|
import { RequestResult } from "./request-result.js";
|
|
5
5
|
const AUTH_HEADERS = ['authorization', 'x-api-key', 'x-csrf-token'];
|
|
6
6
|
export class RequestStore {
|
|
@@ -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) {
|
|
@@ -38,39 +47,23 @@ export class RequestStore {
|
|
|
38
47
|
this.madeRequests.push(result);
|
|
39
48
|
result.save(this.outputDir);
|
|
40
49
|
}
|
|
41
|
-
addRequest(result) {
|
|
42
|
-
this.addMadeRequest(result);
|
|
43
|
-
}
|
|
44
50
|
getCapturedRequests() {
|
|
45
51
|
return this.capturedRequests;
|
|
46
52
|
}
|
|
47
53
|
getMadeRequests() {
|
|
48
54
|
return this.madeRequests;
|
|
49
55
|
}
|
|
50
|
-
getRequests() {
|
|
51
|
-
return this.madeRequests;
|
|
52
|
-
}
|
|
53
56
|
getLastRequest() {
|
|
54
57
|
return this.madeRequests[this.madeRequests.length - 1];
|
|
55
58
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
getRequestsByMethod(method) {
|
|
60
|
-
const upper = method.toUpperCase();
|
|
61
|
-
return this.madeRequests.filter((r) => r.method === upper);
|
|
62
|
-
}
|
|
63
|
-
getRequestsByStatus(status) {
|
|
64
|
-
return this.madeRequests.filter((r) => r.status === status);
|
|
65
|
-
}
|
|
66
|
-
toEndpointList(scopePath) {
|
|
67
|
-
let requests = this.capturedRequests;
|
|
59
|
+
toEndpointList(scopePath, methods = 'write') {
|
|
60
|
+
let requests = this.capturedRequests.filter((r) => matchesFamily(r, methods));
|
|
68
61
|
if (scopePath)
|
|
69
|
-
requests = this.
|
|
62
|
+
requests = this.getRequestsForScope(scopePath, methods);
|
|
70
63
|
const seen = new Set();
|
|
71
64
|
const lines = [];
|
|
72
65
|
for (const req of requests) {
|
|
73
|
-
const key = `${req.method} ${
|
|
66
|
+
const key = `${req.method} ${generalizeUrl(req.path, () => '{id}')}${queryParamHint(req)}`;
|
|
74
67
|
if (seen.has(key))
|
|
75
68
|
continue;
|
|
76
69
|
seen.add(key);
|
|
@@ -92,13 +85,17 @@ export class RequestStore {
|
|
|
92
85
|
}
|
|
93
86
|
findCapturedRequest(method, searchPath) {
|
|
94
87
|
const upper = method.toUpperCase();
|
|
95
|
-
const search =
|
|
88
|
+
const search = generalizeUrl(searchPath, () => '{id}')
|
|
89
|
+
.split('/')
|
|
90
|
+
.filter(Boolean);
|
|
96
91
|
let best;
|
|
97
92
|
let bestScore = -1;
|
|
98
93
|
for (const req of this.capturedRequests) {
|
|
99
94
|
if (req.method !== upper)
|
|
100
95
|
continue;
|
|
101
|
-
const segments =
|
|
96
|
+
const segments = generalizeUrl(req.path, () => '{id}')
|
|
97
|
+
.split('/')
|
|
98
|
+
.filter(Boolean);
|
|
102
99
|
if (segments.length < search.length)
|
|
103
100
|
continue;
|
|
104
101
|
if (!search.every((segment, i) => segment === segments[i]))
|
|
@@ -131,6 +128,12 @@ export class RequestStore {
|
|
|
131
128
|
const result = RequestResult.load(path.join(requestsDir, file));
|
|
132
129
|
if (existingIds.has(result.id))
|
|
133
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
|
+
}
|
|
134
137
|
this.capturedRequests.push(result);
|
|
135
138
|
}
|
|
136
139
|
catch {
|
|
@@ -139,18 +142,29 @@ export class RequestStore {
|
|
|
139
142
|
}
|
|
140
143
|
}
|
|
141
144
|
getWriteRequestsForScope(scopePath) {
|
|
142
|
-
|
|
143
|
-
|
|
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));
|
|
144
158
|
const scopeSegments = scopePath.split('/').filter(Boolean);
|
|
145
159
|
if (scopeSegments.length === 0)
|
|
146
|
-
return
|
|
160
|
+
return candidates;
|
|
147
161
|
let scoped = [];
|
|
148
162
|
let fewest = Number.POSITIVE_INFINITY;
|
|
149
163
|
let ambiguous = false;
|
|
150
164
|
for (const segment of scopeSegments) {
|
|
151
165
|
if (isDynamicSegment(segment))
|
|
152
166
|
continue;
|
|
153
|
-
const matches =
|
|
167
|
+
const matches = candidates.filter((r) => r.path.split('/').includes(segment));
|
|
154
168
|
if (matches.length === 0 || matches.length > fewest)
|
|
155
169
|
continue;
|
|
156
170
|
if (matches.length === fewest) {
|
|
@@ -166,15 +180,29 @@ export class RequestStore {
|
|
|
166
180
|
return [];
|
|
167
181
|
return scoped;
|
|
168
182
|
}
|
|
169
|
-
clear() {
|
|
170
|
-
this.capturedRequests = [];
|
|
171
|
-
this.madeRequests = [];
|
|
172
|
-
this.failedRequests = [];
|
|
173
|
-
}
|
|
174
183
|
}
|
|
175
|
-
function
|
|
176
|
-
return
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
184
|
+
export function isFailedRequest(request) {
|
|
185
|
+
return request.status >= 400 || Boolean(request.error);
|
|
186
|
+
}
|
|
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();
|
|
180
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
|
}
|
|
@@ -5,6 +5,7 @@ import { ConfigParser, EXPLORBOT_ENV_VARS, configuredModels } from '../config.js
|
|
|
5
5
|
import { listSites } from '../global-config.js';
|
|
6
6
|
import { Reporter } from '../reporter.js';
|
|
7
7
|
import { getCliName } from '../utils/cli-name.js';
|
|
8
|
+
import { renderSection } from '../utils/cli-section.js';
|
|
8
9
|
import { tag } from '../utils/logger.js';
|
|
9
10
|
import { BaseCommand } from './base-command.js';
|
|
10
11
|
export class ConfigCommand extends BaseCommand {
|
|
@@ -68,15 +69,7 @@ export class ConfigCommand extends BaseCommand {
|
|
|
68
69
|
if (options.json)
|
|
69
70
|
return JSON.stringify(data, null, 2);
|
|
70
71
|
const lines = [];
|
|
71
|
-
const section = (title, entries) =>
|
|
72
|
-
if (!entries.length)
|
|
73
|
-
return;
|
|
74
|
-
const width = Math.max(...entries.map(([label]) => label.length));
|
|
75
|
-
lines.push(chalk.bold(title));
|
|
76
|
-
for (const [label, value] of entries)
|
|
77
|
-
lines.push(` ${chalk.dim(label.padEnd(width))} ${value}`);
|
|
78
|
-
lines.push('');
|
|
79
|
-
};
|
|
72
|
+
const section = (title, entries) => lines.push(...renderSection(title, entries));
|
|
80
73
|
const general = [['config', data.config || 'EXPLORBOT_* environment variables']];
|
|
81
74
|
if (data.url)
|
|
82
75
|
general.push(['url', data.url]);
|
|
@@ -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
|
+
}
|
|
@@ -26,6 +26,7 @@ import { PlanLoadCommand } from './plan-load-command.js';
|
|
|
26
26
|
import { PlanReloadCommand } from './plan-reload-command.js';
|
|
27
27
|
import { PlanSaveCommand } from './plan-save-command.js';
|
|
28
28
|
import { PlansCommand } from './plans-command.js';
|
|
29
|
+
import { RecommendedModelsCommand } from './recommended-models-command.js';
|
|
29
30
|
import { RerunCommand } from './rerun-command.js';
|
|
30
31
|
import { ResearchCommand } from './research-command.js';
|
|
31
32
|
import { RunsCommand } from './runs-command.js';
|
|
@@ -66,6 +67,7 @@ const commandClasses = [
|
|
|
66
67
|
RerunCommand,
|
|
67
68
|
StatusCommand,
|
|
68
69
|
ConfigCommand,
|
|
70
|
+
RecommendedModelsCommand,
|
|
69
71
|
DebugCommand,
|
|
70
72
|
ExitCommand,
|
|
71
73
|
];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, extname, join, resolve } from 'node:path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import { ConfigParser, PROVIDERS } from "../config.js";
|
|
4
|
+
import { ConfigParser, PROVIDERS, missingModelRoles } from "../config.js";
|
|
5
5
|
import { findGlobalConfig, globalConfigPath, globalDir, globalEnvPath } from "../global-config.js";
|
|
6
6
|
import { getCliName } from "../utils/cli-name.js";
|
|
7
7
|
import { log, tag } from '../utils/logger.js';
|
|
@@ -82,7 +82,7 @@ export function writeGlobalConfig(provider, apiKey) {
|
|
|
82
82
|
const envKey = PROVIDERS[provider].envKey;
|
|
83
83
|
writeEnvKey(envKey, apiKey || '');
|
|
84
84
|
log(`Stored ${envKey} in ${globalEnvPath()}`);
|
|
85
|
-
const missing =
|
|
85
|
+
const missing = missingModelRoles(provider);
|
|
86
86
|
if (missing.length) {
|
|
87
87
|
tag('warning').log(`No recommended ${missing.join(' and ')} for ${provider} — set the model ids in ${globalConfigPath()}`);
|
|
88
88
|
}
|
|
@@ -139,7 +139,7 @@ export function runInitCommand(options) {
|
|
|
139
139
|
else {
|
|
140
140
|
log(`Env file already exists: ${relativeToCwd(envPath)}`);
|
|
141
141
|
}
|
|
142
|
-
const missing =
|
|
142
|
+
const missing = missingModelRoles(provider);
|
|
143
143
|
if (missing.length) {
|
|
144
144
|
tag('warning').log(`No recommended ${missing.join(' and ')} for ${provider} — set the model ids in ${relativeToCwd(outPath)}`);
|
|
145
145
|
}
|
|
@@ -269,10 +269,6 @@ function isModuleProject(configDir) {
|
|
|
269
269
|
currentDir = parentDir;
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
|
-
function missingRoles(provider) {
|
|
273
|
-
const recommended = ConfigParser.recommendedModels()[provider] || {};
|
|
274
|
-
return ['model', 'visionModel', 'agenticModel'].filter((role) => !recommended[role]);
|
|
275
|
-
}
|
|
276
272
|
function writeEnvKey(key, value) {
|
|
277
273
|
const envPath = globalEnvPath();
|
|
278
274
|
let content = '# AI provider API keys';
|
|
@@ -11,7 +11,10 @@ export declare class PlansCommand extends BaseCommand {
|
|
|
11
11
|
getPlanFiles(): PlanFile[];
|
|
12
12
|
printPlans(files: PlanFile[]): void;
|
|
13
13
|
printPlanDetails(plan: Plan, file: PlanFile): void;
|
|
14
|
-
resolvePlanFile(target: string, files: PlanFile[]):
|
|
14
|
+
resolvePlanFile(target: string, files: PlanFile[]): {
|
|
15
|
+
plan: Plan;
|
|
16
|
+
file: PlanFile;
|
|
17
|
+
};
|
|
15
18
|
}
|
|
16
19
|
interface PlanFile {
|
|
17
20
|
name: string;
|
|
@@ -17,8 +17,7 @@ export class PlansCommand extends BaseCommand {
|
|
|
17
17
|
this.printPlans(files);
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
-
const file = this.resolvePlanFile(target, files);
|
|
21
|
-
const plan = Plan.fromMarkdown(file.path);
|
|
20
|
+
const { plan, file } = this.resolvePlanFile(target, files);
|
|
22
21
|
this.printPlanDetails(plan, file);
|
|
23
22
|
}
|
|
24
23
|
getPlanFiles() {
|
|
@@ -68,16 +67,17 @@ export class PlansCommand extends BaseCommand {
|
|
|
68
67
|
const file = files[index - 1];
|
|
69
68
|
if (!file)
|
|
70
69
|
throw new Error(`Plan #${target} not found. Available: 1-${files.length}`);
|
|
71
|
-
return file;
|
|
70
|
+
return { plan: Plan.fromMarkdown(file.path), file };
|
|
72
71
|
}
|
|
73
72
|
const plan = Plan.loadFromFile(target, this.explorBot.getPlansDir());
|
|
74
73
|
if (!plan?.filePath) {
|
|
75
74
|
throw new Error(`Plan file not found: ${target}`);
|
|
76
75
|
}
|
|
77
|
-
|
|
76
|
+
const file = {
|
|
78
77
|
name: path.basename(plan.filePath),
|
|
79
78
|
path: plan.filePath,
|
|
80
79
|
modifiedAt: statSync(plan.filePath).mtimeMs,
|
|
81
80
|
};
|
|
81
|
+
return { plan, file };
|
|
82
82
|
}
|
|
83
83
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { BaseCommand } from './base-command.js';
|
|
3
|
+
export declare class RecommendedModelsCommand extends BaseCommand {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
execute(): Promise<void>;
|
|
7
|
+
static register(program: Command): void;
|
|
8
|
+
static render(json?: boolean): string;
|
|
9
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { ConfigParser, EXPLORBOT_ENV_VARS, MODEL_ROLES, PROVIDERS } from '../config.js';
|
|
3
|
+
import { getCliName } from '../utils/cli-name.js';
|
|
4
|
+
import { renderSection } from '../utils/cli-section.js';
|
|
5
|
+
import { tag } from '../utils/logger.js';
|
|
6
|
+
import { BaseCommand } from './base-command.js';
|
|
7
|
+
const DESCRIPTION = 'Show the models recommended for every AI provider';
|
|
8
|
+
const ROLES = {
|
|
9
|
+
model: { hint: 'fast', env: 'EXPLORBOT_AI_MODEL' },
|
|
10
|
+
visionModel: { hint: 'screenshots', env: 'EXPLORBOT_VISION_MODEL' },
|
|
11
|
+
agenticModel: { hint: 'smart', env: 'EXPLORBOT_AGENTIC_MODEL' },
|
|
12
|
+
};
|
|
13
|
+
const MODEL_ENV_VARS = MODEL_ROLES.map((role) => ROLES[role].env);
|
|
14
|
+
export class RecommendedModelsCommand extends BaseCommand {
|
|
15
|
+
name = 'recommended-models';
|
|
16
|
+
description = DESCRIPTION;
|
|
17
|
+
async execute() {
|
|
18
|
+
tag('info').log(RecommendedModelsCommand.render());
|
|
19
|
+
}
|
|
20
|
+
static register(program) {
|
|
21
|
+
program
|
|
22
|
+
.command('recommended-models')
|
|
23
|
+
.description(DESCRIPTION)
|
|
24
|
+
.option('--json', 'Print the recommendations as JSON')
|
|
25
|
+
.action((options) => console.log(RecommendedModelsCommand.render(options.json)));
|
|
26
|
+
}
|
|
27
|
+
static render(json = false) {
|
|
28
|
+
const recommended = ConfigParser.recommendedModels();
|
|
29
|
+
if (json)
|
|
30
|
+
return JSON.stringify(recommended, null, 2);
|
|
31
|
+
const lines = [
|
|
32
|
+
chalk.bold('Export the provider key, then pick one of two ways to select models:'),
|
|
33
|
+
` ${chalk.bold('EXPLORBOT_AI_PROVIDER')} — every role takes that provider's recommendation below`,
|
|
34
|
+
` ${chalk.bold(MODEL_ENV_VARS.join(' + '))} — no provider variable, pin every role yourself as ${chalk.dim('provider/model-id')}`,
|
|
35
|
+
'',
|
|
36
|
+
];
|
|
37
|
+
for (const [provider, roles] of Object.entries(recommended)) {
|
|
38
|
+
lines.push(chalk.bold.yellow(`${provider}:`));
|
|
39
|
+
const models = [];
|
|
40
|
+
for (const role of MODEL_ROLES) {
|
|
41
|
+
const label = `${role} (${ROLES[role].hint})`;
|
|
42
|
+
if (roles[role])
|
|
43
|
+
models.push([label, roles[role]]);
|
|
44
|
+
if (!roles[role])
|
|
45
|
+
models.push([label, chalk.dim('not served, pair with another provider')]);
|
|
46
|
+
}
|
|
47
|
+
lines.push(...renderSection('models', models, 2));
|
|
48
|
+
const envKey = PROVIDERS[provider]?.envKey;
|
|
49
|
+
if (!envKey) {
|
|
50
|
+
lines.push(` ${chalk.dim('not bundled — import the provider in your config')}`, '');
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const env = [
|
|
54
|
+
['key', envKey],
|
|
55
|
+
['provider', `EXPLORBOT_AI_PROVIDER=${provider}`],
|
|
56
|
+
];
|
|
57
|
+
let label = 'or models';
|
|
58
|
+
for (const role of MODEL_ROLES) {
|
|
59
|
+
if (!roles[role])
|
|
60
|
+
continue;
|
|
61
|
+
env.push([label, `${ROLES[role].env}=${provider}/${roles[role]}`]);
|
|
62
|
+
label = '';
|
|
63
|
+
}
|
|
64
|
+
lines.push(...renderSection('env variables', env, 2));
|
|
65
|
+
}
|
|
66
|
+
const modelVars = ['EXPLORBOT_AI_PROVIDER', ...MODEL_ENV_VARS];
|
|
67
|
+
const current = [];
|
|
68
|
+
for (const variable of EXPLORBOT_ENV_VARS) {
|
|
69
|
+
if (!modelVars.includes(variable.name))
|
|
70
|
+
continue;
|
|
71
|
+
if (process.env[variable.name])
|
|
72
|
+
current.push([variable.name, process.env[variable.name]]);
|
|
73
|
+
}
|
|
74
|
+
for (const [provider, { envKey }] of Object.entries(PROVIDERS)) {
|
|
75
|
+
if (process.env[envKey])
|
|
76
|
+
current.push([envKey, chalk.dim(`set, ${provider} usable`)]);
|
|
77
|
+
}
|
|
78
|
+
if (!current.length)
|
|
79
|
+
lines.push(chalk.dim('Nothing set — export one of the variables above, or write models into your config'), '');
|
|
80
|
+
if (current.length)
|
|
81
|
+
lines.push(...renderSection('Currently set', current));
|
|
82
|
+
lines.push(chalk.bold('Example using OpenRouter:'), ` OPENROUTER_API_KEY=sk-... EXPLORBOT_AI_PROVIDER=openrouter ${getCliName()} <command>`);
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
|
85
|
+
}
|
package/dist/src/config.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type SiteRecord } from './global-config.js';
|
|
2
2
|
export declare const PROVIDERS: Record<string, ProviderInfo>;
|
|
3
|
+
export declare const MODEL_ROLES: ModelRole[];
|
|
3
4
|
interface PlaywrightConfig {
|
|
4
5
|
browser: 'chromium' | 'firefox' | 'webkit';
|
|
5
6
|
url: string;
|
|
@@ -270,6 +271,7 @@ export declare class ConfigParser {
|
|
|
270
271
|
}
|
|
271
272
|
export declare function outputPath(...segments: string[]): string;
|
|
272
273
|
export declare function resolveModel(spec: string, role?: ModelRole): Promise<any>;
|
|
274
|
+
export declare function missingModelRoles(provider: string): ModelRole[];
|
|
273
275
|
export declare class ConfigMissingError extends Error {
|
|
274
276
|
}
|
|
275
277
|
export declare function envConfigRequested(): boolean;
|
package/dist/src/config.js
CHANGED
|
@@ -25,6 +25,7 @@ export const PROVIDERS = {
|
|
|
25
25
|
openrouter: { envKey: 'OPENROUTER_API_KEY', load: async () => (await import('@openrouter/ai-sdk-provider')).createOpenRouter() },
|
|
26
26
|
sambanova: { envKey: 'SAMBANOVA_API_KEY', load: async () => (await import('sambanova-ai-provider')).createSambaNova() },
|
|
27
27
|
};
|
|
28
|
+
export const MODEL_ROLES = ['model', 'visionModel', 'agenticModel'];
|
|
28
29
|
let cachedOutputRoot = null;
|
|
29
30
|
const config = {
|
|
30
31
|
playwright: {
|
|
@@ -458,6 +459,10 @@ export async function resolveModel(spec, role = 'model') {
|
|
|
458
459
|
}
|
|
459
460
|
return createModel(spec, modelId);
|
|
460
461
|
}
|
|
462
|
+
export function missingModelRoles(provider) {
|
|
463
|
+
const recommended = ConfigParser.recommendedModels()[provider] || {};
|
|
464
|
+
return MODEL_ROLES.filter((role) => !recommended[role]);
|
|
465
|
+
}
|
|
461
466
|
export class ConfigMissingError extends Error {
|
|
462
467
|
}
|
|
463
468
|
export function envConfigRequested() {
|
|
@@ -478,6 +483,7 @@ export function missingConfigMessage(configFile = 'explorbot.config.js') {
|
|
|
478
483
|
EXPLORBOT_AI_PROVIDER=openrouter EXPLORBOT_URL=https://your-app.example.com ${cli} ...
|
|
479
484
|
|
|
480
485
|
Providers: ${Object.keys(PROVIDERS).join(', ')}
|
|
486
|
+
See the models each one recommends: ${cli} recommended-models
|
|
481
487
|
`;
|
|
482
488
|
}
|
|
483
489
|
export function modelName(model) {
|
|
@@ -511,8 +517,7 @@ export function configuredModels(ai) {
|
|
|
511
517
|
export async function resolveConfigModels(ai) {
|
|
512
518
|
if (!ai)
|
|
513
519
|
return;
|
|
514
|
-
const
|
|
515
|
-
for (const role of roles) {
|
|
520
|
+
for (const role of MODEL_ROLES) {
|
|
516
521
|
if (typeof ai[role] === 'string')
|
|
517
522
|
ai[role] = await resolveModel(ai[role], role);
|
|
518
523
|
}
|
|
@@ -105,7 +105,7 @@ export class ExperienceTracker {
|
|
|
105
105
|
url: state.url ? extractStatePath(state.url) : '',
|
|
106
106
|
title: state.title,
|
|
107
107
|
};
|
|
108
|
-
if (state.overlay.
|
|
108
|
+
if (state.overlay.isOpen && state.overlay.name) {
|
|
109
109
|
frontmatter.region = state.overlay.name;
|
|
110
110
|
if (state.overlay.root)
|
|
111
111
|
frontmatter.root = state.overlay.root;
|
package/dist/src/explorbot.js
CHANGED
|
@@ -399,10 +399,10 @@ export class ExplorBot {
|
|
|
399
399
|
return plan;
|
|
400
400
|
}
|
|
401
401
|
loadPlans(filename) {
|
|
402
|
-
const
|
|
403
|
-
if (!
|
|
402
|
+
const filePath = Plan.resolveFile(filename, this.getPlansDir());
|
|
403
|
+
if (!filePath)
|
|
404
404
|
throw new Error(`Plan file not found: ${filename}`);
|
|
405
|
-
return parsePlansFromMarkdown(
|
|
405
|
+
return parsePlansFromMarkdown(filePath);
|
|
406
406
|
}
|
|
407
407
|
setCurrentPlan(plan) {
|
|
408
408
|
this.currentPlan = plan;
|
|
@@ -6,6 +6,8 @@ export declare function isGlobalConfigPath(configPath: string): boolean;
|
|
|
6
6
|
export declare function sitesDir(): string;
|
|
7
7
|
export declare function siteFolderName(url: string): string;
|
|
8
8
|
export declare function listSites(): SiteRecord[];
|
|
9
|
+
export declare function findSiteWith(subpath: string): SiteRecord | undefined;
|
|
10
|
+
export declare function listSitePlanDirs(): string[];
|
|
9
11
|
export declare function registerSite(baseUrl: string): SiteRecord;
|
|
10
12
|
export declare function resolveSiteTarget(target?: string, defaultBaseUrl?: string): SiteTarget;
|
|
11
13
|
interface SiteRecord {
|
|
@@ -38,6 +38,12 @@ export function listSites() {
|
|
|
38
38
|
.filter((site) => !!site)
|
|
39
39
|
.sort((a, b) => b.lastRunAt.localeCompare(a.lastRunAt));
|
|
40
40
|
}
|
|
41
|
+
export function findSiteWith(subpath) {
|
|
42
|
+
return listSites().find((site) => existsSync(join(site.dir, subpath)));
|
|
43
|
+
}
|
|
44
|
+
export function listSitePlanDirs() {
|
|
45
|
+
return listSites().map((site) => join(site.dir, 'output', 'plans'));
|
|
46
|
+
}
|
|
41
47
|
export function registerSite(baseUrl) {
|
|
42
48
|
const folder = siteFolderName(baseUrl);
|
|
43
49
|
const dir = join(sitesDir(), folder);
|