Architecture
System design, core components, data flow, and the decisions behind Forge.
On this page
System Overview
Forge is a single Node.js process that communicates over stdio (JSON-RPC) or HTTP. It manages real terminal processes via node-pty and optionally serves a web dashboard for live monitoring.
┌─────────────────────────────────────┐ │ MCP Client (any agent) │ └──────────────┬──────────────────────┘ │ stdio (JSON-RPC) or HTTP ┌──────────────▼──────────────────────┐ │ MCP Server (server.ts) │ │ 23 tools + 1 resource template │ │ │ │ ┌──────────────────────────┐ │ │ │ SessionManager │ │ │ │ create / close / list │ │ │ │ persistence ~/.forge/ │ │ │ └──────────┬───────────────┘ │ │ │ │ │ ┌─────────▼─────────┐ │ │ │ TerminalSession(s) │ │ │ │ node-pty (PTY) │ │ │ │ RingBuffer (1MB) │ │ │ │ @xterm/headless │ │ │ │ CommandHistory │ │ │ └─────────┬─────────┘ │ └─────────────┼─────────────────────┘ │ ┌──────────────┼──────────────┐ ▼ ▼ ▼ MCP Client Dashboard WS Event Subs (read_*) (live stream) (notifications)
Core Components
1. MCP Server src/server.ts
Single file, ~2100 lines. Registers all 23 tools and 1 resource template with the MCP SDK. Each tool follows the pattern:
server.tool("name", "description", { /* zod schema */ }, async (params) => { return { content: [{ type: "text", text: JSON.stringify(result) }] }; });
Error handling: try-catch per tool, returns { isError: true } on failure. Never throws.
2. Session Manager src/core/session-manager.ts
CRUD for terminal sessions. Enforces max session limit. Tracks stale entries from previous server runs. Emits events on session create/close for dashboard updates.
create(opts)
Spawns TerminalSession, generates 8-char UUID, wires exit callback
close(id)
Kills PTY, removes from map, persists state
listByTag(tag)
Filter active sessions by tag
closeByTag(tag)
Batch close (used by close_group tool)
3. Terminal Session src/core/terminal-session.ts
Wraps a single PTY process. Each session has:
Real PTY with signals, colors, TUI support
1MB circular buffer, per-consumer cursors for incremental reads
Server-side terminal emulator for read_screen (no ANSI codes)
Auto-closes after 30min inactivity (configurable)
Listener pattern: onData(fn) and onExit(fn) — used by wait_for, subscribe_events, and templates.
4. Ring Buffer src/core/ring-buffer.ts
Circular byte buffer. The core differentiator over simple string concatenation.
- • Fixed size (default 1MB), old data silently overwritten
-
•
Per-consumer cursors: each
read()returns only NEW data since that consumer's last read -
•
droppedBytestells the consumer how much was lost to wrapping -
•
readFullBuffer()returns everything currently in the buffer (used by grep_terminal, wait_for backlog check)
5. Command History src/core/command-history.ts
Persists tool call events as JSONL to ~/.forge/history/{sessionId}.jsonl.
Event types: session_init, tool_call, tool_result.
For agent sessions, terminal output streams through StreamJsonParser which extracts internal JSON-RPC events.
Data Flow
Incremental Read
Agent calls read_terminal(id) -> SessionManager.get(id) -> TerminalSession.read() -> RingBuffer.read(consumerId) // only bytes since last read -> { status, data, bytes }
Write
Agent calls write_terminal(id, input) -> SessionManager.get(id) -> TerminalSession.write(input + "\n") -> node-pty.write() -> PTY process receives input
wait_for (pattern mode)
1. Check backlog: session.readFullBuffer().match(regex) -> instant return if found 2. Subscribe: session.onData(chunk => accumulated.match(regex)) 3. Subscribe: session.onExit() -> resolve as "session_exited" 4. setTimeout -> resolve as "timeout" 5. First to fire wins, cleanup unsubscribes all others
wait_for (exit mode)
1. Check: session.status === "exited" -> instant return with exitCode 2. Subscribe: session.onExit(exitCode) -> resolve 3. setTimeout -> resolve as "timeout"
spawn_claude / spawn_codex / spawn_gemini
1. Build args: ["--print", "-p", prompt] + model/budget/tools flags 2. If worktree: git worktree add -> set cwd to worktree path 3. Create: manager.create({ command: agentPath, args }) 4. Wire: StreamJsonParser for history extraction 5. Return session info + worktree path
Key Design Decisions
stdout is sacred
Only MCP JSON-RPC goes to stdout. All logging to stderr via structured JSON logger.
Ring buffer, not unbounded arrays
Bounded memory per session. Old data silently overwritten. Terminal output can be massive (npm install, log tailing).
Per-consumer cursors
Each read_terminal caller gets their own read position. Two consumers reading the same session see different data depending on when they last read.
Fire-and-forget history
CommandHistory appends are non-blocking. A failed disk write doesn't break the terminal session.
Agent env stripping
Spawned terminals have agent-specific env vars (e.g., CLAUDECODE) removed to prevent nesting errors.
Session preservation after exit
Exited sessions remain readable (buffer intact) until explicitly closed or server restarts. Allows post-mortem inspection.
30KB read cap
read_terminal caps output at 30KB per call to prevent MCP token overflow. Full buffer available via readFullBuffer() in grep_terminal and wait_for.
Desktop split-port
When an existing daemon occupies port 3141, the desktop app serves its own HTML on a random port and bridges to the daemon via WebSocket, rather than conflicting.
Dashboard Architecture
Preact + htm + Preact Signals UI. Zero build step — loaded from CDN, code bundled as string constants inside the server binary.
Frontend
app.ts
Root component, keyboard handlers, WebSocket init
state.ts
17+ Preact Signals for global state
components/
sidebar, terminal-view, chat-view, code-review, modals
Server-side
dashboard-server.ts
HTTP server + REST API + WebSocket
ws-handler.ts
subscribe/select/input/resize messages
REST endpoints
/api/sessions, /api/chats, /api/chats/{id}
Desktop App Architecture
Native macOS Electron app. The main process either runs the Forge server in-process or bridges to an existing daemon.
Electron Main Process (Node.js / CJS) ├── daemon.ts -> Detects existing daemon OR starts server in-process │ ├── In-process: createServer() + DashboardServer on 127.0.0.1:3141 │ └── External: DesktopHtmlServer (random port) + DaemonBridge (WS relay) ├── window.ts -> BrowserWindow (hiddenInset title bar, sandbox, CSP) ├── preload.ts -> contextBridge { isDesktop, platform } ├── tray.ts -> System tray icon + session count ├── menu.ts -> Standard macOS app menu └── notifications.ts -> Native alerts on session create/exit
Security
Project Structure
src/ cli.ts # Entry point, arg parsing, stdio transport server.ts # MCP Server + 23 tool registrations core/ types.ts # ForgeConfig, SessionInfo, defaults ring-buffer.ts # Circular buffer with multi-consumer cursors terminal-session.ts # PTY + headless xterm + ring buffer session-manager.ts # CRUD, max sessions, groups, persistence state-store.ts # ~/.forge/sessions.json persistence templates.ts # Built-in session templates claude-chats.ts # Claude Code chat session discovery codex-chats.ts # Codex chat history gemini-chats.ts # Gemini CLI chat history command-history.ts # Tool call history tracking stream-json-parser.ts # Real-time event extraction dashboard/ dashboard-server.ts # HTTP + WebSocket + MCP transport dashboard-html.ts # HTML assembler ws-handler.ts # WebSocket message handling frontend/ app.ts # Root Preact component state.ts # Preact Signals + WebSocket styles.ts # Tokyo Night theme CSS components/ # sidebar, terminal-view, chat-view, modals utils/ logger.ts # stderr-only JSON logger config.ts # CLI flags > env vars > defaults daemon.ts # Daemon lifecycle (PID, port, lock) desktop/ main/ # Electron main process test/ unit/ # ring-buffer, config, control-chars, etc. integration/ # terminal-session, session-manager, MCP E2E
Test Coverage
| Suite | Tests | Type |
|---|---|---|
| Ring Buffer | 13 | Unit |
| Config | 10 | Unit |
| Control Chars | 6 | Unit |
| State Store | 4 | Unit |
| Templates | 3 | Unit |
| Stream JSON Parser | 11 | Unit |
| Command History | 6 | Unit/Integration |
| Claude Chats | 14 | Unit |
| Terminal Session | 8 | Integration |
| Session Manager | 7 | Integration |
| MCP Tools E2E | 51 | Integration |
| Forge 0.7+ Features | 28 | Integration |
| Total | 161 |