@taurgis/bonsai 3.2.0 → 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 (173) hide show
  1. package/README.md +9 -6
  2. package/agents/agents/salesforce-docs-researcher.agent.md +17 -4
  3. package/agents/instructions/salesforce-research.instructions.md +2 -1
  4. package/bin/cli.mjs +13 -18
  5. package/dist/base-command.d.ts +59 -12
  6. package/dist/base-command.js +179 -51
  7. package/dist/commands/config/base.js +12 -2
  8. package/dist/commands/config/get.js +8 -12
  9. package/dist/commands/config/index.js +2 -2
  10. package/dist/commands/config/list.js +11 -17
  11. package/dist/commands/config/set.d.ts +2 -1
  12. package/dist/commands/config/set.js +35 -28
  13. package/dist/commands/config/unset.d.ts +2 -1
  14. package/dist/commands/config/unset.js +26 -25
  15. package/dist/commands/fetch.d.ts +7 -15
  16. package/dist/commands/fetch.js +61 -401
  17. package/dist/commands/import.d.ts +4 -9
  18. package/dist/commands/import.js +56 -284
  19. package/dist/commands/inspect.d.ts +7 -1
  20. package/dist/commands/inspect.js +112 -52
  21. package/dist/commands/list.d.ts +18 -3
  22. package/dist/commands/list.js +121 -46
  23. package/dist/commands/prune.d.ts +6 -1
  24. package/dist/commands/prune.js +89 -78
  25. package/dist/commands/status.d.ts +3 -2
  26. package/dist/commands/status.js +62 -79
  27. package/dist/hooks/command-not-found/suggest.d.ts +2 -1
  28. package/dist/hooks/command-not-found/suggest.js +78 -41
  29. package/dist/lib/argv.d.ts +29 -3
  30. package/dist/lib/argv.js +147 -54
  31. package/dist/lib/batch.d.ts +41 -0
  32. package/dist/lib/batch.js +39 -0
  33. package/dist/lib/cache-view.d.ts +21 -0
  34. package/dist/lib/cache-view.js +30 -0
  35. package/dist/lib/cli-emit.d.ts +28 -0
  36. package/dist/lib/cli-emit.js +44 -0
  37. package/dist/lib/cli-error-policy.d.ts +57 -0
  38. package/dist/lib/cli-error-policy.js +74 -0
  39. package/dist/lib/cli-flag-manifest.d.ts +12 -0
  40. package/dist/lib/cli-flag-manifest.js +15 -0
  41. package/dist/lib/cli-presentation.d.ts +41 -0
  42. package/dist/lib/cli-presentation.js +53 -0
  43. package/dist/lib/cli-result-types.d.ts +112 -0
  44. package/dist/lib/cli-result-types.js +2 -0
  45. package/dist/lib/config/index.d.ts +23 -5
  46. package/dist/lib/config/index.js +60 -5
  47. package/dist/lib/config/io.d.ts +9 -0
  48. package/dist/lib/config/io.js +71 -21
  49. package/dist/lib/config/schema.d.ts +2 -0
  50. package/dist/lib/config/schema.js +7 -0
  51. package/dist/lib/config-persist.d.ts +28 -0
  52. package/dist/lib/config-persist.js +29 -0
  53. package/dist/lib/envelope.d.ts +41 -3
  54. package/dist/lib/envelope.js +154 -4
  55. package/dist/lib/help-preflight.d.ts +5 -8
  56. package/dist/lib/help-preflight.js +13 -32
  57. package/dist/lib/json-meta.d.ts +0 -2
  58. package/dist/lib/json-meta.js +21 -25
  59. package/dist/lib/limit-flag.d.ts +11 -1
  60. package/dist/lib/limit-flag.js +19 -6
  61. package/dist/lib/parse-error-ux.d.ts +5 -0
  62. package/dist/lib/parse-error-ux.js +73 -0
  63. package/dist/lib/prune-flags.d.ts +30 -0
  64. package/dist/lib/prune-flags.js +86 -0
  65. package/dist/lib/research/artifact-index.d.ts +11 -2
  66. package/dist/lib/research/artifact-index.js +14 -5
  67. package/dist/lib/research/artifact.js +20 -2
  68. package/dist/lib/research/browser.d.ts +53 -2
  69. package/dist/lib/research/browser.js +202 -58
  70. package/dist/lib/research/capture.d.ts +24 -2
  71. package/dist/lib/research/capture.js +24 -4
  72. package/dist/lib/research/cli-io.d.ts +40 -0
  73. package/dist/lib/research/cli-io.js +27 -0
  74. package/dist/lib/research/compress.d.ts +7 -0
  75. package/dist/lib/research/compress.js +7 -3
  76. package/dist/lib/research/docs/capabilities.d.ts +15 -0
  77. package/dist/lib/research/docs/capabilities.js +5 -0
  78. package/dist/lib/research/docs/clean-dom.d.ts +2 -0
  79. package/dist/lib/research/docs/clean-dom.js +2 -0
  80. package/dist/lib/research/docs/code-blocks.d.ts +2 -0
  81. package/dist/lib/research/docs/code-blocks.js +2 -0
  82. package/dist/lib/research/docs/detect.d.ts +4 -0
  83. package/dist/lib/research/docs/detect.js +4 -0
  84. package/dist/lib/research/docs/machine-readable.d.ts +18 -0
  85. package/dist/lib/research/docs/machine-readable.js +10 -0
  86. package/dist/lib/research/docs/markdown-source.d.ts +20 -0
  87. package/dist/lib/research/docs/markdown-source.js +19 -2
  88. package/dist/lib/research/docs/quality-gates.d.ts +5 -0
  89. package/dist/lib/research/docs/quality-gates.js +4 -0
  90. package/dist/lib/research/docs/rsc.d.ts +9 -2
  91. package/dist/lib/research/docs/rsc.js +5 -2
  92. package/dist/lib/research/docs/section-artifacts.d.ts +12 -0
  93. package/dist/lib/research/docs/section-artifacts.js +12 -0
  94. package/dist/lib/research/docs/sections.d.ts +18 -0
  95. package/dist/lib/research/docs/sections.js +17 -1
  96. package/dist/lib/research/docs/source-map.d.ts +28 -0
  97. package/dist/lib/research/docs/source-map.js +27 -0
  98. package/dist/lib/research/docs/validate.d.ts +35 -2
  99. package/dist/lib/research/docs/validate.js +47 -2
  100. package/dist/lib/research/extract.d.ts +16 -0
  101. package/dist/lib/research/extract.js +12 -1
  102. package/dist/lib/research/fetch-command-service.d.ts +36 -0
  103. package/dist/lib/research/fetch-command-service.js +367 -0
  104. package/dist/lib/research/fetch-result.d.ts +114 -0
  105. package/dist/lib/research/fetch-result.js +212 -0
  106. package/dist/lib/research/fetcher.d.ts +46 -8
  107. package/dist/lib/research/fetcher.js +61 -31
  108. package/dist/lib/research/freshness.d.ts +54 -11
  109. package/dist/lib/research/freshness.js +91 -31
  110. package/dist/lib/research/import-command-service.d.ts +56 -0
  111. package/dist/lib/research/import-command-service.js +402 -0
  112. package/dist/lib/research/keywords.js +5 -1
  113. package/dist/lib/research/markdown.d.ts +12 -1
  114. package/dist/lib/research/markdown.js +13 -5
  115. package/dist/lib/research/metadata-filters.d.ts +18 -0
  116. package/dist/lib/research/metadata-filters.js +37 -0
  117. package/dist/lib/research/metadata-flags.d.ts +34 -0
  118. package/dist/lib/research/metadata-flags.js +45 -0
  119. package/dist/lib/research/persist-artifact.d.ts +23 -0
  120. package/dist/lib/research/persist-artifact.js +27 -0
  121. package/dist/lib/research/prompt-injection.d.ts +9 -0
  122. package/dist/lib/research/prompt-injection.js +59 -14
  123. package/dist/lib/research/proxy.d.ts +14 -8
  124. package/dist/lib/research/proxy.js +14 -10
  125. package/dist/lib/research/resolve-target.d.ts +2 -0
  126. package/dist/lib/research/resolve-target.js +1 -1
  127. package/dist/lib/research/revalidate.d.ts +49 -20
  128. package/dist/lib/research/revalidate.js +80 -15
  129. package/dist/lib/research/schema.d.ts +7 -0
  130. package/dist/lib/research/schema.js +2 -0
  131. package/dist/lib/research/secret-scan.d.ts +6 -0
  132. package/dist/lib/research/secret-scan.js +8 -0
  133. package/dist/lib/research/secure-write.d.ts +4 -0
  134. package/dist/lib/research/secure-write.js +31 -2
  135. package/dist/lib/research/storage.d.ts +9 -8
  136. package/dist/lib/research/storage.js +34 -28
  137. package/dist/lib/research/summarize.d.ts +15 -0
  138. package/dist/lib/research/summarize.js +11 -0
  139. package/dist/lib/research/url.d.ts +9 -6
  140. package/dist/lib/research/url.js +33 -21
  141. package/dist/lib/text.d.ts +19 -8
  142. package/dist/lib/text.js +32 -9
  143. package/dist/lib/value-flags.d.ts +14 -0
  144. package/dist/lib/value-flags.js +28 -0
  145. package/dist/sites/coveo.d.ts +10 -0
  146. package/dist/sites/coveo.js +10 -0
  147. package/dist/sites/index.d.ts +16 -0
  148. package/dist/sites/index.js +16 -4
  149. package/dist/sites/markdown-twin.d.ts +31 -0
  150. package/dist/sites/markdown-twin.js +56 -0
  151. package/dist/sites/salesforce/fetch-page.d.ts +8 -2
  152. package/dist/sites/salesforce/fetch-page.js +14 -3
  153. package/dist/sites/salesforce/help-tooling-markdown.d.ts +30 -0
  154. package/dist/sites/salesforce/help-tooling-markdown.js +74 -0
  155. package/dist/sites/salesforce/index.d.ts +1 -0
  156. package/dist/sites/salesforce/index.js +1 -0
  157. package/dist/sites/salesforce-developer/fetch-page.d.ts +6 -1
  158. package/dist/sites/salesforce-developer/fetch-page.js +6 -1
  159. package/dist/sites/salesforce-developer/index.d.ts +1 -0
  160. package/dist/sites/salesforce-developer/index.js +1 -0
  161. package/dist/sites/salesforce-developer/route-markdown.d.ts +18 -6
  162. package/dist/sites/salesforce-developer/route-markdown.js +29 -67
  163. package/dist/sites/salesforce-doc-fetch.d.ts +39 -5
  164. package/dist/sites/salesforce-doc-fetch.js +52 -7
  165. package/dist/sites/salesforce-dom-probe.d.ts +20 -2
  166. package/dist/sites/salesforce-dom-probe.js +20 -2
  167. package/dist/sites/types.d.ts +14 -8
  168. package/dist/sites/types.js +3 -0
  169. package/oclif.manifest.json +216 -102
  170. package/package.json +20 -7
  171. package/agents/agents/official-docs-researcher.agent.md +0 -87
  172. package/agents/instructions/web-research.instructions.md +0 -44
  173. package/agents/skills/web-research/SKILL.md +0 -118
