@taurgis/bonsai 3.2.2 → 4.1.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 (100) hide show
  1. package/README.md +25 -2
  2. package/agents/agents/official-docs-researcher.agent.md +102 -0
  3. package/agents/agents/salesforce-docs-researcher.agent.md +17 -15
  4. package/agents/hooks/README.md +18 -2
  5. package/agents/hooks/claude-code/hooks/bonsai-web-fetch.sh +1 -1
  6. package/agents/hooks/codex/hooks/bonsai-user-prompt-context.sh +1 -1
  7. package/agents/hooks/codex/hooks/bonsai-web-fetch.sh +1 -1
  8. package/agents/hooks/cursor/hooks/bonsai-web-fetch.sh +1 -1
  9. package/agents/hooks/github-copilot/bonsai-web-fetch.sh +1 -1
  10. package/agents/instructions/salesforce-research.instructions.md +5 -4
  11. package/agents/instructions/web-research.instructions.md +45 -0
  12. package/agents/skills/web-research/SKILL.md +139 -0
  13. package/bin/cli.mjs +7 -4
  14. package/dist/base-command.d.ts +42 -0
  15. package/dist/base-command.js +106 -6
  16. package/dist/commands/config/base.d.ts +2 -7
  17. package/dist/commands/config/base.js +4 -17
  18. package/dist/commands/config/get.js +2 -2
  19. package/dist/commands/config/list.js +2 -2
  20. package/dist/commands/config/set.js +2 -2
  21. package/dist/commands/config/unset.js +2 -2
  22. package/dist/commands/context.d.ts +20 -0
  23. package/dist/commands/context.js +65 -0
  24. package/dist/commands/fetch.js +4 -0
  25. package/dist/commands/inspect.js +11 -2
  26. package/dist/commands/list.d.ts +23 -11
  27. package/dist/commands/list.js +110 -121
  28. package/dist/commands/prune.d.ts +2 -0
  29. package/dist/commands/prune.js +35 -4
  30. package/dist/commands/search.d.ts +43 -0
  31. package/dist/commands/search.js +233 -0
  32. package/dist/commands/setup.d.ts +27 -0
  33. package/dist/commands/setup.js +148 -0
  34. package/dist/commands/status.js +10 -0
  35. package/dist/commands.d.ts +6 -0
  36. package/dist/commands.js +6 -0
  37. package/dist/hooks/command-not-found/suggest.d.ts +1 -1
  38. package/dist/hooks/command-not-found/suggest.js +12 -2
  39. package/dist/lib/argv.d.ts +5 -0
  40. package/dist/lib/argv.js +30 -5
  41. package/dist/lib/cli-flag-manifest.d.ts +7 -0
  42. package/dist/lib/cli-flag-manifest.js +7 -0
  43. package/dist/lib/cli-presentation.d.ts +25 -4
  44. package/dist/lib/cli-presentation.js +34 -4
  45. package/dist/lib/cli-result-types.d.ts +84 -1
  46. package/dist/lib/color.d.ts +2 -0
  47. package/dist/lib/color.js +6 -0
  48. package/dist/lib/common-metadata-filter-flags.d.ts +14 -0
  49. package/dist/lib/common-metadata-filter-flags.js +37 -0
  50. package/dist/lib/config/index.d.ts +2 -2
  51. package/dist/lib/config/index.js +2 -2
  52. package/dist/lib/config/io.d.ts +9 -0
  53. package/dist/lib/config/io.js +71 -21
  54. package/dist/lib/config/schema.d.ts +2 -0
  55. package/dist/lib/config/schema.js +7 -0
  56. package/dist/lib/envelope.js +11 -4
  57. package/dist/lib/next-command.d.ts +15 -0
  58. package/dist/lib/next-command.js +40 -0
  59. package/dist/lib/prune-flags.d.ts +2 -0
  60. package/dist/lib/prune-flags.js +18 -6
  61. package/dist/lib/research/artifact-index.js +7 -1
  62. package/dist/lib/research/artifact.js +20 -2
  63. package/dist/lib/research/browser.d.ts +12 -0
  64. package/dist/lib/research/browser.js +63 -8
  65. package/dist/lib/research/cli-io.d.ts +7 -0
  66. package/dist/lib/research/context-dashboard.d.ts +19 -0
  67. package/dist/lib/research/context-dashboard.js +43 -0
  68. package/dist/lib/research/fetch-command-service.js +90 -23
  69. package/dist/lib/research/fetch-result.d.ts +1 -0
  70. package/dist/lib/research/fetch-result.js +4 -0
  71. package/dist/lib/research/fetcher.js +3 -1
  72. package/dist/lib/research/import-command-service.js +22 -1
  73. package/dist/lib/research/list-row.d.ts +15 -0
  74. package/dist/lib/research/list-row.js +31 -0
  75. package/dist/lib/research/metadata-filters.d.ts +39 -0
  76. package/dist/lib/research/metadata-filters.js +71 -0
  77. package/dist/lib/research/metadata-flags.d.ts +34 -0
  78. package/dist/lib/research/metadata-flags.js +45 -0
  79. package/dist/lib/research/prompt-injection.d.ts +2 -1
  80. package/dist/lib/research/prompt-injection.js +18 -2
  81. package/dist/lib/research/resolve-target.d.ts +2 -0
  82. package/dist/lib/research/resolve-target.js +1 -1
  83. package/dist/lib/research/schema.d.ts +2 -0
  84. package/dist/lib/research/schema.js +4 -0
  85. package/dist/lib/research/search-match.d.ts +39 -0
  86. package/dist/lib/research/search-match.js +134 -0
  87. package/dist/lib/research/storage.d.ts +4 -7
  88. package/dist/lib/research/storage.js +26 -25
  89. package/dist/lib/scope-flags.d.ts +19 -0
  90. package/dist/lib/scope-flags.js +27 -0
  91. package/dist/lib/setup/agents.d.ts +16 -0
  92. package/dist/lib/setup/agents.js +20 -0
  93. package/dist/lib/setup/bin-command.d.ts +14 -0
  94. package/dist/lib/setup/bin-command.js +42 -0
  95. package/dist/lib/setup/hook-file-io.d.ts +37 -0
  96. package/dist/lib/setup/hook-file-io.js +80 -0
  97. package/dist/lib/text.d.ts +20 -8
  98. package/dist/lib/text.js +32 -9
  99. package/oclif.manifest.json +406 -13
  100. package/package.json +2 -1
