@stackmemoryai/stackmemory 0.8.0 → 0.8.1

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.
@@ -1,84 +0,0 @@
1
- #!/bin/bash
2
- # Install SMS notification hook for Claude Code (optional)
3
-
4
- set -e
5
-
6
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
- HOOK_SOURCE="$SCRIPT_DIR/../templates/claude-hooks/notify-review-hook.js"
8
- CLAUDE_DIR="$HOME/.claude"
9
- HOOKS_DIR="$CLAUDE_DIR/hooks"
10
- SETTINGS_FILE="$CLAUDE_DIR/settings.json"
11
-
12
- echo "Installing SMS notification hook for Claude Code..."
13
- echo "(Optional feature - requires Twilio setup)"
14
- echo ""
15
-
16
- # Create directories
17
- mkdir -p "$HOOKS_DIR"
18
-
19
- # Copy hook script
20
- HOOK_DEST="$HOOKS_DIR/notify-review-hook.js"
21
- cp "$HOOK_SOURCE" "$HOOK_DEST"
22
- chmod +x "$HOOK_DEST"
23
- echo "Installed hook to $HOOK_DEST"
24
-
25
- # Update Claude Code settings
26
- if [ -f "$SETTINGS_FILE" ]; then
27
- if command -v jq &> /dev/null; then
28
- cp "$SETTINGS_FILE" "$SETTINGS_FILE.bak"
29
-
30
- HOOK_CMD="node $HOOK_DEST"
31
-
32
- # Add to post_tool_use hooks
33
- if jq -e '.hooks.post_tool_use' "$SETTINGS_FILE" > /dev/null 2>&1; then
34
- if ! jq -e ".hooks.post_tool_use | index(\"$HOOK_CMD\")" "$SETTINGS_FILE" > /dev/null 2>&1; then
35
- jq ".hooks.post_tool_use += [\"$HOOK_CMD\"]" "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp"
36
- mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
37
- echo "Added hook to post_tool_use array"
38
- else
39
- echo "Hook already configured"
40
- fi
41
- else
42
- jq ".hooks = (.hooks // {}) | .hooks.post_tool_use = [\"$HOOK_CMD\"]" "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp"
43
- mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
44
- echo "Created hooks.post_tool_use with notify hook"
45
- fi
46
- else
47
- echo ""
48
- echo "jq not found. Please manually add to $SETTINGS_FILE:"
49
- echo ""
50
- echo ' "hooks": {'
51
- echo ' "post_tool_use": ["node '$HOOK_DEST'"]'
52
- echo ' }'
53
- fi
54
- else
55
- cat > "$SETTINGS_FILE" << EOF
56
- {
57
- "hooks": {
58
- "post_tool_use": ["node $HOOK_DEST"]
59
- }
60
- }
61
- EOF
62
- echo "Created settings file with hook"
63
- fi
64
-
65
- echo ""
66
- echo "Notification hook installed!"
67
- echo ""
68
- echo "To enable SMS notifications:"
69
- echo " 1. Set Twilio environment variables:"
70
- echo " export TWILIO_ACCOUNT_SID=your_sid"
71
- echo " export TWILIO_AUTH_TOKEN=your_token"
72
- echo " export TWILIO_FROM_NUMBER=+1234567890"
73
- echo " export TWILIO_TO_NUMBER=+1234567890"
74
- echo ""
75
- echo " 2. Enable notifications:"
76
- echo " stackmemory notify enable"
77
- echo ""
78
- echo " 3. Test:"
79
- echo " stackmemory notify test"
80
- echo ""
81
- echo "Notifications will be sent when:"
82
- echo " - PR is created (gh pr create)"
83
- echo " - Package is published (npm publish)"
84
- echo " - Deployment completes"
@@ -1,103 +0,0 @@
1
- #!/bin/bash
2
- # Auto-setup for StackMemory WhatsApp/SMS webhook loop
3
-
4
- set -e
5
-
6
- WEBHOOK_PORT="${1:-3456}"
7
- TWILIO_ACCOUNT_SID="${TWILIO_ACCOUNT_SID}"
8
- TWILIO_AUTH_TOKEN="${TWILIO_AUTH_TOKEN}"
9
-
10
- echo "=== StackMemory Webhook Setup ==="
11
- echo ""
12
-
13
- # Check dependencies
14
- if ! command -v ngrok &> /dev/null; then
15
- echo "Installing ngrok..."
16
- if command -v brew &> /dev/null; then
17
- brew install ngrok
18
- else
19
- echo "Please install ngrok: https://ngrok.com/download"
20
- exit 1
21
- fi
22
- fi
23
-
24
- # Check Twilio credentials
25
- if [ -z "$TWILIO_ACCOUNT_SID" ] || [ -z "$TWILIO_AUTH_TOKEN" ]; then
26
- echo "Error: Set TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN"
27
- exit 1
28
- fi
29
-
30
- # Kill any existing processes
31
- pkill -f "notify webhook" 2>/dev/null || true
32
- pkill -f "ngrok http $WEBHOOK_PORT" 2>/dev/null || true
33
- sleep 1
34
-
35
- # Start webhook server in background
36
- echo "Starting webhook server on port $WEBHOOK_PORT..."
37
- stackmemory notify webhook -p "$WEBHOOK_PORT" > /tmp/webhook.log 2>&1 &
38
- WEBHOOK_PID=$!
39
- sleep 2
40
-
41
- # Start ngrok in background
42
- echo "Starting ngrok tunnel..."
43
- ngrok http "$WEBHOOK_PORT" --log=stdout > /tmp/ngrok.log 2>&1 &
44
- NGROK_PID=$!
45
- sleep 3
46
-
47
- # Get ngrok public URL
48
- NGROK_URL=$(curl -s http://localhost:4040/api/tunnels | grep -o '"public_url":"https://[^"]*' | head -1 | cut -d'"' -f4)
49
-
50
- if [ -z "$NGROK_URL" ]; then
51
- echo "Error: Could not get ngrok URL. Check /tmp/ngrok.log"
52
- exit 1
53
- fi
54
-
55
- WEBHOOK_URL="${NGROK_URL}/sms/incoming"
56
- echo ""
57
- echo "Webhook URL: $WEBHOOK_URL"
58
-
59
- # Configure Twilio WhatsApp sandbox webhook
60
- echo ""
61
- echo "Configuring Twilio WhatsApp sandbox..."
62
-
63
- # Get sandbox configuration
64
- SANDBOX_RESPONSE=$(curl -s "https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNT_SID}/Sandbox.json" \
65
- -u "${TWILIO_ACCOUNT_SID}:${TWILIO_AUTH_TOKEN}" 2>/dev/null)
66
-
67
- if echo "$SANDBOX_RESPONSE" | grep -q "sms_url"; then
68
- # Update sandbox webhook URL
69
- curl -s -X POST "https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNT_SID}/Sandbox.json" \
70
- -u "${TWILIO_ACCOUNT_SID}:${TWILIO_AUTH_TOKEN}" \
71
- -d "SmsUrl=${WEBHOOK_URL}" \
72
- -d "SmsMethod=POST" > /dev/null
73
- echo "Sandbox webhook configured!"
74
- else
75
- echo "Note: Configure webhook manually in Twilio console:"
76
- echo " URL: $WEBHOOK_URL"
77
- echo " https://console.twilio.com/us1/develop/sms/try-it-out/whatsapp-learn"
78
- fi
79
-
80
- # Install Claude hook
81
- echo ""
82
- echo "Installing Claude response hook..."
83
- stackmemory notify install-response-hook 2>/dev/null || true
84
-
85
- # Save PIDs for cleanup
86
- echo "$WEBHOOK_PID" > /tmp/stackmemory-webhook.pid
87
- echo "$NGROK_PID" > /tmp/stackmemory-ngrok.pid
88
-
89
- echo ""
90
- echo "=== Setup Complete ==="
91
- echo ""
92
- echo "Webhook server: http://localhost:$WEBHOOK_PORT (PID: $WEBHOOK_PID)"
93
- echo "Ngrok tunnel: $NGROK_URL (PID: $NGROK_PID)"
94
- echo "Webhook URL: $WEBHOOK_URL"
95
- echo ""
96
- echo "The loop is now active:"
97
- echo " 1. Send notification: stackmemory notify review 'Task'"
98
- echo " 2. User replies via WhatsApp"
99
- echo " 3. Response queued for action"
100
- echo " 4. Claude hook processes it"
101
- echo ""
102
- echo "To stop: ./scripts/stop-notify-webhook.sh"
103
- echo "Logs: /tmp/webhook.log, /tmp/ngrok.log"
@@ -1,360 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Claude Code hook for WhatsApp/SMS notifications
4
- *
5
- * Triggers notifications when:
6
- * - AskUserQuestion tool is used (allows remote response)
7
- * - PR is created
8
- * - Task is marked complete
9
- * - User explicitly requests notification
10
- *
11
- * Install: stackmemory notify install-hook
12
- */
13
-
14
- const fs = require('fs');
15
- const path = require('path');
16
- const os = require('os');
17
- const https = require('https');
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
-
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
- );
48
-
49
- function loadConfig() {
50
- try {
51
- if (fs.existsSync(CONFIG_PATH)) {
52
- return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
53
- }
54
- } catch {}
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
- }
96
- }
97
-
98
- function shouldNotify(toolName, toolInput, output) {
99
- const config = loadConfig();
100
- if (!config.enabled) return null;
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
-
152
- // Check for PR creation
153
- if (toolName === 'Bash') {
154
- const cmd = toolInput?.command || '';
155
- const out = output || '';
156
-
157
- // gh pr create
158
- if (cmd.includes('gh pr create') && out.includes('github.com')) {
159
- const prUrl = out.match(/https:\/\/github\.com\/[^\s]+\/pull\/\d+/)?.[0];
160
- return {
161
- type: 'review_ready',
162
- title: 'PR Ready for Review',
163
- message: prUrl || 'Pull request created successfully',
164
- options: ['Approve', 'Review', 'Skip'],
165
- };
166
- }
167
-
168
- // npm publish
169
- if (cmd.includes('npm publish') && out.includes('+')) {
170
- const pkg = out.match(/\+ ([^\s]+)/)?.[1];
171
- return {
172
- type: 'task_complete',
173
- title: 'Package Published',
174
- message: pkg ? `Published ${pkg}` : 'Package published successfully',
175
- };
176
- }
177
-
178
- // Deployment
179
- if (
180
- (cmd.includes('deploy') || cmd.includes('railway up')) &&
181
- (out.includes('deployed') || out.includes('success'))
182
- ) {
183
- return {
184
- type: 'review_ready',
185
- title: 'Deployment Complete',
186
- message: 'Ready for verification',
187
- options: ['Verify', 'Rollback', 'Skip'],
188
- };
189
- }
190
- }
191
-
192
- return null;
193
- }
194
-
195
- function getChannelNumbers(config) {
196
- const channel = config.channel || 'whatsapp';
197
-
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
- };
207
- }
208
- }
209
-
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;
260
- }
261
-
262
- let message = `${notification.title}\n\n${notification.message}`;
263
-
264
- if (notification.options) {
265
- message += '\n\n';
266
- notification.options.forEach((opt, i) => {
267
- message += `${i + 1}. ${opt}\n`;
268
- });
269
- message += '\nReply with number to select';
270
- }
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
-
285
- const postData = new URLSearchParams({
286
- From: numbers.from,
287
- To: numbers.to,
288
- Body: message,
289
- }).toString();
290
-
291
- const options = {
292
- hostname: 'api.twilio.com',
293
- port: 443,
294
- path: `/2010-04-01/Accounts/${config.accountSid}/Messages.json`,
295
- method: 'POST',
296
- headers: {
297
- Authorization:
298
- 'Basic ' +
299
- Buffer.from(`${config.accountSid}:${config.authToken}`).toString(
300
- 'base64'
301
- ),
302
- 'Content-Type': 'application/x-www-form-urlencoded',
303
- 'Content-Length': Buffer.byteLength(postData),
304
- },
305
- };
306
-
307
- const req = https.request(options, (res) => {
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
- });
325
- });
326
-
327
- req.on('error', (e) => {
328
- console.error(`[notify-hook] Error: ${e.message}`);
329
- });
330
-
331
- req.write(postData);
332
- req.end();
333
- }
334
-
335
- // Read hook input from stdin (post-tool-use hook)
336
- let input = '';
337
- process.stdin.setEncoding('utf8');
338
- process.stdin.on('data', (chunk) => (input += chunk));
339
- process.stdin.on('end', () => {
340
- try {
341
- const hookData = JSON.parse(input);
342
- const { tool_name, tool_input, tool_output } = hookData;
343
-
344
- logDebug('PostToolUse', { tool: tool_name, session: hookData.session_id });
345
-
346
- const notification = shouldNotify(tool_name, tool_input, tool_output);
347
-
348
- if (notification) {
349
- notification.sessionId = hookData.session_id;
350
- sendNotification(notification);
351
- }
352
-
353
- // Always allow (post-tool hooks don't block)
354
- console.log(JSON.stringify({ status: 'ok' }));
355
- } catch (err) {
356
- logDebug('ERROR', err.message);
357
- console.error('[notify-hook] Error:', err.message);
358
- console.log(JSON.stringify({ status: 'ok' }));
359
- }
360
- });