@stackmemoryai/stackmemory 0.5.7 → 0.5.9

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.
@@ -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": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,286 @@
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
+ const CONFIG_PATH = join(homedir(), ".stackmemory", "sms-notify.json");
9
+ const DEFAULT_CONFIG = {
10
+ enabled: false,
11
+ notifyOn: {
12
+ taskComplete: true,
13
+ reviewReady: true,
14
+ error: true,
15
+ custom: true
16
+ },
17
+ quietHours: {
18
+ enabled: false,
19
+ start: "22:00",
20
+ end: "08:00"
21
+ },
22
+ responseTimeout: 300,
23
+ // 5 minutes
24
+ pendingPrompts: []
25
+ };
26
+ function loadSMSConfig() {
27
+ try {
28
+ if (existsSync(CONFIG_PATH)) {
29
+ const data = readFileSync(CONFIG_PATH, "utf8");
30
+ return { ...DEFAULT_CONFIG, ...JSON.parse(data) };
31
+ }
32
+ } catch {
33
+ }
34
+ const config = { ...DEFAULT_CONFIG };
35
+ if (process.env["TWILIO_ACCOUNT_SID"]) {
36
+ config.accountSid = process.env["TWILIO_ACCOUNT_SID"];
37
+ }
38
+ if (process.env["TWILIO_AUTH_TOKEN"]) {
39
+ config.authToken = process.env["TWILIO_AUTH_TOKEN"];
40
+ }
41
+ if (process.env["TWILIO_FROM_NUMBER"]) {
42
+ config.fromNumber = process.env["TWILIO_FROM_NUMBER"];
43
+ }
44
+ if (process.env["TWILIO_TO_NUMBER"]) {
45
+ config.toNumber = process.env["TWILIO_TO_NUMBER"];
46
+ }
47
+ return config;
48
+ }
49
+ function saveSMSConfig(config) {
50
+ try {
51
+ const dir = join(homedir(), ".stackmemory");
52
+ if (!existsSync(dir)) {
53
+ mkdirSync(dir, { recursive: true });
54
+ }
55
+ const safeConfig = { ...config };
56
+ delete safeConfig.accountSid;
57
+ delete safeConfig.authToken;
58
+ writeFileSync(CONFIG_PATH, JSON.stringify(safeConfig, null, 2));
59
+ } catch {
60
+ }
61
+ }
62
+ function isQuietHours(config) {
63
+ if (!config.quietHours?.enabled) return false;
64
+ const now = /* @__PURE__ */ new Date();
65
+ const currentTime = now.getHours() * 60 + now.getMinutes();
66
+ const [startH, startM] = config.quietHours.start.split(":").map(Number);
67
+ const [endH, endM] = config.quietHours.end.split(":").map(Number);
68
+ const startTime = startH * 60 + startM;
69
+ const endTime = endH * 60 + endM;
70
+ if (startTime > endTime) {
71
+ return currentTime >= startTime || currentTime < endTime;
72
+ }
73
+ return currentTime >= startTime && currentTime < endTime;
74
+ }
75
+ function generatePromptId() {
76
+ return Math.random().toString(36).substring(2, 10);
77
+ }
78
+ function formatPromptMessage(payload) {
79
+ let message = `${payload.title}
80
+
81
+ ${payload.message}`;
82
+ if (payload.prompt) {
83
+ message += "\n\n";
84
+ if (payload.prompt.question) {
85
+ message += `${payload.prompt.question}
86
+ `;
87
+ }
88
+ if (payload.prompt.type === "yesno") {
89
+ message += "Reply Y for Yes, N for No";
90
+ } else if (payload.prompt.type === "options" && payload.prompt.options) {
91
+ payload.prompt.options.forEach((opt) => {
92
+ message += `${opt.key}. ${opt.label}
93
+ `;
94
+ });
95
+ message += "\nReply with number to select";
96
+ } else if (payload.prompt.type === "freeform") {
97
+ message += "Reply with your response";
98
+ }
99
+ }
100
+ return message;
101
+ }
102
+ async function sendSMSNotification(payload) {
103
+ const config = loadSMSConfig();
104
+ if (!config.enabled) {
105
+ return { success: false, error: "SMS notifications disabled" };
106
+ }
107
+ const typeMap = {
108
+ task_complete: "taskComplete",
109
+ review_ready: "reviewReady",
110
+ error: "error",
111
+ custom: "custom"
112
+ };
113
+ if (!config.notifyOn[typeMap[payload.type]]) {
114
+ return {
115
+ success: false,
116
+ error: `Notifications for ${payload.type} disabled`
117
+ };
118
+ }
119
+ if (isQuietHours(config)) {
120
+ return { success: false, error: "Quiet hours active" };
121
+ }
122
+ if (!config.accountSid || !config.authToken || !config.fromNumber || !config.toNumber) {
123
+ return {
124
+ success: false,
125
+ error: "Missing Twilio credentials. Set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER, TWILIO_TO_NUMBER"
126
+ };
127
+ }
128
+ const message = formatPromptMessage(payload);
129
+ let promptId;
130
+ if (payload.prompt) {
131
+ promptId = generatePromptId();
132
+ const expiresAt = new Date(
133
+ Date.now() + config.responseTimeout * 1e3
134
+ ).toISOString();
135
+ const pendingPrompt = {
136
+ id: promptId,
137
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
138
+ message: payload.message,
139
+ options: payload.prompt.options || [],
140
+ type: payload.prompt.type,
141
+ expiresAt
142
+ };
143
+ config.pendingPrompts.push(pendingPrompt);
144
+ saveSMSConfig(config);
145
+ }
146
+ try {
147
+ const twilioUrl = `https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`;
148
+ const response = await fetch(twilioUrl, {
149
+ method: "POST",
150
+ headers: {
151
+ Authorization: "Basic " + Buffer.from(`${config.accountSid}:${config.authToken}`).toString(
152
+ "base64"
153
+ ),
154
+ "Content-Type": "application/x-www-form-urlencoded"
155
+ },
156
+ body: new URLSearchParams({
157
+ From: config.fromNumber,
158
+ To: config.toNumber,
159
+ Body: message
160
+ })
161
+ });
162
+ if (!response.ok) {
163
+ const errorData = await response.text();
164
+ return { success: false, error: `Twilio error: ${errorData}` };
165
+ }
166
+ return { success: true, promptId };
167
+ } catch (err) {
168
+ return {
169
+ success: false,
170
+ error: `Failed to send SMS: ${err instanceof Error ? err.message : String(err)}`
171
+ };
172
+ }
173
+ }
174
+ function processIncomingResponse(from, body) {
175
+ const config = loadSMSConfig();
176
+ const response = body.trim().toLowerCase();
177
+ const now = /* @__PURE__ */ new Date();
178
+ const validPrompts = config.pendingPrompts.filter(
179
+ (p) => new Date(p.expiresAt) > now
180
+ );
181
+ if (validPrompts.length === 0) {
182
+ return { matched: false };
183
+ }
184
+ const prompt = validPrompts[validPrompts.length - 1];
185
+ let matchedOption;
186
+ if (prompt.type === "yesno") {
187
+ if (response === "y" || response === "yes") {
188
+ matchedOption = { key: "y", label: "Yes" };
189
+ } else if (response === "n" || response === "no") {
190
+ matchedOption = { key: "n", label: "No" };
191
+ }
192
+ } else if (prompt.type === "options") {
193
+ matchedOption = prompt.options.find(
194
+ (opt) => opt.key.toLowerCase() === response
195
+ );
196
+ } else if (prompt.type === "freeform") {
197
+ matchedOption = { key: response, label: response };
198
+ }
199
+ config.pendingPrompts = config.pendingPrompts.filter(
200
+ (p) => p.id !== prompt.id
201
+ );
202
+ saveSMSConfig(config);
203
+ if (matchedOption) {
204
+ return {
205
+ matched: true,
206
+ prompt,
207
+ response: matchedOption.key,
208
+ action: matchedOption.action
209
+ };
210
+ }
211
+ return { matched: false, prompt };
212
+ }
213
+ async function notifyReviewReady(title, description, options) {
214
+ const payload = {
215
+ type: "review_ready",
216
+ title: `Review Ready: ${title}`,
217
+ message: description
218
+ };
219
+ if (options && options.length > 0) {
220
+ payload.prompt = {
221
+ type: "options",
222
+ options: options.map((opt, i) => ({
223
+ key: String(i + 1),
224
+ label: opt.label,
225
+ action: opt.action
226
+ })),
227
+ question: "What would you like to do?"
228
+ };
229
+ }
230
+ return sendSMSNotification(payload);
231
+ }
232
+ async function notifyWithYesNo(title, question, yesAction, noAction) {
233
+ return sendSMSNotification({
234
+ type: "custom",
235
+ title,
236
+ message: question,
237
+ prompt: {
238
+ type: "yesno",
239
+ options: [
240
+ { key: "y", label: "Yes", action: yesAction },
241
+ { key: "n", label: "No", action: noAction }
242
+ ]
243
+ }
244
+ });
245
+ }
246
+ async function notifyTaskComplete(taskName, summary) {
247
+ return sendSMSNotification({
248
+ type: "task_complete",
249
+ title: `Task Complete: ${taskName}`,
250
+ message: summary
251
+ });
252
+ }
253
+ async function notifyError(error, context) {
254
+ return sendSMSNotification({
255
+ type: "error",
256
+ title: "Error Alert",
257
+ message: context ? `${error}
258
+
259
+ Context: ${context}` : error
260
+ });
261
+ }
262
+ function cleanupExpiredPrompts() {
263
+ const config = loadSMSConfig();
264
+ const now = /* @__PURE__ */ new Date();
265
+ const before = config.pendingPrompts.length;
266
+ config.pendingPrompts = config.pendingPrompts.filter(
267
+ (p) => new Date(p.expiresAt) > now
268
+ );
269
+ const removed = before - config.pendingPrompts.length;
270
+ if (removed > 0) {
271
+ saveSMSConfig(config);
272
+ }
273
+ return removed;
274
+ }
275
+ export {
276
+ cleanupExpiredPrompts,
277
+ loadSMSConfig,
278
+ notifyError,
279
+ notifyReviewReady,
280
+ notifyTaskComplete,
281
+ notifyWithYesNo,
282
+ processIncomingResponse,
283
+ saveSMSConfig,
284
+ sendSMSNotification
285
+ };
286
+ //# sourceMappingURL=sms-notify.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,144 @@
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 { createServer } from "http";
6
+ import { parse as parseUrl } from "url";
7
+ import { existsSync, writeFileSync, mkdirSync } from "fs";
8
+ import { join } from "path";
9
+ import { homedir } from "os";
10
+ import { processIncomingResponse, loadSMSConfig } from "./sms-notify.js";
11
+ import { queueAction } from "./sms-action-runner.js";
12
+ function parseFormData(body) {
13
+ const params = new URLSearchParams(body);
14
+ const result = {};
15
+ params.forEach((value, key) => {
16
+ result[key] = value;
17
+ });
18
+ return result;
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
+ }
36
+ function handleSMSWebhook(payload) {
37
+ const { From, Body } = payload;
38
+ console.log(`[sms-webhook] Received from ${From}: ${Body}`);
39
+ const result = processIncomingResponse(From, Body);
40
+ if (!result.matched) {
41
+ if (result.prompt) {
42
+ return {
43
+ response: `Invalid response. Expected: ${result.prompt.options.map((o) => o.key).join(", ")}`
44
+ };
45
+ }
46
+ return { response: "No pending prompt found." };
47
+ }
48
+ storeLatestResponse(
49
+ result.prompt?.id || "unknown",
50
+ result.response || Body,
51
+ result.action
52
+ );
53
+ if (result.action) {
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
+ };
65
+ }
66
+ return {
67
+ response: `Received: ${result.response}. Next action will be triggered.`
68
+ };
69
+ }
70
+ function twimlResponse(message) {
71
+ return `<?xml version="1.0" encoding="UTF-8"?>
72
+ <Response>
73
+ <Message>${escapeXml(message)}</Message>
74
+ </Response>`;
75
+ }
76
+ function escapeXml(str) {
77
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
78
+ }
79
+ function startWebhookServer(port = 3456) {
80
+ const server = createServer(
81
+ async (req, res) => {
82
+ const url = parseUrl(req.url || "/", true);
83
+ if (url.pathname === "/health") {
84
+ res.writeHead(200, { "Content-Type": "application/json" });
85
+ res.end(JSON.stringify({ status: "ok" }));
86
+ return;
87
+ }
88
+ if (url.pathname === "/sms" && req.method === "POST") {
89
+ let body = "";
90
+ req.on("data", (chunk) => {
91
+ body += chunk;
92
+ });
93
+ req.on("end", () => {
94
+ try {
95
+ const payload = parseFormData(
96
+ body
97
+ );
98
+ const result = handleSMSWebhook(payload);
99
+ res.writeHead(200, { "Content-Type": "text/xml" });
100
+ res.end(twimlResponse(result.response));
101
+ } catch (err) {
102
+ console.error("[sms-webhook] Error:", err);
103
+ res.writeHead(500, { "Content-Type": "text/xml" });
104
+ res.end(twimlResponse("Error processing message"));
105
+ }
106
+ });
107
+ return;
108
+ }
109
+ if (url.pathname === "/status") {
110
+ const config = loadSMSConfig();
111
+ res.writeHead(200, { "Content-Type": "application/json" });
112
+ res.end(
113
+ JSON.stringify({
114
+ enabled: config.enabled,
115
+ pendingPrompts: config.pendingPrompts.length
116
+ })
117
+ );
118
+ return;
119
+ }
120
+ res.writeHead(404);
121
+ res.end("Not found");
122
+ }
123
+ );
124
+ server.listen(port, () => {
125
+ console.log(`[sms-webhook] Server listening on port ${port}`);
126
+ console.log(`[sms-webhook] Webhook URL: http://localhost:${port}/sms`);
127
+ console.log(`[sms-webhook] Configure this URL in Twilio console`);
128
+ });
129
+ }
130
+ function smsWebhookMiddleware(req, res) {
131
+ const result = handleSMSWebhook(req.body);
132
+ res.type("text/xml");
133
+ res.send(twimlResponse(result.response));
134
+ }
135
+ if (process.argv[1]?.endsWith("sms-webhook.js")) {
136
+ const port = parseInt(process.env["SMS_WEBHOOK_PORT"] || "3456", 10);
137
+ startWebhookServer(port);
138
+ }
139
+ export {
140
+ handleSMSWebhook,
141
+ smsWebhookMiddleware,
142
+ startWebhookServer
143
+ };
144
+ //# sourceMappingURL=sms-webhook.js.map