feat(ai-tools): install/update AI CLIs, remote catalog sync, cached favicons (#40 A2)
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* @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, and
|
||||
* enable/disable a tool or make it the default. Exports `{ tags, schemas,
|
||||
* 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ĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
@@ -29,7 +30,7 @@ const schemas = {
|
||||
installed: { type: "boolean" },
|
||||
path: { type: "string", nullable: true, example: "/home/u/.local/bin/agy" },
|
||||
enabled: { type: "boolean" },
|
||||
source: { type: "string", enum: ["builtin", "custom"] },
|
||||
source: { type: "string", enum: ["builtin", "remote", "custom"] },
|
||||
launcher: { type: "string", nullable: true, enum: ["claude", "codex", null] },
|
||||
prompt: {
|
||||
type: "object",
|
||||
@@ -39,6 +40,37 @@ const schemas = {
|
||||
resume: { type: "boolean", description: "Offers Resume last session" },
|
||||
bypass: { type: "boolean", description: "Offers the per-run Bypass permissions switch" },
|
||||
routerCapable: { type: "boolean", description: "Can use an AI Router profile" },
|
||||
install: {
|
||||
type: "object",
|
||||
nullable: true,
|
||||
description: "How Install runs: npm install -g <package>@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: {
|
||||
@@ -47,6 +79,14 @@ const schemas = {
|
||||
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: {
|
||||
@@ -125,4 +165,128 @@ const paths = {
|
||||
},
|
||||
};
|
||||
|
||||
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 <package>@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 };
|
||||
|
||||
Reference in New Issue
Block a user