DorkOS
Integrations

Building Integrations

Create custom clients using the DorkOS Transport interface

Building Integrations

This whole section is for builders: people writing custom clients, extensions, marketplace adapters, or MCP servers on top of DorkOS. If you just want to connect a chat app or a service to your agents, like Telegram or Gmail, you want the Connections page instead, no code required.

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<SessionUpdateResponse>;
  // SessionUpdateResponse is the session, plus `permissionModePendingUntilNextTurn`
  // when a STRICTER permission mode was saved but could not reach the reply that
  // is already running — that reply keeps the mode it started under. It also
  // carries `runtimeUnbound` while no runtime is recorded for the session: the
  // `runtime` it reports is a guess then, not an owner. Only an interactive
  // session's first turn records one, so this is not just a brand-new 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, reason?: string): Promise<{ ok: boolean }>;
  // `reason` is optional free text (max 1000 chars) handed to the agent with the
  // refusal, so it can adjust instead of retrying the same call.
  submitAnswers(sessionId, toolCallId, answers): Promise<{ ok: boolean }>;
  // Both stop verbs answer an InterruptReceipt: `{ outcome, reason?, runtime }`,
  // where `outcome` is one of `acked` (the agent confirmed the stop), `closed`
  // (DorkOS ended it after the agent didn't answer), `not-running` (there was
  // nothing to stop), `unconfirmed` (the request went out and the runtime cannot
  // say whether it landed) or `failed` (the stop could not be delivered).
  // Say "stopped" only about `acked` and `closed`.
  stopTask(
    sessionId: string,
    taskId: string
  ): Promise<{ receipt: InterruptReceipt; taskId: string }>;
  interruptSession(
    sessionId: string
  ): Promise<{ receipt: InterruptReceipt; cancelledQueued: QueuedMessage[] }>;
  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

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 app 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.

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