@stackmemoryai/stackmemory 0.5.14 → 0.5.16

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.
@@ -0,0 +1,90 @@
1
+ {
2
+ "timestamp": "2026-01-06T15:16:51.046Z",
3
+ "tests": [
4
+ {
5
+ "test": "status_command",
6
+ "iterations": 3,
7
+ "measurements": [
8
+ 393.8040000000001,
9
+ 396.434667,
10
+ 393.71524999999997
11
+ ],
12
+ "average": 394.6513056666667,
13
+ "unit": "ms"
14
+ },
15
+ {
16
+ "test": "context_operations",
17
+ "operations": [
18
+ {
19
+ "operation": "version_check",
20
+ "duration": 398.65762500000005,
21
+ "success": true
22
+ },
23
+ {
24
+ "operation": "tasks_list",
25
+ "duration": 518.715792,
26
+ "success": true
27
+ }
28
+ ],
29
+ "totalTime": 917.373417,
30
+ "unit": "ms"
31
+ },
32
+ {
33
+ "test": "task_operations",
34
+ "operations": [
35
+ {
36
+ "operation": "add_task",
37
+ "duration": 518.383875,
38
+ "success": true,
39
+ "outputSize": 245
40
+ },
41
+ {
42
+ "operation": "list_tasks",
43
+ "duration": 526.2628340000001,
44
+ "success": true,
45
+ "outputSize": 2751
46
+ },
47
+ {
48
+ "operation": "show_task",
49
+ "duration": 395.6398339999996,
50
+ "success": true,
51
+ "outputSize": 92
52
+ }
53
+ ],
54
+ "unit": "ms"
55
+ },
56
+ {
57
+ "test": "storage_performance",
58
+ "results": {
59
+ "database": {
60
+ "exists": true,
61
+ "size": 602112,
62
+ "sizeFormatted": "588.00 KB",
63
+ "modified": "2026-01-06T15:16:54.814Z"
64
+ },
65
+ "tasks": {
66
+ "exists": true,
67
+ "size": 219410,
68
+ "sizeFormatted": "214.27 KB",
69
+ "lineCount": 249,
70
+ "modified": "2026-01-06T15:16:54.286Z"
71
+ }
72
+ }
73
+ },
74
+ {
75
+ "test": "baseline_comparison",
76
+ "baseline": {
77
+ "taskListing": {
78
+ "withStackMemory": 501.72233400000005,
79
+ "withoutStackMemory": 5000,
80
+ "unit": "ms"
81
+ },
82
+ "taskCreation": {
83
+ "withStackMemory": 500.09279100000003,
84
+ "withoutStackMemory": 30000,
85
+ "unit": "ms"
86
+ }
87
+ }
88
+ }
89
+ ]
90
+ }
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ import Database from 'better-sqlite3';
4
+ import { RailwayOptimizedStorage } from './dist/core/storage/railway-optimized-storage.js';
5
+ import { ConfigManager } from './dist/core/config/config-manager.js';
6
+ import { v4 as uuidv4 } from 'uuid';
7
+
8
+ async function testTierMigration() {
9
+ const db = new Database('.stackmemory/context.db');
10
+ const configManager = new ConfigManager();
11
+
12
+ // Create storage with shorter tier durations for testing
13
+ const storage = new RailwayOptimizedStorage(db, configManager, {
14
+ tiers: {
15
+ hotHours: 0.001, // Very short for testing (3.6 seconds)
16
+ warmDays: 0.0001, // Very short for testing (8.64 seconds)
17
+ compressionScore: 0.5
18
+ }
19
+ });
20
+
21
+ // Create a test trace
22
+ const traceId = `test_${uuidv4()}`;
23
+ const testTrace = {
24
+ id: traceId,
25
+ type: 'test',
26
+ score: 0.7,
27
+ summary: 'Test trace for tier migration',
28
+ metadata: {
29
+ startTime: Date.now() - 1000, // 1 second ago
30
+ endTime: Date.now(),
31
+ filesModified: ['test.js'],
32
+ errorsEncountered: [],
33
+ decisionsRecorded: [],
34
+ causalChain: []
35
+ },
36
+ tools: [
37
+ { tool: 'test', input: 'test input', output: 'test output' }
38
+ ],
39
+ compressed: false
40
+ };
41
+
42
+ console.log('📝 Creating test trace:', testTrace.id);
43
+
44
+ // First, insert the trace into the traces table to satisfy foreign key
45
+ db.prepare(`
46
+ INSERT INTO traces (id, type, score, summary, start_time, end_time,
47
+ files_modified, errors_encountered, decisions_recorded,
48
+ causal_chain, compressed_data, created_at)
49
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
50
+ `).run(
51
+ traceId,
52
+ 'test',
53
+ 0.7,
54
+ 'Test trace for tier migration',
55
+ testTrace.metadata.startTime,
56
+ testTrace.metadata.endTime,
57
+ JSON.stringify(testTrace.metadata.filesModified),
58
+ JSON.stringify(testTrace.metadata.errorsEncountered),
59
+ JSON.stringify(testTrace.metadata.decisionsRecorded),
60
+ testTrace.metadata.causalChain.length,
61
+ JSON.stringify(testTrace),
62
+ Date.now()
63
+ );
64
+
65
+ // Store the trace in storage tiers
66
+ const tier = await storage.storeTrace(testTrace);
67
+ console.log(`✅ Stored in ${tier} tier`);
68
+
69
+ // Check storage location
70
+ const location = db.prepare('SELECT * FROM storage_tiers WHERE trace_id = ?').get(testTrace.id);
71
+ console.log('📍 Initial location:', location);
72
+
73
+ // Wait a moment for the trace to age
74
+ console.log('⏳ Waiting 5 seconds for trace to age...');
75
+ await new Promise(resolve => setTimeout(resolve, 5000));
76
+
77
+ // Trigger migration
78
+ console.log('🔄 Triggering migration...');
79
+ const results = await storage.migrateTiers();
80
+ console.log('Migration results:', results);
81
+
82
+ // Check new location
83
+ const newLocation = db.prepare('SELECT * FROM storage_tiers WHERE trace_id = ?').get(testTrace.id);
84
+ console.log('📍 New location:', newLocation);
85
+
86
+ // Try to retrieve the trace
87
+ console.log('🔍 Retrieving trace after migration...');
88
+ const retrieved = await storage.retrieveTrace(testTrace.id);
89
+ console.log('✅ Retrieved:', retrieved ? 'Success' : 'Failed');
90
+
91
+ if (retrieved) {
92
+ console.log(' ID matches:', retrieved.id === testTrace.id);
93
+ console.log(' Summary matches:', retrieved.summary === testTrace.summary);
94
+ }
95
+
96
+ db.close();
97
+ console.log('✨ Test complete!');
98
+ }
99
+
100
+ testTierMigration().catch(console.error);
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Claude Code hook for SMS notifications on review-ready events
3
+ * Claude Code hook for WhatsApp/SMS notifications
4
4
  *
