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