# Mohe & XiaoXiaoMo Chat Viewer # Lists [mohe]/[xxm] messages from an OpenCode session. # # Usage: # .\moho_chat.ps1 [minutes] # # Examples: # .\moho_chat.ps1 ses_1d95d15c4ffehQaZ6hrbIbak5k # .\moho_chat.ps1 ses_1d95d15c4ffehQaZ6hrbIbak5k 30 param( [Parameter(Mandatory=$true)][string]$SessionId, [int]$Minutes = 0 ) $ErrorActionPreference = 'Stop' Write-Host "Exporting session..." -ForegroundColor DarkGray $tmpFile = [System.IO.Path]::GetTempFileName() + ".json" $env:CI = 'true' opencode.cmd export $SessionId 2>$null | Set-Content -Path $tmpFile -NoNewline -Encoding UTF8 if (-not (Test-Path $tmpFile) -or (Get-Item $tmpFile).Length -eq 0) { Write-Error "Export failed" exit 1 } $size = (Get-Item $tmpFile).Length Write-Host "Read ($($size / 1MB -as [int]) MB)" -ForegroundColor DarkGray $raw = Get-Content $tmpFile -Raw -Encoding UTF8 Remove-Item $tmpFile -Force # Check if it's actually UTF-16LE in disguise (opencode export uses UTF-16LE) if ($raw.Length -eq 0 -or $raw[0] -ne '{') { # Re-read as UTF-16 $bytes = [System.IO.File]::ReadAllBytes($tmpFile) Remove-Item $tmpFile -Force $raw = [System.Text.Encoding]::Unicode.GetString($bytes) } $brace = $raw.IndexOf('{') if ($brace -gt 0) { $raw = $raw.Substring($brace) } # Use regex to find "text": "[mohe]..." patterns directly (no JSON parsing) $pattern = '"text":\s*"\[(mohe|xxm)\][^"]*"' $cutoff = if ($Minutes -gt 0) { (Get-Date).ToUniversalTime().AddMinutes(-$Minutes) } else { $null } $results = @() $matchPos = 0 while ($true) { $m = [regex]::Match($raw, $pattern, $matchPos) if (-not $m.Success) { break } $matchPos = $m.Index + 1 $tag = $m.Groups[1].Value $content = $m.Value # Extract the full text content (everything between "text": " and the closing ") $start = $m.Index + $m.Value.IndexOf('"', $m.Value.IndexOf(':')+1) + 1 $fullText = '' $escape = $false for ($i = $start; $i -lt $raw.Length; $i++) { $c = $raw[$i] if ($escape) { $fullText += $c; $escape = $false } elseif ($c -eq '\') { $fullText += $c; $escape = $true } elseif ($c -eq '"') { break } else { $fullText += $c } } $fullText = $fullText.Trim() if (-not ($fullText.StartsWith('[mohe]') -or $fullText.StartsWith('[xxm]'))) { continue } $sender = if ($fullText.StartsWith('[mohe]')) { '[mohe] Mohe' } else { '[xxm] XiaoXiaoMo' } $display = $fullText -replace '^\[\w+\]\s*', '' # Get timestamp by looking backward $before = $raw.Substring([Math]::Max(0, $m.Index - 3000), [Math]::Min(3000, $m.Index)) $tsMatch = [regex]::Match($before, '"timestamp":\s*"([^"]+)"') $tsStr = if ($tsMatch.Success) { $tsMatch.Groups[1].Value } else { '' } if (-not $tsStr) { $crMatch = [regex]::Match($before, '"created":\s*(\d+)') if ($crMatch.Success) { $tsStr = ([DateTimeOffset]::FromUnixTimeMilliseconds([long]$crMatch.Groups[1].Value).UtcDateTime).ToString('o') } } $ts = $null if ($tsStr) { try { $ts = [DateTime]::Parse($tsStr, $null, [System.Globalization.DateTimeStyles]::AssumeUniversal) } catch {} } if ($cutoff -and $ts -and $ts -lt $cutoff) { continue } $timeLocal = if ($ts) { $ts.ToLocalTime().ToString('HH:mm:ss') } else { '??' } $results += [PSCustomObject]@{ Time=$timeLocal; Sort=if($ts){$ts.Ticks}else{0}; Sender=$sender; Message=$display } } $results = $results | Sort-Object Sort if ($results.Count -eq 0) { Write-Host "No [mohe]/[xxm] messages found" -ForegroundColor Yellow if ($Minutes -gt 0) { Write-Host "(last $Minutes minutes)" } exit } Write-Host "`n$($results.Count) message(s)" -ForegroundColor Cyan if ($Minutes -gt 0) { Write-Host "(last $Minutes min)" } Write-Host ("=" * 60) foreach ($r in $results) { Write-Host ("[{0}] {1}: {2}" -f $r.Time, $r.Sender, $r.Message) }