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