Skip to content

Commit 4163ee4

Browse files
feat: Add settings command and .env loading for notifications
- stackmemory settings: View all settings and missing config - stackmemory settings notifications: Interactive setup wizard - stackmemory settings env: Show required environment variables - Load credentials from .env files (project, home, ~/.stackmemory) - getMissingConfig() to check what's needed
1 parent bb31918 commit 4163ee4

3 files changed

Lines changed: 448 additions & 0 deletions

File tree

src/cli/commands/settings.ts

Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
/**
2+
* CLI command for viewing and configuring StackMemory settings
3+
*/
4+
5+
import { Command } from 'commander';
6+
import chalk from 'chalk';
7+
import inquirer from 'inquirer';
8+
import { existsSync, writeFileSync, mkdirSync, readFileSync } from 'fs';
9+
import { join } from 'path';
10+
import { homedir } from 'os';
11+
import {
12+
loadSMSConfig,
13+
saveSMSConfig,
14+
getMissingConfig,
15+
type MessageChannel,
16+
} from '../../hooks/sms-notify.js';
17+
18+
export function createSettingsCommand(): Command {
19+
const cmd = new Command('settings')
20+
.description('View and configure StackMemory settings')
21+
.addHelpText(
22+
'after',
23+
`
24+
Examples:
25+
stackmemory settings Show all settings and missing config
26+
stackmemory settings notifications Configure notifications interactively
27+
stackmemory settings env Show required environment variables
28+
`
29+
);
30+
31+
cmd
32+
.command('show')
33+
.description('Show current settings and what is missing')
34+
.action(() => {
35+
showSettings();
36+
});
37+
38+
cmd
39+
.command('notifications')
40+
.alias('notify')
41+
.description('Configure notifications interactively')
42+
.action(async () => {
43+
await configureNotifications();
44+
});
45+
46+
cmd
47+
.command('env')
48+
.description('Show required environment variables')
49+
.action(() => {
50+
showEnvVars();
51+
});
52+
53+
// Default action - show settings
54+
cmd.action(() => {
55+
showSettings();
56+
});
57+
58+
return cmd;
59+
}
60+
61+
function showSettings(): void {
62+
console.log(chalk.blue.bold('\nStackMemory Settings\n'));
63+
64+
// Notification settings
65+
const config = loadSMSConfig();
66+
const { missing, configured, ready } = getMissingConfig();
67+
68+
console.log(chalk.cyan('Notifications:'));
69+
console.log(
70+
` ${chalk.gray('Enabled:')} ${config.enabled ? chalk.green('yes') : chalk.yellow('no')}`
71+
);
72+
console.log(
73+
` ${chalk.gray('Channel:')} ${config.channel === 'whatsapp' ? chalk.cyan('WhatsApp') : chalk.blue('SMS')}`
74+
);
75+
console.log(
76+
` ${chalk.gray('Ready:')} ${ready ? chalk.green('yes') : chalk.red('no')}`
77+
);
78+
79+
if (configured.length > 0) {
80+
console.log(`\n ${chalk.green('Configured:')}`);
81+
configured.forEach((item) => {
82+
console.log(` ${chalk.green('✓')} ${item}`);
83+
});
84+
}
85+
86+
if (missing.length > 0) {
87+
console.log(`\n ${chalk.red('Missing:')}`);
88+
missing.forEach((item) => {
89+
console.log(` ${chalk.red('✗')} ${item}`);
90+
});
91+
92+
console.log(
93+
chalk.yellow('\n Run "stackmemory settings notifications" to configure')
94+
);
95+
}
96+
97+
// Show ngrok URL if available
98+
const ngrokUrlPath = join(homedir(), '.stackmemory', 'ngrok-url.txt');
99+
if (existsSync(ngrokUrlPath)) {
100+
const ngrokUrl = readFileSync(ngrokUrlPath, 'utf8').trim();
101+
console.log(`\n ${chalk.gray('Webhook URL:')} ${ngrokUrl}/sms/incoming`);
102+
}
103+
104+
console.log();
105+
}
106+
107+
function showEnvVars(): void {
108+
console.log(chalk.blue.bold('\nRequired Environment Variables\n'));
109+
110+
const { missing, configured } = getMissingConfig();
111+
const config = loadSMSConfig();
112+
113+
console.log(chalk.cyan('Twilio Credentials (required):'));
114+
console.log(
115+
` ${configured.includes('TWILIO_ACCOUNT_SID') ? chalk.green('✓') : chalk.red('✗')} TWILIO_ACCOUNT_SID`
116+
);
117+
console.log(
118+
` ${configured.includes('TWILIO_AUTH_TOKEN') ? chalk.green('✓') : chalk.red('✗')} TWILIO_AUTH_TOKEN`
119+
);
120+
121+
console.log(
122+
chalk.cyan(
123+
`\n${config.channel === 'whatsapp' ? 'WhatsApp' : 'SMS'} Numbers:`
124+
)
125+
);
126+
if (config.channel === 'whatsapp') {
127+
console.log(
128+
` ${configured.includes('TWILIO_WHATSAPP_FROM') ? chalk.green('✓') : chalk.red('✗')} TWILIO_WHATSAPP_FROM`
129+
);
130+
console.log(
131+
` ${configured.includes('TWILIO_WHATSAPP_TO') ? chalk.green('✓') : chalk.red('✗')} TWILIO_WHATSAPP_TO`
132+
);
133+
} else {
134+
console.log(
135+
` ${configured.includes('TWILIO_SMS_FROM') ? chalk.green('✓') : chalk.red('✗')} TWILIO_SMS_FROM`
136+
);
137+
console.log(
138+
` ${configured.includes('TWILIO_SMS_TO') ? chalk.green('✓') : chalk.red('✗')} TWILIO_SMS_TO`
139+
);
140+
}
141+
142+
if (missing.length > 0) {
143+
console.log(chalk.yellow('\nAdd to your .env file or shell profile:'));
144+
console.log(chalk.gray('─'.repeat(50)));
145+
146+
if (missing.includes('TWILIO_ACCOUNT_SID')) {
147+
console.log('export TWILIO_ACCOUNT_SID="your_account_sid"');
148+
}
149+
if (missing.includes('TWILIO_AUTH_TOKEN')) {
150+
console.log('export TWILIO_AUTH_TOKEN="your_auth_token"');
151+
}
152+
if (missing.includes('TWILIO_WHATSAPP_FROM')) {
153+
console.log(
154+
'export TWILIO_WHATSAPP_FROM="+14155238886" # Twilio sandbox'
155+
);
156+
}
157+
if (missing.includes('TWILIO_WHATSAPP_TO')) {
158+
console.log('export TWILIO_WHATSAPP_TO="+1234567890" # Your phone');
159+
}
160+
if (missing.includes('TWILIO_SMS_FROM')) {
161+
console.log('export TWILIO_SMS_FROM="+1234567890" # Twilio number');
162+
}
163+
if (missing.includes('TWILIO_SMS_TO')) {
164+
console.log('export TWILIO_SMS_TO="+1234567890" # Your phone');
165+
}
166+
167+
console.log(chalk.gray('─'.repeat(50)));
168+
}
169+
170+
console.log();
171+
}
172+
173+
async function configureNotifications(): Promise<void> {
174+
console.log(chalk.blue.bold('\nNotification Setup\n'));
175+
176+
const config = loadSMSConfig();
177+
const { missing } = getMissingConfig();
178+
179+
// Ask if they want to enable
180+
const { enable } = await inquirer.prompt([
181+
{
182+
type: 'confirm',
183+
name: 'enable',
184+
message: 'Enable SMS/WhatsApp notifications?',
185+
default: config.enabled,
186+
},
187+
]);
188+
189+
if (!enable) {
190+
config.enabled = false;
191+
saveSMSConfig(config);
192+
console.log(chalk.yellow('Notifications disabled'));
193+
return;
194+
}
195+
196+
// Choose channel
197+
const { channel } = await inquirer.prompt([
198+
{
199+
type: 'list',
200+
name: 'channel',
201+
message: 'Which channel do you want to use?',
202+
choices: [
203+
{
204+
name: 'WhatsApp (recommended - cheaper for conversations)',
205+
value: 'whatsapp',
206+
},
207+
{ name: 'SMS (requires A2P 10DLC registration for US)', value: 'sms' },
208+
],
209+
default: config.channel,
210+
},
211+
]);
212+
213+
config.channel = channel as MessageChannel;
214+
215+
// Check for missing credentials
216+
if (
217+
missing.includes('TWILIO_ACCOUNT_SID') ||
218+
missing.includes('TWILIO_AUTH_TOKEN')
219+
) {
220+
console.log(chalk.yellow('\nTwilio credentials not found in environment.'));
221+
222+
const { hasAccount } = await inquirer.prompt([
223+
{
224+
type: 'confirm',
225+
name: 'hasAccount',
226+
message: 'Do you have a Twilio account?',
227+
default: true,
228+
},
229+
]);
230+
231+
if (!hasAccount) {
232+
console.log(chalk.cyan('\nCreate a free Twilio account:'));
233+
console.log(' https://www.twilio.com/try-twilio\n');
234+
console.log('Then run this command again.');
235+
return;
236+
}
237+
238+
const { saveToEnv } = await inquirer.prompt([
239+
{
240+
type: 'confirm',
241+
name: 'saveToEnv',
242+
message: 'Would you like to save credentials to ~/.stackmemory/.env?',
243+
default: true,
244+
},
245+
]);
246+
247+
if (saveToEnv) {
248+
const { accountSid, authToken } = await inquirer.prompt([
249+
{
250+
type: 'input',
251+
name: 'accountSid',
252+
message: 'Twilio Account SID:',
253+
validate: (input: string) =>
254+
input.startsWith('AC') ? true : 'Account SID should start with AC',
255+
},
256+
{
257+
type: 'password',
258+
name: 'authToken',
259+
message: 'Twilio Auth Token:',
260+
mask: '*',
261+
},
262+
]);
263+
264+
saveToEnvFile({
265+
TWILIO_ACCOUNT_SID: accountSid,
266+
TWILIO_AUTH_TOKEN: authToken,
267+
});
268+
console.log(chalk.green('Credentials saved to ~/.stackmemory/.env'));
269+
}
270+
}
271+
272+
// Get phone numbers
273+
if (channel === 'whatsapp') {
274+
console.log(chalk.cyan('\nWhatsApp Setup:'));
275+
console.log(
276+
' 1. Go to: https://console.twilio.com/us1/develop/sms/try-it-out/whatsapp-learn'
277+
);
278+
console.log(' 2. Note the sandbox number (e.g., +14155238886)');
279+
console.log(' 3. Send the join code from your phone\n');
280+
281+
const { whatsappFrom, whatsappTo } = await inquirer.prompt([
282+
{
283+
type: 'input',
284+
name: 'whatsappFrom',
285+
message: 'Twilio WhatsApp number (sandbox):',
286+
default: config.whatsappFromNumber || '+14155238886',
287+
},
288+
{
289+
type: 'input',
290+
name: 'whatsappTo',
291+
message: 'Your phone number:',
292+
default: config.whatsappToNumber,
293+
validate: (input: string) =>
294+
input.startsWith('+')
295+
? true
296+
: 'Include country code (e.g., +1234567890)',
297+
},
298+
]);
299+
300+
saveToEnvFile({
301+
TWILIO_WHATSAPP_FROM: whatsappFrom,
302+
TWILIO_WHATSAPP_TO: whatsappTo,
303+
TWILIO_CHANNEL: 'whatsapp',
304+
});
305+
} else {
306+
console.log(chalk.cyan('\nSMS Setup:'));
307+
console.log(
308+
chalk.yellow(' Note: US carriers require A2P 10DLC registration')
309+
);
310+
console.log(
311+
' Register at: https://console.twilio.com/us1/develop/sms/settings/compliance\n'
312+
);
313+
314+
const { smsFrom, smsTo } = await inquirer.prompt([
315+
{
316+
type: 'input',
317+
name: 'smsFrom',
318+
message: 'Twilio SMS number:',
319+
default: config.smsFromNumber,
320+
},
321+
{
322+
type: 'input',
323+
name: 'smsTo',
324+
message: 'Your phone number:',
325+
default: config.smsToNumber,
326+
validate: (input: string) =>
327+
input.startsWith('+')
328+
? true
329+
: 'Include country code (e.g., +1234567890)',
330+
},
331+
]);
332+
333+
saveToEnvFile({
334+
TWILIO_SMS_FROM: smsFrom,
335+
TWILIO_SMS_TO: smsTo,
336+
TWILIO_CHANNEL: 'sms',
337+
});
338+
}
339+
340+
config.enabled = true;
341+
saveSMSConfig(config);
342+
343+
console.log(chalk.green('\nNotifications configured!'));
344+
console.log(chalk.gray('Test with: stackmemory notify test'));
345+
}
346+
347+
function saveToEnvFile(vars: Record<string, string>): void {
348+
const envDir = join(homedir(), '.stackmemory');
349+
const envPath = join(envDir, '.env');
350+
351+
if (!existsSync(envDir)) {
352+
mkdirSync(envDir, { recursive: true });
353+
}
354+
355+
let content = '';
356+
if (existsSync(envPath)) {
357+
content = readFileSync(envPath, 'utf8');
358+
}
359+
360+
for (const [key, value] of Object.entries(vars)) {
361+
const regex = new RegExp(`^${key}=.*$`, 'm');
362+
const line = `${key}="${value}"`;
363+
364+
if (regex.test(content)) {
365+
content = content.replace(regex, line);
366+
} else {
367+
content += `${content.endsWith('\n') || content === '' ? '' : '\n'}${line}\n`;
368+
}
369+
}
370+
371+
writeFileSync(envPath, content);
372+
}
373+
374+
export default createSettingsCommand;

src/cli/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { createAPICommand } from './commands/api.js';
5555
import { createCleanupProcessesCommand } from './commands/cleanup-processes.js';
5656
import { createAutoBackgroundCommand } from './commands/auto-background.js';
5757
import { createSMSNotifyCommand } from './commands/sms-notify.js';
58+
import { createSettingsCommand } from './commands/settings.js';
5859
import { ProjectManager } from '../core/projects/project-manager.js';
5960
import Database from 'better-sqlite3';
6061
import { join } from 'path';
@@ -668,6 +669,7 @@ program.addCommand(createAPICommand());
668669
program.addCommand(createCleanupProcessesCommand());
669670
program.addCommand(createAutoBackgroundCommand());
670671
program.addCommand(createSMSNotifyCommand());
672+
program.addCommand(createSettingsCommand());
671673

672674
// Register dashboard command
673675
program

0 commit comments

Comments
 (0)