vendor: import codex-plugins as packages/omo-codex/{plugin,scripts,marketplace.json,MARKETPLACE.md}

This commit is contained in:
YeonGyu-Kim
2026-05-25 22:24:37 +09:00
parent 06c86f526a
commit 2415f37bc0
260 changed files with 22715 additions and 0 deletions
@@ -0,0 +1,152 @@
import { basename, dirname, join, sep } from "node:path";
import { cp, lstat, mkdir, readFile, readdir, readlink, rename, rm, symlink, writeFile } from "node:fs/promises";
import { exists, isRecord } from "./utils.mjs";
export async function installCachedPlugin({ codexHome, marketplaceName, name, runCommand, sourcePath, version }) {
await maybeRunNpmInstall(sourcePath, runCommand);
await maybeRunNpmBuild(sourcePath, runCommand);
const targetPath = join(codexHome, "plugins", "cache", marketplaceName, name, version);
await replaceDirectory(sourcePath, targetPath, shouldCopyPluginPath);
await maybeRunNpmInstall(targetPath, runCommand, ["install", "--omit=dev"]);
await rewriteCachedMcpManifest(targetPath);
return { name, version, path: targetPath };
}
export async function pruneMarketplaceCache({ codexHome, marketplaceName, keepPluginNames }) {
const cacheRoot = join(codexHome, "plugins", "cache", marketplaceName);
if (!(await exists(cacheRoot))) return;
const keep = new Set(keepPluginNames);
const entries = await readdir(cacheRoot, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory() || keep.has(entry.name)) continue;
await rm(join(cacheRoot, entry.name), { recursive: true, force: true });
}
}
export async function linkCachedPluginBins({ binDir, pluginRoot }) {
const binLinks = await discoverPackageBins(pluginRoot);
await mkdir(binDir, { recursive: true });
const linked = [];
for (const link of binLinks) {
const linkPath = join(binDir, link.name);
await replaceSymlink(linkPath, link.target);
linked.push({ name: link.name, path: linkPath, target: link.target });
}
return linked;
}
async function maybeRunNpmInstall(cwd, runCommand, args = ["install"]) {
if (!(await exists(join(cwd, "package.json")))) return;
await runCommand("npm", args, { cwd });
}
async function maybeRunNpmBuild(cwd, runCommand) {
if (!(await exists(join(cwd, "package.json")))) return;
const packageJson = JSON.parse(await readFile(join(cwd, "package.json"), "utf8"));
if (!isRecord(packageJson.scripts) || typeof packageJson.scripts.build !== "string") return;
await runCommand("npm", ["run", "build"], { cwd });
}
async function replaceDirectory(sourcePath, targetPath, filter) {
await mkdir(dirname(targetPath), { recursive: true });
const tempPath = join(dirname(targetPath), `.tmp-${basename(targetPath)}-${process.pid}-${Date.now()}`);
await rm(tempPath, { recursive: true, force: true });
await cp(sourcePath, tempPath, {
recursive: true,
filter: (source) => filter(source, sourcePath),
});
await rm(targetPath, { recursive: true, force: true });
await rename(tempPath, targetPath);
}
async function discoverPackageBins(root) {
const links = [];
await collectPackageBins(root, root, links);
return links;
}
async function collectPackageBins(directory, root, links) {
const entries = await readdir(directory, { withFileTypes: true });
const packageJsonPath = join(directory, "package.json");
if (entries.some((entry) => entry.isFile() && entry.name === "package.json")) {
await appendPackageBinLinks(packageJsonPath, directory, links);
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") continue;
const childPath = join(directory, entry.name);
if (!childPath.startsWith(root)) continue;
await collectPackageBins(childPath, root, links);
}
}
async function appendPackageBinLinks(packageJsonPath, packageRoot, links) {
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
if (!isRecord(packageJson)) return;
const bin = packageJson.bin;
if (typeof bin === "string" && typeof packageJson.name === "string") {
links.push({ name: basename(packageJson.name), target: join(packageRoot, bin) });
return;
}
if (!isRecord(bin)) return;
for (const [name, target] of Object.entries(bin)) {
if (typeof target !== "string") continue;
links.push({ name, target: join(packageRoot, target) });
}
}
async function replaceSymlink(linkPath, targetPath) {
if (await existingNonSymlink(linkPath)) {
throw new Error(`${linkPath} already exists and is not a symlink`);
}
await rm(linkPath, { force: true });
await symlink(targetPath, linkPath);
}
async function existingNonSymlink(path) {
try {
const stat = await lstat(path);
if (!stat.isSymbolicLink()) return true;
await readlink(path);
return false;
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
throw error;
}
}
function shouldCopyPluginPath(path, root) {
const relative = path === root ? "" : path.slice(root.length + sep.length);
if (relative === "") return true;
const parts = relative.split(sep);
return !parts.some((part) => part === ".git" || part === "node_modules");
}
async function rewriteCachedMcpManifest(pluginRoot) {
const manifestPath = join(pluginRoot, ".mcp.json");
if (!(await exists(manifestPath))) return;
const raw = await readFile(manifestPath, "utf8");
const parsed = JSON.parse(raw);
if (!isRecord(parsed) || !isRecord(parsed.mcpServers)) return;
let changed = false;
for (const server of Object.values(parsed.mcpServers)) {
if (!isRecord(server)) continue;
if (server.cwd === "." || server.cwd === "./") {
delete server.cwd;
changed = true;
}
if (!Array.isArray(server.args)) continue;
const nextArgs = server.args.map((arg) => {
if (typeof arg !== "string") return arg;
if (arg.startsWith("./") || arg.startsWith("../")) return join(pluginRoot, arg);
return arg;
});
if (nextArgs.some((value, index) => value !== server.args[index])) {
server.args = nextArgs;
changed = true;
}
}
if (changed) await writeFile(manifestPath, `${JSON.stringify(parsed, null, "\t")}\n`);
}
@@ -0,0 +1,174 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname } from "node:path";
import { exists } from "./utils.mjs";
export async function updateCodexConfig({ configPath, repoRoot, marketplaceName, pluginNames, trustedHookStates = [] }) {
await mkdir(dirname(configPath), { recursive: true });
let config = "";
if (await exists(configPath)) config = await readFile(configPath, "utf8");
config = removeStaleMarketplacePluginBlocks(config, marketplaceName, new Set(pluginNames));
config = removeStaleMarketplaceHookStateBlocks(config, marketplaceName, new Set(pluginNames));
config = ensureFeatureEnabled(config, "plugins");
config = ensureFeatureEnabled(config, "plugin_hooks");
config = ensureMarketplaceBlock(config, marketplaceName, repoRoot);
for (const pluginName of pluginNames) {
config = ensurePluginEnabled(config, `${pluginName}@${marketplaceName}`);
}
for (const state of trustedHookStates) {
config = ensureHookTrusted(config, state.key, state.trustedHash);
}
await writeFile(configPath, config.trimEnd() + "\n");
}
function removeStaleMarketplacePluginBlocks(config, marketplaceName, keepPluginNames) {
return removeTomlSections(config, (header) => {
const pluginKey = parseQuotedPluginHeader(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 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, repoRoot) {
const header = `marketplaces.${marketplaceName}`;
if (findTomlSection(config, header)) return config;
return appendBlock(
config,
[
`[${header}]`,
`last_updated = "${new Date().toISOString().replace(/\.\d{3}Z$/, "Z")}"`,
"source_type = \"local\"",
`source = ${JSON.stringify(repoRoot)}`,
"",
].join("\n"),
);
}
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 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 findTomlSection(config, header) {
const headerLine = `[${header}]`;
const lines = config.match(/[^\n]*\n?|$/g) ?? [];
let offset = 0;
let start = -1;
for (const line of lines) {
if (line.length === 0) break;
const trimmed = line.trim();
if (start === -1) {
if (trimmed === headerLine) start = offset;
} else if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
return { start, end: offset, text: config.slice(start, offset) };
}
offset += line.length;
}
if (start === -1) return null;
return { start, end: config.length, text: config.slice(start) };
}
function replaceOrInsertSetting(config, section, key, value) {
const linePattern = new RegExp(`^${escapeRegExp(key)}\\s*=.*$`, "m");
const replacement = linePattern.test(section.text)
? section.text.replace(linePattern, `${key} = ${value}`)
: insertSetting(section.text, key, value);
return config.slice(0, section.start) + replacement + config.slice(section.end);
}
function insertSetting(sectionText, key, value) {
const lines = sectionText.split("\n");
lines.splice(1, 0, `${key} = ${value}`);
return lines.join("\n");
}
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 parseQuotedPluginHeader(header) {
const prefix = "plugins.";
if (!header.startsWith(prefix)) return null;
return parseJsonString(header.slice(prefix.length));
}
function parseJsonString(value) {
try {
const parsed = JSON.parse(value);
return typeof parsed === "string" ? parsed : null;
} catch {
return null;
}
}
function appendBlock(config, block) {
const prefix = config.trimEnd();
return `${prefix}${prefix.length > 0 ? "\n\n" : ""}${block.trimEnd()}\n`;
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
@@ -0,0 +1,84 @@
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { exists, isRecord } from "./utils.mjs";
const EVENT_LABELS = new Map([
["PreToolUse", "pre_tool_use"],
["PermissionRequest", "permission_request"],
["PostToolUse", "post_tool_use"],
["PreCompact", "pre_compact"],
["PostCompact", "post_compact"],
["SessionStart", "session_start"],
["UserPromptSubmit", "user_prompt_submit"],
["SubagentStart", "subagent_start"],
["SubagentStop", "subagent_stop"],
["Stop", "stop"],
]);
export async function trustedHookStatesForPlugin({ marketplaceName, pluginName, pluginRoot }) {
const manifestPath = join(pluginRoot, ".codex-plugin", "plugin.json");
if (!(await exists(manifestPath))) return [];
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
if (!isRecord(manifest) || typeof manifest.hooks !== "string") return [];
const hooksPath = join(pluginRoot, manifest.hooks);
if (!(await exists(hooksPath))) return [];
const parsed = JSON.parse(await readFile(hooksPath, "utf8"));
if (!isRecord(parsed) || !isRecord(parsed.hooks)) return [];
const keySource = `${pluginName}@${marketplaceName}:${stripDotSlash(manifest.hooks)}`;
const states = [];
for (const [eventName, groups] of Object.entries(parsed.hooks)) {
if (!Array.isArray(groups)) continue;
const eventLabel = EVENT_LABELS.get(eventName);
if (eventLabel === undefined) continue;
for (const [groupIndex, group] of groups.entries()) {
if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
for (const [handlerIndex, handler] of group.hooks.entries()) {
if (!isRecord(handler) || handler.type !== "command") continue;
if (handler.async === true) continue;
if (typeof handler.command !== "string" || handler.command.trim() === "") continue;
const key = `${keySource}:${eventLabel}:${groupIndex}:${handlerIndex}`;
states.push({
key,
trustedHash: commandHookHash(eventLabel, group.matcher, handler),
});
}
}
}
return states;
}
function commandHookHash(eventName, matcher, handler) {
const command = handler.command;
const timeout = Math.max(Number(handler.timeout ?? 600), 1);
const normalizedHandler = {
type: "command",
command,
timeout,
async: false,
};
if (typeof handler.statusMessage === "string") normalizedHandler.statusMessage = handler.statusMessage;
const identity = {
event_name: eventName,
hooks: [normalizedHandler],
};
if (typeof matcher === "string") identity.matcher = matcher;
return `sha256:${createHash("sha256").update(JSON.stringify(canonicalJson(identity))).digest("hex")}`;
}
function canonicalJson(value) {
if (Array.isArray(value)) return value.map(canonicalJson);
if (!isRecord(value)) return value;
const result = {};
for (const key of Object.keys(value).sort()) {
result[key] = canonicalJson(value[key]);
}
return result;
}
function stripDotSlash(value) {
return value.startsWith("./") ? value.slice(2) : value;
}
@@ -0,0 +1,93 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { isRecord } from "./utils.mjs";
const MARKETPLACE_PATH = ".agents/plugins/marketplace.json";
export async function readMarketplace(repoRoot) {
const marketplacePath = join(repoRoot, MARKETPLACE_PATH);
const raw = await readFile(marketplacePath, "utf8");
const parsed = JSON.parse(raw);
if (!isRecord(parsed)) throw new Error("marketplace.json must be an object");
if (typeof parsed.name !== "string" || parsed.name.trim() === "") {
throw new Error("marketplace.json name must be a non-empty string");
}
validatePathSegment(parsed.name, "marketplace name");
if (!Array.isArray(parsed.plugins)) throw new Error("marketplace.json plugins must be an array");
return {
name: parsed.name,
plugins: parsed.plugins.map((plugin, index) => normalizeMarketplacePlugin(plugin, index)),
};
}
export function resolvePluginSource(repoRoot, plugin) {
const sourcePath = localSourcePath(plugin.source);
const relativePath = sourcePath.slice(2);
return join(repoRoot, ...relativePath.split(/[\\/]/));
}
export async function readPluginManifest(pluginRoot) {
const raw = await readFile(join(pluginRoot, ".codex-plugin", "plugin.json"), "utf8");
const parsed = JSON.parse(raw);
if (!isRecord(parsed)) throw new Error(`${pluginRoot} plugin.json must be an object`);
if (typeof parsed.name !== "string" || parsed.name.trim() === "") {
throw new Error(`${pluginRoot} plugin.json name must be a non-empty string`);
}
const manifest = { name: parsed.name };
if (parsed.version !== undefined) {
if (typeof parsed.version !== "string" || parsed.version.trim() === "") {
throw new Error(`${pluginRoot} plugin.json version must be a non-empty string`);
}
manifest.version = parsed.version.trim();
}
return manifest;
}
export function validatePathSegment(value, label) {
if (!/^[A-Za-z0-9._+-]+$/.test(value)) {
throw new Error(`${label} contains unsupported characters: ${value}`);
}
if (value === "." || value === "..") {
throw new Error(`${label} must not be a path traversal segment`);
}
}
function normalizeMarketplacePlugin(plugin, index) {
if (!isRecord(plugin)) throw new Error(`marketplace plugin ${index} must be an object`);
if (typeof plugin.name !== "string" || plugin.name.trim() === "") {
throw new Error(`marketplace plugin ${index} name must be a non-empty string`);
}
validatePathSegment(plugin.name, "plugin name");
return {
name: plugin.name,
source: plugin.source,
};
}
function localSourcePath(source) {
if (typeof source === "string") return validateLocalSourcePath(source);
if (
isRecord(source) &&
source.source === "local" &&
typeof source.path === "string"
) {
return validateLocalSourcePath(source.path);
}
throw new Error("local plugin source must be a string path or { source: \"local\", path } object");
}
function validateLocalSourcePath(path) {
if (!path.startsWith("./")) {
throw new Error("local plugin source path must start with ./");
}
const relative = path.slice(2);
if (relative.length === 0) throw new Error("local plugin source path must not be empty");
for (const part of relative.split(/[\\/]/)) {
if (part === "" || part === "." || part === "..") {
throw new Error("local plugin source path must stay within the marketplace root");
}
}
return path;
}
@@ -0,0 +1,19 @@
import { spawn } from "node:child_process";
export async function defaultRunCommand(command, args, options) {
await new Promise((resolvePromise, reject) => {
const child = spawn(command, args, {
cwd: options.cwd,
stdio: "inherit",
});
child.once("error", reject);
child.once("exit", (code, signal) => {
if (code === 0) {
resolvePromise();
return;
}
const suffix = signal ? `signal ${signal}` : `exit code ${code}`;
reject(new Error(`${command} ${args.join(" ")} failed in ${options.cwd} with ${suffix}`));
});
});
}
@@ -0,0 +1,15 @@
import { constants as fsConstants } from "node:fs";
import { access } from "node:fs/promises";
export async function exists(path) {
try {
await access(path, fsConstants.F_OK);
return true;
} catch {
return false;
}
}
export function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}