cc-transcript 14.26.0__tar.gz → 14.27.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.

Potentially problematic release.


This version of cc-transcript might be problematic. Click here for more details.

Files changed (184) hide show
  1. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/PKG-INFO +1 -1
  2. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/pyproject.toml +1 -1
  3. cc_transcript-14.27.0/rust/crates/cli/src/commands/grep.rs +561 -0
  4. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/lib.rs +24 -0
  5. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/activity.rs +34 -8
  6. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/facts.rs +11 -17
  7. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/lib.rs +2 -0
  8. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/render.rs +235 -5
  9. cc_transcript-14.27.0/rust/crates/core/src/scan.rs +648 -0
  10. cc_transcript-14.27.0/rust/crates/core/src/scan_grep.rs +1042 -0
  11. cc_transcript-14.27.0/rust/crates/core/src/scan_regressions.rs +451 -0
  12. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/snapshot.rs +163 -8
  13. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/snapshot_projection.rs +3 -3
  14. cc_transcript-14.26.0/rust/crates/cli/src/commands/grep.rs +0 -366
  15. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/Cargo.lock +0 -0
  16. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/Cargo.toml +0 -0
  17. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/LICENSE +0 -0
  18. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/README.md +0 -0
  19. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/__init__.py +0 -0
  20. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/__init__.pyi +0 -0
  21. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/_native.pyi +0 -0
  22. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/activity.py +0 -0
  23. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/activity_probe.py +0 -0
  24. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/builders.py +0 -0
  25. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/codex.py +0 -0
  26. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/command.py +0 -0
  27. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/context.py +0 -0
  28. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/corrections.py +0 -0
  29. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/cost.py +0 -0
  30. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/decision_store.py +0 -0
  31. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/decisions.py +0 -0
  32. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/discovery.py +0 -0
  33. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/disktruth.py +0 -0
  34. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/evidence.py +0 -0
  35. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/extract/__init__.py +0 -0
  36. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/extract/correct.py +0 -0
  37. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/facts.py +0 -0
  38. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/filterspec.py +0 -0
  39. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/heartbeats.py +0 -0
  40. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/ids.py +0 -0
  41. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/judge/__init__.py +0 -0
  42. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/judge/llm.py +0 -0
  43. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/judge/similar.py +0 -0
  44. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/judge/verdicts.py +0 -0
  45. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/ledger.py +0 -0
  46. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/literals.py +0 -0
  47. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/__init__.py +0 -0
  48. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/candidates.py +0 -0
  49. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/confidence.py +0 -0
  50. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/engine.py +0 -0
  51. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/formats.py +0 -0
  52. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/sampling.py +0 -0
  53. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/signals.py +0 -0
  54. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/sourcekind.py +0 -0
  55. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/spec.py +0 -0
  56. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/mining/store.py +0 -0
  57. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/models.py +0 -0
  58. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/nlp.py +0 -0
  59. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/notifications.py +0 -0
  60. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/parser.py +0 -0
  61. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/py.typed +0 -0
  62. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/query.py +0 -0
  63. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/render.py +0 -0
  64. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/sentiment/__init__.py +0 -0
  65. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/sentiment/buckets.py +0 -0
  66. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/sentiment/data/afinn-en-165.tsv +0 -0
  67. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/sentiment/data/domain_overrides.tsv +0 -0
  68. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/sentiment/data/en-ewt.udpipe +0 -0
  69. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/sentiment/engine.py +0 -0
  70. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/sentiment/lexicon.py +0 -0
  71. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/sentiment/scorespec.py +0 -0
  72. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/snapshot_schema/config.schema.json +0 -0
  73. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/snapshot_schema/context.schema.json +0 -0
  74. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/snapshot_schema/request.schema.json +0 -0
  75. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/snapshot_schema/response.schema.json +0 -0
  76. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/snapshot_schema/tool_registry.schema.json +0 -0
  77. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/snapshots.py +0 -0
  78. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/synthetic.py +0 -0
  79. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/tools.py +0 -0
  80. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/cc_transcript/watch.py +0 -0
  81. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/Cargo.toml +0 -0
  82. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/blame.rs +0 -0
  83. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/corpus.rs +0 -0
  84. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/corrections.rs +0 -0
  85. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/digest.rs +0 -0
  86. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/facts.rs +0 -0
  87. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/list.rs +0 -0
  88. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/mod.rs +0 -0
  89. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/scratchpad.rs +0 -0
  90. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/show.rs +0 -0
  91. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/slice.rs +0 -0
  92. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/stats.rs +0 -0
  93. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/commands/watch.rs +0 -0
  94. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/main.rs +0 -0
  95. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/output.rs +0 -0
  96. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/target.rs +0 -0
  97. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/cli/src/timearg.rs +0 -0
  98. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/Cargo.toml +0 -0
  99. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/benches/parse.rs +0 -0
  100. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/benches/render.rs +0 -0
  101. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/benches/toolcall.rs +0 -0
  102. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/actor.rs +0 -0
  103. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/blame.rs +0 -0
  104. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/buckets.rs +0 -0
  105. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/codex/discovery.rs +0 -0
  106. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/codex/lower.rs +0 -0
  107. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/codex/mod.rs +0 -0
  108. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/codex/parse.rs +0 -0
  109. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/codex/probe.rs +0 -0
  110. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/codex/protocol.rs +0 -0
  111. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/codex/types.rs +0 -0
  112. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/command.rs +0 -0
  113. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/context.rs +0 -0
  114. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/corrections.rs +0 -0
  115. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/cost.rs +0 -0
  116. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/discovery.rs +0 -0
  117. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/feedback.rs +0 -0
  118. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/filter.rs +0 -0
  119. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/gateway.rs +0 -0
  120. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/ids.rs +0 -0
  121. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/judge.rs +0 -0
  122. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/literals/command.rs +0 -0
  123. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/literals/corrections.rs +0 -0
  124. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/literals/feedback.rs +0 -0
  125. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/literals/mining.rs +0 -0
  126. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/literals/mod.rs +0 -0
  127. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/literals/protocol.rs +0 -0
  128. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/notifications.rs +0 -0
  129. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/parse.rs +0 -0
  130. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/protocol.rs +0 -0
  131. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/pystr.rs +0 -0
  132. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/query.rs +0 -0
  133. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/rng.rs +0 -0
  134. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/schema.rs +0 -0
  135. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/snapshot_activity.rs +0 -0
  136. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/snapshot_codec.rs +0 -0
  137. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/snapshot_labels.rs +0 -0
  138. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/snapshot_memory.rs +0 -0
  139. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/snapshot_owned.rs +0 -0
  140. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/snapshot_regressions.rs +0 -0
  141. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/snapshot_text.rs +0 -0
  142. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/sqlite.rs +0 -0
  143. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/toolcall.rs +0 -0
  144. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/types.rs +0 -0
  145. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/value.rs +0 -0
  146. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/core/src/watch.rs +0 -0
  147. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/Cargo.toml +0 -0
  148. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/build.rs +0 -0
  149. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/data/command_prefix_pins.tsv +0 -0
  150. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/actor_bridge.rs +0 -0
  151. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/bin/stub_gen.rs +0 -0
  152. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/codex.rs +0 -0
  153. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/command.rs +0 -0
  154. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/context.rs +0 -0
  155. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/corrections.rs +0 -0
  156. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/discovery.rs +0 -0
  157. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/feedback.rs +0 -0
  158. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/judge.rs +0 -0
  159. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/lexicon.rs +0 -0
  160. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/lib.rs +0 -0
  161. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/mining.rs +0 -0
  162. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/nlp.rs +0 -0
  163. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/python.rs +0 -0
  164. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/render.rs +0 -0
  165. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/score.rs +0 -0
  166. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/snapshot_decode.rs +0 -0
  167. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/snapshots.rs +0 -0
  168. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/sqlite.rs +0 -0
  169. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/toolcall.rs +0 -0
  170. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/attachment.rs +0 -0
  171. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/blocks.rs +0 -0
  172. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/command.rs +0 -0
  173. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/convert.rs +0 -0
  174. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/dunder.rs +0 -0
  175. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/events.rs +0 -0
  176. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/meta.rs +0 -0
  177. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/mod.rs +0 -0
  178. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/print.rs +0 -0
  179. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/store.rs +0 -0
  180. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/system.rs +0 -0
  181. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/toolcall.rs +0 -0
  182. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/toolresult.rs +0 -0
  183. {cc_transcript-14.26.0 → cc_transcript-14.27.0}/rust/crates/py/src/views/transcript.rs +0 -0
  184. {cc_transcript-14.26.0 → cc_transcript-14.27.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.26.0
3
+ Version: 14.27.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cc-transcript"
3
- version = "14.26.0" # informational — releases stamp the version from the git tag
3
+ version = "14.27.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,561 @@
1
+ use std::path::{Path, PathBuf};
2
+
3
+ use cc_transcript_core::render::{compact_line, display_path, event_json, transcript_header, Json};
4
+ use cc_transcript_core::scan::{
5
+ scan_corpus, ScanBudget, ScanControl, ScanOutcome, ScanPlan, ScanSession,
6
+ };
7
+ use cc_transcript_core::scan_grep::{GrepOptions, GrepPatternSpec, GrepReducer, GrepSourceResult};
8
+ use cc_transcript_core::snapshot::{Cancellation, NativeStore, SnapshotError, Status, WorkLimits};
9
+ use cc_transcript_core::types::{ContentBlock, Entry};
10
+ use regex::{Regex, RegexBuilder};
11
+ use sonic_rs::json;
12
+
13
+ use crate::output::{click_error, eline, usage_error, CliExit, Out};
14
+ use crate::target::py_path;
15
+ use crate::DiscoveryOpts;
16
+
17
+ const USAGE: &str = "cc-transcript grep [OPTIONS] PATTERN [PATHS]...";
18
+ const HELP_PATH: &str = "cc-transcript grep";
19
+ const DEFAULT_MAX_MATCHES: usize = 20;
20
+
21
+ #[derive(clap::Args, Default)]
22
+ pub struct ScanOptions {
23
+ #[arg(long, help = "Maximum cumulative source and logical projection bytes.")]
24
+ pub max_read_bytes: Option<usize>,
25
+ #[arg(
26
+ long,
27
+ help = "Maximum cumulative preparation and event traversal work."
28
+ )]
29
+ pub max_events: Option<usize>,
30
+ #[arg(long, help = "Maximum emitted output bytes.")]
31
+ pub max_output_bytes: Option<usize>,
32
+ #[arg(long, help = "Maximum filesystem entries examined during discovery.")]
33
+ pub max_discovery_entries: Option<usize>,
34
+ #[arg(long, help = "Maximum sources in the discovered inventory.")]
35
+ pub max_sources: Option<usize>,
36
+ #[arg(long, help = "Deadline for the complete scan in milliseconds.")]
37
+ pub timeout_ms: Option<u64>,
38
+ }
39
+
40
+ impl ScanOptions {
41
+ fn apply(&self, mut limits: WorkLimits) -> Result<WorkLimits, CliExit> {
42
+ if let Some(n) = self.max_read_bytes {
43
+ limits.max_read_bytes = n;
44
+ }
45
+ if let Some(n) = self.max_events {
46
+ limits.max_events = n;
47
+ limits.max_items = n;
48
+ }
49
+ if let Some(n) = self.max_output_bytes {
50
+ limits.max_output_bytes = n;
51
+ }
52
+ if let Some(n) = self.max_discovery_entries {
53
+ limits.max_discovery_entries = n;
54
+ }
55
+ if let Some(n) = self.max_sources {
56
+ limits.max_sources = n;
57
+ }
58
+ if let Some(n) = self.timeout_ms {
59
+ limits.deadline_unix_ms = cc_transcript_core::snapshot::now_ms().saturating_add(n);
60
+ }
61
+ if [
62
+ limits.max_read_bytes,
63
+ limits.max_events,
64
+ limits.max_output_bytes,
65
+ limits.max_discovery_entries,
66
+ limits.max_sources,
67
+ ]
68
+ .contains(&0)
69
+ || self.timeout_ms == Some(0)
70
+ {
71
+ return Err(usage_error(
72
+ USAGE,
73
+ HELP_PATH,
74
+ "scan work limits must be positive",
75
+ ));
76
+ }
77
+ Ok(limits)
78
+ }
79
+ }
80
+
81
+ pub struct GrepArgs {
82
+ pub pattern: String,
83
+ pub patterns: Vec<String>,
84
+ pub scan_json: bool,
85
+ pub scan_limits: ScanOptions,
86
+ pub paths: Vec<PathBuf>,
87
+ pub discovery: DiscoveryOpts,
88
+ pub corpus: Option<PathBuf>,
89
+ pub kinds: Vec<String>,
90
+ pub tool: Option<String>,
91
+ pub errors: bool,
92
+ pub ignore_case: bool,
93
+ pub wheres: Vec<String>,
94
+ pub context: usize,
95
+ pub max_matches: Option<usize>,
96
+ pub width: usize,
97
+ pub uuids: bool,
98
+ pub with_result: bool,
99
+ pub json: bool,
100
+ }
101
+
102
+ pub fn compile_pattern(
103
+ pattern: &str,
104
+ ignore_case: bool,
105
+ usage: &str,
106
+ help_path: &str,
107
+ ) -> Result<Regex, CliExit> {
108
+ RegexBuilder::new(pattern)
109
+ .case_insensitive(ignore_case)
110
+ .build()
111
+ .map_err(|e| usage_error(usage, help_path, &format!("invalid pattern: {e}")))
112
+ }
113
+
114
+ /// An empty `--where` searches everywhere (cli.py's default).
115
+ pub fn where_flags(wheres: &[String]) -> (bool, bool, bool) {
116
+ if wheres.is_empty() {
117
+ return (true, true, true);
118
+ }
119
+ (
120
+ wheres.iter().any(|w| w == "text"),
121
+ wheres.iter().any(|w| w == "thinking"),
122
+ wheres.iter().any(|w| w == "tools"),
123
+ )
124
+ }
125
+
126
+ fn cap(args: &GrepArgs) -> Option<usize> {
127
+ args.max_matches.map_or_else(
128
+ || args.corpus.is_none().then_some(DEFAULT_MAX_MATCHES),
129
+ |n| (n > 0).then_some(n),
130
+ )
131
+ }
132
+
133
+ fn snapshot_error(error: SnapshotError) -> CliExit {
134
+ click_error(&format!("{}: {}", error.status.as_str(), error.reason))
135
+ }
136
+
137
+ fn output_error() -> SnapshotError {
138
+ SnapshotError::new(Status::OutputLimit, "stdout write failed")
139
+ }
140
+
141
+ struct Emitter {
142
+ out: Out,
143
+ scan_json: bool,
144
+ first: bool,
145
+ exit: Option<i32>,
146
+ }
147
+
148
+ impl Emitter {
149
+ fn new(
150
+ scan_json: bool,
151
+ budget: &mut ScanBudget,
152
+ _cancel: &Cancellation,
153
+ ) -> Result<Self, CliExit> {
154
+ let mut out = Out::new();
155
+ if scan_json {
156
+ if budget.remaining().max_output_bytes < 4096 {
157
+ return Err(click_error(
158
+ "structured scan output requires 4096 status bytes",
159
+ ));
160
+ }
161
+ budget.progress.output_bytes += 4096;
162
+ out.line("{\"schema\":\"cc-transcript.scan/1\",\"matches\":[")?;
163
+ }
164
+ Ok(Self {
165
+ out,
166
+ scan_json,
167
+ first: true,
168
+ exit: None,
169
+ })
170
+ }
171
+
172
+ fn emit(
173
+ &mut self,
174
+ line: String,
175
+ budget: &mut ScanBudget,
176
+ cancel: &Cancellation,
177
+ ) -> Result<(), SnapshotError> {
178
+ budget.charge_output(line.len().saturating_add(2), cancel)?;
179
+ let result = if self.scan_json && !self.first {
180
+ self.out.line(",").and_then(|()| self.out.line(&line))
181
+ } else {
182
+ self.out.line(&line)
183
+ };
184
+ self.first = false;
185
+ result.map_err(|error| {
186
+ self.exit = Some(error.0);
187
+ output_error()
188
+ })
189
+ }
190
+
191
+ fn finish(mut self, outcome: &ScanOutcome, counts: &[usize]) -> Result<(), CliExit> {
192
+ if let Some(code) = self.exit {
193
+ return Err(CliExit(code));
194
+ }
195
+ if self.scan_json {
196
+ let status = cc_transcript_core::snapshot_codec::encode(
197
+ &json!({"outcome":outcome,"counts":counts}),
198
+ 4032,
199
+ )
200
+ .map_err(snapshot_error)?;
201
+ if status.len().saturating_add(64) > 4096 {
202
+ return Err(click_error("scan status exceeds reserved output"));
203
+ }
204
+ self.out.line(&format!("],{}", &status[1..]))?;
205
+ }
206
+ self.out.finish()?;
207
+ if let Some(reason) = &outcome.reason {
208
+ if reason == "result_limit" {
209
+ eline(&format!("warning: stopped at --max-matches {}; more matches may exist — raise it, or pass --max-matches 0 for no cap",counts.first().copied().unwrap_or(0)));
210
+ } else {
211
+ eline(&format!("warning: scan incomplete: {reason}"));
212
+ return Err(CliExit(3));
213
+ }
214
+ }
215
+ if counts.iter().all(|count| *count == 0) {
216
+ return Err(CliExit(1));
217
+ }
218
+ Ok(())
219
+ }
220
+ }
221
+
222
+ fn result_fields(event: &Entry, result: &GrepSourceResult<'_, '_>) -> Vec<(String, Json)> {
223
+ if !matches!(event, Entry::Assistant(_)) {
224
+ return Vec::new();
225
+ }
226
+ event
227
+ .blocks()
228
+ .iter()
229
+ .filter_map(|block| {
230
+ let ContentBlock::ToolUse(tool) = block else {
231
+ return None;
232
+ };
233
+ let value = result.results.get(tool.id.as_str())?;
234
+ Some((
235
+ tool.id.clone(),
236
+ Json::Obj(vec![
237
+ ("is_error".into(), Json::Bool(value.is_error)),
238
+ ("denied".into(), Json::Bool(value.denied)),
239
+ (
240
+ "duration_ms".into(),
241
+ value.duration_ms.map_or(Json::Null, Json::Int),
242
+ ),
243
+ ]),
244
+ ))
245
+ })
246
+ .collect()
247
+ }
248
+
249
+ fn result_suffix(event: &Entry, result: &GrepSourceResult<'_, '_>) -> String {
250
+ if !matches!(event, Entry::Assistant(_)) {
251
+ return String::new();
252
+ }
253
+ let markers: Vec<_> = event
254
+ .blocks()
255
+ .iter()
256
+ .filter_map(|block| {
257
+ let ContentBlock::ToolUse(tool) = block else {
258
+ return None;
259
+ };
260
+ let value = result.results.get(tool.id.as_str())?;
261
+ let status = if value.denied {
262
+ "[denied]"
263
+ } else if value.is_error {
264
+ "[err]"
265
+ } else {
266
+ ""
267
+ };
268
+ let duration = value
269
+ .duration_ms
270
+ .map(|ms| format!("({ms}ms)"))
271
+ .unwrap_or_default();
272
+ let marker = [status.to_owned(), duration]
273
+ .into_iter()
274
+ .filter(|part| !part.is_empty())
275
+ .collect::<Vec<_>>()
276
+ .join(" ");
277
+ (!marker.is_empty()).then_some(marker)
278
+ })
279
+ .collect();
280
+ if markers.is_empty() {
281
+ String::new()
282
+ } else {
283
+ format!(" {}", markers.join(" "))
284
+ }
285
+ }
286
+
287
+ pub fn run(args: GrepArgs) -> Result<(), CliExit> {
288
+ let store = NativeStore::new(&json!({})).map_err(snapshot_error)?;
289
+ let limits = args.scan_limits.apply(store.scan_limits())?;
290
+ let cancel = Cancellation::default();
291
+ *crate::SCAN_CANCELLATION.lock().expect("scan cancellation") = Some(cancel.clone());
292
+ let _interrupt = ScanInterrupt;
293
+ if let Some(path) = &args.corpus {
294
+ return run_over_corpus(&store, &args, path, limits, cancel);
295
+ }
296
+ let mut session = ScanSession::new(&store, limits, cancel.clone());
297
+ let mut reducer = match prepare_reducer(&args, &mut session.budget, &cancel) {
298
+ Ok(reducer) => reducer,
299
+ Err(error) if error.status == Status::InvalidRequest => {
300
+ return Err(usage_error(USAGE, HELP_PATH, &error.reason))
301
+ }
302
+ Err(error) => {
303
+ let emitter = Emitter::new(
304
+ args.scan_json,
305
+ &mut session.budget,
306
+ &Cancellation::default(),
307
+ )?;
308
+ return emitter.finish(
309
+ &failed_outcome(error, &session.budget),
310
+ &vec![0; args.patterns.len() + 1],
311
+ );
312
+ }
313
+ };
314
+ let plan = ScanPlan {
315
+ paths: args
316
+ .paths
317
+ .iter()
318
+ .map(|path| py_path(&path.to_string_lossy()))
319
+ .collect(),
320
+ root: args.discovery.validated_root(USAGE, HELP_PATH)?,
321
+ project: args.discovery.project.clone(),
322
+ contains: args.discovery.contains.clone(),
323
+ source_limit: args.discovery.effective_limit(),
324
+ };
325
+ let mut emitter = Emitter::new(args.scan_json, &mut session.budget, &cancel)?;
326
+ let mut files = 0usize;
327
+ let mut outcome = session.run(&plan, |path, snapshot, budget, cancel| {
328
+ let result = reducer.scan_source(snapshot, budget, cancel)?;
329
+ if result.hits.is_empty() {
330
+ return Ok(if result.quota_reached {
331
+ ScanControl::Stop {
332
+ source_complete: result.source_complete,
333
+ }
334
+ } else {
335
+ ScanControl::Continue
336
+ });
337
+ }
338
+ files += 1;
339
+ if !args.json && !args.scan_json {
340
+ emitter.emit(transcript_header(&path.to_string_lossy()), budget, cancel)?;
341
+ }
342
+ for (window_index, window) in result.windows.iter().enumerate() {
343
+ if !args.json && !args.scan_json && args.context > 0 && window_index > 0 {
344
+ emitter.emit("--".to_owned(), budget, cancel)?;
345
+ }
346
+ for index in window.clone() {
347
+ let _render_staging = result.preflight_render(snapshot, index, budget, cancel)?;
348
+ let event = snapshot.entry(index);
349
+ let hit = result.hits.iter().find(|hit| hit.event_index == index);
350
+ let line = if args.json || args.scan_json {
351
+ let Json::Obj(mut fields) = event_json(index, event) else {
352
+ unreachable!("event_json object")
353
+ };
354
+ fields.insert(
355
+ 0,
356
+ (
357
+ "path".into(),
358
+ Json::Str(path.to_string_lossy().into_owned()),
359
+ ),
360
+ );
361
+ if hit.is_none() {
362
+ fields.push(("context".into(), Json::Bool(true)));
363
+ }
364
+ if args.with_result {
365
+ let results = result_fields(event, &result);
366
+ if !results.is_empty() {
367
+ fields.push(("results".into(), Json::Obj(results)));
368
+ }
369
+ }
370
+ if args.scan_json {
371
+ fields.push(("generation".into(), Json::Str(snapshot.id.clone())));
372
+ fields.push((
373
+ "pattern_ids".into(),
374
+ Json::Arr(
375
+ hit.map(|hit| {
376
+ hit.pattern_ids
377
+ .iter()
378
+ .map(|id| Json::Int(*id as i64))
379
+ .collect()
380
+ })
381
+ .unwrap_or_default(),
382
+ ),
383
+ ));
384
+ }
385
+ Json::Obj(fields).dumps()
386
+ } else {
387
+ let mut line =
388
+ compact_line(index, event, &result.names, args.width, false, args.uuids);
389
+ if args.with_result {
390
+ line.push_str(&result_suffix(event, &result));
391
+ }
392
+ line
393
+ };
394
+ emitter.emit(line, budget, cancel)?;
395
+ }
396
+ }
397
+ Ok(if result.quota_reached {
398
+ ScanControl::Stop {
399
+ source_complete: result.source_complete,
400
+ }
401
+ } else {
402
+ ScanControl::Continue
403
+ })
404
+ });
405
+ if outcome.complete && !reducer.complete() {
406
+ outcome.complete = false;
407
+ outcome.reason = Some("result_limit".to_owned());
408
+ }
409
+ let counts = reducer.counts();
410
+ if !args.json && !args.scan_json {
411
+ let note = if outcome.selected_sources < outcome.available_sources {
412
+ format!(
413
+ " · searched {} of {} transcripts — use --all",
414
+ outcome.selected_sources, outcome.available_sources
415
+ )
416
+ } else {
417
+ String::new()
418
+ };
419
+ if let Err(error) = emitter.emit(
420
+ format!(
421
+ "{files} files, {} matches{note}",
422
+ counts.iter().sum::<usize>()
423
+ ),
424
+ &mut session.budget,
425
+ &cancel,
426
+ ) {
427
+ outcome.complete = false;
428
+ outcome.reason = Some(error.reason);
429
+ }
430
+ outcome.progress = session.budget.progress.clone();
431
+ }
432
+ emitter.finish(&outcome, counts)
433
+ }
434
+
435
+ fn run_over_corpus(
436
+ store: &NativeStore,
437
+ args: &GrepArgs,
438
+ path: &Path,
439
+ limits: WorkLimits,
440
+ cancel: Cancellation,
441
+ ) -> Result<(), CliExit> {
442
+ let mut budget = ScanBudget::new(store, limits);
443
+ let mut reducer = match prepare_reducer(args, &mut budget, &cancel) {
444
+ Ok(reducer) => reducer,
445
+ Err(error) if error.status == Status::InvalidRequest => {
446
+ return Err(usage_error(USAGE, HELP_PATH, &error.reason))
447
+ }
448
+ Err(error) => {
449
+ let emitter = Emitter::new(args.scan_json, &mut budget, &Cancellation::default())?;
450
+ return emitter.finish(
451
+ &failed_outcome(error, &budget),
452
+ &vec![0; args.patterns.len() + 1],
453
+ );
454
+ }
455
+ };
456
+ let mut emitter = Emitter::new(args.scan_json, &mut budget, &cancel)?;
457
+ let mut outcome = scan_corpus(
458
+ path,
459
+ &mut budget,
460
+ &cancel,
461
+ |line_number, line, budget, cancel| {
462
+ let (ids, _match_staging) = reducer.scan_text(line, budget, cancel)?;
463
+ if !ids.is_empty() {
464
+ let render_bound = if args.scan_json {
465
+ line.len()
466
+ .saturating_add(path.as_os_str().len())
467
+ .saturating_mul(6)
468
+ .saturating_add(1024)
469
+ } else {
470
+ line.len().saturating_add(2)
471
+ };
472
+ if render_bound > budget.remaining().max_output_bytes {
473
+ return Err(SnapshotError::new(
474
+ Status::OutputLimit,
475
+ "corpus match exceeds remaining output budget",
476
+ ));
477
+ }
478
+ let _render_staging = budget.reserve_staging(render_bound, cancel)?;
479
+ budget.charge_projection(
480
+ line.len().saturating_add(path.as_os_str().len()),
481
+ 0,
482
+ cancel,
483
+ )?;
484
+ let rendered = if args.scan_json {
485
+ sonic_rs::to_string(&json!({"path":path.to_string_lossy().as_ref(),"line":line_number,"text":line,"pattern_ids":ids})).map_err(|e|SnapshotError::new(Status::OutputLimit,e.to_string()))?
486
+ } else {
487
+ line.to_owned()
488
+ };
489
+ emitter.emit(rendered, budget, cancel)?;
490
+ }
491
+ Ok(reducer.satisfied())
492
+ },
493
+ );
494
+ if outcome.complete && !reducer.complete() {
495
+ outcome.complete = false;
496
+ outcome.reason = Some("result_limit".to_owned());
497
+ }
498
+ let counts = reducer.counts();
499
+ if !args.scan_json {
500
+ if let Err(error) = emitter.emit(
501
+ format!(
502
+ "{} matches in {}",
503
+ counts.iter().sum::<usize>(),
504
+ display_path(&path.to_string_lossy())
505
+ ),
506
+ &mut budget,
507
+ &cancel,
508
+ ) {
509
+ outcome.complete = false;
510
+ outcome.reason = Some(error.reason);
511
+ }
512
+ outcome.progress = budget.progress.clone();
513
+ }
514
+ emitter.finish(&outcome, counts)
515
+ }
516
+
517
+ fn failed_outcome(error: SnapshotError, budget: &ScanBudget) -> ScanOutcome {
518
+ ScanOutcome {
519
+ complete: false,
520
+ reason: Some(format!("{}: {}", error.status.as_str(), error.reason)),
521
+ selected_sources: 0,
522
+ available_sources: 0,
523
+ progress: budget.progress.clone(),
524
+ }
525
+ }
526
+
527
+ struct ScanInterrupt;
528
+ impl Drop for ScanInterrupt {
529
+ fn drop(&mut self) {
530
+ *crate::SCAN_CANCELLATION.lock().expect("scan cancellation") = None;
531
+ }
532
+ }
533
+
534
+ fn prepare_reducer<'store>(
535
+ args: &GrepArgs,
536
+ budget: &mut ScanBudget<'store>,
537
+ cancel: &Cancellation,
538
+ ) -> Result<GrepReducer<'store>, SnapshotError> {
539
+ let (where_text, where_thinking, where_tools) = where_flags(&args.wheres);
540
+ let patterns = std::iter::once(&args.pattern)
541
+ .chain(args.patterns.iter())
542
+ .enumerate()
543
+ .map(|(id, pattern)| GrepPatternSpec {
544
+ id,
545
+ pattern: pattern.clone(),
546
+ max_matches: cap(&args),
547
+ })
548
+ .collect();
549
+ let options = GrepOptions {
550
+ kinds: args.kinds.clone(),
551
+ tool: args.tool.clone(),
552
+ errors: args.errors,
553
+ ignore_case: args.ignore_case,
554
+ where_text,
555
+ where_thinking,
556
+ where_tools,
557
+ context: args.context,
558
+ with_result: args.with_result,
559
+ };
560
+ GrepReducer::new(patterns, options, budget, cancel)
561
+ }
@@ -18,6 +18,9 @@ use output::CliExit;
18
18
  /// (cli.py watch_ catches KeyboardInterrupt); outside watch it means exit 130.
