v0.3.6 4657b32

args.go RawHistoryFolder

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

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