akm-cli 0.6.0 → 0.7.0-rc1

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 (319) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/{cli.js → src/cli.js} +672 -29
  3. package/dist/{commands → src/commands}/config-cli.js +5 -4
  4. package/dist/src/commands/distill.js +283 -0
  5. package/dist/src/commands/events.js +108 -0
  6. package/dist/src/commands/history.js +120 -0
  7. package/dist/{commands → src/commands}/installed-stashes.js +28 -2
  8. package/dist/src/commands/proposal.js +119 -0
  9. package/dist/src/commands/propose.js +171 -0
  10. package/dist/src/commands/reflect.js +193 -0
  11. package/dist/{commands → src/commands}/registry-search.js +2 -1
  12. package/dist/{commands → src/commands}/remember.js +12 -0
  13. package/dist/{commands → src/commands}/search.js +74 -1
  14. package/dist/{commands → src/commands}/self-update.js +4 -3
  15. package/dist/{commands → src/commands}/show.js +67 -2
  16. package/dist/{core → src/core}/asset-ref.js +5 -5
  17. package/dist/{core → src/core}/asset-spec.js +12 -0
  18. package/dist/{core → src/core}/common.js +1 -1
  19. package/dist/{core → src/core}/config.js +175 -121
  20. package/dist/{core → src/core}/errors.js +4 -0
  21. package/dist/src/core/events.js +239 -0
  22. package/dist/src/core/lesson-lint.js +86 -0
  23. package/dist/src/core/proposals.js +406 -0
  24. package/dist/src/core/warn.js +72 -0
  25. package/dist/{core → src/core}/write-source.js +80 -5
  26. package/dist/{indexer → src/indexer}/db-search.js +119 -27
  27. package/dist/{indexer → src/indexer}/db.js +76 -23
  28. package/dist/{indexer → src/indexer}/file-context.js +0 -3
  29. package/dist/src/indexer/graph-boost.js +179 -0
  30. package/dist/src/indexer/graph-extraction.js +212 -0
  31. package/dist/{indexer → src/indexer}/indexer.js +73 -6
  32. package/dist/src/indexer/memory-inference.js +263 -0
  33. package/dist/{indexer → src/indexer}/metadata.js +114 -11
  34. package/dist/src/integrations/agent/config.js +292 -0
  35. package/dist/src/integrations/agent/detect.js +94 -0
  36. package/dist/src/integrations/agent/index.js +17 -0
  37. package/dist/src/integrations/agent/profiles.js +65 -0
  38. package/dist/src/integrations/agent/prompts.js +167 -0
  39. package/dist/src/integrations/agent/spawn.js +221 -0
  40. package/dist/{integrations → src/integrations}/lockfile.js +0 -26
  41. package/dist/{llm → src/llm}/client.js +33 -2
  42. package/dist/src/llm/feature-gate.js +108 -0
  43. package/dist/src/llm/graph-extract.js +107 -0
  44. package/dist/src/llm/index-passes.js +35 -0
  45. package/dist/src/llm/memory-infer.js +86 -0
  46. package/dist/{output → src/output}/renderers.js +60 -1
  47. package/dist/src/output/shapes.js +516 -0
  48. package/dist/{output → src/output}/text.js +447 -4
  49. package/dist/{registry → src/registry}/build-index.js +14 -4
  50. package/dist/{registry → src/registry}/factory.js +0 -8
  51. package/dist/{registry → src/registry}/providers/static-index.js +3 -2
  52. package/dist/{registry → src/registry}/resolve.js +68 -2
  53. package/dist/{setup → src/setup}/setup.js +43 -5
  54. package/dist/{sources → src/sources}/providers/git.js +7 -15
  55. package/dist/{wiki → src/wiki}/wiki.js +9 -11
  56. package/dist/tests/add-website-source.test.js +119 -0
  57. package/dist/tests/agent/agent-config-loader.test.js +70 -0
  58. package/dist/tests/agent/agent-config.test.js +221 -0
  59. package/dist/tests/agent/agent-detect.test.js +100 -0
  60. package/dist/tests/agent/agent-spawn.test.js +234 -0
  61. package/dist/tests/agent-output.test.js +186 -0
  62. package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +103 -0
  63. package/dist/tests/architecture/agent-spawn-seam.test.js +193 -0
  64. package/dist/tests/architecture/llm-stateless-seam.test.js +112 -0
  65. package/dist/tests/asset-ref.test.js +192 -0
  66. package/dist/tests/asset-registry.test.js +103 -0
  67. package/dist/tests/asset-spec.test.js +241 -0
  68. package/dist/tests/bench/attribution.test.js +995 -0
  69. package/dist/tests/bench/cleanup-sigint.test.js +83 -0
  70. package/dist/tests/bench/cleanup.js +203 -0
  71. package/dist/tests/bench/cleanup.test.js +166 -0
  72. package/dist/tests/bench/cli.js +683 -0
  73. package/dist/tests/bench/cli.test.js +177 -0
  74. package/dist/tests/bench/compare.test.js +556 -0
  75. package/dist/tests/bench/corpus.js +314 -0
  76. package/dist/tests/bench/corpus.test.js +258 -0
  77. package/dist/tests/bench/driver.js +346 -0
  78. package/dist/tests/bench/driver.test.js +443 -0
  79. package/dist/tests/bench/evolve-metrics.js +179 -0
  80. package/dist/tests/bench/evolve-metrics.test.js +187 -0
  81. package/dist/tests/bench/evolve.js +580 -0
  82. package/dist/tests/bench/evolve.test.js +616 -0
  83. package/dist/tests/bench/failure-modes.test.js +300 -0
  84. package/dist/tests/bench/feedback-integrity.test.js +456 -0
  85. package/dist/tests/bench/leakage.test.js +125 -0
  86. package/dist/tests/bench/learning-curve.test.js +133 -0
  87. package/dist/tests/bench/metrics.js +2319 -0
  88. package/dist/tests/bench/metrics.test.js +1144 -0
  89. package/dist/tests/bench/no-os-tmpdir-invariant.test.js +43 -0
  90. package/dist/tests/bench/report.js +1821 -0
  91. package/dist/tests/bench/report.test.js +989 -0
  92. package/dist/tests/bench/runner.js +536 -0
  93. package/dist/tests/bench/runner.test.js +958 -0
  94. package/dist/tests/bench/search-bridge.test.js +331 -0
  95. package/dist/tests/bench/tmp.js +41 -0
  96. package/dist/tests/bench/trajectory.js +116 -0
  97. package/dist/tests/bench/trajectory.test.js +127 -0
  98. package/dist/tests/bench/verifier.js +109 -0
  99. package/dist/tests/bench/verifier.test.js +118 -0
  100. package/dist/tests/bench/workflow-evaluator.js +557 -0
  101. package/dist/tests/bench/workflow-evaluator.test.js +421 -0
  102. package/dist/tests/bench/workflow-spec.js +358 -0
  103. package/dist/tests/bench/workflow-spec.test.js +363 -0
  104. package/dist/tests/bench/workflow-trace.js +438 -0
  105. package/dist/tests/bench/workflow-trace.test.js +254 -0
  106. package/dist/tests/benchmark-search-quality.js +536 -0
  107. package/dist/tests/benchmark-suite.js +1441 -0
  108. package/dist/tests/capture-cli.test.js +112 -0
  109. package/dist/tests/cli-errors.test.js +203 -0
  110. package/dist/tests/commands/events.test.js +370 -0
  111. package/dist/tests/commands/history.test.js +223 -0
  112. package/dist/tests/commands/import.test.js +103 -0
  113. package/dist/tests/commands/proposal-cli.test.js +209 -0
  114. package/dist/tests/commands/reflect-propose-cli.test.js +333 -0
  115. package/dist/tests/commands/remember.test.js +97 -0
  116. package/dist/tests/commands/scope-flags.test.js +300 -0
  117. package/dist/tests/commands/search.test.js +537 -0
  118. package/dist/tests/commands/show-indexer-parity.test.js +117 -0
  119. package/dist/tests/commands/show.test.js +294 -0
  120. package/dist/tests/common.test.js +266 -0
  121. package/dist/tests/completions.test.js +142 -0
  122. package/dist/tests/config-cli.test.js +193 -0
  123. package/dist/tests/config-llm-features.test.js +139 -0
  124. package/dist/tests/config.test.js +544 -0
  125. package/dist/tests/contracts/migration-baseline.test.js +43 -0
  126. package/dist/tests/contracts/reflect-propose-envelope.test.js +139 -0
  127. package/dist/tests/contracts/spec-helpers.js +46 -0
  128. package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +228 -0
  129. package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +56 -0
  130. package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +34 -0
  131. package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +94 -0
  132. package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +39 -0
  133. package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +44 -0
  134. package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +47 -0
  135. package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +40 -0
  136. package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +58 -0
  137. package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +34 -0
  138. package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +75 -0
  139. package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +36 -0
  140. package/dist/tests/core/write-source.test.js +366 -0
  141. package/dist/tests/curate-command.test.js +87 -0
  142. package/dist/tests/db-scoring.test.js +201 -0
  143. package/dist/tests/db.test.js +654 -0
  144. package/dist/tests/distill-cli-flag.test.js +208 -0
  145. package/dist/tests/distill.test.js +515 -0
  146. package/dist/tests/docker-install.test.js +120 -0
  147. package/dist/tests/e2e.test.js +1398 -0
  148. package/dist/tests/embedder.test.js +340 -0
  149. package/dist/tests/embedding-model-config.test.js +379 -0
  150. package/dist/tests/feedback-command.test.js +172 -0
  151. package/dist/tests/file-context.test.js +552 -0
  152. package/dist/tests/fixtures/scripts/git/summarize-diff.js +9 -0
  153. package/dist/tests/fixtures/scripts/lint/eslint-check.js +7 -0
  154. package/dist/tests/fixtures/stashes/load.js +166 -0
  155. package/dist/tests/fixtures/stashes/load.test.js +88 -0
  156. package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +12 -0
  157. package/dist/tests/frontmatter.test.js +190 -0
  158. package/dist/tests/fts-field-weighting.test.js +254 -0
  159. package/dist/tests/fuzzy-search.test.js +230 -0
  160. package/dist/tests/git-provider-clone.test.js +45 -0
  161. package/dist/tests/github.test.js +161 -0
  162. package/dist/tests/graph-boost-ranking.test.js +305 -0
  163. package/dist/tests/graph-extraction.test.js +282 -0
  164. package/dist/tests/helpers/usage-events.js +8 -0
  165. package/dist/tests/index-pass-llm.test.js +161 -0
  166. package/dist/tests/indexer.test.js +559 -0
  167. package/dist/tests/info-command.test.js +166 -0
  168. package/dist/tests/init.test.js +69 -0
  169. package/dist/tests/install-script.test.js +246 -0
  170. package/dist/tests/integration/agent-real-profile.test.js +94 -0
  171. package/dist/tests/issue-36-repro.test.js +304 -0
  172. package/dist/tests/issues-191-194.test.js +160 -0
  173. package/dist/tests/lesson-lint.test.js +111 -0
  174. package/dist/tests/llm-client.test.js +115 -0
  175. package/dist/tests/llm-feature-gate.test.js +151 -0
  176. package/dist/tests/llm.test.js +139 -0
  177. package/dist/tests/lockfile.test.js +216 -0
  178. package/dist/tests/manifest.test.js +205 -0
  179. package/dist/tests/markdown.test.js +126 -0
  180. package/dist/tests/matchers-unit.test.js +189 -0
  181. package/dist/tests/memory-inference.test.js +299 -0
  182. package/dist/tests/merge-scoring.test.js +136 -0
  183. package/dist/tests/metadata.test.js +313 -0
  184. package/dist/tests/migration-help.test.js +89 -0
  185. package/dist/tests/origin-resolve.test.js +124 -0
  186. package/dist/tests/output-baseline.test.js +217 -0
  187. package/dist/tests/output-shapes-unit.test.js +476 -0
  188. package/dist/tests/parallel-search.test.js +272 -0
  189. package/dist/tests/parameter-metadata.test.js +365 -0
  190. package/dist/tests/paths.test.js +177 -0
  191. package/dist/tests/progressive-disclosure.test.js +280 -0
  192. package/dist/tests/proposals.test.js +279 -0
  193. package/dist/tests/proposed-quality.test.js +271 -0
  194. package/dist/tests/provider-registry.test.js +32 -0
  195. package/dist/tests/ranking-regression.test.js +548 -0
  196. package/dist/tests/reflect-propose.test.js +455 -0
  197. package/dist/tests/registry-build-index.test.js +378 -0
  198. package/dist/tests/registry-cli.test.js +290 -0
  199. package/dist/tests/registry-index-v2.test.js +430 -0
  200. package/dist/tests/registry-install.test.js +728 -0
  201. package/dist/tests/registry-providers/parity.test.js +189 -0
  202. package/dist/tests/registry-providers/skills-sh.test.js +309 -0
  203. package/dist/tests/registry-providers/static-index.test.js +204 -0
  204. package/dist/tests/registry-resolve.test.js +126 -0
  205. package/dist/tests/registry-search.test.js +723 -0
  206. package/dist/tests/remember-frontmatter.test.js +380 -0
  207. package/dist/tests/remember-unit.test.js +123 -0
  208. package/dist/tests/ripgrep-install.test.js +251 -0
  209. package/dist/tests/ripgrep-resolve.test.js +108 -0
  210. package/dist/tests/ripgrep.test.js +163 -0
  211. package/dist/tests/save-command.test.js +94 -0
  212. package/dist/tests/save-trust-qa-fixes.test.js +270 -0
  213. package/dist/tests/scoring-pipeline.test.js +648 -0
  214. package/dist/tests/search-include-proposed-cli.test.js +118 -0
  215. package/dist/tests/self-update.test.js +442 -0
  216. package/dist/tests/semantic-search-e2e.test.js +512 -0
  217. package/dist/tests/semantic-status.test.js +471 -0
  218. package/dist/tests/setup-run.integration.js +877 -0
  219. package/dist/tests/setup-wizard.test.js +198 -0
  220. package/dist/tests/setup.test.js +131 -0
  221. package/dist/tests/source-add.test.js +11 -0
  222. package/dist/tests/source-clone.test.js +254 -0
  223. package/dist/tests/source-manage.test.js +366 -0
  224. package/dist/tests/source-providers/filesystem.test.js +82 -0
  225. package/dist/tests/source-providers/git.test.js +252 -0
  226. package/dist/tests/source-providers/website.test.js +128 -0
  227. package/dist/tests/source-qa-fixes.test.js +268 -0
  228. package/dist/tests/source-registry.test.js +350 -0
  229. package/dist/tests/source-resolve.test.js +100 -0
  230. package/dist/tests/source-source.test.js +221 -0
  231. package/dist/tests/source.test.js +533 -0
  232. package/dist/tests/tar-utils-scan.test.js +73 -0
  233. package/dist/tests/toggle-components.test.js +73 -0
  234. package/dist/tests/usage-telemetry.test.js +265 -0
  235. package/dist/tests/utility-scoring.test.js +558 -0
  236. package/dist/tests/vault-load-error.test.js +78 -0
  237. package/dist/tests/vault-qa-fixes.test.js +194 -0
  238. package/dist/tests/vault.test.js +429 -0
  239. package/dist/tests/vector-search.test.js +608 -0
  240. package/dist/tests/walker.test.js +252 -0
  241. package/dist/tests/wave2-cluster-bc.test.js +228 -0
  242. package/dist/tests/wave2-cluster-d.test.js +180 -0
  243. package/dist/tests/wave2-cluster-e.test.js +179 -0
  244. package/dist/tests/wiki-qa-fixes.test.js +270 -0
  245. package/dist/tests/wiki.test.js +529 -0
  246. package/dist/tests/workflow-cli.test.js +271 -0
  247. package/dist/tests/workflow-markdown.test.js +171 -0
  248. package/dist/tests/workflow-path-escape.test.js +132 -0
  249. package/dist/tests/workflow-qa-fixes.test.js +377 -0
  250. package/dist/tests/workflows/indexer-rejection.test.js +213 -0
  251. package/docs/README.md +8 -0
  252. package/docs/migration/release-notes/0.7.0.md +244 -0
  253. package/package.json +2 -2
  254. package/dist/core/warn.js +0 -27
  255. package/dist/output/shapes.js +0 -212
  256. /package/dist/{commands → src/commands}/completions.js +0 -0
  257. /package/dist/{commands → src/commands}/curate.js +0 -0
  258. /package/dist/{commands → src/commands}/info.js +0 -0
  259. /package/dist/{commands → src/commands}/init.js +0 -0
  260. /package/dist/{commands → src/commands}/install-audit.js +0 -0
  261. /package/dist/{commands → src/commands}/migration-help.js +0 -0
  262. /package/dist/{commands → src/commands}/source-add.js +0 -0
  263. /package/dist/{commands → src/commands}/source-clone.js +0 -0
  264. /package/dist/{commands → src/commands}/source-manage.js +0 -0
  265. /package/dist/{commands → src/commands}/vault.js +0 -0
  266. /package/dist/{core → src/core}/asset-registry.js +0 -0
  267. /package/dist/{core → src/core}/frontmatter.js +0 -0
  268. /package/dist/{core → src/core}/markdown.js +0 -0
  269. /package/dist/{core → src/core}/paths.js +0 -0
  270. /package/dist/{indexer → src/indexer}/manifest.js +0 -0
  271. /package/dist/{indexer → src/indexer}/matchers.js +0 -0
  272. /package/dist/{indexer → src/indexer}/search-fields.js +0 -0
  273. /package/dist/{indexer → src/indexer}/search-source.js +0 -0
  274. /package/dist/{indexer → src/indexer}/semantic-status.js +0 -0
  275. /package/dist/{indexer → src/indexer}/usage-events.js +0 -0
  276. /package/dist/{indexer → src/indexer}/walker.js +0 -0
  277. /package/dist/{integrations → src/integrations}/github.js +0 -0
  278. /package/dist/{llm → src/llm}/embedder.js +0 -0
  279. /package/dist/{llm → src/llm}/embedders/cache.js +0 -0
  280. /package/dist/{llm → src/llm}/embedders/local.js +0 -0
  281. /package/dist/{llm → src/llm}/embedders/remote.js +0 -0
  282. /package/dist/{llm → src/llm}/embedders/types.js +0 -0
  283. /package/dist/{llm → src/llm}/metadata-enhance.js +0 -0
  284. /package/dist/{output → src/output}/cli-hints.js +0 -0
  285. /package/dist/{output → src/output}/context.js +0 -0
  286. /package/dist/{registry → src/registry}/create-provider-registry.js +0 -0
  287. /package/dist/{registry → src/registry}/origin-resolve.js +0 -0
  288. /package/dist/{registry → src/registry}/providers/index.js +0 -0
  289. /package/dist/{registry → src/registry}/providers/skills-sh.js +0 -0
  290. /package/dist/{registry → src/registry}/providers/types.js +0 -0
  291. /package/dist/{registry → src/registry}/types.js +0 -0
  292. /package/dist/{setup → src/setup}/detect.js +0 -0
  293. /package/dist/{setup → src/setup}/ripgrep-install.js +0 -0
  294. /package/dist/{setup → src/setup}/ripgrep-resolve.js +0 -0
  295. /package/dist/{setup → src/setup}/steps.js +0 -0
  296. /package/dist/{sources → src/sources}/include.js +0 -0
  297. /package/dist/{sources → src/sources}/provider-factory.js +0 -0
  298. /package/dist/{sources → src/sources}/provider.js +0 -0
  299. /package/dist/{sources → src/sources}/providers/filesystem.js +0 -0
  300. /package/dist/{sources → src/sources}/providers/index.js +0 -0
  301. /package/dist/{sources → src/sources}/providers/install-types.js +0 -0
  302. /package/dist/{sources → src/sources}/providers/npm.js +0 -0
  303. /package/dist/{sources → src/sources}/providers/provider-utils.js +0 -0
  304. /package/dist/{sources → src/sources}/providers/sync-from-ref.js +0 -0
  305. /package/dist/{sources → src/sources}/providers/tar-utils.js +0 -0
  306. /package/dist/{sources → src/sources}/providers/website.js +0 -0
  307. /package/dist/{sources → src/sources}/resolve.js +0 -0
  308. /package/dist/{sources → src/sources}/types.js +0 -0
  309. /package/dist/{templates → src/templates}/wiki-templates.js +0 -0
  310. /package/dist/{version.js → src/version.js} +0 -0
  311. /package/dist/{workflows → src/workflows}/authoring.js +0 -0
  312. /package/dist/{workflows → src/workflows}/cli.js +0 -0
  313. /package/dist/{workflows → src/workflows}/db.js +0 -0
  314. /package/dist/{workflows → src/workflows}/document-cache.js +0 -0
  315. /package/dist/{workflows → src/workflows}/parser.js +0 -0
  316. /package/dist/{workflows → src/workflows}/renderer.js +0 -0
  317. /package/dist/{workflows → src/workflows}/runs.js +0 -0
  318. /package/dist/{workflows → src/workflows}/schema.js +0 -0
  319. /package/dist/{workflows → src/workflows}/validator.js +0 -0
