# OpenClaw VOC Skills Deploy Script v3.0.0 # # Deploys skills, workshop playbook, and memory templates to OpenClaw # # Usage: # .\deploy-to-openclaw.ps1 # Deploy everything # .\deploy-to-openclaw.ps1 -DryRun # Preview only # .\deploy-to-openclaw.ps1 -SkillsOnly # Deploy skills only # .\deploy-to-openclaw.ps1 -WorkshopOnly # Deploy workshop only # .\deploy-to-openclaw.ps1 -OpenClawDir "D:\path" # Custom OpenClaw directory param( [string]$OpenClawDir = "$env:USERPROFILE\.openclaw", [string]$ProjectRoot = (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent), [switch]$SkillsOnly, [switch]$WorkshopOnly, [switch]$DryRun ) $ErrorActionPreference = "Stop" $utf8NoBom = New-Object System.Text.UTF8Encoding($false) # ============================================ # Paths # ============================================ $SkillsTarget = Join-Path $OpenClawDir "skills" $WorkspaceTarget = Join-Path $OpenClawDir "workspace" $MemoryTarget = Join-Path $WorkspaceTarget "memory" $ToolsTarget = Join-Path $OpenClawDir "tools" $WorkspaceToolsTarget = Join-Path $WorkspaceTarget "scripts\tools" $ToolItems = @( @{ Source = Join-Path $ProjectRoot "scripts\tools\openclaw-tool-runner.js"; Dest = "openclaw-tool-runner.js" }, @{ Source = Join-Path $ProjectRoot "scripts\tools\set-voc-token.js"; Dest = "set-voc-token.js" }, @{ Source = Join-Path $ProjectRoot "scripts\tools\voc-token-preflight.js"; Dest = "voc-token-preflight.js" }, @{ Source = Join-Path $ProjectRoot "scripts\tools\voc-single-platform-report.js"; Dest = "voc-single-platform-report.js" }, @{ Source = Join-Path $ProjectRoot "scripts\tools\voc-data-normalizer.js"; Dest = "voc-data-normalizer.js" }, @{ Source = Join-Path $ProjectRoot "scripts\tools\platform-mini-report-generator.js"; Dest = "platform-mini-report-generator.js" }, @{ Source = Join-Path $ProjectRoot "scripts\tools\chapter-insight-writer.js"; Dest = "chapter-insight-writer.js" }, @{ Source = Join-Path $ProjectRoot "scripts\tools\voc-report-auditor.js"; Dest = "voc-report-auditor.js" }, @{ Source = Join-Path $ProjectRoot "scripts\tools\douyin-speaking-profile-memory.js"; Dest = "douyin-speaking-profile-memory.js" }, @{ Source = Join-Path $ProjectRoot "scripts\tools\douyin-speaking-daily-runner.js"; Dest = "douyin-speaking-daily-runner.js" }, @{ Source = Join-Path $ProjectRoot "scripts\tools\douyin-speaking-daily-report.js"; Dest = "douyin-speaking-daily-report.js" }, @{ Source = Join-Path $ProjectRoot "scripts\tools\douyin-viral-script-analyzer.js"; Dest = "douyin-viral-script-analyzer.js" }, @{ Source = Join-Path $ProjectRoot "scripts\tools\douyin-video-transcriber.js"; Dest = "douyin-video-transcriber.js" }, @{ Source = Join-Path $ProjectRoot "voc-report-factory\html-v2\gen-report-template.js"; Dest = "voc-report-factory\html-v2\gen-report-template.js" }, @{ Source = Join-Path $ProjectRoot "voc-report-factory\html-v2\report-renderer-template.js"; Dest = "voc-report-factory\html-v2\report-renderer-template.js" }, @{ Source = Join-Path $ProjectRoot "demo\xiaohongshu\raw-notes.json"; Dest = "course-samples\xiaohongshu\raw-notes.json" }, @{ Source = Join-Path $ProjectRoot "demo\xiaohongshu\raw-comments.json"; Dest = "course-samples\xiaohongshu\raw-comments.json" } ) $SkillCategories = @( "voc", "social-media", "competitor-analysis", "review-analysis", "synthesis", "social-voc", "douyin", "xiaohongshu", "video-creation", "jimeng", "payment", "test" ) # ============================================ # Helper Functions # ============================================ function Remove-Bom { param([string]$FilePath) $bytes = [System.IO.File]::ReadAllBytes($FilePath) if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $clean = $bytes[3..($bytes.Length - 1)] [System.IO.File]::WriteAllBytes($FilePath, $clean) return $true } return $false } function Test-SkillValid { param([string]$SkillDir, [string]$SkillName) $skillMd = Join-Path $SkillDir "SKILL.md" $apiConf = Join-Path $SkillDir "api-config.json" if (-not (Test-Path $skillMd)) { Write-Host " [SKIP] $SkillName - missing SKILL.md" -ForegroundColor DarkGray return $false } # Validate YAML frontmatter $mdContent = [System.IO.File]::ReadAllText($skillMd, [System.Text.Encoding]::UTF8) if ($mdContent -notmatch "^---\s*\r?\n") { Write-Host " [WARN] $SkillName - SKILL.md missing YAML frontmatter" -ForegroundColor Yellow } # Validate api-config.json if exists if (Test-Path $apiConf) { try { $null = Get-Content $apiConf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { Write-Host " [ERROR] $SkillName - invalid JSON in api-config.json" -ForegroundColor Red return $false } } return $true } function Deploy-File { param([string]$Source, [string]$Dest, [string]$Label) if ($DryRun) { $tag = if (Test-Path $Dest) { "UPDATE" } else { "NEW" } Write-Host " [$tag] $Label" -ForegroundColor White } else { $destDir = Split-Path $Dest -Parent if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null } Copy-Item $Source -Destination $Dest -Force } } # ============================================ # Banner # ============================================ Write-Host "" Write-Host " ====================================================" -ForegroundColor Cyan Write-Host " OpenClaw VOC Skills Deploy v3.0.0" -ForegroundColor Cyan Write-Host " ====================================================" -ForegroundColor Cyan Write-Host "" Write-Host " Project: $ProjectRoot" Write-Host " OpenClaw: $OpenClawDir" if ($DryRun) { Write-Host " Mode: DRY RUN (preview only)" -ForegroundColor Yellow } if ($SkillsOnly) { Write-Host " Scope: Skills only" -ForegroundColor Yellow } if ($WorkshopOnly) { Write-Host " Scope: Workshop only" -ForegroundColor Yellow } Write-Host "" # Pre-flight check if (-not (Test-Path $OpenClawDir)) { Write-Host " [ERROR] OpenClaw directory not found: $OpenClawDir" -ForegroundColor Red Write-Host " Is OpenClaw installed? Specify -OpenClawDir to override." -ForegroundColor Yellow exit 1 } $totalDeployed = 0 $totalSkipped = 0 $totalErrors = 0 $bomFixed = 0 # ============================================ # Part 1: Deploy Skills # ============================================ if (-not $WorkshopOnly) { Write-Host " [1/4] Deploying Skills" -ForegroundColor Cyan Write-Host " $('-' * 48)" if (-not $DryRun -and -not (Test-Path $SkillsTarget)) { New-Item -ItemType Directory -Path $SkillsTarget -Force | Out-Null } foreach ($cat in $SkillCategories) { $catPath = Join-Path $ProjectRoot $cat if (-not (Test-Path $catPath)) { continue } $skillDirs = Get-ChildItem -Directory -Path $catPath -ErrorAction SilentlyContinue if ($null -eq $skillDirs -or $skillDirs.Count -eq 0) { continue } Write-Host " [$cat]" -ForegroundColor DarkCyan foreach ($skillDir in $skillDirs) { $skillName = $skillDir.Name if (-not (Test-SkillValid $skillDir.FullName $skillName)) { $totalSkipped++ continue } $destDir = Join-Path $SkillsTarget $skillName if ($DryRun) { $tag = if (Test-Path $destDir) { "UPDATE" } else { "NEW" } Write-Host " [$tag] $skillName" -ForegroundColor White } else { if (Test-Path $destDir) { Remove-Item -Recurse -Force $destDir } New-Item -ItemType Directory -Path $destDir -Force | Out-Null Get-ChildItem -File -Path $skillDir.FullName | ForEach-Object { Copy-Item $_.FullName -Destination $destDir } # Fix UTF-8 BOM in SKILL.md (causes OpenClaw parsing failures) $deployedMd = Join-Path $destDir "SKILL.md" if (Test-Path $deployedMd) { if (Remove-Bom $deployedMd) { $bomFixed++ } } Write-Host " [OK] $skillName" -ForegroundColor Green } $totalDeployed++ } } # Also deploy workshop skills (e.g. brand-context-builder) $workshopSkillsPath = Join-Path $ProjectRoot "workshop" if (Test-Path $workshopSkillsPath) { $wsSkills = Get-ChildItem -Directory -Path $workshopSkillsPath | Where-Object { Test-Path (Join-Path $_.FullName "SKILL.md") } if ($wsSkills.Count -gt 0) { Write-Host " [workshop skills]" -ForegroundColor DarkCyan foreach ($ws in $wsSkills) { $skillName = $ws.Name $destDir = Join-Path $SkillsTarget $skillName if ($DryRun) { $tag = if (Test-Path $destDir) { "UPDATE" } else { "NEW" } Write-Host " [$tag] $skillName" -ForegroundColor White } else { if (Test-Path $destDir) { Remove-Item -Recurse -Force $destDir } New-Item -ItemType Directory -Path $destDir -Force | Out-Null Get-ChildItem -File -Path $ws.FullName | ForEach-Object { Copy-Item $_.FullName -Destination $destDir } $deployedMd = Join-Path $destDir "SKILL.md" if (Test-Path $deployedMd) { if (Remove-Bom $deployedMd) { $bomFixed++ } } Write-Host " [OK] $skillName" -ForegroundColor Green } $totalDeployed++ } } } Write-Host "" } # ============================================ # Part 2: Deploy Workshop Playbooks (v3 split-file architecture) # ============================================ if (-not $SkillsOnly) { Write-Host " [2/4] Deploying Workshop Playbooks" -ForegroundColor Cyan Write-Host " $('-' * 48)" # v3: Deploy all workshop .md files (global rules + session files + supporting docs) $workshopMdFiles = Get-ChildItem -File -Path (Join-Path $ProjectRoot "workshop") -Filter "*.md" -ErrorAction SilentlyContinue if ($workshopMdFiles -and $workshopMdFiles.Count -gt 0) { foreach ($mdFile in $workshopMdFiles) { $dest = Join-Path $WorkspaceTarget $mdFile.Name Deploy-File $mdFile.FullName $dest $mdFile.Name if (-not $DryRun) { Write-Host " [OK] $($mdFile.Name)" -ForegroundColor Green } $totalDeployed++ } } else { Write-Host " [SKIP] No workshop .md files found" -ForegroundColor DarkGray } Write-Host "" } # ============================================ # Part 2b: Deploy Douyin Speaking Daily Package # ============================================ if (-not $SkillsOnly) { Write-Host " [2b/4] Deploying Douyin Speaking Daily Package" -ForegroundColor Cyan Write-Host " $('-' * 48)" $douyinSpeakingSrc = Join-Path $ProjectRoot "douyin-speaking-daily" $douyinSpeakingDest = Join-Path $WorkspaceTarget "douyin-speaking-daily" if (Test-Path $douyinSpeakingSrc) { Get-ChildItem -File -Recurse -Path $douyinSpeakingSrc | ForEach-Object { $relative = $_.FullName.Substring($douyinSpeakingSrc.Length).TrimStart('\', '/') $dest = Join-Path $douyinSpeakingDest $relative Deploy-File $_.FullName $dest ("douyin-speaking-daily/" + ($relative -replace '\\', '/')) if (-not $DryRun) { Write-Host " [OK] douyin-speaking-daily/$($relative -replace '\\', '/')" -ForegroundColor Green } $totalDeployed++ } } else { Write-Host " [SKIP] douyin-speaking-daily directory not found" -ForegroundColor DarkGray } Write-Host "" } # ============================================ # Part 3: Deploy Memory Templates # ============================================ if (-not $SkillsOnly) { Write-Host " [3/4] Deploying Memory Templates" -ForegroundColor Cyan Write-Host " $('-' * 48)" $memSrc = Join-Path $ProjectRoot "workshop\memory-templates" if (Test-Path $memSrc) { if (-not $DryRun -and -not (Test-Path $MemoryTarget)) { New-Item -ItemType Directory -Path $MemoryTarget -Force | Out-Null } Get-ChildItem -File -Path $memSrc -Filter "*.json" | ForEach-Object { $dest = Join-Path $MemoryTarget $_.Name Deploy-File $_.FullName $dest $_.Name if (-not $DryRun) { Write-Host " [OK] $($_.Name)" -ForegroundColor Green } $totalDeployed++ } } else { Write-Host " [SKIP] memory-templates directory not found" -ForegroundColor DarkGray } Write-Host "" } # ============================================ # Part 4: Deploy Local Tools # ============================================ Write-Host " [4/4] Deploying Local Tools" -ForegroundColor Cyan Write-Host " $('-' * 48)" foreach ($tool in $ToolItems) { if (-not (Test-Path $tool.Source)) { Write-Host " [SKIP] $($tool.Dest) - source not found" -ForegroundColor DarkGray $totalSkipped++ continue } $toolsDest = Join-Path $ToolsTarget $tool.Dest $workspaceDest = Join-Path $WorkspaceToolsTarget $tool.Dest Deploy-File $tool.Source $toolsDest $tool.Dest Deploy-File $tool.Source $workspaceDest ("workspace/scripts/tools/" + $tool.Dest) if (-not $DryRun) { Write-Host " [OK] $($tool.Dest)" -ForegroundColor Green } $totalDeployed++ } Write-Host "" # ============================================ # Part 5: Credentials Template # ============================================ if (-not $DryRun -and -not $WorkshopOnly) { $credTemplate = Join-Path $ProjectRoot "__config\voc-credentials.template.json" $credTarget = Join-Path $OpenClawDir "voc-credentials.template.json" $credFile = Join-Path $OpenClawDir "voc-credentials.json" if (Test-Path $credTemplate) { Copy-Item $credTemplate -Destination $credTarget -Force if (-not (Test-Path $credFile)) { Write-Host " [INFO] Credentials template copied. Fill in vocToken in:" -ForegroundColor Yellow Write-Host " $credFile" -ForegroundColor Yellow } } } # ============================================ # Summary # ============================================ Write-Host " ====================================================" -ForegroundColor Cyan Write-Host " Deploy Summary" -ForegroundColor Cyan Write-Host " ====================================================" -ForegroundColor Cyan Write-Host "" Write-Host " Deployed: $totalDeployed" Write-Host " Skipped: $totalSkipped" Write-Host " Errors: $totalErrors" if ($bomFixed -gt 0) { Write-Host " BOM fixed: $bomFixed" -ForegroundColor Yellow } Write-Host "" if ($DryRun) { Write-Host " Remove -DryRun to execute actual deployment." -ForegroundColor Yellow } else { Write-Host " Done! Restart OpenClaw gateway to load new skills." -ForegroundColor Green Write-Host "" Write-Host " Verify:" -ForegroundColor DarkGray Write-Host " openclaw skills list" -ForegroundColor DarkGray Write-Host " openclaw skills info " -ForegroundColor DarkGray Write-Host " Test-Path ~/.openclaw/workspace/scripts/tools/openclaw-tool-runner.js" -ForegroundColor DarkGray } Write-Host ""