Files
ai-agent/sidecar/README.md
Gabriel Vidal 3f5c09f67b refactor: adapt to standalone-repo layout
The repo now lives outside the homelab monorepo. Drop the homelab-specific
deploy files from HEAD (docker-compose.yml, traefik.yml, deploy.sh,
deploy.override.yml — canonical copies live in the homelab's
services/ai-agent shim; history keeps these as reference), fix stale
services/ai-agent path references, and make sidecar/install.sh resolve its
.env (SIDECAR_ENV_FILE > repo .env > ~/homelab/.env) and SIDECAR_CWD instead
of assuming the monorepo nesting. Mark the GOAL.md repo-split as done.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 13:23:30 +02:00

83 lines
3.4 KiB
Markdown

# claude-sidecar
A tiny **host-side** FastAPI wrapper that launches `claude -p` sessions on behalf
of the [ai-agent](../) viewer.
## Why a host process (not a container)
The ai-agent viewer runs in Docker and can't reach the host's `claude` CLI — its
auth (`~/.claude/.credentials.json`), RTK/vault hooks, and skills all live in the
host user's home. This sidecar runs natively on the host **as the repo owner**, so
a session it spawns is exactly like one started from a terminal.
## Flow
```
Frontend (sticky input)
│ POST /api/spawn {prompt}
ai-agent backend (container)
│ generates a session UUID, POST /spawn to the sidecar
▼ http://host.docker.internal:8790/spawn (Bearer SIDECAR_TOKEN)
claude-sidecar (host)
└─ claude -p <prompt> --session-id <uuid> --output-format json \
--permission-mode bypassPermissions --model opus \
--remote-control (detached)
```
`--remote-control` is added by default so every spawned run registers with
Claude Code Remote Control and shows up in the Claude app — you can watch and
drive the conversation from your phone. Set `SIDECAR_REMOTE_CONTROL=0` to opt
out.
Claude writes its transcript to
`~/.claude/projects/-home-gabrielvidal-homelab/<uuid>.jsonl`, which the ai-agent
container already watches read-only. The new conversation appears in the viewer
within ~2s and the frontend redirects to it once it has synced.
The conversation view has a matching sticky composer that continues an existing
thread: it POSTs `/api/spawn`'s sibling `/api/resume`, which the backend proxies
to `POST /resume` here — `claude -p <prompt> --resume <sessionId>` in the
session's original cwd. Because resume reuses the session id, the new turns
append to the same transcript and stream straight into the open conversation.
## Install (host)
```bash
sidecar/install.sh
```
Creates a venv, writes a systemd **user** unit (`~/.config/systemd/user/claude-sidecar.service`),
and starts it. Reboot survival needs `sudo loginctl enable-linger $USER`.
Config comes from the repo `.env`: `SIDECAR_TOKEN`, `SIDECAR_PORT` (default 8790).
The ai-agent container reads `SIDECAR_URL` + `SIDECAR_TOKEN` (see its
`docker-compose.yml`, which also adds `host.docker.internal:host-gateway`).
## Endpoints
- `GET /health``{ok, cwd, claude}`
- `POST /spawn` (Bearer auth) `{prompt, sessionId?, model?, cwd?}`
`{sessionId, pid, log}` — spawns detached, returns immediately.
- `POST /resume` (Bearer auth) `{sessionId, prompt, model?, cwd?}`
`{sessionId, pid, log}` — continues an existing conversation by running
`claude -p <prompt> --resume <sessionId>`. Reuses the original session id, so
the new turns append to the same transcript and stream into the open
conversation. `--resume` is directory-scoped, so pass the session's original
`cwd`.
- `POST /interrupt` (Bearer auth) `{sessionId}``{sessionId, pid, signal, ok}`
— sends the run's process group a `SIGINT` (like Ctrl+C), so Claude aborts the
turn, writes a `[Request interrupted by user]` marker and exits. `404` if no
live process is tracked for that session.
Per-session stdout/stderr is captured under `logs/<sessionId>.log`; the pid is
recorded in `logs/<sessionId>.pid` so `/interrupt` can find the run.
## Manage
```bash
systemctl --user status claude-sidecar
systemctl --user restart claude-sidecar
journalctl --user -u claude-sidecar -f
```