Skip to content

Commit 5df0926

Browse files
gfraiteurclaude
andcommitted
Improve RunClaude.ps1 monitoring output and add CLAUDE_ git identity precedence
- Replace hardcoded tool switch with generic property-based display - Cap tool output to 5 lines with overflow indicator - Strip system-reminder and function_calls markup from output - Silence Read, Glob, Grep tools and Bash ls/grep/find commands - Add blank lines before messages for readability - Give CLAUDE_GIT_USER_NAME/EMAIL precedence in DockerBuild.ps1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 108f45f commit 5df0926

3 files changed

Lines changed: 345 additions & 17 deletions

File tree

eng/RunClaude.ps1

Lines changed: 169 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,145 @@ if ($env:RUNNING_IN_DOCKER -ne "true")
1616
exit 1
1717
}
1818

19+
# --- Output sanitization (matches ClaudeCodeHelper.SanitizeOutput) ---
20+
function Sanitize-ClaudeOutput {
21+
param([string]$Text)
22+
23+
if ([string]::IsNullOrEmpty($Text)) { return "" }
24+
25+
# Strip ANSI escape sequences
26+
$stripped = $Text -replace '\x1b\[[0-9;]*m','' -replace '\[\d+(?:;\d+)*m',''
27+
28+
$sb = [System.Text.StringBuilder]::new($stripped.Length)
29+
foreach ($c in $stripped.ToCharArray()) {
30+
$code = [int]$c
31+
if (($code -ge 32 -and $code -le 126) -or $c -eq "`n" -or $c -eq "`r" -or $c -eq "`t") {
32+
[void]$sb.Append($c)
33+
}
34+
elseif ([char]::IsWhiteSpace($c)) {
35+
[void]$sb.Append(' ')
36+
}
37+
# Skip all other characters (including extended Unicode)
38+
}
39+
return $sb.ToString()
40+
}
41+
42+
# Tools whose output is silenced in the monitoring display
43+
$script:SilentTools = @('Read', 'Glob', 'Grep')
44+
$script:SilentToolIds = @{}
45+
46+
# --- JSON stream line parser (matches ClaudeCodeHelper.TranslateJsonToHumanReadable) ---
47+
function ConvertFrom-ClaudeJsonLine {
48+
param([string]$Line)
49+
50+
if ([string]::IsNullOrWhiteSpace($Line)) { return }
51+
52+
try {
53+
$json = $Line | ConvertFrom-Json
54+
} catch {
55+
Write-Host (Sanitize-ClaudeOutput $Line)
56+
return
57+
}
58+
59+
switch ($json.type) {
60+
'system' {
61+
if ($json.subtype -eq 'init') {
62+
$model = if ($json.model) { $json.model } else { "unknown" }
63+
Write-Host (Sanitize-ClaudeOutput "[Claude Code initialized - model: $model]") -ForegroundColor Green
64+
}
65+
}
66+
'assistant' {
67+
if ($json.message -and $json.message.content) {
68+
foreach ($block in $json.message.content) {
69+
if ($block.type -eq 'text') {
70+
Write-Host ""
71+
Write-Host (Sanitize-ClaudeOutput $block.text) -ForegroundColor Cyan
72+
}
73+
elseif ($block.type -eq 'tool_use') {
74+
$toolName = if ($block.name) { $block.name } else { "unknown" }
75+
if ($script:SilentTools -contains $toolName) {
76+
if ($block.id) { $script:SilentToolIds[$block.id] = $true }
77+
continue
78+
}
79+
# Silence Bash calls for read-only commands (ls, grep)
80+
if ($toolName -eq 'Bash' -and $block.input.command -match '^\s*(ls|grep|find)\b') {
81+
if ($block.id) { $script:SilentToolIds[$block.id] = $true }
82+
continue
83+
}
84+
Write-Host ""
85+
Write-Host (Sanitize-ClaudeOutput "[Tool: $toolName]") -ForegroundColor Yellow
86+
if ($block.input) {
87+
# Display the most identifying property from the input
88+
$displayProps = @(
89+
@{ Key = 'file_path'; Label = 'File' }
90+
@{ Key = 'command'; Label = '$' }
91+
@{ Key = 'pattern'; Label = 'Pattern' }
92+
@{ Key = 'query'; Label = 'Query' }
93+
@{ Key = 'url'; Label = 'URL' }
94+
@{ Key = 'skill'; Label = 'Skill' }
95+
@{ Key = 'prompt'; Label = 'Prompt' }
96+
@{ Key = 'description'; Label = 'Task' }
97+
)
98+
$shown = $false
99+
foreach ($dp in $displayProps) {
100+
$val = $block.input.($dp.Key)
101+
if ($val) {
102+
$truncated = if ($val.Length -gt 120) { $val.Substring(0, 120) + "..." } else { $val }
103+
Write-Host (Sanitize-ClaudeOutput " $($dp.Label): $truncated") -ForegroundColor Gray
104+
$shown = $true
105+
break
106+
}
107+
}
108+
if (-not $shown) {
109+
# Fallback: show the property names so the user at least sees what was passed
110+
$keys = ($block.input.PSObject.Properties | Select-Object -ExpandProperty Name) -join ', '
111+
if ($keys) {
112+
Write-Host (Sanitize-ClaudeOutput " [$keys]") -ForegroundColor Gray
113+
}
114+
}
115+
}
116+
}
117+
}
118+
}
119+
}
120+
'user' {
121+
if ($json.message -and $json.message.content) {
122+
foreach ($block in $json.message.content) {
123+
if ($block.type -eq 'tool_result') {
124+
if ($block.tool_use_id -and $script:SilentToolIds.ContainsKey($block.tool_use_id)) {
125+
$script:SilentToolIds.Remove($block.tool_use_id)
126+
continue
127+
}
128+
$content = if ($block.content) { $block.content } else { "" }
129+
# Strip system reminders and tool use markup
130+
$content = $content -replace '(?s)<system-reminder>.*?</system-reminder>', ''
131+
$closingTag = '</function_calls>'
132+
$content = $content -replace "(?s)<function_calls>.*?$closingTag", ''
133+
$sanitized = Sanitize-ClaudeOutput $content
134+
$lines = $sanitized -split "`n"
135+
$maxLines = 5
136+
$color = if ($block.is_error) { "Red" } else { "DarkGray" }
137+
$prefix = if ($block.is_error) { " [ERROR] " } else { " ->" }
138+
for ($i = 0; $i -lt [Math]::Min($lines.Count, $maxLines); $i++) {
139+
Write-Host "$prefix$($lines[$i])" -ForegroundColor $color
140+
}
141+
if ($lines.Count -gt $maxLines) {
142+
Write-Host " ... ($($lines.Count - $maxLines) more lines)" -ForegroundColor $color
143+
}
144+
}
145+
}
146+
}
147+
}
148+
'result' {
149+
Write-Host (Sanitize-ClaudeOutput "[Session completed]") -ForegroundColor Green
150+
}
151+
'error' {
152+
$msg = if ($json.error.message) { $json.error.message } elseif ($json.error) { $json.error } else { "Unknown error" }
153+
Write-Host (Sanitize-ClaudeOutput "[ERROR] $msg") -ForegroundColor Red
154+
}
155+
}
156+
}
157+
19158
# Configure MCP approval server if port is specified
20159
$mcpConfigArg = ""
21160
if ($McpPort -gt 0)
@@ -59,9 +198,36 @@ if ($Prompt)
59198
$Prompt | Set-Content -Path $promptFile -Encoding UTF8 -NoNewline
60199
Write-Host "Running Claude with prompt from file: $promptFile" -ForegroundColor Cyan
61200

