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