nlm-memory 0.12.0 → 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 +314 -55
- 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,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)")
|
|
@@ -480,11 +531,11 @@ program
|
|
|
480
531
|
.description("Run TS classifier against ~/.nlm/canonical.sqlite and diff vs persisted Python output")
|
|
481
532
|
.option("-l, --limit <n>", "sessions to sample", (v) => Number.parseInt(v, 10), 10)
|
|
482
533
|
.option("-p, --provider <name>", "deepseek | ollama", "deepseek")
|
|
483
|
-
.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)")
|
|
484
535
|
.option("-v, --verbose", "per-session diff lines on stderr")
|
|
485
536
|
.action(async (opts) => {
|
|
486
537
|
const provider = opts.provider === "ollama" ? "ollama" : "deepseek";
|
|
487
|
-
const defaultModel = provider === "deepseek" ? "deepseek-v4-flash" : "qwen3:4b
|
|
538
|
+
const defaultModel = provider === "deepseek" ? "deepseek-v4-flash" : "qwen3.5:4b";
|
|
488
539
|
const report = await runParity({
|
|
489
540
|
limit: opts.limit,
|
|
490
541
|
dbPath: dbPath(),
|
|
@@ -532,8 +583,7 @@ program
|
|
|
532
583
|
const { storage, store, facts, embedder, classifier } = await buildStack();
|
|
533
584
|
try {
|
|
534
585
|
const report = await backfillFacts({
|
|
535
|
-
|
|
536
|
-
store: store,
|
|
586
|
+
store,
|
|
537
587
|
factStore: facts,
|
|
538
588
|
classifier,
|
|
539
589
|
embedder: opts.embed === false ? null : embedder,
|
|
@@ -576,8 +626,16 @@ program
|
|
|
576
626
|
.command("mcp")
|
|
577
627
|
.description("Run as an MCP stdio server (for ~/.mcp.json)")
|
|
578
628
|
.action(async () => {
|
|
579
|
-
const { recall, store, facts, factRecall } = await buildStack();
|
|
580
|
-
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
|
+
});
|
|
581
639
|
const transport = new StdioServerTransport();
|
|
582
640
|
await server.connect(transport);
|
|
583
641
|
});
|
|
@@ -1029,6 +1087,7 @@ connect
|
|
|
1029
1087
|
.option("--source <source>", "marketplace source (owner/repo, git URL, or local path)", "pbmagnet4/nlm-memory")
|
|
1030
1088
|
.option("--local", "shortcut for --source <repo-root>; use during dev")
|
|
1031
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")
|
|
1032
1091
|
.option("--dry-run", "print what would happen without invoking codex")
|
|
1033
1092
|
.action((opts) => {
|
|
1034
1093
|
if (!opts.dryRun && !codexBinaryAvailable()) {
|
|
@@ -1036,8 +1095,19 @@ connect
|
|
|
1036
1095
|
process.exit(1);
|
|
1037
1096
|
}
|
|
1038
1097
|
const source = opts.local ? REPO_ROOT : opts.source;
|
|
1039
|
-
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
|
+
}
|
|
1040
1108
|
if (report.dryRun) {
|
|
1109
|
+
if (repair)
|
|
1110
|
+
console.error(" --repair: would strip any stale nlm-memory-ts block, then:");
|
|
1041
1111
|
console.error("nlm connect codex (dry run):");
|
|
1042
1112
|
console.error(` codex plugin marketplace add ${report.source}`);
|
|
1043
1113
|
console.error(` codex plugin add ${report.pluginName}@${report.marketplaceName}`);
|
|
@@ -1065,6 +1135,9 @@ connect
|
|
|
1065
1135
|
if (report.mcpServerWritten) {
|
|
1066
1136
|
console.error(` Wrote [mcp_servers.nlm-memory] to ${report.mcpServerWritten}`);
|
|
1067
1137
|
}
|
|
1138
|
+
else if (report.mcpServerAlreadyPresent) {
|
|
1139
|
+
console.error(" Left your existing [mcp_servers.nlm-memory] block in place (not overwriting it)");
|
|
1140
|
+
}
|
|
1068
1141
|
if (report.legacyHooksWritten) {
|
|
1069
1142
|
console.error(` Wrote hooks.json fallback to ${report.legacyHooksWritten}`);
|
|
1070
1143
|
}
|
|
@@ -1150,9 +1223,7 @@ connect
|
|
|
1150
1223
|
const storage = SqliteStorage.create({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
|
|
1151
1224
|
await storage.init();
|
|
1152
1225
|
try {
|
|
1153
|
-
|
|
1154
|
-
const registry = new SourceRegistry(storage.rawDb());
|
|
1155
|
-
const report = connectCursor(registry, {
|
|
1226
|
+
const report = await connectCursor(storage.sources, {
|
|
1156
1227
|
...(opts.dbPath ? { dbPath: opts.dbPath } : {}),
|
|
1157
1228
|
dryRun: Boolean(opts.dryRun),
|
|
1158
1229
|
});
|
|
@@ -1184,9 +1255,7 @@ connect
|
|
|
1184
1255
|
const storage = SqliteStorage.create({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
|
|
1185
1256
|
await storage.init();
|
|
1186
1257
|
try {
|
|
1187
|
-
|
|
1188
|
-
const registry = new SourceRegistry(storage.rawDb());
|
|
1189
|
-
const report = connectWindsurf(registry, {
|
|
1258
|
+
const report = await connectWindsurf(storage.sources, {
|
|
1190
1259
|
...(opts.userDir ? { userDir: opts.userDir } : {}),
|
|
1191
1260
|
dryRun: Boolean(opts.dryRun),
|
|
1192
1261
|
});
|
|
@@ -1349,9 +1418,7 @@ disconnect
|
|
|
1349
1418
|
const storage = SqliteStorage.create({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
|
|
1350
1419
|
await storage.init();
|
|
1351
1420
|
try {
|
|
1352
|
-
|
|
1353
|
-
const registry = new SourceRegistry(storage.rawDb());
|
|
1354
|
-
const report = disconnectCursor(registry, { dryRun: Boolean(opts.dryRun) });
|
|
1421
|
+
const report = await disconnectCursor(storage.sources, { dryRun: Boolean(opts.dryRun) });
|
|
1355
1422
|
if (opts.dryRun) {
|
|
1356
1423
|
console.error("nlm disconnect cursor (dry run): disable Cursor source in registry");
|
|
1357
1424
|
if (opts.withRules)
|
|
@@ -1379,9 +1446,7 @@ disconnect
|
|
|
1379
1446
|
const storage = SqliteStorage.create({ dbPath: dbPath(), migrationsDir: MIGRATIONS_DIR });
|
|
1380
1447
|
await storage.init();
|
|
1381
1448
|
try {
|
|
1382
|
-
|
|
1383
|
-
const registry = new SourceRegistry(storage.rawDb());
|
|
1384
|
-
const report = disconnectWindsurf(registry, { dryRun: Boolean(opts.dryRun) });
|
|
1449
|
+
const report = await disconnectWindsurf(storage.sources, { dryRun: Boolean(opts.dryRun) });
|
|
1385
1450
|
if (opts.dryRun) {
|
|
1386
1451
|
console.error("nlm disconnect windsurf (dry run): disable Windsurf source in registry");
|
|
1387
1452
|
if (opts.withRules)
|
|
@@ -1498,9 +1563,69 @@ program
|
|
|
1498
1563
|
process.exit(1);
|
|
1499
1564
|
}
|
|
1500
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
|
+
}
|
|
1501
1626
|
program
|
|
1502
1627
|
.command("doctor")
|
|
1503
|
-
.description("Check database integrity invariants and optionally repair safe violations")
|
|
1628
|
+
.description("Check database integrity invariants and install/runtime health, optionally repair safe violations")
|
|
1504
1629
|
.option("--fix", "repair mechanically safe violations: delete self-loop edges (I1), restore orphaned superseded/replaced sessions to closed (I2)")
|
|
1505
1630
|
.action(async (opts) => {
|
|
1506
1631
|
const storage = await buildStorage(dbPath());
|
|
@@ -1537,6 +1662,7 @@ program
|
|
|
1537
1662
|
const ALL_CHECKS = ["I1", "I2", "I3", "I4", "I5a", "I5b", "I6"];
|
|
1538
1663
|
const byId = new Map(violations.map((v) => [v.id, v]));
|
|
1539
1664
|
let anyFail = false;
|
|
1665
|
+
console.log("Database integrity:");
|
|
1540
1666
|
for (const id of ALL_CHECKS) {
|
|
1541
1667
|
const v = byId.get(id);
|
|
1542
1668
|
if (v) {
|
|
@@ -1548,9 +1674,142 @@ program
|
|
|
1548
1674
|
console.log(`PASS ${id}`);
|
|
1549
1675
|
}
|
|
1550
1676
|
}
|
|
1677
|
+
console.log("\nInstall & runtime health:");
|
|
1678
|
+
if (printHealthChecks(evaluateInstallHealth(await gatherInstallProbe())))
|
|
1679
|
+
anyFail = true;
|
|
1551
1680
|
if (anyFail)
|
|
1552
1681
|
process.exit(1);
|
|
1553
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");
|
|
1748
|
+
if (anyFail)
|
|
1749
|
+
process.exit(1);
|
|
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
|
+
});
|
|
1554
1813
|
program.parseAsync().catch((e) => {
|
|
1555
1814
|
console.error("nlm: fatal", e);
|
|
1556
1815
|
process.exit(1);
|