11 KiB
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
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.tsparses subcommands and routes execution. - API client:
src/apihandles HTTP + Socket.IO, encryption, and RPC. - Daemon:
src/daemonruns in the background, spawns sessions, and maintains machine state. - Persistence/config:
src/persistence.ts+src/configuration.tsmanage local state in~/.happy. - Agents:
src/claude,src/codex,src/geminiprovide provider-specific runners.
CLI entry flow
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
graph LR
subgraph "~/.happy"
direction TB
settings["settings.json<br/><i>profile, onboarding</i>"]
access["access.key<br/><i>encryption keys</i>"]
daemon["daemon.state.json<br/><i>PID, port, version</i>"]
logs["logs/<br/><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_URLandHAPPY_WEBAPP_URLoverride defaults.HAPPY_VARIANT,HAPPY_EXPERIMENTAL,HAPPY_DISABLE_CAFFEINATEcontrol behavior.
API client architecture
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
ApiSessionClientandApiMachineClient.
WebSocket
graph LR
subgraph "ApiSessionClient"
S_In[Receive: update]
S_Out[Emit: message, update-metadata,<br/>update-state, session-alive, usage-report]
end
subgraph "ApiMachineClient"
M_In[Receive: machine updates]
M_Out[Emit: machine-alive,<br/>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
updateevents and decrypts message content. - Emits
message,update-metadata,update-state,session-alive, andusage-report.
ApiMachineClient (src/api/apiMachine.ts) connects as a machine-scoped client:
- Sends
machine-aliveheartbeats. - Updates machine metadata/daemon state with optimistic concurrency.
- Receives machine updates and merges them locally.
Encryption
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
graph TB
subgraph "Daemon Process"
Control[Control Server<br/>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
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.- It authenticates and registers the machine with the server.
- It starts a local control server for IPC.
- It keeps a map of tracked child sessions and updates daemon state on the server.
Control server (local IPC)
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
flowchart LR
subgraph "Session Sources"
CLI[CLI<br/><i>foreground</i>]
Daemon[Daemon<br/><i>background</i>]
Remote[Mobile/Web<br/><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
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
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<br/>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