claude-memory-hub 0.9.2 → 0.9.5

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,65 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
5
5
 
6
6
  ---
7
7
 
8
+ ## [0.9.5] - 2026-04-03
9
+
10
+ Stable install path — hooks no longer break after reboot or bunx cache cleanup.
11
+
12
+ ### Bug Fixes
13
+
14
+ - **Hooks pointing to temp `bunx` path** — `bunx claude-memory-hub install` registered hooks at `/private/tmp/bunx-*/...` (macOS) or `%TEMP%/bunx-*/...` (Windows). These paths are ephemeral and get deleted on reboot or cache cleanup, causing **all hooks to silently fail** — sessions stop being captured with no error visible to the user
15
+ - **Install now copies `dist/` to `~/.claude-memory-hub/dist/`** — a stable, persistent location under the user's home directory. Both hooks and MCP server reference this path instead of the package install location
16
+ - **Old hook entries auto-replaced** — `install` removes previous claude-memory-hub hook entries before registering new ones, fixing stale paths from prior installs without manual cleanup
17
+ - **`install.sh` updated** — shell-based installer uses the same stable path strategy with full bun binary resolution
18
+
19
+ ### How It Works
20
+
21
+ ```
22
+ bunx claude-memory-hub install
23
+ 1. Downloads package to temp dir (bunx behavior)
24
+ 2. Copies dist/*.js + dist/hooks/*.js → ~/.claude-memory-hub/dist/ ← NEW
25
+ 3. Registers hooks pointing to ~/.claude-memory-hub/dist/hooks/ ← STABLE
26
+ 4. Registers MCP server pointing to ~/.claude-memory-hub/dist/index.js
27
+ ```
28
+
29
+ ### Upgrade Note
30
+
31
+ Run `bunx claude-memory-hub@latest install` to fix broken hooks. No data loss — only hook paths are updated.
32
+
33
+ ---
34
+
35
+ ## [0.9.4] - 2026-04-02
36
+
37
+ Windows path fix — backslashes no longer eaten by bash.
38
+
39
+ ### Bug Fixes
40
+
41
+ - **Windows backslash paths in hooks** — `C:\Users\Admin\.bun\bin\bun.exe` was passed raw into bash commands, which stripped backslashes → `C:UsersAdmin.bunbinbun.exe`. New `shellPath()` utility converts all paths to forward slashes (`C:/Users/Admin/.bun/bin/bun.exe`) and quotes paths with spaces. Applied to: bun binary path, hook script paths, MCP server path
42
+ - **`where` output parsing on Windows** — `where bun` returns `\r\n` line endings; now splits on `/\r?\n/` instead of `\n`
43
+
44
+ ---
45
+
46
+ ## [0.9.3] - 2026-04-02
47
+
48
+ Summary quality improvements — cleaner data in, garbage data out.
49
+
50
+ ### Summary Quality
51
+
52
+ - **Strip IDE tags from user_prompt** — `<ide_opened_file>`, `<ide_selection>`, `<system-reminder>` tags are now removed before storing `user_prompt` in L2. Summaries and search results no longer contain IDE noise
53
+ - **Skip low-value sessions** — sessions with only `file_read` entities (browsing, no edits) and no errors/decisions/notes/observations are no longer summarized. Prevents generic "Session in project X" entries from polluting L3
54
+ - **`hasModifiedFiles()` method** — new SessionStore method checks for `file_modified` or `file_created` entities efficiently
55
+
56
+ ### New CLI Command
57
+
58
+ - **`prune` command** — removes low-quality summaries from L3: generic text ("Session worked on...", "Session in project..."), IDE tag noise, and empty summaries with no files/decisions/errors. Supports `--dry-run` for safe preview. Also cleans related embeddings
59
+
60
+ ```bash
61
+ bunx claude-memory-hub prune --dry-run # preview
62
+ bunx claude-memory-hub prune # delete
63
+ ```
64
+
65
+ ---
66
+
8
67
  ## [0.9.2] - 2026-04-02
9
68
 
10
69
  Cross-platform hook reliability — Windows/WSL no longer fails with "bun: command not found".
package/README.md CHANGED
@@ -285,6 +285,7 @@ bunx claude-memory-hub reindex # Rebuild TF-IDF + embedding indexes
285
285
  bunx claude-memory-hub export # Export data as JSONL to stdout
286
286
  bunx claude-memory-hub import # Import JSONL from stdin (--dry-run)
287
287
  bunx claude-memory-hub cleanup # Remove old data (--days N, default 90)
