v0.2.7 ea58887

args.go RawHistoryFolder

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

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close