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