1use crate::core::config::{APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR};
4use std::fmt::Write as _;
5
6#[derive(Clone, Debug, Eq, PartialEq)]
8pub struct ExtensionFlagHelp {
9 pub name: String,
11 pub description: Option<String>,
13 pub takes_value: bool,
15 pub extension_path: String,
17}
18
19#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
21pub struct HelpStyle {
22 pub styled: bool,
24}
25
26#[must_use]
32pub fn format_help(extension_flags: Option<&[ExtensionFlagHelp]>, style: HelpStyle) -> String {
33 let bold = if style.styled {
34 anstyle::Style::new().bold()
35 } else {
36 anstyle::Style::new()
37 };
38 let bold_s = bold.render();
39 let bold_e = bold.render_reset();
40
41 let extension_flags_text = format_extension_flags(extension_flags, style);
42
43 let env_agent = format!("{ENV_AGENT_DIR:<32}");
44 let env_session = format!("{ENV_SESSION_DIR:<32}");
45
46 format!(
47 "{bold_s}{APP_NAME}{bold_e} - AI coding assistant with read, bash, edit, write tools
48
49{bold_s}Usage:{bold_e}
50 {APP_NAME} [options] [@files...] [messages...]
51
52{bold_s}Commands:{bold_e}
53 {APP_NAME} install <source> [-l] Install extension source and add to settings
54 {APP_NAME} remove <source> [-l] Remove extension source from settings
55 {APP_NAME} uninstall <source> [-l] Alias for remove
56 {APP_NAME} update [source|self|pi] Update pi, extensions, or model catalogs
57 {APP_NAME} list List installed extensions from settings
58 {APP_NAME} config [-l] Open TUI to enable/disable package resources (Tab switches scope)
59 {APP_NAME} <command> --help Show help for install/remove/uninstall/update/list/config
60
61{bold_s}Options:{bold_e}
62 --provider <name> Provider name (default: google)
63 --model <pattern> Model pattern or ID (supports \"provider/id\" and optional \":<thinking>\")
64 --api-key <key> API key (defaults to env vars)
65 --system-prompt <text> System prompt (default: coding assistant prompt)
66 --append-system-prompt <text> Append text or file contents to the system prompt (can be used multiple times)
67 --mode <mode> Output mode: text (default), json, or rpc
68 --print, -p Non-interactive mode: process prompt and exit
69 --continue, -c Continue previous session
70 --resume, -r Select a session to resume
71 --session <path|id> Use specific session file or partial UUID
72 --session-id <id> Use exact project session ID, creating it if missing
73 --fork <path|id> Fork specific session file or partial UUID into a new session
74 --session-dir <dir> Directory for session storage and lookup
75 --no-session Don't save session (ephemeral)
76 --name, -n <name> Set session display name
77 --models <patterns> Comma-separated model patterns for Ctrl+P cycling
78 Supports globs (anthropic/*, *sonnet*) and fuzzy matching
79 --no-tools, -nt Disable all tools by default (built-in and extension)
80 --no-builtin-tools, -nbt Disable built-in tools by default but keep extension/custom tools enabled
81 --tools, -t <tools> Comma-separated allowlist of tool names to enable
82 Applies to built-in, extension, and custom tools
83 --exclude-tools, -xt <tools> Comma-separated denylist of tool names to disable
84 Applies to built-in, extension, and custom tools
85 --thinking <level> Set thinking level: off, minimal, low, medium, high, xhigh, max
86 --extension, -e <path> Load an extension file (can be used multiple times)
87 --no-extensions, -ne Disable extension discovery (explicit -e paths still work)
88 --skill <path> Load a skill file or directory (can be used multiple times)
89 --no-skills, -ns Disable skills discovery and loading
90 --prompt-template <path> Load a prompt template file or directory (can be used multiple times)
91 --no-prompt-templates, -np Disable prompt template discovery and loading
92 --theme <path> Load a theme file or directory (can be used multiple times)
93 --no-themes Disable theme discovery and loading
94 --no-context-files, -nc Disable AGENTS.md and CLAUDE.md discovery and loading
95 --export <file> Export session file to HTML and exit
96 --list-models [search] List available models (with optional fuzzy search)
97 --verbose Force verbose startup (overrides quietStartup setting)
98 --approve, -a Trust project-local files for this run
99 --no-approve, -na Ignore project-local files for this run
100 --offline Disable startup network operations (same as PI_OFFLINE=1)
101 --help, -h Show this help
102 --version, -v Show version number
103
104Extensions can register additional flags (e.g., --plan from plan-mode extension).{extension_flags_text}
105
106{bold_s}Examples:{bold_e}
107 # Interactive mode
108 {APP_NAME}
109
110 # Interactive mode with initial prompt
111 {APP_NAME} \"List all .ts files in src/\"
112
113 # Include files in initial message
114 {APP_NAME} @prompt.md @image.png \"What color is the sky?\"
115
116 # Non-interactive mode (process and exit)
117 {APP_NAME} -p \"List all .ts files in src/\"
118
119 # Multiple messages (interactive)
120 {APP_NAME} \"Read package.json\" \"What dependencies do we have?\"
121
122 # Continue previous session
123 {APP_NAME} --continue \"What did we discuss?\"
124
125 # Start a named session
126 {APP_NAME} --name \"Refactor auth module\"
127
128 # Use different model
129 {APP_NAME} --provider openai --model gpt-4o-mini \"Help me refactor this code\"
130
131 # Use model with provider prefix (no --provider needed)
132 {APP_NAME} --model openai/gpt-4o \"Help me refactor this code\"
133
134 # Use model with thinking level shorthand
135 {APP_NAME} --model sonnet:high \"Solve this complex problem\"
136
137 # Limit model cycling to specific models
138 {APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o
139
140 # Limit to a specific provider with glob pattern
141 {APP_NAME} --models \"github-copilot/*\"
142
143 # Cycle models with fixed thinking levels
144 {APP_NAME} --models sonnet:high,haiku:low
145
146 # Start with a specific thinking level
147 {APP_NAME} --thinking high \"Solve this complex problem\"
148
149 # Read-only mode (no file modifications possible)
150 {APP_NAME} --tools read,grep,find,ls -p \"Review the code in src/\"
151
152 # Disable one tool while keeping the rest available
153 {APP_NAME} --exclude-tools ask_question
154
155 # Export a session file to HTML
156 {APP_NAME} --export ~/{CONFIG_DIR_NAME}/agent/sessions/--path--/session.jsonl
157 {APP_NAME} --export session.jsonl output.html
158
159{bold_s}Environment Variables:{bold_e}
160 ANTHROPIC_API_KEY - Anthropic Claude API key
161 ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth token (alternative to API key)
162 ANT_LING_API_KEY - Ant Ling API key
163 OPENAI_API_KEY - OpenAI GPT API key
164 AZURE_OPENAI_API_KEY - Azure OpenAI API key
165 AZURE_OPENAI_BASE_URL - Azure OpenAI/Cognitive Services base URL (e.g. https://{{resource}}.openai.azure.com)
166 AZURE_OPENAI_RESOURCE_NAME - Azure OpenAI resource name (alternative to base URL)
167 AZURE_OPENAI_API_VERSION - Azure OpenAI API version (default: v1)
168 AZURE_OPENAI_DEPLOYMENT_NAME_MAP - Azure OpenAI model=deployment map (comma-separated)
169 DEEPSEEK_API_KEY - DeepSeek API key
170 NVIDIA_API_KEY - NVIDIA NIM API key
171 GEMINI_API_KEY - Google Gemini API key
172 GROQ_API_KEY - Groq API key
173 CEREBRAS_API_KEY - Cerebras API key
174 XAI_API_KEY - xAI Grok API key
175 FIREWORKS_API_KEY - Fireworks API key
176 TOGETHER_API_KEY - Together AI API key
177 OPENROUTER_API_KEY - OpenRouter API key
178 AI_GATEWAY_API_KEY - Vercel AI Gateway API key
179 ZAI_API_KEY - ZAI Coding Plan API key (Global)
180 ZAI_CODING_CN_API_KEY - ZAI Coding Plan API key (China)
181 MISTRAL_API_KEY - Mistral API key
182 MINIMAX_API_KEY - MiniMax API key
183 MOONSHOT_API_KEY - Moonshot AI API key
184 OPENCODE_API_KEY - OpenCode Zen/OpenCode Go API key
185 KIMI_API_KEY - Kimi For Coding API key
186 CLOUDFLARE_API_KEY - Cloudflare API token (Workers AI and AI Gateway)
187 CLOUDFLARE_ACCOUNT_ID - Cloudflare account id (required for both)
188 CLOUDFLARE_GATEWAY_ID - Cloudflare AI Gateway slug (required for AI Gateway)
189 XIAOMI_API_KEY - Xiaomi MiMo API key (api.xiaomimimo.com billing)
190 XIAOMI_TOKEN_PLAN_CN_API_KEY - Xiaomi MiMo Token Plan API key (China region)
191 XIAOMI_TOKEN_PLAN_AMS_API_KEY - Xiaomi MiMo Token Plan API key (Amsterdam region)
192 XIAOMI_TOKEN_PLAN_SGP_API_KEY - Xiaomi MiMo Token Plan API key (Singapore region)
193 AWS_PROFILE - AWS profile for Amazon Bedrock
194 AWS_ACCESS_KEY_ID - AWS access key for Amazon Bedrock
195 AWS_SECRET_ACCESS_KEY - AWS secret key for Amazon Bedrock
196 AWS_BEARER_TOKEN_BEDROCK - Bedrock API key (bearer token)
197 AWS_REGION - AWS region for Amazon Bedrock (e.g., us-east-1)
198 {env_agent} - Config directory (default: ~/{CONFIG_DIR_NAME}/agent)
199 {env_session} - Session storage directory (overridden by --session-dir)
200 PI_PACKAGE_DIR - Override package directory (for Nix/Guix store paths)
201 PI_OFFLINE - Disable startup network operations when set to 1/true/yes
202 PI_TELEMETRY - Override install telemetry when set to 1/true/yes or 0/false/no
203 PI_SHARE_VIEWER_URL - Base URL for /share command (default: https://pi.dev/session/)
204
205{bold_s}Built-in Tool Names:{bold_e}
206 read - Read file contents
207 bash - Execute bash commands
208 edit - Edit files with find/replace
209 write - Write files (creates/overwrites)
210 grep - Search file contents (read-only, off by default)
211 find - Find files by glob pattern (read-only, off by default)
212 ls - List directory contents (read-only, off by default)
213"
214 )
215}
216
217fn format_extension_flags(
218 extension_flags: Option<&[ExtensionFlagHelp]>,
219 style: HelpStyle,
220) -> String {
221 let Some(flags) = extension_flags else {
222 return String::new();
223 };
224 if flags.is_empty() {
225 return String::new();
226 }
227
228 let bold = if style.styled {
229 anstyle::Style::new().bold()
230 } else {
231 anstyle::Style::new()
232 };
233 let bold_s = bold.render();
234 let bold_e = bold.render_reset();
235
236 let mut lines = String::new();
237 lines.push('\n');
238 let Ok(()) = writeln!(lines, "{bold_s}Extension CLI Flags:{bold_e}") else {
239 return lines;
240 };
241 for flag in flags {
242 let value = if flag.takes_value { " <value>" } else { "" };
243 let left = format!(" --{}{value}", flag.name);
244 let description = flag
245 .description
246 .clone()
247 .unwrap_or_else(|| format!("Registered by {}", flag.extension_path));
248 let padded = if left.len() < 30 {
249 format!("{left:<30}")
250 } else {
251 left
252 };
253 lines.push_str(&padded);
254 lines.push_str(&description);
255 lines.push('\n');
256 }
257 lines
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn plain_help_contains_required_sections() {
266 let text = format_help(None, HelpStyle { styled: false });
267 assert!(text.starts_with("pi - AI coding assistant with read, bash, edit, write tools"));
268 assert!(text.contains("Usage:"));
269 assert!(text.contains(" pi [options] [@files...] [messages...]"));
270 assert!(text.contains("Commands:"));
271 assert!(text.contains("Options:"));
272 assert!(text.contains("--list-models [search]"));
273 assert!(text.contains("--thinking <level>"));
274 assert!(text.contains("Examples:"));
275 assert!(text.contains("Environment Variables:"));
276 assert!(text.contains("PI_CODING_AGENT_DIR"));
277 assert!(text.contains("PI_CODING_AGENT_SESSION_DIR"));
278 assert!(text.contains("Built-in Tool Names:"));
279 assert!(text.contains(" read - Read file contents"));
280 assert!(text.contains(" ls - List directory contents (read-only, off by default)"));
281 assert!(!text.contains("Extension CLI Flags:"));
282 assert!(!text.contains('\u{1b}'));
283 }
284
285 #[test]
286 fn styled_help_uses_ansi_bold_on_headers() {
287 let text = format_help(None, HelpStyle { styled: true });
288 assert!(text.contains("\u{1b}[1mpi\u{1b}[0m") || text.contains("\u{1b}[1mpi\u{1b}[m"));
289 assert!(text.contains("Usage:"));
290 assert!(text.contains('\u{1b}'));
291 }
292
293 #[test]
294 fn extension_flags_section_is_optional_and_padded() {
295 let flags = [
296 ExtensionFlagHelp {
297 name: "plan".to_owned(),
298 description: Some("Enable plan mode".to_owned()),
299 takes_value: false,
300 extension_path: "/tmp/plan.ts".to_owned(),
301 },
302 ExtensionFlagHelp {
303 name: "depth".to_owned(),
304 description: None,
305 takes_value: true,
306 extension_path: "/tmp/depth.ts".to_owned(),
307 },
308 ];
309 let text = format_help(Some(&flags), HelpStyle { styled: false });
310 assert!(text.contains("Extension CLI Flags:"));
311 assert!(text.contains(" --plan"));
312 assert!(text.contains("Enable plan mode"));
313 assert!(text.contains(" --depth <value>"));
314 assert!(text.contains("Registered by /tmp/depth.ts"));
315
316 let empty = format_help(Some(&[]), HelpStyle { styled: false });
317 assert!(!empty.contains("Extension CLI Flags:"));
318 }
319
320 #[test]
321 fn export_examples_use_config_dir_name() {
322 let text = format_help(None, HelpStyle::default());
323 assert!(text.contains("--export ~/.pi/agent/sessions/--path--/session.jsonl"));
324 assert!(text.contains("--export session.jsonl output.html"));
325 }
326}