explorbot 0.4.0 → 0.4.2
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 +13 -6
- package/boat/api-tester/src/ai/chief.ts +7 -1
- package/boat/api-tester/src/ai/curler-tools.ts +1 -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 +15 -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/ai/documentarian.ts +57 -5
- package/boat/doc-collector/src/ai/tools.ts +1 -1
- package/boat/doc-collector/src/cli.ts +21 -3
- package/boat/doc-collector/src/config.ts +3 -0
- package/boat/doc-collector/src/docbot.ts +57 -12
- 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/bin/prima-cli.ts +2 -0
- package/boat/prima/src/cli.ts +5 -0
- package/boat/prima/src/prima.ts +3 -4
- package/dist/bin/explorbot-cli.js +14 -6
- 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-tools.js +1 -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 +14 -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/ai/documentarian.js +62 -4
- package/dist/boat/doc-collector/src/ai/tools.js +1 -1
- package/dist/boat/doc-collector/src/cli.js +18 -3
- package/dist/boat/doc-collector/src/config.js +1 -0
- package/dist/boat/doc-collector/src/docbot.js +53 -11
- 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/bin/prima-cli.js +2 -0
- package/dist/boat/prima/src/cli.js +5 -0
- package/dist/boat/prima/src/prima.js +3 -4
- package/dist/package.json +1 -1
- package/dist/rules/planner/styles/normal.md +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/captain.js +1 -1
- package/dist/src/ai/fisherman/request-haul.d.ts +11 -0
- package/dist/src/ai/fisherman/request-haul.js +28 -0
- package/dist/src/ai/fisherman-tools.d.ts +10 -1
- package/dist/src/ai/fisherman-tools.js +35 -31
- package/dist/src/ai/fisherman.d.ts +3 -2
- package/dist/src/ai/fisherman.js +8 -6
- package/dist/src/ai/navigator.js +1 -1
- package/dist/src/ai/pilot.js +21 -16
- package/dist/src/ai/planner.js +9 -7
- 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 +3 -1
- package/dist/src/ai/rules.js +11 -3
- 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-store.d.ts +1 -5
- package/dist/src/api/request-store.js +12 -22
- package/dist/src/api/spec-reader.js +1 -1
- package/dist/src/commands/config-command.js +3 -10
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/explore-command.js +12 -1
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/init-command.js +3 -7
- 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.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 +3 -0
- package/dist/src/config.js +18 -2
- package/dist/src/experience-tracker.js +1 -1
- package/dist/src/explorbot.js +4 -4
- package/dist/src/global-config.d.ts +2 -0
- package/dist/src/global-config.js +6 -0
- 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 +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/api-testing/basics.md +15 -0
- package/docs/api-testing/planning.md +10 -1
- package/docs/doc-collection/basics.md +11 -5
- package/docs/doc-collection/crawling.md +21 -1
- package/docs/reference/commands.md +27 -4
- package/docs/superpowers/plans/2026-09-03-model-resolves-click-ambiguity.md +509 -0
- package/docs/workflow/agentic-usage.md +11 -2
- package/docs/workflow/application-spec.md +4 -0
- package/docs/workflow/knowledge.md +46 -2
- package/package.json +1 -1
- package/rules/planner/styles/normal.md +1 -1
- package/src/action-result.ts +15 -16
- package/src/action.ts +16 -15
- package/src/ai/captain.ts +1 -1
- package/src/ai/fisherman/request-haul.ts +32 -0
- package/src/ai/fisherman-tools.ts +38 -32
- package/src/ai/fisherman.ts +8 -7
- package/src/ai/navigator.ts +1 -1
- package/src/ai/pilot.ts +19 -14
- package/src/ai/planner.ts +9 -8
- 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 +2 -1
- package/src/ai/rules.ts +11 -3
- package/src/ai/tester.ts +18 -18
- package/src/ai/tools.ts +18 -86
- package/src/api/request-store.ts +13 -27
- package/src/api/spec-reader.ts +1 -1
- package/src/commands/config-command.ts +3 -8
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/explore-command.ts +12 -1
- package/src/commands/index.ts +2 -0
- package/src/commands/init-command.ts +4 -11
- 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 +5 -5
- package/src/commands/recommended-models-command.ts +92 -0
- package/src/config.ts +20 -2
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +4 -4
- package/src/global-config.ts +8 -0
- package/src/knowledge-tracker.ts +94 -36
- package/src/remote.ts +0 -20
- 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/utils/request-map.d.ts +0 -7
- package/dist/src/utils/request-map.js +0 -16
- package/src/utils/request-map.ts +0 -19
|
@@ -8,8 +8,10 @@ import figureSet from 'figures';
|
|
|
8
8
|
import { render } from 'ink';
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import { flushTelemetry } from '../src/ai/provider.js';
|
|
11
|
+
import { RecommendedModelsCommand } from '../src/commands/recommended-models-command.js';
|
|
11
12
|
import { App } from '../src/components/App.js';
|
|
12
13
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
14
|
+
import { knowledgeOption, wsOption } from '../src/commands/options/index.js';
|
|
13
15
|
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
14
16
|
import { ExplorBot } from '../src/explorbot.js';
|
|
15
17
|
import { remote } from '../src/remote.js';
|
|
@@ -25,7 +27,8 @@ const cli = getCliName();
|
|
|
25
27
|
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../package.json');
|
|
26
28
|
const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
|
|
27
29
|
program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
|
|
28
|
-
|
|
30
|
+
wsOption.register(program);
|
|
31
|
+
knowledgeOption.register(program);
|
|
29
32
|
process.on('uncaughtException', async (error) => {
|
|
30
33
|
tag('error').log(`Uncaught exception: ${error instanceof Error ? `${error.message}\n${error.stack}` : String(error)}`);
|
|
31
34
|
await flushTelemetry();
|
|
@@ -294,10 +297,14 @@ addCommonOptions(program.command('test <planfile> [index]').description('Execute
|
|
|
294
297
|
planfileArg = options.fromPlan;
|
|
295
298
|
indexArg = planfile;
|
|
296
299
|
}
|
|
297
|
-
const
|
|
298
|
-
const explorBot = new ExplorBot(buildExplorBotOptions(
|
|
300
|
+
const peeked = Plan.loadFromFile(planfileArg);
|
|
301
|
+
const explorBot = new ExplorBot(buildExplorBotOptions(peeked?.startUrl, options));
|
|
299
302
|
await explorBot.start();
|
|
300
|
-
|
|
303
|
+
let plan = peeked;
|
|
304
|
+
if (plan)
|
|
305
|
+
explorBot.setCurrentPlan(plan);
|
|
306
|
+
if (!plan)
|
|
307
|
+
plan = explorBot.loadPlan(planfileArg);
|
|
301
308
|
const pending = plan.getPendingTests();
|
|
302
309
|
log(`Plan loaded: "${plan.title}" (${plan.tests.length} tests, ${pending.length} pending)`);
|
|
303
310
|
const startUrl = plan.startUrl;
|
|
@@ -418,6 +425,7 @@ program
|
|
|
418
425
|
process.exit(1);
|
|
419
426
|
}
|
|
420
427
|
});
|
|
428
|
+
RecommendedModelsCommand.register(program);
|
|
421
429
|
program
|
|
422
430
|
.command('init')
|
|
423
431
|
.description('Initialize configuration for a project or for this machine')
|
|
@@ -644,13 +652,13 @@ addCommonOptions(program.command('navigate <url>').description('Navigate to a UR
|
|
|
644
652
|
await showStatsAndExit(1);
|
|
645
653
|
}
|
|
646
654
|
});
|
|
647
|
-
addCommonOptions(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')).action(async (url, options) => {
|
|
655
|
+
addCommonOptions(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')).action(async (url, options) => {
|
|
648
656
|
try {
|
|
649
657
|
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
650
658
|
await explorBot.start();
|
|
651
659
|
await explorBot.visit(url);
|
|
652
660
|
const plan = await explorBot.agentDriller().drill({
|
|
653
|
-
knowledgePath: options.
|
|
661
|
+
knowledgePath: options.saveKnowledge,
|
|
654
662
|
maxComponents: Number.parseInt(options.maxComponents || '30', 10),
|
|
655
663
|
interactive: false,
|
|
656
664
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
2
|
+
import { knowledgeOption, wsOption } from "../../../src/commands/options/index.js";
|
|
3
3
|
import { createApiCommands } from "../src/cli.js";
|
|
4
4
|
const program = createApiCommands('apibot');
|
|
5
|
-
|
|
5
|
+
wsOption.register(program);
|
|
6
|
+
knowledgeOption.register(program);
|
|
6
7
|
program.parse();
|
|
@@ -24,15 +24,17 @@ export class Chief extends ChiefBase {
|
|
|
24
24
|
provider;
|
|
25
25
|
config;
|
|
26
26
|
apiClient;
|
|
27
|
+
knowledgeTracker;
|
|
27
28
|
currentPlan = null;
|
|
28
29
|
lastStyleName = '';
|
|
29
30
|
MIN_TASKS = 3;
|
|
30
31
|
MAX_TASKS = 10;
|
|
31
|
-
constructor(provider, config, apiClient) {
|
|
32
|
+
constructor(provider, config, apiClient, knowledgeTracker) {
|
|
32
33
|
super();
|
|
33
34
|
this.provider = provider;
|
|
34
35
|
this.config = config;
|
|
35
36
|
this.apiClient = apiClient || null;
|
|
37
|
+
this.knowledgeTracker = knowledgeTracker;
|
|
36
38
|
}
|
|
37
39
|
async plan(endpoint, opts) {
|
|
38
40
|
tag('info').log(`Planning API tests for ${endpoint}`);
|
|
@@ -42,6 +44,9 @@ export class Chief extends ChiefBase {
|
|
|
42
44
|
await Observability.run(`chief: ${endpoint}`, { tags: ['chief'], sessionId: endpoint }, async () => {
|
|
43
45
|
const sampleData = await this.collectSampleData(endpoint);
|
|
44
46
|
const conversation = this.buildConversation(endpoint, opts?.style, sampleData);
|
|
47
|
+
const knowledge = this.knowledgeTracker?.renderEndpointKnowledge(endpoint);
|
|
48
|
+
if (knowledge)
|
|
49
|
+
conversation.addUserText(knowledge);
|
|
45
50
|
if (opts?.specDefinition) {
|
|
46
51
|
conversation.addUserText(dedent `
|
|
47
52
|
<api_spec>
|
|
@@ -61,7 +61,7 @@ export function createCurlerTools(apiClient, requestState, test, searchSpec) {
|
|
|
61
61
|
body: input.body,
|
|
62
62
|
queryParams: input.queryParams,
|
|
63
63
|
});
|
|
64
|
-
requestState.
|
|
64
|
+
requestState.addMadeRequest(result);
|
|
65
65
|
if (result.error) {
|
|
66
66
|
tag('error').log(`${input.method} ${input.path} > Network error: ${result.error}`);
|
|
67
67
|
activeNote.commit(TestResult.FAILED);
|
|
@@ -11,11 +11,13 @@ export class Curler {
|
|
|
11
11
|
apiClient;
|
|
12
12
|
requestState;
|
|
13
13
|
reporter;
|
|
14
|
-
|
|
14
|
+
knowledgeTracker;
|
|
15
|
+
constructor(provider, apiClient, requestState, reporter, knowledgeTracker) {
|
|
15
16
|
this.provider = provider;
|
|
16
17
|
this.apiClient = apiClient;
|
|
17
18
|
this.requestState = requestState;
|
|
18
19
|
this.reporter = reporter;
|
|
20
|
+
this.knowledgeTracker = knowledgeTracker;
|
|
19
21
|
}
|
|
20
22
|
async test(test, opts) {
|
|
21
23
|
tag('info').log(`Testing: ${test.scenario}`);
|
|
@@ -25,6 +27,9 @@ export class Curler {
|
|
|
25
27
|
await this.reporter.reportTestStart(test);
|
|
26
28
|
const conversation = this.provider.startConversation(this.buildSystemPrompt(), 'curler', this.provider.getAgenticModel('curler'));
|
|
27
29
|
const tools = createCurlerTools(this.apiClient, this.requestState, test, opts?.searchSpec);
|
|
30
|
+
const knowledge = test.startUrl && this.knowledgeTracker?.renderEndpointKnowledge(test.startUrl);
|
|
31
|
+
if (knowledge)
|
|
32
|
+
conversation.addUserText(knowledge);
|
|
28
33
|
const initialPrompt = this.buildTestPrompt(test, opts?.specDefinition, opts?.baseEndpoint);
|
|
29
34
|
conversation.addUserText(initialPrompt);
|
|
30
35
|
await loop(async ({ stop, iteration }) => {
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { AIProvider } from "../../../src/ai/provider.js";
|
|
4
4
|
import { RequestStore } from "../../../src/api/request-store.js";
|
|
5
5
|
import { extractEndpointDefinition, loadSpec, searchEndpoints, validateSpecs } from "../../../src/api/spec-reader.js";
|
|
6
|
+
import { KnowledgeTracker } from "../../../src/knowledge-tracker.js";
|
|
6
7
|
import { Reporter } from "../../../src/reporter.js";
|
|
7
8
|
import { Plan } from "../../../src/test-plan.js";
|
|
8
9
|
import { setVerboseMode, tag } from "../../../src/utils/logger.js";
|
|
@@ -19,6 +20,7 @@ export class ApiBot {
|
|
|
19
20
|
apiClient;
|
|
20
21
|
requestState;
|
|
21
22
|
reporter;
|
|
23
|
+
knowledgeTracker;
|
|
22
24
|
options;
|
|
23
25
|
apiSpec;
|
|
24
26
|
constructor(options = {}) {
|
|
@@ -30,7 +32,7 @@ export class ApiBot {
|
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
async start() {
|
|
33
|
-
this.config = await this.configParser.loadConfig(
|
|
35
|
+
this.config = await this.configParser.loadConfig(this.options);
|
|
34
36
|
this.provider = new AIProvider(this.config.ai);
|
|
35
37
|
await this.provider.validateConnection();
|
|
36
38
|
this.apiClient = new ApiClient(this.config.api.baseEndpoint, this.config.api.headers || {}, {
|
|
@@ -42,6 +44,7 @@ export class ApiBot {
|
|
|
42
44
|
this.configParser.ensureDirectory(outputDir);
|
|
43
45
|
this.requestState = new RequestStore(outputDir);
|
|
44
46
|
this.reporter = new Reporter(this.config.reporter);
|
|
47
|
+
this.knowledgeTracker = new KnowledgeTracker({ knowledgeDir: this.configParser.getKnowledgeDir() });
|
|
45
48
|
validateSpecs(this.config.api.spec);
|
|
46
49
|
this.apiSpec = await loadSpec(this.config.api.spec, outputDir);
|
|
47
50
|
tag('info').log('OpenAPI spec loaded');
|
|
@@ -78,13 +81,14 @@ export class ApiBot {
|
|
|
78
81
|
config: this.config,
|
|
79
82
|
apiClient: this.apiClient,
|
|
80
83
|
requestState: this.requestState,
|
|
84
|
+
knowledge: this.knowledgeTracker,
|
|
81
85
|
});
|
|
82
86
|
}
|
|
83
87
|
agentChief() {
|
|
84
|
-
return (this.agents.chief ||= this.createAgent(({ ai, config, apiClient }) => new Chief(ai, config, apiClient)));
|
|
88
|
+
return (this.agents.chief ||= this.createAgent(({ ai, config, apiClient, knowledge }) => new Chief(ai, config, apiClient, knowledge)));
|
|
85
89
|
}
|
|
86
90
|
agentCurler() {
|
|
87
|
-
return (this.agents.curler ||= this.createAgent(({ ai, apiClient, requestState }) => new Curler(ai, apiClient, requestState, this.reporter)));
|
|
91
|
+
return (this.agents.curler ||= this.createAgent(({ ai, apiClient, requestState, knowledge }) => new Curler(ai, apiClient, requestState, this.reporter, knowledge)));
|
|
88
92
|
}
|
|
89
93
|
async plan(target, opts = {}) {
|
|
90
94
|
if (opts.fresh) {
|
|
@@ -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.js";
|
|
5
|
+
import { RecommendedModelsCommand } from "../../../src/commands/recommended-models-command.js";
|
|
5
6
|
import { listSites } from "../../../src/global-config.js";
|
|
6
7
|
import { setPreserveConsoleLogs } from "../../../src/utils/logger.js";
|
|
7
8
|
import { getStyles } from "./ai/chief/styles.js";
|
|
@@ -12,10 +13,18 @@ function buildOptions(options) {
|
|
|
12
13
|
verbose: options.verbose || options.debug,
|
|
13
14
|
config: options.config,
|
|
14
15
|
path: options.path,
|
|
16
|
+
baseEndpoint: options.endpoint,
|
|
17
|
+
spec: options.spec,
|
|
15
18
|
};
|
|
16
19
|
}
|
|
17
20
|
function addCommonOptions(cmd) {
|
|
18
|
-
return cmd
|
|
21
|
+
return cmd
|
|
22
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
23
|
+
.option('--debug', 'Enable debug logging')
|
|
24
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
25
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
26
|
+
.option('--endpoint <url>', 'Base API endpoint to test (env: EXPLORBOT_URL)')
|
|
27
|
+
.option('--spec <path>', 'OpenAPI spec file or URL (env: EXPLORBOT_API_SPEC)');
|
|
19
28
|
}
|
|
20
29
|
function selectTests(tests, index) {
|
|
21
30
|
if (!index || index === '*' || index === 'all') {
|
|
@@ -76,8 +85,10 @@ export function createApiCommands(name = 'api') {
|
|
|
76
85
|
.action(async (endpoint, options) => {
|
|
77
86
|
const parser = ApibotConfigParser.getInstance();
|
|
78
87
|
const [site] = listSites();
|
|
88
|
+
const runOptions = buildOptions(options);
|
|
89
|
+
runOptions.endpoint = endpoint || site?.url;
|
|
79
90
|
try {
|
|
80
|
-
const config = await parser.loadConfig(
|
|
91
|
+
const config = await parser.loadConfig(runOptions);
|
|
81
92
|
console.log(ConfigCommand.render(config, { configPath: parser.getConfigPath(), root: parser.getProjectRoot(), json: options.json }));
|
|
82
93
|
}
|
|
83
94
|
catch (error) {
|
|
@@ -85,6 +96,7 @@ export function createApiCommands(name = 'api') {
|
|
|
85
96
|
process.exit(1);
|
|
86
97
|
}
|
|
87
98
|
});
|
|
99
|
+
RecommendedModelsCommand.register(cmd);
|
|
88
100
|
addCommonOptions(cmd.command('test <planfile> [index]').description('Execute tests from a plan file. Index: 1, 1-3, *')).action(async (planfile, index, options) => {
|
|
89
101
|
setPreserveConsoleLogs(true);
|
|
90
102
|
try {
|
|
@@ -39,6 +39,7 @@ export class ApibotConfigParser {
|
|
|
39
39
|
}
|
|
40
40
|
ApibotConfigParser.loadEnv(globalEnvPath());
|
|
41
41
|
ApibotConfigParser.loadEnv('.env');
|
|
42
|
+
this.applyRunOptions(options);
|
|
42
43
|
const resolvedPath = options?.config || this.findConfigFile();
|
|
43
44
|
if (!resolvedPath) {
|
|
44
45
|
try {
|
|
@@ -63,6 +64,9 @@ export class ApibotConfigParser {
|
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
66
|
this.config = this.mergeWithDefaults(loadedConfig);
|
|
67
|
+
this.applyEnvSpec(this.config.api);
|
|
68
|
+
if (options?.baseEndpoint)
|
|
69
|
+
this.config.api.baseEndpoint = options.baseEndpoint.replace(/\/$/, '');
|
|
66
70
|
await resolveConfigModels(this.config.ai);
|
|
67
71
|
this.configPath = resolvedPath;
|
|
68
72
|
this.site = null;
|
|
@@ -104,6 +108,13 @@ export class ApibotConfigParser {
|
|
|
104
108
|
const resolved = resolveSiteTarget(endpoint, this.site.url);
|
|
105
109
|
if (resolved.baseUrl !== this.site.url)
|
|
106
110
|
return endpoint;
|
|
111
|
+
const basePath = new URL(this.getConfig().api.baseEndpoint).pathname.replace(/\/$/, '');
|
|
112
|
+
if (!basePath)
|
|
113
|
+
return resolved.path;
|
|
114
|
+
if (resolved.path === basePath)
|
|
115
|
+
return '/';
|
|
116
|
+
if (resolved.path.startsWith(`${basePath}/`))
|
|
117
|
+
return resolved.path.slice(basePath.length);
|
|
107
118
|
return resolved.path;
|
|
108
119
|
}
|
|
109
120
|
getPlansDir() {
|
|
@@ -121,13 +132,27 @@ export class ApibotConfigParser {
|
|
|
121
132
|
mkdirSync(dirPath, { recursive: true });
|
|
122
133
|
}
|
|
123
134
|
}
|
|
135
|
+
applyRunOptions(options) {
|
|
136
|
+
if (options?.baseEndpoint)
|
|
137
|
+
process.env.EXPLORBOT_URL = options.baseEndpoint;
|
|
138
|
+
if (options?.spec)
|
|
139
|
+
process.env.EXPLORBOT_API_SPEC = options.spec;
|
|
140
|
+
}
|
|
141
|
+
applyEnvSpec(api) {
|
|
142
|
+
if (!process.env.EXPLORBOT_API_SPEC)
|
|
143
|
+
return;
|
|
144
|
+
api.spec = [process.env.EXPLORBOT_API_SPEC];
|
|
145
|
+
}
|
|
124
146
|
enterGlobalMode(config, endpoint) {
|
|
125
147
|
const site = resolveSiteTarget(endpoint);
|
|
126
148
|
this.site = registerSite(site.baseUrl);
|
|
149
|
+
let baseEndpoint = site.baseUrl;
|
|
150
|
+
const envUrl = process.env.EXPLORBOT_URL;
|
|
151
|
+
if (envUrl && URL.parse(envUrl)?.origin === site.baseUrl)
|
|
152
|
+
baseEndpoint = envUrl.replace(/\/$/, '');
|
|
127
153
|
config.dirs = { output: 'output', knowledge: 'knowledge' };
|
|
128
|
-
config.api = { ...config.api, baseEndpoint
|
|
129
|
-
|
|
130
|
-
config.api.spec = [process.env.EXPLORBOT_API_SPEC];
|
|
154
|
+
config.api = { ...config.api, baseEndpoint };
|
|
155
|
+
materializeKnowledge(this.site.dir);
|
|
131
156
|
}
|
|
132
157
|
async loadEnvConfig() {
|
|
133
158
|
const provider = process.env.EXPLORBOT_AI_PROVIDER;
|
|
@@ -140,14 +165,12 @@ export class ApibotConfigParser {
|
|
|
140
165
|
}
|
|
141
166
|
const baseEndpoint = process.env.EXPLORBOT_URL;
|
|
142
167
|
if (!baseEndpoint) {
|
|
143
|
-
throw new Error('No API endpoint to test.
|
|
168
|
+
throw new Error('No API endpoint to test. Pass --endpoint or set EXPLORBOT_URL to the API base endpoint');
|
|
144
169
|
}
|
|
145
170
|
const outputRoot = resolveOutputRoot();
|
|
146
171
|
materializeKnowledge(outputRoot);
|
|
147
172
|
const api = { baseEndpoint };
|
|
148
|
-
|
|
149
|
-
api.spec = [process.env.EXPLORBOT_API_SPEC];
|
|
150
|
-
}
|
|
173
|
+
this.applyEnvSpec(api);
|
|
151
174
|
let model;
|
|
152
175
|
if (provider && modelSpec)
|
|
153
176
|
model = await createModel(provider, modelSpec);
|
|
@@ -210,7 +233,7 @@ export class ApibotConfigParser {
|
|
|
210
233
|
}
|
|
211
234
|
}
|
|
212
235
|
mergeWithDefaults(config) {
|
|
213
|
-
return this.deepMerge({ dirs: { output: 'output' } }, config);
|
|
236
|
+
return this.deepMerge({ dirs: { output: 'output' }, api: {} }, config);
|
|
214
237
|
}
|
|
215
238
|
deepMerge(target, source) {
|
|
216
239
|
const result = { ...target };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
2
|
+
import { knowledgeOption, wsOption } from "../../../src/commands/options/index.js";
|
|
3
3
|
import { createDocsCommands } from "../src/cli.js";
|
|
4
4
|
const program = createDocsCommands('doc-collector');
|
|
5
|
-
|
|
5
|
+
wsOption.register(program);
|
|
6
|
+
knowledgeOption.register(program);
|
|
6
7
|
program.parse();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { parseAriaLocator } from "../../../../src/utils/aria.js";
|
|
3
4
|
import { tag } from "../../../../src/utils/logger.js";
|
|
5
|
+
import { parseResearchSections } from "../../../../src/ai/researcher/parser.js";
|
|
4
6
|
import { collectDocInteractions } from "./tools.js";
|
|
5
7
|
class Documentarian {
|
|
6
8
|
provider;
|
|
@@ -147,6 +149,9 @@ class Documentarian {
|
|
|
147
149
|
Only list capabilities that are grounded in the provided page research.
|
|
148
150
|
Put actions into "can" only when there is direct evidence in the page context.
|
|
149
151
|
Put actions into "might" only when the UI strongly suggests a capability but proof is incomplete.
|
|
152
|
+
Going to another page is navigation, not a capability. "can" and "might" list only actions the user performs on this page; links and menus are documented in a separate navigation section, never here.
|
|
153
|
+
Raw interaction observations that change the URL are navigation evidence only. Never turn them into "can" or "might" actions.
|
|
154
|
+
Exclude capabilities supplied by unrelated embedded support, marketing, consent, or feedback widgets. Include an embedded interface only when it is part of the current page's documented purpose.
|
|
150
155
|
Describe each action from the end-user perspective.
|
|
151
156
|
Be explicit about scope:
|
|
152
157
|
- one item
|
|
@@ -154,7 +159,7 @@ class Documentarian {
|
|
|
154
159
|
- bulk operations
|
|
155
160
|
- all items
|
|
156
161
|
- page-level
|
|
157
|
-
Avoid implementation details, selectors, and QA wording.
|
|
162
|
+
Avoid implementation details, selectors, and QA wording — except the machine-consumed element field, which is a locator, not prose.
|
|
158
163
|
Avoid duplicate actions with different phrasing.
|
|
159
164
|
</rules>
|
|
160
165
|
|
|
@@ -203,6 +208,8 @@ class Documentarian {
|
|
|
203
208
|
- action: concise user-facing capability phrased as "user can ..."
|
|
204
209
|
- scope: one of one item, list of items, bulk operations, all items, page-level
|
|
205
210
|
- evidence: short reason based on visible UI or research
|
|
211
|
+
- element: required for every "can" and "might" action. Return the proving or suggesting control's locator copied verbatim from the page research table — the value from the ARIA column, or from the CSS column when the ARIA column is empty. Return null only when no single element supports the action. It is machine metadata used for validation and never shown as prose.
|
|
212
|
+
Never copy the human-readable Element description from interaction_observations into element. It is not a locator.
|
|
206
213
|
</output_requirements>
|
|
207
214
|
`;
|
|
208
215
|
}
|
|
@@ -233,8 +240,37 @@ class Documentarian {
|
|
|
233
240
|
const message = error instanceof Error ? error.message : String(error);
|
|
234
241
|
return message.includes('Failed to generate JSON') || message.includes('Failed to validate JSON') || message.includes('failed_generation') || message.includes('No object generated') || message.includes('response did not match schema');
|
|
235
242
|
}
|
|
236
|
-
normalizeDocumentation(documentation, _state,
|
|
243
|
+
normalizeDocumentation(documentation, _state, research) {
|
|
237
244
|
const normalized = { ...documentation };
|
|
245
|
+
const navigationLocators = new Set();
|
|
246
|
+
const researchLocators = new Set();
|
|
247
|
+
for (const section of parseResearchSections(research)) {
|
|
248
|
+
for (const element of section.elements) {
|
|
249
|
+
const aria = element.aria ? `{ role: '${element.aria.role}', text: '${element.aria.text}' }` : null;
|
|
250
|
+
if (aria)
|
|
251
|
+
researchLocators.add(this.locatorKey(aria));
|
|
252
|
+
if (element.css)
|
|
253
|
+
researchLocators.add(this.locatorKey(element.css));
|
|
254
|
+
if (element.aria?.role !== 'link')
|
|
255
|
+
continue;
|
|
256
|
+
if (aria)
|
|
257
|
+
navigationLocators.add(this.locatorKey(aria));
|
|
258
|
+
if (element.css)
|
|
259
|
+
navigationLocators.add(this.locatorKey(element.css));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
normalized.can = normalized.can.filter((capability) => {
|
|
263
|
+
if (!capability.element)
|
|
264
|
+
return true;
|
|
265
|
+
const key = this.locatorKey(capability.element);
|
|
266
|
+
return researchLocators.has(key) && !navigationLocators.has(key);
|
|
267
|
+
});
|
|
268
|
+
normalized.might = normalized.might.filter((capability) => {
|
|
269
|
+
if (!capability.element)
|
|
270
|
+
return false;
|
|
271
|
+
const key = this.locatorKey(capability.element);
|
|
272
|
+
return researchLocators.has(key) && !navigationLocators.has(key);
|
|
273
|
+
});
|
|
238
274
|
if (!normalized.interactions) {
|
|
239
275
|
normalized.interactions = undefined;
|
|
240
276
|
}
|
|
@@ -244,6 +280,18 @@ class Documentarian {
|
|
|
244
280
|
qualityNotes,
|
|
245
281
|
});
|
|
246
282
|
}
|
|
283
|
+
locatorKey(locator) {
|
|
284
|
+
const aria = parseAriaLocator(locator);
|
|
285
|
+
if (aria)
|
|
286
|
+
return `aria:${aria.role}:${aria.text}`;
|
|
287
|
+
const trimmed = locator.trim();
|
|
288
|
+
const first = trimmed.at(0);
|
|
289
|
+
const last = trimmed.at(-1);
|
|
290
|
+
if (first === last && (first === '`' || first === '"' || first === "'")) {
|
|
291
|
+
return `css:${trimmed.slice(1, -1).trim()}`;
|
|
292
|
+
}
|
|
293
|
+
return `css:${trimmed}`;
|
|
294
|
+
}
|
|
247
295
|
evaluateDocumentationQuality(documentation) {
|
|
248
296
|
const notes = [];
|
|
249
297
|
if ((documentation.interactions || []).length === 0 && this.config.docs?.interactive) {
|
|
@@ -300,6 +348,13 @@ const capabilitySchema = z.object({
|
|
|
300
348
|
action: z.string(),
|
|
301
349
|
scope: z.enum(['one item', 'list of items', 'bulk operations', 'all items', 'page-level']),
|
|
302
350
|
evidence: z.string(),
|
|
351
|
+
element: z.string().nullable().optional(),
|
|
352
|
+
});
|
|
353
|
+
const generatedCapabilitySchema = z.object({
|
|
354
|
+
action: z.string(),
|
|
355
|
+
scope: z.enum(['one item', 'list of items', 'bulk operations', 'all items', 'page-level']),
|
|
356
|
+
evidence: z.string(),
|
|
357
|
+
element: z.string().nullable(),
|
|
303
358
|
});
|
|
304
359
|
const stateTransitionSchema = z.object({
|
|
305
360
|
action: z.string(),
|
|
@@ -339,11 +394,14 @@ const stateTransitionSchema = z.object({
|
|
|
339
394
|
.optional(),
|
|
340
395
|
});
|
|
341
396
|
const generatedPageDocumentationSchema = z.object({
|
|
397
|
+
summary: z.string(),
|
|
398
|
+
can: z.array(generatedCapabilitySchema),
|
|
399
|
+
might: z.array(generatedCapabilitySchema),
|
|
400
|
+
});
|
|
401
|
+
const pageDocumentationSchema = z.object({
|
|
342
402
|
summary: z.string(),
|
|
343
403
|
can: z.array(capabilitySchema),
|
|
344
404
|
might: z.array(capabilitySchema),
|
|
345
|
-
});
|
|
346
|
-
const pageDocumentationSchema = generatedPageDocumentationSchema.extend({
|
|
347
405
|
interactions: z.array(stateTransitionSchema).optional(),
|
|
348
406
|
qualityNotes: z.array(z.string()).optional(),
|
|
349
407
|
});
|
|
@@ -132,7 +132,7 @@ function buildTransition(candidate, beforeState, afterState, changes) {
|
|
|
132
132
|
function describeTargetState(beforeState, afterState, candidate) {
|
|
133
133
|
const beforeFocus = detectFocusArea(beforeState.ariaSnapshot || null);
|
|
134
134
|
const afterFocus = detectFocusArea(afterState.ariaSnapshot || null);
|
|
135
|
-
if (afterFocus.
|
|
135
|
+
if (afterFocus.type && (!beforeFocus.type || beforeFocus.name !== afterFocus.name)) {
|
|
136
136
|
return {
|
|
137
137
|
kind: afterFocus.type || 'dialog',
|
|
138
138
|
label: afterFocus.name || candidate.element.name.trim(),
|
|
@@ -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.js";
|
|
5
|
+
import { RecommendedModelsCommand } from "../../../src/commands/recommended-models-command.js";
|
|
5
6
|
import { remote } from "../../../src/remote.js";
|
|
6
7
|
import { isVerboseMode, setPreserveConsoleLogs, setQuietMode } from "../../../src/utils/logger.js";
|
|
7
8
|
import { DocBot } from "./docbot.js";
|
|
@@ -15,6 +16,7 @@ function buildOptions(options) {
|
|
|
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
|
function addCommonOptions(cmd) {
|
|
@@ -24,6 +26,7 @@ function addCommonOptions(cmd) {
|
|
|
24
26
|
.option('-c, --config <path>', 'Path to explorbot configuration file')
|
|
25
27
|
.option('--docs-config <path>', 'Path to doc collector configuration file')
|
|
26
28
|
.option('-p, --path <path>', 'Working directory path')
|
|
29
|
+
.option('--url <url>', 'Base URL of the site, when the path argument is relative (env: EXPLORBOT_URL)')
|
|
27
30
|
.option('-s, --show', 'Show browser window')
|
|
28
31
|
.option('--headless', 'Run browser in headless mode')
|
|
29
32
|
.option('--incognito', 'Run without recording experiences')
|
|
@@ -32,7 +35,12 @@ function addCommonOptions(cmd) {
|
|
|
32
35
|
export function createDocsCommands(name = 'docs') {
|
|
33
36
|
const cmd = new Command(name);
|
|
34
37
|
cmd.description('AI-powered website documentation collector');
|
|
35
|
-
addCommonOptions(cmd
|
|
38
|
+
addCommonOptions(cmd
|
|
39
|
+
.command('collect <path>')
|
|
40
|
+
.description('Crawl pages and generate documentation spec')
|
|
41
|
+
.option('--max-pages <count>', 'Maximum number of pages to document')
|
|
42
|
+
.option('--no-collapse-template-pages', 'Visit every page even when its layout matches a documented page')
|
|
43
|
+
.option('--template-similarity <percent>', 'Structural similarity percent that counts pages as the same layout (default 90)')).action(async (startPath, options) => {
|
|
36
44
|
setPreserveConsoleLogs(true);
|
|
37
45
|
try {
|
|
38
46
|
const bot = new DocBot({
|
|
@@ -44,7 +52,11 @@ export function createDocsCommands(name = 'docs') {
|
|
|
44
52
|
if (options.maxPages) {
|
|
45
53
|
maxPages = Number.parseInt(options.maxPages, 10);
|
|
46
54
|
}
|
|
47
|
-
|
|
55
|
+
let templateSimilarity;
|
|
56
|
+
if (options.templateSimilarity) {
|
|
57
|
+
templateSimilarity = Number.parseInt(options.templateSimilarity, 10);
|
|
58
|
+
}
|
|
59
|
+
const result = await bot.collect(startPath, { maxPages, collapseTemplatePages: options.collapseTemplatePages, templateSimilarity });
|
|
48
60
|
console.log(`\nDocumented ${result.pages.length} page(s)`);
|
|
49
61
|
console.log(`Skipped ${result.skipped.length} page(s)`);
|
|
50
62
|
console.log(`Spec index: ${result.indexPath}`);
|
|
@@ -65,13 +77,14 @@ export function createDocsCommands(name = 'docs') {
|
|
|
65
77
|
.action(async (url, options) => {
|
|
66
78
|
setQuietMode(!isVerboseMode());
|
|
67
79
|
try {
|
|
68
|
-
console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
|
|
80
|
+
console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url: url || options.url, json: options.json }));
|
|
69
81
|
}
|
|
70
82
|
catch (error) {
|
|
71
83
|
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
72
84
|
process.exit(1);
|
|
73
85
|
}
|
|
74
86
|
});
|
|
87
|
+
RecommendedModelsCommand.register(cmd);
|
|
75
88
|
cmd
|
|
76
89
|
.command('init')
|
|
77
90
|
.description('Initialize doc collector configuration')
|
|
@@ -99,6 +112,8 @@ export function createDocsCommands(name = 'docs') {
|
|
|
99
112
|
interactive: false,
|
|
100
113
|
ignoreErrors: true,
|
|
101
114
|
collapseDynamicPages: true,
|
|
115
|
+
collapseTemplatePages: true,
|
|
116
|
+
templateSimilarity: 90,
|
|
102
117
|
scope: 'site',
|
|
103
118
|
includePaths: [],
|
|
104
119
|
excludePaths: [],
|