pisama-claude-code 0.3.4__tar.gz → 0.3.6__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.
Files changed (38) hide show
  1. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/CHANGELOG.md +9 -1
  2. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/PKG-INFO +5 -1
  3. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/README.md +4 -0
  4. pisama_claude_code-0.3.6/assets/demo.gif +0 -0
  5. pisama_claude_code-0.3.6/demo/demo.cast +153 -0
  6. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/pyproject.toml +1 -1
  7. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/src/pisama_claude_code/__init__.py +1 -1
  8. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/src/pisama_claude_code/cli.py +16 -2
  9. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/src/pisama_claude_code/hooks/capture_hook.py +91 -36
  10. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  11. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  12. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  13. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  14. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/.github/workflows/ci.yml +0 -0
  15. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/.github/workflows/publish.yml +0 -0
  16. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/.gitignore +0 -0
  17. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/CODE_OF_CONDUCT.md +0 -0
  18. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/CONTRIBUTING.md +0 -0
  19. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/LICENSE +0 -0
  20. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/SECURITY.md +0 -0
  21. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/demo/README.md +0 -0
  22. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/demo/demo-simulated.sh +0 -0
  23. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/demo/record-demo.sh +0 -0
  24. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/src/pisama_claude_code/adapter.py +0 -0
  25. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/src/pisama_claude_code/guardian.py +0 -0
  26. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/src/pisama_claude_code/hooks/__init__.py +0 -0
  27. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/src/pisama_claude_code/hooks/guardian_hook.py +0 -0
  28. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/src/pisama_claude_code/install.py +0 -0
  29. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/src/pisama_claude_code/py.typed +0 -0
  30. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/src/pisama_claude_code/storage.py +0 -0
  31. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/src/pisama_claude_code/trace_converter.py +0 -0
  32. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/src/pisama_claude_code/trace_types.py +0 -0
  33. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/tests/__init__.py +0 -0
  34. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/tests/conftest.py +0 -0
  35. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/tests/test_adapter.py +0 -0
  36. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/tests/test_cli.py +0 -0
  37. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/tests/test_guardian.py +0 -0
  38. {pisama_claude_code-0.3.4 → pisama_claude_code-0.3.6}/tests/test_storage.py +0 -0
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.5] - 2025-01-05
11
+
12
+ ### Fixed
13
+ - Correctly extract user input from transcript (skip tool_result blocks)
14
+ - Collect reasoning/output from multiple assistant messages (Claude Code splits them)
15
+ - Look back 200 lines to find user prompts
16
+
10
17
  ## [0.3.4] - 2025-01-05
11
18
 
12
19
  ### Fixed
@@ -84,7 +91,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
84
91
  - Support for Bash, Read, Write, Edit, Grep, Glob tools
85
92
  - Local storage in `~/.claude/pisama/traces/`
86
93
 
87
- [Unreleased]: https://github.com/tn-pisama/pisama-claude-code/compare/v0.3.4...HEAD
94
+ [Unreleased]: https://github.com/tn-pisama/pisama-claude-code/compare/v0.3.5...HEAD
95
+ [0.3.5]: https://github.com/tn-pisama/pisama-claude-code/compare/v0.3.4...v0.3.5
88
96
  [0.3.4]: https://github.com/tn-pisama/pisama-claude-code/compare/v0.3.3...v0.3.4
89
97
  [0.3.3]: https://github.com/tn-pisama/pisama-claude-code/compare/v0.3.2...v0.3.3
90
98
  [0.3.2]: https://github.com/tn-pisama/pisama-claude-code/compare/v0.3.1...v0.3.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pisama-claude-code
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: Trace capture for Claude Code sessions - sync to PISAMA platform for analysis
5
5
  Project-URL: Homepage, https://pisama.dev
6
6
  Project-URL: Documentation, https://pisama.dev/docs/claude-code
