claude-memory-hub 0.5.2 → 0.6.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,86 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
5
5
 
6
6
  ---
7
7
 
8
+ ## [0.6.0] - 2026-04-01
9
+
10
+ Major release: semantic search, resource intelligence, observation capture, CLAUDE.md tracking, LLM summarization.
11
+
12
+ ### Phase 1 — ResourceRegistry + Entity Coverage
13
+
14
+ - **ResourceRegistry** — unified scanner for ALL `.claude` locations: skills (58), agents (36), commands (65), workflows (10), CLAUDE.md. Parses agent frontmatter `name:` for correct resolution (e.g., `ios-developer` → `~/.claude/agent_mobile/ios/AGENT.md`). 3-level token estimation: listing (~50-200), full (200-8000), total (all files on disk)
15
+ - **OverheadReport** — `memory_context_budget` MCP tool now shows: fixed token overhead breakdown, unused skill/agent detection, potential savings recommendations
16
+ - **InjectionValidator** — sanitizes context before `UserPromptSubmit` injection. Strips HTML comments, caps at 4500 chars, filters dead resource recommendations via `filterAliveRecommendations()`
17
+ - **Agent/Skill entities** — `Agent` and `Skill` tool calls now produce `entity_type="decision"` entities (importance 3/2), visible in summarization and compact scoring
18
+ - **Expanded resource types** — `resource_usage` table tracks 8 types: skill, agent, command, workflow, claude_md, memory, mcp_tool, hook (was 5)
19
+ - **Real token costs** — `SmartResourceLoader` uses ResourceRegistry for actual file-size-based estimates instead of hardcoded 500 fallback
20
+
21
+ ### Phase 2 — Schema v3 + Observations + CLAUDE.md Tracking
22
+
23
+ - **Schema migration v3** — entities table rebuilt with `observation` type in CHECK constraint + new `claude_md_registry` table
24
+ - **Observation extractor** — heuristic-based free-form capture from tool output and user prompts. Keywords: IMPORTANT/CRITICAL (importance 4), decision:/NOTE: (3), TODO:/FIXME: (2). Max 1 observation per tool call, capped at 300 chars
25
+ - **CLAUDE.md tracker** — walks from `cwd` to root, finds all CLAUDE.md files, extracts `## sections` + 200-char previews, content-hash change detection (only re-parses on change), injects rule summary into context
26
+ - **Session summarizer** includes top 5 observations in L3 summaries
27
+ - **Vector search** reindexes observation entities alongside decisions and errors
28
+
29
+ ### Phase 3 — LLM Summarization Pipeline
30
+
31
+ - **3-tier fallback** — Tier 1: PostCompact summary (free, already existed). Tier 2: `claude -p ... --print` subprocess with 30s timeout. Tier 3: Rule-based (always available)
32
+ - **Hook recursion guard** — `CLAUDE_MEMORY_HUB_SKIP_HOOKS=1` env var set on CLI subprocess, checked by all 5 hook entry scripts. Prevents infinite loop when CLI summarizer triggers hooks
33
+ - **Configurable** — `CLAUDE_MEMORY_HUB_LLM=auto|cli-only|rule-based` env var. `CLAUDE_MEMORY_HUB_LLM_TIMEOUT_MS` for custom timeout
34
+
35
+ ### Phase 4 — Semantic Search
36
+
37
+ - **Embedding model** — `@huggingface/transformers` with `all-MiniLM-L6-v2` (384-dim, 90MB cached, 9ms warm inference). Lazy-loaded: only imports when first embedding requested. Graceful degradation if package not installed
38
+ - **Pure JS cosine similarity** — no native sqlite-vec binary needed. Fast enough for <1000 docs. Embeddings stored as BLOBs in new `embeddings` table (schema v4)
39
+ - **Hybrid search** — `searchIndex()` now merges FTS5 BM25 + TF-IDF + semantic cosine similarity. Deduplicates by id+type, keeps highest score
40
+ - **Auto-indexing** — session-end hook generates embedding for new summaries automatically
41
+ - **Opt-in** — `CLAUDE_MEMORY_HUB_EMBEDDINGS=auto|disabled` env var. `@huggingface/transformers` is `optionalDependencies` — install failure doesn't break anything
42
+
43
+ ### New Environment Variables
44
+
45
+ | Variable | Default | Description |
46
+ |----------|---------|-------------|
47
+ | `CLAUDE_MEMORY_HUB_LLM` | `auto` | Summarization mode: auto, cli-only, rule-based |
48
+ | `CLAUDE_MEMORY_HUB_LLM_TIMEOUT_MS` | `30000` | CLI summarizer timeout in ms |
49
+ | `CLAUDE_MEMORY_HUB_EMBEDDINGS` | `auto` | Embedding mode: auto, disabled |
50
+ | `CLAUDE_MEMORY_HUB_SKIP_HOOKS` | — | Set to `1` to suppress hooks (internal use) |
51
+
52
+ ### New/Modified Files
53
+
54
+ ```
55
+ NEW:
56
+ src/context/resource-registry.ts — unified resource scanner
57
+ src/context/injection-validator.ts — context sanitization
58
+ src/capture/observation-extractor.ts — free-form observation capture
59
+ src/context/claude-md-tracker.ts — CLAUDE.md scanning + tracking
60
+ src/summarizer/cli-summarizer.ts — Tier 2 CLI summarization
61
+ src/search/embedding-model.ts — lazy @huggingface/transformers
62
+ src/search/semantic-search.ts — cosine similarity search
63
+
64
+ MODIFIED:
65
+ src/db/schema.ts — migrations v3 + v4
66
+ src/types/index.ts — EntityType += observation
67
+ src/capture/entity-extractor.ts — Agent/Skill + observation extraction
68
+ src/capture/hook-handler.ts — registry + validator + CLAUDE.md + observations
69
+ src/context/smart-resource-loader.ts — uses ResourceRegistry
70
+ src/context/resource-tracker.ts — 8 resource types
71
+ src/mcp/tool-handlers.ts — overhead report in context_budget
72
+ src/summarizer/session-summarizer.ts — 3-tier pipeline
73
+ src/search/search-workflow.ts — hybrid FTS5+TF-IDF+semantic
74
+ src/search/vector-search.ts — reindex includes observations+embeddings
75
+ src/db/session-store.ts — getSessionObservations()
76
+ src/hooks-entry/*.ts — SKIP_HOOKS recursion guard
77
+ ```
78
+
79
+ ### Dependencies
80
+
81
+ ```
82
+ KEPT: @modelcontextprotocol/sdk
83
+ ADDED: @huggingface/transformers (optional — semantic search)
84
+ ```
85
+
86
+ ---
87
+
8
88
  ## [0.5.2] - 2026-04-01
