akm-cli 0.8.2 → 0.9.0-beta.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 (316) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/dist/assets/help/help-proposals.md +1 -2
  3. package/dist/assets/hints/cli-hints-full.md +34 -19
  4. package/dist/assets/hints/cli-hints-short.md +1 -1
  5. package/dist/assets/profiles/catchup.json +13 -0
  6. package/dist/assets/profiles/consolidate.json +13 -0
  7. package/dist/assets/profiles/frequent.json +13 -0
  8. package/dist/assets/tasks/core/backup.yml +4 -0
  9. package/dist/assets/tasks/core/extract.yml +4 -0
  10. package/dist/assets/tasks/core/improve.yml +4 -0
  11. package/dist/assets/tasks/core/index-refresh.yml +4 -0
  12. package/dist/assets/tasks/core/sync.yml +4 -0
  13. package/dist/assets/tasks/core/update-stashes.yml +4 -0
  14. package/dist/assets/tasks/core/version-check.yml +4 -0
  15. package/dist/cli/config-migrate.js +6 -6
  16. package/dist/cli/config-validate.js +4 -4
  17. package/dist/cli/confirm.js +3 -3
  18. package/dist/cli/parse-args.js +1 -1
  19. package/dist/cli/shared.js +51 -14
  20. package/dist/cli-node.mjs +26 -0
  21. package/dist/cli.js +171 -3862
  22. package/dist/commands/{agent-dispatch.js → agent/agent-dispatch.js} +6 -6
  23. package/dist/commands/{agent-support.js → agent/agent-support.js} +2 -2
  24. package/dist/commands/agent/contribute-cli.js +200 -0
  25. package/dist/commands/completions.js +1 -1
  26. package/dist/commands/config-cli.js +240 -3
  27. package/dist/commands/config-edit.js +344 -0
  28. package/dist/commands/db-cli.js +2 -2
  29. package/dist/commands/env/env-cli.js +529 -0
  30. package/dist/commands/env/env.js +410 -0
  31. package/dist/commands/env/secret-cli.js +259 -0
  32. package/dist/commands/{secret.js → env/secret.js} +6 -47
  33. package/dist/commands/events.js +4 -4
  34. package/dist/commands/feedback-cli.js +18 -34
  35. package/dist/commands/graph/graph-cli.js +132 -0
  36. package/dist/commands/{graph.js → graph/graph.js} +22 -16
  37. package/dist/commands/health/checks.js +279 -0
  38. package/dist/commands/health.js +94 -262
  39. package/dist/commands/{consolidate.js → improve/consolidate.js} +48 -36
  40. package/dist/commands/{distill-promotion-policy.js → improve/distill-promotion-policy.js} +3 -3
  41. package/dist/commands/{distill.js → improve/distill.js} +39 -18
  42. package/dist/commands/{eval-cases.js → improve/eval-cases.js} +1 -1
  43. package/dist/commands/{extract-cli.js → improve/extract-cli.js} +4 -4
  44. package/dist/commands/{extract-prompt.js → improve/extract-prompt.js} +2 -2
  45. package/dist/commands/{extract.js → improve/extract.js} +185 -26
  46. package/dist/commands/{improve-auto-accept.js → improve/improve-auto-accept.js} +4 -4
  47. package/dist/commands/{improve-cli.js → improve/improve-cli.js} +44 -22
  48. package/dist/commands/{improve-profiles.js → improve/improve-profiles.js} +13 -7
  49. package/dist/commands/{improve-result-file.js → improve/improve-result-file.js} +1 -1
  50. package/dist/commands/{improve.js → improve/improve.js} +509 -245
  51. package/dist/{core → commands/improve/memory}/memory-belief.js +2 -2
  52. package/dist/{core → commands/improve/memory}/memory-contradiction-detect.js +5 -5
  53. package/dist/{core → commands/improve/memory}/memory-improve.js +4 -4
  54. package/dist/commands/{reflect.js → improve/reflect.js} +33 -28
  55. package/dist/commands/improve/session-asset.js +248 -0
  56. package/dist/commands/lint/agent-linter.js +1 -1
  57. package/dist/commands/lint/base-linter.js +55 -37
  58. package/dist/commands/lint/command-linter.js +1 -1
  59. package/dist/commands/lint/default-linter.js +1 -1
  60. package/dist/commands/lint/env-key-rules.js +1 -1
  61. package/dist/commands/lint/index.js +19 -25
  62. package/dist/commands/lint/knowledge-linter.js +1 -1
  63. package/dist/commands/lint/memory-linter.js +1 -1
  64. package/dist/commands/lint/registry.js +8 -8
  65. package/dist/commands/lint/skill-linter.js +1 -1
  66. package/dist/commands/lint/task-linter.js +1 -1
  67. package/dist/commands/lint/workflow-linter.js +1 -1
  68. package/dist/commands/lint.js +1 -1
  69. package/dist/commands/observability-cli.js +244 -0
  70. package/dist/commands/proposal/drain-policies.js +3 -3
  71. package/dist/commands/proposal/drain.js +15 -10
  72. package/dist/commands/proposal/proposal-cli.js +478 -0
  73. package/dist/commands/{proposal.js → proposal/proposal.js} +5 -5
  74. package/dist/commands/{propose.js → proposal/propose.js} +11 -11
  75. package/dist/{core → commands/proposal/validators}/proposal-quality-validators.js +8 -3
  76. package/dist/{core → commands/proposal/validators}/proposal-validators.js +5 -5
  77. package/dist/{core → commands/proposal/validators}/proposals.js +13 -7
  78. package/dist/commands/{curate.js → read/curate.js} +7 -7
  79. package/dist/commands/{knowledge.js → read/knowledge.js} +22 -9
  80. package/dist/commands/{registry-search.js → read/registry-search.js} +5 -5
  81. package/dist/commands/{remember-cli.js → read/remember-cli.js} +15 -7
  82. package/dist/commands/read/search-cli.js +207 -0
  83. package/dist/commands/{search.js → read/search.js} +22 -27
  84. package/dist/commands/{show.js → read/show.js} +31 -45
  85. package/dist/commands/registry-cli.js +8 -8
  86. package/dist/commands/remember.js +8 -8
  87. package/dist/commands/sources/add-cli.js +293 -0
  88. package/dist/commands/{history.js → sources/history.js} +27 -25
  89. package/dist/commands/{info.js → sources/info.js} +6 -6
  90. package/dist/commands/{init.js → sources/init.js} +6 -6
  91. package/dist/commands/{installed-stashes.js → sources/installed-stashes.js} +12 -12
  92. package/dist/commands/{migration-help.js → sources/migration-help.js} +3 -2
  93. package/dist/commands/{schema-repair.js → sources/schema-repair.js} +8 -8
  94. package/dist/commands/{self-update.js → sources/self-update.js} +10 -9
  95. package/dist/commands/{source-add.js → sources/source-add.js} +10 -10
  96. package/dist/commands/{source-clone.js → sources/source-clone.js} +7 -7
  97. package/dist/commands/{source-manage.js → sources/source-manage.js} +4 -4
  98. package/dist/commands/sources/sources-cli.js +305 -0
  99. package/dist/commands/sources/stash-cli.js +219 -0
  100. package/dist/commands/{stash-skeleton.js → sources/stash-skeleton.js} +2 -1
  101. package/dist/commands/tasks/default-tasks.js +173 -0
  102. package/dist/commands/tasks/tasks-cli.js +210 -0
  103. package/dist/commands/{tasks.js → tasks/tasks.js} +14 -14
  104. package/dist/commands/wiki-cli.js +307 -0
  105. package/dist/commands/workflow-cli.js +329 -0
  106. package/dist/core/action-contributors.js +1 -1
  107. package/dist/core/assert.js +40 -0
  108. package/dist/core/asset/asset-create.js +54 -0
  109. package/dist/core/{asset-ref.js → asset/asset-ref.js} +21 -4
  110. package/dist/core/{asset-registry.js → asset/asset-registry.js} +3 -3
  111. package/dist/core/{asset-spec.js → asset/asset-spec.js} +17 -31
  112. package/dist/core/{markdown.js → asset/markdown.js} +1 -1
  113. package/dist/core/{stash-meta.js → asset/stash-meta.js} +1 -1
  114. package/dist/core/best-effort.js +64 -0
  115. package/dist/core/common.js +32 -18
  116. package/dist/core/{config-io.js → config/config-io.js} +29 -19
  117. package/dist/core/{config-migration.js → config/config-migration.js} +11 -9
  118. package/dist/core/{config-schema.js → config/config-schema.js} +45 -1
  119. package/dist/core/config/config-types.js +16 -0
  120. package/dist/core/{config-walker.js → config/config-walker.js} +2 -2
  121. package/dist/core/{config.js → config/config.js} +10 -8
  122. package/dist/core/env-secret-ref.js +90 -0
  123. package/dist/core/errors.js +13 -3
  124. package/dist/core/events.js +27 -4
  125. package/dist/core/file-lock.js +1 -1
  126. package/dist/core/improve-types.js +48 -0
  127. package/dist/core/lesson-lint.js +2 -2
  128. package/dist/core/paths.js +2 -2
  129. package/dist/core/ripgrep/install.js +2 -2
  130. package/dist/core/ripgrep/resolve.js +2 -2
  131. package/dist/core/state-db.js +88 -46
  132. package/dist/core/text-truncation.js +148 -0
  133. package/dist/core/time.js +1 -1
  134. package/dist/core/write-source.js +98 -85
  135. package/dist/indexer/{db-backup.js → db/db-backup.js} +9 -24
  136. package/dist/indexer/{db.js → db/db.js} +126 -116
  137. package/dist/indexer/{graph-db.js → db/graph-db.js} +9 -4
  138. package/dist/indexer/{llm-cache.js → db/llm-cache.js} +15 -12
  139. package/dist/indexer/ensure-index.js +4 -4
  140. package/dist/indexer/{graph-boost.js → graph/graph-boost.js} +1 -1
  141. package/dist/indexer/{graph-extraction.js → graph/graph-extraction.js} +55 -13
  142. package/dist/indexer/indexer.js +37 -30
  143. package/dist/indexer/init.js +54 -0
  144. package/dist/indexer/manifest.js +10 -10
  145. package/dist/indexer/{memory-inference.js → passes/memory-inference.js} +92 -23
  146. package/dist/indexer/{metadata-contributors.js → passes/metadata-contributors.js} +10 -8
  147. package/dist/indexer/{metadata.js → passes/metadata.js} +15 -19
  148. package/dist/indexer/{staleness-detect.js → passes/staleness-detect.js} +53 -12
  149. package/dist/indexer/{db-search.js → search/db-search.js} +28 -16
  150. package/dist/indexer/{ranking-contributors.js → search/ranking-contributors.js} +1 -1
  151. package/dist/indexer/{ranking.js → search/ranking.js} +2 -2
  152. package/dist/indexer/{search-hit-enrichers.js → search/search-hit-enrichers.js} +3 -3
  153. package/dist/indexer/{search-source.js → search/search-source.js} +8 -8
  154. package/dist/indexer/{semantic-status.js → search/semantic-status.js} +3 -3
  155. package/dist/indexer/usage/unmigrated-vaults-guard.js +94 -0
  156. package/dist/indexer/{usage-events.js → usage/usage-events.js} +32 -0
  157. package/dist/indexer/{file-context.js → walk/file-context.js} +10 -15
  158. package/dist/indexer/{matchers.js → walk/matchers.js} +13 -9
  159. package/dist/indexer/{path-resolver.js → walk/path-resolver.js} +6 -6
  160. package/dist/indexer/{project-context.js → walk/project-context.js} +1 -1
  161. package/dist/indexer/{walker.js → walk/walker.js} +4 -3
  162. package/dist/integrations/agent/builder-shared.js +39 -0
  163. package/dist/integrations/agent/builders.js +14 -81
  164. package/dist/integrations/agent/config.js +6 -4
  165. package/dist/integrations/agent/detect.js +1 -1
  166. package/dist/integrations/agent/index.js +23 -8
  167. package/dist/integrations/agent/prompts.js +2 -3
  168. package/dist/integrations/agent/runner.js +22 -3
  169. package/dist/integrations/agent/spawn.js +9 -10
  170. package/dist/integrations/harnesses/claude/agent-builder.js +48 -0
  171. package/dist/integrations/harnesses/claude/config-import.js +70 -0
  172. package/dist/integrations/harnesses/claude/index.js +64 -0
  173. package/dist/integrations/{session-logs/providers/claude-code.js → harnesses/claude/session-log.js} +16 -1
  174. package/dist/integrations/harnesses/index.js +144 -0
  175. package/dist/integrations/harnesses/opencode/agent-builder.js +43 -0
  176. package/dist/integrations/harnesses/opencode/config-import.js +82 -0
  177. package/dist/integrations/harnesses/opencode/index.js +59 -0
  178. package/dist/integrations/{session-logs/providers/opencode.js → harnesses/opencode/session-log.js} +1 -1
  179. package/dist/integrations/harnesses/opencode-sdk/index.js +49 -0
  180. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +234 -0
  181. package/dist/integrations/harnesses/types.js +43 -0
  182. package/dist/integrations/lockfile.js +7 -16
  183. package/dist/integrations/session-logs/index.js +82 -9
  184. package/dist/llm/call-ai.js +4 -4
  185. package/dist/llm/client.js +131 -6
  186. package/dist/llm/embedder.js +6 -6
  187. package/dist/llm/embedders/local.js +9 -22
  188. package/dist/llm/embedders/remote.js +2 -2
  189. package/dist/llm/embedders/types.js +1 -1
  190. package/dist/llm/graph-extract.js +31 -12
  191. package/dist/llm/index-passes.js +1 -1
  192. package/dist/llm/memory-infer.js +12 -5
  193. package/dist/llm/metadata-enhance.js +2 -2
  194. package/dist/output/context.js +6 -44
  195. package/dist/output/renderers.js +88 -58
  196. package/dist/output/shapes/curate.js +7 -3
  197. package/dist/output/shapes/distill.js +7 -3
  198. package/dist/output/shapes/env-list.js +18 -16
  199. package/dist/output/shapes/events.js +5 -4
  200. package/dist/output/shapes/helpers.js +2 -4
  201. package/dist/output/shapes/history.js +7 -3
  202. package/dist/output/shapes/passthrough.js +8 -11
  203. package/dist/output/shapes/{proposal-accept.js → proposal/accept.js} +7 -3
  204. package/dist/output/shapes/{proposal-diff.js → proposal/diff.js} +7 -3
  205. package/dist/output/shapes/{proposal-list.js → proposal/list.js} +7 -3
  206. package/dist/output/shapes/{proposal-producer.js → proposal/producer.js} +5 -4
  207. package/dist/output/shapes/{proposal-reject.js → proposal/reject.js} +7 -3
  208. package/dist/output/shapes/{proposal-show.js → proposal/show.js} +7 -3
  209. package/dist/output/shapes/registry-search.js +7 -3
  210. package/dist/output/shapes/registry.js +12 -0
  211. package/dist/output/shapes/search.js +7 -3
  212. package/dist/output/shapes/secret-list.js +18 -16
  213. package/dist/output/shapes/show.js +7 -3
  214. package/dist/output/shapes.js +55 -30
  215. package/dist/output/text/add.js +2 -3
  216. package/dist/output/text/clone.js +2 -3
  217. package/dist/output/text/config.js +2 -3
  218. package/dist/output/text/curate.js +4 -3
  219. package/dist/output/text/distill.js +2 -3
  220. package/dist/output/text/enable-disable.js +5 -4
  221. package/dist/output/text/env.js +13 -0
  222. package/dist/output/text/events.js +5 -4
  223. package/dist/output/text/feedback.js +4 -3
  224. package/dist/output/text/helpers.js +54 -39
  225. package/dist/output/text/history.js +2 -3
  226. package/dist/output/text/import.js +2 -3
  227. package/dist/output/text/index.js +2 -3
  228. package/dist/output/text/info.js +2 -3
  229. package/dist/output/text/init.js +2 -3
  230. package/dist/output/text/list.js +2 -3
  231. package/dist/output/text/proposal/producer.js +9 -0
  232. package/dist/output/text/proposal/proposal.js +13 -0
  233. package/dist/output/text/registry-commands.js +8 -7
  234. package/dist/output/text/registry.js +12 -0
  235. package/dist/output/text/remember.js +4 -3
  236. package/dist/output/text/remove.js +2 -3
  237. package/dist/output/text/save.js +2 -3
  238. package/dist/output/text/search.js +4 -3
  239. package/dist/output/text/show.js +4 -3
  240. package/dist/output/text/update.js +2 -3
  241. package/dist/output/text/upgrade.js +2 -3
  242. package/dist/output/text/wiki.js +12 -11
  243. package/dist/output/text/workflow.js +12 -10
  244. package/dist/output/text.js +66 -32
  245. package/dist/registry/build-index.js +11 -10
  246. package/dist/registry/factory.js +1 -1
  247. package/dist/registry/origin-resolve.js +1 -1
  248. package/dist/registry/providers/index.js +2 -2
  249. package/dist/registry/providers/skills-sh.js +91 -72
  250. package/dist/registry/providers/static-index.js +75 -52
  251. package/dist/registry/resolve.js +3 -3
  252. package/dist/runtime.js +242 -0
  253. package/dist/scripts/migrate-storage.js +1594 -673
  254. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +240 -166
  255. package/dist/setup/detect.js +311 -9
  256. package/dist/setup/harness-config-import.js +6 -120
  257. package/dist/setup/setup.js +454 -43
  258. package/dist/sources/include.js +1 -1
  259. package/dist/sources/provider-factory.js +2 -2
  260. package/dist/sources/providers/filesystem.js +3 -3
  261. package/dist/sources/providers/git.js +9 -9
  262. package/dist/sources/providers/index.js +4 -4
  263. package/dist/sources/providers/npm.js +6 -6
  264. package/dist/sources/providers/provider-utils.js +13 -20
  265. package/dist/sources/providers/sync-from-ref.js +5 -5
  266. package/dist/sources/providers/tar-utils.js +2 -2
  267. package/dist/sources/providers/website.js +2 -2
  268. package/dist/sources/resolve.js +5 -5
  269. package/dist/sources/website-ingest.js +5 -5
  270. package/dist/storage/database.js +102 -0
  271. package/dist/storage/engines/sqlite-migrations.js +42 -0
  272. package/dist/storage/locations.js +25 -0
  273. package/dist/storage/repositories/index-db.js +43 -0
  274. package/dist/storage/repositories/workflow-runs-repository.js +141 -0
  275. package/dist/tasks/backends/cron.js +4 -4
  276. package/dist/tasks/backends/exec-utils.js +32 -0
  277. package/dist/tasks/backends/index.js +3 -3
  278. package/dist/tasks/backends/launchd.js +7 -14
  279. package/dist/tasks/backends/schtasks.js +7 -16
  280. package/dist/tasks/embedded.js +71 -0
  281. package/dist/tasks/parser.js +2 -2
  282. package/dist/tasks/resolveAkmBin.js +1 -1
  283. package/dist/tasks/runner.js +28 -15
  284. package/dist/tasks/schedule.js +1 -1
  285. package/dist/tasks/validator.js +7 -7
  286. package/dist/text-import-hook.mjs +51 -0
  287. package/dist/version.js +2 -1
  288. package/dist/wiki/wiki.js +7 -7
  289. package/dist/workflows/{authoring.js → authoring/authoring.js} +6 -6
  290. package/dist/workflows/{scope-key.js → authoring/scope-key.js} +1 -1
  291. package/dist/workflows/cli.js +1 -1
  292. package/dist/workflows/db.js +50 -32
  293. package/dist/workflows/parser.js +4 -4
  294. package/dist/workflows/renderer.js +5 -5
  295. package/dist/workflows/runtime/agent-identity.js +56 -0
  296. package/dist/workflows/runtime/checkin.js +57 -0
  297. package/dist/workflows/{runs.js → runtime/runs.js} +197 -101
  298. package/dist/workflows/validate-summary.js +82 -0
  299. package/docs/README.md +1 -1
  300. package/docs/data-and-telemetry.md +6 -6
  301. package/package.json +16 -8
  302. package/dist/commands/add-cli.js +0 -279
  303. package/dist/commands/env.js +0 -213
  304. package/dist/integrations/agent/sdk-runner.js +0 -126
  305. package/dist/output/shapes/vault-list.js +0 -19
  306. package/dist/output/text/proposal-producer.js +0 -8
  307. package/dist/output/text/proposal.js +0 -12
  308. package/dist/output/text/vault.js +0 -16
  309. /package/dist/core/{asset-serialize.js → asset/asset-serialize.js} +0 -0
  310. /package/dist/core/{frontmatter.js → asset/frontmatter.js} +0 -0
  311. /package/dist/core/{config-sources.js → config/config-sources.js} +0 -0
  312. /package/dist/indexer/{graph-dedup.js → graph/graph-dedup.js} +0 -0
  313. /package/dist/{core/config-types.js → indexer/passes/pass-context.js} +0 -0
  314. /package/dist/indexer/{search-fields.js → search/search-fields.js} +0 -0
  315. /package/dist/indexer/{index-context.js → walk/index-context.js} +0 -0
  316. /package/dist/workflows/{document-cache.js → runtime/document-cache.js} +0 -0
