claude-memory-hub 0.8.1 → 0.9.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/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
5
5
 
6
6
  ---
7
7
 
8
+ ## [0.9.0] - 2026-04-02
9
+
10
+ Smart context budget allocation — memory never gets pushed out by lower-priority content.
11
+
12
+ ### Smart Budget Allocation (breaking change in injection behavior)
13
+
14
+ - **Priority-based `fitWithinBudget()`** — replaces naive sequential concatenation. Four content sections now compete for 8,000 chars with explicit priorities: P1 memory (min 500 chars) > P2 CLAUDE.md (min 200 chars) > P3 resource advice (droppable) > P4 overhead warning (droppable). When total fits, everything is kept. When over budget, lowest priority sections are shrunk or dropped first
15
+ - **Memory context guaranteed** — past session context always gets first claim on budget. Previously could be truncated when CLAUDE.md + advice consumed too much space
16
+
17
+ ### CLAUDE.md Adaptive Compression
18
+
19
+ - **3-level `formatForInjection()`** — CLAUDE.md summary now adapts to available budget: Level 3 (full: headings + token cost, >500 chars), Level 2 (compact: file + token cost, >200 chars), Level 1 (minimal: file names only, <200 chars). Previously always used full format regardless of remaining space
20
+
21
+ ### Overhead Warning Injection
22
+
23
+ - **Auto-inject warning when unused resources > 10K tokens** — UserPromptSubmit hook now analyzes `OverheadReport` and injects a one-line note if unused skills/agents exceed 10,000 listing tokens. Points user to `memory_context_budget` tool for details. Zero-cost when overhead is acceptable
24
+
25
+ ### Context Injection Limits
26
+
27
+ - **UserPromptSubmit cap doubled** — `MAX_CHARS` increased from 4,500 (~1,125 tokens) to 8,000 (~2,000 tokens)
28
+ - **Proactive retrieval cap doubled** — `MAX_INJECTION_CHARS` increased from 1,500 (~375 tokens) to 3,000 (~750 tokens)
29
+ - **Proactive summary slice increased** — per-result summary increased from 200 to 400 chars for richer mid-session context
30
+ - **Memory result summary increased** — session-start per-result summary increased from 300 to 400 chars
31
+
32
+ ---
33
+
8
34
  ## [0.8.1] - 2026-04-02
9
35
 
10
36
  Token-budget-aware MCP tools + proactive mid-session memory retrieval.
package/README.md CHANGED
@@ -17,6 +17,31 @@ 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
47
  Claude Code forgets everything between sessions. Within long sessions, auto-compact destroys 90% of context. Search is keyword-only with no ranking.
@@ -49,6 +74,8 @@ Search: Keyword-only, no semantic ranking
49
74
  | Multi-agent memory sharing | -- | -- | **Yes (free)** |
50
75
  | Permission-aware (approved only) | -- | -- | **Yes** |
51
76
  | Data export/import (JSONL) | -- | -- | **Yes** |
77
+ | Smart budget allocation (priority-based) | -- | -- | **Yes** |
78
+ | Overhead warning (unused resources) | -- | -- | **Yes** |
52
79
  | Hook batching (3ms vs 75ms) | -- | -- | **Yes** |
53
80
  | Browser UI | -- | Yes | **Yes** |
54
81
  | Health monitoring + auto-cleanup | -- | -- | **Yes** |
@@ -156,10 +183,10 @@ User prompt contains "remember that we use TypeScript strict"
156
183
  ## Architecture
157
184
 
158
185
  ```
159
- ┌──────────────────────────────────────────────────────────────┐
160
- │ Claude Code
161
-
162
- │ 5 Lifecycle Hooks
186
+ ┌─────────────────────────────────────────────────────────────┐
187
+ │ Claude Code
188
+
189
+ │ 5 Lifecycle Hooks
163
190
  │ ┌───────────────┐ ┌──────────────┐ ┌──────────────┐ │
164
191
  │ │ PostToolUse │ │ PreCompact │ │ PostCompact │ │
165
192
  │ │ batch queue │ │ inject │ │ save summary │ │
@@ -169,25 +196,25 @@ User prompt contains "remember that we use TypeScript strict"
169
196
  │ │UserPrompt │ │ │ Stop │ │
170
197
  │ │Submit: inject│ │ │ session end │ │
171
198
  │ │past context │ │ │ summarize │ │
172
- │ └──────────────┘ │ └──────────────┘ │
199
+ │ └──────────────┘ │ └──────────────┘ │
173
200
  │ │ │
174
201
  │ MCP Server (stdio, long-lived) │
175
202
  │ ┌─────────────────────────────────────────────────────┐ │
176
203
  │ │ memory_recall memory_search (L1 index) │ │
177
204
  │ │ memory_entities memory_timeline (L2 context) │ │
178
205
  │ │ memory_session_notes memory_fetch (L3 full) │ │
179
- │ │ memory_store memory_context_budget │ │
180
- │ │ memory_health │ │
181
- │ │ │ │
182
- │ │ L1 WorkingMemory: read-through cache over L2 │ │
206
+ │ │ memory_store memory_context_budget │ │
207
+ │ │ memory_health │ │
208
+ │ │ │ │
209
+ │ │ L1 WorkingMemory: read-through cache over L2 │ │
183
210
  │ └─────────────────────────────────────────────────────┘ │
184
-
185
- │ Resource Intelligence Browser UI (:37888)
211
+
212
+ │ Resource Intelligence Browser UI (:37888)
186
213
  │ ┌──────────────────┐ ┌──────────────────┐ │
187
214
  │ │ scan → track → │ │ search, browse, │ │
188
- │ │ analyze overhead │ │ stats, health │ │
215
+ │ │ analyze overhead │ │ stats, health │ │
189
216
  │ └──────────────────┘ └──────────────────┘ │
190
- └──────────────────────────────────────────────────────────────┘
217
+ └─────────────────────────────────────────────────────────────┘
191
218
 
192
219
  ┌─────────┴──────────┐
193
220
  │ SQLite + FTS5 │
@@ -195,7 +222,7 @@ User prompt contains "remember that we use TypeScript strict"
195
222
  │ memory-hub/ │
196
223
  │ │
197
224
  │ memory.db │
198
- │ batch/queue.jsonl
225
+ │ batch/queue.jsonl│
199
226
  │ logs/ │
200
227
  └────────────────────┘
201
228
  ```
