@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,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/hooks/sms-notify.ts"],
4
+ "sourcesContent": ["/**\n * SMS Notification Hook for StackMemory\n * Sends text messages when tasks are ready for review\n * Supports interactive prompts with numbered options or yes/no\n *\n * Optional feature - requires Twilio setup\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\nexport interface SMSConfig {\n enabled: boolean;\n // Twilio credentials (from env or config)\n accountSid?: string;\n authToken?: string;\n fromNumber?: string;\n toNumber?: string;\n // Webhook URL for receiving responses\n webhookUrl?: string;\n // Notification preferences\n notifyOn: {\n taskComplete: boolean;\n reviewReady: boolean;\n error: boolean;\n custom: boolean;\n };\n // Quiet hours (don't send during these times)\n quietHours?: {\n enabled: boolean;\n start: string; // \"22:00\"\n end: string; // \"08:00\"\n };\n // Response timeout (seconds)\n responseTimeout: number;\n // Pending prompts awaiting response\n pendingPrompts: PendingPrompt[];\n}\n\nexport interface PendingPrompt {\n id: string;\n timestamp: string;\n message: string;\n options: PromptOption[];\n type: 'options' | 'yesno' | 'freeform';\n callback?: string; // Command to run with response\n expiresAt: string;\n}\n\nexport interface PromptOption {\n key: string; // \"1\", \"2\", \"y\", \"n\", etc.\n label: string;\n action?: string; // Command to execute\n}\n\nexport interface NotificationPayload {\n type: 'task_complete' | 'review_ready' | 'error' | 'custom';\n title: string;\n message: string;\n prompt?: {\n type: 'options' | 'yesno' | 'freeform';\n options?: PromptOption[];\n question?: string;\n };\n metadata?: Record<string, unknown>;\n}\n\nconst CONFIG_PATH = join(homedir(), '.stackmemory', 'sms-notify.json');\n\nconst DEFAULT_CONFIG: SMSConfig = {\n enabled: false,\n notifyOn: {\n taskComplete: true,\n reviewReady: true,\n error: true,\n custom: true,\n },\n quietHours: {\n enabled: false,\n start: '22:00',\n end: '08:00',\n },\n responseTimeout: 300, // 5 minutes\n pendingPrompts: [],\n};\n\nexport function loadSMSConfig(): SMSConfig {\n try {\n if (existsSync(CONFIG_PATH)) {\n const data = readFileSync(CONFIG_PATH, 'utf8');\n return { ...DEFAULT_CONFIG, ...JSON.parse(data) };\n }\n } catch {\n // Use defaults\n }\n\n // Check environment variables\n const config = { ...DEFAULT_CONFIG };\n if (process.env['TWILIO_ACCOUNT_SID']) {\n config.accountSid = process.env['TWILIO_ACCOUNT_SID'];\n }\n if (process.env['TWILIO_AUTH_TOKEN']) {\n config.authToken = process.env['TWILIO_AUTH_TOKEN'];\n }\n if (process.env['TWILIO_FROM_NUMBER']) {\n config.fromNumber = process.env['TWILIO_FROM_NUMBER'];\n }\n if (process.env['TWILIO_TO_NUMBER']) {\n config.toNumber = process.env['TWILIO_TO_NUMBER'];\n }\n\n return config;\n}\n\nexport function saveSMSConfig(config: SMSConfig): void {\n try {\n const dir = join(homedir(), '.stackmemory');\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n // Don't save sensitive credentials to file\n const safeConfig = { ...config };\n delete safeConfig.accountSid;\n delete safeConfig.authToken;\n writeFileSync(CONFIG_PATH, JSON.stringify(safeConfig, null, 2));\n } catch {\n // Silently fail\n }\n}\n\nfunction isQuietHours(config: SMSConfig): boolean {\n if (!config.quietHours?.enabled) return false;\n\n const now = new Date();\n const currentTime = now.getHours() * 60 + now.getMinutes();\n\n const [startH, startM] = config.quietHours.start.split(':').map(Number);\n const [endH, endM] = config.quietHours.end.split(':').map(Number);\n\n const startTime = startH * 60 + startM;\n const endTime = endH * 60 + endM;\n\n // Handle overnight quiet hours (e.g., 22:00 - 08:00)\n if (startTime > endTime) {\n return currentTime >= startTime || currentTime < endTime;\n }\n\n return currentTime >= startTime && currentTime < endTime;\n}\n\nfunction generatePromptId(): string {\n return Math.random().toString(36).substring(2, 10);\n}\n\nfunction formatPromptMessage(payload: NotificationPayload): string {\n let message = `${payload.title}\\n\\n${payload.message}`;\n\n if (payload.prompt) {\n message += '\\n\\n';\n\n if (payload.prompt.question) {\n message += `${payload.prompt.question}\\n`;\n }\n\n if (payload.prompt.type === 'yesno') {\n message += 'Reply Y for Yes, N for No';\n } else if (payload.prompt.type === 'options' && payload.prompt.options) {\n payload.prompt.options.forEach((opt) => {\n message += `${opt.key}. ${opt.label}\\n`;\n });\n message += '\\nReply with number to select';\n } else if (payload.prompt.type === 'freeform') {\n message += 'Reply with your response';\n }\n }\n\n return message;\n}\n\nexport async function sendSMSNotification(\n payload: NotificationPayload\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const config = loadSMSConfig();\n\n if (!config.enabled) {\n return { success: false, error: 'SMS notifications disabled' };\n }\n\n // Check notification type is enabled\n const typeMap: Record<string, keyof typeof config.notifyOn> = {\n task_complete: 'taskComplete',\n review_ready: 'reviewReady',\n error: 'error',\n custom: 'custom',\n };\n\n if (!config.notifyOn[typeMap[payload.type]]) {\n return {\n success: false,\n error: `Notifications for ${payload.type} disabled`,\n };\n }\n\n // Check quiet hours\n if (isQuietHours(config)) {\n return { success: false, error: 'Quiet hours active' };\n }\n\n // Validate credentials\n if (\n !config.accountSid ||\n !config.authToken ||\n !config.fromNumber ||\n !config.toNumber\n ) {\n return {\n success: false,\n error:\n 'Missing Twilio credentials. Set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER, TWILIO_TO_NUMBER',\n };\n }\n\n const message = formatPromptMessage(payload);\n let promptId: string | undefined;\n\n // Store pending prompt if interactive\n if (payload.prompt) {\n promptId = generatePromptId();\n const expiresAt = new Date(\n Date.now() + config.responseTimeout * 1000\n ).toISOString();\n\n const pendingPrompt: PendingPrompt = {\n id: promptId,\n timestamp: new Date().toISOString(),\n message: payload.message,\n options: payload.prompt.options || [],\n type: payload.prompt.type,\n expiresAt,\n };\n\n config.pendingPrompts.push(pendingPrompt);\n saveSMSConfig(config);\n }\n\n try {\n // Use Twilio API\n const twilioUrl = `https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`;\n\n const response = await fetch(twilioUrl, {\n method: 'POST',\n headers: {\n Authorization:\n 'Basic ' +\n Buffer.from(`${config.accountSid}:${config.authToken}`).toString(\n 'base64'\n ),\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n From: config.fromNumber,\n To: config.toNumber,\n Body: message,\n }),\n });\n\n if (!response.ok) {\n const errorData = await response.text();\n return { success: false, error: `Twilio error: ${errorData}` };\n }\n\n return { success: true, promptId };\n } catch (err) {\n return {\n success: false,\n error: `Failed to send SMS: ${err instanceof Error ? err.message : String(err)}`,\n };\n }\n}\n\nexport function processIncomingResponse(\n from: string,\n body: string\n): {\n matched: boolean;\n prompt?: PendingPrompt;\n response?: string;\n action?: string;\n} {\n const config = loadSMSConfig();\n\n // Normalize response\n const response = body.trim().toLowerCase();\n\n // Find matching pending prompt (most recent first)\n const now = new Date();\n const validPrompts = config.pendingPrompts.filter(\n (p) => new Date(p.expiresAt) > now\n );\n\n if (validPrompts.length === 0) {\n return { matched: false };\n }\n\n // Get most recent prompt\n const prompt = validPrompts[validPrompts.length - 1];\n\n let matchedOption: PromptOption | undefined;\n\n if (prompt.type === 'yesno') {\n if (response === 'y' || response === 'yes') {\n matchedOption = { key: 'y', label: 'Yes' };\n } else if (response === 'n' || response === 'no') {\n matchedOption = { key: 'n', label: 'No' };\n }\n } else if (prompt.type === 'options') {\n matchedOption = prompt.options.find(\n (opt) => opt.key.toLowerCase() === response\n );\n } else if (prompt.type === 'freeform') {\n matchedOption = { key: response, label: response };\n }\n\n // Remove processed prompt\n config.pendingPrompts = config.pendingPrompts.filter(\n (p) => p.id !== prompt.id\n );\n saveSMSConfig(config);\n\n if (matchedOption) {\n return {\n matched: true,\n prompt,\n response: matchedOption.key,\n action: matchedOption.action,\n };\n }\n\n return { matched: false, prompt };\n}\n\n// Convenience functions for common notifications\n\nexport async function notifyReviewReady(\n title: string,\n description: string,\n options?: { label: string; action?: string }[]\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n const payload: NotificationPayload = {\n type: 'review_ready',\n title: `Review Ready: ${title}`,\n message: description,\n };\n\n if (options && options.length > 0) {\n payload.prompt = {\n type: 'options',\n options: options.map((opt, i) => ({\n key: String(i + 1),\n label: opt.label,\n action: opt.action,\n })),\n question: 'What would you like to do?',\n };\n }\n\n return sendSMSNotification(payload);\n}\n\nexport async function notifyWithYesNo(\n title: string,\n question: string,\n yesAction?: string,\n noAction?: string\n): Promise<{ success: boolean; promptId?: string; error?: string }> {\n return sendSMSNotification({\n type: 'custom',\n title,\n message: question,\n prompt: {\n type: 'yesno',\n options: [\n { key: 'y', label: 'Yes', action: yesAction },\n { key: 'n', label: 'No', action: noAction },\n ],\n },\n });\n}\n\nexport async function notifyTaskComplete(\n taskName: string,\n summary: string\n): Promise<{ success: boolean; error?: string }> {\n return sendSMSNotification({\n type: 'task_complete',\n title: `Task Complete: ${taskName}`,\n message: summary,\n });\n}\n\nexport async function notifyError(\n error: string,\n context?: string\n): Promise<{ success: boolean; error?: string }> {\n return sendSMSNotification({\n type: 'error',\n title: 'Error Alert',\n message: context ? `${error}\\n\\nContext: ${context}` : error,\n });\n}\n\n// Clean up expired prompts\nexport function cleanupExpiredPrompts(): number {\n const config = loadSMSConfig();\n const now = new Date();\n const before = config.pendingPrompts.length;\n\n config.pendingPrompts = config.pendingPrompts.filter(\n (p) => new Date(p.expiresAt) > now\n );\n\n const removed = before - config.pendingPrompts.length;\n if (removed > 0) {\n saveSMSConfig(config);\n }\n\n return removed;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,122 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import { createServer } from "http";
6
+ import { parse as parseUrl } from "url";
7
+ import { processIncomingResponse, loadSMSConfig } from "./sms-notify.js";
8
+ import { execSync } from "child_process";
9
+ function parseFormData(body) {
10
+ const params = new URLSearchParams(body);
11
+ const result = {};
12
+ params.forEach((value, key) => {
13
+ result[key] = value;
14
+ });
15
+ return result;
16
+ }
17
+ function handleSMSWebhook(payload) {
18
+ const { From, Body } = payload;
19
+ console.log(`[sms-webhook] Received from ${From}: ${Body}`);
20
+ const result = processIncomingResponse(From, Body);
21
+ if (!result.matched) {
22
+ if (result.prompt) {
23
+ return {
24
+ response: `Invalid response. Expected: ${result.prompt.options.map((o) => o.key).join(", ")}`
25
+ };
26
+ }
27
+ return { response: "No pending prompt found." };
28
+ }
29
+ if (result.action) {
30
+ try {
31
+ console.log(`[sms-webhook] Executing action: ${result.action}`);
32
+ execSync(result.action, { stdio: "inherit" });
33
+ return {
34
+ response: `Got it! Executing: ${result.action}`,
35
+ action: result.action
36
+ };
37
+ } catch {
38
+ return {
39
+ response: `Received "${result.response}" but action failed.`,
40
+ action: result.action
41
+ };
42
+ }
43
+ }
44
+ return {
45
+ response: `Received: ${result.response}`
46
+ };
47
+ }
48
+ function twimlResponse(message) {
49
+ return `<?xml version="1.0" encoding="UTF-8"?>
50
+ <Response>
51
+ <Message>${escapeXml(message)}</Message>
52
+ </Response>`;
53
+ }
54
+ function escapeXml(str) {
55
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
56
+ }
57
+ function startWebhookServer(port = 3456) {
58
+ const server = createServer(
59
+ async (req, res) => {
60
+ const url = parseUrl(req.url || "/", true);
61
+ if (url.pathname === "/health") {
62
+ res.writeHead(200, { "Content-Type": "application/json" });
63
+ res.end(JSON.stringify({ status: "ok" }));
64
+ return;
65
+ }
66
+ if (url.pathname === "/sms" && req.method === "POST") {
67
+ let body = "";
68
+ req.on("data", (chunk) => {
69
+ body += chunk;
70
+ });
71
+ req.on("end", () => {
72
+ try {
73
+ const payload = parseFormData(
74
+ body
75
+ );
76
+ const result = handleSMSWebhook(payload);
77
+ res.writeHead(200, { "Content-Type": "text/xml" });
78
+ res.end(twimlResponse(result.response));
79
+ } catch (err) {
80
+ console.error("[sms-webhook] Error:", err);
81
+ res.writeHead(500, { "Content-Type": "text/xml" });
82
+ res.end(twimlResponse("Error processing message"));
83
+ }
84
+ });
85
+ return;
86
+ }
87
+ if (url.pathname === "/status") {
88
+ const config = loadSMSConfig();
89
+ res.writeHead(200, { "Content-Type": "application/json" });
90
+ res.end(
91
+ JSON.stringify({
92
+ enabled: config.enabled,
93
+ pendingPrompts: config.pendingPrompts.length
94
+ })
95
+ );
96
+ return;
97
+ }
98
+ res.writeHead(404);
99
+ res.end("Not found");
100
+ }
101
+ );
102
+ server.listen(port, () => {
103
+ console.log(`[sms-webhook] Server listening on port ${port}`);
104
+ console.log(`[sms-webhook] Webhook URL: http://localhost:${port}/sms`);
105
+ console.log(`[sms-webhook] Configure this URL in Twilio console`);
106
+ });
107
+ }
108
+ function smsWebhookMiddleware(req, res) {
109
+ const result = handleSMSWebhook(req.body);
110
+ res.type("text/xml");
111
+ res.send(twimlResponse(result.response));
112
+ }
113
+ if (process.argv[1]?.endsWith("sms-webhook.js")) {
114
+ const port = parseInt(process.env["SMS_WEBHOOK_PORT"] || "3456", 10);
115
+ startWebhookServer(port);
116
+ }
117
+ export {
118
+ handleSMSWebhook,
119
+ smsWebhookMiddleware,
120
+ startWebhookServer
121
+ };
122
+ //# sourceMappingURL=sms-webhook.js.map
@@ -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 { processIncomingResponse, loadSMSConfig } from './sms-notify.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\nexport function handleSMSWebhook(payload: TwilioWebhookPayload): {\n response: string;\n action?: string;\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 // Execute action if specified\n if (result.action) {\n try {\n console.log(`[sms-webhook] Executing action: ${result.action}`);\n execSync(result.action, { stdio: 'inherit' });\n return {\n response: `Got it! Executing: ${result.action}`,\n action: result.action,\n };\n } catch {\n return {\n response: `Received \"${result.response}\" but action failed.`,\n action: result.action,\n };\n }\n }\n\n return {\n response: `Received: ${result.response}`,\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, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\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": ";;;;AAKA,SAAS,oBAAqD;AAC9D,SAAS,SAAS,gBAAgB;AAClC,SAAS,yBAAyB,qBAAqB;AACvD,SAAS,gBAAgB;AASzB,SAAS,cAAc,MAAsC;AAC3D,QAAM,SAAS,IAAI,gBAAgB,IAAI;AACvC,QAAM,SAAiC,CAAC;AACxC,SAAO,QAAQ,CAAC,OAAO,QAAQ;AAC7B,WAAO,GAAG,IAAI;AAAA,EAChB,CAAC;AACD,SAAO;AACT;AAEO,SAAS,iBAAiB,SAG/B;AACA,QAAM,EAAE,MAAM,KAAK,IAAI;AAEvB,UAAQ,IAAI,+BAA+B,IAAI,KAAK,IAAI,EAAE;AAE1D,QAAM,SAAS,wBAAwB,MAAM,IAAI;AAEjD,MAAI,CAAC,OAAO,SAAS;AACnB,QAAI,OAAO,QAAQ;AACjB,aAAO;AAAA,QACL,UAAU,+BAA+B,OAAO,OAAO,QAAQ,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;AAAA,MAC7F;AAAA,IACF;AACA,WAAO,EAAE,UAAU,2BAA2B;AAAA,EAChD;AAGA,MAAI,OAAO,QAAQ;AACjB,QAAI;AACF,cAAQ,IAAI,mCAAmC,OAAO,MAAM,EAAE;AAC9D,eAAS,OAAO,QAAQ,EAAE,OAAO,UAAU,CAAC;AAC5C,aAAO;AAAA,QACL,UAAU,sBAAsB,OAAO,MAAM;AAAA,QAC7C,QAAQ,OAAO;AAAA,MACjB;AAAA,IACF,QAAQ;AACN,aAAO;AAAA,QACL,UAAU,aAAa,OAAO,QAAQ;AAAA,QACtC,QAAQ,OAAO;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,UAAU,aAAa,OAAO,QAAQ;AAAA,EACxC;AACF;AAGA,SAAS,cAAc,SAAyB;AAC9C,SAAO;AAAA;AAAA,aAEI,UAAU,OAAO,CAAC;AAAA;AAE/B;AAEA,SAAS,UAAU,KAAqB;AACtC,SAAO,IACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,QAAQ;AAC3B;AAGO,SAAS,mBAAmB,OAAe,MAAY;AAC5D,QAAM,SAAS;AAAA,IACb,OAAO,KAAsB,QAAwB;AACnD,YAAM,MAAM,SAAS,IAAI,OAAO,KAAK,IAAI;AAGzC,UAAI,IAAI,aAAa,WAAW;AAC9B,YAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,YAAI,IAAI,KAAK,UAAU,EAAE,QAAQ,KAAK,CAAC,CAAC;AACxC;AAAA,MACF;AAGA,UAAI,IAAI,aAAa,UAAU,IAAI,WAAW,QAAQ;AACpD,YAAI,OAAO;AACX,YAAI,GAAG,QAAQ,CAAC,UAAU;AACxB,kBAAQ;AAAA,QACV,CAAC;AAED,YAAI,GAAG,OAAO,MAAM;AAClB,cAAI;AACF,kBAAM,UAAU;AAAA,cACd;AAAA,YACF;AACA,kBAAM,SAAS,iBAAiB,OAAO;AAEvC,gBAAI,UAAU,KAAK,EAAE,gBAAgB,WAAW,CAAC;AACjD,gBAAI,IAAI,cAAc,OAAO,QAAQ,CAAC;AAAA,UACxC,SAAS,KAAK;AACZ,oBAAQ,MAAM,wBAAwB,GAAG;AACzC,gBAAI,UAAU,KAAK,EAAE,gBAAgB,WAAW,CAAC;AACjD,gBAAI,IAAI,cAAc,0BAA0B,CAAC;AAAA,UACnD;AAAA,QACF,CAAC;AACD;AAAA,MACF;AAGA,UAAI,IAAI,aAAa,WAAW;AAC9B,cAAM,SAAS,cAAc;AAC7B,YAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,YAAI;AAAA,UACF,KAAK,UAAU;AAAA,YACb,SAAS,OAAO;AAAA,YAChB,gBAAgB,OAAO,eAAe;AAAA,UACxC,CAAC;AAAA,QACH;AACA;AAAA,MACF;AAEA,UAAI,UAAU,GAAG;AACjB,UAAI,IAAI,WAAW;AAAA,IACrB;AAAA,EACF;AAEA,SAAO,OAAO,MAAM,MAAM;AACxB,YAAQ,IAAI,0CAA0C,IAAI,EAAE;AAC5D,YAAQ,IAAI,+CAA+C,IAAI,MAAM;AACrE,YAAQ,IAAI,oDAAoD;AAAA,EAClE,CAAC;AACH;AAGO,SAAS,qBACd,KACA,KACM;AACN,QAAM,SAAS,iBAAiB,IAAI,IAAI;AACxC,MAAI,KAAK,UAAU;AACnB,MAAI,KAAK,cAAc,OAAO,QAAQ,CAAC;AACzC;AAGA,IAAI,QAAQ,KAAK,CAAC,GAAG,SAAS,gBAAgB,GAAG;AAC/C,QAAM,OAAO,SAAS,QAAQ,IAAI,kBAAkB,KAAK,QAAQ,EAAE;AACnE,qBAAmB,IAAI;AACzB;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackmemoryai/stackmemory",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
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,144 @@
1
+ #!/bin/bash
2
+ # Install auto-background hook for Claude Code
3
+
4
+ set -e
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ HOOK_SOURCE="$SCRIPT_DIR/../templates/claude-hooks/auto-background-hook.js"
8
+ CLAUDE_DIR="$HOME/.claude"
9
+ HOOKS_DIR="$CLAUDE_DIR/hooks"
10
+ SETTINGS_FILE="$CLAUDE_DIR/settings.json"
11
+ CONFIG_DIR="$HOME/.stackmemory"
12
+
13
+ echo "Installing auto-background hook for Claude Code..."
14
+
15
+ # Create directories
16
+ mkdir -p "$HOOKS_DIR"
17
+ mkdir -p "$CONFIG_DIR"
18
+
19
+ # Copy hook script
20
+ HOOK_DEST="$HOOKS_DIR/auto-background-hook.js"
21
+ cp "$HOOK_SOURCE" "$HOOK_DEST"
22
+ chmod +x "$HOOK_DEST"
23
+ echo "Installed hook to $HOOK_DEST"
24
+
25
+ # Create default config if not exists
26
+ CONFIG_FILE="$CONFIG_DIR/auto-background.json"
27
+ if [ ! -f "$CONFIG_FILE" ]; then
28
+ cat > "$CONFIG_FILE" << 'EOF'
29
+ {
30
+ "enabled": true,
31
+ "timeoutMs": 5000,
32
+ "alwaysBackground": [
33
+ "npm install",
34
+ "npm ci",
35
+ "yarn install",
36
+ "pnpm install",
37
+ "bun install",
38
+ "npm run build",
39
+ "yarn build",
40
+ "pnpm build",
41
+ "cargo build",
42
+ "go build",
43
+ "make",
44
+ "npm test",
45
+ "npm run test",
46
+ "yarn test",
47
+ "pytest",
48
+ "jest",
49
+ "vitest",
50
+ "cargo test",
51
+ "docker build",
52
+ "docker-compose up",
53
+ "docker compose up",
54
+ "git clone",
55
+ "git fetch --all",
56
+ "npx tsc",
57
+ "tsc --noEmit",
58
+ "eslint .",
59
+ "npm run lint"
60
+ ],
61
+ "neverBackground": [
62
+ "vim",
63
+ "nvim",
64
+ "nano",
65
+ "less",
66
+ "more",
67
+ "top",
68
+ "htop",
69
+ "echo",
70
+ "cat",
71
+ "ls",
72
+ "pwd",
73
+ "cd",
74
+ "which",
75
+ "git status",
76
+ "git diff",
77
+ "git log"
78
+ ],
79
+ "verbose": false
80
+ }
81
+ EOF
82
+ echo "Created config at $CONFIG_FILE"
83
+ fi
84
+
85
+ # Update Claude Code settings
86
+ if [ -f "$SETTINGS_FILE" ]; then
87
+ # Check if jq is available
88
+ if command -v jq &> /dev/null; then
89
+ # Backup existing settings
90
+ cp "$SETTINGS_FILE" "$SETTINGS_FILE.bak"
91
+
92
+ # Add hook to settings
93
+ HOOK_CMD="node $HOOK_DEST"
94
+
95
+ # Check if hooks.pre_tool_use exists
96
+ if jq -e '.hooks.pre_tool_use' "$SETTINGS_FILE" > /dev/null 2>&1; then
97
+ # Check if hook already added
98
+ if ! jq -e ".hooks.pre_tool_use | index(\"$HOOK_CMD\")" "$SETTINGS_FILE" > /dev/null 2>&1; then
99
+ jq ".hooks.pre_tool_use += [\"$HOOK_CMD\"]" "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp"
100
+ mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
101
+ echo "Added hook to existing pre_tool_use array"
102
+ else
103
+ echo "Hook already configured"
104
+ fi
105
+ else
106
+ # Create hooks.pre_tool_use array
107
+ jq ".hooks = (.hooks // {}) | .hooks.pre_tool_use = [\"$HOOK_CMD\"]" "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp"
108
+ mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
109
+ echo "Created hooks.pre_tool_use with auto-background hook"
110
+ fi
111
+ else
112
+ echo ""
113
+ echo "jq not found. Please manually add to $SETTINGS_FILE:"
114
+ echo ""
115
+ echo ' "hooks": {'
116
+ echo ' "pre_tool_use": ["node '$HOOK_DEST'"]'
117
+ echo ' }'
118
+ fi
119
+ else
120
+ # Create new settings file
121
+ cat > "$SETTINGS_FILE" << EOF
122
+ {
123
+ "hooks": {
124
+ "pre_tool_use": ["node $HOOK_DEST"]
125
+ }
126
+ }
127
+ EOF
128
+ echo "Created settings file with hook"
129
+ fi
130
+
131
+ echo ""
132
+ echo "Auto-background hook installed!"
133
+ echo ""
134
+ echo "Configuration: $CONFIG_FILE"
135
+ echo " - Edit to customize which commands auto-background"
136
+ echo " - Set 'enabled': false to disable"
137
+ echo " - Set 'verbose': true for debug logging"
138
+ echo ""
139
+ echo "Commands that will auto-background:"
140
+ echo " - npm install/build/test"
141
+ echo " - yarn/pnpm/bun install"
142
+ echo " - docker build"
143
+ echo " - cargo/go build/test"
144
+ echo " - And more (see config)"
@@ -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,156 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Claude Code pre-tool-use hook for auto-backgrounding commands
4
+ *
5
+ * Install: Add to ~/.claude/settings.json hooks.pre_tool_use
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ const CONFIG_PATH = path.join(
13
+ os.homedir(),
14
+ '.stackmemory',
15
+ 'auto-background.json'
16
+ );
17
+
18
+ const DEFAULT_CONFIG = {
19
+ enabled: true,
20
+ timeoutMs: 5000,
21
+ alwaysBackground: [
22
+ 'npm install',
23
+ 'npm ci',
24
+ 'yarn install',
25
+ 'pnpm install',
26
+ 'bun install',
27
+ 'npm run build',
28
+ 'yarn build',
29
+ 'pnpm build',
30
+ 'cargo build',
31
+ 'go build',
32
+ 'make',
33
+ 'npm test',
34
+ 'npm run test',
35
+ 'yarn test',
36
+ 'pytest',
37
+ 'jest',
38
+ 'vitest',
39
+ 'cargo test',
40
+ 'docker build',
41
+ 'docker-compose up',
42
+ 'docker compose up',
43
+ 'git clone',
44
+ 'git fetch --all',
45
+ 'npx tsc',
46
+ 'tsc --noEmit',
47
+ 'eslint .',
48
+ 'npm run lint',
49
+ ],
50
+ neverBackground: [
51
+ 'vim',
52
+ 'nvim',
53
+ 'nano',
54
+ 'less',
55
+ 'more',
56
+ 'top',
57
+ 'htop',
58
+ 'echo',
59
+ 'cat',
60
+ 'ls',
61
+ 'pwd',
62
+ 'cd',
63
+ 'which',
64
+ 'git status',
65
+ 'git diff',
66
+ 'git log',
67
+ ],
68
+ verbose: false,
69
+ };
70
+
71
+ function loadConfig() {
72
+ try {
73
+ if (fs.existsSync(CONFIG_PATH)) {
74
+ return {
75
+ ...DEFAULT_CONFIG,
76
+ ...JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')),
77
+ };
78
+ }
79
+ } catch {}
80
+ return DEFAULT_CONFIG;
81
+ }
82
+
83
+ function shouldAutoBackground(command, config) {
84
+ if (!config.enabled) return false;
85
+
86
+ const cmd = command.trim().toLowerCase();
87
+
88
+ // Never background these
89
+ for (const pattern of config.neverBackground) {
90
+ if (cmd.startsWith(pattern.toLowerCase())) return false;
91
+ }
92
+
93
+ // Always background these
94
+ for (const pattern of config.alwaysBackground) {
95
+ if (cmd.startsWith(pattern.toLowerCase())) return true;
96
+ }
97
+
98
+ return false;
99
+ }
100
+
101
+ // Read hook input from stdin
102
+ let input = '';
103
+ process.stdin.setEncoding('utf8');
104
+ process.stdin.on('data', (chunk) => (input += chunk));
105
+ process.stdin.on('end', () => {
106
+ try {
107
+ const hookData = JSON.parse(input);
108
+ const { tool_name, tool_input } = hookData;
109
+
110
+ // Only process Bash tool
111
+ if (tool_name !== 'Bash') {
112
+ // Allow other tools through unchanged
113
+ console.log(JSON.stringify({ decision: 'allow' }));
114
+ return;
115
+ }
116
+
117
+ const command = tool_input?.command;
118
+ if (!command) {
119
+ console.log(JSON.stringify({ decision: 'allow' }));
120
+ return;
121
+ }
122
+
123
+ // Already backgrounded
124
+ if (tool_input.run_in_background === true) {
125
+ console.log(JSON.stringify({ decision: 'allow' }));
126
+ return;
127
+ }
128
+
129
+ const config = loadConfig();
130
+
131
+ if (shouldAutoBackground(command, config)) {
132
+ if (config.verbose) {
133
+ console.error(
134
+ `[auto-bg] Backgrounding: ${command.substring(0, 60)}...`
135
+ );
136
+ }
137
+
138
+ // Modify the tool input to add run_in_background
139
+ console.log(
140
+ JSON.stringify({
141
+ decision: 'modify',
142
+ tool_input: {
143
+ ...tool_input,
144
+ run_in_background: true,
145
+ },
146
+ })
147
+ );
148
+ } else {
149
+ console.log(JSON.stringify({ decision: 'allow' }));
150
+ }
151
+ } catch (err) {
152
+ // On error, allow the command through unchanged
153
+ console.error('[auto-bg] Error:', err.message);
154
+ console.log(JSON.stringify({ decision: 'allow' }));
155
+ }
156
+ });