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:
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user