# clarc self-hosted remote control for claude code, pure bun stack. ## summary wrap claude cli in PTY, stream output to server, approve prompts from phone. ## tech stack - bun-pty (native PTY via FFI to Rust) - bun:sqlite (persistence) - Bun.serve() (HTTP + WebSocket + SSE) - no frameworks ## project location `/home/jtm/projects/agentry/repos/clarc/` ## structure ``` clarc/ ├── src/ │ ├── cli.ts # PTY wrapper, connects to server │ ├── server.ts # HTTP + WebSocket + SSE │ ├── db.ts # SQLite schema + queries │ ├── auth.ts # HMAC signing │ └── types.ts # shared types ├── public/ │ └── index.html # mobile dashboard ├── docs/ │ └── plan.md # this plan (moved here) ├── CLAUDE.md ├── package.json ├── Dockerfile ├── compose.yml └── justfile ``` ## sqlite schema ```sql CREATE TABLE devices ( id INTEGER PRIMARY KEY, secret TEXT NOT NULL UNIQUE, name TEXT, created_at INTEGER NOT NULL, last_seen INTEGER NOT NULL ); CREATE TABLE sessions ( id INTEGER PRIMARY KEY, device_id INTEGER NOT NULL, started_at INTEGER NOT NULL, ended_at INTEGER, cwd TEXT, command TEXT ); CREATE TABLE prompts ( id INTEGER PRIMARY KEY, session_id INTEGER NOT NULL, created_at INTEGER NOT NULL, prompt_text TEXT NOT NULL, response TEXT, responded_at INTEGER ); CREATE TABLE output_log ( id INTEGER PRIMARY KEY, session_id INTEGER NOT NULL, timestamp INTEGER NOT NULL, line TEXT NOT NULL ); ``` ## api REST: - `GET /` - dashboard - `GET /api/sessions` - list sessions - `POST /api/prompts/:id/approve` - approve - `POST /api/prompts/:id/reject` - reject WebSocket `/ws` (CLI <-> Server): - cli sends: auth, output, resize, exit - server sends: input, resize, ping SSE `/events` (Server -> Dashboard): - session_start, session_end, output, prompt, prompt_response ## implementation phases ### phase 1: foundation (~300 lines) 1. create project structure 2. `src/types.ts` - message types 3. `src/db.ts` - sqlite schema + queries 4. `src/auth.ts` - device secret + HMAC ### phase 2: server (~250 lines) 5. `src/server.ts` - Bun.serve with routes 6. WebSocket handler for CLI 7. SSE endpoint for viewers 8. REST endpoints for approval ### phase 3: cli wrapper (~150 lines) 9. `src/cli.ts` - spawn claude with bun-pty 10. WebSocket client to server 11. forward PTY output, handle resize ### phase 4: dashboard (~300 lines) 12. `public/index.html` - mobile-first 13. SSE listener 14. terminal output display 15. approve/reject buttons ### phase 5: deployment (~100 lines) 16. Dockerfile (oven/bun) 17. compose.yml with volume 18. justfile commands 19. error handling + reconnection ## key decisions - bun-pty over node-pty: pure FFI, pre-built binaries - SQLite over in-memory: persistence across restarts - SSE over WebSocket for dashboard: simpler, auto-reconnect - HMAC over TLS certs: simpler deployment - plain text output over xterm.js: mobile-friendly, simpler ## verification 1. `bun run src/cli.ts` - starts PTY wrapper 2. visit `http://localhost:7200` - see dashboard 3. type in CLI - output appears in dashboard 4. trigger approval prompt - approve from dashboard 5. `docker compose up` - deploys successfully ## dependencies ```json { "dependencies": { "bun-pty": "^0.4.8" } } ``` that's it. one external dep. ## CLAUDE.md notes - if bun-pty needs changes, fork and contribute upstream - use bun:sqlite, not better-sqlite3 - no frameworks (no express, no hono) - target ~1000-1500 lines total