forked from Crockan/MercuryToolbox
469 lines
20 KiB
PowerShell
469 lines
20 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[ValidateSet('Debug', 'Release', 'ReleaseFast', 'ReleaseSize')]
|
|
[string]$Configuration = 'ReleaseFast',
|
|
[string]$SkillRoot = (Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'skills') 'mercury-toolbox'),
|
|
[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 Select-CatalogExample {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string[]]$Examples
|
|
)
|
|
|
|
if ($Examples.Count -eq 0) {
|
|
return $null
|
|
}
|
|
|
|
foreach ($example in $Examples) {
|
|
if (
|
|
-not $example.Contains('Get-Content') -and
|
|
-not $example.Contains('fixtures') -and
|
|
($example.Contains('|') -or $example.Contains('ConvertFrom-Json'))
|
|
) {
|
|
return $example
|
|
}
|
|
}
|
|
|
|
foreach ($example in $Examples) {
|
|
if (-not $example.Contains('Get-Content') -and -not $example.Contains('fixtures')) {
|
|
return $example
|
|
}
|
|
}
|
|
|
|
foreach ($example in $Examples) {
|
|
if (-not $example.Contains('Get-Content')) {
|
|
return $example
|
|
}
|
|
}
|
|
|
|
return $Examples[0]
|
|
}
|
|
|
|
function Normalize-GeneratedExample {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Example
|
|
)
|
|
|
|
return ($Example -replace '(?i)(?:[A-Z]:)?[^''"\s|]*fixtures(?:[\\/][^''"\s|]+)+', '<PATH>')
|
|
}
|
|
|
|
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 Get-CategorySpecs {
|
|
return @(
|
|
@{
|
|
Title = 'Code and Context'
|
|
Commands = @('fileprobe', 'outline', 'codeshape', 'refs', 'snip', 'defsnip', 'ctxpack', 'chunkcat', 'hitsnip', 'diagpick', 'gitshape', 'reposhape', 'dotnetshape')
|
|
},
|
|
@{
|
|
Title = 'Data and Config'
|
|
Commands = @('cjson', 'ison', 'isonl', 'zon', 'tonl', 'jsonlgrep', 'jsonshape', 'mhash', 'toon', 'csvshape', 'sqliteshape', 'sqlshape', 'config')
|
|
},
|
|
@{
|
|
Title = 'Logs, Process, and Waiting'
|
|
Commands = @('logshape', 'envdiff', 'proctree', 'sysshape', 'runprobe', 'await', 'argv', 'recent', 'pathshadow')
|
|
},
|
|
@{
|
|
Title = 'Network and Locks'
|
|
Commands = @('portping', 'portunlock', 'msudo', 'unlock')
|
|
},
|
|
@{
|
|
Title = 'Managed, Unity, and Binary Inspection'
|
|
Commands = @('asmtype', 'asmmember', 'asmref', 'asmapi', 'asmflow', 'llvmobjdump', 'llvmreadobj', 'llvmnm', 'peexports', 'peimports', 'pecalls', 'pesig', 'pestrrefs', 'drvshape', 'ioctlscan', 'unityasset', 'unityprobe', 'unitydiag', 'binmeta', 'stringscan')
|
|
}
|
|
)
|
|
}
|
|
|
|
function Build-SkillContent {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[hashtable]$Notes
|
|
)
|
|
|
|
$builder = [System.Text.StringBuilder]::new()
|
|
[void]$builder.AppendLine('---')
|
|
[void]$builder.AppendLine('name: mercury-toolbox')
|
|
[void]$builder.AppendLine("description: 'Use whenever Mercury Toolbox binaries are present and the task is local terminal inspection or triage: code and log reading, structured-data shaping, repo or runtime diagnosis, process, port, or lock troubleshooting, managed assembly or Unity analysis, or when the agent would otherwise reach for Get-Content, cat, tree, grep, findstr, netstat, tasklist, or ad-hoc PowerShell glue.'")
|
|
[void]$builder.AppendLine('---')
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('# Mercury Toolbox')
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('Use this skill immediately when Mercury Toolbox binaries are present and the job is local terminal inspection, shaping, or triage. Read it first, then route through Mercury before falling back to raw dumps, legacy builtins, or ad-hoc shell glue.')
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('## First Choice Rules')
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('- Prefer Mercury readers over `Get-Content`, `cat`, `tree`, `tasklist`, `netstat`, or whole-file dumps.')
|
|
[void]$builder.AppendLine('- For source and logs, usually start with `fileprobe`, `outline`, `snip`, or `chunkcat`, then move to `hitsnip`, `defsnip`, `refs`, `codeshape`, or `ctxpack` as needed.')
|
|
[void]$builder.AppendLine('- Prefer compact text by default, switch to `--json` when the next step parses the result, and switch to `--toon` or `--format toon` when the next consumer is an AI/model.')
|
|
[void]$builder.AppendLine('- Pipe external JSON into `toon` only when the producer is not a Mercury tool; TOON auto-detects JSON and emits dense TOON by default.')
|
|
[void]$builder.AppendLine('- Prefer stdin and pipelines over reopening the same file repeatedly.')
|
|
[void]$builder.AppendLine('- Pair Mercury with modern CLI companions instead of legacy builtins.')
|
|
[void]$builder.AppendLine('- Treat `msudo` as the top-level high-risk toolbox command: start with `msudo status --json` or `--help` before considering any privileged launch.')
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('## Modern Pairings')
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('- Search and files: `rg`, `fd`, and optionally `fzf` or `PSFzf` for interactive narrowing.')
|
|
[void]$builder.AppendLine('- Viewing and editing: `bat`, `hexyl`, and `nvim` for bounded human reads instead of raw file dumps.')
|
|
[void]$builder.AppendLine('- Data and text: `jq`, `yq`, and `sd` for structured queries and small safe rewrites.')
|
|
[void]$builder.AppendLine('- Git and navigation: `git`, `lazygit`, `delta`, and `zoxide` when they answer the question faster than shell glue.')
|
|
[void]$builder.AppendLine('- Stats and diagnostics: `tokei`, `eza`, `procs`, `dust`, and `hyperfine` for fast shape or performance checks.')
|
|
[void]$builder.AppendLine('- Utilities: `xh`, `ouch`, and `tealdeer` for quick HTTP work, archives, and terse help.')
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('## Modern Default Replacements')
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('- Use `rg` over recursive `grep` or broad `Select-String`.')
|
|
[void]$builder.AppendLine('- Use `fd` over `Get-ChildItem -Recurse` when you only need file discovery.')
|
|
[void]$builder.AppendLine('- Use `bat` or Mercury readers over raw `Get-Content` for bounded viewing.')
|
|
[void]$builder.AppendLine('- Use `jq` or `yq` over manual JSON or YAML parsing.')
|
|
[void]$builder.AppendLine('- Use `sd` for simple regex replacements and `xh` over `curl` for quick HTTP checks.')
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('## Fast Routing')
|
|
[void]$builder.AppendLine()
|
|
foreach ($category in Get-CategorySpecs) {
|
|
$toolList = ($category.Commands | ForEach-Object { ('`{0}`' -f $_) }) -join ', '
|
|
[void]$builder.AppendLine("- $($category.Title): $toolList")
|
|
}
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('## Job Routing')
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('- Unknown file: start with `fileprobe <PATH>`, then choose `binmeta`, `stringscan`, `snip`, or `chunkcat` from the detected shape.')
|
|
[void]$builder.AppendLine('- Repository map: start with `reposhape . --json`, then use `codeshape`, `gitshape`, `dotnetshape`, `sqlshape`, or `ctxpack`.')
|
|
[void]$builder.AppendLine('- Symbol or source context: start with `defsnip <SYMBOL> .` or `refs <SYMBOL> .`, then pack evidence with `hitsnip` or `ctxpack`.')
|
|
[void]$builder.AppendLine('- Logs and failures: start with `diagpick <LOG>`, then use `logshape`, `snip --match`, or `runprobe`.')
|
|
[void]$builder.AppendLine('- Managed or Unity DLL: start with `asmref diagnose <ASSEMBLY> --resolve-dir <DIR>`, then use `asmtype`, `asmmember`, `asmflow`, or `asmapi diff`.')
|
|
[void]$builder.AppendLine('- Windows EXE/DLL: start with `peimports <PATH>`; every catalog entry includes guided answer/trust/next actions, and PE deep tools also expose runtime `report_quality` and `next_actions` in `--json`/`--toon`.')
|
|
[void]$builder.AppendLine('- Windows driver: start with `drvshape <SYS>`, then follow `ioctlscan`, `peimports --category device_io`, `pecalls --category device_io`, or `pestrrefs`.')
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('## Read Next')
|
|
[void]$builder.AppendLine()
|
|
[void]$builder.AppendLine('- Read `references/command-catalog.md` beside this skill when you need command-by-command routing, usage, and examples.')
|
|
[void]$builder.AppendLine('- In packaged installs, the skill lives under `share\\mercury-toolbox\\skills\\mercury-toolbox` and the generated prompt lives under `share\\mercury-toolbox\\docs\\ai`.')
|
|
|
|
return $builder.ToString()
|
|
}
|
|
|
|
function Convert-ToonExample {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Example,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$CommandName,
|
|
[bool]$SupportsJson
|
|
)
|
|
|
|
if (-not $SupportsJson -or $CommandName -eq 'toon') {
|
|
return ''
|
|
}
|
|
|
|
$prefix = ($Example -split '\|\s*ConvertFrom-Json', 2)[0].TrimEnd()
|
|
if ($prefix -match '(?i)\b--format\s+toon\b') {
|
|
return ([regex]::Replace($prefix, '(?i)\s+--format\s+toon\b', ' --toon')).TrimEnd()
|
|
}
|
|
if ($prefix.Contains('--json')) {
|
|
$lastJson = $prefix.LastIndexOf('--json')
|
|
return ($prefix.Remove($lastJson, 6).Insert($lastJson, '--toon')).TrimEnd()
|
|
}
|
|
else {
|
|
$escapedCommand = [regex]::Escape($CommandName)
|
|
if ($CommandName -eq 'tonl') {
|
|
$withToon = [regex]::Replace($prefix, "(^|\|\s*)($escapedCommand\s+\S+)(\s|$)", '$1$2 --toon$3', 1)
|
|
if ($withToon -cne $prefix) {
|
|
return $withToon
|
|
}
|
|
}
|
|
$withToon = [regex]::Replace($prefix, "(^|\|\s*)($escapedCommand)(\s|$)", '$1$2 --toon$3', 1)
|
|
if ($withToon -ceq $prefix) {
|
|
return ''
|
|
}
|
|
return $withToon
|
|
}
|
|
}
|
|
|
|
function Build-CatalogContent {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[hashtable]$Notes,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$BinaryRoot
|
|
)
|
|
|
|
$builder = [System.Text.StringBuilder]::new()
|
|
[void]$builder.AppendLine('# Mercury Toolbox Command Catalog')
|
|
[void]$builder.AppendLine('Use this catalog when choosing among nearby Mercury commands. Keep output compact, prefer `--json` for machine handoff, prefer `--toon` for model-facing structured output, and use `toon` for external JSON producers.')
|
|
|
|
foreach ($category in Get-CategorySpecs) {
|
|
[void]$builder.AppendLine("## $($category.Title)")
|
|
foreach ($commandName in $category.Commands) {
|
|
$toolNotes = $Notes.tools[$commandName]
|
|
if ($null -eq $toolNotes) {
|
|
throw "Missing AI skill notes for tool '$commandName'"
|
|
}
|
|
|
|
$helpText = (& (Join-Path $BinaryRoot "$commandName.exe") '--help' | Out-String).TrimEnd()
|
|
$help = Get-ToolHelpSections -HelpText $helpText
|
|
$usageLine = if ($toolNotes.ContainsKey('prompt_usage') -and -not [string]::IsNullOrWhiteSpace([string]$toolNotes.prompt_usage)) {
|
|
[string]$toolNotes.prompt_usage
|
|
} elseif ($help.Usage.Count -gt 0) {
|
|
(([string[]]$help.Usage) -join ' | ')
|
|
} else {
|
|
"$commandName --help"
|
|
}
|
|
$exampleLine = if ($toolNotes.ContainsKey('prompt_example') -and -not [string]::IsNullOrWhiteSpace([string]$toolNotes.prompt_example)) {
|
|
[string]$toolNotes.prompt_example
|
|
} else {
|
|
Select-CatalogExample -Examples ([string[]]$help.Examples)
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($exampleLine)) {
|
|
$exampleLine = "$commandName --help"
|
|
} else {
|
|
$exampleLine = Normalize-GeneratedExample -Example $exampleLine
|
|
}
|
|
$supportsJson = $helpText.Contains('--json')
|
|
$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}`' -f $commandName))
|
|
[void]$builder.AppendLine(('Use: {0} Better than: {1}' -f $toolNotes.use_when, $toolNotes.why))
|
|
[void]$builder.AppendLine(('Usage: `{0}`' -f $usageLine))
|
|
[void]$builder.AppendLine(('Example: `{0}`' -f $exampleLine))
|
|
[void]$builder.AppendLine(('Guided answer: {0}' -f $guided.answer))
|
|
[void]$builder.AppendLine(('Trust basis: {0}' -f $guided.trust))
|
|
[void]$builder.AppendLine(('Next actions: {0}' -f $nextActions))
|
|
$toonExample = Convert-ToonExample -Example $exampleLine -CommandName $commandName -SupportsJson $supportsJson
|
|
if (-not [string]::IsNullOrWhiteSpace($toonExample)) {
|
|
[void]$builder.AppendLine(('TOON example: `{0}`' -f $toonExample))
|
|
}
|
|
}
|
|
}
|
|
|
|
return $builder.ToString()
|
|
}
|
|
|
|
function Build-OpenAiYamlContent {
|
|
return @'
|
|
interface:
|
|
display_name: "Mercury Toolbox"
|
|
short_description: "Route proactive local CLI triage through Mercury Toolbox"
|
|
icon_small: "./assets/logo.png"
|
|
icon_large: "./assets/logo.png"
|
|
brand_color: "#35C2FF"
|
|
default_prompt: "Use $mercury-toolbox first for local code, log, data, repo, process, port, lock, managed, or Unity triage, and pair it with modern CLI tools before falling back to raw dumps or legacy builtins."
|
|
|
|
policy:
|
|
allow_implicit_invocation: true
|
|
'@
|
|
}
|
|
|
|
function Assert-GeneratedFileMatches {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Content,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Label
|
|
)
|
|
|
|
if (-not (Test-Path -LiteralPath $Path)) {
|
|
throw "Generated $Label is missing: $Path"
|
|
}
|
|
|
|
$temporaryPath = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())
|
|
try {
|
|
$encoding = [System.Text.UTF8Encoding]::new($false)
|
|
[System.IO.File]::WriteAllText($temporaryPath, (ConvertTo-LfText -Content $Content), $encoding)
|
|
$existingBytes = [System.IO.File]::ReadAllBytes((Resolve-Path -LiteralPath $Path))
|
|
$generatedBytes = [System.IO.File]::ReadAllBytes($temporaryPath)
|
|
if (-not (Test-ByteSequenceEqual -Left $existingBytes -Right $generatedBytes)) {
|
|
throw "Generated $Label is out of date. Run pwsh -NoProfile -File .\scripts\generate-ai-skill.ps1"
|
|
}
|
|
}
|
|
finally {
|
|
Remove-Item -LiteralPath $temporaryPath -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
$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-skill.ps1 -SkipBuild."
|
|
}
|
|
Invoke-ToolboxBuild -CargoPath $cargoPath -Configuration $Configuration
|
|
}
|
|
|
|
$referencesRoot = Join-Path $SkillRoot 'references'
|
|
$agentsRoot = Join-Path $SkillRoot 'agents'
|
|
$skillPath = Join-Path $SkillRoot 'SKILL.md'
|
|
$catalogPath = Join-Path $referencesRoot 'command-catalog.md'
|
|
$openAiYamlPath = Join-Path $agentsRoot 'openai.yaml'
|
|
|
|
$skillContent = Build-SkillContent -Notes $notes
|
|
$catalogContent = Build-CatalogContent -Notes $notes -BinaryRoot $binaryRoot
|
|
$openAiYamlContent = Build-OpenAiYamlContent
|
|
|
|
if ($Check) {
|
|
Assert-GeneratedFileMatches -Path $skillPath -Content $skillContent -Label 'Mercury Toolbox skill'
|
|
Assert-GeneratedFileMatches -Path $catalogPath -Content $catalogContent -Label 'Mercury Toolbox command catalog'
|
|
Assert-GeneratedFileMatches -Path $openAiYamlPath -Content $openAiYamlContent -Label 'Mercury Toolbox openai.yaml'
|
|
Write-Host "AI skill is up to date: $SkillRoot"
|
|
return
|
|
}
|
|
|
|
New-Item -ItemType Directory -Force -Path $SkillRoot, $referencesRoot, $agentsRoot | Out-Null
|
|
Write-Utf8FileWithRetry -Path $skillPath -Content $skillContent
|
|
Write-Utf8FileWithRetry -Path $catalogPath -Content $catalogContent
|
|
Write-Utf8FileWithRetry -Path $openAiYamlPath -Content $openAiYamlContent
|
|
Write-Host "Generated AI skill: $SkillRoot"
|