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