Skip to content

Commit 2f363c7

Browse files
feat: Auto-execute actions on SMS response + check command
- Webhook now auto-executes actions immediately when response received - macOS notification triggered on response - Add 'notify check' command for Claude to poll responses - Add 'notify watch-responses' for background watching - Signal file written for external process detection
1 parent 21acd3a commit 2f363c7

3 files changed

Lines changed: 241 additions & 12 deletions

File tree

src/cli/commands/sms-notify.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Command } from 'commander';
66
import chalk from 'chalk';
77
import { execSync } from 'child_process';
88
import { join } from 'path';
9+
import { existsSync, readFileSync, unlinkSync } from 'fs';
910
import {
1011
loadSMSConfig,
1112
saveSMSConfig,
@@ -437,6 +438,42 @@ Examples:
437438
);
438439
});
439440

441+
cmd
442+
.command('check')
443+
.description(
444+
'Check for new SMS/WhatsApp responses (use in Claude sessions)'
445+
)
446+
.action(() => {
447+
const responsePath = join(
448+
process.env['HOME'] || '~',
449+
'.stackmemory',
450+
'sms-latest-response.json'
451+
);
452+
453+
try {
454+
if (existsSync(responsePath)) {
455+
const data = JSON.parse(readFileSync(responsePath, 'utf8'));
456+
const age = Date.now() - new Date(data.timestamp).getTime();
457+
458+
if (age < 5 * 60 * 1000) {
459+
// Less than 5 minutes old
460+
console.log(chalk.green.bold('\n*** NEW SMS RESPONSE ***'));
461+
console.log(` Response: "${data.response}"`);
462+
console.log(` Prompt ID: ${data.promptId}`);
463+
console.log(` Received: ${Math.round(age / 1000)}s ago\n`);
464+
465+
// Clear it after reading
466+
unlinkSync(responsePath);
467+
return;
468+
}
469+
}
470+
} catch {
471+
// Ignore errors
472+
}
473+
474+
console.log(chalk.gray('No new responses'));
475+
});
476+
440477
cmd
441478
.command('pending')
442479
.description('List pending prompts awaiting response')
@@ -560,6 +597,17 @@ Examples:
560597
console.log(chalk.green(`Removed ${removed} old action(s)`));
561598
});
562599

600+
cmd
601+
.command('watch-responses')
602+
.description('Watch for incoming SMS/WhatsApp responses and notify')
603+
.option('-i, --interval <ms>', 'Check interval in milliseconds', '2000')
604+
.action(async (options: { interval: string }) => {
605+
const { startResponseWatcher } =
606+
await import('../../hooks/sms-watcher.js');
607+
const interval = parseInt(options.interval, 10);
608+
startResponseWatcher(interval);
609+
});
610+
563611
// Hook installation commands
564612
cmd
565613
.command('install-hook')

src/hooks/sms-watcher.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/env node
2+
/**
3+
* SMS Response Watcher
4+
* Watches for incoming SMS/WhatsApp responses and triggers notifications
5+
*
6+
* Run in background: stackmemory notify watch-responses &
7+
*/
8+
9+
import { existsSync, readFileSync, watchFile, writeFileSync } from 'fs';
10+
import { join } from 'path';
11+
import { homedir } from 'os';
12+
import { execSync } from 'child_process';
13+
14+
const RESPONSE_PATH = join(
15+
homedir(),
16+
'.stackmemory',
17+
'sms-latest-response.json'
18+
);
19+
const SIGNAL_PATH = join(homedir(), '.stackmemory', 'sms-signal.txt');
20+
21+
interface SMSResponse {
22+
promptId: string;
23+
response: string;
24+
timestamp: string;
25+
}
26+
27+
let lastProcessedTimestamp = '';
28+
29+
function checkForResponse(): SMSResponse | null {
30+
try {
31+
if (existsSync(RESPONSE_PATH)) {
32+
const data = JSON.parse(
33+
readFileSync(RESPONSE_PATH, 'utf8')
34+
) as SMSResponse;
35+
36+
// Only process new responses
37+
if (data.timestamp !== lastProcessedTimestamp) {
38+
lastProcessedTimestamp = data.timestamp;
39+
return data;
40+
}
41+
}
42+
} catch {
43+
// Ignore errors
44+
}
45+
return null;
46+
}
47+
48+
function triggerNotification(response: SMSResponse): void {
49+
const message = `SMS Response: "${response.response}"`;
50+
51+
// macOS notification
52+
try {
53+
execSync(
54+
`osascript -e 'display notification "${message}" with title "StackMemory"'`,
55+
{
56+
stdio: 'ignore',
57+
}
58+
);
59+
} catch {
60+
// Ignore if not on macOS
61+
}
62+
63+
// Terminal bell
64+
process.stdout.write('\x07');
65+
66+
// Write to signal file (for other processes to detect)
67+
try {
68+
writeFileSync(
69+
SIGNAL_PATH,
70+
JSON.stringify({
71+
type: 'sms_response',
72+
response: response.response,
73+
promptId: response.promptId,
74+
timestamp: new Date().toISOString(),
75+
})
76+
);
77+
} catch {
78+
// Ignore
79+
}
80+
81+
// Output to terminal
82+
console.log(`\n[SMS] User responded: "${response.response}"`);
83+
console.log(`[SMS] Run: stackmemory notify run-actions\n`);
84+
}
85+
86+
export function startResponseWatcher(intervalMs: number = 2000): void {
87+
console.log('[SMS Watcher] Watching for responses...');
88+
console.log('[SMS Watcher] Press Ctrl+C to stop\n');
89+
90+
// Initial check
91+
const initial = checkForResponse();
92+
if (initial) {
93+
triggerNotification(initial);
94+
}
95+
96+
// Poll for changes
97+
setInterval(() => {
98+
const response = checkForResponse();
99+
if (response) {
100+
triggerNotification(response);
101+
}
102+
}, intervalMs);
103+
}
104+
105+
// Also watch file for immediate notification
106+
export function startFileWatcher(): void {
107+
console.log('[SMS Watcher] Watching for responses (file mode)...');
108+
109+
watchFile(RESPONSE_PATH, { interval: 1000 }, () => {
110+
const response = checkForResponse();
111+
if (response) {
112+
triggerNotification(response);
113+
}
114+
});
115+
}
116+
117+
// CLI entry
118+
if (process.argv[1]?.includes('sms-watcher')) {
119+
startResponseWatcher();
120+
}
121+
122+
export { checkForResponse, triggerNotification };

