// Command harness is the CLI entry point: flag parsing, wiring, and the REPL. package main import ( "bufio" "context" "errors" "flag" "fmt" "os" "path/filepath" "strings" "time" "harness/internal/agent" "harness/internal/llm" "harness/internal/metrics" "harness/internal/tools" "harness/internal/ui" ) func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, "harness:", err) os.Exit(1) } } func run() error { workdir := flag.String("C", ".", "working root the agent operates in") model := flag.String("model", "", "model id (default "+llm.DefaultModel+")") baseURL := flag.String("base-url", "", "API endpoint, for a proxy, gateway, or local server speaking the Messages API (env ANTHROPIC_BASE_URL, default "+llm.DefaultBaseURL+")") apiKey := flag.String("api-key", "", "credential sent as x-api-key (env ANTHROPIC_API_KEY). Visible to other processes; prefer the env var") authToken := flag.String("auth-token", "", "credential sent as Authorization: Bearer, for OAuth tokens and most gateways (env ANTHROPIC_AUTH_TOKEN)") effort := flag.String("effort", "", "output_config.effort: low|medium|high|xhigh|max (default: the API's, high)") maxTurns := flag.Int("max-turns", agent.DefaultMaxTurns, "turn cap per prompt") contextWindow := flag.Int("context-window", agent.DefaultContextWindow, "model context window, used to decide when to compact") compactAt := flag.Float64("compact-at", agent.DefaultCompactAt, "fraction of the window at which to compact") noCache := flag.Bool("no-cache", false, "disable prompt cache breakpoints") logPath := flag.String("metrics", metrics.DefaultPath, "JSONL event log path, relative to the working root") prompt := flag.String("p", "", "run one prompt and exit, instead of starting the REPL") verbose := flag.Bool("v", false, "print full tool output") flag.Parse() root, err := filepath.Abs(*workdir) if err != nil { return err } session := time.Now().UTC().Format("20060102T150405Z") log, err := metrics.Open(filepath.Join(root, *logPath), session) if err != nil { return err } defer log.Close() // Flags win over the environment, which wins over the defaults. Every // option below ignores an empty value, so an unset flag leaves the // environment in place. client := llm.New( llm.WithModel(*model), llm.WithBaseURL(*baseURL), llm.WithAPIKey(*apiKey), llm.WithAuthToken(*authToken), ) if !client.HasCredentials() { return errNoCredentials } log.Log(metrics.Event{Type: "session_start", Extra: map[string]any{ "root": root, "model": client.Model, "base_url": client.BaseURL, }}) if !client.IsDefaultEndpoint() { // Worth saying out loud: the prompt and every file the agent reads go // to this endpoint, not to Anthropic. fmt.Fprintf(os.Stderr, "harness: sending requests to %s\n", client.BaseURL) } state := agent.NewState() reg := tools.NewRegistry(log).Register( tools.NewRead(root, state), tools.NewGlob(root), tools.NewGrep(root), tools.NewEdit(root, state), tools.NewWrite(root, state), tools.NewBash(root), ) render := &ui.Renderer{W: os.Stdout, Verbose: *verbose} a := agent.New(client, reg, state, log, agent.Options{ MaxTurns: *maxTurns, Effort: *effort, ContextWindow: *contextWindow, CompactAt: *compactAt, NoCache: *noCache, }) a.Observe = render.Handle ctx := context.Background() if *prompt != "" { return once(ctx, a, render, *prompt) } fmt.Printf("harness · %s · %s\ntools: %s\ntype a prompt, or ctrl-d to exit\n\n", client.Model, root, strings.Join(toolNames(reg), ", ")) in := bufio.NewScanner(os.Stdin) in.Buffer(make([]byte, 0, 64*1024), 1024*1024) for { fmt.Print("> ") if !in.Scan() { fmt.Println() return in.Err() } line := strings.TrimSpace(in.Text()) if line == "" { continue } if line == "/exit" || line == "/quit" { return nil } if err := once(ctx, a, render, line); err != nil { render.Error(err) } fmt.Println() } } // once runs a single prompt and prints the session totals afterwards. func once(ctx context.Context, a *agent.Agent, render *ui.Renderer, prompt string) error { _, err := a.Run(ctx, prompt) usage, turns := a.State().Usage() render.Usage(usage, turns) return err } // errNoCredentials spells out every way to supply one, because "unauthorized" // against a gateway that ignores the header is a confusing failure. var errNoCredentials = errors.New(`no credentials. Supply one of: export ANTHROPIC_API_KEY=sk-ant-... (sent as x-api-key) export ANTHROPIC_AUTH_TOKEN=... (sent as Authorization: Bearer) -api-key / -auth-token (flags override the environment) To use a non-Anthropic endpoint, set ANTHROPIC_BASE_URL or -base-url as well.`) func toolNames(reg *tools.Registry) []string { var names []string for _, t := range reg.List() { names = append(names, t.Name()) } return names }