diff --git a/packages/opencode/src/tool/shell/bash.ts b/packages/opencode/src/tool/shell/bash.ts index 3539f18482..95fc524a01 100644 --- a/packages/opencode/src/tool/shell/bash.ts +++ b/packages/opencode/src/tool/shell/bash.ts @@ -1,7 +1,13 @@ import { createShellTool } from "./util" -export const BashTool = createShellTool( - "bash", - "Bash", - "use a single Bash call with '&&' to chain them together (e.g., `git add . && git commit -m \"message\" && git push`).", -) +export const BashTool = createShellTool({ + id: "bash", + shellName: "Bash", + chaining: + "use a single Bash call with '&&' to chain them together (e.g., `git add . && git commit -m \"message\" && git push`).", + guidance: `# Bash shell notes +- This is a POSIX-compatible shell. Standard Unix conventions apply. +- Use double quotes for variable interpolation, single quotes for literal strings. +- Use \`$(...)\` for command substitution (not backticks). +- Redirect stderr with \`2>&1\` or \`2>/dev/null\`.`, +}) diff --git a/packages/opencode/src/tool/shell/powershell.ts b/packages/opencode/src/tool/shell/powershell.ts index 0910ec1347..0df02aee49 100644 --- a/packages/opencode/src/tool/shell/powershell.ts +++ b/packages/opencode/src/tool/shell/powershell.ts @@ -1,7 +1,17 @@ import { createShellTool } from "./util" -export const PowershellTool = createShellTool( - "powershell", - "Windows PowerShell", - "avoid '&&' in this shell because Windows PowerShell 5.1 does not support it. Use PowerShell conditionals such as `cmd1; if ($?) { cmd2 }`", -) +export const PowershellTool = createShellTool({ + id: "powershell", + shellName: "Windows PowerShell 5.1", + chaining: + "avoid '&&' in this shell because Windows PowerShell 5.1 does not support it. Use PowerShell conditionals such as `cmd1; if ($?) { cmd2 }` when later commands must depend on earlier success.", + guidance: `# Windows PowerShell 5.1 shell notes +- This is Windows PowerShell 5.1 (legacy), NOT PowerShell 7+. It does NOT support \`&&\` or \`||\` pipeline chain operators. +- For conditional chaining use: \`cmd1; if ($?) { cmd2 }\` +- Use double quotes for interpolated strings (\`"Hello $name"\`), single quotes for verbatim strings. +- Cmdlets use Verb-Noun naming (e.g., \`Get-ChildItem\`, \`Set-Content\`). Common aliases like \`ls\`, \`cat\`, \`rm\` resolve to cmdlets with different behavior than Unix equivalents. +- Use \`$(...)\` for subexpressions. Use \`@(...)\` for array expressions. +- To call a native executable whose path contains spaces, use the call operator: \`& "path/to/exe" args\`. +- Escape special characters with backtick (\\\`) not backslash. +- Some modern PowerShell features (ternary operator, null-coalescing, etc.) are NOT available in 5.1.`, +}) diff --git a/packages/opencode/src/tool/shell/pwsh.ts b/packages/opencode/src/tool/shell/pwsh.ts index d22dc2bd70..cff2cd702a 100644 --- a/packages/opencode/src/tool/shell/pwsh.ts +++ b/packages/opencode/src/tool/shell/pwsh.ts @@ -1,7 +1,15 @@ import { createShellTool } from "./util" -export const PwshTool = createShellTool( - "pwsh", - "PowerShell Core", - "use a single PowerShell call with '&&' to chain them together (e.g., `git add . && git commit -m \"message\" && git push`).", -) +export const PwshTool = createShellTool({ + id: "pwsh", + shellName: "PowerShell 7+", + chaining: + "use a single PowerShell call with '&&' to chain them together (e.g., `git add . && git commit -m \"message\" && git push`).", + guidance: `# PowerShell 7+ (pwsh) shell notes +- This is PowerShell 7+ (Core), a cross-platform shell. It supports pipeline chain operators (\`&&\` and \`||\`). +- Use double quotes for interpolated strings (\`"Hello $name"\`), single quotes for verbatim strings. +- Cmdlets use Verb-Noun naming (e.g., \`Get-ChildItem\`, \`Set-Content\`). Common aliases like \`ls\`, \`cat\`, \`rm\` are available but resolve to cmdlets. +- Use \`$(...)\` for subexpressions. Use \`@(...)\` for array expressions. +- To call a native executable whose path contains spaces, use the call operator: \`& "path/to/exe" args\`. +- Escape special characters with backtick (\\\`) not backslash.`, +}) diff --git a/packages/opencode/src/tool/shell/shell.txt b/packages/opencode/src/tool/shell/shell.txt index 7ef2ca73fb..f6454cab8f 100644 --- a/packages/opencode/src/tool/shell/shell.txt +++ b/packages/opencode/src/tool/shell/shell.txt @@ -6,6 +6,8 @@ All commands run in ${directory} by default. Use the \`workdir\` parameter if yo IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO NOT use it for file operations (reading, writing, editing, searching, finding files) - use the specialized tools for this instead. +${guidance} + Before executing the command, please follow these steps: 1. Directory Verification: diff --git a/packages/opencode/src/tool/shell/util.ts b/packages/opencode/src/tool/shell/util.ts index 335fe3432f..dbd78a192e 100644 --- a/packages/opencode/src/tool/shell/util.ts +++ b/packages/opencode/src/tool/shell/util.ts @@ -80,13 +80,17 @@ export async function resolvePath(text: string, root: string, shell: string) { return path.resolve(root, text) } -export function formatShellDescription(template: string, opts: { name: string; shellName: string; chaining: string }) { +export function formatShellDescription( + template: string, + opts: { name: string; shellName: string; chaining: string; guidance: string }, +) { return template .replaceAll("${directory}", Instance.directory) .replaceAll("${os}", process.platform) .replaceAll("${shell}", opts.name) .replaceAll("${shellName}", opts.shellName) .replaceAll("${chaining}", opts.chaining) + .replaceAll("${guidance}", opts.guidance) .replaceAll("${maxLines}", String(Truncate.MAX_LINES)) .replaceAll("${maxBytes}", String(Truncate.MAX_BYTES)) } @@ -102,16 +106,21 @@ export type ShellType = "bash" | "pwsh" | "powershell" const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000 -export function createShellTool(id: ShellType, shellName: string, chaining: string) { - const log = Log.create({ service: `${id}-tool` }) +export function createShellTool(opts: { id: ShellType; shellName: string; chaining: string; guidance: string }) { + const log = Log.create({ service: `${opts.id}-tool` }) - return Tool.define(id, async () => { + return Tool.define(opts.id, async () => { const shell = Shell.acceptable() const name = Shell.name(shell) - log.info(`${id} tool using shell`, { shell, name }) + log.info(`${opts.id} tool using shell`, { shell, name }) return { - description: formatShellDescription(DESCRIPTION, { name, shellName, chaining }), + description: formatShellDescription(DESCRIPTION, { + name, + shellName: opts.shellName, + chaining: opts.chaining, + guidance: opts.guidance, + }), parameters: z.object({ command: z.string().describe("The command to execute"), timeout: z.number().describe("Optional timeout in milliseconds").optional(), @@ -138,11 +147,11 @@ export function createShellTool(id: ShellType, shellName: string, chaining: stri command: params.command, cwd, shell, - shellType: id, + shellType: opts.id, }) if (!Instance.containsPath(cwd)) scan.dirs.add(cwd) - await askPermission(ctx, scan, id) + await askPermission(ctx, scan, opts.id) return ShellRunner.run( {