@@ -1,11 +1,25 @@
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 { colors } from './lib/color.js';
6
+ import { formatTip, sanitizeForTerminal } from './lib/text.js';
4
7
  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';
8
+ import { buildEnvelope, enrichCacheMissEnvelope, enrichRowErrorEnvelope } from './lib/envelope.js';
6
9
  import { enrichParseError } from './lib/parse-error-ux.js';
7
10
  import { resolveResearchTarget, } from './lib/research/resolve-target.js';
8
11
  import { failInvalidUrl } from './lib/research/cli-io.js';
12
+ /**
13
+ * Mirrors oclif's own `--json` argv-scan (respecting a `--` pass-through boundary) so `--toon`
14
+ * gets identical treatment: present in argv, regardless of parse state.
15
+ */
16
+ function argvHasFlag(argv, flag) {
17
+ const passThroughIndex = argv.indexOf('--');
18
+ const flagIndex = argv.indexOf(flag);
19
+ return passThroughIndex === -1
20
+ ? flagIndex !== -1
21
+ : flagIndex !== -1 && flagIndex < passThroughIndex;
22
+ }
9
23
  /**
10
24
  * Shared base for every Bonsai command. Enables oclif's native `--json` flag,
11
25
  * parses args/flags once in `init()` so commands read `this.args`/`this.flags`
@@ -26,6 +40,10 @@ export class BaseCommand extends Command {
26
40
  default: false,
27
41
  description: CLI_FLAG_DESCRIPTIONS.readOnly,
28
42
  }),
43
+ toon: Flags.boolean({
44
+ default: false,
45
+ description: CLI_FLAG_DESCRIPTIONS.toon,
46
+ }),
29
47
  };
30
48
  flags;
31
49
  args;
@@ -42,11 +60,52 @@ export class BaseCommand extends Command {
42
60
  this.flags = flags;
43
61
  this.args = args;
44
62
  this.parsedArgv = argv;
63
+ // --json and --toon both claim the machine-output seat; picking one silently over the other
64
+ // would surprise a caller who set both by mistake, so reject the combination outright. Reuses
65
+ // the existing CONFLICTING_FLAGS code (not a new one) — the catalog already documents it as the
66
+ // one code for "choose one of the mutually exclusive options", regardless of which flags.
67
+ if (this.flags?.['json'] && this.flags?.['toon']) {
68
+ this.error('Cannot combine --json and --toon: pick one output format.', {
69
+ exit: 2,
70
+ code: 'CONFLICTING_FLAGS',
71
+ suggestions: ['Pass only one of --json or --toon'],
72
+ });
73
+ }
45
74
  // Surface a set-but-invalid BONSAI_* override once per run. Resolution silently drops such a
46
75
  // value, so without this a typo'd env var would take no effect with no signal. Warnings go to
47
76
  // stderr (even under --json), so machine output stays clean.
48
77
  for (const warning of invalidEnvOverrideWarnings(process.env))
49
78
  this.warn(warning);
79
+ // Same signal for a corrupted or hand-edited config file: parsing silently degrades to `{}`
80
+ // (or drops just the offending key), which would otherwise mask the problem entirely.
81
+ for (const warning of invalidConfigFileWarnings(this.config.configDir, process.cwd(), this.config.bin)) {
82
+ this.warn(warning);
83
+ }
84
+ }
85
+ /**
86
+ * Treat `--toon` as machine mode too, so every existing `!this.jsonEnabled()` human-output branch
87
+ * across every command (table rendering, tips, empty-state guidance) is suppressed under `--toon`
88
+ * for free, exactly as it already is under `--json`.
89
+ */
90
+ jsonEnabled() {
91
+ // Mirrors oclif's own enableJsonFlag guard (every command in this CLI sets it, but the base
92
+ // class's contract shouldn't silently break if a future command ever opts out).
93
+ return (super.jsonEnabled() ||
94
+ (Boolean(this.ctor?.enableJsonFlag) && argvHasFlag(this.argv, '--toon')));
95
+ }
96
+ /**
97
+ * Real `--json` keeps oclif's own colorized-JSON rendering untouched (zero regression risk).
98
+ * `--toon` (and no real `--json`) re-encodes the identical envelope as TOON instead — same data,
99
+ * ~40% fewer tokens for callers who opted in.
100
+ */
101
+ logJson(json) {
102
+ if (super.jsonEnabled()) {
103
+ super.logJson(json);
104
+ return;
105
+ }
106
+ // this.log() no-ops whenever jsonEnabled() is true (by design, everywhere else in this CLI) —
107
+ // ux.stdout writes directly, matching how oclif's own default logJson bypasses that guard too.
108
+ ux.stdout(encode(json));
50
109
  }
