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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const GLOBAL_CONFIG_NAMES = ['config.js', 'config.mjs', 'config.ts'];
|
|
6
|
+
const SITE_DIRS = ['knowledge', 'experience', 'output'];
|
|
7
|
+
|
|
8
|
+
export function globalDir(): string {
|
|
9
|
+
return join(os.homedir(), '.explorbot');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function globalEnvPath(): string {
|
|
13
|
+
return join(globalDir(), '.env');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function globalConfigPath(): string {
|
|
17
|
+
return join(globalDir(), 'config.js');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function findGlobalConfig(): string | null {
|
|
21
|
+
for (const name of GLOBAL_CONFIG_NAMES) {
|
|
22
|
+
const fullPath = join(globalDir(), name);
|
|
23
|
+
if (existsSync(fullPath)) return fullPath;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isGlobalConfigPath(configPath: string): boolean {
|
|
29
|
+
return GLOBAL_CONFIG_NAMES.some((name) => join(globalDir(), name) === configPath);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function sitesDir(): string {
|
|
33
|
+
return join(globalDir(), 'sites');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function siteFolderName(url: string): string {
|
|
37
|
+
return new URL(url).host.toLowerCase().replace(/[^a-z0-9._-]/g, '_');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function listSites(): SiteRecord[] {
|
|
41
|
+
if (!existsSync(sitesDir())) return [];
|
|
42
|
+
|
|
43
|
+
return readdirSync(sitesDir(), { withFileTypes: true })
|
|
44
|
+
.filter((entry) => entry.isDirectory())
|
|
45
|
+
.map((entry) => readSite(entry.name))
|
|
46
|
+
.filter((site): site is SiteRecord => !!site)
|
|
47
|
+
.sort((a, b) => b.lastRunAt.localeCompare(a.lastRunAt));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function registerSite(baseUrl: string): SiteRecord {
|
|
51
|
+
const folder = siteFolderName(baseUrl);
|
|
52
|
+
const dir = join(sitesDir(), folder);
|
|
53
|
+
for (const subDir of SITE_DIRS) {
|
|
54
|
+
mkdirSync(join(dir, subDir), { recursive: true, mode: 0o700 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const now = new Date().toISOString();
|
|
58
|
+
const meta = {
|
|
59
|
+
url: baseUrl,
|
|
60
|
+
createdAt: readSite(folder)?.createdAt || now,
|
|
61
|
+
lastRunAt: now,
|
|
62
|
+
};
|
|
63
|
+
writeFileSync(join(dir, 'site.json'), `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
|
|
64
|
+
|
|
65
|
+
return { folder, dir, ...meta };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function resolveSiteTarget(target?: string, defaultBaseUrl?: string): SiteTarget {
|
|
69
|
+
const raw = (target || process.env.EXPLORBOT_URL || defaultBaseUrl || '').trim();
|
|
70
|
+
if (!raw) {
|
|
71
|
+
throw new Error(withSites('No site to explore. Pass a URL to the command or set EXPLORBOT_URL.'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (raw.startsWith('http://') || raw.startsWith('https://')) {
|
|
75
|
+
const url = new URL(raw);
|
|
76
|
+
return { baseUrl: url.origin, path: `${url.pathname}${url.search}${url.hash}` };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (raw.startsWith('/')) {
|
|
80
|
+
const base = defaultBaseUrl || process.env.EXPLORBOT_URL;
|
|
81
|
+
if (!base) {
|
|
82
|
+
throw new Error(withSites(`Cannot resolve path "${raw}" without a site.`));
|
|
83
|
+
}
|
|
84
|
+
return { baseUrl: new URL(base).origin, path: raw };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let reference = raw;
|
|
88
|
+
let path = '/';
|
|
89
|
+
const separator = raw.indexOf('/');
|
|
90
|
+
if (separator > -1) {
|
|
91
|
+
reference = raw.slice(0, separator);
|
|
92
|
+
path = raw.slice(separator);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const site = findSite(reference);
|
|
96
|
+
if (!site) {
|
|
97
|
+
throw new Error(withSites(`Unknown site "${reference}".`));
|
|
98
|
+
}
|
|
99
|
+
return { baseUrl: site.url, path };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function readSite(folder: string): SiteRecord | null {
|
|
103
|
+
const dir = join(sitesDir(), folder);
|
|
104
|
+
const metaPath = join(dir, 'site.json');
|
|
105
|
+
if (!existsSync(metaPath)) return null;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
109
|
+
new URL(meta.url);
|
|
110
|
+
return { folder, dir, url: meta.url, createdAt: meta.createdAt, lastRunAt: meta.lastRunAt };
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function findSite(reference: string): SiteRecord | null {
|
|
117
|
+
const normalized = reference.toLowerCase().replace(/[^a-z0-9._-]/g, '_');
|
|
118
|
+
const sites = listSites();
|
|
119
|
+
const byFolder = sites.find((site) => site.folder === normalized);
|
|
120
|
+
if (byFolder) return byFolder;
|
|
121
|
+
return sites.find((site) => new URL(site.url).host.toLowerCase() === reference.toLowerCase()) || null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function withSites(message: string): string {
|
|
125
|
+
const lines = [message];
|
|
126
|
+
const sites = listSites();
|
|
127
|
+
if (sites.length) {
|
|
128
|
+
lines.push('Registered sites:');
|
|
129
|
+
for (const site of sites) lines.push(` ${site.folder} → ${site.url}`);
|
|
130
|
+
}
|
|
131
|
+
lines.push('Explore a new site by passing its full URL, e.g. https://app.example.com/login');
|
|
132
|
+
return lines.join('\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface SiteRecord {
|
|
136
|
+
folder: string;
|
|
137
|
+
dir: string;
|
|
138
|
+
url: string;
|
|
139
|
+
createdAt: string;
|
|
140
|
+
lastRunAt: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface SiteTarget {
|
|
144
|
+
baseUrl: string;
|
|
145
|
+
path: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export type { SiteRecord, SiteTarget };
|
package/src/knowledge-tracker.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import dedent from 'dedent';
|
|
4
4
|
import matter from 'gray-matter';
|
|
5
5
|
import { ActionResult } from './action-result.js';
|
|
6
|
+
import { ApplicationSpec } from './application-spec.ts';
|
|
6
7
|
import { ConfigParser } from './config.js';
|
|
7
8
|
import { getCliName } from './utils/cli-name.ts';
|
|
8
9
|
import { createDebug, pluralize, tag } from './utils/logger.js';
|
|
@@ -24,8 +25,9 @@ export class KnowledgeTracker {
|
|
|
24
25
|
private knowledgeDir: string;
|
|
25
26
|
private knowledgeFiles: Knowledge[] = [];
|
|
26
27
|
private isLoaded = false;
|
|
28
|
+
private applicationSpec?: ApplicationSpec;
|
|
27
29
|
|
|
28
|
-
constructor() {
|
|
30
|
+
constructor(applicationSpecPath?: string) {
|
|
29
31
|
const configParser = ConfigParser.getInstance();
|
|
30
32
|
const config = configParser.getConfig();
|
|
31
33
|
this.knowledgeDir = configParser.resolveProjectDir(config.dirs?.knowledge || 'knowledge');
|
|
@@ -33,6 +35,12 @@ export class KnowledgeTracker {
|
|
|
33
35
|
if (!existsSync(this.knowledgeDir)) {
|
|
34
36
|
mkdirSync(this.knowledgeDir, { recursive: true });
|
|
35
37
|
}
|
|
38
|
+
|
|
39
|
+
const specPath = applicationSpecPath || config.dirs?.spec;
|
|
40
|
+
if (specPath) {
|
|
41
|
+
this.applicationSpec = new ApplicationSpec(specPath);
|
|
42
|
+
tag('info').log(`Loaded application spec with ${this.applicationSpec.pageCount} documented pages`);
|
|
43
|
+
}
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
private loadKnowledgeFiles(): void {
|
|
@@ -79,6 +87,14 @@ export class KnowledgeTracker {
|
|
|
79
87
|
`;
|
|
80
88
|
}
|
|
81
89
|
|
|
90
|
+
renderRelevantContext(state: ActionResult): string {
|
|
91
|
+
return [this.renderRelevantKnowledge(state), this.renderApplicationSpec(state)].filter(Boolean).join('\n\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
renderApplicationSpec(state: ActionResult): string {
|
|
95
|
+
return this.applicationSpec?.renderFor(state) || '';
|
|
96
|
+
}
|
|
97
|
+
|
|
82
98
|
addKnowledge(urlPattern: string, description: string): { filename: string; filePath: string; isNewFile: boolean } {
|
|
83
99
|
const configParser = ConfigParser.getInstance();
|
|
84
100
|
const configPath = configParser.getConfigPath();
|
package/src/utils/cli-name.ts
CHANGED
|
@@ -7,7 +7,10 @@ export function getCliName(): string {
|
|
|
7
7
|
const ua = process.env.npm_config_user_agent || '';
|
|
8
8
|
if (ua.includes('bun')) cached = 'bunx explorbot';
|
|
9
9
|
else if (ua.includes('npm')) cached = 'npx explorbot';
|
|
10
|
-
else if (process.argv[1]?.endsWith('.ts'))
|
|
11
|
-
|
|
10
|
+
else if (process.argv[1]?.endsWith('.ts')) {
|
|
11
|
+
let script = path.relative(process.cwd(), process.argv[1]);
|
|
12
|
+
if (script.startsWith('..')) script = process.argv[1];
|
|
13
|
+
cached = `bun ${script}`;
|
|
14
|
+
} else cached = 'explorbot';
|
|
12
15
|
return cached;
|
|
13
16
|
}
|
package/src/utils/test-files.ts
CHANGED
|
@@ -67,8 +67,7 @@ export async function dryRunTestFile(filePath: string): Promise<void> {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const config = ConfigParser.getInstance().getConfig();
|
|
70
|
-
const
|
|
71
|
-
const projectRoot = configPath ? path.dirname(configPath) : process.cwd();
|
|
70
|
+
const projectRoot = ConfigParser.getInstance().getProjectRoot();
|
|
72
71
|
|
|
73
72
|
const codeceptConfig = {
|
|
74
73
|
helpers: {
|
package/src/utils/url-matcher.ts
CHANGED
|
@@ -93,3 +93,13 @@ export function extractStatePath(url: string): string {
|
|
|
93
93
|
return url;
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
+
|
|
97
|
+
export function matchesNavigationUrl(expected: string, current: string): boolean {
|
|
98
|
+
const expectedPath = extractStatePath(expected);
|
|
99
|
+
let currentPath = extractStatePath(current);
|
|
100
|
+
if (!expectedPath.includes('#')) {
|
|
101
|
+
currentPath = currentPath.split('#')[0];
|
|
102
|
+
}
|
|
103
|
+
const normalize = (value: string) => value.replace(/^\/+|\/+$/g, '').toLowerCase();
|
|
104
|
+
return normalize(expectedPath) === normalize(currentPath);
|
|
105
|
+
}
|