Happy CLI is a command-line wrapper for Claude Code and OpenAI Codex CLI that enables remote control via mobile app and session sharing. It consists of three components:
- handy-cli (this project) - CLI wrapper
- handy - React Native mobile client
- handy-server - Node.js server at https://api.happy-servers.com/
- Claude Code: Integrated via
@anthropic-ai/claude-codeSDK - OpenAI Codex: Integrated via Model Context Protocol (MCP) subprocess
Mobile App (user input)
↓ WebSocket (encrypted)
Server
↓ WebSocket 'update' event
ApiSessionClient
↓ onUserMessage callback
MessageQueue
↓ batch processing
Agent MCP Client (Claude/Codex)
↓ MCP tool calls (startSession/continueSession)
Agent subprocess
↓ MCP event notifications
Message handlers
↓ sendCodexMessage/sendClaudeSessionMessage
Server
↓ WebSocket broadcast
Mobile App
Purpose: Manages WebSocket connection to server with end-to-end encryption
Key Features:
- Lazy connection via
ensureConnected()to prevent race conditions - E2E encryption using TweetNaCl (NaCl crypto library)
- Optimistic concurrency control for metadata/state updates
- RPC handler registration for bidirectional communication
- Message queue buffering during initialization
Connection Lifecycle:
- Constructor creates socket with
autoConnect: false - Registers event handlers for 'update', 'connect', 'disconnect'
onUserMessage(callback)sets callback and callsensureConnected()ensureConnected()initiates connection, returns Promise- Socket 'connect' event resolves promise
- Messages flow directly to registered callback
Race Condition Prevention:
- Old behavior: Socket connected immediately in constructor → messages arrived before callback set
- New behavior: Connection deferred until
onUserMessage()called → callback registered BEFORE connect
Purpose: Wrapper for Codex CLI via Model Context Protocol (stdio transport)
Key Features:
- Spawns Codex as subprocess using
StdioClientTransport - Bidirectional MCP communication over stdio
- Session ID extraction and tracking
- Permission request handling via
ElicitRequestSchema - Windows console window hiding workaround
Windows-Specific Issue:
- MCP SDK only sets
windowsHide: truewhenisElectron()returns true - Workaround: Temporarily set
process.typeduring transport creation - Result: Prevents visible CMD windows on Windows platforms
Session Management:
startSession(config)- Creates new Codex session via 'codex' toolcontinueSession(prompt)- Continues existing session via 'codex-reply' toolsessionIdandconversationIdextracted from MCP responses- Session persistence for resume functionality
Permission Flow:
- Codex requests permission via MCP
ElicitRequestSchema - CodexMcpClient receives request in
setRequestHandler - Delegates to
CodexPermissionHandler.handleToolCall() - Handler sends RPC request to mobile app
- Mobile app responds with approval/denial
- Response returned to Codex via MCP
Purpose: Main entry point for Codex agent mode, coordinates all Codex operations
Key Components:
MessageQueue2: Batches user messages with mode tracking (normal/direct/plan/edit)MessageBuffer: Accumulates Codex output for streaming to mobileCodexMcpClient: MCP communication with Codex subprocessCodexPermissionHandler: Handles tool approval requests via RPCApiSessionClient: Server communication and encryption
Message Flow:
session.onUserMessage()callback pushes tomessageQueue- Main loop calls
messageQueue.waitForMessagesAndGetAsString() - Batched messages sent to Codex via
client.startSession()orcontinueSession() - Codex events flow through MCP to
client.setHandler() - Handler processes events, updates
messageBuffer - Buffer periodically flushed to server via
session.sendCodexMessage()
Session State Management:
wasCreated: Tracks if session was successfully createdstoredSessionIdForResume: Preserved session ID for crash recoverycurrentModeHash: Detects mode changes requiring session restart- Resume logic: On abort/error, stores session ID for next message continuation
Abort Handling:
- AbortController signal passed to MCP tool calls
- On abort: Stores session ID, marks
wasCreated = false - Next message resumes from stored session instead of creating new one
Purpose: Bridges Codex permission requests to mobile app via RPC
Flow:
handleToolCall(callId, toolName, args)called by CodexMcpClient- Creates pending request entry in Map
- Sends RPC request via
rpcManager.sendRequest('tool-approval-request') - Waits for RPC response callback
- Returns
{ decision: 'approved' | 'denied' }to Codex - Updates agent state with permission status
Issue: No timeout on permission requests - will hang indefinitely if mobile doesn't respond
Purpose: Encrypt all data before sending to server using TweetNaCl
Encryption Variants:
legacy: Uses session's encryption key directly (backward compatibility)dataKey: Derives per-message key from base key + nonce (more secure)
Key Operations:
encrypt(key, variant, data): Encrypts JSON object → Uint8Arraydecrypt(key, variant, encrypted): Decrypts Uint8Array → JSON object- All messages encrypted before
socket.emit('message', ...) - All incoming messages decrypted in 'update' event handler
Cause: MCP SDK only enables windowsHide when running in Electron
Impact: Visible CMD window appears for every Codex interaction on Windows
Fix: Temporarily set process.type to trick SDK into enabling windowsHide
Location: /src/codex/codexMcpClient.ts:90-126
Status: ✅ Committed (3ce58f6)
Cause: WebSocket connected before onUserMessage() callback registered
Impact: First user message lost in race condition
Fix: Defer connection until onUserMessage() called, await connection
Location: /src/api/apiSession.ts:161-209
Status: ✅ Committed (f8a1fc2)
Cause: No subprocess health monitoring, no reconnection logic Impact: Codex crashes silently, agent appears offline Fix Needed:
- Add heartbeat mechanism to detect subprocess death
- Implement exponential backoff retry on MCP connection failure
- Monitor subprocess exit events, auto-restart
Location:
/src/codex/codexMcpClient.ts,/src/codex/runCodex.tsStatus: ⏳ Pending
Cause: 400+ lines between callback registration and processing loop start Impact: Messages queued but not processed immediately Fix Needed:
- Start message processing loop immediately after
onUserMessage() - Reduce initialization gap between callback and loop
Location:
/src/codex/runCodex.ts:148-565Status: ⏳ Pending
Cause: No timeouts on MCP operations or permission requests Impact: Operations hang indefinitely on failure Fix Needed:
- Reduce MCP timeout from 14 days to reasonable value (e.g., 30s)
- Add timeout to permission handler RPC calls
- Add timeout to WebSocket operations Location: Multiple files Status: ⏳ Pending
- Try-catch blocks with specific error logging
- AbortController for cancellable operations
- Exponential backoff for retryable operations (metadata/state updates)
- All debugging via file logs (
~/.happy-dev/logs/) - Prevents interference with agent terminal UI
logger.debug()for standard logslogger.debugLargeJson()for objects (auto-truncates)
- Zod schemas for runtime validation (
UserMessageSchema, etc.) - Explicit TypeScript types throughout
- No
anytypes except in MCP SDK interfaces
- Promise-based async/await throughout
- Lock mechanisms for concurrent state updates (
AsyncLock) - Fire-and-forget for non-critical operations (notifications)
- Unit tests using Vitest
- Tests colocated with source (
.test.ts) - No mocking - tests make real API calls
- Test authentication flow, encryption, session management
npm testnpm run build
# Uses pkgroll to bundle TypeScript
# Output: dist/ directory- CLI:
bin/happy.mjs - Daemon: Spawns via
src/daemon/run.ts - Codex:
src/codex/runCodex.ts - Claude:
src/claude/runClaude.ts
- Upstream MCP SDK Fix: Submit PR to always hide windows on Windows
- Heartbeat Implementation: Monitor agent subprocess health
- Retry Logic: Exponential backoff for failed agent connections
- Timeout Configuration: Configurable timeouts for all operations
- Connection Pooling: Reuse WebSocket connections across sessions
- Structured Logging: JSON-formatted logs for better parsing
DEBUG=1 happy codextail -f ~/.happy-dev/logs/$(ls -t ~/.happy-dev/logs/ | head -1)happy daemon status# Find process
ps aux | grep codex
# Kill by PID
kill -9 <PID>