Architecture

System design, core components, data flow, and the decisions behind Forge.

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:

node-pty

Real PTY with signals, colors, TUI support

RingBuffer

1MB circular buffer, per-consumer cursors for incremental reads

@xterm/headless

Server-side terminal emulator for read_screen (no ANSI codes)

Idle timer

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
  • droppedBytes tells 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

1

stdout is sacred

Only MCP JSON-RPC goes to stdout. All logging to stderr via structured JSON logger.

2

Ring buffer, not unbounded arrays

Bounded memory per session. Old data silently overwritten. Terminal output can be massive (npm install, log tailing).

3

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.

4

Fire-and-forget history

CommandHistory appends are non-blocking. A failed disk write doesn't break the terminal session.

5

Agent env stripping

Spawned terminals have agent-specific env vars (e.g., CLAUDECODE) removed to prevent nesting errors.

6

Session preservation after exit

Exited sessions remain readable (buffer intact) until explicitly closed or server restarts. Allows post-mortem inspection.

7

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.

8

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

sandbox + contextIsolation enabled
Navigation locked to localhost
CSP restricts to self + jsdelivr CDN
All permissions denied (camera, mic, etc.)

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 Buffer13Unit
Config10Unit
Control Chars6Unit
State Store4Unit
Templates3Unit
Stream JSON Parser11Unit
Command History6Unit/Integration
Claude Chats14Unit
Terminal Session8Integration
Session Manager7Integration
MCP Tools E2E51Integration
Forge 0.7+ Features28Integration
Total161