claude-memory-layer 1.0.17 → 1.0.19
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 +372 -74
- package/dist/cli/index.js.map +3 -3
- package/dist/hooks/post-tool-use.js +6 -0
- package/dist/hooks/post-tool-use.js.map +2 -2
- package/dist/hooks/session-end.js +6 -0
- package/dist/hooks/session-end.js.map +2 -2
- package/dist/hooks/session-start.js +29 -13
- package/dist/hooks/session-start.js.map +2 -2
- package/dist/hooks/stop.js +6 -0
- package/dist/hooks/stop.js.map +2 -2
- package/dist/hooks/user-prompt-submit.js +245 -31
- package/dist/hooks/user-prompt-submit.js.map +3 -3
- package/dist/server/api/index.js +329 -31
- package/dist/server/api/index.js.map +3 -3
- package/dist/server/index.js +336 -38
- package/dist/server/index.js.map +3 -3
- package/dist/services/memory-service.js +6 -0
- package/dist/services/memory-service.js.map +2 -2
- package/dist/ui/app.js +236 -4
- package/dist/ui/index.html +51 -0
- package/dist/ui/style.css +34 -0
- package/memory/_index.md +4 -0
- package/memory/agent_response/uncategorized/2026-02-26.md +151 -1
- package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
- package/memory/session_summary/uncategorized/2026-02-26.md +13 -0
- package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
- package/memory/tool_observation/uncategorized/2026-02-26.md +9 -1
- package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
- package/memory/user_prompt/uncategorized/2026-02-26.md +9 -0
- package/package.json +3 -2
- package/scripts/delete-unknown-projects.js +154 -0
- package/src/hooks/session-start.ts +9 -3
- package/src/hooks/user-prompt-submit.ts +225 -29
- package/src/server/api/events.ts +1 -0
- package/src/server/api/stats.ts +346 -0
- package/src/services/memory-service.ts +7 -0
- package/src/ui/app.js +236 -4
- package/src/ui/index.html +51 -0
- package/src/ui/style.css +34 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
## 2026-02-26T13:53:54.921Z | 3fa78495-3f90-45ff-a85c-43dc29ddedb7
|
|
3
|
+
- type: session_summary
|
|
4
|
+
- session: bench-session-2
|
|
5
|
+
Session with 1 user prompts and 6 responses.
|
|
6
|
+
Topics discussed:
|
|
7
|
+
- 최근 변경 사항 때문에 claude code 가 느려진 원인을 분석해줘.
|
|
8
|
+
## 2026-02-26T13:54:46.584Z | cb94a8e4-4a9d-4bfc-8ee5-1458f5e304e7
|
|
9
|
+
- type: session_summary
|
|
10
|
+
- session: bench-session-3
|
|
11
|
+
Session with 1 user prompts and 6 responses.
|
|
12
|
+
Topics discussed:
|
|
13
|
+
- 최근 변경 사항 때문에 claude code 가 느려진 원인을 분석해줘.
|
|
@@ -198,4 +198,12 @@
|
|
|
198
198
|
## 2026-02-26T12:51:57.850Z | 3b09d82b-7fa3-4673-978e-87ec65d901dd
|
|
199
199
|
- type: tool_observation
|
|
200
200
|
- session: 2f324dae-7d8f-4f67-842e-acf507b79c59
|
|
201
|
-
{"toolName":"Bash","toolInput":{"command":"git add src/core/sqlite-event-store.ts && git commit -m \"$(cat <<'EOF'\nfix(api): handle missing retrieval_traces table in read-only mode\n\nDashboard API creates read-only MemoryService which skips schema\ncreation. If the DB was created before retrieval_traces was added,\nqueries fail with \"no such table\". Now returns empty results gracefully.\n\nCo-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>\nEOF\n)\"","description":"Commit retrieval_traces fix"},"toolOutput":"[main 60d59c6] fix(api): handle missing retrieval_traces table in read-only mode\n 1 file changed, 52 insertions(+), 40 deletions(-)","durationMs":0,"success":true,"metadata":{"command":"git add","turnId":"6ee6b497-8756-43c8-a208-8830a70efc1a"}}
|
|
201
|
+
{"toolName":"Bash","toolInput":{"command":"git add src/core/sqlite-event-store.ts && git commit -m \"$(cat <<'EOF'\nfix(api): handle missing retrieval_traces table in read-only mode\n\nDashboard API creates read-only MemoryService which skips schema\ncreation. If the DB was created before retrieval_traces was added,\nqueries fail with \"no such table\". Now returns empty results gracefully.\n\nCo-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>\nEOF\n)\"","description":"Commit retrieval_traces fix"},"toolOutput":"[main 60d59c6] fix(api): handle missing retrieval_traces table in read-only mode\n 1 file changed, 52 insertions(+), 40 deletions(-)","durationMs":0,"success":true,"metadata":{"command":"git add","turnId":"6ee6b497-8756-43c8-a208-8830a70efc1a"}}
|
|
202
|
+
## 2026-02-26T13:53:54.436Z | 4ced656c-1426-44fd-9f2d-148c5432c1f8
|
|
203
|
+
- type: tool_observation
|
|
204
|
+
- session: bench-session-2
|
|
205
|
+
{"toolName":"Bash","toolInput":{"cmd":"echo hello"},"toolOutput":"hello\n","durationMs":0,"success":true,"metadata":{"turnId":"ccd580af-30da-4227-8a48-6ce91c3bfeba"}}
|
|
206
|
+
## 2026-02-26T13:54:46.108Z | 0fa770e5-f71d-4eda-be4d-7a6529171718
|
|
207
|
+
- type: tool_observation
|
|
208
|
+
- session: bench-session-3
|
|
209
|
+
{"toolName":"Bash","toolInput":{"cmd":"echo hello"},"toolOutput":"hello\n","durationMs":0,"success":true,"metadata":{"turnId":"03e597c8-295a-43d8-9959-bc77607d30f8"}}
|
|
@@ -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"}}
|
|
@@ -14,3 +14,12 @@ api/stats/retrieval-traces?limit=20
|
|
|
14
14
|
suggested_actions 를 생성할때
|
|
15
15
|
"각 색상·사이즈별 재고 가능 여부 확인해줘"
|
|
16
16
|
같이 없는 기능은 생성하지 않도록 프롬프트 개선해줘
|
|
17
|
+
|
|
18
|
+
## 2026-02-26T13:53:54.228Z | efefccef-1a9a-4e89-b1fa-b855fd8fdc85
|
|
19
|
+
- type: user_prompt
|
|
20
|
+
- session: bench-session-2
|
|
21
|
+
최근 변경 사항 때문에 claude code 가 느려진 원인을 분석해줘.
|
|
22
|
+
## 2026-02-26T13:54:45.879Z | f7fca9e1-5169-4732-8845-34fdb5e699c5
|
|
23
|
+
- type: user_prompt
|
|
24
|
+
- session: bench-session-3
|
|
25
|
+
최근 변경 사항 때문에 claude code 가 느려진 원인을 분석해줘.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-memory-layer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
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();
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
getLightweightMemoryService,
|
|
9
9
|
registerSession
|
|
10
10
|
} from '../services/memory-service.js';
|
|
11
11
|
import type { SessionStartInput, SessionStartOutput } from '../core/types.js';
|
|
@@ -18,8 +18,8 @@ async function main(): Promise<void> {
|
|
|
18
18
|
// Register session with project path for other hooks to find
|
|
19
19
|
registerSession(input.session_id, input.cwd);
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
const memoryService =
|
|
21
|
+
// Use lightweight service to avoid starting background workers in hook process
|
|
22
|
+
const memoryService = getLightweightMemoryService(input.session_id);
|
|
23
23
|
|
|
24
24
|
try {
|
|
25
25
|
// Start session in memory service
|
|
@@ -42,6 +42,12 @@ async function main(): Promise<void> {
|
|
|
42
42
|
} catch (error) {
|
|
43
43
|
console.error('Memory hook error:', error);
|
|
44
44
|
console.log(JSON.stringify({ context: '' }));
|
|
45
|
+
} finally {
|
|
46
|
+
try {
|
|
47
|
+
await memoryService.close();
|
|
48
|
+
} catch {
|
|
49
|
+
// Best-effort cleanup
|
|
50
|
+
}
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
53
|
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { randomUUID } from 'crypto';
|
|
14
|
-
import
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import * as os from 'os';
|
|
17
|
+
import { getLightweightMemoryService, getMemoryServiceForSession } from '../services/memory-service.js';
|
|
15
18
|
import { writeTurnState } from '../core/turn-state.js';
|
|
16
19
|
import type { UserPromptSubmitInput, UserPromptSubmitOutput } from '../core/types.js';
|
|
17
20
|
|
|
@@ -21,6 +24,20 @@ const MAX_MEMORIES = parseInt(process.env.CLAUDE_MEMORY_MAX_COUNT || '5');
|
|
|
21
24
|
const BASE_MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_MIN_SCORE || '0.4');
|
|
22
25
|
const FALLBACK_MIN_SCORE = parseFloat(process.env.CLAUDE_MEMORY_FALLBACK_MIN_SCORE || '0.3');
|
|
23
26
|
const ENABLE_SEARCH = process.env.CLAUDE_MEMORY_SEARCH !== 'false';
|
|
27
|
+
const RETRIEVAL_MODE = (process.env.CLAUDE_MEMORY_RETRIEVAL_MODE || 'hybrid') as 'keyword' | 'semantic' | 'hybrid';
|
|
28
|
+
const SEMANTIC_TIMEOUT_MS = parseInt(process.env.CLAUDE_MEMORY_SEMANTIC_TIMEOUT_MS || '1200');
|
|
29
|
+
const ADHERENCE_INTERVAL_TURNS = parseInt(process.env.CLAUDE_MEMORY_ADHERENCE_INTERVAL_TURNS || '3');
|
|
30
|
+
|
|
31
|
+
const ADHERENCE_STATE_DIR = path.join(os.homedir(), '.claude-code', 'memory');
|
|
32
|
+
|
|
33
|
+
interface AdherenceState {
|
|
34
|
+
sessionId: string;
|
|
35
|
+
turnCount: number;
|
|
36
|
+
lastCheckedTurn: number;
|
|
37
|
+
lastPrompt: string;
|
|
38
|
+
lastReason?: string;
|
|
39
|
+
updatedAt: string;
|
|
40
|
+
}
|
|
24
41
|
|
|
25
42
|
/**
|
|
26
43
|
* Determine if a prompt is worth storing as a memory.
|
|
@@ -42,6 +59,120 @@ function getDynamicMinScore(prompt: string): number {
|
|
|
42
59
|
return BASE_MIN_SCORE;
|
|
43
60
|
}
|
|
44
61
|
|
|
62
|
+
function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const timer = setTimeout(() => reject(new Error(`semantic retrieval timeout (${timeoutMs}ms)`)), timeoutMs);
|
|
65
|
+
promise
|
|
66
|
+
.then((result) => {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
resolve(result);
|
|
69
|
+
})
|
|
70
|
+
.catch((error) => {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
reject(error);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatMemoryContext(items: Array<{ type: string; content: string }>): string {
|
|
78
|
+
if (items.length === 0) return '';
|
|
79
|
+
const lines = items.map((m) => {
|
|
80
|
+
const preview = m.content.length > 300 ? m.content.substring(0, 300) + '...' : m.content;
|
|
81
|
+
return `- [${m.type}] ${preview}`;
|
|
82
|
+
});
|
|
83
|
+
return `💡 **Related memories found:**\n\n${lines.join('\n\n')}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getAdherenceStatePath(sessionId: string): string {
|
|
87
|
+
return path.join(ADHERENCE_STATE_DIR, `.adherence-state-${sessionId}.json`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readAdherenceState(sessionId: string): AdherenceState {
|
|
91
|
+
try {
|
|
92
|
+
const filePath = getAdherenceStatePath(sessionId);
|
|
93
|
+
if (!fs.existsSync(filePath)) {
|
|
94
|
+
return {
|
|
95
|
+
sessionId,
|
|
96
|
+
turnCount: 0,
|
|
97
|
+
lastCheckedTurn: 0,
|
|
98
|
+
lastPrompt: '',
|
|
99
|
+
lastReason: 'init',
|
|
100
|
+
updatedAt: new Date().toISOString()
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const data = fs.readFileSync(filePath, 'utf8');
|
|
105
|
+
const parsed = JSON.parse(data) as AdherenceState;
|
|
106
|
+
if (parsed.sessionId !== sessionId) throw new Error('session mismatch');
|
|
107
|
+
return parsed;
|
|
108
|
+
} catch {
|
|
109
|
+
return {
|
|
110
|
+
sessionId,
|
|
111
|
+
turnCount: 0,
|
|
112
|
+
lastCheckedTurn: 0,
|
|
113
|
+
lastPrompt: '',
|
|
114
|
+
lastReason: 'init',
|
|
115
|
+
updatedAt: new Date().toISOString()
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function writeAdherenceState(state: AdherenceState): void {
|
|
121
|
+
try {
|
|
122
|
+
if (!fs.existsSync(ADHERENCE_STATE_DIR)) {
|
|
123
|
+
fs.mkdirSync(ADHERENCE_STATE_DIR, { recursive: true });
|
|
124
|
+
}
|
|
125
|
+
const filePath = getAdherenceStatePath(state.sessionId);
|
|
126
|
+
const tempPath = filePath + '.tmp';
|
|
127
|
+
fs.writeFileSync(tempPath, JSON.stringify(state));
|
|
128
|
+
fs.renameSync(tempPath, filePath);
|
|
129
|
+
} catch {
|
|
130
|
+
// non-critical
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function hasWriteIntent(prompt: string): boolean {
|
|
135
|
+
return /(fix|refactor|implement|change|modify|edit|update|rewrite|patch|create|add|remove|delete|버그|수정|리팩터|구현|추가|삭제|개선)/i.test(prompt);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function tokenize(text: string): string[] {
|
|
139
|
+
const stopwords = new Set(['the', 'and', 'for', 'with', 'that', 'this', 'from', 'have', 'what', 'when', 'where', 'how', 'why', '그리고', '그리고요', '이거', '그거', '해주세요', '해줘', '좀', '에서', '으로', '하는', '해']);
|
|
140
|
+
return text
|
|
141
|
+
.toLowerCase()
|
|
142
|
+
.replace(/[^a-z0-9가-힣\s]/g, ' ')
|
|
143
|
+
.split(/\s+/)
|
|
144
|
+
.filter((w) => w.length >= 2 && !stopwords.has(w));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isTopicShift(currentPrompt: string, lastPrompt: string): boolean {
|
|
148
|
+
if (!lastPrompt || lastPrompt.length < 10) return false;
|
|
149
|
+
const a = new Set(tokenize(currentPrompt));
|
|
150
|
+
const b = new Set(tokenize(lastPrompt));
|
|
151
|
+
if (a.size === 0 || b.size === 0) return false;
|
|
152
|
+
|
|
153
|
+
let intersection = 0;
|
|
154
|
+
for (const token of a) {
|
|
155
|
+
if (b.has(token)) intersection++;
|
|
156
|
+
}
|
|
157
|
+
const union = a.size + b.size - intersection;
|
|
158
|
+
const similarity = union > 0 ? intersection / union : 0;
|
|
159
|
+
return similarity < 0.2;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function shouldRunAdherenceCheck(turnCount: number, prompt: string, state: AdherenceState): { run: boolean; reason: string } {
|
|
163
|
+
if (turnCount === 1) return { run: true, reason: 'first-turn' };
|
|
164
|
+
if (hasWriteIntent(prompt)) return { run: true, reason: 'write-intent' };
|
|
165
|
+
if (isTopicShift(prompt, state.lastPrompt)) return { run: true, reason: 'topic-shift' };
|
|
166
|
+
if (turnCount - state.lastCheckedTurn >= ADHERENCE_INTERVAL_TURNS) return { run: true, reason: 'interval' };
|
|
167
|
+
return { run: false, reason: 'skip' };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function logAdherenceDecision(sessionId: string, turn: number, run: boolean, reason: string): void {
|
|
171
|
+
if (!process.env.CLAUDE_MEMORY_DEBUG) return;
|
|
172
|
+
const mode = run ? 'enforced' : 'skipped';
|
|
173
|
+
console.error(`[adherence] session=${sessionId} turn=${turn} mode=${mode} reason=${reason}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
45
176
|
async function main(): Promise<void> {
|
|
46
177
|
// Read input from stdin
|
|
47
178
|
const inputData = await readStdin();
|
|
@@ -58,62 +189,127 @@ async function main(): Promise<void> {
|
|
|
58
189
|
const memoryService = getLightweightMemoryService(input.session_id);
|
|
59
190
|
|
|
60
191
|
try {
|
|
192
|
+
let context = '';
|
|
193
|
+
|
|
194
|
+
const adherenceState = readAdherenceState(input.session_id);
|
|
195
|
+
const currentTurn = adherenceState.turnCount + 1;
|
|
196
|
+
const adherenceDecision = shouldRunAdherenceCheck(currentTurn, input.prompt, adherenceState);
|
|
197
|
+
logAdherenceDecision(input.session_id, currentTurn, adherenceDecision.run, adherenceDecision.reason);
|
|
198
|
+
|
|
61
199
|
// Store only non-trivial prompts (skip /commands, short inputs)
|
|
62
200
|
if (shouldStorePrompt(input.prompt)) {
|
|
63
201
|
await memoryService.storeUserPrompt(
|
|
64
202
|
input.session_id,
|
|
65
203
|
input.prompt,
|
|
66
|
-
{
|
|
204
|
+
{
|
|
205
|
+
turnId,
|
|
206
|
+
adherence: {
|
|
207
|
+
checked: adherenceDecision.run,
|
|
208
|
+
reason: adherenceDecision.reason,
|
|
209
|
+
turn: currentTurn
|
|
210
|
+
}
|
|
211
|
+
}
|
|
67
212
|
);
|
|
68
213
|
}
|
|
69
214
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (ENABLE_SEARCH && input.prompt.length > 10) {
|
|
215
|
+
// Search strategy: turn-1 always enforce adherence check,
|
|
216
|
+
// then adaptively enforce on write-intent/topic-shift/interval
|
|
217
|
+
if (ENABLE_SEARCH && input.prompt.length > 10 && adherenceDecision.run) {
|
|
74
218
|
const minScore = getDynamicMinScore(input.prompt);
|
|
75
|
-
let
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
219
|
+
let mergedMemories: Array<{ type: string; content: string; id?: string; score?: number }> = [];
|
|
220
|
+
|
|
221
|
+
const canUseSemantic = RETRIEVAL_MODE === 'semantic' || RETRIEVAL_MODE === 'hybrid';
|
|
222
|
+
if (canUseSemantic) {
|
|
223
|
+
try {
|
|
224
|
+
const semanticService = getMemoryServiceForSession(input.session_id);
|
|
225
|
+
const semantic = await withTimeout(
|
|
226
|
+
semanticService.retrieveMemories(input.prompt, {
|
|
227
|
+
topK: MAX_MEMORIES,
|
|
228
|
+
minScore,
|
|
229
|
+
sessionId: input.session_id,
|
|
230
|
+
intentRewrite: true,
|
|
231
|
+
adaptiveRerank: true,
|
|
232
|
+
projectScopeMode: 'strict'
|
|
233
|
+
}),
|
|
234
|
+
SEMANTIC_TIMEOUT_MS
|
|
235
|
+
);
|
|
79
236
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
237
|
+
mergedMemories = semantic.memories.map((m) => ({
|
|
238
|
+
type: m.event.eventType,
|
|
239
|
+
content: m.event.content,
|
|
240
|
+
id: m.event.id,
|
|
241
|
+
score: m.score
|
|
242
|
+
}));
|
|
243
|
+
} catch {
|
|
244
|
+
// Semantic retrieval is best-effort; fallback below handles the rest
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const shouldUseKeywordFallback =
|
|
249
|
+
RETRIEVAL_MODE === 'keyword' ||
|
|
250
|
+
RETRIEVAL_MODE === 'hybrid' ||
|
|
251
|
+
mergedMemories.length === 0;
|
|
252
|
+
|
|
253
|
+
if (shouldUseKeywordFallback && mergedMemories.length < MAX_MEMORIES) {
|
|
254
|
+
let results = await memoryService.keywordSearch(input.prompt, {
|
|
83
255
|
topK: MAX_MEMORIES,
|
|
84
|
-
minScore
|
|
256
|
+
minScore
|
|
85
257
|
});
|
|
258
|
+
|
|
259
|
+
// recall rescue: if nothing found at tuned threshold, retry with fallback floor
|
|
260
|
+
if (results.length === 0 && FALLBACK_MIN_SCORE < minScore) {
|
|
261
|
+
results = await memoryService.keywordSearch(input.prompt, {
|
|
262
|
+
topK: MAX_MEMORIES,
|
|
263
|
+
minScore: FALLBACK_MIN_SCORE
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const existingIds = new Set(mergedMemories.map((m) => m.id).filter(Boolean));
|
|
268
|
+
for (const r of results) {
|
|
269
|
+
if (existingIds.has(r.event.id)) continue;
|
|
270
|
+
mergedMemories.push({
|
|
271
|
+
type: r.event.eventType,
|
|
272
|
+
content: r.event.content,
|
|
273
|
+
id: r.event.id,
|
|
274
|
+
score: r.score
|
|
275
|
+
});
|
|
276
|
+
if (mergedMemories.length >= MAX_MEMORIES) break;
|
|
277
|
+
}
|
|
86
278
|
}
|
|
87
279
|
|
|
88
|
-
if (
|
|
280
|
+
if (mergedMemories.length > 0) {
|
|
89
281
|
// Increment access count for found memories
|
|
90
|
-
const eventIds =
|
|
91
|
-
|
|
282
|
+
const eventIds = mergedMemories.map((m) => m.id).filter((v): v is string => Boolean(v));
|
|
283
|
+
if (eventIds.length > 0) {
|
|
284
|
+
await memoryService.incrementMemoryAccess(eventIds);
|
|
285
|
+
}
|
|
92
286
|
|
|
93
287
|
// Record each retrieval for helpfulness tracking
|
|
94
|
-
for (const
|
|
288
|
+
for (const m of mergedMemories) {
|
|
289
|
+
if (!m.id) continue;
|
|
95
290
|
try {
|
|
96
291
|
await memoryService.recordRetrieval(
|
|
97
|
-
|
|
292
|
+
m.id,
|
|
98
293
|
input.session_id,
|
|
99
|
-
|
|
294
|
+
m.score ?? minScore,
|
|
100
295
|
input.prompt
|
|
101
296
|
);
|
|
102
297
|
} catch { /* non-critical */ }
|
|
103
298
|
}
|
|
104
299
|
|
|
105
|
-
|
|
106
|
-
const memories = results.map(r => {
|
|
107
|
-
const preview = r.event.content.length > 300
|
|
108
|
-
? r.event.content.substring(0, 300) + '...'
|
|
109
|
-
: r.event.content;
|
|
110
|
-
return `- [${r.event.eventType}] ${preview}`;
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
context = `💡 **Related memories found:**\n\n${memories.join('\n\n')}`;
|
|
300
|
+
context = formatMemoryContext(mergedMemories);
|
|
114
301
|
}
|
|
115
302
|
}
|
|
116
303
|
|
|
304
|
+
writeAdherenceState({
|
|
305
|
+
sessionId: input.session_id,
|
|
306
|
+
turnCount: currentTurn,
|
|
307
|
+
lastCheckedTurn: adherenceDecision.run ? currentTurn : adherenceState.lastCheckedTurn,
|
|
308
|
+
lastPrompt: input.prompt,
|
|
309
|
+
lastReason: adherenceDecision.reason,
|
|
310
|
+
updatedAt: new Date().toISOString()
|
|
311
|
+
});
|
|
312
|
+
|
|
117
313
|
const output: UserPromptSubmitOutput = { context };
|
|
118
314
|
console.log(JSON.stringify(output));
|
|
119
315
|
} catch (error) {
|
package/src/server/api/events.ts
CHANGED
|
@@ -66,6 +66,7 @@ eventsRouter.get('/', async (c) => {
|
|
|
66
66
|
sessionId: e.sessionId,
|
|
67
67
|
preview: e.content.slice(0, 200) + (e.content.length > 200 ? '...' : ''),
|
|
68
68
|
contentLength: e.content.length,
|
|
69
|
+
metadata: e.metadata,
|
|
69
70
|
accessCount: (e as any).access_count || 0,
|
|
70
71
|
lastAccessedAt: (e as any).last_accessed_at || null
|
|
71
72
|
})),
|