nlm-memory 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (472) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.github/workflows/ci.yml +30 -0
  3. package/LICENSE +151 -0
  4. package/README.md +119 -0
  5. package/dist/cli/classify-parity.d.ts +48 -0
  6. package/dist/cli/classify-parity.js +182 -0
  7. package/dist/cli/classify-parity.js.map +1 -0
  8. package/dist/cli/launchctl-helpers.d.ts +26 -0
  9. package/dist/cli/launchctl-helpers.js +42 -0
  10. package/dist/cli/launchctl-helpers.js.map +1 -0
  11. package/dist/cli/nlm.d.ts +25 -0
  12. package/dist/cli/nlm.js +832 -0
  13. package/dist/cli/nlm.js.map +1 -0
  14. package/dist/core/actions/actions-log.d.ts +40 -0
  15. package/dist/core/actions/actions-log.js +72 -0
  16. package/dist/core/actions/actions-log.js.map +1 -0
  17. package/dist/core/actions/overlay.d.ts +30 -0
  18. package/dist/core/actions/overlay.js +101 -0
  19. package/dist/core/actions/overlay.js.map +1 -0
  20. package/dist/core/adapters/aider.d.ts +33 -0
  21. package/dist/core/adapters/aider.js +167 -0
  22. package/dist/core/adapters/aider.js.map +1 -0
  23. package/dist/core/adapters/claude-code.d.ts +32 -0
  24. package/dist/core/adapters/claude-code.js +270 -0
  25. package/dist/core/adapters/claude-code.js.map +1 -0
  26. package/dist/core/adapters/common.d.ts +20 -0
  27. package/dist/core/adapters/common.js +60 -0
  28. package/dist/core/adapters/common.js.map +1 -0
  29. package/dist/core/adapters/from-source.d.ts +11 -0
  30. package/dist/core/adapters/from-source.js +55 -0
  31. package/dist/core/adapters/from-source.js.map +1 -0
  32. package/dist/core/adapters/hermes-agent.d.ts +34 -0
  33. package/dist/core/adapters/hermes-agent.js +192 -0
  34. package/dist/core/adapters/hermes-agent.js.map +1 -0
  35. package/dist/core/adapters/hermes.d.ts +31 -0
  36. package/dist/core/adapters/hermes.js +247 -0
  37. package/dist/core/adapters/hermes.js.map +1 -0
  38. package/dist/core/adapters/jsonl-generic.d.ts +56 -0
  39. package/dist/core/adapters/jsonl-generic.js +185 -0
  40. package/dist/core/adapters/jsonl-generic.js.map +1 -0
  41. package/dist/core/adapters/opencode.d.ts +36 -0
  42. package/dist/core/adapters/opencode.js +213 -0
  43. package/dist/core/adapters/opencode.js.map +1 -0
  44. package/dist/core/adapters/pi.d.ts +32 -0
  45. package/dist/core/adapters/pi.js +233 -0
  46. package/dist/core/adapters/pi.js.map +1 -0
  47. package/dist/core/classifier/prompt.d.ts +60 -0
  48. package/dist/core/classifier/prompt.js +178 -0
  49. package/dist/core/classifier/prompt.js.map +1 -0
  50. package/dist/core/dataset/build-dataset.d.ts +87 -0
  51. package/dist/core/dataset/build-dataset.js +335 -0
  52. package/dist/core/dataset/build-dataset.js.map +1 -0
  53. package/dist/core/embedding/chunk-body.d.ts +30 -0
  54. package/dist/core/embedding/chunk-body.js +60 -0
  55. package/dist/core/embedding/chunk-body.js.map +1 -0
  56. package/dist/core/embedding/embed-backfill.d.ts +36 -0
  57. package/dist/core/embedding/embed-backfill.js +168 -0
  58. package/dist/core/embedding/embed-backfill.js.map +1 -0
  59. package/dist/core/embedding/embed-normalize.d.ts +28 -0
  60. package/dist/core/embedding/embed-normalize.js +98 -0
  61. package/dist/core/embedding/embed-normalize.js.map +1 -0
  62. package/dist/core/facts/backfill-facts.d.ts +58 -0
  63. package/dist/core/facts/backfill-facts.js +169 -0
  64. package/dist/core/facts/backfill-facts.js.map +1 -0
  65. package/dist/core/facts/extract-facts.d.ts +20 -0
  66. package/dist/core/facts/extract-facts.js +37 -0
  67. package/dist/core/facts/extract-facts.js.map +1 -0
  68. package/dist/core/hook/citation-detect.d.ts +32 -0
  69. package/dist/core/hook/citation-detect.js +105 -0
  70. package/dist/core/hook/citation-detect.js.map +1 -0
  71. package/dist/core/hook/cite-memo.d.ts +20 -0
  72. package/dist/core/hook/cite-memo.js +68 -0
  73. package/dist/core/hook/cite-memo.js.map +1 -0
  74. package/dist/core/hook/claude-settings.d.ts +34 -0
  75. package/dist/core/hook/claude-settings.js +117 -0
  76. package/dist/core/hook/claude-settings.js.map +1 -0
  77. package/dist/core/hook/gate.d.ts +11 -0
  78. package/dist/core/hook/gate.js +19 -0
  79. package/dist/core/hook/gate.js.map +1 -0
  80. package/dist/core/hook/hook-log.d.ts +25 -0
  81. package/dist/core/hook/hook-log.js +28 -0
  82. package/dist/core/hook/hook-log.js.map +1 -0
  83. package/dist/core/hook/memo-sweep.d.ts +55 -0
  84. package/dist/core/hook/memo-sweep.js +134 -0
  85. package/dist/core/hook/memo-sweep.js.map +1 -0
  86. package/dist/core/hook/memo.d.ts +20 -0
  87. package/dist/core/hook/memo.js +66 -0
  88. package/dist/core/hook/memo.js.map +1 -0
  89. package/dist/core/hook/pointer-block.d.ts +14 -0
  90. package/dist/core/hook/pointer-block.js +19 -0
  91. package/dist/core/hook/pointer-block.js.map +1 -0
  92. package/dist/core/hook/select.d.ts +21 -0
  93. package/dist/core/hook/select.js +15 -0
  94. package/dist/core/hook/select.js.map +1 -0
  95. package/dist/core/hook/transcript.d.ts +31 -0
  96. package/dist/core/hook/transcript.js +103 -0
  97. package/dist/core/hook/transcript.js.map +1 -0
  98. package/dist/core/ingest/ingest-session.d.ts +40 -0
  99. package/dist/core/ingest/ingest-session.js +71 -0
  100. package/dist/core/ingest/ingest-session.js.map +1 -0
  101. package/dist/core/providers/provider-models.d.ts +24 -0
  102. package/dist/core/providers/provider-models.js +72 -0
  103. package/dist/core/providers/provider-models.js.map +1 -0
  104. package/dist/core/providers/provider-registry.d.ts +62 -0
  105. package/dist/core/providers/provider-registry.js +143 -0
  106. package/dist/core/providers/provider-registry.js.map +1 -0
  107. package/dist/core/recall/citation-log.d.ts +28 -0
  108. package/dist/core/recall/citation-log.js +90 -0
  109. package/dist/core/recall/citation-log.js.map +1 -0
  110. package/dist/core/recall/filter.d.ts +11 -0
  111. package/dist/core/recall/filter.js +20 -0
  112. package/dist/core/recall/filter.js.map +1 -0
  113. package/dist/core/recall/index.d.ts +6 -0
  114. package/dist/core/recall/index.js +5 -0
  115. package/dist/core/recall/index.js.map +1 -0
  116. package/dist/core/recall/match-fields.d.ts +10 -0
  117. package/dist/core/recall/match-fields.js +37 -0
  118. package/dist/core/recall/match-fields.js.map +1 -0
  119. package/dist/core/recall/query-log.d.ts +36 -0
  120. package/dist/core/recall/query-log.js +112 -0
  121. package/dist/core/recall/query-log.js.map +1 -0
  122. package/dist/core/recall/query-shape.d.ts +22 -0
  123. package/dist/core/recall/query-shape.js +64 -0
  124. package/dist/core/recall/query-shape.js.map +1 -0
  125. package/dist/core/recall/recall-service.d.ts +19 -0
  126. package/dist/core/recall/recall-service.js +252 -0
  127. package/dist/core/recall/recall-service.js.map +1 -0
  128. package/dist/core/recall/recent-log.d.ts +16 -0
  129. package/dist/core/recall/recent-log.js +46 -0
  130. package/dist/core/recall/recent-log.js.map +1 -0
  131. package/dist/core/recall/tokenize.d.ts +7 -0
  132. package/dist/core/recall/tokenize.js +18 -0
  133. package/dist/core/recall/tokenize.js.map +1 -0
  134. package/dist/core/recall/useful-scan.d.ts +52 -0
  135. package/dist/core/recall/useful-scan.js +300 -0
  136. package/dist/core/recall/useful-scan.js.map +1 -0
  137. package/dist/core/recall-facts/fact-query-log.d.ts +42 -0
  138. package/dist/core/recall-facts/fact-query-log.js +115 -0
  139. package/dist/core/recall-facts/fact-query-log.js.map +1 -0
  140. package/dist/core/recall-facts/fact-recall-service.d.ts +34 -0
  141. package/dist/core/recall-facts/fact-recall-service.js +246 -0
  142. package/dist/core/recall-facts/fact-recall-service.js.map +1 -0
  143. package/dist/core/scheduler/scan-once.d.ts +32 -0
  144. package/dist/core/scheduler/scan-once.js +100 -0
  145. package/dist/core/scheduler/scan-once.js.map +1 -0
  146. package/dist/core/scheduler/scheduler.d.ts +59 -0
  147. package/dist/core/scheduler/scheduler.js +158 -0
  148. package/dist/core/scheduler/scheduler.js.map +1 -0
  149. package/dist/core/sources/source-registry.d.ts +68 -0
  150. package/dist/core/sources/source-registry.js +208 -0
  151. package/dist/core/sources/source-registry.js.map +1 -0
  152. package/dist/core/storage/db-restore.d.ts +53 -0
  153. package/dist/core/storage/db-restore.js +113 -0
  154. package/dist/core/storage/db-restore.js.map +1 -0
  155. package/dist/core/storage/live-status.d.ts +15 -0
  156. package/dist/core/storage/live-status.js +43 -0
  157. package/dist/core/storage/live-status.js.map +1 -0
  158. package/dist/core/storage/migrate.d.ts +14 -0
  159. package/dist/core/storage/migrate.js +52 -0
  160. package/dist/core/storage/migrate.js.map +1 -0
  161. package/dist/core/storage/sqlite-fact-store.d.ts +50 -0
  162. package/dist/core/storage/sqlite-fact-store.js +256 -0
  163. package/dist/core/storage/sqlite-fact-store.js.map +1 -0
  164. package/dist/core/storage/sqlite-session-store.d.ts +152 -0
  165. package/dist/core/storage/sqlite-session-store.js +587 -0
  166. package/dist/core/storage/sqlite-session-store.js.map +1 -0
  167. package/dist/hook/pre-compact-hook.d.ts +26 -0
  168. package/dist/hook/pre-compact-hook.js +94 -0
  169. package/dist/hook/pre-compact-hook.js.map +1 -0
  170. package/dist/hook/prompt-recall-hook.d.ts +23 -0
  171. package/dist/hook/prompt-recall-hook.js +141 -0
  172. package/dist/hook/prompt-recall-hook.js.map +1 -0
  173. package/dist/hook/session-end-hook.d.ts +18 -0
  174. package/dist/hook/session-end-hook.js +67 -0
  175. package/dist/hook/session-end-hook.js.map +1 -0
  176. package/dist/hook/session-start-hook.d.ts +25 -0
  177. package/dist/hook/session-start-hook.js +129 -0
  178. package/dist/hook/session-start-hook.js.map +1 -0
  179. package/dist/hook/stop-hook.d.ts +38 -0
  180. package/dist/hook/stop-hook.js +171 -0
  181. package/dist/hook/stop-hook.js.map +1 -0
  182. package/dist/hook/subagent-start-hook.d.ts +30 -0
  183. package/dist/hook/subagent-start-hook.js +108 -0
  184. package/dist/hook/subagent-start-hook.js.map +1 -0
  185. package/dist/http/app.d.ts +65 -0
  186. package/dist/http/app.js +1009 -0
  187. package/dist/http/app.js.map +1 -0
  188. package/dist/install/claude-code.d.ts +57 -0
  189. package/dist/install/claude-code.js +76 -0
  190. package/dist/install/claude-code.js.map +1 -0
  191. package/dist/install/codex.d.ts +82 -0
  192. package/dist/install/codex.js +277 -0
  193. package/dist/install/codex.js.map +1 -0
  194. package/dist/install/hermes-agent.d.ts +35 -0
  195. package/dist/install/hermes-agent.js +48 -0
  196. package/dist/install/hermes-agent.js.map +1 -0
  197. package/dist/install/hermes.d.ts +29 -0
  198. package/dist/install/hermes.js +52 -0
  199. package/dist/install/hermes.js.map +1 -0
  200. package/dist/install/ollama.d.ts +54 -0
  201. package/dist/install/ollama.js +178 -0
  202. package/dist/install/ollama.js.map +1 -0
  203. package/dist/install/setup.d.ts +37 -0
  204. package/dist/install/setup.js +339 -0
  205. package/dist/install/setup.js.map +1 -0
  206. package/dist/llm/classifier-box.d.ts +29 -0
  207. package/dist/llm/classifier-box.js +43 -0
  208. package/dist/llm/classifier-box.js.map +1 -0
  209. package/dist/llm/deepseek-client.d.ts +40 -0
  210. package/dist/llm/deepseek-client.js +114 -0
  211. package/dist/llm/deepseek-client.js.map +1 -0
  212. package/dist/llm/env-autoload.d.ts +8 -0
  213. package/dist/llm/env-autoload.js +54 -0
  214. package/dist/llm/env-autoload.js.map +1 -0
  215. package/dist/llm/ollama-client.d.ts +47 -0
  216. package/dist/llm/ollama-client.js +156 -0
  217. package/dist/llm/ollama-client.js.map +1 -0
  218. package/dist/mcp/server.d.ts +64 -0
  219. package/dist/mcp/server.js +430 -0
  220. package/dist/mcp/server.js.map +1 -0
  221. package/dist/ports/fact-store.d.ts +82 -0
  222. package/dist/ports/fact-store.js +16 -0
  223. package/dist/ports/fact-store.js.map +1 -0
  224. package/dist/ports/llm-client.d.ts +42 -0
  225. package/dist/ports/llm-client.js +14 -0
  226. package/dist/ports/llm-client.js.map +1 -0
  227. package/dist/ports/logger.d.ts +13 -0
  228. package/dist/ports/logger.js +8 -0
  229. package/dist/ports/logger.js.map +1 -0
  230. package/dist/ports/session-store.d.ts +29 -0
  231. package/dist/ports/session-store.js +9 -0
  232. package/dist/ports/session-store.js.map +1 -0
  233. package/dist/ports/transcript-adapter.d.ts +48 -0
  234. package/dist/ports/transcript-adapter.js +15 -0
  235. package/dist/ports/transcript-adapter.js.map +1 -0
  236. package/dist/shared/types.d.ts +129 -0
  237. package/dist/shared/types.js +6 -0
  238. package/dist/shared/types.js.map +1 -0
  239. package/dist/ui/assets/index-BA6IpU8g.css +1 -0
  240. package/dist/ui/assets/index-B_qIVV0k.js +69 -0
  241. package/dist/ui/index.html +13 -0
  242. package/docs/methodology/re-derivation-rate.md +112 -0
  243. package/docs/methodology/useful-hit-rate.md +79 -0
  244. package/docs/plans/2026-05-20-fts5-lexical-recall.md +1088 -0
  245. package/docs/plans/2026-05-20-recall-daemon-wedge-fix.md +662 -0
  246. package/docs/plans/2026-05-20-recall-hook-design.md +131 -0
  247. package/docs/plans/2026-05-20-recall-hook-implementation.md +1222 -0
  248. package/docs/plans/desktop-product.md +69 -0
  249. package/docs/plans/factstore-design.md +236 -0
  250. package/logs/CHANGELOG/CHANGELOG-2026.md +1389 -0
  251. package/logs/CHANGELOG/CHANGELOG.md +320 -0
  252. package/migrations/000_initial_schema.sql +174 -0
  253. package/migrations/001_entity_type_rename.sql +17 -0
  254. package/migrations/002_adapter_state_extend.sql +12 -0
  255. package/migrations/003_session_embeddings.sql +11 -0
  256. package/migrations/004_facts.sql +46 -0
  257. package/migrations/005_sources.sql +31 -0
  258. package/migrations/006_providers.sql +33 -0
  259. package/migrations/007_source_tokens.sql +17 -0
  260. package/migrations/008_fts_rebuild.sql +9 -0
  261. package/migrations/009_session_embedding_chunks.sql +46 -0
  262. package/migrations/010_sources_opencode.sql +30 -0
  263. package/migrations/011_sources_hermes_agent.sql +30 -0
  264. package/migrations/012_sources_aider.sql +30 -0
  265. package/migrations/013_adapter_state_failure_count.sql +12 -0
  266. package/package.json +56 -0
  267. package/plugin/.codex-plugin/plugin.json +22 -0
  268. package/plugin/.mcp.json +8 -0
  269. package/plugin/README.md +51 -0
  270. package/plugin/hooks/hooks.json +25 -0
  271. package/plugin/scripts/prompt-recall-hook.mjs +202 -0
  272. package/plugin/scripts/stop-hook.mjs +306 -0
  273. package/plugin-hermes-agent/README.md +49 -0
  274. package/plugin-hermes-agent/__init__.py +75 -0
  275. package/plugin-hermes-agent/plugin.yaml +15 -0
  276. package/scripts/backfill-citations.mjs +0 -0
  277. package/scripts/build-codex-plugin.mjs +61 -0
  278. package/scripts/deepseek-probe.mjs +67 -0
  279. package/scripts/extract-triples.mjs +207 -0
  280. package/scripts/longmemeval/embedding-cache.ts +77 -0
  281. package/scripts/longmemeval/fetch-dataset.sh +25 -0
  282. package/scripts/longmemeval/run-harness.ts +315 -0
  283. package/scripts/longmemeval/scorer.ts +99 -0
  284. package/scripts/longmemeval/tsconfig.json +9 -0
  285. package/scripts/longmemeval/types.ts +35 -0
  286. package/scripts/nlm-daily-digest.py +239 -0
  287. package/scripts/nlm-daily-digest.sh +28 -0
  288. package/src/cli/classify-parity.ts +257 -0
  289. package/src/cli/launchctl-helpers.ts +49 -0
  290. package/src/cli/nlm.ts +885 -0
  291. package/src/core/actions/actions-log.ts +118 -0
  292. package/src/core/actions/overlay.ts +117 -0
  293. package/src/core/adapters/aider.ts +205 -0
  294. package/src/core/adapters/claude-code.ts +293 -0
  295. package/src/core/adapters/common.ts +54 -0
  296. package/src/core/adapters/from-source.ts +57 -0
  297. package/src/core/adapters/hermes-agent.ts +240 -0
  298. package/src/core/adapters/hermes.ts +277 -0
  299. package/src/core/adapters/jsonl-generic.ts +208 -0
  300. package/src/core/adapters/opencode.ts +281 -0
  301. package/src/core/adapters/pi.ts +264 -0
  302. package/src/core/classifier/prompt.ts +200 -0
  303. package/src/core/dataset/build-dataset.ts +463 -0
  304. package/src/core/embedding/chunk-body.ts +76 -0
  305. package/src/core/embedding/embed-backfill.ts +210 -0
  306. package/src/core/embedding/embed-normalize.ts +135 -0
  307. package/src/core/facts/backfill-facts.ts +254 -0
  308. package/src/core/facts/extract-facts.ts +50 -0
  309. package/src/core/hook/citation-detect.ts +124 -0
  310. package/src/core/hook/cite-memo.ts +68 -0
  311. package/src/core/hook/claude-settings.ts +166 -0
  312. package/src/core/hook/gate.ts +25 -0
  313. package/src/core/hook/hook-log.ts +41 -0
  314. package/src/core/hook/memo-sweep.ts +164 -0
  315. package/src/core/hook/memo.ts +67 -0
  316. package/src/core/hook/pointer-block.ts +26 -0
  317. package/src/core/hook/select.ts +32 -0
  318. package/src/core/hook/transcript.ts +121 -0
  319. package/src/core/ingest/ingest-session.ts +111 -0
  320. package/src/core/providers/provider-models.ts +100 -0
  321. package/src/core/providers/provider-registry.ts +196 -0
  322. package/src/core/recall/citation-log.ts +108 -0
  323. package/src/core/recall/filter.ts +27 -0
  324. package/src/core/recall/index.ts +6 -0
  325. package/src/core/recall/match-fields.ts +40 -0
  326. package/src/core/recall/query-log.ts +149 -0
  327. package/src/core/recall/query-shape.ts +66 -0
  328. package/src/core/recall/recall-service.ts +320 -0
  329. package/src/core/recall/recent-log.ts +59 -0
  330. package/src/core/recall/tokenize.ts +18 -0
  331. package/src/core/recall/useful-scan.ts +336 -0
  332. package/src/core/recall-facts/fact-query-log.ts +150 -0
  333. package/src/core/recall-facts/fact-recall-service.ts +327 -0
  334. package/src/core/scheduler/scan-once.ts +142 -0
  335. package/src/core/scheduler/scheduler.ts +225 -0
  336. package/src/core/sources/source-registry.ts +260 -0
  337. package/src/core/storage/db-restore.ts +133 -0
  338. package/src/core/storage/live-status.ts +45 -0
  339. package/src/core/storage/migrate.ts +72 -0
  340. package/src/core/storage/sqlite-fact-store.ts +304 -0
  341. package/src/core/storage/sqlite-session-store.ts +765 -0
  342. package/src/hook/prompt-recall-hook.ts +174 -0
  343. package/src/hook/session-end-hook.ts +81 -0
  344. package/src/hook/session-start-hook.ts +165 -0
  345. package/src/hook/stop-hook.ts +236 -0
  346. package/src/http/app.ts +1114 -0
  347. package/src/install/claude-code.ts +128 -0
  348. package/src/install/codex.ts +367 -0
  349. package/src/install/hermes-agent.ts +76 -0
  350. package/src/install/hermes.ts +78 -0
  351. package/src/install/ollama.ts +208 -0
  352. package/src/install/setup.ts +368 -0
  353. package/src/llm/classifier-box.ts +64 -0
  354. package/src/llm/deepseek-client.ts +150 -0
  355. package/src/llm/env-autoload.ts +55 -0
  356. package/src/llm/ollama-client.ts +189 -0
  357. package/src/mcp/server.ts +534 -0
  358. package/src/ports/fact-store.ts +102 -0
  359. package/src/ports/llm-client.ts +52 -0
  360. package/src/ports/logger.ts +16 -0
  361. package/src/ports/session-store.ts +45 -0
  362. package/src/ports/transcript-adapter.ts +55 -0
  363. package/src/shared/types.ts +145 -0
  364. package/src/ui/App.tsx +58 -0
  365. package/src/ui/components/PromoteOpenButton.tsx +65 -0
  366. package/src/ui/components/SessionDrawer.tsx +136 -0
  367. package/src/ui/components/SideNav.tsx +162 -0
  368. package/src/ui/components/Skeleton.tsx +107 -0
  369. package/src/ui/index.html +13 -0
  370. package/src/ui/lib/actions.ts +30 -0
  371. package/src/ui/lib/api.ts +92 -0
  372. package/src/ui/lib/dataset.ts +141 -0
  373. package/src/ui/lib/registries.ts +155 -0
  374. package/src/ui/lib/view-settings.ts +41 -0
  375. package/src/ui/main.tsx +15 -0
  376. package/src/ui/pages/Live.tsx +229 -0
  377. package/src/ui/pages/Pulse.tsx +415 -0
  378. package/src/ui/pages/Recall.tsx +190 -0
  379. package/src/ui/pages/River.tsx +308 -0
  380. package/src/ui/pages/Search.tsx +93 -0
  381. package/src/ui/pages/Stub.tsx +9 -0
  382. package/src/ui/pages/Thread.tsx +262 -0
  383. package/src/ui/pages/settings/Classifier.tsx +227 -0
  384. package/src/ui/pages/settings/Data.tsx +190 -0
  385. package/src/ui/pages/settings/Index.tsx +65 -0
  386. package/src/ui/pages/settings/Labels.tsx +224 -0
  387. package/src/ui/pages/settings/Providers.tsx +305 -0
  388. package/src/ui/pages/settings/SettingsSubnav.tsx +28 -0
  389. package/src/ui/pages/settings/Sources.tsx +326 -0
  390. package/src/ui/pages/settings/Views.tsx +96 -0
  391. package/src/ui/styles.css +1766 -0
  392. package/src/ui/tsconfig.json +21 -0
  393. package/src/ui/vite.config.ts +19 -0
  394. package/tests/fixtures/claude_code/short_session.jsonl +2 -0
  395. package/tests/fixtures/claude_code/standard_iso.jsonl +4 -0
  396. package/tests/fixtures/claude_code/tool_heavy.jsonl +8 -0
  397. package/tests/fixtures/claude_code/with_subagent.jsonl +7 -0
  398. package/tests/fixtures/facts.ts +17 -0
  399. package/tests/fixtures/golden-corpus.ts +85 -0
  400. package/tests/fixtures/hermes/paired_request_dump.json +24 -0
  401. package/tests/fixtures/hermes/paired_session.json +23 -0
  402. package/tests/fixtures/hermes/request_dump.json +28 -0
  403. package/tests/fixtures/hermes/session_iso.json +38 -0
  404. package/tests/fixtures/hermes/session_unix.json +38 -0
  405. package/tests/fixtures/hermes/system_only.json +18 -0
  406. package/tests/fixtures/pi/error-connection-abort.jsonl +8 -0
  407. package/tests/fixtures/pi/short-successful.jsonl +5 -0
  408. package/tests/fixtures/pi/with-custom-message.jsonl +6 -0
  409. package/tests/fixtures/sessions.ts +22 -0
  410. package/tests/integration/backfill-facts.test.ts +362 -0
  411. package/tests/integration/citation-explicit.test.ts +111 -0
  412. package/tests/integration/cite-event.test.ts +169 -0
  413. package/tests/integration/cite-memo.test.ts +87 -0
  414. package/tests/integration/db-restore.test.ts +153 -0
  415. package/tests/integration/embed-backfill.test.ts +176 -0
  416. package/tests/integration/fact-supersedence.test.ts +313 -0
  417. package/tests/integration/fts-index.test.ts +60 -0
  418. package/tests/integration/getbyids-sqlite.test.ts +60 -0
  419. package/tests/integration/hermes-agent-hooks.test.ts +248 -0
  420. package/tests/integration/hook-claude-settings.test.ts +205 -0
  421. package/tests/integration/hook-log.test.ts +54 -0
  422. package/tests/integration/hook-memo.test.ts +68 -0
  423. package/tests/integration/hook-pre-compact.test.ts +105 -0
  424. package/tests/integration/hook-subagent-start.test.ts +102 -0
  425. package/tests/integration/http.test.ts +401 -0
  426. package/tests/integration/keyword-search-fts.test.ts +66 -0
  427. package/tests/integration/mcp-recall-logging.test.ts +88 -0
  428. package/tests/integration/mcp.test.ts +248 -0
  429. package/tests/integration/memo-sweep.test.ts +91 -0
  430. package/tests/integration/prompt-recall-hook.test.ts +88 -0
  431. package/tests/integration/provider-registry.test.ts +107 -0
  432. package/tests/integration/recall-golden.test.ts +59 -0
  433. package/tests/integration/recall-sqlite.test.ts +169 -0
  434. package/tests/integration/scheduler.test.ts +391 -0
  435. package/tests/integration/session-end-hook.test.ts +48 -0
  436. package/tests/integration/session-start-hook.test.ts +126 -0
  437. package/tests/integration/source-registry.test.ts +120 -0
  438. package/tests/integration/sqlite-fact-store.test.ts +346 -0
  439. package/tests/integration/stop-hook.test.ts +560 -0
  440. package/tests/integration/wal-checkpoint.test.ts +49 -0
  441. package/tests/unit/cli/launchctl-helpers.test.ts +60 -0
  442. package/tests/unit/core/adapters/aider.test.ts +230 -0
  443. package/tests/unit/core/adapters/claude-code.test.ts +118 -0
  444. package/tests/unit/core/adapters/hermes-agent.test.ts +329 -0
  445. package/tests/unit/core/adapters/hermes.test.ts +81 -0
  446. package/tests/unit/core/adapters/jsonl-generic.test.ts +142 -0
  447. package/tests/unit/core/adapters/opencode.test.ts +354 -0
  448. package/tests/unit/core/adapters/pi.test.ts +110 -0
  449. package/tests/unit/core/classifier/prompt.test.ts +126 -0
  450. package/tests/unit/core/embedding/chunk-body.test.ts +100 -0
  451. package/tests/unit/core/facts/extract-facts.test.ts +117 -0
  452. package/tests/unit/core/filter.test.ts +40 -0
  453. package/tests/unit/core/hook/citation-detect-cite-session.test.ts +96 -0
  454. package/tests/unit/core/hook/citation-detect.test.ts +124 -0
  455. package/tests/unit/core/hook/gate.test.ts +29 -0
  456. package/tests/unit/core/hook/pointer-block.test.ts +22 -0
  457. package/tests/unit/core/hook/select.test.ts +66 -0
  458. package/tests/unit/core/match-fields.test.ts +39 -0
  459. package/tests/unit/core/mcp-cite-session.test.ts +51 -0
  460. package/tests/unit/core/providers/provider-models.test.ts +101 -0
  461. package/tests/unit/core/query-shape.test.ts +92 -0
  462. package/tests/unit/core/recall-facts/fact-recall-service.test.ts +258 -0
  463. package/tests/unit/core/recall-service.test.ts +200 -0
  464. package/tests/unit/core/storage/live-status.test.ts +54 -0
  465. package/tests/unit/core/tokenize.test.ts +32 -0
  466. package/tests/unit/core/useful-scan.test.ts +537 -0
  467. package/tests/unit/llm/embed.test.ts +93 -0
  468. package/tests/unit/llm/ollama-client.test.ts +124 -0
  469. package/tests/unit/scripts/longmemeval-scorer.test.ts +114 -0
  470. package/tsconfig.json +31 -0
  471. package/tsconfig.test.json +11 -0
  472. package/vitest.config.ts +22 -0
