@taurgis/bonsai 3.2.2 → 4.0.0

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 (57) hide show
  1. package/README.md +2 -2
  2. package/agents/agents/salesforce-docs-researcher.agent.md +4 -4
  3. package/bin/cli.mjs +7 -4
  4. package/dist/base-command.d.ts +37 -0
  5. package/dist/base-command.js +97 -6
  6. package/dist/commands/config/base.js +1 -0
  7. package/dist/commands/config/get.js +2 -2
  8. package/dist/commands/config/list.js +2 -2
  9. package/dist/commands/config/set.js +2 -2
  10. package/dist/commands/config/unset.js +2 -2
  11. package/dist/commands/fetch.js +4 -0
  12. package/dist/commands/inspect.js +11 -2
  13. package/dist/commands/list.d.ts +12 -9
  14. package/dist/commands/list.js +67 -39
  15. package/dist/commands/prune.d.ts +2 -0
  16. package/dist/commands/prune.js +35 -4
  17. package/dist/commands/status.js +10 -0
  18. package/dist/hooks/command-not-found/suggest.d.ts +1 -1
  19. package/dist/hooks/command-not-found/suggest.js +12 -2
  20. package/dist/lib/argv.d.ts +5 -0
  21. package/dist/lib/argv.js +27 -5
  22. package/dist/lib/cli-flag-manifest.d.ts +7 -0
  23. package/dist/lib/cli-flag-manifest.js +7 -0
  24. package/dist/lib/cli-presentation.d.ts +6 -4
  25. package/dist/lib/cli-presentation.js +7 -4
  26. package/dist/lib/cli-result-types.d.ts +25 -1
  27. package/dist/lib/config/index.d.ts +2 -2
  28. package/dist/lib/config/index.js +2 -2
  29. package/dist/lib/config/io.d.ts +9 -0
  30. package/dist/lib/config/io.js +71 -21
  31. package/dist/lib/config/schema.d.ts +2 -0
  32. package/dist/lib/config/schema.js +7 -0
  33. package/dist/lib/envelope.js +11 -4
  34. package/dist/lib/prune-flags.d.ts +2 -0
  35. package/dist/lib/prune-flags.js +18 -6
  36. package/dist/lib/research/artifact-index.js +7 -1
  37. package/dist/lib/research/artifact.js +20 -2
  38. package/dist/lib/research/browser.d.ts +12 -0
  39. package/dist/lib/research/browser.js +63 -8
  40. package/dist/lib/research/cli-io.d.ts +7 -0
  41. package/dist/lib/research/fetch-command-service.js +80 -23
  42. package/dist/lib/research/fetcher.js +3 -1
  43. package/dist/lib/research/import-command-service.js +22 -1
  44. package/dist/lib/research/metadata-filters.d.ts +18 -0
  45. package/dist/lib/research/metadata-filters.js +37 -0
  46. package/dist/lib/research/metadata-flags.d.ts +34 -0
  47. package/dist/lib/research/metadata-flags.js +45 -0
  48. package/dist/lib/research/prompt-injection.d.ts +2 -1
  49. package/dist/lib/research/prompt-injection.js +18 -2
  50. package/dist/lib/research/resolve-target.d.ts +2 -0
  51. package/dist/lib/research/resolve-target.js +1 -1
  52. package/dist/lib/research/storage.d.ts +4 -7
  53. package/dist/lib/research/storage.js +26 -25
  54. package/dist/lib/text.d.ts +14 -8
  55. package/dist/lib/text.js +21 -9
  56. package/oclif.manifest.json +120 -12
  57. package/package.json +2 -1
package/README.md CHANGED
@@ -141,7 +141,7 @@ npx @taurgis/bonsai inspect <url>
141
141
 
142
142
  ### `list`
143
143
 
