Back to Happy

CLI Architecture

docs/cli-architecture.md

310.7 KB
Original Source

CLI Architecture

This document describes the Happy CLI (packages/happy-cli) and its daemon. The CLI is both an interactive tool and a background session manager that keeps machine state in sync with the server.

System overview

mermaid
graph TB
    subgraph "Happy CLI"
        Entry[src/index.ts]
        API[API Client]
        Daemon[Daemon Process]
        Agents[Agent Runners]
        Persist[Persistence]
    end

    subgraph "~/.happy"
        Settings[settings.json]
        AccessKey[access.key]
        DaemonState[daemon.state.json]
        Logs[logs/]
    end

    subgraph Server
        HTTP[HTTP API]
        Socket[Socket.IO]
    end

    Entry --> API
    Entry --> Daemon
    Entry --> Agents
    Entry --> Persist

    Persist --> Settings & AccessKey & DaemonState & Logs

    API --> HTTP & Socket
    Daemon --> API
    Agents --> API

High-level layout

  • Entry point: src/index.ts parses subcommands and routes execution.
  • API client: src/api handles HTTP + Socket.IO, encryption, and RPC.
  • Daemon: src/daemon runs in the background, spawns sessions, and maintains machine state.
  • Persistence/config: src/persistence.ts + src/configuration.ts manage local state in ~/.happy.
  • Agents: src/claude, src/codex, src/gemini provide provider-specific runners.

CLI entry flow

mermaid
flowchart TD
    Start([happy ...]) --> Parse[Parse subcommand]

    Parse --> Doctor{doctor?}
    Parse --> Auth{auth?}
    Parse --> Connect{connect?}
    Parse --> Agent{codex/gemini?}
    Parse --> Default{default}

    Doctor --> RunDoctor[Run diagnostics]
    Auth --> RunAuth[Auth flow]
    Connect --> RunConnect[Connect machine]

    Agent --> Setup[authAndSetupMachineIfNeeded]
    Default --> Setup

    Setup --> Context{Background?}
    Context --> |Yes| StartDaemon[Start daemon]
    Context --> |No| RunAgent[Run agent directly]

    StartDaemon --> SpawnSession[Spawn session]

src/index.ts is the CLI router. It:

  • Parses subcommands (doctor, auth, connect, codex, gemini, and default run flows).
  • Ensures auth and machine setup when needed (authAndSetupMachineIfNeeded).
  • Starts the daemon or runs an agent directly based on subcommand/context.

Local state and configuration

