claude-memory-layer 1.0.27 → 1.0.29

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 (331) hide show
  1. package/.env.example +7 -0
  2. package/AGENTS.md +11 -0
  3. package/README.md +374 -49
  4. package/benchmarks/replay/anonymized-real-sessions.json +48 -0
  5. package/dist/cli/index.js +10097 -6003
  6. package/dist/cli/index.js.map +4 -4
  7. package/dist/core/index.js +9745 -5587
  8. package/dist/core/index.js.map +4 -4
  9. package/dist/hooks/post-tool-use.js +6545 -5270
  10. package/dist/hooks/post-tool-use.js.map +4 -4
  11. package/dist/hooks/semantic-daemon.js +6646 -5354
  12. package/dist/hooks/semantic-daemon.js.map +4 -4
  13. package/dist/hooks/session-end.js +6618 -5347
  14. package/dist/hooks/session-end.js.map +4 -4
  15. package/dist/hooks/session-start.js +6619 -5354
  16. package/dist/hooks/session-start.js.map +4 -4
  17. package/dist/hooks/stop.js +6614 -5325
  18. package/dist/hooks/stop.js.map +4 -4
  19. package/dist/hooks/user-prompt-submit.js +6702 -5356
  20. package/dist/hooks/user-prompt-submit.js.map +4 -4
  21. package/dist/index.js +13537 -0
  22. package/dist/index.js.map +7 -0
  23. package/dist/mcp/index.js +20770 -0
  24. package/dist/mcp/index.js.map +7 -0
  25. package/dist/server/api/index.js +6632 -5319
  26. package/dist/server/api/index.js.map +4 -4
  27. package/dist/server/index.js +6667 -5340
  28. package/dist/server/index.js.map +4 -4
  29. package/dist/services/memory-service.js +6568 -5350
  30. package/dist/services/memory-service.js.map +4 -4
  31. package/dist/ui/assets/js/bootstrap.js +244 -0
  32. package/dist/ui/assets/js/chat.js +373 -0
  33. package/dist/ui/assets/js/disclosure.js +232 -0
  34. package/dist/ui/assets/js/modals.js +298 -0
  35. package/dist/ui/assets/js/overview.js +655 -0
  36. package/dist/ui/assets/js/state.js +72 -0
  37. package/dist/ui/assets/js/views.js +468 -0
  38. package/dist/ui/index.html +43 -1
  39. package/dist/ui/index.ts +3 -0
  40. package/dist/ui/style.css +222 -0
  41. package/docs/ARCHITECTURE_COMPARISON_AND_RECOMMENDATIONS.md +627 -0
  42. package/docs/HERMES_MEMORY_INGESTION_ANALYSIS.md +440 -0
  43. package/docs/MEMORY_USEFULNESS_AUDIT.md +371 -0
  44. package/docs/MEMORY_USEFULNESS_AUDIT_RAW.json +80 -0
  45. package/docs/MEMSEARCH_PROJECT_STRUCTURE_ANALYSIS.md +333 -0
  46. package/docs/PRODUCT_VALIDATION_MATRIX.md +82 -0
  47. package/docs/PROJECT_STRUCTURE_ANALYSIS.md +421 -0
  48. package/docs/REFACTORING_MILESTONES_AND_ISSUES.md +501 -0
  49. package/docs/REFACTORING_PLAN_THIN_CORE.md +414 -0
  50. package/docs/REFERENCE_PROJECT_ANALYSES.md +25 -0
  51. package/docs/SUPERLOCALMEMORY_PROJECT_STRUCTURE_ANALYSIS.md +452 -0
  52. package/docs/TARGET_ARCHITECTURE_AND_FOLDER_STRUCTURE.md +446 -0
  53. package/docs/architecture/comparison-index.md +47 -0
  54. package/docs/reports/codex-real-data-validation-20260505T040447Z.md +46 -0
  55. package/package.json +12 -5
  56. package/scripts/build.ts +25 -8
  57. package/scripts/generate-session-qrels.ts +126 -0
  58. package/scripts/postinstall-embedding-backend.cjs +142 -0
  59. package/scripts/replay-retrieval-benchmark.ts +69 -0
  60. package/specs/thin-core-refactor/context.md +275 -0
  61. package/specs/thin-core-refactor/plan.md +536 -0
  62. package/specs/thin-core-refactor/spec.md +465 -0
  63. package/src/adapters/claude/capture/index.ts +3 -0
  64. package/src/adapters/claude/context/index.ts +3 -0
  65. package/src/adapters/claude/hooks/index.ts +21 -0
  66. package/src/adapters/claude/hooks/post-tool-use.ts +239 -0
  67. package/src/adapters/claude/hooks/prompt-injection-policy.ts +104 -0
  68. package/src/adapters/claude/hooks/semantic-daemon-client.ts +209 -0
  69. package/src/adapters/claude/hooks/semantic-daemon.ts +283 -0
  70. package/src/adapters/claude/hooks/session-end.ts +59 -0
  71. package/src/adapters/claude/hooks/session-start.ts +73 -0
  72. package/src/adapters/claude/hooks/stop.ts +128 -0
  73. package/src/adapters/claude/hooks/user-prompt-submit.ts +361 -0
  74. package/src/adapters/claude/index.ts +4 -0
  75. package/src/adapters/claude/transcript/index.ts +4 -0
  76. package/src/adapters/claude/transcript/transcript-reader.ts +57 -0
  77. package/src/adapters/claude/transcript/turn-reconstructor.ts +65 -0
  78. package/src/apps/cli/claude-settings-hooks.ts +138 -0
  79. package/src/apps/cli/codex-import-runner.ts +125 -0
  80. package/src/apps/cli/codex-validation-output.ts +95 -0
  81. package/src/apps/cli/hermes-import-runner.ts +130 -0
  82. package/src/apps/cli/hermes-validation-output.ts +91 -0
  83. package/src/apps/cli/index.ts +1731 -0
  84. package/src/apps/cli/mcp-install.ts +106 -0
  85. package/src/apps/cli/retrieval-disclosure-output.ts +196 -0
  86. package/src/apps/dashboard/assets/js/bootstrap.js +244 -0
  87. package/src/apps/dashboard/assets/js/chat.js +373 -0
  88. package/src/apps/dashboard/assets/js/disclosure.js +232 -0
  89. package/src/apps/dashboard/assets/js/modals.js +298 -0
  90. package/src/apps/dashboard/assets/js/overview.js +655 -0
  91. package/src/apps/dashboard/assets/js/state.js +72 -0
  92. package/src/apps/dashboard/assets/js/views.js +468 -0
  93. package/src/{ui → apps/dashboard}/index.html +43 -1
  94. package/src/apps/dashboard/index.ts +3 -0
  95. package/src/{ui → apps/dashboard}/style.css +222 -0
  96. package/src/apps/index.ts +5 -0
  97. package/src/apps/server/api/chat.ts +244 -0
  98. package/src/apps/server/api/citations.ts +105 -0
  99. package/src/apps/server/api/events.ts +137 -0
  100. package/src/apps/server/api/health.ts +53 -0
  101. package/src/apps/server/api/index.ts +26 -0
  102. package/src/apps/server/api/projects.ts +74 -0
  103. package/src/apps/server/api/search.ts +184 -0
  104. package/src/apps/server/api/sessions.ts +115 -0
  105. package/src/apps/server/api/stats.ts +723 -0
  106. package/src/apps/server/api/turns.ts +143 -0
  107. package/src/apps/server/api/utils.ts +65 -0
  108. package/src/apps/server/index.ts +111 -0
  109. package/src/cli/index.ts +2 -1311
  110. package/src/cli/retrieval-disclosure-output.ts +2 -0
  111. package/src/compat/index.ts +5 -0
  112. package/src/core/derive/fact-deriver.ts +170 -0
  113. package/src/core/derive/index.ts +2 -0
  114. package/src/core/derive/summary-deriver.ts +76 -0
  115. package/src/core/embedder.ts +4 -152
  116. package/src/core/engine/embedding-maintenance-service.ts +187 -0
  117. package/src/core/engine/endless-memory-services.ts +4 -0
  118. package/src/core/engine/index.ts +19 -0
  119. package/src/core/engine/memory-engine-services.ts +170 -0
  120. package/src/core/engine/memory-ingest-service.ts +317 -0
  121. package/src/core/engine/memory-query-service.ts +173 -0
  122. package/src/core/engine/memory-runtime-service.ts +162 -0
  123. package/src/core/engine/memory-service-composition.ts +231 -0
  124. package/src/core/engine/retrieval-analytics-service.ts +181 -0
  125. package/src/core/engine/retrieval-disclosure-service.ts +420 -0
  126. package/src/core/engine/retrieval-orchestrator.ts +377 -0
  127. package/src/core/engine/retrieval-services.ts +176 -0
  128. package/src/core/engine/shared-memory-services.ts +4 -0
  129. package/src/core/entity-repo.ts +1 -3
  130. package/src/core/event-store.ts +3 -3
  131. package/src/core/evidence-aligner.ts +2 -2
  132. package/src/core/external-market-context.ts +582 -0
  133. package/src/core/graduation.ts +2 -3
  134. package/src/core/index.ts +21 -0
  135. package/src/core/matcher.ts +2 -4
  136. package/src/core/model/memory-fact.ts +30 -0
  137. package/src/core/model/memory-rule.ts +14 -0
  138. package/src/core/model/memory-summary.ts +21 -0
  139. package/src/core/model/raw-event.ts +28 -0
  140. package/src/core/model/retrieval-result.ts +35 -0
  141. package/src/core/privacy/filter.ts +21 -10
  142. package/src/core/product-validation-matrix.ts +314 -0
  143. package/src/core/progressive-retriever.ts +1 -2
  144. package/src/core/registry/project-path.ts +54 -0
  145. package/src/core/registry/session-registry.ts +69 -0
  146. package/src/core/replay-evaluator.ts +625 -0
  147. package/src/core/retrieval-benchmark.ts +117 -0
  148. package/src/core/retrieval-quality.ts +109 -0
  149. package/src/core/retriever.ts +53 -15
  150. package/src/core/session-qrels.ts +360 -0
  151. package/src/core/shared-event-store.ts +1 -1
  152. package/src/core/sqlite-event-store.ts +35 -11
  153. package/src/core/task/blocker-resolver.ts +2 -2
  154. package/src/core/task/task-resolver.ts +0 -1
  155. package/src/core/vector-outbox.ts +1 -10
  156. package/src/core/vector-worker.ts +1 -1
  157. package/src/extensions/endless-memory/endless-memory-services.ts +350 -0
  158. package/src/extensions/endless-memory/index.ts +1 -0
  159. package/src/extensions/index.ts +5 -0
  160. package/src/extensions/mcp/handlers.ts +960 -0
  161. package/src/extensions/mcp/index.ts +48 -0
  162. package/src/extensions/mcp/tools.ts +252 -0
  163. package/src/extensions/shared-memory/index.ts +1 -0
  164. package/src/extensions/shared-memory/shared-memory-services.ts +211 -0
  165. package/src/extensions/vector/embedder.ts +197 -0
  166. package/src/extensions/vector/index.ts +1 -0
  167. package/src/hooks/post-tool-use.ts +3 -236
  168. package/src/hooks/semantic-daemon-client.ts +1 -208
  169. package/src/hooks/semantic-daemon.ts +6 -271
  170. package/src/hooks/session-end.ts +4 -79
  171. package/src/hooks/session-start.ts +4 -73
  172. package/src/hooks/stop.ts +3 -173
  173. package/src/hooks/user-prompt-submit.ts +3 -338
  174. package/src/index.ts +13 -0
  175. package/src/mcp/handlers.ts +2 -212
  176. package/src/mcp/index.ts +3 -46
  177. package/src/mcp/tools.ts +2 -78
  178. package/src/server/api/chat.ts +2 -244
  179. package/src/server/api/citations.ts +2 -105
  180. package/src/server/api/events.ts +2 -137
  181. package/src/server/api/health.ts +2 -53
  182. package/src/server/api/index.ts +2 -26
  183. package/src/server/api/projects.ts +2 -74
  184. package/src/server/api/search.ts +2 -102
  185. package/src/server/api/sessions.ts +2 -115
  186. package/src/server/api/stats.ts +2 -724
  187. package/src/server/api/turns.ts +2 -143
  188. package/src/server/api/utils.ts +2 -46
  189. package/src/server/index.ts +2 -100
  190. package/src/services/bootstrap-organizer.ts +46 -26
  191. package/src/services/codex-session-history-importer.ts +521 -29
  192. package/src/services/hermes-session-history-importer.ts +733 -0
  193. package/src/services/memory-service-config.ts +36 -0
  194. package/src/services/memory-service-registry.ts +150 -0
  195. package/src/services/memory-service.ts +211 -1325
  196. package/src/services/session-history-importer.ts +58 -14
  197. package/tests/README.md +23 -0
  198. package/tests/adapters/claude/claude-semantic-daemon-adapter.test.ts +54 -0
  199. package/tests/adapters/claude/claude-transcript-reconstructor.test.ts +98 -0
  200. package/tests/adapters/claude-hook-prompt-injection-policy.test.ts +99 -0
  201. package/tests/apps/app-layer-boundary.test.ts +48 -0
  202. package/tests/apps/claude-settings-hooks.test.ts +107 -0
  203. package/tests/apps/cli-disclosure-output.test.ts +212 -0
  204. package/tests/apps/codex-import-runner.test.ts +99 -0
  205. package/tests/apps/codex-validation-output.test.ts +100 -0
  206. package/tests/apps/hermes-import-runner.test.ts +99 -0
  207. package/tests/apps/mcp-install-command.test.ts +59 -0
  208. package/tests/apps/package-build-entrypoints.test.ts +30 -0
  209. package/tests/apps/postinstall-embedding-backend.test.ts +167 -0
  210. package/tests/apps/search-api-disclosure.test.ts +162 -0
  211. package/tests/apps/stats-api-lightweight.test.ts +67 -0
  212. package/tests/apps/ui-disclosure-output.test.ts +140 -0
  213. package/tests/{bootstrap-organizer.test.ts → core/bootstrap-organizer.test.ts} +1 -1
  214. package/tests/{canonical-key.test.ts → core/canonical-key.test.ts} +1 -1
  215. package/tests/core/codex-session-history-importer-validation.test.ts +185 -0
  216. package/tests/{consolidation-worker.test.ts → core/consolidation-worker.test.ts} +2 -2
  217. package/tests/core/embedding-maintenance-service.test.ts +282 -0
  218. package/tests/{evidence-aligner.test.ts → core/evidence-aligner.test.ts} +1 -1
  219. package/tests/core/external-market-context.test.ts +209 -0
  220. package/tests/core/fact-deriver.test.ts +79 -0
  221. package/tests/core/hermes-session-history-importer-validation.test.ts +609 -0
  222. package/tests/{ingest-interceptor.test.ts → core/ingest-interceptor.test.ts} +1 -1
  223. package/tests/{markdown-mirror.test.ts → core/markdown-mirror.test.ts} +2 -2
  224. package/tests/{matcher.test.ts → core/matcher.test.ts} +1 -1
  225. package/tests/{md-mirror.test.ts → core/md-mirror.test.ts} +2 -2
  226. package/tests/core/memory-engine-services.test.ts +240 -0
  227. package/tests/core/memory-ingest-service.test.ts +296 -0
  228. package/tests/core/memory-query-service.test.ts +129 -0
  229. package/tests/core/memory-runtime-service.test.ts +201 -0
  230. package/tests/core/memory-service-composition.test.ts +192 -0
  231. package/tests/core/memory-service-config.test.ts +41 -0
  232. package/tests/core/memory-service-facade.test.ts +30 -0
  233. package/tests/core/memory-service-registry.test.ts +206 -0
  234. package/tests/core/product-validation-matrix.test.ts +61 -0
  235. package/tests/core/project-registry.test.ts +78 -0
  236. package/tests/core/replay-evaluator.test.ts +181 -0
  237. package/tests/core/retrieval-analytics-service.test.ts +210 -0
  238. package/tests/core/retrieval-benchmark.test.ts +93 -0
  239. package/tests/core/retrieval-disclosure-service.test.ts +264 -0
  240. package/tests/core/retrieval-orchestrator.test.ts +403 -0
  241. package/tests/core/retrieval-quality.test.ts +31 -0
  242. package/tests/core/retrieval-services.test.ts +185 -0
  243. package/tests/{retriever-fallback-chain.test.ts → core/retriever-fallback-chain.test.ts} +3 -3
  244. package/tests/{retriever-strategy-scope.test.ts → core/retriever-strategy-scope.test.ts} +70 -3
  245. package/tests/{retriever.memu-adoption.test.ts → core/retriever.memu-adoption.test.ts} +3 -3
  246. package/tests/core/session-history-importer-filter.test.ts +78 -0
  247. package/tests/core/session-qrels.test.ts +250 -0
  248. package/tests/{sqlite-event-store-replication.test.ts → core/sqlite-event-store-replication.test.ts} +36 -1
  249. package/tests/core/summary-deriver.test.ts +66 -0
  250. package/tests/extensions/embedder-warning-suppression.test.ts +53 -0
  251. package/tests/extensions/endless-memory-extension-boundary.test.ts +17 -0
  252. package/tests/extensions/endless-memory-services.test.ts +325 -0
  253. package/tests/extensions/mcp-context-tools.test.ts +905 -0
  254. package/tests/extensions/mcp-extension-boundary.test.ts +21 -0
  255. package/tests/extensions/mcp-package-build.test.ts +22 -0
  256. package/tests/extensions/mcp-project-aware-tools.test.ts +102 -0
  257. package/tests/extensions/shared-memory-extension-boundary.test.ts +24 -0
  258. package/tests/extensions/shared-memory-services.test.ts +309 -0
  259. package/tests/extensions/vector-extension-boundary.test.ts +21 -0
  260. package/.claude/settings.local.json +0 -25
  261. package/.npm-cache/_cacache/content-v2/sha512/04/76/c098f88dfe584a2b80870bff7421b05d17d3d9ee1027f77772332a22d3f93a9a57101a2855107f6ad82077a818bba912b2bc317f2361b5ddb09ad284d9ce +0 -0
  262. package/.npm-cache/_cacache/content-v2/sha512/60/25/d2ecd39cfc7cab58351162814be77f935c6d6491c10c3745d456da7ddb2117ffd90c10e53fe3c0f1ed16b403307841543634504398b16ee4e6b6dd8e0c45 +0 -0
  263. package/.npm-cache/_cacache/index-v5/2b/9a/7f8f40206ed8a2e0a84efaa953ccaed1f5d001e14b931083f2e7a0738007 +0 -2
  264. package/.npm-cache/_cacache/index-v5/2e/d9/fcfa5c6a6abdc2a3644ab84a95936047298c465a2f47ee03db8f7fe1e946 +0 -3
  265. package/.npm-cache/_cacache/index-v5/a9/42/e519633356d12d3d2f19da66a8301016d496c8f5c3e0554124aaa62dc043 +0 -2
  266. package/.npm-cache/_logs/2026-02-26T12_04_52_729Z-debug-0.log +0 -256
  267. package/.npm-cache/_logs/2026-02-26T12_05_36_835Z-debug-0.log +0 -18
  268. package/.npm-cache/_logs/2026-02-26T12_05_45_982Z-debug-0.log +0 -32
  269. package/.npm-cache/_logs/2026-02-26T12_05_48_515Z-debug-0.log +0 -260
  270. package/.npm-cache/_logs/2026-02-26T12_05_53_567Z-debug-0.log +0 -69
  271. package/.npm-cache/_update-notifier-last-checked +0 -0
  272. package/bootstrap-kb/decisions/decisions.md +0 -244
  273. package/bootstrap-kb/glossary/glossary.md +0 -46
  274. package/bootstrap-kb/modules/.claude-plugin.md +0 -22
  275. package/bootstrap-kb/modules/agents.md.md +0 -15
  276. package/bootstrap-kb/modules/claude.md.md +0 -15
  277. package/bootstrap-kb/modules/context.md.md +0 -15
  278. package/bootstrap-kb/modules/docs.md +0 -18
  279. package/bootstrap-kb/modules/handoff.md.md +0 -15
  280. package/bootstrap-kb/modules/package-lock.json.md +0 -15
  281. package/bootstrap-kb/modules/package.json.md +0 -15
  282. package/bootstrap-kb/modules/plan.md.md +0 -15
  283. package/bootstrap-kb/modules/readme.md.md +0 -15
  284. package/bootstrap-kb/modules/scripts.md +0 -26
  285. package/bootstrap-kb/modules/spec.md.md +0 -15
  286. package/bootstrap-kb/modules/specs.md +0 -20
  287. package/bootstrap-kb/modules/src.md +0 -51
  288. package/bootstrap-kb/modules/tests.md +0 -42
  289. package/bootstrap-kb/modules/tsconfig.json.md +0 -15
  290. package/bootstrap-kb/modules/vitest.config.ts.md +0 -15
  291. package/bootstrap-kb/overview/overview.md +0 -40
  292. package/bootstrap-kb/sources/manifest.json +0 -950
  293. package/bootstrap-kb/sources/manifest.md +0 -227
  294. package/bootstrap-kb/timeline/timeline.md +0 -57
  295. package/claude-memory-layer-1.0.14.tgz +0 -0
  296. package/d.sh +0 -3
  297. package/deploy.sh +0 -3
  298. package/dist/ui/app.js +0 -2101
  299. package/memory/.claude-plugin/commands/2026-02-25.md +0 -263
  300. package/memory/_index.md +0 -419
  301. package/memory/agent_response/uncategorized/2026-02-26.md +0 -176
  302. package/memory/agent_response/uncategorized/2026-03-03.md +0 -14
  303. package/memory/agent_response/uncategorized/2026-03-04.md +0 -1421
  304. package/memory/agent_response/uncategorized/2026-03-05.md +0 -157
  305. package/memory/default/uncategorized/2026-02-25.md +0 -4839
  306. package/memory/session_summary/uncategorized/2026-02-26.md +0 -13
  307. package/memory/session_summary/uncategorized/2026-03-03.md +0 -5
  308. package/memory/session_summary/uncategorized/2026-03-04.md +0 -50
  309. package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +0 -142
  310. package/memory/specs/citations-system/2026-02-25.md +0 -1121
  311. package/memory/specs/endless-mode/2026-02-25.md +0 -1392
  312. package/memory/specs/entity-edge-model/2026-02-25.md +0 -1263
  313. package/memory/specs/evidence-aligner-v2/2026-02-25.md +0 -1028
  314. package/memory/specs/mcp-desktop-integration/2026-02-25.md +0 -1334
  315. package/memory/specs/post-tool-use-hook/2026-02-25.md +0 -1164
  316. package/memory/specs/private-tags/2026-02-25.md +0 -1057
  317. package/memory/specs/progressive-disclosure/2026-02-25.md +0 -1436
  318. package/memory/specs/task-entity-system/2026-02-25.md +0 -924
  319. package/memory/specs/vector-outbox-v2/2026-02-25.md +0 -1510
  320. package/memory/specs/web-viewer-ui/2026-02-25.md +0 -1709
  321. package/memory/tool_observation/uncategorized/2026-02-26.md +0 -209
  322. package/memory/tool_observation/uncategorized/2026-03-03.md +0 -21
  323. package/memory/tool_observation/uncategorized/2026-03-04.md +0 -1033
  324. package/memory/tool_observation/uncategorized/2026-03-05.md +0 -33
  325. package/memory/user_prompt/uncategorized/2026-02-26.md +0 -25
  326. package/memory/user_prompt/uncategorized/2026-03-04.md +0 -634
  327. package/memory/user_prompt/uncategorized/2026-03-05.md +0 -6
  328. package/specs/optional-duckdb/context.md +0 -77
  329. package/specs/optional-duckdb/plan.md +0 -142
  330. package/specs/optional-duckdb/spec.md +0 -35
  331. package/src/ui/app.js +0 -2101