@@ -62,9 +62,10 @@ export function parseConfigValue(key, value) {
62
62
  case "security.installAudit.allowedFindings":
63
63
  return { security: { installAudit: { allowedFindings: parseAllowedFindingsValue(value, key) } } };
64
64
  default:
65
- throw new UsageError(`Unknown config key: ${key}`);
65
+ throw new UsageError(`Unknown config key: ${key}`, "INVALID_FLAG_VALUE", UNKNOWN_CONFIG_KEY_HINT);
66
66
  }
67
67
  }
68
+ 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.";
68
69
  export function getConfigValue(config, key) {
69
70
  switch (key) {
70
71
  case "stashDir":
@@ -114,7 +115,7 @@ export function getConfigValue(config, key) {
114
115
  case "security.installAudit.allowedFindings":
115
116
  return config.security?.installAudit?.allowedFindings ?? null;
116
117
  default:
117
- throw new UsageError(`Unknown config key: ${key}`);
118
+ throw new UsageError(`Unknown config key: ${key}`, "INVALID_FLAG_VALUE", UNKNOWN_CONFIG_KEY_HINT);
118
119
  }
119
120
  }
120
121
  export function setConfigValue(config, key, rawValue) {
@@ -168,7 +169,7 @@ export function setConfigValue(config, key, rawValue) {
168
169
  return { ...config, defaultWriteTarget: name };
169
170
  }
170
171
  default:
171
- throw new UsageError(`Unknown config key: ${key}`);
172
+ throw new UsageError(`Unknown config key: ${key}`, "INVALID_FLAG_VALUE", UNKNOWN_CONFIG_KEY_HINT);
172
173
  }
173
174
  }
