explorbot 0.4.5 → 0.4.7
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/boat/api-tester/src/ai/curler.ts +70 -66
- package/boat/api-tester/src/apibot.ts +19 -2
- package/boat/api-tester/src/cli.ts +87 -274
- package/boat/api-tester/src/commands/api-command.ts +10 -0
- package/boat/api-tester/src/commands/explore-command.ts +52 -0
- package/boat/api-tester/src/commands/init-command.ts +119 -0
- package/boat/api-tester/src/commands/know-command.ts +44 -0
- package/boat/api-tester/src/commands/plan-command.ts +42 -0
- package/boat/api-tester/src/commands/test-command.ts +54 -0
- package/boat/api-tester/src/config.ts +18 -1
- package/dist/boat/api-tester/src/ai/curler.js +55 -56
- package/dist/boat/api-tester/src/apibot.js +15 -1
- package/dist/boat/api-tester/src/cli.js +89 -243
- package/dist/boat/api-tester/src/commands/api-command.js +7 -0
- package/dist/boat/api-tester/src/commands/explore-command.js +41 -0
- package/dist/boat/api-tester/src/commands/init-command.js +88 -0
- package/dist/boat/api-tester/src/commands/know-command.js +39 -0
- package/dist/boat/api-tester/src/commands/plan-command.js +37 -0
- package/dist/boat/api-tester/src/commands/test-command.js +45 -0
- package/dist/boat/api-tester/src/config.js +3 -1
- package/dist/package.json +4 -4
- package/dist/rules/researcher/pagination.md +6 -0
- package/dist/src/action-result.d.ts +6 -0
- package/dist/src/action-result.js +12 -0
- package/dist/src/ai/planner.js +4 -0
- package/dist/src/ai/researcher/deep-analysis.d.ts +1 -1
- package/dist/src/ai/researcher/deep-analysis.js +14 -6
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher/pagination.d.ts +16 -0
- package/dist/src/ai/researcher/pagination.js +62 -0
- package/dist/src/ai/researcher/parser.d.ts +3 -0
- package/dist/src/ai/researcher/parser.js +22 -6
- package/dist/src/ai/researcher/sections.js +1 -1
- package/dist/src/ai/researcher.js +7 -2
- package/dist/src/ai/rules.js +16 -0
- package/dist/src/ai/scout.js +8 -2
- package/dist/src/ai/tools.d.ts +1 -1
- package/dist/src/ai/tools.js +25 -13
- package/dist/src/api/spec-reader.d.ts +1 -0
- package/dist/src/api/spec-reader.js +93 -1
- package/dist/src/commands/base-command.d.ts +3 -3
- package/dist/src/commands/init-command.d.ts +3 -0
- package/dist/src/commands/init-command.js +6 -3
- package/dist/src/commands/options/ws-option.d.ts +7 -0
- package/dist/src/commands/options/ws-option.js +14 -0
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +14 -11
- package/dist/src/explorer.d.ts +1 -1
- package/dist/src/explorer.js +1 -1
- package/dist/src/remote.d.ts +2 -0
- package/dist/src/remote.js +23 -16
- package/dist/src/utils/aria.d.ts +2 -0
- package/dist/src/utils/aria.js +6 -1
- package/dist/src/utils/html-diff.js +4 -1
- package/dist/src/utils/markdown-query.d.ts +2 -0
- package/dist/src/utils/markdown-query.js +39 -0
- package/dist/src/utils/pagination.d.ts +16 -0
- package/dist/src/utils/pagination.js +20 -0
- package/docs/api-testing/basics.md +26 -2
- package/docs/superpowers/plans/2026-09-10-pagination.md +1420 -0
- package/docs/superpowers/specs/2026-09-09-pagination-rule-design.md +345 -0
- package/package.json +4 -4
- package/rules/researcher/pagination.md +6 -0
- package/src/action-result.ts +16 -0
- package/src/ai/planner.ts +4 -0
- package/src/ai/researcher/deep-analysis.ts +13 -6
- package/src/ai/researcher/locators.ts +1 -1
- package/src/ai/researcher/pagination.ts +68 -0
- package/src/ai/researcher/parser.ts +23 -5
- package/src/ai/researcher/sections.ts +1 -1
- package/src/ai/researcher.ts +9 -3
- package/src/ai/rules.ts +16 -0
- package/src/ai/scout.ts +9 -2
- package/src/ai/tools.ts +22 -14
- package/src/api/spec-reader.ts +106 -1
- package/src/commands/base-command.ts +3 -3
- package/src/commands/init-command.ts +6 -3
- package/src/commands/options/ws-option.ts +14 -0
- package/src/config.ts +15 -11
- package/src/explorer.ts +1 -1
- package/src/remote.ts +22 -15
- package/src/utils/aria.ts +8 -1
- package/src/utils/html-diff.ts +3 -1
- package/src/utils/markdown-query.ts +39 -0
- package/src/utils/pagination.ts +36 -0
|
@@ -42,13 +42,35 @@ export function extractEndpointDefinition(schema, endpoint, baseEndpoint) {
|
|
|
42
42
|
throw new Error('OpenAPI spec has no paths defined');
|
|
43
43
|
}
|
|
44
44
|
const basePath = toBasePath(baseEndpoint);
|
|
45
|
-
const matched =
|
|
45
|
+
const matched = collectEndpointPaths(schema, basePath, endpoint);
|
|
46
46
|
if (!Object.keys(matched).length) {
|
|
47
47
|
const available = listNormalizedPaths(schema, basePath);
|
|
48
48
|
throw new Error(`Endpoint "${endpoint}" not found in spec. Available: ${available}`);
|
|
49
49
|
}
|
|
50
50
|
return safeStringify(matched);
|
|
51
51
|
}
|
|
52
|
+
export function resolveEndpoints(schema, pattern, baseEndpoint) {
|
|
53
|
+
if (!schema?.paths) {
|
|
54
|
+
throw new Error('OpenAPI spec has no paths defined');
|
|
55
|
+
}
|
|
56
|
+
const basePath = toBasePath(baseEndpoint);
|
|
57
|
+
const normalized = Object.keys(schema.paths).map((specPath) => stripBasePath(specPath, basePath));
|
|
58
|
+
const matched = normalized.filter((specPath) => matchesPattern(specPath, pattern));
|
|
59
|
+
if (!matched.length) {
|
|
60
|
+
throw new Error(`Endpoint "${pattern}" not found in spec. Available: ${listNormalizedPaths(schema, basePath)}`);
|
|
61
|
+
}
|
|
62
|
+
const roots = matched.map((specPath) => toCollection(specPath, normalized, pattern));
|
|
63
|
+
const resolved = [...new Set(roots.map((root) => fillParameters(root, pattern)))];
|
|
64
|
+
const endpoints = resolved.filter((specPath) => !specPath.includes('{'));
|
|
65
|
+
if (!endpoints.length) {
|
|
66
|
+
throw new Error(`Endpoint "${pattern}" leaves ${listParameters(resolved)} unresolved. Give the value in the endpoint or in the base endpoint.`);
|
|
67
|
+
}
|
|
68
|
+
const skipped = resolved.filter((specPath) => specPath.includes('{'));
|
|
69
|
+
if (skipped.length) {
|
|
70
|
+
tag('warning').log(`Skipped, no value for their parameters: ${skipped.join(', ')}`);
|
|
71
|
+
}
|
|
72
|
+
return endpoints;
|
|
73
|
+
}
|
|
52
74
|
export function searchEndpoints(schema, query, baseEndpoint) {
|
|
53
75
|
if (!schema?.paths)
|
|
54
76
|
return 'No endpoints available';
|
|
@@ -136,6 +158,76 @@ function stripBasePath(specPath, basePath) {
|
|
|
136
158
|
}
|
|
137
159
|
return `/${specSegments.slice(i).join('/')}`;
|
|
138
160
|
}
|
|
161
|
+
function collectEndpointPaths(schema, basePath, endpoint) {
|
|
162
|
+
const normalized = Object.keys(schema.paths).map((specPath) => stripBasePath(specPath, basePath));
|
|
163
|
+
const roots = resolveEndpoint(normalized, endpoint);
|
|
164
|
+
if (!roots.length)
|
|
165
|
+
return collectMatchingPaths(schema, basePath, (path) => matchesEndpoint(path, endpoint));
|
|
166
|
+
return collectMatchingPaths(schema, basePath, (path) => roots.some((root) => path === root || path.startsWith(`${root}/`)));
|
|
167
|
+
}
|
|
168
|
+
function resolveEndpoint(specPaths, endpoint) {
|
|
169
|
+
const wanted = toSegments(endpoint);
|
|
170
|
+
if (!wanted.length)
|
|
171
|
+
return [];
|
|
172
|
+
const matched = specPaths.filter((specPath) => {
|
|
173
|
+
const segments = toSegments(specPath);
|
|
174
|
+
if (segments.length !== wanted.length)
|
|
175
|
+
return false;
|
|
176
|
+
return segmentsMatch(segments, wanted);
|
|
177
|
+
});
|
|
178
|
+
const literals = matched.map((specPath) => toSegments(specPath).filter((segment, i) => segment === wanted[i]).length);
|
|
179
|
+
const best = Math.max(0, ...literals);
|
|
180
|
+
return matched.filter((_, i) => literals[i] === best);
|
|
181
|
+
}
|
|
182
|
+
function matchesPattern(specPath, pattern) {
|
|
183
|
+
const wanted = toSegments(pattern);
|
|
184
|
+
const segments = toSegments(specPath);
|
|
185
|
+
if (segments.length < wanted.length)
|
|
186
|
+
return false;
|
|
187
|
+
return segmentsMatch(segments, wanted);
|
|
188
|
+
}
|
|
189
|
+
function segmentsMatch(segments, wanted) {
|
|
190
|
+
return wanted.every((want, i) => want === '*' || segments[i] === want || segments[i].startsWith('{'));
|
|
191
|
+
}
|
|
192
|
+
function toCollection(specPath, specPaths, pattern) {
|
|
193
|
+
const segments = toSegments(specPath);
|
|
194
|
+
const filled = toSegments(fillParameters(specPath, pattern));
|
|
195
|
+
let deepest = segments.length;
|
|
196
|
+
const unfilled = filled.findIndex((segment) => segment.startsWith('{'));
|
|
197
|
+
if (unfilled >= 0)
|
|
198
|
+
deepest = unfilled;
|
|
199
|
+
for (let i = Math.max(1, Math.min(toSegments(pattern).length, deepest)); i <= deepest; i++) {
|
|
200
|
+
const prefix = `/${segments.slice(0, i).join('/')}`;
|
|
201
|
+
if (specPaths.includes(prefix))
|
|
202
|
+
return prefix;
|
|
203
|
+
}
|
|
204
|
+
return specPath;
|
|
205
|
+
}
|
|
206
|
+
function fillParameters(specPath, pattern) {
|
|
207
|
+
const wanted = toSegments(pattern);
|
|
208
|
+
const segments = toSegments(specPath);
|
|
209
|
+
for (let i = 0; i < wanted.length && i < segments.length; i++) {
|
|
210
|
+
if (wanted[i] === '*')
|
|
211
|
+
continue;
|
|
212
|
+
if (!segments[i].startsWith('{'))
|
|
213
|
+
continue;
|
|
214
|
+
segments[i] = wanted[i];
|
|
215
|
+
}
|
|
216
|
+
return `/${segments.join('/')}`;
|
|
217
|
+
}
|
|
218
|
+
function listParameters(specPaths) {
|
|
219
|
+
const found = new Set();
|
|
220
|
+
for (const specPath of specPaths) {
|
|
221
|
+
for (const segment of toSegments(specPath)) {
|
|
222
|
+
if (segment.startsWith('{'))
|
|
223
|
+
found.add(segment);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return [...found].join(', ');
|
|
227
|
+
}
|
|
228
|
+
function toSegments(path) {
|
|
229
|
+
return path.split('/').filter(Boolean);
|
|
230
|
+
}
|
|
139
231
|
function matchesEndpoint(specPath, endpoint) {
|
|
140
232
|
if (specPath === endpoint)
|
|
141
233
|
return true;
|
|
@@ -7,15 +7,15 @@ export interface Suggestion {
|
|
|
7
7
|
command?: string;
|
|
8
8
|
hint: string;
|
|
9
9
|
}
|
|
10
|
-
export declare abstract class BaseCommand {
|
|
10
|
+
export declare abstract class BaseCommand<T = ExplorBot> {
|
|
11
11
|
abstract name: string;
|
|
12
12
|
abstract description: string;
|
|
13
13
|
aliases: string[];
|
|
14
14
|
options: CommandOption[];
|
|
15
15
|
tuiEnabled: boolean;
|
|
16
16
|
suggestions: Suggestion[];
|
|
17
|
-
explorBot:
|
|
18
|
-
constructor(explorBot:
|
|
17
|
+
explorBot: T;
|
|
18
|
+
constructor(explorBot: T);
|
|
19
19
|
abstract execute(args: string): Promise<void>;
|
|
20
20
|
matches(commandName: string): boolean;
|
|
21
21
|
printSuggestions(): void;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { type ModelRole } from '../config.js';
|
|
2
|
+
export declare function envTemplate(provider: string): string;
|
|
1
3
|
export declare function runInit(options: InitCommandOptions): Promise<void>;
|
|
2
4
|
export declare function writeGlobalConfig(provider: string, apiKey?: string): void;
|
|
3
5
|
export declare function runInitCommand(options: InitCommandOptions): void;
|
|
6
|
+
export declare function modelLines(provider: string, only?: ModelRole[]): string;
|
|
4
7
|
type InitCommandOptions = {
|
|
5
8
|
configPath?: string;
|
|
6
9
|
force?: boolean;
|
|
@@ -37,7 +37,7 @@ ${modelLines(provider)}
|
|
|
37
37
|
${moduleExport}
|
|
38
38
|
`;
|
|
39
39
|
}
|
|
40
|
-
function envTemplate(provider) {
|
|
40
|
+
export function envTemplate(provider) {
|
|
41
41
|
const keyLines = Object.entries(PROVIDERS).map(([name, { envKey }]) => {
|
|
42
42
|
if (name === provider)
|
|
43
43
|
return `${envKey}=`;
|
|
@@ -219,14 +219,17 @@ async function renderLocalProviderWizard() {
|
|
|
219
219
|
}), { exitOnCtrlC: false, patchConsole: false });
|
|
220
220
|
});
|
|
221
221
|
}
|
|
222
|
-
function modelLines(provider) {
|
|
222
|
+
export function modelLines(provider, only) {
|
|
223
223
|
const recommended = ConfigParser.recommendedModels()[provider] || {};
|
|
224
224
|
const roles = [
|
|
225
225
|
['model', 'fast model with tool calling capabilities'],
|
|
226
226
|
['visionModel', 'vision model for screenshot analysis'],
|
|
227
227
|
['agenticModel', 'agentic model for decision making'],
|
|
228
228
|
];
|
|
229
|
-
|
|
229
|
+
let selected = roles;
|
|
230
|
+
if (only)
|
|
231
|
+
selected = roles.filter(([role]) => only.includes(role));
|
|
232
|
+
return selected.map(([role, comment]) => ` // ${comment}\n ${role}: '${provider}/${recommended[role] || '<model-id>'}',`).join('\n');
|
|
230
233
|
}
|
|
231
234
|
function globalConfigTemplate(provider) {
|
|
232
235
|
const { envKey } = PROVIDERS[provider];
|
|
@@ -3,5 +3,12 @@ import { BaseOption } from './base-option.js';
|
|
|
3
3
|
export declare class WsOption extends BaseOption {
|
|
4
4
|
flags: string;
|
|
5
5
|
description: string;
|
|
6
|
+
/**
|
|
7
|
+
* An open socket holds the event loop, and `WebSocket` has no `unref` on
|
|
8
|
+
* either runtime — so a command that ends by returning rather than through
|
|
9
|
+
* `showStatsAndExit` would never exit once it is attached. The option that
|
|
10
|
+
* opened the connection is what closes it.
|
|
11
|
+
*/
|
|
12
|
+
register(command: Command): void;
|
|
6
13
|
apply(options: Record<string, any>, command: Command): void;
|
|
7
14
|
}
|
|
@@ -3,6 +3,20 @@ import { BaseOption } from './base-option.js';
|
|
|
3
3
|
export class WsOption extends BaseOption {
|
|
4
4
|
flags = '--ws <url>';
|
|
5
5
|
description = 'Stream this run to a remote UI over WebSocket';
|
|
6
|
+
/**
|
|
7
|
+
* An open socket holds the event loop, and `WebSocket` has no `unref` on
|
|
8
|
+
* either runtime — so a command that ends by returning rather than through
|
|
9
|
+
* `showStatsAndExit` would never exit once it is attached. The option that
|
|
10
|
+
* opened the connection is what closes it.
|
|
11
|
+
*/
|
|
12
|
+
register(command) {
|
|
13
|
+
super.register(command);
|
|
14
|
+
command.hook('postAction', async () => {
|
|
15
|
+
if (!remote.isAttached())
|
|
16
|
+
return;
|
|
17
|
+
await remote.close(0);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
6
20
|
apply(options, command) {
|
|
7
21
|
const url = options.ws || process.env.EXPLORBOT_WS_URL;
|
|
8
22
|
if (!url)
|
package/dist/src/config.d.ts
CHANGED
|
@@ -287,6 +287,7 @@ export declare function modelName(model: unknown): string;
|
|
|
287
287
|
export declare function modelProvider(model: unknown): string;
|
|
288
288
|
export declare function configuredModels(ai?: AIConfig): Record<string, ConfiguredModel>;
|
|
289
289
|
export declare function resolveConfigModels(ai?: AIConfig): Promise<void>;
|
|
290
|
+
export declare function resolveLangfuse(ai?: AIConfig): void;
|
|
290
291
|
export declare function resolveOutputRoot(baseUrl?: string): string;
|
|
291
292
|
export declare function resolveStateRoot(baseUrl: string, ephemeral?: boolean): string;
|
|
292
293
|
export declare function materializeKnowledge(outputRoot: string): void;
|
package/dist/src/config.js
CHANGED
|
@@ -377,17 +377,7 @@ export class ConfigParser {
|
|
|
377
377
|
config.playwright = config.playwright || { browser: 'chromium', url: '' };
|
|
378
378
|
config.playwright.url = options.baseUrl;
|
|
379
379
|
}
|
|
380
|
-
|
|
381
|
-
const langfuse = config.ai.langfuse;
|
|
382
|
-
const publicKey = langfuse?.publicKey || process.env.LANGFUSE_PUBLIC_KEY;
|
|
383
|
-
const secretKey = langfuse?.secretKey || process.env.LANGFUSE_SECRET_KEY;
|
|
384
|
-
config.ai.langfuse = {
|
|
385
|
-
enabled: langfuse?.enabled ?? Boolean(publicKey && secretKey),
|
|
386
|
-
publicKey,
|
|
387
|
-
secretKey,
|
|
388
|
-
baseUrl: langfuse?.baseUrl || process.env.LANGFUSE_BASE_URL || process.env.LANGFUSE_HOST,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
380
|
+
resolveLangfuse(config.ai);
|
|
391
381
|
return config;
|
|
392
382
|
}
|
|
393
383
|
validateConfig(config) {
|
|
@@ -540,6 +530,19 @@ export async function resolveConfigModels(ai) {
|
|
|
540
530
|
agent.model = await resolveModel(agent.model);
|
|
541
531
|
}
|
|
542
532
|
}
|
|
533
|
+
export function resolveLangfuse(ai) {
|
|
534
|
+
if (!ai)
|
|
535
|
+
return;
|
|
536
|
+
const langfuse = ai.langfuse;
|
|
537
|
+
const publicKey = langfuse?.publicKey || process.env.LANGFUSE_PUBLIC_KEY;
|
|
538
|
+
const secretKey = langfuse?.secretKey || process.env.LANGFUSE_SECRET_KEY;
|
|
539
|
+
ai.langfuse = {
|
|
540
|
+
enabled: langfuse?.enabled ?? Boolean(publicKey && secretKey),
|
|
541
|
+
publicKey,
|
|
542
|
+
secretKey,
|
|
543
|
+
baseUrl: langfuse?.baseUrl || process.env.LANGFUSE_BASE_URL || process.env.LANGFUSE_HOST,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
543
546
|
export function resolveOutputRoot(baseUrl) {
|
|
544
547
|
if (cachedOutputRoot)
|
|
545
548
|
return cachedOutputRoot;
|
package/dist/src/explorer.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ declare class Explorer {
|
|
|
39
39
|
testPageErrorHandler: ((error: Error) => void) | null;
|
|
40
40
|
testConsoleHandler: ((message: any) => void) | null;
|
|
41
41
|
testDialogHandler: ((dialog: any) => void) | null;
|
|
42
|
-
eventDispatcher:
|
|
42
|
+
eventDispatcher: NodeJS.EventEmitter<[never]>;
|
|
43
43
|
constructor(config: ExplorbotConfig, options: ExplorerOptions | undefined, deps: ExplorerDeps);
|
|
44
44
|
get actor(): CodeceptJS.I;
|
|
45
45
|
get page(): Page | null;
|
package/dist/src/explorer.js
CHANGED
|
@@ -208,7 +208,7 @@ class Explorer {
|
|
|
208
208
|
codeceptjs.container.create(this.convertToCodeceptConfig(this.config), {});
|
|
209
209
|
}
|
|
210
210
|
convertToCodeceptConfig(config) {
|
|
211
|
-
const playwrightConfig = { ...config.playwright };
|
|
211
|
+
const playwrightConfig = { visibleLocator: true, ...config.playwright };
|
|
212
212
|
if (this.options?.show !== undefined) {
|
|
213
213
|
playwrightConfig.show = this.options.show;
|
|
214
214
|
}
|
package/dist/src/remote.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export declare class Remote implements LogDestination {
|
|
|
18
18
|
asks: Map<string, (value: string | null) => void>;
|
|
19
19
|
askCounter: number;
|
|
20
20
|
lastActivity: string | null;
|
|
21
|
+
closing: Promise<void> | null;
|
|
21
22
|
attach(url: string, command: string): void;
|
|
22
23
|
isAttached(): boolean;
|
|
23
24
|
send(type: string, data?: Record<string, unknown>): void;
|
|
@@ -30,6 +31,7 @@ export declare class Remote implements LogDestination {
|
|
|
30
31
|
* back out of flattened text.
|
|
31
32
|
*/
|
|
32
33
|
write(entry: TaggedLogEntry): void;
|
|
34
|
+
shutdown(exitCode: number): Promise<void>;
|
|
33
35
|
connect(): void;
|
|
34
36
|
reconnect(): void;
|
|
35
37
|
flush(): Promise<void>;
|
package/dist/src/remote.js
CHANGED
|
@@ -26,6 +26,7 @@ export class Remote {
|
|
|
26
26
|
asks = new Map();
|
|
27
27
|
askCounter = 0;
|
|
28
28
|
lastActivity = null;
|
|
29
|
+
closing = null;
|
|
29
30
|
attach(url, command) {
|
|
30
31
|
if (this.url)
|
|
31
32
|
return;
|
|
@@ -68,23 +69,12 @@ export class Remote {
|
|
|
68
69
|
});
|
|
69
70
|
});
|
|
70
71
|
}
|
|
71
|
-
|
|
72
|
+
close(exitCode) {
|
|
72
73
|
if (!this.url)
|
|
73
|
-
return;
|
|
74
|
-
this.
|
|
75
|
-
|
|
76
|
-
this.
|
|
77
|
-
if (this.reconnectTimer)
|
|
78
|
-
clearTimeout(this.reconnectTimer);
|
|
79
|
-
// Whoever asks next has nobody to ask — leaving the callback installed would
|
|
80
|
-
// route them into a closed socket and park them until the ask times out.
|
|
81
|
-
executionController.clearInputCallback();
|
|
82
|
-
for (const resolve of this.asks.values())
|
|
83
|
-
resolve(null);
|
|
84
|
-
this.asks.clear();
|
|
85
|
-
this.queue = [];
|
|
86
|
-
this.socket?.close();
|
|
87
|
-
this.socket = null;
|
|
74
|
+
return Promise.resolve();
|
|
75
|
+
if (!this.closing)
|
|
76
|
+
this.closing = this.shutdown(exitCode);
|
|
77
|
+
return this.closing;
|
|
88
78
|
}
|
|
89
79
|
isEnabled() {
|
|
90
80
|
return this.isAttached();
|
|
@@ -112,6 +102,23 @@ export class Remote {
|
|
|
112
102
|
error: this.errorOf(entry.originalArgs),
|
|
113
103
|
});
|
|
114
104
|
}
|
|
105
|
+
async shutdown(exitCode) {
|
|
106
|
+
this.send('result', { ok: exitCode === 0, exitCode });
|
|
107
|
+
await this.flush();
|
|
108
|
+
this.url = null;
|
|
109
|
+
if (this.reconnectTimer)
|
|
110
|
+
clearTimeout(this.reconnectTimer);
|
|
111
|
+
// Whoever asks next has nobody to ask — leaving the callback installed would
|
|
112
|
+
// route them into a closed socket and park them until the ask times out.
|
|
113
|
+
executionController.clearInputCallback();
|
|
114
|
+
for (const resolve of this.asks.values())
|
|
115
|
+
resolve(null);
|
|
116
|
+
this.asks.clear();
|
|
117
|
+
this.queue = [];
|
|
118
|
+
this.socket?.close();
|
|
119
|
+
this.socket = null;
|
|
120
|
+
this.closing = null;
|
|
121
|
+
}
|
|
115
122
|
connect() {
|
|
116
123
|
if (!this.url)
|
|
117
124
|
return;
|
package/dist/src/utils/aria.d.ts
CHANGED
package/dist/src/utils/aria.js
CHANGED
|
@@ -511,7 +511,12 @@ export const diffAriaSnapshots = (previous, current) => {
|
|
|
511
511
|
const renames = detectRenames(prev, curr, prevTotals, currTotals);
|
|
512
512
|
const added = [...byCount.added, ...renames.added];
|
|
513
513
|
const removed = [...byCount.removed, ...renames.removed];
|
|
514
|
-
return {
|
|
514
|
+
return {
|
|
515
|
+
text: formatDiff(added, removed, toggled, typed),
|
|
516
|
+
count: added.length + removed.length + toggled.length + typed.length,
|
|
517
|
+
added: added.length,
|
|
518
|
+
removed: removed.length,
|
|
519
|
+
};
|
|
515
520
|
};
|
|
516
521
|
export const detectFocusArea = (snapshot) => {
|
|
517
522
|
let tree = parseSnapshot(snapshot);
|
|
@@ -4,7 +4,7 @@ import { isDynamicId, isGenericClass } from "./xpath.js";
|
|
|
4
4
|
const IGNORED_PATHS = new Set(['html[1]', 'html[1]/head[1]', 'html[1]/body[1]']);
|
|
5
5
|
const SHELL_RATIO = 0.8;
|
|
6
6
|
const ROOT_CONTENT_RATIO = 0.8;
|
|
7
|
-
const LIVE_REGION_ROLES = new Set(['alert', 'alertdialog', 'status', 'log']);
|
|
7
|
+
const LIVE_REGION_ROLES = new Set(['alert', 'alertdialog', 'status', 'log', 'tooltip']);
|
|
8
8
|
const TEXT_LINE_PREFIX = 'TEXT:';
|
|
9
9
|
const MESSAGE_MAX_LENGTH = 200;
|
|
10
10
|
const MESSAGE_LIMIT = 8;
|
|
@@ -447,6 +447,9 @@ function semanticSelectorFor(element, allElements) {
|
|
|
447
447
|
const selector = buildContainerSelector(current, allElements);
|
|
448
448
|
if (selector)
|
|
449
449
|
return selector;
|
|
450
|
+
const classAttr = (current.attrs ?? []).find((a) => a.name === 'class')?.value;
|
|
451
|
+
if (classAttr && filterContainerClasses(classAttr.split(/\s+/).filter(Boolean)).length > 0)
|
|
452
|
+
return undefined;
|
|
450
453
|
current = dominantChild(current);
|
|
451
454
|
}
|
|
452
455
|
return undefined;
|
|
@@ -29,6 +29,8 @@ export declare class MarkdownQuery {
|
|
|
29
29
|
text(): string;
|
|
30
30
|
get(): string;
|
|
31
31
|
toJson(): Record<string, string>[];
|
|
32
|
+
keyValue(): Record<string, string>;
|
|
33
|
+
setKeyValue(key: string, value: string | null): string;
|
|
32
34
|
replace(content: string): string;
|
|
33
35
|
replaceEach(replacer: (match: MarkdownQuery, index: number) => string): string;
|
|
34
36
|
count(): number;
|
|
@@ -129,6 +129,12 @@ function matchText(text, matcher) {
|
|
|
129
129
|
}
|
|
130
130
|
return matcher.negated ? !result : result;
|
|
131
131
|
}
|
|
132
|
+
function entryKey(line) {
|
|
133
|
+
const separator = line.indexOf(':');
|
|
134
|
+
if (separator < 1)
|
|
135
|
+
return null;
|
|
136
|
+
return line.slice(0, separator).trim().toLowerCase();
|
|
137
|
+
}
|
|
132
138
|
function getTokenText(token) {
|
|
133
139
|
const t = token;
|
|
134
140
|
switch (token.type) {
|
|
@@ -340,6 +346,39 @@ export class MarkdownQuery {
|
|
|
340
346
|
}
|
|
341
347
|
return results;
|
|
342
348
|
}
|
|
349
|
+
keyValue() {
|
|
350
|
+
const entries = {};
|
|
351
|
+
for (const range of this.matches) {
|
|
352
|
+
for (const line of getTokenText(range.token).split('\n')) {
|
|
353
|
+
const key = entryKey(line);
|
|
354
|
+
if (!key)
|
|
355
|
+
continue;
|
|
356
|
+
const value = line.slice(line.indexOf(':') + 1).trim();
|
|
357
|
+
if (value)
|
|
358
|
+
entries[key] = value;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return entries;
|
|
362
|
+
}
|
|
363
|
+
setKeyValue(key, value) {
|
|
364
|
+
return this.replaceEach((match) => {
|
|
365
|
+
const token = match.matches[0].token;
|
|
366
|
+
const lines = getTokenText(token)
|
|
367
|
+
.split('\n')
|
|
368
|
+
.map((line) => line.trim())
|
|
369
|
+
.filter(Boolean);
|
|
370
|
+
const index = lines.findIndex((line) => entryKey(line) === key.toLowerCase());
|
|
371
|
+
if (index < 0 && value)
|
|
372
|
+
lines.push(`${key}: ${value}`);
|
|
373
|
+
if (index >= 0 && value)
|
|
374
|
+
lines[index] = `${key}: ${value}`;
|
|
375
|
+
if (index >= 0 && !value)
|
|
376
|
+
lines.splice(index, 1);
|
|
377
|
+
if (token.type !== 'blockquote')
|
|
378
|
+
return lines.join('\n');
|
|
379
|
+
return lines.map((line) => `> ${line}`).join('\n');
|
|
380
|
+
});
|
|
381
|
+
}
|
|
343
382
|
replace(content) {
|
|
344
383
|
return this.replaceEach(() => content);
|
|
345
384
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare function inspectList(css: string): ListMeasure | null;
|
|
2
|
+
export declare function restoreScroll({ css, scrollTop, pageScrollY }: ScrollPosition): void;
|
|
3
|
+
export type PaginationStrategy = 'controls' | 'infinite';
|
|
4
|
+
export interface ListMeasure {
|
|
5
|
+
hasPagingControls: boolean;
|
|
6
|
+
isFeed: boolean;
|
|
7
|
+
scrolls: boolean;
|
|
8
|
+
items: number;
|
|
9
|
+
scrollTop: number;
|
|
10
|
+
pageScrollY: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ScrollPosition {
|
|
13
|
+
css: string;
|
|
14
|
+
scrollTop: number;
|
|
15
|
+
pageScrollY: number;
|
|
16
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function inspectList(css) {
|
|
2
|
+
const element = document.querySelector(css);
|
|
3
|
+
if (!element)
|
|
4
|
+
return null;
|
|
5
|
+
const rect = element.getBoundingClientRect();
|
|
6
|
+
return {
|
|
7
|
+
hasPagingControls: !!element.querySelector('a[rel="next"], a[rel="prev"]'),
|
|
8
|
+
isFeed: element.matches('[role="feed"]') || !!element.querySelector('[role="feed"]'),
|
|
9
|
+
scrolls: element.scrollHeight > element.clientHeight + 1 || rect.bottom > window.innerHeight,
|
|
10
|
+
items: element.querySelectorAll(':scope > *').length,
|
|
11
|
+
scrollTop: element.scrollTop,
|
|
12
|
+
pageScrollY: window.scrollY,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function restoreScroll({ css, scrollTop, pageScrollY }) {
|
|
16
|
+
const element = document.querySelector(css);
|
|
17
|
+
if (element)
|
|
18
|
+
element.scrollTop = scrollTop;
|
|
19
|
+
window.scrollTo(0, pageScrollY);
|
|
20
|
+
}
|
|
@@ -66,7 +66,7 @@ npx explorbot api explore https://api.example.com/v1 \
|
|
|
66
66
|
-H "Authorization: Bearer $TOKEN"
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
`api explore` takes
|
|
69
|
+
`api explore` takes an endpoint or a pattern as its argument, so one line covers the whole run: it plans, executes each plan, and reports the totals. Passing the base endpoint covers every collection the spec describes. The other commands take a path within the API and read the base from `--endpoint`:
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
72
|
npx explorbot api plan /users \
|
|
@@ -81,7 +81,7 @@ The base endpoint keeps its path prefix: given `https://api.example.com/v1`, ste
|
|
|
81
81
|
|
|
82
82
|
### A dedicated API project
|
|
83
83
|
|
|
84
|
-
If you don't have a web `explorbot.config.js`, run `npx explorbot api init`. It asks for your base endpoint, spec, and a one-line description of the API, then writes a standalone `apibot.config.
|
|
84
|
+
If you don't have a web `explorbot.config.js`, run `npx explorbot api init`. It asks for your base endpoint, spec, and a one-line description of the API, then writes a standalone `apibot.config.js` (with an `ai` and `api` section) plus `output/` and `knowledge/` directories. When both files exist, `apibot.config.*` takes precedence over `explorbot.config.*`.
|
|
85
85
|
|
|
86
86
|
## Your first run
|
|
87
87
|
|
|
@@ -99,6 +99,30 @@ npx explorbot api test output/plans/users.md
|
|
|
99
99
|
|
|
100
100
|
Curler runs the scenarios and prints how many passed and failed.
|
|
101
101
|
|
|
102
|
+
### Covering many endpoints
|
|
103
|
+
|
|
104
|
+
`api explore` runs the whole loop for you: plan, test, re-plan. Given one endpoint it plans in every style.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npx explorbot api explore /users
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The endpoint may be a pattern. `*` stands for one path segment, and a pattern also covers the paths below it, so `/users` and `/users/*` both cover `/users/{id}`. Quote it, or your shell will try to expand it first.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npx explorbot api explore '/projects/acme/*'
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Explorbot explores collections, not raw paths: `/users/{id}` and `/users/{id}/posts` fold into `/users`, whose spec lookup brings them along anyway. When a pattern matches several collections the planning styles spread across them, one style per collection, so covering a whole API stays one plan per collection rather than one per style.
|
|
117
|
+
|
|
118
|
+
Pass `/` to take every collection in the spec. The path parameters have to come from somewhere, so put them in the base endpoint.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npx explorbot api explore / --endpoint https://api.example.com/v2/acme
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
If a parameter is left with no value the run stops and names it, rather than sending requests to a literal `{project_id}`. Collections whose own parameters no pattern can fill, like `/analytics/stats/{kind}`, are listed and skipped.
|
|
125
|
+
|
|
102
126
|
## Output files
|
|
103
127
|
|
|
104
128
|
| Output | Location | What it is |
|