Skip to content

Commit 53c8278

Browse files
feat: Add webhook setup scripts and status callback endpoint
- Add /sms/incoming and /sms/status endpoints for Twilio - Add setup-notify-webhook.sh for auto-configuration - Add stop-notify-webhook.sh for cleanup - Track delivery status (sent/delivered/read)
1 parent 3413169 commit 53c8278

3 files changed

Lines changed: 175 additions & 6 deletions

File tree

scripts/setup-notify-webhook.sh

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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"

scripts/stop-notify-webhook.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/bin/bash
2+
# Stop StackMemory webhook services
3+
4+
echo "Stopping webhook services..."
5+
6+
if [ -f /tmp/stackmemory-webhook.pid ]; then
7+
kill $(cat /tmp/stackmemory-webhook.pid) 2>/dev/null && echo "Webhook server stopped"
8+
rm /tmp/stackmemory-webhook.pid
9+
fi
10+
11+
if [ -f /tmp/stackmemory-ngrok.pid ]; then
12+
kill $(cat /tmp/stackmemory-ngrok.pid) 2>/dev/null && echo "Ngrok tunnel stopped"
13+
rm /tmp/stackmemory-ngrok.pid
14+
fi
15+
16+
pkill -f "notify webhook" 2>/dev/null || true
17+
pkill -f "ngrok http" 2>/dev/null || true
18+
19+
echo "Done"

src/hooks/sms-webhook.ts

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { createServer, IncomingMessage, ServerResponse } from 'http';
77
import { parse as parseUrl } from 'url';
8-
import { existsSync, writeFileSync, mkdirSync } from 'fs';
8+
import { existsSync, writeFileSync, mkdirSync, readFileSync } from 'fs';
99
import { join } from 'path';
1010
import { homedir } from 'os';
1111
import { processIncomingResponse, loadSMSConfig } from './sms-notify.js';
@@ -127,8 +127,13 @@ export function startWebhookServer(port: number = 3456): void {
127127
return;
128128
}
129129

130-
// SMS webhook endpoint
131-
if (url.pathname === '/sms' && req.method === 'POST') {
130+
// SMS webhook endpoint (incoming messages)
131+
if (
132+
(url.pathname === '/sms' ||
133+
url.pathname === '/sms/incoming' ||
134+
url.pathname === '/webhook') &&
135+
req.method === 'POST'
136+
) {
132137
let body = '';
133138
req.on('data', (chunk) => {
134139
body += chunk;
@@ -152,7 +157,44 @@ export function startWebhookServer(port: number = 3456): void {
152157
return;
153158
}
154159

155-
// Status endpoint
160+
// Status callback endpoint (delivery status updates)
161+
if (url.pathname === '/sms/status' && req.method === 'POST') {
162+
let body = '';
163+
req.on('data', (chunk) => {
164+
body += chunk;
165+
});
166+
167+
req.on('end', () => {
168+
try {
169+
const payload = parseFormData(body);
170+
console.log(
171+
`[sms-webhook] Status update: ${payload['MessageSid']} -> ${payload['MessageStatus']}`
172+
);
173+
174+
// Store status for tracking
175+
const statusPath = join(
176+
homedir(),
177+
'.stackmemory',
178+
'sms-status.json'
179+
);
180+
const statuses: Record<string, string> = existsSync(statusPath)
181+
? JSON.parse(readFileSync(statusPath, 'utf8'))
182+
: {};
183+
statuses[payload['MessageSid']] = payload['MessageStatus'];
184+
writeFileSync(statusPath, JSON.stringify(statuses, null, 2));
185+
186+
res.writeHead(200, { 'Content-Type': 'text/plain' });
187+
res.end('OK');
188+
} catch (err) {
189+
console.error('[sms-webhook] Status error:', err);
190+
res.writeHead(500);
191+
res.end('Error');
192+
}
193+
});
194+
return;
195+
}
196+
197+
// Server status endpoint
156198
if (url.pathname === '/status') {
157199
const config = loadSMSConfig();
158200
res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -172,8 +214,13 @@ export function startWebhookServer(port: number = 3456): void {
172214

173215
server.listen(port, () => {
174216
console.log(`[sms-webhook] Server listening on port ${port}`);
175-
console.log(`[sms-webhook] Webhook URL: http://localhost:${port}/sms`);
176-
console.log(`[sms-webhook] Configure this URL in Twilio console`);
217+
console.log(
218+
`[sms-webhook] Incoming messages: http://localhost:${port}/sms/incoming`
219+
);
220+
console.log(
221+
`[sms-webhook] Status callback: http://localhost:${port}/sms/status`
222+
);
223+
console.log(`[sms-webhook] Configure these URLs in Twilio console`);
177224
});
178225
}
179226

0 commit comments

Comments
 (0)