Skip to main content

ai_agent/tools/powershell/
prompt.rs

1// Source: /data/home/swei/claudecode/openclaudecode/src/tools/PowerShellTool/prompt.rs
2//! PowerShell tool prompt
3
4use crate::constants::env::ai;
5use crate::utils::env_utils::is_env_truthy;
6
7/// Get default timeout in milliseconds
8pub fn get_default_timeout_ms() -> u64 {
9    // Match BashTool's default timeout
10    120000 // 2 minutes
11}
12
13/// Get maximum timeout in milliseconds
14pub fn get_max_timeout_ms() -> u64 {
15    // Match BashTool's max timeout
16    600000 // 10 minutes
17}
18
19/// Check if background tasks are disabled
20pub fn is_background_tasks_disabled() -> bool {
21    is_env_truthy(std::env::var(ai::DISABLE_BACKGROUND_TASKS).ok().as_deref())
22}
23
24/// Get background usage note
25fn get_background_usage_note() -> Option<&'static str> {
26    if is_background_tasks_disabled() {
27        return None;
28    }
29    Some(
30        "  - You can use the `run_in_background` parameter to run the command in the background. Only use this if you don't need the result immediately and are OK being notified when the command completes later.",
31    )
32}
33
34/// Get sleep guidance
35fn get_sleep_guidance() -> Option<&'static str> {
36    if is_background_tasks_disabled() {
37        return None;
38    }
39    Some(
40        r#"  - Avoid unnecessary `Start-Sleep` commands:
41    - Do not sleep between commands that can run immediately — just run them.
42    - If your command is long running and you would like to be notified when it finishes — simply run your command using `run_in_background`. There is no need to sleep in this case.
43    - Do not retry failing commands in a sleep loop — diagnose the root cause or consider an alternative approach.
44    - If waiting for a background task you started with `run_in_background`, you will be notified when it completes — do not poll.
45    - If you must poll an external process, use a check command rather than sleeping first.
46    - If you must sleep, keep the duration short (1-5 seconds) to avoid blocking the user."#,
47    )
48}
49
50/// PowerShell edition
51#[derive(Debug, Clone, Copy, PartialEq)]
52pub enum PowerShellEdition {
53    Desktop, // Windows PowerShell 5.1
54    Core,    // PowerShell 7+
55}
56
57/// Get edition-specific section
58fn get_edition_section(edition: Option<PowerShellEdition>) -> String {
59    match edition {
60        Some(PowerShellEdition::Desktop) => {
61            "PowerShell edition: Windows PowerShell 5.1 (powershell.exe)
62   - Pipeline chain operators `&&` and `||` are NOT available — they cause a parser error. To run B only if A succeeds: `A; if ($?) { B }`. To chain unconditionally: `A; B`.
63   - Ternary (`?:`), null-coalescing (`??`), and null-conditional (`?.`) operators are NOT available. Use `if/else` and explicit `$null -eq` checks instead.
64   - Avoid `2>&1` on native executables. In 5.1, redirecting a native command's stderr inside PowerShell wraps each line in an ErrorRecord (NativeCommandError) and sets `$?` to `$false` even when the exe returned exit code 0. stderr is already captured for you — don't redirect it.
65   - Default file encoding is UTF-16 LE (with BOM). When writing files other tools will read, pass `-Encoding utf8` to `Out-File`/`Set-Content`.
66   - `ConvertFrom-Json` returns a PSCustomObject, not a hashtable. `-AsHashtable` is not available.".to_string()
67        }
68        Some(PowerShellEdition::Core) => {
69            "PowerShell edition: PowerShell 7+ (pwsh)
70   - Pipeline chain operators `&&` and `||` ARE available and work like bash. Prefer `cmd1 && cmd2` over `cmd1; cmd2` when cmd2 should only run if cmd1 succeeds.
71   - Ternary (`$cond ? $a : $b`), null-coalescing (`??`), and null-conditional (`?.`) operators are available.
72   - Default file encoding is UTF-8 without BOM.".to_string()
73        }
74        None => {
75            "PowerShell edition: unknown — assume Windows PowerShell 5.1 for compatibility
76   - Do NOT use `&&`, `||`, ternary `?:`, null-coalescing `??`, or null-conditional `?.`. These are PowerShell 7+ only and parser-error on 5.1.
77   - To chain commands conditionally: `A; if ($?) { B }`. Unconditionally: `A; B`.".to_string()
78        }
79    }
80}
81
82/// Get the full tool prompt
83pub async fn get_prompt() -> String {
84    let background_note = get_background_usage_note();
85    let sleep_guidance = get_sleep_guidance();
86
87    // Note: In Rust, we can't easily detect PowerShell edition at startup
88    // We'll return the unknown edition guidance by default
89    let edition_section = get_edition_section(None);
90
91    let mut prompt = format!(
92        r#"Executes a given PowerShell command with optional timeout. Working directory persists between commands; shell state (variables, functions) does not.
93
94IMPORTANT: This tool is for terminal operations via PowerShell: git, npm, docker, and PS cmdlets. DO NOT use it for file operations (reading, writing, editing, searching, finding files) - use the specialized tools for this instead.
95
96{}
97
98Before executing the command, please follow these steps:
99
1001. Directory Verification:
101   - If the command will create new directories or files, first use `Get-ChildItem` (or `ls`) to verify the parent directory exists and is the correct location
102
1032. Command Execution:
104   - Always quote file paths that contain spaces with double quotes
105   - Capture the output of the command.
106
107PowerShell Syntax Notes:
108   - Variables use $ prefix: $myVar = "value"
109   - Escape character is backtick (`), not backslash
110   - Use Verb-Noun cmdlet naming: Get-ChildItem, Set-Location, New-Item, Remove-Item
111   - Common aliases: ls (Get-ChildItem), cd (Set-Location), cat (Get-Content), rm (Remove-Item)
112   - Pipe operator | works similarly to bash but passes objects, not text
113   - Use Select-Object, Where-Object, ForEach-Object for filtering and transformation
114   - String interpolation: "Hello $name" or "Hello $($obj.Property)"
115   - Registry access uses PSDrive prefixes: `HKLM:\SOFTWARE\...`, `HKCU:\...` — NOT raw `HKEY_LOCAL_MACHINE\...`
116   - Environment variables: read with `$env:NAME`, set with `$env:NAME = "value"` (NOT `Set-Variable` or bash `export`)
117   - Call native exe with spaces in path via call operator: `& "C:\Program Files\App\app.exe" arg1 arg2`
118
119Interactive and blocking commands (will hang — this tool runs with -NonInteractive):
120   - NEVER use `Read-Host`, `Get-Credential`, `Out-GridView`, `$Host.UI.PromptForChoice`, or `pause`
121   - Destructive cmdlets (`Remove-Item`, `Stop-Process`, `Clear-Content`, etc.) may prompt for confirmation. Add `-Confirm:$false` when you intend the action to proceed. Use `-Force` for read-only/hidden items.
122   - Never use `git rebase -i`, `git add -i`, or other commands that open an interactive editor
123
124Passing multiline strings (commit messages, file content) to native executables:
125   - Use a single-quoted here-string so PowerShell does not expand `$` or backticks inside. The closing `'@` MUST be at column 0 (no leading whitespace) on its own line — indenting it is a parse error:
126<example>
127git commit -m @'
128Commit message here.
129Second line with $literal dollar signs.
130'@
131</example>
132   - Use `@'...'@` (single-quoted, literal) not `@"..."@` (double-quoted, interpolated) unless you need variable expansion
133   - For arguments containing `-`, `@`, or other characters PowerShell parses as operators, use the stop-parsing token: `git log --% --format=%H`
134
135Usage notes:
136  - The command argument is required.
137  - You can specify an optional timeout in milliseconds (up to {}ms / {} minutes). If not specified, commands will timeout after {}ms ({} minutes).
138  - It is very helpful if you write a clear, concise description of what this command does.
139  - If the output exceeds 30000 characters, output will be truncated before being returned to you.
140"#,
141        edition_section,
142        get_max_timeout_ms(),
143        get_max_timeout_ms() / 60000,
144        get_default_timeout_ms(),
145        get_default_timeout_ms() / 60000
146    );
147
148    if let Some(note) = background_note {
149        prompt.push_str(note);
150        prompt.push('\n');
151    }
152
153    prompt.push_str(
154        r#"  - Avoid using PowerShell to run commands that have dedicated tools, unless explicitly instructed:
155    - File search: Use Glob (NOT Get-ChildItem -Recurse)
156    - Content search: Use Grep (NOT Select-String)
157    - Read files: Use FileRead (NOT Get-Content)
158    - Edit files: Use FileEdit
159    - Write files: Use FileWrite (NOT Set-Content/Out-File)
160    - Communication: Output text directly (NOT Write-Output/Write-Host)
161  - When issuing multiple commands:
162    - If the commands are independent and can run in parallel, make multiple PowerShell tool calls in a single message.
163    - If the commands depend on each other and must run sequentially, chain them in a single PowerShell call (see edition-specific chaining syntax above).
164    - Use `;` only when you need to run commands sequentially but don't care if earlier commands fail.
165    - DO NOT use newlines to separate commands (newlines are ok in quoted strings and here-strings)
166  - Do NOT prefix commands with `cd` or `Set-Location` -- the working directory is already set to the correct project directory automatically.
167"#,
168    );
169
170    if let Some(guidance) = sleep_guidance {
171        prompt.push_str(guidance);
172        prompt.push('\n');
173    }
174
175    prompt.push_str(
176        "  - For git commands:\n\
177         - Prefer to create a new commit rather than amending an existing commit.\n\
178         - Before running destructive operations (e.g., git reset --hard, git push --force, git checkout --), consider whether there is a safer alternative that achieves the same goal. Only use destructive operations when they are truly the best approach.\n\
179         - Never skip hooks (--no-verify) or bypass signing (--no-gpg-sign, -c commit.gpgsign=false) unless the user has explicitly asked for it. If a hook fails, investigate and fix the underlying issue."
180    );
181
182    prompt
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_get_default_timeout_ms() {
191        let timeout = get_default_timeout_ms();
192        assert!(timeout > 0);
193        assert!(timeout <= get_max_timeout_ms());
194    }
195
196    #[test]
197    fn test_get_max_timeout_ms() {
198        let max = get_max_timeout_ms();
199        assert!(max > 0);
200    }
201
202    #[test]
203    fn test_default_less_than_max_timeout() {
204        assert!(get_default_timeout_ms() <= get_max_timeout_ms());
205    }
206
207    #[test]
208    fn test_default_timeout_reasonable() {
209        // Default should be between 30s and 5 minutes
210        let default = get_default_timeout_ms();
211        assert!(default >= 30_000);
212        assert!(default <= 300_000);
213    }
214
215    #[test]
216    fn test_max_timeout_reasonable() {
217        // Max should be at least 1 minute
218        let max = get_max_timeout_ms();
219        assert!(max >= 60_000);
220    }
221}