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,832 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * nlm — CLI entry point. Composition root for the whole stack.
4
+ *
5
+ * This is the one file that knows about every concrete implementation:
6
+ * SqliteSessionStore (storage), OllamaClient (LLM), Hono (HTTP),
7
+ * McpServer (MCP). Every other module depends on ports. Swapping a
8
+ * backend means editing this file, not anything inside core/.
9
+ *
10
+ * Subcommands:
11
+ * nlm start — boot HTTP server on $NLM_PORT (default 3940)
12
+ * nlm migrate — run pending migrations against the canonical SQLite
13
+ * nlm recall — one-shot recall query from the shell (debugging)
14
+ * nlm mcp — run as an MCP stdio server (for ~/.mcp.json wiring)
15
+ * nlm setup — interactive first-run wizard (recommended entry point)
16
+ * nlm install — install the macOS LaunchAgent (auto-start on login)
17
+ * nlm uninstall — remove the macOS LaunchAgent
18
+ * nlm hook install — add the recall hook to Claude Code (shadow mode)
19
+ * nlm hook uninstall — remove the recall hook from Claude Code
20
+ * nlm connect claude-code — write MCP server block to ~/.mcp.json
21
+ * nlm connect codex — install Codex marketplace plugin
22
+ * nlm disconnect claude-code — remove MCP block from ~/.mcp.json
23
+ * nlm disconnect codex — remove Codex plugin
24
+ */
25
+ import { fileURLToPath } from "node:url";
26
+ import { dirname, resolve, join } from "node:path";
27
+ import { homedir } from "node:os";
28
+ import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs";
29
+ import { execFileSync } from "node:child_process";
30
+ import { Command } from "commander";
31
+ import { serve } from "@hono/node-server";
32
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
33
+ import { FactRecallService } from "../core/recall-facts/fact-recall-service.js";
34
+ import { RecallService } from "../core/recall/recall-service.js";
35
+ import { SqliteFactStore } from "../core/storage/sqlite-fact-store.js";
36
+ import { ProviderRegistry } from "../core/providers/provider-registry.js";
37
+ import { SourceRegistry } from "../core/sources/source-registry.js";
38
+ import { SqliteSessionStore } from "../core/storage/sqlite-session-store.js";
39
+ import { applyPendingRestore } from "../core/storage/db-restore.js";
40
+ import { createApp } from "../http/app.js";
41
+ import { createMcpServer } from "../mcp/server.js";
42
+ import { ClassifierBox } from "../llm/classifier-box.js";
43
+ import { DeepSeekClient } from "../llm/deepseek-client.js";
44
+ import { OllamaClient } from "../llm/ollama-client.js";
45
+ import { autoloadEnv } from "../llm/env-autoload.js";
46
+ import { addHook, buildHookCommand, removeHook, smokeTestHookCommand } from "../core/hook/claude-settings.js";
47
+ import { codexBinaryAvailable, connectCodex, disconnectCodex, pluginScriptsDir, } from "../install/codex.js";
48
+ import { connectClaudeCode, disconnectClaudeCode, installClaudeCodeHooks } from "../install/claude-code.js";
49
+ import { connectHermes, disconnectHermes, hermesConfigPath } from "../install/hermes.js";
50
+ import { connectHermesAgent, disconnectHermesAgent, hermesAgentPluginDir } from "../install/hermes-agent.js";
51
+ import { runSetup } from "../install/setup.js";
52
+ import { runParity } from "./classify-parity.js";
53
+ import { reembedCorpus } from "../core/embedding/embed-backfill.js";
54
+ import { backfillFacts } from "../core/facts/backfill-facts.js";
55
+ import { normalizeEmbeddings } from "../core/embedding/embed-normalize.js";
56
+ import { ScanScheduler } from "../core/scheduler/scheduler.js";
57
+ import { MemoSweepScheduler } from "../core/hook/memo-sweep.js";
58
+ import { isAgentLoaded, isBenignBootoutError } from "./launchctl-helpers.js";
59
+ import { adapterFromSource } from "../core/adapters/from-source.js";
60
+ import { scanUsefulHits } from "../core/recall/useful-scan.js";
61
+ const __filename = fileURLToPath(import.meta.url);
62
+ const __dirname = dirname(__filename);
63
+ const MIGRATIONS_DIR = resolve(__dirname, "../../migrations");
64
+ const UI_DIST = resolve(__dirname, "../../dist/ui");
65
+ const DEFAULT_DB_PATH = resolve(homedir(), ".nlm/canonical.sqlite");
66
+ const DEFAULT_PORT = 3940;
67
+ function dbPath() {
68
+ return process.env["NLM_DB_PATH"] ?? DEFAULT_DB_PATH;
69
+ }
70
+ function port() {
71
+ const raw = process.env["NLM_PORT"];
72
+ if (!raw)
73
+ return DEFAULT_PORT;
74
+ const n = Number.parseInt(raw, 10);
75
+ if (!Number.isFinite(n) || n < 1 || n > 65_535)
76
+ return DEFAULT_PORT;
77
+ return n;
78
+ }
79
+ function ollamaUrl() {
80
+ return process.env["NLM_OLLAMA_URL"] ?? "http://localhost:11434";
81
+ }
82
+ function buildClassifier() {
83
+ // DeepSeek V4 Flash is the default for the ingest classifier per the
84
+ // 2026-05-19 parity run: ~5s/session, 90% first-try success vs Ollama
85
+ // phi4-mini's 0% on the same first three sessions. Override with
86
+ // NLM_CLASSIFIER=ollama if you need offline-only operation.
87
+ const provider = (process.env["NLM_CLASSIFIER"] ?? "deepseek").toLowerCase();
88
+ if (provider !== "ollama")
89
+ autoloadEnv();
90
+ const model = process.env["NLM_CLASSIFIER_MODEL"]
91
+ ?? (provider === "ollama" ? "phi4-mini:latest" : "deepseek-v4-flash");
92
+ return new ClassifierBox({ provider, model, ollamaUrl: ollamaUrl() });
93
+ }
94
+ function buildAdapters(sources) {
95
+ // Sources table is the source of truth. Each enabled row maps to one
96
+ // adapter via adapterFromSource(). Detection still gates registration —
97
+ // a row pointing at a missing dir won't poll. NLM_ADAPTERS keeps working
98
+ // as a name-based filter for forcing a subset during dev.
99
+ const explicit = process.env["NLM_ADAPTERS"];
100
+ const allowed = explicit ? new Set(explicit.split(",").map((s) => s.trim())) : null;
101
+ const out = [];
102
+ for (const row of sources.list()) {
103
+ if (!row.enabled)
104
+ continue;
105
+ const adapter = adapterFromSource(row);
106
+ if (!adapter)
107
+ continue;
108
+ if (allowed && !allowed.has(adapter.name))
109
+ continue;
110
+ if (!adapter.detect().enabled)
111
+ continue;
112
+ out.push(adapter);
113
+ }
114
+ return out;
115
+ }
116
+ function buildStack() {
117
+ // Load .env before any registry seeds so secrets carried in env vars
118
+ // (DEEPSEEK_API_KEY today; OPENAI_API_KEY etc. tomorrow) bridge into
119
+ // the providers table on first boot under launchd.
120
+ autoloadEnv();
121
+ // A restore staged via POST /api/data/restore is promoted here, before
122
+ // the store opens — the daemon can't swap a DB file it already holds.
123
+ const restored = applyPendingRestore(dbPath());
124
+ if (restored.applied) {
125
+ console.error(`nlm-memory: restored database from staged backup`);
126
+ if (restored.archivedTo)
127
+ console.error(` previous db archived at ${restored.archivedTo}`);
128
+ }
129
+ const store = new SqliteSessionStore({
130
+ dbPath: dbPath(),
131
+ migrationsDir: MIGRATIONS_DIR,
132
+ });
133
+ // FactStore shares the SessionStore's connection so session+facts ingest
134
+ // can commit in one transaction. Phase B.1 wires it in; no callers yet.
135
+ const facts = new SqliteFactStore(store.rawDb());
136
+ const sources = new SourceRegistry(store.rawDb());
137
+ sources.seedDefaults();
138
+ const providers = new ProviderRegistry(store.rawDb());
139
+ providers.seedDefaults();
140
+ // Recall only uses embed(). Embeddings live on Ollama; DeepSeek doesn't
141
+ // expose them. Classifier is wired separately for Phase D ingest.
142
+ const embedder = new OllamaClient({ baseUrl: ollamaUrl() });
143
+ const classifier = buildClassifier();
144
+ const recall = new RecallService({ store, llm: embedder });
145
+ const factRecall = new FactRecallService({ factStore: facts, llm: embedder });
146
+ return { store, facts, sources, providers, recall, factRecall, embedder, classifier };
147
+ }
148
+ const program = new Command();
149
+ program
150
+ .name("nlm")
151
+ .description("Local-first memory operating system for AI operators")
152
+ .version("0.3.0");
153
+ program
154
+ .command("start")
155
+ .description("Boot the HTTP server + ingest scheduler")
156
+ .option("--no-scheduler", "HTTP only; skip the ingest tick loop")
157
+ .option("--interval-min <n>", "scheduler tick interval (min, default 30)", (v) => Number.parseInt(v, 10), 30)
158
+ .action(async (opts) => {
159
+ const { store, facts, sources, providers, recall, factRecall, embedder, classifier } = buildStack();
160
+ const { existsSync } = await import("node:fs");
161
+ const hasMcpToken = Boolean(process.env["NLM_MCP_TOKEN"]);
162
+ const app = createApp({
163
+ recall,
164
+ store,
165
+ liveStore: store,
166
+ factRecall,
167
+ factStore: facts,
168
+ dbPath: dbPath(),
169
+ classifier,
170
+ sources,
171
+ providers,
172
+ ingest: { classifier, embedder, store, factStore: facts },
173
+ embedderInfo: { provider: "ollama", model: "nomic-embed-text", dims: 768 },
174
+ ...(existsSync(UI_DIST) ? { uiDist: UI_DIST } : {}),
175
+ // Wire POST /mcp only when NLM_MCP_TOKEN is present. Absent = route never
176
+ // mounts, zero attack surface. Present = token-gated Streamable-HTTP MCP
177
+ // endpoint for container agents (e.g. Hermes WebUI).
178
+ ...(hasMcpToken
179
+ ? { mcpDeps: { recall, store, factRecall, factStore: facts } }
180
+ : {}),
181
+ });
182
+ const p = port();
183
+ serve({ fetch: app.fetch, port: p }, (info) => {
184
+ console.error(`nlm-memory http listening on http://localhost:${info.port}`);
185
+ if (hasMcpToken) {
186
+ console.error(` mcp: http://localhost:${info.port}/mcp (token-gated)`);
187
+ }
188
+ console.error(` db: ${dbPath()}`);
189
+ console.error(` ollama: ${ollamaUrl()}`);
190
+ });
191
+ // Keep the SQLite WAL bounded. WAL mode is on but nothing else
192
+ // checkpoints it; under continuous readers it grows without limit
193
+ // (it had reached 38 MB), which slows every read. Drain once at boot,
194
+ // then every 5 minutes.
195
+ const WAL_CHECKPOINT_INTERVAL_MS = 5 * 60_000;
196
+ try {
197
+ store.checkpoint();
198
+ }
199
+ catch {
200
+ // Boot checkpoint can lose a race with readers — the interval retries.
201
+ }
202
+ const checkpointTimer = setInterval(() => {
203
+ try {
204
+ store.checkpoint();
205
+ }
206
+ catch {
207
+ // Checkpoint contention — the next tick retries.
208
+ }
209
+ }, WAL_CHECKPOINT_INTERVAL_MS);
210
+ checkpointTimer.unref();
211
+ // Memo sweep runs independently of the transcript scheduler — it's the
212
+ // backstop for SessionEnd hook unreliability (crashes, kill -9, IDE
213
+ // force-close don't fire SessionEnd, so memo files would otherwise
214
+ // accumulate forever). Always on, even when --no-scheduler.
215
+ const memoSweep = new MemoSweepScheduler();
216
+ memoSweep.start();
217
+ console.error(" memo sweep: dormant cleanup every 5m (threshold 24h)");
218
+ if (opts.scheduler !== false) {
219
+ const adapters = buildAdapters(sources);
220
+ if (adapters.length === 0) {
221
+ console.error(" scheduler: no adapters detected (set NLM_ADAPTERS to force-enable)");
222
+ }
223
+ else {
224
+ const scheduler = new ScanScheduler({
225
+ store,
226
+ adapters,
227
+ classifier,
228
+ embedder,
229
+ factStore: facts,
230
+ intervalMs: opts.intervalMin * 60_000,
231
+ });
232
+ scheduler.start();
233
+ console.error(` scheduler: ${adapters.map((a) => a.name).join(", ")} every ${opts.intervalMin}m`);
234
+ const shutdown = () => {
235
+ clearInterval(checkpointTimer);
236
+ scheduler.stop();
237
+ memoSweep.stop();
238
+ store.close();
239
+ process.exit(0);
240
+ };
241
+ process.on("SIGINT", shutdown);
242
+ process.on("SIGTERM", shutdown);
243
+ }
244
+ }
245
+ });
246
+ program
247
+ .command("migrate")
248
+ .description("Run pending migrations against the canonical SQLite")
249
+ .action(() => {
250
+ // SqliteSessionStore's constructor loads sqlite-vec and runs migrations.
251
+ // Opening + closing is the whole operation.
252
+ const store = new SqliteSessionStore({
253
+ dbPath: dbPath(),
254
+ migrationsDir: MIGRATIONS_DIR,
255
+ });
256
+ store.close();
257
+ console.error(`nlm-memory: migrations applied at ${dbPath()}`);
258
+ });
259
+ program
260
+ .command("recall")
261
+ .description("One-shot recall query (for shell debugging)")
262
+ .argument("<query>", "search query")
263
+ .option("-e, --entity <name>", "filter by entity")
264
+ .option("-k, --kind <kind>", "filter by marker kind (decision|open)")
265
+ .option("-m, --mode <mode>", "keyword|semantic|hybrid", "keyword")
266
+ .option("-l, --limit <n>", "max results", (v) => Number.parseInt(v, 10), 10)
267
+ .action(async (query, opts) => {
268
+ const { store, recall } = buildStack();
269
+ try {
270
+ const result = await recall.search({
271
+ query,
272
+ mode: opts.mode,
273
+ limit: opts.limit,
274
+ ...(opts.entity ? { entity: opts.entity } : {}),
275
+ ...(opts.kind ? { kind: opts.kind } : {}),
276
+ });
277
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
278
+ }
279
+ finally {
280
+ store.close();
281
+ }
282
+ });
283
+ program
284
+ .command("classify-parity")
285
+ .description("Run TS classifier against ~/.nlm/canonical.sqlite and diff vs persisted Python output")
286
+ .option("-l, --limit <n>", "sessions to sample", (v) => Number.parseInt(v, 10), 10)
287
+ .option("-p, --provider <name>", "deepseek | ollama", "deepseek")
288
+ .option("-m, --model <name>", "model tag (default: deepseek-v4-flash for deepseek, phi4-mini:latest for ollama)")
289
+ .option("-v, --verbose", "per-session diff lines on stderr")
290
+ .action(async (opts) => {
291
+ const provider = opts.provider === "ollama" ? "ollama" : "deepseek";
292
+ const defaultModel = provider === "deepseek" ? "deepseek-v4-flash" : "phi4-mini:latest";
293
+ const report = await runParity({
294
+ limit: opts.limit,
295
+ dbPath: dbPath(),
296
+ ollamaUrl: ollamaUrl(),
297
+ classifyModel: opts.model ?? defaultModel,
298
+ provider,
299
+ verbose: Boolean(opts.verbose),
300
+ });
301
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
302
+ });
303
+ program
304
+ .command("embed-backfill")
305
+ .description("Re-embed every session into session_embedding_chunks (chunk + max-pool)")
306
+ .option("-l, --limit <n>", "session cap (default: all)", (v) => Number.parseInt(v, 10))
307
+ .option("--state <path>", "resume state file (default ~/.nlm/embed_reembed.state)")
308
+ .option("-v, --verbose", "per-session progress on stderr")
309
+ .action(async (opts) => {
310
+ const embedder = new OllamaClient({ baseUrl: ollamaUrl() });
311
+ const report = await reembedCorpus({
312
+ dbPath: dbPath(),
313
+ embedder,
314
+ ...(opts.state ? { statePath: opts.state } : {}),
315
+ ...(opts.limit ? { limit: opts.limit } : {}),
316
+ ...(opts.verbose
317
+ ? {
318
+ onProgress: (i, n, sid, status) => {
319
+ process.stderr.write(` [${i}/${n}] ${sid} ${status}\n`);
320
+ },
321
+ }
322
+ : {}),
323
+ });
324
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
325
+ });
326
+ program
327
+ .command("backfill-facts")
328
+ .description("One-shot: classify historical sessions and populate the FactStore (Phase B.5)")
329
+ .option("-l, --limit <n>", "max sessions to process this run", (v) => Number.parseInt(v, 10))
330
+ .option("--from <session-id>", "skip sessions with id <= this value (operator-resume)")
331
+ .option("--state <path>", "resume state file (default ~/.nlm/backfill_facts.state)")
332
+ .option("--dry-run", "count what would happen without writing facts")
333
+ .option("--reprocess", "re-classify sessions that already have facts")
334
+ .option("--no-embed", "skip per-fact embedding (faster but disables semantic recall)")
335
+ .option("-v, --verbose", "per-session progress on stderr")
336
+ .action(async (opts) => {
337
+ const { store, facts, embedder, classifier } = buildStack();
338
+ try {
339
+ const report = await backfillFacts({
340
+ store,
341
+ factStore: facts,
342
+ classifier,
343
+ embedder: opts.embed === false ? null : embedder,
344
+ ...(opts.state ? { statePath: opts.state } : {}),
345
+ ...(opts.limit ? { limit: opts.limit } : {}),
346
+ ...(opts.from ? { from: opts.from } : {}),
347
+ dryRun: Boolean(opts.dryRun),
348
+ reprocess: Boolean(opts.reprocess),
349
+ ...(opts.verbose
350
+ ? {
351
+ onProgress: (i, n, sid, status, detail) => {
352
+ const tail = detail ? ` ${detail}` : "";
353
+ process.stderr.write(` [${i}/${n}] ${sid} ${status}${tail}\n`);
354
+ },
355
+ }
356
+ : {}),
357
+ });
358
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
359
+ }
360
+ finally {
361
+ store.close();
362
+ }
363
+ });
364
+ program
365
+ .command("embed-normalize")
366
+ .description("L2-normalize every row in session_embeddings (idempotent)")
367
+ .option("--dim <n>", "vector dimension (default 768)", (v) => Number.parseInt(v, 10), 768)
368
+ .option("--batch <n>", "rows per commit batch (default 100)", (v) => Number.parseInt(v, 10), 100)
369
+ .option("--dry-run", "report what would change without writing")
370
+ .action((opts) => {
371
+ const report = normalizeEmbeddings({
372
+ dbPath: dbPath(),
373
+ dim: opts.dim,
374
+ batchSize: opts.batch,
375
+ dryRun: Boolean(opts.dryRun),
376
+ });
377
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
378
+ });
379
+ program
380
+ .command("mcp")
381
+ .description("Run as an MCP stdio server (for ~/.mcp.json)")
382
+ .action(async () => {
383
+ const { recall, store, facts, factRecall } = buildStack();
384
+ const server = createMcpServer({ recall, store, factStore: facts, factRecall });
385
+ const transport = new StdioServerTransport();
386
+ await server.connect(transport);
387
+ });
388
+ const LAUNCH_AGENT_LABEL = "com.github.pbmagnet4.nlm-memory";
389
+ const LAUNCH_AGENT_PLIST = join(homedir(), "Library", "LaunchAgents", `${LAUNCH_AGENT_LABEL}.plist`);
390
+ function buildPlist(nodeExec, nlmJs) {
391
+ const logDir = join(homedir(), ".nlm", "logs");
392
+ return `<?xml version="1.0" encoding="UTF-8"?>
393
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
394
+ <plist version="1.0">
395
+ <dict>
396
+ <key>Label</key>
397
+ <string>${LAUNCH_AGENT_LABEL}</string>
398
+ <key>ProgramArguments</key>
399
+ <array>
400
+ <string>${nodeExec}</string>
401
+ <string>${nlmJs}</string>
402
+ <string>start</string>
403
+ </array>
404
+ <key>WorkingDirectory</key>
405
+ <string>${homedir()}</string>
406
+ <key>RunAtLoad</key>
407
+ <true/>
408
+ <key>KeepAlive</key>
409
+ <dict>
410
+ <key>Crashed</key>
411
+ <true/>
412
+ <key>SuccessfulExit</key>
413
+ <false/>
414
+ </dict>
415
+ <key>ThrottleInterval</key>
416
+ <integer>10</integer>
417
+ <key>StandardOutPath</key>
418
+ <string>${logDir}/daemon-out.log</string>
419
+ <key>StandardErrorPath</key>
420
+ <string>${logDir}/daemon-err.log</string>
421
+ </dict>
422
+ </plist>
423
+ `;
424
+ }
425
+ program
426
+ .command("install")
427
+ .description("Install the macOS LaunchAgent so nlm-memory auto-starts on login")
428
+ .action(() => {
429
+ if (process.platform !== "darwin") {
430
+ console.error("nlm install: only macOS is supported. On Linux, add `nlm start` to your init system manually.");
431
+ process.exit(1);
432
+ }
433
+ const uid = process.getuid?.();
434
+ if (uid === undefined) {
435
+ console.error("nlm install: could not determine UID");
436
+ process.exit(1);
437
+ }
438
+ mkdirSync(join(homedir(), ".nlm", "logs"), { recursive: true });
439
+ writeFileSync(LAUNCH_AGENT_PLIST, buildPlist(process.execPath, __filename), "utf8");
440
+ console.error(`nlm: wrote ${LAUNCH_AGENT_PLIST}`);
441
+ try {
442
+ execFileSync("launchctl", ["bootout", `gui/${uid}`, LAUNCH_AGENT_LABEL], { stdio: "ignore" });
443
+ }
444
+ catch {
445
+ // not loaded yet — expected on first install
446
+ }
447
+ execFileSync("launchctl", ["bootstrap", `gui/${uid}`, LAUNCH_AGENT_PLIST]);
448
+ console.error("nlm: daemon installed and started.");
449
+ console.error(` UI: http://localhost:${port()}/ui`);
450
+ console.error(` To stop: launchctl stop ${LAUNCH_AGENT_LABEL}`);
451
+ console.error(" To remove: nlm uninstall");
452
+ });
453
+ program
454
+ .command("uninstall")
455
+ .description("Remove the macOS LaunchAgent")
456
+ .action(() => {
457
+ if (process.platform !== "darwin") {
458
+ console.error("nlm uninstall: only macOS is supported.");
459
+ process.exit(1);
460
+ }
461
+ const uid = process.getuid?.();
462
+ if (uid === undefined) {
463
+ console.error("nlm uninstall: could not determine UID");
464
+ process.exit(1);
465
+ }
466
+ let bootoutFailed = false;
467
+ let bootoutStderr = "";
468
+ try {
469
+ execFileSync("launchctl", ["bootout", `gui/${uid}`, LAUNCH_AGENT_LABEL], { stdio: "pipe" });
470
+ console.error("nlm: daemon stopped.");
471
+ }
472
+ catch (e) {
473
+ const err = e;
474
+ bootoutStderr = err.stderr ? err.stderr.toString() : "";
475
+ if (isBenignBootoutError(bootoutStderr)) {
476
+ // Agent wasn't loaded — fine, proceed to plist cleanup.
477
+ }
478
+ else {
479
+ bootoutFailed = true;
480
+ }
481
+ }
482
+ // Source of truth: did launchd actually unload the agent? Same shape
483
+ // of bug as #161 — silent partial success is worse than loud failure.
484
+ if (isAgentLoaded(LAUNCH_AGENT_LABEL)) {
485
+ console.error("nlm: uninstall FAILED — agent is still loaded after bootout.");
486
+ if (bootoutStderr.trim()) {
487
+ console.error(` launchctl stderr: ${bootoutStderr.trim()}`);
488
+ }
489
+ console.error(" Recovery options:");
490
+ console.error(` 1. launchctl bootout gui/${uid}/${LAUNCH_AGENT_LABEL}`);
491
+ console.error(" 2. If a stale process is holding the port, find it:");
492
+ console.error(" ps aux | grep 'nlm.js start' | grep -v grep");
493
+ console.error(" Then: kill <pid> (or kill -9 <pid> if it ignores TERM)");
494
+ console.error(" Plist NOT removed — re-run `nlm uninstall` after the agent is gone.");
495
+ process.exit(1);
496
+ }
497
+ if (bootoutFailed) {
498
+ // launchctl errored AND the agent isn't loaded — odd but recoverable.
499
+ // Flag it so the user knows something off-script happened.
500
+ console.error(`nlm: bootout reported an error but agent is unloaded: ${bootoutStderr.trim()}`);
501
+ }
502
+ if (existsSync(LAUNCH_AGENT_PLIST)) {
503
+ rmSync(LAUNCH_AGENT_PLIST);
504
+ console.error(`nlm: removed ${LAUNCH_AGENT_PLIST}`);
505
+ }
506
+ console.error("nlm: uninstalled. Run `nlm install` to reinstall.");
507
+ });
508
+ const HOOK_JS = resolve(__dirname, "../hook/prompt-recall-hook.js");
509
+ const SESSION_START_HOOK_JS = resolve(__dirname, "../hook/session-start-hook.js");
510
+ const SESSION_END_HOOK_JS = resolve(__dirname, "../hook/session-end-hook.js");
511
+ const STOP_HOOK_JS = resolve(__dirname, "../hook/stop-hook.js");
512
+ const PRE_COMPACT_HOOK_JS = resolve(__dirname, "../hook/pre-compact-hook.js");
513
+ const SUBAGENT_START_HOOK_JS = resolve(__dirname, "../hook/subagent-start-hook.js");
514
+ const ALL_HOOKS = [
515
+ { event: "UserPromptSubmit", script: HOOK_JS, label: "recall" },
516
+ { event: "SessionStart", script: SESSION_START_HOOK_JS, label: "session-start" },
517
+ { event: "SessionEnd", script: SESSION_END_HOOK_JS, label: "session-end" },
518
+ { event: "Stop", script: STOP_HOOK_JS, label: "stop" },
519
+ { event: "PreCompact", script: PRE_COMPACT_HOOK_JS, label: "pre-compact" },
520
+ { event: "SubagentStart", script: SUBAGENT_START_HOOK_JS, label: "subagent-start" },
521
+ ];
522
+ function claudeSettingsPath() {
523
+ return process.env["NLM_CLAUDE_SETTINGS"] ?? join(homedir(), ".claude", "settings.json");
524
+ }
525
+ const hook = program
526
+ .command("hook")
527
+ .description("Manage the Claude Code NLM hooks");
528
+ hook
529
+ .command("install")
530
+ .description("Add the NLM hooks (recall + session-end + stop) to ~/.claude/settings.json (shadow mode)")
531
+ .action(() => {
532
+ const path = claudeSettingsPath();
533
+ const hookLogPath = process.env["NLM_HOOK_LOG"] ?? join(homedir(), ".nlm", "hook-log.jsonl");
534
+ // Install + smoke each hook. If any fails, revert all and exit nonzero.
535
+ // Atomic install matters because partial state ("recall installed but
536
+ // session-end didn't smoke-test") is worse than no install — silent
537
+ // partial failure is the bug class we shipped #161 to prevent.
538
+ const installed = [];
539
+ for (const spec of ALL_HOOKS) {
540
+ const command = buildHookCommand(process.execPath, spec.script, "shadow");
541
+ addHook(path, command, spec.event);
542
+ const smoke = smokeTestHookCommand(command, hookLogPath);
543
+ if (!smoke.ok) {
544
+ // Revert every hook we installed this run (including the failing one).
545
+ for (const prior of [...installed, spec]) {
546
+ removeHook(path, prior.event);
547
+ }
548
+ console.error(`nlm: ${spec.label} hook (${spec.event}) FAILED smoke test — all NLM hooks reverted.`);
549
+ console.error(` reason: ${smoke.reason}`);
550
+ if (smoke.stderr) {
551
+ const trimmed = smoke.stderr.trim();
552
+ if (trimmed)
553
+ console.error(` stderr: ${trimmed}`);
554
+ }
555
+ console.error(` command was: ${command}`);
556
+ process.exit(1);
557
+ }
558
+ installed.push(spec);
559
+ }
560
+ console.error(`nlm: NLM hooks installed in ${path} (shadow mode):`);
561
+ for (const spec of installed) {
562
+ console.error(` - ${spec.event} → ${spec.label}-hook`);
563
+ }
564
+ console.error(" Smoke tests passed — all hooks appended synthetic entries to hook-log.jsonl.");
565
+ console.error(" Recall hooks log to ~/.nlm/hook-log.jsonl and inject nothing in shadow mode.");
566
+ console.error(" Session-end hook cleans up ~/.nlm/hook-state/<session>.json on session close.");
567
+ console.error(" To go live later: change NLM_HOOK_MODE=shadow to live for the recall hook.");
568
+ console.error(" To remove: nlm hook uninstall");
569
+ });
570
+ hook
571
+ .command("uninstall")
572
+ .description("Remove all NLM hooks from ~/.claude/settings.json")
573
+ .action(() => {
574
+ const path = claudeSettingsPath();
575
+ removeHook(path, "*");
576
+ console.error(`nlm: all NLM hooks removed from ${path}.`);
577
+ });
578
+ // Repo root resolves to <pkg>/dist/cli/nlm.js → <pkg>/. The plugin tree is
579
+ // shipped alongside dist/ so plugin/scripts/ is reachable from both local
580
+ // dev and the globally-installed package.
581
+ const REPO_ROOT = resolve(__dirname, "../..");
582
+ const connect = program
583
+ .command("connect")
584
+ .description("Connect nlm-memory to an AI coding runtime");
585
+ connect
586
+ .command("codex")
587
+ .description("Install nlm-memory as a Codex CLI plugin (marketplace + plugin add)")
588
+ .option("--source <source>", "marketplace source (owner/repo, git URL, or local path)", "pbmagnet4/nlm-memory-ts")
589
+ .option("--local", "shortcut for --source <repo-root>; use during dev")
590
+ .option("--with-hooks", "additionally write absolute paths to ~/.codex/hooks.json (Codex Desktop fallback for openai/codex#16430)")
591
+ .option("--dry-run", "print what would happen without invoking codex")
592
+ .action((opts) => {
593
+ if (!opts.dryRun && !codexBinaryAvailable()) {
594
+ console.error("nlm connect codex: `codex` binary not on PATH. Install via `npm i -g @openai/codex` or `brew install codex`.");
595
+ process.exit(1);
596
+ }
597
+ const source = opts.local ? REPO_ROOT : opts.source;
598
+ const report = connectCodex({ source, withHooks: Boolean(opts.withHooks), dryRun: Boolean(opts.dryRun) }, pluginScriptsDir(REPO_ROOT));
599
+ if (report.dryRun) {
600
+ console.error("nlm connect codex (dry run):");
601
+ console.error(` codex plugin marketplace add ${report.source}`);
602
+ console.error(` codex plugin add ${report.pluginName}@${report.marketplaceName}`);
603
+ console.error(` write [mcp_servers.nlm-memory] block to ${report.mcpServerWritten}`);
604
+ if (report.legacyHooksWritten) {
605
+ console.error(` write legacy fallback to ${report.legacyHooksWritten}`);
606
+ }
607
+ return;
608
+ }
609
+ if (report.marketplaceAdd && report.marketplaceAdd.status !== 0) {
610
+ const stderr = report.marketplaceAdd.stderr.trim();
611
+ console.error(`nlm connect codex: marketplace add failed (exit ${report.marketplaceAdd.status}).`);
612
+ if (stderr)
613
+ console.error(` codex stderr: ${stderr}`);
614
+ process.exit(1);
615
+ }
616
+ if (report.pluginAdd && report.pluginAdd.status !== 0) {
617
+ const stderr = report.pluginAdd.stderr.trim();
618
+ console.error(`nlm connect codex: plugin add failed (exit ${report.pluginAdd.status}).`);
619
+ if (stderr)
620
+ console.error(` codex stderr: ${stderr}`);
621
+ process.exit(1);
622
+ }
623
+ console.error(`nlm: connected to Codex via marketplace ${report.marketplaceName}, plugin ${report.pluginName}.`);
624
+ if (report.mcpServerWritten) {
625
+ console.error(` Wrote [mcp_servers.nlm-memory] to ${report.mcpServerWritten}`);
626
+ }
627
+ if (report.legacyHooksWritten) {
628
+ console.error(` Wrote hooks.json fallback to ${report.legacyHooksWritten}`);
629
+ }
630
+ console.error(" Next: run `codex` interactively and approve the hook trust prompts. Then prompt — recall should fire.");
631
+ });
632
+ connect
633
+ .command("claude-code")
634
+ .description("Write the nlm-memory MCP server block into ~/.mcp.json")
635
+ .option("--with-hooks", "also install Claude Code session hooks")
636
+ .option("--dry-run", "print what would happen without changing files")
637
+ .action((opts) => {
638
+ if (opts.dryRun) {
639
+ console.error("nlm connect claude-code (dry run):");
640
+ console.error(` write [mcpServers.nlm-memory] to ${join(homedir(), ".mcp.json")}`);
641
+ if (opts.withHooks)
642
+ console.error(" install 6 Claude Code hooks");
643
+ return;
644
+ }
645
+ const report = connectClaudeCode({ nlmBinPath: __filename, nodeExecPath: process.execPath });
646
+ const action = report.alreadyPresent ? "updated" : "written";
647
+ console.error(`nlm: [mcpServers.nlm-memory] ${action} → ${report.mcpConfigPath}`);
648
+ console.error(" Restart Claude Code to activate the MCP server.");
649
+ if (opts.withHooks) {
650
+ const path = claudeSettingsPath();
651
+ const hookLogPath = process.env["NLM_HOOK_LOG"] ?? join(homedir(), ".nlm", "hook-log.jsonl");
652
+ const result = installClaudeCodeHooks({
653
+ nodeExecPath: process.execPath,
654
+ hooks: ALL_HOOKS,
655
+ settingsPath: path,
656
+ hookLogPath,
657
+ addHook,
658
+ removeHook,
659
+ buildHookCommand,
660
+ smokeTestHookCommand,
661
+ });
662
+ if (!result.ok) {
663
+ console.error(`nlm: ${result.failedLabel ?? "hook"} smoke test failed — all hooks reverted. Run \`nlm hook install\` manually.`);
664
+ process.exit(1);
665
+ }
666
+ console.error(`nlm: ${result.count} hooks installed → ${path}`);
667
+ }
668
+ });
669
+ connect
670
+ .command("hermes")
671
+ .description("Write the nlm-memory MCP server entry into ~/.hermes/config.yaml")
672
+ .option("--dry-run", "print what would happen without changing files")
673
+ .action((opts) => {
674
+ if (opts.dryRun) {
675
+ console.error(`nlm connect hermes (dry run): write [mcp_servers.nlm-memory] to ${hermesConfigPath()}`);
676
+ return;
677
+ }
678
+ const report = connectHermes({ nlmBinPath: __filename, nodeExecPath: process.execPath, dryRun: false });
679
+ const action = report.alreadyPresent ? "updated" : "written";
680
+ console.error(`nlm: [mcp_servers.nlm-memory] ${action} → ${report.configPath}`);
681
+ console.error(" Restart Hermes to activate the MCP server.");
682
+ });
683
+ connect
684
+ .command("hermes-agent")
685
+ .description("Install the nlm-memory plugin into NousResearch Hermes Agent (~/.hermes/plugins/nlm-memory/)")
686
+ .option("--dry-run", "print what would happen without changing files")
687
+ .action((opts) => {
688
+ const pluginSrcDir = join(REPO_ROOT, "plugin-hermes-agent");
689
+ if (opts.dryRun) {
690
+ console.error(`nlm connect hermes-agent (dry run): copy ${pluginSrcDir} → ${hermesAgentPluginDir()}`);
691
+ console.error(" then: hermes plugins enable nlm-memory");
692
+ return;
693
+ }
694
+ const report = connectHermesAgent({ pluginSrcDir, dryRun: false });
695
+ const action = report.alreadyPresent ? "updated" : "installed";
696
+ console.error(`nlm: nlm-memory plugin ${action} → ${report.destDir}`);
697
+ if (report.enabledViaCli) {
698
+ console.error(" Enabled via: hermes plugins enable nlm-memory");
699
+ }
700
+ else {
701
+ console.error(" Run: hermes plugins enable nlm-memory (if hermes binary is on PATH)");
702
+ }
703
+ console.error(" Also run: nlm connect hermes (to wire the MCP server)");
704
+ });
705
+ const disconnect = program
706
+ .command("disconnect")
707
+ .description("Disconnect nlm-memory from an AI coding runtime");
708
+ disconnect
709
+ .command("codex")
710
+ .description("Remove the nlm-memory plugin + marketplace from Codex")
711
+ .option("--with-hooks", "also strip our entries from ~/.codex/hooks.json")
712
+ .option("--dry-run", "print what would happen without invoking codex")
713
+ .action((opts) => {
714
+ if (!opts.dryRun && !codexBinaryAvailable()) {
715
+ console.error("nlm disconnect codex: `codex` binary not on PATH.");
716
+ process.exit(1);
717
+ }
718
+ const report = disconnectCodex({
719
+ withHooks: Boolean(opts.withHooks),
720
+ dryRun: Boolean(opts.dryRun),
721
+ });
722
+ if (report.dryRun) {
723
+ console.error("nlm disconnect codex (dry run):");
724
+ console.error(` codex plugin remove ${report.pluginName}@${report.marketplaceName}`);
725
+ console.error(` codex plugin marketplace remove ${report.marketplaceName}`);
726
+ console.error(" strip [mcp_servers.nlm-memory] block from ~/.codex/config.toml");
727
+ if (opts.withHooks)
728
+ console.error(" strip our entries from ~/.codex/hooks.json");
729
+ return;
730
+ }
731
+ // Best-effort removal — non-zero exits from codex are reported but
732
+ // don't abort, because partial cleanup (plugin removed, marketplace
733
+ // already gone) is the common case for repeat invocations.
734
+ const pluginStderr = (report.pluginRemove?.stderr ?? "").trim();
735
+ const marketStderr = (report.marketplaceRemove?.stderr ?? "").trim();
736
+ if (report.pluginRemove?.status !== 0 && pluginStderr) {
737
+ console.error(` plugin remove: ${pluginStderr}`);
738
+ }
739
+ if (report.marketplaceRemove?.status !== 0 && marketStderr) {
740
+ console.error(` marketplace remove: ${marketStderr}`);
741
+ }
742
+ console.error("nlm: disconnected from Codex.");
743
+ console.error(report.mcpServerRemoved
744
+ ? " Stripped [mcp_servers.nlm-memory] block from ~/.codex/config.toml"
745
+ : " No [mcp_servers.nlm-memory] block to remove from ~/.codex/config.toml");
746
+ if (opts.withHooks) {
747
+ console.error(report.legacyHooksRemoved
748
+ ? " Stripped our entries from ~/.codex/hooks.json"
749
+ : " No legacy hooks to remove from ~/.codex/hooks.json");
750
+ }
751
+ });
752
+ disconnect
753
+ .command("claude-code")
754
+ .description("Remove the nlm-memory MCP server block from ~/.mcp.json")
755
+ .option("--dry-run", "print what would happen without changing files")
756
+ .action((opts) => {
757
+ const report = disconnectClaudeCode({ dryRun: Boolean(opts.dryRun) });
758
+ if (opts.dryRun) {
759
+ console.error(`nlm disconnect claude-code (dry run): strip [mcpServers.nlm-memory] from ${report.mcpConfigPath}`);
760
+ return;
761
+ }
762
+ console.error(report.removed
763
+ ? `nlm: removed [mcpServers.nlm-memory] from ${report.mcpConfigPath}`
764
+ : `nlm: no [mcpServers.nlm-memory] entry found in ${report.mcpConfigPath}`);
765
+ });
766
+ disconnect
767
+ .command("hermes")
768
+ .description("Remove the nlm-memory MCP server entry from ~/.hermes/config.yaml")
769
+ .option("--dry-run", "print what would happen without changing files")
770
+ .action((opts) => {
771
+ const report = disconnectHermes({ dryRun: Boolean(opts.dryRun) });
772
+ if (opts.dryRun) {
773
+ console.error(`nlm disconnect hermes (dry run): strip [mcp_servers.nlm-memory] from ${report.configPath}`);
774
+ return;
775
+ }
776
+ console.error(report.removed
777
+ ? `nlm: removed [mcp_servers.nlm-memory] from ${report.configPath}`
778
+ : `nlm: no [mcp_servers.nlm-memory] entry found in ${report.configPath}`);
779
+ });
780
+ disconnect
781
+ .command("hermes-agent")
782
+ .description("Remove the nlm-memory plugin from ~/.hermes/plugins/nlm-memory/")
783
+ .option("--dry-run", "print what would happen without changing files")
784
+ .action((opts) => {
785
+ const report = disconnectHermesAgent({ dryRun: Boolean(opts.dryRun) });
786
+ if (opts.dryRun) {
787
+ console.error(`nlm disconnect hermes-agent (dry run): remove ${hermesAgentPluginDir()}`);
788
+ return;
789
+ }
790
+ console.error(report.removed
791
+ ? `nlm: removed plugin directory ${report.destDir}`
792
+ : `nlm: no plugin directory found at ${report.destDir}`);
793
+ });
794
+ program
795
+ .command("setup")
796
+ .description("Interactive first-run setup: detect runtimes, wire MCP + hooks, start daemon")
797
+ .action(async () => {
798
+ await runSetup({
799
+ nlmBinPath: __filename,
800
+ nodeExecPath: process.execPath,
801
+ migrationsDir: MIGRATIONS_DIR,
802
+ repoRoot: REPO_ROOT,
803
+ dbPath: dbPath(),
804
+ launchAgentLabel: LAUNCH_AGENT_LABEL,
805
+ launchAgentPlist: LAUNCH_AGENT_PLIST,
806
+ buildPlist,
807
+ claudeSettingsPath: claudeSettingsPath(),
808
+ allHooks: ALL_HOOKS,
809
+ addHook,
810
+ removeHook,
811
+ buildHookCommand,
812
+ smokeTestHookCommand,
813
+ });
814
+ });
815
+ program
816
+ .command("useful-scan")
817
+ .description("Scan hook log for useful recall hits; writes to ~/.nlm/useful-hit-log.jsonl")
818
+ .option("-d, --days <n>", "rolling window in days", (v) => Number.parseInt(v, 10), 1)
819
+ .option("--dry-run", "compute without writing to disk")
820
+ .action(async (opts) => {
821
+ const result = await scanUsefulHits({ days: opts.days, ...(opts.dryRun ? { dryRun: true } : {}) });
822
+ const rate = result.measurable === 0
823
+ ? "no measurable entries"
824
+ : `${result.useful}/${result.measurable} useful (${Math.round((result.useful / result.measurable) * 100)}%)`;
825
+ console.error(`nlm useful-scan: scanned ${result.total} recalls in the last ${opts.days}d — ${rate}` +
826
+ (opts.dryRun ? " (dry-run)" : `, ${result.appended} appended`));
827
+ });
828
+ program.parseAsync().catch((e) => {
829
+ console.error("nlm: fatal", e);
830
+ process.exit(1);
831
+ });
832
+ //# sourceMappingURL=nlm.js.map