51
110
  /**
52
111
  * Always surface warnings on stderr, even under `--json`. oclif silences `warn()` in JSON mode,
@@ -58,6 +117,43 @@ export class BaseCommand extends Command {
58
117
  Errors.warn(input);
59
118
  return input;
60
119
  }
120
+ error(input, options = {}) {
121
+ if (!this.jsonEnabled()) {
122
+ if (typeof input === 'string')
123
+ input = sanitizeForTerminal(input);
124
+ if (options.suggestions) {
125
+ options = { ...options, suggestions: options.suggestions.map(sanitizeForTerminal) };
126
+ }
127
+ }
128
+ // `exit: false` is oclif's non-throwing error render (see `errorRow` on the CliIo port and
129
+ // `mapUrlsAllowingBatchErrors`/prune's per-row failures, all of which route through here for the
130
+ // same terminal-injection sanitization above). Destructure `exit` into its own binding so
131
+ // narrowing it to the `false` literal actually narrows the object passed to `super.error()` into
132
+ // each of oclif's two overloads — narrowing a property read alone (`options.exit === false`)
133
+ // doesn't narrow the enclosing object's type, only a local variable's.
134
+ const { exit, ...rest } = options;
135
+ if (exit === false)
136
+ return super.error(input, { ...rest, exit });
137
+ return super.error(input, { ...rest, exit });
138
+ }
139
+ /**
140
+ * Human-mode-only contextual next-step suggestion after a successful command, mirroring the
141
+ * "Try this:" pattern already used on errors. A no-op under `--json`/`--toon`: the envelope's
142
+ * `data` is self-describing, so machine callers get no prose.
143
+ */
144
+ tip(message) {
145
+ if (this.jsonEnabled())
146
+ return;
147
+ this.warn(formatTip(message));
148
+ }
149
+ /**
150
+ * Human-mode tip for a truncated `list`/`search` result: the copy-pasteable command that raises
151
+ * `--limit` (see `next-command.ts`). A no-op when nothing was truncated (`nextCommand` is `null`).
152
+ */
153
+ tipNextCommand(nextCommand) {
154
+ if (nextCommand)
155
+ this.tip(`see the rest: ${colors.cyan(nextCommand)}`);
156
+ }
61
157
  /** Whether read-only/plan mode is active for this invocation (flag OR either env var). */