@@ -216,7 +243,7 @@ User prompt contains "remember that we use TypeScript strict"
216
243
  │ files, errors, decisions Per-session scope │
217
244
  │ observations (14 patterns) Importance scored 1-5 │
218
245
  ├─────────────────────────────────────────────────────┤
219
- │ L3: LongTermStore SQLite + FTS5 + TF-IDF
246
+ │ L3: LongTermStore SQLite + FTS5 + TF-IDF
220
247
  │ Cross-session summaries <100ms access │
221
248
  │ Hybrid ranked search Persistent forever │
222
249
  │ Semantic embeddings 3-layer progressive │
@@ -387,6 +414,7 @@ Migration is idempotent — safe to run multiple times with zero duplicates.
387
414
  | **v0.7.0** | Honest resource analysis, semantic search scaling, batch embeddings, 14 observation patterns, DB auto-cleanup, summarizer retry |
388
415
  | **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 |
389
416
  | **v0.8.1** | Token-budget-aware MCP tools (`max_tokens`), proactive mid-session memory retrieval (topic-shift detection), session-end batch flush |
417
+ | **v0.9.0** | Smart budget allocation (priority-based, memory never pushed out), CLAUDE.md adaptive compression (3 levels), overhead warning auto-injection, doubled injection limits |
390
418
 
391
419
  See [CHANGELOG.md](CHANGELOG.md) for full details.
392
420
 
@@ -1717,7 +1717,7 @@ function safeJson(text, fallback) {
1717
1717
 
1718
1718
  // src/context/injection-validator.ts
1719
1719
  var log5 = createLogger("injection-validator");
1720
- var MAX_CHARS = 4500;
1720
+ var MAX_CHARS = 8000;
1721
1721
 
1722
1722
  class InjectionValidator {
1723
1723
  registry;
@@ -1829,15 +1829,26 @@ class ClaudeMdTracker {
1829
1829
  tokenCost: r.token_cost
1830
1830
  }));
1831
1831
  }
1832
- formatForInjection(entries) {
1832
+ formatForInjection(entries, maxChars) {
1833
1833
  if (entries.length === 0)
1834
1834
  return "";
1835
- const lines = ["**Active CLAUDE.md rules:**"];
1835
+ const minimal = `CLAUDE.md: ${entries.map((e) => basename2(dirname(e.path)) + "/" + basename2(e.path)).join(", ")}`;
1836
+ if (maxChars !== undefined && maxChars < 200)
1837
+ return minimal;
1838
+ const compactLines = ["**Active CLAUDE.md rules:**"];
1839
+ for (const e of entries) {
1840
+ compactLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok)`);
1841
+ }
1842
+ const compact = compactLines.join(`
1843
+ `);
1844
+ if (maxChars !== undefined && maxChars < 500)
1845
+ return compact;
1846
+ const fullLines = ["**Active CLAUDE.md rules:**"];
1836
1847
  for (const e of entries) {
1837
1848
  const headings = e.sections.map((s) => s.heading).join(", ");
1838
- lines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1849
+ fullLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1839
1850
  }
1840
- return lines.join(`
1851
+ return fullLines.join(`
1841
1852
  `);
1842
1853
  }
1843
1854
  computeHash(content) {
@@ -1955,36 +1966,77 @@ async function handleUserPromptSubmit(hook, project) {
1955
1966
  plan.recommendations = validator.filterAliveRecommendations(plan.recommendations);
1956
1967
  plan.skipped = validator.filterAliveRecommendations(plan.skipped);
1957
1968
  const advice = loader.formatContextAdvice(plan);
1958
- const lines = [];
1959
- if (results.length > 0) {
1960
- lines.push("**Past session context:**");
1961
- for (const r of results) {
1962
- const date = new Date(r.created_at).toLocaleDateString();
1963
- const files = safeJson3(r.files_touched, []);
1964
- lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 300)}`);
1965
- if (files.length > 0)
1966
- lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
1967
- }
1968
- }
1969
- if (advice)
1970
- lines.push("", advice);
1969
+ let mdSummary = "";
1971
1970
  if (hook.cwd) {
1972
1971
  try {
1973
1972
  const mdTracker = new ClaudeMdTracker;
1974
1973
  const mdEntries = mdTracker.scanAndUpdate(hook.cwd, project);
1975
- const mdSummary = mdTracker.formatForInjection(mdEntries);
1976
- if (mdSummary)
1977
- lines.push("", mdSummary);
1974
+ mdSummary = mdTracker.formatForInjection(mdEntries);
1978
1975
  const tracker = new ResourceTracker;
1979
1976
  for (const entry of mdEntries) {
1980
1977
  tracker.trackUsage(hook.session_id, project, "claude_md", entry.path, entry.tokenCost);
1981
1978
  }
1982
1979
  } catch {}
1983
1980
  }
1984
- const safeContext = validator.validate(lines.join(`
1985
- `));
1981
+ let overheadWarning = "";
1982
+ try {
1983
+ const overhead = await registry.getOverheadReport(project);
1984
+ const unusedTokens = overhead.potential_savings.if_remove_unused_skills + overhead.potential_savings.if_remove_unused_agents;
1985
+ if (unusedTokens > 1e4) {
1986
+ const unusedCount = overhead.usage_analysis.skills_never_used.length + overhead.usage_analysis.agents_never_used.length;
1987
+ overheadWarning = `Note: ${unusedCount} unused resources (~${unusedTokens} listing tok overhead). Run \`memory_context_budget\` for details.`;
1988
+ }
1989
+ } catch {}
1990
+ const memorySection = buildMemorySection(results);
1991
+ const safeContext = validator.validate(fitWithinBudget(memorySection, mdSummary, advice, overheadWarning));
1986
1992
  return { additionalContext: safeContext };
1987
1993
  }
