@taurgis/bonsai 1.0.5 → 1.0.6

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.
Files changed (61) hide show
  1. package/README.md +53 -0
  2. package/agents/agents/official-docs-researcher.agent.md +10 -1
  3. package/agents/agents/salesforce-docs-researcher.agent.md +10 -1
  4. package/agents/instructions/salesforce-research.instructions.md +4 -2
  5. package/agents/instructions/web-research.instructions.md +4 -2
  6. package/agents/skills/web-research/SKILL.md +8 -3
  7. package/bin/cli.mjs +30 -13
  8. package/dist/base-command.d.ts +3 -0
  9. package/dist/base-command.js +40 -7
  10. package/dist/commands/config/base.d.ts +2 -0
  11. package/dist/commands/config/base.js +15 -1
  12. package/dist/commands/config/get.d.ts +1 -1
  13. package/dist/commands/config/get.js +4 -5
  14. package/dist/commands/config/index.d.ts +12 -0
  15. package/dist/commands/config/index.js +34 -0
  16. package/dist/commands/config/list.js +2 -2
  17. package/dist/commands/config/set.js +18 -12
  18. package/dist/commands/config/unset.d.ts +1 -1
  19. package/dist/commands/config/unset.js +9 -6
  20. package/dist/commands/fetch.js +44 -34
  21. package/dist/commands/import.d.ts +3 -0
  22. package/dist/commands/import.js +82 -52
  23. package/dist/commands/inspect.d.ts +4 -0
  24. package/dist/commands/inspect.js +24 -19
  25. package/dist/commands/list.d.ts +2 -2
  26. package/dist/commands/list.js +23 -15
  27. package/dist/commands/prune.d.ts +1 -1
  28. package/dist/commands/prune.js +30 -13
  29. package/dist/commands/search.d.ts +4 -0
  30. package/dist/commands/search.js +83 -21
  31. package/dist/commands/status.d.ts +6 -0
  32. package/dist/commands/status.js +50 -24
  33. package/dist/commands.d.ts +2 -0
  34. package/dist/commands.js +2 -0
  35. package/dist/hooks/command-not-found/suggest.js +37 -8
  36. package/dist/lib/argv.d.ts +10 -0
  37. package/dist/lib/argv.js +53 -0
  38. package/dist/lib/envelope.d.ts +36 -0
  39. package/dist/lib/envelope.js +76 -0
  40. package/dist/lib/json-meta.d.ts +10 -0
  41. package/dist/lib/json-meta.js +98 -0
  42. package/dist/lib/research/revalidate.js +3 -1
  43. package/dist/lib/research/schema.d.ts +5 -2
  44. package/dist/lib/research/schema.js +12 -1
  45. package/dist/lib/text.d.ts +2 -0
  46. package/dist/lib/text.js +2 -0
  47. package/dist/sites/algolia.d.ts +9 -0
  48. package/dist/sites/algolia.js +26 -0
  49. package/dist/sites/auto.d.ts +8 -0
  50. package/dist/sites/auto.js +30 -0
  51. package/dist/sites/index.js +17 -1
  52. package/dist/sites/nextjs/index.d.ts +2 -0
  53. package/dist/sites/nextjs/index.js +52 -0
  54. package/dist/sites/react/index.d.ts +2 -0
  55. package/dist/sites/react/index.js +17 -0
  56. package/dist/sites/tailwind/index.d.ts +2 -0
  57. package/dist/sites/tailwind/index.js +17 -0
  58. package/dist/sites/vue/index.d.ts +2 -0
  59. package/dist/sites/vue/index.js +17 -0
  60. package/oclif.manifest.json +149 -76
  61. package/package.json +8 -2
