@taurgis/bonsai 1.0.4 → 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 (93) hide show
  1. package/README.md +81 -2
  2. package/agents/agents/official-docs-researcher.agent.md +10 -1
  3. package/agents/agents/salesforce-docs-researcher.agent.md +126 -0
  4. package/agents/hooks/README.md +62 -0
  5. package/agents/hooks/claude-code/hooks/bonsai-web-fetch.sh +22 -0
  6. package/agents/hooks/claude-code/settings.json +16 -0
  7. package/agents/hooks/codex/hooks/bonsai-user-prompt-context.sh +18 -0
  8. package/agents/hooks/codex/hooks/bonsai-web-fetch.sh +22 -0
  9. package/agents/hooks/codex/hooks.json +28 -0
  10. package/agents/hooks/cursor/hooks/bonsai-web-fetch.sh +21 -0
  11. package/agents/hooks/cursor/hooks.json +12 -0
  12. package/agents/hooks/github-copilot/bonsai-web-fetch.json +23 -0
  13. package/agents/hooks/github-copilot/bonsai-web-fetch.sh +19 -0
  14. package/agents/instructions/salesforce-research.instructions.md +59 -0
  15. package/agents/instructions/web-research.instructions.md +5 -2
  16. package/agents/skills/web-research/SKILL.md +8 -3
  17. package/bin/cli.mjs +35 -8
  18. package/dist/base-command.d.ts +23 -0
  19. package/dist/base-command.js +67 -8
  20. package/dist/commands/config/base.d.ts +2 -0
  21. package/dist/commands/config/base.js +15 -1
  22. package/dist/commands/config/get.d.ts +1 -1
  23. package/dist/commands/config/get.js +6 -7
  24. package/dist/commands/config/index.d.ts +12 -0
  25. package/dist/commands/config/index.js +34 -0
  26. package/dist/commands/config/list.js +4 -4
  27. package/dist/commands/config/set.js +20 -14
  28. package/dist/commands/config/unset.d.ts +1 -1
  29. package/dist/commands/config/unset.js +11 -8
  30. package/dist/commands/fetch.d.ts +5 -0
  31. package/dist/commands/fetch.js +122 -47
  32. package/dist/commands/import.d.ts +4 -0
  33. package/dist/commands/import.js +99 -37
  34. package/dist/commands/inspect.d.ts +4 -0
  35. package/dist/commands/inspect.js +24 -19
  36. package/dist/commands/list.d.ts +2 -2
  37. package/dist/commands/list.js +39 -16
  38. package/dist/commands/prune.d.ts +1 -1
  39. package/dist/commands/prune.js +50 -17
  40. package/dist/commands/search.d.ts +4 -0
  41. package/dist/commands/search.js +102 -41
  42. package/dist/commands/status.d.ts +6 -0
  43. package/dist/commands/status.js +58 -31
  44. package/dist/commands.d.ts +2 -0
  45. package/dist/commands.js +2 -0
  46. package/dist/hooks/command-not-found/suggest.d.ts +9 -0
  47. package/dist/hooks/command-not-found/suggest.js +122 -0
  48. package/dist/lib/argv.d.ts +10 -0
  49. package/dist/lib/argv.js +53 -0
  50. package/dist/lib/config/index.d.ts +1 -1
  51. package/dist/lib/config/index.js +1 -1
  52. package/dist/lib/config/resolve.d.ts +7 -0
  53. package/dist/lib/config/resolve.js +23 -0
  54. package/dist/lib/envelope.d.ts +36 -0
  55. package/dist/lib/envelope.js +76 -0
  56. package/dist/lib/json-meta.d.ts +10 -0
  57. package/dist/lib/json-meta.js +98 -0
  58. package/dist/lib/research/browser.d.ts +12 -0
  59. package/dist/lib/research/browser.js +40 -3
  60. package/dist/lib/research/docs/capabilities.d.ts +1 -1
  61. package/dist/lib/research/docs/discovery-index.d.ts +14 -0
  62. package/dist/lib/research/docs/discovery-index.js +112 -0
  63. package/dist/lib/research/docs/machine-readable.d.ts +2 -2
  64. package/dist/lib/research/docs/machine-readable.js +3 -7
  65. package/dist/lib/research/docs/remote-search-runner.js +61 -1
  66. package/dist/lib/research/docs/sections.d.ts +1 -0
  67. package/dist/lib/research/docs/sections.js +19 -1
  68. package/dist/lib/research/freshness.d.ts +9 -1
  69. package/dist/lib/research/freshness.js +24 -2
  70. package/dist/lib/research/keywords.d.ts +10 -3
  71. package/dist/lib/research/keywords.js +32 -5
  72. package/dist/lib/research/revalidate.js +3 -1
  73. package/dist/lib/research/schema.d.ts +5 -2
  74. package/dist/lib/research/schema.js +12 -1
  75. package/dist/lib/research/secret-scan.js +4 -2
  76. package/dist/lib/research/url.js +5 -2
  77. package/dist/lib/text.d.ts +35 -0
  78. package/dist/lib/text.js +65 -0
  79. package/dist/sites/algolia.d.ts +9 -0
  80. package/dist/sites/algolia.js +26 -0
  81. package/dist/sites/auto.d.ts +8 -0
  82. package/dist/sites/auto.js +30 -0
  83. package/dist/sites/index.js +17 -1
  84. package/dist/sites/nextjs/index.d.ts +2 -0
  85. package/dist/sites/nextjs/index.js +52 -0
  86. package/dist/sites/react/index.d.ts +2 -0
  87. package/dist/sites/react/index.js +17 -0
  88. package/dist/sites/tailwind/index.d.ts +2 -0
  89. package/dist/sites/tailwind/index.js +17 -0
  90. package/dist/sites/vue/index.d.ts +2 -0
  91. package/dist/sites/vue/index.js +17 -0
  92. package/oclif.manifest.json +154 -81
  93. package/package.json +24 -4
