-
Notifications
You must be signed in to change notification settings - Fork 0
fix(logs): declutter and clarify the readable session log #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d60510b
78ed877
72114fe
7a0a6d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,37 +1,247 @@ | ||
| # Formats claude -p --output-format stream-json events into clean, | ||
| # human-readable lines for a live terminal. Fed raw NDJSON on stdin. | ||
| # | ||
| # Stateful: walks the stream via foreach so tool_result lines can be traced | ||
| # back to the tool_use that produced them (matched by tool_use_id), which is | ||
| # what lets Read/Glob/Grep/Bash results be recognized and condensed below. | ||
|
|
||
| def truncate(n): | ||
| if (. | length) > n then .[0:n] + "..." else . end; | ||
|
|
||
| def tool_summary: | ||
| if .name == "Bash" then (.input.command // "" | tostring | truncate(150)) | ||
| else (.input | tostring | truncate(150)) | ||
| def strip_ansi: | ||
| gsub("\\[[0-9;]*[a-zA-Z]"; ""); | ||
|
|
||
| def strip_cr: | ||
| gsub("\r"; ""); | ||
|
|
||
| # git's own core.autocrlf notice - fires on add/commit/checkout for any file | ||
| # with mixed line endings, never actionable, pure noise. | ||
| def strip_git_crlf_warnings: | ||
| split("\n") | ||
| | map(select(test("^warning: in the working copy of .*LF will be replaced by CRLF") | not)) | ||
| | join("\n"); | ||
|
|
||
| # drops everything from the first "diff --git" marker onward - the actual | ||
| # diff is redundant with what the PR itself will show, and a unified diff | ||
| # in the middle of a status/commit summary is pure noise here. | ||
| def strip_git_diff_body: | ||
| (split("\n")) as $lines | | ||
| (reduce $lines[] as $line | ||
| ({out: [], in_diff: false}; | ||
| if ($line | test("^diff --git ")) then {out: .out, in_diff: true} | ||
| elif .in_diff then . | ||
| else {out: (.out + [$line]), in_diff: false} | ||
| end) | ||
| ).out | join("\n"); | ||
|
|
||
| def non_empty_lines: | ||
| split("\n") | map(select(length > 0)); | ||
|
|
||
| # aligns continuation lines under the " < " prefix (6 chars) so multi-line | ||
| # results read as one indented block instead of falling back to column 0. | ||
| def indent_continuations: | ||
| gsub("\n"; "\n "); | ||
|
|
||
| def tool_summary($name; $input): | ||
| if $name == "Bash" then | ||
| ($input.command // "" | tostring | strip_ansi) as $cmd | | ||
| if ($cmd | test("git\\s+commit\\b.*-m\\s+\"")) then | ||
| "git commit: " + ($cmd | capture("-m\\s+\"(?<msg>[^\\n\"]*)").msg | truncate(150)) | ||
| else | ||
| ($cmd | truncate(150)) | ||
| end | ||
| else ($input | tostring | strip_ansi | truncate(150)) | ||
| end; | ||
|
|
||
| if .type == "system" and .subtype == "init" then | ||
| "=== session started (model: \(.model)) ===" | ||
| elif .type == "assistant" then | ||
| (.message.content[]? | | ||
| if .type == "text" then | ||
| .text | ||
| elif .type == "tool_use" then | ||
| " > " + .name + "(" + tool_summary + ")" | ||
| else empty | ||
| end) | ||
| elif .type == "user" then | ||
| (.message.content[]? | | ||
| if .type == "tool_result" then | ||
| (if (.content | type) == "array" then | ||
| (.content | map(.text? // "") | join(" ")) | ||
| # vitest: pull the "Test Files"/"Tests" summary lines plus each FAIL entry, | ||
| # regardless of where they land in the (already tail-truncated) output. | ||
| def summarize_vitest: | ||
| (split("\n")) as $lines | | ||
| ($lines | map(select(test("^\\s*(Test Files|Tests)\\s")))) as $summary_lines | | ||
| ($lines | ||
| | map(select(test("^\\s*FAIL\\s+\\S+\\s+"))) | ||
| | map(capture("^\\s*FAIL\\s+\\S+\\s+(?<rest>.+)$").rest) | ||
| ) as $failed | | ||
| ($summary_lines | join("\n")) as $summary | | ||
| if ($failed | length) > 0 then | ||
| $summary + "\n" + ($failed | map(" FAILED " + .) | join("\n")) | ||
| else | ||
| $summary | ||
| end; | ||
|
|
||
| # tsc: each "<file>:<line>:<col> - error TSxxxx: ..." line plus the closing | ||
| # "Found N errors in M files." line. | ||
| def summarize_tsc: | ||
| (split("\n")) as $lines | | ||
| ($lines | map(select(test("^\\S+\\.tsx?:[0-9]+:[0-9]+ - error TS")))) as $errors | | ||
| ($lines | map(select(test("^Found [0-9]+ errors? in"))) | first) as $found_line | | ||
| ($errors | map(" " + .) | join("\n")) as $body | | ||
| if $found_line then $found_line + "\n" + $body else $body end; | ||
|
|
||
| # eslint (stylish formatter): each file header groups its own indented | ||
| # "<line>:<col> error|warning message rule" lines; track the current file | ||
| # as we walk so each problem line can be reattached to it. | ||
| def summarize_eslint: | ||
| (split("\n")) as $lines | | ||
| (reduce $lines[] as $line | ||
| ({file: null, out: []}; | ||
| if ($line | test("^\\S.*\\.(ts|tsx|js|jsx|mjs|cjs)$")) then | ||
| {file: $line, out: .out} | ||
| elif ($line | test("^\\s+[0-9]+:[0-9]+\\s+(error|warning)\\s")) then | ||
| .out += [(.file // "?") + " " + ($line | ltrimstr(" "))] | ||
| else | ||
| (.content | tostring) | ||
| end) as $text | | ||
| " < " + ($text | truncate(200)) | ||
| else empty | ||
| end) | ||
| elif .type == "result" then | ||
| "=== session done: \(.subtype) | $\((.total_cost_usd * 10000 | round) / 10000) | \((.duration_ms / 1000) | floor)s | \(.num_turns) turns ===" | ||
| else | ||
| empty | ||
| end | ||
| . | ||
| end) | ||
| ) as $acc | | ||
| ($lines | map(select(test("^✖ [0-9]+ problems? \\("))) | first) as $summary_line | | ||
| ($acc.out | map(" " + .) | join("\n")) as $body | | ||
| if $summary_line then $summary_line + "\n" + $body else $body end; | ||
|
|
||
| # git status --short: condense the "XY <path>" lines into a count per status | ||
| # code (modified/added/deleted/renamed/untracked); any non-status lines in | ||
| # the same output (e.g. a chained "git log --oneline") pass through as-is. | ||
| def summarize_git_status: | ||
| (non_empty_lines) as $lines | | ||
| ($lines | map(select(test("^[ MADRCU?]{2}\\s\\S")))) as $status_lines | | ||
| ($lines | map(select(test("^[ MADRCU?]{2}\\s\\S") | not))) as $other_lines | | ||
| (if ($status_lines | length) == 0 then null | ||
| else | ||
| ($status_lines | map(.[0:2])) as $codes | | ||
| ([ | ||
| {k: "modified", n: ($codes | map(select(contains("M"))) | length)}, | ||
| {k: "added", n: ($codes | map(select(contains("A"))) | length)}, | ||
| {k: "deleted", n: ($codes | map(select(contains("D"))) | length)}, | ||
| {k: "renamed", n: ($codes | map(select(contains("R"))) | length)}, | ||
| {k: "untracked", n: ($codes | map(select(. == "??")) | length)} | ||
| ] | map(select(.n > 0)) | map("\(.n) \(.k)") | join(", ")) | ||
| end) as $status_summary | | ||
| ([$status_summary] + $other_lines | map(select(. != null)) | join("\n")); | ||
|
|
||
| # git commit: keep just the "[branch hash] subject" confirmation line and the | ||
| # diffstat summary, dropping the lefthook/commitlint box-drawing noise and | ||
| # per-file "create mode"/"delete mode" listing in between. | ||
| def summarize_git_commit: | ||
| (split("\n")) as $lines | | ||
| ($lines | map(select(test("^\\[\\S+ [0-9a-f]{6,}\\] "))) | first) as $commit_line | | ||
| ($lines | map(select(test("[0-9]+ files? changed"))) | first) as $diffstat_line | | ||
| ([$commit_line, $diffstat_line] | map(select(. != null)) | join("\n")); | ||
|
|
||
| # true for a Bash command that's purely dumping/listing file contents - | ||
| # chains of cat/ls/echo/head/tail joined by "&&"/";"/"||" (optionally behind | ||
| # "cd ... &&", "2>/dev/null" fallbacks, and "| head -N"/"| tail -N" trims). | ||
| # Same noise-vs-signal tradeoff as the Read tool suppression above: the | ||
| # request line already shows what was inspected, so the dump adds nothing. | ||
| def is_pure_inspect($cmd): | ||
| ($cmd | sub("^cd\\s+\"[^\"]*\"\\s*&&\\s*"; "")) as $rest | | ||
| ([$rest | splits("\\s*(&&|\\|\\||;)\\s*")]) as $parts | | ||
| ($parts | length) > 0 and | ||
| ($parts | all( | ||
| test("^(cat|ls|echo|head|tail|find)\\b[^|<>]*(\\s+2>\\s*(/dev/null|&1))?(\\s*\\|\\s*(head|tail)\\b[^|<>]*)?$") | ||
| or | ||
| test("^(tasklist|ps)\\b[^|<>]*(\\s+2>\\s*(/dev/null|&1))?(\\s*\\|\\s*grep\\b[^|<>]*)?$") | ||
| )); | ||
|
|
||
| def summarize_bash: | ||
| . as $text | | ||
| if ($text | test(" Test Files ")) then summarize_vitest | ||
| elif ($text | test("error TS[0-9]+:")) then summarize_tsc | ||
| elif ($text | test("✖ [0-9]+ problems? \\(")) then summarize_eslint | ||
| elif ($text | test("Ready in [0-9.]+\\s*m?s")) then | ||
| (split("\n") | map(select(test("Ready in [0-9.]+\\s*m?s"))) | first) | ||
| else | ||
| (non_empty_lines) as $lines | | ||
| ($lines | map(select(test("^\\[[A-Z]+\\]"))) | length) as $tagged | | ||
| if $tagged >= 2 and | ||
| ($lines[-1] | test("success|complete|done|failed|error"; "i") or test("[✅❌]")) then | ||
| $lines[-1] | ||
| else | ||
| ($lines | if length > 5 then .[-5:] else . end | join("\n") | truncate(300)) | ||
| end | ||
| end; | ||
|
|
||
| # recognizes a Bash-invoked "grep <pattern> ..." (optionally behind "cd ... | ||
| # &&" and a trailing "| head/tail -N"); returns the pattern so it gets the | ||
| # same one-line match-count summary as the dedicated Grep tool, instead of | ||
| # dumping every matched line. | ||
| def bash_grep_pattern($cmd): | ||
| ($cmd | sub("^cd\\s+\"[^\"]*\"\\s*&&\\s*"; "")) as $rest | | ||
| ($rest | sub("\\s*\\|\\s*(head|tail)\\b.*$"; "")) as $core | | ||
| if ($core | test("^grep\\s")) then | ||
| ($core | capture("^grep\\s+(-[a-zA-Z]+\\s+)*\"(?<pat>[^\"]*)\"").pat // null) | ||
| else | ||
| null | ||
| end; | ||
|
|
||
| def tool_result_text($content): | ||
| if ($content | type) == "array" then | ||
| ($content | map(.text? // "") | join(" ")) | ||
| else | ||
| ($content | tostring) | ||
| end; | ||
|
|
||
| def format_event($event; $tools): | ||
| if $event.type == "system" and $event.subtype == "init" then | ||
| "=== session started (model: \($event.model)) ===" | ||
| elif $event.type == "assistant" then | ||
| ($event.message.content[]? | | ||
| if .type == "text" then | ||
| .text | ||
| elif .type == "tool_use" then | ||
| " > " + .name + "(" + tool_summary(.name; .input) + ")" | ||
| else empty | ||
| end) | ||
| elif $event.type == "user" then | ||
| ($event.message.content[]? | | ||
| if .type == "tool_result" then | ||
| ($tools[.tool_use_id] // {}) as $tool | | ||
| ($tool.name // "") as $tool_name | | ||
| (tool_result_text(.content) | strip_ansi | strip_cr | strip_git_crlf_warnings | strip_git_diff_body) as $text | | ||
| if $tool_name == "Read" then | ||
| empty | ||
| elif $tool_name == "Glob" then | ||
| ($text | non_empty_lines | length) as $n | | ||
| " < found " + ($n | tostring) + | ||
| (if $n == 1 then " file matching \"" else " files matching \"" end) + | ||
| ($tool.input.pattern // "?") + "\"" | ||
| elif $tool_name == "Grep" then | ||
| ($text | non_empty_lines | length) as $n | | ||
| ($tool.input.output_mode == "files_with_matches") as $is_files | | ||
| " < " + ($n | tostring) + | ||
| (if $is_files then (if $n == 1 then " file" else " files" end) | ||
| else (if $n == 1 then " match" else " matches" end) end) + | ||
| " for \"" + ($tool.input.pattern // "?") + "\"" | ||
| elif $tool_name == "Bash" and is_pure_inspect($tool.input.command // "") then | ||
| empty | ||
|
Comment on lines
+199
to
+214
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 4 'is_error|tool_name == "Read"|is_pure_inspect' format-stream.jq overnight.shRepository: c0d3ster/nightlight Length of output: 2951 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- format-stream.jq relevant sections ---'
sed -n '1,80p;110,145p;175,235p' format-stream.jq
echo
echo '--- overnight.sh relevant sections ---'
sed -n '100,135p' overnight.shRepository: c0d3ster/nightlight Length of output: 9127 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '{
"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"1","is_error":true,"content":"Read failed: missing file at /tmp/missing.txt"}]}}
' | jq -r -f format-stream.jq
printf '%s\n' '{
"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"2","is_error":true,"content":"bash -i exited 1"}], "previous_event_id":"old"}}, {
"type":"assistant","message":{"content":[{"type":"tool_use","id":"1","name":"Read","input":{"file":"/tmp/missing.txt"}}]}
' | jq -n --slurp -r -f format-stream.jqRepository: c0d3ster/nightlight Length of output: 283 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '{
"type":"assistant","message":{"content":[{"type":"tool_use","id":"1","name":"Read","input":{"file":"/tmp/missing.txt"}}]}
},{
"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"1","is_error":true,"content":"Read failed: missing file at /tmp/missing.txt"}]}}
' | jq -r -f format-stream.jq
printf '%s\n' '{
"type":"assistant","message":{"content":[{"type":"tool_use","id":"2","name":"Bash","input":{"command":"cat /tmp/missing.txt"}}]}
},{
"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"2","is_error":true,"content":"cat: /tmp/missing.txt: No such file or directory"}]}}
' | jq -r -f format-stream.jq
printf '%s\n' '{
"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"3","is_error":true,"content":"permission denied"}]}}
' | jq -r -f format-stream.jqRepository: c0d3ster/nightlight Length of output: 400 Expose failed inspection tool errors before suppression. When a correlated 🤖 Prompt for AI Agents |
||
| elif $tool_name == "Bash" then | ||
| ($tool.input.command // "") as $cmd | | ||
| (bash_grep_pattern($cmd)) as $grep_pat | | ||
| if $grep_pat then | ||
| ($text | non_empty_lines | length) as $n | | ||
| " < " + ($n | tostring) + (if $n == 1 then " match" else " matches" end) + | ||
| " for \"" + $grep_pat + "\"" | ||
| elif ($cmd | test("git\\s+commit\\b")) then | ||
| " < " + ($text | summarize_git_commit | indent_continuations) | ||
| elif ($cmd | test("git\\s+status\\b.*(--short|-s)\\b")) then | ||
| " < " + ($text | summarize_git_status | indent_continuations) | ||
| else | ||
| " < " + ($text | summarize_bash | indent_continuations) | ||
| end | ||
| else | ||
| " < " + ($text | truncate(200) | indent_continuations) | ||
| end | ||
| else empty | ||
| end) | ||
| elif $event.type == "result" then | ||
| "=== session done: \($event.subtype) | $\(($event.total_cost_usd * 10000 | round) / 10000) | \(($event.duration_ms / 1000) | floor)s | \($event.num_turns) turns ===" | ||
| else | ||
| empty | ||
| end; | ||
|
|
||
| foreach (., inputs) as $event | ||
| ( {}; | ||
| if $event.type == "assistant" then | ||
| reduce ($event.message.content[]? | select(.type == "tool_use")) as $tu | ||
| (.; .[$tu.id] = {name: $tu.name, input: $tu.input}) | ||
| else . end; | ||
| format_event($event; .) | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: c0d3ster/nightlight
Length of output: 197
🏁 Script executed:
Repository: c0d3ster/nightlight
Length of output: 254
🏁 Script executed:
Repository: c0d3ster/nightlight
Length of output: 8535
Remove private CSI escape sequences in both log paths.
Use the complete CSI regex in
format-stream.jqand the matching error handler inovernight.shso private sequences such asESC[?25ldo not remain in the logs.📍 Affects 2 files
format-stream.jq#L11-L12(this comment)overnight.sh#L130-L130🤖 Prompt for AI Agents