Skip to content
Merged
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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,35 @@ All runtime config is environment variables. Defaults are in [`backend/internal/

---

## Local code-generation agent (optional)

Managed Agents covers most use cases — Anthropic hosts the harness, you just
ship prompts. For the cases where you instead need an agent that runs **on
this machine** (filesystem access, on-prem deployments, off-platform tools, or
swapping in Codex / Aider / OpenHands / Cline) the template ships an opt-in
wrapper around [`teslashibe/codegen-go`](https://github.com/teslashibe/codegen-go).

```bash
npm install -g @anthropic-ai/claude-code # or your CLI of choice
claude login

# add to backend/.env (see backend/.env.example for the full block):
echo 'CODEGEN_AGENT=claude-code' >> backend/.env

# kick the tires from the repo root:
go run ./backend/cmd/codegen-demo "Summarise this directory in one paragraph."
```

The wiring lives in [`backend/internal/codegen`](./backend/internal/codegen/codegen.go) —
~100 lines that read `CODEGEN_*` env vars and hand back a
`codegen.Agent`. It is independent of the Managed Agents path; you can use one,
the other, or both side-by-side. See the
[`codegen-go` README](https://github.com/teslashibe/codegen-go#readme) for the
full API and the supported CLI presets (Claude Code, Codex, Aider, OpenHands,
Cline, custom).

---

## Common tasks

```bash
Expand Down
13 changes: 13 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ AGENT_SYSTEM_PROMPT=You are a helpful assistant.
AGENT_RUN_RATE_LIMIT=10
AGENT_RUN_RATE_WINDOW_SECONDS=60

# --- Local code-generation agent (OPTIONAL) --------------------------------
# Pluggable wrapper around `claude` (and any other prompt-on-stdin coding-agent
# CLI) via github.com/teslashibe/codegen-go. Used by `cmd/codegen-demo` and any
# code that calls `internal/codegen.LoadFromEnv()`. Independent of the
# Anthropic Managed Agents path above; leave commented out if you only use
# Managed Agents.
# CODEGEN_AGENT=claude-code # claude-code (default) | generic
# CODEGEN_MODEL=claude-sonnet-4-5 # optional --model override
# CODEGEN_TIMEOUT=30m # per-run cap
# CODEGEN_MAX_OUTPUT_BYTES=10485760 # cap captured stdout+stderr (10 MiB)
# CODEGEN_COMMAND= # generic only: binary path
# CODEGEN_ARGS= # generic only: comma-separated argv

# Teams — multi-tenant collaboration with Owner / Admin / Member roles.
# When TEAMS_ENABLED is false the /api/teams and /api/invites routes are
# not mounted but personal teams still get auto-created on first login so
Expand Down
58 changes: 58 additions & 0 deletions backend/cmd/codegen-demo/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// codegen-demo is a tiny CLI that proves the local Claude Code wiring works
// end-to-end. It loads CODEGEN_* env vars, builds an Agent, runs it against
// the current directory with the prompt provided on argv, and prints the
// captured output.
//
// Usage:
//
// go run ./cmd/codegen-demo "Summarise this directory in one paragraph."
//
// Prerequisites: `claude` on PATH and `claude login` already done (or
// CODEGEN_AGENT=generic with CODEGEN_COMMAND set to your CLI of choice).
package main

import (
"context"
"fmt"
"log"
"os"
"path/filepath"

"github.com/joho/godotenv"

"github.com/teslashibe/agent-setup/backend/internal/codegen"
)

func main() {
_ = godotenv.Load(".env", "backend/.env")

if len(os.Args) < 2 {
log.Fatalf("usage: %s <prompt> [workDir]", filepath.Base(os.Args[0]))
}
prompt := os.Args[1]
workDir, _ := os.Getwd()
if len(os.Args) >= 3 {
workDir = os.Args[2]
}

agent, err := codegen.LoadFromEnv()
if err != nil {
log.Fatalf("load agent: %v", err)
}

res, err := agent.Run(context.Background(), prompt, workDir)
if err != nil {
log.Fatalf("%s failed (exit=%d): %v\n--- output (tail) ---\n%s",
agent.Name(), res.ExitCode, err, tail(res.Output, 4000))
}

fmt.Printf("--- %s (%s) ---\n%s\n",
agent.Name(), res.Duration.Round(1e6), res.Output)
}

func tail(s string, n int) string {
if len(s) <= n {
return s
}
return s[len(s)-n:]
}
32 changes: 17 additions & 15 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,22 @@ require (
github.com/jackc/pgx/v5 v5.9.2
github.com/joho/godotenv v1.5.1
github.com/pressly/goose/v3 v3.27.0
github.com/teslashibe/codegen-go v0.1.1
github.com/teslashibe/elevenlabs-go v1.1.2
github.com/teslashibe/facebook-go v0.3.2
github.com/teslashibe/hn-go v0.3.2
github.com/teslashibe/instagram-go v1.2.2
github.com/teslashibe/linkedin-go v1.2.3
github.com/teslashibe/magiclink-auth-go v0.2.0
github.com/teslashibe/mcptool v0.1.1
github.com/teslashibe/nextdoor-go v1.2.1
github.com/teslashibe/producthunt-go v0.3.3
github.com/teslashibe/reddit-go v1.2.3
github.com/teslashibe/redditviral-go v0.2.0
github.com/teslashibe/threads-go v1.2.0
github.com/teslashibe/tiktok-go v1.2.1
github.com/teslashibe/x-go v1.6.1
github.com/teslashibe/x-viral-go v0.2.0
github.com/valyala/fasthttp v1.70.0
)

Expand All @@ -23,29 +38,16 @@ require (
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/teslashibe/codegen-go v0.1.1 // indirect
github.com/teslashibe/elevenlabs-go v1.1.2 // indirect
github.com/teslashibe/facebook-go v0.3.2 // indirect
github.com/teslashibe/hn-go v0.3.2 // indirect
github.com/teslashibe/instagram-go v1.2.2 // indirect
github.com/teslashibe/linkedin-go v1.2.3 // indirect
github.com/teslashibe/mcptool v0.1.1 // indirect
github.com/teslashibe/nextdoor-go v1.2.1 // indirect
github.com/teslashibe/producthunt-go v0.3.3 // indirect
github.com/teslashibe/reddit-go v1.2.3 // indirect
github.com/teslashibe/redditviral-go v0.2.0 // indirect
github.com/teslashibe/threads-go v1.2.0 // indirect
github.com/teslashibe/tiktok-go v1.2.1 // indirect
github.com/teslashibe/x-go v1.6.1 // indirect
github.com/teslashibe/x-viral-go v0.2.0 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
Expand Down
13 changes: 9 additions & 4 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -34,6 +35,10 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
Expand All @@ -57,15 +62,15 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/teslashibe/codegen-go v0.0.0-20260422194151-eab5fd43375e h1:yqNWDftP0+p3zqeRQNwrzIAnx0VnNZkexE8rHwSoRUI=
github.com/teslashibe/codegen-go v0.0.0-20260422194151-eab5fd43375e/go.mod h1:O3u8V2cEXlR8T13EOZAwWVjSGhqkjVm+kZU6cUrwFk8=
github.com/teslashibe/codegen-go v0.1.1 h1:kubDBOg5QaZurVGcm+WmKgRC6IuJhKTTDpKV5nmimf0=
github.com/teslashibe/codegen-go v0.1.1/go.mod h1:MOw2rOI6r8Tp2oZMl6vpORD2zrQ7huPG5pBZk44M2K4=
github.com/teslashibe/elevenlabs-go v1.1.2 h1:Ni4Mhw1ekjtmBcCik7JAtGwHya2oadsyBW1dpBWX0c0=
Expand All @@ -90,8 +95,6 @@ github.com/teslashibe/reddit-go v1.2.3 h1:DRpXhRbNblPl6kqTD8LuN+UO+I46eXcvp6mXHX
github.com/teslashibe/reddit-go v1.2.3/go.mod h1:Mxq5g2H6mkZkLKtgimo5gsKpl0pTI600YvCzumUR+3I=
github.com/teslashibe/redditviral-go v0.2.0 h1:Y2rdFU1RioPR0AgPolQjItGytyeT2AHs1a1/IBIBjvU=
github.com/teslashibe/redditviral-go v0.2.0/go.mod h1:+W9P/ooqtFY34vRwjH7fZGBW0ehxuJA9GtvbcXGo1Aw=
github.com/teslashibe/threads-go v1.1.2 h1:l5+NvPDfRsqEqBJ5mUURmFLe2uHbZvCbJ3QxkTKTCdw=
github.com/teslashibe/threads-go v1.1.2/go.mod h1:JsSax+WV15LA1WiC784loAJfdQLjdQUFFh0YUjumOIQ=
github.com/teslashibe/threads-go v1.2.0 h1:ZVdjt3n5BpJYiCMKjYyxkck/Q3Hhlp/gqhk8DuD6cKM=
github.com/teslashibe/threads-go v1.2.0/go.mod h1:Et38f/uB6+Fl9mPCmtg2mCrTj9dQ1+S+W/2jS+N1fsE=
github.com/teslashibe/tiktok-go v1.2.1 h1:5SZJkU62Xf4KT3HKj2HwiMu5zKHAB8iRJwT4HkFIA50=
Expand Down Expand Up @@ -135,6 +138,8 @@ golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
113 changes: 113 additions & 0 deletions backend/internal/codegen/codegen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Package codegen wires github.com/teslashibe/codegen-go into the agent-setup
// template as an OPTIONAL local-execution path that runs the Claude Code CLI
// (or any other "prompt-on-stdin, edit-files-in-cwd" agent) inside this
// process's working directory.
//
// This is complementary to — not a replacement for — the Anthropic Managed
// Agents path used by the `internal/agent` package. Use this shim when you
// need the agent to:
//
// - Run on this machine (filesystem access, on-prem, off-platform tools).
// - Drive a different CLI (Codex, Aider, OpenHands, Cline, your own).
// - Operate inside a long-running process under your direct control.
//
// Configuration is read from CODEGEN_* environment variables; see
// LoadFromEnv. Defaults match codegen-go upstream.
package codegen

import (
"fmt"
"os"
"strconv"
"strings"
"time"

cgg "github.com/teslashibe/codegen-go"
)

// Re-exports so callers can keep using `codegen.X` without juggling two
// import paths.
type (
Agent = cgg.Agent
Config = cgg.Config
Result = cgg.Result
)

// New constructs an Agent directly from cfg. Thin alias around cgg.NewAgent.
func New(cfg Config) (Agent, error) { return cgg.NewAgent(cfg) }

// LoadFromEnv builds an Agent from CODEGEN_* environment variables:
//
// CODEGEN_AGENT "claude-code" (default) | "generic"
// CODEGEN_MODEL optional --model override forwarded to claude
// CODEGEN_TIMEOUT Go duration (default 30m)
// CODEGEN_MAX_OUTPUT_BYTES cap on captured stdout+stderr (default 10 MiB)
// CODEGEN_COMMAND binary for the generic CLI agent
// CODEGEN_ARGS comma-separated argv prepended to the generic CLI
//
// Returns an error if CODEGEN_AGENT is unrecognised or if generic mode is
// selected without CODEGEN_COMMAND.
func LoadFromEnv() (Agent, error) {
cfg := Config{
Type: getEnv("CODEGEN_AGENT", "claude-code"),
Model: strings.TrimSpace(os.Getenv("CODEGEN_MODEL")),
Timeout: getDurationEnv("CODEGEN_TIMEOUT", cgg.DefaultTimeout),
MaxOutputBytes: getIntEnv("CODEGEN_MAX_OUTPUT_BYTES", cgg.DefaultMaxOutputBytes),
Command: strings.TrimSpace(os.Getenv("CODEGEN_COMMAND")),
Args: splitCSV(os.Getenv("CODEGEN_ARGS")),
}
if cfg.Type == "generic" && cfg.Command == "" {
return nil, fmt.Errorf("codegen: CODEGEN_AGENT=generic requires CODEGEN_COMMAND")
}
return cgg.NewAgent(cfg)
}

func getEnv(key, fallback string) string {
v, ok := os.LookupEnv(key)
if !ok || strings.TrimSpace(v) == "" {
return fallback
}
return strings.TrimSpace(v)
}

func getIntEnv(key string, fallback int) int {
raw := strings.TrimSpace(os.Getenv(key))
if raw == "" {
return fallback
}
n, err := strconv.Atoi(raw)
if err != nil {
return fallback
}
return n
}

func getDurationEnv(key string, fallback time.Duration) time.Duration {
raw := strings.TrimSpace(os.Getenv(key))
if raw == "" {
return fallback
}
d, err := time.ParseDuration(raw)
if err != nil {
return fallback
}
return d
}

func splitCSV(raw string) []string {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
if v := strings.TrimSpace(p); v != "" {
out = append(out, v)
}
}
if len(out) == 0 {
return nil
}
return out
}
Loading
Loading