/** * Minimal Herdr socket client — one request, one reply, socket closed. * * Only what this extension needs (cell geometry and pane lookups). Frame * streaming is not used: images here are static, so they go through the * terminal's own graphics protocol rather than herdr's video path. */ import { connect } from "node:net"; import { homedir } from "node:os"; import { join } from "node:path"; export function herdrSocketPath(): string { const explicit = process.env.HERDR_SOCKET_PATH?.trim(); if (explicit) return explicit; const session = process.env.HERDR_SESSION?.trim(); const base = join(homedir(), ".config", "herdr"); return session ? join(base, "sessions", session, "herdr.sock") : join(base, "herdr.sock"); } export function isHerdrPane(): boolean { return process.env.HERDR_ENV === "1" && Boolean(process.env.HERDR_PANE_ID); } interface Envelope { result?: Record; error?: { code?: string; message?: string }; } export function herdrRequest( method: string, params: Record, timeoutMs = 3_000, ): Promise> { return new Promise((resolve, reject) => { const sock = connect(herdrSocketPath()); let buf = ""; let settled = false; const done = (fn: () => void) => { if (settled) return; settled = true; clearTimeout(timer); sock.destroy(); fn(); }; const timer = setTimeout( () => done(() => reject(new Error(`herdr ${method} timed out`))), timeoutMs, ); sock.on("connect", () => { sock.write(`${JSON.stringify({ id: `wl-${method}`, method, params })}\n`); }); sock.on("data", (chunk) => { buf += chunk.toString("utf-8"); const nl = buf.indexOf("\n"); if (nl < 0) return; let envelope: Envelope; try { envelope = JSON.parse(buf.slice(0, nl)) as Envelope; } catch { done(() => reject(new Error(`herdr ${method}: invalid JSON`))); return; } done(() => { if (envelope.error) { reject(new Error(envelope.error.message ?? `herdr ${method} failed`)); return; } resolve(envelope.result ?? {}); }); }); sock.on("error", (err) => done(() => reject(err))); sock.on("close", () => done(() => reject(new Error(`herdr ${method}: closed early`)))); }); }