Newer
Older
navi-1 / webclient / src / api / index.js
const BASE = import.meta.env.DEV ? '/api' : ''

// Global safety net: log unhandled rejections so the UI doesn't silently break.
window.addEventListener('unhandledrejection', (ev) => {
  console.error('[API] Unhandled rejection:', ev.reason)
})

async function request(method, path, body) {
  const opts = {
    method,
    headers: {},
    credentials: 'include',
  }
  if (body !== undefined) {
    opts.headers['Content-Type'] = 'application/json'
    opts.body = JSON.stringify(body)
  }
  const res = await fetch(`${BASE}${path}`, opts)

  // 401 must surface immediately: invalidate local auth so the login overlay appears.
  if (res.status === 401) {
    try {
      const { useAuthStore } = await import('@/stores/auth.js')
      const auth = useAuthStore()
      auth.user = null
    } catch {
      // Pinia not ready — ignore, the login overlay won't appear but the error still throws
    }
    const text = await res.text().catch(() => '')
    throw new Error(`${method} ${path} → 401: ${text}`)
  }

  if (!res.ok) {
    const text = await res.text().catch(() => '')
    throw new Error(`${method} ${path} → ${res.status}: ${text}`)
  }
  if (res.status === 204) return null
  return res.json()
}

// ─── Auth ─────────────────────────────────────────────────────────────────
export function getMe() {
  return request('GET', '/auth/me')
}

export function logout() {
  return request('POST', '/auth/logout')
}

// ─── Profiles ──────────────────────────────────────────────────────────────
export function getProfiles() {
  return request('GET', '/agents/profiles')
}

// ─── Sessions ──────────────────────────────────────────────────────────────
export function getSessions({ limit = 30, offset = 0, profileId = null, search = null, special = null } = {}) {
  const params = new URLSearchParams({
    limit: String(limit),
    offset: String(offset),
  })
  if (profileId) params.set('profile_id', profileId)
  if (search) params.set('search', search)
  // null — no filtering (server regular-only default); true — service sessions only
  if (special != null) params.set('special', String(special))
  return request('GET', `/sessions?${params.toString()}`)
}

export function getSession(id) {
  return request('GET', `/sessions/${id}`)
}

export function getSessionMeta(id) {
  return request('GET', `/sessions/${id}/meta`)
}

export function getSessionHistory(id, beforeSeq = null, limit = 200) {
  const params = new URLSearchParams({ limit: String(limit) })
  if (beforeSeq != null) params.set('before_seq', String(beforeSeq))
  return request('GET', `/sessions/${id}/messages/history?${params.toString()}`)
}

export function createSession(profileId) {
  return request('POST', '/sessions', { profile_id: profileId })
}

export function deleteSession(id) {
  return request('DELETE', `/sessions/${id}`)
}

export function pinSession(id, pinned) {
  return request('PATCH', `/sessions/${id}/pin`, { pinned })
}

// Empty name → the server regenerates the title from session content
export function renameSession(id, name) {
  return request('PATCH', `/sessions/${id}/name`, { name })
}

export function stopSession(id) {
  return request('POST', `/sessions/${id}/stop`)
}

export function generateSessionName(id) {
  return request('POST', `/sessions/${id}/generate-name`)
}

export function getSessionContent(id) {
  return request('GET', `/sessions/${id}/content`)
}

// ─── Background tasks ──────────────────────────────────────────────────────
export function getSessionTasks(sessionId) {
  return request('GET', `/sessions/${sessionId}/tasks`)
}

export function getArchivedMessages(id, beforeSeq, limit = 50) {
  const params = new URLSearchParams({ limit: String(limit) })
  if (beforeSeq != null) params.set('before_seq', String(beforeSeq))
  return request('GET', `/sessions/${id}/messages/archive?${params.toString()}`)
}

