claude-mem 2.0.10 → 2.1.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/hook-utils.js +318 -0
- package/hooks/pre-compact.js +41 -121
- package/hooks/session-end.js +55 -135
- package/hooks/session-start.js +54 -169
- package/package.json +1 -1
- package/src/claude-mem.js +0 -859
package/claude-mem
CHANGED
|
Binary file
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for Claude Code hooks
|
|
3
|
+
*
|
|
4
|
+
* This module provides common functionality for all hook files to eliminate
|
|
5
|
+
* duplicated code patterns across pre-compact.js, session-start.js, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from 'child_process';
|
|
9
|
+
import { readFileSync, existsSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
|
|
13
|
+
// Import constants - note: hooks are JavaScript, not TypeScript
|
|
14
|
+
const PACKAGE_NAME = 'claude-mem';
|
|
15
|
+
const CLAUDE_MEM_BASE = '.claude-mem';
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// HOOK CONFIGURATION LOADING
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Load hook configuration from settings (eliminates ~33 lines per hook)
|
|
23
|
+
*/
|
|
24
|
+
export function loadHookConfig() {
|
|
25
|
+
const settingsPath = join(homedir(), CLAUDE_MEM_BASE, 'settings.json');
|
|
26
|
+
|
|
27
|
+
if (!existsSync(settingsPath)) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const content = readFileSync(settingsPath, 'utf8');
|
|
33
|
+
return JSON.parse(content);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.warn(`Warning: Could not load hook config: ${error.message}`);
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// STDIN JSON READING
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read and parse JSON from stdin (removes 30+ duplicate lines)
|
|
46
|
+
* Returns a Promise that resolves with the parsed JSON data
|
|
47
|
+
*/
|
|
48
|
+
export function readStdinJson() {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
let input = '';
|
|
51
|
+
|
|
52
|
+
process.stdin.on('data', chunk => {
|
|
53
|
+
input += chunk;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
process.stdin.on('end', () => {
|
|
57
|
+
try {
|
|
58
|
+
const data = JSON.parse(input);
|
|
59
|
+
resolve(data);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
reject(new Error(`Invalid JSON input: ${error.message}`));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
process.stdin.on('error', error => {
|
|
66
|
+
reject(new Error(`Stdin error: ${error.message}`));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// HOOK RESPONSE HELPERS
|
|
73
|
+
// =============================================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Standard hook response objects (matches constants.ts HOOK_RESPONSES pattern)
|
|
77
|
+
*/
|
|
78
|
+
export const HookResponse = {
|
|
79
|
+
/**
|
|
80
|
+
* Success response with optional message (matches HOOK_RESPONSES.SUCCESS pattern)
|
|
81
|
+
*/
|
|
82
|
+
success(hookEventName, message) {
|
|
83
|
+
return {
|
|
84
|
+
hookSpecificOutput: {
|
|
85
|
+
hookEventName,
|
|
86
|
+
status: "success",
|
|
87
|
+
message
|
|
88
|
+
},
|
|
89
|
+
suppressOutput: true
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Skipped response (matches HOOK_RESPONSES.SKIPPED pattern)
|
|
95
|
+
*/
|
|
96
|
+
skipped(hookEventName, message) {
|
|
97
|
+
return {
|
|
98
|
+
hookSpecificOutput: {
|
|
99
|
+
hookEventName,
|
|
100
|
+
status: "skipped",
|
|
101
|
+
message
|
|
102
|
+
},
|
|
103
|
+
suppressOutput: true
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Error response (matches HOOK_RESPONSES.ERROR pattern)
|
|
109
|
+
*/
|
|
110
|
+
error(hookEventName, message) {
|
|
111
|
+
return {
|
|
112
|
+
continue: false,
|
|
113
|
+
stopReason: message,
|
|
114
|
+
suppressOutput: true,
|
|
115
|
+
hookSpecificOutput: {
|
|
116
|
+
hookEventName,
|
|
117
|
+
status: "error",
|
|
118
|
+
message
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Permission response for tool use
|
|
125
|
+
*/
|
|
126
|
+
permission(decision, reason = '') {
|
|
127
|
+
return {
|
|
128
|
+
continue: true,
|
|
129
|
+
permissionDecision: decision, // 'allow' or 'deny'
|
|
130
|
+
permissionDecisionReason: reason
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// =============================================================================
|
|
136
|
+
// CLI SPAWNING UTILITIES
|
|
137
|
+
// =============================================================================
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Spawn CLI command (consolidates 90+ lines of spawn logic)
|
|
141
|
+
*/
|
|
142
|
+
export function spawnCLI(command, args = [], options = {}) {
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
const defaultOptions = {
|
|
145
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
146
|
+
env: process.env,
|
|
147
|
+
timeout: 30000, // 30 second default timeout
|
|
148
|
+
...options
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Find CLI executable
|
|
152
|
+
const possibleCommands = [
|
|
153
|
+
PACKAGE_NAME,
|
|
154
|
+
`npx ${PACKAGE_NAME}`,
|
|
155
|
+
join(process.cwd(), 'node_modules', '.bin', PACKAGE_NAME),
|
|
156
|
+
join(homedir(), '.npm-global', 'bin', PACKAGE_NAME)
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
let claudeMemCmd = command;
|
|
160
|
+
if (!claudeMemCmd) {
|
|
161
|
+
// Try to find the best command
|
|
162
|
+
claudeMemCmd = PACKAGE_NAME; // Default fallback
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const child = spawn(claudeMemCmd, args, defaultOptions);
|
|
166
|
+
|
|
167
|
+
let stdout = '';
|
|
168
|
+
let stderr = '';
|
|
169
|
+
|
|
170
|
+
child.stdout?.on('data', (data) => {
|
|
171
|
+
stdout += data.toString();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
child.stderr?.on('data', (data) => {
|
|
175
|
+
stderr += data.toString();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
child.on('close', (code) => {
|
|
179
|
+
if (code === 0) {
|
|
180
|
+
resolve({
|
|
181
|
+
code,
|
|
182
|
+
stdout: stdout.trim(),
|
|
183
|
+
stderr: stderr.trim()
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
reject(new Error(`Command failed with code ${code}: ${stderr.trim()}`));
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
child.on('error', (error) => {
|
|
191
|
+
reject(new Error(`Failed to spawn command: ${error.message}`));
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Handle timeout
|
|
195
|
+
if (defaultOptions.timeout) {
|
|
196
|
+
setTimeout(() => {
|
|
197
|
+
child.kill('SIGTERM');
|
|
198
|
+
reject(new Error(`Command timed out after ${defaultOptions.timeout}ms`));
|
|
199
|
+
}, defaultOptions.timeout);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Run CLI command with specific subcommand and arguments
|
|
206
|
+
*/
|
|
207
|
+
export async function runClaudeMemCommand(subcommand, args = [], options = {}) {
|
|
208
|
+
const fullArgs = [subcommand, ...args];
|
|
209
|
+
return spawnCLI(PACKAGE_NAME, fullArgs, options);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// =============================================================================
|
|
213
|
+
// PROJECT NAME EXTRACTION
|
|
214
|
+
// =============================================================================
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Extract project name from transcript path
|
|
218
|
+
* Consolidates the 3 different implementations found in the codebase
|
|
219
|
+
*/
|
|
220
|
+
export function extractProjectName(transcriptPath) {
|
|
221
|
+
if (!transcriptPath) {
|
|
222
|
+
return 'unknown';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Standard pattern: Scripts/[project-name]/...
|
|
226
|
+
const match = transcriptPath.match(/Scripts[\\/\\]([^\\/\\]+)/);
|
|
227
|
+
if (match) {
|
|
228
|
+
return match[1];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Fallback: try to extract from directory structure
|
|
232
|
+
const parts = transcriptPath.split(/[\\/]/);
|
|
233
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
234
|
+
if (parts[i] === 'Scripts' && parts[i + 1]) {
|
|
235
|
+
return parts[i + 1];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return 'unknown';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// =============================================================================
|
|
243
|
+
// UTILITY HELPERS
|
|
244
|
+
// =============================================================================
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Safe JSON output for hook responses
|
|
248
|
+
*/
|
|
249
|
+
export function outputJSON(data) {
|
|
250
|
+
try {
|
|
251
|
+
console.log(JSON.stringify(data, null, 2));
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error(JSON.stringify({
|
|
254
|
+
continue: false,
|
|
255
|
+
stopReason: `Failed to serialize response: ${error.message}`
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Debug logging helper
|
|
262
|
+
*/
|
|
263
|
+
export function debugLog(message) {
|
|
264
|
+
if (process.env.CLAUDE_MEM_DEBUG) {
|
|
265
|
+
console.error(`[DEBUG] ${message}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Validate hook payload structure
|
|
271
|
+
*/
|
|
272
|
+
export function validateHookPayload(payload, requiredFields = []) {
|
|
273
|
+
const errors = [];
|
|
274
|
+
|
|
275
|
+
if (!payload || typeof payload !== 'object') {
|
|
276
|
+
errors.push('Payload must be an object');
|
|
277
|
+
return errors;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
requiredFields.forEach(field => {
|
|
281
|
+
if (!(field in payload)) {
|
|
282
|
+
errors.push(`Missing required field: ${field}`);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return errors;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Create hook execution context
|
|
291
|
+
*/
|
|
292
|
+
export function createHookContext(payload) {
|
|
293
|
+
return {
|
|
294
|
+
sessionId: payload.session_id || 'unknown',
|
|
295
|
+
transcriptPath: payload.transcript_path || null,
|
|
296
|
+
hookEventName: payload.hook_event_name || 'unknown',
|
|
297
|
+
source: payload.source || 'unknown', // For SessionStart hooks
|
|
298
|
+
timestamp: new Date().toISOString(),
|
|
299
|
+
projectName: payload.transcript_path ? extractProjectName(payload.transcript_path) : null
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// =============================================================================
|
|
304
|
+
// EXPORT ALL UTILITIES
|
|
305
|
+
// =============================================================================
|
|
306
|
+
|
|
307
|
+
export default {
|
|
308
|
+
loadHookConfig,
|
|
309
|
+
readStdinJson,
|
|
310
|
+
HookResponse,
|
|
311
|
+
spawnCLI,
|
|
312
|
+
runClaudeMemCommand,
|
|
313
|
+
extractProjectName,
|
|
314
|
+
outputJSON,
|
|
315
|
+
debugLog,
|
|
316
|
+
validateHookPayload,
|
|
317
|
+
createHookContext
|
|
318
|
+
};
|
package/hooks/pre-compact.js
CHANGED
|
@@ -1,138 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
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
|
|
4
|
+
* Claude Memory System - Pre-Compact Hook (Refactored)
|
|
5
|
+
* Uses shared utilities to eliminate code duplication
|
|
25
6
|
*/
|
|
26
7
|
|
|
27
|
-
import {
|
|
28
|
-
import { join, dirname } from 'path';
|
|
29
|
-
import { fileURLToPath } from 'url';
|
|
30
|
-
import { readFileSync, existsSync } from 'fs';
|
|
31
|
-
|
|
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)) {
|
|
38
|
-
try {
|
|
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
|
-
}
|
|
8
|
+
import { loadHookConfig, readStdinJson, HookResponse, spawnCLI } from './hook-utils.js';
|
|
45
9
|
|
|
46
10
|
async function preCompactHook() {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
11
|
+
try {
|
|
12
|
+
// Load config and read input using utilities
|
|
13
|
+
const config = loadHookConfig();
|
|
14
|
+
const data = await readStdinJson();
|
|
15
|
+
|
|
16
|
+
const transcriptPath = data.transcript_path;
|
|
17
|
+
|
|
18
|
+
if (!transcriptPath) {
|
|
19
|
+
console.log(JSON.stringify(HookResponse.error("No transcript path provided")));
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Use spawnCLI utility for compression
|
|
24
|
+
const cliCommand = config.cliCommand || 'claude-mem';
|
|
25
|
+
|
|
55
26
|
try {
|
|
56
|
-
|
|
57
|
-
const transcriptPath = data.transcript_path;
|
|
58
|
-
|
|
59
|
-
if (!transcriptPath) {
|
|
60
|
-
// Output error in Claude Code expected format
|
|
61
|
-
// @docs-ref: continue: false with stopReason for blocking operations
|
|
62
|
-
console.log(JSON.stringify({
|
|
63
|
-
continue: false,
|
|
64
|
-
stopReason: "No transcript path provided"
|
|
65
|
-
}));
|
|
66
|
-
process.exit(2); // Exit 2 tells Claude Code to show error
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Call the CLI compress command using configured name
|
|
70
|
-
const compressor = spawn(cliCommand, ['compress', transcriptPath], {
|
|
71
|
-
stdio: ['ignore', 'pipe', 'pipe'] // Capture output instead of inherit
|
|
72
|
-
});
|
|
27
|
+
await spawnCLI(cliCommand, ['compress', transcriptPath]);
|
|
73
28
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
compressor.stderr.on('data', (data) => {
|
|
82
|
-
stderr += data.toString();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
compressor.on('close', (code) => {
|
|
86
|
-
if (code !== 0) {
|
|
87
|
-
// Check if it's the Claude Code executable error
|
|
88
|
-
if (stderr.includes('Claude Code executable not found')) {
|
|
89
|
-
// This is expected when running outside Claude Code context
|
|
90
|
-
// Just output success since we can't compress without Claude SDK
|
|
91
|
-
// @docs-ref: PreCompact hooks should only use continue field, not hookSpecificOutput
|
|
92
|
-
console.log(JSON.stringify({
|
|
93
|
-
continue: true,
|
|
94
|
-
suppressOutput: true,
|
|
95
|
-
systemMessage: "Memory compression skipped - Claude SDK not available in this context"
|
|
96
|
-
}));
|
|
97
|
-
process.exit(0);
|
|
98
|
-
} else {
|
|
99
|
-
// Real error - report it
|
|
100
|
-
// @docs-ref: Use continue: false with stopReason for errors
|
|
101
|
-
console.log(JSON.stringify({
|
|
102
|
-
continue: false,
|
|
103
|
-
stopReason: `Compression failed: ${stderr}`
|
|
104
|
-
}));
|
|
105
|
-
process.exit(2);
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
// Success! Output confirmation
|
|
109
|
-
// @docs-ref: PreCompact hooks should only use continue field for success
|
|
110
|
-
console.log(JSON.stringify({
|
|
111
|
-
continue: true,
|
|
112
|
-
suppressOutput: true
|
|
113
|
-
}));
|
|
114
|
-
process.exit(0);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
29
|
+
// Success - output response
|
|
30
|
+
console.log(JSON.stringify({
|
|
31
|
+
continue: true,
|
|
32
|
+
suppressOutput: true
|
|
33
|
+
}));
|
|
34
|
+
process.exit(0);
|
|
117
35
|
|
|
118
|
-
|
|
119
|
-
|
|
36
|
+
} catch (error) {
|
|
37
|
+
// Check for expected Claude SDK error
|
|
38
|
+
if (error.message.includes('Claude Code executable not found')) {
|
|
120
39
|
console.log(JSON.stringify({
|
|
121
|
-
continue:
|
|
122
|
-
|
|
40
|
+
continue: true,
|
|
41
|
+
suppressOutput: true,
|
|
42
|
+
systemMessage: "Memory compression skipped - Claude SDK not available in this context"
|
|
123
43
|
}));
|
|
44
|
+
process.exit(0);
|
|
45
|
+
} else {
|
|
46
|
+
// Real error
|
|
47
|
+
console.log(JSON.stringify(HookResponse.error(`Compression failed: ${error.message}`)));
|
|
124
48
|
process.exit(2);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
} catch (error) {
|
|
128
|
-
// @docs-ref: Use continue: false with stopReason for errors
|
|
129
|
-
console.log(JSON.stringify({
|
|
130
|
-
continue: false,
|
|
131
|
-
stopReason: `Hook error: ${error.message}`
|
|
132
|
-
}));
|
|
133
|
-
process.exit(2);
|
|
49
|
+
}
|
|
134
50
|
}
|
|
135
|
-
|
|
51
|
+
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.log(JSON.stringify(HookResponse.error(`Hook error: ${error.message}`)));
|
|
54
|
+
process.exit(2);
|
|
55
|
+
}
|
|
136
56
|
}
|
|
137
57
|
|
|
138
58
|
preCompactHook();
|