cc-transcript 14.9.0__tar.gz → 14.10.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.
Files changed (160) hide show
  1. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/PKG-INFO +9 -1
  2. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/README.md +8 -0
  3. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/__init__.py +1 -0
  4. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/__init__.pyi +2 -0
  5. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/_native.pyi +22 -1
  6. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/facts.py +7 -0
  7. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/models.py +2 -0
  8. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/render.py +1 -1
  9. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/pyproject.toml +1 -1
  10. cc_transcript-14.10.0/rust/crates/cli/src/commands/blame.rs +234 -0
  11. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/cli/src/commands/facts.rs +29 -4
  12. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/cli/src/commands/grep.rs +44 -10
  13. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/cli/src/commands/mod.rs +1 -0
  14. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/cli/src/commands/show.rs +10 -2
  15. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/cli/src/commands/slice.rs +5 -32
  16. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/cli/src/commands/stats.rs +33 -6
  17. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/cli/src/lib.rs +97 -3
  18. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/cli/src/target.rs +8 -0
  19. cc_transcript-14.10.0/rust/crates/cli/src/timearg.rs +208 -0
  20. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/activity.rs +4 -1
  21. cc_transcript-14.10.0/rust/crates/core/src/blame.rs +809 -0
  22. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/command.rs +53 -18
  23. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/discovery.rs +21 -0
  24. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/facts.rs +4 -0
  25. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/lib.rs +1 -0
  26. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/parse.rs +12 -6
  27. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/render.rs +336 -38
  28. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/types.rs +8 -0
  29. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/data/command_prefix_pins.tsv +4 -4
  30. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/bin/stub_gen.rs +10 -0
  31. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/python.rs +2 -0
  32. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/views/attachment.rs +81 -3
  33. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/views/mod.rs +1 -0
  34. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/Cargo.lock +0 -0
  35. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/Cargo.toml +0 -0
  36. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/LICENSE +0 -0
  37. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/activity.py +0 -0
  38. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/activity_probe.py +0 -0
  39. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/builders.py +0 -0
  40. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/codex.py +0 -0
  41. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/command.py +0 -0
  42. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/context.py +0 -0
  43. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/corrections.py +0 -0
  44. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/cost.py +0 -0
  45. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/decisions.py +0 -0
  46. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/discovery.py +0 -0
  47. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/disktruth.py +0 -0
  48. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/evidence.py +0 -0
  49. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/extract/__init__.py +0 -0
  50. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/extract/correct.py +0 -0
  51. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/filterspec.py +0 -0
  52. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/heartbeats.py +0 -0
  53. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/ids.py +0 -0
  54. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/judge/__init__.py +0 -0
  55. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/judge/llm.py +0 -0
  56. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/judge/similar.py +0 -0
  57. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/judge/verdicts.py +0 -0
  58. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/ledger.py +0 -0
  59. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/literals.py +0 -0
  60. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/mining/__init__.py +0 -0
  61. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/mining/candidates.py +0 -0
  62. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/mining/confidence.py +0 -0
  63. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/mining/engine.py +0 -0
  64. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/mining/formats.py +0 -0
  65. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/mining/sampling.py +0 -0
  66. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/mining/signals.py +0 -0
  67. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/mining/sourcekind.py +0 -0
  68. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/mining/spec.py +0 -0
  69. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/mining/store.py +0 -0
  70. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/nlp.py +0 -0
  71. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/notifications.py +0 -0
  72. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/parser.py +0 -0
  73. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/py.typed +0 -0
  74. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/query.py +0 -0
  75. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/sentiment/__init__.py +0 -0
  76. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/sentiment/buckets.py +0 -0
  77. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/sentiment/data/afinn-en-165.tsv +0 -0
  78. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/sentiment/data/domain_overrides.tsv +0 -0
  79. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/sentiment/data/en-ewt.udpipe +0 -0
  80. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/sentiment/engine.py +0 -0
  81. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/sentiment/lexicon.py +0 -0
  82. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/sentiment/scorespec.py +0 -0
  83. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/synthetic.py +0 -0
  84. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/tools.py +0 -0
  85. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/cc_transcript/watch.py +0 -0
  86. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/cli/Cargo.toml +0 -0
  87. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/cli/src/commands/corrections.rs +0 -0
  88. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/cli/src/commands/digest.rs +0 -0
  89. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/cli/src/commands/list.rs +0 -0
  90. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/cli/src/commands/scratchpad.rs +0 -0
  91. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/cli/src/commands/watch.rs +0 -0
  92. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/cli/src/main.rs +0 -0
  93. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/cli/src/output.rs +0 -0
  94. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/Cargo.toml +0 -0
  95. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/benches/parse.rs +0 -0
  96. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/benches/render.rs +0 -0
  97. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/benches/toolcall.rs +0 -0
  98. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/actor.rs +0 -0
  99. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/buckets.rs +0 -0
  100. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/codex/discovery.rs +0 -0
  101. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/codex/lower.rs +0 -0
  102. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/codex/mod.rs +0 -0
  103. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/codex/parse.rs +0 -0
  104. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/codex/probe.rs +0 -0
  105. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/codex/protocol.rs +0 -0
  106. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/codex/types.rs +0 -0
  107. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/context.rs +0 -0
  108. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/corrections.rs +0 -0
  109. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/cost.rs +0 -0
  110. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/feedback.rs +0 -0
  111. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/filter.rs +0 -0
  112. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/gateway.rs +0 -0
  113. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/ids.rs +0 -0
  114. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/judge.rs +0 -0
  115. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/literals/command.rs +0 -0
  116. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/literals/corrections.rs +0 -0
  117. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/literals/feedback.rs +0 -0
  118. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/literals/mining.rs +0 -0
  119. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/literals/mod.rs +0 -0
  120. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/literals/protocol.rs +0 -0
  121. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/notifications.rs +0 -0
  122. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/protocol.rs +0 -0
  123. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/pystr.rs +0 -0
  124. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/query.rs +0 -0
  125. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/rng.rs +0 -0
  126. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/sqlite.rs +0 -0
  127. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/toolcall.rs +0 -0
  128. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/value.rs +0 -0
  129. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/core/src/watch.rs +0 -0
  130. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/Cargo.toml +0 -0
  131. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/build.rs +0 -0
  132. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/actor_bridge.rs +0 -0
  133. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/codex.rs +0 -0
  134. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/command.rs +0 -0
  135. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/context.rs +0 -0
  136. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/corrections.rs +0 -0
  137. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/discovery.rs +0 -0
  138. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/feedback.rs +0 -0
  139. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/judge.rs +0 -0
  140. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/lexicon.rs +0 -0
  141. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/lib.rs +0 -0
  142. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/mining.rs +0 -0
  143. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/nlp.rs +0 -0
  144. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/render.rs +0 -0
  145. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/score.rs +0 -0
  146. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/sqlite.rs +0 -0
  147. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/toolcall.rs +0 -0
  148. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/views/blocks.rs +0 -0
  149. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/views/command.rs +0 -0
  150. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/views/convert.rs +0 -0
  151. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/views/dunder.rs +0 -0
  152. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/views/events.rs +0 -0
  153. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/views/meta.rs +0 -0
  154. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/views/print.rs +0 -0
  155. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/views/store.rs +0 -0
  156. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/views/system.rs +0 -0
  157. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/views/toolcall.rs +0 -0
  158. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/views/toolresult.rs +0 -0
  159. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/views/transcript.rs +0 -0
  160. {cc_transcript-14.9.0 → cc_transcript-14.10.0}/rust/crates/py/src/watch.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cc-transcript
