dv serve adds an HTTP API server to dv, exposing the full CLI surface over
JSON + Server-Sent Events. The primary consumer is Jarvis (running in a Docker
container), which reaches the host via host.docker.internal or the Docker
bridge gateway. Any HTTP client works — curl, scripts, future web UIs.
Design principles:
- Full CLI parity — every non-interactive command is reachable
- Long-running operations stream via SSE so callers see live output
- One auth primitive: bearer token (no sessions, no cookies)
- Config state lives on the host as normal —
dv servereads/writes~/.config/dv/config.jsonthe same way the CLI does enterand other TTY commands are intentionally excluded — they require an interactive terminal and have no meaningful async equivalent
dv serve [--port PORT] [--host HOST] [--token TOKEN]
| Flag | Default | Description |
|---|---|---|
--port |
7373 |
TCP port to listen on |
--host |
127.0.0.1 |
Bind address. Set 0.0.0.0 to allow Docker bridge access |
--token |
(auto) | Bearer token. Auto-generated and saved to config on first run if omitted |
On first run with no --token, dv serve generates a random 32-byte hex token,
writes it to ~/.config/dv/config.json as serveToken, and prints it once.
Subsequent runs read it from config automatically.
For Docker container access, the recommended invocation is:
dv serve --host 0.0.0.0The Jarvis skill stores the token in a memory fragment and calls
http://host.docker.internal:7373.
Every request must include:
Authorization: Bearer <token>
Missing or invalid token → 401 Unauthorized.
Success (non-streaming): 200 OK, Content-Type: application/json
{ "ok": true, "data": { ... } }Error: 4xx / 5xx, Content-Type: application/json
{ "ok": false, "error": "human-readable message" }Streaming: 200 OK, Content-Type: text/event-stream
SSE events:
event: output
data: {"stream":"stdout","text":"Building image...\n"}
event: output
data: {"stream":"stderr","text":"warning: ...\n"}
event: done
data: {"exit_code":0}
event: error
data: {"error":"container not running"}
The done event is always the final event. Clients should read until the
connection closes or done is received. On error mid-stream, an error event
is sent followed by connection close (no done).
Health check and version info.
Response:
{
"ok": true,
"data": {
"version": "0.1.0",
"selected_container": "ai_agent",
"selected_image": "discourse"
}
}List all containers belonging to the selected image. Equivalent to dv list.
Query params:
sessions=true— include active exec session counts (slower, matchesdv list --sessions)
Response:
{
"ok": true,
"data": {
"containers": [
{
"name": "ai_agent",
"status": "Running",
"time": "2 hours",
"image": "ai_agent",
"urls": ["http://localhost:4200"],
"selected": true,
"sessions": 2
}
],
"selected": "ai_agent"
}
}Create and start a new container. Equivalent to dv new (creates with name) or
dv start (creates default).
Request body:
{
"name": "my-agent",
"image": "discourse",
"host_starting_port": 4201,
"container_port": 4200,
"reset": false
}All fields optional. name defaults to currentAgentName(cfg).
Streaming — streams build progress and startup output as SSE output events,
then a done event with {"exit_code": 0}.
Get state for a single container.
Response:
{
"ok": true,
"data": {
"name": "ai_agent",
"status": "Running",
"time": "2 hours",
"image": "ai_agent",
"urls": ["http://localhost:4200"],
"selected": true
}
}404 if container doesn't exist.
Start an existing stopped container. Equivalent to dv start <name>.
Request body: (empty or {})
reset: bool— stop, remove, and recreate before starting
Streaming — SSE output events then done.
Stop a running container. Equivalent to dv stop.
Streaming.
Restart a container. Equivalent to dv restart.
Streaming.
Restart Discourse (unicorn/rails) inside the container without restarting the
container itself. Equivalent to dv restart discourse.
Streaming.
Remove a container. Equivalent to dv remove.
Request body:
{
"remove_image": false,
"force": false
}Response: 200 { "ok": true } on success, 409 if container has active
sessions and force is false.
Rename a container. Equivalent to dv rename <old> <new>.
Request body:
{ "new_name": "better-agent" }Response: 200 { "ok": true }.
Set this container as the selected agent. Equivalent to dv select <name>.
Updates global config (SelectedAgent). Note: session-local state (pid-based)
only applies to the host terminal — remote callers set global config only.
Response: 200 { "ok": true }.
Build a Docker image. Equivalent to dv build [TARGET].
Request body:
{
"target": "discourse",
"no_cache": false,
"build_args": ["KEY=VALUE"],
"tag": "",
"classic_build": false,
"builder": "",
"rm_existing": false
}All fields optional. target defaults to cfg.SelectedImage.
Streaming — this is the main automation entry point for daily rebuilds.
Docker build output streams as SSE output events in real time. Final done
event includes exit_code.
Example daily automation use: Jarvis calls POST /images/build each morning,
streams output to a Telegram message, then calls POST /containers/ai_agent/start
with reset: true.
Pull a prebuilt image from Docker Hub. Equivalent to dv pull.
Request body:
{
"image_name": "discourse",
"tag": "",
"rm_existing": false
}Streaming.
List configured images from cfg.Images.
Response:
{
"ok": true,
"data": {
"images": [
{
"name": "discourse",
"tag": "ai_agent",
"kind": "discourse",
"workdir": "/var/www/discourse",
"selected": true
}
],
"selected": "discourse"
}
}Run an arbitrary command inside the container as discourse. Equivalent to
dv run -- <cmd>.
Request body:
{
"cmd": "bundle exec rails db:migrate",
"workdir": "/var/www/discourse",
"as_root": false,
"env": { "RAILS_ENV": "development" }
}Streaming — stdout/stderr interleaved as SSE output events.
Run an AI agent inside the container with a prompt. Equivalent to dv ra <agent> <prompt>. This is the primary Jarvis automation endpoint.
Request body:
{
"agent": "claude",
"prompt": "Fix the N+1 query in TopicQuery#latest",
"raw_args": []
}raw_args maps to the -- ARGS... passthrough (for flags/interactive modes).
Omit prompt and provide raw_args: [] to get interactive mode — though
interactive mode is only useful if the client implements a terminal bridge
(not the intended use for Jarvis).
Streaming. Agent runs can take minutes. Keep the SSE connection open.
The done event includes exit_code.
Extract container changes to a local host repo. Equivalent to dv extract.
Request body:
{
"path": "",
"dir": "",
"sync": false
}path — optional container path to extract (absolute or relative to workdir).
dir — override local repo destination path on host.
sync — start a sync watcher (will stream continuously until client disconnects).
Streaming — git clone/reset/copy progress, then done.
Note: sync mode streams indefinitely. The connection stays open and
output events arrive when files change. Client disconnect stops the watcher.
Extract a specific plugin. Equivalent to dv extract plugin <name>.
Request body:
{ "plugin_name": "discourse-ai" }Streaming.
Extract a specific theme. Equivalent to dv extract theme <name>.
Request body:
{ "theme_name": "my-theme" }Streaming.
Import a local host repo back into the container. Equivalent to dv import.
Request body:
{
"repo_path": "/home/sam/discourse_src",
"base": "main"
}Streaming.
Check out a Discourse branch in the container and reset/migrate. Equivalent to
dv branch <branch>.
Request body:
{
"branch": "feature/my-fix",
"no_reset": false,
"new": false
}new: true creates the branch if it doesn't exist on the remote.
Streaming.
Check out a GitHub PR in the container. Equivalent to dv pr <number>.
Request body:
{
"number": 12345,
"no_reset": false
}Streaming.
Rebase the container's branch onto origin/main and run migrations. Equivalent
to dv catchup.
Request body: {}
Streaming.
Full Discourse reset (db:reset + migrate + seed). Equivalent to dv reset.
Request body:
{ "discourse_reset": false }discourse_reset: true runs dv discourse-reset (deeper wipe, reseeds from
scratch) rather than the standard db:reset.
Streaming. This is slow — can take several minutes.
Fetch recent development emails (MailHog or letter_opener). Equivalent to dv mail.
Response:
{
"ok": true,
"data": {
"messages": [
{
"from": "...",
"to": "...",
"subject": "...",
"body": "..."
}
]
}
}Show active exec sessions in a container. Equivalent to dv ps.
Response:
{
"ok": true,
"data": {
"sessions": [
{ "pid": 1234, "command": "ruby", "user": "discourse", "cpu": "2.1", "mem": "3.2" }
]
}
}Update all AI agents inside the container (dv update agents). Runs npm/curl
installs for codex, gemini, claude, aider, cursor, etc.
Streaming. Long — expect 2-5 minutes.
Update Discourse inside the container (dv update discourse). Pulls latest
Dockerfile, rebuilds.
Streaming.
Upgrade the dv binary itself on the host (dv upgrade).
Streaming.
Copy files between host and container. Equivalent to dv cp.
Request body:
{
"direction": "host_to_container",
"host_path": "/home/sam/myfile.rb",
"container_path": "/var/www/discourse/myfile.rb"
}direction is "host_to_container" or "container_to_host".
Response: 200 { "ok": true } on success. Non-streaming (files are small).
Return full config JSON. Equivalent to dv config show.
Response:
{
"ok": true,
"data": { ...Config struct... }
}Set one config key. Equivalent to dv config set KEY VALUE.
Request body:
{ "key": "discourseRepo", "value": "https://github.com/myorg/discourse.git" }Valid keys: imageTag, defaultContainerName, workdir, hostStartingPort,
containerPort, selectedAgent, discourseRepo, extractBranchPrefix.
Response: 200 { "ok": true }.
Stream live container logs (stdout/stderr of the container process, not
Discourse app logs). Optional since query param (duration, e.g. "5m").
Streaming — SSE output events, streams until client disconnects.
Stream the Discourse unicorn (Rails) log. Equivalent to tailing
/var/www/discourse/log/unicorn.log inside the container.
Query params:
lines=50— how many historical lines to include before streaming (default 50)
Streaming — SSE output events, streams until client disconnects.
Stream the ember-cli log (log/ember-cli.log). Same params as unicorn.
Streaming.
Use os/exec with cmd.StdoutPipe() + cmd.StderrPipe(), multiplex both to
the SSE writer in goroutines. SSE flush after each event. The HTTP handler holds
the response open until the command exits, then sends done.
For docker.ExecInteractive-style commands (which currently pipe directly to
os.Stdout/os.Stderr), a new docker.ExecStream(name, workdir, envs, argv, stdout, stderr io.Writer) variant is needed that writes to arbitrary writers
rather than os.Stdout. This is the main new primitive required.
No artificial serialisation. Multiple streaming requests can run concurrently —
callers are responsible for not starting two builds at once if that matters to
them. A future /jobs endpoint could track active operations but that's
out of scope for v1.
Send a comment (: keep-alive\n\n) every 15 seconds on idle streaming
connections to prevent proxy/load balancer timeouts. Relevant for long build
and update agents runs.
dv serve uses the same xdg.ConfigDir() as all other commands — typically
~/.config/dv. No special server config file.
All implementation lives in internal/cli/serve.go + registered in root.go.
No other files need modification except adding docker.ExecStream to
internal/docker/docker.go.
Once the server is running, the Jarvis dv skill provides:
Base URL: http://host.docker.internal:7373
Token: <stored in memory fragment>
Key operations Jarvis can automate:
| Task | Endpoint |
|---|---|
| Daily fresh build | POST /images/build → POST /containers/ai_agent/start (reset) |
| Run an agent task | POST /containers/ai_agent/run-agent |
| Check what's running | GET /containers |
| Extract and open PR | POST /containers/ai_agent/extract → host-side git/gh |
| Watch for errors | GET /containers/ai_agent/logs/unicorn |
| Update all agents | POST /containers/ai_agent/update/agents |