Skip to content

Commit e54c6b3

Browse files
feat(sms): add incoming request queue for WhatsApp triggers
1 parent 53033f4 commit e54c6b3

1 file changed

Lines changed: 93 additions & 1 deletion

File tree

src/hooks/sms-webhook.ts

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,77 @@ function storeLatestResponse(
140140
);
141141
}
142142

143+
/**
144+
* Store incoming request for Claude to pick up
145+
* Used when a WhatsApp/SMS message arrives without a pending prompt
146+
*/
147+
function storeIncomingRequest(from: string, message: string): void {
148+
ensureSecureDir(join(homedir(), '.stackmemory'));
149+
const requestPath = join(
150+
homedir(),
151+
'.stackmemory',
152+
'sms-incoming-request.json'
153+
);
154+
writeFileSecure(
155+
requestPath,
156+
JSON.stringify({
157+
from,
158+
message,
159+
timestamp: new Date().toISOString(),
160+
processed: false,
161+
})
162+
);
163+
}
164+
165+
/**
166+
* Get pending incoming request (if any)
167+
*/
168+
export function getIncomingRequest(): {
169+
from: string;
170+
message: string;
171+
timestamp: string;
172+
processed: boolean;
173+
} | null {
174+
const requestPath = join(
175+
homedir(),
176+
'.stackmemory',
177+
'sms-incoming-request.json'
178+
);
179+
if (!existsSync(requestPath)) {
180+
return null;
181+
}
182+
try {
183+
const data = JSON.parse(readFileSync(requestPath, 'utf-8'));
184+
if (data.processed) {
185+
return null;
186+
}
187+
return data;
188+
} catch {
189+
return null;
190+
}
191+
}
192+
193+
/**
194+
* Mark incoming request as processed
195+
*/
196+
export function markRequestProcessed(): void {
197+
const requestPath = join(
198+
homedir(),
199+
'.stackmemory',
200+
'sms-incoming-request.json'
201+
);
202+
if (!existsSync(requestPath)) {
203+
return;
204+
}
205+
try {
206+
const data = JSON.parse(readFileSync(requestPath, 'utf-8'));
207+
data.processed = true;
208+
writeFileSecure(requestPath, JSON.stringify(data));
209+
} catch {
210+
// Ignore errors
211+
}
212+
}
213+
143214
export async function handleSMSWebhook(payload: TwilioWebhookPayload): Promise<{
144215
response: string;
145216
action?: string;
@@ -168,7 +239,12 @@ export async function handleSMSWebhook(payload: TwilioWebhookPayload): Promise<{
168239
response: `Invalid response. Expected: ${result.prompt.options.map((o) => o.key).join(', ')}`,
169240
};
170241
}
171-
return { response: 'No pending prompt found.' };
242+
// No pending prompt - store as new incoming request for Claude
243+
storeIncomingRequest(From, Body);
244+
console.log(
245+
`[sms-webhook] Stored new request from ${From}: ${Body.substring(0, 50)}...`
246+
);
247+
return { response: 'Got it! Your request has been queued.' };
172248
}
173249

174250
// Store response for Claude hook
@@ -450,6 +526,22 @@ export function startWebhookServer(port: number = 3456): void {
450526
return;
451527
}
452528

529+
// Get pending incoming request endpoint
530+
if (url.pathname === '/request' && req.method === 'GET') {
531+
const request = getIncomingRequest();
532+
res.writeHead(200, { 'Content-Type': 'application/json' });
533+
res.end(JSON.stringify({ request }));
534+
return;
535+
}
536+
537+
// Mark request as processed endpoint
538+
if (url.pathname === '/request/ack' && req.method === 'POST') {
539+
markRequestProcessed();
540+
res.writeHead(200, { 'Content-Type': 'application/json' });
541+
res.end(JSON.stringify({ success: true }));
542+
return;
543+
}
544+
453545
// Send outgoing notification endpoint
454546
if (url.pathname === '/send' && req.method === 'POST') {
455547
let body = '';

0 commit comments

Comments
 (0)