explorbot 0.4.8 → 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/cli.ts +1 -1
- package/boat/api-tester/src/config.ts +34 -26
- 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/src/ai/fisherman/tools.js +10 -4
- package/dist/src/ai/pilot.js +11 -6
- package/dist/src/ai/tools.js +8 -1
- 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/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/src/ai/fisherman/tools.ts +10 -4
- package/src/ai/pilot.ts +11 -6
- package/src/ai/tools.ts +7 -1
- 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/html.ts +6 -0
- package/src/utils/merge.ts +13 -0
package/src/config.ts
CHANGED
|
@@ -5,8 +5,9 @@ import { pathToFileURL } from 'node:url';
|
|
|
5
5
|
import { parseEnv } from 'node:util';
|
|
6
6
|
import dedent from 'dedent';
|
|
7
7
|
import matter from 'gray-matter';
|
|
8
|
-
import { type SiteRecord, findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from './global-config.js';
|
|
8
|
+
import { EXPLORBOT_CONFIG_PATHS, type SiteRecord, findGlobalConfig, globalEnvPath, isGlobalConfigPath, loadSiteConfig, registerSite, resolveSiteTarget } from './global-config.js';
|
|
9
9
|
import { getCliName } from './utils/cli-name.js';
|
|
10
|
+
import { deepMerge } from './utils/merge.js';
|
|
10
11
|
import { log, tag } from './utils/logger.js';
|
|
11
12
|
|
|
12
13
|
export const PROVIDERS: Record<string, ProviderInfo> = {
|
|
@@ -268,7 +269,7 @@ const config: ExplorbotConfig = {
|
|
|
268
269
|
|
|
269
270
|
type RuleEntry = string | Record<string, string>;
|
|
270
271
|
|
|
271
|
-
export
|
|
272
|
+
export { EXPLORBOT_CONFIG_PATHS };
|
|
272
273
|
|
|
273
274
|
export const EXPLORBOT_ENV_VARS: EnvVar[] = [
|
|
274
275
|
{ name: 'EXPLORBOT_AI_PROVIDER', required: true, description: 'Provider name; fills every model role from its recommended models. Turns on config-free mode' },
|
|
@@ -323,6 +324,7 @@ export class ConfigParser {
|
|
|
323
324
|
private runtimeTarget: string | null = null;
|
|
324
325
|
private site: SiteRecord | null = null;
|
|
325
326
|
private siteStartPath = '/';
|
|
327
|
+
private siteConfigPath: string | null = null;
|
|
326
328
|
|
|
327
329
|
private constructor() {}
|
|
328
330
|
|
|
@@ -407,16 +409,18 @@ export class ConfigParser {
|
|
|
407
409
|
log(`Configuration built from EXPLORBOT_* environment variables. Output: ${outputRoot}`);
|
|
408
410
|
}
|
|
409
411
|
|
|
410
|
-
|
|
411
|
-
await resolveConfigModels(
|
|
412
|
-
this.runtimeTarget = target;
|
|
413
|
-
this.configPath = sourcePath;
|
|
412
|
+
let config = this.resolveConfig(loadedConfig as ExplorbotConfig, options);
|
|
413
|
+
await resolveConfigModels(config.ai);
|
|
414
414
|
this.site = null;
|
|
415
|
+
this.siteConfigPath = null;
|
|
415
416
|
|
|
416
417
|
if (resolvedPath && isGlobalConfigPath(resolvedPath)) {
|
|
417
|
-
this.enterGlobalMode(
|
|
418
|
+
config = await this.enterGlobalMode(config, target);
|
|
418
419
|
}
|
|
419
420
|
|
|
421
|
+
this.config = config;
|
|
422
|
+
this.runtimeTarget = target;
|
|
423
|
+
this.configPath = sourcePath;
|
|
420
424
|
this.applyEnvSpec(this.config);
|
|
421
425
|
|
|
422
426
|
// Restore original directory after successful config load
|
|
@@ -473,6 +477,10 @@ export class ConfigParser {
|
|
|
473
477
|
return this.site;
|
|
474
478
|
}
|
|
475
479
|
|
|
480
|
+
public getSiteConfigPath(): string | null {
|
|
481
|
+
return this.siteConfigPath;
|
|
482
|
+
}
|
|
483
|
+
|
|
476
484
|
public resolveTargetPath(target?: string): string {
|
|
477
485
|
if (!this.site) {
|
|
478
486
|
const configured = this.config?.playwright?.url || this.config?.web?.url;
|
|
@@ -512,6 +520,7 @@ export class ConfigParser {
|
|
|
512
520
|
ConfigParser.instance.runtimeTarget = null;
|
|
513
521
|
ConfigParser.instance.site = null;
|
|
514
522
|
ConfigParser.instance.siteStartPath = '/';
|
|
523
|
+
ConfigParser.instance.siteConfigPath = null;
|
|
515
524
|
}
|
|
516
525
|
}
|
|
517
526
|
|
|
@@ -563,16 +572,24 @@ export class ConfigParser {
|
|
|
563
572
|
}
|
|
564
573
|
}
|
|
565
574
|
|
|
566
|
-
private enterGlobalMode(config: ExplorbotConfig, target: string | null):
|
|
575
|
+
private async enterGlobalMode(config: ExplorbotConfig, target: string | null): Promise<ExplorbotConfig> {
|
|
567
576
|
const site = resolveSiteTarget(target || undefined, config.web?.url || config.playwright?.url);
|
|
568
577
|
this.site = registerSite(site.baseUrl);
|
|
569
578
|
this.siteStartPath = site.path;
|
|
570
579
|
|
|
571
|
-
|
|
572
|
-
|
|
580
|
+
const { path: sitePath, config: siteConfig } = await loadSiteConfig(this.site.dir, site.baseUrl);
|
|
581
|
+
this.siteConfigPath = sitePath;
|
|
582
|
+
|
|
583
|
+
const merged = deepMerge(config, siteConfig);
|
|
584
|
+
await resolveConfigModels(merged.ai);
|
|
585
|
+
resolveLangfuse(merged.ai);
|
|
586
|
+
|
|
587
|
+
merged.dirs = { knowledge: 'knowledge', experience: 'experience', output: 'output' };
|
|
588
|
+
merged.playwright = { ...merged.playwright, browser: merged.playwright?.browser || 'chromium', url: site.baseUrl };
|
|
573
589
|
materializeKnowledge(this.site.dir);
|
|
574
590
|
|
|
575
591
|
log(`Global mode: ${site.baseUrl} stored in ${this.site.dir}`);
|
|
592
|
+
return merged;
|
|
576
593
|
}
|
|
577
594
|
|
|
578
595
|
private applyEnvSpec(config: ExplorbotConfig): void {
|
|
@@ -720,21 +737,7 @@ export class ConfigParser {
|
|
|
720
737
|
},
|
|
721
738
|
};
|
|
722
739
|
|
|
723
|
-
return
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
private deepMerge(target: any, source: any): any {
|
|
727
|
-
const result = { ...target };
|
|
728
|
-
|
|
729
|
-
for (const key in source) {
|
|
730
|
-
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) && source[key].constructor === Object) {
|
|
731
|
-
result[key] = this.deepMerge(result[key] || {}, source[key]);
|
|
732
|
-
} else {
|
|
733
|
-
result[key] = source[key];
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
return result;
|
|
740
|
+
return deepMerge(defaults, config);
|
|
738
741
|
}
|
|
739
742
|
|
|
740
743
|
public ensureDirectory(path: string): void {
|
package/src/global-config.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import dedent from 'dedent';
|
|
4
6
|
|
|
5
7
|
const GLOBAL_CONFIG_NAMES = ['config.js', 'config.mjs', 'config.ts'];
|
|
6
8
|
const SITE_DIRS = ['knowledge', 'experience', 'output'];
|
|
7
9
|
|
|
10
|
+
export const EXPLORBOT_CONFIG_PATHS = ['explorbot.config.js', 'explorbot.config.mjs', 'explorbot.config.ts'];
|
|
11
|
+
|
|
8
12
|
export function globalDir(): string {
|
|
9
13
|
return join(os.homedir(), '.explorbot');
|
|
10
14
|
}
|
|
@@ -18,11 +22,7 @@ export function globalConfigPath(): string {
|
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
export function findGlobalConfig(): string | null {
|
|
21
|
-
|
|
22
|
-
const fullPath = join(globalDir(), name);
|
|
23
|
-
if (existsSync(fullPath)) return fullPath;
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
25
|
+
return firstExisting(globalDir(), GLOBAL_CONFIG_NAMES);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export function isGlobalConfigPath(configPath: string): boolean {
|
|
@@ -47,6 +47,52 @@ export function listSites(): SiteRecord[] {
|
|
|
47
47
|
.sort((a, b) => b.lastRunAt.localeCompare(a.lastRunAt));
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export function findSiteConfig(dir: string): string | null {
|
|
51
|
+
return firstExisting(dir, EXPLORBOT_CONFIG_PATHS);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ensureSiteConfig(dir: string, baseUrl: string): string {
|
|
55
|
+
const existing = findSiteConfig(dir);
|
|
56
|
+
if (existing) return existing;
|
|
57
|
+
|
|
58
|
+
const path = join(dir, EXPLORBOT_CONFIG_PATHS[0]);
|
|
59
|
+
writeFileSync(path, siteConfigTemplate(baseUrl), 'utf8');
|
|
60
|
+
return path;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function loadSiteConfig(dir: string, baseUrl: string): Promise<{ path: string; config: any }> {
|
|
64
|
+
const path = ensureSiteConfig(dir, baseUrl);
|
|
65
|
+
const module = await import(pathToFileURL(resolve(path)).href);
|
|
66
|
+
const config = module.default || module;
|
|
67
|
+
validateSiteConfig(config, path, baseUrl);
|
|
68
|
+
return { path, config };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function validateSiteConfig(config: any, configPath: string, baseUrl: string): void {
|
|
72
|
+
const url = config?.web?.url;
|
|
73
|
+
if (!url) {
|
|
74
|
+
throw new Error(dedent`
|
|
75
|
+
Site config is missing web.url.
|
|
76
|
+
${configPath}
|
|
77
|
+
|
|
78
|
+
Add it so the config states which site it configures:
|
|
79
|
+
web: { url: '${baseUrl}' },
|
|
80
|
+
`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const declared = URL.parse(url)?.origin;
|
|
84
|
+
if (declared === baseUrl) return;
|
|
85
|
+
|
|
86
|
+
throw new Error(dedent`
|
|
87
|
+
Site config declares a different site.
|
|
88
|
+
${configPath}
|
|
89
|
+
web.url: ${url}
|
|
90
|
+
site: ${baseUrl}
|
|
91
|
+
|
|
92
|
+
Fix web.url, or explore ${url} to register it as its own site.
|
|
93
|
+
`);
|
|
94
|
+
}
|
|
95
|
+
|
|
50
96
|
export function findSiteWith(subpath: string): SiteRecord | undefined {
|
|
51
97
|
return listSites().find((site) => existsSync(join(site.dir, subpath)));
|
|
52
98
|
}
|
|
@@ -107,6 +153,35 @@ export function resolveSiteTarget(target?: string, defaultBaseUrl?: string): Sit
|
|
|
107
153
|
return { baseUrl: site.url, path };
|
|
108
154
|
}
|
|
109
155
|
|
|
156
|
+
function firstExisting(dir: string, names: string[]): string | null {
|
|
157
|
+
for (const name of names) {
|
|
158
|
+
const fullPath = join(dir, name);
|
|
159
|
+
if (existsSync(fullPath)) return fullPath;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function siteConfigTemplate(baseUrl: string): string {
|
|
165
|
+
return `// Config for ${baseUrl}
|
|
166
|
+
// Extends ~/.explorbot/config.js — set only what differs.
|
|
167
|
+
const config = {
|
|
168
|
+
web: {
|
|
169
|
+
url: '${baseUrl}',
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// ai: {
|
|
173
|
+
// model: 'openrouter/openai/gpt-oss-120b',
|
|
174
|
+
// },
|
|
175
|
+
|
|
176
|
+
// playwright: {
|
|
177
|
+
// show: true,
|
|
178
|
+
// },
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export default config;
|
|
182
|
+
`;
|
|
183
|
+
}
|
|
184
|
+
|
|
110
185
|
function readSite(folder: string): SiteRecord | null {
|
|
111
186
|
const dir = join(sitesDir(), folder);
|
|
112
187
|
const metaPath = join(dir, 'site.json');
|
package/src/utils/html.ts
CHANGED
|
@@ -1420,6 +1420,12 @@ function cleanElement(element: parse5TreeAdapter.Element): void {
|
|
|
1420
1420
|
'aria-labelledby',
|
|
1421
1421
|
'aria-describedby',
|
|
1422
1422
|
'aria-owns',
|
|
1423
|
+
'aria-checked',
|
|
1424
|
+
'aria-expanded',
|
|
1425
|
+
'aria-selected',
|
|
1426
|
+
'aria-pressed',
|
|
1427
|
+
'aria-current',
|
|
1428
|
+
'aria-disabled',
|
|
1423
1429
|
'role',
|
|
1424
1430
|
'title',
|
|
1425
1431
|
'href',
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function deepMerge(target: any, source: any): any {
|
|
2
|
+
const result = { ...target };
|
|
3
|
+
|
|
4
|
+
for (const key in source) {
|
|
5
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) && source[key].constructor === Object) {
|
|
6
|
+
result[key] = deepMerge(result[key] || {}, source[key]);
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
result[key] = source[key];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return result;
|
|
13
|
+
}
|