@stackmemoryai/stackmemory 0.5.15 → 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.
- package/dist/cli/commands/ralph.js +152 -0
- package/dist/cli/commands/ralph.js.map +2 -2
- package/dist/integrations/ralph/bridge/ralph-stackmemory-bridge.js +20 -1
- package/dist/integrations/ralph/bridge/ralph-stackmemory-bridge.js.map +2 -2
- package/dist/integrations/ralph/swarm/git-workflow-manager.js +67 -5
- package/dist/integrations/ralph/swarm/git-workflow-manager.js.map +2 -2
- package/dist/integrations/ralph/swarm/swarm-coordinator.js +139 -1
- package/dist/integrations/ralph/swarm/swarm-coordinator.js.map +2 -2
- package/package.json +1 -1
- package/scripts/testing/results/real-performance-results.json +90 -0
- package/scripts/testing/test-tier-migration.js +100 -0
- package/templates/claude-hooks/notify-review-hook.js +219 -30
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Claude Code hook for SMS notifications
|
|
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
|
|
77
|
-
const
|
|
195
|
+
function getChannelNumbers(config) {
|
|
196
|
+
const channel = config.channel || 'whatsapp';
|
|
78
197
|
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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:
|
|
114
|
-
To:
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
}
|