nlm-memory 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -34
- package/dist/cli/nlm.js +223 -33
- package/dist/cli/nlm.js.map +1 -1
- package/dist/core/adapters/cursor.d.ts +45 -0
- package/dist/core/adapters/cursor.js +397 -0
- package/dist/core/adapters/cursor.js.map +1 -0
- package/dist/core/adapters/from-source.js +10 -0
- package/dist/core/adapters/from-source.js.map +1 -1
- package/dist/core/adapters/windsurf.d.ts +44 -0
- package/dist/core/adapters/windsurf.js +299 -0
- package/dist/core/adapters/windsurf.js.map +1 -0
- package/dist/core/hook/claude-settings.d.ts +12 -5
- package/dist/core/hook/claude-settings.js +21 -6
- package/dist/core/hook/claude-settings.js.map +1 -1
- package/dist/core/sources/source-registry.d.ts +1 -1
- package/dist/core/sources/source-registry.js +18 -0
- package/dist/core/sources/source-registry.js.map +1 -1
- package/dist/core/storage/sqlite-session-store.d.ts +2 -0
- package/dist/core/storage/sqlite-session-store.js +38 -2
- package/dist/core/storage/sqlite-session-store.js.map +1 -1
- package/dist/hook/hook-auth.d.ts +13 -0
- package/dist/hook/hook-auth.js +19 -0
- package/dist/hook/hook-auth.js.map +1 -0
- package/dist/hook/prompt-recall-hook.js +7 -1
- package/dist/hook/prompt-recall-hook.js.map +1 -1
- package/dist/hook/session-start-hook.js +4 -1
- package/dist/hook/session-start-hook.js.map +1 -1
- package/dist/hook/stop-hook.js +4 -1
- package/dist/hook/stop-hook.js.map +1 -1
- package/dist/http/app.d.ts +2 -0
- package/dist/http/app.js +76 -1
- package/dist/http/app.js.map +1 -1
- package/dist/install/claude-code.js +1 -1
- package/dist/install/claude-code.js.map +1 -1
- package/dist/install/cursor.d.ts +25 -0
- package/dist/install/cursor.js +43 -0
- package/dist/install/cursor.js.map +1 -0
- package/dist/install/nlm-dir-perms.d.ts +19 -0
- package/dist/install/nlm-dir-perms.js +43 -0
- package/dist/install/nlm-dir-perms.js.map +1 -0
- package/dist/install/ollama.d.ts +18 -1
- package/dist/install/ollama.js +62 -7
- package/dist/install/ollama.js.map +1 -1
- package/dist/install/setup.d.ts +4 -0
- package/dist/install/setup.js +141 -18
- package/dist/install/setup.js.map +1 -1
- package/dist/install/windsurf.d.ts +25 -0
- package/dist/install/windsurf.js +43 -0
- package/dist/install/windsurf.js.map +1 -0
- package/dist/mcp/server.js +20 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/types.d.ts +4 -0
- package/dist/ui/assets/{index-BA6IpU8g.css → index-Beo8psd-.css} +1 -1
- package/dist/ui/assets/index-CSPTTeeM.js +69 -0
- package/dist/ui/index.html +2 -2
- package/package.json +26 -1
- package/plugin/scripts/prompt-recall-hook.mjs +55 -4
- package/plugin/scripts/stop-hook.mjs +57 -6
- package/.agents/plugins/marketplace.json +0 -20
- package/.github/workflows/ci.yml +0 -30
- package/dist/ui/assets/index-B_qIVV0k.js +0 -69
- package/docs/methodology/re-derivation-rate.md +0 -112
- package/docs/methodology/useful-hit-rate.md +0 -79
- package/docs/plans/2026-05-20-fts5-lexical-recall.md +0 -1088
- package/docs/plans/2026-05-20-recall-daemon-wedge-fix.md +0 -662
- package/docs/plans/2026-05-20-recall-hook-design.md +0 -131
- package/docs/plans/2026-05-20-recall-hook-implementation.md +0 -1222
- package/docs/plans/desktop-product.md +0 -69
- package/docs/plans/factstore-design.md +0 -236
- package/logs/CHANGELOG/CHANGELOG-2026.md +0 -1389
- package/logs/CHANGELOG/CHANGELOG.md +0 -337
- package/migrations/000_initial_schema.sql +0 -174
- package/migrations/001_entity_type_rename.sql +0 -17
- package/migrations/002_adapter_state_extend.sql +0 -12
- package/migrations/003_session_embeddings.sql +0 -11
- package/migrations/004_facts.sql +0 -46
- package/migrations/005_sources.sql +0 -31
- package/migrations/006_providers.sql +0 -33
- package/migrations/007_source_tokens.sql +0 -17
- package/migrations/008_fts_rebuild.sql +0 -9
- package/migrations/009_session_embedding_chunks.sql +0 -46
- package/migrations/010_sources_opencode.sql +0 -30
- package/migrations/011_sources_hermes_agent.sql +0 -30
- package/migrations/012_sources_aider.sql +0 -30
- package/migrations/013_adapter_state_failure_count.sql +0 -12
- package/plugin-hermes-agent/README.md +0 -49
- package/plugin-hermes-agent/__init__.py +0 -75
- package/plugin-hermes-agent/plugin.yaml +0 -15
- package/scripts/backfill-citations.mjs +0 -0
- package/scripts/build-codex-plugin.mjs +0 -61
- package/scripts/deepseek-probe.mjs +0 -67
- package/scripts/extract-triples.mjs +0 -207
- package/scripts/longmemeval/embedding-cache.ts +0 -77
- package/scripts/longmemeval/fetch-dataset.sh +0 -25
- package/scripts/longmemeval/run-harness.ts +0 -315
- package/scripts/longmemeval/scorer.ts +0 -99
- package/scripts/longmemeval/tsconfig.json +0 -9
- package/scripts/longmemeval/types.ts +0 -35
- package/scripts/nlm-daily-digest.py +0 -239
- package/scripts/nlm-daily-digest.sh +0 -28
- package/src/cli/classify-parity.ts +0 -257
- package/src/cli/launchctl-helpers.ts +0 -49
- package/src/cli/nlm.ts +0 -885
- package/src/core/actions/actions-log.ts +0 -118
- package/src/core/actions/overlay.ts +0 -117
- package/src/core/adapters/aider.ts +0 -205
- package/src/core/adapters/claude-code.ts +0 -293
- package/src/core/adapters/common.ts +0 -54
- package/src/core/adapters/from-source.ts +0 -57
- package/src/core/adapters/hermes-agent.ts +0 -240
- package/src/core/adapters/hermes.ts +0 -277
- package/src/core/adapters/jsonl-generic.ts +0 -208
- package/src/core/adapters/opencode.ts +0 -281
- package/src/core/adapters/pi.ts +0 -264
- package/src/core/classifier/prompt.ts +0 -200
- package/src/core/dataset/build-dataset.ts +0 -463
- package/src/core/embedding/chunk-body.ts +0 -76
- package/src/core/embedding/embed-backfill.ts +0 -210
- package/src/core/embedding/embed-normalize.ts +0 -135
- package/src/core/facts/backfill-facts.ts +0 -254
- package/src/core/facts/extract-facts.ts +0 -50
- package/src/core/hook/citation-detect.ts +0 -124
- package/src/core/hook/cite-memo.ts +0 -68
- package/src/core/hook/claude-settings.ts +0 -166
- package/src/core/hook/gate.ts +0 -25
- package/src/core/hook/hook-log.ts +0 -41
- package/src/core/hook/memo-sweep.ts +0 -164
- package/src/core/hook/memo.ts +0 -67
- package/src/core/hook/pointer-block.ts +0 -26
- package/src/core/hook/select.ts +0 -32
- package/src/core/hook/transcript.ts +0 -121
- package/src/core/ingest/ingest-session.ts +0 -111
- package/src/core/providers/provider-models.ts +0 -100
- package/src/core/providers/provider-registry.ts +0 -196
- package/src/core/recall/citation-log.ts +0 -108
- package/src/core/recall/filter.ts +0 -27
- package/src/core/recall/index.ts +0 -6
- package/src/core/recall/match-fields.ts +0 -40
- package/src/core/recall/query-log.ts +0 -149
- package/src/core/recall/query-shape.ts +0 -66
- package/src/core/recall/recall-service.ts +0 -320
- package/src/core/recall/recent-log.ts +0 -59
- package/src/core/recall/tokenize.ts +0 -18
- package/src/core/recall/useful-scan.ts +0 -336
- package/src/core/recall-facts/fact-query-log.ts +0 -150
- package/src/core/recall-facts/fact-recall-service.ts +0 -327
- package/src/core/scheduler/scan-once.ts +0 -142
- package/src/core/scheduler/scheduler.ts +0 -225
- package/src/core/sources/source-registry.ts +0 -260
- package/src/core/storage/db-restore.ts +0 -133
- package/src/core/storage/live-status.ts +0 -45
- package/src/core/storage/migrate.ts +0 -72
- package/src/core/storage/sqlite-fact-store.ts +0 -304
- package/src/core/storage/sqlite-session-store.ts +0 -765
- package/src/hook/prompt-recall-hook.ts +0 -174
- package/src/hook/session-end-hook.ts +0 -81
- package/src/hook/session-start-hook.ts +0 -165
- package/src/hook/stop-hook.ts +0 -236
- package/src/http/app.ts +0 -1137
- package/src/install/claude-code.ts +0 -128
- package/src/install/codex.ts +0 -367
- package/src/install/hermes-agent.ts +0 -76
- package/src/install/hermes.ts +0 -78
- package/src/install/ollama.ts +0 -211
- package/src/install/setup.ts +0 -368
- package/src/llm/classifier-box.ts +0 -64
- package/src/llm/deepseek-client.ts +0 -150
- package/src/llm/env-autoload.ts +0 -55
- package/src/llm/ollama-client.ts +0 -189
- package/src/mcp/server.ts +0 -534
- package/src/ports/fact-store.ts +0 -102
- package/src/ports/llm-client.ts +0 -52
- package/src/ports/logger.ts +0 -16
- package/src/ports/session-store.ts +0 -45
- package/src/ports/transcript-adapter.ts +0 -55
- package/src/shared/types.ts +0 -145
- package/src/ui/App.tsx +0 -58
- package/src/ui/components/PromoteOpenButton.tsx +0 -65
- package/src/ui/components/SessionDrawer.tsx +0 -136
- package/src/ui/components/SideNav.tsx +0 -162
- package/src/ui/components/Skeleton.tsx +0 -107
- package/src/ui/index.html +0 -13
- package/src/ui/lib/actions.ts +0 -30
- package/src/ui/lib/api.ts +0 -92
- package/src/ui/lib/dataset.ts +0 -141
- package/src/ui/lib/registries.ts +0 -155
- package/src/ui/lib/view-settings.ts +0 -41
- package/src/ui/main.tsx +0 -15
- package/src/ui/pages/Live.tsx +0 -229
- package/src/ui/pages/Pulse.tsx +0 -415
- package/src/ui/pages/Recall.tsx +0 -190
- package/src/ui/pages/River.tsx +0 -308
- package/src/ui/pages/Search.tsx +0 -93
- package/src/ui/pages/Stub.tsx +0 -9
- package/src/ui/pages/Thread.tsx +0 -262
- package/src/ui/pages/settings/Classifier.tsx +0 -227
- package/src/ui/pages/settings/Data.tsx +0 -190
- package/src/ui/pages/settings/Index.tsx +0 -65
- package/src/ui/pages/settings/Labels.tsx +0 -224
- package/src/ui/pages/settings/Providers.tsx +0 -305
- package/src/ui/pages/settings/SettingsSubnav.tsx +0 -28
- package/src/ui/pages/settings/Sources.tsx +0 -326
- package/src/ui/pages/settings/Views.tsx +0 -96
- package/src/ui/styles.css +0 -1766
- package/src/ui/tsconfig.json +0 -21
- package/src/ui/vite.config.ts +0 -19
- package/tests/fixtures/claude_code/short_session.jsonl +0 -2
- package/tests/fixtures/claude_code/standard_iso.jsonl +0 -4
- package/tests/fixtures/claude_code/tool_heavy.jsonl +0 -8
- package/tests/fixtures/claude_code/with_subagent.jsonl +0 -7
- package/tests/fixtures/facts.ts +0 -17
- package/tests/fixtures/golden-corpus.ts +0 -85
- package/tests/fixtures/hermes/paired_request_dump.json +0 -24
- package/tests/fixtures/hermes/paired_session.json +0 -23
- package/tests/fixtures/hermes/request_dump.json +0 -28
- package/tests/fixtures/hermes/session_iso.json +0 -38
- package/tests/fixtures/hermes/session_unix.json +0 -38
- package/tests/fixtures/hermes/system_only.json +0 -18
- package/tests/fixtures/pi/error-connection-abort.jsonl +0 -8
- package/tests/fixtures/pi/short-successful.jsonl +0 -5
- package/tests/fixtures/pi/with-custom-message.jsonl +0 -6
- package/tests/fixtures/sessions.ts +0 -22
- package/tests/integration/backfill-facts.test.ts +0 -362
- package/tests/integration/citation-explicit.test.ts +0 -111
- package/tests/integration/cite-event.test.ts +0 -169
- package/tests/integration/cite-memo.test.ts +0 -87
- package/tests/integration/db-restore.test.ts +0 -153
- package/tests/integration/embed-backfill.test.ts +0 -176
- package/tests/integration/fact-supersedence.test.ts +0 -313
- package/tests/integration/fts-index.test.ts +0 -60
- package/tests/integration/getbyids-sqlite.test.ts +0 -60
- package/tests/integration/hermes-agent-hooks.test.ts +0 -248
- package/tests/integration/hook-claude-settings.test.ts +0 -205
- package/tests/integration/hook-log.test.ts +0 -54
- package/tests/integration/hook-memo.test.ts +0 -68
- package/tests/integration/hook-pre-compact.test.ts +0 -105
- package/tests/integration/hook-subagent-start.test.ts +0 -102
- package/tests/integration/http.test.ts +0 -401
- package/tests/integration/keyword-search-fts.test.ts +0 -66
- package/tests/integration/mcp-recall-logging.test.ts +0 -88
- package/tests/integration/mcp.test.ts +0 -248
- package/tests/integration/memo-sweep.test.ts +0 -91
- package/tests/integration/prompt-recall-hook.test.ts +0 -88
- package/tests/integration/provider-registry.test.ts +0 -107
- package/tests/integration/recall-golden.test.ts +0 -59
- package/tests/integration/recall-sqlite.test.ts +0 -169
- package/tests/integration/scheduler.test.ts +0 -391
- package/tests/integration/session-end-hook.test.ts +0 -48
- package/tests/integration/session-start-hook.test.ts +0 -126
- package/tests/integration/source-registry.test.ts +0 -120
- package/tests/integration/sqlite-fact-store.test.ts +0 -346
- package/tests/integration/stop-hook.test.ts +0 -560
- package/tests/integration/wal-checkpoint.test.ts +0 -49
- package/tests/unit/cli/launchctl-helpers.test.ts +0 -60
- package/tests/unit/core/adapters/aider.test.ts +0 -230
- package/tests/unit/core/adapters/claude-code.test.ts +0 -118
- package/tests/unit/core/adapters/hermes-agent.test.ts +0 -329
- package/tests/unit/core/adapters/hermes.test.ts +0 -81
- package/tests/unit/core/adapters/jsonl-generic.test.ts +0 -142
- package/tests/unit/core/adapters/opencode.test.ts +0 -354
- package/tests/unit/core/adapters/pi.test.ts +0 -110
- package/tests/unit/core/classifier/prompt.test.ts +0 -126
- package/tests/unit/core/embedding/chunk-body.test.ts +0 -100
- package/tests/unit/core/facts/extract-facts.test.ts +0 -117
- package/tests/unit/core/filter.test.ts +0 -40
- package/tests/unit/core/hook/citation-detect-cite-session.test.ts +0 -96
- package/tests/unit/core/hook/citation-detect.test.ts +0 -124
- package/tests/unit/core/hook/gate.test.ts +0 -29
- package/tests/unit/core/hook/pointer-block.test.ts +0 -22
- package/tests/unit/core/hook/select.test.ts +0 -66
- package/tests/unit/core/match-fields.test.ts +0 -39
- package/tests/unit/core/mcp-cite-session.test.ts +0 -51
- package/tests/unit/core/providers/provider-models.test.ts +0 -101
- package/tests/unit/core/query-shape.test.ts +0 -92
- package/tests/unit/core/recall-facts/fact-recall-service.test.ts +0 -258
- package/tests/unit/core/recall-service.test.ts +0 -200
- package/tests/unit/core/storage/live-status.test.ts +0 -54
- package/tests/unit/core/tokenize.test.ts +0 -32
- package/tests/unit/core/useful-scan.test.ts +0 -537
- package/tests/unit/llm/embed.test.ts +0 -93
- package/tests/unit/llm/ollama-client.test.ts +0 -124
- package/tests/unit/scripts/longmemeval-scorer.test.ts +0 -114
- package/tsconfig.json +0 -31
- package/tsconfig.test.json +0 -11
- package/vitest.config.ts +0 -22
package/src/cli/nlm.ts
DELETED
|
@@ -1,885 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* nlm — CLI entry point. Composition root for the whole stack.
|
|
4
|
-
*
|
|
5
|
-
* This is the one file that knows about every concrete implementation:
|
|
6
|
-
* SqliteSessionStore (storage), OllamaClient (LLM), Hono (HTTP),
|
|
7
|
-
* McpServer (MCP). Every other module depends on ports. Swapping a
|
|
8
|
-
* backend means editing this file, not anything inside core/.
|
|
9
|
-
*
|
|
10
|
-
* Subcommands:
|
|
11
|
-
* nlm start — boot HTTP server on $NLM_PORT (default 3940)
|
|
12
|
-
* nlm migrate — run pending migrations against the canonical SQLite
|
|
13
|
-
* nlm recall — one-shot recall query from the shell (debugging)
|
|
14
|
-
* nlm mcp — run as an MCP stdio server (for ~/.mcp.json wiring)
|
|
15
|
-
* nlm setup — interactive first-run wizard (recommended entry point)
|
|
16
|
-
* nlm install — install the macOS LaunchAgent (auto-start on login)
|
|
17
|
-
* nlm uninstall — remove the macOS LaunchAgent
|
|
18
|
-
* nlm hook install — add the recall hook to Claude Code (shadow mode)
|
|
19
|
-
* nlm hook uninstall — remove the recall hook from Claude Code
|
|
20
|
-
* nlm connect claude-code — write MCP server block to ~/.mcp.json
|
|
21
|
-
* nlm connect codex — install Codex marketplace plugin
|
|
22
|
-
* nlm disconnect claude-code — remove MCP block from ~/.mcp.json
|
|
23
|
-
* nlm disconnect codex — remove Codex plugin
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { fileURLToPath } from "node:url";
|
|
27
|
-
import { dirname, resolve, join } from "node:path";
|
|
28
|
-
import { homedir } from "node:os";
|
|
29
|
-
import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs";
|
|
30
|
-
import { execFileSync } from "node:child_process";
|
|
31
|
-
import { Command } from "commander";
|
|
32
|
-
import { serve } from "@hono/node-server";
|
|
33
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
34
|
-
import { FactRecallService } from "../core/recall-facts/fact-recall-service.js";
|
|
35
|
-
import { RecallService } from "../core/recall/recall-service.js";
|
|
36
|
-
import { SqliteFactStore } from "../core/storage/sqlite-fact-store.js";
|
|
37
|
-
import { ProviderRegistry } from "../core/providers/provider-registry.js";
|
|
38
|
-
import { SourceRegistry } from "../core/sources/source-registry.js";
|
|
39
|
-
import { SqliteSessionStore } from "../core/storage/sqlite-session-store.js";
|
|
40
|
-
import { applyPendingRestore } from "../core/storage/db-restore.js";
|
|
41
|
-
import { createApp } from "../http/app.js";
|
|
42
|
-
import { createMcpServer } from "../mcp/server.js";
|
|
43
|
-
import { ClassifierBox, type ClassifierProvider } from "../llm/classifier-box.js";
|
|
44
|
-
import { DeepSeekClient } from "../llm/deepseek-client.js";
|
|
45
|
-
import { OllamaClient } from "../llm/ollama-client.js";
|
|
46
|
-
import { autoloadEnv } from "../llm/env-autoload.js";
|
|
47
|
-
import { addHook, buildHookCommand, removeHook, smokeTestHookCommand } from "../core/hook/claude-settings.js";
|
|
48
|
-
import {
|
|
49
|
-
codexBinaryAvailable,
|
|
50
|
-
connectCodex,
|
|
51
|
-
disconnectCodex,
|
|
52
|
-
pluginScriptsDir,
|
|
53
|
-
} from "../install/codex.js";
|
|
54
|
-
import { connectClaudeCode, disconnectClaudeCode, installClaudeCodeHooks } from "../install/claude-code.js";
|
|
55
|
-
import { connectHermes, disconnectHermes, hermesConfigPath } from "../install/hermes.js";
|
|
56
|
-
import { connectHermesAgent, disconnectHermesAgent, hermesAgentPluginDir } from "../install/hermes-agent.js";
|
|
57
|
-
import { runSetup } from "../install/setup.js";
|
|
58
|
-
import { runParity } from "./classify-parity.js";
|
|
59
|
-
import { reembedCorpus } from "../core/embedding/embed-backfill.js";
|
|
60
|
-
import { backfillFacts } from "../core/facts/backfill-facts.js";
|
|
61
|
-
import { normalizeEmbeddings } from "../core/embedding/embed-normalize.js";
|
|
62
|
-
import { ScanScheduler } from "../core/scheduler/scheduler.js";
|
|
63
|
-
import { MemoSweepScheduler } from "../core/hook/memo-sweep.js";
|
|
64
|
-
import { isAgentLoaded, isBenignBootoutError } from "./launchctl-helpers.js";
|
|
65
|
-
import { adapterFromSource } from "../core/adapters/from-source.js";
|
|
66
|
-
import type { TranscriptAdapter } from "../ports/transcript-adapter.js";
|
|
67
|
-
import { scanUsefulHits } from "../core/recall/useful-scan.js";
|
|
68
|
-
|
|
69
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
70
|
-
const __dirname = dirname(__filename);
|
|
71
|
-
const MIGRATIONS_DIR = resolve(__dirname, "../../migrations");
|
|
72
|
-
const UI_DIST = resolve(__dirname, "../../dist/ui");
|
|
73
|
-
const DEFAULT_DB_PATH = resolve(homedir(), ".nlm/canonical.sqlite");
|
|
74
|
-
const DEFAULT_PORT = 3940;
|
|
75
|
-
|
|
76
|
-
function dbPath(): string {
|
|
77
|
-
return process.env["NLM_DB_PATH"] ?? DEFAULT_DB_PATH;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function port(): number {
|
|
81
|
-
const raw = process.env["NLM_PORT"];
|
|
82
|
-
if (!raw) return DEFAULT_PORT;
|
|
83
|
-
const n = Number.parseInt(raw, 10);
|
|
84
|
-
if (!Number.isFinite(n) || n < 1 || n > 65_535) return DEFAULT_PORT;
|
|
85
|
-
return n;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function ollamaUrl(): string {
|
|
89
|
-
return process.env["NLM_OLLAMA_URL"] ?? "http://localhost:11434";
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function buildClassifier(): ClassifierBox {
|
|
93
|
-
// DeepSeek V4 Flash is the default for the ingest classifier per the
|
|
94
|
-
// 2026-05-19 parity run: ~5s/session, 90% first-try success vs Ollama
|
|
95
|
-
// phi4-mini's 0% on the same first three sessions. Override with
|
|
96
|
-
// NLM_CLASSIFIER=ollama if you need offline-only operation.
|
|
97
|
-
const provider = ((process.env["NLM_CLASSIFIER"] ?? "deepseek").toLowerCase() as ClassifierProvider);
|
|
98
|
-
if (provider !== "ollama") autoloadEnv();
|
|
99
|
-
const model = process.env["NLM_CLASSIFIER_MODEL"]
|
|
100
|
-
?? (provider === "ollama" ? "phi4-mini:latest" : "deepseek-v4-flash");
|
|
101
|
-
return new ClassifierBox({ provider, model, ollamaUrl: ollamaUrl() });
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function buildAdapters(sources: SourceRegistry): TranscriptAdapter[] {
|
|
105
|
-
// Sources table is the source of truth. Each enabled row maps to one
|
|
106
|
-
// adapter via adapterFromSource(). Detection still gates registration —
|
|
107
|
-
// a row pointing at a missing dir won't poll. NLM_ADAPTERS keeps working
|
|
108
|
-
// as a name-based filter for forcing a subset during dev.
|
|
109
|
-
const explicit = process.env["NLM_ADAPTERS"];
|
|
110
|
-
const allowed = explicit ? new Set(explicit.split(",").map((s) => s.trim())) : null;
|
|
111
|
-
const out: TranscriptAdapter[] = [];
|
|
112
|
-
for (const row of sources.list()) {
|
|
113
|
-
if (!row.enabled) continue;
|
|
114
|
-
const adapter = adapterFromSource(row);
|
|
115
|
-
if (!adapter) continue;
|
|
116
|
-
if (allowed && !allowed.has(adapter.name)) continue;
|
|
117
|
-
if (!adapter.detect().enabled) continue;
|
|
118
|
-
out.push(adapter);
|
|
119
|
-
}
|
|
120
|
-
return out;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function buildStack() {
|
|
124
|
-
// Load .env before any registry seeds so secrets carried in env vars
|
|
125
|
-
// (DEEPSEEK_API_KEY today; OPENAI_API_KEY etc. tomorrow) bridge into
|
|
126
|
-
// the providers table on first boot under launchd.
|
|
127
|
-
autoloadEnv();
|
|
128
|
-
// A restore staged via POST /api/data/restore is promoted here, before
|
|
129
|
-
// the store opens — the daemon can't swap a DB file it already holds.
|
|
130
|
-
const restored = applyPendingRestore(dbPath());
|
|
131
|
-
if (restored.applied) {
|
|
132
|
-
console.error(`nlm-memory: restored database from staged backup`);
|
|
133
|
-
if (restored.archivedTo) console.error(` previous db archived at ${restored.archivedTo}`);
|
|
134
|
-
}
|
|
135
|
-
const store = new SqliteSessionStore({
|
|
136
|
-
dbPath: dbPath(),
|
|
137
|
-
migrationsDir: MIGRATIONS_DIR,
|
|
138
|
-
});
|
|
139
|
-
// FactStore shares the SessionStore's connection so session+facts ingest
|
|
140
|
-
// can commit in one transaction. Phase B.1 wires it in; no callers yet.
|
|
141
|
-
const facts = new SqliteFactStore(store.rawDb());
|
|
142
|
-
const sources = new SourceRegistry(store.rawDb());
|
|
143
|
-
sources.seedDefaults();
|
|
144
|
-
const providers = new ProviderRegistry(store.rawDb());
|
|
145
|
-
providers.seedDefaults();
|
|
146
|
-
// Recall only uses embed(). Embeddings live on Ollama; DeepSeek doesn't
|
|
147
|
-
// expose them. Classifier is wired separately for Phase D ingest.
|
|
148
|
-
const embedder = new OllamaClient({ baseUrl: ollamaUrl() });
|
|
149
|
-
const classifier = buildClassifier();
|
|
150
|
-
const recall = new RecallService({ store, llm: embedder });
|
|
151
|
-
const factRecall = new FactRecallService({ factStore: facts, llm: embedder });
|
|
152
|
-
return { store, facts, sources, providers, recall, factRecall, embedder, classifier };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const program = new Command();
|
|
156
|
-
program
|
|
157
|
-
.name("nlm")
|
|
158
|
-
.description("Local-first memory operating system for AI operators")
|
|
159
|
-
.version("0.3.0");
|
|
160
|
-
|
|
161
|
-
program
|
|
162
|
-
.command("start")
|
|
163
|
-
.description("Boot the HTTP server + ingest scheduler")
|
|
164
|
-
.option("--no-scheduler", "HTTP only; skip the ingest tick loop")
|
|
165
|
-
.option("--interval-min <n>", "scheduler tick interval (min, default 30)", (v) => Number.parseInt(v, 10), 30)
|
|
166
|
-
.action(async (opts) => {
|
|
167
|
-
const { store, facts, sources, providers, recall, factRecall, embedder, classifier } = buildStack();
|
|
168
|
-
const { existsSync } = await import("node:fs");
|
|
169
|
-
const hasMcpToken = Boolean(process.env["NLM_MCP_TOKEN"]);
|
|
170
|
-
const app = createApp({
|
|
171
|
-
recall,
|
|
172
|
-
store,
|
|
173
|
-
liveStore: store,
|
|
174
|
-
factRecall,
|
|
175
|
-
factStore: facts,
|
|
176
|
-
dbPath: dbPath(),
|
|
177
|
-
classifier,
|
|
178
|
-
sources,
|
|
179
|
-
providers,
|
|
180
|
-
ingest: { classifier, embedder, store, factStore: facts },
|
|
181
|
-
embedderInfo: { provider: "ollama", model: "nomic-embed-text", dims: 768 },
|
|
182
|
-
...(existsSync(UI_DIST) ? { uiDist: UI_DIST } : {}),
|
|
183
|
-
// Wire POST /mcp only when NLM_MCP_TOKEN is present. Absent = route never
|
|
184
|
-
// mounts, zero attack surface. Present = token-gated Streamable-HTTP MCP
|
|
185
|
-
// endpoint for container agents (e.g. Hermes WebUI).
|
|
186
|
-
...(hasMcpToken
|
|
187
|
-
? { mcpDeps: { recall, store, factRecall, factStore: facts } }
|
|
188
|
-
: {}),
|
|
189
|
-
});
|
|
190
|
-
const p = port();
|
|
191
|
-
serve({ fetch: app.fetch, port: p, hostname: "127.0.0.1" }, (info) => {
|
|
192
|
-
console.error(`nlm-memory http listening on http://localhost:${info.port}`);
|
|
193
|
-
if (hasMcpToken) {
|
|
194
|
-
console.error(` mcp: http://localhost:${info.port}/mcp (token-gated)`);
|
|
195
|
-
}
|
|
196
|
-
console.error(` db: ${dbPath()}`);
|
|
197
|
-
console.error(` ollama: ${ollamaUrl()}`);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// Keep the SQLite WAL bounded. WAL mode is on but nothing else
|
|
201
|
-
// checkpoints it; under continuous readers it grows without limit
|
|
202
|
-
// (it had reached 38 MB), which slows every read. Drain once at boot,
|
|
203
|
-
// then every 5 minutes.
|
|
204
|
-
const WAL_CHECKPOINT_INTERVAL_MS = 5 * 60_000;
|
|
205
|
-
try {
|
|
206
|
-
store.checkpoint();
|
|
207
|
-
} catch {
|
|
208
|
-
// Boot checkpoint can lose a race with readers — the interval retries.
|
|
209
|
-
}
|
|
210
|
-
const checkpointTimer = setInterval(() => {
|
|
211
|
-
try {
|
|
212
|
-
store.checkpoint();
|
|
213
|
-
} catch {
|
|
214
|
-
// Checkpoint contention — the next tick retries.
|
|
215
|
-
}
|
|
216
|
-
}, WAL_CHECKPOINT_INTERVAL_MS);
|
|
217
|
-
checkpointTimer.unref();
|
|
218
|
-
|
|
219
|
-
// Memo sweep runs independently of the transcript scheduler — it's the
|
|
220
|
-
// backstop for SessionEnd hook unreliability (crashes, kill -9, IDE
|
|
221
|
-
// force-close don't fire SessionEnd, so memo files would otherwise
|
|
222
|
-
// accumulate forever). Always on, even when --no-scheduler.
|
|
223
|
-
const memoSweep = new MemoSweepScheduler();
|
|
224
|
-
memoSweep.start();
|
|
225
|
-
console.error(" memo sweep: dormant cleanup every 5m (threshold 24h)");
|
|
226
|
-
|
|
227
|
-
if (opts.scheduler !== false) {
|
|
228
|
-
const adapters = buildAdapters(sources);
|
|
229
|
-
if (adapters.length === 0) {
|
|
230
|
-
console.error(" scheduler: no adapters detected (set NLM_ADAPTERS to force-enable)");
|
|
231
|
-
} else {
|
|
232
|
-
const scheduler = new ScanScheduler({
|
|
233
|
-
store,
|
|
234
|
-
adapters,
|
|
235
|
-
classifier,
|
|
236
|
-
embedder,
|
|
237
|
-
factStore: facts,
|
|
238
|
-
intervalMs: opts.intervalMin * 60_000,
|
|
239
|
-
});
|
|
240
|
-
scheduler.start();
|
|
241
|
-
console.error(
|
|
242
|
-
` scheduler: ${adapters.map((a) => a.name).join(", ")} every ${opts.intervalMin}m`,
|
|
243
|
-
);
|
|
244
|
-
const shutdown = () => {
|
|
245
|
-
clearInterval(checkpointTimer);
|
|
246
|
-
scheduler.stop();
|
|
247
|
-
memoSweep.stop();
|
|
248
|
-
store.close();
|
|
249
|
-
process.exit(0);
|
|
250
|
-
};
|
|
251
|
-
process.on("SIGINT", shutdown);
|
|
252
|
-
process.on("SIGTERM", shutdown);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
program
|
|
258
|
-
.command("migrate")
|
|
259
|
-
.description("Run pending migrations against the canonical SQLite")
|
|
260
|
-
.action(() => {
|
|
261
|
-
// SqliteSessionStore's constructor loads sqlite-vec and runs migrations.
|
|
262
|
-
// Opening + closing is the whole operation.
|
|
263
|
-
const store = new SqliteSessionStore({
|
|
264
|
-
dbPath: dbPath(),
|
|
265
|
-
migrationsDir: MIGRATIONS_DIR,
|
|
266
|
-
});
|
|
267
|
-
store.close();
|
|
268
|
-
console.error(`nlm-memory: migrations applied at ${dbPath()}`);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
program
|
|
272
|
-
.command("recall")
|
|
273
|
-
.description("One-shot recall query (for shell debugging)")
|
|
274
|
-
.argument("<query>", "search query")
|
|
275
|
-
.option("-e, --entity <name>", "filter by entity")
|
|
276
|
-
.option("-k, --kind <kind>", "filter by marker kind (decision|open)")
|
|
277
|
-
.option("-m, --mode <mode>", "keyword|semantic|hybrid", "keyword")
|
|
278
|
-
.option("-l, --limit <n>", "max results", (v) => Number.parseInt(v, 10), 10)
|
|
279
|
-
.action(async (query, opts) => {
|
|
280
|
-
const { store, recall } = buildStack();
|
|
281
|
-
try {
|
|
282
|
-
const result = await recall.search({
|
|
283
|
-
query,
|
|
284
|
-
mode: opts.mode,
|
|
285
|
-
limit: opts.limit,
|
|
286
|
-
...(opts.entity ? { entity: opts.entity } : {}),
|
|
287
|
-
...(opts.kind ? { kind: opts.kind } : {}),
|
|
288
|
-
});
|
|
289
|
-
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
290
|
-
} finally {
|
|
291
|
-
store.close();
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
program
|
|
296
|
-
.command("classify-parity")
|
|
297
|
-
.description("Run TS classifier against ~/.nlm/canonical.sqlite and diff vs persisted Python output")
|
|
298
|
-
.option("-l, --limit <n>", "sessions to sample", (v) => Number.parseInt(v, 10), 10)
|
|
299
|
-
.option("-p, --provider <name>", "deepseek | ollama", "deepseek")
|
|
300
|
-
.option("-m, --model <name>", "model tag (default: deepseek-v4-flash for deepseek, phi4-mini:latest for ollama)")
|
|
301
|
-
.option("-v, --verbose", "per-session diff lines on stderr")
|
|
302
|
-
.action(async (opts) => {
|
|
303
|
-
const provider = opts.provider === "ollama" ? "ollama" : "deepseek";
|
|
304
|
-
const defaultModel = provider === "deepseek" ? "deepseek-v4-flash" : "phi4-mini:latest";
|
|
305
|
-
const report = await runParity({
|
|
306
|
-
limit: opts.limit,
|
|
307
|
-
dbPath: dbPath(),
|
|
308
|
-
ollamaUrl: ollamaUrl(),
|
|
309
|
-
classifyModel: opts.model ?? defaultModel,
|
|
310
|
-
provider,
|
|
311
|
-
verbose: Boolean(opts.verbose),
|
|
312
|
-
});
|
|
313
|
-
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
program
|
|
317
|
-
.command("embed-backfill")
|
|
318
|
-
.description("Re-embed every session into session_embedding_chunks (chunk + max-pool)")
|
|
319
|
-
.option("-l, --limit <n>", "session cap (default: all)", (v) => Number.parseInt(v, 10))
|
|
320
|
-
.option("--state <path>", "resume state file (default ~/.nlm/embed_reembed.state)")
|
|
321
|
-
.option("-v, --verbose", "per-session progress on stderr")
|
|
322
|
-
.action(async (opts) => {
|
|
323
|
-
const embedder = new OllamaClient({ baseUrl: ollamaUrl() });
|
|
324
|
-
const report = await reembedCorpus({
|
|
325
|
-
dbPath: dbPath(),
|
|
326
|
-
embedder,
|
|
327
|
-
...(opts.state ? { statePath: opts.state } : {}),
|
|
328
|
-
...(opts.limit ? { limit: opts.limit } : {}),
|
|
329
|
-
...(opts.verbose
|
|
330
|
-
? {
|
|
331
|
-
onProgress: (i: number, n: number, sid: string, status: string) => {
|
|
332
|
-
process.stderr.write(` [${i}/${n}] ${sid} ${status}\n`);
|
|
333
|
-
},
|
|
334
|
-
}
|
|
335
|
-
: {}),
|
|
336
|
-
});
|
|
337
|
-
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
program
|
|
341
|
-
.command("backfill-facts")
|
|
342
|
-
.description("One-shot: classify historical sessions and populate the FactStore (Phase B.5)")
|
|
343
|
-
.option("-l, --limit <n>", "max sessions to process this run", (v) => Number.parseInt(v, 10))
|
|
344
|
-
.option("--from <session-id>", "skip sessions with id <= this value (operator-resume)")
|
|
345
|
-
.option("--state <path>", "resume state file (default ~/.nlm/backfill_facts.state)")
|
|
346
|
-
.option("--dry-run", "count what would happen without writing facts")
|
|
347
|
-
.option("--reprocess", "re-classify sessions that already have facts")
|
|
348
|
-
.option("--no-embed", "skip per-fact embedding (faster but disables semantic recall)")
|
|
349
|
-
.option("-v, --verbose", "per-session progress on stderr")
|
|
350
|
-
.action(async (opts) => {
|
|
351
|
-
const { store, facts, embedder, classifier } = buildStack();
|
|
352
|
-
try {
|
|
353
|
-
const report = await backfillFacts({
|
|
354
|
-
store,
|
|
355
|
-
factStore: facts,
|
|
356
|
-
classifier,
|
|
357
|
-
embedder: opts.embed === false ? null : embedder,
|
|
358
|
-
...(opts.state ? { statePath: opts.state } : {}),
|
|
359
|
-
...(opts.limit ? { limit: opts.limit } : {}),
|
|
360
|
-
...(opts.from ? { from: opts.from } : {}),
|
|
361
|
-
dryRun: Boolean(opts.dryRun),
|
|
362
|
-
reprocess: Boolean(opts.reprocess),
|
|
363
|
-
...(opts.verbose
|
|
364
|
-
? {
|
|
365
|
-
onProgress: (i, n, sid, status, detail) => {
|
|
366
|
-
const tail = detail ? ` ${detail}` : "";
|
|
367
|
-
process.stderr.write(` [${i}/${n}] ${sid} ${status}${tail}\n`);
|
|
368
|
-
},
|
|
369
|
-
}
|
|
370
|
-
: {}),
|
|
371
|
-
});
|
|
372
|
-
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
373
|
-
} finally {
|
|
374
|
-
store.close();
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
program
|
|
379
|
-
.command("embed-normalize")
|
|
380
|
-
.description("L2-normalize every row in session_embeddings (idempotent)")
|
|
381
|
-
.option("--dim <n>", "vector dimension (default 768)", (v) => Number.parseInt(v, 10), 768)
|
|
382
|
-
.option("--batch <n>", "rows per commit batch (default 100)", (v) => Number.parseInt(v, 10), 100)
|
|
383
|
-
.option("--dry-run", "report what would change without writing")
|
|
384
|
-
.action((opts) => {
|
|
385
|
-
const report = normalizeEmbeddings({
|
|
386
|
-
dbPath: dbPath(),
|
|
387
|
-
dim: opts.dim,
|
|
388
|
-
batchSize: opts.batch,
|
|
389
|
-
dryRun: Boolean(opts.dryRun),
|
|
390
|
-
});
|
|
391
|
-
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
program
|
|
395
|
-
.command("mcp")
|
|
396
|
-
.description("Run as an MCP stdio server (for ~/.mcp.json)")
|
|
397
|
-
.action(async () => {
|
|
398
|
-
const { recall, store, facts, factRecall } = buildStack();
|
|
399
|
-
const server = createMcpServer({ recall, store, factStore: facts, factRecall });
|
|
400
|
-
const transport = new StdioServerTransport();
|
|
401
|
-
await server.connect(transport);
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
const LAUNCH_AGENT_LABEL = "com.github.pbmagnet4.nlm-memory";
|
|
405
|
-
const LAUNCH_AGENT_PLIST = join(
|
|
406
|
-
homedir(), "Library", "LaunchAgents", `${LAUNCH_AGENT_LABEL}.plist`,
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
function buildPlist(nodeExec: string, nlmJs: string): string {
|
|
410
|
-
const logDir = join(homedir(), ".nlm", "logs");
|
|
411
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
412
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
413
|
-
<plist version="1.0">
|
|
414
|
-
<dict>
|
|
415
|
-
<key>Label</key>
|
|
416
|
-
<string>${LAUNCH_AGENT_LABEL}</string>
|
|
417
|
-
<key>ProgramArguments</key>
|
|
418
|
-
<array>
|
|
419
|
-
<string>${nodeExec}</string>
|
|
420
|
-
<string>${nlmJs}</string>
|
|
421
|
-
<string>start</string>
|
|
422
|
-
</array>
|
|
423
|
-
<key>WorkingDirectory</key>
|
|
424
|
-
<string>${homedir()}</string>
|
|
425
|
-
<key>RunAtLoad</key>
|
|
426
|
-
<true/>
|
|
427
|
-
<key>KeepAlive</key>
|
|
428
|
-
<dict>
|
|
429
|
-
<key>Crashed</key>
|
|
430
|
-
<true/>
|
|
431
|
-
<key>SuccessfulExit</key>
|
|
432
|
-
<false/>
|
|
433
|
-
</dict>
|
|
434
|
-
<key>ThrottleInterval</key>
|
|
435
|
-
<integer>10</integer>
|
|
436
|
-
<key>StandardOutPath</key>
|
|
437
|
-
<string>${logDir}/daemon-out.log</string>
|
|
438
|
-
<key>StandardErrorPath</key>
|
|
439
|
-
<string>${logDir}/daemon-err.log</string>
|
|
440
|
-
</dict>
|
|
441
|
-
</plist>
|
|
442
|
-
`;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
program
|
|
446
|
-
.command("install")
|
|
447
|
-
.description("Install the macOS LaunchAgent so nlm-memory auto-starts on login")
|
|
448
|
-
.action(() => {
|
|
449
|
-
if (process.platform !== "darwin") {
|
|
450
|
-
console.error("nlm install: only macOS is supported. On Linux, add `nlm start` to your init system manually.");
|
|
451
|
-
process.exit(1);
|
|
452
|
-
}
|
|
453
|
-
const uid = process.getuid?.();
|
|
454
|
-
if (uid === undefined) {
|
|
455
|
-
console.error("nlm install: could not determine UID");
|
|
456
|
-
process.exit(1);
|
|
457
|
-
}
|
|
458
|
-
mkdirSync(join(homedir(), ".nlm", "logs"), { recursive: true });
|
|
459
|
-
writeFileSync(LAUNCH_AGENT_PLIST, buildPlist(process.execPath, __filename), "utf8");
|
|
460
|
-
console.error(`nlm: wrote ${LAUNCH_AGENT_PLIST}`);
|
|
461
|
-
try {
|
|
462
|
-
execFileSync("launchctl", ["bootout", `gui/${uid}`, LAUNCH_AGENT_LABEL], { stdio: "ignore" });
|
|
463
|
-
} catch {
|
|
464
|
-
// not loaded yet — expected on first install
|
|
465
|
-
}
|
|
466
|
-
execFileSync("launchctl", ["bootstrap", `gui/${uid}`, LAUNCH_AGENT_PLIST]);
|
|
467
|
-
console.error("nlm: daemon installed and started.");
|
|
468
|
-
console.error(` UI: http://localhost:${port()}/ui`);
|
|
469
|
-
console.error(` To stop: launchctl stop ${LAUNCH_AGENT_LABEL}`);
|
|
470
|
-
console.error(" To remove: nlm uninstall");
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
program
|
|
474
|
-
.command("uninstall")
|
|
475
|
-
.description("Remove the macOS LaunchAgent")
|
|
476
|
-
.action(() => {
|
|
477
|
-
if (process.platform !== "darwin") {
|
|
478
|
-
console.error("nlm uninstall: only macOS is supported.");
|
|
479
|
-
process.exit(1);
|
|
480
|
-
}
|
|
481
|
-
const uid = process.getuid?.();
|
|
482
|
-
if (uid === undefined) {
|
|
483
|
-
console.error("nlm uninstall: could not determine UID");
|
|
484
|
-
process.exit(1);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
let bootoutFailed = false;
|
|
488
|
-
let bootoutStderr = "";
|
|
489
|
-
try {
|
|
490
|
-
execFileSync("launchctl", ["bootout", `gui/${uid}`, LAUNCH_AGENT_LABEL], { stdio: "pipe" });
|
|
491
|
-
console.error("nlm: daemon stopped.");
|
|
492
|
-
} catch (e) {
|
|
493
|
-
const err = e as { stderr?: Buffer | string };
|
|
494
|
-
bootoutStderr = err.stderr ? err.stderr.toString() : "";
|
|
495
|
-
if (isBenignBootoutError(bootoutStderr)) {
|
|
496
|
-
// Agent wasn't loaded — fine, proceed to plist cleanup.
|
|
497
|
-
} else {
|
|
498
|
-
bootoutFailed = true;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Source of truth: did launchd actually unload the agent? Same shape
|
|
503
|
-
// of bug as #161 — silent partial success is worse than loud failure.
|
|
504
|
-
if (isAgentLoaded(LAUNCH_AGENT_LABEL)) {
|
|
505
|
-
console.error("nlm: uninstall FAILED — agent is still loaded after bootout.");
|
|
506
|
-
if (bootoutStderr.trim()) {
|
|
507
|
-
console.error(` launchctl stderr: ${bootoutStderr.trim()}`);
|
|
508
|
-
}
|
|
509
|
-
console.error(" Recovery options:");
|
|
510
|
-
console.error(` 1. launchctl bootout gui/${uid}/${LAUNCH_AGENT_LABEL}`);
|
|
511
|
-
console.error(" 2. If a stale process is holding the port, find it:");
|
|
512
|
-
console.error(" ps aux | grep 'nlm.js start' | grep -v grep");
|
|
513
|
-
console.error(" Then: kill <pid> (or kill -9 <pid> if it ignores TERM)");
|
|
514
|
-
console.error(" Plist NOT removed — re-run `nlm uninstall` after the agent is gone.");
|
|
515
|
-
process.exit(1);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (bootoutFailed) {
|
|
519
|
-
// launchctl errored AND the agent isn't loaded — odd but recoverable.
|
|
520
|
-
// Flag it so the user knows something off-script happened.
|
|
521
|
-
console.error(`nlm: bootout reported an error but agent is unloaded: ${bootoutStderr.trim()}`);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
if (existsSync(LAUNCH_AGENT_PLIST)) {
|
|
525
|
-
rmSync(LAUNCH_AGENT_PLIST);
|
|
526
|
-
console.error(`nlm: removed ${LAUNCH_AGENT_PLIST}`);
|
|
527
|
-
}
|
|
528
|
-
console.error("nlm: uninstalled. Run `nlm install` to reinstall.");
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
const HOOK_JS = resolve(__dirname, "../hook/prompt-recall-hook.js");
|
|
532
|
-
const SESSION_START_HOOK_JS = resolve(__dirname, "../hook/session-start-hook.js");
|
|
533
|
-
const SESSION_END_HOOK_JS = resolve(__dirname, "../hook/session-end-hook.js");
|
|
534
|
-
const STOP_HOOK_JS = resolve(__dirname, "../hook/stop-hook.js");
|
|
535
|
-
const PRE_COMPACT_HOOK_JS = resolve(__dirname, "../hook/pre-compact-hook.js");
|
|
536
|
-
const SUBAGENT_START_HOOK_JS = resolve(__dirname, "../hook/subagent-start-hook.js");
|
|
537
|
-
|
|
538
|
-
interface HookSpec {
|
|
539
|
-
readonly event: "UserPromptSubmit" | "SessionStart" | "SessionEnd" | "Stop" | "PreCompact" | "SubagentStart";
|
|
540
|
-
readonly script: string;
|
|
541
|
-
readonly label: string;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
const ALL_HOOKS: ReadonlyArray<HookSpec> = [
|
|
545
|
-
{ event: "UserPromptSubmit", script: HOOK_JS, label: "recall" },
|
|
546
|
-
{ event: "SessionStart", script: SESSION_START_HOOK_JS, label: "session-start" },
|
|
547
|
-
{ event: "SessionEnd", script: SESSION_END_HOOK_JS, label: "session-end" },
|
|
548
|
-
{ event: "Stop", script: STOP_HOOK_JS, label: "stop" },
|
|
549
|
-
{ event: "PreCompact", script: PRE_COMPACT_HOOK_JS, label: "pre-compact" },
|
|
550
|
-
{ event: "SubagentStart", script: SUBAGENT_START_HOOK_JS, label: "subagent-start" },
|
|
551
|
-
];
|
|
552
|
-
|
|
553
|
-
function claudeSettingsPath(): string {
|
|
554
|
-
return process.env["NLM_CLAUDE_SETTINGS"] ?? join(homedir(), ".claude", "settings.json");
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
const hook = program
|
|
558
|
-
.command("hook")
|
|
559
|
-
.description("Manage the Claude Code NLM hooks");
|
|
560
|
-
|
|
561
|
-
hook
|
|
562
|
-
.command("install")
|
|
563
|
-
.description("Add the NLM hooks (recall + session-end + stop) to ~/.claude/settings.json (shadow mode)")
|
|
564
|
-
.action(() => {
|
|
565
|
-
const path = claudeSettingsPath();
|
|
566
|
-
const hookLogPath = process.env["NLM_HOOK_LOG"] ?? join(homedir(), ".nlm", "hook-log.jsonl");
|
|
567
|
-
|
|
568
|
-
// Install + smoke each hook. If any fails, revert all and exit nonzero.
|
|
569
|
-
// Atomic install matters because partial state ("recall installed but
|
|
570
|
-
// session-end didn't smoke-test") is worse than no install — silent
|
|
571
|
-
// partial failure is the bug class we shipped #161 to prevent.
|
|
572
|
-
const installed: HookSpec[] = [];
|
|
573
|
-
for (const spec of ALL_HOOKS) {
|
|
574
|
-
const command = buildHookCommand(process.execPath, spec.script, "shadow");
|
|
575
|
-
addHook(path, command, spec.event);
|
|
576
|
-
const smoke = smokeTestHookCommand(command, hookLogPath);
|
|
577
|
-
if (!smoke.ok) {
|
|
578
|
-
// Revert every hook we installed this run (including the failing one).
|
|
579
|
-
for (const prior of [...installed, spec]) {
|
|
580
|
-
removeHook(path, prior.event);
|
|
581
|
-
}
|
|
582
|
-
console.error(`nlm: ${spec.label} hook (${spec.event}) FAILED smoke test — all NLM hooks reverted.`);
|
|
583
|
-
console.error(` reason: ${smoke.reason}`);
|
|
584
|
-
if (smoke.stderr) {
|
|
585
|
-
const trimmed = smoke.stderr.trim();
|
|
586
|
-
if (trimmed) console.error(` stderr: ${trimmed}`);
|
|
587
|
-
}
|
|
588
|
-
console.error(` command was: ${command}`);
|
|
589
|
-
process.exit(1);
|
|
590
|
-
}
|
|
591
|
-
installed.push(spec);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
console.error(`nlm: NLM hooks installed in ${path} (shadow mode):`);
|
|
595
|
-
for (const spec of installed) {
|
|
596
|
-
console.error(` - ${spec.event} → ${spec.label}-hook`);
|
|
597
|
-
}
|
|
598
|
-
console.error(" Smoke tests passed — all hooks appended synthetic entries to hook-log.jsonl.");
|
|
599
|
-
console.error(" Recall hooks log to ~/.nlm/hook-log.jsonl and inject nothing in shadow mode.");
|
|
600
|
-
console.error(" Session-end hook cleans up ~/.nlm/hook-state/<session>.json on session close.");
|
|
601
|
-
console.error(" To go live later: change NLM_HOOK_MODE=shadow to live for the recall hook.");
|
|
602
|
-
console.error(" To remove: nlm hook uninstall");
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
hook
|
|
606
|
-
.command("uninstall")
|
|
607
|
-
.description("Remove all NLM hooks from ~/.claude/settings.json")
|
|
608
|
-
.action(() => {
|
|
609
|
-
const path = claudeSettingsPath();
|
|
610
|
-
removeHook(path, "*");
|
|
611
|
-
console.error(`nlm: all NLM hooks removed from ${path}.`);
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
// Repo root resolves to <pkg>/dist/cli/nlm.js → <pkg>/. The plugin tree is
|
|
615
|
-
// shipped alongside dist/ so plugin/scripts/ is reachable from both local
|
|
616
|
-
// dev and the globally-installed package.
|
|
617
|
-
const REPO_ROOT = resolve(__dirname, "../..");
|
|
618
|
-
|
|
619
|
-
const connect = program
|
|
620
|
-
.command("connect")
|
|
621
|
-
.description("Connect nlm-memory to an AI coding runtime");
|
|
622
|
-
|
|
623
|
-
connect
|
|
624
|
-
.command("codex")
|
|
625
|
-
.description("Install nlm-memory as a Codex CLI plugin (marketplace + plugin add)")
|
|
626
|
-
.option("--source <source>", "marketplace source (owner/repo, git URL, or local path)", "pbmagnet4/nlm-memory-ts")
|
|
627
|
-
.option("--local", "shortcut for --source <repo-root>; use during dev")
|
|
628
|
-
.option("--with-hooks", "additionally write absolute paths to ~/.codex/hooks.json (Codex Desktop fallback for openai/codex#16430)")
|
|
629
|
-
.option("--dry-run", "print what would happen without invoking codex")
|
|
630
|
-
.action((opts) => {
|
|
631
|
-
if (!opts.dryRun && !codexBinaryAvailable()) {
|
|
632
|
-
console.error("nlm connect codex: `codex` binary not on PATH. Install via `npm i -g @openai/codex` or `brew install codex`.");
|
|
633
|
-
process.exit(1);
|
|
634
|
-
}
|
|
635
|
-
const source = opts.local ? REPO_ROOT : opts.source;
|
|
636
|
-
const report = connectCodex(
|
|
637
|
-
{ source, withHooks: Boolean(opts.withHooks), dryRun: Boolean(opts.dryRun) },
|
|
638
|
-
pluginScriptsDir(REPO_ROOT),
|
|
639
|
-
);
|
|
640
|
-
|
|
641
|
-
if (report.dryRun) {
|
|
642
|
-
console.error("nlm connect codex (dry run):");
|
|
643
|
-
console.error(` codex plugin marketplace add ${report.source}`);
|
|
644
|
-
console.error(` codex plugin add ${report.pluginName}@${report.marketplaceName}`);
|
|
645
|
-
console.error(` write [mcp_servers.nlm-memory] block to ${report.mcpServerWritten}`);
|
|
646
|
-
if (report.legacyHooksWritten) {
|
|
647
|
-
console.error(` write legacy fallback to ${report.legacyHooksWritten}`);
|
|
648
|
-
}
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
if (report.marketplaceAdd && report.marketplaceAdd.status !== 0) {
|
|
653
|
-
const stderr = report.marketplaceAdd.stderr.trim();
|
|
654
|
-
console.error(`nlm connect codex: marketplace add failed (exit ${report.marketplaceAdd.status}).`);
|
|
655
|
-
if (stderr) console.error(` codex stderr: ${stderr}`);
|
|
656
|
-
process.exit(1);
|
|
657
|
-
}
|
|
658
|
-
if (report.pluginAdd && report.pluginAdd.status !== 0) {
|
|
659
|
-
const stderr = report.pluginAdd.stderr.trim();
|
|
660
|
-
console.error(`nlm connect codex: plugin add failed (exit ${report.pluginAdd.status}).`);
|
|
661
|
-
if (stderr) console.error(` codex stderr: ${stderr}`);
|
|
662
|
-
process.exit(1);
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
console.error(`nlm: connected to Codex via marketplace ${report.marketplaceName}, plugin ${report.pluginName}.`);
|
|
666
|
-
if (report.mcpServerWritten) {
|
|
667
|
-
console.error(` Wrote [mcp_servers.nlm-memory] to ${report.mcpServerWritten}`);
|
|
668
|
-
}
|
|
669
|
-
if (report.legacyHooksWritten) {
|
|
670
|
-
console.error(` Wrote hooks.json fallback to ${report.legacyHooksWritten}`);
|
|
671
|
-
}
|
|
672
|
-
console.error(" Next: run `codex` interactively and approve the hook trust prompts. Then prompt — recall should fire.");
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
connect
|
|
676
|
-
.command("claude-code")
|
|
677
|
-
.description("Write the nlm-memory MCP server block into ~/.mcp.json")
|
|
678
|
-
.option("--with-hooks", "also install Claude Code session hooks")
|
|
679
|
-
.option("--dry-run", "print what would happen without changing files")
|
|
680
|
-
.action((opts) => {
|
|
681
|
-
if (opts.dryRun) {
|
|
682
|
-
console.error("nlm connect claude-code (dry run):");
|
|
683
|
-
console.error(` write [mcpServers.nlm-memory] to ${join(homedir(), ".mcp.json")}`);
|
|
684
|
-
if (opts.withHooks) console.error(" install 6 Claude Code hooks");
|
|
685
|
-
return;
|
|
686
|
-
}
|
|
687
|
-
const report = connectClaudeCode({ nlmBinPath: __filename, nodeExecPath: process.execPath });
|
|
688
|
-
const action = report.alreadyPresent ? "updated" : "written";
|
|
689
|
-
console.error(`nlm: [mcpServers.nlm-memory] ${action} → ${report.mcpConfigPath}`);
|
|
690
|
-
console.error(" Restart Claude Code to activate the MCP server.");
|
|
691
|
-
if (opts.withHooks) {
|
|
692
|
-
const path = claudeSettingsPath();
|
|
693
|
-
const hookLogPath = process.env["NLM_HOOK_LOG"] ?? join(homedir(), ".nlm", "hook-log.jsonl");
|
|
694
|
-
const result = installClaudeCodeHooks({
|
|
695
|
-
nodeExecPath: process.execPath,
|
|
696
|
-
hooks: ALL_HOOKS,
|
|
697
|
-
settingsPath: path,
|
|
698
|
-
hookLogPath,
|
|
699
|
-
addHook,
|
|
700
|
-
removeHook,
|
|
701
|
-
buildHookCommand,
|
|
702
|
-
smokeTestHookCommand,
|
|
703
|
-
});
|
|
704
|
-
if (!result.ok) {
|
|
705
|
-
console.error(`nlm: ${result.failedLabel ?? "hook"} smoke test failed — all hooks reverted. Run \`nlm hook install\` manually.`);
|
|
706
|
-
process.exit(1);
|
|
707
|
-
}
|
|
708
|
-
console.error(`nlm: ${result.count} hooks installed → ${path}`);
|
|
709
|
-
}
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
connect
|
|
713
|
-
.command("hermes")
|
|
714
|
-
.description("Write the nlm-memory MCP server entry into ~/.hermes/config.yaml")
|
|
715
|
-
.option("--dry-run", "print what would happen without changing files")
|
|
716
|
-
.action((opts) => {
|
|
717
|
-
if (opts.dryRun) {
|
|
718
|
-
console.error(`nlm connect hermes (dry run): write [mcp_servers.nlm-memory] to ${hermesConfigPath()}`);
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
const report = connectHermes({ nlmBinPath: __filename, nodeExecPath: process.execPath, dryRun: false });
|
|
722
|
-
const action = report.alreadyPresent ? "updated" : "written";
|
|
723
|
-
console.error(`nlm: [mcp_servers.nlm-memory] ${action} → ${report.configPath}`);
|
|
724
|
-
console.error(" Restart Hermes to activate the MCP server.");
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
connect
|
|
728
|
-
.command("hermes-agent")
|
|
729
|
-
.description("Install the nlm-memory plugin into NousResearch Hermes Agent (~/.hermes/plugins/nlm-memory/)")
|
|
730
|
-
.option("--dry-run", "print what would happen without changing files")
|
|
731
|
-
.action((opts) => {
|
|
732
|
-
const pluginSrcDir = join(REPO_ROOT, "plugin-hermes-agent");
|
|
733
|
-
if (opts.dryRun) {
|
|
734
|
-
console.error(`nlm connect hermes-agent (dry run): copy ${pluginSrcDir} → ${hermesAgentPluginDir()}`);
|
|
735
|
-
console.error(" then: hermes plugins enable nlm-memory");
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
const report = connectHermesAgent({ pluginSrcDir, dryRun: false });
|
|
739
|
-
const action = report.alreadyPresent ? "updated" : "installed";
|
|
740
|
-
console.error(`nlm: nlm-memory plugin ${action} → ${report.destDir}`);
|
|
741
|
-
if (report.enabledViaCli) {
|
|
742
|
-
console.error(" Enabled via: hermes plugins enable nlm-memory");
|
|
743
|
-
} else {
|
|
744
|
-
console.error(" Run: hermes plugins enable nlm-memory (if hermes binary is on PATH)");
|
|
745
|
-
}
|
|
746
|
-
console.error(" Also run: nlm connect hermes (to wire the MCP server)");
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
const disconnect = program
|
|
750
|
-
.command("disconnect")
|
|
751
|
-
.description("Disconnect nlm-memory from an AI coding runtime");
|
|
752
|
-
|
|
753
|
-
disconnect
|
|
754
|
-
.command("codex")
|
|
755
|
-
.description("Remove the nlm-memory plugin + marketplace from Codex")
|
|
756
|
-
.option("--with-hooks", "also strip our entries from ~/.codex/hooks.json")
|
|
757
|
-
.option("--dry-run", "print what would happen without invoking codex")
|
|
758
|
-
.action((opts) => {
|
|
759
|
-
if (!opts.dryRun && !codexBinaryAvailable()) {
|
|
760
|
-
console.error("nlm disconnect codex: `codex` binary not on PATH.");
|
|
761
|
-
process.exit(1);
|
|
762
|
-
}
|
|
763
|
-
const report = disconnectCodex({
|
|
764
|
-
withHooks: Boolean(opts.withHooks),
|
|
765
|
-
dryRun: Boolean(opts.dryRun),
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
if (report.dryRun) {
|
|
769
|
-
console.error("nlm disconnect codex (dry run):");
|
|
770
|
-
console.error(` codex plugin remove ${report.pluginName}@${report.marketplaceName}`);
|
|
771
|
-
console.error(` codex plugin marketplace remove ${report.marketplaceName}`);
|
|
772
|
-
console.error(" strip [mcp_servers.nlm-memory] block from ~/.codex/config.toml");
|
|
773
|
-
if (opts.withHooks) console.error(" strip our entries from ~/.codex/hooks.json");
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// Best-effort removal — non-zero exits from codex are reported but
|
|
778
|
-
// don't abort, because partial cleanup (plugin removed, marketplace
|
|
779
|
-
// already gone) is the common case for repeat invocations.
|
|
780
|
-
const pluginStderr = (report.pluginRemove?.stderr ?? "").trim();
|
|
781
|
-
const marketStderr = (report.marketplaceRemove?.stderr ?? "").trim();
|
|
782
|
-
if (report.pluginRemove?.status !== 0 && pluginStderr) {
|
|
783
|
-
console.error(` plugin remove: ${pluginStderr}`);
|
|
784
|
-
}
|
|
785
|
-
if (report.marketplaceRemove?.status !== 0 && marketStderr) {
|
|
786
|
-
console.error(` marketplace remove: ${marketStderr}`);
|
|
787
|
-
}
|
|
788
|
-
console.error("nlm: disconnected from Codex.");
|
|
789
|
-
console.error(report.mcpServerRemoved
|
|
790
|
-
? " Stripped [mcp_servers.nlm-memory] block from ~/.codex/config.toml"
|
|
791
|
-
: " No [mcp_servers.nlm-memory] block to remove from ~/.codex/config.toml");
|
|
792
|
-
if (opts.withHooks) {
|
|
793
|
-
console.error(report.legacyHooksRemoved
|
|
794
|
-
? " Stripped our entries from ~/.codex/hooks.json"
|
|
795
|
-
: " No legacy hooks to remove from ~/.codex/hooks.json");
|
|
796
|
-
}
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
disconnect
|
|
800
|
-
.command("claude-code")
|
|
801
|
-
.description("Remove the nlm-memory MCP server block from ~/.mcp.json")
|
|
802
|
-
.option("--dry-run", "print what would happen without changing files")
|
|
803
|
-
.action((opts) => {
|
|
804
|
-
const report = disconnectClaudeCode({ dryRun: Boolean(opts.dryRun) });
|
|
805
|
-
if (opts.dryRun) {
|
|
806
|
-
console.error(`nlm disconnect claude-code (dry run): strip [mcpServers.nlm-memory] from ${report.mcpConfigPath}`);
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
|
-
console.error(report.removed
|
|
810
|
-
? `nlm: removed [mcpServers.nlm-memory] from ${report.mcpConfigPath}`
|
|
811
|
-
: `nlm: no [mcpServers.nlm-memory] entry found in ${report.mcpConfigPath}`);
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
disconnect
|
|
815
|
-
.command("hermes")
|
|
816
|
-
.description("Remove the nlm-memory MCP server entry from ~/.hermes/config.yaml")
|
|
817
|
-
.option("--dry-run", "print what would happen without changing files")
|
|
818
|
-
.action((opts) => {
|
|
819
|
-
const report = disconnectHermes({ dryRun: Boolean(opts.dryRun) });
|
|
820
|
-
if (opts.dryRun) {
|
|
821
|
-
console.error(`nlm disconnect hermes (dry run): strip [mcp_servers.nlm-memory] from ${report.configPath}`);
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
console.error(report.removed
|
|
825
|
-
? `nlm: removed [mcp_servers.nlm-memory] from ${report.configPath}`
|
|
826
|
-
: `nlm: no [mcp_servers.nlm-memory] entry found in ${report.configPath}`);
|
|
827
|
-
});
|
|
828
|
-
|
|
829
|
-
disconnect
|
|
830
|
-
.command("hermes-agent")
|
|
831
|
-
.description("Remove the nlm-memory plugin from ~/.hermes/plugins/nlm-memory/")
|
|
832
|
-
.option("--dry-run", "print what would happen without changing files")
|
|
833
|
-
.action((opts) => {
|
|
834
|
-
const report = disconnectHermesAgent({ dryRun: Boolean(opts.dryRun) });
|
|
835
|
-
if (opts.dryRun) {
|
|
836
|
-
console.error(`nlm disconnect hermes-agent (dry run): remove ${hermesAgentPluginDir()}`);
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
console.error(report.removed
|
|
840
|
-
? `nlm: removed plugin directory ${report.destDir}`
|
|
841
|
-
: `nlm: no plugin directory found at ${report.destDir}`);
|
|
842
|
-
});
|
|
843
|
-
|
|
844
|
-
program
|
|
845
|
-
.command("setup")
|
|
846
|
-
.description("Interactive first-run setup: detect runtimes, wire MCP + hooks, start daemon")
|
|
847
|
-
.action(async () => {
|
|
848
|
-
await runSetup({
|
|
849
|
-
nlmBinPath: __filename,
|
|
850
|
-
nodeExecPath: process.execPath,
|
|
851
|
-
migrationsDir: MIGRATIONS_DIR,
|
|
852
|
-
repoRoot: REPO_ROOT,
|
|
853
|
-
dbPath: dbPath(),
|
|
854
|
-
launchAgentLabel: LAUNCH_AGENT_LABEL,
|
|
855
|
-
launchAgentPlist: LAUNCH_AGENT_PLIST,
|
|
856
|
-
buildPlist,
|
|
857
|
-
claudeSettingsPath: claudeSettingsPath(),
|
|
858
|
-
allHooks: ALL_HOOKS,
|
|
859
|
-
addHook,
|
|
860
|
-
removeHook,
|
|
861
|
-
buildHookCommand,
|
|
862
|
-
smokeTestHookCommand,
|
|
863
|
-
});
|
|
864
|
-
});
|
|
865
|
-
|
|
866
|
-
program
|
|
867
|
-
.command("useful-scan")
|
|
868
|
-
.description("Scan hook log for useful recall hits; writes to ~/.nlm/useful-hit-log.jsonl")
|
|
869
|
-
.option("-d, --days <n>", "rolling window in days", (v) => Number.parseInt(v, 10), 1)
|
|
870
|
-
.option("--dry-run", "compute without writing to disk")
|
|
871
|
-
.action(async (opts) => {
|
|
872
|
-
const result = await scanUsefulHits({ days: opts.days as number, ...(opts.dryRun ? { dryRun: true } : {}) });
|
|
873
|
-
const rate = result.measurable === 0
|
|
874
|
-
? "no measurable entries"
|
|
875
|
-
: `${result.useful}/${result.measurable} useful (${Math.round((result.useful / result.measurable) * 100)}%)`;
|
|
876
|
-
console.error(
|
|
877
|
-
`nlm useful-scan: scanned ${result.total} recalls in the last ${opts.days as number}d — ${rate}` +
|
|
878
|
-
(opts.dryRun ? " (dry-run)" : `, ${result.appended} appended`),
|
|
879
|
-
);
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
program.parseAsync().catch((e) => {
|
|
883
|
-
console.error("nlm: fatal", e);
|
|
884
|
-
process.exit(1);
|
|
885
|
-
});
|