62-
# Use stdin redirection to pass the prompt, avoiding command line length issues
63-
$cmd = "Get-Content -Path '$promptFile' -Raw | claude --model $Model --dangerously-skip-permissions $mcpConfigArg"
64-
$exitCode = Invoke-Expression $cmd
201+
# Stream JSON output for human-readable real-time monitoring
202+
$processArgs = "-p --output-format stream-json --verbose --model $Model --dangerously-skip-permissions $mcpConfigArg"
203+
204+
$psi = [System.Diagnostics.ProcessStartInfo]::new()
205+
$psi.FileName = "claude.cmd"
206+
$psi.Arguments = $processArgs
207+
$psi.RedirectStandardInput = $true
208+
$psi.RedirectStandardOutput = $true
209+
$psi.RedirectStandardError = $true
210+
$psi.UseShellExecute = $false
211+
$psi.CreateNoWindow = $true
212+
213+
$process = [System.Diagnostics.Process]::Start($psi)
214+
215+
# Send prompt via stdin
216+
$promptContent = Get-Content -Path $promptFile -Raw
217+
$process.StandardInput.Write($promptContent)
218+
$process.StandardInput.Close()
219+
220+
# Read and parse stdout line by line (real-time streaming)
221+
while ($null -ne ($line = $process.StandardOutput.ReadLine())) {
222+
ConvertFrom-ClaudeJsonLine -Line $line
223+
}
224+
225+
# Also capture stderr
226+
$stderr = $process.StandardError.ReadToEnd()
227+
if ($stderr) { Write-Host (Sanitize-ClaudeOutput $stderr) -ForegroundColor Red }
228+
229+
$process.WaitForExit()
230+
$exitCode = $process.ExitCode
65231

