sessionmem 1.1.2 → 1.3.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 CHANGED
@@ -1,18 +1,41 @@
1
1
  # sessionmem
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/sessionmem.svg)](https://www.npmjs.com/package/sessionmem) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/catfish-1234/sessionmem/blob/main/LICENSE)
3
+ <p align="center">
4
+ <img src=".github/sessionmem-logo.svg" alt="sessionmem" width="480">
5
+ </p>
4
6
 
5
- **Give your AI coding assistant a memory that lasts beyond one conversation — stored entirely on your own computer.**
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/sessionmem"><img src="https://img.shields.io/npm/v/sessionmem.svg" alt="npm version"></a>
9
+ <a href="https://github.com/catfish-1234/sessionmem/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
10
+ </p>
6
11
 
7
- `sessionmem` is a local-first memory layer for AI coding assistants (Claude Code, Cursor, Codex, Cline, Windsurf, Antigravity, QCoder, and any other tool that speaks [MCP](https://modelcontextprotocol.io)). It watches your coding sessions, writes down the important stuff (decisions, warnings, facts about your project), and quietly reminds the assistant about it the next time you start working — without you having to repeat yourself.
12
+ **85.6% fewer tokens. Every session starts knowing your codebase. Stored entirely on your machine.**
8
13
 
9
- Everything happens on your machine. There is no account to create, no cloud service to trust, and no data that leaves your computer unless you explicitly turn that on.
14
+ ```
15
+ New session. Claude starts fresh.
16
+
17
+ WITHOUT sessionmem:
18
+ You explain the stack. The JWT bug from last week.
19
+ The Stripe migration that's halfway done. The billing code to stay away from.
20
+ Same questions. Different day.
21
+
22
+ WITH sessionmem:
23
+ [warning] JWT blacklist must be checked before issuing new tokens. Fixed PR #47.
24
+ [decision] Stripe migration ~50% done. Do NOT use /lib/billing-v1.
25
+ [fact] Stack: TypeScript, Next.js, Postgres.
26
+
27
+ Claude: "Looks like you're mid-Stripe migration. Where do you want to pick up?"
28
+ ```
29
+
30
+ `sessionmem` is an MCP server that watches your coding sessions and stores what actually mattered -- decisions, warnings, things that would bite you if Claude forgot them. At the start of each new session it injects the relevant bits automatically. Works with Claude Code, Cursor, Cline, Codex, Windsurf, and anything that speaks [MCP](https://modelcontextprotocol.io).
31
+
32
+ Everything stays on your machine. No account, no cloud, no data leaving your computer unless you explicitly turn that on.
10
33
 
11
34
  ---
12
35
 
13
36
  ## Quickstart
14
37
 
15
- No programming experience needed for these steps — just a terminal (Command Prompt, Terminal, or PowerShell) and [Node.js](https://nodejs.org) installed.
38
+ No programming experience needed. You just need a terminal (Command Prompt, Terminal, or PowerShell) and [Node.js](https://nodejs.org) installed.
16
39
 
17
40
  ### 1. Install sessionmem
18
41
 
@@ -30,8 +53,8 @@ sessionmem install
30
53
 
31
54
  This does three things:
32
55
  - Tells your AI tool's MCP host about `sessionmem` so it can be launched automatically.
33
- - Creates a config file at `~/.sessionmem/config.json` with safe, privacy-respecting defaults — **only if one doesn't already exist.**
34
- - Injects instructions into `~/.claude/CLAUDE.md` so Claude Code knows about sessionmem's tools and uses them proactively (idempotent — safe to re-run).
56
+ - Creates a config file at `~/.sessionmem/config.json` with safe, privacy-respecting defaults, but only if one doesn't already exist.
57
+ - Injects instructions into `~/.claude/CLAUDE.md` so Claude Code knows about sessionmem's tools and uses them proactively (idempotent, so safe to re-run).
35
58
 
36
59
  ### 3. Start using your AI tool as normal
37
60
 
@@ -39,7 +62,7 @@ This does three things:
39
62
  sessionmem run
40
63
  ```
41
64
 
42
- (Most of the time you won't run this yourself — your AI tool's host starts it for you automatically once it's registered.)
65
+ (Most of the time you won't run this yourself. Your AI tool's host starts it automatically once it's registered.)
43
66
 
44
67
  That's it. From here:
45
68
  - `sessionmem` watches your sessions in the background.
@@ -85,7 +108,7 @@ This happens because most AI assistants only "know" what's inside the current co
85
108
  `sessionmem` fixes this by sitting quietly between your assistant and your project:
86
109
 
87
110
  1. While you work, it **captures** what happens in the session.
88
- 2. When the session ends, it **summarizes** the important parts — decisions made, warnings, useful facts — into short, durable notes.
111
+ 2. When the session ends, it **summarizes** the important parts (decisions made, warnings, useful facts) into short, durable notes.
89
112
  3. The next time you start a session, it **reminds** the assistant of the most relevant notes, automatically, in a small amount of text.
90
113
 
91
114
  You don't run any of these steps yourself. Once installed, it just works in the background.
@@ -99,21 +122,21 @@ There are other "memory for Claude" projects out there (for example, tools like
99
122
  | | **sessionmem** | Typical cloud/Claude-only memory tools |
100
123
  |---|---|---|
101
124
  | **Where is data stored?** | A single SQLite file on your computer (`~/.sessionmem/memories.db`) | Often a hosted service, a cloud vector database, or a separate server process you have to run |
102
- | **Account / sign-up required?** | No — never | Sometimes |
125
+ | **Account / sign-up required?** | No, never | Sometimes |
103
126
  | **Which AI tools does it work with?** | Claude Code, Cursor, Codex, Cline, Windsurf, Antigravity, QCoder, and any other MCP-compatible host | Usually just one specific tool (commonly Claude Code only) |
104
- | **Secret redaction** | Built in and on by default — API keys, tokens, passwords, private keys, etc. are scrubbed before anything is saved | Often not handled, or left to the user |
127
+ | **Secret redaction** | Built in, on by default. API keys, tokens, passwords, and private keys are scrubbed before anything is saved. | Often not handled, or left to the user |
105
128
  | **Token budget control** | Injected memories are trimmed to a small, fixed token budget so they don't bloat every conversation (see benchmarks below) | Varies, often unbounded |
106
129
  | **Old/stale memory cleanup** | Built-in retention policy automatically prunes old memories (configurable, on by default) | Often grows forever ("memory rot") |
107
- | **Team sharing** | Optional, via a shared folder you already control (network drive, synced directory) — no server to host | Usually requires a shared hosted backend |
108
- | **Offline-capable** | Yes, fully — works with no network connection by default | Usually requires network access to the memory service |
130
+ | **Team sharing** | Optional, via a shared folder you already control (network drive, synced directory). No server needed. | Usually requires a shared hosted backend |
131
+ | **Offline-capable** | Yes, fully. Works with no network connection by default. | Usually requires network access to the memory service |
109
132
 
110
- In short: `sessionmem` aims to be the **boring, local, "just a SQLite file" option** — easy to inspect, easy to back up, easy to delete, and not tied to any one vendor's AI tool.
133
+ In short: `sessionmem` is the **boring, local, "just a SQLite file" option**: easy to inspect, back up, and delete, with no lock-in to any one vendor's AI tool.
111
134
 
112
135
  ---
113
136
 
114
137
  ## Benchmark results
115
138
 
116
- These numbers come from `npm run benchmark` (`scripts/benchmark.mjs`), which runs the **real** production retrieval and injection code over a fixed, synthetic set of test data — no network calls, fully reproducible. See [`docs/benchmark.md`](docs/benchmark.md) for the full report and how to regenerate it.
139
+ These numbers come from `npm run benchmark` (`scripts/benchmark.mjs`), which runs the real production retrieval and injection code over a fixed, synthetic set of test data with no network calls. The results are fully reproducible. See [`docs/benchmark.md`](docs/benchmark.md) for the full report and how to regenerate it.
117
140
 
118
141
  ### Token savings
119
142
 
@@ -124,11 +147,11 @@ These numbers come from `npm run benchmark` (`scripts/benchmark.mjs`), which run
124
147
  | Full session history (baseline) | 1,587 |
125
148
  | What sessionmem injects at the start of your next session | 228 |
126
149
 
127
- In practice: instead of re-reading (or re-explaining) ~1,600 tokens of past context every session, the assistant gets a ~230-token summary of just the things that matter — decisions, warnings, and key facts.
150
+ In practice: instead of re-reading (or re-explaining) about 1,600 tokens of past context every session, the assistant gets a 230-token summary of just the things that matter: decisions, warnings, and key facts.
128
151
 
129
152
  ### Retrieval accuracy
130
153
 
131
- > **100% hit-rate** — every one of the 10 test queries successfully retrieved the memory it was supposed to.
154
+ > **100% hit-rate**: every one of the 10 test queries successfully retrieved the memory it was supposed to.
132
155
 
133
156
  | Metric | Result |
134
157
  |---|---|
@@ -136,9 +159,9 @@ In practice: instead of re-reading (or re-explaining) ~1,600 tokens of past cont
136
159
  | Recall | 100.0% |
137
160
  | Precision | 33.3% |
138
161
 
139
- Precision of 33.3% is expected here: each query retrieves the top 3 candidate memories, and only one of those three is the "expected" match for a given test query — the other two are still relevant context for the agent, just not the one being scored. The important number is recall/hit-rate: **the right memory is never missed.**
162
+ Precision of 33.3% is expected here: each query retrieves the top 3 candidate memories, and only one of those three is the "expected" match for a given test query. The other two are still relevant context for the agent, just not the one being scored. The important number is recall/hit-rate: **the right memory is never missed.**
140
163
 
141
- These benchmarks are deterministic and reproducible — run them yourself:
164
+ These benchmarks are deterministic and reproducible. Run them yourself:
142
165
 
143
166
  ```bash
144
167
  npm run build # benchmark imports the compiled code from dist/
@@ -175,7 +198,7 @@ npm run benchmark # regenerates docs/benchmark.md
175
198
  └──────────────────────────────────────────────┘
176
199
  ```
177
200
 
178
- - **Adapters** are small pieces that know how to talk to each specific AI tool. This is why sessionmem can support many tools — adding a new one doesn't change how memory itself works.
201
+ - **Adapters** are small pieces that know how to talk to each specific AI tool. This is why sessionmem can support many tools: adding a new one doesn't change how memory itself works.
179
202
  - **The core engine** is the same no matter which tool you use. It decides what's worth remembering, how relevant it is later, and how much of it fits in a small "reminder" at the start of your next session.
180
203
  - **The database** is just a file. You can back it up, move it, inspect it, or delete it like any other file on your computer.
181
204
 
@@ -234,24 +257,24 @@ Full details: [`docs/privacy-and-retention.md`](docs/privacy-and-retention.md).
234
257
 
235
258
  ## Memory rot: keeping memory accurate over time
236
259
 
237
- "Memory rot" is what happens when a memory system keeps accumulating notes forever — eventually it's full of outdated decisions, duplicate facts, and noise, and the assistant starts surfacing **wrong or stale** information instead of helpful information.
260
+ "Memory rot" is what happens when a memory system keeps accumulating notes forever. Eventually it fills up with outdated decisions, duplicate facts, and noise, and the assistant starts surfacing stale information instead of helpful information.
238
261
 
239
262
  `sessionmem` is designed to avoid this in a few ways:
240
263
 
241
- 1. **Retention pruning** — memories older than a configurable window (default **90 days**) are automatically eligible for cleanup. This runs as a light check at the end of every session, and can also be run manually:
264
+ 1. **Retention pruning**: memories older than a configurable window (default 90 days) are automatically eligible for cleanup. This runs as a light check at the end of every session, and can also be run manually:
242
265
 
243
266
  ```bash
244
- sessionmem retention prune # dry run — shows what *would* be deleted
267
+ sessionmem retention prune # dry run - shows what *would* be deleted
245
268
  sessionmem retention prune --force # actually deletes
246
269
  ```
247
270
 
248
- 2. **Importance-weighted ranking** — when memories are retrieved, they're ranked by a blend of *semantic relevance*, *recency*, and *importance*. Old, low-importance notes naturally sink to the bottom and stop being surfaced even before they're pruned.
271
+ 2. **Importance-weighted ranking**: when memories are retrieved, they're ranked by a blend of semantic relevance, recency, and importance. Old, low-importance notes naturally sink to the bottom and stop being surfaced even before they're pruned.
249
272
 
250
- 3. **Token-budgeted injection** — only the top-ranked, most relevant memories are injected (trimmed to a small token budget, see [benchmarks](#benchmark-results)), so even a large memory store doesn't translate into bloated, noisy context.
273
+ 3. **Token-budgeted injection**: only the top-ranked, most relevant memories are injected (trimmed to a small token budget, see [benchmarks](#benchmark-results)), so even a large memory store doesn't produce bloated, noisy context.
251
274
 
252
- 4. **Conflict resolution in team mode** — when memories are merged from teammates, the system uses last-write-wins by id (so stale duplicates don't pile up) while preserving the higher importance score (so a critical warning doesn't get silently downgraded).
275
+ 4. **Conflict resolution in team mode**: when memories are merged from teammates, the system uses last-write-wins by id (so stale duplicates don't pile up) while preserving the higher importance score (so a critical warning doesn't get silently downgraded).
253
276
 
254
- The retrieval benchmark above (100% hit-rate / 100% recall) demonstrates that even with the ranking and trimming in place, the *right* memory still surfaces — accuracy isn't traded away for compactness.
277
+ The retrieval benchmark above (100% hit-rate / 100% recall) demonstrates that even with the ranking and trimming in place, the right memory still surfaces. Accuracy is not traded away for compactness.
255
278
 
256
279
  You're always in control: export everything first if you want a permanent record before pruning:
257
280
 
@@ -263,15 +286,15 @@ sessionmem export
263
286
 
264
287
  ## Team mode (optional)
265
288
 
266
- Want your whole team's AI assistants to share decisions and warnings? Point `sessionmem` at a shared folder (a network drive, a synced directory — anything everyone can read and write):
289
+ Want your whole team's AI assistants to share decisions and warnings? Point `sessionmem` at a shared folder (a network drive, a synced directory, or any location everyone can read and write):
267
290
 
268
291
  ```bash
269
292
  sessionmem team enable <shared-path>
270
293
  sessionmem sync
271
294
  ```
272
295
 
273
- - **Off by default** — nothing is shared until you turn it on.
274
- - No server to host — it's just files in a folder you already control.
296
+ - **Off by default**: nothing is shared until you turn it on.
297
+ - No server needed. It's just files in a folder you already control.
275
298
  - Teammates' memories show up with an `author:` prefix so you know where they came from.
276
299
  - Secrets are re-redacted on every pulled memory, so a teammate's snapshot can't reintroduce something your redaction policy would have stripped.
277
300
 
@@ -281,9 +304,9 @@ Full details, including the trust model: [`docs/team-mode.md`](docs/team-mode.md
281
304
 
282
305
  ## Cloud summarization (optional, off by default)
283
306
 
284
- By default, summarization (turning a session into a short memory) happens **entirely locally** — no API calls.
307
+ By default, summarization (turning a session into a short memory) happens entirely locally, with no API calls.
285
308
 
286
- If you explicitly opt in (`allowCloudSummarization=true`) **and** provide an `ANTHROPIC_API_KEY`, summarization can use Claude's API for higher-quality summaries. If that ever fails, it automatically falls back to local summarization — your sessions are never left unsummarized.
309
+ If you explicitly opt in (`allowCloudSummarization=true`) and provide an `ANTHROPIC_API_KEY`, summarization can use Claude's API for higher-quality summaries. If that ever fails, it automatically falls back to local summarization. Your sessions are never left unsummarized.
287
310
 
288
311
  Details: [`docs/cloud-summarization.md`](docs/cloud-summarization.md).
289
312
 
@@ -307,19 +330,28 @@ Details: [`docs/cloud-summarization.md`](docs/cloud-summarization.md).
307
330
 
308
331
  ## Further documentation
309
332
 
310
- - [Architecture](docs/architecture.md) — how the core engine, adapters, CLI, and SQLite storage fit together.
311
- - [Benchmark](docs/benchmark.md) — full token-reduction and retrieval-accuracy report, and how to reproduce it.
312
- - [Privacy and retention](docs/privacy-and-retention.md) — secret redaction, retention pruning, and config.
313
- - [Team mode](docs/team-mode.md) — shared-path team memory.
314
- - [Cloud summarization](docs/cloud-summarization.md) — the opt-in cloud summarization path.
315
- - [Migration](docs/migration.md) — the SQLite migration system and version-upgrade policy.
316
- - [Troubleshooting](docs/troubleshooting.md) — install failures, adapter issues, and `better-sqlite3` native-build problems.
333
+ - [Architecture](docs/architecture.md): how the core engine, adapters, CLI, and SQLite storage fit together.
334
+ - [Benchmark](docs/benchmark.md): full token-reduction and retrieval-accuracy report, and how to reproduce it.
335
+ - [Privacy and retention](docs/privacy-and-retention.md): secret redaction, retention pruning, and config.
336
+ - [Team mode](docs/team-mode.md): shared-path team memory.
337
+ - [Cloud summarization](docs/cloud-summarization.md): the opt-in cloud summarization path.
338
+ - [Migration](docs/migration.md): the SQLite migration system and version-upgrade policy.
339
+ - [Troubleshooting](docs/troubleshooting.md): install failures, adapter issues, and `better-sqlite3` native-build problems.
317
340
 
318
341
  ---
319
342
 
320
343
  ## Troubleshooting
321
344
 
322
- Run into trouble installing or running `sessionmem`? Start with [`docs/troubleshooting.md`](docs/troubleshooting.md) — it covers install failures, adapter-specific issues, and native module (`better-sqlite3`) build problems on different platforms.
345
+ Run into trouble installing or running `sessionmem`? Start with [`docs/troubleshooting.md`](docs/troubleshooting.md). It covers install failures, adapter-specific issues, missing session data, and native module (`better-sqlite3`) build problems on different platforms.
346
+
347
+ Two quick checks that resolve most reports:
348
+
349
+ ```bash
350
+ sessionmem install # idempotent — re-registers the MCP server and all three hooks
351
+ sessionmem stats # memories, sessions, and session_events for the current project
352
+ ```
353
+
354
+ If `stats` shows `sessions: 0` after real work, see [“0 sessions” / no session data recorded](docs/troubleshooting.md#0-sessions--no-session-data-recorded). Memories are keyed to the **repository root**, so every directory inside one repo shares a bucket; outside a repo the working directory itself is the key.
323
355
 
324
356
  Quick checks:
325
357
 
@@ -339,10 +371,10 @@ Install sessionmem (`npm i -g sessionmem`), then run `sessionmem install` in you
339
371
  Same install. sessionmem also ships as a Claude Code plugin (the `.claude-plugin` file in the repo), so it works with Claude Code's native plugin system too.
340
372
 
341
373
  **Is there a local MCP memory server that works offline with no API key?**
342
- Yes — sessionmem stores everything in a single SQLite file at `~/.sessionmem/memories.db` and works fully offline by default. Nothing leaves your machine unless you explicitly enable the optional cloud summarization path.
374
+ Yes. sessionmem stores everything in a single SQLite file at `~/.sessionmem/memories.db` and works fully offline by default. Nothing leaves your machine unless you explicitly enable the optional cloud summarization path.
343
375
 
344
376
  **How is sessionmem different from claude-mem?**
345
- sessionmem is not Claude-only. It works with Cursor, Cline, Codex, Windsurf, Antigravity, QCoder, and any other MCP host — not just Claude Code. It also redacts secrets (API keys, tokens, JWTs) by default, prunes stale memory automatically, and ships reproducible benchmarks you can run yourself.
377
+ sessionmem is not Claude-only. It works with Cursor, Cline, Codex, Windsurf, Antigravity, QCoder, and any other MCP host, not just Claude Code. It also redacts secrets (API keys, tokens, JWTs) by default, prunes stale memory automatically, and ships reproducible benchmarks you can run yourself.
346
378
 
347
379
  **Does sessionmem send my code to the cloud?**
348
380
  No. Nothing leaves your machine by default. The optional cloud summarization path is opt-in and off by default.
@@ -19,13 +19,24 @@ hook support), call it once before any task work, or call \`retrieveMemories\`
19
19
  with the current task as the query. Never inject twice if context was already
20
20
  provided.
21
21
 
22
- ### When to store memories (storeMemory)
22
+ ### Session event ingestion (auto-ingest path)
23
+ The \`PostToolUse\` hook installed by sessionmem automatically captures Bash, Edit,
24
+ Write, and MultiEdit tool uses as session events. These are summarized by the
25
+ \`SessionEnd\` hook into a durable memory — no manual action required. To ingest
26
+ additional context (e.g. key decisions made mid-session that weren't captured as
27
+ tool use), call \`ingestSessionEvents\` at task boundaries with a description of
28
+ what was accomplished.
29
+
30
+ ### When to store memories explicitly (storeMemory / batchStoreMemory)
23
31
  - User makes an architectural or design decision
24
32
  - You discover a non-obvious project constraint or convention
25
33
  - A debugging session reveals an important root cause
26
34
  - User states a preference about how they want things done
27
35
  - A warning or pitfall is discovered that future sessions should know about
28
36
 
37
+ Use \`tags\` to categorize memories for better retrieval (e.g. \`tags: ["auth", "security"]\`).
38
+ Use \`expiresAt\` for time-limited context (e.g. a temporary workaround).
39
+
29
40
  ### When to retrieve memories mid-session (retrieveMemories)
30
41
  - Before making architectural decisions (check if prior decisions exist)
31
42
  - When the user references something from a previous session
@@ -37,6 +48,9 @@ informed. Store a concise \`summary\` memory (importance 7) of the key outcomes,
37
48
  any new decisions, facts, or warnings. Use \`batchStoreMemory\` to write several at
38
49
  once. This is what makes context survive across sessions and saves tokens later.
39
50
 
51
+ Note: The SessionEnd hook also auto-summarizes captured session events — you only
52
+ need to call \`batchStoreMemory\` for decisions and facts NOT already captured.
53
+
40
54
  ### Memory kinds
41
55
  - \`decision\` — architectural or design choices (importance: 7-9)
42
56
  - \`fact\` — project constraints, conventions, patterns (importance: 5-7)
@@ -49,6 +63,7 @@ once. This is what makes context survive across sessions and saves tokens later.
49
63
  - \`getMemory\` — fetch a specific memory by ID
50
64
  - \`forgetMemory\` — delete an outdated or incorrect memory
51
65
  - \`batchStoreMemory\` — store multiple memories in one call (use at session end)
66
+ - \`ingestSessionEvents\` — push raw session events for auto-summarization
52
67
  - \`stats\` — check memory count and health
53
68
 
54
69
  ### Guidelines
@@ -56,6 +71,7 @@ once. This is what makes context survive across sessions and saves tokens later.
56
71
  - Don't retrieve memories every single turn — retrieve at task boundaries
57
72
  - Keep memory content concise (1-3 sentences) and self-contained
58
73
  - Use appropriate importance scores (see kinds above)
74
+ - Use tags to group related memories (\`tags: ["topic"]\`)
59
75
  `;
60
76
  export function generateClaudeMdBlock() {
61
77
  return `${SESSIONMEM_BLOCK_START}\n${BLOCK_CONTENT}\n${SESSIONMEM_BLOCK_END}`;
@@ -68,6 +68,7 @@ const TOOL_DEFINITIONS = [
68
68
  limit: retrieveMemoriesRequestSchema.shape.limit.describe("Maximum number of memories to return. Integer 1-100, default 20. Increase for broad topic sweeps; keep at default for focused lookups."),
69
69
  mode: retrieveMemoriesRequestSchema.shape.mode.describe("'auto' for the standard startup context-load path. 'on-demand' for an explicit mid-session retrieval triggered by a specific task or question."),
70
70
  depth: retrieveMemoriesRequestSchema.shape.depth.describe("'default' for standard semantic search. 'deep' for a broader sweep that surfaces less-similar memories — use when the topic is new or unfamiliar."),
71
+ tags: retrieveMemoriesRequestSchema.shape.tags.describe("Optional tag filter. When provided, only return memories that have ALL listed tags."),
71
72
  },
72
73
  },
73
74
  {
@@ -86,6 +87,8 @@ const TOOL_DEFINITIONS = [
86
87
  content: storeMemoryRequestSchema.shape.content.describe("The memory text. Must be self-contained and specific — written so it is useful without surrounding conversation context. Avoid vague phrases like 'the user decided to...'."),
87
88
  importance: storeMemoryRequestSchema.shape.importance.describe("Integer 1-10 indicating criticality (10 = most important). Directly affects ranking in future retrieveMemories calls. Use 8-10 for decisions that must not be forgotten; 3-5 for useful but non-critical facts."),
88
89
  redactionEnabled: storeMemoryRequestSchema.shape.redactionEnabled.describe("If true, PII is stripped from content before storage. Omit to use the project-level redaction setting from config.json."),
90
+ tags: storeMemoryRequestSchema.shape.tags.describe("Optional array of tag strings (max 10, each max 50 chars) for categorizing this memory. Example: ['auth', 'db-schema']. Tags are filterable via retrieveMemories."),
91
+ expiresAt: storeMemoryRequestSchema.shape.expiresAt.describe("Optional UTC ISO timestamp (e.g. '2026-12-31T00:00:00Z') after which this memory is excluded from retrieval. Use for time-limited context like a temporary workaround."),
89
92
  },
90
93
  },
91
94
  {
@@ -1,7 +1,7 @@
1
1
  import { join } from "path";
2
2
  import { homedir } from "os";
3
3
  import { GenericMCPAdapter } from "../generic.js";
4
- import { IDEInstaller, SESSIONMEM_HOOK_COMMAND, SESSIONMEM_SESSION_END_HOOK_COMMAND, } from "../ide/installer.js";
4
+ import { IDEInstaller, SESSIONMEM_HOOK_COMMAND, SESSIONMEM_SESSION_END_HOOK_COMMAND, SESSIONMEM_POST_TOOL_HOOK_COMMAND, SESSIONMEM_POST_TOOL_MATCHER, } from "../ide/installer.js";
5
5
  export class ClaudeCodeAdapter extends GenericMCPAdapter {
6
6
  name = "Claude Code";
7
7
  // Capabilities inherited from GenericMCPAdapter (tools only) so the
@@ -44,7 +44,11 @@ export class ClaudeCodeAdapter extends GenericMCPAdapter {
44
44
  // session ends — the deterministic write-side counterpart to the
45
45
  // SessionStart read-side hook.
46
46
  const endHookOk = await IDEInstaller.injectClaudeHook(this.settingsPath(), SESSIONMEM_SESSION_END_HOOK_COMMAND, "SessionEnd");
47
- return mcpOk && startHookOk && endHookOk;
47
+ // Register a PostToolUse hook so high-signal tool uses (Bash/Edit/Write/
48
+ // MultiEdit) are captured as session events automatically, feeding the
49
+ // SessionEnd auto-summarizer without the agent calling ingestSessionEvents.
50
+ const postToolOk = await IDEInstaller.injectClaudeHook(this.settingsPath(), SESSIONMEM_POST_TOOL_HOOK_COMMAND, "PostToolUse", SESSIONMEM_POST_TOOL_MATCHER);
51
+ return mcpOk && startHookOk && endHookOk && postToolOk;
48
52
  }
49
53
  async uninstall() {
50
54
  const configPath = join(homedir(), ".claude.json");
@@ -53,6 +57,7 @@ export class ClaudeCodeAdapter extends GenericMCPAdapter {
53
57
  // reported as an uninstall failure rather than silently ignored.
54
58
  const startOk = await IDEInstaller.removeClaudeHook(this.settingsPath(), SESSIONMEM_HOOK_COMMAND);
55
59
  const endOk = await IDEInstaller.removeClaudeHook(this.settingsPath(), SESSIONMEM_SESSION_END_HOOK_COMMAND, "SessionEnd");
56
- return mcpOk && startOk && endOk;
60
+ const postToolOk = await IDEInstaller.removeClaudeHook(this.settingsPath(), SESSIONMEM_POST_TOOL_HOOK_COMMAND, "PostToolUse");
61
+ return mcpOk && startOk && endOk && postToolOk;
57
62
  }
58
63
  }
@@ -1,17 +1,38 @@
1
+ // On Windows, `sessionmem` resolves to a `.cmd` shim which requires cmd.exe to
2
+ // execute. Claude Code spawns hook commands without a shell on Windows, so a
3
+ // bare `sessionmem …` invocation fails silently. Route through `cmd /c` —
4
+ // mirroring the MCP server install fix in claudeCode.ts — so the shim is
5
+ // resolved correctly. On non-Windows platforms the bare command works fine.
6
+ const _isWindows = process.platform === "win32";
1
7
  /**
2
8
  * The exact command Claude Code runs at session start. Installed as a
3
9
  * `SessionStart` hook in ~/.claude/settings.json so prior memories are injected
4
10
  * into every session automatically — the deterministic counterpart to the
5
11
  * advisory `startup_inject_memories` tool (which the agent must choose to call).
6
12
  */
7
- export const SESSIONMEM_HOOK_COMMAND = "sessionmem session-start";
13
+ export const SESSIONMEM_HOOK_COMMAND = _isWindows
14
+ ? "cmd /c sessionmem session-start"
15
+ : "sessionmem session-start";
8
16
  /**
9
17
  * The command Claude Code runs when a session ends. Installed as a `SessionEnd`
10
18
  * hook so the session-end pipeline (light retention prune + auto-summarization
11
19
  * of any ingested session events) runs automatically once per session, without
12
20
  * relying on the agent choosing to call a tool.
13
21
  */
14
- export const SESSIONMEM_SESSION_END_HOOK_COMMAND = "sessionmem session-end";
22
+ export const SESSIONMEM_SESSION_END_HOOK_COMMAND = _isWindows
23
+ ? "cmd /c sessionmem session-end"
24
+ : "sessionmem session-end";
25
+ /**
26
+ * The command Claude Code runs after each tool use. Installed as a `PostToolUse`
27
+ * hook so high-signal tool uses (Bash/Edit/Write/MultiEdit) are captured as
28
+ * session events automatically, giving the SessionEnd auto-summarizer material
29
+ * to work with without the agent choosing to call ingestSessionEvents.
30
+ */
31
+ export const SESSIONMEM_POST_TOOL_HOOK_COMMAND = _isWindows
32
+ ? "cmd /c sessionmem ingest-hook"
33
+ : "sessionmem ingest-hook";
34
+ /** Tool matcher for the PostToolUse auto-ingest hook. */
35
+ export const SESSIONMEM_POST_TOOL_MATCHER = "Bash|Edit|Write|MultiEdit";
15
36
  /** Default Claude Code hook event for the legacy 2-arg hook helpers. */
16
37
  const DEFAULT_HOOK_EVENT = "SessionStart";
17
38
  export class IDEInstaller {
@@ -166,9 +187,11 @@ export class IDEInstaller {
166
187
  * every other key (mcpServers, other hook events, user settings). Defaults to
167
188
  * the `SessionStart` event; pass `eventName` for another event (e.g.
168
189
  * `SessionEnd`). Idempotent: re-injecting the same command on the same event
169
- * never produces a duplicate entry.
190
+ * never produces a duplicate entry. When `matcher` is provided (e.g. for
191
+ * `PostToolUse`), the pushed entry carries the matcher so Claude Code scopes
192
+ * the hook to the matching tools.
170
193
  */
171
- static injectClaudeHookBlock(content, command, eventName = DEFAULT_HOOK_EVENT) {
194
+ static injectClaudeHookBlock(content, command, eventName = DEFAULT_HOOK_EVENT, matcher) {
172
195
  const config = content.trim()
173
196
  ? this.parseJsonc(content)
174
197
  : {};
@@ -179,9 +202,13 @@ export class IDEInstaller {
179
202
  ? hooks[eventName]
180
203
  : [];
181
204
  // Drop any pre-existing sessionmem entry so re-install stays idempotent and
182
- // a stale command form is replaced rather than duplicated.
205
+ // a stale command form is replaced rather than duplicated. hookEntryHasCommand
206
+ // only inspects entry.hooks, so it dedupes correctly whether or not the entry
207
+ // carries a matcher.
183
208
  const filtered = eventEntries.filter((entry) => !this.hookEntryHasCommand(entry, command));
184
- filtered.push({ hooks: [{ type: "command", command }] });
209
+ filtered.push(matcher !== undefined
210
+ ? { matcher, hooks: [{ type: "command", command }] }
211
+ : { hooks: [{ type: "command", command }] });
185
212
  hooks[eventName] = filtered;
186
213
  config.hooks = hooks;
187
214
  return JSON.stringify(config, null, 2);
@@ -214,14 +241,14 @@ export class IDEInstaller {
214
241
  }
215
242
  return JSON.stringify(config, null, 2);
216
243
  }
217
- static async injectClaudeHook(filePath, command, eventName = DEFAULT_HOOK_EVENT) {
244
+ static async injectClaudeHook(filePath, command, eventName = DEFAULT_HOOK_EVENT, matcher) {
218
245
  try {
219
246
  const { readFileSync, writeFileSync, existsSync, mkdirSync } = await import("fs");
220
247
  const { dirname } = await import("path");
221
248
  const existing = existsSync(filePath)
222
249
  ? readFileSync(filePath, "utf-8")
223
250
  : "{}";
224
- const updated = this.injectClaudeHookBlock(existing, command, eventName);
251
+ const updated = this.injectClaudeHookBlock(existing, command, eventName, matcher);
225
252
  const dir = dirname(filePath);
226
253
  if (!existsSync(dir)) {
227
254
  mkdirSync(dir, { recursive: true });
@@ -0,0 +1,33 @@
1
+ import { createCliContext } from "../context.js";
2
+ export async function browseCommand(options = {}, ctx) {
3
+ const context = ctx ?? createCliContext();
4
+ const db = context.db;
5
+ if (options.project) {
6
+ // Show memories for a specific project
7
+ const memories = db.prepare("SELECT id, kind, importance, content, created_at FROM memories WHERE project_id = ? ORDER BY updated_at DESC LIMIT 50").all(options.project);
8
+ if (memories.length === 0) {
9
+ process.stdout.write(`No memories found for project: ${options.project}\n`);
10
+ return;
11
+ }
12
+ process.stdout.write(`Memories for project: ${options.project} (${memories.length} shown)\n\n`);
13
+ for (const m of memories) {
14
+ const preview = m.content.slice(0, 120).replace(/\n/g, " ");
15
+ process.stdout.write(`[${m.kind}] imp=${m.importance} id=${m.id.slice(0, 8)}…\n ${preview}${m.content.length > 120 ? "…" : ""}\n\n`);
16
+ }
17
+ return;
18
+ }
19
+ // List all projects
20
+ const projects = db.prepare("SELECT project_id, COUNT(*) as memory_count, MAX(updated_at) as newest FROM memories GROUP BY project_id ORDER BY newest DESC").all();
21
+ if (projects.length === 0) {
22
+ process.stdout.write("No projects with memories yet.\n");
23
+ process.stdout.write(`Current project: ${context.projectId}\n`);
24
+ return;
25
+ }
26
+ process.stdout.write(`Projects with memories (${projects.length} total):\n\n`);
27
+ for (const p of projects) {
28
+ const marker = p.project_id === context.projectId ? " ← current" : "";
29
+ process.stdout.write(`${p.project_id}${marker}\n`);
30
+ process.stdout.write(` memories: ${p.memory_count} last_updated: ${p.newest}\n\n`);
31
+ }
32
+ process.stdout.write(`Use --project <id> to list memories for a specific project.\n`);
33
+ }
@@ -0,0 +1,139 @@
1
+ import { createCliContext } from "../context.js";
2
+ import { listMemoriesByProject } from "../../core/storage/memoryRepo.js";
3
+ const SIMILARITY_THRESHOLD = 0.85;
4
+ const MAX_PREVIEW_LEN = 200;
5
+ function cosineSimilarity(a, b) {
6
+ if (a.length !== b.length || a.length === 0)
7
+ return 0;
8
+ let dot = 0, normA = 0, normB = 0;
9
+ for (let i = 0; i < a.length; i++) {
10
+ dot += a[i] * b[i];
11
+ normA += a[i] * a[i];
12
+ normB += b[i] * b[i];
13
+ }
14
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
15
+ return denom === 0 ? 0 : dot / denom;
16
+ }
17
+ /**
18
+ * Parse and validate the `--threshold` flag. An unparseable or out-of-range
19
+ * value used to flow through as NaN, and every `sim >= NaN` comparison is
20
+ * false — so `--threshold banana` reported "no duplicates found" rather than
21
+ * telling the user their input was rejected.
22
+ */
23
+ function resolveThreshold(raw) {
24
+ if (raw === undefined)
25
+ return SIMILARITY_THRESHOLD;
26
+ const parsed = Number.parseFloat(raw);
27
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 1) {
28
+ console.error(`Invalid --threshold "${raw}": expected a number greater than 0 and at most 1.`);
29
+ process.exit(1);
30
+ }
31
+ return parsed;
32
+ }
33
+ /** Parse a stored embedding, returning null when it is absent or malformed. */
34
+ function parseVector(embedding) {
35
+ if (!embedding)
36
+ return null;
37
+ try {
38
+ const parsed = JSON.parse(embedding);
39
+ if (!Array.isArray(parsed) || !parsed.every((n) => Number.isFinite(n))) {
40
+ return null;
41
+ }
42
+ return parsed;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ export async function dedupeCommand(options = {}, ctx) {
49
+ const context = ctx ?? createCliContext();
50
+ const threshold = resolveThreshold(options.threshold);
51
+ const dryRun = options.dry !== false;
52
+ // Parse each embedding ONCE. The inner loop used to re-parse the same
53
+ // candidate's JSON for every outer element — O(n²) JSON.parse calls over a
54
+ // list that only has n distinct vectors.
55
+ const candidates = [];
56
+ for (const memory of listMemoriesByProject(context.db, context.projectId)) {
57
+ const vector = parseVector(memory.embedding);
58
+ if (!vector || !memory.embedding_dim)
59
+ continue;
60
+ candidates.push({
61
+ id: memory.id,
62
+ content: memory.content,
63
+ importance: memory.importance,
64
+ vector,
65
+ });
66
+ }
67
+ if (candidates.length < 2) {
68
+ process.stdout.write("Not enough memories with embeddings to compare.\n");
69
+ return;
70
+ }
71
+ // Highest importance first, id as the tie-break. Pairs are emitted in this
72
+ // order and the first element of a pair is the one kept, so the survivor of
73
+ // any duplicate cluster is deterministic.
74
+ //
75
+ // Without this the order came from `ORDER BY updated_at DESC`, which is not a
76
+ // total order — memories written in the same clock tick tie, and the winner
77
+ // varied by platform. The same three memories could collapse to a different
78
+ // survivor on Windows than on Linux, and `--apply` is a hard delete.
79
+ candidates.sort((left, right) => right.importance - left.importance || left.id.localeCompare(right.id));
80
+ const pairs = [];
81
+ for (let i = 0; i < candidates.length; i++) {
82
+ const a = candidates[i];
83
+ for (let j = i + 1; j < candidates.length; j++) {
84
+ const b = candidates[j];
85
+ const sim = cosineSimilarity(a.vector, b.vector);
86
+ if (sim >= threshold) {
87
+ pairs.push({
88
+ idA: a.id,
89
+ idB: b.id,
90
+ similarity: sim,
91
+ contentA: a.content.slice(0, MAX_PREVIEW_LEN),
92
+ contentB: b.content.slice(0, MAX_PREVIEW_LEN),
93
+ });
94
+ }
95
+ }
96
+ }
97
+ if (pairs.length === 0) {
98
+ process.stdout.write(`No near-duplicate memories found (threshold: ${threshold}).\n`);
99
+ return;
100
+ }
101
+ const byId = new Map(candidates.map((c) => [c.id, c]));
102
+ const deleted = new Set();
103
+ process.stdout.write(`Found ${pairs.length} near-duplicate pair(s) (threshold: ${threshold}):\n\n`);
104
+ for (const pair of pairs) {
105
+ // Skip a pair whose partner is already gone. Similarity is not transitive:
106
+ // in a chain A~B, B~C with A≁C, deleting B for the (A,B) pair left the
107
+ // (B,C) pair still to process, and that pass could delete C — a memory
108
+ // whose only duplicate no longer existed. Nothing kept C from being
109
+ // dropped on the strength of an already-deleted row.
110
+ if (deleted.has(pair.idA) || deleted.has(pair.idB))
111
+ continue;
112
+ process.stdout.write(`Similarity: ${(pair.similarity * 100).toFixed(1)}%\n`);
113
+ process.stdout.write(` A [${pair.idA}]: ${pair.contentA}${pair.contentA.length >= MAX_PREVIEW_LEN ? "…" : ""}\n`);
114
+ process.stdout.write(` B [${pair.idB}]: ${pair.contentB}${pair.contentB.length >= MAX_PREVIEW_LEN ? "…" : ""}\n`);
115
+ if (!dryRun) {
116
+ const memA = byId.get(pair.idA);
117
+ const memB = byId.get(pair.idB);
118
+ if (!memA || !memB)
119
+ continue;
120
+ const keepId = memA.importance >= memB.importance ? pair.idA : pair.idB;
121
+ const dropId = keepId === pair.idA ? pair.idB : pair.idA;
122
+ const result = await context.service.call("forgetMemory", {
123
+ projectId: context.projectId,
124
+ memoryId: dropId,
125
+ });
126
+ if (result.ok) {
127
+ deleted.add(dropId);
128
+ process.stdout.write(` → Deleted ${dropId} (kept ${keepId})\n`);
129
+ }
130
+ else {
131
+ process.stdout.write(` → Failed to delete ${dropId}: ${result.error.message}\n`);
132
+ }
133
+ }
134
+ process.stdout.write("\n");
135
+ }
136
+ if (dryRun) {
137
+ process.stdout.write(`Run with --apply to delete the lower-importance duplicate in each pair.\n`);
138
+ }
139
+ }