diff --git a/.claude/bootstrap/RESTORE.md b/.claude/bootstrap/RESTORE.md new file mode 100644 index 0000000..8b99298 --- /dev/null +++ b/.claude/bootstrap/RESTORE.md @@ -0,0 +1,124 @@ +# ClaudeTools Windows Bootstrap & Recovery Runbook + +Rebuild this workstation (GURU-5070, Lenovo Legion Pro 7 16IAX10H) after a clean +Windows reset. Everything here is driven by two scripts in this folder: + +- `windows-bootstrap.ps1` — installs tools, restores secrets, clones repos, wires tasks +- `restore-secrets.ps1` — copies secrets/identity from the recovery bundle back into place + +The recovery bundle lives on the removable drives: + +| Drive | Label | Holds | +|-------|---------|-------| +| **E:** | (FAT32) | `claudetools-recovery\` — secrets + identity + manifests (redundant copy) | +| **F:** | Ventoy | `claudetools-recovery\` — same bundle **plus** `data\` (large client data) | + +> F: is also a bootable rescue stick (SystemRescue, Boot Repair) — keep it; it can +> help fix the machine. The bundle lives in `F:\claudetools-recovery\`, Ventoy is untouched. + +--- + +## What's in the bundle (and why it can't just be re-cloned) + +`claudetools-recovery\` +- `secrets\` + - `sops-age\keys.txt` — **THE most critical file.** The SOPS age private key. Without + it the entire vault (`D:\vault`) is permanently undecryptable. Not stored in any repo. + - `ssh\` — `id_ed25519` (+pub), `pst-cc-ucg` (+pub), `config`, `known_hosts` + - `claude\` — `.claude.json`, `.credentials.json` (Claude Code login), settings, keybindings, statusline + - `grok\` — `auth.json`, `config.toml`, `agent_id` + - `gemini\` — `oauth_creds.json`, `google_accounts.json`, settings, installation_id + - `git\.gitconfig`, `powershell\Microsoft.PowerShell_profile.ps1` +- `identity\` — repo-local gitignored files: `identity.json`, `settings.local.json`, + `current-mode`, `coord-broadcasts-seen`, `mcp.json`, `.claude/state\`, ticktick tokens, dataforth oauth +- `config\` — Windows Terminal settings, fleet `hosts` file, quote-wizard `.env.production` +- `manifests\` — `installed-tools.txt`, `ollama-models.txt`, `git-global-config.txt`, + `repos.txt`, `user-environment.reg` / `.txt` (incl. `OLLAMA_MODELS`/`OLLAMA_HOST`/`PROTOC`), `scheduled-tasks\*.xml` +- `data\` (F: only) — large non-Gitea client/project data, repo-relative paths + +Everything else (all tracked code, skills, commands, docs, session logs, wiki) comes +back from Gitea on clone — no need to back it up. + +--- + +## Fast path (one shot) + +From an **elevated PowerShell**, with E: or F: plugged in: + +```powershell +# copy the script off the drive first (so it survives a re-clone) +Copy-Item F:\claudetools-recovery\bootstrap\windows-bootstrap.ps1 $env:TEMP\boot.ps1 +& $env:TEMP\.. # or just run directly: +F:\claudetools-recovery\bootstrap\windows-bootstrap.ps1 -SkipModels +``` + +`-SkipModels` defers the ~50 GB Ollama downloads. Drop it (or run Phase 8 later) when +you want them. Add `-RestoreData` to also pull back the large client data from `F:\...\data`. + +The script is **idempotent** — safe to re-run; it skips anything already done. To run +just part of it: `-OnlyPhases "1,2,3"`. + +--- + +## Manual path (if you'd rather do it by hand) + +1. **Install App Installer** (winget) from the Microsoft Store if missing. +2. **Core tools** (winget ids): + `Git.Git`, `OpenJS.NodeJS.LTS`, `Python.Python.3.14`, `Rustlang.Rustup`, + `Microsoft.VisualStudioCode`, `Ollama.Ollama`, `jqlang.jq`, + `SecretsOPerationS.SOPS`, `FiloSottile.age`, `GitHub.cli`, `AgileBits.1Password.CLI`, + `Microsoft.DotNet.SDK.8`, `Google.Protobuf`, `oschwartz10612.Poppler`, `Tailscale.Tailscale` + Then `dotnet tool install --global wix` (MSI builds). + Set env: `OLLAMA_MODELS=D:\OllamaModels`, `OLLAMA_HOST=0.0.0.0:11434`, `PROTOC=`. +3. **AI CLIs:** + - Claude: `irm https://claude.ai/install.ps1 | iex` → `~/.local/bin/claude.exe` + - Gemini: `npm install -g @google/gemini-cli` + - Grok: `bash -c "curl -fsSL https://x.ai/cli/install.sh | bash"` (Git Bash) +4. **Restore home secrets:** `F:\claudetools-recovery\bootstrap\restore-secrets.ps1 -Group home` +5. **Clone repos:** + ``` + git clone https://git.azcomputerguru.com/azcomputerguru/claudetools.git D:\claudetools + cd D:\claudetools; git submodule update --init --recursive + git clone https://git.azcomputerguru.com/azcomputerguru/vault.git D:\vault + ``` + (On-network you can use `http://172.16.3.20:3000/...` to bypass the SSL-renewal blips.) +6. **Restore identity:** `restore-secrets.ps1 -Group repo` +7. **Ollama models (proper set for this 12 GB-VRAM laptop):** + `ollama pull nomic-embed-text:latest` (GrepAI embeddings) and `ollama pull qwen3:8b` (prose_model). + Models live on `D:\OllamaModels` (47.8 GB) — **if D: survived the reset they're already there, skip this.** + Heavy extras (`qwen3:14b`, `codestral:22b`, `qwen3.6:latest`) are opt-in only; they over-saturate 12 GB VRAM. +8. **Scheduled tasks:** import each XML in `manifests\scheduled-tasks\` via + `Register-ScheduledTask -Xml (Get-Content x.xml -Raw) -TaskName "..."`. +9. **Verify:** `D:\claudetools\.claude\scripts\onboarding-diagnostic.ps1`, then `/self-check` in Claude Code. + +--- + +## Post-install: things that need an interactive login + +Auth tokens are backed up, but some expire. If a tool says it's unauthenticated: + +- **Claude Code:** run `claude`, then `/login` (browser). +- **GitHub CLI:** `gh auth login` +- **1Password:** `op signin` +- **Gemini:** launch `gemini`, complete the Google OAuth browser flow. +- **Grok:** `grok login` (tokens expire after 7 days). +- **Gitea git push:** uses the Windows Credential Manager (`credential.helper=manager`). + First push prompts for the shared `azcomputerguru` account. **Do NOT** bake the password + into the remote URL (the old `D:\work\gururmm` clone did — reset it to a clean URL). + +## Verify the vault decrypts (proves the age key restored correctly) + +``` +bash D:/claudetools/.claude/scripts/vault.sh list +bash D:/claudetools/.claude/scripts/vault.sh get-field projects/claudetools/database.sops.yaml credentials.password +``` + +If that returns the password, recovery succeeded. If it errors about decryption, the +age key at `%APPDATA%\sops\age\keys.txt` and `~/.config/sops/age/keys.txt` is missing/wrong. + +--- + +## Refreshing this bundle later + +Re-run the backup any time (it's just file copies): +`D:\claudetools\.claude\bootstrap\backup-to-bundle.ps1` (writes to E: and F:). diff --git a/.claude/bootstrap/backup-to-bundle.ps1 b/.claude/bootstrap/backup-to-bundle.ps1 new file mode 100644 index 0000000..92475a4 --- /dev/null +++ b/.claude/bootstrap/backup-to-bundle.ps1 @@ -0,0 +1,133 @@ +<# +.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 diff --git a/.claude/bootstrap/restore-secrets.ps1 b/.claude/bootstrap/restore-secrets.ps1 new file mode 100644 index 0000000..96ac8c8 --- /dev/null +++ b/.claude/bootstrap/restore-secrets.ps1 @@ -0,0 +1,147 @@ +<# +.SYNOPSIS + Restore ClaudeTools secrets + machine identity from a recovery bundle + (produced by the Windows bootstrap backup) back to their real locations. + +.DESCRIPTION + Two restore groups: + [home] -> out-of-repo secrets that live under the user profile + (SOPS age key, SSH keys, Claude/grok/gemini auth, git config, + PowerShell profile). These are needed BEFORE cloning repos. + [repo] -> repo-local, gitignored files that go back into D:\claudetools + (identity.json, settings.local.json, current-mode, .mcp.json, + .claude/state, ticktick tokens, dataforth oauth). These require + the claudetools repo to already be cloned. + + Idempotent. Only restores files that exist in the bundle. Never overwrites a + newer file unless -Force is given. + +.PARAMETER BundlePath + Path to the recovery bundle root (the folder containing 'secrets' and + 'identity'). Auto-detected from F:\ then E:\ if not supplied. + +.PARAMETER ClaudeToolsRoot + Where claudetools is / will be cloned. Default D:\claudetools. + +.PARAMETER Group + home | repo | all (default all). + +.EXAMPLE + .\restore-secrets.ps1 -Group home # before cloning repos + .\restore-secrets.ps1 -Group repo # after cloning claudetools +#> +[CmdletBinding()] +param( + [string]$BundlePath, + [string]$ClaudeToolsRoot = 'D:\claudetools', + [ValidateSet('home','repo','all')][string]$Group = 'all', + [switch]$Force +) +$ErrorActionPreference = 'Stop' + +function Find-Bundle { + foreach ($d in 'F:','E:','D:') { + $p = "$d\claudetools-recovery" + if (Test-Path "$p\secrets") { return $p } + } + return $null +} +if (-not $BundlePath) { $BundlePath = Find-Bundle } +if (-not $BundlePath -or -not (Test-Path "$BundlePath\secrets")) { + throw "Recovery bundle not found. Plug in the drive or pass -BundlePath. Looked for :\claudetools-recovery\secrets" +} +Write-Host "[INFO] Using recovery bundle: $BundlePath" -ForegroundColor Cyan + +function Restore-One($src, $dst) { + if (-not (Test-Path -LiteralPath $src)) { Write-Host "[SKIP] not in bundle: $src"; return } + $parent = Split-Path $dst -Parent + if ($parent -and -not (Test-Path $parent)) { New-Item -ItemType Directory -Force -Path $parent | Out-Null } + if ((Test-Path -LiteralPath $dst) -and -not $Force) { + Write-Host "[KEEP] exists (use -Force to overwrite): $dst" -ForegroundColor Yellow + return + } + Copy-Item -LiteralPath $src -Destination $dst -Force + Write-Host "[OK] $dst" -ForegroundColor Green +} + +# ---------------------------------------------------------------- HOME secrets +if ($Group -in 'home','all') { + Write-Host "`n=== Restoring home-profile secrets ===" -ForegroundColor Cyan + $u = $env:USERPROFILE + $s = "$BundlePath\secrets" + + # SOPS age key (CRITICAL - vault is undecryptable without it) + New-Item -ItemType Directory -Force -Path "$u\.config\sops\age" | Out-Null + New-Item -ItemType Directory -Force -Path "$env:APPDATA\sops\age" | Out-Null + Restore-One "$s\sops-age\keys.txt" "$u\.config\sops\age\keys.txt" + Restore-One "$s\sops-age\keys.txt" "$env:APPDATA\sops\age\keys.txt" + + # SSH + New-Item -ItemType Directory -Force -Path "$u\.ssh" | Out-Null + if (Test-Path "$s\ssh") { + Get-ChildItem "$s\ssh" -File | ForEach-Object { Restore-One $_.FullName "$u\.ssh\$($_.Name)" } + # lock down private key perms (remove inheritance, owner-only) + Get-ChildItem "$u\.ssh" -File | Where-Object { $_.Name -notmatch '\.pub$' -and $_.Name -ne 'known_hosts' -and $_.Name -ne 'config' } | ForEach-Object { + icacls $_.FullName /inheritance:r /grant:r "$($env:USERNAME):(F)" 2>$null | Out-Null + } + } + + # Claude Code auth/config + Restore-One "$s\claude\.claude.json" "$u\.claude.json" + Restore-One "$s\claude\.credentials.json" "$u\.claude\.credentials.json" + Restore-One "$s\claude\settings.json" "$u\.claude\settings.json" + Restore-One "$s\claude\keybindings.json" "$u\.claude\keybindings.json" + Restore-One "$s\claude\statusline-command.sh" "$u\.claude\statusline-command.sh" + + # grok + Restore-One "$s\grok\auth.json" "$u\.grok\auth.json" + Restore-One "$s\grok\config.toml" "$u\.grok\config.toml" + Restore-One "$s\grok\agent_id" "$u\.grok\agent_id" + + # gemini + Restore-One "$s\gemini\oauth_creds.json" "$u\.gemini\oauth_creds.json" + Restore-One "$s\gemini\google_accounts.json" "$u\.gemini\google_accounts.json" + Restore-One "$s\gemini\settings.json" "$u\.gemini\settings.json" + Restore-One "$s\gemini\installation_id" "$u\.gemini\installation_id" + + # user-global Claude commands + plugins (not in the repo) + if (Test-Path "$s\claude-global\commands") { + New-Item -ItemType Directory -Force -Path "$u\.claude\commands" | Out-Null + Copy-Item "$s\claude-global\commands\*" "$u\.claude\commands\" -Recurse -Force + Write-Host "[OK] $u\.claude\commands\*" -ForegroundColor Green + } + if (Test-Path "$s\claude-global\plugins") { + New-Item -ItemType Directory -Force -Path "$u\.claude\plugins" | Out-Null + Copy-Item "$s\claude-global\plugins\*" "$u\.claude\plugins\" -Recurse -Force + Write-Host "[OK] $u\.claude\plugins\*" -ForegroundColor Green + } + + # git global config + Restore-One "$s\git\.gitconfig" "$u\.gitconfig" + + # PowerShell profile + Restore-One "$s\powershell\Microsoft.PowerShell_profile.ps1" $PROFILE +} + +# ---------------------------------------------------------------- REPO-local +if ($Group -in 'repo','all') { + Write-Host "`n=== Restoring repo-local identity files ===" -ForegroundColor Cyan + if (-not (Test-Path $ClaudeToolsRoot)) { + Write-Host "[WARN] $ClaudeToolsRoot does not exist yet. Clone the repo first, then re-run with -Group repo." -ForegroundColor Yellow + } else { + $i = "$BundlePath\identity" + Restore-One "$i\identity.json" "$ClaudeToolsRoot\.claude\identity.json" + Restore-One "$i\settings.local.json" "$ClaudeToolsRoot\.claude\settings.local.json" + Restore-One "$i\current-mode" "$ClaudeToolsRoot\.claude\current-mode" + Restore-One "$i\coord-broadcasts-seen" "$ClaudeToolsRoot\.claude\coord-broadcasts-seen" + Restore-One "$i\mcp.json" "$ClaudeToolsRoot\.mcp.json" + Restore-One "$i\ticktick-tokens.json" "$ClaudeToolsRoot\mcp-servers\ticktick\.tokens.json" + Restore-One "$i\dataforth-oauth.txt" "$ClaudeToolsRoot\clients\dataforth\Oauth.txt" + if (Test-Path "$i\state") { + New-Item -ItemType Directory -Force -Path "$ClaudeToolsRoot\.claude\state" | Out-Null + Copy-Item "$i\state\*" "$ClaudeToolsRoot\.claude\state\" -Recurse -Force + Write-Host "[OK] $ClaudeToolsRoot\.claude\state\*" -ForegroundColor Green + } + } +} +Write-Host "`n[DONE] restore-secrets.ps1 ($Group)" -ForegroundColor Cyan diff --git a/.claude/bootstrap/windows-bootstrap.ps1 b/.claude/bootstrap/windows-bootstrap.ps1 new file mode 100644 index 0000000..4fc4028 --- /dev/null +++ b/.claude/bootstrap/windows-bootstrap.ps1 @@ -0,0 +1,268 @@ +<# +.SYNOPSIS + ClaudeTools Windows bootstrap - rebuild a workstation after a clean OS reset. + +.DESCRIPTION + Installs every tool ClaudeTools needs, restores secrets + identity from the + recovery bundle, clones the repos, wires up scheduled tasks, and verifies. + Designed to be run top-to-bottom on a fresh Windows 11 install. Idempotent: + re-running skips anything already present. + + ORDER OF OPERATIONS (each phase depends on the previous): + 0. Preflight - winget, execution policy, UTF-8 + 1. Core tooling - git, node, python, rust, vscode, ollama, jq, sops, age, gh, op + 2. PATH refresh - make freshly-installed tools callable this session + 3. AI CLIs - claude (native), gemini (npm), grok (git-bash installer) + 4. Restore secrets - age key, ssh, tool auth, git config, PS profile [home group] + 5. Clone repos - claudetools + vault + submodules + 6. Restore identity - identity.json, settings.local, .mcp.json, state [repo group] + 7. Python deps - pip installs for MCP servers / scripts + 8. Ollama models - pull qwen/codestral/nomic (optional, large) + 9. Scheduled tasks - GrepAI watcher, orphan detector, smartbadge + 10. Large data - restore client data from bundle (optional) + 11. Verify - onboarding diagnostic + +.PARAMETER BundlePath + Recovery bundle root (folder containing 'secrets'/'identity'). Auto-detect F:\ then E:\. + +.PARAMETER SkipModels Skip the multi-GB ollama model pulls. +.PARAMETER RestoreData Also restore the large client data from \data. +.PARAMETER GiteaHost Gitea base URL. Default git.azcomputerguru.com (use 172.16.3.20:3000 on-network). +.PARAMETER OnlyPhases Comma list of phase numbers to run (e.g. "1,2,3"). Default: all. + +.EXAMPLE + # full rebuild, skip giant model downloads for now + .\windows-bootstrap.ps1 -SkipModels + +.NOTES + Run from an elevated PowerShell for cleanest winget machine-scope installs, + though most packages also install at user scope without admin. +#> +[CmdletBinding()] +param( + [string]$BundlePath, + [switch]$SkipModels, + [switch]$RestoreData, + [string]$GiteaHost = 'https://git.azcomputerguru.com', + [string]$ClaudeToolsRoot = 'D:\claudetools', + [string]$VaultRoot = 'D:\vault', + [string]$OnlyPhases +) +$ErrorActionPreference = 'Stop' +$here = Split-Path -Parent $MyInvocation.MyCommand.Path + +function Phase($n,$title){ if ($OnlyPhases -and ($OnlyPhases -split ',').Trim() -notcontains "$n") { return $false }; Write-Host "`n========== PHASE $n : $title ==========" -ForegroundColor Cyan; return $true } +function Info($m){ Write-Host "[INFO] $m" } +function Ok($m){ Write-Host "[OK] $m" -ForegroundColor Green } +function Warn($m){ Write-Host "[WARN] $m" -ForegroundColor Yellow } +function Have($cmd){ [bool](Get-Command $cmd -ErrorAction SilentlyContinue) } +function Refresh-Path { $env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User') } + +function Find-Bundle { + if ($BundlePath -and (Test-Path "$BundlePath\secrets")) { return $BundlePath } + foreach ($d in 'F:','E:','D:') { if (Test-Path "$d\claudetools-recovery\secrets") { return "$d\claudetools-recovery" } } + return $null +} + +# ============================================================ PHASE 0 +if (Phase 0 'Preflight') { + [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + try { Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force } catch {} + if (-not (Have winget)) { throw "winget not found. Install 'App Installer' from the Microsoft Store, then re-run." } + Ok "winget present: $((Get-Command winget).Source)" + $script:Bundle = Find-Bundle + if ($script:Bundle) { Ok "recovery bundle: $script:Bundle" } else { Warn "no recovery bundle found - secret/identity restore phases will be skipped" } +} + +# ============================================================ PHASE 1 +if (Phase 1 'Core tooling (winget)') { + $pkgs = @( + @{id='Git.Git'; cmd='git'}, + @{id='OpenJS.NodeJS.LTS'; cmd='node'}, + @{id='Python.Python.3.14'; cmd='py'}, + @{id='Rustlang.Rustup'; cmd='cargo'}, + @{id='Microsoft.VisualStudioCode'; cmd='code'}, + @{id='Ollama.Ollama'; cmd='ollama'}, + @{id='jqlang.jq'; cmd='jq'}, + @{id='SecretsOPerationS.SOPS'; cmd='sops'}, + @{id='FiloSottile.age'; cmd='age'}, + @{id='GitHub.cli'; cmd='gh'}, + @{id='AgileBits.1Password.CLI'; cmd='op'}, + @{id='Microsoft.DotNet.SDK.8'; cmd='dotnet'}, # MSI builds / wix + @{id='Google.Protobuf'; cmd='protoc'}, # gururmm prost builds (PROTOC env) + @{id='oschwartz10612.Poppler'; cmd='pdftoppm'}, # dataforth datasheet PDF pipeline + @{id='Tailscale.Tailscale'; cmd='tailscale'} # fleet connectivity (100.x mesh) + ) + foreach ($p in $pkgs) { + if (Have $p.cmd) { Ok "$($p.cmd) already installed"; continue } + Info "installing $($p.id) ..." + winget install --id $p.id --exact --silent --accept-package-agreements --accept-source-agreements --disable-interactivity + if ($LASTEXITCODE -ne 0) { Warn "winget returned $LASTEXITCODE for $($p.id) (may already be installed or need elevation)" } + } + Refresh-Path +} + +# ============================================================ PHASE 2 +if (Phase 2 'PATH refresh') { + Refresh-Path + foreach ($c in 'git','node','npm','py','cargo','jq','sops','age','gh','op','ollama','code','dotnet','protoc','tailscale') { + if (Have $c) { Ok "$c -> $((Get-Command $c).Source)" } else { Warn "$c still not on PATH (open a new shell after install)" } + } + # PROTOC env var for Rust prost builds (path is version-specific, so resolve it live) + $protoc = (Get-Command protoc -ErrorAction SilentlyContinue).Source + if ($protoc) { [Environment]::SetEnvironmentVariable('PROTOC',$protoc,'User'); $env:PROTOC=$protoc; Ok "PROTOC=$protoc" } +} + +# ============================================================ PHASE 3 +if (Phase 3 'AI CLIs') { + # Claude Code - official native installer -> %USERPROFILE%\.local\bin\claude.exe + if (Have claude) { Ok "claude already installed" } else { + Info "installing Claude Code (native installer)" + try { irm https://claude.ai/install.ps1 | iex } catch { Warn "claude install failed: $_ (manual: irm https://claude.ai/install.ps1 | iex)" } + } + # Gemini CLI - npm global + if (Have gemini) { Ok "gemini already installed" } else { + Info "installing @google/gemini-cli" + npm install -g @google/gemini-cli + } + # Grok CLI - xAI installer (bash; needs Git Bash from Phase 1) + if (Have grok) { Ok "grok already installed" } else { + $bash = 'C:\Program Files\Git\bin\bash.exe' + if (Test-Path $bash) { Info "installing grok via $bash"; & $bash -lc "curl -fsSL https://x.ai/cli/install.sh | bash" } + else { Warn "Git Bash not found; install Git first, then: bash -c 'curl -fsSL https://x.ai/cli/install.sh | bash'" } + } + Refresh-Path + $env:Path += ";$env:USERPROFILE\.local\bin;$env:USERPROFILE\.grok\bin;$env:APPDATA\npm" +} + +# ============================================================ PHASE 4 +if (Phase 4 'Restore home secrets + machine config') { + if ($script:Bundle) { + & "$here\restore-secrets.ps1" -BundlePath $script:Bundle -Group home + + # Stable machine env vars (NOT a blanket reg import - the saved PATH has stale + # version-pinned winget paths. user-environment.reg is kept as reference only.) + [Environment]::SetEnvironmentVariable('OLLAMA_MODELS','D:\OllamaModels','User'); $env:OLLAMA_MODELS='D:\OllamaModels' + [Environment]::SetEnvironmentVariable('OLLAMA_HOST','0.0.0.0:11434','User'); $env:OLLAMA_HOST='0.0.0.0:11434' + Ok "set OLLAMA_MODELS=D:\OllamaModels, OLLAMA_HOST=0.0.0.0:11434" + + # Windows Terminal settings + $wtDst = "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json" + if (Test-Path "$script:Bundle\config\windows-terminal-settings.json") { + $p = Split-Path $wtDst -Parent + if (Test-Path $p) { Copy-Item "$script:Bundle\config\windows-terminal-settings.json" $wtDst -Force; Ok "Windows Terminal settings restored" } + else { Warn "Windows Terminal not installed yet - restore its settings.json later from config\" } + } + + # hosts file (fleet Tailscale MagicDNS entries) - needs admin; merge note only + if (Test-Path "$script:Bundle\config\hosts") { + Warn "fleet hosts entries are in config\hosts - merge into $env:WINDIR\System32\drivers\etc\hosts as admin if Tailscale MagicDNS isn't resolving" + } + } + else { Warn "no bundle - skipping. Restore the SOPS age key + SSH keys manually or the vault will not decrypt." } +} + +# ============================================================ PHASE 5 +if (Phase 5 'Clone repos') { + if (-not (Test-Path "$ClaudeToolsRoot\.git")) { + Info "cloning claudetools -> $ClaudeToolsRoot" + git clone "$GiteaHost/azcomputerguru/claudetools.git" $ClaudeToolsRoot + Push-Location $ClaudeToolsRoot + Info "initializing submodules (gururmm / guruconnect)" + git submodule update --init --recursive + Pop-Location + } else { Ok "claudetools repo already present" } + + if (-not (Test-Path "$VaultRoot\.git")) { + Info "cloning vault -> $VaultRoot" + git clone "$GiteaHost/azcomputerguru/vault.git" $VaultRoot + } else { Ok "vault repo already present" } + + # safe.directory entries (mirror the prior machine) + foreach ($d in $ClaudeToolsRoot,$VaultRoot,"$ClaudeToolsRoot/projects/msp-tools/guru-rmm") { + git config --global --add safe.directory ($d -replace '\\','/') 2>$null + } +} + +# ============================================================ PHASE 6 +if (Phase 6 'Restore repo-local identity') { + if ($script:Bundle) { & "$here\restore-secrets.ps1" -BundlePath $script:Bundle -Group repo -ClaudeToolsRoot $ClaudeToolsRoot } + else { Warn "no bundle - you must hand-create .claude/identity.json (see CLAUDE.md multi-user section)" } +} + +# ============================================================ PHASE 7 +if (Phase 7 'Python deps + .NET tools') { + # WiX toolset (MSI builds, e.g. gururmm agent) - dotnet global tool + if (Have dotnet) { + if (dotnet tool list --global 2>$null | Select-String '\bwix\b') { Ok "wix tool already installed" } + else { Info "installing wix dotnet tool"; dotnet tool install --global wix 2>$null } + } + if (Have py) { + py -m pip install --upgrade pip 2>$null + $reqs = Get-ChildItem $ClaudeToolsRoot -Recurse -Filter 'requirements*.txt' -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -notmatch '\\(node_modules|\.venv|venv|target)\\' } + foreach ($r in $reqs) { Info "pip install -r $($r.FullName)"; py -m pip install -r $r.FullName 2>$null } + # baseline libs used by helper scripts / MCP + py -m pip install requests paramiko mcp 2>$null + Ok "python deps installed (best-effort)" + } else { Warn "py launcher missing - skip" } +} + +# ============================================================ PHASE 8 +if (Phase 8 'Ollama models') { + # Proper model set for THIS machine (Legion Pro 7, RTX 5070 Ti = 12 GB VRAM): + # nomic-embed-text - REQUIRED for GrepAI semantic search (embeddings) + # qwen3:8b - configured prose_model (identity.json) + # The heavier models (qwen3:14b 9GB, codestral:22b 12GB, qwen3.6:latest 23GB) + # over-saturate 12 GB VRAM on a laptop. Pull them by hand only if needed: + # ollama pull qwen3:14b # codestral:22b # qwen3.6:latest + $models = @('nomic-embed-text:latest','qwen3:8b') + if ($SkipModels) { Warn "-SkipModels set, skipping model pulls" } + elseif (Have ollama) { + # Models live on D:\OllamaModels. If D: survived the reset they are already here. + if (-not $env:OLLAMA_MODELS) { [Environment]::SetEnvironmentVariable('OLLAMA_MODELS','D:\OllamaModels','User'); $env:OLLAMA_MODELS='D:\OllamaModels' } + $have = (ollama list 2>$null | Out-String) + foreach ($m in $models) { + $short = $m -replace ':latest$','' + if ($have -match [regex]::Escape($short)) { Ok "$m already present on D:\OllamaModels (no download)" } + else { Info "ollama pull $m"; ollama pull $m } + } + } else { Warn "ollama missing - skip" } +} + +# ============================================================ PHASE 9 +if (Phase 9 'Scheduled tasks') { + $tdir = "$script:Bundle\manifests\scheduled-tasks" + if ($script:Bundle -and (Test-Path $tdir)) { + Get-ChildItem $tdir -Filter *.xml | ForEach-Object { + $name = ($_.BaseName -replace '_',' ') + try { + $xml = Get-Content $_.FullName -Raw + Register-ScheduledTask -TaskName $name -Xml $xml -Force -ErrorAction Stop | Out-Null + Ok "registered task: $name" + } catch { Warn "task '$name' import failed: $($_.Exception.Message) (paths/user may differ - re-create manually)" } + } + } else { Warn "no exported tasks in bundle - skip (see manifests\scheduled-tasks)" } +} + +# ============================================================ PHASE 10 +if (Phase 10 'Large client data (optional)') { + if ($RestoreData -and $script:Bundle -and (Test-Path "$script:Bundle\data")) { + Info "restoring large data $script:Bundle\data -> $ClaudeToolsRoot" + robocopy "$script:Bundle\data" $ClaudeToolsRoot /E /R:1 /W:1 /NFL /NDL /NP | Out-Null + Ok "large data restored" + } else { Warn "skipped (pass -RestoreData to restore client data clusters)" } +} + +# ============================================================ PHASE 11 +if (Phase 11 'Verify') { + $diag = "$ClaudeToolsRoot\.claude\scripts\onboarding-diagnostic.ps1" + if (Test-Path $diag) { Info "running onboarding diagnostic"; & $diag } + else { Warn "diagnostic not found - run '/self-check' inside Claude Code to verify wiring" } + Write-Host "`n[NEXT] Interactive logins that may need a refresh (tokens expire):" -ForegroundColor Cyan + Write-Host " claude (if .credentials.json expired: run 'claude' and /login)" + Write-Host " gh auth login op signin gemini (browser) grok login" + Write-Host " Verify vault: bash $ClaudeToolsRoot/.claude/scripts/vault.sh list" +} + +Write-Host "`n[DONE] windows-bootstrap.ps1 complete." -ForegroundColor Green diff --git a/.claude/machines/guru-5070.md b/.claude/machines/guru-5070.md new file mode 100644 index 0000000..8322fe2 --- /dev/null +++ b/.claude/machines/guru-5070.md @@ -0,0 +1,67 @@ +# Machine: GURU-5070 (Windows) + +**Hostname:** GURU-5070 +**User:** Mike Swanson (mike) — admin +**Platform:** Windows 11 Pro 10.0.26200 +**Last Updated:** 2026-06-06 + +> Same physical hardware as `acg-guru-5070.md` (Lenovo Legion Pro 7 16IAX10H) — +> that profile documents the prior CachyOS Linux install. This box now runs Windows. + +--- + +## Hardware + +| Spec | Value | +|------|-------| +| Model | Lenovo Legion Pro 7 16IAX10H (DMI 83F5) | +| CPU | Intel Core Ultra 9 275HX (24 cores) | +| Memory | 32 GB DDR5 | +| GPU | NVIDIA GeForce RTX 5070 Ti Laptop (12 GB) | +| Disks | C: 952 GB NVMe (OS), D: 953 GB NVMe (dev — `D:\claudetools`, `D:\vault`, `D:\work`) | + +## Paths + +| What | Where | +|------|-------| +| ClaudeTools | `D:\claudetools` | +| Vault | `D:\vault` | +| Other repos | `D:\work\gururmm` | +| SOPS age key | `%APPDATA%\sops\age\keys.txt` and `~\.config\sops\age\keys.txt` | +| Claude CLI | `~\.local\bin\claude.exe` (native installer) | +| Grok CLI | `~\.grok\bin\grok.exe` | +| Gemini CLI | npm global (`@google/gemini-cli`) | + +## Toolchain (as of 2026-06-06) + +node 24.x · npm 11.x · py/Python 3.14 · git 2.53 · cargo/rustc 1.96 · +ollama 0.30.6 · jq 1.8 · sops 3.7→3.12 · age 1.3 · op 2.33 · VS Code 1.113 · +claude 2.1.x · gemini 0.45 · grok 0.2.x. **gh was missing** — bootstrap installs it. + +Ollama models: `nomic-embed-text`, `qwen3:8b`, `qwen3:14b`, `codestral:22b`, `qwen3.6:latest`. + +## Scheduled tasks (ClaudeTools) + +- `GrepAI Watcher - claudetools` → `D:\claudetools\grepai.exe watch --background` (logon) +- `ClaudeTools - Orphaned Session Detector` → `py detect_orphaned_sessions.py` (logon + daily) +- `ClaudeTools - KSTEEN SmartBadge Daily` → git-bash `check-ksteen-smartbadge.sh` (daily) + +## Capabilities + +- [x] Git / Gitea, SSH to infra +- [x] GrepAI watcher +- [x] Ollama local AI (RTX 5070 Ti — light/inference OK) +- [x] MCP: ticktick, grepai +- [x] claude / gemini / grok CLIs (fleet host for all three) + +## Recovery + +Full rebuild after a reset: `.claude\bootstrap\RESTORE.md`. +Recovery bundle on **E:** and **F:** (`\claudetools-recovery\`). Refresh it with +`.claude\bootstrap\backup-to-bundle.ps1`. + +## Known issues + +- Old `D:\work\gururmm` remote URL embedded the shared Gitea password in plaintext — + reset to a clean URL + Windows Credential Manager on rebuild. +- (Hardware) RTX 5070 Ti GSP firmware bug under sustained GPU compute — see `acg-guru-5070.md`. diff --git a/session-logs/2026-06-06-windows-bootstrap-recovery.md b/session-logs/2026-06-06-windows-bootstrap-recovery.md new file mode 100644 index 0000000..52c008c --- /dev/null +++ b/session-logs/2026-06-06-windows-bootstrap-recovery.md @@ -0,0 +1,71 @@ +# Session Log: Windows Bootstrap & Recovery Toolkit (GURU-5070) + +**Date:** 2026-06-06 +**Mode:** infra +**Topic:** Build a full backup + bootstrap to rebuild this workstation after a clean Windows reset. + +## User +- **User:** Mike Swanson (mike) +- **Machine:** GURU-5070 +- **Role:** admin + +## Context + +The machine became unstable ("super broken all of the sudden") and a full OS reset is +planned. Goal: preserve everything that won't return from a `git clone` (identity + +secrets + select large data), and produce a clean, less-bloated reinstall path. + +## What was done + +### 1. Inventory +Full read-only sweep of the environment: +- Tools/versions/paths (node 24, py 3.14, git 2.53, cargo 1.96, ollama 0.30.6, claude 2.1.x, + gemini 0.45, grok 0.2.x, jq, sops 3.7, age 1.3, op 2.33, VS Code 1.113). **`gh` was missing.** +- Out-of-repo secrets: SOPS **age key** (`%APPDATA%\sops\age\keys.txt`), SSH keys, Claude/grok/gemini auth. +- Gitignored repo data, scheduled tasks, MCP config, ollama models, env vars, hosts file. +- Drives: **E:** FAT32 removable 14.6 GB · **F:** Ventoy exFAT 57.7 GB (26.8 GB free). + +### 2. Backup (two-drive) +- **E:** redundant copy of the crown jewels (secrets + identity + config + manifests + bootstrap), ~10 MB. +- **F:** full bundle **+** `data\` 9.11 GB of large non-Gitea client/project data. +- Bundle layout: `secrets\` (age key, ssh, claude/grok/gemini auth, git config, PS profile, + user-global `~/.claude` commands+plugins), `identity\` (identity.json, settings.local.json, + current-mode, .mcp.json, .claude/state, ticktick tokens, dataforth oauth), + `config\` (Windows Terminal, hosts, quote-wizard .env.production), + `manifests\` (tools, ollama models, scheduled-task XML, user-environment.reg). +- **Age key hash-verified identical** across source/E:/F: (pubkey `age1qz7ct84...`). + +### 3. Bootstrap toolkit (committed to `.claude/bootstrap/`) +- `windows-bootstrap.ps1` — 11-phase idempotent rebuild (winget core tools → AI CLIs → + restore secrets → clone repos+submodules → restore identity → python/.NET deps → + ollama models → scheduled tasks → optional data → verify). +- `restore-secrets.ps1` — restores home secrets + repo-local identity. +- `backup-to-bundle.ps1` — repeatable backup (re-run anytime to refresh the drives). +- `RESTORE.md` — human runbook. + +### Key findings folded into the bootstrap +- **47.8 GB of Ollama models live on `D:\OllamaModels`** (OLLAMA_MODELS env). If the reset + only wipes C:, models + repos survive — bootstrap auto-detects and skips re-download. +- Added missing tooling: **.NET 8 SDK + WiX 5** (MSI builds), **Protobuf/protoc** (gururmm + prost builds, PROTOC env), **Poppler** (dataforth PDF pipeline), **Tailscale** (fleet mesh), **gh**. +- Env vars restored: `OLLAMA_MODELS=D:\OllamaModels`, `OLLAMA_HOST=0.0.0.0:11434`, `PROTOC` (resolved live). +- **Ollama model set trimmed** to the proper two for a 12 GB-VRAM laptop: `nomic-embed-text` + (GrepAI) + `qwen3:8b` (prose_model). Heavy `qwen3.6:latest`/`codestral:22b`/`qwen3:14b` are opt-in. +- **Radio-show MP3s excluded** from backup (they live on IX Web Hosting). + +### Notes / gotchas +- `D:\work\gururmm` remote URL embeds the shared Gitea password in plaintext — rebuild uses + Windows Credential Manager instead (flagged in RESTORE.md and machines/guru-5070.md). +- Reset scope (C: only vs whole disk) is unknown → backup assumes worst case, safe either way. +- Created `machines/guru-5070.md` (the only prior profile, `acg-guru-5070.md`, documents the + old CachyOS install on the same Lenovo Legion Pro 7 hardware). + +## State at end of session +- E: and F: recovery bundles complete and verified. +- Bootstrap toolkit committed to repo (3rd copy, fleet-reusable). +- Part 2 (the actual reset + rebuild) pending — driven by `F:\claudetools-recovery\bootstrap\windows-bootstrap.ps1`. + +## Follow-ups +- When ready to reset: run the bootstrap from the drive; verify vault decrypts via + `vault.sh get-field projects/claudetools/database.sops.yaml credentials.password`. +- Re-auth interactive logins if tokens expired (claude /login, gh, op, gemini, grok).