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 ───▶ Hono (HTTP + SSE)
│ Transport interface │
Desktop ───▶ │ │
└───────────────────────┘The browser, phone web app, and desktop renderer use HttpTransport. Tests can inject a mock through the same interface.
The Transport Interface
The Transport interface (packages/shared/src/transport.ts) defines all client-server communication. It is large (100+ methods spanning chats, 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 chat-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: { create?: boolean; 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 and `create: true` in options):
// 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 HTTP Transport
HttpTransport is used by the browser, phone web app, and desktop renderer. It communicates with the Hono server over HTTP and WebSocket:
- Standard
fetch()for CRUD operations postMessage()triggers a turn (202with the canonical chat ID)subscribeSession()consumes the durable WebSocket streamGET /api/sessions/:id/events;subscribeSessionList()consumesGET /api/events. The same routes also support SSE for external clients.- Includes
X-Client-Idheader so only one client writes to a chat at a time
Building a Custom Client
Use the REST API directly for any language or platform:
# List sessions
GET /api/sessions
# Subscribe to a session's durable event stream (SSE: snapshot → replay → live)
GET /api/sessions/:id/events
# Start a session with its first message: pick a new UUID, send create: true
# (202 with the session id; events arrive on /events)
POST /api/sessions/:id/messages
Content-Type: application/json
{ "content": "Hello", "cwd": "/path/to/project", "create": true }
# Send another message to the same session (an unknown id is a 404)
POST /api/sessions/:id/messages
Content-Type: application/json
{ "content": "And then?" }
# 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 sign-in cookie or a per-user API key sent as Authorization: Bearer <key>. The separate external MCP server at /mcp is the exception to the first sentence: even with login off it wants this instance's local MCP token for anything that changes state, and only the handshake, discovery calls, and read-only tools answer without one. In headless or CI deployments you can set a static MCP_API_KEY and hand that to every client instead.
SessionEvent Types
Events delivered on a chat's event stream. Every event carries a per-chat monotonic seq for gap-free resumption:
Prop
Type