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