1994
+ function fitWithinBudget(memoryText, mdText, adviceText, overheadText) {
1995
+ const MAX_CHARS2 = 8000;
1996
+ const sections = [
1997
+ { text: memoryText, priority: 1, minChars: 500 },
1998
+ { text: mdText, priority: 2, minChars: 200 },
1999
+ { text: adviceText, priority: 3, minChars: 0 },
2000
+ { text: overheadText, priority: 4, minChars: 0 }
2001
+ ].filter((s) => s.text.length > 0);
2002
+ const totalNeeded = sections.reduce((sum, s) => sum + s.text.length, 0);
2003
+ if (totalNeeded <= MAX_CHARS2) {
2004
+ return sections.map((s) => s.text).join(`
2005
+
2006
+ `);
2007
+ }
2008
+ let remaining = MAX_CHARS2;
2009
+ const allocated = [];
2010
+ sections.sort((a, b) => a.priority - b.priority);
2011
+ for (const section of sections) {
2012
+ if (remaining <= 0)
2013
+ break;
2014
+ if (section.text.length <= remaining) {
2015
+ allocated.push(section.text);
2016
+ remaining -= section.text.length + 2;
2017
+ } else if (remaining >= section.minChars) {
2018
+ allocated.push(section.text.slice(0, remaining));
2019
+ remaining = 0;
2020
+ }
2021
+ }
2022
+ return allocated.join(`
2023
+
2024
+ `);
2025
+ }
2026
+ function buildMemorySection(results) {
2027
+ if (results.length === 0)
2028
+ return "";
2029
+ const lines = ["**Past session context:**"];
2030
+ for (const r of results) {
2031
+ const date = new Date(r.created_at).toLocaleDateString();
2032
+ const files = safeJson3(r.files_touched, []);
2033
+ lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 400)}`);
2034
+ if (files.length > 0)
2035
+ lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
2036
+ }
2037
+ return lines.join(`
2038
+ `);
2039
+ }
1988
2040
  async function handleSessionEnd(hook, project) {
1989
2041
  const store = new SessionStore;
1990
2042
  store.completeSession(hook.session_id);
@@ -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 = 4500;
1427
+ var MAX_CHARS = 8000;
1428
1428
 
1429
1429
  class InjectionValidator {
1430
1430
  registry;
@@ -1536,15 +1536,26 @@ class ClaudeMdTracker {
1536
1536
  tokenCost: r.token_cost
1537
1537
  }));
1538
1538
  }
1539
- formatForInjection(entries) {
1539
+ formatForInjection(entries, maxChars) {
1540
1540
  if (entries.length === 0)
1541
1541
  return "";
1542
- const lines = ["**Active CLAUDE.md rules:**"];
1542
+ const minimal = `CLAUDE.md: ${entries.map((e) => basename2(dirname(e.path)) + "/" + basename2(e.path)).join(", ")}`;
1543
+ if (maxChars !== undefined && maxChars < 200)
1544
+ return minimal;
1545
+ const compactLines = ["**Active CLAUDE.md rules:**"];
1546
+ for (const e of entries) {
1547
+ compactLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok)`);
1548
+ }
1549
+ const compact = compactLines.join(`
1550
+ `);
1551
+ if (maxChars !== undefined && maxChars < 500)
1552
+ return compact;
1553
+ const fullLines = ["**Active CLAUDE.md rules:**"];
1543
1554
  for (const e of entries) {
1544
1555
  const headings = e.sections.map((s) => s.heading).join(", ");
1545
- lines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1556
+ fullLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1546
1557
  }
