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 CHANGED
Binary file
@@ -1,58 +1,211 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Claude Memory System - Pre-Compact Hook (Refactored)
5
- * Uses shared utilities to eliminate code duplication
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 { loadHookConfig, readStdinJson, HookResponse, spawnCLI } from './hook-utils.js';
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
- async function preCompactHook() {
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
- // 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
-
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
- await spawnCLI(cliCommand, ['compress', transcriptPath]);
89
+ const data = JSON.parse(input);
90
+ const transcriptPath = data.transcript_path;
28
91
 
29
- // Success - output response
30
- console.log(JSON.stringify({
31
- continue: true,
32
- suppressOutput: true
33
- }));
34
- process.exit(0);
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
- } catch (error) {
37
- // Check for expected Claude SDK error
38
- if (error.message.includes('Claude Code executable not found')) {
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: true,
41
- suppressOutput: true,
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();
@@ -1,77 +1,157 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Claude Memory System - Session-End Hook (Refactored)
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
- * Always returns continue: true to avoid blocking session termination
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 { loadHookConfig, readStdinJson, HookResponse, spawnCLI } from './hook-utils.js';
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
- async function sessionEndHook() {
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
- // 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
-
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
- await spawnCLI(cliCommand, ['compress', transcript_path]);
63
+ const data = JSON.parse(input);
64
+ const { reason, transcript_path, session_id } = data;
41
65
 
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
- }));
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
- } catch (error) {
50
- // Check for expected Claude SDK error
51
- if (error.message.includes('Claude Code executable not found')) {
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
- suppressOutput: true
82
+ systemMessage: "Warning: No transcript path provided for compression"
55
83
  }));
56
- } else {
57
- // Real error - report it but allow session to end
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: `Memory compression failed: ${error.message}`
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();