@@ -0,0 +1,344 @@
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
+ * Interactive `akm config edit` — a schema-driven, menu-based config editor.
6
+ *
7
+ * ## Why @clack/prompts (not a widget TUI)
8
+ *
9
+ * The issue (#513) originally proposed a `neo-blessed` BIOS-style widget TUI.
10
+ * After evaluation (see `docs/technical/ink-tui-evaluation.md` and the #513
11
+ * comments) we ship this on `@clack/prompts` — the prompt library akm already
12
+ * uses for `akm setup` and `confirmDestructive`. Zero new deps, the same
13
+ * interaction paradigm as setup, and a proven packaging path through the
14
+ * `bun build --compile` single binary.
15
+ *
16
+ * ## Schema-driven, single source of truth
17
+ *
18
+ * The section list, the per-section fields, and each field's input type are
19
+ * DERIVED from the Zod config schema (`core/config/config-schema.ts`) by
20
+ * {@link buildConfigEditModel}. There is no hand-maintained parallel field
21
+ * table — adding a field to the schema makes it appear in the editor for free.
22
+ *
23
+ * ## Reuse, don't reimplement
24
+ *
25
+ * The write path reuses the existing machinery verbatim:
26
+ * - {@link setConfigValue} (the config-cli walker front-end) for coercion,
27
+ * validation, legacy aliasing, and apiKey rejection.
28
+ * - {@link loadConfig} / {@link saveConfig} for read/write.
29
+ * - {@link backupExistingConfig} for the timestamped pre-write snapshot.
30
+ *
31
+ * ## Pure core, thin shell
32
+ *
33
+ * {@link buildConfigEditModel} and {@link applyConfigEdit} are pure and unit
34
+ * tested directly — no TTY required. {@link runConfigEdit} is the thin
35
+ * @clack interaction layer.
36
+ */
37
+ import * as p from "@clack/prompts";
38
+ import { z } from "zod";
39
+ import { loadConfig, saveConfig } from "../core/config/config.js";
40
+ import { backupExistingConfig } from "../core/config/config-io.js";
41
+ import { AkmConfigShape } from "../core/config/config-schema.js";
42
+ import { UsageError } from "../core/errors.js";
43
+ import { getConfigPath } from "../core/paths.js";
44
+ import { getConfigValue, setConfigValue } from "./config-cli.js";
45
+ /** Maximum nesting depth walked when deriving fields. Guards against records. */
46
+ const MAX_FIELD_DEPTH = 3;
47
+ /** Strip Zod wrappers (.optional/.default/.nullable/.catch/.effects). */
48
+ function unwrapSchema(schema) {
49
+ let current = schema;
50
+ for (;;) {
51
+ if (current instanceof z.ZodOptional)
52
+ current = current._def.innerType;
53
+ else if (current instanceof z.ZodDefault)
54
+ current = current._def.innerType;
55
+ else if (current instanceof z.ZodNullable)
56
+ current = current._def.innerType;
57
+ else if (current instanceof z.ZodCatch)
58
+ current = current._def.innerType;
59
+ else if (current instanceof z.ZodReadonly)
60
+ current = current._def.innerType;
61
+ else if (current instanceof z.ZodEffects)
62
+ current = current._def.schema;
63
+ else
64
+ return current;
65
+ }
66
+ }
67
+ /** Classify an unwrapped leaf schema into a {@link ConfigFieldKind}. */
68
+ function classifyLeaf(schema, isSecret) {
69
+ if (isSecret)
70
+ return "secret";
71
+ if (schema instanceof z.ZodString)
72
+ return "text";
73
+ if (schema instanceof z.ZodNumber)
74
+ return "number";
75
+ if (schema instanceof z.ZodBoolean)
76
+ return "boolean";
77
+ if (schema instanceof z.ZodEnum)
78
+ return "select";
79
+ // ZodNativeEnum / ZodLiteral are treated as select/text fallbacks.
80
+ if (schema instanceof z.ZodLiteral)
81
+ return "text";
82
+ // Unions of primitives (e.g. configVersion: string|number) → text.
83
+ if (schema instanceof z.ZodUnion) {
84
+ const opts = schema._def.options.map(unwrapSchema);
85
+ if (opts.some((o) => o instanceof z.ZodString || o instanceof z.ZodNumber))
86
+ return "text";
87
+ }
88
+ return null;
89
+ }
90
+ /**
91
+ * Recursively collect editable leaf fields from an object schema. Descends
92
+ * into nested `z.object(...)` shapes (building dotted paths); records, arrays,
93
+ * and unknown composites are surfaced as a single `json` field so the user can
94
+ * still edit them as raw JSON via the walker's JSON coercion path.
95
+ */
96
+ function collectFields(schema, prefix, depth) {
97
+ const unwrapped = unwrapSchema(schema);
98
+ const fields = [];
99
+ if (unwrapped instanceof z.ZodObject && depth < MAX_FIELD_DEPTH) {
100
+ const shape = unwrapped.shape;
101
+ for (const [key, child] of Object.entries(shape)) {
102
+ const path = prefix ? `${prefix}.${key}` : key;
103
+ const childUnwrapped = unwrapSchema(child);
104
+ const isSecret = key === "apiKey";
105
+ if (childUnwrapped instanceof z.ZodObject && depth + 1 < MAX_FIELD_DEPTH) {
106
+ fields.push(...collectFields(childUnwrapped, path, depth + 1));
107
+ continue;
108
+ }
109
+ const kind = classifyLeaf(childUnwrapped, isSecret);
110
+ if (kind === null) {
111
+ // Records / arrays / nested composites at the depth limit: editable as JSON.
112
+ if (childUnwrapped instanceof z.ZodRecord ||
113
+ childUnwrapped instanceof z.ZodArray ||
114
+ childUnwrapped instanceof z.ZodObject) {
115
+ fields.push({ path, label: key, kind: "json", secret: false });
116
+ }
117
+ continue;
118
+ }
119
+ const field = {
120
+ path,
121
+ label: key,
122
+ kind,
123
+ secret: kind === "secret",
124
+ };
125
+ if (kind === "select") {
126
+ field.options = [...childUnwrapped._def.values];
127
+ }
128
+ fields.push(field);
129
+ }
130
+ }
131
+ return fields;
132
+ }
133
+ /**
134
+ * Build the schema-driven edit model: one section per top-level config key,
135
+ * each with its editable leaf fields and input kinds. Pure — depends only on
136
+ * the schema (the `config` argument is reserved for future value-aware
137
+ * shaping; current callers pass it through unchanged for symmetry with
138
+ * {@link applyConfigEdit}).
139
+ *
140
+ * Sections that yield no editable fields (pure records/arrays like `sources`,
141
+ * `installed`, `registries`, `index`, `profiles`) are still surfaced with a
142
+ * single `json` field so they remain reachable in the menu.
143
+ */
144
+ export function buildConfigEditModel(shape = AkmConfigShape, _config) {
145
+ const sections = [];
146
+ for (const [key, schema] of Object.entries(shape)) {
147
+ const unwrapped = unwrapSchema(schema);
148
+ let fields;
149
+ if (unwrapped instanceof z.ZodObject) {
150
+ fields = collectFields(unwrapped, key, 1);
151
+ if (fields.length === 0) {
152
+ fields = [{ path: key, label: key, kind: "json", secret: false }];
153
+ }
154
+ }
155
+ else {
156
+ const kind = classifyLeaf(unwrapped, false);
157
+ if (kind) {
158
+ const field = { path: key, label: key, kind, secret: false };
159
+ if (kind === "select")
160
+ field.options = [...unwrapped._def.values];
161
+ fields = [field];
162
+ }
163
+ else {
164
+ // Arrays / records / unknown top-level shapes → editable as JSON.
165
+ fields = [{ path: key, label: key, kind: "json", secret: false }];
166
+ }
167
+ }
168
+ sections.push({ key, fields });
169
+ }
170
+ return sections.length > 0 ? { sections } : { sections: [] };
171
+ }
172
+ // ── Apply (pure write delegation) ────────────────────────────────────────────
173
+ /**
174
+ * Apply a single edit to a config object, returning the next config. Pure —
175
+ * delegates to {@link setConfigValue} (the existing walker front-end), so it
176
+ * inherits coercion, schema validation, legacy aliasing, AND the apiKey
177
+ * rejection guard (#454). Callers must NOT pass apiKey paths; the editor shell
178
+ * routes secrets to env-var guidance and never reaches here for them.
179
+ *
180
+ * @throws UsageError on apiKey paths, unknown keys, or invalid values.
181
+ */
182
+ export function applyConfigEdit(config, path, value) {
183
+ return setConfigValue(config, path, value);
184
+ }
185
+ /** Environment variable a secret field steers the user toward (#454). */
186
+ export function envVarForSecret(path) {
187
+ if (path === "embedding.apiKey")
188
+ return "AKM_EMBED_API_KEY";
189
+ if (path === "llm.apiKey")
190
+ return "AKM_LLM_API_KEY";
191
+ if (path.startsWith("profiles.llm."))
192
+ return "AKM_LLM_API_KEY";
193
+ return "AKM_LLM_API_KEY / AKM_EMBED_API_KEY";
194
+ }
195
+ // ── Interactive shell (thin @clack layer) ────────────────────────────────────
196
+ /**
197
+ * Determine whether the current process can run an interactive editor.
198
+ * Requires a real TTY on both stdin and stdout and a non-CI environment.
199
+ */
200
+ export function isInteractiveTerminal(env = process.env) {
201
+ const ci = env.CI;
202
+ const isCi = ci !== undefined && ci !== null && !["", "0", "false"].includes(String(ci).trim().toLowerCase());
203
+ if (isCi)
204
+ return false;
205
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
206
+ }
207
+ const NON_INTERACTIVE_MESSAGE = "`akm config edit` is interactive and requires a TTY. " +
208
+ "Use `akm config set <key> <value>` for scripted or CI edits.";
209
+ function formatValue(value) {
210
+ if (value === null || value === undefined)
211
+ return "(unset)";
212
+ if (typeof value === "object")
213
+ return JSON.stringify(value);
214
+ return String(value);
215
+ }
216
+ /**
217
+ * Run the interactive config editor. Throws {@link UsageError} when no TTY is
218
+ * available (CI / piped). Otherwise drives a menu loop:
219
+ * section select → field select → typed value prompt → confirm → backup+save.
220
+ */
221
+ export async function runConfigEdit() {
222
+ if (!isInteractiveTerminal()) {
223
+ throw new UsageError(NON_INTERACTIVE_MESSAGE, "NON_INTERACTIVE_REQUIRES_YES");
224
+ }
225
+ let config = loadConfig();
226
+ const model = buildConfigEditModel(AkmConfigShape, config);
227
+ let dirty = false;
228
+ p.intro("akm config edit");
229
+ for (;;) {
230
+ const sectionKey = await p.select({
231
+ message: "Select a config section to edit:",
232
+ options: [
233
+ ...model.sections.map((s) => ({ value: s.key, label: s.key })),
234
+ { value: "__exit__", label: dirty ? "Save and exit" : "Exit" },
235
+ ],
236
+ });
237
+ if (p.isCancel(sectionKey) || sectionKey === "__exit__")
238
+ break;
239
+ const section = model.sections.find((s) => s.key === sectionKey);
240
+ if (!section)
241
+ continue;
242
+ const fieldPath = await p.select({
243
+ message: `Select a field in "${section.key}":`,
244
+ options: [
245
+ ...section.fields.map((f) => ({
246
+ value: f.path,
247
+ label: f.label,
248
+ hint: `${f.kind} — ${formatValue(safeGet(config, f.path))}`,
249
+ })),
250
+ { value: "__back__", label: "← Back" },
251
+ ],
252
+ });
253
+ if (p.isCancel(fieldPath) || fieldPath === "__back__")
254
+ continue;
255
+ const field = section.fields.find((f) => f.path === fieldPath);
256
+ if (!field)
257
+ continue;
258
+ // #454: never persist secrets. Show env-var guidance and skip the write.
259
+ if (field.secret) {
260
+ p.note(`API keys are never stored in config (they leak through backups, logs, and version control).\n` +
261
+ `Export the environment variable instead:\n\n export ${envVarForSecret(field.path)}=…\n\n` +
262
+ `AKM reads it at request time.`, "apiKey is not persisted");
263
+ continue;
264
+ }
265
+ const newValue = await promptForField(field, safeGet(config, field.path));
266
+ if (newValue === undefined)
267
+ continue; // cancelled / back
268
+ try {
269
+ config = applyConfigEdit(config, field.path, newValue);
270
+ dirty = true;
271
+ p.log.success(`Set ${field.path} = ${newValue}`);
272
+ }
273
+ catch (err) {
274
+ const msg = err instanceof Error ? err.message : String(err);
275
+ p.log.error(msg);
276
+ }
277
+ }
278
+ if (!dirty) {
279
+ p.outro("No changes made.");
280
+ return;
281
+ }
282
+ const confirmed = await p.confirm({ message: "Save changes to config?", initialValue: true });
283
+ if (p.isCancel(confirmed) || confirmed !== true) {
284
+ p.outro("Discarded changes.");
285
+ return;
286
+ }
287
+ const backup = backupExistingConfig(getConfigPath());
288
+ saveConfig(config);
289
+ if (backup) {
290
+ p.outro(`Saved. Backup written to ${backup.timestamped}`);
291
+ }
292
+ else {
293
+ p.outro("Saved.");
294
+ }
295
+ }
296
+ /** Read a value via the existing walker front-end, swallowing unknown-key errors. */
297
+ function safeGet(config, path) {
298
+ try {
299
+ return getConfigValue(config, path);
300
+ }
301
+ catch {
302
+ return undefined;
303
+ }
304
+ }
305
+ /**
306
+ * Prompt for a single field's new value, typed by its schema-derived kind.
307
+ * Returns the raw string to pass to {@link applyConfigEdit}, or `undefined`
308
+ * when the user cancels.
309
+ */
310
+ async function promptForField(field, current) {
311
+ if (field.kind === "boolean") {
312
+ const v = await p.confirm({
313
+ message: `${field.label}:`,
314
+ initialValue: current === true,
315
+ });
316
+ if (p.isCancel(v))
317
+ return undefined;
318
+ return v ? "true" : "false";
319
+ }
320
+ if (field.kind === "select" && field.options) {
321
+ const v = await p.select({
322
+ message: `${field.label}:`,
323
+ options: field.options.map((o) => ({ value: o, label: o })),
324
+ initialValue: typeof current === "string" ? current : undefined,
325
+ });
326
+ if (p.isCancel(v))
327
+ return undefined;
328
+ return v;
329
+ }
330
+ const placeholder = field.kind === "json" ? "JSON value (or empty to clear)" : "";
331
+ const initial = current === null || current === undefined
332
+ ? ""
333
+ : typeof current === "object"
334
+ ? JSON.stringify(current)
335
+ : String(current);
336
+ const v = await p.text({
337
+ message: `${field.label}${field.kind === "number" ? " (number)" : ""}:`,
338
+ placeholder,
339
+ initialValue: initial,
340
+ });
341
+ if (p.isCancel(v))
342
+ return undefined;
343
+ return v;
344
+ }
@@ -12,8 +12,8 @@
12
12
  * plain `mv`/`cp`) to recover. Keeping the surface narrow lets us evolve the
13
13
  * backup format under the hood without locking in an API.
14
14
  */
15
- import { getDataDir } from "../core/paths";
16
- import { listBackups } from "../indexer/db-backup";
15
+ import { getDataDir } from "../core/paths.js";
16
+ import { listBackups } from "../indexer/db/db-backup.js";
17
17
  export function akmDbBackups() {
18
18
  const dataDir = getDataDir();
19
19
  return {