nlm-memory 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/README.md +72 -34
  2. package/dist/cli/nlm.js +223 -33
  3. package/dist/cli/nlm.js.map +1 -1
  4. package/dist/core/adapters/cursor.d.ts +45 -0
  5. package/dist/core/adapters/cursor.js +397 -0
  6. package/dist/core/adapters/cursor.js.map +1 -0
  7. package/dist/core/adapters/from-source.js +10 -0
  8. package/dist/core/adapters/from-source.js.map +1 -1
  9. package/dist/core/adapters/windsurf.d.ts +44 -0
  10. package/dist/core/adapters/windsurf.js +299 -0
  11. package/dist/core/adapters/windsurf.js.map +1 -0
  12. package/dist/core/hook/claude-settings.d.ts +12 -5
  13. package/dist/core/hook/claude-settings.js +21 -6
  14. package/dist/core/hook/claude-settings.js.map +1 -1
  15. package/dist/core/sources/source-registry.d.ts +1 -1
  16. package/dist/core/sources/source-registry.js +18 -0
  17. package/dist/core/sources/source-registry.js.map +1 -1
  18. package/dist/core/storage/sqlite-session-store.d.ts +2 -0
  19. package/dist/core/storage/sqlite-session-store.js +38 -2
  20. package/dist/core/storage/sqlite-session-store.js.map +1 -1
  21. package/dist/hook/hook-auth.d.ts +13 -0
  22. package/dist/hook/hook-auth.js +19 -0
  23. package/dist/hook/hook-auth.js.map +1 -0
  24. package/dist/hook/prompt-recall-hook.js +7 -1
  25. package/dist/hook/prompt-recall-hook.js.map +1 -1
  26. package/dist/hook/session-start-hook.js +4 -1
  27. package/dist/hook/session-start-hook.js.map +1 -1
  28. package/dist/hook/stop-hook.js +4 -1
  29. package/dist/hook/stop-hook.js.map +1 -1
  30. package/dist/http/app.d.ts +2 -0
  31. package/dist/http/app.js +76 -1
  32. package/dist/http/app.js.map +1 -1
  33. package/dist/install/claude-code.js +1 -1
  34. package/dist/install/claude-code.js.map +1 -1
  35. package/dist/install/cursor.d.ts +25 -0
  36. package/dist/install/cursor.js +43 -0
  37. package/dist/install/cursor.js.map +1 -0
  38. package/dist/install/nlm-dir-perms.d.ts +19 -0
  39. package/dist/install/nlm-dir-perms.js +43 -0
  40. package/dist/install/nlm-dir-perms.js.map +1 -0
  41. package/dist/install/ollama.d.ts +18 -1
  42. package/dist/install/ollama.js +62 -7
  43. package/dist/install/ollama.js.map +1 -1
  44. package/dist/install/setup.d.ts +4 -0
  45. package/dist/install/setup.js +141 -18
  46. package/dist/install/setup.js.map +1 -1
  47. package/dist/install/windsurf.d.ts +25 -0
  48. package/dist/install/windsurf.js +43 -0
  49. package/dist/install/windsurf.js.map +1 -0
  50. package/dist/mcp/server.js +20 -1
  51. package/dist/mcp/server.js.map +1 -1
  52. package/dist/shared/types.d.ts +4 -0
  53. package/dist/ui/assets/{index-BA6IpU8g.css → index-Beo8psd-.css} +1 -1
  54. package/dist/ui/assets/index-CSPTTeeM.js +69 -0
  55. package/dist/ui/index.html +2 -2
  56. package/package.json +26 -1
  57. package/plugin/scripts/prompt-recall-hook.mjs +55 -4
  58. package/plugin/scripts/stop-hook.mjs +57 -6
  59. package/.agents/plugins/marketplace.json +0 -20
  60. package/.github/workflows/ci.yml +0 -30
  61. package/dist/ui/assets/index-B_qIVV0k.js +0 -69
  62. package/docs/methodology/re-derivation-rate.md +0 -112
  63. package/docs/methodology/useful-hit-rate.md +0 -79
  64. package/docs/plans/2026-05-20-fts5-lexical-recall.md +0 -1088
  65. package/docs/plans/2026-05-20-recall-daemon-wedge-fix.md +0 -662
  66. package/docs/plans/2026-05-20-recall-hook-design.md +0 -131
  67. package/docs/plans/2026-05-20-recall-hook-implementation.md +0 -1222
  68. package/docs/plans/desktop-product.md +0 -69
  69. package/docs/plans/factstore-design.md +0 -236
  70. package/logs/CHANGELOG/CHANGELOG-2026.md +0 -1389
  71. package/logs/CHANGELOG/CHANGELOG.md +0 -337
  72. package/migrations/000_initial_schema.sql +0 -174
  73. package/migrations/001_entity_type_rename.sql +0 -17
  74. package/migrations/002_adapter_state_extend.sql +0 -12
  75. package/migrations/003_session_embeddings.sql +0 -11
  76. package/migrations/004_facts.sql +0 -46
  77. package/migrations/005_sources.sql +0 -31
  78. package/migrations/006_providers.sql +0 -33
  79. package/migrations/007_source_tokens.sql +0 -17
  80. package/migrations/008_fts_rebuild.sql +0 -9
  81. package/migrations/009_session_embedding_chunks.sql +0 -46
  82. package/migrations/010_sources_opencode.sql +0 -30
  83. package/migrations/011_sources_hermes_agent.sql +0 -30
  84. package/migrations/012_sources_aider.sql +0 -30
  85. package/migrations/013_adapter_state_failure_count.sql +0 -12
  86. package/plugin-hermes-agent/README.md +0 -49
  87. package/plugin-hermes-agent/__init__.py +0 -75
  88. package/plugin-hermes-agent/plugin.yaml +0 -15
  89. package/scripts/backfill-citations.mjs +0 -0
  90. package/scripts/build-codex-plugin.mjs +0 -61
  91. package/scripts/deepseek-probe.mjs +0 -67
  92. package/scripts/extract-triples.mjs +0 -207
  93. package/scripts/longmemeval/embedding-cache.ts +0 -77
  94. package/scripts/longmemeval/fetch-dataset.sh +0 -25
  95. package/scripts/longmemeval/run-harness.ts +0 -315
  96. package/scripts/longmemeval/scorer.ts +0 -99
  97. package/scripts/longmemeval/tsconfig.json +0 -9
  98. package/scripts/longmemeval/types.ts +0 -35
  99. package/scripts/nlm-daily-digest.py +0 -239
  100. package/scripts/nlm-daily-digest.sh +0 -28
  101. package/src/cli/classify-parity.ts +0 -257
  102. package/src/cli/launchctl-helpers.ts +0 -49
  103. package/src/cli/nlm.ts +0 -885
  104. package/src/core/actions/actions-log.ts +0 -118
  105. package/src/core/actions/overlay.ts +0 -117
  106. package/src/core/adapters/aider.ts +0 -205
  107. package/src/core/adapters/claude-code.ts +0 -293
  108. package/src/core/adapters/common.ts +0 -54
  109. package/src/core/adapters/from-source.ts +0 -57
  110. package/src/core/adapters/hermes-agent.ts +0 -240
  111. package/src/core/adapters/hermes.ts +0 -277
  112. package/src/core/adapters/jsonl-generic.ts +0 -208
  113. package/src/core/adapters/opencode.ts +0 -281
  114. package/src/core/adapters/pi.ts +0 -264
  115. package/src/core/classifier/prompt.ts +0 -200
  116. package/src/core/dataset/build-dataset.ts +0 -463
  117. package/src/core/embedding/chunk-body.ts +0 -76
  118. package/src/core/embedding/embed-backfill.ts +0 -210
  119. package/src/core/embedding/embed-normalize.ts +0 -135
  120. package/src/core/facts/backfill-facts.ts +0 -254
  121. package/src/core/facts/extract-facts.ts +0 -50
  122. package/src/core/hook/citation-detect.ts +0 -124
  123. package/src/core/hook/cite-memo.ts +0 -68
  124. package/src/core/hook/claude-settings.ts +0 -166
  125. package/src/core/hook/gate.ts +0 -25
  126. package/src/core/hook/hook-log.ts +0 -41
  127. package/src/core/hook/memo-sweep.ts +0 -164
  128. package/src/core/hook/memo.ts +0 -67
  129. package/src/core/hook/pointer-block.ts +0 -26
  130. package/src/core/hook/select.ts +0 -32
  131. package/src/core/hook/transcript.ts +0 -121
  132. package/src/core/ingest/ingest-session.ts +0 -111
  133. package/src/core/providers/provider-models.ts +0 -100
  134. package/src/core/providers/provider-registry.ts +0 -196
  135. package/src/core/recall/citation-log.ts +0 -108
  136. package/src/core/recall/filter.ts +0 -27
  137. package/src/core/recall/index.ts +0 -6
  138. package/src/core/recall/match-fields.ts +0 -40
  139. package/src/core/recall/query-log.ts +0 -149
  140. package/src/core/recall/query-shape.ts +0 -66
  141. package/src/core/recall/recall-service.ts +0 -320
  142. package/src/core/recall/recent-log.ts +0 -59
  143. package/src/core/recall/tokenize.ts +0 -18
  144. package/src/core/recall/useful-scan.ts +0 -336
  145. package/src/core/recall-facts/fact-query-log.ts +0 -150
  146. package/src/core/recall-facts/fact-recall-service.ts +0 -327
  147. package/src/core/scheduler/scan-once.ts +0 -142
  148. package/src/core/scheduler/scheduler.ts +0 -225
  149. package/src/core/sources/source-registry.ts +0 -260
  150. package/src/core/storage/db-restore.ts +0 -133
  151. package/src/core/storage/live-status.ts +0 -45
  152. package/src/core/storage/migrate.ts +0 -72
  153. package/src/core/storage/sqlite-fact-store.ts +0 -304
  154. package/src/core/storage/sqlite-session-store.ts +0 -765
  155. package/src/hook/prompt-recall-hook.ts +0 -174
  156. package/src/hook/session-end-hook.ts +0 -81
  157. package/src/hook/session-start-hook.ts +0 -165
  158. package/src/hook/stop-hook.ts +0 -236
  159. package/src/http/app.ts +0 -1137
  160. package/src/install/claude-code.ts +0 -128
  161. package/src/install/codex.ts +0 -367
  162. package/src/install/hermes-agent.ts +0 -76
  163. package/src/install/hermes.ts +0 -78
  164. package/src/install/ollama.ts +0 -211
  165. package/src/install/setup.ts +0 -368
  166. package/src/llm/classifier-box.ts +0 -64
  167. package/src/llm/deepseek-client.ts +0 -150
  168. package/src/llm/env-autoload.ts +0 -55
  169. package/src/llm/ollama-client.ts +0 -189
  170. package/src/mcp/server.ts +0 -534
  171. package/src/ports/fact-store.ts +0 -102
  172. package/src/ports/llm-client.ts +0 -52
  173. package/src/ports/logger.ts +0 -16
  174. package/src/ports/session-store.ts +0 -45
  175. package/src/ports/transcript-adapter.ts +0 -55
  176. package/src/shared/types.ts +0 -145
  177. package/src/ui/App.tsx +0 -58
  178. package/src/ui/components/PromoteOpenButton.tsx +0 -65
  179. package/src/ui/components/SessionDrawer.tsx +0 -136
  180. package/src/ui/components/SideNav.tsx +0 -162
  181. package/src/ui/components/Skeleton.tsx +0 -107
  182. package/src/ui/index.html +0 -13
  183. package/src/ui/lib/actions.ts +0 -30
  184. package/src/ui/lib/api.ts +0 -92
  185. package/src/ui/lib/dataset.ts +0 -141
  186. package/src/ui/lib/registries.ts +0 -155
  187. package/src/ui/lib/view-settings.ts +0 -41
  188. package/src/ui/main.tsx +0 -15
  189. package/src/ui/pages/Live.tsx +0 -229
  190. package/src/ui/pages/Pulse.tsx +0 -415
  191. package/src/ui/pages/Recall.tsx +0 -190
  192. package/src/ui/pages/River.tsx +0 -308
  193. package/src/ui/pages/Search.tsx +0 -93
  194. package/src/ui/pages/Stub.tsx +0 -9
  195. package/src/ui/pages/Thread.tsx +0 -262
  196. package/src/ui/pages/settings/Classifier.tsx +0 -227
  197. package/src/ui/pages/settings/Data.tsx +0 -190
  198. package/src/ui/pages/settings/Index.tsx +0 -65
  199. package/src/ui/pages/settings/Labels.tsx +0 -224
  200. package/src/ui/pages/settings/Providers.tsx +0 -305
  201. package/src/ui/pages/settings/SettingsSubnav.tsx +0 -28
  202. package/src/ui/pages/settings/Sources.tsx +0 -326
  203. package/src/ui/pages/settings/Views.tsx +0 -96
  204. package/src/ui/styles.css +0 -1766
  205. package/src/ui/tsconfig.json +0 -21
  206. package/src/ui/vite.config.ts +0 -19
  207. package/tests/fixtures/claude_code/short_session.jsonl +0 -2
  208. package/tests/fixtures/claude_code/standard_iso.jsonl +0 -4
  209. package/tests/fixtures/claude_code/tool_heavy.jsonl +0 -8
  210. package/tests/fixtures/claude_code/with_subagent.jsonl +0 -7
  211. package/tests/fixtures/facts.ts +0 -17
  212. package/tests/fixtures/golden-corpus.ts +0 -85
  213. package/tests/fixtures/hermes/paired_request_dump.json +0 -24
  214. package/tests/fixtures/hermes/paired_session.json +0 -23
  215. package/tests/fixtures/hermes/request_dump.json +0 -28
  216. package/tests/fixtures/hermes/session_iso.json +0 -38
  217. package/tests/fixtures/hermes/session_unix.json +0 -38
  218. package/tests/fixtures/hermes/system_only.json +0 -18
  219. package/tests/fixtures/pi/error-connection-abort.jsonl +0 -8
  220. package/tests/fixtures/pi/short-successful.jsonl +0 -5
  221. package/tests/fixtures/pi/with-custom-message.jsonl +0 -6
  222. package/tests/fixtures/sessions.ts +0 -22
  223. package/tests/integration/backfill-facts.test.ts +0 -362
  224. package/tests/integration/citation-explicit.test.ts +0 -111
  225. package/tests/integration/cite-event.test.ts +0 -169
  226. package/tests/integration/cite-memo.test.ts +0 -87
  227. package/tests/integration/db-restore.test.ts +0 -153
  228. package/tests/integration/embed-backfill.test.ts +0 -176
  229. package/tests/integration/fact-supersedence.test.ts +0 -313
  230. package/tests/integration/fts-index.test.ts +0 -60
  231. package/tests/integration/getbyids-sqlite.test.ts +0 -60
  232. package/tests/integration/hermes-agent-hooks.test.ts +0 -248
  233. package/tests/integration/hook-claude-settings.test.ts +0 -205
  234. package/tests/integration/hook-log.test.ts +0 -54
  235. package/tests/integration/hook-memo.test.ts +0 -68
  236. package/tests/integration/hook-pre-compact.test.ts +0 -105
  237. package/tests/integration/hook-subagent-start.test.ts +0 -102
  238. package/tests/integration/http.test.ts +0 -401
  239. package/tests/integration/keyword-search-fts.test.ts +0 -66
  240. package/tests/integration/mcp-recall-logging.test.ts +0 -88
  241. package/tests/integration/mcp.test.ts +0 -248
  242. package/tests/integration/memo-sweep.test.ts +0 -91
  243. package/tests/integration/prompt-recall-hook.test.ts +0 -88
  244. package/tests/integration/provider-registry.test.ts +0 -107
  245. package/tests/integration/recall-golden.test.ts +0 -59
  246. package/tests/integration/recall-sqlite.test.ts +0 -169
  247. package/tests/integration/scheduler.test.ts +0 -391
  248. package/tests/integration/session-end-hook.test.ts +0 -48
  249. package/tests/integration/session-start-hook.test.ts +0 -126
  250. package/tests/integration/source-registry.test.ts +0 -120
  251. package/tests/integration/sqlite-fact-store.test.ts +0 -346
  252. package/tests/integration/stop-hook.test.ts +0 -560
  253. package/tests/integration/wal-checkpoint.test.ts +0 -49
  254. package/tests/unit/cli/launchctl-helpers.test.ts +0 -60
  255. package/tests/unit/core/adapters/aider.test.ts +0 -230
  256. package/tests/unit/core/adapters/claude-code.test.ts +0 -118
  257. package/tests/unit/core/adapters/hermes-agent.test.ts +0 -329
  258. package/tests/unit/core/adapters/hermes.test.ts +0 -81
  259. package/tests/unit/core/adapters/jsonl-generic.test.ts +0 -142
  260. package/tests/unit/core/adapters/opencode.test.ts +0 -354
  261. package/tests/unit/core/adapters/pi.test.ts +0 -110
  262. package/tests/unit/core/classifier/prompt.test.ts +0 -126
  263. package/tests/unit/core/embedding/chunk-body.test.ts +0 -100
  264. package/tests/unit/core/facts/extract-facts.test.ts +0 -117
  265. package/tests/unit/core/filter.test.ts +0 -40
  266. package/tests/unit/core/hook/citation-detect-cite-session.test.ts +0 -96
  267. package/tests/unit/core/hook/citation-detect.test.ts +0 -124
  268. package/tests/unit/core/hook/gate.test.ts +0 -29
  269. package/tests/unit/core/hook/pointer-block.test.ts +0 -22
  270. package/tests/unit/core/hook/select.test.ts +0 -66
  271. package/tests/unit/core/match-fields.test.ts +0 -39
  272. package/tests/unit/core/mcp-cite-session.test.ts +0 -51
  273. package/tests/unit/core/providers/provider-models.test.ts +0 -101
  274. package/tests/unit/core/query-shape.test.ts +0 -92
  275. package/tests/unit/core/recall-facts/fact-recall-service.test.ts +0 -258
  276. package/tests/unit/core/recall-service.test.ts +0 -200
  277. package/tests/unit/core/storage/live-status.test.ts +0 -54
  278. package/tests/unit/core/tokenize.test.ts +0 -32
  279. package/tests/unit/core/useful-scan.test.ts +0 -537
  280. package/tests/unit/llm/embed.test.ts +0 -93
  281. package/tests/unit/llm/ollama-client.test.ts +0 -124
  282. package/tests/unit/scripts/longmemeval-scorer.test.ts +0 -114
  283. package/tsconfig.json +0 -31
  284. package/tsconfig.test.json +0 -11
  285. package/vitest.config.ts +0 -22
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  > Local-first memory OS for AI operators — one corpus across every runtime you use.
4
4
 