9
89
 
10
90
  ### Fixed
package/README.md CHANGED
@@ -45,8 +45,13 @@ Search: Keyword-only, no semantic ranking
45
45
  | Influence what compact preserves | -- | -- | **Yes** |
46
46
  | Save compact output | -- | -- | **Yes** |
47
47
  | Token budget optimization | -- | -- | **Yes** |
48
- | Hybrid search (FTS5 + TF-IDF) | -- | Partial | **Yes** |
48
+ | Semantic search (embeddings) | -- | Chroma (external) | **Yes (offline)** |
49
+ | Hybrid search (FTS5 + TF-IDF + semantic) | -- | Partial | **Yes** |
49
50
  | 3-layer progressive search | -- | Yes | **Yes** |
51
+ | Resource overhead analysis | -- | -- | **Yes** |
52
+ | CLAUDE.md rule tracking | -- | -- | **Yes** |
53
+ | Free-form observation capture | -- | Yes | **Yes** |
54
+ | LLM summarization (3-tier) | -- | Yes (API) | **Yes (free)** |
50
55
  | Browser UI | -- | Yes | **Yes** |
51
56
  | Health monitoring | -- | -- | **Yes** |
52
57
  | Migrate from claude-mem | N/A | N/A | **Yes** |
@@ -127,7 +132,7 @@ Session N+1 → UserPromptSubmit hook fires
127
132
 
128
133
  memory-hub tracks which skills/agents/tools you **actually use**, then recommends only those for future sessions. Rare resources load on demand via SkillTool.
129
134
 
130
- ### Layer 5 — 3-Layer Progressive Search (new in v0.5)
135
+ ### Layer 5 — 3-Layer Progressive Search + Semantic (new in v0.5/v0.6)
131
136
 