288
+ bunx claude-memory-hub prune # Remove low-quality summaries (--dry-run)
288
289
  ```
289
290
 
290
291
  ### Requirements
package/dist/cli.js CHANGED
@@ -110,6 +110,13 @@ var init_logger = __esm(() => {
110
110
  });
111
111
 
112
112
  // src/db/schema.ts
113
+ var exports_schema = {};
114
+ __export(exports_schema, {
115
+ initDatabase: () => initDatabase,
116
+ getDbPath: () => getDbPath,
117
+ getDatabase: () => getDatabase,
118
+ closeDatabase: () => closeDatabase
119
+ });
113
120
  import { Database } from "bun:sqlite";
114
121
  import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
115
122
  import { homedir as homedir2 } from "os";
@@ -242,6 +249,12 @@ function getDatabase() {
242
249
  }
243
250
  return _db;
244
251
  }
252
+ function closeDatabase() {
253
+ if (_db) {
254
+ _db.close();
255
+ _db = null;
256
+ }
257
+ }
245
258
  var log, CREATE_TABLES = `
246
259
  -- Migration version tracking
247
260
  CREATE TABLE IF NOT EXISTS schema_versions (
@@ -1656,7 +1669,7 @@ var init_importer = __esm(() => {
1656
1669
  });
1657
1670
 
1658
1671
  // src/cli/main.ts
1659
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync, writeFileSync } from "fs";
1672
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync, writeFileSync, readdirSync } from "fs";
1660
1673
  import { homedir as homedir5 } from "os";
1661
1674
  import { join as join5, resolve, dirname } from "path";
1662
1675
 
@@ -1954,29 +1967,59 @@ import { spawnSync } from "child_process";
1954
1967
  var CLAUDE_DIR = join5(homedir5(), ".claude");
1955
1968
  var SETTINGS_PATH = join5(CLAUDE_DIR, "settings.json");
1956
1969
  var PKG_DIR = resolve(dirname(import.meta.dir));
1970
+ var STABLE_DIR = join5(homedir5(), ".claude-memory-hub");
1971
+ function shellPath(p) {
1972
+ const normalized = p.replace(/\\/g, "/");
1973
+ return normalized.includes(" ") ? `"${normalized}"` : normalized;
1974
+ }
1957
1975
  function getBunPath() {
1958
1976
  const result = spawnSync(process.platform === "win32" ? "where" : "which", ["bun"], {
1959
1977
  encoding: "utf-8"
1960
1978
  });
1961
- const resolved = result.stdout?.trim().split(`
1962
- `)[0]?.trim();
1979
+ const resolved = result.stdout?.trim().split(/\r?\n/)[0]?.trim();
1963
1980
  if (resolved && existsSync5(resolved))
1964
- return resolved;
1981
+ return shellPath(resolved);
1965
1982
  const candidates = [
1966
1983
  join5(homedir5(), ".bun", "bin", "bun"),
1967
1984
  join5(homedir5(), ".bun", "bin", "bun.exe")
1968
1985
  ];
1969
1986
  for (const c of candidates) {
1970
1987
  if (existsSync5(c))
1971
- return c;
1988
+ return shellPath(c);
1972
1989
  }
1973
1990
  return "bun";
1974
1991
  }
1992
+ function copyDistToStableDir() {
1993
+ const srcDist = join5(PKG_DIR, "dist");
1994
+ const destDist = join5(STABLE_DIR, "dist");
1995
+ if (!existsSync5(srcDist)) {
1996
+ throw new Error(`dist/ not found at ${srcDist}. Run 'bun run build:all' first.`);
1997
+ }
1998
+ const destHooks = join5(destDist, "hooks");
1999
+ mkdirSync3(destHooks, { recursive: true });
2000
+ for (const file of readdirSync(srcDist)) {
2001
+ if (file.endsWith(".js")) {
2002
+ const src = join5(srcDist, file);
2003
+ const dest = join5(destDist, file);
2004
+ writeFileSync(dest, readFileSync(src));
2005
+ }
2006
+ }
2007
+ const srcHooks = join5(srcDist, "hooks");
2008
+ if (existsSync5(srcHooks)) {
2009
+ for (const file of readdirSync(srcHooks)) {
2010
+ if (file.endsWith(".js")) {
2011
+ const src = join5(srcHooks, file);
2012
+ const dest = join5(destHooks, file);
2013
+ writeFileSync(dest, readFileSync(src));
2014
+ }
2015
+ }
2016
+ }
2017
+ }
1975
2018
  function getHookPath(hookName) {
1976
- return join5(PKG_DIR, "dist", "hooks", `${hookName}.js`);
2019
+ return shellPath(join5(STABLE_DIR, "dist", "hooks", `${hookName}.js`));
1977
2020
  }