package/README.md CHANGED
@@ -180,6 +180,35 @@ Both keys can also be overridden per-invocation via the `BONSAI_STORAGE` and `BO
180
180
 
181
181
  ---
182
182
 
183
+ ## Shell Autocomplete
184
+
185
+ Bonsai ships with shell autocomplete via `@oclif/plugin-autocomplete`. After installing globally, set it up once for your shell:
186
+
187
+ ```bash
188
+ # Bash — add to ~/.bashrc
189
+ eval "$(bonsai autocomplete bash)"
190
+
191
+ # Zsh — add to ~/.zshrc
192
+ eval "$(bonsai autocomplete zsh)"
193
+
194
+ # Fish
195
+ bonsai autocomplete fish > ~/.config/fish/completions/bonsai.fish
196
+ ```
197
+
198
+ To see full setup instructions including the copy-paste snippet for your shell:
199
+
200
+ ```bash
201
+ bonsai autocomplete
202
+ ```
203
+
204
+ Refresh cached completions after upgrading:
205
+
206
+ ```bash
207
+ bonsai autocomplete --refresh-cache
208
+ ```
209
+
210
+ ---
211
+
183
212
  ## Freshness and Cache Rules
184
213
 
185
214
  Global cache files live in Bonsai's oclif data directory, typically:
@@ -246,6 +275,30 @@ When run with `--json`, commands return a stable envelope:
246
275
 
247
276
  ---
248
277
 
278
+ ## Environment Variables
279
+
280
+ | Variable | Effect |
281
+ | --- | --- |
282
+ | `BONSAI_STORAGE` | Override the default storage location per-invocation (`global` or `project`). |
283
+ | `BONSAI_SUMMARY` | Override the summary compression level per-invocation (`conservative`, `balanced`, or `aggressive`). |
284
+ | `NO_COLOR` | Disable all ANSI color output (also respected via `TERM=dumb`). |
285
+ | `NO_UPDATE_NOTIFIER` | Suppress the "update available" notification. |
286
+ | `CI` | Set to `1` or `true` to suppress the update notification in CI environments (detected automatically). |
287
+ | `DEBUG` | Enable debug output for specific Bonsai namespaces, e.g. `DEBUG=bonsai:*`. |
288
+
289
+ ---
290
+
291
+ ## Exit Codes
292
+
293
+ | Code | Meaning |
294
+ | --- | --- |
295
+ | `0` | Success. |
296
+ | `1` | Runtime failure (network error, fetch failed, file read error). |
297
+ | `2` | Usage error (invalid flag, bad URL, missing required argument). |
298
+ | `5` | Stale content served — the cache entry is past its grace window but `--allow-stale` is set. The returned content is still usable. |
299
+
300
+ ---
301
+
249
302
  ## Security and Limits
250
303
 
251
304
  * Fetched HTML is treated as untrusted input.
@@ -5,7 +5,7 @@ model: 'Auto'
5
5
  tools: ['vscode/askQuestions', 'execute', 'read', 'search', 'web', 'vscode/memory']
6
6
  argument-hint: 'What topic should I research in official docs?'
7
7
  metadata:
8
- version: '3.0.0'
8
+ version: '3.0.1'
9
9
  ---
10
10
 
11
11
  # Official Docs Researcher Agent
@@ -24,7 +24,16 @@ Do not research from memory alone. Training-data knowledge does not satisfy this
24
24
  2. Search the Bonsai cache first:
25
25
 
26
26
  ```bash
27
+ # 1. Default search: Checks the local cache across EVERYTHING you've researched,
28
+ # including domains that do not support online search APIs.
27
29
  bonsai search "<topic or keywords>"
30
+
31
+ # 2. Remote API search: Use this to quickly find URLs for documentation pages online
32
+ # ONLY if you don't find any information in the local cache.
33
+ bonsai search "<topic or keywords>" --domain <domain>
34
+ # Supported domains: help.salesforce.com, react.dev, vuejs.org, tailwindcss.com, nextjs.org,
35
+ # jestjs.io, cypress.io, vitest.dev, vitepress.dev, angular.dev, redux.js.org,
36
+ # vitejs.dev, fastify.dev, rollupjs.org, vueuse.org
28
37
  ```
29
38
 
30
39
  3. If the cache misses or does not cover the question, locate official source URLs. Prefer vendor docs, standards bodies, API references, and official release notes.
@@ -5,7 +5,7 @@ model: 'Auto'
5
5
  tools: ['vscode/askQuestions', 'execute', 'read', 'search', 'web', 'vscode/memory']
6
6
  argument-hint: 'What Salesforce topic should I research in official docs?'
7
7
  metadata:
8
- version: '1.0.0'
8
+ version: '1.0.1'
9
9
  ---
10
10
 
11
11
  # Salesforce Docs Researcher Agent
@@ -24,7 +24,16 @@ Do not research from memory alone. Training-data knowledge does not satisfy this
24
24
  2. Search the Bonsai cache first:
25
25
 
26
26
  ```bash
27
+ # 1. Default search: Checks the local cache across EVERYTHING you've researched,
28
+ # including domains that do not support online search APIs.
27
29
  bonsai search "<topic or keywords>"
