explorbot 0.4.0 → 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 +5 -3
- 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 +3 -1
- package/boat/doc-collector/src/docbot.ts +1 -1
- package/boat/prima/bin/prima-cli.ts +2 -0
- package/boat/prima/src/cli.ts +2 -0
- package/dist/bin/explorbot-cli.js +5 -3
- 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 +3 -1
- package/dist/boat/doc-collector/src/docbot.js +1 -1
- package/dist/boat/prima/bin/prima-cli.js +2 -0
- package/dist/boat/prima/src/cli.js +3 -0
- package/dist/package.json +1 -1
- package/dist/rules/planner/styles/normal.md +1 -1
- package/dist/src/ai/captain.js +1 -1
- package/dist/src/ai/navigator.js +1 -1
- package/dist/src/ai/planner.js +9 -7
- package/dist/src/ai/researcher.js +2 -0
- package/dist/src/ai/rules.js +3 -3
- 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.js +12 -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/config.d.ts +1 -0
- package/dist/src/config.js +11 -0
- package/dist/src/explorbot.js +1 -1
- 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/docs/api-testing/basics.md +15 -0
- package/docs/api-testing/planning.md +10 -1
- package/docs/reference/commands.md +24 -4
- package/docs/workflow/agentic-usage.md +11 -2
- package/docs/workflow/knowledge.md +46 -2
- package/package.json +1 -1
- package/rules/planner/styles/normal.md +1 -1
- package/src/ai/captain.ts +1 -1
- package/src/ai/navigator.ts +1 -1
- package/src/ai/planner.ts +9 -8
- package/src/ai/researcher.ts +1 -0
- package/src/ai/rules.ts +3 -3
- 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 +12 -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/config.ts +11 -0
- package/src/explorbot.ts +1 -1
- package/src/knowledge-tracker.ts +94 -36
- package/src/remote.ts +0 -20
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)}`);
|
|
@@ -706,7 +708,7 @@ addCommonOptions(program.command('navigate <url>').description('Navigate to a UR
|
|
|
706
708
|
});
|
|
707
709
|
|
|
708
710
|
addCommonOptions(
|
|
709
|
-
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')
|
|
710
712
|
).action(async (url, options) => {
|
|
711
713
|
try {
|
|
712
714
|
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
@@ -715,7 +717,7 @@ addCommonOptions(
|
|
|
715
717
|
await explorBot.visit(url);
|
|
716
718
|
|
|
717
719
|
const plan = await explorBot.agentDriller().drill({
|
|
718
|
-
knowledgePath: options.
|
|
720
|
+
knowledgePath: options.saveKnowledge,
|
|
719
721
|
maxComponents: Number.parseInt(options.maxComponents || '30', 10),
|
|
720
722
|
interactive: false,
|
|
721
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();
|
|
@@ -16,6 +16,7 @@ function buildOptions(options: any): DocbotOptions {
|
|
|
16
16
|
incognito: options.incognito,
|
|
17
17
|
session: options.session,
|
|
18
18
|
docsConfig: options.docsConfig,
|
|
19
|
+
baseUrl: options.url,
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -26,6 +27,7 @@ function addCommonOptions(cmd: Command): Command {
|
|
|
26
27
|
.option('-c, --config <path>', 'Path to explorbot configuration file')
|
|
27
28
|
.option('--docs-config <path>', 'Path to doc collector configuration file')
|
|
28
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)')
|
|
29
31
|
.option('-s, --show', 'Show browser window')
|
|
30
32
|
.option('--headless', 'Run browser in headless mode')
|
|
31
33
|
.option('--incognito', 'Run without recording experiences')
|
|
@@ -74,7 +76,7 @@ export function createDocsCommands(name = 'docs'): Command {
|
|
|
74
76
|
.action(async (url, options) => {
|
|
75
77
|
setQuietMode(!isVerboseMode());
|
|
76
78
|
try {
|
|
77
|
-
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 }));
|
|
78
80
|
} catch (error) {
|
|
79
81
|
console.error(error instanceof Error ? error.message : 'Unknown error');
|
|
80
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,
|
package/boat/prima/src/cli.ts
CHANGED
|
@@ -116,6 +116,7 @@ function addCommonOptions(cmd: Command): Command {
|
|
|
116
116
|
.option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
|
|
117
117
|
.option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
|
|
118
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')
|
|
119
120
|
.option('--endpoint <ep>', 'Websocket endpoint of a browser server to attach to, skipping discovery')
|
|
120
121
|
.option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
|
|
121
122
|
.addHelpText('after', `\n${sessionHelp}`);
|
|
@@ -126,6 +127,7 @@ function primaFor(options: any): Prima {
|
|
|
126
127
|
if (options.ephemeral) process.env.EXPLORBOT_EPHEMERAL = '1';
|
|
127
128
|
if (options.model) process.env.EXPLORBOT_AI_MODEL = options.model;
|
|
128
129
|
if (options.visionModel) process.env.EXPLORBOT_VISION_MODEL = options.visionModel;
|
|
130
|
+
if (options.spec) process.env.EXPLORBOT_SPEC = options.spec;
|
|
129
131
|
return new Prima(buildOptions(options));
|
|
130
132
|
}
|
|
131
133
|
|
|
@@ -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 } from '../src/explorbot.js';
|
|
15
16
|
import { remote } from '../src/remote.js';
|
|
@@ -25,7 +26,8 @@ const cli = getCliName();
|
|
|
25
26
|
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../package.json');
|
|
26
27
|
const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
|
|
27
28
|
program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
|
|
28
|
-
|
|
29
|
+
wsOption.register(program);
|
|
30
|
+
knowledgeOption.register(program);
|
|
29
31
|
process.on('uncaughtException', async (error) => {
|
|
30
32
|
tag('error').log(`Uncaught exception: ${error instanceof Error ? `${error.message}\n${error.stack}` : String(error)}`);
|
|
31
33
|
await flushTelemetry();
|
|
@@ -644,13 +646,13 @@ addCommonOptions(program.command('navigate <url>').description('Navigate to a UR
|
|
|
644
646
|
await showStatsAndExit(1);
|
|
645
647
|
}
|
|
646
648
|
});
|
|
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) => {
|
|
649
|
+
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
650
|
try {
|
|
649
651
|
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
650
652
|
await explorBot.start();
|
|
651
653
|
await explorBot.visit(url);
|
|
652
654
|
const plan = await explorBot.agentDriller().drill({
|
|
653
|
-
knowledgePath: options.
|
|
655
|
+
knowledgePath: options.saveKnowledge,
|
|
654
656
|
maxComponents: Number.parseInt(options.maxComponents || '30', 10),
|
|
655
657
|
interactive: false,
|
|
656
658
|
});
|
|
@@ -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>
|
|
@@ -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) {
|
|
@@ -12,10 +12,18 @@ function buildOptions(options) {
|
|
|
12
12
|
verbose: options.verbose || options.debug,
|
|
13
13
|
config: options.config,
|
|
14
14
|
path: options.path,
|
|
15
|
+
baseEndpoint: options.endpoint,
|
|
16
|
+
spec: options.spec,
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
function addCommonOptions(cmd) {
|
|
18
|
-
return cmd
|
|
20
|
+
return cmd
|
|
21
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
22
|
+
.option('--debug', 'Enable debug logging')
|
|
23
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
24
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
25
|
+
.option('--endpoint <url>', 'Base API endpoint to test (env: EXPLORBOT_URL)')
|
|
26
|
+
.option('--spec <path>', 'OpenAPI spec file or URL (env: EXPLORBOT_API_SPEC)');
|
|
19
27
|
}
|
|
20
28
|
function selectTests(tests, index) {
|
|
21
29
|
if (!index || index === '*' || index === 'all') {
|
|
@@ -76,8 +84,10 @@ export function createApiCommands(name = 'api') {
|
|
|
76
84
|
.action(async (endpoint, options) => {
|
|
77
85
|
const parser = ApibotConfigParser.getInstance();
|
|
78
86
|
const [site] = listSites();
|
|
87
|
+
const runOptions = buildOptions(options);
|
|
88
|
+
runOptions.endpoint = endpoint || site?.url;
|
|
79
89
|
try {
|
|
80
|
-
const config = await parser.loadConfig(
|
|
90
|
+
const config = await parser.loadConfig(runOptions);
|
|
81
91
|
console.log(ConfigCommand.render(config, { configPath: parser.getConfigPath(), root: parser.getProjectRoot(), json: options.json }));
|
|
82
92
|
}
|
|
83
93
|
catch (error) {
|
|
@@ -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 };
|