132
137
  ```
133
138
  Traditional search: query → ALL full records → 5000+ tokens wasted
@@ -140,7 +145,35 @@ memory-hub search: query → Layer 1 (index) → ~50 tokens/result
140
145
  Token savings: ~80-90% vs. full context
141
146
  ```
142
147
 
143
- Hybrid ranking: FTS5 BM25 for keyword matches + TF-IDF cosine similarity for semantic ranking. Zero external dependencies pure TypeScript implementation.
148
+ Hybrid ranking: FTS5 BM25 (keyword) + TF-IDF (term frequency) + **semantic cosine similarity** (384-dim embeddings, v0.6). "debugging tips" now matches "error fixing" even without shared keywords.
149
+
150
+ ### Layer 6 — Resource Intelligence (new in v0.6)
151
+
152
+ ```
153
+ ResourceRegistry scans ALL .claude locations:
154
+ ~/.claude/skills/ 58 skills → listing + full + total tokens
155
+ ~/.claude/agents/ 36 agents → frontmatter name: resolution
156
+ ~/.claude/agent_mobile/ ios-developer → agent_mobile/ios/AGENT.md
157
+ ~/.claude/commands/ 65 commands → relative path naming
158
+ ~/.claude/workflows/ 10 workflows
159
+ ~/.claude/CLAUDE.md + project CLAUDE.md chain
160
+
161
+ OverheadReport:
162
+ "56/64 skills unused in last 10 sessions → ~1033 listing tokens wasted"
163
+ "CLAUDE.md chain is 3222 tokens"
164
+ ```
165
+
166
+ ### Layer 7 — Observation Capture (new in v0.6)
167
+
168
+ ```
169
+ Tool output contains "IMPORTANT: always pool DB connections"
170
+ → observation entity (importance=4) saved to L2
171
+ → included in session summary
172
+ → searchable across sessions
173
+
174
+ User prompt contains "remember that we use TypeScript strict"
175
+ → observation entity (importance=3) saved to L2
176
+ ```
144
177
 
145
178
  ---
146
179
 
@@ -195,6 +228,9 @@ Hybrid ranking: FTS5 BM25 for keyword matches + TF-IDF cosine similarity for sem
195
228
  │ resource_usage │
196
229
  │ fts_memories │
197
230
  │ tfidf_index │
231
+ │ embeddings │
232
+ │ claude_md_ │
233
+ │ registry │
198
234
  │ health_checks │
199
235
  └────────────────────┘
200
236
  ```
@@ -361,6 +397,7 @@ Migration is idempotent — safe to run multiple times with zero duplicates.
361
397
  | **v0.3.0** | Removed API key requirement, 1-command install |
362
398
  | **v0.4.0** | Smart resource loading, token budget optimization |
363
399
  | **v0.5.0** | Production hardening, hybrid search, 3-layer progressive search, browser UI, health monitoring, claude-mem migration |
400
+ | **v0.6.0** | ResourceRegistry (170 resources), semantic search (384-dim embeddings), observation capture, CLAUDE.md tracking, 3-tier LLM summarization, overhead analysis |
364
401
 
365
402
  See [CHANGELOG.md](CHANGELOG.md) for full details.
366
403
 
@@ -369,13 +406,21 @@ See [CHANGELOG.md](CHANGELOG.md) for full details.
369
406
  ## Dependencies
370
407
 
371
408
  ```
