summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-06-03 22:46:47 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-06-03 22:46:47 +0000
commit7388337d3be5cc7f65ef547e30b2e39884dd165b (patch)
tree0dc287c9b4f841069a6abf1f5c9a8f28a37bfd9d /internal
parentc86c3771489c94bf4c15c1deff9d3cb568fbfc46 (diff)
feat: add --base-path flag to configure UI URL prefix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/api/server.go33
-rw-r--r--internal/cli/serve.go7
2 files changed, 37 insertions, 3 deletions
diff --git a/internal/api/server.go b/internal/api/server.go
index ffa8cd4..fe883bb 100644
--- a/internal/api/server.go
+++ b/internal/api/server.go
@@ -1,9 +1,11 @@
package api
import (
+ "bytes"
"context"
"encoding/json"
"fmt"
+ "io/fs"
"log/slog"
"net/http"
"os"
@@ -62,6 +64,7 @@ type Server struct {
dropsDir string
llm *llm.Client
budget budgetReporter // optional; per-provider spend headroom for GET /api/budget
+ basePath string // URL prefix the UI is mounted at, e.g. "/claudomator-oss"
}
// SetAPIToken configures a bearer token that must be supplied to access the API.
@@ -91,6 +94,11 @@ func (s *Server) SetWorkspaceRoot(path string) {
s.workspaceRoot = path
}
+// SetBasePath sets the URL prefix the UI is served under (e.g. "/claudomator-oss").
+func (s *Server) SetBasePath(p string) {
+ s.basePath = p
+}
+
// Pool returns the executor pool, for graceful shutdown by the caller.
func (s *Server) Pool() *executor.Pool { return s.pool }
@@ -199,7 +207,7 @@ func (s *Server) routes() {
s.mux.Handle("POST /chatbot/mcp", chatbotHandler)
s.mux.Handle("GET /chatbot/mcp", chatbotHandler)
s.mux.Handle("DELETE /chatbot/mcp", chatbotHandler)
- s.mux.Handle("GET /", http.FileServerFS(webui.Files))
+ s.mux.HandleFunc("GET /", s.handleStaticFiles)
}
// forwardResults listens on the executor pool's result and started channels and broadcasts via WebSocket.
@@ -695,6 +703,29 @@ func (s *Server) handleGetExecution(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, exec)
}
+func (s *Server) handleStaticFiles(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/" || r.URL.Path == "/index.html" {
+ raw, err := fs.ReadFile(webui.Files, "index.html")
+ if err != nil {
+ http.Error(w, "not found", http.StatusNotFound)
+ return
+ }
+ basePath := s.basePath
+ if basePath == "" {
+ basePath = "/claudomator"
+ }
+ rewritten := bytes.ReplaceAll(raw,
+ []byte(`content="/claudomator"`),
+ []byte(`content="`+basePath+`"`),
+ )
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write(rewritten)
+ return
+ }
+ http.FileServerFS(webui.Files).ServeHTTP(w, r)
+}
+
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
diff --git a/internal/cli/serve.go b/internal/cli/serve.go
index 9aa765b..fe0ba8e 100644
--- a/internal/cli/serve.go
+++ b/internal/cli/serve.go
@@ -23,6 +23,7 @@ import (
func newServeCmd() *cobra.Command {
var addr string
var workspaceRoot string
+ var basePath string
cmd := &cobra.Command{
Use: "serve",
@@ -31,11 +32,12 @@ func newServeCmd() *cobra.Command {
if cmd.Flags().Changed("workspace-root") {
cfg.WorkspaceRoot = workspaceRoot
}
- return serve(addr)
+ return serve(addr, basePath)
},
}
cmd.Flags().StringVar(&addr, "addr", "127.0.0.1:8484", "listen address (loopback by default; set external_bind_allowed=true in config to bind externally)")
+ cmd.Flags().StringVar(&basePath, "base-path", "/claudomator", "URL prefix the UI is served under (e.g. /claudomator-oss)")
cmd.Flags().StringVar(&workspaceRoot, "workspace-root", "/workspace", "root directory for listing workspaces")
cmd.Flags().StringVar(&cfg.ClaudeImage, "claude-image", cfg.ClaudeImage, "docker image for claude agents")
cmd.Flags().StringVar(&cfg.GeminiImage, "gemini-image", cfg.GeminiImage, "docker image for gemini agents")
@@ -43,7 +45,7 @@ func newServeCmd() *cobra.Command {
return cmd
}
-func serve(addr string) error {
+func serve(addr, basePath string) error {
if err := config.ValidateBindAddr(addr, cfg.ExternalBindAllowed); err != nil {
return err
}
@@ -175,6 +177,7 @@ func serve(addr string) error {
pool.RecoverStaleBlocked()
srv := api.NewServer(store, pool, agentRegistry, logger, cfg.ClaudeBinaryPath, cfg.GeminiBinaryPath)
+ srv.SetBasePath(basePath)
if accountant != nil {
srv.SetBudget(accountant)
}