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