62
158
  get readOnly() {
63
159
  return resolveReadOnly({ flag: Boolean(this.flags?.['read-only']), env: process.env });
@@ -108,6 +204,7 @@ export class BaseCommand extends Command {
108
204
  configDir: this.config.configDir,
109
205
  cwd: process.cwd(),
110
206
  dataDir: this.config.dataDir,
207
+ readOnly: this.readOnly,
111
208
  url,
112
209
  ...extra,
113
210
  });
@@ -131,6 +228,7 @@ export class BaseCommand extends Command {
131
228
  warn: (msg) => void this.warn(msg),
132
229
  log: (msg) => this.log(msg),
133
230
  error: (msg, opts) => this.error(msg, opts),
231
+ errorRow: (msg, opts) => this.error(msg, { ...opts, exit: false }),
134
232
  };
135
233
  }
136
234
  /** Single source of truth for the `--json` envelope shape, shared by success and error output. */
@@ -187,10 +285,12 @@ export class BaseCommand extends Command {
187
285
  catch (err) {
188
286
  if (!batch || !(err instanceof Errors.CLIError))
189
287
  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.
288
+ // Render with the same "Error:" formatting as a single-URL failure (this.error's exit:false
289
+ // form prints the CLIError's own message/Code/Try this and returns instead of throwing) rather
290
+ // than this.warn, since a row failure still fails the command overall (status/inspect batch
291
+ // exit code, JSON ok:false) and should never read as merely a warning.
192
292
  if (!this.jsonEnabled())
193
- this.warn(formatErrorForJson(err));
293
+ this.error(err, { exit: false });
194
294
  results.push(failureRow(url, err));
195
295
  }
196
296
  }
@@ -1,6 +1,7 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import { BaseCommand } from '../../base-command.js';
3
3
  import type { ConfigKey, ConfigScope } from '../../lib/config/index.js';
4
+ import { scopeFlags } from '../../lib/scope-flags.js';
4
5
  /**
5
6
  * Shared base for the four `config` subcommands: the mutually-exclusive
6
7
  * scope-flag guard, the binary write-scope helper, and known-key validation.
@@ -16,10 +17,4 @@ export declare abstract class ConfigCommand<T extends typeof Command> extends Ba
16
17
  protected validateConfigKeyAndScope(key: string | undefined, global?: boolean, local?: boolean): asserts key is ConfigKey;
17
18
  }
18
19
  /** Build the `--global`/`--local` scope-flag pair with per-command descriptions. */
19
- export declare function configScopeFlags(descriptions: {
20
- global: string;
21
- local: string;
22
- }): {
23
- global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
24
- local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
25
- };
20
+ export declare const configScopeFlags: typeof scopeFlags;
@@ -1,18 +1,14 @@
1
- import { Flags, toConfiguredId } from '@oclif/core';
1
+ import { toConfiguredId } from '@oclif/core';
2
2
  import { BaseCommand } from '../../base-command.js';
3
3
  import { isKnownKey, suggestKey, validKeysHint } from '../../lib/config/index.js';
4
+ import { assertScopeFlagsExclusive, scopeFlags } from '../../lib/scope-flags.js';
4
5
  /**
5
6
  * Shared base for the four `config` subcommands: the mutually-exclusive
6
7
  * scope-flag guard, the binary write-scope helper, and known-key validation.
7
8
  */
