explorbot 0.2.2 → 0.2.4
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 +59 -38
- 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 +43 -10
- package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
- package/boat/doc-collector/src/ai/documentarian.ts +61 -31
- package/boat/doc-collector/src/cli.ts +1 -0
- package/boat/doc-collector/src/config.ts +4 -2
- package/boat/doc-collector/src/docs-renderer.ts +18 -4
- 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/activity-line.ts +33 -0
- package/boat/prima/src/cli.ts +251 -0
- package/boat/prima/src/envelope.ts +169 -0
- package/boat/prima/src/prima.ts +1124 -0
- package/boat/prima/src/pw-parser.ts +27 -0
- package/boat/prima/src/pw-registry.ts +74 -0
- package/boat/prima/src/session-log.ts +126 -0
- package/dist/bin/explorbot-cli.js +51 -32
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
- 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 +40 -10
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
- package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
- package/dist/boat/doc-collector/src/cli.js +1 -0
- package/dist/boat/doc-collector/src/config.js +4 -2
- package/dist/boat/doc-collector/src/docs-renderer.js +17 -3
- 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/activity-line.js +30 -0
- package/dist/boat/prima/src/cli.js +220 -0
- package/dist/boat/prima/src/envelope.js +142 -0
- package/dist/boat/prima/src/prima.js +1031 -0
- package/dist/boat/prima/src/pw-parser.js +30 -0
- package/dist/boat/prima/src/pw-registry.js +65 -0
- package/dist/boat/prima/src/session-log.js +108 -0
- package/dist/models.json +3 -0
- package/dist/package.json +8 -3
- package/dist/rules/navigator/verification-actions.md +20 -0
- package/dist/src/action-result.d.ts +7 -0
- package/dist/src/action-result.js +4 -0
- package/dist/src/action.d.ts +7 -2
- package/dist/src/action.js +41 -6
- package/dist/src/ai/captain/mixin.js +3 -4
- package/dist/src/ai/captain/web-mode.js +7 -4
- package/dist/src/ai/captain.js +2 -0
- package/dist/src/ai/navigator.d.ts +11 -0
- package/dist/src/ai/navigator.js +21 -12
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +48 -2
- package/dist/src/ai/planner.d.ts +1 -0
- package/dist/src/ai/planner.js +6 -0
- package/dist/src/ai/provider.js +2 -2
- package/dist/src/ai/rerunner.js +2 -1
- package/dist/src/ai/researcher.js +2 -2
- package/dist/src/ai/rules.js +17 -10
- package/dist/src/ai/task-agent.js +5 -2
- package/dist/src/ai/tester.d.ts +1 -0
- package/dist/src/ai/tester.js +33 -20
- package/dist/src/ai/tools.d.ts +14 -0
- package/dist/src/ai/tools.js +113 -36
- 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 +25 -5
- package/dist/src/config.js +158 -40
- package/dist/src/execution-controller.d.ts +2 -0
- package/dist/src/execution-controller.js +6 -0
- package/dist/src/explorbot.d.ts +11 -1
- package/dist/src/explorbot.js +27 -6
- package/dist/src/explorer.d.ts +4 -1
- package/dist/src/explorer.js +42 -9
- 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/playwright-recorder.js +30 -0
- package/dist/src/remote.d.ts +54 -0
- package/dist/src/remote.js +229 -0
- package/dist/src/reporter.d.ts +1 -0
- package/dist/src/reporter.js +7 -1
- package/dist/src/state-manager.d.ts +2 -1
- package/dist/src/stats.d.ts +1 -0
- package/dist/src/stats.js +1 -0
- package/dist/src/test-plan.d.ts +2 -0
- package/dist/src/test-plan.js +7 -0
- package/dist/src/utils/aria.d.ts +2 -8
- package/dist/src/utils/aria.js +69 -40
- package/dist/src/utils/cli-name.js +6 -2
- package/dist/src/utils/html.js +1 -0
- package/dist/src/utils/logger.d.ts +6 -0
- package/dist/src/utils/logger.js +24 -0
- package/dist/src/utils/page-readiness.js +18 -1
- 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 +12 -0
- package/dist/src/utils/web-element.d.ts +2 -0
- package/dist/src/utils/web-element.js +8 -0
- package/dist/src/utils/web-sandbox.d.ts +1 -1
- package/dist/src/utils/web-sandbox.js +2 -3
- package/docs/api-testing/basics.md +90 -0
- package/docs/api-testing/planning.md +57 -0
- package/docs/api-testing/running-tests.md +55 -0
- package/docs/assets/cloud-report.png +0 -0
- package/docs/assets/html-report.png +0 -0
- package/docs/assets/langfuse-trace.png +0 -0
- package/docs/assets/successful-explore-run.png +0 -0
- package/docs/basics/getting-started.md +140 -0
- package/docs/basics/prerequisites.md +63 -0
- package/docs/basics/providers.md +362 -0
- package/docs/basics/running.md +78 -0
- package/docs/contributing/ai-integration-tests.md +57 -0
- package/docs/contributing/contributing.md +90 -0
- package/docs/contributing/demo-videos.md +36 -0
- package/docs/contributing/npm-package.md +138 -0
- package/docs/contributing/observability.md +227 -0
- package/docs/contributing/regression-tests.md +103 -0
- package/docs/contributing/testing.md +95 -0
- package/docs/doc-collection/basics.md +128 -0
- package/docs/doc-collection/crawling.md +67 -0
- package/docs/doc-collection/interactive-mode.md +99 -0
- package/docs/index.json +86 -0
- package/docs/reference/commands.md +994 -0
- package/docs/reference/configuration.md +569 -0
- package/docs/reference/scripting.md +303 -0
- package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
- package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
- package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
- package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
- package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
- package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
- package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
- package/docs/web-testing/agents.md +158 -0
- package/docs/web-testing/automated-tests.md +134 -0
- package/docs/web-testing/basics.md +91 -0
- package/docs/web-testing/customization.md +131 -0
- package/docs/web-testing/hooks.md +238 -0
- package/docs/web-testing/page-interaction.md +84 -0
- package/docs/web-testing/planner.md +122 -0
- package/docs/web-testing/rerun.md +164 -0
- package/docs/web-testing/researcher.md +380 -0
- package/docs/workflow/agentic-usage.md +233 -0
- package/docs/workflow/application-spec.md +73 -0
- package/docs/workflow/ci.md +202 -0
- package/docs/workflow/knowledge.md +310 -0
- package/docs/workflow/planning-styles.md +67 -0
- package/docs/workflow/reporting.md +133 -0
- package/docs/workflow/test-plans.md +90 -0
- package/models.json +3 -0
- package/package.json +8 -3
- package/rules/navigator/verification-actions.md +20 -0
- package/src/action-result.ts +11 -0
- package/src/action.ts +47 -7
- package/src/ai/captain/mixin.ts +3 -3
- package/src/ai/captain/web-mode.ts +7 -4
- package/src/ai/captain.ts +3 -0
- package/src/ai/navigator.ts +26 -15
- package/src/ai/pilot.ts +53 -2
- package/src/ai/planner.ts +7 -0
- package/src/ai/provider.ts +2 -4
- package/src/ai/rerunner.ts +2 -1
- package/src/ai/researcher.ts +2 -2
- package/src/ai/rules.ts +17 -10
- package/src/ai/task-agent.ts +4 -2
- package/src/ai/tester.ts +32 -18
- package/src/ai/tools.ts +132 -36
- 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 +177 -43
- package/src/execution-controller.ts +8 -0
- package/src/explorbot.ts +34 -7
- package/src/explorer.ts +47 -9
- package/src/global-config.ts +148 -0
- package/src/knowledge-tracker.ts +17 -1
- package/src/playwright-recorder.ts +23 -0
- package/src/remote.ts +238 -0
- package/src/reporter.ts +7 -1
- package/src/state-manager.ts +2 -1
- package/src/stats.ts +1 -0
- package/src/test-plan.ts +9 -0
- package/src/utils/aria.ts +65 -45
- package/src/utils/cli-name.ts +5 -2
- package/src/utils/html.ts +1 -0
- package/src/utils/logger.ts +24 -1
- package/src/utils/page-readiness.ts +24 -1
- package/src/utils/test-files.ts +1 -2
- package/src/utils/url-matcher.ts +13 -0
- package/src/utils/web-element.ts +9 -0
- package/src/utils/web-sandbox.ts +3 -4
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 site directory, `~/.explorbot/sites/<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
|
@@ -11,6 +11,7 @@ import { App } from '../src/components/App.js';
|
|
|
11
11
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
12
12
|
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
13
13
|
import { ExplorBot, type ExplorBotOptions } from '../src/explorbot.js';
|
|
14
|
+
import { remote } from '../src/remote.js';
|
|
14
15
|
import { Stats } from '../src/stats.js';
|
|
15
16
|
import { Plan } from '../src/test-plan.js';
|
|
16
17
|
import { getCliName } from '../src/utils/cli-name.ts';
|
|
@@ -26,8 +27,9 @@ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../p
|
|
|
26
27
|
const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version as string;
|
|
27
28
|
|
|
28
29
|
program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
|
|
30
|
+
remote.registerOption(program);
|
|
29
31
|
|
|
30
|
-
if (!process.env.EXPLORBOT_NO_BANNER) {
|
|
32
|
+
if (!process.env.EXPLORBOT_NO_BANNER && !process.argv.includes('prima')) {
|
|
31
33
|
console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -40,6 +42,7 @@ interface CLIOptions {
|
|
|
40
42
|
headless?: boolean;
|
|
41
43
|
incognito?: boolean;
|
|
42
44
|
session?: string | boolean;
|
|
45
|
+
spec?: string;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
function buildExplorBotOptions(from: string | undefined, options: CLIOptions): ExplorBotOptions {
|
|
@@ -52,6 +55,7 @@ function buildExplorBotOptions(from: string | undefined, options: CLIOptions): E
|
|
|
52
55
|
headless: options.headless,
|
|
53
56
|
incognito: options.incognito,
|
|
54
57
|
session: options.session,
|
|
58
|
+
applicationSpec: options.spec,
|
|
55
59
|
} as ExplorBotOptions;
|
|
56
60
|
}
|
|
57
61
|
|
|
@@ -64,6 +68,7 @@ function addCommonOptions(cmd: Command): Command {
|
|
|
64
68
|
.option('-s, --show', 'Show browser window')
|
|
65
69
|
.option('--headless', 'Run browser in headless mode')
|
|
66
70
|
.option('--incognito', 'Run without recording experiences')
|
|
71
|
+
.option('--spec <path>', 'Use a Docbot application spec directory or index.md')
|
|
67
72
|
.option('--session [file]', 'Save/restore browser session from file');
|
|
68
73
|
}
|
|
69
74
|
|
|
@@ -96,6 +101,10 @@ async function startTUI(explorBot: ExplorBot): Promise<void> {
|
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
async function showStatsAndExit(code: number): Promise<never> {
|
|
104
|
+
if (remote.isAttached()) {
|
|
105
|
+
await remote.close(code);
|
|
106
|
+
process.exit(code);
|
|
107
|
+
}
|
|
99
108
|
if (Stats.hasActivity()) {
|
|
100
109
|
await new Promise<void>((resolve) => {
|
|
101
110
|
const { unmount } = render(
|
|
@@ -386,6 +395,14 @@ program
|
|
|
386
395
|
}
|
|
387
396
|
});
|
|
388
397
|
|
|
398
|
+
program
|
|
399
|
+
.command('sites')
|
|
400
|
+
.description('List sites registered in the global installation')
|
|
401
|
+
.action(async () => {
|
|
402
|
+
const { SitesCommand } = await import('../src/commands/sites-command.js');
|
|
403
|
+
await new SitesCommand(new ExplorBot()).execute('');
|
|
404
|
+
});
|
|
405
|
+
|
|
389
406
|
addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests with AI auto-healing')).action(async (filename, index, options) => {
|
|
390
407
|
try {
|
|
391
408
|
const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
|
|
@@ -421,17 +438,28 @@ addCommonOptions(
|
|
|
421
438
|
|
|
422
439
|
program
|
|
423
440
|
.command('init')
|
|
424
|
-
.description('Initialize a
|
|
425
|
-
.option('-c, --config-path <path>', 'Path for the config file'
|
|
441
|
+
.description('Initialize configuration for a project or for this machine')
|
|
442
|
+
.option('-c, --config-path <path>', 'Path for the config file')
|
|
426
443
|
.option('-f, --force', 'Overwrite existing config file')
|
|
427
444
|
.option('-p, --path <path>', 'Working directory for initialization')
|
|
445
|
+
.option('-g, --global', 'Configure explorbot in ~/.explorbot to run from anywhere')
|
|
446
|
+
.option('--provider <name>', `AI provider for the global config: ${Object.keys(PROVIDERS).join(', ')}`)
|
|
447
|
+
.option('--api-key <key>', 'API key stored in ~/.explorbot/.env')
|
|
428
448
|
.action(async (options) => {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
449
|
+
try {
|
|
450
|
+
const { runInit } = await import('../src/commands/init-command.js');
|
|
451
|
+
await runInit({
|
|
452
|
+
configPath: options.configPath,
|
|
453
|
+
force: options.force,
|
|
454
|
+
path: options.path,
|
|
455
|
+
global: options.global,
|
|
456
|
+
provider: options.provider,
|
|
457
|
+
apiKey: options.apiKey,
|
|
458
|
+
});
|
|
459
|
+
} catch (error) {
|
|
460
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
435
463
|
});
|
|
436
464
|
|
|
437
465
|
program
|
|
@@ -694,7 +722,7 @@ program
|
|
|
694
722
|
const explorBot = new ExplorBot(mainOptions);
|
|
695
723
|
await explorBot.start();
|
|
696
724
|
|
|
697
|
-
await explorBot.
|
|
725
|
+
await explorBot.visit(url);
|
|
698
726
|
|
|
699
727
|
const { ContextCommand } = await import('../src/commands/context-command.js');
|
|
700
728
|
const argParts: string[] = [];
|
|
@@ -716,7 +744,7 @@ addCommonOptions(program.command('shell <url> <command>').description('Execute a
|
|
|
716
744
|
try {
|
|
717
745
|
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
718
746
|
await explorBot.start();
|
|
719
|
-
await explorBot.
|
|
747
|
+
await explorBot.visit(url);
|
|
720
748
|
|
|
721
749
|
const action = explorBot.getExplorer().action();
|
|
722
750
|
await action.execute(command);
|
|
@@ -740,10 +768,11 @@ browserCmd
|
|
|
740
768
|
.description('Launch a persistent browser server')
|
|
741
769
|
.option('-s, --show', 'Launch browser in headed mode (visible window)')
|
|
742
770
|
.option('--headless', 'Launch browser in headless mode')
|
|
771
|
+
.option('--instance <name>', 'Named browser instance (lowercase letters, digits, dashes)')
|
|
743
772
|
.option('-c, --config <path>', 'Path to configuration file')
|
|
744
773
|
.option('-p, --path <path>', 'Working directory path')
|
|
745
774
|
.action(async (options) => {
|
|
746
|
-
const { launchServer, removeEndpointFile } = await import('../src/browser-server.js');
|
|
775
|
+
const { launchServer, removeEndpointFile, keepServerRunning } = await import('../src/browser-server.js');
|
|
747
776
|
await ConfigParser.getInstance().loadConfig({
|
|
748
777
|
config: options.config,
|
|
749
778
|
path: options.path,
|
|
@@ -754,55 +783,45 @@ browserCmd
|
|
|
754
783
|
if (options.show !== undefined) show = true;
|
|
755
784
|
if (options.headless !== undefined) show = false;
|
|
756
785
|
|
|
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
|
-
};
|
|
786
|
+
const server = await launchServer(
|
|
787
|
+
{
|
|
788
|
+
browser: config.playwright.browser,
|
|
789
|
+
show,
|
|
790
|
+
},
|
|
791
|
+
options.instance
|
|
792
|
+
);
|
|
770
793
|
|
|
771
|
-
|
|
772
|
-
|
|
794
|
+
await keepServerRunning(async () => {
|
|
795
|
+
await server.close();
|
|
796
|
+
removeEndpointFile(options.instance);
|
|
797
|
+
});
|
|
773
798
|
});
|
|
774
799
|
|
|
775
800
|
browserCmd
|
|
776
801
|
.command('stop')
|
|
777
802
|
.description('Stop a running browser server')
|
|
803
|
+
.option('--instance <name>', 'Named browser instance (lowercase letters, digits, dashes)')
|
|
778
804
|
.option('-c, --config <path>', 'Path to configuration file')
|
|
779
805
|
.option('-p, --path <path>', 'Working directory path')
|
|
780
806
|
.action(async (options) => {
|
|
781
|
-
const {
|
|
807
|
+
const { stopServer } = await import('../src/browser-server.js');
|
|
782
808
|
await ConfigParser.getInstance().loadConfig({
|
|
783
809
|
config: options.config,
|
|
784
810
|
path: options.path,
|
|
785
811
|
});
|
|
786
812
|
|
|
787
|
-
|
|
788
|
-
if (!endpoint) {
|
|
813
|
+
if (!(await stopServer(options.instance))) {
|
|
789
814
|
console.log('No running browser server found.');
|
|
790
815
|
process.exit(0);
|
|
791
816
|
}
|
|
792
817
|
|
|
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
818
|
console.log('Browser server stopped.');
|
|
801
819
|
});
|
|
802
820
|
|
|
803
821
|
browserCmd
|
|
804
822
|
.command('status')
|
|
805
823
|
.description('Check if a browser server is running')
|
|
824
|
+
.option('--instance <name>', 'Named browser instance (lowercase letters, digits, dashes)')
|
|
806
825
|
.option('-c, --config <path>', 'Path to configuration file')
|
|
807
826
|
.option('-p, --path <path>', 'Working directory path')
|
|
808
827
|
.action(async (options) => {
|
|
@@ -812,7 +831,7 @@ browserCmd
|
|
|
812
831
|
path: options.path,
|
|
813
832
|
});
|
|
814
833
|
|
|
815
|
-
const endpoint = await getAliveEndpoint();
|
|
834
|
+
const endpoint = await getAliveEndpoint(options.instance);
|
|
816
835
|
if (endpoint) {
|
|
817
836
|
console.log(`Browser server is running at: ${endpoint}`);
|
|
818
837
|
} else {
|
|
@@ -871,8 +890,10 @@ program
|
|
|
871
890
|
|
|
872
891
|
import { createApiCommands } from '../boat/api-tester/src/cli.ts';
|
|
873
892
|
import { createDocsCommands } from '../boat/doc-collector/src/cli.ts';
|
|
893
|
+
import { createPrimaCommands } from '../boat/prima/src/cli.ts';
|
|
874
894
|
program.addCommand(createApiCommands('api'));
|
|
875
895
|
program.addCommand(createDocsCommands('docs'));
|
|
896
|
+
program.addCommand(createPrimaCommands('prima'));
|
|
876
897
|
|
|
877
898
|
const envHelp = () => {
|
|
878
899
|
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,9 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path, { resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
3
4
|
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';
|
|
5
|
+
import { type AIConfig, type ApiHookFn, type ApiConfig as BaseApiConfig, ConfigMissingError, EXPLORBOT_CONFIG_PATHS, createModel, envConfigRequested, materializeKnowledge, missingConfigMessage, resolveConfigModels, resolveModel, resolveOutputRoot } from '../../../src/config.ts';
|
|
6
|
+
import { type SiteRecord, findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from '../../../src/global-config.ts';
|
|
5
7
|
|
|
6
8
|
export type { AIConfig };
|
|
7
9
|
|
|
@@ -26,6 +28,7 @@ export class ApibotConfigParser {
|
|
|
26
28
|
private static instance: ApibotConfigParser;
|
|
27
29
|
private config: ApibotConfig | null = null;
|
|
28
30
|
private configPath: string | null = null;
|
|
31
|
+
private site: SiteRecord | null = null;
|
|
29
32
|
|
|
30
33
|
private constructor() {}
|
|
31
34
|
|
|
@@ -42,7 +45,7 @@ export class ApibotConfigParser {
|
|
|
42
45
|
Object.assign(process.env, parseEnv(readFileSync(resolved, 'utf8')));
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
async loadConfig(options?: { config?: string; path?: string }): Promise<ApibotConfig> {
|
|
48
|
+
async loadConfig(options?: { config?: string; path?: string; endpoint?: string }): Promise<ApibotConfig> {
|
|
46
49
|
if (this.config && !options?.config && !options?.path) return this.config;
|
|
47
50
|
|
|
48
51
|
const originalCwd = process.cwd();
|
|
@@ -50,6 +53,7 @@ export class ApibotConfigParser {
|
|
|
50
53
|
process.chdir(resolve(options.path));
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
ApibotConfigParser.loadEnv(globalEnvPath());
|
|
53
57
|
ApibotConfigParser.loadEnv('.env');
|
|
54
58
|
|
|
55
59
|
const resolvedPath = options?.config || this.findConfigFile();
|
|
@@ -78,7 +82,14 @@ export class ApibotConfigParser {
|
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
this.config = this.mergeWithDefaults(loadedConfig);
|
|
85
|
+
await resolveConfigModels(this.config.ai);
|
|
81
86
|
this.configPath = resolvedPath;
|
|
87
|
+
this.site = null;
|
|
88
|
+
|
|
89
|
+
if (isGlobalConfigPath(resolvedPath)) {
|
|
90
|
+
this.enterGlobalMode(this.config, options?.endpoint);
|
|
91
|
+
}
|
|
92
|
+
|
|
82
93
|
this.validateConfig(this.config);
|
|
83
94
|
|
|
84
95
|
return this.config;
|
|
@@ -100,9 +111,22 @@ export class ApibotConfigParser {
|
|
|
100
111
|
|
|
101
112
|
getOutputDir(): string {
|
|
102
113
|
const config = this.getConfig();
|
|
114
|
+
return path.join(this.getProjectRoot(), config.dirs?.output || 'output');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getProjectRoot(): string {
|
|
118
|
+
if (this.site) return this.site.dir;
|
|
103
119
|
const configPath = this.getConfigPath();
|
|
104
120
|
if (!configPath) throw new Error('Config path not found');
|
|
105
|
-
return path.
|
|
121
|
+
return path.dirname(configPath);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
resolveEndpointPath(endpoint: string): string {
|
|
125
|
+
if (!this.site) return endpoint;
|
|
126
|
+
|
|
127
|
+
const resolved = resolveSiteTarget(endpoint, this.site.url);
|
|
128
|
+
if (resolved.baseUrl !== this.site.url) return endpoint;
|
|
129
|
+
return resolved.path;
|
|
106
130
|
}
|
|
107
131
|
|
|
108
132
|
getPlansDir(): string {
|
|
@@ -115,9 +139,7 @@ export class ApibotConfigParser {
|
|
|
115
139
|
|
|
116
140
|
getKnowledgeDir(): string {
|
|
117
141
|
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');
|
|
142
|
+
return path.join(this.getProjectRoot(), config.dirs?.knowledge || 'knowledge');
|
|
121
143
|
}
|
|
122
144
|
|
|
123
145
|
ensureDirectory(dirPath: string): void {
|
|
@@ -126,11 +148,20 @@ export class ApibotConfigParser {
|
|
|
126
148
|
}
|
|
127
149
|
}
|
|
128
150
|
|
|
151
|
+
private enterGlobalMode(config: ApibotConfig, endpoint?: string): void {
|
|
152
|
+
const site = resolveSiteTarget(endpoint);
|
|
153
|
+
this.site = registerSite(site.baseUrl);
|
|
154
|
+
|
|
155
|
+
config.dirs = { output: 'output', knowledge: 'knowledge' };
|
|
156
|
+
config.api = { ...config.api, baseEndpoint: site.baseUrl };
|
|
157
|
+
if (process.env.EXPLORBOT_API_SPEC) config.api.spec = [process.env.EXPLORBOT_API_SPEC];
|
|
158
|
+
}
|
|
159
|
+
|
|
129
160
|
private async loadEnvConfig(): Promise<ApibotConfig> {
|
|
130
161
|
const provider = process.env.EXPLORBOT_AI_PROVIDER;
|
|
131
162
|
const modelSpec = process.env.EXPLORBOT_AI_MODEL;
|
|
132
163
|
if (!provider && !modelSpec) {
|
|
133
|
-
throw new
|
|
164
|
+
throw new ConfigMissingError(missingConfigMessage('apibot.config.js'));
|
|
134
165
|
}
|
|
135
166
|
if (modelSpec && !provider && !modelSpec.includes('/')) {
|
|
136
167
|
throw new Error('EXPLORBOT_AI_MODEL needs a provider — set EXPLORBOT_AI_PROVIDER, or write it as "provider/model-id"');
|
|
@@ -177,15 +208,17 @@ export class ApibotConfigParser {
|
|
|
177
208
|
if (existsSync(fullPath)) return fullPath;
|
|
178
209
|
}
|
|
179
210
|
|
|
180
|
-
return null;
|
|
211
|
+
if (envConfigRequested()) return null;
|
|
212
|
+
return findGlobalConfig();
|
|
181
213
|
}
|
|
182
214
|
|
|
183
215
|
private async loadConfigModule(configPath: string): Promise<any> {
|
|
184
216
|
const ext = configPath.split('.').pop();
|
|
217
|
+
const moduleUrl = pathToFileURL(resolve(configPath)).href;
|
|
185
218
|
|
|
186
219
|
if (ext === 'ts') {
|
|
187
220
|
try {
|
|
188
|
-
return await import(
|
|
221
|
+
return await import(moduleUrl);
|
|
189
222
|
} catch {
|
|
190
223
|
const require = (await import('node:module')).createRequire(import.meta.url);
|
|
191
224
|
return require(configPath);
|
|
@@ -193,7 +226,7 @@ export class ApibotConfigParser {
|
|
|
193
226
|
}
|
|
194
227
|
|
|
195
228
|
if (ext === 'js' || ext === 'mjs') {
|
|
196
|
-
return await import(
|
|
229
|
+
return await import(moduleUrl);
|
|
197
230
|
}
|
|
198
231
|
|
|
199
232
|
const content = readFileSync(configPath, 'utf8');
|
|
@@ -67,17 +67,32 @@ class Documentarian {
|
|
|
67
67
|
const message = error instanceof Error ? error.message : String(error);
|
|
68
68
|
tag('warning').log(`Interactive documentation failed: ${message}.`);
|
|
69
69
|
if (meaningfulInteractions.length > 0) {
|
|
70
|
-
tag('info').log(`
|
|
71
|
-
return this.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
70
|
+
tag('info').log(`Retrying static documentation while preserving ${meaningfulInteractions.length} observed interaction(s).`);
|
|
71
|
+
return this.documentStatic(state, research)
|
|
72
|
+
.then((documentation) =>
|
|
73
|
+
this.normalizeDocumentation(
|
|
74
|
+
{
|
|
75
|
+
...documentation,
|
|
76
|
+
interactions: meaningfulInteractions,
|
|
77
|
+
},
|
|
78
|
+
state,
|
|
79
|
+
research
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
.catch((fallbackError) => {
|
|
83
|
+
const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
|
|
84
|
+
tag('warning').log(`Static documentation fallback failed: ${fallbackMessage}. Preserving observed interactions without AI summary.`);
|
|
85
|
+
return this.normalizeDocumentation(
|
|
86
|
+
{
|
|
87
|
+
summary: `Observed ${meaningfulInteractions.length} interaction(s); AI-generated summary was unavailable.`,
|
|
88
|
+
can: [],
|
|
89
|
+
might: [],
|
|
90
|
+
interactions: meaningfulInteractions,
|
|
91
|
+
},
|
|
92
|
+
state,
|
|
93
|
+
research
|
|
94
|
+
);
|
|
95
|
+
});
|
|
81
96
|
}
|
|
82
97
|
return this.documentStatic(state, research);
|
|
83
98
|
}
|
|
@@ -113,13 +128,13 @@ class Documentarian {
|
|
|
113
128
|
},
|
|
114
129
|
];
|
|
115
130
|
|
|
116
|
-
const response = await this.provider.generateObject(messages,
|
|
131
|
+
const response = await this.provider.generateObject(messages, generatedPageDocumentationSchema, undefined, {
|
|
117
132
|
agentName: 'documentarian',
|
|
118
133
|
});
|
|
119
134
|
|
|
120
135
|
return this.normalizeDocumentation(
|
|
121
136
|
{
|
|
122
|
-
...(response.object as
|
|
137
|
+
...(response.object as GeneratedPageDocumentation),
|
|
123
138
|
interactions,
|
|
124
139
|
},
|
|
125
140
|
state,
|
|
@@ -139,11 +154,11 @@ class Documentarian {
|
|
|
139
154
|
},
|
|
140
155
|
];
|
|
141
156
|
|
|
142
|
-
const response = await this.provider.generateObject(messages,
|
|
157
|
+
const response = await this.provider.generateObject(messages, generatedPageDocumentationSchema, undefined, {
|
|
143
158
|
agentName: 'documentarian',
|
|
144
159
|
});
|
|
145
160
|
|
|
146
|
-
return this.normalizeDocumentation(response.object as
|
|
161
|
+
return this.normalizeDocumentation(response.object as GeneratedPageDocumentation, state, research);
|
|
147
162
|
}
|
|
148
163
|
|
|
149
164
|
private getSystemPrompt(): string {
|
|
@@ -259,7 +274,7 @@ class Documentarian {
|
|
|
259
274
|
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');
|
|
260
275
|
}
|
|
261
276
|
|
|
262
|
-
private normalizeDocumentation(documentation: PageDocumentation, _state: WebPageState, _research: string): PageDocumentation {
|
|
277
|
+
private normalizeDocumentation(documentation: GeneratedPageDocumentation & Partial<Pick<PageDocumentation, 'interactions'>>, _state: WebPageState, _research: string): PageDocumentation {
|
|
263
278
|
const normalized = { ...documentation };
|
|
264
279
|
if (!normalized.interactions) {
|
|
265
280
|
normalized.interactions = undefined;
|
|
@@ -267,10 +282,10 @@ class Documentarian {
|
|
|
267
282
|
|
|
268
283
|
const qualityNotes = this.evaluateDocumentationQuality(normalized);
|
|
269
284
|
|
|
270
|
-
return {
|
|
285
|
+
return pageDocumentationSchema.parse({
|
|
271
286
|
...normalized,
|
|
272
287
|
qualityNotes,
|
|
273
|
-
};
|
|
288
|
+
});
|
|
274
289
|
}
|
|
275
290
|
|
|
276
291
|
private evaluateDocumentationQuality(documentation: PageDocumentation): string[] {
|
|
@@ -346,39 +361,54 @@ const stateTransitionSchema = z.object({
|
|
|
346
361
|
action: z.string(),
|
|
347
362
|
before: z.string(),
|
|
348
363
|
after: z.string(),
|
|
349
|
-
targetUrl: z.string().
|
|
350
|
-
discoveredUrls: z.array(z.string()).
|
|
351
|
-
newCapabilities: z.array(z.string()).
|
|
364
|
+
targetUrl: z.string().optional(),
|
|
365
|
+
discoveredUrls: z.array(z.string()).optional(),
|
|
366
|
+
newCapabilities: z.array(z.string()).optional(),
|
|
352
367
|
element: z
|
|
353
368
|
.object({
|
|
354
369
|
role: z.string(),
|
|
355
370
|
name: z.string(),
|
|
356
371
|
section: z.string(),
|
|
357
|
-
container: z.string().
|
|
358
|
-
locator: z.string().
|
|
372
|
+
container: z.string().optional(),
|
|
373
|
+
locator: z.string().optional(),
|
|
359
374
|
})
|
|
360
|
-
.
|
|
375
|
+
.optional(),
|
|
361
376
|
changes: z
|
|
362
377
|
.object({
|
|
363
378
|
urlChanged: z.boolean(),
|
|
364
379
|
newElements: z.number(),
|
|
365
380
|
removedElements: z.number(),
|
|
366
381
|
})
|
|
367
|
-
.
|
|
382
|
+
.optional(),
|
|
383
|
+
targetState: z
|
|
384
|
+
.object({
|
|
385
|
+
kind: z.enum(['page', 'dialog', 'modal', 'section']),
|
|
386
|
+
label: z.string(),
|
|
387
|
+
url: z.string(),
|
|
388
|
+
})
|
|
389
|
+
.optional(),
|
|
390
|
+
screenshot: z
|
|
391
|
+
.object({
|
|
392
|
+
title: z.string(),
|
|
393
|
+
relativePath: z.string(),
|
|
394
|
+
})
|
|
395
|
+
.optional(),
|
|
368
396
|
});
|
|
369
397
|
|
|
370
|
-
const
|
|
398
|
+
const generatedPageDocumentationSchema = z.object({
|
|
371
399
|
summary: z.string(),
|
|
372
400
|
can: z.array(capabilitySchema),
|
|
373
401
|
might: z.array(capabilitySchema),
|
|
374
|
-
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const pageDocumentationSchema = generatedPageDocumentationSchema.extend({
|
|
405
|
+
interactions: z.array(stateTransitionSchema).optional(),
|
|
406
|
+
qualityNotes: z.array(z.string()).optional(),
|
|
375
407
|
});
|
|
376
408
|
|
|
377
409
|
type StateTransition = DocStateTransition;
|
|
378
|
-
type
|
|
379
|
-
|
|
380
|
-
qualityNotes?: string[];
|
|
381
|
-
};
|
|
410
|
+
type GeneratedPageDocumentation = z.infer<typeof generatedPageDocumentationSchema>;
|
|
411
|
+
type PageDocumentation = z.infer<typeof pageDocumentationSchema>;
|
|
382
412
|
|
|
383
413
|
export { Documentarian };
|
|
384
414
|
export type { PageDocumentation, StateTransition };
|
|
@@ -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);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path, { resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
3
4
|
import { parseEnv } from 'node:util';
|
|
4
5
|
import { ConfigParser } from '../../../src/config.ts';
|
|
5
6
|
|
|
@@ -90,10 +91,11 @@ class DocbotConfigParser {
|
|
|
90
91
|
|
|
91
92
|
private async loadConfigModule(configPath: string): Promise<any> {
|
|
92
93
|
const ext = configPath.split('.').pop();
|
|
94
|
+
const moduleUrl = pathToFileURL(resolve(configPath)).href;
|
|
93
95
|
|
|
94
96
|
if (ext === 'ts') {
|
|
95
97
|
try {
|
|
96
|
-
return await import(
|
|
98
|
+
return await import(moduleUrl);
|
|
97
99
|
} catch {
|
|
98
100
|
const require = (await import('node:module')).createRequire(import.meta.url);
|
|
99
101
|
return require(configPath);
|
|
@@ -101,7 +103,7 @@ class DocbotConfigParser {
|
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
if (ext === 'js' || ext === 'mjs') {
|
|
104
|
-
return await import(
|
|
106
|
+
return await import(moduleUrl);
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
return JSON.parse(readFileSync(configPath, 'utf8'));
|