1547
- return lines.join(`
1558
+ return fullLines.join(`
1548
1559
  `);
1549
1560
  }
1550
1561
  computeHash(content) {
@@ -1662,36 +1673,77 @@ async function handleUserPromptSubmit(hook, project) {
1662
1673
  plan.recommendations = validator.filterAliveRecommendations(plan.recommendations);
1663
1674
  plan.skipped = validator.filterAliveRecommendations(plan.skipped);
1664
1675
  const advice = loader.formatContextAdvice(plan);
1665
- const lines = [];
1666
- if (results.length > 0) {
1667
- lines.push("**Past session context:**");
1668
- for (const r of results) {
1669
- const date = new Date(r.created_at).toLocaleDateString();
1670
- const files = safeJson3(r.files_touched, []);
1671
- lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 300)}`);
1672
- if (files.length > 0)
1673
- lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
1674
- }
1675
- }
1676
- if (advice)
1677
- lines.push("", advice);
1676
+ let mdSummary = "";
1678
1677
  if (hook.cwd) {
1679
1678
  try {
1680
1679
  const mdTracker = new ClaudeMdTracker;
1681
1680
  const mdEntries = mdTracker.scanAndUpdate(hook.cwd, project);
1682
- const mdSummary = mdTracker.formatForInjection(mdEntries);
1683
- if (mdSummary)
1684
- lines.push("", mdSummary);
1681
+ mdSummary = mdTracker.formatForInjection(mdEntries);
1685
1682
  const tracker = new ResourceTracker;
1686
1683
  for (const entry of mdEntries) {
1687
1684
  tracker.trackUsage(hook.session_id, project, "claude_md", entry.path, entry.tokenCost);
1688
1685
  }
1689
1686
  } catch {}
1690
1687
  }
1691
- const safeContext = validator.validate(lines.join(`
1692
- `));
1688
+ let overheadWarning = "";
1689
+ try {
1690
+ const overhead = await registry.getOverheadReport(project);
1691
+ const unusedTokens = overhead.potential_savings.if_remove_unused_skills + overhead.potential_savings.if_remove_unused_agents;
1692
+ if (unusedTokens > 1e4) {
1693
+ const unusedCount = overhead.usage_analysis.skills_never_used.length + overhead.usage_analysis.agents_never_used.length;
1694
+ overheadWarning = `Note: ${unusedCount} unused resources (~${unusedTokens} listing tok overhead). Run \`memory_context_budget\` for details.`;
1695
+ }
1696
+ } catch {}
1697
+ const memorySection = buildMemorySection(results);
1698
+ const safeContext = validator.validate(fitWithinBudget(memorySection, mdSummary, advice, overheadWarning));
1693
1699
  return { additionalContext: safeContext };
1694
1700
  }
1701
+ function fitWithinBudget(memoryText, mdText, adviceText, overheadText) {
1702
+ const MAX_CHARS2 = 8000;
1703
+ const sections = [
1704
+ { text: memoryText, priority: 1, minChars: 500 },
1705
+ { text: mdText, priority: 2, minChars: 200 },
1706
+ { text: adviceText, priority: 3, minChars: 0 },
1707
+ { text: overheadText, priority: 4, minChars: 0 }
1708
+ ].filter((s) => s.text.length > 0);
1709
+ const totalNeeded = sections.reduce((sum, s) => sum + s.text.length, 0);
1710
+ if (totalNeeded <= MAX_CHARS2) {
1711
+ return sections.map((s) => s.text).join(`
1712
+
1713
+ `);
1714
+ }
1715
+ let remaining = MAX_CHARS2;
1716
+ const allocated = [];
1717
+ sections.sort((a, b) => a.priority - b.priority);
1718
+ for (const section of sections) {
1719
+ if (remaining <= 0)
1720
+ break;
1721
+ if (section.text.length <= remaining) {
1722
+ allocated.push(section.text);
1723
+ remaining -= section.text.length + 2;
1724
+ } else if (remaining >= section.minChars) {
1725
+ allocated.push(section.text.slice(0, remaining));
1726
+ remaining = 0;
1727
+ }
1728
+ }
1729
+ return allocated.join(`
1730
+
1731
+ `);
1732
+ }
1733
+ function buildMemorySection(results) {
1734
+ if (results.length === 0)
1735
+ return "";
1736
+ const lines = ["**Past session context:**"];
1737
+ for (const r of results) {
1738
+ const date = new Date(r.created_at).toLocaleDateString();
1739
+ const files = safeJson3(r.files_touched, []);
1740
+ lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 400)}`);
1741
+ if (files.length > 0)
1742
+ lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
1743
+ }
1744
+ return lines.join(`
1745
+ `);
1746
+ }
1695
1747
  async function handleSessionEnd(hook, project) {
1696
1748
  const store = new SessionStore;
1697
1749
  store.completeSession(hook.session_id);
@@ -1841,7 +1893,7 @@ var log6 = createLogger("proactive-retrieval");
1841
1893
  var DATA_DIR2 = join6(homedir5(), ".claude-memory-hub");
1842
1894
  var PROACTIVE_DIR = join6(DATA_DIR2, "proactive");
1843
1895
  var TOOL_CALL_INTERVAL = 15;
1844
- var MAX_INJECTION_CHARS = 1500;
1896
+ var MAX_INJECTION_CHARS = 3000;
1845
1897
  function evaluateProactiveInjection(sessionId, toolName, toolInput, toolResponse) {
1846
1898
  const state = loadState(sessionId);
1847
1899
  state.toolCallCount++;
@@ -1869,7 +1921,7 @@ function evaluateProactiveInjection(sessionId, toolName, toolInput, toolResponse
1869
1921
  const lines = [`**Relevant past context** (topic: ${currentTopic}):`];
1870
1922
  for (const r of results) {
1871
1923
  const date = new Date(r.created_at).toLocaleDateString();
1872
- lines.push(`- [${date}] ${r.summary.slice(0, 200)}`);
1924
+ lines.push(`- [${date}] ${r.summary.slice(0, 400)}`);
1873
1925
  const files = safeJson4(r.files_touched, []);
1874
1926
  if (files.length > 0)
1875
1927
  lines.push(` Files: ${files.slice(0, 3).join(", ")}`);
@@ -1717,7 +1717,7 @@ function safeJson(text, fallback) {
1717
1717
 
1718
1718
  // src/context/injection-validator.ts
1719
1719
  var log5 = createLogger("injection-validator");
1720
- var MAX_CHARS = 4500;
1720
+ var MAX_CHARS = 8000;
1721
1721
 
1722
1722
  class InjectionValidator {
1723
1723
  registry;
@@ -1829,15 +1829,26 @@ class ClaudeMdTracker {
1829
1829
  tokenCost: r.token_cost
1830
1830
  }));
1831
1831
  }
1832
- formatForInjection(entries) {
1832
+ formatForInjection(entries, maxChars) {
1833
1833
  if (entries.length === 0)
1834
1834
  return "";
1835
- const lines = ["**Active CLAUDE.md rules:**"];
1835
+ const minimal = `CLAUDE.md: ${entries.map((e) => basename2(dirname(e.path)) + "/" + basename2(e.path)).join(", ")}`;
1836
+ if (maxChars !== undefined && maxChars < 200)
1837
+ return minimal;
1838
+ const compactLines = ["**Active CLAUDE.md rules:**"];
1839
+ for (const e of entries) {
1840
+ compactLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok)`);
1841
+ }
1842
+ const compact = compactLines.join(`
1843
+ `);
1844
+ if (maxChars !== undefined && maxChars < 500)
1845
+ return compact;
1846
+ const fullLines = ["**Active CLAUDE.md rules:**"];
1836
1847
  for (const e of entries) {
1837
1848
  const headings = e.sections.map((s) => s.heading).join(", ");
1838
- lines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1849
+ fullLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1839
1850
  }
