Files
shumengya 50edff80f5 feat: 导出 SproutClaw .sproutclaw 配置
包含 extensions、skills、prompts、settings、auth、models、mcp 等配置。
排除 node_modules、npm 缓存、sessions 等运行时数据。
2026-06-26 15:48:56 +08:00

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");
},
});
}