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 (202with the canonical session id)subscribeSession()consumes the durable SSE streamGET /api/sessions/:id/events;subscribeSessionList()consumesGET /api/events- Includes
X-Client-Idheader 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