explorbot 0.4.7 โ 0.4.9
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/boat/api-tester/src/ai/chief.ts +3 -1
- package/boat/api-tester/src/ai/curler.ts +4 -0
- package/boat/api-tester/src/cli.ts +1 -1
- package/boat/api-tester/src/config.ts +34 -26
- package/dist/boat/api-tester/src/ai/chief.js +3 -1
- package/dist/boat/api-tester/src/ai/curler.js +4 -0
- package/dist/boat/api-tester/src/cli.js +1 -1
- package/dist/boat/api-tester/src/config.js +32 -26
- package/dist/package.json +1 -1
- package/dist/rules/chief/general.md +2 -0
- package/dist/rules/researcher/pagination.md +1 -0
- package/dist/src/action.js +3 -2
- package/dist/src/ai/fisherman/tools.js +10 -4
- package/dist/src/ai/navigator.js +1 -4
- package/dist/src/ai/pilot.js +19 -18
- package/dist/src/ai/planner/session-dedup.d.ts +2 -1
- package/dist/src/ai/planner/session-dedup.js +18 -1
- package/dist/src/ai/planner.js +8 -4
- package/dist/src/ai/provider.js +18 -4
- package/dist/src/ai/rules.js +1 -0
- package/dist/src/ai/tester.js +1 -1
- package/dist/src/ai/tools.js +10 -2
- package/dist/src/commands/config-command.d.ts +2 -0
- package/dist/src/commands/config-command.js +8 -2
- package/dist/src/commands/init-command.js +6 -26
- package/dist/src/commands/sites-command.js +6 -1
- package/dist/src/config.d.ts +5 -4
- package/dist/src/config.js +25 -23
- package/dist/src/global-config.d.ts +6 -0
- package/dist/src/global-config.js +82 -7
- package/dist/src/utils/code-extractor.js +6 -2
- package/dist/src/utils/html.js +6 -0
- package/dist/src/utils/merge.d.ts +1 -0
- package/dist/src/utils/merge.js +11 -0
- package/docs/reference/commands.md +10 -2
- package/docs/reference/configuration.md +37 -4
- package/docs/superpowers/plans/2026-09-15-mdq-package.md +2029 -0
- package/docs/superpowers/specs/2026-09-14-mdq-package-design.md +397 -0
- package/package.json +1 -1
- package/rules/chief/general.md +2 -0
- package/rules/researcher/pagination.md +1 -0
- package/src/action.ts +3 -2
- package/src/ai/fisherman/tools.ts +10 -4
- package/src/ai/navigator.ts +1 -4
- package/src/ai/pilot.ts +19 -18
- package/src/ai/planner/session-dedup.ts +16 -2
- package/src/ai/planner.ts +8 -4
- package/src/ai/provider.ts +18 -3
- package/src/ai/rules.ts +1 -0
- package/src/ai/tester.ts +1 -1
- package/src/ai/tools.ts +9 -2
- package/src/commands/config-command.ts +9 -2
- package/src/commands/init-command.ts +6 -27
- package/src/commands/sites-command.ts +6 -1
- package/src/config.ts +28 -25
- package/src/global-config.ts +81 -6
- package/src/utils/code-extractor.ts +6 -2
- package/src/utils/html.ts +6 -0
- package/src/utils/merge.ts +13 -0
package/dist/src/ai/tester.js
CHANGED
|
@@ -29,7 +29,7 @@ const SAMPLE_FILES = {
|
|
|
29
29
|
};
|
|
30
30
|
export class Tester extends TaskAgent {
|
|
31
31
|
ACTION_TOOLS = ['click', 'hover', 'pressKey', 'form'];
|
|
32
|
-
DELEGATED_ACTION_TOOLS = ['interact'];
|
|
32
|
+
DELEGATED_ACTION_TOOLS = ['interact', 'visualClick'];
|
|
33
33
|
SPECIAL_CONTEXT_ACTION_TOOLS = ['exitIframe'];
|
|
34
34
|
emoji = '๐งช';
|
|
35
35
|
requestStore;
|
package/dist/src/ai/tools.js
CHANGED
|
@@ -312,6 +312,7 @@ export function createCodeceptJSTools({ explorer, stateManager }, task) {
|
|
|
312
312
|
- Performing multiple form actions in a single batch
|
|
313
313
|
- Complex interactions requiring sequential commands
|
|
314
314
|
- Reaching items further down a list (I.scrollTo)
|
|
315
|
+
- Reloading the page to prove a change outlived it (I.reloadPage)
|
|
315
316
|
|
|
316
317
|
Example - filling a form with context (PREFERRED):
|
|
317
318
|
I.fillField('Username', 'John', '.login-form')
|
|
@@ -324,7 +325,7 @@ export function createCodeceptJSTools({ explorer, stateManager }, task) {
|
|
|
324
325
|
I.selectOption({"role":"combobox","text":"Category"}, 'Technology')
|
|
325
326
|
|
|
326
327
|
Do not submit form - use verify() first to check fields were filled correctly, then click() to submit.
|
|
327
|
-
Do not use: wait functions, amOnPage,
|
|
328
|
+
Do not use: wait functions, amOnPage, saveScreenshot
|
|
328
329
|
`,
|
|
329
330
|
inputSchema: z.object({
|
|
330
331
|
codeBlock: z.string().describe('Valid CodeceptJS code starting with I. Can contain multiple commands separated by newlines.'),
|
|
@@ -1201,7 +1202,8 @@ async function extractWebElements(error) {
|
|
|
1201
1202
|
function formatElementList(matched) {
|
|
1202
1203
|
if (!matched)
|
|
1203
1204
|
return 'Could not fetch element details. Repeat the action to get better info.';
|
|
1204
|
-
|
|
1205
|
+
const keys = matched.map((el) => `${el.text}::${el.visible}::${el.html}`);
|
|
1206
|
+
const list = matched
|
|
1205
1207
|
.map((el, i) => {
|
|
1206
1208
|
const lines = [`Element ${i + 1}:`, `Text: "${el.text}"`];
|
|
1207
1209
|
if (el.visible !== undefined)
|
|
@@ -1209,10 +1211,16 @@ function formatElementList(matched) {
|
|
|
1209
1211
|
const wrapped = matched.map((_, j) => j).filter((j) => j !== i && matched[j].xpath.startsWith(`${el.xpath}/`));
|
|
1210
1212
|
if (wrapped.length)
|
|
1211
1213
|
lines.push(`Wraps: element ${wrapped.map((j) => j + 1).join(', ')}`);
|
|
1214
|
+
const same = keys.map((_, j) => j).filter((j) => j !== i && keys[j] === keys[i]);
|
|
1215
|
+
if (same.length)
|
|
1216
|
+
lines.push(`Identical to element ${same.map((j) => j + 1).join(', ')}`);
|
|
1212
1217
|
lines.push(`XPath: ${el.xpath}`, `HTML: ${el.html}`);
|
|
1213
1218
|
return lines.join('\n');
|
|
1214
1219
|
})
|
|
1215
1220
|
.join('\n\n');
|
|
1221
|
+
if (new Set(keys).size === matched.length)
|
|
1222
|
+
return list;
|
|
1223
|
+
return `${list}\n\nIdentical matches are not told apart by their number โ picking one is a guess. Click the one you mean by appearance with visualClick().`;
|
|
1216
1224
|
}
|
|
1217
1225
|
export async function formatMatchedElements(error) {
|
|
1218
1226
|
return formatElementList(await extractWebElements(error));
|
|
@@ -15,11 +15,13 @@ export declare class ConfigCommand extends BaseCommand {
|
|
|
15
15
|
}
|
|
16
16
|
interface ConfigSummaryOptions {
|
|
17
17
|
configPath?: string | null;
|
|
18
|
+
siteConfigPath?: string | null;
|
|
18
19
|
root?: string;
|
|
19
20
|
json?: boolean;
|
|
20
21
|
}
|
|
21
22
|
export interface ConfigData {
|
|
22
23
|
config: string;
|
|
24
|
+
siteConfig: string;
|
|
23
25
|
url: string;
|
|
24
26
|
browser: string;
|
|
25
27
|
headless: boolean;
|
|
@@ -13,7 +13,7 @@ export class ConfigCommand extends BaseCommand {
|
|
|
13
13
|
description = 'Show models, config file and paths used by this run';
|
|
14
14
|
async execute() {
|
|
15
15
|
const parser = ConfigParser.getInstance();
|
|
16
|
-
tag('info').log(ConfigCommand.render(this.explorBot.getConfig(), { configPath: parser.getConfigPath(), root: parser.getProjectRoot() }));
|
|
16
|
+
tag('info').log(ConfigCommand.render(this.explorBot.getConfig(), { configPath: parser.getConfigPath(), siteConfigPath: parser.getSiteConfigPath(), root: parser.getProjectRoot() }));
|
|
17
17
|
}
|
|
18
18
|
static async summary(options = {}) {
|
|
19
19
|
const parser = ConfigParser.getInstance();
|
|
@@ -24,12 +24,15 @@ export class ConfigCommand extends BaseCommand {
|
|
|
24
24
|
throw error;
|
|
25
25
|
return load(site.url);
|
|
26
26
|
});
|
|
27
|
-
return ConfigCommand.render(config, { configPath: parser.getConfigPath(), root: parser.getProjectRoot(), json: options.json });
|
|
27
|
+
return ConfigCommand.render(config, { configPath: parser.getConfigPath(), siteConfigPath: parser.getSiteConfigPath(), root: parser.getProjectRoot(), json: options.json });
|
|
28
28
|
}
|
|
29
29
|
static data(config, options = {}) {
|
|
30
30
|
let configPath = '';
|
|
31
31
|
if (options.configPath && existsSync(options.configPath))
|
|
32
32
|
configPath = options.configPath;
|
|
33
|
+
let siteConfigPath = '';
|
|
34
|
+
if (options.siteConfigPath && existsSync(options.siteConfigPath))
|
|
35
|
+
siteConfigPath = options.siteConfigPath;
|
|
33
36
|
const dirs = {};
|
|
34
37
|
if (options.root) {
|
|
35
38
|
for (const [name, dir] of Object.entries({ output: 'output', ...config.dirs })) {
|
|
@@ -55,6 +58,7 @@ export class ConfigCommand extends BaseCommand {
|
|
|
55
58
|
}
|
|
56
59
|
return {
|
|
57
60
|
config: configPath,
|
|
61
|
+
siteConfig: siteConfigPath,
|
|
58
62
|
url: config.playwright?.url || config.web?.url || config.api?.baseEndpoint || '',
|
|
59
63
|
browser: config.playwright?.browser || '',
|
|
60
64
|
headless: !config.playwright?.show,
|
|
@@ -75,6 +79,8 @@ export class ConfigCommand extends BaseCommand {
|
|
|
75
79
|
const lines = [];
|
|
76
80
|
const section = (title, entries) => lines.push(...renderSection(title, entries));
|
|
77
81
|
const general = [['config', data.config || 'EXPLORBOT_* environment variables']];
|
|
82
|
+
if (data.siteConfig)
|
|
83
|
+
general.push(['site config', data.siteConfig]);
|
|
78
84
|
if (data.url)
|
|
79
85
|
general.push(['url', data.url]);
|
|
80
86
|
if (data.browser) {
|
|
@@ -6,10 +6,7 @@ import { findGlobalConfig, globalConfigPath, globalDir, globalEnvPath } from "..
|
|
|
6
6
|
import { getCliName } from "../utils/cli-name.js";
|
|
7
7
|
import { log, tag } from '../utils/logger.js';
|
|
8
8
|
import { relativeToCwd } from "../utils/next-steps.js";
|
|
9
|
-
function defaultConfigTemplate(provider
|
|
10
|
-
let moduleExport = 'module.exports = config;';
|
|
11
|
-
if (esm)
|
|
12
|
-
moduleExport = 'export default config;';
|
|
9
|
+
function defaultConfigTemplate(provider) {
|
|
13
10
|
return `// 'provider/model-id' uses a bundled provider.
|
|
14
11
|
// It is also possible to import provider as a module from Vercel AI SDK.
|
|
15
12
|
// https://github.com/testomatio/explorbot/blob/main/docs/basics/providers.md
|
|
@@ -34,7 +31,7 @@ ${modelLines(provider)}
|
|
|
34
31
|
},
|
|
35
32
|
};
|
|
36
33
|
|
|
37
|
-
|
|
34
|
+
export default config;
|
|
38
35
|
`;
|
|
39
36
|
}
|
|
40
37
|
export function envTemplate(provider) {
|
|
@@ -128,8 +125,7 @@ export function runInitCommand(options) {
|
|
|
128
125
|
log('Use --force to overwrite existing file');
|
|
129
126
|
process.exit(1);
|
|
130
127
|
}
|
|
131
|
-
|
|
132
|
-
writeFileSync(outPath, defaultConfigTemplate(provider, esm), 'utf8');
|
|
128
|
+
writeFileSync(outPath, defaultConfigTemplate(provider), 'utf8');
|
|
133
129
|
log(`Created config file: ${relativeToCwd(outPath)}`);
|
|
134
130
|
const envPath = resolve(process.cwd(), '.env');
|
|
135
131
|
if (!existsSync(envPath)) {
|
|
@@ -234,6 +230,8 @@ export function modelLines(provider, only) {
|
|
|
234
230
|
function globalConfigTemplate(provider) {
|
|
235
231
|
const { envKey } = PROVIDERS[provider];
|
|
236
232
|
return `// Global Explorbot configuration โ used by every directory without its own explorbot.config.js.
|
|
233
|
+
// Settings shared by every site. Each site extends them in
|
|
234
|
+
// ~/.explorbot/sites/<host>/explorbot.config.js, written on its first run.
|
|
237
235
|
// Models are written as 'provider/model-id' so they resolve without a local node_modules.
|
|
238
236
|
// The key is read from ${envKey} in ~/.explorbot/.env
|
|
239
237
|
// Model ids are snapshotted from the recommendations of this Explorbot version.
|
|
@@ -251,27 +249,9 @@ ${modelLines(provider)}
|
|
|
251
249
|
},
|
|
252
250
|
};
|
|
253
251
|
|
|
254
|
-
|
|
252
|
+
export default config;
|
|
255
253
|
`;
|
|
256
254
|
}
|
|
257
|
-
function isModuleProject(configDir) {
|
|
258
|
-
let currentDir = resolve(configDir);
|
|
259
|
-
while (true) {
|
|
260
|
-
const packagePath = join(currentDir, 'package.json');
|
|
261
|
-
if (existsSync(packagePath)) {
|
|
262
|
-
try {
|
|
263
|
-
return JSON.parse(readFileSync(packagePath, 'utf8')).type === 'module';
|
|
264
|
-
}
|
|
265
|
-
catch {
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
const parentDir = dirname(currentDir);
|
|
270
|
-
if (parentDir === currentDir)
|
|
271
|
-
return false;
|
|
272
|
-
currentDir = parentDir;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
255
|
function writeEnvKey(key, value) {
|
|
276
256
|
const envPath = globalEnvPath();
|
|
277
257
|
let content = '# AI provider API keys';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { listSites, sitesDir } from '../global-config.js';
|
|
1
|
+
import { findSiteConfig, listSites, sitesDir } from '../global-config.js';
|
|
2
2
|
import { getCliName } from '../utils/cli-name.js';
|
|
3
3
|
import { tag } from '../utils/logger.js';
|
|
4
4
|
import { BaseCommand } from './base-command.js';
|
|
@@ -16,6 +16,11 @@ export class SitesCommand extends BaseCommand {
|
|
|
16
16
|
tag('info').log(`Registered sites (${sites.length}):`);
|
|
17
17
|
for (const site of sites) {
|
|
18
18
|
tag('info').log(` ${site.folder.padEnd(width)} ${site.url} last run ${site.lastRunAt.slice(0, 16).replace('T', ' ')}`);
|
|
19
|
+
let config = 'inherits global config';
|
|
20
|
+
const sitePath = findSiteConfig(site.dir);
|
|
21
|
+
if (sitePath)
|
|
22
|
+
config = sitePath;
|
|
23
|
+
tag('info').log(` ${' '.repeat(width)} ${config}`);
|
|
19
24
|
}
|
|
20
25
|
tag('info').log('');
|
|
21
26
|
tag('info').log(`Stored in ${sitesDir()}`);
|
package/dist/src/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SiteRecord } from './global-config.js';
|
|
1
|
+
import { EXPLORBOT_CONFIG_PATHS, type SiteRecord } from './global-config.js';
|
|
2
2
|
export declare const PROVIDERS: Record<string, ProviderInfo>;
|
|
3
3
|
export declare const MODEL_ROLES: ModelRole[];
|
|
4
4
|
interface PlaywrightConfig {
|
|
@@ -224,7 +224,7 @@ interface ExplorbotConfig {
|
|
|
224
224
|
dynamicPageRegex?: string;
|
|
225
225
|
}
|
|
226
226
|
type RuleEntry = string | Record<string, string>;
|
|
227
|
-
export
|
|
227
|
+
export { EXPLORBOT_CONFIG_PATHS };
|
|
228
228
|
export declare const EXPLORBOT_ENV_VARS: EnvVar[];
|
|
229
229
|
export type { ExplorbotConfig, PlaywrightConfig, AIConfig, HtmlConfig, ActionConfig, AgentConfig, AgentsConfig, HistorianAgentConfig, ResearcherAgentConfig, NavigatorAgentConfig, PlannerAgentConfig, ScoutAgentConfig, RerunnerAgentConfig, HealRecipe, Hook, HookConfig, HooksConfig, PlaywrightHook, CodeceptJSHook, HookPatternMap, RuleEntry, ReporterConfig, ApiConfig, WebConfig, ApiHookFn, };
|
|
230
230
|
export declare class ConfigParser {
|
|
@@ -235,6 +235,7 @@ export declare class ConfigParser {
|
|
|
235
235
|
runtimeTarget: string | null;
|
|
236
236
|
site: SiteRecord | null;
|
|
237
237
|
siteStartPath: string;
|
|
238
|
+
siteConfigPath: string | null;
|
|
238
239
|
constructor();
|
|
239
240
|
static loadEnv(filePath: string, keepExisting?: boolean): void;
|
|
240
241
|
static recommendedModels(): Record<string, Record<string, string>>;
|
|
@@ -252,6 +253,7 @@ export declare class ConfigParser {
|
|
|
252
253
|
resolveProjectDir(relativeDir: string): string;
|
|
253
254
|
isGlobalMode(): boolean;
|
|
254
255
|
getSite(): SiteRecord | null;
|
|
256
|
+
getSiteConfigPath(): string | null;
|
|
255
257
|
resolveTargetPath(target?: string): string;
|
|
256
258
|
getStatesDir(): string;
|
|
257
259
|
getPlansDir(): string;
|
|
@@ -260,7 +262,7 @@ export declare class ConfigParser {
|
|
|
260
262
|
static setupTestConfig(): void;
|
|
261
263
|
static getTestDirectories(): string[];
|
|
262
264
|
static cleanupAllTestDirectories(): void;
|
|
263
|
-
enterGlobalMode(config: ExplorbotConfig, target: string | null):
|
|
265
|
+
enterGlobalMode(config: ExplorbotConfig, target: string | null): Promise<ExplorbotConfig>;
|
|
264
266
|
applyEnvSpec(config: ExplorbotConfig): void;
|
|
265
267
|
buildEnvConfig(baseUrl: string | undefined, outputRoot: string): Promise<ExplorbotConfig>;
|
|
266
268
|
findConfigFile(): string | null;
|
|
@@ -271,7 +273,6 @@ export declare class ConfigParser {
|
|
|
271
273
|
validateConfig(config: ExplorbotConfig): void;
|
|
272
274
|
getNestedValue(obj: any, path: string): any;
|
|
273
275
|
mergeWithDefaults(config: Partial<ExplorbotConfig>): ExplorbotConfig;
|
|
274
|
-
deepMerge(target: any, source: any): any;
|
|
275
276
|
ensureDirectory(path: string): void;
|
|
276
277
|
}
|
|
277
278
|
export declare function setOutputDir(dir: string): void;
|
package/dist/src/config.js
CHANGED
|
@@ -13,8 +13,9 @@ import { pathToFileURL } from 'node:url';
|
|
|
13
13
|
import { parseEnv } from 'node:util';
|
|
14
14
|
import dedent from 'dedent';
|
|
15
15
|
import matter from 'gray-matter';
|
|
16
|
-
import { findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from './global-config.js';
|
|
16
|
+
import { EXPLORBOT_CONFIG_PATHS, findGlobalConfig, globalEnvPath, isGlobalConfigPath, loadSiteConfig, registerSite, resolveSiteTarget } from './global-config.js';
|
|
17
17
|
import { getCliName } from './utils/cli-name.js';
|
|
18
|
+
import { deepMerge } from './utils/merge.js';
|
|
18
19
|
import { log, tag } from './utils/logger.js';
|
|
19
20
|
export const PROVIDERS = {
|
|
20
21
|
openai: { envKey: 'OPENAI_API_KEY', load: async () => (await import('@ai-sdk/openai')).createOpenAI() },
|
|
@@ -37,7 +38,7 @@ const config = {
|
|
|
37
38
|
model: null,
|
|
38
39
|
},
|
|
39
40
|
};
|
|
40
|
-
export
|
|
41
|
+
export { EXPLORBOT_CONFIG_PATHS };
|
|
41
42
|
export const EXPLORBOT_ENV_VARS = [
|
|
42
43
|
{ name: 'EXPLORBOT_AI_PROVIDER', required: true, description: 'Provider name; fills every model role from its recommended models. Turns on config-free mode' },
|
|
43
44
|
{ name: 'EXPLORBOT_AI_MODEL', description: 'Pins the main model โ a model id for the provider, or a standalone provider/model-id' },
|
|
@@ -62,6 +63,7 @@ export class ConfigParser {
|
|
|
62
63
|
runtimeTarget = null;
|
|
63
64
|
site = null;
|
|
64
65
|
siteStartPath = '/';
|
|
66
|
+
siteConfigPath = null;
|
|
65
67
|
constructor() { }
|
|
66
68
|
static loadEnv(filePath, keepExisting = false) {
|
|
67
69
|
const resolved = resolve(filePath);
|
|
@@ -125,14 +127,16 @@ export class ConfigParser {
|
|
|
125
127
|
sourcePath = join(outputRoot, 'explorbot.config.js');
|
|
126
128
|
log(`Configuration built from EXPLORBOT_* environment variables. Output: ${outputRoot}`);
|
|
127
129
|
}
|
|
128
|
-
|
|
129
|
-
await resolveConfigModels(
|
|
130
|
-
this.runtimeTarget = target;
|
|
131
|
-
this.configPath = sourcePath;
|
|
130
|
+
let config = this.resolveConfig(loadedConfig, options);
|
|
131
|
+
await resolveConfigModels(config.ai);
|
|
132
132
|
this.site = null;
|
|
133
|
+
this.siteConfigPath = null;
|
|
133
134
|
if (resolvedPath && isGlobalConfigPath(resolvedPath)) {
|
|
134
|
-
this.enterGlobalMode(
|
|
135
|
+
config = await this.enterGlobalMode(config, target);
|
|
135
136
|
}
|
|
137
|
+
this.config = config;
|
|
138
|
+
this.runtimeTarget = target;
|
|
139
|
+
this.configPath = sourcePath;
|
|
136
140
|
this.applyEnvSpec(this.config);
|
|
137
141
|
// Restore original directory after successful config load
|
|
138
142
|
if (options?.path && originalCwd !== process.cwd()) {
|
|
@@ -186,6 +190,9 @@ export class ConfigParser {
|
|
|
186
190
|
getSite() {
|
|
187
191
|
return this.site;
|
|
188
192
|
}
|
|
193
|
+
getSiteConfigPath() {
|
|
194
|
+
return this.siteConfigPath;
|
|
195
|
+
}
|
|
189
196
|
resolveTargetPath(target) {
|
|
190
197
|
if (!this.site) {
|
|
191
198
|
const configured = this.config?.playwright?.url || this.config?.web?.url;
|
|
@@ -222,6 +229,7 @@ export class ConfigParser {
|
|
|
222
229
|
ConfigParser.instance.runtimeTarget = null;
|
|
223
230
|
ConfigParser.instance.site = null;
|
|
224
231
|
ConfigParser.instance.siteStartPath = '/';
|
|
232
|
+
ConfigParser.instance.siteConfigPath = null;
|
|
225
233
|
}
|
|
226
234
|
}
|
|
227
235
|
// For testing purposes only - sets up minimal default config
|
|
@@ -269,14 +277,20 @@ export class ConfigParser {
|
|
|
269
277
|
// Ignore cleanup errors
|
|
270
278
|
}
|
|
271
279
|
}
|
|
272
|
-
enterGlobalMode(config, target) {
|
|
280
|
+
async enterGlobalMode(config, target) {
|
|
273
281
|
const site = resolveSiteTarget(target || undefined, config.web?.url || config.playwright?.url);
|
|
274
282
|
this.site = registerSite(site.baseUrl);
|
|
275
283
|
this.siteStartPath = site.path;
|
|
276
|
-
|
|
277
|
-
|
|
284
|
+
const { path: sitePath, config: siteConfig } = await loadSiteConfig(this.site.dir, site.baseUrl);
|
|
285
|
+
this.siteConfigPath = sitePath;
|
|
286
|
+
const merged = deepMerge(config, siteConfig);
|
|
287
|
+
await resolveConfigModels(merged.ai);
|
|
288
|
+
resolveLangfuse(merged.ai);
|
|
289
|
+
merged.dirs = { knowledge: 'knowledge', experience: 'experience', output: 'output' };
|
|
290
|
+
merged.playwright = { ...merged.playwright, browser: merged.playwright?.browser || 'chromium', url: site.baseUrl };
|
|
278
291
|
materializeKnowledge(this.site.dir);
|
|
279
292
|
log(`Global mode: ${site.baseUrl} stored in ${this.site.dir}`);
|
|
293
|
+
return merged;
|
|
280
294
|
}
|
|
281
295
|
applyEnvSpec(config) {
|
|
282
296
|
const spec = process.env.EXPLORBOT_SPEC;
|
|
@@ -414,19 +428,7 @@ export class ConfigParser {
|
|
|
414
428
|
output: 'output',
|
|
415
429
|
},
|
|
416
430
|
};
|
|
417
|
-
return
|
|
418
|
-
}
|
|
419
|
-
deepMerge(target, source) {
|
|
420
|
-
const result = { ...target };
|
|
421
|
-
for (const key in source) {
|
|
422
|
-
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) && source[key].constructor === Object) {
|
|
423
|
-
result[key] = this.deepMerge(result[key] || {}, source[key]);
|
|
424
|
-
}
|
|
425
|
-
else {
|
|
426
|
-
result[key] = source[key];
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
return result;
|
|
431
|
+
return deepMerge(defaults, config);
|
|
430
432
|
}
|
|
431
433
|
ensureDirectory(path) {
|
|
432
434
|
if (!existsSync(path)) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare const EXPLORBOT_CONFIG_PATHS: string[];
|
|
1
2
|
export declare function globalDir(): string;
|
|
2
3
|
export declare function globalEnvPath(): string;
|
|
3
4
|
export declare function globalConfigPath(): string;
|
|
@@ -6,6 +7,11 @@ export declare function isGlobalConfigPath(configPath: string): boolean;
|
|
|
6
7
|
export declare function sitesDir(): string;
|
|
7
8
|
export declare function siteFolderName(url: string): string;
|
|
8
9
|
export declare function listSites(): SiteRecord[];
|
|
10
|
+
export declare function findSiteConfig(dir: string): string | null;
|
|
11
|
+
export declare function loadSiteConfig(dir: string, baseUrl: string): Promise<{
|
|
12
|
+
path: string;
|
|
13
|
+
config: any;
|
|
14
|
+
}>;
|
|
9
15
|
export declare function findSiteWith(subpath: string): SiteRecord | undefined;
|
|
10
16
|
export declare function listSitePlanDirs(): string[];
|
|
11
17
|
export declare function registerSite(baseUrl: string): SiteRecord;
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
1
9
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
10
|
import os from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
11
|
+
import { join, resolve } from 'node:path';
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
13
|
+
import dedent from 'dedent';
|
|
4
14
|
const GLOBAL_CONFIG_NAMES = ['config.js', 'config.mjs', 'config.ts'];
|
|
5
15
|
const SITE_DIRS = ['knowledge', 'experience', 'output'];
|
|
16
|
+
export const EXPLORBOT_CONFIG_PATHS = ['explorbot.config.js', 'explorbot.config.mjs', 'explorbot.config.ts'];
|
|
6
17
|
export function globalDir() {
|
|
7
18
|
return join(os.homedir(), '.explorbot');
|
|
8
19
|
}
|
|
@@ -13,12 +24,7 @@ export function globalConfigPath() {
|
|
|
13
24
|
return join(globalDir(), 'config.js');
|
|
14
25
|
}
|
|
15
26
|
export function findGlobalConfig() {
|
|
16
|
-
|
|
17
|
-
const fullPath = join(globalDir(), name);
|
|
18
|
-
if (existsSync(fullPath))
|
|
19
|
-
return fullPath;
|
|
20
|
-
}
|
|
21
|
-
return null;
|
|
27
|
+
return firstExisting(globalDir(), GLOBAL_CONFIG_NAMES);
|
|
22
28
|
}
|
|
23
29
|
export function isGlobalConfigPath(configPath) {
|
|
24
30
|
return GLOBAL_CONFIG_NAMES.some((name) => join(globalDir(), name) === configPath);
|
|
@@ -38,6 +44,47 @@ export function listSites() {
|
|
|
38
44
|
.filter((site) => !!site)
|
|
39
45
|
.sort((a, b) => b.lastRunAt.localeCompare(a.lastRunAt));
|
|
40
46
|
}
|
|
47
|
+
export function findSiteConfig(dir) {
|
|
48
|
+
return firstExisting(dir, EXPLORBOT_CONFIG_PATHS);
|
|
49
|
+
}
|
|
50
|
+
function ensureSiteConfig(dir, baseUrl) {
|
|
51
|
+
const existing = findSiteConfig(dir);
|
|
52
|
+
if (existing)
|
|
53
|
+
return existing;
|
|
54
|
+
const path = join(dir, EXPLORBOT_CONFIG_PATHS[0]);
|
|
55
|
+
writeFileSync(path, siteConfigTemplate(baseUrl), 'utf8');
|
|
56
|
+
return path;
|
|
57
|
+
}
|
|
58
|
+
export async function loadSiteConfig(dir, baseUrl) {
|
|
59
|
+
const path = ensureSiteConfig(dir, baseUrl);
|
|
60
|
+
const module = await import(__rewriteRelativeImportExtension(pathToFileURL(resolve(path)).href));
|
|
61
|
+
const config = module.default || module;
|
|
62
|
+
validateSiteConfig(config, path, baseUrl);
|
|
63
|
+
return { path, config };
|
|
64
|
+
}
|
|
65
|
+
function validateSiteConfig(config, configPath, baseUrl) {
|
|
66
|
+
const url = config?.web?.url;
|
|
67
|
+
if (!url) {
|
|
68
|
+
throw new Error(dedent `
|
|
69
|
+
Site config is missing web.url.
|
|
70
|
+
${configPath}
|
|
71
|
+
|
|
72
|
+
Add it so the config states which site it configures:
|
|
73
|
+
web: { url: '${baseUrl}' },
|
|
74
|
+
`);
|
|
75
|
+
}
|
|
76
|
+
const declared = URL.parse(url)?.origin;
|
|
77
|
+
if (declared === baseUrl)
|
|
78
|
+
return;
|
|
79
|
+
throw new Error(dedent `
|
|
80
|
+
Site config declares a different site.
|
|
81
|
+
${configPath}
|
|
82
|
+
web.url: ${url}
|
|
83
|
+
site: ${baseUrl}
|
|
84
|
+
|
|
85
|
+
Fix web.url, or explore ${url} to register it as its own site.
|
|
86
|
+
`);
|
|
87
|
+
}
|
|
41
88
|
export function findSiteWith(subpath) {
|
|
42
89
|
return listSites().find((site) => existsSync(join(site.dir, subpath)));
|
|
43
90
|
}
|
|
@@ -88,6 +135,34 @@ export function resolveSiteTarget(target, defaultBaseUrl) {
|
|
|
88
135
|
}
|
|
89
136
|
return { baseUrl: site.url, path };
|
|
90
137
|
}
|
|
138
|
+
function firstExisting(dir, names) {
|
|
139
|
+
for (const name of names) {
|
|
140
|
+
const fullPath = join(dir, name);
|
|
141
|
+
if (existsSync(fullPath))
|
|
142
|
+
return fullPath;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
function siteConfigTemplate(baseUrl) {
|
|
147
|
+
return `// Config for ${baseUrl}
|
|
148
|
+
// Extends ~/.explorbot/config.js โ set only what differs.
|
|
149
|
+
const config = {
|
|
150
|
+
web: {
|
|
151
|
+
url: '${baseUrl}',
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// ai: {
|
|
155
|
+
// model: 'openrouter/openai/gpt-oss-120b',
|
|
156
|
+
// },
|
|
157
|
+
|
|
158
|
+
// playwright: {
|
|
159
|
+
// show: true,
|
|
160
|
+
// },
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export default config;
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
91
166
|
function readSite(folder) {
|
|
92
167
|
const dir = join(sitesDir(), folder);
|
|
93
168
|
const metaPath = join(dir, 'site.json');
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { createDebug } from './logger.js';
|
|
2
2
|
const debugLog = createDebug('explorbot:code-extractor');
|
|
3
|
+
const JS_LANGUAGES = new Set(['', 'js', 'javascript']);
|
|
3
4
|
export function extractCodeBlocks(aiResponse) {
|
|
4
|
-
const codeBlockRegex = /```(
|
|
5
|
+
const codeBlockRegex = /```([^\n`]*)\n([\s\S]*?)\n```/g;
|
|
5
6
|
const codeBlocks = [];
|
|
6
7
|
let match = null;
|
|
7
8
|
while ((match = codeBlockRegex.exec(aiResponse))) {
|
|
8
|
-
const
|
|
9
|
+
const language = match[1].trim().toLowerCase();
|
|
10
|
+
if (!JS_LANGUAGES.has(language))
|
|
11
|
+
continue;
|
|
12
|
+
const code = match[2].trim();
|
|
9
13
|
if (!code)
|
|
10
14
|
continue;
|
|
11
15
|
try {
|
package/dist/src/utils/html.js
CHANGED
|
@@ -1283,6 +1283,12 @@ function cleanElement(element) {
|
|
|
1283
1283
|
'aria-labelledby',
|
|
1284
1284
|
'aria-describedby',
|
|
1285
1285
|
'aria-owns',
|
|
1286
|
+
'aria-checked',
|
|
1287
|
+
'aria-expanded',
|
|
1288
|
+
'aria-selected',
|
|
1289
|
+
'aria-pressed',
|
|
1290
|
+
'aria-current',
|
|
1291
|
+
'aria-disabled',
|
|
1286
1292
|
'role',
|
|
1287
1293
|
'title',
|
|
1288
1294
|
'href',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function deepMerge(target: any, source: any): any;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function deepMerge(target, source) {
|
|
2
|
+
const result = { ...target };
|
|
3
|
+
for (const key in source) {
|
|
4
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) && source[key].constructor === Object) {
|
|
5
|
+
result[key] = deepMerge(result[key] || {}, source[key]);
|
|
6
|
+
continue;
|
|
7
|
+
}
|
|
8
|
+
result[key] = source[key];
|
|
9
|
+
}
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
@@ -127,7 +127,7 @@ EXPLORBOT_AI_PROVIDER=openrouter \
|
|
|
127
127
|
|
|
128
128
|
`npx explorbot recommended-models` prints, per provider, the model this version recommends for each role, and the two ways to select it. Both need the provider's API key exported. Set `EXPLORBOT_AI_PROVIDER=<name>` and every role takes that provider's recommendation; leave it out and pin the roles yourself with `EXPLORBOT_AI_MODEL`, `EXPLORBOT_VISION_MODEL` and `EXPLORBOT_AGENTIC_MODEL`, each written as `provider/model-id` โ the command prints those three lines filled in, ready to paste. A role a provider does not serve is named as such, so you know to pair it with another. It closes with the model variables and provider keys currently exported, and a ready-to-run OpenRouter one-liner. It reads nothing but the bundled recommendations, so it answers before any configuration exists and every CLI carries it: `npx explorbot api recommended-models`, `npx explorbot docs recommended-models`, `npx prima recommended-models`. `--json` prints the bundled recommendations as an object.
|
|
129
129
|
|
|
130
|
-
Explorbot resolves its configuration in this order: the path given to `--config`, then `explorbot.config.*` in the working directory, then the `EXPLORBOT_*` variables, and finally `~/.explorbot/config.*` from the global installation. A bare provider name fills every model role from the recommendations in [Providers](../basics/providers.md); a `provider/model-id` spec pins one model and splits on the first slash, so `openrouter/openai/gpt-oss-120b:nitro` selects OpenRouter with model `openai/gpt-oss-120b:nitro`. Supported providers: `openai`, `anthropic`, `google`, `groq`, `mistral`, `openrouter`, `sambanova`.
|
|
130
|
+
Explorbot resolves its configuration in this order: the path given to `--config`, then `explorbot.config.*` in the working directory, then the `EXPLORBOT_*` variables, and finally `~/.explorbot/config.*` from the global installation, which each site then extends with its own [per-site config](configuration.md#per-site-configuration). A bare provider name fills every model role from the recommendations in [Providers](../basics/providers.md); a `provider/model-id` spec pins one model and splits on the first slash, so `openrouter/openai/gpt-oss-120b:nitro` selects OpenRouter with model `openai/gpt-oss-120b:nitro`. Supported providers: `openai`, `anthropic`, `google`, `groq`, `mistral`, `openrouter`, `sambanova`.
|
|
131
131
|
|
|
132
132
|
In this mode output goes to `~/.explorbot/sites/<host>/output/` (or `EXPLORBOT_OUTPUT`, or a temp directory with `EXPLORBOT_EPHEMERAL=1`), experience is kept beside it unless the run is ephemeral, and the Historian is off, so no generated test files appear. See [Agentic Usage](../workflow/agentic-usage.md) for the full picture.
|
|
133
133
|
|
|
@@ -934,12 +934,20 @@ See [Configuration](configuration.md#running-from-anywhere-the-global-installati
|
|
|
934
934
|
|
|
935
935
|
### `npx explorbot sites`
|
|
936
936
|
|
|
937
|
-
List the sites registered in the global installation โ folder name, base URL, and
|
|
937
|
+
List the sites registered in the global installation โ folder name, base URL, last run, and the [per-site config](configuration.md#per-site-configuration) each one uses. Sites register themselves the first time you explore them by URL.
|
|
938
938
|
|
|
939
939
|
```bash
|
|
940
940
|
npx explorbot sites
|
|
941
941
|
```
|
|
942
942
|
|
|
943
|
+
```
|
|
944
|
+
Registered sites (2):
|
|
945
|
+
app.example.com https://app.example.com last run 2026-09-16 09:57
|
|
946
|
+
/home/you/.explorbot/sites/app.example.com/explorbot.config.js
|
|
947
|
+
other.example.com https://other.example.com last run 2026-09-01 10:00
|
|
948
|
+
inherits global config
|
|
949
|
+
```
|
|
950
|
+
|
|
943
951
|
### `npx explorbot clean [target]`
|
|
944
952
|
|
|
945
953
|
Clean generated files. Targets: `states`, `research`, `plans`, `tests`, `experiences`, `output`.
|
|
@@ -413,7 +413,7 @@ Explorbot looks for a config file in this order:
|
|
|
413
413
|
7. `src/config/explorbot.config.js`
|
|
414
414
|
8. `src/config/explorbot.config.mjs`
|
|
415
415
|
9. `src/config/explorbot.config.ts`
|
|
416
|
-
10. `~/.explorbot/config.js` (or `.mjs`, `.ts`) โ the global installation
|
|
416
|
+
10. `~/.explorbot/config.js` (or `.mjs`, `.ts`) โ the global installation, extended per site by `~/.explorbot/sites/<host>/explorbot.config.js`
|
|
417
417
|
|
|
418
418
|
Or pass a custom path:
|
|
419
419
|
|
|
@@ -431,14 +431,15 @@ Env files fill in rather than override: the `.env` of the working directory is r
|
|
|
431
431
|
|
|
432
432
|
```
|
|
433
433
|
~/.explorbot/
|
|
434
|
-
โโโ config.js
|
|
434
|
+
โโโ config.js # AI models and keys, shared by every site
|
|
435
435
|
โโโ .env
|
|
436
436
|
โโโ sites/
|
|
437
437
|
โโโ app.example.com/
|
|
438
|
-
โ โโโ
|
|
438
|
+
โ โโโ explorbot.config.js # this site's settings, extends config.js
|
|
439
|
+
โ โโโ site.json # base URL, first and last run
|
|
439
440
|
โ โโโ knowledge/
|
|
440
441
|
โ โโโ experience/
|
|
441
|
-
โ โโโ output/
|
|
442
|
+
โ โโโ output/ # states, plans, reports, tests
|
|
442
443
|
โโโ localhost_3000/
|
|
443
444
|
```
|
|
444
445
|
|
|
@@ -456,6 +457,38 @@ npx explorbot sites # list registered sites
|
|
|
456
457
|
|
|
457
458
|
A `dirs` section in the global config is ignored in favor of the layout above. A `web.url` is allowed and acts as the default site for commands that pass no URL of their own.
|
|
458
459
|
|
|
460
|
+
#### Per-site configuration
|
|
461
|
+
|
|
462
|
+
`~/.explorbot/config.js` holds what every site shares โ models, keys, reporter settings. Anything one site needs differently goes in its own `explorbot.config.js`, written into the site folder the first time that site is explored:
|
|
463
|
+
|
|
464
|
+
```javascript
|
|
465
|
+
// Config for https://app.example.com
|
|
466
|
+
// Extends ~/.explorbot/config.js โ set only what differs.
|
|
467
|
+
const config = {
|
|
468
|
+
web: {
|
|
469
|
+
url: 'https://app.example.com',
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
ai: {
|
|
473
|
+
model: 'openrouter/anthropic/claude-sonnet-5',
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
playwright: {
|
|
477
|
+
show: true,
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
export default config;
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
The two are merged section by section, and the site wins. A site that overrides `ai.model` keeps the global `ai.visionModel`. Use it to give a slow or unusual app a stronger model, a visible browser, or its own reporter settings, without changing how every other site runs.
|
|
485
|
+
|
|
486
|
+
`web.url` is required, and must be the site the folder belongs to โ it is what makes the file readable on its own rather than meaningful only by where it sits. Explorbot refuses to run when it is missing or names a different site, instead of quietly ignoring the mismatch. To configure a different site, explore it and edit the config in its own folder.
|
|
487
|
+
|
|
488
|
+
`dirs` and the base URL stay owned by the layout above and cannot be overridden. The file is never rewritten once created, and a site without one simply uses the global config.
|
|
489
|
+
|
|
490
|
+
Per-site configs apply to the global installation only. A directory with its own `explorbot.config.js` and the `EXPLORBOT_*` environment mode below both resolve to a single config with nothing to extend.
|
|
491
|
+
|
|
459
492
|
### Running without a config file
|
|
460
493
|
|
|
461
494
|
When the working directory has no config file and `EXPLORBOT_AI_PROVIDER` (or `EXPLORBOT_AI_MODEL`) is set, Explorbot synthesizes a configuration from `EXPLORBOT_*` environment variables, in preference to a global installation. Output goes to the site folder `~/.explorbot/sites/<host>/` (`EXPLORBOT_OUTPUT` overrides it, `EXPLORBOT_EPHEMERAL=1` sends it to a temp directory instead), experience is written there and reused by later runs against the same host unless the run is ephemeral, and the Historian is off. This is meant for one-liner CI jobs, demos, and coding agents โ see [Agentic Usage](../workflow/agentic-usage.md) for the variable list and the trade-offs.
|