v0.1.82 25e7da2

args.go RawHistoryFolder

1package agent
2
3import (
4	"fmt"
5	"os"
6	"strings"
7
8	"github.com/patriceckhart/zot/internal/tui"
9	"golang.org/x/term"
10)
11
12// Mode is the CLI run mode.
13type Mode string
14
15const (
16	ModeInteractive Mode = "interactive"
17	ModePrint       Mode = "print"
18	ModeJSON        Mode = "json"
19	ModeRPC         Mode = "rpc"
20	// ModeSwarmAgent is the long-lived, headless daemon mode used by
21	// swarm-spawned agents. The binary opens a unix-socket inbox at
22	// the path provided by --swarm-agent, reads supervisor messages
23	// off it ("user ...", "cancel", "shutdown"), runs each user turn
24	// against a persistent session, and streams JSONL events on
25	// stdout. See internal/swarm/inbox.go for the wire protocol.
26	ModeSwarmAgent Mode = "swarm-agent"
27)
28
29// Args holds parsed command-line options.
30type Args struct {
31	Mode     Mode
32	Provider string
33	Model    string
34	APIKey   string
35
36	BaseURL            string // override provider base URL (for tests/self-hosted)
37	SystemPrompt       string
38	AppendSystemPrompt []string
39	Reasoning          string
40
41	Continue bool
42	Resume   bool
43	Session  string
44	NoSess   bool
45
46	CWD      string
47	NoTools  bool
48	Tools    []string
49	MaxSteps int
50
51	// Exts is a list of directory paths the user passed via --ext.
52	// Each must contain an extension.json. Loaded for one session
53	// only; never persisted. Take precedence over installed exts of
54	// the same name.
55	Exts []string
56
57	// NoExt disables extension discovery + spawn entirely for this
58	// run. --ext PATH still works (explicit beats implicit) so you
59	// can run "with only this one extension" via --no-ext --ext PATH.
60	NoExt bool
61
62	// NoSkill disables ALL skill discovery for this run, including
63	// the built-in skills compiled into the binary. The system
64	// prompt loses its "Available skills" manifest and the `skill`
65	// tool isn't registered. Useful for running zot without any
66	// extra context biasing the model.
67	NoSkill bool
68
69	// WithSkills opts into loading user-installed skills from
70	// $ZOT_HOME/skills/, .zot/skills/, .claude/skills/, and
71	// .agents/skills/. Without this flag only the built-in skills
72	// shipped with the zot binary are available, so a fresh install
73	// has a deterministic skill set regardless of what's lying
74	// around in the user's home directory.
75	WithSkills bool
76
77	// NoYolo turns on per-tool confirmation. Before each tool
78	// invocation the TUI prompts the user with the tool name + args
79	// and waits for an explicit yes/no. The user can also pick
80	// "always for this tool this session" or "always for anything
81	// this session" to stop being prompted again. Defaults off
82	// (yolo mode): tools run without asking.
83	//
84	// No effect in -p / --json / rpc modes, which have no
85	// interactive prompt. A warning is printed to stderr on startup
86	// so scripts know the flag is ignored, but tools still run
87	// freely so automated workflows keep working.
88	NoYolo bool
89
90	ListModels bool
91	Help       bool
92	Version    bool
93
94	Prompt string // concatenated positional args
95
96	// SwarmAgent is the inbox-socket path when this process is a
97	// swarm-spawned agent. Empty in every other mode. Set by
98	// --swarm-agent <path>; presence flips Mode to ModeSwarmAgent.
99	SwarmAgent string
100}
101
102// ParseArgs parses the process arguments (excluding argv[0]).
103func ParseArgs(in []string) (Args, error) {
104	a := Args{Mode: ModeInteractive, MaxSteps: 0}
105	positional := []string{}
106
107	want := func(i *int, flag string) (string, error) {
108		*i++
109		if *i >= len(in) {
110			return "", fmt.Errorf("%s requires a value", flag)
111		}
112		return in[*i], nil
113	}
114
115	for i := 0; i < len(in); i++ {
116		arg := in[i]
117		switch arg {
118		case "-h", "--help":
119			a.Help = true
120		case "-v", "--version":
121			a.Version = true
122		case "-p", "--print":
123			a.Mode = ModePrint
124		case "--json":
125			a.Mode = ModeJSON
126		case "--rpc":
127			a.Mode = ModeRPC
128		case "-c", "--continue":
129			a.Continue = true
130		case "-r", "--resume":
131			a.Resume = true
132		case "--no-session":
133			a.NoSess = true
134		case "--no-tools":
135			a.NoTools = true
136		case "--list-models":
137			a.ListModels = true
138		case "--experimental-oauth":
139			// deprecated: subscription login is always available.
140			// accepted silently for backwards compatibility.
141		case "--provider":
142			v, err := want(&i, arg)
143			if err != nil {
144				return a, err
145			}
146			a.Provider = v
147		case "--model":
148			v, err := want(&i, arg)
149			if err != nil {
150				return a, err
151			}
152			a.Model = v
153		case "--api-key":
154			v, err := want(&i, arg)
155			if err != nil {
156				return a, err
157			}
158			a.APIKey = v
159		case "--base-url":
160			v, err := want(&i, arg)
161			if err != nil {
162				return a, err
163			}
164			a.BaseURL = v
165		case "--system-prompt":
166			v, err := want(&i, arg)
167			if err != nil {
168				return a, err
169			}
170			a.SystemPrompt = v
171		case "--append-system-prompt":
172			v, err := want(&i, arg)
173			if err != nil {
174				return a, err
175			}
176			a.AppendSystemPrompt = append(a.AppendSystemPrompt, v)
177		case "--ext", "-e":
178			v, err := want(&i, arg)
179			if err != nil {
180				return a, err
181			}
182			// Repeatable; each value is a directory containing an
183			// extension.json. Resolved to absolute later so paths like
184			// "." survive a later cwd change.
185			a.Exts = append(a.Exts, v)
186		case "--no-ext", "--no-extensions":
187			a.NoExt = true
188		case "--no-skill", "--no-skills":
189			a.NoSkill = true
190		case "--with-skills", "--with-skill":
191			a.WithSkills = true
192		case "--no-yolo":
193			a.NoYolo = true
194		case "--reasoning":
195			v, err := want(&i, arg)
196			if err != nil {
197				return a, err
198			}
199			switch strings.ToLower(v) {
200			case "", "low", "medium", "high":
201				a.Reasoning = strings.ToLower(v)
202			default:
203				return a, fmt.Errorf("--reasoning must be low|medium|high")
204			}
205		case "--session":
206			v, err := want(&i, arg)
207			if err != nil {
208				return a, err
209			}
210			a.Session = v
211		case "--cwd":
212			v, err := want(&i, arg)
213			if err != nil {
214				return a, err
215			}
216			a.CWD = v
217		case "--swarm-agent":
218			v, err := want(&i, arg)
219			if err != nil {
220				return a, err
221			}
222			a.SwarmAgent = v
223			a.Mode = ModeSwarmAgent
224		case "--tools":
225			v, err := want(&i, arg)
226			if err != nil {
227				return a, err
228			}
229			for _, t := range strings.Split(v, ",") {
230				t = strings.TrimSpace(t)
231				if t != "" {
232					a.Tools = append(a.Tools, t)
233				}
234			}
235		case "--max-steps":
236			v, err := want(&i, arg)
237			if err != nil {
238				return a, err
239			}
240			var n int
241			if _, err := fmt.Sscanf(v, "%d", &n); err != nil || n <= 0 {
242				return a, fmt.Errorf("--max-steps must be a positive integer")
243			}
244			a.MaxSteps = n
245		default:
246			if strings.HasPrefix(arg, "-") && arg != "-" {
247				return a, fmt.Errorf("unknown flag %q", arg)
248			}
249			positional = append(positional, arg)
250		}
251	}
252
253	if len(positional) > 0 {
254		a.Prompt = strings.Join(positional, " ")
255	}
256
257	if a.CWD == "" {
258		a.CWD, _ = os.Getwd()
259	}
260	return a, nil
261}
262
263// PrintHelp writes the help text to stderr. When stderr is a TTY it
264// uses the same palette as zot's TUI; when redirected it falls back to
265// plain text with no ANSI escapes.
266func PrintHelp(version string) {
267	th := tui.Dark
268	fd := int(os.Stderr.Fd())
269	useColor := term.IsTerminal(fd)
270	style := func(c int, s string) string {
271		if !useColor {
272			return s
273		}
274		return th.FG256(c, s)
275	}
276	assistant := func(s string) string { return style(th.Assistant, s) }
277	muted := func(s string) string { return style(th.Muted, s) }
278	fg := func(s string) string { return style(th.FG, s) }
279	width := 96
280	if useColor {
281		if w, _, err := term.GetSize(fd); err == nil && w > 20 {
282			width = w
283		}
284	}
285	ruleWidth := width
286	if ruleWidth < 40 {
287		ruleWidth = 40
288	}
289	rule := strings.Repeat("─", ruleWidth)
290	if useColor {
291		rule = muted(rule)
292	}
293	leftW := 34
294	if width >= 120 {
295		leftW = 40
296	}
297	if width >= 140 {
298		leftW = 46
299	}
300	type row struct{ left, right string }
301	section := func(title string, rows ...row) {
302		fmt.Fprintln(os.Stderr)
303		fmt.Fprintln(os.Stderr, assistant(title))
304		fmt.Fprintln(os.Stderr, rule)
305		narrow := width < 100
306		for _, r := range rows {
307			if narrow {
308				fmt.Fprintf(os.Stderr, "  %s\n", fg(r.left))
309				fmt.Fprintf(os.Stderr, "    %s\n", muted(r.right))
310				fmt.Fprintln(os.Stderr)
311				continue
312			}
313			left := r.left
314			if len([]rune(left)) < leftW {
315				left += strings.Repeat(" ", leftW-len([]rune(left)))
316			}
317			fmt.Fprintf(os.Stderr, "  %s    %s\n", fg(left), muted(r.right))
318		}
319	}
320
321	fmt.Fprintln(os.Stderr)
322	var headline string
323	if useColor {
324		headline = th.AccentBar(th.Assistant) + assistant(tui.Bold("i'm zot. yet another coding agent harness."))
325	} else {
326		headline = "i'm zot. yet another coding agent harness."
327	}
328	fmt.Fprintln(os.Stderr, headline)
329	fmt.Fprintln(os.Stderr, muted("ask anything, or type /help inside the tui to see commands."))
330	fmt.Fprintf(os.Stderr, "%s %s\n", muted("version:"), fg(version))
331
332	section("modes",
333		row{"zot", "interactive tui"},
334		row{"zot \"prompt\"", "interactive, pre-filled prompt"},
335		row{"zot -p \"prompt\"", "print final text, exit"},
336		row{"zot --json \"prompt\"", "newline-delimited json events, exit"},
337		row{"zot rpc", "json-rpc loop on stdin/stdout (see docs/rpc.md)"},
338	)
339	section("extensions",
340		row{"zot ext list", "list installed extensions"},
341		row{"zot ext install <path|url>", "install into $ZOT_HOME/extensions/"},
342		row{"zot --ext ./path/to/ext", "load an extension for this run only"},
343		row{"zot ext help", "show all extension subcommands"},
344	)
345	section("self-update",
346		row{"zot update", "download and install the latest release"},
347		row{"zot update --check", "show whether a new release is available"},
348	)
349	section("telegram",
350		row{"zot telegram-bot setup", "configure a telegram bot (from BotFather)"},
351		row{"zot telegram-bot run", "foreground bridge (ctrl+c to stop)"},
352		row{"zot telegram-bot start", "background bridge (detached)"},
353		row{"zot telegram-bot stop", "stop the background bridge"},
354		row{"zot telegram-bot logs [-f]", "tail the background bridge log"},
355		row{"zot telegram-bot status", "config + running state"},
356		row{"zot telegram-bot reset", "forget saved token"},
357		row{"zot tg ...", "short alias for telegram-bot"},
358	)
359	section("provider and model flags",
360		row{"--provider", "provider to use (anthropic|openai|kimi|deepseek|google|ollama)"},
361		row{"--model ID", "model id (see --list-models)"},
362		row{"--api-key KEY", "api key for this run (env / auth.json fallback)"},
363		row{"--base-url URL", "override provider api base url"},
364		row{"--reasoning low|medium|high", "enable reasoning on supported models"},
365	)
366	section("prompt and session flags",
367		row{"--system-prompt TEXT", "replace the default system prompt"},
368		row{"--append-system-prompt TEXT", "append to the system prompt (repeatable)"},
369		row{"-c, --continue", "continue the most recent session for this cwd"},
370		row{"-r, --resume", "pick a session to resume"},
371		row{"--session PATH", "resume a specific session file"},
372		row{"--no-session", "do not read or write a session file"},
373	)
374	section("workspace, tools, skills",
375		row{"--cwd PATH", "treat PATH as the working directory"},
376		row{"--no-tools", "disable all tools"},
377		row{"--tools csv", "only enable the listed tools"},
378		row{"--no-yolo", "ask before running every tool call"},
379		row{"--no-ext", "skip extension discovery for this run"},
380		row{"--no-skill", "skip all skill discovery for this run"},
381		row{"--with-skills", "load user-installed skills in addition to built-ins"},
382	)
383	section("misc",
384		row{"--max-steps N", "agent loop iteration cap (default: unlimited)"},
385		row{"--list-models", "print known models and exit"},
386		row{"-h, --help", "show this help"},
387		row{"-v, --version", "show version info"},
388	)
389	fmt.Fprintln(os.Stderr)
390	fmt.Fprintln(os.Stderr, assistant("see also: docs/extensions.md, docs/rpc.md, docs/skills.md"))
391	fmt.Fprintln(os.Stderr)
392}

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