@stackmemoryai/stackmemory 0.5.34 → 0.5.36

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.
@@ -178,7 +178,7 @@ ${payload.message}`;
178
178
  message += "Reply with your response";
179
179
  }
180
180
  }
181
- return message;
181
+ return appendSessionUrl(message);
182
182
  }
183
183
  function getChannelNumbers(config) {
184
184
  const channel = config.channel || "whatsapp";
@@ -335,6 +335,22 @@ function processIncomingResponse(from, body) {
335
335
  function getSessionId() {
336
336
  return process.env["CLAUDE_INSTANCE_ID"] || process.env["STACKMEMORY_SESSION_ID"] || Math.random().toString(36).substring(2, 8);
337
337
  }
338
+ function getSessionUrl() {
339
+ const sessionId = process.env["CLAUDE_SESSION_ID"];
340
+ if (sessionId?.startsWith("session_")) {
341
+ return `https://claude.ai/code/${sessionId}`;
342
+ }
343
+ return process.env["CLAUDE_SESSION_URL"];
344
+ }
345
+ function appendSessionUrl(message) {
346
+ const url = getSessionUrl();
347
+ if (url) {
348
+ return `${message}
349
+
350
+ Session: ${url}`;
351
+ }
352
+ return message;
353
+ }
338
354
  async function notifyReviewReady(title, description, options) {
339
355
  const sessionId = getSessionId();
340
356
  let finalOptions = options || [];
@@ -428,14 +444,60 @@ function cleanupExpiredPrompts() {
428
444
  }
429
445
  return removed;
430
446
  }
