akm-cli 0.7.0-rc1 → 0.7.1

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 (314) hide show
  1. package/dist/{src/cli.js → cli.js} +100 -16
  2. package/dist/{src/commands → commands}/config-cli.js +42 -0
  3. package/dist/{src/commands → commands}/history.js +78 -7
  4. package/dist/{src/commands → commands}/registry-search.js +69 -6
  5. package/dist/{src/commands → commands}/search.js +30 -3
  6. package/dist/{src/commands → commands}/show.js +29 -0
  7. package/dist/{src/commands → commands}/source-add.js +5 -1
  8. package/dist/{src/commands → commands}/source-manage.js +7 -1
  9. package/dist/{src/core → core}/config.js +28 -0
  10. package/dist/{src/indexer → indexer}/db-search.js +1 -0
  11. package/dist/{src/indexer → indexer}/indexer.js +16 -2
  12. package/dist/{src/indexer → indexer}/matchers.js +1 -1
  13. package/dist/{src/indexer → indexer}/search-source.js +4 -2
  14. package/dist/{src/integrations → integrations}/agent/profiles.js +1 -1
  15. package/dist/{src/integrations → integrations}/agent/spawn.js +67 -16
  16. package/dist/{src/integrations → integrations}/github.js +9 -3
  17. package/dist/{src/llm → llm}/embedders/remote.js +37 -3
  18. package/dist/{src/output → output}/cli-hints.js +15 -2
  19. package/dist/{src/output → output}/renderers.js +3 -1
  20. package/dist/{src/output → output}/shapes.js +8 -1
  21. package/dist/{src/output → output}/text.js +156 -3
  22. package/dist/{src/registry → registry}/build-index.js +5 -4
  23. package/dist/{src/registry → registry}/providers/static-index.js +3 -1
  24. package/dist/{src/setup → setup}/setup.js +9 -0
  25. package/dist/{src/wiki → wiki}/wiki.js +54 -6
  26. package/dist/{src/workflows → workflows}/runs.js +37 -3
  27. package/package.json +8 -8
  28. package/dist/tests/add-website-source.test.js +0 -119
  29. package/dist/tests/agent/agent-config-loader.test.js +0 -70
  30. package/dist/tests/agent/agent-config.test.js +0 -221
  31. package/dist/tests/agent/agent-detect.test.js +0 -100
  32. package/dist/tests/agent/agent-spawn.test.js +0 -234
  33. package/dist/tests/agent-output.test.js +0 -186
  34. package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +0 -103
  35. package/dist/tests/architecture/agent-spawn-seam.test.js +0 -193
  36. package/dist/tests/architecture/llm-stateless-seam.test.js +0 -112
  37. package/dist/tests/asset-ref.test.js +0 -192
  38. package/dist/tests/asset-registry.test.js +0 -103
  39. package/dist/tests/asset-spec.test.js +0 -241
  40. package/dist/tests/bench/attribution.test.js +0 -995
  41. package/dist/tests/bench/cleanup-sigint.test.js +0 -83
  42. package/dist/tests/bench/cleanup.js +0 -203
  43. package/dist/tests/bench/cleanup.test.js +0 -166
  44. package/dist/tests/bench/cli.js +0 -683
  45. package/dist/tests/bench/cli.test.js +0 -177
  46. package/dist/tests/bench/compare.test.js +0 -556
  47. package/dist/tests/bench/corpus.js +0 -314
  48. package/dist/tests/bench/corpus.test.js +0 -258
  49. package/dist/tests/bench/driver.js +0 -346
  50. package/dist/tests/bench/driver.test.js +0 -443
  51. package/dist/tests/bench/evolve-metrics.js +0 -179
  52. package/dist/tests/bench/evolve-metrics.test.js +0 -187
  53. package/dist/tests/bench/evolve.js +0 -580
  54. package/dist/tests/bench/evolve.test.js +0 -616
  55. package/dist/tests/bench/failure-modes.test.js +0 -300
  56. package/dist/tests/bench/feedback-integrity.test.js +0 -456
  57. package/dist/tests/bench/leakage.test.js +0 -125
  58. package/dist/tests/bench/learning-curve.test.js +0 -133
  59. package/dist/tests/bench/metrics.js +0 -2319
  60. package/dist/tests/bench/metrics.test.js +0 -1144
  61. package/dist/tests/bench/no-os-tmpdir-invariant.test.js +0 -43
  62. package/dist/tests/bench/report.js +0 -1821
  63. package/dist/tests/bench/report.test.js +0 -989
  64. package/dist/tests/bench/runner.js +0 -536
  65. package/dist/tests/bench/runner.test.js +0 -958
  66. package/dist/tests/bench/search-bridge.test.js +0 -331
  67. package/dist/tests/bench/tmp.js +0 -41
  68. package/dist/tests/bench/trajectory.js +0 -116
  69. package/dist/tests/bench/trajectory.test.js +0 -127
  70. package/dist/tests/bench/verifier.js +0 -109
  71. package/dist/tests/bench/verifier.test.js +0 -118
  72. package/dist/tests/bench/workflow-evaluator.js +0 -557
  73. package/dist/tests/bench/workflow-evaluator.test.js +0 -421
  74. package/dist/tests/bench/workflow-spec.js +0 -358
  75. package/dist/tests/bench/workflow-spec.test.js +0 -363
  76. package/dist/tests/bench/workflow-trace.js +0 -438
  77. package/dist/tests/bench/workflow-trace.test.js +0 -254
  78. package/dist/tests/benchmark-search-quality.js +0 -536
  79. package/dist/tests/benchmark-suite.js +0 -1441
  80. package/dist/tests/capture-cli.test.js +0 -112
  81. package/dist/tests/cli-errors.test.js +0 -203
  82. package/dist/tests/commands/events.test.js +0 -370
  83. package/dist/tests/commands/history.test.js +0 -223
  84. package/dist/tests/commands/import.test.js +0 -103
  85. package/dist/tests/commands/proposal-cli.test.js +0 -209
  86. package/dist/tests/commands/reflect-propose-cli.test.js +0 -333
  87. package/dist/tests/commands/remember.test.js +0 -97
  88. package/dist/tests/commands/scope-flags.test.js +0 -300
  89. package/dist/tests/commands/search.test.js +0 -537
  90. package/dist/tests/commands/show-indexer-parity.test.js +0 -117
  91. package/dist/tests/commands/show.test.js +0 -294
  92. package/dist/tests/common.test.js +0 -266
  93. package/dist/tests/completions.test.js +0 -142
  94. package/dist/tests/config-cli.test.js +0 -193
  95. package/dist/tests/config-llm-features.test.js +0 -139
  96. package/dist/tests/config.test.js +0 -544
  97. package/dist/tests/contracts/migration-baseline.test.js +0 -43
  98. package/dist/tests/contracts/reflect-propose-envelope.test.js +0 -139
  99. package/dist/tests/contracts/spec-helpers.js +0 -46
  100. package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +0 -228
  101. package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +0 -56
  102. package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +0 -34
  103. package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +0 -94
  104. package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +0 -39
  105. package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +0 -44
  106. package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +0 -47
  107. package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +0 -40
  108. package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +0 -58
  109. package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +0 -34
  110. package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +0 -75
  111. package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +0 -36
  112. package/dist/tests/core/write-source.test.js +0 -366
  113. package/dist/tests/curate-command.test.js +0 -87
  114. package/dist/tests/db-scoring.test.js +0 -201
  115. package/dist/tests/db.test.js +0 -654
  116. package/dist/tests/distill-cli-flag.test.js +0 -208
  117. package/dist/tests/distill.test.js +0 -515
  118. package/dist/tests/docker-install.test.js +0 -120
  119. package/dist/tests/e2e.test.js +0 -1398
  120. package/dist/tests/embedder.test.js +0 -340
  121. package/dist/tests/embedding-model-config.test.js +0 -379
  122. package/dist/tests/feedback-command.test.js +0 -172
  123. package/dist/tests/file-context.test.js +0 -552
  124. package/dist/tests/fixtures/scripts/git/summarize-diff.js +0 -9
  125. package/dist/tests/fixtures/scripts/lint/eslint-check.js +0 -7
  126. package/dist/tests/fixtures/stashes/load.js +0 -166
  127. package/dist/tests/fixtures/stashes/load.test.js +0 -88
  128. package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +0 -12
  129. package/dist/tests/frontmatter.test.js +0 -190
  130. package/dist/tests/fts-field-weighting.test.js +0 -254
  131. package/dist/tests/fuzzy-search.test.js +0 -230
  132. package/dist/tests/git-provider-clone.test.js +0 -45
  133. package/dist/tests/github.test.js +0 -161
  134. package/dist/tests/graph-boost-ranking.test.js +0 -305
  135. package/dist/tests/graph-extraction.test.js +0 -282
  136. package/dist/tests/helpers/usage-events.js +0 -8
  137. package/dist/tests/index-pass-llm.test.js +0 -161
  138. package/dist/tests/indexer.test.js +0 -559
  139. package/dist/tests/info-command.test.js +0 -166
  140. package/dist/tests/init.test.js +0 -69
  141. package/dist/tests/install-script.test.js +0 -246
  142. package/dist/tests/integration/agent-real-profile.test.js +0 -94
  143. package/dist/tests/issue-36-repro.test.js +0 -304
  144. package/dist/tests/issues-191-194.test.js +0 -160
  145. package/dist/tests/lesson-lint.test.js +0 -111
  146. package/dist/tests/llm-client.test.js +0 -115
  147. package/dist/tests/llm-feature-gate.test.js +0 -151
  148. package/dist/tests/llm.test.js +0 -139
  149. package/dist/tests/lockfile.test.js +0 -216
  150. package/dist/tests/manifest.test.js +0 -205
  151. package/dist/tests/markdown.test.js +0 -126
  152. package/dist/tests/matchers-unit.test.js +0 -189
  153. package/dist/tests/memory-inference.test.js +0 -299
  154. package/dist/tests/merge-scoring.test.js +0 -136
  155. package/dist/tests/metadata.test.js +0 -313
  156. package/dist/tests/migration-help.test.js +0 -89
  157. package/dist/tests/origin-resolve.test.js +0 -124
  158. package/dist/tests/output-baseline.test.js +0 -217
  159. package/dist/tests/output-shapes-unit.test.js +0 -476
  160. package/dist/tests/parallel-search.test.js +0 -272
  161. package/dist/tests/parameter-metadata.test.js +0 -365
  162. package/dist/tests/paths.test.js +0 -177
  163. package/dist/tests/progressive-disclosure.test.js +0 -280
  164. package/dist/tests/proposals.test.js +0 -279
  165. package/dist/tests/proposed-quality.test.js +0 -271
  166. package/dist/tests/provider-registry.test.js +0 -32
  167. package/dist/tests/ranking-regression.test.js +0 -548
  168. package/dist/tests/reflect-propose.test.js +0 -455
  169. package/dist/tests/registry-build-index.test.js +0 -378
  170. package/dist/tests/registry-cli.test.js +0 -290
  171. package/dist/tests/registry-index-v2.test.js +0 -430
  172. package/dist/tests/registry-install.test.js +0 -728
  173. package/dist/tests/registry-providers/parity.test.js +0 -189
  174. package/dist/tests/registry-providers/skills-sh.test.js +0 -309
  175. package/dist/tests/registry-providers/static-index.test.js +0 -204
  176. package/dist/tests/registry-resolve.test.js +0 -126
  177. package/dist/tests/registry-search.test.js +0 -723
  178. package/dist/tests/remember-frontmatter.test.js +0 -380
  179. package/dist/tests/remember-unit.test.js +0 -123
  180. package/dist/tests/ripgrep-install.test.js +0 -251
  181. package/dist/tests/ripgrep-resolve.test.js +0 -108
  182. package/dist/tests/ripgrep.test.js +0 -163
  183. package/dist/tests/save-command.test.js +0 -94
  184. package/dist/tests/save-trust-qa-fixes.test.js +0 -270
  185. package/dist/tests/scoring-pipeline.test.js +0 -648
  186. package/dist/tests/search-include-proposed-cli.test.js +0 -118
  187. package/dist/tests/self-update.test.js +0 -442
  188. package/dist/tests/semantic-search-e2e.test.js +0 -512
  189. package/dist/tests/semantic-status.test.js +0 -471
  190. package/dist/tests/setup-run.integration.js +0 -877
  191. package/dist/tests/setup-wizard.test.js +0 -198
  192. package/dist/tests/setup.test.js +0 -131
  193. package/dist/tests/source-add.test.js +0 -11
  194. package/dist/tests/source-clone.test.js +0 -254
  195. package/dist/tests/source-manage.test.js +0 -366
  196. package/dist/tests/source-providers/filesystem.test.js +0 -82
  197. package/dist/tests/source-providers/git.test.js +0 -252
  198. package/dist/tests/source-providers/website.test.js +0 -128
  199. package/dist/tests/source-qa-fixes.test.js +0 -268
  200. package/dist/tests/source-registry.test.js +0 -350
  201. package/dist/tests/source-resolve.test.js +0 -100
  202. package/dist/tests/source-source.test.js +0 -221
  203. package/dist/tests/source.test.js +0 -533
  204. package/dist/tests/tar-utils-scan.test.js +0 -73
  205. package/dist/tests/toggle-components.test.js +0 -73
  206. package/dist/tests/usage-telemetry.test.js +0 -265
  207. package/dist/tests/utility-scoring.test.js +0 -558
  208. package/dist/tests/vault-load-error.test.js +0 -78
  209. package/dist/tests/vault-qa-fixes.test.js +0 -194
  210. package/dist/tests/vault.test.js +0 -429
  211. package/dist/tests/vector-search.test.js +0 -608
  212. package/dist/tests/walker.test.js +0 -252
  213. package/dist/tests/wave2-cluster-bc.test.js +0 -228
  214. package/dist/tests/wave2-cluster-d.test.js +0 -180
  215. package/dist/tests/wave2-cluster-e.test.js +0 -179
  216. package/dist/tests/wiki-qa-fixes.test.js +0 -270
  217. package/dist/tests/wiki.test.js +0 -529
  218. package/dist/tests/workflow-cli.test.js +0 -271
  219. package/dist/tests/workflow-markdown.test.js +0 -171
  220. package/dist/tests/workflow-path-escape.test.js +0 -132
  221. package/dist/tests/workflow-qa-fixes.test.js +0 -377
  222. package/dist/tests/workflows/indexer-rejection.test.js +0 -213
  223. /package/dist/{src/commands → commands}/completions.js +0 -0
  224. /package/dist/{src/commands → commands}/curate.js +0 -0
  225. /package/dist/{src/commands → commands}/distill.js +0 -0
  226. /package/dist/{src/commands → commands}/events.js +0 -0
  227. /package/dist/{src/commands → commands}/info.js +0 -0
  228. /package/dist/{src/commands → commands}/init.js +0 -0
  229. /package/dist/{src/commands → commands}/install-audit.js +0 -0
  230. /package/dist/{src/commands → commands}/installed-stashes.js +0 -0
  231. /package/dist/{src/commands → commands}/migration-help.js +0 -0
  232. /package/dist/{src/commands → commands}/proposal.js +0 -0
  233. /package/dist/{src/commands → commands}/propose.js +0 -0
  234. /package/dist/{src/commands → commands}/reflect.js +0 -0
  235. /package/dist/{src/commands → commands}/remember.js +0 -0
  236. /package/dist/{src/commands → commands}/self-update.js +0 -0
  237. /package/dist/{src/commands → commands}/source-clone.js +0 -0
  238. /package/dist/{src/commands → commands}/vault.js +0 -0
  239. /package/dist/{src/core → core}/asset-ref.js +0 -0
  240. /package/dist/{src/core → core}/asset-registry.js +0 -0
  241. /package/dist/{src/core → core}/asset-spec.js +0 -0
  242. /package/dist/{src/core → core}/common.js +0 -0
  243. /package/dist/{src/core → core}/errors.js +0 -0
  244. /package/dist/{src/core → core}/events.js +0 -0
  245. /package/dist/{src/core → core}/frontmatter.js +0 -0
  246. /package/dist/{src/core → core}/lesson-lint.js +0 -0
  247. /package/dist/{src/core → core}/markdown.js +0 -0
  248. /package/dist/{src/core → core}/paths.js +0 -0
  249. /package/dist/{src/core → core}/proposals.js +0 -0
  250. /package/dist/{src/core → core}/warn.js +0 -0
  251. /package/dist/{src/core → core}/write-source.js +0 -0
  252. /package/dist/{src/indexer → indexer}/db.js +0 -0
  253. /package/dist/{src/indexer → indexer}/file-context.js +0 -0
  254. /package/dist/{src/indexer → indexer}/graph-boost.js +0 -0
  255. /package/dist/{src/indexer → indexer}/graph-extraction.js +0 -0
  256. /package/dist/{src/indexer → indexer}/manifest.js +0 -0
  257. /package/dist/{src/indexer → indexer}/memory-inference.js +0 -0
  258. /package/dist/{src/indexer → indexer}/metadata.js +0 -0
  259. /package/dist/{src/indexer → indexer}/search-fields.js +0 -0
  260. /package/dist/{src/indexer → indexer}/semantic-status.js +0 -0
  261. /package/dist/{src/indexer → indexer}/usage-events.js +0 -0
  262. /package/dist/{src/indexer → indexer}/walker.js +0 -0
  263. /package/dist/{src/integrations → integrations}/agent/config.js +0 -0
  264. /package/dist/{src/integrations → integrations}/agent/detect.js +0 -0
  265. /package/dist/{src/integrations → integrations}/agent/index.js +0 -0
  266. /package/dist/{src/integrations → integrations}/agent/prompts.js +0 -0
  267. /package/dist/{src/integrations → integrations}/lockfile.js +0 -0
  268. /package/dist/{src/llm → llm}/client.js +0 -0
  269. /package/dist/{src/llm → llm}/embedder.js +0 -0
  270. /package/dist/{src/llm → llm}/embedders/cache.js +0 -0
  271. /package/dist/{src/llm → llm}/embedders/local.js +0 -0
  272. /package/dist/{src/llm → llm}/embedders/types.js +0 -0
  273. /package/dist/{src/llm → llm}/feature-gate.js +0 -0
  274. /package/dist/{src/llm → llm}/graph-extract.js +0 -0
  275. /package/dist/{src/llm → llm}/index-passes.js +0 -0
  276. /package/dist/{src/llm → llm}/memory-infer.js +0 -0
  277. /package/dist/{src/llm → llm}/metadata-enhance.js +0 -0
  278. /package/dist/{src/output → output}/context.js +0 -0
  279. /package/dist/{src/registry → registry}/create-provider-registry.js +0 -0
  280. /package/dist/{src/registry → registry}/factory.js +0 -0
  281. /package/dist/{src/registry → registry}/origin-resolve.js +0 -0
  282. /package/dist/{src/registry → registry}/providers/index.js +0 -0
  283. /package/dist/{src/registry → registry}/providers/skills-sh.js +0 -0
  284. /package/dist/{src/registry → registry}/providers/types.js +0 -0
  285. /package/dist/{src/registry → registry}/resolve.js +0 -0
  286. /package/dist/{src/registry → registry}/types.js +0 -0
  287. /package/dist/{src/setup → setup}/detect.js +0 -0
  288. /package/dist/{src/setup → setup}/ripgrep-install.js +0 -0
  289. /package/dist/{src/setup → setup}/ripgrep-resolve.js +0 -0
  290. /package/dist/{src/setup → setup}/steps.js +0 -0
  291. /package/dist/{src/sources → sources}/include.js +0 -0
  292. /package/dist/{src/sources → sources}/provider-factory.js +0 -0
  293. /package/dist/{src/sources → sources}/provider.js +0 -0
  294. /package/dist/{src/sources → sources}/providers/filesystem.js +0 -0
  295. /package/dist/{src/sources → sources}/providers/git.js +0 -0
  296. /package/dist/{src/sources → sources}/providers/index.js +0 -0
  297. /package/dist/{src/sources → sources}/providers/install-types.js +0 -0
  298. /package/dist/{src/sources → sources}/providers/npm.js +0 -0
  299. /package/dist/{src/sources → sources}/providers/provider-utils.js +0 -0
  300. /package/dist/{src/sources → sources}/providers/sync-from-ref.js +0 -0
  301. /package/dist/{src/sources → sources}/providers/tar-utils.js +0 -0
  302. /package/dist/{src/sources → sources}/providers/website.js +0 -0
  303. /package/dist/{src/sources → sources}/resolve.js +0 -0
  304. /package/dist/{src/sources → sources}/types.js +0 -0
  305. /package/dist/{src/templates → templates}/wiki-templates.js +0 -0
  306. /package/dist/{src/version.js → version.js} +0 -0
  307. /package/dist/{src/workflows → workflows}/authoring.js +0 -0
  308. /package/dist/{src/workflows → workflows}/cli.js +0 -0
  309. /package/dist/{src/workflows → workflows}/db.js +0 -0
  310. /package/dist/{src/workflows → workflows}/document-cache.js +0 -0
  311. /package/dist/{src/workflows → workflows}/parser.js +0 -0
  312. /package/dist/{src/workflows → workflows}/renderer.js +0 -0
  313. /package/dist/{src/workflows → workflows}/schema.js +0 -0
  314. /package/dist/{src/workflows → workflows}/validator.js +0 -0