1978
2021
  function getMcpServerPath() {
1979
- return join5(PKG_DIR, "dist", "index.js");
2022
+ return shellPath(join5(STABLE_DIR, "dist", "index.js"));
1980
2023
  }
1981
2024
  function loadSettings() {
1982
2025
  if (!existsSync5(SETTINGS_PATH))
@@ -1996,7 +2039,16 @@ function saveSettings(settings) {
1996
2039
  function install() {
1997
2040
  console.log(`claude-memory-hub \u2014 install
1998
2041
  `);
1999
- console.log("1. Registering MCP server...");
2042
+ console.log("0. Copying dist/ to ~/.claude-memory-hub/dist/...");
2043
+ try {
2044
+ copyDistToStableDir();
2045
+ console.log(" Files copied to stable location.");
2046
+ } catch (e) {
2047
+ console.error(` Failed to copy dist/: ${e}`);
2048
+ console.error(" Hooks will reference package location (may break after bunx cleanup).");
2049
+ }
2050
+ console.log(`
2051
+ 1. Registering MCP server...`);
2000
2052
  const mcpPath = getMcpServerPath();
2001
2053
  const bunBin = getBunPath();
2002
2054
  const result = spawnSync("claude", ["mcp", "add", "claude-memory-hub", "-s", "user", "--", bunBin, "run", mcpPath], {
@@ -2028,14 +2080,12 @@ function install() {
2028
2080
  for (const [event, scriptPath] of hookEntries) {
2029
2081
  const hooks = settings.hooks;
2030
2082
  hooks[event] ??= [];
2031
- const exists = hooks[event].some((e) => JSON.stringify(e).includes("claude-memory-hub"));
2032
- if (!exists) {
2033
- hooks[event].push({
2034
- matcher: "",
2035
- hooks: [{ type: "command", command: `${bunBin} run ${scriptPath}` }]
2036
- });
2037
- registered++;
2038
- }
2083
+ hooks[event] = hooks[event].filter((e) => !JSON.stringify(e).includes("claude-memory-hub"));
2084
+ hooks[event].push({
2085
+ matcher: "",
2086
+ hooks: [{ type: "command", command: `${bunBin} run ${scriptPath}` }]
2087
+ });
2088
+ registered++;
2039
2089
  }
2040
2090
  saveSettings(settings);
2041
2091
  console.log(` ${registered} hook(s) registered. (${5 - registered} already existed)`);
@@ -2223,6 +2273,42 @@ switch (command) {
2223
2273
  console.log(`Deleted: ${result.sessions_deleted} sessions, ${result.entities_deleted} entities, ${result.embeddings_deleted} embeddings`);
2224
2274
  break;
2225
2275
  }
2276
+ case "prune": {
2277
+ const { getDatabase: getDatabase2 } = (init_schema(), __toCommonJS(exports_schema));
2278
+ const db = getDatabase2();
2279
+ const dryRun = process.argv.includes("--dry-run");
2280
+ console.log(`claude-memory-hub \u2014 prune low-quality summaries${dryRun ? " (dry run)" : ""}
2281
+ `);
2282
+ const garbage = db.query(`SELECT id, session_id, summary FROM long_term_summaries
2283
+ WHERE length(summary) < 50
2284
+ OR summary LIKE '%Session worked on%'
2285
+ OR summary LIKE '%Session in project%'
2286
+ OR summary LIKE '%<ide_%'
2287
+ OR summary LIKE '%<system-reminder>%'
2288
+ OR (files_touched = '[]' AND decisions = '[]' AND errors_fixed = '[]' AND length(summary) < 100)`).all();
2289
+ if (garbage.length === 0) {
2290
+ console.log(" No low-quality summaries found. Database is clean.");
2291
+ break;
2292
+ }
2293
+ console.log(` Found ${garbage.length} low-quality summaries:`);
2294
+ for (const g of garbage.slice(0, 10)) {
2295
+ console.log(` [${g.id}] "${g.summary.slice(0, 80)}${g.summary.length > 80 ? "..." : ""}"`);
2296
+ }
2297
+ if (garbage.length > 10)
2298
+ console.log(` ... and ${garbage.length - 10} more`);
2299
+ if (dryRun) {
2300
+ console.log(`
2301
+ Dry run \u2014 no changes made. Remove --dry-run to delete.`);
2302
+ } else {
2303
+ const ids = garbage.map((g) => g.id);
2304
+ const placeholders = ids.map(() => "?").join(",");
2305
+ db.run(`DELETE FROM long_term_summaries WHERE id IN (${placeholders})`, ids);
2306
+ db.run(`DELETE FROM embeddings WHERE doc_type = 'summary' AND doc_id IN (${placeholders})`, ids.map(String));
2307
+ console.log(`
2308
+ Deleted ${garbage.length} summaries + related embeddings.`);
2309
+ }
2310
+ break;
2311
+ }
2226
2312
  default:
2227
2313
  console.log(`claude-memory-hub \u2014 persistent memory for Claude Code
2228
2314
  `);
@@ -2237,6 +2323,7 @@ switch (command) {
2237
2323
  console.log(" export Export data as JSONL (--since T, --table T)");
2238
2324
  console.log(" import Import JSONL from stdin (--dry-run)");
2239
2325
  console.log(" cleanup Remove old data (--days N, default 90)");
2326
+ console.log(" prune Remove low-quality summaries (--dry-run)");
2240
2327
  console.log(`
2241
2328
  Usage: npx claude-memory-hub <command>`);
2242
2329
  break;
@@ -376,6 +376,11 @@ class SessionStore {
376
376
  this.db.run("UPDATE sessions SET status = 'completed', ended_at = ? WHERE id = ?", [Date.now(), id]);
377
377
  }
378
378
  insertEntity(entity) {
379
+ if (entity.entity_type === "decision" || entity.entity_type === "observation") {
380
+ const existing = this.db.query("SELECT COUNT(*) as c FROM entities WHERE session_id = ? AND entity_type = ? AND entity_value = ?").get(entity.session_id, entity.entity_type, entity.entity_value);
381
+ if (existing && existing.c > 0)
382
+ return -1;
383
+ }
379
384
  const result = this.db.run(`INSERT INTO entities(session_id, project, tool_name, entity_type, entity_value, context, importance, created_at, prompt_number)
380
385
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
381
386
  entity.session_id,
@@ -407,6 +412,11 @@ class SessionStore {
407
412
  WHERE session_id = ? AND entity_type IN ('file_read','file_modified','file_created')
408
413
  ORDER BY importance DESC, created_at DESC`).all(session_id).map((r) => r.entity_value);
409
414
  }
415
+ hasModifiedFiles(session_id) {
416
+ const row = this.db.query(`SELECT COUNT(*) as c FROM entities
417
+ WHERE session_id = ? AND entity_type IN ('file_modified','file_created') LIMIT 1`).get(session_id);
418
+ return (row?.c ?? 0) > 0;
419
+ }
410
420
  insertNote(note) {
411
421
  this.db.run("INSERT INTO session_notes(session_id, content, created_at) VALUES (?, ?, ?)", [note.session_id, note.content, note.created_at]);
412
422
  }
@@ -658,6 +668,9 @@ class SessionSummarizer {
658
668
  const notes = this.sessionStore.getSessionNotes(session_id).map((n) => n.content);
659
669
  if (files.length === 0 && errors.length === 0 && notes.length === 0)
660
670
  return;
671
+ const hasModified = this.sessionStore.hasModifiedFiles(session_id);
672
+ if (!hasModified && errors.length === 0 && decisions.length === 0 && notes.length === 0 && observations.length === 0)
673
+ return;
661
674
  const obsValues = observations.slice(0, 5).map((o) => o.entity_value);
662
675
  let summaryText;
663
676
  let tier = "rule-based";
@@ -1912,6 +1925,9 @@ function safeJson2(text, fallback) {
1912
1925
 
1913
1926
  // src/capture/hook-handler.ts
1914
1927
  import { basename as basename3 } from "path";
1928
+ function stripIdeTags(prompt) {
1929
+ return prompt.replace(/<ide_opened_file>[\s\S]*?<\/ide_opened_file>\s*/g, "").replace(/<ide_selection>[\s\S]*?<\/ide_selection>\s*/g, "").replace(/<system-reminder>[\s\S]*?<\/system-reminder>\s*/g, "").trim();
1930
+ }
1915
1931
  async function handlePostToolUse(hook, project) {
1916
1932
  const store = new SessionStore;
1917
1933
  store.upsertSession({
@@ -1947,14 +1963,15 @@ async function handlePostToolUse(hook, project) {
1947
1963
  async function handleUserPromptSubmit(hook, project) {
1948
1964
  const store = new SessionStore;
1949
1965
  const ltStore = new LongTermStore;
1966
+ const cleanPrompt = stripIdeTags(hook.prompt);
1950
1967
  store.upsertSession({
1951
1968
  id: hook.session_id,
1952
1969
  project,
1953
1970
  started_at: Date.now(),
1954
- user_prompt: hook.prompt.slice(0, 500),
1971
+ user_prompt: cleanPrompt.slice(0, 500) || hook.prompt.slice(0, 500),
1955
1972
  status: "active"
1956
1973
  });
1957
- const promptObs = extractObservationFromPrompt(hook.prompt, hook.session_id, project, 0);
1974
+ const promptObs = extractObservationFromPrompt(cleanPrompt || hook.prompt, hook.session_id, project, 0);
1958
1975
  if (promptObs)
1959
1976
  store.insertEntity({ ...promptObs, project });
1960
1977
  const results = ltStore.search(hook.prompt, 3);
@@ -376,6 +376,11 @@ class SessionStore {
376
376
  this.db.run("UPDATE sessions SET status = 'completed', ended_at = ? WHERE id = ?", [Date.now(), id]);
377
377
  }
378
378
  insertEntity(entity) {
379
+ if (entity.entity_type === "decision" || entity.entity_type === "observation") {
380
+ const existing = this.db.query("SELECT COUNT(*) as c FROM entities WHERE session_id = ? AND entity_type = ? AND entity_value = ?").get(entity.session_id, entity.entity_type, entity.entity_value);
381
+ if (existing && existing.c > 0)
382
+ return -1;
383
+ }
379
384
  const result = this.db.run(`INSERT INTO entities(session_id, project, tool_name, entity_type, entity_value, context, importance, created_at, prompt_number)
380
385
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
381
386
  entity.session_id,
@@ -407,6 +412,11 @@ class SessionStore {
407
412
  WHERE session_id = ? AND entity_type IN ('file_read','file_modified','file_created')
408
413
  ORDER BY importance DESC, created_at DESC`).all(session_id).map((r) => r.entity_value);
409
414
  }
415
+ hasModifiedFiles(session_id) {
416
+ const row = this.db.query(`SELECT COUNT(*) as c FROM entities
417
+ WHERE session_id = ? AND entity_type IN ('file_modified','file_created') LIMIT 1`).get(session_id);
418
+ return (row?.c ?? 0) > 0;
419
+ }
410
420
  insertNote(note) {
411
421
  this.db.run("INSERT INTO session_notes(session_id, content, created_at) VALUES (?, ?, ?)", [note.session_id, note.content, note.created_at]);
412
422
  }
@@ -1619,6 +1629,9 @@ function safeJson2(text, fallback) {
1619
1629
 
1620
1630
  // src/capture/hook-handler.ts
1621
1631
  import { basename as basename3 } from "path";
1632
+ function stripIdeTags(prompt) {
1633
+ return prompt.replace(/<ide_opened_file>[\s\S]*?<\/ide_opened_file>\s*/g, "").replace(/<ide_selection>[\s\S]*?<\/ide_selection>\s*/g, "").replace(/<system-reminder>[\s\S]*?<\/system-reminder>\s*/g, "").trim();
1634
+ }
1622
1635
  async function handlePostToolUse(hook, project) {
1623
1636
  const store = new SessionStore;
1624
1637
  store.upsertSession({
@@ -1654,14 +1667,15 @@ async function handlePostToolUse(hook, project) {
1654
1667
  async function handleUserPromptSubmit(hook, project) {
1655
1668
  const store = new SessionStore;
1656
1669
  const ltStore = new LongTermStore;
1670
+ const cleanPrompt = stripIdeTags(hook.prompt);
1657
1671
  store.upsertSession({
1658
1672
  id: hook.session_id,
1659
1673
  project,
1660
1674
  started_at: Date.now(),
1661
- user_prompt: hook.prompt.slice(0, 500),
1675
+ user_prompt: cleanPrompt.slice(0, 500) || hook.prompt.slice(0, 500),
1662
1676
  status: "active"
1663
1677
  });
1664
- const promptObs = extractObservationFromPrompt(hook.prompt, hook.session_id, project, 0);
1678
+ const promptObs = extractObservationFromPrompt(cleanPrompt || hook.prompt, hook.session_id, project, 0);
1665
1679
  if (promptObs)
1666
1680
  store.insertEntity({ ...promptObs, project });
1667
1681
  const results = ltStore.search(hook.prompt, 3);
@@ -376,6 +376,11 @@ class SessionStore {
376
376
  this.db.run("UPDATE sessions SET status = 'completed', ended_at = ? WHERE id = ?", [Date.now(), id]);
377
377
  }
378
378
  insertEntity(entity) {
379
+ if (entity.entity_type === "decision" || entity.entity_type === "observation") {
380
+ const existing = this.db.query("SELECT COUNT(*) as c FROM entities WHERE session_id = ? AND entity_type = ? AND entity_value = ?").get(entity.session_id, entity.entity_type, entity.entity_value);
381
+ if (existing && existing.c > 0)
382
+ return -1;
383
+ }
379
384
  const result = this.db.run(`INSERT INTO entities(session_id, project, tool_name, entity_type, entity_value, context, importance, created_at, prompt_number)
380
385
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
381
386
  entity.session_id,
@@ -407,6 +412,11 @@ class SessionStore {
407
412
  WHERE session_id = ? AND entity_type IN ('file_read','file_modified','file_created')
408
413
  ORDER BY importance DESC, created_at DESC`).all(session_id).map((r) => r.entity_value);
409
414
  }
415
+ hasModifiedFiles(session_id) {
416
+ const row = this.db.query(`SELECT COUNT(*) as c FROM entities
417
+ WHERE session_id = ? AND entity_type IN ('file_modified','file_created') LIMIT 1`).get(session_id);
418
+ return (row?.c ?? 0) > 0;
419
+ }
410
420
  insertNote(note) {
411
421
  this.db.run("INSERT INTO session_notes(session_id, content, created_at) VALUES (?, ?, ?)", [note.session_id, note.content, note.created_at]);
412
422
  }
@@ -658,6 +668,9 @@ class SessionSummarizer {
658
668
  const notes = this.sessionStore.getSessionNotes(session_id).map((n) => n.content);
659
669
  if (files.length === 0 && errors.length === 0 && notes.length === 0)
660
670
  return;
671
+ const hasModified = this.sessionStore.hasModifiedFiles(session_id);
672
+ if (!hasModified && errors.length === 0 && decisions.length === 0 && notes.length === 0 && observations.length === 0)
673
+ return;
661
674
  const obsValues = observations.slice(0, 5).map((o) => o.entity_value);
662
675
  let summaryText;
663
676
  let tier = "rule-based";
@@ -1912,6 +1925,9 @@ function safeJson2(text, fallback) {
1912
1925
 
1913
1926
  // src/capture/hook-handler.ts
1914
1927
  import { basename as basename3 } from "path";
1928
+ function stripIdeTags(prompt) {
1929
+ return prompt.replace(/<ide_opened_file>[\s\S]*?<\/ide_opened_file>\s*/g, "").replace(/<ide_selection>[\s\S]*?<\/ide_selection>\s*/g, "").replace(/<system-reminder>[\s\S]*?<\/system-reminder>\s*/g, "").trim();
1930
+ }
1915
1931
  async function handlePostToolUse(hook, project) {
1916
1932
  const store = new SessionStore;
1917
1933
  store.upsertSession({
@@ -1947,14 +1963,15 @@ async function handlePostToolUse(hook, project) {
1947
1963
  async function handleUserPromptSubmit(hook, project) {
1948
1964
  const store = new SessionStore;
1949
1965
  const ltStore = new LongTermStore;
1966
+ const cleanPrompt = stripIdeTags(hook.prompt);
1950
1967
  store.upsertSession({
1951
1968
  id: hook.session_id,
1952
1969
  project,
1953
1970
  started_at: Date.now(),
1954
- user_prompt: hook.prompt.slice(0, 500),
1971
+ user_prompt: cleanPrompt.slice(0, 500) || hook.prompt.slice(0, 500),
1955
1972
  status: "active"
1956
1973
  });
1957
- const promptObs = extractObservationFromPrompt(hook.prompt, hook.session_id, project, 0);
1974
+ const promptObs = extractObservationFromPrompt(cleanPrompt || hook.prompt, hook.session_id, project, 0);
1958
1975
  if (promptObs)
1959
1976
  store.insertEntity({ ...promptObs, project });
1960
1977
  const results = ltStore.search(hook.prompt, 3);
@@ -376,6 +376,11 @@ class SessionStore {
376
376
  this.db.run("UPDATE sessions SET status = 'completed', ended_at = ? WHERE id = ?", [Date.now(), id]);
377
377
  }
378
378
  insertEntity(entity) {
379
+ if (entity.entity_type === "decision" || entity.entity_type === "observation") {
380
+ const existing = this.db.query("SELECT COUNT(*) as c FROM entities WHERE session_id = ? AND entity_type = ? AND entity_value = ?").get(entity.session_id, entity.entity_type, entity.entity_value);
381
+ if (existing && existing.c > 0)
382
+ return -1;
383
+ }
379
384
  const result = this.db.run(`INSERT INTO entities(session_id, project, tool_name, entity_type, entity_value, context, importance, created_at, prompt_number)
380
385
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
381
386
  entity.session_id,
@@ -407,6 +412,11 @@ class SessionStore {
407
412
  WHERE session_id = ? AND entity_type IN ('file_read','file_modified','file_created')
408
413
  ORDER BY importance DESC, created_at DESC`).all(session_id).map((r) => r.entity_value);
409
414
  }
415
+ hasModifiedFiles(session_id) {
416
+ const row = this.db.query(`SELECT COUNT(*) as c FROM entities
417
+ WHERE session_id = ? AND entity_type IN ('file_modified','file_created') LIMIT 1`).get(session_id);
418
+ return (row?.c ?? 0) > 0;
419
+ }
410
420
  insertNote(note) {
411
421
  this.db.run("INSERT INTO session_notes(session_id, content, created_at) VALUES (?, ?, ?)", [note.session_id, note.content, note.created_at]);
412
422
  }
@@ -1619,6 +1629,9 @@ function safeJson2(text, fallback) {
1619
1629
 
1620
1630
  // src/capture/hook-handler.ts
1621
1631
  import { basename as basename3 } from "path";
1632
+ function stripIdeTags(prompt) {
1633
+ return prompt.replace(/<ide_opened_file>[\s\S]*?<\/ide_opened_file>\s*/g, "").replace(/<ide_selection>[\s\S]*?<\/ide_selection>\s*/g, "").replace(/<system-reminder>[\s\S]*?<\/system-reminder>\s*/g, "").trim();
1634
+ }
1622
1635
  async function handlePostToolUse(hook, project) {
1623
1636
  const store = new SessionStore;
1624
1637
  store.upsertSession({
@@ -1654,14 +1667,15 @@ async function handlePostToolUse(hook, project) {
1654
1667
  async function handleUserPromptSubmit(hook, project) {
1655
1668
  const store = new SessionStore;
1656
1669
  const ltStore = new LongTermStore;
1670
+ const cleanPrompt = stripIdeTags(hook.prompt);
1657
1671
  store.upsertSession({
1658
1672
  id: hook.session_id,
1659
1673
  project,
1660
1674
  started_at: Date.now(),
1661
- user_prompt: hook.prompt.slice(0, 500),
1675
+ user_prompt: cleanPrompt.slice(0, 500) || hook.prompt.slice(0, 500),
1662
1676
  status: "active"
1663
1677
  });
1664
- const promptObs = extractObservationFromPrompt(hook.prompt, hook.session_id, project, 0);
1678
+ const promptObs = extractObservationFromPrompt(cleanPrompt || hook.prompt, hook.session_id, project, 0);
1665
1679
  if (promptObs)
1666
1680
  store.insertEntity({ ...promptObs, project });
1667
1681
  const results = ltStore.search(hook.prompt, 3);
@@ -1936,6 +1950,9 @@ class SessionSummarizer {
1936
1950
  const notes = this.sessionStore.getSessionNotes(session_id).map((n) => n.content);
1937
1951
  if (files.length === 0 && errors.length === 0 && notes.length === 0)
1938
1952
  return;
1953
+ const hasModified = this.sessionStore.hasModifiedFiles(session_id);
1954
+ if (!hasModified && errors.length === 0 && decisions.length === 0 && notes.length === 0 && observations.length === 0)
1955
+ return;
1939
1956
  const obsValues = observations.slice(0, 5).map((o) => o.entity_value);
1940
1957
  let summaryText;
1941
1958
  let tier = "rule-based";
@@ -376,6 +376,11 @@ class SessionStore {
376
376
  this.db.run("UPDATE sessions SET status = 'completed', ended_at = ? WHERE id = ?", [Date.now(), id]);
377
377
  }
378
378
  insertEntity(entity) {
379
+ if (entity.entity_type === "decision" || entity.entity_type === "observation") {
380
+ const existing = this.db.query("SELECT COUNT(*) as c FROM entities WHERE session_id = ? AND entity_type = ? AND entity_value = ?").get(entity.session_id, entity.entity_type, entity.entity_value);
381
+ if (existing && existing.c > 0)
382
+ return -1;
383
+ }
379
384
  const result = this.db.run(`INSERT INTO entities(session_id, project, tool_name, entity_type, entity_value, context, importance, created_at, prompt_number)
380
385
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
381
386
  entity.session_id,
@@ -407,6 +412,11 @@ class SessionStore {
407
412
  WHERE session_id = ? AND entity_type IN ('file_read','file_modified','file_created')
408
413
  ORDER BY importance DESC, created_at DESC`).all(session_id).map((r) => r.entity_value);
409
414
  }
415
+ hasModifiedFiles(session_id) {
416
+ const row = this.db.query(`SELECT COUNT(*) as c FROM entities
417
+ WHERE session_id = ? AND entity_type IN ('file_modified','file_created') LIMIT 1`).get(session_id);
418
+ return (row?.c ?? 0) > 0;
419
+ }
410
420
  insertNote(note) {
411
421
  this.db.run("INSERT INTO session_notes(session_id, content, created_at) VALUES (?, ?, ?)", [note.session_id, note.content, note.created_at]);
412
422
  }
@@ -1619,6 +1629,9 @@ function safeJson2(text, fallback) {
1619
1629
 
1620
1630
  // src/capture/hook-handler.ts
1621
1631
  import { basename as basename3 } from "path";
1632
+ function stripIdeTags(prompt) {
1633
+ return prompt.replace(/<ide_opened_file>[\s\S]*?<\/ide_opened_file>\s*/g, "").replace(/<ide_selection>[\s\S]*?<\/ide_selection>\s*/g, "").replace(/<system-reminder>[\s\S]*?<\/system-reminder>\s*/g, "").trim();
1634
+ }
1622
1635
  async function handlePostToolUse(hook, project) {
1623
1636
  const store = new SessionStore;
1624
1637
  store.upsertSession({
@@ -1654,14 +1667,15 @@ async function handlePostToolUse(hook, project) {
1654
1667
  async function handleUserPromptSubmit(hook, project) {
1655
1668
  const store = new SessionStore;
1656
1669
  const ltStore = new LongTermStore;
1670
+ const cleanPrompt = stripIdeTags(hook.prompt);
1657
1671
  store.upsertSession({
1658
1672
  id: hook.session_id,
1659
1673
  project,
1660
1674
  started_at: Date.now(),
1661
- user_prompt: hook.prompt.slice(0, 500),
1675
+ user_prompt: cleanPrompt.slice(0, 500) || hook.prompt.slice(0, 500),
1662
1676
  status: "active"
1663
1677
  });
1664
- const promptObs = extractObservationFromPrompt(hook.prompt, hook.session_id, project, 0);
1678
+ const promptObs = extractObservationFromPrompt(cleanPrompt || hook.prompt, hook.session_id, project, 0);
1665
1679
  if (promptObs)
1666
1680
  store.insertEntity({ ...promptObs, project });
1667
1681
  const results = ltStore.search(hook.prompt, 3);
package/dist/index.js CHANGED
@@ -14140,6 +14140,11 @@ class SessionStore {
14140
14140
  this.db.run("UPDATE sessions SET status = 'completed', ended_at = ? WHERE id = ?", [Date.now(), id]);
14141
14141
  }
14142
14142
  insertEntity(entity) {
14143
+ if (entity.entity_type === "decision" || entity.entity_type === "observation") {
14144
+ const existing = this.db.query("SELECT COUNT(*) as c FROM entities WHERE session_id = ? AND entity_type = ? AND entity_value = ?").get(entity.session_id, entity.entity_type, entity.entity_value);
14145
+ if (existing && existing.c > 0)
14146
+ return -1;
14147
+ }
14143
14148
  const result = this.db.run(`INSERT INTO entities(session_id, project, tool_name, entity_type, entity_value, context, importance, created_at, prompt_number)
14144
14149
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
14145
14150
  entity.session_id,
@@ -14171,6 +14176,11 @@ class SessionStore {
14171
14176
  WHERE session_id = ? AND entity_type IN ('file_read','file_modified','file_created')
14172
14177
  ORDER BY importance DESC, created_at DESC`).all(session_id).map((r) => r.entity_value);
14173
14178
  }
14179
+ hasModifiedFiles(session_id) {
14180
+ const row = this.db.query(`SELECT COUNT(*) as c FROM entities
14181
+ WHERE session_id = ? AND entity_type IN ('file_modified','file_created') LIMIT 1`).get(session_id);
14182
+ return (row?.c ?? 0) > 0;
14183
+ }
14174
14184
  insertNote(note) {
14175
14185
  this.db.run("INSERT INTO session_notes(session_id, content, created_at) VALUES (?, ?, ?)", [note.session_id, note.content, note.created_at]);
14176
14186
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-memory-hub",
3
- "version": "0.9.2",
3
+ "version": "0.9.5",
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",