From 7388337d3be5cc7f65ef547e30b2e39884dd165b Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Wed, 3 Jun 2026 22:46:47 +0000 Subject: feat: add --base-path flag to configure UI URL prefix Co-Authored-By: Claude Sonnet 4.6 --- internal/api/server.go | 33 ++++++++++++++++++++++++++++++++- internal/cli/serve.go | 7 +++++-- 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) } -- cgit v1.2.3