REST API
The Gretl daemon exposes a JSON HTTP API on http://127.0.0.1:11611.
Every CLI and SDK command goes through it. Bind address and port are configurable in
~/.gr/config.toml; loopback only by default.
Conventions
- All requests and responses are JSON.
Content-Type: application/json. - Names are URL-safe. URL-encode anything weird, but stick to
[a-z0-9-_]in practice. - Timestamps are RFC 3339 with timezone offset. Durations are milliseconds.
- The daemon never accepts requests from non-loopback origins. CORS is locked to
chrome-extension://*for the official extension.
Authentication
By default the API is unauthenticated — it only listens on loopback. If you set auth.token in
~/.gr/config.toml, every request must carry it as Authorization: Bearer <token>.
Services
Returns every registered service, regardless of state.
Response 200
{
"services": [
{
"name": "jobs-server",
"port": 7401,
"group": "@jobs",
"status": "running",
"pid": 38219,
"url": "http://localhost:7401",
"startedAt": "2026-04-30T09:42:11+02:00",
"cpu": 0.051,
"rssMB": 142
}
]
}
Response 200
{
"name": "jobs-server",
"port": 7401,
"status": "running",
...
}
Errors
404 unknown_service — no service registered under that name.
Creates or updates a service. Idempotent on name.
Request body
{
"name": "jobs-server",
"port": 7401,
"cmd": "npm run dev:server",
"cwd": "/Users/d/dev/jobs",
"group": "@jobs",
"ready": "http://localhost:7401/health",
"env": { "NODE_ENV": "development" }
}
Response 201
{
"name": "jobs-server",
"status": "stopped",
...
}
Returns once the ready-check passes (or 502 if it doesn't within ?timeout=ms, default 30000).
Response 200
{ "name": "jobs-server", "status": "running", "pid": 38219 }
Sends SIGTERM, then SIGKILL after ?grace=ms (default 5000).
Stops the service if running, then forgets about it.
Resolution
The hot path used by SDKs. Returns the URL even if the service is stopped (so you can decide what to do).
Response 200
{
"name": "jobs-server",
"url": "http://localhost:7401",
"port": 7401,
"status": "running"
}
Groups
{
"groups": [
{ "name": "@jobs", "services": 3, "running": 3 },
{ "name": "@data", "services": 2, "running": 2 }
]
}
Brings up every service in the group in dependsOn order. Returns once all ready-checks pass.
Reverse-order stop.
Events (SSE)
Long-lived stream of lifecycle events. The desktop app, Chrome extension, and SDKs all consume this.
Event types
| type | Fields | When |
|---|---|---|
| service.registered | name, port, group | POST /v1/services |
| service.started | name, pid, url | Process is up & ready |
| service.stopped | name, exitCode | Clean exit or SIGTERM |
| service.crashed | name, exitCode, signal | Non-zero exit, no stop request |
| service.stats | name, cpu, rssMB | Every 2s for running services |
| port.conflict | port, claimedBy, holder | Two services want the same port |
Example frame
event: service.crashed id: 4429 data: { "name": "jobs-worker", "exitCode": 137, "signal": "SIGKILL" }
Replay
Pass Last-Event-ID to replay events you missed during a reconnect; the daemon retains the last
1024.
Kubernetes
All K8s endpoints are on the cloud API at https://api.gretl.dev/k8s. Authenticate with Authorization: Bearer <GR_TOKEN>.
Response 200
[{
"id": "abc123...",
"name": "prod-eks",
"provider": "eks",
"in_cluster": false,
"created_at": "2026-05-01T10:00:00Z"
}]
Request body
{
"name": "prod-eks",
"provider": "eks",
"kubeconfig": "<base64-or-raw-kubeconfig>",
"in_cluster": false
}
Response 201
{
"id": "abc123...",
"name": "prod-eks"
}
Pass in_cluster: true (no kubeconfig needed) when running the Gretl agent inside the same cluster as a pod.
Discovers Deployments and StatefulSets in all namespaces and upserts them into Gretl. Returns the count synced.
Response 200
{ "synced": 14 }
Returns all workloads across every registered cluster. Filter by cluster with ?cluster_id=.
Response 200
[{
"id": "def456...",
"name": "langgraph-agent",
"namespace": "agents",
"kind": "Deployment",
"replicas": 0,
"desired_replicas": 2,
"auto_sleep_enabled": true,
"idle_timeout_minutes": 30,
"last_activity_at": "2026-05-17T23:11:00Z"
}]
Saves desired_replicas then patches the Deployment/StatefulSet to 0. Idempotent.
Response 200
{ "replicas": 0 }
Scales back to desired_replicas (or 1 if never set).
Response 200
{ "replicas": 2 }
Creates a tether record and returns the tether ID. Call /activate to open the TCP proxy.
Request body
{
"container_port": 8000,
"label": "HTTP API"
}
Response 201
{
"tether_id": "ghi789...",
"local_port": 21042
}
Destroys open sockets, closes the TCP listener, and releases the local port back to the pool.
Response 200
{ "ok": true }
PATCH body
{
"auto_sleep_enabled": true,
"idle_timeout_minutes": 30
}
Response 200
{
"auto_sleep_enabled": true,
"idle_timeout_minutes": 30
}
The auto-sleep sweeper runs every 10 minutes. Activity is tracked via tether connections — each new TCP connection to the port-forward updates last_activity_at.
Errors
Errors are JSON with a stable code and human-readable message:
{
"code": "port_in_use",
"message": "port 7401 is held by pid 12044 (chrome)",
"hint": "run 'gr adopt 7401 --as <name>' or 'gr kill :7401'"
}
| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_spec | Missing required field, or invalid shape. |
| 401 | unauthorized | Auth token missing or wrong. |
| 404 | unknown_service | No service registered with that name. |
| 409 | port_in_use | The port is held by another process. |
| 409 | already_running | Service is already in running state. |
| 502 | ready_timeout | Process started but ready-check didn't pass. |
| 503 | daemon_busy | Daemon is doing a long-running op; retry shortly. |