Public/Invoke-PSClaudeCode.ps1
|
<#
.SYNOPSIS Invokes Claude Code, an AI-powered PowerShell agent that can perform tasks using structured tools. .DESCRIPTION Invoke-PSClaudeCode uses Anthropic's Claude AI model to execute tasks by leveraging various tools including file reading/writing, command execution, and sub-agent delegation. The function supports pipeline input and provides safety checks for potentially dangerous operations. .PARAMETER Task The task description for the AI agent to perform. If not provided and pipeline input exists, the piped content becomes the task. .PARAMETER InputObject Accepts pipeline input that can be used as part of the task description. .PARAMETER Model The Claude model to use. Defaults to "claude-sonnet-4-5-20250929". .PARAMETER dangerouslySkipPermissions Switch to skip permission prompts for potentially dangerous operations. Use with caution. .EXAMPLE PS> Invoke-PSClaudeCode "Create a new file called 'test.txt' with the content 'Hello, World!'" This example instructs the AI agent to create a file with specific content. .EXAMPLE PS> Get-Content "data.txt" | Invoke-PSClaudeCode "Analyze this data and create a summary report" This example pipes file content to the function for analysis. .EXAMPLE PS> Invoke-PSClaudeCode -Task "List all files in the current directory" -dangerouslySkipPermissions This example runs a command without permission prompts. .NOTES Requires ANTHROPIC_API_KEY environment variable to be set. The function includes safety checks for file operations and command execution. #> function Invoke-PSClaudeCode { [CmdletBinding()] param( [Parameter(Position = 0)] [string]$Task, [Parameter(ValueFromPipeline = $true)] [object]$InputObject, [string]$Model = "claude-sonnet-4-5-20250929", [switch]$dangerouslySkipPermissions ) begin { $pipelineBuffer = "" } process { if ($PSBoundParameters.ContainsKey("InputObject") -and $null -ne $InputObject) { $pipelineBuffer += ($InputObject | Out-String) } } end { # If the caller piped content but didn't provide a Task, use the piped content as the Task. if (-not $Task -and $pipelineBuffer) { $Task = $pipelineBuffer.TrimEnd("`r", "`n") } elseif ($Task -and $pipelineBuffer) { # Merge the task and piped content with clear separators so the model sees both. $Task = "$Task`n`n--- Begin piped input ---`n$($pipelineBuffer.TrimEnd("`r","`n"))`n--- End piped input ---" } # proceed with the rest of the function using the possibly-updated $Task $apiKey = $env:ANTHROPIC_API_KEY if (-not $apiKey) { Write-Host "Set ANTHROPIC_API_KEY"; exit } Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Processing request..." $tools = @( @{ name = "Read-File" description = "Read the contents of a file" input_schema = @{ type = "object" properties = @{ path = @{ type = "string"; description = "Path to the file" } } required = @("path") } } @{ name = "Write-File" description = "Write content to a file" input_schema = @{ type = "object" properties = @{ path = @{ type = "string"; description = "Path to the file" } content = @{ type = "string"; description = "Content to write" } } required = @("path", "content") } } @{ name = "Run-Command" description = "Run a PowerShell command" input_schema = @{ type = "object" properties = @{ command = @{ type = "string"; description = "The command to run" } } required = @("command") } } @{ name = "Delegate-Task" description = "Delegate a focused task to a sub-agent with limited context" input_schema = @{ type = "object" properties = @{ task = @{ type = "string"; description = "The task to delegate" } maxTurns = @{ type = "integer"; description = "Maximum turns for the sub-agent (default 10)" } } required = @("task") } } ) function Execute-Tool { param([string]$Name, $ToolInput) switch ($Name) { "Read-File" { try { $content = Get-Content $ToolInput.path -Raw return "Contents of $($ToolInput.path):`n$content" } catch { return "Error: $_" } } "Write-File" { try { Set-Content $ToolInput.path $ToolInput.content return "Successfully wrote to $($ToolInput.path)" } catch { return "Error: $_" } } "Run-Command" { try { $output = Invoke-Expression $ToolInput.command 2>&1 | Out-String return "`$ $($ToolInput.command)`n$output" } catch { return "Error: $_" } } "Delegate-Task" { $subTask = $ToolInput.task $maxTurns = if ($ToolInput.maxTurns) { $ToolInput.maxTurns } else { 10 } return Run-SubAgent $subTask $maxTurns } default { return "Unknown tool: $Name" } } } function Run-SubAgent { param([string]$SubTask, [int]$MaxTurns = 10) Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Starting sub-agent for: $SubTask" $subMessages = @(@{ role = "user"; content = $SubTask }) $turns = 0 while ($turns -lt $MaxTurns) { $turns++ Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Consulting Claude..." $body = @{ model = $Model messages = $subMessages max_tokens = 4096 tools = $tools } | ConvertTo-Json -Depth 10 $response = Invoke-RestMethod -Uri "https://api.anthropic.com/v1/messages" -Method Post -Headers @{ "x-api-key" = $apiKey "anthropic-version" = "2023-06-01" "Content-Type" = "application/json" } -Body $body Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Response received, analyzing..." $assistantMessage = @{ role = "assistant"; content = $response.content } $subMessages += $assistantMessage $toolUses = $response.content | Where-Object { $_.type -eq "tool_use" } if ($toolUses) { $toolResults = @() foreach ($toolUse in $toolUses) { $toolName = $toolUse.name $toolInput = $toolUse.input Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🔧 $toolName`: $($toolInput | ConvertTo-Json -Compress)" if (Check-Permission $toolName $toolInput) { $result = Execute-Tool $toolName $toolInput Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] → $($result.Substring(0, [Math]::Min(100, $result.Length)))..." } else { $result = "Permission denied by user" Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🚫 $result" } $toolResults += @{ type = "tool_result" tool_use_id = $toolUse.id content = $result } } $userMessage = @{ role = "user"; content = $toolResults } $subMessages += $userMessage } else { $textContent = ($response.content | Where-Object { $_.type -eq "text" } | ForEach-Object { $_.text }) -join "" Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Sub-agent result: $textContent" return $textContent } } return "Sub-agent reached max turns without completion." } function Check-Permission { param([string]$ToolName, $ToolInput) if ($dangerouslySkipPermissions) { return $true } if ($ToolName -eq "Run-Command") { $cmd = $ToolInput.command if ($cmd -match "rm|del|Remove-Item|rmdir|rd|Set-Content.*>.*|.*\|.*iex") { Write-Host "⚠️ Potentially dangerous command: $cmd" $response = Read-Host "Allow? (y/n)" return $response -eq "y" } } elseif ($ToolName -eq "Write-File") { Write-Host "📝 Will write to: $($ToolInput.path)" $response = Read-Host "Allow? (y/n)" return $response -eq "y" } return $true } $messages = @(@{ role = "user"; content = $Task }) while ($true) { if ($messages.Count -gt 1) { Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Continuing analysis..." } Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Consulting Claude..." $body = @{ model = $Model messages = $messages max_tokens = 4096 tools = $tools } | ConvertTo-Json -Depth 10 $response = Invoke-RestMethod -Uri "https://api.anthropic.com/v1/messages" -Method Post -Headers @{ "x-api-key" = $apiKey "anthropic-version" = "2023-06-01" "Content-Type" = "application/json" } -Body $body Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Response received, analyzing..." $assistantMessage = @{ role = "assistant"; content = $response.content } $messages += $assistantMessage $toolUses = $response.content | Where-Object { $_.type -eq "tool_use" } if ($toolUses) { $toolResults = @() foreach ($toolUse in $toolUses) { $toolName = $toolUse.name $toolInput = $toolUse.input Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🔧 $toolName`: $($toolInput | ConvertTo-Json -Compress)" if (Check-Permission $toolName $toolInput) { $result = Execute-Tool $toolName $toolInput Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] → $($result.Substring(0, [Math]::Min(200, $result.Length)))..." } else { $result = "Permission denied by user" Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🚫 $result" } $toolResults += @{ type = "tool_result" tool_use_id = $toolUse.id content = $result } } $userMessage = @{ role = "user"; content = $toolResults } $messages += $userMessage } else { $textContent = ($response.content | Where-Object { $_.type -eq "text" } | ForEach-Object { $_.text }) -join "" Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] ✅ $textContent" break } } } } |