claude-memory-layer 1.0.18 → 1.0.20

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.
Files changed (42) hide show
  1. package/config/kpi-thresholds.json +7 -0
  2. package/dist/cli/index.js +532 -79
  3. package/dist/cli/index.js.map +3 -3
  4. package/dist/core/index.js +49 -4
  5. package/dist/core/index.js.map +2 -2
  6. package/dist/hooks/post-tool-use.js +140 -3
  7. package/dist/hooks/post-tool-use.js.map +2 -2
  8. package/dist/hooks/session-end.js +140 -3
  9. package/dist/hooks/session-end.js.map +2 -2
  10. package/dist/hooks/session-start.js +140 -3
  11. package/dist/hooks/session-start.js.map +2 -2
  12. package/dist/hooks/stop.js +140 -3
  13. package/dist/hooks/stop.js.map +2 -2
  14. package/dist/hooks/user-prompt-submit.js +379 -34
  15. package/dist/hooks/user-prompt-submit.js.map +3 -3
  16. package/dist/server/api/index.js +467 -34
  17. package/dist/server/api/index.js.map +3 -3
  18. package/dist/server/index.js +474 -41
  19. package/dist/server/index.js.map +3 -3
  20. package/dist/services/memory-service.js +140 -3
  21. package/dist/services/memory-service.js.map +2 -2
  22. package/dist/ui/app.js +362 -4
  23. package/dist/ui/index.html +90 -0
  24. package/dist/ui/style.css +41 -0
  25. package/memory/_index.md +3 -0
  26. package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
  27. package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
  28. package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
  29. package/package.json +3 -2
  30. package/scripts/delete-unknown-projects.js +154 -0
  31. package/src/cli/index.ts +23 -1
  32. package/src/core/embedder.ts +3 -2
  33. package/src/core/sqlite-event-store.ts +32 -0
  34. package/src/core/types.ts +2 -2
  35. package/src/core/vector-store.ts +20 -0
  36. package/src/hooks/user-prompt-submit.ts +225 -29
  37. package/src/server/api/events.ts +7 -0
  38. package/src/server/api/stats.ts +346 -0
  39. package/src/services/memory-service.ts +119 -2
  40. package/src/ui/app.js +362 -4
  41. package/src/ui/index.html +90 -0
  42. package/src/ui/style.css +41 -0
package/dist/ui/style.css CHANGED
@@ -329,6 +329,23 @@ body {
329
329
  opacity: 0.5;
330
330
  }
331
331
 
