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.
@@ -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
- for (const name of GLOBAL_CONFIG_NAMES) {
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');
@@ -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 last run. Sites register themselves the first time you explore them by URL.
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 # AI models and keys, no URL
434
+ ├── config.js # AI models and keys, shared by every site
435
435
  ├── .env
436
436
  └── sites/
437
437
  ├── app.example.com/
438
- │ ├── site.json # base URL, first and last run
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/ # states, plans, reports, tests
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.