Skip to content

Commit 3413169

Browse files
feat: Add WhatsApp/SMS dual-channel support for notifications
- Add channel preference (whatsapp default, cheaper for conversations) - Add separate env vars for WhatsApp and SMS numbers - Add 'channel' CLI command to switch between WhatsApp/SMS - Update status command to show channel info - Update test command to show which channel was used - Update help text with all env var options
1 parent a221860 commit 3413169

4 files changed

Lines changed: 260 additions & 46 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stackmemoryai/stackmemory",
3-
"version": "0.5.9",
3+
"version": "0.5.10",
44
"description": "Lossless memory runtime for AI coding tools - organizes context as a call stack instead of linear chat logs, with team collaboration and infinite retention",
55
"engines": {
66
"node": ">=20.0.0",

src/cli/commands/sms-notify.ts

Lines changed: 130 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import { join } from 'path';
99
import {
1010
loadSMSConfig,
1111
saveSMSConfig,
12+
sendNotification,
1213
sendSMSNotification,
1314
notifyReviewReady,
1415
notifyWithYesNo,
1516
notifyTaskComplete,
1617
cleanupExpiredPrompts,
18+
type MessageChannel,
1719
} from '../../hooks/sms-notify.js';
1820
import {
1921
loadActionQueue,
@@ -34,17 +36,32 @@ export function createSMSNotifyCommand(): Command {
3436
`
3537
Setup (optional):
3638
1. Create Twilio account at https://twilio.com
37-
2. Get Account SID, Auth Token, and phone number
39+
2. Get Account SID, Auth Token, and phone numbers
3840
3. Set environment variables:
3941
export TWILIO_ACCOUNT_SID=your_sid
4042
export TWILIO_AUTH_TOKEN=your_token
43+
44+
For WhatsApp (recommended - cheaper for conversations):
45+
export TWILIO_WHATSAPP_FROM=+1234567890
46+
export TWILIO_WHATSAPP_TO=+1234567890
47+
export TWILIO_CHANNEL=whatsapp
48+
49+
For SMS:
50+
export TWILIO_SMS_FROM=+1234567890
51+
export TWILIO_SMS_TO=+1234567890
52+
export TWILIO_CHANNEL=sms
53+
54+
Legacy (works for both, defaults to WhatsApp):
4155
export TWILIO_FROM_NUMBER=+1234567890
4256
export TWILIO_TO_NUMBER=+1234567890
57+
4358
4. Enable: stackmemory notify enable
4459
4560
Examples:
4661
stackmemory notify status Check configuration
4762
stackmemory notify enable Enable notifications
63+
stackmemory notify channel whatsapp Switch to WhatsApp
64+
stackmemory notify channel sms Switch to SMS
4865
stackmemory notify test Send test message
4966
stackmemory notify send "PR ready" Send custom message
5067
stackmemory notify review "PR #123" Send review notification with options
@@ -58,28 +75,57 @@ Examples:
5875
.action(() => {
5976
const config = loadSMSConfig();
6077

61-
console.log(chalk.blue('SMS Notification Status:'));
78+
console.log(chalk.blue('Notification Status:'));
6279
console.log();
6380

64-
// Check if configured
65-
const hasCreds =
66-
config.accountSid &&
67-
config.authToken &&
68-
config.fromNumber &&
81+
// Check credentials
82+
const hasCreds = config.accountSid && config.authToken;
83+
84+
// Check channel-specific numbers
85+
const channel = config.channel || 'whatsapp';
86+
const hasWhatsApp =
87+
config.whatsappFromNumber ||
88+
config.fromNumber ||
89+
config.whatsappToNumber ||
90+
config.toNumber;
91+
const hasSMS =
92+
config.smsFromNumber ||
93+
config.fromNumber ||
94+
config.smsToNumber ||
6995
config.toNumber;
96+
const hasNumbers = channel === 'whatsapp' ? hasWhatsApp : hasSMS;
7097

7198
console.log(
7299
` ${chalk.gray('Enabled:')} ${config.enabled ? chalk.green('yes') : chalk.red('no')}`
73100
);
74101
console.log(
75-
` ${chalk.gray('Configured:')} ${hasCreds ? chalk.green('yes') : chalk.yellow('no (set env vars)')}`
102+
` ${chalk.gray('Channel:')} ${channel === 'whatsapp' ? chalk.cyan('WhatsApp') : chalk.blue('SMS')}`
103+
);
104+
console.log(
105+
` ${chalk.gray('Configured:')} ${hasCreds && hasNumbers ? chalk.green('yes') : chalk.yellow('no (set env vars)')}`
76106
);
77107

78-
if (config.fromNumber) {
79-
console.log(` ${chalk.gray('From:')} ${maskPhone(config.fromNumber)}`);
80-
}
81-
if (config.toNumber) {
82-
console.log(` ${chalk.gray('To:')} ${maskPhone(config.toNumber)}`);
108+
// Show channel-specific numbers
109+
console.log();
110+
console.log(chalk.blue('Numbers:'));
111+
if (channel === 'whatsapp') {
112+
const from = config.whatsappFromNumber || config.fromNumber;
113+
const to = config.whatsappToNumber || config.toNumber;
114+
if (from) {
115+
console.log(` ${chalk.gray('WhatsApp From:')} ${maskPhone(from)}`);
116+
}
117+
if (to) {
118+
console.log(` ${chalk.gray('WhatsApp To:')} ${maskPhone(to)}`);
119+
}
120+
} else {
121+
const from = config.smsFromNumber || config.fromNumber;
122+
const to = config.smsToNumber || config.toNumber;
123+
if (from) {
124+
console.log(` ${chalk.gray('SMS From:')} ${maskPhone(from)}`);
125+
}
126+
if (to) {
127+
console.log(` ${chalk.gray('SMS To:')} ${maskPhone(to)}`);
128+
}
83129
}
84130

85131
console.log();
@@ -111,15 +157,21 @@ Examples:
111157
` ${chalk.gray('Response Timeout:')} ${config.responseTimeout}s`
112158
);
113159

114-
if (!hasCreds) {
160+
if (!hasCreds || !hasNumbers) {
115161
console.log();
116162
console.log(
117163
chalk.yellow('To configure, set these environment variables:')
118164
);
119165
console.log(chalk.gray(' export TWILIO_ACCOUNT_SID=your_sid'));
120166
console.log(chalk.gray(' export TWILIO_AUTH_TOKEN=your_token'));
121-
console.log(chalk.gray(' export TWILIO_FROM_NUMBER=+1234567890'));
122-
console.log(chalk.gray(' export TWILIO_TO_NUMBER=+1234567890'));
167+
console.log();
168+
console.log(chalk.gray(' For WhatsApp (recommended):'));
169+
console.log(chalk.gray(' export TWILIO_WHATSAPP_FROM=+1234567890'));
170+
console.log(chalk.gray(' export TWILIO_WHATSAPP_TO=+1234567890'));
171+
console.log();
172+
console.log(chalk.gray(' For SMS:'));
173+
console.log(chalk.gray(' export TWILIO_SMS_FROM=+1234567890'));
174+
console.log(chalk.gray(' export TWILIO_SMS_TO=+1234567890'));
123175
}
124176
});
125177

@@ -156,20 +208,74 @@ Examples:
156208
console.log(chalk.yellow('SMS notifications disabled'));
157209
});
158210

211+
cmd
212+
.command('channel <type>')
213+
.description('Set notification channel (whatsapp|sms)')
214+
.action((type: string) => {
215+
const validChannels: MessageChannel[] = ['whatsapp', 'sms'];
216+
const channel = type.toLowerCase() as MessageChannel;
217+
218+
if (!validChannels.includes(channel)) {
219+
console.log(
220+
chalk.red(`Invalid channel. Use: ${validChannels.join(', ')}`)
221+
);
222+
return;
223+
}
224+
225+
const config = loadSMSConfig();
226+
config.channel = channel;
227+
saveSMSConfig(config);
228+
229+
const label = channel === 'whatsapp' ? 'WhatsApp' : 'SMS';
230+
console.log(chalk.green(`Notification channel set to ${label}`));
231+
232+
// Show relevant env vars
233+
if (channel === 'whatsapp') {
234+
const hasNumbers = config.whatsappFromNumber || config.fromNumber;
235+
if (!hasNumbers) {
236+
console.log(
237+
chalk.yellow('Set TWILIO_WHATSAPP_FROM and TWILIO_WHATSAPP_TO')
238+
);
239+
}
240+
} else {
241+
const hasNumbers = config.smsFromNumber || config.fromNumber;
242+
if (!hasNumbers) {
243+
console.log(chalk.yellow('Set TWILIO_SMS_FROM and TWILIO_SMS_TO'));
244+
}
245+
}
246+
});
247+
159248
cmd
160249
.command('test')
161250
.description('Send a test notification')
162-
.action(async () => {
163-
console.log(chalk.blue('Sending test notification...'));
251+
.option('--sms', 'Force SMS channel')
252+
.option('--whatsapp', 'Force WhatsApp channel')
253+
.action(async (options: { sms?: boolean; whatsapp?: boolean }) => {
254+
const config = loadSMSConfig();
255+
const channelOverride: MessageChannel | undefined = options.sms
256+
? 'sms'
257+
: options.whatsapp
258+
? 'whatsapp'
259+
: undefined;
260+
const channelLabel =
261+
channelOverride || config.channel === 'whatsapp' ? 'WhatsApp' : 'SMS';
164262

165-
const result = await sendSMSNotification({
166-
type: 'custom',
167-
title: 'StackMemory Test',
168-
message: 'This is a test notification from StackMemory.',
169-
});
263+
console.log(
264+
chalk.blue(`Sending test notification via ${channelLabel}...`)
265+
);
266+
267+
const result = await sendNotification(
268+
{
269+
type: 'custom',
270+
title: 'StackMemory Test',
271+
message: 'This is a test notification from StackMemory.',
272+
},
273+
channelOverride
274+
);
170275

171276
if (result.success) {
172-
console.log(chalk.green('Test message sent successfully!'));
277+
const usedChannel = result.channel === 'whatsapp' ? 'WhatsApp' : 'SMS';
278+
console.log(chalk.green(`Test message sent via ${usedChannel}!`));
173279
} else {
174280
console.log(chalk.red(`Failed: ${result.error}`));
175281
}

0 commit comments

Comments
 (0)