@@ -11,7 +11,8 @@ import * as path from 'path';
11
11
  import * as os from 'os';
12
12
  import * as readline from 'readline';
13
13
  import { randomUUID } from 'crypto';
14
- import { MemoryService, registerSession } from './memory-service.js';
14
+ import { MemoryService } from './memory-service.js';
15
+ import { registerSession } from '../core/registry/session-registry.js';
15
16
 
16
17
  export type ProgressEvent =
17
18
  | { phase: 'scan'; message: string }
@@ -25,6 +26,8 @@ export interface ImportOptions {
25
26
  projectPath?: string;
26
27
  sessionId?: string;
27
28
  limit?: number;
29
+ /** Limit how many matching sessions are imported. Useful for freshness jobs that only need the latest active session. */
30
+ sessionLimit?: number;
28
31
  skipExisting?: boolean;
29
32
  force?: boolean;
30
33
  verbose?: boolean;
@@ -63,14 +66,48 @@ export interface ClaudeMessage {
63
66
  * Filter trivial user inputs that aren't worth storing.
64
67
  * Mirrors the shouldStorePrompt() logic from user-prompt-submit.ts.
65
68
  */
66
- function isWorthStoringPrompt(content: string): boolean {
69
+ export function isClaudeLocalCommandArtifact(content: string): boolean {
67
70
  const trimmed = content.trim();
71
+ return (
72
+ /^<local-command-(stdout|stderr)>/.test(trimmed) ||
73
+ /^<command-(name|message)>/.test(trimmed) ||
74
+ (trimmed.includes('<command-name>') && trimmed.includes('<local-command-stdout>'))
75
+ );
76
+ }
77
+
78
+ export function isWorthStoringPrompt(content: string): boolean {
79
+ const trimmed = content.trim();
80
+ if (isClaudeLocalCommandArtifact(trimmed)) return false;
68
81
  if (trimmed.startsWith('/')) return false;
69
82
  if (trimmed.length < 15) return false;
70
83
  if (!/[a-zA-Z가-힣]{2,}/.test(trimmed)) return false;
71
84
  return true;
72
85
  }
73
86
 
87
+ function getFileMtimeMs(filePath: string): number {
88
+ try {
89
+ return fs.statSync(filePath).mtimeMs;
90
+ } catch {
91
+ return 0;
92
+ }
93
+ }
94
+
95
+ function selectRecentSessionFiles(files: string[], sessionLimit?: number): string[] {
96
+ if (sessionLimit === undefined) return files;
97
+ return [...files]
98
+ .sort((a, b) => getFileMtimeMs(b) - getFileMtimeMs(a) || b.localeCompare(a))
99
+ .slice(0, sessionLimit);
100
+ }
101
+
102
+ function parseSessionLimit(value?: number): number | undefined {
103
+ if (value === undefined) return undefined;
104
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : undefined;
105
+ }
106
+
107
+ function applySessionLimit(files: string[], options: ImportOptions): string[] {
108
+ return selectRecentSessionFiles(files, parseSessionLimit(options.sessionLimit));
109
+ }
110
+
74
111
  function classifyEntry(entry: ClaudeMessage): 'user_prompt' | 'tool_result' | 'agent_text' | 'tool_use' | 'thinking' | 'skip' {
75
112
  if (entry.type !== 'user' && entry.type !== 'assistant') {
76
113
  return 'skip';
@@ -151,7 +188,8 @@ export class SessionHistoryImporter {
151
188
  allSessionFiles.push(...files);
152
189
  }
153
190
  const sessionFiles = [...new Set(allSessionFiles)];
154
- result.totalSessions = sessionFiles.length;
191
+ const selectedSessionFiles = applySessionLimit(sessionFiles, options);
192
+ result.totalSessions = selectedSessionFiles.length;
155
193
  onProgress?.({
156
194
  phase: 'scan',
157
195
  message: `Found ${sessionFiles.length} sessions in ${projectDirs.length} matched project folder(s)`
@@ -165,11 +203,11 @@ export class SessionHistoryImporter {
165
203
  console.log(`Found ${sessionFiles.length} session files across matched folders`);
166
204
  }
167
205
 
168
- // Import each session
169
- for (let i = 0; i < sessionFiles.length; i++) {
170
- const sessionFile = sessionFiles[i];
206
+ // Import each selected session
207
+ for (let i = 0; i < selectedSessionFiles.length; i++) {
208
+ const sessionFile = selectedSessionFiles[i];
171
209
  try {
172
- onProgress?.({ phase: 'session-start', sessionIndex: i, totalSessions: sessionFiles.length, filePath: sessionFile });
210
+ onProgress?.({ phase: 'session-start', sessionIndex: i, totalSessions: selectedSessionFiles.length, filePath: sessionFile });
173
211
  const sessionResult = await this.importSessionFile(sessionFile, {
174
212
  ...options,
175
213
  _sessionIndex: i,
@@ -270,7 +308,7 @@ export class SessionHistoryImporter {
270
308
  { importedFrom: filePath, originalTimestamp: lastTimestamp, turnId: currentTurnId }
271
309
  );
272
310
 
273
- if (appendResult.isDuplicate) {
311
+ if (appendResult.success && appendResult.isDuplicate) {
274
312
  result.skippedDuplicates++;
275
313
  } else {
276
314
  result.importedResponses++;
@@ -310,7 +348,7 @@ export class SessionHistoryImporter {
310
348
  { importedFrom: filePath, originalTimestamp: entry.timestamp, turnId: currentTurnId }
311
349
  );
312
350
 
313
- if (appendResult.isDuplicate) {
351
+ if (appendResult.success && appendResult.isDuplicate) {
314
352
  result.skippedDuplicates++;
315
353
  } else {
316
354
  result.importedPrompts++;
@@ -401,16 +439,22 @@ export class SessionHistoryImporter {
401
439
  console.log(`Found ${projectDirs.length} project directories, ${allSessionFiles.length} sessions`);
402
440
  }
403
441
 
404
- // Import all session files with progress tracking
405
- for (let i = 0; i < allSessionFiles.length; i++) {
406
- const sessionFile = allSessionFiles[i];
442
+ const selectedSessionFiles = applySessionLimit(allSessionFiles, options);
443
+ result.totalSessions = selectedSessionFiles.length;
444
+ onProgress?.({
445
+ phase: 'scan',
446
+ message: `Selected ${selectedSessionFiles.length} of ${allSessionFiles.length} session(s) for import`
447
+ });
448
+
449
+ // Import selected session files with progress tracking
450
+ for (let i = 0; i < selectedSessionFiles.length; i++) {
451
+ const sessionFile = selectedSessionFiles[i];
407
452
  try {
408
- onProgress?.({ phase: 'session-start', sessionIndex: i, totalSessions: allSessionFiles.length, filePath: sessionFile });
453
+ onProgress?.({ phase: 'session-start', sessionIndex: i, totalSessions: selectedSessionFiles.length, filePath: sessionFile });
409
454
  const sessionResult = await this.importSessionFile(sessionFile, {
410
455
  ...options,
411
456
  _sessionIndex: i,
412
457
  } as ImportOptions & { _sessionIndex: number });
413
- result.totalSessions++;
414
458
  result.totalMessages += sessionResult.totalMessages;
415
459
  result.importedPrompts += sessionResult.importedPrompts;
416
460
  result.importedResponses += sessionResult.importedResponses;
@@ -0,0 +1,23 @@
1
+ # Test Matrix
2
+
3
+ The test suite mirrors the thin-core architecture so regressions are easier to localize.
4
+
5
+ - `core/` — core models, stores, retrieval, ingestion, derivation, runtime services, and service facades.
6
+ - `adapters/claude/` — Claude Code hook and transcript adapter behavior.
7
+ - `extensions/` — optional vector, shared-memory, endless-memory, and MCP extension boundaries/services.
8
+ - `apps/` — CLI, server/API, dashboard/UI, and packaging-facing application behavior.
9
+
10
+ Run all tests with:
11
+
12
+ ```bash
13
+ npm test -- --run
14
+ ```
15
+
16
+ Run one architecture slice with:
17
+
18
+ ```bash
19
+ npm test -- --run tests/core
20
+ npm test -- --run tests/adapters/claude
21
+ npm test -- --run tests/extensions
22
+ npm test -- --run tests/apps
23
+ ```
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ handleSemanticDaemonRequest,
4
+ isValidSemanticDaemonRequest,
5
+ isVectorSessionFilterError,
6
+ makeSemanticDaemonErrorResponse,
7
+ parseSemanticDaemonRequest
8
+ } from '../../../src/adapters/claude/hooks/semantic-daemon.js';
9
+
10
+ describe('Claude semantic daemon adapter', () => {
11
+ it('parses JSON requests and treats malformed payloads as empty requests', () => {
12
+ expect(parseSemanticDaemonRequest('{"type":"retrieve","sessionId":"s1"}')).toEqual({
13
+ type: 'retrieve',
14
+ sessionId: 's1'
15
+ });
16
+ expect(parseSemanticDaemonRequest('{not-json')).toEqual({});
17
+ });
18
+
19
+ it('validates retrieve requests before touching MemoryService', () => {
20
+ expect(isValidSemanticDaemonRequest({
21
+ type: 'retrieve',
22
+ sessionId: 'session-1',
23
+ prompt: 'find checkout fix',
24
+ topK: 5,
25
+ minScore: 0.2
26
+ })).toBe(true);
27
+
28
+ expect(isValidSemanticDaemonRequest({
29
+ type: 'retrieve',
30
+ sessionId: 'session-1',
31
+ prompt: 'find checkout fix',
32
+ topK: Number.NaN,
33
+ minScore: 0.2
34
+ })).toBe(false);
35
+ });
36
+
37
+ it('returns a deterministic invalid request response without initializing retrieval', async () => {
38
+ await expect(handleSemanticDaemonRequest('{"type":"retrieve"}')).resolves.toEqual({
39
+ ok: false,
40
+ error: 'invalid request'
41
+ });
42
+ });
43
+
44
+ it('detects LanceDB sessionId field-case filter failures', () => {
45
+ expect(isVectorSessionFilterError(new Error('No field named sessionId in schema'))).toBe(true);
46
+ expect(isVectorSessionFilterError(new Error('connection refused'))).toBe(false);
47
+ expect(isVectorSessionFilterError('no field named sessionId')).toBe(false);
48
+ });
49
+
50
+ it('formats daemon errors without leaking non-Error values', () => {
51
+ expect(makeSemanticDaemonErrorResponse(new Error('boom'))).toEqual({ ok: false, error: 'boom' });
52
+ expect(makeSemanticDaemonErrorResponse('boom')).toEqual({ ok: false, error: 'unknown daemon error' });
53
+ });
54
+ });
@@ -0,0 +1,98 @@
1
+ import { mkdtemp, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { readTranscriptTailEntries } from '../../../src/adapters/claude/transcript/transcript-reader.js';
6
+ import {
7
+ extractAssistantTextMessages,
8
+ extractAssistantMessages,
9
+ generateSessionSummary
10
+ } from '../../../src/adapters/claude/transcript/turn-reconstructor.js';
11
+
12
+ describe('Claude transcript reader', () => {
13
+ it('reads JSONL transcript tail entries and skips malformed partial lines', async () => {
14
+ const dir = await mkdtemp(join(tmpdir(), 'cml-transcript-'));
15
+ const transcriptPath = join(dir, 'transcript.jsonl');
16
+ await writeFile(
17
+ transcriptPath,
18
+ [
19
+ '{"type":"user","message":{"content":"hello"}}',
20
+ 'not-json',
21
+ 'null',
22
+ '42',
23
+ '{"type":"assistant","message":{"content":[{"type":"text","text":"answer"}]}}'
24
+ ].join('\n'),
25
+ 'utf8'
26
+ );
27
+
28
+ const entries = await readTranscriptTailEntries(transcriptPath);
29
+
30
+ expect(entries.map((entry) => entry.type)).toEqual(['user', 'assistant']);
31
+ });
32
+
33
+ it('skips a partial first line when tail reading starts mid-record', async () => {
34
+ const dir = await mkdtemp(join(tmpdir(), 'cml-transcript-'));
35
+ const transcriptPath = join(dir, 'transcript.jsonl');
36
+ const firstLine = `{"type":"user","message":{"content":"${'x'.repeat(500)}"}}`;
37
+ const secondLine = '{"type":"user","message":{"content":"recent question"}}';
38
+ const thirdLine = '{"type":"assistant","message":{"content":[{"type":"text","text":"recent answer"}]}}';
39
+ await writeFile(transcriptPath, [firstLine, secondLine, thirdLine].join('\n'), 'utf8');
40
+
41
+ const maxBytes = Buffer.byteLength(`${secondLine}\n${thirdLine}`) + 10;
42
+ const entries = await readTranscriptTailEntries(transcriptPath, { maxBytes });
43
+
44
+ expect(entries.map((entry) => entry.type)).toEqual(['user', 'assistant']);
45
+ expect(entries[0]?.message?.content).toBe('recent question');
46
+ });
47
+
48
+ it('returns an empty entry list for missing transcript files', async () => {
49
+ await expect(readTranscriptTailEntries('/tmp/non-existent-claude-transcript.jsonl')).resolves.toEqual([]);
50
+ });
51
+ });
52
+
53
+ describe('Claude turn reconstructor', () => {
54
+ it('extracts assistant text blocks and joins multi-part text content', async () => {
55
+ const messages = extractAssistantTextMessages([
56
+ { type: 'user', message: { content: 'ignored' } },
57
+ {
58
+ type: 'assistant',
59
+ message: {
60
+ content: [
61
+ { type: 'text', text: 'first' },
62
+ { type: 'tool_use', id: 'tool-1' },
63
+ { type: 'text', text: 'second' }
64
+ ]
65
+ }
66
+ },
67
+ { type: 'assistant', message: { content: [{ type: 'tool_use', id: 'tool-2' }] } }
68
+ ]);
69
+
70
+ expect(messages).toEqual(['first\nsecond']);
71
+ });
72
+
73
+ it('reads assistant text messages from a transcript file', async () => {
74
+ const dir = await mkdtemp(join(tmpdir(), 'cml-transcript-'));
75
+ const transcriptPath = join(dir, 'transcript.jsonl');
76
+ await writeFile(
77
+ transcriptPath,
78
+ '{"type":"assistant","message":{"content":[{"type":"text","text":"final answer"}]}}\n',
79
+ 'utf8'
80
+ );
81
+
82
+ await expect(extractAssistantMessages(transcriptPath)).resolves.toEqual(['final answer']);
83
+ });
84
+
85
+ it('generates deterministic summaries from session events', () => {
86
+ const summary = generateSessionSummary([
87
+ { eventType: 'user_prompt', content: 'How should we refactor transcript parsing?' },
88
+ { eventType: 'agent_response', content: 'Move it behind adapter modules.' },
89
+ { eventType: 'tool_observation', content: 'ignored' }
90
+ ]);
91
+
92
+ expect(summary).toBe(
93
+ 'Session with 1 user prompts and 1 responses.\n' +
94
+ 'Topics discussed:\n' +
95
+ '- How should we refactor transcript parsing?'
96
+ );
97
+ });
98
+ });
@@ -0,0 +1,99 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { MemoryQueryService } from '../../src/core/engine/memory-query-service.js';
4
+ import type { MemoryEvent } from '../../src/core/types.js';
5
+ import {
6
+ filterHookInjectableMemories,
7
+ getHookInjectionPolicy,
8
+ summarizeHookInjectionConfidence,
9
+ type HookMemoryCandidate
10
+ } from '../../src/adapters/claude/hooks/prompt-injection-policy.js';
11
+
12
+ function event(id: string, content = 'low confidence keyword result'): MemoryEvent {
13
+ return {
14
+ id,
15
+ eventType: 'user_prompt',
16
+ sessionId: 'session-1',
17
+ timestamp: new Date('2026-05-02T00:00:00.000Z'),
18
+ content,
19
+ canonicalKey: `test/${id}`,
20
+ dedupeKey: `session-1:${id}`,
21
+ metadata: {}
22
+ };
23
+ }
24
+
25
+ describe('Claude hook prompt injection policy', () => {
26
+ it('filters low-confidence hook candidates before prompt injection', () => {
27
+ const candidates: HookMemoryCandidate[] = [
28
+ { id: 'low-semantic', type: 'agent_response', content: 'maybe related', score: 0.58, source: 'semantic' },
29
+ { id: 'low-keyword', type: 'user_prompt', content: 'weak keyword rescue', score: 0.49, source: 'keyword' },
30
+ { id: 'high-keyword', type: 'user_prompt', content: 'exact high-confidence keyword', score: 0.84, source: 'keyword' },
31
+ { id: 'high-semantic', type: 'session_summary', content: 'high semantic match', score: 0.76, source: 'semantic' }
32
+ ];
33
+
34
+ expect(filterHookInjectableMemories(candidates, getHookInjectionPolicy())).toEqual([
35
+ candidates[2],
36
+ candidates[3]
37
+ ]);
38
+ expect(summarizeHookInjectionConfidence(candidates)).toBe('high');
39
+ expect(summarizeHookInjectionConfidence([])).toBe('none');
40
+ });
41
+
42
+ it('limits injected hook memories by highest confidence rather than first passing candidate', () => {
43
+ const candidates: HookMemoryCandidate[] = [
44
+ { id: 'medium-first', type: 'session_summary', content: 'passes but weaker', score: 0.67, source: 'semantic' },
45
+ { id: 'best-later', type: 'user_prompt', content: 'best exact keyword', score: 0.93, source: 'keyword' },
46
+ { id: 'second-best-later', type: 'agent_response', content: 'strong semantic match', score: 0.88, source: 'semantic' }
47
+ ];
48
+
49
+ expect(filterHookInjectableMemories(candidates, { ...getHookInjectionPolicy(), maxMemories: 2 }))
50
+ .toEqual([candidates[1], candidates[2]]);
51
+ });
52
+
53
+ it('uses bounded hook policy thresholds so unsafe env overrides cannot inject weak memories', () => {
54
+ const policy = getHookInjectionPolicy({
55
+ CLAUDE_MEMORY_HOOK_INJECTION_MIN_SCORE: '-1',
56
+ CLAUDE_MEMORY_HOOK_SEMANTIC_MIN_SCORE: '2',
57
+ CLAUDE_MEMORY_HOOK_KEYWORD_MIN_SCORE: 'not-a-number',
58
+ CLAUDE_MEMORY_HOOK_FALLBACK_KEYWORD_MIN_SCORE: '-0.2',
59
+ CLAUDE_MEMORY_HOOK_MAX_INJECTED: '2'
60
+ } as NodeJS.ProcessEnv);
61
+
62
+ expect(policy).toEqual({
63
+ minScore: 0.65,
64
+ semanticMinScore: 0.65,
65
+ keywordMinScore: 0.7,
66
+ fallbackKeywordMinScore: 0.8,
67
+ maxMemories: 2
68
+ });
69
+
70
+ expect(filterHookInjectableMemories([
71
+ { id: 'weak-unknown', type: 'user_prompt', content: 'weak unknown source', score: 0.1, source: 'unknown' },
72
+ { id: 'fallback-too-weak', type: 'user_prompt', content: 'weak fallback', score: 0.79, source: 'keyword', fallback: true }
73
+ ], policy)).toEqual([]);
74
+ });
75
+
76
+ it('keeps regular CLI/query search behavior independent from hook injection policy', async () => {
77
+ const memory = event('regular-low');
78
+ const service = new MemoryQueryService(
79
+ async () => {},
80
+ {
81
+ keywordSearch: async () => [{ event: memory, rank: -0.5 }],
82
+ getSessionEvents: async () => [memory],
83
+ getRecentEvents: async () => [memory]
84
+ }
85
+ );
86
+
87
+ const regularResults = await service.keywordSearch('weak keyword rescue', { topK: 1, minScore: 0.2 });
88
+ expect(regularResults).toEqual([{ event: memory, score: 1 }]);
89
+
90
+ const hookCandidates: HookMemoryCandidate[] = regularResults.map((result) => ({
91
+ id: result.event.id,
92
+ type: result.event.eventType,
93
+ content: result.event.content,
94
+ score: 0.5,
95
+ source: 'keyword'
96
+ }));
97
+ expect(filterHookInjectableMemories(hookCandidates, getHookInjectionPolicy())).toEqual([]);
98
+ });
99
+ });
@@ -0,0 +1,48 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ const apiModules = [
5
+ 'chat',
6
+ 'citations',
7
+ 'events',
8
+ 'health',
9
+ 'projects',
10
+ 'search',
11
+ 'sessions',
12
+ 'stats',
13
+ 'turns',
14
+ 'utils'
15
+ ];
16
+
17
+ describe('app-layer entrypoint boundaries', () => {
18
+ it('keeps CLI implementation under src/apps/cli while preserving src/cli compatibility', () => {
19
+ const packageJson = JSON.parse(readFileSync('package.json', 'utf8')) as {
20
+ scripts?: Record<string, string>;
21
+ };
22
+ const buildScript = readFileSync('scripts/build.ts', 'utf8');
23
+ const cliCompatSource = readFileSync('src/cli/index.ts', 'utf8');
24
+ const cliDisclosureCompatSource = readFileSync('src/cli/retrieval-disclosure-output.ts', 'utf8');
25
+
26
+ expect(packageJson.scripts?.dev).toBe('tsx src/apps/cli/index.ts');
27
+ expect(buildScript).toContain("entryPoints: ['src/apps/cli/index.ts']");
28
+ expect(cliCompatSource).toContain("../apps/cli/index.js");
29
+ expect(cliDisclosureCompatSource).toContain("../apps/cli/retrieval-disclosure-output.js");
30
+ });
31
+
32
+ it('keeps server implementation under src/apps/server while preserving src/server compatibility', () => {
33
+ const buildScript = readFileSync('scripts/build.ts', 'utf8');
34
+ const serverCompatSource = readFileSync('src/server/index.ts', 'utf8');
35
+ const apiIndexCompatSource = readFileSync('src/server/api/index.ts', 'utf8');
36
+
37
+ expect(buildScript).toContain("entryPoints: ['src/apps/server/index.ts']");
38
+ expect(buildScript).toContain("entryPoints: ['src/apps/server/api/index.ts']");
39
+ expect(serverCompatSource).toContain("../apps/server/index.js");
40
+ expect(apiIndexCompatSource).toContain("../../apps/server/api/index.js");
41
+
42
+ for (const moduleName of apiModules) {
43
+ const compatPath = `src/server/api/${moduleName}.ts`;
44
+ expect(existsSync(compatPath), `${compatPath} should exist`).toBe(true);
45
+ expect(readFileSync(compatPath, 'utf8')).toContain(`../../apps/server/api/${moduleName}.js`);
46
+ }
47
+ });
48
+ });
@@ -0,0 +1,107 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ buildHookCommand,
5
+ getHooksConfig,
6
+ mergePluginHooksIntoSettings,
7
+ removePluginHooksFromSettings,
8
+ type ClaudeSettingsWithHooks
9
+ } from '../../src/apps/cli/claude-settings-hooks.js';
10
+
11
+ describe('Claude Code hook settings helpers', () => {
12
+ it('quotes hook paths so plugin installs work when the path contains spaces', () => {
13
+ expect(buildHookCommand('/tmp/project with spaces/dist', 'user-prompt-submit.js'))
14
+ .toBe("node '/tmp/project with spaces/dist/hooks/user-prompt-submit.js'");
15
+
16
+ const complexCommand = buildHookCommand("/tmp/project with $dollars 'quotes' and `ticks`/dist", 'stop.js');
17
+ expect(complexCommand).toContain("'\\''quotes'\\''");
18
+ expect(complexCommand).toContain('$dollars');
19
+ expect(complexCommand).toContain('`ticks`');
20
+
21
+ expect(getHooksConfig('/tmp/project with spaces/dist').UserPromptSubmit?.[0].hooks[0].command)
22
+ .toBe("node '/tmp/project with spaces/dist/hooks/user-prompt-submit.js'");
23
+ });
24
+
25
+ it('merges plugin hooks without replacing unrelated hooks in the same categories', () => {
26
+ const settings: ClaudeSettingsWithHooks = {
27
+ theme: 'dark',
28
+ hooks: {
29
+ UserPromptSubmit: [
30
+ {
31
+ matcher: 'existing',
32
+ hooks: [{ type: 'command', command: 'node /other/plugin.js' }]
33
+ }
34
+ ],
35
+ Stop: [
36
+ {
37
+ matcher: 'old-plugin',
38
+ hooks: [{ type: 'command', command: 'node /old/claude-memory-layer/dist/hooks/stop.js' }]
39
+ }
40
+ ]
41
+ }
42
+ };
43
+
44
+ const merged = mergePluginHooksIntoSettings(settings, '/new/plugin dist');
45
+
46
+ expect(merged.theme).toBe('dark');
47
+ expect(merged.hooks?.UserPromptSubmit).toEqual([
48
+ {
49
+ matcher: 'existing',
50
+ hooks: [{ type: 'command', command: 'node /other/plugin.js' }]
51
+ },
52
+ {
53
+ matcher: '',
54
+ hooks: [{ type: 'command', command: "node '/new/plugin dist/hooks/user-prompt-submit.js'" }]
55
+ }
56
+ ]);
57
+ expect(merged.hooks?.Stop).toEqual([
58
+ {
59
+ matcher: '',
60
+ hooks: [{ type: 'command', command: "node '/new/plugin dist/hooks/stop.js'" }]
61
+ }
62
+ ]);
63
+ });
64
+
65
+ it('uninstall removes only claude-memory-layer hook commands and leaves other hooks intact', () => {
66
+ const settings: ClaudeSettingsWithHooks = {
67
+ hooks: {
68
+ SessionStart: [
69
+ {
70
+ matcher: 'keep-and-remove',
71
+ hooks: [
72
+ { type: 'command', command: 'node /opt/claude-memory-layer/dist/hooks/session-start.js' },
73
+ { type: 'command', command: 'node /other/session-start-helper.js' }
74
+ ]
75
+ }
76
+ ],
77
+ PostToolUse: [
78
+ {
79
+ matcher: 'keep',
80
+ hooks: [
81
+ { type: 'command', command: 'node /other/post-tool-use-helper.js' },
82
+ { type: 'command', command: 'node /other-plugin/hooks/post-tool-use.js' }
83
+ ]
84
+ }
85
+ ]
86
+ }
87
+ };
88
+
89
+ const removed = removePluginHooksFromSettings(settings, '/opt/claude-memory-layer/dist');
90
+
91
+ expect(removed.hooks?.SessionStart).toEqual([
92
+ {
93
+ matcher: 'keep-and-remove',
94
+ hooks: [{ type: 'command', command: 'node /other/session-start-helper.js' }]
95
+ }
96
+ ]);
97
+ expect(removed.hooks?.PostToolUse).toEqual([
98
+ {
99
+ matcher: 'keep',
100
+ hooks: [
101
+ { type: 'command', command: 'node /other/post-tool-use-helper.js' },
102
+ { type: 'command', command: 'node /other-plugin/hooks/post-tool-use.js' }
103
+ ]
104
+ }
105
+ ]);
106
+ });
107
+ });