@stackmemoryai/stackmemory 0.5.8 → 0.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/sms-notify.js +187 -20
- package/dist/cli/commands/sms-notify.js.map +2 -2
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.js.map +2 -2
- package/dist/hooks/sms-action-runner.js +170 -0
- package/dist/hooks/sms-action-runner.js.map +7 -0
- package/dist/hooks/sms-notify.js +75 -11
- package/dist/hooks/sms-notify.js.map +3 -3
- package/dist/hooks/sms-webhook.js +37 -15
- package/dist/hooks/sms-webhook.js.map +2 -2
- package/package.json +1 -1
- package/templates/claude-hooks/sms-response-handler.js +185 -0
|
@@ -4,15 +4,24 @@ const __filename = __fileURLToPath(import.meta.url);
|
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import chalk from "chalk";
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
import { join } from "path";
|
|
7
9
|
import {
|
|
8
10
|
loadSMSConfig,
|
|
9
11
|
saveSMSConfig,
|
|
12
|
+
sendNotification,
|
|
10
13
|
sendSMSNotification,
|
|
11
14
|
notifyReviewReady,
|
|
12
15
|
notifyWithYesNo,
|
|
13
16
|
notifyTaskComplete,
|
|
14
17
|
cleanupExpiredPrompts
|
|
15
18
|
} from "../../hooks/sms-notify.js";
|
|
19
|
+
import {
|
|
20
|
+
loadActionQueue,
|
|
21
|
+
processAllPendingActions,
|
|
22
|
+
cleanupOldActions,
|
|
23
|
+
startActionWatcher
|
|
24
|
+
} from "../../hooks/sms-action-runner.js";
|
|
16
25
|
function createSMSNotifyCommand() {
|
|
17
26
|
const cmd = new Command("notify").description(
|
|
18
27
|
"SMS notification system for review alerts (optional, requires Twilio)"
|
|
@@ -21,17 +30,32 @@ function createSMSNotifyCommand() {
|
|
|
21
30
|
`
|
|
22
31
|
Setup (optional):
|
|
23
32
|
1. Create Twilio account at https://twilio.com
|
|
24
|
-
2. Get Account SID, Auth Token, and phone
|
|
33
|
+
2. Get Account SID, Auth Token, and phone numbers
|
|
25
34
|
3. Set environment variables:
|
|
26
35
|
export TWILIO_ACCOUNT_SID=your_sid
|
|
27
36
|
export TWILIO_AUTH_TOKEN=your_token
|
|
37
|
+
|
|
38
|
+
For WhatsApp (recommended - cheaper for conversations):
|
|
39
|
+
export TWILIO_WHATSAPP_FROM=+1234567890
|
|
40
|
+
export TWILIO_WHATSAPP_TO=+1234567890
|
|
41
|
+
export TWILIO_CHANNEL=whatsapp
|
|
42
|
+
|
|
43
|
+
For SMS:
|
|
44
|
+
export TWILIO_SMS_FROM=+1234567890
|
|
45
|
+
export TWILIO_SMS_TO=+1234567890
|
|
46
|
+
export TWILIO_CHANNEL=sms
|
|
47
|
+
|
|
48
|
+
Legacy (works for both, defaults to WhatsApp):
|
|
28
49
|
export TWILIO_FROM_NUMBER=+1234567890
|
|
29
50
|
export TWILIO_TO_NUMBER=+1234567890
|
|
51
|
+
|
|
30
52
|
4. Enable: stackmemory notify enable
|
|
31
53
|
|
|
32
54
|
Examples:
|
|
33
55
|
stackmemory notify status Check configuration
|
|
34
56
|
stackmemory notify enable Enable notifications
|
|
57
|
+
stackmemory notify channel whatsapp Switch to WhatsApp
|
|
58
|
+
stackmemory notify channel sms Switch to SMS
|
|
35
59
|
stackmemory notify test Send test message
|
|
36
60
|
stackmemory notify send "PR ready" Send custom message
|
|
37
61
|
stackmemory notify review "PR #123" Send review notification with options
|
|
@@ -40,20 +64,42 @@ Examples:
|
|
|
40
64
|
);
|
|
41
65
|
cmd.command("status").description("Show notification configuration status").action(() => {
|
|
42
66
|
const config = loadSMSConfig();
|
|
43
|
-
console.log(chalk.blue("
|
|
67
|
+
console.log(chalk.blue("Notification Status:"));
|
|
44
68
|
console.log();
|
|
45
|
-
const hasCreds = config.accountSid && config.authToken
|
|
69
|
+
const hasCreds = config.accountSid && config.authToken;
|
|
70
|
+
const channel = config.channel || "whatsapp";
|
|
71
|
+
const hasWhatsApp = config.whatsappFromNumber || config.fromNumber || config.whatsappToNumber || config.toNumber;
|
|
72
|
+
const hasSMS = config.smsFromNumber || config.fromNumber || config.smsToNumber || config.toNumber;
|
|
73
|
+
const hasNumbers = channel === "whatsapp" ? hasWhatsApp : hasSMS;
|
|
46
74
|
console.log(
|
|
47
75
|
` ${chalk.gray("Enabled:")} ${config.enabled ? chalk.green("yes") : chalk.red("no")}`
|
|
48
76
|
);
|
|
49
77
|
console.log(
|
|
50
|
-
` ${chalk.gray("
|
|
78
|
+
` ${chalk.gray("Channel:")} ${channel === "whatsapp" ? chalk.cyan("WhatsApp") : chalk.blue("SMS")}`
|
|
51
79
|
);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
80
|
+
console.log(
|
|
81
|
+
` ${chalk.gray("Configured:")} ${hasCreds && hasNumbers ? chalk.green("yes") : chalk.yellow("no (set env vars)")}`
|
|
82
|
+
);
|
|
83
|
+
console.log();
|
|
84
|
+
console.log(chalk.blue("Numbers:"));
|
|
85
|
+
if (channel === "whatsapp") {
|
|
86
|
+
const from = config.whatsappFromNumber || config.fromNumber;
|
|
87
|
+
const to = config.whatsappToNumber || config.toNumber;
|
|
88
|
+
if (from) {
|
|
89
|
+
console.log(` ${chalk.gray("WhatsApp From:")} ${maskPhone(from)}`);
|
|
90
|
+
}
|
|
91
|
+
if (to) {
|
|
92
|
+
console.log(` ${chalk.gray("WhatsApp To:")} ${maskPhone(to)}`);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
const from = config.smsFromNumber || config.fromNumber;
|
|
96
|
+
const to = config.smsToNumber || config.toNumber;
|
|
97
|
+
if (from) {
|
|
98
|
+
console.log(` ${chalk.gray("SMS From:")} ${maskPhone(from)}`);
|
|
99
|
+
}
|
|
100
|
+
if (to) {
|
|
101
|
+
console.log(` ${chalk.gray("SMS To:")} ${maskPhone(to)}`);
|
|
102
|
+
}
|
|
57
103
|
}
|
|
58
104
|
console.log();
|
|
59
105
|
console.log(chalk.blue("Notify On:"));
|
|
@@ -81,15 +127,21 @@ Examples:
|
|
|
81
127
|
console.log(
|
|
82
128
|
` ${chalk.gray("Response Timeout:")} ${config.responseTimeout}s`
|
|
83
129
|
);
|
|
84
|
-
if (!hasCreds) {
|
|
130
|
+
if (!hasCreds || !hasNumbers) {
|
|
85
131
|
console.log();
|
|
86
132
|
console.log(
|
|
87
133
|
chalk.yellow("To configure, set these environment variables:")
|
|
88
134
|
);
|
|
89
135
|
console.log(chalk.gray(" export TWILIO_ACCOUNT_SID=your_sid"));
|
|
90
136
|
console.log(chalk.gray(" export TWILIO_AUTH_TOKEN=your_token"));
|
|
91
|
-
console.log(
|
|
92
|
-
console.log(chalk.gray("
|
|
137
|
+
console.log();
|
|
138
|
+
console.log(chalk.gray(" For WhatsApp (recommended):"));
|
|
139
|
+
console.log(chalk.gray(" export TWILIO_WHATSAPP_FROM=+1234567890"));
|
|
140
|
+
console.log(chalk.gray(" export TWILIO_WHATSAPP_TO=+1234567890"));
|
|
141
|
+
console.log();
|
|
142
|
+
console.log(chalk.gray(" For SMS:"));
|
|
143
|
+
console.log(chalk.gray(" export TWILIO_SMS_FROM=+1234567890"));
|
|
144
|
+
console.log(chalk.gray(" export TWILIO_SMS_TO=+1234567890"));
|
|
93
145
|
}
|
|
94
146
|
});
|
|
95
147
|
cmd.command("enable").description("Enable SMS notifications").action(() => {
|
|
@@ -112,15 +164,52 @@ Examples:
|
|
|
112
164
|
saveSMSConfig(config);
|
|
113
165
|
console.log(chalk.yellow("SMS notifications disabled"));
|
|
114
166
|
});
|
|
115
|
-
cmd.command("
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
167
|
+
cmd.command("channel <type>").description("Set notification channel (whatsapp|sms)").action((type) => {
|
|
168
|
+
const validChannels = ["whatsapp", "sms"];
|
|
169
|
+
const channel = type.toLowerCase();
|
|
170
|
+
if (!validChannels.includes(channel)) {
|
|
171
|
+
console.log(
|
|
172
|
+
chalk.red(`Invalid channel. Use: ${validChannels.join(", ")}`)
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const config = loadSMSConfig();
|
|
177
|
+
config.channel = channel;
|
|
178
|
+
saveSMSConfig(config);
|
|
179
|
+
const label = channel === "whatsapp" ? "WhatsApp" : "SMS";
|
|
180
|
+
console.log(chalk.green(`Notification channel set to ${label}`));
|
|
181
|
+
if (channel === "whatsapp") {
|
|
182
|
+
const hasNumbers = config.whatsappFromNumber || config.fromNumber;
|
|
183
|
+
if (!hasNumbers) {
|
|
184
|
+
console.log(
|
|
185
|
+
chalk.yellow("Set TWILIO_WHATSAPP_FROM and TWILIO_WHATSAPP_TO")
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
const hasNumbers = config.smsFromNumber || config.fromNumber;
|
|
190
|
+
if (!hasNumbers) {
|
|
191
|
+
console.log(chalk.yellow("Set TWILIO_SMS_FROM and TWILIO_SMS_TO"));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
cmd.command("test").description("Send a test notification").option("--sms", "Force SMS channel").option("--whatsapp", "Force WhatsApp channel").action(async (options) => {
|
|
196
|
+
const config = loadSMSConfig();
|
|
197
|
+
const channelOverride = options.sms ? "sms" : options.whatsapp ? "whatsapp" : void 0;
|
|
198
|
+
const channelLabel = channelOverride || config.channel === "whatsapp" ? "WhatsApp" : "SMS";
|
|
199
|
+
console.log(
|
|
200
|
+
chalk.blue(`Sending test notification via ${channelLabel}...`)
|
|
201
|
+
);
|
|
202
|
+
const result = await sendNotification(
|
|
203
|
+
{
|
|
204
|
+
type: "custom",
|
|
205
|
+
title: "StackMemory Test",
|
|
206
|
+
message: "This is a test notification from StackMemory."
|
|
207
|
+
},
|
|
208
|
+
channelOverride
|
|
209
|
+
);
|
|
122
210
|
if (result.success) {
|
|
123
|
-
|
|
211
|
+
const usedChannel = result.channel === "whatsapp" ? "WhatsApp" : "SMS";
|
|
212
|
+
console.log(chalk.green(`Test message sent via ${usedChannel}!`));
|
|
124
213
|
} else {
|
|
125
214
|
console.log(chalk.red(`Failed: ${result.error}`));
|
|
126
215
|
}
|
|
@@ -264,6 +353,84 @@ Examples:
|
|
|
264
353
|
saveSMSConfig(config);
|
|
265
354
|
console.log(chalk.green(`Response timeout set to ${timeout} seconds`));
|
|
266
355
|
});
|
|
356
|
+
cmd.command("actions").description("List queued actions from SMS responses").action(() => {
|
|
357
|
+
const queue = loadActionQueue();
|
|
358
|
+
if (queue.actions.length === 0) {
|
|
359
|
+
console.log(chalk.gray("No actions in queue"));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
console.log(chalk.blue("Action Queue:"));
|
|
363
|
+
queue.actions.forEach((a) => {
|
|
364
|
+
const statusColor = a.status === "completed" ? chalk.green : a.status === "failed" ? chalk.red : a.status === "running" ? chalk.yellow : chalk.gray;
|
|
365
|
+
console.log();
|
|
366
|
+
console.log(` ${chalk.gray("ID:")} ${a.id}`);
|
|
367
|
+
console.log(` ${chalk.gray("Status:")} ${statusColor(a.status)}`);
|
|
368
|
+
console.log(
|
|
369
|
+
` ${chalk.gray("Action:")} ${a.action.substring(0, 60)}...`
|
|
370
|
+
);
|
|
371
|
+
console.log(` ${chalk.gray("Response:")} ${a.response}`);
|
|
372
|
+
if (a.error) {
|
|
373
|
+
console.log(` ${chalk.gray("Error:")} ${chalk.red(a.error)}`);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
cmd.command("run-actions").description("Execute all pending actions from SMS responses").action(() => {
|
|
378
|
+
console.log(chalk.blue("Processing pending actions..."));
|
|
379
|
+
const result = processAllPendingActions();
|
|
380
|
+
console.log(
|
|
381
|
+
chalk.green(
|
|
382
|
+
`Processed ${result.processed} action(s): ${result.succeeded} succeeded, ${result.failed} failed`
|
|
383
|
+
)
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
cmd.command("watch").description("Watch for and execute SMS response actions").option("-i, --interval <ms>", "Check interval in milliseconds", "5000").action((options) => {
|
|
387
|
+
const interval = parseInt(options.interval, 10);
|
|
388
|
+
console.log(chalk.blue(`Watching for actions (interval: ${interval}ms)`));
|
|
389
|
+
console.log(chalk.gray("Press Ctrl+C to stop"));
|
|
390
|
+
startActionWatcher(interval);
|
|
391
|
+
});
|
|
392
|
+
cmd.command("cleanup-actions").description("Remove old completed actions").action(() => {
|
|
393
|
+
const removed = cleanupOldActions();
|
|
394
|
+
console.log(chalk.green(`Removed ${removed} old action(s)`));
|
|
395
|
+
});
|
|
396
|
+
cmd.command("install-hook").description("Install Claude Code notification hook").action(() => {
|
|
397
|
+
try {
|
|
398
|
+
const scriptPath = join(
|
|
399
|
+
__dirname,
|
|
400
|
+
"../../../scripts/install-notify-hook.sh"
|
|
401
|
+
);
|
|
402
|
+
execSync(`bash "${scriptPath}"`, { stdio: "inherit" });
|
|
403
|
+
} catch {
|
|
404
|
+
console.error(chalk.red("Failed to install hook"));
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
cmd.command("install-response-hook").description("Install Claude Code response handler hook").action(() => {
|
|
408
|
+
try {
|
|
409
|
+
const hooksDir = join(process.env["HOME"] || "~", ".claude", "hooks");
|
|
410
|
+
const hookSrc = join(
|
|
411
|
+
__dirname,
|
|
412
|
+
"../../../templates/claude-hooks/sms-response-handler.js"
|
|
413
|
+
);
|
|
414
|
+
const hookDest = join(hooksDir, "sms-response-handler.js");
|
|
415
|
+
execSync(`mkdir -p "${hooksDir}"`, { stdio: "inherit" });
|
|
416
|
+
execSync(`cp "${hookSrc}" "${hookDest}"`, { stdio: "inherit" });
|
|
417
|
+
execSync(`chmod +x "${hookDest}"`, { stdio: "inherit" });
|
|
418
|
+
console.log(chalk.green("Response handler hook installed!"));
|
|
419
|
+
console.log(chalk.gray(`Location: ${hookDest}`));
|
|
420
|
+
console.log();
|
|
421
|
+
console.log(chalk.blue("Add to ~/.claude/settings.json:"));
|
|
422
|
+
console.log(
|
|
423
|
+
chalk.gray(` "hooks": { "pre_tool_use": ["node ${hookDest}"] }`)
|
|
424
|
+
);
|
|
425
|
+
} catch {
|
|
426
|
+
console.error(chalk.red("Failed to install response hook"));
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
cmd.command("webhook").description("Start SMS webhook server for receiving responses").option("-p, --port <port>", "Port to listen on", "3456").action(async (options) => {
|
|
430
|
+
const { startWebhookServer } = await import("../../hooks/sms-webhook.js");
|
|
431
|
+
const port = parseInt(options.port, 10);
|
|
432
|
+
startWebhookServer(port);
|
|
433
|
+
});
|
|
267
434
|
return cmd;
|
|
268
435
|
}
|
|
269
436
|
function maskPhone(phone) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/cli/commands/sms-notify.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * CLI command for SMS notification management\n */\n\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport {\n loadSMSConfig,\n saveSMSConfig,\n sendSMSNotification,\n notifyReviewReady,\n notifyWithYesNo,\n notifyTaskComplete,\n cleanupExpiredPrompts,\n} from '../../hooks/sms-notify.js';\n\nexport function createSMSNotifyCommand(): Command {\n const cmd = new Command('notify')\n .description(\n 'SMS notification system for review alerts (optional, requires Twilio)'\n )\n .addHelpText(\n 'after',\n `\nSetup (optional):\n 1. Create Twilio account at https://twilio.com\n 2. Get Account SID, Auth Token, and phone number\n 3. Set environment variables:\n export TWILIO_ACCOUNT_SID=your_sid\n export TWILIO_AUTH_TOKEN=your_token\n export TWILIO_FROM_NUMBER=+1234567890\n export TWILIO_TO_NUMBER=+1234567890\n 4. Enable: stackmemory notify enable\n\nExamples:\n stackmemory notify status Check configuration\n stackmemory notify enable Enable notifications\n stackmemory notify test Send test message\n stackmemory notify send \"PR ready\" Send custom message\n stackmemory notify review \"PR #123\" Send review notification with options\n stackmemory notify ask \"Deploy?\" Send yes/no prompt\n`\n );\n\n cmd\n .command('status')\n .description('Show notification configuration status')\n .action(() => {\n const config = loadSMSConfig();\n\n console.log(chalk.blue('SMS Notification Status:'));\n console.log();\n\n // Check if configured\n const hasCreds =\n config.accountSid &&\n config.authToken &&\n config.fromNumber &&\n config.toNumber;\n\n console.log(\n ` ${chalk.gray('Enabled:')} ${config.enabled ? chalk.green('yes') : chalk.red('no')}`\n );\n console.log(\n ` ${chalk.gray('Configured:')} ${hasCreds ? chalk.green('yes') : chalk.yellow('no (set env vars)')}`\n );\n\n if (config.fromNumber) {\n console.log(` ${chalk.gray('From:')} ${maskPhone(config.fromNumber)}`);\n }\n if (config.toNumber) {\n console.log(` ${chalk.gray('To:')} ${maskPhone(config.toNumber)}`);\n }\n\n console.log();\n console.log(chalk.blue('Notify On:'));\n console.log(\n ` ${chalk.gray('Task Complete:')} ${config.notifyOn.taskComplete ? 'yes' : 'no'}`\n );\n console.log(\n ` ${chalk.gray('Review Ready:')} ${config.notifyOn.reviewReady ? 'yes' : 'no'}`\n );\n console.log(\n ` ${chalk.gray('Errors:')} ${config.notifyOn.error ? 'yes' : 'no'}`\n );\n\n if (config.quietHours?.enabled) {\n console.log();\n console.log(\n chalk.blue(\n `Quiet Hours: ${config.quietHours.start} - ${config.quietHours.end}`\n )\n );\n }\n\n console.log();\n console.log(\n ` ${chalk.gray('Pending Prompts:')} ${config.pendingPrompts.length}`\n );\n console.log(\n ` ${chalk.gray('Response Timeout:')} ${config.responseTimeout}s`\n );\n\n if (!hasCreds) {\n console.log();\n console.log(\n chalk.yellow('To configure, set these environment variables:')\n );\n console.log(chalk.gray(' export TWILIO_ACCOUNT_SID=your_sid'));\n console.log(chalk.gray(' export TWILIO_AUTH_TOKEN=your_token'));\n console.log(chalk.gray(' export TWILIO_FROM_NUMBER=+1234567890'));\n console.log(chalk.gray(' export TWILIO_TO_NUMBER=+1234567890'));\n }\n });\n\n cmd\n .command('enable')\n .description('Enable SMS notifications')\n .action(() => {\n const config = loadSMSConfig();\n config.enabled = true;\n saveSMSConfig(config);\n console.log(chalk.green('SMS notifications enabled'));\n\n const hasCreds =\n config.accountSid &&\n config.authToken &&\n config.fromNumber &&\n config.toNumber;\n if (!hasCreds) {\n console.log(\n chalk.yellow(\n 'Note: Set Twilio environment variables to send messages'\n )\n );\n }\n });\n\n cmd\n .command('disable')\n .description('Disable SMS notifications')\n .action(() => {\n const config = loadSMSConfig();\n config.enabled = false;\n saveSMSConfig(config);\n console.log(chalk.yellow('SMS notifications disabled'));\n });\n\n cmd\n .command('test')\n .description('Send a test notification')\n .action(async () => {\n console.log(chalk.blue('Sending test notification...'));\n\n const result = await sendSMSNotification({\n type: 'custom',\n title: 'StackMemory Test',\n message: 'This is a test notification from StackMemory.',\n });\n\n if (result.success) {\n console.log(chalk.green('Test message sent successfully!'));\n } else {\n console.log(chalk.red(`Failed: ${result.error}`));\n }\n });\n\n cmd\n .command('send <message>')\n .description('Send a custom notification')\n .option('-t, --title <title>', 'Message title', 'StackMemory Alert')\n .action(async (message: string, options: { title: string }) => {\n const result = await sendSMSNotification({\n type: 'custom',\n title: options.title,\n message,\n });\n\n if (result.success) {\n console.log(chalk.green('Message sent!'));\n } else {\n console.log(chalk.red(`Failed: ${result.error}`));\n }\n });\n\n cmd\n .command('review <title>')\n .description('Send review-ready notification with options')\n .option('-m, --message <msg>', 'Description', 'Ready for your review')\n .option(\n '-o, --options <opts>',\n 'Comma-separated options',\n 'Approve,Request Changes,Skip'\n )\n .action(\n async (title: string, options: { message: string; options: string }) => {\n const opts = options.options.split(',').map((o) => ({\n label: o.trim(),\n }));\n\n console.log(chalk.blue('Sending review notification...'));\n\n const result = await notifyReviewReady(title, options.message, opts);\n\n if (result.success) {\n console.log(chalk.green('Review notification sent!'));\n if (result.promptId) {\n console.log(chalk.gray(`Prompt ID: ${result.promptId}`));\n }\n } else {\n console.log(chalk.red(`Failed: ${result.error}`));\n }\n }\n );\n\n cmd\n .command('ask <question>')\n .description('Send a yes/no prompt')\n .option('-t, --title <title>', 'Message title', 'StackMemory')\n .action(async (question: string, options: { title: string }) => {\n console.log(chalk.blue('Sending yes/no prompt...'));\n\n const result = await notifyWithYesNo(options.title, question);\n\n if (result.success) {\n console.log(chalk.green('Prompt sent!'));\n if (result.promptId) {\n console.log(chalk.gray(`Prompt ID: ${result.promptId}`));\n }\n } else {\n console.log(chalk.red(`Failed: ${result.error}`));\n }\n });\n\n cmd\n .command('complete <task>')\n .description('Send task completion notification')\n .option('-s, --summary <text>', 'Task summary', '')\n .action(async (task: string, options: { summary: string }) => {\n const result = await notifyTaskComplete(\n task,\n options.summary || `Task \"${task}\" has been completed.`\n );\n\n if (result.success) {\n console.log(chalk.green('Completion notification sent!'));\n } else {\n console.log(chalk.red(`Failed: ${result.error}`));\n }\n });\n\n cmd\n .command('quiet')\n .description('Configure quiet hours')\n .option('--enable', 'Enable quiet hours')\n .option('--disable', 'Disable quiet hours')\n .option('--start <time>', 'Start time (HH:MM)', '22:00')\n .option('--end <time>', 'End time (HH:MM)', '08:00')\n .action(\n (options: {\n enable?: boolean;\n disable?: boolean;\n start: string;\n end: string;\n }) => {\n const config = loadSMSConfig();\n\n if (!config.quietHours) {\n config.quietHours = { enabled: false, start: '22:00', end: '08:00' };\n }\n\n if (options.enable) {\n config.quietHours.enabled = true;\n } else if (options.disable) {\n config.quietHours.enabled = false;\n }\n\n if (options.start) {\n config.quietHours.start = options.start;\n }\n if (options.end) {\n config.quietHours.end = options.end;\n }\n\n saveSMSConfig(config);\n\n if (config.quietHours.enabled) {\n console.log(\n chalk.green(\n `Quiet hours enabled: ${config.quietHours.start} - ${config.quietHours.end}`\n )\n );\n } else {\n console.log(chalk.yellow('Quiet hours disabled'));\n }\n }\n );\n\n cmd\n .command('toggle <type>')\n .description(\n 'Toggle notification type (taskComplete|reviewReady|error|custom)'\n )\n .action((type: string) => {\n const config = loadSMSConfig();\n const validTypes = ['taskComplete', 'reviewReady', 'error', 'custom'];\n\n if (!validTypes.includes(type)) {\n console.log(chalk.red(`Invalid type. Use: ${validTypes.join(', ')}`));\n return;\n }\n\n const key = type as keyof typeof config.notifyOn;\n config.notifyOn[key] = !config.notifyOn[key];\n saveSMSConfig(config);\n\n console.log(\n chalk.green(\n `${type} notifications ${config.notifyOn[key] ? 'enabled' : 'disabled'}`\n )\n );\n });\n\n cmd\n .command('pending')\n .description('List pending prompts awaiting response')\n .action(() => {\n const config = loadSMSConfig();\n\n if (config.pendingPrompts.length === 0) {\n console.log(chalk.gray('No pending prompts'));\n return;\n }\n\n console.log(chalk.blue('Pending Prompts:'));\n config.pendingPrompts.forEach((p) => {\n const expires = new Date(p.expiresAt);\n const remaining = Math.round((expires.getTime() - Date.now()) / 1000);\n\n console.log();\n console.log(` ${chalk.gray('ID:')} ${p.id}`);\n console.log(` ${chalk.gray('Type:')} ${p.type}`);\n console.log(\n ` ${chalk.gray('Message:')} ${p.message.substring(0, 50)}...`\n );\n console.log(\n ` ${chalk.gray('Expires:')} ${remaining > 0 ? `${remaining}s` : chalk.red('expired')}`\n );\n });\n });\n\n cmd\n .command('cleanup')\n .description('Remove expired pending prompts')\n .action(() => {\n const removed = cleanupExpiredPrompts();\n console.log(chalk.green(`Removed ${removed} expired prompt(s)`));\n });\n\n cmd\n .command('timeout <seconds>')\n .description('Set response timeout for prompts')\n .action((seconds: string) => {\n const config = loadSMSConfig();\n const timeout = parseInt(seconds, 10);\n\n if (isNaN(timeout) || timeout < 30) {\n console.log(chalk.red('Timeout must be at least 30 seconds'));\n return;\n }\n\n config.responseTimeout = timeout;\n saveSMSConfig(config);\n console.log(chalk.green(`Response timeout set to ${timeout} seconds`));\n });\n\n return cmd;\n}\n\nfunction maskPhone(phone: string): string {\n if (phone.length < 8) return phone;\n return phone.substring(0, 4) + '****' + phone.substring(phone.length - 2);\n}\n"],
|
|
5
|
-
"mappings": ";;;;AAIA,SAAS,eAAe;AACxB,OAAO,WAAW;AAClB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,SAAS,yBAAkC;AAChD,QAAM,MAAM,IAAI,QAAQ,QAAQ,EAC7B;AAAA,IACC;AAAA,EACF,EACC;AAAA,IACC;AAAA,IACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBF;AAEF,MACG,QAAQ,QAAQ,EAChB,YAAY,wCAAwC,EACpD,OAAO,MAAM;AACZ,UAAM,SAAS,cAAc;AAE7B,YAAQ,IAAI,MAAM,KAAK,0BAA0B,CAAC;AAClD,YAAQ,IAAI;AAGZ,UAAM,WACJ,OAAO,cACP,OAAO,aACP,OAAO,cACP,OAAO;AAET,YAAQ;AAAA,MACN,KAAK,MAAM,KAAK,UAAU,CAAC,IAAI,OAAO,UAAU,MAAM,MAAM,KAAK,IAAI,MAAM,IAAI,IAAI,CAAC;AAAA,IACtF;AACA,YAAQ;AAAA,MACN,KAAK,MAAM,KAAK,aAAa,CAAC,IAAI,WAAW,MAAM,MAAM,KAAK,IAAI,MAAM,OAAO,mBAAmB,CAAC;AAAA,IACrG;AAEA,QAAI,OAAO,YAAY;AACrB,cAAQ,IAAI,KAAK,MAAM,KAAK,OAAO,CAAC,IAAI,UAAU,OAAO,UAAU,CAAC,EAAE;AAAA,IACxE;AACA,QAAI,OAAO,UAAU;AACnB,cAAQ,IAAI,KAAK,MAAM,KAAK,KAAK,CAAC,IAAI,UAAU,OAAO,QAAQ,CAAC,EAAE;AAAA,IACpE;AAEA,YAAQ,IAAI;AACZ,YAAQ,IAAI,MAAM,KAAK,YAAY,CAAC;AACpC,YAAQ;AAAA,MACN,KAAK,MAAM,KAAK,gBAAgB,CAAC,IAAI,OAAO,SAAS,eAAe,QAAQ,IAAI;AAAA,IAClF;AACA,YAAQ;AAAA,MACN,KAAK,MAAM,KAAK,eAAe,CAAC,IAAI,OAAO,SAAS,cAAc,QAAQ,IAAI;AAAA,IAChF;AACA,YAAQ;AAAA,MACN,KAAK,MAAM,KAAK,SAAS,CAAC,IAAI,OAAO,SAAS,QAAQ,QAAQ,IAAI;AAAA,IACpE;AAEA,QAAI,OAAO,YAAY,SAAS;AAC9B,cAAQ,IAAI;AACZ,cAAQ;AAAA,QACN,MAAM;AAAA,UACJ,gBAAgB,OAAO,WAAW,KAAK,MAAM,OAAO,WAAW,GAAG;AAAA,QACpE;AAAA,MACF;AAAA,IACF;AAEA,YAAQ,IAAI;AACZ,YAAQ;AAAA,MACN,KAAK,MAAM,KAAK,kBAAkB,CAAC,IAAI,OAAO,eAAe,MAAM;AAAA,IACrE;AACA,YAAQ;AAAA,MACN,KAAK,MAAM,KAAK,mBAAmB,CAAC,IAAI,OAAO,eAAe;AAAA,IAChE;AAEA,QAAI,CAAC,UAAU;AACb,cAAQ,IAAI;AACZ,cAAQ;AAAA,QACN,MAAM,OAAO,gDAAgD;AAAA,MAC/D;AACA,cAAQ,IAAI,MAAM,KAAK,sCAAsC,CAAC;AAC9D,cAAQ,IAAI,MAAM,KAAK,uCAAuC,CAAC;AAC/D,cAAQ,IAAI,MAAM,KAAK,yCAAyC,CAAC;AACjE,cAAQ,IAAI,MAAM,KAAK,uCAAuC,CAAC;AAAA,IACjE;AAAA,EACF,CAAC;AAEH,MACG,QAAQ,QAAQ,EAChB,YAAY,0BAA0B,EACtC,OAAO,MAAM;AACZ,UAAM,SAAS,cAAc;AAC7B,WAAO,UAAU;AACjB,kBAAc,MAAM;AACpB,YAAQ,IAAI,MAAM,MAAM,2BAA2B,CAAC;AAEpD,UAAM,WACJ,OAAO,cACP,OAAO,aACP,OAAO,cACP,OAAO;AACT,QAAI,CAAC,UAAU;AACb,cAAQ;AAAA,QACN,MAAM;AAAA,UACJ;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AAEH,MACG,QAAQ,SAAS,EACjB,YAAY,2BAA2B,EACvC,OAAO,MAAM;AACZ,UAAM,SAAS,cAAc;AAC7B,WAAO,UAAU;AACjB,kBAAc,MAAM;AACpB,YAAQ,IAAI,MAAM,OAAO,4BAA4B,CAAC;AAAA,EACxD,CAAC;AAEH,MACG,QAAQ,MAAM,EACd,YAAY,0BAA0B,EACtC,OAAO,YAAY;AAClB,YAAQ,IAAI,MAAM,KAAK,8BAA8B,CAAC;AAEtD,UAAM,SAAS,MAAM,oBAAoB;AAAA,MACvC,MAAM;AAAA,MACN,OAAO;AAAA,MACP,SAAS;AAAA,IACX,CAAC;AAED,QAAI,OAAO,SAAS;AAClB,cAAQ,IAAI,MAAM,MAAM,iCAAiC,CAAC;AAAA,IAC5D,OAAO;AACL,cAAQ,IAAI,MAAM,IAAI,WAAW,OAAO,KAAK,EAAE,CAAC;AAAA,IAClD;AAAA,EACF,CAAC;AAEH,MACG,QAAQ,gBAAgB,EACxB,YAAY,4BAA4B,EACxC,OAAO,uBAAuB,iBAAiB,mBAAmB,EAClE,OAAO,OAAO,SAAiB,YAA+B;AAC7D,UAAM,SAAS,MAAM,oBAAoB;AAAA,MACvC,MAAM;AAAA,MACN,OAAO,QAAQ;AAAA,MACf;AAAA,IACF,CAAC;AAED,QAAI,OAAO,SAAS;AAClB,cAAQ,IAAI,MAAM,MAAM,eAAe,CAAC;AAAA,IAC1C,OAAO;AACL,cAAQ,IAAI,MAAM,IAAI,WAAW,OAAO,KAAK,EAAE,CAAC;AAAA,IAClD;AAAA,EACF,CAAC;AAEH,MACG,QAAQ,gBAAgB,EACxB,YAAY,6CAA6C,EACzD,OAAO,uBAAuB,eAAe,uBAAuB,EACpE;AAAA,IACC;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACC;AAAA,IACC,OAAO,OAAe,YAAkD;AACtE,YAAM,OAAO,QAAQ,QAAQ,MAAM,GAAG,EAAE,IAAI,CAAC,OAAO;AAAA,QAClD,OAAO,EAAE,KAAK;AAAA,MAChB,EAAE;AAEF,cAAQ,IAAI,MAAM,KAAK,gCAAgC,CAAC;AAExD,YAAM,SAAS,MAAM,kBAAkB,OAAO,QAAQ,SAAS,IAAI;AAEnE,UAAI,OAAO,SAAS;AAClB,gBAAQ,IAAI,MAAM,MAAM,2BAA2B,CAAC;AACpD,YAAI,OAAO,UAAU;AACnB,kBAAQ,IAAI,MAAM,KAAK,cAAc,OAAO,QAAQ,EAAE,CAAC;AAAA,QACzD;AAAA,MACF,OAAO;AACL,gBAAQ,IAAI,MAAM,IAAI,WAAW,OAAO,KAAK,EAAE,CAAC;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AAEF,MACG,QAAQ,gBAAgB,EACxB,YAAY,sBAAsB,EAClC,OAAO,uBAAuB,iBAAiB,aAAa,EAC5D,OAAO,OAAO,UAAkB,YAA+B;AAC9D,YAAQ,IAAI,MAAM,KAAK,0BAA0B,CAAC;AAElD,UAAM,SAAS,MAAM,gBAAgB,QAAQ,OAAO,QAAQ;AAE5D,QAAI,OAAO,SAAS;AAClB,cAAQ,IAAI,MAAM,MAAM,cAAc,CAAC;AACvC,UAAI,OAAO,UAAU;AACnB,gBAAQ,IAAI,MAAM,KAAK,cAAc,OAAO,QAAQ,EAAE,CAAC;AAAA,MACzD;AAAA,IACF,OAAO;AACL,cAAQ,IAAI,MAAM,IAAI,WAAW,OAAO,KAAK,EAAE,CAAC;AAAA,IAClD;AAAA,EACF,CAAC;AAEH,MACG,QAAQ,iBAAiB,EACzB,YAAY,mCAAmC,EAC/C,OAAO,wBAAwB,gBAAgB,EAAE,EACjD,OAAO,OAAO,MAAc,YAAiC;AAC5D,UAAM,SAAS,MAAM;AAAA,MACnB;AAAA,MACA,QAAQ,WAAW,SAAS,IAAI;AAAA,IAClC;AAEA,QAAI,OAAO,SAAS;AAClB,cAAQ,IAAI,MAAM,MAAM,+BAA+B,CAAC;AAAA,IAC1D,OAAO;AACL,cAAQ,IAAI,MAAM,IAAI,WAAW,OAAO,KAAK,EAAE,CAAC;AAAA,IAClD;AAAA,EACF,CAAC;AAEH,MACG,QAAQ,OAAO,EACf,YAAY,uBAAuB,EACnC,OAAO,YAAY,oBAAoB,EACvC,OAAO,aAAa,qBAAqB,EACzC,OAAO,kBAAkB,sBAAsB,OAAO,EACtD,OAAO,gBAAgB,oBAAoB,OAAO,EAClD;AAAA,IACC,CAAC,YAKK;AACJ,YAAM,SAAS,cAAc;AAE7B,UAAI,CAAC,OAAO,YAAY;AACtB,eAAO,aAAa,EAAE,SAAS,OAAO,OAAO,SAAS,KAAK,QAAQ;AAAA,MACrE;AAEA,UAAI,QAAQ,QAAQ;AAClB,eAAO,WAAW,UAAU;AAAA,MAC9B,WAAW,QAAQ,SAAS;AAC1B,eAAO,WAAW,UAAU;AAAA,MAC9B;AAEA,UAAI,QAAQ,OAAO;AACjB,eAAO,WAAW,QAAQ,QAAQ;AAAA,MACpC;AACA,UAAI,QAAQ,KAAK;AACf,eAAO,WAAW,MAAM,QAAQ;AAAA,MAClC;AAEA,oBAAc,MAAM;AAEpB,UAAI,OAAO,WAAW,SAAS;AAC7B,gBAAQ;AAAA,UACN,MAAM;AAAA,YACJ,wBAAwB,OAAO,WAAW,KAAK,MAAM,OAAO,WAAW,GAAG;AAAA,UAC5E;AAAA,QACF;AAAA,MACF,OAAO;AACL,gBAAQ,IAAI,MAAM,OAAO,sBAAsB,CAAC;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AAEF,MACG,QAAQ,eAAe,EACvB;AAAA,IACC;AAAA,EACF,EACC,OAAO,CAAC,SAAiB;AACxB,UAAM,SAAS,cAAc;AAC7B,UAAM,aAAa,CAAC,gBAAgB,eAAe,SAAS,QAAQ;AAEpE,QAAI,CAAC,WAAW,SAAS,IAAI,GAAG;AAC9B,cAAQ,IAAI,MAAM,IAAI,sBAAsB,WAAW,KAAK,IAAI,CAAC,EAAE,CAAC;AACpE;AAAA,IACF;AAEA,UAAM,MAAM;AACZ,WAAO,SAAS,GAAG,IAAI,CAAC,OAAO,SAAS,GAAG;AAC3C,kBAAc,MAAM;AAEpB,YAAQ;AAAA,MACN,MAAM;AAAA,QACJ,GAAG,IAAI,kBAAkB,OAAO,SAAS,GAAG,IAAI,YAAY,UAAU;AAAA,MACxE;AAAA,IACF;AAAA,EACF,CAAC;AAEH,MACG,QAAQ,SAAS,EACjB,YAAY,wCAAwC,EACpD,OAAO,MAAM;AACZ,UAAM,SAAS,cAAc;AAE7B,QAAI,OAAO,eAAe,WAAW,GAAG;AACtC,cAAQ,IAAI,MAAM,KAAK,oBAAoB,CAAC;AAC5C;AAAA,IACF;AAEA,YAAQ,IAAI,MAAM,KAAK,kBAAkB,CAAC;AAC1C,WAAO,eAAe,QAAQ,CAAC,MAAM;AACnC,YAAM,UAAU,IAAI,KAAK,EAAE,SAAS;AACpC,YAAM,YAAY,KAAK,OAAO,QAAQ,QAAQ,IAAI,KAAK,IAAI,KAAK,GAAI;AAEpE,cAAQ,IAAI;AACZ,cAAQ,IAAI,KAAK,MAAM,KAAK,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE;AAC5C,cAAQ,IAAI,KAAK,MAAM,KAAK,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE;AAChD,cAAQ;AAAA,QACN,KAAK,MAAM,KAAK,UAAU,CAAC,IAAI,EAAE,QAAQ,UAAU,GAAG,EAAE,CAAC;AAAA,MAC3D;AACA,cAAQ;AAAA,QACN,KAAK,MAAM,KAAK,UAAU,CAAC,IAAI,YAAY,IAAI,GAAG,SAAS,MAAM,MAAM,IAAI,SAAS,CAAC;AAAA,MACvF;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AAEH,MACG,QAAQ,SAAS,EACjB,YAAY,gCAAgC,EAC5C,OAAO,MAAM;AACZ,UAAM,UAAU,sBAAsB;AACtC,YAAQ,IAAI,MAAM,MAAM,WAAW,OAAO,oBAAoB,CAAC;AAAA,EACjE,CAAC;AAEH,MACG,QAAQ,mBAAmB,EAC3B,YAAY,kCAAkC,EAC9C,OAAO,CAAC,YAAoB;AAC3B,UAAM,SAAS,cAAc;AAC7B,UAAM,UAAU,SAAS,SAAS,EAAE;AAEpC,QAAI,MAAM,OAAO,KAAK,UAAU,IAAI;AAClC,cAAQ,IAAI,MAAM,IAAI,qCAAqC,CAAC;AAC5D;AAAA,IACF;AAEA,WAAO,kBAAkB;AACzB,kBAAc,MAAM;AACpB,YAAQ,IAAI,MAAM,MAAM,2BAA2B,OAAO,UAAU,CAAC;AAAA,EACvE,CAAC;AAEH,SAAO;AACT;AAEA,SAAS,UAAU,OAAuB;AACxC,MAAI,MAAM,SAAS,EAAG,QAAO;AAC7B,SAAO,MAAM,UAAU,GAAG,CAAC,IAAI,SAAS,MAAM,UAAU,MAAM,SAAS,CAAC;AAC1E;",
|
|
4
|
+
"sourcesContent": ["/**\n * CLI command for SMS notification management\n */\n\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport { execSync } from 'child_process';\nimport { join } from 'path';\nimport {\n loadSMSConfig,\n saveSMSConfig,\n sendNotification,\n sendSMSNotification,\n notifyReviewReady,\n notifyWithYesNo,\n notifyTaskComplete,\n cleanupExpiredPrompts,\n type MessageChannel,\n} from '../../hooks/sms-notify.js';\nimport {\n loadActionQueue,\n processAllPendingActions,\n cleanupOldActions,\n startActionWatcher,\n} from '../../hooks/sms-action-runner.js';\n\n// __dirname provided by esbuild banner\n\nexport function createSMSNotifyCommand(): Command {\n const cmd = new Command('notify')\n .description(\n 'SMS notification system for review alerts (optional, requires Twilio)'\n )\n .addHelpText(\n 'after',\n `\nSetup (optional):\n 1. Create Twilio account at https://twilio.com\n 2. Get Account SID, Auth Token, and phone numbers\n 3. Set environment variables:\n export TWILIO_ACCOUNT_SID=your_sid\n export TWILIO_AUTH_TOKEN=your_token\n\n For WhatsApp (recommended - cheaper for conversations):\n export TWILIO_WHATSAPP_FROM=+1234567890\n export TWILIO_WHATSAPP_TO=+1234567890\n export TWILIO_CHANNEL=whatsapp\n\n For SMS:\n export TWILIO_SMS_FROM=+1234567890\n export TWILIO_SMS_TO=+1234567890\n export TWILIO_CHANNEL=sms\n\n Legacy (works for both, defaults to WhatsApp):\n export TWILIO_FROM_NUMBER=+1234567890\n export TWILIO_TO_NUMBER=+1234567890\n\n 4. Enable: stackmemory notify enable\n\nExamples:\n stackmemory notify status Check configuration\n stackmemory notify enable Enable notifications\n stackmemory notify channel whatsapp Switch to WhatsApp\n stackmemory notify channel sms Switch to SMS\n stackmemory notify test Send test message\n stackmemory notify send \"PR ready\" Send custom message\n stackmemory notify review \"PR #123\" Send review notification with options\n stackmemory notify ask \"Deploy?\" Send yes/no prompt\n`\n );\n\n cmd\n .command('status')\n .description('Show notification configuration status')\n .action(() => {\n const config = loadSMSConfig();\n\n console.log(chalk.blue('Notification Status:'));\n console.log();\n\n // Check credentials\n const hasCreds = config.accountSid && config.authToken;\n\n // Check channel-specific numbers\n const channel = config.channel || 'whatsapp';\n const hasWhatsApp =\n config.whatsappFromNumber ||\n config.fromNumber ||\n config.whatsappToNumber ||\n config.toNumber;\n const hasSMS =\n config.smsFromNumber ||\n config.fromNumber ||\n config.smsToNumber ||\n config.toNumber;\n const hasNumbers = channel === 'whatsapp' ? hasWhatsApp : hasSMS;\n\n console.log(\n ` ${chalk.gray('Enabled:')} ${config.enabled ? chalk.green('yes') : chalk.red('no')}`\n );\n console.log(\n ` ${chalk.gray('Channel:')} ${channel === 'whatsapp' ? chalk.cyan('WhatsApp') : chalk.blue('SMS')}`\n );\n console.log(\n ` ${chalk.gray('Configured:')} ${hasCreds && hasNumbers ? chalk.green('yes') : chalk.yellow('no (set env vars)')}`\n );\n\n // Show channel-specific numbers\n console.log();\n console.log(chalk.blue('Numbers:'));\n if (channel === 'whatsapp') {\n const from = config.whatsappFromNumber || config.fromNumber;\n const to = config.whatsappToNumber || config.toNumber;\n if (from) {\n console.log(` ${chalk.gray('WhatsApp From:')} ${maskPhone(from)}`);\n }\n if (to) {\n console.log(` ${chalk.gray('WhatsApp To:')} ${maskPhone(to)}`);\n }\n } else {\n const from = config.smsFromNumber || config.fromNumber;\n const to = config.smsToNumber || config.toNumber;\n if (from) {\n console.log(` ${chalk.gray('SMS From:')} ${maskPhone(from)}`);\n }\n if (to) {\n console.log(` ${chalk.gray('SMS To:')} ${maskPhone(to)}`);\n }\n }\n\n console.log();\n console.log(chalk.blue('Notify On:'));\n console.log(\n ` ${chalk.gray('Task Complete:')} ${config.notifyOn.taskComplete ? 'yes' : 'no'}`\n );\n console.log(\n ` ${chalk.gray('Review Ready:')} ${config.notifyOn.reviewReady ? 'yes' : 'no'}`\n );\n console.log(\n ` ${chalk.gray('Errors:')} ${config.notifyOn.error ? 'yes' : 'no'}`\n );\n\n if (config.quietHours?.enabled) {\n console.log();\n console.log(\n chalk.blue(\n `Quiet Hours: ${config.quietHours.start} - ${config.quietHours.end}`\n )\n );\n }\n\n console.log();\n console.log(\n ` ${chalk.gray('Pending Prompts:')} ${config.pendingPrompts.length}`\n );\n console.log(\n ` ${chalk.gray('Response Timeout:')} ${config.responseTimeout}s`\n );\n\n if (!hasCreds || !hasNumbers) {\n console.log();\n console.log(\n chalk.yellow('To configure, set these environment variables:')\n );\n console.log(chalk.gray(' export TWILIO_ACCOUNT_SID=your_sid'));\n console.log(chalk.gray(' export TWILIO_AUTH_TOKEN=your_token'));\n console.log();\n console.log(chalk.gray(' For WhatsApp (recommended):'));\n console.log(chalk.gray(' export TWILIO_WHATSAPP_FROM=+1234567890'));\n console.log(chalk.gray(' export TWILIO_WHATSAPP_TO=+1234567890'));\n console.log();\n console.log(chalk.gray(' For SMS:'));\n console.log(chalk.gray(' export TWILIO_SMS_FROM=+1234567890'));\n console.log(chalk.gray(' export TWILIO_SMS_TO=+1234567890'));\n }\n });\n\n cmd\n .command('enable')\n .description('Enable SMS notifications')\n .action(() => {\n const config = loadSMSConfig();\n config.enabled = true;\n saveSMSConfig(config);\n console.log(chalk.green('SMS notifications enabled'));\n\n const hasCreds =\n config.accountSid &&\n config.authToken &&\n config.fromNumber &&\n config.toNumber;\n if (!hasCreds) {\n console.log(\n chalk.yellow(\n 'Note: Set Twilio environment variables to send messages'\n )\n );\n }\n });\n\n cmd\n .command('disable')\n .description('Disable SMS notifications')\n .action(() => {\n const config = loadSMSConfig();\n config.enabled = false;\n saveSMSConfig(config);\n console.log(chalk.yellow('SMS notifications disabled'));\n });\n\n cmd\n .command('channel <type>')\n .description('Set notification channel (whatsapp|sms)')\n .action((type: string) => {\n const validChannels: MessageChannel[] = ['whatsapp', 'sms'];\n const channel = type.toLowerCase() as MessageChannel;\n\n if (!validChannels.includes(channel)) {\n console.log(\n chalk.red(`Invalid channel. Use: ${validChannels.join(', ')}`)\n );\n return;\n }\n\n const config = loadSMSConfig();\n config.channel = channel;\n saveSMSConfig(config);\n\n const label = channel === 'whatsapp' ? 'WhatsApp' : 'SMS';\n console.log(chalk.green(`Notification channel set to ${label}`));\n\n // Show relevant env vars\n if (channel === 'whatsapp') {\n const hasNumbers = config.whatsappFromNumber || config.fromNumber;\n if (!hasNumbers) {\n console.log(\n chalk.yellow('Set TWILIO_WHATSAPP_FROM and TWILIO_WHATSAPP_TO')\n );\n }\n } else {\n const hasNumbers = config.smsFromNumber || config.fromNumber;\n if (!hasNumbers) {\n console.log(chalk.yellow('Set TWILIO_SMS_FROM and TWILIO_SMS_TO'));\n }\n }\n });\n\n cmd\n .command('test')\n .description('Send a test notification')\n .option('--sms', 'Force SMS channel')\n .option('--whatsapp', 'Force WhatsApp channel')\n .action(async (options: { sms?: boolean; whatsapp?: boolean }) => {\n const config = loadSMSConfig();\n const channelOverride: MessageChannel | undefined = options.sms\n ? 'sms'\n : options.whatsapp\n ? 'whatsapp'\n : undefined;\n const channelLabel =\n channelOverride || config.channel === 'whatsapp' ? 'WhatsApp' : 'SMS';\n\n console.log(\n chalk.blue(`Sending test notification via ${channelLabel}...`)\n );\n\n const result = await sendNotification(\n {\n type: 'custom',\n title: 'StackMemory Test',\n message: 'This is a test notification from StackMemory.',\n },\n channelOverride\n );\n\n if (result.success) {\n const usedChannel = result.channel === 'whatsapp' ? 'WhatsApp' : 'SMS';\n console.log(chalk.green(`Test message sent via ${usedChannel}!`));\n } else {\n console.log(chalk.red(`Failed: ${result.error}`));\n }\n });\n\n cmd\n .command('send <message>')\n .description('Send a custom notification')\n .option('-t, --title <title>', 'Message title', 'StackMemory Alert')\n .action(async (message: string, options: { title: string }) => {\n const result = await sendSMSNotification({\n type: 'custom',\n title: options.title,\n message,\n });\n\n if (result.success) {\n console.log(chalk.green('Message sent!'));\n } else {\n console.log(chalk.red(`Failed: ${result.error}`));\n }\n });\n\n cmd\n .command('review <title>')\n .description('Send review-ready notification with options')\n .option('-m, --message <msg>', 'Description', 'Ready for your review')\n .option(\n '-o, --options <opts>',\n 'Comma-separated options',\n 'Approve,Request Changes,Skip'\n )\n .action(\n async (title: string, options: { message: string; options: string }) => {\n const opts = options.options.split(',').map((o) => ({\n label: o.trim(),\n }));\n\n console.log(chalk.blue('Sending review notification...'));\n\n const result = await notifyReviewReady(title, options.message, opts);\n\n if (result.success) {\n console.log(chalk.green('Review notification sent!'));\n if (result.promptId) {\n console.log(chalk.gray(`Prompt ID: ${result.promptId}`));\n }\n } else {\n console.log(chalk.red(`Failed: ${result.error}`));\n }\n }\n );\n\n cmd\n .command('ask <question>')\n .description('Send a yes/no prompt')\n .option('-t, --title <title>', 'Message title', 'StackMemory')\n .action(async (question: string, options: { title: string }) => {\n console.log(chalk.blue('Sending yes/no prompt...'));\n\n const result = await notifyWithYesNo(options.title, question);\n\n if (result.success) {\n console.log(chalk.green('Prompt sent!'));\n if (result.promptId) {\n console.log(chalk.gray(`Prompt ID: ${result.promptId}`));\n }\n } else {\n console.log(chalk.red(`Failed: ${result.error}`));\n }\n });\n\n cmd\n .command('complete <task>')\n .description('Send task completion notification')\n .option('-s, --summary <text>', 'Task summary', '')\n .action(async (task: string, options: { summary: string }) => {\n const result = await notifyTaskComplete(\n task,\n options.summary || `Task \"${task}\" has been completed.`\n );\n\n if (result.success) {\n console.log(chalk.green('Completion notification sent!'));\n } else {\n console.log(chalk.red(`Failed: ${result.error}`));\n }\n });\n\n cmd\n .command('quiet')\n .description('Configure quiet hours')\n .option('--enable', 'Enable quiet hours')\n .option('--disable', 'Disable quiet hours')\n .option('--start <time>', 'Start time (HH:MM)', '22:00')\n .option('--end <time>', 'End time (HH:MM)', '08:00')\n .action(\n (options: {\n enable?: boolean;\n disable?: boolean;\n start: string;\n end: string;\n }) => {\n const config = loadSMSConfig();\n\n if (!config.quietHours) {\n config.quietHours = { enabled: false, start: '22:00', end: '08:00' };\n }\n\n if (options.enable) {\n config.quietHours.enabled = true;\n } else if (options.disable) {\n config.quietHours.enabled = false;\n }\n\n if (options.start) {\n config.quietHours.start = options.start;\n }\n if (options.end) {\n config.quietHours.end = options.end;\n }\n\n saveSMSConfig(config);\n\n if (config.quietHours.enabled) {\n console.log(\n chalk.green(\n `Quiet hours enabled: ${config.quietHours.start} - ${config.quietHours.end}`\n )\n );\n } else {\n console.log(chalk.yellow('Quiet hours disabled'));\n }\n }\n );\n\n cmd\n .command('toggle <type>')\n .description(\n 'Toggle notification type (taskComplete|reviewReady|error|custom)'\n )\n .action((type: string) => {\n const config = loadSMSConfig();\n const validTypes = ['taskComplete', 'reviewReady', 'error', 'custom'];\n\n if (!validTypes.includes(type)) {\n console.log(chalk.red(`Invalid type. Use: ${validTypes.join(', ')}`));\n return;\n }\n\n const key = type as keyof typeof config.notifyOn;\n config.notifyOn[key] = !config.notifyOn[key];\n saveSMSConfig(config);\n\n console.log(\n chalk.green(\n `${type} notifications ${config.notifyOn[key] ? 'enabled' : 'disabled'}`\n )\n );\n });\n\n cmd\n .command('pending')\n .description('List pending prompts awaiting response')\n .action(() => {\n const config = loadSMSConfig();\n\n if (config.pendingPrompts.length === 0) {\n console.log(chalk.gray('No pending prompts'));\n return;\n }\n\n console.log(chalk.blue('Pending Prompts:'));\n config.pendingPrompts.forEach((p) => {\n const expires = new Date(p.expiresAt);\n const remaining = Math.round((expires.getTime() - Date.now()) / 1000);\n\n console.log();\n console.log(` ${chalk.gray('ID:')} ${p.id}`);\n console.log(` ${chalk.gray('Type:')} ${p.type}`);\n console.log(\n ` ${chalk.gray('Message:')} ${p.message.substring(0, 50)}...`\n );\n console.log(\n ` ${chalk.gray('Expires:')} ${remaining > 0 ? `${remaining}s` : chalk.red('expired')}`\n );\n });\n });\n\n cmd\n .command('cleanup')\n .description('Remove expired pending prompts')\n .action(() => {\n const removed = cleanupExpiredPrompts();\n console.log(chalk.green(`Removed ${removed} expired prompt(s)`));\n });\n\n cmd\n .command('timeout <seconds>')\n .description('Set response timeout for prompts')\n .action((seconds: string) => {\n const config = loadSMSConfig();\n const timeout = parseInt(seconds, 10);\n\n if (isNaN(timeout) || timeout < 30) {\n console.log(chalk.red('Timeout must be at least 30 seconds'));\n return;\n }\n\n config.responseTimeout = timeout;\n saveSMSConfig(config);\n console.log(chalk.green(`Response timeout set to ${timeout} seconds`));\n });\n\n // Action queue commands\n cmd\n .command('actions')\n .description('List queued actions from SMS responses')\n .action(() => {\n const queue = loadActionQueue();\n\n if (queue.actions.length === 0) {\n console.log(chalk.gray('No actions in queue'));\n return;\n }\n\n console.log(chalk.blue('Action Queue:'));\n queue.actions.forEach((a) => {\n const statusColor =\n a.status === 'completed'\n ? chalk.green\n : a.status === 'failed'\n ? chalk.red\n : a.status === 'running'\n ? chalk.yellow\n : chalk.gray;\n\n console.log();\n console.log(` ${chalk.gray('ID:')} ${a.id}`);\n console.log(` ${chalk.gray('Status:')} ${statusColor(a.status)}`);\n console.log(\n ` ${chalk.gray('Action:')} ${a.action.substring(0, 60)}...`\n );\n console.log(` ${chalk.gray('Response:')} ${a.response}`);\n if (a.error) {\n console.log(` ${chalk.gray('Error:')} ${chalk.red(a.error)}`);\n }\n });\n });\n\n cmd\n .command('run-actions')\n .description('Execute all pending actions from SMS responses')\n .action(() => {\n console.log(chalk.blue('Processing pending actions...'));\n const result = processAllPendingActions();\n\n console.log(\n chalk.green(\n `Processed ${result.processed} action(s): ${result.succeeded} succeeded, ${result.failed} failed`\n )\n );\n });\n\n cmd\n .command('watch')\n .description('Watch for and execute SMS response actions')\n .option('-i, --interval <ms>', 'Check interval in milliseconds', '5000')\n .action((options: { interval: string }) => {\n const interval = parseInt(options.interval, 10);\n console.log(chalk.blue(`Watching for actions (interval: ${interval}ms)`));\n console.log(chalk.gray('Press Ctrl+C to stop'));\n\n startActionWatcher(interval);\n });\n\n cmd\n .command('cleanup-actions')\n .description('Remove old completed actions')\n .action(() => {\n const removed = cleanupOldActions();\n console.log(chalk.green(`Removed ${removed} old action(s)`));\n });\n\n // Hook installation commands\n cmd\n .command('install-hook')\n .description('Install Claude Code notification hook')\n .action(() => {\n try {\n const scriptPath = join(\n __dirname,\n '../../../scripts/install-notify-hook.sh'\n );\n execSync(`bash \"${scriptPath}\"`, { stdio: 'inherit' });\n } catch {\n console.error(chalk.red('Failed to install hook'));\n }\n });\n\n cmd\n .command('install-response-hook')\n .description('Install Claude Code response handler hook')\n .action(() => {\n try {\n // Create install script inline\n const hooksDir = join(process.env['HOME'] || '~', '.claude', 'hooks');\n const hookSrc = join(\n __dirname,\n '../../../templates/claude-hooks/sms-response-handler.js'\n );\n const hookDest = join(hooksDir, 'sms-response-handler.js');\n\n execSync(`mkdir -p \"${hooksDir}\"`, { stdio: 'inherit' });\n execSync(`cp \"${hookSrc}\" \"${hookDest}\"`, { stdio: 'inherit' });\n execSync(`chmod +x \"${hookDest}\"`, { stdio: 'inherit' });\n\n console.log(chalk.green('Response handler hook installed!'));\n console.log(chalk.gray(`Location: ${hookDest}`));\n console.log();\n console.log(chalk.blue('Add to ~/.claude/settings.json:'));\n console.log(\n chalk.gray(` \"hooks\": { \"pre_tool_use\": [\"node ${hookDest}\"] }`)\n );\n } catch {\n console.error(chalk.red('Failed to install response hook'));\n }\n });\n\n cmd\n .command('webhook')\n .description('Start SMS webhook server for receiving responses')\n .option('-p, --port <port>', 'Port to listen on', '3456')\n .action(async (options: { port: string }) => {\n const { startWebhookServer } = await import('../../hooks/sms-webhook.js');\n const port = parseInt(options.port, 10);\n startWebhookServer(port);\n });\n\n return cmd;\n}\n\nfunction maskPhone(phone: string): string {\n if (phone.length < 8) return phone;\n return phone.substring(0, 4) + '****' + phone.substring(phone.length - 2);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/hooks/index.js
CHANGED
package/dist/hooks/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/hooks/index.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * StackMemory Hooks Module\n * User-configurable hook system for automation and suggestions\n */\n\nexport * from './events.js';\nexport * from './config.js';\nexport * from './daemon.js';\nexport * from './auto-background.js';\nexport * from './sms-notify.js';\nexport * from './sms-webhook.js';\n"],
|
|
5
|
-
"mappings": ";;;;AAKA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;",
|
|
4
|
+
"sourcesContent": ["/**\n * StackMemory Hooks Module\n * User-configurable hook system for automation and suggestions\n */\n\nexport * from './events.js';\nexport * from './config.js';\nexport * from './daemon.js';\nexport * from './auto-background.js';\nexport * from './sms-notify.js';\nexport * from './sms-webhook.js';\nexport * from './sms-action-runner.js';\n"],
|
|
5
|
+
"mappings": ";;;;AAKA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
const QUEUE_PATH = join(homedir(), ".stackmemory", "sms-action-queue.json");
|
|
10
|
+
function loadActionQueue() {
|
|
11
|
+
try {
|
|
12
|
+
if (existsSync(QUEUE_PATH)) {
|
|
13
|
+
return JSON.parse(readFileSync(QUEUE_PATH, "utf8"));
|
|
14
|
+
}
|
|
15
|
+
} catch {
|
|
16
|
+
}
|
|
17
|
+
return { actions: [], lastChecked: (/* @__PURE__ */ new Date()).toISOString() };
|
|
18
|
+
}
|
|
19
|
+
function saveActionQueue(queue) {
|
|
20
|
+
try {
|
|
21
|
+
const dir = join(homedir(), ".stackmemory");
|
|
22
|
+
if (!existsSync(dir)) {
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
writeFileSync(QUEUE_PATH, JSON.stringify(queue, null, 2));
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function queueAction(promptId, response, action) {
|
|
30
|
+
const queue = loadActionQueue();
|
|
31
|
+
const id = Math.random().toString(36).substring(2, 10);
|
|
32
|
+
queue.actions.push({
|
|
33
|
+
id,
|
|
34
|
+
promptId,
|
|
35
|
+
response,
|
|
36
|
+
action,
|
|
37
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
38
|
+
status: "pending"
|
|
39
|
+
});
|
|
40
|
+
saveActionQueue(queue);
|
|
41
|
+
return id;
|
|
42
|
+
}
|
|
43
|
+
function getPendingActions() {
|
|
44
|
+
const queue = loadActionQueue();
|
|
45
|
+
return queue.actions.filter((a) => a.status === "pending");
|
|
46
|
+
}
|
|
47
|
+
function markActionRunning(id) {
|
|
48
|
+
const queue = loadActionQueue();
|
|
49
|
+
const action = queue.actions.find((a) => a.id === id);
|
|
50
|
+
if (action) {
|
|
51
|
+
action.status = "running";
|
|
52
|
+
saveActionQueue(queue);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function markActionCompleted(id, result, error) {
|
|
56
|
+
const queue = loadActionQueue();
|
|
57
|
+
const action = queue.actions.find((a) => a.id === id);
|
|
58
|
+
if (action) {
|
|
59
|
+
action.status = error ? "failed" : "completed";
|
|
60
|
+
action.result = result;
|
|
61
|
+
action.error = error;
|
|
62
|
+
saveActionQueue(queue);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function executeAction(action) {
|
|
66
|
+
markActionRunning(action.id);
|
|
67
|
+
try {
|
|
68
|
+
console.log(`[sms-action] Executing: ${action.action}`);
|
|
69
|
+
const output = execSync(action.action, {
|
|
70
|
+
encoding: "utf8",
|
|
71
|
+
timeout: 6e4,
|
|
72
|
+
// 1 minute timeout
|
|
73
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
74
|
+
});
|
|
75
|
+
markActionCompleted(action.id, output);
|
|
76
|
+
return { success: true, output };
|
|
77
|
+
} catch (err) {
|
|
78
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
79
|
+
markActionCompleted(action.id, void 0, error);
|
|
80
|
+
return { success: false, error };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function processAllPendingActions() {
|
|
84
|
+
const pending = getPendingActions();
|
|
85
|
+
let succeeded = 0;
|
|
86
|
+
let failed = 0;
|
|
87
|
+
for (const action of pending) {
|
|
88
|
+
const result = executeAction(action);
|
|
89
|
+
if (result.success) {
|
|
90
|
+
succeeded++;
|
|
91
|
+
} else {
|
|
92
|
+
failed++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { processed: pending.length, succeeded, failed };
|
|
96
|
+
}
|
|
97
|
+
function cleanupOldActions() {
|
|
98
|
+
const queue = loadActionQueue();
|
|
99
|
+
const completed = queue.actions.filter(
|
|
100
|
+
(a) => a.status === "completed" || a.status === "failed"
|
|
101
|
+
);
|
|
102
|
+
if (completed.length > 50) {
|
|
103
|
+
const toRemove = completed.slice(0, completed.length - 50);
|
|
104
|
+
queue.actions = queue.actions.filter(
|
|
105
|
+
(a) => !toRemove.find((r) => r.id === a.id)
|
|
106
|
+
);
|
|
107
|
+
saveActionQueue(queue);
|
|
108
|
+
return toRemove.length;
|
|
109
|
+
}
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
const ACTION_TEMPLATES = {
|
|
113
|
+
// Git/PR actions
|
|
114
|
+
approvePR: (prNumber) => `gh pr review ${prNumber} --approve && gh pr merge ${prNumber} --auto`,
|
|
115
|
+
requestChanges: (prNumber) => `gh pr review ${prNumber} --request-changes -b "Changes requested via SMS"`,
|
|
116
|
+
mergePR: (prNumber) => `gh pr merge ${prNumber} --squash`,
|
|
117
|
+
closePR: (prNumber) => `gh pr close ${prNumber}`,
|
|
118
|
+
// Deployment actions
|
|
119
|
+
deploy: (env = "production") => `npm run deploy:${env}`,
|
|
120
|
+
rollback: (env = "production") => `npm run rollback:${env}`,
|
|
121
|
+
verifyDeployment: (url) => `curl -sf ${url}/health || exit 1`,
|
|
122
|
+
// Build actions
|
|
123
|
+
rebuild: () => `npm run build`,
|
|
124
|
+
retest: () => `npm test`,
|
|
125
|
+
lint: () => `npm run lint:fix`,
|
|
126
|
+
// Notification actions
|
|
127
|
+
notifySlack: (message) => `curl -X POST $SLACK_WEBHOOK -d '{"text":"${message}"}'`,
|
|
128
|
+
notifyTeam: (message) => `stackmemory notify send "${message}" --title "Team Alert"`
|
|
129
|
+
};
|
|
130
|
+
function createAction(template, ...args) {
|
|
131
|
+
const fn = ACTION_TEMPLATES[template];
|
|
132
|
+
if (typeof fn === "function") {
|
|
133
|
+
return fn(...args);
|
|
134
|
+
}
|
|
135
|
+
return fn;
|
|
136
|
+
}
|
|
137
|
+
function startActionWatcher(intervalMs = 5e3) {
|
|
138
|
+
console.log(
|
|
139
|
+
`[sms-action] Starting action watcher (interval: ${intervalMs}ms)`
|
|
140
|
+
);
|
|
141
|
+
return setInterval(() => {
|
|
142
|
+
const pending = getPendingActions();
|
|
143
|
+
if (pending.length > 0) {
|
|
144
|
+
console.log(`[sms-action] Found ${pending.length} pending action(s)`);
|
|
145
|
+
processAllPendingActions();
|
|
146
|
+
}
|
|
147
|
+
}, intervalMs);
|
|
148
|
+
}
|
|
149
|
+
function handleSMSResponse(promptId, response, action) {
|
|
150
|
+
if (action) {
|
|
151
|
+
const actionId = queueAction(promptId, response, action);
|
|
152
|
+
console.log(`[sms-action] Queued action ${actionId}: ${action}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export {
|
|
156
|
+
ACTION_TEMPLATES,
|
|
157
|
+
cleanupOldActions,
|
|
158
|
+
createAction,
|
|
159
|
+
executeAction,
|
|
160
|
+
getPendingActions,
|
|
161
|
+
handleSMSResponse,
|
|
162
|
+
loadActionQueue,
|
|
163
|
+
markActionCompleted,
|
|
164
|
+
markActionRunning,
|
|
165
|
+
processAllPendingActions,
|
|
166
|
+
queueAction,
|
|
167
|
+
saveActionQueue,
|
|
168
|
+
startActionWatcher
|
|
169
|
+
};
|
|
170
|
+
//# sourceMappingURL=sms-action-runner.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/hooks/sms-action-runner.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * SMS Action Runner - Executes actions based on SMS responses\n * Bridges SMS responses to Claude Code actions\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { execSync } from 'child_process';\n\nexport interface PendingAction {\n id: string;\n promptId: string;\n response: string;\n action: string;\n timestamp: string;\n status: 'pending' | 'running' | 'completed' | 'failed';\n result?: string;\n error?: string;\n}\n\nexport interface ActionQueue {\n actions: PendingAction[];\n lastChecked: string;\n}\n\nconst QUEUE_PATH = join(homedir(), '.stackmemory', 'sms-action-queue.json');\n\nexport function loadActionQueue(): ActionQueue {\n try {\n if (existsSync(QUEUE_PATH)) {\n return JSON.parse(readFileSync(QUEUE_PATH, 'utf8'));\n }\n } catch {\n // Use defaults\n }\n return { actions: [], lastChecked: new Date().toISOString() };\n}\n\nexport function saveActionQueue(queue: ActionQueue): void {\n try {\n const dir = join(homedir(), '.stackmemory');\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(QUEUE_PATH, JSON.stringify(queue, null, 2));\n } catch {\n // Silently fail\n }\n}\n\nexport function queueAction(\n promptId: string,\n response: string,\n action: string\n): string {\n const queue = loadActionQueue();\n const id = Math.random().toString(36).substring(2, 10);\n\n queue.actions.push({\n id,\n promptId,\n response,\n action,\n timestamp: new Date().toISOString(),\n status: 'pending',\n });\n\n saveActionQueue(queue);\n return id;\n}\n\nexport function getPendingActions(): PendingAction[] {\n const queue = loadActionQueue();\n return queue.actions.filter((a) => a.status === 'pending');\n}\n\nexport function markActionRunning(id: string): void {\n const queue = loadActionQueue();\n const action = queue.actions.find((a) => a.id === id);\n if (action) {\n action.status = 'running';\n saveActionQueue(queue);\n }\n}\n\nexport function markActionCompleted(\n id: string,\n result?: string,\n error?: string\n): void {\n const queue = loadActionQueue();\n const action = queue.actions.find((a) => a.id === id);\n if (action) {\n action.status = error ? 'failed' : 'completed';\n action.result = result;\n action.error = error;\n saveActionQueue(queue);\n }\n}\n\nexport function executeAction(action: PendingAction): {\n success: boolean;\n output?: string;\n error?: string;\n} {\n markActionRunning(action.id);\n\n try {\n console.log(`[sms-action] Executing: ${action.action}`);\n\n // Execute the action\n const output = execSync(action.action, {\n encoding: 'utf8',\n timeout: 60000, // 1 minute timeout\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n\n markActionCompleted(action.id, output);\n return { success: true, output };\n } catch (err) {\n const error = err instanceof Error ? err.message : String(err);\n markActionCompleted(action.id, undefined, error);\n return { success: false, error };\n }\n}\n\nexport function processAllPendingActions(): {\n processed: number;\n succeeded: number;\n failed: number;\n} {\n const pending = getPendingActions();\n let succeeded = 0;\n let failed = 0;\n\n for (const action of pending) {\n const result = executeAction(action);\n if (result.success) {\n succeeded++;\n } else {\n failed++;\n }\n }\n\n return { processed: pending.length, succeeded, failed };\n}\n\n// Clean up old completed actions (keep last 50)\nexport function cleanupOldActions(): number {\n const queue = loadActionQueue();\n const completed = queue.actions.filter(\n (a) => a.status === 'completed' || a.status === 'failed'\n );\n\n if (completed.length > 50) {\n const toRemove = completed.slice(0, completed.length - 50);\n queue.actions = queue.actions.filter(\n (a) => !toRemove.find((r) => r.id === a.id)\n );\n saveActionQueue(queue);\n return toRemove.length;\n }\n\n return 0;\n}\n\n/**\n * Action Templates - Common actions for SMS responses\n */\nexport const ACTION_TEMPLATES = {\n // Git/PR actions\n approvePR: (prNumber: string) =>\n `gh pr review ${prNumber} --approve && gh pr merge ${prNumber} --auto`,\n requestChanges: (prNumber: string) =>\n `gh pr review ${prNumber} --request-changes -b \"Changes requested via SMS\"`,\n mergePR: (prNumber: string) => `gh pr merge ${prNumber} --squash`,\n closePR: (prNumber: string) => `gh pr close ${prNumber}`,\n\n // Deployment actions\n deploy: (env: string = 'production') => `npm run deploy:${env}`,\n rollback: (env: string = 'production') => `npm run rollback:${env}`,\n verifyDeployment: (url: string) => `curl -sf ${url}/health || exit 1`,\n\n // Build actions\n rebuild: () => `npm run build`,\n retest: () => `npm test`,\n lint: () => `npm run lint:fix`,\n\n // Notification actions\n notifySlack: (message: string) =>\n `curl -X POST $SLACK_WEBHOOK -d '{\"text\":\"${message}\"}'`,\n notifyTeam: (message: string) =>\n `stackmemory notify send \"${message}\" --title \"Team Alert\"`,\n};\n\n/**\n * Create action string from template\n */\nexport function createAction(\n template: keyof typeof ACTION_TEMPLATES,\n ...args: string[]\n): string {\n const fn = ACTION_TEMPLATES[template];\n if (typeof fn === 'function') {\n return (fn as (...args: string[]) => string)(...args);\n }\n return fn;\n}\n\n/**\n * Watch for new actions and execute them\n */\nexport function startActionWatcher(intervalMs: number = 5000): NodeJS.Timeout {\n console.log(\n `[sms-action] Starting action watcher (interval: ${intervalMs}ms)`\n );\n\n return setInterval(() => {\n const pending = getPendingActions();\n if (pending.length > 0) {\n console.log(`[sms-action] Found ${pending.length} pending action(s)`);\n processAllPendingActions();\n }\n }, intervalMs);\n}\n\n/**\n * Integration with SMS webhook - queue action when response received\n */\nexport function handleSMSResponse(\n promptId: string,\n response: string,\n action?: string\n): void {\n if (action) {\n const actionId = queueAction(promptId, response, action);\n console.log(`[sms-action] Queued action ${actionId}: ${action}`);\n }\n}\n"],
|
|
5
|
+
"mappings": ";;;;AAKA,SAAS,YAAY,cAAc,eAAe,iBAAiB;AACnE,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,gBAAgB;AAkBzB,MAAM,aAAa,KAAK,QAAQ,GAAG,gBAAgB,uBAAuB;AAEnE,SAAS,kBAA+B;AAC7C,MAAI;AACF,QAAI,WAAW,UAAU,GAAG;AAC1B,aAAO,KAAK,MAAM,aAAa,YAAY,MAAM,CAAC;AAAA,IACpD;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO,EAAE,SAAS,CAAC,GAAG,cAAa,oBAAI,KAAK,GAAE,YAAY,EAAE;AAC9D;AAEO,SAAS,gBAAgB,OAA0B;AACxD,MAAI;AACF,UAAM,MAAM,KAAK,QAAQ,GAAG,cAAc;AAC1C,QAAI,CAAC,WAAW,GAAG,GAAG;AACpB,gBAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,IACpC;AACA,kBAAc,YAAY,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AAAA,EAC1D,QAAQ;AAAA,EAER;AACF;AAEO,SAAS,YACd,UACA,UACA,QACQ;AACR,QAAM,QAAQ,gBAAgB;AAC9B,QAAM,KAAK,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,UAAU,GAAG,EAAE;AAErD,QAAM,QAAQ,KAAK;AAAA,IACjB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC,QAAQ;AAAA,EACV,CAAC;AAED,kBAAgB,KAAK;AACrB,SAAO;AACT;AAEO,SAAS,oBAAqC;AACnD,QAAM,QAAQ,gBAAgB;AAC9B,SAAO,MAAM,QAAQ,OAAO,CAAC,MAAM,EAAE,WAAW,SAAS;AAC3D;AAEO,SAAS,kBAAkB,IAAkB;AAClD,QAAM,QAAQ,gBAAgB;AAC9B,QAAM,SAAS,MAAM,QAAQ,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE;AACpD,MAAI,QAAQ;AACV,WAAO,SAAS;AAChB,oBAAgB,KAAK;AAAA,EACvB;AACF;AAEO,SAAS,oBACd,IACA,QACA,OACM;AACN,QAAM,QAAQ,gBAAgB;AAC9B,QAAM,SAAS,MAAM,QAAQ,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE;AACpD,MAAI,QAAQ;AACV,WAAO,SAAS,QAAQ,WAAW;AACnC,WAAO,SAAS;AAChB,WAAO,QAAQ;AACf,oBAAgB,KAAK;AAAA,EACvB;AACF;AAEO,SAAS,cAAc,QAI5B;AACA,oBAAkB,OAAO,EAAE;AAE3B,MAAI;AACF,YAAQ,IAAI,2BAA2B,OAAO,MAAM,EAAE;AAGtD,UAAM,SAAS,SAAS,OAAO,QAAQ;AAAA,MACrC,UAAU;AAAA,MACV,SAAS;AAAA;AAAA,MACT,OAAO,CAAC,QAAQ,QAAQ,MAAM;AAAA,IAChC,CAAC;AAED,wBAAoB,OAAO,IAAI,MAAM;AACrC,WAAO,EAAE,SAAS,MAAM,OAAO;AAAA,EACjC,SAAS,KAAK;AACZ,UAAM,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC7D,wBAAoB,OAAO,IAAI,QAAW,KAAK;AAC/C,WAAO,EAAE,SAAS,OAAO,MAAM;AAAA,EACjC;AACF;AAEO,SAAS,2BAId;AACA,QAAM,UAAU,kBAAkB;AAClC,MAAI,YAAY;AAChB,MAAI,SAAS;AAEb,aAAW,UAAU,SAAS;AAC5B,UAAM,SAAS,cAAc,MAAM;AACnC,QAAI,OAAO,SAAS;AAClB;AAAA,IACF,OAAO;AACL;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,WAAW,QAAQ,QAAQ,WAAW,OAAO;AACxD;AAGO,SAAS,oBAA4B;AAC1C,QAAM,QAAQ,gBAAgB;AAC9B,QAAM,YAAY,MAAM,QAAQ;AAAA,IAC9B,CAAC,MAAM,EAAE,WAAW,eAAe,EAAE,WAAW;AAAA,EAClD;AAEA,MAAI,UAAU,SAAS,IAAI;AACzB,UAAM,WAAW,UAAU,MAAM,GAAG,UAAU,SAAS,EAAE;AACzD,UAAM,UAAU,MAAM,QAAQ;AAAA,MAC5B,CAAC,MAAM,CAAC,SAAS,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE;AAAA,IAC5C;AACA,oBAAgB,KAAK;AACrB,WAAO,SAAS;AAAA,EAClB;AAEA,SAAO;AACT;AAKO,MAAM,mBAAmB;AAAA;AAAA,EAE9B,WAAW,CAAC,aACV,gBAAgB,QAAQ,6BAA6B,QAAQ;AAAA,EAC/D,gBAAgB,CAAC,aACf,gBAAgB,QAAQ;AAAA,EAC1B,SAAS,CAAC,aAAqB,eAAe,QAAQ;AAAA,EACtD,SAAS,CAAC,aAAqB,eAAe,QAAQ;AAAA;AAAA,EAGtD,QAAQ,CAAC,MAAc,iBAAiB,kBAAkB,GAAG;AAAA,EAC7D,UAAU,CAAC,MAAc,iBAAiB,oBAAoB,GAAG;AAAA,EACjE,kBAAkB,CAAC,QAAgB,YAAY,GAAG;AAAA;AAAA,EAGlD,SAAS,MAAM;AAAA,EACf,QAAQ,MAAM;AAAA,EACd,MAAM,MAAM;AAAA;AAAA,EAGZ,aAAa,CAAC,YACZ,4CAA4C,OAAO;AAAA,EACrD,YAAY,CAAC,YACX,4BAA4B,OAAO;AACvC;AAKO,SAAS,aACd,aACG,MACK;AACR,QAAM,KAAK,iBAAiB,QAAQ;AACpC,MAAI,OAAO,OAAO,YAAY;AAC5B,WAAQ,GAAqC,GAAG,IAAI;AAAA,EACtD;AACA,SAAO;AACT;AAKO,SAAS,mBAAmB,aAAqB,KAAsB;AAC5E,UAAQ;AAAA,IACN,mDAAmD,UAAU;AAAA,EAC/D;AAEA,SAAO,YAAY,MAAM;AACvB,UAAM,UAAU,kBAAkB;AAClC,QAAI,QAAQ,SAAS,GAAG;AACtB,cAAQ,IAAI,sBAAsB,QAAQ,MAAM,oBAAoB;AACpE,+BAAyB;AAAA,IAC3B;AAAA,EACF,GAAG,UAAU;AACf;AAKO,SAAS,kBACd,UACA,UACA,QACM;AACN,MAAI,QAAQ;AACV,UAAM,WAAW,YAAY,UAAU,UAAU,MAAM;AACvD,YAAQ,IAAI,8BAA8B,QAAQ,KAAK,MAAM,EAAE;AAAA,EACjE;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/hooks/sms-notify.js
CHANGED
|
@@ -8,6 +8,8 @@ import { homedir } from "os";
|
|
|
8
8
|
const CONFIG_PATH = join(homedir(), ".stackmemory", "sms-notify.json");
|
|
9
9
|
const DEFAULT_CONFIG = {
|
|
10
10
|
enabled: false,
|
|
11
|
+
channel: "whatsapp",
|
|
12
|
+
// WhatsApp is cheaper for conversations
|
|
11
13
|
notifyOn: {
|
|
12
14
|
taskComplete: true,
|
|
13
15
|
reviewReady: true,
|
|
@@ -27,24 +29,45 @@ function loadSMSConfig() {
|
|
|
27
29
|
try {
|
|
28
30
|
if (existsSync(CONFIG_PATH)) {
|
|
29
31
|
const data = readFileSync(CONFIG_PATH, "utf8");
|
|
30
|
-
|
|
32
|
+
const saved = JSON.parse(data);
|
|
33
|
+
const config2 = { ...DEFAULT_CONFIG, ...saved };
|
|
34
|
+
applyEnvVars(config2);
|
|
35
|
+
return config2;
|
|
31
36
|
}
|
|
32
37
|
} catch {
|
|
33
38
|
}
|
|
34
39
|
const config = { ...DEFAULT_CONFIG };
|
|
40
|
+
applyEnvVars(config);
|
|
41
|
+
return config;
|
|
42
|
+
}
|
|
43
|
+
function applyEnvVars(config) {
|
|
35
44
|
if (process.env["TWILIO_ACCOUNT_SID"]) {
|
|
36
45
|
config.accountSid = process.env["TWILIO_ACCOUNT_SID"];
|
|
37
46
|
}
|
|
38
47
|
if (process.env["TWILIO_AUTH_TOKEN"]) {
|
|
39
48
|
config.authToken = process.env["TWILIO_AUTH_TOKEN"];
|
|
40
49
|
}
|
|
50
|
+
if (process.env["TWILIO_SMS_FROM"] || process.env["TWILIO_FROM_NUMBER"]) {
|
|
51
|
+
config.smsFromNumber = process.env["TWILIO_SMS_FROM"] || process.env["TWILIO_FROM_NUMBER"];
|
|
52
|
+
}
|
|
53
|
+
if (process.env["TWILIO_SMS_TO"] || process.env["TWILIO_TO_NUMBER"]) {
|
|
54
|
+
config.smsToNumber = process.env["TWILIO_SMS_TO"] || process.env["TWILIO_TO_NUMBER"];
|
|
55
|
+
}
|
|
56
|
+
if (process.env["TWILIO_WHATSAPP_FROM"]) {
|
|
57
|
+
config.whatsappFromNumber = process.env["TWILIO_WHATSAPP_FROM"];
|
|
58
|
+
}
|
|
59
|
+
if (process.env["TWILIO_WHATSAPP_TO"]) {
|
|
60
|
+
config.whatsappToNumber = process.env["TWILIO_WHATSAPP_TO"];
|
|
61
|
+
}
|
|
41
62
|
if (process.env["TWILIO_FROM_NUMBER"]) {
|
|
42
63
|
config.fromNumber = process.env["TWILIO_FROM_NUMBER"];
|
|
43
64
|
}
|
|
44
65
|
if (process.env["TWILIO_TO_NUMBER"]) {
|
|
45
66
|
config.toNumber = process.env["TWILIO_TO_NUMBER"];
|
|
46
67
|
}
|
|
47
|
-
|
|
68
|
+
if (process.env["TWILIO_CHANNEL"]) {
|
|
69
|
+
config.channel = process.env["TWILIO_CHANNEL"];
|
|
70
|
+
}
|
|
48
71
|
}
|
|
49
72
|
function saveSMSConfig(config) {
|
|
50
73
|
try {
|
|
@@ -99,10 +122,30 @@ ${payload.message}`;
|
|
|
99
122
|
}
|
|
100
123
|
return message;
|
|
101
124
|
}
|
|
102
|
-
|
|
125
|
+
function getChannelNumbers(config) {
|
|
126
|
+
const channel = config.channel || "whatsapp";
|
|
127
|
+
if (channel === "whatsapp") {
|
|
128
|
+
const from2 = config.whatsappFromNumber || config.fromNumber;
|
|
129
|
+
const to2 = config.whatsappToNumber || config.toNumber;
|
|
130
|
+
if (from2 && to2) {
|
|
131
|
+
return {
|
|
132
|
+
from: from2.startsWith("whatsapp:") ? from2 : `whatsapp:${from2}`,
|
|
133
|
+
to: to2.startsWith("whatsapp:") ? to2 : `whatsapp:${to2}`,
|
|
134
|
+
channel: "whatsapp"
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const from = config.smsFromNumber || config.fromNumber;
|
|
139
|
+
const to = config.smsToNumber || config.toNumber;
|
|
140
|
+
if (from && to) {
|
|
141
|
+
return { from, to, channel: "sms" };
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
async function sendNotification(payload, channelOverride) {
|
|
103
146
|
const config = loadSMSConfig();
|
|
104
147
|
if (!config.enabled) {
|
|
105
|
-
return { success: false, error: "
|
|
148
|
+
return { success: false, error: "Notifications disabled" };
|
|
106
149
|
}
|
|
107
150
|
const typeMap = {
|
|
108
151
|
task_complete: "taskComplete",
|
|
@@ -119,10 +162,22 @@ async function sendSMSNotification(payload) {
|
|
|
119
162
|
if (isQuietHours(config)) {
|
|
120
163
|
return { success: false, error: "Quiet hours active" };
|
|
121
164
|
}
|
|
122
|
-
if (!config.accountSid || !config.authToken
|
|
165
|
+
if (!config.accountSid || !config.authToken) {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
error: "Missing Twilio credentials. Set TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN"
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const originalChannel = config.channel;
|
|
172
|
+
if (channelOverride) {
|
|
173
|
+
config.channel = channelOverride;
|
|
174
|
+
}
|
|
175
|
+
const numbers = getChannelNumbers(config);
|
|
176
|
+
config.channel = originalChannel;
|
|
177
|
+
if (!numbers) {
|
|
123
178
|
return {
|
|
124
179
|
success: false,
|
|
125
|
-
error: "Missing
|
|
180
|
+
error: config.channel === "whatsapp" ? "Missing WhatsApp numbers. Set TWILIO_WHATSAPP_FROM and TWILIO_WHATSAPP_TO" : "Missing SMS numbers. Set TWILIO_SMS_FROM and TWILIO_SMS_TO"
|
|
126
181
|
};
|
|
127
182
|
}
|
|
128
183
|
const message = formatPromptMessage(payload);
|
|
@@ -154,23 +209,31 @@ async function sendSMSNotification(payload) {
|
|
|
154
209
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
155
210
|
},
|
|
156
211
|
body: new URLSearchParams({
|
|
157
|
-
From:
|
|
158
|
-
To:
|
|
212
|
+
From: numbers.from,
|
|
213
|
+
To: numbers.to,
|
|
159
214
|
Body: message
|
|
160
215
|
})
|
|
161
216
|
});
|
|
162
217
|
if (!response.ok) {
|
|
163
218
|
const errorData = await response.text();
|
|
164
|
-
return {
|
|
219
|
+
return {
|
|
220
|
+
success: false,
|
|
221
|
+
channel: numbers.channel,
|
|
222
|
+
error: `Twilio error: ${errorData}`
|
|
223
|
+
};
|
|
165
224
|
}
|
|
166
|
-
return { success: true, promptId };
|
|
225
|
+
return { success: true, promptId, channel: numbers.channel };
|
|
167
226
|
} catch (err) {
|
|
168
227
|
return {
|
|
169
228
|
success: false,
|
|
170
|
-
|
|
229
|
+
channel: numbers.channel,
|
|
230
|
+
error: `Failed to send ${numbers.channel}: ${err instanceof Error ? err.message : String(err)}`
|
|
171
231
|
};
|
|
172
232
|
}
|
|
173
233
|
}
|
|
234
|
+
async function sendSMSNotification(payload) {
|
|
235
|
+
return sendNotification(payload);
|
|
236
|
+
}
|
|
174
237
|
function processIncomingResponse(from, body) {
|
|
175
238
|
const config = loadSMSConfig();
|
|
176
239
|
const response = body.trim().toLowerCase();
|
|
@@ -281,6 +344,7 @@ export {
|
|
|
281
344
|
notifyWithYesNo,
|
|
282
345
|
processIncomingResponse,
|
|
283
346
|
saveSMSConfig,
|
|
347
|
+
sendNotification,
|
|
284
348
|
sendSMSNotification
|
|
285
349
|
};
|
|
286
350
|
//# sourceMappingURL=sms-notify.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/hooks/sms-notify.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * SMS Notification Hook for StackMemory\n * Sends text messages when tasks are ready for review\n * Supports interactive prompts with numbered options or yes/no\n *\n * Optional feature - requires Twilio setup\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\nexport interface SMSConfig {\n enabled: boolean;\n // Twilio credentials (from env or config)\n accountSid?: string;\n authToken?: string;\n fromNumber?: string;\n toNumber?: string;\n // Webhook URL for receiving responses\n webhookUrl?: string;\n // Notification preferences\n notifyOn: {\n taskComplete: boolean;\n reviewReady: boolean;\n error: boolean;\n custom: boolean;\n };\n // Quiet hours (don't send during these times)\n quietHours?: {\n enabled: boolean;\n start: string; // \"22:00\"\n end: string; // \"08:00\"\n };\n // Response timeout (seconds)\n responseTimeout: number;\n // Pending prompts awaiting response\n pendingPrompts: PendingPrompt[];\n}\n\nexport interface PendingPrompt {\n id: string;\n timestamp: string;\n message: string;\n options: PromptOption[];\n type: 'options' | 'yesno' | 'freeform';\n callback?: string; // Command to run with response\n expiresAt: string;\n}\n\nexport interface PromptOption {\n key: string; // \"1\", \"2\", \"y\", \"n\", etc.\n label: string;\n action?: string; // Command to execute\n}\n\nexport interface NotificationPayload {\n type: 'task_complete' | 'review_ready' | 'error' | 'custom';\n title: string;\n message: string;\n prompt?: {\n type: 'options' | 'yesno' | 'freeform';\n options?: PromptOption[];\n question?: string;\n };\n metadata?: Record<string, unknown>;\n}\n\nconst CONFIG_PATH = join(homedir(), '.stackmemory', 'sms-notify.json');\n\nconst DEFAULT_CONFIG: SMSConfig = {\n enabled: false,\n notifyOn: {\n taskComplete: true,\n reviewReady: true,\n error: true,\n custom: true,\n },\n quietHours: {\n enabled: false,\n start: '22:00',\n end: '08:00',\n },\n responseTimeout: 300, // 5 minutes\n pendingPrompts: [],\n};\n\nexport function loadSMSConfig(): SMSConfig {\n try {\n if (existsSync(CONFIG_PATH)) {\n const data = readFileSync(CONFIG_PATH, 'utf8');\n return { ...DEFAULT_CONFIG, ...JSON.parse(data) };\n }\n } catch {\n // Use defaults\n }\n\n // Check environment variables\n const config = { ...DEFAULT_CONFIG };\n if (process.env['TWILIO_ACCOUNT_SID']) {\n config.accountSid = process.env['TWILIO_ACCOUNT_SID'];\n }\n if (process.env['TWILIO_AUTH_TOKEN']) {\n config.authToken = process.env['TWILIO_AUTH_TOKEN'];\n }\n if (process.env['TWILIO_FROM_NUMBER']) {\n config.fromNumber = process.env['TWILIO_FROM_NUMBER'];\n }\n if (process.env['TWILIO_TO_NUMBER']) {\n config.toNumber = process.env['TWILIO_TO_NUMBER'];\n }\n\n return config;\n}\n\nexport function saveSMSConfig(config: SMSConfig): void {\n try {\n const dir = join(homedir(), '.stackmemory');\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n // Don't save sensitive credentials to file\n const safeConfig = { ...config };\n delete safeConfig.accountSid;\n delete safeConfig.authToken;\n writeFileSync(CONFIG_PATH, JSON.stringify(safeConfig, null, 2));\n } catch {\n // Silently fail\n }\n}\n\nfunction isQuietHours(config: SMSConfig): boolean {\n if (!config.quietHours?.enabled) return false;\n\n const now = new Date();\n const currentTime = now.getHours() * 60 + now.getMinutes();\n\n const [startH, startM] = config.quietHours.start.split(':').map(Number);\n const [endH, endM] = config.quietHours.end.split(':').map(Number);\n\n const startTime = startH * 60 + startM;\n const endTime = endH * 60 + endM;\n\n // Handle overnight quiet hours (e.g., 22:00 - 08:00)\n if (startTime > endTime) {\n return currentTime >= startTime || currentTime < endTime;\n }\n\n return currentTime >= startTime && currentTime < endTime;\n}\n\nfunction generatePromptId(): string {\n return Math.random().toString(36).substring(2, 10);\n}\n\nfunction formatPromptMessage(payload: NotificationPayload): string {\n let message = `${payload.title}\\n\\n${payload.message}`;\n\n if (payload.prompt) {\n message += '\\n\\n';\n\n if (payload.prompt.question) {\n message += `${payload.prompt.question}\\n`;\n }\n\n if (payload.prompt.type === 'yesno') {\n message += 'Reply Y for Yes, N for No';\n } else if (payload.prompt.type === 'options' && payload.prompt.options) {\n payload.prompt.options.forEach((opt) => {\n message += `${opt.key}. ${opt.label}\\n`;\n });\n message += '\\nReply with number to select';\n } else if (payload.prompt.type === 'freeform') {\n message += 'Reply with your response';\n }\n }\n\n return message;\n}\n\nexport async function sendSMSNotification(\n payload: NotificationPayload\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const config = loadSMSConfig();\n\n if (!config.enabled) {\n return { success: false, error: 'SMS notifications disabled' };\n }\n\n // Check notification type is enabled\n const typeMap: Record<string, keyof typeof config.notifyOn> = {\n task_complete: 'taskComplete',\n review_ready: 'reviewReady',\n error: 'error',\n custom: 'custom',\n };\n\n if (!config.notifyOn[typeMap[payload.type]]) {\n return {\n success: false,\n error: `Notifications for ${payload.type} disabled`,\n };\n }\n\n // Check quiet hours\n if (isQuietHours(config)) {\n return { success: false, error: 'Quiet hours active' };\n }\n\n // Validate credentials\n if (\n !config.accountSid ||\n !config.authToken ||\n !config.fromNumber ||\n !config.toNumber\n ) {\n return {\n success: false,\n error:\n 'Missing Twilio credentials. Set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER, TWILIO_TO_NUMBER',\n };\n }\n\n const message = formatPromptMessage(payload);\n let promptId: string | undefined;\n\n // Store pending prompt if interactive\n if (payload.prompt) {\n promptId = generatePromptId();\n const expiresAt = new Date(\n Date.now() + config.responseTimeout * 1000\n ).toISOString();\n\n const pendingPrompt: PendingPrompt = {\n id: promptId,\n timestamp: new Date().toISOString(),\n message: payload.message,\n options: payload.prompt.options || [],\n type: payload.prompt.type,\n expiresAt,\n };\n\n config.pendingPrompts.push(pendingPrompt);\n saveSMSConfig(config);\n }\n\n try {\n // Use Twilio API\n const twilioUrl = `https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`;\n\n const response = await fetch(twilioUrl, {\n method: 'POST',\n headers: {\n Authorization:\n 'Basic ' +\n Buffer.from(`${config.accountSid}:${config.authToken}`).toString(\n 'base64'\n ),\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n From: config.fromNumber,\n To: config.toNumber,\n Body: message,\n }),\n });\n\n if (!response.ok) {\n const errorData = await response.text();\n return { success: false, error: `Twilio error: ${errorData}` };\n }\n\n return { success: true, promptId };\n } catch (err) {\n return {\n success: false,\n error: `Failed to send SMS: ${err instanceof Error ? err.message : String(err)}`,\n };\n }\n}\n\nexport function processIncomingResponse(\n from: string,\n body: string\n): {\n matched: boolean;\n prompt?: PendingPrompt;\n response?: string;\n action?: string;\n} {\n const config = loadSMSConfig();\n\n // Normalize response\n const response = body.trim().toLowerCase();\n\n // Find matching pending prompt (most recent first)\n const now = new Date();\n const validPrompts = config.pendingPrompts.filter(\n (p) => new Date(p.expiresAt) > now\n );\n\n if (validPrompts.length === 0) {\n return { matched: false };\n }\n\n // Get most recent prompt\n const prompt = validPrompts[validPrompts.length - 1];\n\n let matchedOption: PromptOption | undefined;\n\n if (prompt.type === 'yesno') {\n if (response === 'y' || response === 'yes') {\n matchedOption = { key: 'y', label: 'Yes' };\n } else if (response === 'n' || response === 'no') {\n matchedOption = { key: 'n', label: 'No' };\n }\n } else if (prompt.type === 'options') {\n matchedOption = prompt.options.find(\n (opt) => opt.key.toLowerCase() === response\n );\n } else if (prompt.type === 'freeform') {\n matchedOption = { key: response, label: response };\n }\n\n // Remove processed prompt\n config.pendingPrompts = config.pendingPrompts.filter(\n (p) => p.id !== prompt.id\n );\n saveSMSConfig(config);\n\n if (matchedOption) {\n return {\n matched: true,\n prompt,\n response: matchedOption.key,\n action: matchedOption.action,\n };\n }\n\n return { matched: false, prompt };\n}\n\n// Convenience functions for common notifications\n\nexport async function notifyReviewReady(\n title: string,\n description: string,\n options?: { label: string; action?: string }[]\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const payload: NotificationPayload = {\n type: 'review_ready',\n title: `Review Ready: ${title}`,\n message: description,\n };\n\n if (options && options.length > 0) {\n payload.prompt = {\n type: 'options',\n options: options.map((opt, i) => ({\n key: String(i + 1),\n label: opt.label,\n action: opt.action,\n })),\n question: 'What would you like to do?',\n };\n }\n\n return sendSMSNotification(payload);\n}\n\nexport async function notifyWithYesNo(\n title: string,\n question: string,\n yesAction?: string,\n noAction?: string\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n return sendSMSNotification({\n type: 'custom',\n title,\n message: question,\n prompt: {\n type: 'yesno',\n options: [\n { key: 'y', label: 'Yes', action: yesAction },\n { key: 'n', label: 'No', action: noAction },\n ],\n },\n });\n}\n\nexport async function notifyTaskComplete(\n taskName: string,\n summary: string\n): Promise<{ success: boolean; error?: string }> {\n return sendSMSNotification({\n type: 'task_complete',\n title: `Task Complete: ${taskName}`,\n message: summary,\n });\n}\n\nexport async function notifyError(\n error: string,\n context?: string\n): Promise<{ success: boolean; error?: string }> {\n return sendSMSNotification({\n type: 'error',\n title: 'Error Alert',\n message: context ? `${error}\\n\\nContext: ${context}` : error,\n });\n}\n\n// Clean up expired prompts\nexport function cleanupExpiredPrompts(): number {\n const config = loadSMSConfig();\n const now = new Date();\n const before = config.pendingPrompts.length;\n\n config.pendingPrompts = config.pendingPrompts.filter(\n (p) => new Date(p.expiresAt) > now\n );\n\n const removed = before - config.pendingPrompts.length;\n if (removed > 0) {\n saveSMSConfig(config);\n }\n\n return removed;\n}\n"],
|
|
5
|
-
"mappings": ";;;;AAQA,SAAS,YAAY,cAAc,eAAe,iBAAiB;AACnE,SAAS,YAAY;AACrB,SAAS,eAAe;
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["/**\n * SMS Notification Hook for StackMemory\n * Sends text messages when tasks are ready for review\n * Supports interactive prompts with numbered options or yes/no\n *\n * Optional feature - requires Twilio setup\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\nexport type MessageChannel = 'whatsapp' | 'sms';\n\nexport interface SMSConfig {\n enabled: boolean;\n // Preferred channel: whatsapp is cheaper for back-and-forth conversations\n channel: MessageChannel;\n // Twilio credentials (from env or config)\n accountSid?: string;\n authToken?: string;\n // SMS numbers\n smsFromNumber?: string;\n smsToNumber?: string;\n // WhatsApp numbers (Twilio prefixes with 'whatsapp:' automatically)\n whatsappFromNumber?: string;\n whatsappToNumber?: string;\n // Legacy fields (backwards compatibility)\n fromNumber?: string;\n toNumber?: string;\n // Webhook URL for receiving responses\n webhookUrl?: string;\n // Notification preferences\n notifyOn: {\n taskComplete: boolean;\n reviewReady: boolean;\n error: boolean;\n custom: boolean;\n };\n // Quiet hours (don't send during these times)\n quietHours?: {\n enabled: boolean;\n start: string; // \"22:00\"\n end: string; // \"08:00\"\n };\n // Response timeout (seconds)\n responseTimeout: number;\n // Pending prompts awaiting response\n pendingPrompts: PendingPrompt[];\n}\n\nexport interface PendingPrompt {\n id: string;\n timestamp: string;\n message: string;\n options: PromptOption[];\n type: 'options' | 'yesno' | 'freeform';\n callback?: string; // Command to run with response\n expiresAt: string;\n}\n\nexport interface PromptOption {\n key: string; // \"1\", \"2\", \"y\", \"n\", etc.\n label: string;\n action?: string; // Command to execute\n}\n\nexport interface NotificationPayload {\n type: 'task_complete' | 'review_ready' | 'error' | 'custom';\n title: string;\n message: string;\n prompt?: {\n type: 'options' | 'yesno' | 'freeform';\n options?: PromptOption[];\n question?: string;\n };\n metadata?: Record<string, unknown>;\n}\n\nconst CONFIG_PATH = join(homedir(), '.stackmemory', 'sms-notify.json');\n\nconst DEFAULT_CONFIG: SMSConfig = {\n enabled: false,\n channel: 'whatsapp', // WhatsApp is cheaper for conversations\n notifyOn: {\n taskComplete: true,\n reviewReady: true,\n error: true,\n custom: true,\n },\n quietHours: {\n enabled: false,\n start: '22:00',\n end: '08:00',\n },\n responseTimeout: 300, // 5 minutes\n pendingPrompts: [],\n};\n\nexport function loadSMSConfig(): SMSConfig {\n try {\n if (existsSync(CONFIG_PATH)) {\n const data = readFileSync(CONFIG_PATH, 'utf8');\n const saved = JSON.parse(data);\n // Merge with defaults, then apply env vars\n const config = { ...DEFAULT_CONFIG, ...saved };\n applyEnvVars(config);\n return config;\n }\n } catch {\n // Use defaults\n }\n\n // Check environment variables\n const config = { ...DEFAULT_CONFIG };\n applyEnvVars(config);\n return config;\n}\n\nfunction applyEnvVars(config: SMSConfig): void {\n // Twilio credentials\n if (process.env['TWILIO_ACCOUNT_SID']) {\n config.accountSid = process.env['TWILIO_ACCOUNT_SID'];\n }\n if (process.env['TWILIO_AUTH_TOKEN']) {\n config.authToken = process.env['TWILIO_AUTH_TOKEN'];\n }\n\n // SMS numbers\n if (process.env['TWILIO_SMS_FROM'] || process.env['TWILIO_FROM_NUMBER']) {\n config.smsFromNumber =\n process.env['TWILIO_SMS_FROM'] || process.env['TWILIO_FROM_NUMBER'];\n }\n if (process.env['TWILIO_SMS_TO'] || process.env['TWILIO_TO_NUMBER']) {\n config.smsToNumber =\n process.env['TWILIO_SMS_TO'] || process.env['TWILIO_TO_NUMBER'];\n }\n\n // WhatsApp numbers\n if (process.env['TWILIO_WHATSAPP_FROM']) {\n config.whatsappFromNumber = process.env['TWILIO_WHATSAPP_FROM'];\n }\n if (process.env['TWILIO_WHATSAPP_TO']) {\n config.whatsappToNumber = process.env['TWILIO_WHATSAPP_TO'];\n }\n\n // Legacy support\n if (process.env['TWILIO_FROM_NUMBER']) {\n config.fromNumber = process.env['TWILIO_FROM_NUMBER'];\n }\n if (process.env['TWILIO_TO_NUMBER']) {\n config.toNumber = process.env['TWILIO_TO_NUMBER'];\n }\n\n // Channel preference\n if (process.env['TWILIO_CHANNEL']) {\n config.channel = process.env['TWILIO_CHANNEL'] as MessageChannel;\n }\n}\n\nexport function saveSMSConfig(config: SMSConfig): void {\n try {\n const dir = join(homedir(), '.stackmemory');\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n // Don't save sensitive credentials to file\n const safeConfig = { ...config };\n delete safeConfig.accountSid;\n delete safeConfig.authToken;\n writeFileSync(CONFIG_PATH, JSON.stringify(safeConfig, null, 2));\n } catch {\n // Silently fail\n }\n}\n\nfunction isQuietHours(config: SMSConfig): boolean {\n if (!config.quietHours?.enabled) return false;\n\n const now = new Date();\n const currentTime = now.getHours() * 60 + now.getMinutes();\n\n const [startH, startM] = config.quietHours.start.split(':').map(Number);\n const [endH, endM] = config.quietHours.end.split(':').map(Number);\n\n const startTime = startH * 60 + startM;\n const endTime = endH * 60 + endM;\n\n // Handle overnight quiet hours (e.g., 22:00 - 08:00)\n if (startTime > endTime) {\n return currentTime >= startTime || currentTime < endTime;\n }\n\n return currentTime >= startTime && currentTime < endTime;\n}\n\nfunction generatePromptId(): string {\n return Math.random().toString(36).substring(2, 10);\n}\n\nfunction formatPromptMessage(payload: NotificationPayload): string {\n let message = `${payload.title}\\n\\n${payload.message}`;\n\n if (payload.prompt) {\n message += '\\n\\n';\n\n if (payload.prompt.question) {\n message += `${payload.prompt.question}\\n`;\n }\n\n if (payload.prompt.type === 'yesno') {\n message += 'Reply Y for Yes, N for No';\n } else if (payload.prompt.type === 'options' && payload.prompt.options) {\n payload.prompt.options.forEach((opt) => {\n message += `${opt.key}. ${opt.label}\\n`;\n });\n message += '\\nReply with number to select';\n } else if (payload.prompt.type === 'freeform') {\n message += 'Reply with your response';\n }\n }\n\n return message;\n}\n\nfunction getChannelNumbers(config: SMSConfig): {\n from: string;\n to: string;\n channel: MessageChannel;\n} | null {\n const channel = config.channel || 'whatsapp';\n\n if (channel === 'whatsapp') {\n // Try WhatsApp first\n const from = config.whatsappFromNumber || config.fromNumber;\n const to = config.whatsappToNumber || config.toNumber;\n if (from && to) {\n // Twilio requires 'whatsapp:' prefix for WhatsApp numbers\n return {\n from: from.startsWith('whatsapp:') ? from : `whatsapp:${from}`,\n to: to.startsWith('whatsapp:') ? to : `whatsapp:${to}`,\n channel: 'whatsapp',\n };\n }\n }\n\n // Fall back to SMS\n const from = config.smsFromNumber || config.fromNumber;\n const to = config.smsToNumber || config.toNumber;\n if (from && to) {\n return { from, to, channel: 'sms' };\n }\n\n return null;\n}\n\nexport async function sendNotification(\n payload: NotificationPayload,\n channelOverride?: MessageChannel\n): Promise<{\n success: boolean;\n promptId?: string;\n channel?: MessageChannel;\n error?: string;\n}> {\n const config = loadSMSConfig();\n\n if (!config.enabled) {\n return { success: false, error: 'Notifications disabled' };\n }\n\n // Check notification type is enabled\n const typeMap: Record<string, keyof typeof config.notifyOn> = {\n task_complete: 'taskComplete',\n review_ready: 'reviewReady',\n error: 'error',\n custom: 'custom',\n };\n\n if (!config.notifyOn[typeMap[payload.type]]) {\n return {\n success: false,\n error: `Notifications for ${payload.type} disabled`,\n };\n }\n\n // Check quiet hours\n if (isQuietHours(config)) {\n return { success: false, error: 'Quiet hours active' };\n }\n\n // Validate credentials\n if (!config.accountSid || !config.authToken) {\n return {\n success: false,\n error:\n 'Missing Twilio credentials. Set TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN',\n };\n }\n\n // Get channel numbers (prefer WhatsApp)\n const originalChannel = config.channel;\n if (channelOverride) {\n config.channel = channelOverride;\n }\n\n const numbers = getChannelNumbers(config);\n config.channel = originalChannel; // Restore\n\n if (!numbers) {\n return {\n success: false,\n error:\n config.channel === 'whatsapp'\n ? 'Missing WhatsApp numbers. Set TWILIO_WHATSAPP_FROM and TWILIO_WHATSAPP_TO'\n : 'Missing SMS numbers. Set TWILIO_SMS_FROM and TWILIO_SMS_TO',\n };\n }\n\n const message = formatPromptMessage(payload);\n let promptId: string | undefined;\n\n // Store pending prompt if interactive\n if (payload.prompt) {\n promptId = generatePromptId();\n const expiresAt = new Date(\n Date.now() + config.responseTimeout * 1000\n ).toISOString();\n\n const pendingPrompt: PendingPrompt = {\n id: promptId,\n timestamp: new Date().toISOString(),\n message: payload.message,\n options: payload.prompt.options || [],\n type: payload.prompt.type,\n expiresAt,\n };\n\n config.pendingPrompts.push(pendingPrompt);\n saveSMSConfig(config);\n }\n\n try {\n // Use Twilio API (same endpoint for SMS and WhatsApp)\n const twilioUrl = `https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`;\n\n const response = await fetch(twilioUrl, {\n method: 'POST',\n headers: {\n Authorization:\n 'Basic ' +\n Buffer.from(`${config.accountSid}:${config.authToken}`).toString(\n 'base64'\n ),\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n From: numbers.from,\n To: numbers.to,\n Body: message,\n }),\n });\n\n if (!response.ok) {\n const errorData = await response.text();\n return {\n success: false,\n channel: numbers.channel,\n error: `Twilio error: ${errorData}`,\n };\n }\n\n return { success: true, promptId, channel: numbers.channel };\n } catch (err) {\n return {\n success: false,\n channel: numbers.channel,\n error: `Failed to send ${numbers.channel}: ${err instanceof Error ? err.message : String(err)}`,\n };\n }\n}\n\n// Backwards compatible alias\nexport async function sendSMSNotification(\n payload: NotificationPayload\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n return sendNotification(payload);\n}\n\nexport function processIncomingResponse(\n from: string,\n body: string\n): {\n matched: boolean;\n prompt?: PendingPrompt;\n response?: string;\n action?: string;\n} {\n const config = loadSMSConfig();\n\n // Normalize response\n const response = body.trim().toLowerCase();\n\n // Find matching pending prompt (most recent first)\n const now = new Date();\n const validPrompts = config.pendingPrompts.filter(\n (p) => new Date(p.expiresAt) > now\n );\n\n if (validPrompts.length === 0) {\n return { matched: false };\n }\n\n // Get most recent prompt\n const prompt = validPrompts[validPrompts.length - 1];\n\n let matchedOption: PromptOption | undefined;\n\n if (prompt.type === 'yesno') {\n if (response === 'y' || response === 'yes') {\n matchedOption = { key: 'y', label: 'Yes' };\n } else if (response === 'n' || response === 'no') {\n matchedOption = { key: 'n', label: 'No' };\n }\n } else if (prompt.type === 'options') {\n matchedOption = prompt.options.find(\n (opt) => opt.key.toLowerCase() === response\n );\n } else if (prompt.type === 'freeform') {\n matchedOption = { key: response, label: response };\n }\n\n // Remove processed prompt\n config.pendingPrompts = config.pendingPrompts.filter(\n (p) => p.id !== prompt.id\n );\n saveSMSConfig(config);\n\n if (matchedOption) {\n return {\n matched: true,\n prompt,\n response: matchedOption.key,\n action: matchedOption.action,\n };\n }\n\n return { matched: false, prompt };\n}\n\n// Convenience functions for common notifications\n\nexport async function notifyReviewReady(\n title: string,\n description: string,\n options?: { label: string; action?: string }[]\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const payload: NotificationPayload = {\n type: 'review_ready',\n title: `Review Ready: ${title}`,\n message: description,\n };\n\n if (options && options.length > 0) {\n payload.prompt = {\n type: 'options',\n options: options.map((opt, i) => ({\n key: String(i + 1),\n label: opt.label,\n action: opt.action,\n })),\n question: 'What would you like to do?',\n };\n }\n\n return sendSMSNotification(payload);\n}\n\nexport async function notifyWithYesNo(\n title: string,\n question: string,\n yesAction?: string,\n noAction?: string\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n return sendSMSNotification({\n type: 'custom',\n title,\n message: question,\n prompt: {\n type: 'yesno',\n options: [\n { key: 'y', label: 'Yes', action: yesAction },\n { key: 'n', label: 'No', action: noAction },\n ],\n },\n });\n}\n\nexport async function notifyTaskComplete(\n taskName: string,\n summary: string\n): Promise<{ success: boolean; error?: string }> {\n return sendSMSNotification({\n type: 'task_complete',\n title: `Task Complete: ${taskName}`,\n message: summary,\n });\n}\n\nexport async function notifyError(\n error: string,\n context?: string\n): Promise<{ success: boolean; error?: string }> {\n return sendSMSNotification({\n type: 'error',\n title: 'Error Alert',\n message: context ? `${error}\\n\\nContext: ${context}` : error,\n });\n}\n\n// Clean up expired prompts\nexport function cleanupExpiredPrompts(): number {\n const config = loadSMSConfig();\n const now = new Date();\n const before = config.pendingPrompts.length;\n\n config.pendingPrompts = config.pendingPrompts.filter(\n (p) => new Date(p.expiresAt) > now\n );\n\n const removed = before - config.pendingPrompts.length;\n if (removed > 0) {\n saveSMSConfig(config);\n }\n\n return removed;\n}\n"],
|
|
5
|
+
"mappings": ";;;;AAQA,SAAS,YAAY,cAAc,eAAe,iBAAiB;AACnE,SAAS,YAAY;AACrB,SAAS,eAAe;AAqExB,MAAM,cAAc,KAAK,QAAQ,GAAG,gBAAgB,iBAAiB;AAErE,MAAM,iBAA4B;AAAA,EAChC,SAAS;AAAA,EACT,SAAS;AAAA;AAAA,EACT,UAAU;AAAA,IACR,cAAc;AAAA,IACd,aAAa;AAAA,IACb,OAAO;AAAA,IACP,QAAQ;AAAA,EACV;AAAA,EACA,YAAY;AAAA,IACV,SAAS;AAAA,IACT,OAAO;AAAA,IACP,KAAK;AAAA,EACP;AAAA,EACA,iBAAiB;AAAA;AAAA,EACjB,gBAAgB,CAAC;AACnB;AAEO,SAAS,gBAA2B;AACzC,MAAI;AACF,QAAI,WAAW,WAAW,GAAG;AAC3B,YAAM,OAAO,aAAa,aAAa,MAAM;AAC7C,YAAM,QAAQ,KAAK,MAAM,IAAI;AAE7B,YAAMA,UAAS,EAAE,GAAG,gBAAgB,GAAG,MAAM;AAC7C,mBAAaA,OAAM;AACnB,aAAOA;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AAGA,QAAM,SAAS,EAAE,GAAG,eAAe;AACnC,eAAa,MAAM;AACnB,SAAO;AACT;AAEA,SAAS,aAAa,QAAyB;AAE7C,MAAI,QAAQ,IAAI,oBAAoB,GAAG;AACrC,WAAO,aAAa,QAAQ,IAAI,oBAAoB;AAAA,EACtD;AACA,MAAI,QAAQ,IAAI,mBAAmB,GAAG;AACpC,WAAO,YAAY,QAAQ,IAAI,mBAAmB;AAAA,EACpD;AAGA,MAAI,QAAQ,IAAI,iBAAiB,KAAK,QAAQ,IAAI,oBAAoB,GAAG;AACvE,WAAO,gBACL,QAAQ,IAAI,iBAAiB,KAAK,QAAQ,IAAI,oBAAoB;AAAA,EACtE;AACA,MAAI,QAAQ,IAAI,eAAe,KAAK,QAAQ,IAAI,kBAAkB,GAAG;AACnE,WAAO,cACL,QAAQ,IAAI,eAAe,KAAK,QAAQ,IAAI,kBAAkB;AAAA,EAClE;AAGA,MAAI,QAAQ,IAAI,sBAAsB,GAAG;AACvC,WAAO,qBAAqB,QAAQ,IAAI,sBAAsB;AAAA,EAChE;AACA,MAAI,QAAQ,IAAI,oBAAoB,GAAG;AACrC,WAAO,mBAAmB,QAAQ,IAAI,oBAAoB;AAAA,EAC5D;AAGA,MAAI,QAAQ,IAAI,oBAAoB,GAAG;AACrC,WAAO,aAAa,QAAQ,IAAI,oBAAoB;AAAA,EACtD;AACA,MAAI,QAAQ,IAAI,kBAAkB,GAAG;AACnC,WAAO,WAAW,QAAQ,IAAI,kBAAkB;AAAA,EAClD;AAGA,MAAI,QAAQ,IAAI,gBAAgB,GAAG;AACjC,WAAO,UAAU,QAAQ,IAAI,gBAAgB;AAAA,EAC/C;AACF;AAEO,SAAS,cAAc,QAAyB;AACrD,MAAI;AACF,UAAM,MAAM,KAAK,QAAQ,GAAG,cAAc;AAC1C,QAAI,CAAC,WAAW,GAAG,GAAG;AACpB,gBAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,IACpC;AAEA,UAAM,aAAa,EAAE,GAAG,OAAO;AAC/B,WAAO,WAAW;AAClB,WAAO,WAAW;AAClB,kBAAc,aAAa,KAAK,UAAU,YAAY,MAAM,CAAC,CAAC;AAAA,EAChE,QAAQ;AAAA,EAER;AACF;AAEA,SAAS,aAAa,QAA4B;AAChD,MAAI,CAAC,OAAO,YAAY,QAAS,QAAO;AAExC,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,cAAc,IAAI,SAAS,IAAI,KAAK,IAAI,WAAW;AAEzD,QAAM,CAAC,QAAQ,MAAM,IAAI,OAAO,WAAW,MAAM,MAAM,GAAG,EAAE,IAAI,MAAM;AACtE,QAAM,CAAC,MAAM,IAAI,IAAI,OAAO,WAAW,IAAI,MAAM,GAAG,EAAE,IAAI,MAAM;AAEhE,QAAM,YAAY,SAAS,KAAK;AAChC,QAAM,UAAU,OAAO,KAAK;AAG5B,MAAI,YAAY,SAAS;AACvB,WAAO,eAAe,aAAa,cAAc;AAAA,EACnD;AAEA,SAAO,eAAe,aAAa,cAAc;AACnD;AAEA,SAAS,mBAA2B;AAClC,SAAO,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,UAAU,GAAG,EAAE;AACnD;AAEA,SAAS,oBAAoB,SAAsC;AACjE,MAAI,UAAU,GAAG,QAAQ,KAAK;AAAA;AAAA,EAAO,QAAQ,OAAO;AAEpD,MAAI,QAAQ,QAAQ;AAClB,eAAW;AAEX,QAAI,QAAQ,OAAO,UAAU;AAC3B,iBAAW,GAAG,QAAQ,OAAO,QAAQ;AAAA;AAAA,IACvC;AAEA,QAAI,QAAQ,OAAO,SAAS,SAAS;AACnC,iBAAW;AAAA,IACb,WAAW,QAAQ,OAAO,SAAS,aAAa,QAAQ,OAAO,SAAS;AACtE,cAAQ,OAAO,QAAQ,QAAQ,CAAC,QAAQ;AACtC,mBAAW,GAAG,IAAI,GAAG,KAAK,IAAI,KAAK;AAAA;AAAA,MACrC,CAAC;AACD,iBAAW;AAAA,IACb,WAAW,QAAQ,OAAO,SAAS,YAAY;AAC7C,iBAAW;AAAA,IACb;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,kBAAkB,QAIlB;AACP,QAAM,UAAU,OAAO,WAAW;AAElC,MAAI,YAAY,YAAY;AAE1B,UAAMC,QAAO,OAAO,sBAAsB,OAAO;AACjD,UAAMC,MAAK,OAAO,oBAAoB,OAAO;AAC7C,QAAID,SAAQC,KAAI;AAEd,aAAO;AAAA,QACL,MAAMD,MAAK,WAAW,WAAW,IAAIA,QAAO,YAAYA,KAAI;AAAA,QAC5D,IAAIC,IAAG,WAAW,WAAW,IAAIA,MAAK,YAAYA,GAAE;AAAA,QACpD,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AAGA,QAAM,OAAO,OAAO,iBAAiB,OAAO;AAC5C,QAAM,KAAK,OAAO,eAAe,OAAO;AACxC,MAAI,QAAQ,IAAI;AACd,WAAO,EAAE,MAAM,IAAI,SAAS,MAAM;AAAA,EACpC;AAEA,SAAO;AACT;AAEA,eAAsB,iBACpB,SACA,iBAMC;AACD,QAAM,SAAS,cAAc;AAE7B,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,EAAE,SAAS,OAAO,OAAO,yBAAyB;AAAA,EAC3D;AAGA,QAAM,UAAwD;AAAA,IAC5D,eAAe;AAAA,IACf,cAAc;AAAA,IACd,OAAO;AAAA,IACP,QAAQ;AAAA,EACV;AAEA,MAAI,CAAC,OAAO,SAAS,QAAQ,QAAQ,IAAI,CAAC,GAAG;AAC3C,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,qBAAqB,QAAQ,IAAI;AAAA,IAC1C;AAAA,EACF;AAGA,MAAI,aAAa,MAAM,GAAG;AACxB,WAAO,EAAE,SAAS,OAAO,OAAO,qBAAqB;AAAA,EACvD;AAGA,MAAI,CAAC,OAAO,cAAc,CAAC,OAAO,WAAW;AAC3C,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OACE;AAAA,IACJ;AAAA,EACF;AAGA,QAAM,kBAAkB,OAAO;AAC/B,MAAI,iBAAiB;AACnB,WAAO,UAAU;AAAA,EACnB;AAEA,QAAM,UAAU,kBAAkB,MAAM;AACxC,SAAO,UAAU;AAEjB,MAAI,CAAC,SAAS;AACZ,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OACE,OAAO,YAAY,aACf,8EACA;AAAA,IACR;AAAA,EACF;AAEA,QAAM,UAAU,oBAAoB,OAAO;AAC3C,MAAI;AAGJ,MAAI,QAAQ,QAAQ;AAClB,eAAW,iBAAiB;AAC5B,UAAM,YAAY,IAAI;AAAA,MACpB,KAAK,IAAI,IAAI,OAAO,kBAAkB;AAAA,IACxC,EAAE,YAAY;AAEd,UAAM,gBAA+B;AAAA,MACnC,IAAI;AAAA,MACJ,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,SAAS,QAAQ;AAAA,MACjB,SAAS,QAAQ,OAAO,WAAW,CAAC;AAAA,MACpC,MAAM,QAAQ,OAAO;AAAA,MACrB;AAAA,IACF;AAEA,WAAO,eAAe,KAAK,aAAa;AACxC,kBAAc,MAAM;AAAA,EACtB;AAEA,MAAI;AAEF,UAAM,YAAY,8CAA8C,OAAO,UAAU;AAEjF,UAAM,WAAW,MAAM,MAAM,WAAW;AAAA,MACtC,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eACE,WACA,OAAO,KAAK,GAAG,OAAO,UAAU,IAAI,OAAO,SAAS,EAAE,EAAE;AAAA,UACtD;AAAA,QACF;AAAA,QACF,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,IAAI,gBAAgB;AAAA,QACxB,MAAM,QAAQ;AAAA,QACd,IAAI,QAAQ;AAAA,QACZ,MAAM;AAAA,MACR,CAAC;AAAA,IACH,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,YAAY,MAAM,SAAS,KAAK;AACtC,aAAO;AAAA,QACL,SAAS;AAAA,QACT,SAAS,QAAQ;AAAA,QACjB,OAAO,iBAAiB,SAAS;AAAA,MACnC;AAAA,IACF;AAEA,WAAO,EAAE,SAAS,MAAM,UAAU,SAAS,QAAQ,QAAQ;AAAA,EAC7D,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,SAAS;AAAA,MACT,SAAS,QAAQ;AAAA,MACjB,OAAO,kBAAkB,QAAQ,OAAO,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,IAC/F;AAAA,EACF;AACF;AAGA,eAAsB,oBACpB,SACkE;AAClE,SAAO,iBAAiB,OAAO;AACjC;AAEO,SAAS,wBACd,MACA,MAMA;AACA,QAAM,SAAS,cAAc;AAG7B,QAAM,WAAW,KAAK,KAAK,EAAE,YAAY;AAGzC,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,eAAe,OAAO,eAAe;AAAA,IACzC,CAAC,MAAM,IAAI,KAAK,EAAE,SAAS,IAAI;AAAA,EACjC;AAEA,MAAI,aAAa,WAAW,GAAG;AAC7B,WAAO,EAAE,SAAS,MAAM;AAAA,EAC1B;AAGA,QAAM,SAAS,aAAa,aAAa,SAAS,CAAC;AAEnD,MAAI;AAEJ,MAAI,OAAO,SAAS,SAAS;AAC3B,QAAI,aAAa,OAAO,aAAa,OAAO;AAC1C,sBAAgB,EAAE,KAAK,KAAK,OAAO,MAAM;AAAA,IAC3C,WAAW,aAAa,OAAO,aAAa,MAAM;AAChD,sBAAgB,EAAE,KAAK,KAAK,OAAO,KAAK;AAAA,IAC1C;AAAA,EACF,WAAW,OAAO,SAAS,WAAW;AACpC,oBAAgB,OAAO,QAAQ;AAAA,MAC7B,CAAC,QAAQ,IAAI,IAAI,YAAY,MAAM;AAAA,IACrC;AAAA,EACF,WAAW,OAAO,SAAS,YAAY;AACrC,oBAAgB,EAAE,KAAK,UAAU,OAAO,SAAS;AAAA,EACnD;AAGA,SAAO,iBAAiB,OAAO,eAAe;AAAA,IAC5C,CAAC,MAAM,EAAE,OAAO,OAAO;AAAA,EACzB;AACA,gBAAc,MAAM;AAEpB,MAAI,eAAe;AACjB,WAAO;AAAA,MACL,SAAS;AAAA,MACT;AAAA,MACA,UAAU,cAAc;AAAA,MACxB,QAAQ,cAAc;AAAA,IACxB;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,OAAO,OAAO;AAClC;AAIA,eAAsB,kBACpB,OACA,aACA,SACkE;AAClE,QAAM,UAA+B;AAAA,IACnC,MAAM;AAAA,IACN,OAAO,iBAAiB,KAAK;AAAA,IAC7B,SAAS;AAAA,EACX;AAEA,MAAI,WAAW,QAAQ,SAAS,GAAG;AACjC,YAAQ,SAAS;AAAA,MACf,MAAM;AAAA,MACN,SAAS,QAAQ,IAAI,CAAC,KAAK,OAAO;AAAA,QAChC,KAAK,OAAO,IAAI,CAAC;AAAA,QACjB,OAAO,IAAI;AAAA,QACX,QAAQ,IAAI;AAAA,MACd,EAAE;AAAA,MACF,UAAU;AAAA,IACZ;AAAA,EACF;AAEA,SAAO,oBAAoB,OAAO;AACpC;AAEA,eAAsB,gBACpB,OACA,UACA,WACA,UACkE;AAClE,SAAO,oBAAoB;AAAA,IACzB,MAAM;AAAA,IACN;AAAA,IACA,SAAS;AAAA,IACT,QAAQ;AAAA,MACN,MAAM;AAAA,MACN,SAAS;AAAA,QACP,EAAE,KAAK,KAAK,OAAO,OAAO,QAAQ,UAAU;AAAA,QAC5C,EAAE,KAAK,KAAK,OAAO,MAAM,QAAQ,SAAS;AAAA,MAC5C;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAEA,eAAsB,mBACpB,UACA,SAC+C;AAC/C,SAAO,oBAAoB;AAAA,IACzB,MAAM;AAAA,IACN,OAAO,kBAAkB,QAAQ;AAAA,IACjC,SAAS;AAAA,EACX,CAAC;AACH;AAEA,eAAsB,YACpB,OACA,SAC+C;AAC/C,SAAO,oBAAoB;AAAA,IACzB,MAAM;AAAA,IACN,OAAO;AAAA,IACP,SAAS,UAAU,GAAG,KAAK;AAAA;AAAA,WAAgB,OAAO,KAAK;AAAA,EACzD,CAAC;AACH;AAGO,SAAS,wBAAgC;AAC9C,QAAM,SAAS,cAAc;AAC7B,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,SAAS,OAAO,eAAe;AAErC,SAAO,iBAAiB,OAAO,eAAe;AAAA,IAC5C,CAAC,MAAM,IAAI,KAAK,EAAE,SAAS,IAAI;AAAA,EACjC;AAEA,QAAM,UAAU,SAAS,OAAO,eAAe;AAC/C,MAAI,UAAU,GAAG;AACf,kBAAc,MAAM;AAAA,EACtB;AAEA,SAAO;AACT;",
|
|
6
|
+
"names": ["config", "from", "to"]
|
|
7
7
|
}
|
|
@@ -4,8 +4,11 @@ const __filename = __fileURLToPath(import.meta.url);
|
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { createServer } from "http";
|
|
6
6
|
import { parse as parseUrl } from "url";
|
|
7
|
+
import { existsSync, writeFileSync, mkdirSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
7
10
|
import { processIncomingResponse, loadSMSConfig } from "./sms-notify.js";
|
|
8
|
-
import {
|
|
11
|
+
import { queueAction } from "./sms-action-runner.js";
|
|
9
12
|
function parseFormData(body) {
|
|
10
13
|
const params = new URLSearchParams(body);
|
|
11
14
|
const result = {};
|
|
@@ -14,6 +17,22 @@ function parseFormData(body) {
|
|
|
14
17
|
});
|
|
15
18
|
return result;
|
|
16
19
|
}
|
|
20
|
+
function storeLatestResponse(promptId, response, action) {
|
|
21
|
+
const dir = join(homedir(), ".stackmemory");
|
|
22
|
+
if (!existsSync(dir)) {
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
const responsePath = join(dir, "sms-latest-response.json");
|
|
26
|
+
writeFileSync(
|
|
27
|
+
responsePath,
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
promptId,
|
|
30
|
+
response,
|
|
31
|
+
action,
|
|
32
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
}
|
|
17
36
|
function handleSMSWebhook(payload) {
|
|
18
37
|
const { From, Body } = payload;
|
|
19
38
|
console.log(`[sms-webhook] Received from ${From}: ${Body}`);
|
|
@@ -26,23 +45,26 @@ function handleSMSWebhook(payload) {
|
|
|
26
45
|
}
|
|
27
46
|
return { response: "No pending prompt found." };
|
|
28
47
|
}
|
|
48
|
+
storeLatestResponse(
|
|
49
|
+
result.prompt?.id || "unknown",
|
|
50
|
+
result.response || Body,
|
|
51
|
+
result.action
|
|
52
|
+
);
|
|
29
53
|
if (result.action) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
42
|
-
}
|
|
54
|
+
const actionId = queueAction(
|
|
55
|
+
result.prompt?.id || "unknown",
|
|
56
|
+
result.response || Body,
|
|
57
|
+
result.action
|
|
58
|
+
);
|
|
59
|
+
console.log(`[sms-webhook] Queued action ${actionId}: ${result.action}`);
|
|
60
|
+
return {
|
|
61
|
+
response: `Got it! Queued action: ${result.action.substring(0, 30)}...`,
|
|
62
|
+
action: result.action,
|
|
63
|
+
queued: true
|
|
64
|
+
};
|
|
43
65
|
}
|
|
44
66
|
return {
|
|
45
|
-
response: `Received: ${result.response}
|
|
67
|
+
response: `Received: ${result.response}. Next action will be triggered.`
|
|
46
68
|
};
|
|
47
69
|
}
|
|
48
70
|
function twimlResponse(message) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/hooks/sms-webhook.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * SMS Webhook Handler for receiving Twilio responses\n * Can run as standalone server or integrate with existing Express app\n */\n\nimport { createServer, IncomingMessage, ServerResponse } from 'http';\nimport { parse as parseUrl } from 'url';\nimport { processIncomingResponse, loadSMSConfig } from './sms-notify.js';\nimport {
|
|
5
|
-
"mappings": ";;;;AAKA,SAAS,oBAAqD;AAC9D,SAAS,SAAS,gBAAgB;AAClC,SAAS,yBAAyB,qBAAqB;AACvD,SAAS,
|
|
4
|
+
"sourcesContent": ["/**\n * SMS Webhook Handler for receiving Twilio responses\n * Can run as standalone server or integrate with existing Express app\n */\n\nimport { createServer, IncomingMessage, ServerResponse } from 'http';\nimport { parse as parseUrl } from 'url';\nimport { existsSync, writeFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { processIncomingResponse, loadSMSConfig } from './sms-notify.js';\nimport { queueAction } from './sms-action-runner.js';\n\ninterface TwilioWebhookPayload {\n From: string;\n To: string;\n Body: string;\n MessageSid: string;\n}\n\nfunction parseFormData(body: string): Record<string, string> {\n const params = new URLSearchParams(body);\n const result: Record<string, string> = {};\n params.forEach((value, key) => {\n result[key] = value;\n });\n return result;\n}\n\n// Store response for Claude hook to pick up\nfunction storeLatestResponse(\n promptId: string,\n response: string,\n action?: string\n): void {\n const dir = join(homedir(), '.stackmemory');\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n const responsePath = join(dir, 'sms-latest-response.json');\n writeFileSync(\n responsePath,\n JSON.stringify({\n promptId,\n response,\n action,\n timestamp: new Date().toISOString(),\n })\n );\n}\n\nexport function handleSMSWebhook(payload: TwilioWebhookPayload): {\n response: string;\n action?: string;\n queued?: boolean;\n} {\n const { From, Body } = payload;\n\n console.log(`[sms-webhook] Received from ${From}: ${Body}`);\n\n const result = processIncomingResponse(From, Body);\n\n if (!result.matched) {\n if (result.prompt) {\n return {\n response: `Invalid response. Expected: ${result.prompt.options.map((o) => o.key).join(', ')}`,\n };\n }\n return { response: 'No pending prompt found.' };\n }\n\n // Store response for Claude hook\n storeLatestResponse(\n result.prompt?.id || 'unknown',\n result.response || Body,\n result.action\n );\n\n // Queue action for execution (instead of immediate execution)\n if (result.action) {\n const actionId = queueAction(\n result.prompt?.id || 'unknown',\n result.response || Body,\n result.action\n );\n console.log(`[sms-webhook] Queued action ${actionId}: ${result.action}`);\n\n return {\n response: `Got it! Queued action: ${result.action.substring(0, 30)}...`,\n action: result.action,\n queued: true,\n };\n }\n\n return {\n response: `Received: ${result.response}. Next action will be triggered.`,\n };\n}\n\n// TwiML response helper\nfunction twimlResponse(message: string): string {\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response>\n <Message>${escapeXml(message)}</Message>\n</Response>`;\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n// Standalone webhook server\nexport function startWebhookServer(port: number = 3456): void {\n const server = createServer(\n async (req: IncomingMessage, res: ServerResponse) => {\n const url = parseUrl(req.url || '/', true);\n\n // Health check\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'ok' }));\n return;\n }\n\n // SMS webhook endpoint\n if (url.pathname === '/sms' && req.method === 'POST') {\n let body = '';\n req.on('data', (chunk) => {\n body += chunk;\n });\n\n req.on('end', () => {\n try {\n const payload = parseFormData(\n body\n ) as unknown as TwilioWebhookPayload;\n const result = handleSMSWebhook(payload);\n\n res.writeHead(200, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse(result.response));\n } catch (err) {\n console.error('[sms-webhook] Error:', err);\n res.writeHead(500, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Error processing message'));\n }\n });\n return;\n }\n\n // Status endpoint\n if (url.pathname === '/status') {\n const config = loadSMSConfig();\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n enabled: config.enabled,\n pendingPrompts: config.pendingPrompts.length,\n })\n );\n return;\n }\n\n res.writeHead(404);\n res.end('Not found');\n }\n );\n\n server.listen(port, () => {\n console.log(`[sms-webhook] Server listening on port ${port}`);\n console.log(`[sms-webhook] Webhook URL: http://localhost:${port}/sms`);\n console.log(`[sms-webhook] Configure this URL in Twilio console`);\n });\n}\n\n// Express middleware for integration\nexport function smsWebhookMiddleware(\n req: { body: TwilioWebhookPayload },\n res: { type: (t: string) => void; send: (s: string) => void }\n): void {\n const result = handleSMSWebhook(req.body);\n res.type('text/xml');\n res.send(twimlResponse(result.response));\n}\n\n// CLI entry\nif (process.argv[1]?.endsWith('sms-webhook.js')) {\n const port = parseInt(process.env['SMS_WEBHOOK_PORT'] || '3456', 10);\n startWebhookServer(port);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackmemoryai/stackmemory",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
4
4
|
"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",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.0.0",
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code hook for processing SMS responses and triggering next actions
|
|
4
|
+
*
|
|
5
|
+
* This hook:
|
|
6
|
+
* 1. Checks for pending SMS responses on startup
|
|
7
|
+
* 2. Executes queued actions from SMS responses
|
|
8
|
+
* 3. Injects response context into Claude session
|
|
9
|
+
*
|
|
10
|
+
* Install: stackmemory notify install-response-hook
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const { execSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const QUEUE_PATH = path.join(
|
|
19
|
+
os.homedir(),
|
|
20
|
+
'.stackmemory',
|
|
21
|
+
'sms-action-queue.json'
|
|
22
|
+
);
|
|
23
|
+
const RESPONSE_PATH = path.join(
|
|
24
|
+
os.homedir(),
|
|
25
|
+
'.stackmemory',
|
|
26
|
+
'sms-latest-response.json'
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
function loadActionQueue() {
|
|
30
|
+
try {
|
|
31
|
+
if (fs.existsSync(QUEUE_PATH)) {
|
|
32
|
+
return JSON.parse(fs.readFileSync(QUEUE_PATH, 'utf8'));
|
|
33
|
+
}
|
|
34
|
+
} catch {}
|
|
35
|
+
return { actions: [] };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function saveActionQueue(queue) {
|
|
39
|
+
const dir = path.join(os.homedir(), '.stackmemory');
|
|
40
|
+
if (!fs.existsSync(dir)) {
|
|
41
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
fs.writeFileSync(QUEUE_PATH, JSON.stringify(queue, null, 2));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadLatestResponse() {
|
|
47
|
+
try {
|
|
48
|
+
if (fs.existsSync(RESPONSE_PATH)) {
|
|
49
|
+
const data = JSON.parse(fs.readFileSync(RESPONSE_PATH, 'utf8'));
|
|
50
|
+
// Only return if less than 5 minutes old
|
|
51
|
+
const age = Date.now() - new Date(data.timestamp).getTime();
|
|
52
|
+
if (age < 5 * 60 * 1000) {
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function clearLatestResponse() {
|
|
61
|
+
try {
|
|
62
|
+
if (fs.existsSync(RESPONSE_PATH)) {
|
|
63
|
+
fs.unlinkSync(RESPONSE_PATH);
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function executeAction(action) {
|
|
69
|
+
try {
|
|
70
|
+
console.error(`[sms-hook] Executing: ${action.action}`);
|
|
71
|
+
const output = execSync(action.action, {
|
|
72
|
+
encoding: 'utf8',
|
|
73
|
+
timeout: 60000,
|
|
74
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
75
|
+
});
|
|
76
|
+
return { success: true, output };
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return { success: false, error: err.message };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function processPendingActions() {
|
|
83
|
+
const queue = loadActionQueue();
|
|
84
|
+
const pending = queue.actions.filter((a) => a.status === 'pending');
|
|
85
|
+
|
|
86
|
+
if (pending.length === 0) return null;
|
|
87
|
+
|
|
88
|
+
const results = [];
|
|
89
|
+
|
|
90
|
+
for (const action of pending) {
|
|
91
|
+
action.status = 'running';
|
|
92
|
+
saveActionQueue(queue);
|
|
93
|
+
|
|
94
|
+
const result = executeAction(action);
|
|
95
|
+
|
|
96
|
+
action.status = result.success ? 'completed' : 'failed';
|
|
97
|
+
action.result = result.output;
|
|
98
|
+
action.error = result.error;
|
|
99
|
+
saveActionQueue(queue);
|
|
100
|
+
|
|
101
|
+
results.push({
|
|
102
|
+
action: action.action,
|
|
103
|
+
response: action.response,
|
|
104
|
+
success: result.success,
|
|
105
|
+
output: result.output?.substring(0, 500),
|
|
106
|
+
error: result.error,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return results;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Read hook input from stdin
|
|
114
|
+
let input = '';
|
|
115
|
+
process.stdin.setEncoding('utf8');
|
|
116
|
+
process.stdin.on('data', (chunk) => (input += chunk));
|
|
117
|
+
process.stdin.on('end', () => {
|
|
118
|
+
try {
|
|
119
|
+
const hookData = JSON.parse(input);
|
|
120
|
+
const { hook_type } = hookData;
|
|
121
|
+
|
|
122
|
+
// On session start, check for pending responses
|
|
123
|
+
if (hook_type === 'on_startup' || hook_type === 'pre_tool_use') {
|
|
124
|
+
// Check for SMS response waiting
|
|
125
|
+
const latestResponse = loadLatestResponse();
|
|
126
|
+
if (latestResponse) {
|
|
127
|
+
console.error(
|
|
128
|
+
`[sms-hook] SMS response received: "${latestResponse.response}"`
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Inject context for Claude
|
|
132
|
+
const context = {
|
|
133
|
+
type: 'sms_response',
|
|
134
|
+
response: latestResponse.response,
|
|
135
|
+
promptId: latestResponse.promptId,
|
|
136
|
+
timestamp: latestResponse.timestamp,
|
|
137
|
+
message: `User responded via SMS: "${latestResponse.response}"`,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
clearLatestResponse();
|
|
141
|
+
|
|
142
|
+
console.log(
|
|
143
|
+
JSON.stringify({
|
|
144
|
+
decision: 'allow',
|
|
145
|
+
context: context,
|
|
146
|
+
user_message: `[SMS Response] User replied: "${latestResponse.response}"`,
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Process any pending actions
|
|
153
|
+
const results = processPendingActions();
|
|
154
|
+
if (results && results.length > 0) {
|
|
155
|
+
console.error(`[sms-hook] Processed ${results.length} action(s)`);
|
|
156
|
+
|
|
157
|
+
const summary = results
|
|
158
|
+
.map((r) =>
|
|
159
|
+
r.success
|
|
160
|
+
? `Executed: ${r.action.substring(0, 50)}...`
|
|
161
|
+
: `Failed: ${r.action.substring(0, 50)}... (${r.error})`
|
|
162
|
+
)
|
|
163
|
+
.join('\n');
|
|
164
|
+
|
|
165
|
+
console.log(
|
|
166
|
+
JSON.stringify({
|
|
167
|
+
decision: 'allow',
|
|
168
|
+
context: {
|
|
169
|
+
type: 'sms_actions_executed',
|
|
170
|
+
results,
|
|
171
|
+
},
|
|
172
|
+
user_message: `[SMS Actions] Executed queued actions:\n${summary}`,
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Default: allow everything
|
|
180
|
+
console.log(JSON.stringify({ decision: 'allow' }));
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error('[sms-hook] Error:', err.message);
|
|
183
|
+
console.log(JSON.stringify({ decision: 'allow' }));
|
|
184
|
+
}
|
|
185
|
+
});
|