[CmdletBinding()] param( [ValidateSet('Debug', 'Release', 'ReleaseFast', 'ReleaseSize')] [string]$Configuration = 'ReleaseFast', [string]$OutputPath = (Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'docs') 'ai\mercury-toolbox-ai-prompt.md'), [string]$NotesPath = (Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'docs') 'ai\toolbox-ai-prompt-notes.json'), [switch]$Check, [switch]$SkipBuild ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest . (Join-Path $PSScriptRoot 'toolbox-commands.ps1') function Invoke-StrictNative { param( [Parameter(Mandatory = $true)] [string]$FilePath, [Parameter(Mandatory = $true)] [string[]]$ArgumentList ) & $FilePath @ArgumentList if ($LASTEXITCODE -ne 0) { throw "Command failed with exit code ${LASTEXITCODE}: $FilePath $($ArgumentList -join ' ')" } } function Get-ToolHelpSections { param( [Parameter(Mandatory = $true)] [string]$HelpText ) $lines = $HelpText -split "`r?`n" $summary = ($lines | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1).Trim() $usage = [System.Collections.Generic.List[string]]::new() $examples = [System.Collections.Generic.List[string]]::new() $section = '' foreach ($line in $lines) { $trimmed = $line.Trim() if ($trimmed -ceq 'Usage:') { $section = 'usage' continue } if ($trimmed -ceq 'Examples:') { $section = 'examples' continue } if ([string]::IsNullOrWhiteSpace($trimmed)) { continue } if (Test-HelpSectionHeader -Line $trimmed) { $section = '' continue } switch ($section) { 'usage' { $usage.Add($trimmed) } 'examples' { $examples.Add($trimmed) } } } return @{ Summary = $summary Usage = @($usage) Examples = @($examples) } } function Test-HelpSectionHeader { param( [Parameter(Mandatory = $true)] [string]$Line ) return $Line -cmatch '^[A-Z][A-Za-z0-9 /_-]*:$' } function Append-Section { param( [Parameter(Mandatory = $true)] [System.Text.StringBuilder]$Builder, [Parameter(Mandatory = $true)] [string]$Heading, [Parameter(Mandatory = $true)] [string[]]$Items ) [void]$Builder.AppendLine("## $Heading") [void]$Builder.AppendLine() foreach ($item in $Items) { [void]$Builder.AppendLine("- $item") } [void]$Builder.AppendLine() } function Select-PromptExamples { param( [Parameter(Mandatory = $true)] [string[]]$Examples, [int]$Limit = 1 ) $selected = [System.Collections.Generic.List[string]]::new() if ($Examples.Count -eq 0) { return @() } foreach ($example in $Examples) { if (-not $example.Contains('Get-Content') -and -not $example.Contains('fixtures')) { $selected.Add($example) break } } if ($selected.Count -eq 0) { foreach ($example in $Examples) { if (-not $example.Contains('Get-Content')) { $selected.Add($example) break } } } if ($selected.Count -eq 0) { $selected.Add($Examples[0]) } foreach ($example in $Examples | Select-Object -Skip 1) { if ( $selected.Count -lt $Limit -and $example -cne $selected[0] -and -not $example.Contains('Get-Content') -and -not $example.Contains('fixtures') -and ($example.Contains('|') -or $example.Contains('ConvertFrom-Json')) ) { $selected.Add($example) } } foreach ($example in $Examples | Select-Object -Skip 1) { if ($selected.Count -ge $Limit) { break } if ( -not $selected.Contains($example) -and -not $example.Contains('Get-Content') -and -not $example.Contains('fixtures') ) { $selected.Add($example) } } foreach ($example in $Examples | Select-Object -Skip 1) { if ($selected.Count -ge $Limit) { break } if (-not $selected.Contains($example)) { $selected.Add($example) } } return @($selected) } function Normalize-GeneratedExample { param( [Parameter(Mandatory = $true)] [string]$Example ) return ($Example -replace '(?i)(?:[A-Z]:)?[^''"\s|]*fixtures(?:[\\/][^''"\s|]+)+', '') } function Test-ByteSequenceEqual { param( [Parameter(Mandatory = $true)] [byte[]]$Left, [Parameter(Mandatory = $true)] [byte[]]$Right ) if ($Left.Length -ne $Right.Length) { return $false } for ($index = 0; $index -lt $Left.Length; $index += 1) { if ($Left[$index] -ne $Right[$index]) { return $false } } return $true } function ConvertTo-LfText { param( [Parameter(Mandatory = $true)] [string]$Content ) return $Content.Replace("`r`n", "`n").Replace("`r", "`n") } function Write-Utf8FileWithRetry { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$Content, [int]$Attempts = 20, [int]$DelayMilliseconds = 100 ) for ($attempt = 1; $attempt -le $Attempts; $attempt++) { try { $encoding = [System.Text.UTF8Encoding]::new($false) [System.IO.File]::WriteAllText($Path, (ConvertTo-LfText -Content $Content), $encoding) return } catch { if ($attempt -eq $Attempts) { throw } Start-Sleep -Milliseconds $DelayMilliseconds } } } function Select-PromptUsage { param( [Parameter(Mandatory = $true)] [hashtable]$ToolNotes, [Parameter(Mandatory = $true)] $Help, [Parameter(Mandatory = $true)] [string]$CommandName ) if ($ToolNotes.ContainsKey('prompt_usage') -and -not [string]::IsNullOrWhiteSpace([string]$ToolNotes.prompt_usage)) { return [string]$ToolNotes.prompt_usage } if ($Help.Usage.Count -gt 0) { return (([string[]]$Help.Usage) -join ' | ') } return "$CommandName --help" } $workspaceRoot = Split-Path -Parent $PSScriptRoot $profileName = Resolve-ToolboxProfileName -Configuration $Configuration $binaryRoot = Join-Path $workspaceRoot (Join-Path 'target' $profileName) $dependencyRoot = Join-Path $binaryRoot 'deps' if (Test-Path -LiteralPath $dependencyRoot -PathType Container) { $env:PATH = "$dependencyRoot$([System.IO.Path]::PathSeparator)$env:PATH" } $cargoPath = 'cargo' $notes = Get-Content -Raw -LiteralPath $NotesPath | ConvertFrom-Json -AsHashtable $missingBinary = $false foreach ($commandName in Get-ToolboxCommandNames) { if (-not (Test-Path -LiteralPath (Join-Path $binaryRoot "$commandName.exe"))) { $missingBinary = $true break } } if ($missingBinary) { if ($SkipBuild) { throw "Required toolbox binaries are missing under $binaryRoot. Build profile $Configuration before running generate-ai-prompt.ps1 -SkipBuild." } Invoke-ToolboxBuild -CargoPath $cargoPath -Configuration $Configuration } $builder = [System.Text.StringBuilder]::new() [void]$builder.AppendLine("# $($notes.title)") [void]$builder.AppendLine(('Available tools: {0}' -f ((Get-ToolboxCommandNames) -join ', '))) [void]$builder.AppendLine(('Rules: {0}' -f (([string[]]$notes.selection_rules) -join ' '))) foreach ($commandName in Get-ToolboxCommandNames) { $binaryPath = Join-Path $binaryRoot "$commandName.exe" $toolNotes = $notes.tools[$commandName] if ($null -eq $toolNotes) { throw "Missing AI prompt notes for tool '$commandName'" } $helpText = (& $binaryPath '--help' | Out-String).TrimEnd() $help = Get-ToolHelpSections -HelpText $helpText $usageLine = Select-PromptUsage -ToolNotes $toolNotes -Help $help -CommandName $commandName $examples = @(Select-PromptExamples -Examples ([string[]]$help.Examples)) $exampleLine = if ($toolNotes.ContainsKey('prompt_example') -and -not [string]::IsNullOrWhiteSpace([string]$toolNotes.prompt_example)) { [string]$toolNotes.prompt_example } elseif ($examples.Count -gt 0) { $examples[0] } else { "$commandName --help" } $exampleLine = Normalize-GeneratedExample -Example $exampleLine $guided = $toolNotes.guided_triage if ($null -eq $guided) { throw "Missing guided_triage notes for tool '$commandName'" } $nextActions = ([string[]]$guided.next_actions) -join ' | ' [void]$builder.AppendLine(('{0}: {1} Better than: {2} Usage: `{3}` Example: `{4}` Guided: answer={5} trust={6} next={7}' -f $commandName, $toolNotes.use_when, $toolNotes.why, $usageLine, $exampleLine, $guided.answer, $guided.trust, $nextActions)) } $content = $builder.ToString() if ($Check) { if (-not (Test-Path -LiteralPath $OutputPath)) { throw "Generated AI prompt is missing: $OutputPath" } $temporaryOutputPath = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName()) try { $encoding = [System.Text.UTF8Encoding]::new($false) [System.IO.File]::WriteAllText($temporaryOutputPath, (ConvertTo-LfText -Content $content), $encoding) $existingBytes = [System.IO.File]::ReadAllBytes((Resolve-Path -LiteralPath $OutputPath)) $generatedBytes = [System.IO.File]::ReadAllBytes($temporaryOutputPath) if (-not (Test-ByteSequenceEqual -Left $existingBytes -Right $generatedBytes)) { throw "Generated AI prompt is out of date. Run pwsh -NoProfile -File .\scripts\generate-ai-prompt.ps1" } } finally { Remove-Item -LiteralPath $temporaryOutputPath -Force -ErrorAction SilentlyContinue } Write-Host "AI prompt is up to date: $OutputPath" return } $outputDirectory = Split-Path -Parent $OutputPath New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null Write-Utf8FileWithRetry -Path $OutputPath -Content $content Write-Host "Generated AI prompt: $OutputPath"