// ─── Eval feedback ─────────────────────────────────────────────────────────
export function getFeedback(sessionId) {
  return request('GET', `/eval/feedback/${sessionId}`)
}

export function setFeedback(sessionId, messageIndex, rating) {
  return request('POST', '/eval/feedback', {
    session_id: sessionId,
    message_index: messageIndex,
    rating
  })
}

// ─── Files ─────────────────────────────────────────────────────────────────
export function listSessionFiles(sessionId) {
  return request('GET', `/sessions/${sessionId}/files`)
}

export async function uploadFile(sessionId, file) {
  const form = new FormData()
  form.append('file', file)
  const res = await fetch(`${BASE}/sessions/${sessionId}/files`, {
    method: 'POST',
    credentials: 'include',
    body: form
  })
  if (!res.ok) {
    const text = await res.text().catch(() => '')
    throw new Error(`Upload failed: ${res.status}: ${text}`)
  }
  return res.json()
}

// ─── Scheduled recalls ───────────────────────────────────────────────────────
export function getSessionRecall(sessionId) {
  return request('GET', `/sessions/${sessionId}/recall`)
}

export function cancelSessionRecall(sessionId) {
  return request('DELETE', `/sessions/${sessionId}/recall`)
}

export function skipSessionRecall(sessionId) {
  return request('POST', `/sessions/${sessionId}/recall/skip`)
}

// ─── API Tokens ────────────────────────────────────────────────────────────
export function getApiTokens() {
  return request('GET', '/api-tokens')
}

export function createApiToken(name) {
  return request('POST', '/api-tokens', { name })
}

export function revokeApiToken(id) {
  return request('DELETE', `/api-tokens/${id}`)
}

// ─── Synapse delivery targets ──────────────────────────────────────────────
// Per-user secrets of Synapse s2s targets that deliver events to this navi.
// The secret is sent here once (shown once in the UI, never returned by the API).
export function getSynapseTargets() {
  return request('GET', '/synapse-targets')
}

export function createSynapseTarget(tokenRef, secret) {
  return request('POST', '/synapse-targets', { token_ref: tokenRef, secret })
}

export function revokeSynapseTarget(id) {
  return request('DELETE', `/synapse-targets/${id}`)
}

// ─── Synapse reactions ─────────────────────────────────────────────────────
// Per-user settings of the agent-reaction layer: whether Synapse events get
// a background agent run, where notifications go, and the reaction
// instructions (user- and Navi-editable, full edit history).
export function getSynapseReactions() {
  return request('GET', '/synapse-settings')
}

export function updateSynapseReactions(payload) {
  return request('PUT', '/synapse-settings', payload)
}

export function getSynapseReactionVersions(limit = 20) {
  return request('GET', `/synapse-settings/versions?limit=${limit}`)
}

// ─── MCP user keys (BYOK) ──────────────────────────────────────────────────
// Per-user credentials for MCP servers that declare a user_key slot.
// The key is sent when saving; it is never returned by the API.
export function getMcpKeys() {
  return request('GET', '/mcp-keys')
}

export function saveMcpKey(serverName, key) {
  return request('PUT', `/mcp-keys/${serverName}`, { key })
}

export function resetMcpKey(serverName) {
  return request('DELETE', `/mcp-keys/${serverName}`)
}


// ─── Admin ─────────────────────────────────────────────────────────────────
// Hot-reload of the server's tools, context providers and MCP servers.
// Admin only (403 for anyone else) — it rewrites the toolset every profile
// resolves against, mid-flight sessions included.
export function reloadTools() {
  return request('POST', '/admin/tools/reload')
}

// ─── Web push (PWA) ───────────────────────────────────────────────────────
export function getVapidKey() {
  return request('GET', '/push/vapid-key')
}

export function subscribePush(payload) {
  return request('POST', '/push/subscribe', payload)
}

export function unsubscribePush(endpoint) {
  return request('DELETE', '/push/subscribe', { endpoint })
}