# Building Integrations
Source: https://dorkos.ai/docs/integrations/building-integrations

Create custom clients using the DorkOS Transport interface



# Building Integrations [#building-integrations]

DorkOS uses a hexagonal (ports & adapters) architecture centered on the **Transport** interface. Build custom clients by implementing this interface or by calling the REST/SSE API directly.

```
                 ┌───────────────────────┐
   Browser  ───▶ │                       │ ───▶  HttpTransport ───▶ Express (HTTP + SSE)
                 │   Transport interface │
   Obsidian ───▶ │                       │ ───▶  DirectTransport ─▶ in-process services
                 └───────────────────────┘
```

Both adapters implement the same interface, so every hook and component that consumes a Transport works unmodified against either one.

## The Transport Interface [#the-transport-interface]

The Transport interface (`packages/shared/src/transport.ts`) defines all client-server communication. It is large (100+ methods spanning sessions, tasks, relay, mesh, and marketplace) and still evolving alongside the product, so treat it as an internal contract rather than a versioned public API: method signatures can change between releases without a deprecation cycle. All request and response types are exported from `@dorkos/shared`.

Here is the session-and-tool slice, the part most custom clients touch first:

```typescript
interface Transport {
  // Sessions
  listSessions(cwd?: string): Promise<SessionListResponse>;
  getSession(id: string, cwd?: string): Promise<Session>;
  updateSession(id: string, opts: UpdateSessionRequest, cwd?: string): Promise<Session>;
  getMessages(sessionId: string, cwd?: string): Promise<{ messages: HistoryMessage[] }>;
  getSessionSnapshot(sessionId: string, cwd?: string): Promise<SessionSnapshot>;
  subscribeSession(sessionId, sinceCursor?, cwd?, signal?): AsyncIterable<SessionEvent>;
  subscribeSessionList(): AsyncIterable<SessionListEvent>;
  postMessage(sessionId, content, cwd?, options?): Promise<{ sessionId: string }>;
  // options: { clientMessageId?: string; context?: ClientContext; runtime?: string }
  // A brand-new session is created implicitly on its first postMessage() call
  // (pass a client-generated UUID as sessionId): there is no separate createSession.

  // Tool interaction
  approveTool(
    sessionId: string,
    toolCallId: string,
    alwaysAllow?: boolean
  ): Promise<{ ok: boolean }>;
  denyTool(sessionId: string, toolCallId: string): Promise<{ ok: boolean }>;
  submitAnswers(sessionId, toolCallId, answers): Promise<{ ok: boolean }>;
  stopTask(sessionId: string, taskId: string): Promise<{ success: boolean; taskId: string }>;
  interruptSession(sessionId: string): Promise<{ ok: boolean }>;
  getTasks(sessionId: string, cwd?: string): Promise<{ tasks: TaskItem[] }>;

  // Filesystem & git
  browseDirectory(dirPath?, showHidden?): Promise<BrowseDirectoryResponse>;
  getDefaultCwd(): Promise<{ path: string }>;
  listFiles(cwd: string): Promise<FileListResponse>;
  getGitStatus(cwd?: string): Promise<GitStatusResponse | GitStatusError>;

  // Commands & config
  getCommands(refresh?: boolean, cwd?: string, opts?): Promise<CommandRegistry>;
  health(): Promise<HealthResponse>;
  getConfig(): Promise<ServerConfig>;
  updateConfig(patch: Record<string, unknown>): Promise<void>;

  // Runtime
  getModels(opts?): Promise<ModelOption[]>;
  getCapabilities(): Promise<{
    capabilities: Record<string, RuntimeCapabilities>;
    defaultRuntime: string;
  }>;

  // File uploads
  uploadFiles(files: UploadFile[], cwd: string, onProgress?): Promise<UploadResult[]>;

  // Tunnel
  startTunnel(): Promise<{ url: string }>;
  stopTunnel(): Promise<void>;

  // Directory operations
  createDirectory(parentPath: string, folderName: string): Promise<{ path: string }>;

  // Templates
  getTemplates(): Promise<TemplateEntry[]>;

  // Default agent
  setDefaultAgent(agentName: string): Promise<void>;

  // Admin
  resetAllData(confirm: string): Promise<{ message: string }>;
  restartServer(): Promise<{ message: string }>;
}
```

The full interface also covers Tasks (`listTasks`, `createTask`, `triggerTask`, `listTaskRuns`, and friends), Relay, Mesh, and agent identity operations. See `packages/shared/src/transport.ts` for the complete definition.

## Built-in Implementations [#built-in-implementations]

<Tabs items="[&#x22;HttpTransport&#x22;, &#x22;DirectTransport&#x22;]">
  <Tab value="HttpTransport">
    **HttpTransport** is used by the standalone web client and communicates with the Express server over HTTP and SSE:

    * Standard `fetch()` for CRUD operations
    * `postMessage()` triggers a turn (`202` with the canonical session id)
    * `subscribeSession()` consumes the durable SSE stream `GET /api/sessions/:id/events`; `subscribeSessionList()` consumes `GET /api/events`
    * Includes `X-Client-Id` header for session write coordination
  </Tab>

  <Tab value="DirectTransport">
    **DirectTransport** is used by the Obsidian plugin and calls service instances directly in the same process:

    * No HTTP, no port binding, no network serialization
    * Iterates the runtime's snapshot and event generators in-process
    * Lower latency, ideal for embedded contexts
  </Tab>
</Tabs>

## Building a Custom Client [#building-a-custom-client]

