v0.3.69 6abb410

args.go RawHistoryFolder

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

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