UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

198 lines (197 loc) 5.57 kB
#!/usr/bin/env node import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { existsSync, readFileSync, writeFileSync } from "fs"; import { join } from "path"; import { homedir } from "os"; import { sendNotification, loadSMSConfig } from "./sms-notify.js"; import { getFrameDigestData, generateMobileDigest, loadSyncOptions } from "./whatsapp-sync.js"; const STACKMEMORY_DIR = join(homedir(), ".stackmemory"); const INCOMING_REQUEST_PATH = join( STACKMEMORY_DIR, "sms-incoming-request.json" ); const LATEST_RESPONSE_PATH = join(STACKMEMORY_DIR, "sms-latest-response.json"); const HOOK_STATE_PATH = join(STACKMEMORY_DIR, "claude-hook-state.json"); function loadHookState() { try { if (existsSync(HOOK_STATE_PATH)) { return JSON.parse(readFileSync(HOOK_STATE_PATH, "utf8")); } } catch { } return { toolCount: 0, significantChanges: false }; } function saveHookState(state) { try { writeFileSync(HOOK_STATE_PATH, JSON.stringify(state, null, 2)); } catch { } } function checkIncomingRequests() { try { if (!existsSync(INCOMING_REQUEST_PATH)) return null; const data = JSON.parse( readFileSync(INCOMING_REQUEST_PATH, "utf8") ); if (data.processed) return null; return data; } catch { return null; } } function markRequestProcessed() { try { if (!existsSync(INCOMING_REQUEST_PATH)) return; const data = JSON.parse(readFileSync(INCOMING_REQUEST_PATH, "utf8")); data.processed = true; writeFileSync(INCOMING_REQUEST_PATH, JSON.stringify(data, null, 2)); } catch { } } function getLatestResponse() { try { if (!existsSync(LATEST_RESPONSE_PATH)) return null; return JSON.parse(readFileSync(LATEST_RESPONSE_PATH, "utf8")); } catch { return null; } } async function sendDigest() { const data = await getFrameDigestData(); if (!data) return; const options = loadSyncOptions(); const digest = generateMobileDigest(data, options); await sendNotification({ type: "context_sync", title: "Context Update", message: digest }); } async function handlePreToolUse() { const state = loadHookState(); state.toolCount++; if (state.toolCount % 5 === 0) { const incoming = checkIncomingRequests(); if (incoming) { console.error(` [WhatsApp] Message from ${incoming.from}:`); console.error(` "${incoming.message}"`); console.error(` (Received at ${incoming.timestamp}) `); markRequestProcessed(); } } saveHookState(state); } async function handleStop() { const config = loadSMSConfig(); if (!config.enabled) return; const state = loadHookState(); console.error(`[WhatsApp] Session ended after ${state.toolCount} tool calls`); try { await sendDigest(); console.error("[WhatsApp] Session digest sent"); } catch (err) { console.error("[WhatsApp] Failed to send digest:", err); } saveHookState({ toolCount: 0, significantChanges: false }); } async function handleNotification(input) { if (input.includes("[notify]") || input.includes("[whatsapp]")) { const message = input.replace(/\[notify\]|\[whatsapp\]/gi, "").trim(); await sendNotification({ type: "custom", title: "Claude", message: message.slice(0, 300) }); } } async function pollForResponse(timeoutMs = 6e4) { const startTime = Date.now(); const pollInterval = 2e3; while (Date.now() - startTime < timeoutMs) { const response = getLatestResponse(); if (response) { const responseAge = Date.now() - new Date(response.timestamp).getTime(); if (responseAge < 5e3) { return response.response; } } await new Promise((resolve) => setTimeout(resolve, pollInterval)); } return null; } async function askViaWhatsApp(question, options) { const config = loadSMSConfig(); if (!config.enabled) { console.error("[WhatsApp] Notifications not enabled"); return null; } await sendNotification({ type: "custom", title: "Claude Question", message: question, prompt: options ? { type: "options", options: options.map((o) => ({ ...o, action: o.key })) } : void 0 }); return pollForResponse(12e4); } async function main() { const args = process.argv.slice(2); const hookType = args[0]; let input = ""; if (!process.stdin.isTTY) { input = readFileSync(0, "utf8"); } switch (hookType) { case "pre-tool": case "PreToolUse": await handlePreToolUse(); break; case "stop": case "Stop": await handleStop(); break; case "notification": case "Notification": await handleNotification(input); break; case "check": const incoming = checkIncomingRequests(); if (incoming) { console.log(JSON.stringify(incoming)); } break; case "send-digest": await sendDigest(); break; case "poll": const response = await pollForResponse(parseInt(args[1] || "60000", 10)); if (response) { console.log(response); } break; default: console.error("Usage: claude-code-whatsapp-hook.js <hook-type>"); console.error( "Hook types: pre-tool, stop, notification, check, send-digest, poll" ); process.exit(1); } } if (process.argv[1]?.includes("claude-code-whatsapp-hook")) { main().catch(console.error); } export { askViaWhatsApp }; //# sourceMappingURL=claude-code-whatsapp-hook.js.map