@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.
- package/dist/cli/commands/sms-notify.js +362 -0
- package/dist/cli/commands/sms-notify.js.map +7 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +2 -2
- package/dist/hooks/index.js +3 -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 +286 -0
- package/dist/hooks/sms-notify.js.map +7 -0
- package/dist/hooks/sms-webhook.js +144 -0
- package/dist/hooks/sms-webhook.js.map +7 -0
- package/package.json +1 -1
- package/scripts/install-notify-hook.sh +84 -0
- package/templates/claude-hooks/notify-review-hook.js +171 -0
- package/templates/claude-hooks/sms-response-handler.js +185 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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 } 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
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackmemoryai/stackmemory",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
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,84 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Install SMS notification hook for Claude Code (optional)
|
|
3
|
+
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
HOOK_SOURCE="$SCRIPT_DIR/../templates/claude-hooks/notify-review-hook.js"
|
|
8
|
+
CLAUDE_DIR="$HOME/.claude"
|
|
9
|
+
HOOKS_DIR="$CLAUDE_DIR/hooks"
|
|
10
|
+
SETTINGS_FILE="$CLAUDE_DIR/settings.json"
|
|
11
|
+
|
|
12
|
+
echo "Installing SMS notification hook for Claude Code..."
|
|
13
|
+
echo "(Optional feature - requires Twilio setup)"
|
|
14
|
+
echo ""
|
|
15
|
+
|
|
16
|
+
# Create directories
|
|
17
|
+
mkdir -p "$HOOKS_DIR"
|
|
18
|
+
|
|
19
|
+
# Copy hook script
|
|
20
|
+
HOOK_DEST="$HOOKS_DIR/notify-review-hook.js"
|
|
21
|
+
cp "$HOOK_SOURCE" "$HOOK_DEST"
|
|
22
|
+
chmod +x "$HOOK_DEST"
|
|
23
|
+
echo "Installed hook to $HOOK_DEST"
|
|
24
|
+
|
|
25
|
+
# Update Claude Code settings
|
|
26
|
+
if [ -f "$SETTINGS_FILE" ]; then
|
|
27
|
+
if command -v jq &> /dev/null; then
|
|
28
|
+
cp "$SETTINGS_FILE" "$SETTINGS_FILE.bak"
|
|
29
|
+
|
|
30
|
+
HOOK_CMD="node $HOOK_DEST"
|
|
31
|
+
|
|
32
|
+
# Add to post_tool_use hooks
|
|
33
|
+
if jq -e '.hooks.post_tool_use' "$SETTINGS_FILE" > /dev/null 2>&1; then
|
|
34
|
+
if ! jq -e ".hooks.post_tool_use | index(\"$HOOK_CMD\")" "$SETTINGS_FILE" > /dev/null 2>&1; then
|
|
35
|
+
jq ".hooks.post_tool_use += [\"$HOOK_CMD\"]" "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp"
|
|
36
|
+
mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
|
|
37
|
+
echo "Added hook to post_tool_use array"
|
|
38
|
+
else
|
|
39
|
+
echo "Hook already configured"
|
|
40
|
+
fi
|
|
41
|
+
else
|
|
42
|
+
jq ".hooks = (.hooks // {}) | .hooks.post_tool_use = [\"$HOOK_CMD\"]" "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp"
|
|
43
|
+
mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
|
|
44
|
+
echo "Created hooks.post_tool_use with notify hook"
|
|
45
|
+
fi
|
|
46
|
+
else
|
|
47
|
+
echo ""
|
|
48
|
+
echo "jq not found. Please manually add to $SETTINGS_FILE:"
|
|
49
|
+
echo ""
|
|
50
|
+
echo ' "hooks": {'
|
|
51
|
+
echo ' "post_tool_use": ["node '$HOOK_DEST'"]'
|
|
52
|
+
echo ' }'
|
|
53
|
+
fi
|
|
54
|
+
else
|
|
55
|
+
cat > "$SETTINGS_FILE" << EOF
|
|
56
|
+
{
|
|
57
|
+
"hooks": {
|
|
58
|
+
"post_tool_use": ["node $HOOK_DEST"]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
EOF
|
|
62
|
+
echo "Created settings file with hook"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
echo ""
|
|
66
|
+
echo "Notification hook installed!"
|
|
67
|
+
echo ""
|
|
68
|
+
echo "To enable SMS notifications:"
|
|
69
|
+
echo " 1. Set Twilio environment variables:"
|
|
70
|
+
echo " export TWILIO_ACCOUNT_SID=your_sid"
|
|
71
|
+
echo " export TWILIO_AUTH_TOKEN=your_token"
|
|
72
|
+
echo " export TWILIO_FROM_NUMBER=+1234567890"
|
|
73
|
+
echo " export TWILIO_TO_NUMBER=+1234567890"
|
|
74
|
+
echo ""
|
|
75
|
+
echo " 2. Enable notifications:"
|
|
76
|
+
echo " stackmemory notify enable"
|
|
77
|
+
echo ""
|
|
78
|
+
echo " 3. Test:"
|
|
79
|
+
echo " stackmemory notify test"
|
|
80
|
+
echo ""
|
|
81
|
+
echo "Notifications will be sent when:"
|
|
82
|
+
echo " - PR is created (gh pr create)"
|
|
83
|
+
echo " - Package is published (npm publish)"
|
|
84
|
+
echo " - Deployment completes"
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code hook for SMS notifications on review-ready events
|
|
4
|
+
*
|
|
5
|
+
* Triggers notifications when:
|
|
6
|
+
* - PR is created
|
|
7
|
+
* - Task is marked complete
|
|
8
|
+
* - User explicitly requests notification
|
|
9
|
+
*
|
|
10
|
+
* Install: stackmemory notify install-hook
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const https = require('https');
|
|
17
|
+
|
|
18
|
+
const CONFIG_PATH = path.join(os.homedir(), '.stackmemory', 'sms-notify.json');
|
|
19
|
+
|
|
20
|
+
function loadConfig() {
|
|
21
|
+
try {
|
|
22
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
23
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
24
|
+
}
|
|
25
|
+
} catch {}
|
|
26
|
+
return { enabled: false };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function shouldNotify(toolName, toolInput, output) {
|
|
30
|
+
const config = loadConfig();
|
|
31
|
+
if (!config.enabled) return null;
|
|
32
|
+
|
|
33
|
+
// Check for PR creation
|
|
34
|
+
if (toolName === 'Bash') {
|
|
35
|
+
const cmd = toolInput?.command || '';
|
|
36
|
+
const out = output || '';
|
|
37
|
+
|
|
38
|
+
// gh pr create
|
|
39
|
+
if (cmd.includes('gh pr create') && out.includes('github.com')) {
|
|
40
|
+
const prUrl = out.match(/https:\/\/github\.com\/[^\s]+\/pull\/\d+/)?.[0];
|
|
41
|
+
return {
|
|
42
|
+
type: 'review_ready',
|
|
43
|
+
title: 'PR Ready for Review',
|
|
44
|
+
message: prUrl || 'Pull request created successfully',
|
|
45
|
+
options: ['Approve', 'Review', 'Skip'],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// npm publish
|
|
50
|
+
if (cmd.includes('npm publish') && out.includes('+')) {
|
|
51
|
+
const pkg = out.match(/\+ ([^\s]+)/)?.[1];
|
|
52
|
+
return {
|
|
53
|
+
type: 'task_complete',
|
|
54
|
+
title: 'Package Published',
|
|
55
|
+
message: pkg ? `Published ${pkg}` : 'Package published successfully',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Deployment
|
|
60
|
+
if (
|
|
61
|
+
(cmd.includes('deploy') || cmd.includes('railway up')) &&
|
|
62
|
+
(out.includes('deployed') || out.includes('success'))
|
|
63
|
+
) {
|
|
64
|
+
return {
|
|
65
|
+
type: 'review_ready',
|
|
66
|
+
title: 'Deployment Complete',
|
|
67
|
+
message: 'Ready for verification',
|
|
68
|
+
options: ['Verify', 'Rollback', 'Skip'],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sendNotification(notification) {
|
|
77
|
+
const config = loadConfig();
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
!config.accountSid ||
|
|
81
|
+
!config.authToken ||
|
|
82
|
+
!config.fromNumber ||
|
|
83
|
+
!config.toNumber
|
|
84
|
+
) {
|
|
85
|
+
// Try env vars
|
|
86
|
+
const sid = process.env.TWILIO_ACCOUNT_SID;
|
|
87
|
+
const token = process.env.TWILIO_AUTH_TOKEN;
|
|
88
|
+
const from = process.env.TWILIO_FROM_NUMBER;
|
|
89
|
+
const to = process.env.TWILIO_TO_NUMBER;
|
|
90
|
+
|
|
91
|
+
if (!sid || !token || !from || !to) {
|
|
92
|
+
console.error('[notify-hook] Missing Twilio credentials');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
config.accountSid = sid;
|
|
97
|
+
config.authToken = token;
|
|
98
|
+
config.fromNumber = from;
|
|
99
|
+
config.toNumber = to;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let message = `${notification.title}\n\n${notification.message}`;
|
|
103
|
+
|
|
104
|
+
if (notification.options) {
|
|
105
|
+
message += '\n\n';
|
|
106
|
+
notification.options.forEach((opt, i) => {
|
|
107
|
+
message += `${i + 1}. ${opt}\n`;
|
|
108
|
+
});
|
|
109
|
+
message += '\nReply with number to select';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const postData = new URLSearchParams({
|
|
113
|
+
From: config.fromNumber,
|
|
114
|
+
To: config.toNumber,
|
|
115
|
+
Body: message,
|
|
116
|
+
}).toString();
|
|
117
|
+
|
|
118
|
+
const options = {
|
|
119
|
+
hostname: 'api.twilio.com',
|
|
120
|
+
port: 443,
|
|
121
|
+
path: `/2010-04-01/Accounts/${config.accountSid}/Messages.json`,
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: {
|
|
124
|
+
Authorization:
|
|
125
|
+
'Basic ' +
|
|
126
|
+
Buffer.from(`${config.accountSid}:${config.authToken}`).toString(
|
|
127
|
+
'base64'
|
|
128
|
+
),
|
|
129
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
130
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const req = https.request(options, (res) => {
|
|
135
|
+
if (res.statusCode === 201) {
|
|
136
|
+
console.error(`[notify-hook] Sent: ${notification.title}`);
|
|
137
|
+
} else {
|
|
138
|
+
console.error(`[notify-hook] Failed: ${res.statusCode}`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
req.on('error', (e) => {
|
|
143
|
+
console.error(`[notify-hook] Error: ${e.message}`);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
req.write(postData);
|
|
147
|
+
req.end();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Read hook input from stdin (post-tool-use hook)
|
|
151
|
+
let input = '';
|
|
152
|
+
process.stdin.setEncoding('utf8');
|
|
153
|
+
process.stdin.on('data', (chunk) => (input += chunk));
|
|
154
|
+
process.stdin.on('end', () => {
|
|
155
|
+
try {
|
|
156
|
+
const hookData = JSON.parse(input);
|
|
157
|
+
const { tool_name, tool_input, tool_output } = hookData;
|
|
158
|
+
|
|
159
|
+
const notification = shouldNotify(tool_name, tool_input, tool_output);
|
|
160
|
+
|
|
161
|
+
if (notification) {
|
|
162
|
+
sendNotification(notification);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Always allow (post-tool hooks don't block)
|
|
166
|
+
console.log(JSON.stringify({ status: 'ok' }));
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.error('[notify-hook] Error:', err.message);
|
|
169
|
+
console.log(JSON.stringify({ status: 'ok' }));
|
|
170
|
+
}
|
|
171
|
+
});
|
|
@@ -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
|
+
});
|