nlm-memory 0.11.1 → 0.13.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/README.md +1 -1
- package/dist/cli/classify-parity.js +1 -1
- package/dist/cli/classify-parity.js.map +1 -1
- package/dist/cli/nlm.js +323 -62
- package/dist/cli/nlm.js.map +1 -1
- package/dist/core/classifier/prompt.d.ts +70 -0
- package/dist/core/classifier/prompt.js +71 -2
- package/dist/core/classifier/prompt.js.map +1 -1
- package/dist/core/exemplars/extract-exemplar.d.ts +36 -0
- package/dist/core/exemplars/extract-exemplar.js +177 -0
- package/dist/core/exemplars/extract-exemplar.js.map +1 -0
- package/dist/core/exemplars/ingest-exemplar.d.ts +30 -0
- package/dist/core/exemplars/ingest-exemplar.js +71 -0
- package/dist/core/exemplars/ingest-exemplar.js.map +1 -0
- package/dist/core/exemplars/recall-code.d.ts +25 -0
- package/dist/core/exemplars/recall-code.js +49 -0
- package/dist/core/exemplars/recall-code.js.map +1 -0
- package/dist/core/facts/backfill-facts.d.ts +4 -2
- package/dist/core/facts/backfill-facts.js +18 -27
- package/dist/core/facts/backfill-facts.js.map +1 -1
- package/dist/core/hook/strip-injected-context.d.ts +24 -0
- package/dist/core/hook/strip-injected-context.js +73 -0
- package/dist/core/hook/strip-injected-context.js.map +1 -0
- package/dist/core/ingest/ingest-session.d.ts +4 -2
- package/dist/core/ingest/ingest-session.js +12 -4
- package/dist/core/ingest/ingest-session.js.map +1 -1
- package/dist/core/providers/provider-registry.d.ts +31 -11
- package/dist/core/providers/provider-registry.js +27 -13
- package/dist/core/providers/provider-registry.js.map +1 -1
- package/dist/core/recall/metadata-tiebreaker.d.ts +32 -0
- package/dist/core/recall/metadata-tiebreaker.js +59 -0
- package/dist/core/recall/metadata-tiebreaker.js.map +1 -0
- package/dist/core/recall/recall-service.js +11 -3
- package/dist/core/recall/recall-service.js.map +1 -1
- package/dist/core/recall-facts/fact-recall-service.js +75 -28
- package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
- package/dist/core/scheduler/scheduler.d.ts +4 -2
- package/dist/core/scheduler/scheduler.js +26 -15
- package/dist/core/scheduler/scheduler.js.map +1 -1
- package/dist/core/sources/source-registry.d.ts +36 -12
- package/dist/core/sources/source-registry.js +59 -44
- package/dist/core/sources/source-registry.js.map +1 -1
- package/dist/core/storage/backup-rotation.d.ts +38 -0
- package/dist/core/storage/backup-rotation.js +82 -0
- package/dist/core/storage/backup-rotation.js.map +1 -0
- package/dist/core/storage/null-code-exemplar-store.d.ts +21 -0
- package/dist/core/storage/null-code-exemplar-store.js +19 -0
- package/dist/core/storage/null-code-exemplar-store.js.map +1 -0
- package/dist/core/storage/pg-fact-store.d.ts +2 -0
- package/dist/core/storage/pg-fact-store.js +34 -3
- package/dist/core/storage/pg-fact-store.js.map +1 -1
- package/dist/core/storage/pg-session-store.d.ts +14 -4
- package/dist/core/storage/pg-session-store.js +123 -2
- package/dist/core/storage/pg-session-store.js.map +1 -1
- package/dist/core/storage/pg-storage.d.ts +11 -2
- package/dist/core/storage/pg-storage.js +14 -2
- package/dist/core/storage/pg-storage.js.map +1 -1
- package/dist/core/storage/pg-tx-context.d.ts +2 -0
- package/dist/core/storage/pg-tx-context.js +12 -0
- package/dist/core/storage/pg-tx-context.js.map +1 -1
- package/dist/core/storage/sqlite-code-exemplar-store.d.ts +40 -0
- package/dist/core/storage/sqlite-code-exemplar-store.js +247 -0
- package/dist/core/storage/sqlite-code-exemplar-store.js.map +1 -0
- package/dist/core/storage/sqlite-fact-store.d.ts +2 -0
- package/dist/core/storage/sqlite-fact-store.js +30 -2
- package/dist/core/storage/sqlite-fact-store.js.map +1 -1
- package/dist/core/storage/sqlite-session-store.d.ts +21 -0
- package/dist/core/storage/sqlite-session-store.js +23 -0
- package/dist/core/storage/sqlite-session-store.js.map +1 -1
- package/dist/core/storage/sqlite-storage.d.ts +6 -0
- package/dist/core/storage/sqlite-storage.js +14 -2
- package/dist/core/storage/sqlite-storage.js.map +1 -1
- package/dist/core/storage/supersedence-log.d.ts +8 -0
- package/dist/core/storage/supersedence-log.js +19 -0
- package/dist/core/storage/supersedence-log.js.map +1 -1
- package/dist/hook/prompt-recall-hook.d.ts +5 -4
- package/dist/hook/prompt-recall-hook.js +12 -7
- package/dist/hook/prompt-recall-hook.js.map +1 -1
- package/dist/http/app.d.ts +7 -1
- package/dist/http/app.js +226 -83
- package/dist/http/app.js.map +1 -1
- package/dist/install/codex.d.ts +29 -1
- package/dist/install/codex.js +117 -15
- package/dist/install/codex.js.map +1 -1
- package/dist/install/cursor.d.ts +4 -4
- package/dist/install/cursor.js +7 -7
- package/dist/install/cursor.js.map +1 -1
- package/dist/install/health.d.ts +56 -0
- package/dist/install/health.js +133 -0
- package/dist/install/health.js.map +1 -0
- package/dist/install/setup.js +1 -1
- package/dist/install/setup.js.map +1 -1
- package/dist/install/windsurf.d.ts +4 -4
- package/dist/install/windsurf.js +7 -7
- package/dist/install/windsurf.js.map +1 -1
- package/dist/llm/classifier-box.d.ts +4 -0
- package/dist/llm/classifier-box.js +9 -2
- package/dist/llm/classifier-box.js.map +1 -1
- package/dist/llm/ollama-client.d.ts +19 -1
- package/dist/llm/ollama-client.js +14 -6
- package/dist/llm/ollama-client.js.map +1 -1
- package/dist/llm/ollama-code-embedder.d.ts +28 -0
- package/dist/llm/ollama-code-embedder.js +73 -0
- package/dist/llm/ollama-code-embedder.js.map +1 -0
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.js +132 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/ports/code-embedder.d.ts +27 -0
- package/dist/ports/code-embedder.js +15 -0
- package/dist/ports/code-embedder.js.map +1 -0
- package/dist/ports/code-exemplar-store.d.ts +45 -0
- package/dist/ports/code-exemplar-store.js +11 -0
- package/dist/ports/code-exemplar-store.js.map +1 -0
- package/dist/ports/fact-store.d.ts +0 -0
- package/dist/ports/fact-store.js.map +1 -1
- package/dist/ports/storage.d.ts +8 -0
- package/dist/shared/types.d.ts +35 -0
- package/dist/ui/assets/{index-CJgVgxlM.js → index-CVo7KKw2.js} +2 -2
- package/dist/ui/assets/{index-D5lKaksH.css → index-DCuQrzJH.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/migrations/021_code_exemplars.sql +51 -0
- package/migrations/022_facts_retired_at.sql +17 -0
- package/migrations/pg/021_code_exemplars.sql +45 -0
- package/migrations/pg/022_facts_retired_at.sql +12 -0
- package/nlm/index.js +6 -1
- package/package.json +3 -2
- package/plugin/.codex-plugin/plugin.json +2 -2
- package/plugin/hooks/hooks.json +13 -2
- package/plugin/scripts/prompt-recall-hook.mjs +7 -1
- package/plugin/scripts/session-start-hook.mjs +275 -0
package/README.md
CHANGED
|
@@ -335,7 +335,7 @@ recall: prompt / query
|
|
|
335
335
|
| `NLM_MCP_TOKEN` | auto-generated | 256-bit bearer for `/api/*` (non-browser) and `/mcp` |
|
|
336
336
|
| `NLM_MCP_CONFIG` | `~/.mcp.json` | Path the `connect`/`disconnect` commands modify |
|
|
337
337
|
| `NLM_CLASSIFIER` | `ollama` | `ollama` (local, default), `deepseek`, `openai`, `anthropic`, `openrouter`, or `openai-compatible` |
|
|
338
|
-
| `NLM_CLASSIFIER_MODEL` | `qwen3:4b
|
|
338
|
+
| `NLM_CLASSIFIER_MODEL` | `qwen3.5:4b` | Model id for the chosen provider. `qwen3.5:4b` (Q4_K_M, 4.7B) is the recommended local default — scored 74.4% decision-precision vs 58.7% for `qwen3:4b-instruct-2507-q4_K_M` (task #320, 2026-06-12). |
|
|
339
339
|
| `NLM_OLLAMA_URL` | `http://localhost:11434` | Override Ollama endpoint |
|
|
340
340
|
| `NLM_ADAPTERS` | all | Comma-separated allowlist of adapters to enable |
|
|
341
341
|
| `DEEPSEEK_API_KEY` | — | Required only when classifier=deepseek |
|
|
@@ -29,7 +29,7 @@ function parseArgs(argv) {
|
|
|
29
29
|
const limit = Number.parseInt(flag("--limit", "10") ?? "10", 10);
|
|
30
30
|
const providerRaw = (flag("--provider", "deepseek") ?? "deepseek").toLowerCase();
|
|
31
31
|
const provider = providerRaw === "ollama" ? "ollama" : "deepseek";
|
|
32
|
-
const defaultModel = provider === "deepseek" ? "deepseek-v4-flash" : "qwen3:4b
|
|
32
|
+
const defaultModel = provider === "deepseek" ? "deepseek-v4-flash" : "qwen3.5:4b";
|
|
33
33
|
return {
|
|
34
34
|
limit: Number.isFinite(limit) && limit > 0 ? limit : 10,
|
|
35
35
|
dbPath: flag("--db", process.env["NLM_DB_PATH"] ?? resolve(homedir(), ".nlm/canonical.sqlite")) ??
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classify-parity.js","sourceRoot":"","sources":["../../src/cli/classify-parity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,KAAK,SAAS,MAAM,YAAY,CAAC;AAExC,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAC9E,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAqDrD,SAAS,SAAS,CAAC,IAAc;IAC/B,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,QAAiB,EAAsB,EAAE;QACnE,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,CAAC,KAAK,CAAC,CAAC;YAAE,OAAO,QAAQ,CAAC;QAC9B,OAAO,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,QAAQ,CAAC;IACjC,CAAC,CAAC;IACF,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC;IACjE,MAAM,WAAW,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,IAAI,UAAU,CAAC,CAAC,WAAW,EAAE,CAAC;IACjF,MAAM,QAAQ,GAAa,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC;IAC5E,MAAM,YAAY,GAAG,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC
|
|
1
|
+
{"version":3,"file":"classify-parity.js","sourceRoot":"","sources":["../../src/cli/classify-parity.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/nlm.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import { fileURLToPath } from "node:url";
|
|
27
27
|
import { dirname, resolve, join } from "node:path";
|
|
28
28
|
import { homedir } from "node:os";
|
|
29
|
-
import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from "node:fs";
|
|
29
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync, copyFileSync } from "node:fs";
|
|
30
30
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
31
31
|
import { Command } from "commander";
|
|
32
32
|
import pkg from "../../package.json" with { type: "json" };
|
|
@@ -35,23 +35,24 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
35
35
|
import { FactRecallService } from "../core/recall-facts/fact-recall-service.js";
|
|
36
36
|
import { RecallService } from "../core/recall/recall-service.js";
|
|
37
37
|
import { ProviderRegistry } from "../core/providers/provider-registry.js";
|
|
38
|
-
import { SourceRegistry } from "../core/sources/source-registry.js";
|
|
39
38
|
import { SqliteStorage } from "../core/storage/sqlite-storage.js";
|
|
40
39
|
import { PgStorage } from "../core/storage/pg-storage.js";
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import { applyPendingRestore } from "../core/storage/db-restore.js";
|
|
40
|
+
import { applyPendingRestore, stageRestore } from "../core/storage/db-restore.js";
|
|
41
|
+
import { listBackupDates, resolveBackup, runRollingBackup } from "../core/storage/backup-rotation.js";
|
|
44
42
|
import { createApp } from "../http/app.js";
|
|
45
43
|
import { createMcpServer } from "../mcp/server.js";
|
|
46
44
|
import { ClassifierBox } from "../llm/classifier-box.js";
|
|
47
45
|
import { DeepSeekClient } from "../llm/deepseek-client.js";
|
|
48
46
|
import { OllamaClient } from "../llm/ollama-client.js";
|
|
47
|
+
import { OllamaCodeEmbedder } from "../llm/ollama-code-embedder.js";
|
|
49
48
|
import { autoloadEnv } from "../llm/env-autoload.js";
|
|
50
49
|
import { addHook, buildHookCommand, removeHook } from "../core/hook/claude-settings.js";
|
|
51
|
-
import { codexBinaryAvailable, connectCodex, disconnectCodex, pluginScriptsDir, } from "../install/codex.js";
|
|
50
|
+
import { codexBinaryAvailable, connectCodex, disconnectCodex, repairCodex, pluginScriptsDir, } from "../install/codex.js";
|
|
52
51
|
import { connectClaudeCode, disconnectClaudeCode, installClaudeCodeHooks, mcpConfigPath } from "../install/claude-code.js";
|
|
52
|
+
import { evaluateInstallHealth, evaluateModelHealth, evaluateRecallSmoke } from "../install/health.js";
|
|
53
|
+
import { codexConfigPath } from "../install/codex.js";
|
|
53
54
|
import { hardenNlmDirPermissions } from "../install/nlm-dir-perms.js";
|
|
54
|
-
import { ensureMcpToken } from "../install/ollama.js";
|
|
55
|
+
import { embeddingModelPresent, ensureMcpToken, ollamaModelPresent } from "../install/ollama.js";
|
|
55
56
|
import { connectCursor, disconnectCursor } from "../install/cursor.js";
|
|
56
57
|
import { describeRemove, describeUpsert, installCursorRules, installOpencodeRules, installWindsurfRules, uninstallCursorRules, uninstallOpencodeRules, uninstallWindsurfRules, } from "../install/rules-install.js";
|
|
57
58
|
import { runSupersedeCommand } from "./supersede.js";
|
|
@@ -98,17 +99,17 @@ function ollamaUrl() {
|
|
|
98
99
|
return process.env["NLM_OLLAMA_URL"] ?? "http://localhost:11434";
|
|
99
100
|
}
|
|
100
101
|
function buildClassifier() {
|
|
101
|
-
// qwen3:4b
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
// default to keep the daemon local-first and key-free; DeepSeek
|
|
106
|
-
// available via NLM_CLASSIFIER=deepseek for users who prioritize speed.
|
|
102
|
+
// qwen3.5:4b is the production classifier as of 2026-06-12 (NLM task #320).
|
|
103
|
+
// Scored 74.4% decision-precision vs qwen3:4b-instruct-2507-q4_K_M's 58.7% in
|
|
104
|
+
// the 2026-06-11 eval. Requires think:false (handled by ClassifierBox via
|
|
105
|
+
// classifierNeedsThinkDisabled) to stay within the 180s classify timeout.
|
|
106
|
+
// Ollama is the default to keep the daemon local-first and key-free; DeepSeek
|
|
107
|
+
// remains available via NLM_CLASSIFIER=deepseek for users who prioritize speed.
|
|
107
108
|
const provider = (process.env["NLM_CLASSIFIER"] ?? "ollama").toLowerCase();
|
|
108
109
|
if (provider !== "ollama")
|
|
109
110
|
autoloadEnv();
|
|
110
111
|
const model = process.env["NLM_CLASSIFIER_MODEL"]
|
|
111
|
-
?? (provider === "ollama" ? "qwen3:4b
|
|
112
|
+
?? (provider === "ollama" ? "qwen3.5:4b" : "deepseek-v4-flash");
|
|
112
113
|
return new ClassifierBox({ provider, model, ollamaUrl: ollamaUrl() });
|
|
113
114
|
}
|
|
114
115
|
async function buildAdapters(sources) {
|
|
@@ -163,17 +164,14 @@ async function buildStack() {
|
|
|
163
164
|
const facts = storage.facts;
|
|
164
165
|
const signals = storage.signals;
|
|
165
166
|
const scope = installScope();
|
|
166
|
-
|
|
167
|
-
const sources = storage instanceof PgStorage
|
|
168
|
-
? new PgSourceRegistry(storage.pgPool())
|
|
169
|
-
: new SourceRegistry(storage.rawDb());
|
|
167
|
+
const sources = storage.sources;
|
|
170
168
|
await sources.seedDefaults();
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
169
|
+
const providers = storage.providers;
|
|
170
|
+
// Provider seeding is SQLite-only: it bridges from the local DEEPSEEK_API_KEY
|
|
171
|
+
// env, which is wrong for a hosted multi-tenant PG. PgProviderRegistry has no
|
|
172
|
+
// seedDefaults (not on ProviderRegistryPort), so this narrows to the SQLite case.
|
|
175
173
|
if (providers instanceof ProviderRegistry)
|
|
176
|
-
providers.seedDefaults();
|
|
174
|
+
await providers.seedDefaults();
|
|
177
175
|
// Recall only uses embed(). Embeddings live on Ollama; DeepSeek doesn't
|
|
178
176
|
// expose them. Classifier is wired separately for Phase D ingest.
|
|
179
177
|
const embedder = new OllamaClient({ baseUrl: ollamaUrl() });
|
|
@@ -213,17 +211,19 @@ program
|
|
|
213
211
|
classifier,
|
|
214
212
|
sources,
|
|
215
213
|
providers,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
...(facts ? { factStore: facts } : {}),
|
|
223
|
-
},
|
|
224
|
-
} : {}),
|
|
214
|
+
ingest: {
|
|
215
|
+
classifier,
|
|
216
|
+
embedder,
|
|
217
|
+
store,
|
|
218
|
+
...(facts ? { factStore: facts } : {}),
|
|
219
|
+
},
|
|
225
220
|
signalStore: signals,
|
|
226
221
|
installScope: scope,
|
|
222
|
+
// Code-exemplar lane. The store + code embedder are always wired; the
|
|
223
|
+
// NLM_CODE_EXEMPLARS_ENABLED flag gates capture (POST /api/signal) and
|
|
224
|
+
// the /api/exemplar + /api/recall-code routes at request time.
|
|
225
|
+
exemplarStore: storage.exemplars,
|
|
226
|
+
codeEmbedder: new OllamaCodeEmbedder({ baseUrl: ollamaUrl() }),
|
|
227
227
|
embedderInfo: { provider: "ollama", model: "nomic-embed-text", dims: 768 },
|
|
228
228
|
...(existsSync(UI_DIST) ? { uiDist: UI_DIST } : {}),
|
|
229
229
|
// Wire POST /mcp only when NLM_MCP_TOKEN is present. Absent = route never
|
|
@@ -294,15 +294,14 @@ program
|
|
|
294
294
|
const memoSweep = new MemoSweepScheduler();
|
|
295
295
|
memoSweep.start();
|
|
296
296
|
console.error(" memo sweep: dormant cleanup every 5m (threshold 24h)");
|
|
297
|
-
if (opts.scheduler !== false
|
|
297
|
+
if (opts.scheduler !== false) {
|
|
298
298
|
const adapters = await buildAdapters(sources);
|
|
299
299
|
if (adapters.length === 0) {
|
|
300
300
|
console.error(" scheduler: no adapters detected (set NLM_ADAPTERS to force-enable)");
|
|
301
301
|
}
|
|
302
302
|
else {
|
|
303
303
|
const scheduler = new ScanScheduler({
|
|
304
|
-
|
|
305
|
-
store: store,
|
|
304
|
+
store,
|
|
306
305
|
adapters,
|
|
307
306
|
classifier,
|
|
308
307
|
embedder,
|
|
@@ -378,6 +377,58 @@ program
|
|
|
378
377
|
await storage.close();
|
|
379
378
|
}
|
|
380
379
|
});
|
|
380
|
+
program
|
|
381
|
+
.command("recall-code")
|
|
382
|
+
.description("Semantic search over code exemplars (requires NLM_CODE_EXEMPLARS_ENABLED=1)")
|
|
383
|
+
.argument("<query>", "natural-language description of the task you are about to implement")
|
|
384
|
+
.option("-r, --repo <path>", "filter by repository path")
|
|
385
|
+
.option("-l, --lang <lang>", "filter by language (ts, py, go, ...)")
|
|
386
|
+
.option("-k <n>", "max results", (v) => Number.parseInt(v, 10), 5)
|
|
387
|
+
.option("--no-negatives", "exclude fail/exhausted exemplars from results")
|
|
388
|
+
.option("--json", "emit the raw JSON result")
|
|
389
|
+
.action(async (query, opts) => {
|
|
390
|
+
if (process.env["NLM_CODE_EXEMPLARS_ENABLED"] !== "1") {
|
|
391
|
+
process.stderr.write("recall-code requires NLM_CODE_EXEMPLARS_ENABLED=1\n");
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
const { storage } = await buildStack();
|
|
395
|
+
const { OllamaCodeEmbedder } = await import("../llm/ollama-code-embedder.js");
|
|
396
|
+
const { recallCode } = await import("../core/exemplars/recall-code.js");
|
|
397
|
+
const { installScope: getScope } = await import("../core/signals/install-scope.js");
|
|
398
|
+
try {
|
|
399
|
+
const embedder = new OllamaCodeEmbedder();
|
|
400
|
+
const result = await recallCode({
|
|
401
|
+
query,
|
|
402
|
+
installScope: getScope(),
|
|
403
|
+
...(opts.repo ? { repo: opts.repo } : {}),
|
|
404
|
+
...(opts.lang ? { lang: opts.lang } : {}),
|
|
405
|
+
k: opts.k ?? 5,
|
|
406
|
+
includeNegatives: opts.negatives !== false,
|
|
407
|
+
}, storage.exemplars, embedder, null);
|
|
408
|
+
if (opts.json) {
|
|
409
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
const printHits = (label, hits) => {
|
|
413
|
+
if (hits.length === 0)
|
|
414
|
+
return;
|
|
415
|
+
process.stdout.write(`\n── ${label} ──\n`);
|
|
416
|
+
for (const h of hits) {
|
|
417
|
+
process.stdout.write(`[${h.outcome}] ${h.taskContext} (${h.repo}, dist=${h.distance.toFixed(4)})\n`);
|
|
418
|
+
process.stdout.write(h.code.slice(0, 400) + (h.code.length > 400 ? "\n…" : "") + "\n\n");
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
printHits("Positives (pass / fix)", result.positives);
|
|
422
|
+
printHits("Negatives (fail / exhausted) — avoid these patterns", result.negatives);
|
|
423
|
+
if (result.positives.length === 0 && result.negatives.length === 0) {
|
|
424
|
+
process.stdout.write("(no exemplars found)\n");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
finally {
|
|
429
|
+
await storage.close();
|
|
430
|
+
}
|
|
431
|
+
});
|
|
381
432
|
program
|
|
382
433
|
.command("misses")
|
|
383
434
|
.description("Show sessions the agent explicitly fetched but the hook never surfaced (recall miss log)")
|
|
@@ -430,15 +481,17 @@ program
|
|
|
430
481
|
process.stdout.write(`${JSON.stringify({ ...result, perSource, unmeasurableSources: unmeasurable }, null, 2)}\n`);
|
|
431
482
|
return;
|
|
432
483
|
}
|
|
484
|
+
console.log(`Recall precision@k — last ${opts.days} day(s)`);
|
|
433
485
|
if (result.precisionAtK === null) {
|
|
434
|
-
console.log("
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
486
|
+
console.log(" Blended: no hook-lane conversations to score (hook-log.jsonl empty for this window).");
|
|
487
|
+
if (citationEntries.length === 0) {
|
|
488
|
+
console.log(" No citations recorded — run: nlm help close-loop");
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
const pct = (result.precisionAtK * 100).toFixed(1);
|
|
493
|
+
console.log(` Blended: ${pct}% (${result.conversationCount} conversations scored)`);
|
|
438
494
|
}
|
|
439
|
-
const pct = (result.precisionAtK * 100).toFixed(1);
|
|
440
|
-
console.log(`Recall precision@k — last ${opts.days} day(s)`);
|
|
441
|
-
console.log(` Blended: ${pct}% (${result.conversationCount} conversations scored)`);
|
|
442
495
|
if (perSource.length > 0) {
|
|
443
496
|
console.log("\n Per source:");
|
|
444
497
|
for (const row of perSource) {
|
|
@@ -478,11 +531,11 @@ program
|
|
|
478
531
|
.description("Run TS classifier against ~/.nlm/canonical.sqlite and diff vs persisted Python output")
|
|
479
532
|
.option("-l, --limit <n>", "sessions to sample", (v) => Number.parseInt(v, 10), 10)
|
|
480
533
|
.option("-p, --provider <name>", "deepseek | ollama", "deepseek")
|
|
481
|
-
.option("-m, --model <name>", "model tag (default: deepseek-v4-flash for deepseek, qwen3:4b
|
|
534
|
+
.option("-m, --model <name>", "model tag (default: deepseek-v4-flash for deepseek, qwen3.5:4b for ollama)")
|
|
482
535
|
.option("-v, --verbose", "per-session diff lines on stderr")
|
|
483
536
|
.action(async (opts) => {
|
|
484
537
|
const provider = opts.provider === "ollama" ? "ollama" : "deepseek";
|
|
485
|
-
const defaultModel = provider === "deepseek" ? "deepseek-v4-flash" : "qwen3:4b
|
|
538
|
+
const defaultModel = provider === "deepseek" ? "deepseek-v4-flash" : "qwen3.5:4b";
|
|
486
539
|
const report = await runParity({
|
|
487
540
|
limit: opts.limit,
|
|
488
541
|
dbPath: dbPath(),
|
|
@@ -530,8 +583,7 @@ program
|
|
|
530
583
|
const { storage, store, facts, embedder, classifier } = await buildStack();
|
|
531
584
|
try {
|
|
532
585
|
const report = await backfillFacts({
|
|
533
|
-
|
|
534
|
-
store: store,
|
|
586
|
+
store,
|
|
535
587
|
factStore: facts,
|
|
536
588
|
classifier,
|
|
537
589
|
embedder: opts.embed === false ? null : embedder,
|
|
@@ -574,8 +626,16 @@ program
|
|
|
574
626
|
.command("mcp")
|
|
575
627
|
.description("Run as an MCP stdio server (for ~/.mcp.json)")
|
|
576
628
|
.action(async () => {
|
|
577
|
-
const { recall, store, facts, factRecall } = await buildStack();
|
|
578
|
-
const server = createMcpServer({
|
|
629
|
+
const { recall, store, facts, factRecall, storage, scope } = await buildStack();
|
|
630
|
+
const server = createMcpServer({
|
|
631
|
+
recall,
|
|
632
|
+
store,
|
|
633
|
+
factStore: facts,
|
|
634
|
+
factRecall,
|
|
635
|
+
exemplarStore: storage.exemplars,
|
|
636
|
+
codeEmbedder: new OllamaCodeEmbedder({ baseUrl: ollamaUrl() }),
|
|
637
|
+
installScope: scope,
|
|
638
|
+
});
|
|
579
639
|
const transport = new StdioServerTransport();
|
|
580
640
|
await server.connect(transport);
|
|
581
641
|
});
|
|
@@ -1027,6 +1087,7 @@ connect
|
|
|
1027
1087
|
.option("--source <source>", "marketplace source (owner/repo, git URL, or local path)", "pbmagnet4/nlm-memory")
|
|
1028
1088
|
.option("--local", "shortcut for --source <repo-root>; use during dev")
|
|
1029
1089
|
.option("--with-hooks", "additionally write absolute paths to ~/.codex/hooks.json (Codex Desktop fallback for openai/codex#16430)")
|
|
1090
|
+
.option("--repair", "first strip a stale pre-rename nlm-memory-ts install (config block + plugin + marketplace), then connect")
|
|
1030
1091
|
.option("--dry-run", "print what would happen without invoking codex")
|
|
1031
1092
|
.action((opts) => {
|
|
1032
1093
|
if (!opts.dryRun && !codexBinaryAvailable()) {
|
|
@@ -1034,8 +1095,19 @@ connect
|
|
|
1034
1095
|
process.exit(1);
|
|
1035
1096
|
}
|
|
1036
1097
|
const source = opts.local ? REPO_ROOT : opts.source;
|
|
1037
|
-
const
|
|
1098
|
+
const connectOpts = { source, withHooks: Boolean(opts.withHooks), dryRun: Boolean(opts.dryRun) };
|
|
1099
|
+
const repair = opts.repair
|
|
1100
|
+
? repairCodex(connectOpts, pluginScriptsDir(REPO_ROOT))
|
|
1101
|
+
: null;
|
|
1102
|
+
const report = repair ? repair.connect : connectCodex(connectOpts, pluginScriptsDir(REPO_ROOT));
|
|
1103
|
+
if (repair && !repair.dryRun) {
|
|
1104
|
+
console.error(repair.staleMcpRemovedFromConfig
|
|
1105
|
+
? "nlm connect codex --repair: stripped stale nlm-memory-ts MCP block from config.toml"
|
|
1106
|
+
: "nlm connect codex --repair: no stale nlm-memory-ts MCP block found");
|
|
1107
|
+
}
|
|
1038
1108
|
if (report.dryRun) {
|
|
1109
|
+
if (repair)
|
|
1110
|
+
console.error(" --repair: would strip any stale nlm-memory-ts block, then:");
|
|
1039
1111
|
console.error("nlm connect codex (dry run):");
|
|
1040
1112
|
console.error(` codex plugin marketplace add ${report.source}`);
|
|
1041
1113
|
console.error(` codex plugin add ${report.pluginName}@${report.marketplaceName}`);
|
|
@@ -1063,6 +1135,9 @@ connect
|
|
|
1063
1135
|
if (report.mcpServerWritten) {
|
|
1064
1136
|
console.error(` Wrote [mcp_servers.nlm-memory] to ${report.mcpServerWritten}`);
|
|
1065
1137
|
}
|
|
1138
|
+
else if (report.mcpServerAlreadyPresent) {
|
|
1139
|
+
console.error(" Left your existing [mcp_servers.nlm-memory] block in place (not overwriting it)");
|
|
1140
|
+
}
|
|
1066
1141
|
if (report.legacyHooksWritten) {
|
|
1067
1142
|
console.error(` Wrote hooks.json fallback to ${report.legacyHooksWritten}`);
|
|
1068
1143
|
}
|
|
@@ -1148,9 +1223,7 @@ connect
|
|
|
1148
1223
|
const storage = SqliteStorage.create({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
|
|
1149
1224
|
await storage.init();
|
|
1150
1225
|
try {
|
|
1151
|
-
|
|
1152
|
-
const registry = new SourceRegistry(storage.rawDb());
|
|
1153
|
-
const report = connectCursor(registry, {
|
|
1226
|
+
const report = await connectCursor(storage.sources, {
|
|
1154
1227
|
...(opts.dbPath ? { dbPath: opts.dbPath } : {}),
|
|
1155
1228
|
dryRun: Boolean(opts.dryRun),
|
|
1156
1229
|
});
|
|
@@ -1182,9 +1255,7 @@ connect
|
|
|
1182
1255
|
const storage = SqliteStorage.create({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
|
|
1183
1256
|
await storage.init();
|
|
1184
1257
|
try {
|
|
1185
|
-
|
|
1186
|
-
const registry = new SourceRegistry(storage.rawDb());
|
|
1187
|
-
const report = connectWindsurf(registry, {
|
|
1258
|
+
const report = await connectWindsurf(storage.sources, {
|
|
1188
1259
|
...(opts.userDir ? { userDir: opts.userDir } : {}),
|
|
1189
1260
|
dryRun: Boolean(opts.dryRun),
|
|
1190
1261
|
});
|
|
@@ -1347,9 +1418,7 @@ disconnect
|
|
|
1347
1418
|
const storage = SqliteStorage.create({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
|
|
1348
1419
|
await storage.init();
|
|
1349
1420
|
try {
|
|
1350
|
-
|
|
1351
|
-
const registry = new SourceRegistry(storage.rawDb());
|
|
1352
|
-
const report = disconnectCursor(registry, { dryRun: Boolean(opts.dryRun) });
|
|
1421
|
+
const report = await disconnectCursor(storage.sources, { dryRun: Boolean(opts.dryRun) });
|
|
1353
1422
|
if (opts.dryRun) {
|
|
1354
1423
|
console.error("nlm disconnect cursor (dry run): disable Cursor source in registry");
|
|
1355
1424
|
if (opts.withRules)
|
|
@@ -1377,9 +1446,7 @@ disconnect
|
|
|
1377
1446
|
const storage = SqliteStorage.create({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
|
|
1378
1447
|
await storage.init();
|
|
1379
1448
|
try {
|
|
1380
|
-
|
|
1381
|
-
const registry = new SourceRegistry(storage.rawDb());
|
|
1382
|
-
const report = disconnectWindsurf(registry, { dryRun: Boolean(opts.dryRun) });
|
|
1449
|
+
const report = await disconnectWindsurf(storage.sources, { dryRun: Boolean(opts.dryRun) });
|
|
1383
1450
|
if (opts.dryRun) {
|
|
1384
1451
|
console.error("nlm disconnect windsurf (dry run): disable Windsurf source in registry");
|
|
1385
1452
|
if (opts.withRules)
|
|
@@ -1496,9 +1563,69 @@ program
|
|
|
1496
1563
|
process.exit(1);
|
|
1497
1564
|
}
|
|
1498
1565
|
});
|
|
1566
|
+
async function gatherInstallProbe() {
|
|
1567
|
+
autoloadEnv();
|
|
1568
|
+
let reachable = false;
|
|
1569
|
+
let version = null;
|
|
1570
|
+
try {
|
|
1571
|
+
const ctrl = new AbortController();
|
|
1572
|
+
const timer = setTimeout(() => ctrl.abort(), 1_500);
|
|
1573
|
+
const res = await fetch(`http://localhost:${port()}/api/health`, { signal: ctrl.signal });
|
|
1574
|
+
clearTimeout(timer);
|
|
1575
|
+
if (res.ok) {
|
|
1576
|
+
const body = (await res.json());
|
|
1577
|
+
reachable = true;
|
|
1578
|
+
version = typeof body.version === "string" ? body.version : null;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
catch {
|
|
1582
|
+
// unreachable — reported as a FAIL by the evaluator
|
|
1583
|
+
}
|
|
1584
|
+
const mcpPath = mcpConfigPath();
|
|
1585
|
+
let claudeMcp = false;
|
|
1586
|
+
try {
|
|
1587
|
+
if (existsSync(mcpPath)) {
|
|
1588
|
+
const cfg = JSON.parse(readFileSync(mcpPath, "utf8"));
|
|
1589
|
+
claudeMcp = Boolean(cfg.mcpServers && "nlm-memory" in cfg.mcpServers);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
catch {
|
|
1593
|
+
// malformed config → treat as unconfigured (connect will rewrite it)
|
|
1594
|
+
}
|
|
1595
|
+
const codexPath = codexConfigPath();
|
|
1596
|
+
let codexPresent = false;
|
|
1597
|
+
let codexMcp = false;
|
|
1598
|
+
let codexStale = false;
|
|
1599
|
+
if (existsSync(codexPath)) {
|
|
1600
|
+
codexPresent = true;
|
|
1601
|
+
const txt = readFileSync(codexPath, "utf8");
|
|
1602
|
+
// Recognize the MCP table whether it's our managed (sentineled) block or a
|
|
1603
|
+
// hand-authored bare one — both wire the server.
|
|
1604
|
+
codexMcp = txt.split("\n").some((l) => l.trim() === "[mcp_servers.nlm-memory]");
|
|
1605
|
+
// Match the marketplace suffix, not the bare name: a [projects."…/nlm-memory-ts"]
|
|
1606
|
+
// trust entry is Codex's registry (not nlm's), so it must not trip this check.
|
|
1607
|
+
codexStale = txt.includes("@nlm-memory-ts");
|
|
1608
|
+
}
|
|
1609
|
+
const envPath = join(homedir(), ".nlm", ".env");
|
|
1610
|
+
return {
|
|
1611
|
+
daemon: { reachable, version, expectedVersion: pkg.version },
|
|
1612
|
+
env: {
|
|
1613
|
+
path: envPath,
|
|
1614
|
+
exists: existsSync(envPath),
|
|
1615
|
+
hasMcpToken: Boolean(process.env["NLM_MCP_TOKEN"]),
|
|
1616
|
+
},
|
|
1617
|
+
claudeCode: { configPath: mcpPath, mcpConfigured: claudeMcp },
|
|
1618
|
+
codex: {
|
|
1619
|
+
configPath: codexPath,
|
|
1620
|
+
configPresent: codexPresent,
|
|
1621
|
+
mcpConfigured: codexMcp,
|
|
1622
|
+
staleNlmMemoryTs: codexStale,
|
|
1623
|
+
},
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1499
1626
|
program
|
|
1500
1627
|
.command("doctor")
|
|
1501
|
-
.description("Check database integrity invariants and optionally repair safe violations")
|
|
1628
|
+
.description("Check database integrity invariants and install/runtime health, optionally repair safe violations")
|
|
1502
1629
|
.option("--fix", "repair mechanically safe violations: delete self-loop edges (I1), restore orphaned superseded/replaced sessions to closed (I2)")
|
|
1503
1630
|
.action(async (opts) => {
|
|
1504
1631
|
const storage = await buildStorage(dbPath());
|
|
@@ -1535,6 +1662,7 @@ program
|
|
|
1535
1662
|
const ALL_CHECKS = ["I1", "I2", "I3", "I4", "I5a", "I5b", "I6"];
|
|
1536
1663
|
const byId = new Map(violations.map((v) => [v.id, v]));
|
|
1537
1664
|
let anyFail = false;
|
|
1665
|
+
console.log("Database integrity:");
|
|
1538
1666
|
for (const id of ALL_CHECKS) {
|
|
1539
1667
|
const v = byId.get(id);
|
|
1540
1668
|
if (v) {
|
|
@@ -1546,9 +1674,142 @@ program
|
|
|
1546
1674
|
console.log(`PASS ${id}`);
|
|
1547
1675
|
}
|
|
1548
1676
|
}
|
|
1677
|
+
console.log("\nInstall & runtime health:");
|
|
1678
|
+
if (printHealthChecks(evaluateInstallHealth(await gatherInstallProbe())))
|
|
1679
|
+
anyFail = true;
|
|
1680
|
+
if (anyFail)
|
|
1681
|
+
process.exit(1);
|
|
1682
|
+
});
|
|
1683
|
+
function printHealthChecks(checks) {
|
|
1684
|
+
let anyFail = false;
|
|
1685
|
+
for (const c of checks) {
|
|
1686
|
+
if (c.status === "fail")
|
|
1687
|
+
anyFail = true;
|
|
1688
|
+
const label = c.status === "ok" ? "PASS" : c.status === "warn" ? "WARN" : "FAIL";
|
|
1689
|
+
const fix = c.fix ? `\n → fix: ${c.fix}` : "";
|
|
1690
|
+
console.log(`${label} ${c.id} ${c.detail}${fix}`);
|
|
1691
|
+
}
|
|
1692
|
+
return anyFail;
|
|
1693
|
+
}
|
|
1694
|
+
async function dbIntegrityCheck() {
|
|
1695
|
+
const storage = await buildStorage(dbPath());
|
|
1696
|
+
try {
|
|
1697
|
+
const violations = storage instanceof PgStorage
|
|
1698
|
+
? await runChecksOnPg(storage.pgPool())
|
|
1699
|
+
: runChecksOnSqlite(storage.rawDb());
|
|
1700
|
+
if (violations.length === 0)
|
|
1701
|
+
return { id: "db-integrity", status: "ok", detail: "all invariants hold" };
|
|
1702
|
+
return {
|
|
1703
|
+
id: "db-integrity",
|
|
1704
|
+
status: "fail",
|
|
1705
|
+
detail: `violations: ${violations.map((v) => v.id).join(", ")}`,
|
|
1706
|
+
fix: "nlm doctor --fix",
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
finally {
|
|
1710
|
+
await storage.close();
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
async function recallSmokeCheck() {
|
|
1714
|
+
let reachable = false;
|
|
1715
|
+
let status = null;
|
|
1716
|
+
let wellFormed = false;
|
|
1717
|
+
try {
|
|
1718
|
+
const ctrl = new AbortController();
|
|
1719
|
+
const timer = setTimeout(() => ctrl.abort(), 3_000);
|
|
1720
|
+
const res = await fetch(`http://localhost:${port()}/api/recall?q=verify&limit=1`, { signal: ctrl.signal });
|
|
1721
|
+
clearTimeout(timer);
|
|
1722
|
+
reachable = true;
|
|
1723
|
+
status = res.status;
|
|
1724
|
+
const body = (await res.json().catch(() => null));
|
|
1725
|
+
wellFormed = Boolean(body && Array.isArray(body.results));
|
|
1726
|
+
}
|
|
1727
|
+
catch {
|
|
1728
|
+
// unreachable — evaluateRecallSmoke reports the fail
|
|
1729
|
+
}
|
|
1730
|
+
return evaluateRecallSmoke(reachable, status, wellFormed);
|
|
1731
|
+
}
|
|
1732
|
+
program
|
|
1733
|
+
.command("verify")
|
|
1734
|
+
.description("Release-gate: verify the install is wired and recall works end-to-end. Exit 1 on any failure.")
|
|
1735
|
+
.action(async () => {
|
|
1736
|
+
const provider = (process.env["NLM_CLASSIFIER"] ?? "ollama").toLowerCase();
|
|
1737
|
+
const classifierModel = process.env["NLM_CLASSIFIER_MODEL"] ?? (provider === "ollama" ? "qwen3.5:4b" : "deepseek-v4-flash");
|
|
1738
|
+
const classifierPresent = provider === "ollama" ? ollamaModelPresent(classifierModel) : true;
|
|
1739
|
+
const checks = [
|
|
1740
|
+
...evaluateInstallHealth(await gatherInstallProbe()),
|
|
1741
|
+
evaluateModelHealth(embeddingModelPresent(), classifierModel, classifierPresent),
|
|
1742
|
+
await dbIntegrityCheck(),
|
|
1743
|
+
await recallSmokeCheck(),
|
|
1744
|
+
];
|
|
1745
|
+
const anyFail = printHealthChecks(checks);
|
|
1746
|
+
const warns = checks.filter((c) => c.status === "warn").length;
|
|
1747
|
+
console.log(anyFail ? "\nVERIFY: FAIL" : warns > 0 ? `\nVERIFY: PASS (${warns} warning(s))` : "\nVERIFY: PASS");
|
|
1549
1748
|
if (anyFail)
|
|
1550
1749
|
process.exit(1);
|
|
1551
1750
|
});
|
|
1751
|
+
program
|
|
1752
|
+
.command("backup")
|
|
1753
|
+
.description("Write a dated snapshot to ~/.nlm/backups/ and prune ones past the retention window")
|
|
1754
|
+
.option("--retention <days>", "keep snapshots from the last N days", "7")
|
|
1755
|
+
.action(async (opts) => {
|
|
1756
|
+
const retention = Number.parseInt(opts.retention, 10);
|
|
1757
|
+
if (!Number.isFinite(retention) || retention < 1) {
|
|
1758
|
+
console.error("nlm backup: --retention must be a positive integer");
|
|
1759
|
+
process.exit(1);
|
|
1760
|
+
}
|
|
1761
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
1762
|
+
const storage = await buildStorage(dbPath());
|
|
1763
|
+
try {
|
|
1764
|
+
if (storage instanceof PgStorage) {
|
|
1765
|
+
console.error("nlm backup: only the SQLite backend is supported (Postgres: use pg_dump)");
|
|
1766
|
+
process.exit(1);
|
|
1767
|
+
}
|
|
1768
|
+
const result = runRollingBackup(storage.rawDb(), dbPath(), today, retention);
|
|
1769
|
+
console.log(`Wrote ${result.written} (${(result.bytes / 1_048_576).toFixed(1)} MiB)`);
|
|
1770
|
+
if (result.pruned.length > 0)
|
|
1771
|
+
console.log(`Pruned ${result.pruned.length} snapshot(s) older than ${retention}d`);
|
|
1772
|
+
}
|
|
1773
|
+
finally {
|
|
1774
|
+
await storage.close();
|
|
1775
|
+
}
|
|
1776
|
+
});
|
|
1777
|
+
program
|
|
1778
|
+
.command("restore")
|
|
1779
|
+
.description("Stage a dated backup for restore on next daemon start (does not overwrite the live DB in place)")
|
|
1780
|
+
.option("--from <date>", "backup date to restore (YYYY-MM-DD)")
|
|
1781
|
+
.option("--list", "list available backup dates")
|
|
1782
|
+
.action((opts) => {
|
|
1783
|
+
if (opts.list || !opts.from) {
|
|
1784
|
+
const dates = listBackupDates(dbPath());
|
|
1785
|
+
if (dates.length === 0) {
|
|
1786
|
+
console.log("No backups found. Run `nlm backup` (or wait for the daily job).");
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
console.log("Available backups:");
|
|
1790
|
+
for (const d of dates)
|
|
1791
|
+
console.log(` ${d}`);
|
|
1792
|
+
if (!opts.from)
|
|
1793
|
+
console.log("\nRestore one with: nlm restore --from <date>");
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
const backup = resolveBackup(dbPath(), opts.from);
|
|
1797
|
+
if (!backup) {
|
|
1798
|
+
console.error(`nlm restore: no backup for ${opts.from}. Run \`nlm restore --list\` to see options.`);
|
|
1799
|
+
process.exit(1);
|
|
1800
|
+
}
|
|
1801
|
+
// Copy (not move) the snapshot to a scratch candidate so the backup is
|
|
1802
|
+
// preserved; stageRestore renames the candidate into the pending slot.
|
|
1803
|
+
const candidate = `${dbPath()}.restore-candidate`;
|
|
1804
|
+
copyFileSync(backup, candidate);
|
|
1805
|
+
const validation = stageRestore(dbPath(), candidate);
|
|
1806
|
+
if (!validation.ok) {
|
|
1807
|
+
console.error(`nlm restore: ${opts.from} is not a usable backup: ${validation.error}`);
|
|
1808
|
+
process.exit(1);
|
|
1809
|
+
}
|
|
1810
|
+
console.log(`Staged ${opts.from} (${validation.sessions} sessions, schema v${validation.schemaVersion}).`);
|
|
1811
|
+
console.log("Run `nlm restart` to apply — the current DB is archived aside, not deleted.");
|
|
1812
|
+
});
|
|
1552
1813
|
program.parseAsync().catch((e) => {
|
|
1553
1814
|
console.error("nlm: fatal", e);
|
|
1554
1815
|
process.exit(1);
|