30
+
31
+ # 2. Remote API search: Use this to quickly find URLs for documentation pages online
32
+ # ONLY if you don't find any information in the local cache.
33
+ bonsai search "<topic or keywords>" --domain <domain>
34
+ # Supported domains: help.salesforce.com, react.dev, vuejs.org, tailwindcss.com, nextjs.org,
35
+ # jestjs.io, cypress.io, vitest.dev, vitepress.dev, angular.dev, redux.js.org,
36
+ # vitejs.dev, fastify.dev, rollupjs.org, vueuse.org
28
37
  ```
29
38
 
30
39
  3. If the cache misses or does not cover the question, locate official source URLs. Prefer vendor docs, standards bodies, API references, and official release notes.
@@ -4,7 +4,7 @@ applyTo: '**'
4
4
  skills:
5
5
  - web-research
6
6
  metadata:
7
- version: '1.0.1'
7
+ version: '1.0.2'
8
8
  ---
9
9
 
10
10
  # Web Research Requirement (Salesforce)
@@ -24,9 +24,11 @@ metadata:
24
24
 
25
25
  ## Shared Cache
26
26
 
27
- - Both paths use the same freshness-tiered Bonsai cache (data directory or project-local `.bonsai/research/`): it reuses fresh notes, cheaply revalidates stale ones, and re-fetches only on a miss.
27
+ - **Local Cache Search (Default)**: Always search the local cache first using `bonsai search "<query>"`. This checks everything you've researched across all domains, even ones that don't support online search APIs.
28
+ - **Online URL Discovery (Fallback)**: If the local cache comes up empty, use `--domain <domain>` or `--remote <docs-url>` to quickly hit a site's search API and find the official URLs you need to fetch. This is specifically for online URL discovery when local cache fails.
28
29
  - If Bonsai is configured for project storage and `.bonsai/research/` is not ignored by git, treat those cache artifacts as intentional shared project files. It is OK to check them in, and agents must not delete them as incidental generated output without an explicit request.
29
30
  - Re-running on a recent topic is cheap — research the topic rather than skipping it to "save" a fetch.
31
+ - Supported remote API domains: `help.salesforce.com`, `react.dev`, `vuejs.org`, `tailwindcss.com`, `nextjs.org`, `jestjs.io`, `cypress.io`, `vitest.dev`, `vitepress.dev`, `angular.dev`, `redux.js.org`, `vitejs.dev`, `fastify.dev`, `rollupjs.org`, `vueuse.org`.
30
32
 
31
33
  ## Working with Salesforce Sites
32
34
 
@@ -4,7 +4,7 @@ applyTo: '**'
4
4
  skills:
5
5
  - web-research
6
6
  metadata:
7
- version: '3.1.1'
7
+ version: '3.1.2'
8
8
  ---
9
9
 
10
10
  # Web Research Requirement
@@ -24,9 +24,11 @@ metadata:
24
24
 
25
25
  ## Shared Cache
26
26
 
27
- - Both paths use the same freshness-tiered Bonsai cache (data directory or project-local `.bonsai/research/`): it reuses fresh notes, cheaply revalidates stale ones, and re-fetches only on a miss.
27
+ - **Local Cache Search (Default)**: Always search the local cache first using `bonsai search "<query>"`. This checks everything you've researched across all domains, even ones that don't support online search APIs.
28
+ - **Online URL Discovery (Fallback)**: If the local cache comes up empty, use `--domain <domain>` or `--remote <docs-url>` to quickly hit a site's search API and find the official URLs you need to fetch. This is specifically for online URL discovery when local cache fails.
28
29
  - If Bonsai is configured for project storage and `.bonsai/research/` is not ignored by git, treat those cache artifacts as intentional shared project files. It is OK to check them in, and agents must not delete them as incidental generated output without an explicit request.
29
30
  - Re-running on a recent topic is cheap — research the topic rather than skipping it to "save" a fetch.
31
+ - Supported remote API domains: `help.salesforce.com`, `react.dev`, `vuejs.org`, `tailwindcss.com`, `nextjs.org`, `jestjs.io`, `cypress.io`, `vitest.dev`, `vitepress.dev`, `angular.dev`, `redux.js.org`, `vitejs.dev`, `fastify.dev`, `rollupjs.org`, `vueuse.org`.
30
32
 
31
33
  ## When Not to Use
32
34
 
@@ -4,7 +4,7 @@ description: 'Bonsai-backed official documentation and web research workflow. Us
4
4
  license: Forward Proprietary
5
5
  compatibility: VS Code 1.x+, GitHub Copilot
6
6
  metadata:
7
- version: '2.1.0'
7
+ version: '2.1.1'
8
8
  ---
9
9
 
10
10
  # Web Research Skill
@@ -15,10 +15,15 @@ Use Bonsai as the cache-first research path for official documentation and web c
15
15
 
16
16
  Before creating, updating, refactoring, scaffolding, or deleting technical content, verify relevant current official documentation in the same task.
17
17
 
18
- Search the cache first:
19
-
20
18
  ```bash
