import { JsonRpcGatewayClient } from '@hermes/shared' import type { ActionResponse, ActionStatusResponse, AnalyticsResponse, AudioSpeakResponse, AudioTranscriptionResponse, AuxiliaryModelsResponse, ConfigSchemaResponse, CronJob, CronJobCreatePayload, CronJobUpdates, ElevenLabsVoicesResponse, EnvVarInfo, HermesConfig, HermesConfigRecord, LogsResponse, MessagingPlatformsResponse, MessagingPlatformTestResponse, MessagingPlatformUpdate, ModelAssignmentRequest, ModelAssignmentResponse, ModelInfoResponse, ModelOptionsResponse, OAuthPollResponse, OAuthProvidersResponse, OAuthStartResponse, OAuthSubmitResponse, PaginatedSessions, ProfileCreatePayload, ProfileSetupCommand, ProfileSoul, ProfilesResponse, SessionInfo, SessionMessagesResponse, SessionSearchResponse, SkillInfo, StatusResponse, ToolsetConfig, ToolsetInfo } from '@/types/hermes' const DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS = 30_000 const SESSION_LIST_REQUEST_TIMEOUT_MS = 60_000 export type { ActionResponse, ActionStatusResponse, AnalyticsDailyEntry, AnalyticsModelEntry, AnalyticsResponse, AnalyticsSkillEntry, AnalyticsSkillsSummary, AnalyticsTotals, AudioSpeakResponse, AudioTranscriptionResponse, AuxiliaryModelsResponse, ConfigFieldSchema, ConfigSchemaResponse, CronJob, CronJobCreatePayload, CronJobSchedule, CronJobUpdates, ElevenLabsVoice, ElevenLabsVoicesResponse, EnvVarInfo, GatewayReadyPayload, HermesConfig, HermesConfigRecord, LogsResponse, MessagingEnvVarInfo, MessagingHomeChannel, MessagingPlatformInfo, MessagingPlatformsResponse, MessagingPlatformTestResponse, MessagingPlatformUpdate, ModelAssignmentRequest, ModelAssignmentResponse, ModelInfoResponse, ModelOptionProvider, ModelOptionsResponse, PaginatedSessions, ProfileCreatePayload, ProfileInfo, ProfileSetupCommand, ProfileSoul, ProfilesResponse, RpcEvent, SessionCreateResponse, SessionInfo, SessionMessage, SessionMessagesResponse, SessionResumeResponse, SessionRuntimeInfo, SessionSearchResponse, SessionSearchResult, SkillInfo, StaleAuxAssignment, StatusResponse, ToolsetConfig, ToolsetInfo } from '@/types/hermes' export class HermesGateway extends JsonRpcGatewayClient { constructor() { super({ closedErrorMessage: 'Hermes gateway connection closed', connectErrorMessage: 'Could not connect to Hermes gateway', createRequestId: nextId => nextId, notConnectedErrorMessage: 'Hermes gateway is not connected', requestTimeoutMs: DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS }) } } // Profile that profile-scoped REST settings (config/env/skills/tools/model/…) // should target. Mirrors $activeGatewayProfile, pushed in from the store via // setApiRequestProfile so this module needs no store import (avoids a cycle). // Electron main consumes request.profile to pick which backend *process* serves // the call; each pooled backend already has its own HERMES_HOME, so no backend // change is needed. Null → primary, so single-profile users are unaffected. let _apiProfile: null | string = null export function setApiRequestProfile(profile: null | string): void { _apiProfile = profile || null } function profileScoped(): { profile?: string } { return _apiProfile ? { profile: _apiProfile } : {} } export async function listSessions( limit = 40, minMessages = 0, archived: 'exclude' | 'include' | 'only' = 'exclude', order: 'created' | 'recent' = 'recent' ): Promise { const result = await window.hermesDesktop.api({ path: `/api/sessions?limit=${limit}&offset=0&min_messages=${Math.max(0, minMessages)}&archived=${archived}&order=${order}`, timeoutMs: SESSION_LIST_REQUEST_TIMEOUT_MS }) return { ...result, sessions: result.sessions.slice(0, limit), offset: 0 } } // Unified, read-only session list aggregated across ALL profiles. Served by the // primary backend straight off each profile's state.db — no per-profile backend // is spawned. Single-profile users get the same rows as listSessions(), tagged // profile="default". // Source scoping lets callers split the unified list into independent slices: // recents pass `excludeSources: ['cron']`, the cron-jobs section passes // `source: 'cron'`. Without this a burst of (always-newest) cron sessions // consumes the whole recents page and starves real conversations. export interface SessionSourceFilter { source?: string excludeSources?: string[] } export async function listAllProfileSessions( limit = 40, minMessages = 0, archived: 'exclude' | 'include' | 'only' = 'exclude', order: 'created' | 'recent' = 'recent', profile: 'all' | (string & {}) = 'all', filter: SessionSourceFilter = {} ): Promise { const sourceParam = filter.source ? `&source=${encodeURIComponent(filter.source)}` : '' const excludeParam = filter.excludeSources?.length ? `&exclude_sources=${encodeURIComponent(filter.excludeSources.join(','))}` : '' const result = await window.hermesDesktop.api({ path: `/api/profiles/sessions?limit=${limit}&offset=0&min_messages=${Math.max(0, minMessages)}` + `&archived=${archived}&order=${order}&profile=${encodeURIComponent(profile)}${sourceParam}${excludeParam}`, timeoutMs: SESSION_LIST_REQUEST_TIMEOUT_MS }) return { ...result, sessions: result.sessions.slice(0, limit), offset: 0 } } // Mutations take the owning `profile` so Electron routes them to that profile's // backend (remote pool or local primary) via request.profile — matching the // read path. A remote session's row lives only on its remote host, so a mutation // that hit the local primary would no-op or 404. Omit for the current/default. export function setSessionArchived(id: string, archived: boolean, profile?: string | null): Promise<{ ok: boolean }> { return window.hermesDesktop.api<{ ok: boolean }>({ ...(profile ? { profile } : {}), path: `/api/sessions/${encodeURIComponent(id)}`, method: 'PATCH', body: { archived } }) } export function searchSessions(query: string): Promise { return window.hermesDesktop.api({ path: `/api/sessions/search?q=${encodeURIComponent(query)}` }) } // Reads another profile's transcript. For a remote profile Electron reroutes // this GET to the remote backend (which serves its own state.db); for a local // profile the primary opens that profile's state.db via ?profile=. Omit for // the current/default profile. export function getSessionMessages(id: string, profile?: string | null): Promise { const suffix = profile ? `?profile=${encodeURIComponent(profile)}` : '' return window.hermesDesktop.api({ path: `/api/sessions/${encodeURIComponent(id)}/messages${suffix}` }) } export function deleteSession(id: string, profile?: string | null): Promise<{ ok: boolean }> { return window.hermesDesktop.api<{ ok: boolean }>({ ...(profile ? { profile } : {}), path: `/api/sessions/${encodeURIComponent(id)}`, method: 'DELETE' }) } export function renameSession( id: string, title: string, profile?: string | null ): Promise<{ ok: boolean; title: string }> { return window.hermesDesktop.api<{ ok: boolean; title: string }>({ ...(profile ? { profile } : {}), path: `/api/sessions/${encodeURIComponent(id)}`, method: 'PATCH', body: { title, ...(profile ? { profile } : {}) } }) } export function getGlobalModelInfo(): Promise { return window.hermesDesktop.api({ ...profileScoped(), path: '/api/model/info' }) } export function getStatus(): Promise { return window.hermesDesktop.api({ path: '/api/status' }) } export function getLogs(params: { component?: string file?: string level?: string lines?: number }): Promise { const query = new URLSearchParams() if (params.file) { query.set('file', params.file) } if (typeof params.lines === 'number') { query.set('lines', String(params.lines)) } if (params.level && params.level !== 'ALL') { query.set('level', params.level) } if (params.component && params.component !== 'all') { query.set('component', params.component) } const suffix = query.toString() return window.hermesDesktop.api({ ...profileScoped(), path: suffix ? `/api/logs?${suffix}` : '/api/logs' }) } export function getHermesConfig(): Promise { return window.hermesDesktop.api({ ...profileScoped(), path: '/api/config' }) } export function getHermesConfigRecord(): Promise { return window.hermesDesktop.api({ ...profileScoped(), path: '/api/config' }) } export function getHermesConfigDefaults(): Promise { return window.hermesDesktop.api({ ...profileScoped(), path: '/api/config/defaults' }) } export function getHermesConfigSchema(): Promise { return window.hermesDesktop.api({ ...profileScoped(), path: '/api/config/schema' }) } export function saveHermesConfig(config: HermesConfigRecord): Promise<{ ok: boolean }> { return window.hermesDesktop.api<{ ok: boolean }>({ ...profileScoped(), path: '/api/config', method: 'PUT', body: { config } }) } export function getEnvVars(): Promise> { return window.hermesDesktop.api>({ ...profileScoped(), path: '/api/env' }) } export function setEnvVar(key: string, value: string): Promise<{ ok: boolean }> { return window.hermesDesktop.api<{ ok: boolean }>({ ...profileScoped(), path: '/api/env', method: 'PUT', body: { key, value } }) } export function validateProviderCredential( key: string, value: string ): Promise<{ ok: boolean; reachable: boolean; message: string; models?: string[] }> { return window.hermesDesktop.api<{ ok: boolean; reachable: boolean; message: string; models?: string[] }>({ ...profileScoped(), path: '/api/providers/validate', method: 'POST', body: { key, value } }) } export function deleteEnvVar(key: string): Promise<{ ok: boolean }> { return window.hermesDesktop.api<{ ok: boolean }>({ ...profileScoped(), path: '/api/env', method: 'DELETE', body: { key } }) } export function revealEnvVar(key: string): Promise<{ key: string; value: string }> { return window.hermesDesktop.api<{ key: string; value: string }>({ ...profileScoped(), path: '/api/env/reveal', method: 'POST', body: { key } }) } export function listOAuthProviders(): Promise { return window.hermesDesktop.api({ ...profileScoped(), path: '/api/providers/oauth' }) } export function startOAuthLogin(providerId: string): Promise { return window.hermesDesktop.api({ ...profileScoped(), path: `/api/providers/oauth/${encodeURIComponent(providerId)}/start`, method: 'POST', body: {} }) } export function submitOAuthCode(providerId: string, sessionId: string, code: string): Promise { return window.hermesDesktop.api({ ...profileScoped(), path: `/api/providers/oauth/${encodeURIComponent(providerId)}/submit`, method: 'POST', body: { session_id: sessionId, code } }) } export function pollOAuthSession(providerId: string, sessionId: string): Promise { return window.hermesDesktop.api({ ...profileScoped(), path: `/api/providers/oauth/${encodeURIComponent(providerId)}/poll/${encodeURIComponent(sessionId)}` }) } export function cancelOAuthSession(sessionId: string): Promise<{ ok: boolean }> { return window.hermesDesktop.api<{ ok: boolean }>({ ...profileScoped(), path: `/api/providers/oauth/sessions/${encodeURIComponent(sessionId)}`, method: 'DELETE' }) } export function getSkills(): Promise { return window.hermesDesktop.api({ ...profileScoped(), path: '/api/skills' }) } export function toggleSkill(name: string, enabled: boolean): Promise<{ ok: boolean; name: string; enabled: boolean }> { return window.hermesDesktop.api<{ ok: boolean; name: string; enabled: boolean }>({ ...profileScoped(), path: '/api/skills/toggle', method: 'PUT', body: { name, enabled } }) } export function getToolsets(): Promise { return window.hermesDesktop.api({ ...profileScoped(), path: '/api/tools/toolsets' }) } export function toggleToolset( name: string, enabled: boolean ): Promise<{ ok: boolean; name: string; enabled: boolean }> { return window.hermesDesktop.api<{ ok: boolean; name: string; enabled: boolean }>({ ...profileScoped(), path: `/api/tools/toolsets/${encodeURIComponent(name)}`, method: 'PUT', body: { enabled } }) } export function getToolsetConfig(name: string): Promise { return window.hermesDesktop.api({ ...profileScoped(), path: `/api/tools/toolsets/${encodeURIComponent(name)}/config` }) } export function selectToolsetProvider( name: string, provider: string ): Promise<{ ok: boolean; name: string; provider: string }> { return window.hermesDesktop.api<{ ok: boolean; name: string; provider: string }>({ ...profileScoped(), path: `/api/tools/toolsets/${encodeURIComponent(name)}/provider`, method: 'PUT', body: { provider } }) } export function runToolsetPostSetup(name: string, key: string): Promise { return window.hermesDesktop.api({ ...profileScoped(), path: `/api/tools/toolsets/${encodeURIComponent(name)}/post-setup`, method: 'POST', body: { key } }) } export function getMessagingPlatforms(): Promise { return window.hermesDesktop.api({ path: '/api/messaging/platforms' }) } export function updateMessagingPlatform( platformId: string, body: MessagingPlatformUpdate ): Promise<{ ok: boolean; platform: string }> { return window.hermesDesktop.api<{ ok: boolean; platform: string }>({ path: `/api/messaging/platforms/${encodeURIComponent(platformId)}`, method: 'PUT', body }) } export function testMessagingPlatform(platformId: string): Promise { return window.hermesDesktop.api({ path: `/api/messaging/platforms/${encodeURIComponent(platformId)}/test`, method: 'POST' }) } export function getCronJobs(): Promise { return window.hermesDesktop.api({ path: '/api/cron/jobs' }) } export function getCronJob(jobId: string): Promise { return window.hermesDesktop.api({ path: `/api/cron/jobs/${encodeURIComponent(jobId)}` }) } export async function getCronJobRuns(jobId: string, limit = 20): Promise { const { runs } = await window.hermesDesktop.api<{ runs: SessionInfo[] }>({ path: `/api/cron/jobs/${encodeURIComponent(jobId)}/runs?limit=${limit}` }) return runs ?? [] } export function createCronJob(body: CronJobCreatePayload): Promise { return window.hermesDesktop.api({ path: '/api/cron/jobs', method: 'POST', body }) } export function updateCronJob(jobId: string, updates: CronJobUpdates): Promise { return window.hermesDesktop.api({ path: `/api/cron/jobs/${encodeURIComponent(jobId)}`, method: 'PUT', body: { updates } }) } export function pauseCronJob(jobId: string): Promise { return window.hermesDesktop.api({ path: `/api/cron/jobs/${encodeURIComponent(jobId)}/pause`, method: 'POST' }) } export function resumeCronJob(jobId: string): Promise { return window.hermesDesktop.api({ path: `/api/cron/jobs/${encodeURIComponent(jobId)}/resume`, method: 'POST' }) } export function triggerCronJob(jobId: string): Promise { return window.hermesDesktop.api({ path: `/api/cron/jobs/${encodeURIComponent(jobId)}/trigger`, method: 'POST' }) } export function deleteCronJob(jobId: string): Promise<{ ok: boolean }> { return window.hermesDesktop.api<{ ok: boolean }>({ path: `/api/cron/jobs/${encodeURIComponent(jobId)}`, method: 'DELETE' }) } export function getProfiles(): Promise { return window.hermesDesktop.api({ path: '/api/profiles' }) } export function createProfile(body: ProfileCreatePayload): Promise<{ name: string; ok: boolean; path: string }> { return window.hermesDesktop.api<{ name: string; ok: boolean; path: string }>({ path: '/api/profiles', method: 'POST', body }) } export function renameProfile(name: string, newName: string): Promise<{ name: string; ok: boolean; path: string }> { return window.hermesDesktop.api<{ name: string; ok: boolean; path: string }>({ path: `/api/profiles/${encodeURIComponent(name)}`, method: 'PATCH', body: { new_name: newName } }) } export function deleteProfile(name: string): Promise<{ ok: boolean; path: string }> { return window.hermesDesktop.api<{ ok: boolean; path: string }>({ path: `/api/profiles/${encodeURIComponent(name)}`, method: 'DELETE' }) } export function getProfileSoul(name: string): Promise { return window.hermesDesktop.api({ path: `/api/profiles/${encodeURIComponent(name)}/soul` }) } export function updateProfileSoul(name: string, content: string): Promise<{ ok: boolean }> { return window.hermesDesktop.api<{ ok: boolean }>({ path: `/api/profiles/${encodeURIComponent(name)}/soul`, method: 'PUT', body: { content } }) } export function getProfileSetupCommand(name: string): Promise { return window.hermesDesktop.api({ path: `/api/profiles/${encodeURIComponent(name)}/setup-command` }) } export function getUsageAnalytics(days = 30): Promise { return window.hermesDesktop.api({ ...profileScoped(), path: `/api/analytics/usage?days=${Math.max(1, Math.floor(days))}` }) } export function getGlobalModelOptions(): Promise { return window.hermesDesktop.api({ ...profileScoped(), path: '/api/model/options' }) } export interface RecommendedDefaultModel { provider: string model: string /** True/false for Nous (free vs paid tier); null for other providers. */ free_tier: boolean | null } // Recommended default model for a freshly-authenticated provider. Mirrors the // curation `hermes model` does — for Nous it honors the free/paid tier so a // free user gets a free model instead of a paid default. export function getRecommendedDefaultModel(provider: string): Promise { return window.hermesDesktop.api({ ...profileScoped(), path: `/api/model/recommended-default?provider=${encodeURIComponent(provider)}` }) } export function setGlobalModel( provider: string, model: string ): Promise<{ ok: boolean; provider: string; model: string }> { return window.hermesDesktop.api<{ ok: boolean; provider: string; model: string }>({ ...profileScoped(), path: '/api/model/set', method: 'POST', body: { scope: 'main', provider, model } }) } export function getAuxiliaryModels(): Promise { return window.hermesDesktop.api({ ...profileScoped(), path: '/api/model/auxiliary' }) } export function setModelAssignment(body: ModelAssignmentRequest): Promise { return window.hermesDesktop.api({ ...profileScoped(), path: '/api/model/set', method: 'POST', body }) } export function restartGateway(): Promise { return window.hermesDesktop.api({ path: '/api/gateway/restart', method: 'POST' }) } export function updateHermes(): Promise { return window.hermesDesktop.api({ path: '/api/hermes/update', method: 'POST' }) } export function getActionStatus(name: string, lines = 200): Promise { return window.hermesDesktop.api({ path: `/api/actions/${encodeURIComponent(name)}/status?lines=${Math.max(1, lines)}` }) } export function transcribeAudio(dataUrl: string, mimeType?: string): Promise { return window.hermesDesktop.api({ path: '/api/audio/transcribe', method: 'POST', body: { data_url: dataUrl, mime_type: mimeType } }) } export function speakText(text: string): Promise { return window.hermesDesktop.api({ path: '/api/audio/speak', method: 'POST', body: { text } }) } export function getElevenLabsVoices(): Promise { return window.hermesDesktop.api({ path: '/api/audio/elevenlabs/voices' }) }