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 +80 -0
- package/README.md +52 -7
- package/dist/cli.js +274 -21
- package/dist/hooks/post-compact.js +1013 -34
- package/dist/hooks/post-tool-use.js +887 -33
- package/dist/hooks/pre-compact.js +1013 -34
- package/dist/hooks/session-end.js +1106 -35
- package/dist/hooks/user-prompt-submit.js +887 -33
- package/dist/index.js +1149 -425
- package/package.json +11 -6
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
|
-
|
|
|
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
|
|
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
|
|
373
|
-
bun:sqlite
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
728
|
+
log5.info("IDF rebuilt", { totalDocs });
|
|
505
729
|
} catch (e) {
|
|
506
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
774
|
+
var log5, STOP_WORDS;
|
|
550
775
|
var init_vector_search = __esm(() => {
|
|
551
776
|
init_schema();
|
|
552
777
|
init_logger();
|
|
553
|
-
|
|
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
|
-
|
|
712
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1072
|
+
log7.info("Viewer started", { port: server.port });
|
|
820
1073
|
}
|
|
821
|
-
var
|
|
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
|
-
|
|
1334
|
+
log7 = createLogger("viewer");
|
|
1082
1335
|
});
|
|
1083
1336
|
|
|
1084
1337
|
// src/cli/main.ts
|