nlm-memory 0.5.0 → 0.5.2

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 (257) hide show
  1. package/README.md +89 -34
  2. package/dist/cli/digest.d.ts +20 -0
  3. package/dist/cli/digest.js +142 -0
  4. package/dist/cli/digest.js.map +1 -0
  5. package/dist/cli/nlm.d.ts +1 -0
  6. package/dist/cli/nlm.js +25 -1
  7. package/dist/cli/nlm.js.map +1 -1
  8. package/dist/core/digest/compose.d.ts +38 -0
  9. package/dist/core/digest/compose.js +93 -0
  10. package/dist/core/digest/compose.js.map +1 -0
  11. package/dist/core/digest/hook-liveness.d.ts +32 -0
  12. package/dist/core/digest/hook-liveness.js +54 -0
  13. package/dist/core/digest/hook-liveness.js.map +1 -0
  14. package/dist/http/app.js +2 -1
  15. package/dist/http/app.js.map +1 -1
  16. package/dist/mcp/server.js +20 -1
  17. package/dist/mcp/server.js.map +1 -1
  18. package/dist/ui/assets/{index-C8cpwbYJ.css → index-Beo8psd-.css} +1 -1
  19. package/dist/ui/assets/{index-CB50QnL-.js → index-CSPTTeeM.js} +8 -8
  20. package/dist/ui/index.html +2 -2
  21. package/package.json +26 -1
  22. package/.agents/plugins/marketplace.json +0 -20
  23. package/.github/workflows/ci.yml +0 -30
  24. package/docs/methodology/re-derivation-rate.md +0 -112
  25. package/docs/methodology/useful-hit-rate.md +0 -79
  26. package/docs/plans/2026-05-20-fts5-lexical-recall.md +0 -1088
  27. package/docs/plans/2026-05-20-recall-daemon-wedge-fix.md +0 -662
  28. package/docs/plans/2026-05-20-recall-hook-design.md +0 -131
  29. package/docs/plans/2026-05-20-recall-hook-implementation.md +0 -1222
  30. package/docs/plans/desktop-product.md +0 -69
  31. package/docs/plans/factstore-design.md +0 -236
  32. package/logs/CHANGELOG/CHANGELOG-2026.md +0 -1575
  33. package/logs/CHANGELOG/CHANGELOG.md +0 -209
  34. package/migrations/000_initial_schema.sql +0 -174
  35. package/migrations/001_entity_type_rename.sql +0 -17
  36. package/migrations/002_adapter_state_extend.sql +0 -12
  37. package/migrations/003_session_embeddings.sql +0 -11
  38. package/migrations/004_facts.sql +0 -46
  39. package/migrations/005_sources.sql +0 -31
  40. package/migrations/006_providers.sql +0 -33
  41. package/migrations/007_source_tokens.sql +0 -17
  42. package/migrations/008_fts_rebuild.sql +0 -9
  43. package/migrations/009_session_embedding_chunks.sql +0 -46
  44. package/migrations/010_sources_opencode.sql +0 -30
  45. package/migrations/011_sources_hermes_agent.sql +0 -30
  46. package/migrations/012_sources_aider.sql +0 -30
  47. package/migrations/013_adapter_state_failure_count.sql +0 -12
  48. package/migrations/014_sources_cursor.sql +0 -30
  49. package/migrations/015_sources_windsurf.sql +0 -30
  50. package/plugin-hermes-agent/README.md +0 -49
  51. package/plugin-hermes-agent/__init__.py +0 -75
  52. package/plugin-hermes-agent/plugin.yaml +0 -15
  53. package/scripts/backfill-citations.mjs +0 -0
  54. package/scripts/build-codex-plugin.mjs +0 -61
  55. package/scripts/deepseek-probe.mjs +0 -67
  56. package/scripts/extract-triples.mjs +0 -207
  57. package/scripts/longmemeval/embedding-cache.ts +0 -77
  58. package/scripts/longmemeval/fetch-dataset.sh +0 -25
  59. package/scripts/longmemeval/run-harness.ts +0 -315
  60. package/scripts/longmemeval/scorer.ts +0 -99
  61. package/scripts/longmemeval/tsconfig.json +0 -9
  62. package/scripts/longmemeval/types.ts +0 -35
  63. package/scripts/nlm-daily-digest.py +0 -239
  64. package/scripts/nlm-daily-digest.sh +0 -28
  65. package/src/cli/classify-parity.ts +0 -257
  66. package/src/cli/launchctl-helpers.ts +0 -49
  67. package/src/cli/nlm.ts +0 -1078
  68. package/src/core/actions/actions-log.ts +0 -118
  69. package/src/core/actions/overlay.ts +0 -117
  70. package/src/core/adapters/aider.ts +0 -205
  71. package/src/core/adapters/claude-code.ts +0 -293
  72. package/src/core/adapters/common.ts +0 -54
  73. package/src/core/adapters/cursor.ts +0 -486
  74. package/src/core/adapters/from-source.ts +0 -67
  75. package/src/core/adapters/hermes-agent.ts +0 -240
  76. package/src/core/adapters/hermes.ts +0 -277
  77. package/src/core/adapters/jsonl-generic.ts +0 -208
  78. package/src/core/adapters/opencode.ts +0 -281
  79. package/src/core/adapters/pi.ts +0 -264
  80. package/src/core/adapters/windsurf.ts +0 -386
  81. package/src/core/classifier/prompt.ts +0 -200
  82. package/src/core/dataset/build-dataset.ts +0 -463
  83. package/src/core/embedding/chunk-body.ts +0 -76
  84. package/src/core/embedding/embed-backfill.ts +0 -210
  85. package/src/core/embedding/embed-normalize.ts +0 -135
  86. package/src/core/facts/backfill-facts.ts +0 -254
  87. package/src/core/facts/extract-facts.ts +0 -50
  88. package/src/core/hook/citation-detect.ts +0 -124
  89. package/src/core/hook/cite-memo.ts +0 -68
  90. package/src/core/hook/claude-settings.ts +0 -187
  91. package/src/core/hook/gate.ts +0 -25
  92. package/src/core/hook/hook-log.ts +0 -41
  93. package/src/core/hook/memo-sweep.ts +0 -164
  94. package/src/core/hook/memo.ts +0 -67
  95. package/src/core/hook/pointer-block.ts +0 -26
  96. package/src/core/hook/select.ts +0 -32
  97. package/src/core/hook/transcript.ts +0 -121
  98. package/src/core/ingest/ingest-session.ts +0 -111
  99. package/src/core/providers/provider-models.ts +0 -100
  100. package/src/core/providers/provider-registry.ts +0 -196
  101. package/src/core/recall/citation-log.ts +0 -108
  102. package/src/core/recall/filter.ts +0 -27
  103. package/src/core/recall/index.ts +0 -6
  104. package/src/core/recall/match-fields.ts +0 -40
  105. package/src/core/recall/query-log.ts +0 -149
  106. package/src/core/recall/query-shape.ts +0 -66
  107. package/src/core/recall/recall-service.ts +0 -320
  108. package/src/core/recall/recent-log.ts +0 -59
  109. package/src/core/recall/tokenize.ts +0 -18
  110. package/src/core/recall/useful-scan.ts +0 -336
  111. package/src/core/recall-facts/fact-query-log.ts +0 -150
  112. package/src/core/recall-facts/fact-recall-service.ts +0 -327
  113. package/src/core/scheduler/scan-once.ts +0 -142
  114. package/src/core/scheduler/scheduler.ts +0 -225
  115. package/src/core/sources/source-registry.ts +0 -278
  116. package/src/core/storage/db-restore.ts +0 -133
  117. package/src/core/storage/live-status.ts +0 -45
  118. package/src/core/storage/migrate.ts +0 -72
  119. package/src/core/storage/sqlite-fact-store.ts +0 -304
  120. package/src/core/storage/sqlite-session-store.ts +0 -810
  121. package/src/hook/hook-auth.ts +0 -18
  122. package/src/hook/prompt-recall-hook.ts +0 -180
  123. package/src/hook/session-end-hook.ts +0 -81
  124. package/src/hook/session-start-hook.ts +0 -168
  125. package/src/hook/stop-hook.ts +0 -239
  126. package/src/http/app.ts +0 -1215
  127. package/src/install/claude-code.ts +0 -128
  128. package/src/install/codex.ts +0 -367
  129. package/src/install/cursor.ts +0 -68
  130. package/src/install/hermes-agent.ts +0 -76
  131. package/src/install/hermes.ts +0 -78
  132. package/src/install/nlm-dir-perms.ts +0 -55
  133. package/src/install/ollama.ts +0 -284
  134. package/src/install/setup.ts +0 -489
  135. package/src/install/windsurf.ts +0 -68
  136. package/src/llm/classifier-box.ts +0 -64
  137. package/src/llm/deepseek-client.ts +0 -150
  138. package/src/llm/env-autoload.ts +0 -55
  139. package/src/llm/ollama-client.ts +0 -189
  140. package/src/mcp/server.ts +0 -534
  141. package/src/ports/fact-store.ts +0 -102
  142. package/src/ports/llm-client.ts +0 -52
  143. package/src/ports/logger.ts +0 -16
  144. package/src/ports/session-store.ts +0 -45
  145. package/src/ports/transcript-adapter.ts +0 -55
  146. package/src/shared/types.ts +0 -149
  147. package/src/ui/App.tsx +0 -58
  148. package/src/ui/components/PromoteOpenButton.tsx +0 -65
  149. package/src/ui/components/SessionDrawer.tsx +0 -199
  150. package/src/ui/components/SideNav.tsx +0 -162
  151. package/src/ui/components/Skeleton.tsx +0 -107
  152. package/src/ui/index.html +0 -13
  153. package/src/ui/lib/actions.ts +0 -30
  154. package/src/ui/lib/api.ts +0 -92
  155. package/src/ui/lib/dataset.ts +0 -141
  156. package/src/ui/lib/registries.ts +0 -155
  157. package/src/ui/lib/view-settings.ts +0 -41
  158. package/src/ui/main.tsx +0 -15
  159. package/src/ui/pages/Live.tsx +0 -229
  160. package/src/ui/pages/Pulse.tsx +0 -415
  161. package/src/ui/pages/Recall.tsx +0 -190
  162. package/src/ui/pages/River.tsx +0 -354
  163. package/src/ui/pages/Search.tsx +0 -386
  164. package/src/ui/pages/Stub.tsx +0 -9
  165. package/src/ui/pages/Thread.tsx +0 -473
  166. package/src/ui/pages/settings/Classifier.tsx +0 -227
  167. package/src/ui/pages/settings/Data.tsx +0 -190
  168. package/src/ui/pages/settings/Index.tsx +0 -65
  169. package/src/ui/pages/settings/Labels.tsx +0 -224
  170. package/src/ui/pages/settings/Providers.tsx +0 -305
  171. package/src/ui/pages/settings/SettingsSubnav.tsx +0 -28
  172. package/src/ui/pages/settings/Sources.tsx +0 -326
  173. package/src/ui/pages/settings/Views.tsx +0 -96
  174. package/src/ui/styles.css +0 -1890
  175. package/src/ui/tsconfig.json +0 -21
  176. package/src/ui/vite.config.ts +0 -19
  177. package/tests/fixtures/claude_code/short_session.jsonl +0 -2
  178. package/tests/fixtures/claude_code/standard_iso.jsonl +0 -4
  179. package/tests/fixtures/claude_code/tool_heavy.jsonl +0 -8
  180. package/tests/fixtures/claude_code/with_subagent.jsonl +0 -7
  181. package/tests/fixtures/facts.ts +0 -17
  182. package/tests/fixtures/golden-corpus.ts +0 -85
  183. package/tests/fixtures/hermes/paired_request_dump.json +0 -24
  184. package/tests/fixtures/hermes/paired_session.json +0 -23
  185. package/tests/fixtures/hermes/request_dump.json +0 -28
  186. package/tests/fixtures/hermes/session_iso.json +0 -38
  187. package/tests/fixtures/hermes/session_unix.json +0 -38
  188. package/tests/fixtures/hermes/system_only.json +0 -18
  189. package/tests/fixtures/pi/error-connection-abort.jsonl +0 -8
  190. package/tests/fixtures/pi/short-successful.jsonl +0 -5
  191. package/tests/fixtures/pi/with-custom-message.jsonl +0 -6
  192. package/tests/fixtures/sessions.ts +0 -22
  193. package/tests/integration/backfill-facts.test.ts +0 -362
  194. package/tests/integration/citation-explicit.test.ts +0 -111
  195. package/tests/integration/cite-event.test.ts +0 -169
  196. package/tests/integration/cite-memo.test.ts +0 -87
  197. package/tests/integration/db-restore.test.ts +0 -153
  198. package/tests/integration/embed-backfill.test.ts +0 -176
  199. package/tests/integration/fact-supersedence.test.ts +0 -313
  200. package/tests/integration/fts-index.test.ts +0 -60
  201. package/tests/integration/getbyids-sqlite.test.ts +0 -100
  202. package/tests/integration/hermes-agent-hooks.test.ts +0 -248
  203. package/tests/integration/hook-claude-settings.test.ts +0 -218
  204. package/tests/integration/hook-log.test.ts +0 -54
  205. package/tests/integration/hook-memo.test.ts +0 -68
  206. package/tests/integration/hook-pre-compact.test.ts +0 -105
  207. package/tests/integration/hook-subagent-start.test.ts +0 -102
  208. package/tests/integration/http.test.ts +0 -401
  209. package/tests/integration/keyword-search-fts.test.ts +0 -66
  210. package/tests/integration/mcp-recall-logging.test.ts +0 -88
  211. package/tests/integration/mcp.test.ts +0 -260
  212. package/tests/integration/memo-sweep.test.ts +0 -91
  213. package/tests/integration/prompt-recall-hook.test.ts +0 -88
  214. package/tests/integration/provider-registry.test.ts +0 -107
  215. package/tests/integration/recall-golden.test.ts +0 -59
  216. package/tests/integration/recall-sqlite.test.ts +0 -169
  217. package/tests/integration/scheduler.test.ts +0 -391
  218. package/tests/integration/session-end-hook.test.ts +0 -48
  219. package/tests/integration/session-start-hook.test.ts +0 -126
  220. package/tests/integration/source-registry.test.ts +0 -122
  221. package/tests/integration/sqlite-fact-store.test.ts +0 -346
  222. package/tests/integration/stop-hook.test.ts +0 -560
  223. package/tests/integration/wal-checkpoint.test.ts +0 -49
  224. package/tests/unit/cli/launchctl-helpers.test.ts +0 -60
  225. package/tests/unit/core/adapters/aider.test.ts +0 -230
  226. package/tests/unit/core/adapters/claude-code.test.ts +0 -118
  227. package/tests/unit/core/adapters/cursor.test.ts +0 -485
  228. package/tests/unit/core/adapters/hermes-agent.test.ts +0 -329
  229. package/tests/unit/core/adapters/hermes.test.ts +0 -81
  230. package/tests/unit/core/adapters/jsonl-generic.test.ts +0 -142
  231. package/tests/unit/core/adapters/opencode.test.ts +0 -354
  232. package/tests/unit/core/adapters/pi.test.ts +0 -110
  233. package/tests/unit/core/adapters/windsurf.test.ts +0 -416
  234. package/tests/unit/core/classifier/prompt.test.ts +0 -126
  235. package/tests/unit/core/embedding/chunk-body.test.ts +0 -100
  236. package/tests/unit/core/facts/extract-facts.test.ts +0 -117
  237. package/tests/unit/core/filter.test.ts +0 -40
  238. package/tests/unit/core/hook/citation-detect-cite-session.test.ts +0 -96
  239. package/tests/unit/core/hook/citation-detect.test.ts +0 -124
  240. package/tests/unit/core/hook/gate.test.ts +0 -29
  241. package/tests/unit/core/hook/pointer-block.test.ts +0 -22
  242. package/tests/unit/core/hook/select.test.ts +0 -66
  243. package/tests/unit/core/match-fields.test.ts +0 -39
  244. package/tests/unit/core/mcp-cite-session.test.ts +0 -51
  245. package/tests/unit/core/providers/provider-models.test.ts +0 -101
  246. package/tests/unit/core/query-shape.test.ts +0 -92
  247. package/tests/unit/core/recall-facts/fact-recall-service.test.ts +0 -258
  248. package/tests/unit/core/recall-service.test.ts +0 -200
  249. package/tests/unit/core/storage/live-status.test.ts +0 -54
  250. package/tests/unit/core/tokenize.test.ts +0 -32
  251. package/tests/unit/core/useful-scan.test.ts +0 -537
  252. package/tests/unit/llm/embed.test.ts +0 -93
  253. package/tests/unit/llm/ollama-client.test.ts +0 -124
  254. package/tests/unit/scripts/longmemeval-scorer.test.ts +0 -114
  255. package/tsconfig.json +0 -31
  256. package/tsconfig.test.json +0 -11
  257. 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, "live");
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,68 +0,0 @@
1
- /**
2
- * `nlm connect cursor` / `nlm disconnect cursor` — registers or removes the
3
- * Cursor adapter source in the NLM source registry.
4
- *
5
- * Unlike plugin-based runtimes (hermes-agent, codex), Cursor needs no file
6
- * to be installed. NLM reads Cursor's existing state.vscdb directly. The
7
- * connect operation only registers the source row so the daemon scans it.
8
- */
9
-
10
- import { existsSync } from "node:fs";
11
- import { defaultDbPath } from "../core/adapters/cursor.js";
12
- import type { SourceRegistry } from "../core/sources/source-registry.js";
13
-
14
- export interface ConnectCursorOptions {
15
- readonly dbPath?: string;
16
- readonly dryRun?: boolean;
17
- }
18
-
19
- export interface ConnectCursorReport {
20
- readonly adapterDbPath: string;
21
- readonly adapterExists: boolean;
22
- readonly action: "created" | "enabled" | "already-active" | "dry-run";
23
- }
24
-
25
- export interface DisconnectCursorReport {
26
- readonly action: "disabled" | "not-found" | "dry-run";
27
- }
28
-
29
- export function connectCursor(
30
- registry: SourceRegistry,
31
- opts: ConnectCursorOptions = {},
32
- ): ConnectCursorReport {
33
- const adapterDbPath = opts.dbPath ?? defaultDbPath();
34
- const adapterExists = existsSync(adapterDbPath);
35
-
36
- if (opts.dryRun) {
37
- return { adapterDbPath, adapterExists, action: "dry-run" };
38
- }
39
-
40
- const existing = registry.getByName("Cursor");
41
- if (existing) {
42
- if (existing.enabled && existing.pathOrUrl === adapterDbPath) {
43
- return { adapterDbPath, adapterExists, action: "already-active" };
44
- }
45
- registry.update(existing.id, { enabled: true, pathOrUrl: adapterDbPath });
46
- return { adapterDbPath, adapterExists, action: "enabled" };
47
- }
48
-
49
- registry.insert({
50
- kind: "cursor",
51
- name: "Cursor",
52
- pathOrUrl: adapterDbPath,
53
- runtimeLabel: "cursor/1.0",
54
- enabled: adapterExists,
55
- });
56
- return { adapterDbPath, adapterExists, action: "created" };
57
- }
58
-
59
- export function disconnectCursor(
60
- registry: SourceRegistry,
61
- opts: { dryRun?: boolean } = {},
62
- ): DisconnectCursorReport {
63
- if (opts.dryRun) return { action: "dry-run" };
64
- const existing = registry.getByName("Cursor");
65
- if (!existing) return { action: "not-found" };
66
- registry.update(existing.id, { enabled: false });
67
- return { action: "disabled" };
68
- }
@@ -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
- }