mermaid
graph LR
    subgraph "~/.happy"
        direction TB
        settings["settings.json
<i>profile, onboarding</i>"]
        access["access.key
<i>encryption keys</i>"]
        daemon["daemon.state.json
<i>PID, port, version</i>"]
        logs["logs/
<i>CLI/daemon logs</i>"]
    end

    subgraph "Environment Overrides"
        direction TB
        E1[HAPPY_HOME_DIR]
        E2[HAPPY_SERVER_URL]
        E3[HAPPY_WEBAPP_URL]
        E4[HAPPY_VARIANT]
        E5[HAPPY_EXPERIMENTAL]
        E6[HAPPY_DISABLE_CAFFEINATE]
    end

    E1 -.-> settings & access & daemon & logs

Local state lives under ~/.happy (or HAPPY_HOME_DIR):

  • settings.json: onboarding and profile settings (validated/migrated).
  • access.key: local key material for encryption/auth.
  • daemon.state.json: daemon PID + control port + version.
  • logs/: CLI/daemon logs.

Configuration lives in src/configuration.ts:

  • HAPPY_SERVER_URL and HAPPY_WEBAPP_URL override defaults.
  • HAPPY_VARIANT, HAPPY_EXPERIMENTAL, HAPPY_DISABLE_CAFFEINATE control behavior.

API client architecture

mermaid
graph TB
    subgraph "API Clients"
        Base[ApiClient]
        Session[ApiSessionClient]
        Machine[ApiMachineClient]
        Encrypt[encryption.ts]
    end

    subgraph "Server"
        HTTP[HTTP API]
        Socket[Socket.IO]
    end

    Base --> |POST /v1/sessions| HTTP
    Base --> |POST /v1/machines| HTTP

    Session --> |session-scoped| Socket
    Machine --> |machine-scoped| Socket

    Encrypt --> Base & Session & Machine

HTTP

ApiClient (src/api/api.ts) handles:

  • Session creation (POST /v1/sessions) with encrypted metadata/state.
  • Machine registration (POST /v1/machines) with encrypted metadata/daemon state.
  • Other CRUD actions through ApiSessionClient and ApiMachineClient.

WebSocket

mermaid
graph LR
    subgraph "ApiSessionClient"
        S_In[Receive: update]
        S_Out[Emit: message, update-metadata,
update-state, session-alive, usage-report]
    end

    subgraph "ApiMachineClient"
        M_In[Receive: machine updates]
        M_Out[Emit: machine-alive,
update metadata/state]
    end

    Server((Socket.IO)) --> S_In & M_In
    S_Out & M_Out --> Server

ApiSessionClient (src/api/apiSession.ts) connects to Socket.IO as a session-scoped client:

  • Receives update events and decrypts message content.
  • Emits message, update-metadata, update-state, session-alive, and usage-report.

ApiMachineClient (src/api/apiMachine.ts) connects as a machine-scoped client:

  • Sends machine-alive heartbeats.
  • Updates machine metadata/daemon state with optimistic concurrency.
  • Receives machine updates and merges them locally.

Encryption

mermaid
flowchart LR
    subgraph "Client-side"
        Plain[Plaintext Data]
        Encrypt[encryption.ts]
        B64[Base64 Encoded]
    end

    Plain --> |encrypt| Encrypt --> B64 --> |send| Server[(Server)]
    Server --> |receive| B64 --> |decrypt| Encrypt --> Plain

    style Plain fill:#e8f5e9
    style B64 fill:#fff3e0

The CLI encrypts client content before it leaves the machine using src/api/encryption.ts.

  • Session metadata, agent state, messages, machine state, artifacts, and KV values are encrypted client-side.
  • On-wire encoding is base64; see encryption.md.

Daemon architecture

mermaid
graph TB
    subgraph "Daemon Process"
        Control[Control Server
127.0.0.1:port]
        Sessions[Session Map]
        MachineClient[ApiMachineClient]
    end

    subgraph "Child Processes"
        S1[Session 1]
        S2[Session 2]
        S3[Session N]
    end

    CLI[CLI] --> |IPC| Control
    Control --> Sessions
    Sessions --> S1 & S2 & S3

    MachineClient --> |heartbeat| Server[(Server)]
    MachineClient --> |state sync| Server

The daemon is a long-lived process responsible for running sessions in the background and maintaining machine presence.

Lifecycle

mermaid
flowchart TD
    Start([startDaemon]) --> Validate[Validate version]
    Validate --> Lock[Acquire lock file]
    Lock --> Auth[Authenticate]
    Auth --> Register[Register machine with server]
    Register --> Control[Start control server]
    Control --> Track[Track child sessions]
    Track --> Sync[Sync daemon state to server]
    Sync --> Running([Running])

    Running --> |SIGTERM| Shutdown[Cleanup & exit]
  1. startDaemon() validates the running version and acquires a lock file.
  2. It authenticates and registers the machine with the server.
  3. It starts a local control server for IPC.
  4. It keeps a map of tracked child sessions and updates daemon state on the server.

Control server (local IPC)

mermaid
sequenceDiagram
    participant CLI
    participant State as daemon.state.json
    participant Control as Control Server
    participant Daemon

    CLI->>State: Read port
    State-->>CLI: port: 12345

    CLI->>Control: GET /list
    Control-->>CLI: [sessions...]

    CLI->>Control: POST /spawn-session
    Control->>Daemon: Spawn child process
    Daemon-->>Control: Session started
    Control-->>CLI: OK

    CLI->>Control: POST /stop
    Control->>Daemon: Shutdown

startDaemonControlServer() (src/daemon/controlServer.ts) runs an HTTP server on 127.0.0.1 and exposes:

  • /list (list active sessions)
  • /stop-session
  • /spawn-session
  • /stop (shutdown daemon)
  • /session-started (session self-report)

The CLI talks to this server via controlClient.ts, using a port stored in daemon.state.json.

Session spawning

mermaid
flowchart LR
    subgraph "Session Sources"
        CLI[CLI
<i>foreground</i>]
        Daemon[Daemon
<i>background</i>]
        Remote[Mobile/Web
<i>via RPC</i>]
    end

    subgraph "Session Process"
        Session[Agent Session]
        Handlers[RPC Handlers]
    end

    CLI --> Session
    Daemon --> Session
    Remote --> |spawn-session| Daemon --> Session

    Session --> Handlers

    subgraph "RPC Surface"
        Handlers --> Bash[bash]
        Handlers --> Files[file read/write]
        Handlers --> Search[ripgrep]
        Handlers --> Diff[difftastic]
    end

Sessions can be started by:

  • The CLI directly (foreground).
  • The daemon (background).
  • Remote requests over RPC (from mobile/web via machine connection).

Daemon session spawning uses registerCommonHandlers to expose a controlled RPC surface (shell commands, file operations, search/diff helpers).

Machine state

mermaid
graph TB
    subgraph "Machine Metadata (static)"
        M1[host]
        M2[platform]
        M3[CLI version]
        M4[paths]
    end

    subgraph "Daemon State (dynamic)"
        D1[pid]
        D2[httpPort]
        D3[startedAt]
        D4[shutdown info]
    end

    subgraph "Sync Targets"
        Server[(Server)]
        Local[daemon.state.json]
    end

    ApiMachine[ApiMachineClient]

    M1 & M2 & M3 & M4 --> ApiMachine
    D1 & D2 & D3 & D4 --> ApiMachine
    D1 & D2 & D3 & D4 --> Local

    ApiMachine --> Server
  • Machine metadata is static info (host, platform, CLI version, paths).
  • Daemon state is dynamic (pid, httpPort, startedAt, shutdown info).

The daemon updates these via ApiMachineClient and mirrors local state into daemon.state.json for control/diagnostics.

RPC and tool bridge

mermaid
sequenceDiagram
    participant Mobile
    participant Server
    participant Daemon
    participant Session

    Mobile->>Server: RPC: spawn-session
    Server->>Daemon: Forward via Socket.IO
    Daemon->>Session: Spawn process
    Session-->>Daemon: Running

    Mobile->>Server: RPC: bash "ls -la"
    Server->>Session: Forward via Socket.IO
    Session->>Session: Execute command
    Session-->>Server: Result
    Server-->>Mobile: Result

    Note over Mobile,Session: All RPC flows through Socket.IO
No direct REST exposure

RPC is used to send commands over the Socket.IO connection:

  • Sessions register RPC handlers (e.g., bash, file read/write, ripgrep, difftastic).
  • The daemon registers a spawn-session handler so the server/mobile client can ask it to start a local session.

This mechanism allows the server and mobile clients to drive local actions without exposing a broad REST surface.

Implementation references

  • CLI entry: packages/happy-cli/src/index.ts
  • Daemon: packages/happy-cli/src/daemon
  • Control server/client: packages/happy-cli/src/daemon/controlServer.ts, packages/happy-cli/src/daemon/controlClient.ts
  • API clients: packages/happy-cli/src/api
  • Persistence: packages/happy-cli/src/persistence.ts
  • Config: packages/happy-cli/src/configuration.ts