src/hooks/sms-webhook.ts

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { join } from 'path';
1010
import { homedir } from 'os';
1111
import { processIncomingResponse, loadSMSConfig } from './sms-notify.js';
1212
import { queueAction } from './sms-action-runner.js';
13+
import { execSync } from 'child_process';
1314

1415
interface TwilioWebhookPayload {
1516
From: string;
@@ -76,27 +77,85 @@ export function handleSMSWebhook(payload: TwilioWebhookPayload): {
7677
result.action
7778
);
7879

79-
// Queue action for execution (instead of immediate execution)
80+
// Trigger notification to alert user/Claude
81+
triggerResponseNotification(result.response || Body);
82+
83+
// Execute action immediately if present
8084
if (result.action) {
81-
const actionId = queueAction(
82-
result.prompt?.id || 'unknown',
83-
result.response || Body,
84-
result.action
85-
);
86-
console.log(`[sms-webhook] Queued action ${actionId}: ${result.action}`);
85+
console.log(`[sms-webhook] Executing action: ${result.action}`);
86+
87+
try {
88+
const output = execSync(result.action, {
89+
encoding: 'utf8',
90+
timeout: 60000,
91+
stdio: ['pipe', 'pipe', 'pipe'],
92+
});
93+
console.log(
94+
`[sms-webhook] Action completed: ${output.substring(0, 200)}`
95+
);
96+
97+
return {
98+
response: `Done! Action executed successfully.`,
99+
action: result.action,
100+
queued: false,
101+
};
102+
} catch (err) {
103+
const error = err instanceof Error ? err.message : String(err);
104+
console.log(`[sms-webhook] Action failed: ${error}`);
105+
106+
// Queue for retry
107+
queueAction(
108+
result.prompt?.id || 'unknown',
109+
result.response || Body,
110+
result.action
111+
);
87112

88-
return {
89-
response: `Got it! Queued action: ${result.action.substring(0, 30)}...`,
90-
action: result.action,
91-
queued: true,
92-
};
113+
return {
114+
response: `Action failed, queued for retry: ${error.substring(0, 50)}`,
115+
action: result.action,
116+
queued: true,
117+
};
118+
}
93119
}
94120

95121
return {
96122
response: `Received: ${result.response}. Next action will be triggered.`,
97123
};
98124
}
99125

126+
// Trigger notification when response received
127+
function triggerResponseNotification(response: string): void {
128+
const message = `SMS Response: ${response}`;
129+
130+
// macOS notification
131+
try {
132+
execSync(
133+
`osascript -e 'display notification "${message}" with title "StackMemory" sound name "Glass"'`,
134+
{ stdio: 'ignore', timeout: 5000 }
135+
);
136+
} catch {
137+
// Ignore if not on macOS
138+
}
139+
140+
// Write signal file for other processes
141+
try {
142+
const signalPath = join(homedir(), '.stackmemory', 'sms-signal.txt');
143+
writeFileSync(
144+
signalPath,
145+
JSON.stringify({
146+
type: 'sms_response',
147+
response,
148+
timestamp: new Date().toISOString(),
149+
})
150+
);
151+
} catch {
152+
// Ignore
153+
}
154+
155+
console.log(`\n*** SMS RESPONSE RECEIVED: "${response}" ***`);
156+
console.log(`*** Run: stackmemory notify run-actions ***\n`);
157+
}
158+
100159
// TwiML response helper
101160
function twimlResponse(message: string): string {
102161
return `<?xml version="1.0" encoding="UTF-8"?>

0 commit comments

Comments
 (0)