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/hooks/hook-utils.js
DELETED
|
@@ -1,318 +0,0 @@
|
|
|
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
|
-
};
|