1840
- return lines.join(`
1851
+ return fullLines.join(`
1841
1852
  `);
1842
1853
  }
1843
1854
  computeHash(content) {
@@ -1955,36 +1966,77 @@ async function handleUserPromptSubmit(hook, project) {
1955
1966
  plan.recommendations = validator.filterAliveRecommendations(plan.recommendations);
1956
1967
  plan.skipped = validator.filterAliveRecommendations(plan.skipped);
1957
1968
  const advice = loader.formatContextAdvice(plan);
1958
- const lines = [];
1959
- if (results.length > 0) {
1960
- lines.push("**Past session context:**");
1961
- for (const r of results) {
1962
- const date = new Date(r.created_at).toLocaleDateString();
1963
- const files = safeJson3(r.files_touched, []);
1964
- lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 300)}`);
1965
- if (files.length > 0)
1966
- lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
1967
- }
1968
- }
1969
- if (advice)
1970
- lines.push("", advice);
1969
+ let mdSummary = "";
1971
1970
  if (hook.cwd) {
1972
1971
  try {
1973
1972
  const mdTracker = new ClaudeMdTracker;
1974
1973
  const mdEntries = mdTracker.scanAndUpdate(hook.cwd, project);
1975
- const mdSummary = mdTracker.formatForInjection(mdEntries);
1976
- if (mdSummary)
1977
- lines.push("", mdSummary);
1974
+ mdSummary = mdTracker.formatForInjection(mdEntries);
1978
1975
  const tracker = new ResourceTracker;
1979
1976
  for (const entry of mdEntries) {
1980
1977
  tracker.trackUsage(hook.session_id, project, "claude_md", entry.path, entry.tokenCost);
1981
1978
  }
1982
1979
  } catch {}
1983
1980
  }
1984
- const safeContext = validator.validate(lines.join(`
1985
- `));
1981
+ let overheadWarning = "";
1982
+ try {
1983
+ const overhead = await registry.getOverheadReport(project);
1984
+ const unusedTokens = overhead.potential_savings.if_remove_unused_skills + overhead.potential_savings.if_remove_unused_agents;
1985
+ if (unusedTokens > 1e4) {
1986
+ const unusedCount = overhead.usage_analysis.skills_never_used.length + overhead.usage_analysis.agents_never_used.length;
1987
+ overheadWarning = `Note: ${unusedCount} unused resources (~${unusedTokens} listing tok overhead). Run \`memory_context_budget\` for details.`;
1988
+ }
1989
+ } catch {}
1990
+ const memorySection = buildMemorySection(results);
1991
+ const safeContext = validator.validate(fitWithinBudget(memorySection, mdSummary, advice, overheadWarning));
1986
1992
  return { additionalContext: safeContext };
1987
1993
  }
