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

186 lines
5.7 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { pathToFileURL } from "node:url";
const HOME = os.homedir();
function expandHome(input) {
if (input === "~") return HOME;
if (input.startsWith("~/")) return path.resolve(HOME, input.slice(2));
return path.resolve(input);
}
const CONFIG_DIR_NAME = process.env.SPROUTCLAW_CONFIG_DIR?.trim() || ".sproutclaw";
const AGENT_DIR = (process.env.SPROUTCLAW_CODING_AGENT_DIR?.trim() || process.env.PI_CODING_AGENT_DIR?.trim())
? expandHome((process.env.SPROUTCLAW_CODING_AGENT_DIR || process.env.PI_CODING_AGENT_DIR).trim())
: path.join(HOME, CONFIG_DIR_NAME, "agent");
const PI_CONFIG_PATH = path.join(AGENT_DIR, "mcp.json");
const GENERIC_GLOBAL_CONFIG_PATH = path.join(HOME, ".config", "mcp", "mcp.json");
const PROJECT_CONFIG_PATH = path.resolve(process.cwd(), ".mcp.json");
const PROJECT_PI_CONFIG_PATH = path.resolve(process.cwd(), CONFIG_DIR_NAME, "mcp.json");
const IMPORT_PATHS = {
cursor: [path.join(HOME, ".cursor", "mcp.json")],
"claude-code": [
path.join(HOME, ".claude", "mcp.json"),
path.join(HOME, ".claude.json"),
path.join(HOME, ".claude", "claude_desktop_config.json"),
],
"claude-desktop": [path.join(HOME, "Library", "Application Support", "Claude", "claude_desktop_config.json")],
codex: [path.join(HOME, ".codex", "config.json")],
windsurf: [path.join(HOME, ".windsurf", "mcp.json")],
vscode: [path.resolve(process.cwd(), ".vscode", "mcp.json")],
};
function printHelp(log = console.log) {
log("pi-mcp-adapter helper\n");
log("Install the package with:");
log(" pi install npm:pi-mcp-adapter\n");
log("Then optionally run:");
log(" pi-mcp-adapter init Detect host configs and scaffold Pi imports");
log(" pi-mcp-adapter init --dry-run");
}
function readJsonFile(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
}
function loadPiConfig() {
if (!fs.existsSync(PI_CONFIG_PATH)) {
return { mcpServers: {} };
}
const raw = readJsonFile(PI_CONFIG_PATH);
const mcpServers = raw.mcpServers ?? raw["mcp-servers"] ?? {};
if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
throw new Error(`Invalid MCP config at ${PI_CONFIG_PATH}: expected \"mcpServers\" to be an object`);
}
const normalized = { ...raw };
delete normalized["mcp-servers"];
const imports = Array.isArray(raw.imports) ? raw.imports.filter((value) => typeof value === "string") : undefined;
return {
...normalized,
mcpServers,
imports,
};
}
function findAvailableImports() {
const found = [];
for (const [kind, candidates] of Object.entries(IMPORT_PATHS)) {
const existing = candidates.find((candidate) => fs.existsSync(candidate));
if (existing) {
found.push({ kind, path: existing });
}
}
return found;
}
function printDiscovery(log, imports) {
log("Config discovery:\n");
const paths = [
["User-global standard MCP", GENERIC_GLOBAL_CONFIG_PATH],
["Pi global override", PI_CONFIG_PATH],
["Project standard MCP", PROJECT_CONFIG_PATH],
["Project Pi override", PROJECT_PI_CONFIG_PATH],
];
for (const [label, filePath] of paths) {
const prefix = fs.existsSync(filePath) ? "✓" : "-";
log(`${prefix} ${label}: ${filePath}`);
}
log("\nCompatibility imports:\n");
if (imports.length === 0) {
log("- No host-specific MCP configs detected");
return;
}
for (const entry of imports) {
log(`${entry.kind}: ${entry.path}`);
}
}
function writePiConfig(config) {
fs.mkdirSync(path.dirname(PI_CONFIG_PATH), { recursive: true });
fs.writeFileSync(PI_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
}
async function runInit(argv, log = console.log) {
const dryRun = argv.includes("--dry-run");
const foundImports = findAvailableImports();
const existingConfig = loadPiConfig();
const existingImports = new Set(existingConfig.imports ?? []);
const importsToAdd = foundImports
.map((entry) => entry.kind)
.filter((kind) => !existingImports.has(kind));
printDiscovery(log, foundImports);
if (importsToAdd.length === 0) {
log("\nNo Pi config changes needed.");
log("Standard MCP configs are discovered automatically, and host-specific imports are already configured or unavailable.");
return 0;
}
const nextConfig = {
...existingConfig,
imports: [...existingImports, ...importsToAdd],
mcpServers: existingConfig.mcpServers ?? {},
};
log(`\nDetected host configs to import into Pi: ${importsToAdd.join(", ")}`);
if (dryRun) {
log(`Dry run: would update ${PI_CONFIG_PATH}`);
return 0;
}
writePiConfig(nextConfig);
log(`Updated ${PI_CONFIG_PATH}`);
log("Pi will now keep reading standard MCP configs automatically, while these imports cover host-specific config formats.");
return 0;
}
export async function main(argv = process.argv.slice(2), log = console.log, error = console.error) {
const [command, ...rest] = argv;
if (!command || command === "help" || command === "--help" || command === "-h") {
printHelp(log);
return 0;
}
if (command === "install") {
error("The custom downloader has been retired.");
error("Use `pi install npm:pi-mcp-adapter` instead, then optionally run `pi-mcp-adapter init`.");
return 1;
}
if (command === "init") {
return runInit(rest, log);
}
error(`Unknown command: ${command}`);
printHelp(log);
return 1;
}
const isEntrypoint = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
if (isEntrypoint) {
main().then((code) => {
process.exitCode = code;
}).catch((err) => {
console.error(`\nHelper failed: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
});
}