feat(codex): add the codexConfig API namespace

Adds a typed client wrapper for the Codex config API alongside ccConfig,
with methods for reading/writing skills, agents, rules, memory, config,
MCP servers, plugins, and marketplaces. Mirrors ccConfig construction and
error handling. All response shapes match the server's wire format.
This commit is contained in:
2026-09-08 20:20:59 +07:00
parent aca8fd02ee
commit 2ae9b06652
2 changed files with 366 additions and 0 deletions
@@ -0,0 +1,47 @@
/**
* @file Pins the codexConfig API routes and payloads. Tests that the client
* sends the right method, URL, and body shape to the Codex config backend.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { describe, it, expect, vi, afterEach } from "vitest";
import { api } from "../api";
afterEach(() => {
vi.unstubAllGlobals();
});
function stubFetch(response: unknown) {
const spy = vi.fn().mockResolvedValue({
ok: true,
status: 200,
headers: { get: () => "application/json" },
json: async () => response,
text: async () => JSON.stringify(response),
});
vi.stubGlobal("fetch", spy);
return spy;
}
describe("api.codexConfig", () => {
it("sends a write as a single PUT with the artifact envelope", async () => {
const spy = stubFetch({ ok: true, file: "/x", created: true });
await api.codexConfig.write({
scope: "user",
type: "rules",
name: "default",
content: "allow",
});
expect(spy).toHaveBeenCalledTimes(1);
const [, options] = spy.mock.calls[0] as [string, RequestInit | undefined];
expect(options?.method).toBe("PUT");
expect(JSON.parse(options?.body as string)).toEqual({
scope: "user",
type: "rules",
name: "default",
content: "allow",
});
});
});
+319
View File
@@ -1609,6 +1609,162 @@ export const api = {
}),
},
// ─────────────────────────────── Codex-Config API ──────────────────────────────
/** Read/write access to on-disk Codex CLI configuration - skills, agents,
* rules, memory (AGENTS.md), config.toml, MCP servers, plugins, and
* marketplaces - for the dashboard's "Codex Config" explorer/editor pages. */
codexConfig: {
/**
* GET /api/codex-config/overview - counts of every artifact kind, for the
* explorer's landing page.
* @returns {@link CodexOverview} — filesystem roots plus per-kind counts.
*/
overview: () => request<CodexOverview>("/codex-config/overview"),
/**
* GET /api/codex-config/skills - user and/or project skill definitions.
*
* The optional `scope` is appended as `?scope=` only when provided; omitting
* it lets the server apply its default scope.
*
* @param scope Optional {@link CodexScope} ("user"|"project"|"all") filter.
* @returns `{ skills }` — the {@link CodexSkill} items for each skill.
*/
skills: (scope?: CodexScope) =>
request<{ skills: CodexSkill[] }>(`/codex-config/skills${scope ? `?scope=${scope}` : ""}`),
/**
* GET /api/codex-config/agents - user and/or project agent definitions.
* @param scope Optional {@link CodexScope} filter.
* @returns `{ agents }` — {@link CodexAgent} items for each agent.
*/
agents: (scope?: CodexScope) =>
request<{ agents: CodexAgent[] }>(`/codex-config/agents${scope ? `?scope=${scope}` : ""}`),
/**
* GET /api/codex-config/rules - user and/or project rules files.
* @param scope Optional {@link CodexScope} filter.
* @returns `{ rules }` — {@link CodexRule} items for each rule file.
*/
rules: (scope?: CodexScope) =>
request<{ rules: CodexRule[] }>(`/codex-config/rules${scope ? `?scope=${scope}` : ""}`),
/**
* GET /api/codex-config/memory - AGENTS.md files at both scopes.
* @returns `{ memory }` — {@link CodexMemoryFile} items.
*/
memory: () => request<{ memory: CodexMemoryFile[] }>("/codex-config/memory"),
/**
* GET /api/codex-config/config - config.toml files at both scopes.
* @returns `{ config }` — {@link CodexConfigFile} items.
*/
config: () => request<{ config: CodexConfigFile[] }>("/codex-config/config"),
/**
* GET /api/codex-config/file - raw contents of one config file by absolute path.
*
* The absolute path is passed as a URL-encoded `path` query param (not a
* path segment) so arbitrary filesystem paths survive intact.
*
* @param path Absolute path of the config file to read.
* @returns {@link CodexFileResponse} — file text + metadata.
*/
file: (path: string) =>
request<CodexFileResponse>(`/codex-config/file?path=${encodeURIComponent(path)}`),
/**
* PUT /api/codex-config/file - create/overwrite a config artifact; the
* server writes a backup of any previous content first.
*
* @param args {@link CodexWriteArgs} — scope/type/name/content.
* @returns {@link CodexMutationResult} — the written path and whether a new file was created.
*/
write: (args: CodexWriteArgs) =>
request<CodexMutationResult>("/codex-config/file", {
method: "PUT",
body: JSON.stringify(args),
}),
/**
* DELETE /api/codex-config/file - remove a config artifact (also backed up).
*
* @param args {@link CodexDeleteArgs} — which artifact to delete.
* @returns {@link CodexMutationResult} — the deleted path and its backup path.
*/
remove: (args: CodexDeleteArgs) =>
request<CodexMutationResult>("/codex-config/file", {
method: "DELETE",
body: JSON.stringify(args),
}),
/**
* GET /api/codex-config/backups - timestamped backups written by `write`/
* `remove`, optionally filtered by scope/artifact type.
*
* @param params Optional `{ scope, type }` filter for the backup listing.
* @returns `{ items }` — the matching {@link CodexBackup} entries.
*/
backups: (params?: { scope?: "user" | "project"; type?: CodexArtifactType }) =>
requestCodexBackupsHelper(params),
/**
* GET /api/codex-config/mcp - configured MCP servers from the codex CLI.
* @returns `{ mcp }` — {@link CodexMcpServer} items.
*/
mcp: () => request<{ mcp: CodexMcpServer[] }>("/codex-config/mcp"),
/**
* GET /api/codex-config/plugins - installed plugins from the codex CLI.
* @returns `{ plugins }` — {@link CodexPlugin} items.
*/
plugins: () => request<{ plugins: CodexPlugin[] }>("/codex-config/plugins"),
/**
* GET /api/codex-config/marketplaces - registered marketplaces from the codex CLI.
* @returns `{ marketplaces }` — {@link CodexMarketplace} items.
*/
marketplaces: () => request<{ marketplaces: CodexMarketplace[] }>("/codex-config/marketplaces"),
/**
* POST /api/codex-config/mcp/add - add an MCP server via the `codex` CLI.
* @param args Arguments for `codex mcp add`.
* @returns `{ ok, stdout, stderr }` — success and CLI output.
*/
mcpAdd: (args: unknown) =>
request<{ ok: true; stdout: string; stderr: string }>("/codex-config/mcp/add", {
method: "POST",
body: JSON.stringify(args),
}),
/**
* POST /api/codex-config/mcp/remove - remove an MCP server via the `codex` CLI.
* @param name The MCP server name to remove.
* @returns `{ ok, stdout, stderr }` — success and CLI output.
*/
mcpRemove: (name: string) =>
request<{ ok: true; stdout: string; stderr: string }>("/codex-config/mcp/remove", {
method: "POST",
body: JSON.stringify({ name }),
}),
/**
* POST /api/codex-config/plugins/action - add or remove a plugin via the `codex` CLI.
* @param args Arguments with `spec` and `action` fields.
* @returns `{ ok, stdout, stderr }` — success and CLI output.
*/
pluginAction: (args: unknown) =>
request<{ ok: true; stdout: string; stderr: string }>("/codex-config/plugins/action", {
method: "POST",
body: JSON.stringify(args),
}),
/**
* POST /api/codex-config/marketplaces/add - register a marketplace via the `codex` CLI.
* @param url The marketplace URL to register.
* @returns `{ ok, stdout, stderr }` — success and CLI output.
*/
marketplaceAdd: (url: string) =>
request<{ ok: true; stdout: string; stderr: string }>("/codex-config/marketplaces/add", {
method: "POST",
body: JSON.stringify({ url }),
}),
/**
* POST /api/codex-config/marketplaces/action - update or remove a marketplace via the `codex` CLI.
* @param args Arguments with `name` and `action` fields.
* @returns `{ ok, stdout, stderr }` — success and CLI output.
*/
marketplaceAction: (args: unknown) =>
request<{ ok: true; stdout: string; stderr: string }>("/codex-config/marketplaces/action", {
method: "POST",
body: JSON.stringify(args),
}),
},
// ────────────────────────────────── Run API ─────────────────────────────────
/** Spawn/manage headless or conversational `claude` CLI child processes
* launched from the dashboard's Run page, and stream their output. */
@@ -2230,6 +2386,23 @@ function requestBackupsHelper(params?: { scope?: "user" | "project"; type?: CcAr
return request<{ items: CcBackup[] }>(`/cc-config/backups${q ? `?${q}` : ""}`);
}
/** Helper for GET /api/codex-config/backups with optional `scope` and `type` filters.
* Builds an optional `?scope=&type=` query string (each part appended only when
* present) and calls {@link request} for GET /api/codex-config/backups.
*
* @param params Optional `{ scope, type }` filter for the backup listing.
* @returns `{ items }` — the matching {@link CodexBackup} entries. */
function requestCodexBackupsHelper(params?: {
scope?: "user" | "project";
type?: CodexArtifactType;
}) {
const qs = new URLSearchParams();
if (params?.scope) qs.set("scope", params.scope);
if (params?.type) qs.set("type", params.type);
const q = qs.toString();
return request<{ items: CodexBackup[] }>(`/codex-config/backups${q ? `?${q}` : ""}`);
}
// ─────────────────────────────────────────────────────────────────────────────
// CC-Config types — request/response shapes for the "CC Config" explorer/editor.
// These describe on-disk Claude Code configuration artifacts (skills, agents,
@@ -2675,6 +2848,152 @@ export interface CcHookScripts {
items: { name: string; file: string; size: number; mtime: number }[];
}
// ─────────────────────────────────────────────────────────────────────────────
// Codex-Config types — request/response shapes for the "Codex Config" explorer/editor.
// These describe on-disk Codex CLI configuration artifacts (skills, agents, rules,
// memory, config, MCP servers, plugins, marketplaces) as surfaced by the
// /api/codex-config/* routes. They live in this client because they are specific
// to the explorer UI.
// ─────────────────────────────────────────────────────────────────────────────
/** Kind of Codex config artifact manageable via `api.codexConfig.write`/
* `remove` - each maps to a distinct on-disk location. */
export type CodexArtifactType = "skills" | "agents" | "rules" | "memory" | "config";
/** Scope filter accepted by most `codexConfig` list endpoints; "all" merges
* user + project scope in one response. */
export type CodexScope = "user" | "project" | "all";
/** Body for PUT /api/codex-config/file - create or overwrite one artifact. */
export interface CodexWriteArgs {
scope: "user" | "project";
type: CodexArtifactType;
/** Artifact name (e.g. skill/agent/rule name); omitted for singleton
* artifacts like memory/config. */
name?: string;
/** Full file contents to write. */
content: string;
}
/** Body for DELETE /api/codex-config/file - remove one artifact. */
export interface CodexDeleteArgs {
scope: "user" | "project";
type: CodexArtifactType;
name?: string;
}
/** Response shape of a successful `codexConfig.write`/`remove` call. */
export interface CodexMutationResult {
file: string;
created?: boolean;
backup?: { [key: string]: unknown } | null;
}
/** One timestamped backup of a config artifact, from GET /api/codex-config/backups. */
export interface CodexBackup {
type: string;
name: string;
file: string;
mtime: number;
size: number | null;
}
/** A discovered skill: a directory containing a SKILL.md under skills/<name>/. */
export interface CodexSkill {
scope: "user" | "project";
name: string;
path: string;
file: string;
size: number;
mtime: number;
truncated: boolean;
frontmatter: Record<string, string>;
preview: string;
}
/** A discovered agent definition (TOML file). */
export interface CodexAgent {
scope: "user" | "project";
name: string;
file: string;
size: number;
mtime: number;
meta: Record<string, unknown>;
preview: string;
}
/** A discovered rules file. */
export interface CodexRule {
scope: "user" | "project";
name: string;
file: string;
size: number;
mtime: number;
preview: string;
}
/** AGENTS.md file at either user or project scope. */
export interface CodexMemoryFile {
scope: "user" | "project";
file: string;
exists: boolean;
size?: number;
mtime?: number;
preview?: string;
}
/** config.toml file at either user or project scope. */
export interface CodexConfigFile {
scope: "user" | "project";
file: string;
exists: boolean;
size?: number;
mtime?: number;
content?: string;
truncated?: boolean;
}
/** Response shape of GET /api/codex-config/file - full contents of one config artifact. */
export interface CodexFileResponse {
ok: true;
file: string;
size: number;
mtime: number;
truncated: boolean;
text: string;
}
/** Response shape of GET /api/codex-config/overview. */
export interface CodexOverview {
roots: {
codexHome: string;
projectCodexDir: string;
projectRoot: string;
};
counts: {
skills: { user: number; project: number };
agents: { user: number; project: number };
rules: { user: number; project: number };
memory: { user: number; project: number };
config: { user: number; project: number };
};
}
/** One configured MCP server, from GET /api/codex-config/mcp. */
export interface CodexMcpServer {
[key: string]: unknown;
}
/** One installed plugin, from GET /api/codex-config/plugins. */
export interface CodexPlugin {
[key: string]: unknown;
}
/** One registered marketplace, from GET /api/codex-config/marketplaces. */
export interface CodexMarketplace {
[key: string]: unknown;
}
// ─────────────────────────────────────────────────────────────────────────────
// Run types — request/response shapes for the Run page's `claude` process
// spawning/management. `RunMode`/`RunStatus`/`PermissionMode`/`EffortLevel`