feat(desktop): route REST through registered sources

This commit is contained in:
David Dudok de Wit
2026-08-17 11:04:49 +02:00
committed by Teknium
parent 8e5f55f83b
commit 7245b022db
9 changed files with 298 additions and 120 deletions

View File

@@ -455,10 +455,13 @@ test('apiRequestRegistryConnectionId extracts a genuinely non-local connection i
assert.equal(apiRequestRegistryConnectionId({ connectionId: ' gw-1 ', path: '/x' }), 'gw-1')
})
test('apiRequestRegistryConnectionId resolves null for the legacy/local routes', () => {
test('apiRequestRegistryConnectionId preserves an explicit local registry route', () => {
assert.equal(apiRequestRegistryConnectionId({ connectionId: 'local', path: '/x' }), 'local')
})
test('apiRequestRegistryConnectionId resolves null for unscoped legacy routes', () => {
assert.equal(apiRequestRegistryConnectionId({ path: '/api/cron/jobs' }), null)
assert.equal(apiRequestRegistryConnectionId({ connectionId: '', path: '/x' }), null)
assert.equal(apiRequestRegistryConnectionId({ connectionId: 'local', path: '/x' }), null)
assert.equal(apiRequestRegistryConnectionId({ connectionId: null, path: '/x' }), null)
assert.equal(apiRequestRegistryConnectionId(null), null)
assert.equal(apiRequestRegistryConnectionId(undefined), null)

View File

@@ -750,15 +750,16 @@ function pathWithProfileScope(path, profile) {
/**
* Registry connection a REST request is explicitly pinned to, or null for the
* legacy profile-routed path. `''`/`'local'` mean the local pool — callers
* only detour through the registry for a genuinely non-local connection, so
* single-source users keep the byte-identical v1 route.
* legacy profile-routed path. An explicit `local` id must stay registry-scoped:
* when the v1 route is remote, only the registry resolver can force the request
* back to this device. Single-source users omit the id and keep the
* byte-identical v1 route.
*/
function apiRequestRegistryConnectionId(request): null | string {
const raw = request && typeof request === 'object' ? (request as { connectionId?: unknown }).connectionId : ''
const id = String(raw ?? '').trim()
if (!id || id === 'local') {
if (!id) {
return null
}

View File

@@ -28,6 +28,7 @@ import {
REGISTRY_VERSION,
rememberSshEnumeration,
removeConnection,
resolvedConnectionId,
resolveRegistryLocalRoute,
setPrimaryConnection,
shouldDeferLocalEnumeration,
@@ -54,6 +55,49 @@ test('labelSlug kebab-cases and never returns empty for non-empty input', () =>
assert.equal(labelSlug('!!!'), 'connection')
})
test('resolvedConnectionId identifies local and migrated remote descriptors', () => {
const registry = migrateV1ToRegistry({
mode: 'local',
profiles: {
personal: { mode: 'remote', url: 'https://personal.example:9443/', authMode: 'token' },
work: { mode: 'ssh', host: 'work-host', user: 'root' }
}
})
const personal = registry.connections.find(connection => connection.kind === 'remote')
const work = registry.connections.find(connection => connection.kind === 'ssh')
assert.equal(resolvedConnectionId(registry, { mode: 'local' }), LOCAL_CONNECTION_ID)
assert.equal(
resolvedConnectionId(registry, {
baseUrl: 'https://personal.example:9443',
mode: 'remote',
remoteKind: 'url'
}),
personal?.id
)
assert.equal(
resolvedConnectionId(registry, {
baseUrl: 'http://127.0.0.1:49152',
mode: 'remote',
remoteHost: 'root@work-host',
remoteKind: 'ssh'
}),
work?.id
)
})
test('resolvedConnectionId does not guess an unregistered remote', () => {
assert.equal(
resolvedConnectionId(emptyRegistry(), {
baseUrl: 'https://unknown.example',
mode: 'remote',
remoteKind: 'url'
}),
null
)
})
test('agentHandle bare when unique, @name-device shape when duplicated', () => {
assert.equal(agentHandle('research', 'Homelab', false), 'research')
assert.equal(agentHandle('research', 'Homelab', true), 'research-homelab')

View File

@@ -178,6 +178,79 @@ export interface RegistryLocalRoute {
poolKey: string
}
export interface ResolvedConnectionDescriptor {
baseUrl?: string
mode?: 'local' | 'remote'
remoteHost?: string
remoteKind?: 'cloud' | 'ssh' | 'url'
}
/**
* Recover registry identity for a descriptor resolved through the legacy v1
* profile path. Registry-scoped routes already carry `connectionId`; this
* bridge keeps migrated per-profile remotes truthful until v1 is retired.
*/
export function resolvedConnectionId(
registry: ConnectionRegistry,
descriptor: ResolvedConnectionDescriptor
): null | string {
if (descriptor.mode === 'local') {
return registry.connections.find(connection => connection.kind === 'local')?.id ?? null
}
if (descriptor.mode !== 'remote') {
return null
}
if (descriptor.remoteKind === 'ssh') {
const remoteHost = String(descriptor.remoteHost || '')
.trim()
.toLowerCase()
if (!remoteHost) {
return null
}
return (
registry.connections.find(connection => {
if (connection.kind !== 'ssh') {
return false
}
const host = String(connection.host || '')
.trim()
.toLowerCase()
const target = connection.user ? `${String(connection.user).trim().toLowerCase()}@${host}` : host
return target === remoteHost
})?.id ?? null
)
}
let baseUrl = ''
try {
baseUrl = normalizeRemoteBaseUrl(descriptor.baseUrl)
} catch {
return null
}
return (
registry.connections.find(connection => {
if (connection.kind !== 'cloud' && connection.kind !== 'remote') {
return false
}
try {
return normalizeRemoteBaseUrl(connection.url) === baseUrl
} catch {
return false
}
})?.id ?? null
)
}
/**
* How the registry's 'local' entry resolves a backend for `profile`.
*

View File

@@ -106,6 +106,7 @@ import {
normalizeRegistry,
rememberSshEnumeration,
removeConnection,
resolvedConnectionId,
resolveRegistryLocalRoute,
setPrimaryConnection,
shouldDeferLocalEnumeration,
@@ -11941,7 +11942,12 @@ function createWindow() {
})
}
ipcMain.handle('hermes:connection', async (_event, profile) => ensureBackend(profile))
ipcMain.handle('hermes:connection', async (_event, profile) => {
const connection = await ensureBackend(profile)
const connectionId = resolvedConnectionId(readDesktopConnectionsRegistry(), connection)
return connectionId ? { ...connection, connectionId } : connection
})
// Registry-scoped variant: resolve a backend for (connectionId, profile).
// connectionId '' / 'local' / the registry primary all behave sensibly; the
// local kind delegates to ensureBackend when the v1 route is local, and
@@ -11949,8 +11955,11 @@ ipcMain.handle('hermes:connection', async (_event, profile) => ensureBackend(pro
// registry 'local' entry always means this machine).
ipcMain.handle('hermes:connection:for', async (_event, payload) => {
const { connectionId, profile } = payload && typeof payload === 'object' ? (payload as any) : ({} as any)
const registry = readDesktopConnectionsRegistry()
const id = String(connectionId || '').trim() || registry.primary
const connection = await ensureRegistryBackend(id, profile)
return ensureRegistryBackend(connectionId, profile)
return { ...connection, connectionId: id, registryScoped: true }
})
// Reconnect-after-wake recovery. A REMOTE primary backend has no child process,
// so the 'exit'/'error' handlers that would clear a dead connection promise never
@@ -13485,7 +13494,8 @@ async function handleHermesApiRequest(request) {
// profile's. Resolve the backend through the registry (same pool the job
// list and WS traffic use) instead of the legacy profile route; a shared
// remote/cloud host serves every profile via ?profile=, so scope the path.
// '' / 'local' fall through to the byte-identical v1 route below (#87882).
// An absent/empty id falls through to the byte-identical v1 route below.
// Explicit `local` stays registry-pinned so it cannot inherit a v1 remote.
const registryConnectionId = apiRequestRegistryConnectionId(request)
if (registryConnectionId) {

View File

@@ -661,6 +661,10 @@ export interface HermesConnection {
// secondaries carry it directly; legacy primary remotes preserve it from
// their selected stored route before dialing.
connectionId?: string
// True only when getConnectionFor explicitly resolved a v2 registry route.
// An inferred connectionId identifies the visible source but its v1 profile
// name may still be a client-side routing alias rather than a backend profile.
registryScoped?: boolean
// True only when `profile` is a request scope on the shared primary backend.
// A pooled backend also carries `profile`, so presence alone cannot identify
// the shared-primary routing case.
@@ -1076,8 +1080,8 @@ export interface HermesApiRequest {
// Route this REST call to a specific REGISTERED gateway connection (v2
// registry). Data owned by a remote gateway — cron jobs and their run
// sessions — lives in that host's state.db, so requests for it must resolve
// through the owning connection, not the local profile pool. Omit / '' /
// 'local' keep the legacy profile-routed path.
// through the owning connection, not the local profile pool. Omit / '' to
// keep the legacy profile-routed path; explicit 'local' forces this device.
connectionId?: string | null
}

View File

@@ -24,6 +24,7 @@ import {
listSidebarSessions,
pluginSocket,
resetSidebarBatchCapability,
setApiRequestConnection,
setApiRequestProfile,
speakText,
transcribeAudio,
@@ -52,6 +53,7 @@ describe('Hermes REST helpers', () => {
})
afterEach(() => {
setApiRequestConnection(null)
setApiRequestProfile(null)
vi.restoreAllMocks()
Reflect.deleteProperty(window, 'hermesDesktop')
@@ -101,6 +103,40 @@ describe('Hermes REST helpers', () => {
)
})
it('routes session, profile, and model reads through the active registry source', async () => {
api.mockImplementation(async ({ path }: { path: string }) =>
path.startsWith('/api/profiles/sessions/sidebar')
? { recents: { sessions: [] }, cron: { sessions: [] }, messaging: { sessions: [] } }
: emptySessionsResponse
)
setApiRequestConnection('personal')
await listSessions()
await listAllProfileSessions()
await listSidebarSessions({
recentsProfile: 'default',
recentsLimit: 20,
recentsExclude: [],
cronLimit: 20,
messagingLimit: 20,
messagingExclude: []
})
await getProfiles()
await getGlobalModelInfo()
for (const call of api.mock.calls) {
expect(call[0]).toEqual(expect.objectContaining({ connectionId: 'personal' }))
}
})
it('keeps an explicit This device source on REST requests', async () => {
setApiRequestConnection('local')
await getProfiles()
expect(api).toHaveBeenCalledWith(expect.objectContaining({ connectionId: 'local', path: '/api/profiles' }))
})
it('defaults missing sidebar slices to empty session arrays', async () => {
api.mockResolvedValue({})

View File

@@ -1,6 +1,6 @@
import { JsonRpcGatewayClient } from '@hermes/shared'
import type { HermesConnection } from '@/global'
import type { HermesApiRequest, HermesConnection } from '@/global'
import { reconnectBackoffDelayMs } from '@/lib/reconnect-backoff'
import { recordTranscriptTail } from '@/store/transcript-tail'
import type {
@@ -288,8 +288,9 @@ export function setApiRequestConnection(connectionId: null | string): void {
// Registry connection scope for a REST request. A registered remote gateway
// owns its own state.db — cron jobs and their run sessions live THERE — so
// requests for gateway-owned data must carry the connection id for the main
// process to route them to that host (hermes:api's registry branch). Null /
// 'local' resolves to no tag, keeping single-source users byte-identical.
// process to route them to that host (hermes:api's registry branch). Null
// resolves to no tag, keeping single-source users byte-identical; explicit
// 'local' must remain tagged when the legacy primary points elsewhere.
function connectionScoped(): { connectionId?: string } {
return _apiConnectionId ? { connectionId: _apiConnectionId } : {}
}
@@ -341,6 +342,12 @@ export function profileScopeKey(scope?: ProfileScope): string {
return (scope ?? '').trim() || 'default'
}
/** Send a REST request to the renderer's active registry source. Request-level
* routing may override the active source for an explicitly-owned resource. */
export function hermesApi<T>(request: HermesApiRequest): Promise<T> {
return window.hermesDesktop.api<T>({ ...connectionScoped(), ...request })
}
/** Registry connection id that connection-scoped WS calls should target
* (null → the local pool). Read-only twin of setApiRequestConnection. */
export function getApiRequestConnection(): null | string {
@@ -400,7 +407,7 @@ export async function pluginRest<T>(pluginId: string, path: string, opts: Plugin
const suffix = pluginPathSuffix('pluginRest', path)
return window.hermesDesktop.api<T>({
return hermesApi<T>({
path: `/api/plugins/${pluginId}${suffix}`,
method: opts.method,
body: opts.body,
@@ -496,7 +503,7 @@ export async function listSessions(
archived: 'exclude' | 'include' | 'only' = 'exclude',
order: 'created' | 'recent' = 'recent'
): Promise<PaginatedSessions> {
const result = await window.hermesDesktop.api<PaginatedSessions>({
const result = await hermesApi<PaginatedSessions>({
path:
`/api/sessions?limit=${limit}&offset=0&min_messages=${Math.max(0, minMessages)}` +
`&archived=${archived}&order=${order}`,
@@ -537,7 +544,7 @@ export async function listAllProfileSessions(
? `&exclude_sources=${encodeURIComponent(filter.excludeSources.join(','))}`
: ''
const result = await window.hermesDesktop.api<PaginatedSessions>({
const result = await hermesApi<PaginatedSessions>({
path:
`/api/profiles/sessions?limit=${limit}&offset=0&min_messages=${Math.max(0, minMessages)}` +
`&archived=${archived}&order=${order}&profile=${encodeURIComponent(profile)}${sourceParam}${excludeParam}`,
@@ -670,7 +677,7 @@ async function listSidebarSessionsLegacy(req: SidebarSessionsRequest): Promise<S
export function scanSessionPullRequests(
ids: string[]
): Promise<{ pull_requests: Record<string, { number: number; url: string }>; scanned: string[] }> {
return window.hermesDesktop.api<{
return hermesApi<{
pull_requests: Record<string, { number: number; url: string }>
scanned: string[]
}>({
@@ -703,7 +710,7 @@ export async function listSidebarSessions(req: SidebarSessionsRequest): Promise<
let result: SidebarSessionsResponse
try {
result = await window.hermesDesktop.api<SidebarSessionsResponse>({
result = await hermesApi<SidebarSessionsResponse>({
path: `/api/profiles/sessions/sidebar?${params.toString()}`,
timeoutMs: SESSION_LIST_REQUEST_TIMEOUT_MS
})
@@ -729,7 +736,7 @@ export async function listSidebarSessions(req: SidebarSessionsRequest): Promise<
// Mutations take the owning `profile` so Electron can route them to the correct
// remote backend or local profile scope. Omit for the current/default profile.
export function setSessionArchived(id: string, archived: boolean, profile?: string | null): Promise<{ ok: boolean }> {
return window.hermesDesktop.api<{ ok: boolean }>({
return hermesApi<{ ok: boolean }>({
...(profile ? { profile } : {}),
path: `/api/sessions/${encodeURIComponent(id)}`,
method: 'PATCH',
@@ -742,7 +749,7 @@ export function setSessionArchived(id: string, archived: boolean, profile?: stri
// pinned chat. Best-effort: the sidebar stays localStorage-driven for its own
// display; this only feeds the backend policy.
export function setSessionPinnedRemote(id: string, pinned: boolean, profile?: string | null): Promise<{ ok: boolean }> {
return window.hermesDesktop.api<{ ok: boolean }>({
return hermesApi<{ ok: boolean }>({
...(profile ? { profile } : {}),
path: `/api/sessions/${encodeURIComponent(id)}`,
method: 'PATCH',
@@ -755,7 +762,7 @@ export function setSessionPinnedRemote(id: string, pinned: boolean, profile?: st
// routing as the other session mutations: a remote session's row lives only
// on its remote host, so the owning profile must travel with the request.
export function setSessionUnreadRemote(id: string, unread: boolean, profile?: string | null): Promise<{ ok: boolean }> {
return window.hermesDesktop.api<{ ok: boolean }>({
return hermesApi<{ ok: boolean }>({
...(profile ? { profile } : {}),
path: `/api/sessions/${encodeURIComponent(id)}`,
method: 'PATCH',
@@ -764,7 +771,7 @@ export function setSessionUnreadRemote(id: string, unread: boolean, profile?: st
}
export function searchSessions(query: string): Promise<SessionSearchResponse> {
return window.hermesDesktop.api<SessionSearchResponse>({
return hermesApi<SessionSearchResponse>({
path: `/api/sessions/search?q=${encodeURIComponent(query)}`
})
}
@@ -776,7 +783,7 @@ export function searchSessions(query: string): Promise<SessionSearchResponse> {
export function getSession(id: string, profile?: string | null): Promise<SessionInfo> {
const suffix = profile ? `?profile=${encodeURIComponent(profile)}` : ''
return window.hermesDesktop.api<SessionInfo>({
return hermesApi<SessionInfo>({
...(profile ? { profile } : {}),
path: `/api/sessions/${encodeURIComponent(id)}${suffix}`
})
@@ -815,7 +822,7 @@ export function getSessionMessages(
const suffix = query.size ? `?${query.toString()}` : ''
return window.hermesDesktop.api<SessionMessagesResponse>({
return hermesApi<SessionMessagesResponse>({
...(profile ? { profile } : {}),
path: `/api/sessions/${encodeURIComponent(id)}/messages${suffix}`
})
@@ -918,7 +925,7 @@ export async function getAllSessionMessages(
}
export function deleteSession(id: string, profile?: string | null): Promise<{ ok: boolean }> {
return window.hermesDesktop.api<{ ok: boolean }>({
return hermesApi<{ ok: boolean }>({
...(profile ? { profile } : {}),
path: `/api/sessions/${encodeURIComponent(id)}`,
method: 'DELETE'
@@ -930,7 +937,7 @@ export function renameSession(
title: string,
profile?: string | null
): Promise<{ ok: boolean; title: string }> {
return window.hermesDesktop.api<{ ok: boolean; title: string }>({
return hermesApi<{ ok: boolean; title: string }>({
...(profile ? { profile } : {}),
path: `/api/sessions/${encodeURIComponent(id)}`,
method: 'PATCH',
@@ -939,7 +946,7 @@ export function renameSession(
}
export function getGlobalModelInfo(profile?: null | string): Promise<ModelInfoResponse> {
return window.hermesDesktop.api<ModelInfoResponse>({
return hermesApi<ModelInfoResponse>({
...profileScoped(profile),
path: '/api/model/info',
timeoutMs: STARTUP_REQUEST_TIMEOUT_MS
@@ -947,7 +954,7 @@ export function getGlobalModelInfo(profile?: null | string): Promise<ModelInfoRe
}
export function getStatus(): Promise<StatusResponse> {
return window.hermesDesktop.api<StatusResponse>({
return hermesApi<StatusResponse>({
...profileScoped(),
path: '/api/status'
})
@@ -984,14 +991,14 @@ export function getLogs(params: {
const suffix = query.toString()
return window.hermesDesktop.api<LogsResponse>({
return hermesApi<LogsResponse>({
...profileScoped(),
path: suffix ? `/api/logs?${suffix}` : '/api/logs'
})
}
export function getHermesConfig(profile?: string): Promise<HermesConfig> {
return window.hermesDesktop.api<HermesConfig>({
return hermesApi<HermesConfig>({
...profileScoped(profile),
path: '/api/config',
timeoutMs: STARTUP_REQUEST_TIMEOUT_MS
@@ -1006,7 +1013,7 @@ export function getHermesConfigRecord(profile?: ProfileScope): Promise<HermesCon
}
export function getHermesConfigDefaults(): Promise<HermesConfigRecord> {
return window.hermesDesktop.api<HermesConfigRecord>({
return hermesApi<HermesConfigRecord>({
...profileScoped(),
path: '/api/config/defaults',
timeoutMs: STARTUP_REQUEST_TIMEOUT_MS
@@ -1014,14 +1021,14 @@ export function getHermesConfigDefaults(): Promise<HermesConfigRecord> {
}
export function getHermesConfigSchema(profile?: null | string): Promise<ConfigSchemaResponse> {
return window.hermesDesktop.api<ConfigSchemaResponse>({
return hermesApi<ConfigSchemaResponse>({
...profileScoped(profile),
path: '/api/config/schema'
})
}
export function saveHermesConfig(config: HermesConfigRecord, profile?: null | string): Promise<{ ok: boolean }> {
return window.hermesDesktop.api<{ ok: boolean }>({
return hermesApi<{ ok: boolean }>({
...profileScoped(profile),
path: '/api/config',
method: 'PUT',
@@ -1031,7 +1038,7 @@ export function saveHermesConfig(config: HermesConfigRecord, profile?: null | st
// surface=declared serves the curated desktop schema; the dashboard consumes the raw plugin schema.
export function getMemoryProviderConfig(provider: string, profile?: null | string): Promise<MemoryProviderConfig> {
return window.hermesDesktop.api<MemoryProviderConfig>({
return hermesApi<MemoryProviderConfig>({
...profileScoped(profile),
path: `/api/memory/providers/${encodeURIComponent(provider)}/config?surface=declared`
})
@@ -1042,7 +1049,7 @@ export function saveMemoryProviderConfig(
values: Record<string, string>,
profile?: null | string
): Promise<{ ok: boolean }> {
return window.hermesDesktop.api<{ ok: boolean }>({
return hermesApi<{ ok: boolean }>({
...profileScoped(profile),
path: `/api/memory/providers/${encodeURIComponent(provider)}/config?surface=declared`,
method: 'PUT',
@@ -1051,7 +1058,7 @@ export function saveMemoryProviderConfig(
}
export function getEnvVars(profile?: null | string): Promise<Record<string, EnvVarInfo>> {
return window.hermesDesktop.api<Record<string, EnvVarInfo>>({
return hermesApi<Record<string, EnvVarInfo>>({
...profileScoped(profile),
path: '/api/env'
})
@@ -1071,7 +1078,7 @@ export function validateProviderCredential(
value: string,
apiKey?: string
): Promise<{ ok: boolean; reachable: boolean; message: string; models?: string[] }> {
return window.hermesDesktop.api<{ ok: boolean; reachable: boolean; message: string; models?: string[] }>({
return hermesApi<{ ok: boolean; reachable: boolean; message: string; models?: string[] }>({
...profileScoped(),
path: '/api/providers/validate',
method: 'POST',
@@ -1080,14 +1087,14 @@ export function validateProviderCredential(
}
export function getCustomEndpoints(): Promise<CustomEndpointsResponse> {
return window.hermesDesktop.api<CustomEndpointsResponse>({
return hermesApi<CustomEndpointsResponse>({
...profileScoped(),
path: '/api/providers/custom-endpoints'
})
}
export function saveCustomEndpoint(endpoint: CustomEndpointUpdate): Promise<CustomEndpointsResponse> {
return window.hermesDesktop.api<CustomEndpointsResponse>({
return hermesApi<CustomEndpointsResponse>({
...profileScoped(),
path: '/api/providers/custom-endpoints',
method: 'POST',
@@ -1096,7 +1103,7 @@ export function saveCustomEndpoint(endpoint: CustomEndpointUpdate): Promise<Cust
}
export function validateCustomEndpoint(endpoint: CustomEndpointUpdate): Promise<CustomEndpointValidationResponse> {
return window.hermesDesktop.api<CustomEndpointValidationResponse>({
return hermesApi<CustomEndpointValidationResponse>({
path: '/api/providers/custom-endpoints/validate',
method: 'POST',
body: endpoint
@@ -1104,7 +1111,7 @@ export function validateCustomEndpoint(endpoint: CustomEndpointUpdate): Promise<
}
export function activateCustomEndpoint(id: string): Promise<{ ok: boolean; provider: string; model: string }> {
return window.hermesDesktop.api<{ ok: boolean; provider: string; model: string }>({
return hermesApi<{ ok: boolean; provider: string; model: string }>({
...profileScoped(),
path: `/api/providers/custom-endpoints/${encodeURIComponent(id)}/activate`,
method: 'POST'
@@ -1112,7 +1119,7 @@ export function activateCustomEndpoint(id: string): Promise<{ ok: boolean; provi
}
export function deleteCustomEndpoint(id: string): Promise<CustomEndpointsResponse> {
return window.hermesDesktop.api<CustomEndpointsResponse>({
return hermesApi<CustomEndpointsResponse>({
...profileScoped(),
path: `/api/providers/custom-endpoints/${encodeURIComponent(id)}`,
method: 'DELETE'
@@ -1138,14 +1145,14 @@ export function revealEnvVar(key: string, profile?: ProfileScope): Promise<{ key
}
export function listOAuthProviders(): Promise<OAuthProvidersResponse> {
return window.hermesDesktop.api<OAuthProvidersResponse>({
return hermesApi<OAuthProvidersResponse>({
...profileScoped(),
path: '/api/providers/oauth'
})
}
export function disconnectOAuthProvider(providerId: string): Promise<{ ok: boolean; provider: string }> {
return window.hermesDesktop.api<{ ok: boolean; provider: string }>({
return hermesApi<{ ok: boolean; provider: string }>({
...profileScoped(),
path: `/api/providers/oauth/${encodeURIComponent(providerId)}`,
method: 'DELETE'
@@ -1162,7 +1169,7 @@ export function startOAuthLogin(providerId: string, profile?: ProfileScope): Pro
}
export function submitOAuthCode(providerId: string, sessionId: string, code: string): Promise<OAuthSubmitResponse> {
return window.hermesDesktop.api<OAuthSubmitResponse>({
return hermesApi<OAuthSubmitResponse>({
...profileScoped(),
path: `/api/providers/oauth/${encodeURIComponent(providerId)}/submit`,
method: 'POST',
@@ -1182,7 +1189,7 @@ export function pollOAuthSession(
}
export function cancelOAuthSession(sessionId: string): Promise<{ ok: boolean }> {
return window.hermesDesktop.api<{ ok: boolean }>({
return hermesApi<{ ok: boolean }>({
...profileScoped(),
path: `/api/providers/oauth/sessions/${encodeURIComponent(sessionId)}`,
method: 'DELETE'
@@ -1195,7 +1202,7 @@ export function startMemoryProviderOAuth(
provider: string,
profile?: null | string
): Promise<MemoryProviderOAuthStatus> {
return window.hermesDesktop.api<MemoryProviderOAuthStatus>({
return hermesApi<MemoryProviderOAuthStatus>({
...profileScoped(profile),
path: `/api/memory/providers/${encodeURIComponent(provider)}/oauth/start`,
method: 'POST'
@@ -1206,7 +1213,7 @@ export function getMemoryProviderOAuthStatus(
provider: string,
profile?: null | string
): Promise<MemoryProviderOAuthStatus> {
return window.hermesDesktop.api<MemoryProviderOAuthStatus>({
return hermesApi<MemoryProviderOAuthStatus>({
...profileScoped(profile),
path: `/api/memory/providers/${encodeURIComponent(provider)}/oauth/status`
})
@@ -1232,7 +1239,7 @@ export function getSkillContent(
}
export function getStarmapGraph(): Promise<StarmapGraph> {
return window.hermesDesktop.api<StarmapGraph>({
return hermesApi<StarmapGraph>({
...profileScoped(),
// Backend REST contract — stays /api/learning even though the UI feature is
// now "star map". Renaming this would break against an un-upgraded backend.
@@ -1355,7 +1362,7 @@ export function getMcpOAuthFlow(flowId: string, profile?: ProfileScope): Promise
/** Cancel an in-flight MCP OAuth flow server-side, freeing the per-server
* "already in progress" slot so a retry doesn't 409. */
export function cancelMcpOAuthFlow(flowId: string, profile?: null | string): Promise<{ ok: boolean; status: string }> {
return window.hermesDesktop.api<{ ok: boolean; status: string }>({
return hermesApi<{ ok: boolean; status: string }>({
...profileScoped(profile),
path: `/api/mcp/oauth/flows/${encodeURIComponent(flowId)}`,
method: 'DELETE'
@@ -1463,14 +1470,14 @@ export function runToolsetPostSetup(
}
export function getTerminalBackends(): Promise<TerminalBackendsResponse> {
return window.hermesDesktop.api<TerminalBackendsResponse>({
return hermesApi<TerminalBackendsResponse>({
...profileScoped(),
path: '/api/tools/terminal/backends'
})
}
export function selectTerminalBackend(backend: string): Promise<{ ok: boolean; backend: string }> {
return window.hermesDesktop.api<{ ok: boolean; backend: string }>({
return hermesApi<{ ok: boolean; backend: string }>({
...profileScoped(),
path: '/api/tools/terminal/backend',
method: 'PUT',
@@ -1479,14 +1486,14 @@ export function selectTerminalBackend(backend: string): Promise<{ ok: boolean; b
}
export function getComputerUseStatus(): Promise<ComputerUseStatus> {
return window.hermesDesktop.api<ComputerUseStatus>({
return hermesApi<ComputerUseStatus>({
...profileScoped(),
path: '/api/tools/computer-use/status'
})
}
export function grantComputerUsePermissions(): Promise<ActionResponse> {
return window.hermesDesktop.api<ActionResponse>({
return hermesApi<ActionResponse>({
...profileScoped(),
path: '/api/tools/computer-use/permissions/grant',
method: 'POST'
@@ -1494,7 +1501,7 @@ export function grantComputerUsePermissions(): Promise<ActionResponse> {
}
export function getMessagingPlatforms(profile?: null | string): Promise<MessagingPlatformsResponse> {
return window.hermesDesktop.api<MessagingPlatformsResponse>({
return hermesApi<MessagingPlatformsResponse>({
...profileScoped(profile),
path: '/api/messaging/platforms'
})
@@ -1505,7 +1512,7 @@ export function updateMessagingPlatform(
body: MessagingPlatformUpdate,
profile?: null | string
): Promise<{ ok: boolean; platform: string }> {
return window.hermesDesktop.api<{ ok: boolean; platform: string }>({
return hermesApi<{ ok: boolean; platform: string }>({
...profileScoped(profile),
path: `/api/messaging/platforms/${encodeURIComponent(platformId)}`,
method: 'PUT',
@@ -1517,7 +1524,7 @@ export function testMessagingPlatform(
platformId: string,
profile?: null | string
): Promise<MessagingPlatformTestResponse> {
return window.hermesDesktop.api<MessagingPlatformTestResponse>({
return hermesApi<MessagingPlatformTestResponse>({
...profileScoped(profile),
path: `/api/messaging/platforms/${encodeURIComponent(platformId)}/test`,
method: 'POST'
@@ -1532,7 +1539,7 @@ export function testMessagingPlatform(
// a row they can already see.
export function getPairing(profile?: null | string): Promise<PairingResponse> {
return window.hermesDesktop.api<PairingResponse>({
return hermesApi<PairingResponse>({
...profileScoped(profile),
path: '/api/pairing'
})
@@ -1543,7 +1550,7 @@ export function approvePairing(
requestId: string,
profile?: null | string
): Promise<{ ok: boolean; user: PairingUser }> {
return window.hermesDesktop.api<{ ok: boolean; user: PairingUser }>({
return hermesApi<{ ok: boolean; user: PairingUser }>({
...profileScoped(profile),
path: '/api/pairing/approve',
method: 'POST',
@@ -1554,7 +1561,7 @@ export function approvePairing(
}
export function revokePairing(platform: string, userId: string, profile?: null | string): Promise<{ ok: boolean }> {
return window.hermesDesktop.api<{ ok: boolean }>({
return hermesApi<{ ok: boolean }>({
...profileScoped(profile),
path: '/api/pairing/revoke',
method: 'POST',
@@ -1568,14 +1575,14 @@ export function revokePairing(platform: string, userId: string, profile?: null |
// best-effort restarts the gateway; subscription changes hot-reload.
export function getWebhooks(): Promise<WebhooksResponse> {
return window.hermesDesktop.api<WebhooksResponse>({
return hermesApi<WebhooksResponse>({
...profileScoped(),
path: '/api/webhooks'
})
}
export function enableWebhooks(): Promise<WebhookEnableResponse> {
return window.hermesDesktop.api<WebhookEnableResponse>({
return hermesApi<WebhookEnableResponse>({
...profileScoped(),
path: '/api/webhooks/enable',
method: 'POST'
@@ -1583,7 +1590,7 @@ export function enableWebhooks(): Promise<WebhookEnableResponse> {
}
export function createWebhook(body: WebhookCreatePayload): Promise<WebhookCreateResponse> {
return window.hermesDesktop.api<WebhookCreateResponse>({
return hermesApi<WebhookCreateResponse>({
...profileScoped(),
path: '/api/webhooks',
method: 'POST',
@@ -1592,7 +1599,7 @@ export function createWebhook(body: WebhookCreatePayload): Promise<WebhookCreate
}
export function deleteWebhook(name: string): Promise<{ ok: boolean }> {
return window.hermesDesktop.api<{ ok: boolean }>({
return hermesApi<{ ok: boolean }>({
...profileScoped(),
path: `/api/webhooks/${encodeURIComponent(name)}`,
method: 'DELETE'
@@ -1603,7 +1610,7 @@ export function setWebhookEnabled(
name: string,
enabled: boolean
): Promise<{ enabled: boolean; name: string; ok: boolean }> {
return window.hermesDesktop.api<{ enabled: boolean; name: string; ok: boolean }>({
return hermesApi<{ enabled: boolean; name: string; ok: boolean }>({
...profileScoped(),
path: `/api/webhooks/${encodeURIComponent(name)}/enabled`,
method: 'PUT',
@@ -1619,7 +1626,7 @@ export function setWebhookEnabled(
export function getCronJobs(profile?: string): Promise<CronJob[]> {
const suffix = profile ? `?profile=${encodeURIComponent(profile)}` : ''
return window.hermesDesktop.api<CronJob[]>({
return hermesApi<CronJob[]>({
...profileScoped(),
...connectionScoped(),
path: `/api/cron/jobs${suffix}`,
@@ -1628,7 +1635,7 @@ export function getCronJobs(profile?: string): Promise<CronJob[]> {
}
export function getCronJob(jobId: string): Promise<CronJob> {
return window.hermesDesktop.api<CronJob>({
return hermesApi<CronJob>({
...profileScoped(),
...connectionScoped(),
path: `/api/cron/jobs/${encodeURIComponent(jobId)}`
@@ -1636,7 +1643,7 @@ export function getCronJob(jobId: string): Promise<CronJob> {
}
export async function getCronJobRuns(jobId: string, limit = 20): Promise<SessionInfo[]> {
const { runs } = await window.hermesDesktop.api<{ runs: SessionInfo[] }>({
const { runs } = await hermesApi<{ runs: SessionInfo[] }>({
...profileScoped(),
...connectionScoped(),
path: `/api/cron/jobs/${encodeURIComponent(jobId)}/runs?limit=${limit}`
@@ -1649,7 +1656,7 @@ export async function getCronJobRuns(jobId: string, limit = 20): Promise<Session
// gateways). Both the manual cron editor and the blueprint dialog use this so
// they never offer a platform that isn't connected. Mirrors the dashboard.
export async function getCronDeliveryTargets(): Promise<CronDeliveryTarget[]> {
const { targets } = await window.hermesDesktop.api<{ targets: CronDeliveryTarget[] }>({
const { targets } = await hermesApi<{ targets: CronDeliveryTarget[] }>({
...profileScoped(),
...connectionScoped(),
path: '/api/cron/delivery-targets'
@@ -1659,7 +1666,7 @@ export async function getCronDeliveryTargets(): Promise<CronDeliveryTarget[]> {
}
export function createCronJob(body: CronJobCreatePayload): Promise<CronJob> {
return window.hermesDesktop.api<CronJob>({
return hermesApi<CronJob>({
...profileScoped(),
...connectionScoped(),
path: '/api/cron/jobs',
@@ -1669,7 +1676,7 @@ export function createCronJob(body: CronJobCreatePayload): Promise<CronJob> {
}
export function updateCronJob(jobId: string, updates: CronJobUpdates): Promise<CronJob> {
return window.hermesDesktop.api<CronJob>({
return hermesApi<CronJob>({
...profileScoped(),
...connectionScoped(),
path: `/api/cron/jobs/${encodeURIComponent(jobId)}`,
@@ -1679,7 +1686,7 @@ export function updateCronJob(jobId: string, updates: CronJobUpdates): Promise<C
}
export function pauseCronJob(jobId: string): Promise<CronJob> {
return window.hermesDesktop.api<CronJob>({
return hermesApi<CronJob>({
...profileScoped(),
...connectionScoped(),
path: `/api/cron/jobs/${encodeURIComponent(jobId)}/pause`,
@@ -1688,7 +1695,7 @@ export function pauseCronJob(jobId: string): Promise<CronJob> {
}
export function resumeCronJob(jobId: string): Promise<CronJob> {
return window.hermesDesktop.api<CronJob>({
return hermesApi<CronJob>({
...profileScoped(),
...connectionScoped(),
path: `/api/cron/jobs/${encodeURIComponent(jobId)}/resume`,
@@ -1697,7 +1704,7 @@ export function resumeCronJob(jobId: string): Promise<CronJob> {
}
export function triggerCronJob(jobId: string): Promise<CronJob> {
return window.hermesDesktop.api<CronJob>({
return hermesApi<CronJob>({
...profileScoped(),
...connectionScoped(),
path: `/api/cron/jobs/${encodeURIComponent(jobId)}/trigger`,
@@ -1707,7 +1714,7 @@ export function triggerCronJob(jobId: string): Promise<CronJob> {
}
export function deleteCronJob(jobId: string): Promise<{ ok: boolean }> {
return window.hermesDesktop.api<{ ok: boolean }>({
return hermesApi<{ ok: boolean }>({
...profileScoped(),
...connectionScoped(),
path: `/api/cron/jobs/${encodeURIComponent(jobId)}`,
@@ -1727,7 +1734,7 @@ export function deleteCronJob(jobId: string): Promise<{ ok: boolean }> {
// routing. instantiate creates a real per-profile job, so it names the target
// profile explicitly via ?profile=. This mirrors the dashboard's api.ts.
export function getAutomationBlueprints(): Promise<{ blueprints: AutomationBlueprint[] }> {
return window.hermesDesktop.api<{ blueprints: AutomationBlueprint[] }>({
return hermesApi<{ blueprints: AutomationBlueprint[] }>({
...profileScoped(),
...connectionScoped(),
path: '/api/cron/blueprints',
@@ -1739,7 +1746,7 @@ export function instantiateAutomationBlueprint(
body: { blueprint: string; values: Record<string, string> },
profile: string
): Promise<CronJob> {
return window.hermesDesktop.api<CronJob>({
return hermesApi<CronJob>({
...profileScoped(),
...connectionScoped(),
path: `/api/cron/blueprints/instantiate?profile=${encodeURIComponent(profile)}`,
@@ -1749,14 +1756,14 @@ export function instantiateAutomationBlueprint(
}
export function getProfiles(): Promise<ProfilesResponse> {
return window.hermesDesktop.api<ProfilesResponse>({
return hermesApi<ProfilesResponse>({
path: '/api/profiles',
timeoutMs: STARTUP_REQUEST_TIMEOUT_MS
})
}
export function createProfile(body: ProfileCreatePayload): Promise<{ name: string; ok: boolean; path: string }> {
return window.hermesDesktop.api<{ name: string; ok: boolean; path: string }>({
return hermesApi<{ name: string; ok: boolean; path: string }>({
path: '/api/profiles',
method: 'POST',
body
@@ -1764,7 +1771,7 @@ export function createProfile(body: ProfileCreatePayload): Promise<{ name: strin
}
export function renameProfile(name: string, newName: string): Promise<{ name: string; ok: boolean; path: string }> {
return window.hermesDesktop.api<{ name: string; ok: boolean; path: string }>({
return hermesApi<{ name: string; ok: boolean; path: string }>({
path: `/api/profiles/${encodeURIComponent(name)}`,
method: 'PATCH',
body: { new_name: newName }
@@ -1772,20 +1779,20 @@ export function renameProfile(name: string, newName: string): Promise<{ name: st
}
export function deleteProfile(name: string): Promise<{ ok: boolean; path: string }> {
return window.hermesDesktop.api<{ ok: boolean; path: string }>({
return hermesApi<{ ok: boolean; path: string }>({
path: `/api/profiles/${encodeURIComponent(name)}`,
method: 'DELETE'
})
}
export function getProfileSoul(name: string): Promise<ProfileSoul> {
return window.hermesDesktop.api<ProfileSoul>({
return hermesApi<ProfileSoul>({
path: `/api/profiles/${encodeURIComponent(name)}/soul`
})
}
export function updateProfileSoul(name: string, content: string): Promise<{ ok: boolean }> {
return window.hermesDesktop.api<{ ok: boolean }>({
return hermesApi<{ ok: boolean }>({
path: `/api/profiles/${encodeURIComponent(name)}/soul`,
method: 'PUT',
body: { content }
@@ -1793,7 +1800,7 @@ export function updateProfileSoul(name: string, content: string): Promise<{ ok:
}
export function getProfileSetupCommand(name: string): Promise<ProfileSetupCommand> {
return window.hermesDesktop.api<ProfileSetupCommand>({
return hermesApi<ProfileSetupCommand>({
path: `/api/profiles/${encodeURIComponent(name)}/setup-command`
})
}
@@ -1805,7 +1812,7 @@ export function exportProfileArchive(
name: string,
opts: { extraFiles?: Record<string, string>; output?: string } = {}
): Promise<{ archive: string; ok: boolean }> {
return window.hermesDesktop.api<{ archive: string; ok: boolean }>({
return hermesApi<{ archive: string; ok: boolean }>({
path: `/api/profiles/${encodeURIComponent(name)}/export`,
method: 'POST',
body: { extra_files: opts.extraFiles ?? {}, output: opts.output ?? '' },
@@ -1820,7 +1827,7 @@ export function importProfileArchive(
archive: string,
name?: string
): Promise<{ desktop: null | ProfileDesktopOverlay; name: string; ok: boolean; path: string }> {
return window.hermesDesktop.api<{ desktop: null | ProfileDesktopOverlay; name: string; ok: boolean; path: string }>({
return hermesApi<{ desktop: null | ProfileDesktopOverlay; name: string; ok: boolean; path: string }>({
path: '/api/profiles/import',
method: 'POST',
body: { archive, name: name || null },
@@ -1857,7 +1864,7 @@ export function getGlobalModelOptions(
params.set('explicit_only', '1')
}
return window.hermesDesktop.api<ModelOptionsResponse>({
return hermesApi<ModelOptionsResponse>({
...profileScoped(profile),
path: params.size > 0 ? `/api/model/options?${params.toString()}` : '/api/model/options',
timeoutMs: STARTUP_REQUEST_TIMEOUT_MS
@@ -1878,7 +1885,7 @@ export function getRecommendedDefaultModel(
provider: string,
profile?: null | string
): Promise<RecommendedDefaultModel> {
return window.hermesDesktop.api<RecommendedDefaultModel>({
return hermesApi<RecommendedDefaultModel>({
...profileScoped(profile),
path: `/api/model/recommended-default?provider=${encodeURIComponent(provider)}`
})
@@ -1888,7 +1895,7 @@ export function setGlobalModel(
provider: string,
model: string
): Promise<{ ok: boolean; provider: string; model: string }> {
return window.hermesDesktop.api<{ ok: boolean; provider: string; model: string }>({
return hermesApi<{ ok: boolean; provider: string; model: string }>({
...profileScoped(),
path: '/api/model/set',
method: 'POST',
@@ -1901,14 +1908,14 @@ export function setGlobalModel(
}
export function getAuxiliaryModels(profile?: null | string): Promise<AuxiliaryModelsResponse> {
return window.hermesDesktop.api<AuxiliaryModelsResponse>({
return hermesApi<AuxiliaryModelsResponse>({
...profileScoped(profile),
path: '/api/model/auxiliary'
})
}
export function getMoaModels(profile?: null | string): Promise<MoaConfigResponse> {
return window.hermesDesktop.api<MoaConfigResponse>({
return hermesApi<MoaConfigResponse>({
...profileScoped(profile),
path: '/api/model/moa'
})
@@ -1918,7 +1925,7 @@ export function saveMoaModels(
body: MoaConfigResponse,
profile?: null | string
): Promise<MoaConfigResponse & { ok: boolean }> {
return window.hermesDesktop.api<MoaConfigResponse & { ok: boolean }>({
return hermesApi<MoaConfigResponse & { ok: boolean }>({
...profileScoped(profile),
path: '/api/model/moa',
method: 'PUT',
@@ -1930,7 +1937,7 @@ export function setModelAssignment(
body: ModelAssignmentRequest,
profile?: null | string
): Promise<ModelAssignmentResponse> {
return window.hermesDesktop.api<ModelAssignmentResponse>({
return hermesApi<ModelAssignmentResponse>({
...profileScoped(profile),
path: '/api/model/set',
method: 'POST',
@@ -1939,7 +1946,7 @@ export function setModelAssignment(
}
export function restartGateway(): Promise<ActionResponse> {
return window.hermesDesktop.api<ActionResponse>({
return hermesApi<ActionResponse>({
...profileScoped(),
path: '/api/gateway/restart',
method: 'POST'
@@ -1947,7 +1954,7 @@ export function restartGateway(): Promise<ActionResponse> {
}
export function updateHermes(): Promise<ActionResponse> {
return window.hermesDesktop.api<ActionResponse>({
return hermesApi<ActionResponse>({
...profileScoped(),
path: '/api/hermes/update',
method: 'POST'
@@ -1958,7 +1965,7 @@ export function updateHermes(): Promise<ActionResponse> {
* authoritative source for the backend's behind-count + "what's changed",
* distinct from the Electron client clone's git state. */
export function checkHermesUpdate(force = false): Promise<BackendUpdateCheckResponse> {
return window.hermesDesktop.api<BackendUpdateCheckResponse>({
return hermesApi<BackendUpdateCheckResponse>({
...profileScoped(),
path: `/api/hermes/update/check${force ? '?force=true' : ''}`
})
@@ -1972,7 +1979,7 @@ export function getActionStatus(name: string, lines = 200, profile?: ProfileScop
}
export function transcribeAudio(dataUrl: string, mimeType?: string): Promise<AudioTranscriptionResponse> {
return window.hermesDesktop.api<AudioTranscriptionResponse>({
return hermesApi<AudioTranscriptionResponse>({
path: '/api/audio/transcribe',
method: 'POST',
...profileScoped(),
@@ -1988,7 +1995,7 @@ export function transcribeAudio(dataUrl: string, mimeType?: string): Promise<Aud
}
export function speakText(text: string): Promise<AudioSpeakResponse> {
return window.hermesDesktop.api<AudioSpeakResponse>({
return hermesApi<AudioSpeakResponse>({
...profileScoped(),
path: '/api/audio/speak',
method: 'POST',
@@ -2001,7 +2008,7 @@ export function speakText(text: string): Promise<AudioSpeakResponse> {
}
export function getElevenLabsVoices(profile?: null | string): Promise<ElevenLabsVoicesResponse> {
return window.hermesDesktop.api<ElevenLabsVoicesResponse>({
return hermesApi<ElevenLabsVoicesResponse>({
path: '/api/audio/elevenlabs/voices',
...profileScoped(profile)
})
@@ -2016,7 +2023,7 @@ export function getElevenLabsVoices(profile?: null | string): Promise<ElevenLabs
const HUB_REQUEST_TIMEOUT_MS = 45_000
export function getSkillHubSources(profile?: null | string): Promise<SkillHubSourcesResponse> {
return window.hermesDesktop.api<SkillHubSourcesResponse>({
return hermesApi<SkillHubSourcesResponse>({
...profileScoped(profile),
path: '/api/skills/hub/sources',
timeoutMs: HUB_REQUEST_TIMEOUT_MS
@@ -2031,7 +2038,7 @@ export function searchSkillsHub(
): Promise<SkillHubSearchResponse> {
const params = new URLSearchParams({ q: query, source, limit: String(limit) })
return window.hermesDesktop.api<SkillHubSearchResponse>({
return hermesApi<SkillHubSearchResponse>({
...profileScoped(profile),
path: `/api/skills/hub/search?${params.toString()}`,
timeoutMs: HUB_REQUEST_TIMEOUT_MS
@@ -2039,7 +2046,7 @@ export function searchSkillsHub(
}
export function previewSkillHub(identifier: string, profile?: null | string): Promise<SkillHubPreview> {
return window.hermesDesktop.api<SkillHubPreview>({
return hermesApi<SkillHubPreview>({
...profileScoped(profile),
path: `/api/skills/hub/preview?identifier=${encodeURIComponent(identifier)}`,
timeoutMs: HUB_REQUEST_TIMEOUT_MS
@@ -2047,7 +2054,7 @@ export function previewSkillHub(identifier: string, profile?: null | string): Pr
}
export function scanSkillHub(identifier: string, profile?: null | string): Promise<SkillHubScanResult> {
return window.hermesDesktop.api<SkillHubScanResult>({
return hermesApi<SkillHubScanResult>({
...profileScoped(profile),
path: `/api/skills/hub/scan?identifier=${encodeURIComponent(identifier)}`,
timeoutMs: HUB_REQUEST_TIMEOUT_MS
@@ -2088,7 +2095,7 @@ export function updateSkillsFromHub(profile?: ProfileScope): Promise<ActionRespo
// ---------------------------------------------------------------------------
export function listMcpServers(): Promise<{ servers: McpServerSummary[] }> {
return window.hermesDesktop.api<{ servers: McpServerSummary[] }>({
return hermesApi<{ servers: McpServerSummary[] }>({
...profileScoped(),
path: '/api/mcp/servers'
})
@@ -2104,7 +2111,7 @@ export function addMcpServer(body: {
env?: Record<string, string>
auth?: string
}): Promise<McpServerSummary> {
return window.hermesDesktop.api<McpServerSummary>({
return hermesApi<McpServerSummary>({
...profileScoped(),
path: '/api/mcp/servers',
method: 'POST',
@@ -2115,7 +2122,7 @@ export function addMcpServer(body: {
/** Remove one server from `mcp_servers` (the inline setup card's rollback
* when a directory install is cancelled after the config write). */
export function removeMcpServer(name: string): Promise<{ ok: boolean }> {
return window.hermesDesktop.api<{ ok: boolean }>({
return hermesApi<{ ok: boolean }>({
...profileScoped(),
path: `/api/mcp/servers/${encodeURIComponent(name)}`,
method: 'DELETE'
@@ -2123,7 +2130,7 @@ export function removeMcpServer(name: string): Promise<{ ok: boolean }> {
}
export function setMcpServerEnabled(name: string, enabled: boolean): Promise<{ ok: boolean }> {
return window.hermesDesktop.api<{ ok: boolean }>({
return hermesApi<{ ok: boolean }>({
...profileScoped(),
path: `/api/mcp/servers/${encodeURIComponent(name)}/enabled`,
method: 'PUT',
@@ -2142,7 +2149,7 @@ export function getMcpCatalog(profile?: ProfileScope): Promise<McpCatalogRespons
* (GitHub is deliberately not an MCP — the github/* skills are the
* integration). Backend caches for 5 minutes; `refresh` bypasses. */
export function getGhAuthStatus(refresh = false): Promise<{ available: boolean; authenticated: boolean }> {
return window.hermesDesktop.api<{ available: boolean; authenticated: boolean }>({
return hermesApi<{ available: boolean; authenticated: boolean }>({
...profileScoped(),
path: `/api/git/gh-auth${refresh ? '?refresh=true' : ''}`
})
@@ -2167,14 +2174,14 @@ export function installMcpCatalogEntry(
// ---------------------------------------------------------------------------
export function getMemoryStatus(): Promise<MemoryStatusResponse> {
return window.hermesDesktop.api<MemoryStatusResponse>({
return hermesApi<MemoryStatusResponse>({
...profileScoped(),
path: '/api/memory'
})
}
export function resetMemory(target: 'all' | 'memory' | 'user'): Promise<{ ok: boolean; deleted: string[] }> {
return window.hermesDesktop.api<{ ok: boolean; deleted: string[] }>({
return hermesApi<{ ok: boolean; deleted: string[] }>({
...profileScoped(),
path: '/api/memory/reset',
method: 'POST',
@@ -2183,14 +2190,14 @@ export function resetMemory(target: 'all' | 'memory' | 'user'): Promise<{ ok: bo
}
export function getCuratorStatus(): Promise<CuratorStatusResponse> {
return window.hermesDesktop.api<CuratorStatusResponse>({
return hermesApi<CuratorStatusResponse>({
...profileScoped(),
path: '/api/curator'
})
}
export function setCuratorPaused(paused: boolean): Promise<{ ok: boolean; paused: boolean }> {
return window.hermesDesktop.api<{ ok: boolean; paused: boolean }>({
return hermesApi<{ ok: boolean; paused: boolean }>({
...profileScoped(),
path: '/api/curator/paused',
method: 'PUT',
@@ -2199,7 +2206,7 @@ export function setCuratorPaused(paused: boolean): Promise<{ ok: boolean; paused
}
export function runCurator(): Promise<ActionResponse> {
return window.hermesDesktop.api<ActionResponse>({
return hermesApi<ActionResponse>({
...profileScoped(),
path: '/api/curator/run',
method: 'POST',
@@ -2215,15 +2222,15 @@ export function runCurator(): Promise<ActionResponse> {
// ---------------------------------------------------------------------------
export function runDoctor(): Promise<ActionResponse> {
return window.hermesDesktop.api<ActionResponse>({ path: '/api/ops/doctor', method: 'POST', body: {} })
return hermesApi<ActionResponse>({ path: '/api/ops/doctor', method: 'POST', body: {} })
}
export function runSecurityAudit(): Promise<ActionResponse> {
return window.hermesDesktop.api<ActionResponse>({ path: '/api/ops/security-audit', method: 'POST', body: {} })
return hermesApi<ActionResponse>({ path: '/api/ops/security-audit', method: 'POST', body: {} })
}
export function runBackup(): Promise<ActionResponse & { archive?: string }> {
return window.hermesDesktop.api<ActionResponse & { archive?: string }>({
return hermesApi<ActionResponse & { archive?: string }>({
path: '/api/ops/backup',
method: 'POST',
body: {}
@@ -2231,7 +2238,7 @@ export function runBackup(): Promise<ActionResponse & { archive?: string }> {
}
export function runDebugShare(): Promise<DebugShareResponse> {
return window.hermesDesktop.api<DebugShareResponse>({
return hermesApi<DebugShareResponse>({
path: '/api/ops/debug-share',
method: 'POST',
body: {},

View File

@@ -1,7 +1,7 @@
import { atom, batch, computed } from 'nanostores'
import type { HermesConnection } from '@/global'
import { getProfiles, setApiRequestProfile, STARTUP_REQUEST_TIMEOUT_MS } from '@/hermes'
import { getProfiles, hermesApi, setApiRequestProfile, STARTUP_REQUEST_TIMEOUT_MS } from '@/hermes'
import { invalidateProfileScopedQueries } from '@/lib/query-client'
import {
arraysEqual,
@@ -141,7 +141,7 @@ export async function refreshActiveProfile(): Promise<void> {
const epoch = profileListEpoch
try {
const res = await window.hermesDesktop.api<ActiveProfileResponse>({
const res = await hermesApi<ActiveProfileResponse>({
path: '/api/profiles/active',
timeoutMs: STARTUP_REQUEST_TIMEOUT_MS
})