Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Release

on:
push:
tags:
- "v*"

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: web/package-lock.json

- name: Install frontend dependencies
run: cd web && npm ci

- name: Build frontend
run: cd web && npm run build

- name: Copy frontend assets to proxy embed directory
run: |
rm -rf proxy/frontend/dist
cp -r web/build/client proxy/frontend/dist

- name: Run GoReleaser with goreleaser-cross
run: |
docker run \
--rm \
-e CGO_ENABLED=1 \
-e GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ${{ github.workspace }}:/go/src/github.com/${{ github.repository }} \
-w /go/src/github.com/${{ github.repository }} \
ghcr.io/goreleaser/goreleaser-cross:v1.23.6 \
release --clean
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ bin/
dist/
*.exe

# Embedded frontend assets (populated by build)
proxy/frontend/dist/*
!proxy/frontend/dist/.gitkeep

# IDE and system files
.DS_Store
.vscode/
Expand Down
73 changes: 73 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
version: 2

project_name: claude-code-proxy

builds:
- id: linux-amd64
dir: proxy
main: ./cmd/proxy
binary: claude-code-proxy
goos:
- linux
goarch:
- amd64
env:
- CGO_ENABLED=1
- CC=x86_64-linux-gnu-gcc
- CXX=x86_64-linux-gnu-g++

- id: linux-arm64
dir: proxy
main: ./cmd/proxy
binary: claude-code-proxy
goos:
- linux
goarch:
- arm64
env:
- CGO_ENABLED=1
- CC=aarch64-linux-gnu-gcc
- CXX=aarch64-linux-gnu-g++

- id: darwin-amd64
dir: proxy
main: ./cmd/proxy
binary: claude-code-proxy
goos:
- darwin
goarch:
- amd64
env:
- CGO_ENABLED=1
- CC=o64-clang
- CXX=o64-clang++

- id: darwin-arm64
dir: proxy
main: ./cmd/proxy
binary: claude-code-proxy
goos:
- darwin
goarch:
- arm64
env:
- CGO_ENABLED=1
- CC=oa64-clang
- CXX=oa64-clang++

archives:
- formats:
- tar.gz
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"

checksum:
name_template: "checksums.txt"

changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
- "^ci:"
- "^chore:"
58 changes: 23 additions & 35 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
# Multi-stage Dockerfile for Claude Code Proxy
# Builds both Go proxy server and Remix frontend in a single container
# Builds a single Go binary with embedded frontend assets

# Stage 1: Build Go Backend
# Stage 1: Build Node.js Frontend
FROM node:20-alpine AS node-builder

WORKDIR /app/web

COPY web/package*.json ./
RUN npm ci

COPY web/ ./
RUN npm run build

# Stage 2: Build Go Binary with embedded frontend
FROM golang:1.21-alpine AS go-builder

WORKDIR /app
Expand All @@ -16,28 +27,15 @@ RUN go mod download

# Copy Go source code
COPY proxy/ ./
# Build with CGO enabled for SQLite support
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o /app/bin/proxy cmd/proxy/main.go

# Stage 2: Build Node.js Frontend
FROM node:20-alpine AS node-builder

WORKDIR /app

# Copy package files
COPY web/package*.json ./web/
WORKDIR /app/web
RUN npm ci

# Copy web source code and build
COPY web/ ./
RUN npm run build
# Copy built frontend assets into embed directory
COPY --from=node-builder /app/web/build/client ./frontend/dist

# Clean up dev dependencies after build
RUN npm ci --only=production && npm cache clean --force
# Build with CGO enabled for SQLite support
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o /app/bin/proxy cmd/proxy/main.go

# Stage 3: Production Runtime
FROM node:20-alpine
FROM alpine:3.19

WORKDIR /app

Expand All @@ -48,25 +46,15 @@ RUN apk add --no-cache sqlite wget
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup

# Copy built Go binary
# Copy built Go binary (includes embedded frontend)
COPY --from=go-builder /app/bin/proxy ./bin/proxy
RUN chmod +x ./bin/proxy

# Copy built Remix application
COPY --from=node-builder /app/web/build ./web/build
COPY --from=node-builder /app/web/package*.json ./web/
COPY --from=node-builder /app/web/node_modules ./web/node_modules

# Create data directory for SQLite database
RUN mkdir -p /app/data && chown -R appuser:appgroup /app

# Copy startup script
COPY docker-entrypoint.sh ./
RUN chmod +x docker-entrypoint.sh

# Environment variables with defaults
ENV PORT=3001
ENV WEB_PORT=5173
ENV READ_TIMEOUT=600
ENV WRITE_TIMEOUT=600
ENV IDLE_TIMEOUT=600
Expand All @@ -75,8 +63,8 @@ ENV ANTHROPIC_VERSION=2023-06-01
ENV ANTHROPIC_MAX_RETRIES=3
ENV DB_PATH=/app/data/requests.db

# Expose ports
EXPOSE 3001 5173
# Expose single port
EXPOSE 3001

# Switch to app user
USER appuser
Expand All @@ -85,5 +73,5 @@ USER appuser
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget -qO- http://localhost:3001/health > /dev/null || exit 1

# Start both services
CMD ["./docker-entrypoint.sh"]
# Start single binary
CMD ["./bin/proxy"]
18 changes: 13 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: all build run clean install dev
.PHONY: all build run clean install dev

# Default target
all: install build
Expand All @@ -10,17 +10,22 @@ install:
@echo "📦 Installing Node dependencies..."
cd web && npm install

# Build both services
build: build-proxy build-web
# Build single binary (frontend → copy → Go)
build: build-web copy-frontend build-proxy

build-proxy:
@echo "🔨 Building proxy server..."
cd proxy && go build -o ../bin/proxy cmd/proxy/main.go
cd proxy && CGO_ENABLED=1 go build -o ../bin/proxy cmd/proxy/main.go

build-web:
@echo "🔨 Building web interface..."
cd web && npm run build

copy-frontend:
@echo "📋 Copying frontend assets into Go embed directory..."
rm -rf proxy/frontend/dist
cp -r web/build/client proxy/frontend/dist

# Run in development mode
dev:
@echo "🚀 Starting development servers..."
Expand All @@ -40,6 +45,9 @@ clean:
rm -rf bin/
rm -rf web/build/
rm -rf web/.cache/
rm -rf proxy/frontend/dist
mkdir -p proxy/frontend/dist
touch proxy/frontend/dist/.gitkeep
rm -f requests.db
rm -rf requests/

Expand All @@ -53,7 +61,7 @@ db-reset:
help:
@echo "Claude Code Monitor - Available targets:"
@echo " make install - Install all dependencies"
@echo " make build - Build both services"
@echo " make build - Build single binary (frontend + Go)"
@echo " make dev - Run in development mode"
@echo " make run-proxy - Run proxy server only"
@echo " make run-web - Run web interface only"
Expand Down
64 changes: 61 additions & 3 deletions proxy/cmd/proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@ package main

import (
"context"
"io/fs"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"

"github.com/gorilla/handlers"
"github.com/gorilla/mux"

"github.com/seifghazi/claude-code-monitor/frontend"
"github.com/seifghazi/claude-code-monitor/internal/config"
"github.com/seifghazi/claude-code-monitor/internal/handler"
"github.com/seifghazi/claude-code-monitor/internal/middleware"
Expand Down Expand Up @@ -62,15 +65,45 @@ func main() {
r.HandleFunc("/v1/models", h.Models).Methods("GET")
r.HandleFunc("/health", h.Health).Methods("GET")

r.HandleFunc("/", h.UI).Methods("GET")
r.HandleFunc("/ui", h.UI).Methods("GET")
r.HandleFunc("/api/requests", h.GetRequests).Methods("GET")
r.HandleFunc("/api/requests", h.DeleteRequests).Methods("DELETE")
r.HandleFunc("/api/conversations", h.GetConversations).Methods("GET")
r.HandleFunc("/api/conversations/{id}", h.GetConversationByID).Methods("GET")
r.HandleFunc("/api/conversations/project", h.GetConversationsByProject).Methods("GET")

r.NotFoundHandler = http.HandlerFunc(h.NotFound)
// Serve embedded frontend assets
distFS, err := fs.Sub(frontend.Assets, "dist")
if err != nil {
logger.Fatalf("❌ Failed to create sub filesystem: %v", err)
}
fileServer := http.FileServer(http.FS(distFS))

// Serve static assets (JS, CSS, fonts, etc.)
r.PathPrefix("/assets/").Handler(fileServer)

// SPA fallback: serve index.html for all non-API, non-asset paths
r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// Let API routes return 404 normally
if strings.HasPrefix(req.URL.Path, "/api/") || strings.HasPrefix(req.URL.Path, "/v1/") {
h.NotFound(w, req)
return
}
// Try to serve the file directly first (e.g. favicon.ico)
f, err := distFS.Open(strings.TrimPrefix(req.URL.Path, "/"))
if err == nil {
f.Close()
fileServer.ServeHTTP(w, req)
return
}
// Fall back to index.html for SPA routing
indexHTML, err := fs.ReadFile(distFS, "index.html")
if err != nil {
h.NotFound(w, req)
return
}
w.Header().Set("Content-Type", "text/html")
w.Write(indexHTML)
})

srv := &http.Server{
Addr: ":" + cfg.Server.Port,
Expand All @@ -80,6 +113,26 @@ func main() {
IdleTimeout: cfg.Server.IdleTimeout,
}

// Detect HTTP/HTTPS proxy from environment
proxyEnvVars := []string{"HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "NO_PROXY", "no_proxy"}
var detectedProxy bool
for _, env := range proxyEnvVars {
if val := os.Getenv(env); val != "" {
if !detectedProxy {
logger.Println("⚠️ HTTP/HTTPS proxy detected! All outbound API requests will be routed through:")
detectedProxy = true
}
logger.Printf(" %s=%s", env, val)
}
}
if !detectedProxy {
logger.Println("⚠️ No HTTP/HTTPS proxy detected. If you are behind a firewall, API calls may fail!")
logger.Println(" Set proxy environment variables before starting:")
logger.Printf(" export HTTP_PROXY=http://your-proxy:port")
logger.Printf(" export HTTPS_PROXY=http://your-proxy:port")
logger.Printf(" export NO_PROXY=localhost,127.0.0.1")
}

go func() {
logger.Printf("🚀 Claude Code Monitor Server running on http://localhost:%s", cfg.Server.Port)
logger.Printf("📡 API endpoints available at:")
Expand All @@ -89,6 +142,11 @@ func main() {
logger.Printf("🎨 Web UI available at:")
logger.Printf(" - GET http://localhost:%s/ (Request Visualizer)", cfg.Server.Port)
logger.Printf(" - GET http://localhost:%s/api/requests (Request API)", cfg.Server.Port)
logger.Println("")
logger.Println("👉 To use with Claude Code, run:")
logger.Printf(" export ANTHROPIC_BASE_URL=http://localhost:%s", cfg.Server.Port)
logger.Println(" claude")
logger.Println("")

if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatalf("❌ Server failed to start: %v", err)
Expand Down
Loading