144
- List cached artifacts by metadata filters (`--topic`, `--tags`, `--freshness`, `--artifact-type`, `--capture-method`, `--limit`), newest first, without printing content.
144
+ List cached artifacts by metadata filters (`--topic`, `--tags`, `--url`, `--freshness`, `--artifact-type`, `--capture-method`, `--limit`), newest first, without printing content. The default `--json`/`--toon` row is a minimal 4-field shape (`sourceUrls`, `topic`, `freshness`, `tokenEstimate`); pass `--full` for every metadata field. Running `bonsai` with no arguments at all is shorthand for `bonsai list`.
145
145
 
146
146
  ```bash
147
147
  npx @taurgis/bonsai list [flags]
@@ -149,7 +149,7 @@ npx @taurgis/bonsai list [flags]
149
149
 
150
150
  ### `prune`
151
151
 
152
- Delete old or inactive cache entries. Requires a filter (`--older-than`, `--inactive`, `--artifact-type`, or `--url`) and exactly one of `--dry-run` or `--yes` (passing both is a usage error). In [read-only mode](#read-only--plan-mode), `--yes` is rejected and the safety check is implicitly satisfied by a preview.
152
+ Delete old or inactive cache entries. Requires a filter (`--older-than`, `--inactive`, `--artifact-type`, `--url`, `--topic`, or `--tags`) and exactly one of `--dry-run` or `--yes` (passing both is a usage error). In [read-only mode](#read-only--plan-mode), `--yes` is rejected and the safety check is implicitly satisfied by a preview.
153
153
 
154
154
  ```bash
155
155
  npx @taurgis/bonsai prune --older-than 90d --dry-run
@@ -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: '2.1.0'
8
+ version: '2.2.0'
9
9
  ---
10
10
 
11
11
  # Salesforce Docs Researcher Agent
@@ -20,7 +20,7 @@ Do not research from memory alone. Training-data knowledge does not satisfy this
20
20
 
21
21
  ## Invocation
22
22
 
23
- Run Bonsai as `npx @taurgis/bonsai ...`. Add `--json` when you need structured output.
23
+ Run Bonsai as `npx @taurgis/bonsai ...`. Add `--toon` when you need structured output at a lower token cost; `--json` is also available if you specifically need JSON (mutually exclusive with `--toon`).
24
24
 
25
25
  If you are operating in a read-only/plan mode (no filesystem writes allowed), add `--read-only` (alias `--plan`) to every Bonsai call, or export `BONSAI_READ_ONLY=1` (or `BONSAI_PLAN_MODE=1`) once for the session — fetches still run, but nothing is persisted to the local cache or config. See "Read-only / Plan Mode" below.
26
26
 
@@ -45,7 +45,7 @@ If you are operating in a read-only/plan mode (no filesystem writes allowed), ad
45
45
  For structured output:
46
46
 
47
47
  ```bash
48
- npx @taurgis/bonsai <official-url> --format detailed --json
48
+ npx @taurgis/bonsai <official-url> --format detailed --toon
49
49
  ```
50
50
 
51
51
  ## Working with Salesforce Sites
@@ -109,4 +109,4 @@ Return:
109
109
  - Cache status or validation timestamp when Bonsai output provides it.
110
110
  - Gaps or uncertainty where official docs do not answer the question.
111
111
 
112
- Keep stdout clean when using `--json`; warnings and diagnostic notes belong in stderr or in the final prose summary.
112
+ Keep stdout clean when using `--toon`/`--json`; warnings and diagnostic notes belong in stderr or in the final prose summary.
package/bin/cli.mjs CHANGED
@@ -7,7 +7,7 @@ import updateNotifier from 'update-notifier';
7
7
  import { createRequire } from 'node:module';
8
8
 
9
9
  import { normalizeArgv } from '../dist/lib/argv.js';
10
- import { VALUE_TAKING_FLAG_TOKENS } from '../dist/lib/cli-flag-manifest.js';
10
+ import { VALUE_TAKING_FLAG_TOKENS, KNOWN_COMMAND_ROOT_TOKENS } from '../dist/lib/cli-flag-manifest.js';
11
11
  import { exitWithPreflight } from '../dist/lib/cli-emit.js';
12
12
  import { tryUnknownHelpOutput } from '../dist/lib/help-preflight.js';
13
13
  import { tryJsonMetaOutput } from '../dist/lib/json-meta.js';