332
+ .kpi-grid .stat-card .stat-value {
333
+ font-size: 28px;
334
+ }
335
+ .kpi-grid .stat-card .stat-label {
336
+ font-size: 12px;
337
+ }
338
+ .kpi-delta {
339
+ font-size: 11px;
340
+ margin-top: -4px;
341
+ margin-bottom: 8px;
342
+ color: var(--text-muted);
343
+ font-weight: 600;
344
+ }
345
+ .kpi-delta.good { color: var(--success); }
346
+ .kpi-delta.bad { color: var(--error); }
347
+ .kpi-delta.neutral { color: var(--text-muted); }
348
+
332
349
  .stat-value {
333
350
  font-size: 36px;
334
351
  font-weight: 700;
@@ -485,8 +502,11 @@ body {
485
502
  }
486
503
 
487
504
  .type-user { background: rgba(59, 130, 246, 0.2); color: #60A5FA; }
505
+ .type-user-prompt { background: rgba(59, 130, 246, 0.2); color: #60A5FA; }
488
506
  .type-agent { background: rgba(16, 185, 129, 0.2); color: #34D399; }
507
+ .type-agent-response { background: rgba(16, 185, 129, 0.2); color: #34D399; }
489
508
  .type-tool { background: rgba(245, 158, 11, 0.2); color: #FBBF24; }
509
+ .type-tool-observation { background: rgba(245, 158, 11, 0.2); color: #FBBF24; }
490
510
  .type-system { background: rgba(139, 92, 246, 0.2); color: #A78BFA; }
491
511
 
492
512
  .event-time {
@@ -580,6 +600,10 @@ body {
580
600
  color: var(--accent-primary);
581
601
  font-weight: 600;
582
602
  }
603
+ .sort-btn:disabled {
604
+ opacity: 0.35;
605
+ cursor: not-allowed;
606
+ }
583
607
 
584
608
  /* Access Badge */
585
609
  .access-badge {
@@ -594,6 +618,23 @@ body {
594
618
  font-weight: 600;
595
619
  }
596
620
 
621
+ .adherence-badge {
622
+ font-size: 10px;
623
+ padding: 3px 7px;
624
+ border-radius: 6px;
625
+ font-weight: 700;
626
+ text-transform: uppercase;
627
+ letter-spacing: 0.2px;
628
+ }
629
+ .adherence-checked {
630
+ background: rgba(52, 211, 153, 0.14);
631
+ color: #34D399;
632
+ }
633
+ .adherence-skipped {
634
+ background: rgba(148, 163, 184, 0.14);
635
+ color: #94A3B8;
636
+ }
637
+
597
638
  /* Section Label */
598
639
  .section-label {
599
640
  font-size: 12px;
package/memory/_index.md CHANGED
@@ -4,6 +4,7 @@ Generated automatically by MarkdownMirror.
4
4
 
5
5
  - memory/.claude-plugin/commands/2026-02-25.md
6
6
  - memory/agent_response/uncategorized/2026-02-26.md
7
+ - memory/agent_response/uncategorized/2026-03-03.md
7
8
  - memory/default/uncategorized/2026-02-25.md
8
9
  - memory/node_modules/abbrev/2026-02-25.md
9
10
  - memory/node_modules/abort-controller/2026-02-25.md
@@ -393,6 +394,7 @@ Generated automatically by MarkdownMirror.
393
394
  - memory/node_modules/yocto-queue/2026-02-25.md
394
395
  - memory/node_modules/zod/2026-02-25.md
395
396
  - memory/session_summary/uncategorized/2026-02-26.md
397
+ - memory/session_summary/uncategorized/2026-03-03.md
396
398
  - memory/specs/20260207-dashboard-upgrade/2026-02-25.md
397
399
  - memory/specs/citations-system/2026-02-25.md
398
400
  - memory/specs/endless-mode/2026-02-25.md
@@ -406,4 +408,5 @@ Generated automatically by MarkdownMirror.
406
408
  - memory/specs/vector-outbox-v2/2026-02-25.md
407
409
  - memory/specs/web-viewer-ui/2026-02-25.md
408
410
  - memory/tool_observation/uncategorized/2026-02-26.md
411
+ - memory/tool_observation/uncategorized/2026-03-03.md
409
412
  - memory/user_prompt/uncategorized/2026-02-26.md
@@ -0,0 +1,14 @@
1
+
2
+ ## 2026-03-03T14:04:37.701Z | 55805b7d-13d2-4e6e-9c1d-bcdf1f1259f9
3
+ - type: agent_response
4
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
5
+ 커밋 완료했습니다.
6
+
7
+ **커밋 `b37aa13`** - `feat(cli): expand hook coverage to all lifecycle events`
8
+
9
+ 변경 내용:
10
+ - `src/cli/index.ts`: `SessionStart`, `Stop`, `SessionEnd` 훅 추가, `makeHook` 헬퍼로 리팩토링, `REQUIRED_HOOK_FILES` 상수 및 `hasHook` 헬퍼 추가, install/uninstall/status 명령 업데이트
11
+ - `scripts/delete-unknown-projects.js`: 세션 레지스트리에 없는 프로젝트 디렉토리 정리 스크립트 (신규)
12
+ - `package.json` / `package-lock.json`: 버전 1.0.18, `ops:projects:clean-unknown` 스크립트 추가
13
+
14
+ 제외된 파일 (커밋 불포함): `.npm-cache/`, `bootstrap-kb/`, `claude-memory-layer-1.0.14.tgz`, `d.sh`, `deploy.sh`, `memory/`, `yarn.lock` (빌드 아티팩트 및 로컬 파일)
@@ -0,0 +1,5 @@
1
+
2
+ ## 2026-03-03T14:04:47.970Z | 7dfbcdd9-f49f-482d-a0c6-9779331cc5cf
3
+ - type: session_summary
4
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
5
+ Session with 0 user prompts and 1 responses.
@@ -0,0 +1,21 @@
1
+
2
+ ## 2026-03-03T14:03:38.204Z | 46d8dc3c-a7ce-4602-ae18-daf4ed0266f1
3
+ - type: tool_observation
4
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
5
+ {"toolName":"Skill","toolInput":{"skill":"commit"},"toolOutput":"{\"success\":true,\"commandName\":\"commit\"}","durationMs":0,"success":true,"metadata":{"turnId":"5eaeff19-dda2-4bd7-a905-9009b5786672"}}
6
+ ## 2026-03-03T14:03:43.897Z | ae933ad8-5f37-4a0b-9028-85bb29bdb6fc
7
+ - type: tool_observation
8
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
9
+ {"toolName":"Bash","toolInput":{"command":"git status && git diff && git log --oneline -5","description":"Check git status, diff, and recent commits"},"toolOutput":"On branch main\nYour branch is up to date with 'origin/main'.\n\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\tmodified: package-lock.json\n\tmodified: package.json\n\tmodified: src/cli/index.ts\n\nUntracked files:\n (use \"git add <file>...\" to include in what will be committed)\n\t.npm-cache/\n\tbootstrap-kb/\n\tclaude-memory-layer-1.0.14.tgz\n\td.sh\n\tdeploy.sh\n\tmemory/\n\tscripts/delete-unknown-projects.js\n\tyarn.lock\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\ndiff --git package-lock.json package-lock.json\nindex c01b2e9..1181958 100644\n--- package-lock.json\n+++ package-lock.json\n@@ -1,12 +1,12 @@\n {\n \"name\": \"claude-memory-layer\",\n- \"version\": \"1.0.15\",\n+ \"version\": \"1.0.18\",\n \"lockfileVersion\": 3,\n \"requires\": true,\n \"packages\": {\n \"\": {\n \"name\": \"claude-memory-layer\",\n- \"version\": \"1.0.15\",\n+ \"version\": \"1.0.18\",\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@hono/node-server\": \"^1.13.0\",\ndiff --git package.json package.json\nindex bd41ac1..453d235 100644\n--- package.json\n+++ package.json\n@@ -1,6 +1,6 @@\n {\n \"name\": \"claude-memory-layer\",\n- \"version\": \"1.0.15\",\n+ \"version\": \"1.0.18\",\n\n... [121 lines truncated] ...\n\n delete settings.hooks.PostToolUse;\n+ delete settings.hooks.Stop;\n+ delete settings.hooks.SessionEnd;\n \n // Clean up empty hooks object\n if (Object.keys(settings.hooks).length === 0) {\n@@ -219,19 +238,22 @@ program\n console.log('\\n🧠 Claude Memory Layer Status\\n');\n \n // Check hooks\n- const hasUserPromptHook = settings.hooks?.UserPromptSubmit?.some(h =>\n- h.hooks?.some(hook => hook.command?.includes('user-prompt-submit'))\n- );\n- const hasPostToolHook = settings.hooks?.PostToolUse?.some(h =>\n- h.hooks?.some(hook => hook.command?.includes('post-tool-use'))\n- );\n+ const hasSessionStartHook = hasHook(settings, 'SessionStart', 'session-start');\n+ const hasUserPromptHook = hasHook(settings, 'UserPromptSubmit', 'user-prompt-submit');\n+ const hasPostToolHook = hasHook(settings, 'PostToolUse', 'post-tool-use');\n+ const hasStopHook = hasHook(settings, 'Stop', 'stop');\n+ const hasSessionEndHook = hasHook(settings, 'SessionEnd', 'session-end');\n \n console.log('Hooks:');\n+ console.log(` SessionStart: ${hasSessionStartHook ? '✅ Installed' : '❌ Not installed'}`);\n console.log(` UserPromptSubmit: ${hasUserPromptHook ? '✅ Installed' : '❌ Not installed'}`);\n console.log(` PostToolUse: ${hasPostToolHook ? '✅ Installed' : '❌ Not installed'}`);\n+ console.log(` Stop: ${hasStopHook ? '✅ Installed' : '❌ Not installed'}`);\n+ console.log(` SessionEnd: ${hasSessionEndHook ? '✅ Installed' : '❌ Not installed'}`);\n \n // Check plugin files\n- const hooksExist = fs.existsSync(path.join(pluginPath, 'hooks', 'user-prompt-submit.js'));\n+ const hooksExist = REQUIRED_HOOK_FILES\n+ .every((file) => fs.existsSync(path.join(pluginPath, 'hooks', file)));\n console.log(`\\nPlugin files: ${hooksExist ? '✅ Found' : '❌ Not found'}`);\n console.log(` Path: ${pluginPath}`);\n \n@@ -239,7 +261,7 @@ program\n const dashboardRunning = await isServerRunning(37777);\n console.log(`\\nDashboard: ${dashboardRunning ? '✅ Running at http://localhost:37777' : '⏹️ Not running'}`);\n \n- if (!hasUserPromptHook || !hasPostToolHook) {\n+ if (!hasSessionStartHook || !hasUserPromptHook || !hasPostToolHook || !hasStopHook || !hasSessionEndHook) {\n console.log('\\n💡 Run \"claude-memory-layer install\" to set up hooks.\\n');\n } else {\n console.log('\\n✅ Plugin is fully installed and configured.\\n');\n257241b fix(hook): use lightweight service in session-start\n60d59c6 fix(api): handle missing retrieval_traces table in read-only mode\n1ab58f2 feat(build): inject version from package.json at build time\n4d48779 tune(hook): raise default injection threshold with fallback retry for recall\n3c618db fix(import): normalize underscore/hyphen path variants for project folder matching","durationMs":0,"success":true,"metadata":{"command":"git status","turnId":"5eaeff19-dda2-4bd7-a905-9009b5786672"}}
10
+ ## 2026-03-03T14:03:55.698Z | 101afa33-2fd1-4008-88b8-c824465ccd69
11
+ - type: tool_observation
12
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
13
+ {"toolName":"Bash","toolInput":{"command":"cat scripts/delete-unknown-projects.js","description":"Check new script content"},"toolOutput":"#!/usr/bin/env node\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nfunction parseArgs(argv) {\n const result = {\n apply: false,\n hashes: []\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const arg = argv[i];\n if (arg === '--apply') {\n result.apply = true;\n continue;\n }\n if (arg === '--hash' && i + 1 < argv.length) {\n result.hashes.push(String(argv[i + 1]).trim().toLowerCase());\n i += 1;\n continue;\n }\n if (arg.startsWith('--hash=')) {\n result.hashes.push(arg.slice('--hash='.length).trim().toLowerCase());\n }\n }\n\n return result;\n}\n\nfunction loadRegistryHashes(registryPath) {\n if (!fs.existsSync(registryPath)) return new Set();\n try {\n const raw = JSON.parse(fs.readFileSync(registryPath, 'utf8'));\n const sessions = raw && typeof raw === 'object' ? raw.sessions : null;\n if (!sessions || typeof sessions !== 'object') return new Set();\n\n const hashes = new Set();\n for (const entry of Object.values(sessions)) {\n if (!entry || typeof entry !== 'object') continue;\n const hash = entry.projectHash;\n if (typeof hash === 'string' && /^[a-f0-9]{8}$/.test(hash)) {\n hashes.add(hash);\n }\n }\n return hashes;\n } catch {\n return new Set();\n }\n}\n\n... [54 lines truncated] ...\n\n .map((entry) => entry.name)\n .filter((name) => /^[a-f0-9]{8}$/.test(name))\n .filter((hash) => !knownHashes.has(hash))\n .filter((hash) => filterHashes.size === 0 || filterHashes.has(hash));\n\n const projectInfo = candidates.map((hash) => {\n const fullPath = path.join(projectsDir, hash);\n const sizeBytes = getDirSizeBytes(fullPath);\n return {\n hash,\n path: fullPath,\n sizeBytes,\n sizeHuman: formatBytes(sizeBytes)\n };\n });\n\n const totalBytes = projectInfo.reduce((sum, item) => sum + item.sizeBytes, 0);\n\n if (!args.apply) {\n console.log(JSON.stringify({\n status: 'dry-run',\n message: 'Run with --apply to delete the directories below.',\n memoryRoot,\n projectsDir,\n registryPath,\n unknownCount: projectInfo.length,\n totalSizeBytes: totalBytes,\n totalSizeHuman: formatBytes(totalBytes),\n projects: projectInfo\n }, null, 2));\n process.exit(0);\n }\n\n let deleted = 0;\n for (const item of projectInfo) {\n fs.rmSync(item.path, { recursive: true, force: true });\n deleted += 1;\n }\n\n console.log(JSON.stringify({\n status: 'ok',\n mode: 'apply',\n deletedCount: deleted,\n deletedSizeBytes: totalBytes,\n deletedSizeHuman: formatBytes(totalBytes),\n deletedHashes: projectInfo.map((item) => item.hash)\n }, null, 2));\n}\n\nmain();","durationMs":0,"success":true,"metadata":{"command":"cat","turnId":"5eaeff19-dda2-4bd7-a905-9009b5786672"}}
14
+ ## 2026-03-03T14:04:06.818Z | e92edb69-bd60-41e4-a490-08a7c50deadd
15
+ - type: tool_observation
16
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
17
+ {"toolName":"Bash","toolInput":{"command":"git add package.json package-lock.json src/cli/index.ts scripts/delete-unknown-projects.js","description":"Stage relevant changed and new files"},"toolOutput":"","durationMs":0,"success":true,"metadata":{"command":"git add","turnId":"5eaeff19-dda2-4bd7-a905-9009b5786672"}}
18
+ ## 2026-03-03T14:04:29.483Z | bff186d2-1eba-4fb3-bbb4-98a5dccbc508
19
+ - type: tool_observation
20
+ - session: 3252b490-16ca-40fb-b30c-1171456e2c89
21
+ {"toolName":"Bash","toolInput":{"command":"git commit -m \"$(cat <<'EOF'\nfeat(cli): expand hook coverage to all lifecycle events\n\n- Add SessionStart, Stop, SessionEnd hooks alongside existing UserPromptSubmit and PostToolUse\n- Refactor getHooksConfig with makeHook helper to reduce repetition\n- Add REQUIRED_HOOK_FILES constant and hasHook helper for validation\n- Update install/uninstall/status commands to handle all 5 hooks\n- Add scripts/delete-unknown-projects.js for cleaning orphaned project dirs\n- Add ops:projects:clean-unknown npm script\n- Bump version to 1.0.18\n\nCo-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>\nEOF\n)\"","description":"Commit all staged changes"},"toolOutput":"[main b37aa13] feat(cli): expand hook coverage to all lifecycle events\n 4 files changed, 214 insertions(+), 37 deletions(-)\n create mode 100755 scripts/delete-unknown-projects.js","durationMs":0,"success":true,"metadata":{"command":"git commit","turnId":"5eaeff19-dda2-4bd7-a905-9009b5786672"}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-memory-layer",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "Claude Code plugin that learns from conversations to provide personalized assistance",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -19,7 +19,8 @@
19
19
  "ops:sync-gap:heal": "bash scripts/sync-gap-auto-heal.sh",
20
20
  "ops:review:resolve": "node scripts/review-queue-auto-resolve.js",
21
21
  "ops:heartbeat": "bash scripts/heartbeat-memory-orchestrator.sh",
22
- "ops:health": "npm run ops:sync-gap:report && npm run ops:sync-gap:heal && npm run ops:review:resolve"
22
+ "ops:health": "npm run ops:sync-gap:report && npm run ops:sync-gap:heal && npm run ops:review:resolve",
23
+ "ops:projects:clean-unknown": "node scripts/delete-unknown-projects.js"
23
24
  },
24
25
  "keywords": [
25
26
  "claude-code",
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ function parseArgs(argv) {
7
+ const result = {
8
+ apply: false,
9
+ hashes: []
10
+ };
11
+
12
+ for (let i = 0; i < argv.length; i += 1) {
13
+ const arg = argv[i];
14
+ if (arg === '--apply') {
15
+ result.apply = true;
16
+ continue;
17
+ }
18
+ if (arg === '--hash' && i + 1 < argv.length) {
19
+ result.hashes.push(String(argv[i + 1]).trim().toLowerCase());
20
+ i += 1;
21
+ continue;
22
+ }
23
+ if (arg.startsWith('--hash=')) {
24
+ result.hashes.push(arg.slice('--hash='.length).trim().toLowerCase());
25
+ }
26
+ }
27
+
28
+ return result;
29
+ }
30
+
31
+ function loadRegistryHashes(registryPath) {
32
+ if (!fs.existsSync(registryPath)) return new Set();
33
+ try {
34
+ const raw = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
35
+ const sessions = raw && typeof raw === 'object' ? raw.sessions : null;
36
+ if (!sessions || typeof sessions !== 'object') return new Set();
37
+
38
+ const hashes = new Set();
39
+ for (const entry of Object.values(sessions)) {
40
+ if (!entry || typeof entry !== 'object') continue;
41
+ const hash = entry.projectHash;
42
+ if (typeof hash === 'string' && /^[a-f0-9]{8}$/.test(hash)) {
43
+ hashes.add(hash);
44
+ }
45
+ }
46
+ return hashes;
47
+ } catch {
48
+ return new Set();
49
+ }
50
+ }
51
+
52
+ function getDirSizeBytes(targetPath) {
53
+ if (!fs.existsSync(targetPath)) return 0;
54
+ const stat = fs.statSync(targetPath);
55
+ if (stat.isFile()) return stat.size;
56
+ if (!stat.isDirectory()) return 0;
57
+
58
+ let total = 0;
59
+ const entries = fs.readdirSync(targetPath, { withFileTypes: true });
60
+ for (const entry of entries) {
61
+ const child = path.join(targetPath, entry.name);
62
+ if (entry.isDirectory()) {
63
+ total += getDirSizeBytes(child);
64
+ } else if (entry.isFile()) {
65
+ total += fs.statSync(child).size;
66
+ }
67
+ }
68
+ return total;
69
+ }
70
+
71
+ function formatBytes(bytes) {
72
+ if (bytes <= 0) return '0 B';
73
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
74
+ let value = bytes;
75
+ let unitIdx = 0;
76
+ while (value >= 1024 && unitIdx < units.length - 1) {
77
+ value /= 1024;
78
+ unitIdx += 1;
79
+ }
80
+ return `${value.toFixed(1)} ${units[unitIdx]}`;
81
+ }
82
+
83
+ function main() {
84
+ const args = parseArgs(process.argv.slice(2));
85
+ const memoryRoot = process.env.CML_MEMORY_ROOT || path.join(os.homedir(), '.claude-code', 'memory');
86
+ const projectsDir = path.join(memoryRoot, 'projects');
87
+ const registryPath = path.join(memoryRoot, 'session-registry.json');
88
+
89
+ if (!fs.existsSync(projectsDir)) {
90
+ console.log(JSON.stringify({
91
+ status: 'skip',
92
+ reason: 'projects_dir_not_found',
93
+ projectsDir
94
+ }, null, 2));
95
+ process.exit(0);
96
+ }
97
+
98
+ const knownHashes = loadRegistryHashes(registryPath);
99
+ const filterHashes = new Set(
100
+ args.hashes.filter((h) => /^[a-f0-9]{8}$/.test(h))
101
+ );
102
+
103
+ const candidates = fs.readdirSync(projectsDir, { withFileTypes: true })
104
+ .filter((entry) => entry.isDirectory())
105
+ .map((entry) => entry.name)
106
+ .filter((name) => /^[a-f0-9]{8}$/.test(name))
107
+ .filter((hash) => !knownHashes.has(hash))
108
+ .filter((hash) => filterHashes.size === 0 || filterHashes.has(hash));
109
+
110
+ const projectInfo = candidates.map((hash) => {
111
+ const fullPath = path.join(projectsDir, hash);
112
+ const sizeBytes = getDirSizeBytes(fullPath);
113
+ return {
114
+ hash,
115
+ path: fullPath,
116
+ sizeBytes,
117
+ sizeHuman: formatBytes(sizeBytes)
118
+ };
119
+ });
120
+
121
+ const totalBytes = projectInfo.reduce((sum, item) => sum + item.sizeBytes, 0);
122
+
123
+ if (!args.apply) {
124
+ console.log(JSON.stringify({
125
+ status: 'dry-run',
126
+ message: 'Run with --apply to delete the directories below.',
127
+ memoryRoot,
128
+ projectsDir,
129
+ registryPath,
130
+ unknownCount: projectInfo.length,
131
+ totalSizeBytes: totalBytes,
132
+ totalSizeHuman: formatBytes(totalBytes),
133
+ projects: projectInfo
134
+ }, null, 2));
135
+ process.exit(0);
136
+ }
137
+
138
+ let deleted = 0;
139
+ for (const item of projectInfo) {
140
+ fs.rmSync(item.path, { recursive: true, force: true });
141
+ deleted += 1;
142
+ }
143
+
144
+ console.log(JSON.stringify({
145
+ status: 'ok',
146
+ mode: 'apply',
147
+ deletedCount: deleted,
148
+ deletedSizeBytes: totalBytes,
149
+ deletedSizeHuman: formatBytes(totalBytes),
150
+ deletedHashes: projectInfo.map((item) => item.hash)
151
+ }, null, 2));
152
+ }
153
+
154
+ main();
package/src/cli/index.ts CHANGED
@@ -814,6 +814,7 @@ program
814
814
  .option('-a, --all', 'Import all sessions from all projects')
815
815
  .option('-l, --limit <number>', 'Limit messages per session')
816
816
  .option('-f, --force', 'Force reimport: delete existing events and reimport with turn_id grouping')
817
+ .option('--embedding-model <name>', 'Embedding model override (default: jinaai/jina-embeddings-v5-text-nano, or env CLAUDE_MEMORY_EMBEDDING_MODEL)')
817
818
  .option('-v, --verbose', 'Show detailed progress')
818
819
  .action(async (options) => {
819
820
  const startTime = Date.now();
@@ -821,6 +822,10 @@ program
821
822
  // Determine target project path for storage
822
823
  const targetProjectPath = options.project || process.cwd();
823
824
 
825
+ if (options.embeddingModel) {
826
+ process.env.CLAUDE_MEMORY_EMBEDDING_MODEL = options.embeddingModel;
827
+ }
828
+
824
829
  // Use project-specific memory service
825
830
  const service = getMemoryServiceForProject(targetProjectPath);
826
831
  const importer = createSessionHistoryImporter(service);
@@ -835,7 +840,16 @@ program
835
840
  try {
836
841
  console.log('\n⏳ Initializing memory service...');
837
842
  await service.initialize();
838
- console.log(' ✅ Ready\n');
843
+ console.log(` ✅ Ready (embedder: ${service.getEmbeddingModelName()})\n`);
844
+
845
+ const migration = await service.ensureEmbeddingModelForImport({ autoMigrate: true });
846
+ if (migration.changed) {
847
+ console.log('🔁 Embedding model migration detected/required');
848
+ console.log(` Previous: ${migration.previousModel || 'legacy-unknown'}`);
849
+ console.log(` Current: ${migration.currentModel}`);
850
+ console.log(` Re-queued embeddings: ${migration.enqueued}`);
851
+ console.log(' (Import will continue and process embeddings with the new model)\n');
852
+ }
839
853
 
840
854
  if (options.force) {
841
855
  console.log('🔄 Force mode: existing events will be deleted and reimported with turn_id grouping\n');
@@ -862,6 +876,14 @@ program
862
876
  const globalService = getDefaultMemoryService();
863
877
  const globalImporter = createSessionHistoryImporter(globalService);
864
878
  await globalService.initialize();
879
+ console.log(` ✅ Global service ready (embedder: ${globalService.getEmbeddingModelName()})`);
880
+ const globalMigration = await globalService.ensureEmbeddingModelForImport({ autoMigrate: true });
881
+ if (globalMigration.changed) {
882
+ console.log('🔁 Global embedding migration detected');
883
+ console.log(` Previous: ${globalMigration.previousModel || 'legacy-unknown'}`);
884
+ console.log(` Current: ${globalMigration.currentModel}`);
885
+ console.log(` Re-queued embeddings: ${globalMigration.enqueued}`);
886
+ }
865
887
  result = await globalImporter.importAll(importOpts);
866
888
 
867
889
  // Process embeddings
@@ -16,7 +16,7 @@ export class Embedder {
16
16
  private readonly modelName: string;
17
17
  private initialized = false;
18
18
 
19
- constructor(modelName: string = 'Xenova/all-MiniLM-L6-v2') {
19
+ constructor(modelName: string = 'jinaai/jina-embeddings-v5-text-nano') {
20
20
  this.modelName = modelName;
21
21
  }
22
22
 
@@ -117,8 +117,9 @@ export class Embedder {
117
117
  let defaultEmbedder: Embedder | null = null;
118
118
 
119
119
  export function getDefaultEmbedder(): Embedder {
120
+ const envModel = process.env.CLAUDE_MEMORY_EMBEDDING_MODEL;
120
121
  if (!defaultEmbedder) {
121
- defaultEmbedder = new Embedder();
122
+ defaultEmbedder = new Embedder(envModel || undefined);
122
123
  }
123
124
  return defaultEmbedder;
124
125
  }
@@ -865,6 +865,38 @@ export class SQLiteEventStore {
865
865
  );
866
866
  }
867
867
 
868
+ /**
869
+ * Clear embedding outbox (used for embedding model migration)
870
+ */
871
+ async clearEmbeddingOutbox(): Promise<void> {
872
+ await this.initialize();
873
+ sqliteRun(this.db, `DELETE FROM embedding_outbox`);
874
+ }
875
+
876
+ /**
877
+ * Count total events
878
+ */
879
+ async countEvents(): Promise<number> {
880
+ await this.initialize();
881
+ const row = sqliteGet<{ count: number }>(this.db, `SELECT COUNT(*) as count FROM events`);
882
+ return row?.count || 0;
883
+ }
884
+
885
+ /**
886
+ * Get events page in timestamp ascending order (stable migration/reindex scans)
887
+ */
888
+ async getEventsPage(limit: number = 1000, offset: number = 0): Promise<MemoryEvent[]> {
889
+ await this.initialize();
890
+
891
+ const rows = sqliteAll<Record<string, unknown>>(
892
+ this.db,
893
+ `SELECT * FROM events ORDER BY timestamp ASC LIMIT ? OFFSET ?`,
894
+ [limit, offset]
895
+ );
896
+
897
+ return rows.map(this.rowToEvent);
898
+ }
899
+
868
900
  /**
869
901
  * Mark outbox items as failed
870
902
  */
package/src/core/types.ts CHANGED
@@ -152,8 +152,8 @@ export const ConfigSchema = z.object({
152
152
  }).default({}),
153
153
  embedding: z.object({
154
154
  provider: z.enum(['local', 'openai']).default('local'),
155
- model: z.string().default('Xenova/all-MiniLM-L6-v2'),
156
- openaiModel: z.string().default('text-embedding-3-small'),
155
+ model: z.string().default('jinaai/jina-embeddings-v5-text-nano'),
156
+ openaiModel: z.string().default('jinaai/jina-embeddings-v5-text-nano'),
157
157
  batchSize: z.number().default(32)
158
158
  }).default({}),
159
159
  retrieval: z.object({
@@ -194,6 +194,26 @@ export class VectorStore {
194
194
  return result;
195
195
  }
196
196
 
197
+ /**
198
+ * Clear all vectors (used for embedding model migration)
199
+ */
200
+ async clearAll(): Promise<void> {
201
+ await this.initialize();
202
+ if (!this.db) return;
203
+
204
+ try {
205
+ if (typeof (this.db as any).dropTable === 'function') {
206
+ await (this.db as any).dropTable(this.tableName);
207
+ } else if (typeof (this.db as any).drop_table === 'function') {
208
+ await (this.db as any).drop_table(this.tableName);
209
+ }
210
+ } catch {
211
+ // Ignore if table does not exist
212
+ }
213
+
214
+ this.table = null;
215
+ }
216
+
197
217
  /**
198
218
  * Check if vector exists for event
199
219
  */