feat(omo-codex): link bundled component agents into codex home

Discover component .toml agent files and symlink (or copy on Windows)
them into the Codex agents dir during install, recording an installed
agent manifest and wiring agent config_file entries into config.toml.
Add CodexAgentConfig type and support local marketplace source.
This commit is contained in:
YeonGyu-Kim
2026-05-29 11:18:01 +09:00
parent bb8ef30bbe
commit a49acecc3a
12 changed files with 423 additions and 15 deletions
@@ -0,0 +1,84 @@
import { basename, join } from "node:path";
import { copyFile, lstat, mkdir, readdir, rm, symlink, writeFile } from "node:fs/promises";
import { exists } from "./utils.mjs";
const MANIFEST_FILE = ".installed-agents.json";
export async function linkCachedPluginAgents({ codexHome, pluginRoot, platform = process.platform }) {
const bundledAgents = await discoverBundledAgents(pluginRoot);
if (bundledAgents.length === 0) {
await writeManifest(pluginRoot, []);
return [];
}
const agentsDir = join(codexHome, "agents");
await mkdir(agentsDir, { recursive: true });
const linked = [];
for (const agentPath of bundledAgents) {
const linkPath = join(agentsDir, basename(agentPath));
if (platform === "win32") {
await replaceWithCopy(linkPath, agentPath);
} else {
await replaceWithSymlink(linkPath, agentPath);
}
linked.push({ name: basename(agentPath), path: linkPath, target: agentPath });
}
await writeManifest(pluginRoot, linked.map((entry) => entry.path));
return linked;
}
async function discoverBundledAgents(pluginRoot) {
const componentsRoot = join(pluginRoot, "components");
if (!(await exists(componentsRoot))) return [];
const componentEntries = await readdir(componentsRoot, { withFileTypes: true });
const agents = [];
for (const entry of componentEntries) {
if (!entry.isDirectory()) continue;
const agentsRoot = join(componentsRoot, entry.name, "agents");
if (!(await exists(agentsRoot))) continue;
const agentEntries = await readdir(agentsRoot, { withFileTypes: true });
for (const file of agentEntries) {
if (!file.isFile() || !file.name.endsWith(".toml")) continue;
agents.push(join(agentsRoot, file.name));
}
}
agents.sort();
return agents;
}
async function replaceWithSymlink(linkPath, target) {
await prepareReplacement(linkPath);
await symlink(target, linkPath);
}
async function replaceWithCopy(linkPath, target) {
await prepareReplacement(linkPath);
await copyFile(target, linkPath);
}
async function prepareReplacement(linkPath) {
if (!(await lstatExists(linkPath))) return;
const entryStat = await lstat(linkPath);
if (entryStat.isDirectory() && !entryStat.isSymbolicLink()) {
throw new Error(`${linkPath} already exists and is a directory; refusing to replace`);
}
await rm(linkPath, { force: true });
}
async function writeManifest(pluginRoot, agentPaths) {
const manifestPath = join(pluginRoot, MANIFEST_FILE);
const payload = { agents: [...agentPaths].sort() };
await writeFile(manifestPath, `${JSON.stringify(payload, null, "\t")}\n`);
}
async function lstatExists(path) {
try {
await lstat(path);
return true;
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
throw error;
}
}
@@ -18,6 +18,7 @@ export async function updateCodexConfig({
marketplaceSource = defaultMarketplaceSource(marketplaceName, repoRoot),
pluginNames,
trustedHookStates = [],
agentConfigs = [],
}) {
await mkdir(dirname(configPath), { recursive: true });
let config = "";
@@ -39,6 +40,9 @@ export async function updateCodexConfig({
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");
}
@@ -119,6 +123,18 @@ function ensureHookTrusted(config, key, trustedHash) {
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))