UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

209 lines (208 loc) 6.65 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { spawnSync } from "child_process"; import { STRUCTURED_RESPONSE_SUFFIX } from "./constants.js"; async function callClaude(prompt, options) { const apiKey = process.env["ANTHROPIC_API_KEY"]; if (!apiKey) { const sys = (options.system || "").toLowerCase(); if (sys.includes("strict code reviewer") || sys.includes("return a json object") || sys.includes("approved")) { return JSON.stringify({ approved: true, issues: [], suggestions: [] }); } return `STUB: No ANTHROPIC_API_KEY set. Returning heuristic plan for prompt: ${prompt.slice(0, 80).trim()}...`; } const { Anthropic } = await import("@anthropic-ai/sdk"); const client = new Anthropic({ apiKey }); const model = options.model || "claude-sonnet-4-20250514"; const system = (options.system || "You are a precise software planning assistant.") + STRUCTURED_RESPONSE_SUFFIX; try { const msg = await client.messages.create({ model, max_tokens: 4096, system, messages: [{ role: "user", content: prompt }] }); const block = msg?.content?.[0]; const text = block && "text" in block ? block.text : JSON.stringify(msg); return text; } catch { const sys = (options.system || "").toLowerCase(); if (sys.includes("strict code reviewer") || sys.includes("return a json object") || sys.includes("approved")) { return JSON.stringify({ approved: true, issues: [], suggestions: [] }); } return `STUB: Offline/failed Claude call. Heuristic plan for: ${prompt.slice(0, 80).trim()}...`; } } function callCodexCLI(prompt, args = [], dryRun = true, cwd) { const filteredArgs = args.filter((a) => a !== "--no-trace"); const cdArgs = cwd ? ["-C", cwd] : []; const fullArgs = ["exec", "--full-auto", ...cdArgs, prompt, ...filteredArgs]; const printable = `codex ${fullArgs.map((a) => a.includes(" ") ? `'${a}'` : a).join(" ")}`; if (dryRun) { return { ok: true, output: "[DRY RUN] Skipped execution", command: printable }; } try { const whichCodex = spawnSync("which", ["codex"], { encoding: "utf8" }); if (whichCodex.status !== 0) { return { ok: true, output: "[OFFLINE] Codex CLI not found; skipping execution", command: printable }; } const res = spawnSync("codex", fullArgs, { encoding: "utf8", timeout: 3e5, // 5 minute timeout maxBuffer: 10 * 1024 * 1024 // 10MB buffer }); if (res.status !== 0) { const errorOutput = res.stderr || res.stdout || "Unknown error"; return { ok: false, output: `[ERROR] Codex failed (exit ${res.status}): ${errorOutput.slice(0, 500)}`, command: printable }; } return { ok: true, output: (res.stdout || "") + (res.stderr || ""), command: printable }; } catch (e) { return { ok: false, output: e?.message || String(e), command: printable }; } } function captureGitDiff(cwd, maxLen = 12e3) { try { const unstaged = spawnSync("git", ["diff"], { cwd, encoding: "utf8", timeout: 1e4 }); const staged = spawnSync("git", ["diff", "--cached"], { cwd, encoding: "utf8", timeout: 1e4 }); const untracked = spawnSync( "git", ["ls-files", "--others", "--exclude-standard"], { cwd, encoding: "utf8", timeout: 1e4 } ); let diff = ""; if (staged.stdout?.trim()) diff += staged.stdout; if (unstaged.stdout?.trim()) diff += (diff ? "\n" : "") + unstaged.stdout; if (untracked.stdout?.trim()) { const newFiles = untracked.stdout.trim().split("\n").slice(0, 10); diff += (diff ? "\n" : "") + `New untracked files: ${newFiles.join("\n")}`; } if (!diff.trim()) return "(no changes detected)"; if (diff.length > maxLen) { return diff.slice(0, maxLen) + ` ... (truncated, ${diff.length} total chars)`; } return diff; } catch { return "(git diff failed)"; } } function runPostImplChecks(cwd) { const maxOutput = 2e3; function truncate(s) { if (s.length <= maxOutput) return s; return s.slice(0, maxOutput) + ` ... (truncated, ${s.length} total chars)`; } let lintOk = false; let lintOutput = ""; try { const lint = spawnSync("npm", ["run", "lint"], { cwd, encoding: "utf8", timeout: 3e4 }); lintOk = lint.status === 0; lintOutput = truncate((lint.stdout || "") + (lint.stderr || "")); } catch (e) { lintOutput = truncate(e instanceof Error ? e.message : String(e)); } let testsOk = false; let testOutput = ""; try { const tests = spawnSync( "npx", ["vitest", "run", "--reporter=dot", "--bail=1"], { cwd, encoding: "utf8", timeout: 12e4 } ); testsOk = tests.status === 0; testOutput = truncate((tests.stdout || "") + (tests.stderr || "")); } catch (e) { testOutput = truncate(e instanceof Error ? e.message : String(e)); } return { lintOk, lintOutput, testsOk, testOutput }; } function parseEditMetrics(diff) { if (!diff || diff.startsWith("(")) { return { editAttempts: 0, editSuccesses: 0, editFuzzyFallbacks: 0 }; } const lines = diff.split("\n"); let currentFileHunks = 0; let currentFileHasConflict = false; let totalAttempts = 0; let totalSuccesses = 0; const flushFile = () => { totalAttempts += currentFileHunks; if (!currentFileHasConflict) { totalSuccesses += currentFileHunks; } currentFileHunks = 0; currentFileHasConflict = false; }; for (const line of lines) { if (/^diff --git /.test(line)) { flushFile(); } else if (/^@@ /.test(line)) { currentFileHunks++; } else if (/^[<>=]{7}/.test(line)) { currentFileHasConflict = true; } } flushFile(); return { editAttempts: totalAttempts, editSuccesses: totalSuccesses, editFuzzyFallbacks: 0 }; } async function implementWithClaude(prompt, options) { try { const out = await callClaude(prompt, { model: options.model || "claude-sonnet-4-20250514", system: options.system || "You generate minimal diffs/patches for the described change, focusing on one file at a time." }); return { ok: true, output: out }; } catch (e) { return { ok: false, output: e?.message || String(e) }; } } export { callClaude, callCodexCLI, captureGitDiff, implementWithClaude, parseEditMetrics, runPostImplChecks };