v0.2.33 94ece7d

args.go RawHistoryFolder

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

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