explorbot 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/explorbot-cli.ts +52 -37
- package/boat/api-tester/src/apibot.ts +4 -2
- package/boat/api-tester/src/cli.ts +2 -2
- package/boat/api-tester/src/config.ts +39 -8
- package/boat/doc-collector/src/ai/documentarian.ts +3 -0
- package/boat/doc-collector/src/ai/tools.ts +17 -5
- package/boat/doc-collector/src/cli.ts +2 -0
- package/boat/doc-collector/src/config.ts +2 -0
- package/boat/doc-collector/src/docbot.ts +39 -11
- package/boat/doc-collector/src/docs-renderer.ts +18 -4
- package/boat/doc-collector/src/interaction-screenshots.ts +160 -0
- package/boat/doc-collector/src/screenshots.ts +22 -14
- package/boat/doc-collector/src/state-diagram.ts +61 -14
- package/boat/prima/bin/prima-cli.ts +5 -0
- package/boat/prima/package.json +16 -0
- package/boat/prima/src/cli.ts +222 -0
- package/boat/prima/src/envelope.ts +141 -0
- package/boat/prima/src/prima.ts +705 -0
- package/boat/prima/src/pw-parser.ts +17 -0
- package/boat/prima/src/pw-registry.ts +75 -0
- package/dist/bin/explorbot-cli.js +44 -31
- package/dist/boat/api-tester/src/apibot.js +3 -2
- package/dist/boat/api-tester/src/cli.js +2 -2
- package/dist/boat/api-tester/src/config.js +36 -8
- package/dist/boat/doc-collector/src/ai/documentarian.js +3 -0
- package/dist/boat/doc-collector/src/ai/tools.js +10 -4
- package/dist/boat/doc-collector/src/cli.js +2 -0
- package/dist/boat/doc-collector/src/config.js +1 -0
- package/dist/boat/doc-collector/src/docbot.js +36 -10
- package/dist/boat/doc-collector/src/docs-renderer.js +17 -3
- package/dist/boat/doc-collector/src/interaction-screenshots.js +156 -0
- package/dist/boat/doc-collector/src/screenshots.js +23 -14
- package/dist/boat/doc-collector/src/state-diagram.js +57 -13
- package/dist/boat/prima/bin/prima-cli.js +4 -0
- package/dist/boat/prima/src/cli.js +200 -0
- package/dist/boat/prima/src/envelope.js +116 -0
- package/dist/boat/prima/src/prima.js +635 -0
- package/dist/boat/prima/src/pw-parser.js +18 -0
- package/dist/boat/prima/src/pw-registry.js +66 -0
- package/dist/models.json +3 -0
- package/dist/package.json +9 -2
- package/dist/src/action.d.ts +5 -2
- package/dist/src/action.js +5 -5
- package/dist/src/ai/captain/mixin.js +3 -4
- package/dist/src/ai/captain/web-mode.js +1 -1
- package/dist/src/ai/navigator.d.ts +4 -0
- package/dist/src/ai/navigator.js +11 -6
- package/dist/src/ai/planner.d.ts +1 -0
- package/dist/src/ai/planner.js +6 -0
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher.js +1 -1
- package/dist/src/ai/task-agent.js +1 -1
- package/dist/src/ai/tester.d.ts +1 -0
- package/dist/src/ai/tester.js +13 -0
- package/dist/src/application-spec-contract.d.ts +8 -0
- package/dist/src/application-spec-contract.js +8 -0
- package/dist/src/application-spec.d.ts +15 -0
- package/dist/src/application-spec.js +71 -0
- package/dist/src/browser-server.d.ts +12 -6
- package/dist/src/browser-server.js +74 -19
- package/dist/src/commands/clean-command.js +2 -7
- package/dist/src/commands/init-command.d.ts +5 -0
- package/dist/src/commands/init-command.js +119 -1
- package/dist/src/commands/navigate-command.js +1 -1
- package/dist/src/commands/research-command.js +1 -1
- package/dist/src/commands/sites-command.d.ts +6 -0
- package/dist/src/commands/sites-command.js +23 -0
- package/dist/src/components/InitWizard.d.ts +10 -0
- package/dist/src/components/InitWizard.js +133 -0
- package/dist/src/components/InputReadline.d.ts +1 -0
- package/dist/src/components/InputReadline.js +7 -4
- package/dist/src/config.d.ts +24 -5
- package/dist/src/config.js +146 -37
- package/dist/src/explorbot.d.ts +9 -0
- package/dist/src/explorbot.js +24 -5
- package/dist/src/explorer.d.ts +5 -1
- package/dist/src/explorer.js +54 -19
- package/dist/src/global-config.d.ts +22 -0
- package/dist/src/global-config.js +117 -0
- package/dist/src/knowledge-tracker.d.ts +5 -1
- package/dist/src/knowledge-tracker.js +14 -1
- package/dist/src/utils/cli-name.js +6 -2
- package/dist/src/utils/test-files.js +1 -2
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +9 -0
- package/models.json +3 -0
- package/package.json +9 -2
- package/src/action.ts +9 -5
- package/src/ai/captain/mixin.ts +3 -3
- package/src/ai/captain/web-mode.ts +1 -1
- package/src/ai/navigator.ts +12 -7
- package/src/ai/planner.ts +7 -0
- package/src/ai/researcher/locators.ts +1 -1
- package/src/ai/researcher.ts +1 -1
- package/src/ai/task-agent.ts +1 -1
- package/src/ai/tester.ts +15 -0
- package/src/application-spec-contract.ts +10 -0
- package/src/application-spec.ts +87 -0
- package/src/browser-server.ts +74 -19
- package/src/commands/clean-command.ts +1 -6
- package/src/commands/init-command.ts +146 -1
- package/src/commands/navigate-command.ts +1 -1
- package/src/commands/research-command.ts +1 -1
- package/src/commands/sites-command.ts +27 -0
- package/src/components/InitWizard.tsx +166 -0
- package/src/components/InputReadline.tsx +8 -4
- package/src/config.ts +162 -39
- package/src/explorbot.ts +30 -5
- package/src/explorer.ts +59 -20
- package/src/global-config.ts +148 -0
- package/src/knowledge-tracker.ts +17 -1
- package/src/utils/cli-name.ts +5 -2
- package/src/utils/test-files.ts +1 -2
- package/src/utils/url-matcher.ts +10 -0
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ EXPLORBOT_KNOWLEDGE="Log in as admin@example.com / secret123" \
|
|
|
138
138
|
npx explorbot explore /admin/users --max-tests 3
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
-
Output lands in a
|
|
141
|
+
Output lands in a per-host state directory, `~/.explorbot/state/<host>/`, so runs against the same app collect in one place and nothing is written to your project. Set `EXPLORBOT_EPHEMERAL=1` to keep nothing between runs. See [Agentic Usage](docs/workflow/agentic-usage.md).
|
|
142
142
|
|
|
143
143
|
## Teaching Explorbot
|
|
144
144
|
|
package/bin/explorbot-cli.ts
CHANGED
|
@@ -40,6 +40,7 @@ interface CLIOptions {
|
|
|
40
40
|
headless?: boolean;
|
|
41
41
|
incognito?: boolean;
|
|
42
42
|
session?: string | boolean;
|
|
43
|
+
spec?: string;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
function buildExplorBotOptions(from: string | undefined, options: CLIOptions): ExplorBotOptions {
|
|
@@ -52,6 +53,7 @@ function buildExplorBotOptions(from: string | undefined, options: CLIOptions): E
|
|
|
52
53
|
headless: options.headless,
|
|
53
54
|
incognito: options.incognito,
|
|
54
55
|
session: options.session,
|
|
56
|
+
applicationSpec: options.spec,
|
|
55
57
|
} as ExplorBotOptions;
|
|
56
58
|
}
|
|
57
59
|
|
|
@@ -64,6 +66,7 @@ function addCommonOptions(cmd: Command): Command {
|
|
|
64
66
|
.option('-s, --show', 'Show browser window')
|
|
65
67
|
.option('--headless', 'Run browser in headless mode')
|
|
66
68
|
.option('--incognito', 'Run without recording experiences')
|
|
69
|
+
.option('--spec <path>', 'Use a Docbot application spec directory or index.md')
|
|
67
70
|
.option('--session [file]', 'Save/restore browser session from file');
|
|
68
71
|
}
|
|
69
72
|
|
|
@@ -386,6 +389,14 @@ program
|
|
|
386
389
|
}
|
|
387
390
|
});
|
|
388
391
|
|
|
392
|
+
program
|
|
393
|
+
.command('sites')
|
|
394
|
+
.description('List sites registered in the global installation')
|
|
395
|
+
.action(async () => {
|
|
396
|
+
const { SitesCommand } = await import('../src/commands/sites-command.js');
|
|
397
|
+
await new SitesCommand(new ExplorBot()).execute('');
|
|
398
|
+
});
|
|
399
|
+
|
|
389
400
|
addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests with AI auto-healing')).action(async (filename, index, options) => {
|
|
390
401
|
try {
|
|
391
402
|
const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
|
|
@@ -421,17 +432,28 @@ addCommonOptions(
|
|
|
421
432
|
|
|
422
433
|
program
|
|
423
434
|
.command('init')
|
|
424
|
-
.description('Initialize a
|
|
425
|
-
.option('-c, --config-path <path>', 'Path for the config file'
|
|
435
|
+
.description('Initialize configuration for a project or for this machine')
|
|
436
|
+
.option('-c, --config-path <path>', 'Path for the config file')
|
|
426
437
|
.option('-f, --force', 'Overwrite existing config file')
|
|
427
438
|
.option('-p, --path <path>', 'Working directory for initialization')
|
|
439
|
+
.option('-g, --global', 'Configure explorbot in ~/.explorbot to run from anywhere')
|
|
440
|
+
.option('--provider <name>', `AI provider for the global config: ${Object.keys(PROVIDERS).join(', ')}`)
|
|
441
|
+
.option('--api-key <key>', 'API key stored in ~/.explorbot/.env')
|
|
428
442
|
.action(async (options) => {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
443
|
+
try {
|
|
444
|
+
const { runInit } = await import('../src/commands/init-command.js');
|
|
445
|
+
await runInit({
|
|
446
|
+
configPath: options.configPath,
|
|
447
|
+
force: options.force,
|
|
448
|
+
path: options.path,
|
|
449
|
+
global: options.global,
|
|
450
|
+
provider: options.provider,
|
|
451
|
+
apiKey: options.apiKey,
|
|
452
|
+
});
|
|
453
|
+
} catch (error) {
|
|
454
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
435
457
|
});
|
|
436
458
|
|
|
437
459
|
program
|
|
@@ -694,7 +716,7 @@ program
|
|
|
694
716
|
const explorBot = new ExplorBot(mainOptions);
|
|
695
717
|
await explorBot.start();
|
|
696
718
|
|
|
697
|
-
await explorBot.
|
|
719
|
+
await explorBot.visit(url);
|
|
698
720
|
|
|
699
721
|
const { ContextCommand } = await import('../src/commands/context-command.js');
|
|
700
722
|
const argParts: string[] = [];
|
|
@@ -716,7 +738,7 @@ addCommonOptions(program.command('shell <url> <command>').description('Execute a
|
|
|
716
738
|
try {
|
|
717
739
|
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
718
740
|
await explorBot.start();
|
|
719
|
-
await explorBot.
|
|
741
|
+
await explorBot.visit(url);
|
|
720
742
|
|
|
721
743
|
const action = explorBot.getExplorer().action();
|
|
722
744
|
await action.execute(command);
|
|
@@ -740,10 +762,11 @@ browserCmd
|
|
|
740
762
|
.description('Launch a persistent browser server')
|
|
741
763
|
.option('-s, --show', 'Launch browser in headed mode (visible window)')
|
|
742
764
|
.option('--headless', 'Launch browser in headless mode')
|
|
765
|
+
.option('--instance <name>', 'Named browser instance (lowercase letters, digits, dashes)')
|
|
743
766
|
.option('-c, --config <path>', 'Path to configuration file')
|
|
744
767
|
.option('-p, --path <path>', 'Working directory path')
|
|
745
768
|
.action(async (options) => {
|
|
746
|
-
const { launchServer, removeEndpointFile } = await import('../src/browser-server.js');
|
|
769
|
+
const { launchServer, removeEndpointFile, keepServerRunning } = await import('../src/browser-server.js');
|
|
747
770
|
await ConfigParser.getInstance().loadConfig({
|
|
748
771
|
config: options.config,
|
|
749
772
|
path: options.path,
|
|
@@ -754,55 +777,45 @@ browserCmd
|
|
|
754
777
|
if (options.show !== undefined) show = true;
|
|
755
778
|
if (options.headless !== undefined) show = false;
|
|
756
779
|
|
|
757
|
-
const server = await launchServer(
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
const cleanup = () => {
|
|
765
|
-
console.log('\nStopping browser server...');
|
|
766
|
-
server.close();
|
|
767
|
-
removeEndpointFile();
|
|
768
|
-
process.exit(0);
|
|
769
|
-
};
|
|
780
|
+
const server = await launchServer(
|
|
781
|
+
{
|
|
782
|
+
browser: config.playwright.browser,
|
|
783
|
+
show,
|
|
784
|
+
},
|
|
785
|
+
options.instance
|
|
786
|
+
);
|
|
770
787
|
|
|
771
|
-
|
|
772
|
-
|
|
788
|
+
await keepServerRunning(async () => {
|
|
789
|
+
await server.close();
|
|
790
|
+
removeEndpointFile(options.instance);
|
|
791
|
+
});
|
|
773
792
|
});
|
|
774
793
|
|
|
775
794
|
browserCmd
|
|
776
795
|
.command('stop')
|
|
777
796
|
.description('Stop a running browser server')
|
|
797
|
+
.option('--instance <name>', 'Named browser instance (lowercase letters, digits, dashes)')
|
|
778
798
|
.option('-c, --config <path>', 'Path to configuration file')
|
|
779
799
|
.option('-p, --path <path>', 'Working directory path')
|
|
780
800
|
.action(async (options) => {
|
|
781
|
-
const {
|
|
801
|
+
const { stopServer } = await import('../src/browser-server.js');
|
|
782
802
|
await ConfigParser.getInstance().loadConfig({
|
|
783
803
|
config: options.config,
|
|
784
804
|
path: options.path,
|
|
785
805
|
});
|
|
786
806
|
|
|
787
|
-
|
|
788
|
-
if (!endpoint) {
|
|
807
|
+
if (!(await stopServer(options.instance))) {
|
|
789
808
|
console.log('No running browser server found.');
|
|
790
809
|
process.exit(0);
|
|
791
810
|
}
|
|
792
811
|
|
|
793
|
-
try {
|
|
794
|
-
const { chromium } = await import('playwright-core');
|
|
795
|
-
const browser = await chromium.connect(endpoint, { timeout: 3000 });
|
|
796
|
-
await browser.close();
|
|
797
|
-
} catch {}
|
|
798
|
-
|
|
799
|
-
removeEndpointFile();
|
|
800
812
|
console.log('Browser server stopped.');
|
|
801
813
|
});
|
|
802
814
|
|
|
803
815
|
browserCmd
|
|
804
816
|
.command('status')
|
|
805
817
|
.description('Check if a browser server is running')
|
|
818
|
+
.option('--instance <name>', 'Named browser instance (lowercase letters, digits, dashes)')
|
|
806
819
|
.option('-c, --config <path>', 'Path to configuration file')
|
|
807
820
|
.option('-p, --path <path>', 'Working directory path')
|
|
808
821
|
.action(async (options) => {
|
|
@@ -812,7 +825,7 @@ browserCmd
|
|
|
812
825
|
path: options.path,
|
|
813
826
|
});
|
|
814
827
|
|
|
815
|
-
const endpoint = await getAliveEndpoint();
|
|
828
|
+
const endpoint = await getAliveEndpoint(options.instance);
|
|
816
829
|
if (endpoint) {
|
|
817
830
|
console.log(`Browser server is running at: ${endpoint}`);
|
|
818
831
|
} else {
|
|
@@ -871,8 +884,10 @@ program
|
|
|
871
884
|
|
|
872
885
|
import { createApiCommands } from '../boat/api-tester/src/cli.ts';
|
|
873
886
|
import { createDocsCommands } from '../boat/doc-collector/src/cli.ts';
|
|
887
|
+
import { createPrimaCommands } from '../boat/prima/src/cli.ts';
|
|
874
888
|
program.addCommand(createApiCommands('api'));
|
|
875
889
|
program.addCommand(createDocsCommands('docs'));
|
|
890
|
+
program.addCommand(createPrimaCommands('prima'));
|
|
876
891
|
|
|
877
892
|
const envHelp = () => {
|
|
878
893
|
const width = Math.max(...EXPLORBOT_ENV_VARS.map((v) => v.name.length));
|
|
@@ -33,7 +33,7 @@ export class ApiBot {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
async start(): Promise<void> {
|
|
36
|
-
this.config = await this.configParser.loadConfig({ config: this.options.config, path: this.options.path });
|
|
36
|
+
this.config = await this.configParser.loadConfig({ config: this.options.config, path: this.options.path, endpoint: this.options.endpoint });
|
|
37
37
|
this.provider = new AIProvider(this.config.ai);
|
|
38
38
|
await this.provider.validateConnection();
|
|
39
39
|
|
|
@@ -101,12 +101,13 @@ export class ApiBot {
|
|
|
101
101
|
return (this.agents.curler ||= this.createAgent(({ ai, apiClient, requestState }) => new Curler(ai, apiClient, requestState, this.reporter)));
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
async plan(
|
|
104
|
+
async plan(target: string, opts: { style?: string; fresh?: boolean } = {}): Promise<Plan> {
|
|
105
105
|
if (opts.fresh) {
|
|
106
106
|
this.currentPlan = undefined;
|
|
107
107
|
this.agents.chief = undefined;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
const endpoint = this.configParser.resolveEndpointPath(target);
|
|
110
111
|
const chief = this.agentChief();
|
|
111
112
|
const specDefinition = this.getEndpointDefinition(endpoint);
|
|
112
113
|
this.currentPlan = await chief.plan(endpoint, { style: opts.style, specDefinition });
|
|
@@ -198,6 +199,7 @@ interface ApibotOptions {
|
|
|
198
199
|
verbose?: boolean;
|
|
199
200
|
config?: string;
|
|
200
201
|
path?: string;
|
|
202
|
+
endpoint?: string;
|
|
201
203
|
}
|
|
202
204
|
|
|
203
205
|
export type { ApibotOptions };
|
|
@@ -49,7 +49,7 @@ export function createApiCommands(name = 'api'): Command {
|
|
|
49
49
|
addCommonOptions(cmd.command('plan <endpoint>').description('Generate test plan for an API endpoint').option('--style <style>', 'Planning style: basename of a file in rules/chief/styles/').option('--fresh', 'Start planning from scratch')).action(async (endpoint, options) => {
|
|
50
50
|
setPreserveConsoleLogs(true);
|
|
51
51
|
try {
|
|
52
|
-
const bot = new ApiBot(buildOptions(options));
|
|
52
|
+
const bot = new ApiBot({ ...buildOptions(options), endpoint });
|
|
53
53
|
await bot.start();
|
|
54
54
|
|
|
55
55
|
await bot.plan(endpoint, { style: options.style, fresh: options.fresh });
|
|
@@ -122,7 +122,7 @@ export function createApiCommands(name = 'api'): Command {
|
|
|
122
122
|
addCommonOptions(cmd.command('explore <endpoint>').description('Full cycle: plan all styles, execute tests, re-plan')).action(async (endpoint, options) => {
|
|
123
123
|
setPreserveConsoleLogs(true);
|
|
124
124
|
try {
|
|
125
|
-
const bot = new ApiBot(buildOptions(options));
|
|
125
|
+
const bot = new ApiBot({ ...buildOptions(options), endpoint });
|
|
126
126
|
await bot.start();
|
|
127
127
|
|
|
128
128
|
const styles = Object.keys(getStyles());
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path, { resolve } from 'node:path';
|
|
3
3
|
import { parseEnv } from 'node:util';
|
|
4
|
-
import { type AIConfig, type ApiHookFn, type ApiConfig as BaseApiConfig, EXPLORBOT_CONFIG_PATHS, createModel, materializeKnowledge, resolveModel, resolveOutputRoot } from '../../../src/config.ts';
|
|
4
|
+
import { type AIConfig, type ApiHookFn, type ApiConfig as BaseApiConfig, ConfigMissingError, EXPLORBOT_CONFIG_PATHS, createModel, envConfigRequested, materializeKnowledge, missingConfigMessage, resolveConfigModels, resolveModel, resolveOutputRoot } from '../../../src/config.ts';
|
|
5
|
+
import { type SiteRecord, findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from '../../../src/global-config.ts';
|
|
5
6
|
|
|
6
7
|
export type { AIConfig };
|
|
7
8
|
|
|
@@ -26,6 +27,7 @@ export class ApibotConfigParser {
|
|
|
26
27
|
private static instance: ApibotConfigParser;
|
|
27
28
|
private config: ApibotConfig | null = null;
|
|
28
29
|
private configPath: string | null = null;
|
|
30
|
+
private site: SiteRecord | null = null;
|
|
29
31
|
|
|
30
32
|
private constructor() {}
|
|
31
33
|
|
|
@@ -42,7 +44,7 @@ export class ApibotConfigParser {
|
|
|
42
44
|
Object.assign(process.env, parseEnv(readFileSync(resolved, 'utf8')));
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
async loadConfig(options?: { config?: string; path?: string }): Promise<ApibotConfig> {
|
|
47
|
+
async loadConfig(options?: { config?: string; path?: string; endpoint?: string }): Promise<ApibotConfig> {
|
|
46
48
|
if (this.config && !options?.config && !options?.path) return this.config;
|
|
47
49
|
|
|
48
50
|
const originalCwd = process.cwd();
|
|
@@ -50,6 +52,7 @@ export class ApibotConfigParser {
|
|
|
50
52
|
process.chdir(resolve(options.path));
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
ApibotConfigParser.loadEnv(globalEnvPath());
|
|
53
56
|
ApibotConfigParser.loadEnv('.env');
|
|
54
57
|
|
|
55
58
|
const resolvedPath = options?.config || this.findConfigFile();
|
|
@@ -78,7 +81,14 @@ export class ApibotConfigParser {
|
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
this.config = this.mergeWithDefaults(loadedConfig);
|
|
84
|
+
await resolveConfigModels(this.config.ai);
|
|
81
85
|
this.configPath = resolvedPath;
|
|
86
|
+
this.site = null;
|
|
87
|
+
|
|
88
|
+
if (isGlobalConfigPath(resolvedPath)) {
|
|
89
|
+
this.enterGlobalMode(this.config, options?.endpoint);
|
|
90
|
+
}
|
|
91
|
+
|
|
82
92
|
this.validateConfig(this.config);
|
|
83
93
|
|
|
84
94
|
return this.config;
|
|
@@ -100,9 +110,22 @@ export class ApibotConfigParser {
|
|
|
100
110
|
|
|
101
111
|
getOutputDir(): string {
|
|
102
112
|
const config = this.getConfig();
|
|
113
|
+
return path.join(this.getProjectRoot(), config.dirs?.output || 'output');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getProjectRoot(): string {
|
|
117
|
+
if (this.site) return this.site.dir;
|
|
103
118
|
const configPath = this.getConfigPath();
|
|
104
119
|
if (!configPath) throw new Error('Config path not found');
|
|
105
|
-
return path.
|
|
120
|
+
return path.dirname(configPath);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
resolveEndpointPath(endpoint: string): string {
|
|
124
|
+
if (!this.site) return endpoint;
|
|
125
|
+
|
|
126
|
+
const resolved = resolveSiteTarget(endpoint, this.site.url);
|
|
127
|
+
if (resolved.baseUrl !== this.site.url) return endpoint;
|
|
128
|
+
return resolved.path;
|
|
106
129
|
}
|
|
107
130
|
|
|
108
131
|
getPlansDir(): string {
|
|
@@ -115,9 +138,7 @@ export class ApibotConfigParser {
|
|
|
115
138
|
|
|
116
139
|
getKnowledgeDir(): string {
|
|
117
140
|
const config = this.getConfig();
|
|
118
|
-
|
|
119
|
-
if (!configPath) throw new Error('Config path not found');
|
|
120
|
-
return path.join(path.dirname(configPath), config.dirs?.knowledge || 'knowledge');
|
|
141
|
+
return path.join(this.getProjectRoot(), config.dirs?.knowledge || 'knowledge');
|
|
121
142
|
}
|
|
122
143
|
|
|
123
144
|
ensureDirectory(dirPath: string): void {
|
|
@@ -126,11 +147,20 @@ export class ApibotConfigParser {
|
|
|
126
147
|
}
|
|
127
148
|
}
|
|
128
149
|
|
|
150
|
+
private enterGlobalMode(config: ApibotConfig, endpoint?: string): void {
|
|
151
|
+
const site = resolveSiteTarget(endpoint);
|
|
152
|
+
this.site = registerSite(site.baseUrl);
|
|
153
|
+
|
|
154
|
+
config.dirs = { output: 'output', knowledge: 'knowledge' };
|
|
155
|
+
config.api = { ...config.api, baseEndpoint: site.baseUrl };
|
|
156
|
+
if (process.env.EXPLORBOT_API_SPEC) config.api.spec = [process.env.EXPLORBOT_API_SPEC];
|
|
157
|
+
}
|
|
158
|
+
|
|
129
159
|
private async loadEnvConfig(): Promise<ApibotConfig> {
|
|
130
160
|
const provider = process.env.EXPLORBOT_AI_PROVIDER;
|
|
131
161
|
const modelSpec = process.env.EXPLORBOT_AI_MODEL;
|
|
132
162
|
if (!provider && !modelSpec) {
|
|
133
|
-
throw new
|
|
163
|
+
throw new ConfigMissingError(missingConfigMessage('apibot.config.js'));
|
|
134
164
|
}
|
|
135
165
|
if (modelSpec && !provider && !modelSpec.includes('/')) {
|
|
136
166
|
throw new Error('EXPLORBOT_AI_MODEL needs a provider — set EXPLORBOT_AI_PROVIDER, or write it as "provider/model-id"');
|
|
@@ -177,7 +207,8 @@ export class ApibotConfigParser {
|
|
|
177
207
|
if (existsSync(fullPath)) return fullPath;
|
|
178
208
|
}
|
|
179
209
|
|
|
180
|
-
return null;
|
|
210
|
+
if (envConfigRequested()) return null;
|
|
211
|
+
return findGlobalConfig();
|
|
181
212
|
}
|
|
182
213
|
|
|
183
214
|
private async loadConfigModule(configPath: string): Promise<any> {
|
|
@@ -49,7 +49,10 @@ export interface InteractionScreenshot {
|
|
|
49
49
|
relativePath: string;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export
|
|
52
|
+
export interface CaptureInteractionState {
|
|
53
|
+
before(): Promise<Buffer | null>;
|
|
54
|
+
after(beforeScreenshot: Buffer | null, state: WebPageState, transition: DocStateTransition): Promise<InteractionScreenshot | null>;
|
|
55
|
+
}
|
|
53
56
|
|
|
54
57
|
const DEFAULT_MAX_PRIMARY_CANDIDATES = 3;
|
|
55
58
|
const DEFAULT_MAX_INTERACTIONS = 5;
|
|
@@ -130,6 +133,8 @@ async function executeInteraction(explorer: Explorer, stateManager: StateManager
|
|
|
130
133
|
return null;
|
|
131
134
|
}
|
|
132
135
|
|
|
136
|
+
const beforeScreenshot = await captureState?.before();
|
|
137
|
+
|
|
133
138
|
const executed = await attemptInteraction(explorer, candidate);
|
|
134
139
|
if (!executed) {
|
|
135
140
|
return null;
|
|
@@ -151,13 +156,13 @@ async function executeInteraction(explorer: Explorer, stateManager: StateManager
|
|
|
151
156
|
});
|
|
152
157
|
|
|
153
158
|
if (captureState && isMeaningfulStateTransition(transition)) {
|
|
154
|
-
const screenshot = await captureState(afterState, transition);
|
|
159
|
+
const screenshot = await captureState.after(beforeScreenshot ?? null, afterState, transition);
|
|
155
160
|
if (screenshot) {
|
|
156
161
|
transition.screenshot = screenshot;
|
|
157
162
|
}
|
|
158
163
|
}
|
|
159
164
|
|
|
160
|
-
if (urlChanged || ariaChanges.newCount > 0) {
|
|
165
|
+
if (urlChanged || ariaChanges.newCount > 0 || ariaChanges.removedCount > 0) {
|
|
161
166
|
await restoreInteractionState(explorer, restoreUrl);
|
|
162
167
|
}
|
|
163
168
|
|
|
@@ -192,11 +197,18 @@ async function restoreInteractionState(explorer: Explorer, restoreUrl: string, p
|
|
|
192
197
|
}
|
|
193
198
|
|
|
194
199
|
function buildTransition(candidate: InteractionCandidate, beforeState: WebPageState, afterState: WebPageState, changes: InteractionChanges): DocStateTransition {
|
|
200
|
+
const existingUrls = new Set(collectLinks(beforeState).map((link) => link.url));
|
|
195
201
|
const transition: DocStateTransition = {
|
|
196
202
|
action: describeAction(candidate),
|
|
197
203
|
before: summarizeInteractiveState(beforeState),
|
|
198
204
|
after: summarizeInteractiveState(afterState),
|
|
199
|
-
discoveredUrls:
|
|
205
|
+
discoveredUrls: [
|
|
206
|
+
...new Set(
|
|
207
|
+
collectLinks(afterState)
|
|
208
|
+
.map((link) => link.url)
|
|
209
|
+
.filter((url) => !existingUrls.has(url))
|
|
210
|
+
),
|
|
211
|
+
],
|
|
200
212
|
newCapabilities: collectDiscoveryNotes(afterState, changes),
|
|
201
213
|
element: buildInteractionElement(candidate),
|
|
202
214
|
changes,
|
|
@@ -239,7 +251,7 @@ function isMeaningfulStateTransition(transition: DocStateTransition): boolean {
|
|
|
239
251
|
if (transition.targetUrl || transition.changes?.urlChanged) {
|
|
240
252
|
return true;
|
|
241
253
|
}
|
|
242
|
-
return (transition.changes?.newElements || 0) > 0;
|
|
254
|
+
return (transition.changes?.newElements || 0) > 0 || (transition.changes?.removedElements || 0) > 0;
|
|
243
255
|
}
|
|
244
256
|
|
|
245
257
|
function buildInteractionElement(candidate: InteractionCandidate): InteractionElement {
|
|
@@ -55,6 +55,7 @@ export function createDocsCommands(name = 'docs'): Command {
|
|
|
55
55
|
console.log(`Skipped ${result.skipped.length} page(s)`);
|
|
56
56
|
console.log(`Spec index: ${result.indexPath}`);
|
|
57
57
|
console.log(`Pages dir: ${path.join(result.outputDir, 'pages')}`);
|
|
58
|
+
console.log(`Use in Explorbot: npx explorbot start ${startPath} --spec "${result.outputDir}"`);
|
|
58
59
|
|
|
59
60
|
await bot.stop();
|
|
60
61
|
process.exit(0);
|
|
@@ -91,6 +92,7 @@ export function createDocsCommands(name = 'docs'): Command {
|
|
|
91
92
|
output: 'docs',
|
|
92
93
|
screenshot: true,
|
|
93
94
|
interactive: false,
|
|
95
|
+
ignoreErrors: true,
|
|
94
96
|
collapseDynamicPages: true,
|
|
95
97
|
scope: 'site',
|
|
96
98
|
includePaths: [],
|
|
@@ -115,6 +115,7 @@ class DocbotConfigParser {
|
|
|
115
115
|
output: 'docs',
|
|
116
116
|
screenshot: true,
|
|
117
117
|
interactive: false,
|
|
118
|
+
ignoreErrors: true,
|
|
118
119
|
collapseDynamicPages: true,
|
|
119
120
|
scope: 'site',
|
|
120
121
|
includePaths: [],
|
|
@@ -151,6 +152,7 @@ interface DocbotConfig {
|
|
|
151
152
|
maxPages?: number;
|
|
152
153
|
output?: string;
|
|
153
154
|
screenshot?: boolean;
|
|
155
|
+
ignoreErrors?: boolean | string[];
|
|
154
156
|
prompt?: string;
|
|
155
157
|
collapseDynamicPages?: boolean;
|
|
156
158
|
scope?: 'site' | 'section' | 'subtree';
|
|
@@ -6,11 +6,12 @@ import { normalizeUrl } from '../../../src/state-manager.ts';
|
|
|
6
6
|
import { tag } from '../../../src/utils/logger.ts';
|
|
7
7
|
import { sanitizeFilename } from '../../../src/utils/strings.ts';
|
|
8
8
|
import { Documentarian, type PageDocumentation } from './ai/documentarian.ts';
|
|
9
|
+
import type { DocStateTransition } from './ai/tools.ts';
|
|
9
10
|
import { type DocbotConfig, DocbotConfigParser } from './config.ts';
|
|
10
11
|
import { type DocumentedPage, type SkippedPage, renderPageDocumentation, renderSpecIndex } from './docs-renderer.ts';
|
|
11
12
|
import { getDocPageKey, shouldCrawlDocPath } from './path-filter.ts';
|
|
12
13
|
import { extractResearchNavigationTargets } from './research-navigation.ts';
|
|
13
|
-
import { type DocumentationScreenshot, captureDocumentationScreenshots, captureInteractionScreenshot } from './screenshots.ts';
|
|
14
|
+
import { type DocumentationScreenshot, captureBeforeInteraction, captureDocumentationScreenshots, captureInteractionScreenshot } from './screenshots.ts';
|
|
14
15
|
import { renderMermaidBody } from './state-diagram.ts';
|
|
15
16
|
|
|
16
17
|
class DocBot {
|
|
@@ -113,16 +114,18 @@ class DocBot {
|
|
|
113
114
|
force: true,
|
|
114
115
|
});
|
|
115
116
|
const pagePath = this.getPageFilePath(state.url);
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
117
|
+
const captureState = this.shouldUseScreenshots()
|
|
118
|
+
? {
|
|
119
|
+
before: () => captureBeforeInteraction(this.explorBot.getExplorer()),
|
|
120
|
+
after: (beforeScreenshot: Buffer | null, interactionState: WebPageState, transition: DocStateTransition) =>
|
|
121
|
+
captureInteractionScreenshot(this.explorBot.getExplorer(), beforeScreenshot, interactionState, transition, {
|
|
122
|
+
pageFilePath: pagePath,
|
|
123
|
+
screenshotsDir: this.getScreenshotsDir(),
|
|
124
|
+
config: this.config,
|
|
125
|
+
}),
|
|
126
|
+
}
|
|
127
|
+
: undefined;
|
|
128
|
+
const documentation = await this.documentarian.document(state, research, captureState);
|
|
126
129
|
const lowSignalReason = this.getLowSignalReason(documentation, research);
|
|
127
130
|
if (lowSignalReason) {
|
|
128
131
|
skipped.push({
|
|
@@ -163,6 +166,9 @@ class DocBot {
|
|
|
163
166
|
}
|
|
164
167
|
} catch (error) {
|
|
165
168
|
const reason = error instanceof Error ? error.message : String(error);
|
|
169
|
+
if (!this.shouldIgnoreError(error)) {
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
166
172
|
tag('warning').log(`Skipping ${target}: ${reason}`);
|
|
167
173
|
skipped.push({
|
|
168
174
|
url: target,
|
|
@@ -410,6 +416,28 @@ class DocBot {
|
|
|
410
416
|
return `low-signal page: only ${documentation.can.length} proven actions and ${interactiveCount} interactive elements`;
|
|
411
417
|
}
|
|
412
418
|
|
|
419
|
+
private shouldIgnoreError(error: unknown): boolean {
|
|
420
|
+
const ignoreErrors = this.config.docs?.ignoreErrors;
|
|
421
|
+
if (ignoreErrors === undefined || ignoreErrors === true) return true;
|
|
422
|
+
if (ignoreErrors === false) return false;
|
|
423
|
+
|
|
424
|
+
const details = [error instanceof Error ? error.name : '', error instanceof Error ? error.message : String(error)];
|
|
425
|
+
if (typeof error === 'object' && error && 'code' in error) {
|
|
426
|
+
details.push(String(error.code));
|
|
427
|
+
}
|
|
428
|
+
const normalized = details
|
|
429
|
+
.join(' ')
|
|
430
|
+
.toLowerCase()
|
|
431
|
+
.replaceAll(/[\W_]+/g, ' ');
|
|
432
|
+
return ignoreErrors.some((pattern) => {
|
|
433
|
+
const normalizedPattern = pattern
|
|
434
|
+
.trim()
|
|
435
|
+
.toLowerCase()
|
|
436
|
+
.replaceAll(/[\W_]+/g, ' ');
|
|
437
|
+
return normalizedPattern.length > 0 && normalized.includes(normalizedPattern);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
413
441
|
private countInteractiveElements(research: string): number {
|
|
414
442
|
const matches = [...research.matchAll(/\((\d+) elements?\)/g)];
|
|
415
443
|
return matches.reduce((sum, match) => sum + Number.parseInt(match[1], 10), 0);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import matter from 'gray-matter';
|
|
3
|
+
import { APPLICATION_SPEC_FORMAT, APPLICATION_SPEC_VERSION } from '../../../src/application-spec-contract.ts';
|
|
2
4
|
import { type WebPageState } from '../../../src/state-manager.ts';
|
|
5
|
+
import { normalizeInlineText } from '../../../src/utils/strings.ts';
|
|
3
6
|
import type { PageDocumentation, StateTransition } from './ai/documentarian.ts';
|
|
4
7
|
import type { DocumentationScreenshot } from './screenshots.ts';
|
|
5
|
-
import {
|
|
6
|
-
import { normalizeInlineText } from '../../../src/utils/strings.ts';
|
|
8
|
+
import { type DocumentedPage, type SkippedPage, buildStateGraph, renderMermaidFromGraph, renderPageStateDiagram, renderStateMapFromGraph } from './state-diagram.ts';
|
|
7
9
|
|
|
8
10
|
function renderPageDocumentation(state: WebPageState, documentation: PageDocumentation, screenshots: DocumentationScreenshot[] = []): string {
|
|
9
11
|
const lines: string[] = [];
|
|
@@ -33,6 +35,13 @@ function renderPageDocumentation(state: WebPageState, documentation: PageDocumen
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
const interactions = documentation.interactions;
|
|
38
|
+
const pageStateDiagram = renderPageStateDiagram(state.title || state.url || 'page', state.url || '', interactions || []);
|
|
39
|
+
if (pageStateDiagram) {
|
|
40
|
+
lines.push('## State Map');
|
|
41
|
+
lines.push('');
|
|
42
|
+
lines.push(`\`\`\`mermaid\n${pageStateDiagram}\n\`\`\``);
|
|
43
|
+
lines.push('');
|
|
44
|
+
}
|
|
36
45
|
if (interactions && interactions.length > 0) {
|
|
37
46
|
lines.push('## State Transitions');
|
|
38
47
|
lines.push('');
|
|
@@ -101,7 +110,11 @@ function renderPageDocumentation(state: WebPageState, documentation: PageDocumen
|
|
|
101
110
|
lines.push('');
|
|
102
111
|
}
|
|
103
112
|
|
|
104
|
-
return `${lines.join('\n').trimEnd()}\n
|
|
113
|
+
return matter.stringify(`${lines.join('\n').trimEnd()}\n`, {
|
|
114
|
+
url: state.url,
|
|
115
|
+
format: APPLICATION_SPEC_FORMAT,
|
|
116
|
+
version: APPLICATION_SPEC_VERSION,
|
|
117
|
+
});
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
function renderSpecIndex(outputDir: string, startPath: string, pages: DocumentedPage[], skipped: SkippedPage[], maxPages: number): string {
|
|
@@ -116,9 +129,10 @@ function renderSpecIndex(outputDir: string, startPath: string, pages: Documented
|
|
|
116
129
|
lines.push(`Max pages: ${maxPages}`);
|
|
117
130
|
lines.push('');
|
|
118
131
|
const graph = buildStateGraph(outputDir, pages);
|
|
132
|
+
const mermaid = renderMermaidFromGraph(graph, true);
|
|
119
133
|
lines.push('## State Transitions');
|
|
120
134
|
lines.push('');
|
|
121
|
-
lines.push(`\`\`\`mermaid\n${
|
|
135
|
+
lines.push(`\`\`\`mermaid\n${mermaid}\n\`\`\``);
|
|
122
136
|
lines.push('');
|
|
123
137
|
const stateMap = renderStateMapFromGraph(graph);
|
|
124
138
|
if (stateMap) {
|