DorkOS
Contributing

Architecture

Architecture overview for DorkOS contributors

Architecture

DorkOS uses a hexagonal (ports & adapters) architecture that lets the same React client run in two modes:

  1. Standalone web — Express server with HTTP/SSE communication
  2. Obsidian plugin — In-process services, no server or network required

This guide explains how the pieces fit together.

Monorepo Structure

DorkOS is a Turborepo monorepo with five apps and nine shared packages:

The Transport Interface

The core abstraction is the Transport interface (packages/shared/src/transport.ts). It defines all client-server communication — session management, messaging, tool interaction, Tasks, Relay, Mesh, and agent identity.

A condensed view of the core methods:

interface Transport {
  createSession(opts): Promise<Session>
  listSessions(cwd?)       → Promise<Session[]>
  getSession(id, cwd?)     → Promise<Session>
  getMessages(sessionId)   → Promise<{ messages: HistoryMessage[] }>
  sendMessage(id, content, onEvent, signal?, cwd?, options?) → Promise<void>
  // options: { clientMessageId?: string; uiState?: UiState }
  stopTask(sessionId, taskId) → Promise<{ success: boolean; taskId: string }>
  approveTool(sessionId, toolCallId)  → Promise<{ ok: boolean }>
  denyTool(sessionId, toolCallId)     → Promise<{ ok: boolean }>
  getCommands(refresh?)    → Promise<CommandRegistry>
  getModels()              → Promise<ModelOption[]>
  getCapabilities()        → Promise<{ capabilities, defaultRuntime }>
  health()                 → Promise<{ status, version, uptime }>
}

The interface extends further to cover Tasks schedules, Relay messaging, Mesh agents, bindings, and admin operations. See packages/shared/src/transport.ts for the complete definition.

Two Transport Implementations

HttpTransport communicates with the Express server over HTTP:

  • Uses fetch() for CRUD operations
  • Parses Server-Sent Events (SSE) streams in sendMessage()
  • Converts SSE events into StreamEvent objects that update the UI

Location: apps/client/src/layers/shared/lib/http-transport.ts

DirectTransport calls service instances directly in the same process:

  • No HTTP, no port binding, no network serialization
  • Iterates AsyncGenerator<StreamEvent> from the Claude Agent SDK
  • Lower latency, ideal for embedded contexts

Location: apps/client/src/layers/shared/lib/direct-transport.ts

Both implementations expose the same interface, so the React client doesn't know which one it's using.

Dependency Injection via React Context

The Transport is injected into the React app via a context provider:

// main.tsx
const transport = new HttpTransport({ baseUrl: '/api' })

<TransportProvider transport={transport}>
  <App />
</TransportProvider>
// CopilotView.tsx
const runtime = new ClaudeCodeRuntime(repoRoot)
const commandRegistry = new CommandRegistryService(repoRoot)

const transport = new DirectTransport({
runtime,
transcriptReader: runtime.getTranscriptReader(),
commandRegistry,
vaultRoot: repoRoot
})

<TransportProvider transport={transport}>
  <ObsidianApp>
    <App />
  </ObsidianApp>
</TransportProvider>

Components and hooks access the transport via useTransport():

import { useTransport } from '@/layers/shared/model/TransportContext';

function MyComponent() {
  const transport = useTransport();
  const sessions = await transport.listSessions();
}

Server Architecture

The Express server (apps/server/) is organized into routes and services.

Routes

Routes are thin HTTP handlers. They obtain the active runtime via runtimeRegistry.getDefault():

Prop

Type

Services

Prop

Type

Session Storage: SDK JSONL Transcripts

