Add teabot daemon implementation
teabot watches Gitea repos and dispatches one-shot Claude Code sessions in Docker containers to work issues and review PRs, acting as configurable bot personalities. Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.unkin.net/unkin/teabot/internal/config"
|
||||
"git.unkin.net/unkin/teabot/internal/dispatch"
|
||||
"git.unkin.net/unkin/teabot/internal/docker"
|
||||
"git.unkin.net/unkin/teabot/internal/gitea"
|
||||
"git.unkin.net/unkin/teabot/internal/state"
|
||||
)
|
||||
|
||||
func newRunCmd() *cobra.Command {
|
||||
var (
|
||||
once bool
|
||||
logJSON bool
|
||||
logLevel string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "run",
|
||||
Short: "Run the teabot daemon (foreground)",
|
||||
Long: "run starts teabot in the foreground: it polls the configured repos on an\n" +
|
||||
"interval and dispatches Claude sessions. Use --once for a single poll cycle\n" +
|
||||
"(handy for testing). This command is what the systemd user unit executes.",
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
logger := newLogger(logJSON, logLevel)
|
||||
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
store, err := state.New(cfg.StateDirOrDefault())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// One Gitea client, authenticated as the first personality, is
|
||||
// enough for read-only polling; actions happen inside containers as
|
||||
// the per-event personality.
|
||||
pollTok := cfg.Personalities[0].Token
|
||||
client := gitea.NewClient(cfg.GiteaURL, pollTok)
|
||||
|
||||
runner := docker.NewDockerRunner()
|
||||
runner.Stdout = os.Stderr
|
||||
|
||||
d := dispatch.New(cfg, store, client, runner, logger)
|
||||
|
||||
logger.Info("teabot starting",
|
||||
"repos", cfg.Repos,
|
||||
"personalities", len(cfg.Personalities),
|
||||
"poll_interval", cfg.PollInterval.String(),
|
||||
"max_concurrent", cfg.MaxConcurrent,
|
||||
"job_image", cfg.JobImage,
|
||||
"once", once)
|
||||
|
||||
if once {
|
||||
return d.PollOnce(cmd.Context(), true)
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(cmd.Context(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
return d.Run(ctx)
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&once, "once", false, "run a single poll cycle then exit")
|
||||
cmd.Flags().BoolVar(&logJSON, "log-json", false, "emit structured JSON logs")
|
||||
cmd.Flags().StringVar(&logLevel, "log-level", "info", "log level: debug, info, warn, error")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newLogger(jsonOut bool, level string) *slog.Logger {
|
||||
var lvl slog.Level
|
||||
switch level {
|
||||
case "debug":
|
||||
lvl = slog.LevelDebug
|
||||
case "warn":
|
||||
lvl = slog.LevelWarn
|
||||
case "error":
|
||||
lvl = slog.LevelError
|
||||
default:
|
||||
lvl = slog.LevelInfo
|
||||
}
|
||||
opts := &slog.HandlerOptions{Level: lvl}
|
||||
var h slog.Handler
|
||||
if jsonOut {
|
||||
h = slog.NewJSONHandler(os.Stderr, opts)
|
||||
} else {
|
||||
h = slog.NewTextHandler(os.Stderr, opts)
|
||||
}
|
||||
return slog.New(h)
|
||||
}
|
||||
Reference in New Issue
Block a user