@@ -23,7 +23,10 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
23
23
  // trying to resolve the bare URL as a command — which crashes with a stack trace. Rewrite
24
24
  // process.argv itself so both the run path and the help/error path agree.
25
25
  const rawArgv = process.argv.slice(2);
26
- const result = normalizeArgv(rawArgv, { valueTakingFlags: VALUE_TAKING_FLAG_TOKENS });
26
+ const result = normalizeArgv(rawArgv, {
27
+ valueTakingFlags: VALUE_TAKING_FLAG_TOKENS,
28
+ knownCommandRoots: KNOWN_COMMAND_ROOT_TOKENS,
29
+ });
27
30
  const root = __dirname + '/../';
28
31
 
29
32
  if (result.earlyExit) {
@@ -41,8 +44,8 @@ if (jsonMeta) {
41
44
  }
42
45
 
43
46
  // Only notify after the earlyExit fast-path so the notifier never fires before
44
- // a usage-error envelope exits. Also skip under --json so agent callers never see stderr noise.
45
- if (!process.argv.includes('--json')) {
47
+ // a usage-error envelope exits. Also skip under --json/--toon so agent callers never see stderr noise.
48
+ if (!process.argv.includes('--json') && !process.argv.includes('--toon')) {
46
49
  updateNotifier({ pkg }).notify();
47
50
  }
48
51
 
@@ -17,11 +17,24 @@ export declare abstract class BaseCommand<T extends typeof Command> extends Comm
17
17
  */
18
18
  static baseFlags: {
19
19
  'read-only': Interfaces.BooleanFlag<boolean>;
20
+ toon: Interfaces.BooleanFlag<boolean>;
20
21
  };
21
22
  flags: Interfaces.InferredFlags<T['flags']>;
22
23
  args: Interfaces.InferredArgs<T['args']>;
23
24
  parsedArgv: string[];
24
25
  init(): Promise<void>;
26
+ /**
27
+ * Treat `--toon` as machine mode too, so every existing `!this.jsonEnabled()` human-output branch
28
+ * across every command (table rendering, tips, empty-state guidance) is suppressed under `--toon`
29
+ * for free, exactly as it already is under `--json`.
30
+ */
31
+ jsonEnabled(): boolean;
32
+ /**
33
+ * Real `--json` keeps oclif's own colorized-JSON rendering untouched (zero regression risk).
34
+ * `--toon` (and no real `--json`) re-encodes the identical envelope as TOON instead — same data,
35
+ * ~40% fewer tokens for callers who opted in.
36
+ */
37
+ protected logJson(json: unknown): void;
25
38
  /**
26
39
  * Always surface warnings on stderr, even under `--json`. oclif silences `warn()` in JSON mode,
27
40
  * but our warnings (secret-redirect, stale-serve, prune failures) are
@@ -29,6 +42,30 @@ export declare abstract class BaseCommand<T extends typeof Command> extends Comm
29
42
  * JSON envelope, so machine output stays clean.
30
43
  */
31
44
  warn(input: string | Error): string | Error;
45
+ /**
46
+ * Sanitize the human-readable error surface before handing off to oclif. Error text throughout
47
+ * this CLI routinely echoes raw user input back (a rejected URL, `--ttl`, an unknown config key)
48
+ * so the message stays actionable, but that value is untrusted per the repo's trust-boundary
49
+ * rules — the same reasoning that already strips ANSI/control bytes from cached content before
50
+ * terminal render (see `sanitizeForTerminal`). Left unsanitized, a value containing raw escape
51
+ * bytes would replay as a terminal-injection attack the moment the rejected command's own error
52
+ * is printed. `--json` is untouched: `JSON.stringify` already escapes control characters, and the
53
+ * raw value there preserves exact input for programmatic callers.
54
+ */
55
+ error(input: string | Error, options: {
56
+ code?: string;
57
+ exit: false;
58
+ } & Errors.PrettyPrintableError): void;
59
+ error(input: string | Error, options?: {
60
+ code?: string;
61
+ exit?: number;
62
+ } & Errors.PrettyPrintableError): never;
63
+ /**
64
+ * Human-mode-only contextual next-step suggestion after a successful command, mirroring the
65
+ * "Try this:" pattern already used on errors. A no-op under `--json`/`--toon`: the envelope's
66
+ * `data` is self-describing, so machine callers get no prose.
67
+ */
68
+ protected tip(message: string): void;
32
69
  /** Whether read-only/plan mode is active for this invocation (flag OR either env var). */
33
70
  protected get readOnly(): boolean;
34
71
  /**
@@ -1,11 +1,24 @@
1
- import { Command, Errors, Flags, toConfiguredId } from '@oclif/core';
2
- import { invalidEnvOverrideWarnings, resolveReadOnly } from './lib/config/index.js';
1
+ import { Command, Errors, Flags, toConfiguredId, ux } from '@oclif/core';
2
+ import { encode } from '@toon-format/toon';
3
+ import { invalidConfigFileWarnings, invalidEnvOverrideWarnings, resolveReadOnly, } from './lib/config/index.js';
3
4
  import { CLI_FLAG_DESCRIPTIONS } from './lib/cli-presentation.js';
5
+ import { formatTip, sanitizeForTerminal } from './lib/text.js';
4
6
  import { enrichErrorForDisplay, resolveExitCode, prepareCliError, EXIT_OK, EXIT_STALE_SERVED, } from './lib/cli-error-policy.js';
5
- import { buildEnvelope, enrichCacheMissEnvelope, enrichRowErrorEnvelope, formatErrorForJson, } from './lib/envelope.js';
7
+ import { buildEnvelope, enrichCacheMissEnvelope, enrichRowErrorEnvelope } from './lib/envelope.js';
6
8
  import { enrichParseError } from './lib/parse-error-ux.js';
7
9
  import { resolveResearchTarget, } from './lib/research/resolve-target.js';
8
10
  import { failInvalidUrl } from './lib/research/cli-io.js';
11
+ /**
12
+ * Mirrors oclif's own `--json` argv-scan (respecting a `--` pass-through boundary) so `--toon`
13
+ * gets identical treatment: present in argv, regardless of parse state.
14
+ */
15
+ function argvHasFlag(argv, flag) {
16
+ const passThroughIndex = argv.indexOf('--');
17
+ const flagIndex = argv.indexOf(flag);
18
+ return passThroughIndex === -1
19
+ ? flagIndex !== -1
20
+ : flagIndex !== -1 && flagIndex < passThroughIndex;
21
+ }
9
22
  /**
10
23
  * Shared base for every Bonsai command. Enables oclif's native `--json` flag,
11
24
  * parses args/flags once in `init()` so commands read `this.args`/`this.flags`
@@ -26,6 +39,10 @@ export class BaseCommand extends Command {
26
39
  default: false,
27
40
  description: CLI_FLAG_DESCRIPTIONS.readOnly,
28
41
  }),
42
+ toon: Flags.boolean({
43
+ default: false,
44
+ description: CLI_FLAG_DESCRIPTIONS.toon,
45
+ }),
29
46
  };
30
47
  flags;
31
48
  args;
@@ -42,11 +59,52 @@ export class BaseCommand extends Command {
42
59
  this.flags = flags;
43
60
  this.args = args;
44
61
  this.parsedArgv = argv;
62
+ // --json and --toon both claim the machine-output seat; picking one silently over the other
63
+ // would surprise a caller who set both by mistake, so reject the combination outright. Reuses
64
+ // the existing CONFLICTING_FLAGS code (not a new one) — the catalog already documents it as the
65
+ // one code for "choose one of the mutually exclusive options", regardless of which flags.
66
+ if (this.flags?.['json'] && this.flags?.['toon']) {
67
+ this.error('Cannot combine --json and --toon: pick one output format.', {
68
+ exit: 2,
69
+ code: 'CONFLICTING_FLAGS',
70
+ suggestions: ['Pass only one of --json or --toon'],
71
+ });
72
+ }
45
73
  // Surface a set-but-invalid BONSAI_* override once per run. Resolution silently drops such a
46
74
  // value, so without this a typo'd env var would take no effect with no signal. Warnings go to
47
75
  // stderr (even under --json), so machine output stays clean.
48
76
  for (const warning of invalidEnvOverrideWarnings(process.env))
49
77
  this.warn(warning);
78
+ // Same signal for a corrupted or hand-edited config file: parsing silently degrades to `{}`
79
+ // (or drops just the offending key), which would otherwise mask the problem entirely.
80
+ for (const warning of invalidConfigFileWarnings(this.config.configDir, process.cwd(), this.config.bin)) {
81
+ this.warn(warning);
82
+ }
83
+ }
84
+ /**
85
+ * Treat `--toon` as machine mode too, so every existing `!this.jsonEnabled()` human-output branch
86
+ * across every command (table rendering, tips, empty-state guidance) is suppressed under `--toon`
87
+ * for free, exactly as it already is under `--json`.
88
+ */
89
+ jsonEnabled() {
90
+ // Mirrors oclif's own enableJsonFlag guard (every command in this CLI sets it, but the base
91
+ // class's contract shouldn't silently break if a future command ever opts out).
92
+ return (super.jsonEnabled() ||
93
+ (Boolean(this.ctor?.enableJsonFlag) && argvHasFlag(this.argv, '--toon')));
94
+ }
95
+ /**
96
+ * Real `--json` keeps oclif's own colorized-JSON rendering untouched (zero regression risk).
97
+ * `--toon` (and no real `--json`) re-encodes the identical envelope as TOON instead — same data,
98
+ * ~40% fewer tokens for callers who opted in.
99
+ */
100
+ logJson(json) {
101
+ if (super.jsonEnabled()) {
102
+ super.logJson(json);
103
+ return;
104
+ }
105
+ // this.log() no-ops whenever jsonEnabled() is true (by design, everywhere else in this CLI) —
106
+ // ux.stdout writes directly, matching how oclif's own default logJson bypasses that guard too.
107
+ ux.stdout(encode(json));
50
108
  }
51
109
  /**
52
110
  * Always surface warnings on stderr, even under `--json`. oclif silences `warn()` in JSON mode,
@@ -58,6 +116,35 @@ export class BaseCommand extends Command {
58
116
  Errors.warn(input);
59
117
  return input;
60
118
  }
119
+ error(input, options = {}) {
120
+ if (!this.jsonEnabled()) {
121
+ if (typeof input === 'string')
122
+ input = sanitizeForTerminal(input);
123
+ if (options.suggestions) {
124
+ options = { ...options, suggestions: options.suggestions.map(sanitizeForTerminal) };
125
+ }
126
+ }
127
+ // `exit: false` is oclif's non-throwing error render (see `errorRow` on the CliIo port and
128
+ // `mapUrlsAllowingBatchErrors`/prune's per-row failures, all of which route through here for the
129
+ // same terminal-injection sanitization above). Destructure `exit` into its own binding so
130
+ // narrowing it to the `false` literal actually narrows the object passed to `super.error()` into
131
+ // each of oclif's two overloads — narrowing a property read alone (`options.exit === false`)
132
+ // doesn't narrow the enclosing object's type, only a local variable's.
133
+ const { exit, ...rest } = options;
134
+ if (exit === false)
135
+ return super.error(input, { ...rest, exit });
136
+ return super.error(input, { ...rest, exit });
137
+ }
138
+ /**
139
+ * Human-mode-only contextual next-step suggestion after a successful command, mirroring the
140
+ * "Try this:" pattern already used on errors. A no-op under `--json`/`--toon`: the envelope's
141
+ * `data` is self-describing, so machine callers get no prose.
142
+ */
143
+ tip(message) {
144
+ if (this.jsonEnabled())
145
+ return;
146
+ this.warn(formatTip(message));
147
+ }
61
148
  /** Whether read-only/plan mode is active for this invocation (flag OR either env var). */
62
149
  get readOnly() {
63
150
  return resolveReadOnly({ flag: Boolean(this.flags?.['read-only']), env: process.env });
@@ -108,6 +195,7 @@ export class BaseCommand extends Command {
108
195
  configDir: this.config.configDir,
109
196
  cwd: process.cwd(),
110
197
  dataDir: this.config.dataDir,
198
+ readOnly: this.readOnly,
111
199
  url,
112
200
  ...extra,
113
201
  });
@@ -131,6 +219,7 @@ export class BaseCommand extends Command {
131
219
  warn: (msg) => void this.warn(msg),
132
220
  log: (msg) => this.log(msg),
133
221
  error: (msg, opts) => this.error(msg, opts),
222
+ errorRow: (msg, opts) => this.error(msg, { ...opts, exit: false }),
134
223
  };
135
224
  }
136
225
  /** Single source of truth for the `--json` envelope shape, shared by success and error output. */
@@ -187,10 +276,12 @@ export class BaseCommand extends Command {
187
276
  catch (err) {
188
277
  if (!batch || !(err instanceof Errors.CLIError))
189
278
  throw err;
190
- // Mirror the single-URL error format (message + Code: + Try this:) rather than a bare
191
- // message, so a row failure in a batch is exactly as actionable as it is standalone.
279
+ // Render with the same "Error:" formatting as a single-URL failure (this.error's exit:false
280
+ // form prints the CLIError's own message/Code/Try this and returns instead of throwing) rather
281
+ // than this.warn, since a row failure still fails the command overall (status/inspect batch
282
+ // exit code, JSON ok:false) and should never read as merely a warning.
192
283
  if (!this.jsonEnabled())
193
- this.warn(formatErrorForJson(err));
284
+ this.error(err, { exit: false });
194
285
  results.push(failureRow(url, err));
195
286
  }
196
287
  }
@@ -11,6 +11,7 @@ export class ConfigCommand extends BaseCommand {
11
11
  this.error('--global and --local are mutually exclusive.', {
12
12
  exit: 2,
13
13
  code: 'CONFLICTING_FLAGS',
14
+ suggestions: ['Pass --global or --local, not both.'],
14
15
  });
15
16
  }
16
17
  }
