- JavaScript 78.8%
- Shell 13.3%
- Dockerfile 7.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| defaults | ||
| .gitignore | ||
| build-stats.md | ||
| CHANGELOG.md | ||
| CloudronManifest.json | ||
| CloudronVersions.json | ||
| configure.js | ||
| DESCRIPTION.md | ||
| Dockerfile | ||
| LICENSE | ||
| logo.png | ||
| POSTINSTALL.md | ||
| README.md | ||
| server.js | ||
| start.sh | ||
| version.md | ||
opencode-worker for Cloudron
A lean, headless execution engine for the opencode CLI. Peer apps dispatch AI coding tasks over an authenticated HTTPS API; the worker runs each task as an opencode child process and streams output to a per-task log file.
Inspiration from Marcus Quinn https://aidevops.sh gratefully acknowledged.
API
All endpoints except /health require Authorization: Bearer <AUTH_TOKEN>.
| Method | Endpoint | Body | Description |
|---|---|---|---|
GET |
/health |
— | App name, status message, version and uptime |
GET |
/guide |
— | Consumer how-to guide, read live from /app/data/config/consumer-guide.md |
POST |
/dispatch |
{"prompt":"...", "repo":"...", "branch":"...", "model":"..."} |
Start a task, returns {"taskId":"...","status":"dispatched"} |
GET |
/status |
— | Active task count, concurrency limit and memory usage |
GET |
/logs/:id |
— | Current text content of the task log |
DELETE |
/logs/:id |
— | Delete the task log |
repo may be an owner/name GitHub-style reference (cloned into /app/data/workspace/<name>), a full git URL, or an absolute path that already exists in the container. Set GIT_HOST if the host is not github.com. branch is optional and checks that branch out (fresh and reused clones alike) before the task runs; omitted, the repo's default branch is used. model is optional and uses the configured default when omitted.
Example: dispatch and read the log
curl -X POST https://<app-domain>/dispatch \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"repo":"myuser/myrepo","prompt":"Fix the login bug"}'
curl -H "Authorization: Bearer $AUTH_TOKEN" https://<app-domain>/logs/<taskId>
Example: non-destructive task
--auto is passed to opencode, so a read-only task relies on the prompt itself to forbid changes:
curl -X POST https://<app-domain>/dispatch \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"repo":"myuser/myrepo","prompt":"Inspect this repository in read-only mode. Do not create, edit, delete, move, commit, push, or otherwise modify any file, and do not run any command that changes repository or system state. Locate the project'"'"'s primary version file (e.g. package.json, VERSION, pyproject.toml, composer.json, Cargo.toml, go.mod) and report: 1. the project name, 2. the declared version, 3. the path to the version file, 4. the language/runtime and any declared dependencies. If no version file exists, say so explicitly. Output a short summary only."}'
Retrieving results
There is no separate result endpoint: the result is the task log. On success it ends with [worker] Task Completed Successfully; on failure with [worker] Task Failed: <reason>. opencode's own output is streamed into the same file live.
# Dispatch and capture the task id
TASK_ID=$(curl -s -X POST https://<app-domain>/dispatch \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"repo":"myuser/myrepo","prompt":"Fix the login bug"}' | jq -r .taskId)
# Read the log at any time (including while the task is still running)
curl -s -H "Authorization: Bearer $AUTH_TOKEN" "https://<app-domain>/logs/$TASK_ID"
# Block until the task finishes (success or failure), then print the log
while true; do
LOG=$(curl -s -H "Authorization: Bearer $AUTH_TOKEN" "https://<app-domain>/logs/$TASK_ID")
echo "$LOG" | grep -qE "Task (Completed Successfully|Failed)" && { echo "$LOG"; break; }
sleep 5
done
# Check what is running right now
curl -s -H "Authorization: Bearer $AUTH_TOKEN" https://<app-domain>/status | jq .
# Tidy up once done
curl -s -X DELETE -H "Authorization: Bearer $AUTH_TOKEN" "https://<app-domain>/logs/$TASK_ID"
Configuration
All settings live in /app/data/config/worker.env. Edit the file and restart the app.
| Key | Purpose |
|---|---|
AUTH_TOKEN |
Required bearer token for API access (auto-generated on first run if blank) |
MAX_CONCURRENT |
Maximum simultaneous opencode processes (default 2) |
COMPAT_BASE_URL |
Base URL of an OpenAI v1-compatible endpoint, including /v1 |
COMPAT_API_KEY |
API key for that endpoint |
COMPAT_MODEL |
Default model name exposed by that endpoint |
DEFAULT_MODEL |
Default provider/model for native providers (e.g. anthropic/claude-sonnet-4-5) |
ANTHROPIC_API_KEY |
Native provider credentials (passed through to opencode) |
GIT_TOKEN |
Token used when cloning private repositories (read-only scope is enough) |
GIT_HOST |
Host used to resolve owner/name references (default github.com) |
Generating an AUTH_TOKEN
Leave AUTH_TOKEN blank and the worker creates one on first start, writes it back to worker.env, and logs that it did so. To generate one yourself:
head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n'
OpenAI v1-compatible endpoints
Setting COMPAT_BASE_URL, COMPAT_API_KEY and COMPAT_MODEL registers a compat provider (via @ai-sdk/openai-compatible, since these endpoints are not necessarily OpenAI) and makes compat/<COMPAT_MODEL> the default model. This works with OpenAI, OpenRouter, LiteLLM, vLLM, Ollama, LM Studio and other v1-compatible servers.
A dispatch request can override the model per task with any provider/model string, for example:
{ "repo": "myuser/myrepo", "prompt": "Add tests", "model": "compat/qwen2.5-coder" }
Storage
/app/data/workspace/— cloned repositories/app/data/logs/— task logs/app/data/config/— the app's own config:worker.envandconsumer-guide.md/app/data/.opencode/— everything opencode owns, kept hidden and separate: its generatedconfig/(includingopencode.jsonand the singlenode_modulesfor provider/plugin packages),data/(sessions DB, logs),cache/,state/andhome/
Consumer guide
A how-to document for peer apps that consume this service is served at GET /guide (requires AUTH_TOKEN). It is read from disk on every request, so /app/data/config/consumer-guide.md can be edited on the server without rebuilding or restarting the app. The shipped default lives at defaults/consumer-guide.md.
Documentation
Packaged by @timconsidine https://communityapps.appx.uk (full portfolio: https://myca.appx.uk)