174
175
  export function unsetConfigValue(config, key) {
@@ -241,7 +242,7 @@ export function unsetConfigValue(config, key) {
241
242
  }),
242
243
  };
243
244
  default:
244
- throw new UsageError(`Unknown or unsupported unset key: ${key}`);
245
+ throw new UsageError(`Unknown or unsupported unset key: ${key}`, "INVALID_FLAG_VALUE", UNKNOWN_CONFIG_KEY_HINT);
245
246
  }
246
247
  }
247
248
  export function listConfig(config) {
@@ -0,0 +1,283 @@
1
+ /**
2
+ * `akm distill <ref>` — feedback distillation into lesson proposals (#228).
3
+ *
4
+ * The command reads a target asset and any recent feedback events about it,
5
+ * asks an LLM to distil a *lesson* (per v1 spec §13) the agent should
6
+ * remember next time, and queues the result as a {@link Proposal} (source
7
+ * `"distill"`). The proposal queue is the *only* path to a live asset — this
8
+ * command never mutates source files directly. Acceptance is a human (or
9
+ * automated) decision via `akm proposal accept`.
10
+ *
11
+ * # Architectural seams
12
+ *
13
+ * - **Single bounded in-tree LLM call.** Wrapped in {@link tryLlmFeature}
14
+ * under the `feedback_distillation` gate (v1 spec §14). The wrapper
15
+ * enforces the 30 s hard timeout and converts disable / throw / timeout
16
+ * into a `null` return from `fn`, which we treat as a graceful
17
+ * "skipped" outcome (exit 0, no proposal, `distill_invoked` event with
18
+ * `outcome: "skipped"`).
19
+ * - **Stateless.** No module-level state — every callable is a pure
20
+ * function of its arguments and an injectable `chat` seam. The
21
+ * architecture seam test (`tests/architecture/llm-stateless-seam.test.ts`)
22
+ * applies.
23
+ * - **Output substrate.** Proposal creation goes through the `proposals`
24
+ * module so distill shares its persistence + validation pipeline with
25
+ * `akm reflect` / `akm propose`. Validation failures (LLM returned a
26
+ * lesson without required `description` / `when_to_use` frontmatter) are
27
+ * a *different* graceful path: no proposal is created, the structured
28
+ * error is surfaced, and the command exits non-zero.
29
+ *
30
+ * # Lesson-name derivation rule
31
+ *
32
+ * The proposed lesson ref is `lesson:<original-ref-slug>-lesson`, where
33
+ * `<original-ref-slug>` is `<type>-<name>` from the parsed input ref (so
34
+ * `skill:deploy` → `lesson:skill-deploy-lesson`, and `team//memory:auth-tips`
35
+ * → `lesson:memory-auth-tips-lesson`). Origin prefixes are dropped from the
36
+ * derived name so two sources with the same asset-type/name collapse onto
37
+ * the same lesson queue entry rather than each generating its own — the
38
+ * proposal queue tolerates duplicate refs (id is a UUID), so the human
39
+ * reviewer can decide which one to accept.
40
+ *
41
+ * # Why we do not call `runAgent`
42
+ *
43
+ * Distillation is in-tree per the v1 spec ("bounded in-tree LLM call"). The
44
+ * agent dispatch path is a heavier shell-out used by the curator/agent
45
+ * surfaces — distill must be cheap, deterministic-ish, and bounded so it can
46
+ * be invoked from CI / automation without spinning up an agent harness.
47
+ */
48
+ import fs from "node:fs";
49
+ import { parseAssetRef } from "../core/asset-ref";
50
+ import { resolveStashDir } from "../core/common";
51
+ import { loadConfig } from "../core/config";
52
+ import { ConfigError, UsageError } from "../core/errors";
53
+ import { appendEvent, readEvents } from "../core/events";
54
+ import { parseFrontmatter } from "../core/frontmatter";
55
+ import { lintLessonContent } from "../core/lesson-lint";
56
+ import { createProposal } from "../core/proposals";
57
+ import { lookup as indexerLookup } from "../indexer/indexer";
58
+ import { chatCompletion } from "../llm/client";
59
+ import { tryLlmFeature } from "../llm/feature-gate";
60
+ // ── Lesson-ref derivation ───────────────────────────────────────────────────
61
+ /** Derive the proposed lesson ref from the input ref. See module docblock. */
62
+ export function deriveLessonRef(inputRef) {
63
+ const parsed = parseAssetRef(inputRef);
64
+ // Strip origin: a feedback signal recorded against `team//skill:deploy`
65
+ // distils into the same lesson namespace as `skill:deploy`. The proposal
66
+ // id (a UUID) keeps the queue entries distinct, so collisions are not a
67
+ // problem — and reviewers want to see them next to each other anyway.
68
+ const slug = `${parsed.type}-${parsed.name}`.toLowerCase();
69
+ // Replace anything outside the canonical asset-name charset with `-`. Keep
70
+ // it deterministic so re-runs produce the same ref.
71
+ const safe = slug
72
+ .replace(/[^a-z0-9-]+/g, "-")
73
+ .replace(/-+/g, "-")
74
+ .replace(/^-|-$/g, "");
75
+ return `lesson:${safe}-lesson`;
76
+ }
77
+ // ── Prompt assembly ─────────────────────────────────────────────────────────
78
+ const SYSTEM_PROMPT = [
79
+ "You are the akm `distill` distiller.",
80
+ "Given an asset and recent feedback events about it, produce a single",
81
+ "concise *lesson* an agent should remember next time it works on this",
82
+ "asset's domain.",
83
+ "",
84
+ "Output MUST be a complete markdown file with YAML frontmatter:",
85
+ " ---",
86
+ " description: <one-line summary of what the lesson teaches>",
87
+ " when_to_use: <one-line trigger that should make a caller apply it>",
88
+ " ---",
89
+ "",
90
+ " <lesson body, plain markdown, 1–3 short paragraphs>",
91
+ "",
92
+ "Both `description` and `when_to_use` MUST be non-empty single-line strings.",
93
+ "Output ONLY the lesson file contents — no prose, no fences, no preamble.",
94
+ ].join("\n");
95
+ /** Pure: build the user-prompt body. Exported for tests. */
96
+ export function buildDistillPrompt(input) {
97
+ const lines = [];
98
+ lines.push(`Asset ref: ${input.inputRef}`);
99
+ lines.push("");
100
+ lines.push("Asset content:");
101
+ if (input.assetContent) {
102
+ lines.push("```");
103
+ lines.push(input.assetContent.trim());
104
+ lines.push("```");
105
+ }
106
+ else {
107
+ lines.push("(asset is not currently indexed; distil from feedback signal alone)");
108
+ }
109
+ lines.push("");
110
+ lines.push("Recent feedback events (most recent last):");
111
+ if (input.feedback.length === 0) {
112
+ lines.push("(no feedback events recorded — distil from the asset itself)");
113
+ }
114
+ else {
115
+ for (const event of input.feedback) {
116
+ const meta = event.metadata ? ` ${JSON.stringify(event.metadata)}` : "";
117
+ lines.push(`- ${event.ts} ${event.eventType}${meta}`);
118
+ }
119
+ }
120
+ lines.push("");
121
+ lines.push("Produce the lesson markdown file now.");
122
+ return lines.join("\n");
123
+ }
124
+ // ── Main entry point ────────────────────────────────────────────────────────
125
+ /**
126
+ * Run a single bounded distillation pass for `ref`. Always emits exactly one
127
+ * `distill_invoked` event (with `outcome` in the metadata) regardless of the
128
+ * branch taken — so observers can count invocations cheaply.
129
+ */
130
+ export async function akmDistill(options) {
131
+ const inputRef = options.ref.trim();
132
+ if (!inputRef) {
133
+ throw new UsageError("Asset ref is required. Usage: akm distill <ref>", "MISSING_REQUIRED_ARGUMENT");
134
+ }
135
+ // Validate the ref shape up front so a typo never reaches the LLM.
136
+ parseAssetRef(inputRef);
137
+ const lessonRef = deriveLessonRef(inputRef);
138
+ const config = options.config ?? loadConfig();
139
+ const stash = options.stashDir ?? resolveStashDir();
140
+ const chat = options.chat ?? chatCompletion;
141
+ const lookup = options.lookupFn ?? defaultLookup;
142
+ const readEventsImpl = options.readEventsFn ?? readEvents;
143
+ // Best-effort load: when the asset is not yet indexed we still proceed —
144
+ // the LLM is asked to distil from "available signal" (feedback alone).
145
+ let assetContent = null;
146
+ try {
147
+ const filePath = await lookup(inputRef);
148
+ if (filePath && fs.existsSync(filePath)) {
149
+ assetContent = fs.readFileSync(filePath, "utf8");
150
+ }
151
+ }
152
+ catch {
153
+ assetContent = null;
154
+ }
155
+ const { events } = readEventsImpl({ ref: inputRef, type: "feedback" });
156
+ // #267 — feedback exclusion. Filter events whose `ref` matches the
157
+ // exclusion list BEFORE the prompt is built. The original event stream
158
+ // is never mutated; only the `feedback` slice that reaches the LLM is
159
+ // affected. The exclusion set is normalised through `parseAssetRef` →
160
+ // re-serialised so callers can pass canonical or origin-prefixed refs
161
+ // and the comparison still works against the event payload's `ref`.
162
+ const exclusionList = options.excludeFeedbackFromRefs ?? [];
163
+ const exclusionSet = new Set(exclusionList.map((ref) => ref.trim()).filter((ref) => ref.length > 0));
164
+ const originalEventCount = events.length;
165
+ const filteredEvents = exclusionSet.size > 0 ? events.filter((e) => !(e.ref !== undefined && exclusionSet.has(e.ref))) : events;
166
+ const filteredFeedbackCount = originalEventCount - filteredEvents.length;
167
+ const feedbackFullyFiltered = exclusionSet.size > 0 && originalEventCount > 0 && filteredEvents.length === 0;
168
+ const feedback = filteredEvents.slice(-20).map((e) => ({
169
+ ts: e.ts,
170
+ eventType: e.eventType,
171
+ ...(e.metadata !== undefined ? { metadata: e.metadata } : {}),
172
+ }));
173
+ const userPrompt = buildDistillPrompt({ inputRef, assetContent, feedback });
174
+ const messages = [
175
+ { role: "system", content: SYSTEM_PROMPT },
176
+ { role: "user", content: userPrompt },
177
+ ];
178
+ // Single bounded LLM call. The wrapper handles the gate-check, 30 s
179
+ // timeout, and error fallback (returning `null`).
180
+ const raw = await tryLlmFeature("feedback_distillation", config, async () => {
181
+ if (!config.llm) {
182
+ // No LLM connection configured — treat as gate-disabled. Throwing
183
+ // here lets `tryLlmFeature` route us through the "error" fallback,
184
+ // which is the same graceful skipped path.
185
+ throw new ConfigError("No LLM connection configured. Set `llm.endpoint` and `llm.model` in the akm config.", "LLM_NOT_CONFIGURED");
186
+ }
187
+ return chat(config.llm, messages);
188
+ }, null);
189
+ if (raw === null || raw.trim() === "") {
190
+ appendEvent({
191
+ eventType: "distill_invoked",
192
+ ref: inputRef,
193
+ metadata: {
194
+ outcome: "skipped",
195
+ lessonRef,
196
+ ...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
197
+ },
198
+ });
199
+ return {
200
+ schemaVersion: 1,
201
+ ok: true,
202
+ outcome: "skipped",
203
+ inputRef,
204
+ lessonRef,
205
+ message: "feedback distillation is disabled or the LLM call failed; no proposal created.",
206
+ ...(exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
207
+ };
208
+ }
209
+ // Strip any stray fence the LLM might have added around the markdown.
210
+ const content = stripMarkdownFences(raw);
211
+ // Parse + lint the lesson before creating the proposal. The lint is the
212
+ // canonical gate for required frontmatter (v1 spec §13). On failure we
213
+ // surface a structured error and exit non-zero — but still emit
214
+ // `distill_invoked` so the failure is observable.
215
+ const lintReport = lintLessonContent(content, `distill:${inputRef}`);
216
+ if (lintReport.findings.length > 0) {
217
+ appendEvent({
218
+ eventType: "distill_invoked",
219
+ ref: inputRef,
220
+ metadata: {
221
+ outcome: "validation_failed",
222
+ lessonRef,
223
+ findingKinds: lintReport.findings.map((f) => f.kind),
224
+ ...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
225
+ },
226
+ });
227
+ const message = lintReport.findings.map((f) => f.message).join("\n");
228
+ throw new UsageError(`Distilled lesson failed validation:\n${message}`, "MISSING_REQUIRED_ARGUMENT", "Lessons require non-empty `description` and `when_to_use` frontmatter fields. See v1 spec §13.");
229
+ }
230
+ // Round-trip the parsed frontmatter so the proposal carries it as a
231
+ // structured payload alongside the raw content (matches the shape used by
232
+ // other proposal sources).
233
+ const parsed = parseFrontmatter(content);
234
+ const proposal = createProposal(stash, {
235
+ ref: lessonRef,
236
+ source: "distill",
237
+ ...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
238
+ payload: {
239
+ content,
240
+ ...(Object.keys(parsed.data).length > 0 ? { frontmatter: parsed.data } : {}),
241
+ },
242
+ }, options.ctx);
243
+ appendEvent({
244
+ eventType: "distill_invoked",
245
+ ref: inputRef,
246
+ metadata: {
247
+ outcome: "queued",
248
+ lessonRef,
249
+ proposalId: proposal.id,
250
+ ...(options.sourceRun !== undefined ? { sourceRun: options.sourceRun } : {}),
251
+ ...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
252
+ },
253
+ });
254
+ return {
255
+ schemaVersion: 1,
256
+ ok: true,
257
+ outcome: "queued",
258
+ inputRef,
259
+ lessonRef,
260
+ proposalId: proposal.id,
261
+ proposal,
262
+ ...(exclusionSet.size > 0 ? { filteredFeedbackCount, feedbackFullyFiltered } : {}),
263
+ };
264
+ }
265
+ // ── Helpers ─────────────────────────────────────────────────────────────────
266
+ async function defaultLookup(ref) {
267
+ try {
268
+ const entry = await indexerLookup(parseAssetRef(ref));
269
+ return entry?.filePath ?? null;
270
+ }
271
+ catch {
272
+ return null;
273
+ }
274
+ }
275
+ /** Best-effort fence stripping. Keeps the body intact when no fence is present. */
276
+ function stripMarkdownFences(raw) {
277
+ const trimmed = raw.trim();
278
+ // Only strip outer triple-fence pairs — leave inner code blocks alone.
279
+ const fence = trimmed.match(/^```(?:markdown|md)?\s*\n([\s\S]*?)\n```\s*$/i);
280
+ if (fence)
281
+ return fence[1].trim();
282
+ return trimmed;
283
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * `akm events list` and `akm events tail` (#204).
3
+ *
4
+ * Programmatic surface — the CLI dispatcher in `src/cli.ts` registers two
5
+ * verbs that delegate here. Both return JSON envelopes shaped by
6
+ * `src/output/shapes.ts` so the output flows through the same shape and
7
+ * text-renderer pipeline as the rest of the CLI (no silent
8
+ * `JSON.stringify` fallback).
9
+ */
10
+ import { parseAssetRef } from "../core/asset-ref";
11
+ import { UsageError } from "../core/errors";
12
+ import { readEvents, tailEvents } from "../core/events";
13
+ /**
14
+ * Parse `--since` accepting either a byte-offset cursor (`@offset:<int>`) for
15
+ * cross-process resumption, or a timestamp / epoch-ms (the existing form).
16
+ * Returns one of `{ sinceOffset }` or `{ since }`.
17
+ */
18
+ function parseSinceFlag(since) {
19
+ if (since === undefined)
20
+ return {};
21
+ const trimmed = since.trim();
22
+ if (!trimmed) {
23
+ throw new UsageError("--since cannot be empty.", "INVALID_FLAG_VALUE");
24
+ }
25
+ if (trimmed.startsWith("@offset:")) {
26
+ const raw = trimmed.slice("@offset:".length);
27
+ const value = Number.parseInt(raw, 10);
28
+ if (Number.isNaN(value) || value < 0) {
29
+ throw new UsageError(`Invalid --since byte offset: "${since}". Expected @offset:<non-negative integer>.`, "INVALID_FLAG_VALUE");
30
+ }
31
+ return { sinceOffset: value };
32
+ }
33
+ return { since: normalizeSince(trimmed) };
34
+ }
35
+ function validateRef(ref) {
36
+ if (ref === undefined)
37
+ return undefined;
38
+ const trimmed = ref.trim();
39
+ if (!trimmed) {
40
+ throw new UsageError("--ref cannot be empty.", "INVALID_FLAG_VALUE");
41
+ }
42
+ parseAssetRef(trimmed);
43
+ return trimmed;
44
+ }
45
+ function normalizeSince(since) {
46
+ if (since === undefined)
47
+ return undefined;
48
+ const trimmed = since.trim();
49
+ if (!trimmed) {
50
+ throw new UsageError("--since cannot be empty.", "INVALID_FLAG_VALUE");
51
+ }
52
+ // Accept ISO timestamp (preferred), epoch ms, or plain date.
53
+ if (/^\d+$/.test(trimmed)) {
54
+ const ms = Number.parseInt(trimmed, 10);
55
+ const d = new Date(ms);
56
+ if (Number.isNaN(d.getTime())) {
57
+ throw new UsageError(`Invalid --since value: ${since}`, "INVALID_FLAG_VALUE");
58
+ }
59
+ return d.toISOString();
60
+ }
61
+ const parsed = new Date(trimmed);
62
+ if (Number.isNaN(parsed.getTime())) {
63
+ throw new UsageError(`Invalid --since value: ${since}. Expected ISO timestamp (e.g. 2026-04-01T00:00:00Z) or epoch ms.`, "INVALID_FLAG_VALUE");
64
+ }
65
+ return parsed.toISOString();
66
+ }
67
+ export function akmEventsList(options = {}) {
68
+ const ref = validateRef(options.ref);
69
+ const parsed = parseSinceFlag(options.since);
70
+ const result = readEvents({ since: parsed.since, sinceOffset: parsed.sinceOffset, type: options.type, ref }, options.ctx);
71
+ return {
72
+ schemaVersion: 1,
73
+ totalCount: result.events.length,
74
+ ...(ref !== undefined ? { ref } : {}),
75
+ ...(options.type !== undefined ? { type: options.type } : {}),
76
+ ...(parsed.since !== undefined ? { since: parsed.since } : {}),
77
+ ...(parsed.sinceOffset !== undefined ? { sinceOffset: parsed.sinceOffset } : {}),
78
+ nextOffset: result.nextOffset,
79
+ events: result.events,
80
+ };
81
+ }
82
+ export async function akmEventsTail(options = {}) {
83
+ const ref = validateRef(options.ref);
84
+ const parsed = parseSinceFlag(options.since);
85
+ const tailOptions = {
86
+ since: parsed.since,
87
+ sinceOffset: parsed.sinceOffset,
88
+ type: options.type,
89
+ ref,
90
+ intervalMs: options.intervalMs,
91
+ maxDurationMs: options.maxDurationMs,
92
+ maxEvents: options.maxEvents,
93
+ signal: options.signal,
94
+ onEvent: options.onEvent,
95
+ };
96
+ const result = await tailEvents(tailOptions, options.ctx);
97
+ return {
98
+ schemaVersion: 1,
99
+ totalCount: result.events.length,
100
+ ...(ref !== undefined ? { ref } : {}),
101
+ ...(options.type !== undefined ? { type: options.type } : {}),
102
+ ...(parsed.since !== undefined ? { since: parsed.since } : {}),
103
+ ...(parsed.sinceOffset !== undefined ? { sinceOffset: parsed.sinceOffset } : {}),
104
+ nextOffset: result.nextOffset,
105
+ events: result.events,
106
+ reason: result.reason,
107
+ };
108
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * `akm history` — surfaces internal mutation/usage events captured in the
3
+ * `usage_events` SQLite table for a single asset (`--ref`) or stash-wide.
4
+ *
5
+ * Backed by `usage_events` (search/show/feedback today). Richer per-asset
6
+ * lifecycle entries (add/edit/delete) require the events stream introduced
7
+ * in #204; this command surfaces whatever the indexer has captured so
8
+ * downstream tooling stops reinventing audit trails.
9
+ */
10
+ import { parseAssetRef } from "../core/asset-ref";
11
+ import { UsageError } from "../core/errors";
12
+ import { closeDatabase, openDatabase } from "../indexer/db";
13
+ import { ensureUsageEventsSchema } from "../indexer/usage-events";
14
+ // ── Helpers ──────────────────────────────────────────────────────────────────
15
+ function normalizeSince(since) {
16
+ // Accept "YYYY-MM-DD", "YYYY-MM-DDTHH:MM:SSZ", epoch ms, or anything Date can parse.
17
+ const trimmed = since.trim();
18
+ if (!trimmed) {
19
+ throw new UsageError("--since cannot be empty.", "INVALID_FLAG_VALUE");
20
+ }
21
+ // Pure-digit input → epoch milliseconds
22
+ if (/^\d+$/.test(trimmed)) {
23
+ const ms = Number.parseInt(trimmed, 10);
24
+ const d = new Date(ms);
25
+ if (Number.isNaN(d.getTime())) {
26
+ throw new UsageError(`Invalid --since value: ${since}`, "INVALID_FLAG_VALUE");
27
+ }
28
+ return d
29
+ .toISOString()
30
+ .replace("T", " ")
31
+ .replace(/\.\d+Z$/, "");
32
+ }
33
+ const parsed = new Date(trimmed);
34
+ if (Number.isNaN(parsed.getTime())) {
35
+ throw new UsageError(`Invalid --since value: ${since}. Expected ISO timestamp (e.g. 2026-04-01T00:00:00Z) or epoch ms.`, "INVALID_FLAG_VALUE");
36
+ }
37
+ // Match the "YYYY-MM-DD HH:MM:SS" format SQLite's datetime('now') stores.
38
+ return parsed
39
+ .toISOString()
40
+ .replace("T", " ")
41
+ .replace(/\.\d+Z$/, "");
42
+ }
43
+ function parseMetadata(raw) {
44
+ if (!raw)
45
+ return null;
46
+ try {
47
+ return JSON.parse(raw);
48
+ }
49
+ catch {
50
+ return raw;
51
+ }
52
+ }
53
+ function toEntry(row) {
54
+ return {
55
+ id: row.id,
56
+ eventType: row.event_type,
57
+ ref: row.entry_ref,
58
+ entryId: row.entry_id,
59
+ query: row.query,
60
+ signal: row.signal,
61
+ metadata: parseMetadata(row.metadata),
62
+ createdAt: row.created_at,
63
+ };
64
+ }
65
+ // ── Main ─────────────────────────────────────────────────────────────────────
66
+ /**
67
+ * Read mutation/usage history. When `ref` is provided, results are filtered to
68
+ * that asset (validated via `parseAssetRef`). Always returns chronological
69
+ * order (oldest first) so consumers can display a lifecycle trail.
70
+ */
71
+ export async function akmHistory(options = {}) {
72
+ let normalizedRef;
73
+ if (options.ref !== undefined) {
74
+ const trimmed = options.ref.trim();
75
+ if (!trimmed) {
76
+ throw new UsageError("--ref cannot be empty.", "INVALID_FLAG_VALUE");
77
+ }
78
+ // Validate the ref grammar; we still query by exact entry_ref string so
79
+ // the user gets back exactly what they asked for.
80
+ parseAssetRef(trimmed);
81
+ normalizedRef = trimmed;
82
+ }
83
+ const sinceNormalized = options.since !== undefined ? normalizeSince(options.since) : undefined;
84
+ const db = options.db ?? openDatabase();
85
+ const ownsDb = options.db === undefined;
86
+ try {
87
+ // The schema is normally created during `akm index`; ensure it exists so
88
+ // `akm history` works on a freshly-initialised stash that has never been
89
+ // indexed (and just returns an empty list rather than an error).
90
+ ensureUsageEventsSchema(db);
91
+ const conditions = [];
92
+ const params = [];
93
+ if (normalizedRef !== undefined) {
94
+ conditions.push("entry_ref = ?");
95
+ params.push(normalizedRef);
96
+ }
97
+ if (sinceNormalized !== undefined) {
98
+ conditions.push("created_at >= ?");
99
+ params.push(sinceNormalized);
100
+ }
101
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
102
+ const sql = `SELECT id, event_type, query, entry_id, entry_ref, signal, metadata, created_at
103
+ FROM usage_events ${where}
104
+ ORDER BY id ASC`;
105
+ const rows = db.prepare(sql).all(...params);
106
+ const entries = rows.map(toEntry);
107
+ const response = {
108
+ schemaVersion: 1,
109
+ ...(normalizedRef !== undefined ? { ref: normalizedRef } : {}),
110
+ ...(sinceNormalized !== undefined ? { since: sinceNormalized } : {}),
111
+ totalCount: entries.length,
112
+ entries,
113
+ };
114
+ return response;
115
+ }
116
+ finally {
117
+ if (ownsDb)
118
+ closeDatabase(db);
119
+ }
120
+ }
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import fs from "node:fs";
8
8
  import path from "node:path";
9
- import { resolveStashDir } from "../core/common";
9
+ import { isWithin, resolveStashDir } from "../core/common";
10
10
  import { loadConfig } from "../core/config";
11
11
  import { NotFoundError, UsageError } from "../core/errors";
12
12
  import { akmIndex } from "../indexer/indexer";
@@ -14,6 +14,7 @@ import { removeLockEntry, upsertLockEntry } from "../integrations/lockfile";
14
14
  import { parseRegistryRef } from "../registry/resolve";
15
15
  import { syncFromRef } from "../sources/providers/sync-from-ref";
16
16
  import { ensureWebsiteMirror } from "../sources/providers/website";
17
+ import { listWikis, resolveWikisRoot } from "../wiki/wiki";
17
18
  import { auditInstallCandidate, deriveRegistryLabels, enforceRegistryInstallPolicy, formatInstallAuditFailure, } from "./install-audit";
18
19
  import { removeInstalledRegistryEntry, upsertInstalledRegistryEntry } from "./source-add";
19
20
  import { removeStash } from "./source-manage";
@@ -57,6 +58,31 @@ export async function akmListSources(input) {
57
58
  status: { exists: directoryExists(entry.stashRoot) },
58
59
  });
59
60
  }
61
+ if (!kindFilter || kindFilter.includes("filesystem")) {
62
+ const wikisRoot = resolveWikisRoot(stashDir);
63
+ const seenPaths = new Set(sources
64
+ .map((source) => source.path)
65
+ .filter((sourcePath) => typeof sourcePath === "string")
66
+ .map((sourcePath) => path.resolve(sourcePath)));
67
+ for (const wiki of listWikis(stashDir)) {
68
+ // `listWikis()` also includes externally-registered wikis. `akm list`
69
+ // should synthesize source entries here only for stash-owned wiki dirs.
70
+ if (!isWithin(wiki.path, wikisRoot))
71
+ continue;
72
+ const resolvedPath = path.resolve(wiki.path);
73
+ if (seenPaths.has(resolvedPath))
74
+ continue;
75
+ seenPaths.add(resolvedPath);
76
+ sources.push({
77
+ name: wiki.name,
78
+ kind: "filesystem",
79
+ wiki: wiki.name,
80
+ path: wiki.path,
81
+ writable: true,
82
+ status: { exists: directoryExists(wiki.path) },
83
+ });
84
+ }
85
+ }
60
86
  return {
61
87
  schemaVersion: 1,
62
88
  stashDir,
@@ -307,7 +333,7 @@ function selectTargets(installed, target, all) {
307
333
  }
308
334
  throw new UsageError(`"${target}" is a local directory — it reflects your files in place. To refresh the search index, run: akm index`, "TARGET_NOT_UPDATABLE");
309
335
  }
310
- throw new NotFoundError(`No matching source for target: ${target}`);
336
+ throw new NotFoundError(`No matching source for target: ${target}`, "SOURCE_NOT_FOUND");
311
337
  }
312
338
  function tryResolveInstalledTarget(installed, target) {
313
339
  const byId = installed.find((entry) => entry.id === target);