v0.27.0 01c3d31

args.go RawHistoryFolder

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

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