<# .SYNOPSIS Back up ClaudeTools secrets + identity (and optionally large client data) to a recovery bundle on a removable drive. The inverse of restore-secrets.ps1. .DESCRIPTION Captures everything that will NOT come back from a `git clone`: - out-of-repo secrets under the user profile (age key, ssh, tool auth, git, PS profile) - repo-local gitignored identity files - environment manifests (installed tools, ollama models, scheduled-task XML, vscode ext) - (optional) large gitignored client/project data clusters Safe to re-run; it refreshes the bundle in place. .PARAMETER Drives Target drive roots. Default 'E:','F:' (writes the small bundle to both). .PARAMETER IncludeData Also copy the large client-data clusters (only to the FIRST drive with room; exFAT recommended). .PARAMETER ClaudeToolsRoot Default D:\claudetools. .EXAMPLE .\backup-to-bundle.ps1 # secrets+identity+manifests to E: and F: .\backup-to-bundle.ps1 -IncludeData # also large data (to F:) #> [CmdletBinding()] param( [string[]]$Drives = @('E:','F:'), [switch]$IncludeData, [string]$ClaudeToolsRoot = 'D:\claudetools', [string]$DataDrive = 'F:' ) $ErrorActionPreference = 'Stop' $u = $env:USERPROFILE function Save($src,$dst){ if (Test-Path -LiteralPath $src) { $p = Split-Path $dst -Parent; if (-not (Test-Path $p)) { New-Item -ItemType Directory -Force -Path $p | Out-Null } Copy-Item -LiteralPath $src -Destination $dst -Force; Write-Host "[OK] $src" } else { Write-Host "[MISS] $src" } } # Build the bundle once under the first available target, then mirror to the rest. $primary = $Drives | Where-Object { Test-Path "$_\" } | Select-Object -First 1 if (-not $primary) { throw "None of the target drives are accessible: $($Drives -join ', ')" } $root = "$primary\claudetools-recovery" Write-Host "=== building bundle at $root ===" -ForegroundColor Cyan foreach ($d in 'secrets\sops-age','secrets\ssh','secrets\claude','secrets\grok','secrets\gemini','secrets\git','secrets\powershell','identity\state','manifests\scheduled-tasks','bootstrap') { New-Item -ItemType Directory -Force -Path "$root\$d" | Out-Null } # --- secrets --- Save "$u\.config\sops\age\keys.txt" "$root\secrets\sops-age\keys.txt" if (Test-Path "$u\.ssh") { Copy-Item "$u\.ssh\*" "$root\secrets\ssh\" -Force; Write-Host "[OK] ~/.ssh/*" } Save "$u\.claude.json" "$root\secrets\claude\.claude.json" Save "$u\.claude\.credentials.json" "$root\secrets\claude\.credentials.json" Save "$u\.claude\settings.json" "$root\secrets\claude\settings.json" Save "$u\.claude\keybindings.json" "$root\secrets\claude\keybindings.json" Save "$u\.claude\statusline-command.sh" "$root\secrets\claude\statusline-command.sh" Save "$u\.grok\auth.json" "$root\secrets\grok\auth.json" Save "$u\.grok\config.toml" "$root\secrets\grok\config.toml" Save "$u\.grok\agent_id" "$root\secrets\grok\agent_id" Save "$u\.gemini\oauth_creds.json" "$root\secrets\gemini\oauth_creds.json" Save "$u\.gemini\google_accounts.json" "$root\secrets\gemini\google_accounts.json" Save "$u\.gemini\settings.json" "$root\secrets\gemini\settings.json" Save "$u\.gemini\installation_id" "$root\secrets\gemini\installation_id" Save "$u\.gitconfig" "$root\secrets\git\.gitconfig" # user-global Claude commands + plugins (not in repo) if (Test-Path "$u\.claude\commands") { New-Item -ItemType Directory -Force -Path "$root\secrets\claude-global\commands" | Out-Null; robocopy "$u\.claude\commands" "$root\secrets\claude-global\commands" /E /R:1 /W:1 /NFL /NDL /NJH /NJS /NP | Out-Null; Write-Host "[OK] ~/.claude/commands" } if (Test-Path "$u\.claude\plugins") { New-Item -ItemType Directory -Force -Path "$root\secrets\claude-global\plugins" | Out-Null; robocopy "$u\.claude\plugins" "$root\secrets\claude-global\plugins" /E /R:1 /W:1 /NFL /NDL /NJH /NJS /NP | Out-Null; Write-Host "[OK] ~/.claude/plugins" } Save $PROFILE "$root\secrets\powershell\Microsoft.PowerShell_profile.ps1" # --- repo-local identity --- Save "$ClaudeToolsRoot\.claude\identity.json" "$root\identity\identity.json" Save "$ClaudeToolsRoot\.claude\settings.local.json" "$root\identity\settings.local.json" Save "$ClaudeToolsRoot\.claude\current-mode" "$root\identity\current-mode" Save "$ClaudeToolsRoot\.claude\coord-broadcasts-seen" "$root\identity\coord-broadcasts-seen" Save "$ClaudeToolsRoot\.mcp.json" "$root\identity\mcp.json" Save "$ClaudeToolsRoot\mcp-servers\ticktick\.tokens.json" "$root\identity\ticktick-tokens.json" Save "$ClaudeToolsRoot\clients\dataforth\Oauth.txt" "$root\identity\dataforth-oauth.txt" if (Test-Path "$ClaudeToolsRoot\.claude\state") { Copy-Item "$ClaudeToolsRoot\.claude\state\*" "$root\identity\state\" -Recurse -Force -ErrorAction SilentlyContinue } # --- bootstrap scripts (so the drive is self-contained) --- Copy-Item "$ClaudeToolsRoot\.claude\bootstrap\*.ps1" "$root\bootstrap\" -Force -ErrorAction SilentlyContinue Copy-Item "$ClaudeToolsRoot\.claude\bootstrap\RESTORE.md" "$root\bootstrap\" -Force -ErrorAction SilentlyContinue # --- manifests --- $m = "$root\manifests" $tools = 'node','npm','claude','gemini','grok','ollama','py','git','gh','jq','sops','age','cargo','rustc','code','op' ($tools | ForEach-Object { $c = Get-Command $_ -ErrorAction SilentlyContinue; if ($c) { $v = try { (& $_ --version 2>$null | Select-Object -First 1) } catch {''}; "{0,-10} {1,-55} {2}" -f $_,$c.Source,$v } else { "{0,-10} NOT INSTALLED" -f $_ } }) | Out-File "$m\installed-tools.txt" -Encoding utf8 ollama list 2>$null | Out-File "$m\ollama-models.txt" -Encoding utf8 git config --global --list | Out-File "$m\git-global-config.txt" -Encoding utf8 $ext = & code --list-extensions 2>$null; if ($ext) { $ext | Out-File "$m\vscode-extensions.txt" -Encoding utf8 } foreach ($tn in "GrepAI Watcher - claudetools","ClaudeTools - Orphaned Session Detector","ClaudeTools - KSTEEN SmartBadge Daily") { $safe = ($tn -replace '[^\w\-]','_') try { Export-ScheduledTask -TaskName $tn 2>$null | Out-File "$m\scheduled-tasks\$safe.xml" -Encoding utf8 } catch {} } # user environment vars (.reg restorable + readable) reg export "HKCU\Environment" "$m\user-environment.reg" /y 2>$null | Out-Null (Get-Item 'HKCU:\Environment' | Select-Object -ExpandProperty Property | ForEach-Object { "{0}={1}" -f $_, (Get-ItemProperty 'HKCU:\Environment' -Name $_).$_ }) | Out-File "$m\user-environment.txt" -Encoding utf8 # --- machine config (Windows Terminal, hosts, repo-local real .env files) --- New-Item -ItemType Directory -Force -Path "$root\config" | Out-Null $wt = "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json" if (Test-Path $wt) { Save $wt "$root\config\windows-terminal-settings.json" } Save "$env:WINDIR\System32\drivers\etc\hosts" "$root\config\hosts" Save "$ClaudeToolsRoot\projects\msp-tools\quote-wizard\frontend\.env.production" "$root\config\quote-wizard.frontend.env.production" # --- large data (optional) --- if ($IncludeData) { $base = "$DataDrive\claudetools-recovery\data" $xd = @('node_modules','.venv','venv','__pycache__','target','.grepai','.pytest_cache','dist','build') $xf = @('Thumbs.db','desktop.ini','*.pyc','*.mp3') # radio-show MP3s live on IX Web Hosting - not backed up here $clusters = @( 'clients\valleywide\app-modernization\source-analysis', 'clients\grabb-durando\ai-demand-review', 'projects\dataforth-dos\datasheet-pipeline', 'projects\dataforth-dos\dfwds-research', 'projects\radio-show\audio-processor' ) Write-Host "=== copying large data to $base ===" -ForegroundColor Cyan foreach ($c in $clusters) { if (Test-Path "$ClaudeToolsRoot\$c") { robocopy "$ClaudeToolsRoot\$c" "$base\$c" /E /R:1 /W:1 /XD $xd /XF $xf /NFL /NDL /NP | Out-Null; Write-Host "[OK] $c" } } } # --- mirror small bundle to the other drives --- foreach ($d in $Drives) { if ($d -eq $primary) { continue } if (Test-Path "$d\") { Write-Host "=== mirroring bundle -> $d\claudetools-recovery ===" -ForegroundColor Cyan robocopy $root "$d\claudetools-recovery" /E /R:1 /W:1 /XD data /NFL /NDL /NP | Out-Null Write-Host "[OK] mirrored to $d" } } Write-Host "`n[DONE] backup-to-bundle.ps1" -ForegroundColor Green