@stackmemoryai/stackmemory 0.5.15 → 0.5.17
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/hooks/sms-action-runner.js +84 -16
- package/dist/hooks/sms-action-runner.js.map +2 -2
- package/dist/hooks/sms-webhook.js +94 -16
- package/dist/hooks/sms-webhook.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/auto-background-hook.js +9 -8
- package/templates/claude-hooks/notify-review-hook.js +219 -30
- package/templates/claude-hooks/sms-response-handler.js +8 -19
|
@@ -110,19 +110,19 @@ process.stdin.on('end', () => {
|
|
|
110
110
|
// Only process Bash tool
|
|
111
111
|
if (tool_name !== 'Bash') {
|
|
112
112
|
// Allow other tools through unchanged
|
|
113
|
-
console.log(JSON.stringify({
|
|
113
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
114
114
|
return;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
const command = tool_input?.command;
|
|
118
118
|
if (!command) {
|
|
119
|
-
console.log(JSON.stringify({
|
|
119
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
// Already backgrounded
|
|
124
124
|
if (tool_input.run_in_background === true) {
|
|
125
|
-
console.log(JSON.stringify({
|
|
125
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
128
|
|
|
@@ -135,22 +135,23 @@ process.stdin.on('end', () => {
|
|
|
135
135
|
);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
// Modify the tool input to add run_in_background
|
|
138
|
+
// Modify the tool input to add run_in_background using correct schema
|
|
139
139
|
console.log(
|
|
140
140
|
JSON.stringify({
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
hookEventName: 'PreToolUse',
|
|
142
|
+
permissionDecision: 'allow',
|
|
143
|
+
updatedInput: {
|
|
143
144
|
...tool_input,
|
|
144
145
|
run_in_background: true,
|
|
145
146
|
},
|
|
146
147
|
})
|
|
147
148
|
);
|
|
148
149
|
} else {
|
|
149
|
-
console.log(JSON.stringify({
|
|
150
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
150
151
|
}
|
|
151
152
|
} catch (err) {
|
|
152
153
|
// On error, allow the command through unchanged
|
|
153
154
|
console.error('[auto-bg] Error:', err.message);
|
|
154
|
-
console.log(JSON.stringify({
|
|
155
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
155
156
|
}
|
|
156
157
|
});
|
|
@@ -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
|
}
|
|
@@ -139,13 +139,9 @@ process.stdin.on('end', () => {
|
|
|
139
139
|
|
|
140
140
|
clearLatestResponse();
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
context: context,
|
|
146
|
-
user_message: `[SMS Response] User replied: "${latestResponse.response}"`,
|
|
147
|
-
})
|
|
148
|
-
);
|
|
142
|
+
// Log context to stderr for visibility, allow the tool
|
|
143
|
+
console.error(`[sms-hook] Context: ${JSON.stringify(context)}`);
|
|
144
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
149
145
|
return;
|
|
150
146
|
}
|
|
151
147
|
|
|
@@ -162,24 +158,17 @@ process.stdin.on('end', () => {
|
|
|
162
158
|
)
|
|
163
159
|
.join('\n');
|
|
164
160
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
context: {
|
|
169
|
-
type: 'sms_actions_executed',
|
|
170
|
-
results,
|
|
171
|
-
},
|
|
172
|
-
user_message: `[SMS Actions] Executed queued actions:\n${summary}`,
|
|
173
|
-
})
|
|
174
|
-
);
|
|
161
|
+
// Log results to stderr for visibility, allow the tool
|
|
162
|
+
console.error(`[sms-hook] Actions summary:\n${summary}`);
|
|
163
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
175
164
|
return;
|
|
176
165
|
}
|
|
177
166
|
}
|
|
178
167
|
|
|
179
168
|
// Default: allow everything
|
|
180
|
-
console.log(JSON.stringify({
|
|
169
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
181
170
|
} catch (err) {
|
|
182
171
|
console.error('[sms-hook] Error:', err.message);
|
|
183
|
-
console.log(JSON.stringify({
|
|
172
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
184
173
|
}
|
|
185
174
|
});
|