explorbot 0.3.5 → 0.4.1
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 +23 -16
- package/boat/api-tester/src/ai/chief.ts +7 -1
- package/boat/api-tester/src/ai/curler.ts +7 -1
- package/boat/api-tester/src/apibot.ts +10 -4
- package/boat/api-tester/src/cli.ts +12 -2
- package/boat/api-tester/src/config.ts +28 -8
- package/boat/doc-collector/bin/doc-collector-cli.ts +3 -2
- package/boat/doc-collector/src/cli.ts +6 -1
- package/boat/doc-collector/src/docbot.ts +4 -2
- package/boat/prima/bin/prima-cli.ts +2 -0
- package/boat/prima/src/cli.ts +23 -8
- package/boat/prima/src/envelope.ts +35 -9
- package/boat/prima/src/prima.ts +23 -10
- package/dist/bin/explorbot-cli.js +24 -16
- package/dist/boat/api-tester/bin/apibot-cli.js +3 -2
- package/dist/boat/api-tester/src/ai/chief.js +6 -1
- package/dist/boat/api-tester/src/ai/curler.js +6 -1
- package/dist/boat/api-tester/src/apibot.js +7 -3
- package/dist/boat/api-tester/src/cli.js +12 -2
- package/dist/boat/api-tester/src/config.js +31 -8
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +3 -2
- package/dist/boat/doc-collector/src/cli.js +6 -1
- package/dist/boat/doc-collector/src/docbot.js +4 -2
- package/dist/boat/prima/bin/prima-cli.js +2 -0
- package/dist/boat/prima/src/cli.js +22 -8
- package/dist/boat/prima/src/envelope.js +24 -6
- package/dist/boat/prima/src/prima.js +23 -11
- package/dist/package.json +2 -2
- package/dist/rules/planner/styles/normal.md +1 -1
- package/dist/src/action-result.d.ts +9 -1
- package/dist/src/action-result.js +57 -18
- package/dist/src/action.d.ts +1 -1
- package/dist/src/action.js +87 -12
- package/dist/src/ai/captain.js +1 -1
- package/dist/src/ai/driller.d.ts +0 -1
- package/dist/src/ai/driller.js +8 -20
- package/dist/src/ai/fisherman-tools.d.ts +9 -0
- package/dist/src/ai/fisherman-tools.js +52 -6
- package/dist/src/ai/fisherman.d.ts +4 -2
- package/dist/src/ai/fisherman.js +48 -27
- package/dist/src/ai/historian/codeceptjs.js +1 -1
- package/dist/src/ai/historian/playwright.js +1 -1
- package/dist/src/ai/navigator.js +1 -1
- package/dist/src/ai/pilot.d.ts +1 -0
- package/dist/src/ai/pilot.js +13 -1
- package/dist/src/ai/planner.js +9 -7
- package/dist/src/ai/provider.js +20 -3
- package/dist/src/ai/researcher/deep-analysis.js +1 -3
- package/dist/src/ai/researcher.js +5 -3
- package/dist/src/ai/rules.js +3 -3
- package/dist/src/ai/tester.d.ts +3 -0
- package/dist/src/ai/tester.js +40 -3
- package/dist/src/ai/tools.d.ts +1 -0
- package/dist/src/ai/tools.js +13 -6
- package/dist/src/api/request-result.d.ts +2 -0
- package/dist/src/api/request-result.js +8 -2
- package/dist/src/api/request-store.d.ts +3 -2
- package/dist/src/api/request-store.js +66 -14
- package/dist/src/api/spec-reader.js +1 -1
- package/dist/src/commands/config-command.js +1 -1
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/explore-command.d.ts +6 -0
- package/dist/src/commands/explore-command.js +39 -3
- package/dist/src/commands/freesail-command.js +10 -1
- package/dist/src/commands/options/base-option.d.ts +8 -0
- package/dist/src/commands/options/base-option.js +12 -0
- package/dist/src/commands/options/index.d.ts +5 -0
- package/dist/src/commands/options/index.js +5 -0
- package/dist/src/commands/options/knowledge-option.d.ts +7 -0
- package/dist/src/commands/options/knowledge-option.js +12 -0
- package/dist/src/commands/options/ws-option.d.ts +7 -0
- package/dist/src/commands/options/ws-option.js +21 -0
- package/dist/src/commands/plans-command.js +6 -6
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +12 -0
- package/dist/src/experience-tracker.js +5 -0
- package/dist/src/explorbot.d.ts +0 -1
- package/dist/src/explorbot.js +24 -37
- package/dist/src/knowledge-tracker.d.ts +20 -7
- package/dist/src/knowledge-tracker.js +69 -31
- package/dist/src/remote.d.ts +0 -3
- package/dist/src/remote.js +0 -18
- package/dist/src/state-manager.d.ts +5 -1
- package/dist/src/state-manager.js +10 -7
- package/dist/src/test-plan.d.ts +3 -0
- package/dist/src/test-plan.js +27 -0
- package/dist/src/utils/aria.d.ts +1 -1
- package/dist/src/utils/aria.js +6 -42
- package/dist/src/utils/html-diff.d.ts +4 -0
- package/dist/src/utils/html-diff.js +62 -7
- package/dist/src/utils/html.d.ts +5 -15
- package/dist/src/utils/html.js +14 -85
- package/dist/src/utils/overlay.d.ts +56 -11
- package/dist/src/utils/overlay.js +191 -21
- package/dist/src/utils/request-map.d.ts +7 -0
- package/dist/src/utils/request-map.js +16 -0
- package/dist/src/utils/url-matcher.js +4 -2
- package/docs/api-testing/basics.md +15 -0
- package/docs/api-testing/planning.md +10 -1
- package/docs/reference/commands.md +32 -5
- package/docs/reference/websocket.md +1 -0
- package/docs/superpowers/plans/2026-08-29-fisherman-reliability.md +953 -0
- package/docs/superpowers/plans/2026-08-29-region-states.md +1292 -0
- package/docs/superpowers/plans/2026-08-30-fisherman-live-session-auth.md +457 -0
- package/docs/superpowers/specs/2026-08-29-fisherman-reliability-design.md +45 -0
- package/docs/superpowers/specs/2026-08-29-region-states-design.md +262 -0
- package/docs/superpowers/specs/2026-08-29-region-states-fixes-design.md +269 -0
- package/docs/superpowers/specs/2026-08-30-fisherman-live-session-auth-design.md +37 -0
- package/docs/workflow/agentic-usage.md +12 -2
- package/docs/workflow/ci.md +1 -0
- package/docs/workflow/knowledge.md +46 -2
- package/package.json +2 -2
- package/rules/planner/styles/normal.md +1 -1
- package/src/action-result.ts +61 -22
- package/src/action.ts +87 -14
- package/src/ai/captain.ts +1 -1
- package/src/ai/driller.ts +7 -39
- package/src/ai/fisherman-tools.ts +56 -7
- package/src/ai/fisherman.ts +48 -28
- package/src/ai/historian/codeceptjs.ts +1 -1
- package/src/ai/historian/playwright.ts +1 -1
- package/src/ai/navigator.ts +1 -1
- package/src/ai/pilot.ts +9 -1
- package/src/ai/planner.ts +9 -8
- package/src/ai/provider.ts +21 -3
- package/src/ai/researcher/deep-analysis.ts +1 -2
- package/src/ai/researcher.ts +4 -3
- package/src/ai/rules.ts +3 -3
- package/src/ai/tester.ts +40 -3
- package/src/ai/tools.ts +17 -9
- package/src/api/request-result.ts +10 -2
- package/src/api/request-store.ts +60 -13
- package/src/api/spec-reader.ts +1 -1
- package/src/commands/config-command.ts +1 -1
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/explore-command.ts +37 -3
- package/src/commands/freesail-command.ts +7 -1
- package/src/commands/options/base-option.ts +18 -0
- package/src/commands/options/index.ts +7 -0
- package/src/commands/options/knowledge-option.ts +14 -0
- package/src/commands/options/ws-option.ts +24 -0
- package/src/commands/plans-command.ts +6 -6
- package/src/config.ts +12 -0
- package/src/experience-tracker.ts +5 -1
- package/src/explorbot.ts +21 -37
- package/src/knowledge-tracker.ts +94 -36
- package/src/remote.ts +0 -20
- package/src/state-manager.ts +13 -7
- package/src/test-plan.ts +29 -0
- package/src/utils/aria.ts +7 -44
- package/src/utils/html-diff.ts +62 -7
- package/src/utils/html.ts +14 -91
- package/src/utils/overlay.ts +226 -23
- package/src/utils/request-map.ts +19 -0
- package/src/utils/url-matcher.ts +3 -2
package/bin/explorbot-cli.ts
CHANGED
|
@@ -10,6 +10,7 @@ import React from 'react';
|
|
|
10
10
|
import { flushTelemetry } from '../src/ai/provider.js';
|
|
11
11
|
import { App } from '../src/components/App.js';
|
|
12
12
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
13
|
+
import { knowledgeOption, wsOption } from '../src/commands/options/index.js';
|
|
13
14
|
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
14
15
|
import { ExplorBot, type ExplorBotOptions } from '../src/explorbot.js';
|
|
15
16
|
import { remote } from '../src/remote.js';
|
|
@@ -28,7 +29,8 @@ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../p
|
|
|
28
29
|
const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version as string;
|
|
29
30
|
|
|
30
31
|
program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
|
|
31
|
-
|
|
32
|
+
wsOption.register(program);
|
|
33
|
+
knowledgeOption.register(program);
|
|
32
34
|
|
|
33
35
|
process.on('uncaughtException', async (error) => {
|
|
34
36
|
tag('error').log(`Uncaught exception: ${error instanceof Error ? `${error.message}\n${error.stack}` : String(error)}`);
|
|
@@ -146,6 +148,7 @@ addCommonOptions(
|
|
|
146
148
|
.command('explore <path>')
|
|
147
149
|
.description('Explore a page autonomously and run invented scenarios')
|
|
148
150
|
.option('--max-tests <count>', 'Maximum number of tests to run')
|
|
151
|
+
.option('--max-duration <minutes>', 'Wall-clock budget in minutes for the whole run; wraps up before the limit is hit')
|
|
149
152
|
.option('--focus <feature>', 'Focus area for exploration')
|
|
150
153
|
.option('--configure <spec>', 'Reuse spec: keys new|from|style|subpages|pick_by|priority, e.g. "new:25%;pick_by=random;priority=critical,high"')
|
|
151
154
|
.option('--dry-run', 'Mark picked tests as skipped without executing or generating new ones')
|
|
@@ -157,6 +160,8 @@ addCommonOptions(
|
|
|
157
160
|
const { ExploreCommand } = await import('../src/commands/explore-command.js');
|
|
158
161
|
const cmd = new ExploreCommand(explorBot);
|
|
159
162
|
if (options.maxTests) cmd.maxTests = Number.parseInt(options.maxTests, 10);
|
|
163
|
+
if (options.maxDuration) cmd.maxDurationMinutes = Number.parseInt(options.maxDuration, 10);
|
|
164
|
+
else if (process.env.EXPLORBOT_MAX_DURATION) cmd.maxDurationMinutes = Number.parseInt(process.env.EXPLORBOT_MAX_DURATION, 10);
|
|
160
165
|
if (options.dryRun) cmd.dryRun = true;
|
|
161
166
|
const execArgs: string[] = [];
|
|
162
167
|
if (options.focus) execArgs.push('--focus', `"${options.focus}"`);
|
|
@@ -257,14 +262,13 @@ addCommonOptions(program.command('plan <path>').description('Generate test plan
|
|
|
257
262
|
|
|
258
263
|
addCommonOptions(program.command('plan:load <planfile> [index]').description('Load a plan file and display its tests. Pass index to see test details.')).action(async (planfile: string, index: string | undefined) => {
|
|
259
264
|
try {
|
|
260
|
-
const
|
|
261
|
-
if (!
|
|
262
|
-
console.error(`Plan file not found: ${
|
|
265
|
+
const plan = Plan.loadFromFile(planfile);
|
|
266
|
+
if (!plan?.filePath) {
|
|
267
|
+
console.error(`Plan file not found: ${planfile}`);
|
|
263
268
|
process.exit(1);
|
|
264
269
|
}
|
|
265
270
|
|
|
266
|
-
const
|
|
267
|
-
const planFile = path.basename(resolvedPath);
|
|
271
|
+
const planFile = path.basename(plan.filePath);
|
|
268
272
|
|
|
269
273
|
if (index) {
|
|
270
274
|
const idx = Number.parseInt(index, 10);
|
|
@@ -276,7 +280,7 @@ addCommonOptions(program.command('plan:load <planfile> [index]').description('Lo
|
|
|
276
280
|
const lines: string[] = [];
|
|
277
281
|
lines.push(`## #${idx} ${test.scenario}\n`);
|
|
278
282
|
lines.push(`**Priority:** ${test.priority}`);
|
|
279
|
-
const planUrl = plan.
|
|
283
|
+
const planUrl = plan.startUrl;
|
|
280
284
|
if (planUrl) lines.push(`**Plan URL:** ${planUrl}`);
|
|
281
285
|
if (test.startUrl && test.startUrl !== planUrl) lines.push(`**Test URL:** ${test.startUrl}`);
|
|
282
286
|
if (test.plannedSteps.length) {
|
|
@@ -293,7 +297,7 @@ addCommonOptions(program.command('plan:load <planfile> [index]').description('Lo
|
|
|
293
297
|
return;
|
|
294
298
|
}
|
|
295
299
|
|
|
296
|
-
const planUrl = plan.
|
|
300
|
+
const planUrl = plan.startUrl;
|
|
297
301
|
const lines: string[] = [`**${plan.title}** (${plan.tests.length} tests)\n`];
|
|
298
302
|
if (planUrl) {
|
|
299
303
|
lines.push(`URL: ${planUrl}\n`);
|
|
@@ -325,9 +329,6 @@ addCommonOptions(program.command('plan:load <planfile> [index]').description('Lo
|
|
|
325
329
|
addCommonOptions(program.command('test <planfile> [index]').description('Execute tests from a plan file. Index: 1, 1,3, 1-5, *, all').option('--grep <pattern>', 'Run tests matching pattern').option('--from-plan <file>', 'Load plan file when the first argument is a test index')).action(
|
|
326
330
|
async (planfile, index, options) => {
|
|
327
331
|
try {
|
|
328
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
|
|
329
|
-
await explorBot.start();
|
|
330
|
-
|
|
331
332
|
let planfileArg = planfile;
|
|
332
333
|
let indexArg = index;
|
|
333
334
|
if (options.fromPlan) {
|
|
@@ -335,11 +336,16 @@ addCommonOptions(program.command('test <planfile> [index]').description('Execute
|
|
|
335
336
|
indexArg = planfile;
|
|
336
337
|
}
|
|
337
338
|
|
|
339
|
+
const planTarget = Plan.loadFromFile(planfileArg)?.startUrl;
|
|
340
|
+
|
|
341
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(planTarget, options));
|
|
342
|
+
await explorBot.start();
|
|
343
|
+
|
|
338
344
|
const plan = explorBot.loadPlan(planfileArg);
|
|
339
345
|
const pending = plan.getPendingTests();
|
|
340
346
|
log(`Plan loaded: "${plan.title}" (${plan.tests.length} tests, ${pending.length} pending)`);
|
|
341
347
|
|
|
342
|
-
const startUrl = plan.
|
|
348
|
+
const startUrl = plan.startUrl;
|
|
343
349
|
if (!startUrl) {
|
|
344
350
|
throw new Error('No URL found in plan or tests. Cannot determine where to navigate.');
|
|
345
351
|
}
|
|
@@ -347,7 +353,7 @@ addCommonOptions(program.command('test <planfile> [index]').description('Execute
|
|
|
347
353
|
log(`Navigating to ${startUrl}`);
|
|
348
354
|
await explorBot.visit(startUrl);
|
|
349
355
|
|
|
350
|
-
let args = '';
|
|
356
|
+
let args = '*';
|
|
351
357
|
if (indexArg) args = indexArg;
|
|
352
358
|
else if (options.grep) args = options.grep;
|
|
353
359
|
|
|
@@ -437,10 +443,11 @@ addCommonOptions(
|
|
|
437
443
|
.option('--shallow', 'Breadth-first: pick globally least-visited page')
|
|
438
444
|
.option('--scope <prefix>', 'Restrict navigation to URL prefix')
|
|
439
445
|
.option('--max-tests <count>', 'Maximum number of tests to run')
|
|
446
|
+
.option('--max-duration <minutes>', 'Wall-clock budget in minutes for the whole run')
|
|
440
447
|
).action(async (startUrl, options) => {
|
|
441
448
|
const explorBot = new ExplorBot(buildExplorBotOptions(startUrl || '/', options));
|
|
442
449
|
await explorBot.start();
|
|
443
|
-
const args = [options.deep && '--deep', options.shallow && '--shallow', options.scope && `--scope ${options.scope}`, options.maxTests && `--max-tests ${options.maxTests}`].filter(Boolean).join(' ');
|
|
450
|
+
const args = [options.deep && '--deep', options.shallow && '--shallow', options.scope && `--scope ${options.scope}`, options.maxTests && `--max-tests ${options.maxTests}`, options.maxDuration && `--max-duration ${options.maxDuration}`].filter(Boolean).join(' ');
|
|
444
451
|
const { FreesailCommand } = await import('../src/commands/freesail-command.js');
|
|
445
452
|
const cmd = new FreesailCommand(explorBot);
|
|
446
453
|
await cmd.execute(args);
|
|
@@ -701,7 +708,7 @@ addCommonOptions(program.command('navigate <url>').description('Navigate to a UR
|
|
|
701
708
|
});
|
|
702
709
|
|
|
703
710
|
addCommonOptions(
|
|
704
|
-
program.command('drill <url>').alias('driller').description('Drill all components on a page to learn interactions').option('--knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max-components <count>', 'Maximum number of components to drill')
|
|
711
|
+
program.command('drill <url>').alias('driller').description('Drill all components on a page to learn interactions').option('--save-knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max-components <count>', 'Maximum number of components to drill')
|
|
705
712
|
).action(async (url, options) => {
|
|
706
713
|
try {
|
|
707
714
|
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
@@ -710,7 +717,7 @@ addCommonOptions(
|
|
|
710
717
|
await explorBot.visit(url);
|
|
711
718
|
|
|
712
719
|
const plan = await explorBot.agentDriller().drill({
|
|
713
|
-
knowledgePath: options.
|
|
720
|
+
knowledgePath: options.saveKnowledge,
|
|
714
721
|
maxComponents: Number.parseInt(options.maxComponents || '30', 10),
|
|
715
722
|
interactive: false,
|
|
716
723
|
});
|
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { Conversation } from '../../../../src/ai/conversation.ts';
|
|
4
4
|
import { WithSessionDedup } from '../../../../src/ai/planner/session-dedup.ts';
|
|
5
5
|
import type { AIProvider } from '../../../../src/ai/provider.ts';
|
|
6
|
+
import type { KnowledgeTracker } from '../../../../src/knowledge-tracker.ts';
|
|
6
7
|
import { Observability } from '../../../../src/observability.ts';
|
|
7
8
|
import { Plan, Test } from '../../../../src/test-plan.ts';
|
|
8
9
|
import { createDebug, tag } from '../../../../src/utils/logger.ts';
|
|
@@ -33,17 +34,19 @@ export class Chief extends ChiefBase {
|
|
|
33
34
|
private provider: AIProvider;
|
|
34
35
|
private config: ApibotConfig;
|
|
35
36
|
private apiClient: ApiClient | null;
|
|
37
|
+
private knowledgeTracker?: KnowledgeTracker;
|
|
36
38
|
currentPlan: Plan | null = null;
|
|
37
39
|
private lastStyleName = '';
|
|
38
40
|
|
|
39
41
|
MIN_TASKS = 3;
|
|
40
42
|
MAX_TASKS = 10;
|
|
41
43
|
|
|
42
|
-
constructor(provider: AIProvider, config: ApibotConfig, apiClient?: ApiClient | null) {
|
|
44
|
+
constructor(provider: AIProvider, config: ApibotConfig, apiClient?: ApiClient | null, knowledgeTracker?: KnowledgeTracker) {
|
|
43
45
|
super();
|
|
44
46
|
this.provider = provider;
|
|
45
47
|
this.config = config;
|
|
46
48
|
this.apiClient = apiClient || null;
|
|
49
|
+
this.knowledgeTracker = knowledgeTracker;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
async plan(endpoint: string, opts?: { style?: string; specDefinition?: string }): Promise<Plan> {
|
|
@@ -56,6 +59,9 @@ export class Chief extends ChiefBase {
|
|
|
56
59
|
const sampleData = await this.collectSampleData(endpoint);
|
|
57
60
|
const conversation = this.buildConversation(endpoint, opts?.style, sampleData);
|
|
58
61
|
|
|
62
|
+
const knowledge = this.knowledgeTracker?.renderEndpointKnowledge(endpoint);
|
|
63
|
+
if (knowledge) conversation.addUserText(knowledge);
|
|
64
|
+
|
|
59
65
|
if (opts?.specDefinition) {
|
|
60
66
|
conversation.addUserText(dedent`
|
|
61
67
|
<api_spec>
|
|
@@ -2,6 +2,7 @@ import dedent from 'dedent';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import type { AIProvider } from '../../../../src/ai/provider.ts';
|
|
4
4
|
import type { RequestStore } from '../../../../src/api/request-store.ts';
|
|
5
|
+
import type { KnowledgeTracker } from '../../../../src/knowledge-tracker.ts';
|
|
5
6
|
import type { Reporter } from '../../../../src/reporter.ts';
|
|
6
7
|
import { type Test, TestResult } from '../../../../src/test-plan.ts';
|
|
7
8
|
import { createDebug, tag } from '../../../../src/utils/logger.ts';
|
|
@@ -18,12 +19,14 @@ export class Curler {
|
|
|
18
19
|
private apiClient: ApiClient;
|
|
19
20
|
private requestState: RequestStore;
|
|
20
21
|
private reporter: Reporter;
|
|
22
|
+
private knowledgeTracker?: KnowledgeTracker;
|
|
21
23
|
|
|
22
|
-
constructor(provider: AIProvider, apiClient: ApiClient, requestState: RequestStore, reporter: Reporter) {
|
|
24
|
+
constructor(provider: AIProvider, apiClient: ApiClient, requestState: RequestStore, reporter: Reporter, knowledgeTracker?: KnowledgeTracker) {
|
|
23
25
|
this.provider = provider;
|
|
24
26
|
this.apiClient = apiClient;
|
|
25
27
|
this.requestState = requestState;
|
|
26
28
|
this.reporter = reporter;
|
|
29
|
+
this.knowledgeTracker = knowledgeTracker;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
async test(test: Test, opts?: { specDefinition?: string; baseEndpoint?: string; searchSpec?: (query: string) => string }): Promise<{ success: boolean }> {
|
|
@@ -37,6 +40,9 @@ export class Curler {
|
|
|
37
40
|
const conversation = this.provider.startConversation(this.buildSystemPrompt(), 'curler', this.provider.getAgenticModel('curler'));
|
|
38
41
|
const tools = createCurlerTools(this.apiClient, this.requestState, test, opts?.searchSpec);
|
|
39
42
|
|
|
43
|
+
const knowledge = test.startUrl && this.knowledgeTracker?.renderEndpointKnowledge(test.startUrl);
|
|
44
|
+
if (knowledge) conversation.addUserText(knowledge);
|
|
45
|
+
|
|
40
46
|
const initialPrompt = this.buildTestPrompt(test, opts?.specDefinition, opts?.baseEndpoint);
|
|
41
47
|
conversation.addUserText(initialPrompt);
|
|
42
48
|
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { AIProvider } from '../../../src/ai/provider.ts';
|
|
4
4
|
import { RequestStore } from '../../../src/api/request-store.ts';
|
|
5
5
|
import { extractEndpointDefinition, loadSpec, searchEndpoints, validateSpecs } from '../../../src/api/spec-reader.ts';
|
|
6
|
+
import { KnowledgeTracker } from '../../../src/knowledge-tracker.ts';
|
|
6
7
|
import { Reporter } from '../../../src/reporter.ts';
|
|
7
8
|
import { Plan } from '../../../src/test-plan.ts';
|
|
8
9
|
import { setVerboseMode, tag } from '../../../src/utils/logger.ts';
|
|
@@ -20,6 +21,7 @@ export class ApiBot {
|
|
|
20
21
|
private apiClient!: ApiClient;
|
|
21
22
|
private requestState!: RequestStore;
|
|
22
23
|
private reporter!: Reporter;
|
|
24
|
+
private knowledgeTracker!: KnowledgeTracker;
|
|
23
25
|
private options: ApibotOptions;
|
|
24
26
|
private apiSpec: any;
|
|
25
27
|
|
|
@@ -33,7 +35,7 @@ export class ApiBot {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
async start(): Promise<void> {
|
|
36
|
-
this.config = await this.configParser.loadConfig(
|
|
38
|
+
this.config = await this.configParser.loadConfig(this.options);
|
|
37
39
|
this.provider = new AIProvider(this.config.ai);
|
|
38
40
|
await this.provider.validateConnection();
|
|
39
41
|
|
|
@@ -47,6 +49,7 @@ export class ApiBot {
|
|
|
47
49
|
this.configParser.ensureDirectory(outputDir);
|
|
48
50
|
this.requestState = new RequestStore(outputDir);
|
|
49
51
|
this.reporter = new Reporter(this.config.reporter);
|
|
52
|
+
this.knowledgeTracker = new KnowledgeTracker({ knowledgeDir: this.configParser.getKnowledgeDir() });
|
|
50
53
|
|
|
51
54
|
validateSpecs(this.config.api.spec);
|
|
52
55
|
this.apiSpec = await loadSpec(this.config.api.spec!, outputDir);
|
|
@@ -84,21 +87,22 @@ export class ApiBot {
|
|
|
84
87
|
await this.apiClient?.teardown();
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
createAgent<T>(factory: (deps: { ai: AIProvider; config: ApibotConfig; apiClient: ApiClient; requestState: RequestStore }) => T): T {
|
|
90
|
+
createAgent<T>(factory: (deps: { ai: AIProvider; config: ApibotConfig; apiClient: ApiClient; requestState: RequestStore; knowledge: KnowledgeTracker }) => T): T {
|
|
88
91
|
return factory({
|
|
89
92
|
ai: this.provider,
|
|
90
93
|
config: this.config,
|
|
91
94
|
apiClient: this.apiClient,
|
|
92
95
|
requestState: this.requestState,
|
|
96
|
+
knowledge: this.knowledgeTracker,
|
|
93
97
|
});
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
agentChief(): Chief {
|
|
97
|
-
return (this.agents.chief ||= this.createAgent(({ ai, config, apiClient }) => new Chief(ai, config, apiClient)));
|
|
101
|
+
return (this.agents.chief ||= this.createAgent(({ ai, config, apiClient, knowledge }) => new Chief(ai, config, apiClient, knowledge)));
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
agentCurler(): Curler {
|
|
101
|
-
return (this.agents.curler ||= this.createAgent(({ ai, apiClient, requestState }) => new Curler(ai, apiClient, requestState, this.reporter)));
|
|
105
|
+
return (this.agents.curler ||= this.createAgent(({ ai, apiClient, requestState, knowledge }) => new Curler(ai, apiClient, requestState, this.reporter, knowledge)));
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
async plan(target: string, opts: { style?: string; fresh?: boolean } = {}): Promise<Plan> {
|
|
@@ -200,6 +204,8 @@ interface ApibotOptions {
|
|
|
200
204
|
config?: string;
|
|
201
205
|
path?: string;
|
|
202
206
|
endpoint?: string;
|
|
207
|
+
baseEndpoint?: string;
|
|
208
|
+
spec?: string;
|
|
203
209
|
}
|
|
204
210
|
|
|
205
211
|
export type { ApibotOptions };
|
|
@@ -13,11 +13,19 @@ function buildOptions(options: any): ApibotOptions {
|
|
|
13
13
|
verbose: options.verbose || options.debug,
|
|
14
14
|
config: options.config,
|
|
15
15
|
path: options.path,
|
|
16
|
+
baseEndpoint: options.endpoint,
|
|
17
|
+
spec: options.spec,
|
|
16
18
|
};
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
function addCommonOptions(cmd: Command): Command {
|
|
20
|
-
return cmd
|
|
22
|
+
return cmd
|
|
23
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
24
|
+
.option('--debug', 'Enable debug logging')
|
|
25
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
26
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
27
|
+
.option('--endpoint <url>', 'Base API endpoint to test (env: EXPLORBOT_URL)')
|
|
28
|
+
.option('--spec <path>', 'OpenAPI spec file or URL (env: EXPLORBOT_API_SPEC)');
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
function selectTests(tests: any[], index?: string): any[] {
|
|
@@ -90,8 +98,10 @@ export function createApiCommands(name = 'api'): Command {
|
|
|
90
98
|
.action(async (endpoint, options) => {
|
|
91
99
|
const parser = ApibotConfigParser.getInstance();
|
|
92
100
|
const [site] = listSites();
|
|
101
|
+
const runOptions = buildOptions(options);
|
|
102
|
+
runOptions.endpoint = endpoint || site?.url;
|
|
93
103
|
try {
|
|
94
|
-
const config = await parser.loadConfig(
|
|
104
|
+
const config = await parser.loadConfig(runOptions);
|
|
95
105
|
console.log(ConfigCommand.render(config, { configPath: parser.getConfigPath(), root: parser.getProjectRoot(), json: options.json }));
|
|
96
106
|
} catch (error) {
|
|
97
107
|
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
@@ -45,7 +45,7 @@ export class ApibotConfigParser {
|
|
|
45
45
|
Object.assign(process.env, parseEnv(readFileSync(resolved, 'utf8')));
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
async loadConfig(options?: { config?: string; path?: string; endpoint?: string }): Promise<ApibotConfig> {
|
|
48
|
+
async loadConfig(options?: { config?: string; path?: string; endpoint?: string; baseEndpoint?: string; spec?: string }): Promise<ApibotConfig> {
|
|
49
49
|
if (this.config && !options?.config && !options?.path) return this.config;
|
|
50
50
|
|
|
51
51
|
const originalCwd = process.cwd();
|
|
@@ -55,6 +55,7 @@ export class ApibotConfigParser {
|
|
|
55
55
|
|
|
56
56
|
ApibotConfigParser.loadEnv(globalEnvPath());
|
|
57
57
|
ApibotConfigParser.loadEnv('.env');
|
|
58
|
+
this.applyRunOptions(options);
|
|
58
59
|
|
|
59
60
|
const resolvedPath = options?.config || this.findConfigFile();
|
|
60
61
|
if (!resolvedPath) {
|
|
@@ -82,6 +83,8 @@ export class ApibotConfigParser {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
this.config = this.mergeWithDefaults(loadedConfig);
|
|
86
|
+
this.applyEnvSpec(this.config.api);
|
|
87
|
+
if (options?.baseEndpoint) this.config.api.baseEndpoint = options.baseEndpoint.replace(/\/$/, '');
|
|
85
88
|
await resolveConfigModels(this.config.ai);
|
|
86
89
|
this.configPath = resolvedPath;
|
|
87
90
|
this.site = null;
|
|
@@ -126,6 +129,11 @@ export class ApibotConfigParser {
|
|
|
126
129
|
|
|
127
130
|
const resolved = resolveSiteTarget(endpoint, this.site.url);
|
|
128
131
|
if (resolved.baseUrl !== this.site.url) return endpoint;
|
|
132
|
+
|
|
133
|
+
const basePath = new URL(this.getConfig().api.baseEndpoint).pathname.replace(/\/$/, '');
|
|
134
|
+
if (!basePath) return resolved.path;
|
|
135
|
+
if (resolved.path === basePath) return '/';
|
|
136
|
+
if (resolved.path.startsWith(`${basePath}/`)) return resolved.path.slice(basePath.length);
|
|
129
137
|
return resolved.path;
|
|
130
138
|
}
|
|
131
139
|
|
|
@@ -148,13 +156,27 @@ export class ApibotConfigParser {
|
|
|
148
156
|
}
|
|
149
157
|
}
|
|
150
158
|
|
|
159
|
+
private applyRunOptions(options?: { baseEndpoint?: string; spec?: string }): void {
|
|
160
|
+
if (options?.baseEndpoint) process.env.EXPLORBOT_URL = options.baseEndpoint;
|
|
161
|
+
if (options?.spec) process.env.EXPLORBOT_API_SPEC = options.spec;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private applyEnvSpec(api: ApiConfig): void {
|
|
165
|
+
if (!process.env.EXPLORBOT_API_SPEC) return;
|
|
166
|
+
api.spec = [process.env.EXPLORBOT_API_SPEC];
|
|
167
|
+
}
|
|
168
|
+
|
|
151
169
|
private enterGlobalMode(config: ApibotConfig, endpoint?: string): void {
|
|
152
170
|
const site = resolveSiteTarget(endpoint);
|
|
153
171
|
this.site = registerSite(site.baseUrl);
|
|
154
172
|
|
|
173
|
+
let baseEndpoint = site.baseUrl;
|
|
174
|
+
const envUrl = process.env.EXPLORBOT_URL;
|
|
175
|
+
if (envUrl && URL.parse(envUrl)?.origin === site.baseUrl) baseEndpoint = envUrl.replace(/\/$/, '');
|
|
176
|
+
|
|
155
177
|
config.dirs = { output: 'output', knowledge: 'knowledge' };
|
|
156
|
-
config.api = { ...config.api, baseEndpoint
|
|
157
|
-
|
|
178
|
+
config.api = { ...config.api, baseEndpoint };
|
|
179
|
+
materializeKnowledge(this.site.dir);
|
|
158
180
|
}
|
|
159
181
|
|
|
160
182
|
private async loadEnvConfig(): Promise<ApibotConfig> {
|
|
@@ -169,16 +191,14 @@ export class ApibotConfigParser {
|
|
|
169
191
|
|
|
170
192
|
const baseEndpoint = process.env.EXPLORBOT_URL;
|
|
171
193
|
if (!baseEndpoint) {
|
|
172
|
-
throw new Error('No API endpoint to test.
|
|
194
|
+
throw new Error('No API endpoint to test. Pass --endpoint or set EXPLORBOT_URL to the API base endpoint');
|
|
173
195
|
}
|
|
174
196
|
|
|
175
197
|
const outputRoot = resolveOutputRoot();
|
|
176
198
|
materializeKnowledge(outputRoot);
|
|
177
199
|
|
|
178
200
|
const api: ApiConfig = { baseEndpoint };
|
|
179
|
-
|
|
180
|
-
api.spec = [process.env.EXPLORBOT_API_SPEC];
|
|
181
|
-
}
|
|
201
|
+
this.applyEnvSpec(api);
|
|
182
202
|
|
|
183
203
|
let model: any;
|
|
184
204
|
if (provider && modelSpec) model = await createModel(provider, modelSpec);
|
|
@@ -246,7 +266,7 @@ export class ApibotConfigParser {
|
|
|
246
266
|
}
|
|
247
267
|
|
|
248
268
|
private mergeWithDefaults(config: Partial<ApibotConfig>): ApibotConfig {
|
|
249
|
-
return this.deepMerge({ dirs: { output: 'output' } }, config);
|
|
269
|
+
return this.deepMerge({ dirs: { output: 'output' }, api: {} }, config);
|
|
250
270
|
}
|
|
251
271
|
|
|
252
272
|
private deepMerge(target: any, source: any): any {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
2
|
+
import { knowledgeOption, wsOption } from '../../../src/commands/options/index.ts';
|
|
3
3
|
import { createDocsCommands } from '../src/cli.ts';
|
|
4
4
|
|
|
5
5
|
const program = createDocsCommands('doc-collector');
|
|
6
|
-
|
|
6
|
+
wsOption.register(program);
|
|
7
|
+
knowledgeOption.register(program);
|
|
7
8
|
program.parse();
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { ConfigCommand } from '../../../src/commands/config-command.ts';
|
|
5
|
+
import { remote } from '../../../src/remote.ts';
|
|
5
6
|
import { isVerboseMode, setPreserveConsoleLogs, setQuietMode } from '../../../src/utils/logger.ts';
|
|
6
7
|
import { DocBot, type DocbotOptions } from './docbot.ts';
|
|
7
8
|
|
|
@@ -15,6 +16,7 @@ function buildOptions(options: any): DocbotOptions {
|
|
|
15
16
|
incognito: options.incognito,
|
|
16
17
|
session: options.session,
|
|
17
18
|
docsConfig: options.docsConfig,
|
|
19
|
+
baseUrl: options.url,
|
|
18
20
|
};
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -25,6 +27,7 @@ function addCommonOptions(cmd: Command): Command {
|
|
|
25
27
|
.option('-c, --config <path>', 'Path to explorbot configuration file')
|
|
26
28
|
.option('--docs-config <path>', 'Path to doc collector configuration file')
|
|
27
29
|
.option('-p, --path <path>', 'Working directory path')
|
|
30
|
+
.option('--url <url>', 'Base URL of the site, when the path argument is relative (env: EXPLORBOT_URL)')
|
|
28
31
|
.option('-s, --show', 'Show browser window')
|
|
29
32
|
.option('--headless', 'Run browser in headless mode')
|
|
30
33
|
.option('--incognito', 'Run without recording experiences')
|
|
@@ -59,9 +62,11 @@ export function createDocsCommands(name = 'docs'): Command {
|
|
|
59
62
|
console.log(`Use in Explorbot: npx explorbot start ${startPath} --spec "${result.outputDir}"`);
|
|
60
63
|
|
|
61
64
|
await bot.stop();
|
|
65
|
+
await remote.close(0);
|
|
62
66
|
process.exit(0);
|
|
63
67
|
} catch (error) {
|
|
64
68
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
69
|
+
await remote.close(1);
|
|
65
70
|
process.exit(1);
|
|
66
71
|
}
|
|
67
72
|
});
|
|
@@ -71,7 +76,7 @@ export function createDocsCommands(name = 'docs'): Command {
|
|
|
71
76
|
.action(async (url, options) => {
|
|
72
77
|
setQuietMode(!isVerboseMode());
|
|
73
78
|
try {
|
|
74
|
-
console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
|
|
79
|
+
console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url: url || options.url, json: options.json }));
|
|
75
80
|
} catch (error) {
|
|
76
81
|
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
77
82
|
process.exit(1);
|
|
@@ -24,7 +24,7 @@ class DocBot {
|
|
|
24
24
|
|
|
25
25
|
constructor(options: DocbotOptions = {}) {
|
|
26
26
|
this.options = options;
|
|
27
|
-
const baseUrl = this.extractAbsoluteBaseUrl(options.startUrl || '/');
|
|
27
|
+
const baseUrl = this.extractAbsoluteBaseUrl(options.startUrl || '/') || options.baseUrl;
|
|
28
28
|
this.explorBot = new ExplorBot({
|
|
29
29
|
baseUrl,
|
|
30
30
|
verbose: options.verbose,
|
|
@@ -465,7 +465,9 @@ class DocBot {
|
|
|
465
465
|
private saveIndex(startPath: string, pages: DocumentedPage[], skipped: SkippedPage[], maxPages: number): { indexPath: string; diagramPath: string } {
|
|
466
466
|
const outputDir = this.configParser.getOutputDir();
|
|
467
467
|
const indexPath = path.join(outputDir, 'index.md');
|
|
468
|
-
|
|
468
|
+
const index = renderSpecIndex(outputDir, startPath, pages, skipped, maxPages);
|
|
469
|
+
writeFileSync(indexPath, index, 'utf8');
|
|
470
|
+
tag('data').log('docs', { path: indexPath, content: index });
|
|
469
471
|
const diagramPath = path.join(outputDir, 'state-diagram.mmd');
|
|
470
472
|
writeFileSync(diagramPath, renderMermaidBody(outputDir, pages), 'utf8');
|
|
471
473
|
return { indexPath, diagramPath };
|
package/boat/prima/src/cli.ts
CHANGED
|
@@ -31,9 +31,9 @@ const checkHelp = dedent`
|
|
|
31
31
|
as false.
|
|
32
32
|
Outcomes are settled against a screenshot of the whole page: what a user can see is
|
|
33
33
|
the proof, and the run log only says what was done. CONTRADICTION means the two
|
|
34
|
-
disagree - reported with both sides rather than settled one way,
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
disagree - reported with both sides rather than settled one way, so read the html,
|
|
35
|
+
aria and screenshot named under ### Artifacts and judge it yourself. Not finding
|
|
36
|
+
something in the picture is not enough on its own; that is "not verified".
|
|
37
37
|
ok: follows those outcomes - false when one FAILED or CONTRADICTED, or when the run
|
|
38
38
|
could not complete, which is reported as such rather than as an app failure.
|
|
39
39
|
Page problems seen on the way appear under ### Answer, not as step failures.
|
|
@@ -53,6 +53,13 @@ const verifyHelp = dedent`
|
|
|
53
53
|
could not be expressed, which is not the same as false.
|
|
54
54
|
`;
|
|
55
55
|
|
|
56
|
+
const statusHelp = dedent`
|
|
57
|
+
Reads the files a command recorded, so it needs no browser and outlives the session.
|
|
58
|
+
The hash is looked up across every recorded site. ### Artifacts names every file kept
|
|
59
|
+
under it: the aria tree, the html, the screenshot and network log when they were
|
|
60
|
+
captured, and the per-step captures of a do run.
|
|
61
|
+
`;
|
|
62
|
+
|
|
56
63
|
const reportHelp = dedent`
|
|
57
64
|
Commands are logged as they run, so the report needs no browser and outlives the session.
|
|
58
65
|
The most recent session is reported unless --pw-session names another.
|
|
@@ -109,6 +116,7 @@ function addCommonOptions(cmd: Command): Command {
|
|
|
109
116
|
.option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
|
|
110
117
|
.option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
|
|
111
118
|
.option('--url <url>', 'Page to open when the session has no page yet')
|
|
119
|
+
.option('--spec <path>', 'Docbot application spec directory or index.md to read as page knowledge')
|
|
112
120
|
.option('--endpoint <ep>', 'Websocket endpoint of a browser server to attach to, skipping discovery')
|
|
113
121
|
.option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
|
|
114
122
|
.addHelpText('after', `\n${sessionHelp}`);
|
|
@@ -119,10 +127,11 @@ function primaFor(options: any): Prima {
|
|
|
119
127
|
if (options.ephemeral) process.env.EXPLORBOT_EPHEMERAL = '1';
|
|
120
128
|
if (options.model) process.env.EXPLORBOT_AI_MODEL = options.model;
|
|
121
129
|
if (options.visionModel) process.env.EXPLORBOT_VISION_MODEL = options.visionModel;
|
|
130
|
+
if (options.spec) process.env.EXPLORBOT_SPEC = options.spec;
|
|
122
131
|
return new Prima(buildOptions(options));
|
|
123
132
|
}
|
|
124
133
|
|
|
125
|
-
async function runPrima(options: any, command: string, run: (prima: Prima) => Promise<EnvelopeData
|
|
134
|
+
async function runPrima(options: any, command: string, run: (prima: Prima) => Promise<EnvelopeData>): Promise<void> {
|
|
126
135
|
setQuietMode(!isVerboseMode());
|
|
127
136
|
trackActivityLine();
|
|
128
137
|
const prima = primaFor(options);
|
|
@@ -136,7 +145,7 @@ async function runPrima(options: any, command: string, run: (prima: Prima) => Pr
|
|
|
136
145
|
envelope = await prima.toolFailureEnvelope(command, error);
|
|
137
146
|
}
|
|
138
147
|
|
|
139
|
-
|
|
148
|
+
prima.record(envelope, Date.now() - startedAt);
|
|
140
149
|
clearActivityLine();
|
|
141
150
|
console.log(renderEnvelope(envelope));
|
|
142
151
|
await prima.stop().catch(() => {});
|
|
@@ -211,9 +220,15 @@ export function createPrimaCommands(name = 'prima'): Command {
|
|
|
211
220
|
process.exit(0);
|
|
212
221
|
});
|
|
213
222
|
|
|
214
|
-
addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command'))
|
|
215
|
-
|
|
216
|
-
|
|
223
|
+
addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command'))
|
|
224
|
+
.addHelpText('after', `\n${statusHelp}`)
|
|
225
|
+
.action(async (hash, options) => {
|
|
226
|
+
setQuietMode(!isVerboseMode());
|
|
227
|
+
const prima = primaFor(options);
|
|
228
|
+
const envelope = await prima.status(hash).catch((error: unknown) => prima.toolFailureEnvelope(`status ${hash}`, error));
|
|
229
|
+
console.log(renderEnvelope(envelope));
|
|
230
|
+
process.exit(envelope.ok ? 0 : 1);
|
|
231
|
+
});
|
|
217
232
|
|
|
218
233
|
addCommonOptions(cmd.command('report').description('Turn every command of a session into one html and markdown report'))
|
|
219
234
|
.addHelpText('after', `\n${reportHelp}`)
|