/** * @file Supplementary OpenAPI 3.0 fragments for the AI Tools routes mounted at * `/api/ai-tools` (see server/routes/ai-tools.js + server/lib/ai-tools.js): * list the catalog with detection, re-detect after hydrating PATH, * enable/disable a tool or make it the default, run an install/update job, * read npm version badges, and fetch a tool's server-cached icon. Exports `{ tags, schemas, * paths }` for server/openapi-extra.js; schemas are prefixed `AiTools`. * @author Nguyễn Ngọc Trí Vĩ */ const tags = [ { name: "AI Tools", description: "The catalog of AI coding CLIs a lane can run (server/data/ai-tools/*.json + DASHBOARD_AI_TOOLS_DIR), what is installed on PATH, and the user's enable/default choice", }, ]; const errorRef = { $ref: "#/components/schemas/AiToolsError" }; const schemas = { AiToolsTool: { type: "object", required: ["id", "label", "installed", "enabled", "source"], properties: { id: { type: "string", example: "antigravity" }, label: { type: "string", example: "Antigravity" }, command: { type: "string", nullable: true, example: "agy" }, homepageUrl: { type: "string", nullable: true }, installed: { type: "boolean" }, path: { type: "string", nullable: true, example: "/home/u/.local/bin/agy" }, enabled: { type: "boolean" }, source: { type: "string", enum: ["builtin", "remote", "custom"] }, launcher: { type: "string", nullable: true, enum: ["claude", "codex", null] }, prompt: { type: "object", nullable: true, properties: { mode: { type: "string", enum: ["argv", "flag", "after-start"] } }, }, resume: { type: "boolean", description: "Offers Resume last session" }, bypass: { type: "boolean", description: "Offers the per-run Bypass permissions switch" }, proxyCapable: { type: "boolean", description: "Can use an AI Proxy profile" }, install: { type: "object", nullable: true, description: "How Install runs: npm install -g @latest, or a vendor script", properties: { kind: { type: "string", enum: ["npm", "script"] }, package: { type: "string", example: "@openai/codex" }, url: { type: "string", example: "https://cursor.com/install" }, }, }, update: { type: "object", nullable: true, properties: { kind: { type: "string", enum: ["npm", "script", "self"] }, args: { type: "array", items: { type: "string" }, description: "self only" }, }, }, }, }, AiToolsJob: { type: "object", properties: { id: { type: "integer" }, tool: { type: "string", nullable: true, description: "null for update-all" }, action: { type: "string", enum: ["install", "update", "update-all"] }, status: { type: "string", enum: ["running", "ok", "failed"] }, startedAt: { type: "string" }, endedAt: { type: "string", nullable: true }, exitCode: { type: "integer", nullable: true }, log: { type: "string", description: "stdout+stderr, last 256 KB" }, }, }, AiToolsView: { type: "object", required: ["tools", "default"], properties: { tools: { type: "array", items: { $ref: "#/components/schemas/AiToolsTool" } }, default: { type: "string", nullable: true, description: "Effective default tool id" }, catalog: { type: "object", properties: { source: { type: "string", enum: ["builtin", "remote"] }, tag: { type: "string", nullable: true }, fetchedAt: { type: "string", nullable: true }, }, }, }, }, AiToolsError: { type: "object", properties: { error: { type: "object", properties: { code: { type: "string" }, message: { type: "string" } }, }, }, }, }; const ok = { 200: { description: "The catalog with detection and the effective default", content: { "application/json": { schema: { $ref: "#/components/schemas/AiToolsView" } } }, }, }; const paths = { "/api/ai-tools": { get: { tags: ["AI Tools"], summary: "List the AI Tools catalog", description: "Every catalog tool with whether it was found on PATH, whether it is enabled, and the effective default (stored default if installed+enabled, else claude, else the first installed enabled tool, else null). Arg arrays are never returned.", operationId: "aiToolsList", responses: ok, }, }, "/api/ai-tools/refresh": { post: { tags: ["AI Tools"], summary: "Re-detect installed tools", description: "Merges the login shell's PATH ($SHELL -ilc, 5 s timeout, off with AGENTHUB_SHELL_PATH=0) into the server's, re-reads the catalog and re-detects. Same-origin only.", operationId: "aiToolsRefresh", responses: ok, }, }, "/api/ai-tools/{id}": { put: { tags: ["AI Tools"], summary: "Enable/disable a tool or make it the default", description: "A disabled tool is hidden from Add lane and a NEW lane for it answers 400 EDISABLED; existing lanes keep working. Disabling the default clears it. Same-origin only.", operationId: "aiToolsUpdate", parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], requestBody: { required: true, content: { "application/json": { schema: { type: "object", properties: { enabled: { type: "boolean" }, default: { type: "boolean", enum: [true] }, }, }, }, }, }, responses: { ...ok, 400: { description: "EBADDEFAULT (not installed or disabled) or EBADFIELD", content: { "application/json": { schema: errorRef } }, }, 404: { description: "ENOTOOL — unknown id", content: { "application/json": { schema: errorRef } }, }, }, }, }, }; const idParam = [{ name: "id", in: "path", required: true, schema: { type: "string" } }]; const jobStarted = { 202: { description: "The job started; poll GET /api/ai-tools/job", content: { "application/json": { schema: { type: "object", properties: { job: { $ref: "#/components/schemas/AiToolsJob" } }, }, }, }, }, }; const jobErr = (description) => ({ description, content: { "application/json": { schema: errorRef } }, }); Object.assign(paths, { "/api/ai-tools/{id}/install": { post: { tags: ["AI Tools"], summary: "Install a tool", description: "Runs the catalog's install (npm install -g @latest with the login shell's npm, or the vendor script downloaded by the server and run with bash). One job at a time. Same-origin only.", operationId: "aiToolsInstall", parameters: idParam, responses: { ...jobStarted, 400: jobErr("ENOINSTALL"), 404: jobErr("ENOTOOL"), 409: jobErr("EINSTALLED, ENONPM or EBUSY"), }, }, }, "/api/ai-tools/{id}/update": { post: { tags: ["AI Tools"], summary: "Update a tool", description: "npm update only for a tool whose binary lives under npm's global prefix (else ENOTNPM); a self updater runs the tool's own command. Same-origin only.", operationId: "aiToolsUpdateTool", parameters: idParam, responses: { ...jobStarted, 400: jobErr("ENOUPDATE"), 404: jobErr("ENOTOOL"), 409: jobErr("ENOTINSTALLED, ENOTNPM, ENONPM or EBUSY"), }, }, }, "/api/ai-tools/update-all": { post: { tags: ["AI Tools"], summary: "Update every tool with an update", description: "One job: every npm-managed tool with a newer version, then every installed self updater. Same-origin only.", operationId: "aiToolsUpdateAll", responses: { ...jobStarted, 400: jobErr("ENOTHING"), 409: jobErr("EBUSY") }, }, }, "/api/ai-tools/job": { get: { tags: ["AI Tools"], summary: "The current or last install/update job", operationId: "aiToolsJob", responses: { 200: { description: "The job, or null", content: { "application/json": { schema: { $ref: "#/components/schemas/AiToolsJob" } }, }, }, }, }, }, "/api/ai-tools/versions": { get: { tags: ["AI Tools"], summary: "npm version badges", description: "For installed tools with an npm update: whether npm manages them, the installed and latest version, and updateAvailable. Cached 24 h.", operationId: "aiToolsVersions", responses: { 200: { description: "Map of tool id to its versions", content: { "application/json": { schema: { type: "object", additionalProperties: { type: "object", properties: { npmManaged: { type: "boolean" }, installed: { type: "string", nullable: true }, latest: { type: "string", nullable: true }, updateAvailable: { type: "boolean" }, }, }, }, }, }, }, }, }, }, "/api/ai-tools/{id}/icon": { get: { tags: ["AI Tools"], summary: "A tool's icon", description: "The favicon of the tool's faviconDomain (or homepage host), fetched once by the server and cached. Raster only, never SVG; served with nosniff and CSP sandbox.", operationId: "aiToolsIcon", parameters: idParam, responses: { 200: { description: "The image", content: { "image/*": {} } }, 204: { description: "No icon (the client shows a letter avatar)" }, 404: { description: "Unknown tool id" }, }, }, }, }); module.exports = { tags, schemas, paths };