// Package main provides the entry point for the rdev API server. // // rdev (Remote Developer) provides a REST API for controlling Claude Code // instances running in Kubernetes pods. External clients (Discord bots, // CLI tools, etc.) connect via this API. // // Endpoints: // - GET /health - Health check // - GET /ready - Readiness check // - GET /docs - Scalar API documentation // - GET /openapi.json - OpenAPI 3.0 specification // - GET /projects - List available projects // - GET /projects/{id} - Get project details // - POST /projects/{id}/claude - Run Claude command // - POST /projects/{id}/shell - Run shell command // - POST /projects/{id}/git - Run git command // - GET /projects/{id}/events - SSE stream for output package main import ( "github.com/orchard9/rdev/internal/handlers" "github.com/orchard9/rdev/pkg/api" ) func main() { app := api.New("rdev-api", api.WithPort(8080)) // Initialize handlers projectsHandler := handlers.NewProjectsHandler() // Register routes projectsHandler.Mount(app.Router()) // Enable API documentation app.EnableDocs(buildOpenAPISpec()) app.Run() } // buildOpenAPISpec creates the OpenAPI specification for the rdev API. func buildOpenAPISpec() *api.OpenAPISpec { spec := api.NewOpenAPISpec("rdev API", "0.4.0"). WithDescription(`Remote Developer API for controlling Claude Code instances in Kubernetes. rdev runs Claude Code CLI in isolated pods, controlled via this REST API with SSE streaming. External clients (Discord bots, Slack bots, CLI tools) connect here to interact with projects. ## Architecture - **rdev-api**: This Go service (what you're looking at) - **claudebox pods**: Isolated pods running Claude Code CLI - **Projects**: Each project has its own claudebox pod with mounted workspace ## Authentication Authentication is planned for v0.6. Currently the API is internal-only. ## Streaming Command output is streamed via Server-Sent Events (SSE) at the /projects/{id}/events endpoint. `). WithServer("http://localhost:8080", "Local development"). WithServer("http://rdev-api.rdev.svc:8080", "Kubernetes internal") // Tags spec.WithTag("Projects", "Project management and discovery") spec.WithTag("Commands", "Command execution (claude, shell, git)") spec.WithTag("Events", "Server-Sent Events for real-time output") spec.WithTag("System", "Health and readiness endpoints") // System endpoints (auto-added by chassis, but document them) spec.AddPath("/health", "get", api.Op( "Health check", "Returns the health status of the rdev-api service", "System", )) spec.AddPath("/ready", "get", api.Op( "Readiness check", "Returns whether the service is ready to accept traffic", "System", )) // Projects - List and Get spec.AddPath("/projects", "get", withResponse( "List projects", "Returns all available projects (claudebox pods) in the cluster", "Projects", `[ { "id": "pantheon", "name": "Pantheon", "description": "Go API backend", "pod": "claudebox-pantheon-0", "status": "running" } ]`, )) spec.AddPath("/projects/{id}", "get", withParams( "Get project", "Returns details for a specific project including its configuration", "Projects", []param{{Name: "id", In: "path", Description: "Project ID (e.g., 'pantheon')", Required: true}}, )) // Commands spec.AddPath("/projects/{id}/claude", "post", withRequestBody( "Run Claude command", "Executes a Claude Code prompt in the project's claudebox pod. Returns a command ID for tracking via SSE.", "Commands", []param{{Name: "id", In: "path", Description: "Project ID", Required: true}}, `{ "prompt": "fix the bug in auth handler", "stream_id": "optional-correlation-id" }`, `{ "id": "cmd-pantheon-001", "project": "pantheon", "type": "claude", "status": "queued", "stream_url": "/projects/pantheon/events?stream_id=cmd-pantheon-001" }`, )) spec.AddPath("/projects/{id}/shell", "post", withRequestBody( "Run shell command", "Executes a shell command in the project's claudebox pod. Use for running tests, builds, etc.", "Commands", []param{{Name: "id", In: "path", Description: "Project ID", Required: true}}, `{ "command": "go test ./...", "stream_id": "optional-correlation-id" }`, `{ "id": "cmd-pantheon-002", "project": "pantheon", "type": "shell", "status": "queued", "stream_url": "/projects/pantheon/events?stream_id=cmd-pantheon-002" }`, )) spec.AddPath("/projects/{id}/git", "post", withRequestBody( "Run git command", "Executes a git command in the project's claudebox pod. Use for commits, pushes, pulls, etc.", "Commands", []param{{Name: "id", In: "path", Description: "Project ID", Required: true}}, `{ "args": ["status"], "stream_id": "optional-correlation-id" }`, `{ "id": "cmd-pantheon-003", "project": "pantheon", "type": "git", "status": "queued", "stream_url": "/projects/pantheon/events?stream_id=cmd-pantheon-003" }`, )) // SSE Events spec.AddPath("/projects/{id}/events", "get", map[string]any{ "summary": "Stream events", "description": `Server-Sent Events stream for real-time command output. Connect to this endpoint to receive output as commands execute. ## Event Types - **connected**: Initial connection confirmation - **output**: Command output (stdout or stderr) - **error**: Error occurred during execution - **complete**: Command finished (includes exit_code and duration_ms) - **heartbeat**: Keep-alive (sent every 30s) ## Example ` + "```javascript" + ` const events = new EventSource('/projects/pantheon/events?stream_id=cmd-001'); events.addEventListener('output', (e) => { const data = JSON.parse(e.data); console.log(data.line); }); events.addEventListener('complete', (e) => { const data = JSON.parse(e.data); console.log('Done:', data.exit_code); events.close(); }); ` + "```", "tags": []string{"Events"}, "parameters": []map[string]any{ { "name": "id", "in": "path", "description": "Project ID", "required": true, "schema": map[string]any{"type": "string"}, }, { "name": "stream_id", "in": "query", "description": "Command ID to filter events (optional)", "required": false, "schema": map[string]any{"type": "string"}, }, }, "responses": map[string]any{ "200": map[string]any{ "description": "SSE stream", "content": map[string]any{ "text/event-stream": map[string]any{ "schema": map[string]any{ "type": "string", "example": "event: output\ndata: {\"line\": \"Building...\", \"stream\": \"stdout\"}\n\n", }, }, }, }, }, }) return spec } // param represents an OpenAPI parameter. type param struct { Name string In string Description string Required bool } // withResponse creates an operation with example response. func withResponse(summary, description, tag, example string) map[string]any { return map[string]any{ "summary": summary, "description": description, "tags": []string{tag}, "responses": map[string]any{ "200": map[string]any{ "description": "Success", "content": map[string]any{ "application/json": map[string]any{ "example": example, }, }, }, }, } } // withParams creates an operation with path parameters. func withParams(summary, description, tag string, params []param) map[string]any { parameters := make([]map[string]any, len(params)) for i, p := range params { parameters[i] = map[string]any{ "name": p.Name, "in": p.In, "description": p.Description, "required": p.Required, "schema": map[string]any{"type": "string"}, } } return map[string]any{ "summary": summary, "description": description, "tags": []string{tag}, "parameters": parameters, "responses": map[string]any{ "200": map[string]any{"description": "Success"}, }, } } // withRequestBody creates an operation with request body and example response. func withRequestBody(summary, description, tag string, params []param, requestExample, responseExample string) map[string]any { parameters := make([]map[string]any, len(params)) for i, p := range params { parameters[i] = map[string]any{ "name": p.Name, "in": p.In, "description": p.Description, "required": p.Required, "schema": map[string]any{"type": "string"}, } } return map[string]any{ "summary": summary, "description": description, "tags": []string{tag}, "parameters": parameters, "requestBody": map[string]any{ "required": true, "content": map[string]any{ "application/json": map[string]any{ "example": requestExample, }, }, }, "responses": map[string]any{ "201": map[string]any{ "description": "Command queued", "content": map[string]any{ "application/json": map[string]any{ "example": responseExample, }, }, }, }, } }