akm-cli 0.8.0-rc2 → 0.8.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 (295) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +191 -3
  2. package/README.md +22 -6
  3. package/SECURITY.md +93 -0
  4. package/dist/cli/config-migrate.js +144 -0
  5. package/dist/cli/config-validate.js +39 -0
  6. package/dist/cli/confirm.js +73 -0
  7. package/dist/cli/parse-args.js +93 -3
  8. package/dist/cli/shared.js +129 -0
  9. package/dist/cli.js +2141 -1268
  10. package/dist/commands/add-cli.js +279 -0
  11. package/dist/commands/agent-dispatch.js +20 -12
  12. package/dist/commands/agent-support.js +11 -5
  13. package/dist/commands/completions.js +3 -0
  14. package/dist/commands/config-cli.js +129 -517
  15. package/dist/commands/consolidate.js +1533 -144
  16. package/dist/commands/curate.js +44 -3
  17. package/dist/commands/db-cli.js +23 -0
  18. package/dist/commands/distill-promotion-policy.js +5 -3
  19. package/dist/commands/distill.js +906 -100
  20. package/dist/commands/env.js +213 -0
  21. package/dist/commands/eval-cases.js +3 -0
  22. package/dist/commands/events.js +3 -0
  23. package/dist/commands/extract-cli.js +127 -0
  24. package/dist/commands/extract-prompt.js +204 -0
  25. package/dist/commands/extract.js +477 -0
  26. package/dist/commands/feedback-cli.js +331 -0
  27. package/dist/commands/graph.js +260 -5
  28. package/dist/commands/health.js +977 -51
  29. package/dist/commands/help/help-accept.md +6 -3
  30. package/dist/commands/help/help-improve.md +36 -8
  31. package/dist/commands/help/help-proposals.md +7 -4
  32. package/dist/commands/help/help-reject.md +5 -2
  33. package/dist/commands/history.js +51 -16
  34. package/dist/commands/improve-auto-accept.js +97 -0
  35. package/dist/commands/improve-cli.js +236 -0
  36. package/dist/commands/improve-profiles.js +184 -0
  37. package/dist/commands/improve-result-file.js +167 -0
  38. package/dist/commands/improve.js +1725 -332
  39. package/dist/commands/info.js +3 -0
  40. package/dist/commands/init.js +49 -1
  41. package/dist/commands/installed-stashes.js +6 -23
  42. package/dist/commands/knowledge.js +3 -0
  43. package/dist/commands/lint/agent-linter.js +3 -0
  44. package/dist/commands/lint/base-linter.js +199 -5
  45. package/dist/commands/lint/command-linter.js +3 -0
  46. package/dist/commands/lint/default-linter.js +3 -0
  47. package/dist/commands/lint/env-key-rules.js +154 -0
  48. package/dist/commands/lint/index.js +92 -3
  49. package/dist/commands/lint/knowledge-linter.js +3 -0
  50. package/dist/commands/lint/markdown-insertion.js +343 -0
  51. package/dist/commands/lint/memory-linter.js +3 -0
  52. package/dist/commands/lint/registry.js +3 -0
  53. package/dist/commands/lint/skill-linter.js +3 -0
  54. package/dist/commands/lint/task-linter.js +15 -12
  55. package/dist/commands/lint/types.js +3 -0
  56. package/dist/commands/lint/workflow-linter.js +3 -0
  57. package/dist/commands/lint.js +3 -0
  58. package/dist/commands/migration-help.js +5 -2
  59. package/dist/commands/proposal-drain-policies.js +128 -0
  60. package/dist/commands/proposal-drain.js +477 -0
  61. package/dist/commands/proposal.js +60 -6
  62. package/dist/commands/propose.js +24 -19
  63. package/dist/commands/reflect.js +1004 -94
  64. package/dist/commands/registry-cli.js +150 -0
  65. package/dist/commands/registry-search.js +3 -0
  66. package/dist/commands/remember-cli.js +257 -0
  67. package/dist/commands/remember.js +15 -6
  68. package/dist/commands/schema-repair.js +88 -15
  69. package/dist/commands/search.js +99 -14
  70. package/dist/commands/secret.js +173 -0
  71. package/dist/commands/self-update.js +3 -0
  72. package/dist/commands/show.js +32 -13
  73. package/dist/commands/source-add.js +7 -35
  74. package/dist/commands/source-clone.js +3 -0
  75. package/dist/commands/source-manage.js +3 -0
  76. package/dist/commands/tasks.js +161 -95
  77. package/dist/commands/url-checker.js +3 -0
  78. package/dist/core/action-contributors.js +3 -0
  79. package/dist/core/asset-ref.js +13 -2
  80. package/dist/core/asset-registry.js +9 -2
  81. package/dist/core/asset-serialize.js +88 -0
  82. package/dist/core/asset-spec.js +61 -5
  83. package/dist/core/common.js +93 -5
  84. package/dist/core/concurrent.js +3 -0
  85. package/dist/core/config-io.js +347 -0
  86. package/dist/core/config-migration.js +622 -0
  87. package/dist/core/config-schema.js +558 -0
  88. package/dist/core/config-sources.js +108 -0
  89. package/dist/core/config-types.js +4 -0
  90. package/dist/core/config-walker.js +337 -0
  91. package/dist/core/config.js +366 -1077
  92. package/dist/core/errors.js +42 -20
  93. package/dist/core/events.js +31 -25
  94. package/dist/core/file-lock.js +104 -0
  95. package/dist/core/frontmatter.js +75 -10
  96. package/dist/core/lesson-lint.js +3 -0
  97. package/dist/core/markdown.js +3 -0
  98. package/dist/core/memory-belief.js +62 -0
  99. package/dist/core/memory-contradiction-detect.js +274 -0
  100. package/dist/core/memory-improve.js +142 -14
  101. package/dist/core/parse.js +3 -0
  102. package/dist/core/paths.js +218 -50
  103. package/dist/core/proposal-quality-validators.js +380 -0
  104. package/dist/core/proposal-validators.js +11 -3
  105. package/dist/core/proposals.js +464 -5
  106. package/dist/core/state-db.js +349 -56
  107. package/dist/core/text-truncation.js +107 -0
  108. package/dist/core/time.js +3 -0
  109. package/dist/core/tty.js +59 -0
  110. package/dist/core/warn.js +7 -2
  111. package/dist/core/write-source.js +12 -0
  112. package/dist/indexer/db-backup.js +391 -0
  113. package/dist/indexer/db-search.js +136 -28
  114. package/dist/indexer/db.js +661 -166
  115. package/dist/indexer/ensure-index.js +3 -0
  116. package/dist/indexer/file-context.js +3 -0
  117. package/dist/indexer/graph-boost.js +162 -40
  118. package/dist/indexer/graph-db.js +241 -51
  119. package/dist/indexer/graph-dedup.js +3 -7
  120. package/dist/indexer/graph-extraction.js +242 -149
  121. package/dist/indexer/index-context.js +3 -9
  122. package/dist/indexer/indexer.js +84 -14
  123. package/dist/indexer/llm-cache.js +24 -19
  124. package/dist/indexer/manifest.js +3 -0
  125. package/dist/indexer/matchers.js +184 -11
  126. package/dist/indexer/memory-inference.js +94 -50
  127. package/dist/indexer/metadata-contributors.js +3 -0
  128. package/dist/indexer/metadata.js +110 -50
  129. package/dist/indexer/path-resolver.js +3 -0
  130. package/dist/indexer/project-context.js +192 -0
  131. package/dist/indexer/ranking-contributors.js +134 -7
  132. package/dist/indexer/ranking.js +8 -1
  133. package/dist/indexer/search-fields.js +5 -9
  134. package/dist/indexer/search-hit-enrichers.js +91 -2
  135. package/dist/indexer/search-source.js +20 -1
  136. package/dist/indexer/semantic-status.js +4 -1
  137. package/dist/indexer/staleness-detect.js +447 -0
  138. package/dist/indexer/usage-events.js +12 -9
  139. package/dist/indexer/walker.js +3 -0
  140. package/dist/integrations/agent/builders.js +135 -0
  141. package/dist/integrations/agent/config.js +121 -401
  142. package/dist/integrations/agent/detect.js +3 -0
  143. package/dist/integrations/agent/index.js +6 -14
  144. package/dist/integrations/agent/model-aliases.js +55 -0
  145. package/dist/integrations/agent/profiles.js +3 -0
  146. package/dist/integrations/agent/prompts.js +137 -8
  147. package/dist/integrations/agent/runner.js +208 -0
  148. package/dist/integrations/agent/sdk-runner.js +8 -2
  149. package/dist/integrations/agent/spawn.js +54 -14
  150. package/dist/integrations/github.js +3 -0
  151. package/dist/integrations/lockfile.js +22 -51
  152. package/dist/integrations/session-logs/index.js +4 -0
  153. package/dist/integrations/session-logs/inline-refs.js +35 -0
  154. package/dist/integrations/session-logs/pre-filter.js +152 -0
  155. package/dist/integrations/session-logs/providers/claude-code.js +226 -0
  156. package/dist/integrations/session-logs/providers/opencode.js +231 -25
  157. package/dist/integrations/session-logs/types.js +3 -0
  158. package/dist/llm/call-ai.js +14 -26
  159. package/dist/llm/client.js +16 -2
  160. package/dist/llm/embedder.js +20 -29
  161. package/dist/llm/embedders/cache.js +3 -7
  162. package/dist/llm/embedders/local.js +42 -1
  163. package/dist/llm/embedders/remote.js +20 -8
  164. package/dist/llm/embedders/types.js +3 -7
  165. package/dist/llm/feature-gate.js +92 -56
  166. package/dist/llm/graph-extract.js +401 -30
  167. package/dist/llm/index-passes.js +44 -29
  168. package/dist/llm/memory-infer.js +30 -2
  169. package/dist/llm/metadata-enhance.js +3 -7
  170. package/dist/llm/prompts/extract-session.md +80 -0
  171. package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
  172. package/dist/output/cli-hints-full.md +60 -32
  173. package/dist/output/cli-hints-short.md +10 -7
  174. package/dist/output/cli-hints.js +5 -2
  175. package/dist/output/context.js +60 -8
  176. package/dist/output/renderers.js +170 -194
  177. package/dist/output/shapes/curate.js +56 -0
  178. package/dist/output/shapes/distill.js +10 -0
  179. package/dist/output/shapes/env-list.js +19 -0
  180. package/dist/output/shapes/events.js +11 -0
  181. package/dist/output/shapes/helpers.js +424 -0
  182. package/dist/output/shapes/history.js +7 -0
  183. package/dist/output/shapes/passthrough.js +105 -0
  184. package/dist/output/shapes/proposal-accept.js +7 -0
  185. package/dist/output/shapes/proposal-diff.js +7 -0
  186. package/dist/output/shapes/proposal-list.js +7 -0
  187. package/dist/output/shapes/proposal-producer.js +11 -0
  188. package/dist/output/shapes/proposal-reject.js +7 -0
  189. package/dist/output/shapes/proposal-show.js +7 -0
  190. package/dist/output/shapes/registry-search.js +6 -0
  191. package/dist/output/shapes/registry.js +30 -0
  192. package/dist/output/shapes/search.js +6 -0
  193. package/dist/output/shapes/secret-list.js +19 -0
  194. package/dist/output/shapes/show.js +6 -0
  195. package/dist/output/shapes/vault-list.js +19 -0
  196. package/dist/output/shapes.js +51 -549
  197. package/dist/output/text/add.js +6 -0
  198. package/dist/output/text/clone.js +6 -0
  199. package/dist/output/text/config.js +6 -0
  200. package/dist/output/text/curate.js +6 -0
  201. package/dist/output/text/distill.js +7 -0
  202. package/dist/output/text/enable-disable.js +7 -0
  203. package/dist/output/text/events.js +10 -0
  204. package/dist/output/text/feedback.js +6 -0
  205. package/dist/output/text/helpers.js +1059 -0
  206. package/dist/output/text/history.js +7 -0
  207. package/dist/output/text/import.js +6 -0
  208. package/dist/output/text/index.js +6 -0
  209. package/dist/output/text/info.js +6 -0
  210. package/dist/output/text/init.js +6 -0
  211. package/dist/output/text/list.js +6 -0
  212. package/dist/output/text/proposal-producer.js +8 -0
  213. package/dist/output/text/proposal.js +12 -0
  214. package/dist/output/text/registry-commands.js +11 -0
  215. package/dist/output/text/registry.js +30 -0
  216. package/dist/output/text/remember.js +6 -0
  217. package/dist/output/text/remove.js +6 -0
  218. package/dist/output/text/save.js +6 -0
  219. package/dist/output/text/search.js +6 -0
  220. package/dist/output/text/show.js +6 -0
  221. package/dist/output/text/update.js +6 -0
  222. package/dist/output/text/upgrade.js +6 -0
  223. package/dist/output/text/vault.js +16 -0
  224. package/dist/output/text/wiki.js +15 -0
  225. package/dist/output/text/workflow.js +14 -0
  226. package/dist/output/text.js +44 -1329
  227. package/dist/registry/build-index.js +3 -0
  228. package/dist/registry/create-provider-registry.js +3 -0
  229. package/dist/registry/factory.js +4 -1
  230. package/dist/registry/origin-resolve.js +3 -0
  231. package/dist/registry/providers/index.js +3 -0
  232. package/dist/registry/providers/skills-sh.js +11 -2
  233. package/dist/registry/providers/static-index.js +10 -1
  234. package/dist/registry/providers/types.js +3 -24
  235. package/dist/registry/resolve.js +11 -16
  236. package/dist/registry/types.js +3 -0
  237. package/dist/scripts/migrate-storage.js +17767 -0
  238. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  239. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  240. package/dist/setup/detect.js +3 -0
  241. package/dist/setup/ripgrep-install.js +3 -0
  242. package/dist/setup/ripgrep-resolve.js +3 -0
  243. package/dist/setup/setup.js +306 -67
  244. package/dist/setup/steps.js +3 -15
  245. package/dist/sources/include.js +3 -0
  246. package/dist/sources/provider-factory.js +3 -11
  247. package/dist/sources/provider.js +3 -20
  248. package/dist/sources/providers/filesystem.js +19 -23
  249. package/dist/sources/providers/git.js +171 -21
  250. package/dist/sources/providers/index.js +3 -0
  251. package/dist/sources/providers/install-types.js +3 -13
  252. package/dist/sources/providers/npm.js +3 -4
  253. package/dist/sources/providers/provider-utils.js +3 -0
  254. package/dist/sources/providers/sync-from-ref.js +3 -11
  255. package/dist/sources/providers/tar-utils.js +3 -0
  256. package/dist/sources/providers/website.js +18 -22
  257. package/dist/sources/resolve.js +3 -0
  258. package/dist/sources/types.js +3 -0
  259. package/dist/sources/website-ingest.js +3 -0
  260. package/dist/tasks/backends/cron.js +3 -0
  261. package/dist/tasks/backends/exec-utils.js +3 -0
  262. package/dist/tasks/backends/index.js +3 -11
  263. package/dist/tasks/backends/launchd.js +3 -0
  264. package/dist/tasks/backends/schtasks.js +3 -0
  265. package/dist/tasks/parser.js +51 -38
  266. package/dist/tasks/resolveAkmBin.js +3 -0
  267. package/dist/tasks/runner.js +35 -9
  268. package/dist/tasks/schedule.js +20 -1
  269. package/dist/tasks/schema.js +5 -3
  270. package/dist/tasks/validator.js +6 -3
  271. package/dist/version.js +3 -0
  272. package/dist/wiki/wiki-templates.js +3 -0
  273. package/dist/wiki/wiki.js +3 -0
  274. package/dist/workflows/authoring.js +3 -0
  275. package/dist/workflows/cli.js +3 -0
  276. package/dist/workflows/db.js +140 -10
  277. package/dist/workflows/document-cache.js +3 -10
  278. package/dist/workflows/parser.js +3 -0
  279. package/dist/workflows/renderer.js +3 -0
  280. package/dist/workflows/runs.js +18 -1
  281. package/dist/workflows/schema.js +3 -0
  282. package/dist/workflows/scope-key.js +3 -0
  283. package/dist/workflows/validator.js +5 -9
  284. package/docs/README.md +7 -2
  285. package/docs/data-and-telemetry.md +225 -0
  286. package/docs/migration/release-notes/0.7.5.md +2 -2
  287. package/docs/migration/release-notes/0.8.0.md +57 -5
  288. package/docs/migration/v0.7-to-v0.8.md +1378 -0
  289. package/package.json +28 -11
  290. package/.github/LICENSE +0 -374
  291. package/dist/commands/install-audit.js +0 -385
  292. package/dist/commands/vault.js +0 -310
  293. package/dist/indexer/match-contributors.js +0 -141
  294. package/dist/integrations/agent/pipeline.js +0 -39
  295. package/dist/integrations/agent/runners.js +0 -31