@@ -0,0 +1,53 @@
1
+ import { colors } from './color.js';
2
+ import { MAX_TAG_LENGTH, MAX_TOPIC_LENGTH } from './research/metadata-flags.js';
3
+ const HUMAN_LABEL_WIDTH = 25;
4
+ // Repeated verbatim across several flag descriptions; single-use wording lives inline below.
5
+ const DURATION_EXAMPLES = 'e.g. "2h", "7d", "6m"';
6
+ const SECRET_STORAGE_NOTE = 'secrets always stored globally';
7
+ /** Shared flag/help copy so identical flags read identically across commands. */
8
+ export const CLI_FLAG_DESCRIPTIONS = {
9
+ freshnessTierPolicy: 'freshness tier policy',
10
+ statusFreshnessTierPolicy: "freshness tier policy to evaluate; when omitted, uses the cached entry's own tier",
11
+ fetchTtl: `TTL duration for freshness (${DURATION_EXAMPLES})`,
12
+ importTtl: `TTL duration for imported note freshness (${DURATION_EXAMPLES})`,
13
+ statusTtl: `TTL duration to evaluate freshness (${DURATION_EXAMPLES})`,
14
+ maxAge: `maximum cache age to accept (${DURATION_EXAMPLES})`,
15
+ fetchStorage: `override where this result is cached (${SECRET_STORAGE_NOTE})`,
16
+ importStorage: `override where this note is cached (${SECRET_STORAGE_NOTE})`,
17
+ fetchTopic: `main research topic for metadata (max ${MAX_TOPIC_LENGTH} chars)`,
18
+ importTopic: `main topic for this research note (max ${MAX_TOPIC_LENGTH} chars)`,
19
+ filterTopic: 'exact topic (case-insensitive)',
20
+ fetchTags: `taxonomic tags for this research (can be repeated, max ${MAX_TAG_LENGTH} chars each)`,
21
+ importTags: `taxonomic tags (can be repeated, max ${MAX_TAG_LENGTH} chars each)`,
22
+ filterTags: 'tags to require (must match all)',
23
+ format: 'output format',
24
+ readOnly: '(alias: --plan) block filesystem writes/deletes; network fetches still run; also honored via BONSAI_READ_ONLY/BONSAI_PLAN_MODE',
25
+ toon: 'emit the same envelope as --json, encoded as TOON (fewer tokens); cannot be combined with --json',
26
+ sourceUrlGlob: 'source URL glob (case-insensitive, supports * wildcard)',
27
+ listArtifactType: 'artifact type; section children are omitted from list - use inspect to see them',
28
+ pruneArtifactType: 'artifact type to prune, including section children',
29
+ listFull: 'include every metadata field (cache key, path, artifact type, tags, capture method, quality notes, timestamps); default output is a minimal row (source URLs, topic, freshness, token estimate)',
30
+ };
31
+ function formatHumanLabel(label) {
32
+ const text = `${label}:`;
33
+ return colors.cyan(text.padEnd(Math.max(HUMAN_LABEL_WIDTH, text.length)));
34
+ }
35
+ /**
36
+ * Format one cyan-padded label/value line for human CLI output.
37
+ *
38
+ * @param label - Field label (without trailing colon).
39
+ * @param value - Field value.
40
+ * @returns A single formatted line.
41
+ */
42
+ export function formatHumanField(label, value) {
43
+ return `${formatHumanLabel(label)} ${value}`;
44
+ }
45
+ /**
46
+ * Format many label/value lines for human CLI output.
47
+ *
48
+ * @param fields - Label/value pairs.
49
+ * @returns Formatted lines in the same order.
50
+ */
51
+ export function formatHumanFields(fields) {
52
+ return fields.map(([label, value]) => formatHumanField(label, value));
53
+ }
@@ -0,0 +1,112 @@
1
+ /** Shared machine-facing write/result shapes for CLI JSON envelopes. */
2
+ import type { ArtifactType, CaptureMethod, ResearchArtifactMetadata, TokenEstimate } from './research/schema.js';
3
+ /** Result of `config set` / `config unset` for the JSON envelope. */
4
+ export interface ConfigWriteResult {
5
+ key: string;
6
+ scope: 'user' | 'project';
7
+ dryRun: boolean;
8
+ status: 'set' | 'would_set' | 'unset' | 'would_unset';
9
+ value?: unknown;
10
+ }
11
+ /** One prune candidate row before delete/preview. */
12
+ export interface PruneCandidate {
13
+ cacheKey: string;
14
+ path: string;
15
+ topic: string | null;
16
+ url: string;
17
+ }
18
+ /** Result of `prune` for the JSON envelope. */
19
+ export interface PruneWriteResult {
20
+ dryRun: boolean;
21
+ status: 'pruned' | 'would_prune';
22
+ wouldPruneCount: number;
23
+ prunedCount: number;
24
+ candidateCount: number;
25
+ files: Array<{
26
+ cacheKey: string;
27
+ path: string;
28
+ }>;
29
+ }
30
+ /** Cache lookup outcome reported on fetch results. */
31
+ export type CacheHitStatus = 'hit' | 'miss' | 'stale' | 'refreshed' | 'revalidated';
32
+ /** Freshness tier for a cached entry; `none` means no entry exists. */
33
+ export type FreshnessState = 'fresh' | 'stale_grace' | 'stale_expired' | 'none';
34
+ /** Freshness values that appear on list rows (an entry always exists). */
35
+ export type ListFreshness = Exclude<FreshnessState, 'none'>;
36
+ /** One row in `list --json` output when `--full` is passed. */
37
+ export interface ListRow {
38
+ cacheKey: string;
39
+ path: string;
40
+ artifactType: ArtifactType;
41
+ sourceUrls: string[];
42
+ topic: string | null;
43
+ tags: string[];
44
+ freshness: ListFreshness;
45
+ captureMethod: CaptureMethod | null;
46
+ tokenEstimate: TokenEstimate;
47
+ qualityNotes: string[];
48
+ fetchedAt: string | null;
49
+ validatedAt: string | null;
50
+ }
51
+ /**
52
+ * Default `list --json`/`--toon` row: the fields an agent needs to judge relevance and act next
53
+ * (fetch/inspect the source, gauge freshness, budget tokens) without the full metadata dump.
54
+ * Pass `--full` for every field on {@link ListRow}.
55
+ */
56
+ export interface ListRowMinimal {
57
+ sourceUrls: string[];
58
+ topic: string | null;
59
+ freshness: ListFreshness;
60
+ tokenEstimate: TokenEstimate;
61
+ }
62
+ /** Aggregate counts attached to `list`'s envelope so agents skip extra round trips. */
63
+ export interface ListSummary {
64
+ /** Entries matching the given filters, before `--limit` truncation. */
65
+ total: number;
66
+ /** Entries actually returned in `data` (after `--limit`). */
67
+ shown: number;
68
+ limit: number;
69
+ /** True when `total > shown`, i.e. more entries matched than `--limit` allowed through. */
70
+ truncated: boolean;
71
+ /** Explicit signal for `total === 0`, so an empty `data: []` is never ambiguous. */
72
+ empty: boolean;
73
+ byFreshness: Record<ListFreshness, number>;
74
+ }
75
+ /** Section child summary nested under an inspect hit. */
76
+ export interface InspectSectionRow {
77
+ cacheKey: string;
78
+ anchor: string | null;
79
+ headingPath: string | null;
80
+ tokenEstimate: TokenEstimate;
81
+ }
82
+ /**
83
+ * An already-cached artifact (typically a multi-source `research_note`) that lists the requested
84
+ * URL among its own `source_urls`, surfaced on an `inspect` miss so the requested URL isn't
85
+ * mistaken for wholly uncached content when it is really a secondary source of an existing note.
86
+ */
87
+ export interface InspectExistingNoteRow {
88
+ cacheKey: string;
89
+ artifactType: string;
90
+ topic: string | null;
91
+ sourceUrls: string[];
92
+ }
93
+ /** One row in `inspect --json` output. */
94
+ export interface InspectRow {
95
+ cacheKey: string;
96
+ cachePath: string;
97
+ normalizedUrl: string;
98
+ status: 'hit' | 'miss';
99
+ metadata: ResearchArtifactMetadata | null;
100
+ sections: InspectSectionRow[];
101
+ /** Set on a miss when the URL is already a source of a different cached artifact (see above). */
102
+ partOfExistingNote?: InspectExistingNoteRow | null;
103
+ }
104
+ /** One row in `status --json` output. */
105
+ export interface StatusRow {
106
+ cacheKey: string;
107
+ cachePath: string;
108
+ normalizedUrl: string;
109
+ status: 'hit' | 'miss' | 'stale';
110
+ freshness: FreshnessState;
111
+ action: 'would_fetch' | 'would_revalidate' | 'would_return_cached';
112
+ }
@@ -0,0 +1,2 @@
1
+ /** Shared machine-facing write/result shapes for CLI JSON envelopes. */
2
+ export {};
@@ -1,7 +1,7 @@
1
- export { type ConfigValues, type ResolvedConfig, type ConfigKey, type StorageMode, type SummaryLevel, type KeyMeta, SCHEMA_VERSION, STORAGE_MODES, SUMMARY_LEVELS, ALL_KEYS, BUILT_IN_DEFAULTS, KEY_META, isKnownKey, suggestKey, validKeysHint, } from './schema.js';
2
- export { readUserConfig, readProjectConfig, writeUserConfig, writeProjectConfig, USER_CONFIG_FILENAME, PROJECT_CONFIG_FILENAME, } from './io.js';
1
+ export { type ConfigValues, type ResolvedConfig, type ConfigKey, type StorageMode, type SummaryLevel, type KeyMeta, SCHEMA_VERSION, STORAGE_MODES, SUMMARY_LEVELS, ALL_KEYS, BUILT_IN_DEFAULTS, KEY_META, isKnownKey, suggestKey, validKeysHint, keyValuesHint, } from './schema.js';
2
+ export { readUserConfig, readProjectConfig, writeUserConfig, writeProjectConfig, invalidConfigFileWarnings, USER_CONFIG_FILENAME, PROJECT_CONFIG_FILENAME, } from './io.js';
3
3
  export { resolveStorageMode, resolveSummaryLevel, resolveReadOnly, parseEnvStorage, parseEnvSummary, parseEnvBoolean, invalidEnvOverrideWarnings, STORAGE_ENV_VAR, SUMMARY_ENV_VAR, READ_ONLY_ENV_VAR, PLAN_MODE_ENV_VAR, type ResolveStorageInput, type ResolveSummaryInput, type ResolveReadOnlyInput, } from './resolve.js';
