nlm-memory 0.5.1 → 0.5.3
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/LICENSE +202 -151
- package/README.md +221 -63
- package/dist/cli/digest.d.ts +20 -0
- package/dist/cli/digest.js +142 -0
- package/dist/cli/digest.js.map +1 -0
- package/dist/cli/nlm.d.ts +1 -0
- package/dist/cli/nlm.js +23 -0
- package/dist/cli/nlm.js.map +1 -1
- package/dist/core/digest/compose.d.ts +38 -0
- package/dist/core/digest/compose.js +93 -0
- package/dist/core/digest/compose.js.map +1 -0
- package/dist/core/digest/hook-liveness.d.ts +32 -0
- package/dist/core/digest/hook-liveness.js +54 -0
- package/dist/core/digest/hook-liveness.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,27 +1,39 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<strong>nlm-memory</strong><br/>
|
|
3
|
+
Local-first non-linear memory OS for AI operators
|
|
4
|
+
</p>
|
|
5
|
+
|
|
6
|
+
<p align="center">
|
|
7
|
+
<a href="https://www.npmjs.com/package/nlm-memory"><img src="https://img.shields.io/npm/v/nlm-memory?color=CB3837&label=npm&logo=npm" alt="npm version" /></a>
|
|
8
|
+
<a href="https://github.com/pbmagnet4/nlm-memory-ts/blob/main/LICENSE"><img src="https://img.shields.io/github/license/pbmagnet4/nlm-memory-ts?color=blue" alt="License: Apache 2.0" /></a>
|
|
9
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/node/v/nlm-memory?color=brightgreen" alt="Node 20+" /></a>
|
|
10
|
+
<img src="https://img.shields.io/badge/tests-612%20passing-success" alt="612 tests passing" />
|
|
11
|
+
<img src="https://img.shields.io/badge/runtimes-9-8A2BE2" alt="9 runtimes supported" />
|
|
12
|
+
<img src="https://img.shields.io/badge/telemetry-none-informational" alt="Zero telemetry" />
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="#install">Install</a> ·
|
|
17
|
+
<a href="#quick-start">Quick Start</a> ·
|
|
18
|
+
<a href="#runtimes">Runtimes</a> ·
|
|
19
|
+
<a href="#how-recall-works">How recall works</a> ·
|
|
20
|
+
<a href="#mcp-tools">MCP</a> ·
|
|
21
|
+
<a href="#rest-api">REST API</a> ·
|
|
22
|
+
<a href="#daily-digest">Digest</a> ·
|
|
23
|
+
<a href="#configuration">Config</a> ·
|
|
24
|
+
<a href="#security">Security</a> ·
|
|
25
|
+
<a href="#vs-alternatives">vs Alternatives</a>
|
|
26
|
+
</p>
|
|
2
27
|
|
|
3
|
-
|
|
28
|
+
---
|
|
4
29
|
|
|
5
|
-
`nlm-memory` indexes every session from Claude Code, Codex, OpenCode, Cursor, Windsurf, Hermes, Aider, and pi into a single searchable store on your machine. Three properties no
|
|
30
|
+
`nlm-memory` indexes every session from Claude Code, Codex, OpenCode, Cursor, Windsurf, Hermes, Aider, and pi into a single searchable store on your machine. Three properties no other memory layer ships together:
|
|
6
31
|
|
|
7
|
-
1. **Cross-runtime reach.** One index
|
|
8
|
-
2. **Editable timeline.** Sessions can be superseded, retired, or marked aborted.
|
|
32
|
+
1. **Cross-runtime reach.** One index, every adapter.
|
|
33
|
+
2. **Editable timeline.** Sessions can be superseded, retired, or marked aborted. Patch history retroactively — no other tool lets you do this.
|
|
9
34
|
3. **97.2% R@5 baseline.** On a 14-month corpus, keyword recall surfaces the right session in the top 5 on 97.2% of evaluator queries. No fine-tuning, no cloud, no account.
|
|
10
35
|
|
|
11
|
-
Everything stays on your machine. No telemetry, no account
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## Requirements
|
|
16
|
-
|
|
17
|
-
- **Node 20+**
|
|
18
|
-
- **[Ollama](https://ollama.com)** running locally with `nomic-embed-text` pulled for semantic search:
|
|
19
|
-
```sh
|
|
20
|
-
ollama pull nomic-embed-text
|
|
21
|
-
```
|
|
22
|
-
- **A classifier** — pick during setup:
|
|
23
|
-
- **DeepSeek cloud** (recommended for speed) — fast, cheap (~$0.002/session). Sends up to 30K chars of each session transcript to `api.deepseek.com`.
|
|
24
|
-
- **Ollama local** — fully offline. Slower; uses whichever chat model you select from your local pull list.
|
|
36
|
+
Everything stays on your machine. No telemetry, no account beyond your classifier of choice.
|
|
25
37
|
|
|
26
38
|
---
|
|
27
39
|
|
|
@@ -32,49 +44,129 @@ npm install -g nlm-memory
|
|
|
32
44
|
nlm setup
|
|
33
45
|
```
|
|
34
46
|
|
|
35
|
-
`nlm setup` is the interactive first-run wizard. It
|
|
36
|
-
|
|
37
|
-
### Platform support
|
|
47
|
+
`nlm setup` is the interactive first-run wizard. It picks your classifier + model, wires the runtimes you actually use, generates an `NLM_MCP_TOKEN`, hardens permissions on `~/.nlm/`, and installs the daemon supervisor for your platform.
|
|
38
48
|
|
|
39
49
|
| Platform | Daemon | Notes |
|
|
40
50
|
|---|---|---|
|
|
41
51
|
| **macOS** | LaunchAgent at `~/Library/LaunchAgents/com.github.pbmagnet4.nlm-memory.plist` | Auto-starts on login |
|
|
42
|
-
| **Linux** | systemd user unit at `~/.config/systemd/user/nlm.service` |
|
|
43
|
-
| **Windows** | Manual `nlm start` for now | Hook + MCP install paths are platform-aware;
|
|
52
|
+
| **Linux** | systemd user unit at `~/.config/systemd/user/nlm.service` | Headless servers: `loginctl enable-linger $USER` so the daemon survives logout |
|
|
53
|
+
| **Windows** | Manual `nlm start` for now | Hook + MCP install paths are platform-aware; supervisor lands next release |
|
|
54
|
+
|
|
55
|
+
Stop or remove: `nlm uninstall`.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
After `nlm setup` finishes, open **http://localhost:3940/ui** — the daemon is running. A 30-second sanity check:
|
|
44
62
|
|
|
45
|
-
To stop or remove:
|
|
46
63
|
```sh
|
|
47
|
-
nlm
|
|
64
|
+
nlm recall "what was that pgvector decision" # one-shot search from the shell
|
|
65
|
+
nlm digest # yesterday's activity at a glance
|
|
66
|
+
nlm --version
|
|
48
67
|
```
|
|
49
68
|
|
|
50
69
|
---
|
|
51
70
|
|
|
71
|
+
## Runtimes
|
|
72
|
+
|
|
73
|
+
One corpus across every adapter. `nlm connect` wires hooks + MCP for each runtime:
|
|
74
|
+
|
|
75
|
+
| Runtime | Connect | Sessions read from | Hooks |
|
|
76
|
+
|---|---|---|---|
|
|
77
|
+
| **Claude Code** | `nlm connect claude-code` | `~/.claude/projects/**/*.jsonl` | 5 (UserPromptSubmit, SessionStart, Stop, PreCompact, SubagentStart) |
|
|
78
|
+
| **Codex CLI** | `nlm connect codex` | `~/.codex/sessions/` | Marketplace plugin |
|
|
79
|
+
| **Hermes** | `nlm connect hermes` | Hermes session DB | MCP only |
|
|
80
|
+
| **Hermes Agent** | `nlm connect hermes-agent` | Hermes plugin path | pre-turn, post-turn, lifecycle |
|
|
81
|
+
| **Cursor** | `nlm connect cursor` | Cursor IDE chat DB | MCP only |
|
|
82
|
+
| **Windsurf** | `nlm connect windsurf` | Windsurf user dir | MCP only |
|
|
83
|
+
| **OpenCode** | adapter active | `~/.local/share/opencode/` | MCP only |
|
|
84
|
+
| **Aider** | adapter active | `AIDER_CHAT_HISTORY_FILE` | MCP only |
|
|
85
|
+
| **pi.dev** | adapter active | `~/.pi/sessions/` | MCP only |
|
|
86
|
+
|
|
87
|
+
`nlm disconnect <runtime>` reverses any of the above.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
52
91
|
## How recall works
|
|
53
92
|
|
|
54
|
-
|
|
93
|
+
Two delivery paths. They share the same index.
|
|
55
94
|
|
|
56
95
|
### 1. Hooks (Claude Code) — automatic context injection
|
|
57
96
|
|
|
58
|
-
|
|
97
|
+
Five hooks installed into `~/.claude/settings.json`:
|
|
59
98
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
99
|
+
| Event | What NLM does | Mode |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| **UserPromptSubmit** | Score the prompt, silently prepend pointer block listing 0–3 most likely-relevant prior sessions | live by default |
|
|
102
|
+
| **SessionStart** | Cold-start agents (cron, background) hit this; same pointer-block delivery without a user prompt | live by default |
|
|
103
|
+
| **Stop** | Scan the model's response for citations of surfaced session IDs → updates `useful_hit_rate` and builds the reranker training substrate | always on |
|
|
104
|
+
| **PreCompact** | Flush the per-conversation surfaced-IDs memo so post-compaction recalls aren't gated | always on |
|
|
105
|
+
| **SubagentStart** | Record parent→subagent links so threads stay coherent across dispatches | always on |
|
|
63
106
|
|
|
64
|
-
|
|
107
|
+
Switch to **shadow** mode (log-only, no injection) anytime with `NLM_HOOK_MODE=shadow` on the hook command. Toggle for an existing install: re-run `nlm hook install` after changing the env var. The hook fails open — any error yields a clean exit and never blocks the model.
|
|
65
108
|
|
|
66
109
|
### 2. MCP — explicit tools any agent can call
|
|
67
110
|
|
|
111
|
+
Container-hosted agents (Hermes WebUI, Codex CLI, etc.) hit the Streamable-HTTP `POST /mcp` endpoint with `Authorization: Bearer ${NLM_MCP_TOKEN}`. Stdio MCP is also supported for Claude Code via `~/.mcp.json`.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## MCP Tools
|
|
116
|
+
|
|
117
|
+
| Tool | What it does |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `recall_sessions` | Hybrid keyword+semantic search across the full session corpus. Returns label, started_at, snippet, match score. |
|
|
120
|
+
| `get_session` | Full body of one session by ID. Includes enriched `supersedes` / `supersededBy` links (id + label + summary) so chasing corrected facts doesn't need a second round-trip. |
|
|
121
|
+
| `recall_facts` | Search structured facts: decisions, open questions, project state. Filterable by entity and kind. |
|
|
122
|
+
| `get_fact_history` | Full version history of one fact — how a decision evolved over time. |
|
|
123
|
+
| `cite_session` | Mark a session as explicitly referenced. Drives the `useful_hit_rate` metric and the future learned reranker. |
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## REST API
|
|
128
|
+
|
|
129
|
+
Daemon binds `127.0.0.1:3940` (override with `NLM_PORT`). Selected endpoints:
|
|
130
|
+
|
|
131
|
+
| Method | Path | Auth | Purpose |
|
|
132
|
+
|---|---|---|---|
|
|
133
|
+
| GET | `/api/health` | Host-only | Liveness probe; returns `{version, status, service}` |
|
|
134
|
+
| GET | `/api/recall` | Bearer/Origin | Hybrid recall — `?q=`, `?mode=keyword\|semantic\|hybrid`, `?limit=` |
|
|
135
|
+
| GET | `/api/recall/stats` | Bearer/Origin | 7-day stats: total, hit_rate, useful_hit_rate, top queries |
|
|
136
|
+
| GET | `/api/recall/recent` | Bearer/Origin | Last N recall events for live tail/telemetry |
|
|
137
|
+
| GET | `/api/recall/cite-stats` | Bearer/Origin | Citation rate over `?days=` |
|
|
138
|
+
| GET | `/api/session/:id` | Bearer/Origin | Full session body + supersedence links |
|
|
139
|
+
| GET | `/api/recall/facts` | Bearer/Origin | Structured fact search |
|
|
140
|
+
| GET | `/api/facts/history` | Bearer/Origin | Version chain for one fact |
|
|
141
|
+
| GET | `/api/dataset` | Bearer/Origin | Full session list for the UI dataset view |
|
|
142
|
+
| GET | `/api/live/recent-writes` | Bearer/Origin | Live tail of ingested sessions |
|
|
143
|
+
| GET | `/api/data/backup` | Bearer/Origin | Streaming SQLite snapshot download |
|
|
144
|
+
| POST | `/api/data/restore` | Bearer/Origin | Stage a snapshot for apply-on-restart |
|
|
145
|
+
| POST | `/api/hook/pre-compact` | Bearer/Origin | Hook endpoint; flushes the surfaced-IDs memo |
|
|
146
|
+
| ALL | `/mcp` | Bearer required | Streamable-HTTP MCP transport for container agents |
|
|
147
|
+
|
|
148
|
+
`/api/*` is gated by three layers: 127.0.0.1 Host check (defeats DNS rebinding), Origin check when the browser sends one (defeats cross-origin drive-by), Bearer fallback when Origin is absent (server-to-server clients).
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Daily digest
|
|
153
|
+
|
|
154
|
+
Once-a-day summary of yesterday's activity:
|
|
155
|
+
|
|
68
156
|
```sh
|
|
69
|
-
nlm
|
|
70
|
-
nlm
|
|
71
|
-
nlm connect hermes # writes ~/.hermes/config.yaml (MCP)
|
|
72
|
-
nlm connect hermes-agent # installs as a NousResearch Hermes plugin (hooks + MCP)
|
|
157
|
+
nlm digest # print to stdout
|
|
158
|
+
nlm digest --telegram # post to Telegram (TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID)
|
|
73
159
|
```
|
|
74
160
|
|
|
75
|
-
|
|
161
|
+
Reports 24h real-traffic (probes filtered), 7d hit_rate + useful_hit_rate, top 5 queries, and a **`WARN hook silent`** alert when Claude Code ran yesterday but no live hook fires were logged. That alert is the canary for post-install drift — node upgrades, `settings.json` hand-edits, and `dist/` moves silently break the hook while Claude Code keeps working. Setup-time smoke tests can't catch this; only the daily correlation can.
|
|
162
|
+
|
|
163
|
+
Wire to cron for a morning push:
|
|
164
|
+
|
|
165
|
+
```cron
|
|
166
|
+
0 7 * * * nlm digest --telegram >> ~/.nlm/logs/digest.log 2>&1
|
|
167
|
+
```
|
|
76
168
|
|
|
77
|
-
|
|
169
|
+
When the daemon is unreachable, `--telegram` still fires — posts a "daemon unreachable" alert instead of failing silently.
|
|
78
170
|
|
|
79
171
|
---
|
|
80
172
|
|
|
@@ -84,30 +176,88 @@ Open `http://localhost:3940/ui` after the daemon starts.
|
|
|
84
176
|
|
|
85
177
|
| Page | What it shows |
|
|
86
178
|
|---|---|
|
|
87
|
-
| **Live** | Sessions being written in real time, recent reads
|
|
179
|
+
| **Live** | Sessions being written in real time, recent reads, recent decisions |
|
|
88
180
|
| **Pulse** | System health — coherence, runtimes, stale entities, recent sessions |
|
|
89
|
-
| **River** | Full session timeline with density controls
|
|
90
|
-
| **Thread** | Per-entity conversation history with runtime filters |
|
|
91
|
-
| **Search** | Keyword, semantic, or hybrid recall with match snippets |
|
|
92
|
-
| **Recall** | Adoption telemetry —
|
|
181
|
+
| **River** | Full session timeline with density controls + superseded-lane visualization |
|
|
182
|
+
| **Thread** | Per-entity conversation history with runtime filters and ←/→ navigation |
|
|
183
|
+
| **Search** | Keyword, semantic, or hybrid recall with match snippets and field-origin tags |
|
|
184
|
+
| **Recall** | Adoption telemetry — useful_hit_rate, source breakdown, query log |
|
|
93
185
|
| **Settings** | Sources, providers, classifier, data backup/restore |
|
|
94
186
|
|
|
95
187
|
---
|
|
96
188
|
|
|
189
|
+
## Pipeline
|
|
190
|
+
|
|
191
|
+
What happens when an AI runtime writes a session and you later recall it:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
ingest: runtime transcript (jsonl/sqlite)
|
|
195
|
+
-> adapter parses runtime-specific format
|
|
196
|
+
-> classifier (DeepSeek cloud or Ollama local) extracts label + entities + decisions + open questions
|
|
197
|
+
-> embedder (nomic-embed-text via Ollama) computes 768-dim vector
|
|
198
|
+
-> SQLite canonical store + FTS5 keyword index + sqlite-vec ANN index
|
|
199
|
+
|
|
200
|
+
recall: prompt / query
|
|
201
|
+
-> tokenize + match scoring (label x3, entity-exact x4, decision x2, summary x1, phrase-bonus +5)
|
|
202
|
+
-> hybrid: BM25-style keyword + vector cosine, fused by score
|
|
203
|
+
-> select-top-N gate (per-fire cap 3, per-conversation cap 10)
|
|
204
|
+
-> pointer block prepended to model context (hooks) or returned as tool result (MCP)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Configuration
|
|
210
|
+
|
|
211
|
+
### Environment variables
|
|
212
|
+
|
|
213
|
+
| Var | Default | What |
|
|
214
|
+
|---|---|---|
|
|
215
|
+
| `NLM_PORT` | `3940` | Daemon bind port (loopback only) |
|
|
216
|
+
| `NLM_DB_PATH` | `~/.nlm/canonical.sqlite` | SQLite canonical store location |
|
|
217
|
+
| `NLM_HOOK_MODE` | `live` | `live` injects pointer block; `shadow` logs without injecting |
|
|
218
|
+
| `NLM_HOOK_LOG` | `~/.nlm/hook-log.jsonl` | Hook fire log; powers digest's liveness alert |
|
|
219
|
+
| `NLM_USEFUL_HIT_LOG` | `~/.nlm/useful-hit-log.jsonl` | Citation/useful-hit ledger |
|
|
220
|
+
| `NLM_QUERY_LOG` | `~/.nlm/query-log.jsonl` | Recall query telemetry |
|
|
221
|
+
| `NLM_CITATION_LOG` | `~/.nlm/citation-log.jsonl` | Stop-hook citation events |
|
|
222
|
+
| `NLM_MCP_TOKEN` | auto-generated | 256-bit bearer for `/api/*` (non-browser) and `/mcp` |
|
|
223
|
+
| `NLM_MCP_CONFIG` | `~/.mcp.json` | Path the `connect`/`disconnect` commands modify |
|
|
224
|
+
| `NLM_CLASSIFIER` | `deepseek` | `deepseek` (cloud) or `ollama` (local) |
|
|
225
|
+
| `NLM_CLASSIFIER_MODEL` | `deepseek-v4-flash` | Model id for the chosen provider |
|
|
226
|
+
| `NLM_OLLAMA_URL` | `http://localhost:11434` | Override Ollama endpoint |
|
|
227
|
+
| `NLM_ADAPTERS` | all | Comma-separated allowlist of adapters to enable |
|
|
228
|
+
| `DEEPSEEK_API_KEY` | — | Required when classifier=deepseek |
|
|
229
|
+
| `TELEGRAM_BOT_TOKEN` / `TELEGRAM_CHAT_ID` | — | Required for `nlm digest --telegram` |
|
|
230
|
+
|
|
231
|
+
Adapter source paths can be overridden individually: `NLM_CLAUDE_PROJECTS_PATH`, `NLM_CODEX_CONFIG`, `NLM_CURSOR_DB_PATH`, `NLM_HERMES_SESSIONS_PATH`, `NLM_HERMES_AGENT_DB_PATH`, `NLM_WINDSURF_USER_DIR`, `OPENCODE_DB_PATH`, `PI_SESSIONS_PATH`, `AIDER_CHAT_HISTORY_FILE`.
|
|
232
|
+
|
|
233
|
+
### Config file
|
|
234
|
+
|
|
235
|
+
`~/.nlm/.env` — autoloaded by every CLI command. Mode `0600`, owned by you, never readable by other users. The setup wizard writes the initial keys; you can edit it directly.
|
|
236
|
+
|
|
237
|
+
### Ports
|
|
238
|
+
|
|
239
|
+
| Port | Process | Bind | Override |
|
|
240
|
+
|---|---|---|---|
|
|
241
|
+
| `3940` | Daemon HTTP API + MCP | `127.0.0.1` only | `NLM_PORT` |
|
|
242
|
+
| `11434` | Ollama (embedding + local classifier) | localhost | `NLM_OLLAMA_URL` |
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
97
246
|
## Security
|
|
98
247
|
|
|
99
248
|
NLM is local-first by design. The daemon:
|
|
100
249
|
|
|
101
|
-
- Binds to `127.0.0.1` only — never `0.0.0.0
|
|
102
|
-
- Enforces Host + Origin checks on `/api/*` to
|
|
103
|
-
- Generates a 256-bit `NLM_MCP_TOKEN` on first run
|
|
104
|
-
- Recursively enforces `0700` on `~/.nlm/` and `0600` on its contents on every start
|
|
250
|
+
- Binds to `127.0.0.1` only — never `0.0.0.0`
|
|
251
|
+
- Enforces Host + Origin checks on `/api/*` to defeat DNS rebinding and cross-origin drive-by
|
|
252
|
+
- Generates a 256-bit `NLM_MCP_TOKEN` on first run, persists to `~/.nlm/.env` (mode `0600`); non-browser clients authenticate with `Authorization: Bearer ${NLM_MCP_TOKEN}` compared with `timingSafeEqual`
|
|
253
|
+
- Recursively enforces `0700` on `~/.nlm/` and `0600` on its contents on every start
|
|
105
254
|
- Sends nothing outbound except:
|
|
106
|
-
- Ollama (`localhost:11434`) for embeddings
|
|
107
|
-
- DeepSeek API (`api.deepseek.com`)
|
|
255
|
+
- Ollama (`localhost:11434`) for embeddings and the local classifier path
|
|
256
|
+
- DeepSeek API (`api.deepseek.com`) when classifier is set to DeepSeek
|
|
257
|
+
- Telegram API (`api.telegram.org`) when `nlm digest --telegram` is invoked
|
|
108
258
|
- Your AI runtime transcript files (read-only)
|
|
109
259
|
|
|
110
|
-
No telemetry. No vendor
|
|
260
|
+
No telemetry. No vendor pings. No account.
|
|
111
261
|
|
|
112
262
|
Report vulnerabilities via [SECURITY.md](SECURITY.md).
|
|
113
263
|
|
|
@@ -123,13 +273,21 @@ Old installs have `NLM_HOOK_MODE=shadow` hardcoded in `~/.claude/settings.json`
|
|
|
123
273
|
|
|
124
274
|
---
|
|
125
275
|
|
|
126
|
-
##
|
|
276
|
+
## vs Alternatives
|
|
277
|
+
|
|
278
|
+
| | **nlm-memory** | mem0 | Letta / MemGPT | Built-in (`CLAUDE.md`) |
|
|
279
|
+
|---|---|---|---|---|
|
|
280
|
+
| **Unit of memory** | Whole session + extracted markers | Atomic facts | Graph nodes + edges | Static file |
|
|
281
|
+
| **Cross-runtime** | 9 adapters, one corpus | Per-app SDK integration | Per-app SDK integration | Per-runtime config |
|
|
282
|
+
| **Editable timeline** | Sessions can be superseded, retired, aborted | Append-only fact log | Graph edits | Manual file edits |
|
|
283
|
+
| **R@5 baseline** | 97.2% on 14mo corpus | published varies | published varies | n/a |
|
|
284
|
+
| **External deps** | SQLite + Ollama (local) | Postgres or Qdrant | Postgres | none |
|
|
285
|
+
| **Hosted offering** | none — local only | yes | yes | n/a |
|
|
286
|
+
| **Account required** | none | yes (cloud tier) | yes | none |
|
|
287
|
+
| **Telemetry** | none | yes | yes | none |
|
|
288
|
+
| **License** | Apache 2.0 | Apache 2.0 | Apache 2.0 | — |
|
|
127
289
|
|
|
128
|
-
|
|
129
|
-
- **Audience:** you querying your own past work, not an embedded SDK for app developers.
|
|
130
|
-
- **Cross-runtime:** one corpus across Claude Code, Codex, OpenCode, Cursor, Windsurf, Hermes, and more. Competitors target one runtime.
|
|
131
|
-
- **Editable timeline:** sessions can be superseded, retired, aborted. No other tool lets you retrofit memory — a record from 6 months ago can be corrected today.
|
|
132
|
-
- **Local-only:** no hosted offering, no telemetry, no vendor dependency.
|
|
290
|
+
The defining property is the editable timeline. mem0 and Letta append; NLM lets you reach back and mark a session as superseded by a newer one, retire one as no-longer-relevant, or flag one as aborted-mid-flight. The next recall surfaces the corrected version, not the stale one. A claim from 6 months ago can be patched today.
|
|
133
291
|
|
|
134
292
|
---
|
|
135
293
|
|
|
@@ -138,17 +296,17 @@ Old installs have `NLM_HOOK_MODE=shadow` hardcoded in `~/.claude/settings.json`
|
|
|
138
296
|
```sh
|
|
139
297
|
git clone https://github.com/pbmagnet4/nlm-memory-ts
|
|
140
298
|
cd nlm-memory-ts
|
|
141
|
-
npm install
|
|
142
|
-
npm run build
|
|
143
|
-
npm run dev
|
|
144
|
-
npm run ui:dev
|
|
145
|
-
npm test
|
|
299
|
+
npm install
|
|
300
|
+
npm run build # compile dist/ — commit the result, it ships in the repo
|
|
301
|
+
npm run dev # hot-reload daemon
|
|
302
|
+
npm run ui:dev # hot-reload UI at localhost:5173 (proxies /api to :3940)
|
|
303
|
+
npm test # 612 tests across 64 files
|
|
146
304
|
npm run typecheck
|
|
147
305
|
```
|
|
148
306
|
|
|
149
|
-
`
|
|
307
|
+
Architecture: hexagonal. `src/core/` knows about ports (interfaces), not adapters. `src/cli/nlm.ts` is the composition root — the only file that wires concrete implementations (`SqliteSessionStore`, `OllamaClient`, `Hono`, `StdioServerTransport`). Adapters in `src/core/adapters/` are one-way: they parse runtime-specific session formats into NLM's canonical shape; nothing in the runtime sees NLM.
|
|
150
308
|
|
|
151
|
-
|
|
309
|
+
`dist/` is committed so `npm install -g` works without a build step. Rebuild + commit when you change `src/`.
|
|
152
310
|
|
|
153
311
|
---
|
|
154
312
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nlm digest` — compose a daily-activity digest from the running daemon and
|
|
3
|
+
* either print it to stdout (default) or POST it to Telegram.
|
|
4
|
+
*
|
|
5
|
+
* Talks to the daemon over HTTP so it works regardless of where the daemon is
|
|
6
|
+
* actually running. If the daemon is unreachable, the Telegram path posts a
|
|
7
|
+
* "daemon unreachable" alert instead of failing silently — the cron user is
|
|
8
|
+
* specifically watching for this telemetry, so silence is worse than noise.
|
|
9
|
+
*/
|
|
10
|
+
export interface DigestOptions {
|
|
11
|
+
readonly port: number;
|
|
12
|
+
readonly telegram: boolean;
|
|
13
|
+
readonly timeoutMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface DigestResult {
|
|
16
|
+
readonly text: string;
|
|
17
|
+
readonly delivered: "stdout" | "telegram" | "telegram-alert";
|
|
18
|
+
readonly daemonReachable: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function runDigest(opts: DigestOptions): Promise<DigestResult>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nlm digest` — compose a daily-activity digest from the running daemon and
|
|
3
|
+
* either print it to stdout (default) or POST it to Telegram.
|
|
4
|
+
*
|
|
5
|
+
* Talks to the daemon over HTTP so it works regardless of where the daemon is
|
|
6
|
+
* actually running. If the daemon is unreachable, the Telegram path posts a
|
|
7
|
+
* "daemon unreachable" alert instead of failing silently — the cron user is
|
|
8
|
+
* specifically watching for this telemetry, so silence is worse than noise.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { composeDigest, } from "../core/digest/compose.js";
|
|
14
|
+
import { checkHookLiveness } from "../core/digest/hook-liveness.js";
|
|
15
|
+
import { hookAuthHeaders } from "../hook/hook-auth.js";
|
|
16
|
+
export async function runDigest(opts) {
|
|
17
|
+
const base = `http://localhost:${opts.port}`;
|
|
18
|
+
const timeoutMs = opts.timeoutMs ?? 8000;
|
|
19
|
+
let stats = null;
|
|
20
|
+
let recent = [];
|
|
21
|
+
let sessions = [];
|
|
22
|
+
let daemonError = null;
|
|
23
|
+
try {
|
|
24
|
+
const [statsRes, recentRes, datasetRes] = await Promise.all([
|
|
25
|
+
fetchJson(`${base}/api/recall/stats`, timeoutMs),
|
|
26
|
+
fetchJson(`${base}/api/recall/recent?limit=200`, timeoutMs),
|
|
27
|
+
fetchJson(`${base}/api/dataset`, timeoutMs * 2),
|
|
28
|
+
]);
|
|
29
|
+
stats = statsRes;
|
|
30
|
+
recent = (recentRes.entries) ?? [];
|
|
31
|
+
sessions = (datasetRes.sessions) ?? [];
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
daemonError = e instanceof Error ? e.message : String(e);
|
|
35
|
+
}
|
|
36
|
+
if (daemonError !== null || stats === null) {
|
|
37
|
+
const text = `NLM digest — ${todayStr()}\n\n` +
|
|
38
|
+
`Daemon unreachable at ${base}\n${daemonError ?? "no stats returned"}`;
|
|
39
|
+
if (opts.telegram) {
|
|
40
|
+
await postTelegram(text);
|
|
41
|
+
return { text, delivered: "telegram-alert", daemonReachable: false };
|
|
42
|
+
}
|
|
43
|
+
process.stdout.write(`${text}\n`);
|
|
44
|
+
return { text, delivered: "stdout", daemonReachable: false };
|
|
45
|
+
}
|
|
46
|
+
const hookLogPath = process.env["NLM_HOOK_LOG"] ?? join(homedir(), ".nlm", "hook-log.jsonl");
|
|
47
|
+
const hookLogExists = existsSync(hookLogPath);
|
|
48
|
+
const hookLog = hookLogExists ? readHookLog(hookLogPath) : [];
|
|
49
|
+
const hookAlert = checkHookLiveness({
|
|
50
|
+
sessions,
|
|
51
|
+
hookLog,
|
|
52
|
+
hookLogPath,
|
|
53
|
+
hookLogExists,
|
|
54
|
+
});
|
|
55
|
+
const text = composeDigest({
|
|
56
|
+
stats,
|
|
57
|
+
recent,
|
|
58
|
+
port: opts.port,
|
|
59
|
+
hookAlert,
|
|
60
|
+
});
|
|
61
|
+
if (opts.telegram) {
|
|
62
|
+
await postTelegram(text);
|
|
63
|
+
return { text, delivered: "telegram", daemonReachable: true };
|
|
64
|
+
}
|
|
65
|
+
process.stdout.write(`${text}\n`);
|
|
66
|
+
return { text, delivered: "stdout", daemonReachable: true };
|
|
67
|
+
}
|
|
68
|
+
async function fetchJson(url, timeoutMs) {
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetch(url, {
|
|
73
|
+
headers: hookAuthHeaders({ "user-agent": "nlm-digest/1.0" }),
|
|
74
|
+
signal: controller.signal,
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
throw new Error(`${url} → ${res.status} ${res.statusText}`);
|
|
78
|
+
}
|
|
79
|
+
return await res.json();
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function readHookLog(path) {
|
|
86
|
+
const out = [];
|
|
87
|
+
const raw = readFileSync(path, "utf8");
|
|
88
|
+
for (const line of raw.split("\n")) {
|
|
89
|
+
if (!line)
|
|
90
|
+
continue;
|
|
91
|
+
try {
|
|
92
|
+
out.push(JSON.parse(line));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Corrupt line — skip silently. The digest is best-effort observability,
|
|
96
|
+
// not a parser test.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
async function postTelegram(text) {
|
|
102
|
+
const token = process.env["TELEGRAM_BOT_TOKEN"];
|
|
103
|
+
const chatId = process.env["TELEGRAM_CHAT_ID"];
|
|
104
|
+
if (!token || !chatId) {
|
|
105
|
+
throw new Error("TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID must be set for --telegram");
|
|
106
|
+
}
|
|
107
|
+
const url = `https://api.telegram.org/bot${token}/sendMessage`;
|
|
108
|
+
const body = new URLSearchParams({
|
|
109
|
+
chat_id: chatId,
|
|
110
|
+
text,
|
|
111
|
+
disable_web_page_preview: "true",
|
|
112
|
+
});
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
115
|
+
try {
|
|
116
|
+
const res = await fetch(url, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: {
|
|
119
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
120
|
+
"user-agent": "nlm-digest/1.0",
|
|
121
|
+
},
|
|
122
|
+
body,
|
|
123
|
+
signal: controller.signal,
|
|
124
|
+
});
|
|
125
|
+
const payload = (await res.json());
|
|
126
|
+
if (!payload.ok) {
|
|
127
|
+
throw new Error(`telegram api error: ${payload.description ?? "unknown"}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function todayStr() {
|
|
135
|
+
const d = new Date();
|
|
136
|
+
const weekday = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()];
|
|
137
|
+
const y = d.getFullYear();
|
|
138
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
139
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
140
|
+
return `${weekday} ${y}-${m}-${day}`;
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=digest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digest.js","sourceRoot":"","sources":["../../src/cli/digest.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/nlm.d.ts
CHANGED
|
@@ -21,5 +21,6 @@
|
|
|
21
21
|
* nlm connect codex — install Codex marketplace plugin
|
|
22
22
|
* nlm disconnect claude-code — remove MCP block from ~/.mcp.json
|
|
23
23
|
* nlm disconnect codex — remove Codex plugin
|
|
24
|
+
* nlm digest — print a daily-activity digest (or --telegram to post)
|
|
24
25
|
*/
|
|
25
26
|
export {};
|
package/dist/cli/nlm.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* nlm connect codex — install Codex marketplace plugin
|
|
22
22
|
* nlm disconnect claude-code — remove MCP block from ~/.mcp.json
|
|
23
23
|
* nlm disconnect codex — remove Codex plugin
|
|
24
|
+
* nlm digest — print a daily-activity digest (or --telegram to post)
|
|
24
25
|
*/
|
|
25
26
|
import { fileURLToPath } from "node:url";
|
|
26
27
|
import { dirname, resolve, join } from "node:path";
|
|
@@ -63,6 +64,7 @@ import { MemoSweepScheduler } from "../core/hook/memo-sweep.js";
|
|
|
63
64
|
import { isAgentLoaded, isBenignBootoutError } from "./launchctl-helpers.js";
|
|
64
65
|
import { adapterFromSource } from "../core/adapters/from-source.js";
|
|
65
66
|
import { scanUsefulHits } from "../core/recall/useful-scan.js";
|
|
67
|
+
import { runDigest } from "./digest.js";
|
|
66
68
|
const __filename = fileURLToPath(import.meta.url);
|
|
67
69
|
const __dirname = dirname(__filename);
|
|
68
70
|
const MIGRATIONS_DIR = resolve(__dirname, "../../migrations");
|
|
@@ -1015,6 +1017,27 @@ program
|
|
|
1015
1017
|
console.error(`nlm useful-scan: scanned ${result.total} recalls in the last ${opts.days}d — ${rate}` +
|
|
1016
1018
|
(opts.dryRun ? " (dry-run)" : `, ${result.appended} appended`));
|
|
1017
1019
|
});
|
|
1020
|
+
program
|
|
1021
|
+
.command("digest")
|
|
1022
|
+
.description("Compose a daily-activity digest from the running daemon (optionally post to Telegram)")
|
|
1023
|
+
.option("-p, --port <n>", "daemon port", (v) => Number.parseInt(v, 10), Number.parseInt(process.env["NLM_PORT"] ?? "3940", 10))
|
|
1024
|
+
.option("--telegram", "post to Telegram instead of printing to stdout (requires TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID)")
|
|
1025
|
+
.action(async (opts) => {
|
|
1026
|
+
autoloadEnv();
|
|
1027
|
+
try {
|
|
1028
|
+
const result = await runDigest({
|
|
1029
|
+
port: opts.port,
|
|
1030
|
+
telegram: opts.telegram === true,
|
|
1031
|
+
});
|
|
1032
|
+
if (!result.daemonReachable) {
|
|
1033
|
+
process.exit(1);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
catch (e) {
|
|
1037
|
+
console.error("nlm digest:", e instanceof Error ? e.message : e);
|
|
1038
|
+
process.exit(1);
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1018
1041
|
program.parseAsync().catch((e) => {
|
|
1019
1042
|
console.error("nlm: fatal", e);
|
|
1020
1043
|
process.exit(1);
|