v0.50.1 a637010

args.go RawHistoryFolder

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

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