4
- import type { ConfigValues, ResolvedConfig, StorageMode, SummaryLevel } from './schema.js';
4
+ import { type ConfigKey, type ConfigValues, type ResolvedConfig, type StorageMode, type SummaryLevel } from './schema.js';
5
5
  /**
6
6
  * Read both config files once and resolve the effective storage mode.
7
7
  * `flag` (a per-invocation `--storage` override) wins over everything else.
@@ -17,7 +17,25 @@ export declare function effectiveConfig(configDir: string | undefined, cwd: stri
17
17
  export type ConfigScope = 'global' | 'local' | 'effective';
18
18
  /**
19
19
  * The config values for a requested scope: the user file (`global`), the project file (`local`),
20
- * or the fully merged result (`effective`). Shared by `config get` and `config list` so the
21
- * scope-selection lives in one place.
20
+ * or the fully merged result (`effective`). Shared by write paths and callers that only need the
21
+ * raw scoped blob (not configured metadata).
22
22
  */
23
23
  export declare function readScopedConfig(scope: ConfigScope, configDir: string | undefined, cwd: string): Partial<ConfigValues>;
24
+ /** One config key resolved against a scope: effective value + whether that scope set it. */
25
+ export interface ConfigEntry {
26
+ key: ConfigKey;
27
+ value: ResolvedConfig[ConfigKey];
28
+ configured: boolean;
29
+ }
30
+ /** Resolve a single key; same semantics as {@link resolveConfigEntries}. */
31
+ export declare function resolveConfigEntry(key: ConfigKey, scope: ConfigScope, configDir: string | undefined, cwd: string, env?: Record<string, string | undefined>): ConfigEntry;
32
+ /**
33
+ * Resolve every config key for a scope. Unset keys fall back to the built-in default while
34
+ * `configured: false` tells agents the value is not pinned beyond that default.
35
+ */
36
+ export declare function resolveConfigEntries(scope: ConfigScope, configDir: string | undefined, cwd: string, env?: Record<string, string | undefined>): ConfigEntry[];
37
+ /**
38
+ * Human display for a resolved entry. Effective scope always has a usable value, so the
39
+ * "(not configured)" suffix is reserved for `--global`/`--local` reads where absence matters.
40
+ */
41
+ export declare function formatConfigEntry(entry: ConfigEntry, scope?: ConfigScope): string;
@@ -1,8 +1,9 @@
1
- export { SCHEMA_VERSION, STORAGE_MODES, SUMMARY_LEVELS, ALL_KEYS, BUILT_IN_DEFAULTS, KEY_META, isKnownKey, suggestKey, validKeysHint, } from './schema.js';
2
- export { readUserConfig, readProjectConfig, writeUserConfig, writeProjectConfig, USER_CONFIG_FILENAME, PROJECT_CONFIG_FILENAME, } from './io.js';
1
+ export { SCHEMA_VERSION, STORAGE_MODES, SUMMARY_LEVELS, ALL_KEYS, BUILT_IN_DEFAULTS, KEY_META, isKnownKey, suggestKey, validKeysHint, keyValuesHint, } from './schema.js';
2
+ export { readUserConfig, readProjectConfig, writeUserConfig, writeProjectConfig, invalidConfigFileWarnings, USER_CONFIG_FILENAME, PROJECT_CONFIG_FILENAME, } from './io.js';
3
3
  export { resolveStorageMode, resolveSummaryLevel, resolveReadOnly, parseEnvStorage, parseEnvSummary, parseEnvBoolean, invalidEnvOverrideWarnings, STORAGE_ENV_VAR, SUMMARY_ENV_VAR, READ_ONLY_ENV_VAR, PLAN_MODE_ENV_VAR, } from './resolve.js';