447
+ async function notify(message) {
448
+ const sessionId = getSessionId();
449
+ return sendNotification({
450
+ type: "custom",
451
+ title: `[Claude ${sessionId}]`,
452
+ message
453
+ });
454
+ }
455
+ async function notifyChoice(message, optionA, optionB) {
456
+ const sessionId = getSessionId();
457
+ return sendNotification({
458
+ type: "custom",
459
+ title: `[Claude ${sessionId}]`,
460
+ message,
461
+ prompt: {
462
+ type: "options",
463
+ options: [
464
+ { key: "1", label: optionA },
465
+ { key: "2", label: optionB }
466
+ ]
467
+ }
468
+ });
469
+ }
470
+ async function notifyYesNo(message) {
471
+ const sessionId = getSessionId();
472
+ return sendNotification({
473
+ type: "custom",
474
+ title: `[Claude ${sessionId}]`,
475
+ message,
476
+ prompt: { type: "yesno" }
477
+ });
478
+ }
479
+ async function notifyStep(step, status = "done") {
480
+ const sessionId = getSessionId();
481
+ const symbol = status === "done" ? "\u2713" : status === "failed" ? "\u2717" : "\u23F3";
482
+ return sendNotification({
483
+ type: "task_complete",
484
+ title: `[Claude ${sessionId}]`,
485
+ message: `${symbol} ${step}`
486
+ });
487
+ }
431
488
  export {
432
489
  cleanupExpiredPrompts,
433
490
  getMissingConfig,
491
+ getSessionUrl,
434
492
  loadSMSConfig,
493
+ notify,
494
+ notifyChoice,
435
495
  notifyError,
436
496
  notifyReviewReady,
497
+ notifyStep,
437
498
  notifyTaskComplete,
438
499
  notifyWithYesNo,
500
+ notifyYesNo,
439
501
  processIncomingResponse,
440
502
  saveSMSConfig,
441
503
  sendNotification,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/hooks/sms-notify.ts"],
4
- "sourcesContent": ["/**\n * SMS Notification Hook for StackMemory\n * Sends text messages when tasks are ready for review\n * Supports interactive prompts with numbered options or yes/no\n *\n * Optional feature - requires Twilio setup\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { config as loadDotenv } from 'dotenv';\nimport { writeFileSecure, ensureSecureDir } from './secure-fs.js';\nimport { SMSConfigSchema, parseConfigSafe } from './schemas.js';\n\nexport type MessageChannel = 'whatsapp' | 'sms';\n\nexport interface SMSConfig {\n enabled: boolean;\n // Preferred channel: whatsapp is cheaper for back-and-forth conversations\n channel: MessageChannel;\n // Twilio credentials (from env or config)\n accountSid?: string;\n authToken?: string;\n // SMS numbers\n smsFromNumber?: string;\n smsToNumber?: string;\n // WhatsApp numbers (Twilio prefixes with 'whatsapp:' automatically)\n whatsappFromNumber?: string;\n whatsappToNumber?: string;\n // Legacy fields (backwards compatibility)\n fromNumber?: string;\n toNumber?: string;\n // Webhook URL for receiving responses\n webhookUrl?: string;\n // Notification preferences\n notifyOn: {\n taskComplete: boolean;\n reviewReady: boolean;\n error: boolean;\n custom: boolean;\n contextSync: boolean;\n };\n // Quiet hours (don't send during these times)\n quietHours?: {\n enabled: boolean;\n start: string; // \"22:00\"\n end: string; // \"08:00\"\n };\n // Response timeout (seconds)\n responseTimeout: number;\n // Pending prompts awaiting response\n pendingPrompts: PendingPrompt[];\n}\n\nexport interface PendingPrompt {\n id: string;\n timestamp: string;\n message: string;\n options: PromptOption[];\n type: 'options' | 'yesno' | 'freeform';\n callback?: string; // Command to run with response\n expiresAt: string;\n}\n\nexport interface PromptOption {\n key: string; // \"1\", \"2\", \"y\", \"n\", etc.\n label: string;\n action?: string; // Command to execute\n}\n\nexport interface NotificationPayload {\n type: 'task_complete' | 'review_ready' | 'error' | 'custom' | 'context_sync';\n title: string;\n message: string;\n prompt?: {\n type: 'options' | 'yesno' | 'freeform';\n options?: PromptOption[];\n question?: string;\n };\n metadata?: Record<string, unknown>;\n}\n\nconst CONFIG_PATH = join(homedir(), '.stackmemory', 'sms-notify.json');\n\nconst DEFAULT_CONFIG: SMSConfig = {\n enabled: false,\n channel: 'whatsapp', // WhatsApp is cheaper for conversations\n notifyOn: {\n taskComplete: true,\n reviewReady: true,\n error: true,\n custom: true,\n contextSync: true,\n },\n quietHours: {\n enabled: false,\n start: '22:00',\n end: '08:00',\n },\n responseTimeout: 300, // 5 minutes\n pendingPrompts: [],\n};\n\nexport function loadSMSConfig(): SMSConfig {\n // Load .env files (project, home, global)\n loadDotenv({ path: join(process.cwd(), '.env') });\n loadDotenv({ path: join(process.cwd(), '.env.local') });\n loadDotenv({ path: join(homedir(), '.env') });\n loadDotenv({ path: join(homedir(), '.stackmemory', '.env') });\n\n try {\n if (existsSync(CONFIG_PATH)) {\n const data = readFileSync(CONFIG_PATH, 'utf8');\n const parsed = JSON.parse(data);\n // Validate with zod schema, fall back to defaults on invalid config\n const validated = parseConfigSafe(\n SMSConfigSchema,\n { ...DEFAULT_CONFIG, ...parsed },\n DEFAULT_CONFIG,\n 'sms-notify'\n );\n applyEnvVars(validated);\n return validated;\n }\n } catch {\n // Use defaults\n }\n\n // Check environment variables\n const config = { ...DEFAULT_CONFIG };\n applyEnvVars(config);\n return config;\n}\n\n// Check what's missing for notifications to work\nexport function getMissingConfig(): {\n missing: string[];\n configured: string[];\n ready: boolean;\n} {\n const config = loadSMSConfig();\n const missing: string[] = [];\n const configured: string[] = [];\n\n // Check credentials\n if (config.accountSid) {\n configured.push('TWILIO_ACCOUNT_SID');\n } else {\n missing.push('TWILIO_ACCOUNT_SID');\n }\n\n if (config.authToken) {\n configured.push('TWILIO_AUTH_TOKEN');\n } else {\n missing.push('TWILIO_AUTH_TOKEN');\n }\n\n // Check channel-specific numbers\n const channel = config.channel || 'whatsapp';\n\n if (channel === 'whatsapp') {\n const from = config.whatsappFromNumber || config.fromNumber;\n const to = config.whatsappToNumber || config.toNumber;\n\n if (from) {\n configured.push('TWILIO_WHATSAPP_FROM');\n } else {\n missing.push('TWILIO_WHATSAPP_FROM');\n }\n\n if (to) {\n configured.push('TWILIO_WHATSAPP_TO');\n } else {\n missing.push('TWILIO_WHATSAPP_TO');\n }\n } else {\n const from = config.smsFromNumber || config.fromNumber;\n const to = config.smsToNumber || config.toNumber;\n\n if (from) {\n configured.push('TWILIO_SMS_FROM');\n } else {\n missing.push('TWILIO_SMS_FROM');\n }\n\n if (to) {\n configured.push('TWILIO_SMS_TO');\n } else {\n missing.push('TWILIO_SMS_TO');\n }\n }\n\n return {\n missing,\n configured,\n ready: missing.length === 0,\n };\n}\n\nfunction applyEnvVars(config: SMSConfig): void {\n // Twilio credentials\n if (process.env['TWILIO_ACCOUNT_SID']) {\n config.accountSid = process.env['TWILIO_ACCOUNT_SID'];\n }\n if (process.env['TWILIO_AUTH_TOKEN']) {\n config.authToken = process.env['TWILIO_AUTH_TOKEN'];\n }\n\n // SMS numbers\n if (process.env['TWILIO_SMS_FROM'] || process.env['TWILIO_FROM_NUMBER']) {\n config.smsFromNumber =\n process.env['TWILIO_SMS_FROM'] || process.env['TWILIO_FROM_NUMBER'];\n }\n if (process.env['TWILIO_SMS_TO'] || process.env['TWILIO_TO_NUMBER']) {\n config.smsToNumber =\n process.env['TWILIO_SMS_TO'] || process.env['TWILIO_TO_NUMBER'];\n }\n\n // WhatsApp numbers\n if (process.env['TWILIO_WHATSAPP_FROM']) {\n config.whatsappFromNumber = process.env['TWILIO_WHATSAPP_FROM'];\n }\n if (process.env['TWILIO_WHATSAPP_TO']) {\n config.whatsappToNumber = process.env['TWILIO_WHATSAPP_TO'];\n }\n\n // Legacy support\n if (process.env['TWILIO_FROM_NUMBER']) {\n config.fromNumber = process.env['TWILIO_FROM_NUMBER'];\n }\n if (process.env['TWILIO_TO_NUMBER']) {\n config.toNumber = process.env['TWILIO_TO_NUMBER'];\n }\n\n // Channel preference\n if (process.env['TWILIO_CHANNEL']) {\n config.channel = process.env['TWILIO_CHANNEL'] as MessageChannel;\n }\n}\n\nexport function saveSMSConfig(config: SMSConfig): void {\n try {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n // Don't save sensitive credentials to file\n const safeConfig = { ...config };\n delete safeConfig.accountSid;\n delete safeConfig.authToken;\n writeFileSecure(CONFIG_PATH, JSON.stringify(safeConfig, null, 2));\n } catch {\n // Silently fail\n }\n}\n\nfunction isQuietHours(config: SMSConfig): boolean {\n if (!config.quietHours?.enabled) return false;\n\n const now = new Date();\n const currentTime = now.getHours() * 60 + now.getMinutes();\n\n const [startH, startM] = config.quietHours.start.split(':').map(Number);\n const [endH, endM] = config.quietHours.end.split(':').map(Number);\n\n const startTime = startH * 60 + startM;\n const endTime = endH * 60 + endM;\n\n // Handle overnight quiet hours (e.g., 22:00 - 08:00)\n if (startTime > endTime) {\n return currentTime >= startTime || currentTime < endTime;\n }\n\n return currentTime >= startTime && currentTime < endTime;\n}\n\nfunction generatePromptId(): string {\n return Math.random().toString(36).substring(2, 10);\n}\n\nfunction formatPromptMessage(payload: NotificationPayload): string {\n let message = `${payload.title}\\n\\n${payload.message}`;\n\n if (payload.prompt) {\n message += '\\n\\n';\n\n if (payload.prompt.question) {\n message += `${payload.prompt.question}\\n`;\n }\n\n if (payload.prompt.type === 'yesno') {\n message += 'Reply Y for Yes, N for No';\n } else if (payload.prompt.type === 'options' && payload.prompt.options) {\n payload.prompt.options.forEach((opt) => {\n message += `${opt.key}. ${opt.label}\\n`;\n });\n message += '\\nReply with number to select';\n } else if (payload.prompt.type === 'freeform') {\n message += 'Reply with your response';\n }\n }\n\n return message;\n}\n\nfunction getChannelNumbers(config: SMSConfig): {\n from: string;\n to: string;\n channel: MessageChannel;\n} | null {\n const channel = config.channel || 'whatsapp';\n\n if (channel === 'whatsapp') {\n // Try WhatsApp first\n const from = config.whatsappFromNumber || config.fromNumber;\n const to = config.whatsappToNumber || config.toNumber;\n if (from && to) {\n // Twilio requires 'whatsapp:' prefix for WhatsApp numbers\n return {\n from: from.startsWith('whatsapp:') ? from : `whatsapp:${from}`,\n to: to.startsWith('whatsapp:') ? to : `whatsapp:${to}`,\n channel: 'whatsapp',\n };\n }\n }\n\n // Fall back to SMS\n const from = config.smsFromNumber || config.fromNumber;\n const to = config.smsToNumber || config.toNumber;\n if (from && to) {\n return { from, to, channel: 'sms' };\n }\n\n return null;\n}\n\nexport async function sendNotification(\n payload: NotificationPayload,\n channelOverride?: MessageChannel\n): Promise<{\n success: boolean;\n promptId?: string;\n channel?: MessageChannel;\n error?: string;\n}> {\n const config = loadSMSConfig();\n\n if (!config.enabled) {\n return { success: false, error: 'Notifications disabled' };\n }\n\n // Check notification type is enabled\n const typeMap: Record<string, keyof typeof config.notifyOn> = {\n task_complete: 'taskComplete',\n review_ready: 'reviewReady',\n error: 'error',\n custom: 'custom',\n context_sync: 'contextSync',\n };\n\n if (!config.notifyOn[typeMap[payload.type]]) {\n return {\n success: false,\n error: `Notifications for ${payload.type} disabled`,\n };\n }\n\n // Check quiet hours\n if (isQuietHours(config)) {\n return { success: false, error: 'Quiet hours active' };\n }\n\n // Validate credentials\n if (!config.accountSid || !config.authToken) {\n return {\n success: false,\n error:\n 'Missing Twilio credentials. Set TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN',\n };\n }\n\n // Get channel numbers (prefer WhatsApp)\n const originalChannel = config.channel;\n if (channelOverride) {\n config.channel = channelOverride;\n }\n\n const numbers = getChannelNumbers(config);\n config.channel = originalChannel; // Restore\n\n if (!numbers) {\n return {\n success: false,\n error:\n config.channel === 'whatsapp'\n ? 'Missing WhatsApp numbers. Set TWILIO_WHATSAPP_FROM and TWILIO_WHATSAPP_TO'\n : 'Missing SMS numbers. Set TWILIO_SMS_FROM and TWILIO_SMS_TO',\n };\n }\n\n const message = formatPromptMessage(payload);\n let promptId: string | undefined;\n\n // Store pending prompt if interactive\n if (payload.prompt) {\n promptId = generatePromptId();\n const expiresAt = new Date(\n Date.now() + config.responseTimeout * 1000\n ).toISOString();\n\n const pendingPrompt: PendingPrompt = {\n id: promptId,\n timestamp: new Date().toISOString(),\n message: payload.message,\n options: payload.prompt.options || [],\n type: payload.prompt.type,\n expiresAt,\n };\n\n config.pendingPrompts.push(pendingPrompt);\n saveSMSConfig(config);\n }\n\n try {\n // Use Twilio API (same endpoint for SMS and WhatsApp)\n const twilioUrl = `https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`;\n\n const response = await fetch(twilioUrl, {\n method: 'POST',\n headers: {\n Authorization:\n 'Basic ' +\n Buffer.from(`${config.accountSid}:${config.authToken}`).toString(\n 'base64'\n ),\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n From: numbers.from,\n To: numbers.to,\n Body: message,\n }),\n });\n\n if (!response.ok) {\n const errorData = await response.text();\n return {\n success: false,\n channel: numbers.channel,\n error: `Twilio error: ${errorData}`,\n };\n }\n\n return { success: true, promptId, channel: numbers.channel };\n } catch (err) {\n return {\n success: false,\n channel: numbers.channel,\n error: `Failed to send ${numbers.channel}: ${err instanceof Error ? err.message : String(err)}`,\n };\n }\n}\n\n// Backwards compatible alias\nexport async function sendSMSNotification(\n payload: NotificationPayload\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n return sendNotification(payload);\n}\n\nexport function processIncomingResponse(\n from: string,\n body: string\n): {\n matched: boolean;\n prompt?: PendingPrompt;\n response?: string;\n action?: string;\n} {\n const config = loadSMSConfig();\n\n // Normalize response\n const response = body.trim().toLowerCase();\n\n // Find matching pending prompt (most recent first)\n const now = new Date();\n const validPrompts = config.pendingPrompts.filter(\n (p) => new Date(p.expiresAt) > now\n );\n\n if (validPrompts.length === 0) {\n return { matched: false };\n }\n\n // Get most recent prompt\n const prompt = validPrompts[validPrompts.length - 1];\n\n let matchedOption: PromptOption | undefined;\n\n if (prompt.type === 'yesno') {\n if (response === 'y' || response === 'yes') {\n matchedOption = { key: 'y', label: 'Yes' };\n } else if (response === 'n' || response === 'no') {\n matchedOption = { key: 'n', label: 'No' };\n }\n } else if (prompt.type === 'options') {\n matchedOption = prompt.options.find(\n (opt) => opt.key.toLowerCase() === response\n );\n } else if (prompt.type === 'freeform') {\n matchedOption = { key: response, label: response };\n }\n\n // Remove processed prompt\n config.pendingPrompts = config.pendingPrompts.filter(\n (p) => p.id !== prompt.id\n );\n saveSMSConfig(config);\n\n if (matchedOption) {\n return {\n matched: true,\n prompt,\n response: matchedOption.key,\n action: matchedOption.action,\n };\n }\n\n return { matched: false, prompt };\n}\n\n// Get session ID from environment or generate short ID\nfunction getSessionId(): string {\n return (\n process.env['CLAUDE_INSTANCE_ID'] ||\n process.env['STACKMEMORY_SESSION_ID'] ||\n Math.random().toString(36).substring(2, 8)\n );\n}\n\n// Convenience functions for common notifications\n\nexport async function notifyReviewReady(\n title: string,\n description: string,\n options?: { label: string; action?: string }[]\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const sessionId = getSessionId();\n\n // Ensure minimum 2 options\n let finalOptions = options || [];\n if (finalOptions.length < 2) {\n const defaults = [\n { label: 'Approve', action: 'echo \"Approved\"' },\n { label: 'Request changes', action: 'echo \"Changes requested\"' },\n ];\n finalOptions = [...finalOptions, ...defaults].slice(\n 0,\n Math.max(2, finalOptions.length)\n );\n }\n\n const payload: NotificationPayload = {\n type: 'review_ready',\n title: `[Claude ${sessionId}] Review Ready: ${title}`,\n message: description,\n prompt: {\n type: 'options',\n options: finalOptions.map((opt, i) => ({\n key: String(i + 1),\n label: opt.label,\n action: opt.action,\n })),\n question: 'What would you like to do?',\n },\n };\n\n return sendSMSNotification(payload);\n}\n\nexport async function notifyWithYesNo(\n title: string,\n question: string,\n yesAction?: string,\n noAction?: string\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const sessionId = getSessionId();\n return sendSMSNotification({\n type: 'custom',\n title: `[Claude ${sessionId}] ${title}`,\n message: question,\n prompt: {\n type: 'yesno',\n options: [\n { key: 'y', label: 'Yes', action: yesAction },\n { key: 'n', label: 'No', action: noAction },\n ],\n },\n });\n}\n\nexport async function notifyTaskComplete(\n taskName: string,\n summary: string\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const sessionId = getSessionId();\n return sendSMSNotification({\n type: 'task_complete',\n title: `[Claude ${sessionId}] Task Complete: ${taskName}`,\n message: summary,\n prompt: {\n type: 'options',\n options: [\n { key: '1', label: 'Start next task', action: 'claude-sm' },\n { key: '2', label: 'View details', action: 'stackmemory status' },\n ],\n },\n });\n}\n\nexport async function notifyError(\n error: string,\n context?: string\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const sessionId = getSessionId();\n return sendSMSNotification({\n type: 'error',\n title: `[Claude ${sessionId}] Error Alert`,\n message: context ? `${error}\\n\\nContext: ${context}` : error,\n prompt: {\n type: 'options',\n options: [\n { key: '1', label: 'Retry', action: 'claude-sm' },\n {\n key: '2',\n label: 'View logs',\n action: 'tail -50 ~/.claude/logs/*.log',\n },\n ],\n },\n });\n}\n\n// Clean up expired prompts\nexport function cleanupExpiredPrompts(): number {\n const config = loadSMSConfig();\n const now = new Date();\n const before = config.pendingPrompts.length;\n\n config.pendingPrompts = config.pendingPrompts.filter(\n (p) => new Date(p.expiresAt) > now\n );\n\n const removed = before - config.pendingPrompts.length;\n if (removed > 0) {\n saveSMSConfig(config);\n }\n\n return removed;\n}\n"],
5
- "mappings": ";;;;AAQA,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,UAAU,kBAAkB;AACrC,SAAS,iBAAiB,uBAAuB;AACjD,SAAS,iBAAiB,uBAAuB;AAsEjD,MAAM,cAAc,KAAK,QAAQ,GAAG,gBAAgB,iBAAiB;AAErE,MAAM,iBAA4B;AAAA,EAChC,SAAS;AAAA,EACT,SAAS;AAAA;AAAA,EACT,UAAU;AAAA,IACR,cAAc;AAAA,IACd,aAAa;AAAA,IACb,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,aAAa;AAAA,EACf;AAAA,EACA,YAAY;AAAA,IACV,SAAS;AAAA,IACT,OAAO;AAAA,IACP,KAAK;AAAA,EACP;AAAA,EACA,iBAAiB;AAAA;AAAA,EACjB,gBAAgB,CAAC;AACnB;AAEO,SAAS,gBAA2B;AAEzC,aAAW,EAAE,MAAM,KAAK,QAAQ,IAAI,GAAG,MAAM,EAAE,CAAC;AAChD,aAAW,EAAE,MAAM,KAAK,QAAQ,IAAI,GAAG,YAAY,EAAE,CAAC;AACtD,aAAW,EAAE,MAAM,KAAK,QAAQ,GAAG,MAAM,EAAE,CAAC;AAC5C,aAAW,EAAE,MAAM,KAAK,QAAQ,GAAG,gBAAgB,MAAM,EAAE,CAAC;AAE5D,MAAI;AACF,QAAI,WAAW,WAAW,GAAG;AAC3B,YAAM,OAAO,aAAa,aAAa,MAAM;AAC7C,YAAM,SAAS,KAAK,MAAM,IAAI;AAE9B,YAAM,YAAY;AAAA,QAChB;AAAA,QACA,EAAE,GAAG,gBAAgB,GAAG,OAAO;AAAA,QAC/B;AAAA,QACA;AAAA,MACF;AACA,mBAAa,SAAS;AACtB,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AAGA,QAAM,SAAS,EAAE,GAAG,eAAe;AACnC,eAAa,MAAM;AACnB,SAAO;AACT;AAGO,SAAS,mBAId;AACA,QAAM,SAAS,cAAc;AAC7B,QAAM,UAAoB,CAAC;AAC3B,QAAM,aAAuB,CAAC;AAG9B,MAAI,OAAO,YAAY;AACrB,eAAW,KAAK,oBAAoB;AAAA,EACtC,OAAO;AACL,YAAQ,KAAK,oBAAoB;AAAA,EACnC;AAEA,MAAI,OAAO,WAAW;AACpB,eAAW,KAAK,mBAAmB;AAAA,EACrC,OAAO;AACL,YAAQ,KAAK,mBAAmB;AAAA,EAClC;AAGA,QAAM,UAAU,OAAO,WAAW;AAElC,MAAI,YAAY,YAAY;AAC1B,UAAM,OAAO,OAAO,sBAAsB,OAAO;AACjD,UAAM,KAAK,OAAO,oBAAoB,OAAO;AAE7C,QAAI,MAAM;AACR,iBAAW,KAAK,sBAAsB;AAAA,IACxC,OAAO;AACL,cAAQ,KAAK,sBAAsB;AAAA,IACrC;AAEA,QAAI,IAAI;AACN,iBAAW,KAAK,oBAAoB;AAAA,IACtC,OAAO;AACL,cAAQ,KAAK,oBAAoB;AAAA,IACnC;AAAA,EACF,OAAO;AACL,UAAM,OAAO,OAAO,iBAAiB,OAAO;AAC5C,UAAM,KAAK,OAAO,eAAe,OAAO;AAExC,QAAI,MAAM;AACR,iBAAW,KAAK,iBAAiB;AAAA,IACnC,OAAO;AACL,cAAQ,KAAK,iBAAiB;AAAA,IAChC;AAEA,QAAI,IAAI;AACN,iBAAW,KAAK,eAAe;AAAA,IACjC,OAAO;AACL,cAAQ,KAAK,eAAe;AAAA,IAC9B;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,OAAO,QAAQ,WAAW;AAAA,EAC5B;AACF;AAEA,SAAS,aAAa,QAAyB;AAE7C,MAAI,QAAQ,IAAI,oBAAoB,GAAG;AACrC,WAAO,aAAa,QAAQ,IAAI,oBAAoB;AAAA,EACtD;AACA,MAAI,QAAQ,IAAI,mBAAmB,GAAG;AACpC,WAAO,YAAY,QAAQ,IAAI,mBAAmB;AAAA,EACpD;AAGA,MAAI,QAAQ,IAAI,iBAAiB,KAAK,QAAQ,IAAI,oBAAoB,GAAG;AACvE,WAAO,gBACL,QAAQ,IAAI,iBAAiB,KAAK,QAAQ,IAAI,oBAAoB;AAAA,EACtE;AACA,MAAI,QAAQ,IAAI,eAAe,KAAK,QAAQ,IAAI,kBAAkB,GAAG;AACnE,WAAO,cACL,QAAQ,IAAI,eAAe,KAAK,QAAQ,IAAI,kBAAkB;AAAA,EAClE;AAGA,MAAI,QAAQ,IAAI,sBAAsB,GAAG;AACvC,WAAO,qBAAqB,QAAQ,IAAI,sBAAsB;AAAA,EAChE;AACA,MAAI,QAAQ,IAAI,oBAAoB,GAAG;AACrC,WAAO,mBAAmB,QAAQ,IAAI,oBAAoB;AAAA,EAC5D;AAGA,MAAI,QAAQ,IAAI,oBAAoB,GAAG;AACrC,WAAO,aAAa,QAAQ,IAAI,oBAAoB;AAAA,EACtD;AACA,MAAI,QAAQ,IAAI,kBAAkB,GAAG;AACnC,WAAO,WAAW,QAAQ,IAAI,kBAAkB;AAAA,EAClD;AAGA,MAAI,QAAQ,IAAI,gBAAgB,GAAG;AACjC,WAAO,UAAU,QAAQ,IAAI,gBAAgB;AAAA,EAC/C;AACF;AAEO,SAAS,cAAc,QAAyB;AACrD,MAAI;AACF,oBAAgB,KAAK,QAAQ,GAAG,cAAc,CAAC;AAE/C,UAAM,aAAa,EAAE,GAAG,OAAO;AAC/B,WAAO,WAAW;AAClB,WAAO,WAAW;AAClB,oBAAgB,aAAa,KAAK,UAAU,YAAY,MAAM,CAAC,CAAC;AAAA,EAClE,QAAQ;AAAA,EAER;AACF;AAEA,SAAS,aAAa,QAA4B;AAChD,MAAI,CAAC,OAAO,YAAY,QAAS,QAAO;AAExC,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,cAAc,IAAI,SAAS,IAAI,KAAK,IAAI,WAAW;AAEzD,QAAM,CAAC,QAAQ,MAAM,IAAI,OAAO,WAAW,MAAM,MAAM,GAAG,EAAE,IAAI,MAAM;AACtE,QAAM,CAAC,MAAM,IAAI,IAAI,OAAO,WAAW,IAAI,MAAM,GAAG,EAAE,IAAI,MAAM;AAEhE,QAAM,YAAY,SAAS,KAAK;AAChC,QAAM,UAAU,OAAO,KAAK;AAG5B,MAAI,YAAY,SAAS;AACvB,WAAO,eAAe,aAAa,cAAc;AAAA,EACnD;AAEA,SAAO,eAAe,aAAa,cAAc;AACnD;AAEA,SAAS,mBAA2B;AAClC,SAAO,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,UAAU,GAAG,EAAE;AACnD;AAEA,SAAS,oBAAoB,SAAsC;AACjE,MAAI,UAAU,GAAG,QAAQ,KAAK;AAAA;AAAA,EAAO,QAAQ,OAAO;AAEpD,MAAI,QAAQ,QAAQ;AAClB,eAAW;AAEX,QAAI,QAAQ,OAAO,UAAU;AAC3B,iBAAW,GAAG,QAAQ,OAAO,QAAQ;AAAA;AAAA,IACvC;AAEA,QAAI,QAAQ,OAAO,SAAS,SAAS;AACnC,iBAAW;AAAA,IACb,WAAW,QAAQ,OAAO,SAAS,aAAa,QAAQ,OAAO,SAAS;AACtE,cAAQ,OAAO,QAAQ,QAAQ,CAAC,QAAQ;AACtC,mBAAW,GAAG,IAAI,GAAG,KAAK,IAAI,KAAK;AAAA;AAAA,MACrC,CAAC;AACD,iBAAW;AAAA,IACb,WAAW,QAAQ,OAAO,SAAS,YAAY;AAC7C,iBAAW;AAAA,IACb;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,kBAAkB,QAIlB;AACP,QAAM,UAAU,OAAO,WAAW;AAElC,MAAI,YAAY,YAAY;AAE1B,UAAMA,QAAO,OAAO,sBAAsB,OAAO;AACjD,UAAMC,MAAK,OAAO,oBAAoB,OAAO;AAC7C,QAAID,SAAQC,KAAI;AAEd,aAAO;AAAA,QACL,MAAMD,MAAK,WAAW,WAAW,IAAIA,QAAO,YAAYA,KAAI;AAAA,QAC5D,IAAIC,IAAG,WAAW,WAAW,IAAIA,MAAK,YAAYA,GAAE;AAAA,QACpD,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AAGA,QAAM,OAAO,OAAO,iBAAiB,OAAO;AAC5C,QAAM,KAAK,OAAO,eAAe,OAAO;AACxC,MAAI,QAAQ,IAAI;AACd,WAAO,EAAE,MAAM,IAAI,SAAS,MAAM;AAAA,EACpC;AAEA,SAAO;AACT;AAEA,eAAsB,iBACpB,SACA,iBAMC;AACD,QAAM,SAAS,cAAc;AAE7B,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,EAAE,SAAS,OAAO,OAAO,yBAAyB;AAAA,EAC3D;AAGA,QAAM,UAAwD;AAAA,IAC5D,eAAe;AAAA,IACf,cAAc;AAAA,IACd,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAEA,MAAI,CAAC,OAAO,SAAS,QAAQ,QAAQ,IAAI,CAAC,GAAG;AAC3C,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,qBAAqB,QAAQ,IAAI;AAAA,IAC1C;AAAA,EACF;AAGA,MAAI,aAAa,MAAM,GAAG;AACxB,WAAO,EAAE,SAAS,OAAO,OAAO,qBAAqB;AAAA,EACvD;AAGA,MAAI,CAAC,OAAO,cAAc,CAAC,OAAO,WAAW;AAC3C,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OACE;AAAA,IACJ;AAAA,EACF;AAGA,QAAM,kBAAkB,OAAO;AAC/B,MAAI,iBAAiB;AACnB,WAAO,UAAU;AAAA,EACnB;AAEA,QAAM,UAAU,kBAAkB,MAAM;AACxC,SAAO,UAAU;AAEjB,MAAI,CAAC,SAAS;AACZ,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OACE,OAAO,YAAY,aACf,8EACA;AAAA,IACR;AAAA,EACF;AAEA,QAAM,UAAU,oBAAoB,OAAO;AAC3C,MAAI;AAGJ,MAAI,QAAQ,QAAQ;AAClB,eAAW,iBAAiB;AAC5B,UAAM,YAAY,IAAI;AAAA,MACpB,KAAK,IAAI,IAAI,OAAO,kBAAkB;AAAA,IACxC,EAAE,YAAY;AAEd,UAAM,gBAA+B;AAAA,MACnC,IAAI;AAAA,MACJ,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,SAAS,QAAQ;AAAA,MACjB,SAAS,QAAQ,OAAO,WAAW,CAAC;AAAA,MACpC,MAAM,QAAQ,OAAO;AAAA,MACrB;AAAA,IACF;AAEA,WAAO,eAAe,KAAK,aAAa;AACxC,kBAAc,MAAM;AAAA,EACtB;AAEA,MAAI;AAEF,UAAM,YAAY,8CAA8C,OAAO,UAAU;AAEjF,UAAM,WAAW,MAAM,MAAM,WAAW;AAAA,MACtC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eACE,WACA,OAAO,KAAK,GAAG,OAAO,UAAU,IAAI,OAAO,SAAS,EAAE,EAAE;AAAA,UACtD;AAAA,QACF;AAAA,QACF,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,IAAI,gBAAgB;AAAA,QACxB,MAAM,QAAQ;AAAA,QACd,IAAI,QAAQ;AAAA,QACZ,MAAM;AAAA,MACR,CAAC;AAAA,IACH,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,YAAY,MAAM,SAAS,KAAK;AACtC,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,QAAQ;AAAA,QACjB,OAAO,iBAAiB,SAAS;AAAA,MACnC;AAAA,IACF;AAEA,WAAO,EAAE,SAAS,MAAM,UAAU,SAAS,QAAQ,QAAQ;AAAA,EAC7D,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS,QAAQ;AAAA,MACjB,OAAO,kBAAkB,QAAQ,OAAO,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,IAC/F;AAAA,EACF;AACF;AAGA,eAAsB,oBACpB,SACkE;AAClE,SAAO,iBAAiB,OAAO;AACjC;AAEO,SAAS,wBACd,MACA,MAMA;AACA,QAAM,SAAS,cAAc;AAG7B,QAAM,WAAW,KAAK,KAAK,EAAE,YAAY;AAGzC,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,eAAe,OAAO,eAAe;AAAA,IACzC,CAAC,MAAM,IAAI,KAAK,EAAE,SAAS,IAAI;AAAA,EACjC;AAEA,MAAI,aAAa,WAAW,GAAG;AAC7B,WAAO,EAAE,SAAS,MAAM;AAAA,EAC1B;AAGA,QAAM,SAAS,aAAa,aAAa,SAAS,CAAC;AAEnD,MAAI;AAEJ,MAAI,OAAO,SAAS,SAAS;AAC3B,QAAI,aAAa,OAAO,aAAa,OAAO;AAC1C,sBAAgB,EAAE,KAAK,KAAK,OAAO,MAAM;AAAA,IAC3C,WAAW,aAAa,OAAO,aAAa,MAAM;AAChD,sBAAgB,EAAE,KAAK,KAAK,OAAO,KAAK;AAAA,IAC1C;AAAA,EACF,WAAW,OAAO,SAAS,WAAW;AACpC,oBAAgB,OAAO,QAAQ;AAAA,MAC7B,CAAC,QAAQ,IAAI,IAAI,YAAY,MAAM;AAAA,IACrC;AAAA,EACF,WAAW,OAAO,SAAS,YAAY;AACrC,oBAAgB,EAAE,KAAK,UAAU,OAAO,SAAS;AAAA,EACnD;AAGA,SAAO,iBAAiB,OAAO,eAAe;AAAA,IAC5C,CAAC,MAAM,EAAE,OAAO,OAAO;AAAA,EACzB;AACA,gBAAc,MAAM;AAEpB,MAAI,eAAe;AACjB,WAAO;AAAA,MACL,SAAS;AAAA,MACT;AAAA,MACA,UAAU,cAAc;AAAA,MACxB,QAAQ,cAAc;AAAA,IACxB;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,OAAO,OAAO;AAClC;AAGA,SAAS,eAAuB;AAC9B,SACE,QAAQ,IAAI,oBAAoB,KAChC,QAAQ,IAAI,wBAAwB,KACpC,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,UAAU,GAAG,CAAC;AAE7C;AAIA,eAAsB,kBACpB,OACA,aACA,SACkE;AAClE,QAAM,YAAY,aAAa;AAG/B,MAAI,eAAe,WAAW,CAAC;AAC/B,MAAI,aAAa,SAAS,GAAG;AAC3B,UAAM,WAAW;AAAA,MACf,EAAE,OAAO,WAAW,QAAQ,kBAAkB;AAAA,MAC9C,EAAE,OAAO,mBAAmB,QAAQ,2BAA2B;AAAA,IACjE;AACA,mBAAe,CAAC,GAAG,cAAc,GAAG,QAAQ,EAAE;AAAA,MAC5C;AAAA,MACA,KAAK,IAAI,GAAG,aAAa,MAAM;AAAA,IACjC;AAAA,EACF;AAEA,QAAM,UAA+B;AAAA,IACnC,MAAM;AAAA,IACN,OAAO,WAAW,SAAS,mBAAmB,KAAK;AAAA,IACnD,SAAS;AAAA,IACT,QAAQ;AAAA,MACN,MAAM;AAAA,MACN,SAAS,aAAa,IAAI,CAAC,KAAK,OAAO;AAAA,QACrC,KAAK,OAAO,IAAI,CAAC;AAAA,QACjB,OAAO,IAAI;AAAA,QACX,QAAQ,IAAI;AAAA,MACd,EAAE;AAAA,MACF,UAAU;AAAA,IACZ;AAAA,EACF;AAEA,SAAO,oBAAoB,OAAO;AACpC;AAEA,eAAsB,gBACpB,OACA,UACA,WACA,UACkE;AAClE,QAAM,YAAY,aAAa;AAC/B,SAAO,oBAAoB;AAAA,IACzB,MAAM;AAAA,IACN,OAAO,WAAW,SAAS,KAAK,KAAK;AAAA,IACrC,SAAS;AAAA,IACT,QAAQ;AAAA,MACN,MAAM;AAAA,MACN,SAAS;AAAA,QACP,EAAE,KAAK,KAAK,OAAO,OAAO,QAAQ,UAAU;AAAA,QAC5C,EAAE,KAAK,KAAK,OAAO,MAAM,QAAQ,SAAS;AAAA,MAC5C;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAEA,eAAsB,mBACpB,UACA,SACkE;AAClE,QAAM,YAAY,aAAa;AAC/B,SAAO,oBAAoB;AAAA,IACzB,MAAM;AAAA,IACN,OAAO,WAAW,SAAS,oBAAoB,QAAQ;AAAA,IACvD,SAAS;AAAA,IACT,QAAQ;AAAA,MACN,MAAM;AAAA,MACN,SAAS;AAAA,QACP,EAAE,KAAK,KAAK,OAAO,mBAAmB,QAAQ,YAAY;AAAA,QAC1D,EAAE,KAAK,KAAK,OAAO,gBAAgB,QAAQ,qBAAqB;AAAA,MAClE;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAEA,eAAsB,YACpB,OACA,SACkE;AAClE,QAAM,YAAY,aAAa;AAC/B,SAAO,oBAAoB;AAAA,IACzB,MAAM;AAAA,IACN,OAAO,WAAW,SAAS;AAAA,IAC3B,SAAS,UAAU,GAAG,KAAK;AAAA;AAAA,WAAgB,OAAO,KAAK;AAAA,IACvD,QAAQ;AAAA,MACN,MAAM;AAAA,MACN,SAAS;AAAA,QACP,EAAE,KAAK,KAAK,OAAO,SAAS,QAAQ,YAAY;AAAA,QAChD;AAAA,UACE,KAAK;AAAA,UACL,OAAO;AAAA,UACP,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAGO,SAAS,wBAAgC;AAC9C,QAAM,SAAS,cAAc;AAC7B,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,SAAS,OAAO,eAAe;AAErC,SAAO,iBAAiB,OAAO,eAAe;AAAA,IAC5C,CAAC,MAAM,IAAI,KAAK,EAAE,SAAS,IAAI;AAAA,EACjC;AAEA,QAAM,UAAU,SAAS,OAAO,eAAe;AAC/C,MAAI,UAAU,GAAG;AACf,kBAAc,MAAM;AAAA,EACtB;AAEA,SAAO;AACT;",
4
+ "sourcesContent": ["/**\n * SMS Notification Hook for StackMemory\n * Sends text messages when tasks are ready for review\n * Supports interactive prompts with numbered options or yes/no\n *\n * Optional feature - requires Twilio setup\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { config as loadDotenv } from 'dotenv';\nimport { writeFileSecure, ensureSecureDir } from './secure-fs.js';\nimport { SMSConfigSchema, parseConfigSafe } from './schemas.js';\n\nexport type MessageChannel = 'whatsapp' | 'sms';\n\nexport interface SMSConfig {\n enabled: boolean;\n // Preferred channel: whatsapp is cheaper for back-and-forth conversations\n channel: MessageChannel;\n // Twilio credentials (from env or config)\n accountSid?: string;\n authToken?: string;\n // SMS numbers\n smsFromNumber?: string;\n smsToNumber?: string;\n // WhatsApp numbers (Twilio prefixes with 'whatsapp:' automatically)\n whatsappFromNumber?: string;\n whatsappToNumber?: string;\n // Legacy fields (backwards compatibility)\n fromNumber?: string;\n toNumber?: string;\n // Webhook URL for receiving responses\n webhookUrl?: string;\n // Notification preferences\n notifyOn: {\n taskComplete: boolean;\n reviewReady: boolean;\n error: boolean;\n custom: boolean;\n contextSync: boolean;\n };\n // Quiet hours (don't send during these times)\n quietHours?: {\n enabled: boolean;\n start: string; // \"22:00\"\n end: string; // \"08:00\"\n };\n // Response timeout (seconds)\n responseTimeout: number;\n // Pending prompts awaiting response\n pendingPrompts: PendingPrompt[];\n}\n\nexport interface PendingPrompt {\n id: string;\n timestamp: string;\n message: string;\n options: PromptOption[];\n type: 'options' | 'yesno' | 'freeform';\n callback?: string; // Command to run with response\n expiresAt: string;\n}\n\nexport interface PromptOption {\n key: string; // \"1\", \"2\", \"y\", \"n\", etc.\n label: string;\n action?: string; // Command to execute\n}\n\nexport interface NotificationPayload {\n type: 'task_complete' | 'review_ready' | 'error' | 'custom' | 'context_sync';\n title: string;\n message: string;\n prompt?: {\n type: 'options' | 'yesno' | 'freeform';\n options?: PromptOption[];\n question?: string;\n };\n metadata?: Record<string, unknown>;\n}\n\nconst CONFIG_PATH = join(homedir(), '.stackmemory', 'sms-notify.json');\n\nconst DEFAULT_CONFIG: SMSConfig = {\n enabled: false,\n channel: 'whatsapp', // WhatsApp is cheaper for conversations\n notifyOn: {\n taskComplete: true,\n reviewReady: true,\n error: true,\n custom: true,\n contextSync: true,\n },\n quietHours: {\n enabled: false,\n start: '22:00',\n end: '08:00',\n },\n responseTimeout: 300, // 5 minutes\n pendingPrompts: [],\n};\n\nexport function loadSMSConfig(): SMSConfig {\n // Load .env files (project, home, global)\n loadDotenv({ path: join(process.cwd(), '.env') });\n loadDotenv({ path: join(process.cwd(), '.env.local') });\n loadDotenv({ path: join(homedir(), '.env') });\n loadDotenv({ path: join(homedir(), '.stackmemory', '.env') });\n\n try {\n if (existsSync(CONFIG_PATH)) {\n const data = readFileSync(CONFIG_PATH, 'utf8');\n const parsed = JSON.parse(data);\n // Validate with zod schema, fall back to defaults on invalid config\n const validated = parseConfigSafe(\n SMSConfigSchema,\n { ...DEFAULT_CONFIG, ...parsed },\n DEFAULT_CONFIG,\n 'sms-notify'\n );\n applyEnvVars(validated);\n return validated;\n }\n } catch {\n // Use defaults\n }\n\n // Check environment variables\n const config = { ...DEFAULT_CONFIG };\n applyEnvVars(config);\n return config;\n}\n\n// Check what's missing for notifications to work\nexport function getMissingConfig(): {\n missing: string[];\n configured: string[];\n ready: boolean;\n} {\n const config = loadSMSConfig();\n const missing: string[] = [];\n const configured: string[] = [];\n\n // Check credentials\n if (config.accountSid) {\n configured.push('TWILIO_ACCOUNT_SID');\n } else {\n missing.push('TWILIO_ACCOUNT_SID');\n }\n\n if (config.authToken) {\n configured.push('TWILIO_AUTH_TOKEN');\n } else {\n missing.push('TWILIO_AUTH_TOKEN');\n }\n\n // Check channel-specific numbers\n const channel = config.channel || 'whatsapp';\n\n if (channel === 'whatsapp') {\n const from = config.whatsappFromNumber || config.fromNumber;\n const to = config.whatsappToNumber || config.toNumber;\n\n if (from) {\n configured.push('TWILIO_WHATSAPP_FROM');\n } else {\n missing.push('TWILIO_WHATSAPP_FROM');\n }\n\n if (to) {\n configured.push('TWILIO_WHATSAPP_TO');\n } else {\n missing.push('TWILIO_WHATSAPP_TO');\n }\n } else {\n const from = config.smsFromNumber || config.fromNumber;\n const to = config.smsToNumber || config.toNumber;\n\n if (from) {\n configured.push('TWILIO_SMS_FROM');\n } else {\n missing.push('TWILIO_SMS_FROM');\n }\n\n if (to) {\n configured.push('TWILIO_SMS_TO');\n } else {\n missing.push('TWILIO_SMS_TO');\n }\n }\n\n return {\n missing,\n configured,\n ready: missing.length === 0,\n };\n}\n\nfunction applyEnvVars(config: SMSConfig): void {\n // Twilio credentials\n if (process.env['TWILIO_ACCOUNT_SID']) {\n config.accountSid = process.env['TWILIO_ACCOUNT_SID'];\n }\n if (process.env['TWILIO_AUTH_TOKEN']) {\n config.authToken = process.env['TWILIO_AUTH_TOKEN'];\n }\n\n // SMS numbers\n if (process.env['TWILIO_SMS_FROM'] || process.env['TWILIO_FROM_NUMBER']) {\n config.smsFromNumber =\n process.env['TWILIO_SMS_FROM'] || process.env['TWILIO_FROM_NUMBER'];\n }\n if (process.env['TWILIO_SMS_TO'] || process.env['TWILIO_TO_NUMBER']) {\n config.smsToNumber =\n process.env['TWILIO_SMS_TO'] || process.env['TWILIO_TO_NUMBER'];\n }\n\n // WhatsApp numbers\n if (process.env['TWILIO_WHATSAPP_FROM']) {\n config.whatsappFromNumber = process.env['TWILIO_WHATSAPP_FROM'];\n }\n if (process.env['TWILIO_WHATSAPP_TO']) {\n config.whatsappToNumber = process.env['TWILIO_WHATSAPP_TO'];\n }\n\n // Legacy support\n if (process.env['TWILIO_FROM_NUMBER']) {\n config.fromNumber = process.env['TWILIO_FROM_NUMBER'];\n }\n if (process.env['TWILIO_TO_NUMBER']) {\n config.toNumber = process.env['TWILIO_TO_NUMBER'];\n }\n\n // Channel preference\n if (process.env['TWILIO_CHANNEL']) {\n config.channel = process.env['TWILIO_CHANNEL'] as MessageChannel;\n }\n}\n\nexport function saveSMSConfig(config: SMSConfig): void {\n try {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n // Don't save sensitive credentials to file\n const safeConfig = { ...config };\n delete safeConfig.accountSid;\n delete safeConfig.authToken;\n writeFileSecure(CONFIG_PATH, JSON.stringify(safeConfig, null, 2));\n } catch {\n // Silently fail\n }\n}\n\nfunction isQuietHours(config: SMSConfig): boolean {\n if (!config.quietHours?.enabled) return false;\n\n const now = new Date();\n const currentTime = now.getHours() * 60 + now.getMinutes();\n\n const [startH, startM] = config.quietHours.start.split(':').map(Number);\n const [endH, endM] = config.quietHours.end.split(':').map(Number);\n\n const startTime = startH * 60 + startM;\n const endTime = endH * 60 + endM;\n\n // Handle overnight quiet hours (e.g., 22:00 - 08:00)\n if (startTime > endTime) {\n return currentTime >= startTime || currentTime < endTime;\n }\n\n return currentTime >= startTime && currentTime < endTime;\n}\n\nfunction generatePromptId(): string {\n return Math.random().toString(36).substring(2, 10);\n}\n\nfunction formatPromptMessage(payload: NotificationPayload): string {\n let message = `${payload.title}\\n\\n${payload.message}`;\n\n if (payload.prompt) {\n message += '\\n\\n';\n\n if (payload.prompt.question) {\n message += `${payload.prompt.question}\\n`;\n }\n\n if (payload.prompt.type === 'yesno') {\n message += 'Reply Y for Yes, N for No';\n } else if (payload.prompt.type === 'options' && payload.prompt.options) {\n payload.prompt.options.forEach((opt) => {\n message += `${opt.key}. ${opt.label}\\n`;\n });\n message += '\\nReply with number to select';\n } else if (payload.prompt.type === 'freeform') {\n message += 'Reply with your response';\n }\n }\n\n // Always append session URL if available\n return appendSessionUrl(message);\n}\n\nfunction getChannelNumbers(config: SMSConfig): {\n from: string;\n to: string;\n channel: MessageChannel;\n} | null {\n const channel = config.channel || 'whatsapp';\n\n if (channel === 'whatsapp') {\n // Try WhatsApp first\n const from = config.whatsappFromNumber || config.fromNumber;\n const to = config.whatsappToNumber || config.toNumber;\n if (from && to) {\n // Twilio requires 'whatsapp:' prefix for WhatsApp numbers\n return {\n from: from.startsWith('whatsapp:') ? from : `whatsapp:${from}`,\n to: to.startsWith('whatsapp:') ? to : `whatsapp:${to}`,\n channel: 'whatsapp',\n };\n }\n }\n\n // Fall back to SMS\n const from = config.smsFromNumber || config.fromNumber;\n const to = config.smsToNumber || config.toNumber;\n if (from && to) {\n return { from, to, channel: 'sms' };\n }\n\n return null;\n}\n\nexport async function sendNotification(\n payload: NotificationPayload,\n channelOverride?: MessageChannel\n): Promise<{\n success: boolean;\n promptId?: string;\n channel?: MessageChannel;\n error?: string;\n}> {\n const config = loadSMSConfig();\n\n if (!config.enabled) {\n return { success: false, error: 'Notifications disabled' };\n }\n\n // Check notification type is enabled\n const typeMap: Record<string, keyof typeof config.notifyOn> = {\n task_complete: 'taskComplete',\n review_ready: 'reviewReady',\n error: 'error',\n custom: 'custom',\n context_sync: 'contextSync',\n };\n\n if (!config.notifyOn[typeMap[payload.type]]) {\n return {\n success: false,\n error: `Notifications for ${payload.type} disabled`,\n };\n }\n\n // Check quiet hours\n if (isQuietHours(config)) {\n return { success: false, error: 'Quiet hours active' };\n }\n\n // Validate credentials\n if (!config.accountSid || !config.authToken) {\n return {\n success: false,\n error:\n 'Missing Twilio credentials. Set TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN',\n };\n }\n\n // Get channel numbers (prefer WhatsApp)\n const originalChannel = config.channel;\n if (channelOverride) {\n config.channel = channelOverride;\n }\n\n const numbers = getChannelNumbers(config);\n config.channel = originalChannel; // Restore\n\n if (!numbers) {\n return {\n success: false,\n error:\n config.channel === 'whatsapp'\n ? 'Missing WhatsApp numbers. Set TWILIO_WHATSAPP_FROM and TWILIO_WHATSAPP_TO'\n : 'Missing SMS numbers. Set TWILIO_SMS_FROM and TWILIO_SMS_TO',\n };\n }\n\n const message = formatPromptMessage(payload);\n let promptId: string | undefined;\n\n // Store pending prompt if interactive\n if (payload.prompt) {\n promptId = generatePromptId();\n const expiresAt = new Date(\n Date.now() + config.responseTimeout * 1000\n ).toISOString();\n\n const pendingPrompt: PendingPrompt = {\n id: promptId,\n timestamp: new Date().toISOString(),\n message: payload.message,\n options: payload.prompt.options || [],\n type: payload.prompt.type,\n expiresAt,\n };\n\n config.pendingPrompts.push(pendingPrompt);\n saveSMSConfig(config);\n }\n\n try {\n // Use Twilio API (same endpoint for SMS and WhatsApp)\n const twilioUrl = `https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`;\n\n const response = await fetch(twilioUrl, {\n method: 'POST',\n headers: {\n Authorization:\n 'Basic ' +\n Buffer.from(`${config.accountSid}:${config.authToken}`).toString(\n 'base64'\n ),\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n From: numbers.from,\n To: numbers.to,\n Body: message,\n }),\n });\n\n if (!response.ok) {\n const errorData = await response.text();\n return {\n success: false,\n channel: numbers.channel,\n error: `Twilio error: ${errorData}`,\n };\n }\n\n return { success: true, promptId, channel: numbers.channel };\n } catch (err) {\n return {\n success: false,\n channel: numbers.channel,\n error: `Failed to send ${numbers.channel}: ${err instanceof Error ? err.message : String(err)}`,\n };\n }\n}\n\n// Backwards compatible alias\nexport async function sendSMSNotification(\n payload: NotificationPayload\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n return sendNotification(payload);\n}\n\nexport function processIncomingResponse(\n from: string,\n body: string\n): {\n matched: boolean;\n prompt?: PendingPrompt;\n response?: string;\n action?: string;\n} {\n const config = loadSMSConfig();\n\n // Normalize response\n const response = body.trim().toLowerCase();\n\n // Find matching pending prompt (most recent first)\n const now = new Date();\n const validPrompts = config.pendingPrompts.filter(\n (p) => new Date(p.expiresAt) > now\n );\n\n if (validPrompts.length === 0) {\n return { matched: false };\n }\n\n // Get most recent prompt\n const prompt = validPrompts[validPrompts.length - 1];\n\n let matchedOption: PromptOption | undefined;\n\n if (prompt.type === 'yesno') {\n if (response === 'y' || response === 'yes') {\n matchedOption = { key: 'y', label: 'Yes' };\n } else if (response === 'n' || response === 'no') {\n matchedOption = { key: 'n', label: 'No' };\n }\n } else if (prompt.type === 'options') {\n matchedOption = prompt.options.find(\n (opt) => opt.key.toLowerCase() === response\n );\n } else if (prompt.type === 'freeform') {\n matchedOption = { key: response, label: response };\n }\n\n // Remove processed prompt\n config.pendingPrompts = config.pendingPrompts.filter(\n (p) => p.id !== prompt.id\n );\n saveSMSConfig(config);\n\n if (matchedOption) {\n return {\n matched: true,\n prompt,\n response: matchedOption.key,\n action: matchedOption.action,\n };\n }\n\n return { matched: false, prompt };\n}\n\n// Get session ID from environment or generate short ID\nfunction getSessionId(): string {\n return (\n process.env['CLAUDE_INSTANCE_ID'] ||\n process.env['STACKMEMORY_SESSION_ID'] ||\n Math.random().toString(36).substring(2, 8)\n );\n}\n\n// Get Claude session URL if available\nexport function getSessionUrl(): string | undefined {\n // Check for remote session URL in environment\n const sessionId = process.env['CLAUDE_SESSION_ID'];\n if (sessionId?.startsWith('session_')) {\n return `https://claude.ai/code/${sessionId}`;\n }\n // Check for explicit URL\n return process.env['CLAUDE_SESSION_URL'];\n}\n\n// Format message with session URL\nfunction appendSessionUrl(message: string): string {\n const url = getSessionUrl();\n if (url) {\n return `${message}\\n\\nSession: ${url}`;\n }\n return message;\n}\n\n// Convenience functions for common notifications\n\nexport async function notifyReviewReady(\n title: string,\n description: string,\n options?: { label: string; action?: string }[]\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const sessionId = getSessionId();\n\n // Ensure minimum 2 options\n let finalOptions = options || [];\n if (finalOptions.length < 2) {\n const defaults = [\n { label: 'Approve', action: 'echo \"Approved\"' },\n { label: 'Request changes', action: 'echo \"Changes requested\"' },\n ];\n finalOptions = [...finalOptions, ...defaults].slice(\n 0,\n Math.max(2, finalOptions.length)\n );\n }\n\n const payload: NotificationPayload = {\n type: 'review_ready',\n title: `[Claude ${sessionId}] Review Ready: ${title}`,\n message: description,\n prompt: {\n type: 'options',\n options: finalOptions.map((opt, i) => ({\n key: String(i + 1),\n label: opt.label,\n action: opt.action,\n })),\n question: 'What would you like to do?',\n },\n };\n\n return sendSMSNotification(payload);\n}\n\nexport async function notifyWithYesNo(\n title: string,\n question: string,\n yesAction?: string,\n noAction?: string\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const sessionId = getSessionId();\n return sendSMSNotification({\n type: 'custom',\n title: `[Claude ${sessionId}] ${title}`,\n message: question,\n prompt: {\n type: 'yesno',\n options: [\n { key: 'y', label: 'Yes', action: yesAction },\n { key: 'n', label: 'No', action: noAction },\n ],\n },\n });\n}\n\nexport async function notifyTaskComplete(\n taskName: string,\n summary: string\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const sessionId = getSessionId();\n return sendSMSNotification({\n type: 'task_complete',\n title: `[Claude ${sessionId}] Task Complete: ${taskName}`,\n message: summary,\n prompt: {\n type: 'options',\n options: [\n { key: '1', label: 'Start next task', action: 'claude-sm' },\n { key: '2', label: 'View details', action: 'stackmemory status' },\n ],\n },\n });\n}\n\nexport async function notifyError(\n error: string,\n context?: string\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const sessionId = getSessionId();\n return sendSMSNotification({\n type: 'error',\n title: `[Claude ${sessionId}] Error Alert`,\n message: context ? `${error}\\n\\nContext: ${context}` : error,\n prompt: {\n type: 'options',\n options: [\n { key: '1', label: 'Retry', action: 'claude-sm' },\n {\n key: '2',\n label: 'View logs',\n action: 'tail -50 ~/.claude/logs/*.log',\n },\n ],\n },\n });\n}\n\n// Clean up expired prompts\nexport function cleanupExpiredPrompts(): number {\n const config = loadSMSConfig();\n const now = new Date();\n const before = config.pendingPrompts.length;\n\n config.pendingPrompts = config.pendingPrompts.filter(\n (p) => new Date(p.expiresAt) > now\n );\n\n const removed = before - config.pendingPrompts.length;\n if (removed > 0) {\n saveSMSConfig(config);\n }\n\n return removed;\n}\n\n// ============================================================================\n// SIMPLIFIED API - Use these for basic notifications\n// ============================================================================\n\n/**\n * Send a simple status notification\n * Always includes session URL if available\n */\nexport async function notify(\n message: string\n): Promise<{ success: boolean; error?: string }> {\n const sessionId = getSessionId();\n return sendNotification({\n type: 'custom',\n title: `[Claude ${sessionId}]`,\n message,\n });\n}\n\n/**\n * Send a notification with A/B choice (1 or 2)\n * Always includes session URL if available\n */\nexport async function notifyChoice(\n message: string,\n optionA: string,\n optionB: string\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const sessionId = getSessionId();\n return sendNotification({\n type: 'custom',\n title: `[Claude ${sessionId}]`,\n message,\n prompt: {\n type: 'options',\n options: [\n { key: '1', label: optionA },\n { key: '2', label: optionB },\n ],\n },\n });\n}\n\n/**\n * Send a notification with Yes/No choice\n * Always includes session URL if available\n */\nexport async function notifyYesNo(\n message: string\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const sessionId = getSessionId();\n return sendNotification({\n type: 'custom',\n title: `[Claude ${sessionId}]`,\n message,\n prompt: { type: 'yesno' },\n });\n}\n\n/**\n * Send step completion notification\n * Always includes session URL if available\n */\nexport async function notifyStep(\n step: string,\n status: 'done' | 'failed' | 'waiting' = 'done'\n): Promise<{ success: boolean; error?: string }> {\n const sessionId = getSessionId();\n const symbol = status === 'done' ? '\u2713' : status === 'failed' ? '\u2717' : '\u23F3';\n return sendNotification({\n type: 'task_complete',\n title: `[Claude ${sessionId}]`,\n message: `${symbol} ${step}`,\n });\n}\n"],
5
+ "mappings": ";;;;AAQA,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,UAAU,kBAAkB;AACrC,SAAS,iBAAiB,uBAAuB;AACjD,SAAS,iBAAiB,uBAAuB;AAsEjD,MAAM,cAAc,KAAK,QAAQ,GAAG,gBAAgB,iBAAiB;AAErE,MAAM,iBAA4B;AAAA,EAChC,SAAS;AAAA,EACT,SAAS;AAAA;AAAA,EACT,UAAU;AAAA,IACR,cAAc;AAAA,IACd,aAAa;AAAA,IACb,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,aAAa;AAAA,EACf;AAAA,EACA,YAAY;AAAA,IACV,SAAS;AAAA,IACT,OAAO;AAAA,IACP,KAAK;AAAA,EACP;AAAA,EACA,iBAAiB;AAAA;AAAA,EACjB,gBAAgB,CAAC;AACnB;AAEO,SAAS,gBAA2B;AAEzC,aAAW,EAAE,MAAM,KAAK,QAAQ,IAAI,GAAG,MAAM,EAAE,CAAC;AAChD,aAAW,EAAE,MAAM,KAAK,QAAQ,IAAI,GAAG,YAAY,EAAE,CAAC;AACtD,aAAW,EAAE,MAAM,KAAK,QAAQ,GAAG,MAAM,EAAE,CAAC;AAC5C,aAAW,EAAE,MAAM,KAAK,QAAQ,GAAG,gBAAgB,MAAM,EAAE,CAAC;AAE5D,MAAI;AACF,QAAI,WAAW,WAAW,GAAG;AAC3B,YAAM,OAAO,aAAa,aAAa,MAAM;AAC7C,YAAM,SAAS,KAAK,MAAM,IAAI;AAE9B,YAAM,YAAY;AAAA,QAChB;AAAA,QACA,EAAE,GAAG,gBAAgB,GAAG,OAAO;AAAA,QAC/B;AAAA,QACA;AAAA,MACF;AACA,mBAAa,SAAS;AACtB,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AAGA,QAAM,SAAS,EAAE,GAAG,eAAe;AACnC,eAAa,MAAM;AACnB,SAAO;AACT;AAGO,SAAS,mBAId;AACA,QAAM,SAAS,cAAc;AAC7B,QAAM,UAAoB,CAAC;AAC3B,QAAM,aAAuB,CAAC;AAG9B,MAAI,OAAO,YAAY;AACrB,eAAW,KAAK,oBAAoB;AAAA,EACtC,OAAO;AACL,YAAQ,KAAK,oBAAoB;AAAA,EACnC;AAEA,MAAI,OAAO,WAAW;AACpB,eAAW,KAAK,mBAAmB;AAAA,EACrC,OAAO;AACL,YAAQ,KAAK,mBAAmB;AAAA,EAClC;AAGA,QAAM,UAAU,OAAO,WAAW;AAElC,MAAI,YAAY,YAAY;AAC1B,UAAM,OAAO,OAAO,sBAAsB,OAAO;AACjD,UAAM,KAAK,OAAO,oBAAoB,OAAO;AAE7C,QAAI,MAAM;AACR,iBAAW,KAAK,sBAAsB;AAAA,IACxC,OAAO;AACL,cAAQ,KAAK,sBAAsB;AAAA,IACrC;AAEA,QAAI,IAAI;AACN,iBAAW,KAAK,oBAAoB;AAAA,IACtC,OAAO;AACL,cAAQ,KAAK,oBAAoB;AAAA,IACnC;AAAA,EACF,OAAO;AACL,UAAM,OAAO,OAAO,iBAAiB,OAAO;AAC5C,UAAM,KAAK,OAAO,eAAe,OAAO;AAExC,QAAI,MAAM;AACR,iBAAW,KAAK,iBAAiB;AAAA,IACnC,OAAO;AACL,cAAQ,KAAK,iBAAiB;AAAA,IAChC;AAEA,QAAI,IAAI;AACN,iBAAW,KAAK,eAAe;AAAA,IACjC,OAAO;AACL,cAAQ,KAAK,eAAe;AAAA,IAC9B;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,OAAO,QAAQ,WAAW;AAAA,EAC5B;AACF;AAEA,SAAS,aAAa,QAAyB;AAE7C,MAAI,QAAQ,IAAI,oBAAoB,GAAG;AACrC,WAAO,aAAa,QAAQ,IAAI,oBAAoB;AAAA,EACtD;AACA,MAAI,QAAQ,IAAI,mBAAmB,GAAG;AACpC,WAAO,YAAY,QAAQ,IAAI,mBAAmB;AAAA,EACpD;AAGA,MAAI,QAAQ,IAAI,iBAAiB,KAAK,QAAQ,IAAI,oBAAoB,GAAG;AACvE,WAAO,gBACL,QAAQ,IAAI,iBAAiB,KAAK,QAAQ,IAAI,oBAAoB;AAAA,EACtE;AACA,MAAI,QAAQ,IAAI,eAAe,KAAK,QAAQ,IAAI,kBAAkB,GAAG;AACnE,WAAO,cACL,QAAQ,IAAI,eAAe,KAAK,QAAQ,IAAI,kBAAkB;AAAA,EAClE;AAGA,MAAI,QAAQ,IAAI,sBAAsB,GAAG;AACvC,WAAO,qBAAqB,QAAQ,IAAI,sBAAsB;AAAA,EAChE;AACA,MAAI,QAAQ,IAAI,oBAAoB,GAAG;AACrC,WAAO,mBAAmB,QAAQ,IAAI,oBAAoB;AAAA,EAC5D;AAGA,MAAI,QAAQ,IAAI,oBAAoB,GAAG;AACrC,WAAO,aAAa,QAAQ,IAAI,oBAAoB;AAAA,EACtD;AACA,MAAI,QAAQ,IAAI,kBAAkB,GAAG;AACnC,WAAO,WAAW,QAAQ,IAAI,kBAAkB;AAAA,EAClD;AAGA,MAAI,QAAQ,IAAI,gBAAgB,GAAG;AACjC,WAAO,UAAU,QAAQ,IAAI,gBAAgB;AAAA,EAC/C;AACF;AAEO,SAAS,cAAc,QAAyB;AACrD,MAAI;AACF,oBAAgB,KAAK,QAAQ,GAAG,cAAc,CAAC;AAE/C,UAAM,aAAa,EAAE,GAAG,OAAO;AAC/B,WAAO,WAAW;AAClB,WAAO,WAAW;AAClB,oBAAgB,aAAa,KAAK,UAAU,YAAY,MAAM,CAAC,CAAC;AAAA,EAClE,QAAQ;AAAA,EAER;AACF;AAEA,SAAS,aAAa,QAA4B;AAChD,MAAI,CAAC,OAAO,YAAY,QAAS,QAAO;AAExC,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,cAAc,IAAI,SAAS,IAAI,KAAK,IAAI,WAAW;AAEzD,QAAM,CAAC,QAAQ,MAAM,IAAI,OAAO,WAAW,MAAM,MAAM,GAAG,EAAE,IAAI,MAAM;AACtE,QAAM,CAAC,MAAM,IAAI,IAAI,OAAO,WAAW,IAAI,MAAM,GAAG,EAAE,IAAI,MAAM;AAEhE,QAAM,YAAY,SAAS,KAAK;AAChC,QAAM,UAAU,OAAO,KAAK;AAG5B,MAAI,YAAY,SAAS;AACvB,WAAO,eAAe,aAAa,cAAc;AAAA,EACnD;AAEA,SAAO,eAAe,aAAa,cAAc;AACnD;AAEA,SAAS,mBAA2B;AAClC,SAAO,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,UAAU,GAAG,EAAE;AACnD;AAEA,SAAS,oBAAoB,SAAsC;AACjE,MAAI,UAAU,GAAG,QAAQ,KAAK;AAAA;AAAA,EAAO,QAAQ,OAAO;AAEpD,MAAI,QAAQ,QAAQ;AAClB,eAAW;AAEX,QAAI,QAAQ,OAAO,UAAU;AAC3B,iBAAW,GAAG,QAAQ,OAAO,QAAQ;AAAA;AAAA,IACvC;AAEA,QAAI,QAAQ,OAAO,SAAS,SAAS;AACnC,iBAAW;AAAA,IACb,WAAW,QAAQ,OAAO,SAAS,aAAa,QAAQ,OAAO,SAAS;AACtE,cAAQ,OAAO,QAAQ,QAAQ,CAAC,QAAQ;AACtC,mBAAW,GAAG,IAAI,GAAG,KAAK,IAAI,KAAK;AAAA;AAAA,MACrC,CAAC;AACD,iBAAW;AAAA,IACb,WAAW,QAAQ,OAAO,SAAS,YAAY;AAC7C,iBAAW;AAAA,IACb;AAAA,EACF;AAGA,SAAO,iBAAiB,OAAO;AACjC;AAEA,SAAS,kBAAkB,QAIlB;AACP,QAAM,UAAU,OAAO,WAAW;AAElC,MAAI,YAAY,YAAY;AAE1B,UAAMA,QAAO,OAAO,sBAAsB,OAAO;AACjD,UAAMC,MAAK,OAAO,oBAAoB,OAAO;AAC7C,QAAID,SAAQC,KAAI;AAEd,aAAO;AAAA,QACL,MAAMD,MAAK,WAAW,WAAW,IAAIA,QAAO,YAAYA,KAAI;AAAA,QAC5D,IAAIC,IAAG,WAAW,WAAW,IAAIA,MAAK,YAAYA,GAAE;AAAA,QACpD,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AAGA,QAAM,OAAO,OAAO,iBAAiB,OAAO;AAC5C,QAAM,KAAK,OAAO,eAAe,OAAO;AACxC,MAAI,QAAQ,IAAI;AACd,WAAO,EAAE,MAAM,IAAI,SAAS,MAAM;AAAA,EACpC;AAEA,SAAO;AACT;AAEA,eAAsB,iBACpB,SACA,iBAMC;AACD,QAAM,SAAS,cAAc;AAE7B,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,EAAE,SAAS,OAAO,OAAO,yBAAyB;AAAA,EAC3D;AAGA,QAAM,UAAwD;AAAA,IAC5D,eAAe;AAAA,IACf,cAAc;AAAA,IACd,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB;AAEA,MAAI,CAAC,OAAO,SAAS,QAAQ,QAAQ,IAAI,CAAC,GAAG;AAC3C,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,qBAAqB,QAAQ,IAAI;AAAA,IAC1C;AAAA,EACF;AAGA,MAAI,aAAa,MAAM,GAAG;AACxB,WAAO,EAAE,SAAS,OAAO,OAAO,qBAAqB;AAAA,EACvD;AAGA,MAAI,CAAC,OAAO,cAAc,CAAC,OAAO,WAAW;AAC3C,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OACE;AAAA,IACJ;AAAA,EACF;AAGA,QAAM,kBAAkB,OAAO;AAC/B,MAAI,iBAAiB;AACnB,WAAO,UAAU;AAAA,EACnB;AAEA,QAAM,UAAU,kBAAkB,MAAM;AACxC,SAAO,UAAU;AAEjB,MAAI,CAAC,SAAS;AACZ,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OACE,OAAO,YAAY,aACf,8EACA;AAAA,IACR;AAAA,EACF;AAEA,QAAM,UAAU,oBAAoB,OAAO;AAC3C,MAAI;AAGJ,MAAI,QAAQ,QAAQ;AAClB,eAAW,iBAAiB;AAC5B,UAAM,YAAY,IAAI;AAAA,MACpB,KAAK,IAAI,IAAI,OAAO,kBAAkB;AAAA,IACxC,EAAE,YAAY;AAEd,UAAM,gBAA+B;AAAA,MACnC,IAAI;AAAA,MACJ,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,SAAS,QAAQ;AAAA,MACjB,SAAS,QAAQ,OAAO,WAAW,CAAC;AAAA,MACpC,MAAM,QAAQ,OAAO;AAAA,MACrB;AAAA,IACF;AAEA,WAAO,eAAe,KAAK,aAAa;AACxC,kBAAc,MAAM;AAAA,EACtB;AAEA,MAAI;AAEF,UAAM,YAAY,8CAA8C,OAAO,UAAU;AAEjF,UAAM,WAAW,MAAM,MAAM,WAAW;AAAA,MACtC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eACE,WACA,OAAO,KAAK,GAAG,OAAO,UAAU,IAAI,OAAO,SAAS,EAAE,EAAE;AAAA,UACtD;AAAA,QACF;AAAA,QACF,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,IAAI,gBAAgB;AAAA,QACxB,MAAM,QAAQ;AAAA,QACd,IAAI,QAAQ;AAAA,QACZ,MAAM;AAAA,MACR,CAAC;AAAA,IACH,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,YAAY,MAAM,SAAS,KAAK;AACtC,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,QAAQ;AAAA,QACjB,OAAO,iBAAiB,SAAS;AAAA,MACnC;AAAA,IACF;AAEA,WAAO,EAAE,SAAS,MAAM,UAAU,SAAS,QAAQ,QAAQ;AAAA,EAC7D,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS,QAAQ;AAAA,MACjB,OAAO,kBAAkB,QAAQ,OAAO,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,IAC/F;AAAA,EACF;AACF;AAGA,eAAsB,oBACpB,SACkE;AAClE,SAAO,iBAAiB,OAAO;AACjC;AAEO,SAAS,wBACd,MACA,MAMA;AACA,QAAM,SAAS,cAAc;AAG7B,QAAM,WAAW,KAAK,KAAK,EAAE,YAAY;AAGzC,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,eAAe,OAAO,eAAe;AAAA,IACzC,CAAC,MAAM,IAAI,KAAK,EAAE,SAAS,IAAI;AAAA,EACjC;AAEA,MAAI,aAAa,WAAW,GAAG;AAC7B,WAAO,EAAE,SAAS,MAAM;AAAA,EAC1B;AAGA,QAAM,SAAS,aAAa,aAAa,SAAS,CAAC;AAEnD,MAAI;AAEJ,MAAI,OAAO,SAAS,SAAS;AAC3B,QAAI,aAAa,OAAO,aAAa,OAAO;AAC1C,sBAAgB,EAAE,KAAK,KAAK,OAAO,MAAM;AAAA,IAC3C,WAAW,aAAa,OAAO,aAAa,MAAM;AAChD,sBAAgB,EAAE,KAAK,KAAK,OAAO,KAAK;AAAA,IAC1C;AAAA,EACF,WAAW,OAAO,SAAS,WAAW;AACpC,oBAAgB,OAAO,QAAQ;AAAA,MAC7B,CAAC,QAAQ,IAAI,IAAI,YAAY,MAAM;AAAA,IACrC;AAAA,EACF,WAAW,OAAO,SAAS,YAAY;AACrC,oBAAgB,EAAE,KAAK,UAAU,OAAO,SAAS;AAAA,EACnD;AAGA,SAAO,iBAAiB,OAAO,eAAe;AAAA,IAC5C,CAAC,MAAM,EAAE,OAAO,OAAO;AAAA,EACzB;AACA,gBAAc,MAAM;AAEpB,MAAI,eAAe;AACjB,WAAO;AAAA,MACL,SAAS;AAAA,MACT;AAAA,MACA,UAAU,cAAc;AAAA,MACxB,QAAQ,cAAc;AAAA,IACxB;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,OAAO,OAAO;AAClC;AAGA,SAAS,eAAuB;AAC9B,SACE,QAAQ,IAAI,oBAAoB,KAChC,QAAQ,IAAI,wBAAwB,KACpC,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,UAAU,GAAG,CAAC;AAE7C;AAGO,SAAS,gBAAoC;AAElD,QAAM,YAAY,QAAQ,IAAI,mBAAmB;AACjD,MAAI,WAAW,WAAW,UAAU,GAAG;AACrC,WAAO,0BAA0B,SAAS;AAAA,EAC5C;AAEA,SAAO,QAAQ,IAAI,oBAAoB;AACzC;AAGA,SAAS,iBAAiB,SAAyB;AACjD,QAAM,MAAM,cAAc;AAC1B,MAAI,KAAK;AACP,WAAO,GAAG,OAAO;AAAA;AAAA,WAAgB,GAAG;AAAA,EACtC;AACA,SAAO;AACT;AAIA,eAAsB,kBACpB,OACA,aACA,SACkE;AAClE,QAAM,YAAY,aAAa;AAG/B,MAAI,eAAe,WAAW,CAAC;AAC/B,MAAI,aAAa,SAAS,GAAG;AAC3B,UAAM,WAAW;AAAA,MACf,EAAE,OAAO,WAAW,QAAQ,kBAAkB;AAAA,MAC9C,EAAE,OAAO,mBAAmB,QAAQ,2BAA2B;AAAA,IACjE;AACA,mBAAe,CAAC,GAAG,cAAc,GAAG,QAAQ,EAAE;AAAA,MAC5C;AAAA,MACA,KAAK,IAAI,GAAG,aAAa,MAAM;AAAA,IACjC;AAAA,EACF;AAEA,QAAM,UAA+B;AAAA,IACnC,MAAM;AAAA,IACN,OAAO,WAAW,SAAS,mBAAmB,KAAK;AAAA,IACnD,SAAS;AAAA,IACT,QAAQ;AAAA,MACN,MAAM;AAAA,MACN,SAAS,aAAa,IAAI,CAAC,KAAK,OAAO;AAAA,QACrC,KAAK,OAAO,IAAI,CAAC;AAAA,QACjB,OAAO,IAAI;AAAA,QACX,QAAQ,IAAI;AAAA,MACd,EAAE;AAAA,MACF,UAAU;AAAA,IACZ;AAAA,EACF;AAEA,SAAO,oBAAoB,OAAO;AACpC;AAEA,eAAsB,gBACpB,OACA,UACA,WACA,UACkE;AAClE,QAAM,YAAY,aAAa;AAC/B,SAAO,oBAAoB;AAAA,IACzB,MAAM;AAAA,IACN,OAAO,WAAW,SAAS,KAAK,KAAK;AAAA,IACrC,SAAS;AAAA,IACT,QAAQ;AAAA,MACN,MAAM;AAAA,MACN,SAAS;AAAA,QACP,EAAE,KAAK,KAAK,OAAO,OAAO,QAAQ,UAAU;AAAA,QAC5C,EAAE,KAAK,KAAK,OAAO,MAAM,QAAQ,SAAS;AAAA,MAC5C;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAEA,eAAsB,mBACpB,UACA,SACkE;AAClE,QAAM,YAAY,aAAa;AAC/B,SAAO,oBAAoB;AAAA,IACzB,MAAM;AAAA,IACN,OAAO,WAAW,SAAS,oBAAoB,QAAQ;AAAA,IACvD,SAAS;AAAA,IACT,QAAQ;AAAA,MACN,MAAM;AAAA,MACN,SAAS;AAAA,QACP,EAAE,KAAK,KAAK,OAAO,mBAAmB,QAAQ,YAAY;AAAA,QAC1D,EAAE,KAAK,KAAK,OAAO,gBAAgB,QAAQ,qBAAqB;AAAA,MAClE;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAEA,eAAsB,YACpB,OACA,SACkE;AAClE,QAAM,YAAY,aAAa;AAC/B,SAAO,oBAAoB;AAAA,IACzB,MAAM;AAAA,IACN,OAAO,WAAW,SAAS;AAAA,IAC3B,SAAS,UAAU,GAAG,KAAK;AAAA;AAAA,WAAgB,OAAO,KAAK;AAAA,IACvD,QAAQ;AAAA,MACN,MAAM;AAAA,MACN,SAAS;AAAA,QACP,EAAE,KAAK,KAAK,OAAO,SAAS,QAAQ,YAAY;AAAA,QAChD;AAAA,UACE,KAAK;AAAA,UACL,OAAO;AAAA,UACP,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAGO,SAAS,wBAAgC;AAC9C,QAAM,SAAS,cAAc;AAC7B,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,SAAS,OAAO,eAAe;AAErC,SAAO,iBAAiB,OAAO,eAAe;AAAA,IAC5C,CAAC,MAAM,IAAI,KAAK,EAAE,SAAS,IAAI;AAAA,EACjC;AAEA,QAAM,UAAU,SAAS,OAAO,eAAe;AAC/C,MAAI,UAAU,GAAG;AACf,kBAAc,MAAM;AAAA,EACtB;AAEA,SAAO;AACT;AAUA,eAAsB,OACpB,SAC+C;AAC/C,QAAM,YAAY,aAAa;AAC/B,SAAO,iBAAiB;AAAA,IACtB,MAAM;AAAA,IACN,OAAO,WAAW,SAAS;AAAA,IAC3B;AAAA,EACF,CAAC;AACH;AAMA,eAAsB,aACpB,SACA,SACA,SACkE;AAClE,QAAM,YAAY,aAAa;AAC/B,SAAO,iBAAiB;AAAA,IACtB,MAAM;AAAA,IACN,OAAO,WAAW,SAAS;AAAA,IAC3B;AAAA,IACA,QAAQ;AAAA,MACN,MAAM;AAAA,MACN,SAAS;AAAA,QACP,EAAE,KAAK,KAAK,OAAO,QAAQ;AAAA,QAC3B,EAAE,KAAK,KAAK,OAAO,QAAQ;AAAA,MAC7B;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAMA,eAAsB,YACpB,SACkE;AAClE,QAAM,YAAY,aAAa;AAC/B,SAAO,iBAAiB;AAAA,IACtB,MAAM;AAAA,IACN,OAAO,WAAW,SAAS;AAAA,IAC3B;AAAA,IACA,QAAQ,EAAE,MAAM,QAAQ;AAAA,EAC1B,CAAC;AACH;AAMA,eAAsB,WACpB,MACA,SAAwC,QACO;AAC/C,QAAM,YAAY,aAAa;AAC/B,QAAM,SAAS,WAAW,SAAS,WAAM,WAAW,WAAW,WAAM;AACrE,SAAO,iBAAiB;AAAA,IACtB,MAAM;AAAA,IACN,OAAO,WAAW,SAAS;AAAA,IAC3B,SAAS,GAAG,MAAM,IAAI,IAAI;AAAA,EAC5B,CAAC;AACH;",
6
6
  "names": ["from", "to"]
7
7
  }