19
+ # 1. Default search: Checks the local cache across EVERYTHING you've researched,
20
+ # including domains that do not support online search APIs.
21
21
  bonsai search "<topic or keywords>"
22
+
23
+ # 2. Remote API search: Use this to quickly find URLs for documentation pages online
24
+ # ONLY if you don't find any information in the local cache.
25
+ bonsai search "<topic or keywords>" --domain <domain>
26
+ # Supported domains: help.salesforce.com, react.dev, vuejs.org, tailwindcss.com, nextjs.org, jestjs.io, cypress.io, vitest.dev, vitepress.dev, angular.dev, redux.js.org, vitejs.dev, fastify.dev, rollupjs.org, vueuse.org
22
27
  ```
23
28
 
24
29
  If the cache misses or does not cover the question, fetch the source through Bonsai:
package/bin/cli.mjs CHANGED
@@ -3,6 +3,14 @@
3
3
  import { execute } from '@oclif/core';
4
4
  import { dirname } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
+ import updateNotifier from 'update-notifier';
7
+ import { createRequire } from 'node:module';
8
+
9
+ import { normalizeArgv } from '../dist/lib/argv.js';
10
+ import { tryJsonMetaOutput } from '../dist/lib/json-meta.js';
11
+
12
+ const req = createRequire(import.meta.url);
13
+ const pkg = req('../package.json');
6
14
 
7
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
16
 
@@ -12,19 +20,28 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
12
20
  // trying to resolve the bare URL as a command — which crashes with a stack trace. Rewrite
13
21
  // process.argv itself so both the run path and the help/error path agree.
14
22
  const rawArgv = process.argv.slice(2);
15
- const [firstArg, ...restArgv] = rawArgv;
16
- // Treat any first arg that looks like a URL (carries a scheme) as the `fetch` shorthand.
17
- // Routing wrong-scheme URLs (ftp://, file://) here too means fetch can answer with a clear
18
- // "only http/https" error instead of oclif's cryptic "command not found".
19
- const looksLikeUrl = firstArg?.includes('://') ?? false;
20
- const normalizedArgv =
21
- firstArg === 'help'
22
- ? [...restArgv, '--help']
23
- : looksLikeUrl
24
- ? ['fetch', ...rawArgv]
25
- : rawArgv;
26
-
27
- process.argv = [process.argv[0], process.argv[1], ...normalizedArgv];
23
+ const result = normalizeArgv(rawArgv);
24
+
25
+ if (result.exitWithJson) {
26
+ process.exitCode = result.exitWithJson.exitCode;
27
+ console.log(JSON.stringify(result.exitWithJson.envelope, null, 2));
28
+ process.exit();
29
+ }
30
+
31
+ const jsonMeta = await tryJsonMetaOutput(result.argv, __dirname + '/../');
32
+ if (jsonMeta) {
33
+ process.exitCode = jsonMeta.exitCode;
34
+ console.log(JSON.stringify(jsonMeta.envelope, null, 2));
35
+ process.exit();
36
+ }
37
+
38
+ // Only notify after the exitWithJson fast-path so the notifier never fires before
39
+ // a JSON envelope exits. Also skip under --json so agent callers never see stderr noise.
40
+ if (!process.argv.includes('--json')) {
41
+ updateNotifier({ pkg }).notify();
42
+ }
43
+
44
+ process.argv = [process.argv[0], process.argv[1], ...result.argv];
28
45
 
29
46
  await execute({
30
47
  development: false,
@@ -1,4 +1,5 @@
1
1
  import { Command, Interfaces } from '@oclif/core';
2
+ import { type ResolveResearchTargetOptions, type ResolvedResearchTarget } from './lib/research/resolve-target.js';
2
3
  /**
3
4
  * Shared base for every Bonsai command. Enables oclif's native `--json` flag,
4
5
  * parses args/flags once in `init()` so commands read `this.args`/`this.flags`
@@ -39,6 +40,8 @@ export declare abstract class BaseCommand<T extends typeof Command> extends Comm
39
40
  }): Promise<any>;
40
41
  /** Command id for the JSON envelope; falls back to the binary name when a command has no id. */
41
42
  protected envelopeCommandId(): string;
43
+ /** Resolve a URL against the research cache, exiting with INVALID_URL on normalization failure. */
44
+ protected resolveResearchTargetOrFail(url: string, extra?: Pick<ResolveResearchTargetOptions, 'flagOverride' | 'lookup'>): ResolvedResearchTarget;
42
45
  /** Single source of truth for the `--json` envelope shape, shared by success and error output. */
43
46
  private envelope;
44
47
  /** Wrap a command's return value in the machine-readable envelope emitted under `--json`. */
@@ -1,5 +1,7 @@
1
- import { Command, Errors } from '@oclif/core';
1
+ import { Command, Errors, toConfiguredId } from '@oclif/core';
2
2
  import { invalidEnvOverrideWarnings } from './lib/config/index.js';
3
+ import { buildEnvelope, formatErrorForJson, normalizeCliErrorMessage, stableErrorCodeFrom, } from './lib/envelope.js';
4
+ import { resolveResearchTarget, } from './lib/research/resolve-target.js';
3
5
  /**
4
6
  * Shared base for every Bonsai command. Enables oclif's native `--json` flag,
5
7
  * parses args/flags once in `init()` so commands read `this.args`/`this.flags`
@@ -53,24 +55,41 @@ export class BaseCommand extends Command {
53
55
  * the shared resolver so the framework's `??` keeps it, then defer to the default behavior.
54
56
  */
55
57
  async catch(err) {
58
+ // Parse failures throw before `this.parse()` sets `parsed`, which makes oclif emit a spurious
59
+ // [UnparsedCommand] warning to stderr even under `--json`. CLIParseError subclasses carry `parse`.
60
+ if (err && typeof err === 'object' && 'parse' in err)
61
+ this.parsed = true;
56
62
  process.exitCode = process.exitCode ?? BaseCommand.exitCodeOf(err);
57
63
  return super.catch(err);
58
64
  }
59
65
  /** Command id for the JSON envelope; falls back to the binary name when a command has no id. */
60
66
  envelopeCommandId() {
61
- return this.ctor.id || this.config.bin;
67
+ return this.ctor.id ? toConfiguredId(this.ctor.id, this.config) : this.config.bin;
68
+ }
69
+ /** Resolve a URL against the research cache, exiting with INVALID_URL on normalization failure. */
70
+ resolveResearchTargetOrFail(url, extra) {
71
+ try {
72
+ return resolveResearchTarget({
73
+ configDir: this.config.configDir,
74
+ cwd: process.cwd(),
75
+ dataDir: this.config.dataDir,
76
+ url,
77
+ ...extra,
78
+ });
79
+ }
80
+ catch (err) {
81
+ this.error(`Invalid URL: ${err.message}`, { exit: 2, code: 'INVALID_URL' });
82
+ }
62
83
  }
63
84
  /** Single source of truth for the `--json` envelope shape, shared by success and error output. */
64
85
  envelope(parts) {
65
- return {
66
- schemaVersion: 1,
86
+ return buildEnvelope({
67
87
  command: this.envelopeCommandId(),
68
88
  ok: parts.ok,
69
89
  exitCode: parts.exitCode,
70
- stdout: '',
71
90
  stderr: parts.stderr,
72
91
  data: parts.data,
73
- };
92
+ });
74
93
  }
75
94
  /** Wrap a command's return value in the machine-readable envelope emitted under `--json`. */
76
95
  toSuccessJson(data) {
@@ -85,6 +104,20 @@ export class BaseCommand extends Command {
85
104
  toErrorJson(err) {
86
105
  const e = err;
87
106
  const exitCode = BaseCommand.exitCodeOf(e);
88
- return this.envelope({ ok: false, exitCode, stderr: e?.message ?? String(err), data: null });
107
+ const code = stableErrorCodeFrom(e);
108
+ const message = typeof e?.message === 'string' ? normalizeCliErrorMessage(e.message) : undefined;
109
+ const stderr = message || code || e?.suggestions?.length || e?.ref
110
+ ? formatErrorForJson({ ...e, message, code })
111
+ : String(err);
112
+ return buildEnvelope({
113
+ command: this.envelopeCommandId(),
114
+ ok: false,
115
+ exitCode,
116
+ stderr,
117
+ data: null,
118
+ code,
119
+ suggestions: e.suggestions?.length ? e.suggestions : undefined,
120
+ ref: e.ref,
121
+ });
89
122
  }
90
123
  }
@@ -12,6 +12,8 @@ export declare abstract class ConfigCommand<T extends typeof Command> extends Ba
12
12
  /** Read scope for get/list: an explicit `--global`/`--local`, else the merged effective view. */
13
13
  protected readScope(global?: boolean, local?: boolean): ConfigScope;
14
14
  protected assertKnownKey(keyArg: string): asserts keyArg is ConfigKey;
15
+ protected requireConfigKey(key: string | undefined): asserts key is ConfigKey;
16
+ protected validateConfigKeyAndScope(key: string | undefined, global?: boolean, local?: boolean): asserts key is ConfigKey;
15
17
  }
16
18
  /** Build the `--global`/`--local` scope-flag pair with per-command descriptions. */
17
19
  export declare function configScopeFlags(descriptions: {
@@ -8,7 +8,10 @@ import { isKnownKey, suggestKey, validKeysHint } from '../../lib/config/index.js
8
8
  export class ConfigCommand extends BaseCommand {
9
9
  assertScopeFlagsExclusive(global, local) {
10
10
  if (global && local) {
11
- this.error('--global and --local are mutually exclusive.', { exit: 2 });
11
+ this.error('--global and --local are mutually exclusive.', {
12
+ exit: 2,
13
+ code: 'CONFLICTING_FLAGS',
14
+ });
12
15
  }
13
16
  }
14
17
  /** `--local` writes/reads the project file; otherwise the user-level file. */
@@ -29,9 +32,20 @@ export class ConfigCommand extends BaseCommand {
29
32
  const hint = suggestion ? ` Did you mean: ${suggestion}?` : '';
30
33
  this.error(`Unknown config key: "${keyArg}".${hint} Valid keys: ${validKeysHint()}.`, {
31
34
  exit: 2,
35
+ code: 'UNKNOWN_KEY',
32
36
  });
33
37
  }
34
38
  }
39
+ requireConfigKey(key) {
40
+ if (!key) {
41
+ this.error('Missing required argument: key', { exit: 2, code: 'MISSING_ARGUMENT' });
42
+ }
43
+ this.assertKnownKey(key);
44
+ }
45
+ validateConfigKeyAndScope(key, global, local) {
46
+ this.requireConfigKey(key);
47
+ this.assertScopeFlagsExclusive(global, local);
48
+ }
35
49
  }
36
50
  /** Build the `--global`/`--local` scope-flag pair with per-command descriptions. */
37
51
  export function configScopeFlags(descriptions) {
@@ -8,7 +8,7 @@ export default class ConfigGet extends ConfigCommand<typeof ConfigGet> {
8
8
  command: string;
9
9
  }[];
10
10
  static args: {
11
- key: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
11
+ key: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
12
12
  };
13
13
  static flags: {
14
14
  global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -7,16 +7,16 @@ export default class ConfigGet extends ConfigCommand {
7
7
  static description = `Print a config value. Without --global/--local, shows the merged effective value (flag > env > project > user > default).\n\nValid keys: ${validKeysHint()}.`;
8
8
  static examples = [
9
9
  {
10
- description: 'Get the effective storage mode',
10
+ description: 'get the effective storage mode',
11
11
  command: '<%= config.bin %> config get storage',
12
12
  },
13
13
  {
14
- description: 'Read only the project-level value',
14
+ description: 'read only the project-level value',
15
15
  command: '<%= config.bin %> config get storage --local',
16
16
  },
17
17
  ];
18
18
  static args = {
19
- key: Args.string({ required: true, description: 'The configuration key to read.' }),
19
+ key: Args.string({ required: false, description: 'the configuration key to read' }),
20
20
  };
21
21
  static flags = {
22
22
  ...configScopeFlags({
@@ -27,8 +27,7 @@ export default class ConfigGet extends ConfigCommand {
27
27
  static stdoutIsPrimaryData = true;
28
28
  async run() {
29
29
  const key = this.args.key;
30
- this.assertKnownKey(key);
31
- this.assertScopeFlagsExclusive(this.flags.global, this.flags.local);
30
+ this.validateConfigKeyAndScope(key, this.flags.global, this.flags.local);
32
31
  const scope = this.readScope(this.flags.global, this.flags.local);
33
32
  const value = readScopedConfig(scope, this.config.configDir, process.cwd())[key];
34
33
  const meta = KEY_META[key];
@@ -0,0 +1,12 @@
1
+ import { BaseCommand } from '../../base-command.js';
2
+ export default class ConfigIndex extends BaseCommand<typeof ConfigIndex> {
3
+ static id: string;
4
+ static summary: string;
5
+ static description: string;
6
+ static examples: {
7
+ description: string;
8
+ command: string;
9
+ }[];
10
+ static stdoutIsPrimaryData: boolean;
11
+ run(): Promise<unknown>;
12
+ }
@@ -0,0 +1,34 @@
1
+ import { Help, toConfiguredId } from '@oclif/core';
2
+ import { BaseCommand } from '../../base-command.js';
3
+ export default class ConfigIndex extends BaseCommand {
4
+ static id = 'config';
5
+ static summary = 'Manage research cache storage configuration (global vs project).';
6
+ static description = 'Show config subcommands for reading and writing Bonsai research cache configuration.';
7
+ static examples = [
8
+ {
9
+ description: 'get a configuration value',
10
+ command: '<%= config.bin %> <%= command.id %> get storage',
11
+ },
12
+ {
13
+ description: 'set a configuration value for the current project',
14
+ command: '<%= config.bin %> <%= command.id %> set storage project --local',
15
+ },
16
+ ];
17
+ static stdoutIsPrimaryData = true;
18
+ async run() {
19
+ if (!this.jsonEnabled()) {
20
+ await new Help(this.config, this.config.pjson.oclif.helpOptions ?? {}).showHelp(['config']);
21
+ }
22
+ const commands = this.config.commands
23
+ .filter((c) => c.id.startsWith('config:') && !c.hidden)
24
+ .map((c) => ({
25
+ id: toConfiguredId(c.id, this.config),
26
+ summary: c.summary || c.description || '',
27
+ }))
28
+ .sort((a, b) => a.id.localeCompare(b.id));
29
+ return {
30
+ topic: 'config',
31
+ commands,
32
+ };
33
+ }
34
+ }
@@ -6,11 +6,11 @@ export default class ConfigList extends ConfigCommand {
6
6
  static description = `Show every configuration key with its current value. Use --global/--local to show a single scope.\n\nValid keys: ${validKeysHint()}.`;
7
7
  static examples = [
8
8
  {
9
- description: 'Show all effective config values',
9
+ description: 'show all effective config values',
10
10
  command: '<%= config.bin %> config list',
11
11
  },
12
12
  {
13
- description: 'Show only project-level config',
13
+ description: 'show only project-level config',
14
14
  command: '<%= config.bin %> config list --local',
15
15
  },
16
16
  ];
@@ -8,17 +8,17 @@ export default class ConfigSet extends ConfigCommand {
8
8
  `Valid keys: ${validKeysHint()}. The inline form \`<key>=<value>\` is also accepted.`;
