1package agent
2
3import (
4 "fmt"
5 "os"
6 "strings"
7
8 "github.com/patriceckhart/zot/packages/tui"
9 "golang.org/x/term"
10)
11
12// Mode is the CLI run mode.
13type Mode string
14
15const (
16 ModeInteractive Mode = "interactive"
17 ModePrint Mode = "print"
18 ModeJSON Mode = "json"
19 ModeRPC Mode = "rpc"
20 // ModeSwarmAgent is the long-lived, headless daemon mode used by
21 // swarm-spawned agents. The binary opens a unix-socket inbox at
22 // the path provided by --swarm-agent, reads supervisor messages
23 // off it ("user ...", "cancel", "shutdown"), runs each user turn
24 // against a persistent session, and streams JSONL events on
25 // stdout. See packages/agent/swarm/inbox.go for the wire protocol.
26 ModeSwarmAgent Mode = "swarm-agent"
27)
28
29// Args holds parsed command-line options.
30type Args struct {
31 Mode Mode
32 Provider string
33 Model string
34 APIKey string
35
36 BaseURL string // override provider base URL (for tests/self-hosted)
37 SystemPrompt string
38 AppendSystemPrompt []string
39 Reasoning string
40
41 Continue bool
42 Resume bool
43 Session string
44 NoSess bool
45
46 CWD string
47 NoTools bool
48 Tools []string
49 MaxSteps int
50
51 // Exts is a list of directory paths the user passed via --ext.
52 // Each must contain an extension.json. Loaded for one session
53 // only; never persisted. Take precedence over installed exts of
54 // the same name.
55 Exts []string
56
57 // NoExt disables extension discovery + spawn entirely for this
58 // run. --ext PATH still works (explicit beats implicit) so you
59 // can run "with only this one extension" via --no-ext --ext PATH.
60 NoExt bool
61
62 // NoSkill disables ALL skill discovery for this run, including
63 // the built-in skills compiled into the binary. The system
64 // prompt loses its "Available skills" manifest and the `skill`
65 // tool isn't registered. Useful for running zot without any
66 // extra context biasing the model.
67 NoSkill bool
68
69 // WithSkills controls loading user-installed skills from
70 // $ZOT_HOME/skills/, .zot/skills/, .claude/skills/, and
71 // .agents/skills/. It defaults to true; --no-skill disables all
72 // skill discovery, including built-ins.
73 WithSkills bool
74
75 // NoYolo turns on per-tool confirmation. Before each tool
76 // invocation the TUI prompts the user with the tool name + args
77 // and waits for an explicit yes/no. The user can also pick
78 // "always for this tool this session" or "always for anything
79 // this session" to stop being prompted again. Defaults off
80 // (yolo mode): tools run without asking.
81 //
82 // No effect in -p / --json / rpc modes, which have no
83 // interactive prompt. A warning is printed to stderr on startup
84 // so scripts know the flag is ignored, but tools still run
85 // freely so automated workflows keep working.
86 NoYolo bool
87
88 ListModels bool
89 Help bool
90 Version bool
91
92 Prompt string // concatenated positional args
93
94 // SwarmAgent is the inbox-socket path when this process is a
95 // swarm-spawned agent. Empty in every other mode. Set by
96 // --swarm-agent <path>; presence flips Mode to ModeSwarmAgent.
97 SwarmAgent string
98}
99
100// ParseArgs parses the process arguments (excluding argv[0]).
101func ParseArgs(in []string) (Args, error) {
102 a := Args{Mode: ModeInteractive, MaxSteps: 0, WithSkills: true}
103 positional := []string{}
104
105 want := func(i *int, flag string) (string, error) {
106 *i++
107 if *i >= len(in) {
108 return "", fmt.Errorf("%s requires a value", flag)
109 }
110 return in[*i], nil
111 }
112
113 for i := 0; i < len(in); i++ {
114 arg := in[i]
115 switch arg {
116 case "-h", "--help":
117 a.Help = true
118 case "-v", "--version":
119 a.Version = true
120 case "-p", "--print":
121 a.Mode = ModePrint
122 case "--json":
123 a.Mode = ModeJSON
124 case "--rpc":
125 a.Mode = ModeRPC
126 case "-c", "--continue":
127 a.Continue = true
128 case "-r", "--resume":
129 a.Resume = true
130 case "--no-session":
131 a.NoSess = true
132 case "--no-tools":
133 a.NoTools = true
134 case "--list-models":
135 a.ListModels = true
136 case "--experimental-oauth":
137 // deprecated: subscription login is always available.
138 // accepted silently for backwards compatibility.
139 case "--provider":
140 v, err := want(&i, arg)
141 if err != nil {
142 return a, err
143 }
144 a.Provider = v
145 case "--model":
146 v, err := want(&i, arg)
147 if err != nil {
148 return a, err
149 }
150 a.Model = v
151 case "--api-key":
152 v, err := want(&i, arg)
153 if err != nil {
154 return a, err
155 }
156 a.APIKey = v
157 case "--base-url":
158 v, err := want(&i, arg)
159 if err != nil {
160 return a, err
161 }
162 a.BaseURL = v
163 case "--system-prompt":
164 v, err := want(&i, arg)
165 if err != nil {
166 return a, err
167 }
168 a.SystemPrompt = v
169 case "--append-system-prompt":
170 v, err := want(&i, arg)
171 if err != nil {
172 return a, err
173 }
174 a.AppendSystemPrompt = append(a.AppendSystemPrompt, v)
175 case "--ext", "-e":
176 v, err := want(&i, arg)
177 if err != nil {
178 return a, err
179 }
180 // Repeatable; each value is a directory containing an
181 // extension.json. Resolved to absolute later so paths like
182 // "." survive a later cwd change.
183 a.Exts = append(a.Exts, v)
184 case "--no-ext", "--no-extensions":
185 a.NoExt = true
186 case "--no-skill", "--no-skills":
187 a.NoSkill = true
188 case "--with-skills", "--with-skill":
189 // Deprecated no-op: user skills are loaded by default.
190 a.WithSkills = true
191 case "--no-yolo":
192 a.NoYolo = true
193 case "--reasoning":
194 v, err := want(&i, arg)
195 if err != nil {
196 return a, err
197 }
198 switch strings.ToLower(v) {
199 case "", "off", "minimum", "minimal", "low", "medium", "high", "maximum", "max":
200 a.Reasoning = strings.ToLower(v)
201 default:
202 return a, fmt.Errorf("--reasoning must be off|minimum|low|medium|high|maximum")
203 }
204 case "--session":
205 v, err := want(&i, arg)
206 if err != nil {
207 return a, err
208 }
209 a.Session = v
210 case "--cwd":
211 v, err := want(&i, arg)
212 if err != nil {
213 return a, err
214 }
215 a.CWD = v
216 case "--swarm-agent":
217 v, err := want(&i, arg)
218 if err != nil {
219 return a, err
220 }
221 a.SwarmAgent = v
222 a.Mode = ModeSwarmAgent
223 case "--tools":
224 v, err := want(&i, arg)
225 if err != nil {
226 return a, err
227 }
228 for _, t := range strings.Split(v, ",") {
229 t = strings.TrimSpace(t)
230 if t != "" {
231 a.Tools = append(a.Tools, t)
232 }
233 }
234 case "--max-steps":
235 v, err := want(&i, arg)
236 if err != nil {
237 return a, err
238 }
239 var n int
240 if _, err := fmt.Sscanf(v, "%d", &n); err != nil || n <= 0 {
241 return a, fmt.Errorf("--max-steps must be a positive integer")
242 }
243 a.MaxSteps = n
244 default:
245 if strings.HasPrefix(arg, "-") && arg != "-" {
246 return a, fmt.Errorf("unknown flag %q", arg)
247 }
248 positional = append(positional, arg)
249 }
250 }
251
252 if len(positional) > 0 {
253 a.Prompt = strings.Join(positional, " ")
254 }
255
256 if a.CWD == "" {
257 a.CWD, _ = os.Getwd()
258 }
259 return a, nil
260}
261
262// PrintHelp writes the help text to stderr. When stderr is a TTY it
263// uses the same palette as zot's TUI; when redirected it falls back to
264// plain text with no ANSI escapes.
265func PrintHelp(version string) {
266 th := tui.Dark
267 fd := int(os.Stderr.Fd())
268 useColor := term.IsTerminal(fd)
269 style := func(c int, s string) string {
270 if !useColor {
271 return s
272 }
273 return th.FG256(c, s)
274 }
275 assistant := func(s string) string { return style(th.Assistant, s) }
276 muted := func(s string) string { return style(th.Muted, s) }
277 fg := func(s string) string { return style(th.FG, s) }
278 width := 96
279 if useColor {
280 if w, _, err := term.GetSize(fd); err == nil && w > 20 {
281 width = w
282 }
283 }
284 ruleWidth := width
285 if ruleWidth < 40 {
286 ruleWidth = 40
287 }
288 rule := strings.Repeat("─", ruleWidth)
289 if useColor {
290 rule = muted(rule)
291 }
292 leftW := 34
293 if width >= 120 {
294 leftW = 40
295 }
296 if width >= 140 {
297 leftW = 46
298 }
299 type row struct{ left, right string }
300 section := func(title string, rows ...row) {
301 fmt.Fprintln(os.Stderr)
302 fmt.Fprintln(os.Stderr, assistant(title))
303 fmt.Fprintln(os.Stderr, rule)
304 narrow := width < 100
305 for _, r := range rows {
306 if narrow {
307 fmt.Fprintf(os.Stderr, " %s\n", fg(r.left))
308 fmt.Fprintf(os.Stderr, " %s\n", muted(r.right))
309 fmt.Fprintln(os.Stderr)
310 continue
311 }
312 left := r.left
313 if len([]rune(left)) < leftW {
314 left += strings.Repeat(" ", leftW-len([]rune(left)))
315 }
316 fmt.Fprintf(os.Stderr, " %s %s\n", fg(left), muted(r.right))
317 }
318 }
319
320 fmt.Fprintln(os.Stderr)
321 var headline string
322 if useColor {
323 headline = th.AccentBar(th.Assistant) + assistant(tui.Bold("i'm zot. yet another coding agent harness."))
324 } else {
325 headline = "i'm zot. yet another coding agent harness."
326 }
327 fmt.Fprintln(os.Stderr, headline)
328 fmt.Fprintln(os.Stderr, muted("ask anything, or type /help inside the tui to see commands."))
329 fmt.Fprintf(os.Stderr, "%s %s\n", muted("version:"), fg(version))
330
331 section("modes",
332 row{"zot", "interactive tui"},
333 row{"zot \"prompt\"", "interactive, pre-filled prompt"},
334 row{"zot -p \"prompt\"", "print final text, exit"},
335 row{"zot --json \"prompt\"", "newline-delimited json events, exit"},
336 row{"zot rpc", "json-rpc loop on stdin/stdout (see docs/rpc.md)"},
337 )
338 section("extensions",
339 row{"zot ext list", "list installed extensions"},
340 row{"zot ext install <path|url>", "install into $ZOT_HOME/extensions/"},
341 row{"zot --ext ./path/to/ext", "load an extension for this run only"},
342 row{"zot ext help", "show all extension subcommands"},
343 )
344 section("self-update",
345 row{"zot update", "download and install the latest release"},
346 row{"zot update --check", "show whether a new release is available"},
347 )
348 section("telegram",
349 row{"zot telegram-bot setup", "configure a telegram bot (from BotFather)"},
350 row{"zot telegram-bot run", "foreground bridge (ctrl+c to stop)"},
351 row{"zot telegram-bot start", "background bridge (detached)"},
352 row{"zot telegram-bot stop", "stop the background bridge"},
353 row{"zot telegram-bot logs [-f]", "tail the background bridge log"},
354 row{"zot telegram-bot status", "config + running state"},
355 row{"zot telegram-bot reset", "forget saved token"},
356 row{"zot tg ...", "short alias for telegram-bot"},
357 )
358 section("provider and model flags",
359 row{"--provider", "provider to use (anthropic|openai|openai-codex|kimi|deepseek|google|ollama)"},
360 row{"--model ID", "model id (see --list-models)"},
361 row{"--api-key KEY", "api key for this run (env / auth.json fallback)"},
362 row{"--base-url URL", "override provider api base url"},
363 row{"--reasoning off|minimum|low|medium|high|maximum", "set thinking level on supported models"},
364 )
365 section("prompt and session flags",
366 row{"--system-prompt TEXT", "replace the default system prompt"},
367 row{"--append-system-prompt TEXT", "append to the system prompt (repeatable)"},
368 row{"-c, --continue", "continue the most recent session for this cwd"},
369 row{"-r, --resume", "pick a session to resume"},
370 row{"--session PATH", "resume a specific session file"},
371 row{"--no-session", "do not read or write a session file"},
372 )
373 section("workspace, tools, skills",
374 row{"--cwd PATH", "treat PATH as the working directory"},
375 row{"--no-tools", "disable all tools"},
376 row{"--tools csv", "only enable the listed tools"},
377 row{"--no-yolo", "ask before running every tool call"},
378 row{"--no-ext", "skip extension discovery for this run"},
379 row{"--no-skill", "skip all skill discovery for this run"},
380 )
381 section("misc",
382 row{"--max-steps N", "agent loop iteration cap (default: unlimited)"},
383 row{"--list-models", "print known models and exit"},
384 row{"-h, --help", "show this help"},
385 row{"-v, --version", "show version info"},
386 )
387 fmt.Fprintln(os.Stderr)
388 fmt.Fprintln(os.Stderr, assistant("see also: docs/extensions.md, docs/rpc.md, docs/skills.md"))
389 fmt.Fprintln(os.Stderr)
390}