@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
@@ -0,0 +1,42 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { realpathSync } from 'node:fs';
3
+ /** `which`/`where` lookup for a command name; `null` when not found on `PATH`. */
4
+ export function findOnPath(command) {
5
+ const finder = process.platform === 'win32' ? 'where' : 'which';
6
+ try {
7
+ const output = execFileSync(finder, [command], {
8
+ encoding: 'utf-8',
9
+ stdio: ['ignore', 'pipe', 'ignore'],
10
+ });
11
+ // `where` on Windows can print multiple matches, one per line; the first is the one PATH resolves to.
12
+ return output.split(/\r?\n/).find((line) => line.trim() !== '') ?? null;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ /**
19
+ * Resolves the command a hook config should invoke to run this CLI (AXI rule: "hook commands
20
+ * should use a PATH-verified binary name when it resolves to the current executable, and fall
21
+ * back to the full absolute path otherwise"). This keeps a global install portable across
22
+ * machines while guaranteeing a hook never silently runs a different `bonsai` than the one that
23
+ * installed it.
24
+ *
25
+ * @param scriptPath - The currently running script (`process.argv[1]`).
26
+ * @param lookup - `PATH` lookup, injectable for tests.
27
+ * @returns `"bonsai"` when `PATH` resolves to this same script, otherwise `node "<absolute path>"`.
28
+ */
29
+ export function resolveBinCommand(scriptPath, lookup = findOnPath) {
30
+ const realScript = realpathSync(scriptPath);
31
+ const onPath = lookup('bonsai');
32
+ if (onPath) {
33
+ try {
34
+ if (realpathSync(onPath) === realScript)
35
+ return 'bonsai';
36
+ }
37
+ catch {
38
+ // onPath didn't resolve to a real file (stale PATH entry) — fall through to the absolute path.
39
+ }
40
+ }
41
+ return `node "${realScript}"`;
42
+ }
@@ -0,0 +1,37 @@
1
+ /** The SessionStart hook Bonsai wants installed. */
2
+ export interface SessionStartHookSpec {
3
+ /** Hook event name, e.g. `"SessionStart"`. */
4
+ eventName: string;
5
+ matcher: string;
6
+ /** Full command to run (already resolved to a PATH-verified name or absolute path). */
7
+ command: string;
8
+ timeout: number;
9
+ /**
10
+ * Tag stamped on the installed handler (e.g. `"bonsai"`) so a later run can find *this exact*
11
+ * entry to repair or leave alone, without guessing from `command` text — a user's own hook could
12
+ * otherwise end in the same word Bonsai's command does (e.g. a wrapped `... && bonsai context`)
13
+ * and get silently claimed and overwritten.
14
+ */
15
+ managedBy: string;
16
+ }
17
+ /** Outcome of planning a hook-file install: what changed, and the file content to persist. */
18
+ export interface HookInstallPlan {
19
+ status: 'installed' | 'repaired' | 'unchanged';
20
+ content: string;
21
+ }
22
+ /** Thrown when an existing hook file can't be safely merged into (not JSON, or not an object). */
23
+ export declare class InvalidHookFileError extends Error {
24
+ }
25
+ /**
26
+ * Plans installing or repairing a Bonsai-managed `SessionStart` hook entry inside an existing (or
27
+ * not-yet-created) hook-config file, without touching any other hooks already in that file (AXI
28
+ * rules: explicit opt-in install, path repair, idempotent). Pure — callers own reading the
29
+ * existing file and persisting `content` (see `setup.ts`).
30
+ *
31
+ * - No existing Bonsai entry for this event → append a new matcher group (`installed`).
32
+ * - An existing Bonsai entry with a stale `command` → update it in place (`repaired`).
33
+ * - An existing Bonsai entry with the same `command` → no-op, original bytes returned (`unchanged`).
34
+ *
35
+ * @throws {InvalidHookFileError} When `existingContent` isn't a mergeable JSON object.
36
+ */
37
+ export declare function planSessionStartHookInstall(existingContent: string | null, spec: SessionStartHookSpec): HookInstallPlan;
@@ -0,0 +1,80 @@
1
+ /** Thrown when an existing hook file can't be safely merged into (not JSON, or not an object). */
2
+ export class InvalidHookFileError extends Error {
3
+ }
4
+ function isPlainObject(value) {
5
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
6
+ }
7
+ function isBonsaiHandler(handler, managedBy) {
8
+ return isPlainObject(handler) && handler.managedBy === managedBy;
9
+ }
10
+ function serialize(root) {
11
+ return `${JSON.stringify(root, null, 2)}\n`;
12
+ }
13
+ /**
14
+ * Parses an existing hook-file's content into its root JSON object, or `{}` when the file doesn't
15
+ * exist yet. Deliberately refuses (rather than silently discarding) content that isn't a JSON
16
+ * object — the file may carry a user's own unrelated hooks, and Bonsai must never clobber content
17
+ * it can't understand.
18
+ *
19
+ * @throws {InvalidHookFileError} When `existingContent` is set but isn't a JSON object.
20
+ */
21
+ function parseHookFileRoot(existingContent) {
22
+ if (existingContent === null || existingContent.trim() === '')
23
+ return {};
24
+ let parsed;
25
+ try {
26
+ parsed = JSON.parse(existingContent);
27
+ }
28
+ catch (err) {
29
+ throw new InvalidHookFileError(`not valid JSON: ${err.message}`);
30
+ }
31
+ if (!isPlainObject(parsed)) {
32
+ throw new InvalidHookFileError('expected a JSON object at the top level');
33
+ }
34
+ return parsed;
35
+ }
36
+ /**
37
+ * Plans installing or repairing a Bonsai-managed `SessionStart` hook entry inside an existing (or
38
+ * not-yet-created) hook-config file, without touching any other hooks already in that file (AXI
39
+ * rules: explicit opt-in install, path repair, idempotent). Pure — callers own reading the
40
+ * existing file and persisting `content` (see `setup.ts`).
41
+ *
42
+ * - No existing Bonsai entry for this event → append a new matcher group (`installed`).
43
+ * - An existing Bonsai entry with a stale `command` → update it in place (`repaired`).
44
+ * - An existing Bonsai entry with the same `command` → no-op, original bytes returned (`unchanged`).
45
+ *
46
+ * @throws {InvalidHookFileError} When `existingContent` isn't a mergeable JSON object.
47
+ */
48
+ export function planSessionStartHookInstall(existingContent, spec) {
49
+ const root = parseHookFileRoot(existingContent);
50
+ const hooks = isPlainObject(root.hooks) ? { ...root.hooks } : {};
51
+ const eventGroups = Array.isArray(hooks[spec.eventName])
52
+ ? [...hooks[spec.eventName]]
53
+ : [];
54
+ for (const group of eventGroups) {
55
+ if (!isPlainObject(group) || !Array.isArray(group.hooks))
56
+ continue;
57
+ const index = group.hooks.findIndex((handler) => isBonsaiHandler(handler, spec.managedBy));
58
+ if (index === -1)
59
+ continue;
60
+ const existingHandler = group.hooks[index];
61
+ if (existingHandler.command === spec.command) {
62
+ // Byte-identical no-op: return the original content rather than a reserialized copy, so a
63
+ // repeated install never churns the file's formatting.
64
+ return { status: 'unchanged', content: existingContent ?? serialize(root) };
65
+ }
66
+ group.hooks[index] = { ...existingHandler, command: spec.command };
67
+ hooks[spec.eventName] = eventGroups;
68
+ root.hooks = hooks;
69
+ return { status: 'repaired', content: serialize(root) };
70
+ }
71
+ eventGroups.push({
72
+ matcher: spec.matcher,
73
+ hooks: [
74
+ { type: 'command', command: spec.command, timeout: spec.timeout, managedBy: spec.managedBy },
75
+ ],
76
+ });
77
+ hooks[spec.eventName] = eventGroups;
78
+ root.hooks = hooks;
79
+ return { status: 'installed', content: serialize(root) };
80
+ }
@@ -1,27 +1,39 @@
1
1
  /** Display label when an artifact has no topic — not a filterable topic value. */
2
2
  export declare const NO_TOPIC_LABEL = "(no topic)";
3
+ /**
4
+ * Collapse a leading home-directory prefix to `~`, matching how shells and tools like `git`
5
+ * display paths. Used for the home-view "bin:" identity line (AXI principle 10) so the executable
6
+ * path stays readable instead of a long absolute path.
7
+ */
8
+ export declare function collapseHomeDir(path: string, homeDir: string): string;
9
+ export declare const CONTROL_CHAR_PATTERN: RegExp;
10
+ /**
11
+ * Strip ANSI escape codes and other control characters from a cached metadata string before it
12
+ * reaches a human-readable terminal line. Fields like `topic` and section headings can originate
13
+ * from fetched web content or agent-supplied notes — both untrusted per the repo's trust-boundary
14
+ * rules — so a value containing raw escape bytes must never be echoed as-is to a TTY. Newlines and
15
+ * tabs collapse to a single space rather than being stripped outright, so injected content can't
16
+ * fake extra output lines while still being removed. JSON output is unaffected: `JSON.stringify`
17
+ * already escapes control characters, so this only needs to run on the human-readable path.
18
+ */
19
+ export declare function sanitizeForTerminal(value: string): string;
3
20
  /**
4
21
  * Pick the singular or plural noun for a count so human-readable output stays grammatical
5
22
  * ("1 entry" / "2 entries"). English has irregular plurals, so the plural is passed explicitly
6
23
  * rather than derived.
7
24
  */
8
25
  export declare function pluralize(count: number, singular: string, plural: string): string;
9
- /** How a command labels a result listing, so its heading and truncation notice stay consistent. */
26
+ /** Shared "Tip: " prefix for contextual next-step suggestions in human-mode output. */
27
+ export declare function formatTip(message: string): string;
28
+ /** How a command labels a result listing, so its heading stays consistent. */
10
29
  export interface ResultListLabels {
11
30
  /** Noun phrase after the count, e.g. "cached research"; the "entry/entries" plural is appended. */
12
31
  noun: string;
13
32
  /** Ordering word shown when truncated: "first" (recency order) or "top" (ranked order). */
14
33
  order: 'first' | 'top';
15
- /** The command's --limit ceiling, surfaced in the truncation hint. */
16
- maxLimit: number;
17
34
  }
18
35
  /** Heading for a result listing; notes truncation inline when more matched than are shown. */
19
36
  export declare function resultListHeading(total: number, shown: number, labels: ResultListLabels): string;
20
- /**
21
- * One-line truncation notice for stderr, or null when nothing was cut. Available for human-mode
22
- * tips; `list --json` surfaces truncation via the envelope `truncation` field instead (#91).
23
- */
24
- export declare function truncationNotice(total: number, shown: number, labels: ResultListLabels): string | null;
25
37
  /** Max edit distance for a plausible typo; scales with input length. */
26
38
  export declare function maxFuzzyDistance(input: string): number;
27
39
  /**
package/dist/lib/text.js CHANGED
@@ -1,5 +1,33 @@
1
1
  /** Display label when an artifact has no topic — not a filterable topic value. */
2
2
  export const NO_TOPIC_LABEL = '(no topic)';
3
+ /**
4
+ * Collapse a leading home-directory prefix to `~`, matching how shells and tools like `git`
5
+ * display paths. Used for the home-view "bin:" identity line (AXI principle 10) so the executable
6
+ * path stays readable instead of a long absolute path.
7
+ */
8
+ export function collapseHomeDir(path, homeDir) {
9
+ if (homeDir && (path === homeDir || path.startsWith(`${homeDir}/`))) {
10
+ return `~${path.slice(homeDir.length)}`;
11
+ }
12
+ return path;
13
+ }
14
+ // Unicode "Control" category (C0 + C1) covers ANSI/terminal escape sequences: the ESC control
15
+ // character that starts them is a member, so stripping this category is what keeps injected
16
+ // control bytes from reaching a real terminal. Exported so `prompt-injection.ts`'s
17
+ // multi-line-content variant (which preserves tab/newline/CR) shares this one definition.
18
+ export const CONTROL_CHAR_PATTERN = /\p{Cc}/gu;
19
+ /**
20
+ * Strip ANSI escape codes and other control characters from a cached metadata string before it
21
+ * reaches a human-readable terminal line. Fields like `topic` and section headings can originate
22
+ * from fetched web content or agent-supplied notes — both untrusted per the repo's trust-boundary
23
+ * rules — so a value containing raw escape bytes must never be echoed as-is to a TTY. Newlines and
24
+ * tabs collapse to a single space rather than being stripped outright, so injected content can't
25
+ * fake extra output lines while still being removed. JSON output is unaffected: `JSON.stringify`
26
+ * already escapes control characters, so this only needs to run on the human-readable path.
27
+ */
28
+ export function sanitizeForTerminal(value) {
29
+ return value.replace(/[\t\n\r]/g, ' ').replace(CONTROL_CHAR_PATTERN, '');
30
+ }
3
31
  /**
4
32
  * Pick the singular or plural noun for a count so human-readable output stays grammatical
5
33
  * ("1 entry" / "2 entries"). English has irregular plurals, so the plural is passed explicitly
@@ -8,6 +36,10 @@ export const NO_TOPIC_LABEL = '(no topic)';
8
36
  export function pluralize(count, singular, plural) {
9
37
  return count === 1 ? singular : plural;
10
38
  }
39
+ /** Shared "Tip: " prefix for contextual next-step suggestions in human-mode output. */
40
+ export function formatTip(message) {
41
+ return `Tip: ${message}`;
42
+ }
11
43
  /** Heading for a result listing; notes truncation inline when more matched than are shown. */
12
44
  export function resultListHeading(total, shown, labels) {
13
45
  const noun = `${labels.noun} ${pluralize(total, 'entry', 'entries')}`;
@@ -15,15 +47,6 @@ export function resultListHeading(total, shown, labels) {
15
47
  ? `Found ${total} ${noun} (showing ${labels.order} ${shown}; raise --limit to see more):`
16
48
  : `Found ${total} ${noun}:`;
17
49
  }
18
- /**
19
- * One-line truncation notice for stderr, or null when nothing was cut. Available for human-mode
20
- * tips; `list --json` surfaces truncation via the envelope `truncation` field instead (#91).
21
- */
22
- export function truncationNotice(total, shown, labels) {
23
- if (total <= shown)
24
- return null;
25
- return `${total} entries matched; returning the ${labels.order} ${shown}. Raise --limit (max ${labels.maxLimit}) to see more.`;
26
- }
27
50
  /** Max edit distance for a plausible typo; scales with input length. */
28
51
  export function maxFuzzyDistance(input) {
29
52
  if (input.length <= 3)