nlm-memory 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/nlm.js +221 -32
- package/dist/cli/nlm.js.map +1 -1
- package/dist/core/adapters/cursor.d.ts +45 -0
- package/dist/core/adapters/cursor.js +397 -0
- package/dist/core/adapters/cursor.js.map +1 -0
- package/dist/core/adapters/from-source.js +10 -0
- package/dist/core/adapters/from-source.js.map +1 -1
- package/dist/core/adapters/windsurf.d.ts +44 -0
- package/dist/core/adapters/windsurf.js +299 -0
- package/dist/core/adapters/windsurf.js.map +1 -0
- package/dist/core/hook/claude-settings.d.ts +12 -5
- package/dist/core/hook/claude-settings.js +21 -6
- package/dist/core/hook/claude-settings.js.map +1 -1
- package/dist/core/sources/source-registry.d.ts +1 -1
- package/dist/core/sources/source-registry.js +18 -0
- package/dist/core/sources/source-registry.js.map +1 -1
- package/dist/core/storage/sqlite-session-store.d.ts +2 -0
- package/dist/core/storage/sqlite-session-store.js +38 -2
- package/dist/core/storage/sqlite-session-store.js.map +1 -1
- package/dist/hook/hook-auth.d.ts +13 -0
- package/dist/hook/hook-auth.js +19 -0
- package/dist/hook/hook-auth.js.map +1 -0
- package/dist/hook/prompt-recall-hook.js +7 -1
- package/dist/hook/prompt-recall-hook.js.map +1 -1
- package/dist/hook/session-start-hook.js +4 -1
- package/dist/hook/session-start-hook.js.map +1 -1
- package/dist/hook/stop-hook.js +4 -1
- package/dist/hook/stop-hook.js.map +1 -1
- package/dist/http/app.d.ts +2 -0
- package/dist/http/app.js +74 -0
- package/dist/http/app.js.map +1 -1
- package/dist/install/claude-code.js +1 -1
- package/dist/install/claude-code.js.map +1 -1
- package/dist/install/cursor.d.ts +25 -0
- package/dist/install/cursor.js +43 -0
- package/dist/install/cursor.js.map +1 -0
- package/dist/install/nlm-dir-perms.d.ts +19 -0
- package/dist/install/nlm-dir-perms.js +43 -0
- package/dist/install/nlm-dir-perms.js.map +1 -0
- package/dist/install/ollama.d.ts +18 -1
- package/dist/install/ollama.js +62 -7
- package/dist/install/ollama.js.map +1 -1
- package/dist/install/setup.d.ts +4 -0
- package/dist/install/setup.js +141 -18
- package/dist/install/setup.js.map +1 -1
- package/dist/install/windsurf.d.ts +25 -0
- package/dist/install/windsurf.js +43 -0
- package/dist/install/windsurf.js.map +1 -0
- package/dist/shared/types.d.ts +4 -0
- package/dist/ui/assets/{index-BA6IpU8g.css → index-C8cpwbYJ.css} +1 -1
- package/dist/ui/assets/index-CB50QnL-.js +69 -0
- package/dist/ui/index.html +2 -2
- package/logs/CHANGELOG/CHANGELOG-2026.md +186 -0
- package/logs/CHANGELOG/CHANGELOG.md +107 -235
- package/migrations/014_sources_cursor.sql +30 -0
- package/migrations/015_sources_windsurf.sql +30 -0
- package/package.json +1 -1
- package/plugin/scripts/prompt-recall-hook.mjs +55 -4
- package/plugin/scripts/stop-hook.mjs +57 -6
- package/src/cli/nlm.ts +224 -31
- package/src/core/adapters/cursor.ts +486 -0
- package/src/core/adapters/from-source.ts +10 -0
- package/src/core/adapters/windsurf.ts +386 -0
- package/src/core/hook/claude-settings.ts +30 -9
- package/src/core/sources/source-registry.ts +19 -1
- package/src/core/storage/sqlite-session-store.ts +46 -1
- package/src/hook/hook-auth.ts +18 -0
- package/src/hook/prompt-recall-hook.ts +7 -1
- package/src/hook/session-start-hook.ts +4 -1
- package/src/hook/stop-hook.ts +4 -1
- package/src/http/app.ts +78 -0
- package/src/install/claude-code.ts +1 -1
- package/src/install/cursor.ts +68 -0
- package/src/install/nlm-dir-perms.ts +55 -0
- package/src/install/ollama.ts +80 -7
- package/src/install/setup.ts +138 -17
- package/src/install/windsurf.ts +68 -0
- package/src/shared/types.ts +4 -0
- package/src/ui/components/SessionDrawer.tsx +97 -34
- package/src/ui/pages/River.tsx +90 -44
- package/src/ui/pages/Search.tsx +357 -64
- package/src/ui/pages/Thread.tsx +267 -56
- package/src/ui/styles.css +129 -5
- package/tests/integration/getbyids-sqlite.test.ts +40 -0
- package/tests/integration/hook-claude-settings.test.ts +14 -1
- package/tests/integration/mcp.test.ts +12 -0
- package/tests/integration/source-registry.test.ts +5 -3
- package/tests/unit/core/adapters/cursor.test.ts +485 -0
- package/tests/unit/core/adapters/windsurf.test.ts +416 -0
- package/dist/ui/assets/index-B_qIVV0k.js +0 -69
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/hook/stop-hook.ts
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { appendFileSync, mkdirSync as mkdirSync3 } from "node:fs";
|
|
6
|
-
import { homedir as
|
|
6
|
+
import { homedir as homedir4 } from "node:os";
|
|
7
7
|
import { dirname, join as join3 } from "node:path";
|
|
8
8
|
|
|
9
9
|
// src/core/hook/citation-detect.ts
|
|
@@ -162,6 +162,56 @@ function readAllAssistantTurns(transcriptPath) {
|
|
|
162
162
|
return turns;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
// src/llm/env-autoload.ts
|
|
166
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
|
|
167
|
+
import { homedir as homedir3 } from "node:os";
|
|
168
|
+
import { resolve } from "node:path";
|
|
169
|
+
var DEFAULT_SEARCH_PATHS = [
|
|
170
|
+
"~/.nlm/.env",
|
|
171
|
+
"./.env",
|
|
172
|
+
"../.env",
|
|
173
|
+
"../../.env"
|
|
174
|
+
];
|
|
175
|
+
function expandHome(p) {
|
|
176
|
+
if (p.startsWith("~/")) return resolve(homedir3(), p.slice(2));
|
|
177
|
+
return p;
|
|
178
|
+
}
|
|
179
|
+
function autoloadEnv(extraPaths = []) {
|
|
180
|
+
const loaded = [];
|
|
181
|
+
const paths = [...DEFAULT_SEARCH_PATHS, ...extraPaths];
|
|
182
|
+
for (const raw of paths) {
|
|
183
|
+
const path = expandHome(raw);
|
|
184
|
+
if (!existsSync4(path)) continue;
|
|
185
|
+
try {
|
|
186
|
+
const content = readFileSync4(path, "utf8");
|
|
187
|
+
for (const line of content.split("\n")) {
|
|
188
|
+
const trimmed = line.trim();
|
|
189
|
+
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
190
|
+
const eq = trimmed.indexOf("=");
|
|
191
|
+
const key = trimmed.slice(0, eq).trim();
|
|
192
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
193
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
194
|
+
value = value.slice(1, -1);
|
|
195
|
+
}
|
|
196
|
+
if (key && process.env[key] === void 0) {
|
|
197
|
+
process.env[key] = value;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
loaded.push(path);
|
|
201
|
+
} catch {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return loaded;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/hook/hook-auth.ts
|
|
209
|
+
function hookAuthHeaders(extra = {}) {
|
|
210
|
+
const token = process.env["NLM_MCP_TOKEN"];
|
|
211
|
+
if (!token) return { ...extra };
|
|
212
|
+
return { ...extra, authorization: `Bearer ${token}` };
|
|
213
|
+
}
|
|
214
|
+
|
|
165
215
|
// src/hook/stop-hook.ts
|
|
166
216
|
var RESPONSE_PREVIEW_CHARS = 200;
|
|
167
217
|
var POST_TIMEOUT_MS = 1500;
|
|
@@ -229,7 +279,7 @@ async function runStopHook(input, deps) {
|
|
|
229
279
|
};
|
|
230
280
|
}
|
|
231
281
|
function logPath() {
|
|
232
|
-
return process.env["NLM_HOOK_LOG"] ?? join3(
|
|
282
|
+
return process.env["NLM_HOOK_LOG"] ?? join3(homedir4(), ".nlm", "hook-log.jsonl");
|
|
233
283
|
}
|
|
234
284
|
function logStopResult(result) {
|
|
235
285
|
try {
|
|
@@ -254,12 +304,12 @@ function logStopResult(result) {
|
|
|
254
304
|
}
|
|
255
305
|
}
|
|
256
306
|
function readStdin() {
|
|
257
|
-
return new Promise((
|
|
307
|
+
return new Promise((resolve2) => {
|
|
258
308
|
let data = "";
|
|
259
309
|
process.stdin.setEncoding("utf8");
|
|
260
310
|
process.stdin.on("data", (chunk) => data += chunk);
|
|
261
|
-
process.stdin.on("end", () =>
|
|
262
|
-
process.stdin.on("error", () =>
|
|
311
|
+
process.stdin.on("end", () => resolve2(data));
|
|
312
|
+
process.stdin.on("error", () => resolve2(data));
|
|
263
313
|
});
|
|
264
314
|
}
|
|
265
315
|
async function postCitationOverHttp(conversationId, citedId, kind, responsePreview) {
|
|
@@ -270,7 +320,7 @@ async function postCitationOverHttp(conversationId, citedId, kind, responsePrevi
|
|
|
270
320
|
try {
|
|
271
321
|
await fetch(url, {
|
|
272
322
|
method: "POST",
|
|
273
|
-
headers: { "content-type": "application/json" },
|
|
323
|
+
headers: hookAuthHeaders({ "content-type": "application/json" }),
|
|
274
324
|
body: JSON.stringify({
|
|
275
325
|
conversation_id: conversationId,
|
|
276
326
|
cited_id: citedId,
|
|
@@ -285,6 +335,7 @@ async function postCitationOverHttp(conversationId, citedId, kind, responsePrevi
|
|
|
285
335
|
}
|
|
286
336
|
async function main() {
|
|
287
337
|
try {
|
|
338
|
+
autoloadEnv();
|
|
288
339
|
const raw = await readStdin();
|
|
289
340
|
const payload = JSON.parse(raw);
|
|
290
341
|
const conversationId = typeof payload.session_id === "string" ? payload.session_id : "unknown";
|
package/src/cli/nlm.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { fileURLToPath } from "node:url";
|
|
|
27
27
|
import { dirname, resolve, join } from "node:path";
|
|
28
28
|
import { homedir } from "node:os";
|
|
29
29
|
import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs";
|
|
30
|
-
import { execFileSync } from "node:child_process";
|
|
30
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
31
31
|
import { Command } from "commander";
|
|
32
32
|
import { serve } from "@hono/node-server";
|
|
33
33
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -51,9 +51,13 @@ import {
|
|
|
51
51
|
disconnectCodex,
|
|
52
52
|
pluginScriptsDir,
|
|
53
53
|
} from "../install/codex.js";
|
|
54
|
-
import { connectClaudeCode, disconnectClaudeCode, installClaudeCodeHooks } from "../install/claude-code.js";
|
|
54
|
+
import { connectClaudeCode, disconnectClaudeCode, installClaudeCodeHooks, mcpConfigPath } from "../install/claude-code.js";
|
|
55
|
+
import { hardenNlmDirPermissions } from "../install/nlm-dir-perms.js";
|
|
56
|
+
import { ensureMcpToken } from "../install/ollama.js";
|
|
57
|
+
import { connectCursor, disconnectCursor } from "../install/cursor.js";
|
|
55
58
|
import { connectHermes, disconnectHermes, hermesConfigPath } from "../install/hermes.js";
|
|
56
59
|
import { connectHermesAgent, disconnectHermesAgent, hermesAgentPluginDir } from "../install/hermes-agent.js";
|
|
60
|
+
import { connectWindsurf, disconnectWindsurf } from "../install/windsurf.js";
|
|
57
61
|
import { runSetup } from "../install/setup.js";
|
|
58
62
|
import { runParity } from "./classify-parity.js";
|
|
59
63
|
import { reembedCorpus } from "../core/embedding/embed-backfill.js";
|
|
@@ -164,6 +168,13 @@ program
|
|
|
164
168
|
.option("--no-scheduler", "HTTP only; skip the ingest tick loop")
|
|
165
169
|
.option("--interval-min <n>", "scheduler tick interval (min, default 30)", (v) => Number.parseInt(v, 10), 30)
|
|
166
170
|
.action(async (opts) => {
|
|
171
|
+
// Self-heal perms on every daemon start. Idempotent. Covers upgrade
|
|
172
|
+
// path from pre-v0.4.2 installs where ~/.nlm contents were world-readable.
|
|
173
|
+
hardenNlmDirPermissions();
|
|
174
|
+
// Generate NLM_MCP_TOKEN if missing so /api/* gets Bearer-protected for
|
|
175
|
+
// non-browser callers. Idempotent: re-reads persisted token first.
|
|
176
|
+
autoloadEnv();
|
|
177
|
+
ensureMcpToken();
|
|
167
178
|
const { store, facts, sources, providers, recall, factRecall, embedder, classifier } = buildStack();
|
|
168
179
|
const { existsSync } = await import("node:fs");
|
|
169
180
|
const hasMcpToken = Boolean(process.env["NLM_MCP_TOKEN"]);
|
|
@@ -406,6 +417,40 @@ const LAUNCH_AGENT_PLIST = join(
|
|
|
406
417
|
homedir(), "Library", "LaunchAgents", `${LAUNCH_AGENT_LABEL}.plist`,
|
|
407
418
|
);
|
|
408
419
|
|
|
420
|
+
const LINUX_SYSTEMD_UNIT_NAME = "nlm.service";
|
|
421
|
+
const LINUX_SYSTEMD_UNIT_PATH = join(
|
|
422
|
+
homedir(), ".config", "systemd", "user", LINUX_SYSTEMD_UNIT_NAME,
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
function buildSystemdUnit(nodeExec: string, nlmJs: string): string {
|
|
426
|
+
const logDir = join(homedir(), ".nlm", "logs");
|
|
427
|
+
return `[Unit]
|
|
428
|
+
Description=NLM Memory — local AI session memory daemon
|
|
429
|
+
After=network.target
|
|
430
|
+
|
|
431
|
+
[Service]
|
|
432
|
+
Type=simple
|
|
433
|
+
ExecStart=${nodeExec} ${nlmJs} start
|
|
434
|
+
WorkingDirectory=${homedir()}
|
|
435
|
+
Restart=on-failure
|
|
436
|
+
RestartSec=10
|
|
437
|
+
StandardOutput=append:${logDir}/daemon-out.log
|
|
438
|
+
StandardError=append:${logDir}/daemon-err.log
|
|
439
|
+
|
|
440
|
+
[Install]
|
|
441
|
+
WantedBy=default.target
|
|
442
|
+
`;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// systemd user instance needs XDG_RUNTIME_DIR (a real user session) and
|
|
446
|
+
// systemctl --user to respond. Both are missing on headless servers without
|
|
447
|
+
// loginctl enable-linger and in many minimal containers.
|
|
448
|
+
function linuxSystemdUserAvailable(): boolean {
|
|
449
|
+
if (process.platform !== "linux") return false;
|
|
450
|
+
if (!process.env["XDG_RUNTIME_DIR"]) return false;
|
|
451
|
+
return spawnSync("systemctl", ["--user", "--version"], { encoding: "utf8" }).status === 0;
|
|
452
|
+
}
|
|
453
|
+
|
|
409
454
|
function buildPlist(nodeExec: string, nlmJs: string): string {
|
|
410
455
|
const logDir = join(homedir(), ".nlm", "logs");
|
|
411
456
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -444,38 +489,92 @@ function buildPlist(nodeExec: string, nlmJs: string): string {
|
|
|
444
489
|
|
|
445
490
|
program
|
|
446
491
|
.command("install")
|
|
447
|
-
.description("Install the
|
|
492
|
+
.description("Install the auto-start daemon (LaunchAgent on macOS, systemd user unit on Linux)")
|
|
448
493
|
.action(() => {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
494
|
+
// Harden before installing the daemon so the persisted unit owner-
|
|
495
|
+
// checks succeed against locked-down ~/.nlm logs.
|
|
496
|
+
hardenNlmDirPermissions();
|
|
497
|
+
if (process.platform === "darwin") {
|
|
498
|
+
const uid = process.getuid?.();
|
|
499
|
+
if (uid === undefined) {
|
|
500
|
+
console.error("nlm install: could not determine UID");
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
mkdirSync(join(homedir(), ".nlm", "logs"), { recursive: true });
|
|
504
|
+
writeFileSync(LAUNCH_AGENT_PLIST, buildPlist(process.execPath, __filename), "utf8");
|
|
505
|
+
console.error(`nlm: wrote ${LAUNCH_AGENT_PLIST}`);
|
|
506
|
+
try {
|
|
507
|
+
execFileSync("launchctl", ["bootout", `gui/${uid}`, LAUNCH_AGENT_LABEL], { stdio: "ignore" });
|
|
508
|
+
} catch {
|
|
509
|
+
// not loaded yet — expected on first install
|
|
510
|
+
}
|
|
511
|
+
execFileSync("launchctl", ["bootstrap", `gui/${uid}`, LAUNCH_AGENT_PLIST]);
|
|
512
|
+
console.error("nlm: daemon installed and started.");
|
|
513
|
+
console.error(` UI: http://localhost:${port()}/ui`);
|
|
514
|
+
console.error(` To stop: launchctl stop ${LAUNCH_AGENT_LABEL}`);
|
|
515
|
+
console.error(" To remove: nlm uninstall");
|
|
516
|
+
return;
|
|
457
517
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
518
|
+
|
|
519
|
+
if (process.platform === "linux") {
|
|
520
|
+
if (!linuxSystemdUserAvailable()) {
|
|
521
|
+
console.error("nlm install: systemd user instance not available.");
|
|
522
|
+
console.error(" XDG_RUNTIME_DIR missing or `systemctl --user` did not respond.");
|
|
523
|
+
console.error(" Common on headless servers without an active user session.");
|
|
524
|
+
console.error(" Start manually with: nlm start &");
|
|
525
|
+
console.error(" Or enable lingering so user units run without login:");
|
|
526
|
+
console.error(" sudo loginctl enable-linger $USER");
|
|
527
|
+
console.error(" Then re-run: nlm install");
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
mkdirSync(dirname(LINUX_SYSTEMD_UNIT_PATH), { recursive: true });
|
|
531
|
+
mkdirSync(join(homedir(), ".nlm", "logs"), { recursive: true });
|
|
532
|
+
writeFileSync(LINUX_SYSTEMD_UNIT_PATH, buildSystemdUnit(process.execPath, __filename), "utf8");
|
|
533
|
+
console.error(`nlm: wrote ${LINUX_SYSTEMD_UNIT_PATH}`);
|
|
534
|
+
execFileSync("systemctl", ["--user", "daemon-reload"]);
|
|
535
|
+
execFileSync("systemctl", ["--user", "enable", "--now", LINUX_SYSTEMD_UNIT_NAME]);
|
|
536
|
+
console.error("nlm: daemon installed and started.");
|
|
537
|
+
console.error(` UI: http://localhost:${port()}/ui`);
|
|
538
|
+
console.error(` Status: systemctl --user status ${LINUX_SYSTEMD_UNIT_NAME}`);
|
|
539
|
+
console.error(` To stop: systemctl --user stop ${LINUX_SYSTEMD_UNIT_NAME}`);
|
|
540
|
+
console.error(" To remove: nlm uninstall");
|
|
541
|
+
console.error(" Headless? Run `sudo loginctl enable-linger $USER` so the daemon survives logout.");
|
|
542
|
+
return;
|
|
465
543
|
}
|
|
466
|
-
|
|
467
|
-
console.error("nlm:
|
|
468
|
-
console.error(
|
|
469
|
-
|
|
470
|
-
console.error(" To remove: nlm uninstall");
|
|
544
|
+
|
|
545
|
+
console.error("nlm install: only macOS and Linux (systemd) are supported.");
|
|
546
|
+
console.error(" On Windows, run `nlm start` manually or via Task Scheduler.");
|
|
547
|
+
process.exit(1);
|
|
471
548
|
});
|
|
472
549
|
|
|
473
550
|
program
|
|
474
551
|
.command("uninstall")
|
|
475
|
-
.description("Remove the macOS
|
|
552
|
+
.description("Remove the auto-start daemon (LaunchAgent on macOS, systemd user unit on Linux)")
|
|
476
553
|
.action(() => {
|
|
554
|
+
if (process.platform === "linux") {
|
|
555
|
+
// Stop + disable, then remove the unit. Idempotent: ignore "not loaded"
|
|
556
|
+
// errors so re-running uninstall on a half-removed state still finishes.
|
|
557
|
+
try {
|
|
558
|
+
execFileSync("systemctl", ["--user", "disable", "--now", LINUX_SYSTEMD_UNIT_NAME], { stdio: "pipe" });
|
|
559
|
+
console.error(`nlm: stopped and disabled ${LINUX_SYSTEMD_UNIT_NAME}`);
|
|
560
|
+
} catch {
|
|
561
|
+
// Unit wasn't loaded — fine, proceed to file cleanup.
|
|
562
|
+
}
|
|
563
|
+
if (existsSync(LINUX_SYSTEMD_UNIT_PATH)) {
|
|
564
|
+
rmSync(LINUX_SYSTEMD_UNIT_PATH);
|
|
565
|
+
console.error(`nlm: removed ${LINUX_SYSTEMD_UNIT_PATH}`);
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
execFileSync("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
|
|
569
|
+
} catch {
|
|
570
|
+
// systemd unavailable — file already removed, nothing more to do.
|
|
571
|
+
}
|
|
572
|
+
console.error("nlm: uninstalled. Run `nlm install` to reinstall.");
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
477
576
|
if (process.platform !== "darwin") {
|
|
478
|
-
console.error("nlm uninstall: only macOS
|
|
577
|
+
console.error("nlm uninstall: only macOS and Linux (systemd) are supported.");
|
|
479
578
|
process.exit(1);
|
|
480
579
|
}
|
|
481
580
|
const uid = process.getuid?.();
|
|
@@ -560,7 +659,7 @@ const hook = program
|
|
|
560
659
|
|
|
561
660
|
hook
|
|
562
661
|
.command("install")
|
|
563
|
-
.description("Add the NLM hooks (recall + session-end + stop) to ~/.claude/settings.json (
|
|
662
|
+
.description("Add the NLM hooks (recall + session-end + stop) to ~/.claude/settings.json (live mode)")
|
|
564
663
|
.action(() => {
|
|
565
664
|
const path = claudeSettingsPath();
|
|
566
665
|
const hookLogPath = process.env["NLM_HOOK_LOG"] ?? join(homedir(), ".nlm", "hook-log.jsonl");
|
|
@@ -571,7 +670,7 @@ hook
|
|
|
571
670
|
// partial failure is the bug class we shipped #161 to prevent.
|
|
572
671
|
const installed: HookSpec[] = [];
|
|
573
672
|
for (const spec of ALL_HOOKS) {
|
|
574
|
-
const command = buildHookCommand(process.execPath, spec.script, "
|
|
673
|
+
const command = buildHookCommand(process.execPath, spec.script, "live");
|
|
575
674
|
addHook(path, command, spec.event);
|
|
576
675
|
const smoke = smokeTestHookCommand(command, hookLogPath);
|
|
577
676
|
if (!smoke.ok) {
|
|
@@ -591,14 +690,14 @@ hook
|
|
|
591
690
|
installed.push(spec);
|
|
592
691
|
}
|
|
593
692
|
|
|
594
|
-
console.error(`nlm: NLM hooks installed in ${path} (
|
|
693
|
+
console.error(`nlm: NLM hooks installed in ${path} (live mode):`);
|
|
595
694
|
for (const spec of installed) {
|
|
596
695
|
console.error(` - ${spec.event} → ${spec.label}-hook`);
|
|
597
696
|
}
|
|
598
697
|
console.error(" Smoke tests passed — all hooks appended synthetic entries to hook-log.jsonl.");
|
|
599
|
-
console.error(" Recall hooks log to ~/.nlm/hook-log.jsonl
|
|
698
|
+
console.error(" Recall hooks inject prior-session context on UserPromptSubmit and log to ~/.nlm/hook-log.jsonl.");
|
|
600
699
|
console.error(" Session-end hook cleans up ~/.nlm/hook-state/<session>.json on session close.");
|
|
601
|
-
console.error(" To
|
|
700
|
+
console.error(" To run silently for calibration (no injection): set NLM_HOOK_MODE=shadow in the command.");
|
|
602
701
|
console.error(" To remove: nlm hook uninstall");
|
|
603
702
|
});
|
|
604
703
|
|
|
@@ -680,7 +779,7 @@ connect
|
|
|
680
779
|
.action((opts) => {
|
|
681
780
|
if (opts.dryRun) {
|
|
682
781
|
console.error("nlm connect claude-code (dry run):");
|
|
683
|
-
console.error(` write [mcpServers.nlm-memory] to ${
|
|
782
|
+
console.error(` write [mcpServers.nlm-memory] to ${mcpConfigPath()}`);
|
|
684
783
|
if (opts.withHooks) console.error(" install 6 Claude Code hooks");
|
|
685
784
|
return;
|
|
686
785
|
}
|
|
@@ -746,6 +845,54 @@ connect
|
|
|
746
845
|
console.error(" Also run: nlm connect hermes (to wire the MCP server)");
|
|
747
846
|
});
|
|
748
847
|
|
|
848
|
+
connect
|
|
849
|
+
.command("cursor")
|
|
850
|
+
.description("Register Cursor as an nlm source (reads state.vscdb directly — no files installed)")
|
|
851
|
+
.option("--db-path <path>", "override path to globalStorage/state.vscdb")
|
|
852
|
+
.option("--dry-run", "print what would happen without changing files")
|
|
853
|
+
.action((opts) => {
|
|
854
|
+
const store = new SqliteSessionStore({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
|
|
855
|
+
try {
|
|
856
|
+
const registry = new SourceRegistry(store.rawDb());
|
|
857
|
+
const report = connectCursor(registry, {
|
|
858
|
+
...(opts.dbPath ? { dbPath: opts.dbPath as string } : {}),
|
|
859
|
+
dryRun: Boolean(opts.dryRun),
|
|
860
|
+
});
|
|
861
|
+
if (opts.dryRun) {
|
|
862
|
+
console.error(`nlm connect cursor (dry run): register source at ${report.adapterDbPath}${report.adapterExists ? "" : " (not found yet)"}`);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const suffix = report.adapterExists ? "" : " (DB not found — will activate when Cursor is installed)";
|
|
866
|
+
console.error(`nlm: Cursor source ${report.action} → ${report.adapterDbPath}${suffix}`);
|
|
867
|
+
} finally {
|
|
868
|
+
store.close();
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
connect
|
|
873
|
+
.command("windsurf")
|
|
874
|
+
.description("Register Windsurf as an nlm source (reads state.vscdb files directly — no files installed)")
|
|
875
|
+
.option("--user-dir <path>", "override path to Windsurf User directory")
|
|
876
|
+
.option("--dry-run", "print what would happen without changing files")
|
|
877
|
+
.action((opts) => {
|
|
878
|
+
const store = new SqliteSessionStore({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
|
|
879
|
+
try {
|
|
880
|
+
const registry = new SourceRegistry(store.rawDb());
|
|
881
|
+
const report = connectWindsurf(registry, {
|
|
882
|
+
...(opts.userDir ? { userDir: opts.userDir as string } : {}),
|
|
883
|
+
dryRun: Boolean(opts.dryRun),
|
|
884
|
+
});
|
|
885
|
+
if (opts.dryRun) {
|
|
886
|
+
console.error(`nlm connect windsurf (dry run): register source at ${report.userDir}${report.dirExists ? "" : " (not found yet)"}`);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
const suffix = report.dirExists ? "" : " (User dir not found — will activate when Windsurf is installed)";
|
|
890
|
+
console.error(`nlm: Windsurf source ${report.action} → ${report.userDir}${suffix}`);
|
|
891
|
+
} finally {
|
|
892
|
+
store.close();
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
|
|
749
896
|
const disconnect = program
|
|
750
897
|
.command("disconnect")
|
|
751
898
|
.description("Disconnect nlm-memory from an AI coding runtime");
|
|
@@ -841,6 +988,48 @@ disconnect
|
|
|
841
988
|
: `nlm: no plugin directory found at ${report.destDir}`);
|
|
842
989
|
});
|
|
843
990
|
|
|
991
|
+
disconnect
|
|
992
|
+
.command("cursor")
|
|
993
|
+
.description("Disable the Cursor source in the nlm registry (leaves Cursor untouched)")
|
|
994
|
+
.option("--dry-run", "print what would happen without changing files")
|
|
995
|
+
.action((opts) => {
|
|
996
|
+
const store = new SqliteSessionStore({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
|
|
997
|
+
try {
|
|
998
|
+
const registry = new SourceRegistry(store.rawDb());
|
|
999
|
+
const report = disconnectCursor(registry, { dryRun: Boolean(opts.dryRun) });
|
|
1000
|
+
if (opts.dryRun) {
|
|
1001
|
+
console.error("nlm disconnect cursor (dry run): disable Cursor source in registry");
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
console.error(report.action === "disabled"
|
|
1005
|
+
? "nlm: Cursor source disabled"
|
|
1006
|
+
: "nlm: no Cursor source found in registry");
|
|
1007
|
+
} finally {
|
|
1008
|
+
store.close();
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
disconnect
|
|
1013
|
+
.command("windsurf")
|
|
1014
|
+
.description("Disable the Windsurf source in the nlm registry (leaves Windsurf untouched)")
|
|
1015
|
+
.option("--dry-run", "print what would happen without changing files")
|
|
1016
|
+
.action((opts) => {
|
|
1017
|
+
const store = new SqliteSessionStore({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
|
|
1018
|
+
try {
|
|
1019
|
+
const registry = new SourceRegistry(store.rawDb());
|
|
1020
|
+
const report = disconnectWindsurf(registry, { dryRun: Boolean(opts.dryRun) });
|
|
1021
|
+
if (opts.dryRun) {
|
|
1022
|
+
console.error("nlm disconnect windsurf (dry run): disable Windsurf source in registry");
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
console.error(report.action === "disabled"
|
|
1026
|
+
? "nlm: Windsurf source disabled"
|
|
1027
|
+
: "nlm: no Windsurf source found in registry");
|
|
1028
|
+
} finally {
|
|
1029
|
+
store.close();
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
|
|
844
1033
|
program
|
|
845
1034
|
.command("setup")
|
|
846
1035
|
.description("Interactive first-run setup: detect runtimes, wire MCP + hooks, start daemon")
|
|
@@ -854,6 +1043,10 @@ program
|
|
|
854
1043
|
launchAgentLabel: LAUNCH_AGENT_LABEL,
|
|
855
1044
|
launchAgentPlist: LAUNCH_AGENT_PLIST,
|
|
856
1045
|
buildPlist,
|
|
1046
|
+
linuxSystemdUnitName: LINUX_SYSTEMD_UNIT_NAME,
|
|
1047
|
+
linuxSystemdUnitPath: LINUX_SYSTEMD_UNIT_PATH,
|
|
1048
|
+
buildSystemdUnit,
|
|
1049
|
+
linuxSystemdUserAvailable,
|
|
857
1050
|
claudeSettingsPath: claudeSettingsPath(),
|
|
858
1051
|
allHooks: ALL_HOOKS,
|
|
859
1052
|
addHook,
|