5
- `nlm-memory` indexes every session from Claude Code, Codex, OpenCode, Hermes, Aider, and pi into a single searchable store on your machine. Three properties no competitor ships together:
5
+ `nlm-memory` indexes every session from Claude Code, Codex, OpenCode, Cursor, Windsurf, Hermes, Aider, and pi into a single searchable store on your machine. Three properties no competitor ships together:
6
6
 
7
- 1. **Cross-runtime reach.** Claude Code, Codex, and OpenCode ship today. Hermes, pi, and Aider follow the same adapter pattern. One index, every tool — not one per runtime.
7
+ 1. **Cross-runtime reach.** One index spans every adapter — not one per runtime.
8
8
  2. **Editable timeline.** Sessions can be superseded, retired, or marked aborted. Memory is non-linear: patch history retroactively. No other memory layer lets you do this.
9
9
  3. **97.2% R@5 baseline.** On a 14-month corpus, keyword recall surfaces the right session in the top 5 on 97.2% of evaluator queries. No fine-tuning, no cloud, no account.
10
10
 
@@ -15,81 +15,119 @@ Everything stays on your machine. No telemetry, no account required beyond your
15
15
  ## Requirements
16
16
 
17
17
  - **Node 20+**
18
- - **[Ollama](https://ollama.com)** running locally with `nomic-embed-text` pulled:
18
+ - **[Ollama](https://ollama.com)** running locally with `nomic-embed-text` pulled for semantic search:
19
19
  ```sh
20
20
  ollama pull nomic-embed-text
21
21
  ```
22
- - **A classifier** — [DeepSeek](https://platform.deepseek.com) is recommended (fast, cheap, ~$0.002/session). Set `DEEPSEEK_API_KEY` in `~/.nlm/.env`. Ollama works offline with `NLM_CLASSIFIER=ollama`.
22
+ - **A classifier** — pick during setup:
23
+ - **DeepSeek cloud** (recommended for speed) — fast, cheap (~$0.002/session). Sends up to 30K chars of each session transcript to `api.deepseek.com`.
24
+ - **Ollama local** — fully offline. Slower; uses whichever chat model you select from your local pull list.
23
25
 
24
26
  ---
25
27
 
26
28
  ## Install
27
29
 
28
30
  ```sh
29
- npm install -g github:pbmagnet4/nlm-memory-ts
30
- nlm migrate
31
- nlm install
31
+ npm install -g nlm-memory
32
+ nlm setup
32
33
  ```
33
34
 
34
- `nlm install` writes a macOS LaunchAgent that starts the daemon on login and keeps it running. Open **http://localhost:3940/ui** — done.
35
+ `nlm setup` is the interactive first-run wizard. It asks you to pick your classifier, model, and which runtimes to connect (Claude Code, Codex, Hermes), then installs the daemon. After it finishes, open **http://localhost:3940/ui** — done.
36
+
37
+ ### Platform support
38
+
39
+ | Platform | Daemon | Notes |
40
+ |---|---|---|
41
+ | **macOS** | LaunchAgent at `~/Library/LaunchAgents/com.github.pbmagnet4.nlm-memory.plist` | Auto-starts on login |
42
+ | **Linux** | systemd user unit at `~/.config/systemd/user/nlm.service` | Run `loginctl enable-linger $USER` on headless servers so the daemon survives logout |
43
+ | **Windows** | Manual `nlm start` for now | Hook + MCP install paths are platform-aware; daemon supervisor lands in the next release |
35
44
 
36
45
  To stop or remove:
37
46
  ```sh
38
- launchctl stop com.github.pbmagnet4.nlm-memory # stop without uninstalling
39
- nlm uninstall # remove the LaunchAgent entirely
47
+ nlm uninstall # remove the daemon supervisor on your platform
40
48
  ```
41
49
 
42
50
  ---
43
51
 
44
- ## Wire to your AI agents (MCP)
52
+ ## How recall works
45
53
 
46
- Add to `~/.mcp.json` (or your editor's MCP config):
54
+ Once installed, NLM runs as a quiet background daemon. Two ways your AI agents get to it:
47
55
 
48
- ```json
49
- {
50
- "mcpServers": {
51
- "nlm-memory": {
52
- "command": "node",
53
- "args": ["<path-to-global-npm>/lib/node_modules/nlm-memory/dist/cli/nlm.js", "mcp"]
54
- }
55
- }
56
- }
57
- ```
56
+ ### 1. Hooks (Claude Code) — automatic context injection
58
57
 
59
- Find the path with `npm root -g` the full path is `$(npm root -g)/nlm-memory/dist/cli/nlm.js`.
58
+ `nlm connect claude-code` installs five hooks into `~/.claude/settings.json`:
60
59
 
61
- Or use the runtime-specific connect commands:
60
+ - **UserPromptSubmit / SessionStart** — before each turn, NLM scores the prompt against your past sessions and silently prepends a pointer block listing the 0–3 most likely-relevant prior sessions. The model sees them as conversational context.
61
+ - **Stop** — after the model responds, NLM scans the response for citations of surfaced session IDs to measure useful-hit rate.
62
+ - **PreCompact / SubagentStart** — link conversations across compactions and subagent dispatches so threads stay coherent.
63
+
64
+ Default mode is **live** (recall injected into prompts). Switch to **shadow** (log-only, no injection) by setting `NLM_HOOK_MODE=shadow` in your hook commands.
65
+
66
+ ### 2. MCP — explicit tools any agent can call
62
67
 
63
68
  ```sh
64
- nlm connect claude-code # writes to ~/.mcp.json + installs hooks
69
+ nlm connect claude-code # writes ~/.mcp.json + installs hooks
65
70
  nlm connect codex # installs as a Codex marketplace plugin
66
- nlm connect hermes # writes to ~/.hermes/config.yaml (MCP)
71
+ nlm connect hermes # writes ~/.hermes/config.yaml (MCP)
67
72
  nlm connect hermes-agent # installs as a NousResearch Hermes plugin (hooks + MCP)
68
73
  ```
69
74
 
70
- Once wired, agents can call `recall_sessions` (search past conversations) and `recall_facts` (pull structured facts like decisions and project state) automatically.
75
+ Once wired, agents can call `recall_sessions` (search past conversations), `recall_facts` (decisions/open questions/project state), `get_session` (pull a full session), `get_fact_history` (how a decision evolved), and `cite_session` (explicitly mark a session as referenced).
76
+
77
+ For container-hosted agents that can't use stdio MCP, the daemon also exposes Streamable-HTTP MCP at `POST /mcp`. Use the auto-generated `NLM_MCP_TOKEN` from `~/.nlm/.env` as a bearer.
71
78
 
72
79
  ---
73
80
 
74
- ## What's inside
81
+ ## What's inside the UI
82
+
83
+ Open `http://localhost:3940/ui` after the daemon starts.
75
84
 
76
85
  | Page | What it shows |
77
86
  |---|---|
78
87
  | **Live** | Sessions being written in real time, recent reads and decisions |
79
88
  | **Pulse** | System health — coherence, runtimes, stale entities, recent sessions |
80
- | **River** | Full session timeline with density controls |
81
- | **Thread** | Per-entity conversation history |
82
- | **Search** | Keyword, semantic, or hybrid recall |
89
+ | **River** | Full session timeline with density controls and supersedence visualization |
90
+ | **Thread** | Per-entity conversation history with runtime filters |
91
+ | **Search** | Keyword, semantic, or hybrid recall with match snippets |
83
92
  | **Recall** | Adoption telemetry — is the memory system actually being used? |
84
93
  | **Settings** | Sources, providers, classifier, data backup/restore |
85
94
 
86
95
  ---
87
96
 
97
+ ## Security
98
+
99
+ NLM is local-first by design. The daemon:
100
+
101
+ - Binds to `127.0.0.1` only — never `0.0.0.0`.
102
+ - Enforces Host + Origin checks on `/api/*` to block DNS rebinding and cross-origin drive-by.
103
+ - Generates a 256-bit `NLM_MCP_TOKEN` on first run and persists to `~/.nlm/.env` (mode `0600`). All non-browser API requests (hooks, MCP container clients) authenticate with `Authorization: Bearer ${NLM_MCP_TOKEN}`.
104
+ - Recursively enforces `0700` on `~/.nlm/` and `0600` on its contents on every start.
105
+ - Sends nothing outbound except:
106
+ - Ollama (`localhost:11434`) for embeddings + local classifier
107
+ - DeepSeek API (`api.deepseek.com`) — only when classifier is set to DeepSeek
108
+ - Your AI runtime transcript files (read-only)
109
+
110
+ No telemetry. No vendor calls. No account.
111
+
112
+ Report vulnerabilities via [SECURITY.md](SECURITY.md).
113
+
114
+ ---
115
+
116
+ ## Upgrading from v0.4.x
117
+
118
+ ```sh
119
+ npm update -g nlm-memory
120
+ ```
121
+
122
+ Old installs have `NLM_HOOK_MODE=shadow` hardcoded in `~/.claude/settings.json` — shadow mode is silent, so re-run `nlm hook install` to switch to live recall injection. Permissions and `NLM_MCP_TOKEN` self-heal on the next `nlm start`.
123
+
124
+ ---
125
+
88
126
  ## How it differs from mem0 and graphiti
89
127
 
90
128
  - **Unit of memory:** whole sessions with extracted markers (decisions, open questions, entities), not individual facts or graph edges.
91
129
  - **Audience:** you querying your own past work, not an embedded SDK for app developers.
92
- - **Cross-runtime:** one corpus across Claude Code, Codex, OpenCode, Hermes, and more. Competitors target one runtime.
130
+ - **Cross-runtime:** one corpus across Claude Code, Codex, OpenCode, Cursor, Windsurf, Hermes, and more. Competitors target one runtime.
93
131
  - **Editable timeline:** sessions can be superseded, retired, aborted. No other tool lets you retrofit memory — a record from 6 months ago can be corrected today.
94
132
  - **Local-only:** no hosted offering, no telemetry, no vendor dependency.
95
133
 
@@ -104,11 +142,11 @@ npm install # install dependencies
104
142
  npm run build # compile dist/ — commit the result, it ships in the repo
105
143
  npm run dev # hot-reload daemon
106
144
  npm run ui:dev # hot-reload UI at localhost:5173 (proxies /api to :3940)
107
- npm test # unit + integration tests
145
+ npm test # 601 tests across 62 files
108
146
  npm run typecheck
109
147
  ```
110
148
 
111
- `dist/` is committed to the repo so `npm install -g github:…` works without a build step on the user's machine. Rebuild and commit `dist/` whenever you change `src/`.
149
+ `dist/` is committed to the repo so the global install works without a build step on the user's machine. Rebuild and commit `dist/` whenever you change `src/`.
112
150
 
113
151
  Database lives at `~/.nlm/canonical.sqlite`. Override with `NLM_DB_PATH`.
114
152
 
package/dist/cli/nlm.js CHANGED
@@ -26,8 +26,9 @@ import { fileURLToPath } from "node:url";
26
26
  import { dirname, resolve, join } from "node:path";
27
27
  import { homedir } from "node:os";
28
28
  import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs";
29
- import { execFileSync } from "node:child_process";
29
+ import { execFileSync, spawnSync } from "node:child_process";
30
30
  import { Command } from "commander";
31
+ import pkg from "../../package.json" with { type: "json" };
31
32
  import { serve } from "@hono/node-server";
32
33
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
33
34
  import { FactRecallService } from "../core/recall-facts/fact-recall-service.js";
@@ -45,9 +46,13 @@ import { OllamaClient } from "../llm/ollama-client.js";
45
46
  import { autoloadEnv } from "../llm/env-autoload.js";
46
47
  import { addHook, buildHookCommand, removeHook, smokeTestHookCommand } from "../core/hook/claude-settings.js";
47
48
  import { codexBinaryAvailable, connectCodex, disconnectCodex, pluginScriptsDir, } from "../install/codex.js";
48
- import { connectClaudeCode, disconnectClaudeCode, installClaudeCodeHooks } from "../install/claude-code.js";
49
+ import { connectClaudeCode, disconnectClaudeCode, installClaudeCodeHooks, mcpConfigPath } from "../install/claude-code.js";
50
+ import { hardenNlmDirPermissions } from "../install/nlm-dir-perms.js";
51
+ import { ensureMcpToken } from "../install/ollama.js";
52
+ import { connectCursor, disconnectCursor } from "../install/cursor.js";
49
53
  import { connectHermes, disconnectHermes, hermesConfigPath } from "../install/hermes.js";
50
54
  import { connectHermesAgent, disconnectHermesAgent, hermesAgentPluginDir } from "../install/hermes-agent.js";
55
+ import { connectWindsurf, disconnectWindsurf } from "../install/windsurf.js";
51
56
  import { runSetup } from "../install/setup.js";
52
57
  import { runParity } from "./classify-parity.js";
53
58
  import { reembedCorpus } from "../core/embedding/embed-backfill.js";
@@ -149,13 +154,20 @@ const program = new Command();
149
154
  program
150
155
  .name("nlm")
151
156
  .description("Local-first memory operating system for AI operators")
152
- .version("0.3.0");
157
+ .version(pkg.version);
153
158
  program
154
159
  .command("start")
155
160
  .description("Boot the HTTP server + ingest scheduler")
156
161
  .option("--no-scheduler", "HTTP only; skip the ingest tick loop")
157
162
  .option("--interval-min <n>", "scheduler tick interval (min, default 30)", (v) => Number.parseInt(v, 10), 30)
158
163
  .action(async (opts) => {
164
+ // Self-heal perms on every daemon start. Idempotent. Covers upgrade
165
+ // path from pre-v0.4.2 installs where ~/.nlm contents were world-readable.
166
+ hardenNlmDirPermissions();
167
+ // Generate NLM_MCP_TOKEN if missing so /api/* gets Bearer-protected for
168
+ // non-browser callers. Idempotent: re-reads persisted token first.
169
+ autoloadEnv();
170
+ ensureMcpToken();
159
171
  const { store, facts, sources, providers, recall, factRecall, embedder, classifier } = buildStack();
160
172
  const { existsSync } = await import("node:fs");
161
173
  const hasMcpToken = Boolean(process.env["NLM_MCP_TOKEN"]);
@@ -387,6 +399,37 @@ program
387
399
  });
388
400
  const LAUNCH_AGENT_LABEL = "com.github.pbmagnet4.nlm-memory";
389
401
  const LAUNCH_AGENT_PLIST = join(homedir(), "Library", "LaunchAgents", `${LAUNCH_AGENT_LABEL}.plist`);
402
+ const LINUX_SYSTEMD_UNIT_NAME = "nlm.service";
403
+ const LINUX_SYSTEMD_UNIT_PATH = join(homedir(), ".config", "systemd", "user", LINUX_SYSTEMD_UNIT_NAME);
404
+ function buildSystemdUnit(nodeExec, nlmJs) {
405
+ const logDir = join(homedir(), ".nlm", "logs");
406
+ return `[Unit]
407
+ Description=NLM Memory — local AI session memory daemon
408
+ After=network.target
409
+
410
+ [Service]
411
+ Type=simple
412
+ ExecStart=${nodeExec} ${nlmJs} start
413
+ WorkingDirectory=${homedir()}
414
+ Restart=on-failure
415
+ RestartSec=10
416
+ StandardOutput=append:${logDir}/daemon-out.log
417
+ StandardError=append:${logDir}/daemon-err.log
418
+
419
+ [Install]
420
+ WantedBy=default.target
421
+ `;
422
+ }
423
+ // systemd user instance needs XDG_RUNTIME_DIR (a real user session) and
424
+ // systemctl --user to respond. Both are missing on headless servers without
425
+ // loginctl enable-linger and in many minimal containers.
426
+ function linuxSystemdUserAvailable() {
427
+ if (process.platform !== "linux")
428
+ return false;
429
+ if (!process.env["XDG_RUNTIME_DIR"])
430
+ return false;
431
+ return spawnSync("systemctl", ["--user", "--version"], { encoding: "utf8" }).status === 0;
432
+ }
390
433
  function buildPlist(nodeExec, nlmJs) {
391
434
  const logDir = join(homedir(), ".nlm", "logs");
392
435
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -424,38 +467,91 @@ function buildPlist(nodeExec, nlmJs) {
424
467
  }
425
468
  program
426
469
  .command("install")
427
- .description("Install the macOS LaunchAgent so nlm-memory auto-starts on login")
470
+ .description("Install the auto-start daemon (LaunchAgent on macOS, systemd user unit on Linux)")
428
471
  .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" });
472
+ // Harden before installing the daemon so the persisted unit owner-
473
+ // checks succeed against locked-down ~/.nlm logs.
474
+ hardenNlmDirPermissions();
475
+ if (process.platform === "darwin") {
476
+ const uid = process.getuid?.();
477
+ if (uid === undefined) {
478
+ console.error("nlm install: could not determine UID");
479
+ process.exit(1);
480
+ }
481
+ mkdirSync(join(homedir(), ".nlm", "logs"), { recursive: true });
482
+ writeFileSync(LAUNCH_AGENT_PLIST, buildPlist(process.execPath, __filename), "utf8");
483
+ console.error(`nlm: wrote ${LAUNCH_AGENT_PLIST}`);
484
+ try {
485
+ execFileSync("launchctl", ["bootout", `gui/${uid}`, LAUNCH_AGENT_LABEL], { stdio: "ignore" });
486
+ }
487
+ catch {
488
+ // not loaded yet — expected on first install
489
+ }
490
+ execFileSync("launchctl", ["bootstrap", `gui/${uid}`, LAUNCH_AGENT_PLIST]);
491
+ console.error("nlm: daemon installed and started.");
492
+ console.error(` UI: http://localhost:${port()}/ui`);
493
+ console.error(` To stop: launchctl stop ${LAUNCH_AGENT_LABEL}`);
494
+ console.error(" To remove: nlm uninstall");
495
+ return;
443
496
  }
444
- catch {
445
- // not loaded yet — expected on first install
497
+ if (process.platform === "linux") {
498
+ if (!linuxSystemdUserAvailable()) {
499
+ console.error("nlm install: systemd user instance not available.");
500
+ console.error(" XDG_RUNTIME_DIR missing or `systemctl --user` did not respond.");
501
+ console.error(" Common on headless servers without an active user session.");
502
+ console.error(" Start manually with: nlm start &");
503
+ console.error(" Or enable lingering so user units run without login:");
504
+ console.error(" sudo loginctl enable-linger $USER");
505
+ console.error(" Then re-run: nlm install");
506
+ process.exit(1);
507
+ }
508
+ mkdirSync(dirname(LINUX_SYSTEMD_UNIT_PATH), { recursive: true });
509
+ mkdirSync(join(homedir(), ".nlm", "logs"), { recursive: true });
510
+ writeFileSync(LINUX_SYSTEMD_UNIT_PATH, buildSystemdUnit(process.execPath, __filename), "utf8");
511
+ console.error(`nlm: wrote ${LINUX_SYSTEMD_UNIT_PATH}`);
512
+ execFileSync("systemctl", ["--user", "daemon-reload"]);
513
+ execFileSync("systemctl", ["--user", "enable", "--now", LINUX_SYSTEMD_UNIT_NAME]);
514
+ console.error("nlm: daemon installed and started.");
515
+ console.error(` UI: http://localhost:${port()}/ui`);
516
+ console.error(` Status: systemctl --user status ${LINUX_SYSTEMD_UNIT_NAME}`);
517
+ console.error(` To stop: systemctl --user stop ${LINUX_SYSTEMD_UNIT_NAME}`);
518
+ console.error(" To remove: nlm uninstall");
519
+ console.error(" Headless? Run `sudo loginctl enable-linger $USER` so the daemon survives logout.");
520
+ return;
446
521
  }
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");
522
+ console.error("nlm install: only macOS and Linux (systemd) are supported.");
523
+ console.error(" On Windows, run `nlm start` manually or via Task Scheduler.");
524
+ process.exit(1);
452
525
  });
453
526
  program
454
527
  .command("uninstall")
455
- .description("Remove the macOS LaunchAgent")
528
+ .description("Remove the auto-start daemon (LaunchAgent on macOS, systemd user unit on Linux)")
456
529
  .action(() => {
530
+ if (process.platform === "linux") {
531
+ // Stop + disable, then remove the unit. Idempotent: ignore "not loaded"
532
+ // errors so re-running uninstall on a half-removed state still finishes.
533
+ try {
534
+ execFileSync("systemctl", ["--user", "disable", "--now", LINUX_SYSTEMD_UNIT_NAME], { stdio: "pipe" });
535
+ console.error(`nlm: stopped and disabled ${LINUX_SYSTEMD_UNIT_NAME}`);
536
+ }
537
+ catch {
538
+ // Unit wasn't loaded — fine, proceed to file cleanup.
539
+ }
540
+ if (existsSync(LINUX_SYSTEMD_UNIT_PATH)) {
541
+ rmSync(LINUX_SYSTEMD_UNIT_PATH);
542
+ console.error(`nlm: removed ${LINUX_SYSTEMD_UNIT_PATH}`);
543
+ }
544
+ try {
545
+ execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
546
+ }
547
+ catch {
548
+ // systemd unavailable — file already removed, nothing more to do.
549
+ }
550
+ console.error("nlm: uninstalled. Run `nlm install` to reinstall.");
551
+ return;
552
+ }
457
553
  if (process.platform !== "darwin") {
458
- console.error("nlm uninstall: only macOS is supported.");
554
+ console.error("nlm uninstall: only macOS and Linux (systemd) are supported.");
459
555
  process.exit(1);
460
556
  }
461
557
  const uid = process.getuid?.();
@@ -527,7 +623,7 @@ const hook = program
527
623
  .description("Manage the Claude Code NLM hooks");
528
624
  hook
529
625
  .command("install")
530
- .description("Add the NLM hooks (recall + session-end + stop) to ~/.claude/settings.json (shadow mode)")
626
+ .description("Add the NLM hooks (recall + session-end + stop) to ~/.claude/settings.json (live mode)")
531
627
  .action(() => {
532
628
  const path = claudeSettingsPath();
533
629
  const hookLogPath = process.env["NLM_HOOK_LOG"] ?? join(homedir(), ".nlm", "hook-log.jsonl");
@@ -537,7 +633,7 @@ hook
537
633
  // partial failure is the bug class we shipped #161 to prevent.
538
634
  const installed = [];
539
635
  for (const spec of ALL_HOOKS) {
540
- const command = buildHookCommand(process.execPath, spec.script, "shadow");
636
+ const command = buildHookCommand(process.execPath, spec.script, "live");
541
637
  addHook(path, command, spec.event);
542
638
  const smoke = smokeTestHookCommand(command, hookLogPath);
543
639
  if (!smoke.ok) {
@@ -557,14 +653,14 @@ hook
557
653
  }
558
654
  installed.push(spec);
559
655
  }
560
- console.error(`nlm: NLM hooks installed in ${path} (shadow mode):`);
656
+ console.error(`nlm: NLM hooks installed in ${path} (live mode):`);
561
657
  for (const spec of installed) {
562
658
  console.error(` - ${spec.event} → ${spec.label}-hook`);
563
659
  }
564
660
  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.");
661
+ console.error(" Recall hooks inject prior-session context on UserPromptSubmit and log to ~/.nlm/hook-log.jsonl.");
566
662
  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.");
663
+ console.error(" To run silently for calibration (no injection): set NLM_HOOK_MODE=shadow in the command.");
568
664
  console.error(" To remove: nlm hook uninstall");
569
665
  });
570
666
  hook
@@ -637,7 +733,7 @@ connect
637
733
  .action((opts) => {
638
734
  if (opts.dryRun) {
639
735
  console.error("nlm connect claude-code (dry run):");
640
- console.error(` write [mcpServers.nlm-memory] to ${join(homedir(), ".mcp.json")}`);
736
+ console.error(` write [mcpServers.nlm-memory] to ${mcpConfigPath()}`);
641
737
  if (opts.withHooks)
642
738
  console.error(" install 6 Claude Code hooks");
643
739
  return;
@@ -702,6 +798,54 @@ connect
702
798
  }
703
799
  console.error(" Also run: nlm connect hermes (to wire the MCP server)");
704
800
  });
801
+ connect
802
+ .command("cursor")
803
+ .description("Register Cursor as an nlm source (reads state.vscdb directly — no files installed)")
804
+ .option("--db-path <path>", "override path to globalStorage/state.vscdb")
805
+ .option("--dry-run", "print what would happen without changing files")
806
+ .action((opts) => {
807
+ const store = new SqliteSessionStore({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
808
+ try {
809
+ const registry = new SourceRegistry(store.rawDb());
810
+ const report = connectCursor(registry, {
811
+ ...(opts.dbPath ? { dbPath: opts.dbPath } : {}),
812
+ dryRun: Boolean(opts.dryRun),
813
+ });
814
+ if (opts.dryRun) {
815
+ console.error(`nlm connect cursor (dry run): register source at ${report.adapterDbPath}${report.adapterExists ? "" : " (not found yet)"}`);
816
+ return;
817
+ }
818
+ const suffix = report.adapterExists ? "" : " (DB not found — will activate when Cursor is installed)";
819
+ console.error(`nlm: Cursor source ${report.action} → ${report.adapterDbPath}${suffix}`);
820
+ }
821
+ finally {
822
+ store.close();
823
+ }
824
+ });
825
+ connect
826
+ .command("windsurf")
827
+ .description("Register Windsurf as an nlm source (reads state.vscdb files directly — no files installed)")
828
+ .option("--user-dir <path>", "override path to Windsurf User directory")
829
+ .option("--dry-run", "print what would happen without changing files")
830
+ .action((opts) => {
831
+ const store = new SqliteSessionStore({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
832
+ try {
833
+ const registry = new SourceRegistry(store.rawDb());
834
+ const report = connectWindsurf(registry, {
835
+ ...(opts.userDir ? { userDir: opts.userDir } : {}),
836
+ dryRun: Boolean(opts.dryRun),
837
+ });
838
+ if (opts.dryRun) {
839
+ console.error(`nlm connect windsurf (dry run): register source at ${report.userDir}${report.dirExists ? "" : " (not found yet)"}`);
840
+ return;
841
+ }
842
+ const suffix = report.dirExists ? "" : " (User dir not found — will activate when Windsurf is installed)";
843
+ console.error(`nlm: Windsurf source ${report.action} → ${report.userDir}${suffix}`);
844
+ }
845
+ finally {
846
+ store.close();
847
+ }
848
+ });
705
849
  const disconnect = program
706
850
  .command("disconnect")
707
851
  .description("Disconnect nlm-memory from an AI coding runtime");
@@ -791,6 +935,48 @@ disconnect
791
935
  ? `nlm: removed plugin directory ${report.destDir}`
792
936
  : `nlm: no plugin directory found at ${report.destDir}`);
793
937
  });
938
+ disconnect
939
+ .command("cursor")
940
+ .description("Disable the Cursor source in the nlm registry (leaves Cursor untouched)")
941
+ .option("--dry-run", "print what would happen without changing files")
942
+ .action((opts) => {
943
+ const store = new SqliteSessionStore({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
944
+ try {
945
+ const registry = new SourceRegistry(store.rawDb());
946
+ const report = disconnectCursor(registry, { dryRun: Boolean(opts.dryRun) });
947
+ if (opts.dryRun) {
948
+ console.error("nlm disconnect cursor (dry run): disable Cursor source in registry");
949
+ return;
950
+ }
951
+ console.error(report.action === "disabled"
952
+ ? "nlm: Cursor source disabled"
953
+ : "nlm: no Cursor source found in registry");
954
+ }
955
+ finally {
956
+ store.close();
957
+ }
958
+ });
959
+ disconnect
960
+ .command("windsurf")
961
+ .description("Disable the Windsurf source in the nlm registry (leaves Windsurf untouched)")
962
+ .option("--dry-run", "print what would happen without changing files")
963
+ .action((opts) => {
964
+ const store = new SqliteSessionStore({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
965
+ try {
966
+ const registry = new SourceRegistry(store.rawDb());
967
+ const report = disconnectWindsurf(registry, { dryRun: Boolean(opts.dryRun) });
968
+ if (opts.dryRun) {
969
+ console.error("nlm disconnect windsurf (dry run): disable Windsurf source in registry");
970
+ return;
971
+ }
972
+ console.error(report.action === "disabled"
973
+ ? "nlm: Windsurf source disabled"
974
+ : "nlm: no Windsurf source found in registry");
975
+ }
976
+ finally {
977
+ store.close();
978
+ }
979
+ });
794
980
  program
795
981
  .command("setup")
796
982
  .description("Interactive first-run setup: detect runtimes, wire MCP + hooks, start daemon")
@@ -804,6 +990,10 @@ program
804
990
  launchAgentLabel: LAUNCH_AGENT_LABEL,
805
991
  launchAgentPlist: LAUNCH_AGENT_PLIST,
806
992
  buildPlist,
993
+ linuxSystemdUnitName: LINUX_SYSTEMD_UNIT_NAME,
994
+ linuxSystemdUnitPath: LINUX_SYSTEMD_UNIT_PATH,
995
+ buildSystemdUnit,
996
+ linuxSystemdUserAvailable,
807
997
  claudeSettingsPath: claudeSettingsPath(),
808
998
  allHooks: ALL_HOOKS,
809
999
  addHook,