3
- Version: 14.9.0
3
+ Version: 14.10.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -120,6 +120,14 @@ print(len(transcript.events), "events after the noise drop")
120
120
 
121
121
  `discover()` lists every transcript on disk newest first, `stream` fans a whole corpus across the parse pool, and `resolve` finds one session by UUID. `parse` also reads OpenAI Codex CLI rollouts — hand it a path from `~/.codex/sessions` and the same typed events come back. The [getting-started guide](https://yasyf.github.io/cc-transcript/docs/getting-started/index.html) builds this out to a composed filter and a sentiment score.
122
122
 
123
+ ## JSON output schema
124
+
125
+ `show --json` and `grep --json` emit JSONL, with one object per event. Each event
126
+ uses the envelope `{i, kind, meta, model, text, blocks, stop_reason, usage}`;
127
+ fields that do not apply to its kind are omitted. Every object in `blocks` has
128
+ a `type` discriminator such as `text`, `thinking`, `tool_use`, `tool_result`,
129
+ or `fallback`.
130
+
123
131
  ## More in the docs
124
132
 
125
133
  - [The Python library](https://yasyf.github.io/cc-transcript/docs/getting-started/index.html) turns a transcript into typed lazy views, a composed filter, and a score in a dozen lines.
@@ -84,6 +84,14 @@ print(len(transcript.events), "events after the noise drop")
84
84
 
85
85
  `discover()` lists every transcript on disk newest first, `stream` fans a whole corpus across the parse pool, and `resolve` finds one session by UUID. `parse` also reads OpenAI Codex CLI rollouts — hand it a path from `~/.codex/sessions` and the same typed events come back. The [getting-started guide](https://yasyf.github.io/cc-transcript/docs/getting-started/index.html) builds this out to a composed filter and a sentiment score.
86
86
 
87
+ ## JSON output schema
88
+
89
+ `show --json` and `grep --json` emit JSONL, with one object per event. Each event
90
+ uses the envelope `{i, kind, meta, model, text, blocks, stop_reason, usage}`;
91
+ fields that do not apply to its kind are omitted. Every object in `blocks` has
92
+ a `type` discriminator such as `text`, `thinking`, `tool_use`, `tool_result`,
93
+ or `fallback`.
94
+
87
95
  ## More in the docs
88
96
 
89
97
  - [The Python library](https://yasyf.github.io/cc-transcript/docs/getting-started/index.html) turns a transcript into typed lazy views, a composed filter, and a score in a dozen lines.
@@ -109,6 +109,7 @@ EXPORTS: dict[str, str] = {
109
109
  "CcVersion",
110
110
  "CompactBoundary",
111
111
  "ContentBlock",
112
+ "DeferredToolsDelta",
112
113
  "EntryMeta",
113
114
  "EventList",
114
115
  "FallbackBlock",
@@ -147,6 +147,7 @@ from cc_transcript.models import (
147
147
  CcVersion as CcVersion,
148
148
  CompactBoundary as CompactBoundary,
149
149
  ContentBlock as ContentBlock,
150
+ DeferredToolsDelta as DeferredToolsDelta,
150
151
  EntryMeta as EntryMeta,
151
152
  EventList as EventList,
152
153
  FallbackBlock as FallbackBlock,
@@ -339,6 +340,7 @@ __all__ = [
339
340
  "DecisionLog",
340
341
  "DeepSession",
341
342
  "DeepView",
343
+ "DeferredToolsDelta",
342
344
  "DiskTruth",
343
345
  "Edit",
344
346
  "EditCall",
@@ -28,6 +28,7 @@ __all__ = [
28
28
  "CommandLine",
29
29
  "CommandLineQuery",
30
30
  "CompactBoundary",
31
+ "DeferredToolsDelta",
31
32
  "EditCall",
32
33
  "EditResult",
33
34
  "EditSpan",
@@ -588,6 +589,27 @@ class CompactBoundary:
588
589
  @property
589
590
  def precomputed(self) -> typing.Optional[builtins.bool]: ...
590
591
 
592
+ @typing.final
593
+ class DeferredToolsDelta(OtherAttachment):
594
+ r"""
595
+ Tools added to and removed from the deferred tool inventory.
596
+
597
+ Attributes:
598
+ added_count: Number of added tools.
599
+ removed_count: Number of removed tools.
600
+ added_names: Added tool names, in transcript order.
601
+ removed_names: Removed tool names, in transcript order.
602
+ """
603
+ __match_args__: typing.ClassVar[tuple[str, ...]]
604
+ @property
605
+ def added_count(self) -> builtins.int: ...
606
+ @property
607
+ def removed_count(self) -> builtins.int: ...
608
+ @property
609
+ def added_names(self) -> tuple[str, ...]: ...
610
+ @property
611
+ def removed_names(self) -> tuple[str, ...]: ...
612
+
591
613
  @typing.final
592
614
  class EditCall(ToolCallBase):
593
615
  r"""
@@ -1163,7 +1185,6 @@ class Occurrence:
1163
1185
  One shell-word spelling of ``text`` that survives every enclosing layer, or None.
1164
1186
  """
1165
1187
 
1166
- @typing.final
1167
1188
  class OtherAttachment:
1168
1189
  r"""
1169
1190
  Any attachment whose type has no typed detail, carried verbatim.
@@ -33,6 +33,7 @@ class ToolFact:
33
33
  ts: The timestamp of the assistant entry that made the call, or None.
34
34
  session_id: The Claude session the call belongs to.
35
35
  path: The transcript file the call was lifted from.
36
+ cwd: The working directory recorded on the originating event, or None.
36
37
  tool_use_id: The tool-use block id — the join key back to the event stream.
37
38
  tool: The tool name exactly as invoked.
38
39
  command_prefixes: The permission-style prefixes of each Bash command
@@ -42,6 +43,8 @@ class ToolFact:
42
43
  mcp_tool: The MCP tool segment for an ``mcp__server__tool`` call, else None.
43
44
  mcp_access: Whether the MCP tool reads or writes, else None.
44
45
  file_path: The file the call targets, when it targets one.
46
+ file_paths: Every file the call touches, apply_patch multi-file included;
47
+ ``file_path`` mirrors the first entry.
45
48
  is_error: Whether the call's result reported a failure.
46
49
  denied: Whether the result is a user rejection of the tool use.
47
50
  denial_kind: The structured tool-denial kind (``user-rejected`` for a human
@@ -53,6 +56,7 @@ class ToolFact:
53
56
  ts: datetime | None
54
57
  session_id: SessionId
55
58
  path: Path
59
+ cwd: str | None
56
60
  tool_use_id: ToolUseId
57
61
  tool: str
58
62
  command_prefixes: tuple[str, ...]
@@ -61,6 +65,7 @@ class ToolFact:
61
65
  mcp_tool: str | None
62
66
  mcp_access: Literal["read", "write"] | None
63
67
  file_path: str | None
68
+ file_paths: tuple[str, ...]
64
69
  is_error: bool
65
70
  denied: bool
66
71
  denial_kind: str | None
@@ -73,6 +78,7 @@ def rehydrate_fact(fact: Mapping[str, Any], path: Path) -> ToolFact:
73
78
  ts=datetime.fromisoformat(fact["ts"]),
74
79
  session_id=SessionId(fact["session_id"]),
75
80
  path=path,
81
+ cwd=fact["cwd"],
76
82
  tool_use_id=ToolUseId(fact["tool_use_id"]),
77
83
  tool=fact["tool"],
78
84
  command_prefixes=tuple(fact["command_prefixes"]),
@@ -81,6 +87,7 @@ def rehydrate_fact(fact: Mapping[str, Any], path: Path) -> ToolFact:
81
87
  mcp_tool=fact["mcp_tool"],
82
88
  mcp_access=fact["mcp_access"],
83
89
  file_path=fact["file_path"],
90
+ file_paths=tuple(fact["file_paths"]),
84
91
  is_error=fact["is_error"],
85
92
  denied=fact["denied"],
86
93
  denial_kind=fact["denial_kind"],
@@ -17,6 +17,7 @@ from cc_transcript._native import AttachmentEvent as AttachmentEvent
17
17
  from cc_transcript._native import Attribution as Attribution
18
18
  from cc_transcript._native import CacheCreation as CacheCreation
19
19
  from cc_transcript._native import CompactBoundary as CompactBoundary
20
+ from cc_transcript._native import DeferredToolsDelta as DeferredToolsDelta
20
21
  from cc_transcript._native import EntryMeta as EntryMeta
21
22
  from cc_transcript._native import EventList as EventList
22
23
  from cc_transcript._native import FallbackBlock as FallbackBlock
@@ -121,6 +122,7 @@ AttachmentDetail = (
121
122
  | HookAdditionalContext
122
123
  | AsyncHookResponse
123
124
  | QueuedCommand
125
+ | DeferredToolsDelta
124
126
  | OtherAttachment
125
127
  )
126
128
 
@@ -40,7 +40,7 @@ def render_tool_call(call: ToolCall | FallbackCall, *, budget: Budget) -> str:
40
40
  The single tool-input renderer: Edit renders the path plus ``- old`` /
41
41
  ``+ new`` lines, MultiEdit renders every span under an ``edit i/n``
42
42
  marker, Write renders the path plus content, Bash renders the command,
43
- and everything else renders a compact ``name(primary-arg)`` line.
43
+ and everything else renders the tool name with its full compact-JSON input.
44
44
 
45
45
  Example:
46
46
  >>> render_tool_call(parse_tool_call("Bash", {"command": "ls"}), budget=Budget())
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cc-transcript"
3
- version = "14.9.0" # informational — releases stamp the version from the git tag
3
+ version = "14.10.0" # informational — releases stamp the version from the git tag
4
4
  description = "Grep every Claude Code session you've ever run."
5
5
  readme = "README.md"
6
6
  license = "PolyForm-Noncommercial-1.0.0"
@@ -0,0 +1,234 @@
1
+ //! The file-centric `blame` and `attribute` verbs — native-only, with no click twin.
2
+ //! `blame` lists the sessions that wrote a working-tree file, newest first; `attribute`
3
+ //! classifies how the file came to exist. Both resolve the target's repo root from a
4
+ //! `.git`/`.jj` ancestor and scan only that repo's project dirs unless `--all-projects`.
5
+
6
+ use std::fs;
7
+ use std::path::{Path, PathBuf};
8
+
9
+ use cc_transcript_core::blame::{attribute as classify, blame as rank, session_writes, RepoPaths};
10
+ use cc_transcript_core::discovery::{find_in, mtime_secs, project_dir_name};
11
+ use cc_transcript_core::render::{attribute_json, attribute_lines, blame_json, blame_line};
12
+ use cc_transcript_core::types::Entry;
13
+ use chrono::{DateTime, FixedOffset};
14
+
15
+ use crate::output::{click_error, eline, py_repr, usage_error, CliExit, Out};
16
+ use crate::target::{claude_projects_dir, parse_transcripts, require_dir, Parsed};
17
+
18
+ const BLAME_USAGE: &str = "cc-transcript blame [OPTIONS] <PATH>";
19
+ const BLAME_HELP: &str = "cc-transcript blame";
20
+ const ATTRIBUTE_USAGE: &str = "cc-transcript attribute [OPTIONS] <PATH>";
21
+ const ATTRIBUTE_HELP: &str = "cc-transcript attribute";
22
+
23
+ pub struct BlameArgs {
24
+ pub path: PathBuf,
25
+ pub root: Option<PathBuf>,
26
+ pub all_projects: bool,
27
+ pub since: Option<String>,
28
+ pub until: Option<String>,
29
+ pub limit: Option<usize>,
30
+ pub json: bool,
31
+ }
32
+
33
+ pub struct AttributeArgs {
34
+ pub path: PathBuf,
35
+ pub root: Option<PathBuf>,
36
+ pub all_projects: bool,
37
+ pub json: bool,
38
+ }
39
+
40
+ /// A resolved target: the repo it belongs to, its repo-relative path, and its canonical
41
+ /// on-disk location.
42
+ struct Target {
43
+ repo: RepoPaths,
44
+ rel_target: String,
45
+ canonical: PathBuf,
46
+ }
47
+
48
+ /// The nearest ancestor directory carrying a `.git` or `.jj` entry (file or dir — a
49
+ /// worktree's `.git` is a file), starting from the target's own parent.
50
+ fn marker_root(canonical: &Path) -> Option<&Path> {
51
+ canonical
52
+ .ancestors()
53
+ .skip(1)
54
+ .find(|dir| dir.join(".git").exists() || dir.join(".jj").exists())
55
+ }
56
+
57
+ /// A worktree's marker root lifts to the main checkout: everything before the
58
+ /// `/.claude/worktrees/` segment. Any other marker root is the repo root as found.
59
+ fn repo_root(marker: &Path) -> String {
60
+ let text = marker.to_string_lossy();
61
+ let marker_segment = format!("/{}", cc_transcript_core::blame::WORKTREES_SEGMENT);
62
+ match text.find(&marker_segment) {
63
+ Some(cut) => text[..cut].to_string(),
64
+ None => text.into_owned(),
65
+ }
66
+ }
67
+
68
+ fn resolve_target(path: &Path, usage: &str, help_path: &str) -> Result<Target, CliExit> {
69
+ let invalid = |what: &str| {
70
+ usage_error(
71
+ usage,
72
+ help_path,
73
+ &format!(
74
+ "Invalid value for 'PATH': File {} {what}.",
75
+ py_repr(&path.to_string_lossy())
76
+ ),
77
+ )
78
+ };
79
+ let canonical = fs::canonicalize(path).map_err(|_| invalid("does not exist"))?;
80
+ if canonical.is_dir() {
81
+ return Err(invalid("is a directory"));
82
+ }
83
+ let marker = marker_root(&canonical).ok_or_else(|| {
84
+ click_error(&format!(
85
+ "{} is not inside a repository (no .git or .jj ancestor)",
86
+ path.display()
87
+ ))
88
+ })?;
89
+ let rel_target = canonical
90
+ .strip_prefix(marker)
91
+ .expect("canonical target sits under its marker root")
92
+ .to_string_lossy()
93
+ .into_owned();
94
+ Ok(Target {
95
+ repo: RepoPaths {
96
+ root: repo_root(marker),
97
+ },
98
+ rel_target,
99
+ canonical,
100
+ })
101
+ }
102
+
103
+ /// Parse every transcript under a project dir belonging to `repo_root` (or every dir
104
+ /// under `--all-projects`), dropping files older than `min_mtime` (blame's `--since`).
105
+ /// The `{encoded}-` prefix admits prefix-collided siblings (`<root>-rust`); the engine's
106
+ /// per-call cwd check drops their writes.
107
+ fn select_transcripts(
108
+ projects_root: &Path,
109
+ repo_root: &str,
110
+ all_projects: bool,
111
+ min_mtime: Option<f64>,
112
+ usage: &str,
113
+ help_path: &str,
114
+ ) -> Result<Vec<Parsed>, CliExit> {
115
+ require_dir(projects_root, "'--root'", usage, help_path)?;
116
+ let encoded = project_dir_name(repo_root);
117
+ let prefix = format!("{encoded}-");
118
+ let pairs: Vec<(PathBuf, f64)> = fs::read_dir(projects_root)
119
+ .into_iter()
120
+ .flatten()
121
+ .flatten()
122
+ .filter(|entry| {
123
+ let name = entry.file_name().to_string_lossy().into_owned();
124
+ all_projects || name == encoded || name.starts_with(&prefix)
125
+ })
126
+ .flat_map(|entry| find_in(&entry.path(), None, None, None))
127
+ .filter(|(_, mtime)| min_mtime.is_none_or(|floor| *mtime >= floor))
128
+ .collect();
129
+ Ok(parse_transcripts(&pairs))
130
+ }
131
+
132
+ /// A file mtime (`os.stat` seconds) as a UTC-offset datetime, for the attribute window.
133
+ fn mtime_datetime(mtime: f64) -> DateTime<FixedOffset> {
134
+ let secs = mtime.floor();
135
+ let nanos = ((mtime - secs) * 1e9) as u32;
136
+ DateTime::from_timestamp(secs as i64, nanos)
137
+ .expect("file mtime is in range")
138
+ .fixed_offset()
139
+ }
140
+
141
+ pub fn blame(args: BlameArgs) -> Result<(), CliExit> {
142
+ let target = resolve_target(&args.path, BLAME_USAGE, BLAME_HELP)?;
143
+ let now = chrono::Local::now();
144
+ let since = args
145
+ .since
146
+ .as_deref()
147
+ .map(|value| crate::timearg::parse_time("--since", value, now, BLAME_USAGE, BLAME_HELP))
148
+ .transpose()?;
149
+ let until = args
150
+ .until
151
+ .as_deref()
152
+ .map(|value| crate::timearg::parse_time("--until", value, now, BLAME_USAGE, BLAME_HELP))
153
+ .transpose()?;
154
+ let projects_root = args.root.unwrap_or_else(claude_projects_dir);
155
+ let parsed = select_transcripts(
156
+ &projects_root,
157
+ &target.repo.root,
158
+ args.all_projects,
159
+ since.map(|ts| ts.timestamp() as f64),
160
+ BLAME_USAGE,
161
+ BLAME_HELP,
162
+ )?;
163
+ let mut records = rank(
164
+ parsed
165
+ .iter()
166
+ .filter_map(|p| p.session_id.as_deref().map(|session| (session, p)))
167
+ .flat_map(|(session, p)| {
168
+ session_writes(
169
+ session,
170
+ &p.path.to_string_lossy(),
171
+ &p.entries,
172
+ &target.repo,
173
+ &target.rel_target,
174
+ since,
175
+ until,
176
+ )
177
+ })
178
+ .collect(),
179
+ );
180
+ if let Some(limit) = args.limit {
181
+ records.truncate(limit);
182
+ }
183
+ if records.is_empty() {
184
+ eline(&format!("no sessions wrote {}", target.rel_target));
185
+ return Err(CliExit(1));
186
+ }
187
+ let mut out = Out::new();
188
+ for record in &records {
189
+ out.line(&match args.json {
190
+ true => blame_json(record).dumps(),
191
+ false => blame_line(record),
192
+ })?;
193
+ }
194
+ out.finish()
195
+ }
196
+
197
+ pub fn attribute(args: AttributeArgs) -> Result<(), CliExit> {
198
+ let target = resolve_target(&args.path, ATTRIBUTE_USAGE, ATTRIBUTE_HELP)?;
199
+ let meta = fs::metadata(&target.canonical)
200
+ .map_err(|e| click_error(&format!("{}: {e}", args.path.display())))?;
201
+ let mtime = mtime_datetime(mtime_secs(&meta));
202
+ let projects_root = args.root.unwrap_or_else(claude_projects_dir);
203
+ let parsed = select_transcripts(
204
+ &projects_root,
205
+ &target.repo.root,
206
+ args.all_projects,
207
+ None,
208
+ ATTRIBUTE_USAGE,
209
+ ATTRIBUTE_HELP,
210
+ )?;
211
+ let owned: Vec<(&str, String, &[Entry])> = parsed
212
+ .iter()
213
+ .filter_map(|p| {
214
+ p.session_id.as_deref().map(|session| {
215
+ (
216
+ session,
217
+ p.path.to_string_lossy().into_owned(),
218
+ p.entries.as_slice(),
219
+ )
220
+ })
221
+ })
222
+ .collect();
223
+ let sessions: Vec<(&str, &str, &[Entry])> = owned
224
+ .iter()
225
+ .map(|(session, path, entries)| (*session, path.as_str(), *entries))
226
+ .collect();
227
+ let verdict = classify(&sessions, &target.repo, &target.rel_target, mtime);
228
+ let mut out = Out::new();
229
+ match args.json {
230
+ true => out.line(&attribute_json(&verdict, &target.rel_target, mtime).dumps())?,
231
+ false => out.lines(attribute_lines(&verdict, &target.rel_target))?,
232
+ }
233
+ out.finish()
234
+ }
@@ -3,6 +3,7 @@
3
3
  use std::path::PathBuf;
4
4
 
5
5
  use cc_transcript_core::facts::{command_prefix_counts, mcp_summary, tool_facts, ToolFact};
6
+ use cc_transcript_core::query::FileRef;
6
7
  use cc_transcript_core::render::{
7
8
  denial_json, denial_line, fact_json, fact_line, render_counts, render_mcp, Json,
8
9
  };
@@ -18,6 +19,7 @@ fn gather(
18
19
  paths: &[PathBuf],
19
20
  discovery: &DiscoveryOpts,
20
21
  name: &str,
22
+ min_mtime: Option<f64>,
21
23
  ) -> Result<(Targets, Vec<ToolFact>), CliExit> {
22
24
  let paths: Vec<PathBuf> = paths
23
25
  .iter()
@@ -32,6 +34,7 @@ fn gather(
32
34
  discovery.project.as_deref(),
33
35
  discovery.contains.as_deref(),
34
36
  discovery.effective_limit(),
37
+ min_mtime,
35
38
  )?;
36
39
  let facts: Vec<ToolFact> = parse_transcripts(&targets.paths)
37
40
  .iter()
@@ -54,12 +57,34 @@ pub fn tools(
54
57
  paths: &[PathBuf],
55
58
  discovery: &DiscoveryOpts,
56
59
  tool: Option<&str>,
60
+ file: Option<&str>,
61
+ since: Option<&str>,
62
+ until: Option<&str>,
57
63
  json: bool,
58
64
  ) -> Result<(), CliExit> {
59
- let (targets, facts) = gather(paths, discovery, "tools")?;
65
+ let usage = "cc-transcript tools [OPTIONS] [PATHS]...";
66
+ let help_path = "cc-transcript tools";
67
+ let now = chrono::Local::now();
68
+ let since_ts = since
69
+ .map(|value| crate::timearg::parse_time("--since", value, now, usage, help_path))
70
+ .transpose()?;
71
+ let until_ts = until
72
+ .map(|value| crate::timearg::parse_time("--until", value, now, usage, help_path))
73
+ .transpose()?;
74
+ let min_mtime = since_ts.map(|ts| ts.timestamp() as f64);
75
+ let (targets, facts) = gather(paths, discovery, "tools", min_mtime)?;
60
76
  let facts: Vec<&ToolFact> = facts
61
77
  .iter()
62
78
  .filter(|fact| tool.is_none_or(|t| tool_name_matches(&fact.tool, t)))
79
+ .filter(|fact| {
80
+ file.is_none_or(|glob| {
81
+ fact.file_paths
82
+ .iter()
83
+ .any(|p| FileRef::new(p).matches(&[glob]))
84
+ })
85
+ })
86
+ .filter(|fact| since_ts.is_none_or(|s| fact.ts >= s))
87
+ .filter(|fact| until_ts.is_none_or(|u| fact.ts < u))
63
88
  .collect();
64
89
  let mut out = Out::new();
65
90
  if json {
@@ -76,7 +101,7 @@ pub fn tools(
76
101
  }
77
102
 
78
103
  pub fn commands(paths: &[PathBuf], discovery: &DiscoveryOpts, json: bool) -> Result<(), CliExit> {
79
- let (targets, facts) = gather(paths, discovery, "commands")?;
104
+ let (targets, facts) = gather(paths, discovery, "commands", None)?;
80
105
  let counts = command_prefix_counts(facts.iter());
81
106
  let mut out = Out::new();
82
107
  if json {
@@ -103,7 +128,7 @@ pub fn permissions(
103
128
  discovery: &DiscoveryOpts,
104
129
  json: bool,
105
130
  ) -> Result<(), CliExit> {
106
- let (targets, facts) = gather(paths, discovery, "permissions")?;
131
+ let (targets, facts) = gather(paths, discovery, "permissions", None)?;
107
132
  let denials: Vec<&ToolFact> = facts
108
133
  .iter()
109
134
  .filter(|fact| fact.denied)
@@ -124,7 +149,7 @@ pub fn permissions(
124
149
  }
125
150
 
126
151
  pub fn mcp(paths: &[PathBuf], discovery: &DiscoveryOpts, json: bool) -> Result<(), CliExit> {
127
- let (targets, facts) = gather(paths, discovery, "mcp")?;
152
+ let (targets, facts) = gather(paths, discovery, "mcp", None)?;
128
153
  let summary = mcp_summary(facts.iter());
129
154
  let mut out = Out::new();
130
155
  if json {
@@ -5,7 +5,9 @@ use std::path::PathBuf;
5
5
 
6
6
  use cc_transcript_core::facts::{tool_facts, ToolFact};
7
7
  use cc_transcript_core::filter::event_kind;
8
- use cc_transcript_core::render::{compact_line, event_json, haystack, transcript_header, Json};
8
+ use cc_transcript_core::render::{
9
+ compact_line, event_json, haystack, tool_haystack, transcript_header, Json,
10
+ };
9
11
  use cc_transcript_core::toolcall::tool_name_matches;
10
12
  use cc_transcript_core::types::{ContentBlock, Entry};
11
13
  use regex::{Regex, RegexBuilder};
@@ -23,6 +25,7 @@ pub struct GrepArgs {
23
25
  pub discovery: DiscoveryOpts,
24
26
  pub kinds: Vec<String>,
25
27
  pub tool: Option<String>,
28
+ pub errors: bool,
26
29
  pub ignore_case: bool,
27
30
  pub wheres: Vec<String>,
28
31
  pub context: usize,
@@ -61,6 +64,28 @@ fn event_facts<'a>(event: &Entry, facts: &'a HashMap<String, ToolFact>) -> Vec<&
61
64
  }
62
65
  }
63
66
 
67
+ fn error_call_matches(
68
+ event: &Entry,
69
+ facts: &HashMap<String, ToolFact>,
70
+ regex: &Regex,
71
+ tool: Option<&str>,
72
+ where_tools: bool,
73
+ ) -> bool {
74
+ where_tools
75
+ && event.blocks().iter().any(|block| {
76
+ let fact = match block {
77
+ ContentBlock::ToolUse(tool_use) => facts.get(&tool_use.id),
78
+ ContentBlock::ToolResult(result) => facts.get(&result.tool_use_id),
79
+ _ => None,
80
+ };
81
+ fact.is_some_and(|fact| {
82
+ fact.is_error
83
+ && tool.is_none_or(|name| tool_name_matches(&fact.tool, name))
84
+ && regex.is_match(&tool_haystack(block))
85
+ })
86
+ })
87
+ }
88
+
64
89
  fn result_key(fact: &ToolFact) -> Json {
65
90
  Json::Obj(vec![
66
91
  ("is_error".into(), Json::Bool(fact.is_error)),
@@ -156,6 +181,7 @@ pub fn run(args: GrepArgs) -> Result<(), CliExit> {
156
181
  args.discovery.project.as_deref(),
157
182
  args.discovery.contains.as_deref(),
158
183
  args.discovery.effective_limit(),
184
+ None,
159
185
  )?;
160
186
  let mut out_lines: Vec<String> = Vec::new();
161
187
  let mut files_matched = 0usize;
@@ -166,6 +192,11 @@ pub fn run(args: GrepArgs) -> Result<(), CliExit> {
166
192
  break;
167
193
  }
168
194
  let names = tool_names(&parsed.entries);
195
+ let facts = if args.with_result || args.errors {
196
+ fact_index(&parsed)
197
+ } else {
198
+ HashMap::new()
199
+ };
169
200
  let hits: Vec<usize> = parsed
170
201
  .entries
171
202
  .iter()
@@ -174,22 +205,25 @@ pub fn run(args: GrepArgs) -> Result<(), CliExit> {
174
205
  args.kinds.is_empty() || args.kinds.iter().any(|k| k == event_kind(event))
175
206
  })
176
207
  .filter(|(_, event)| {
177
- args.tool
178
- .as_deref()
179
- .is_none_or(|tool| uses_tool(event, tool, &names))
208
+ args.errors
209
+ || args
210
+ .tool
211
+ .as_deref()
212
+ .is_none_or(|tool| uses_tool(event, tool, &names))
213
+ })
214
+ .filter(|(_, event)| {
215
+ if args.errors {
216
+ error_call_matches(event, &facts, &regex, args.tool.as_deref(), w_tools)
217
+ } else {
218
+ regex.is_match(&haystack(event, w_text, w_thinking, w_tools))
219
+ }
180
220
  })
181
- .filter(|(_, event)| regex.is_match(&haystack(event, w_text, w_thinking, w_tools)))
182
221
  .map(|(index, _)| index)
183
222
  .take(budget)
184
223
  .collect();
185
224
  if hits.is_empty() {
186
225
  continue;
187
226
  }
188
- let facts = if args.with_result {
189
- fact_index(&parsed)
190
- } else {
191
- HashMap::new()
192
- };
193
227
  files_matched += 1;
194
228
  matched += hits.len();
195
229
  budget -= hits.len();
@@ -1,3 +1,4 @@
1
+ pub mod blame;
1
2
  pub mod corrections;
2
3
  pub mod digest;
3
4
  pub mod facts;
@@ -6,7 +6,7 @@ use std::path::PathBuf;
6
6
 
7
7
  use cc_transcript_core::filter::{event_kind, spec_keep, CompiledSpec, NOISE_SPEC, SIGNAL_SPEC};
8
8
  use cc_transcript_core::render::{compact_line, event_json};
9
- use cc_transcript_core::types::Entry;
9
+ use cc_transcript_core::types::{ContentBlock, Entry};
10
10
 
11
11
  use crate::output::{eline, py_repr, usage_error, CliExit, Out};
12
12
  use crate::target::{parse_single, py_path, require_files, tool_names};
@@ -24,6 +24,7 @@ pub struct ShowArgs {
24
24
  pub kinds: Vec<String>,
25
25
  pub signal: bool,
26
26
  pub no_junk: bool,
27
+ pub errors: bool,
27
28
  pub thinking: bool,
28
29
  pub width: usize,
29
30
  pub uuids: bool,
@@ -60,12 +61,19 @@ pub fn filter_rows<'a>(
60
61
  entries: &'a [Entry],
61
62
  kinds: &[String],
62
63
  spec: Option<&CompiledSpec>,
64
+ errors: bool,
63
65
  ) -> Vec<(usize, &'a Entry)> {
64
66
  entries
65
67
  .iter()
66
68
  .enumerate()
67
69
  .filter(|(_, event)| kinds.is_empty() || kinds.iter().any(|k| k == event_kind(event)))
68
70
  .filter(|(_, event)| spec.is_none_or(|s| spec_keep(s, event)))
71
+ .filter(|(_, event)| {
72
+ !errors
73
+ || matches!(event, Entry::User(u) if u.blocks().iter().any(
74
+ |block| matches!(block, ContentBlock::ToolResult(result) if result.is_error)
75
+ ))
76
+ })
69
77
  .collect()
70
78
  }
71
79
 
@@ -133,7 +141,7 @@ pub fn run(args: ShowArgs) -> Result<(), CliExit> {
133
141
  } else {
134
142
  None
135
143
  };
136
- let rows = filter_rows(&parsed.entries, &args.kinds, spec);
144
+ let rows = filter_rows(&parsed.entries, &args.kinds, spec, args.errors);
137
145
  let (selected, notice) = slice_rows(rows, args.head, args.tail, bounds, args.all);
138
146
  let mut out = Out::new();
139
147
  if args.json {