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.
- package/config/kpi-thresholds.json +7 -0
- package/dist/cli/index.js +532 -79
- package/dist/cli/index.js.map +3 -3
- package/dist/core/index.js +49 -4
- package/dist/core/index.js.map +2 -2
- package/dist/hooks/post-tool-use.js +140 -3
- package/dist/hooks/post-tool-use.js.map +2 -2
- package/dist/hooks/session-end.js +140 -3
- package/dist/hooks/session-end.js.map +2 -2
- package/dist/hooks/session-start.js +140 -3
- package/dist/hooks/session-start.js.map +2 -2
- package/dist/hooks/stop.js +140 -3
- package/dist/hooks/stop.js.map +2 -2
- package/dist/hooks/user-prompt-submit.js +379 -34
- package/dist/hooks/user-prompt-submit.js.map +3 -3
- package/dist/server/api/index.js +467 -34
- package/dist/server/api/index.js.map +3 -3
- package/dist/server/index.js +474 -41
- package/dist/server/index.js.map +3 -3
- package/dist/services/memory-service.js +140 -3
- package/dist/services/memory-service.js.map +2 -2
- package/dist/ui/app.js +362 -4
- package/dist/ui/index.html +90 -0
- package/dist/ui/style.css +41 -0
- package/memory/_index.md +3 -0
- package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
- package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
- package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
- package/package.json +3 -2
- package/scripts/delete-unknown-projects.js +154 -0
- package/src/cli/index.ts +23 -1
- package/src/core/embedder.ts +3 -2
- package/src/core/sqlite-event-store.ts +32 -0
- package/src/core/types.ts +2 -2
- package/src/core/vector-store.ts +20 -0
- package/src/hooks/user-prompt-submit.ts +225 -29
- package/src/server/api/events.ts +7 -0
- package/src/server/api/stats.ts +346 -0
- package/src/services/memory-service.ts +119 -2
- package/src/ui/app.js +362 -4
- package/src/ui/index.html +90 -0
- 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,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.
|
|
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(
|
|
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
|
package/src/core/embedder.ts
CHANGED
|
@@ -16,7 +16,7 @@ export class Embedder {
|
|
|
16
16
|
private readonly modelName: string;
|
|
17
17
|
private initialized = false;
|
|
18
18
|
|
|
19
|
-
constructor(modelName: string = '
|
|
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('
|
|
156
|
-
openaiModel: z.string().default('
|
|
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({
|
package/src/core/vector-store.ts
CHANGED
|
@@ -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
|
*/
|