nlm-memory 0.12.0 → 0.13.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.
- 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 +327 -56
- 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-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-Fr5AMEPj.js → index-CVo7KKw2.js} +1 -1
- package/dist/ui/assets/{index-C74N8APQ.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,24 +211,38 @@ 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
|
|
230
230
|
// mounts, zero attack surface. Present = token-gated Streamable-HTTP MCP
|
|
231
231
|
// endpoint for container agents (e.g. Hermes WebUI).
|
|
232
232
|
...(hasMcpToken
|
|
233
|
-
? {
|
|
233
|
+
? {
|
|
234
|
+
mcpDeps: {
|
|
235
|
+
recall,
|
|
236
|
+
store,
|
|
237
|
+
factRecall,
|
|
238
|
+
factStore: facts,
|
|
239
|
+
// Parity with the stdio `nlm mcp` server: remote/container MCP
|
|
240
|
+
// clients hitting POST /mcp get recall_code too when the flag is on.
|
|
241
|
+
exemplarStore: storage.exemplars,
|
|
242
|
+
codeEmbedder: new OllamaCodeEmbedder({ baseUrl: ollamaUrl() }),
|
|
243
|
+
installScope: scope,
|
|
244
|
+
},
|
|
245
|
+
}
|
|
234
246
|
: {}),
|
|
235
247
|
});
|
|
236
248
|
const p = port();
|
|
@@ -294,15 +306,14 @@ program
|
|
|
294
306
|
const memoSweep = new MemoSweepScheduler();
|
|
295
307
|
memoSweep.start();
|
|
296
308
|
console.error(" memo sweep: dormant cleanup every 5m (threshold 24h)");
|
|
297
|
-
if (opts.scheduler !== false
|
|
309
|
+
if (opts.scheduler !== false) {
|
|
298
310
|
const adapters = await buildAdapters(sources);
|
|
299
311
|
if (adapters.length === 0) {
|
|
300
312
|
console.error(" scheduler: no adapters detected (set NLM_ADAPTERS to force-enable)");
|
|
301
313
|
}
|
|
302
314
|
else {
|
|
303
315
|
const scheduler = new ScanScheduler({
|
|
304
|
-
|
|
305
|
-
store: store,
|
|
316
|
+
store,
|
|
306
317
|
adapters,
|
|
307
318
|
classifier,
|
|
308
319
|
embedder,
|
|
@@ -378,6 +389,58 @@ program
|
|
|
378
389
|
await storage.close();
|
|
379
390
|
}
|
|
380
391
|
});
|
|
392
|
+
program
|
|
393
|
+
.command("recall-code")
|
|
394
|
+
.description("Semantic search over code exemplars (requires NLM_CODE_EXEMPLARS_ENABLED=1)")
|
|
395
|
+
.argument("<query>", "natural-language description of the task you are about to implement")
|
|
396
|
+
.option("-r, --repo <path>", "filter by repository path")
|
|
397
|
+
.option("-l, --lang <lang>", "filter by language (ts, py, go, ...)")
|
|
398
|
+
.option("-k <n>", "max results", (v) => Number.parseInt(v, 10), 5)
|
|
399
|
+
.option("--no-negatives", "exclude fail/exhausted exemplars from results")
|
|
400
|
+
.option("--json", "emit the raw JSON result")
|
|
401
|
+
.action(async (query, opts) => {
|
|
402
|
+
if (process.env["NLM_CODE_EXEMPLARS_ENABLED"] !== "1") {
|
|
403
|
+
process.stderr.write("recall-code requires NLM_CODE_EXEMPLARS_ENABLED=1\n");
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
const { storage } = await buildStack();
|
|
407
|
+
const { OllamaCodeEmbedder } = await import("../llm/ollama-code-embedder.js");
|
|
408
|
+
const { recallCode } = await import("../core/exemplars/recall-code.js");
|
|
409
|
+
const { installScope: getScope } = await import("../core/signals/install-scope.js");
|
|
410
|
+
try {
|
|
411
|
+
const embedder = new OllamaCodeEmbedder();
|
|
412
|
+
const result = await recallCode({
|
|
413
|
+
query,
|
|
414
|
+
installScope: getScope(),
|
|
415
|
+
...(opts.repo ? { repo: opts.repo } : {}),
|
|
416
|
+
...(opts.lang ? { lang: opts.lang } : {}),
|
|
417
|
+
k: opts.k ?? 5,
|
|
418
|
+
includeNegatives: opts.negatives !== false,
|
|
419
|
+
}, storage.exemplars, embedder, null);
|
|
420
|
+
if (opts.json) {
|
|
421
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
const printHits = (label, hits) => {
|
|
425
|
+
if (hits.length === 0)
|
|
426
|
+
return;
|
|
427
|
+
process.stdout.write(`\n── ${label} ──\n`);
|
|
428
|
+
for (const h of hits) {
|
|
429
|
+
process.stdout.write(`[${h.outcome}] ${h.taskContext} (${h.repo}, dist=${h.distance.toFixed(4)})\n`);
|
|
430
|
+
process.stdout.write(h.code.slice(0, 400) + (h.code.length > 400 ? "\n…" : "") + "\n\n");
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
printHits("Positives (pass / fix)", result.positives);
|
|
434
|
+
printHits("Negatives (fail / exhausted) — avoid these patterns", result.negatives);
|
|
435
|
+
if (result.positives.length === 0 && result.negatives.length === 0) {
|
|
436
|
+
process.stdout.write("(no exemplars found)\n");
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
finally {
|
|
441
|
+
await storage.close();
|
|
442
|
+
}
|
|
443
|
+
});
|
|
381
444
|
program
|
|
382
445
|
.command("misses")
|
|
383
446
|
.description("Show sessions the agent explicitly fetched but the hook never surfaced (recall miss log)")
|
|
@@ -480,11 +543,11 @@ program
|
|
|
480
543
|
.description("Run TS classifier against ~/.nlm/canonical.sqlite and diff vs persisted Python output")
|
|
481
544
|
.option("-l, --limit <n>", "sessions to sample", (v) => Number.parseInt(v, 10), 10)
|
|
482
545
|
.option("-p, --provider <name>", "deepseek | ollama", "deepseek")
|
|
483
|
-
.option("-m, --model <name>", "model tag (default: deepseek-v4-flash for deepseek, qwen3:4b
|
|
546
|
+
.option("-m, --model <name>", "model tag (default: deepseek-v4-flash for deepseek, qwen3.5:4b for ollama)")
|
|
484
547
|
.option("-v, --verbose", "per-session diff lines on stderr")
|
|
485
548
|
.action(async (opts) => {
|
|
486
549
|
const provider = opts.provider === "ollama" ? "ollama" : "deepseek";
|
|
487
|
-
const defaultModel = provider === "deepseek" ? "deepseek-v4-flash" : "qwen3:4b
|
|
550
|
+
const defaultModel = provider === "deepseek" ? "deepseek-v4-flash" : "qwen3.5:4b";
|
|
488
551
|
const report = await runParity({
|
|
489
552
|
limit: opts.limit,
|
|
490
553
|
dbPath: dbPath(),
|
|
@@ -532,8 +595,7 @@ program
|
|
|
532
595
|
const { storage, store, facts, embedder, classifier } = await buildStack();
|
|
533
596
|
try {
|
|
534
597
|
const report = await backfillFacts({
|
|
535
|
-
|
|
536
|
-
store: store,
|
|
598
|
+
store,
|
|
537
599
|
factStore: facts,
|
|
538
600
|
classifier,
|
|
539
601
|
embedder: opts.embed === false ? null : embedder,
|
|
@@ -576,8 +638,16 @@ program
|
|
|
576
638
|
.command("mcp")
|
|
577
639
|
.description("Run as an MCP stdio server (for ~/.mcp.json)")
|
|
578
640
|
.action(async () => {
|
|
579
|
-
const { recall, store, facts, factRecall } = await buildStack();
|
|
580
|
-
const server = createMcpServer({
|
|
641
|
+
const { recall, store, facts, factRecall, storage, scope } = await buildStack();
|
|
642
|
+
const server = createMcpServer({
|
|
643
|
+
recall,
|
|
644
|
+
store,
|
|
645
|
+
factStore: facts,
|
|
646
|
+
factRecall,
|
|
647
|
+
exemplarStore: storage.exemplars,
|
|
648
|
+
codeEmbedder: new OllamaCodeEmbedder({ baseUrl: ollamaUrl() }),
|
|
649
|
+
installScope: scope,
|
|
650
|
+
});
|
|
581
651
|
const transport = new StdioServerTransport();
|
|
582
652
|
await server.connect(transport);
|
|
583
653
|
});
|
|
@@ -1029,6 +1099,7 @@ connect
|
|
|
1029
1099
|
.option("--source <source>", "marketplace source (owner/repo, git URL, or local path)", "pbmagnet4/nlm-memory")
|
|
1030
1100
|
.option("--local", "shortcut for --source <repo-root>; use during dev")
|
|
1031
1101
|
.option("--with-hooks", "additionally write absolute paths to ~/.codex/hooks.json (Codex Desktop fallback for openai/codex#16430)")
|
|
1102
|
+
.option("--repair", "first strip a stale pre-rename nlm-memory-ts install (config block + plugin + marketplace), then connect")
|
|
1032
1103
|
.option("--dry-run", "print what would happen without invoking codex")
|
|
1033
1104
|
.action((opts) => {
|
|
1034
1105
|
if (!opts.dryRun && !codexBinaryAvailable()) {
|
|
@@ -1036,8 +1107,19 @@ connect
|
|
|
1036
1107
|
process.exit(1);
|
|
1037
1108
|
}
|
|
1038
1109
|
const source = opts.local ? REPO_ROOT : opts.source;
|
|
1039
|
-
const
|
|
1110
|
+
const connectOpts = { source, withHooks: Boolean(opts.withHooks), dryRun: Boolean(opts.dryRun) };
|
|
1111
|
+
const repair = opts.repair
|
|
1112
|
+
? repairCodex(connectOpts, pluginScriptsDir(REPO_ROOT))
|
|
1113
|
+
: null;
|
|
1114
|
+
const report = repair ? repair.connect : connectCodex(connectOpts, pluginScriptsDir(REPO_ROOT));
|
|
1115
|
+
if (repair && !repair.dryRun) {
|
|
1116
|
+
console.error(repair.staleMcpRemovedFromConfig
|
|
1117
|
+
? "nlm connect codex --repair: stripped stale nlm-memory-ts MCP block from config.toml"
|
|
1118
|
+
: "nlm connect codex --repair: no stale nlm-memory-ts MCP block found");
|
|
1119
|
+
}
|
|
1040
1120
|
if (report.dryRun) {
|
|
1121
|
+
if (repair)
|
|
1122
|
+
console.error(" --repair: would strip any stale nlm-memory-ts block, then:");
|
|
1041
1123
|
console.error("nlm connect codex (dry run):");
|
|
1042
1124
|
console.error(` codex plugin marketplace add ${report.source}`);
|
|
1043
1125
|
console.error(` codex plugin add ${report.pluginName}@${report.marketplaceName}`);
|
|
@@ -1065,6 +1147,9 @@ connect
|
|
|
1065
1147
|
if (report.mcpServerWritten) {
|
|
1066
1148
|
console.error(` Wrote [mcp_servers.nlm-memory] to ${report.mcpServerWritten}`);
|
|
1067
1149
|
}
|
|
1150
|
+
else if (report.mcpServerAlreadyPresent) {
|
|
1151
|
+
console.error(" Left your existing [mcp_servers.nlm-memory] block in place (not overwriting it)");
|
|
1152
|
+
}
|
|
1068
1153
|
if (report.legacyHooksWritten) {
|
|
1069
1154
|
console.error(` Wrote hooks.json fallback to ${report.legacyHooksWritten}`);
|
|
1070
1155
|
}
|
|
@@ -1150,9 +1235,7 @@ connect
|
|
|
1150
1235
|
const storage = SqliteStorage.create({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
|
|
1151
1236
|
await storage.init();
|
|
1152
1237
|
try {
|
|
1153
|
-
|
|
1154
|
-
const registry = new SourceRegistry(storage.rawDb());
|
|
1155
|
-
const report = connectCursor(registry, {
|
|
1238
|
+
const report = await connectCursor(storage.sources, {
|
|
1156
1239
|
...(opts.dbPath ? { dbPath: opts.dbPath } : {}),
|
|
1157
1240
|
dryRun: Boolean(opts.dryRun),
|
|
1158
1241
|
});
|
|
@@ -1184,9 +1267,7 @@ connect
|
|
|
1184
1267
|
const storage = SqliteStorage.create({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
|
|
1185
1268
|
await storage.init();
|
|
1186
1269
|
try {
|
|
1187
|
-
|
|
1188
|
-
const registry = new SourceRegistry(storage.rawDb());
|
|
1189
|
-
const report = connectWindsurf(registry, {
|
|
1270
|
+
const report = await connectWindsurf(storage.sources, {
|
|
1190
1271
|
...(opts.userDir ? { userDir: opts.userDir } : {}),
|
|
1191
1272
|
dryRun: Boolean(opts.dryRun),
|
|
1192
1273
|
});
|
|
@@ -1349,9 +1430,7 @@ disconnect
|
|
|
1349
1430
|
const storage = SqliteStorage.create({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
|
|
1350
1431
|
await storage.init();
|
|
1351
1432
|
try {
|
|
1352
|
-
|
|
1353
|
-
const registry = new SourceRegistry(storage.rawDb());
|
|
1354
|
-
const report = disconnectCursor(registry, { dryRun: Boolean(opts.dryRun) });
|
|
1433
|
+
const report = await disconnectCursor(storage.sources, { dryRun: Boolean(opts.dryRun) });
|
|
1355
1434
|
if (opts.dryRun) {
|
|
1356
1435
|
console.error("nlm disconnect cursor (dry run): disable Cursor source in registry");
|
|
1357
1436
|
if (opts.withRules)
|
|
@@ -1379,9 +1458,7 @@ disconnect
|
|
|
1379
1458
|
const storage = SqliteStorage.create({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
|
|
1380
1459
|
await storage.init();
|
|
1381
1460
|
try {
|
|
1382
|
-
|
|
1383
|
-
const registry = new SourceRegistry(storage.rawDb());
|
|
1384
|
-
const report = disconnectWindsurf(registry, { dryRun: Boolean(opts.dryRun) });
|
|
1461
|
+
const report = await disconnectWindsurf(storage.sources, { dryRun: Boolean(opts.dryRun) });
|
|
1385
1462
|
if (opts.dryRun) {
|
|
1386
1463
|
console.error("nlm disconnect windsurf (dry run): disable Windsurf source in registry");
|
|
1387
1464
|
if (opts.withRules)
|
|
@@ -1498,9 +1575,69 @@ program
|
|
|
1498
1575
|
process.exit(1);
|
|
1499
1576
|
}
|
|
1500
1577
|
});
|
|
1578
|
+
async function gatherInstallProbe() {
|
|
1579
|
+
autoloadEnv();
|
|
1580
|
+
let reachable = false;
|
|
1581
|
+
let version = null;
|
|
1582
|
+
try {
|
|
1583
|
+
const ctrl = new AbortController();
|
|
1584
|
+
const timer = setTimeout(() => ctrl.abort(), 1_500);
|
|
1585
|
+
const res = await fetch(`http://localhost:${port()}/api/health`, { signal: ctrl.signal });
|
|
1586
|
+
clearTimeout(timer);
|
|
1587
|
+
if (res.ok) {
|
|
1588
|
+
const body = (await res.json());
|
|
1589
|
+
reachable = true;
|
|
1590
|
+
version = typeof body.version === "string" ? body.version : null;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
catch {
|
|
1594
|
+
// unreachable — reported as a FAIL by the evaluator
|
|
1595
|
+
}
|
|
1596
|
+
const mcpPath = mcpConfigPath();
|
|
1597
|
+
let claudeMcp = false;
|
|
1598
|
+
try {
|
|
1599
|
+
if (existsSync(mcpPath)) {
|
|
1600
|
+
const cfg = JSON.parse(readFileSync(mcpPath, "utf8"));
|
|
1601
|
+
claudeMcp = Boolean(cfg.mcpServers && "nlm-memory" in cfg.mcpServers);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
catch {
|
|
1605
|
+
// malformed config → treat as unconfigured (connect will rewrite it)
|
|
1606
|
+
}
|
|
1607
|
+
const codexPath = codexConfigPath();
|
|
1608
|
+
let codexPresent = false;
|
|
1609
|
+
let codexMcp = false;
|
|
1610
|
+
let codexStale = false;
|
|
1611
|
+
if (existsSync(codexPath)) {
|
|
1612
|
+
codexPresent = true;
|
|
1613
|
+
const txt = readFileSync(codexPath, "utf8");
|
|
1614
|
+
// Recognize the MCP table whether it's our managed (sentineled) block or a
|
|
1615
|
+
// hand-authored bare one — both wire the server.
|
|
1616
|
+
codexMcp = txt.split("\n").some((l) => l.trim() === "[mcp_servers.nlm-memory]");
|
|
1617
|
+
// Match the marketplace suffix, not the bare name: a [projects."…/nlm-memory-ts"]
|
|
1618
|
+
// trust entry is Codex's registry (not nlm's), so it must not trip this check.
|
|
1619
|
+
codexStale = txt.includes("@nlm-memory-ts");
|
|
1620
|
+
}
|
|
1621
|
+
const envPath = join(homedir(), ".nlm", ".env");
|
|
1622
|
+
return {
|
|
1623
|
+
daemon: { reachable, version, expectedVersion: pkg.version },
|
|
1624
|
+
env: {
|
|
1625
|
+
path: envPath,
|
|
1626
|
+
exists: existsSync(envPath),
|
|
1627
|
+
hasMcpToken: Boolean(process.env["NLM_MCP_TOKEN"]),
|
|
1628
|
+
},
|
|
1629
|
+
claudeCode: { configPath: mcpPath, mcpConfigured: claudeMcp },
|
|
1630
|
+
codex: {
|
|
1631
|
+
configPath: codexPath,
|
|
1632
|
+
configPresent: codexPresent,
|
|
1633
|
+
mcpConfigured: codexMcp,
|
|
1634
|
+
staleNlmMemoryTs: codexStale,
|
|
1635
|
+
},
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1501
1638
|
program
|
|
1502
1639
|
.command("doctor")
|
|
1503
|
-
.description("Check database integrity invariants and optionally repair safe violations")
|
|
1640
|
+
.description("Check database integrity invariants and install/runtime health, optionally repair safe violations")
|
|
1504
1641
|
.option("--fix", "repair mechanically safe violations: delete self-loop edges (I1), restore orphaned superseded/replaced sessions to closed (I2)")
|
|
1505
1642
|
.action(async (opts) => {
|
|
1506
1643
|
const storage = await buildStorage(dbPath());
|
|
@@ -1537,6 +1674,7 @@ program
|
|
|
1537
1674
|
const ALL_CHECKS = ["I1", "I2", "I3", "I4", "I5a", "I5b", "I6"];
|
|
1538
1675
|
const byId = new Map(violations.map((v) => [v.id, v]));
|
|
1539
1676
|
let anyFail = false;
|
|
1677
|
+
console.log("Database integrity:");
|
|
1540
1678
|
for (const id of ALL_CHECKS) {
|
|
1541
1679
|
const v = byId.get(id);
|
|
1542
1680
|
if (v) {
|
|
@@ -1548,9 +1686,142 @@ program
|
|
|
1548
1686
|
console.log(`PASS ${id}`);
|
|
1549
1687
|
}
|
|
1550
1688
|
}
|
|
1689
|
+
console.log("\nInstall & runtime health:");
|
|
1690
|
+
if (printHealthChecks(evaluateInstallHealth(await gatherInstallProbe())))
|
|
1691
|
+
anyFail = true;
|
|
1551
1692
|
if (anyFail)
|
|
1552
1693
|
process.exit(1);
|
|
1553
1694
|
});
|
|
1695
|
+
function printHealthChecks(checks) {
|
|
1696
|
+
let anyFail = false;
|
|
1697
|
+
for (const c of checks) {
|
|
1698
|
+
if (c.status === "fail")
|
|
1699
|
+
anyFail = true;
|
|
1700
|
+
const label = c.status === "ok" ? "PASS" : c.status === "warn" ? "WARN" : "FAIL";
|
|
1701
|
+
const fix = c.fix ? `\n → fix: ${c.fix}` : "";
|
|
1702
|
+
console.log(`${label} ${c.id} ${c.detail}${fix}`);
|
|
1703
|
+
}
|
|
1704
|
+
return anyFail;
|
|
1705
|
+
}
|
|
1706
|
+
async function dbIntegrityCheck() {
|
|
1707
|
+
const storage = await buildStorage(dbPath());
|
|
1708
|
+
try {
|
|
1709
|
+
const violations = storage instanceof PgStorage
|
|
1710
|
+
? await runChecksOnPg(storage.pgPool())
|
|
1711
|
+
: runChecksOnSqlite(storage.rawDb());
|
|
1712
|
+
if (violations.length === 0)
|
|
1713
|
+
return { id: "db-integrity", status: "ok", detail: "all invariants hold" };
|
|
1714
|
+
return {
|
|
1715
|
+
id: "db-integrity",
|
|
1716
|
+
status: "fail",
|
|
1717
|
+
detail: `violations: ${violations.map((v) => v.id).join(", ")}`,
|
|
1718
|
+
fix: "nlm doctor --fix",
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
finally {
|
|
1722
|
+
await storage.close();
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
async function recallSmokeCheck() {
|
|
1726
|
+
let reachable = false;
|
|
1727
|
+
let status = null;
|
|
1728
|
+
let wellFormed = false;
|
|
1729
|
+
try {
|
|
1730
|
+
const ctrl = new AbortController();
|
|
1731
|
+
const timer = setTimeout(() => ctrl.abort(), 3_000);
|
|
1732
|
+
const res = await fetch(`http://localhost:${port()}/api/recall?q=verify&limit=1`, { signal: ctrl.signal });
|
|
1733
|
+
clearTimeout(timer);
|
|
1734
|
+
reachable = true;
|
|
1735
|
+
status = res.status;
|
|
1736
|
+
const body = (await res.json().catch(() => null));
|
|
1737
|
+
wellFormed = Boolean(body && Array.isArray(body.results));
|
|
1738
|
+
}
|
|
1739
|
+
catch {
|
|
1740
|
+
// unreachable — evaluateRecallSmoke reports the fail
|
|
1741
|
+
}
|
|
1742
|
+
return evaluateRecallSmoke(reachable, status, wellFormed);
|
|
1743
|
+
}
|
|
1744
|
+
program
|
|
1745
|
+
.command("verify")
|
|
1746
|
+
.description("Release-gate: verify the install is wired and recall works end-to-end. Exit 1 on any failure.")
|
|
1747
|
+
.action(async () => {
|
|
1748
|
+
const provider = (process.env["NLM_CLASSIFIER"] ?? "ollama").toLowerCase();
|
|
1749
|
+
const classifierModel = process.env["NLM_CLASSIFIER_MODEL"] ?? (provider === "ollama" ? "qwen3.5:4b" : "deepseek-v4-flash");
|
|
1750
|
+
const classifierPresent = provider === "ollama" ? ollamaModelPresent(classifierModel) : true;
|
|
1751
|
+
const checks = [
|
|
1752
|
+
...evaluateInstallHealth(await gatherInstallProbe()),
|
|
1753
|
+
evaluateModelHealth(embeddingModelPresent(), classifierModel, classifierPresent),
|
|
1754
|
+
await dbIntegrityCheck(),
|
|
1755
|
+
await recallSmokeCheck(),
|
|
1756
|
+
];
|
|
1757
|
+
const anyFail = printHealthChecks(checks);
|
|
1758
|
+
const warns = checks.filter((c) => c.status === "warn").length;
|
|
1759
|
+
console.log(anyFail ? "\nVERIFY: FAIL" : warns > 0 ? `\nVERIFY: PASS (${warns} warning(s))` : "\nVERIFY: PASS");
|
|
1760
|
+
if (anyFail)
|
|
1761
|
+
process.exit(1);
|
|
1762
|
+
});
|
|
1763
|
+
program
|
|
1764
|
+
.command("backup")
|
|
1765
|
+
.description("Write a dated snapshot to ~/.nlm/backups/ and prune ones past the retention window")
|
|
1766
|
+
.option("--retention <days>", "keep snapshots from the last N days", "7")
|
|
1767
|
+
.action(async (opts) => {
|
|
1768
|
+
const retention = Number.parseInt(opts.retention, 10);
|
|
1769
|
+
if (!Number.isFinite(retention) || retention < 1) {
|
|
1770
|
+
console.error("nlm backup: --retention must be a positive integer");
|
|
1771
|
+
process.exit(1);
|
|
1772
|
+
}
|
|
1773
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
1774
|
+
const storage = await buildStorage(dbPath());
|
|
1775
|
+
try {
|
|
1776
|
+
if (storage instanceof PgStorage) {
|
|
1777
|
+
console.error("nlm backup: only the SQLite backend is supported (Postgres: use pg_dump)");
|
|
1778
|
+
process.exit(1);
|
|
1779
|
+
}
|
|
1780
|
+
const result = runRollingBackup(storage.rawDb(), dbPath(), today, retention);
|
|
1781
|
+
console.log(`Wrote ${result.written} (${(result.bytes / 1_048_576).toFixed(1)} MiB)`);
|
|
1782
|
+
if (result.pruned.length > 0)
|
|
1783
|
+
console.log(`Pruned ${result.pruned.length} snapshot(s) older than ${retention}d`);
|
|
1784
|
+
}
|
|
1785
|
+
finally {
|
|
1786
|
+
await storage.close();
|
|
1787
|
+
}
|
|
1788
|
+
});
|
|
1789
|
+
program
|
|
1790
|
+
.command("restore")
|
|
1791
|
+
.description("Stage a dated backup for restore on next daemon start (does not overwrite the live DB in place)")
|
|
1792
|
+
.option("--from <date>", "backup date to restore (YYYY-MM-DD)")
|
|
1793
|
+
.option("--list", "list available backup dates")
|
|
1794
|
+
.action((opts) => {
|
|
1795
|
+
if (opts.list || !opts.from) {
|
|
1796
|
+
const dates = listBackupDates(dbPath());
|
|
1797
|
+
if (dates.length === 0) {
|
|
1798
|
+
console.log("No backups found. Run `nlm backup` (or wait for the daily job).");
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
console.log("Available backups:");
|
|
1802
|
+
for (const d of dates)
|
|
1803
|
+
console.log(` ${d}`);
|
|
1804
|
+
if (!opts.from)
|
|
1805
|
+
console.log("\nRestore one with: nlm restore --from <date>");
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
const backup = resolveBackup(dbPath(), opts.from);
|
|
1809
|
+
if (!backup) {
|
|
1810
|
+
console.error(`nlm restore: no backup for ${opts.from}. Run \`nlm restore --list\` to see options.`);
|
|
1811
|
+
process.exit(1);
|
|
1812
|
+
}
|
|
1813
|
+
// Copy (not move) the snapshot to a scratch candidate so the backup is
|
|
1814
|
+
// preserved; stageRestore renames the candidate into the pending slot.
|
|
1815
|
+
const candidate = `${dbPath()}.restore-candidate`;
|
|
1816
|
+
copyFileSync(backup, candidate);
|
|
1817
|
+
const validation = stageRestore(dbPath(), candidate);
|
|
1818
|
+
if (!validation.ok) {
|
|
1819
|
+
console.error(`nlm restore: ${opts.from} is not a usable backup: ${validation.error}`);
|
|
1820
|
+
process.exit(1);
|
|
1821
|
+
}
|
|
1822
|
+
console.log(`Staged ${opts.from} (${validation.sessions} sessions, schema v${validation.schemaVersion}).`);
|
|
1823
|
+
console.log("Run `nlm restart` to apply — the current DB is archived aside, not deleted.");
|
|
1824
|
+
});
|
|
1554
1825
|
program.parseAsync().catch((e) => {
|
|
1555
1826
|
console.error("nlm: fatal", e);
|
|
1556
1827
|
process.exit(1);
|