UNPKG

pi-lens

Version:

Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo

85 lines (84 loc) • 3.46 kB
/** * Warm side-channel for the push path. The MCP server is a long-lived process * with a warm LSP, but its stdio is owned by the MCP client — so the * PostToolUse-hook bin can't reach it that way. Instead the server listens on a * local IPC endpoint (Unix domain socket / Windows named pipe), and the hook * connects to it to get LSP-complete diagnostics from the warm process instead * of running its own cold analysis. * * This module is the CLIENT + the shared path derivation only — deliberately * light (node:net + type-only result), so the bin can try the warm path WITHOUT * loading the dispatch graph. The server side lives in mcp/server.ts (which * already holds the analysis engine). If the warm path is unavailable, the * client resolves `undefined` and the caller falls back to cold local analysis. */ import * as crypto from "node:crypto"; import * as net from "node:net"; import * as os from "node:os"; import * as path from "node:path"; /** * Stable per-workspace endpoint path. The server (from its launch cwd) and the * hook (from the PostToolUse cwd) must resolve the same path — both hash the * resolved root (lowercased for case-insensitive filesystems), so when they're * the same project they meet. Mismatch → the client just falls back to cold. */ export function ipcPathForCwd(cwd) { const root = path.resolve(cwd).toLowerCase(); // sha256 (not for security — just a stable short id for the IPC socket/pipe // name keyed by cwd; sha256 over sha1 keeps SonarCloud's weak-hash check quiet) const hash = crypto .createHash("sha256") .update(root) .digest("hex") .slice(0, 16); if (process.platform === "win32") { return `\\\\.\\pipe\\pi-lens-mcp-${hash}`; } return path.join(os.tmpdir(), `pi-lens-mcp-${hash}.sock`); } /** * Ask the warm server to analyze a file. Resolves the server's result, or * `undefined` on ANY failure (no server, refused, stale socket, timeout, bad * response) so the caller transparently falls back to cold local analysis. */ export function requestWarmAnalyze(cwd, file, timeoutMs = 30_000) { return new Promise((resolve) => { let settled = false; const finish = (value) => { if (settled) return; settled = true; clearTimeout(timer); resolve(value); }; const socket = net.createConnection(ipcPathForCwd(cwd)); socket.setEncoding("utf8"); let buffer = ""; const timer = setTimeout(() => { socket.destroy(); finish(undefined); }, timeoutMs); timer.unref(); socket.on("connect", () => { const request = { file, cwd }; socket.write(`${JSON.stringify(request)}\n`); }); socket.on("data", (chunk) => { buffer += chunk; const newline = buffer.indexOf("\n"); if (newline === -1) return; try { const message = JSON.parse(buffer.slice(0, newline)); finish(message.error ? undefined : message.result); } catch { finish(undefined); } socket.end(); }); // No server / connection refused / reset → cold fallback. socket.on("error", () => finish(undefined)); socket.on("close", () => finish(undefined)); }); }