<Tabs items="[&#x22;REST/SSE API&#x22;, &#x22;Custom Transport&#x22;, &#x22;React Integration&#x22;]">
  <Tab value="REST/SSE API">
    Use the REST API directly for any language or platform:

    ```bash
    # List sessions
    GET /api/sessions

    # Create a session
    POST /api/sessions
    Content-Type: application/json
    { "cwd": "/path/to/project" }

    # Subscribe to a session's durable event stream (SSE: snapshot → replay → live)
    GET /api/sessions/:id/events

    # Trigger a turn (202 with the canonical session id; events arrive on /events)
    POST /api/sessions/:id/messages
    Content-Type: application/json
    { "content": "Hello", "cwd": "/path/to/project" }

    # Approve a tool call
    POST /api/sessions/:id/approve
    Content-Type: application/json
    { "toolCallId": "tc_123" }
    ```

    <Callout type="info">
      The full REST API is documented interactively at `/api/docs` (Scalar UI) when the server is
      running, or see the [API Reference](/docs/api).
    </Callout>

    **Authentication.** By default, local login is off (`auth.enabled: false`), so a local DorkOS instance accepts these requests with no credentials, same as the web cockpit talking to its own server. If you turn local login on, every `/api/*` and `/mcp` request needs either a Better Auth session cookie or a per-user API key sent as `Authorization: Bearer <key>`. The separate external MCP server at `/mcp` can also be locked down independently with a static `MCP_API_KEY`, which is the option to reach for in headless or CI deployments.
  </Tab>

  <Tab value="Custom Transport">
    Implement the Transport interface for deeper integration. Most methods are thin wrappers around the same REST endpoints shown above, so a first pass can just forward to `fetch()`:

    ```typescript
    import type { Transport } from '@dorkos/shared/transport';

    class MyCustomTransport implements Transport {
      constructor(private baseUrl: string) {}

      async listSessions(cwd?: string) {
        const url = new URL(`${this.baseUrl}/api/sessions`);
        if (cwd) url.searchParams.set('cwd', cwd);
        const res = await fetch(url);
        return res.json();
      }

      async postMessage(sessionId: string, content: string, cwd?: string, options?) {
        const res = await fetch(`${this.baseUrl}/api/sessions/${sessionId}/messages`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ content, cwd, ...options }),
        });
        return res.json(); // { sessionId }, events arrive on subscribeSession
      }

      async *subscribeSession(
        sessionId: string,
        sinceCursor?: number,
        cwd?: string,
        signal?: AbortSignal
      ) {
        // Open GET /api/sessions/:id/events as an EventSource and yield each
        // parsed SessionEvent here. See the SSE Protocol page for the wire format.
      }

      // ... implement the remaining methods the same way
    }
    ```

    <Callout type="warn">
      All methods in the Transport interface must be implemented. TypeScript reports a compile error for every missing one, and it will not let up until the interface is complete.
    </Callout>
  </Tab>

  <Tab value="React Integration">
    Inject your Transport via context:

    ```typescript
    import { TransportProvider } from '@dorkos/shared';

    const transport = new MyCustomTransport();

    function App() {
      return (
        <TransportProvider transport={transport}>
          <YourApp />
        </TransportProvider>
      );
    }
    ```

    All DorkOS hooks (`useSessions`, `useChatSession`, etc.) consume the Transport from context, so your implementation powers the entire UI automatically.
  </Tab>
</Tabs>

## SessionEvent Types [#sessionevent-types]

Events delivered on the session event stream. Every event carries a per-session monotonic `seq` for gap-free resumption:

<TypeTable
  type="{
  turn_start: {
    type: 'SessionEvent',
    description: 'Assistant turn begins (carries the triggering user message)',
  },
  text_delta: {
    type: 'SessionEvent',
    description: 'Incremental text chunk for the assistant message',
  },
  thinking_delta: { type: 'SessionEvent', description: 'Incremental extended thinking content' },
  tool_call: {
    type: 'SessionEvent',
    description: 'Tool invocation begins or changes status (includes toolCallId, toolName)',
  },
  tool_result: { type: 'SessionEvent', description: 'Tool execution result' },
  tool_progress: { type: 'SessionEvent', description: 'Live output from a running tool' },
  approval_required: {
    type: 'SessionEvent',
    description: 'Tool needs user approval before executing (with resumable countdown)',
  },
  question_prompt: {
    type: 'SessionEvent',
    description: 'Claude is asking the user a question with options',
  },
  elicitation_prompt: {
    type: 'SessionEvent',
    description: 'Agent requests structured form input via the MCP elicitation protocol',
  },
  interaction_resolved: {
    type: 'SessionEvent',
    description: 'A pending interaction was approved, denied, answered, or cancelled',
  },
  status_change: {
    type: 'SessionEvent',
    description: 'Partial session status update (tokens, cost, model, lifecycle)',
  },
  todo_update: { type: 'SessionEvent', description: 'Todo list updated' },
  subagent_update: { type: 'SessionEvent', description: 'Background subagent lifecycle update' },
  hook_update: { type: 'SessionEvent', description: 'Hook lifecycle update' },
  memory_recall: { type: 'SessionEvent', description: 'Memories surfaced into the turn' },
  turn_end: {
    type: 'SessionEvent',
    description: 'Assistant turn finished (with terminal reason)',
  },
}"
/>

## Next Steps [#next-steps]

<Cards>
  <Card title="SSE Protocol" href="/docs/integrations/sse-protocol">
    Wire format details, event schemas, and connection lifecycle for the streaming protocol.
  </Card>

  <Card title="API Reference" href="/docs/api">
    Full interactive API documentation with request and response schemas.
  </Card>

  <Card title="Architecture" href="/docs/contributing/architecture">
    Deep dive into the hexagonal architecture and Transport interface design.
  </Card>
</Cards>
