DorkOS
Integrations

Building Integrations

Create custom clients using the DorkOS Transport interface

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 (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:

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

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

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

Building a Custom Client

Use the REST API directly for any language or platform:

# 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" }

The full REST API is documented interactively at /api/docs (Scalar UI) when the server is running, or see the API Reference.

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.

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():

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
}

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.

Inject your Transport via context:

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.

SessionEvent Types

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

Prop

Type

Next Steps