@stackmemoryai/stackmemory 0.5.6 → 0.5.8

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,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
+ });