@@ -1,311 +1,131 @@
1
- import { DEFAULT_CONFIG, getSources, } from "../core/config";
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Config CLI commands — `akm config get/set/unset/list`.
6
+ *
7
+ * Thin wrappers around the schema walker in `core/config-walker.ts`. Adding a
8
+ * new config field is one line of Zod schema in `core/config-schema.ts` and
9
+ * zero lines here — the walker handles get/set/unset/coercion uniformly.
10
+ *
11
+ * Legacy behaviour preserved:
12
+ * - `akm config set llm.<x>` writes to `profiles.llm.<defaults.llm>` (or
13
+ * auto-creates a "default" profile), mirroring the pre-rewrite shim.
14
+ * - `akm config set embedding.ollamaOptions.numCtx` is sugar for
15
+ * `embedding.ollamaOptions.num_ctx` (camelCase ↔ snake_case bridge).
16
+ * - `parseConfigValue` returns a Partial<AkmConfig> so it can be merged with
17
+ * the runtime config object via `mergeConfigValue`.
18
+ */
19
+ import { DEFAULT_CONFIG, getSources } from "../core/config";
20
+ import { configGet, configSet, configUnset, unknownKeyHint } from "../core/config-walker";
2
21
  import { UsageError } from "../core/errors";
3
- import { assertWritableAllowedForKind } from "../core/write-source";
4
- // ── Merge helpers for LLM/embedding subkey set ───────────────────────────────
5
- function mergeLlmLike(base, patch) {
6
- return { endpoint: "", model: "", ...(base ?? {}), ...patch };
7
- }
8
- function mergeLlmLikeEmbedding(base, patch) {
9
- return { endpoint: "", model: "", ...(base ?? {}), ...patch };
10
- }
11
- function validateSources(entries) {
12
- if (!entries)
13
- return undefined;
14
- for (const entry of entries) {
15
- assertWritableAllowedForKind(entry);
16
- }
17
- return entries;
18
- }
19
- export function parseConfigValue(key, value) {
20
- switch (key) {
21
- case "stashDir":
22
- return { stashDir: requireNonEmptyString(value, key) };
23
- case "defaultWriteTarget":
24
- return { defaultWriteTarget: requireNonEmptyString(value, key) };
25
- case "semanticSearchMode":
26
- // Accept legacy boolean-style strings from CLI
27
- if (value === "true")
28
- return { semanticSearchMode: "auto" };
29
- if (value === "false")
30
- return { semanticSearchMode: "off" };
31
- if (value !== "off" && value !== "auto") {
32
- throw new UsageError(`Invalid value for semanticSearchMode: expected "off" or "auto"`);
33
- }
34
- return { semanticSearchMode: value };
35
- case "embedding":
36
- return { embedding: parseEmbeddingConnectionValue(value) };
37
- case "embedding.endpoint":
38
- return { embedding: mergeLlmLikeEmbedding(undefined, { endpoint: requireNonEmptyString(value, key) }) };
39
- case "embedding.model":
40
- return { embedding: mergeLlmLikeEmbedding(undefined, { model: requireNonEmptyString(value, key) }) };
41
- case "embedding.apiKey":
42
- return { embedding: mergeLlmLikeEmbedding(undefined, { apiKey: requireNonEmptyString(value, key) }) };
43
- case "embedding.contextLength":
44
- return { embedding: mergeLlmLikeEmbedding(undefined, { contextLength: parsePositiveInteger(value, key) }) };
45
- case "embedding.ollamaOptions.numCtx":
46
- return {
47
- embedding: mergeLlmLikeEmbedding(undefined, { ollamaOptions: { num_ctx: parsePositiveInteger(value, key) } }),
48
- };
49
- case "llm":
50
- return { llm: parseLlmConnectionValue(value) };
51
- case "llm.endpoint":
52
- return { llm: mergeLlmLike(undefined, { endpoint: requireNonEmptyString(value, key) }) };
53
- case "llm.model":
54
- return { llm: mergeLlmLike(undefined, { model: requireNonEmptyString(value, key) }) };
55
- case "llm.apiKey":
56
- return { llm: mergeLlmLike(undefined, { apiKey: requireNonEmptyString(value, key) }) };
57
- case "llm.contextLength":
58
- return { llm: mergeLlmLike(undefined, { contextLength: parsePositiveInteger(value, key) }) };
59
- case "registries":
60
- return { registries: parseRegistriesValue(value) };
61
- case "sources":
62
- case "stashes":
63
- // "stashes" is kept as an alias for backwards-compat; both write to `sources`.
64
- return { sources: validateSources(parseStashesValue(value)) };
65
- case "output.format":
66
- return { output: { format: parseOutputFormat(value) } };
67
- case "output.detail":
68
- return { output: { detail: parseOutputDetail(value) } };
69
- case "security.installAudit.enabled":
70
- return { security: { installAudit: { enabled: parseBooleanValue(value, key) } } };
71
- case "security.installAudit.blockOnCritical":
72
- return { security: { installAudit: { blockOnCritical: parseBooleanValue(value, key) } } };
73
- case "security.installAudit.blockUnlistedRegistries":
74
- return { security: { installAudit: { blockUnlistedRegistries: parseBooleanValue(value, key) } } };
75
- case "security.installAudit.registryAllowlist":
76
- return { security: { installAudit: { registryAllowlist: parseStringArrayValue(value, key) } } };
77
- case "security.installAudit.registryWhitelist":
78
- return { security: { installAudit: { registryAllowlist: parseStringArrayValue(value, key) } } };
79
- case "security.installAudit.allowedFindings":
80
- return { security: { installAudit: { allowedFindings: parseAllowedFindingsValue(value, key) } } };
81
- default:
82
- throw new UsageError(`Unknown config key: ${key}`, "INVALID_FLAG_VALUE", UNKNOWN_CONFIG_KEY_HINT);
83
- }
84
- }
85
- const UNKNOWN_CONFIG_KEY_HINT = "Valid top-level keys: stashDir, embedding, llm, registries, sources, agent, output, semanticSearchMode. Use dotted paths like `embedding.endpoint` or `output.format` for nested values.";
22
+ // ── Legacy `llm.*` `profiles.llm.<default>.*` aliasing ────────────────────
23
+ /**
24
+ * Map a legacy top-level `llm.<sub>` path onto the actual schema path. The
25
+ * default profile name is "default" when `defaults.llm` is unset.
26
+ */
27
+ function rewriteLegacyLlmPath(config, key) {
28
+ if (key !== "llm" && !key.startsWith("llm."))
29
+ return key;
30
+ const sub = key === "llm" ? "" : key.slice("llm.".length);
31
+ const profileName = config.defaults?.llm ?? "default";
32
+ return sub ? `profiles.llm.${profileName}.${sub}` : `profiles.llm.${profileName}`;
33
+ }
34
+ /**
35
+ * Translate the legacy `embedding.ollamaOptions.numCtx` to the actual schema
36
+ * key `embedding.ollamaOptions.num_ctx`.
37
+ */
38
+ function rewriteEmbeddingPath(key) {
39
+ if (key === "embedding.ollamaOptions.numCtx")
40
+ return "embedding.ollamaOptions.num_ctx";
41
+ return key;
42
+ }
43
+ /**
44
+ * Translate the deprecated `stashes` alias for `sources` (one-way: both read
45
+ * and write go through `sources`).
46
+ */
47
+ function rewriteSourcesAlias(key) {
48
+ if (key === "stashes")
49
+ return "sources";
50
+ if (key.startsWith("stashes."))
51
+ return `sources.${key.slice("stashes.".length)}`;
52
+ return key;
53
+ }
54
+ function rewriteKey(config, key) {
55
+ let k = rewriteLegacyLlmPath(config, key);
56
+ k = rewriteEmbeddingPath(k);
57
+ k = rewriteSourcesAlias(k);
58
+ return k;
59
+ }
60
+ // ── Public API ──────────────────────────────────────────────────────────────
86
61
  export function getConfigValue(config, key) {
87
- switch (key) {
88
- case "stashDir":
89
- return config.stashDir ?? null;
90
- case "defaultWriteTarget":
91
- return config.defaultWriteTarget ?? null;
92
- case "semanticSearchMode":
93
- return config.semanticSearchMode;
94
- case "embedding":
95
- return config.embedding ?? null;
96
- case "embedding.endpoint":
97
- return config.embedding?.endpoint ?? null;
98
- case "embedding.model":
99
- return config.embedding?.model ?? null;
100
- case "embedding.apiKey":
101
- return config.embedding?.apiKey ?? null;
102
- case "embedding.contextLength":
103
- return config.embedding?.contextLength ?? null;
104
- case "embedding.ollamaOptions.numCtx":
105
- return config.embedding?.ollamaOptions?.num_ctx ?? null;
106
- case "llm":
107
- return config.llm ?? null;
108
- case "llm.endpoint":
109
- return config.llm?.endpoint ?? null;
110
- case "llm.model":
111
- return config.llm?.model ?? null;
112
- case "llm.apiKey":
113
- return config.llm?.apiKey ?? null;
114
- case "llm.contextLength":
115
- return config.llm?.contextLength ?? null;
116
- case "registries":
117
- return config.registries ?? DEFAULT_CONFIG.registries ?? [];
118
- case "sources":
119
- case "stashes":
120
- // "stashes" is an alias for "sources" for backwards-compat.
121
- return getSources(config);
122
- case "output.format":
123
- return config.output?.format ?? null;
124
- case "output.detail":
125
- return config.output?.detail ?? null;
126
- case "security":
127
- return config.security ?? null;
128
- case "security.installAudit.enabled":
129
- return config.security?.installAudit?.enabled ?? null;
130
- case "security.installAudit.blockOnCritical":
131
- return config.security?.installAudit?.blockOnCritical ?? null;
132
- case "security.installAudit.blockUnlistedRegistries":
133
- return config.security?.installAudit?.blockUnlistedRegistries ?? null;
134
- case "security.installAudit.registryAllowlist":
135
- return getInstallAuditAllowlist(config);
136
- case "security.installAudit.registryWhitelist":
137
- return getInstallAuditAllowlist(config);
138
- case "security.installAudit.allowedFindings":
139
- return config.security?.installAudit?.allowedFindings ?? null;
140
- default:
141
- throw new UsageError(`Unknown config key: ${key}`, "INVALID_FLAG_VALUE", UNKNOWN_CONFIG_KEY_HINT);
142
- }
62
+ const k = rewriteKey(config, key);
63
+ return configGet(config, k);
143
64
  }
