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
@@ -1,128 +0,0 @@
1
- /**
2
- * `nlm connect claude-code` / `nlm disconnect claude-code` — writes the
3
- * nlm-memory MCP server block into ~/.mcp.json and removes it on disconnect.
4
- *
5
- * ~/.mcp.json is the global MCP config file that Claude Code reads on
6
- * startup. We merge our entry into the existing mcpServers object rather
7
- * than replacing the file, so other MCP servers the user has configured are
8
- * preserved.
9
- */
10
-
11
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
- import { homedir } from "node:os";
13
- import { dirname, join } from "node:path";
14
- import type { ClaudeHookEvent } from "../core/hook/claude-settings.js";
15
-
16
- export interface ConnectClaudeCodeOptions {
17
- readonly nlmBinPath: string;
18
- readonly nodeExecPath: string;
19
- readonly dryRun?: boolean;
20
- }
21
-
22
- export interface ConnectClaudeCodeReport {
23
- readonly mcpConfigPath: string;
24
- readonly alreadyPresent: boolean;
25
- readonly written: boolean;
26
- readonly dryRun: boolean;
27
- }
28
-
29
- export interface DisconnectClaudeCodeReport {
30
- readonly mcpConfigPath: string;
31
- readonly removed: boolean;
32
- readonly dryRun: boolean;
33
- }
34
-
35
- export function mcpConfigPath(): string {
36
- return process.env["NLM_MCP_CONFIG"] ?? join(homedir(), ".mcp.json");
37
- }
38
-
39
- function readConfig(path: string): Record<string, unknown> {
40
- if (!existsSync(path)) return {};
41
- try {
42
- return JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
43
- } catch {
44
- throw new Error(`${path} is not valid JSON. Fix or remove it, then re-run \`nlm connect claude-code\`.`);
45
- }
46
- }
47
-
48
- export function connectClaudeCode(opts: ConnectClaudeCodeOptions): ConnectClaudeCodeReport {
49
- const configPath = mcpConfigPath();
50
- const config = readConfig(configPath);
51
- const mcpServers = (config["mcpServers"] ?? {}) as Record<string, unknown>;
52
- const alreadyPresent = "nlm-memory" in mcpServers;
53
-
54
- if (!opts.dryRun) {
55
- mcpServers["nlm-memory"] = {
56
- command: opts.nodeExecPath,
57
- args: [opts.nlmBinPath, "mcp"],
58
- };
59
- config["mcpServers"] = mcpServers;
60
- mkdirSync(dirname(configPath), { recursive: true });
61
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
62
- }
63
-
64
- return { mcpConfigPath: configPath, alreadyPresent, written: !opts.dryRun, dryRun: opts.dryRun ?? false };
65
- }
66
-
67
- // ── Hook install helper (shared by setup wizard + connect --with-hooks) ──────
68
-
69
- export interface HookSpec {
70
- readonly event: ClaudeHookEvent;
71
- readonly script: string;
72
- readonly label: string;
73
- }
74
-
75
- export interface HookInstallOptions {
76
- readonly nodeExecPath: string;
77
- readonly hooks: ReadonlyArray<HookSpec>;
78
- readonly settingsPath: string;
79
- readonly hookLogPath: string;
80
- readonly addHook: (path: string, command: string, event?: ClaudeHookEvent) => void;
81
- readonly removeHook: (path: string, event?: ClaudeHookEvent | "*") => void;
82
- readonly buildHookCommand: (nodeExec: string, script: string, mode: "shadow" | "live") => string;
83
- readonly smokeTestHookCommand: (command: string, logPath: string) => { ok: boolean; reason?: string; stderr?: string };
84
- }
85
-
86
- export interface HookInstallResult {
87
- readonly ok: boolean;
88
- readonly count: number;
89
- readonly failedLabel?: string;
90
- readonly errorMessage?: string;
91
- }
92
-
93
- export function installClaudeCodeHooks(opts: HookInstallOptions): HookInstallResult {
94
- const installed: HookSpec[] = [];
95
- for (const spec of opts.hooks) {
96
- try {
97
- const command = opts.buildHookCommand(opts.nodeExecPath, spec.script, "shadow");
98
- opts.addHook(opts.settingsPath, command, spec.event);
99
- const smoke = opts.smokeTestHookCommand(command, opts.hookLogPath);
100
- if (!smoke.ok) {
101
- for (const prior of [...installed, spec]) opts.removeHook(opts.settingsPath, prior.event);
102
- const result: HookInstallResult = { ok: false, count: installed.length, failedLabel: spec.label };
103
- return smoke.reason ? { ...result, errorMessage: smoke.reason } : result;
104
- }
105
- installed.push(spec);
106
- } catch (e) {
107
- return { ok: false, count: installed.length, failedLabel: spec.label, errorMessage: e instanceof Error ? e.message : String(e) };
108
- }
109
- }
110
- return { ok: true, count: installed.length };
111
- }
112
-
113
- export function disconnectClaudeCode(opts?: { dryRun?: boolean }): DisconnectClaudeCodeReport {
114
- const configPath = mcpConfigPath();
115
- const config = readConfig(configPath);
116
- const mcpServers = config["mcpServers"] as Record<string, unknown> | undefined;
117
-
118
- if (!mcpServers || !("nlm-memory" in mcpServers)) {
119
- return { mcpConfigPath: configPath, removed: false, dryRun: opts?.dryRun ?? false };
120
- }
121
-
122
- if (!opts?.dryRun) {
123
- delete mcpServers["nlm-memory"];
124
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
125
- }
126
-
127
- return { mcpConfigPath: configPath, removed: true, dryRun: opts?.dryRun ?? false };
128
- }
@@ -1,367 +0,0 @@
1
- /**
2
- * `nlm connect codex` / `nlm disconnect codex` — installs nlm-memory as a
3
- * Codex CLI plugin via the marketplace mechanism.
4
- *
5
- * Two distribution surfaces:
6
- *
7
- * 1. The plugin path (default). Registers a Codex marketplace pointing at
8
- * pbmagnet4/nlm-memory-ts and installs the `nlm-memory` plugin from it.
9
- * Codex prompts for hook trust on first invocation; once trusted,
10
- * UserPromptSubmit + Stop hooks fire, and the .mcp.json wires the
11
- * `nlm-memory` MCP server alongside.
12
- *
13
- * 2. The legacy hooks.json fallback (--with-hooks). For Codex Desktop
14
- * builds where openai/codex#16430 blocks plugin-local hook dispatch,
15
- * additionally writes absolute paths into ~/.codex/hooks.json so the
16
- * hooks fire via the project-local code path. MCP still comes through
17
- * the plugin's .mcp.json.
18
- *
19
- * Marketplace + plugin add are delegated to the `codex` binary rather than
20
- * mutating ~/.codex/config.toml directly — the binary owns the trust state
21
- * machine and the snapshot fetch flow, and writing TOML by hand would race
22
- * against codex's own writes. The legacy hooks.json IS authored directly
23
- * because it's a project-local file the binary doesn't manage.
24
- */
25
-
26
- import { spawnSync } from "node:child_process";
27
- import {
28
- existsSync,
29
- mkdirSync,
30
- readFileSync,
31
- writeFileSync,
32
- } from "node:fs";
33
- import { homedir } from "node:os";
34
- import { dirname, join, resolve } from "node:path";
35
-
36
- const DEFAULT_SOURCE = "pbmagnet4/nlm-memory-ts";
37
- const PLUGIN_NAME = "nlm-memory";
38
- // Marketplace name is derived from the source's basename by codex when
39
- // `codex plugin marketplace add <source>` runs without a --name flag. For
40
- // owner/repo this is the repo name; for a local path it's the directory
41
- // basename. Both resolve to "nlm-memory-ts" in our case.
42
- const MARKETPLACE_NAME = "nlm-memory-ts";
43
-
44
- // Marker substring identifying entries this CLI owns in ~/.codex/hooks.json
45
- // so disconnect can strip only our entries and leave anything the user
46
- // added by hand intact.
47
- const LEGACY_HOOK_MARKER = "/plugin/scripts/";
48
-
49
- // Sentinels bracketing the [mcp_servers.nlm-memory] block we manage in
50
- // ~/.codex/config.toml. Sentinel-bracketed regions are removed atomically
51
- // on disconnect and replaced atomically on connect — no TOML parser
52
- // required, no risk of mangling user-authored entries above or below.
53
- const MCP_SENTINEL_BEGIN = "# >>> nlm-memory (managed by nlm connect codex)";
54
- const MCP_SENTINEL_END = "# <<< nlm-memory";
55
-
56
- export interface ConnectOptions {
57
- readonly source?: string;
58
- readonly withHooks?: boolean;
59
- readonly dryRun?: boolean;
60
- }
61
-
62
- export interface DisconnectOptions {
63
- readonly withHooks?: boolean;
64
- readonly dryRun?: boolean;
65
- }
66
-
67
- export interface CodexCommandResult {
68
- readonly status: number | null;
69
- readonly stdout: string;
70
- readonly stderr: string;
71
- }
72
-
73
- function runCodex(args: ReadonlyArray<string>): CodexCommandResult {
74
- const result = spawnSync("codex", args, { encoding: "utf8" });
75
- return {
76
- status: result.status,
77
- stdout: result.stdout ?? "",
78
- stderr: result.stderr ?? "",
79
- };
80
- }
81
-
82
- export function codexBinaryAvailable(): boolean {
83
- const r = spawnSync("codex", ["--version"], { encoding: "utf8" });
84
- return r.status === 0;
85
- }
86
-
87
- export function codexHooksPath(): string {
88
- return process.env["NLM_CODEX_HOOKS"] ?? join(homedir(), ".codex", "hooks.json");
89
- }
90
-
91
- export function codexConfigPath(): string {
92
- return process.env["NLM_CODEX_CONFIG"] ?? join(homedir(), ".codex", "config.toml");
93
- }
94
-
95
- /**
96
- * Idempotently insert (or update) the [mcp_servers.nlm-memory] block in
97
- * ~/.codex/config.toml. The block is bracketed by sentinel comments so a
98
- * later disconnect can strip the exact region without touching anything
99
- * else. MCP wiring is universal infrastructure — every runtime gets its
100
- * MCP server registered in its native format. Codex's is TOML in
101
- * config.toml; we write that directly rather than relying on the plugin
102
- * system's .mcp.json indirection (which we can't currently verify works
103
- * outside the upstream plugin pipeline).
104
- */
105
- export function writeMcpServerToConfig(configPath: string): void {
106
- mkdirSync(dirname(configPath), { recursive: true });
107
- const existing = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
108
-
109
- const block = `${MCP_SENTINEL_BEGIN}\n[mcp_servers.nlm-memory]\ncommand = "nlm"\nargs = ["mcp"]\n${MCP_SENTINEL_END}\n`;
110
-
111
- const next = stripSentinelBlock(existing);
112
- const sep = next.length > 0 && !next.endsWith("\n\n") ? (next.endsWith("\n") ? "\n" : "\n\n") : "";
113
- writeFileSync(configPath, next + sep + block, "utf8");
114
- }
115
-
116
- export function removeMcpServerFromConfig(configPath: string): boolean {
117
- if (!existsSync(configPath)) return false;
118
- const existing = readFileSync(configPath, "utf8");
119
- const next = stripSentinelBlock(existing);
120
- if (next === existing) return false;
121
- writeFileSync(configPath, next, "utf8");
122
- return true;
123
- }
124
-
125
- /**
126
- * Remove our sentinel-bracketed region from a config.toml string. Tolerant
127
- * of an unterminated begin sentinel (treats it as a no-op rather than
128
- * eating the rest of the file) so a corrupted config never amplifies.
129
- */
130
- function stripSentinelBlock(content: string): string {
131
- const beginIdx = content.indexOf(MCP_SENTINEL_BEGIN);
132
- if (beginIdx < 0) return content;
133
- const endMarker = MCP_SENTINEL_END;
134
- const endIdx = content.indexOf(endMarker, beginIdx + MCP_SENTINEL_BEGIN.length);
135
- if (endIdx < 0) return content; // unterminated — refuse to mutate
136
- let cutEnd = endIdx + endMarker.length;
137
- if (content[cutEnd] === "\n") cutEnd += 1;
138
- let cutStart = beginIdx;
139
- // Also eat the single leading newline that connected this block to the
140
- // prior section, so repeated connect/disconnect cycles don't accrete blanks.
141
- if (cutStart > 0 && content[cutStart - 1] === "\n") cutStart -= 1;
142
- return content.slice(0, cutStart) + content.slice(cutEnd);
143
- }
144
-
145
- interface CodexHookEntry {
146
- readonly type: string;
147
- readonly command: string;
148
- readonly statusMessage?: string;
149
- }
150
-
151
- interface CodexHookGroup {
152
- readonly matcher?: string;
153
- readonly hooks: CodexHookEntry[];
154
- }
155
-
156
- interface CodexHooksFile {
157
- hooks?: Record<string, CodexHookGroup[]>;
158
- [k: string]: unknown;
159
- }
160
-
161
- function readHooksFile(path: string): CodexHooksFile {
162
- if (!existsSync(path)) return {};
163
- try {
164
- return JSON.parse(readFileSync(path, "utf8")) as CodexHooksFile;
165
- } catch {
166
- // Treat a malformed hooks.json as empty rather than silently
167
- // overwriting the user's intent. The legacy writer below merges
168
- // entries — if the file is broken we'd rather error than clobber.
169
- throw new Error(`~/.codex/hooks.json is not valid JSON: ${path}`);
170
- }
171
- }
172
-
173
- function writeHooksFile(path: string, content: CodexHooksFile): void {
174
- mkdirSync(dirname(path), { recursive: true });
175
- writeFileSync(path, JSON.stringify(content, null, 2) + "\n", "utf8");
176
- }
177
-
178
- /**
179
- * Append our two hook entries into ~/.codex/hooks.json without touching any
180
- * pre-existing entries. Idempotent: a second call replaces our entries
181
- * rather than duplicating them (matched by LEGACY_HOOK_MARKER substring).
182
- */
183
- export function writeLegacyHooks(
184
- pluginScriptsDir: string,
185
- hooksPath: string,
186
- ): void {
187
- const file = readHooksFile(hooksPath);
188
- const hooks = (file.hooks ??= {});
189
-
190
- const ourEntries: Record<string, CodexHookGroup> = {
191
- UserPromptSubmit: {
192
- hooks: [
193
- {
194
- type: "command",
195
- command: `node "${join(pluginScriptsDir, "prompt-recall-hook.mjs")}"`,
196
- statusMessage: "nlm-memory: recalling prior sessions",
197
- },
198
- ],
199
- },
200
- Stop: {
201
- hooks: [
202
- {
203
- type: "command",
204
- command: `node "${join(pluginScriptsDir, "stop-hook.mjs")}"`,
205
- },
206
- ],
207
- },
208
- };
209
-
210
- for (const [event, ourGroup] of Object.entries(ourEntries)) {
211
- const existing = hooks[event] ?? [];
212
- const kept = existing.filter(
213
- (group) =>
214
- !group.hooks.some((h) => h.command.includes(LEGACY_HOOK_MARKER)),
215
- );
216
- kept.push(ourGroup);
217
- hooks[event] = kept;
218
- }
219
-
220
- writeHooksFile(hooksPath, file);
221
- }
222
-
223
- export function removeLegacyHooks(hooksPath: string): boolean {
224
- if (!existsSync(hooksPath)) return false;
225
- const file = readHooksFile(hooksPath);
226
- const hooks = file.hooks;
227
- if (!hooks) return false;
228
-
229
- let mutated = false;
230
- for (const [event, groups] of Object.entries(hooks)) {
231
- const kept = groups.filter(
232
- (group) =>
233
- !group.hooks.some((h) => h.command.includes(LEGACY_HOOK_MARKER)),
234
- );
235
- if (kept.length !== groups.length) mutated = true;
236
- if (kept.length === 0) delete hooks[event];
237
- else hooks[event] = kept;
238
- }
239
-
240
- if (mutated) writeHooksFile(hooksPath, file);
241
- return mutated;
242
- }
243
-
244
- export interface ConnectReport {
245
- readonly source: string;
246
- readonly marketplaceName: string;
247
- readonly pluginName: string;
248
- readonly marketplaceAdd: CodexCommandResult | null;
249
- readonly pluginAdd: CodexCommandResult | null;
250
- readonly legacyHooksWritten: string | null;
251
- readonly mcpServerWritten: string | null;
252
- readonly dryRun: boolean;
253
- }
254
-
255
- export function connectCodex(
256
- opts: ConnectOptions,
257
- pluginScriptsDir: string,
258
- ): ConnectReport {
259
- const source = opts.source ?? DEFAULT_SOURCE;
260
- const marketplaceName = MARKETPLACE_NAME;
261
- const pluginName = PLUGIN_NAME;
262
-
263
- if (opts.dryRun) {
264
- return {
265
- source,
266
- marketplaceName,
267
- pluginName,
268
- marketplaceAdd: null,
269
- pluginAdd: null,
270
- legacyHooksWritten: opts.withHooks ? codexHooksPath() : null,
271
- mcpServerWritten: codexConfigPath(),
272
- dryRun: true,
273
- };
274
- }
275
-
276
- // Marketplace add is idempotent at the codex layer; a re-add of the same
277
- // source no-ops or refreshes the snapshot depending on the binary.
278
- const marketplaceAdd = runCodex(["plugin", "marketplace", "add", source]);
279
- // plugin add is the action that triggers the trust-prompt path on first
280
- // run. We let codex's exit code propagate to the caller.
281
- const pluginAdd = runCodex([
282
- "plugin",
283
- "add",
284
- `${pluginName}@${marketplaceName}`,
285
- ]);
286
-
287
- let legacyHooksWritten: string | null = null;
288
- if (opts.withHooks) {
289
- const hooksPath = codexHooksPath();
290
- writeLegacyHooks(pluginScriptsDir, hooksPath);
291
- legacyHooksWritten = hooksPath;
292
- }
293
-
294
- // MCP wiring is always written directly to config.toml — it's the
295
- // universal infrastructure that should work whether or not the plugin
296
- // system honors the bundled .mcp.json indirection.
297
- const configPath = codexConfigPath();
298
- writeMcpServerToConfig(configPath);
299
-
300
- return {
301
- source,
302
- marketplaceName,
303
- pluginName,
304
- marketplaceAdd,
305
- pluginAdd,
306
- legacyHooksWritten,
307
- mcpServerWritten: configPath,
308
- dryRun: false,
309
- };
310
- }
311
-
312
- export interface DisconnectReport {
313
- readonly marketplaceName: string;
314
- readonly pluginName: string;
315
- readonly pluginRemove: CodexCommandResult | null;
316
- readonly marketplaceRemove: CodexCommandResult | null;
317
- readonly legacyHooksRemoved: boolean;
318
- readonly mcpServerRemoved: boolean;
319
- readonly dryRun: boolean;
320
- }
321
-
322
- export function disconnectCodex(opts: DisconnectOptions): DisconnectReport {
323
- if (opts.dryRun) {
324
- return {
325
- marketplaceName: MARKETPLACE_NAME,
326
- pluginName: PLUGIN_NAME,
327
- pluginRemove: null,
328
- marketplaceRemove: null,
329
- legacyHooksRemoved: opts.withHooks ?? false,
330
- mcpServerRemoved: true,
331
- dryRun: true,
332
- };
333
- }
334
-
335
- const pluginRemove = runCodex([
336
- "plugin",
337
- "remove",
338
- `${PLUGIN_NAME}@${MARKETPLACE_NAME}`,
339
- ]);
340
- const marketplaceRemove = runCodex([
341
- "plugin",
342
- "marketplace",
343
- "remove",
344
- MARKETPLACE_NAME,
345
- ]);
346
-
347
- let legacyHooksRemoved = false;
348
- if (opts.withHooks) {
349
- legacyHooksRemoved = removeLegacyHooks(codexHooksPath());
350
- }
351
-
352
- const mcpServerRemoved = removeMcpServerFromConfig(codexConfigPath());
353
-
354
- return {
355
- marketplaceName: MARKETPLACE_NAME,
356
- pluginName: PLUGIN_NAME,
357
- pluginRemove,
358
- marketplaceRemove,
359
- legacyHooksRemoved,
360
- mcpServerRemoved,
361
- dryRun: false,
362
- };
363
- }
364
-
365
- export function pluginScriptsDir(repoRoot: string): string {
366
- return resolve(repoRoot, "plugin", "scripts");
367
- }
@@ -1,76 +0,0 @@
1
- /**
2
- * `nlm connect hermes-agent` / `nlm disconnect hermes-agent` — installs the
3
- * nlm-memory plugin into NousResearch Hermes Agent's plugin directory and
4
- * optionally enables it via the `hermes` binary.
5
- *
6
- * The plugin lives in plugin-hermes-agent/ at the repo root. `connect`
7
- * copies it to ~/.hermes/plugins/nlm-memory/ (flat layout, one category
8
- * level max per Hermes plugin discovery rules). `disconnect` removes that
9
- * directory.
10
- *
11
- * MCP server wiring (the [mcp_servers.nlm-memory] block in
12
- * ~/.hermes/config.yaml) is handled separately by `nlm connect hermes`.
13
- */
14
-
15
- import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
16
- import { homedir } from "node:os";
17
- import { dirname, join } from "node:path";
18
- import { spawnSync } from "node:child_process";
19
-
20
- export interface ConnectHermesAgentOptions {
21
- readonly pluginSrcDir: string;
22
- readonly dryRun?: boolean;
23
- readonly enableViaCliIfAvailable?: boolean;
24
- }
25
-
26
- export interface ConnectHermesAgentReport {
27
- readonly destDir: string;
28
- readonly copied: boolean;
29
- readonly alreadyPresent: boolean;
30
- readonly enabledViaCli: boolean;
31
- readonly dryRun: boolean;
32
- }
33
-
34
- export interface DisconnectHermesAgentReport {
35
- readonly destDir: string;
36
- readonly removed: boolean;
37
- readonly dryRun: boolean;
38
- }
39
-
40
- export function hermesAgentPluginDir(): string {
41
- return process.env["NLM_HERMES_PLUGIN_DIR"] ?? join(homedir(), ".hermes", "plugins", "nlm-memory");
42
- }
43
-
44
- export function connectHermesAgent(opts: ConnectHermesAgentOptions): ConnectHermesAgentReport {
45
- const destDir = hermesAgentPluginDir();
46
- const alreadyPresent = existsSync(destDir);
47
-
48
- if (!opts.dryRun) {
49
- mkdirSync(dirname(destDir), { recursive: true });
50
- cpSync(opts.pluginSrcDir, destDir, { recursive: true });
51
-
52
- let enabledViaCli = false;
53
- if (opts.enableViaCliIfAvailable !== false) {
54
- const result = spawnSync("hermes", ["plugins", "enable", "nlm-memory"], {
55
- encoding: "utf8",
56
- timeout: 10_000,
57
- });
58
- enabledViaCli = result.status === 0;
59
- }
60
- return { destDir, copied: true, alreadyPresent, enabledViaCli, dryRun: false };
61
- }
62
-
63
- return { destDir, copied: false, alreadyPresent, enabledViaCli: false, dryRun: true };
64
- }
65
-
66
- export function disconnectHermesAgent(opts?: { dryRun?: boolean }): DisconnectHermesAgentReport {
67
- const destDir = hermesAgentPluginDir();
68
- const present = existsSync(destDir);
69
-
70
- if (!opts?.dryRun && present) {
71
- rmSync(destDir, { recursive: true, force: true });
72
- return { destDir, removed: true, dryRun: false };
73
- }
74
-
75
- return { destDir, removed: false, dryRun: opts?.dryRun ?? false };
76
- }
@@ -1,78 +0,0 @@
1
- /**
2
- * `nlm connect hermes` / `nlm disconnect hermes` — writes the nlm-memory
3
- * MCP server entry into ~/.hermes/config.yaml.
4
- *
5
- * Uses yaml's Document API (parseDocument / doc.setIn / doc.toString) to
6
- * preserve any comments the user has written in their config file. Round-
7
- * tripping through parse+stringify would silently destroy comments.
8
- */
9
-
10
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
- import { homedir } from "node:os";
12
- import { dirname, join } from "node:path";
13
- import { Document as YamlDocument, parseDocument as parseYamlDocument } from "yaml";
14
-
15
- export interface ConnectHermesOptions {
16
- readonly nlmBinPath: string;
17
- readonly nodeExecPath: string;
18
- readonly dryRun?: boolean;
19
- }
20
-
21
- export interface ConnectHermesReport {
22
- readonly configPath: string;
23
- readonly alreadyPresent: boolean;
24
- readonly written: boolean;
25
- readonly dryRun: boolean;
26
- }
27
-
28
- export interface DisconnectHermesReport {
29
- readonly configPath: string;
30
- readonly removed: boolean;
31
- readonly dryRun: boolean;
32
- }
33
-
34
- export function hermesConfigPath(): string {
35
- return process.env["NLM_HERMES_CONFIG"] ?? join(homedir(), ".hermes", "config.yaml");
36
- }
37
-
38
- function readDocument(path: string): YamlDocument {
39
- if (!existsSync(path)) return new YamlDocument();
40
- try {
41
- return parseYamlDocument(readFileSync(path, "utf8"));
42
- } catch {
43
- throw new Error(`${path} is not valid YAML. Fix or remove it, then re-run \`nlm connect hermes\`.`);
44
- }
45
- }
46
-
47
- export function connectHermes(opts: ConnectHermesOptions): ConnectHermesReport {
48
- const configPath = hermesConfigPath();
49
- const doc = readDocument(configPath);
50
- const alreadyPresent = doc.getIn(["mcp_servers", "nlm-memory"]) !== undefined;
51
-
52
- if (!opts.dryRun) {
53
- doc.setIn(["mcp_servers", "nlm-memory"], {
54
- command: opts.nodeExecPath,
55
- args: [opts.nlmBinPath, "mcp"],
56
- });
57
- mkdirSync(dirname(configPath), { recursive: true });
58
- writeFileSync(configPath, doc.toString(), "utf8");
59
- }
60
-
61
- return { configPath, alreadyPresent, written: !opts.dryRun, dryRun: opts.dryRun ?? false };
62
- }
63
-
64
- export function disconnectHermes(opts?: { dryRun?: boolean }): DisconnectHermesReport {
65
- const configPath = hermesConfigPath();
66
- const doc = readDocument(configPath);
67
-
68
- if (doc.getIn(["mcp_servers", "nlm-memory"]) === undefined) {
69
- return { configPath, removed: false, dryRun: opts?.dryRun ?? false };
70
- }
71
-
72
- if (!opts?.dryRun) {
73
- doc.deleteIn(["mcp_servers", "nlm-memory"]);
74
- writeFileSync(configPath, doc.toString(), "utf8");
75
- }
76
-
77
- return { configPath, removed: true, dryRun: opts?.dryRun ?? false };
78
- }