@stackmemoryai/stackmemory 0.8.0 → 0.8.1
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/README.md +39 -1
- package/dist/src/cli/claude-sm.js +69 -372
- package/dist/src/cli/claude-sm.js.map +2 -2
- package/dist/src/cli/codex-sm.js +28 -0
- package/dist/src/cli/codex-sm.js.map +2 -2
- package/dist/src/cli/commands/onboard.js +22 -6
- package/dist/src/cli/commands/onboard.js.map +2 -2
- package/dist/src/cli/commands/setup.js +1 -2
- package/dist/src/cli/commands/setup.js.map +2 -2
- package/dist/src/cli/index.js +1 -71
- package/dist/src/cli/index.js.map +3 -3
- package/dist/src/cli/opencode-sm.js +28 -0
- package/dist/src/cli/opencode-sm.js.map +2 -2
- package/dist/src/core/config/feature-flags.js +1 -13
- package/dist/src/core/config/feature-flags.js.map +2 -2
- package/dist/src/features/sweep/pty-wrapper.js +11 -1
- package/dist/src/features/sweep/pty-wrapper.js.map +2 -2
- package/dist/src/hooks/index.js +0 -3
- package/dist/src/hooks/index.js.map +2 -2
- package/dist/src/hooks/schemas.js +0 -117
- package/dist/src/hooks/schemas.js.map +2 -2
- package/dist/src/hooks/session-summary.js.map +1 -1
- package/package.json +2 -2
- package/scripts/smoke-init-db.sh +77 -0
- package/scripts/test-pre-publish-quick.sh +26 -34
- package/scripts/install-notify-hook.sh +0 -84
- package/scripts/setup-notify-webhook.sh +0 -103
- package/templates/claude-hooks/notify-review-hook.js +0 -360
- package/templates/claude-hooks/sms-response-handler.js +0 -174
|
@@ -1,84 +0,0 @@
|
|
|
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"
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Auto-setup for StackMemory WhatsApp/SMS webhook loop
|
|
3
|
-
|
|
4
|
-
set -e
|
|
5
|
-
|
|
6
|
-
WEBHOOK_PORT="${1:-3456}"
|
|
7
|
-
TWILIO_ACCOUNT_SID="${TWILIO_ACCOUNT_SID}"
|
|
8
|
-
TWILIO_AUTH_TOKEN="${TWILIO_AUTH_TOKEN}"
|
|
9
|
-
|
|
10
|
-
echo "=== StackMemory Webhook Setup ==="
|
|
11
|
-
echo ""
|
|
12
|
-
|
|
13
|
-
# Check dependencies
|
|
14
|
-
if ! command -v ngrok &> /dev/null; then
|
|
15
|
-
echo "Installing ngrok..."
|
|
16
|
-
if command -v brew &> /dev/null; then
|
|
17
|
-
brew install ngrok
|
|
18
|
-
else
|
|
19
|
-
echo "Please install ngrok: https://ngrok.com/download"
|
|
20
|
-
exit 1
|
|
21
|
-
fi
|
|
22
|
-
fi
|
|
23
|
-
|
|
24
|
-
# Check Twilio credentials
|
|
25
|
-
if [ -z "$TWILIO_ACCOUNT_SID" ] || [ -z "$TWILIO_AUTH_TOKEN" ]; then
|
|
26
|
-
echo "Error: Set TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN"
|
|
27
|
-
exit 1
|
|
28
|
-
fi
|
|
29
|
-
|
|
30
|
-
# Kill any existing processes
|
|
31
|
-
pkill -f "notify webhook" 2>/dev/null || true
|
|
32
|
-
pkill -f "ngrok http $WEBHOOK_PORT" 2>/dev/null || true
|
|
33
|
-
sleep 1
|
|
34
|
-
|
|
35
|
-
# Start webhook server in background
|
|
36
|
-
echo "Starting webhook server on port $WEBHOOK_PORT..."
|
|
37
|
-
stackmemory notify webhook -p "$WEBHOOK_PORT" > /tmp/webhook.log 2>&1 &
|
|
38
|
-
WEBHOOK_PID=$!
|
|
39
|
-
sleep 2
|
|
40
|
-
|
|
41
|
-
# Start ngrok in background
|
|
42
|
-
echo "Starting ngrok tunnel..."
|
|
43
|
-
ngrok http "$WEBHOOK_PORT" --log=stdout > /tmp/ngrok.log 2>&1 &
|
|
44
|
-
NGROK_PID=$!
|
|
45
|
-
sleep 3
|
|
46
|
-
|
|
47
|
-
# Get ngrok public URL
|
|
48
|
-
NGROK_URL=$(curl -s http://localhost:4040/api/tunnels | grep -o '"public_url":"https://[^"]*' | head -1 | cut -d'"' -f4)
|
|
49
|
-
|
|
50
|
-
if [ -z "$NGROK_URL" ]; then
|
|
51
|
-
echo "Error: Could not get ngrok URL. Check /tmp/ngrok.log"
|
|
52
|
-
exit 1
|
|
53
|
-
fi
|
|
54
|
-
|
|
55
|
-
WEBHOOK_URL="${NGROK_URL}/sms/incoming"
|
|
56
|
-
echo ""
|
|
57
|
-
echo "Webhook URL: $WEBHOOK_URL"
|
|
58
|
-
|
|
59
|
-
# Configure Twilio WhatsApp sandbox webhook
|
|
60
|
-
echo ""
|
|
61
|
-
echo "Configuring Twilio WhatsApp sandbox..."
|
|
62
|
-
|
|
63
|
-
# Get sandbox configuration
|
|
64
|
-
SANDBOX_RESPONSE=$(curl -s "https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNT_SID}/Sandbox.json" \
|
|
65
|
-
-u "${TWILIO_ACCOUNT_SID}:${TWILIO_AUTH_TOKEN}" 2>/dev/null)
|
|
66
|
-
|
|
67
|
-
if echo "$SANDBOX_RESPONSE" | grep -q "sms_url"; then
|
|
68
|
-
# Update sandbox webhook URL
|
|
69
|
-
curl -s -X POST "https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNT_SID}/Sandbox.json" \
|
|
70
|
-
-u "${TWILIO_ACCOUNT_SID}:${TWILIO_AUTH_TOKEN}" \
|
|
71
|
-
-d "SmsUrl=${WEBHOOK_URL}" \
|
|
72
|
-
-d "SmsMethod=POST" > /dev/null
|
|
73
|
-
echo "Sandbox webhook configured!"
|
|
74
|
-
else
|
|
75
|
-
echo "Note: Configure webhook manually in Twilio console:"
|
|
76
|
-
echo " URL: $WEBHOOK_URL"
|
|
77
|
-
echo " https://console.twilio.com/us1/develop/sms/try-it-out/whatsapp-learn"
|
|
78
|
-
fi
|
|
79
|
-
|
|
80
|
-
# Install Claude hook
|
|
81
|
-
echo ""
|
|
82
|
-
echo "Installing Claude response hook..."
|
|
83
|
-
stackmemory notify install-response-hook 2>/dev/null || true
|
|
84
|
-
|
|
85
|
-
# Save PIDs for cleanup
|
|
86
|
-
echo "$WEBHOOK_PID" > /tmp/stackmemory-webhook.pid
|
|
87
|
-
echo "$NGROK_PID" > /tmp/stackmemory-ngrok.pid
|
|
88
|
-
|
|
89
|
-
echo ""
|
|
90
|
-
echo "=== Setup Complete ==="
|
|
91
|
-
echo ""
|
|
92
|
-
echo "Webhook server: http://localhost:$WEBHOOK_PORT (PID: $WEBHOOK_PID)"
|
|
93
|
-
echo "Ngrok tunnel: $NGROK_URL (PID: $NGROK_PID)"
|
|
94
|
-
echo "Webhook URL: $WEBHOOK_URL"
|
|
95
|
-
echo ""
|
|
96
|
-
echo "The loop is now active:"
|
|
97
|
-
echo " 1. Send notification: stackmemory notify review 'Task'"
|
|
98
|
-
echo " 2. User replies via WhatsApp"
|
|
99
|
-
echo " 3. Response queued for action"
|
|
100
|
-
echo " 4. Claude hook processes it"
|
|
101
|
-
echo ""
|
|
102
|
-
echo "To stop: ./scripts/stop-notify-webhook.sh"
|
|
103
|
-
echo "Logs: /tmp/webhook.log, /tmp/ngrok.log"
|
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Claude Code hook for WhatsApp/SMS notifications
|
|
4
|
-
*
|
|
5
|
-
* Triggers notifications when:
|
|
6
|
-
* - AskUserQuestion tool is used (allows remote response)
|
|
7
|
-
* - PR is created
|
|
8
|
-
* - Task is marked complete
|
|
9
|
-
* - User explicitly requests notification
|
|
10
|
-
*
|
|
11
|
-
* Install: stackmemory notify install-hook
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const fs = require('fs');
|
|
15
|
-
const path = require('path');
|
|
16
|
-
const os = require('os');
|
|
17
|
-
const https = require('https');
|
|
18
|
-
|
|
19
|
-
// Load .env files (check multiple locations)
|
|
20
|
-
const envPaths = [
|
|
21
|
-
path.join(process.cwd(), '.env'),
|
|
22
|
-
path.join(os.homedir(), 'Dev/stackmemory/.env'),
|
|
23
|
-
path.join(os.homedir(), '.stackmemory/.env'),
|
|
24
|
-
path.join(os.homedir(), '.env'),
|
|
25
|
-
];
|
|
26
|
-
for (const envPath of envPaths) {
|
|
27
|
-
if (fs.existsSync(envPath)) {
|
|
28
|
-
try {
|
|
29
|
-
const content = fs.readFileSync(envPath, 'utf8');
|
|
30
|
-
for (const line of content.split('\n')) {
|
|
31
|
-
const match = line.match(/^([^#=]+)=(.*)$/);
|
|
32
|
-
if (match && !process.env[match[1].trim()]) {
|
|
33
|
-
process.env[match[1].trim()] = match[2]
|
|
34
|
-
.trim()
|
|
35
|
-
.replace(/^["']|["']$/g, '');
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
} catch {}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const CONFIG_PATH = path.join(os.homedir(), '.stackmemory', 'sms-notify.json');
|
|
43
|
-
const DEBUG_LOG = path.join(
|
|
44
|
-
os.homedir(),
|
|
45
|
-
'.stackmemory',
|
|
46
|
-
'claude-session-debug.log'
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
function loadConfig() {
|
|
50
|
-
try {
|
|
51
|
-
if (fs.existsSync(CONFIG_PATH)) {
|
|
52
|
-
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
53
|
-
}
|
|
54
|
-
} catch {}
|
|
55
|
-
return { enabled: false, pendingPrompts: [] };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function saveConfig(config) {
|
|
59
|
-
try {
|
|
60
|
-
const dir = path.join(os.homedir(), '.stackmemory');
|
|
61
|
-
if (!fs.existsSync(dir)) {
|
|
62
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
63
|
-
}
|
|
64
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
65
|
-
} catch (err) {
|
|
66
|
-
console.error('[notify-hook] Failed to save config:', err.message);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function logDebug(event, data) {
|
|
71
|
-
try {
|
|
72
|
-
const entry = `[${new Date().toISOString()}] ${event}: ${typeof data === 'string' ? data : JSON.stringify(data)}\n`;
|
|
73
|
-
fs.appendFileSync(DEBUG_LOG, entry);
|
|
74
|
-
} catch {}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function savePendingPrompt(prompt) {
|
|
78
|
-
try {
|
|
79
|
-
const config = loadConfig();
|
|
80
|
-
if (!config.pendingPrompts) {
|
|
81
|
-
config.pendingPrompts = [];
|
|
82
|
-
}
|
|
83
|
-
config.pendingPrompts.push(prompt);
|
|
84
|
-
// Keep only last 10 prompts
|
|
85
|
-
if (config.pendingPrompts.length > 10) {
|
|
86
|
-
config.pendingPrompts = config.pendingPrompts.slice(-10);
|
|
87
|
-
}
|
|
88
|
-
saveConfig(config);
|
|
89
|
-
logDebug('PENDING_PROMPT', {
|
|
90
|
-
id: prompt.id,
|
|
91
|
-
options: prompt.options.length,
|
|
92
|
-
});
|
|
93
|
-
} catch (err) {
|
|
94
|
-
console.error('[notify-hook] Failed to save pending prompt:', err.message);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function shouldNotify(toolName, toolInput, output) {
|
|
99
|
-
const config = loadConfig();
|
|
100
|
-
if (!config.enabled) return null;
|
|
101
|
-
|
|
102
|
-
// AskUserQuestion - send question via WhatsApp for remote response
|
|
103
|
-
if (toolName === 'AskUserQuestion') {
|
|
104
|
-
const questions = toolInput?.questions || [];
|
|
105
|
-
if (questions.length === 0) return null;
|
|
106
|
-
|
|
107
|
-
// Take first question (most common case)
|
|
108
|
-
const q = questions[0];
|
|
109
|
-
const promptId = Math.random().toString(36).substring(2, 10);
|
|
110
|
-
|
|
111
|
-
// Build message text
|
|
112
|
-
let messageText = q.question;
|
|
113
|
-
const options = [];
|
|
114
|
-
|
|
115
|
-
if (q.options && q.options.length > 0) {
|
|
116
|
-
messageText += '\n';
|
|
117
|
-
q.options.forEach((opt, i) => {
|
|
118
|
-
const key = String(i + 1);
|
|
119
|
-
messageText += `${key}. ${opt.label}`;
|
|
120
|
-
if (opt.description) {
|
|
121
|
-
messageText += ` - ${opt.description}`;
|
|
122
|
-
}
|
|
123
|
-
messageText += '\n';
|
|
124
|
-
options.push({ key, label: opt.label });
|
|
125
|
-
});
|
|
126
|
-
// Add "Other" option
|
|
127
|
-
const otherKey = String(q.options.length + 1);
|
|
128
|
-
messageText += `${otherKey}. Other (type your answer)`;
|
|
129
|
-
options.push({ key: otherKey, label: 'Other' });
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Store pending prompt in format webhook expects
|
|
133
|
-
const pendingPrompt = {
|
|
134
|
-
id: promptId,
|
|
135
|
-
timestamp: new Date().toISOString(),
|
|
136
|
-
message: q.question,
|
|
137
|
-
options: options,
|
|
138
|
-
type: options.length > 0 ? 'options' : 'freeform',
|
|
139
|
-
expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hour
|
|
140
|
-
};
|
|
141
|
-
savePendingPrompt(pendingPrompt);
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
type: 'custom',
|
|
145
|
-
title: 'Claude needs your input',
|
|
146
|
-
message: messageText,
|
|
147
|
-
promptId: promptId,
|
|
148
|
-
isQuestion: true,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Check for PR creation
|
|
153
|
-
if (toolName === 'Bash') {
|
|
154
|
-
const cmd = toolInput?.command || '';
|
|
155
|
-
const out = output || '';
|
|
156
|
-
|
|
157
|
-
// gh pr create
|
|
158
|
-
if (cmd.includes('gh pr create') && out.includes('github.com')) {
|
|
159
|
-
const prUrl = out.match(/https:\/\/github\.com\/[^\s]+\/pull\/\d+/)?.[0];
|
|
160
|
-
return {
|
|
161
|
-
type: 'review_ready',
|
|
162
|
-
title: 'PR Ready for Review',
|
|
163
|
-
message: prUrl || 'Pull request created successfully',
|
|
164
|
-
options: ['Approve', 'Review', 'Skip'],
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// npm publish
|
|
169
|
-
if (cmd.includes('npm publish') && out.includes('+')) {
|
|
170
|
-
const pkg = out.match(/\+ ([^\s]+)/)?.[1];
|
|
171
|
-
return {
|
|
172
|
-
type: 'task_complete',
|
|
173
|
-
title: 'Package Published',
|
|
174
|
-
message: pkg ? `Published ${pkg}` : 'Package published successfully',
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Deployment
|
|
179
|
-
if (
|
|
180
|
-
(cmd.includes('deploy') || cmd.includes('railway up')) &&
|
|
181
|
-
(out.includes('deployed') || out.includes('success'))
|
|
182
|
-
) {
|
|
183
|
-
return {
|
|
184
|
-
type: 'review_ready',
|
|
185
|
-
title: 'Deployment Complete',
|
|
186
|
-
message: 'Ready for verification',
|
|
187
|
-
options: ['Verify', 'Rollback', 'Skip'],
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function getChannelNumbers(config) {
|
|
196
|
-
const channel = config.channel || 'whatsapp';
|
|
197
|
-
|
|
198
|
-
if (channel === 'whatsapp') {
|
|
199
|
-
const from = config.whatsappFromNumber || config.fromNumber;
|
|
200
|
-
const to = config.whatsappToNumber || config.toNumber;
|
|
201
|
-
if (from && to) {
|
|
202
|
-
return {
|
|
203
|
-
from: from.startsWith('whatsapp:') ? from : `whatsapp:${from}`,
|
|
204
|
-
to: to.startsWith('whatsapp:') ? to : `whatsapp:${to}`,
|
|
205
|
-
channel: 'whatsapp',
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Fallback to SMS
|
|
211
|
-
const from = config.smsFromNumber || config.fromNumber;
|
|
212
|
-
const to = config.smsToNumber || config.toNumber;
|
|
213
|
-
if (from && to) {
|
|
214
|
-
return { from, to, channel: 'sms' };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function sendNotification(notification) {
|
|
221
|
-
let config = loadConfig();
|
|
222
|
-
|
|
223
|
-
// Apply env vars
|
|
224
|
-
config.accountSid = config.accountSid || process.env.TWILIO_ACCOUNT_SID;
|
|
225
|
-
config.authToken = config.authToken || process.env.TWILIO_AUTH_TOKEN;
|
|
226
|
-
config.channel = config.channel || process.env.TWILIO_CHANNEL || 'whatsapp';
|
|
227
|
-
|
|
228
|
-
// WhatsApp numbers
|
|
229
|
-
config.whatsappFromNumber =
|
|
230
|
-
config.whatsappFromNumber || process.env.TWILIO_WHATSAPP_FROM;
|
|
231
|
-
config.whatsappToNumber =
|
|
232
|
-
config.whatsappToNumber || process.env.TWILIO_WHATSAPP_TO;
|
|
233
|
-
|
|
234
|
-
// SMS numbers (fallback)
|
|
235
|
-
config.smsFromNumber =
|
|
236
|
-
config.smsFromNumber ||
|
|
237
|
-
process.env.TWILIO_SMS_FROM ||
|
|
238
|
-
process.env.TWILIO_FROM_NUMBER;
|
|
239
|
-
config.smsToNumber =
|
|
240
|
-
config.smsToNumber ||
|
|
241
|
-
process.env.TWILIO_SMS_TO ||
|
|
242
|
-
process.env.TWILIO_TO_NUMBER;
|
|
243
|
-
|
|
244
|
-
// Legacy support
|
|
245
|
-
config.fromNumber = config.fromNumber || process.env.TWILIO_FROM_NUMBER;
|
|
246
|
-
config.toNumber = config.toNumber || process.env.TWILIO_TO_NUMBER;
|
|
247
|
-
|
|
248
|
-
if (!config.accountSid || !config.authToken) {
|
|
249
|
-
console.error('[notify-hook] Missing Twilio credentials');
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const numbers = getChannelNumbers(config);
|
|
254
|
-
if (!numbers) {
|
|
255
|
-
console.error(
|
|
256
|
-
'[notify-hook] Missing phone numbers for channel:',
|
|
257
|
-
config.channel
|
|
258
|
-
);
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
let message = `${notification.title}\n\n${notification.message}`;
|
|
263
|
-
|
|
264
|
-
if (notification.options) {
|
|
265
|
-
message += '\n\n';
|
|
266
|
-
notification.options.forEach((opt, i) => {
|
|
267
|
-
message += `${i + 1}. ${opt}\n`;
|
|
268
|
-
});
|
|
269
|
-
message += '\nReply with number to select';
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// For questions, add reply instruction
|
|
273
|
-
if (notification.isQuestion) {
|
|
274
|
-
message += '\n\nReply with your choice number or type your answer.';
|
|
275
|
-
if (notification.promptId) {
|
|
276
|
-
message += `\n[ID: ${notification.promptId}]`;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Add session link if available
|
|
281
|
-
if (notification.sessionId) {
|
|
282
|
-
message += `\n\nSession: https://claude.ai/share/${notification.sessionId}`;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const postData = new URLSearchParams({
|
|
286
|
-
From: numbers.from,
|
|
287
|
-
To: numbers.to,
|
|
288
|
-
Body: message,
|
|
289
|
-
}).toString();
|
|
290
|
-
|
|
291
|
-
const options = {
|
|
292
|
-
hostname: 'api.twilio.com',
|
|
293
|
-
port: 443,
|
|
294
|
-
path: `/2010-04-01/Accounts/${config.accountSid}/Messages.json`,
|
|
295
|
-
method: 'POST',
|
|
296
|
-
headers: {
|
|
297
|
-
Authorization:
|
|
298
|
-
'Basic ' +
|
|
299
|
-
Buffer.from(`${config.accountSid}:${config.authToken}`).toString(
|
|
300
|
-
'base64'
|
|
301
|
-
),
|
|
302
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
303
|
-
'Content-Length': Buffer.byteLength(postData),
|
|
304
|
-
},
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
const req = https.request(options, (res) => {
|
|
308
|
-
let body = '';
|
|
309
|
-
res.on('data', (chunk) => (body += chunk));
|
|
310
|
-
res.on('end', () => {
|
|
311
|
-
if (res.statusCode === 201) {
|
|
312
|
-
console.error(
|
|
313
|
-
`[notify-hook] Sent via ${numbers.channel}: ${notification.title}`
|
|
314
|
-
);
|
|
315
|
-
logDebug('MESSAGE_SENT', {
|
|
316
|
-
channel: numbers.channel,
|
|
317
|
-
title: notification.title,
|
|
318
|
-
promptId: notification.promptId,
|
|
319
|
-
});
|
|
320
|
-
} else {
|
|
321
|
-
console.error(`[notify-hook] Failed (${res.statusCode}): ${body}`);
|
|
322
|
-
logDebug('MESSAGE_FAILED', { status: res.statusCode, error: body });
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
req.on('error', (e) => {
|
|
328
|
-
console.error(`[notify-hook] Error: ${e.message}`);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
req.write(postData);
|
|
332
|
-
req.end();
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Read hook input from stdin (post-tool-use hook)
|
|
336
|
-
let input = '';
|
|
337
|
-
process.stdin.setEncoding('utf8');
|
|
338
|
-
process.stdin.on('data', (chunk) => (input += chunk));
|
|
339
|
-
process.stdin.on('end', () => {
|
|
340
|
-
try {
|
|
341
|
-
const hookData = JSON.parse(input);
|
|
342
|
-
const { tool_name, tool_input, tool_output } = hookData;
|
|
343
|
-
|
|
344
|
-
logDebug('PostToolUse', { tool: tool_name, session: hookData.session_id });
|
|
345
|
-
|
|
346
|
-
const notification = shouldNotify(tool_name, tool_input, tool_output);
|
|
347
|
-
|
|
348
|
-
if (notification) {
|
|
349
|
-
notification.sessionId = hookData.session_id;
|
|
350
|
-
sendNotification(notification);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Always allow (post-tool hooks don't block)
|
|
354
|
-
console.log(JSON.stringify({ status: 'ok' }));
|
|
355
|
-
} catch (err) {
|
|
356
|
-
logDebug('ERROR', err.message);
|
|
357
|
-
console.error('[notify-hook] Error:', err.message);
|
|
358
|
-
console.log(JSON.stringify({ status: 'ok' }));
|
|
359
|
-
}
|
|
360
|
-
});
|