@@ -0,0 +1,20 @@
1
+ /**
2
+ * extractFacts — pure transform from ClassifyResult to Fact[].
3
+ *
4
+ * Lives in core/, has no framework imports, no clock or randomness coupling
5
+ * (id generator and timestamp are injected so tests are deterministic).
6
+ * Phase B.2 — see docs/plans/factstore-design.md Section 3.
7
+ *
8
+ * Confidence policy (Section 3 of the plan): facts inherit the session-level
9
+ * confidence verbatim. Below 0.4 the function returns an empty array — the
10
+ * session still ingests with markers, but its facts are dropped as
11
+ * extraction-quality noise. Between 0.4 and 0.6 facts are written but will
12
+ * be filtered out of recall by the FactStore default `minConfidence: 0.6`.
13
+ */
14
+ import type { ClassifyResult } from "../../ports/llm-client.js";
15
+ import type { Fact } from "../../shared/types.js";
16
+ export interface ExtractFactsOptions {
17
+ /** Generator for fact ids. Defaults to `fact_<randomUUID()>`. */
18
+ readonly idGenerator?: () => string;
19
+ }
20
+ export declare function extractFacts(result: ClassifyResult, sessionId: string, createdAt: string, opts?: ExtractFactsOptions): Fact[];
@@ -0,0 +1,37 @@
1
+ /**
2
+ * extractFacts — pure transform from ClassifyResult to Fact[].
3
+ *
4
+ * Lives in core/, has no framework imports, no clock or randomness coupling
5
+ * (id generator and timestamp are injected so tests are deterministic).
6
+ * Phase B.2 — see docs/plans/factstore-design.md Section 3.
7
+ *
8
+ * Confidence policy (Section 3 of the plan): facts inherit the session-level
9
+ * confidence verbatim. Below 0.4 the function returns an empty array — the
10
+ * session still ingests with markers, but its facts are dropped as
11
+ * extraction-quality noise. Between 0.4 and 0.6 facts are written but will
12
+ * be filtered out of recall by the FactStore default `minConfidence: 0.6`.
13
+ */
14
+ import { randomUUID } from "node:crypto";
15
+ const CONFIDENCE_FLOOR = 0.4;
16
+ export function extractFacts(result, sessionId, createdAt, opts = {}) {
17
+ if (result.confidence < CONFIDENCE_FLOOR)
18
+ return [];
19
+ const genId = opts.idGenerator ?? (() => `fact_${randomUUID()}`);
20
+ const out = [];
21
+ for (const raw of result.facts) {
22
+ out.push({
23
+ id: genId(),
24
+ kind: raw.kind,
25
+ subject: raw.subject,
26
+ predicate: raw.predicate,
27
+ value: raw.value,
28
+ sourceSessionId: sessionId,
29
+ sourceQuote: raw.sourceQuote ?? null,
30
+ createdAt,
31
+ supersededBy: null,
32
+ confidence: result.confidence,
33
+ });
34
+ }
35
+ return out;
36
+ }
37
+ //# sourceMappingURL=extract-facts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-facts.js","sourceRoot":"","sources":["../../../src/core/facts/extract-facts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAIzC,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAO7B,MAAM,UAAU,YAAY,CAC1B,MAAsB,EACtB,SAAiB,EACjB,SAAiB,EACjB,OAA4B,EAAE;IAE9B,IAAI,MAAM,CAAC,UAAU,GAAG,gBAAgB;QAAE,OAAO,EAAE,CAAC;IACpD,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,UAAU,EAAE,EAAE,CAAC,CAAC;IACjE,MAAM,GAAG,GAAW,EAAE,CAAC;IACvB,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAC/B,GAAG,CAAC,IAAI,CAAC;YACP,EAAE,EAAE,KAAK,EAAE;YACX,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,OAAO,EAAE,GAAG,CAAC,OAAO;YACpB,SAAS,EAAE,GAAG,CAAC,SAAS;YACxB,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,eAAe,EAAE,SAAS;YAC1B,WAAW,EAAE,GAAG,CAAC,WAAW,IAAI,IAAI;YACpC,SAAS;YACT,YAAY,EAAE,IAAI;YAClB,UAAU,EAAE,MAAM,CAAC,UAAU;SAC9B,CAAC,CAAC;IACL,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Detects which surfaced recall IDs an assistant turn cited.
3
+ *
4
+ * Two channels, ordered by signal strength:
5
+ * - tool_use: the model invoked an MCP NLM tool (get_session, recall_facts,
6
+ * get_fact_history, recall_sessions) whose input references a
7
+ * surfaced ID. This is the strong "the model dug into the
8
+ * surfaced session" signal. Almost no false positives.
9
+ * - prose: the surfaced ID appears as a substring in the response text.
10
+ * Models rarely echo session IDs verbatim, so this channel
11
+ * fires in practice almost never — kept for completeness.
12
+ *
13
+ * Returns both the union of cited IDs and the per-ID channel so the citation
14
+ * log can carry kind metadata. ID minimum length keeps generic short tokens
15
+ * from false-positiving against either channel.
16
+ *
17
+ * This is the training-data substrate for a future learned reranker.
18
+ */
19
+ import type { ToolUseBlock } from "./transcript.js";
20
+ export type CitationKind = "tool_use" | "prose";
21
+ export interface CitationDetectInput {
22
+ readonly responseText: string;
23
+ readonly toolUses: ReadonlyArray<ToolUseBlock>;
24
+ readonly surfacedIds: Iterable<string>;
25
+ }
26
+ export interface DetectedCitation {
27
+ readonly id: string;
28
+ readonly kind: CitationKind;
29
+ }
30
+ export declare function detectCitations(input: CitationDetectInput): DetectedCitation[];
31
+ /** Back-compat: prose-only detector returning a flat id list. */
32
+ export declare function detectCitedIds(responseText: string, surfacedIds: Iterable<string>): string[];
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Detects which surfaced recall IDs an assistant turn cited.
3
+ *
4
+ * Two channels, ordered by signal strength:
5
+ * - tool_use: the model invoked an MCP NLM tool (get_session, recall_facts,
6
+ * get_fact_history, recall_sessions) whose input references a
7
+ * surfaced ID. This is the strong "the model dug into the
8
+ * surfaced session" signal. Almost no false positives.
9
+ * - prose: the surfaced ID appears as a substring in the response text.
10
+ * Models rarely echo session IDs verbatim, so this channel
11
+ * fires in practice almost never — kept for completeness.
12
+ *
13
+ * Returns both the union of cited IDs and the per-ID channel so the citation
14
+ * log can carry kind metadata. ID minimum length keeps generic short tokens
15
+ * from false-positiving against either channel.
16
+ *
17
+ * This is the training-data substrate for a future learned reranker.
18
+ */
19
+ const MIN_ID_LEN = 6;
20
+ export function detectCitations(input) {
21
+ const surfaced = [];
22
+ const seen = new Set();
23
+ for (const id of input.surfacedIds) {
24
+ if (id.length < MIN_ID_LEN)
25
+ continue;
26
+ if (seen.has(id))
27
+ continue;
28
+ seen.add(id);
29
+ surfaced.push(id);
30
+ }
31
+ const cited = [];
32
+ const claimedByToolUse = new Set();
33
+ // Channel A: tool_use. Two sub-cases:
34
+ //
35
+ // A1: cite_session — the model called the explicit citation primitive with
36
+ // the session ID in tu.input.id. Strongest possible signal: structured,
37
+ // deterministic, zero ambiguity. ID must be a surfaced session ID.
38
+ //
39
+ // A2: other NLM tools (get_session, recall_sessions, recall_facts,
40
+ // get_fact_history) — stringify the input and substring-scan for surfaced
41
+ // IDs. These tools accept ids via top-level fields, so the serialization
42
+ // always includes the id when used.
43
+ for (const tu of input.toolUses) {
44
+ if (!isNlmTool(tu.name))
45
+ continue;
46
+ if (isCiteSessionTool(tu.name)) {
47
+ // A1: explicit cite_session call — the MCP server handler already wrote
48
+ // this citation directly to the citation log (citeSessionHandler →
49
+ // appendCitation). Detecting it here again would produce a second log
50
+ // entry for the same model action (double-count). Skip so the Stop hook
51
+ // only captures implicit citations the MCP handler didn't see.
52
+ continue;
53
+ }
54
+ // A2: other NLM tools — serialize and substring-scan.
55
+ const serialized = safeStringify(tu.input);
56
+ if (!serialized)
57
+ continue;
58
+ for (const id of surfaced) {
59
+ if (claimedByToolUse.has(id))
60
+ continue;
61
+ if (serialized.includes(id)) {
62
+ cited.push({ id, kind: "tool_use" });
63
+ claimedByToolUse.add(id);
64
+ }
65
+ }
66
+ }
67
+ // Channel B: prose. Only emit if the tool_use channel didn't already
68
+ // claim this id — same id shouldn't double-count.
69
+ if (input.responseText) {
70
+ for (const id of surfaced) {
71
+ if (claimedByToolUse.has(id))
72
+ continue;
73
+ if (input.responseText.includes(id)) {
74
+ cited.push({ id, kind: "prose" });
75
+ }
76
+ }
77
+ }
78
+ return cited;
79
+ }
80
+ /** Back-compat: prose-only detector returning a flat id list. */
81
+ export function detectCitedIds(responseText, surfacedIds) {
82
+ return detectCitations({
83
+ responseText,
84
+ toolUses: [],
85
+ surfacedIds,
86
+ }).map((c) => c.id);
87
+ }
88
+ function isNlmTool(name) {
89
+ // Claude Code namespaces MCP tools as `mcp__<server>__<tool>`. The NLM
90
+ // server name is "nlm-memory" in the user's .mcp.json today; accept any
91
+ // server name containing "nlm" so future renames stay covered.
92
+ return /^mcp__[^_]*nlm[^_]*__/.test(name);
93
+ }
94
+ function isCiteSessionTool(name) {
95
+ return name.endsWith("__cite_session");
96
+ }
97
+ function safeStringify(value) {
98
+ try {
99
+ return JSON.stringify(value);
100
+ }
101
+ catch {
102
+ return "";
103
+ }
104
+ }
105
+ //# sourceMappingURL=citation-detect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"citation-detect.js","sourceRoot":"","sources":["../../../src/core/hook/citation-detect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Per-conversation dedup memo for the Stop hook's citation detector.
3
+ *
4
+ * The Stop hook scans the full transcript every fire, so a long conversation
5
+ * with repeated Stop firings would otherwise re-detect the same tool_use
6
+ * citations every turn and double-count them in the citation log. This memo
7
+ * holds the set of (conversationId, citedId) pairs already posted, so each
8
+ * citation lands exactly once regardless of how many times Stop fires.
9
+ *
10
+ * Storage parallels the surfaced-memo (`memo.ts`): same state directory
11
+ * (`~/.nlm/hook-state/`, overridable via NLM_HOOK_STATE_DIR), filename suffix
12
+ * `.cited.json` to distinguish from the surfaced memo's `.json`. The existing
13
+ * memo-sweep walks the directory by mtime and cleans both files together.
14
+ *
15
+ * Defensive: a missing or corrupt file yields an empty set; a write failure
16
+ * is swallowed. Telemetry path — must never break the hook.
17
+ */
18
+ export declare function loadCited(conversationId: string): Set<string>;
19
+ export declare function recordCited(conversationId: string, ids: ReadonlyArray<string>): void;
20
+ export declare function clearCited(conversationId: string): boolean;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Per-conversation dedup memo for the Stop hook's citation detector.
3
+ *
4
+ * The Stop hook scans the full transcript every fire, so a long conversation
5
+ * with repeated Stop firings would otherwise re-detect the same tool_use
6
+ * citations every turn and double-count them in the citation log. This memo
7
+ * holds the set of (conversationId, citedId) pairs already posted, so each
8
+ * citation lands exactly once regardless of how many times Stop fires.
9
+ *
10
+ * Storage parallels the surfaced-memo (`memo.ts`): same state directory
11
+ * (`~/.nlm/hook-state/`, overridable via NLM_HOOK_STATE_DIR), filename suffix
12
+ * `.cited.json` to distinguish from the surfaced memo's `.json`. The existing
13
+ * memo-sweep walks the directory by mtime and cleans both files together.
14
+ *
15
+ * Defensive: a missing or corrupt file yields an empty set; a write failure
16
+ * is swallowed. Telemetry path — must never break the hook.
17
+ */
18
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
19
+ import { homedir } from "node:os";
20
+ import { join } from "node:path";
21
+ function stateDir() {
22
+ return process.env["NLM_HOOK_STATE_DIR"] ?? join(homedir(), ".nlm", "hook-state");
23
+ }
24
+ function memoPath(conversationId) {
25
+ const safe = conversationId.replace(/[^A-Za-z0-9_-]/g, "_") || "unknown";
26
+ return join(stateDir(), `${safe}.cited.json`);
27
+ }
28
+ export function loadCited(conversationId) {
29
+ try {
30
+ const path = memoPath(conversationId);
31
+ if (!existsSync(path))
32
+ return new Set();
33
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
34
+ if (!Array.isArray(parsed))
35
+ return new Set();
36
+ return new Set(parsed.filter((x) => typeof x === "string"));
37
+ }
38
+ catch {
39
+ return new Set();
40
+ }
41
+ }
42
+ export function recordCited(conversationId, ids) {
43
+ if (ids.length === 0)
44
+ return;
45
+ try {
46
+ const merged = loadCited(conversationId);
47
+ for (const id of ids)
48
+ merged.add(id);
49
+ mkdirSync(stateDir(), { recursive: true });
50
+ writeFileSync(memoPath(conversationId), JSON.stringify([...merged]), "utf8");
51
+ }
52
+ catch {
53
+ // Memo write failure must never break the hook.
54
+ }
55
+ }
56
+ export function clearCited(conversationId) {
57
+ try {
58
+ const path = memoPath(conversationId);
59
+ if (!existsSync(path))
60
+ return false;
61
+ rmSync(path);
62
+ return true;
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ }
68
+ //# sourceMappingURL=cite-memo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cite-memo.js","sourceRoot":"","sources":["../../../src/core/hook/cite-memo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,SAAS,QAAQ;IACf,OAAO,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC;AACpF,CAAC;AAED,SAAS,QAAQ,CAAC,cAAsB;IACtC,MAAM,IAAI,GAAG,cAAc,CAAC,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,IAAI,SAAS,CAAC;IACzE,OAAO,IAAI,CAAC,QAAQ,EAAE,EAAE,GAAG,IAAI,aAAa,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,cAAsB;IAC9C,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAC;QACtC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,GAAG,EAAE,CAAC;QACxC,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QAC/D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;YAAE,OAAO,IAAI,GAAG,EAAE,CAAC;QAC7C,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC;IAC3E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,GAAG,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CACzB,cAAsB,EACtB,GAA0B;IAE1B,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAC7B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,SAAS,CAAC,cAAc,CAAC,CAAC;QACzC,KAAK,MAAM,EAAE,IAAI,GAAG;YAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACrC,SAAS,CAAC,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3C,aAAa,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC/E,CAAC;IAAC,MAAM,CAAC;QACP,gDAAgD;IAClD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,cAAsB;IAC/C,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAC;QACtC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QACpC,MAAM,CAAC,IAAI,CAAC,CAAC;QACb,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Adds/removes NLM hook entries in a Claude Code settings.json.
3
+ *
4
+ * NLM-owned entries are identified by HOOK_SCRIPT_MARKERS. add is idempotent
5
+ * (replaces any prior NLM entry for the same event); remove strips only NLM
6
+ * entries and preserves everything else.
7
+ */
8
+ /**
9
+ * Single-quote a shell argument so paths with spaces or other shell
10
+ * metacharacters survive `sh -c` tokenization. Without this, a path like
11
+ * `/Users/echalupa/Documents/Coding Projects/...` is split on whitespace
12
+ * and node receives the wrong argv — silent hook bricking.
13
+ */
14
+ export declare function shellQuote(arg: string): string;
15
+ export declare function buildHookCommand(execPath: string, hookJs: string, mode: "shadow" | "live"): string;
16
+ export interface SmokeTestResult {
17
+ readonly ok: boolean;
18
+ readonly reason?: string;
19
+ readonly stderr?: string;
20
+ }
21
+ /**
22
+ * Invoke the wired command exactly the way Claude Code does (sh -c with
23
+ * JSON on stdin) and confirm the hook log gained an entry. Catches the
24
+ * class of failures where settings.json looks valid but the hook fails
25
+ * at startup (path tokenization, missing modules, etc.).
26
+ */
27
+ export declare function smokeTestHookCommand(command: string, hookLogPath: string, timeoutMs?: number): SmokeTestResult;
28
+ export type ClaudeHookEvent = "UserPromptSubmit" | "SessionStart" | "SessionEnd" | "Stop" | "PreCompact" | "SubagentStart" | "PostToolUse" | "PreToolUse";
29
+ export declare function addHook(settingsPath: string, command: string, event?: ClaudeHookEvent): void;
30
+ /**
31
+ * Remove the NLM-tagged hook entry from one event (default UserPromptSubmit)
32
+ * or every event when `event === "*"`. Leaves unrelated entries untouched.
33
+ */
34
+ export declare function removeHook(settingsPath: string, event?: ClaudeHookEvent | "*"): void;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Adds/removes NLM hook entries in a Claude Code settings.json.
3
+ *
4
+ * NLM-owned entries are identified by HOOK_SCRIPT_MARKERS. add is idempotent
5
+ * (replaces any prior NLM entry for the same event); remove strips only NLM
6
+ * entries and preserves everything else.
7
+ */
8
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
9
+ import { dirname } from "node:path";
10
+ import { spawnSync } from "node:child_process";
11
+ // Every NLM hook script ends in `-hook.js`. We tag entries we own by
12
+ // matching the filename suffix against this list. Add new entries here
13
+ // when a new hook script ships.
14
+ const HOOK_SCRIPT_MARKERS = [
15
+ "prompt-recall-hook.js",
16
+ "session-end-hook.js",
17
+ "stop-hook.js",
18
+ "session-start-hook.js",
19
+ "pre-compact-hook.js",
20
+ "subagent-start-hook.js",
21
+ ];
22
+ /**
23
+ * Single-quote a shell argument so paths with spaces or other shell
24
+ * metacharacters survive `sh -c` tokenization. Without this, a path like
25
+ * `/Users/echalupa/Documents/Coding Projects/...` is split on whitespace
26
+ * and node receives the wrong argv — silent hook bricking.
27
+ */
28
+ export function shellQuote(arg) {
29
+ return `'${arg.replace(/'/g, "'\\''")}'`;
30
+ }
31
+ export function buildHookCommand(execPath, hookJs, mode) {
32
+ return `NLM_HOOK_MODE=${mode} ${shellQuote(execPath)} ${shellQuote(hookJs)}`;
33
+ }
34
+ /**
35
+ * Invoke the wired command exactly the way Claude Code does (sh -c with
36
+ * JSON on stdin) and confirm the hook log gained an entry. Catches the
37
+ * class of failures where settings.json looks valid but the hook fails
38
+ * at startup (path tokenization, missing modules, etc.).
39
+ */
40
+ export function smokeTestHookCommand(command, hookLogPath, timeoutMs = 5000) {
41
+ const sizeBefore = existsSync(hookLogPath) ? statSync(hookLogPath).size : 0;
42
+ const result = spawnSync("sh", ["-c", command], {
43
+ input: JSON.stringify({ prompt: "smoke test", session_id: "install-smoke" }),
44
+ timeout: timeoutMs,
45
+ encoding: "utf8",
46
+ });
47
+ if (result.error) {
48
+ return { ok: false, reason: `spawn failed: ${result.error.message}` };
49
+ }
50
+ if (result.status !== 0) {
51
+ return {
52
+ ok: false,
53
+ reason: `exit code ${result.status ?? "null"}`,
54
+ stderr: result.stderr,
55
+ };
56
+ }
57
+ const sizeAfter = existsSync(hookLogPath) ? statSync(hookLogPath).size : 0;
58
+ if (sizeAfter <= sizeBefore) {
59
+ return {
60
+ ok: false,
61
+ reason: `no entry appended to ${hookLogPath}`,
62
+ stderr: result.stderr,
63
+ };
64
+ }
65
+ return { ok: true };
66
+ }
67
+ function read(path) {
68
+ if (!existsSync(path))
69
+ return {};
70
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
71
+ if (typeof parsed !== "object" || parsed === null) {
72
+ throw new Error(`Claude settings at ${path} is not a JSON object`);
73
+ }
74
+ return parsed;
75
+ }
76
+ function write(path, settings) {
77
+ mkdirSync(dirname(path), { recursive: true });
78
+ writeFileSync(path, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
79
+ }
80
+ function isNlmEntry(entry) {
81
+ return entry.hooks.some((h) => HOOK_SCRIPT_MARKERS.some((marker) => h.command.includes(marker)));
82
+ }
83
+ export function addHook(settingsPath, command, event = "UserPromptSubmit") {
84
+ const settings = read(settingsPath);
85
+ const hooks = settings.hooks ?? {};
86
+ const existing = hooks[event] ?? [];
87
+ const others = existing.filter((e) => !isNlmEntry(e));
88
+ const next = [
89
+ ...others,
90
+ { hooks: [{ type: "command", command }] },
91
+ ];
92
+ write(settingsPath, { ...settings, hooks: { ...hooks, [event]: next } });
93
+ }
94
+ /**
95
+ * Remove the NLM-tagged hook entry from one event (default UserPromptSubmit)
96
+ * or every event when `event === "*"`. Leaves unrelated entries untouched.
97
+ */
98
+ export function removeHook(settingsPath, event = "UserPromptSubmit") {
99
+ if (!existsSync(settingsPath))
100
+ return;
101
+ const settings = read(settingsPath);
102
+ const allHooks = settings.hooks ?? {};
103
+ const events = event === "*" ? Object.keys(allHooks) : [event];
104
+ const nextHooks = { ...allHooks };
105
+ for (const ev of events) {
106
+ const existing = nextHooks[ev];
107
+ if (!existing)
108
+ continue;
109
+ const kept = existing.filter((e) => !isNlmEntry(e));
110
+ if (kept.length > 0)
111
+ nextHooks[ev] = kept;
112
+ else
113
+ delete nextHooks[ev];
114
+ }
115
+ write(settingsPath, { ...settings, hooks: nextHooks });
116
+ }
117
+ //# sourceMappingURL=claude-settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-settings.js","sourceRoot":"","sources":["../../../src/core/hook/claude-settings.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Prompt gate for the recall hook. Pure — no I/O.
3
+ *
4
+ * A conservative generative *excluder*: the default is "evaluate" (query
5
+ * recall); only high-precision generative openers short-circuit to
6
+ * "generative". A false "generative" wrongly skips recall — the exact
7
+ * failure this feature fixes — so the generative set is deliberately tight.
8
+ * It is calibrated further against shadow-mode logs.
9
+ */
10
+ export type PromptClass = "generative" | "evaluate";
11
+ export declare function classifyPrompt(prompt: string): PromptClass;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Prompt gate for the recall hook. Pure — no I/O.
3
+ *
4
+ * A conservative generative *excluder*: the default is "evaluate" (query
5
+ * recall); only high-precision generative openers short-circuit to
6
+ * "generative". A false "generative" wrongly skips recall — the exact
7
+ * failure this feature fixes — so the generative set is deliberately tight.
8
+ * It is calibrated further against shadow-mode logs.
9
+ */
10
+ const LEADING_FILLER = /^(please|can you|could you|would you|will you|i need you to|i'd like you to|i want you to|i would like you to|help me|let's|lets|hey|ok|okay)\b[\s,]*/i;
11
+ const GENERATIVE_OPENER = /^(write|draft|create|compose|generate|brainstorm|design|outline|sketch|invent|rename|come up with)\b/i;
12
+ export function classifyPrompt(prompt) {
13
+ let p = prompt.trim();
14
+ for (let i = 0; i < 3 && LEADING_FILLER.test(p); i++) {
15
+ p = p.replace(LEADING_FILLER, "");
16
+ }
17
+ return GENERATIVE_OPENER.test(p) ? "generative" : "evaluate";
18
+ }
19
+ //# sourceMappingURL=gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate.js","sourceRoot":"","sources":["../../../src/core/hook/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,MAAM,cAAc,GAClB,wJAAwJ,CAAC;AAE3J,MAAM,iBAAiB,GACrB,uGAAuG,CAAC;AAE1G,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,IAAI,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACrD,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IACpC,CAAC;IACD,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,UAAU,CAAC;AAC/D,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Append-only JSONL log for the recall hook. One line per prompt the hook
3
+ * evaluated. This is the dataset the relevance gate (generative patterns +
4
+ * score threshold) is calibrated against during the shadow window.
5
+ *
6
+ * Path defaults to ~/.nlm/hook-log.jsonl, overridable via NLM_HOOK_LOG.
7
+ * appendHookLog swallows its own errors — telemetry must never break the hook.
8
+ * Uses synchronous I/O: the hook is a short-lived per-prompt process, and an
9
+ * async write could be lost if the process exits before it flushes.
10
+ */
11
+ import type { PromptClass } from "./gate.js";
12
+ export interface HookLogEntry {
13
+ readonly ts: string;
14
+ readonly conversationId: string;
15
+ readonly promptPreview: string;
16
+ readonly gate: PromptClass;
17
+ readonly hits: ReadonlyArray<{
18
+ readonly id: string;
19
+ readonly score: number;
20
+ }>;
21
+ readonly wouldInject: ReadonlyArray<string>;
22
+ readonly estTokens: number;
23
+ readonly mode: "shadow" | "live";
24
+ }
25
+ export declare function appendHookLog(entry: HookLogEntry): void;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Append-only JSONL log for the recall hook. One line per prompt the hook
3
+ * evaluated. This is the dataset the relevance gate (generative patterns +
4
+ * score threshold) is calibrated against during the shadow window.
5
+ *
6
+ * Path defaults to ~/.nlm/hook-log.jsonl, overridable via NLM_HOOK_LOG.
7
+ * appendHookLog swallows its own errors — telemetry must never break the hook.
8
+ * Uses synchronous I/O: the hook is a short-lived per-prompt process, and an
9
+ * async write could be lost if the process exits before it flushes.
10
+ */
11
+ import { appendFileSync, mkdirSync } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import { dirname, join } from "node:path";
14
+ function logPath() {
15
+ return process.env["NLM_HOOK_LOG"] ?? join(homedir(), ".nlm", "hook-log.jsonl");
16
+ }
17
+ export function appendHookLog(entry) {
18
+ try {
19
+ const path = logPath();
20
+ mkdirSync(dirname(path), { recursive: true });
21
+ // Sync I/O: hook is a short-lived process — async write could be lost on exit.
22
+ appendFileSync(path, `${JSON.stringify(entry)}\n`, "utf8");
23
+ }
24
+ catch {
25
+ // Telemetry failure must never break the hook.
26
+ }
27
+ }
28
+ //# sourceMappingURL=hook-log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-log.js","sourceRoot":"","sources":["../../../src/core/hook/hook-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAc1C,SAAS,OAAO;IACd,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC;AAClF,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAmB;IAC/C,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;QACvB,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9C,+EAA+E;QAC/E,cAAc,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC7D,CAAC;IAAC,MAAM,CAAC;QACP,+CAA+C;IACjD,CAAC;AACH,CAAC"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Idle sweep for per-conversation hook memo files.
3
+ *
4
+ * The SessionEnd hook is best-effort — Claude Code doesn't fire it on
5
+ * crashes, kill -9, or IDE force-close. Without a backstop, memo files
6
+ * at ~/.nlm/hook-state/<conv>.json accumulate forever for any session
7
+ * that didn't close cleanly.
8
+ *
9
+ * This sweep is the daemon-side backstop. It runs on a timer, scans the
10
+ * state directory, and deletes any memo whose mtime is older than the
11
+ * dormant threshold. Reuses the same `age > day` threshold the dataset
12
+ * builder uses to mark runtimes as "dormant" so the UI/dataset semantics
13
+ * stay consistent across the system.
14
+ *
15
+ * Hooks are fast-path; this is the always-correct backstop.
16
+ */
17
+ export interface MemoSweepOptions {
18
+ /** Directory holding per-conversation memo files. Defaults to ~/.nlm/hook-state/. */
19
+ readonly stateDir?: string;
20
+ /** Age threshold in ms beyond which a memo is swept. Default 24h (dormant). */
21
+ readonly dormantMs?: number;
22
+ /** Tick interval in ms. Default 5 min. */
23
+ readonly intervalMs?: number;
24
+ /** Defaults to console.error. Set to a noop in tests. */
25
+ readonly logger?: (msg: string) => void;
26
+ /** Override for time source — for deterministic tests. */
27
+ readonly now?: () => number;
28
+ }
29
+ export interface SweepReport {
30
+ readonly scanned: number;
31
+ readonly deleted: number;
32
+ readonly kept: number;
33
+ readonly errors: number;
34
+ }
35
+ /**
36
+ * One-shot sweep. Returns the report; safe to call from tests or one-off
37
+ * CLI invocations without standing up the scheduler.
38
+ */
39
+ export declare function sweepMemoDir(opts?: MemoSweepOptions): SweepReport;
40
+ /**
41
+ * Periodic sweep loop. Mirrors ScanScheduler's start/stop shape so the
42
+ * daemon can manage it the same way. First tick fires immediately on
43
+ * start() — the daemon picking up after a long downtime should sweep
44
+ * accumulated memos right away, not wait an interval.
45
+ */
46
+ export declare class MemoSweepScheduler {
47
+ private readonly opts;
48
+ private stopped;
49
+ private timer;
50
+ constructor(opts?: MemoSweepOptions);
51
+ start(): void;
52
+ stop(): void;
53
+ tick(): SweepReport;
54
+ private scheduleNext;
55
+ }