@stackmemoryai/stackmemory 0.5.16 → 0.5.17
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/hooks/sms-action-runner.js +84 -16
- package/dist/hooks/sms-action-runner.js.map +2 -2
- package/dist/hooks/sms-webhook.js +94 -16
- package/dist/hooks/sms-webhook.js.map +2 -2
- package/package.json +1 -1
- package/templates/claude-hooks/auto-background-hook.js +9 -8
- package/templates/claude-hooks/sms-response-handler.js +8 -19
|
@@ -5,7 +5,31 @@ const __dirname = __pathDirname(__filename);
|
|
|
5
5
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
import { homedir } from "os";
|
|
8
|
-
import { execSync } from "child_process";
|
|
8
|
+
import { execSync, execFileSync } from "child_process";
|
|
9
|
+
import { randomBytes } from "crypto";
|
|
10
|
+
const SAFE_ACTION_PATTERNS = [
|
|
11
|
+
// Git/GitHub CLI commands (limited to safe operations)
|
|
12
|
+
{ pattern: /^gh pr (view|list|status|checks) (\d+)$/ },
|
|
13
|
+
{ pattern: /^gh pr review (\d+) --approve$/ },
|
|
14
|
+
{ pattern: /^gh pr merge (\d+) --squash$/ },
|
|
15
|
+
{ pattern: /^gh issue (view|list) (\d+)?$/ },
|
|
16
|
+
// NPM commands (limited to safe operations)
|
|
17
|
+
{ pattern: /^npm run (build|test|lint|lint:fix|test:run)$/ },
|
|
18
|
+
{ pattern: /^npm (test|run build)$/ },
|
|
19
|
+
// StackMemory commands
|
|
20
|
+
{ pattern: /^stackmemory (status|notify check|context list)$/ },
|
|
21
|
+
// Simple echo/confirmation (no variables)
|
|
22
|
+
{ pattern: /^echo "?(Done|OK|Confirmed|Acknowledged)"?$/ }
|
|
23
|
+
];
|
|
24
|
+
function isActionAllowed(action) {
|
|
25
|
+
const trimmed = action.trim();
|
|
26
|
+
return SAFE_ACTION_PATTERNS.some(({ pattern, validate }) => {
|
|
27
|
+
const match = trimmed.match(pattern);
|
|
28
|
+
if (!match) return false;
|
|
29
|
+
if (validate && !validate(match)) return false;
|
|
30
|
+
return true;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
9
33
|
const QUEUE_PATH = join(homedir(), ".stackmemory", "sms-action-queue.json");
|
|
10
34
|
function loadActionQueue() {
|
|
11
35
|
try {
|
|
@@ -28,7 +52,7 @@ function saveActionQueue(queue) {
|
|
|
28
52
|
}
|
|
29
53
|
function queueAction(promptId, response, action) {
|
|
30
54
|
const queue = loadActionQueue();
|
|
31
|
-
const id =
|
|
55
|
+
const id = randomBytes(8).toString("hex");
|
|
32
56
|
queue.actions.push({
|
|
33
57
|
id,
|
|
34
58
|
promptId,
|
|
@@ -40,6 +64,32 @@ function queueAction(promptId, response, action) {
|
|
|
40
64
|
saveActionQueue(queue);
|
|
41
65
|
return id;
|
|
42
66
|
}
|
|
67
|
+
function executeActionSafe(action, _response) {
|
|
68
|
+
if (!isActionAllowed(action)) {
|
|
69
|
+
console.error(`[sms-action] Action not in allowlist: ${action}`);
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
error: `Action not allowed. Only pre-approved commands can be executed via SMS.`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
console.log(`[sms-action] Executing safe action: ${action}`);
|
|
77
|
+
const parts = action.split(" ");
|
|
78
|
+
const cmd = parts[0];
|
|
79
|
+
const args = parts.slice(1);
|
|
80
|
+
const output = execFileSync(cmd, args, {
|
|
81
|
+
encoding: "utf8",
|
|
82
|
+
timeout: 6e4,
|
|
83
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
84
|
+
shell: false
|
|
85
|
+
// Explicitly disable shell
|
|
86
|
+
});
|
|
87
|
+
return { success: true, output };
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
90
|
+
return { success: false, error };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
43
93
|
function getPendingActions() {
|
|
44
94
|
const queue = loadActionQueue();
|
|
45
95
|
return queue.actions.filter((a) => a.status === "pending");
|
|
@@ -110,22 +160,39 @@ function cleanupOldActions() {
|
|
|
110
160
|
return 0;
|
|
111
161
|
}
|
|
112
162
|
const ACTION_TEMPLATES = {
|
|
113
|
-
// Git/PR actions
|
|
114
|
-
approvePR: (prNumber) =>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
163
|
+
// Git/PR actions (PR numbers must be validated as integers)
|
|
164
|
+
approvePR: (prNumber) => {
|
|
165
|
+
if (!/^\d+$/.test(prNumber)) {
|
|
166
|
+
throw new Error("Invalid PR number");
|
|
167
|
+
}
|
|
168
|
+
return `gh pr review ${prNumber} --approve`;
|
|
169
|
+
},
|
|
170
|
+
mergePR: (prNumber) => {
|
|
171
|
+
if (!/^\d+$/.test(prNumber)) {
|
|
172
|
+
throw new Error("Invalid PR number");
|
|
173
|
+
}
|
|
174
|
+
return `gh pr merge ${prNumber} --squash`;
|
|
175
|
+
},
|
|
176
|
+
viewPR: (prNumber) => {
|
|
177
|
+
if (!/^\d+$/.test(prNumber)) {
|
|
178
|
+
throw new Error("Invalid PR number");
|
|
179
|
+
}
|
|
180
|
+
return `gh pr view ${prNumber}`;
|
|
181
|
+
},
|
|
182
|
+
// Build actions (no user input)
|
|
123
183
|
rebuild: () => `npm run build`,
|
|
124
|
-
retest: () => `npm test`,
|
|
184
|
+
retest: () => `npm run test:run`,
|
|
125
185
|
lint: () => `npm run lint:fix`,
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
186
|
+
// Status actions (no user input)
|
|
187
|
+
status: () => `stackmemory status`,
|
|
188
|
+
checkNotifications: () => `stackmemory notify check`
|
|
189
|
+
// REMOVED for security - these templates allowed arbitrary user input:
|
|
190
|
+
// - requestChanges (allowed arbitrary message)
|
|
191
|
+
// - closePR (could be used maliciously)
|
|
192
|
+
// - deploy/rollback (too dangerous for SMS)
|
|
193
|
+
// - verifyDeployment (allowed arbitrary URL)
|
|
194
|
+
// - notifySlack (allowed arbitrary message - command injection)
|
|
195
|
+
// - notifyTeam (allowed arbitrary message - command injection)
|
|
129
196
|
};
|
|
130
197
|
function createAction(template, ...args) {
|
|
131
198
|
const fn = ACTION_TEMPLATES[template];
|
|
@@ -157,6 +224,7 @@ export {
|
|
|
157
224
|
cleanupOldActions,
|
|
158
225
|
createAction,
|
|
159
226
|
executeAction,
|
|
227
|
+
executeActionSafe,
|
|
160
228
|
getPendingActions,
|
|
161
229
|
handleSMSResponse,
|
|
162
230
|
loadActionQueue,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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 =
|
|
5
|
-
"mappings": ";;;;
|
|
4
|
+
"sourcesContent": ["/**\n * SMS Action Runner - Executes actions based on SMS responses\n * Bridges SMS responses to Claude Code actions\n *\n * Security: Uses allowlist-based action execution to prevent command injection\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { execSync, execFileSync } from 'child_process';\nimport { randomBytes } from 'crypto';\n\n// Allowlist of safe action patterns\nconst SAFE_ACTION_PATTERNS: Array<{\n pattern: RegExp;\n validate?: (match: RegExpMatchArray) => boolean;\n}> = [\n // Git/GitHub CLI commands (limited to safe operations)\n { pattern: /^gh pr (view|list|status|checks) (\\d+)$/ },\n { pattern: /^gh pr review (\\d+) --approve$/ },\n { pattern: /^gh pr merge (\\d+) --squash$/ },\n { pattern: /^gh issue (view|list) (\\d+)?$/ },\n\n // NPM commands (limited to safe operations)\n { pattern: /^npm run (build|test|lint|lint:fix|test:run)$/ },\n { pattern: /^npm (test|run build)$/ },\n\n // StackMemory commands\n { pattern: /^stackmemory (status|notify check|context list)$/ },\n\n // Simple echo/confirmation (no variables)\n { pattern: /^echo \"?(Done|OK|Confirmed|Acknowledged)\"?$/ },\n];\n\n/**\n * Check if an action is in the allowlist\n */\nfunction isActionAllowed(action: string): boolean {\n const trimmed = action.trim();\n return SAFE_ACTION_PATTERNS.some(({ pattern, validate }) => {\n const match = trimmed.match(pattern);\n if (!match) return false;\n if (validate && !validate(match)) return false;\n return true;\n });\n}\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 // Use cryptographically secure random ID\n const id = randomBytes(8).toString('hex');\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\n/**\n * Execute an action safely using allowlist validation\n * This prevents command injection by only allowing pre-approved commands\n */\nexport function executeActionSafe(\n action: string,\n _response: string\n): { success: boolean; output?: string; error?: string } {\n // Check if action is in allowlist\n if (!isActionAllowed(action)) {\n console.error(`[sms-action] Action not in allowlist: ${action}`);\n return {\n success: false,\n error: `Action not allowed. Only pre-approved commands can be executed via SMS.`,\n };\n }\n\n try {\n console.log(`[sms-action] Executing safe action: ${action}`);\n\n // Parse the action into command and args\n const parts = action.split(' ');\n const cmd = parts[0];\n const args = parts.slice(1);\n\n // Use execFileSync for commands without shell interpretation\n // This prevents shell injection even if the allowlist is somehow bypassed\n const output = execFileSync(cmd, args, {\n encoding: 'utf8',\n timeout: 60000,\n stdio: ['pipe', 'pipe', 'pipe'],\n shell: false, // Explicitly disable shell\n });\n\n return { success: true, output };\n } catch (err) {\n const error = err instanceof Error ? err.message : String(err);\n return { success: false, error };\n }\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 *\n * SECURITY NOTE: These templates return command strings that must be\n * validated against SAFE_ACTION_PATTERNS before execution.\n * Templates that accept user input are removed to prevent injection.\n */\nexport const ACTION_TEMPLATES = {\n // Git/PR actions (PR numbers must be validated as integers)\n approvePR: (prNumber: string) => {\n // Validate PR number is numeric only\n if (!/^\\d+$/.test(prNumber)) {\n throw new Error('Invalid PR number');\n }\n return `gh pr review ${prNumber} --approve`;\n },\n mergePR: (prNumber: string) => {\n if (!/^\\d+$/.test(prNumber)) {\n throw new Error('Invalid PR number');\n }\n return `gh pr merge ${prNumber} --squash`;\n },\n viewPR: (prNumber: string) => {\n if (!/^\\d+$/.test(prNumber)) {\n throw new Error('Invalid PR number');\n }\n return `gh pr view ${prNumber}`;\n },\n\n // Build actions (no user input)\n rebuild: () => `npm run build`,\n retest: () => `npm run test:run`,\n lint: () => `npm run lint:fix`,\n\n // Status actions (no user input)\n status: () => `stackmemory status`,\n checkNotifications: () => `stackmemory notify check`,\n\n // REMOVED for security - these templates allowed arbitrary user input:\n // - requestChanges (allowed arbitrary message)\n // - closePR (could be used maliciously)\n // - deploy/rollback (too dangerous for SMS)\n // - verifyDeployment (allowed arbitrary URL)\n // - notifySlack (allowed arbitrary message - command injection)\n // - notifyTeam (allowed arbitrary message - command injection)\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
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -7,9 +7,45 @@ import { parse as parseUrl } from "url";
|
|
|
7
7
|
import { existsSync, writeFileSync, mkdirSync, readFileSync } from "fs";
|
|
8
8
|
import { join } from "path";
|
|
9
9
|
import { homedir } from "os";
|
|
10
|
+
import { createHmac } from "crypto";
|
|
11
|
+
import { execFileSync } from "child_process";
|
|
10
12
|
import { processIncomingResponse, loadSMSConfig } from "./sms-notify.js";
|
|
11
|
-
import { queueAction } from "./sms-action-runner.js";
|
|
12
|
-
|
|
13
|
+
import { queueAction, executeActionSafe } from "./sms-action-runner.js";
|
|
14
|
+
const MAX_BODY_SIZE = 50 * 1024;
|
|
15
|
+
const RATE_LIMIT_WINDOW_MS = 60 * 1e3;
|
|
16
|
+
const RATE_LIMIT_MAX_REQUESTS = 30;
|
|
17
|
+
const rateLimitStore = /* @__PURE__ */ new Map();
|
|
18
|
+
function checkRateLimit(ip) {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
const record = rateLimitStore.get(ip);
|
|
21
|
+
if (!record || now > record.resetTime) {
|
|
22
|
+
rateLimitStore.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS });
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (record.count >= RATE_LIMIT_MAX_REQUESTS) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
record.count++;
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
function verifyTwilioSignature(url, params, signature) {
|
|
32
|
+
const authToken = process.env["TWILIO_AUTH_TOKEN"];
|
|
33
|
+
if (!authToken) {
|
|
34
|
+
console.warn(
|
|
35
|
+
"[sms-webhook] TWILIO_AUTH_TOKEN not set, skipping signature verification"
|
|
36
|
+
);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
const sortedKeys = Object.keys(params).sort();
|
|
40
|
+
let data = url;
|
|
41
|
+
for (const key of sortedKeys) {
|
|
42
|
+
data += key + params[key];
|
|
43
|
+
}
|
|
44
|
+
const hmac = createHmac("sha1", authToken);
|
|
45
|
+
hmac.update(data);
|
|
46
|
+
const expectedSignature = hmac.digest("base64");
|
|
47
|
+
return signature === expectedSignature;
|
|
48
|
+
}
|
|
13
49
|
function parseFormData(body) {
|
|
14
50
|
const params = new URLSearchParams(body);
|
|
15
51
|
const result = {};
|
|
@@ -54,30 +90,28 @@ function handleSMSWebhook(payload) {
|
|
|
54
90
|
triggerResponseNotification(result.response || Body);
|
|
55
91
|
if (result.action) {
|
|
56
92
|
console.log(`[sms-webhook] Executing action: ${result.action}`);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
93
|
+
const actionResult = executeActionSafe(
|
|
94
|
+
result.action,
|
|
95
|
+
result.response || Body
|
|
96
|
+
);
|
|
97
|
+
if (actionResult.success) {
|
|
63
98
|
console.log(
|
|
64
|
-
`[sms-webhook] Action completed: ${output.substring(0, 200)}`
|
|
99
|
+
`[sms-webhook] Action completed: ${(actionResult.output || "").substring(0, 200)}`
|
|
65
100
|
);
|
|
66
101
|
return {
|
|
67
102
|
response: `Done! Action executed successfully.`,
|
|
68
103
|
action: result.action,
|
|
69
104
|
queued: false
|
|
70
105
|
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
console.log(`[sms-webhook] Action failed: ${error}`);
|
|
106
|
+
} else {
|
|
107
|
+
console.log(`[sms-webhook] Action failed: ${actionResult.error}`);
|
|
74
108
|
queueAction(
|
|
75
109
|
result.prompt?.id || "unknown",
|
|
76
110
|
result.response || Body,
|
|
77
111
|
result.action
|
|
78
112
|
);
|
|
79
113
|
return {
|
|
80
|
-
response: `Action failed, queued for retry: ${error.substring(0, 50)}`,
|
|
114
|
+
response: `Action failed, queued for retry: ${(actionResult.error || "").substring(0, 50)}`,
|
|
81
115
|
action: result.action,
|
|
82
116
|
queued: true
|
|
83
117
|
};
|
|
@@ -87,11 +121,18 @@ function handleSMSWebhook(payload) {
|
|
|
87
121
|
response: `Received: ${result.response}. Next action will be triggered.`
|
|
88
122
|
};
|
|
89
123
|
}
|
|
124
|
+
function escapeAppleScript(str) {
|
|
125
|
+
return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").substring(0, 200);
|
|
126
|
+
}
|
|
90
127
|
function triggerResponseNotification(response) {
|
|
91
|
-
const
|
|
128
|
+
const safeMessage = escapeAppleScript(`SMS Response: ${response}`);
|
|
92
129
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
130
|
+
execFileSync(
|
|
131
|
+
"osascript",
|
|
132
|
+
[
|
|
133
|
+
"-e",
|
|
134
|
+
`display notification "${safeMessage}" with title "StackMemory" sound name "Glass"`
|
|
135
|
+
],
|
|
95
136
|
{ stdio: "ignore", timeout: 5e3 }
|
|
96
137
|
);
|
|
97
138
|
} catch {
|
|
@@ -132,15 +173,52 @@ function startWebhookServer(port = 3456) {
|
|
|
132
173
|
return;
|
|
133
174
|
}
|
|
134
175
|
if ((url.pathname === "/sms" || url.pathname === "/sms/incoming" || url.pathname === "/webhook") && req.method === "POST") {
|
|
176
|
+
const clientIp = req.socket.remoteAddress || "unknown";
|
|
177
|
+
if (!checkRateLimit(clientIp)) {
|
|
178
|
+
res.writeHead(429, {
|
|
179
|
+
"Content-Type": "text/xml",
|
|
180
|
+
"Retry-After": "60"
|
|
181
|
+
});
|
|
182
|
+
res.end(twimlResponse("Too many requests. Please try again later."));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const contentType = req.headers["content-type"] || "";
|
|
186
|
+
if (!contentType.includes("application/x-www-form-urlencoded")) {
|
|
187
|
+
res.writeHead(400, { "Content-Type": "text/xml" });
|
|
188
|
+
res.end(twimlResponse("Invalid content type"));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
135
191
|
let body = "";
|
|
192
|
+
let bodyTooLarge = false;
|
|
136
193
|
req.on("data", (chunk) => {
|
|
137
194
|
body += chunk;
|
|
195
|
+
if (body.length > MAX_BODY_SIZE) {
|
|
196
|
+
bodyTooLarge = true;
|
|
197
|
+
req.destroy();
|
|
198
|
+
}
|
|
138
199
|
});
|
|
139
200
|
req.on("end", () => {
|
|
201
|
+
if (bodyTooLarge) {
|
|
202
|
+
res.writeHead(413, { "Content-Type": "text/xml" });
|
|
203
|
+
res.end(twimlResponse("Request too large"));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
140
206
|
try {
|
|
141
207
|
const payload = parseFormData(
|
|
142
208
|
body
|
|
143
209
|
);
|
|
210
|
+
const twilioSignature = req.headers["x-twilio-signature"];
|
|
211
|
+
const webhookUrl = `${req.headers["x-forwarded-proto"] || "http"}://${req.headers.host}${req.url}`;
|
|
212
|
+
if (twilioSignature && !verifyTwilioSignature(
|
|
213
|
+
webhookUrl,
|
|
214
|
+
payload,
|
|
215
|
+
twilioSignature
|
|
216
|
+
)) {
|
|
217
|
+
console.error("[sms-webhook] Invalid Twilio signature");
|
|
218
|
+
res.writeHead(401, { "Content-Type": "text/xml" });
|
|
219
|
+
res.end(twimlResponse("Unauthorized"));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
144
222
|
const result = handleSMSWebhook(payload);
|
|
145
223
|
res.writeHead(200, { "Content-Type": "text/xml" });
|
|
146
224
|
res.end(twimlResponse(result.response));
|
|
@@ -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 { existsSync, writeFileSync, mkdirSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { processIncomingResponse, loadSMSConfig } from './sms-notify.js';\nimport { queueAction } from './sms-action-runner.js';\nimport { execSync } from 'child_process';\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 // Trigger notification to alert user/Claude\n triggerResponseNotification(result.response || Body);\n\n // Execute action immediately if present\n if (result.action) {\n console.log(`[sms-webhook] Executing action: ${result.action}`);\n\n try {\n const output = execSync(result.action, {\n encoding: 'utf8',\n timeout: 60000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n console.log(\n `[sms-webhook] Action completed: ${output.substring(0, 200)}`\n );\n\n return {\n response: `Done! Action executed successfully.`,\n action: result.action,\n queued: false,\n };\n } catch (err) {\n const error = err instanceof Error ? err.message : String(err);\n console.log(`[sms-webhook] Action failed: ${error}`);\n\n // Queue for retry\n queueAction(\n result.prompt?.id || 'unknown',\n result.response || Body,\n result.action\n );\n\n return {\n response: `Action failed, queued for retry: ${error.substring(0, 50)}`,\n action: result.action,\n queued: true,\n };\n }\n }\n\n return {\n response: `Received: ${result.response}. Next action will be triggered.`,\n };\n}\n\n// Trigger notification when response received\nfunction triggerResponseNotification(response: string): void {\n const message = `SMS Response: ${response}`;\n\n // macOS notification\n try {\n execSync(\n `osascript -e 'display notification \"${message}\" with title \"StackMemory\" sound name \"Glass\"'`,\n { stdio: 'ignore', timeout: 5000 }\n );\n } catch {\n // Ignore if not on macOS\n }\n\n // Write signal file for other processes\n try {\n const signalPath = join(homedir(), '.stackmemory', 'sms-signal.txt');\n writeFileSync(\n signalPath,\n JSON.stringify({\n type: 'sms_response',\n response,\n timestamp: new Date().toISOString(),\n })\n );\n } catch {\n // Ignore\n }\n\n console.log(`\\n*** SMS RESPONSE RECEIVED: \"${response}\" ***`);\n console.log(`*** Run: stackmemory notify run-actions ***\\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 (incoming messages)\n if (\n (url.pathname === '/sms' ||\n url.pathname === '/sms/incoming' ||\n url.pathname === '/webhook') &&\n req.method === 'POST'\n ) {\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 callback endpoint (delivery status updates)\n if (url.pathname === '/sms/status' && 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(body);\n console.log(\n `[sms-webhook] Status update: ${payload['MessageSid']} -> ${payload['MessageStatus']}`\n );\n\n // Store status for tracking\n const statusPath = join(\n homedir(),\n '.stackmemory',\n 'sms-status.json'\n );\n const statuses: Record<string, string> = existsSync(statusPath)\n ? JSON.parse(readFileSync(statusPath, 'utf8'))\n : {};\n statuses[payload['MessageSid']] = payload['MessageStatus'];\n writeFileSync(statusPath, JSON.stringify(statuses, null, 2));\n\n res.writeHead(200, { 'Content-Type': 'text/plain' });\n res.end('OK');\n } catch (err) {\n console.error('[sms-webhook] Status error:', err);\n res.writeHead(500);\n res.end('Error');\n }\n });\n return;\n }\n\n // Server 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(\n `[sms-webhook] Incoming messages: http://localhost:${port}/sms/incoming`\n );\n console.log(\n `[sms-webhook] Status callback: http://localhost:${port}/sms/status`\n );\n console.log(`[sms-webhook] Configure these URLs 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": ";;;;
|
|
4
|
+
"sourcesContent": ["/**\n * SMS Webhook Handler for receiving Twilio responses\n * Can run as standalone server or integrate with existing Express app\n *\n * Security features:\n * - Twilio signature verification\n * - Rate limiting per IP\n * - Body size limits\n * - Content-type validation\n * - Safe action execution (no shell injection)\n */\n\nimport { createServer, IncomingMessage, ServerResponse } from 'http';\nimport { parse as parseUrl } from 'url';\nimport { existsSync, writeFileSync, mkdirSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { createHmac } from 'crypto';\nimport { execFileSync } from 'child_process';\nimport { processIncomingResponse, loadSMSConfig } from './sms-notify.js';\nimport { queueAction, executeActionSafe } from './sms-action-runner.js';\n\n// Security constants\nconst MAX_BODY_SIZE = 50 * 1024; // 50KB max body\nconst RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute\nconst RATE_LIMIT_MAX_REQUESTS = 30; // 30 requests per minute per IP\n\n// Rate limiting store (in production, use Redis)\nconst rateLimitStore = new Map<string, { count: number; resetTime: number }>();\n\nfunction checkRateLimit(ip: string): boolean {\n const now = Date.now();\n const record = rateLimitStore.get(ip);\n\n if (!record || now > record.resetTime) {\n rateLimitStore.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS });\n return true;\n }\n\n if (record.count >= RATE_LIMIT_MAX_REQUESTS) {\n return false;\n }\n\n record.count++;\n return true;\n}\n\n// Twilio signature verification\nfunction verifyTwilioSignature(\n url: string,\n params: Record<string, string>,\n signature: string\n): boolean {\n const authToken = process.env['TWILIO_AUTH_TOKEN'];\n if (!authToken) {\n console.warn(\n '[sms-webhook] TWILIO_AUTH_TOKEN not set, skipping signature verification'\n );\n return true; // Allow in development, but log warning\n }\n\n // Build the data string (URL + sorted params)\n const sortedKeys = Object.keys(params).sort();\n let data = url;\n for (const key of sortedKeys) {\n data += key + params[key];\n }\n\n // Calculate expected signature\n const hmac = createHmac('sha1', authToken);\n hmac.update(data);\n const expectedSignature = hmac.digest('base64');\n\n return signature === expectedSignature;\n}\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 // Trigger notification to alert user/Claude\n triggerResponseNotification(result.response || Body);\n\n // Execute action safely if present (no shell injection)\n if (result.action) {\n console.log(`[sms-webhook] Executing action: ${result.action}`);\n\n const actionResult = executeActionSafe(\n result.action,\n result.response || Body\n );\n\n if (actionResult.success) {\n console.log(\n `[sms-webhook] Action completed: ${(actionResult.output || '').substring(0, 200)}`\n );\n\n return {\n response: `Done! Action executed successfully.`,\n action: result.action,\n queued: false,\n };\n } else {\n console.log(`[sms-webhook] Action failed: ${actionResult.error}`);\n\n // Queue for retry\n queueAction(\n result.prompt?.id || 'unknown',\n result.response || Body,\n result.action\n );\n\n return {\n response: `Action failed, queued for retry: ${(actionResult.error || '').substring(0, 50)}`,\n action: result.action,\n queued: true,\n };\n }\n }\n\n return {\n response: `Received: ${result.response}. Next action will be triggered.`,\n };\n}\n\n// Escape string for AppleScript (prevent injection)\nfunction escapeAppleScript(str: string): string {\n return str\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .substring(0, 200); // Limit length\n}\n\n// Trigger notification when response received\nfunction triggerResponseNotification(response: string): void {\n const safeMessage = escapeAppleScript(`SMS Response: ${response}`);\n\n // macOS notification using execFile (safer than execSync with shell)\n try {\n execFileSync(\n 'osascript',\n [\n '-e',\n `display notification \"${safeMessage}\" with title \"StackMemory\" sound name \"Glass\"`,\n ],\n { stdio: 'ignore', timeout: 5000 }\n );\n } catch {\n // Ignore if not on macOS\n }\n\n // Write signal file for other processes\n try {\n const signalPath = join(homedir(), '.stackmemory', 'sms-signal.txt');\n writeFileSync(\n signalPath,\n JSON.stringify({\n type: 'sms_response',\n response,\n timestamp: new Date().toISOString(),\n })\n );\n } catch {\n // Ignore\n }\n\n console.log(`\\n*** SMS RESPONSE RECEIVED: \"${response}\" ***`);\n console.log(`*** Run: stackmemory notify run-actions ***\\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 (incoming messages)\n if (\n (url.pathname === '/sms' ||\n url.pathname === '/sms/incoming' ||\n url.pathname === '/webhook') &&\n req.method === 'POST'\n ) {\n // Rate limiting\n const clientIp = req.socket.remoteAddress || 'unknown';\n if (!checkRateLimit(clientIp)) {\n res.writeHead(429, {\n 'Content-Type': 'text/xml',\n 'Retry-After': '60',\n });\n res.end(twimlResponse('Too many requests. Please try again later.'));\n return;\n }\n\n // Content-type validation\n const contentType = req.headers['content-type'] || '';\n if (!contentType.includes('application/x-www-form-urlencoded')) {\n res.writeHead(400, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Invalid content type'));\n return;\n }\n\n let body = '';\n let bodyTooLarge = false;\n\n req.on('data', (chunk) => {\n body += chunk;\n // Body size limit\n if (body.length > MAX_BODY_SIZE) {\n bodyTooLarge = true;\n req.destroy();\n }\n });\n\n req.on('end', () => {\n if (bodyTooLarge) {\n res.writeHead(413, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Request too large'));\n return;\n }\n\n try {\n const payload = parseFormData(\n body\n ) as unknown as TwilioWebhookPayload;\n\n // Verify Twilio signature\n const twilioSignature = req.headers['x-twilio-signature'] as string;\n const webhookUrl = `${req.headers['x-forwarded-proto'] || 'http'}://${req.headers.host}${req.url}`;\n\n if (\n twilioSignature &&\n !verifyTwilioSignature(\n webhookUrl,\n payload as unknown as Record<string, string>,\n twilioSignature\n )\n ) {\n console.error('[sms-webhook] Invalid Twilio signature');\n res.writeHead(401, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Unauthorized'));\n return;\n }\n\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 callback endpoint (delivery status updates)\n if (url.pathname === '/sms/status' && 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(body);\n console.log(\n `[sms-webhook] Status update: ${payload['MessageSid']} -> ${payload['MessageStatus']}`\n );\n\n // Store status for tracking\n const statusPath = join(\n homedir(),\n '.stackmemory',\n 'sms-status.json'\n );\n const statuses: Record<string, string> = existsSync(statusPath)\n ? JSON.parse(readFileSync(statusPath, 'utf8'))\n : {};\n statuses[payload['MessageSid']] = payload['MessageStatus'];\n writeFileSync(statusPath, JSON.stringify(statuses, null, 2));\n\n res.writeHead(200, { 'Content-Type': 'text/plain' });\n res.end('OK');\n } catch (err) {\n console.error('[sms-webhook] Status error:', err);\n res.writeHead(500);\n res.end('Error');\n }\n });\n return;\n }\n\n // Server 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(\n `[sms-webhook] Incoming messages: http://localhost:${port}/sms/incoming`\n );\n console.log(\n `[sms-webhook] Status callback: http://localhost:${port}/sms/status`\n );\n console.log(`[sms-webhook] Configure these URLs 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.17",
|
|
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",
|
|
@@ -110,19 +110,19 @@ process.stdin.on('end', () => {
|
|
|
110
110
|
// Only process Bash tool
|
|
111
111
|
if (tool_name !== 'Bash') {
|
|
112
112
|
// Allow other tools through unchanged
|
|
113
|
-
console.log(JSON.stringify({
|
|
113
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
114
114
|
return;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
const command = tool_input?.command;
|
|
118
118
|
if (!command) {
|
|
119
|
-
console.log(JSON.stringify({
|
|
119
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
// Already backgrounded
|
|
124
124
|
if (tool_input.run_in_background === true) {
|
|
125
|
-
console.log(JSON.stringify({
|
|
125
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
128
|
|
|
@@ -135,22 +135,23 @@ process.stdin.on('end', () => {
|
|
|
135
135
|
);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
// Modify the tool input to add run_in_background
|
|
138
|
+
// Modify the tool input to add run_in_background using correct schema
|
|
139
139
|
console.log(
|
|
140
140
|
JSON.stringify({
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
hookEventName: 'PreToolUse',
|
|
142
|
+
permissionDecision: 'allow',
|
|
143
|
+
updatedInput: {
|
|
143
144
|
...tool_input,
|
|
144
145
|
run_in_background: true,
|
|
145
146
|
},
|
|
146
147
|
})
|
|
147
148
|
);
|
|
148
149
|
} else {
|
|
149
|
-
console.log(JSON.stringify({
|
|
150
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
150
151
|
}
|
|
151
152
|
} catch (err) {
|
|
152
153
|
// On error, allow the command through unchanged
|
|
153
154
|
console.error('[auto-bg] Error:', err.message);
|
|
154
|
-
console.log(JSON.stringify({
|
|
155
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
155
156
|
}
|
|
156
157
|
});
|
|
@@ -139,13 +139,9 @@ process.stdin.on('end', () => {
|
|
|
139
139
|
|
|
140
140
|
clearLatestResponse();
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
context: context,
|
|
146
|
-
user_message: `[SMS Response] User replied: "${latestResponse.response}"`,
|
|
147
|
-
})
|
|
148
|
-
);
|
|
142
|
+
// Log context to stderr for visibility, allow the tool
|
|
143
|
+
console.error(`[sms-hook] Context: ${JSON.stringify(context)}`);
|
|
144
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
149
145
|
return;
|
|
150
146
|
}
|
|
151
147
|
|
|
@@ -162,24 +158,17 @@ process.stdin.on('end', () => {
|
|
|
162
158
|
)
|
|
163
159
|
.join('\n');
|
|
164
160
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
context: {
|
|
169
|
-
type: 'sms_actions_executed',
|
|
170
|
-
results,
|
|
171
|
-
},
|
|
172
|
-
user_message: `[SMS Actions] Executed queued actions:\n${summary}`,
|
|
173
|
-
})
|
|
174
|
-
);
|
|
161
|
+
// Log results to stderr for visibility, allow the tool
|
|
162
|
+
console.error(`[sms-hook] Actions summary:\n${summary}`);
|
|
163
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
175
164
|
return;
|
|
176
165
|
}
|
|
177
166
|
}
|
|
178
167
|
|
|
179
168
|
// Default: allow everything
|
|
180
|
-
console.log(JSON.stringify({
|
|
169
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
181
170
|
} catch (err) {
|
|
182
171
|
console.error('[sms-hook] Error:', err.message);
|
|
183
|
-
console.log(JSON.stringify({
|
|
172
|
+
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
184
173
|
}
|
|
185
174
|
});
|