workflows/default/systems/runtime/ProviderCLI/parsers/Parse-ClaudeStream.ps1

<#
.SYNOPSIS
Claude stream parser for ProviderCLI.

.DESCRIPTION
Processes Claude CLI stream-json output lines. This parser is used as a fallback;
the primary Claude path delegates to Invoke-ClaudeStream in ClaudeCLI.psm1 directly.
Provides Process-StreamLine function for the ProviderCLI dispatcher.
#>


# Import helpers from ClaudeCLI if not already available
if (-not (Get-Command Write-ClaudeLog -ErrorAction SilentlyContinue)) {
    Import-Module "$PSScriptRoot\..\..\ClaudeCLI\ClaudeCLI.psm1" -Force
}

function Process-StreamLine {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Line,

        [Parameter(Mandatory)]
        [hashtable]$State,

        [switch]$ShowDebugJson,
        [switch]$ShowVerbose
    )

    $t = $State.theme

    # Check for rate limit
    if ($Line -match "hit your limit|error.*rate_limit") {
        try {
            $jsonObj = $Line | ConvertFrom-Json -ErrorAction Stop
            if ($jsonObj.error -eq "rate_limit" -or ($jsonObj.result -and $jsonObj.result -match "resets?")) {
                $State.rateLimitMessage = if ($jsonObj.result) { $jsonObj.result } else { "Rate limit hit" }
                [Console]::Error.WriteLine("$($t.Amber)Rate limit: $($State.rateLimitMessage)$($t.Reset)")
                [Console]::Error.Flush()
                Write-ActivityLog -Type "rate_limit" -Message $State.rateLimitMessage
                return 'rate_limit'
            }
        } catch { Write-BotLog -Level Debug -Message "Failed to parse data" -Exception $_ }
    }

    # Skip non-JSON
    if ($Line[0] -ne '{') { return 'skip' }

    if ($ShowDebugJson) {
        [Console]::Error.WriteLine("$($t.Bezel)[JSON] $Line$($t.Reset)")
        [Console]::Error.Flush()
    }

    $evt = $null
    try { $evt = $Line | ConvertFrom-Json -ErrorAction Stop } catch { return 'skip' }
    if (-not $evt) { return 'skip' }

    # Assistant text streaming
    $text = $null
    if ($evt.message?.delta?.text) {
        $text = $evt.message.delta.text
    }
    elseif ($evt.message?.content -is [System.Array]) {
        foreach ($b in $evt.message.content) {
            if ($b.type -eq "text" -and $b.text) { $text += $b.text }
            elseif ($b.delta?.text) { $text += $b.delta.text }
        }
    }
    elseif ($evt.message?.content -is [string]) {
        $text = $evt.message.content
    }

    # Track token usage
    if ($evt.message?.usage) {
        $usage = $evt.message.usage
        if ($usage.input_tokens) { $State.totalInputTokens += $usage.input_tokens }
        if ($usage.output_tokens) { $State.totalOutputTokens += $usage.output_tokens }
        if ($usage.cache_read_input_tokens) { $State.totalCacheRead += $usage.cache_read_input_tokens }
        if ($usage.cache_creation_input_tokens) { $State.totalCacheCreate += $usage.cache_creation_input_tokens }
    }

    if ($text) {
        [void]$State.assistantText.Append($text)
        return 'text'
    }

    # Init event
    if ($evt.type -and $evt.subtype -and $evt.model -and $evt.cwd) {
        Write-ClaudeLog "init" "$($evt.model)" "*"
        return 'init'
    }

    # Tool use
    if ($evt.type -eq "assistant" -and $evt.message?.content -is [System.Array]) {
        $toolUses = @($evt.message.content | Where-Object { $_.type -eq "tool_use" })
        if ($toolUses.Count -gt 0) {
            # Flush assistant text
            if ($State.assistantText.Length -gt 0) {
                $rendered = ConvertTo-RenderedMarkdown $State.assistantText.ToString()
                [Console]::WriteLine("")
                [Console]::Write($rendered)
                $textPreview = (Get-PreviewText $State.assistantText.ToString() 200)
                Write-ActivityLog -Type "text" -Message $textPreview
                [Console]::Out.Flush()
                $State.assistantText.Length = 0
            }
            foreach ($tu in $toolUses) {
                if ($tu.name -eq "TodoWrite") { continue }
                $detail = ""
                if ($tu.input) {
                    if ($tu.input.command) { $detail = Get-PreviewText $tu.input.command 140 }
                    elseif ($tu.input.pattern) { $detail = "pattern=`"$($tu.input.pattern)`"" }
                    elseif ($tu.input.file_path) { $detail = $tu.input.file_path -replace '\\\\', '\\' -replace [regex]::Escape($PWD.Path + '\'), '' }
                    elseif ($tu.input.description) { $detail = Get-PreviewText $tu.input.description 140 }
                    elseif ($tu.input.prompt) { $detail = Get-PreviewText $tu.input.prompt 140 }
                }
                if (-not $detail) { $detail = "" }
                Write-ClaudeLog $tu.name $detail ">"
            }
            return 'tool_use'
        }
    }

    # Tool result
    if ($evt.type -eq "user" -and $evt.message?.content -is [System.Array]) {
        $toolResults = @($evt.message.content | Where-Object { $_.type -eq "tool_result" })
        if ($toolResults.Count -gt 0) {
            if ($State.assistantText.Length -gt 0) {
                $rendered = ConvertTo-RenderedMarkdown $State.assistantText.ToString()
                [Console]::WriteLine("")
                [Console]::Write($rendered)
                Write-ActivityLog -Type "text" -Message (Get-PreviewText $State.assistantText.ToString() 200)
                [Console]::Out.Flush()
                $State.assistantText.Length = 0
            }
            foreach ($tr in $toolResults) {
                $isErr = [bool]$tr.is_error
                $icon = if ($isErr) { "x" } else { "+" }
                $meta = @()
                if ($evt.tool_use_result) {
                    if ($evt.tool_use_result.durationMs -ne $null -and $evt.tool_use_result.durationMs -gt 100) {
                        $meta += "$($evt.tool_use_result.durationMs)ms"
                    }
                    if ($evt.tool_use_result.numFiles -ne $null) {
                        $meta += "$($evt.tool_use_result.numFiles) files"
                    }
                }
                $msg = if ($meta.Count -gt 0) { $meta -join ", " } else { "" }
                if ($msg) { Write-ClaudeLog "done" $msg $icon }
            }
            return 'tool_result'
        }
    }

    # Result summary
    if ($evt.type -eq "result") {
        if ($State.assistantText.Length -gt 0) {
            $rendered = ConvertTo-RenderedMarkdown $State.assistantText.ToString()
            [Console]::WriteLine("")
            [Console]::Write($rendered)
            Write-ActivityLog -Type "text" -Message (Get-PreviewText $State.assistantText.ToString() 200)
            [Console]::Out.Flush()
            $State.assistantText.Length = 0
        }
        Format-ResultSummary $evt
        return 'result'
    }

    return 'unknown'
}