Files
AgentHub/server/routes/tool-profiles.js
T
nntrivi2001 3113479cd2 feat(settings): every AI tool's login profiles in one Settings section, linked to the account pool
Settings → AI tools → Profiles holds one tab per tool: Claude Code (the
cc-profiles panel, moved out of the Claude Code page's modal — its button
now links here), Codex (the whole $CODEX_HOME/auth.json) and Oh My Pi
(`omp auth export` / `omp auth import -`, secrets on stdin; omp's credential
tables are emptied first because import merges, and a failed import puts
the outgoing set back). Antigravity is listed as not switchable: agy keeps
its login in the OS keyring.

server/lib/tool-profiles.js + /api/tool-profiles keep cc-profiles' rules
(one login = one profile, the outgoing login is re-saved before a switch,
an unsaved one refuses it, the live login's profile is not deletable) and
never return a token; /tool-profiles joins ADMIN_WRITE_PREFIXES and
STAFF_ONLY_PREFIXES, writes are same-origin only.

The account pool takes a saved Codex profile too (moved in like a Claude
one, GET /api/accounts/profiles?tool=codex). Each Claude/Codex profile has
a "Move to pool" link (/admin?poolAdd=&poolProfile= opens the pool's Add
dialog on it), Settings shows the pool's size, and the pool card links back.
2026-10-22 11:11:38 +07:00

84 lines
2.5 KiB
JavaScript

/**
* @file tool-profiles.js
* @description `/api/tool-profiles` — the login profiles of the AI tools other
* than Claude Code (server/lib/tool-profiles.js; Claude's stay under
* `/api/cc-config/profiles`, and Settings → AI tools → Profiles shows both).
* `GET /` lists every covered tool (`supported: false` + `reason` for one that
* cannot be switched); `POST /:tool` saves the live login, `PATCH /:tool`
* renames, `POST /:tool/activate` switches, `DELETE /:tool` deletes — the
* profile name always in the body. Identity metadata only, never a token. The
* writes change the HOST's own login, so `/tool-profiles` is in
* ADMIN_WRITE_PREFIXES and STAFF_ONLY_PREFIXES like `/cc-config`.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { Router } = require("express");
const profiles = require("../lib/tool-profiles");
const { sameOriginGuard } = require("../lib/same-origin");
const router = Router();
// Every write swaps a live login: never from a cross-site form.
router.use((req, res, next) => (req.method === "GET" ? next() : sameOriginGuard(req, res, next)));
const STATUS = {
EBADNAME: 400,
EBADLABEL: 400,
EBADTOOL: 404,
ENOTFOUND: 404,
EUNSUPPORTED: 409,
ENOTINSTALLED: 409,
EBADJSON: 409,
EACTIVE: 409,
EUNSAVED: 409,
ENOAUTH: 409,
EDUPLICATE: 409,
ELABELTAKEN: 409,
EOMP: 502,
};
const handle = (fn) => async (req, res) => {
try {
res.json(await fn(req));
} catch (e) {
res
.status(STATUS[e.code] || 500)
.json({ error: { code: e.code || "EINTERNAL", message: e.message } });
}
};
router.get(
"/",
handle(() => ({
tools: profiles.tools().map((t) => {
if (!t.supported) return { ...t, items: [], active: null };
try {
return { ...t, ...profiles.listProfiles(t.tool), error: null };
} catch (e) {
// One tool's failure (omp not installed) never hides the others.
return { ...t, items: [], active: null, error: { code: e.code, message: e.message } };
}
}),
}))
);
const name = (req) => (req.body || {}).name;
router.post(
"/:tool",
handle((req) => profiles.saveProfile(req.params.tool, name(req)))
);
router.patch(
"/:tool",
handle((req) => profiles.renameProfile(req.params.tool, name(req), (req.body || {}).label))
);
router.post(
"/:tool/activate",
handle((req) => profiles.activateProfile(req.params.tool, name(req)))
);
router.delete(
"/:tool",
handle((req) => profiles.deleteProfile(req.params.tool, name(req)))
);
module.exports = router;