372
- @modelcontextprotocol/sdk MCP stdio server
373
- bun:sqlite Built-in, zero install
409
+ @modelcontextprotocol/sdk MCP stdio server (required)
410
+ bun:sqlite Built-in, zero install
411
+ @huggingface/transformers Semantic search embeddings (optional)
374
412
  ```
375
413
 
376
- That's it. **One npm package.** The other is built into Bun.
414
+ **Two npm packages + one optional.** No Python. No Chroma. No HTTP server. No API key. No Docker.
415
+
416
+ ### Environment Variables
377
417
 
378
- No Python. No Chroma. No HTTP server. No API key. No Docker.
418
+ | Variable | Default | Description |
419
+ |----------|---------|-------------|
420
+ | `CLAUDE_MEMORY_HUB_LLM` | `auto` | Summarization: auto, cli-only, rule-based |
421
+ | `CLAUDE_MEMORY_HUB_LLM_TIMEOUT_MS` | `30000` | CLI summarizer timeout |
422
+ | `CLAUDE_MEMORY_HUB_EMBEDDINGS` | `auto` | Embeddings: auto, disabled |
423
+ | `CMH_LOG_LEVEL` | `info` | Log level: debug, info, warn, error |
379
424
 
380
425
  ---
381
426
 
package/dist/cli.js CHANGED
@@ -1,9 +1,22 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
3
5
  var __defProp = Object.defineProperty;
4
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
7
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __toESM = (mod, isNodeMode, target) => {
10
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
11
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
12
+ for (let key of __getOwnPropNames(mod))
13
+ if (!__hasOwnProp.call(to, key))
14
+ __defProp(to, key, {
15
+ get: () => mod[key],
16
+ enumerable: true
17
+ });
18
+ return to;
19
+ };
7
20
  var __moduleCache = /* @__PURE__ */ new WeakMap;
8
21
  var __toCommonJS = (from) => {
9
22
  var entry = __moduleCache.get(from), desc;
@@ -158,6 +171,68 @@ function applyMigrations(db) {
158
171
  db.run("INSERT OR IGNORE INTO schema_versions(version, applied_at) VALUES (2, ?)", [Date.now()]);
159
172
  log.info("Migration v2 complete");
160
173
  }
174
+ if (currentVersion < 3) {
175
+ log.info("Applying migration v3: observation entity type + claude_md_registry");
176
+ db.transaction(() => {
177
+ db.run(`
178
+ CREATE TABLE entities_v3 (
179
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
180
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
181
+ project TEXT NOT NULL,
182
+ tool_name TEXT NOT NULL,
183
+ entity_type TEXT NOT NULL
184
+ CHECK(entity_type IN ('file_read','file_modified','file_created','error','decision','observation')),
185
+ entity_value TEXT NOT NULL,
186
+ context TEXT,
187
+ importance INTEGER NOT NULL DEFAULT 1
188
+ CHECK(importance BETWEEN 1 AND 5),
189
+ created_at INTEGER NOT NULL,
190
+ prompt_number INTEGER NOT NULL DEFAULT 0,
191
+ discovery_tokens INTEGER NOT NULL DEFAULT 0
192
+ )
193
+ `);
194
+ db.run(`INSERT INTO entities_v3 SELECT * FROM entities`);
195
+ db.run(`DROP TABLE entities`);
196
+ db.run(`ALTER TABLE entities_v3 RENAME TO entities`);
197
+ db.run(`CREATE INDEX IF NOT EXISTS idx_entities_session ON entities(session_id)`);
198
+ db.run(`CREATE INDEX IF NOT EXISTS idx_entities_project ON entities(project)`);
199
+ db.run(`CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(entity_type)`);
200
+ db.run(`CREATE INDEX IF NOT EXISTS idx_entities_value ON entities(entity_value)`);
201
+ db.run(`CREATE INDEX IF NOT EXISTS idx_entities_created ON entities(created_at DESC)`);
202
+ db.run(`
203
+ CREATE TABLE IF NOT EXISTS claude_md_registry (
204
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
205
+ path TEXT NOT NULL UNIQUE,
206
+ project TEXT NOT NULL,
207
+ content_hash TEXT NOT NULL,
208
+ sections_json TEXT NOT NULL DEFAULT '[]',
209
+ last_seen INTEGER NOT NULL,
210
+ token_cost INTEGER NOT NULL DEFAULT 0
211
+ )
212
+ `);
213
+ db.run(`CREATE INDEX IF NOT EXISTS idx_cmr_project ON claude_md_registry(project)`);
214
+ db.run(`CREATE INDEX IF NOT EXISTS idx_cmr_path ON claude_md_registry(path)`);
215
+ })();
216
+ db.run("INSERT OR IGNORE INTO schema_versions(version, applied_at) VALUES (3, ?)", [Date.now()]);
217
+ log.info("Migration v3 complete");
218
+ }
219
+ if (currentVersion < 4) {
220
+ log.info("Applying migration v4: embeddings table for semantic search");
221
+ db.run(`
222
+ CREATE TABLE IF NOT EXISTS embeddings (
223
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
224
+ doc_type TEXT NOT NULL CHECK(doc_type IN ('summary','entity','note')),
225
+ doc_id INTEGER NOT NULL,
226
+ model TEXT NOT NULL DEFAULT 'all-MiniLM-L6-v2',
227
+ vector BLOB NOT NULL,
228
+ created_at INTEGER NOT NULL
229
+ )
230
+ `);
231
+ db.run(`CREATE UNIQUE INDEX IF NOT EXISTS idx_embeddings_doc ON embeddings(doc_type, doc_id)`);
232
+ db.run(`CREATE INDEX IF NOT EXISTS idx_embeddings_model ON embeddings(model)`);
233
+ db.run("INSERT OR IGNORE INTO schema_versions(version, applied_at) VALUES (4, ?)", [Date.now()]);
234
+ log.info("Migration v4 complete");
235
+ }
161
236
  }
162
237
  function getDatabase() {
163
238
  if (!_db) {
@@ -453,6 +528,155 @@ var init_monitor = __esm(() => {
453
528
  log2 = createLogger("health");
454
529
  });
455
530
 
531
+ // src/search/embedding-model.ts
532
+ class EmbeddingModel {
533
+ pipeline = null;
534
+ loading = null;
535
+ available = true;
536
+ async embed(text) {
537
+ if (!this.available)
538
+ return null;
539
+ await this.ensureLoaded();
540
+ if (!this.pipeline)
541
+ return null;
542
+ try {
543
+ const result = await this.pipeline(text, { pooling: "mean", normalize: true });
544
+ return new Float32Array(result.data);
545
+ } catch (err) {
546
+ log3.error("embed failed", { error: String(err) });
547
+ return null;
548
+ }
549
+ }
550
+ async embedBatch(texts) {
551
+ if (!this.available || texts.length === 0)
552
+ return texts.map(() => null);
553
+ await this.ensureLoaded();
554
+ if (!this.pipeline)
555
+ return texts.map(() => null);
556
+ const results = [];
557
+ for (const text of texts) {
558
+ try {
559
+ const result = await this.pipeline(text, { pooling: "mean", normalize: true });
560
+ results.push(new Float32Array(result.data));
561
+ } catch {
562
+ results.push(null);
563
+ }
564
+ }
565
+ return results;
566
+ }
567
+ get isAvailable() {
568
+ return this.available && this.pipeline !== null;
569
+ }
570
+ get isLoadAttempted() {
571
+ return this.loading !== null;
572
+ }
573
+ async ensureLoaded() {
574
+ if (this.pipeline || !this.available)
575
+ return;
576
+ if (!this.loading)
577
+ this.loading = this.loadModel();
578
+ await this.loading;
579
+ }
580
+ async loadModel() {
581
+ if (process.env["CLAUDE_MEMORY_HUB_EMBEDDINGS"] === "disabled") {
582
+ this.available = false;
583
+ return;
584
+ }
585
+ try {
586
+ const { pipeline, env } = await import("@huggingface/transformers");
587
+ env.allowLocalModels = true;
588
+ env.allowRemoteModels = true;
589
+ const t0 = Date.now();
590
+ this.pipeline = await pipeline("feature-extraction", MODEL_NAME, { dtype: "fp32" });
591
+ log3.info("Embedding model loaded", { model: MODEL_NAME, ms: Date.now() - t0 });
592
+ } catch (err) {
593
+ log3.warn("Embedding model unavailable", { error: String(err) });
594
+ this.available = false;
595
+ }
596
+ }
597
+ }
598
+ var log3, MODEL_NAME = "Xenova/all-MiniLM-L6-v2", EMBEDDING_DIM = 384, embeddingModel;
599
+ var init_embedding_model = __esm(() => {
600
+ init_logger();
601
+ log3 = createLogger("embedding-model");
602
+ embeddingModel = new EmbeddingModel;
603
+ });
604
+
605
+ // src/search/semantic-search.ts
606
+ var exports_semantic_search = {};
607
+ __export(exports_semantic_search, {
608
+ semanticSearch: () => semanticSearch,
609
+ reindexAllEmbeddings: () => reindexAllEmbeddings,
610
+ indexEmbedding: () => indexEmbedding
611
+ });
612
+ async function indexEmbedding(docType, docId, text, db) {
613
+ const vector = await embeddingModel.embed(text);
614
+ if (!vector)
615
+ return;
616
+ const d = db ?? getDatabase();
617
+ const blob = Buffer.from(vector.buffer);
618
+ d.run(`INSERT INTO embeddings(doc_type, doc_id, model, vector, created_at)
619
+ VALUES (?, ?, 'all-MiniLM-L6-v2', ?, ?)
620
+ ON CONFLICT(doc_type, doc_id) DO UPDATE SET
621
+ vector = excluded.vector,
622
+ created_at = excluded.created_at`, [docType, docId, blob, Date.now()]);
623
+ }
624
+ async function semanticSearch(query, limit = 10, db) {
625
+ const queryVec = await embeddingModel.embed(query);
626
+ if (!queryVec)
627
+ return [];
628
+ const d = db ?? getDatabase();
629
+ const rows = d.query("SELECT doc_type, doc_id, vector FROM embeddings").all();
630
+ if (rows.length === 0)
631
+ return [];
632
+ const scored = [];
633
+ for (const row of rows) {
634
+ const docVec = new Float32Array(row.vector.buffer, row.vector.byteOffset, EMBEDDING_DIM);
635
+ const score = cosineSimilarity(queryVec, docVec);
636
+ if (score > 0.2) {
637
+ scored.push({ doc_type: row.doc_type, doc_id: row.doc_id, score });
638
+ }
639
+ }
640
+ scored.sort((a, b) => b.score - a.score);
641
+ return scored.slice(0, limit);
642
+ }
643
+ async function reindexAllEmbeddings(db) {
644
+ if (!embeddingModel.isAvailable && embeddingModel.isLoadAttempted)
645
+ return;
646
+ const d = db ?? getDatabase();
647
+ log4.info("Starting embedding reindex...");
648
+ const summaries = d.query("SELECT id, summary, files_touched, decisions FROM long_term_summaries").all();
649
+ let indexed = 0;
650
+ for (const s of summaries) {
651
+ const text = [s.summary, s.files_touched, s.decisions].join(" ");
652
+ await indexEmbedding("summary", s.id, text, d);
653
+ indexed++;
654
+ if (indexed % 50 === 0)
655
+ log4.info("Embedding reindex progress", { indexed, total: summaries.length });
656
+ }
657
+ const entities = d.query("SELECT id, entity_value, context FROM entities WHERE entity_type IN ('decision', 'error', 'observation')").all();
658
+ for (const e of entities) {
659
+ const text = [e.entity_value, e.context || ""].join(" ");
660
+ await indexEmbedding("entity", e.id, text, d);
661
+ indexed++;
662
+ }
663
+ log4.info("Embedding reindex complete", { summaries: summaries.length, entities: entities.length });
664
+ }
665
+ function cosineSimilarity(a, b) {
666
+ let dot = 0;
667
+ for (let i = 0;i < a.length; i++) {
668
+ dot += a[i] * b[i];
669
+ }
670
+ return dot;
671
+ }
672
+ var log4;
673
+ var init_semantic_search = __esm(() => {
674
+ init_schema();
675
+ init_embedding_model();
676
+ init_logger();
677
+ log4 = createLogger("semantic-search");
678
+ });
679
+
456
680
  // src/search/vector-search.ts
457
681
  var exports_vector_search = {};
458
682
  __export(exports_vector_search, {
@@ -501,9 +725,9 @@ function rebuildIDF(db) {
501
725
  WHERE t2.term = tfidf_index.term
502
726
  )
503
727
  `, [totalDocs]);
