Files
NetMesh/electron/bridges/externalMcp/codexSetup.cjs

329 lines
9.7 KiB
JavaScript
Raw Normal View History

"use strict";
const { runBoundedCliCommand } = require("./boundedCliCommand.cjs");
const EXTERNAL_MCP_CODEX_NAME = "netcatty-external";
const {
formatDiscoveryEnvCliFlags,
} = require("../../cli/externalMcpDiscoveryPath.cjs");
function loadShellUtils() {
return require("../ai/shellUtils.cjs");
}
function loadDesktopCliResolver() {
return require("./desktopCliResolver.cjs");
}
function parseCodexMcpList(rawOutput) {
const parsed = JSON.parse(String(rawOutput || "[]"));
if (!Array.isArray(parsed)) {
throw new Error("Codex MCP list returned an unexpected payload.");
}
return parsed
.filter((entry) => entry && typeof entry === "object")
.map((entry) => ({
name: typeof entry.name === "string" ? entry.name : "",
enabled: entry.enabled !== false,
transport: entry.transport && typeof entry.transport === "object"
? { ...entry.transport }
: null,
env: entry.env && typeof entry.env === "object" ? entry.env : null,
}));
}
function formatCodexCommandText(args, cliPath = "codex") {
const executable = typeof cliPath === "string" && cliPath.trim()
? cliPath.trim()
: "codex";
return [quoteCommandArg(executable), ...args.map(quoteCommandArg)].join(" ");
}
function quoteCommandArg(value) {
if (typeof value !== "string" || value.length === 0) return '""';
// Match ExternalMcpCard quoteShellArg so copyable commands stay shell-safe
// for paths with spaces, quotes, apostrophes, or backslashes.
if (!/[\s"'\\]/u.test(value)) return value;
return `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
}
function formatExistingCommand(transport) {
if (!transport || typeof transport !== "object") return null;
if (transport.type === "stdio") {
const command = typeof transport.command === "string" ? transport.command.trim() : "";
const args = Array.isArray(transport.args)
? transport.args.filter((arg) => typeof arg === "string" && arg.trim())
: [];
return [command, ...args].filter(Boolean).join(" ").trim() || null;
}
if (typeof transport.url === "string" && transport.url.trim()) {
return transport.url.trim();
}
return null;
}
function normalizePathForCompare(value) {
if (typeof value !== "string") return "";
let normalized = value.trim().replace(/^["']|["']$/gu, "");
if (process.platform === "win32") {
normalized = normalized.replace(/\.cmd$/iu, "");
}
return normalized;
}
function pathsMatch(left, right) {
return normalizePathForCompare(left) === normalizePathForCompare(right);
}
function buildCodexAddArgs(launcherPath, discoveryEnv) {
return [
"mcp",
"add",
EXTERNAL_MCP_CODEX_NAME,
...formatDiscoveryEnvCliFlags(discoveryEnv, "codex"),
"--",
launcherPath,
];
}
function getCodexEntryEnv(entry) {
const transportEnv = entry?.transport?.env;
if (transportEnv && typeof transportEnv === "object") return transportEnv;
if (entry?.env && typeof entry.env === "object") return entry.env;
return null;
}
function hasRequiredDiscoveryEnv(entryEnv, discoveryEnv) {
const required = discoveryEnv && typeof discoveryEnv === "object" ? discoveryEnv : {};
const keys = Object.keys(required).filter((key) => typeof required[key] === "string" && required[key]);
if (keys.length === 0) return true;
if (!entryEnv || typeof entryEnv !== "object") return false;
return keys.every((key) => String(entryEnv[key] || "") === String(required[key]));
}
function classifyCodexExternalMcpStatus({
entries,
launcherPath,
codexPath,
discoveryEnv,
commandExecutable,
}) {
const commandArgs = buildCodexAddArgs(launcherPath, discoveryEnv || {});
const base = {
ok: true,
codexPath: codexPath || null,
launcherPath: launcherPath || null,
command: formatCodexCommandText(commandArgs, commandExecutable || codexPath),
existingCommand: null,
error: null,
};
const entry = Array.isArray(entries)
? entries.find((item) => item?.name === EXTERNAL_MCP_CODEX_NAME)
: null;
if (!entry) {
return {
...base,
state: codexPath ? "not_configured" : "codex_not_found",
};
}
const transport = entry.transport || null;
const existingCommand = formatExistingCommand(transport) || launcherPath || EXTERNAL_MCP_CODEX_NAME;
if (entry.enabled === false) {
return {
...base,
state: "not_configured",
existingCommand,
};
}
if (
transport?.type === "stdio"
&& pathsMatch(transport.command, launcherPath)
&& (!Array.isArray(transport.args) || transport.args.length === 0)
) {
if (!hasRequiredDiscoveryEnv(getCodexEntryEnv(entry), discoveryEnv)) {
return {
...base,
state: "not_configured",
existingCommand,
};
}
return {
...base,
state: "configured",
existingCommand,
};
}
return {
...base,
state: "conflict",
existingCommand,
};
}
function createExternalMcpCodexSetup(options = {}) {
const deps = {
launcherPath: options.launcherPath || null,
discoveryEnv: options.discoveryEnv && typeof options.discoveryEnv === "object"
? options.discoveryEnv
: {},
getShellEnv: options.getShellEnv || loadShellUtils().getShellEnv,
resolveCliFromPath: options.resolveCliFromPath || loadShellUtils().resolveCliFromPath,
resolveDesktopManagedCli: options.resolveDesktopManagedCli
|| loadDesktopCliResolver().resolveDesktopManagedCli,
prepareCommandForSpawn: options.prepareCommandForSpawn || loadShellUtils().prepareCommandForSpawn,
spawn: options.spawn || require("node:child_process").spawn,
stripAnsi: options.stripAnsi || loadShellUtils().stripAnsi,
};
function getManualCommand(cliPath) {
return formatCodexCommandText(
buildCodexAddArgs(deps.launcherPath, deps.discoveryEnv),
cliPath,
);
}
async function resolveCodex() {
const shellEnv = await deps.getShellEnv();
// PATH installs keep the bare `codex` copyable command (portable across
// shells). Desktop-managed absolute paths only appear when PATH misses.
const pathResolved = deps.resolveCliFromPath("codex", shellEnv) || null;
const desktopResolved = pathResolved
? null
: (deps.resolveDesktopManagedCli("codex") || null);
const codexPath = pathResolved || desktopResolved;
return {
shellEnv,
codexPath,
commandExecutable: pathResolved ? "codex" : (desktopResolved || "codex"),
};
}
async function runCodex(codexPath, shellEnv, args) {
return await runBoundedCliCommand(deps, codexPath, args, { env: shellEnv });
}
function summarizeFailure(result, fallback) {
return String(result?.stderr || result?.stdout || fallback || "Codex command failed").trim();
}
async function getStatus() {
const { shellEnv, codexPath, commandExecutable } = await resolveCodex();
if (!codexPath) {
return {
ok: true,
state: "codex_not_found",
codexPath: null,
launcherPath: deps.launcherPath,
command: getManualCommand(),
existingCommand: null,
error: null,
};
}
try {
const result = await runCodex(codexPath, shellEnv, ["mcp", "list", "--json"]);
if (result.exitCode !== 0) {
return {
ok: true,
state: "error",
codexPath,
launcherPath: deps.launcherPath,
command: getManualCommand(commandExecutable),
existingCommand: null,
error: summarizeFailure(result, `Codex exited with code ${result.exitCode ?? "unknown"}`),
};
}
const status = classifyCodexExternalMcpStatus({
entries: parseCodexMcpList(result.stdout),
launcherPath: deps.launcherPath,
codexPath,
discoveryEnv: deps.discoveryEnv,
commandExecutable,
});
return {
...status,
command: getManualCommand(commandExecutable),
};
} catch (error) {
return {
ok: true,
state: "error",
codexPath,
launcherPath: deps.launcherPath,
command: getManualCommand(commandExecutable),
existingCommand: null,
error: error?.message || String(error),
};
}
}
async function addToCodex() {
const status = await getStatus();
if (status.state === "codex_not_found" || status.state === "conflict" || status.state === "configured") {
return status;
}
if (status.state === "error") {
return status;
}
const { shellEnv, codexPath, commandExecutable } = await resolveCodex();
if (!codexPath) {
return {
...status,
state: "codex_not_found",
codexPath: null,
};
}
try {
if (status.existingCommand) {
await runCodex(codexPath, shellEnv, ["mcp", "remove", EXTERNAL_MCP_CODEX_NAME]);
}
const addResult = await runCodex(
codexPath,
shellEnv,
buildCodexAddArgs(deps.launcherPath, deps.discoveryEnv),
);
if (addResult.exitCode !== 0) {
return {
ok: true,
state: "error",
codexPath,
launcherPath: deps.launcherPath,
command: getManualCommand(commandExecutable),
existingCommand: null,
error: summarizeFailure(addResult, `Codex exited with code ${addResult.exitCode ?? "unknown"}`),
};
}
return await getStatus();
} catch (error) {
return {
ok: true,
state: "error",
codexPath,
launcherPath: deps.launcherPath,
command: getManualCommand(commandExecutable),
existingCommand: null,
error: error?.message || String(error),
};
}
}
return {
getStatus,
addToCodex,
};
}
module.exports = {
EXTERNAL_MCP_CODEX_NAME,
createExternalMcpCodexSetup,
parseCodexMcpList,
classifyCodexExternalMcpStatus,
};