8
9
  export class ConfigCommand extends BaseCommand {
9
10
  assertScopeFlagsExclusive(global, local) {
10
- if (global && local) {
11
- this.error('--global and --local are mutually exclusive.', {
12
- exit: 2,
13
- code: 'CONFLICTING_FLAGS',
14
- });
15
- }
11
+ assertScopeFlagsExclusive(this.error.bind(this), global, local);
16
12
  }
17
13
  /** `--local` writes/reads the project file; otherwise the user-level file. */
18
14
  writeScope(local) {
@@ -57,13 +53,4 @@ export class ConfigCommand extends BaseCommand {
57
53
  }
58
54
  }
59
55
  /** Build the `--global`/`--local` scope-flag pair with per-command descriptions. */
60
- export function configScopeFlags(descriptions) {
61
- return {
62
- global: Flags.boolean({ char: 'g', description: descriptions.global }),
63
- local: Flags.boolean({
64
- description: descriptions.local,
65
- aliases: ['project'],
66
- charAliases: ['p'],
67
- }),
68
- };
69
- }
56
+ export const configScopeFlags = scopeFlags;
@@ -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',
@@ -0,0 +1,20 @@
1
+ import { BaseCommand } from '../base-command.js';
2
+ import type { ContextDashboard } from '../lib/cli-result-types.js';
3
+ /**
4
+ * Compact, directory-scoped cache summary (AXI principle 7: ambient session-start context).
5
+ * Plain-text human output is deliberately short — it's meant to be piped verbatim into a
6
+ * SessionStart hook's `additionalContext` by `bonsai setup`, so every line here is also a
7
+ * reasonable thing for an agent to read unprompted at the start of a session.
8
+ */
9
+ export default class Context extends BaseCommand<typeof Context> {
10
+ static id: string;
11
+ static summary: string;
12
+ static description: string;
13
+ static examples: {
14
+ description: string;
15
+ command: string;
16
+ }[];
17
+ static stdoutIsPrimaryData: boolean;
18
+ private logDashboard;
19
+ run(): Promise<ContextDashboard>;
20
+ }
@@ -0,0 +1,65 @@
1
+ import { BaseCommand } from '../base-command.js';
2
+ import { loadStoreRoots } from '../lib/research/store-roots.js';
3
+ import { buildContextDashboard } from '../lib/research/context-dashboard.js';
4
+ import { NO_TOPIC_LABEL, formatTip, pluralize, sanitizeForTerminal } from '../lib/text.js';
5
+ import { colors, FRESHNESS_COLOR } from '../lib/color.js';
6
+ /**
7
+ * Compact, directory-scoped cache summary (AXI principle 7: ambient session-start context).
8
+ * Plain-text human output is deliberately short — it's meant to be piped verbatim into a
9
+ * SessionStart hook's `additionalContext` by `bonsai setup`, so every line here is also a
10
+ * reasonable thing for an agent to read unprompted at the start of a session.
11
+ */
12
+ export default class Context extends BaseCommand {
13
+ static id = 'context';
14
+ static summary = 'Show a compact cache dashboard for ambient session context';
15
+ static description = 'Print a short summary of the current research cache: total entries, a freshness ' +
16
+ 'breakdown, and the most recently touched pages. This is what `bonsai setup` wires into a ' +
17
+ "SessionStart hook, so a session starts with the cache's state already visible.";
18
+ static examples = [
19
+ {
20
+ description: 'preview what a SessionStart hook would show',
21
+ command: '<%= config.bin %> context',
22
+ },
23
+ {
24
+ description: 'get the dashboard as JSON',
25
+ command: '<%= config.bin %> context --json',
26
+ },
27
+ ];
28
+ static stdoutIsPrimaryData = true;
29
+ logDashboard(dashboard) {
30
+ if (this.jsonEnabled())
31
+ return;
32
+ if (dashboard.total === 0) {
33
+ this.log(`${this.config.bin} cache: 0 entries found.`);
34
+ this.log(formatTip(`populate the cache: ${colors.cyan(`${this.config.bin} <url>`)}`));
35
+ return;
36
+ }
37
+ const { fresh, stale_grace: staleGrace, stale_expired: staleExpired } = dashboard.byFreshness;
38
+ this.log(`${this.config.bin} cache: ${dashboard.total} ${pluralize(dashboard.total, 'entry', 'entries')} ` +
39
+ `(${fresh} fresh, ${staleGrace} stale_grace, ${staleExpired} stale_expired)`);
40
+ for (const entry of dashboard.entries) {
41
+ const topicStr = entry.topic
42
+ ? colors.cyan(sanitizeForTerminal(entry.topic))
43
+ : colors.gray(NO_TOPIC_LABEL);
44
+ const freshnessStr = FRESHNESS_COLOR[entry.freshness](entry.freshness);
45
+ const url = entry.sourceUrls[0] ?? '';
46
+ this.log(`- [${topicStr}] ${freshnessStr} — ${colors.gray(url)}`);
47
+ }
48
+ if (dashboard.total > dashboard.shown) {
49
+ this.log(formatTip(`see all ${dashboard.total} entries: ${colors.cyan(`${this.config.bin} list`)}`));
50
+ }
51
+ this.log(formatTip(`research a new page: ${colors.cyan(`${this.config.bin} <url>`)}`));
52
+ }
53
+ async run() {
54
+ const roots = loadStoreRoots({
55
+ configDir: this.config.configDir,
56
+ cwd: process.cwd(),
57
+ dataDir: this.config.dataDir,
58
+ });
59
+ const dashboard = buildContextDashboard(roots.readRoots, new Date(), {
60
+ persistIndex: !this.readOnly,
61
+ });
62
+ this.logDashboard(dashboard);
63
+ return dashboard;
64
+ }
65
+ }
@@ -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,38 +1,50 @@
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;
16
17
  }[];