5
5
  * Triggers notifications when:
6
+ * - AskUserQuestion tool is used (allows remote response)
6
7
  * - PR is created
7
8
  * - Task is marked complete
8
9
  * - User explicitly requests notification
@@ -15,7 +16,35 @@ const path = require('path');
15
16
  const os = require('os');
16
17
  const https = require('https');
17
18
 
19
+ // Load .env files (check multiple locations)
20
+ const envPaths = [
21
+ path.join(process.cwd(), '.env'),
22
+ path.join(os.homedir(), 'Dev/stackmemory/.env'),
23
+ path.join(os.homedir(), '.stackmemory/.env'),
24
+ path.join(os.homedir(), '.env'),
25
+ ];
26
+ for (const envPath of envPaths) {
27
+ if (fs.existsSync(envPath)) {
28
+ try {
29
+ const content = fs.readFileSync(envPath, 'utf8');
30
+ for (const line of content.split('\n')) {
31
+ const match = line.match(/^([^#=]+)=(.*)$/);
32
+ if (match && !process.env[match[1].trim()]) {
33
+ process.env[match[1].trim()] = match[2]
34
+ .trim()
35
+ .replace(/^["']|["']$/g, '');
36
+ }
37
+ }
38
+ } catch {}
39
+ }
40
+ }
41
+
18
42
  const CONFIG_PATH = path.join(os.homedir(), '.stackmemory', 'sms-notify.json');
43
+ const DEBUG_LOG = path.join(
44
+ os.homedir(),
45
+ '.stackmemory',
46
+ 'claude-session-debug.log'
47
+ );
19
48
 
20
49
  function loadConfig() {
21
50
  try {
@@ -23,13 +52,103 @@ function loadConfig() {
23
52
  return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
24
53
  }
25
54
  } catch {}
26
- return { enabled: false };
55
+ return { enabled: false, pendingPrompts: [] };
56
+ }
57
+
58
+ function saveConfig(config) {
59
+ try {
60
+ const dir = path.join(os.homedir(), '.stackmemory');
61
+ if (!fs.existsSync(dir)) {
62
+ fs.mkdirSync(dir, { recursive: true });
63
+ }
64
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
65
+ } catch (err) {
66
+ console.error('[notify-hook] Failed to save config:', err.message);
67
+ }
68
+ }
69
+
70
+ function logDebug(event, data) {
71
+ try {
72
+ const entry = `[${new Date().toISOString()}] ${event}: ${typeof data === 'string' ? data : JSON.stringify(data)}\n`;
73
+ fs.appendFileSync(DEBUG_LOG, entry);
74
+ } catch {}
75
+ }
76
+
77
+ function savePendingPrompt(prompt) {
78
+ try {
79
+ const config = loadConfig();
80
+ if (!config.pendingPrompts) {
81
+ config.pendingPrompts = [];
82
+ }
83
+ config.pendingPrompts.push(prompt);
84
+ // Keep only last 10 prompts
85
+ if (config.pendingPrompts.length > 10) {
86
+ config.pendingPrompts = config.pendingPrompts.slice(-10);
87
+ }
88
+ saveConfig(config);
89
+ logDebug('PENDING_PROMPT', {
90
+ id: prompt.id,
91
+ options: prompt.options.length,
92
+ });
93
+ } catch (err) {
94
+ console.error('[notify-hook] Failed to save pending prompt:', err.message);
95
+ }
27
96
  }
28
97
 
29
98
  function shouldNotify(toolName, toolInput, output) {
30
99
  const config = loadConfig();
31
100
  if (!config.enabled) return null;
32
101
 
102
+ // AskUserQuestion - send question via WhatsApp for remote response
103
+ if (toolName === 'AskUserQuestion') {
104
+ const questions = toolInput?.questions || [];
105
+ if (questions.length === 0) return null;
106
+
107
+ // Take first question (most common case)
108
+ const q = questions[0];
109
+ const promptId = Math.random().toString(36).substring(2, 10);
110
+
111
+ // Build message text
112
+ let messageText = q.question;
113
+ const options = [];
114
+
115
+ if (q.options && q.options.length > 0) {
116
+ messageText += '\n';
117
+ q.options.forEach((opt, i) => {
118
+ const key = String(i + 1);
119
+ messageText += `${key}. ${opt.label}`;
120
+ if (opt.description) {
121
+ messageText += ` - ${opt.description}`;
122
+ }
123
+ messageText += '\n';
124
+ options.push({ key, label: opt.label });
125
+ });
126
+ // Add "Other" option
127
+ const otherKey = String(q.options.length + 1);
128
+ messageText += `${otherKey}. Other (type your answer)`;
129
+ options.push({ key: otherKey, label: 'Other' });
130
+ }
131
+
132
+ // Store pending prompt in format webhook expects
133
+ const pendingPrompt = {
134
+ id: promptId,
135
+ timestamp: new Date().toISOString(),
136
+ message: q.question,
137
+ options: options,
138
+ type: options.length > 0 ? 'options' : 'freeform',
139
+ expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hour
140
+ };
141
+ savePendingPrompt(pendingPrompt);
142
+
143
+ return {
144
+ type: 'custom',
145
+ title: 'Claude needs your input',
146
+ message: messageText,
147
+ promptId: promptId,
148
+ isQuestion: true,
149
+ };
150
+ }
151
+
33
152
  // Check for PR creation
34
153
  if (toolName === 'Bash') {
35
154
  const cmd = toolInput?.command || '';
@@ -73,30 +192,71 @@ function shouldNotify(toolName, toolInput, output) {
73
192
  return null;
74
193
  }
75
194
 
76
- function sendNotification(notification) {
77
- const config = loadConfig();
195
+ function getChannelNumbers(config) {
196
+ const channel = config.channel || 'whatsapp';
78
197
 
79
- if (
80
- !config.accountSid ||
81
- !config.authToken ||
82
- !config.fromNumber ||
83
- !config.toNumber
84
- ) {
85
- // Try env vars
86
- const sid = process.env.TWILIO_ACCOUNT_SID;
87
- const token = process.env.TWILIO_AUTH_TOKEN;
88
- const from = process.env.TWILIO_FROM_NUMBER;
89
- const to = process.env.TWILIO_TO_NUMBER;
90
-
91
- if (!sid || !token || !from || !to) {
92
- console.error('[notify-hook] Missing Twilio credentials');
93
- return;
198
+ if (channel === 'whatsapp') {
199
+ const from = config.whatsappFromNumber || config.fromNumber;
200
+ const to = config.whatsappToNumber || config.toNumber;
201
+ if (from && to) {
202
+ return {
203
+ from: from.startsWith('whatsapp:') ? from : `whatsapp:${from}`,
204
+ to: to.startsWith('whatsapp:') ? to : `whatsapp:${to}`,
205
+ channel: 'whatsapp',
206
+ };
94
207
  }
208
+ }
95
209
 
96
- config.accountSid = sid;
97
- config.authToken = token;
98
- config.fromNumber = from;
99
- config.toNumber = to;
210
+ // Fallback to SMS
211
+ const from = config.smsFromNumber || config.fromNumber;
212
+ const to = config.smsToNumber || config.toNumber;
213
+ if (from && to) {
214
+ return { from, to, channel: 'sms' };
215
+ }
216
+
217
+ return null;
218
+ }
219
+
220
+ function sendNotification(notification) {
221
+ let config = loadConfig();
222
+
223
+ // Apply env vars
224
+ config.accountSid = config.accountSid || process.env.TWILIO_ACCOUNT_SID;
225
+ config.authToken = config.authToken || process.env.TWILIO_AUTH_TOKEN;
226
+ config.channel = config.channel || process.env.TWILIO_CHANNEL || 'whatsapp';
227
+
228
+ // WhatsApp numbers
229
+ config.whatsappFromNumber =
230
+ config.whatsappFromNumber || process.env.TWILIO_WHATSAPP_FROM;
231
+ config.whatsappToNumber =
232
+ config.whatsappToNumber || process.env.TWILIO_WHATSAPP_TO;
233
+
234
+ // SMS numbers (fallback)
235
+ config.smsFromNumber =
236
+ config.smsFromNumber ||
237
+ process.env.TWILIO_SMS_FROM ||
238
+ process.env.TWILIO_FROM_NUMBER;
239
+ config.smsToNumber =
240
+ config.smsToNumber ||
241
+ process.env.TWILIO_SMS_TO ||
242
+ process.env.TWILIO_TO_NUMBER;
243
+
244
+ // Legacy support
245
+ config.fromNumber = config.fromNumber || process.env.TWILIO_FROM_NUMBER;
246
+ config.toNumber = config.toNumber || process.env.TWILIO_TO_NUMBER;
247
+
248
+ if (!config.accountSid || !config.authToken) {
249
+ console.error('[notify-hook] Missing Twilio credentials');
250
+ return;
251
+ }
252
+
253
+ const numbers = getChannelNumbers(config);
254
+ if (!numbers) {
255
+ console.error(
256
+ '[notify-hook] Missing phone numbers for channel:',
257
+ config.channel
258
+ );
259
+ return;
100
260
  }
101
261
 
102
262
  let message = `${notification.title}\n\n${notification.message}`;
@@ -109,9 +269,22 @@ function sendNotification(notification) {
109
269
  message += '\nReply with number to select';
110
270
  }
111
271
 
272
+ // For questions, add reply instruction
273
+ if (notification.isQuestion) {
274
+ message += '\n\nReply with your choice number or type your answer.';
275
+ if (notification.promptId) {
276
+ message += `\n[ID: ${notification.promptId}]`;
277
+ }
278
+ }
279
+
280
+ // Add session link if available
281
+ if (notification.sessionId) {
282
+ message += `\n\nSession: https://claude.ai/share/${notification.sessionId}`;
283
+ }
284
+
112
285
  const postData = new URLSearchParams({
113
- From: config.fromNumber,
114
- To: config.toNumber,
286
+ From: numbers.from,
287
+ To: numbers.to,
115
288
  Body: message,
116
289
  }).toString();
117
290
 
@@ -132,11 +305,23 @@ function sendNotification(notification) {
132
305
  };
133
306
 
134
307
  const req = https.request(options, (res) => {
135
- if (res.statusCode === 201) {
136
- console.error(`[notify-hook] Sent: ${notification.title}`);
137
- } else {
138
- console.error(`[notify-hook] Failed: ${res.statusCode}`);
139
- }
308
+ let body = '';
309
+ res.on('data', (chunk) => (body += chunk));
310
+ res.on('end', () => {
311
+ if (res.statusCode === 201) {
312
+ console.error(
313
+ `[notify-hook] Sent via ${numbers.channel}: ${notification.title}`
314
+ );
315
+ logDebug('MESSAGE_SENT', {
316
+ channel: numbers.channel,
317
+ title: notification.title,
318
+ promptId: notification.promptId,
319
+ });
320
+ } else {
321
+ console.error(`[notify-hook] Failed (${res.statusCode}): ${body}`);
322
+ logDebug('MESSAGE_FAILED', { status: res.statusCode, error: body });
323
+ }
324
+ });
140
325
  });
141
326
 
142
327
  req.on('error', (e) => {
@@ -156,15 +341,19 @@ process.stdin.on('end', () => {
156
341
  const hookData = JSON.parse(input);
157
342
  const { tool_name, tool_input, tool_output } = hookData;
158
343
 
344
+ logDebug('PostToolUse', { tool: tool_name, session: hookData.session_id });
345
+
159
346
  const notification = shouldNotify(tool_name, tool_input, tool_output);
160
347
 
161
348
  if (notification) {
349
+ notification.sessionId = hookData.session_id;
162
350
  sendNotification(notification);
163
351
  }
164
352
 
165
353
  // Always allow (post-tool hooks don't block)
166
354
  console.log(JSON.stringify({ status: 'ok' }));
167
355
  } catch (err) {
356
+ logDebug('ERROR', err.message);
168
357
  console.error('[notify-hook] Error:', err.message);
169
358
  console.log(JSON.stringify({ status: 'ok' }));
170
359
  }