DorkOS
Concepts

Transport

How the Transport interface enables DorkOS to run in multiple environments

Transport

The Transport interface is the core abstraction that makes DorkOS work across different environments. It defines a contract between the React client and whatever backend serves Claude responses — whether that is an Express server over HTTP or direct in-process calls inside Obsidian.

Why Transport Exists

DorkOS runs in two very different environments:

  • Standalone web — A browser talking to an Express server over the network
  • Obsidian plugin — A React app embedded in Electron, calling services directly in the same process

Without this abstraction, client code would need conditional logic everywhere: "if we're in a browser, use fetch; if we're in Obsidian, call the service directly." Transport eliminates this entirely. The client calls transport.sendMessage() and never knows how that message reaches Claude.

The Interface

Transport defines methods for every operation the client needs, organized by domain:

Sessions

MethodPurpose
createSessionStart a new Claude conversation
listSessionsGet all sessions, optionally filtered by directory
getSessionFetch metadata for a single session
updateSessionChange session settings (permission mode, model)
getMessagesLoad message history for a session
sendMessageSend a message and stream the response (accepts optional uiState for agent UI awareness)
stopTaskStop a running background task by ID
interruptSessionInterrupt the active query for a session (best-effort, graceful then forceful)
approveToolApprove a pending tool call
denyToolDeny a pending tool call
submitAnswersRespond to an interactive question prompt
getTasksGet the current task list for a session
getLastMessageIdsGet JSONL-assigned IDs for the last user/assistant messages (client-server reconciliation)
forkSessionFork an existing session — creates a copy with the same history up to a given message
renameSessionRename a session (updates the JSONL first-message summary)
submitElicitationSubmit a response to an MCP elicitation prompt (structured form input from the agent)

Filesystem and Config

MethodPurpose
browseDirectoryBrowse the filesystem for directory selection
getDefaultCwdGet the server's default working directory
getCommandsList available slash commands
listFilesList files in a directory
getGitStatusGet git branch and change information
getMcpConfigRead MCP server entries from .mcp.json in a project directory
healthServer health check
getConfigRetrieve server configuration
updateConfigPartially update the persisted user config

Runtime and Capabilities

MethodPurpose
getModelsList available Claude models
getCapabilitiesGet capability flags for all registered runtimes
startTunnelStart the ngrok tunnel and return the public URL
stopTunnelStop the ngrok tunnel
getTemplatesFetch the merged template catalog (builtin + user)
setDefaultAgentSet the default agent by name
createDirectoryCreate a new directory (used by DirectoryPicker UI)
reloadPluginsReload all MCP plugin servers for a session (picks up new MCP config)

File Uploads

MethodPurpose
uploadFilesUpload files to the session's working directory for agent access

Tasks, Relay, Mesh

Transport also exposes full Tasks scheduler methods (listSchedules, createSchedule, triggerSchedule, listRuns, etc.), Relay messaging methods (sendRelayMessage, listRelayAdapters, getRelayTrace, etc.), and Mesh discovery methods (listMeshAgents, registerMeshAgent, getMeshTopology, etc.). These follow the same pattern as the core session methods.

The Transport interface is defined in @dorkos/shared so both the client and server reference the same types.

Streaming with Callbacks

sendMessage handles real-time streaming. Rather than returning a stream object, it accepts an onEvent callback:

transport.sendMessage(sessionId, content, onEvent, signal, cwd, options)

The optional options parameter accepts clientMessageId (for server-echo ID reconciliation) and uiState (a snapshot of the client's current UI state, enabling agent-driven UI control via MCP tools like controlUI and get_ui_state).

Each time a new event arrives — a text chunk, a tool call, an approval request — the transport calls onEvent with a StreamEvent object. This callback pattern works naturally with both adapters:

  • HttpTransport parses SSE lines from a ReadableStream and calls onEvent for each parsed event
  • DirectTransport iterates an AsyncGenerator from the Agent SDK and calls onEvent for each yielded event

The optional AbortSignal parameter lets you cancel an in-progress request, which is useful when the user clicks "Stop" during streaming.

HttpTransport

The default adapter for standalone web deployments. It communicates with the DorkOS Express server using standard web APIs.

  • CRUD operations (listSessions, getMessages, etc.) use fetch() with JSON bodies
  • sendMessage uses fetch() with a streaming response — the server sends Server-Sent Events, and the transport parses them into StreamEvent objects
  • Constructor takes a baseUrl (defaults to /api)

Use this for any deployment where DorkOS runs as a separate process — local development, self-hosted, or tunneled remote access.

DirectTransport

The adapter for the Obsidian plugin. It calls service instances directly without any network communication.

  • Service methods are called directly as function calls in the same process
  • sendMessage iterates the AsyncGenerator<StreamEvent> returned by the AgentRuntime and forwards each event to the callback
  • Session IDs are generated locally with crypto.randomUUID()
  • No HTTP overhead, no serialization, no port binding

Use this inside the Obsidian plugin, where the React client and backend services share the same Electron process.

Dependency Injection

Transport is injected into the React component tree via React Context. At the app root, a TransportProvider wraps the component tree with the appropriate adapter:

Standalone web: The entry point creates an HttpTransport pointing at the server's API base URL and wraps the app with TransportProvider.

Obsidian plugin: The plugin view creates service instances, passes them to a DirectTransport, and wraps the app with TransportProvider.

Any component or hook that needs to communicate with the backend calls useTransport() to get the current transport instance. All client code stays transport-agnostic.

Next Steps