@@ -57,6 +57,10 @@ Description-Content-Type: text/markdown
57
57
  [![Downloads/month](https://img.shields.io/pypi/dm/pisama-claude-code)](https://pypistats.org/packages/pisama-claude-code)
58
58
  [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
59
59
 
60
+ ## Demo
61
+
62
+ ![pisama-claude-code demo](assets/demo.gif)
63
+
60
64
  ## Why PISAMA?
61
65
 
62
66
  When working with Claude Code, have you ever wondered:
@@ -10,6 +10,10 @@
10
10
  [![Downloads/month](https://img.shields.io/pypi/dm/pisama-claude-code)](https://pypistats.org/packages/pisama-claude-code)
11
11
  [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
12
12
 
13
+ ## Demo
14
+
15
+ ![pisama-claude-code demo](assets/demo.gif)
16
+
13
17
  ## Why PISAMA?
14
18
 
15
19
  When working with Claude Code, have you ever wondered:
Binary file
@@ -0,0 +1,153 @@
1
+ {"version":3,"term":{"cols":80,"rows":24,"type":"xterm-256color"},"timestamp":1767597795,"command":"./demo/demo-simulated.sh","env":{"SHELL":"/bin/zsh"}}
2
+ [0.011, "o", "\u001b[3J\u001b[H\u001b[2J"]
3
+ [0.000, "o", "\u001b[1;36m"]
4
+ [0.003, "o", "╔═══════════════════════════════════════════════════╗\r\n║ pisama-claude-code ║\r\n║ Track token costs for Claude Code sessions ║\r\n╚═══════════════════════════════════════════════════╝\r\n"]
5
+ [0.000, "o", "\u001b[0m\r\n"]
6
+ [2.004, "o", "\u001b[1;33m# Install\u001b[0m\r\n"]
7
+ [0.000, "o", "\u001b[0;32m$\u001b[0m "]
8
+ [0.000, "o", "p"]
9
+ [0.048, "o", "i"]
10
+ [0.046, "o", "p"]
11
+ [0.046, "o", " "]
12
+ [0.043, "o", "i"]
13
+ [0.043, "o", "n"]
14
+ [0.048, "o", "s"]
15
+ [0.043, "o", "t"]
16
+ [0.045, "o", "a"]
17
+ [0.047, "o", "l"]
18
+ [0.047, "o", "l"]
19
+ [0.046, "o", " "]
20
+ [0.048, "o", "p"]
21
+ [0.046, "o", "i"]
22
+ [0.047, "o", "s"]
23
+ [0.047, "o", "a"]
24
+ [0.043, "o", "m"]
25
+ [0.044, "o", "a"]
26
+ [0.046, "o", "-"]
27
+ [0.049, "o", "c"]
28
+ [0.045, "o", "l"]
29
+ [0.047, "o", "a"]
30
+ [0.048, "o", "u"]
31
+ [0.043, "o", "d"]
32
+ [0.048, "o", "e"]
33
+ [0.047, "o", "-"]
34
+ [0.045, "o", "c"]
35
+ [0.047, "o", "o"]
36
+ [0.048, "o", "d"]
37
+ [0.044, "o", "e"]
38
+ [0.044, "o", "\r\n"]
39
+ [0.207, "o", "Successfully installed pisama-claude-code-0.3.2\r\n"]
40
+ [1.508, "o", "\r\n\u001b[1;33m# Set up hooks\u001b[0m\r\n\u001b[0;32m$\u001b[0m "]
41
+ [0.000, "o", "p"]
42
+ [0.047, "o", "i"]
43
+ [0.045, "o", "s"]
44
+ [0.046, "o", "a"]
45
+ [0.046, "o", "m"]
46
+ [0.045, "o", "a"]
47
+ [0.047, "o", "-"]
48
+ [0.043, "o", "c"]
49
+ [0.047, "o", "c"]
50
+ [0.047, "o", " "]
51
+ [0.048, "o", "i"]
52
+ [0.043, "o", "n"]
53
+ [0.046, "o", "s"]
54
+ [0.044, "o", "t"]
55
+ [0.046, "o", "a"]
56
+ [0.047, "o", "l"]
57
+ [0.045, "o", "l"]
58
+ [0.047, "o", "\r\n"]
59
+ [0.209, "o", "✅ Installed pisama-capture.py\r\n✅ Installed pisama-pre.sh\r\n✅ Installed pisama-post.sh\r\n✅ Updated ~/.claude/settings.local.json\r\n\r\nHooks installed! Traces will be captured automatically.\r\n"]
60
+ [2.005, "o", "\r\n\u001b[1;33m# After a coding session, check status\u001b[0m\r\n\u001b[0;32m$\u001b[0m "]
61
+ [0.000, "o", "p"]
62
+ [0.045, "o", "i"]
63
+ [0.047, "o", "s"]
64
+ [0.046, "o", "a"]
65
+ [0.048, "o", "m"]
66
+ [0.045, "o", "a"]
67
+ [0.045, "o", "-"]
68
+ [0.045, "o", "c"]
69
+ [0.048, "o", "c"]
70
+ [0.048, "o", " "]
71
+ [0.048, "o", "s"]
72
+ [0.043, "o", "t"]
73
+ [0.044, "o", "a"]
74
+ [0.049, "o", "t"]
75
+ [0.047, "o", "u"]
76
+ [0.045, "o", "s"]
77
+ [0.043, "o", "\r\n"]
78
+ [0.211, "o", "📊 PISAMA Status\r\n========================================\r\n\r\n🔧 Hook Installation:\r\n ✅ pisama-capture.py\r\n ✅ pisama-pre.sh\r\n ✅ pisama-post.sh\r\n All hooks installed\r\n\r\n🔗 Platform Connection:\r\n ❌ Not connected (local-only mode)\r\n\r\n📁 Local Traces: 1,847\r\n Input tokens: 142,580\r\n Output tokens: 891,234\r\n Total cost: $52.34\r\n Models: claude-opus-4-5-20251101\r\n\r\n📝 Claude Code Settings:\r\n ✅ PISAMA hooks configured in settings\r\n"]
79
+ [2.509, "o", "\r\n\u001b[1;33m# View recent tool calls\u001b[0m\r\n\u001b[0;32m$\u001b[0m "]
80
+ [0.000, "o", "p"]
81
+ [0.046, "o", "i"]
82
+ [0.043, "o", "s"]
83
+ [0.043, "o", "a"]
84
+ [0.043, "o", "m"]
85
+ [0.043, "o", "a"]
86
+ [0.044, "o", "-"]
87
+ [0.047, "o", "c"]
88
+ [0.046, "o", "c"]
89
+ [0.046, "o", " "]
90
+ [0.042, "o", "t"]
91
+ [0.043, "o", "r"]
92
+ [0.046, "o", "a"]
93
+ [0.047, "o", "c"]
94
+ [0.048, "o", "e"]
95
+ [0.045, "o", "s"]
96
+ [0.045, "o", " "]
97
+ [0.047, "o", "-"]
98
+ [0.047, "o", "v"]
99
+ [0.047, "o", " "]
100
+ [0.043, "o", "-"]
101
+ [0.045, "o", "-"]
102
+ [0.048, "o", "l"]
103
+ [0.047, "o", "a"]
104
+ [0.046, "o", "s"]
105
+ [0.046, "o", "t"]
106
+ [0.048, "o", " "]
107
+ [0.043, "o", "5"]
108
+ [0.045, "o", "\r\n"]
109
+ [0.207, "o", "📋 Recent Traces (5 shown)\r\n======================================================================\r\n2025-01-04T14:23:45 | Bash | a3f8c2d1 | post | 2341i 8923o | $0.7012\r\n2025-01-04T14:23:38 | Read | a3f8c2d1 | post | 1823i 2341o | $0.1892\r\n2025-01-04T14:23:31 | Edit | a3f8c2d1 | post | 3421i 12893o | $1.0234\r\n2025-01-04T14:23:22 | Grep | a3f8c2d1 | post | 892i 1234o | $0.1023\r\n2025-01-04T14:23:15 | Glob | a3f8c2d1 | post | 423i 891o | $0.0712\r\n──────────────────────────────────────────────────────────────────────\r\nTotals: 8,900 input + 26,282 output tokens = $2.0873\r\n"]
110
+ [2.504, "o", "\r\n\u001b[1;33m# Detailed breakdown\u001b[0m\r\n"]
111
+ [0.000, "o", "\u001b[0;32m$\u001b[0m "]
112
+ [0.000, "o", "p"]
113
+ [0.045, "o", "i"]
114
+ [0.043, "o", "s"]
115
+ [0.045, "o", "a"]
116
+ [0.044, "o", "m"]
117
+ [0.045, "o", "a"]
118
+ [0.048, "o", "-"]
119
+ [0.044, "o", "c"]
120
+ [0.047, "o", "c"]
121
+ [0.043, "o", " "]
122
+ [0.043, "o", "u"]
123
+ [0.043, "o", "s"]
124
+ [0.047, "o", "a"]
125
+ [0.048, "o", "g"]
126
+ [0.046, "o", "e"]
127
+ [0.042, "o", " "]
128
+ [0.044, "o", "-"]
129
+ [0.043, "o", "-"]
130
+ [0.046, "o", "b"]
131
+ [0.046, "o", "y"]
132
+ [0.045, "o", "-"]
133
+ [0.046, "o", "m"]
134
+ [0.046, "o", "o"]
135
+ [0.046, "o", "d"]
136
+ [0.047, "o", "e"]
137
+ [0.044, "o", "l"]
138
+ [0.048, "o", " "]
139
+ [0.044, "o", "-"]
140
+ [0.048, "o", "-"]
141
+ [0.045, "o", "b"]
142
+ [0.045, "o", "y"]
143
+ [0.044, "o", "-"]
144
+ [0.048, "o", "t"]
145
+ [0.042, "o", "o"]
146
+ [0.047, "o", "o"]
147
+ [0.046, "o", "l"]
148
+ [0.046, "o", "\r\n"]
149
+ [0.210, "o", "📊 Token Usage Summary (last 100 traces)\r\n==================================================\r\nInput tokens: 142,580\r\nOutput tokens: 891,234\r\nCache read tokens: 1,234,567\r\nTotal tokens: 1,033,814\r\nTotal cost: $ 52.34\r\n\r\n📈 By Model:\r\n--------------------------------------------------\r\n claude-opus-4-5-20251101 $52.34\r\n\r\n🔧 By Tool:\r\n--------------------------------------------------\r\n Bash 145 calls $23.45\r\n Read 89 calls $12.34\r\n Edit 67 calls $8.92\r\n Write 34 calls $4.21\r\n Grep 23 calls $2.12\r\n Glob 12 calls $1.30\r\n"]
150
+ [2.002, "o", "\r\n\u001b[1;36m"]
151
+ [0.003, "o", "════════════════════════════════════════════════════\r\n ✓ Track every token. Know your costs.\r\n\r\n pip install pisama-claude-code\r\n════════════════════════════════════════════════════\r\n"]
152
+ [0.000, "o", "\u001b[0m"]
153
+ [3.008, "x", "0"]
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pisama-claude-code"
7
- version = "0.3.4"
7
+ version = "0.3.6"
8
8
  description = "Trace capture for Claude Code sessions - sync to PISAMA platform for analysis"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  """PISAMA Claude Code Integration - Trace capture, failure detection, and self-healing."""
2
2
 
3
- __version__ = "0.3.4"
3
+ __version__ = "0.3.5"
4
4
 
5
5
  # Lazy imports to avoid loading everything at startup
6
6
  def install(force: bool = False):
@@ -53,12 +53,15 @@ def save_config(config: dict):
53
53
 
54
54
 
55
55
  @click.group()
56
- @click.version_option(version="0.3.4")
56
+ @click.version_option(version="0.3.6")
57
57
  def main():
58
58
  """PISAMA Claude Code - Trace capture and sync."""
59
59
  pass
60
60
 
61
61
 
62
+ GITHUB_URL = "https://github.com/tn-pisama/pisama-claude-code"
63
+
64
+
62
65
  @main.command()
63
66
  @click.option("--force", "-f", is_flag=True, help="Overwrite existing hooks")
64
67
  def install(force: bool):
@@ -66,6 +69,13 @@ def install(force: bool):
66
69
  from pisama_claude_code.install import install as do_install
67
70
  do_install(force=force)
68
71
 
72
+ # Star reminder after install
73
+ click.echo("")
74
+ click.echo("=" * 50)
75
+ click.echo("If pisama-claude-code is useful, please star us!")
76
+ click.echo(f" {GITHUB_URL}")
77
+ click.echo("=" * 50)
78
+
69
79
 
70
80
  @main.command()
71
81
  def uninstall():
@@ -439,6 +449,10 @@ def status():
439
449
  else:
440
450
  click.echo(" ❌ No settings file found")
441
451
 
452
+ # Star reminder
453
+ click.echo("")
454
+ click.echo(f"Star us on GitHub: {GITHUB_URL}")
455
+
442
456
 
443
457
  @main.command()
444
458
  @click.option("--last", default=100, help="Number of traces to analyze")
@@ -675,7 +689,7 @@ def prepare_sync_payload(traces: list, include_outputs: bool) -> dict:
675
689
 
676
690
  return {
677
691
  "source": "claude-code",
678
- "version": "0.3.4",
692
+ "version": "0.3.5",
679
693
  "uploaded_at": datetime.now(timezone.utc).isoformat(),
680
694
  "trace_count": len(clean_traces),
681
695
  "traces": clean_traces,
@@ -185,7 +185,14 @@ def extract_content_parts(content_blocks: list) -> dict:
185
185
 
186
186
 
187
187
  def get_last_user_message(transcript_path: str) -> dict:
188
- """Read transcript and get the last user message (input)."""
188
+ """Read transcript and get the last user text message (input).
189
+
190
+ Note: Claude Code "user" messages can be either:
191
+ 1. Actual user text input (content is a string or has type="text" blocks)
192
+ 2. Tool results (content has type="tool_result" blocks)
193
+
194
+ We want the actual user input, not tool results.
195
+ """
189
196
  try:
190
197
  from pathlib import Path
191
198
  transcript = Path(transcript_path)
@@ -193,29 +200,47 @@ def get_last_user_message(transcript_path: str) -> dict:
193
200
  return {}
194
201
 
195
202
  with open(transcript) as f:
196
- lines = f.readlines()[-50:] # Look back further for user message
203
+ lines = f.readlines()[-200:] # Look back further for user message
197
204
 
198
205
  for line in reversed(lines):
199
206
  try:
200
207
  entry = json.loads(line)
201
- if entry.get("type") == "human" and "message" in entry:
208
+ # Claude Code uses "user" type
209
+ if entry.get("type") in ("user", "human") and "message" in entry:
202
210
  msg = entry["message"]
203
211
  content = msg.get("content", [])
204
212
 
205
- # Extract text from content blocks
206
- if isinstance(content, list):
213
+ # Content can be a direct string (actual user input)
214
+ if isinstance(content, str):
215
+ # Skip system messages and interrupts
216
+ if not content.startswith("[Request") and not content.startswith("[System"):
217
+ return {"content": content, "role": "user"}
218
+
219
+ # Content can be a list of blocks
220
+ elif isinstance(content, list):
207
221
  text_parts = []
222
+ is_tool_result = False
223
+
208
224
  for block in content:
209
- if isinstance(block, dict) and block.get("type") == "text":
210
- text_parts.append(block.get("text", ""))
225
+ if isinstance(block, dict):
226
+ block_type = block.get("type", "")
227
+ if block_type == "tool_result":
228
+ is_tool_result = True
229
+ break # This is a tool result, not user input
230
+ elif block_type == "text":
231
+ text = block.get("text", "")
232
+ if text and not text.startswith("[Request"):
233
+ text_parts.append(text)
211
234
  elif isinstance(block, str):
212
235
  text_parts.append(block)
213
- return {
214
- "content": "\n".join(text_parts) if text_parts else None,
215
- "role": "user",
216
- }
217
- elif isinstance(content, str):
218
- return {"content": content, "role": "user"}
236
+
237
+ # Only return if this is actual user text, not tool results
238
+ if text_parts and not is_tool_result:
239
+ return {
240
+ "content": "\n".join(text_parts),
241
+ "role": "user",
242
+ }
243
+
219
244
  except json.JSONDecodeError:
220
245
  continue
221
246
  return {}
@@ -224,14 +249,17 @@ def get_last_user_message(transcript_path: str) -> dict:
224
249
 
225
250
 
226
251
  def get_last_assistant_message(transcript_path: str) -> dict:
227
- """Read transcript and get the last assistant message with usage.
252
+ """Read transcript and get recent assistant messages with usage.
253
+
254
+ Claude Code separates thinking, text, and tool_use into different messages,
255
+ so we need to collect content from multiple recent assistant messages.
228
256
 
229
- Now extracts:
257
+ Extracts:
230
258
  - model: The Claude model used
231
259
  - usage: Token counts (input, output, cache)
232
260
  - input: User's input message
233
- - reasoning: Extended thinking content
234
- - output: Text response content
261
+ - reasoning: Extended thinking content (collected from recent messages)
262
+ - output: Text response content (collected from recent messages)
235
263
  - tool_calls: Any tool calls made
236
264
  """
237
265
  try:
@@ -240,45 +268,72 @@ def get_last_assistant_message(transcript_path: str) -> dict:
240
268
  if not transcript.exists():
241
269
  return {}
242
270
 
243
- # Read last 50 lines to find messages
271
+ # Read last 100 lines to find messages
244
272
  with open(transcript) as f:
245
- lines = f.readlines()[-50:]
273
+ lines = f.readlines()[-100:]
274
+
275
+ # Collect content from recent assistant messages (up to 10)
276
+ # Claude Code sends thinking, text, tool_use as separate messages
277
+ all_reasoning = []
278
+ all_output = []
279
+ all_tool_calls = []
280
+ model = None
281
+ usage = {}
282
+ stop_reason = None
283
+ messages_checked = 0
284
+ max_messages = 10
246
285
 
247
- assistant_msg = None
248
286
  for line in reversed(lines):
287
+ if messages_checked >= max_messages:
288
+ break
249
289
  try:
250
290
  entry = json.loads(line)
251
291
  if entry.get("type") == "assistant" and "message" in entry:
292
+ messages_checked += 1
252
293
  msg = entry["message"]
253
- assistant_msg = msg
294
+
295
+ # Get model and usage from first (most recent) message
296
+ if model is None:
297
+ model = msg.get("model")
298
+ usage = msg.get("usage", {})
299
+ stop_reason = msg.get("stop_reason")
300
+
301
+ # Extract content from this message
302
+ content = msg.get("content", [])
303
+ if isinstance(content, list):
304
+ parts = extract_content_parts(content)
305
+ if parts.get("reasoning", {}).get("content"):
306
+ all_reasoning.append(parts["reasoning"]["content"])
307
+ if parts.get("output", {}).get("content"):
308
+ all_output.append(parts["output"]["content"])
309
+ if parts.get("tool_calls"):
310
+ all_tool_calls.extend(parts["tool_calls"])
311
+
312
+ # Stop if we hit a user message (different turn)
313
+ elif entry.get("type") == "human":
254
314
  break
315
+
255
316
  except json.JSONDecodeError:
256
317
  continue
257
318
 
258
- if not assistant_msg:
259
- return {}
260
-
261
- # Extract content parts (reasoning, output, tool_calls)
262
- content = assistant_msg.get("content", [])
263
- content_parts = extract_content_parts(content) if isinstance(content, list) else {}
264
-
265
319
  # Get user input
266
320
  user_msg = get_last_user_message(transcript_path)
267
321
 
268
- # Get usage breakdown
269
- usage = assistant_msg.get("usage", {})
322
+ # Combine collected content (reverse to get chronological order)
323
+ reasoning_text = "\n\n".join(reversed(all_reasoning)) if all_reasoning else None
324
+ output_text = "\n\n".join(reversed(all_output)) if all_output else None
270
325
 
271
326
  return {
272
- "model": assistant_msg.get("model"),
327
+ "model": model,
273
328
  "usage": usage,
274
- "stop_reason": assistant_msg.get("stop_reason"),
329
+ "stop_reason": stop_reason,
275
330
  # Structured content
276
331
  "input": user_msg.get("content"),
277
- "reasoning": content_parts.get("reasoning", {}).get("content"),
278
- "output": content_parts.get("output", {}).get("content"),
279
- "tool_calls": content_parts.get("tool_calls"),
332
+ "reasoning": reasoning_text,
333
+ "output": output_text,
334
+ "tool_calls": all_tool_calls if all_tool_calls else None,
280
335
  # Legacy field for backward compatibility
281
- "content": content,
336
+ "content": [],
282
337
  }
283
338
  except Exception:
284
339
  return {}