504
- log3.info("IDF rebuilt", { totalDocs });
728
+ log5.info("IDF rebuilt", { totalDocs });
505
729
  } catch (e) {
506
- log3.error("IDF rebuild failed", { error: String(e) });
730
+ log5.error("IDF rebuild failed", { error: String(e) });
507
731
  }
508
732
  }
509
733
  function vectorSearch(query, limit = 10, docTypeFilter, db) {
@@ -526,31 +750,32 @@ function vectorSearch(query, limit = 10, docTypeFilter, db) {
526
750
  `).all(...queryTokens, limit);
527
751
  return results;
528
752
  } catch (e) {
529
- log3.error("Vector search failed", { error: String(e) });
753
+ log5.error("Vector search failed", { error: String(e) });
530
754
  return [];
531
755
  }
532
756
  }
533
757
  function reindexAll(db) {
534
758
  const d = db ?? getDatabase();
535
- log3.info("Starting full reindex...");
759
+ log5.info("Starting full reindex...");
536
760
  const summaries = d.query("SELECT id, summary, files_touched, decisions FROM long_term_summaries").all();
537
761
  for (const s of summaries) {
538
762
  const text = [s.summary, s.files_touched, s.decisions].join(" ");
539
763
  indexDocument("summary", s.id, text, d);
540
764
  }
541
- const entities = d.query("SELECT id, entity_value, context FROM entities WHERE entity_type IN ('decision', 'error')").all();
765
+ const entities = d.query("SELECT id, entity_value, context FROM entities WHERE entity_type IN ('decision', 'error', 'observation')").all();
542
766
  for (const e of entities) {
543
767
  const text = [e.entity_value, e.context || ""].join(" ");
544
768
  indexDocument("entity", e.id, text, d);
545
769
  }
546
770
  rebuildIDF(d);
547
- log3.info("Full reindex complete", { summaries: summaries.length, entities: entities.length });
771
+ Promise.resolve().then(() => (init_semantic_search(), exports_semantic_search)).then(({ reindexAllEmbeddings: reindexAllEmbeddings2 }) => reindexAllEmbeddings2(d)).catch(() => {});
772
+ log5.info("Full reindex complete", { summaries: summaries.length, entities: entities.length });
548
773
  }
549
- var log3, STOP_WORDS;
774
+ var log5, STOP_WORDS;
550
775
  var init_vector_search = __esm(() => {
551
776
  init_schema();
552
777
  init_logger();
553
- log3 = createLogger("vector-search");
778
+ log5 = createLogger("vector-search");
554
779
  STOP_WORDS = new Set([
555
780
  "the",
556
781
  "a",
@@ -665,7 +890,7 @@ var init_vector_search = __esm(() => {
665
890
  });
666
891
 
667
892
  // src/search/search-workflow.ts
668
- function searchIndex(query, opts = {}, db) {
893
+ async function searchIndex(query, opts = {}, db) {
669
894
  const d = db ?? getDatabase();
670
895
  const limit = opts.limit ?? 20;
671
896
  const offset = opts.offset ?? 0;
@@ -707,9 +932,36 @@ function searchIndex(query, opts = {}, db) {
707
932
  }
708
933
  }
709
934
  }
935
+ try {
936
+ const semResults = await semanticSearch(query, limit, d);
937
+ for (const sr of semResults) {
938
+ const key = `${sr.doc_type}:${sr.doc_id}`;
939
+ if (results.some((r) => `${r.type}:${r.id}` === key))
940
+ continue;
941
+ if (sr.doc_type === "summary") {
942
+ const row = d.prepare("SELECT id, project, SUBSTR(summary, 1, 80) as summary, created_at FROM long_term_summaries WHERE id = ?").get(sr.doc_id);
943
+ if (row) {
944
+ results.push({ id: row.id, type: "summary", title: row.summary, project: row.project, created_at: row.created_at, score: sr.score });
945
+ }
946
+ } else if (sr.doc_type === "entity") {
947
+ const row = d.prepare("SELECT id, project, SUBSTR(entity_value, 1, 80) as entity_value, created_at FROM entities WHERE id = ?").get(sr.doc_id);
948
+ if (row) {
949
+ results.push({ id: row.id, type: "entity", title: row.entity_value, project: row.project, created_at: row.created_at, score: sr.score });
950
+ }
951
+ }
952
+ }
953
+ } catch {}
710
954
  const filtered = opts.project ? results.filter((r) => r.project === opts.project) : results;
711
- filtered.sort((a, b) => b.score - a.score);
712
- return filtered.slice(0, limit);
955
+ const deduped = new Map;
956
+ for (const r of filtered) {
957
+ const key = `${r.type}:${r.id}`;
958
+ const existing = deduped.get(key);
959
+ if (!existing || r.score > existing.score)
960
+ deduped.set(key, r);
961
+ }
962
+ const merged = [...deduped.values()];
963
+ merged.sort((a, b) => b.score - a.score);
964
+ return merged.slice(0, limit);
713
965
  }
714
966
  function sanitizeFtsQuery(query) {
715
967
  const words = query.trim().split(/\s+/).filter(Boolean).map((w) => w.replace(/["*^():{}[\]]/g, "").trim()).filter((w) => w.length > 1);
@@ -721,12 +973,13 @@ function sanitizeFtsQuery(query) {
721
973
  const last = words[words.length - 1];
722
974
  return [...head, `"${last}"*`].join(" ");
723
975
  }
724
- var log4;
976
+ var log6;
725
977
  var init_search_workflow = __esm(() => {
726
978
  init_schema();
727
979
  init_vector_search();
980
+ init_semantic_search();
728
981
  init_logger();
729
- log4 = createLogger("search-workflow");
982
+ log6 = createLogger("search-workflow");
730
983
  });
731
984
 
732
985
  // src/ui/viewer.ts
@@ -734,7 +987,7 @@ var exports_viewer = {};
734
987
  __export(exports_viewer, {
735
988
  startViewer: () => startViewer
736
989
  });
737
- function handleApi(url) {
990
+ async function handleApi(url) {
738
991
  const db = getDatabase();
739
992
  const path = url.pathname;
740
993
  try {
@@ -753,7 +1006,7 @@ function handleApi(url) {
753
1006
  const limit = parseInt(url.searchParams.get("limit") || "20");
754
1007
  const offset = parseInt(url.searchParams.get("offset") || "0");
755
1008
  const project = url.searchParams.get("project");
756
- return json(searchIndex(query, { limit, offset, ...project ? { project } : {} }, db));
1009
+ return json(await searchIndex(query, { limit, offset, ...project ? { project } : {} }, db));
757
1010
  }
758
1011
  if (path === "/api/sessions") {
759
1012
  const limit = parseInt(url.searchParams.get("limit") || "50");
@@ -780,7 +1033,7 @@ function handleApi(url) {
780
1033
  }
781
1034
  return json({ error: "Not found" }, 404);
782
1035
  } catch (e) {
783
- log5.error("API error", { path, error: String(e) });
1036
+ log7.error("API error", { path, error: String(e) });
784
1037
  return json({ error: String(e) }, 500);
785
1038
  }
786
1039
  }
@@ -800,7 +1053,7 @@ function startViewer() {
800
1053
  return handleApi(url);
801
1054
  return new Response(HTML, { headers: { "Content-Type": "text/html" } });
802
1055
  } catch (e) {
803
- log5.error("Server fetch error", { error: String(e) });
1056
+ log7.error("Server fetch error", { error: String(e) });
804
1057
  return new Response(JSON.stringify({ error: String(e) }), {
805
1058
  status: 500,
806
1059
  headers: { "Content-Type": "application/json" }
@@ -808,7 +1061,7 @@ function startViewer() {
808
1061
  }
809
1062
  },
810
1063
  error(err) {
811
- log5.error("Server error", { error: String(err) });
1064
+ log7.error("Server error", { error: String(err) });
812
1065
  return new Response(JSON.stringify({ error: String(err) }), {
813
1066
  status: 500,
814
1067
  headers: { "Content-Type": "application/json" }
@@ -816,9 +1069,9 @@ function startViewer() {
816
1069
  }
817
1070
  });
818
1071
  console.log(`claude-memory-hub viewer running at http://localhost:${server.port}`);
819
- log5.info("Viewer started", { port: server.port });
1072
+ log7.info("Viewer started", { port: server.port });
820
1073
  }
821
- var log5, PORT = 37888, HTML = `<!DOCTYPE html>
1074
+ var log7, PORT = 37888, HTML = `<!DOCTYPE html>
822
1075
  <html lang="en">
823
1076
  <head>
824
1077
  <meta charset="utf-8">
@@ -1078,7 +1331,7 @@ var init_viewer = __esm(() => {
1078
1331
  init_logger();
1079
1332
  init_monitor();
1080
1333
  init_search_workflow();
1081
- log5 = createLogger("viewer");
1334
+ log7 = createLogger("viewer");
1082
1335
  });
1083
1336
 
1084
1337
  // src/cli/main.ts