66232
# Clean up prompt file
67233
Remove-Item $promptFile -ErrorAction SilentlyContinue

src/PostSharp.Engineering.BuildTools/Resources/DockerBuild.ps1

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -369,17 +369,13 @@ try
369369
$claudeEnv["IS_TEAMCITY_AGENT"] = $env:IS_TEAMCITY_AGENT
370370
}
371371

372-
# Git identity - read from host git config if not set in environment
373-
$gitUserName = $env:GIT_USER_NAME
374-
$gitUserEmail = $env:GIT_USER_EMAIL
375-
if (-not $gitUserName)
376-
{
377-
$gitUserName = git config --global user.name
378-
}
379-
if (-not $gitUserEmail)
380-
{
381-
$gitUserEmail = git config --global user.email
382-
}
372+
# Git identity - CLAUDE_ prefixed vars take precedence, then GIT_USER_*, then git config
373+
$gitUserName = $env:CLAUDE_GIT_USER_NAME
374+
if (-not $gitUserName) { $gitUserName = $env:GIT_USER_NAME }
375+
if (-not $gitUserName) { $gitUserName = git config --global user.name }
376+
$gitUserEmail = $env:CLAUDE_GIT_USER_EMAIL
377+
if (-not $gitUserEmail) { $gitUserEmail = $env:GIT_USER_EMAIL }
378+
if (-not $gitUserEmail) { $gitUserEmail = git config --global user.email }
383379
if ($gitUserName)
384380
{
385381
$claudeEnv["GIT_USER_NAME"] = $gitUserName

src/PostSharp.Engineering.BuildTools/Resources/RunClaude.ps1

Lines changed: 169 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,145 @@ if ($env:RUNNING_IN_DOCKER -ne "true")
1616
exit 1
1717
}
1818

