v0.1.1 7141aff

args.go RawHistoryFolder

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

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