1994
+ function fitWithinBudget(memoryText, mdText, adviceText, overheadText) {
1995
+ const MAX_CHARS2 = 8000;
1996
+ const sections = [
1997
+ { text: memoryText, priority: 1, minChars: 500 },
1998
+ { text: mdText, priority: 2, minChars: 200 },
1999
+ { text: adviceText, priority: 3, minChars: 0 },
2000
+ { text: overheadText, priority: 4, minChars: 0 }
2001
+ ].filter((s) => s.text.length > 0);
2002
+ const totalNeeded = sections.reduce((sum, s) => sum + s.text.length, 0);
2003
+ if (totalNeeded <= MAX_CHARS2) {
2004
+ return sections.map((s) => s.text).join(`
2005
+
2006
+ `);
2007
+ }
2008
+ let remaining = MAX_CHARS2;
2009
+ const allocated = [];
2010
+ sections.sort((a, b) => a.priority - b.priority);
2011
+ for (const section of sections) {
2012
+ if (remaining <= 0)
2013
+ break;
2014
+ if (section.text.length <= remaining) {
2015
+ allocated.push(section.text);
2016
+ remaining -= section.text.length + 2;
2017
+ } else if (remaining >= section.minChars) {
2018
+ allocated.push(section.text.slice(0, remaining));
2019
+ remaining = 0;
2020
+ }
2021
+ }
2022
+ return allocated.join(`
2023
+
2024
+ `);
2025
+ }
2026
+ function buildMemorySection(results) {
2027
+ if (results.length === 0)
2028
+ return "";
2029
+ const lines = ["**Past session context:**"];
2030
+ for (const r of results) {
2031
+ const date = new Date(r.created_at).toLocaleDateString();
2032
+ const files = safeJson3(r.files_touched, []);
2033
+ lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 400)}`);
2034
+ if (files.length > 0)
2035
+ lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
2036
+ }
2037
+ return lines.join(`
2038
+ `);
2039
+ }
1988
2040
  async function handleSessionEnd(hook, project) {
1989
2041
  const store = new SessionStore;
1990
2042
  store.completeSession(hook.session_id);
@@ -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 = 4500;
1427
+ var MAX_CHARS = 8000;
1428
1428
 
1429
1429
  class InjectionValidator {
1430
1430
  registry;
@@ -1536,15 +1536,26 @@ class ClaudeMdTracker {
1536
1536
  tokenCost: r.token_cost
1537
1537
  }));
1538
1538
  }
1539
- formatForInjection(entries) {
1539
+ formatForInjection(entries, maxChars) {
1540
1540
  if (entries.length === 0)
1541
1541
  return "";
1542
- const lines = ["**Active CLAUDE.md rules:**"];
1542
+ const minimal = `CLAUDE.md: ${entries.map((e) => basename2(dirname(e.path)) + "/" + basename2(e.path)).join(", ")}`;
1543
+ if (maxChars !== undefined && maxChars < 200)
1544
+ return minimal;
1545
+ const compactLines = ["**Active CLAUDE.md rules:**"];
1546
+ for (const e of entries) {
1547
+ compactLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok)`);
1548
+ }
1549
+ const compact = compactLines.join(`
1550
+ `);
1551
+ if (maxChars !== undefined && maxChars < 500)
1552
+ return compact;
1553
+ const fullLines = ["**Active CLAUDE.md rules:**"];
1543
1554
  for (const e of entries) {
1544
1555
  const headings = e.sections.map((s) => s.heading).join(", ");
1545
- lines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1556
+ fullLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1546
1557
  }
1547
- return lines.join(`
1558
+ return fullLines.join(`
1548
1559
  `);
1549
1560
  }
1550
1561
  computeHash(content) {
@@ -1662,36 +1673,77 @@ async function handleUserPromptSubmit(hook, project) {
1662
1673
  plan.recommendations = validator.filterAliveRecommendations(plan.recommendations);
1663
1674
  plan.skipped = validator.filterAliveRecommendations(plan.skipped);
1664
1675
  const advice = loader.formatContextAdvice(plan);
1665
- const lines = [];
1666
- if (results.length > 0) {
1667
- lines.push("**Past session context:**");
1668
- for (const r of results) {
1669
- const date = new Date(r.created_at).toLocaleDateString();
1670
- const files = safeJson3(r.files_touched, []);
1671
- lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 300)}`);
1672
- if (files.length > 0)
1673
- lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
1674
- }
1675
- }
1676
- if (advice)
1677
- lines.push("", advice);
1676
+ let mdSummary = "";
1678
1677
  if (hook.cwd) {
1679
1678
  try {
1680
1679
  const mdTracker = new ClaudeMdTracker;
1681
1680
  const mdEntries = mdTracker.scanAndUpdate(hook.cwd, project);
1682
- const mdSummary = mdTracker.formatForInjection(mdEntries);
1683
- if (mdSummary)
1684
- lines.push("", mdSummary);
1681
+ mdSummary = mdTracker.formatForInjection(mdEntries);
1685
1682
  const tracker = new ResourceTracker;
1686
1683
  for (const entry of mdEntries) {
1687
1684
  tracker.trackUsage(hook.session_id, project, "claude_md", entry.path, entry.tokenCost);
1688
1685
  }
1689
1686
  } catch {}
1690
1687
  }
1691
- const safeContext = validator.validate(lines.join(`
1692
- `));
1688
+ let overheadWarning = "";
1689
+ try {
1690
+ const overhead = await registry.getOverheadReport(project);
1691
+ const unusedTokens = overhead.potential_savings.if_remove_unused_skills + overhead.potential_savings.if_remove_unused_agents;
1692
+ if (unusedTokens > 1e4) {
1693
+ const unusedCount = overhead.usage_analysis.skills_never_used.length + overhead.usage_analysis.agents_never_used.length;
1694
+ overheadWarning = `Note: ${unusedCount} unused resources (~${unusedTokens} listing tok overhead). Run \`memory_context_budget\` for details.`;
1695
+ }
1696
+ } catch {}
1697
+ const memorySection = buildMemorySection(results);
1698
+ const safeContext = validator.validate(fitWithinBudget(memorySection, mdSummary, advice, overheadWarning));
1693
1699
  return { additionalContext: safeContext };
