<# .SYNOPSIS Dumps Windows event logs, firewall state, network config and OBS logs to plain text so Claude can actually read them. Self-elevating. .DESCRIPTION Event logs and firewall rules are not files, so the MCP filesystem server cannot reach them -- .evtx is binary and useless to read directly. This writes a text snapshot into a folder that IS in Claude's scope. Run it when something breaks, then ask Claude to read the newest folder in Diagnostics. PRIVACY: the output contains the hostname, local and public-facing IPs, the Windows username, installed software and recent error text. That is the point -- it is diagnostics -- but it is also what gets pasted into a chat, so do not post these files publicly. .PARAMETER OutputRoot Where snapshots land. Defaults to \Diagnostics, which the setup script grants Claude access to. .PARAMETER Hours How far back to pull events. Default 48. .EXAMPLE .\collect-diagnostics.ps1 #> [CmdletBinding()] param( [string]$OutputRoot = "$env:USERPROFILE\Diagnostics", [int]$Hours = 48, [int]$KeepLast = 10 ) $ErrorActionPreference = "Continue" # --- Self-elevate: event logs and firewall need admin --- $isAdmin = ([Security.Principal.WindowsPrincipal] ` [Security.Principal.WindowsIdentity]::GetCurrent() ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin) { Write-Host "Elevating..." $argList = @( "-NoProfile" "-ExecutionPolicy", "Bypass" "-File", "`"$PSCommandPath`"" "-OutputRoot", "`"$OutputRoot`"" "-Hours", $Hours ) Start-Process powershell -Verb RunAs -ArgumentList $argList return } $stamp = Get-Date -Format "yyyy-MM-dd_HHmmss" $out = Join-Path $OutputRoot $stamp New-Item -Path $out -ItemType Directory -Force | Out-Null function Save($name, $scriptblock) { $path = Join-Path $out $name Write-Host " $name" try { & $scriptblock 2>&1 | Out-String -Width 400 | Set-Content -Path $path -Encoding UTF8 } catch { "FAILED: $($_.Exception.Message)" | Set-Content -Path $path -Encoding UTF8 } } Write-Host "Collecting to $out" # --- Machine summary --- Save "summary.txt" { $os = Get-CimInstance Win32_OperatingSystem $cs = Get-CimInstance Win32_ComputerSystem $cpu = Get-CimInstance Win32_Processor [PSCustomObject]@{ Collected = Get-Date Computer = $env:COMPUTERNAME OS = $os.Caption Build = $os.BuildNumber LastBoot = $os.LastBootUpTime UptimeHours = [math]::Round(((Get-Date) - $os.LastBootUpTime).TotalHours, 1) CPU = $cpu.Name RAM_GB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 1) } | Format-List } # --- GPU + driver: the usual suspect for crashes and encoder failures --- Save "gpu.txt" { Get-CimInstance Win32_VideoController | Select-Object Name, DriverVersion, DriverDate, VideoProcessor, @{n = "VRAM_GB"; e = { [math]::Round($_.AdapterRAM / 1GB, 1) } }, Status | Format-List } # --- Event logs. Level 1=Critical 2=Error 3=Warning --- foreach ($log in @("System", "Application")) { Save "events-$($log.ToLower()).txt" { $filter = @{ LogName = $log; Level = 1, 2, 3; StartTime = (Get-Date).AddHours(-$Hours) } $events = Get-WinEvent -FilterHashtable $filter -MaxEvents 300 -ErrorAction SilentlyContinue if (-not $events) { "No critical/error/warning events in the last $Hours hours. That is good news." } else { $events | Select-Object TimeCreated, LevelDisplayName, ProviderName, Id, Message | Format-List } } } # --- Unexpected shutdowns / bugchecks, called out separately --- Save "crashes.txt" { $ids = 41, 1001, 6008 # kernel power, bugcheck, unexpected shutdown $ev = Get-WinEvent -FilterHashtable @{ LogName = "System"; Id = $ids } -MaxEvents 30 -ErrorAction SilentlyContinue if (-not $ev) { "No bugchecks or unexpected shutdowns recorded." } else { $ev | Select-Object TimeCreated, Id, ProviderName, Message | Format-List } } # --- Firewall --- # netsh rather than Get-NetFirewallRule: the cmdlet joins port and address # filters per rule and takes the better part of a minute on a normal box. Save "firewall-profiles.txt" { Get-NetFirewallProfile | Select-Object Name, Enabled, DefaultInboundAction, DefaultOutboundAction, LogFileName, LogBlocked | Format-List } Save "firewall-rules.txt" { netsh advfirewall firewall show rule name=all } # --- Dropped packets, if firewall logging is on (setup-gaming-box.ps1 enables it) --- $fwLog = "$env:SystemRoot\system32\LogFiles\Firewall\pfirewall.log" if (Test-Path $fwLog) { Save "firewall-drops.txt" { "Last 400 firewall log lines from $fwLog" "" Get-Content $fwLog -Tail 400 -ErrorAction SilentlyContinue } } else { "No firewall log at $fwLog -- logging may be off." | Set-Content (Join-Path $out "firewall-drops-missing.txt") } # --- Network --- Save "network.txt" { ipconfig /all "`n--- Adapters ---" Get-NetAdapter | Select-Object Name, InterfaceDescription, Status, LinkSpeed | Format-Table -AutoSize "`n--- Routes ---" Get-NetRoute -AddressFamily IPv4 | Select-Object DestinationPrefix, NextHop, RouteMetric, InterfaceAlias | Sort-Object RouteMetric | Format-Table -AutoSize "`n--- Listening ports ---" Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Select-Object LocalAddress, LocalPort, OwningProcess | Sort-Object LocalPort | Format-Table -AutoSize } # --- Installed software --- Save "installed.txt" { winget list --accept-source-agreements } # --- OBS logs: the single most useful artifact for a streaming problem --- $obsLogDir = "$env:APPDATA\obs-studio\logs" if (Test-Path $obsLogDir) { $obsOut = Join-Path $out "obs-logs" New-Item -Path $obsOut -ItemType Directory -Force | Out-Null Get-ChildItem $obsLogDir -Filter *.txt | Sort-Object LastWriteTime -Descending | Select-Object -First 3 | Copy-Item -Destination $obsOut Write-Host " obs-logs (last 3)" } else { "OBS log folder not found at $obsLogDir" | Set-Content (Join-Path $out "obs-logs-missing.txt") } # --- Prune old snapshots --- Get-ChildItem $OutputRoot -Directory | Sort-Object Name -Descending | Select-Object -Skip $KeepLast | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue Write-Host "" Write-Host "Done: $out" Write-Host "" Write-Host "Now ask Claude:" Write-Host " 'Read the newest folder in my Diagnostics folder and tell me what broke.'" Write-Host "" Start-Process explorer.exe $out