PleasePwsh.psm1

using namespace System.Management.Automation.Host

if ($PSVersionTable.PSEdition -ne 'Core' -or $PSVersionTable.PSVersion.Major -lt 7) {
    throw "This script requires PowerShell Core version 7 or higher."
}

Set-PSDebug -Strict
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"

<#
.SYNOPSIS
Translates a prompt into a PowerShell command using OpenAI GPT.
 
.DESCRIPTION
The main function, 'Please', takes a PowerShell command as input and prompts the user with a list of actions to perform on that command: abort, copy, explain and invoke.
 
.PARAMETER Prompt
Specifies the prompt in natural language that gets translated into a PowerShell command.
 
.PARAMETER Explain
If this switch is provided, the script will return a brief explanation of the specified command.
 
.EXAMPLE
PS> Please "Get all files in current directory that are larger than 1 MB"
 
PS> Please -Explain "Get-ChildItem -Path 'C:\'"
#>

function Please {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$Prompt,
        [Alias("e")][switch]$Explain

    )

    Test-ApiKey

    if ($Explain) {
        $Explanation = Get-CommandExplanation $Prompt
        Write-Output "`u{261D} $Explanation"
        $Command = $Prompt
    }
    else {
        $Command = Get-PwshCommand $Prompt
        if ($Command.contains("I do not know")) {
            Write-Output $Command
            Return
        }
    }
    $Action = Show-Menu $Command
    Invoke-Action $Action
}

function Test-ApiKey {
    if ($null -eq $env:OPENAI_API_KEY) {
        Write-Output "`u{1F50E} Api key missing. See https://help.openai.com/en/articles/4936850-where-do-i-find-my-secret-api-key"
        $Key = Read-Host "Please provide the api key"

        if ([string]::IsNullOrWhiteSpace($Key)) {
            throw "`u{1F937} Api key still missing. Aborting."
        }
        $env:OPENAI_API_KEY = $Key
    }
}

function Get-PwshCommand([string]$Prompt) {
    $Role = "You translate the input given into PowerShell command. You may not use natural language, but only a PowerShell commands as answer. Do not use markdown. Do not quote the whole output. If you do not know the answer, answer only with 'I do not know'"

    $Payload = @{
        'model'    = "gpt-3.5-turbo"
        'messages' = @(
            @{ 'role' = 'system'; 'content' = $Role },
            @{ 'role' = 'user'; 'content' = $Prompt }
        )
    }

    Return Invoke-OpenAIRequest $Payload
}

function Show-Menu($Command) {
    $Title = "`u{1F523} Command:`n $Command"

    $Question = "`u{2753} What should I do?"

    $OptionAbort = [ChoiceDescription]::new('&Abort')
    $OptionCopy = [ChoiceDescription]::new('&Copy')
    $OptionInvoke = [ChoiceDescription]::new('&Invoke')
    $Options = [ChoiceDescription[]]($OptionAbort, $OptionCopy, $OptionInvoke)

    Return $Host.UI.PromptForChoice($Title, $Question, $Options, 0)
}

function Invoke-Action ($Action) {
    switch ($Action) {
        0 {
            Write-Output "`u{274C} Aborting"
        }
        1 {
            Write-Output "`u{00A9} Copying to clipboard"
            Set-Clipboard -Value $Command
        }
        2 {
            Write-Output "`u{25B6} Invoking command"
            Invoke-Expression $Command
        }
        Default {
            Write-Output "Invalid action"
        }
    }
}

function Get-CommandExplanation([string]$Command) {
    $Prompt = "Explain what the command $Command does. Don't be too verbose."

    $Payload = @{
        'max_tokens' = 100
        'model'      = "gpt-3.5-turbo"
        'messages'   = @(
            @{ 'role' = 'user'; 'content' = $Prompt }
        )
    }

    Return Invoke-OpenAIRequest $Payload
}

function Invoke-OpenAIRequest($Payload) {
    $Uri = "https://api.openai.com/v1/chat/completions"

    $Headers = @{
        'Content-Type'  = 'application/json'
        'Authorization' = "Bearer $env:OPENAI_API_KEY"
    }

    try {
        $Response = Invoke-RestMethod -Uri $Uri -Method Post -Headers $Headers -Body ($Payload | ConvertTo-Json)
    }
    catch {
        Write-Error "Received $($_.Exception.Response.ReasonPhrase): $($_.Exception.Response.Content | ConvertTo-Json)"
    }

    Return $Response.choices[0].message.content
}

Export-ModuleMember -Function "Please"