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/dist/src/config.js
CHANGED
|
@@ -10,16 +10,19 @@ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync,
|
|
|
10
10
|
import { tmpdir } from 'node:os';
|
|
11
11
|
import path, { basename, dirname, join, resolve } from 'node:path';
|
|
12
12
|
import { parseEnv } from 'node:util';
|
|
13
|
+
import dedent from 'dedent';
|
|
13
14
|
import matter from 'gray-matter';
|
|
15
|
+
import { findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from './global-config.js';
|
|
16
|
+
import { getCliName } from './utils/cli-name.js';
|
|
14
17
|
import { log } from './utils/logger.js';
|
|
15
18
|
export const PROVIDERS = {
|
|
16
|
-
openai: async () => (await import('@ai-sdk/openai')).createOpenAI(),
|
|
17
|
-
anthropic: async () => (await import('@ai-sdk/anthropic')).createAnthropic(),
|
|
18
|
-
google: async () => (await import('@ai-sdk/google')).createGoogleGenerativeAI(),
|
|
19
|
-
groq: async () => (await import('@ai-sdk/groq')).createGroq(),
|
|
20
|
-
mistral: async () => (await import('@ai-sdk/mistral')).createMistral(),
|
|
21
|
-
openrouter: async () => (await import('@openrouter/ai-sdk-provider')).createOpenRouter(),
|
|
22
|
-
sambanova: async () => (await import('sambanova-ai-provider')).createSambaNova(),
|
|
19
|
+
openai: { envKey: 'OPENAI_API_KEY', load: async () => (await import('@ai-sdk/openai')).createOpenAI() },
|
|
20
|
+
anthropic: { envKey: 'ANTHROPIC_API_KEY', load: async () => (await import('@ai-sdk/anthropic')).createAnthropic() },
|
|
21
|
+
google: { envKey: 'GOOGLE_GENERATIVE_AI_API_KEY', load: async () => (await import('@ai-sdk/google')).createGoogleGenerativeAI() },
|
|
22
|
+
groq: { envKey: 'GROQ_API_KEY', load: async () => (await import('@ai-sdk/groq')).createGroq() },
|
|
23
|
+
mistral: { envKey: 'MISTRAL_API_KEY', load: async () => (await import('@ai-sdk/mistral')).createMistral() },
|
|
24
|
+
openrouter: { envKey: 'OPENROUTER_API_KEY', load: async () => (await import('@openrouter/ai-sdk-provider')).createOpenRouter() },
|
|
25
|
+
sambanova: { envKey: 'SAMBANOVA_API_KEY', load: async () => (await import('sambanova-ai-provider')).createSambaNova() },
|
|
23
26
|
};
|
|
24
27
|
let cachedOutputRoot = null;
|
|
25
28
|
const config = {
|
|
@@ -38,7 +41,8 @@ export const EXPLORBOT_ENV_VARS = [
|
|
|
38
41
|
{ name: 'EXPLORBOT_URL', required: true, description: 'Base URL to test; the API boat reads it as the base endpoint' },
|
|
39
42
|
{ name: 'EXPLORBOT_VISION_MODEL', description: 'Screenshot analysis; overrides the provider recommendation' },
|
|
40
43
|
{ name: 'EXPLORBOT_AGENTIC_MODEL', description: 'Captain and Pilot decisions; overrides the provider recommendation' },
|
|
41
|
-
{ name: 'EXPLORBOT_OUTPUT', description: 'Output root for states, plans, research, and reports. Defaults to
|
|
44
|
+
{ name: 'EXPLORBOT_OUTPUT', description: 'Output root for states, plans, research, and reports. Defaults to the site dir under ~/.explorbot/sites' },
|
|
45
|
+
{ name: 'EXPLORBOT_EPHEMERAL', description: 'Keep no state between runs — output goes to a fresh temp directory instead of the site dir' },
|
|
42
46
|
{ name: 'EXPLORBOT_KNOWLEDGE', description: 'Inline knowledge text, applied to every page' },
|
|
43
47
|
{ name: 'EXPLORBOT_KNOWLEDGE_FILE', description: 'Path to a knowledge markdown file' },
|
|
44
48
|
{ name: 'EXPLORBOT_API_SPEC', description: 'OpenAPI spec path for the API boat' },
|
|
@@ -49,13 +53,24 @@ export class ConfigParser {
|
|
|
49
53
|
static recommended = null;
|
|
50
54
|
config = null;
|
|
51
55
|
configPath = null;
|
|
52
|
-
|
|
56
|
+
runtimeTarget = null;
|
|
57
|
+
site = null;
|
|
58
|
+
siteStartPath = '/';
|
|
53
59
|
constructor() { }
|
|
54
|
-
static loadEnv(filePath) {
|
|
60
|
+
static loadEnv(filePath, keepExisting = false) {
|
|
55
61
|
const resolved = resolve(filePath);
|
|
56
62
|
if (!existsSync(resolved))
|
|
57
63
|
return;
|
|
58
|
-
|
|
64
|
+
const parsed = parseEnv(readFileSync(resolved, 'utf8'));
|
|
65
|
+
if (!keepExisting) {
|
|
66
|
+
Object.assign(process.env, parsed);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
70
|
+
if (key in process.env)
|
|
71
|
+
continue;
|
|
72
|
+
process.env[key] = value;
|
|
73
|
+
}
|
|
59
74
|
}
|
|
60
75
|
static recommendedModels() {
|
|
61
76
|
ConfigParser.recommended ||= JSON.parse(readFileSync(new URL('../models.json', import.meta.url), 'utf8'));
|
|
@@ -68,7 +83,8 @@ export class ConfigParser {
|
|
|
68
83
|
return ConfigParser.instance;
|
|
69
84
|
}
|
|
70
85
|
async loadConfig(options) {
|
|
71
|
-
|
|
86
|
+
const target = options?.baseUrl || options?.from || null;
|
|
87
|
+
if (this.config && !options?.config && !options?.path && this.runtimeTarget === target) {
|
|
72
88
|
return this.config;
|
|
73
89
|
}
|
|
74
90
|
// Store the initial working directory for reference
|
|
@@ -81,6 +97,7 @@ export class ConfigParser {
|
|
|
81
97
|
process.chdir(resolvedWorkingPath);
|
|
82
98
|
}
|
|
83
99
|
ConfigParser.loadEnv('.env');
|
|
100
|
+
ConfigParser.loadEnv(globalEnvPath(), true);
|
|
84
101
|
try {
|
|
85
102
|
const resolvedPath = options?.config || this.findConfigFile();
|
|
86
103
|
let loadedConfig = null;
|
|
@@ -94,14 +111,22 @@ export class ConfigParser {
|
|
|
94
111
|
log(`Configuration loaded from: ${resolvedPath}`);
|
|
95
112
|
}
|
|
96
113
|
if (!resolvedPath) {
|
|
97
|
-
|
|
98
|
-
|
|
114
|
+
let envUrl = options?.baseUrl;
|
|
115
|
+
if (!envUrl && target?.startsWith('http'))
|
|
116
|
+
envUrl = target;
|
|
117
|
+
const outputRoot = resolveOutputRoot(process.env.EXPLORBOT_URL || envUrl);
|
|
118
|
+
loadedConfig = await this.buildEnvConfig(envUrl, outputRoot);
|
|
99
119
|
sourcePath = join(outputRoot, 'explorbot.config.js');
|
|
100
120
|
log(`Configuration built from EXPLORBOT_* environment variables. Output: ${outputRoot}`);
|
|
101
121
|
}
|
|
102
122
|
this.config = this.resolveConfig(loadedConfig, options);
|
|
103
|
-
this.
|
|
123
|
+
await resolveConfigModels(this.config.ai);
|
|
124
|
+
this.runtimeTarget = target;
|
|
104
125
|
this.configPath = sourcePath;
|
|
126
|
+
this.site = null;
|
|
127
|
+
if (resolvedPath && isGlobalConfigPath(resolvedPath)) {
|
|
128
|
+
this.enterGlobalMode(this.config, target);
|
|
129
|
+
}
|
|
105
130
|
// Restore original directory after successful config load
|
|
106
131
|
if (options?.path && originalCwd !== process.cwd()) {
|
|
107
132
|
process.chdir(originalCwd);
|
|
@@ -113,6 +138,8 @@ export class ConfigParser {
|
|
|
113
138
|
if (options?.path && originalCwd !== process.cwd()) {
|
|
114
139
|
process.chdir(originalCwd);
|
|
115
140
|
}
|
|
141
|
+
if (error instanceof ConfigMissingError)
|
|
142
|
+
throw error;
|
|
116
143
|
throw new Error(`Failed to load configuration: ${error}`);
|
|
117
144
|
}
|
|
118
145
|
}
|
|
@@ -127,22 +154,38 @@ export class ConfigParser {
|
|
|
127
154
|
}
|
|
128
155
|
getOutputDir() {
|
|
129
156
|
const config = this.getConfig();
|
|
130
|
-
|
|
131
|
-
if (!configPath)
|
|
157
|
+
if (!this.configPath)
|
|
132
158
|
throw new Error('Config path not found');
|
|
133
|
-
return path.join(
|
|
159
|
+
return path.join(this.getProjectRoot(), config.dirs?.output || 'output');
|
|
134
160
|
}
|
|
135
161
|
getProjectRoot() {
|
|
162
|
+
if (this.site)
|
|
163
|
+
return this.site.dir;
|
|
136
164
|
const configPath = this.getConfigPath();
|
|
137
165
|
if (configPath)
|
|
138
166
|
return path.dirname(configPath);
|
|
139
167
|
return process.cwd();
|
|
140
168
|
}
|
|
141
169
|
resolveProjectDir(relativeDir) {
|
|
142
|
-
|
|
143
|
-
if (!configPath)
|
|
170
|
+
if (!this.configPath)
|
|
144
171
|
return relativeDir;
|
|
145
|
-
return path.join(
|
|
172
|
+
return path.join(this.getProjectRoot(), relativeDir);
|
|
173
|
+
}
|
|
174
|
+
isGlobalMode() {
|
|
175
|
+
return !!this.site;
|
|
176
|
+
}
|
|
177
|
+
getSite() {
|
|
178
|
+
return this.site;
|
|
179
|
+
}
|
|
180
|
+
resolveTargetPath(target) {
|
|
181
|
+
if (!this.site)
|
|
182
|
+
return target || '/';
|
|
183
|
+
if (!target)
|
|
184
|
+
return this.siteStartPath;
|
|
185
|
+
const resolved = resolveSiteTarget(target, this.site.url);
|
|
186
|
+
if (resolved.baseUrl !== this.site.url)
|
|
187
|
+
return target;
|
|
188
|
+
return resolved.path;
|
|
146
189
|
}
|
|
147
190
|
getStatesDir() {
|
|
148
191
|
return outputPath('states');
|
|
@@ -159,7 +202,9 @@ export class ConfigParser {
|
|
|
159
202
|
if (ConfigParser.instance) {
|
|
160
203
|
ConfigParser.instance.config = null;
|
|
161
204
|
ConfigParser.instance.configPath = null;
|
|
162
|
-
ConfigParser.instance.
|
|
205
|
+
ConfigParser.instance.runtimeTarget = null;
|
|
206
|
+
ConfigParser.instance.site = null;
|
|
207
|
+
ConfigParser.instance.siteStartPath = '/';
|
|
163
208
|
}
|
|
164
209
|
}
|
|
165
210
|
// For testing purposes only - sets up minimal default config
|
|
@@ -206,11 +251,19 @@ export class ConfigParser {
|
|
|
206
251
|
// Ignore cleanup errors
|
|
207
252
|
}
|
|
208
253
|
}
|
|
254
|
+
enterGlobalMode(config, target) {
|
|
255
|
+
const site = resolveSiteTarget(target || undefined, config.web?.url || config.playwright?.url);
|
|
256
|
+
this.site = registerSite(site.baseUrl);
|
|
257
|
+
this.siteStartPath = site.path;
|
|
258
|
+
config.dirs = { knowledge: 'knowledge', experience: 'experience', output: 'output' };
|
|
259
|
+
config.playwright = { ...config.playwright, browser: config.playwright?.browser || 'chromium', url: site.baseUrl };
|
|
260
|
+
log(`Global mode: ${site.baseUrl} stored in ${this.site.dir}`);
|
|
261
|
+
}
|
|
209
262
|
async buildEnvConfig(baseUrl, outputRoot) {
|
|
210
263
|
const provider = process.env.EXPLORBOT_AI_PROVIDER;
|
|
211
264
|
const modelSpec = process.env.EXPLORBOT_AI_MODEL;
|
|
212
265
|
if (!provider && !modelSpec) {
|
|
213
|
-
throw new
|
|
266
|
+
throw new ConfigMissingError(missingConfigMessage());
|
|
214
267
|
}
|
|
215
268
|
if (modelSpec && !provider && !modelSpec.includes('/')) {
|
|
216
269
|
throw new Error('EXPLORBOT_AI_MODEL needs a provider — set EXPLORBOT_AI_PROVIDER, or write it as "provider/model-id"');
|
|
@@ -244,11 +297,14 @@ export class ConfigParser {
|
|
|
244
297
|
ai.agenticModel = await resolveModel(agenticSpec, 'agenticModel');
|
|
245
298
|
if (!agenticSpec && recommended.agenticModel)
|
|
246
299
|
ai.agenticModel = await resolveModel(provider, 'agenticModel');
|
|
300
|
+
const dirs = { knowledge: 'knowledge', experience: 'experience', output: 'output' };
|
|
301
|
+
if (process.env.EXPLORBOT_OUTPUT)
|
|
302
|
+
dirs.output = '.';
|
|
247
303
|
return {
|
|
248
304
|
playwright: { browser: 'chromium', url, show: false },
|
|
249
305
|
ai,
|
|
250
|
-
dirs
|
|
251
|
-
experience: { disabled:
|
|
306
|
+
dirs,
|
|
307
|
+
experience: { disabled: !!process.env.EXPLORBOT_EPHEMERAL },
|
|
252
308
|
};
|
|
253
309
|
}
|
|
254
310
|
findConfigFile() {
|
|
@@ -259,7 +315,9 @@ export class ConfigParser {
|
|
|
259
315
|
return fullPath;
|
|
260
316
|
}
|
|
261
317
|
}
|
|
262
|
-
|
|
318
|
+
if (envConfigRequested())
|
|
319
|
+
return null;
|
|
320
|
+
return findGlobalConfig();
|
|
263
321
|
}
|
|
264
322
|
async loadConfigModule(configPath) {
|
|
265
323
|
const ext = configPath.split('.').pop();
|
|
@@ -365,27 +423,77 @@ export async function resolveModel(spec, role = 'model') {
|
|
|
365
423
|
}
|
|
366
424
|
return createModel(spec, modelId);
|
|
367
425
|
}
|
|
368
|
-
export
|
|
426
|
+
export class ConfigMissingError extends Error {
|
|
427
|
+
}
|
|
428
|
+
export function envConfigRequested() {
|
|
429
|
+
return !!(process.env.EXPLORBOT_AI_PROVIDER || process.env.EXPLORBOT_AI_MODEL);
|
|
430
|
+
}
|
|
431
|
+
export function missingConfigMessage(configFile = 'explorbot.config.js') {
|
|
432
|
+
const cli = getCliName();
|
|
433
|
+
return dedent `
|
|
434
|
+
No AI configuration found. Set up explorbot in one of these ways:
|
|
435
|
+
|
|
436
|
+
Global - configure this machine once, then run from any directory:
|
|
437
|
+
${cli} init --global
|
|
438
|
+
|
|
439
|
+
Local - create ${configFile} for this project:
|
|
440
|
+
${cli} init
|
|
441
|
+
|
|
442
|
+
Environment - one-off run, no files written:
|
|
443
|
+
EXPLORBOT_AI_PROVIDER=openrouter EXPLORBOT_URL=https://your-app.example.com ${cli} ...
|
|
444
|
+
|
|
445
|
+
Providers: ${Object.keys(PROVIDERS).join(', ')}
|
|
446
|
+
`;
|
|
447
|
+
}
|
|
448
|
+
export async function resolveConfigModels(ai) {
|
|
449
|
+
if (!ai)
|
|
450
|
+
return;
|
|
451
|
+
const roles = ['model', 'visionModel', 'agenticModel'];
|
|
452
|
+
for (const role of roles) {
|
|
453
|
+
if (typeof ai[role] === 'string')
|
|
454
|
+
ai[role] = await resolveModel(ai[role], role);
|
|
455
|
+
}
|
|
456
|
+
for (const agent of Object.values(ai.agents || {})) {
|
|
457
|
+
if (typeof agent?.model === 'string')
|
|
458
|
+
agent.model = await resolveModel(agent.model);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
export function resolveOutputRoot(baseUrl) {
|
|
369
462
|
if (cachedOutputRoot)
|
|
370
463
|
return cachedOutputRoot;
|
|
371
464
|
const configured = process.env.EXPLORBOT_OUTPUT;
|
|
372
|
-
if (
|
|
373
|
-
cachedOutputRoot =
|
|
465
|
+
if (configured) {
|
|
466
|
+
cachedOutputRoot = resolve(configured);
|
|
467
|
+
mkdirSync(cachedOutputRoot, { recursive: true });
|
|
374
468
|
return cachedOutputRoot;
|
|
375
469
|
}
|
|
376
|
-
|
|
377
|
-
|
|
470
|
+
if (baseUrl) {
|
|
471
|
+
cachedOutputRoot = resolveStateRoot(baseUrl, !!process.env.EXPLORBOT_EPHEMERAL);
|
|
472
|
+
return cachedOutputRoot;
|
|
473
|
+
}
|
|
474
|
+
cachedOutputRoot = mkdtempSync(join(tmpdir(), 'explorbot-'));
|
|
378
475
|
return cachedOutputRoot;
|
|
379
476
|
}
|
|
477
|
+
export function resolveStateRoot(baseUrl, ephemeral) {
|
|
478
|
+
const url = URL.parse(baseUrl);
|
|
479
|
+
if (ephemeral || !url?.host)
|
|
480
|
+
return mkdtempSync(join(tmpdir(), 'explorbot-'));
|
|
481
|
+
return registerSite(url.origin).dir;
|
|
482
|
+
}
|
|
380
483
|
export function materializeKnowledge(outputRoot) {
|
|
381
484
|
const inline = process.env.EXPLORBOT_KNOWLEDGE;
|
|
382
485
|
const knowledgeFile = process.env.EXPLORBOT_KNOWLEDGE_FILE;
|
|
486
|
+
const knowledgeDir = join(outputRoot, 'knowledge');
|
|
487
|
+
const globalFile = join(knowledgeDir, 'global.md');
|
|
488
|
+
const envDir = join(knowledgeDir, 'env');
|
|
489
|
+
if (!inline)
|
|
490
|
+
rmSync(globalFile, { force: true });
|
|
491
|
+
rmSync(envDir, { recursive: true, force: true });
|
|
383
492
|
if (!inline && !knowledgeFile)
|
|
384
493
|
return;
|
|
385
|
-
const knowledgeDir = join(outputRoot, 'knowledge');
|
|
386
494
|
mkdirSync(knowledgeDir, { recursive: true });
|
|
387
495
|
if (inline) {
|
|
388
|
-
writeFileSync(
|
|
496
|
+
writeFileSync(globalFile, matter.stringify(inline, { url: '*', endpoint: '*' }));
|
|
389
497
|
}
|
|
390
498
|
if (!knowledgeFile)
|
|
391
499
|
return;
|
|
@@ -393,12 +501,13 @@ export function materializeKnowledge(outputRoot) {
|
|
|
393
501
|
if (!existsSync(source)) {
|
|
394
502
|
throw new Error(`Knowledge file from EXPLORBOT_KNOWLEDGE_FILE not found: ${source}`);
|
|
395
503
|
}
|
|
396
|
-
|
|
504
|
+
mkdirSync(envDir, { recursive: true });
|
|
505
|
+
copyFileSync(source, join(envDir, basename(source)));
|
|
397
506
|
}
|
|
398
507
|
export async function createModel(provider, modelId) {
|
|
399
|
-
const
|
|
400
|
-
if (!
|
|
508
|
+
const info = PROVIDERS[provider];
|
|
509
|
+
if (!info) {
|
|
401
510
|
throw new Error(`Unknown AI provider "${provider}". Supported providers: ${Object.keys(PROVIDERS).join(', ')}`);
|
|
402
511
|
}
|
|
403
|
-
return (await
|
|
512
|
+
return (await info.load())(modelId);
|
|
404
513
|
}
|
package/dist/src/explorbot.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Browser } from 'playwright';
|
|
1
2
|
import type { AgentDeps } from './ai/agent.js';
|
|
2
3
|
import { Captain } from './ai/captain.js';
|
|
3
4
|
import { Driller } from './ai/driller.js';
|
|
@@ -34,6 +35,10 @@ export interface ExplorBotOptions {
|
|
|
34
35
|
headless?: boolean;
|
|
35
36
|
incognito?: boolean;
|
|
36
37
|
session?: string | boolean;
|
|
38
|
+
instance?: string;
|
|
39
|
+
optionalAi?: boolean;
|
|
40
|
+
attachedBrowser?: Browser;
|
|
41
|
+
applicationSpec?: string;
|
|
37
42
|
}
|
|
38
43
|
export type UserResolveFunction = (error?: Error, showWelcome?: boolean) => Promise<string | null>;
|
|
39
44
|
export declare class ExplorBot {
|
|
@@ -47,6 +52,7 @@ export declare class ExplorBot {
|
|
|
47
52
|
planFeature?: string;
|
|
48
53
|
lastPlanError: Error | null;
|
|
49
54
|
lastSavedPlanPath: string | null;
|
|
55
|
+
aiFailure: string | null;
|
|
50
56
|
agents: Record<string, any>;
|
|
51
57
|
sessionPlans: Plan[];
|
|
52
58
|
lastReportedTestCount: number;
|
|
@@ -59,8 +65,10 @@ export declare class ExplorBot {
|
|
|
59
65
|
constructor(options?: ExplorBotOptions);
|
|
60
66
|
get isExploring(): boolean;
|
|
61
67
|
setUserResolve(fn: UserResolveFunction): void;
|
|
68
|
+
attachBrowser(browser: Browser): void;
|
|
62
69
|
start(): Promise<void>;
|
|
63
70
|
startProviderOnly(): Promise<void>;
|
|
71
|
+
aiFailureReason(): string | null;
|
|
64
72
|
stop(): Promise<void>;
|
|
65
73
|
visitInitialState(): Promise<void>;
|
|
66
74
|
visit(url: string): Promise<void>;
|
|
@@ -112,4 +120,5 @@ export declare class ExplorBot {
|
|
|
112
120
|
getSessionTests(): Test[];
|
|
113
121
|
printSessionAnalysis(): Promise<void>;
|
|
114
122
|
isHistorianEnabled(): boolean;
|
|
123
|
+
bootstrapOptionalProvider(): Promise<void>;
|
|
115
124
|
}
|
package/dist/src/explorbot.js
CHANGED
|
@@ -43,6 +43,7 @@ export class ExplorBot {
|
|
|
43
43
|
planFeature;
|
|
44
44
|
lastPlanError = null;
|
|
45
45
|
lastSavedPlanPath = null;
|
|
46
|
+
aiFailure = null;
|
|
46
47
|
agents = {};
|
|
47
48
|
sessionPlans = [];
|
|
48
49
|
lastReportedTestCount = 0;
|
|
@@ -66,6 +67,9 @@ export class ExplorBot {
|
|
|
66
67
|
setUserResolve(fn) {
|
|
67
68
|
this.userResolveFn = fn;
|
|
68
69
|
}
|
|
70
|
+
attachBrowser(browser) {
|
|
71
|
+
this.options.attachedBrowser = browser;
|
|
72
|
+
}
|
|
69
73
|
async start() {
|
|
70
74
|
if (this.explorer) {
|
|
71
75
|
return;
|
|
@@ -81,7 +85,7 @@ export class ExplorBot {
|
|
|
81
85
|
playwrightRecorder: this.playwrightRecorder(),
|
|
82
86
|
});
|
|
83
87
|
await this.explorer.start();
|
|
84
|
-
if (!this.options.incognito) {
|
|
88
|
+
if (!this.options.incognito && this.provider) {
|
|
85
89
|
await this.agentExperienceCompactor().autocompact();
|
|
86
90
|
}
|
|
87
91
|
}
|
|
@@ -97,19 +101,23 @@ export class ExplorBot {
|
|
|
97
101
|
this.config = await this.configParser.loadConfig(this.options);
|
|
98
102
|
if (this.options.session === true)
|
|
99
103
|
this.options.session = path.join(this.configParser.getOutputDir(), 'session.json');
|
|
104
|
+
if (this.options.optionalAi)
|
|
105
|
+
return this.bootstrapOptionalProvider();
|
|
100
106
|
this.provider = new AIProvider(this.config.ai);
|
|
101
107
|
await this.provider.validateConnection();
|
|
102
108
|
}
|
|
109
|
+
aiFailureReason() {
|
|
110
|
+
return this.aiFailure;
|
|
111
|
+
}
|
|
103
112
|
async stop() {
|
|
104
113
|
this.agents.quartermaster?.stop();
|
|
105
114
|
await this.explorer?.stop();
|
|
106
115
|
}
|
|
107
116
|
async visitInitialState() {
|
|
108
|
-
|
|
109
|
-
await this.visit(url);
|
|
117
|
+
await this.visit(this.options.from || '/');
|
|
110
118
|
}
|
|
111
119
|
async visit(url) {
|
|
112
|
-
return this.agentNavigator().visit(url);
|
|
120
|
+
return this.agentNavigator().visit(this.configParser.resolveTargetPath(url));
|
|
113
121
|
}
|
|
114
122
|
async openTab() {
|
|
115
123
|
await this.explorer.openTab();
|
|
@@ -121,7 +129,7 @@ export class ExplorBot {
|
|
|
121
129
|
return this.explorer;
|
|
122
130
|
}
|
|
123
131
|
knowledgeTracker() {
|
|
124
|
-
return (this._knowledgeTracker ||= new KnowledgeTracker());
|
|
132
|
+
return (this._knowledgeTracker ||= new KnowledgeTracker(this.options.applicationSpec));
|
|
125
133
|
}
|
|
126
134
|
experienceTracker() {
|
|
127
135
|
return (this._experienceTracker ||= new ExperienceTracker(this.knowledgeTracker()));
|
|
@@ -451,4 +459,15 @@ export class ExplorBot {
|
|
|
451
459
|
isHistorianEnabled() {
|
|
452
460
|
return this.config.ai?.agents?.historian?.enabled !== false;
|
|
453
461
|
}
|
|
462
|
+
async bootstrapOptionalProvider() {
|
|
463
|
+
try {
|
|
464
|
+
const provider = new AIProvider(this.config.ai);
|
|
465
|
+
await provider.validateConnection();
|
|
466
|
+
this.provider = provider;
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
this.aiFailure = browserErrorMessage(error);
|
|
470
|
+
tag('debug').log(`AI provider unavailable: ${this.aiFailure}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
454
473
|
}
|
package/dist/src/explorer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BrowserContextOptions, Page } from 'playwright';
|
|
1
|
+
import type { Browser, BrowserContextOptions, Page } from 'playwright';
|
|
2
2
|
import { ActionResult } from './action-result.js';
|
|
3
3
|
import Action from './action.js';
|
|
4
4
|
import type { RequestStore } from './api/request-store.js';
|
|
@@ -56,6 +56,7 @@ declare class Explorer {
|
|
|
56
56
|
initializeContainer(): void;
|
|
57
57
|
convertToCodeceptConfig(config: ExplorbotConfig): any;
|
|
58
58
|
connectOrLaunchBrowser(): Promise<void>;
|
|
59
|
+
openContextPage(): Promise<void>;
|
|
59
60
|
createBrowserContextOptions(): BrowserContextOptions;
|
|
60
61
|
attachXhrCapture(): void;
|
|
61
62
|
runWithRecovery<T>(label: string, operation: () => Promise<T>): Promise<T>;
|
|
@@ -81,6 +82,8 @@ export interface ExplorerOptions {
|
|
|
81
82
|
headless?: boolean;
|
|
82
83
|
incognito?: boolean;
|
|
83
84
|
session?: string;
|
|
85
|
+
instance?: string;
|
|
86
|
+
attachedBrowser?: Browser;
|
|
84
87
|
}
|
|
85
88
|
export interface ExplorerDeps {
|
|
86
89
|
stateManager: StateManager;
|
package/dist/src/explorer.js
CHANGED
|
@@ -74,8 +74,8 @@ class Explorer {
|
|
|
74
74
|
throw new Error('Playwright helper not available');
|
|
75
75
|
}
|
|
76
76
|
await this.connectOrLaunchBrowser();
|
|
77
|
-
const hasSession = this.options?.session && existsSync(this.options.session);
|
|
78
|
-
await this.
|
|
77
|
+
const hasSession = !this.options?.attachedBrowser && this.options?.session && existsSync(this.options.session);
|
|
78
|
+
await this.openContextPage();
|
|
79
79
|
await this.playwrightRecorder.start(this.playwrightHelper.browserContext);
|
|
80
80
|
this.attachXhrCapture();
|
|
81
81
|
if (hasSession) {
|
|
@@ -91,8 +91,9 @@ class Explorer {
|
|
|
91
91
|
if (!this.started)
|
|
92
92
|
return;
|
|
93
93
|
this.started = false;
|
|
94
|
+
const attached = !!this.options?.attachedBrowser;
|
|
94
95
|
await this.stopCaptures();
|
|
95
|
-
if (this.options?.session && this.playwrightHelper?.browserContext) {
|
|
96
|
+
if (!attached && this.options?.session && this.playwrightHelper?.browserContext) {
|
|
96
97
|
const dir = path.dirname(this.options.session);
|
|
97
98
|
if (!existsSync(dir))
|
|
98
99
|
mkdirSync(dir, { recursive: true });
|
|
@@ -105,7 +106,14 @@ class Explorer {
|
|
|
105
106
|
await Promise.all([this.reporter.finishRun(), this.playwrightHelper._stopBrowser(), codeceptjs.recorder.stop()]);
|
|
106
107
|
return;
|
|
107
108
|
}
|
|
108
|
-
|
|
109
|
+
if (attached) {
|
|
110
|
+
tag('info').log('Disconnecting from attached browser (its pages stay open)');
|
|
111
|
+
await this.playwrightHelper.browser?.close().catch((err) => {
|
|
112
|
+
debugLog('Failed to disconnect from attached browser:', err);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (!attached)
|
|
116
|
+
tag('info').log('Closing browser context (persistent browser stays running)');
|
|
109
117
|
await this.closeBrowserContext();
|
|
110
118
|
this.playwrightHelper.browser = null;
|
|
111
119
|
this.playwrightHelper.isRunning = false;
|
|
@@ -247,8 +255,15 @@ class Explorer {
|
|
|
247
255
|
return codeceptConfig;
|
|
248
256
|
}
|
|
249
257
|
async connectOrLaunchBrowser() {
|
|
258
|
+
if (this.options?.attachedBrowser) {
|
|
259
|
+
this.playwrightHelper.browser = this.options.attachedBrowser;
|
|
260
|
+
this.playwrightHelper.isRunning = true;
|
|
261
|
+
this.isSharedBrowser = true;
|
|
262
|
+
tag('success').log('Attached to a browser opened by another tool');
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
250
265
|
const { getAliveEndpoint } = await import('./browser-server.js');
|
|
251
|
-
const endpoint = await getAliveEndpoint();
|
|
266
|
+
const endpoint = await getAliveEndpoint(this.options?.instance);
|
|
252
267
|
if (endpoint) {
|
|
253
268
|
const browserName = this.config.playwright.browser || 'chromium';
|
|
254
269
|
this.playwrightHelper.options[browserName] ||= {};
|
|
@@ -261,6 +276,19 @@ class Explorer {
|
|
|
261
276
|
}
|
|
262
277
|
await this.playwrightHelper._startBrowser();
|
|
263
278
|
}
|
|
279
|
+
async openContextPage() {
|
|
280
|
+
const attached = this.options?.attachedBrowser;
|
|
281
|
+
if (!attached) {
|
|
282
|
+
await this.playwrightHelper._createContextPage(this.createBrowserContextOptions());
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const context = attached.contexts()[0] || (await attached.newContext());
|
|
286
|
+
const pages = context.pages();
|
|
287
|
+
const page = pages[pages.length - 1] || (await context.newPage());
|
|
288
|
+
this.playwrightHelper.browserContext = context;
|
|
289
|
+
await this.playwrightHelper._setPage(page);
|
|
290
|
+
debugLog(`Adopted attached browser page: ${page.url()}`);
|
|
291
|
+
}
|
|
264
292
|
createBrowserContextOptions() {
|
|
265
293
|
const helperOptions = this.playwrightHelper.options || {};
|
|
266
294
|
const contextOptions = {
|
|
@@ -427,6 +455,10 @@ class Explorer {
|
|
|
427
455
|
await this.playwrightRecorder.stop();
|
|
428
456
|
}
|
|
429
457
|
async closeBrowserContext() {
|
|
458
|
+
if (this.options?.attachedBrowser) {
|
|
459
|
+
this.playwrightHelper.browserContext = null;
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
430
462
|
if (!this.playwrightHelper.browserContext)
|
|
431
463
|
return;
|
|
432
464
|
await this.playwrightHelper.browserContext.close().catch((err) => {
|
|
@@ -447,7 +479,7 @@ class Explorer {
|
|
|
447
479
|
});
|
|
448
480
|
}
|
|
449
481
|
await this.connectOrLaunchBrowser();
|
|
450
|
-
await this.
|
|
482
|
+
await this.openContextPage();
|
|
451
483
|
await this.playwrightRecorder.start(this.playwrightHelper.browserContext);
|
|
452
484
|
this.attachXhrCapture();
|
|
453
485
|
this.listenToStateChanged();
|
|
@@ -542,6 +574,8 @@ class Explorer {
|
|
|
542
574
|
async closeOtherTabs() {
|
|
543
575
|
if (!this.playwrightHelper)
|
|
544
576
|
return;
|
|
577
|
+
if (this.options?.attachedBrowser)
|
|
578
|
+
return;
|
|
545
579
|
const context = this.playwrightHelper.page.context();
|
|
546
580
|
const pages = context.pages();
|
|
547
581
|
if (pages.length <= 1)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare function globalDir(): string;
|
|
2
|
+
export declare function globalEnvPath(): string;
|
|
3
|
+
export declare function globalConfigPath(): string;
|
|
4
|
+
export declare function findGlobalConfig(): string | null;
|
|
5
|
+
export declare function isGlobalConfigPath(configPath: string): boolean;
|
|
6
|
+
export declare function sitesDir(): string;
|
|
7
|
+
export declare function siteFolderName(url: string): string;
|
|
8
|
+
export declare function listSites(): SiteRecord[];
|
|
9
|
+
export declare function registerSite(baseUrl: string): SiteRecord;
|
|
10
|
+
export declare function resolveSiteTarget(target?: string, defaultBaseUrl?: string): SiteTarget;
|
|
11
|
+
interface SiteRecord {
|
|
12
|
+
folder: string;
|
|
13
|
+
dir: string;
|
|
14
|
+
url: string;
|
|
15
|
+
createdAt: string;
|
|
16
|
+
lastRunAt: string;
|
|
17
|
+
}
|
|
18
|
+
interface SiteTarget {
|
|
19
|
+
baseUrl: string;
|
|
20
|
+
path: string;
|
|
21
|
+
}
|
|
22
|
+
export type { SiteRecord, SiteTarget };
|