feat: 导出 SproutClaw .sproutclaw 配置
包含 extensions、skills、prompts、settings、auth、models、mcp 等配置。 排除 node_modules、npm 缓存、sessions 等运行时数据。
This commit is contained in:
281
agent/extensions/system-info/index.ts
Normal file
281
agent/extensions/system-info/index.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* 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");
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user