1package agent
2
3import (
4 "fmt"
5 "os"
6 "strings"
7
8 "github.com/patriceckhart/zot/internal/tui"
9 "golang.org/x/term"
10)
11
12// Mode is the CLI run mode.
13type Mode string
14
15const (
16 ModeInteractive Mode = "interactive"
17 ModePrint Mode = "print"
18 ModeJSON Mode = "json"
19 ModeRPC Mode = "rpc"
20)
21
22// Args holds parsed command-line options.
23type Args struct {
24 Mode Mode
25 Provider string
26 Model string
27 APIKey string
28
29 BaseURL string // override provider base URL (for tests/self-hosted)
30 SystemPrompt string
31 AppendSystemPrompt []string
32 Reasoning string
33
34 Continue bool
35 Resume bool
36 Session string
37 NoSess bool
38
39 CWD string
40 NoTools bool
41 Tools []string
42 MaxSteps int
43
44 // Exts is a list of directory paths the user passed via --ext.
45 // Each must contain an extension.json. Loaded for one session
46 // only; never persisted. Take precedence over installed exts of
47 // the same name.
48 Exts []string
49
50 // NoExt disables extension discovery + spawn entirely for this
51 // run. --ext PATH still works (explicit beats implicit) so you
52 // can run "with only this one extension" via --no-ext --ext PATH.
53 NoExt bool
54
55 // NoSkill disables ALL skill discovery for this run, including
56 // the built-in skills compiled into the binary. The system
57 // prompt loses its "Available skills" manifest and the `skill`
58 // tool isn't registered. Useful for running zot without any
59 // extra context biasing the model.
60 NoSkill bool
61
62 // WithSkills opts into loading user-installed skills from
63 // $ZOT_HOME/skills/, .zot/skills/, .claude/skills/, and
64 // .agents/skills/. Without this flag only the built-in skills
65 // shipped with the zot binary are available, so a fresh install
66 // has a deterministic skill set regardless of what's lying
67 // around in the user's home directory.
68 WithSkills bool
69
70 // NoYolo turns on per-tool confirmation. Before each tool
71 // invocation the TUI prompts the user with the tool name + args
72 // and waits for an explicit yes/no. The user can also pick
73 // "always for this tool this session" or "always for anything
74 // this session" to stop being prompted again. Defaults off
75 // (yolo mode): tools run without asking.
76 //
77 // No effect in -p / --json / rpc modes, which have no
78 // interactive prompt. A warning is printed to stderr on startup
79 // so scripts know the flag is ignored, but tools still run
80 // freely so automated workflows keep working.
81 NoYolo bool
82
83 ListModels bool
84 Help bool
85 Version bool
86
87 Prompt string // concatenated positional args
88}
89
90// ParseArgs parses the process arguments (excluding argv[0]).
91func ParseArgs(in []string) (Args, error) {
92 a := Args{Mode: ModeInteractive, MaxSteps: 50}
93 positional := []string{}
94
95 want := func(i *int, flag string) (string, error) {
96 *i++
97 if *i >= len(in) {
98 return "", fmt.Errorf("%s requires a value", flag)
99 }
100 return in[*i], nil
101 }
102
103 for i := 0; i < len(in); i++ {
104 arg := in[i]
105 switch arg {
106 case "-h", "--help":
107 a.Help = true
108 case "-v", "--version":
109 a.Version = true
110 case "-p", "--print":
111 a.Mode = ModePrint
112 case "--json":
113 a.Mode = ModeJSON
114 case "--rpc":
115 a.Mode = ModeRPC
116 case "-c", "--continue":
117 a.Continue = true
118 case "-r", "--resume":
119 a.Resume = true
120 case "--no-session":
121 a.NoSess = true
122 case "--no-tools":
123 a.NoTools = true
124 case "--list-models":
125 a.ListModels = true
126 case "--experimental-oauth":
127 // deprecated: subscription login is always available.
128 // accepted silently for backwards compatibility.
129 case "--provider":
130 v, err := want(&i, arg)
131 if err != nil {
132 return a, err
133 }
134 a.Provider = v
135 case "--model":
136 v, err := want(&i, arg)
137 if err != nil {
138 return a, err
139 }
140 a.Model = v
141 case "--api-key":
142 v, err := want(&i, arg)
143 if err != nil {
144 return a, err
145 }
146 a.APIKey = v
147 case "--base-url":
148 v, err := want(&i, arg)
149 if err != nil {
150 return a, err
151 }
152 a.BaseURL = v
153 case "--system-prompt":
154 v, err := want(&i, arg)
155 if err != nil {
156 return a, err
157 }
158 a.SystemPrompt = v
159 case "--append-system-prompt":
160 v, err := want(&i, arg)
161 if err != nil {
162 return a, err
163 }
164 a.AppendSystemPrompt = append(a.AppendSystemPrompt, v)
165 case "--ext", "-e":
166 v, err := want(&i, arg)
167 if err != nil {
168 return a, err
169 }
170 // Repeatable; each value is a directory containing an
171 // extension.json. Resolved to absolute later so paths like
172 // "." survive a later cwd change.
173 a.Exts = append(a.Exts, v)
174 case "--no-ext", "--no-extensions":
175 a.NoExt = true
176 case "--no-skill", "--no-skills":
177 a.NoSkill = true
178 case "--with-skills", "--with-skill":
179 a.WithSkills = true
180 case "--no-yolo":
181 a.NoYolo = true
182 case "--reasoning":
183 v, err := want(&i, arg)
184 if err != nil {
185 return a, err
186 }
187 switch strings.ToLower(v) {
188 case "", "low", "medium", "high":
189 a.Reasoning = strings.ToLower(v)
190 default:
191 return a, fmt.Errorf("--reasoning must be low|medium|high")
192 }
193 case "--session":
194 v, err := want(&i, arg)
195 if err != nil {
196 return a, err
197 }
198 a.Session = v
199 case "--cwd":
200 v, err := want(&i, arg)
201 if err != nil {
202 return a, err
203 }
204 a.CWD = v
205 case "--tools":
206 v, err := want(&i, arg)
207 if err != nil {
208 return a, err
209 }
210 for _, t := range strings.Split(v, ",") {
211 t = strings.TrimSpace(t)
212 if t != "" {
213 a.Tools = append(a.Tools, t)
214 }
215 }
216 case "--max-steps":
217 v, err := want(&i, arg)
218 if err != nil {
219 return a, err
220 }
221 var n int
222 if _, err := fmt.Sscanf(v, "%d", &n); err != nil || n <= 0 {
223 return a, fmt.Errorf("--max-steps must be a positive integer")
224 }
225 a.MaxSteps = n
226 default:
227 if strings.HasPrefix(arg, "-") && arg != "-" {
228 return a, fmt.Errorf("unknown flag %q", arg)
229 }
230 positional = append(positional, arg)
231 }
232 }
233
234 if len(positional) > 0 {
235 a.Prompt = strings.Join(positional, " ")
236 }
237
238 if a.CWD == "" {
239 a.CWD, _ = os.Getwd()
240 }
241 return a, nil
242}
243
244// PrintHelp writes the help text to stderr. When stderr is a TTY it
245// uses the same palette as zot's TUI; when redirected it falls back to
246// plain text with no ANSI escapes.
247func PrintHelp(version string) {
248 th := tui.Dark
249 fd := int(os.Stderr.Fd())
250 useColor := term.IsTerminal(fd)
251 style := func(c int, s string) string {
252 if !useColor {
253 return s
254 }
255 return th.FG256(c, s)
256 }
257 assistant := func(s string) string { return style(th.Assistant, s) }
258 muted := func(s string) string { return style(th.Muted, s) }
259 fg := func(s string) string { return style(th.FG, s) }
260 width := 96
261 if useColor {
262 if w, _, err := term.GetSize(fd); err == nil && w > 20 {
263 width = w
264 }
265 }
266 ruleWidth := width
267 if ruleWidth < 40 {
268 ruleWidth = 40
269 }
270 rule := strings.Repeat("─", ruleWidth)
271 if useColor {
272 rule = muted(rule)
273 }
274 leftW := 34
275 if width >= 120 {
276 leftW = 40
277 }
278 if width >= 140 {
279 leftW = 46
280 }
281 type row struct{ left, right string }
282 section := func(title string, rows ...row) {
283 fmt.Fprintln(os.Stderr)
284 fmt.Fprintln(os.Stderr, assistant(title))
285 fmt.Fprintln(os.Stderr, rule)
286 narrow := width < 100
287 for _, r := range rows {
288 if narrow {
289 fmt.Fprintf(os.Stderr, " %s\n", fg(r.left))
290 fmt.Fprintf(os.Stderr, " %s\n", muted(r.right))
291 fmt.Fprintln(os.Stderr)
292 continue
293 }
294 left := r.left
295 if len([]rune(left)) < leftW {
296 left += strings.Repeat(" ", leftW-len([]rune(left)))
297 }
298 fmt.Fprintf(os.Stderr, " %s %s\n", fg(left), muted(r.right))
299 }
300 }
301
302 fmt.Fprintln(os.Stderr)
303 fmt.Fprintln(os.Stderr, assistant(tui.Bold("▌ i'm zot. yet another coding agent harness.")))
304 fmt.Fprintln(os.Stderr, muted("ask anything, or type /help inside the tui to see commands."))
305 fmt.Fprintf(os.Stderr, "%s %s\n", muted("version:"), fg(version))
306
307 section("modes",
308 row{"zot", "interactive tui"},
309 row{"zot \"prompt\"", "interactive, pre-filled prompt"},
310 row{"zot -p \"prompt\"", "print final text, exit"},
311 row{"zot --json \"prompt\"", "newline-delimited json events, exit"},
312 row{"zot rpc", "json-rpc loop on stdin/stdout (see docs/rpc.md)"},
313 )
314 section("extensions",
315 row{"zot ext list", "list installed extensions"},
316 row{"zot ext install <path|url>", "install into $ZOT_HOME/extensions/"},
317 row{"zot --ext ./path/to/ext", "load an extension for this run only"},
318 row{"zot ext help", "show all extension subcommands"},
319 )
320 section("telegram",
321 row{"zot telegram-bot setup", "configure a telegram bot (from BotFather)"},
322 row{"zot telegram-bot run", "foreground bridge (ctrl+c to stop)"},
323 row{"zot telegram-bot start", "background bridge (detached)"},
324 row{"zot telegram-bot stop", "stop the background bridge"},
325 row{"zot telegram-bot logs [-f]", "tail the background bridge log"},
326 row{"zot telegram-bot status", "config + running state"},
327 row{"zot telegram-bot reset", "forget saved token"},
328 row{"zot tg ...", "short alias for telegram-bot"},
329 )
330 section("provider and model flags",
331 row{"--provider", "provider to use (anthropic|openai|ollama)"},
332 row{"--model ID", "model id (see --list-models)"},
333 row{"--api-key KEY", "api key for this run (env / auth.json fallback)"},
334 row{"--base-url URL", "override provider api base url"},
335 row{"--reasoning low|medium|high", "enable reasoning on supported models"},
336 )
337 section("prompt and session flags",
338 row{"--system-prompt TEXT", "replace the default system prompt"},
339 row{"--append-system-prompt TEXT", "append to the system prompt (repeatable)"},
340 row{"-c, --continue", "continue the most recent session for this cwd"},
341 row{"-r, --resume", "pick a session to resume"},
342 row{"--session PATH", "resume a specific session file"},
343 row{"--no-session", "do not read or write a session file"},
344 )
345 section("workspace, tools, skills",
346 row{"--cwd PATH", "treat PATH as the working directory"},
347 row{"--no-tools", "disable all tools"},
348 row{"--tools csv", "only enable the listed tools"},
349 row{"--no-yolo", "ask before running every tool call"},
350 row{"--no-ext", "skip extension discovery for this run"},
351 row{"--no-skill", "skip all skill discovery for this run"},
352 row{"--with-skills", "load user-installed skills in addition to built-ins"},
353 )
354 section("misc",
355 row{"--max-steps N", "agent loop iteration cap (default 50)"},
356 row{"--list-models", "print known models and exit"},
357 row{"-h, --help", "show this help"},
358 row{"-v, --version", "show version info"},
359 )
360 fmt.Fprintln(os.Stderr)
361 fmt.Fprintln(os.Stderr, assistant("see also: docs/extensions.md, docs/rpc.md, docs/skills.md"))
362 fmt.Fprintln(os.Stderr)
363}