v0.11.1 ab30a57

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

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