Bridge SDK

BridgeClient

WebSocket client for real-time streaming events from ORO Claude Bridge.

BridgeClient

Socket.IO-based client for real-time events. Handles automatic reconnection, room management, and typed event emission.

Constructor

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

const client = new BridgeClient({
  url: 'http://localhost:3000',
  auth: {
    clientId: 'my-adapter',
    clientType: 'adapter',
    nickname: 'TG Adapter'
  },
  autoReconnect: true,            // default: true
  reconnectionDelayMs: 1000,      // default: 1000
  reconnectionDelayMaxMs: 10000,  // default: 10000
  transports: ['websocket'],      // default: ['websocket', 'polling']
})
OptionTypeDefaultDescription
urlstringrequiredBridge server URL
pathstring/__bridge_socket/Custom Socket.IO path
authBridgeClientAuth—Handshake payload (clientId, clientType, nickname, token)
autoConnectbooleantrueConnect immediately on construction
autoReconnectbooleantrueReconnect on disconnect
reconnectionDelayMsnumber1000Initial reconnection delay
reconnectionDelayMaxMsnumber10000Max reconnection delay
transportsstring[]['websocket', 'polling']Socket.IO transports
withCredentialsbooleanfalseSend cookies in the handshake to a bridge on another origin
loggerLogger—Structured logger

Lifecycle

client.connect()

// Wait for connection (with timeout)
await client.connected(10_000)

console.log(client.isConnected()) // true

// Change handshake identity (reconnects, rooms are kept); undefined removes a field
client.setAuth({ token: newToken })
client.getAuth()                  // current payload, token redacted

client.close()

Room Management

Each bridge session is a "room". You must join a room to receive its stream events.

client.join('claude_abc123')
client.leave('claude_abc123')

// Join and wait for the in-flight turn snapshot (null when idle; bridge ≥ 0.4.0-rc.7)
const snapshot = await client.joinAndSync('claude_abc123', { timeoutMs: 5_000 })

client.isJoined('claude_abc123')  // boolean
client.getJoinedRooms()           // string[]

Outgoing Events

// Send a message through the socket (alternative to REST)
client.emitSendMessage({
  conversationId: 'claude_abc123',
  content: 'Hello!',
  attachments: []
})

// Stop generation
client.emitStopGeneration('claude_abc123')

// Create a session over the socket — resolves with its id, the socket is already in its room
const conversationId = await client.createSession({ title: 'My Session', cwd: '/work/project' })

// Stop the session's process (history stays on disk; use BridgeApi.deleteSession to delete it)
client.emitDestroySession('claude_abc123')

// Ask for share-mode participants — answered with `share:users`
client.emitRequestShareUsers('claude_abc123')

// Register for shared session
client.emitRegisterShare({
  conversationId: 'claude_abc123',
  clientId: 'my-client',
  nickname: 'John'
})

Stream Events

All stream events include a conversationId field for routing.

Connection Events

client.on('connect', () => {
  console.log('Connected to bridge')
})

client.on('disconnect', (reason: string) => {
  console.log('Disconnected:', reason)
})

client.on('connect_error', (err: Error) => {
  console.error('Connection failed:', err.message)
})

Session List

Broadcasted when sessions change (created, deleted, status update).

client.on('sessions:list', (sessions: SessionSummary[]) => {
  // Full session list — reconcile your containers
})

Server Status

Periodic heartbeat across all sessions. Useful for "any activity?" dashboards and liveness probes.

client.on('server:status', (status) => {
  // status.anyProcessing: boolean
  // status.activeSessions: number
  // status.uptime: number (seconds)
})

Stream Lifecycle

// User message accepted by bridge
client.on('stream:user_message', (e) => {
  // e.id, e.content, e.senderId, e.senderNickname, e.conversationId
})

// Message buffered (multi-user, waiting to flush)
client.on('stream:buffered', (e) => {
  // e.id, e.content, e.senderId, e.timestamp, e.conversationId
})

// Buffer flushed into a composed prompt
client.on('stream:buffer_flushed', (e) => {
  // e.messages[], e.composedPrompt, e.conversationId
})

// Assistant turn begins
client.on('stream:message_start', (e) => {
  // e.id, e.conversationId, e.triggerMessageId
})

// Incremental text
client.on('stream:text_delta', (e) => {
  // e.text, e.conversationId
})

// Tool invocation
client.on('stream:tool_use', (e) => {
  // e.id, e.name, e.input, e.conversationId
})

// Tool result
client.on('stream:tool_result', (e) => {
  // e.tool_use_id, e.name, e.content, e.is_error, e.conversationId
})

