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 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();