| 1 | // The `lchat agents` subcommand family (docs/superpowers/specs/ |
| 2 | // 2026-08-05-eleven-claude-agents-design.md): |
| 3 | // |
| 4 | // - `lchat agents run` IS the workstation bridge (internal/agents) — run on |
| 5 | // the machine whose Claude Code sessions a group should share. A pure |
| 6 | // outbound client in the bot-sidecar mould: it dials the instance, holds |
| 7 | // its member key and the machine's authority HERE, and opens no ports. |
| 8 | // |
| 9 | // Unlike bot/hooks there is no add-agents control verb: the bridge runs on a |
| 10 | // developer workstation, not the router box, so an operator stands it up by |
| 11 | // hand — mint an agent invite, write the config, run the command. |
| 12 | package main |
| 13 | |
| 14 | import ( |
| 15 | "context" |
| 16 | "flag" |
| 17 | "fmt" |
| 18 | "log" |
| 19 | "os" |
| 20 | "strings" |
| 21 | |
| 22 | "lchat/internal/agents" |
| 23 | "lchat/internal/bot" |
| 24 | ) |
| 25 | |
| 26 | func runAgentsCmd(args []string) { |
| 27 | if len(args) < 1 { |
| 28 | fmt.Fprintln(os.Stderr, `usage: lchat agents <command> |
| 29 | |
| 30 | commands: |
| 31 | run run the workstation bridge: front this machine's Claude Code sessions |
| 32 | into the group threads the config grants (config: agents.json)`) |
| 33 | os.Exit(2) |
| 34 | } |
| 35 | switch args[0] { |
| 36 | case "run": |
| 37 | runAgentsRun(args[1:]) |
| 38 | default: |
| 39 | die("agents: unknown command %q", args[0]) |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | func runAgentsRun(args []string) { |
| 44 | fs := flag.NewFlagSet("agents run", flag.ExitOnError) |
| 45 | base := fs.String("base", envOr("LCHAT_BASE", ""), "instance URL (env: LCHAT_BASE)") |
| 46 | session := fs.String("session", envOr("LCHAT_SESSION", "agents-session.json"), "member session file (env: LCHAT_SESSION)") |
| 47 | config := fs.String("config", envOr("LCHAT_AGENTS_CONFIG", "agents.json"), "bridge config: which groups get which roots (env: LCHAT_AGENTS_CONFIG)") |
| 48 | _ = fs.Parse(reorderFlagsFirst(fs, args)) |
| 49 | |
| 50 | if *base == "" { |
| 51 | die("agents run: set -base (or LCHAT_BASE) to your instance URL") |
| 52 | } |
| 53 | // The config is the blast radius — fail-closed before touching the network. |
| 54 | cfg, err := agents.LoadConfig(*config) |
| 55 | if err != nil { |
| 56 | die("agents run: %v", err) |
| 57 | } |
| 58 | // Self-provision on first boot from a one-time agent invite, exactly like |
| 59 | // the bot sidecar (#806). Env-only — a flag would leak it into the process |
| 60 | // table. |
| 61 | sess, err := bot.EnsureSession(strings.TrimRight(*base, "/"), *session, os.Getenv("LCHAT_INVITE"), "Claude", log.Printf) |
| 62 | if err != nil { |
| 63 | die("agents run: %v", err) |
| 64 | } |
| 65 | if sess == nil { |
| 66 | die("agents run: no session at %s and no LCHAT_INVITE — mint an agent invite (admin → helpers) first", *session) |
| 67 | } |
| 68 | |
| 69 | b := agents.New(strings.TrimRight(*base, "/"), sess, cfg, log.Printf) |
| 70 | log.Printf("agents run: bridging %d group(s) on %s", len(cfg.Groups), *base) |
| 71 | if err := b.Run(context.Background()); err != nil { |
| 72 | die("agents run: %v", err) |
| 73 | } |
| 74 | } |
| 75 | |