claude-code-pretty 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_code_pretty-0.1.0/.bumpversion.cfg +8 -0
- claude_code_pretty-0.1.0/.github/images/demo.png +0 -0
- claude_code_pretty-0.1.0/.github/test-prompt.md +15 -0
- claude_code_pretty-0.1.0/.github/workflows/callable-ci.yml +77 -0
- claude_code_pretty-0.1.0/.github/workflows/prs.yml +9 -0
- claude_code_pretty-0.1.0/.github/workflows/push-to-main.yml +10 -0
- claude_code_pretty-0.1.0/.github/workflows/release.yml +59 -0
- claude_code_pretty-0.1.0/.gitignore +12 -0
- claude_code_pretty-0.1.0/.pre-commit-config.yaml +7 -0
- claude_code_pretty-0.1.0/Makefile +27 -0
- claude_code_pretty-0.1.0/PKG-INFO +90 -0
- claude_code_pretty-0.1.0/README.md +61 -0
- claude_code_pretty-0.1.0/pyproject.toml +55 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/__init__.py +1 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/cli.py +54 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/colors.py +56 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/constants.py +9 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/handlers/__init__.py +16 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/handlers/assistant.py +24 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/handlers/base.py +81 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/handlers/result.py +27 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/handlers/stream.py +39 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/handlers/system.py +16 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/__init__.py +31 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/bash.py +6 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/edit.py +6 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/glob.py +6 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/grep.py +12 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/notebook.py +6 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/read.py +6 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/task.py +11 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/todo.py +18 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/write.py +6 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/handlers/user.py +51 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/modes/__init__.py +4 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/modes/replay.py +34 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/modes/stream.py +44 -0
- claude_code_pretty-0.1.0/src/claudecodepretty/parser.py +40 -0
- claude_code_pretty-0.1.0/tests/__init__.py +0 -0
- claude_code_pretty-0.1.0/tests/fixtures/sample_session.jsonl +7 -0
- claude_code_pretty-0.1.0/tests/test_parser.py +180 -0
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
You are being tested to exercise all Claude Code internal tools. Do each step below in order:
|
|
2
|
+
|
|
3
|
+
1. Use Glob to list all files matching `*.sh` in the current directory
|
|
4
|
+
2. Use Read to read the file `show-colors.sh`
|
|
5
|
+
3. Use Write to create a file called `tmp-test-output.txt` with the content "hello from claude test"
|
|
6
|
+
4. Use Edit to change "hello from claude test" to "hello from claude test - updated" in `tmp-test-output.txt`
|
|
7
|
+
5. Use Grep to search for the word "color" in all `.sh` files
|
|
8
|
+
6. Use Bash to run `echo "bash tool works" && date`
|
|
9
|
+
7. Use the Task tool to launch a subagent with subagent_type "general-purpose" and the following prompt: "Do ALL of these steps in order: (a) Use Glob to find every file in the repo recursively with **/*. (b) Use Read to read ALL files you found - read them in parallel. (c) Use WebSearch to search for 'ANSI color codes terminal 2025 best practices'. (d) Use WebFetch to fetch https://en.wikipedia.org/wiki/ANSI_escape_code and extract the list of standard color codes. (e) Based on what you read from the repo files and the web research, use Write to create a file called 'subagent-report.md' with a detailed report that includes: a summary of what this repo does, a list of all ANSI colors currently used in claude-pretty.sh, a comparison with standard ANSI colors from your web research, and recommendations for additional colors that could improve readability. Make the report thorough."
|
|
10
|
+
8. Use the Task tool to launch a subagent with subagent_type "Bash" to run: "echo '--- bash subagent test ---' && uname -a && node --version && which claude && echo '--- done ---'"
|
|
11
|
+
9. Use MultiEdit on `tmp-test-output.txt` to make two changes at once: replace "hello" with "hey" and replace "updated" with "modified"
|
|
12
|
+
10. Use Bash to run `echo '{"cells":[{"cell_type":"code","source":["print(1+1)"],"metadata":{},"outputs":[],"execution_count":null}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python"}},"nbformat":4,"nbformat_minor":5}' > tmp-test.ipynb` to create a test notebook
|
|
13
|
+
11. Use NotebookEdit on `tmp-test.ipynb` to insert a new cell with edit_mode "insert" and cell_type "code" with the source `print("notebook edit works")`
|
|
14
|
+
12. Use TodoWrite to create a todo list with 3 items: "step one" (completed), "step two" (in_progress), "step three" (pending)
|
|
15
|
+
13. Print a final summary saying "All tools tested successfully"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
check:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
- uses: actions/setup-python@v5
|
|
12
|
+
with:
|
|
13
|
+
python-version: "3.12"
|
|
14
|
+
- run: pip install -e ".[dev]"
|
|
15
|
+
- run: ruff check .
|
|
16
|
+
- run: ruff format --check .
|
|
17
|
+
|
|
18
|
+
test:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
strategy:
|
|
21
|
+
matrix:
|
|
22
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: ${{ matrix.python-version }}
|
|
28
|
+
- run: pip install -e ".[dev]"
|
|
29
|
+
- run: pytest -v
|
|
30
|
+
|
|
31
|
+
e2e:
|
|
32
|
+
strategy:
|
|
33
|
+
matrix:
|
|
34
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
35
|
+
runs-on: ${{ matrix.os }}
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v4
|
|
38
|
+
- uses: actions/setup-python@v5
|
|
39
|
+
with:
|
|
40
|
+
python-version: "3.12"
|
|
41
|
+
- run: pip install -e ".[dev]"
|
|
42
|
+
- run: pytest -v
|
|
43
|
+
|
|
44
|
+
live:
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
|
|
50
|
+
- uses: actions/setup-python@v5
|
|
51
|
+
with:
|
|
52
|
+
python-version: "3.12"
|
|
53
|
+
|
|
54
|
+
- uses: actions/setup-node@v4
|
|
55
|
+
with:
|
|
56
|
+
node-version: "20"
|
|
57
|
+
|
|
58
|
+
- run: pip install -e .
|
|
59
|
+
|
|
60
|
+
- name: Install Claude Code
|
|
61
|
+
run: npm install -g @anthropic-ai/claude-code
|
|
62
|
+
|
|
63
|
+
- name: Run claudep with test prompt
|
|
64
|
+
continue-on-error: true
|
|
65
|
+
timeout-minutes: 10
|
|
66
|
+
env:
|
|
67
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
68
|
+
run: |
|
|
69
|
+
claudep -p "$(cat .github/test-prompt.md)"
|
|
70
|
+
|
|
71
|
+
- name: Upload session logs
|
|
72
|
+
if: always()
|
|
73
|
+
uses: actions/upload-artifact@v4
|
|
74
|
+
with:
|
|
75
|
+
name: claude-session-logs
|
|
76
|
+
path: ~/.claude/projects/
|
|
77
|
+
retention-days: 7
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
bump:
|
|
7
|
+
description: "Version bump type"
|
|
8
|
+
required: true
|
|
9
|
+
type: choice
|
|
10
|
+
options:
|
|
11
|
+
- patch
|
|
12
|
+
- minor
|
|
13
|
+
- major
|
|
14
|
+
- initial
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: write
|
|
18
|
+
id-token: write
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
release:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
environment: pypi
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
with:
|
|
27
|
+
fetch-depth: 0
|
|
28
|
+
|
|
29
|
+
- uses: actions/setup-python@v5
|
|
30
|
+
with:
|
|
31
|
+
python-version: "3.12"
|
|
32
|
+
|
|
33
|
+
- run: pip install --upgrade pip bump2version build
|
|
34
|
+
|
|
35
|
+
- name: Configure git
|
|
36
|
+
run: |
|
|
37
|
+
git config user.name "github-actions[bot]"
|
|
38
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
39
|
+
|
|
40
|
+
- name: Bump version
|
|
41
|
+
if: inputs.bump != 'initial'
|
|
42
|
+
run: bump2version ${{ inputs.bump }}
|
|
43
|
+
|
|
44
|
+
- name: Read new version
|
|
45
|
+
id: version
|
|
46
|
+
run: echo "version=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")" >> "$GITHUB_OUTPUT"
|
|
47
|
+
|
|
48
|
+
- name: Build
|
|
49
|
+
run: python -m build
|
|
50
|
+
|
|
51
|
+
- name: Publish to PyPI
|
|
52
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
53
|
+
|
|
54
|
+
- name: Commit and tag
|
|
55
|
+
run: |
|
|
56
|
+
git add -A
|
|
57
|
+
git commit -m "chore: release v${{ steps.version.outputs.version }}" || true
|
|
58
|
+
git tag "v${{ steps.version.outputs.version }}"
|
|
59
|
+
git push origin main --tags
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
install:
|
|
2
|
+
python3 -m venv .venv
|
|
3
|
+
.venv/bin/pip install -e ".[dev]"
|
|
4
|
+
.venv/bin/pre-commit install
|
|
5
|
+
|
|
6
|
+
check:
|
|
7
|
+
.venv/bin/ruff check .
|
|
8
|
+
.venv/bin/ruff format --check .
|
|
9
|
+
|
|
10
|
+
format:
|
|
11
|
+
.venv/bin/ruff check --fix .
|
|
12
|
+
.venv/bin/ruff format .
|
|
13
|
+
|
|
14
|
+
test:
|
|
15
|
+
.venv/bin/pytest -v
|
|
16
|
+
|
|
17
|
+
test-replay:
|
|
18
|
+
.venv/bin/claudep -f tests/fixtures/sample_session.jsonl
|
|
19
|
+
|
|
20
|
+
build:
|
|
21
|
+
.venv/bin/pip install hatch
|
|
22
|
+
.venv/bin/hatch build
|
|
23
|
+
|
|
24
|
+
clean:
|
|
25
|
+
rm -rf .venv dist build *.egg-info src/*.egg-info
|
|
26
|
+
|
|
27
|
+
.PHONY: install check format test test-replay build clean
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: claude-code-pretty
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pretty formatter for Claude Code stream-json output
|
|
5
|
+
Project-URL: Homepage, https://github.com/lucasvtiradentes/claude-code-pretty
|
|
6
|
+
Project-URL: Repository, https://github.com/lucasvtiradentes/claude-code-pretty
|
|
7
|
+
Project-URL: Issues, https://github.com/lucasvtiradentes/claude-code-pretty/issues
|
|
8
|
+
Author: lucasvtiradentes
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Software Development
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pre-commit>=4; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.9; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# claude-code-pretty
|
|
31
|
+
|
|
32
|
+
Pretty formatter for Claude Code - stream sessions in real-time or replay saved .jsonl files.
|
|
33
|
+
|
|
34
|
+
<div align="center">
|
|
35
|
+
<img src=".github/images/demo.png" width="70%">
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- stream mode - run claude with pretty output in real-time
|
|
41
|
+
- replay mode - replay saved .jsonl session files
|
|
42
|
+
- tool display - formatted output for Glob, Grep, Bash, Read, Edit, etc.
|
|
43
|
+
- markdown - renders **bold** and `code` with ANSI styles
|
|
44
|
+
- subagent depth - visual indentation for nested Task calls
|
|
45
|
+
- cost tracking - shows duration, cost, tokens per session
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install claude-code-pretty
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Aliases: `claudep`, `ccp`, `claude-code-pretty`
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# stream mode - run claude with pretty output
|
|
59
|
+
claudep -p "explain this code"
|
|
60
|
+
|
|
61
|
+
# replay mode - replay a saved session
|
|
62
|
+
claudep -f ~/.claude/projects/.../session.jsonl
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Stream mode runs claude with these flags automatically:
|
|
66
|
+
```
|
|
67
|
+
--print --verbose --dangerously-skip-permissions --output-format stream-json --include-partial-messages
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Environment
|
|
71
|
+
|
|
72
|
+
| Variable | Default | Description |
|
|
73
|
+
|--------------------------|---------|-------------------------------------|
|
|
74
|
+
| CP_TOOL_RESULT_MAX_CHARS | 300 | Max chars for tool results (0=hide) |
|
|
75
|
+
| CP_READ_PREVIEW_LINES | 5 | Lines to preview from Read (0=hide) |
|
|
76
|
+
|
|
77
|
+
## Development
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
make install # setup venv + install
|
|
81
|
+
make test # run pytest
|
|
82
|
+
make check # ruff lint
|
|
83
|
+
|
|
84
|
+
# dev alias
|
|
85
|
+
ln -sf $(pwd)/.venv/bin/claudep ~/.local/bin/claudepd
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# claude-code-pretty
|
|
2
|
+
|
|
3
|
+
Pretty formatter for Claude Code - stream sessions in real-time or replay saved .jsonl files.
|
|
4
|
+
|
|
5
|
+
<div align="center">
|
|
6
|
+
<img src=".github/images/demo.png" width="70%">
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- stream mode - run claude with pretty output in real-time
|
|
12
|
+
- replay mode - replay saved .jsonl session files
|
|
13
|
+
- tool display - formatted output for Glob, Grep, Bash, Read, Edit, etc.
|
|
14
|
+
- markdown - renders **bold** and `code` with ANSI styles
|
|
15
|
+
- subagent depth - visual indentation for nested Task calls
|
|
16
|
+
- cost tracking - shows duration, cost, tokens per session
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install claude-code-pretty
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Aliases: `claudep`, `ccp`, `claude-code-pretty`
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# stream mode - run claude with pretty output
|
|
30
|
+
claudep -p "explain this code"
|
|
31
|
+
|
|
32
|
+
# replay mode - replay a saved session
|
|
33
|
+
claudep -f ~/.claude/projects/.../session.jsonl
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Stream mode runs claude with these flags automatically:
|
|
37
|
+
```
|
|
38
|
+
--print --verbose --dangerously-skip-permissions --output-format stream-json --include-partial-messages
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Environment
|
|
42
|
+
|
|
43
|
+
| Variable | Default | Description |
|
|
44
|
+
|--------------------------|---------|-------------------------------------|
|
|
45
|
+
| CP_TOOL_RESULT_MAX_CHARS | 300 | Max chars for tool results (0=hide) |
|
|
46
|
+
| CP_READ_PREVIEW_LINES | 5 | Lines to preview from Read (0=hide) |
|
|
47
|
+
|
|
48
|
+
## Development
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
make install # setup venv + install
|
|
52
|
+
make test # run pytest
|
|
53
|
+
make check # ruff lint
|
|
54
|
+
|
|
55
|
+
# dev alias
|
|
56
|
+
ln -sf $(pwd)/.venv/bin/claudep ~/.local/bin/claudepd
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "claude-code-pretty"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Pretty formatter for Claude Code stream-json output"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "lucasvtiradentes" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Topic :: Software Development",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/lucasvtiradentes/claude-code-pretty"
|
|
31
|
+
Repository = "https://github.com/lucasvtiradentes/claude-code-pretty"
|
|
32
|
+
Issues = "https://github.com/lucasvtiradentes/claude-code-pretty/issues"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = ["pytest>=7", "ruff>=0.9", "pre-commit>=4"]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
claudep = "claudecodepretty.cli:main"
|
|
39
|
+
ccp = "claudecodepretty.cli:main"
|
|
40
|
+
claude-code-pretty = "claudecodepretty.cli:main"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/claudecodepretty"]
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel.sources]
|
|
46
|
+
"src" = ""
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = ["tests"]
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 120
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint]
|
|
55
|
+
select = ["E", "F", "I"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from claudecodepretty import __version__
|
|
4
|
+
from claudecodepretty.constants import CLI_NAME
|
|
5
|
+
from claudecodepretty.modes import run_replay, run_stream
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def print_help():
|
|
9
|
+
print(f"""{CLI_NAME} - Pretty formatter for Claude Code stream-json output
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
{CLI_NAME} [OPTIONS] [CLAUDE_ARGS...]
|
|
13
|
+
{CLI_NAME} -f <session.jsonl>
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
-f, --file FILE Replay a saved session .jsonl file
|
|
17
|
+
-h, --help Show this help
|
|
18
|
+
-v, --version Show version
|
|
19
|
+
|
|
20
|
+
Environment:
|
|
21
|
+
CP_TOOL_RESULT_MAX_CHARS Max chars for tool results (default: 300, 0=hide)
|
|
22
|
+
CP_READ_PREVIEW_LINES Lines to preview from Read (default: 5, 0=hide)
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
{CLI_NAME} -p "explain this code"
|
|
26
|
+
{CLI_NAME} -f ~/.claude/projects/.../session.jsonl
|
|
27
|
+
cat session.jsonl | {CLI_NAME} -f -""")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main():
|
|
31
|
+
args = sys.argv[1:]
|
|
32
|
+
|
|
33
|
+
if not args or "-h" in args or "--help" in args:
|
|
34
|
+
print_help()
|
|
35
|
+
sys.exit(0)
|
|
36
|
+
|
|
37
|
+
if "-v" in args or "--version" in args:
|
|
38
|
+
print(__version__)
|
|
39
|
+
sys.exit(0)
|
|
40
|
+
|
|
41
|
+
if "-f" in args or "--file" in args:
|
|
42
|
+
try:
|
|
43
|
+
idx = args.index("-f") if "-f" in args else args.index("--file")
|
|
44
|
+
file_path = args[idx + 1]
|
|
45
|
+
sys.exit(run_replay(file_path))
|
|
46
|
+
except IndexError:
|
|
47
|
+
print("Error: -f requires a file path")
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
sys.exit(run_stream(args))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
main()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
BOLD = "\033[1m"
|
|
2
|
+
INVERSE = "\033[7m"
|
|
3
|
+
GREEN = "\033[32m"
|
|
4
|
+
ORANGE = "\033[33m"
|
|
5
|
+
PURPLE = "\033[35m"
|
|
6
|
+
CYAN = "\033[36m"
|
|
7
|
+
BLUE = "\033[34m"
|
|
8
|
+
YELLOW = "\033[93m"
|
|
9
|
+
DIM = "\033[90m"
|
|
10
|
+
RED = "\033[31m"
|
|
11
|
+
RESET = "\033[0m"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def green(text: str) -> str:
|
|
15
|
+
return f"{GREEN}{text}{RESET}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def orange(text: str) -> str:
|
|
19
|
+
return f"{ORANGE}{text}{RESET}"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def purple(text: str) -> str:
|
|
23
|
+
return f"{PURPLE}{text}{RESET}"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def cyan(text: str) -> str:
|
|
27
|
+
return f"{CYAN}{text}{RESET}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def blue(text: str) -> str:
|
|
31
|
+
return f"{BLUE}{text}{RESET}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def yellow(text: str) -> str:
|
|
35
|
+
return f"{YELLOW}{text}{RESET}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def dim(text: str) -> str:
|
|
39
|
+
return f"{DIM}{text}{RESET}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def red(text: str) -> str:
|
|
43
|
+
return f"{RED}{text}{RESET}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def bold(text: str) -> str:
|
|
47
|
+
return f"{BOLD}{text}{RESET}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def render_markdown(text: str) -> str:
|
|
51
|
+
import re
|
|
52
|
+
|
|
53
|
+
text = re.sub(r"\*\*(.+?)\*\*", rf"{BOLD}\1{RESET}", text)
|
|
54
|
+
text = re.sub(r"__(.+?)__", rf"{BOLD}\1{RESET}", text)
|
|
55
|
+
text = re.sub(r"`([^`]+)`", rf"{INVERSE}\1{RESET}", text)
|
|
56
|
+
return text
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
CLI_NAME = "claudep"
|
|
4
|
+
DIST_NAME = "claude-code-pretty"
|
|
5
|
+
INDENT = " "
|
|
6
|
+
HIDE_TOOLS = {"Write", "TodoWrite", "Read", "Glob", "Grep", "Bash", "Task", "Edit", "MultiEdit", "NotebookEdit"}
|
|
7
|
+
|
|
8
|
+
TOOL_RESULT_MAX_CHARS = int(os.environ.get("CP_TOOL_RESULT_MAX_CHARS", "300"))
|
|
9
|
+
READ_PREVIEW_LINES = int(os.environ.get("CP_READ_PREVIEW_LINES", "5"))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from claudecodepretty.handlers.assistant import handle_assistant_message
|
|
2
|
+
from claudecodepretty.handlers.base import ParseResult, ParserState
|
|
3
|
+
from claudecodepretty.handlers.result import handle_result
|
|
4
|
+
from claudecodepretty.handlers.stream import handle_stream_event
|
|
5
|
+
from claudecodepretty.handlers.system import handle_system
|
|
6
|
+
from claudecodepretty.handlers.user import handle_user_message
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ParseResult",
|
|
10
|
+
"ParserState",
|
|
11
|
+
"handle_system",
|
|
12
|
+
"handle_stream_event",
|
|
13
|
+
"handle_user_message",
|
|
14
|
+
"handle_assistant_message",
|
|
15
|
+
"handle_result",
|
|
16
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from claudecodepretty.colors import render_markdown
|
|
2
|
+
from claudecodepretty.handlers.base import ParseResult, ParserState
|
|
3
|
+
from claudecodepretty.handlers.tools import dispatch_tool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def handle_assistant_message(data: dict, state: ParserState, result: ParseResult):
|
|
7
|
+
message = data.get("message", {})
|
|
8
|
+
content = message.get("content", [])
|
|
9
|
+
|
|
10
|
+
if not isinstance(content, list):
|
|
11
|
+
return
|
|
12
|
+
|
|
13
|
+
if state.mode == "replay":
|
|
14
|
+
for block in content:
|
|
15
|
+
if block.get("type") == "text":
|
|
16
|
+
result.add(render_markdown(block.get("text", "")))
|
|
17
|
+
|
|
18
|
+
for block in content:
|
|
19
|
+
if block.get("type") != "tool_use":
|
|
20
|
+
continue
|
|
21
|
+
|
|
22
|
+
name = block.get("name", "")
|
|
23
|
+
inp = block.get("input", {})
|
|
24
|
+
dispatch_tool(name, inp, state, result)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
from claudecodepretty.colors import BOLD, DIM, INVERSE, RESET
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class ParserState:
|
|
8
|
+
mode: str = "stream"
|
|
9
|
+
current_tool: str = ""
|
|
10
|
+
subagent_depth: int = 0
|
|
11
|
+
sp: str = ""
|
|
12
|
+
in_bold: bool = False
|
|
13
|
+
in_code: bool = False
|
|
14
|
+
pending_char: str = ""
|
|
15
|
+
|
|
16
|
+
def update_sp(self):
|
|
17
|
+
self.sp = "".join(f"{DIM}│{RESET} " for _ in range(self.subagent_depth))
|
|
18
|
+
|
|
19
|
+
def increment_depth(self):
|
|
20
|
+
self.subagent_depth += 1
|
|
21
|
+
self.update_sp()
|
|
22
|
+
|
|
23
|
+
def decrement_depth(self):
|
|
24
|
+
if self.subagent_depth > 0:
|
|
25
|
+
self.subagent_depth -= 1
|
|
26
|
+
self.update_sp()
|
|
27
|
+
|
|
28
|
+
def _get_style(self) -> str:
|
|
29
|
+
if self.in_bold and self.in_code:
|
|
30
|
+
return BOLD + INVERSE
|
|
31
|
+
if self.in_bold:
|
|
32
|
+
return BOLD
|
|
33
|
+
if self.in_code:
|
|
34
|
+
return INVERSE
|
|
35
|
+
return ""
|
|
36
|
+
|
|
37
|
+
def render_text(self, text: str) -> str:
|
|
38
|
+
out = []
|
|
39
|
+
i = 0
|
|
40
|
+
buf = self.pending_char + text
|
|
41
|
+
self.pending_char = ""
|
|
42
|
+
|
|
43
|
+
while i < len(buf):
|
|
44
|
+
ch = buf[i]
|
|
45
|
+
if ch == "*" and not self.in_code:
|
|
46
|
+
if i + 1 < len(buf):
|
|
47
|
+
if buf[i + 1] == "*":
|
|
48
|
+
self.in_bold = not self.in_bold
|
|
49
|
+
out.append(RESET + self._get_style())
|
|
50
|
+
i += 2
|
|
51
|
+
continue
|
|
52
|
+
else:
|
|
53
|
+
out.append(ch)
|
|
54
|
+
i += 1
|
|
55
|
+
else:
|
|
56
|
+
self.pending_char = "*"
|
|
57
|
+
break
|
|
58
|
+
elif ch == "`":
|
|
59
|
+
self.in_code = not self.in_code
|
|
60
|
+
out.append(RESET + self._get_style())
|
|
61
|
+
i += 1
|
|
62
|
+
else:
|
|
63
|
+
out.append(ch)
|
|
64
|
+
i += 1
|
|
65
|
+
|
|
66
|
+
return "".join(out)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class ParseResult:
|
|
71
|
+
output: str = ""
|
|
72
|
+
messages: list[str] = field(default_factory=list)
|
|
73
|
+
|
|
74
|
+
def add(self, text: str):
|
|
75
|
+
self.messages.append(text)
|
|
76
|
+
|
|
77
|
+
def add_inline(self, text: str):
|
|
78
|
+
self.messages.append(text)
|
|
79
|
+
|
|
80
|
+
def get_output(self) -> str:
|
|
81
|
+
return "".join(self.messages)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from claudecodepretty.colors import DIM, RED, RESET
|
|
2
|
+
from claudecodepretty.handlers.base import ParseResult, ParserState
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def handle_result(data: dict, state: ParserState, result: ParseResult):
|
|
6
|
+
while state.subagent_depth > 0:
|
|
7
|
+
result.add(f"{state.sp}{DIM}└───────────────────────────────{RESET}\n")
|
|
8
|
+
state.decrement_depth()
|
|
9
|
+
|
|
10
|
+
if data.get("is_error"):
|
|
11
|
+
result.add(f"\n{RED}[error] {data.get('result', 'unknown error')}{RESET}\n")
|
|
12
|
+
else:
|
|
13
|
+
duration_ms = data.get("duration_ms", 0)
|
|
14
|
+
duration = f"{duration_ms / 1000:.1f}"
|
|
15
|
+
cost = f"{data.get('total_cost_usd', 0):.4f}"
|
|
16
|
+
turns = data.get("num_turns", 0)
|
|
17
|
+
usage = data.get("usage", {})
|
|
18
|
+
input_tokens = sum(
|
|
19
|
+
[
|
|
20
|
+
usage.get("input_tokens", 0),
|
|
21
|
+
usage.get("cache_read_input_tokens", 0),
|
|
22
|
+
usage.get("cache_creation_input_tokens", 0),
|
|
23
|
+
]
|
|
24
|
+
)
|
|
25
|
+
output_tokens = usage.get("output_tokens", 0)
|
|
26
|
+
stats = f"{duration}s, ${cost}, {turns} turns, {input_tokens} in / {output_tokens} out"
|
|
27
|
+
result.add(f"\n{DIM}[done] {stats}{RESET}\n")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from claudecodepretty.colors import DIM, PURPLE, RED, RESET
|
|
2
|
+
from claudecodepretty.constants import HIDE_TOOLS
|
|
3
|
+
from claudecodepretty.handlers.base import ParseResult, ParserState
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def handle_stream_event(data: dict, state: ParserState, result: ParseResult):
|
|
7
|
+
event = data.get("event", {})
|
|
8
|
+
event_type = event.get("type", "")
|
|
9
|
+
|
|
10
|
+
if event_type == "content_block_start":
|
|
11
|
+
if state.subagent_depth > 0:
|
|
12
|
+
result.add(f"{state.sp}{DIM}└───────────────────────────────{RESET}\n")
|
|
13
|
+
state.decrement_depth()
|
|
14
|
+
|
|
15
|
+
block = event.get("content_block", {})
|
|
16
|
+
if block.get("type") == "tool_use":
|
|
17
|
+
name = block.get("name", "")
|
|
18
|
+
state.current_tool = name
|
|
19
|
+
if name not in HIDE_TOOLS:
|
|
20
|
+
result.add(f"\n{state.sp}{PURPLE}[{name}] ")
|
|
21
|
+
|
|
22
|
+
elif event_type == "content_block_delta":
|
|
23
|
+
delta = event.get("delta", {})
|
|
24
|
+
delta_type = delta.get("type", "")
|
|
25
|
+
|
|
26
|
+
if delta_type == "text_delta":
|
|
27
|
+
result.add_inline(state.render_text(delta.get("text", "")))
|
|
28
|
+
elif delta_type == "input_json_delta":
|
|
29
|
+
if state.current_tool not in HIDE_TOOLS:
|
|
30
|
+
result.add_inline(delta.get("partial_json", ""))
|
|
31
|
+
|
|
32
|
+
elif event_type == "content_block_stop":
|
|
33
|
+
if state.current_tool not in HIDE_TOOLS:
|
|
34
|
+
result.add(f"{RESET}\n")
|
|
35
|
+
state.current_tool = ""
|
|
36
|
+
|
|
37
|
+
elif event_type == "error":
|
|
38
|
+
error = event.get("error", str(event))
|
|
39
|
+
result.add(f"\n{state.sp}{RED}[error] {error}{RESET}\n")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from claudecodepretty.colors import DIM, RESET
|
|
2
|
+
from claudecodepretty.constants import INDENT
|
|
3
|
+
from claudecodepretty.handlers.base import ParseResult, ParserState
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def handle_system(data: dict, state: ParserState, result: ParseResult):
|
|
7
|
+
if data.get("subtype") == "init":
|
|
8
|
+
session_id = data.get("session_id", "")
|
|
9
|
+
cwd = data.get("cwd", "").replace("/", "-").replace("_", "-")
|
|
10
|
+
model = data.get("model", "")
|
|
11
|
+
model_name = model.split("-")[1] if "-" in model else model
|
|
12
|
+
|
|
13
|
+
result.add(f"{DIM}[session]\n")
|
|
14
|
+
result.add(f"{INDENT}id: {session_id}\n")
|
|
15
|
+
result.add(f"{INDENT}path: ~/.claude/projects/{cwd}/{session_id}.jsonl\n")
|
|
16
|
+
result.add(f"{INDENT}model: {model_name}{RESET}\n\n")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from claudecodepretty.handlers.base import ParseResult, ParserState
|
|
2
|
+
from claudecodepretty.handlers.tools.bash import handle_bash
|
|
3
|
+
from claudecodepretty.handlers.tools.edit import handle_edit
|
|
4
|
+
from claudecodepretty.handlers.tools.glob import handle_glob
|
|
5
|
+
from claudecodepretty.handlers.tools.grep import handle_grep
|
|
6
|
+
from claudecodepretty.handlers.tools.notebook import handle_notebook
|
|
7
|
+
from claudecodepretty.handlers.tools.read import handle_read
|
|
8
|
+
from claudecodepretty.handlers.tools.task import handle_task
|
|
9
|
+
from claudecodepretty.handlers.tools.todo import handle_todo
|
|
10
|
+
from claudecodepretty.handlers.tools.write import handle_write
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def dispatch_tool(name: str, inp: dict, state: ParserState, result: ParseResult):
|
|
14
|
+
if name == "TodoWrite":
|
|
15
|
+
handle_todo(inp, state, result)
|
|
16
|
+
elif name == "Write":
|
|
17
|
+
handle_write(inp, state, result)
|
|
18
|
+
elif name == "Read":
|
|
19
|
+
handle_read(inp, state, result)
|
|
20
|
+
elif name == "Glob":
|
|
21
|
+
handle_glob(inp, state, result)
|
|
22
|
+
elif name == "Grep":
|
|
23
|
+
handle_grep(inp, state, result)
|
|
24
|
+
elif name in ("Edit", "MultiEdit"):
|
|
25
|
+
handle_edit(name, inp, state, result)
|
|
26
|
+
elif name == "NotebookEdit":
|
|
27
|
+
handle_notebook(inp, state, result)
|
|
28
|
+
elif name == "Bash":
|
|
29
|
+
handle_bash(inp, state, result)
|
|
30
|
+
elif name == "Task":
|
|
31
|
+
handle_task(inp, state, result)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from claudecodepretty.colors import PURPLE, RESET
|
|
2
|
+
from claudecodepretty.handlers.base import ParseResult, ParserState
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def handle_bash(inp: dict, state: ParserState, result: ParseResult):
|
|
6
|
+
result.add(f"\n{state.sp}{PURPLE}[Bash] {inp.get('command', '')}{RESET}\n")
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from claudecodepretty.colors import ORANGE, RESET
|
|
2
|
+
from claudecodepretty.handlers.base import ParseResult, ParserState
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def handle_edit(name: str, inp: dict, state: ParserState, result: ParseResult):
|
|
6
|
+
result.add(f"\n{state.sp}{ORANGE}[{name}] {inp.get('file_path', '')}{RESET}\n")
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from claudecodepretty.colors import PURPLE, RESET
|
|
2
|
+
from claudecodepretty.handlers.base import ParseResult, ParserState
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def handle_glob(inp: dict, state: ParserState, result: ParseResult):
|
|
6
|
+
result.add(f"\n{state.sp}{PURPLE}[Glob] {inp.get('pattern', '')}{RESET}\n")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from claudecodepretty.colors import PURPLE, RESET
|
|
2
|
+
from claudecodepretty.handlers.base import ParseResult, ParserState
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def handle_grep(inp: dict, state: ParserState, result: ParseResult):
|
|
6
|
+
pattern = inp.get("pattern", "")
|
|
7
|
+
path = inp.get("path", "")
|
|
8
|
+
if path:
|
|
9
|
+
path = path.split("/")[-1]
|
|
10
|
+
result.add(f'\n{state.sp}{PURPLE}[Grep] "{pattern}" in {path}{RESET}\n')
|
|
11
|
+
else:
|
|
12
|
+
result.add(f'\n{state.sp}{PURPLE}[Grep] "{pattern}"{RESET}\n')
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from claudecodepretty.colors import ORANGE, RESET
|
|
2
|
+
from claudecodepretty.handlers.base import ParseResult, ParserState
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def handle_notebook(inp: dict, state: ParserState, result: ParseResult):
|
|
6
|
+
result.add(f"\n{state.sp}{ORANGE}[NotebookEdit] {inp.get('notebook_path', '')}{RESET}\n")
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from claudecodepretty.colors import GREEN, RESET
|
|
2
|
+
from claudecodepretty.handlers.base import ParseResult, ParserState
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def handle_read(inp: dict, state: ParserState, result: ParseResult):
|
|
6
|
+
result.add(f"\n{state.sp}{GREEN}[Read] {inp.get('file_path', '')}{RESET}\n")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from claudecodepretty.colors import BLUE, DIM, RESET
|
|
2
|
+
from claudecodepretty.handlers.base import ParseResult, ParserState
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def handle_task(inp: dict, state: ParserState, result: ParseResult):
|
|
6
|
+
prompt = inp.get("prompt", inp.get("description", ""))[:50]
|
|
7
|
+
model = inp.get("model", "sonnet")
|
|
8
|
+
result.add(f'\n{state.sp}{BLUE}[Task] "{prompt}" ({model}){RESET}\n')
|
|
9
|
+
if state.mode == "stream":
|
|
10
|
+
state.increment_depth()
|
|
11
|
+
result.add(f"{state.sp}{DIM}┌───────────────────────────────{RESET}\n")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from claudecodepretty.colors import DIM, GREEN, ORANGE, RESET, YELLOW
|
|
2
|
+
from claudecodepretty.constants import INDENT
|
|
3
|
+
from claudecodepretty.handlers.base import ParseResult, ParserState
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def handle_todo(inp: dict, state: ParserState, result: ParseResult):
|
|
7
|
+
result.add(f"\n{state.sp}{YELLOW}[Todo]{RESET}\n")
|
|
8
|
+
for todo in inp.get("todos", []):
|
|
9
|
+
status = todo.get("status", "pending")
|
|
10
|
+
text = todo.get("content", "")
|
|
11
|
+
if status == "completed":
|
|
12
|
+
mark = f"{GREEN}[x]{RESET}"
|
|
13
|
+
elif status == "in_progress":
|
|
14
|
+
mark = f"{ORANGE}[~]{RESET}"
|
|
15
|
+
else:
|
|
16
|
+
mark = f"{DIM}[ ]{RESET}"
|
|
17
|
+
result.add(f"{state.sp}{INDENT}{mark} {text}\n")
|
|
18
|
+
result.add("\n")
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from claudecodepretty.colors import ORANGE, RESET
|
|
2
|
+
from claudecodepretty.handlers.base import ParseResult, ParserState
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def handle_write(inp: dict, state: ParserState, result: ParseResult):
|
|
6
|
+
result.add(f"\n{state.sp}{ORANGE}[Write] {inp.get('file_path', '')}{RESET}\n")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from claudecodepretty.colors import DIM, GREEN, RED, RESET
|
|
4
|
+
from claudecodepretty.constants import INDENT, READ_PREVIEW_LINES, TOOL_RESULT_MAX_CHARS
|
|
5
|
+
from claudecodepretty.handlers.base import ParseResult, ParserState
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def handle_user_message(data: dict, state: ParserState, result: ParseResult):
|
|
9
|
+
message = data.get("message", {})
|
|
10
|
+
content = message.get("content", "")
|
|
11
|
+
|
|
12
|
+
if isinstance(content, str):
|
|
13
|
+
if state.mode == "replay":
|
|
14
|
+
text = content[:200]
|
|
15
|
+
result.add(f"\n{GREEN}[user]{RESET} {text}")
|
|
16
|
+
if len(content) > 200:
|
|
17
|
+
result.add(f"{DIM}...{RESET}")
|
|
18
|
+
result.add("\n")
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
if not isinstance(content, list) or not content:
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
first = content[0]
|
|
25
|
+
content_type = first.get("type", "")
|
|
26
|
+
|
|
27
|
+
if content_type == "tool_result":
|
|
28
|
+
tool_content = first.get("content", "")
|
|
29
|
+
|
|
30
|
+
if isinstance(tool_content, str):
|
|
31
|
+
if tool_content.startswith(("Todos have been", "The file")) and "has been" in tool_content:
|
|
32
|
+
return
|
|
33
|
+
if "<tool_use_error>" in tool_content:
|
|
34
|
+
error_msg = re.sub(r"<[^>]*>", "", tool_content)
|
|
35
|
+
result.add(f"{state.sp}{RED}{INDENT}✗ {error_msg}{RESET}\n\n")
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
if "\n" in tool_content:
|
|
39
|
+
if READ_PREVIEW_LINES == 0:
|
|
40
|
+
return
|
|
41
|
+
lines = tool_content.split("\n")[:READ_PREVIEW_LINES]
|
|
42
|
+
for line in lines:
|
|
43
|
+
result.add(f"{state.sp}{DIM}{INDENT}→ {line}{RESET}\n")
|
|
44
|
+
if len(tool_content.split("\n")) > READ_PREVIEW_LINES:
|
|
45
|
+
result.add(f"{state.sp}{INDENT}...\n")
|
|
46
|
+
result.add("\n")
|
|
47
|
+
else:
|
|
48
|
+
if TOOL_RESULT_MAX_CHARS == 0:
|
|
49
|
+
return
|
|
50
|
+
text = tool_content if TOOL_RESULT_MAX_CHARS < 0 else tool_content[:TOOL_RESULT_MAX_CHARS]
|
|
51
|
+
result.add(f"{state.sp}{DIM}{INDENT}→ {text}{RESET}\n\n")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from claudecodepretty.colors import DIM, RESET
|
|
5
|
+
from claudecodepretty.handlers import ParserState
|
|
6
|
+
from claudecodepretty.parser import parse_json_line
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run_replay(file_path: str) -> int:
|
|
10
|
+
state = ParserState(mode="replay")
|
|
11
|
+
|
|
12
|
+
if file_path == "-":
|
|
13
|
+
source = sys.stdin
|
|
14
|
+
print(f"{DIM}[replay] stdin{RESET}\n")
|
|
15
|
+
else:
|
|
16
|
+
if not os.path.exists(file_path):
|
|
17
|
+
print(f"Error: File not found: {file_path}")
|
|
18
|
+
return 1
|
|
19
|
+
source = open(file_path, "r")
|
|
20
|
+
print(f"{DIM}[replay] {file_path}{RESET}\n")
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
for line in source:
|
|
24
|
+
line = line.strip()
|
|
25
|
+
if line:
|
|
26
|
+
result = parse_json_line(line, state)
|
|
27
|
+
output = result.get_output()
|
|
28
|
+
if output:
|
|
29
|
+
print(output, end="", flush=True)
|
|
30
|
+
finally:
|
|
31
|
+
if file_path != "-":
|
|
32
|
+
source.close()
|
|
33
|
+
|
|
34
|
+
return 0
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from claudecodepretty.handlers import ParserState
|
|
5
|
+
from claudecodepretty.parser import parse_json_line
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_stream(args: list[str]) -> int:
|
|
9
|
+
cmd = [
|
|
10
|
+
"claude",
|
|
11
|
+
"--print",
|
|
12
|
+
"--verbose",
|
|
13
|
+
"--dangerously-skip-permissions",
|
|
14
|
+
"--output-format",
|
|
15
|
+
"stream-json",
|
|
16
|
+
"--include-partial-messages",
|
|
17
|
+
*args,
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
state = ParserState(mode="stream")
|
|
21
|
+
|
|
22
|
+
process = subprocess.Popen(
|
|
23
|
+
cmd,
|
|
24
|
+
stdout=subprocess.PIPE,
|
|
25
|
+
stderr=subprocess.STDOUT,
|
|
26
|
+
text=True,
|
|
27
|
+
bufsize=1,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
for line in process.stdout:
|
|
32
|
+
line = line.strip()
|
|
33
|
+
if line:
|
|
34
|
+
result = parse_json_line(line, state)
|
|
35
|
+
output = result.get_output()
|
|
36
|
+
if output:
|
|
37
|
+
print(output, end="", flush=True)
|
|
38
|
+
except KeyboardInterrupt:
|
|
39
|
+
process.terminate()
|
|
40
|
+
sys.exit(130)
|
|
41
|
+
finally:
|
|
42
|
+
process.wait()
|
|
43
|
+
|
|
44
|
+
return process.returncode
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from claudecodepretty.colors import RED, RESET
|
|
4
|
+
from claudecodepretty.handlers import (
|
|
5
|
+
ParseResult,
|
|
6
|
+
ParserState,
|
|
7
|
+
handle_assistant_message,
|
|
8
|
+
handle_result,
|
|
9
|
+
handle_stream_event,
|
|
10
|
+
handle_system,
|
|
11
|
+
handle_user_message,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = ["parse_json_line", "ParserState", "ParseResult"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_json_line(line: str, state: ParserState) -> ParseResult:
|
|
18
|
+
result = ParseResult()
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
data = json.loads(line)
|
|
22
|
+
except json.JSONDecodeError:
|
|
23
|
+
return result
|
|
24
|
+
|
|
25
|
+
msg_type = data.get("type", "")
|
|
26
|
+
|
|
27
|
+
if msg_type == "system":
|
|
28
|
+
handle_system(data, state, result)
|
|
29
|
+
elif msg_type == "stream_event":
|
|
30
|
+
handle_stream_event(data, state, result)
|
|
31
|
+
elif msg_type == "user":
|
|
32
|
+
handle_user_message(data, state, result)
|
|
33
|
+
elif msg_type == "assistant":
|
|
34
|
+
handle_assistant_message(data, state, result)
|
|
35
|
+
elif msg_type == "result":
|
|
36
|
+
handle_result(data, state, result)
|
|
37
|
+
elif msg_type == "error":
|
|
38
|
+
result.add(f"\n{state.sp}{RED}[error] {data.get('error', 'unknown error')}{RESET}")
|
|
39
|
+
|
|
40
|
+
return result
|
|
File without changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{"type":"system","subtype":"init","session_id":"test-session-123","cwd":"/Users/test/project","model":"claude-sonnet-4-20250514"}
|
|
2
|
+
{"type":"user","message":{"content":"List all Python files"}}
|
|
3
|
+
{"type":"assistant","message":{"content":[{"type":"text","text":"I'll list all Python files for you."}]}}
|
|
4
|
+
{"type":"assistant","message":{"content":[{"type":"tool_use","name":"Glob","input":{"pattern":"*.py"}}]}}
|
|
5
|
+
{"type":"user","message":{"content":[{"type":"tool_result","content":"main.py\nutils.py\ntest.py"}]}}
|
|
6
|
+
{"type":"assistant","message":{"content":[{"type":"text","text":"Found 3 Python files: main.py, utils.py, and test.py."}]}}
|
|
7
|
+
{"type":"result","is_error":false,"duration_ms":2500,"total_cost_usd":0.025,"num_turns":2,"usage":{"input_tokens":500,"output_tokens":100}}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from claudecodepretty.parser import ParserState, parse_json_line
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_parse_system_init():
|
|
7
|
+
state = ParserState(mode="stream")
|
|
8
|
+
line = json.dumps(
|
|
9
|
+
{
|
|
10
|
+
"type": "system",
|
|
11
|
+
"subtype": "init",
|
|
12
|
+
"session_id": "abc-123",
|
|
13
|
+
"cwd": "/Users/test/project",
|
|
14
|
+
"model": "claude-sonnet-4-20250514",
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
result = parse_json_line(line, state)
|
|
18
|
+
output = result.get_output()
|
|
19
|
+
|
|
20
|
+
assert "[session]" in output
|
|
21
|
+
assert "abc-123" in output
|
|
22
|
+
assert "sonnet" in output
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_parse_tool_use_glob():
|
|
26
|
+
state = ParserState(mode="stream")
|
|
27
|
+
line = json.dumps(
|
|
28
|
+
{
|
|
29
|
+
"type": "assistant",
|
|
30
|
+
"message": {
|
|
31
|
+
"content": [
|
|
32
|
+
{
|
|
33
|
+
"type": "tool_use",
|
|
34
|
+
"name": "Glob",
|
|
35
|
+
"input": {"pattern": "*.py"},
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
result = parse_json_line(line, state)
|
|
42
|
+
output = result.get_output()
|
|
43
|
+
|
|
44
|
+
assert "[Glob]" in output
|
|
45
|
+
assert "*.py" in output
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_parse_tool_use_bash():
|
|
49
|
+
state = ParserState(mode="stream")
|
|
50
|
+
line = json.dumps(
|
|
51
|
+
{
|
|
52
|
+
"type": "assistant",
|
|
53
|
+
"message": {
|
|
54
|
+
"content": [
|
|
55
|
+
{
|
|
56
|
+
"type": "tool_use",
|
|
57
|
+
"name": "Bash",
|
|
58
|
+
"input": {"command": "echo hello"},
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
result = parse_json_line(line, state)
|
|
65
|
+
output = result.get_output()
|
|
66
|
+
|
|
67
|
+
assert "[Bash]" in output
|
|
68
|
+
assert "echo hello" in output
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_parse_todo_write():
|
|
72
|
+
state = ParserState(mode="stream")
|
|
73
|
+
line = json.dumps(
|
|
74
|
+
{
|
|
75
|
+
"type": "assistant",
|
|
76
|
+
"message": {
|
|
77
|
+
"content": [
|
|
78
|
+
{
|
|
79
|
+
"type": "tool_use",
|
|
80
|
+
"name": "TodoWrite",
|
|
81
|
+
"input": {
|
|
82
|
+
"todos": [
|
|
83
|
+
{"status": "completed", "content": "item one"},
|
|
84
|
+
{"status": "in_progress", "content": "item two"},
|
|
85
|
+
{"status": "pending", "content": "item three"},
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
result = parse_json_line(line, state)
|
|
94
|
+
output = result.get_output()
|
|
95
|
+
|
|
96
|
+
assert "[Todo]" in output
|
|
97
|
+
assert "item one" in output
|
|
98
|
+
assert "item two" in output
|
|
99
|
+
assert "item three" in output
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_parse_result():
|
|
103
|
+
state = ParserState(mode="stream")
|
|
104
|
+
line = json.dumps(
|
|
105
|
+
{
|
|
106
|
+
"type": "result",
|
|
107
|
+
"is_error": False,
|
|
108
|
+
"duration_ms": 5000,
|
|
109
|
+
"total_cost_usd": 0.05,
|
|
110
|
+
"num_turns": 3,
|
|
111
|
+
"usage": {
|
|
112
|
+
"input_tokens": 1000,
|
|
113
|
+
"output_tokens": 500,
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
result = parse_json_line(line, state)
|
|
118
|
+
output = result.get_output()
|
|
119
|
+
|
|
120
|
+
assert "[done]" in output
|
|
121
|
+
assert "5.0s" in output
|
|
122
|
+
assert "$0.0500" in output
|
|
123
|
+
assert "3 turns" in output
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_parse_task_increments_depth():
|
|
127
|
+
state = ParserState(mode="stream")
|
|
128
|
+
line = json.dumps(
|
|
129
|
+
{
|
|
130
|
+
"type": "assistant",
|
|
131
|
+
"message": {
|
|
132
|
+
"content": [
|
|
133
|
+
{
|
|
134
|
+
"type": "tool_use",
|
|
135
|
+
"name": "Task",
|
|
136
|
+
"input": {"prompt": "do something", "model": "sonnet"},
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
parse_json_line(line, state)
|
|
143
|
+
|
|
144
|
+
assert state.subagent_depth == 1
|
|
145
|
+
assert "│" in state.sp
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_replay_mode_shows_user_prompt():
|
|
149
|
+
state = ParserState(mode="replay")
|
|
150
|
+
line = json.dumps(
|
|
151
|
+
{
|
|
152
|
+
"type": "user",
|
|
153
|
+
"message": {
|
|
154
|
+
"content": "Hello, can you help me?",
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
result = parse_json_line(line, state)
|
|
159
|
+
output = result.get_output()
|
|
160
|
+
|
|
161
|
+
assert "[user]" in output
|
|
162
|
+
assert "Hello" in output
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_replay_mode_shows_assistant_text():
|
|
166
|
+
state = ParserState(mode="replay")
|
|
167
|
+
line = json.dumps(
|
|
168
|
+
{
|
|
169
|
+
"type": "assistant",
|
|
170
|
+
"message": {
|
|
171
|
+
"content": [
|
|
172
|
+
{"type": "text", "text": "Sure, I can help!"},
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
result = parse_json_line(line, state)
|
|
178
|
+
output = result.get_output()
|
|
179
|
+
|
|
180
|
+
assert "Sure, I can help!" in output
|