4
4
  import { readUserConfig, readProjectConfig } from './io.js';
5
- import { resolveStorageMode, resolveSummaryLevel } from './resolve.js';
5
+ import { parseEnvStorage, parseEnvSummary, resolveStorageMode, resolveSummaryLevel, } from './resolve.js';
6
+ import { ALL_KEYS, BUILT_IN_DEFAULTS, KEY_META, } from './schema.js';
6
7
  /**
7
8
  * Read both config files once and resolve the effective storage mode.
8
9
  * `flag` (a per-invocation `--storage` override) wins over everything else.
@@ -36,8 +37,8 @@ export function effectiveConfig(configDir, cwd) {
36
37
  }
37
38
  /**
38
39
  * The config values for a requested scope: the user file (`global`), the project file (`local`),
39
- * or the fully merged result (`effective`). Shared by `config get` and `config list` so the
40
- * scope-selection lives in one place.
40
+ * or the fully merged result (`effective`). Shared by write paths and callers that only need the
41
+ * raw scoped blob (not configured metadata).
41
42
  */
42
43
  export function readScopedConfig(scope, configDir, cwd) {
43
44
  if (scope === 'global')
@@ -46,3 +47,57 @@ export function readScopedConfig(scope, configDir, cwd) {
46
47
  return readProjectConfig(cwd);
47
48
  return effectiveConfig(configDir, cwd);
48
49
  }
50
+ /**
51
+ * Whether any non-default layer pins `key`. For `--global`/`--local`, that is the file itself.
52
+ * For effective, project file, user file, or a valid BONSAI_* env override counts — so a bare
53
+ * default never reports `configured: true` just because `effectiveConfig` always fills values.
54
+ */
55
+ function isKeyConfigured(key, scope, project, user, env) {
56
+ if (scope === 'global')
57
+ return key in user;
58
+ if (scope === 'local')
59
+ return key in project;
60
+ if (key in project || key in user)
61
+ return true;
62
+ if (key === 'storage')
63
+ return parseEnvStorage(env) !== undefined;
64
+ if (key === 'summary')
65
+ return parseEnvSummary(env) !== undefined;
66
+ return false;
67
+ }
68
+ function buildEntry(key, scope, project, user, env) {
69
+ const configured = isKeyConfigured(key, scope, project, user, env);
70
+ if (key === 'storage') {
71
+ const value = scope === 'effective'
72
+ ? resolveStorageMode({ env, projectConfig: project, userConfig: user })
73
+ : ((scope === 'global' ? user : project).storage ?? BUILT_IN_DEFAULTS.storage);
74
+ return { key, value, configured };
75
+ }
76
+ const value = scope === 'effective'
77
+ ? resolveSummaryLevel({ env, projectConfig: project, userConfig: user })
78
+ : ((scope === 'global' ? user : project).summary ?? BUILT_IN_DEFAULTS.summary);
79
+ return { key, value, configured };
80
+ }
81
+ /** Resolve a single key; same semantics as {@link resolveConfigEntries}. */
82
+ export function resolveConfigEntry(key, scope, configDir, cwd, env = process.env) {
83
+ return buildEntry(key, scope, readProjectConfig(cwd), readUserConfig(configDir), env);
84
+ }
85
+ /**
86
+ * Resolve every config key for a scope. Unset keys fall back to the built-in default while
87
+ * `configured: false` tells agents the value is not pinned beyond that default.
88
+ */
89
+ export function resolveConfigEntries(scope, configDir, cwd, env = process.env) {
90
+ const project = readProjectConfig(cwd);
91
+ const user = readUserConfig(configDir);
92
+ return ALL_KEYS.map((key) => buildEntry(key, scope, project, user, env));
93
+ }
94
+ /**
95
+ * Human display for a resolved entry. Effective scope always has a usable value, so the
96
+ * "(not configured)" suffix is reserved for `--global`/`--local` reads where absence matters.
97
+ */
98
+ export function formatConfigEntry(entry, scope = 'local') {
99
+ const formatted = KEY_META[entry.key].format(entry.value);
100
+ if (scope === 'effective' || entry.configured)
101
+ return formatted;
102
+ return `${formatted} (not configured)`;
103
+ }
@@ -4,5 +4,14 @@ export declare const USER_CONFIG_FILENAME = "config.json";
4
4
  export declare const PROJECT_CONFIG_FILENAME = ".bonsai.json";
5
5
  export declare function readUserConfig(configDir: string | undefined): Partial<ConfigValues>;
6
6
  export declare function readProjectConfig(cwd: string): Partial<ConfigValues>;
7
+ /**
8
+ * Warnings for the user and project config files, so a corrupted or hand-edited file that would
9
+ * otherwise silently degrade to `{}` (or drop just the offending key) is never a silent surprise —
10
+ * mirrors {@link invalidEnvOverrideWarnings} for env vars. Empty when both files are missing,
11
+ * unreadable, or valid.
12
+ *
13
+ * @param bin - CLI binary name (`this.config.bin`) for the "fix it" suggestion in a warning.
14
+ */
15
+ export declare function invalidConfigFileWarnings(configDir: string | undefined, cwd: string, bin: string): string[];
7
16
  export declare function writeUserConfig(configDir: string, patch: Partial<ConfigValues>): void;
8
17
  export declare function writeProjectConfig(cwd: string, patch: Partial<ConfigValues>): void;
@@ -5,43 +5,93 @@ import { KEY_META, ALL_KEYS } from './schema.js';
5
5
  /** Global, user-level config lives in the OCLIF config dir; project config sits at the repo root. */
6
6
  export const USER_CONFIG_FILENAME = 'config.json';
7
7
  export const PROJECT_CONFIG_FILENAME = '.bonsai.json';
8
- /** Parse a config file, keeping only known keys with valid values. Invalid input degrades to {}. */
9
- function parseConfigFile(raw) {
8
+ /**
9
+ * Parse a config file, keeping only known keys with valid values. Unparseable JSON, a non-object
10
+ * top level, or an invalid value for a known key degrades that part to "absent" rather than
11
+ * throwing — callers that only need values (`readUserConfig`/`readProjectConfig`) ignore
12
+ * `warnings`; {@link invalidConfigFileWarnings} surfaces them so the degradation is never silent.
13
+ */
14
+ function parseConfigFile(filePath, raw, scopeFlag, bin) {
15
+ let parsed;
10
16
  try {
11
- const parsed = JSON.parse(raw);
12
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
13
- return {};
14
- const obj = parsed;
15
- const result = {};
16
- for (const key of ALL_KEYS) {
17
- const val = obj[key];
18
- if (val !== undefined && KEY_META[key].isValid(val)) {
19
- Object.assign(result, { [key]: val });
20
- }
21
- }
22
- return result;
17
+ parsed = JSON.parse(raw);
23
18
  }
24
19
  catch {
25
- return {};
20
+ return {
21
+ values: {},
22
+ warnings: [
23
+ `Ignoring ${filePath}: not valid JSON. Using the configured value or default instead. ` +
24
+ `Fix the file by hand, or overwrite it with ${bin} config set <key> <value> ${scopeFlag}.`,
25
+ ],
26
+ };
27
+ }
28
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
29
+ return {
30
+ values: {},
31
+ warnings: [
32
+ `Ignoring ${filePath}: expected a JSON object at the top level. Using the configured value or default instead.`,
33
+ ],
34
+ };
35
+ }
36
+ const obj = parsed;
37
+ const values = {};
38
+ const warnings = [];
39
+ for (const key of ALL_KEYS) {
40
+ const val = obj[key];
41
+ if (val === undefined)
42
+ continue;
43
+ if (KEY_META[key].isValid(val)) {
44
+ Object.assign(values, { [key]: val });
45
+ }
46
+ else {
47
+ const validValues = KEY_META[key].values;
48
+ const validValuesHint = validValues ? ` Valid values: ${validValues.join(', ')}.` : '';
49
+ warnings.push(`Ignoring "${key}" in ${filePath}: invalid value ${JSON.stringify(val)}.${validValuesHint} ` +
50
+ `Using the configured value or default instead.`);
51
+ }
26
52
  }
53
+ return { values, warnings };
27
54
  }
28
- function readConfigFile(filePath) {
55
+ // Only the "fix it" suggestion in a warning message names the binary; every value-only caller
56
+ // below (readUserConfig/readProjectConfig, and transitively write*Config) discards warnings
57
+ // entirely, so this default never reaches a user — it's a placeholder for an unused code path,
58
+ // not a claim about the real bin name. invalidConfigFileWarnings passes the real `this.config.bin`.
59
+ const DEFAULT_BIN = 'bonsai';
60
+ function readConfigFile(filePath, scopeFlag, bin = DEFAULT_BIN) {
29
61
  if (!existsSync(filePath))
30
- return {};
62
+ return { values: {}, warnings: [] };
63
+ let raw;
31
64
  try {
32
- return parseConfigFile(readFileSync(filePath, 'utf-8'));
65
+ raw = readFileSync(filePath, 'utf-8');
33
66
  }
34
67
  catch {
35
- return {};
68
+ return { values: {}, warnings: [] };
36
69
  }
70
+ return parseConfigFile(filePath, raw, scopeFlag, bin);
37
71
  }
38
72
  export function readUserConfig(configDir) {
39
73
  if (!configDir)
40
74
  return {};
41
- return readConfigFile(join(configDir, USER_CONFIG_FILENAME));
75
+ return readConfigFile(join(configDir, USER_CONFIG_FILENAME), '--global').values;
42
76
  }
43
77
  export function readProjectConfig(cwd) {
44
- return readConfigFile(join(cwd, PROJECT_CONFIG_FILENAME));
78
+ return readConfigFile(join(cwd, PROJECT_CONFIG_FILENAME), '--local').values;
79
+ }
80
+ /**
81
+ * Warnings for the user and project config files, so a corrupted or hand-edited file that would
82
+ * otherwise silently degrade to `{}` (or drop just the offending key) is never a silent surprise —
83
+ * mirrors {@link invalidEnvOverrideWarnings} for env vars. Empty when both files are missing,
84
+ * unreadable, or valid.
85
+ *
86
+ * @param bin - CLI binary name (`this.config.bin`) for the "fix it" suggestion in a warning.
87
+ */
88
+ export function invalidConfigFileWarnings(configDir, cwd, bin) {
89
+ const warnings = [];
90
+ if (configDir) {
91
+ warnings.push(...readConfigFile(join(configDir, USER_CONFIG_FILENAME), '--global', bin).warnings);
92
+ }
93
+ warnings.push(...readConfigFile(join(cwd, PROJECT_CONFIG_FILENAME), '--local', bin).warnings);
94
+ return warnings;
45
95
  }
46
96
  function writeAtomically(filePath, data) {
47
97
  atomicWriteFile(filePath, JSON.stringify(data, null, 2) + '\n');
@@ -29,3 +29,5 @@ export declare function isKnownKey(key: string): key is ConfigKey;
29
29
  export declare function suggestKey(input: string): ConfigKey | undefined;
30
30
  /** Comma-joined valid key list for error messages, kept in one place. */
31
31
  export declare function validKeysHint(): string;
32
+ /** Valid keys with their accepted values, e.g. "storage (global|project), summary (…)", for `--help` descriptions. */
33
+ export declare function keyValuesHint(): string;
@@ -40,3 +40,10 @@ export function suggestKey(input) {
40
40
  export function validKeysHint() {
41
41
  return ALL_KEYS.join(', ');
42
42
  }
43
+ /** Valid keys with their accepted values, e.g. "storage (global|project), summary (…)", for `--help` descriptions. */
44
+ export function keyValuesHint() {
45
+ return ALL_KEYS.map((key) => {
46
+ const { values } = KEY_META[key];
47
+ return values ? `${key} (${values.join('|')})` : key;
48
+ }).join(', ');
49
+ }
@@ -0,0 +1,28 @@
1
+ import { type ConfigKey, type ConfigValues } from './config/index.js';
2
+ /** Scope for config writes (`user` = XDG config dir; `project` = cwd `.bonsai`). */
3
+ export type ConfigWriteScope = 'user' | 'project';
4
+ /** Successful config persist outcome. */
5
+ export interface ConfigPersistResult {
6
+ ok: true;
7
+ }
8
+ /** Failed config persist outcome with stable code and recovery tips. */
9
+ export interface ConfigPersistFailure {
10
+ ok: false;
11
+ code: 'CONFIG_DIR_UNAVAILABLE';
12
+ message: string;
13
+ suggestions: string[];
14
+ }
15
+ /**
16
+ * Persist a config patch to user or project scope. Pure of oclif — callers map failures to
17
+ * this.error(...). Shared by config set and unset so the write branch cannot drift.
18
+ */
19
+ export declare function persistConfigPatch(opts: {
20
+ scope: ConfigWriteScope;
21
+ cwd: string;
22
+ configDir: string | undefined;
23
+ patch: Partial<ConfigValues>;
24
+ /** Used in failure suggestions (set vs unset). */
25
+ action: 'set' | 'unset';
26
+ bin: string;
27
+ key: ConfigKey;
28
+ }): ConfigPersistResult | ConfigPersistFailure;
@@ -0,0 +1,29 @@
1
+ import { writeUserConfig, writeProjectConfig, } from './config/index.js';
2
+ /**
3
+ * Persist a config patch to user or project scope. Pure of oclif — callers map failures to
4
+ * this.error(...). Shared by config set and unset so the write branch cannot drift.
5
+ */
6
+ export function persistConfigPatch(opts) {
7
+ if (opts.scope === 'project') {
8
+ writeProjectConfig(opts.cwd, opts.patch);
9
+ return { ok: true };
10
+ }
11
+ if (!opts.configDir) {
12
+ const verb = opts.action === 'set' ? 'write' : 'remove';
13
+ const example = opts.action === 'set'
14
+ ? `${opts.bin} config set ${opts.key} <value> --local`
15
+ : `${opts.bin} config unset ${opts.key} --local`;
16
+ return {
17
+ ok: false,
18
+ code: 'CONFIG_DIR_UNAVAILABLE',
19
+ message: `Could not determine user config directory. Use --local to ${verb} project config.`,
20
+ suggestions: [
21
+ opts.action === 'set'
22
+ ? `Write project config instead: ${example}`
23
+ : `Remove project config instead: ${example}`,
24
+ ],
25
+ };
26
+ }
27
+ writeUserConfig(opts.configDir, opts.patch);
28
+ return { ok: true };
29
+ }
@@ -7,8 +7,12 @@ export interface CliErrorShape {
7
7
  }
8
8
  /** Map oclif parse-time errors to stable Bonsai codes when the throw site did not set one. */
9
9
  export declare function stableErrorCodeFrom(err: unknown): string | undefined;
10
- /** Strip oclif's generic help suffix from JSON stderr — agents already have structured codes. */
10
+ /**
11
+ * Strip oclif's generic help suffix and the `Parsing --flag` wrapper so agents and humans see the
12
+ * actionable message we threw (e.g. `Limit must be between 1 and 100.`) instead of the wrapper.
13
+ */
11
14
  export declare function normalizeCliErrorMessage(message: string): string;
15
+ /** Machine-readable envelope fields for `--json` success and error paths. */
12
16
  export interface EnvelopeParts {
13
17
  command: string;
14
18
  ok: boolean;
@@ -25,12 +29,46 @@ export interface EnvelopeParts {
25
29
  * without ANSI or the leading "Error:" prefix (the message is already the primary line).
26
30
  */
27
31
  export declare function formatErrorForJson(err: CliErrorShape): string;
32
+ /** Stable CLIError fields for batch failure rows (status/inspect/fetch). */
33
+ export declare function cliErrorFields(err: {
34
+ code?: string;
35
+ message?: string;
36
+ suggestions?: string[];
37
+ ref?: string;
38
+ }, fallbackCode?: string): {
39
+ code: string;
40
+ message: string;
41
+ suggestions?: string[];
42
+ ref?: string;
43
+ };
44
+ /**
45
+ * CACHE_MISS overlay. Message wording is fixed so status/inspect cannot drift.
46
+ */
47
+ export declare function enrichCacheMissEnvelope(envelope: Record<string, unknown>, data: unknown, bin: string): Record<string, unknown>;
48
+ /**
49
+ * Per-row `.error` overlay for multi-URL batches (fetch, status, inspect).
50
+ * Keeps prior successes in `data` and surfaces the first row's code/message.
51
+ */
52
+ export declare function enrichRowErrorEnvelope(envelope: Record<string, unknown>, data: unknown): Record<string, unknown>;
53
+ /**
54
+ * Partial prune unlink failure: candidates remain in `data`, envelope reports the stable code.
55
+ */
56
+ export declare function enrichPrunePartialEnvelope(envelope: Record<string, unknown>, data: unknown): Record<string, unknown>;
28
57
  /**
29
58
  * Builds the standard Bonsai CLI JSON envelope structure.
30
59
  * This is the single source of truth for the output envelope schema.
31
60
  */
32
61
  export declare function buildEnvelope(parts: EnvelopeParts): Record<string, unknown>;
33
62
  /**
34
- * Formats a JSON envelope to a string with indentation.
63
+ * Usage/not-found style error envelope: message + Code (+ Try this) in `stderr`, stable `code`,
64
+ * and top-level `suggestions` when present. Shared by argv preflight, help-preflight, and the
65
+ * command_not_found hook so those paths cannot drift.
35
66
  */
36
- export declare function formatEnvelope(parts: EnvelopeParts): string;
67
+ export declare function buildCliErrorEnvelope(opts: {
68
+ command: string;
69
+ message: string;
70
+ code: string;
71
+ suggestions?: string[];
72
+ exitCode?: number;
73
+ ref?: string;
74
+ }): Record<string, unknown>;