19+
# --- Output sanitization (matches ClaudeCodeHelper.SanitizeOutput) ---
20+
function Sanitize-ClaudeOutput {
21+
param([string]$Text)
22+
23+
if ([string]::IsNullOrEmpty($Text)) { return "" }
24+
25+
# Strip ANSI escape sequences
26+
$stripped = $Text -replace '\x1b\[[0-9;]*m','' -replace '\[\d+(?:;\d+)*m',''
27+
28+
$sb = [System.Text.StringBuilder]::new($stripped.Length)
29+
foreach ($c in $stripped.ToCharArray()) {
30+
$code = [int]$c
31+
if (($code -ge 32 -and $code -le 126) -or $c -eq "`n" -or $c -eq "`r" -or $c -eq "`t") {
32+
[void]$sb.Append($c)
33+
}
34+
elseif ([char]::IsWhiteSpace($c)) {
35+
[void]$sb.Append(' ')
36+
}
37+
# Skip all other characters (including extended Unicode)
38+
}
39+
return $sb.ToString()
40+
}
41+
42+
# Tools whose output is silenced in the monitoring display
43+
$script:SilentTools = @('Read', 'Glob', 'Grep')
44+
$script:SilentToolIds = @{}
45+
46+
# --- JSON stream line parser (matches ClaudeCodeHelper.TranslateJsonToHumanReadable) ---
47+
function ConvertFrom-ClaudeJsonLine {
48+
param([string]$Line)
49+
50+
if ([string]::IsNullOrWhiteSpace($Line)) { return }
51+
52+
try {
53+
$json = $Line | ConvertFrom-Json
54+
} catch {
55+
Write-Host (Sanitize-ClaudeOutput $Line)
56+
return
57+
}
58+
59+
switch ($json.type) {
60+
'system' {
61+
if ($json.subtype -eq 'init') {
62+
$model = if ($json.model) { $json.model } else { "unknown" }
63+
Write-Host (Sanitize-ClaudeOutput "[Claude Code initialized - model: $model]") -ForegroundColor Green
64+
}
65+
}
66+
'assistant' {
67+
if ($json.message -and $json.message.content) {
68+
foreach ($block in $json.message.content) {
69+
if ($block.type -eq 'text') {
70+
Write-Host ""
71+
Write-Host (Sanitize-ClaudeOutput $block.text) -ForegroundColor Cyan
72+
}
73+
elseif ($block.type -eq 'tool_use') {
74+
$toolName = if ($block.name) { $block.name } else { "unknown" }
75+
if ($script:SilentTools -contains $toolName) {
76+
if ($block.id) { $script:SilentToolIds[$block.id] = $true }
77+
continue
78+
}
79+
# Silence Bash calls for read-only commands (ls, grep)
80+
if ($toolName -eq 'Bash' -and $block.input.command -match '^\s*(ls|grep|find)\b') {
81+
if ($block.id) { $script:SilentToolIds[$block.id] = $true }
82+
continue
83+
}
84+
Write-Host ""
85+
Write-Host (Sanitize-ClaudeOutput "[Tool: $toolName]") -ForegroundColor Yellow
86+
if ($block.input) {
87+
# Display the most identifying property from the input
88+
$displayProps = @(
89+
@{ Key = 'file_path'; Label = 'File' }
90+
@{ Key = 'command'; Label = '$' }
91+
@{ Key = 'pattern'; Label = 'Pattern' }
92+
@{ Key = 'query'; Label = 'Query' }
93+
@{ Key = 'url'; Label = 'URL' }
94+
@{ Key = 'skill'; Label = 'Skill' }
95+
@{ Key = 'prompt'; Label = 'Prompt' }
96+
@{ Key = 'description'; Label = 'Task' }
97+
)
98+
$shown = $false
99+
foreach ($dp in $displayProps) {
100+
$val = $block.input.($dp.Key)
101+
if ($val) {
102+
$truncated = if ($val.Length -gt 120) { $val.Substring(0, 120) + "..." } else { $val }
103+
Write-Host (Sanitize-ClaudeOutput " $($dp.Label): $truncated") -ForegroundColor Gray
104+
$shown = $true
105+
break
106+
}
107+
}
108+
if (-not $shown) {
109+
# Fallback: show the property names so the user at least sees what was passed
110+
$keys = ($block.input.PSObject.Properties | Select-Object -ExpandProperty Name) -join ', '
111+
if ($keys) {
112+
Write-Host (Sanitize-ClaudeOutput " [$keys]") -ForegroundColor Gray
113+
}
114+
}
115+
}
116+
}
117+
}
118+
}
119+
}
120+
'user' {
121+
if ($json.message -and $json.message.content) {
122+
foreach ($block in $json.message.content) {
123+
if ($block.type -eq 'tool_result') {
124+
if ($block.tool_use_id -and $script:SilentToolIds.ContainsKey($block.tool_use_id)) {
125+
$script:SilentToolIds.Remove($block.tool_use_id)
126+
continue
127+
}
128+
$content = if ($block.content) { $block.content } else { "" }
129+
# Strip system reminders and tool use markup
130+
$content = $content -replace '(?s)<system-reminder>.*?</system-reminder>', ''
131+
$closingTag = '</function_calls>'
132+
$content = $content -replace "(?s)<function_calls>.*?$closingTag", ''
133+
$sanitized = Sanitize-ClaudeOutput $content
134+
$lines = $sanitized -split "`n"
135+
$maxLines = 5
136+
$color = if ($block.is_error) { "Red" } else { "DarkGray" }
137+
$prefix = if ($block.is_error) { " [ERROR] " } else { " ->" }
138+
for ($i = 0; $i -lt [Math]::Min($lines.Count, $maxLines); $i++) {
139+
Write-Host "$prefix$($lines[$i])" -ForegroundColor $color
140+
}
141+
if ($lines.Count -gt $maxLines) {
142+
Write-Host " ... ($($lines.Count - $maxLines) more lines)" -ForegroundColor $color
143+
}
144+
}
145+
}
146+
}
147+
}
148+
'result' {
149+
Write-Host (Sanitize-ClaudeOutput "[Session completed]") -ForegroundColor Green
150+
}
151+
'error' {
152+
$msg = if ($json.error.message) { $json.error.message } elseif ($json.error) { $json.error } else { "Unknown error" }
153+
Write-Host (Sanitize-ClaudeOutput "[ERROR] $msg") -ForegroundColor Red
154+
}
155+
}
156+
}
157+
19158
# Configure MCP approval server if port is specified
20159
$mcpConfigArg = ""
21160
if ($McpPort -gt 0)
@@ -59,9 +198,36 @@ if ($Prompt)
59198
$Prompt | Set-Content -Path $promptFile -Encoding UTF8 -NoNewline
60199
Write-Host "Running Claude with prompt from file: $promptFile" -ForegroundColor Cyan
61200

