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