19
19
  pub static WATCH_ACTIVE: AtomicBool = AtomicBool::new(false);
20
20
  pub static WATCH_INTERRUPTED: AtomicBool = AtomicBool::new(false);
21
+ pub(crate) static SCAN_CANCELLATION: std::sync::Mutex<
22
+ Option<cc_transcript_core::snapshot::Cancellation>,
23
+ > = std::sync::Mutex::new(None);
21
24
 
22
25
  /// The real SIGINT handler spike C mandates: with the GIL detached, Python-side
23
26
  /// KeyboardInterrupt delivery never fires, so the Rust side owns the signal.
@@ -25,6 +28,12 @@ pub fn install_sigint_handler() {
25
28
  let _ = ctrlc::set_handler(|| {
26
29
  if WATCH_ACTIVE.load(Ordering::SeqCst) {
27
30
  WATCH_INTERRUPTED.store(true, Ordering::SeqCst);
31
+ } else if let Some(cancel) = SCAN_CANCELLATION
32
+ .lock()
33
+ .expect("scan cancellation")
34
+ .as_ref()
35
+ {
36
+ cancel.cancel();
28
37
  } else {
29
38
  std::process::exit(130);
30
39
  }
@@ -192,8 +201,17 @@ pub enum Cmd {
192
201
  json: bool,
193
202
  },
194
203
  /// Search transcript events for a regex pattern.
204
+ #[command(
205
+ long_about = "Search transcripts with finite read, event, source, output, and time limits. --all removes only the selected-file limit. Use repeated --pattern with --scan-json to share one traversal; incomplete scans cannot establish absence."
206
+ )]
195
207
  Grep {
196
208
  pattern: String,
209
+ #[arg(long = "pattern", requires = "scan_json")]
210
+ patterns: Vec<String>,
211
+ #[arg(long, conflicts_with = "json")]
212
+ scan_json: bool,
213
+ #[command(flatten)]
214
+ scan_limits: commands::grep::ScanOptions,
197
215
  paths: Vec<PathBuf>,
198
216
  #[command(flatten)]
199
217
  discovery: DiscoveryOpts,
@@ -545,6 +563,9 @@ fn dispatch(cmd: Cmd) -> Result<(), CliExit> {
545
563
  }),
546
564
  Cmd::Grep {
547
565
  pattern,
566
+ patterns,
567
+ scan_json,
568
+ scan_limits,
548
569
  paths,
549
570
  discovery,
550
571
  corpus,
@@ -561,6 +582,9 @@ fn dispatch(cmd: Cmd) -> Result<(), CliExit> {
561
582
  json,
562
583
  } => commands::grep::run(commands::grep::GrepArgs {
563
584
  pattern,
585
+ patterns,
586
+ scan_json,
587
+ scan_limits,
564
588
  paths,
565
589
  discovery,
566
590
  corpus,
@@ -313,16 +313,42 @@ pub struct ToolUse<'a> {
313
313
  pub edits: Vec<(String, Vec<Hunk>)>,
314
314
  }
315
315
 
316
+ pub(crate) fn tool_duration_ms(
317
+ start: DateTime<FixedOffset>,
318
+ end: Option<DateTime<FixedOffset>>,
319
+ ) -> Option<i64> {
320
+ end.map(|end| {
321
+ let micros = (end - start)
322
+ .num_microseconds()
323
+ .expect("duration fits i64 microseconds");
324
+ ((micros as f64 / 1_000_000.0) * 1000.0).round_ties_even() as i64
325
+ })
326
+ }
327
+
328
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
329
+ pub struct ToolResultMetadata {
330
+ pub is_error: bool,
331
+ pub denied: bool,
332
+ pub duration_ms: Option<i64>,
333
+ }
334
+
335
+ pub fn tool_result_metadata(
336
+ start: DateTime<FixedOffset>,
337
+ result: Option<&ToolResultBlock>,
338
+ end: Option<DateTime<FixedOffset>>,
339
+ ) -> ToolResultMetadata {
340
+ ToolResultMetadata {
341
+ is_error: result.is_some_and(|result| result.is_error),
342
+ denied: result.is_some_and(|result| {
343
+ result.denial_kind.as_deref() == Some(crate::protocol::DENIAL_KIND_USER_REJECTED)
344
+ }),
345
+ duration_ms: tool_duration_ms(start, end),
346
+ }
347
+ }
348
+
316
349
  impl ToolUse<'_> {
317
- /// Milliseconds from the call to its result, or None without a result
318
- /// timestamp (activity.py ToolUse.duration_ms).
319
350
  pub fn duration_ms(&self) -> Option<i64> {
320
- self.result_ts.map(|rt| {
321
- let micros = (rt - self.ts)
322
- .num_microseconds()
323
- .expect("duration fits i64 microseconds");
324
- ((micros as f64 / 1_000_000.0) * 1000.0).round_ties_even() as i64
325
- })
351
+ tool_duration_ms(self.ts, self.result_ts)
326
352
  }
327
353
  }
328
354