@@ -32,6 +32,7 @@ interface ClaudeSMConfig {
3232 defaultTracing: boolean ;
3333 defaultRemote: boolean ;
3434 defaultNotifyOnDone: boolean ;
35+ defaultWhatsApp: boolean ;
3536}
3637
3738interface ClaudeConfig {
@@ -42,6 +43,7 @@ interface ClaudeConfig {
4243 useWorktree: boolean ;
4344 useRemote: boolean ;
4445 notifyOnDone: boolean ;
46+ useWhatsApp: boolean ;
4547 contextEnabled: boolean ;
4648 branch ? : string ;
4749 task ? : string ;
@@ -58,6 +60,7 @@ const DEFAULT_SM_CONFIG: ClaudeSMConfig = {
5860 defaultTracing : true ,
5961 defaultRemote : false ,
6062 defaultNotifyOnDone : true ,
63+ defaultWhatsApp : false ,
6164} ;
6265
6366function getConfigPath ( ) : string {
@@ -104,6 +107,7 @@ class ClaudeSM {
104107 useWorktree : this . smConfig . defaultWorktree ,
105108 useRemote : this . smConfig . defaultRemote ,
106109 notifyOnDone : this . smConfig . defaultNotifyOnDone ,
110+ useWhatsApp : this . smConfig . defaultWhatsApp ,
107111 contextEnabled : true ,
108112 tracingEnabled : this . smConfig . defaultTracing ,
109113 verboseTracing : false ,
@@ -340,6 +344,79 @@ class ClaudeSM {
340344 }
341345 }
342346
347+ private async startWhatsAppServices ( ) : Promise < void > {
348+ const WEBHOOK_PORT = 3456 ;
349+
350+ console . log ( chalk . cyan ( 'Starting WhatsApp services...' ) ) ;
351+
352+ // Check if webhook is already running
353+ const webhookRunning = await fetch (
354+ `http://localhost:${ WEBHOOK_PORT } /health`
355+ )
356+ . then ( ( r ) => r . ok )
357+ . catch ( ( ) => false ) ;
358+
359+ if ( ! webhookRunning ) {
360+ // Start webhook in background
361+ const webhookPath = path . join ( __dirname , '../hooks/sms-webhook.js' ) ;
362+ const webhookProcess = spawn ( 'node' , [ webhookPath ] , {
363+ detached : true ,
364+ stdio : 'ignore' ,
365+ env : { ...process . env , SMS_WEBHOOK_PORT : String ( WEBHOOK_PORT ) } ,
366+ } ) ;
367+ webhookProcess . unref ( ) ;
368+ console . log (
369+ chalk . gray ( ` Webhook server starting on port ${ WEBHOOK_PORT } ` )
370+ ) ;
371+ } else {
372+ console . log (
373+ chalk . gray ( ` Webhook already running on port ${ WEBHOOK_PORT } ` )
374+ ) ;
375+ }
376+
377+ // Check if ngrok is running
378+ const ngrokRunning = await fetch ( 'http://localhost:4040/api/tunnels' )
379+ . then ( ( r ) => r . ok )
380+ . catch ( ( ) => false ) ;
381+
382+ if ( ! ngrokRunning ) {
383+ // Start ngrok in background
384+ const ngrokProcess = spawn ( 'ngrok' , [ 'http' , String ( WEBHOOK_PORT ) ] , {
385+ detached : true ,
386+ stdio : 'ignore' ,
387+ } ) ;
388+ ngrokProcess . unref ( ) ;
389+ console . log ( chalk . gray ( ' ngrok tunnel starting...' ) ) ;
390+
391+ // Wait for ngrok to start and get URL
392+ await new Promise ( ( resolve ) => setTimeout ( resolve , 3000 ) ) ;
393+ }
394+
395+ // Get and display ngrok URL
396+ try {
397+ const tunnels = await fetch ( 'http://localhost:4040/api/tunnels' ) . then (
398+ ( r ) => r . json ( ) as Promise < { tunnels : Array < { public_url : string } > } >
399+ ) ;
400+ const publicUrl = tunnels ?. tunnels ?. [ 0 ] ?. public_url ;
401+ if ( publicUrl ) {
402+ // Save URL for other processes
403+ const configDir = path . join ( os . homedir ( ) , '.stackmemory' ) ;
404+ const configPath = path . join ( configDir , 'ngrok-url.txt' ) ;
405+ if ( ! fs . existsSync ( configDir ) ) {
406+ fs . mkdirSync ( configDir , { recursive : true } ) ;
407+ }
408+ fs . writeFileSync ( configPath , publicUrl ) ;
409+ console . log (
410+ chalk . green ( ` WhatsApp webhook: ${ publicUrl } /sms/incoming` )
411+ ) ;
412+ }
413+ } catch {
414+ console . log (
415+ chalk . yellow ( ' Waiting for ngrok... URL will be available shortly' )
416+ ) ;
417+ }
418+ }
419+
343420 private async sendDoneNotification ( exitCode : number | null ) : Promise < void > {
344421 try {
345422 const context : SessionContext = {
@@ -423,6 +500,13 @@ class ClaudeSM {
423500 case '--no-notify-done' :
424501 this . config . notifyOnDone = false ;
425502 break ;
503+ case '--whatsapp' :
504+ this . config . useWhatsApp = true ;
505+ this . config . notifyOnDone = true ; // Auto-enable notifications
506+ break ;
507+ case '--no-whatsapp' :
508+ this . config . useWhatsApp = false ;
509+ break ;
426510 case '--sandbox' :
427511 case '-s' :
428512 this . config . useSandbox = true ;
@@ -577,6 +661,14 @@ class ClaudeSM {
577661 }
578662 }
579663
664+ // Start WhatsApp services if enabled
665+ if ( this . config . useWhatsApp ) {
666+ console . log (
667+ chalk . cyan ( '📱 WhatsApp mode: notifications + webhook enabled' )
668+ ) ;
669+ await this . startWhatsAppServices ( ) ;
670+ }
671+
580672 console . log ( ) ;
581673 console . log ( chalk . gray ( 'Starting Claude...' ) ) ;
582674 console . log ( chalk . gray ( '─' . repeat ( 42 ) ) ) ;
@@ -708,6 +800,9 @@ configCmd
708800 console . log (
709801 ` defaultNotifyOnDone: ${ config . defaultNotifyOnDone ? chalk . green ( 'true' ) : chalk . gray ( 'false' ) } `
710802 ) ;
803+ console . log (
804+ ` defaultWhatsApp: ${ config . defaultWhatsApp ? chalk . green ( 'true' ) : chalk . gray ( 'false' ) } `
805+ ) ;
711806 console . log ( chalk . gray ( `\nConfig: ${ getConfigPath ( ) } ` ) ) ;
712807 } ) ;
713808
@@ -726,14 +821,15 @@ configCmd
726821 remote : 'defaultRemote' ,
727822 'notify-done' : 'defaultNotifyOnDone' ,
728823 notifyondone : 'defaultNotifyOnDone' ,
824+ whatsapp : 'defaultWhatsApp' ,
729825 } ;
730826
731827 const configKey = keyMap [ key ] ;
732828 if ( ! configKey ) {
733829 console . log ( chalk . red ( `Unknown key: ${ key } ` ) ) ;
734830 console . log (
735831 chalk . gray (
736- 'Valid keys: worktree, sandbox, chrome, tracing, remote, notify-done'
832+ 'Valid keys: worktree, sandbox, chrome, tracing, remote, notify-done, whatsapp '
737833 )
738834 ) ;
739835 process . exit ( 1 ) ;
@@ -804,6 +900,28 @@ configCmd
804900 console . log ( chalk . green ( 'Notify-on-done disabled by default' ) ) ;
805901 } ) ;
806902
903+ configCmd
904+ . command ( 'whatsapp-on' )
905+ . description ( 'Enable WhatsApp mode by default (auto-starts webhook + ngrok)' )
906+ . action ( ( ) => {
907+ const config = loadSMConfig ( ) ;
908+ config . defaultWhatsApp = true ;
909+ config . defaultNotifyOnDone = true ; // Also enable notifications
910+ saveSMConfig ( config ) ;
911+ console . log ( chalk . green ( 'WhatsApp mode enabled by default' ) ) ;
912+ console . log ( chalk . gray ( 'Sessions will auto-start webhook and ngrok' ) ) ;
913+ } ) ;
914+
915+ configCmd
916+ . command ( 'whatsapp-off' )
917+ . description ( 'Disable WhatsApp mode by default' )
918+ . action ( ( ) => {
919+ const config = loadSMConfig ( ) ;
920+ config . defaultWhatsApp = false ;
921+ saveSMConfig ( config ) ;
922+ console . log ( chalk . green ( 'WhatsApp mode disabled by default' ) ) ;
923+ } ) ;
924+
807925// Main command (default action when no subcommand)
808926program
809927 . option ( '-w, --worktree' , 'Create isolated worktree for this instance' )
@@ -812,6 +930,11 @@ program
812930 . option ( '--no-remote' , 'Disable remote mode (override default)' )
813931 . option ( '-n, --notify-done' , 'Send WhatsApp notification when session ends' )
814932 . option ( '--no-notify-done' , 'Disable notification when session ends' )
933+ . option (
934+ '--whatsapp' ,
935+ 'Enable WhatsApp mode (auto-start webhook + ngrok + notifications)'
936+ )
937+ . option ( '--no-whatsapp' , 'Disable WhatsApp mode (override default)' )
815938 . option ( '-s, --sandbox' , 'Enable sandbox mode (file/network restrictions)' )
816939 . option ( '-c, --chrome' , 'Enable Chrome automation' )
817940 . option ( '-a, --auto' , 'Automatically detect and apply best settings' )
0 commit comments