Server mode
larik serve exposes the current directory's agent over a local HTTP API. Each session streams its events over Server-Sent Events (SSE), so editors, web UIs and scripts can drive Larik. One server can run several sessions at once. They share MCP connections, language servers and the sandbox.
./larik serve # 127.0.0.1:4096
./larik serve --addr 127.0.0.1:0 # pick a free port
./larik serve --model ollama/qwen3-coder --mode accept-edits # defaults for new sessions
On startup the server prints one JSON line to stdout, {"url": "...", "token": "..."}, for programs that launch it.
Auth and safety:
- Every request except
GET /v1/healthneedsAuthorization: Bearer <token>. - The token comes from
--token, then$LARIK_SERVER_TOKEN, and otherwise is generated at random. GET …/eventsalso accepts?token=, because browserEventSourcecan't set headers.- The server only listens on loopback. Requests whose
Hostisn't a loopback name are rejected, which blocks DNS rebinding. --allow-remotelifts both restrictions. Anyone who can reach the port and has the token can then run commands.
T=<token>; U=http://127.0.0.1:4096
ID=$(curl -s -XPOST $U/v1/sessions -H "Authorization: Bearer $T" -d '{}' | jq -r .id)
curl -N "$U/v1/sessions/$ID/events?token=$T" & # live events
curl -s -XPOST $U/v1/sessions/$ID/prompt -H "Authorization: Bearer $T" \
-d '{"text":"summarize this repo","wait":true}' # blocks, returns the answer
| Endpoint | Purpose |
|---|---|
GET /v1/health |
Liveness check (no auth) |
GET /v1/info |
Version, cwd, providers, sandbox |
GET /v1/sessions |
Sessions in this directory, with loaded/busy flags |
POST /v1/sessions |
New session {model, effort, mode}, or load one with {resume: id} / {continue: true} |
GET /v1/sessions/{id} |
Model, mode, busy, usage, pending permissions, running tasks |
PATCH /v1/sessions/{id} |
Change model, effort or mode |
DELETE /v1/sessions/{id} |
Stop and unload (the transcript stays on disk) |
GET /v1/sessions/{id}/messages |
Full transcript (works for unloaded sessions too) |
POST /v1/sessions/{id}/fork |
Branch into a new loaded session. {at: i} keeps the messages before index i (which must be a prompt) and returns that prompt's text; with no at, everything is kept |
GET /v1/sessions/{id}/events |
SSE event stream |
POST /v1/sessions/{id}/prompt |
{text} starts a run and returns 202 (409 if busy); {text, wait: true} returns the final answer |
POST /v1/sessions/{id}/cancel |
Interrupt the current run |
GET /v1/sessions/{id}/permissions |
Pending permission requests |
POST /v1/sessions/{id}/permissions/{request_id} |
Answer: {allow, always, reason}. An always answer returns {allowed, persisted} and an error if saving failed; other answers return 204. |
POST /v1/sessions/{id}/compact · /undo · /clear |
Same as the TUI commands |
GET /v1/sessions/{id}/tasks · DELETE …/tasks/{task_id} |
List or stop background tasks |
The event stream:
- Each SSE message has
id: <seq>andevent: <type>. Itsdatais JSON: the agent event (the same schema as-p --output json) plusseq,session, andrequest_idon permission requests. - The server adds four event types:
status(busytrue/false)user_messagepermission_resolved(allowed,denied, orexpiredwhen the run was cancelled)session_closed
- Reconnect with
Last-Event-ID(or?after=<seq>) to replay missed events from a 4096-event buffer. Without it, a stream starts with new events only. - A run's permission requests wait until some client answers them or the run is cancelled.
- When background tasks finish while a session is idle, the server starts a turn by itself to hand their results to the model.