claude-cortex 1.8.3 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/setup/hooks.d.ts.map +1 -1
- package/dist/setup/hooks.js +2 -1
- package/dist/setup/hooks.js.map +1 -1
- package/package.json +1 -1
- package/scripts/session-end-hook.mjs +548 -0
package/README.md
CHANGED
|
@@ -77,7 +77,6 @@ Add to `~/.claude/settings.json` for automatic memory extraction and context loa
|
|
|
77
77
|
"hooks": {
|
|
78
78
|
"PreCompact": [
|
|
79
79
|
{
|
|
80
|
-
"matcher": "",
|
|
81
80
|
"hooks": [
|
|
82
81
|
{
|
|
83
82
|
"type": "command",
|
|
@@ -89,7 +88,6 @@ Add to `~/.claude/settings.json` for automatic memory extraction and context loa
|
|
|
89
88
|
],
|
|
90
89
|
"SessionStart": [
|
|
91
90
|
{
|
|
92
|
-
"matcher": "",
|
|
93
91
|
"hooks": [
|
|
94
92
|
{
|
|
95
93
|
"type": "command",
|
|
@@ -98,6 +96,17 @@ Add to `~/.claude/settings.json` for automatic memory extraction and context loa
|
|
|
98
96
|
}
|
|
99
97
|
]
|
|
100
98
|
}
|
|
99
|
+
],
|
|
100
|
+
"SessionEnd": [
|
|
101
|
+
{
|
|
102
|
+
"hooks": [
|
|
103
|
+
{
|
|
104
|
+
"type": "command",
|
|
105
|
+
"command": "npx -y claude-cortex hook session-end",
|
|
106
|
+
"timeout": 10
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}
|
|
101
110
|
]
|
|
102
111
|
}
|
|
103
112
|
}
|
|
@@ -105,6 +114,7 @@ Add to `~/.claude/settings.json` for automatic memory extraction and context loa
|
|
|
105
114
|
|
|
106
115
|
- **PreCompact**: Auto-saves important context before compaction events
|
|
107
116
|
- **SessionStart**: Auto-loads project context at the start of each session
|
|
117
|
+
- **SessionEnd**: Auto-saves context when the session exits
|
|
108
118
|
|
|
109
119
|
### 4. Run Setup (Recommended)
|
|
110
120
|
|
|
@@ -246,6 +256,22 @@ Claude: Let me check my memory.
|
|
|
246
256
|
> Found: "Using PostgreSQL for the database" (architecture, 95% salience)
|
|
247
257
|
```
|
|
248
258
|
|
|
259
|
+
## Hook Coverage
|
|
260
|
+
|
|
261
|
+
Claude Cortex uses three hooks to cover the full session lifecycle:
|
|
262
|
+
|
|
263
|
+
| Hook | Fires When | What It Does | Reliability |
|
|
264
|
+
|------|-----------|--------------|-------------|
|
|
265
|
+
| **SessionStart** | Session begins | Loads project context from memory | Reliable |
|
|
266
|
+
| **PreCompact** | Before context compaction | Extracts important content before context is lost | Reliable (primary safety net) |
|
|
267
|
+
| **SessionEnd** | Session terminates | Extracts important content on exit | Best-effort* |
|
|
268
|
+
|
|
269
|
+
*SessionEnd does not fire on forced termination (terminal killed, SSH drops, crash). PreCompact remains the primary safety net since compaction happens more frequently than session exits.
|
|
270
|
+
|
|
271
|
+
### Stop Hook (Opt-in, Future)
|
|
272
|
+
|
|
273
|
+
A prompt-based Stop hook that uses Haiku to evaluate each Claude response for important events is planned. This calls the Haiku API on every response, which adds latency and cost. It will be opt-in via `--with-stop-hook` flag.
|
|
274
|
+
|
|
249
275
|
## Configuration
|
|
250
276
|
|
|
251
277
|
### Database Location
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* npx claude-cortex setup # Configure Claude for proactive memory use
|
|
16
16
|
* npx claude-cortex hook pre-compact # Run pre-compact hook (for settings.json)
|
|
17
17
|
* npx claude-cortex hook session-start # Run session-start hook (for settings.json)
|
|
18
|
+
* npx claude-cortex hook session-end # Run session-end hook (for settings.json)
|
|
18
19
|
* npx claude-cortex service install # Auto-start dashboard on login
|
|
19
20
|
* npx claude-cortex service uninstall # Remove auto-start
|
|
20
21
|
* npx claude-cortex service status # Check service status
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG"}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* npx claude-cortex setup # Configure Claude for proactive memory use
|
|
16
16
|
* npx claude-cortex hook pre-compact # Run pre-compact hook (for settings.json)
|
|
17
17
|
* npx claude-cortex hook session-start # Run session-start hook (for settings.json)
|
|
18
|
+
* npx claude-cortex hook session-end # Run session-end hook (for settings.json)
|
|
18
19
|
* npx claude-cortex service install # Auto-start dashboard on login
|
|
19
20
|
* npx claude-cortex service uninstall # Remove auto-start
|
|
20
21
|
* npx claude-cortex service status # Check service status
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/setup/hooks.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/setup/hooks.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAkBH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBvE"}
|
package/dist/setup/hooks.js
CHANGED
|
@@ -12,13 +12,14 @@ const SCRIPTS_DIR = path.resolve(__dirname, '..', '..', 'scripts');
|
|
|
12
12
|
const HOOKS = {
|
|
13
13
|
'pre-compact': 'pre-compact-hook.mjs',
|
|
14
14
|
'session-start': 'session-start-hook.mjs',
|
|
15
|
+
'session-end': 'session-end-hook.mjs',
|
|
15
16
|
};
|
|
16
17
|
export async function handleHookCommand(hookName) {
|
|
17
18
|
const scriptFile = HOOKS[hookName];
|
|
18
19
|
if (!scriptFile) {
|
|
19
20
|
console.error(`Unknown hook: ${hookName}`);
|
|
20
21
|
console.log(`Available hooks: ${Object.keys(HOOKS).join(', ')}`);
|
|
21
|
-
console.log('Usage: claude-cortex hook <pre-compact|session-start>');
|
|
22
|
+
console.log('Usage: claude-cortex hook <pre-compact|session-start|session-end>');
|
|
22
23
|
process.exit(1);
|
|
23
24
|
}
|
|
24
25
|
const scriptPath = path.join(SCRIPTS_DIR, scriptFile);
|
package/dist/setup/hooks.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../src/setup/hooks.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACtC,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAE3C,qDAAqD;AACrD,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;AAEnE,MAAM,KAAK,GAA2B;IACpC,aAAa,EAAE,sBAAsB;IACrC,eAAe,EAAE,wBAAwB;
|
|
1
|
+
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../src/setup/hooks.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACtC,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAE3C,qDAAqD;AACrD,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;AAEnE,MAAM,KAAK,GAA2B;IACpC,aAAa,EAAE,sBAAsB;IACrC,eAAe,EAAE,wBAAwB;IACzC,aAAa,EAAE,sBAAsB;CACtC,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,QAAgB;IACtD,MAAM,UAAU,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC;IACnC,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,CAAC,KAAK,CAAC,iBAAiB,QAAQ,EAAE,CAAC,CAAC;QAC3C,OAAO,CAAC,GAAG,CAAC,oBAAoB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACjE,OAAO,CAAC,GAAG,CAAC,mEAAmE,CAAC,CAAC;QACjF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IAEtD,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,EAAE;QAClD,KAAK,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,SAAS,CAAC;KACtC,CAAC,CAAC;IAEH,kCAAkC;IAClC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAEhC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;QACxB,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Session-end hook for Claude Cortex - Automatic Memory Extraction on Exit
|
|
4
|
+
*
|
|
5
|
+
* This script runs when a Claude Code session ends and:
|
|
6
|
+
* 1. Reads the session transcript from the JSONL file
|
|
7
|
+
* 2. Analyzes conversation content for important information
|
|
8
|
+
* 3. Auto-extracts high-salience items (decisions, patterns, errors, etc.)
|
|
9
|
+
* 4. Saves them to the memory database automatically
|
|
10
|
+
*
|
|
11
|
+
* NOTE: SessionEnd doesn't always fire reliably (e.g. terminal killed, SSH drops).
|
|
12
|
+
* PreCompact remains the primary safety net for context preservation.
|
|
13
|
+
*
|
|
14
|
+
* Input (stdin JSON):
|
|
15
|
+
* {
|
|
16
|
+
* "session_id": "abc123",
|
|
17
|
+
* "transcript_path": "~/.claude/projects/.../abc.jsonl",
|
|
18
|
+
* "cwd": "/path/to/project",
|
|
19
|
+
* "hook_event_name": "SessionEnd",
|
|
20
|
+
* "reason": "exit" | "clear" | "logout" | "prompt_input_exit" | "other"
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import Database from 'better-sqlite3';
|
|
25
|
+
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
|
26
|
+
import { join } from 'path';
|
|
27
|
+
import { homedir } from 'os';
|
|
28
|
+
|
|
29
|
+
// Database paths (with legacy fallback)
|
|
30
|
+
const NEW_DB_DIR = join(homedir(), '.claude-cortex');
|
|
31
|
+
const LEGACY_DB_DIR = join(homedir(), '.claude-memory');
|
|
32
|
+
|
|
33
|
+
function getDbPath() {
|
|
34
|
+
const newPath = join(NEW_DB_DIR, 'memories.db');
|
|
35
|
+
const legacyPath = join(LEGACY_DB_DIR, 'memories.db');
|
|
36
|
+
if (existsSync(newPath) || !existsSync(legacyPath)) {
|
|
37
|
+
return { dir: NEW_DB_DIR, path: newPath };
|
|
38
|
+
}
|
|
39
|
+
return { dir: LEGACY_DB_DIR, path: legacyPath };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { dir: DB_DIR, path: DB_PATH } = getDbPath();
|
|
43
|
+
|
|
44
|
+
// Memory limits
|
|
45
|
+
const MAX_SHORT_TERM_MEMORIES = 100;
|
|
46
|
+
const MAX_LONG_TERM_MEMORIES = 1000;
|
|
47
|
+
const BASE_THRESHOLD = 0.35;
|
|
48
|
+
const MAX_AUTO_MEMORIES = 5;
|
|
49
|
+
|
|
50
|
+
const CATEGORY_EXTRACTION_THRESHOLDS = {
|
|
51
|
+
architecture: 0.28,
|
|
52
|
+
error: 0.30,
|
|
53
|
+
context: 0.32,
|
|
54
|
+
learning: 0.32,
|
|
55
|
+
pattern: 0.35,
|
|
56
|
+
preference: 0.38,
|
|
57
|
+
note: 0.42,
|
|
58
|
+
todo: 0.40,
|
|
59
|
+
relationship: 0.35,
|
|
60
|
+
custom: 0.35,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ==================== PROJECT DETECTION ====================
|
|
64
|
+
|
|
65
|
+
const SKIP_DIRECTORIES = [
|
|
66
|
+
'src', 'lib', 'dist', 'build', 'out',
|
|
67
|
+
'node_modules', '.git', '.next', '.cache',
|
|
68
|
+
'test', 'tests', '__tests__', 'spec',
|
|
69
|
+
'bin', 'scripts', 'config', 'public', 'static',
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
function extractProjectFromPath(path) {
|
|
73
|
+
if (!path) return null;
|
|
74
|
+
const segments = path.split(/[/\\]/).filter(Boolean);
|
|
75
|
+
if (segments.length === 0) return null;
|
|
76
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
77
|
+
const segment = segments[i];
|
|
78
|
+
if (!SKIP_DIRECTORIES.includes(segment.toLowerCase())) {
|
|
79
|
+
if (segment.startsWith('.')) continue;
|
|
80
|
+
return segment;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ==================== DYNAMIC THRESHOLD ====================
|
|
87
|
+
|
|
88
|
+
function getMemoryStats(db) {
|
|
89
|
+
try {
|
|
90
|
+
const stats = db.prepare(`
|
|
91
|
+
SELECT
|
|
92
|
+
COUNT(*) as total,
|
|
93
|
+
SUM(CASE WHEN type = 'short_term' THEN 1 ELSE 0 END) as shortTerm,
|
|
94
|
+
SUM(CASE WHEN type = 'long_term' THEN 1 ELSE 0 END) as longTerm
|
|
95
|
+
FROM memories
|
|
96
|
+
`).get();
|
|
97
|
+
return stats || { total: 0, shortTerm: 0, longTerm: 0 };
|
|
98
|
+
} catch {
|
|
99
|
+
return { total: 0, shortTerm: 0, longTerm: 0 };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getDynamicThreshold(memoryCount, maxMemories) {
|
|
104
|
+
const fullness = memoryCount / maxMemories;
|
|
105
|
+
if (fullness > 0.8) return 0.50;
|
|
106
|
+
if (fullness > 0.6) return 0.42;
|
|
107
|
+
if (fullness > 0.4) return 0.35;
|
|
108
|
+
if (fullness > 0.2) return 0.30;
|
|
109
|
+
return 0.25;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getExtractionThreshold(category, dynamicThreshold) {
|
|
113
|
+
const categoryThreshold = CATEGORY_EXTRACTION_THRESHOLDS[category] || BASE_THRESHOLD;
|
|
114
|
+
return Math.min(categoryThreshold, dynamicThreshold);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ==================== SALIENCE DETECTION ====================
|
|
118
|
+
|
|
119
|
+
const ARCHITECTURE_KEYWORDS = [
|
|
120
|
+
'architecture', 'design', 'pattern', 'structure', 'system',
|
|
121
|
+
'database', 'api', 'schema', 'model', 'framework', 'stack',
|
|
122
|
+
'microservice', 'monolith', 'serverless', 'infrastructure'
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const ERROR_KEYWORDS = [
|
|
126
|
+
'error', 'bug', 'fix', 'issue', 'problem', 'crash', 'fail',
|
|
127
|
+
'exception', 'debug', 'resolve', 'solution', 'workaround'
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
const PREFERENCE_KEYWORDS = [
|
|
131
|
+
'prefer', 'always', 'never', 'style', 'convention', 'standard',
|
|
132
|
+
'like', 'want', 'should', 'must', 'require'
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const PATTERN_KEYWORDS = [
|
|
136
|
+
'pattern', 'practice', 'approach', 'method', 'technique',
|
|
137
|
+
'implementation', 'strategy', 'algorithm', 'workflow'
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
const DECISION_KEYWORDS = [
|
|
141
|
+
'decided', 'decision', 'chose', 'chosen', 'selected', 'going with',
|
|
142
|
+
'will use', 'opted for', 'settled on', 'agreed'
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const LEARNING_KEYWORDS = [
|
|
146
|
+
'learned', 'discovered', 'realized', 'found out', 'turns out',
|
|
147
|
+
'TIL', 'now know', 'understand now', 'figured out'
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
const EMOTIONAL_MARKERS = [
|
|
151
|
+
'important', 'critical', 'crucial', 'essential', 'key',
|
|
152
|
+
'finally', 'breakthrough', 'eureka', 'aha', 'got it',
|
|
153
|
+
'frustrating', 'annoying', 'tricky', 'remember'
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
const CODE_REFERENCE_PATTERNS = [
|
|
157
|
+
/\b[A-Z][a-zA-Z]*\.[a-zA-Z]+\b/,
|
|
158
|
+
/\b[a-z_][a-zA-Z0-9_]*\.(ts|js|py|go|rs)\b/,
|
|
159
|
+
/`[^`]+`/,
|
|
160
|
+
/\b(function|class|interface|type|const|let|var)\s+\w+/i,
|
|
161
|
+
/\bline\s*\d+\b/i,
|
|
162
|
+
/\b(src|lib|app|components?)\/\S+/,
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
function detectKeywords(text, keywords) {
|
|
166
|
+
const lower = text.toLowerCase();
|
|
167
|
+
return keywords.some(keyword => lower.includes(keyword.toLowerCase()));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function detectCodeReferences(content) {
|
|
171
|
+
return CODE_REFERENCE_PATTERNS.some(pattern => pattern.test(content));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function detectExplicitRequest(text) {
|
|
175
|
+
const patterns = [
|
|
176
|
+
/\bremember\s+(this|that)\b/i,
|
|
177
|
+
/\bdon'?t\s+forget\b/i,
|
|
178
|
+
/\bkeep\s+(in\s+)?mind\b/i,
|
|
179
|
+
/\bnote\s+(this|that)\b/i,
|
|
180
|
+
/\bsave\s+(this|that)\b/i,
|
|
181
|
+
/\bimportant[:\s]/i,
|
|
182
|
+
/\bfor\s+future\s+reference\b/i,
|
|
183
|
+
];
|
|
184
|
+
return patterns.some(pattern => pattern.test(text));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function calculateSalience(text) {
|
|
188
|
+
let score = 0.25;
|
|
189
|
+
if (detectExplicitRequest(text)) score += 0.5;
|
|
190
|
+
if (detectKeywords(text, ARCHITECTURE_KEYWORDS)) score += 0.4;
|
|
191
|
+
if (detectKeywords(text, ERROR_KEYWORDS)) score += 0.35;
|
|
192
|
+
if (detectKeywords(text, DECISION_KEYWORDS)) score += 0.35;
|
|
193
|
+
if (detectKeywords(text, LEARNING_KEYWORDS)) score += 0.3;
|
|
194
|
+
if (detectKeywords(text, PATTERN_KEYWORDS)) score += 0.25;
|
|
195
|
+
if (detectKeywords(text, PREFERENCE_KEYWORDS)) score += 0.25;
|
|
196
|
+
if (detectCodeReferences(text)) score += 0.15;
|
|
197
|
+
if (detectKeywords(text, EMOTIONAL_MARKERS)) score += 0.2;
|
|
198
|
+
return Math.min(1.0, score);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function suggestCategory(text) {
|
|
202
|
+
const lower = text.toLowerCase();
|
|
203
|
+
if (detectKeywords(lower, ARCHITECTURE_KEYWORDS)) return 'architecture';
|
|
204
|
+
if (detectKeywords(lower, ERROR_KEYWORDS)) return 'error';
|
|
205
|
+
if (detectKeywords(lower, DECISION_KEYWORDS)) return 'context';
|
|
206
|
+
if (detectKeywords(lower, LEARNING_KEYWORDS)) return 'learning';
|
|
207
|
+
if (detectKeywords(lower, PREFERENCE_KEYWORDS)) return 'preference';
|
|
208
|
+
if (detectKeywords(lower, PATTERN_KEYWORDS)) return 'pattern';
|
|
209
|
+
if (/\b(todo|fixme|hack|xxx)\b/i.test(lower)) return 'todo';
|
|
210
|
+
return 'note';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function extractTags(text, extractorName = null) {
|
|
214
|
+
const tags = new Set();
|
|
215
|
+
const hashtagMatches = text.match(/#[a-zA-Z][a-zA-Z0-9_-]*/g);
|
|
216
|
+
if (hashtagMatches) {
|
|
217
|
+
hashtagMatches.forEach(tag => tags.add(tag.slice(1).toLowerCase()));
|
|
218
|
+
}
|
|
219
|
+
const techTerms = [
|
|
220
|
+
'react', 'vue', 'angular', 'node', 'python', 'typescript', 'javascript',
|
|
221
|
+
'api', 'database', 'sql', 'mongodb', 'postgresql', 'mysql',
|
|
222
|
+
'docker', 'kubernetes', 'aws', 'git', 'testing', 'auth', 'security'
|
|
223
|
+
];
|
|
224
|
+
const lowerText = text.toLowerCase();
|
|
225
|
+
techTerms.forEach(term => {
|
|
226
|
+
if (lowerText.includes(term)) tags.add(term);
|
|
227
|
+
});
|
|
228
|
+
tags.add('auto-extracted');
|
|
229
|
+
tags.add('session-end');
|
|
230
|
+
if (extractorName) {
|
|
231
|
+
tags.add(`source:${extractorName}`);
|
|
232
|
+
}
|
|
233
|
+
return Array.from(tags).slice(0, 12);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function calculateFrequencyBoost(segment, allSegments) {
|
|
237
|
+
const commonWords = new Set([
|
|
238
|
+
'about', 'after', 'before', 'being', 'between', 'could', 'during',
|
|
239
|
+
'every', 'found', 'through', 'would', 'should', 'which', 'where',
|
|
240
|
+
'there', 'these', 'their', 'other', 'using', 'because', 'without'
|
|
241
|
+
]);
|
|
242
|
+
const words = segment.content.toLowerCase().split(/\s+/);
|
|
243
|
+
const keyTerms = words.filter(w =>
|
|
244
|
+
w.length > 5 && !commonWords.has(w) && /^[a-z]+$/.test(w)
|
|
245
|
+
);
|
|
246
|
+
let boost = 0;
|
|
247
|
+
const seenTerms = new Set();
|
|
248
|
+
for (const term of keyTerms) {
|
|
249
|
+
if (seenTerms.has(term)) continue;
|
|
250
|
+
seenTerms.add(term);
|
|
251
|
+
const mentions = allSegments.filter(s =>
|
|
252
|
+
s !== segment && s.content.toLowerCase().includes(term)
|
|
253
|
+
).length;
|
|
254
|
+
if (mentions > 1) {
|
|
255
|
+
boost += 0.03 * Math.min(mentions, 5);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return Math.min(0.15, boost);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ==================== CONTENT EXTRACTION ====================
|
|
262
|
+
|
|
263
|
+
function extractMemorableSegments(conversationText) {
|
|
264
|
+
const segments = [];
|
|
265
|
+
const extractors = [
|
|
266
|
+
{
|
|
267
|
+
name: 'decision',
|
|
268
|
+
patterns: [
|
|
269
|
+
/(?:we\s+)?decided\s+(?:to\s+)?(.{15,200})/gi,
|
|
270
|
+
/(?:going|went)\s+with\s+(.{15,150})/gi,
|
|
271
|
+
/(?:chose|chosen|selected)\s+(.{15,150})/gi,
|
|
272
|
+
/the\s+(?:approach|solution|fix)\s+(?:is|was)\s+(.{15,200})/gi,
|
|
273
|
+
/(?:using|will\s+use)\s+(.{15,150})/gi,
|
|
274
|
+
/(?:opted\s+for|settled\s+on)\s+(.{15,150})/gi,
|
|
275
|
+
],
|
|
276
|
+
titlePrefix: 'Decision: ',
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: 'error-fix',
|
|
280
|
+
patterns: [
|
|
281
|
+
/(?:fixed|solved|resolved)\s+(?:by\s+)?(.{15,200})/gi,
|
|
282
|
+
/the\s+(?:fix|solution|workaround)\s+(?:is|was)\s+(.{15,200})/gi,
|
|
283
|
+
/(?:root\s+cause|issue)\s+(?:is|was)\s+(.{15,200})/gi,
|
|
284
|
+
/(?:error|bug)\s+(?:was\s+)?caused\s+by\s+(.{15,200})/gi,
|
|
285
|
+
/(?:problem|issue)\s+was\s+(.{15,150})/gi,
|
|
286
|
+
/(?:the\s+)?bug\s+(?:is|was)\s+(.{15,150})/gi,
|
|
287
|
+
/(?:debugging|debugged)\s+(.{15,150})/gi,
|
|
288
|
+
],
|
|
289
|
+
titlePrefix: 'Fix: ',
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: 'learning',
|
|
293
|
+
patterns: [
|
|
294
|
+
/(?:learned|discovered|realized|found\s+out)\s+(?:that\s+)?(.{15,200})/gi,
|
|
295
|
+
/turns\s+out\s+(?:that\s+)?(.{15,200})/gi,
|
|
296
|
+
/(?:TIL|today\s+I\s+learned)[:\s]+(.{15,200})/gi,
|
|
297
|
+
/(?:now\s+)?(?:understand|know)\s+(?:that\s+)?(.{15,150})/gi,
|
|
298
|
+
/(?:figured\s+out|worked\s+out)\s+(.{15,150})/gi,
|
|
299
|
+
],
|
|
300
|
+
titlePrefix: 'Learned: ',
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
name: 'architecture',
|
|
304
|
+
patterns: [
|
|
305
|
+
/the\s+architecture\s+(?:is|uses|consists\s+of)\s+(.{15,200})/gi,
|
|
306
|
+
/(?:design|pattern)\s+(?:is|uses)\s+(.{15,200})/gi,
|
|
307
|
+
/(?:system|api|database)\s+(?:structure|design)\s+(?:is|uses)\s+(.{15,200})/gi,
|
|
308
|
+
/(?:created|added|implemented|built)\s+(?:a\s+)?(.{15,200})/gi,
|
|
309
|
+
/(?:refactored|updated|changed)\s+(?:the\s+)?(.{15,150})/gi,
|
|
310
|
+
],
|
|
311
|
+
titlePrefix: 'Architecture: ',
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: 'preference',
|
|
315
|
+
patterns: [
|
|
316
|
+
/(?:always|never)\s+(.{10,150})/gi,
|
|
317
|
+
/(?:prefer|want)\s+to\s+(.{10,150})/gi,
|
|
318
|
+
/(?:should|must)\s+(?:always\s+)?(.{10,150})/gi,
|
|
319
|
+
],
|
|
320
|
+
titlePrefix: 'Preference: ',
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: 'important-note',
|
|
324
|
+
patterns: [
|
|
325
|
+
/important[:\s]+(.{15,200})/gi,
|
|
326
|
+
/(?:note|remember)[:\s]+(.{15,200})/gi,
|
|
327
|
+
/(?:key|critical)\s+(?:point|thing)[:\s]+(.{15,200})/gi,
|
|
328
|
+
/(?:this\s+is\s+)?(?:crucial|essential)[:\s]+(.{15,150})/gi,
|
|
329
|
+
/(?:don't\s+forget|keep\s+in\s+mind)[:\s]+(.{15,150})/gi,
|
|
330
|
+
],
|
|
331
|
+
titlePrefix: 'Note: ',
|
|
332
|
+
},
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
for (const extractor of extractors) {
|
|
336
|
+
for (const pattern of extractor.patterns) {
|
|
337
|
+
let match;
|
|
338
|
+
while ((match = pattern.exec(conversationText)) !== null) {
|
|
339
|
+
const content = match[1].trim();
|
|
340
|
+
if (content.length >= 20) {
|
|
341
|
+
const titleContent = content.slice(0, 50).replace(/\s+/g, ' ').trim();
|
|
342
|
+
const title = extractor.titlePrefix + (titleContent.length < 50 ? titleContent : titleContent + '...');
|
|
343
|
+
segments.push({
|
|
344
|
+
title,
|
|
345
|
+
content: content.slice(0, 500),
|
|
346
|
+
extractorType: extractor.name,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return segments;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function processSegments(segments, dynamicThreshold = BASE_THRESHOLD) {
|
|
357
|
+
const unique = [];
|
|
358
|
+
for (const seg of segments) {
|
|
359
|
+
const isDupe = unique.some(existing => {
|
|
360
|
+
const overlap = calculateOverlap(existing.content, seg.content);
|
|
361
|
+
return overlap > 0.8;
|
|
362
|
+
});
|
|
363
|
+
if (!isDupe) {
|
|
364
|
+
const text = seg.title + ' ' + seg.content;
|
|
365
|
+
const baseSalience = calculateSalience(text);
|
|
366
|
+
const category = suggestCategory(text);
|
|
367
|
+
unique.push({
|
|
368
|
+
...seg,
|
|
369
|
+
baseSalience,
|
|
370
|
+
category,
|
|
371
|
+
tags: extractTags(text, seg.extractorType),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (const seg of unique) {
|
|
377
|
+
const frequencyBoost = calculateFrequencyBoost(seg, unique);
|
|
378
|
+
seg.salience = Math.min(1.0, seg.baseSalience + frequencyBoost);
|
|
379
|
+
seg.frequencyBoost = frequencyBoost;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
unique.sort((a, b) => b.salience - a.salience);
|
|
383
|
+
|
|
384
|
+
const filtered = unique.filter(seg => {
|
|
385
|
+
const threshold = getExtractionThreshold(seg.category, dynamicThreshold);
|
|
386
|
+
return seg.salience >= threshold;
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
return filtered.slice(0, MAX_AUTO_MEMORIES);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function calculateOverlap(text1, text2) {
|
|
393
|
+
const words1 = new Set(text1.toLowerCase().split(/\s+/));
|
|
394
|
+
const words2 = new Set(text2.toLowerCase().split(/\s+/));
|
|
395
|
+
const intersection = new Set([...words1].filter(w => words2.has(w)));
|
|
396
|
+
const union = new Set([...words1, ...words2]);
|
|
397
|
+
return intersection.size / union.size;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ==================== DATABASE OPERATIONS ====================
|
|
401
|
+
|
|
402
|
+
function saveMemory(db, memory, project) {
|
|
403
|
+
const timestamp = new Date().toISOString();
|
|
404
|
+
const stmt = db.prepare(`
|
|
405
|
+
INSERT INTO memories (title, content, type, category, salience, tags, project, created_at, last_accessed)
|
|
406
|
+
VALUES (?, ?, 'short_term', ?, ?, ?, ?, ?, ?)
|
|
407
|
+
`);
|
|
408
|
+
stmt.run(
|
|
409
|
+
memory.title,
|
|
410
|
+
memory.content,
|
|
411
|
+
memory.category,
|
|
412
|
+
memory.salience,
|
|
413
|
+
JSON.stringify(memory.tags),
|
|
414
|
+
project || null,
|
|
415
|
+
timestamp,
|
|
416
|
+
timestamp
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ==================== TRANSCRIPT READING ====================
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Read conversation text from the session transcript JSONL file.
|
|
424
|
+
*/
|
|
425
|
+
function readTranscript(transcriptPath) {
|
|
426
|
+
if (!transcriptPath) return '';
|
|
427
|
+
|
|
428
|
+
// Expand ~ to homedir
|
|
429
|
+
const resolvedPath = transcriptPath.replace(/^~/, homedir());
|
|
430
|
+
|
|
431
|
+
if (!existsSync(resolvedPath)) {
|
|
432
|
+
console.error(`[session-end] Transcript not found: ${resolvedPath}`);
|
|
433
|
+
return '';
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
const content = readFileSync(resolvedPath, 'utf-8');
|
|
438
|
+
const lines = content.trim().split('\n');
|
|
439
|
+
|
|
440
|
+
// Read last 50 lines to get recent conversation
|
|
441
|
+
const recentLines = lines.slice(-50);
|
|
442
|
+
const messages = [];
|
|
443
|
+
|
|
444
|
+
for (const line of recentLines) {
|
|
445
|
+
try {
|
|
446
|
+
const entry = JSON.parse(line);
|
|
447
|
+
const role = entry.type || entry.message?.role;
|
|
448
|
+
const msgContent = entry.message?.content;
|
|
449
|
+
if ((role === 'user' || role === 'assistant') && msgContent) {
|
|
450
|
+
const text = Array.isArray(msgContent)
|
|
451
|
+
? msgContent.filter(c => c.type === 'text').map(c => c.text).join('\n')
|
|
452
|
+
: msgContent;
|
|
453
|
+
if (text && !text.startsWith('/')) {
|
|
454
|
+
messages.push(text);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
} catch {
|
|
458
|
+
// Skip invalid lines
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const result = messages.join('\n\n');
|
|
463
|
+
console.error(`[session-end] Read ${messages.length} messages from transcript (${result.length} chars)`);
|
|
464
|
+
return result;
|
|
465
|
+
} catch (err) {
|
|
466
|
+
console.error(`[session-end] Failed to read transcript: ${err.message}`);
|
|
467
|
+
return '';
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ==================== MAIN HOOK LOGIC ====================
|
|
472
|
+
|
|
473
|
+
let input = '';
|
|
474
|
+
process.stdin.setEncoding('utf8');
|
|
475
|
+
|
|
476
|
+
process.stdin.on('readable', () => {
|
|
477
|
+
let chunk;
|
|
478
|
+
while ((chunk = process.stdin.read()) !== null) {
|
|
479
|
+
input += chunk;
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
process.stdin.on('end', () => {
|
|
484
|
+
try {
|
|
485
|
+
const hookData = JSON.parse(input || '{}');
|
|
486
|
+
|
|
487
|
+
const reason = hookData.reason || 'unknown';
|
|
488
|
+
const project = extractProjectFromPath(hookData.cwd);
|
|
489
|
+
|
|
490
|
+
// Skip extraction on /clear — session is being intentionally wiped
|
|
491
|
+
if (reason === 'clear') {
|
|
492
|
+
console.error('[session-end] Session cleared, skipping extraction');
|
|
493
|
+
process.exit(0);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Read conversation from transcript_path (provided by Claude Code)
|
|
497
|
+
const conversationText = readTranscript(hookData.transcript_path);
|
|
498
|
+
|
|
499
|
+
if (!conversationText || conversationText.length < 100) {
|
|
500
|
+
console.error('[session-end] Not enough conversation content to extract from');
|
|
501
|
+
process.exit(0);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Ensure database directory exists
|
|
505
|
+
if (!existsSync(DB_DIR)) {
|
|
506
|
+
mkdirSync(DB_DIR, { recursive: true });
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (!existsSync(DB_PATH)) {
|
|
510
|
+
console.error('[session-end] Memory database not found, skipping extraction');
|
|
511
|
+
process.exit(0);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const db = new Database(DB_PATH, { timeout: 5000 });
|
|
515
|
+
|
|
516
|
+
const stats = getMemoryStats(db);
|
|
517
|
+
const totalMemories = stats.shortTerm + stats.longTerm;
|
|
518
|
+
const maxMemories = MAX_SHORT_TERM_MEMORIES + MAX_LONG_TERM_MEMORIES;
|
|
519
|
+
const dynamicThreshold = getDynamicThreshold(totalMemories, maxMemories);
|
|
520
|
+
|
|
521
|
+
console.error(`[session-end] Memory status: ${totalMemories}/${maxMemories} (${(totalMemories/maxMemories*100).toFixed(0)}% full)`);
|
|
522
|
+
console.error(`[session-end] Reason: ${reason}, Dynamic threshold: ${dynamicThreshold.toFixed(2)}`);
|
|
523
|
+
|
|
524
|
+
// Extract memorable segments
|
|
525
|
+
const segments = extractMemorableSegments(conversationText);
|
|
526
|
+
const processedSegments = processSegments(segments, dynamicThreshold);
|
|
527
|
+
|
|
528
|
+
let autoExtractedCount = 0;
|
|
529
|
+
for (const memory of processedSegments) {
|
|
530
|
+
try {
|
|
531
|
+
saveMemory(db, memory, project);
|
|
532
|
+
autoExtractedCount++;
|
|
533
|
+
const boostInfo = memory.frequencyBoost > 0 ? ` +${memory.frequencyBoost.toFixed(2)} boost` : '';
|
|
534
|
+
console.error(`[session-end] Saved: ${memory.title} (salience: ${memory.salience.toFixed(2)}${boostInfo}, category: ${memory.category})`);
|
|
535
|
+
} catch (err) {
|
|
536
|
+
console.error(`[session-end] Failed to save "${memory.title}": ${err.message}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
console.error(`[session-end] Complete: ${autoExtractedCount} memories auto-extracted on session ${reason}`);
|
|
541
|
+
|
|
542
|
+
db.close();
|
|
543
|
+
process.exit(0);
|
|
544
|
+
} catch (error) {
|
|
545
|
+
console.error(`[session-end] Error: ${error.message}`);
|
|
546
|
+
process.exit(0); // Don't block session exit on errors
|
|
547
|
+
}
|
|
548
|
+
});
|