144
65
  export function setConfigValue(config, key, rawValue) {
145
- switch (key) {
146
- case "stashDir":
147
- case "semanticSearchMode":
148
- case "embedding":
149
- case "llm":
150
- case "registries":
151
- case "sources":
152
- case "stashes":
153
- case "output.format":
154
- case "output.detail":
155
- case "security.installAudit.enabled":
156
- case "security.installAudit.blockOnCritical":
157
- case "security.installAudit.blockUnlistedRegistries":
158
- case "security.installAudit.registryAllowlist":
159
- case "security.installAudit.registryWhitelist":
160
- case "security.installAudit.allowedFindings":
161
- return mergeConfigValue(config, parseConfigValue(key, rawValue));
162
- // Subkey setters use deep-merge so sibling fields are preserved
163
- case "embedding.endpoint":
164
- return {
165
- ...config,
166
- embedding: mergeLlmLikeEmbedding(config.embedding, { endpoint: requireNonEmptyString(rawValue, key) }),
167
- };
168
- case "embedding.model":
169
- return {
170
- ...config,
171
- embedding: mergeLlmLikeEmbedding(config.embedding, { model: requireNonEmptyString(rawValue, key) }),
66
+ // #454: reject the legacy aliases up front so the error message names the
67
+ // env var the user typed (AKM_LLM_API_KEY) rather than the rewritten profile
68
+ // env var (AKM_PROFILE_DEFAULT_API_KEY) — both work at runtime, but the
69
+ // shorter name matches the user's mental model.
70
+ if (key === "llm.apiKey") {
71
+ throw new UsageError("apiKey cannot be persisted in config; export AKM_LLM_API_KEY instead. (key: llm.apiKey)", "INVALID_FLAG_VALUE", "Storing API keys in config.json leaks them through backups, logs, and version control. " +
72
+ "Use the corresponding environment variable. AKM reads it at request time.");
73
+ }
74
+ if (key === "embedding.apiKey") {
75
+ throw new UsageError("apiKey cannot be persisted in config; export AKM_EMBED_API_KEY instead. (key: embedding.apiKey)", "INVALID_FLAG_VALUE", "Storing API keys in config.json leaks them through backups, logs, and version control. " +
76
+ "Use the corresponding environment variable. AKM reads it at request time.");
77
+ }
78
+ const k = rewriteKey(config, key);
79
+ // Legacy ergonomic: `akm config set semanticSearchMode true|false`
80
+ let coerced = rawValue;
81
+ if (k === "semanticSearchMode") {
82
+ if (rawValue === "true")
83
+ coerced = "auto";
84
+ else if (rawValue === "false")
85
+ coerced = "off";
86
+ }
87
+ let next = configSet(config, k, coerced);
88
+ // Legacy ergonomic shim: when the user sets `llm.<field>` and no
89
+ // `defaults.llm` is set, point it at the freshly-created profile so the
90
+ // value actually takes effect at runtime.
91
+ if (key === "llm" || key.startsWith("llm.")) {
92
+ if (!next.defaults?.llm) {
93
+ next = {
94
+ ...next,
95
+ defaults: { ...(next.defaults ?? {}), llm: "default" },
172
96
  };
173
- case "embedding.apiKey":
174
- return {
175
- ...config,
176
- embedding: mergeLlmLikeEmbedding(config.embedding, { apiKey: requireNonEmptyString(rawValue, key) }),
177
- };
178
- case "embedding.contextLength":
179
- return {
180
- ...config,
181
- embedding: mergeLlmLikeEmbedding(config.embedding, { contextLength: parsePositiveInteger(rawValue, key) }),
182
- };
183
- case "embedding.ollamaOptions.numCtx":
184
- return {
185
- ...config,
186
- embedding: mergeLlmLikeEmbedding(config.embedding, {
187
- ollamaOptions: { ...(config.embedding?.ollamaOptions ?? {}), num_ctx: parsePositiveInteger(rawValue, key) },
188
- }),
189
- };
190
- case "llm.endpoint":
191
- return { ...config, llm: mergeLlmLike(config.llm, { endpoint: requireNonEmptyString(rawValue, key) }) };
192
- case "llm.model":
193
- return { ...config, llm: mergeLlmLike(config.llm, { model: requireNonEmptyString(rawValue, key) }) };
194
- case "llm.apiKey":
195
- return { ...config, llm: mergeLlmLike(config.llm, { apiKey: requireNonEmptyString(rawValue, key) }) };
196
- case "llm.contextLength":
197
- return { ...config, llm: mergeLlmLike(config.llm, { contextLength: parsePositiveInteger(rawValue, key) }) };
198
- case "defaultWriteTarget": {
199
- const name = requireNonEmptyString(rawValue, key);
200
- const knownNames = getSources(config)
201
- .map((s) => s.name)
202
- .filter((n) => typeof n === "string");
203
- if (knownNames.length > 0 && !knownNames.includes(name)) {
204
- throw new UsageError(`Unknown source name "${name}" for defaultWriteTarget; configured source names: ${knownNames.map((n) => `"${n}"`).join(", ")}`);
205
- }
206
- return { ...config, defaultWriteTarget: name };
207
97
  }
208
- default:
209
- throw new UsageError(`Unknown config key: ${key}`, "INVALID_FLAG_VALUE", UNKNOWN_CONFIG_KEY_HINT);
210
98
  }
99
+ return next;
211
100
  }
212
101
  export function unsetConfigValue(config, key) {
213
- switch (key) {
214
- case "stashDir":
215
- return { ...config, stashDir: undefined };
216
- case "defaultWriteTarget":
217
- return { ...config, defaultWriteTarget: undefined };
218
- case "embedding":
219
- return { ...config, embedding: undefined };
220
- case "embedding.endpoint":
221
- return { ...config, embedding: mergeLlmLikeEmbedding(config.embedding, { endpoint: "" }) };
222
- case "embedding.model":
223
- return { ...config, embedding: mergeLlmLikeEmbedding(config.embedding, { model: "" }) };
224
- case "embedding.apiKey": {
225
- if (!config.embedding)
226
- return config;
227
- const { apiKey: _a, ...rest } = config.embedding;
228
- return { ...config, embedding: rest };
229
- }
230
- case "embedding.contextLength": {
231
- if (!config.embedding)
232
- return config;
233
- const { contextLength: _cl, ...rest } = config.embedding;
234
- return { ...config, embedding: rest };
235
- }
236
- case "embedding.ollamaOptions.numCtx": {
237
- if (!config.embedding?.ollamaOptions)
238
- return config;
239
- const { num_ctx: _nc, ...restOpts } = config.embedding.ollamaOptions;
240
- const ollamaOptions = Object.keys(restOpts).length > 0 ? restOpts : undefined;
241
- return { ...config, embedding: { ...config.embedding, ollamaOptions } };
242
- }
243
- case "llm":
244
- return { ...config, llm: undefined };
245
- case "llm.endpoint":
246
- return { ...config, llm: mergeLlmLike(config.llm, { endpoint: "" }) };
247
- case "llm.model":
248
- return { ...config, llm: mergeLlmLike(config.llm, { model: "" }) };
249
- case "llm.apiKey": {
250
- if (!config.llm)
251
- return config;
252
- const { apiKey: _b, ...restLlm } = config.llm;
253
- return { ...config, llm: restLlm };
254
- }
255
- case "llm.contextLength": {
256
- if (!config.llm)
257
- return config;
258
- const { contextLength: _lctx, ...restLlm2 } = config.llm;
259
- return { ...config, llm: restLlm2 };
102
+ const k = rewriteKey(config, key);
103
+ return configUnset(config, k);
104
+ }
105
+ /**
106
+ * Compatibility shim: returns a `Partial<AkmConfig>` containing just the
107
+ * change. Older code merged this onto the live config — new code should call
108
+ * `setConfigValue` directly (which returns the full merged config).
109
+ */
110
+ export function parseConfigValue(key, value) {
111
+ // Use a "marker" base so we can detect which top-level fields actually got
112
+ // touched by the set call. Anything still equal to the marker is untouched.
113
+ const SENTINEL = Symbol("untouched");
114
+ const base = { semanticSearchMode: SENTINEL };
115
+ const next = setConfigValue(base, key, value);
116
+ const patch = {};
117
+ for (const k of Object.keys(next)) {
118
+ if (next[k] !== SENTINEL) {
119
+ patch[k] = next[k];
260
120
  }
261
- case "registries":
262
- return { ...config, registries: undefined };
263
- case "sources":
264
- case "stashes":
265
- // "stashes" is kept as an alias for backwards-compat; both clear `sources`.
266
- return { ...config, sources: undefined };
267
- case "output.format":
268
- return { ...config, output: mergeOutputConfig(config.output, { format: undefined }) };
269
- case "output.detail":
270
- return { ...config, output: mergeOutputConfig(config.output, { detail: undefined }) };
271
- case "security":
272
- return { ...config, security: undefined };
273
- case "security.installAudit.enabled":
274
- return { ...config, security: mergeSecurityConfig(config.security, { installAudit: { enabled: undefined } }) };
275
- case "security.installAudit.blockOnCritical":
276
- return {
277
- ...config,
278
- security: mergeSecurityConfig(config.security, { installAudit: { blockOnCritical: undefined } }),
279
- };
280
- case "security.installAudit.blockUnlistedRegistries":
281
- return {
282
- ...config,
283
- security: mergeSecurityConfig(config.security, { installAudit: { blockUnlistedRegistries: undefined } }),
284
- };
285
- case "security.installAudit.registryAllowlist":
286
- case "security.installAudit.registryWhitelist":
287
- return {
288
- ...config,
289
- security: mergeSecurityConfig(config.security, {
290
- installAudit: { registryAllowlist: undefined, registryWhitelist: undefined },
291
- }),
292
- };
293
- case "security.installAudit.allowedFindings":
294
- return {
295
- ...config,
296
- security: mergeSecurityConfig(config.security, {
297
- installAudit: { allowedFindings: undefined },
298
- }),
299
- };
300
- default:
301
- throw new UsageError(`Unknown or unsupported unset key: ${key}`, "INVALID_FLAG_VALUE", UNKNOWN_CONFIG_KEY_HINT);
302
121
  }
122
+ return patch;
303
123
  }
304
124
  export function listConfig(config) {
305
125
  const result = {
306
126
  semanticSearchMode: config.semanticSearchMode,
307
127
  registries: config.registries ?? DEFAULT_CONFIG.registries ?? [],
308
- output: mergeOutputConfig(DEFAULT_CONFIG.output, config.output) ?? null,
128
+ output: { ...(DEFAULT_CONFIG.output ?? {}), ...(config.output ?? {}) },
309
129
  stashDir: config.stashDir ?? null,
310
130
  installed: config.installed ?? [],
311
131
  sources: getSources(config),
@@ -314,230 +134,22 @@ export function listConfig(config) {
314
134
  result.defaultWriteTarget = config.defaultWriteTarget;
315
135
  if (config.embedding)
316
136
  result.embedding = config.embedding;
317
- if (config.llm)
318
- result.llm = config.llm;
319
- if (config.security)
320
- result.security = config.security;
137
+ if (config.profiles)
138
+ result.profiles = config.profiles;
139
+ if (config.defaults)
140
+ result.defaults = config.defaults;
141
+ if (config.search)
142
+ result.search = config.search;
143
+ if (config.index)
144
+ result.index = config.index;
145
+ if (config.feedback)
146
+ result.feedback = config.feedback;
147
+ if (config.improve)
148
+ result.improve = config.improve;
149
+ if (config.archiveRetentionDays !== undefined)
150
+ result.archiveRetentionDays = config.archiveRetentionDays;
151
+ if (config.configVersion !== undefined)
152
+ result.configVersion = config.configVersion;
321
153
  return result;
322
154
  }
323
- function mergeConfigValue(config, partial) {
324
- return {
325
- ...config,
326
- ...partial,
327
- output: mergeOutputConfig(config.output, partial.output),
328
- security: mergeSecurityConfig(config.security, partial.security),
329
- };
330
- }
331
- function mergeOutputConfig(base, override) {
332
- const merged = {
333
- ...(base ?? {}),
334
- ...(override ?? {}),
335
- };
336
- return merged.format || merged.detail ? merged : undefined;
337
- }
338
- function mergeSecurityConfig(base, override) {
339
- const mergedInstallAudit = mergeInstallAuditConfig(base?.installAudit, override?.installAudit);
340
- return mergedInstallAudit ? { installAudit: mergedInstallAudit } : undefined;
341
- }
342
- function mergeInstallAuditConfig(base, override) {
343
- const merged = {
344
- ...(base ?? {}),
345
- ...(override ?? {}),
346
- };
347
- const hasValue = Object.values(merged).some((value) => value !== undefined);
348
- return hasValue ? merged : undefined;
349
- }
350
- function parseOutputFormat(value) {
351
- if (value === "json" || value === "yaml" || value === "text")
352
- return value;
353
- throw new UsageError(`Invalid value for output.format: expected one of json|yaml|text`);
354
- }
355
- function parseOutputDetail(value) {
356
- if (value === "brief" || value === "normal" || value === "full")
357
- return value;
358
- throw new UsageError(`Invalid value for output.detail: expected one of brief|normal|full`);
359
- }
360
- function parseBooleanValue(value, key) {
361
- if (value === "true")
362
- return true;
363
- if (value === "false")
364
- return false;
365
- throw new UsageError(`Invalid value for ${key}: expected true or false`);
366
- }
367
- function parseStringArrayValue(value, key) {
368
- let parsed;
369
- try {
370
- parsed = JSON.parse(value);
371
- }
372
- catch {
373
- throw new UsageError(`Invalid value for ${key}: expected a JSON array of strings`);
374
- }
375
- if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) {
376
- throw new UsageError(`Invalid value for ${key}: expected a JSON array of strings`);
377
- }
378
- return parsed;
379
- }
380
- function getInstallAuditAllowlist(config) {
381
- return config.security?.installAudit?.registryAllowlist ?? config.security?.installAudit?.registryWhitelist ?? null;
382
- }
383
- function parseAllowedFindingsValue(value, key) {
384
- let parsed;
385
- try {
386
- parsed = JSON.parse(value);
387
- }
388
- catch {
389
- throw new UsageError(`Invalid value for ${key}: expected a JSON array of {id, ref?, path?, reason?} objects`);
390
- }
391
- if (!Array.isArray(parsed)) {
392
- throw new UsageError(`Invalid value for ${key}: expected a JSON array`);
393
- }
394
- return parsed.map((entry, i) => {
395
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
396
- throw new UsageError(`Invalid value for ${key}[${i}]: expected an object with an "id" field`);
397
- }
398
- const obj = entry;
399
- if (typeof obj.id !== "string" || !obj.id) {
400
- throw new UsageError(`Invalid value for ${key}[${i}]: "id" is required`);
401
- }
402
- const result = { id: obj.id };
403
- if (typeof obj.ref === "string" && obj.ref)
404
- result.ref = obj.ref;
405
- if (typeof obj.path === "string" && obj.path)
406
- result.path = obj.path;
407
- if (typeof obj.reason === "string" && obj.reason)
408
- result.reason = obj.reason;
409
- return result;
410
- });
411
- }
412
- function parseRegistriesValue(value) {
413
- if (value === "null" || value === "")
414
- return undefined;
415
- let parsed;
416
- try {
417
- parsed = JSON.parse(value);
418
- }
419
- catch {
420
- throw new UsageError(`Invalid value for registries: expected JSON array of {url, name?, enabled?, provider?, options?} objects` +
421
- ` (e.g. '[{"url":"https://example.com/index.json","name":"my-registry"}]')`);
422
- }
423
- if (!Array.isArray(parsed)) {
424
- throw new UsageError(`Invalid value for registries: expected a JSON array`);
425
- }
426
- return parsed.map((entry, i) => {
427
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
428
- throw new UsageError(`Invalid value for registries[${i}]: expected an object with a "url" field`);
429
- }
430
- const obj = entry;
431
- if (typeof obj.url !== "string" || !obj.url) {
432
- throw new UsageError(`Invalid value for registries[${i}]: "url" is required`);
433
- }
434
- // Spread the full entry so unknown/future fields round-trip intact.
435
- return { ...obj };
436
- });
437
- }
438
- function parseEmbeddingConnectionValue(value) {
439
- if (value === "null" || value === "")
440
- return undefined;
441
- const parsed = parseJsonObject(value, "embedding", {
442
- endpoint: "http://localhost:11434/v1/embeddings",
443
- model: "nomic-embed-text",
444
- });
445
- // Require either a non-empty endpoint (remote) or a localModel (local-only).
446
- const hasEndpoint = typeof parsed.endpoint === "string" && parsed.endpoint !== "";
447
- const hasLocalModel = typeof parsed.localModel === "string" && parsed.localModel !== "";
448
- if (!hasEndpoint && !hasLocalModel) {
449
- throw new UsageError(`Invalid value for embedding: "endpoint" is required for remote embeddings, or provide "localModel" for local-only`);
450
- }
451
- // Validate the types of the required/structural fields that the runtime
452
- // depends on, but do not reconstruct the object — pass everything through.
453
- if (parsed.endpoint !== undefined && typeof parsed.endpoint !== "string") {
454
- throw new UsageError(`Invalid value for embedding: "endpoint" must be a string`);
455
- }
456
- if (parsed.model !== undefined && typeof parsed.model !== "string") {
457
- throw new UsageError(`Invalid value for embedding: "model" must be a string`);
458
- }
459
- if (parsed.dimension !== undefined && !Number.isInteger(parsed.dimension)) {
460
- throw new UsageError(`embedding.dimension: expected a positive integer, got ${parsed.dimension}`, "INVALID_FLAG_VALUE");
461
- }
462
- // Spread the full parsed object so unknown/future fields round-trip intact.
463
- return { endpoint: "", model: "", ...parsed };
464
- }
465
- function parseLlmConnectionValue(value) {
466
- if (value === "null" || value === "")
467
- return undefined;
468
- const parsed = parseJsonObject(value, "llm", {
469
- endpoint: "http://localhost:11434/v1/chat/completions",
470
- model: "llama3.2",
471
- });
472
- if (parsed.endpoint !== undefined && typeof parsed.endpoint !== "string") {
473
- throw new UsageError(`Invalid value for llm: "endpoint" is a required string field`);
474
- }
475
- if (parsed.model !== undefined && typeof parsed.model !== "string") {
476
- throw new UsageError(`Invalid value for llm: "model" is a required string field`);
477
- }
478
- if (typeof parsed.endpoint !== "string" || !parsed.endpoint) {
479
- throw new UsageError(`Invalid value for llm: "endpoint" is a required string field`);
480
- }
481
- if (parsed.model === undefined) {
482
- return { endpoint: parsed.endpoint, model: "", ...parsed };
483
- }
484
- if (!parsed.model) {
485
- throw new UsageError(`Invalid value for llm: "model" must be a non-empty string when provided`);
486
- }
487
- // Spread the full parsed object so unknown/future fields round-trip intact.
488
- // The config loader (config.ts) handles warn-and-ignore for unknown sub-keys
489
- // at read time, so we do not need to whitelist here.
490
- return { ...parsed };
491
- }
492
- function parseJsonObject(value, key, example) {
493
- let parsed;
494
- try {
495
- parsed = JSON.parse(value);
496
- }
497
- catch {
498
- throw new UsageError(`Invalid value for ${key}: expected JSON object with endpoint and model` +
499
- ` (e.g. '{"endpoint":"${example.endpoint}","model":"${example.model}"}')`, "INVALID_JSON_CONFIG_VALUE");
500
- }
501
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
502
- throw new UsageError(`Invalid value for ${key}: expected a JSON object`);
503
- }
504
- return parsed;
505
- }
506
- function requireNonEmptyString(value, key) {
507
- if (!value) {
508
- throw new UsageError(`Invalid value for ${key}: expected a non-empty string`);
509
- }
510
- return value;
511
- }
512
- function parsePositiveInteger(value, key) {
513
- const n = Number(value);
514
- if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
515
- throw new UsageError(`Invalid value for ${key}: expected a positive integer`);
516
- }
517
- return n;
518
- }
519
- function parseStashesValue(value) {
520
- if (value === "null" || value === "")
521
- return undefined;
522
- let parsed;
523
- try {
524
- parsed = JSON.parse(value);
525
- }
526
- catch {
527
- throw new UsageError(`Invalid value for sources: expected JSON array of {type, path?, url?, name?, enabled?, options?} objects`);
528
- }
529
- if (!Array.isArray(parsed)) {
530
- throw new UsageError(`Invalid value for sources: expected a JSON array`);
531
- }
532
- return parsed.map((entry, i) => {
533
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
534
- throw new UsageError(`Invalid value for sources[${i}]: expected an object with a "type" field`);
535
- }
536
- const obj = entry;
537
- if (typeof obj.type !== "string" || !obj.type) {
538
- throw new UsageError(`Invalid value for sources[${i}]: "type" is required`);
539
- }
540
- // Spread the full entry so unknown/future fields round-trip intact.
541
- return { ...obj };
542
- });
543
- }
155
+ export { unknownKeyHint };