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