v0.48.1 deaab59

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

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