@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.
- package/README.md +25 -2
- package/agents/agents/official-docs-researcher.agent.md +102 -0
- package/agents/agents/salesforce-docs-researcher.agent.md +17 -15
- package/agents/hooks/README.md +18 -2
- package/agents/hooks/claude-code/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/codex/hooks/bonsai-user-prompt-context.sh +1 -1
- package/agents/hooks/codex/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/cursor/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/github-copilot/bonsai-web-fetch.sh +1 -1
- package/agents/instructions/salesforce-research.instructions.md +5 -4
- package/agents/instructions/web-research.instructions.md +45 -0
- package/agents/skills/web-research/SKILL.md +139 -0
- package/bin/cli.mjs +7 -4
- package/dist/base-command.d.ts +42 -0
- package/dist/base-command.js +106 -6
- package/dist/commands/config/base.d.ts +2 -7
- package/dist/commands/config/base.js +4 -17
- package/dist/commands/config/get.js +2 -2
- package/dist/commands/config/list.js +2 -2
- package/dist/commands/config/set.js +2 -2
- package/dist/commands/config/unset.js +2 -2
- package/dist/commands/context.d.ts +20 -0
- package/dist/commands/context.js +65 -0
- package/dist/commands/fetch.js +4 -0
- package/dist/commands/inspect.js +11 -2
- package/dist/commands/list.d.ts +23 -11
- package/dist/commands/list.js +110 -121
- package/dist/commands/prune.d.ts +2 -0
- package/dist/commands/prune.js +35 -4
- package/dist/commands/search.d.ts +43 -0
- package/dist/commands/search.js +233 -0
- package/dist/commands/setup.d.ts +27 -0
- package/dist/commands/setup.js +148 -0
- package/dist/commands/status.js +10 -0
- package/dist/commands.d.ts +6 -0
- package/dist/commands.js +6 -0
- package/dist/hooks/command-not-found/suggest.d.ts +1 -1
- package/dist/hooks/command-not-found/suggest.js +12 -2
- package/dist/lib/argv.d.ts +5 -0
- package/dist/lib/argv.js +30 -5
- package/dist/lib/cli-flag-manifest.d.ts +7 -0
- package/dist/lib/cli-flag-manifest.js +7 -0
- package/dist/lib/cli-presentation.d.ts +25 -4
- package/dist/lib/cli-presentation.js +34 -4
- package/dist/lib/cli-result-types.d.ts +84 -1
- package/dist/lib/color.d.ts +2 -0
- package/dist/lib/color.js +6 -0
- package/dist/lib/common-metadata-filter-flags.d.ts +14 -0
- package/dist/lib/common-metadata-filter-flags.js +37 -0
- package/dist/lib/config/index.d.ts +2 -2
- package/dist/lib/config/index.js +2 -2
- package/dist/lib/config/io.d.ts +9 -0
- package/dist/lib/config/io.js +71 -21
- package/dist/lib/config/schema.d.ts +2 -0
- package/dist/lib/config/schema.js +7 -0
- package/dist/lib/envelope.js +11 -4
- package/dist/lib/next-command.d.ts +15 -0
- package/dist/lib/next-command.js +40 -0
- package/dist/lib/prune-flags.d.ts +2 -0
- package/dist/lib/prune-flags.js +18 -6
- package/dist/lib/research/artifact-index.js +7 -1
- package/dist/lib/research/artifact.js +20 -2
- package/dist/lib/research/browser.d.ts +12 -0
- package/dist/lib/research/browser.js +63 -8
- package/dist/lib/research/cli-io.d.ts +7 -0
- package/dist/lib/research/context-dashboard.d.ts +19 -0
- package/dist/lib/research/context-dashboard.js +43 -0
- package/dist/lib/research/fetch-command-service.js +90 -23
- package/dist/lib/research/fetch-result.d.ts +1 -0
- package/dist/lib/research/fetch-result.js +4 -0
- package/dist/lib/research/fetcher.js +3 -1
- package/dist/lib/research/import-command-service.js +22 -1
- package/dist/lib/research/list-row.d.ts +15 -0
- package/dist/lib/research/list-row.js +31 -0
- package/dist/lib/research/metadata-filters.d.ts +39 -0
- package/dist/lib/research/metadata-filters.js +71 -0
- package/dist/lib/research/metadata-flags.d.ts +34 -0
- package/dist/lib/research/metadata-flags.js +45 -0
- package/dist/lib/research/prompt-injection.d.ts +2 -1
- package/dist/lib/research/prompt-injection.js +18 -2
- package/dist/lib/research/resolve-target.d.ts +2 -0
- package/dist/lib/research/resolve-target.js +1 -1
- package/dist/lib/research/schema.d.ts +2 -0
- package/dist/lib/research/schema.js +4 -0
- package/dist/lib/research/search-match.d.ts +39 -0
- package/dist/lib/research/search-match.js +134 -0
- package/dist/lib/research/storage.d.ts +4 -7
- package/dist/lib/research/storage.js +26 -25
- package/dist/lib/scope-flags.d.ts +19 -0
- package/dist/lib/scope-flags.js +27 -0
- package/dist/lib/setup/agents.d.ts +16 -0
- package/dist/lib/setup/agents.js +20 -0
- package/dist/lib/setup/bin-command.d.ts +14 -0
- package/dist/lib/setup/bin-command.js +42 -0
- package/dist/lib/setup/hook-file-io.d.ts +37 -0
- package/dist/lib/setup/hook-file-io.js +80 -0
- package/dist/lib/text.d.ts +20 -8
- package/dist/lib/text.js +32 -9
- package/oclif.manifest.json +406 -13
- 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
|
+
}
|
package/dist/lib/text.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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)
|