Bridge SDK

BridgeApi

REST API client for ORO Claude Bridge — sessions, messages, attachments, voice, identity, skills, and bridge state.

BridgeApi

Typed HTTP client for the Bridge REST API. All methods return promises and throw BridgeApiError on non-2xx responses.

Constructor

import { BridgeApi } from '@oro.ad/bridge-sdk'

const api = new BridgeApi({
  baseUrl: 'http://localhost:3000',
  logger: myLogger,           // optional Logger
  timeoutMs: 30_000,          // optional default client-side timeout
  defaultHeaders: {           // optional
    'X-Client-Id': 'my-app'
  }
})
OptionTypeDefaultDescription
baseUrlstringrequiredBridge server URL (trailing slash stripped)
fetchFetchglobalThis.fetchCustom fetch implementation
loggerLoggernoopLoggerOptional structured logger
defaultHeadersRecord<string, string>{}Headers sent with every request
timeoutMsnumber—Default per-request client-side timeout; raises BridgeTimeoutError via AbortController
Every method throws one of BridgeApiError (non-2xx), BridgeConnectError (transport), BridgeTimeoutError (timeout exceeded), or BridgeValidationError (bad local input, e.g. unreadable attachment file). See errors.

Session Management

listSessions

const sessions: SessionSummary[] = await api.listSessions()

Returns all active sessions with their status, title, model, message count, and loop state.

getSession

const session = await api.getSession('claude_abc123')

createSession

const { conversationId } = await api.createSession({
  title: 'My Session'
})

adoptSession

Attach an existing Claude Code session to the bridge.

const result = await api.adoptSession({
  claudeSessionId: 'sess_xyz',
  title: 'Adopted Session'
})

updateSession / updateSessionTitle

Updates mutable session fields — title, model, contextSummary, loop (see UpdateSessionBody). Fields absent from the body are left untouched; null for model / contextSummary resets to the bridge default / unpins the summary.

const { loop } = await api.updateSession(conversationId, { title: 'New Title', model: null })
// or shorthand:
await api.updateSessionTitle(conversationId, 'New Title')

deleteSession

await api.deleteSession(conversationId)

Session Control

compactSession

Triggers context compaction (summarization) for a session.

await api.compactSession(conversationId)

clearSession

Clears all messages from a session.

await api.clearSession(conversationId)

setLoopMode

Enable or disable autonomous loop mode.

const { loop } = await api.setLoopMode(conversationId, {
  enabled: true,
  config: {
    trigger: 'idle_timeout',
    idleTimeoutSec: 300,
    resumePrompt: 'Continue working on the task.',
    maxCostUsdPerHour: 5.0
  }
})

getAutoMode / setAutoMode

Read or toggle auto-mode (the bridge compacts the conversation itself past a token threshold). The bridge broadcasts session:auto_changed to the session room.

const { autoMode } = await api.getAutoMode(conversationId)
await api.setAutoMode(conversationId, true)   // { ok, conversationId, autoMode }

reviveSession

Restores a conversation's transcript in ~/.claude from the bridge's history mirror so --resume can continue it. Idempotent; tool results are not restored.

const { revived, lines, path } = await api.reviveSession(conversationId)

setSessionMcpServers

Declares HTTP MCP servers for one conversation — added to its --mcp-config on every run. Replaces the previous declaration; {} revokes it. At most eight servers; header values are never echoed back.

const { servers } = await api.setSessionMcpServers(conversationId, {
  docs: { type: 'http', url: 'https://mcp.example.com', headers: { authorization: `Bearer ${token}` } }
})
// servers: ['docs']

searchSessions

