import * as fs from "node:fs"; import * as path from "node:path"; import { execFile } from "node:child_process"; import { BASE, TRUNC } from "./constants"; export function truncate(s: string, n = TRUNC): string { return s.length > n ? s.slice(0, n) + `\n… [truncated ${s.length - n} chars]` : s; } /** Strip envelope markers from peer-supplied text so it cannot forge provenance boundaries. */ export function sanitizeInbound(s: unknown): string { return String(s ?? "").replace(/\[\[\/?AGENT-COMM\]\]/g, "[stripped-agent-comm-marker]"); } export function pidAlive(pid: number): boolean { try { process.kill(pid, 0); return true; } catch { return false; } } export function atomicWriteJson(file: string, data: unknown) { const tmp = `${file}.tmp-${process.pid}`; fs.writeFileSync(tmp, JSON.stringify(data, null, 1)); fs.renameSync(tmp, file); } export function git(cwd: string, args: string[]): Promise { return new Promise((resolve) => { execFile("git", args, { cwd, timeout: 4000, maxBuffer: 2 ** 22 }, (err, stdout) => resolve(err ? "" : stdout.toString().trimEnd()), ); }); } /** Absolute git common dir — identical for every worktree of the same repo. */ export async function commonGitDir(cwd: string): Promise { const out = await git(cwd, ["rev-parse", "--git-common-dir"]); return out ? path.resolve(cwd, out) : ""; } export function kebab(s: string, maxLen = 48): string { return s .toLowerCase() .replace(/[`"'.,:;!?()[\]{}<>]/g, " ") .split(/\s+/) .filter(Boolean) .join("-") .replace(/[^a-z0-9-]/g, "") .replace(/-+/g, "-") .replace(/^-|-$/g, "") .slice(0, maxLen) .replace(/-$/g, ""); } const STOPWORDS = new Set( "a an the and or but so of in on at to for with from by is are was were be been being do does did can could should would will i you we they it this that these those my your our their me us them please help lets let's want need make just".split( " ", ), ); export function slugFromPrompt(prompt: string): string { const words = prompt .toLowerCase() .replace(/[^a-z0-9\s._/-]/g, " ") .split(/\s+/) .filter((w) => w.length > 1 && !STOPWORDS.has(w)) .slice(0, 5); return kebab(words.join(" ")); } /** Debug log, opt-in via PI_AGENT_COMM_DEBUG=1. Never throws. */ export function dbg(...parts: unknown[]) { if (!process.env.PI_AGENT_COMM_DEBUG) return; try { fs.mkdirSync(BASE, { recursive: true, mode: 0o700 }); fs.appendFileSync( path.join(BASE, "debug.log"), `${new Date().toISOString()} [${process.pid}] ${parts.map((p) => (typeof p === "string" ? p : JSON.stringify(p))).join(" ")}\n`, ); } catch {} }