57f02f0d89
- openapi-extra/run.js describes tmux pane runs only: the removed
POST /api/run/{id}/message and stream-json run_stream/run_input_ack are gone.
- 108 operations added to OpenAPI (lanes, run, settings, reports, cc-config,
pets, ai-proxies, tool-profiles, ai-tools, snapshots, locks, ...);
openapi.yaml regenerated.
- docs/API.md: 74 routes added, 4 endpoints that no longer exist removed.
- Role is `developer`, not `member` (CLI.md, DATABASE.md, OVERVIEW.md);
`agenthub update` -> `update-check`; help lists `server install-system|tools`
and `users provision`; ARCHITECTURE.md's stale paths and names fixed.
- cc-config/codex-config writes carry sameOriginGuard (an MCP server or plugin
is code Claude runs later), with a test; `command -v` takes its name as $1.
799 lines
29 KiB
JavaScript
799 lines
29 KiB
JavaScript
/**
|
|
* @file OpenAPI fragment for the AI-tool configuration routes the base spec
|
|
* and the other fragments left out: AI Proxy profiles (`/api/ai-proxies`, also
|
|
* mounted at the pre-rename `/api/ai-routers`), the non-Claude login profiles
|
|
* (`/api/tool-profiles`), a tool's catalog-listed config files, model list and
|
|
* usage (`/api/ai-tools/{id}/…`), the Claude plugin/marketplace CLI routes and
|
|
* project-memory bootstrap under `/api/cc-config` (plus its Codex twin), and
|
|
* AgentHub's own git credential store (`/api/git-credentials`). No operation
|
|
* here returns key material — profiles and credentials report `hasKey` /
|
|
* `hasToken` only. Exports `{ tags, schemas, paths }` for
|
|
* server/openapi-extra.js.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const tags = [
|
|
{
|
|
name: "AI Proxies",
|
|
description:
|
|
"AI Proxy profiles (endpoint + key + model) a new Claude/Codex session may name. Also mounted at /api/ai-routers. No route returns a key.",
|
|
},
|
|
{
|
|
name: "Tool Profiles",
|
|
description:
|
|
"Saved login profiles of the AI tools other than Claude Code (Codex, Oh My Pi). Identity metadata only, never a token.",
|
|
},
|
|
{
|
|
name: "Git Credentials",
|
|
description:
|
|
"AgentHub's own git credential store (one login per protocol + host). Passwords go in, never out.",
|
|
},
|
|
];
|
|
|
|
const err = { $ref: "#/components/schemas/ErrorResponse" };
|
|
const json = (schema, description) => ({
|
|
description,
|
|
content: { "application/json": { schema } },
|
|
});
|
|
const errResp = (description) => json(err, description);
|
|
const body = (schema, required = true) => ({
|
|
required,
|
|
content: { "application/json": { schema } },
|
|
});
|
|
|
|
const schemas = {
|
|
AiProxyProfile: {
|
|
type: "object",
|
|
description:
|
|
"The browser-safe view of a profile: every stored field except the key, plus `hasKey` and `scope`.",
|
|
properties: {
|
|
name: { type: "string", description: "Server-made slug; never changes once saved." },
|
|
label: { type: "string" },
|
|
baseUrl: { type: "string", description: "The proxy's ROOT (a trailing /v1 is dropped)." },
|
|
model: { type: "string" },
|
|
models: { type: "object", additionalProperties: { type: "string" } },
|
|
behavesAs: {
|
|
type: "string",
|
|
description: "User-chosen Claude model the proxy model acts as; empty when unset.",
|
|
},
|
|
savedAt: { type: "string", format: "date-time" },
|
|
builtin: { type: "boolean", description: "True only for the built-in `native` profile." },
|
|
scope: { type: "string", enum: ["shared", "personal"] },
|
|
hasKey: { type: "boolean" },
|
|
},
|
|
},
|
|
AiProxyWriteRequest: {
|
|
type: "object",
|
|
properties: {
|
|
label: { type: "string" },
|
|
baseUrl: { type: "string" },
|
|
apiKey: {
|
|
type: "string",
|
|
description: "Write-only. On PATCH an omitted/blank key keeps the stored one.",
|
|
},
|
|
model: { type: "string" },
|
|
models: { type: "object", additionalProperties: { type: "string" } },
|
|
behavesAs: { type: "string" },
|
|
scope: {
|
|
type: "string",
|
|
enum: ["shared", "personal"],
|
|
description:
|
|
"POST only (server mode). `shared` is admin-only; a non-admin always writes a personal profile.",
|
|
},
|
|
},
|
|
},
|
|
AiProxyTestResult: {
|
|
type: "object",
|
|
properties: {
|
|
ok: { type: "boolean" },
|
|
status: {
|
|
type: "integer",
|
|
nullable: true,
|
|
description: "HTTP status the proxy answered with; null when unreachable.",
|
|
},
|
|
message: {
|
|
type: "string",
|
|
description:
|
|
"The model id on success, else up to 300 chars of the proxy's reply or the transport error.",
|
|
},
|
|
},
|
|
},
|
|
ToolProfileIdentity: {
|
|
type: "object",
|
|
properties: {
|
|
kind: { type: "string" },
|
|
email: { type: "string", nullable: true },
|
|
detail: { type: "string", nullable: true },
|
|
},
|
|
},
|
|
ToolProfileItem: {
|
|
type: "object",
|
|
properties: {
|
|
name: { type: "string" },
|
|
label: { type: "string" },
|
|
savedAt: { type: "integer", nullable: true, description: "Epoch ms." },
|
|
active: { type: "boolean", description: "This profile holds the login that is live now." },
|
|
kind: { type: "string" },
|
|
email: { type: "string", nullable: true },
|
|
detail: { type: "string", nullable: true },
|
|
},
|
|
},
|
|
ToolProfilesListResponse: {
|
|
type: "object",
|
|
properties: {
|
|
tools: {
|
|
type: "array",
|
|
items: {
|
|
type: "object",
|
|
properties: {
|
|
tool: { type: "string", example: "codex" },
|
|
supported: { type: "boolean" },
|
|
reason: {
|
|
type: "string",
|
|
description: "Why the tool cannot be switched (supported: false).",
|
|
},
|
|
items: { type: "array", items: { $ref: "#/components/schemas/ToolProfileItem" } },
|
|
active: {
|
|
allOf: [{ $ref: "#/components/schemas/ToolProfileIdentity" }],
|
|
nullable: true,
|
|
},
|
|
error: { allOf: [{ $ref: "#/components/schemas/ErrorObject" }], nullable: true },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ToolProfileNameRequest: {
|
|
type: "object",
|
|
properties: {
|
|
name: { type: "string", description: "Profile name (`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,39}$`)." },
|
|
label: { type: "string", description: "PATCH only: the new display label." },
|
|
},
|
|
},
|
|
GitCredentialItem: {
|
|
type: "object",
|
|
properties: {
|
|
protocol: { type: "string", enum: ["http", "https"] },
|
|
host: { type: "string", example: "git.example.com" },
|
|
username: { type: "string" },
|
|
savedAt: { type: "string" },
|
|
hasToken: { type: "boolean", enum: [true] },
|
|
},
|
|
},
|
|
GitCredentialsListResponse: {
|
|
type: "object",
|
|
properties: {
|
|
items: { type: "array", items: { $ref: "#/components/schemas/GitCredentialItem" } },
|
|
},
|
|
},
|
|
CcConfigCliResult: {
|
|
type: "object",
|
|
description:
|
|
"A finished `claude plugin …` run. A non-zero exit answers 422 `ECLIEXIT` with `code`, `stdout`, `stderr` beside the error.",
|
|
properties: {
|
|
ok: { type: "boolean", enum: [true] },
|
|
stdout: { type: "string" },
|
|
stderr: { type: "string" },
|
|
},
|
|
},
|
|
ProjectMemoryCreateRequest: {
|
|
type: "object",
|
|
required: ["dir", "content"],
|
|
properties: {
|
|
dir: { type: "string", description: "Absolute path of an existing directory." },
|
|
content: { type: "string", description: "File text, at most 1 MB." },
|
|
},
|
|
},
|
|
ProjectMemoryCreateResponse: {
|
|
type: "object",
|
|
properties: {
|
|
file: { type: "string" },
|
|
size: { type: "integer" },
|
|
created: { type: "boolean", enum: [true] },
|
|
},
|
|
},
|
|
};
|
|
|
|
const cliErrors = {
|
|
400: errResp("`EBADARG` (invalid identifier, source or scope) or `EBADACTION`."),
|
|
422: errResp(
|
|
"The CLI exited non-zero (`ECLIEXIT`); the body also carries `code`, `stdout`, `stderr`."
|
|
),
|
|
502: errResp("`ECLIFAILED`."),
|
|
503: errResp("`ECLINOTFOUND` — no `claude` binary."),
|
|
504: errResp("`ETIMEOUT`."),
|
|
};
|
|
const cliOk = json(
|
|
{ $ref: "#/components/schemas/CcConfigCliResult" },
|
|
"The CLI succeeded; a `cc_config_changed` WebSocket message is broadcast."
|
|
);
|
|
|
|
const nameParam = { name: "name", in: "path", required: true, schema: { type: "string" } };
|
|
const toolParam = {
|
|
name: "tool",
|
|
in: "path",
|
|
required: true,
|
|
schema: { type: "string", example: "codex" },
|
|
description: "A tool the profiles module covers (`codex`, `omp`).",
|
|
};
|
|
const toolErrors = {
|
|
400: errResp("`EBADNAME` / `EBADLABEL`."),
|
|
404: errResp("`EBADTOOL` (unknown tool) or `ENOTFOUND` (no such profile)."),
|
|
409: errResp(
|
|
"`EUNSUPPORTED`, `ENOTINSTALLED`, `EBADJSON`, `EACTIVE`, `EUNSAVED`, `ENOAUTH`, `EDUPLICATE` or `ELABELTAKEN`."
|
|
),
|
|
502: errResp("`EOMP` — Oh My Pi's own auth command failed."),
|
|
};
|
|
const toolOk = json(
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
ok: { type: "boolean", enum: [true] },
|
|
name: { type: "string" },
|
|
label: { type: "string" },
|
|
},
|
|
},
|
|
"Done."
|
|
);
|
|
const idParam = {
|
|
name: "id",
|
|
in: "path",
|
|
required: true,
|
|
schema: { type: "string" },
|
|
description: "AI Tools catalog id.",
|
|
};
|
|
const indexParam = {
|
|
name: "index",
|
|
in: "path",
|
|
required: true,
|
|
schema: { type: "integer", minimum: 0 },
|
|
description: "Index into the tool's catalog-declared `config` list.",
|
|
};
|
|
const configErrors = {
|
|
400: errResp("`EBADFIELD` (text not a string) or `EBADSYNTAX` (JSON/JSONC/TOML does not parse)."),
|
|
403: errResp("`ESECRET` — the file (or its symlink target) has a credential-like name."),
|
|
404: errResp("`ENOTOOL`, `ENOFILE` (no such index) or `ENOENT` (missing / not a regular file)."),
|
|
409: errResp("`ESTALE` — the file changed since it was opened (PUT)."),
|
|
413: errResp("`ETOOBIG` — over 1 MB."),
|
|
415: errResp("`EBINARY` — not a text file."),
|
|
};
|
|
|
|
const projectMemoryOp = (tool, file, operationId, tag) => ({
|
|
post: {
|
|
tags: [tag],
|
|
summary: `Create ${file} in a project directory`,
|
|
description: `Creates \`<dir>/${file}\` for a project the dashboard has never run in — the one ${tag} write that takes a caller-chosen directory. \`dir\` must be an absolute, existing directory (resolved with realpath); an existing \`${file}\` — a symlink included — is refused (\`EEXISTS\`), never overwritten. Same-origin guarded; admin-only in server mode. Broadcasts \`${tool === "claude" ? "cc_config_changed" : "codex_config_changed"}\`.`,
|
|
operationId,
|
|
requestBody: body({ $ref: "#/components/schemas/ProjectMemoryCreateRequest" }),
|
|
responses: {
|
|
200: json({ $ref: "#/components/schemas/ProjectMemoryCreateResponse" }, "Created."),
|
|
400: errResp("`EBADDIR`, `EBADCONTENT` or `EOUTOFROOT`."),
|
|
409: errResp("`EEXISTS`."),
|
|
413: errResp("`ETOOLARGE`."),
|
|
},
|
|
},
|
|
});
|
|
|
|
const paths = {
|
|
"/api/ai-proxies": {
|
|
get: {
|
|
tags: ["AI Proxies"],
|
|
summary: "List AI Proxy profiles",
|
|
description:
|
|
"Every profile the viewer may choose: the built-in `native` first, then the shared ones, then the viewer's own personal ones (nobody lists another user's personal profiles, an admin included). Never carries a key — `hasKey` only.",
|
|
operationId: "listAiProxies",
|
|
responses: {
|
|
200: json(
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
items: { type: "array", items: { $ref: "#/components/schemas/AiProxyProfile" } },
|
|
},
|
|
},
|
|
"Profiles."
|
|
),
|
|
},
|
|
},
|
|
post: {
|
|
tags: ["AI Proxies"],
|
|
summary: "Create an AI Proxy profile",
|
|
description:
|
|
'Saves a profile under a server-made name derived from `label`. In server mode a non-admin always creates a PERSONAL profile; `scope: "shared"` from a non-admin is `EADMIN`. Local mode writes the shared store. Same-origin guarded. The response is the browser-safe view (no key).',
|
|
operationId: "createAiProxy",
|
|
requestBody: body({ $ref: "#/components/schemas/AiProxyWriteRequest" }),
|
|
responses: {
|
|
201: json({ $ref: "#/components/schemas/AiProxyProfile" }, "Created."),
|
|
400: errResp("`EBADNAME`, `EBADLABEL`, `EBADURL` or `EBADMODEL`."),
|
|
403: errResp("`EADMIN` — only an admin may write a shared profile."),
|
|
409: errResp("`EBUILTIN` (the name `native`) or `EBADJSON`."),
|
|
},
|
|
},
|
|
},
|
|
"/api/ai-proxies/9router": {
|
|
get: {
|
|
tags: ["AI Proxies"],
|
|
summary: "Is 9router installed",
|
|
description:
|
|
"Reports whether a `9router` binary is on the server's login-shell PATH, so Settings can offer a link to its own UI. A file lookup only; nothing is spawned.",
|
|
operationId: "getAiProxy9router",
|
|
responses: {
|
|
200: json(
|
|
{ type: "object", properties: { installed: { type: "boolean" } } },
|
|
"Lookup result."
|
|
),
|
|
},
|
|
},
|
|
},
|
|
"/api/ai-proxies/test": {
|
|
post: {
|
|
tags: ["AI Proxies"],
|
|
summary: "Test an AI Proxy endpoint",
|
|
description:
|
|
"Probes `<baseUrl>/v1/messages` (Claude Code's shape, 1 output token) and `<baseUrl>/v1/responses` (Codex's, 16 tokens) in parallel, 15 s timeout, redirects not followed. A reachable proxy is a 200 whatever it replies. A blank `apiKey` falls back to the stored key of profile `name` ONLY while the submitted `baseUrl` equals the saved one (and only for the profile's owner); otherwise `EBADKEY`. The key is never echoed. Same-origin guarded.",
|
|
operationId: "testAiProxy",
|
|
requestBody: body({
|
|
type: "object",
|
|
required: ["baseUrl", "model"],
|
|
properties: {
|
|
baseUrl: { type: "string" },
|
|
model: { type: "string" },
|
|
apiKey: { type: "string" },
|
|
name: { type: "string", description: "Saved profile whose key to fall back to." },
|
|
},
|
|
}),
|
|
responses: {
|
|
200: json(
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
claude: { $ref: "#/components/schemas/AiProxyTestResult" },
|
|
codex: { $ref: "#/components/schemas/AiProxyTestResult" },
|
|
},
|
|
},
|
|
"Probe outcomes."
|
|
),
|
|
400: errResp("`EBADKEY` (no key to test with), `EBADURL` or `EBADMODEL`."),
|
|
},
|
|
},
|
|
},
|
|
"/api/ai-proxies/{name}": {
|
|
patch: {
|
|
tags: ["AI Proxies"],
|
|
summary: "Update an AI Proxy profile",
|
|
description:
|
|
"Merges the given fields; the name never changes and an omitted/blank `apiKey` keeps the stored one. Also rewrites the per-session settings files that copy this profile, so a rotated key reaches live sessions. A personal profile is writable only by its owner (anyone else gets 404); a shared one only by an admin. Same-origin guarded.",
|
|
operationId: "updateAiProxy",
|
|
parameters: [nameParam],
|
|
requestBody: body({ $ref: "#/components/schemas/AiProxyWriteRequest" }),
|
|
responses: {
|
|
200: json({ $ref: "#/components/schemas/AiProxyProfile" }, "Updated."),
|
|
400: errResp("`EBADLABEL`, `EBADURL` or `EBADMODEL`."),
|
|
403: errResp("`EADMIN`."),
|
|
404: errResp("`ENOTFOUND`."),
|
|
409: errResp("`EBUILTIN` or `EBADJSON`."),
|
|
},
|
|
},
|
|
delete: {
|
|
tags: ["AI Proxies"],
|
|
summary: "Delete an AI Proxy profile",
|
|
description:
|
|
"Removes the profile and the per-session files that copy its key. Same ownership rules as PATCH. Same-origin guarded.",
|
|
operationId: "deleteAiProxy",
|
|
parameters: [nameParam],
|
|
responses: {
|
|
200: json({ type: "object", properties: { removed: { type: "string" } } }, "Removed."),
|
|
403: errResp("`EADMIN`."),
|
|
404: errResp("`ENOTFOUND`."),
|
|
409: errResp("`EBUILTIN`."),
|
|
},
|
|
},
|
|
},
|
|
|
|
"/api/tool-profiles": {
|
|
get: {
|
|
tags: ["Tool Profiles"],
|
|
summary: "List login profiles of every non-Claude tool",
|
|
description:
|
|
"One entry per covered tool. A tool that cannot be switched (Antigravity keeps its login in the OS keyring) is `supported: false` with a `reason`; one tool failing (Oh My Pi not installed) sets that entry's `error` without hiding the others. Identity metadata only, never a token. Staff-only in server mode. Claude's own profiles are `/api/cc-config/profiles`.",
|
|
operationId: "listToolProfiles",
|
|
responses: {
|
|
200: json({ $ref: "#/components/schemas/ToolProfilesListResponse" }, "Profiles by tool."),
|
|
},
|
|
},
|
|
},
|
|
"/api/tool-profiles/{tool}": {
|
|
post: {
|
|
tags: ["Tool Profiles"],
|
|
summary: "Save the tool's live login as a profile",
|
|
description:
|
|
"Snapshots the login the tool holds right now (Codex: `auth.json`; Oh My Pi: `omp auth export`). Without `name`, re-saves into the login's own profile or picks a free name. A second copy of the same account is `EDUPLICATE`; no live login is `ENOAUTH`. Same-origin guarded; admin-only in server mode.",
|
|
operationId: "saveToolProfile",
|
|
parameters: [toolParam],
|
|
requestBody: body({ $ref: "#/components/schemas/ToolProfileNameRequest" }, false),
|
|
responses: { 200: toolOk, ...toolErrors },
|
|
},
|
|
patch: {
|
|
tags: ["Tool Profiles"],
|
|
summary: "Rename a tool login profile",
|
|
description:
|
|
"Changes the display `label` of profile `name` (`ELABELTAKEN` when another profile already uses it).",
|
|
operationId: "renameToolProfile",
|
|
parameters: [toolParam],
|
|
requestBody: body({ $ref: "#/components/schemas/ToolProfileNameRequest" }),
|
|
responses: { 200: toolOk, ...toolErrors },
|
|
},
|
|
delete: {
|
|
tags: ["Tool Profiles"],
|
|
summary: "Delete a tool login profile",
|
|
description:
|
|
"Deletes profile `name` (body). The saved copy of the login in use right now is not deletable (`EACTIVE`).",
|
|
operationId: "deleteToolProfile",
|
|
parameters: [toolParam],
|
|
requestBody: body({ $ref: "#/components/schemas/ToolProfileNameRequest" }),
|
|
responses: {
|
|
200: json(
|
|
{ type: "object", properties: { ok: { type: "boolean" }, name: { type: "string" } } },
|
|
"Deleted."
|
|
),
|
|
...toolErrors,
|
|
},
|
|
},
|
|
},
|
|
"/api/tool-profiles/{tool}/activate": {
|
|
post: {
|
|
tags: ["Tool Profiles"],
|
|
summary: "Switch the tool to a saved login",
|
|
description:
|
|
"Makes profile `name` the tool's live login. The outgoing login is first saved back into its own profile; switching away from a login that was never saved is refused (`EUNSAVED`). Changes the HOST's login — admin-only in server mode, same-origin guarded.",
|
|
operationId: "activateToolProfile",
|
|
parameters: [toolParam],
|
|
requestBody: body({ $ref: "#/components/schemas/ToolProfileNameRequest" }),
|
|
responses: {
|
|
200: json(
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
ok: { type: "boolean" },
|
|
name: { type: "string" },
|
|
savedOutgoing: {
|
|
type: "string",
|
|
nullable: true,
|
|
description: "Profile the outgoing login was saved into.",
|
|
},
|
|
written: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
description: "Files the switch wrote.",
|
|
},
|
|
},
|
|
},
|
|
"Switched."
|
|
),
|
|
...toolErrors,
|
|
},
|
|
},
|
|
},
|
|
|
|
"/api/ai-tools/{id}/models": {
|
|
get: {
|
|
tags: ["AI Tools"],
|
|
summary: "Model choices for a tool",
|
|
description:
|
|
"The models a run of this tool offers, from the tool's own source (Claude's and Codex's lists, else the catalog file's, plus models seen in its recorded token usage), and the user's chosen default. `flag` is false when the tool takes no model flag.",
|
|
operationId: "getAiToolModels",
|
|
parameters: [idParam],
|
|
responses: {
|
|
200: json(
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
flag: { type: "boolean" },
|
|
models: {
|
|
type: "array",
|
|
items: {
|
|
type: "object",
|
|
properties: {
|
|
id: { type: "string" },
|
|
label: { type: "string" },
|
|
hint: { type: "string" },
|
|
},
|
|
},
|
|
},
|
|
defaultModel: { type: "string", nullable: true },
|
|
},
|
|
},
|
|
"Models."
|
|
),
|
|
404: errResp("`ENOTOOL`."),
|
|
},
|
|
},
|
|
},
|
|
"/api/ai-tools/{id}/usage": {
|
|
get: {
|
|
tags: ["AI Tools"],
|
|
summary: "How much a tool has been used",
|
|
description:
|
|
"Count of the tool's sessions and the priced cost of their token usage (same `calculateCost` as everywhere else). A developer in server mode sees only their own sessions.",
|
|
operationId: "getAiToolUsage",
|
|
parameters: [idParam],
|
|
responses: {
|
|
200: json(
|
|
{
|
|
type: "object",
|
|
properties: { sessions: { type: "integer" }, cost: { type: "number" } },
|
|
},
|
|
"Usage."
|
|
),
|
|
404: errResp("`ENOTOOL`."),
|
|
},
|
|
},
|
|
},
|
|
"/api/ai-tools/{id}/config": {
|
|
get: {
|
|
tags: ["AI Tools"],
|
|
summary: "List a tool's editable config files",
|
|
description:
|
|
"The `config` files a RELEASE catalog file declares for this tool (never `DASHBOARD_AI_TOOLS_DIR`), resolved under the home directory, with whether each exists. Staff-only in server mode (they may hold keys).",
|
|
operationId: "listAiToolConfig",
|
|
parameters: [idParam],
|
|
responses: {
|
|
200: json(
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
files: {
|
|
type: "array",
|
|
items: {
|
|
type: "object",
|
|
properties: {
|
|
index: { type: "integer" },
|
|
path: { type: "string" },
|
|
format: { type: "string", example: "json" },
|
|
exists: { type: "boolean" },
|
|
size: { type: "integer", nullable: true },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"Declared files."
|
|
),
|
|
404: errResp("`ENOTOOL`."),
|
|
},
|
|
},
|
|
},
|
|
"/api/ai-tools/{id}/config/{index}": {
|
|
get: {
|
|
tags: ["AI Tools"],
|
|
summary: "Read one config file",
|
|
description:
|
|
"Returns the text of an existing regular text file ≤ 1 MB from the tool's declared list, with its sha to save against. Refuses a credential-named file (`ESECRET`, checked on the basename and its symlink target). The guard is by name only — a settings file may still contain an API key and is returned whole. Staff-only in server mode.",
|
|
operationId: "readAiToolConfig",
|
|
parameters: [idParam, indexParam],
|
|
responses: {
|
|
200: json(
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
path: { type: "string" },
|
|
format: { type: "string" },
|
|
text: { type: "string" },
|
|
sha: { type: "string" },
|
|
},
|
|
},
|
|
"File text."
|
|
),
|
|
...configErrors,
|
|
},
|
|
},
|
|
put: {
|
|
tags: ["AI Tools"],
|
|
summary: "Save one config file",
|
|
description:
|
|
"Overwrites an EXISTING declared file (never creates one) only when `baseSha` matches the file on disk (`ESTALE` otherwise); JSON/JSONC/TOML text must parse. The old text is backed up under `<data dir>/ai-tool-config-backups/` first. Same-origin guarded; admin-only in server mode.",
|
|
operationId: "writeAiToolConfig",
|
|
parameters: [idParam, indexParam],
|
|
requestBody: body({
|
|
type: "object",
|
|
required: ["text", "baseSha"],
|
|
properties: { text: { type: "string" }, baseSha: { type: "string" } },
|
|
}),
|
|
responses: {
|
|
200: json(
|
|
{ type: "object", properties: { sha: { type: "string" }, backup: { type: "string" } } },
|
|
"Saved."
|
|
),
|
|
...configErrors,
|
|
},
|
|
},
|
|
},
|
|
|
|
"/api/cc-config/plugins/available": {
|
|
get: {
|
|
tags: ["CcConfig"],
|
|
summary: "List installable plugins",
|
|
description:
|
|
"Runs `claude plugin list --available --json` (30 s timeout) and returns its installed and available lists. Read-only; skips the CLI mutation queue.",
|
|
operationId: "ccConfigListAvailablePlugins",
|
|
responses: {
|
|
200: json(
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
installed: { type: "array", items: { type: "object" } },
|
|
available: { type: "array", items: { type: "object" } },
|
|
},
|
|
},
|
|
"CLI output."
|
|
),
|
|
502: errResp("`ECLIFAILED` — the CLI failed or printed unparseable JSON."),
|
|
503: errResp("`ECLINOTFOUND`."),
|
|
504: errResp("`ETIMEOUT`."),
|
|
},
|
|
},
|
|
},
|
|
"/api/cc-config/plugins/install": {
|
|
post: {
|
|
tags: ["CcConfig"],
|
|
summary: "Install a plugin",
|
|
description:
|
|
"Runs `claude plugin install <spec> --scope <scope> -y` as an argv array on the serialized CLI queue. `spec` is `<name>` or `<name>@<marketplace>`; `scope` is `user`, `project` or `local`. Admin-only in server mode.",
|
|
operationId: "ccConfigInstallPlugin",
|
|
requestBody: body({
|
|
type: "object",
|
|
required: ["spec", "scope"],
|
|
properties: {
|
|
spec: { type: "string" },
|
|
scope: { type: "string", enum: ["user", "project", "local"] },
|
|
},
|
|
}),
|
|
responses: { 200: cliOk, ...cliErrors },
|
|
},
|
|
},
|
|
"/api/cc-config/plugins/action": {
|
|
post: {
|
|
tags: ["CcConfig"],
|
|
summary: "Enable, disable, update or uninstall a plugin",
|
|
description:
|
|
"Runs `claude plugin <action> <key>` (`update` adds `-y`). With `reload: true` and a successful run, `/reload-plugins` + `/reload-skills` are typed into the open Claude lane panes (`reloaded` in the response); a failed reload never fails the action. Admin-only in server mode.",
|
|
operationId: "ccConfigPluginAction",
|
|
requestBody: body({
|
|
type: "object",
|
|
required: ["key", "action"],
|
|
properties: {
|
|
key: { type: "string", example: "agenthub@agenthub" },
|
|
action: { type: "string", enum: ["enable", "disable", "update", "uninstall"] },
|
|
reload: { type: "boolean" },
|
|
},
|
|
}),
|
|
responses: {
|
|
200: json(
|
|
{
|
|
allOf: [
|
|
{ $ref: "#/components/schemas/CcConfigCliResult" },
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
reloaded: { type: "object", description: "Reload outcome, when requested." },
|
|
},
|
|
},
|
|
],
|
|
},
|
|
"The CLI succeeded."
|
|
),
|
|
...cliErrors,
|
|
},
|
|
},
|
|
},
|
|
"/api/cc-config/marketplaces/add": {
|
|
post: {
|
|
tags: ["CcConfig"],
|
|
summary: "Add a plugin marketplace",
|
|
description:
|
|
"Runs `claude plugin marketplace add <source> --scope <scope>`. `source` is `owner/repo`, an http(s) URL or an absolute path (no `..`, no leading `-`). Admin-only in server mode.",
|
|
operationId: "ccConfigAddMarketplace",
|
|
requestBody: body({
|
|
type: "object",
|
|
required: ["source", "scope"],
|
|
properties: {
|
|
source: { type: "string" },
|
|
scope: { type: "string", enum: ["user", "project", "local"] },
|
|
},
|
|
}),
|
|
responses: { 200: cliOk, ...cliErrors },
|
|
},
|
|
},
|
|
"/api/cc-config/marketplaces/action": {
|
|
post: {
|
|
tags: ["CcConfig"],
|
|
summary: "Remove or update a plugin marketplace",
|
|
description:
|
|
"Runs `claude plugin marketplace remove|update <name>`. Admin-only in server mode.",
|
|
operationId: "ccConfigMarketplaceAction",
|
|
requestBody: body({
|
|
type: "object",
|
|
required: ["name", "action"],
|
|
properties: {
|
|
name: { type: "string" },
|
|
action: { type: "string", enum: ["remove", "update"] },
|
|
},
|
|
}),
|
|
responses: { 200: cliOk, ...cliErrors },
|
|
},
|
|
},
|
|
"/api/cc-config/project-memory": projectMemoryOp(
|
|
"claude",
|
|
"CLAUDE.md",
|
|
"ccConfigCreateProjectMemory",
|
|
"CcConfig"
|
|
),
|
|
"/api/codex-config/project-memory": projectMemoryOp(
|
|
"codex",
|
|
"AGENTS.md",
|
|
"codexConfigCreateProjectMemory",
|
|
"CodexConfig"
|
|
),
|
|
|
|
"/api/git-credentials": {
|
|
get: {
|
|
tags: ["Git Credentials"],
|
|
summary: "List stored git credentials",
|
|
description:
|
|
"One row per protocol + host. Never returns a password — `hasToken` only. Staff-only in server mode.",
|
|
operationId: "listGitCredentials",
|
|
responses: {
|
|
200: json({ $ref: "#/components/schemas/GitCredentialsListResponse" }, "Credentials."),
|
|
},
|
|
},
|
|
post: {
|
|
tags: ["Git Credentials"],
|
|
summary: "Save or replace a git credential",
|
|
description:
|
|
"Stores a username + password/token for `protocol` (`http`/`https`) + plain `host[:port]` in AgentHub's own store (0600), replacing any record for that key. Same-origin guarded; admin-only in server mode. Returns the updated list.",
|
|
operationId: "saveGitCredential",
|
|
requestBody: body({
|
|
type: "object",
|
|
required: ["protocol", "host", "username", "password"],
|
|
properties: {
|
|
protocol: { type: "string", enum: ["http", "https"] },
|
|
host: { type: "string" },
|
|
username: { type: "string" },
|
|
password: { type: "string", description: "Write-only." },
|
|
},
|
|
}),
|
|
responses: {
|
|
200: json({ $ref: "#/components/schemas/GitCredentialsListResponse" }, "Saved."),
|
|
400: errResp("`EBADHOST` or `EBADCRED`."),
|
|
},
|
|
},
|
|
},
|
|
"/api/git-credentials/{protocol}/{host}": {
|
|
delete: {
|
|
tags: ["Git Credentials"],
|
|
summary: "Delete a git credential",
|
|
description:
|
|
"Removes the record for this protocol + host. Same-origin guarded; admin-only in server mode.",
|
|
operationId: "deleteGitCredential",
|
|
parameters: [
|
|
{
|
|
name: "protocol",
|
|
in: "path",
|
|
required: true,
|
|
schema: { type: "string", enum: ["http", "https"] },
|
|
},
|
|
{ name: "host", in: "path", required: true, schema: { type: "string" } },
|
|
],
|
|
responses: {
|
|
200: json(
|
|
{ $ref: "#/components/schemas/GitCredentialsListResponse" },
|
|
"Deleted; the remaining list."
|
|
),
|
|
400: errResp("`EBADHOST`."),
|
|
404: errResp("`ENOTFOUND`."),
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
module.exports = { tags, schemas, paths };
|