claude-memory-hub 0.8.0 → 0.8.2
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/CHANGELOG.md +53 -0
- package/README.md +150 -138
- package/dist/hooks/post-compact.js +1 -1
- package/dist/hooks/post-tool-use.js +139 -4
- package/dist/hooks/pre-compact.js +1 -1
- package/dist/hooks/session-end.js +249 -1
- package/dist/hooks/user-prompt-submit.js +1 -1
- package/dist/index.js +22 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,59 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [0.8.2] - 2026-04-02
|
|
9
|
+
|
|
10
|
+
Increased context injection limits for richer cross-session memory.
|
|
11
|
+
|
|
12
|
+
### Context Injection Limits
|
|
13
|
+
|
|
14
|
+
- **UserPromptSubmit cap doubled** — `MAX_CHARS` increased from 4,500 (~1,125 tokens) to 8,000 (~2,000 tokens). Session-start context injection now carries significantly more past knowledge
|
|
15
|
+
- **Proactive retrieval cap doubled** — `MAX_INJECTION_CHARS` increased from 1,500 (~375 tokens) to 3,000 (~750 tokens). Mid-session topic-shift injections now include fuller context from L3
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## [0.8.1] - 2026-04-02
|
|
20
|
+
|
|
21
|
+
Token-budget-aware MCP tools + proactive mid-session memory retrieval.
|
|
22
|
+
|
|
23
|
+
### Token Budget Management
|
|
24
|
+
|
|
25
|
+
- **`max_tokens` parameter** — added to `memory_recall`, `memory_search`, `memory_fetch` MCP tools. When set, output is truncated to fit within the specified token budget (~4 chars/token). Helps Claude manage context window when many tools compete for space
|
|
26
|
+
- **`truncateToTokenBudget()` utility** — shared truncation function with `[...truncated to fit ~N token budget]` suffix
|
|
27
|
+
|
|
28
|
+
### Proactive Memory Retrieval
|
|
29
|
+
|
|
30
|
+
- **Topic-shift detection** — PostToolUse hook now monitors file activity and detects when conversation drifts to a new domain (e.g., auth → payment → migration). Detection uses directory clustering + keyword matching across recent files
|
|
31
|
+
- **Mid-session context injection** — when topic shift detected, hook searches L3 for relevant past context and returns `additionalContext` via stdout JSON. Claude Code injects this into the conversation automatically
|
|
32
|
+
- **Trigger conditions:** every 15 tool calls OR on Bash errors after warmup (5+ calls)
|
|
33
|
+
- **State tracking** — per-session state at `~/.claude-memory-hub/proactive/<session_id>.json`, cleaned up on session end
|
|
34
|
+
- **Injection cap:** ~375 tokens (1500 chars) per injection, deduplicated by topic
|
|
35
|
+
|
|
36
|
+
### Session End Improvements
|
|
37
|
+
|
|
38
|
+
- **Batch queue flush on session end** — `tryFlush()` called during Stop hook to prevent data loss from unflushed batch events
|
|
39
|
+
- **Proactive state cleanup** — per-session state files removed on session end
|
|
40
|
+
|
|
41
|
+
### Research Findings (documented, no code changes needed)
|
|
42
|
+
|
|
43
|
+
Based on deep Claude Code source analysis:
|
|
44
|
+
- **Resource filtering:** Claude Code already defers MCP tools automatically via `isDeferredTool()`. Skill listings have budget system (`SKILL_BUDGET_CONTEXT_PERCENT=1%`). No external filtering needed
|
|
45
|
+
- **Multi-agent sharing:** Subagents inherit parent MCP servers via `initializeAgentMcpServers()`. Memory sharing via `memory_recall` works out-of-box — zero implementation needed
|
|
46
|
+
- **Permission-aware:** PostToolUse hook only fires for approved tools. Denied tools fire separate `PermissionDenied` hook. memory-hub is already permission-aware by design
|
|
47
|
+
- **IDE context:** Available as attachments in conversation (ide_selection, ide_opened_file) but not in hook inputs directly. Entity extraction captures file activity indirectly
|
|
48
|
+
|
|
49
|
+
### Modified Files
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
src/mcp/tool-definitions.ts — max_tokens param on 3 tools
|
|
53
|
+
src/mcp/tool-handlers.ts — truncateToTokenBudget() utility
|
|
54
|
+
src/retrieval/proactive-retrieval.ts — NEW: topic detection + injection
|
|
55
|
+
src/hooks-entry/post-tool-use.ts — proactive retrieval integration
|
|
56
|
+
src/hooks-entry/session-end.ts — batch flush + proactive cleanup
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
8
61
|
## [0.8.0] - 2026-04-02
|
|
9
62
|
|
|
10
63
|
Major release: test infrastructure, architectural fixes, hook performance, data portability.
|
package/README.md
CHANGED
|
@@ -17,9 +17,34 @@ Zero API key. Zero Python. Zero config. One install command.
|
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
+
## Why memory-hub?
|
|
21
|
+
|
|
22
|
+
**Claude Code forgets everything.** Every session starts from zero. Auto-compact destroys 90% of your context. You lose files, decisions, errors — hours of work, gone.
|
|
23
|
+
|
|
24
|
+
**claude-memory-hub fixes this.** One install command. No API key. No Python. No Docker.
|
|
25
|
+
|
|
26
|
+
What makes it different? **The Compact Interceptor** — something no other memory tool has. When Claude Code auto-compacts at 200K tokens, memory-hub *tells the compact engine what matters*. PreCompact hook injects priority instructions. PostCompact hook saves the full summary. Result: 90% context salvage instead of vaporization.
|
|
27
|
+
|
|
28
|
+
But it doesn't stop there:
|
|
29
|
+
- **Cross-session memory** — past work auto-injected when you start a new session
|
|
30
|
+
- **3-engine hybrid search** — FTS5 + TF-IDF + semantic embeddings (384-dim, offline)
|
|
31
|
+
- **Proactive retrieval** — detects topic shifts mid-session, injects relevant context automatically
|
|
32
|
+
- **91 unit tests**, batch queue (75ms→3ms), JSONL export/import, browser UI
|
|
33
|
+
- **Multi-agent ready** — subagents share memory for free via MCP
|
|
34
|
+
|
|
35
|
+
Built for developers who use Claude Code daily and are tired of repeating themselves.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bunx claude-memory-hub install
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That's it. Your Claude now remembers.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
20
45
|
## The Problem
|
|
21
46
|
|
|
22
|
-
Claude Code forgets everything between sessions. Within long sessions, auto-compact destroys 90% of context.
|
|
47
|
+
Claude Code forgets everything between sessions. Within long sessions, auto-compact destroys 90% of context. Search is keyword-only with no ranking.
|
|
23
48
|
|
|
24
49
|
```
|
|
25
50
|
Session 1: You spend 2 hours building auth system
|
|
@@ -29,37 +54,31 @@ Long session: Claude auto-compacts at 200K tokens
|
|
|
29
54
|
→ 180K tokens of context vaporized
|
|
30
55
|
→ Claude loses track of files, decisions, errors
|
|
31
56
|
|
|
32
|
-
Every session: ALL skills + agents + rules loaded
|
|
33
|
-
→ 23-51K tokens consumed before you type anything
|
|
34
|
-
→ No external tool can prevent this (Claude Code limitation)
|
|
35
|
-
→ But you CAN identify and remove unused resources
|
|
36
|
-
|
|
37
57
|
Search: Keyword-only, no semantic ranking
|
|
38
58
|
→ Irrelevant results, wasted tokens on full records
|
|
39
59
|
```
|
|
40
60
|
|
|
41
|
-
**Four problems. memory-hub solves three directly and provides analysis for the fourth.**
|
|
42
|
-
|
|
43
61
|
| Problem | Claude Code built-in | claude-mem | memory-hub |
|
|
44
62
|
|---------|:-------------------:|:----------:|:----------:|
|
|
45
63
|
| Cross-session memory | -- | Yes | **Yes** |
|
|
46
64
|
| Influence what compact preserves | -- | -- | **Yes** |
|
|
47
|
-
| Save compact output | -- | -- | **Yes** |
|
|
48
|
-
| Token overhead analysis | -- | -- | **Yes** |
|
|
49
|
-
| Semantic search (embeddings) | -- | Chroma (external) | **Yes (offline)** |
|
|
65
|
+
| Save compact output to L3 | -- | -- | **Yes** |
|
|
50
66
|
| Hybrid search (FTS5 + TF-IDF + semantic) | -- | Partial | **Yes** |
|
|
51
67
|
| 3-layer progressive search | -- | Yes | **Yes** |
|
|
52
68
|
| Resource overhead analysis | -- | -- | **Yes** |
|
|
53
69
|
| CLAUDE.md rule tracking | -- | -- | **Yes** |
|
|
54
|
-
|
|
|
70
|
+
| Observation capture (14 patterns) | -- | Yes | **Yes** |
|
|
55
71
|
| LLM summarization (3-tier) | -- | Yes (API) | **Yes (free)** |
|
|
72
|
+
| Token-budget-aware tools (`max_tokens`) | -- | -- | **Yes** |
|
|
73
|
+
| Proactive mid-session retrieval | -- | -- | **Yes** |
|
|
74
|
+
| Multi-agent memory sharing | -- | -- | **Yes (free)** |
|
|
75
|
+
| Permission-aware (approved only) | -- | -- | **Yes** |
|
|
76
|
+
| Data export/import (JSONL) | -- | -- | **Yes** |
|
|
77
|
+
| Hook batching (3ms vs 75ms) | -- | -- | **Yes** |
|
|
56
78
|
| Browser UI | -- | Yes | **Yes** |
|
|
57
|
-
| Health monitoring | -- | -- | **Yes** |
|
|
58
|
-
|
|
|
59
|
-
| No API key
|
|
60
|
-
| No Python/Chroma needed | N/A | -- | **Yes** |
|
|
61
|
-
| No XML format required | N/A | -- | **Yes** |
|
|
62
|
-
| No HTTP server to manage | N/A | -- | **Yes** |
|
|
79
|
+
| Health monitoring + auto-cleanup | -- | -- | **Yes** |
|
|
80
|
+
| Unit tests (91 tests) | N/A | -- | **Yes** |
|
|
81
|
+
| No API key / Python / Chroma | N/A | Partial | **Yes** |
|
|
63
82
|
|
|
64
83
|
---
|
|
65
84
|
|
|
@@ -75,6 +94,8 @@ Claude makes a decision → memory-hub records: decision text + importance score
|
|
|
75
94
|
```
|
|
76
95
|
|
|
77
96
|
No XML. No special format. Extracted directly from hook JSON metadata.
|
|
97
|
+
PostToolUse events are batched via write-through queue (~3ms per event vs ~75ms direct).
|
|
98
|
+
Mid-session topic shifts auto-inject relevant past context (proactive retrieval).
|
|
78
99
|
|
|
79
100
|
### Layer 2 — Compact Interceptor (the key innovation)
|
|
80
101
|
|
|
@@ -99,41 +120,21 @@ No XML. No special format. Extracted directly from hook JSON metadata.
|
|
|
99
120
|
zero information loss
|
|
100
121
|
```
|
|
101
122
|
|
|
102
|
-
**
|
|
123
|
+
**No other memory tool does this.** memory-hub is the only system that **tells the compact what matters**.
|
|
103
124
|
|
|
104
125
|
### Layer 3 — Cross-Session Memory
|
|
105
126
|
|
|
106
127
|
```
|
|
107
|
-
Session N ends →
|
|
108
|
-
|
|
128
|
+
Session N ends → 3-tier summarization: PostCompact > CLI claude > rule-based
|
|
129
|
+
→ Summary saved to SQLite L3 with FTS5 indexing
|
|
109
130
|
|
|
110
131
|
Session N+1 → UserPromptSubmit hook fires
|
|
111
|
-
→ FTS5 + TF-IDF
|
|
112
|
-
→
|
|
132
|
+
→ FTS5 + TF-IDF + semantic search: match user prompt
|
|
133
|
+
→ Inject relevant past context automatically
|
|
113
134
|
→ Claude starts with history, not from zero
|
|
114
135
|
```
|
|
115
136
|
|
|
116
|
-
### Layer 4 —
|
|
117
|
-
|
|
118
|
-
```
|
|
119
|
-
ResourceRegistry scans your setup:
|
|
120
|
-
58 skills, 36 agents, 65 commands, 10 workflows, CLAUDE.md chain
|
|
121
|
-
|
|
122
|
-
ResourceTracker records actual usage per session:
|
|
123
|
-
"skill:mobile-development used 4/5 recent sessions"
|
|
124
|
-
"agent:veo3-prompt-expert used 0/5 recent sessions"
|
|
125
|
-
|
|
126
|
-
OverheadReport identifies waste:
|
|
127
|
-
"42/58 skills never used → ~1500 listing tokens overhead"
|
|
128
|
-
"CLAUDE.md chain is 8200 tokens → consider consolidating"
|
|
129
|
-
|
|
130
|
-
UserPromptSubmit injects priority hints:
|
|
131
|
-
"Frequently-used: skill:debugging, agent:planner, agent:tester"
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
> **Transparency note:** Claude Code loads ALL resources into its system prompt — no external tool can prevent this. memory-hub provides **analysis and prioritization**, not filtering. To actually reduce token overhead, remove or relocate unused skills/agents based on the overhead report.
|
|
135
|
-
|
|
136
|
-
### Layer 5 — 3-Layer Progressive Search + Semantic (new in v0.5/v0.6)
|
|
137
|
+
### Layer 4 — 3-Layer Progressive Search
|
|
137
138
|
|
|
138
139
|
```
|
|
139
140
|
Traditional search: query → ALL full records → 5000+ tokens wasted
|
|
@@ -146,34 +147,33 @@ memory-hub search: query → Layer 1 (index) → ~50 tokens/result
|
|
|
146
147
|
Token savings: ~80-90% vs. full context
|
|
147
148
|
```
|
|
148
149
|
|
|
149
|
-
Hybrid ranking: FTS5 BM25 (keyword) + TF-IDF (term frequency) +
|
|
150
|
+
Hybrid ranking: FTS5 BM25 (keyword) + TF-IDF (term frequency) + semantic cosine similarity (384-dim embeddings). "debugging tips" matches "error fixing" even without shared keywords.
|
|
150
151
|
|
|
151
|
-
### Layer
|
|
152
|
+
### Layer 5 — Resource Intelligence
|
|
152
153
|
|
|
153
154
|
```
|
|
154
155
|
ResourceRegistry scans ALL .claude locations:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
~/.claude/agent_mobile/ ios-developer → agent_mobile/ios/AGENT.md
|
|
158
|
-
~/.claude/commands/ 65 commands → relative path naming
|
|
159
|
-
~/.claude/workflows/ 10 workflows
|
|
160
|
-
~/.claude/CLAUDE.md + project CLAUDE.md chain
|
|
156
|
+
skills, agents, commands, workflows, CLAUDE.md chain
|
|
157
|
+
→ 3-level token estimation: listing, full, total
|
|
161
158
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
"CLAUDE.md chain is 3222 tokens"
|
|
159
|
+
ResourceTracker records actual usage per session
|
|
160
|
+
OverheadReport identifies unused resources + token waste
|
|
165
161
|
```
|
|
166
162
|
|
|
167
|
-
|
|
163
|
+
> **Transparency note:** Claude Code loads ALL resources into its system prompt — no external tool can prevent this. memory-hub provides **analysis and prioritization**, not filtering. To reduce token overhead, remove or relocate unused skills/agents based on the overhead report.
|
|
164
|
+
|
|
165
|
+
### Layer 6 — Observation Capture
|
|
168
166
|
|
|
169
167
|
```
|
|
170
168
|
Tool output contains "IMPORTANT: always pool DB connections"
|
|
171
169
|
→ observation entity (importance=4) saved to L2
|
|
172
|
-
→ included in session summary
|
|
173
|
-
→ searchable across sessions
|
|
174
170
|
|
|
175
171
|
User prompt contains "remember that we use TypeScript strict"
|
|
176
172
|
→ observation entity (importance=3) saved to L2
|
|
173
|
+
|
|
174
|
+
14 heuristic patterns: IMPORTANT, CRITICAL, SECURITY, DEPRECATED,
|
|
175
|
+
decision:, discovered, root cause, switched to, TODO:, FIXME:,
|
|
176
|
+
HACK:, performance:, bottleneck, OOM, don't, never, prefer, etc.
|
|
177
177
|
```
|
|
178
178
|
|
|
179
179
|
---
|
|
@@ -181,58 +181,47 @@ User prompt contains "remember that we use TypeScript strict"
|
|
|
181
181
|
## Architecture
|
|
182
182
|
|
|
183
183
|
```
|
|
184
|
-
|
|
185
|
-
│ Claude Code
|
|
186
|
-
│
|
|
187
|
-
│ 5 Lifecycle Hooks
|
|
184
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
185
|
+
│ Claude Code │
|
|
186
|
+
│ │
|
|
187
|
+
│ 5 Lifecycle Hooks │
|
|
188
188
|
│ ┌───────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
189
189
|
│ │ PostToolUse │ │ PreCompact │ │ PostCompact │ │
|
|
190
|
-
│ │
|
|
190
|
+
│ │ batch queue │ │ inject │ │ save summary │ │
|
|
191
191
|
│ └──────┬────────┘ │ priorities │ └──────┬───────┘ │
|
|
192
192
|
│ │ └──────┬───────┘ │ │
|
|
193
193
|
│ ┌──────┴───────┐ │ ┌──────┴───────┐ │
|
|
194
194
|
│ │UserPrompt │ │ │ Stop │ │
|
|
195
195
|
│ │Submit: inject│ │ │ session end │ │
|
|
196
196
|
│ │past context │ │ │ summarize │ │
|
|
197
|
-
│ └──────────────┘ │ └──────────────┘ │
|
|
198
|
-
│ │ │
|
|
199
|
-
│ MCP Server (stdio) │ Health Monitor │
|
|
200
|
-
│ ┌─────────────────────┐ │ ┌────────────────────────┐ │
|
|
201
|
-
│ │ memory_recall │ │ │ sqlite, fts5, disk, │ │
|
|
202
|
-
│ │ memory_entities │ │ │ integrity checks │ │
|
|
203
|
-
│ │ memory_session_notes│ │ └────────────────────────┘ │
|
|
204
|
-
│ │ memory_store │ │ │
|
|
205
|
-
│ │ memory_context_budget│ │ Smart Resource Loader │
|
|
206
|
-
│ │ memory_search ←L1 │ │ ┌────────────────────────┐ │
|
|
207
|
-
│ │ memory_timeline ←L2 │ │ │ track usage → predict │ │
|
|
208
|
-
│ │ memory_fetch ←L3 │ │ │ → budget → recommend │ │
|
|
209
|
-
│ │ memory_health │ │ └────────────────────────┘ │
|
|
210
|
-
│ └─────────────────────┘ │ │
|
|
211
|
-
│ │ Browser UI (:37888) │
|
|
212
|
-
│ │ ┌────────────────────────┐ │
|
|
213
|
-
│ │ │ search, browse, stats │ │
|
|
214
|
-
│ │ └────────────────────────┘ │
|
|
197
|
+
│ └──────────────┘ │ └──────────────┘ │
|
|
215
198
|
│ │ │
|
|
216
|
-
|
|
199
|
+
│ MCP Server (stdio, long-lived) │
|
|
200
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
201
|
+
│ │ memory_recall memory_search (L1 index) │ │
|
|
202
|
+
│ │ memory_entities memory_timeline (L2 context) │ │
|
|
203
|
+
│ │ memory_session_notes memory_fetch (L3 full) │ │
|
|
204
|
+
│ │ memory_store memory_context_budget │ │
|
|
205
|
+
│ │ memory_health │ │
|
|
206
|
+
│ │ │ │
|
|
207
|
+
│ │ L1 WorkingMemory: read-through cache over L2 │ │
|
|
208
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
209
|
+
│ │
|
|
210
|
+
│ Resource Intelligence Browser UI (:37888) │
|
|
211
|
+
│ ┌──────────────────┐ ┌──────────────────┐ │
|
|
212
|
+
│ │ scan → track → │ │ search, browse, │ │
|
|
213
|
+
│ │ analyze overhead │ │ stats, health │ │
|
|
214
|
+
│ └──────────────────┘ └──────────────────┘ │
|
|
215
|
+
└─────────────────────────────────────────────────────────────┘
|
|
217
216
|
│
|
|
218
217
|
┌─────────┴──────────┐
|
|
219
218
|
│ SQLite + FTS5 │
|
|
220
219
|
│ ~/.claude- │
|
|
221
220
|
│ memory-hub/ │
|
|
222
|
-
│ memory.db │
|
|
223
221
|
│ │
|
|
224
|
-
│
|
|
225
|
-
│
|
|
226
|
-
│
|
|
227
|
-
│ long_term_ │
|
|
228
|
-
│ summaries │
|
|
229
|
-
│ resource_usage │
|
|
230
|
-
│ fts_memories │
|
|
231
|
-
│ tfidf_index │
|
|
232
|
-
│ embeddings │
|
|
233
|
-
│ claude_md_ │
|
|
234
|
-
│ registry │
|
|
235
|
-
│ health_checks │
|
|
222
|
+
│ memory.db │
|
|
223
|
+
│ batch/queue.jsonl│
|
|
224
|
+
│ logs/ │
|
|
236
225
|
└────────────────────┘
|
|
237
226
|
```
|
|
238
227
|
|
|
@@ -242,19 +231,20 @@ User prompt contains "remember that we use TypeScript strict"
|
|
|
242
231
|
|
|
243
232
|
```
|
|
244
233
|
┌─────────────────────────────────────────────────────┐
|
|
245
|
-
│ L1: WorkingMemory
|
|
246
|
-
│
|
|
247
|
-
│
|
|
234
|
+
│ L1: WorkingMemory Read-through cache │
|
|
235
|
+
│ Lives in MCP server <1ms (cache hit) │
|
|
236
|
+
│ Backed by SessionStore Auto-refresh on miss │
|
|
237
|
+
│ TTL: 5 minutes Max 50 entries/session │
|
|
248
238
|
├─────────────────────────────────────────────────────┤
|
|
249
239
|
│ L2: SessionStore SQLite │
|
|
250
240
|
│ Entities + notes <10ms access │
|
|
251
|
-
│
|
|
252
|
-
│
|
|
241
|
+
│ files, errors, decisions Per-session scope │
|
|
242
|
+
│ observations (14 patterns) Importance scored 1-5 │
|
|
253
243
|
├─────────────────────────────────────────────────────┤
|
|
254
|
-
│ L3: LongTermStore SQLite + FTS5 + TF-IDF
|
|
244
|
+
│ L3: LongTermStore SQLite + FTS5 + TF-IDF │
|
|
255
245
|
│ Cross-session summaries <100ms access │
|
|
256
246
|
│ Hybrid ranked search Persistent forever │
|
|
257
|
-
│
|
|
247
|
+
│ Semantic embeddings 3-layer progressive │
|
|
258
248
|
└─────────────────────────────────────────────────────┘
|
|
259
249
|
```
|
|
260
250
|
|
|
@@ -270,7 +260,7 @@ bunx claude-memory-hub install
|
|
|
270
260
|
|
|
271
261
|
One command. Registers MCP server + 5 hooks globally. Works on CLI, VS Code, JetBrains.
|
|
272
262
|
|
|
273
|
-
**Coming from claude-mem?** The installer auto-detects `~/.claude-mem/claude-mem.db` and migrates your data automatically.
|
|
263
|
+
**Coming from claude-mem?** The installer auto-detects `~/.claude-mem/claude-mem.db` and migrates your data automatically.
|
|
274
264
|
|
|
275
265
|
### Update
|
|
276
266
|
|
|
@@ -278,24 +268,8 @@ One command. Registers MCP server + 5 hooks globally. Works on CLI, VS Code, Jet
|
|
|
278
268
|
bunx claude-memory-hub@latest install
|
|
279
269
|
```
|
|
280
270
|
|
|
281
|
-
Or if installed globally:
|
|
282
|
-
|
|
283
|
-
```bash
|
|
284
|
-
bun install -g claude-memory-hub@latest
|
|
285
|
-
claude-memory-hub install
|
|
286
|
-
```
|
|
287
|
-
|
|
288
271
|
Your data at `~/.claude-memory-hub/` is preserved across updates. Schema migrations run automatically.
|
|
289
272
|
|
|
290
|
-
### From source
|
|
291
|
-
|
|
292
|
-
```bash
|
|
293
|
-
git clone https://github.com/TranHoaiHung/claude-memory-hub.git ~/.claude-memory-hub
|
|
294
|
-
cd ~/.claude-memory-hub
|
|
295
|
-
bun install && bun run build:all
|
|
296
|
-
bunx . install
|
|
297
|
-
```
|
|
298
|
-
|
|
299
273
|
### All CLI commands
|
|
300
274
|
|
|
301
275
|
```bash
|
|
@@ -305,10 +279,10 @@ bunx claude-memory-hub status # Check installation
|
|
|
305
279
|
bunx claude-memory-hub migrate # Import data from claude-mem
|
|
306
280
|
bunx claude-memory-hub viewer # Open browser UI at localhost:37888
|
|
307
281
|
bunx claude-memory-hub health # Run health diagnostics
|
|
308
|
-
bunx claude-memory-hub reindex # Rebuild TF-IDF
|
|
282
|
+
bunx claude-memory-hub reindex # Rebuild TF-IDF + embedding indexes
|
|
309
283
|
bunx claude-memory-hub export # Export data as JSONL to stdout
|
|
310
|
-
bunx claude-memory-hub import # Import JSONL from stdin
|
|
311
|
-
bunx claude-memory-hub cleanup # Remove old data (default
|
|
284
|
+
bunx claude-memory-hub import # Import JSONL from stdin (--dry-run)
|
|
285
|
+
bunx claude-memory-hub cleanup # Remove old data (--days N, default 90)
|
|
312
286
|
```
|
|
313
287
|
|
|
314
288
|
### Requirements
|
|
@@ -327,13 +301,13 @@ Claude can call these tools directly during conversation:
|
|
|
327
301
|
|
|
328
302
|
| Tool | What it does | When to use |
|
|
329
303
|
|------|-------------|-------------|
|
|
330
|
-
| `memory_recall` | FTS5 search past
|
|
304
|
+
| `memory_recall` | FTS5 search past sessions (supports `max_tokens`) | Starting a task, looking for prior work |
|
|
331
305
|
| `memory_entities` | Find all sessions that touched a file | Before editing a file, understanding history |
|
|
332
|
-
| `memory_session_notes` | Current session activity
|
|
306
|
+
| `memory_session_notes` | Current session activity (L1 cache) | Mid-session, checking what's been done |
|
|
333
307
|
| `memory_store` | Manually save a note or decision | Preserving important context |
|
|
334
|
-
| `memory_context_budget` | Analyze token costs +
|
|
308
|
+
| `memory_context_budget` | Analyze token costs + overhead report | Understanding resource usage |
|
|
335
309
|
|
|
336
|
-
### 3-Layer Search
|
|
310
|
+
### 3-Layer Search
|
|
337
311
|
|
|
338
312
|
| Tool | Layer | Tokens/result | When to use |
|
|
339
313
|
|------|-------|---------------|-------------|
|
|
@@ -345,7 +319,46 @@ Claude can call these tools directly during conversation:
|
|
|
345
319
|
|
|
346
320
|
| Tool | What it does |
|
|
347
321
|
|------|-------------|
|
|
348
|
-
| `memory_health` | Check database, FTS5, disk, integrity status |
|
|
322
|
+
| `memory_health` | Check database, FTS5, disk, embeddings, integrity status |
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Data Export/Import
|
|
327
|
+
|
|
328
|
+
### Export
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
# Full export
|
|
332
|
+
bunx claude-memory-hub export > backup.jsonl
|
|
333
|
+
|
|
334
|
+
# Incremental (since timestamp)
|
|
335
|
+
bunx claude-memory-hub export --since 1743580800000 > incremental.jsonl
|
|
336
|
+
|
|
337
|
+
# Single table
|
|
338
|
+
bunx claude-memory-hub export --table sessions > sessions.jsonl
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Import
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
# Import from file
|
|
345
|
+
bunx claude-memory-hub import < backup.jsonl
|
|
346
|
+
|
|
347
|
+
# Validate without writing
|
|
348
|
+
bunx claude-memory-hub import --dry-run < backup.jsonl
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Cleanup
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
# Remove data older than 90 days (default)
|
|
355
|
+
bunx claude-memory-hub cleanup
|
|
356
|
+
|
|
357
|
+
# Custom retention
|
|
358
|
+
bunx claude-memory-hub cleanup --days 30
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Format: JSONL (one JSON object per line). Embedding BLOBs encoded as base64. Import uses UPSERT — safe to re-run.
|
|
349
362
|
|
|
350
363
|
---
|
|
351
364
|
|
|
@@ -366,20 +379,14 @@ Opens a dark-themed dashboard at `http://localhost:37888` with:
|
|
|
366
379
|
|
|
367
380
|
## Migrating from claude-mem
|
|
368
381
|
|
|
369
|
-
If you're already using [claude-mem](https://github.com/nicobailey-llc/claude-mem), migration is seamless:
|
|
370
|
-
|
|
371
382
|
```bash
|
|
372
383
|
# Automatic (during install)
|
|
373
384
|
bunx claude-memory-hub install
|
|
374
|
-
# → Detects ~/.claude-mem/claude-mem.db automatically
|
|
375
|
-
# → Migrates sessions, observations, summaries
|
|
376
385
|
|
|
377
386
|
# Manual
|
|
378
387
|
bunx claude-memory-hub migrate
|
|
379
388
|
```
|
|
380
389
|
|
|
381
|
-
### What gets migrated
|
|
382
|
-
|
|
383
390
|
| claude-mem | → | memory-hub |
|
|
384
391
|
|------------|---|------------|
|
|
385
392
|
| `sdk_sessions` | → | `sessions` |
|
|
@@ -397,13 +404,14 @@ Migration is idempotent — safe to run multiple times with zero duplicates.
|
|
|
397
404
|
| Version | What it solved |
|
|
398
405
|
|---------|---------------|
|
|
399
406
|
| **v0.1.0** | Cross-session memory, entity tracking, FTS5 search |
|
|
400
|
-
| **v0.2.0** | Compact interceptor (PreCompact/PostCompact
|
|
407
|
+
| **v0.2.0** | Compact interceptor (PreCompact/PostCompact), context enrichment, importance scoring |
|
|
401
408
|
| **v0.3.0** | Removed API key requirement, 1-command install |
|
|
402
|
-
| **v0.4.0** |
|
|
409
|
+
| **v0.4.0** | Resource usage tracking, token overhead analysis |
|
|
403
410
|
| **v0.5.0** | Production hardening, hybrid search, 3-layer progressive search, browser UI, health monitoring, claude-mem migration |
|
|
404
|
-
| **v0.6.0** | ResourceRegistry (170 resources), semantic search (384-dim embeddings), observation capture, CLAUDE.md tracking, 3-tier LLM summarization
|
|
411
|
+
| **v0.6.0** | ResourceRegistry (170 resources), semantic search (384-dim embeddings), observation capture, CLAUDE.md tracking, 3-tier LLM summarization |
|
|
405
412
|
| **v0.7.0** | Honest resource analysis, semantic search scaling, batch embeddings, 14 observation patterns, DB auto-cleanup, summarizer retry |
|
|
406
|
-
| **v0.8.0** | 91 unit tests (was 0%), L1
|
|
413
|
+
| **v0.8.0** | 91 unit tests (was 0%), L1 read-through cache, PostToolUse batch queue (75ms→3ms), JSONL export/import, data cleanup CLI, CI/CD auto-publish |
|
|
414
|
+
| **v0.8.1** | Token-budget-aware MCP tools (`max_tokens`), proactive mid-session memory retrieval (topic-shift detection), session-end batch flush |
|
|
407
415
|
|
|
408
416
|
See [CHANGELOG.md](CHANGELOG.md) for full details.
|
|
409
417
|
|
|
@@ -437,7 +445,11 @@ All data stored locally at `~/.claude-memory-hub/`.
|
|
|
437
445
|
|
|
438
446
|
```
|
|
439
447
|
~/.claude-memory-hub/
|
|
440
|
-
├── memory.db
|
|
448
|
+
├── memory.db # SQLite database (all memory data)
|
|
449
|
+
├── batch/
|
|
450
|
+
│ └── queue.jsonl # PostToolUse batch queue (auto-flushed)
|
|
451
|
+
├── proactive/
|
|
452
|
+
│ └── <session>.json # Topic tracking state (auto-cleaned)
|
|
441
453
|
└── logs/
|
|
442
454
|
└── memory-hub.log # Structured JSON logs (auto-rotated at 5MB)
|
|
443
455
|
```
|
|
@@ -1424,7 +1424,7 @@ function safeJson(text, fallback) {
|
|
|
1424
1424
|
|
|
1425
1425
|
// src/context/injection-validator.ts
|
|
1426
1426
|
var log3 = createLogger("injection-validator");
|
|
1427
|
-
var MAX_CHARS =
|
|
1427
|
+
var MAX_CHARS = 8000;
|
|
1428
1428
|
|
|
1429
1429
|
class InjectionValidator {
|
|
1430
1430
|
registry;
|
|
@@ -1833,6 +1833,132 @@ function isBatchEnabled() {
|
|
|
1833
1833
|
return mode !== "disabled";
|
|
1834
1834
|
}
|
|
1835
1835
|
|
|
1836
|
+
// src/retrieval/proactive-retrieval.ts
|
|
1837
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync4 } from "fs";
|
|
1838
|
+
import { join as join6 } from "path";
|
|
1839
|
+
import { homedir as homedir5 } from "os";
|
|
1840
|
+
var log6 = createLogger("proactive-retrieval");
|
|
1841
|
+
var DATA_DIR2 = join6(homedir5(), ".claude-memory-hub");
|
|
1842
|
+
var PROACTIVE_DIR = join6(DATA_DIR2, "proactive");
|
|
1843
|
+
var TOOL_CALL_INTERVAL = 15;
|
|
1844
|
+
var MAX_INJECTION_CHARS = 3000;
|
|
1845
|
+
function evaluateProactiveInjection(sessionId, toolName, toolInput, toolResponse) {
|
|
1846
|
+
const state = loadState(sessionId);
|
|
1847
|
+
state.toolCallCount++;
|
|
1848
|
+
const filePath = extractFilePath(toolName, toolInput);
|
|
1849
|
+
if (filePath) {
|
|
1850
|
+
state.recentFiles = [...new Set([filePath, ...state.recentFiles])].slice(0, 20);
|
|
1851
|
+
}
|
|
1852
|
+
const shouldTrigger = state.toolCallCount % TOOL_CALL_INTERVAL === 0 || toolName === "Bash" && typeof toolResponse.exit_code === "number" && toolResponse.exit_code !== 0 && state.toolCallCount > 5;
|
|
1853
|
+
if (!shouldTrigger) {
|
|
1854
|
+
saveState(sessionId, state);
|
|
1855
|
+
return { shouldInject: false };
|
|
1856
|
+
}
|
|
1857
|
+
const currentTopic = detectTopic(state.recentFiles);
|
|
1858
|
+
if (!currentTopic || state.injectedTopics.includes(currentTopic)) {
|
|
1859
|
+
saveState(sessionId, state);
|
|
1860
|
+
return { shouldInject: false };
|
|
1861
|
+
}
|
|
1862
|
+
const ltStore = new LongTermStore;
|
|
1863
|
+
const results = ltStore.search(currentTopic, 2);
|
|
1864
|
+
if (results.length === 0) {
|
|
1865
|
+
state.injectedTopics.push(currentTopic);
|
|
1866
|
+
saveState(sessionId, state);
|
|
1867
|
+
return { shouldInject: false };
|
|
1868
|
+
}
|
|
1869
|
+
const lines = [`**Relevant past context** (topic: ${currentTopic}):`];
|
|
1870
|
+
for (const r of results) {
|
|
1871
|
+
const date = new Date(r.created_at).toLocaleDateString();
|
|
1872
|
+
lines.push(`- [${date}] ${r.summary.slice(0, 200)}`);
|
|
1873
|
+
const files = safeJson4(r.files_touched, []);
|
|
1874
|
+
if (files.length > 0)
|
|
1875
|
+
lines.push(` Files: ${files.slice(0, 3).join(", ")}`);
|
|
1876
|
+
}
|
|
1877
|
+
let context = lines.join(`
|
|
1878
|
+
`);
|
|
1879
|
+
if (context.length > MAX_INJECTION_CHARS) {
|
|
1880
|
+
context = context.slice(0, MAX_INJECTION_CHARS) + `
|
|
1881
|
+
[...truncated]`;
|
|
1882
|
+
}
|
|
1883
|
+
state.injectedTopics.push(currentTopic);
|
|
1884
|
+
state.lastInjectionAt = Date.now();
|
|
1885
|
+
saveState(sessionId, state);
|
|
1886
|
+
log6.info("proactive injection triggered", { sessionId, topic: currentTopic, results: results.length });
|
|
1887
|
+
return { shouldInject: true, additionalContext: context };
|
|
1888
|
+
}
|
|
1889
|
+
function cleanupProactiveState(sessionId) {
|
|
1890
|
+
const path = statePath(sessionId);
|
|
1891
|
+
try {
|
|
1892
|
+
if (existsSync6(path)) {
|
|
1893
|
+
const { unlinkSync: unlinkSync2 } = __require("fs");
|
|
1894
|
+
unlinkSync2(path);
|
|
1895
|
+
}
|
|
1896
|
+
} catch {}
|
|
1897
|
+
}
|
|
1898
|
+
function detectTopic(recentFiles) {
|
|
1899
|
+
if (recentFiles.length < 3)
|
|
1900
|
+
return null;
|
|
1901
|
+
const dirs = recentFiles.map((f) => f.split("/").slice(0, -1).join("/")).filter(Boolean);
|
|
1902
|
+
const dirCounts = new Map;
|
|
1903
|
+
for (const d of dirs) {
|
|
1904
|
+
const parts = d.split("/").filter(Boolean);
|
|
1905
|
+
const leaf = parts[parts.length - 1];
|
|
1906
|
+
if (leaf && leaf !== "src" && leaf !== "lib" && leaf !== "utils") {
|
|
1907
|
+
dirCounts.set(leaf, (dirCounts.get(leaf) ?? 0) + 1);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
let bestTopic = null;
|
|
1911
|
+
let bestCount = 0;
|
|
1912
|
+
for (const [topic, count] of dirCounts) {
|
|
1913
|
+
if (count > bestCount) {
|
|
1914
|
+
bestTopic = topic;
|
|
1915
|
+
bestCount = count;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
const fileNames = recentFiles.map((f) => f.split("/").pop() ?? "").filter(Boolean);
|
|
1919
|
+
const keywords = ["auth", "payment", "user", "api", "database", "config", "test", "migration", "deploy", "search"];
|
|
1920
|
+
for (const kw of keywords) {
|
|
1921
|
+
const matches = fileNames.filter((f) => f.toLowerCase().includes(kw));
|
|
1922
|
+
if (matches.length >= 2)
|
|
1923
|
+
return kw;
|
|
1924
|
+
}
|
|
1925
|
+
return bestTopic;
|
|
1926
|
+
}
|
|
1927
|
+
function statePath(sessionId) {
|
|
1928
|
+
return join6(PROACTIVE_DIR, `${sessionId.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`);
|
|
1929
|
+
}
|
|
1930
|
+
function loadState(sessionId) {
|
|
1931
|
+
const path = statePath(sessionId);
|
|
1932
|
+
try {
|
|
1933
|
+
if (existsSync6(path)) {
|
|
1934
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
1935
|
+
}
|
|
1936
|
+
} catch {}
|
|
1937
|
+
return { toolCallCount: 0, lastInjectionAt: 0, injectedTopics: [], recentFiles: [] };
|
|
1938
|
+
}
|
|
1939
|
+
function saveState(sessionId, state) {
|
|
1940
|
+
try {
|
|
1941
|
+
if (!existsSync6(PROACTIVE_DIR)) {
|
|
1942
|
+
mkdirSync4(PROACTIVE_DIR, { recursive: true, mode: 448 });
|
|
1943
|
+
}
|
|
1944
|
+
writeFileSync2(statePath(sessionId), JSON.stringify(state), "utf-8");
|
|
1945
|
+
} catch {}
|
|
1946
|
+
}
|
|
1947
|
+
function extractFilePath(toolName, toolInput) {
|
|
1948
|
+
if (toolName === "Read" || toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit") {
|
|
1949
|
+
const fp = toolInput.file_path;
|
|
1950
|
+
return typeof fp === "string" ? fp : undefined;
|
|
1951
|
+
}
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
function safeJson4(text, fallback) {
|
|
1955
|
+
try {
|
|
1956
|
+
return JSON.parse(text);
|
|
1957
|
+
} catch {
|
|
1958
|
+
return fallback;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1836
1962
|
// src/hooks-entry/post-tool-use.ts
|
|
1837
1963
|
async function main() {
|
|
1838
1964
|
if (process.env["CLAUDE_MEMORY_HUB_SKIP_HOOKS"] === "1")
|
|
@@ -1871,9 +1997,18 @@ async function main() {
|
|
|
1871
1997
|
timestamp: Date.now()
|
|
1872
1998
|
});
|
|
1873
1999
|
tryFlush();
|
|
1874
|
-
|
|
1875
|
-
|
|
2000
|
+
} catch {
|
|
2001
|
+
await handlePostToolUse(hook, project);
|
|
2002
|
+
}
|
|
2003
|
+
} else {
|
|
2004
|
+
await handlePostToolUse(hook, project);
|
|
1876
2005
|
}
|
|
1877
|
-
|
|
2006
|
+
try {
|
|
2007
|
+
const result = evaluateProactiveInjection(hook.session_id, hook.tool_name, hook.tool_input ?? {}, hook.tool_response ?? {});
|
|
2008
|
+
if (result.shouldInject && result.additionalContext) {
|
|
2009
|
+
process.stdout.write(JSON.stringify({ additionalContext: result.additionalContext }) + `
|
|
2010
|
+
`);
|
|
2011
|
+
}
|
|
2012
|
+
} catch {}
|
|
1878
2013
|
}
|
|
1879
2014
|
main().catch(() => {}).finally(() => process.exit(0));
|
|
@@ -1424,7 +1424,7 @@ function safeJson(text, fallback) {
|
|
|
1424
1424
|
|
|
1425
1425
|
// src/context/injection-validator.ts
|
|
1426
1426
|
var log3 = createLogger("injection-validator");
|
|
1427
|
-
var MAX_CHARS =
|
|
1427
|
+
var MAX_CHARS = 8000;
|
|
1428
1428
|
|
|
1429
1429
|
class InjectionValidator {
|
|
1430
1430
|
registry;
|
|
@@ -2021,6 +2021,250 @@ async function indexEmbedding(docType, docId, text, db) {
|
|
|
2021
2021
|
created_at = excluded.created_at`, [docType, docId, blob, Date.now()]);
|
|
2022
2022
|
}
|
|
2023
2023
|
|
|
2024
|
+
// src/retrieval/proactive-retrieval.ts
|
|
2025
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync3 } from "fs";
|
|
2026
|
+
import { join as join5 } from "path";
|
|
2027
|
+
import { homedir as homedir4 } from "os";
|
|
2028
|
+
var log9 = createLogger("proactive-retrieval");
|
|
2029
|
+
var DATA_DIR = join5(homedir4(), ".claude-memory-hub");
|
|
2030
|
+
var PROACTIVE_DIR = join5(DATA_DIR, "proactive");
|
|
2031
|
+
var TOOL_CALL_INTERVAL = 15;
|
|
2032
|
+
var MAX_INJECTION_CHARS = 3000;
|
|
2033
|
+
function evaluateProactiveInjection(sessionId, toolName, toolInput, toolResponse) {
|
|
2034
|
+
const state = loadState(sessionId);
|
|
2035
|
+
state.toolCallCount++;
|
|
2036
|
+
const filePath = extractFilePath(toolName, toolInput);
|
|
2037
|
+
if (filePath) {
|
|
2038
|
+
state.recentFiles = [...new Set([filePath, ...state.recentFiles])].slice(0, 20);
|
|
2039
|
+
}
|
|
2040
|
+
const shouldTrigger = state.toolCallCount % TOOL_CALL_INTERVAL === 0 || toolName === "Bash" && typeof toolResponse.exit_code === "number" && toolResponse.exit_code !== 0 && state.toolCallCount > 5;
|
|
2041
|
+
if (!shouldTrigger) {
|
|
2042
|
+
saveState(sessionId, state);
|
|
2043
|
+
return { shouldInject: false };
|
|
2044
|
+
}
|
|
2045
|
+
const currentTopic = detectTopic(state.recentFiles);
|
|
2046
|
+
if (!currentTopic || state.injectedTopics.includes(currentTopic)) {
|
|
2047
|
+
saveState(sessionId, state);
|
|
2048
|
+
return { shouldInject: false };
|
|
2049
|
+
}
|
|
2050
|
+
const ltStore = new LongTermStore;
|
|
2051
|
+
const results = ltStore.search(currentTopic, 2);
|
|
2052
|
+
if (results.length === 0) {
|
|
2053
|
+
state.injectedTopics.push(currentTopic);
|
|
2054
|
+
saveState(sessionId, state);
|
|
2055
|
+
return { shouldInject: false };
|
|
2056
|
+
}
|
|
2057
|
+
const lines = [`**Relevant past context** (topic: ${currentTopic}):`];
|
|
2058
|
+
for (const r of results) {
|
|
2059
|
+
const date = new Date(r.created_at).toLocaleDateString();
|
|
2060
|
+
lines.push(`- [${date}] ${r.summary.slice(0, 200)}`);
|
|
2061
|
+
const files = safeJson4(r.files_touched, []);
|
|
2062
|
+
if (files.length > 0)
|
|
2063
|
+
lines.push(` Files: ${files.slice(0, 3).join(", ")}`);
|
|
2064
|
+
}
|
|
2065
|
+
let context = lines.join(`
|
|
2066
|
+
`);
|
|
2067
|
+
if (context.length > MAX_INJECTION_CHARS) {
|
|
2068
|
+
context = context.slice(0, MAX_INJECTION_CHARS) + `
|
|
2069
|
+
[...truncated]`;
|
|
2070
|
+
}
|
|
2071
|
+
state.injectedTopics.push(currentTopic);
|
|
2072
|
+
state.lastInjectionAt = Date.now();
|
|
2073
|
+
saveState(sessionId, state);
|
|
2074
|
+
log9.info("proactive injection triggered", { sessionId, topic: currentTopic, results: results.length });
|
|
2075
|
+
return { shouldInject: true, additionalContext: context };
|
|
2076
|
+
}
|
|
2077
|
+
function cleanupProactiveState(sessionId) {
|
|
2078
|
+
const path = statePath(sessionId);
|
|
2079
|
+
try {
|
|
2080
|
+
if (existsSync5(path)) {
|
|
2081
|
+
const { unlinkSync } = __require("fs");
|
|
2082
|
+
unlinkSync(path);
|
|
2083
|
+
}
|
|
2084
|
+
} catch {}
|
|
2085
|
+
}
|
|
2086
|
+
function detectTopic(recentFiles) {
|
|
2087
|
+
if (recentFiles.length < 3)
|
|
2088
|
+
return null;
|
|
2089
|
+
const dirs = recentFiles.map((f) => f.split("/").slice(0, -1).join("/")).filter(Boolean);
|
|
2090
|
+
const dirCounts = new Map;
|
|
2091
|
+
for (const d of dirs) {
|
|
2092
|
+
const parts = d.split("/").filter(Boolean);
|
|
2093
|
+
const leaf = parts[parts.length - 1];
|
|
2094
|
+
if (leaf && leaf !== "src" && leaf !== "lib" && leaf !== "utils") {
|
|
2095
|
+
dirCounts.set(leaf, (dirCounts.get(leaf) ?? 0) + 1);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
let bestTopic = null;
|
|
2099
|
+
let bestCount = 0;
|
|
2100
|
+
for (const [topic, count] of dirCounts) {
|
|
2101
|
+
if (count > bestCount) {
|
|
2102
|
+
bestTopic = topic;
|
|
2103
|
+
bestCount = count;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
const fileNames = recentFiles.map((f) => f.split("/").pop() ?? "").filter(Boolean);
|
|
2107
|
+
const keywords = ["auth", "payment", "user", "api", "database", "config", "test", "migration", "deploy", "search"];
|
|
2108
|
+
for (const kw of keywords) {
|
|
2109
|
+
const matches = fileNames.filter((f) => f.toLowerCase().includes(kw));
|
|
2110
|
+
if (matches.length >= 2)
|
|
2111
|
+
return kw;
|
|
2112
|
+
}
|
|
2113
|
+
return bestTopic;
|
|
2114
|
+
}
|
|
2115
|
+
function statePath(sessionId) {
|
|
2116
|
+
return join5(PROACTIVE_DIR, `${sessionId.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`);
|
|
2117
|
+
}
|
|
2118
|
+
function loadState(sessionId) {
|
|
2119
|
+
const path = statePath(sessionId);
|
|
2120
|
+
try {
|
|
2121
|
+
if (existsSync5(path)) {
|
|
2122
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
2123
|
+
}
|
|
2124
|
+
} catch {}
|
|
2125
|
+
return { toolCallCount: 0, lastInjectionAt: 0, injectedTopics: [], recentFiles: [] };
|
|
2126
|
+
}
|
|
2127
|
+
function saveState(sessionId, state) {
|
|
2128
|
+
try {
|
|
2129
|
+
if (!existsSync5(PROACTIVE_DIR)) {
|
|
2130
|
+
mkdirSync3(PROACTIVE_DIR, { recursive: true, mode: 448 });
|
|
2131
|
+
}
|
|
2132
|
+
writeFileSync(statePath(sessionId), JSON.stringify(state), "utf-8");
|
|
2133
|
+
} catch {}
|
|
2134
|
+
}
|
|
2135
|
+
function extractFilePath(toolName, toolInput) {
|
|
2136
|
+
if (toolName === "Read" || toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit") {
|
|
2137
|
+
const fp = toolInput.file_path;
|
|
2138
|
+
return typeof fp === "string" ? fp : undefined;
|
|
2139
|
+
}
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
function safeJson4(text, fallback) {
|
|
2143
|
+
try {
|
|
2144
|
+
return JSON.parse(text);
|
|
2145
|
+
} catch {
|
|
2146
|
+
return fallback;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
// src/capture/batch-queue.ts
|
|
2151
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, appendFileSync as appendFileSync2, unlinkSync, statSync as statSync2 } from "fs";
|
|
2152
|
+
import { join as join6 } from "path";
|
|
2153
|
+
import { homedir as homedir5 } from "os";
|
|
2154
|
+
var log10 = createLogger("batch-queue");
|
|
2155
|
+
var DATA_DIR2 = join6(homedir5(), ".claude-memory-hub");
|
|
2156
|
+
var BATCH_DIR = join6(DATA_DIR2, "batch");
|
|
2157
|
+
var QUEUE_PATH = join6(BATCH_DIR, "queue.jsonl");
|
|
2158
|
+
var LOCK_PATH = join6(BATCH_DIR, "queue.lock");
|
|
2159
|
+
var MAX_QUEUE_SIZE = 100 * 1024;
|
|
2160
|
+
var LOCK_STALE_MS = 30000;
|
|
2161
|
+
function enqueueEvent(event) {
|
|
2162
|
+
try {
|
|
2163
|
+
ensureBatchDir();
|
|
2164
|
+
const line = JSON.stringify(event) + `
|
|
2165
|
+
`;
|
|
2166
|
+
appendFileSync2(QUEUE_PATH, line, "utf-8");
|
|
2167
|
+
} catch (err) {
|
|
2168
|
+
log10.error("enqueue failed", { error: String(err) });
|
|
2169
|
+
throw err;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
function tryFlush() {
|
|
2173
|
+
try {
|
|
2174
|
+
if (!existsSync6(QUEUE_PATH))
|
|
2175
|
+
return false;
|
|
2176
|
+
const stat = statSync2(QUEUE_PATH);
|
|
2177
|
+
if (stat.size === 0)
|
|
2178
|
+
return false;
|
|
2179
|
+
if (!tryAcquireLock())
|
|
2180
|
+
return false;
|
|
2181
|
+
try {
|
|
2182
|
+
flushQueue();
|
|
2183
|
+
return true;
|
|
2184
|
+
} finally {
|
|
2185
|
+
releaseLock();
|
|
2186
|
+
}
|
|
2187
|
+
} catch (err) {
|
|
2188
|
+
log10.error("flush failed", { error: String(err) });
|
|
2189
|
+
return false;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
function flushQueue() {
|
|
2193
|
+
const content = readFileSync4(QUEUE_PATH, "utf-8").trim();
|
|
2194
|
+
if (!content)
|
|
2195
|
+
return;
|
|
2196
|
+
const events = [];
|
|
2197
|
+
for (const line of content.split(`
|
|
2198
|
+
`)) {
|
|
2199
|
+
try {
|
|
2200
|
+
events.push(JSON.parse(line));
|
|
2201
|
+
} catch {
|
|
2202
|
+
log10.warn("skipping malformed queue line");
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
if (events.length === 0)
|
|
2206
|
+
return;
|
|
2207
|
+
const store = new SessionStore;
|
|
2208
|
+
const tracker = new ResourceTracker;
|
|
2209
|
+
const registry = getResourceRegistry();
|
|
2210
|
+
const db = store["db"];
|
|
2211
|
+
db.transaction(() => {
|
|
2212
|
+
for (const event of events) {
|
|
2213
|
+
store.upsertSession({
|
|
2214
|
+
id: event.session.id,
|
|
2215
|
+
project: event.session.project,
|
|
2216
|
+
started_at: event.session.started_at,
|
|
2217
|
+
status: "active"
|
|
2218
|
+
});
|
|
2219
|
+
for (const entity of event.entities) {
|
|
2220
|
+
store.insertEntity({ ...entity, project: event.session.project });
|
|
2221
|
+
}
|
|
2222
|
+
if (event.resources) {
|
|
2223
|
+
for (const r of event.resources) {
|
|
2224
|
+
const resource = registry.resolve(r.type, r.name);
|
|
2225
|
+
tracker.trackUsage(event.session.id, event.session.project, r.type, r.name, r.tokenCost ?? resource?.full_tokens ?? 0);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
})();
|
|
2230
|
+
writeFileSync2(QUEUE_PATH, "", "utf-8");
|
|
2231
|
+
log10.info("batch flushed", { events: events.length });
|
|
2232
|
+
}
|
|
2233
|
+
function tryAcquireLock() {
|
|
2234
|
+
try {
|
|
2235
|
+
if (existsSync6(LOCK_PATH)) {
|
|
2236
|
+
const lockContent = readFileSync4(LOCK_PATH, "utf-8").trim();
|
|
2237
|
+
const [pidStr, timestampStr] = lockContent.split(":");
|
|
2238
|
+
const lockTime = Number(timestampStr);
|
|
2239
|
+
if (Date.now() - lockTime < LOCK_STALE_MS) {
|
|
2240
|
+
const pid = Number(pidStr);
|
|
2241
|
+
try {
|
|
2242
|
+
process.kill(pid, 0);
|
|
2243
|
+
return false;
|
|
2244
|
+
} catch {}
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
writeFileSync2(LOCK_PATH, `${process.pid}:${Date.now()}`, "utf-8");
|
|
2248
|
+
return true;
|
|
2249
|
+
} catch {
|
|
2250
|
+
return false;
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
function releaseLock() {
|
|
2254
|
+
try {
|
|
2255
|
+
unlinkSync(LOCK_PATH);
|
|
2256
|
+
} catch {}
|
|
2257
|
+
}
|
|
2258
|
+
function ensureBatchDir() {
|
|
2259
|
+
if (!existsSync6(BATCH_DIR)) {
|
|
2260
|
+
mkdirSync4(BATCH_DIR, { recursive: true, mode: 448 });
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
function isBatchEnabled() {
|
|
2264
|
+
const mode = process.env["CLAUDE_MEMORY_HUB_BATCH"] ?? "auto";
|
|
2265
|
+
return mode !== "disabled";
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2024
2268
|
// src/hooks-entry/session-end.ts
|
|
2025
2269
|
async function main() {
|
|
2026
2270
|
if (process.env["CLAUDE_MEMORY_HUB_SKIP_HOOKS"] === "1")
|
|
@@ -2036,6 +2280,10 @@ async function main() {
|
|
|
2036
2280
|
}
|
|
2037
2281
|
const project = projectFromCwd(hook.cwd ?? process.env["CLAUDE_CWD"] ?? process.cwd());
|
|
2038
2282
|
await handleSessionEnd(hook, project);
|
|
2283
|
+
try {
|
|
2284
|
+
tryFlush();
|
|
2285
|
+
} catch {}
|
|
2286
|
+
cleanupProactiveState(hook.session_id);
|
|
2039
2287
|
const store = new SessionStore;
|
|
2040
2288
|
if (store.getSession(hook.session_id)) {
|
|
2041
2289
|
await new SessionSummarizer().summarize(hook.session_id, project).catch(() => {});
|
package/dist/index.js
CHANGED
|
@@ -15593,8 +15593,17 @@ function sanitizeFtsQuery2(query) {
|
|
|
15593
15593
|
}
|
|
15594
15594
|
|
|
15595
15595
|
// src/mcp/tool-handlers.ts
|
|
15596
|
+
function truncateToTokenBudget(text, maxTokens) {
|
|
15597
|
+
if (!maxTokens || maxTokens <= 0)
|
|
15598
|
+
return text;
|
|
15599
|
+
const maxChars = maxTokens * 4;
|
|
15600
|
+
if (text.length <= maxChars)
|
|
15601
|
+
return text;
|
|
15602
|
+
return text.slice(0, maxChars) + `
|
|
15603
|
+
[...truncated to fit ~` + maxTokens + " token budget]";
|
|
15604
|
+
}
|
|
15596
15605
|
async function handleMemoryRecall(args) {
|
|
15597
|
-
const { query, limit = 5 } = args;
|
|
15606
|
+
const { query, limit = 5, max_tokens } = args;
|
|
15598
15607
|
if (!query?.trim())
|
|
15599
15608
|
return "No query provided.";
|
|
15600
15609
|
const builder = new ContextBuilder;
|
|
@@ -15604,9 +15613,10 @@ async function handleMemoryRecall(args) {
|
|
|
15604
15613
|
|
|
15605
15614
|
This appears to be a new topic with no prior history.`;
|
|
15606
15615
|
}
|
|
15607
|
-
|
|
15616
|
+
const output = `Found ${ctx.resultCount} relevant memory(ies) (~${ctx.tokenEstimate} tokens):
|
|
15608
15617
|
|
|
15609
15618
|
${ctx.text}`;
|
|
15619
|
+
return truncateToTokenBudget(output, max_tokens);
|
|
15610
15620
|
}
|
|
15611
15621
|
async function handleMemoryEntities(args) {
|
|
15612
15622
|
const { file_path } = args;
|
|
@@ -15648,11 +15658,11 @@ async function handleMemoryStore(args) {
|
|
|
15648
15658
|
return `Note saved to session ${session_id}.`;
|
|
15649
15659
|
}
|
|
15650
15660
|
async function handleMemorySearch(args) {
|
|
15651
|
-
const { query, limit = 20, offset = 0, project } = args;
|
|
15661
|
+
const { query, limit = 20, offset = 0, project, max_tokens } = args;
|
|
15652
15662
|
if (!query?.trim())
|
|
15653
15663
|
return "No query provided.";
|
|
15654
15664
|
const results = await searchIndex(query, { limit: Math.min(limit, 50), offset, ...project ? { project } : {} });
|
|
15655
|
-
return formatSearchIndex(results);
|
|
15665
|
+
return truncateToTokenBudget(formatSearchIndex(results), max_tokens);
|
|
15656
15666
|
}
|
|
15657
15667
|
async function handleMemoryTimeline(args) {
|
|
15658
15668
|
const { id, type, depth = 3 } = args;
|
|
@@ -15662,11 +15672,11 @@ async function handleMemoryTimeline(args) {
|
|
|
15662
15672
|
return formatTimeline(entries);
|
|
15663
15673
|
}
|
|
15664
15674
|
async function handleMemoryFetch(args) {
|
|
15665
|
-
const { ids } = args;
|
|
15675
|
+
const { ids, max_tokens } = args;
|
|
15666
15676
|
if (!ids?.length)
|
|
15667
15677
|
return "No IDs provided.";
|
|
15668
15678
|
const records = fetchFullRecords(ids.slice(0, 20));
|
|
15669
|
-
return formatFullRecords(records);
|
|
15679
|
+
return truncateToTokenBudget(formatFullRecords(records), max_tokens);
|
|
15670
15680
|
}
|
|
15671
15681
|
async function handleMemoryHealth() {
|
|
15672
15682
|
const report = runHealthCheck();
|
|
@@ -15741,7 +15751,8 @@ var TOOL_DEFINITIONS = [
|
|
|
15741
15751
|
type: "object",
|
|
15742
15752
|
properties: {
|
|
15743
15753
|
query: { type: "string", description: "Natural language search query" },
|
|
15744
|
-
limit: { type: "number", description: "Max results (default 5, max 10)" }
|
|
15754
|
+
limit: { type: "number", description: "Max results (default 5, max 10)" },
|
|
15755
|
+
max_tokens: { type: "number", description: "Max output tokens. Results truncated to fit budget. Default: unlimited" }
|
|
15745
15756
|
},
|
|
15746
15757
|
required: ["query"]
|
|
15747
15758
|
}
|
|
@@ -15800,7 +15811,8 @@ var TOOL_DEFINITIONS = [
|
|
|
15800
15811
|
query: { type: "string", description: "Search query" },
|
|
15801
15812
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
15802
15813
|
offset: { type: "number", description: "Pagination offset (default 0)" },
|
|
15803
|
-
project: { type: "string", description: "Filter by project" }
|
|
15814
|
+
project: { type: "string", description: "Filter by project" },
|
|
15815
|
+
max_tokens: { type: "number", description: "Max output tokens. Results truncated to fit budget" }
|
|
15804
15816
|
},
|
|
15805
15817
|
required: ["query"]
|
|
15806
15818
|
}
|
|
@@ -15835,7 +15847,8 @@ var TOOL_DEFINITIONS = [
|
|
|
15835
15847
|
required: ["id", "type"]
|
|
15836
15848
|
},
|
|
15837
15849
|
description: "Array of {id, type} from search results"
|
|
15838
|
-
}
|
|
15850
|
+
},
|
|
15851
|
+
max_tokens: { type: "number", description: "Max output tokens. Records truncated to fit budget" }
|
|
15839
15852
|
},
|
|
15840
15853
|
required: ["ids"]
|
|
15841
15854
|
}
|
package/package.json
CHANGED