import type { StatusResponse, ToolSpec, CronJob, CronRun, Integration, DiagResult, MemoryEntry, CostSummary, CliTool, HealthSnapshot, Session, ChannelDetail, BrowserSession, UserNotification, DebugStoreResponse, DebugPlan, DebugTask, DebugRun, DashboardResponse, FlowResponse, SkillSpec, CreatePlanRequest, RouteDecisionTimelineResponse, IngressRouteRequest, IngressRouteResponse, GovernanceCommandRequest, GovernanceCommandResponse, DigitalEmployeeWorkdayProjection, DigitalEmployeeWorkdayActionRequest, PlanDispatchResponse, } from '../types/api'; import { clearToken, getToken, setToken } from './auth'; import { apiOrigin, apiBase } from './basePath'; import { handleQimingclawDigitalApi } from './qimingclawAdapter'; // --------------------------------------------------------------------------- // Base fetch wrapper // --------------------------------------------------------------------------- const API_FETCH_TIMEOUT_MS = 9000; const DIGITAL_EMPLOYEE_WORKDAY_TIMEOUT_MS = 20000; type ApiFetchOptions = RequestInit & { timeoutMs?: number; }; export class UnauthorizedError extends Error { constructor() { super('Unauthorized'); this.name = 'UnauthorizedError'; } } export async function apiFetch( path: string, options: ApiFetchOptions = {}, ): Promise { const adapterData = await handleQimingclawDigitalApi(path, options); if (adapterData !== undefined) { return adapterData; } const { timeoutMs = API_FETCH_TIMEOUT_MS, ...fetchOptions } = options; const token = getToken(); const headers = new Headers(fetchOptions.headers); const controller = new AbortController(); let timedOut = false; const timeout = window.setTimeout(() => { timedOut = true; controller.abort(); }, timeoutMs); const externalSignal = fetchOptions.signal; const abortFromExternal = () => controller.abort(); if (externalSignal) { if (externalSignal.aborted) { controller.abort(); } else { externalSignal.addEventListener('abort', abortFromExternal, { once: true }); } } if (token) { headers.set('Authorization', `Bearer ${token}`); } if ( fetchOptions.body && typeof fetchOptions.body === 'string' && !headers.has('Content-Type') ) { headers.set('Content-Type', 'application/json'); } let response: Response; try { response = await fetch(`${apiOrigin()}${apiBase}${path}`, { ...fetchOptions, headers, signal: controller.signal, }); } catch (error) { if (error instanceof DOMException && error.name === 'AbortError' && timedOut) { throw new Error(`API request timed out after ${timeoutMs / 1000}s: ${path}`); } throw error; } finally { window.clearTimeout(timeout); externalSignal?.removeEventListener('abort', abortFromExternal); } if (response.status === 401) { clearToken(); window.dispatchEvent(new Event('zeroclaw-unauthorized')); throw new UnauthorizedError(); } if (!response.ok) { const text = await response.text().catch(() => ''); let message = text || response.statusText; if (text) { try { const parsed = JSON.parse(text) as { message?: unknown; error?: unknown }; if (typeof parsed.message === 'string' && parsed.message.trim()) { message = parsed.message.trim(); } else if (typeof parsed.error === 'string' && parsed.error.trim()) { message = parsed.error.trim(); } } catch { message = text; } } throw new Error(`API ${response.status}: ${message}`); } // Some endpoints may return 204 No Content if (response.status === 204) { return undefined as unknown as T; } return response.json() as Promise; } function unwrapField(value: T | Record, key: string): T { if (value !== null && typeof value === 'object' && !Array.isArray(value) && key in value) { const unwrapped = (value as Record)[key]; if (unwrapped !== undefined) { return unwrapped; } } return value as T; } // --------------------------------------------------------------------------- // Pairing // --------------------------------------------------------------------------- export async function pair(code: string): Promise<{ token: string }> { const response = await fetch(`${apiOrigin()}${apiBase}/pair`, { method: 'POST', headers: { 'X-Pairing-Code': code }, }); if (!response.ok) { const text = await response.text().catch(() => ''); throw new Error(`Pairing failed (${response.status}): ${text || response.statusText}`); } const data = (await response.json()) as { token: string }; setToken(data.token); return data; } export async function getAdminPairCode(): Promise<{ pairing_code: string | null; pairing_required: boolean }> { const response = await fetch(`${apiOrigin()}${apiBase}/admin/paircode`); if (!response.ok) { throw new Error(`Failed to fetch pairing code (${response.status})`); } return response.json() as Promise<{ pairing_code: string | null; pairing_required: boolean }>; } // --------------------------------------------------------------------------- // Public health (no auth required) // --------------------------------------------------------------------------- export async function getPublicHealth(): Promise<{ require_pairing: boolean; paired: boolean }> { const response = await fetch(`${apiOrigin()}${apiBase}/health`); if (!response.ok) { throw new Error(`Health check failed (${response.status})`); } return response.json() as Promise<{ require_pairing: boolean; paired: boolean }>; } // --------------------------------------------------------------------------- // Status / Health // --------------------------------------------------------------------------- export function getStatus(): Promise { return apiFetch('/api/status'); } export function getHealth(): Promise { return apiFetch('/api/health').then((data) => unwrapField(data, 'health'), ); } // --------------------------------------------------------------------------- // Config // --------------------------------------------------------------------------- export function getConfig(): Promise { return apiFetch('/api/config').then((data) => typeof data === 'string' ? data : data.content, ); } export function putConfig(content: string, contentType = 'application/json'): Promise { return apiFetch('/api/config', { method: 'PUT', headers: { 'Content-Type': contentType }, body: content, }); } // --------------------------------------------------------------------------- // Tools // --------------------------------------------------------------------------- export function getTools(): Promise { return apiFetch('/api/tools').then((data) => unwrapField(data, 'tools'), ); } // --------------------------------------------------------------------------- // Cron // --------------------------------------------------------------------------- export function getCronJobs(): Promise { return apiFetch('/api/cron').then((data) => unwrapField(data, 'jobs'), ); } export function getSchedulerJobs(): Promise { return apiFetch('/api/scheduler/jobs').then((data) => unwrapField(data, 'jobs'), ); } export function addCronJob(body: { name?: string; command?: string; prompt?: string; job_type?: string; schedule: string; enabled?: boolean; session_target?: string; model?: string; allowed_tools?: string[]; delete_after_run?: boolean; }): Promise { return apiFetch('/api/cron', { method: 'POST', body: JSON.stringify(body), }).then((data) => (typeof (data as { job?: CronJob }).job === 'object' ? (data as { job: CronJob }).job : (data as CronJob))); } export function deleteCronJob(id: string): Promise { return apiFetch(`/api/cron/${encodeURIComponent(id)}`, { method: 'DELETE', }); } export function patchCronJob( id: string, patch: { name?: string; schedule?: string; command?: string }, ): Promise { return apiFetch( `/api/cron/${encodeURIComponent(id)}`, { method: 'PATCH', body: JSON.stringify(patch), }, ).then((data) => (typeof (data as { job?: CronJob }).job === 'object' ? (data as { job: CronJob }).job : (data as CronJob))); } export function getCronRuns( jobId: string, limit: number = 20, ): Promise { const params = new URLSearchParams({ limit: String(limit) }); return apiFetch( `/api/cron/${encodeURIComponent(jobId)}/runs?${params}`, ).then((data) => unwrapField(data, 'runs')); } export interface CronSettings { enabled: boolean; catch_up_on_startup: boolean; max_run_history: number; } export function getCronSettings(): Promise { return apiFetch('/api/cron/settings'); } export function patchCronSettings( patch: Partial, ): Promise { return apiFetch('/api/cron/settings', { method: 'PATCH', body: JSON.stringify(patch), }); } // --------------------------------------------------------------------------- // Integrations // --------------------------------------------------------------------------- export function getIntegrations(): Promise { return apiFetch('/api/integrations').then( (data) => unwrapField(data, 'integrations'), ); } // --------------------------------------------------------------------------- // Doctor / Diagnostics // --------------------------------------------------------------------------- export function runDoctor(): Promise { return apiFetch('/api/doctor', { method: 'POST', body: JSON.stringify({}), }).then((data) => (Array.isArray(data) ? data : data.results)); } // --------------------------------------------------------------------------- // Memory // --------------------------------------------------------------------------- export function getMemory( query?: string, category?: string, ): Promise { const params = new URLSearchParams(); if (query) params.set('query', query); if (category) params.set('category', category); const qs = params.toString(); return apiFetch(`/api/memory${qs ? `?${qs}` : ''}`).then( (data) => unwrapField(data, 'entries'), ); } export function storeMemory( key: string, content: string, category?: string, ): Promise { return apiFetch('/api/memory', { method: 'POST', body: JSON.stringify({ key, content, category }), }).then(() => undefined); } export function deleteMemory(key: string): Promise { return apiFetch(`/api/memory/${encodeURIComponent(key)}`, { method: 'DELETE', }); } // --------------------------------------------------------------------------- // Cost // --------------------------------------------------------------------------- export function getCost(): Promise { return apiFetch('/api/cost').then((data) => unwrapField(data, 'cost'), ); } // --------------------------------------------------------------------------- // Sessions // --------------------------------------------------------------------------- export function getSessions(): Promise { return apiFetch<{ sessions: Session[] }>('/api/sessions').then((data) => data.sessions ?? [], ); } export function getSession(id: string): Promise { return apiFetch(`/api/sessions/${encodeURIComponent(id)}`); } export function renameSession(id: string, name: string): Promise<{ session_id: string; name: string }> { return apiFetch<{ session_id: string; name: string }>(`/api/sessions/${encodeURIComponent(id)}`, { method: 'PUT', body: JSON.stringify({ name }), }); } // --------------------------------------------------------------------------- // Channels (detailed) // --------------------------------------------------------------------------- export function getChannels(): Promise { return apiFetch('/api/channels').then((data) => unwrapField(data, 'channels'), ); } // --------------------------------------------------------------------------- // CLI Tools // --------------------------------------------------------------------------- export function getCliTools(): Promise { return apiFetch('/api/cli-tools').then((data) => unwrapField(data, 'cli_tools'), ); } // --------------------------------------------------------------------------- // Browser / SuperRPA // --------------------------------------------------------------------------- export interface LaunchBrowserResponse { session_id: string; status: string; } export interface BrowserStatusResponse { running: boolean; sessions: BrowserSession[]; } export function launchBrowser(url?: string, superrpaPath?: string): Promise { return apiFetch('/api/browser/launch', { method: 'POST', body: JSON.stringify({ url: url || '', superrpa_path: superrpaPath || '' }), }); } export function getBrowserStatus(): Promise { return apiFetch('/api/browser/status'); } export function closeBrowserSession(sessionId: string): Promise { return apiFetch(`/api/browser/session/${encodeURIComponent(sessionId)}/close`, { method: 'POST', }); } // --------------------------------------------------------------------------- // Plans // --------------------------------------------------------------------------- export function getPlans() { return apiFetch<{ plans: import('../types/api').PlanSummary[] }>('/api/debug/plans'); } export interface PlanMessage { role: string; content: string; created_at?: string | null; } export function getSessionMessages(sessionId: string): Promise<{ messages: PlanMessage[] }> { return apiFetch<{ session_id: string; messages: PlanMessage[] }>( `/api/sessions/${encodeURIComponent(sessionId)}/messages`, ).then((d) => ({ messages: d.messages || [] })); } export function getPlanMessages(planId: string): Promise<{ messages: PlanMessage[] }> { return apiFetch<{ plan_id: string; messages: PlanMessage[] }>( `/api/plan/${encodeURIComponent(planId)}/messages`, ).then((d) => ({ messages: d.messages || [] })); } // --------------------------------------------------------------------------- // Execution governance // --------------------------------------------------------------------------- export function getRouteDecisionTimeline(): Promise { return apiFetch('/api/route-decisions'); } export function routeIngress( body: IngressRouteRequest, ): Promise { return apiFetch('/api/ingress/route', { method: 'POST', body: JSON.stringify(body), }); } export function governanceCommand( body: GovernanceCommandRequest, ): Promise { return apiFetch('/api/governance/command', { method: 'POST', body: JSON.stringify(body), }); } export function runSchedulerNow(): Promise { const stamp = Date.now(); return governanceCommand({ command_kind: 'run_schedule_now', context_refs: { schedule_id: 'scheduler-manual-check' }, payload: { source: 'scheduler-jobs-page' }, correlation_id: `ui-run-schedule-now-${stamp}`, idempotency_key: `ui-run-schedule-now-${stamp}`, }); } export function getPlanRevisions(planId: string): Promise { return apiFetch(`/api/plan/${encodeURIComponent(planId)}/revisions`); } export function getEventLog(params: { correlation_id?: string; plan_id?: string; task_id?: string; run_id?: string; limit?: number; } = {}): Promise { const query = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null && `${value}`.trim() !== '') { query.set(key, `${value}`); } }); const suffix = query.toString(); return apiFetch(`/api/event-log${suffix ? `?${suffix}` : ''}`); } // --------------------------------------------------------------------------- // Notifications // --------------------------------------------------------------------------- export { type UserNotification }; export function getNotifications( status: string = 'unread', ): Promise { return apiFetch<{ notifications: UserNotification[] }>( `/api/notifications?status=${encodeURIComponent(status)}`, ).then((d) => d.notifications ?? []); } export function getUnreadNotificationCount(): Promise { return apiFetch<{ count: number }>('/api/notifications/unread-count').then( (d) => d.count ?? 0, ); } export function markNotificationRead(id: string): Promise { return apiFetch<{ ok: boolean }>(`/api/notifications/${encodeURIComponent(id)}/read`, { method: 'POST', }).then((d) => d.ok ?? false); } export function dismissNotification(id: string): Promise { return apiFetch<{ ok: boolean }>( `/api/notifications/${encodeURIComponent(id)}/dismiss`, { method: 'POST' }, ).then((d) => d.ok ?? false); } export function replyToNotification( id: string, content: string, ): Promise<{ ok: boolean; task_id?: string; status?: string }> { return apiFetch<{ ok: boolean; task_id?: string; status?: string }>( `/api/notifications/${encodeURIComponent(id)}/reply`, { method: 'POST', body: JSON.stringify({ content }), }, ); } // --------------------------------------------------------------------------- // Console: Debug Store, Plans, Tasks // --------------------------------------------------------------------------- export function getDebugStore(): Promise { return apiFetch('/api/debug/store?limit=200'); } export function getDashboardStats(): Promise { return apiFetch('/api/dashboard') .then((data) => ('tasks' in data && !('runtime' in data)) ? { runtime: { active_tasks: 0, failed_tasks: 0, pending_confirmations: 0 }, tasks: data.tasks, approvals: { total: 0, pending: 0 } } : data as DashboardResponse); } export function getDigitalEmployeeWorkday(date?: string): Promise { const query = date ? `?date=${encodeURIComponent(date)}` : ''; return apiFetch(`/api/digital-employee/workday${query}`, { timeoutMs: DIGITAL_EMPLOYEE_WORKDAY_TIMEOUT_MS, }); } export function postDigitalEmployeeWorkdayAction( body: DigitalEmployeeWorkdayActionRequest, ): Promise { return apiFetch('/api/digital-employee/workday/actions', { method: 'POST', body: JSON.stringify(body), }); } export function digitalEmployeeReportPdfUrl(date: string): string { return `${apiOrigin()}${apiBase}/api/digital-employee/reports/${encodeURIComponent(date)}/pdf`; } export function dispatchPlanNow(planId: string): Promise { return apiFetch(`/api/debug/plan/${encodeURIComponent(planId)}/dispatch`, { method: 'POST', timeoutMs: DIGITAL_EMPLOYEE_WORKDAY_TIMEOUT_MS, }); } export function getDebugPlans(): Promise { return apiFetch<{ plans: DebugPlan[] }>('/api/debug/plans?limit=80') .then((data) => data.plans ?? []); } export function searchDebugPlans(query: string, limit = 50): Promise { const params = new URLSearchParams({ limit: String(limit), q: query }); return apiFetch<{ plans: DebugPlan[] }>(`/api/debug/plans?${params}`) .then((data) => data.plans ?? []); } export function getDebugTasks(): Promise<{ tasks: DebugTask[]; runs: DebugRun[]; total?: number; truncated?: boolean }> { return apiFetch<{ tasks: DebugTask[]; runs: DebugRun[]; total?: number; truncated?: boolean }>('/api/debug/tasks?limit=80') .then((data) => ({ tasks: data.tasks ?? [], runs: data.runs ?? [], total: data.total, truncated: data.truncated })); } export function getDebugTasksForPlan(planId: string): Promise<{ tasks: DebugTask[]; runs: DebugRun[]; total?: number; truncated?: boolean }> { const params = new URLSearchParams({ limit: '50', plan_id: planId }); return apiFetch<{ tasks: DebugTask[]; runs: DebugRun[]; total?: number; truncated?: boolean }>(`/api/debug/tasks?${params}`) .then((data) => ({ tasks: data.tasks ?? [], runs: data.runs ?? [], total: data.total, truncated: data.truncated })); } export function getDebugPlanFlow(planId: string): Promise { return apiFetch(`/api/debug/plan/${encodeURIComponent(planId)}/flow`); } export function getDebugTaskFlow(taskId: string): Promise { return apiFetch(`/api/debug/task/${encodeURIComponent(taskId)}/flow`); } export function getSkills(): Promise { return apiFetch<{ skills: SkillSpec[] }>('/api/skill') .then((data) => data.skills ?? []); } export function setSkillEnabled(skillId: string, enabled: boolean): Promise<{ ok: boolean; skill_id: string; enabled: boolean; error?: string }> { return apiFetch<{ ok: boolean; skill_id: string; enabled: boolean; error?: string }>( `/api/skill/${encodeURIComponent(skillId)}/enabled`, { method: 'POST', body: JSON.stringify({ enabled }), }, ); } export function createPlan(body: CreatePlanRequest): Promise<{ ok?: boolean; plan_id: string; task_count?: number }> { return apiFetch('/api/debug/plan/create', { method: 'POST', body: JSON.stringify(body), }); } export function planAction(planId: string, action: string): Promise { const endpoint = action === 'submit' ? `/api/plan/${encodeURIComponent(planId)}/submit` : `/api/debug/plan/${encodeURIComponent(planId)}/${action}`; return apiFetch(endpoint, { method: 'POST' }); } export function taskAction(taskId: string, action: string): Promise { return apiFetch(`/api/task/${encodeURIComponent(taskId)}/${action}`, { method: 'POST' }); } /** * Legacy compatibility only. * Current React pages must trigger scheduler checks through runSchedulerNow() * so backend governance rejection and command errors are surfaced correctly. */ export function triggerCronCheck(): Promise<{ ok?: boolean; activated: number }> { return apiFetch<{ ok?: boolean; activated: number }>('/api/cron/check', { method: 'POST' }); } export function getAuditTrace(kind: string, id: string): Promise { const params = new URLSearchParams({ kind, id }); return apiFetch(`/api/audit?${params}`); } export function getArtifacts(): Promise { return apiFetch<{ artifacts: any[] }>('/api/artifact') .then((data) => data.artifacts ?? []); } export function getDebugConversations(): Promise { return apiFetch<{ conversations: any[] }>('/api/debug/conversations') .then((data) => data.conversations ?? []); }