v0.21.0 cd9706d

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

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