Skip to main content

pi/cli/
help.rs

1//! Help text for the `pi` CLI, matching coding-agent `printHelp`.
2
3use crate::core::config::{APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, ENV_SESSION_DIR};
4use std::fmt::Write as _;
5
6/// Extension CLI flag metadata rendered under the help "Extension CLI Flags" section.
7#[derive(Clone, Debug, Eq, PartialEq)]
8pub struct ExtensionFlagHelp {
9    /// Flag name without the leading `--`.
10    pub name: String,
11    /// Optional human description.
12    pub description: Option<String>,
13    /// Whether the flag expects a string value.
14    pub takes_value: bool,
15    /// Absolute or package-relative extension path used for the default description.
16    pub extension_path: String,
17}
18
19/// Options controlling help rendering.
20#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
21pub struct HelpStyle {
22    /// When true, bold section headers with ANSI styles.
23    pub styled: bool,
24}
25
26/// Format the full help text without writing to stdout.
27///
28/// When `style.styled` is false the output is plain text (tests / redirected
29/// consumers). When true, section headers use `anstyle` bold, matching chalk
30/// in the TypeScript `printHelp` path.
31#[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}