/** * API endpoint wrappers: one function per API. * DTO types come from @prismshadow/penguin-server/api (**type import only**, resolved via * tsconfig paths to the server contract file types.ts; must not be a value import — server * code must not enter the browser bundle). */ import type { AdminPasswordResetRequest, AdminUserCreateRequest, AdminUserCreateResponse, AdminUsersResponse, AgentConfigResponse, AgentConfigUpdateRequest, AgentCreateRequest, AgentCreateResponse, AgentImportRequest, AgentImportResponse, AgentSkillsResponse, AgentsResponse, AgentTracesResponse, ApprovalDecisionRequest, AuthLoginRequest, AuthResponse, BenchmarksResponse, DirListResponse, FilesStatRequest, FilesStatResponse, GoalResponse, MeResponse, MemberAddRequest, MemberAddResponse, MembersResponse, MessagesResponse, ModelsResponse, ModelsUpdateRequest, ModelTestRequest, ModelTestResponse, PasswordChangeRequest, PrefsResponse, ProjectCreateRequest, ProjectCreateResponse, ProjectsResponse, ScheduleItem, SchedulesResponse, ScheduleUpsertRequest, SessionCategory, SessionCreateRequest, SessionCreateResponse, SessionPatchRequest, SessionResponse, SessionsResponse, SessionTracesResponse, SkillInstallRequest, SkillLibraryResponse, RetryNowResponse, SteerRequest, TaskCreateRequest, TaskCreateResponse, TraceAnalysisResponse, TraceEventsResponse, TraceImportRequest, TraceImportResponse, UiPrefs, UpdateCheckResponse, UpdateRunResponse, UsageGroupBy, UsageResponse, VaultResponse, VaultUpdateRequest, VersionResponse, WorkspaceFilesResponse, } from "@prismshadow/penguin-server/api"; import { apiFetch } from "./client"; // Auth & user ----------------------------------------------------------------- export const login = (body: AuthLoginRequest) => apiFetch("/api/auth/login", { method: "POST", body }); export const logout = () => apiFetch("/api/auth/logout", { method: "POST", body: {} }); export const getMe = () => apiFetch("/api/me"); export const changePassword = (body: PasswordChangeRequest) => apiFetch("/api/me/password", { method: "PUT", body }); export const getPrefs = () => apiFetch("/api/me/prefs"); export const putPrefs = (prefs: UiPrefs) => apiFetch("/api/me/prefs", { method: "PUT", body: prefs }); // Admin user management (admin only) ----------------------------------------------------- export const adminListUsers = () => apiFetch("/api/admin/users"); export const adminCreateUser = (body: AdminUserCreateRequest) => apiFetch("/api/admin/users", { method: "POST", body }); export const adminResetPassword = (userId: string, body: AdminPasswordResetRequest) => apiFetch(`/api/admin/users/${encodeURIComponent(userId)}/password`, { method: "POST", body, }); export const adminDeleteUser = (userId: string) => apiFetch(`/api/admin/users/${encodeURIComponent(userId)}`, { method: "DELETE" }); // Project & members -------------------------------------------------------------- export const listProjects = () => apiFetch("/api/projects"); export const createProject = (body: ProjectCreateRequest) => apiFetch("/api/projects", { method: "POST", body }); export const deleteProject = (projectId: string) => apiFetch(`/api/projects/${encodeURIComponent(projectId)}`, { method: "DELETE" }); export const listMembers = (projectId: string) => apiFetch(`/api/projects/${encodeURIComponent(projectId)}/members`); export const addMember = (projectId: string, body: MemberAddRequest) => apiFetch(`/api/projects/${encodeURIComponent(projectId)}/members`, { method: "POST", body, }); export const removeMember = (projectId: string, username: string) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/members/${encodeURIComponent(username)}`, { method: "DELETE" }, ); // Model configuration ------------------------------------------------------------------- export const getModels = (projectId: string) => apiFetch(`/api/projects/${encodeURIComponent(projectId)}/models`); export const putModels = (projectId: string, body: ModelsUpdateRequest) => apiFetch(`/api/projects/${encodeURIComponent(projectId)}/models`, { method: "PUT", body, }); /** Connectivity test: model reference (provider, modelId) is passed in the request body (may include an unsaved apiKey / baseUrl). */ export const testModel = (projectId: string, body: ModelTestRequest) => apiFetch(`/api/projects/${encodeURIComponent(projectId)}/models/test`, { method: "POST", body, }); // Vault environment variables (Agent-level) ------------------------------------------------------- export const getVault = (projectId: string, agentId: string) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}/vault`, ); export const putVault = (projectId: string, agentId: string, body: VaultUpdateRequest) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}/vault`, { method: "PUT", body }, ); // Agent & its configuration ---------------------------------------------------------------- export const listAgents = (projectId: string) => apiFetch(`/api/projects/${encodeURIComponent(projectId)}/agents`); export const createAgent = (projectId: string, body: AgentCreateRequest) => apiFetch(`/api/projects/${encodeURIComponent(projectId)}/agents`, { method: "POST", body, }); export const getAgentConfig = (projectId: string, agentId: string) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}/config`, ); export const putAgentConfig = ( projectId: string, agentId: string, body: AgentConfigUpdateRequest, ) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}/config`, { method: "PUT", body }, ); /** Overwrite system_config.yaml with the current defaults (keeps only name/description/version). */ export const resetAgentConfig = (projectId: string, agentId: string) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}/config/reset`, { method: "POST" }, ); export const getAgentTraces = (projectId: string, agentId: string) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}/traces`, ); // Session --------------------------------------------------------------------- /** * Optional paging (absent = full unfiltered list): the store requests `limit+1` per page to * detect "has more". `category` filters server-side (paging applies within the category); * `withCounts` asks for per-category totals over the whole list alongside the page. */ export const listSessions = ( projectId: string, agentId: string, opts?: { offset: number; limit: number; category?: SessionCategory; withCounts?: boolean }, ) => { const qs = opts ? `?limit=${opts.limit}&offset=${opts.offset}` + (opts.category ? `&category=${opts.category}` : "") + (opts.withCounts ? "&counts=1" : "") : ""; return apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}/sessions${qs}`, ); }; /** Server directory browsing: `path` is an absolute path; empty means start from the server's home directory. */ export const listDirs = (projectId: string, path = "") => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/dirs?path=${encodeURIComponent(path)}`, ); export const createSession = (projectId: string, agentId: string, body: SessionCreateRequest) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}/sessions`, { method: "POST", body }, ); export const getSession = (sessionId: string) => apiFetch(`/api/sessions/${encodeURIComponent(sessionId)}`); export const patchSession = (sessionId: string, body: SessionPatchRequest) => apiFetch(`/api/sessions/${encodeURIComponent(sessionId)}`, { method: "PATCH", body, }); export const deleteSession = (sessionId: string) => apiFetch(`/api/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" }); export const getMessages = (sessionId: string) => apiFetch(`/api/sessions/${encodeURIComponent(sessionId)}/messages`); // Task execution, approval, abort, compaction ------------------------------------------------------ export const postTask = (sessionId: string, body: TaskCreateRequest) => apiFetch(`/api/sessions/${encodeURIComponent(sessionId)}/tasks`, { method: "POST", body, }); export const getGoal = (sessionId: string) => apiFetch(`/api/sessions/${encodeURIComponent(sessionId)}/goal`); export const postApproval = ( sessionId: string, toolCallId: string, body: ApprovalDecisionRequest, ) => apiFetch( `/api/sessions/${encodeURIComponent(sessionId)}/approvals/${encodeURIComponent(toolCallId)}`, { method: "POST", body }, ); export const postAbort = (sessionId: string) => apiFetch(`/api/sessions/${encodeURIComponent(sessionId)}/abort`, { method: "POST", body: {}, }); /** "Retry now" on the reconnect countdown: skips the remaining backoff wait server-side (skipped:false is the benign "no wait in progress" case — e.g. the timer elapsed in a race — never an error). */ export const postRetryNow = (sessionId: string) => apiFetch(`/api/sessions/${encodeURIComponent(sessionId)}/retry-now`, { method: "POST", body: {}, }); /** Mid-run steering: queues a message for the running Task (delivered between turns as a standalone `[user_steering]` user message); 409 not_running when no Task is in progress. */ export const postSteer = (sessionId: string, body: SteerRequest) => apiFetch(`/api/sessions/${encodeURIComponent(sessionId)}/steer`, { method: "POST", body, }); export const postCompact = (sessionId: string) => // Same shape as tasks: the response carries the actual current session_id (a new id after self-healing; the frontend updates its route accordingly). apiFetch(`/api/sessions/${encodeURIComponent(sessionId)}/compact`, { method: "POST", body: {}, }); // Trace browsing & performance analysis ----------------------------------------------------------- export const getSessionTraces = (sessionId: string) => apiFetch(`/api/sessions/${encodeURIComponent(sessionId)}/traces`); export const getTraceEvents = (sessionId: string, index: number, offset: number, limit: number) => apiFetch(`/api/sessions/${encodeURIComponent(sessionId)}/traces/${index}`, { query: { offset, limit }, }); export const getTraceAnalysis = (sessionId: string, index: number) => apiFetch( `/api/sessions/${encodeURIComponent(sessionId)}/traces/${index}/analysis`, ); // Agent-level Trace details (read-only, independent of sessions-table registration): the Trace // page's directory tree comes from an Agent-level scan (including subagent child Sessions and // Sessions created by the CLI); details go through the Agent-level endpoint to avoid 404s for // unregistered sessions. export const getAgentTraceEvents = ( projectId: string, agentId: string, sessionId: string, index: number, offset: number, limit: number, ) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}` + `/traces/${encodeURIComponent(sessionId)}/${index}`, { query: { offset, limit } }, ); export const getAgentTraceAnalysis = ( projectId: string, agentId: string, sessionId: string, index: number, ) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}` + `/traces/${encodeURIComponent(sessionId)}/${index}/analysis`, ); /** Trace file download URL: the server sets Content-Disposition attachment, usable directly in . */ export const agentTraceDownloadUrl = ( projectId: string, agentId: string, sessionId: string, index: number, ): string => `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}` + `/traces/${encodeURIComponent(sessionId)}/${index}/download`; /** Imports a Trace JSONL file (owner only); the response says where the file landed (sessionId / index / date). */ export const importAgentTrace = (projectId: string, agentId: string, body: TraceImportRequest) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}/traces/import`, { method: "POST", body }, ); // Usage statistics ---------------------------------------------------------------------- export const getUsage = ( projectId: string, params: { from?: string; to?: string; groupBy: UsageGroupBy; agentId?: string; /** Model filter is always a whole pair — both fields or neither; a model is never referenced by id alone. */ provider?: string; modelId?: string; }, ) => apiFetch(`/api/projects/${encodeURIComponent(projectId)}/usage`, { query: { from: params.from, to: params.to, groupBy: params.groupBy, agentId: params.agentId, provider: params.provider, modelId: params.modelId, }, }); // Agent deletion & Workspace files -------------------------------------------------- export const deleteAgent = (projectId: string, agentId: string) => apiFetch(`/api/projects/${projectId}/agents/${agentId}`, { method: "DELETE" }); export const listWorkspaceFiles = (sessionId: string, path: string) => apiFetch(`/api/sessions/${sessionId}/files`, { query: { path } }); /** File content URL (inline preview / download=1 triggers download; usable directly in //fetch). */ export const workspaceFileUrl = (sessionId: string, path: string, download = false): string => `/api/sessions/${sessionId}/files/content?path=${encodeURIComponent(path)}${download ? "&download=1" : ""}`; /** * "Open in a new tab" for a Workspace html file: an App-origin link that mints a signed * token and 302s to the separate preview origin, where the page gets a real origin with * working storage, cookies and third-party embeds. * * A link (not a fetch + `window.open`) on purpose — opening a tab after an await trips * popup blockers, and a script-opened window keeps an `opener` handle back to the App, * which is precisely the reference the separate origin exists to deny. Use it with * `rel="noopener noreferrer"`. * * Falls back server-side to the sandboxed same-origin preview when the deployment has no * usable preview origin; `previewIsolated` from /api/me says so in advance. */ export const workspaceFilePreviewUrl = (sessionId: string, path: string): string => `/api/sessions/${sessionId}/files/preview-redirect?path=${encodeURIComponent(path)}`; export const uploadWorkspaceFile = (sessionId: string, path: string, dataBase64: string) => apiFetch(`/api/sessions/${sessionId}/files/content`, { method: "PUT", body: { dataBase64 }, query: { path }, }); /** Batch file-existence check (message file cards): both out-of-bounds and missing paths simply don't appear in `existing`; always returns 200. */ export const statSessionFiles = (sessionId: string, paths: string[]) => apiFetch(`/api/sessions/${sessionId}/files/stat`, { method: "POST", body: { paths } satisfies FilesStatRequest, }); // Scheduled tasks ---------------------------------------------------------------------- export const listSchedules = (projectId: string, agentId: string) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}/schedules`, ); export const createSchedule = ( projectId: string, agentId: string, body: ScheduleUpsertRequest & { name: string }, ) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}/schedules`, { method: "POST", body }, ); export const updateSchedule = ( projectId: string, agentId: string, name: string, body: ScheduleUpsertRequest, ) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}` + `/schedules/${encodeURIComponent(name)}`, { method: "PUT", body }, ); export const deleteSchedule = (projectId: string, agentId: string, name: string) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}` + `/schedules/${encodeURIComponent(name)}`, { method: "DELETE" }, ); // Skill library & Agent-installed Skills ------------------------------------------------------ /** Skill library (available to any logged-in user): groups and metadata, excludes SKILL.md body content. */ export const getSkillLibrary = () => apiFetch("/api/skills"); export const getAgentSkills = (projectId: string, agentId: string) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}/skills`, ); /** Installs (if already installed, overwrites with the library content); 201 returns the Agent's latest installed list. */ export const installAgentSkills = (projectId: string, agentId: string, names: string[]) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}/skills`, { method: "POST", body: { names } satisfies SkillInstallRequest }, ); export const removeAgentSkill = (projectId: string, agentId: string, name: string) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}` + `/skills/${encodeURIComponent(name)}`, { method: "DELETE" }, ); // Benchmark scoring (read-only display) ------------------------------------------------------- export const listBenchmarks = (projectId: string, agentId: string) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}/benchmarks`, ); // Agent State snapshot export / import ------------------------------------------------------ /** Snapshot bundle (tar.gz) download URL: the server sets Content-Disposition attachment, usable directly in . */ export const agentExportUrl = (projectId: string, agentId: string): string => `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}/export`; export const importAgent = (projectId: string, agentId: string, body: AgentImportRequest) => apiFetch( `/api/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(agentId)}/import`, { method: "POST", body }, ); // Version & self-update ---------------------------------------------------------------- export const getVersion = () => apiFetch("/api/version"); /** `force` (the manual "check for updates" action) bypasses the server's TTL cache. */ export const checkUpdate = (force = false) => apiFetch(`/api/version/update-check${force ? "?force=1" : ""}`); /** Admin only: runs `penguin update` on the server host (long request — up to 10 minutes). */ export const runUpdate = () => apiFetch("/api/version/update", { method: "POST", body: {} });