#!/usr/bin/env bun import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; const HOME = os.homedir(); function expandHome(input: string): string { if (input === "~") return HOME; if (input.startsWith("~/")) return path.resolve(HOME, input.slice(2)); return path.resolve(input); } const AGENT_DIR = process.env.PI_CODING_AGENT_DIR?.trim() ? expandHome(process.env.PI_CODING_AGENT_DIR.trim()) : path.join(HOME, ".pi", "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(), ".pi", "mcp.json"); const IMPORT_PATHS: Record = { 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")], }; type Log = (message: string) => void; type PiConfig = Record & { mcpServers: Record; imports?: string[]; }; function printHelp(log: Log = console.log) { log("pix-mcp helper\n"); log("Install the package with:"); log(" pi install npm:@xynogen/pix-mcp\n"); log("Then optionally run:"); log(" pix-mcp init Detect host configs and scaffold Pi imports"); log(" pix-mcp init --dry-run"); } function readJsonFile(filePath: string): Record { try { return JSON.parse(fs.readFileSync(filePath, "utf-8")) as Record; } catch (cause) { const message = cause instanceof Error ? cause.message : String(cause); throw new Error(`Cannot read JSON from ${filePath}: ${message}`, { cause }); } } function loadPiConfig(): PiConfig { 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: mcpServers as Record, imports, }; } function findAvailableImports(): Array<{ kind: string; path: string }> { const found: Array<{ kind: string; path: string }> = []; 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: Log, imports: Array<{ kind: string; path: string }>): void { log("Config discovery:\n"); const paths: Array<[string, string]> = [ ["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: PiConfig): void { 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: string[], log: Log = console.log): Promise { 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: string[] = process.argv.slice(2), log: Log = console.log, error: Log = console.error, ): Promise { 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:@xynogen/pix-mcp` instead, then optionally run `pix-mcp init`."); return 1; } if (command === "init") { return runInit(rest, log); } error(`Unknown command: ${command}`); printHelp(log); return 1; } const resolvedEntrypoint = process.argv[1] ? fs.realpathSync(process.argv[1]) : undefined; const isEntrypoint = resolvedEntrypoint && import.meta.url === pathToFileURL(resolvedEntrypoint).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); }); }