Skip to content

Commit cdbdd98

Browse files
feat(cli): add --whatsapp flag to claude-sm for automatic notifications
- Add --whatsapp flag to auto-start webhook server + ngrok tunnel - Enable WhatsApp notifications when session ends - Add config commands: whatsapp-on, whatsapp-off - Add config set key: whatsapp - Add defaultWhatsApp to persistent configuration
1 parent e54c6b3 commit cdbdd98

1 file changed

Lines changed: 124 additions & 1 deletion

File tree

src/cli/claude-sm.ts

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ interface ClaudeSMConfig {
3232
defaultTracing: boolean;
3333
defaultRemote: boolean;
3434
defaultNotifyOnDone: boolean;
35+
defaultWhatsApp: boolean;
3536
}
3637

3738
interface 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

6366
function 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)
808926
program
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

Comments
 (0)