claude-mem 2.0.9 → 2.1.1

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 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
+ };
@@ -1,138 +1,58 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
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
4
+ * Claude Memory System - Pre-Compact Hook (Refactored)
5
+ * Uses shared utilities to eliminate code duplication
25
6
  */
26
7
 
27
- import { spawn } from 'child_process';
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
- let input = '';
48
-
49
- // Read JSON input from stdin
50
- process.stdin.on('data', chunk => {
51
- input += chunk;
52
- });
53
-
54
- process.stdin.on('end', async () => {
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
- const data = JSON.parse(input);
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
- let stdout = '';
75
- let stderr = '';
76
-
77
- compressor.stdout.on('data', (data) => {
78
- stdout += data.toString();
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
- compressor.on('error', (error) => {
119
- // @docs-ref: Use continue: false with stopReason for errors
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: false,
122
- stopReason: `Failed to start compression: ${error.message}`
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();
@@ -1,157 +1,77 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * 🔑 LOCKED by @docs-agent | Change to 🔑 to allow @docs-agent edits
4
+ * Claude Memory System - Session-End Hook (Refactored)
5
+ * Uses shared utilities to eliminate code duplication
5
6
  *
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
11
7
  * Triggers compression when session ends with reason 'clear'
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
8
+ * Always returns continue: true to avoid blocking session termination
32
9
  */
33
10
 
34
- import { spawn } from 'child_process';
35
- import { join, dirname } from 'path';
36
- import { fileURLToPath } from 'url';
37
- import { readFileSync, existsSync } from 'fs';
38
-
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)) {
45
- try {
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
- }
11
+ import { loadHookConfig, readStdinJson, HookResponse, spawnCLI } from './hook-utils.js';
52
12
 
53
13
  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 () => {
14
+ try {
15
+ // Load config and read input using utilities
16
+ const config = loadHookConfig();
17
+ const data = await readStdinJson();
18
+
19
+ const { reason, transcript_path } = data;
20
+
21
+ // Only proceed if reason is 'clear'
22
+ if (reason !== 'clear') {
23
+ console.log(JSON.stringify({ continue: true }));
24
+ process.exit(0);
25
+ }
26
+
27
+ // Validate transcript path
28
+ if (!transcript_path) {
29
+ console.log(JSON.stringify({
30
+ continue: true,
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
+
62
39
  try {
63
- const data = JSON.parse(input);
64
- const { reason, transcript_path, session_id } = data;
40
+ await spawnCLI(cliCommand, ['compress', transcript_path]);
65
41
 
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
- }
42
+ // Success! Memory compressed before session clear
43
+ console.log(JSON.stringify({
44
+ continue: true,
45
+ suppressOutput: true,
46
+ systemMessage: "Memory compressed successfully before session clear"
47
+ }));
75
48
 
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
49
+ } catch (error) {
50
+ // Check for expected Claude SDK error
51
+ if (error.message.includes('Claude Code executable not found')) {
80
52
  console.log(JSON.stringify({
81
53
  continue: true,
82
- systemMessage: "Warning: No transcript path provided for compression"
54
+ suppressOutput: true
83
55
  }));
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
56
+ } else {
57
+ // Real error - report it but allow session to end
138
58
  console.log(JSON.stringify({
139
59
  continue: true,
140
- systemMessage: `Failed to start compression: ${error.message}`
60
+ systemMessage: `Memory compression failed: ${error.message}`
141
61
  }));
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);
62
+ }
153
63
  }
154
- });
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
+ }
155
75
  }
156
76
 
157
77
  sessionEndHook();
@@ -1,190 +1,75 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * 🔒 LOCKED by @docs-agent | Change to 🔑 to allow @docs-agent edits
4
+ * Claude Memory System - Session-Start Hook (Refactored)
5
+ * Uses shared utilities to eliminate code duplication
5
6
  *
6
- * OFFICIAL DOCS: Claude Code SessionStart Hook v2025
7
- * Last Verified: 2025-08-31
8
- * @see https://docs.anthropic.com/en/docs/claude-code/hooks
9
- *
10
- * SessionStart Hook Payload:
11
- * {
12
- * "session_id": "string",
13
- * "transcript_path": "string",
14
- * "hook_event_name": "SessionStart",
15
- * "source": "startup" | "compact" | "vscode" | "web"
16
- * }
17
- *
18
- * The 'source' field indicates how the session was initiated:
19
- * - "startup": New session started normally
20
- * - "compact": Session started after compaction
21
- * - "vscode": Session initiated from VS Code
22
- * - "web": Session initiated from web interface
23
- *
24
- * Valid Response Format:
25
- * Success without context: { "continue": true }
26
- * Success with context: {
27
- * "continue": true,
28
- * "hookSpecificOutput": {
29
- * "hookEventName": "SessionStart",
30
- * "additionalContext": "string" // Context to add to session
31
- * }
32
- * }
33
- * Failure: { "continue": false, "stopReason": "error message" }
34
- *
35
- * @docs-ref: SessionStart supports hookSpecificOutput.additionalContext
36
- * @see https://docs.anthropic.com/claude-code/hooks#sessionstart
7
+ * Always loads context regardless of source (startup/compact/vscode/web)
37
8
  */
38
9
 
39
- import { spawn } from 'child_process';
40
- import { join, dirname, basename } from 'path';
41
- import { fileURLToPath } from 'url';
42
- import { readFileSync, existsSync } from 'fs';
43
-
44
- const __dirname = dirname(fileURLToPath(import.meta.url));
45
-
46
- // Load configuration to get the CLI command name
47
- let cliCommand = 'claude-mem'; // Default fallback
48
- const configPath = join(__dirname, 'config.json');
49
- if (existsSync(configPath)) {
50
- try {
51
- const config = JSON.parse(readFileSync(configPath, 'utf-8'));
52
- cliCommand = config.cliCommand || 'claude-mem';
53
- } catch (e) {
54
- // Fallback to default if config read fails
55
- }
56
- }
10
+ import { loadHookConfig, readStdinJson, HookResponse, spawnCLI } from './hook-utils.js';
11
+ import { dirname, basename } from 'path';
57
12
 
58
13
  async function sessionStartHook() {
59
- let input = '';
60
-
61
- // Read JSON input from stdin
62
- process.stdin.on('data', chunk => {
63
- input += chunk;
64
- });
65
-
66
- process.stdin.on('end', async () => {
67
- try {
68
- const data = JSON.parse(input);
69
-
70
- /**
71
- * @docs-ref: SessionStart payload includes 'source' field
72
- * See: https://docs.anthropic.com/en/docs/claude-code/hooks#sessionstart
73
- *
74
- * The 'source' field indicates the session start context:
75
- * - 'startup': New session (load context)
76
- * - 'compact': Session after compaction (SHOULD load context)
77
- * - 'vscode': VS Code initiated session
78
- * - 'web': Web interface initiated session
79
- *
80
- * IMPORTANT: We always load context regardless of source.
81
- * After compaction, users expect to see their compressed memories loaded.
82
- * The session after compact is effectively a new session with compressed context.
83
- *
84
- * There is no specific indicator for /continue command in the payload,
85
- * so we treat all session starts equally and load available context.
86
- */
87
- // Always proceed to load context for all session types
14
+ try {
15
+ // Load config and read input using utilities
16
+ const config = loadHookConfig();
17
+ const data = await readStdinJson();
18
+
19
+ // Extract project name from transcript path if available
20
+ let projectName = null;
21
+ if (data.transcript_path) {
22
+ const dir = dirname(data.transcript_path);
23
+ const dirName = basename(dir);
88
24
 
89
- // Extract project name from transcript path if available
90
- let projectName = null;
91
- if (data.transcript_path) {
92
- const dir = dirname(data.transcript_path);
93
- const dirName = basename(dir);
94
-
95
- // Extract project name from directory pattern
96
- if (dirName.includes('-Scripts-')) {
97
- const parts = dirName.split('-Scripts-');
98
- if (parts.length > 1) {
99
- projectName = parts[1];
100
- }
101
- } else {
102
- projectName = dirName;
103
- }
104
-
105
- // Sanitize project name to match what compression uses
106
- // This ensures we match the exact project name in the index
107
- if (projectName) {
108
- projectName = projectName.replace(/[^a-zA-Z0-9]/g, '_');
25
+ // Extract project name from directory pattern
26
+ if (dirName.includes('-Scripts-')) {
27
+ const parts = dirName.split('-Scripts-');
28
+ if (parts.length > 1) {
29
+ projectName = parts[1];
109
30
  }
31
+ } else {
32
+ projectName = dirName;
110
33
  }
111
34
 
112
- // Build command to run
113
- const args = ['load-context', '--format', 'session-start'];
114
-
115
- // Add project filter if we have a project name
35
+ // Sanitize project name to match what compression uses
116
36
  if (projectName) {
117
- args.push('--project', projectName);
37
+ projectName = projectName.replace(/[^a-zA-Z0-9]/g, '_');
118
38
  }
39
+ }
40
+
41
+ // Build command to run
42
+ const args = ['load-context', '--format', 'session-start'];
43
+ if (projectName) {
44
+ args.push('--project', projectName);
45
+ }
46
+
47
+ // Use spawnCLI utility for loading context
48
+ const cliCommand = config.cliCommand || 'claude-mem';
49
+
50
+ try {
51
+ const result = await spawnCLI(cliCommand, args);
119
52
 
120
- // Call the CLI load-context command directly
121
- // Use the configured CLI command name (claude-mem)
122
- // This ensures we use the correct version with full summaries
123
- const loader = spawn(cliCommand, args, {
124
- stdio: ['ignore', 'pipe', 'pipe']
125
- });
126
-
127
- let stdout = '';
128
- let stderr = '';
129
-
130
- loader.stdout.on('data', (data) => {
131
- stdout += data.toString();
132
- });
133
-
134
- loader.stderr.on('data', (data) => {
135
- stderr += data.toString();
136
- });
137
-
138
- loader.on('close', (code) => {
139
- if (code !== 0) {
140
- // If load-context fails, just continue without context
141
- // This could happen if no index exists yet
142
- // @docs-ref: Always return valid response format
143
- console.log(JSON.stringify({
144
- continue: true
145
- }));
146
- process.exit(0);
147
- }
148
-
149
- // Output the formatted context for Claude to see using proper JSON format
150
- // @docs-ref: SessionStart supports hookSpecificOutput.additionalContext
151
- if (stdout && stdout.trim()) {
152
- console.log(JSON.stringify({
153
- continue: true,
154
- hookSpecificOutput: {
155
- hookEventName: "SessionStart",
156
- additionalContext: stdout
157
- }
158
- }));
159
- } else {
160
- // No context to add, just continue
161
- console.log(JSON.stringify({
162
- continue: true
163
- }));
164
- }
165
-
166
- process.exit(0);
167
- });
168
-
169
- loader.on('error', () => {
170
- // If there's an error running the command, continue without context
171
- // We don't want to break the session start
172
- // @docs-ref: Always return valid response format
173
- console.log(JSON.stringify({
174
- continue: true
175
- }));
176
- process.exit(0);
177
- });
53
+ // Output the formatted context if we have any
54
+ if (result.stdout && result.stdout.trim()) {
55
+ console.log(JSON.stringify(HookResponse.success("SessionStart", result.stdout)));
56
+ } else {
57
+ console.log(JSON.stringify({ continue: true }));
58
+ }
178
59
 
179
60
  } catch (error) {
180
- // Any errors, just continue without additional context
181
- // @docs-ref: Always return valid response format
182
- console.log(JSON.stringify({
183
- continue: true
184
- }));
185
- process.exit(0);
61
+ // If load-context fails, just continue without context
62
+ // This could happen if no index exists yet
63
+ console.log(JSON.stringify({ continue: true }));
186
64
  }
187
- });
65
+
66
+ process.exit(0);
67
+
68
+ } catch (error) {
69
+ // Any errors, just continue without additional context
70
+ console.log(JSON.stringify({ continue: true }));
71
+ process.exit(0);
72
+ }
188
73
  }
189
74
 
190
75
  sessionStartHook();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem",
3
- "version": "2.0.9",
3
+ "version": "2.1.1",
4
4
  "description": "Memory compression system for Claude Code - persist context across sessions",
5
5
  "keywords": [
6
6
  "claude",
package/src/claude-mem.js CHANGED
@@ -6,6 +6,7 @@ import path, { join } from 'path';
6
6
  import os, { homedir } from 'os';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { createAnalysisPrompt, DEBUG_MESSAGES } from '../dist/constants.js';
9
+ import { log } from '../dist/utils/logger.js';
9
10
 
10
11
  const DEBUG_MODE = true;
11
12
  const CLAUDE_MEM_HOME = path.join(homedir(), '.claude-mem');
@@ -438,9 +439,10 @@ class TranscriptCompressor {
438
439
 
439
440
  return archivePath;
440
441
  } catch (error) {
441
- console.error(`\n❌ COMPRESSION FAILED`);
442
- console.error(` Error: ${error.message}`);
443
- console.error(` Stack: ${error.stack}`);
442
+ log.error('COMPRESSION FAILED', error, {
443
+ errorMessage: error.message,
444
+ stack: error.stack
445
+ });
444
446
  throw error;
445
447
  }
446
448
  }
@@ -823,9 +825,7 @@ async function main() {
823
825
  const args = process.argv.slice(2);
824
826
 
825
827
  if (args.length === 0) {
826
- console.error(
827
- 'Usage: node src/claude-mem.js <transcript-path> [session-id]'
828
- );
828
+ log.error('Usage: node src/claude-mem.js <transcript-path> [session-id]');
829
829
  closeDebugLog();
830
830
  process.exit(1);
831
831
  }
@@ -847,7 +847,7 @@ async function main() {
847
847
  debugLog(`❌ Compression failed: ${error.message}`);
848
848
  debugLog(`💥 Stack trace: ${error.stack}`);
849
849
  closeDebugLog();
850
- console.error('Compression failed:', error.message);
850
+ log.error('Compression failed', error);
851
851
  process.exit(1);
852
852
  }
853
853
  }