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.

182 lines (181 loc) 5.3 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { join } from "path"; import { writeFileSync, existsSync, mkdirSync } from "fs"; import { execSync } from "child_process"; import { SweepStateWatcher } from "./state-watcher.js"; import { StatusBar } from "./status-bar.js"; import { TabInterceptor } from "./tab-interceptor.js"; const HOME = process.env["HOME"] || "/tmp"; function getSweepDir() { return process.env["SWEEP_STATE_DIR"] || join(HOME, ".stackmemory"); } function getSweepPath(filename) { return join(getSweepDir(), filename); } const ALT_SCREEN_ENTER = "\x1B[?1049h"; const ALT_SCREEN_EXIT = "\x1B[?1049l"; class PtyWrapper { config; stateWatcher; statusBar; tabInterceptor; currentPrediction = null; inAltScreen = false; ptyProcess = null; constructor(config = {}) { this.config = { claudeBin: config.claudeBin || this.findClaude(), claudeArgs: config.claudeArgs || [], stateFile: config.stateFile || getSweepPath("sweep-state.json") }; this.stateWatcher = new SweepStateWatcher(this.config.stateFile); this.statusBar = new StatusBar(); this.tabInterceptor = new TabInterceptor({ onAccept: () => this.acceptPrediction(), onDismiss: () => this.dismissPrediction(), onPassthrough: (data) => this.ptyProcess?.write(data.toString("utf-8")) }); } async start() { const sweepDir = getSweepDir(); if (!existsSync(sweepDir)) { mkdirSync(sweepDir, { recursive: true }); } let pty; try { pty = await import("node-pty"); } catch { throw new Error( "node-pty is required for the PTY wrapper.\nInstall with: npm install node-pty" ); } const cols = process.stdout.columns || 80; const rows = process.stdout.rows || 24; const env = {}; for (const [k, v] of Object.entries(process.env)) { if (v !== void 0) env[k] = v; } this.ptyProcess = pty.spawn(this.config.claudeBin, this.config.claudeArgs, { name: process.env["TERM"] || "xterm-256color", cols, rows: rows - 1, cwd: process.cwd(), env }); if (process.stdin.isTTY) { process.stdin.setRawMode(true); } process.stdin.resume(); this.ptyProcess.onData((data) => { if (data.includes(ALT_SCREEN_ENTER)) { this.inAltScreen = true; this.statusBar.hide(); } if (data.includes(ALT_SCREEN_EXIT)) { this.inAltScreen = false; } process.stdout.write(data); }); process.stdin.on("data", (data) => { this.tabInterceptor.process(data); }); this.stateWatcher.on("loading", () => { if (!this.inAltScreen) { this.statusBar.showLoading(); } }); this.stateWatcher.on("prediction", (event) => { this.currentPrediction = event; this.tabInterceptor.setPredictionActive(true); if (!this.inAltScreen) { this.statusBar.show( event.prediction, event.file_path, event.latency_ms ); } }); this.stateWatcher.start(); process.stdout.on("resize", () => { const newCols = process.stdout.columns || 80; const newRows = process.stdout.rows || 24; this.ptyProcess?.resize(newCols, newRows - 1); this.statusBar.resize(newRows, newCols); }); this.ptyProcess.onExit(({ exitCode }) => { this.cleanup(); process.exit(exitCode); }); const onSignal = () => { this.cleanup(); process.exit(0); }; process.on("SIGINT", onSignal); process.on("SIGTERM", onSignal); } acceptPrediction() { if (!this.currentPrediction || !this.ptyProcess) return; const dir = getSweepDir(); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } const pendingFile = getSweepPath("sweep-pending.json"); writeFileSync( pendingFile, JSON.stringify( { file_path: this.currentPrediction.file_path, predicted_content: this.currentPrediction.prediction, timestamp: Date.now() }, null, 2 ) ); const prompt = `Apply the Sweep prediction from ${pendingFile} `; this.ptyProcess.write(prompt); this.dismissPrediction(); } dismissPrediction() { this.currentPrediction = null; this.tabInterceptor.setPredictionActive(false); this.statusBar.hide(); } cleanup() { this.stateWatcher.stop(); this.statusBar.hide(); if (process.stdin.isTTY) { process.stdin.setRawMode(false); } process.stdin.pause(); } findClaude() { try { const resolved = execSync("which claude", { encoding: "utf-8" }).trim(); if (resolved) return resolved; } catch { } const candidates = [ join(HOME, ".bun", "bin", "claude"), "/usr/local/bin/claude", "/opt/homebrew/bin/claude" ]; for (const c of candidates) { if (existsSync(c)) return c; } return "claude"; } } async function launchWrapper(config) { const wrapper = new PtyWrapper(config); await wrapper.start(); } export { PtyWrapper, launchWrapper }; //# sourceMappingURL=pty-wrapper.js.map