import { execFile } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { arch, platform, release } from "node:os"; import { promisify } from "node:util"; import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { registerPromptSegment } from "../../../vera-prompt-inspector/src/registry"; const execFileAsync = promisify(execFile); // The OS line and the parent-shell identity are cached per session. // cwd is re-read on every before_agent_start so a session that changes // directory still reflects accurately. let cachedOsLine: string | null = null; let cachedParentShell: string | null = null; function formatBlock(osLine: string, parentShell: string, cwd: string): string { const shellLine = platform() === "win32" ? "prefer `pwsh` on this Windows host (`bash` routes through Git Bash with MSYS path translation)" : "prefer `bash` on this host"; return [ "System environment:", ` OS: ${osLine}`, ` Kernel: ${release()}`, ` Arch: ${arch()}`, ` Node: ${process.version}`, ` Shell tool: ${shellLine}`, ` Parent shell: ${parentShell}`, ` Current working directory: ${cwd}`, ].join("\n"); } function fallbackOsLine(): string { const p = platform(); if (p === "win32") return `Windows (kernel ${release()})`; if (p === "darwin") return `macOS (Darwin ${release()})`; if (p === "linux") return `Linux (kernel ${release()})`; return `${p} ${release()}`; } function detectLinux(): string { for (const path of ["/etc/os-release", "/etc/lsb-release"]) { if (!existsSync(path)) continue; try { const text = readFileSync(path, "utf8"); const match = text.match(/^PRETTY_NAME="?([^"\n]+)"?/m) ?? text.match(/^DISTRIB_DESCRIPTION="?([^"\n]+)"?/m); if (match) return match[1].trim(); } catch { // fall through to next candidate } } if (existsSync("/etc/system-release")) { try { return readFileSync("/etc/system-release", "utf8").trim(); } catch { // fall through } } return fallbackOsLine(); } async function detectMacOS(): Promise { try { const [name, version, build] = await Promise.all([ execFileAsync("sw_vers", ["-productName"], { timeout: 3000 }).then((r) => r.stdout.trim()), execFileAsync("sw_vers", ["-productVersion"], { timeout: 3000 }).then((r) => r.stdout.trim()), execFileAsync("sw_vers", ["-buildVersion"], { timeout: 3000 }).then((r) => r.stdout.trim()), ]); if (name && version) { return build ? `${name} ${version} (${build})` : `${name} ${version}`; } } catch { // fall through } return fallbackOsLine(); } async function detectWindows(): Promise { try { // `reg query` only accepts a single `/v`; query the whole key once and parse out // the values we care about. Output size is ~30 lines, parsing is cheap. const { stdout } = await execFileAsync( "reg", ["query", "HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"], { timeout: 3000 }, ); const pick = (key: string): string | null => { const re = new RegExp(`^\\s+${key}\\s+REG_\\w+\\s+(.+?)\\s*$`, "m"); return stdout.match(re)?.[1]?.trim() ?? null; }; let productName = pick("ProductName") ?? "Windows"; const display = pick("DisplayVersion"); const build = pick("CurrentBuild"); const ubrRaw = pick("UBR"); // Microsoft never updated ProductName for Windows 11; detect by build >= 22000. const buildNum = build ? parseInt(build, 10) : NaN; if (Number.isFinite(buildNum) && buildNum >= 22000 && productName.startsWith("Windows 10")) { productName = productName.replace("Windows 10", "Windows 11"); } // UBR is REG_DWORD, reg.exe prints it as 0xNNNN; Number() handles the hex prefix. const ubrNum = ubrRaw ? Number(ubrRaw) : NaN; const buildSuffix = build ? Number.isFinite(ubrNum) ? `${build}.${ubrNum}` : build : null; const parts = [productName]; if (display) parts.push(display); if (buildSuffix) parts.push(`(build ${buildSuffix})`); return parts.join(" "); } catch { // fall through } return fallbackOsLine(); } /** * Identify the immediate parent process of this Node runtime — i.e. the * shell the user used to launch pi. This is informational only: when pi * is launched through wrappers (npm scripts, nvm shims) the parent may * be that wrapper rather than the user's interactive shell. We do not * try to walk further up the process tree. * * Falls back to env-var heuristics if the subprocess query fails. */ async function detectParentShell(): Promise { const ppid = process.ppid; if (!ppid) return "(unknown)"; try { if (platform() === "win32") { // tasklist is markedly faster than wmic. CSV format: "name","pid",... const { stdout } = await execFileAsync( "tasklist", ["/FI", `PID eq ${ppid}`, "/FO", "CSV", "/NH"], { timeout: 2000 }, ); const m = stdout.match(/^"([^"]+)"/m); if (m) return `${m[1]} (pid ${ppid})`; } else { const { stdout } = await execFileAsync( "ps", ["-p", String(ppid), "-o", "comm="], { timeout: 2000 }, ); const name = stdout.trim(); if (name) return `${name} (pid ${ppid})`; } } catch { // fall through to heuristic fallback } // Heuristic fallback from environment variables. const env = process.env; if (env.MSYSTEM) return `bash (MSYS ${env.MSYSTEM}, pid ${ppid})`; if (env.PSModulePath && env.PSHOME) return `powershell/pwsh (pid ${ppid})`; if (env.SHELL) { const name = env.SHELL.split(/[/\\]/).pop() || env.SHELL; return `${name} (pid ${ppid})`; } return `(unknown, pid ${ppid})`; } async function detectOsLine(): Promise { switch (platform()) { case "linux": return detectLinux(); case "darwin": return await detectMacOS(); case "win32": return await detectWindows(); default: return fallbackOsLine(); } } export default function systemEnv(pi: ExtensionAPI) { pi.on("session_start", async () => { [cachedOsLine, cachedParentShell] = await Promise.all([ detectOsLine(), detectParentShell(), ]); }); pi.on("before_agent_start", async (_event, ctx) => { if (!cachedOsLine) cachedOsLine = await detectOsLine(); if (!cachedParentShell) cachedParentShell = await detectParentShell(); const block = formatBlock(cachedOsLine, cachedParentShell, ctx.cwd ?? ""); registerPromptSegment({ id: "system-env", label: "system env", category: "context", kind: "cached-per-session", text: block, details: [ { label: "cwd", value: String(ctx.cwd ?? "(unknown)") }, { label: "shell", value: cachedParentShell }, ], }); return undefined; }); }