Download web/src/lib/api.ts from SaylorTwift/hermes-agent: direct link, hf CLI and curl.
- Browser
- Download file 88.7 kB
-
https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/web/src/lib/api.ts
- Command line
-
hf download hf://SaylorTwift/hermes-agent/web/src/lib/api.ts
-
curl -L -o api.ts https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/web/src/lib/api.ts
88.7 kB
| import { | |
| buildHermesWebSocketUrl, | |
| type ModelOptionProvider, | |
| type ModelOptionsResult, | |
| } from "@hermes/shared"; | |
| // The dashboard can be served either at the root of its host (e.g. | |
| // https://kanban.tilos.com/) or under a URL prefix when reverse-proxied | |
| // (e.g. https://mission-control.tilos.com/hermes/). The Python backend | |
| // injects ``window.__HERMES_BASE_PATH__`` into index.html based on the | |
| // incoming ``X-Forwarded-Prefix`` header so the SPA can address its own | |
| // ``/api/...`` and ``/dashboard-plugins/...`` URLs correctly without a | |
| // rebuild. Empty string means "served at root". | |
| function readBasePath(): string { | |
| if (typeof window === "undefined") return ""; | |
| const raw = window.__HERMES_BASE_PATH__ ?? ""; | |
| if (!raw) return ""; | |
| // Normalise: ensure leading slash, strip trailing slash. | |
| const withLead = raw.startsWith("/") ? raw : `/${raw}`; | |
| return withLead.replace(/\/+$/, ""); | |
| } | |
| export const HERMES_BASE_PATH = readBasePath(); | |
| const BASE = HERMES_BASE_PATH; | |
| import type { DashboardTheme } from "@/themes/types"; | |
| import { | |
| attemptDashboardTokenReloadOnce, | |
| clearDashboardTokenReloadAttempt, | |
| } from "@/lib/dashboard-auth-reload"; | |
| // Ephemeral session token for protected endpoints. | |
| // Injected into index.html by the server β never fetched via API. | |
| declare global { | |
| interface Window { | |
| __HERMES_SESSION_TOKEN__?: string; | |
| __HERMES_BASE_PATH__?: string; | |
| /** Server-injected flag: ``true`` when the dashboard's OAuth gate is | |
| * engaged (public bind, no ``--insecure``). Toggles the SPA's | |
| * WS-upgrade path from legacy ``?token=`` to single-use ``?ticket=`` | |
| * fetched via :func:`getWsTicket`. */ | |
| __HERMES_AUTH_REQUIRED__?: boolean; | |
| } | |
| } | |
| const SESSION_HEADER = "X-Hermes-Session-Token"; | |
| function setSessionHeader(headers: Headers, token: string): void { | |
| if (!headers.has(SESSION_HEADER)) { | |
| headers.set(SESSION_HEADER, token); | |
| } | |
| } | |
| // ββ Global management-profile scope ββββββββββββββββββββββββββββββββββ | |
| // The dashboard is a machine-level management surface: one header switcher | |
| // (ProfileProvider in App.tsx) decides which profile the management pages | |
| // read/write, and fetchJSON transparently appends ?profile=<name> to the | |
| // profile-scoped endpoint families below. "" = the dashboard process's own | |
| // profile (legacy behavior). Calls that already carry an explicit profile | |
| // (e.g. ProfileBuilder writes) are left untouched β explicit beats global. | |
| let _managementProfile = ""; | |
| export function setManagementProfile(name: string): void { | |
| _managementProfile = (name || "").trim(); | |
| } | |
| export function getManagementProfile(): string { | |
| return _managementProfile; | |
| } | |
| // Endpoint families that honor ?profile= on the backend (web_server.py | |
| // _profile_scope or explicit per-profile DB opens). Anything else β ops, | |
| // cron (which has its own per-job profile params), profiles themselves β is | |
| // machine-global or self-scoped and must NOT be rewritten. | |
| const PROFILE_SCOPED_PREFIXES = [ | |
| "/api/status", | |
| "/api/gateway", | |
| "/api/analytics", | |
| "/api/skills", | |
| "/api/tools/toolsets", | |
| "/api/config", | |
| "/api/env", | |
| "/api/mcp", | |
| "/api/messaging/platforms", | |
| "/api/messaging/telegram/onboarding", | |
| "/api/messaging/whatsapp/onboarding", | |
| // OAuth/account state is profile-owned too: status, login sessions, polling, | |
| // cancellation, and disconnect must all follow the selected management | |
| // profile rather than silently targeting the dashboard process's profile. | |
| "/api/providers/oauth", | |
| "/api/model/info", | |
| "/api/model/set", | |
| "/api/model/auxiliary", | |
| "/api/model/moa", | |
| "/api/model/options", | |
| // A named profile keeps its own pairing whitelist, and its gateway only | |
| // consults that one β approving into the global store would grant access | |
| // the running gateway never sees. | |
| "/api/pairing", | |
| ]; | |
| function withManagementProfile(url: string): string { | |
| if (!_managementProfile) return url; | |
| if (url.includes("profile=")) return url; // explicit param wins | |
| const path = url.split("?")[0]; | |
| if (!PROFILE_SCOPED_PREFIXES.some((p) => path.startsWith(p))) return url; | |
| const sep = url.includes("?") ? "&" : "?"; | |
| return `${url}${sep}profile=${encodeURIComponent(_managementProfile)}`; | |
| } | |
| export async function fetchJSON<T>( | |
| url: string, | |
| init?: RequestInit, | |
| options?: FetchJSONOptions, | |
| ): Promise<T> { | |
| url = withManagementProfile(url); | |
| // Inject the session token into all /api/ requests. | |
| const headers = new Headers(init?.headers); | |
| const token = window.__HERMES_SESSION_TOKEN__; | |
| if (token) { | |
| setSessionHeader(headers, token); | |
| } | |
| const res = await fetch(`${BASE}${url}`, { | |
| ...init, | |
| headers, | |
| // ``credentials: 'include'`` so the cookie-auth path (gated mode) works | |
| // for any fetch routed through here. Loopback mode is unaffected β the | |
| // server doesn't read cookies and the legacy session-token header is | |
| // already attached above. | |
| credentials: init?.credentials ?? "include", | |
| }); | |
| if (res.status === 401) { | |
| // Phase 6: the gated middleware emits a structured envelope so the | |
| // SPA can full-page-navigate to /login on session expiry. Parse it, | |
| // and only redirect on the known error codes β domain-level 401s | |
| // (e.g. "you don't have permission to read this monitor") bubble | |
| // up as regular errors so callers can handle them. | |
| let body: { error?: string; login_url?: string } = {}; | |
| try { | |
| body = await res.clone().json(); | |
| } catch { | |
| /* non-JSON 401 β let it fall through */ | |
| } | |
| if ( | |
| (body.error === "unauthenticated" || body.error === "session_expired") && | |
| body.login_url | |
| ) { | |
| // Preserve where the user was so /auth/callback can land them back | |
| // after re-auth. The gate's login_url already carries a ``next=`` | |
| // built from the request path, but the SPA may be deep inside a | |
| // SPA route the gate never saw β e.g. a hash route or a client-side | |
| // /sessions/<id> deep link. Save the current location as a | |
| // fallback the post-login handler can read. | |
| try { | |
| sessionStorage.setItem( | |
| "hermes.lastLocation", | |
| window.location.pathname + window.location.search, | |
| ); | |
| } catch { | |
| /* SSR / privacy mode β ignore */ | |
| } | |
| window.location.assign(body.login_url); | |
| // Never resolve β the page is about to unload. | |
| return new Promise<T>(() => {}); | |
| } | |
| // Loopback mode: ``_SESSION_TOKEN`` rotates on every server restart | |
| // (``hermes update``, ``hermes gateway restart``, etc.). A tab kept | |
| // open across the restart holds the OLD token in | |
| // ``window.__HERMES_SESSION_TOKEN__`` from the previous HTML render, | |
| // so every fetch returns 401. The HTML is served ``Cache-Control: | |
| // no-store`` so a reload picks up the freshly-injected token. Trigger | |
| // that reload once on the first stale-token 401 β gated mode is | |
| // handled above, so reaching here in gated mode means a real | |
| // middleware failure that should not reload-loop. | |
| if (!window.__HERMES_AUTH_REQUIRED__ && !options?.allowUnauthorized) { | |
| if (attemptDashboardTokenReloadOnce()) { | |
| return new Promise<T>(() => {}); | |
| } | |
| } | |
| } | |
| if (res.ok) { | |
| // Clear the stale-token reload guard: a successful 2xx proves the | |
| // current ``window.__HERMES_SESSION_TOKEN__`` is valid, so the next | |
| // 401 β if any β should be allowed to trigger its own reload cycle. | |
| clearDashboardTokenReloadAttempt(); | |
| } | |
| if (!res.ok) { | |
| const text = await res.text().catch(() => res.statusText); | |
| throw new Error(`${res.status}: ${text}`); | |
| } | |
| return res.json(); | |
| } | |
| /** Encode a plugin registry key for URL paths (preserves `/` segment separators). */ | |
| function pluginPath(name: string): string { | |
| return name.split("/").map(encodeURIComponent).join("/"); | |
| } | |
| /** | |
| * Fetch a single-use ticket for a WebSocket upgrade in gated mode. | |
| * | |
| * The dashboard's gated-mode WS auth (``hermes_cli.web_server._ws_auth_ok``) | |
| * rejects the legacy ``?token=<_SESSION_TOKEN>`` path and only accepts | |
| * ``?ticket=<minted>`` consumed against the in-memory ticket store. Browsers | |
| * can't set ``Authorization`` on a WS upgrade, so this round-trip via the | |
| * authenticated REST endpoint is the bridge from cookie auth to WS auth. | |
| * | |
| * Tickets are single-use and TTL=30s β every WS connect attempt must | |
| * fetch a fresh ticket. | |
| */ | |
| export async function getWsTicket(): Promise<{ ticket: string; ttl_seconds: number }> { | |
| const res = await fetch(`${BASE}/api/auth/ws-ticket`, { | |
| method: "POST", | |
| credentials: "include", | |
| }); | |
| if (!res.ok) { | |
| throw new Error(`/api/auth/ws-ticket: HTTP ${res.status}`); | |
| } | |
| return res.json(); | |
| } | |
| /** | |
| * Resolve the auth query-param pair (``[name, value]``) for a WebSocket | |
| * connect. In gated mode mints a fresh single-use ticket; in loopback | |
| * mode returns the injected session token. | |
| */ | |
| export async function buildWsAuthParam(): Promise<[string, string]> { | |
| if (window.__HERMES_AUTH_REQUIRED__) { | |
| const { ticket } = await getWsTicket(); | |
| return ["ticket", ticket]; | |
| } | |
| const token = window.__HERMES_SESSION_TOKEN__ ?? ""; | |
| return ["token", token]; | |
| } | |
| /** | |
| * Authenticated ``fetch`` for dashboard ``/api/...`` requests that aren't | |
| * plain JSON β file uploads (``FormData``), binary downloads (blobs), etc. | |
| * Mirrors ``fetchJSON``'s auth handling but returns the raw ``Response`` so | |
| * the caller can read ``.blob()`` / ``.formData()`` / stream it. | |
| * | |
| * Auth, in both modes, exactly as ``fetchJSON`` does it: | |
| * - loopback / ``--insecure``: attach the ``X-Hermes-Session-Token`` header. | |
| * - gated OAuth: no token header (it's absent by design); the | |
| * ``hermes_session_at`` cookie rides along via ``credentials: 'include'``. | |
| * | |
| * Unlike ``fetchJSON`` this does NOT parse the body, does NOT throw on | |
| * non-2xx (the caller decides β a 404 on a download is meaningful), and | |
| * does NOT run the global 401 β /login redirect (binary endpoints aren't | |
| * navigation targets). Callers that want the redirect behaviour should use | |
| * ``fetchJSON``. | |
| */ | |
| export async function authedFetch( | |
| url: string, | |
| init?: RequestInit, | |
| ): Promise<Response> { | |
| const headers = new Headers(init?.headers); | |
| const token = window.__HERMES_SESSION_TOKEN__; | |
| if (token) { | |
| setSessionHeader(headers, token); | |
| } | |
| return fetch(`${BASE}${url}`, { | |
| ...init, | |
| headers, | |
| credentials: init?.credentials ?? "include", | |
| }); | |
| } | |
| /** | |
| * Build an absolute ``ws(s)://`` URL for a dashboard WebSocket endpoint, | |
| * with the correct auth query param appended for the active mode (fresh | |
| * single-use ``ticket`` in gated mode, ``token`` in loopback). Plugins and | |
| * the SPA should use this instead of hand-assembling a WS URL + reading | |
| * ``window.__HERMES_SESSION_TOKEN__`` directly, so the gated-mode ticket | |
| * path can never be forgotten. | |
| * | |
| * ``path`` is the dashboard-relative path (e.g. | |
| * ``"/api/plugins/kanban/events"``); the base-path prefix and host are | |
| * applied here. Extra query params can be supplied via ``params`` and are | |
| * merged before the auth param. | |
| */ | |
| export async function buildWsUrl( | |
| path: string, | |
| params?: Record<string, string>, | |
| ): Promise<string> { | |
| return buildHermesWebSocketUrl({ | |
| authParam: await buildWsAuthParam(), | |
| basePath: BASE, | |
| params, | |
| path, | |
| }); | |
| } | |
| /** Build a ``?profile=<name>`` query suffix, or "" when unset. | |
| * | |
| * Used by the skills/toolsets endpoints so the dashboard can manage a | |
| * profile other than the one the server process runs under. */ | |
| function profileQuery(profile?: string): string { | |
| return profile ? `?profile=${encodeURIComponent(profile)}` : ""; | |
| } | |
| function appendProfileParam(url: string, profile?: string): string { | |
| if (!profile || url.includes("profile=")) return url; | |
| return `${url}${url.includes("?") ? "&" : "?"}profile=${encodeURIComponent(profile)}`; | |
| } | |
| function appendQueryParam(url: string, key: string, value?: string): string { | |
| if (!value) return url; | |
| return `${url}${url.includes("?") ? "&" : "?"}${key}=${encodeURIComponent(value)}`; | |
| } | |
| export interface SessionQueryOptions { | |
| profile?: string; | |
| order?: "created" | "recent"; | |
| source?: string | null; | |
| sources?: string[]; | |
| excludeSources?: string[]; | |
| } | |
| function normalizeSessionQueryOptions( | |
| profileOrOptions?: string | SessionQueryOptions, | |
| order: "created" | "recent" = "created", | |
| ): SessionQueryOptions { | |
| if (typeof profileOrOptions === "string") { | |
| return { profile: profileOrOptions, order }; | |
| } | |
| return { | |
| profile: getManagementProfile(), | |
| order, | |
| ...(profileOrOptions ?? {}), | |
| }; | |
| } | |
| function appendSessionFilters(url: string, options: SessionQueryOptions): string { | |
| let next = url; | |
| next = appendQueryParam(next, "source", options.source ?? undefined); | |
| if (options.sources && options.sources.length > 0) { | |
| next = appendQueryParam(next, "sources", options.sources.join(",")); | |
| } | |
| if (options.excludeSources && options.excludeSources.length > 0) { | |
| next = appendQueryParam(next, "exclude_sources", options.excludeSources.join(",")); | |
| } | |
| return appendProfileParam(next, options.profile); | |
| } | |
| export const api = { | |
| buildWsUrl, | |
| getStatus: () => fetchJSON<StatusResponse>("/api/status"), | |
| /** | |
| * Identity probe for the dashboard auth gate (Phase 7). | |
| * | |
| * Returns the verified Session as JSON when gated mode is active and a | |
| * valid cookie is attached. Loopback mode is unaffected β the endpoint | |
| * still exists but is never useful there (no Session, no cookie). The | |
| * AuthWidget component swallows 401s from this call: if the gate isn't | |
| * engaged, /api/auth/me returns 401 and the widget renders nothing. | |
| * | |
| * ``allowUnauthorized`` is load-bearing: in loopback mode this endpoint | |
| * 401s by design, and fetchJSON's default loopback behaviour treats a | |
| * 401 as a rotated session token and full-page-reloads to pick up a | |
| * fresh one. Because every *other* dashboard request succeeds (and so | |
| * clears the one-shot reload guard), that turns this expected 401 into | |
| * an infinite reload loop. Opting out keeps the 401 a plain throw the | |
| * widget can catch. | |
| */ | |
| getAuthMe: () => | |
| fetchJSON<AuthMeResponse>("/api/auth/me", undefined, { | |
| allowUnauthorized: true, | |
| }), | |
| logout: () => | |
| fetch(`${BASE}/auth/logout`, { | |
| method: "POST", | |
| credentials: "include", | |
| }).then((r) => { | |
| // /auth/logout returns 302 β /login. Follow that with a full-page | |
| // navigation rather than letting fetch() opaquely consume the | |
| // redirect β the SPA needs to leave the protected area. | |
| window.location.assign("/login"); | |
| return r; | |
| }), | |
| getSessions: ( | |
| limit = 20, | |
| offset = 0, | |
| profileOrOptions: string | SessionQueryOptions = getManagementProfile(), | |
| order: "created" | "recent" = "created", | |
| ) => { | |
| const options = normalizeSessionQueryOptions(profileOrOptions, order); | |
| return fetchJSON<PaginatedSessions>( | |
| appendSessionFilters( | |
| `/api/sessions?limit=${limit}&offset=${offset}&order=${options.order ?? order}`, | |
| options, | |
| ), | |
| ); | |
| }, | |
| getSessionMessages: (id: string, profile = getManagementProfile()) => | |
| fetchJSON<SessionMessagesResponse>( | |
| appendProfileParam( | |
| `/api/sessions/${encodeURIComponent(id)}/messages?limit=500&order=latest`, | |
| profile, | |
| ), | |
| ), | |
| getSessionDetail: (id: string, profile = getManagementProfile()) => | |
| fetchJSON<SessionInfo>( | |
| appendProfileParam(`/api/sessions/${encodeURIComponent(id)}`, profile), | |
| ), | |
| getSessionLatestDescendant: (id: string, profile = getManagementProfile()) => | |
| fetchJSON<SessionLatestDescendantResponse>( | |
| appendProfileParam( | |
| `/api/sessions/${encodeURIComponent(id)}/latest-descendant`, | |
| profile, | |
| ), | |
| ), | |
| deleteSession: (id: string, profile = getManagementProfile()) => | |
| fetchJSON<{ ok: boolean }>( | |
| appendProfileParam(`/api/sessions/${encodeURIComponent(id)}`, profile), | |
| { | |
| method: "DELETE", | |
| }, | |
| ), | |
| getEmptySessionsCount: (profile = getManagementProfile()) => | |
| fetchJSON<{ count: number }>( | |
| appendProfileParam("/api/sessions/empty/count", profile), | |
| ), | |
| deleteEmptySessions: (profile = getManagementProfile()) => | |
| fetchJSON<{ ok: boolean; deleted: number }>( | |
| appendProfileParam("/api/sessions/empty", profile), | |
| { | |
| method: "DELETE", | |
| }, | |
| ), | |
| bulkDeleteSessions: (ids: string[], profile = getManagementProfile()) => | |
| fetchJSON<{ ok: boolean; deleted: number }>("/api/sessions/bulk-delete", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ ids, profile: profile || undefined }), | |
| }), | |
| renameSession: (id: string, title: string, profile = getManagementProfile()) => | |
| fetchJSON<{ ok: boolean; title: string }>( | |
| `/api/sessions/${encodeURIComponent(id)}`, | |
| { | |
| method: "PATCH", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ title, profile: profile || undefined }), | |
| }, | |
| ), | |
| getSessionStats: (profile = getManagementProfile()) => | |
| fetchJSON<SessionStoreStats>(appendProfileParam("/api/sessions/stats", profile)), | |
| exportSessionUrl: (id: string, profile = getManagementProfile()) => | |
| appendProfileParam(`/api/sessions/${encodeURIComponent(id)}/export`, profile), | |
| importSessions: ( | |
| sessions: Array<Record<string, unknown>>, | |
| profile = getManagementProfile(), | |
| ) => | |
| fetchJSON<SessionImportResponse>("/api/sessions/import", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ sessions, profile: profile || undefined }), | |
| }), | |
| pruneSessions: ( | |
| older_than_days: number, | |
| source?: string, | |
| profile = getManagementProfile(), | |
| ) => | |
| fetchJSON<{ ok: boolean; removed: number; skipped_open: number }>( | |
| "/api/sessions/prune", | |
| { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| older_than_days, | |
| source, | |
| profile: profile || undefined, | |
| }), | |
| }, | |
| ), | |
| listFiles: (path?: string) => { | |
| const query = path ? `?path=${encodeURIComponent(path)}` : ""; | |
| return fetchJSON<ManagedFilesResponse>(`/api/files${query}`); | |
| }, | |
| readFile: (path: string) => | |
| fetchJSON<ManagedFileReadResponse>( | |
| `/api/files/read?path=${encodeURIComponent(path)}`, | |
| ), | |
| uploadFile: (path: string, file: File, overwrite = true) => { | |
| // Stream the raw bytes as multipart/form-data. Do NOT set Content-Type β | |
| // the browser adds the multipart boundary automatically. Sending the file | |
| // as base64 JSON (the old path) inflated the body ~33%, buffered the whole | |
| // file in memory, and 502'd on large backup archives behind the proxy | |
| // (NS-501). | |
| const form = new FormData(); | |
| form.append("path", path); | |
| form.append("overwrite", String(overwrite)); | |
| form.append("file", file, file.name); | |
| return fetchJSON<ManagedFileWriteResponse>("/api/files/upload-stream", { | |
| method: "POST", | |
| body: form, | |
| }); | |
| }, | |
| createDirectory: (path: string) => | |
| fetchJSON<ManagedFileWriteResponse>("/api/files/mkdir", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ path }), | |
| }), | |
| deleteFile: (path: string, recursive = false) => | |
| fetchJSON<{ ok: boolean; path: string }>("/api/files", { | |
| method: "DELETE", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ path, recursive }), | |
| }), | |
| getLogs: (params: { file?: string; lines?: number; level?: string; component?: string }) => { | |
| const qs = new URLSearchParams(); | |
| if (params.file) qs.set("file", params.file); | |
| if (params.lines) qs.set("lines", String(params.lines)); | |
| if (params.level && params.level !== "ALL") qs.set("level", params.level); | |
| if (params.component && params.component !== "all") qs.set("component", params.component); | |
| return fetchJSON<LogsResponse>(`/api/logs?${qs.toString()}`); | |
| }, | |
| getAnalytics: (days: number, profile = getManagementProfile()) => | |
| fetchJSON<AnalyticsResponse>( | |
| appendProfileParam(`/api/analytics/usage?days=${days}`, profile), | |
| ), | |
| getModelsAnalytics: (days: number, profile = getManagementProfile()) => | |
| fetchJSON<ModelsAnalyticsResponse>( | |
| appendProfileParam(`/api/analytics/models?days=${days}`, profile), | |
| ), | |
| getConfig: (profile = getManagementProfile()) => | |
| fetchJSON<Record<string, unknown>>(appendProfileParam("/api/config", profile)), | |
| getDefaults: () => fetchJSON<Record<string, unknown>>("/api/config/defaults"), | |
| getSchema: () => fetchJSON<{ fields: Record<string, unknown>; category_order: string[] }>("/api/config/schema"), | |
| getModelInfo: (profile = getManagementProfile()) => | |
| fetchJSON<ModelInfoResponse>(appendProfileParam("/api/model/info", profile)), | |
| getModelOptions: ( | |
| profileOrOptions?: string | { profile?: string; refresh?: boolean }, | |
| ) => { | |
| const profile = | |
| typeof profileOrOptions === "string" | |
| ? profileOrOptions | |
| : profileOrOptions?.profile; | |
| const refresh = | |
| typeof profileOrOptions === "object" && !!profileOrOptions.refresh; | |
| const qs = new URLSearchParams(); | |
| if (profile) qs.set("profile", profile); | |
| if (refresh) qs.set("refresh", "1"); | |
| // Dashboard surfaces (Models page, profile builder, cron) are | |
| // management/setup UIs: keep the full provider universe with setup | |
| // affordances. The endpoint now defaults to the configured subset for | |
| // desktop chat pickers (#56974), so opt in explicitly here. | |
| qs.set("include_unconfigured", "1"); | |
| const suffix = qs.toString() ? `?${qs.toString()}` : ""; | |
| return fetchJSON<ModelOptionsResult>(`/api/model/options${suffix}`); | |
| }, | |
| getAuxiliaryModels: (profile = getManagementProfile()) => | |
| fetchJSON<AuxiliaryModelsResponse>( | |
| appendProfileParam("/api/model/auxiliary", profile), | |
| ), | |
| getMoaModels: () => fetchJSON<MoaConfigResponse>("/api/model/moa"), | |
| saveMoaModels: (body: MoaConfigResponse) => | |
| fetchJSON<MoaConfigResponse & { ok: boolean }>("/api/model/moa", { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(body), | |
| }), | |
| setModelAssignment: ( | |
| body: ModelAssignmentRequest, | |
| profile = getManagementProfile(), | |
| ) => | |
| fetchJSON<ModelAssignmentResponse>( | |
| appendProfileParam("/api/model/set", profile), | |
| { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(body), | |
| }, | |
| ), | |
| saveConfig: (config: Record<string, unknown>, profile = getManagementProfile()) => | |
| fetchJSON<{ ok: boolean }>(appendProfileParam("/api/config", profile), { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ config }), | |
| }), | |
| getConfigRaw: (profile = getManagementProfile()) => | |
| fetchJSON<{ yaml: string; path?: string }>( | |
| appendProfileParam("/api/config/raw", profile), | |
| ), | |
| saveConfigRaw: (yaml_text: string, profile = getManagementProfile()) => | |
| fetchJSON<{ ok: boolean }>(appendProfileParam("/api/config/raw", profile), { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ yaml_text }), | |
| }), | |
| getEnvVars: () => fetchJSON<Record<string, EnvVarInfo>>("/api/env"), | |
| setEnvVar: (key: string, value: string) => | |
| fetchJSON<{ ok: boolean }>("/api/env", { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ key, value }), | |
| }), | |
| deleteEnvVar: (key: string) => | |
| fetchJSON<{ ok: boolean }>("/api/env", { | |
| method: "DELETE", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ key }), | |
| }), | |
| revealEnvVar: (key: string) => | |
| fetchJSON<{ key: string; value: string }>("/api/env/reveal", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ key }), | |
| }), | |
| // Cron jobs | |
| getCronJobs: (profile = "all") => | |
| fetchJSON<CronJob[]>(`/api/cron/jobs?profile=${encodeURIComponent(profile)}`), | |
| getCronDeliveryTargets: () => | |
| fetchJSON<{ targets: CronDeliveryTarget[] }>("/api/cron/delivery-targets"), | |
| createCronJob: (job: CronJobMutation, profile = "default") => | |
| fetchJSON<CronJob>(`/api/cron/jobs?profile=${encodeURIComponent(profile)}`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(job), | |
| }), | |
| pauseCronJob: (id: string, profile = "default") => | |
| fetchJSON<CronJob>(`/api/cron/jobs/${encodeURIComponent(id)}/pause?profile=${encodeURIComponent(profile)}`, { method: "POST" }), | |
| updateCronJob: ( | |
| id: string, | |
| updates: CronJobMutation, | |
| profile = "default", | |
| ) => | |
| fetchJSON<CronJob>( | |
| `/api/cron/jobs/${encodeURIComponent(id)}?profile=${encodeURIComponent(profile)}`, | |
| { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ updates }), | |
| }, | |
| ), | |
| resumeCronJob: (id: string, profile = "default") => | |
| fetchJSON<CronJob>(`/api/cron/jobs/${encodeURIComponent(id)}/resume?profile=${encodeURIComponent(profile)}`, { method: "POST" }), | |
| triggerCronJob: (id: string, profile = "default") => | |
| fetchJSON<CronJob>(`/api/cron/jobs/${encodeURIComponent(id)}/trigger?profile=${encodeURIComponent(profile)}`, { method: "POST" }), | |
| deleteCronJob: (id: string, profile = "default") => | |
| fetchJSON<{ ok: boolean }>(`/api/cron/jobs/${encodeURIComponent(id)}?profile=${encodeURIComponent(profile)}`, { method: "DELETE" }), | |
| // Automation Blueprints β parameterized automation blueprints | |
| getAutomationBlueprints: () => | |
| fetchJSON<{ blueprints: AutomationBlueprint[] }>("/api/cron/blueprints"), | |
| instantiateAutomationBlueprint: ( | |
| body: { blueprint: string; values: Record<string, string> }, | |
| profile = "default", | |
| ) => | |
| fetchJSON<CronJob>(`/api/cron/blueprints/instantiate?profile=${encodeURIComponent(profile)}`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(body), | |
| }), | |
| // Profiles | |
| getProfiles: () => | |
| fetchJSON<{ profiles: ProfileInfo[] }>("/api/profiles"), | |
| getActiveProfile: () => | |
| fetchJSON<ActiveProfileInfo>("/api/profiles/active"), | |
| setActiveProfile: (name: string) => | |
| fetchJSON<{ ok: boolean; active: string }>("/api/profiles/active", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ name }), | |
| }), | |
| createProfile: (body: { | |
| name: string; | |
| clone_from?: string | null; | |
| clone_from_default?: boolean; | |
| clone_all?: boolean; | |
| no_skills?: boolean; | |
| description?: string; | |
| provider?: string; | |
| model?: string; | |
| mcp_servers?: McpServerCreate[]; | |
| keep_skills?: string[]; | |
| hub_skills?: string[]; | |
| }) => | |
| fetchJSON<{ | |
| ok: boolean; | |
| name: string; | |
| path: string; | |
| model_set?: boolean; | |
| mcp_written?: number; | |
| skills_disabled?: number; | |
| hub_installs?: Array<{ identifier: string; pid: number | null }>; | |
| }>("/api/profiles", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(body), | |
| }), | |
| updateProfileDescription: (name: string, description: string) => | |
| fetchJSON<{ ok: boolean; description: string; description_auto: boolean }>( | |
| `/api/profiles/${encodeURIComponent(name)}/description`, | |
| { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ description }), | |
| }, | |
| ), | |
| describeProfileAuto: (name: string, overwrite = true) => | |
| fetchJSON<ProfileDescribeAutoResult>( | |
| `/api/profiles/${encodeURIComponent(name)}/describe-auto`, | |
| { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ overwrite }), | |
| }, | |
| ), | |
| setProfileModel: (name: string, provider: string, model: string) => | |
| fetchJSON<{ ok: boolean; provider: string; model: string }>( | |
| `/api/profiles/${encodeURIComponent(name)}/model`, | |
| { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ provider, model }), | |
| }, | |
| ), | |
| renameProfile: (name: string, newName: string) => | |
| fetchJSON<{ ok: boolean; name: string; path: string }>( | |
| `/api/profiles/${encodeURIComponent(name)}`, | |
| { | |
| method: "PATCH", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ new_name: newName }), | |
| }, | |
| ), | |
| deleteProfile: (name: string) => | |
| fetchJSON<{ ok: boolean }>( | |
| `/api/profiles/${encodeURIComponent(name)}`, | |
| { method: "DELETE" }, | |
| ), | |
| getProfileSetupCommand: (name: string) => | |
| fetchJSON<{ command: string }>( | |
| `/api/profiles/${encodeURIComponent(name)}/setup-command`, | |
| ), | |
| getProfileSoul: (name: string) => | |
| fetchJSON<{ content: string; exists: boolean }>( | |
| `/api/profiles/${encodeURIComponent(name)}/soul`, | |
| ), | |
| updateProfileSoul: (name: string, content: string) => | |
| fetchJSON<{ ok: boolean }>( | |
| `/api/profiles/${encodeURIComponent(name)}/soul`, | |
| { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ content }), | |
| }, | |
| ), | |
| // Skills & Toolsets | |
| // | |
| // All calls accept an optional ``profile`` so the Skills page can manage | |
| // any profile's skills/toolsets β not just the one the dashboard process | |
| // runs under. Omitted/empty profile = the dashboard's own profile. | |
| getSkills: (profile?: string) => | |
| fetchJSON<SkillInfo[]>(`/api/skills${profileQuery(profile)}`), | |
| toggleSkill: (name: string, enabled: boolean, profile?: string) => | |
| fetchJSON<{ ok: boolean }>("/api/skills/toggle", { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ name, enabled, profile: profile || undefined }), | |
| }), | |
| getSkillContent: (name: string, profile?: string) => | |
| fetchJSON<SkillContent>( | |
| `/api/skills/content?name=${encodeURIComponent(name)}${profile ? `&profile=${encodeURIComponent(profile)}` : ""}`, | |
| ), | |
| createSkill: (skill: { name: string; content: string; category?: string }, profile?: string) => | |
| fetchJSON<SkillWriteResult>("/api/skills", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ ...skill, profile: profile || undefined }), | |
| }), | |
| updateSkillContent: (name: string, content: string, profile?: string) => | |
| fetchJSON<SkillWriteResult>("/api/skills/content", { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ name, content, profile: profile || undefined }), | |
| }), | |
| getToolsets: (profile?: string) => | |
| fetchJSON<ToolsetInfo[]>(`/api/tools/toolsets${profileQuery(profile)}`), | |
| toggleToolset: (name: string, enabled: boolean, profile?: string) => | |
| fetchJSON<{ ok: boolean; name: string; platform: string; enabled: boolean }>( | |
| `/api/tools/toolsets/${encodeURIComponent(name)}`, | |
| { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ enabled, profile: profile || undefined }), | |
| }, | |
| ), | |
| getToolsetConfig: (name: string, profile?: string) => | |
| fetchJSON<ToolsetConfig>( | |
| `/api/tools/toolsets/${encodeURIComponent(name)}/config${profileQuery(profile)}`, | |
| ), | |
| selectToolsetProvider: (name: string, provider: string, profile?: string) => | |
| fetchJSON<{ ok: boolean; name: string; provider: string }>( | |
| `/api/tools/toolsets/${encodeURIComponent(name)}/provider`, | |
| { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ provider, profile: profile || undefined }), | |
| }, | |
| ), | |
| saveToolsetEnv: (name: string, env: Record<string, string>, profile?: string) => | |
| fetchJSON<ToolsetEnvResult>( | |
| `/api/tools/toolsets/${encodeURIComponent(name)}/env`, | |
| { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ env, profile: profile || undefined }), | |
| }, | |
| ), | |
| runToolsetPostSetup: (name: string, key: string, profile?: string) => | |
| fetchJSON<ActionResponse & { key: string }>( | |
| `/api/tools/toolsets/${encodeURIComponent(name)}/post-setup`, | |
| { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ key, profile: profile || undefined }), | |
| }, | |
| ), | |
| // Session search (FTS5) | |
| searchSessions: ( | |
| q: string, | |
| profileOrOptions: string | SessionQueryOptions = getManagementProfile(), | |
| ) => { | |
| const options = normalizeSessionQueryOptions(profileOrOptions); | |
| return fetchJSON<SessionSearchResponse>( | |
| appendSessionFilters( | |
| `/api/sessions/search?q=${encodeURIComponent(q)}`, | |
| options, | |
| ), | |
| ); | |
| }, | |
| // OAuth provider management | |
| getOAuthProviders: () => | |
| fetchJSON<OAuthProvidersResponse>("/api/providers/oauth"), | |
| disconnectOAuthProvider: (providerId: string) => | |
| fetchJSON<{ ok: boolean; provider: string }>( | |
| `/api/providers/oauth/${encodeURIComponent(providerId)}`, | |
| { | |
| method: "DELETE", | |
| }, | |
| ), | |
| startOAuthLogin: (providerId: string) => | |
| fetchJSON<OAuthStartResponse>( | |
| `/api/providers/oauth/${encodeURIComponent(providerId)}/start`, | |
| { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: "{}", | |
| }, | |
| ), | |
| submitOAuthCode: (providerId: string, sessionId: string, code: string) => | |
| fetchJSON<OAuthSubmitResponse>( | |
| `/api/providers/oauth/${encodeURIComponent(providerId)}/submit`, | |
| { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ session_id: sessionId, code }), | |
| }, | |
| ), | |
| pollOAuthSession: (providerId: string, sessionId: string) => | |
| fetchJSON<OAuthPollResponse>( | |
| `/api/providers/oauth/${encodeURIComponent(providerId)}/poll/${encodeURIComponent(sessionId)}`, | |
| ), | |
| cancelOAuthSession: (sessionId: string) => | |
| fetchJSON<{ ok: boolean }>( | |
| `/api/providers/oauth/sessions/${encodeURIComponent(sessionId)}`, | |
| { | |
| method: "DELETE", | |
| }, | |
| ), | |
| // Messaging platforms (gateway channels) | |
| getMessagingPlatforms: () => | |
| fetchJSON<MessagingPlatformsResponse>("/api/messaging/platforms"), | |
| // `hot_served`: a live multiplexer serving the selected named profile rebuilt its adapters from the | |
| // new credentials right away (no gateway restart needed). | |
| updateMessagingPlatform: (id: string, body: MessagingPlatformUpdate) => | |
| fetchJSON<{ ok: boolean; platform: string; hot_served?: boolean }>( | |
| `/api/messaging/platforms/${encodeURIComponent(id)}`, | |
| { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(body), | |
| }, | |
| ), | |
| testMessagingPlatform: (id: string) => | |
| fetchJSON<MessagingPlatformTestResult>( | |
| `/api/messaging/platforms/${encodeURIComponent(id)}/test`, | |
| { method: "POST" }, | |
| ), | |
| startTelegramOnboarding: (body: { bot_name?: string }) => | |
| fetchJSON<TelegramOnboardingStartResponse>( | |
| "/api/messaging/telegram/onboarding/start", | |
| { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(body), | |
| }, | |
| ), | |
| getTelegramOnboardingStatus: (pairingId: string) => | |
| fetchJSON<TelegramOnboardingStatusResponse>( | |
| `/api/messaging/telegram/onboarding/${encodeURIComponent(pairingId)}`, | |
| ), | |
| applyTelegramOnboarding: ( | |
| pairingId: string, | |
| body: { allowed_user_ids: string[]; profile?: string }, | |
| ) => | |
| fetchJSON<TelegramOnboardingApplyResponse>( | |
| `/api/messaging/telegram/onboarding/${encodeURIComponent(pairingId)}/apply`, | |
| { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(body), | |
| }, | |
| ), | |
| cancelTelegramOnboarding: (pairingId: string) => | |
| fetchJSON<{ ok: boolean }>( | |
| `/api/messaging/telegram/onboarding/${encodeURIComponent(pairingId)}`, | |
| { method: "DELETE" }, | |
| ), | |
| startWhatsAppOnboarding: (body: { | |
| mode?: "bot" | "self-chat"; | |
| allowed_users?: string; | |
| }) => | |
| fetchJSON<WhatsAppOnboardingStartResponse>( | |
| "/api/messaging/whatsapp/onboarding/start", | |
| { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(body), | |
| }, | |
| ), | |
| getWhatsAppOnboardingStatus: (pairingId: string) => | |
| fetchJSON<WhatsAppOnboardingStatusResponse>( | |
| `/api/messaging/whatsapp/onboarding/${encodeURIComponent(pairingId)}`, | |
| ), | |
| applyWhatsAppOnboarding: ( | |
| pairingId: string, | |
| body: { mode?: "bot" | "self-chat"; allowed_users?: string; profile?: string }, | |
| ) => | |
| fetchJSON<WhatsAppOnboardingApplyResponse>( | |
| `/api/messaging/whatsapp/onboarding/${encodeURIComponent(pairingId)}/apply`, | |
| { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(body), | |
| }, | |
| ), | |
| cancelWhatsAppOnboarding: (pairingId: string) => | |
| fetchJSON<{ ok: boolean }>( | |
| `/api/messaging/whatsapp/onboarding/${encodeURIComponent(pairingId)}`, | |
| { method: "DELETE" }, | |
| ), | |
| // Gateway / update actions | |
| restartGateway: () => | |
| fetchJSON<ActionResponse>("/api/gateway/restart", { method: "POST" }), | |
| getGatewayMigratePlan: () => | |
| fetchJSON<GatewayMigratePlan>("/api/gateway/migrate/plan"), | |
| migrateGatewayToMultiplex: () => | |
| fetchJSON<ActionResponse>("/api/gateway/migrate", { method: "POST" }), | |
| updateHermes: () => | |
| fetchJSON<ActionResponse>("/api/hermes/update", { method: "POST" }), | |
| checkHermesUpdate: (force = false) => | |
| fetchJSON<UpdateCheckResponse>( | |
| `/api/hermes/update/check${force ? "?force=true" : ""}`, | |
| ), | |
| getActionStatus: (name: string, lines = 200) => | |
| fetchJSON<ActionStatusResponse>( | |
| `/api/actions/${encodeURIComponent(name)}/status?lines=${lines}`, | |
| ), | |
| // Dashboard plugins | |
| getPlugins: () => | |
| fetchJSON<PluginManifestResponse[]>("/api/dashboard/plugins"), | |
| rescanPlugins: () => | |
| fetchJSON<{ ok: boolean; count: number }>("/api/dashboard/plugins/rescan"), | |
| getPluginsHub: () => fetchJSON<PluginsHubResponse>("/api/dashboard/plugins/hub"), | |
| getPluginsCatalog: () => | |
| fetchJSON<CatalogResponse>("/api/dashboard/plugins/catalog"), | |
| installAgentPlugin: (body: AgentPluginInstallRequest) => | |
| fetchJSON<AgentPluginInstallResponse>("/api/dashboard/agent-plugins/install", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ ...body }), | |
| }), | |
| enableAgentPlugin: (name: string) => | |
| fetchJSON<{ ok: boolean; name: string; unchanged?: boolean }>( | |
| `/api/dashboard/agent-plugins/${pluginPath(name)}/enable`, | |
| { method: "POST" }, | |
| ), | |
| disableAgentPlugin: (name: string) => | |
| fetchJSON<{ ok: boolean; name: string; unchanged?: boolean }>( | |
| `/api/dashboard/agent-plugins/${pluginPath(name)}/disable`, | |
| { method: "POST" }, | |
| ), | |
| updateAgentPlugin: (name: string) => | |
| fetchJSON<AgentPluginUpdateResponse>( | |
| `/api/dashboard/agent-plugins/${pluginPath(name)}/update`, | |
| { method: "POST" }, | |
| ), | |
| removeAgentPlugin: (name: string) => | |
| fetchJSON<{ ok: boolean; name: string }>( | |
| `/api/dashboard/agent-plugins/${pluginPath(name)}`, | |
| { method: "DELETE" }, | |
| ), | |
| savePluginProviders: (body: PluginProvidersPutRequest) => | |
| fetchJSON<{ ok: boolean }>("/api/dashboard/plugin-providers", { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(body), | |
| }), | |
| setPluginVisibility: (name: string, hidden: boolean) => | |
| fetchJSON<{ ok: boolean; name: string; hidden: boolean }>( | |
| `/api/dashboard/plugins/${pluginPath(name)}/visibility`, | |
| { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ hidden }), | |
| }, | |
| ), | |
| // Dashboard themes | |
| getThemes: () => | |
| fetchJSON<DashboardThemesResponse>("/api/dashboard/themes"), | |
| setTheme: (name: string) => | |
| fetchJSON<{ ok: boolean; theme: string }>("/api/dashboard/theme", { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ name }), | |
| }), | |
| getFontPref: () => | |
| fetchJSON<DashboardFontResponse>("/api/dashboard/font"), | |
| setFontPref: (font: string) => | |
| fetchJSON<{ ok: boolean; font: string }>("/api/dashboard/font", { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ font }), | |
| }), | |
| // ββ Admin: MCP servers ββββββββββββββββββββββββββββββββββββββββββββββ | |
| getMcpServers: () => fetchJSON<{ servers: McpServer[] }>("/api/mcp/servers"), | |
| addMcpServer: (body: McpServerCreate) => | |
| fetchJSON<McpServer>("/api/mcp/servers", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(body), | |
| }), | |
| authMcpServer: (name: string) => | |
| fetchJSON<McpOAuthFlow>( | |
| `/api/mcp/servers/${encodeURIComponent(name)}/auth`, | |
| { method: "POST" }, | |
| ), | |
| getMcpOAuthFlow: (flowId: string) => | |
| fetchJSON<McpOAuthFlow>( | |
| `/api/mcp/oauth/flows/${encodeURIComponent(flowId)}`, | |
| ), | |
| removeMcpServer: (name: string) => | |
| fetchJSON<{ ok: boolean }>(`/api/mcp/servers/${encodeURIComponent(name)}`, { | |
| method: "DELETE", | |
| }), | |
| testMcpServer: (name: string) => | |
| fetchJSON<McpTestResult>( | |
| `/api/mcp/servers/${encodeURIComponent(name)}/test`, | |
| { method: "POST" }, | |
| ), | |
| setMcpServerEnabled: (name: string, enabled: boolean) => | |
| fetchJSON<{ ok: boolean; name: string; enabled: boolean }>( | |
| `/api/mcp/servers/${encodeURIComponent(name)}/enabled`, | |
| { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ enabled }), | |
| }, | |
| ), | |
| getMcpCatalog: () => | |
| fetchJSON<{ entries: McpCatalogEntry[]; diagnostics: McpCatalogDiagnostic[] }>( | |
| "/api/mcp/catalog", | |
| ), | |
| installMcpCatalogEntry: ( | |
| name: string, | |
| env: Record<string, string> = {}, | |
| enable = true, | |
| ) => | |
| fetchJSON<{ ok: boolean; name: string; background: boolean; action?: string }>( | |
| "/api/mcp/catalog/install", | |
| { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ name, env, enable }), | |
| }, | |
| ), | |
| // ββ Admin: Pairing ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // The mutating endpoints read the profile off the BODY, so the query-param | |
| // rewrite in withManagementProfile doesn't reach them β send it explicitly | |
| // or an approval lands in the wrong profile's whitelist. | |
| getPairing: () => fetchJSON<PairingResponse>("/api/pairing"), | |
| approvePairing: (platform: string, request_id: string) => | |
| fetchJSON<{ ok: boolean; user: PairingUser }>("/api/pairing/approve", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| platform, | |
| request_id, | |
| profile: getManagementProfile() || undefined, | |
| }), | |
| }), | |
| revokePairing: (platform: string, user_id: string) => | |
| fetchJSON<{ ok: boolean }>("/api/pairing/revoke", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| platform, | |
| user_id, | |
| profile: getManagementProfile() || undefined, | |
| }), | |
| }), | |
| clearPendingPairing: () => | |
| fetchJSON<{ ok: boolean; cleared: number }>("/api/pairing/clear-pending", { | |
| method: "POST", | |
| }), | |
| // ββ Admin: Webhooks βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| getWebhooks: () => fetchJSON<WebhooksResponse>("/api/webhooks"), | |
| enableWebhooks: () => | |
| fetchJSON<WebhookEnableResponse>("/api/webhooks/enable", { method: "POST" }), | |
| createWebhook: (body: WebhookCreate) => | |
| fetchJSON<WebhookRoute & { secret: string }>("/api/webhooks", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(body), | |
| }), | |
| deleteWebhook: (name: string) => | |
| fetchJSON<{ ok: boolean }>(`/api/webhooks/${encodeURIComponent(name)}`, { | |
| method: "DELETE", | |
| }), | |
| setWebhookEnabled: (name: string, enabled: boolean) => | |
| fetchJSON<{ ok: boolean; name: string; enabled: boolean }>( | |
| `/api/webhooks/${encodeURIComponent(name)}/enabled`, | |
| { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ enabled }), | |
| }, | |
| ), | |
| // ββ Admin: Credential pool ββββββββββββββββββββββββββββββββββββββββββ | |
| getCredentialPool: () => | |
| fetchJSON<{ providers: CredentialPoolProvider[] }>("/api/credentials/pool"), | |
| addCredentialPoolEntry: ( | |
| provider: string, | |
| api_key: string, | |
| label?: string, | |
| ) => | |
| fetchJSON<{ ok: boolean; provider: string; count: number }>( | |
| "/api/credentials/pool", | |
| { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ provider, api_key, label }), | |
| }, | |
| ), | |
| removeCredentialPoolEntry: (provider: string, index: number) => | |
| fetchJSON<{ ok: boolean; provider: string; count: number }>( | |
| `/api/credentials/pool/${encodeURIComponent(provider)}/${index}`, | |
| { method: "DELETE" }, | |
| ), | |
| // ββ Admin: Memory provider ββββββββββββββββββββββββββββββββββββββββββ | |
| getMemory: () => fetchJSON<MemoryStatus>("/api/memory"), | |
| getMemoryProviderConfig: (provider: string) => | |
| fetchJSON<MemoryProviderConfig>( | |
| `/api/memory/providers/${encodeURIComponent(provider)}/config`, | |
| ), | |
| updateMemoryProviderConfig: (provider: string, values: Record<string, unknown>) => | |
| fetchJSON<{ ok: boolean; active: string }>( | |
| `/api/memory/providers/${encodeURIComponent(provider)}/config`, | |
| { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ values }), | |
| }, | |
| ), | |
| setupMemoryProvider: (provider: string, values: Record<string, unknown> = {}) => | |
| fetchJSON<MemoryProviderSetupResponse>( | |
| `/api/memory/providers/${encodeURIComponent(provider)}/setup`, | |
| { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ values }), | |
| }, | |
| ), | |
| setMemoryProvider: (provider: string) => | |
| fetchJSON<{ ok: boolean; active: string }>("/api/memory/provider", { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ provider }), | |
| }), | |
| resetMemory: (target: "all" | "memory" | "user") => | |
| fetchJSON<{ ok: boolean; deleted: string[] }>("/api/memory/reset", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ target }), | |
| }), | |
| // ββ Admin: Gateway lifecycle ββββββββββββββββββββββββββββββββββββββββ | |
| startGateway: () => | |
| fetchJSON<ActionResponse>("/api/gateway/start", { method: "POST" }), | |
| stopGateway: () => | |
| fetchJSON<ActionResponse>("/api/gateway/stop", { method: "POST" }), | |
| // ββ Admin: Operations βββββββββββββββββββββββββββββββββββββββββββββββ | |
| runDoctor: () => | |
| fetchJSON<ActionResponse>("/api/ops/doctor", { method: "POST" }), | |
| runSecurityAudit: () => | |
| fetchJSON<ActionResponse>("/api/ops/security-audit", { method: "POST" }), | |
| runBackup: (output?: string) => | |
| fetchJSON<ActionResponse>("/api/ops/backup", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ output }), | |
| }), | |
| downloadBackup: (archive: string) => | |
| authedFetch( | |
| `/api/ops/backup/download?archive=${encodeURIComponent(archive)}`, | |
| ), | |
| runImport: (archive: string, force = false) => | |
| fetchJSON<ActionResponse>("/api/ops/import", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ archive, force }), | |
| }), | |
| runImportUpload: (file: File, force = false) => { | |
| const form = new FormData(); | |
| form.append("force", String(force)); | |
| form.append("file", file, file.name); | |
| return fetchJSON<ActionResponse>("/api/ops/import-upload", { | |
| method: "POST", | |
| body: form, | |
| }); | |
| }, | |
| getHooks: () => fetchJSON<HooksResponse>("/api/ops/hooks"), | |
| createHook: (body: HookCreate) => | |
| fetchJSON<{ ok: boolean; event: string; command: string; approved: boolean }>( | |
| "/api/ops/hooks", | |
| { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(body), | |
| }, | |
| ), | |
| deleteHook: (event: string, command: string) => | |
| fetchJSON<{ ok: boolean }>("/api/ops/hooks", { | |
| method: "DELETE", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ event, command }), | |
| }), | |
| getSystemStats: () => fetchJSON<SystemStats>("/api/system/stats"), | |
| // ββ Admin: Curator ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| getCurator: () => fetchJSON<CuratorStatus>("/api/curator"), | |
| setCuratorPaused: (paused: boolean) => | |
| fetchJSON<{ ok: boolean; paused: boolean }>("/api/curator/paused", { | |
| method: "PUT", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ paused }), | |
| }), | |
| runCurator: () => | |
| fetchJSON<ActionResponse>("/api/curator/run", { method: "POST" }), | |
| // ββ Admin: Portal βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| getPortal: () => fetchJSON<PortalStatus>("/api/portal"), | |
| // ββ Admin: Diagnostics (backgrounded) βββββββββββββββββββββββββββββββ | |
| runPromptSize: () => | |
| fetchJSON<ActionResponse>("/api/ops/prompt-size", { method: "POST" }), | |
| runDump: () => fetchJSON<ActionResponse>("/api/ops/dump", { method: "POST" }), | |
| runConfigMigrate: () => | |
| fetchJSON<ActionResponse>("/api/ops/config-migrate", { method: "POST" }), | |
| runDebugShare: (opts?: { redact?: boolean; lines?: number }) => | |
| fetchJSON<DebugShareResponse>("/api/ops/debug-share", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| redact: opts?.redact ?? true, | |
| lines: opts?.lines ?? 200, | |
| }), | |
| }), | |
| getCheckpoints: () => fetchJSON<CheckpointsResponse>("/api/ops/checkpoints"), | |
| pruneCheckpoints: () => | |
| fetchJSON<ActionResponse>("/api/ops/checkpoints/prune", { method: "POST" }), | |
| // ββ Admin: Skills hub βββββββββββββββββββββββββββββββββββββββββββββββ | |
| // ``profile`` scopes install/uninstall/update and the installed-state | |
| // annotations to that profile (omitted = the dashboard's own profile). | |
| installSkillFromHub: (identifier: string, profile?: string) => | |
| fetchJSON<ActionResponse>("/api/skills/hub/install", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ identifier, profile: profile || undefined }), | |
| }), | |
| uninstallSkillFromHub: (name: string, profile?: string) => | |
| fetchJSON<ActionResponse>("/api/skills/hub/uninstall", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ name, profile: profile || undefined }), | |
| }), | |
| updateSkillsFromHub: (profile?: string) => | |
| fetchJSON<ActionResponse>("/api/skills/hub/update", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ profile: profile || undefined }), | |
| }), | |
| searchSkillsHub: (q: string, source = "all", limit = 20, profile?: string) => | |
| fetchJSON<SkillHubSearchResponse>( | |
| `/api/skills/hub/search?q=${encodeURIComponent(q)}&source=${encodeURIComponent(source)}&limit=${limit}${profile ? `&profile=${encodeURIComponent(profile)}` : ""}`, | |
| ), | |
| getSkillHubSources: (profile?: string) => | |
| fetchJSON<SkillHubSourcesResponse>( | |
| `/api/skills/hub/sources${profileQuery(profile)}`, | |
| ), | |
| previewSkillFromHub: (identifier: string) => | |
| fetchJSON<SkillHubPreview>( | |
| `/api/skills/hub/preview?identifier=${encodeURIComponent(identifier)}`, | |
| ), | |
| scanSkillFromHub: (identifier: string) => | |
| fetchJSON<SkillHubScan>( | |
| `/api/skills/hub/scan?identifier=${encodeURIComponent(identifier)}`, | |
| ), | |
| }; | |
| /** Identity payload returned by ``GET /api/auth/me`` (Phase 7). | |
| * | |
| * Returned by the dashboard's gated middleware when a valid session cookie | |
| * is attached. ``email`` and ``display_name`` are empty strings under the | |
| * Nous Portal contract V1 (the access token has no email/name claims β | |
| * see Contract Anchor C4 in the plan). The AuthWidget surfaces a | |
| * truncated ``user_id`` instead. | |
| */ | |
| export interface AuthMeResponse { | |
| user_id: string; | |
| email: string; | |
| display_name: string; | |
| org_id: string; | |
| provider: string; | |
| expires_at: number; | |
| } | |
| /** Preflight for `hermes gateway migrate --multiplex` (mirrors the CLI plan JSON). */ | |
| export interface GatewayMigratePlan { | |
| already_multiplexed: boolean; | |
| blockers: string[]; | |
| command: string; | |
| eligible: boolean; | |
| notices: string[]; | |
| profiles: { profile: string; pid: number | null; service: { kind: string; system: boolean } | null }[]; | |
| } | |
| export interface ActionResponse { | |
| archive?: string; | |
| name: string; | |
| ok: boolean; | |
| pid: number | null; | |
| error?: string; | |
| message?: string; | |
| uploaded_bytes?: number; | |
| update_command?: string; | |
| } | |
| export interface DebugShareResponse { | |
| ok: boolean; | |
| // label -> paste URL, e.g. { Report: "https://paste.rs/abc", "agent.log": "..." } | |
| urls: Record<string, string>; | |
| // "label: error" strings for optional full-log uploads that failed. | |
| failures: string[]; | |
| redacted: boolean; | |
| auto_delete_seconds: number; | |
| } | |
| export interface SessionStoreStats { | |
| total: number; | |
| active_store: number; | |
| archived: number; | |
| messages: number; | |
| by_source: Record<string, number>; | |
| } | |
| export interface SessionImportResponse { | |
| ok: boolean; | |
| imported: number; | |
| skipped: number; | |
| detached: number; | |
| imported_ids: string[]; | |
| skipped_ids: string[]; | |
| errors: Array<Record<string, unknown>>; | |
| } | |
| export interface SkillHubResult { | |
| name: string; | |
| description: string; | |
| source: string; | |
| identifier: string; | |
| trust_level: string; | |
| repo: string | null; | |
| tags: string[]; | |
| } | |
| /** Lock-entry summary for an already-installed hub skill (keyed by identifier). */ | |
| export interface SkillHubInstalledEntry { | |
| name: string | null; | |
| trust_level: string | null; | |
| scan_verdict: string | null; | |
| } | |
| export interface SkillHubSearchResponse { | |
| results: SkillHubResult[]; | |
| /** source_id -> number of results returned by that source. */ | |
| source_counts: Record<string, number>; | |
| /** source ids that didn't return within the parallel-search timeout. */ | |
| timed_out: string[]; | |
| /** identifier -> installed lock entry (for "already installed" badges). */ | |
| installed: Record<string, SkillHubInstalledEntry>; | |
| } | |
| export interface SkillHubSource { | |
| id: string; | |
| label: string; | |
| /** GitHub only: whether the API is currently rate-limited. */ | |
| rate_limited?: boolean; | |
| /** hermes-index only: whether the centralized index loaded. */ | |
| available?: boolean; | |
| } | |
| export interface SkillHubSourcesResponse { | |
| sources: SkillHubSource[]; | |
| index_available: boolean; | |
| /** Featured/popular skills from the centralized index (zero extra API calls). */ | |
| featured: SkillHubResult[]; | |
| installed: Record<string, SkillHubInstalledEntry>; | |
| } | |
| export interface SkillHubPreview { | |
| name: string; | |
| description: string; | |
| source: string; | |
| identifier: string; | |
| trust_level: string; | |
| repo: string | null; | |
| tags: string[]; | |
| /** Rendered SKILL.md content (the actual skill text). */ | |
| skill_md: string; | |
| /** Relative paths of every file in the bundle. */ | |
| files: string[]; | |
| } | |
| export interface SkillHubScanFinding { | |
| severity: string; | |
| category: string; | |
| file: string; | |
| line: number; | |
| description: string; | |
| } | |
| export interface SkillHubScan { | |
| name: string; | |
| identifier: string; | |
| source: string; | |
| trust_level: string; | |
| /** "safe" | "caution" | "dangerous". */ | |
| verdict: string; | |
| summary: string; | |
| /** Install-policy decision for this trust+verdict combo. */ | |
| policy: "allow" | "ask" | "block"; | |
| policy_reason: string; | |
| findings: SkillHubScanFinding[]; | |
| severity_counts: Record<string, number>; | |
| } | |
| // ββ Admin types βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export interface McpServer { | |
| name: string; | |
| transport: "http" | "stdio" | "unknown"; | |
| url: string | null; | |
| command: string | null; | |
| args: string[]; | |
| env: Record<string, string>; | |
| auth: "header" | "oauth" | null; | |
| enabled: boolean; | |
| tools: string[] | null; | |
| } | |
| export interface McpCatalogEntry { | |
| name: string; | |
| description: string; | |
| source: string; | |
| transport: "http" | "stdio"; | |
| auth_type: "api_key" | "oauth" | "none"; | |
| required_env: Array<{ name: string; prompt: string; required: boolean }>; | |
| // Transport details β what actually connects (http) or runs (stdio). | |
| command: string | null; | |
| args: string[]; | |
| url: string | null; | |
| // Git bootstrap (only set for entries that clone + build locally). | |
| install_url: string | null; | |
| install_ref: string | null; | |
| bootstrap: string[]; | |
| // Default tool pre-selection (null = all tools pre-checked) + guidance text. | |
| default_enabled: string[] | null; | |
| post_install: string; | |
| needs_install: boolean; | |
| installed: boolean; | |
| enabled: boolean; | |
| } | |
| export interface McpCatalogDiagnostic { | |
| name: string; | |
| kind: string; | |
| message: string; | |
| } | |
| export type McpHttpAuth = "none" | "header" | "oauth"; | |
| export interface McpServerCreate { | |
| name: string; | |
| url?: string; | |
| command?: string; | |
| args?: string[]; | |
| env?: Record<string, string>; | |
| auth?: McpHttpAuth; | |
| bearer_token?: string; | |
| } | |
| export interface McpTestResult { | |
| ok: boolean; | |
| error?: string; | |
| tools: Array<{ name: string; description: string }>; | |
| } | |
| export interface McpOAuthFlow { | |
| flow_id: string; | |
| server_name: string; | |
| status: "starting" | "authorization_required" | "approved" | "error"; | |
| authorization_url: string | null; | |
| error: string | null; | |
| tools?: Array<{ name: string; description: string }>; | |
| } | |
| export interface MessagingPlatformEnvVar { | |
| key: string; | |
| required: boolean; | |
| is_set: boolean; | |
| redacted_value: string | null; | |
| description: string; | |
| prompt: string; | |
| help: string; | |
| url: string | null; | |
| is_password: boolean; | |
| advanced: boolean; | |
| } | |
| export interface MessagingPlatform { | |
| id: string; | |
| name: string; | |
| description: string; | |
| docs_url: string; | |
| enabled: boolean; | |
| configured: boolean; | |
| gateway_running: boolean; | |
| /** | |
| * "connected" | "disabled" | "not_configured" | "pending_restart" | | |
| * "gateway_stopped" | "startup_failed" | "disconnected" | "fatal" | string | |
| */ | |
| state: string; | |
| error_code: string | null; | |
| error_message: string | null; | |
| updated_at: string | null; | |
| home_channel: { platform: string; chat_id: string; name: string; thread_id?: string } | null; | |
| /** Multiplex secondary served on the default profile's shared listener: the vendor callback URL. */ | |
| ingress_url?: string | null; | |
| whatsapp_setup?: { | |
| mode?: string; | |
| allowed_users_set?: boolean; | |
| home_channel_set?: boolean; | |
| } | null; | |
| env_vars: MessagingPlatformEnvVar[]; | |
| } | |
| export interface MessagingPlatformsResponse { | |
| env_path: string; | |
| gateway_start_command: string; | |
| platforms: MessagingPlatform[]; | |
| } | |
| export interface MessagingPlatformUpdate { | |
| enabled?: boolean; | |
| env?: Record<string, string>; | |
| clear_env?: string[]; | |
| } | |
| export interface MessagingPlatformTestResult { | |
| ok: boolean; | |
| state: string; | |
| message: string; | |
| } | |
| export interface PairingUser { | |
| platform: string; | |
| user_id: string; | |
| user_name?: string; | |
| request_id?: string; | |
| age_minutes?: number; | |
| } | |
| export interface PairingResponse { | |
| pending: PairingUser[]; | |
| approved: PairingUser[]; | |
| } | |
| export interface WebhookRoute { | |
| name: string; | |
| description: string; | |
| events: string[]; | |
| deliver: string; | |
| deliver_only: boolean; | |
| prompt: string; | |
| skills: string[]; | |
| created_at: string | null; | |
| url: string; | |
| secret_set: boolean; | |
| enabled: boolean; | |
| } | |
| export interface WebhooksResponse { | |
| enabled: boolean; | |
| base_url: string; | |
| subscriptions: WebhookRoute[]; | |
| } | |
| export interface WebhookEnableResponse { | |
| ok: boolean; | |
| platform: "webhook"; | |
| enabled: true; | |
| needs_restart: boolean; | |
| restart_started?: boolean; | |
| restart_action?: string; | |
| restart_pid?: number | null; | |
| restart_error?: string; | |
| } | |
| export interface WebhookCreate { | |
| name: string; | |
| description?: string; | |
| events?: string[]; | |
| prompt?: string; | |
| skills?: string[]; | |
| deliver?: string; | |
| deliver_only?: boolean; | |
| deliver_chat_id?: string; | |
| } | |
| export interface CredentialPoolEntry { | |
| index: number; | |
| id: string | null; | |
| label: string | null; | |
| auth_type: string | null; | |
| source: string | null; | |
| priority: number; | |
| last_status: string | null; | |
| request_count: number; | |
| token_preview: string; | |
| has_refresh: boolean; | |
| } | |
| export interface CredentialPoolProvider { | |
| provider: string; | |
| entries: CredentialPoolEntry[]; | |
| } | |
| export interface MemoryProviderInfo { | |
| name: string; | |
| description: string; | |
| available: boolean; | |
| configured: boolean; | |
| status: "ready" | "needs_config" | "unavailable" | "missing"; | |
| setup?: MemoryProviderSetupInfo; | |
| } | |
| export interface MemoryStatus { | |
| active: string; | |
| providers: MemoryProviderInfo[]; | |
| builtin_files: { memory: number; user: number }; | |
| } | |
| export interface MemoryProviderExternalDependency { | |
| name: string; | |
| install: string; | |
| check: string; | |
| } | |
| export interface MemoryProviderSetupInfo { | |
| pip_dependencies: string[]; | |
| external_dependencies: MemoryProviderExternalDependency[]; | |
| required_env: string[]; | |
| dependencies_installed: boolean; | |
| } | |
| export interface MemoryProviderSetupResult { | |
| kind: string; | |
| name: string; | |
| status: string; | |
| command: string; | |
| returncode: number | null; | |
| stdout: string; | |
| stderr: string; | |
| } | |
| export interface MemoryProviderSetupResponse { | |
| ok: boolean; | |
| provider: string; | |
| results: MemoryProviderSetupResult[]; | |
| status?: MemoryProviderInfo | null; | |
| } | |
| export interface MemoryProviderFieldOption { | |
| value: string; | |
| label: string; | |
| description?: string; | |
| } | |
| export interface MemoryProviderField { | |
| key: string; | |
| label: string; | |
| kind: "text" | "secret" | "select" | "boolean" | "integer" | "number"; | |
| description: string; | |
| placeholder: string; | |
| required: boolean; | |
| value: string | boolean | number; | |
| is_set: boolean; | |
| options: MemoryProviderFieldOption[]; | |
| url: string; | |
| minimum?: number | null; | |
| maximum?: number | null; | |
| step?: number | null; | |
| when?: Record<string, string | boolean | number> | null; | |
| } | |
| export interface MemoryProviderConfig { | |
| name: string; | |
| label: string; | |
| fields: MemoryProviderField[]; | |
| setup?: MemoryProviderSetupInfo; | |
| } | |
| export interface HookEntry { | |
| event: string; | |
| matcher: string | null; | |
| command: string | null; | |
| timeout: number | null; | |
| allowed: boolean; | |
| approved_at?: string | null; | |
| executable?: boolean; | |
| } | |
| export interface HooksResponse { | |
| hooks: HookEntry[]; | |
| valid_events: string[]; | |
| } | |
| export interface HookCreate { | |
| event: string; | |
| command: string; | |
| matcher?: string; | |
| timeout?: number; | |
| approve?: boolean; | |
| } | |
| export interface UpdateCheckResponse { | |
| install_method: string; | |
| current_version: string; | |
| // commits behind: >=1 known count, 0 up to date, -1 behind by unknown | |
| // count (nix/pypi), or null when the check could not run. | |
| behind: number | null; | |
| update_available: boolean; | |
| can_apply: boolean; | |
| update_command: string; | |
| message: string | null; | |
| } | |
| export interface SystemStats { | |
| os: string; | |
| os_release: string; | |
| os_version: string; | |
| platform: string; | |
| arch: string; | |
| hostname: string; | |
| python_version: string; | |
| python_impl: string; | |
| hermes_version: string; | |
| cpu_count: number | null; | |
| psutil: boolean; | |
| cpu_percent?: number; | |
| load_avg?: number[]; | |
| uptime_seconds?: number; | |
| memory?: { total: number; available: number; used: number; percent: number }; | |
| disk?: { total: number; used: number; free: number; percent: number }; | |
| process?: { pid: number; rss: number; create_time: number; num_threads: number }; | |
| } | |
| export interface CuratorStatus { | |
| enabled: boolean; | |
| paused: boolean; | |
| interval_hours: number | null; | |
| last_run_at: string | null; | |
| min_idle_hours: number | null; | |
| stale_after_days: number | null; | |
| archive_after_days: number | null; | |
| } | |
| export interface PortalFeature { | |
| label: string; | |
| state: string; | |
| } | |
| export interface PortalStatus { | |
| logged_in: boolean; | |
| portal_url: string | null; | |
| inference_url: string | null; | |
| provider: string; | |
| subscription_url: string; | |
| features: PortalFeature[]; | |
| } | |
| export interface CheckpointSession { | |
| session: string; | |
| files: number; | |
| bytes: number; | |
| } | |
| export interface CheckpointsResponse { | |
| sessions: CheckpointSession[]; | |
| total_bytes: number; | |
| } | |
| /** Per-call overrides for {@link fetchJSON}. */ | |
| interface FetchJSONOptions { | |
| /** When true, a 401 response is surfaced as a normal thrown error rather | |
| * than triggering the loopback stale-token page reload. Use for probes | |
| * whose 401 is an expected signal (e.g. /api/auth/me in non-gated mode) | |
| * rather than evidence of a rotated session token. */ | |
| allowUnauthorized?: boolean; | |
| } | |
| export interface ActionStatusResponse { | |
| exit_code: number | null; | |
| lines: string[]; | |
| name: string; | |
| pid: number | null; | |
| running: boolean; | |
| } | |
| export interface PlatformStatus { | |
| error_code?: string; | |
| error_message?: string; | |
| state: string; | |
| updated_at: string; | |
| } | |
| export interface StatusResponse { | |
| active_sessions: number; | |
| /** Phase 7: ``true`` when the dashboard's OAuth gate is engaged | |
| * (public bind, no ``--insecure``). Read alongside ``auth_providers`` | |
| * to render a "gated / loopback" badge. */ | |
| auth_required?: boolean; | |
| /** Phase 7: registered ``DashboardAuthProvider`` names (e.g. ``["nous"]``). | |
| * Empty in loopback mode; empty + ``auth_required=true`` is a | |
| * fail-closed state (the dashboard will refuse to bind). */ | |
| auth_providers?: string[]; | |
| /** Supported dashboard auth flows for the client to choose from. In gated | |
| * mode always includes ``"cookie"``; includes ``"native_pkce"`` when any | |
| * interactive session provider is registered (OAuth providers broker the | |
| * IDP redirect; password providers complete at /login in the system | |
| * browser), signalling that the desktop can use the RFC 8252 | |
| * system-browser + loopback + PKCE flow (no embedded webview, no session | |
| * cookies). Absent / missing ``"native_pkce"`` β an older gateway β the | |
| * desktop falls back to the embedded-webview flow. */ | |
| auth_flows?: string[]; | |
| /** False when the dashboard is running in a hosted/managed layout where | |
| * updates are handled by the outer launcher instead of ``hermes update``. */ | |
| can_update_hermes?: boolean; | |
| config_path: string; | |
| config_version: number; | |
| env_path: string; | |
| gateway_exit_reason: string | null; | |
| gateway_health_url: string | null; | |
| gateway_pid: number | null; | |
| gateway_platforms: Record<string, PlatformStatus>; | |
| gateway_running: boolean; | |
| /** Every profile the gateway process serves when the managed profile is carried by the | |
| * shared multiplexer (e.g. ["default", "alpha", "beta"]); null/absent for a standalone | |
| * gateway or an older backend. */ | |
| gateway_shared_with?: string[] | null; | |
| gateway_state: string | null; | |
| gateway_updated_at: string | null; | |
| hermes_home: string; | |
| latest_config_version: number; | |
| /** NS-656: memory-pressure rollup from the gateway heartbeat + | |
| * lifecycle ledger. Absent on older gateways. */ | |
| memory?: MemoryPressureStatus; | |
| /** NS-656: disk-usage rollup for the HERMES_HOME volume. Absent on | |
| * older gateways. */ | |
| disk?: DiskPressureStatus; | |
| release_date: string; | |
| version: string; | |
| } | |
| /** NS-656: coarse memory telemetry served by /api/status. */ | |
| export interface MemoryPressureStatus { | |
| pressure: "ok" | "elevated" | "critical" | "unknown"; | |
| gateway_rss_mb?: number | null; | |
| system_total_mb?: number | null; | |
| system_available_mb?: number | null; | |
| swap_used_mb?: number | null; | |
| sampled_at?: string | null; | |
| /** Previous gateway life died without running any exit path. */ | |
| last_boot_unclean?: boolean; | |
| /** ...and its final heartbeat showed near-exhausted memory. Heuristic β | |
| * strong evidence of an OOM kill, not proof the kernel OOM killer acted. */ | |
| last_boot_suspected_oom?: boolean; | |
| /** Identity of the current gateway life (sentinel started_at). Changes on | |
| * every restart; keys per-incident banner dismissal. */ | |
| boot_id?: string | null; | |
| } | |
| /** NS-656: coarse disk telemetry served by /api/status. Live statvfs | |
| * sample of the HERMES_HOME volume β no staleness dimension, so no | |
| * sampled_at. */ | |
| export interface DiskPressureStatus { | |
| pressure: "ok" | "elevated" | "critical" | "unknown"; | |
| total_mb?: number | null; | |
| free_mb?: number | null; | |
| used_percent?: number | null; | |
| } | |
| export interface SessionInfo { | |
| id: string; | |
| source: string | null; | |
| model: string | null; | |
| title: string | null; | |
| started_at: number; | |
| ended_at: number | null; | |
| last_active: number; | |
| is_active: boolean; | |
| message_count: number; | |
| tool_call_count: number; | |
| input_tokens: number; | |
| output_tokens: number; | |
| preview: string | null; | |
| parent_session_id?: string | null; | |
| /** Owning profile stamped by the list/detail endpoints (the store the row | |
| * was read from). Absent on search-endpoint rows, which carry no stamp. */ | |
| profile?: string; | |
| } | |
| export interface SessionLatestDescendantResponse { | |
| requested_session_id: string; | |
| session_id: string; | |
| path: string[]; | |
| changed: boolean; | |
| } | |
| export interface PaginatedSessions { | |
| sessions: SessionInfo[]; | |
| total: number; | |
| limit: number; | |
| offset: number; | |
| } | |
| export interface EnvVarInfo { | |
| is_set: boolean; | |
| redacted_value: string | null; | |
| description: string; | |
| url: string | null; | |
| category: string; | |
| is_password: boolean; | |
| tools: string[]; | |
| advanced: boolean; | |
| /** True when this var is a messaging-platform credential owned by the Channels page. */ | |
| channel_managed?: boolean; | |
| /** True when this key is set in .env but not in any catalog (user-added custom key). */ | |
| custom?: boolean; | |
| } | |
| export interface TelegramOnboardingStartResponse { | |
| pairing_id: string; | |
| suggested_username: string; | |
| deep_link: string; | |
| qr_payload: string; | |
| expires_at: string; | |
| } | |
| export type TelegramOnboardingStatusResponse = | |
| | { status: "waiting"; expires_at: string } | |
| | { | |
| status: "ready"; | |
| bot_username: string; | |
| owner_user_id?: string; | |
| expires_at: string; | |
| }; | |
| export interface TelegramOnboardingApplyResponse { | |
| ok: boolean; | |
| platform: "telegram"; | |
| bot_username?: string; | |
| needs_restart: boolean; | |
| restart_started?: boolean; | |
| restart_action?: string; | |
| restart_pid?: number | null; | |
| restart_error?: string; | |
| } | |
| export interface WhatsAppOnboardingStartResponse { | |
| pairing_id: string; | |
| status: | |
| | "starting" | |
| | "installing" | |
| | "waiting" | |
| | "connected" | |
| | "error" | |
| | "expired" | |
| | "cancelled"; | |
| qr_payload?: string | null; | |
| expires_at: string; | |
| mode: "bot" | "self-chat"; | |
| allowed_users: string; | |
| account_id?: string | null; | |
| account_name?: string | null; | |
| account_phone?: string | null; | |
| error?: string | null; | |
| } | |
| export type WhatsAppOnboardingStatusResponse = WhatsAppOnboardingStartResponse; | |
| export interface WhatsAppOnboardingApplyResponse { | |
| ok: boolean; | |
| platform: "whatsapp"; | |
| needs_restart: boolean; | |
| restart_started?: boolean; | |
| restart_action?: string; | |
| restart_pid?: number | null; | |
| restart_error?: string; | |
| } | |
| export interface SessionMessage { | |
| role: "user" | "assistant" | "system" | "tool"; | |
| content: string | null; | |
| tool_calls?: Array<{ | |
| id: string; | |
| function: { name: string; arguments: string }; | |
| }>; | |
| tool_name?: string; | |
| tool_call_id?: string; | |
| timestamp?: number; | |
| } | |
| export interface SessionMessagesResponse { | |
| session_id: string; | |
| messages: SessionMessage[]; | |
| pagination?: { | |
| limit: number; | |
| offset: number; | |
| order: "latest" | "oldest"; | |
| returned: number; | |
| }; | |
| } | |
| export interface LogsResponse { | |
| file: string; | |
| lines: string[]; | |
| } | |
| export interface ManagedFileEntry { | |
| name: string; | |
| path: string; | |
| is_directory: boolean; | |
| size: number | null; | |
| mtime: number; | |
| mime_type: string | null; | |
| } | |
| export interface ManagedFilesResponse { | |
| root: string | null; | |
| path: string; | |
| parent: string | null; | |
| locked_root: string | null; | |
| can_change_path: boolean; | |
| entries: ManagedFileEntry[]; | |
| } | |
| export interface ManagedFileReadResponse { | |
| name: string; | |
| path: string; | |
| size: number; | |
| mime_type: string; | |
| data_url: string; | |
| root: string | null; | |
| locked_root: string | null; | |
| can_change_path: boolean; | |
| } | |
| export interface ManagedFileWriteResponse { | |
| ok: boolean; | |
| path: string; | |
| entry: ManagedFileEntry; | |
| root: string | null; | |
| locked_root: string | null; | |
| can_change_path: boolean; | |
| } | |
| export interface AnalyticsDailyEntry { | |
| day: string; | |
| input_tokens: number; | |
| output_tokens: number; | |
| cache_read_tokens: number; | |
| reasoning_tokens: number; | |
| estimated_cost: number; | |
| actual_cost: number; | |
| sessions: number; | |
| api_calls: number; | |
| } | |
| export interface AnalyticsModelEntry { | |
| model: string; | |
| input_tokens: number; | |
| output_tokens: number; | |
| estimated_cost: number; | |
| sessions: number; | |
| api_calls: number; | |
| } | |
| export interface AnalyticsSkillEntry { | |
| skill: string; | |
| view_count: number; | |
| manage_count: number; | |
| total_count: number; | |
| percentage: number; | |
| last_used_at: number | null; | |
| } | |
| export interface AnalyticsSkillsSummary { | |
| total_skill_loads: number; | |
| total_skill_edits: number; | |
| total_skill_actions: number; | |
| distinct_skills_used: number; | |
| } | |
| export interface AnalyticsResponse { | |
| daily: AnalyticsDailyEntry[]; | |
| by_model: AnalyticsModelEntry[]; | |
| totals: { | |
| total_input: number; | |
| total_output: number; | |
| total_cache_read: number; | |
| total_reasoning: number; | |
| total_estimated_cost: number; | |
| total_actual_cost: number; | |
| total_sessions: number; | |
| total_api_calls: number; | |
| }; | |
| skills: { | |
| summary: AnalyticsSkillsSummary; | |
| top_skills: AnalyticsSkillEntry[]; | |
| }; | |
| } | |
| export interface ActiveProfileInfo { | |
| active: string; | |
| current: string; | |
| } | |
| export interface ProfileDescribeAutoResult { | |
| ok: boolean; | |
| reason: string; | |
| description: string | null; | |
| description_auto: boolean; | |
| } | |
| export interface ProfileInfo { | |
| name: string; | |
| path: string; | |
| is_default: boolean; | |
| model: string | null; | |
| provider: string | null; | |
| has_env: boolean; | |
| skill_count: number; | |
| gateway_running: boolean; | |
| description: string; | |
| description_auto: boolean; | |
| display_name?: string; | |
| distribution_name: string | null; | |
| distribution_version: string | null; | |
| distribution_source: string | null; | |
| has_alias: boolean; | |
| } | |
| export interface ModelsAnalyticsModelEntry { | |
| model: string; | |
| provider: string; | |
| input_tokens: number; | |
| output_tokens: number; | |
| cache_read_tokens: number; | |
| reasoning_tokens: number; | |
| estimated_cost: number; | |
| actual_cost: number; | |
| sessions: number; | |
| api_calls: number; | |
| tool_calls: number; | |
| last_used_at: number; | |
| avg_tokens_per_session: number; | |
| capabilities: { | |
| supports_tools?: boolean; | |
| supports_vision?: boolean; | |
| supports_reasoning?: boolean; | |
| context_window?: number; | |
| max_output_tokens?: number; | |
| model_family?: string; | |
| }; | |
| } | |
| export interface ModelsAnalyticsResponse { | |
| models: ModelsAnalyticsModelEntry[]; | |
| totals: { | |
| distinct_models: number; | |
| total_input: number; | |
| total_output: number; | |
| total_cache_read: number; | |
| total_reasoning: number; | |
| total_estimated_cost: number; | |
| total_actual_cost: number; | |
| total_sessions: number; | |
| total_api_calls: number; | |
| }; | |
| period_days: number; | |
| } | |
| export interface CronJobRepeat { | |
| times: number | null; | |
| completed?: number; | |
| } | |
| export interface CronJobMutation { | |
| name?: string; | |
| prompt?: string; | |
| schedule?: string; | |
| deliver?: string; | |
| skills?: string[]; | |
| provider?: string | null; | |
| model?: string | null; | |
| base_url?: string | null; | |
| script?: string | null; | |
| no_agent?: boolean; | |
| context_from?: string[] | null; | |
| enabled_toolsets?: string[] | null; | |
| workdir?: string | null; | |
| } | |
| export interface CronJob { | |
| id: string; | |
| profile?: string | null; | |
| profile_name?: string | null; | |
| hermes_home?: string | null; | |
| is_default_profile?: boolean; | |
| name?: string | null; | |
| prompt?: string | null; | |
| script?: string | null; | |
| skills?: string[] | null; | |
| schedule?: { kind?: string; expr?: string; run_at?: string; display?: string }; | |
| schedule_display?: string | null; | |
| repeat?: CronJobRepeat | null; | |
| enabled: boolean; | |
| state?: string | null; | |
| deliver?: string | null; | |
| model?: string | null; | |
| provider?: string | null; | |
| base_url?: string | null; | |
| no_agent?: boolean | null; | |
| context_from?: string[] | string | null; | |
| enabled_toolsets?: string[] | null; | |
| workdir?: string | null; | |
| last_run_at?: string | null; | |
| next_run_at?: string | null; | |
| last_status?: string | null; | |
| last_error?: string | null; | |
| last_delivery_error?: string | null; | |
| last_fire_error?: { at?: string | null; detail?: string | null } | null; | |
| } | |
| export interface CronDeliveryTarget { | |
| id: string; | |
| name: string; | |
| home_target_set: boolean; | |
| home_env_var: string | null; | |
| } | |
| export interface AutomationBlueprintField { | |
| name: string; | |
| type: "time" | "enum" | "text" | "weekdays"; | |
| label: string; | |
| default: string | null; | |
| options: string[]; | |
| optional: boolean; | |
| /** When false, options are suggestions β any value is accepted. */ | |
| strict?: boolean; | |
| help: string; | |
| } | |
| export interface AutomationBlueprint { | |
| key: string; | |
| title: string; | |
| description: string; | |
| category: string; | |
| tags: string[]; | |
| fields: AutomationBlueprintField[]; | |
| command: string; | |
| appUrl: string; | |
| } | |
| export interface SkillInfo { | |
| name: string; | |
| description: string; | |
| category: string; | |
| enabled: boolean; | |
| } | |
| export interface SkillContent { | |
| name: string; | |
| content: string; | |
| path: string; | |
| } | |
| export interface SkillWriteResult { | |
| success: boolean; | |
| message?: string; | |
| path?: string; | |
| error?: string; | |
| } | |
| export interface ToolsetInfo { | |
| name: string; | |
| label: string; | |
| description: string; | |
| platform: string; | |
| platform_label: string; | |
| enabled: boolean; | |
| configured: boolean; | |
| tools: string[]; | |
| } | |
| export interface ToolsetProviderEnvVar { | |
| key: string; | |
| prompt: string; | |
| url: string | null; | |
| default: string | null; | |
| is_set: boolean; | |
| } | |
| export interface ToolsetProvider { | |
| name: string; | |
| badge: string; | |
| tag: string; | |
| env_vars: ToolsetProviderEnvVar[]; | |
| post_setup: string | null; | |
| requires_nous_auth: boolean; | |
| is_active: boolean; | |
| } | |
| export interface ToolsetConfig { | |
| name: string; | |
| has_category: boolean; | |
| providers: ToolsetProvider[]; | |
| active_provider: string | null; | |
| } | |
| export interface ToolsetEnvResult { | |
| ok: boolean; | |
| name: string; | |
| saved: string[]; | |
| skipped: string[]; | |
| is_set: Record<string, boolean>; | |
| } | |
| export interface SessionSearchResult extends SessionInfo { | |
| session_id: string; | |
| snippet: string; | |
| role: string | null; | |
| session_started: number | null; | |
| lineage_root?: string; | |
| } | |
| export interface SessionSearchResponse { | |
| results: SessionSearchResult[]; | |
| } | |
| // ββ Model info types ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export interface ModelInfoResponse { | |
| model: string; | |
| provider: string; | |
| auto_context_length: number; | |
| config_context_length: number; | |
| effective_context_length: number; | |
| capabilities: { | |
| supports_tools?: boolean; | |
| supports_vision?: boolean; | |
| supports_reasoning?: boolean; | |
| context_window?: number; | |
| max_output_tokens?: number; | |
| model_family?: string; | |
| }; | |
| } | |
| // ββ Model options / assignment types ββββββββββββββββββββββββββββββββββ | |
| export type { ModelOptionProvider, ModelOptionsResult }; | |
| export interface AuxiliaryTaskAssignment { | |
| task: string; | |
| provider: string; | |
| model: string; | |
| base_url: string; | |
| } | |
| export interface AuxiliaryModelsResponse { | |
| tasks: AuxiliaryTaskAssignment[]; | |
| main: { provider: string; model: string }; | |
| } | |
| export interface MoaModelSlot { | |
| provider: string; | |
| model: string; | |
| /** Optional per-slot reasoning effort β round-tripped, not edited here. */ | |
| reasoning_effort?: string; | |
| enabled?: boolean; | |
| } | |
| export interface MoaConfigResponse { | |
| default_preset: string; | |
| active_preset: string; | |
| presets: Record<string, { | |
| reference_models: MoaModelSlot[]; | |
| aggregator: MoaModelSlot; | |
| reference_temperature: number; | |
| aggregator_temperature: number; | |
| reference_timeout: number | null; | |
| degraded_reference_policy: "loud" | "silent"; | |
| /** Fan-out cadence (user_turn default | per_iteration | every_n:N) β round-tripped. */ | |
| fanout?: string; | |
| enabled: boolean; | |
| }>; | |
| reference_models: MoaModelSlot[]; | |
| aggregator: MoaModelSlot; | |
| reference_temperature: number; | |
| aggregator_temperature: number; | |
| reference_timeout: number | null; | |
| degraded_reference_policy: "loud" | "silent"; | |
| enabled: boolean; | |
| } | |
| export interface ModelAssignmentRequest { | |
| confirm_expensive_model?: boolean; | |
| scope: "main" | "auxiliary"; | |
| provider: string; | |
| model: string; | |
| /** Optional OpenAI-compatible endpoint URL for custom/local main providers. */ | |
| base_url?: string; | |
| /** For auxiliary: task slot name, "" for all, "__reset__" to reset all. */ | |
| task?: string; | |
| } | |
| /** An auxiliary task still pinned to a provider that differs from the | |
| * newly-selected main provider after a main-model switch. */ | |
| export interface StaleAuxAssignment { | |
| task: string; | |
| provider: string; | |
| model: string; | |
| } | |
| export interface ModelAssignmentResponse { | |
| confirm_message?: string; | |
| confirm_required?: boolean; | |
| ok: boolean; | |
| scope?: string; | |
| provider?: string; | |
| model?: string; | |
| tasks?: string[]; | |
| reset?: boolean; | |
| /** Auxiliary slots still pinned to a different provider than the new main. | |
| * Switching main never clears aux pins; this lets the UI warn the user | |
| * their helper tasks aren't following the switch. Only set on scope:'main'. */ | |
| stale_aux?: StaleAuxAssignment[]; | |
| } | |
| // ββ OAuth provider types ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export interface OAuthProviderStatus { | |
| logged_in: boolean; | |
| source?: string | null; | |
| source_label?: string | null; | |
| token_preview?: string | null; | |
| expires_at?: string | null; | |
| has_refresh_token?: boolean; | |
| last_refresh?: string | null; | |
| error?: string; | |
| } | |
| export interface OAuthProvider { | |
| id: string; | |
| name: string; | |
| /** "pkce" (browser redirect + paste code), "device_code" (show code + URL), | |
| * or "external" (delegated to a separate CLI like Claude Code or Qwen). */ | |
| flow: "pkce" | "device_code" | "external"; | |
| cli_command: string; | |
| docs_url: string; | |
| status: OAuthProviderStatus; | |
| } | |
| export interface OAuthProvidersResponse { | |
| providers: OAuthProvider[]; | |
| } | |
| /** Discriminated union β the shape of /start depends on the flow. */ | |
| export type OAuthStartResponse = | |
| | { | |
| session_id: string; | |
| flow: "pkce"; | |
| auth_url: string; | |
| expires_in: number; | |
| } | |
| | { | |
| session_id: string; | |
| flow: "device_code"; | |
| user_code: string; | |
| verification_url: string; | |
| expires_in: number; | |
| poll_interval: number; | |
| }; | |
| export interface OAuthSubmitResponse { | |
| ok: boolean; | |
| status: "approved" | "error"; | |
| message?: string; | |
| } | |
| export interface OAuthPollResponse { | |
| session_id: string; | |
| status: "pending" | "approved" | "denied" | "expired" | "error"; | |
| error_message?: string | null; | |
| expires_at?: number | null; | |
| } | |
| // ββ Dashboard theme types ββββββββββββββββββββββββββββββββββββββββββββββ | |
| export interface DashboardThemeSummary { | |
| description: string; | |
| label: string; | |
| name: string; | |
| /** Full theme definition for user themes; undefined for built-ins | |
| * (which the frontend already has locally). */ | |
| definition?: DashboardTheme; | |
| } | |
| export interface DashboardThemesResponse { | |
| active: string; | |
| themes: DashboardThemeSummary[]; | |
| } | |
| export interface DashboardFontResponse { | |
| /** Active font-override id, or "theme" when no override is set. */ | |
| font: string; | |
| } | |
| // ββ Dashboard plugin types βββββββββββββββββββββββββββββββββββββββββββββ | |
| export interface PluginManifestResponse { | |
| name: string; | |
| label: string; | |
| description: string; | |
| icon: string; | |
| version: string; | |
| tab: { | |
| path: string; | |
| position?: string; | |
| override?: string; | |
| hidden?: boolean; | |
| }; | |
| slots?: string[]; | |
| entry: string; | |
| css?: string | null; | |
| has_api: boolean; | |
| source: string; | |
| } | |
| export interface HubAgentPluginRow { | |
| name: string; | |
| version: string; | |
| description: string; | |
| source: string; | |
| runtime_status: "disabled" | "enabled" | "inactive"; | |
| has_dashboard_manifest: boolean; | |
| dashboard_manifest: PluginManifestResponse | null; | |
| path: string; | |
| can_remove: boolean; | |
| can_update_git: boolean; | |
| auth_required: boolean; | |
| auth_command: string; | |
| user_hidden: boolean; | |
| /** Reason string when this plugin is on the catalog removed blocklist. */ | |
| removed_reason?: string | null; | |
| } | |
| export interface PluginsHubProviders { | |
| memory_provider: string; | |
| memory_options: MemoryProviderInfo[]; | |
| context_engine: string; | |
| context_options: Array<{ name: string; description: string }>; | |
| } | |
| export interface PluginsHubResponse { | |
| plugins: HubAgentPluginRow[]; | |
| orphan_dashboard_plugins: PluginManifestResponse[]; | |
| providers: PluginsHubProviders; | |
| } | |
| export interface AgentPluginInstallRequest { | |
| identifier: string; | |
| force?: boolean; | |
| enable?: boolean; | |
| /** Install by curated-catalog name (resolves repo + pinned SHA server-side). */ | |
| catalog_name?: string; | |
| } | |
| export interface AgentPluginInstallResponse { | |
| ok: boolean; | |
| plugin_name?: string; | |
| warnings?: string[]; | |
| missing_env?: string[]; | |
| after_install_path?: string | null; | |
| enabled?: boolean; | |
| error?: string; | |
| } | |
| // ββ Plugin catalog types βββββββββββββββββββββββββββββββββββββββββββββββ | |
| export interface CatalogCapabilities { | |
| provides_tools: string[]; | |
| provides_hooks: string[]; | |
| provides_middleware: string[]; | |
| requires_env: string[]; | |
| } | |
| export interface CatalogEntry { | |
| name: string; | |
| description: string; | |
| repo: string; | |
| sha: string; | |
| sha_short: string; | |
| tier: "official" | "community"; | |
| maintainer: string; | |
| requires_hermes: string; | |
| platforms: string[]; | |
| capabilities: CatalogCapabilities; | |
| docs_url: string; | |
| capability_summary: string; | |
| /** Installed-state merge (computed server-side). */ | |
| installed: boolean; | |
| installed_sha: string | null; | |
| update_available: boolean; | |
| runtime_status: "disabled" | "enabled" | "inactive" | null; | |
| } | |
| export interface CatalogRemovedEntry { | |
| name: string; | |
| repo: string; | |
| reason: string; | |
| date: string; | |
| } | |
| export interface CatalogResponse { | |
| entries: CatalogEntry[]; | |
| removed: CatalogRemovedEntry[]; | |
| generated_at: string; | |
| } | |
| export interface AgentPluginUpdateResponse { | |
| ok: boolean; | |
| name?: string; | |
| output?: string; | |
| unchanged?: boolean; | |
| error?: string; | |
| } | |
| export interface PluginProvidersPutRequest { | |
| memory_provider?: string; | |
| context_engine?: string; | |
| } | |