v0.3.30 2501d48

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

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