v0.1.55 ea10cba

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

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