@@ -183,10 +183,11 @@ const searchCommand = defineCommand({
183
183
  },
184
184
  async run({ args }) {
185
185
  await runWithJsonErrors(async () => {
186
+ // An empty query enumerates all indexed assets (list mode).
187
+ // The guard that rejected empty queries was removed; akmSearch handles
188
+ // empty strings end-to-end via getAllEntries (DB path) and the
189
+ // substring-search fallback's query-less branch.
186
190
  const query = (args.query ?? "").trim();
187
- if (!query) {
188
- throw new UsageError('A search query is required. Usage: akm search "<query>" [--type <type>] [--limit <n>]', "MISSING_REQUIRED_ARGUMENT", "Provide a query string. Filter by type with --type skill|command|...; limit results with --limit N.");
189
- }
190
191
  const type = args.type;
191
192
  const limitRaw = args.limit ? parseInt(args.limit, 10) : undefined;
192
193
  if (limitRaw !== undefined && Number.isNaN(limitRaw)) {
@@ -502,6 +503,15 @@ const showCommand = defineCommand({
502
503
  },
503
504
  async run({ args }) {
504
505
  await runWithJsonErrors(async () => {
506
+ try {
507
+ parseAssetRef(args.ref);
508
+ }
509
+ catch (error) {
510
+ if (error instanceof UsageError && error.code === "MISSING_REQUIRED_ARGUMENT") {
511
+ throw new UsageError(error.message, "INVALID_FLAG_VALUE", error.hint());
512
+ }
513
+ throw error;
514
+ }
505
515
  // The knowledge-view positional syntax (`akm show knowledge:foo section "Auth"`)
506
516
  // is rewritten to `--akmView` / `--akmHeading` / `--akmStart` / `--akmEnd`
507
517
  // by `normalizeShowArgv` before citty parses argv. We read those values
@@ -863,8 +873,8 @@ const registryCommand = defineCommand({
863
873
  "build-index": defineCommand({
864
874
  meta: { name: "build-index", description: "Build a v2 registry index from discovery and manual entries" },
865
875
  args: {
866
- out: { type: "string", description: "Output path for the generated index", default: "index.json" },
867
- manual: { type: "string", description: "Manual entries JSON file", default: "manual-entries.json" },
876
+ out: { type: "string", description: "Output path for the generated index" },
877
+ manual: { type: "string", description: "Manual entries JSON file" },
868
878
  "npm-registry": { type: "string", description: "Override npm registry base URL" },
869
879
  "github-api": { type: "string", description: "Override GitHub API base URL" },
870
880
  },
@@ -892,15 +902,26 @@ const registryCommand = defineCommand({
892
902
  const feedbackCommand = defineCommand({
893
903
  meta: {
894
904
  name: "feedback",
895
- description: "Record positive or negative feedback for any indexed stash asset",
905
+ description: "Record positive or negative feedback for any indexed stash asset.\n\n" +
906
+ "Positive feedback boosts an asset's EMA utility score, making it rank higher\n" +
907
+ "in future searches without requiring a full reindex.\n\n" +
908
+ "Negative feedback records a negative signal in usage_events and events.jsonl.\n" +
909
+ "It does NOT immediately lower the asset's ranking — the EMA utility score is\n" +
910
+ "updated the next time `akm index` runs (incremental or full). Run `akm index`\n" +
911
+ "after recording negative feedback to have it reflected in search results.",
896
912
  },
897
913
  args: {
898
914
  // Optional in citty so run() is invoked even when omitted; we re-validate
899
915
  // and throw a structured UsageError below so exit code is 2 (USAGE) rather
900
916
  // than citty's default 0 (help banner).
901
917
  ref: { type: "positional", description: "Asset ref (type:name)", required: false },
902
- positive: { type: "boolean", description: "Record positive feedback", default: false },
903
- negative: { type: "boolean", description: "Record negative feedback", default: false },
918
+ positive: { type: "boolean", description: "Record positive feedback (boosts ranking immediately)", default: false },
919
+ negative: {
920
+ type: "boolean",
921
+ description: "Record negative feedback (suppresses ranking after next `akm index`). " +
922
+ "Reindexing is required for the signal to affect search results.",
923
+ default: false,
924
+ },
904
925
  note: { type: "string", description: "Optional note to attach to the feedback" },
905
926
  },
906
927
  run({ args }) {
@@ -924,6 +945,11 @@ const feedbackCommand = defineCommand({
924
945
  if (entryId === undefined) {
925
946
  throw new UsageError(`Ref "${ref}" is not in the current index. Run "akm index" and try again.`);
926
947
  }
948
+ // Persist the feedback signal into usage_events. For positive signals,
949
+ // the EMA utility score is updated immediately on the next read path.
950
+ // For negative signals, the score is adjusted the next time `akm index`
951
+ // runs — the signal is durable in the DB but does NOT suppress ranking
952
+ // in search results until after reindexing.
927
953
  insertUsageEvent(db, {
928
954
  event_type: "feedback",
929
955
  entry_ref: ref,
@@ -947,11 +973,22 @@ const feedbackCommand = defineCommand({
947
973
  const historyCommand = defineCommand({
948
974
  meta: {
949
975
  name: "history",
950
- description: "Show mutation/usage history for a single asset (--ref) or stash-wide. Backed by the internal usage_events log.",
976
+ description: "Show mutation/usage history for a single asset (--ref) or stash-wide.\n\n" +
977
+ "Event sources:\n" +
978
+ " usage_events (default): search, show, and feedback events from the local index.\n" +
979
+ " events.jsonl (--include-proposals): proposal lifecycle events (promoted, rejected)\n" +
980
+ " emitted by `akm proposal accept` / `akm proposal reject`.\n\n" +
981
+ "Results from all active sources are merged and sorted chronologically.",
951
982
  },
952
983
  args: {
953
984
  ref: { type: "string", description: "Asset ref (type:name). Omit for stash-wide history." },
954
985
  since: { type: "string", description: "ISO timestamp or epoch ms — only events on/after this time" },
986
+ "include-proposals": {
987
+ type: "boolean",
988
+ description: "Also include proposal lifecycle events (promoted, rejected) from events.jsonl. " +
989
+ "Default: false (usage_events only).",
990
+ default: false,
991
+ },
955
992
  format: { type: "string", description: "Output format (json|jsonl|text|yaml)" },
956
993
  },
957
994
  run({ args }) {
@@ -959,6 +996,7 @@ const historyCommand = defineCommand({
959
996
  const result = await akmHistory({
960
997
  ref: args.ref,
961
998
  since: args.since,
999
+ includeProposals: args["include-proposals"],
962
1000
  });
963
1001
  output("history", result);
964
1002
  });
@@ -1397,7 +1435,7 @@ const rememberCommand = defineCommand({
1397
1435
  },
1398
1436
  async run({ args }) {
1399
1437
  return runWithJsonErrors(async () => {
1400
- const body = readMemoryContent(args.content);
1438
+ const body = readMemoryContent(resolveRememberContentArg(args.content));
1401
1439
  // Determine if the user has requested any structured metadata mode.
1402
1440
  // Collect all --tag occurrences directly from process.argv because citty
1403
1441
  // only exposes the last value for repeated string flags.
@@ -1415,8 +1453,8 @@ const rememberCommand = defineCommand({
1415
1453
  if (typeof args.channel === "string" && args.channel.trim())
1416
1454
  scopeFields.channel = args.channel.trim();
1417
1455
  const hasScope = Object.keys(scopeFields).length > 0;
1418
- const hasTagRequiringArgs = rawTags.length > 0 || !!args.expires || !!args.source || !!args.description || args.auto || args.enrich;
1419
- const hasStructuredArgs = hasTagRequiringArgs || hasScope;
1456
+ const hasTagRequiringArgs = rawTags.length > 0 || !!args.expires || !!args.source || !!args.description || args.enrich;
1457
+ const hasStructuredArgs = hasTagRequiringArgs || hasScope || args.auto;
1420
1458
  if (!hasStructuredArgs) {
1421
1459
  const result = await writeMarkdownAsset({
1422
1460
  type: "memory",
@@ -1476,10 +1514,12 @@ const rememberCommand = defineCommand({
1476
1514
  observed_at = enriched.observed_at;
1477
1515
  }
1478
1516
  // ── Required-field check (before any write) ───────────────────────────
1479
- // Tags remain required when the user opted into a tag-producing mode
1480
- // (--tag / --auto / --enrich / --description / --source / --expires).
1481
- // Scope-only writes (`akm remember "..." --user u1`) skip this check
1482
- // scope is independent metadata and a memory with only scope is valid.
1517
+ // Tags remain required when the user explicitly asked for tag-bearing
1518
+ // metadata (--tag / --enrich / --description / --source / --expires).
1519
+ // `--auto` alone is allowed even when its heuristics derive zero tags.
1520
+ // Scope-only writes (`akm remember "..." --user u1`) also skip this
1521
+ // check — scope is independent metadata and a memory with only scope is
1522
+ // valid.
1483
1523
  const missing = [];
1484
1524
  if (hasTagRequiringArgs && tags.length === 0)
1485
1525
  missing.push("tags");
@@ -1522,6 +1562,50 @@ const rememberCommand = defineCommand({
1522
1562
  });
1523
1563
  },
1524
1564
  });
1565
+ function resolveRememberContentArg(content) {
1566
+ if (content === undefined)
1567
+ return undefined;
1568
+ const parsedFormat = parseFlagValue(process.argv, "--format");
1569
+ if (parsedFormat !== undefined &&
1570
+ content === parsedFormat &&
1571
+ wasRememberFlagValueConsumedAsContent(content, parsedFormat, "--format")) {
1572
+ return undefined;
1573
+ }
1574
+ const parsedDetail = parseFlagValue(process.argv, "--detail");
1575
+ if (parsedDetail !== undefined &&
1576
+ content === parsedDetail &&
1577
+ wasRememberFlagValueConsumedAsContent(content, parsedDetail, "--detail")) {
1578
+ return undefined;
1579
+ }
1580
+ return content;
1581
+ }
1582
+ function wasRememberFlagValueConsumedAsContent(content, flagValue, flagName) {
1583
+ const argv = process.argv.slice(2);
1584
+ const rememberIndex = argv.indexOf("remember");
1585
+ const tokens = rememberIndex >= 0 ? argv.slice(rememberIndex + 1) : argv;
1586
+ let flagIndex = -1;
1587
+ let flagConsumesNextToken = false;
1588
+ for (let i = 0; i < tokens.length; i += 1) {
1589
+ const token = tokens[i];
1590
+ if (token === flagName) {
1591
+ flagIndex = i;
1592
+ flagConsumesNextToken = true;
1593
+ break;
1594
+ }
1595
+ if (token === `${flagName}=${flagValue}`) {
1596
+ flagIndex = i;
1597
+ break;
1598
+ }
1599
+ }
1600
+ if (flagIndex === -1)
1601
+ return false;
1602
+ if (tokens.slice(0, flagIndex).includes(content))
1603
+ return false;
1604
+ const firstTokenAfterFlag = flagIndex + (flagConsumesNextToken ? 2 : 1);
1605
+ if (tokens.slice(firstTokenAfterFlag).includes(content))
1606
+ return false;
1607
+ return true;
1608
+ }
1525
1609
  const importKnowledgeCommand = defineCommand({
1526
1610
  meta: {
1527
1611
  name: "import",
@@ -31,6 +31,12 @@ export function parseConfigValue(key, value) {
31
31
  return { embedding: mergeLlmLikeEmbedding(undefined, { model: requireNonEmptyString(value, key) }) };
32
32
  case "embedding.apiKey":
33
33
  return { embedding: mergeLlmLikeEmbedding(undefined, { apiKey: requireNonEmptyString(value, key) }) };
34
+ case "embedding.contextLength":
35
+ return { embedding: mergeLlmLikeEmbedding(undefined, { contextLength: parsePositiveInteger(value, key) }) };
36
+ case "embedding.ollamaOptions.numCtx":
37
+ return {
38
+ embedding: mergeLlmLikeEmbedding(undefined, { ollamaOptions: { num_ctx: parsePositiveInteger(value, key) } }),
39
+ };
34
40
  case "llm":
35
41
  return { llm: parseLlmConnectionValue(value) };
36
42
  case "llm.endpoint":
@@ -82,6 +88,10 @@ export function getConfigValue(config, key) {
82
88
  return config.embedding?.model ?? null;
83
89
  case "embedding.apiKey":
84
90
  return config.embedding?.apiKey ?? null;
91
+ case "embedding.contextLength":
92
+ return config.embedding?.contextLength ?? null;
93
+ case "embedding.ollamaOptions.numCtx":
94
+ return config.embedding?.ollamaOptions?.num_ctx ?? null;
85
95
  case "llm":
86
96
  return config.llm ?? null;
87
97
  case "llm.endpoint":
@@ -152,6 +162,18 @@ export function setConfigValue(config, key, rawValue) {
152
162
  ...config,
153
163
  embedding: mergeLlmLikeEmbedding(config.embedding, { apiKey: requireNonEmptyString(rawValue, key) }),
154
164
  };
165
+ case "embedding.contextLength":
166
+ return {
167
+ ...config,
168
+ embedding: mergeLlmLikeEmbedding(config.embedding, { contextLength: parsePositiveInteger(rawValue, key) }),
169
+ };
170
+ case "embedding.ollamaOptions.numCtx":
171
+ return {
172
+ ...config,
173
+ embedding: mergeLlmLikeEmbedding(config.embedding, {
174
+ ollamaOptions: { ...(config.embedding?.ollamaOptions ?? {}), num_ctx: parsePositiveInteger(rawValue, key) },
175
+ }),
176
+ };
155
177
  case "llm.endpoint":
156
178
  return { ...config, llm: mergeLlmLike(config.llm, { endpoint: requireNonEmptyString(rawValue, key) }) };
157
179
  case "llm.model":
@@ -190,6 +212,19 @@ export function unsetConfigValue(config, key) {
190
212
  const { apiKey: _a, ...rest } = config.embedding;
191
213
  return { ...config, embedding: rest };
192
214
  }
215
+ case "embedding.contextLength": {
216
+ if (!config.embedding)
217
+ return config;
218
+ const { contextLength: _cl, ...rest } = config.embedding;
219
+ return { ...config, embedding: rest };
220
+ }
221
+ case "embedding.ollamaOptions.numCtx": {
222
+ if (!config.embedding?.ollamaOptions)
223
+ return config;
224
+ const { num_ctx: _nc, ...restOpts } = config.embedding.ollamaOptions;
225
+ const ollamaOptions = Object.keys(restOpts).length > 0 ? restOpts : undefined;
226
+ return { ...config, embedding: { ...config.embedding, ollamaOptions } };
227
+ }
193
228
  case "llm":
194
229
  return { ...config, llm: undefined };
195
230
  case "llm.endpoint":
@@ -479,6 +514,13 @@ function parseUnknownPositiveInteger(value, key) {
479
514
  }
480
515
  return value;
481
516
  }
517
+ function parsePositiveInteger(value, key) {
518
+ const n = Number(value);
519
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0) {
520
+ throw new UsageError(`Invalid value for ${key}: expected a positive integer`);
521
+ }
522
+ return n;
523
+ }
482
524
  function parseStashesValue(value) {
483
525
  if (value === "null" || value === "")
484
526
  return undefined;
@@ -1,16 +1,25 @@
1
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.
2
+ * `akm history` — surfaces internal mutation/usage events for a single asset
3
+ * (`--ref`) or stash-wide.
4
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.
5
+ * Event sources:
6
+ * - `usage_events` SQLite table: search, show, and feedback events recorded
7
+ * by the local indexer during normal CLI use.
8
+ * - `events.jsonl` append-only stream (opt-in via `--include-proposals`):
9
+ * proposal lifecycle events (`promoted`, `rejected`) emitted by
10
+ * `akm proposal accept` / `akm proposal reject`. Use this flag to see
11
+ * the full proposal review trail alongside usage events.
12
+ *
13
+ * The two sources are merged and sorted chronologically (oldest first) so
14
+ * consumers see a coherent lifecycle trail in a single output.
9
15
  */
10
16
  import { parseAssetRef } from "../core/asset-ref";
11
17
  import { UsageError } from "../core/errors";
18
+ import { readEvents } from "../core/events";
12
19
  import { closeDatabase, openDatabase } from "../indexer/db";
13
20
  import { ensureUsageEventsSchema } from "../indexer/usage-events";
21
+ // Proposal lifecycle event types emitted by the proposal substrate (#225).
22
+ const PROPOSAL_EVENT_TYPES = new Set(["promoted", "rejected"]);
14
23
  // ── Helpers ──────────────────────────────────────────────────────────────────
15
24
  function normalizeSince(since) {
16
25
  // Accept "YYYY-MM-DD", "YYYY-MM-DDTHH:MM:SSZ", epoch ms, or anything Date can parse.
@@ -62,11 +71,27 @@ function toEntry(row) {
62
71
  createdAt: row.created_at,
63
72
  };
64
73
  }
74
+ /**
75
+ * Convert an ISO timestamp from events.jsonl ("2026-04-01T12:00:00.000Z")
76
+ * to the SQLite-style format used in HistoryEntry.createdAt
77
+ * ("2026-04-01 12:00:00") so entries sort consistently.
78
+ */
79
+ function isoToSqliteTimestamp(ts) {
80
+ // Normalise to the "YYYY-MM-DD HH:MM:SS" format used by usage_events rows.
81
+ return ts
82
+ .replace("T", " ")
83
+ .replace(/\.\d+Z$/, "")
84
+ .replace("Z", "");
85
+ }
65
86
  // ── Main ─────────────────────────────────────────────────────────────────────
66
87
  /**
67
88
  * Read mutation/usage history. When `ref` is provided, results are filtered to
68
89
  * that asset (validated via `parseAssetRef`). Always returns chronological
69
90
  * order (oldest first) so consumers can display a lifecycle trail.
91
+ *
92
+ * When `includeProposals` is true, proposal lifecycle events (`promoted`,
93
+ * `rejected`) from events.jsonl are merged into the result set. This provides
94
+ * one coherent view of both usage signals and proposal review decisions.
70
95
  */
71
96
  export async function akmHistory(options = {}) {
72
97
  let normalizedRef;
@@ -103,13 +128,59 @@ export async function akmHistory(options = {}) {
103
128
  FROM usage_events ${where}
104
129
  ORDER BY id ASC`;
105
130
  const rows = db.prepare(sql).all(...params);
106
- const entries = rows.map(toEntry);
131
+ const usageEntries = rows.map(toEntry);
132
+ // ── Proposal lifecycle events (opt-in) ────────────────────────────────
133
+ const sources = ["usage_events"];
134
+ const proposalEntries = [];
135
+ if (options.includeProposals === true) {
136
+ sources.push("events.jsonl");
137
+ // Convert sinceNormalized ("YYYY-MM-DD HH:MM:SS") to ISO for readEvents
138
+ // which uses `ts >= since` where `ts` is ISO-8601.
139
+ const sinceIso = sinceNormalized !== undefined ? `${sinceNormalized.replace(" ", "T")}Z` : undefined;
140
+ const { events } = readEvents({
141
+ since: sinceIso,
142
+ ref: normalizedRef,
143
+ }, options.eventsCtx);
144
+ // Keep only proposal lifecycle event types.
145
+ let counter = -1_000_000; // negative ids mark proposal-stream entries
146
+ for (const event of events) {
147
+ if (!PROPOSAL_EVENT_TYPES.has(event.eventType))
148
+ continue;
149
+ const createdAt = event.ts ? isoToSqliteTimestamp(event.ts) : "";
150
+ // Skip if before `since` (readEvents already filters by ts >= since,
151
+ // but the isoToSqliteTimestamp conversion may introduce drift so we
152
+ // guard again with the normalised form).
153
+ if (sinceNormalized !== undefined && createdAt < sinceNormalized)
154
+ continue;
155
+ proposalEntries.push({
156
+ id: counter--,
157
+ eventType: event.eventType,
158
+ ref: event.ref ?? null,
159
+ entryId: null,
160
+ query: null,
161
+ signal: null,
162
+ metadata: event.metadata ?? null,
163
+ createdAt,
164
+ });
165
+ }
166
+ }
167
+ // ── Merge and sort ────────────────────────────────────────────────────
168
+ const entries = [...usageEntries, ...proposalEntries].sort((a, b) => {
169
+ // Primary sort: chronological by createdAt (string compare is safe for
170
+ // "YYYY-MM-DD HH:MM:SS" format). Secondary sort: id ascending for ties.
171
+ if (a.createdAt < b.createdAt)
172
+ return -1;
173
+ if (a.createdAt > b.createdAt)
174
+ return 1;
175
+ return a.id - b.id;
176
+ });
107
177
  const response = {
108
178
  schemaVersion: 1,
109
179
  ...(normalizedRef !== undefined ? { ref: normalizedRef } : {}),
110
180
  ...(sinceNormalized !== undefined ? { since: sinceNormalized } : {}),
111
181
  totalCount: entries.length,
112
182
  entries,
183
+ sources,
113
184
  };
114
185
  return response;
115
186
  }
@@ -23,6 +23,9 @@ export async function searchRegistry(query, options) {
23
23
  return provider.search({ query: trimmed, limit, includeAssets: options?.includeAssets });
24
24
  }));
25
25
  // Merge results grouped by provider
26
+ // Each provider batch is normalized to [0, 1] before merging so that raw
27
+ // scores from different providers (e.g. static-index can exceed 1.85 while
28
+ // skills-sh uses installs-relative scoring) are comparable in the merged list.
26
29
  const allHits = [];
27
30
  const allAssetHits = [];
28
31
  for (let i = 0; i < results.length; i++) {
@@ -36,23 +39,34 @@ export async function searchRegistry(query, options) {
36
39
  continue;
37
40
  const registryLabel = entries[i].name ? `"${entries[i].name}"` : entries[i].url;
38
41
  let dropped = 0;
42
+ const validHits = [];
39
43
  for (const hit of value.hits) {
40
44
  if (isCompleteHit(hit)) {
41
- allHits.push(hit);
45
+ validHits.push(hit);
42
46
  }
43
47
  else {
44
48
  dropped++;
45
49
  }
46
50
  }
51
+ // Normalize scores within this provider's batch before merging
52
+ normalizeScores(validHits);
53
+ for (const hit of validHits) {
54
+ allHits.push(hit);
55
+ }
47
56
  if (value.assetHits) {
57
+ const validAssetHits = [];
48
58
  for (const hit of value.assetHits) {
49
59
  if (isCompleteAssetHit(hit)) {
50
- allAssetHits.push(hit);
60
+ validAssetHits.push(hit);
51
61
  }
52
62
  else {
53
63
  dropped++;
54
64
  }
55
65
  }
66
+ normalizeScores(validAssetHits);
67
+ for (const hit of validAssetHits) {
68
+ allAssetHits.push(hit);
69
+ }
56
70
  }
57
71
  if (dropped > 0) {
58
72
  warnings.push(`Registry ${registryLabel} returned ${dropped} incomplete hit(s); dropped from response.`);
@@ -60,7 +74,7 @@ export async function searchRegistry(query, options) {
60
74
  if (value.warnings)
61
75
  warnings.push(...value.warnings);
62
76
  }
63
- // Sort merged hits by score descending, apply limit
77
+ // Sort merged hits by normalized score descending, apply limit
64
78
  allHits.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
65
79
  const limitedHits = allHits.slice(0, limit);
66
80
  allAssetHits.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
@@ -80,6 +94,11 @@ export async function searchRegistry(query, options) {
80
94
  * 1. AKM_REGISTRY_URL env var (CI override, comma-separated)
81
95
  * 2. config.registries (filtered by enabled !== false)
82
96
  * 3. Default registries from DEFAULT_CONFIG
97
+ *
98
+ * AKM_REGISTRY_URL syntax (comma-separated):
99
+ * - Bare URL: `https://example.com/index.json` → defaults to provider "static-index"
100
+ * - Typed URL: `skills-sh::https://skills.sh/api` → explicit provider type
101
+ * Format: `<provider-type>::<url>`
83
102
  */
84
103
  export function resolveRegistries(configRegistries) {
85
104
  // Allow env var override (comma-separated URLs) — CI escape hatch
@@ -87,14 +106,30 @@ export function resolveRegistries(configRegistries) {
87
106
  if (envUrls) {
88
107
  const entries = [];
89
108
  for (const raw of envUrls.split(",")) {
90
- const url = raw.trim();
91
- if (!url)
109
+ const trimmed = raw.trim();
110
+ if (!trimmed)
92
111
  continue;
112
+ // Parse optional `<provider-type>::<url>` prefix
113
+ let provider;
114
+ let url;
115
+ const colonColonIdx = trimmed.indexOf("::");
116
+ if (colonColonIdx !== -1 && !trimmed.startsWith("http://") && !trimmed.startsWith("https://")) {
117
+ // Only treat as `provider::url` if the prefix doesn't look like a URL scheme itself
118
+ provider = trimmed.slice(0, colonColonIdx).trim();
119
+ url = trimmed.slice(colonColonIdx + 2).trim();
120
+ if (!provider) {
121
+ warn(`[akm] Ignoring AKM_REGISTRY_URL entry: empty provider type before "::" in "${trimmed}"`);
122
+ continue;
123
+ }
124
+ }
125
+ else {
126
+ url = trimmed;
127
+ }
93
128
  if (!url.startsWith("http://") && !url.startsWith("https://")) {
94
129
  warn(`[akm] Ignoring AKM_REGISTRY_URL entry: must start with http:// or https://, got "${url}"`);
95
130
  continue;
96
131
  }
97
- entries.push({ url });
132
+ entries.push(provider ? { url, provider } : { url });
98
133
  }
99
134
  return entries;
100
135
  }
@@ -113,6 +148,34 @@ function createProvider(entry, warnings) {
113
148
  return factory(entry);
114
149
  }
115
150
  // ── Utilities ───────────────────────────────────────────────────────────────
151
+ /**
152
+ * Normalize the `score` field of a batch of hits in-place to [0, 1].
153
+ *
154
+ * Different registry providers use incompatible score scales
155
+ * (static-index can exceed 1.85; skills-sh uses installs-relative values
156
+ * in [0, 1]). Normalizing each provider's batch independently before merging
157
+ * makes the merged sort order meaningful.
158
+ *
159
+ * When all scores are identical (or absent), scores are left unchanged so
160
+ * relative ordering within the batch is preserved (all-same is effectively
161
+ * already normalized).
162
+ */
163
+ function normalizeScores(hits) {
164
+ if (hits.length === 0)
165
+ return;
166
+ const rawScores = hits.map((h) => h.score ?? 0);
167
+ const max = Math.max(...rawScores);
168
+ if (max <= 0)
169
+ return; // all zero or negative — leave as-is
170
+ const min = Math.min(...rawScores);
171
+ const range = max - min;
172
+ for (let i = 0; i < hits.length; i++) {
173
+ const raw = rawScores[i];
174
+ // Min-max normalize: [0, 1]. When all scores are equal (range === 0),
175
+ // fall back to dividing by max so the value stays in [0, 1].
176
+ hits[i].score = range > 0 ? (raw - min) / range : raw / max;
177
+ }
178
+ }
116
179
  function clampLimit(limit) {
117
180
  if (!limit || !Number.isFinite(limit))
118
181
  return 20;
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { loadConfig } from "../core/config";
12
12
  import { UsageError } from "../core/errors";
13
+ import { appendEvent } from "../core/events";
13
14
  import { closeDatabase, openDatabase } from "../indexer/db";
14
15
  import { searchLocal } from "../indexer/db-search";
15
16
  import { resolveSourceEntries } from "../indexer/search-source";
@@ -147,13 +148,30 @@ function resolveEntryIds(db, hits) {
147
148
  /**
148
149
  * Fire-and-forget: log a search event to the usage_events table.
149
150
  * Never blocks the caller; errors are silently ignored.
151
+ *
152
+ * Result count semantics:
153
+ * - `stashHitCount`: number of local stash hits (response.hits, source-only
154
+ * entries). Always 0 for registry-only searches.
155
+ * - `registryHitCount`: number of registry hits (response.registryHits).
156
+ * Only non-zero when source is "registry" or "both".
157
+ * - `resultCount`: total across both pools so telemetry reflects the actual
158
+ * number of results the user saw, regardless of source mode.
159
+ *
160
+ * Per-entry events are recorded only for stash hits because registry hits
161
+ * have no local entry_id to reference.
150
162
  */
151
163
  function logSearchEvent(query, response, existingDb) {
164
+ // Emit a structured event to events.jsonl so workflow-trace consumers
165
+ // detect akm search invocations without relying on stdout scraping.
166
+ const stashHits = response.hits.filter((h) => h.type !== "registry");
167
+ appendEvent({
168
+ eventType: "search",
169
+ metadata: { query, hitCount: stashHits.length, resultRefs: stashHits.map((h) => h.ref) },
170
+ });
152
171
  try {
153
172
  const db = existingDb ?? openDatabase();
154
173
  try {
155
- const stashHits = response.hits.filter((h) => h.type !== "registry").slice(0, 50);
156
- const resolved = resolveEntryIds(db, stashHits);
174
+ const resolved = resolveEntryIds(db, stashHits.slice(0, 50));
157
175
  for (const { entryId, ref } of resolved) {
158
176
  insertUsageEvent(db, {
159
177
  event_type: "search",
@@ -162,10 +180,19 @@ function logSearchEvent(query, response, existingDb) {
162
180
  entry_ref: ref,
163
181
  });
164
182
  }
183
+ // Count registry hits separately so registry-only searches record a
184
+ // non-zero resultCount. response.hits is always [] when source="registry".
185
+ const stashHitCount = response.hits.length;
186
+ const registryHitCount = Array.isArray(response.registryHits) ? response.registryHits.length : 0;
165
187
  insertUsageEvent(db, {
166
188
  event_type: "search",
167
189
  query,
168
- metadata: JSON.stringify({ resultCount: response.hits.length, resolvedCount: resolved.length }),
190
+ metadata: JSON.stringify({
191
+ resultCount: stashHitCount + registryHitCount,
192
+ stashHitCount,
193
+ registryHitCount,
194
+ resolvedCount: resolved.length,
195
+ }),
169
196
  });
170
197
  }
171
198
  finally {
@@ -24,6 +24,7 @@ import path from "node:path";
24
24
  import { parseAssetRef } from "../core/asset-ref";
25
25
  import { loadConfig } from "../core/config";
26
26
  import { NotFoundError, UsageError } from "../core/errors";
27
+ import { appendEvent, readEvents } from "../core/events";
27
28
  import { parseFrontmatter, toStringOrUndefined } from "../core/frontmatter";
28
29
  import { closeDatabase, findEntryIdByRef, openDatabase } from "../indexer/db";
29
30
  import { buildFileContext, buildRenderContext, getRenderer, runMatchers } from "../indexer/file-context";
@@ -35,6 +36,7 @@ import { resolveSourcesForOrigin } from "../registry/origin-resolve";
35
36
  // Eagerly import source providers to trigger self-registration.
36
37
  import "../sources/providers/index";
37
38
  import { resolveAssetPath } from "../sources/resolve";
39
+ import { getActiveWorkflowRun } from "../workflows/runs";
38
40
  /**
39
41
  * Show a wiki root (no page path) — returns the same payload as
40
42
  * `akm wiki show <name>`.
@@ -136,7 +138,13 @@ export async function akmShowUnified(input) {
136
138
  if (input.scope && hasAnyScopeKey(input.scope) && result.path) {
137
139
  enforceScopeOrThrow(result.path, ref, input.scope);
138
140
  }
141
+ // Count prior shows of this ref before logging the current one.
142
+ const priorShowCount = recentShowCount(ref);
139
143
  logShowEvent(ref);
144
+ if (priorShowCount >= 2) {
145
+ // Agent has shown this same asset 3+ times — inject a loop-break hint.
146
+ result.showLoopWarning = priorShowCount + 1;
147
+ }
140
148
  return result;
141
149
  }
142
150
  function hasAnyScopeKey(scope) {
@@ -176,7 +184,24 @@ function enforceScopeOrThrow(filePath, ref, scope) {
176
184
  }
177
185
  }
178
186
  }
187
+ /**
188
+ * Count how many times `ref` has been shown in the current session by reading
189
+ * recent events. Returns the count BEFORE the current invocation.
190
+ */
191
+ function recentShowCount(ref) {
192
+ try {
193
+ const { events } = readEvents({ type: "show", ref });
194
+ return events.length;
195
+ }
196
+ catch {
197
+ return 0;
198
+ }
199
+ }
179
200
  function logShowEvent(ref, existingDb) {
201
+ // Emit a structured event to events.jsonl so workflow-trace consumers
202
+ // detect akm show invocations without relying on stdout scraping.
203
+ const parsed = parseAssetRef(ref);
204
+ appendEvent({ eventType: "show", ref, metadata: { type: parsed.type, name: parsed.name } });
180
205
  try {
181
206
  const db = existingDb ?? openDatabase();
182
207
  try {
@@ -295,6 +320,10 @@ export async function showLocal(input) {
295
320
  editable,
296
321
  ...(!editable ? { editHint: buildEditHint(assetPath, parsed.type, parsed.name, source?.registryId) } : {}),
297
322
  };
323
+ const activeRun = getActiveWorkflowRun();
324
+ if (activeRun) {
325
+ fullResponse.activeRun = activeRun;
326
+ }
298
327
  if (input.detail === "brief") {
299
328
  return buildBriefResponse(fullResponse, assetPath);
300
329
  }