17
18
  static flags: {
19
+ limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
20
+ full: import("@oclif/core/interfaces").BooleanFlag<boolean>;
21
+ identity: import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
22
  topic: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
23
  tags: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
24
  url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
25
  freshness: import("@oclif/core/interfaces").OptionFlag<"fresh" | "stale_grace" | "stale_expired" | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
26
  'artifact-type': import("@oclif/core/interfaces").OptionFlag<"source" | "research_note" | "index" | undefined, import("@oclif/core/interfaces").CustomOptions>;
23
27
  'capture-method': import("@oclif/core/interfaces").OptionFlag<"static_fetch" | "browser_fallback" | "agent_supplied" | "route_markdown" | "github_source" | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
- limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
25
28
  };
26
29
  static stdoutIsPrimaryData: boolean;
27
- private matchesTopic;
28
- private matchesTags;
30
+ /** The shared filter flags as `matchesCommonMetadataFilters`/`hasActiveMetadataFilters` expect them. */
31
+ private metadataFilterFlags;
29
32
  private matchesFilters;
30
33
  private scanCacheDirForList;
31
- private hasActiveFilters;
34
+ /**
35
+ * Identity header for the AXI "home view" (bare `bonsai`, no args): the tool's own bin path and
36
+ * one-sentence description, shown before the live data so an agent orients on what it's looking
37
+ * at without a separate `--help` round trip. Human mode only — the hidden `--identity` flag is
38
+ * only ever set by the argv rewrite for a bare invocation; it is not documented for direct use.
39
+ */
40
+ private logIdentityHeader;
32
41
  private logListResults;
33
- /** Empty-cache / no-match tip (human mode only; --json returns `data: []` with no messaging). */
42
+ /**
43
+ * Empty-cache / no-match tip (human mode only). Under `--json`/`--toon`, `data: []` is paired with
44
+ * `summary.empty: true` instead of prose — see {@link toSuccessJson}.
45
+ */
34
46
  private emitEmptyListGuidance;
35
- run(): Promise<ListRow[]>;
36
- /** Attach `truncation` when the returned `data` array was capped by `--limit`. */
47
+ run(): Promise<ListRow[] | ListRowMinimal[]>;
48
+ /** Attach the aggregate `summary` computed in `run()` (see {@link ListSummary}). */
37
49
  protected toSuccessJson(data: unknown): Record<string, unknown>;
38
50
  }