157 lines
3.6 KiB
Markdown
157 lines
3.6 KiB
Markdown
# 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
|