/** * System Info Extension * * Injects host environment details into the system prompt: * - OS (Windows / Linux / macOS) * - Terminal / shell environment * - Local IPv4 addresses * - Common programming language toolchains * * Auto-discovered from .sproutclaw/agent/extensions/system-info/ */ import { spawnSync } from "node:child_process"; import os from "node:os"; import { basename } from "node:path"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; interface ToolchainEntry { label: string; version: string; path: string; } interface SystemInfoSnapshot { os: string; platform: string; arch: string; hostname: string; terminal: string; shell: string; ipAddresses: string[]; toolchains: ToolchainEntry[]; capturedAt: string; } const NULL = "NULL"; interface ToolchainProbe { label: string; bins: string[]; args: string[]; } const TOOLCHAIN_PROBES: ToolchainProbe[] = [ { label: "Python", bins: ["python3", "python"], args: ["--version"] }, { label: "Java", bins: ["java"], args: ["-version"] }, { label: "Node.js", bins: ["node"], args: ["--version"] }, { label: "Bun", bins: ["bun"], args: ["--version"] }, { label: "Rust", bins: ["rustc"], args: ["--version"] }, { label: "Clang", bins: ["clang"], args: ["--version"] }, { label: "Golang", bins: ["go"], args: ["version"] }, { label: "GCC", bins: ["gcc"], args: ["--version"] }, ]; function isIPv4(family: string | number): boolean { return family === "IPv4" || family === 4; } function getOsLabel(): string { switch (process.platform) { case "win32": return "Windows"; case "linux": return "Linux"; case "darwin": return "macOS"; default: return process.platform; } } function describeShell(): string { if (process.platform === "win32") { if (process.env.PSModulePath || /powershell/i.test(process.env.SHELL ?? "")) { return "PowerShell"; } const comspec = process.env.ComSpec; return comspec ? basename(comspec) : "unknown"; } const shell = process.env.SHELL; return shell ? basename(shell) : "unknown"; } function describeTerminal(): string { const parts: string[] = []; if (process.env.TERM_PROGRAM) { const version = process.env.TERM_PROGRAM_VERSION; parts.push(version ? `${process.env.TERM_PROGRAM} ${version}` : process.env.TERM_PROGRAM); } else if (process.env.WT_SESSION) { parts.push("Windows Terminal"); } else if (process.env.CURSOR_TRACE_ID || process.env.CURSOR_AGENT) { parts.push("Cursor"); } else if (process.env.VSCODE_PID || process.env.TERM_PROGRAM === "vscode") { parts.push("VS Code Integrated Terminal"); } else if (process.env.TERM) { parts.push(`TERM=${process.env.TERM}`); } const shell = describeShell(); if (shell !== "unknown") { parts.push(`shell=${shell}`); } return parts.length > 0 ? parts.join(", ") : "unknown"; } function getLocalIpAddresses(): string[] { const addresses: string[] = []; for (const [interfaceName, entries] of Object.entries(os.networkInterfaces())) { if (!entries) continue; for (const entry of entries) { if (entry.internal || !isIPv4(entry.family)) continue; addresses.push(`${interfaceName}: ${entry.address}`); } } return addresses; } function firstNonEmptyLine(text: string): string { return ( text .split(/\r?\n/) .map((line) => line.trim()) .find((line) => line.length > 0) ?? "" ); } function findExecutable(candidates: string[]): string | null { for (const candidate of candidates) { if (process.platform === "win32") { const result = spawnSync("where.exe", [candidate], { encoding: "utf8", timeout: 5000 }); const line = firstNonEmptyLine(result.stdout ?? ""); if (result.status === 0 && line) { return line; } continue; } const result = spawnSync("sh", ["-lc", `command -v ${candidate} 2>/dev/null`], { encoding: "utf8", timeout: 5000, }); const line = firstNonEmptyLine(result.stdout ?? ""); if (result.status === 0 && line) { return line; } } return null; } function runVersionCommand(executable: string, args: string[]): string { const result = spawnSync(executable, args, { encoding: "utf8", timeout: 5000, windowsHide: true, }); const output = firstNonEmptyLine(`${result.stdout ?? ""}\n${result.stderr ?? ""}`); return output; } function probeToolchain(probe: ToolchainProbe): ToolchainEntry { const executable = findExecutable(probe.bins); if (!executable) { return { label: probe.label, version: NULL, path: NULL }; } const version = runVersionCommand(executable, probe.args); return { label: probe.label, version: version || NULL, path: executable, }; } function collectToolchains(): ToolchainEntry[] { return TOOLCHAIN_PROBES.map((probe) => probeToolchain(probe)); } function collectSystemInfo(): SystemInfoSnapshot { return { os: getOsLabel(), platform: process.platform, arch: process.arch, hostname: os.hostname(), terminal: describeTerminal(), shell: describeShell(), ipAddresses: getLocalIpAddresses(), toolchains: collectToolchains(), capturedAt: new Date().toISOString(), }; } function formatToolchainLine(entry: ToolchainEntry): string { if (entry.version === NULL && entry.path === NULL) { return `- ${entry.label}: version=${NULL}, path=${NULL}`; } return `- ${entry.label}: version=${entry.version}, path=${entry.path}`; } function formatSystemInfoSection(info: SystemInfoSnapshot): string { const ipLines = info.ipAddresses.length > 0 ? info.ipAddresses.map((address) => `- ${address}`).join("\n") : "- (no non-loopback IPv4 address found)"; const toolchainLines = info.toolchains.map((entry) => formatToolchainLine(entry)).join("\n"); return ` ## Host Environment The following reflects the machine where SproutClaw is running. Use it when choosing commands, paths, or network assumptions. - OS: ${info.os} (${info.platform}/${info.arch}) - Hostname: ${info.hostname} - Terminal: ${info.terminal} - Shell: ${info.shell} - Local IPv4: ${ipLines} - Toolchains: ${toolchainLines} - Captured at: ${info.capturedAt} `.trimEnd(); } function formatSummary(info: SystemInfoSnapshot): string { const ips = info.ipAddresses.length > 0 ? info.ipAddresses.join(", ") : "none"; const toolchainSummary = info.toolchains .map((entry) => { if (entry.version === NULL) { return `${entry.label}: ${NULL}`; } return `${entry.label}: ${entry.version}`; }) .join("\n"); return [ `OS: ${info.os}`, `Terminal: ${info.terminal}`, `Shell: ${info.shell}`, `IPv4: ${ips}`, "", toolchainSummary, ].join("\n"); } export default function systemInfoExtension(pi: ExtensionAPI) { let systemInfo = collectSystemInfo(); function refreshSystemInfo(): SystemInfoSnapshot { systemInfo = collectSystemInfo(); return systemInfo; } pi.on("session_start", async () => { refreshSystemInfo(); }); pi.on("before_agent_start", async (event) => { const section = formatSystemInfoSection(systemInfo); if (event.systemPrompt.includes("## Host Environment")) { return undefined; } return { systemPrompt: `${event.systemPrompt}\n\n${section}`, }; }); pi.registerCommand("system-info", { description: "Show or refresh injected host environment details", handler: async (_args, ctx) => { const info = refreshSystemInfo(); ctx.ui.notify(formatSummary(info), "info"); }, }); }