244 lines
8.5 KiB
JavaScript
244 lines
8.5 KiB
JavaScript
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
import { dirname } from "node:path";
|
|
|
|
import { ensureCodexMultiAgentV2Config } from "./multi-agent-v2-config.mjs";
|
|
import { appendBlock, findTomlSection, replaceOrInsertSetting } from "./toml-editor.mjs";
|
|
import { exists } from "./utils.mjs";
|
|
|
|
const LEGACY_CODEX_PLUGIN_MARKETPLACE = ["code", "yeongyu", "codex", "plugins"].join("-");
|
|
const SISYPHUS_LEGACY_MARKETPLACES = ["lazycodex", LEGACY_CODEX_PLUGIN_MARKETPLACE];
|
|
const MANAGED_CODEX_AGENT_NAMES = [
|
|
"codex-ultrawork-reviewer",
|
|
"explorer",
|
|
"librarian",
|
|
"metis",
|
|
"momus",
|
|
"plan",
|
|
];
|
|
const CONTEXT7_MCP_SERVER_HEADER = "mcp_servers.context7";
|
|
const CONTEXT7_MCP_SERVER_BLOCK = [
|
|
`[${CONTEXT7_MCP_SERVER_HEADER}]`,
|
|
`command = "npx"`,
|
|
`args = ["-y", "@upstash/context7-mcp", "--api-key", "YOUR_API_KEY"]`,
|
|
`startup_timeout_sec = 20`,
|
|
"",
|
|
].join("\n");
|
|
|
|
export async function updateCodexConfig({
|
|
configPath,
|
|
repoRoot,
|
|
marketplaceName,
|
|
marketplaceSource = defaultMarketplaceSource(marketplaceName, repoRoot),
|
|
pluginNames,
|
|
trustedHookStates = [],
|
|
agentConfigs = [],
|
|
}) {
|
|
await mkdir(dirname(configPath), { recursive: true });
|
|
let config = "";
|
|
if (await exists(configPath)) config = await readFile(configPath, "utf8");
|
|
|
|
for (const legacyMarketplaceName of legacyMarketplaceNames(marketplaceName)) {
|
|
config = removeMarketplaceBlock(config, legacyMarketplaceName);
|
|
config = removeStaleMarketplacePluginBlocks(config, legacyMarketplaceName, new Set());
|
|
config = removeStaleMarketplaceHookStateBlocks(config, legacyMarketplaceName, new Set());
|
|
}
|
|
config = removeStaleMarketplacePluginBlocks(config, marketplaceName, new Set(pluginNames));
|
|
config = removeStaleMarketplaceHookStateBlocks(config, marketplaceName, new Set(pluginNames));
|
|
config = removeStaleManagedAgentBlocks(config, new Set(agentConfigs.map((agentConfig) => agentConfig.name)));
|
|
config = ensureFeatureEnabled(config, "plugins");
|
|
config = ensureFeatureEnabled(config, "plugin_hooks");
|
|
config = ensureCodexMultiAgentV2Config(config);
|
|
config = ensureContext7McpServer(config);
|
|
config = ensureMarketplaceBlock(config, marketplaceName, marketplaceSource);
|
|
for (const pluginName of pluginNames) {
|
|
config = ensurePluginEnabled(config, `${pluginName}@${marketplaceName}`);
|
|
}
|
|
for (const state of trustedHookStates) {
|
|
config = ensureHookTrusted(config, state.key, state.trustedHash);
|
|
}
|
|
for (const agentConfig of agentConfigs) {
|
|
config = ensureAgentConfig(config, agentConfig);
|
|
}
|
|
|
|
await writeFile(configPath, config.trimEnd() + "\n");
|
|
}
|
|
|
|
function legacyMarketplaceNames(marketplaceName) {
|
|
return marketplaceName === "sisyphuslabs" ? SISYPHUS_LEGACY_MARKETPLACES : [];
|
|
}
|
|
|
|
function removeMarketplaceBlock(config, marketplaceName) {
|
|
return removeTomlSections(config, (header) => header === `marketplaces.${marketplaceName}`);
|
|
}
|
|
|
|
function defaultMarketplaceSource(marketplaceName, repoRoot) {
|
|
return {
|
|
sourceType: "local",
|
|
source: repoRoot,
|
|
};
|
|
}
|
|
|
|
function removeStaleMarketplacePluginBlocks(config, marketplaceName, keepPluginNames) {
|
|
return removeTomlSections(config, (header) => {
|
|
const pluginKey = parsePluginHeaderKey(header);
|
|
if (pluginKey === null) return false;
|
|
const suffix = `@${marketplaceName}`;
|
|
if (!pluginKey.endsWith(suffix)) return false;
|
|
return !keepPluginNames.has(pluginKey.slice(0, -suffix.length));
|
|
});
|
|
}
|
|
|
|
function removeStaleMarketplaceHookStateBlocks(config, marketplaceName, keepPluginNames) {
|
|
return removeTomlSections(config, (header) => {
|
|
const prefix = "hooks.state.";
|
|
if (!header.startsWith(prefix)) return false;
|
|
const hookKey = parseJsonString(header.slice(prefix.length));
|
|
if (hookKey === null) return false;
|
|
const separator = hookKey.indexOf(":");
|
|
if (separator === -1) return false;
|
|
const pluginKey = hookKey.slice(0, separator);
|
|
const suffix = `@${marketplaceName}`;
|
|
if (!pluginKey.endsWith(suffix)) return false;
|
|
return !keepPluginNames.has(pluginKey.slice(0, -suffix.length));
|
|
});
|
|
}
|
|
|
|
function removeStaleManagedAgentBlocks(config, keepAgentNames) {
|
|
const managedAgentNames = new Set(MANAGED_CODEX_AGENT_NAMES);
|
|
return splitTomlSections(config)
|
|
.filter((section) => {
|
|
if (section.header === null) return true;
|
|
const agentName = parseAgentHeaderName(section.header);
|
|
if (agentName === null || !managedAgentNames.has(agentName) || keepAgentNames.has(agentName)) return true;
|
|
return !section.text.includes(`config_file = ${JSON.stringify(`./agents/${agentName}.toml`)}`);
|
|
})
|
|
.map((section) => section.text)
|
|
.join("")
|
|
.replace(/\n{3,}/g, "\n\n");
|
|
}
|
|
|
|
function ensureFeatureEnabled(config, featureName) {
|
|
const section = findTomlSection(config, "features");
|
|
if (!section) return appendBlock(config, `[features]\n${featureName} = true\n`);
|
|
return replaceOrInsertSetting(config, section, featureName, "true");
|
|
}
|
|
|
|
function ensureMarketplaceBlock(config, marketplaceName, source) {
|
|
const header = `marketplaces.${marketplaceName}`;
|
|
const block = [
|
|
`[${header}]`,
|
|
`last_updated = "${new Date().toISOString().replace(/\.\d{3}Z$/, "Z")}"`,
|
|
`source_type = ${JSON.stringify(source.sourceType)}`,
|
|
`source = ${JSON.stringify(source.source)}`,
|
|
source.ref === undefined ? null : `ref = ${JSON.stringify(source.ref)}`,
|
|
"",
|
|
].filter((line) => line !== null).join("\n");
|
|
const section = findTomlSection(config, header);
|
|
if (section) return config.slice(0, section.start) + block + config.slice(section.end);
|
|
return appendBlock(config, block);
|
|
}
|
|
|
|
function ensureContext7McpServer(config) {
|
|
if (findTomlSection(config, CONTEXT7_MCP_SERVER_HEADER)) return config;
|
|
return appendBlock(config, CONTEXT7_MCP_SERVER_BLOCK);
|
|
}
|
|
|
|
function ensurePluginEnabled(config, pluginKey) {
|
|
const header = `plugins.${JSON.stringify(pluginKey)}`;
|
|
const section = findTomlSection(config, header);
|
|
if (!section) return appendBlock(config, `[${header}]\nenabled = true\n`);
|
|
return replaceOrInsertSetting(config, section, "enabled", "true");
|
|
}
|
|
|
|
function ensureHookTrusted(config, key, trustedHash) {
|
|
const header = `hooks.state.${JSON.stringify(key)}`;
|
|
const section = findTomlSection(config, header);
|
|
if (!section) return appendBlock(config, `[${header}]\ntrusted_hash = ${JSON.stringify(trustedHash)}\n`);
|
|
return replaceOrInsertSetting(config, section, "trusted_hash", JSON.stringify(trustedHash));
|
|
}
|
|
|
|
function ensureAgentConfig(config, agentConfig) {
|
|
const header = `agents.${tomlKeySegment(agentConfig.name)}`;
|
|
const section = findTomlSection(config, header);
|
|
const configFile = JSON.stringify(agentConfig.configFile);
|
|
if (!section) return appendBlock(config, `[${header}]\nconfig_file = ${configFile}\n`);
|
|
return replaceOrInsertSetting(config, section, "config_file", configFile);
|
|
}
|
|
|
|
function tomlKeySegment(value) {
|
|
return /^[A-Za-z0-9_-]+$/.test(value) ? value : JSON.stringify(value);
|
|
}
|
|
|
|
function removeTomlSections(config, shouldRemove) {
|
|
return splitTomlSections(config)
|
|
.filter((section) => section.header === null || !shouldRemove(section.header))
|
|
.map((section) => section.text)
|
|
.join("")
|
|
.replace(/\n{3,}/g, "\n\n");
|
|
}
|
|
|
|
function splitTomlSections(config) {
|
|
const lines = config.match(/[^\n]*\n?|$/g) ?? [];
|
|
const sections = [];
|
|
let current = { header: null, text: "" };
|
|
for (const line of lines) {
|
|
if (line.length === 0) break;
|
|
const header = parseTomlHeader(line);
|
|
if (header !== null) {
|
|
if (current.text.length > 0) sections.push(current);
|
|
current = { header, text: line };
|
|
} else {
|
|
current.text += line;
|
|
}
|
|
}
|
|
if (current.text.length > 0) sections.push(current);
|
|
return sections;
|
|
}
|
|
|
|
function parseTomlHeader(line) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return null;
|
|
if (trimmed.startsWith("[[")) return null;
|
|
return trimmed.slice(1, -1);
|
|
}
|
|
|
|
function parsePluginHeaderKey(header) {
|
|
const prefix = "plugins.";
|
|
if (!header.startsWith(prefix)) return null;
|
|
return parseLeadingJsonString(header.slice(prefix.length));
|
|
}
|
|
|
|
function parseAgentHeaderName(header) {
|
|
const prefix = "agents.";
|
|
if (!header.startsWith(prefix)) return null;
|
|
const key = header.slice(prefix.length);
|
|
return key.startsWith('"') ? parseLeadingJsonString(key) : key;
|
|
}
|
|
|
|
function parseLeadingJsonString(value) {
|
|
if (!value.startsWith('"')) return parseJsonString(value);
|
|
let escaped = false;
|
|
for (let index = 1; index < value.length; index += 1) {
|
|
const char = value[index];
|
|
if (escaped) {
|
|
escaped = false;
|
|
continue;
|
|
}
|
|
if (char === "\\") {
|
|
escaped = true;
|
|
continue;
|
|
}
|
|
if (char === '"') return parseJsonString(value.slice(0, index + 1));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function parseJsonString(value) {
|
|
try {
|
|
const parsed = JSON.parse(value);
|
|
return typeof parsed === "string" ? parsed : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|