@@ -1,10 +1,10 @@
1
1
  import { Args } from '@oclif/core';
2
2
  import { ConfigCommand, configScopeFlags } from './base.js';
3
- import { formatConfigEntry, resolveConfigEntry, validKeysHint } from '../../lib/config/index.js';
3
+ import { formatConfigEntry, resolveConfigEntry, keyValuesHint } from '../../lib/config/index.js';
4
4
  export default class ConfigGet extends ConfigCommand {
5
5
  static id = 'config get';
6
6
  static summary = 'Get a research configuration value';
7
- static description = `Print a config value. Without --global/--local, shows the merged effective value, in precedence order: a per-command override flag (e.g. \`fetch --storage\`) > env var > project file > user file > built-in default.\n\nValid keys: ${validKeysHint()}.`;
7
+ static description = `Print a config value. Without --global/--local, shows the merged effective value, in precedence order: a per-command override flag (e.g. \`fetch --storage\`) > env var > project file > user file > built-in default.\n\nValid keys: ${keyValuesHint()}.`;
8
8
  static examples = [
9
9
  {
10
10
  description: 'get the effective storage mode',
@@ -1,9 +1,9 @@
1
1
  import { ConfigCommand, configScopeFlags } from './base.js';
2
- import { formatConfigEntry, resolveConfigEntries, validKeysHint } from '../../lib/config/index.js';
2
+ import { formatConfigEntry, resolveConfigEntries, keyValuesHint } from '../../lib/config/index.js';
3
3
  export default class ConfigList extends ConfigCommand {
4
4
  static id = 'config list';
5
5
  static summary = 'List research configuration values';
6
- static description = `Show every configuration key with its current value. Use --global/--local to show a single scope.\n\nValid keys: ${validKeysHint()}.`;
6
+ static description = `Show every configuration key with its current value. Use --global/--local to show a single scope.\n\nValid keys: ${keyValuesHint()}.`;
7
7
  static examples = [
8
8
  {
9
9
  description: 'show all effective config values',
@@ -1,12 +1,12 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import { ConfigCommand, configScopeFlags } from './base.js';
3
- import { KEY_META, validKeysHint } from '../../lib/config/index.js';
3
+ import { KEY_META, keyValuesHint } from '../../lib/config/index.js';
4
4
  import { persistConfigPatch } from '../../lib/config-persist.js';
5
5
  export default class ConfigSet extends ConfigCommand {
6
6
  static id = 'config set';
7
7
  static summary = 'Set a research configuration key';
8
8
  static description = 'Persist a configuration value. Writes user-level config by default; pass --local for project-level config (.bonsai.json in cwd).\n\n' +
9
- `Valid keys: ${validKeysHint()}. The inline form \`<key>=<value>\` is also accepted.`;
9
+ `Valid keys: ${keyValuesHint()}. The inline form \`<key>=<value>\` is also accepted.`;
10
10
  static examples = [
11
11
  {
12
12
  description: 'store research cache in the current project',
@@ -1,11 +1,11 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import { ConfigCommand, configScopeFlags } from './base.js';
3
- import { validKeysHint } from '../../lib/config/index.js';
3
+ import { keyValuesHint } from '../../lib/config/index.js';
4
4
  import { persistConfigPatch } from '../../lib/config-persist.js';
5
5
  export default class ConfigUnset extends ConfigCommand {
6
6
  static id = 'config unset';
7
7
  static summary = 'Remove a research configuration key';
8
- 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
+ static description = `Delete a key from user-level config (default) or project-level config (--local), restoring the built-in default.\n\nValid keys: ${keyValuesHint()}.`;
9
9
  static examples = [
10
10
  {
11
11
  description: 'remove the user-level storage setting',
@@ -18,6 +18,10 @@ export default class FetchCommand extends BaseCommand {
18
18
  description: 'cache a volatile page with compressed JSON output',
19
19
  command: '<%= config.bin %> https://news.ycombinator.com/ --format compressed --ttl 2h --json',
20
20
  },
21
+ {
22
+ description: 'fetch multiple URLs in one batch (space-separated)',
23
+ command: '<%= config.bin %> https://docs.nestjs.com/ https://docs.nestjs.com/first-steps',
24
+ },
21
25
  ];
22
26
  static strict = false;
23
27
  static args = {
@@ -6,6 +6,7 @@ import { getArtifactPath, scanCacheDirs } from '../lib/research/storage.js';
6
6
  import { artifactMatchesUrlFilter } from '../lib/research/url.js';
7
7
  import { colors } from '../lib/color.js';
8
8
  import { formatHumanField } from '../lib/cli-presentation.js';
9
+ import { sanitizeForTerminal } from '../lib/text.js';
9
10
  /** Inspect a cached artifact and its section children. */
10
11
  export default class ResearchInspect extends BaseCommand {
11
12
  static id = 'inspect';
@@ -20,6 +21,10 @@ export default class ResearchInspect extends BaseCommand {
20
21
  description: 'inspect cached metadata as JSON',
21
22
  command: '<%= config.bin %> <%= command.id %> https://example.com/docs --json',
22
23
  },
24
+ {
25
+ description: 'inspect multiple URLs in one batch (space-separated)',
26
+ command: '<%= config.bin %> <%= command.id %> https://example.com/docs https://example.com/api',
27
+ },
23
28
  ];
24
29
  static strict = false;
25
30
  static args = {
@@ -110,6 +115,7 @@ export default class ResearchInspect extends BaseCommand {
110
115
  this.logMetadata(cached.metadata);
111
116
  if (sections.length)
112
117
  this.logSections(sections);
118
+ this.tip(`${this.config.bin} status ${normalizedUrl} to check freshness.`);
113
119
  const sep = batchSeparator(this.parsedArgv.length > 1);
114
120
  if (sep)
115
121
  this.log(sep);
@@ -130,14 +136,17 @@ export default class ResearchInspect extends BaseCommand {
130
136
  this.log(formatHumanField(key, colors.bold(JSON.stringify(val))));
131
137
  }
132
138
  else {
133
- this.log(formatHumanField(key, colors.bold(String(val))));
139
+ const display = typeof val === 'string' ? sanitizeForTerminal(val) : String(val);
140
+ this.log(formatHumanField(key, colors.bold(display)));
134
141
  }
135
142
  }
136
143
  }
137
144
  logSections(sections) {
138
145
  this.log(colors.cyan(`--- Sections (${sections.length}) ---`));
139
146
  for (const s of sections) {
140
- this.log(`${colors.cyan(s.headingPath || '')} [${colors.yellow(s.anchor || '')}] (${colors.magenta(String(s.tokenEstimate.detailed || 0))} tokens) ${colors.gray(s.cacheKey)}`);
147
+ const headingPath = sanitizeForTerminal(s.headingPath || '');
148
+ const anchor = sanitizeForTerminal(s.anchor || '');
149
+ this.log(`${colors.cyan(headingPath)} [${colors.yellow(anchor)}] (${colors.magenta(String(s.tokenEstimate.detailed || 0))} tokens) ${colors.gray(s.cacheKey)}`);
141
150
  }
142
151
  }
143
152
  // Section children link back via parent_cache_key; list the active ones for this page (T-22).
@@ -1,15 +1,16 @@
1
1
  import { BaseCommand } from '../base-command.js';
2
- import type { ListRow } from '../lib/cli-result-types.js';
2
+ import type { ListRow, ListRowMinimal } from '../lib/cli-result-types.js';
3
3
  /** List cached research artifacts by metadata filters. */
4
4
  export default class ResearchList extends BaseCommand<typeof ResearchList> {
5
5
  static id: string;
6
6
  static summary: string;
7
7
  static description: string;
8
8
  /**
9
- * When `--json` results were capped by `--limit`, attached to the success envelope so agents can
10
- * detect truncation without scraping prose (#91 / AUDIT_71_FINAL #73 deferred).
9
+ * Aggregate counts (and an explicit empty/truncation signal) attached to the success envelope so
10
+ * agents get a cache-wide summary without a separate round trip (AXI principles: pre-computed
11
+ * aggregates, definitive empty states). Always set by `run()` before it returns.
11
12
  */
12
- private jsonTruncation;
13
+ private jsonSummary;
13
14
  static examples: {
14
15
  description: string;
15
16
  command: string;
@@ -22,17 +23,19 @@ export default class ResearchList extends BaseCommand<typeof ResearchList> {
22
23
  'artifact-type': import("@oclif/core/interfaces").OptionFlag<"source" | "research_note" | "index" | undefined, import("@oclif/core/interfaces").CustomOptions>;
23
24
  'capture-method': import("@oclif/core/interfaces").OptionFlag<"static_fetch" | "browser_fallback" | "agent_supplied" | "route_markdown" | "github_source" | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
25
  limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
26
+ full: import("@oclif/core/interfaces").BooleanFlag<boolean>;
25
27
  };
26
28
  static stdoutIsPrimaryData: boolean;
27
- private matchesTopic;
28
- private matchesTags;
29
29
  private matchesFilters;
30
30
  private scanCacheDirForList;
31
31
  private hasActiveFilters;
32
32
  private logListResults;
33
- /** Empty-cache / no-match tip (human mode only; --json returns `data: []` with no messaging). */
33
+ /**
34
+ * Empty-cache / no-match tip (human mode only). Under `--json`/`--toon`, `data: []` is paired with
35
+ * `summary.empty: true` instead of prose — see {@link toSuccessJson}.
36
+ */
34
37
  private emitEmptyListGuidance;
35
- run(): Promise<ListRow[]>;
36
- /** Attach `truncation` when the returned `data` array was capped by `--limit`. */
38
+ run(): Promise<ListRow[] | ListRowMinimal[]>;
39
+ /** Attach the aggregate `summary` computed in `run()` (see {@link ListSummary}). */
37
40
  protected toSuccessJson(data: unknown): Record<string, unknown>;
38
41
  }