62-
# Use stdin redirection to pass the prompt, avoiding command line length issues
63-
$cmd = "Get-Content -Path '$promptFile' -Raw | claude --model $Model --dangerously-skip-permissions $mcpConfigArg"
64-
$exitCode = Invoke-Expression $cmd
201+
# Stream JSON output for human-readable real-time monitoring
202+
$processArgs = "-p --output-format stream-json --verbose --model $Model --dangerously-skip-permissions $mcpConfigArg"
203+
204+
$psi = [System.Diagnostics.ProcessStartInfo]::new()
205+
$psi.FileName = "claude.cmd"
206+
$psi.Arguments = $processArgs
207+
$psi.RedirectStandardInput = $true
208+
$psi.RedirectStandardOutput = $true
209+
$psi.RedirectStandardError = $true
210+
$psi.UseShellExecute = $false
211+
$psi.CreateNoWindow = $true
212+
213+
$process = [System.Diagnostics.Process]::Start($psi)
214+
215+
# Send prompt via stdin
216+
$promptContent = Get-Content -Path $promptFile -Raw
217+
$process.StandardInput.Write($promptContent)
218+
$process.StandardInput.Close()
219+
220+
# Read and parse stdout line by line (real-time streaming)
221+
while ($null -ne ($line = $process.StandardOutput.ReadLine())) {
222+
ConvertFrom-ClaudeJsonLine -Line $line
223+
}
224+
225+
# Also capture stderr
226+
$stderr = $process.StandardError.ReadToEnd()
227+
if ($stderr) { Write-Host (Sanitize-ClaudeOutput $stderr) -ForegroundColor Red }
228+
229+
$process.WaitForExit()
230+
$exitCode = $process.ExitCode
65231

66232
# Clean up prompt file
67233
Remove-Item $promptFile -ErrorAction SilentlyContinue

0 commit comments

Comments
 (0)