@stackmemoryai/stackmemory 0.7.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.
Files changed (45) hide show
  1. package/README.md +39 -1
  2. package/dist/src/cli/claude-sm.js +69 -372
  3. package/dist/src/cli/claude-sm.js.map +2 -2
  4. package/dist/src/cli/codex-sm.js +28 -0
  5. package/dist/src/cli/codex-sm.js.map +2 -2
  6. package/dist/src/cli/commands/clear.js +10 -15
  7. package/dist/src/cli/commands/clear.js.map +2 -2
  8. package/dist/src/cli/commands/daemon.js +31 -0
  9. package/dist/src/cli/commands/daemon.js.map +2 -2
  10. package/dist/src/cli/commands/onboard.js +22 -6
  11. package/dist/src/cli/commands/onboard.js.map +2 -2
  12. package/dist/src/cli/commands/setup.js +1 -2
  13. package/dist/src/cli/commands/setup.js.map +2 -2
  14. package/dist/src/cli/index.js +1 -71
  15. package/dist/src/cli/index.js.map +3 -3
  16. package/dist/src/cli/opencode-sm.js +28 -0
  17. package/dist/src/cli/opencode-sm.js.map +2 -2
  18. package/dist/src/core/config/feature-flags.js +1 -13
  19. package/dist/src/core/config/feature-flags.js.map +2 -2
  20. package/dist/src/core/context/refactored-frame-manager.js +7 -0
  21. package/dist/src/core/context/refactored-frame-manager.js.map +2 -2
  22. package/dist/src/core/database/sqlite-adapter.js +1 -1
  23. package/dist/src/core/database/sqlite-adapter.js.map +2 -2
  24. package/dist/src/core/session/enhanced-handoff.js +1 -1
  25. package/dist/src/core/session/enhanced-handoff.js.map +2 -2
  26. package/dist/src/daemon/daemon-config.js +11 -0
  27. package/dist/src/daemon/daemon-config.js.map +2 -2
  28. package/dist/src/daemon/services/memory-service.js +190 -0
  29. package/dist/src/daemon/services/memory-service.js.map +7 -0
  30. package/dist/src/daemon/unified-daemon.js +33 -3
  31. package/dist/src/daemon/unified-daemon.js.map +2 -2
  32. package/dist/src/features/sweep/pty-wrapper.js +11 -1
  33. package/dist/src/features/sweep/pty-wrapper.js.map +2 -2
  34. package/dist/src/hooks/index.js +0 -3
  35. package/dist/src/hooks/index.js.map +2 -2
  36. package/dist/src/hooks/schemas.js +0 -117
  37. package/dist/src/hooks/schemas.js.map +2 -2
  38. package/dist/src/hooks/session-summary.js.map +1 -1
  39. package/package.json +2 -2
  40. package/scripts/smoke-init-db.sh +77 -0
  41. package/scripts/test-pre-publish-quick.sh +26 -34
  42. package/scripts/install-notify-hook.sh +0 -84
  43. package/scripts/setup-notify-webhook.sh +0 -103
  44. package/templates/claude-hooks/notify-review-hook.js +0 -360
  45. package/templates/claude-hooks/sms-response-handler.js +0 -174
@@ -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
- });
@@ -1,174 +0,0 @@
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
- // 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' }));
145
- return;
146
- }
147
-
148
- // Process any pending actions
149
- const results = processPendingActions();
150
- if (results && results.length > 0) {
151
- console.error(`[sms-hook] Processed ${results.length} action(s)`);
152
-
153
- const summary = results
154
- .map((r) =>
155
- r.success
156
- ? `Executed: ${r.action.substring(0, 50)}...`
157
- : `Failed: ${r.action.substring(0, 50)}... (${r.error})`
158
- )
159
- .join('\n');
160
-
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' }));
164
- return;
165
- }
166
- }
167
-
168
- // Default: allow everything
169
- console.log(JSON.stringify({ permissionDecision: 'allow' }));
170
- } catch (err) {
171
- console.error('[sms-hook] Error:', err.message);
172
- console.log(JSON.stringify({ permissionDecision: 'allow' }));
173
- }
174
- });