1694
1700
  }
1701
+ function fitWithinBudget(memoryText, mdText, adviceText, overheadText) {
1702
+ const MAX_CHARS2 = 8000;
1703
+ const sections = [
1704
+ { text: memoryText, priority: 1, minChars: 500 },
1705
+ { text: mdText, priority: 2, minChars: 200 },
1706
+ { text: adviceText, priority: 3, minChars: 0 },
1707
+ { text: overheadText, priority: 4, minChars: 0 }
1708
+ ].filter((s) => s.text.length > 0);
1709
+ const totalNeeded = sections.reduce((sum, s) => sum + s.text.length, 0);
1710
+ if (totalNeeded <= MAX_CHARS2) {
1711
+ return sections.map((s) => s.text).join(`
1712
+
1713
+ `);
1714
+ }
1715
+ let remaining = MAX_CHARS2;
1716
+ const allocated = [];
1717
+ sections.sort((a, b) => a.priority - b.priority);
1718
+ for (const section of sections) {
1719
+ if (remaining <= 0)
1720
+ break;
1721
+ if (section.text.length <= remaining) {
1722
+ allocated.push(section.text);
1723
+ remaining -= section.text.length + 2;
1724
+ } else if (remaining >= section.minChars) {
1725
+ allocated.push(section.text.slice(0, remaining));
1726
+ remaining = 0;
1727
+ }
1728
+ }
1729
+ return allocated.join(`
1730
+
1731
+ `);
1732
+ }
1733
+ function buildMemorySection(results) {
1734
+ if (results.length === 0)
1735
+ return "";
1736
+ const lines = ["**Past session context:**"];
1737
+ for (const r of results) {
1738
+ const date = new Date(r.created_at).toLocaleDateString();
1739
+ const files = safeJson3(r.files_touched, []);
1740
+ lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 400)}`);
1741
+ if (files.length > 0)
1742
+ lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
1743
+ }
1744
+ return lines.join(`
1745
+ `);
1746
+ }
1695
1747
  async function handleSessionEnd(hook, project) {
1696
1748
  const store = new SessionStore;
1697
1749
  store.completeSession(hook.session_id);
@@ -2029,7 +2081,7 @@ var log9 = createLogger("proactive-retrieval");
2029
2081
  var DATA_DIR = join5(homedir4(), ".claude-memory-hub");
2030
2082
  var PROACTIVE_DIR = join5(DATA_DIR, "proactive");
2031
2083
  var TOOL_CALL_INTERVAL = 15;
2032
- var MAX_INJECTION_CHARS = 1500;
2084
+ var MAX_INJECTION_CHARS = 3000;
2033
2085
  function evaluateProactiveInjection(sessionId, toolName, toolInput, toolResponse) {
2034
2086
  const state = loadState(sessionId);
2035
2087
  state.toolCallCount++;
@@ -2057,7 +2109,7 @@ function evaluateProactiveInjection(sessionId, toolName, toolInput, toolResponse
2057
2109
  const lines = [`**Relevant past context** (topic: ${currentTopic}):`];
2058
2110
  for (const r of results) {
2059
2111
  const date = new Date(r.created_at).toLocaleDateString();
2060
- lines.push(`- [${date}] ${r.summary.slice(0, 200)}`);
2112
+ lines.push(`- [${date}] ${r.summary.slice(0, 400)}`);
2061
2113
  const files = safeJson4(r.files_touched, []);
2062
2114
  if (files.length > 0)
2063
2115
  lines.push(` Files: ${files.slice(0, 3).join(", ")}`);
@@ -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 = 4500;
1427
+ var MAX_CHARS = 8000;
1428
1428
 
1429
1429
  class InjectionValidator {
1430
1430
  registry;
@@ -1536,15 +1536,26 @@ class ClaudeMdTracker {
1536
1536
  tokenCost: r.token_cost
1537
1537
  }));
1538
1538
  }
1539
- formatForInjection(entries) {
1539
+ formatForInjection(entries, maxChars) {
1540
1540
  if (entries.length === 0)
1541
1541
  return "";
1542
- const lines = ["**Active CLAUDE.md rules:**"];
1542
+ const minimal = `CLAUDE.md: ${entries.map((e) => basename2(dirname(e.path)) + "/" + basename2(e.path)).join(", ")}`;
1543
+ if (maxChars !== undefined && maxChars < 200)
1544
+ return minimal;
1545
+ const compactLines = ["**Active CLAUDE.md rules:**"];
1546
+ for (const e of entries) {
1547
+ compactLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok)`);
1548
+ }
1549
+ const compact = compactLines.join(`
1550
+ `);
1551
+ if (maxChars !== undefined && maxChars < 500)
1552
+ return compact;
1553
+ const fullLines = ["**Active CLAUDE.md rules:**"];
1543
1554
  for (const e of entries) {
1544
1555
  const headings = e.sections.map((s) => s.heading).join(", ");
1545
- lines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1556
+ fullLines.push(`- ${basename2(dirname(e.path))}/${basename2(e.path)} (~${e.tokenCost} tok): ${headings || "no sections"}`);
1546
1557
  }
