docs/cli-architecture.md
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.
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
src/index.ts parses subcommands and routes execution.src/api handles HTTP + Socket.IO, encryption, and RPC.src/daemon runs in the background, spawns sessions, and maintains machine state.src/persistence.ts + src/configuration.ts manage local state in ~/.happy.src/claude, src/codex, src/gemini provide provider-specific runners.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:
doctor, auth, connect, codex, gemini, and default run flows).authAndSetupMachineIfNeeded).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.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
ApiClient (src/api/api.ts) handles:
POST /v1/sessions) with encrypted metadata/state.POST /v1/machines) with encrypted metadata/daemon state.ApiSessionClient and ApiMachineClient.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:
update events and decrypts message content.message, update-metadata, update-state, session-alive, and usage-report.ApiMachineClient (src/api/apiMachine.ts) connects as a machine-scoped client:
machine-alive heartbeats.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.
encryption.md.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.
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]
startDaemon() validates the running version and acquires a lock file.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.
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:
Daemon session spawning uses registerCommonHandlers to expose a controlled RPC surface (shell commands, file operations, search/diff helpers).
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
The daemon updates these via ApiMachineClient and mirrors local state into daemon.state.json for control/diagnostics.
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:
bash, file read/write, ripgrep, difftastic).This mechanism allows the server and mobile clients to drive local actions without exposing a broad REST surface.
packages/happy-cli/src/index.tspackages/happy-cli/src/daemonpackages/happy-cli/src/daemon/controlServer.ts, packages/happy-cli/src/daemon/controlClient.tspackages/happy-cli/src/apipackages/happy-cli/src/persistence.tspackages/happy-cli/src/configuration.ts