package/bin/cli.mjs CHANGED
@@ -3,20 +3,47 @@
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
 
17
+ // Normalize argv so the whole oclif pipeline sees one consistent command. oclif's
18
+ // error handler re-reads process.argv (not the args passed to execute) when it renders
19
+ // help on a parse error, so rewriting only the execute() args leaves the help renderer
20
+ // trying to resolve the bare URL as a command — which crashes with a stack trace. Rewrite
21
+ // process.argv itself so both the run path and the help/error path agree.
9
22
  const rawArgv = process.argv.slice(2);
10
- const [firstArg, ...restArgv] = rawArgv;
11
- const args =
12
- firstArg === 'help'
13
- ? [...restArgv, '--help']
14
- : firstArg?.startsWith('http://') || firstArg?.startsWith('https://')
15
- ? ['fetch', ...rawArgv]
16
- : rawArgv;
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];
17
45
 
18
46
  await execute({
19
- args,
20
47
  development: false,
21
48
  dir: __dirname + '/../',
22
49
  });
@@ -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`
@@ -17,8 +18,30 @@ export declare abstract class BaseCommand<T extends typeof Command> extends Comm
17
18
  * JSON envelope, so machine output stays clean.
18
19
  */
19
20
  warn(input: string | Error): string | Error;
21
+ /**
22
+ * The exit code an error maps to. oclif `CLIError`s carry it in `err.oclif.exit`; other errors may
23
+ * expose `err.exitCode`. Defined once so the process exit code (`catch`) and the JSON envelope
24
+ * (`toErrorJson`) can never disagree — a mismatch between these two was itself the bug this fixes.
25
+ */
26
+ private static exitCodeOf;
27
+ /**
28
+ * Align the process exit code with the code reported in the JSON envelope. oclif's default
29
+ * `catch` sets `process.exitCode = err.exitCode ?? 1`, but oclif `CLIError`s carry their code in
30
+ * `err.oclif.exit` (never `err.exitCode`), so a usage error (`this.error(msg, { exit: 2 })`) would
31
+ * exit the process with 1 under `--json` while `toErrorJson` correctly reports `exitCode: 2`. That
32
+ * contradiction breaks the deterministic-exit-code contract agents rely on. Pre-seed the code from
33
+ * the shared resolver so the framework's `??` keeps it, then defer to the default behavior.
34
+ */
35
+ catch(err: Error & {
36
+ oclif?: {
37
+ exit?: number;
38
+ };
39
+ exitCode?: number;
40
+ }): Promise<any>;
20
41
  /** Command id for the JSON envelope; falls back to the binary name when a command has no id. */
21
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;
22
45
  /** Single source of truth for the `--json` envelope shape, shared by success and error output. */
23
46
  private envelope;
24
47
  /** Wrap a command's return value in the machine-readable envelope emitted under `--json`. */
@@ -1,4 +1,7 @@
1
- import { Command, Errors } from '@oclif/core';
1
+ import { Command, Errors, toConfiguredId } from '@oclif/core';
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';
2
5
  /**
3
6
  * Shared base for every Bonsai command. Enables oclif's native `--json` flag,
4
7
  * parses args/flags once in `init()` so commands read `this.args`/`this.flags`
@@ -19,6 +22,11 @@ export class BaseCommand extends Command {
19
22
  });
20
23
  this.flags = flags;
21
24
  this.args = args;
25
+ // Surface a set-but-invalid BONSAI_* override once per run. Resolution silently drops such a
26
+ // value, so without this a typo'd env var would take no effect with no signal. Warnings go to
27
+ // stderr (even under --json), so machine output stays clean.
28
+ for (const warning of invalidEnvOverrideWarnings(process.env))
29
+ this.warn(warning);
22
30
  }
23
31
  /**
24
32
  * Always surface warnings on stderr, even under `--json`. oclif silences `warn()` in JSON mode,
@@ -30,21 +38,58 @@ export class BaseCommand extends Command {
30
38
  Errors.warn(input);
31
39
  return input;
32
40
  }
41
+ /**
42
+ * The exit code an error maps to. oclif `CLIError`s carry it in `err.oclif.exit`; other errors may
43
+ * expose `err.exitCode`. Defined once so the process exit code (`catch`) and the JSON envelope
44
+ * (`toErrorJson`) can never disagree — a mismatch between these two was itself the bug this fixes.
45
+ */
46
+ static exitCodeOf(err) {
47
+ return err?.oclif?.exit ?? err?.exitCode ?? 1;
48
+ }
49
+ /**
50
+ * Align the process exit code with the code reported in the JSON envelope. oclif's default
51
+ * `catch` sets `process.exitCode = err.exitCode ?? 1`, but oclif `CLIError`s carry their code in
52
+ * `err.oclif.exit` (never `err.exitCode`), so a usage error (`this.error(msg, { exit: 2 })`) would
53
+ * exit the process with 1 under `--json` while `toErrorJson` correctly reports `exitCode: 2`. That
54
+ * contradiction breaks the deterministic-exit-code contract agents rely on. Pre-seed the code from
55
+ * the shared resolver so the framework's `??` keeps it, then defer to the default behavior.
56
+ */
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;
62
+ process.exitCode = process.exitCode ?? BaseCommand.exitCodeOf(err);
63
+ return super.catch(err);
64
+ }
33
65
  /** Command id for the JSON envelope; falls back to the binary name when a command has no id. */
34
66
  envelopeCommandId() {
35
- 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
+ }
36
83
  }
37
84
  /** Single source of truth for the `--json` envelope shape, shared by success and error output. */
38
85
  envelope(parts) {
39
- return {
40
- schemaVersion: 1,
86
+ return buildEnvelope({
41
87
  command: this.envelopeCommandId(),
42
88
  ok: parts.ok,
43
89
  exitCode: parts.exitCode,
44
- stdout: '',
45
90
  stderr: parts.stderr,
46
91
  data: parts.data,
47
- };
92
+ });
48
93
  }
49
94
  /** Wrap a command's return value in the machine-readable envelope emitted under `--json`. */
50
95
  toSuccessJson(data) {
@@ -58,7 +103,21 @@ export class BaseCommand extends Command {
58
103
  /** Mirror the success envelope for failures so JSON consumers get one consistent shape. */
59
104
  toErrorJson(err) {
60
105
  const e = err;
61
- const exitCode = e?.oclif?.exit ?? e?.exitCode ?? 1;
62
- return this.envelope({ ok: false, exitCode, stderr: e?.message ?? String(err), data: null });
106
+ const exitCode = BaseCommand.exitCodeOf(e);
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
+ });
63
122
  }
64
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>;
@@ -1,22 +1,22 @@
1
1
  import { Args } from '@oclif/core';
2
2
  import { ConfigCommand, configScopeFlags } from './base.js';
3
- import { KEY_META, BUILT_IN_DEFAULTS, readScopedConfig } from '../../lib/config/index.js';
3
+ import { KEY_META, BUILT_IN_DEFAULTS, readScopedConfig, validKeysHint, } from '../../lib/config/index.js';
4
4
  export default class ConfigGet extends ConfigCommand {
5
5
  static id = 'config get';
6
6
  static summary = 'Get the effective value of a research configuration key.';
7
- static description = 'Print a config value. Without --global/--local, shows the merged effective value (flag > env > project > user > default).\n\nValid keys: storage.';
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
+ }
@@ -1,16 +1,16 @@
1
1
  import { ConfigCommand, configScopeFlags } from './base.js';
2
- import { ALL_KEYS, KEY_META, BUILT_IN_DEFAULTS, readScopedConfig } from '../../lib/config/index.js';
2
+ import { ALL_KEYS, KEY_META, BUILT_IN_DEFAULTS, readScopedConfig, validKeysHint, } from '../../lib/config/index.js';
3
3
  export default class ConfigList extends ConfigCommand {
4
4
  static id = 'config list';
5
5
  static summary = 'List all research configuration keys and their effective values.';
6
- static description = 'Show every configuration key with its current value. Use --global/--local to show a single scope.\n\nValid keys: storage.';
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
  ];
@@ -1,24 +1,24 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import { ConfigCommand, configScopeFlags } from './base.js';
3
- import { KEY_META, writeUserConfig, writeProjectConfig } from '../../lib/config/index.js';
3
+ import { KEY_META, writeUserConfig, writeProjectConfig, validKeysHint, } from '../../lib/config/index.js';
4
4
  export default class ConfigSet extends ConfigCommand {
5
5
  static id = 'config set';
6
6
  static summary = 'Set a research configuration key.';
7
7
  static description = 'Persist a configuration value. Writes user-level config by default; pass --local to write the project-level config (.bonsai.json in cwd).\n\n' +
8
- 'Valid keys: storage. The inline form `<key>=<value>` is also accepted.';
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>;
@@ -1,22 +1,22 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import { ConfigCommand, configScopeFlags } from './base.js';
3
- import { writeUserConfig, writeProjectConfig } from '../../lib/config/index.js';
3
+ import { writeUserConfig, writeProjectConfig, validKeysHint } from '../../lib/config/index.js';
4
4
  export default class ConfigUnset extends ConfigCommand {
5
5
  static id = 'config unset';
6
6
  static summary = 'Remove a research configuration key.';
7
- static description = 'Delete a key from user-level config (default) or project-level config (--local), restoring the built-in default.\n\nValid keys: storage.';
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);
@@ -30,8 +30,13 @@ export default class FetchCommand extends BaseCommand<typeof FetchCommand> {
30
30
  private executeCacheMiss;
31
31
  private buildResultData;
32
32
  private validateDurationFlags;
33
+ private emitFetchError;
33
34
  private resolveArtifact;
34
35
  private persistFreshArtifact;
35
36
  private persistSectionsIfFresh;
36
37
  run(): Promise<unknown>;
37
38
  }
39
+ export declare function fetchFailureGuidance(message: string, url: string): {
40
+ suggestions: string[];
41
+ ref?: string;
42
+ } | undefined;