49f87c4da3
Whole tree from parallel sessions, plus the version bump: - Clone: a queue with live progress (Add workspace and extra folders), clones run under nice/ionice so the machine stays responsive. - Settings → AI tools → Session defaults: a new session's default permission mode / bypass per account. - Chat lists the background subagents that have not reported back yet. - A file pasted or dropped into a claude pane goes in as an @ mention. - Changes panel: say why amend/undo is refused on a pushed commit. - openapi.yaml regenerated (Artifacts, Host, resumeById).
360 lines
13 KiB
JavaScript
360 lines
13 KiB
JavaScript
/**
|
|
* @file OpenAPI fragment for the Artifacts hub (`/api/artifacts/*`, server/routes/artifacts.js)
|
|
* and the Administration page's Server tab (`GET /api/host`, server/routes/host.js).
|
|
* Hand-written from the routes; docs/API.md § Artifacts hub is the long form.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const tags = [
|
|
{
|
|
name: "Artifacts",
|
|
description:
|
|
"What agents produced, shared as a SNAPSHOT: a claude.ai page link, a pinned or uploaded file, a skill or a plugin. A viewer reads only the snapshot copy (sandbox CSP, nosniff); an install writes into the caller's own account only. Every write is same-origin.",
|
|
},
|
|
{
|
|
name: "Host",
|
|
description:
|
|
"Administration → Server: the machine's hardware, load, disks and each user's share of CPU and memory. Read-only, staff only in server mode.",
|
|
},
|
|
];
|
|
|
|
const errorCodes =
|
|
"400 EBADURL/EBADPATH/EBADNAME/ENOSKILLMD/ENODESCRIPTION/EBADFIELD/EBADUPLOAD, 403 EFORBIDDEN/ESECRET/EFOREIGN/EINSTALLSOFF/EADMIN, 404 ENOTFOUND/ENOLANE/ENOFILE, 409 EEXISTS/ENOTSHARED/ENOACCOUNT/EUPGRADE, 413 ETOOBIG, 502 EPLUGIN/EMARKETFETCH/ENEEDLOGIN.";
|
|
|
|
const schemas = {
|
|
Artifact: {
|
|
type: "object",
|
|
properties: {
|
|
id: { type: "string" },
|
|
kind: { type: "string", enum: ["page", "file", "skill", "plugin"] },
|
|
title: { type: "string" },
|
|
description: { type: "string", nullable: true },
|
|
ownerId: { type: "integer", nullable: true },
|
|
owner: { type: "string", nullable: true, description: "Display name or username." },
|
|
mine: { type: "boolean" },
|
|
canChange: { type: "boolean" },
|
|
laneId: { type: "integer", nullable: true },
|
|
sessionId: { type: "string", nullable: true },
|
|
source: { type: "string", nullable: true, description: "Plugin: `name@marketplace`." },
|
|
market: { type: "string", nullable: true, description: "Plugin: owner/repo, git URL or marketplace.json URL." },
|
|
url: { type: "string", nullable: true, description: "Page: a https://claude.ai/… link." },
|
|
files: { type: "array", items: { type: "object" }, description: "The files the snapshot recorded." },
|
|
size: { type: "integer", nullable: true },
|
|
shared: { type: "boolean" },
|
|
sharedAt: { type: "string", nullable: true },
|
|
installs: { type: "integer" },
|
|
createdAt: { type: "string" },
|
|
updatedAt: { type: "string" },
|
|
},
|
|
},
|
|
};
|
|
|
|
const json = (properties, required = []) => ({
|
|
required: true,
|
|
content: { "application/json": { schema: { type: "object", required, properties } } },
|
|
});
|
|
const multipart = (properties, required = []) => ({
|
|
required: true,
|
|
content: { "multipart/form-data": { schema: { type: "object", required, properties } } },
|
|
});
|
|
const item = {
|
|
type: "object",
|
|
properties: { item: { $ref: "#/components/schemas/Artifact" } },
|
|
};
|
|
const items = {
|
|
type: "object",
|
|
properties: { items: { type: "array", items: { $ref: "#/components/schemas/Artifact" } } },
|
|
};
|
|
const idParam = { name: "id", in: "path", required: true, schema: { type: "string" } };
|
|
const laneQuery = { name: "laneId", in: "query", required: false, schema: { type: "integer" } };
|
|
const errs = { description: `Error \`{error:{code,message}}\`: ${errorCodes}` };
|
|
const created = (schema, description = "Created.") => ({
|
|
201: { description, content: { "application/json": { schema } } },
|
|
400: errs,
|
|
});
|
|
|
|
const paths = {
|
|
"/api/artifacts": {
|
|
get: {
|
|
tags: ["Artifacts"],
|
|
summary: "List artifacts",
|
|
operationId: "listArtifacts",
|
|
description:
|
|
"A developer sees their own rows plus shared ones; staff see every row in `hub` scope.",
|
|
parameters: [
|
|
{ name: "scope", in: "query", schema: { type: "string", enum: ["mine", "hub"] } },
|
|
{ name: "kind", in: "query", schema: { type: "string", enum: ["page", "file", "skill", "plugin"] } },
|
|
{ name: "q", in: "query", schema: { type: "string" } },
|
|
laneQuery,
|
|
{ name: "sessionId", in: "query", schema: { type: "string" } },
|
|
],
|
|
responses: { 200: { description: "`{items}`.", content: { "application/json": { schema: items } } } },
|
|
},
|
|
},
|
|
"/api/artifacts/settings": {
|
|
get: {
|
|
tags: ["Artifacts"],
|
|
summary: "Hub switches and what the caller may do",
|
|
operationId: "getArtifactSettings",
|
|
responses: {
|
|
200: {
|
|
description: "`{serverMode, canShare, installsEnabled, canManage}`.",
|
|
content: {
|
|
"application/json": {
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
serverMode: { type: "boolean" },
|
|
canShare: { type: "boolean" },
|
|
installsEnabled: { type: "boolean" },
|
|
canManage: { type: "boolean" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
put: {
|
|
tags: ["Artifacts"],
|
|
summary: "Turn installs on or off (admin; any caller in local mode)",
|
|
operationId: "setArtifactSettings",
|
|
description: "Audited in server mode (Settings → AI tools → Artifacts Hub).",
|
|
requestBody: json({ installsEnabled: { type: "boolean" } }, ["installsEnabled"]),
|
|
responses: {
|
|
200: { description: "`{installsEnabled}`." },
|
|
400: { description: "EBADFIELD" },
|
|
403: { description: "EADMIN / EBADORIGIN" },
|
|
},
|
|
},
|
|
},
|
|
"/api/artifacts/candidates": {
|
|
get: {
|
|
tags: ["Artifacts"],
|
|
summary: "Documents a workspace's sessions wrote (to pin)",
|
|
operationId: "listArtifactCandidates",
|
|
parameters: [laneQuery],
|
|
responses: {
|
|
200: { description: "`{items: [{path, sessionId, at, exists, pinned}]}`." },
|
|
404: errs,
|
|
},
|
|
},
|
|
},
|
|
"/api/artifacts/skills": {
|
|
get: {
|
|
tags: ["Artifacts"],
|
|
summary: "Skills the caller can share (user + workspace)",
|
|
operationId: "listArtifactSkills",
|
|
parameters: [laneQuery],
|
|
responses: { 200: { description: '`{items: [{name, from: "user"|"workspace"}]}`.' } },
|
|
},
|
|
},
|
|
"/api/artifacts/page": {
|
|
post: {
|
|
tags: ["Artifacts"],
|
|
summary: "Add a claude.ai page link",
|
|
operationId: "addArtifactPage",
|
|
description: "Only `https://claude.ai/…` is accepted; one row per owner and link.",
|
|
requestBody: json(
|
|
{ url: { type: "string" }, title: { type: "string" }, description: { type: "string" } },
|
|
["url"]
|
|
),
|
|
responses: created(item),
|
|
},
|
|
},
|
|
"/api/artifacts/file": {
|
|
post: {
|
|
tags: ["Artifacts"],
|
|
summary: "Pin a workspace file (snapshotted)",
|
|
operationId: "pinArtifactFile",
|
|
description:
|
|
"Read under the download route's guards (`lanePath`, `resolveInLane`, `ESECRET`, ≤ 25 MB) and copied once.",
|
|
requestBody: json(
|
|
{
|
|
laneId: { type: "integer" },
|
|
path: { type: "string" },
|
|
title: { type: "string" },
|
|
sessionId: { type: "string" },
|
|
},
|
|
["laneId", "path"]
|
|
),
|
|
responses: created(item),
|
|
},
|
|
},
|
|
"/api/artifacts/file/upload": {
|
|
post: {
|
|
tags: ["Artifacts"],
|
|
summary: "Keep one uploaded file (≤ 25 MB)",
|
|
operationId: "uploadArtifactFile",
|
|
requestBody: multipart({
|
|
files: { type: "string", format: "binary" },
|
|
title: { type: "string" },
|
|
description: { type: "string" },
|
|
}, ["files"]),
|
|
responses: created(item),
|
|
},
|
|
},
|
|
"/api/artifacts/skill": {
|
|
post: {
|
|
tags: ["Artifacts"],
|
|
summary: "Share an existing skill",
|
|
operationId: "addArtifactSkill",
|
|
requestBody: json(
|
|
{
|
|
name: { type: "string" },
|
|
laneId: { type: "integer" },
|
|
title: { type: "string" },
|
|
description: { type: "string" },
|
|
},
|
|
["name"]
|
|
),
|
|
responses: created(item),
|
|
},
|
|
},
|
|
"/api/artifacts/skill/upload": {
|
|
post: {
|
|
tags: ["Artifacts"],
|
|
summary: "Skills from files: a file, a folder, a .zip/.skill or several skills",
|
|
operationId: "uploadArtifactSkills",
|
|
description:
|
|
"`paths` is a JSON array of each file's relative path. Zip sizes are read before inflating; nothing is stored if any skill fails.",
|
|
requestBody: multipart({
|
|
files: { type: "array", items: { type: "string", format: "binary" } },
|
|
paths: { type: "string" },
|
|
name: { type: "string" },
|
|
title: { type: "string" },
|
|
description: { type: "string" },
|
|
}, ["files"]),
|
|
responses: created(items),
|
|
},
|
|
},
|
|
"/api/artifacts/skill/new": {
|
|
post: {
|
|
tags: ["Artifacts"],
|
|
summary: "Write a skill in the dialog",
|
|
operationId: "writeArtifactSkill",
|
|
requestBody: json(
|
|
{
|
|
name: { type: "string" },
|
|
description: { type: "string" },
|
|
content: { type: "string" },
|
|
title: { type: "string" },
|
|
},
|
|
["name", "description", "content"]
|
|
),
|
|
responses: created({ type: "object", properties: { item: item.properties.item, items: items.properties.items } }),
|
|
},
|
|
},
|
|
"/api/artifacts/plugin/inspect": {
|
|
post: {
|
|
tags: ["Artifacts"],
|
|
summary: "Read a marketplace's plugin list",
|
|
operationId: "inspectArtifactMarket",
|
|
description: "Clones a user-named https link only to read `marketplace.json`; no credential is ever sent.",
|
|
requestBody: json({ market: { type: "string" } }, ["market"]),
|
|
responses: {
|
|
200: { description: "`{market, name, description, plugins: [{name, key, description, version, category}]}`." },
|
|
502: errs,
|
|
},
|
|
},
|
|
},
|
|
"/api/artifacts/plugin": {
|
|
post: {
|
|
tags: ["Artifacts"],
|
|
summary: "Share a plugin from a marketplace",
|
|
operationId: "addArtifactPlugin",
|
|
requestBody: json(
|
|
{
|
|
key: { type: "string", description: "`name@marketplace`." },
|
|
market: { type: "string" },
|
|
title: { type: "string" },
|
|
description: { type: "string" },
|
|
},
|
|
["key", "market"]
|
|
),
|
|
responses: created(item),
|
|
},
|
|
},
|
|
"/api/artifacts/{id}": {
|
|
get: {
|
|
tags: ["Artifacts"],
|
|
summary: "One artifact",
|
|
operationId: "getArtifact",
|
|
parameters: [idParam],
|
|
responses: { 200: { description: "`{item}`.", content: { "application/json": { schema: item } } }, 404: errs },
|
|
},
|
|
patch: {
|
|
tags: ["Artifacts"],
|
|
summary: "Rename, describe, share or unshare (owner or admin)",
|
|
operationId: "updateArtifact",
|
|
parameters: [idParam],
|
|
requestBody: json({
|
|
title: { type: "string" },
|
|
description: { type: "string" },
|
|
shared: { type: "boolean" },
|
|
source: { type: "string", description: "Plugin only." },
|
|
market: { type: "string", description: "Plugin only." },
|
|
}),
|
|
responses: { 200: { description: "`{item}`.", content: { "application/json": { schema: item } } }, 403: errs, 404: errs },
|
|
},
|
|
delete: {
|
|
tags: ["Artifacts"],
|
|
summary: "Delete an artifact and its snapshot (owner or admin)",
|
|
operationId: "deleteArtifact",
|
|
parameters: [idParam],
|
|
responses: { 200: { description: "`{ok: true}`." }, 403: errs, 404: errs },
|
|
},
|
|
},
|
|
"/api/artifacts/{id}/raw": {
|
|
get: {
|
|
tags: ["Artifacts"],
|
|
summary: "One recorded file of a snapshot",
|
|
operationId: "getArtifactRaw",
|
|
description:
|
|
"Always served under a `sandbox` CSP with `nosniff`; an unlisted type, or `download=1`, is an attachment. Only a file the snapshot recorded can be named.",
|
|
parameters: [
|
|
idParam,
|
|
{ name: "path", in: "query", required: true, schema: { type: "string" } },
|
|
{ name: "download", in: "query", schema: { type: "string", enum: ["1"] } },
|
|
],
|
|
responses: { 200: { description: "The file bytes." }, 400: errs, 404: errs },
|
|
},
|
|
},
|
|
"/api/artifacts/{id}/install": {
|
|
post: {
|
|
tags: ["Artifacts"],
|
|
summary: "Install a shared skill or plugin into the caller's own account",
|
|
operationId: "installArtifact",
|
|
description:
|
|
"A skill goes to the caller's `~/.claude/skills/` (never over an existing one without `replace`; the old one is moved aside). A plugin runs `claude plugin install --scope user` as the caller.",
|
|
parameters: [idParam],
|
|
requestBody: json({ replace: { type: "boolean" } }),
|
|
responses: {
|
|
200: { description: "`{installed, replaced?}`." },
|
|
403: errs,
|
|
409: errs,
|
|
502: errs,
|
|
},
|
|
},
|
|
},
|
|
"/api/host": {
|
|
get: {
|
|
tags: ["Host"],
|
|
summary: "The machine and each user's live share of it",
|
|
operationId: "getHost",
|
|
description:
|
|
"Cached 3 s. `usage` is null off Linux. Server mode: admin/analyst only (403 EADMIN for a developer).",
|
|
responses: {
|
|
200: {
|
|
description:
|
|
"`{at, hostname, os, uptime, cpu, memory, disks, homesMount, gpus, usage}` — see docs/API.md.",
|
|
content: { "application/json": { schema: { type: "object" } } },
|
|
},
|
|
500: { description: "EHOST" },
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
module.exports = { tags, schemas, paths };
|