1547
- return lines.join(`
1558
+ return fullLines.join(`
1548
1559
  `);
1549
1560
  }
1550
1561
  computeHash(content) {
@@ -1662,36 +1673,77 @@ async function handleUserPromptSubmit(hook, project) {
1662
1673
  plan.recommendations = validator.filterAliveRecommendations(plan.recommendations);
1663
1674
  plan.skipped = validator.filterAliveRecommendations(plan.skipped);
1664
1675
  const advice = loader.formatContextAdvice(plan);
1665
- const lines = [];
1666
- if (results.length > 0) {
1667
- lines.push("**Past session context:**");
1668
- for (const r of results) {
1669
- const date = new Date(r.created_at).toLocaleDateString();
1670
- const files = safeJson3(r.files_touched, []);
1671
- lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 300)}`);
1672
- if (files.length > 0)
1673
- lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
1674
- }
1675
- }
1676
- if (advice)
1677
- lines.push("", advice);
1676
+ let mdSummary = "";
1678
1677
  if (hook.cwd) {
1679
1678
  try {
1680
1679
  const mdTracker = new ClaudeMdTracker;
1681
1680
  const mdEntries = mdTracker.scanAndUpdate(hook.cwd, project);
1682
- const mdSummary = mdTracker.formatForInjection(mdEntries);
1683
- if (mdSummary)
1684
- lines.push("", mdSummary);
1681
+ mdSummary = mdTracker.formatForInjection(mdEntries);
1685
1682
  const tracker = new ResourceTracker;
1686
1683
  for (const entry of mdEntries) {
1687
1684
  tracker.trackUsage(hook.session_id, project, "claude_md", entry.path, entry.tokenCost);
1688
1685
  }
1689
1686
  } catch {}
1690
1687
  }
1691
- const safeContext = validator.validate(lines.join(`
1692
- `));
1688
+ let overheadWarning = "";
1689
+ try {
1690
+ const overhead = await registry.getOverheadReport(project);
1691
+ const unusedTokens = overhead.potential_savings.if_remove_unused_skills + overhead.potential_savings.if_remove_unused_agents;
1692
+ if (unusedTokens > 1e4) {
1693
+ const unusedCount = overhead.usage_analysis.skills_never_used.length + overhead.usage_analysis.agents_never_used.length;
1694
+ overheadWarning = `Note: ${unusedCount} unused resources (~${unusedTokens} listing tok overhead). Run \`memory_context_budget\` for details.`;
1695
+ }
1696
+ } catch {}
1697
+ const memorySection = buildMemorySection(results);
1698
+ const safeContext = validator.validate(fitWithinBudget(memorySection, mdSummary, advice, overheadWarning));
1693
1699
  return { additionalContext: safeContext };
1694
1700
  }
1701
+ function fitWithinBudget(memoryText, mdText, adviceText, overheadText) {
1702
+ const MAX_CHARS2 = 8000;
1703
+ const sections = [
1704
+ { text: memoryText, priority: 1, minChars: 500 },
1705
+ { text: mdText, priority: 2, minChars: 200 },
1706
+ { text: adviceText, priority: 3, minChars: 0 },
1707
+ { text: overheadText, priority: 4, minChars: 0 }
1708
+ ].filter((s) => s.text.length > 0);
1709
+ const totalNeeded = sections.reduce((sum, s) => sum + s.text.length, 0);
1710
+ if (totalNeeded <= MAX_CHARS2) {
1711
+ return sections.map((s) => s.text).join(`
1712
+
1713
+ `);
1714
+ }
1715
+ let remaining = MAX_CHARS2;
1716
+ const allocated = [];
1717
+ sections.sort((a, b) => a.priority - b.priority);
1718
+ for (const section of sections) {
1719
+ if (remaining <= 0)
1720
+ break;
1721
+ if (section.text.length <= remaining) {
1722
+ allocated.push(section.text);
1723
+ remaining -= section.text.length + 2;
1724
+ } else if (remaining >= section.minChars) {
1725
+ allocated.push(section.text.slice(0, remaining));
1726
+ remaining = 0;
1727
+ }
1728
+ }
1729
+ return allocated.join(`
1730
+
1731
+ `);
1732
+ }
1733
+ function buildMemorySection(results) {
1734
+ if (results.length === 0)
1735
+ return "";
1736
+ const lines = ["**Past session context:**"];
1737
+ for (const r of results) {
1738
+ const date = new Date(r.created_at).toLocaleDateString();
1739
+ const files = safeJson3(r.files_touched, []);
1740
+ lines.push(`- [${date}, ${r.project}] ${r.summary.slice(0, 400)}`);
1741
+ if (files.length > 0)
1742
+ lines.push(` Files: ${files.slice(0, 5).join(", ")}`);
1743
+ }
1744
+ return lines.join(`
1745
+ `);
1746
+ }
1695
1747
  async function handleSessionEnd(hook, project) {
1696
1748
  const store = new SessionStore;
1697
1749
  store.completeSession(hook.session_id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-memory-hub",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Persistent memory system for Claude Code. Zero API key. Zero Python. 5 hooks + MCP server + SQLite FTS5 + semantic search.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",