v0.15.0 6b178f8

args.go RawHistoryFolder

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

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