包含 extensions、skills、prompts、settings、auth、models、mcp 等配置。 排除 node_modules、npm 缓存、sessions 等运行时数据。
282 lines
7.2 KiB
TypeScript
282 lines
7.2 KiB
TypeScript
/**
|
|
* 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");
|
|
},
|
|
});
|
|
}
|