claude-mem 2.1.2 → 3.0.2
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/claude-mem +0 -0
- package/hooks/pre-compact.js +193 -40
- package/hooks/session-end.js +135 -55
- package/hooks/session-start.js +169 -54
- package/package.json +1 -1
- package/src/claude-mem.js +859 -0
- package/hooks/hook-utils.js +0 -318
package/claude-mem
CHANGED
|
Binary file
|
package/hooks/pre-compact.js
CHANGED
|
@@ -1,58 +1,211 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 🔒 LOCKED by @docs-agent | Change to 🔑 to allow @docs-agent edits
|
|
5
|
+
*
|
|
6
|
+
* OFFICIAL DOCS: Claude Code Hooks API v2025
|
|
7
|
+
* Last Verified: 2025-08-31
|
|
8
|
+
* @see https://docs.anthropic.com/en/docs/claude-code/hooks
|
|
9
|
+
*
|
|
10
|
+
* Claude Memory System - Pre-Compact Hook
|
|
11
|
+
*
|
|
12
|
+
* CRITICAL REQUIREMENTS:
|
|
13
|
+
* - Hook responses MUST use 'continue' field (boolean)
|
|
14
|
+
* - When continue is false, MUST provide 'stopReason' field
|
|
15
|
+
* - DO NOT use 'decision' or 'reason' fields (deprecated pattern)
|
|
16
|
+
* - PreCompact hooks DO NOT support hookSpecificOutput field
|
|
17
|
+
* - Exit codes: 0=success, 1=error shown to user, 2=error with stderr shown
|
|
18
|
+
*
|
|
19
|
+
* Valid Response Format:
|
|
20
|
+
* Success: { "continue": true }
|
|
21
|
+
* Failure: { "continue": false, "stopReason": "error message" }
|
|
22
|
+
*
|
|
23
|
+
* @docs-ref: Official hook response format specification
|
|
24
|
+
* @see https://docs.anthropic.com/claude-code/hooks#response-format
|
|
6
25
|
*/
|
|
7
26
|
|
|
8
|
-
import {
|
|
27
|
+
import { spawn } from 'child_process';
|
|
28
|
+
import { join, dirname, resolve, isAbsolute } from 'path';
|
|
29
|
+
import { fileURLToPath } from 'url';
|
|
30
|
+
import { readFileSync, existsSync } from 'fs';
|
|
9
31
|
|
|
10
|
-
|
|
32
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
|
|
34
|
+
// Load configuration to get the CLI command name
|
|
35
|
+
let cliCommand = 'claude-mem'; // Default fallback
|
|
36
|
+
const configPath = join(__dirname, 'config.json');
|
|
37
|
+
if (existsSync(configPath)) {
|
|
11
38
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
39
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
40
|
+
cliCommand = config.cliCommand || 'claude-mem';
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// Fallback to default if config read fails
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Sanitizes and validates file path to prevent command injection
|
|
48
|
+
*/
|
|
49
|
+
function sanitizeFilePath(filePath) {
|
|
50
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Remove any null bytes or control characters
|
|
55
|
+
const cleanPath = filePath.replace(/[\x00-\x1f\x7f-\x9f]/g, '');
|
|
56
|
+
|
|
57
|
+
// Reject paths with shell metacharacters that could be used for injection
|
|
58
|
+
const dangerousChars = /[;&|`$(){}[\]<>]/;
|
|
59
|
+
if (dangerousChars.test(cleanPath)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Convert to absolute path to prevent directory traversal
|
|
64
|
+
let safePath;
|
|
65
|
+
try {
|
|
66
|
+
safePath = isAbsolute(cleanPath) ? resolve(cleanPath) : resolve(process.cwd(), cleanPath);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Basic path validation - must be a reasonable file path
|
|
72
|
+
if (safePath.length > 4096) { // Reasonable path length limit
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return safePath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function preCompactHook() {
|
|
80
|
+
let input = '';
|
|
81
|
+
|
|
82
|
+
// Read JSON input from stdin
|
|
83
|
+
process.stdin.on('data', chunk => {
|
|
84
|
+
input += chunk;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
process.stdin.on('end', async () => {
|
|
26
88
|
try {
|
|
27
|
-
|
|
89
|
+
const data = JSON.parse(input);
|
|
90
|
+
const transcriptPath = data.transcript_path;
|
|
28
91
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
continue:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
92
|
+
if (!transcriptPath) {
|
|
93
|
+
// Output error in Claude Code expected format
|
|
94
|
+
// @docs-ref: continue: false with stopReason for blocking operations
|
|
95
|
+
console.log(JSON.stringify({
|
|
96
|
+
continue: false,
|
|
97
|
+
stopReason: "No transcript path provided"
|
|
98
|
+
}));
|
|
99
|
+
process.exit(2); // Exit 2 tells Claude Code to show error
|
|
100
|
+
}
|
|
35
101
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
102
|
+
// SECURITY: Sanitize transcript path to prevent command injection
|
|
103
|
+
const sanitizedPath = sanitizeFilePath(transcriptPath);
|
|
104
|
+
if (!sanitizedPath) {
|
|
39
105
|
console.log(JSON.stringify({
|
|
40
|
-
continue:
|
|
41
|
-
|
|
42
|
-
systemMessage: "Memory compression skipped - Claude SDK not available in this context"
|
|
106
|
+
continue: false,
|
|
107
|
+
stopReason: "Invalid or potentially unsafe transcript path"
|
|
43
108
|
}));
|
|
44
|
-
process.exit(0);
|
|
45
|
-
} else {
|
|
46
|
-
// Real error
|
|
47
|
-
console.log(JSON.stringify(HookResponse.error(`Compression failed: ${error.message}`)));
|
|
48
109
|
process.exit(2);
|
|
49
110
|
}
|
|
111
|
+
|
|
112
|
+
// Call the CLI compress command using configured name with sanitized path
|
|
113
|
+
const compressor = spawn(cliCommand, ['compress', sanitizedPath], {
|
|
114
|
+
stdio: ['ignore', 'pipe', 'pipe'] // Capture output instead of inherit
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
let stdout = '';
|
|
118
|
+
let stderr = '';
|
|
119
|
+
let isCompleted = false;
|
|
120
|
+
|
|
121
|
+
// Add timeout to prevent hanging hooks
|
|
122
|
+
const hookTimeout = setTimeout(() => {
|
|
123
|
+
if (!isCompleted) {
|
|
124
|
+
compressor.kill('SIGTERM');
|
|
125
|
+
console.log(JSON.stringify({
|
|
126
|
+
continue: true,
|
|
127
|
+
suppressOutput: true,
|
|
128
|
+
systemMessage: "Memory compression timed out - continuing with compaction"
|
|
129
|
+
}));
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
}, 10000); // 10 second timeout
|
|
133
|
+
|
|
134
|
+
compressor.stdout.on('data', (data) => {
|
|
135
|
+
stdout += data.toString();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
compressor.stderr.on('data', (data) => {
|
|
139
|
+
stderr += data.toString();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
compressor.on('close', (code) => {
|
|
143
|
+
isCompleted = true;
|
|
144
|
+
clearTimeout(hookTimeout);
|
|
145
|
+
if (code !== 0) {
|
|
146
|
+
// Check if it's the Claude Code executable error
|
|
147
|
+
if (stderr.includes('Claude Code executable not found')) {
|
|
148
|
+
// This is expected when running outside Claude Code context
|
|
149
|
+
// Just output success since we can't compress without Claude SDK
|
|
150
|
+
// @docs-ref: PreCompact hooks should only use continue field, not hookSpecificOutput
|
|
151
|
+
console.log(JSON.stringify({
|
|
152
|
+
continue: true,
|
|
153
|
+
suppressOutput: true,
|
|
154
|
+
systemMessage: "Memory compression skipped - Claude SDK not available in this context"
|
|
155
|
+
}));
|
|
156
|
+
process.exit(0);
|
|
157
|
+
} else {
|
|
158
|
+
// Real error - report it
|
|
159
|
+
// @docs-ref: Use continue: false with stopReason for errors
|
|
160
|
+
console.log(JSON.stringify({
|
|
161
|
+
continue: false,
|
|
162
|
+
stopReason: `Compression failed: ${stderr}`
|
|
163
|
+
}));
|
|
164
|
+
process.exit(2);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
// Success! Output confirmation
|
|
168
|
+
// @docs-ref: PreCompact hooks should only use continue field for success
|
|
169
|
+
console.log(JSON.stringify({
|
|
170
|
+
continue: true,
|
|
171
|
+
suppressOutput: true
|
|
172
|
+
}));
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
compressor.on('error', (error) => {
|
|
178
|
+
isCompleted = true;
|
|
179
|
+
clearTimeout(hookTimeout);
|
|
180
|
+
|
|
181
|
+
// Handle common error cases gracefully
|
|
182
|
+
if (error.code === 'ENOENT') {
|
|
183
|
+
// CLI command not found - likely running in test environment
|
|
184
|
+
console.log(JSON.stringify({
|
|
185
|
+
continue: true,
|
|
186
|
+
suppressOutput: true,
|
|
187
|
+
systemMessage: "Memory compression skipped - CLI not available in test environment"
|
|
188
|
+
}));
|
|
189
|
+
process.exit(0);
|
|
190
|
+
} else {
|
|
191
|
+
// @docs-ref: Use continue: false with stopReason for errors
|
|
192
|
+
console.log(JSON.stringify({
|
|
193
|
+
continue: false,
|
|
194
|
+
stopReason: `Failed to start compression: ${error.message}`
|
|
195
|
+
}));
|
|
196
|
+
process.exit(2);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
} catch (error) {
|
|
201
|
+
// @docs-ref: Use continue: false with stopReason for errors
|
|
202
|
+
console.log(JSON.stringify({
|
|
203
|
+
continue: false,
|
|
204
|
+
stopReason: `Hook error: ${error.message}`
|
|
205
|
+
}));
|
|
206
|
+
process.exit(2);
|
|
50
207
|
}
|
|
51
|
-
|
|
52
|
-
} catch (error) {
|
|
53
|
-
console.log(JSON.stringify(HookResponse.error(`Hook error: ${error.message}`)));
|
|
54
|
-
process.exit(2);
|
|
55
|
-
}
|
|
208
|
+
});
|
|
56
209
|
}
|
|
57
210
|
|
|
58
211
|
preCompactHook();
|
package/hooks/session-end.js
CHANGED
|
@@ -1,77 +1,157 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* Uses shared utilities to eliminate code duplication
|
|
4
|
+
* 🔑 LOCKED by @docs-agent | Change to 🔑 to allow @docs-agent edits
|
|
6
5
|
*
|
|
6
|
+
* OFFICIAL DOCS: Claude Code SessionEnd Hook v2025
|
|
7
|
+
* Last Verified: 2025-08-31
|
|
8
|
+
* @see https://docs.anthropic.com/en/docs/claude-code/hooks
|
|
9
|
+
*
|
|
10
|
+
* Claude Memory System - Session End Hook
|
|
7
11
|
* Triggers compression when session ends with reason 'clear'
|
|
8
|
-
*
|
|
12
|
+
*
|
|
13
|
+
* SessionEnd Hook Payload:
|
|
14
|
+
* {
|
|
15
|
+
* "session_id": "string",
|
|
16
|
+
* "transcript_path": "string",
|
|
17
|
+
* "cwd": "string",
|
|
18
|
+
* "hook_event_name": "SessionEnd",
|
|
19
|
+
* "reason": "exit" | "clear" | "error" | ...
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* Valid Response Format:
|
|
23
|
+
* Success: { "continue": true }
|
|
24
|
+
* Success with message: { "continue": true, "systemMessage": "string" }
|
|
25
|
+
* Failure: { "continue": false, "stopReason": "error message" }
|
|
26
|
+
*
|
|
27
|
+
* Note: SessionEnd hooks should generally always return continue: true
|
|
28
|
+
* to avoid blocking session termination.
|
|
29
|
+
*
|
|
30
|
+
* @docs-ref: SessionEnd hook should not block session ending
|
|
31
|
+
* @see https://docs.anthropic.com/claude-code/hooks#sessionend
|
|
9
32
|
*/
|
|
10
33
|
|
|
11
|
-
import {
|
|
34
|
+
import { spawn } from 'child_process';
|
|
35
|
+
import { join, dirname } from 'path';
|
|
36
|
+
import { fileURLToPath } from 'url';
|
|
37
|
+
import { readFileSync, existsSync } from 'fs';
|
|
12
38
|
|
|
13
|
-
|
|
39
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
40
|
+
|
|
41
|
+
// Load configuration to get the CLI command name
|
|
42
|
+
let cliCommand = 'claude-mem'; // Default fallback
|
|
43
|
+
const configPath = join(__dirname, 'config.json');
|
|
44
|
+
if (existsSync(configPath)) {
|
|
14
45
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
systemMessage: "Warning: No transcript path provided for compression"
|
|
32
|
-
}));
|
|
33
|
-
process.exit(0);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Use spawnCLI utility for compression
|
|
37
|
-
const cliCommand = config.cliCommand || 'claude-mem';
|
|
38
|
-
|
|
46
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
47
|
+
cliCommand = config.cliCommand || 'claude-mem';
|
|
48
|
+
} catch (e) {
|
|
49
|
+
// Fallback to default if config read fails
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function sessionEndHook() {
|
|
54
|
+
let input = '';
|
|
55
|
+
|
|
56
|
+
// Read JSON input from stdin
|
|
57
|
+
process.stdin.on('data', chunk => {
|
|
58
|
+
input += chunk;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
process.stdin.on('end', async () => {
|
|
39
62
|
try {
|
|
40
|
-
|
|
63
|
+
const data = JSON.parse(input);
|
|
64
|
+
const { reason, transcript_path, session_id } = data;
|
|
41
65
|
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
continue
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
66
|
+
// Only proceed if reason is 'clear'
|
|
67
|
+
if (reason !== 'clear') {
|
|
68
|
+
// For other reasons, just continue without action
|
|
69
|
+
// @docs-ref: SessionEnd hooks should not use hookSpecificOutput
|
|
70
|
+
console.log(JSON.stringify({
|
|
71
|
+
continue: true
|
|
72
|
+
}));
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
48
75
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
76
|
+
// Validate transcript path
|
|
77
|
+
if (!transcript_path) {
|
|
78
|
+
// @docs-ref: SessionEnd should not block session ending
|
|
79
|
+
// Just continue even if we can't compress
|
|
52
80
|
console.log(JSON.stringify({
|
|
53
81
|
continue: true,
|
|
54
|
-
|
|
82
|
+
systemMessage: "Warning: No transcript path provided for compression"
|
|
55
83
|
}));
|
|
56
|
-
|
|
57
|
-
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Call the CLI compress command (same as pre-compact)
|
|
88
|
+
const compressor = spawn(cliCommand, ['compress', transcript_path], {
|
|
89
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
let stdout = '';
|
|
93
|
+
let stderr = '';
|
|
94
|
+
|
|
95
|
+
compressor.stdout.on('data', (data) => {
|
|
96
|
+
stdout += data.toString();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
compressor.stderr.on('data', (data) => {
|
|
100
|
+
stderr += data.toString();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
compressor.on('close', (code) => {
|
|
104
|
+
if (code !== 0) {
|
|
105
|
+
// Check if it's the Claude Code executable error
|
|
106
|
+
if (stderr.includes('Claude Code executable not found')) {
|
|
107
|
+
// This is expected when running outside Claude Code context
|
|
108
|
+
// @docs-ref: SessionEnd hooks should not use hookSpecificOutput
|
|
109
|
+
console.log(JSON.stringify({
|
|
110
|
+
continue: true,
|
|
111
|
+
suppressOutput: true
|
|
112
|
+
}));
|
|
113
|
+
process.exit(0);
|
|
114
|
+
} else {
|
|
115
|
+
// Real error - report it but allow session to end
|
|
116
|
+
// @docs-ref: Always allow session to end, use systemMessage for errors
|
|
117
|
+
console.log(JSON.stringify({
|
|
118
|
+
continue: true,
|
|
119
|
+
systemMessage: `Memory compression failed: ${stderr}`
|
|
120
|
+
}));
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
// Success! Memory compressed before session clear
|
|
125
|
+
// @docs-ref: SessionEnd hooks should not use hookSpecificOutput
|
|
126
|
+
console.log(JSON.stringify({
|
|
127
|
+
continue: true,
|
|
128
|
+
suppressOutput: true,
|
|
129
|
+
systemMessage: "Memory compressed successfully before session clear"
|
|
130
|
+
}));
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
compressor.on('error', (error) => {
|
|
136
|
+
// Report error but allow session to end
|
|
137
|
+
// @docs-ref: Always allow session to end, use systemMessage for errors
|
|
58
138
|
console.log(JSON.stringify({
|
|
59
139
|
continue: true,
|
|
60
|
-
systemMessage: `
|
|
140
|
+
systemMessage: `Failed to start compression: ${error.message}`
|
|
61
141
|
}));
|
|
62
|
-
|
|
142
|
+
process.exit(0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
} catch (error) {
|
|
146
|
+
// Report error but allow session to end
|
|
147
|
+
// @docs-ref: Always allow session to end, use systemMessage for errors
|
|
148
|
+
console.log(JSON.stringify({
|
|
149
|
+
continue: true,
|
|
150
|
+
systemMessage: `Hook error: ${error.message}`
|
|
151
|
+
}));
|
|
152
|
+
process.exit(0);
|
|
63
153
|
}
|
|
64
|
-
|
|
65
|
-
process.exit(0);
|
|
66
|
-
|
|
67
|
-
} catch (error) {
|
|
68
|
-
// Report error but allow session to end
|
|
69
|
-
console.log(JSON.stringify({
|
|
70
|
-
continue: true,
|
|
71
|
-
systemMessage: `Hook error: ${error.message}`
|
|
72
|
-
}));
|
|
73
|
-
process.exit(0);
|
|
74
|
-
}
|
|
154
|
+
});
|
|
75
155
|
}
|
|
76
156
|
|
|
77
157
|
sessionEndHook();
|