@@ -59,10 +59,17 @@ function checkRateLimit(ip) {
59
59
  function verifyTwilioSignature(url, params, signature) {
60
60
  const authToken = process.env["TWILIO_AUTH_TOKEN"];
61
61
  if (!authToken) {
62
- console.warn(
63
- "[sms-webhook] TWILIO_AUTH_TOKEN not set, skipping signature verification"
62
+ const isDev = process.env["NODE_ENV"] === "development" || process.env["SKIP_TWILIO_VERIFICATION"] === "true";
63
+ if (isDev) {
64
+ console.warn(
65
+ "[sms-webhook] TWILIO_AUTH_TOKEN not set, skipping verification (dev mode)"
66
+ );
67
+ return true;
68
+ }
69
+ console.error(
70
+ "[sms-webhook] TWILIO_AUTH_TOKEN not set - rejecting request in production"
64
71
  );
65
- return true;
72
+ return false;
66
73
  }
67
74
  const sortedKeys = Object.keys(params).sort();
68
75
  let data = url;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/hooks/sms-webhook.ts"],
4
- "sourcesContent": ["/**\n * SMS Webhook Handler for receiving Twilio responses\n * Can run as standalone server or integrate with existing Express app\n *\n * Security features:\n * - Twilio signature verification\n * - Rate limiting per IP\n * - Body size limits\n * - Content-type validation\n * - Safe action execution (no shell injection)\n */\n\nimport { createServer, IncomingMessage, ServerResponse } from 'http';\nimport { parse as parseUrl } from 'url';\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { createHmac } from 'crypto';\nimport { execFileSync } from 'child_process';\nimport {\n processIncomingResponse,\n loadSMSConfig,\n cleanupExpiredPrompts,\n sendNotification,\n} from './sms-notify.js';\nimport {\n queueAction,\n executeActionSafe,\n cleanupOldActions,\n} from './sms-action-runner.js';\nimport {\n isCommand,\n processCommand,\n sendCommandResponse,\n} from './whatsapp-commands.js';\nimport { writeFileSecure, ensureSecureDir } from './secure-fs.js';\nimport {\n logWebhookRequest,\n logRateLimit,\n logSignatureInvalid,\n logBodyTooLarge,\n logContentTypeInvalid,\n logActionAllowed,\n logActionBlocked,\n logCleanup,\n} from './security-logger.js';\n\n// Cleanup interval (5 minutes)\nconst CLEANUP_INTERVAL_MS = 5 * 60 * 1000;\n\n// Input validation constants\nconst MAX_SMS_BODY_LENGTH = 1000;\nconst MAX_PHONE_LENGTH = 50; // WhatsApp format: whatsapp:+12345678901\n\n// Security constants\nconst MAX_BODY_SIZE = 50 * 1024; // 50KB max body\nconst RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute\nconst RATE_LIMIT_MAX_REQUESTS = 30; // 30 requests per minute per IP\n\n// Rate limiting store (in production, use Redis)\nconst rateLimitStore = new Map<string, { count: number; resetTime: number }>();\n\nfunction checkRateLimit(ip: string): boolean {\n const now = Date.now();\n const record = rateLimitStore.get(ip);\n\n if (!record || now > record.resetTime) {\n rateLimitStore.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS });\n return true;\n }\n\n if (record.count >= RATE_LIMIT_MAX_REQUESTS) {\n return false;\n }\n\n record.count++;\n return true;\n}\n\n// Twilio signature verification\nfunction verifyTwilioSignature(\n url: string,\n params: Record<string, string>,\n signature: string\n): boolean {\n const authToken = process.env['TWILIO_AUTH_TOKEN'];\n if (!authToken) {\n console.warn(\n '[sms-webhook] TWILIO_AUTH_TOKEN not set, skipping signature verification'\n );\n return true; // Allow in development, but log warning\n }\n\n // Build the data string (URL + sorted params)\n const sortedKeys = Object.keys(params).sort();\n let data = url;\n for (const key of sortedKeys) {\n data += key + params[key];\n }\n\n // Calculate expected signature\n const hmac = createHmac('sha1', authToken);\n hmac.update(data);\n const expectedSignature = hmac.digest('base64');\n\n return signature === expectedSignature;\n}\n\ninterface TwilioWebhookPayload {\n From: string;\n To: string;\n Body: string;\n MessageSid: string;\n}\n\nfunction parseFormData(body: string): Record<string, string> {\n const params = new URLSearchParams(body);\n const result: Record<string, string> = {};\n params.forEach((value, key) => {\n result[key] = value;\n });\n return result;\n}\n\n// Store response for Claude hook to pick up\nfunction storeLatestResponse(\n promptId: string,\n response: string,\n action?: string\n): void {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n const responsePath = join(\n homedir(),\n '.stackmemory',\n 'sms-latest-response.json'\n );\n writeFileSecure(\n responsePath,\n JSON.stringify({\n promptId,\n response,\n action,\n timestamp: new Date().toISOString(),\n })\n );\n}\n\n/**\n * Store incoming request for Claude to pick up\n * Used when a WhatsApp/SMS message arrives without a pending prompt\n */\nfunction storeIncomingRequest(from: string, message: string): void {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n const requestPath = join(\n homedir(),\n '.stackmemory',\n 'sms-incoming-request.json'\n );\n writeFileSecure(\n requestPath,\n JSON.stringify({\n from,\n message,\n timestamp: new Date().toISOString(),\n processed: false,\n })\n );\n}\n\n/**\n * Get pending incoming request (if any)\n */\nexport function getIncomingRequest(): {\n from: string;\n message: string;\n timestamp: string;\n processed: boolean;\n} | null {\n const requestPath = join(\n homedir(),\n '.stackmemory',\n 'sms-incoming-request.json'\n );\n if (!existsSync(requestPath)) {\n return null;\n }\n try {\n const data = JSON.parse(readFileSync(requestPath, 'utf-8'));\n if (data.processed) {\n return null;\n }\n return data;\n } catch {\n return null;\n }\n}\n\n/**\n * Mark incoming request as processed\n */\nexport function markRequestProcessed(): void {\n const requestPath = join(\n homedir(),\n '.stackmemory',\n 'sms-incoming-request.json'\n );\n if (!existsSync(requestPath)) {\n return;\n }\n try {\n const data = JSON.parse(readFileSync(requestPath, 'utf-8'));\n data.processed = true;\n writeFileSecure(requestPath, JSON.stringify(data));\n } catch {\n // Ignore errors\n }\n}\n\nexport async function handleSMSWebhook(payload: TwilioWebhookPayload): Promise<{\n response: string;\n action?: string;\n queued?: boolean;\n}> {\n const { From, Body } = payload;\n\n // Input length validation\n if (Body && Body.length > MAX_SMS_BODY_LENGTH) {\n console.log(`[sms-webhook] Body too long: ${Body.length} chars`);\n return { response: 'Message too long. Max 1000 characters.' };\n }\n\n if (From && From.length > MAX_PHONE_LENGTH) {\n console.log(\n `[sms-webhook] Phone number too long: ${From.length} chars (max ${MAX_PHONE_LENGTH}): ${From.substring(0, 30)}...`\n );\n return { response: 'Invalid phone number.' };\n }\n\n console.log(`[sms-webhook] Received from ${From}: ${Body}`);\n\n // Check for command prefix before prompt matching\n if (isCommand(Body)) {\n console.log(`[sms-webhook] Processing command: ${Body}`);\n const cmdResult = await processCommand(From, Body);\n\n if (cmdResult.handled) {\n // Send response back if we have one\n if (cmdResult.response) {\n // Don't await - fire and forget the response notification\n sendCommandResponse(cmdResult.response).catch(console.error);\n }\n\n return {\n response: cmdResult.response || 'Command processed',\n action: cmdResult.action,\n queued: false,\n };\n }\n // If not handled, fall through to regular prompt matching\n }\n\n const result = processIncomingResponse(From, Body);\n\n if (!result.matched) {\n if (result.prompt) {\n return {\n response: `Invalid response. Expected: ${result.prompt.options.map((o) => o.key).join(', ')}`,\n };\n }\n // No pending prompt - store as new incoming request for Claude\n storeIncomingRequest(From, Body);\n console.log(\n `[sms-webhook] Stored new request from ${From}: ${Body.substring(0, 50)}...`\n );\n return { response: 'Got it! Your request has been queued.' };\n }\n\n // Store response for Claude hook\n storeLatestResponse(\n result.prompt?.id || 'unknown',\n result.response || Body,\n result.action\n );\n\n // Trigger notification to alert user/Claude\n triggerResponseNotification(result.response || Body);\n\n // Execute action safely if present (no shell injection)\n if (result.action) {\n console.log(`[sms-webhook] Executing action: ${result.action}`);\n\n const actionResult = await executeActionSafe(\n result.action,\n result.response || Body\n );\n\n if (actionResult.success) {\n logActionAllowed('sms-webhook', result.action);\n console.log(\n `[sms-webhook] Action completed: ${(actionResult.output || '').substring(0, 200)}`\n );\n\n return {\n response: `Done! Action executed successfully.`,\n action: result.action,\n queued: false,\n };\n } else {\n logActionBlocked(\n 'sms-webhook',\n result.action,\n actionResult.error || 'unknown'\n );\n console.log(`[sms-webhook] Action failed: ${actionResult.error}`);\n\n // Queue for retry\n queueAction(\n result.prompt?.id || 'unknown',\n result.response || Body,\n result.action\n );\n\n return {\n response: `Action failed, queued for retry: ${(actionResult.error || '').substring(0, 50)}`,\n action: result.action,\n queued: true,\n };\n }\n }\n\n return {\n response: `Received: ${result.response}. Next action will be triggered.`,\n };\n}\n\n// Escape string for AppleScript (prevent injection)\nfunction escapeAppleScript(str: string): string {\n return str\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .substring(0, 200); // Limit length\n}\n\n// Trigger notification when response received\nfunction triggerResponseNotification(response: string): void {\n const safeMessage = escapeAppleScript(`SMS Response: ${response}`);\n\n // macOS notification using execFile (safer than execSync with shell)\n try {\n execFileSync(\n 'osascript',\n [\n '-e',\n `display notification \"${safeMessage}\" with title \"StackMemory\" sound name \"Glass\"`,\n ],\n { stdio: 'ignore', timeout: 5000 }\n );\n } catch {\n // Ignore if not on macOS\n }\n\n // Write signal file for other processes\n try {\n const signalPath = join(homedir(), '.stackmemory', 'sms-signal.txt');\n writeFileSecure(\n signalPath,\n JSON.stringify({\n type: 'sms_response',\n response,\n timestamp: new Date().toISOString(),\n })\n );\n } catch {\n // Ignore\n }\n\n console.log(`\\n*** SMS RESPONSE RECEIVED: \"${response}\" ***`);\n console.log(`*** Run: stackmemory notify run-actions ***\\n`);\n}\n\n// TwiML response helper\nfunction twimlResponse(message: string): string {\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response>\n <Message>${escapeXml(message)}</Message>\n</Response>`;\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n\n// Standalone webhook server\nexport function startWebhookServer(port: number = 3456): void {\n const server = createServer(\n async (req: IncomingMessage, res: ServerResponse) => {\n const url = parseUrl(req.url || '/', true);\n\n // Health check\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'ok' }));\n return;\n }\n\n // SMS webhook endpoint (incoming messages)\n if (\n (url.pathname === '/sms' ||\n url.pathname === '/sms/incoming' ||\n url.pathname === '/webhook') &&\n req.method === 'POST'\n ) {\n const clientIp = req.socket.remoteAddress || 'unknown';\n\n // Log webhook request\n logWebhookRequest(\n 'sms-webhook',\n req.method || 'POST',\n url.pathname || '/sms',\n clientIp\n );\n\n // Rate limiting\n if (!checkRateLimit(clientIp)) {\n logRateLimit('sms-webhook', clientIp);\n res.writeHead(429, {\n 'Content-Type': 'text/xml',\n 'Retry-After': '60',\n });\n res.end(twimlResponse('Too many requests. Please try again later.'));\n return;\n }\n\n // Content-type validation\n const contentType = req.headers['content-type'] || '';\n if (!contentType.includes('application/x-www-form-urlencoded')) {\n logContentTypeInvalid('sms-webhook', contentType, clientIp);\n res.writeHead(400, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Invalid content type'));\n return;\n }\n\n let body = '';\n let bodyTooLarge = false;\n\n req.on('data', (chunk) => {\n body += chunk;\n // Body size limit\n if (body.length > MAX_BODY_SIZE) {\n bodyTooLarge = true;\n logBodyTooLarge('sms-webhook', body.length, clientIp);\n req.destroy();\n }\n });\n\n req.on('end', async () => {\n if (bodyTooLarge) {\n res.writeHead(413, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Request too large'));\n return;\n }\n\n try {\n const payload = parseFormData(\n body\n ) as unknown as TwilioWebhookPayload;\n\n // Verify Twilio signature\n const twilioSignature = req.headers['x-twilio-signature'] as string;\n const webhookUrl = `${req.headers['x-forwarded-proto'] || 'http'}://${req.headers.host}${req.url}`;\n\n if (\n twilioSignature &&\n !verifyTwilioSignature(\n webhookUrl,\n payload as unknown as Record<string, string>,\n twilioSignature\n )\n ) {\n logSignatureInvalid('sms-webhook', clientIp);\n console.error('[sms-webhook] Invalid Twilio signature');\n res.writeHead(401, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Unauthorized'));\n return;\n }\n\n const result = await handleSMSWebhook(payload);\n\n res.writeHead(200, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse(result.response));\n } catch (err) {\n console.error('[sms-webhook] Error:', err);\n res.writeHead(500, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Error processing message'));\n }\n });\n return;\n }\n\n // Status callback endpoint (delivery status updates)\n if (url.pathname === '/sms/status' && req.method === 'POST') {\n let body = '';\n req.on('data', (chunk) => {\n body += chunk;\n });\n\n req.on('end', () => {\n try {\n const payload = parseFormData(body);\n console.log(\n `[sms-webhook] Status update: ${payload['MessageSid']} -> ${payload['MessageStatus']}`\n );\n\n // Store status for tracking\n const statusPath = join(\n homedir(),\n '.stackmemory',\n 'sms-status.json'\n );\n const statuses: Record<string, string> = existsSync(statusPath)\n ? JSON.parse(readFileSync(statusPath, 'utf8'))\n : {};\n statuses[payload['MessageSid']] = payload['MessageStatus'];\n writeFileSecure(statusPath, JSON.stringify(statuses, null, 2));\n\n res.writeHead(200, { 'Content-Type': 'text/plain' });\n res.end('OK');\n } catch (err) {\n console.error('[sms-webhook] Status error:', err);\n res.writeHead(500);\n res.end('Error');\n }\n });\n return;\n }\n\n // Server status endpoint\n if (url.pathname === '/status') {\n const config = loadSMSConfig();\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n enabled: config.enabled,\n pendingPrompts: config.pendingPrompts.length,\n })\n );\n return;\n }\n\n // Get pending incoming request endpoint\n if (url.pathname === '/request' && req.method === 'GET') {\n const request = getIncomingRequest();\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ request }));\n return;\n }\n\n // Mark request as processed endpoint\n if (url.pathname === '/request/ack' && req.method === 'POST') {\n markRequestProcessed();\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ success: true }));\n return;\n }\n\n // Send outgoing notification endpoint\n if (url.pathname === '/send' && req.method === 'POST') {\n let body = '';\n req.on('data', (chunk) => {\n body += chunk;\n if (body.length > MAX_BODY_SIZE) {\n req.destroy();\n }\n });\n\n req.on('end', async () => {\n try {\n const payload = JSON.parse(body);\n const message = payload.message || payload.body || '';\n const title = payload.title || 'Notification';\n const type = payload.type || 'custom';\n\n if (!message) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({ success: false, error: 'Message required' })\n );\n return;\n }\n\n const result = await sendNotification({\n type: type as\n | 'task_complete'\n | 'review_ready'\n | 'error'\n | 'custom',\n title,\n message,\n });\n\n res.writeHead(result.success ? 200 : 500, {\n 'Content-Type': 'application/json',\n });\n res.end(JSON.stringify(result));\n } catch (err) {\n console.error('[sms-webhook] Send error:', err);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n success: false,\n error: err instanceof Error ? err.message : 'Send failed',\n })\n );\n }\n });\n return;\n }\n\n res.writeHead(404);\n res.end('Not found');\n }\n );\n\n server.listen(port, () => {\n console.log(`[sms-webhook] Server listening on port ${port}`);\n console.log(\n `[sms-webhook] Incoming messages: http://localhost:${port}/sms/incoming`\n );\n console.log(\n `[sms-webhook] Status callback: http://localhost:${port}/sms/status`\n );\n console.log(`[sms-webhook] Configure these URLs in Twilio console`);\n\n // Start timed cleanup of expired prompts and old actions\n setInterval(() => {\n try {\n const expiredPrompts = cleanupExpiredPrompts();\n const oldActions = cleanupOldActions();\n if (expiredPrompts > 0 || oldActions > 0) {\n logCleanup('sms-webhook', expiredPrompts, oldActions);\n console.log(\n `[sms-webhook] Cleanup: ${expiredPrompts} expired prompts, ${oldActions} old actions`\n );\n }\n } catch {\n // Ignore cleanup errors\n }\n }, CLEANUP_INTERVAL_MS);\n console.log(\n `[sms-webhook] Cleanup interval: every ${CLEANUP_INTERVAL_MS / 1000}s`\n );\n });\n}\n\n// Express middleware for integration\nexport async function smsWebhookMiddleware(\n req: { body: TwilioWebhookPayload },\n res: { type: (t: string) => void; send: (s: string) => void }\n): Promise<void> {\n const result = await handleSMSWebhook(req.body);\n res.type('text/xml');\n res.send(twimlResponse(result.response));\n}\n\n// CLI entry\nif (process.argv[1]?.endsWith('sms-webhook.js')) {\n const port = parseInt(process.env['SMS_WEBHOOK_PORT'] || '3456', 10);\n startWebhookServer(port);\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * SMS Webhook Handler for receiving Twilio responses\n * Can run as standalone server or integrate with existing Express app\n *\n * Security features:\n * - Twilio signature verification\n * - Rate limiting per IP\n * - Body size limits\n * - Content-type validation\n * - Safe action execution (no shell injection)\n */\n\nimport { createServer, IncomingMessage, ServerResponse } from 'http';\nimport { parse as parseUrl } from 'url';\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { createHmac } from 'crypto';\nimport { execFileSync } from 'child_process';\nimport {\n processIncomingResponse,\n loadSMSConfig,\n cleanupExpiredPrompts,\n sendNotification,\n} from './sms-notify.js';\nimport {\n queueAction,\n executeActionSafe,\n cleanupOldActions,\n} from './sms-action-runner.js';\nimport {\n isCommand,\n processCommand,\n sendCommandResponse,\n} from './whatsapp-commands.js';\nimport { writeFileSecure, ensureSecureDir } from './secure-fs.js';\nimport {\n logWebhookRequest,\n logRateLimit,\n logSignatureInvalid,\n logBodyTooLarge,\n logContentTypeInvalid,\n logActionAllowed,\n logActionBlocked,\n logCleanup,\n} from './security-logger.js';\n\n// Cleanup interval (5 minutes)\nconst CLEANUP_INTERVAL_MS = 5 * 60 * 1000;\n\n// Input validation constants\nconst MAX_SMS_BODY_LENGTH = 1000;\nconst MAX_PHONE_LENGTH = 50; // WhatsApp format: whatsapp:+12345678901\n\n// Security constants\nconst MAX_BODY_SIZE = 50 * 1024; // 50KB max body\nconst RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute\nconst RATE_LIMIT_MAX_REQUESTS = 30; // 30 requests per minute per IP\n\n// Rate limiting store (in production, use Redis)\nconst rateLimitStore = new Map<string, { count: number; resetTime: number }>();\n\nfunction checkRateLimit(ip: string): boolean {\n const now = Date.now();\n const record = rateLimitStore.get(ip);\n\n if (!record || now > record.resetTime) {\n rateLimitStore.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS });\n return true;\n }\n\n if (record.count >= RATE_LIMIT_MAX_REQUESTS) {\n return false;\n }\n\n record.count++;\n return true;\n}\n\n// Twilio signature verification\nfunction verifyTwilioSignature(\n url: string,\n params: Record<string, string>,\n signature: string\n): boolean {\n const authToken = process.env['TWILIO_AUTH_TOKEN'];\n if (!authToken) {\n // Only allow bypass in explicit development mode\n const isDev =\n process.env['NODE_ENV'] === 'development' ||\n process.env['SKIP_TWILIO_VERIFICATION'] === 'true';\n\n if (isDev) {\n console.warn(\n '[sms-webhook] TWILIO_AUTH_TOKEN not set, skipping verification (dev mode)'\n );\n return true;\n }\n\n // In production, reject requests without auth token configured\n console.error(\n '[sms-webhook] TWILIO_AUTH_TOKEN not set - rejecting request in production'\n );\n return false;\n }\n\n // Build the data string (URL + sorted params)\n const sortedKeys = Object.keys(params).sort();\n let data = url;\n for (const key of sortedKeys) {\n data += key + params[key];\n }\n\n // Calculate expected signature\n const hmac = createHmac('sha1', authToken);\n hmac.update(data);\n const expectedSignature = hmac.digest('base64');\n\n return signature === expectedSignature;\n}\n\ninterface TwilioWebhookPayload {\n From: string;\n To: string;\n Body: string;\n MessageSid: string;\n}\n\nfunction parseFormData(body: string): Record<string, string> {\n const params = new URLSearchParams(body);\n const result: Record<string, string> = {};\n params.forEach((value, key) => {\n result[key] = value;\n });\n return result;\n}\n\n// Store response for Claude hook to pick up\nfunction storeLatestResponse(\n promptId: string,\n response: string,\n action?: string\n): void {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n const responsePath = join(\n homedir(),\n '.stackmemory',\n 'sms-latest-response.json'\n );\n writeFileSecure(\n responsePath,\n JSON.stringify({\n promptId,\n response,\n action,\n timestamp: new Date().toISOString(),\n })\n );\n}\n\n/**\n * Store incoming request for Claude to pick up\n * Used when a WhatsApp/SMS message arrives without a pending prompt\n */\nfunction storeIncomingRequest(from: string, message: string): void {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n const requestPath = join(\n homedir(),\n '.stackmemory',\n 'sms-incoming-request.json'\n );\n writeFileSecure(\n requestPath,\n JSON.stringify({\n from,\n message,\n timestamp: new Date().toISOString(),\n processed: false,\n })\n );\n}\n\n/**\n * Get pending incoming request (if any)\n */\nexport function getIncomingRequest(): {\n from: string;\n message: string;\n timestamp: string;\n processed: boolean;\n} | null {\n const requestPath = join(\n homedir(),\n '.stackmemory',\n 'sms-incoming-request.json'\n );\n if (!existsSync(requestPath)) {\n return null;\n }\n try {\n const data = JSON.parse(readFileSync(requestPath, 'utf-8'));\n if (data.processed) {\n return null;\n }\n return data;\n } catch {\n return null;\n }\n}\n\n/**\n * Mark incoming request as processed\n */\nexport function markRequestProcessed(): void {\n const requestPath = join(\n homedir(),\n '.stackmemory',\n 'sms-incoming-request.json'\n );\n if (!existsSync(requestPath)) {\n return;\n }\n try {\n const data = JSON.parse(readFileSync(requestPath, 'utf-8'));\n data.processed = true;\n writeFileSecure(requestPath, JSON.stringify(data));\n } catch {\n // Ignore errors\n }\n}\n\nexport async function handleSMSWebhook(payload: TwilioWebhookPayload): Promise<{\n response: string;\n action?: string;\n queued?: boolean;\n}> {\n const { From, Body } = payload;\n\n // Input length validation\n if (Body && Body.length > MAX_SMS_BODY_LENGTH) {\n console.log(`[sms-webhook] Body too long: ${Body.length} chars`);\n return { response: 'Message too long. Max 1000 characters.' };\n }\n\n if (From && From.length > MAX_PHONE_LENGTH) {\n console.log(\n `[sms-webhook] Phone number too long: ${From.length} chars (max ${MAX_PHONE_LENGTH}): ${From.substring(0, 30)}...`\n );\n return { response: 'Invalid phone number.' };\n }\n\n console.log(`[sms-webhook] Received from ${From}: ${Body}`);\n\n // Check for command prefix before prompt matching\n if (isCommand(Body)) {\n console.log(`[sms-webhook] Processing command: ${Body}`);\n const cmdResult = await processCommand(From, Body);\n\n if (cmdResult.handled) {\n // Send response back if we have one\n if (cmdResult.response) {\n // Don't await - fire and forget the response notification\n sendCommandResponse(cmdResult.response).catch(console.error);\n }\n\n return {\n response: cmdResult.response || 'Command processed',\n action: cmdResult.action,\n queued: false,\n };\n }\n // If not handled, fall through to regular prompt matching\n }\n\n const result = processIncomingResponse(From, Body);\n\n if (!result.matched) {\n if (result.prompt) {\n return {\n response: `Invalid response. Expected: ${result.prompt.options.map((o) => o.key).join(', ')}`,\n };\n }\n // No pending prompt - store as new incoming request for Claude\n storeIncomingRequest(From, Body);\n console.log(\n `[sms-webhook] Stored new request from ${From}: ${Body.substring(0, 50)}...`\n );\n return { response: 'Got it! Your request has been queued.' };\n }\n\n // Store response for Claude hook\n storeLatestResponse(\n result.prompt?.id || 'unknown',\n result.response || Body,\n result.action\n );\n\n // Trigger notification to alert user/Claude\n triggerResponseNotification(result.response || Body);\n\n // Execute action safely if present (no shell injection)\n if (result.action) {\n console.log(`[sms-webhook] Executing action: ${result.action}`);\n\n const actionResult = await executeActionSafe(\n result.action,\n result.response || Body\n );\n\n if (actionResult.success) {\n logActionAllowed('sms-webhook', result.action);\n console.log(\n `[sms-webhook] Action completed: ${(actionResult.output || '').substring(0, 200)}`\n );\n\n return {\n response: `Done! Action executed successfully.`,\n action: result.action,\n queued: false,\n };\n } else {\n logActionBlocked(\n 'sms-webhook',\n result.action,\n actionResult.error || 'unknown'\n );\n console.log(`[sms-webhook] Action failed: ${actionResult.error}`);\n\n // Queue for retry\n queueAction(\n result.prompt?.id || 'unknown',\n result.response || Body,\n result.action\n );\n\n return {\n response: `Action failed, queued for retry: ${(actionResult.error || '').substring(0, 50)}`,\n action: result.action,\n queued: true,\n };\n }\n }\n\n return {\n response: `Received: ${result.response}. Next action will be triggered.`,\n };\n}\n\n// Escape string for AppleScript (prevent injection)\nfunction escapeAppleScript(str: string): string {\n return str\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .substring(0, 200); // Limit length\n}\n\n// Trigger notification when response received\nfunction triggerResponseNotification(response: string): void {\n const safeMessage = escapeAppleScript(`SMS Response: ${response}`);\n\n // macOS notification using execFile (safer than execSync with shell)\n try {\n execFileSync(\n 'osascript',\n [\n '-e',\n `display notification \"${safeMessage}\" with title \"StackMemory\" sound name \"Glass\"`,\n ],\n { stdio: 'ignore', timeout: 5000 }\n );\n } catch {\n // Ignore if not on macOS\n }\n\n // Write signal file for other processes\n try {\n const signalPath = join(homedir(), '.stackmemory', 'sms-signal.txt');\n writeFileSecure(\n signalPath,\n JSON.stringify({\n type: 'sms_response',\n response,\n timestamp: new Date().toISOString(),\n })\n );\n } catch {\n // Ignore\n }\n\n console.log(`\\n*** SMS RESPONSE RECEIVED: \"${response}\" ***`);\n console.log(`*** Run: stackmemory notify run-actions ***\\n`);\n}\n\n// TwiML response helper\nfunction twimlResponse(message: string): string {\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response>\n <Message>${escapeXml(message)}</Message>\n</Response>`;\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n\n// Standalone webhook server\nexport function startWebhookServer(port: number = 3456): void {\n const server = createServer(\n async (req: IncomingMessage, res: ServerResponse) => {\n const url = parseUrl(req.url || '/', true);\n\n // Health check\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'ok' }));\n return;\n }\n\n // SMS webhook endpoint (incoming messages)\n if (\n (url.pathname === '/sms' ||\n url.pathname === '/sms/incoming' ||\n url.pathname === '/webhook') &&\n req.method === 'POST'\n ) {\n const clientIp = req.socket.remoteAddress || 'unknown';\n\n // Log webhook request\n logWebhookRequest(\n 'sms-webhook',\n req.method || 'POST',\n url.pathname || '/sms',\n clientIp\n );\n\n // Rate limiting\n if (!checkRateLimit(clientIp)) {\n logRateLimit('sms-webhook', clientIp);\n res.writeHead(429, {\n 'Content-Type': 'text/xml',\n 'Retry-After': '60',\n });\n res.end(twimlResponse('Too many requests. Please try again later.'));\n return;\n }\n\n // Content-type validation\n const contentType = req.headers['content-type'] || '';\n if (!contentType.includes('application/x-www-form-urlencoded')) {\n logContentTypeInvalid('sms-webhook', contentType, clientIp);\n res.writeHead(400, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Invalid content type'));\n return;\n }\n\n let body = '';\n let bodyTooLarge = false;\n\n req.on('data', (chunk) => {\n body += chunk;\n // Body size limit\n if (body.length > MAX_BODY_SIZE) {\n bodyTooLarge = true;\n logBodyTooLarge('sms-webhook', body.length, clientIp);\n req.destroy();\n }\n });\n\n req.on('end', async () => {\n if (bodyTooLarge) {\n res.writeHead(413, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Request too large'));\n return;\n }\n\n try {\n const payload = parseFormData(\n body\n ) as unknown as TwilioWebhookPayload;\n\n // Verify Twilio signature\n const twilioSignature = req.headers['x-twilio-signature'] as string;\n const webhookUrl = `${req.headers['x-forwarded-proto'] || 'http'}://${req.headers.host}${req.url}`;\n\n if (\n twilioSignature &&\n !verifyTwilioSignature(\n webhookUrl,\n payload as unknown as Record<string, string>,\n twilioSignature\n )\n ) {\n logSignatureInvalid('sms-webhook', clientIp);\n console.error('[sms-webhook] Invalid Twilio signature');\n res.writeHead(401, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Unauthorized'));\n return;\n }\n\n const result = await handleSMSWebhook(payload);\n\n res.writeHead(200, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse(result.response));\n } catch (err) {\n console.error('[sms-webhook] Error:', err);\n res.writeHead(500, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Error processing message'));\n }\n });\n return;\n }\n\n // Status callback endpoint (delivery status updates)\n if (url.pathname === '/sms/status' && req.method === 'POST') {\n let body = '';\n req.on('data', (chunk) => {\n body += chunk;\n });\n\n req.on('end', () => {\n try {\n const payload = parseFormData(body);\n console.log(\n `[sms-webhook] Status update: ${payload['MessageSid']} -> ${payload['MessageStatus']}`\n );\n\n // Store status for tracking\n const statusPath = join(\n homedir(),\n '.stackmemory',\n 'sms-status.json'\n );\n const statuses: Record<string, string> = existsSync(statusPath)\n ? JSON.parse(readFileSync(statusPath, 'utf8'))\n : {};\n statuses[payload['MessageSid']] = payload['MessageStatus'];\n writeFileSecure(statusPath, JSON.stringify(statuses, null, 2));\n\n res.writeHead(200, { 'Content-Type': 'text/plain' });\n res.end('OK');\n } catch (err) {\n console.error('[sms-webhook] Status error:', err);\n res.writeHead(500);\n res.end('Error');\n }\n });\n return;\n }\n\n // Server status endpoint\n if (url.pathname === '/status') {\n const config = loadSMSConfig();\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n enabled: config.enabled,\n pendingPrompts: config.pendingPrompts.length,\n })\n );\n return;\n }\n\n // Get pending incoming request endpoint\n if (url.pathname === '/request' && req.method === 'GET') {\n const request = getIncomingRequest();\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ request }));\n return;\n }\n\n // Mark request as processed endpoint\n if (url.pathname === '/request/ack' && req.method === 'POST') {\n markRequestProcessed();\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ success: true }));\n return;\n }\n\n // Send outgoing notification endpoint\n if (url.pathname === '/send' && req.method === 'POST') {\n let body = '';\n req.on('data', (chunk) => {\n body += chunk;\n if (body.length > MAX_BODY_SIZE) {\n req.destroy();\n }\n });\n\n req.on('end', async () => {\n try {\n const payload = JSON.parse(body);\n const message = payload.message || payload.body || '';\n const title = payload.title || 'Notification';\n const type = payload.type || 'custom';\n\n if (!message) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({ success: false, error: 'Message required' })\n );\n return;\n }\n\n const result = await sendNotification({\n type: type as\n | 'task_complete'\n | 'review_ready'\n | 'error'\n | 'custom',\n title,\n message,\n });\n\n res.writeHead(result.success ? 200 : 500, {\n 'Content-Type': 'application/json',\n });\n res.end(JSON.stringify(result));\n } catch (err) {\n console.error('[sms-webhook] Send error:', err);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n success: false,\n error: err instanceof Error ? err.message : 'Send failed',\n })\n );\n }\n });\n return;\n }\n\n res.writeHead(404);\n res.end('Not found');\n }\n );\n\n server.listen(port, () => {\n console.log(`[sms-webhook] Server listening on port ${port}`);\n console.log(\n `[sms-webhook] Incoming messages: http://localhost:${port}/sms/incoming`\n );\n console.log(\n `[sms-webhook] Status callback: http://localhost:${port}/sms/status`\n );\n console.log(`[sms-webhook] Configure these URLs in Twilio console`);\n\n // Start timed cleanup of expired prompts and old actions\n setInterval(() => {\n try {\n const expiredPrompts = cleanupExpiredPrompts();\n const oldActions = cleanupOldActions();\n if (expiredPrompts > 0 || oldActions > 0) {\n logCleanup('sms-webhook', expiredPrompts, oldActions);\n console.log(\n `[sms-webhook] Cleanup: ${expiredPrompts} expired prompts, ${oldActions} old actions`\n );\n }\n } catch {\n // Ignore cleanup errors\n }\n }, CLEANUP_INTERVAL_MS);\n console.log(\n `[sms-webhook] Cleanup interval: every ${CLEANUP_INTERVAL_MS / 1000}s`\n );\n });\n}\n\n// Express middleware for integration\nexport async function smsWebhookMiddleware(\n req: { body: TwilioWebhookPayload },\n res: { type: (t: string) => void; send: (s: string) => void }\n): Promise<void> {\n const result = await handleSMSWebhook(req.body);\n res.type('text/xml');\n res.send(twimlResponse(result.response));\n}\n\n// CLI entry\nif (process.argv[1]?.endsWith('sms-webhook.js')) {\n const port = parseInt(process.env['SMS_WEBHOOK_PORT'] || '3456', 10);\n startWebhookServer(port);\n}\n"],
5
+ "mappings": ";;;;AAYA,SAAS,oBAAqD;AAC9D,SAAS,SAAS,gBAAgB;AAClC,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iBAAiB,uBAAuB;AACjD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,MAAM,sBAAsB,IAAI,KAAK;AAGrC,MAAM,sBAAsB;AAC5B,MAAM,mBAAmB;AAGzB,MAAM,gBAAgB,KAAK;AAC3B,MAAM,uBAAuB,KAAK;AAClC,MAAM,0BAA0B;AAGhC,MAAM,iBAAiB,oBAAI,IAAkD;AAE7E,SAAS,eAAe,IAAqB;AAC3C,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,SAAS,eAAe,IAAI,EAAE;AAEpC,MAAI,CAAC,UAAU,MAAM,OAAO,WAAW;AACrC,mBAAe,IAAI,IAAI,EAAE,OAAO,GAAG,WAAW,MAAM,qBAAqB,CAAC;AAC1E,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,SAAS,yBAAyB;AAC3C,WAAO;AAAA,EACT;AAEA,SAAO;AACP,SAAO;AACT;AAGA,SAAS,sBACP,KACA,QACA,WACS;AACT,QAAM,YAAY,QAAQ,IAAI,mBAAmB;AACjD,MAAI,CAAC,WAAW;AAEd,UAAM,QACJ,QAAQ,IAAI,UAAU,MAAM,iBAC5B,QAAQ,IAAI,0BAA0B,MAAM;AAE9C,QAAI,OAAO;AACT,cAAQ;AAAA,QACN;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAGA,YAAQ;AAAA,MACN;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAGA,QAAM,aAAa,OAAO,KAAK,MAAM,EAAE,KAAK;AAC5C,MAAI,OAAO;AACX,aAAW,OAAO,YAAY;AAC5B,YAAQ,MAAM,OAAO,GAAG;AAAA,EAC1B;AAGA,QAAM,OAAO,WAAW,QAAQ,SAAS;AACzC,OAAK,OAAO,IAAI;AAChB,QAAM,oBAAoB,KAAK,OAAO,QAAQ;AAE9C,SAAO,cAAc;AACvB;AASA,SAAS,cAAc,MAAsC;AAC3D,QAAM,SAAS,IAAI,gBAAgB,IAAI;AACvC,QAAM,SAAiC,CAAC;AACxC,SAAO,QAAQ,CAAC,OAAO,QAAQ;AAC7B,WAAO,GAAG,IAAI;AAAA,EAChB,CAAC;AACD,SAAO;AACT;AAGA,SAAS,oBACP,UACA,UACA,QACM;AACN,kBAAgB,KAAK,QAAQ,GAAG,cAAc,CAAC;AAC/C,QAAM,eAAe;AAAA,IACnB,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,EACF;AACA;AAAA,IACE;AAAA,IACA,KAAK,UAAU;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,MACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC,CAAC;AAAA,EACH;AACF;AAMA,SAAS,qBAAqB,MAAc,SAAuB;AACjE,kBAAgB,KAAK,QAAQ,GAAG,cAAc,CAAC;AAC/C,QAAM,cAAc;AAAA,IAClB,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,EACF;AACA;AAAA,IACE;AAAA,IACA,KAAK,UAAU;AAAA,MACb;AAAA,MACA;AAAA,MACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,WAAW;AAAA,IACb,CAAC;AAAA,EACH;AACF;AAKO,SAAS,qBAKP;AACP,QAAM,cAAc;AAAA,IAClB,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,EACF;AACA,MAAI,CAAC,WAAW,WAAW,GAAG;AAC5B,WAAO;AAAA,EACT;AACA,MAAI;AACF,UAAM,OAAO,KAAK,MAAM,aAAa,aAAa,OAAO,CAAC;AAC1D,QAAI,KAAK,WAAW;AAClB,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKO,SAAS,uBAA6B;AAC3C,QAAM,cAAc;AAAA,IAClB,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,EACF;AACA,MAAI,CAAC,WAAW,WAAW,GAAG;AAC5B;AAAA,EACF;AACA,MAAI;AACF,UAAM,OAAO,KAAK,MAAM,aAAa,aAAa,OAAO,CAAC;AAC1D,SAAK,YAAY;AACjB,oBAAgB,aAAa,KAAK,UAAU,IAAI,CAAC;AAAA,EACnD,QAAQ;AAAA,EAER;AACF;AAEA,eAAsB,iBAAiB,SAIpC;AACD,QAAM,EAAE,MAAM,KAAK,IAAI;AAGvB,MAAI,QAAQ,KAAK,SAAS,qBAAqB;AAC7C,YAAQ,IAAI,gCAAgC,KAAK,MAAM,QAAQ;AAC/D,WAAO,EAAE,UAAU,yCAAyC;AAAA,EAC9D;AAEA,MAAI,QAAQ,KAAK,SAAS,kBAAkB;AAC1C,YAAQ;AAAA,MACN,wCAAwC,KAAK,MAAM,eAAe,gBAAgB,MAAM,KAAK,UAAU,GAAG,EAAE,CAAC;AAAA,IAC/G;AACA,WAAO,EAAE,UAAU,wBAAwB;AAAA,EAC7C;AAEA,UAAQ,IAAI,+BAA+B,IAAI,KAAK,IAAI,EAAE;AAG1D,MAAI,UAAU,IAAI,GAAG;AACnB,YAAQ,IAAI,qCAAqC,IAAI,EAAE;AACvD,UAAM,YAAY,MAAM,eAAe,MAAM,IAAI;AAEjD,QAAI,UAAU,SAAS;AAErB,UAAI,UAAU,UAAU;AAEtB,4BAAoB,UAAU,QAAQ,EAAE,MAAM,QAAQ,KAAK;AAAA,MAC7D;AAEA,aAAO;AAAA,QACL,UAAU,UAAU,YAAY;AAAA,QAChC,QAAQ,UAAU;AAAA,QAClB,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,EAEF;AAEA,QAAM,SAAS,wBAAwB,MAAM,IAAI;AAEjD,MAAI,CAAC,OAAO,SAAS;AACnB,QAAI,OAAO,QAAQ;AACjB,aAAO;AAAA,QACL,UAAU,+BAA+B,OAAO,OAAO,QAAQ,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;AAAA,MAC7F;AAAA,IACF;AAEA,yBAAqB,MAAM,IAAI;AAC/B,YAAQ;AAAA,MACN,yCAAyC,IAAI,KAAK,KAAK,UAAU,GAAG,EAAE,CAAC;AAAA,IACzE;AACA,WAAO,EAAE,UAAU,wCAAwC;AAAA,EAC7D;AAGA;AAAA,IACE,OAAO,QAAQ,MAAM;AAAA,IACrB,OAAO,YAAY;AAAA,IACnB,OAAO;AAAA,EACT;AAGA,8BAA4B,OAAO,YAAY,IAAI;AAGnD,MAAI,OAAO,QAAQ;AACjB,YAAQ,IAAI,mCAAmC,OAAO,MAAM,EAAE;AAE9D,UAAM,eAAe,MAAM;AAAA,MACzB,OAAO;AAAA,MACP,OAAO,YAAY;AAAA,IACrB;AAEA,QAAI,aAAa,SAAS;AACxB,uBAAiB,eAAe,OAAO,MAAM;AAC7C,cAAQ;AAAA,QACN,oCAAoC,aAAa,UAAU,IAAI,UAAU,GAAG,GAAG,CAAC;AAAA,MAClF;AAEA,aAAO;AAAA,QACL,UAAU;AAAA,QACV,QAAQ,OAAO;AAAA,QACf,QAAQ;AAAA,MACV;AAAA,IACF,OAAO;AACL;AAAA,QACE;AAAA,QACA,OAAO;AAAA,QACP,aAAa,SAAS;AAAA,MACxB;AACA,cAAQ,IAAI,gCAAgC,aAAa,KAAK,EAAE;AAGhE;AAAA,QACE,OAAO,QAAQ,MAAM;AAAA,QACrB,OAAO,YAAY;AAAA,QACnB,OAAO;AAAA,MACT;AAEA,aAAO;AAAA,QACL,UAAU,qCAAqC,aAAa,SAAS,IAAI,UAAU,GAAG,EAAE,CAAC;AAAA,QACzF,QAAQ,OAAO;AAAA,QACf,QAAQ;AAAA,MACV;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,UAAU,aAAa,OAAO,QAAQ;AAAA,EACxC;AACF;AAGA,SAAS,kBAAkB,KAAqB;AAC9C,SAAO,IACJ,QAAQ,OAAO,MAAM,EACrB,QAAQ,MAAM,KAAK,EACnB,QAAQ,OAAO,KAAK,EACpB,QAAQ,OAAO,KAAK,EACpB,UAAU,GAAG,GAAG;AACrB;AAGA,SAAS,4BAA4B,UAAwB;AAC3D,QAAM,cAAc,kBAAkB,iBAAiB,QAAQ,EAAE;AAGjE,MAAI;AACF;AAAA,MACE;AAAA,MACA;AAAA,QACE;AAAA,QACA,yBAAyB,WAAW;AAAA,MACtC;AAAA,MACA,EAAE,OAAO,UAAU,SAAS,IAAK;AAAA,IACnC;AAAA,EACF,QAAQ;AAAA,EAER;AAGA,MAAI;AACF,UAAM,aAAa,KAAK,QAAQ,GAAG,gBAAgB,gBAAgB;AACnE;AAAA,MACE;AAAA,MACA,KAAK,UAAU;AAAA,QACb,MAAM;AAAA,QACN;AAAA,QACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MACpC,CAAC;AAAA,IACH;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,UAAQ,IAAI;AAAA,8BAAiC,QAAQ,OAAO;AAC5D,UAAQ,IAAI;AAAA,CAA+C;AAC7D;AAGA,SAAS,cAAc,SAAyB;AAC9C,SAAO;AAAA;AAAA,aAEI,UAAU,OAAO,CAAC;AAAA;AAE/B;AAEA,SAAS,UAAU,KAAqB;AACtC,SAAO,IACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,QAAQ;AAC3B;AAGO,SAAS,mBAAmB,OAAe,MAAY;AAC5D,QAAM,SAAS;AAAA,IACb,OAAO,KAAsB,QAAwB;AACnD,YAAM,MAAM,SAAS,IAAI,OAAO,KAAK,IAAI;AAGzC,UAAI,IAAI,aAAa,WAAW;AAC9B,YAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,YAAI,IAAI,KAAK,UAAU,EAAE,QAAQ,KAAK,CAAC,CAAC;AACxC;AAAA,MACF;AAGA,WACG,IAAI,aAAa,UAChB,IAAI,aAAa,mBACjB,IAAI,aAAa,eACnB,IAAI,WAAW,QACf;AACA,cAAM,WAAW,IAAI,OAAO,iBAAiB;AAG7C;AAAA,UACE;AAAA,UACA,IAAI,UAAU;AAAA,UACd,IAAI,YAAY;AAAA,UAChB;AAAA,QACF;AAGA,YAAI,CAAC,eAAe,QAAQ,GAAG;AAC7B,uBAAa,eAAe,QAAQ;AACpC,cAAI,UAAU,KAAK;AAAA,YACjB,gBAAgB;AAAA,YAChB,eAAe;AAAA,UACjB,CAAC;AACD,cAAI,IAAI,cAAc,4CAA4C,CAAC;AACnE;AAAA,QACF;AAGA,cAAM,cAAc,IAAI,QAAQ,cAAc,KAAK;AACnD,YAAI,CAAC,YAAY,SAAS,mCAAmC,GAAG;AAC9D,gCAAsB,eAAe,aAAa,QAAQ;AAC1D,cAAI,UAAU,KAAK,EAAE,gBAAgB,WAAW,CAAC;AACjD,cAAI,IAAI,cAAc,sBAAsB,CAAC;AAC7C;AAAA,QACF;AAEA,YAAI,OAAO;AACX,YAAI,eAAe;AAEnB,YAAI,GAAG,QAAQ,CAAC,UAAU;AACxB,kBAAQ;AAER,cAAI,KAAK,SAAS,eAAe;AAC/B,2BAAe;AACf,4BAAgB,eAAe,KAAK,QAAQ,QAAQ;AACpD,gBAAI,QAAQ;AAAA,UACd;AAAA,QACF,CAAC;AAED,YAAI,GAAG,OAAO,YAAY;AACxB,cAAI,cAAc;AAChB,gBAAI,UAAU,KAAK,EAAE,gBAAgB,WAAW,CAAC;AACjD,gBAAI,IAAI,cAAc,mBAAmB,CAAC;AAC1C;AAAA,UACF;AAEA,cAAI;AACF,kBAAM,UAAU;AAAA,cACd;AAAA,YACF;AAGA,kBAAM,kBAAkB,IAAI,QAAQ,oBAAoB;AACxD,kBAAM,aAAa,GAAG,IAAI,QAAQ,mBAAmB,KAAK,MAAM,MAAM,IAAI,QAAQ,IAAI,GAAG,IAAI,GAAG;AAEhG,gBACE,mBACA,CAAC;AAAA,cACC;AAAA,cACA;AAAA,cACA;AAAA,YACF,GACA;AACA,kCAAoB,eAAe,QAAQ;AAC3C,sBAAQ,MAAM,wCAAwC;AACtD,kBAAI,UAAU,KAAK,EAAE,gBAAgB,WAAW,CAAC;AACjD,kBAAI,IAAI,cAAc,cAAc,CAAC;AACrC;AAAA,YACF;AAEA,kBAAM,SAAS,MAAM,iBAAiB,OAAO;AAE7C,gBAAI,UAAU,KAAK,EAAE,gBAAgB,WAAW,CAAC;AACjD,gBAAI,IAAI,cAAc,OAAO,QAAQ,CAAC;AAAA,UACxC,SAAS,KAAK;AACZ,oBAAQ,MAAM,wBAAwB,GAAG;AACzC,gBAAI,UAAU,KAAK,EAAE,gBAAgB,WAAW,CAAC;AACjD,gBAAI,IAAI,cAAc,0BAA0B,CAAC;AAAA,UACnD;AAAA,QACF,CAAC;AACD;AAAA,MACF;AAGA,UAAI,IAAI,aAAa,iBAAiB,IAAI,WAAW,QAAQ;AAC3D,YAAI,OAAO;AACX,YAAI,GAAG,QAAQ,CAAC,UAAU;AACxB,kBAAQ;AAAA,QACV,CAAC;AAED,YAAI,GAAG,OAAO,MAAM;AAClB,cAAI;AACF,kBAAM,UAAU,cAAc,IAAI;AAClC,oBAAQ;AAAA,cACN,gCAAgC,QAAQ,YAAY,CAAC,OAAO,QAAQ,eAAe,CAAC;AAAA,YACtF;AAGA,kBAAM,aAAa;AAAA,cACjB,QAAQ;AAAA,cACR;AAAA,cACA;AAAA,YACF;AACA,kBAAM,WAAmC,WAAW,UAAU,IAC1D,KAAK,MAAM,aAAa,YAAY,MAAM,CAAC,IAC3C,CAAC;AACL,qBAAS,QAAQ,YAAY,CAAC,IAAI,QAAQ,eAAe;AACzD,4BAAgB,YAAY,KAAK,UAAU,UAAU,MAAM,CAAC,CAAC;AAE7D,gBAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;AACnD,gBAAI,IAAI,IAAI;AAAA,UACd,SAAS,KAAK;AACZ,oBAAQ,MAAM,+BAA+B,GAAG;AAChD,gBAAI,UAAU,GAAG;AACjB,gBAAI,IAAI,OAAO;AAAA,UACjB;AAAA,QACF,CAAC;AACD;AAAA,MACF;AAGA,UAAI,IAAI,aAAa,WAAW;AAC9B,cAAM,SAAS,cAAc;AAC7B,YAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,YAAI;AAAA,UACF,KAAK,UAAU;AAAA,YACb,SAAS,OAAO;AAAA,YAChB,gBAAgB,OAAO,eAAe;AAAA,UACxC,CAAC;AAAA,QACH;AACA;AAAA,MACF;AAGA,UAAI,IAAI,aAAa,cAAc,IAAI,WAAW,OAAO;AACvD,cAAM,UAAU,mBAAmB;AACnC,YAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,YAAI,IAAI,KAAK,UAAU,EAAE,QAAQ,CAAC,CAAC;AACnC;AAAA,MACF;AAGA,UAAI,IAAI,aAAa,kBAAkB,IAAI,WAAW,QAAQ;AAC5D,6BAAqB;AACrB,YAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,YAAI,IAAI,KAAK,UAAU,EAAE,SAAS,KAAK,CAAC,CAAC;AACzC;AAAA,MACF;AAGA,UAAI,IAAI,aAAa,WAAW,IAAI,WAAW,QAAQ;AACrD,YAAI,OAAO;AACX,YAAI,GAAG,QAAQ,CAAC,UAAU;AACxB,kBAAQ;AACR,cAAI,KAAK,SAAS,eAAe;AAC/B,gBAAI,QAAQ;AAAA,UACd;AAAA,QACF,CAAC;AAED,YAAI,GAAG,OAAO,YAAY;AACxB,cAAI;AACF,kBAAM,UAAU,KAAK,MAAM,IAAI;AAC/B,kBAAM,UAAU,QAAQ,WAAW,QAAQ,QAAQ;AACnD,kBAAM,QAAQ,QAAQ,SAAS;AAC/B,kBAAM,OAAO,QAAQ,QAAQ;AAE7B,gBAAI,CAAC,SAAS;AACZ,kBAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,kBAAI;AAAA,gBACF,KAAK,UAAU,EAAE,SAAS,OAAO,OAAO,mBAAmB,CAAC;AAAA,cAC9D;AACA;AAAA,YACF;AAEA,kBAAM,SAAS,MAAM,iBAAiB;AAAA,cACpC;AAAA,cAKA;AAAA,cACA;AAAA,YACF,CAAC;AAED,gBAAI,UAAU,OAAO,UAAU,MAAM,KAAK;AAAA,cACxC,gBAAgB;AAAA,YAClB,CAAC;AACD,gBAAI,IAAI,KAAK,UAAU,MAAM,CAAC;AAAA,UAChC,SAAS,KAAK;AACZ,oBAAQ,MAAM,6BAA6B,GAAG;AAC9C,gBAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,gBAAI;AAAA,cACF,KAAK,UAAU;AAAA,gBACb,SAAS;AAAA,gBACT,OAAO,eAAe,QAAQ,IAAI,UAAU;AAAA,cAC9C,CAAC;AAAA,YACH;AAAA,UACF;AAAA,QACF,CAAC;AACD;AAAA,MACF;AAEA,UAAI,UAAU,GAAG;AACjB,UAAI,IAAI,WAAW;AAAA,IACrB;AAAA,EACF;AAEA,SAAO,OAAO,MAAM,MAAM;AACxB,YAAQ,IAAI,0CAA0C,IAAI,EAAE;AAC5D,YAAQ;AAAA,MACN,qDAAqD,IAAI;AAAA,IAC3D;AACA,YAAQ;AAAA,MACN,qDAAqD,IAAI;AAAA,IAC3D;AACA,YAAQ,IAAI,sDAAsD;AAGlE,gBAAY,MAAM;AAChB,UAAI;AACF,cAAM,iBAAiB,sBAAsB;AAC7C,cAAM,aAAa,kBAAkB;AACrC,YAAI,iBAAiB,KAAK,aAAa,GAAG;AACxC,qBAAW,eAAe,gBAAgB,UAAU;AACpD,kBAAQ;AAAA,YACN,0BAA0B,cAAc,qBAAqB,UAAU;AAAA,UACzE;AAAA,QACF;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF,GAAG,mBAAmB;AACtB,YAAQ;AAAA,MACN,yCAAyC,sBAAsB,GAAI;AAAA,IACrE;AAAA,EACF,CAAC;AACH;AAGA,eAAsB,qBACpB,KACA,KACe;AACf,QAAM,SAAS,MAAM,iBAAiB,IAAI,IAAI;AAC9C,MAAI,KAAK,UAAU;AACnB,MAAI,KAAK,cAAc,OAAO,QAAQ,CAAC;AACzC;AAGA,IAAI,QAAQ,KAAK,CAAC,GAAG,SAAS,gBAAgB,GAAG;AAC/C,QAAM,OAAO,SAAS,QAAQ,IAAI,kBAAkB,KAAK,QAAQ,EAAE;AACnE,qBAAmB,IAAI;AACzB;",
6
6
  "names": []
7
7
  }
@@ -16,22 +16,48 @@ import {
16
16
  loadSyncOptions
17
17
  } from "./whatsapp-sync.js";
18
18
  import { sendNotification } from "./sms-notify.js";
19
- const REGEX_TIMEOUT_MS = 100;
20
19
  const MAX_REGEX_INPUT_LENGTH = 200;
21
- function safeRegexTest(pattern, input) {
22
- const safeInput = input.slice(0, MAX_REGEX_INPUT_LENGTH);
23
- try {
24
- const regex = new RegExp(pattern);
25
- const startTime = Date.now();
26
- const result = regex.test(safeInput);
27
- const elapsed = Date.now() - startTime;
28
- if (elapsed > REGEX_TIMEOUT_MS) {
20
+ const DANGEROUS_PATTERNS = [
21
+ /(\+|\*|\?)\s*(\+|\*|\?)/,
22
+ // Nested quantifiers like .+* or .*+
23
+ /\(\?[^)]*\)\s*[+*]/,
24
+ // Quantified groups with + or *
25
+ /\[[^\]]*\]\s*[+*]\s*[+*]/,
26
+ // Character classes with nested quantifiers
27
+ /(\.\*|\.\+)\s*(\.\*|\.\+)/,
28
+ // Overlapping .* or .+
29
+ /\(\[[^\]]+\]\+\)\+/,
30
+ // Nested + with character class
31
+ /\(.*\+\).*\+/
32
+ // Nested + quantifiers
33
+ ];
34
+ function isPatternSafe(pattern) {
35
+ for (const dangerous of DANGEROUS_PATTERNS) {
36
+ if (dangerous.test(pattern)) {
29
37
  console.warn(
30
- `[whatsapp-commands] Slow regex detected: ${pattern} took ${elapsed}ms`
38
+ `[whatsapp-commands] Potentially dangerous regex pattern blocked: ${pattern}`
31
39
  );
32
40
  return false;
33
41
  }
34
- return result;
42
+ }
43
+ const quantifierCount = (pattern.match(/[+*?]/g) || []).length;
44
+ const groupCount = (pattern.match(/\(/g) || []).length;
45
+ if (quantifierCount > 5 || groupCount > 3) {
46
+ console.warn(
47
+ `[whatsapp-commands] Complex regex pattern blocked: ${pattern} (${quantifierCount} quantifiers, ${groupCount} groups)`
48
+ );
49
+ return false;
50
+ }
51
+ return true;
52
+ }
53
+ function safeRegexTest(pattern, input) {
54
+ if (!isPatternSafe(pattern)) {
55
+ return false;
56
+ }
57
+ const safeInput = input.slice(0, MAX_REGEX_INPUT_LENGTH);
58
+ try {
59
+ const regex = new RegExp(pattern);
60
+ return regex.test(safeInput);
35
61
  } catch {
36
62
  console.warn(`[whatsapp-commands] Invalid regex pattern: ${pattern}`);
37
63
  return false;
@@ -72,104 +98,41 @@ function getActiveRemoteSessions() {
72
98
  }
73
99
  const DEFAULT_COMMANDS = [
74
100
  {
75
- name: "status",
76
- description: "Get current task/frame status",
101
+ name: "help",
102
+ description: "List available commands",
77
103
  enabled: true
78
- // No action - handled specially in-process
79
104
  },
80
105
  {
81
- name: "tasks",
82
- description: "List active tasks",
106
+ name: "status",
107
+ description: "Get current task/frame status",
83
108
  enabled: true
84
- // No action - handled specially in-process
85
109
  },
86
110
  {
87
- name: "context",
88
- description: "Get latest context digest",
111
+ name: "sessions",
112
+ description: "List active remote sessions with URLs",
89
113
  enabled: true
90
- // No action - handled specially
91
114
  },
92
115
  {
93
- name: "approve",
94
- description: "Approve a PR (requires PR number)",
95
- enabled: true,
96
- requiresArg: true,
97
- argPattern: "^\\d+$"
98
- // PR number must be numeric
99
- },
100
- {
101
- name: "merge",
102
- description: "Merge a PR (requires PR number)",
116
+ name: "remote",
117
+ description: "Launch remote Claude session (requires task prompt)",
103
118
  enabled: true,
104
- requiresArg: true,
105
- argPattern: "^\\d+$"
119
+ requiresArg: true
106
120
  },
121
+ // Disabled by default - can be enabled in config if needed
107
122
  {
108
- name: "help",
109
- description: "List available commands",
110
- enabled: true
111
- // No action - handled specially
123
+ name: "context",
124
+ description: "Get latest context digest",
125
+ enabled: false
112
126
  },
113
127
  {
114
128
  name: "sync",
115
129
  description: "Push current context to WhatsApp",
116
- enabled: true
117
- // No action - handled specially
118
- },
119
- {
120
- name: "build",
121
- description: "Run npm build",
122
- enabled: true,
123
- action: "npm run build"
124
- },
125
- {
126
- name: "test",
127
- description: "Run tests",
128
- enabled: true,
129
- action: "npm run test:run"
130
+ enabled: false
130
131
  },
131
132
  {
132
- name: "lint",
133
- description: "Run linter",
134
- enabled: true,
135
- action: "npm run lint"
136
- },
137
- {
138
- name: "log",
139
- description: "Show recent git commits",
140
- enabled: true,
141
- action: "git log --oneline -5"
142
- },
143
- {
144
- name: "diff",
145
- description: "Show git diff summary",
146
- enabled: true,
147
- action: "git diff --stat"
148
- },
149
- {
150
- name: "pr",
151
- description: "List open PRs",
152
- enabled: true,
153
- action: "gh pr list"
154
- },
155
- {
156
- name: "branch",
157
- description: "Show current branch",
158
- enabled: true,
159
- action: "git branch --show-current"
160
- },
161
- {
162
- name: "remote",
163
- description: "Launch remote Claude session (requires task prompt)",
164
- enabled: true,
165
- requiresArg: true
166
- // No action - handled specially to capture session URL
167
- },
168
- {
169
- name: "sessions",
170
- description: "List active remote sessions",
171
- enabled: true
172
- // No action - handled specially
133
+ name: "tasks",
134
+ description: "List active tasks",
135
+ enabled: false
173
136
  }
174
137
  ];
175
138
  const DEFAULT_CONFIG = {