@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.
480 lines (477 loc) • 14 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 } from "fs";
import { join } from "path";
import { homedir } from "os";
import { execFileSync } from "child_process";
import { writeFileSecure, ensureSecureDir } from "./secure-fs.js";
import { WhatsAppCommandsConfigSchema, parseConfigSafe } from "./schemas.js";
import { executeActionSafe } from "./sms-action-runner.js";
import {
syncContext,
getFrameDigestData,
generateMobileDigest,
loadSyncOptions
} from "./whatsapp-sync.js";
import { sendNotification } from "./sms-notify.js";
const MAX_REGEX_INPUT_LENGTH = 200;
const DANGEROUS_PATTERNS = [
/(\+|\*|\?)\s*(\+|\*|\?)/,
// Nested quantifiers like .+* or .*+
/\(\?[^)]*\)\s*[+*]/,
// Quantified groups with + or *
/\[[^\]]*\]\s*[+*]\s*[+*]/,
// Character classes with nested quantifiers
/(\.\*|\.\+)\s*(\.\*|\.\+)/,
// Overlapping .* or .+
/\(\[[^\]]+\]\+\)\+/,
// Nested + with character class
/\(.*\+\).*\+/
// Nested + quantifiers
];
function isPatternSafe(pattern) {
for (const dangerous of DANGEROUS_PATTERNS) {
if (dangerous.test(pattern)) {
console.warn(
`[whatsapp-commands] Potentially dangerous regex pattern blocked: ${pattern}`
);
return false;
}
}
const quantifierCount = (pattern.match(/[+*?]/g) || []).length;
const groupCount = (pattern.match(/\(/g) || []).length;
if (quantifierCount > 5 || groupCount > 3) {
console.warn(
`[whatsapp-commands] Complex regex pattern blocked: ${pattern} (${quantifierCount} quantifiers, ${groupCount} groups)`
);
return false;
}
return true;
}
function safeRegexTest(pattern, input) {
if (!isPatternSafe(pattern)) {
return false;
}
const safeInput = input.slice(0, MAX_REGEX_INPUT_LENGTH);
try {
const regex = new RegExp(pattern);
return regex.test(safeInput);
} catch {
console.warn(`[whatsapp-commands] Invalid regex pattern: ${pattern}`);
return false;
}
}
const CONFIG_PATH = join(homedir(), ".stackmemory", "whatsapp-commands.json");
const REMOTE_SESSIONS_PATH = join(
homedir(),
".stackmemory",
"remote-sessions.json"
);
function loadRemoteSessions() {
try {
if (existsSync(REMOTE_SESSIONS_PATH)) {
return JSON.parse(readFileSync(REMOTE_SESSIONS_PATH, "utf8"));
}
} catch {
}
return { sessions: [] };
}
function saveRemoteSessions(store) {
try {
ensureSecureDir(join(homedir(), ".stackmemory"));
writeFileSecure(REMOTE_SESSIONS_PATH, JSON.stringify(store, null, 2));
} catch {
}
}
function addRemoteSession(session) {
const store = loadRemoteSessions();
store.sessions = [session, ...store.sessions.slice(0, 19)];
saveRemoteSessions(store);
}
function getRemoteSessions() {
return loadRemoteSessions().sessions;
}
function getActiveRemoteSessions() {
return loadRemoteSessions().sessions.filter((s) => s.status === "active");
}
const DEFAULT_COMMANDS = [
{
name: "help",
description: "List available commands",
enabled: true
},
{
name: "status",
description: "Get current task/frame status",
enabled: true
},
{
name: "sessions",
description: "List active remote sessions with URLs",
enabled: true
},
{
name: "remote",
description: "Launch remote Claude session (requires task prompt)",
enabled: true,
requiresArg: true
},
// Disabled by default - can be enabled in config if needed
{
name: "context",
description: "Get latest context digest",
enabled: false
},
{
name: "sync",
description: "Push current context to WhatsApp",
enabled: false
},
{
name: "tasks",
description: "List active tasks",
enabled: false
}
];
const DEFAULT_CONFIG = {
enabled: true,
commands: DEFAULT_COMMANDS
};
function loadCommandsConfig() {
try {
if (existsSync(CONFIG_PATH)) {
const data = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
return parseConfigSafe(
WhatsAppCommandsConfigSchema,
{ ...DEFAULT_CONFIG, ...data },
DEFAULT_CONFIG,
"whatsapp-commands"
);
}
} catch {
}
return { ...DEFAULT_CONFIG };
}
function saveCommandsConfig(config) {
try {
ensureSecureDir(join(homedir(), ".stackmemory"));
writeFileSecure(CONFIG_PATH, JSON.stringify(config, null, 2));
} catch {
}
}
function isCommand(message) {
const trimmed = message.trim().toLowerCase();
const config = loadCommandsConfig();
if (!config.enabled) return false;
const words = trimmed.split(/\s+/);
const firstWord = words[0];
return config.commands.some(
(cmd) => cmd.enabled && cmd.name.toLowerCase() === firstWord
);
}
function parseCommand(message) {
const trimmed = message.trim();
const words = trimmed.split(/\s+/);
if (words.length === 0) return null;
const name = words[0].toLowerCase();
const arg = words.slice(1).join(" ").trim() || void 0;
return { name, arg };
}
function generateHelpText(config) {
const lines = ["Available commands:"];
config.commands.filter((cmd) => cmd.enabled).forEach((cmd) => {
const argHint = cmd.requiresArg ? " <arg>" : "";
lines.push(` ${cmd.name}${argHint} - ${cmd.description}`);
});
lines.push("");
lines.push("Reply with command name to execute");
return lines.join("\n");
}
async function handleContextCommand() {
const data = await getFrameDigestData();
if (!data) {
return "No context available. Start a task first.";
}
const options = loadSyncOptions();
return generateMobileDigest(data, options);
}
async function handleSyncCommand() {
const result = await syncContext();
if (result.success) {
return `Context synced (${result.digestLength} chars)`;
} else {
return `Sync failed: ${result.error}`;
}
}
async function handleStatusCommand() {
try {
const data = await getFrameDigestData();
if (!data) {
return "No active session. Start with: claude-sm";
}
const lines = [];
lines.push(`Frame: ${data.name || data.frameId}`);
lines.push(`Status: ${data.status}`);
lines.push(`Files: ${data.filesModified?.length || 0} modified`);
lines.push(`Tools: ${data.toolCallCount || 0} calls`);
if (data.errors?.length > 0) {
const unresolved = data.errors.filter((e) => !e.resolved).length;
if (unresolved > 0) lines.push(`Errors: ${unresolved} unresolved`);
}
lines.push(`Duration: ${Math.round(data.durationSeconds / 60)}min`);
return lines.join("\n");
} catch {
return "Status unavailable";
}
}
async function handleTasksCommand() {
try {
const data = await getFrameDigestData();
if (!data) {
return "No active tasks";
}
const lines = [];
if (data.decisions?.length > 0) {
lines.push("Recent decisions:");
data.decisions.slice(0, 3).forEach((d, i) => {
lines.push(
`${i + 1}. ${d.substring(0, 50)}${d.length > 50 ? "..." : ""}`
);
});
}
if (data.risks?.length > 0) {
lines.push("");
lines.push("Risks:");
data.risks.slice(0, 2).forEach((r) => {
lines.push(`- ${r.substring(0, 50)}${r.length > 50 ? "..." : ""}`);
});
}
if (lines.length === 0) {
return "No active tasks or decisions";
}
return lines.join("\n");
} catch {
return "Tasks unavailable";
}
}
async function handleRemoteCommand(prompt) {
try {
const sanitizedPrompt = prompt.replace(/[`$\\]/g, "").replace(/["']/g, "'").substring(0, 500);
if (!sanitizedPrompt.trim()) {
return "Please provide a task prompt. Usage: remote <your task>";
}
console.log(
`[whatsapp-commands] Launching remote session: ${sanitizedPrompt.substring(0, 50)}...`
);
const output = execFileSync("claude", ["--remote", sanitizedPrompt], {
encoding: "utf8",
timeout: 3e4,
stdio: ["pipe", "pipe", "pipe"]
});
const urlMatch = output.match(
/https:\/\/claude\.ai\/code\/session_[a-zA-Z0-9]+/
);
if (urlMatch) {
const sessionUrl = urlMatch[0];
const sessionId = sessionUrl.split("/").pop() || "unknown";
addRemoteSession({
id: sessionId,
url: sessionUrl,
prompt: sanitizedPrompt,
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
status: "active"
});
return `Remote session launched!
${sessionUrl}
Task: ${sanitizedPrompt.substring(0, 100)}`;
}
return `Session launched:
${output.substring(0, 300)}`;
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
console.error(`[whatsapp-commands] Remote launch failed: ${error}`);
return `Failed to launch remote session: ${error.substring(0, 100)}`;
}
}
function handleSessionsCommand() {
const sessions = getActiveRemoteSessions();
if (sessions.length === 0) {
return "No active remote sessions";
}
const lines = ["Active remote sessions:"];
sessions.slice(0, 5).forEach((s, i) => {
const age = Math.round(
(Date.now() - new Date(s.createdAt).getTime()) / 6e4
);
const ageStr = age < 60 ? `${age}m ago` : `${Math.round(age / 60)}h ago`;
lines.push(`${i + 1}. ${s.prompt.substring(0, 40)}... (${ageStr})`);
lines.push(` ${s.url}`);
});
return lines.join("\n");
}
async function processCommand(from, message) {
const config = loadCommandsConfig();
if (!config.enabled) {
return { handled: false };
}
const parsed = parseCommand(message);
if (!parsed) {
return { handled: false };
}
const command = config.commands.find(
(cmd) => cmd.enabled && cmd.name.toLowerCase() === parsed.name
);
if (!command) {
return { handled: false };
}
if (command.name === "help") {
const helpText = generateHelpText(config);
return { handled: true, response: helpText };
}
if (command.name === "context") {
const contextText = await handleContextCommand();
return { handled: true, response: contextText };
}
if (command.name === "sync") {
const syncText = await handleSyncCommand();
return { handled: true, response: syncText };
}
if (command.name === "status") {
const statusText = await handleStatusCommand();
return { handled: true, response: statusText };
}
if (command.name === "tasks") {
const tasksText = await handleTasksCommand();
return { handled: true, response: tasksText };
}
if (command.name === "remote") {
if (!parsed.arg) {
return {
handled: true,
response: "Usage: remote <task prompt>\nExample: remote Fix the login bug",
error: "Missing prompt"
};
}
const remoteText = await handleRemoteCommand(parsed.arg);
return { handled: true, response: remoteText };
}
if (command.name === "sessions") {
const sessionsText = handleSessionsCommand();
return { handled: true, response: sessionsText };
}
if (command.requiresArg && !parsed.arg) {
return {
handled: true,
response: `${command.name} requires an argument. Usage: ${command.name} <arg>`,
error: "Missing argument"
};
}
if (command.argPattern && parsed.arg) {
if (!safeRegexTest(command.argPattern, parsed.arg)) {
return {
handled: true,
response: `Invalid argument format for ${command.name}`,
error: "Invalid argument format"
};
}
}
let action = command.action;
if (action && parsed.arg) {
if (command.name === "approve") {
action = `gh pr review ${parsed.arg} --approve`;
} else if (command.name === "merge") {
action = `gh pr merge ${parsed.arg} --squash`;
}
}
if (action) {
console.log(`[whatsapp-commands] Executing: ${action}`);
const result = await executeActionSafe(action, message);
if (result.success) {
const output = result.output?.slice(0, 200) || "Done";
return {
handled: true,
response: `${command.name}: ${output}`,
action
};
} else {
return {
handled: true,
response: `${command.name} failed: ${result.error?.slice(0, 100)}`,
error: result.error,
action
};
}
}
return {
handled: true,
response: `Command ${command.name} acknowledged`
};
}
async function sendCommandResponse(response) {
const result = await sendNotification({
type: "custom",
title: "Command Result",
message: response
});
return { success: result.success, error: result.error };
}
function enableCommands() {
const config = loadCommandsConfig();
config.enabled = true;
saveCommandsConfig(config);
}
function disableCommands() {
const config = loadCommandsConfig();
config.enabled = false;
saveCommandsConfig(config);
}
function isCommandsEnabled() {
const config = loadCommandsConfig();
return config.enabled;
}
function addCommand(command) {
const config = loadCommandsConfig();
const existingIndex = config.commands.findIndex(
(c) => c.name.toLowerCase() === command.name.toLowerCase()
);
if (existingIndex >= 0) {
config.commands[existingIndex] = command;
} else {
config.commands.push(command);
}
saveCommandsConfig(config);
}
function removeCommand(name) {
const config = loadCommandsConfig();
const initialLength = config.commands.length;
config.commands = config.commands.filter(
(c) => c.name.toLowerCase() !== name.toLowerCase()
);
if (config.commands.length < initialLength) {
saveCommandsConfig(config);
return true;
}
return false;
}
function getAvailableCommands() {
const config = loadCommandsConfig();
return config.commands.filter((c) => c.enabled);
}
export {
addCommand,
disableCommands,
enableCommands,
getActiveRemoteSessions,
getAvailableCommands,
getRemoteSessions,
isCommand,
isCommandsEnabled,
loadCommandsConfig,
processCommand,
removeCommand,
saveCommandsConfig,
sendCommandResponse
};
//# sourceMappingURL=whatsapp-commands.js.map