Guides

Quick Start

Build your first platform adapter in 15 minutes.

Quick Start

Build a minimal platform adapter that connects a console-based "platform" to ORO Claude Bridge.

Prerequisites

  • Node.js 18+
  • ORO Claude Bridge running at http://localhost:3000

1. Create Project

mkdir my-adapter && cd my-adapter
npm init -y
npm install @oro.ad/bridge-adapter-sdk

2. Implement PlatformAdapter

Create platform.ts:

import type {
  PlatformAdapter,
  PlatformHandlers,
  ContainerId,
  PlatformMessageId,
  FormattedContent,
  PlatformFile,
  DownloadedFile,
  SessionSummary,
  MessageAttachment,
} from '@oro.ad/bridge-adapter-sdk'

export class ConsolePlatform implements PlatformAdapter {
  readonly supportsNativeStreaming = false
  private handlers?: PlatformHandlers
  private nextId = 1

  async start(handlers: PlatformHandlers): Promise<void> {
    this.handlers = handlers

    // Read from stdin
    process.stdin.setEncoding('utf-8')
    process.stdin.on('data', async (data: string) => {
      const text = data.trim()
      if (!text) return

      // Format: <containerId> <message>
      const [containerId, ...rest] = text.split(' ')
      const content = rest.join(' ')

      await handlers.onMessage({
        updateId: String(this.nextId++),
        containerId: containerId!,
        platformMessageId: String(this.nextId++),
        text: content,
        files: [],
        sender: { id: 'console-user', displayName: 'Console' },
      })
    })
  }

  async stop(): Promise<void> {
    process.stdin.removeAllListeners()
  }

  async createContainer(session: SessionSummary): Promise<ContainerId> {
    const id = String(this.nextId++)
    console.log(`[container:${id}] Created: ${session.title || session.conversationId}`)
    return id
  }

  async renameContainer(id: ContainerId, title: string): Promise<void> {
    console.log(`[container:${id}] Renamed: ${title}`)
  }

  async closeContainer(id: ContainerId): Promise<void> {
    console.log(`[container:${id}] Closed`)
  }

  async deleteContainer(id: ContainerId): Promise<void> {
    console.log(`[container:${id}] Deleted`)
  }

  async postMessage(id: ContainerId, content: FormattedContent): Promise<PlatformMessageId[]> {
    const msgId = String(this.nextId++)
    console.log(`[container:${id}] ${content.text}`)
    return [msgId]
  }

  async editMessage(id: ContainerId, msgId: PlatformMessageId, content: FormattedContent): Promise<void> {
    // Overwrite last line for streaming effect
    process.stdout.write(`\r[container:${id}] ${content.text?.slice(0, 80)}...`)
  }

  async downloadPlatformFile(file: PlatformFile): Promise<DownloadedFile> {
    throw new Error('Console platform does not support files')
  }

  async postAttachment(id: ContainerId, attachment: MessageAttachment): Promise<PlatformMessageId> {
    const msgId = String(this.nextId++)
    console.log(`[container:${id}] πŸ“Ž ${attachment.filename}`)
    return msgId
  }
}

3. Implement Formatter

Create formatter.ts:

import type { Formatter, SemanticBlock, FormattedContent } from '@oro.ad/bridge-adapter-sdk'

export class ConsoleFormatter implements Formatter {
  render(blocks: SemanticBlock[]): FormattedContent[] {
    const lines: string[] = []

    for (const block of blocks) {
      switch (block.kind) {
        case 'text':
          lines.push(block.text)
          break
        case 'code':
          lines.push(`\`\`\`${block.lang || ''}\n${block.text}\n\`\`\``)
          break
        case 'tool_card':
          lines.push(`πŸ”§ ${block.name}: ${block.summary}`)
          if (block.result) {
            lines.push(`   β†’ ${block.result.content.slice(0, 200)}`)
          }
          break
        case 'result_card':
          if (block.costUsd != null) {
            lines.push(`πŸ’° Cost: $${block.costUsd.toFixed(4)}`)
          }
          break
        case 'error_card':
          lines.push(`❌ Error: ${block.error}`)
          break
        default:
          break
      }
    }

    return [{ kind: 'text', text: lines.join('\n'), parseMode: 'plain' }]
  }

  renderStreaming(text: string): FormattedContent {
    return { kind: 'text', text, parseMode: 'plain' }
  }
}

4. Wire Everything

Create index.ts:

import { createAdapterRuntime } from '@oro.ad/bridge-adapter-sdk'
import { ConsolePlatform } from './platform'
import { ConsoleFormatter } from './formatter'

const runtime = createAdapterRuntime({
  bridge: { url: 'http://localhost:3000' },
  platform: new ConsolePlatform(),
  formatter: new ConsoleFormatter(),
  dataDir: './data',
  skipReplay: true,
  logger: {
    debug: () => {},
    info: (msg, meta) => console.error(`[info] ${msg}`, meta ? JSON.stringify(meta) : ''),
    warn: (msg, meta) => console.error(`[warn] ${msg}`, meta ? JSON.stringify(meta) : ''),
    error: (msg, meta) => console.error(`[error] ${msg}`, meta ? JSON.stringify(meta) : ''),
  },
})

process.on('SIGINT', async () => {
  await runtime.stop()
  process.exit(0)
})

await runtime.start()
console.error('Adapter ready. Type: <containerId> <message>')

5. Run

npx tsx index.ts

You should see containers created for each bridge session. Type a container ID followed by a message to send it to Claude.

Next Steps