// Turn complete (full response available)
client.on('stream:message_complete', (e) => {
  // e.id, e.model, e.content, e.contentBlocks, e.conversationId
})

// Final result with usage/cost
client.on('stream:result', (e) => {
  // e.subtype, e.cost_usd, e.usage, e.duration_ms, e.conversationId
})

// Generation stopped (user or system)
client.on('stream:stopped', (e) => {
  // e.message, e.partialContent, e.conversationId
})

// User message queued behind an in-flight turn
client.on('stream:queued', (e) => {
  // e.messageId, e.position (0-based), e.triggerMessageId, e.conversationId
})

// Mid-turn state snapshot for late joiners / reconnects
client.on('stream:sync', (e) => {
  // e.messageId, e.triggerMessageId, e.contentBlocks[], e.accumulatedText,
  // e.model, e.status ('processing'), e.conversationId
})

Session Status

client.on('session:status', (e) => {
  // e.processing: boolean, e.conversationId
})

client.on('session:error', (e) => {
  // e.error: string, e.conversationId, e.exitCode?
})

// Reply to client.createSession() — only to the socket that asked
client.on('session:created', (e) => {
  // e.conversationId
})

// A provisional `new_*` session got its real `claude_<uuid>` id.
// No conversationId — route by e.oldId; delivered to the OLD room.
client.on('session:id_changed', (e) => {
  // e.oldId, e.newId, e.claudeSessionId
})

client.on('session:auto_changed', (e) => {
  // e.conversationId, e.autoMode: boolean
})

Loop Mode Events

client.on('session:loop_started', (e) => {
  // e.conversationId, e.config: LoopConfig
})

client.on('session:loop_paused', (e) => {
  // e.conversationId, e.reason: string
})

client.on('session:loop_stopped', (e) => {
  // e.conversationId
})

client.on('session:loop_iteration', (e) => {
  // e.conversationId, e.iteration, e.totalCostUsd
})

Shared Session Events

client.on('share:users', (users: ConnectedUser[]) => { })
client.on('share:user_joined', (e) => { })
client.on('share:user_left', (e) => { })
client.on('share:registered', (e) => { })
client.on('share:nickname_taken', (e) => { })

Attachment Events

// Fallback for socket uploads made without an ack callback
client.on('attachment:uploaded', (e) => {
  // e.attachment: MessageAttachment
})
client.on('attachment:error', (e) => {
  // e.error, e.issues?
})

// Voice attachment transcript is ready (arrives after the upload response)
client.on('attachment:transcribed', (e) => {
  // e.conversationId, e.attachmentId, e.transcript: string | null
})

Bridge State Events

Global — broadcast to every socket regardless of joined rooms.

// Who is connected — on connect and on every change (same as BridgeApi.listClients)
client.on('clients:list', (clients: ConnectedClient[]) => { })

// Skills changed (apm, sync, delete) — re-read the list for that scope
client.on('skills:changed', (e) => {
  // e.scope: 'user' | 'project', e.cwd?
})

// Agent appearance or card changed — no payload, re-read BridgeApi.getIdentity()
client.on('identity:changed', () => { })

// The bridge routed an incoming channel message (full DispatchEntry)
client.on('inbox:dispatch', (entry: DispatchEntry) => { })

Credentials

Global and edge-triggered: fired once on the transition into "needs re-auth". Read BridgeApi.getCredentialStatus() at startup too.

client.on('credentials:reauth_required', (status: CredentialStatus) => { })

Validation Errors

The bridge rejected one of your socket payloads. On the wire the event is error; BridgeClient renames it so an unhandled error never crashes the process.

client.on('bridge:validation_error', (e) => {
  // e.error: string, e.issues: { path, code, message }[]
})

Raw Socket Access

For advanced use cases, access the underlying Socket.IO instance:

client.rawSocket.on('custom:event', handler)

Reconnection behavior

BridgeClient enables automatic reconnection via Socket.IO's built-in exponential backoff (reconnectionDelayMs → reconnectionDelayMaxMs). Joined rooms are tracked in memory and re-emitted as session:join automatically on every connect event — you don't need to re-join manually after a disconnect.

To tell "fresh connect" from "reconnect", track state yourself:

let firstConnect = true
client.on('connect', () => {
  if (!firstConnect) console.log('reconnected')
  firstConnect = false
})

Graceful shutdown

async function shutdown() {
  // Unsubscribe from rooms so the server stops pushing events.
  for (const room of client.getJoinedRooms()) client.leave(room)
  client.close()
}
process.on('SIGTERM', shutdown)