@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
JavaScript
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