Sessions are not stored in a database. TranscriptReader scans SDK JSONL files at ~/.claude/projects/\{slug\}/*.jsonl. All sessions are visible regardless of which client created them — CLI, DorkOS, or otherwise.

  • Session ID = SDK session ID (UUID from filename)
  • No delete endpoint — sessions persist in SDK storage
  • Session metadata (title, preview, timestamps) extracted from file content on every request
  • ClaudeCodeRuntime calls SDK query() with resume: sessionId for continuity across clients

Client Architecture

The React client (apps/client/) uses Feature-Sliced Design (FSD):

FSD Layers

LayerPurposeExamples
shared/ui/Reusable UI primitivesBadge, Dialog, Select, Tabs (shadcn)
shared/model/Hooks, stores, contextTransportContext, app-store, useTheme
shared/lib/Domain-agnostic utilitiescn(), font-config, celebrations
entities/session/Session domain hooksuseSessionId, useSessions
entities/runtime/Runtime capabilitiesuseRuntimeCapabilities
entities/agent/Agent identity hooksuseCurrentAgent, useAgentVisual
entities/tasks/Task scheduler hooksuseTasksEnabled, useSchedules, useRuns
entities/relay/Relay messaging hooksuseRelayEnabled, useRelayMessages
entities/mesh/Mesh discovery hooksuseMeshEnabled, useRegisteredAgents
features/chat/Chat interfaceChatPanel, MessageList, ToolCallCard
features/session-list/Session managementSessionSidebar, SessionItem
features/settings/Settings UISettingsDialog
features/tasks/Task scheduler UITasksPanel, CreateScheduleDialog
features/relay/Relay messaging UIRelayPanel, AdapterCard
features/mesh/Mesh discovery UIMeshPanel, TopologyGraph
features/canvas/Agent-controlled UI panelsCanvas renderer, extension host
entities/marketplace/Marketplace data hooksuseMarketplacePackages, useInstallPackage, usePermissionPreview
features/marketplace/Dork Hub browse and install UIDorkHub, PackageCard, PackageDetailSheet, InstallConfirmationDialog
widgets/marketplace/Marketplace pagesDorkHubPage (/marketplace), MarketplaceSourcesPage (/marketplace/sources)
widgets/app-layout/App-level layoutPermissionBanner

Layer imports are strictly unidirectional: sharedentitiesfeatureswidgetsapp. Cross-feature model/hook imports are forbidden.

State Management

  • Zustand for UI state (sidebar, theme, etc.) — layers/shared/model/app-store.ts
  • TanStack Query for server state (sessions, messages, commands, schedules)
  • URL Parameters (standalone mode) — ?session= and ?dir= persist state in the URL

Data Flow: Message from UI to Claude and Back

User types message

ChatPanel → useChatSession.handleSubmit()

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

fetch(POST /api/sessions/:id/messages) + ReadableStream SSE parsing

Express route → runtimeRegistry.getDefault().sendMessage() → SDK query()

SDK yields StreamEvent objects → SSE wire format

HttpTransport parses SSE → calls onEvent(event)

React state updates → UI re-renders with new message chunks
User types message

ChatPanel → useChatSession.handleSubmit()

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

DirectTransport → runtime.sendMessage() → SDK query()

SDK yields AsyncGenerator<StreamEvent>

DirectTransport iterates generator → calls onEvent(event)

React state updates → UI re-renders with new message chunks

StreamEvent Types

Prop

Type

Session Sync Protocol

Subscribe to real-time session changes via GET /api/sessions/:id/stream (persistent SSE). This enables multi-client sync — CLI writes appear in the DorkOS UI automatically.

Events:

  • sync_connected — Sent on initial connection. Data: { sessionId }
  • sync_update — Sent when new content is written to the session's JSONL file. Data: { sessionId, timestamp }

On sync_update, re-fetch message history. GET /messages supports ETag caching for efficient polling.

Testing

Tests use Vitest with vi.mock() for Node modules. Client tests inject mock Transport objects via TransportProvider:

import { createMockTransport } from '@dorkos/test-utils'

const mockTransport = createMockTransport({
  listSessions: vi.fn().mockResolvedValue([]),
  sendMessage: vi.fn(),
})

function Wrapper({ children }: { children: React.ReactNode }) {
  return (
    <TransportProvider transport={mockTransport}>
      {children}
    </TransportProvider>
  )
}

render(<MyComponent />, { wrapper: Wrapper })

This pattern provides type safety and explicit test setup without global mocks.

Key Design Properties

  • Same React app, two deployment modes — Transport abstraction enables full code reuse
  • No network required for Obsidian — DirectTransport runs entirely in-process
  • Type-safe end-to-end — Zod schemas generate both TypeScript types and OpenAPI specs
  • Testable — Mock Transport objects make testing React hooks and components straightforward
  • Real-time sync — SSE streaming and file watching keep all clients in sync
  • SDK-first storage — JSONL transcripts are the single source of truth (no separate database)

Next Steps