explorbot 0.2.2 → 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/cli.ts +1 -0
- 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/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/cli.js +1 -0
- 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/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 +6 -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.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 +4 -1
- package/dist/src/explorer.js +40 -6
- 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 +6 -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.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 +45 -7
- 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> {
|
|
@@ -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,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) {
|
|
@@ -69,8 +69,12 @@ function buildStateGraph(outputDir: string, pages: DocumentedPage[]): StateGraph
|
|
|
69
69
|
continue;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
if (adjacency.get(sourceId)?.has(targetId)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (createsCycle(sourceId, targetId, adjacency)) {
|
|
77
|
+
const pairKey = `${sourceId}>${targetId}`;
|
|
74
78
|
if (drawnBack.has(pairKey)) {
|
|
75
79
|
continue;
|
|
76
80
|
}
|
|
@@ -78,10 +82,6 @@ function buildStateGraph(outputDir: string, pages: DocumentedPage[]): StateGraph
|
|
|
78
82
|
edges.push({ source: sourceId, target: targetId, action: transition.action, isBack: true });
|
|
79
83
|
continue;
|
|
80
84
|
}
|
|
81
|
-
|
|
82
|
-
if (adjacency.get(sourceId)?.has(targetId) || createsCycle(sourceId, targetId, adjacency)) {
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
85
|
adjacency.get(sourceId)?.add(targetId);
|
|
86
86
|
edges.push({ source: sourceId, target: targetId, action: transition.action, isBack: false });
|
|
87
87
|
}
|
|
@@ -104,8 +104,8 @@ function renderMermaidBody(outputDir: string, pages: DocumentedPage[]): string {
|
|
|
104
104
|
return renderMermaidFromGraph(buildStateGraph(outputDir, pages));
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
function renderMermaidFromGraph(graph: StateGraph): string {
|
|
108
|
-
const lines: string[] = ['flowchart TD'];
|
|
107
|
+
function renderMermaidFromGraph(graph: StateGraph, compact = false): string {
|
|
108
|
+
const lines: string[] = [compact ? 'flowchart LR' : 'flowchart TD'];
|
|
109
109
|
if (graph.pages.length === 0) {
|
|
110
110
|
lines.push(' empty["No documented states"]');
|
|
111
111
|
return lines.join('\n');
|
|
@@ -117,6 +117,12 @@ function renderMermaidFromGraph(graph: StateGraph): string {
|
|
|
117
117
|
if (!children || children.length === 0) {
|
|
118
118
|
continue;
|
|
119
119
|
}
|
|
120
|
+
if (compact) {
|
|
121
|
+
for (const child of children) {
|
|
122
|
+
lines.push(` ${renderNodeLine(child)}`);
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
120
126
|
lines.push(` subgraph sg_${page.id} ["${escapeMermaidLabel(page.label)} — transient states"]`);
|
|
121
127
|
for (const child of children) {
|
|
122
128
|
lines.push(` ${renderNodeLine(child)}`);
|
|
@@ -125,11 +131,12 @@ function renderMermaidFromGraph(graph: StateGraph): string {
|
|
|
125
131
|
}
|
|
126
132
|
|
|
127
133
|
for (const edge of graph.edges) {
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
arrow
|
|
134
|
+
const arrow = edge.isBack ? '-.->' : '-->';
|
|
135
|
+
if (compact) {
|
|
136
|
+
lines.push(` ${edge.source} ${arrow} ${edge.target}`);
|
|
137
|
+
} else {
|
|
138
|
+
lines.push(` ${edge.source} ${arrow}|"${escapeMermaidLabel(edge.action)}"| ${edge.target}`);
|
|
131
139
|
}
|
|
132
|
-
lines.push(` ${edge.source} ${arrow}|"${escapeMermaidLabel(edge.action)}"| ${edge.target}`);
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
lines.push(' classDef page fill:#dbeafe,stroke:#2563eb,color:#0f172a;');
|
|
@@ -174,6 +181,46 @@ function renderStateMapFromGraph(graph: StateGraph): string {
|
|
|
174
181
|
return rows.join('\n');
|
|
175
182
|
}
|
|
176
183
|
|
|
184
|
+
function renderPageStateDiagram(label: string, url: string, interactions: StateTransition[]): string {
|
|
185
|
+
const targets = new Map<string, { node: StateNode; action: string; screenshot?: { title: string; relativePath: string } }>();
|
|
186
|
+
let index = 0;
|
|
187
|
+
for (const interaction of interactions) {
|
|
188
|
+
const targetState = interaction.targetState;
|
|
189
|
+
if (!targetState) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const key = `${targetState.kind}:${targetState.label}:${normalizeUrl(targetState.url)}`;
|
|
193
|
+
if (targets.has(key)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
targets.set(key, {
|
|
197
|
+
node: { id: `target${index++}`, kind: targetState.kind, label: targetState.label, subLabel: targetState.kind },
|
|
198
|
+
action: interaction.action,
|
|
199
|
+
screenshot: interaction.screenshot,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (targets.size === 0) {
|
|
204
|
+
return '';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const lines: string[] = ['flowchart LR'];
|
|
208
|
+
lines.push(` ${renderNodeLine({ id: 'self', kind: 'page', label, subLabel: url })}`);
|
|
209
|
+
for (const target of targets.values()) {
|
|
210
|
+
lines.push(` ${renderNodeLine(target.node)}`);
|
|
211
|
+
}
|
|
212
|
+
for (const target of targets.values()) {
|
|
213
|
+
lines.push(` self -->|"${escapeMermaidLabel(target.action)}"| ${target.node.id}`);
|
|
214
|
+
}
|
|
215
|
+
for (const target of targets.values()) {
|
|
216
|
+
if (target.screenshot) {
|
|
217
|
+
lines.push(` click ${target.node.id} "${target.screenshot.relativePath}" "${escapeMermaidLabel(target.screenshot.title)}"`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return lines.join('\n');
|
|
222
|
+
}
|
|
223
|
+
|
|
177
224
|
function renderNodeLine(node: StateNode): string {
|
|
178
225
|
const label = `${escapeMermaidLabel(node.label)}<br/>${escapeMermaidLabel(node.subLabel)}`;
|
|
179
226
|
if (node.kind === 'dialog' || node.kind === 'modal') {
|
|
@@ -207,7 +254,7 @@ function createsCycle(sourceId: string, targetId: string, adjacency: Map<string,
|
|
|
207
254
|
}
|
|
208
255
|
|
|
209
256
|
function escapeMermaidLabel(value: string): string {
|
|
210
|
-
return normalizeInlineText(value).replaceAll('&', '&').replaceAll('"', '"').replaceAll('|', '|');
|
|
257
|
+
return normalizeInlineText(value).replaceAll('&', '&').replaceAll('"', '"').replaceAll('#', '#').replaceAll('<', '<').replaceAll('>', '>').replaceAll('|', '|');
|
|
211
258
|
}
|
|
212
259
|
|
|
213
260
|
function escapeTable(value: string): string {
|
|
@@ -277,5 +324,5 @@ interface StateGraph {
|
|
|
277
324
|
classAssignment: Map<StateClass, string[]>;
|
|
278
325
|
}
|
|
279
326
|
|
|
280
|
-
export { buildStateGraph, renderMermaidBody, renderMermaidFromGraph, renderStateMapFromGraph };
|
|
327
|
+
export { buildStateGraph, renderMermaidBody, renderMermaidFromGraph, renderPageStateDiagram, renderStateMapFromGraph };
|
|
281
328
|
export type { DocumentedPage, SkippedPage, StateGraph, StateNode, StateEdge, StateClick };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "prima",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "High-level browser driver CLI for orchestrating agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "prima": "./bin/prima-cli.ts" },
|
|
7
|
+
"scripts": {
|
|
8
|
+
"format": "biome format --write .",
|
|
9
|
+
"lint:fix": "biome lint --write .",
|
|
10
|
+
"check:fix": "biome check --write ."
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"commander": "^14.0.1",
|
|
14
|
+
"dedent": "^1.6.0"
|
|
15
|
+
}
|
|
16
|
+
}
|