@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.
- package/dist/cli/commands/auto-background.js +180 -0
- package/dist/cli/commands/auto-background.js.map +7 -0
- package/dist/cli/commands/sms-notify.js +276 -0
- package/dist/cli/commands/sms-notify.js.map +7 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +2 -2
- package/dist/hooks/auto-background.js +146 -0
- package/dist/hooks/auto-background.js.map +7 -0
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/index.js.map +2 -2
- package/dist/hooks/sms-notify.js +286 -0
- package/dist/hooks/sms-notify.js.map +7 -0
- package/dist/hooks/sms-webhook.js +122 -0
- package/dist/hooks/sms-webhook.js.map +7 -0
- package/package.json +1 -1
- package/scripts/install-auto-background-hook.sh +144 -0
- package/scripts/install-notify-hook.sh +84 -0
- package/templates/claude-hooks/auto-background-hook.js +156 -0
- package/templates/claude-hooks/notify-review-hook.js +171 -0
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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, '&')\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.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
|
+
});
|