98e40dac97
CLI Smoke Test / smoke-test-linux (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-linux (24) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (24) (push) Has been cancelled
Expo App TypeScript typecheck / typecheck (push) Has been cancelled
382 lines
11 KiB
Markdown
382 lines
11 KiB
Markdown
# 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<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_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,<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 `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<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
|
|
|
|
```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<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
|
|
|
|
```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<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`
|