cc-transcript 14.23.0__tar.gz → 14.25.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 (163) hide show
  1. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/PKG-INFO +1 -1
  2. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/__init__.py +2 -0
  3. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/__init__.pyi +4 -0
  4. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/_native.pyi +3 -0
  5. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/context.py +60 -16
  6. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/render.py +5 -1
  7. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/pyproject.toml +1 -1
  8. cc_transcript-14.25.0/rust/crates/cli/src/commands/corpus.rs +124 -0
  9. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/grep.rs +96 -21
  10. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/mod.rs +1 -0
  11. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/lib.rs +80 -7
  12. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/target.rs +16 -6
  13. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/render.rs +21 -5
  14. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/context.rs +53 -11
  15. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/python.rs +4 -0
  16. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/Cargo.lock +0 -0
  17. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/Cargo.toml +0 -0
  18. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/LICENSE +0 -0
  19. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/README.md +0 -0
  20. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/activity.py +0 -0
  21. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/activity_probe.py +0 -0
  22. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/builders.py +0 -0
  23. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/codex.py +0 -0
  24. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/command.py +0 -0
  25. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/corrections.py +0 -0
  26. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/cost.py +0 -0
  27. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/decision_store.py +0 -0
  28. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/decisions.py +0 -0
  29. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/discovery.py +0 -0
  30. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/disktruth.py +0 -0
  31. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/evidence.py +0 -0
  32. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/extract/__init__.py +0 -0
  33. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/extract/correct.py +0 -0
  34. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/facts.py +0 -0
  35. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/filterspec.py +0 -0
  36. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/heartbeats.py +0 -0
  37. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/ids.py +0 -0
  38. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/judge/__init__.py +0 -0
  39. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/judge/llm.py +0 -0
  40. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/judge/similar.py +0 -0
  41. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/judge/verdicts.py +0 -0
  42. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/ledger.py +0 -0
  43. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/literals.py +0 -0
  44. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/__init__.py +0 -0
  45. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/candidates.py +0 -0
  46. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/confidence.py +0 -0
  47. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/engine.py +0 -0
  48. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/formats.py +0 -0
  49. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/sampling.py +0 -0
  50. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/signals.py +0 -0
  51. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/sourcekind.py +0 -0
  52. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/spec.py +0 -0
  53. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/mining/store.py +0 -0
  54. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/models.py +0 -0
  55. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/nlp.py +0 -0
  56. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/notifications.py +0 -0
  57. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/parser.py +0 -0
  58. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/py.typed +0 -0
  59. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/query.py +0 -0
  60. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/sentiment/__init__.py +0 -0
  61. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/sentiment/buckets.py +0 -0
  62. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/sentiment/data/afinn-en-165.tsv +0 -0
  63. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/sentiment/data/domain_overrides.tsv +0 -0
  64. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/sentiment/data/en-ewt.udpipe +0 -0
  65. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/sentiment/engine.py +0 -0
  66. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/sentiment/lexicon.py +0 -0
  67. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/sentiment/scorespec.py +0 -0
  68. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/synthetic.py +0 -0
  69. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/tools.py +0 -0
  70. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/cc_transcript/watch.py +0 -0
  71. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/Cargo.toml +0 -0
  72. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/blame.rs +0 -0
  73. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/corrections.rs +0 -0
  74. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/digest.rs +0 -0
  75. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/facts.rs +0 -0
  76. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/list.rs +0 -0
  77. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/scratchpad.rs +0 -0
  78. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/show.rs +0 -0
  79. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/slice.rs +0 -0
  80. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/stats.rs +0 -0
  81. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/commands/watch.rs +0 -0
  82. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/main.rs +0 -0
  83. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/output.rs +0 -0
  84. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/cli/src/timearg.rs +0 -0
  85. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/Cargo.toml +0 -0
  86. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/benches/parse.rs +0 -0
  87. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/benches/render.rs +0 -0
  88. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/benches/toolcall.rs +0 -0
  89. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/activity.rs +0 -0
  90. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/actor.rs +0 -0
  91. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/blame.rs +0 -0
  92. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/buckets.rs +0 -0
  93. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/codex/discovery.rs +0 -0
  94. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/codex/lower.rs +0 -0
  95. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/codex/mod.rs +0 -0
  96. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/codex/parse.rs +0 -0
  97. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/codex/probe.rs +0 -0
  98. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/codex/protocol.rs +0 -0
  99. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/codex/types.rs +0 -0
  100. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/command.rs +0 -0
  101. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/context.rs +0 -0
  102. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/corrections.rs +0 -0
  103. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/cost.rs +0 -0
  104. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/discovery.rs +0 -0
  105. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/facts.rs +0 -0
  106. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/feedback.rs +0 -0
  107. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/filter.rs +0 -0
  108. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/gateway.rs +0 -0
  109. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/ids.rs +0 -0
  110. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/judge.rs +0 -0
  111. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/lib.rs +0 -0
  112. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/literals/command.rs +0 -0
  113. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/literals/corrections.rs +0 -0
  114. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/literals/feedback.rs +0 -0
  115. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/literals/mining.rs +0 -0
  116. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/literals/mod.rs +0 -0
  117. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/literals/protocol.rs +0 -0
  118. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/notifications.rs +0 -0
  119. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/parse.rs +0 -0
  120. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/protocol.rs +0 -0
  121. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/pystr.rs +0 -0
  122. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/query.rs +0 -0
  123. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/rng.rs +0 -0
  124. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/schema.rs +0 -0
  125. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/sqlite.rs +0 -0
  126. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/toolcall.rs +0 -0
  127. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/types.rs +0 -0
  128. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/value.rs +0 -0
  129. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/core/src/watch.rs +0 -0
  130. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/Cargo.toml +0 -0
  131. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/build.rs +0 -0
  132. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/data/command_prefix_pins.tsv +0 -0
  133. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/actor_bridge.rs +0 -0
  134. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/bin/stub_gen.rs +0 -0
  135. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/codex.rs +0 -0
  136. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/command.rs +0 -0
  137. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/corrections.rs +0 -0
  138. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/discovery.rs +0 -0
  139. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/feedback.rs +0 -0
  140. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/judge.rs +0 -0
  141. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/lexicon.rs +0 -0
  142. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/lib.rs +0 -0
  143. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/mining.rs +0 -0
  144. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/nlp.rs +0 -0
  145. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/render.rs +0 -0
  146. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/score.rs +0 -0
  147. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/sqlite.rs +0 -0
  148. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/toolcall.rs +0 -0
  149. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/attachment.rs +0 -0
  150. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/blocks.rs +0 -0
  151. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/command.rs +0 -0
  152. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/convert.rs +0 -0
  153. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/dunder.rs +0 -0
  154. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/events.rs +0 -0
  155. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/meta.rs +0 -0
  156. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/mod.rs +0 -0
  157. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/print.rs +0 -0
  158. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/store.rs +0 -0
  159. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/system.rs +0 -0
  160. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/toolcall.rs +0 -0
  161. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/toolresult.rs +0 -0
  162. {cc_transcript-14.23.0 → cc_transcript-14.25.0}/rust/crates/py/src/views/transcript.rs +0 -0
  163. {cc_transcript-14.23.0 → cc_transcript-14.25.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.23.0
3
+ Version: 14.25.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -234,6 +234,8 @@ EXPORTS: dict[str, str] = {
234
234
  "SchemaError",
235
235
  "TurnRef",
236
236
  "capture_window",
237
+ "capture_windows",
238
+ "hydrate_windows",
237
239
  ),
238
240
  "cc_transcript.render": ("Budget", "render_session", "render_tool_call", "render_turn"),
239
241
  "cc_transcript.notifications": ("Notifications",),
@@ -54,6 +54,8 @@ from cc_transcript.context import (
54
54
  SchemaError as SchemaError,
55
55
  TurnRef as TurnRef,
56
56
  capture_window as capture_window,
57
+ capture_windows as capture_windows,
58
+ hydrate_windows as hydrate_windows,
57
59
  )
58
60
 
59
61
  from cc_transcript.corrections import (
@@ -467,6 +469,7 @@ __all__ = [
467
469
  "build_spec",
468
470
  "canonical_json",
469
471
  "capture_window",
472
+ "capture_windows",
470
473
  "command_prefix_counts",
471
474
  "command_prefixes",
472
475
  "cost_of",
@@ -491,6 +494,7 @@ __all__ = [
491
494
  "harvest_pairs",
492
495
  "hunk_overlap",
493
496
  "hunks_of",
497
+ "hydrate_windows",
494
498
  "keep",
495
499
  "keep_only",
496
500
  "labels_for",
@@ -120,6 +120,7 @@ __all__ = [
120
120
  "command_parse",
121
121
  "command_prefixes",
122
122
  "context_capture_window",
123
+ "context_capture_windows",
123
124
  "context_render_preview",
124
125
  "context_roundtrip",
125
126
  "cost_of_json",
@@ -2330,6 +2331,8 @@ def command_prefixes(commands: typing.Sequence[builtins.str]) -> builtins.list[b
2330
2331
 
2331
2332
  def context_capture_window(raw: bytes, session_id: builtins.str, anchor_uuid: builtins.str, anchor_tool_use_id: typing.Optional[builtins.str], before: builtins.int, after: builtins.int, preview_chars: builtins.int) -> builtins.str: ...
2332
2333
 
2334
+ def context_capture_windows(raw: bytes, anchors: typing.Sequence[tuple[builtins.str, builtins.str, typing.Optional[builtins.str]]], before: builtins.int, after: builtins.int, preview_chars: builtins.int) -> builtins.list[builtins.str]: ...
2335
+
2333
2336
  def context_render_preview(data: builtins.str, turn_chars: builtins.int) -> builtins.str: ...
2334
2337
 
2335
2338
  def context_roundtrip(data: builtins.str) -> builtins.str: ...
@@ -21,7 +21,7 @@ from cc_transcript.models import Question
21
21
  from cc_transcript.render import Budget, clip, render_turn
22
22
 
23
23
  if TYPE_CHECKING:
24
- from collections.abc import Mapping
24
+ from collections.abc import Mapping, Sequence
25
25
  from typing import Any
26
26
 
27
27
  from cc_transcript.activity import Turn
@@ -158,17 +158,7 @@ class ContextWindow:
158
158
  away — callers fall back to :meth:`render_preview`, never
159
159
  hydrate-or-fail.
160
160
  """
161
- try:
162
- activity = SessionActivity.from_session(self.anchor.session_id)
163
- except TranscriptExpiredError:
164
- return None
165
- turns: list[Turn] = []
166
- for turn_ref in window_refs(self):
167
- resolved = [turn for ref in turn_ref.refs if (turn := activity.turn_of(ref)) is not None]
168
- if len(resolved) != len(turn_ref.refs) or not resolved:
169
- return None
170
- turns.append(resolved[0])
171
- return HydratedWindow(window=self, turns=tuple(turns))
161
+ return hydrate_windows((self,))[0]
172
162
 
173
163
  def to_json(self) -> str:
174
164
  """Serialize to the ``cc-transcript.context/2`` wire schema, byte-stably.
@@ -259,11 +249,65 @@ def capture_window(
259
249
  Raises:
260
250
  ValueError: When ``anchor`` does not resolve within ``raw``.
261
251
  """
262
- return ContextWindow.from_json(
263
- _native.context_capture_window(
264
- raw, anchor.session_id, anchor.event_uuid, anchor.tool_use_id, before, after, preview_chars
252
+ return capture_windows(raw, (anchor,), before=before, after=after, preview_chars=preview_chars)[0]
253
+
254
+
255
+ def capture_windows(
256
+ raw: bytes,
257
+ anchors: Sequence[EventRef],
258
+ *,
259
+ before: int = 6,
260
+ after: int = 2,
261
+ preview_chars: int = 200,
262
+ ) -> list[ContextWindow]:
263
+ """Capture ordered windows with one parse and one activity lift per session.
264
+
265
+ Uses the same budgets and anchor resolution as :func:`capture_window`.
266
+ An empty batch returns without parsing; any unresolved anchor raises
267
+ ValueError for the entire batch.
268
+ """
269
+ return [
270
+ ContextWindow.from_json(data)
271
+ for data in _native.context_capture_windows(
272
+ raw,
273
+ [(anchor.session_id, anchor.event_uuid, anchor.tool_use_id) for anchor in anchors],
274
+ before,
275
+ after,
276
+ preview_chars,
265
277
  )
266
- )
278
+ ]
279
+
280
+
281
+ def hydrate_windows(windows: Sequence[ContextWindow]) -> list[HydratedWindow | None]:
282
+ """Resolve an ordered batch, loading each session at most once.
283
+
284
+ Missing transcripts and compacted references yield None at that window's
285
+ position. The batch owns its reads; no parsed-session cache persists
286
+ between calls, so a later call observes transcript changes.
287
+ """
288
+ sessions: dict[SessionId, list[tuple[int, ContextWindow]]] = {}
289
+ for index, window in enumerate(windows):
290
+ sessions.setdefault(window.anchor.session_id, []).append((index, window))
291
+ hydrated: list[HydratedWindow | None] = [None] * len(windows)
292
+ for session_id, batch in sessions.items():
293
+ try:
294
+ activity = SessionActivity.from_session(session_id)
295
+ except TranscriptExpiredError:
296
+ continue
297
+ for index, window in batch:
298
+ hydrated[index] = hydrate_from_activity(window, activity)
299
+ del activity
300
+ return hydrated
301
+
302
+
303
+ def hydrate_from_activity(window: ContextWindow, activity: SessionActivity) -> HydratedWindow | None:
304
+ turns: list[Turn] = []
305
+ for turn_ref in window_refs(window):
306
+ resolved = [turn for ref in turn_ref.refs if (turn := activity.turn_of(ref)) is not None]
307
+ if len(resolved) != len(turn_ref.refs) or not resolved:
308
+ return None
309
+ turns.append(resolved[0])
310
+ return HydratedWindow(window=window, turns=tuple(turns))
267
311
 
268
312
 
269
313
  def window_refs(window: ContextWindow) -> tuple[TurnRef, ...]:
@@ -62,7 +62,11 @@ def prefixed(prefix: str, text: str) -> tuple[str, ...]:
62
62
 
63
63
 
64
64
  def render_turn(turn: Turn, *, budget: Budget, tool_results: bool = False) -> str:
65
- """Render one turn: the prompt, the user's mid-turn messages, assistant prose, and every tool call, in order.
65
+ """Render one turn: the prompt, the messages queued mid-turn, assistant prose, and every tool call, in order.
66
+
67
+ A message queued while the agent worked renders under who sent it: ``user:`` for
68
+ the user's own, ``notification:`` for a background task's, ``peer:`` for another
69
+ agent's, ``channel:`` for an MCP channel's.
66
70
 
67
71
  Prose chunks clip to ``budget.turn_chars``; each tool call renders via
68
72
  :func:`render_tool_call` under ``budget.tool_chars``. An AskUserQuestion answer
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cc-transcript"
3
- version = "14.23.0" # informational — releases stamp the version from the git tag
3
+ version = "14.25.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,124 @@
1
+ //! `corpus`: flatten a whole sweep into one deduped file of character windows, so the
2
+ //! questions that follow grep the extract instead of re-reading every transcript.
3
+
4
+ use std::collections::HashSet;
5
+ use std::fs::File;
6
+ use std::io::{BufWriter, Write};
7
+ use std::path::PathBuf;
8
+
9
+ use cc_transcript_core::render::haystack;
10
+ use regex::Regex;
11
+
12
+ use crate::commands::grep::{compile_pattern, where_flags};
13
+ use crate::output::{click_error, eline, CliExit};
14
+ use crate::target::{map_transcripts, py_path, require_files, resolve_targets};
15
+ use crate::DiscoveryOpts;
16
+
17
+ const USAGE: &str = "cc-transcript corpus [OPTIONS] --out <OUT> PATTERN [PATHS]...";
18
+ const HELP_PATH: &str = "cc-transcript corpus";
19
+
20
+ pub struct CorpusArgs {
21
+ pub pattern: String,
22
+ pub paths: Vec<PathBuf>,
23
+ pub discovery: DiscoveryOpts,
24
+ pub wheres: Vec<String>,
25
+ pub ignore_case: bool,
26
+ pub window: usize,
27
+ pub out: PathBuf,
28
+ }
29
+
30
+ /// Byte offset of the `window`th character before `at`, or the start of `text`.
31
+ fn window_start(text: &str, at: usize, window: usize) -> usize {
32
+ match window.checked_sub(1) {
33
+ None => at,
34
+ Some(back) => text[..at]
35
+ .char_indices()
36
+ .rev()
37
+ .nth(back)
38
+ .map_or(0, |(index, _)| index),
39
+ }
40
+ }
41
+
42
+ /// Byte offset just past the `window`th character after `at`, or the end of `text`.
43
+ fn window_end(text: &str, at: usize, window: usize) -> usize {
44
+ text[at..]
45
+ .char_indices()
46
+ .nth(window)
47
+ .map_or(text.len(), |(index, _)| at + index)
48
+ }
49
+
50
+ /// One window per line: line breaks inside a window become their two-character escapes,
51
+ /// the shape `grep --corpus` then reads back.
52
+ fn flatten(window: &str) -> String {
53
+ window.replace('\n', "\\n").replace('\r', "\\r")
54
+ }
55
+
56
+ fn windows_in(text: &str, regex: &Regex, window: usize) -> Vec<String> {
57
+ regex
58
+ .find_iter(text)
59
+ .map(|hit| {
60
+ flatten(
61
+ &text[window_start(text, hit.start(), window)..window_end(text, hit.end(), window)],
62
+ )
63
+ })
64
+ .collect()
65
+ }
66
+
67
+ pub fn run(args: CorpusArgs) -> Result<(), CliExit> {
68
+ let regex = compile_pattern(&args.pattern, args.ignore_case, USAGE, HELP_PATH)?;
69
+ let (w_text, w_thinking, w_tools) = where_flags(&args.wheres);
70
+ let paths: Vec<PathBuf> = args
71
+ .paths
72
+ .iter()
73
+ .map(|p| py_path(&p.to_string_lossy()))
74
+ .collect();
75
+ require_files(&paths, "'[PATHS]...'", USAGE, HELP_PATH)?;
76
+ let targets = resolve_targets(
77
+ &paths,
78
+ &args.discovery.validated_root(USAGE, HELP_PATH)?,
79
+ args.discovery.project.as_deref(),
80
+ args.discovery.contains.as_deref(),
81
+ args.discovery.sweep_limit(),
82
+ None,
83
+ )?;
84
+ let per_file = map_transcripts(&targets.paths, |parsed| {
85
+ parsed
86
+ .entries
87
+ .iter()
88
+ .flat_map(|event| {
89
+ windows_in(
90
+ &haystack(event, w_text, w_thinking, w_tools),
91
+ &regex,
92
+ args.window,
93
+ )
94
+ })
95
+ .collect::<Vec<String>>()
96
+ });
97
+ let file = File::create(&args.out)
98
+ .map_err(|e| click_error(&format!("{}: {e}", args.out.display())))?;
99
+ let mut out = BufWriter::new(file);
100
+ let mut seen: HashSet<String> = HashSet::new();
101
+ let mut duplicates = 0usize;
102
+ let mut bytes = 0usize;
103
+ for line in per_file.into_iter().flatten() {
104
+ if !seen.insert(line.clone()) {
105
+ duplicates += 1;
106
+ continue;
107
+ }
108
+ out.write_all(line.as_bytes())
109
+ .and_then(|()| out.write_all(b"\n"))
110
+ .map_err(|e| click_error(&format!("{}: {e}", args.out.display())))?;
111
+ bytes += line.len() + 1;
112
+ }
113
+ out.flush()
114
+ .map_err(|e| click_error(&format!("{}: {e}", args.out.display())))?;
115
+ eline(&format!(
116
+ "{} files scanned, {} windows kept, {duplicates} duplicates dropped, {bytes} bytes written",
117
+ targets.paths.len(),
118
+ seen.len()
119
+ ));
120
+ if seen.is_empty() {
121
+ return Err(CliExit(1));
122
+ }
123
+ Ok(())
124
+ }
@@ -1,35 +1,39 @@
1
1
  //! cli.py `grep`.
2
2
 
3
3
  use std::collections::HashMap;
4
- use std::path::PathBuf;
4
+ use std::fs::File;
5
+ use std::io::{BufRead, BufReader};
6
+ use std::path::{Path, PathBuf};
5
7
 
6
8
  use cc_transcript_core::facts::{tool_facts, ToolFact};
7
9
  use cc_transcript_core::filter::event_kind;
8
10
  use cc_transcript_core::render::{
9
- compact_line, event_json, haystack, tool_haystack, transcript_header, Json,
11
+ compact_line, display_path, event_json, haystack, tool_haystack, transcript_header, Json,
10
12
  };
11
13
  use cc_transcript_core::toolcall::tool_name_matches;
12
14
  use cc_transcript_core::types::{ContentBlock, Entry};
13
15
  use regex::{Regex, RegexBuilder};
14
16
 
15
- use crate::output::{usage_error, CliExit, Out};
17
+ use crate::output::{click_error, eline, usage_error, CliExit, Out};
16
18
  use crate::target::{parse_transcripts, py_path, resolve_targets, scope_note, tool_names, Parsed};
17
19
  use crate::DiscoveryOpts;
18
20
 
19
21
  const USAGE: &str = "cc-transcript grep [OPTIONS] PATTERN [PATHS]...";
20
22
  const HELP_PATH: &str = "cc-transcript grep";
23
+ const DEFAULT_MAX_MATCHES: usize = 20;
21
24
 
22
25
  pub struct GrepArgs {
23
26
  pub pattern: String,
24
27
  pub paths: Vec<PathBuf>,
25
28
  pub discovery: DiscoveryOpts,
29
+ pub corpus: Option<PathBuf>,
26
30
  pub kinds: Vec<String>,
27
31
  pub tool: Option<String>,
28
32
  pub errors: bool,
29
33
  pub ignore_case: bool,
30
34
  pub wheres: Vec<String>,
31
35
  pub context: usize,
32
- pub max_matches: usize,
36
+ pub max_matches: Option<usize>,
33
37
  pub width: usize,
34
38
  pub uuids: bool,
35
39
  pub with_result: bool,
@@ -141,11 +145,78 @@ fn merge_windows(hits: &[usize], context: usize, size: usize) -> Vec<(usize, usi
141
145
  merged
142
146
  }
143
147
 
144
- fn compile_pattern(pattern: &str, ignore_case: bool) -> Result<Regex, CliExit> {
148
+ pub fn compile_pattern(
149
+ pattern: &str,
150
+ ignore_case: bool,
151
+ usage: &str,
152
+ help_path: &str,
153
+ ) -> Result<Regex, CliExit> {
145
154
  RegexBuilder::new(pattern)
146
155
  .case_insensitive(ignore_case)
147
156
  .build()
148
- .map_err(|e| usage_error(USAGE, HELP_PATH, &format!("invalid pattern: {e}")))
157
+ .map_err(|e| usage_error(usage, help_path, &format!("invalid pattern: {e}")))
158
+ }
159
+
160
+ /// An empty `--where` searches everywhere (cli.py's default).
161
+ pub fn where_flags(wheres: &[String]) -> (bool, bool, bool) {
162
+ if wheres.is_empty() {
163
+ return (true, true, true);
164
+ }
165
+ (
166
+ wheres.iter().any(|w| w == "text"),
167
+ wheres.iter().any(|w| w == "thinking"),
168
+ wheres.iter().any(|w| w == "tools"),
169
+ )
170
+ }
171
+
172
+ /// `--max-matches 0` lifts the cap and so does leaving it off in corpus mode, where each
173
+ /// match is one bounded window; anything else is a budget that must announce itself when
174
+ /// it bites, so a whole-corpus sweep never looks complete at 20 rows.
175
+ fn match_cap(max_matches: Option<usize>, default: Option<usize>) -> Option<usize> {
176
+ max_matches.map_or(default, |cap| (cap > 0).then_some(cap))
177
+ }
178
+
179
+ fn warn_truncated(cap: usize) {
180
+ eline(&format!(
181
+ "warning: stopped at --max-matches {cap}; more matches may exist — raise it, or pass --max-matches 0 for no cap"
182
+ ));
183
+ }
184
+
185
+ /// `grep --corpus`: one `corpus` window per line, matched verbatim. There is no event
186
+ /// structure left to filter on, so only the pattern, `-i`, and `--max-matches` apply.
187
+ fn run_over_corpus(args: &GrepArgs, corpus: &Path) -> Result<(), CliExit> {
188
+ let regex = compile_pattern(&args.pattern, args.ignore_case, USAGE, HELP_PATH)?;
189
+ let file =
190
+ File::open(corpus).map_err(|e| click_error(&format!("{}: {e}", corpus.display())))?;
191
+ let cap = match_cap(args.max_matches, None);
192
+ let mut out_lines: Vec<String> = Vec::new();
193
+ let mut truncated = false;
194
+ for line in BufReader::new(file).lines() {
195
+ let line = line.map_err(|e| click_error(&format!("{}: {e}", corpus.display())))?;
196
+ if !regex.is_match(&line) {
197
+ continue;
198
+ }
199
+ if cap.is_some_and(|cap| out_lines.len() == cap) {
200
+ truncated = true;
201
+ break;
202
+ }
203
+ out_lines.push(line);
204
+ }
205
+ let matched = out_lines.len();
206
+ out_lines.push(format!(
207
+ "{matched} matches in {}",
208
+ display_path(&corpus.to_string_lossy())
209
+ ));
210
+ let mut out = Out::new();
211
+ out.lines(out_lines)?;
212
+ out.finish()?;
213
+ if let Some(cap) = cap.filter(|_| truncated) {
214
+ warn_truncated(cap);
215
+ }
216
+ if matched == 0 {
217
+ return Err(CliExit(1));
218
+ }
219
+ Ok(())
149
220
  }
150
221
 
151
222
  fn fact_index(parsed: &Parsed) -> HashMap<String, ToolFact> {
@@ -159,16 +230,11 @@ fn fact_index(parsed: &Parsed) -> HashMap<String, ToolFact> {
159
230
  }
160
231
 
161
232
  pub fn run(args: GrepArgs) -> Result<(), CliExit> {
162
- let regex = compile_pattern(&args.pattern, args.ignore_case)?;
163
- let (w_text, w_thinking, w_tools) = if args.wheres.is_empty() {
164
- (true, true, true)
165
- } else {
166
- (
167
- args.wheres.iter().any(|w| w == "text"),
168
- args.wheres.iter().any(|w| w == "thinking"),
169
- args.wheres.iter().any(|w| w == "tools"),
170
- )
171
- };
233
+ if let Some(corpus) = &args.corpus {
234
+ return run_over_corpus(&args, corpus);
235
+ }
236
+ let regex = compile_pattern(&args.pattern, args.ignore_case, USAGE, HELP_PATH)?;
237
+ let (w_text, w_thinking, w_tools) = where_flags(&args.wheres);
172
238
  let paths: Vec<PathBuf> = args
173
239
  .paths
174
240
  .iter()
@@ -186,9 +252,11 @@ pub fn run(args: GrepArgs) -> Result<(), CliExit> {
186
252
  let mut out_lines: Vec<String> = Vec::new();
187
253
  let mut files_matched = 0usize;
188
254
  let mut matched = 0usize;
189
- let mut budget = args.max_matches;
255
+ let cap = match_cap(args.max_matches, Some(DEFAULT_MAX_MATCHES));
256
+ let mut truncated = false;
190
257
  for parsed in parse_transcripts(&targets.paths) {
191
- if budget == 0 {
258
+ if cap.is_some_and(|cap| matched == cap) {
259
+ truncated = true;
192
260
  break;
193
261
  }
194
262
  let names = tool_names(&parsed.entries);
@@ -197,7 +265,7 @@ pub fn run(args: GrepArgs) -> Result<(), CliExit> {
197
265
  } else {
198
266
  HashMap::new()
199
267
  };
200
- let hits: Vec<usize> = parsed
268
+ let mut hits: Vec<usize> = parsed
201
269
  .entries
202
270
  .iter()
203
271
  .enumerate()
@@ -219,14 +287,18 @@ pub fn run(args: GrepArgs) -> Result<(), CliExit> {
219
287
  }
220
288
  })
221
289
  .map(|(index, _)| index)
222
- .take(budget)
223
290
  .collect();
291
+ if let Some(cap) = cap {
292
+ if matched + hits.len() > cap {
293
+ truncated = true;
294
+ hits.truncate(cap - matched);
295
+ }
296
+ }
224
297
  if hits.is_empty() {
225
298
  continue;
226
299
  }
227
300
  files_matched += 1;
228
301
  matched += hits.len();
229
- budget -= hits.len();
230
302
  let windows = merge_windows(&hits, args.context, parsed.entries.len());
231
303
  if args.json {
232
304
  let hit_set: std::collections::HashSet<usize> = hits.iter().copied().collect();
@@ -284,6 +356,9 @@ pub fn run(args: GrepArgs) -> Result<(), CliExit> {
284
356
  let mut out = Out::new();
285
357
  out.lines(out_lines)?;
286
358
  out.finish()?;
359
+ if let Some(cap) = cap.filter(|_| truncated) {
360
+ warn_truncated(cap);
361
+ }
287
362
  if matched == 0 {
288
363
  return Err(CliExit(1));
289
364
  }
@@ -1,4 +1,5 @@
1
1
  pub mod blame;
2
+ pub mod corpus;
2
3
  pub mod corrections;
3
4
  pub mod digest;
4
5
  pub mod facts;
@@ -44,6 +44,27 @@ fn pkg_version() -> String {
44
44
  const KIND_CHOICES: [&str; 6] = ["user", "assistant", "system", "mode", "other", "attachment"];
45
45
  const WHERE_CHOICES: [&str; 3] = ["text", "thinking", "tools"];
46
46
  const LIST_PROVIDER_CHOICES: [&str; 3] = ["claude", "codex", "all"];
47
+ const DEFAULT_LIMIT: usize = 50;
48
+
49
+ /// `grep --corpus` reads a flattened extract, so every flag that selects transcripts or
50
+ /// reaches into an event's structure is rejected rather than silently ignored.
51
+ const CORPUS_INCOMPATIBLE: [&str; 15] = [
52
+ "paths",
53
+ "root",
54
+ "project",
55
+ "contains",
56
+ "limit",
57
+ "all",
58
+ "kinds",
59
+ "tool",
60
+ "errors",
61
+ "wheres",
62
+ "context",
63
+ "width",
64
+ "uuids",
65
+ "with_result",
66
+ "json",
67
+ ];
47
68
 
48
69
  #[derive(Parser)]
49
70
  #[command(
@@ -68,9 +89,9 @@ pub struct DiscoveryOpts {
68
89
  /// Substring filter over transcript file names.
69
90
  #[arg(long)]
70
91
  pub contains: Option<String>,
71
- /// Keep only the newest N transcripts.
72
- #[arg(long, default_value_t = 50)]
73
- pub limit: usize,
92
+ /// Keep only the newest N transcripts [default: 50, uncapped for corpus].
93
+ #[arg(long)]
94
+ pub limit: Option<usize>,
74
95
  /// Ignore --limit.
75
96
  #[arg(long)]
76
97
  pub all: bool,
@@ -93,7 +114,17 @@ impl DiscoveryOpts {
93
114
  if self.all {
94
115
  None
95
116
  } else {
96
- Some(self.limit)
117
+ Some(self.limit.unwrap_or(DEFAULT_LIMIT))
118
+ }
119
+ }
120
+
121
+ /// A whole-corpus sweep's limit: unbounded unless `--limit` asks for a cap, so an
122
+ /// extract never silently covers only the newest 50 transcripts.
123
+ pub fn sweep_limit(&self) -> Option<usize> {
124
+ if self.all {
125
+ None
126
+ } else {
127
+ self.limit
97
128
  }
98
129
  }
99
130
  }
@@ -166,6 +197,10 @@ pub enum Cmd {
166
197
  paths: Vec<PathBuf>,
167
198
  #[command(flatten)]
168
199
  discovery: DiscoveryOpts,
200
+ /// Search a `corpus` extract instead of transcripts: its windows carry no event
201
+ /// structure, so discovery, kind, tool, context, and rendering flags do not apply.
202
+ #[arg(long, conflicts_with_all = CORPUS_INCOMPATIBLE)]
203
+ corpus: Option<PathBuf>,
169
204
  /// Keep only these event kinds.
170
205
  #[arg(long = "kind", value_parser = KIND_CHOICES)]
171
206
  kinds: Vec<String>,
@@ -184,9 +219,9 @@ pub enum Cmd {
184
219
  /// Events of context around each hit.
185
220
  #[arg(short = 'C', long, default_value_t = 0)]
186
221
  context: usize,
187
- /// Stop after this many matches.
188
- #[arg(long = "max-matches", default_value_t = 20)]
189
- max_matches: usize,
222
+ /// Stop after this many matches; 0 lifts the cap [default: 20, uncapped with --corpus].
223
+ #[arg(long = "max-matches")]
224
+ max_matches: Option<usize>,
190
225
  /// Truncation width per chunk (0 = no cut).
191
226
  #[arg(long, default_value_t = 100)]
192
227
  width: usize,
@@ -200,6 +235,25 @@ pub enum Cmd {
200
235
  #[arg(long)]
201
236
  json: bool,
202
237
  },
238
+ /// Sweep the matched transcripts into a deduped file of character windows around every hit.
239
+ Corpus {
240
+ pattern: String,
241
+ paths: Vec<PathBuf>,
242
+ #[command(flatten)]
243
+ discovery: DiscoveryOpts,
244
+ /// Search only these areas [default: all].
245
+ #[arg(long = "where", value_parser = WHERE_CHOICES)]
246
+ wheres: Vec<String>,
247
+ /// Case-insensitive matching.
248
+ #[arg(short = 'i', long = "ignore-case")]
249
+ ignore_case: bool,
250
+ /// Characters kept either side of each match.
251
+ #[arg(long, default_value_t = 200)]
252
+ window: usize,
253
+ /// Write the extract here, one window per line.
254
+ #[arg(short = 'o', long, required = true)]
255
+ out: PathBuf,
256
+ },
203
257
  /// Summarize event, model, and tool statistics.
204
258
  Stats {
205
259
  paths: Vec<PathBuf>,
@@ -493,6 +547,7 @@ fn dispatch(cmd: Cmd) -> Result<(), CliExit> {
493
547
  pattern,
494
548
  paths,
495
549
  discovery,
550
+ corpus,
496
551
  kinds,
497
552
  tool,
498
553
  errors,
@@ -508,6 +563,7 @@ fn dispatch(cmd: Cmd) -> Result<(), CliExit> {
508
563
  pattern,
509
564
  paths,
510
565
  discovery,
566
+ corpus,
511
567
  kinds,
512
568
  tool,
513
569
  errors,
@@ -520,6 +576,23 @@ fn dispatch(cmd: Cmd) -> Result<(), CliExit> {
520
576
  with_result,
521
577
  json,
522
578
  }),
579
+ Cmd::Corpus {
580
+ pattern,
581
+ paths,
582
+ discovery,
583
+ wheres,
584
+ ignore_case,
585
+ window,
586
+ out,
587
+ } => commands::corpus::run(commands::corpus::CorpusArgs {
588
+ pattern,
589
+ paths,
590
+ discovery,
591
+ wheres,
592
+ ignore_case,
593
+ window,
594
+ out,
595
+ }),
523
596
  Cmd::Stats {
524
597
  paths,
525
598
  discovery,