package main // The command line another program drives Codey through. // // Three invocations here are not for a person. A control plane that runs // coding agents — T3 Code is the one this was built against — spawns the // binary three ways: once to ask its version, once to ask what models it has // and whether it is logged in, and once to run the agent itself over a pipe. // Only the third does any work; the first two exist so the provider card can // say "installed" and "authenticated" before anyone starts a thread. // // The shapes are not ours to choose. They are what the client already sends to // the binary whose slot Codey is occupying, which is why the flags arrive // around the subcommand rather than before it and why the two probes print // prose a regular expression reads. import ( "context" "fmt" "io" "os" "sort" "strings" "time" "github.com/johannSo/codey/internal/acp" "github.com/johannSo/codey/internal/agent" "github.com/johannSo/codey/internal/llm" "github.com/johannSo/codey/internal/metrics" "github.com/johannSo/codey/internal/serve" "github.com/johannSo/codey/internal/subagent" "github.com/johannSo/codey/internal/tools" ) // invKind is which of the four things this command line is. type invKind int const ( invNormal invKind = iota invVersion invModels invAgent ) // invocation is the command line, read. type invocation struct { Kind invKind // Permission is the approval floor an agent run was launched with. Permission serve.PermissionMode // Rest is what is left for ordinary flag parsing once the words that // belong to the subcommand are taken out. Rest []string } // scanArgs reads the command line before the flag package sees it. // // It has to, for two reasons. `--version` is not one of our flags, and flag // parsing would exit(2) on it rather than answer. And the agent invocation // splits its flags around the subcommand — `--permission-mode default agent // stdio`, but `agent --always-approve stdio` — which no flag package parses, // because it is not one grammar but a client's habit. // // Everything unrecognised is handed on rather than rejected. The launch // arguments are a free-text field in somebody's settings panel, and the flags // Codey does understand should keep working when they arrive through it. func scanArgs(args []string) invocation { // The probes are exact: the client runs the binary with this one word and // nothing else. Requiring that keeps `-C models` from being mistaken for // a request to list them. if len(args) == 1 { switch args[0] { case "--version", "-version": return invocation{Kind: invVersion} case "models": return invocation{Kind: invModels} } } var ( sawAgent, sawStdio, always bool mode string rest []string ) for i := 0; i < len(args); i++ { a := args[i] switch { case a == "agent": sawAgent = true case a == "stdio": sawStdio = true case a == "--always-approve", a == "-always-approve": always = true case a == "--permission-mode", a == "-permission-mode": if i+1 < len(args) { mode = args[i+1] i++ } case strings.HasPrefix(a, "--permission-mode="): mode = strings.TrimPrefix(a, "--permission-mode=") case strings.HasPrefix(a, "-permission-mode="): mode = strings.TrimPrefix(a, "-permission-mode=") default: rest = append(rest, a) } } if !sawAgent || !sawStdio { // Not an agent run after all, so nothing was ours to consume: hand the // line back exactly as it came. return invocation{Kind: invNormal, Rest: args} } return invocation{Kind: invAgent, Permission: permissionMode(mode, always), Rest: rest} } // permissionMode maps the client's vocabulary onto ours. An unrecognised mode // is the strictest one: a name we do not know is not a reason to stop asking. func permissionMode(mode string, always bool) serve.PermissionMode { if always { return serve.ModeFullAccess } switch mode { case "acceptEdits": return serve.ModeAcceptEdits case "auto": return serve.ModeAuto case "bypassPermissions", "full-access": return serve.ModeFullAccess default: return serve.ModeAsk } } // printVersion answers the version probe. // // The line has to contain something matching \bv?(\d+\.\d+\.\d+)\b, because // that is the regular expression on the other end. Anything else reads as an // installed binary that cannot run. func printVersion(w io.Writer) { fmt.Fprintf(w, "codey %s\n", version) } // modelsTimeout bounds the listing. The probe that runs this has its own, // shorter patience, and a listing that has not arrived by then is better // reported as the one model we know about than not reported at all. const modelsTimeout = 8 * time.Second // printModels answers the model-and-login probe. // // The format is prose because the reader is a regular expression: a line // saying whether we are logged in, then one bullet per model with the default // marked. It exits zero even when it has nothing good to say — a non-zero exit // makes the client discard the whole listing, including the part that was // true. // // The client is the one startup built, so a Claude Code login counts as being // logged in exactly when the session would use it: when the config says // "claude_code_auth": "on". claudeCode says there is such a login on this // machine going unused, which is worth naming in the logged-out line — the // probe runs with no flags, so the config is the way to turn it on here. func printModels(w io.Writer, client *llm.Client, claudeCode bool) { if !client.HasCredentials() { if claudeCode { fmt.Fprintln(w, `You are not logged in. Set ANTHROPIC_API_KEY, or set "claude_code_auth": "on" in Codey's config to use your Claude Code login.`) return } fmt.Fprintln(w, "You are not logged in. Set ANTHROPIC_API_KEY.") return } fmt.Fprintf(w, "You are logged in to %s.\n", client.BaseURL) ctx, cancel := context.WithTimeout(context.Background(), modelsTimeout) defer cancel() models, err := client.ListModels(ctx) if err != nil || len(models) == 0 { // An endpoint that does not answer still serves the model we were // configured with, and offering that beats offering nothing. fmt.Fprintf(w, "* %s (default)\n", client.Model()) return } for _, m := range sortedModels(models) { if m.ID == client.Model() { fmt.Fprintf(w, "* %s (default)\n", m.ID) continue } fmt.Fprintf(w, "- %s\n", m.ID) } } // sortedModels puts the newest first, which is the order a picker should show // and not the order every endpoint answers in. func sortedModels(models []llm.Model) []llm.Model { out := append([]llm.Model(nil), models...) sort.SliceStable(out, func(i, j int) bool { return out[i].CreatedAt.After(out[j].CreatedAt) }) return out } // acpDeps is what an agent run borrows from the ordinary startup path. Nothing // here is built for this mode: it is the same client, the same event log, the // same options and the same tool factory the TUI gets, which is the point — // an agent driven through a pipe should be able to do exactly what an agent // driven through a keyboard can. type acpDeps struct { Permission serve.PermissionMode Client *llm.Client // NoAgents leaves the subagent tools out, so the agent works alone. NoAgents bool // ClientOpts rebuilds the client for another model. The model lives on the // client rather than on the agent, so a session that selects one needs a // client of its own. ClientOpts []llm.Option Log *metrics.Logger Base agent.Options // Tools is main's factory: a root in, a tool-set builder out. Tools func(root string) func(*agent.State, *metrics.Logger) *tools.Registry } // serveACP runs the agent over stdin and stdout until the client hangs up. // // Nothing else in this process may write to stdout from here on. The stream is // the protocol, and one stray line desynchronises a parser that has no way to // resynchronise. func serveACP(ctx context.Context, deps acpDeps) error { found := listModels(ctx, deps.Client) // What a subagent runs under is its parent's settings without the model // listing: a child never chooses a model for a child of its own, and a // paragraph about a choice it will not make is one it pays for on every // request. The parent does get the listing, because it is the one that // chooses. subBase := deps.Base base := deps.Base base.Models = found srv := serve.New(serve.Options{ Version: version, Permission: deps.Permission, Models: offeredModels(found), DefaultModel: deps.Client.Model(), NewAgent: func(req serve.AgentRequest) (serve.AgentSet, error) { client := deps.Client if req.Model != "" && req.Model != client.Model() { client = llm.New(append(deps.ClientOpts, llm.WithModel(req.Model))...) } // One factory for the session and for every child of it: an agent // that could reach further than its parent would be a way around // the parent's limits. newTools := deps.Tools(req.Root) reg := newTools(req.State, deps.Log) set := serve.AgentSet{} if !deps.NoAgents { sup := subagent.New(ctx, subagent.Options{ Client: client, Log: deps.Log, Policy: req.Approve, Base: subBase, Tools: newTools, }) reg.Register(sup.Tools()...) set.Agents = sup } set.Agent = agent.New(client, reg, req.State, deps.Log, base) return set, nil }, }) deps.Log.Log(metrics.Event{Type: "acp_start", Extra: map[string]any{ "permission": string(deps.Permission), "model": deps.Client.Model(), }}) return srv.Run(ctx, os.Stdin, os.Stdout) } // listModels asks the endpoint what it serves. An endpoint that will not say // leaves the list empty, which is the honest answer: no list beats a made-up // one, and both the picker and the agent's own model choices degrade to the // model this process was configured with. func listModels(ctx context.Context, client *llm.Client) []llm.Model { listCtx, cancel := context.WithTimeout(ctx, modelsTimeout) defer cancel() found, err := client.ListModels(listCtx) if err != nil { return nil } return sortedModels(found) } // offeredModels is that list as the client's model picker wants it. func offeredModels(found []llm.Model) []acp.ModelInfo { if len(found) == 0 { return nil } out := make([]acp.ModelInfo, 0, len(found)) for _, m := range found { out = append(out, acp.ModelInfo{ModelID: m.ID, Name: displayName(m)}) } return out } func displayName(m llm.Model) string { if m.DisplayName != "" { return m.DisplayName } return m.ID }