9
9
  static examples = [
10
10
  {
11
- description: 'Store research cache inside the current project',
11
+ description: 'store research cache inside the current project',
12
12
  command: '<%= config.bin %> config set storage project --local',
13
13
  },
14
14
  {
15
- description: 'Set the user-level default using the inline key=value form',
15
+ description: 'set the user-level default using the inline key=value form',
16
16
  command: '<%= config.bin %> config set storage=global',
17
17
  },
18
18
  ];
19
19
  static args = {
20
- key: Args.string({ required: false, description: 'The configuration key to set.' }),
21
- value: Args.string({ required: false, description: 'The value to assign.' }),
20
+ key: Args.string({ required: false, description: 'the configuration key to set' }),
21
+ value: Args.string({ required: false, description: 'the value to assign' }),
22
22
  };
23
23
  static flags = {
24
24
  ...configScopeFlags({
@@ -26,26 +26,28 @@ export default class ConfigSet extends ConfigCommand {
26
26
  local: 'Write to project-level config (.bonsai.json in cwd).',
27
27
  }),
28
28
  'dry-run': Flags.boolean({
29
- description: 'Show what would be written without saving.',
29
+ description: 'show what would be written without saving',
30
30
  default: false,
31
31
  }),
32
32
  };
33
33
  static stdoutIsPrimaryData = true;
34
34
  async run() {
35
35
  const { keyArg, valueArg } = splitInlineKeyValue(this.args.key, this.args.value);
36
- if (!keyArg) {
37
- this.error('Missing required argument: key', { exit: 2 });
38
- }
39
- this.assertKnownKey(keyArg);
40
- this.assertScopeFlagsExclusive(this.flags.global, this.flags.local);
36
+ this.validateConfigKeyAndScope(keyArg, this.flags.global, this.flags.local);
41
37
  const meta = KEY_META[keyArg];
42
38
  if (valueArg === undefined) {
43
- this.error(`Missing required argument: value for key "${keyArg}"`, { exit: 2 });
39
+ this.error(`Missing required argument: value for key "${keyArg}"`, {
40
+ exit: 2,
41
+ code: 'MISSING_ARGUMENT',
42
+ });
44
43
  }
45
44
  const parsed = meta.parseValue(valueArg);
46
45
  if (!meta.isValid(parsed)) {
47
46
  const guidance = meta.values ? `Valid values: ${meta.values.join(', ')}.` : meta.description;
48
- this.error(`Invalid value "${valueArg}" for "${keyArg}". ${guidance}`, { exit: 2 });
47
+ this.error(`Invalid value "${valueArg}" for "${keyArg}". ${guidance}`, {
48
+ exit: 2,
49
+ code: 'INVALID_VALUE',
50
+ });
49
51
  }
50
52
  const scope = this.writeScope(this.flags.local);
51
53
  const formatted = meta.format(parsed);
@@ -63,6 +65,10 @@ export default class ConfigSet extends ConfigCommand {
63
65
  if (!configDir) {
64
66
  this.error('Could not determine user config directory. Use --local to write project config.', {
65
67
  exit: 1,
68
+ code: 'CONFIG_DIR_UNAVAILABLE',
69
+ suggestions: [
70
+ `Write project config instead: ${this.config.bin} config set <key> <value> --local`,
71
+ ],
66
72
  });
67
73
  }
68
74
  writeUserConfig(configDir, patch);
@@ -8,7 +8,7 @@ export default class ConfigUnset extends ConfigCommand<typeof ConfigUnset> {
8
8
  command: string;
9
9
  }[];
10
10
  static args: {
11
- key: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
11
+ key: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
12
12
  };
13
13
  static flags: {
14
14
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -7,16 +7,16 @@ export default class ConfigUnset extends ConfigCommand {
7
7
  static description = `Delete a key from user-level config (default) or project-level config (--local), restoring the built-in default.\n\nValid keys: ${validKeysHint()}.`;
8
8
  static examples = [
9
9
  {
10
- description: 'Remove the user-level storage setting',
10
+ description: 'remove the user-level storage setting',
11
11
  command: '<%= config.bin %> config unset storage',
12
12
  },
13
13
  {
14
- description: 'Remove the project-level storage setting',
14
+ description: 'remove the project-level storage setting',
15
15
  command: '<%= config.bin %> config unset storage --local',
16
16
  },
17
17
  ];
18
18
  static args = {
19
- key: Args.string({ required: true, description: 'The configuration key to remove.' }),
19
+ key: Args.string({ required: false, description: 'the configuration key to remove' }),
20
20
  };
21
21
  static flags = {
22
22
  ...configScopeFlags({
@@ -24,15 +24,14 @@ export default class ConfigUnset extends ConfigCommand {
24
24
  local: 'Remove from project-level config (.bonsai.json in cwd).',
25
25
  }),
26
26
  'dry-run': Flags.boolean({
27
- description: 'Show what would be removed without saving.',
27
+ description: 'show what would be removed without saving',
28
28
  default: false,
29
29
  }),
30
30
  };
31
31
  static stdoutIsPrimaryData = true;
32
32
  async run() {
33
33
  const key = this.args.key;
34
- this.assertKnownKey(key);
35
- this.assertScopeFlagsExclusive(this.flags.global, this.flags.local);
34
+ this.validateConfigKeyAndScope(key, this.flags.global, this.flags.local);
36
35
  const scope = this.writeScope(this.flags.local);
37
36
  if (this.flags['dry-run']) {
38
37
  if (!this.jsonEnabled())
@@ -48,6 +47,10 @@ export default class ConfigUnset extends ConfigCommand {
48
47
  if (!configDir) {
49
48
  this.error('Could not determine user config directory. Use --local to remove project config.', {
50
49
  exit: 1,
50
+ code: 'CONFIG_DIR_UNAVAILABLE',
51
+ suggestions: [
52
+ `Remove project config instead: ${this.config.bin} config unset ${key} --local`,
53
+ ],
51
54
  });
52
55
  }
53
56
  writeUserConfig(configDir, patch);