Happy CLI (handy-cli) is a command-line tool that wraps Claude Code to enable remote control and session sharing. It's part of a three-component system:
- handy-cli (this project) - CLI wrapper for Claude Code
- handy - React Native mobile client
- handy-server - Node.js server with Prisma (hosted at https://api.happy-servers.com/)
- Strict typing: No untyped code ("I despise untyped code")
- Clean function signatures: Explicit parameter and return types
- As little as possible classes
- Comprehensive JSDoc comments: Each file includes header comments explaining responsibilities.
- Import style: Uses
@/alias for src imports, e.g.,import { logger } from '@/ui/logger' - File extensions: Uses
.tsfor TypeScript files - Export style: Named exports preferred, with occasional default exports for main functions
- Create stupid small functions / getters / setters
- Excessive use of
ifstatements - especially if you can avoid control flow changes with a better design - NEVER import modules mid-code - ALL imports must be at the top of the file
- Graceful error handling with proper error messages
- Use of
try-catchblocks with specific error logging - Abort controllers for cancellable operations
- Careful handling of process lifecycle and cleanup
- Unit tests using Vitest
- No mocking - tests make real API calls
- Test files colocated with source files (
.test.ts) - Descriptive test names and proper async handling
- All debugging through file logs to avoid disturbing Claude sessions
- Console output only for user-facing messages
- Special handling for large JSON objects with truncation
Handles server communication and encryption.
api.ts: Main API client class for session managementapiSession.ts: WebSocket-based real-time session client with RPC supportauth.ts: Authentication flow using TweetNaCl for cryptographic signaturesencryption.ts: End-to-end encryption utilities using TweetNaCltypes.ts: Zod schemas for type-safe API communication
Key Features:
- End-to-end encryption for all communications
- Socket.IO for real-time messaging
- Optimistic concurrency control for state updates
- RPC handler registration for remote procedure calls
Core Claude Code integration layer.
-
loop.ts: Main control loop managing interactive/remote modes -
types.ts: Claude message type definitions with parsers -
claudeSdk.ts: Direct SDK integration using@anthropic-ai/claude-code -
interactive.ts: LIKELY WILL BE DEPRECATED in favor of running through SDK PTY-based interactive Claude sessions -
watcher.ts: File system watcher for Claude session files (for interactive mode snooping) -
mcp/startPermissionServer.ts: MCP (Model Context Protocol) permission server
Key Features:
- Dual mode operation: interactive (terminal) and remote (mobile control)
- Session persistence and resumption
- Real-time message streaming
- Permission intercepting via MCP [Permission checking not implemented yet]
User interface components.
logger.ts: Centralized logging system with file outputqrcode.ts: QR code generation for mobile authenticationstart.ts: Main application startup and orchestration
Key Features:
- Clean console UI with chalk styling
- QR code display for easy mobile connection
- Graceful mode switching between interactive and remote
index.ts: CLI entry point with argument parsingpersistence.ts: Local storage for settings and keysutils/time.ts: Exponential backoff utilities
-
Authentication:
- Generate/load secret key → Create signature challenge → Get auth token
-
Session Creation:
- Create encrypted session with server → Establish WebSocket connection
-
Message Flow:
- Interactive mode: User input → PTY → Claude → File watcher → Server
- Remote mode: Mobile app → Server → Claude SDK → Server → Mobile app
-
Permission Handling:
- Claude requests permission → MCP server intercepts → Sends to mobile → Mobile responds → MCP approves/denies
- File-based logging: Prevents interference with Claude's terminal UI
- Dual Claude integration: Process spawning for interactive, SDK for remote
- End-to-end encryption: All data encrypted before leaving the device
- Session persistence: Allows resuming sessions across restarts
- Optimistic concurrency: Handles distributed state updates gracefully
- Private keys stored in
~/.handy/access.keywith restricted permissions - All communications encrypted using TweetNaCl
- Challenge-response authentication prevents replay attacks
- Session isolation through unique session IDs
- Core: Node.js, TypeScript
- Claude:
@anthropic-ai/claude-codeSDK - Networking: Socket.IO client, Axios
- Crypto: TweetNaCl
- Terminal: node-pty, chalk, qrcode-terminal
- Validation: Zod
- Testing: Vitest
# From the happy-cli directory:
./bin/happy.mjs daemon start
# With custom server URL (for local development):
HAPPY_SERVER_URL=http://localhost:3005 ./bin/happy.mjs daemon start
# Stop the daemon:
./bin/happy.mjs daemon stop
# Check daemon status:
./bin/happy.mjs daemon status- Daemon logs are stored in
~/.happy-dev/logs/(or$HAPPY_HOME_DIR/logs/) - Named with format:
YYYY-MM-DD-HH-MM-SS-daemon.log
claude --print --output-format stream-json --verbose 'list files in this directory'- Original Session ID:
aada10c6-9299-4c45-abc4-91db9c0f935d - Created file:
~/.claude/projects/.../aada10c6-9299-4c45-abc4-91db9c0f935d.jsonl
claude --print --output-format stream-json --verbose --resume aada10c6-9299-4c45-abc4-91db9c0f935d 'what file did we just see?'- New Session ID:
1433467f-ff14-4292-b5b2-2aac77a808f0 - Created file:
~/.claude/projects/.../1433467f-ff14-4292-b5b2-2aac77a808f0.jsonl
- Creates a NEW session file with NEW session ID
- Original session file remains unchanged
- Two separate files exist after resumption
- The new session file contains the COMPLETE history from the original session
- History is prefixed at the beginning of the new file
- Includes a summary line at the very top
- CRITICAL FINDING: All historical messages have their sessionId field UPDATED to the new session ID
- Original messages from session
aada10c6-9299-4c45-abc4-91db9c0f935dnow showsessionId: "1433467f-ff14-4292-b5b2-2aac77a808f0" - This creates a unified session history under the new ID
Line 1: Summary of previous conversation
Lines 2-6: Complete history from original session (with updated session IDs)
Lines 7-8: New messages from current interaction
- Claude successfully maintains full context
- Can answer questions about previous interactions
- Behaves as if it's a continuous conversation
- Contains only messages from the original session
- All messages have original session ID
- Remains untouched after resume
{"type":"summary","summary":"Listing directory files in current location","leafUuid":"..."}
{"parentUuid":null,"sessionId":"1433467f-ff14-4292-b5b2-2aac77a808f0","message":{"role":"user","content":[{"type":"text","text":"list files in this directory"}]},...}
// ... all historical messages with NEW session ID ...
{"parentUuid":"...","sessionId":"1433467f-ff14-4292-b5b2-2aac77a808f0","message":{"role":"user","content":"what file did we just see?"},...}When using --resume:
- Must handle new session ID in responses
- Original session remains as historical record
- All context preserved but under new session identity
- Session ID in stream-json output will be the new one, not the resumed one