Full-text search over message content (the bridge's history mirror). Titles and projects are not searched. Queries shorter than two characters return []. At most one hit per conversation, newest first.

const hits: SessionSearchHit[] = await api.searchSessions('deploy', {
  limit: 20,                 // 1–200, default 50
  signal: controller.signal  // cancel a stale query
})
// [{ bridgeSessionId, snippet, role, timestamp }]

Messages

sendMessage

const { messageId } = await api.sendMessage(conversationId, {
  content: 'Hello Claude!',
  attachments: [],
  senderId: 'user_123',
  senderNickname: '@john'
})

getMessages

const { messages, total } = await api.getMessages(conversationId, {
  limit: 50,
  before: 'msg_xyz'   // cursor pagination
})

getMessage

const message: BridgeMessage = await api.getMessage(conversationId, messageId)

getMessageResult

Waits for the assistant to complete processing a message.

const result: MessageResult = await api.getMessageResult(
  conversationId,
  messageId,
  { timeoutMs: 120_000 }
)

console.log(result.content)     // assistant response
console.log(result.costUsd)     // cost in USD
console.log(result.usage)       // token usage breakdown

Attachments

uploadAttachment

Upload from file path or raw bytes.

// From file
const attachment = await api.uploadAttachment(conversationId, {
  filepath: '/path/to/image.png',
  filename: 'image.png',
  mimeType: 'image/png'
})

// From buffer
const attachment = await api.uploadAttachment(conversationId, {
  data: buffer,
  filename: 'doc.pdf',
  mimeType: 'application/pdf'
})

downloadAttachment

const bytes: Uint8Array = await api.downloadAttachment(attachmentId)

deleteAttachment

await api.deleteAttachment(attachmentId)

getAttachmentPreview

Server-highlighted preview of a text attachment. Non-text attachments answer 415.

const preview: AttachmentPreview = await api.getAttachmentPreview(attachmentId)
// { id, filename, mimeType, size, lang, highlighted, reason?, html }  — html is an escaped <pre>…</pre>

Voice

transcribePreview

Transcribes a chunk of audio without creating an attachment — live text while the user is still speaking.

const { text } = await api.transcribePreview(conversationId, {
  data: chunk,
  filename: 'chunk.webm',
  mimeType: 'audio/webm'
})

synthesize

Text-to-speech. Returns null when the bridge has no synthesis (204) or refuses — fall back to browser speech. Network errors and timeouts still throw.

const audio: Blob | null = await api.synthesize({ text: 'Hello', voice: 'ru_RU-dmitri-medium', lengthScale: 1.1 })

readVoiceProfile / writeVoiceProfile

Per-user voice settings stored in the user's profile. readVoiceProfile returns null when there is none or the profile is unavailable; writeVoiceProfile returns the bridge's ok. The bridge replaces the whole object — always send the full profile.

const profile = await api.readVoiceProfile()   // VoiceProfile | null
const saved: boolean = await api.writeVoiceProfile({ ...profile, voiceName: 'ru_RU-dmitri-medium' })

routeVoice

Picks which live session in cwd should receive a spoken phrase. Only the addressee — send the words yourself with sendMessage. null when there are no live sessions there (204).

const route: VoiceRoute | null = await api.routeVoice({ cwd: '/work/project', text: 'run the tests' })
// { sessionId, fromCache }

Identity

getIdentity

Identity of the bridge itself (the agent), not of the caller. An unconfigured identity comes back as a synthetic local (isLocal: true) without the agent section.

const me: BridgeIdentity = await api.getIdentity()
// { sub, username, email, groups, isAgent, isLocal, description?, capabilities?, appearance?, agent?, ... }

getAppearance / setAppearance

Read or write the agent's appearance (oro id appearance). null clears it. On write the bridge re-announces the card to the hub and broadcasts identity:changed. 400 — malformed appearance, 503 — no oro client or identity on the machine.

const { appearance } = await api.getAppearance()   // AgentAppearance | null
await api.setAppearance({ v: 1, head: 'visor', paint: 'teal', metal: 'steel', lens: 'navy', glow: 'cyan' })  // fields: @oro.ad/agent-look
await api.setAppearance(null)

setCard

Writes the agent card. Only the fields passed are changed; description: null clears it. At least one field is required.

const card: AgentCard = await api.setCard({ description: 'Reviews MRs', capabilities: ['review'] })
// { ok, description, capabilities }

Skills

listSkills

Agent skills. Without cwd — user scope (~/.claude/skills); with cwd — the project scope (<cwd>/.claude/skills). These are separate lists, not a filter.

const { scope, skills, syncedAt } = await api.listSkills()
const project = await api.listSkills({ cwd: '/work/project' })

deleteSkill

Removes a single skill from the user scope. 404 if it does not exist.

await api.deleteSkill('my-skill')

installSkillPackage / uninstallSkillPackage

Installs (apm install … --target claude) or removes an apm package. Target is exactly one of global: true, workDir, or conversationId (see ApmTarget). Can take minutes — give it a generous timeout. An apm failure is a 502 with the apm output in the error body.

const res: ApmResult = await api.installSkillPackage(
  { packageRef: 'org/skills-pack', global: true },
  { timeoutMs: 300_000 }
)
await api.uninstallSkillPackage({ packageRef: 'org/skills-pack', global: true })

updateSkillPackages

Updates the target's apm packages (apm update --yes).

await api.updateSkillPackages({ conversationId }, { timeoutMs: 300_000 })

syncSkills

Writes skills as files into ~/.claude/skills — additive, other skills stay. Returns the user scope list after the write.

const { skills, syncedAt } = await api.syncSkills({
  skills: [{ name: 'greet', files: { 'SKILL.md': '---\nname: greet\n---\n…' } }]
})

Bridge State

listClients

Sockets connected to the bridge right now. The same list is pushed as the clients:list event.

const { clients } = await api.listClients()
// [{ socketId, app, clientType, user, connectedAt, conversations }]

Bridge ↔ hub link state from the open /reactive/link route (outside the /api/** auth gate), so it works for anonymous viewers.

const link: HubLinkStatus = await api.getHubLink()
// { state, hubUrl, lastConnectedAt, reconnects }

getReactiveStatus

The bridge's reactive loop: hub link, agent identity, claude preflight, counters, and recent inbox dispatches (full entries for the agent owner only).

const status: ReactiveStatus = await api.getReactiveStatus()
// { link, identity, spawnHealthy, claudeCommand, counters, recentDispatches }

deliverInbox

Delivers a hub channel message into the bridge — the same routing as live delivery (dedup by seq, dispatcher picks the session). Only the bridge owner or a trusted service may call it; answers 202.

const res: InboxDeliveryResult = await api.deliverInbox({
  channelId: 'ch_1',
  fromAgentId: 'agent_2',
  messageId: 'm_42',
  seq: 42,
  content: 'Please review !12'
})
// { status: 'delivered', routed, deduped, dispatcherUsed }

Credentials

getCredentialStatus

Health of the bridge's Claude credentials (process state, not per session). Read it explicitly on startup — credentials:reauth_required fires only once, on the transition.

const s: CredentialStatus = await api.getCredentialStatus()
// { loaded, expiresAt, expiresInMs, reauthRequired, lastCheckedAt, lastError }

reauthCredentials

Reinstalls the organization's credential bundle (oro auth-restore). Failure is ok: false in a 200 response, not an HTTP error; reauthWizardNeeded means the owner has to sign in again.

const r: CredentialReauthResult = await api.reauthCredentials({ timeoutMs: 60_000 })
if (!r.ok) console.warn(r.error)

Health & Info

health

Liveness probe and the only route outside the auth gate. version / workDir are absent on bridges older than 0.4.0-rc.5.

await api.health()   // { ok: true, version?, workDir? }

info

const info = await api.info()
// { version, uptime, claudeVersion, workDir, activeSessions }

Timeouts & cancellation

Set a default timeout on construction (applies to every request) or per-call (overrides the default). Exceeded timeouts throw BridgeTimeoutError:

const api = new BridgeApi({ baseUrl, timeoutMs: 15_000 })

// Per-call override — useful for attachment uploads or long polls:
await api.uploadAttachment(id, file, { timeoutMs: 60_000 })
await api.downloadAttachment(id, { timeoutMs: 60_000 })

getMessageResult has its own timeoutMs that's passed to the server as the long-poll deadline. Set BridgeApiOptions.timeoutMs at least as high to avoid the client timing out before the server responds.

Error handling

import {
  BridgeApiError,
  BridgeConnectError,
  BridgeTimeoutError,
  BridgeValidationError,
} from '@oro.ad/bridge-sdk'

try {
  await api.sendMessage(id, body)
}
catch (err) {
  if (err instanceof BridgeTimeoutError) { /* retry / surface */ }
  else if (err instanceof BridgeApiError && err.status === 404) { /* gone */ }
  else if (err instanceof BridgeConnectError) { /* network down */ }
  else if (err instanceof BridgeValidationError) { /* bad input */ }
  else throw err
}