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
@@ -14,6 +14,7 @@ import { getDefaultStashDir } from "../core/paths";
14
14
  import { closeDatabase, isVecAvailable, openDatabase } from "../indexer/db";
15
15
  import { akmIndex } from "../indexer/indexer";
16
16
  import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "../indexer/semantic-status";
17
+ import { detectAgentCliProfiles, pickDefaultAgentProfile, } from "../integrations/agent";
17
18
  import { probeLlmCapabilities } from "../llm/client";
18
19
  import { checkEmbeddingAvailability, DEFAULT_LOCAL_MODEL, isTransformersAvailable } from "../llm/embedder";
19
20
  import { detectAgentPlatforms, detectOllama } from "./detect";
@@ -284,6 +285,7 @@ async function stepOllama(current) {
284
285
  bge: 384,
285
286
  };
286
287
  const guessedDim = Object.entries(knownDims).find(([k]) => embChoice.includes(k))?.[1] ?? 384;
288
+ p.note("Embedding dimension must match the model. Common values: 384 (BGE small), 768 (BGE base), 1024 (BGE large). Press Enter to accept the detected default.", "Embedding dimension");
287
289
  const dimChoice = await prompt(() => p.text({
288
290
  message: `Embedding dimension for ${embChoice}:`,
289
291
  placeholder: String(guessedDim),
@@ -313,7 +315,6 @@ const LLM_PRESETS = [
313
315
  endpoint: "https://api.anthropic.com/v1/chat/completions",
314
316
  defaultModel: "claude-sonnet-4-5",
315
317
  hint: "beta OpenAI-compat layer; set AKM_LLM_API_KEY; override the model if the default is unavailable",
316
- contextWindow: 200_000,
317
318
  },
318
319
  {
319
320
  value: "openai",
@@ -321,7 +322,6 @@ const LLM_PRESETS = [
321
322
  endpoint: "https://api.openai.com/v1/chat/completions",
322
323
  defaultModel: "gpt-4o-mini",
323
324
  hint: "AKM_LLM_API_KEY required",
324
- contextWindow: 128_000,
325
325
  },
326
326
  {
327
327
  value: "google",
@@ -329,7 +329,6 @@ const LLM_PRESETS = [
329
329
  endpoint: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
330
330
  defaultModel: "gemini-2.0-flash",
331
331
  hint: "OpenAI-compat endpoint, AKM_LLM_API_KEY required",
332
- contextWindow: 1_000_000,
333
332
  },
334
333
  ];
335
334
  /**
@@ -431,7 +430,6 @@ export async function stepLlm(current, ollamaEndpoint, ollamaChatModels) {
431
430
  model: model.trim() || preset.defaultModel,
432
431
  temperature: 0.3,
433
432
  maxTokens: 1024,
434
- contextWindow: preset.contextWindow,
435
433
  };
436
434
  }
437
435
  // Remind the user about API key placement. We do not offer a "store in config"
@@ -649,6 +647,29 @@ async function stepAgentPlatforms(current) {
649
647
  }
650
648
  return entries;
651
649
  }
650
+ /**
651
+ * Detect installed agent CLIs and produce an updated `agent` config block
652
+ * with a sensible `default` (the first detected profile that the user has
653
+ * not already overridden).
654
+ *
655
+ * Pure-ish: file system / PATH probes are routed through `detectFn` so
656
+ * tests can drive the branches without touching the real PATH.
657
+ *
658
+ * @internal Exported for testing only.
659
+ */
660
+ export function stepAgentCliDetection(current, detectFn = detectAgentCliProfiles) {
661
+ const detections = detectFn(current.agent);
662
+ const defaultName = pickDefaultAgentProfile(detections, current.agent?.default);
663
+ // No installed agents found and no existing config → leave block absent.
664
+ if (!defaultName && !current.agent) {
665
+ return { detections };
666
+ }
667
+ const agent = {
668
+ ...(current.agent ?? {}),
669
+ ...(defaultName ? { default: defaultName } : {}),
670
+ };
671
+ return { agent, detections };
672
+ }
652
673
  // ── Main Wizard ─────────────────────────────────────────────────────────────
653
674
  /**
654
675
  * Build the canonical list of `SetupStep`s for the interactive wizard.
@@ -731,6 +752,23 @@ export function buildSetupSteps(options) {
731
752
  ctx.apply({ sources: merged.length > 0 ? merged : undefined });
732
753
  },
733
754
  },
755
+ {
756
+ id: "agent-cli",
757
+ label: "Agent CLI",
758
+ nonInteractive: true,
759
+ async run(ctx) {
760
+ const result = stepAgentCliDetection(ctx.config);
761
+ const detected = result.detections.filter((d) => d.available);
762
+ if (detected.length > 0) {
763
+ p.log.info(`Detected agent CLIs: ${detected.map((d) => d.name).join(", ")}.` +
764
+ (result.agent?.default ? ` Default profile: ${result.agent.default}.` : ""));
765
+ }
766
+ else {
767
+ p.log.info("No agent CLIs detected on PATH. Agent commands will be disabled until one is installed and `akm setup` is re-run.");
768
+ }
769
+ ctx.apply({ agent: result.agent });
770
+ },
771
+ },
734
772
  ];
735
773
  return { steps, outcome };
736
774
  }
@@ -771,7 +809,7 @@ export async function runSetupWizard() {
771
809
  const embedding = newConfig.embedding;
772
810
  const llm = newConfig.llm;
773
811
  const registries = newConfig.registries;
774
- const allStashes = newConfig.stashes ?? [];
812
+ const allStashes = newConfig.sources ?? newConfig.stashes ?? [];
775
813
  // Confirm before saving
776
814
  const effectiveRegistries = registries ?? DEFAULT_CONFIG.registries ?? [];
777
815
  p.note([
@@ -6,6 +6,7 @@ import { resolveStashDir } from "../../core/common";
6
6
  import { loadConfig } from "../../core/config";
7
7
  import { ConfigError, UsageError } from "../../core/errors";
8
8
  import { getRegistryCacheDir, getRegistryIndexCacheDir } from "../../core/paths";
9
+ import { sanitizeCommitMessage } from "../../core/write-source";
9
10
  import { parseRegistryRef, resolveRegistryArtifact, validateGitRef, validateGitUrl } from "../../registry/resolve";
10
11
  import { registerSourceProvider } from "../provider-factory";
11
12
  import { applyAkmIncludeConfig, buildInstallCacheDir, copyDirectoryContents, detectStashRoot, isDirectory, isExpired, sanitizeString, } from "./provider-utils";
@@ -77,20 +78,6 @@ function getCachePaths(repoUrl) {
77
78
  const key = createHash("sha256").update(repoUrl).digest("hex").slice(0, 16);
78
79
  const cacheRoot = getRegistryIndexCacheDir();
79
80
  const rootDir = path.join(cacheRoot, `git-${key}`);
80
- // One-time silent migration: legacy `context-hub-${key}` directories were
81
- // created for ALL git stashes (not just the andrewyng/context-hub repo). If
82
- // the new path doesn't yet exist but the legacy one does, rename it in place
83
- // so existing clones aren't silently invalidated. Failures are non-fatal —
84
- // worst case the repo is re-cloned on the next refresh.
85
- try {
86
- const legacyRootDir = path.join(cacheRoot, `context-hub-${key}`);
87
- if (!fs.existsSync(rootDir) && fs.existsSync(legacyRootDir)) {
88
- fs.renameSync(legacyRootDir, rootDir);
89
- }
90
- }
91
- catch {
92
- /* migration is best-effort */
93
- }
94
81
  return {
95
82
  rootDir,
96
83
  repoDir: path.join(rootDir, "repo"),
@@ -398,7 +385,12 @@ function parseGitRepoUrl(rawUrl) {
398
385
  */
399
386
  export function saveGitStash(name, message, writableOverride) {
400
387
  const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
401
- const commitMessage = message?.trim() || `akm save ${timestamp}`;
388
+ // Sanitize the user-supplied message: strip CR/LF/NUL, collapse whitespace,
389
+ // clamp length. An attacker can otherwise pass `--message "subject\n\n\
390
+ // Co-Authored-By: someone-else"` and forge trailers in the commit log.
391
+ // Empty result falls back to the timestamped default.
392
+ const sanitized = message ? sanitizeCommitMessage(message) : "";
393
+ const commitMessage = sanitized || `akm save ${timestamp}`;
402
394
  let repoDir;
403
395
  let writable = false;
404
396
  if (name) {
@@ -99,8 +99,9 @@ export function ensureWikiNameAvailable(stashDir, name) {
99
99
  * Walk a wiki directory and bucket files into pages vs raws.
100
100
  *
101
101
  * "Pages" are any `.md` files under the wiki root EXCEPT `schema.md`,
102
- * `index.md`, `log.md`, or anything under `raw/`. This matches the set the
103
- * agent edits, and the set `akm wiki pages` exposes.
102
+ * `index.md`, or `log.md`. Raw sources are bucketed separately so callers can
103
+ * distinguish authored pages from ingested source material while still
104
+ * surfacing both.
104
105
  *
105
106
  * Returns two mtime signals:
106
107
  * - `lastModifiedMs` — newest across all .md files. Used for the `show` /
@@ -495,15 +496,16 @@ function readPageFrontmatter(absPath) {
495
496
  return out;
496
497
  }
497
498
  /**
498
- * List the pages in a wiki, excluding `schema.md`, `index.md`, `log.md`, and
499
- * anything under `raw/`. Each entry carries its ref (`wiki:<name>/<page>`),
500
- * path, and frontmatter-derived fields for orientation.
499
+ * List the addressable markdown entries in a wiki, excluding only the
500
+ * infrastructure files `schema.md`, `index.md`, and `log.md`. This includes
501
+ * both authored pages and `raw/` sources so `wiki pages` can inventory content
502
+ * written via `akm wiki stash`.
501
503
  */
502
504
  export function listPages(stashDir, name) {
503
505
  const wikiDir = resolveWikiSource(stashDir, name).path;
504
- const { pages } = scanWikiFiles(wikiDir);
506
+ const { pages, raws } = scanWikiFiles(wikiDir);
505
507
  const result = [];
506
- for (const abs of pages) {
508
+ for (const abs of [...pages, ...raws]) {
507
509
  const pageName = pageNameFromPath(wikiDir, abs);
508
510
  const ref = `wiki:${name}/${pageName}`;
509
511
  const fm = readPageFrontmatter(abs);
@@ -540,7 +542,6 @@ export async function searchInWiki(input) {
540
542
  }
541
543
  throw err;
542
544
  }
543
- const rawDir = path.join(wikiDir, RAW_SUBDIR);
544
545
  const filtered = [];
545
546
  for (const hit of response.hits) {
546
547
  // hits can be SourceSearchHit or RegistrySearchResultHit (union); filter
@@ -556,9 +557,6 @@ export async function searchInWiki(input) {
556
557
  const basename = path.basename(stashHit.path);
557
558
  if (WIKI_SPECIAL_FILES.has(basename) && path.dirname(stashHit.path) === wikiDir)
558
559
  continue;
559
- // Exclude anything under raw/
560
- if (isWithin(stashHit.path, rawDir))
561
- continue;
562
560
  filtered.push(stashHit);
563
561
  }
564
562
  return { ...response, hits: filtered, registryHits: undefined };
@@ -0,0 +1,119 @@
1
+ import { afterAll, afterEach, describe, expect, test } from "bun:test";
2
+ import { spawn } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ const CLI = path.join(__dirname, "..", "src", "cli.ts");
7
+ const tempDirs = [];
8
+ const servers = [];
9
+ const CLI_TIMEOUT_MS = 30_000;
10
+ const TEST_TIMEOUT_MS = 60_000;
11
+ function makeTempDir(prefix) {
12
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
13
+ tempDirs.push(dir);
14
+ return dir;
15
+ }
16
+ function createWorkingStash() {
17
+ const dir = makeTempDir("akm-add-website-stash-");
18
+ for (const sub of ["skills", "commands", "agents", "knowledge", "scripts"]) {
19
+ fs.mkdirSync(path.join(dir, sub), { recursive: true });
20
+ }
21
+ return dir;
22
+ }
23
+ function serveWebsite() {
24
+ const server = Bun.serve({
25
+ port: 0,
26
+ fetch(request) {
27
+ const url = new URL(request.url);
28
+ if (url.pathname === "/") {
29
+ return new Response("<html><head><title>Example Docs</title></head><body><h1>Example Docs</h1><p>Welcome to the docs.</p><a href='/getting-started'>Getting started</a></body></html>", {
30
+ headers: { "Content-Type": "text/html; charset=utf-8" },
31
+ });
32
+ }
33
+ if (url.pathname === "/getting-started") {
34
+ return new Response("<html><body><h1>Getting started</h1><p>Run setup first.</p></body></html>", {
35
+ headers: { "Content-Type": "text/html; charset=utf-8" },
36
+ });
37
+ }
38
+ return new Response("not found", { status: 404 });
39
+ },
40
+ });
41
+ servers.push(server);
42
+ return `http://127.0.0.1:${server.port}`;
43
+ }
44
+ afterEach(() => {
45
+ for (const server of servers.splice(0)) {
46
+ server.stop(true);
47
+ }
48
+ for (const dir of tempDirs.splice(0)) {
49
+ fs.rmSync(dir, { recursive: true, force: true });
50
+ }
51
+ });
52
+ afterAll(() => {
53
+ for (const server of servers.splice(0)) {
54
+ server.stop(true);
55
+ }
56
+ });
57
+ describe("akm add website", () => {
58
+ test("adds a website stash source, caches markdown, and indexes it", async () => {
59
+ const stashDir = createWorkingStash();
60
+ const xdgCache = makeTempDir("akm-add-website-cache-");
61
+ const xdgConfig = makeTempDir("akm-add-website-config-");
62
+ const websiteUrl = serveWebsite();
63
+ const configDir = path.join(xdgConfig, "akm");
64
+ fs.mkdirSync(configDir, { recursive: true });
65
+ fs.writeFileSync(path.join(configDir, "config.json"), `${JSON.stringify({ semanticSearchMode: "off" }, null, 2)}\n`);
66
+ const child = spawn("bun", [CLI, "add", websiteUrl, "--name", "docs-site", "--format=json"], {
67
+ stdio: ["ignore", "pipe", "pipe"],
68
+ env: {
69
+ ...process.env,
70
+ AKM_STASH_DIR: stashDir,
71
+ XDG_CACHE_HOME: xdgCache,
72
+ XDG_CONFIG_HOME: xdgConfig,
73
+ },
74
+ });
75
+ let stdout = "";
76
+ let stderr = "";
77
+ child.stdout.on("data", (chunk) => {
78
+ stdout += String(chunk);
79
+ });
80
+ child.stderr.on("data", (chunk) => {
81
+ stderr += String(chunk);
82
+ });
83
+ const exitCode = await new Promise((resolve, reject) => {
84
+ const timer = setTimeout(() => {
85
+ child.kill("SIGKILL");
86
+ reject(new Error("CLI website add timed out"));
87
+ }, CLI_TIMEOUT_MS);
88
+ child.on("error", (err) => {
89
+ clearTimeout(timer);
90
+ reject(err);
91
+ });
92
+ child.on("close", (code) => {
93
+ clearTimeout(timer);
94
+ resolve(code ?? 1);
95
+ });
96
+ });
97
+ expect(exitCode).toBe(0);
98
+ expect(stderr.trim()).toBe("");
99
+ const parsed = JSON.parse(stdout.trim());
100
+ const normalizedWebsiteUrl = `${websiteUrl}/`;
101
+ expect(parsed.sourceAdded).toBeDefined();
102
+ expect(parsed.sourceAdded?.type).toBe("website");
103
+ expect(parsed.sourceAdded?.url).toBe(normalizedWebsiteUrl);
104
+ expect(parsed.sourceAdded?.name).toBe("docs-site");
105
+ expect(parsed.index?.totalEntries).toBeGreaterThanOrEqual(2);
106
+ const configPath = path.join(xdgConfig, "akm", "config.json");
107
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
108
+ expect(config.sources).toContainEqual({
109
+ type: "website",
110
+ url: normalizedWebsiteUrl,
111
+ name: "docs-site",
112
+ });
113
+ expect(parsed.sourceAdded?.stashRoot).toBeDefined();
114
+ const knowledgeFiles = fs.readdirSync(path.join(parsed.sourceAdded?.stashRoot, "knowledge")).sort();
115
+ expect(knowledgeFiles).toEqual(["getting-started.md", "index.md"]);
116
+ const homeDoc = fs.readFileSync(path.join(parsed.sourceAdded?.stashRoot, "knowledge", "index.md"), "utf8");
117
+ expect(homeDoc).toContain("Example Docs");
118
+ }, { timeout: TEST_TIMEOUT_MS });
119
+ });
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Integration test: the AkmConfig loader propagates the `agent` block
3
+ * through `loadConfig()` (and through the on-disk JSONC parser, exercising
4
+ * the `pickKnownKeys` path).
5
+ *
6
+ * The acceptance criterion "config schema accepts an optional agent block"
7
+ * lives at the loader boundary, not just the parser; this test pins the
8
+ * end-to-end shape.
9
+ */
10
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
11
+ import fs from "node:fs";
12
+ import os from "node:os";
13
+ import path from "node:path";
14
+ let tmpHome;
15
+ let originalHome;
16
+ let originalXdg;
17
+ beforeEach(() => {
18
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "akm-agent-cfg-"));
19
+ originalHome = process.env.HOME;
20
+ originalXdg = process.env.XDG_CONFIG_HOME;
21
+ process.env.HOME = tmpHome;
22
+ process.env.XDG_CONFIG_HOME = path.join(tmpHome, ".config");
23
+ });
24
+ afterEach(() => {
25
+ if (originalHome === undefined)
26
+ delete process.env.HOME;
27
+ else
28
+ process.env.HOME = originalHome;
29
+ if (originalXdg === undefined)
30
+ delete process.env.XDG_CONFIG_HOME;
31
+ else
32
+ process.env.XDG_CONFIG_HOME = originalXdg;
33
+ fs.rmSync(tmpHome, { recursive: true, force: true });
34
+ });
35
+ describe("AkmConfig loader — agent block", () => {
36
+ test("loads agent.default + agent.profiles from disk", async () => {
37
+ const { getConfigPath, loadUserConfig, resetConfigCache } = await import("../../src/core/config");
38
+ const cfgPath = getConfigPath();
39
+ fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
40
+ fs.writeFileSync(cfgPath, JSON.stringify({
41
+ semanticSearchMode: "auto",
42
+ agent: {
43
+ default: "claude",
44
+ timeoutMs: 45000,
45
+ profiles: {
46
+ claude: { args: ["--print"] },
47
+ rover: { bin: "rover-cli", parseOutput: "json" },
48
+ },
49
+ // Unknown key — must not throw at load.
50
+ mystery: 1,
51
+ },
52
+ }, null, 2));
53
+ resetConfigCache();
54
+ const cfg = loadUserConfig();
55
+ expect(cfg.agent?.default).toBe("claude");
56
+ expect(cfg.agent?.timeoutMs).toBe(45000);
57
+ expect(cfg.agent?.profiles?.claude?.args).toEqual(["--print"]);
58
+ expect(cfg.agent?.profiles?.rover?.bin).toBe("rover-cli");
59
+ expect(cfg.agent?.profiles?.rover?.parseOutput).toBe("json");
60
+ });
61
+ test("agent block absent → cfg.agent is undefined → requireAgentProfile throws", async () => {
62
+ const { loadUserConfig, resetConfigCache } = await import("../../src/core/config");
63
+ const { requireAgentProfile } = await import("../../src/integrations/agent/config");
64
+ const { ConfigError } = await import("../../src/core/errors");
65
+ resetConfigCache();
66
+ const cfg = loadUserConfig();
67
+ expect(cfg.agent).toBeUndefined();
68
+ expect(() => requireAgentProfile(cfg.agent)).toThrow(ConfigError);
69
+ });
70
+ });
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Tests for the `agent.*` config block parser and profile resolver.
3
+ *
4
+ * Acceptance coverage:
5
+ * • Parser accepts the documented shape.
6
+ * • Unknown keys are warn-and-ignored (no throw).
7
+ * • Built-in profiles resolve for opencode, claude, codex, gemini, aider.
8
+ * • Missing block surfaces a stable ConfigError via requireAgentProfile.
9
+ */
10
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
11
+ const warnings = [];
12
+ // NOTE: `mock.module` in Bun is process-global — once installed it persists
13
+ // across test files run in the same `bun test` invocation. So this mock has
14
+ // to remain a faithful drop-in for the real `src/core/warn` module:
15
+ //
16
+ // 1. Every export that the real module ships must be represented here,
17
+ // otherwise tests in other files that import a missing export get
18
+ // `undefined` and silently break (issue #273).
19
+ // 2. `warn()` must also forward to `console.warn` so other test files that
20
+ // capture stderr (e.g. the noise-gate tests in
21
+ // tests/workflows/indexer-rejection.test.ts) continue to see the calls.
22
+ // We push to the local `warnings[]` so this file's own assertions still
23
+ // work, AND forward to `console.warn` so callers that intercept it
24
+ // still observe what was emitted.
25
+ let mockedQuiet = false;
26
+ let mockedVerbose = false;
27
+ mock.module("../../src/core/warn", () => ({
28
+ warn: (...args) => {
29
+ warnings.push(args.join(" "));
30
+ if (!mockedQuiet)
31
+ console.warn(...args);
32
+ },
33
+ warnVerbose: (...args) => {
34
+ if (!mockedVerbose)
35
+ return;
36
+ warnings.push(args.join(" "));
37
+ if (!mockedQuiet)
38
+ console.warn(...args);
39
+ },
40
+ setQuiet: (value) => {
41
+ mockedQuiet = value;
42
+ },
43
+ resetQuiet: () => {
44
+ mockedQuiet = false;
45
+ },
46
+ isQuiet: () => mockedQuiet,
47
+ setVerbose: (value) => {
48
+ mockedVerbose = value;
49
+ },
50
+ resetVerbose: () => {
51
+ mockedVerbose = false;
52
+ },
53
+ isVerbose: () => {
54
+ const env = process.env.AKM_VERBOSE?.trim().toLowerCase();
55
+ if (env === "1" || env === "true" || env === "yes" || env === "on")
56
+ return true;
57
+ if (env === "0" || env === "false" || env === "no" || env === "off")
58
+ return false;
59
+ return mockedVerbose;
60
+ },
61
+ }));
62
+ beforeEach(() => {
63
+ warnings.length = 0;
64
+ });
65
+ afterEach(() => {
66
+ warnings.length = 0;
67
+ });
68
+ describe("parseAgentConfig", () => {
69
+ test("returns undefined when block is absent", async () => {
70
+ const { parseAgentConfig } = await import("../../src/integrations/agent/config");
71
+ expect(parseAgentConfig(undefined)).toBeUndefined();
72
+ expect(warnings).toHaveLength(0);
73
+ });
74
+ test("warns and returns undefined for non-object root", async () => {
75
+ const { parseAgentConfig } = await import("../../src/integrations/agent/config");
76
+ expect(parseAgentConfig("oops")).toBeUndefined();
77
+ expect(warnings.some((w) => w.includes('"agent"'))).toBe(true);
78
+ });
79
+ test("accepts the documented shape", async () => {
80
+ const { parseAgentConfig } = await import("../../src/integrations/agent/config");
81
+ const parsed = parseAgentConfig({
82
+ default: "opencode",
83
+ timeoutMs: 30000,
84
+ profiles: {
85
+ opencode: { bin: "opencode", args: ["--non-interactive"], stdio: "captured" },
86
+ },
87
+ });
88
+ expect(parsed?.default).toBe("opencode");
89
+ expect(parsed?.timeoutMs).toBe(30000);
90
+ expect(parsed?.profiles?.opencode).toEqual({
91
+ bin: "opencode",
92
+ args: ["--non-interactive"],
93
+ stdio: "captured",
94
+ });
95
+ });
96
+ test("warn-and-ignore unknown top-level keys (no throw)", async () => {
97
+ const { parseAgentConfig } = await import("../../src/integrations/agent/config");
98
+ const parsed = parseAgentConfig({
99
+ default: "claude",
100
+ moonRoutingTable: { foo: "bar" }, // unknown
101
+ });
102
+ expect(parsed?.default).toBe("claude");
103
+ expect(warnings.some((w) => w.includes("moonRoutingTable"))).toBe(true);
104
+ });
105
+ test("warn-and-ignore unknown per-profile keys", async () => {
106
+ const { parseAgentConfig } = await import("../../src/integrations/agent/config");
107
+ const parsed = parseAgentConfig({
108
+ profiles: {
109
+ custom: { bin: "ok", quirks: "nope" },
110
+ },
111
+ });
112
+ expect(parsed?.profiles?.custom?.bin).toBe("ok");
113
+ expect(warnings.some((w) => w.includes("quirks"))).toBe(true);
114
+ });
115
+ test("warn-and-ignore malformed timeoutMs", async () => {
116
+ const { parseAgentConfig } = await import("../../src/integrations/agent/config");
117
+ const parsed = parseAgentConfig({ timeoutMs: "60s" });
118
+ expect(parsed?.timeoutMs).toBeUndefined();
119
+ expect(warnings.some((w) => w.includes("timeoutMs"))).toBe(true);
120
+ });
121
+ test("rejects non-string args entries", async () => {
122
+ const { parseAgentConfig } = await import("../../src/integrations/agent/config");
123
+ const parsed = parseAgentConfig({
124
+ profiles: { opencode: { args: ["--ok", 5, "--also-ok"] } },
125
+ });
126
+ expect(parsed?.profiles?.opencode?.args).toEqual(["--ok", "--also-ok"]);
127
+ expect(warnings.some((w) => w.includes("args"))).toBe(true);
128
+ });
129
+ test("rejects bad stdio mode", async () => {
130
+ const { parseAgentConfig } = await import("../../src/integrations/agent/config");
131
+ const parsed = parseAgentConfig({
132
+ profiles: { opencode: { stdio: "weird" } },
133
+ });
134
+ expect(parsed?.profiles?.opencode?.stdio).toBeUndefined();
135
+ expect(warnings.some((w) => w.includes("stdio"))).toBe(true);
136
+ });
137
+ });
138
+ describe("built-in profile resolution", () => {
139
+ test("resolves opencode, claude, codex, gemini, aider out of the box", async () => {
140
+ const { BUILTIN_AGENT_PROFILE_NAMES, getBuiltinAgentProfile } = await import("../../src/integrations/agent/profiles");
141
+ expect(BUILTIN_AGENT_PROFILE_NAMES).toEqual(["aider", "claude", "codex", "gemini", "opencode"]);
142
+ for (const name of ["opencode", "claude", "codex", "gemini", "aider"]) {
143
+ const profile = getBuiltinAgentProfile(name);
144
+ expect(profile).toBeDefined();
145
+ expect(profile?.bin).toBeTruthy();
146
+ expect(profile?.envPassthrough).toContain("PATH");
147
+ }
148
+ });
149
+ test("user override merges on top of built-in", async () => {
150
+ const { resolveAgentProfile } = await import("../../src/integrations/agent/config");
151
+ const merged = resolveAgentProfile("opencode", { args: ["--scripted"], stdio: "captured" });
152
+ expect(merged?.bin).toBe("opencode"); // built-in default
153
+ expect(merged?.args).toEqual(["--scripted"]); // override
154
+ expect(merged?.stdio).toBe("captured"); // override
155
+ expect(merged?.envPassthrough).toContain("PATH"); // built-in retained
156
+ });
157
+ test("user-defined profile (no built-in) requires bin", async () => {
158
+ const { resolveAgentProfile } = await import("../../src/integrations/agent/config");
159
+ expect(resolveAgentProfile("rover", undefined)).toBeUndefined();
160
+ expect(resolveAgentProfile("rover", {})).toBeUndefined();
161
+ const ok = resolveAgentProfile("rover", { bin: "rover-cli", args: ["--silent"] });
162
+ expect(ok?.bin).toBe("rover-cli");
163
+ expect(ok?.args).toEqual(["--silent"]);
164
+ expect(ok?.stdio).toBe("captured");
165
+ });
166
+ test("envPassthrough merges base + override", async () => {
167
+ const { resolveAgentProfile } = await import("../../src/integrations/agent/config");
168
+ const merged = resolveAgentProfile("opencode", { envPassthrough: ["MY_TOKEN"] });
169
+ expect(merged?.envPassthrough).toContain("PATH"); // from built-in
170
+ expect(merged?.envPassthrough).toContain("MY_TOKEN"); // from override
171
+ });
172
+ test("listAgentProfileNames includes built-ins plus user-defined", async () => {
173
+ const { listAgentProfileNames } = await import("../../src/integrations/agent/config");
174
+ const names = listAgentProfileNames({ profiles: { rover: { bin: "rover" } } });
175
+ expect(names).toContain("rover");
176
+ expect(names).toContain("opencode");
177
+ expect(names).toContain("claude");
178
+ });
179
+ });
180
+ describe("requireAgentProfile", () => {
181
+ test("throws ConfigError when the agent block is missing", async () => {
182
+ const { requireAgentProfile } = await import("../../src/integrations/agent/config");
183
+ const { ConfigError } = await import("../../src/core/errors");
184
+ let caught;
185
+ try {
186
+ requireAgentProfile(undefined);
187
+ }
188
+ catch (err) {
189
+ caught = err;
190
+ }
191
+ expect(caught).toBeInstanceOf(ConfigError);
192
+ expect(caught.message).toContain("agent commands are disabled");
193
+ const hint = caught.hint();
194
+ expect(hint).toBeTruthy();
195
+ expect(hint).toContain("akm setup");
196
+ });
197
+ test("throws when no default and no requested name", async () => {
198
+ const { requireAgentProfile } = await import("../../src/integrations/agent/config");
199
+ const { ConfigError } = await import("../../src/core/errors");
200
+ let caught;
201
+ try {
202
+ requireAgentProfile({});
203
+ }
204
+ catch (err) {
205
+ caught = err;
206
+ }
207
+ expect(caught).toBeInstanceOf(ConfigError);
208
+ expect(caught.message).toContain("require a profile");
209
+ });
210
+ test("resolves the requested profile when valid", async () => {
211
+ const { requireAgentProfile } = await import("../../src/integrations/agent/config");
212
+ const profile = requireAgentProfile({ default: "claude" });
213
+ expect(profile.name).toBe("claude");
214
+ expect(profile.bin).toBe("claude");
215
+ });
216
+ test("explicit requested name beats config default", async () => {
217
+ const { requireAgentProfile } = await import("../../src/integrations/agent/config");
218
+ const profile = requireAgentProfile({ default: "claude" }, "codex");
219
+ expect(profile.name).toBe("codex");
220
+ });
221
+ });