cc-transcript 14.0.0__tar.gz → 14.1.1__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 (137) hide show
  1. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/PKG-INFO +1 -1
  2. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/_native.pyi +6 -0
  3. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/pyproject.toml +1 -1
  4. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/command.rs +166 -7
  5. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/parse.rs +2 -0
  6. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/types.rs +2 -0
  7. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/data/command_prefix_pins.tsv +1 -1
  8. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/views/command.rs +1 -0
  9. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/views/print.rs +15 -1
  10. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/views/store.rs +7 -2
  11. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/Cargo.lock +0 -0
  12. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/Cargo.toml +0 -0
  13. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/LICENSE +0 -0
  14. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/README.md +0 -0
  15. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/__init__.py +0 -0
  16. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/activity.py +0 -0
  17. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/activity_probe.py +0 -0
  18. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/builders.py +0 -0
  19. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/command.py +0 -0
  20. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/context.py +0 -0
  21. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/corrections.py +0 -0
  22. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/cost.py +0 -0
  23. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/decisions.py +0 -0
  24. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/discovery.py +0 -0
  25. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/disktruth.py +0 -0
  26. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/evidence.py +0 -0
  27. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/extract/__init__.py +0 -0
  28. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/extract/correct.py +0 -0
  29. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/facts.py +0 -0
  30. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/filterspec.py +0 -0
  31. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/heartbeats.py +0 -0
  32. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/ids.py +0 -0
  33. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/judge/__init__.py +0 -0
  34. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/judge/llm.py +0 -0
  35. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/judge/similar.py +0 -0
  36. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/judge/verdicts.py +0 -0
  37. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/ledger.py +0 -0
  38. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/literals.py +0 -0
  39. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/mining/__init__.py +0 -0
  40. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/mining/candidates.py +0 -0
  41. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/mining/confidence.py +0 -0
  42. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/mining/engine.py +0 -0
  43. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/mining/filterspec.py +0 -0
  44. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/mining/formats.py +0 -0
  45. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/mining/sampling.py +0 -0
  46. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/mining/signals.py +0 -0
  47. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/mining/sourcekind.py +0 -0
  48. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/mining/spec.py +0 -0
  49. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/mining/store.py +0 -0
  50. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/models.py +0 -0
  51. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/nlp.py +0 -0
  52. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/notifications.py +0 -0
  53. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/parser.py +0 -0
  54. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/py.typed +0 -0
  55. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/query.py +0 -0
  56. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/render.py +0 -0
  57. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/sentiment/__init__.py +0 -0
  58. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/sentiment/buckets.py +0 -0
  59. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/sentiment/data/afinn-en-165.tsv +0 -0
  60. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/sentiment/data/domain_overrides.tsv +0 -0
  61. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/sentiment/data/en-ewt.udpipe +0 -0
  62. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/sentiment/engine.py +0 -0
  63. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/sentiment/lexicon.py +0 -0
  64. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/sentiment/scorespec.py +0 -0
  65. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/store.py +0 -0
  66. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/tools.py +0 -0
  67. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/cc_transcript/watch.py +0 -0
  68. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/cli/Cargo.toml +0 -0
  69. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/cli/src/commands/corrections.rs +0 -0
  70. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/cli/src/commands/digest.rs +0 -0
  71. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/cli/src/commands/facts.rs +0 -0
  72. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/cli/src/commands/grep.rs +0 -0
  73. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/cli/src/commands/list.rs +0 -0
  74. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/cli/src/commands/mod.rs +0 -0
  75. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/cli/src/commands/scratchpad.rs +0 -0
  76. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/cli/src/commands/show.rs +0 -0
  77. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/cli/src/commands/slice.rs +0 -0
  78. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/cli/src/commands/stats.rs +0 -0
  79. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/cli/src/commands/watch.rs +0 -0
  80. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/cli/src/lib.rs +0 -0
  81. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/cli/src/main.rs +0 -0
  82. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/cli/src/output.rs +0 -0
  83. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/cli/src/target.rs +0 -0
  84. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/Cargo.toml +0 -0
  85. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/benches/parse.rs +0 -0
  86. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/benches/render.rs +0 -0
  87. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/benches/toolcall.rs +0 -0
  88. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/activity.rs +0 -0
  89. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/buckets.rs +0 -0
  90. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/context.rs +0 -0
  91. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/corrections.rs +0 -0
  92. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/cost.rs +0 -0
  93. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/discovery.rs +0 -0
  94. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/facts.rs +0 -0
  95. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/filter.rs +0 -0
  96. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/ids.rs +0 -0
  97. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/lib.rs +0 -0
  98. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/literals/command.rs +0 -0
  99. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/literals/corrections.rs +0 -0
  100. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/literals/mining.rs +0 -0
  101. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/literals/mod.rs +0 -0
  102. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/literals/protocol.rs +0 -0
  103. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/notifications.rs +0 -0
  104. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/protocol.rs +0 -0
  105. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/pystr.rs +0 -0
  106. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/query.rs +0 -0
  107. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/render.rs +0 -0
  108. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/toolcall.rs +0 -0
  109. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/value.rs +0 -0
  110. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/core/src/watch.rs +0 -0
  111. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/Cargo.toml +0 -0
  112. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/build.rs +0 -0
  113. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/bin/stub_gen.rs +0 -0
  114. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/command.rs +0 -0
  115. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/context.rs +0 -0
  116. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/corrections.rs +0 -0
  117. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/discovery.rs +0 -0
  118. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/lexicon.rs +0 -0
  119. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/lib.rs +0 -0
  120. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/mining.rs +0 -0
  121. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/nlp.rs +0 -0
  122. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/python.rs +0 -0
  123. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/render.rs +0 -0
  124. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/score.rs +0 -0
  125. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/toolcall.rs +0 -0
  126. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/views/attachment.rs +0 -0
  127. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/views/blocks.rs +0 -0
  128. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/views/convert.rs +0 -0
  129. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/views/dunder.rs +0 -0
  130. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/views/events.rs +0 -0
  131. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/views/meta.rs +0 -0
  132. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/views/mod.rs +0 -0
  133. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/views/system.rs +0 -0
  134. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/views/toolcall.rs +0 -0
  135. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/views/toolresult.rs +0 -0
  136. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/rust/crates/py/src/views/transcript.rs +0 -0
  137. {cc_transcript-14.0.0 → cc_transcript-14.1.1}/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.0.0
3
+ Version: 14.1.1
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -1259,6 +1259,8 @@ class PrintMessage:
1259
1259
  blocks: The structured content blocks of the message.
1260
1260
  uuid: The message's event uuid, when present.
1261
1261
  session_id: The session the message belongs to.
1262
+ id: The API message id, when present (assistant messages carry one).
1263
+ usage: The message's token usage, when present (assistant messages carry one).
1262
1264
  """
1263
1265
  __match_args__: typing.ClassVar[tuple[str, ...]]
1264
1266
  @property
@@ -1273,6 +1275,10 @@ class PrintMessage:
1273
1275
  def uuid(self) -> ids.EventUuid | None: ...
1274
1276
  @property
1275
1277
  def session_id(self) -> ids.SessionId: ...
1278
+ @property
1279
+ def id(self) -> typing.Optional[builtins.str]: ...
1280
+ @property
1281
+ def usage(self) -> typing.Optional[Usage]: ...
1276
1282
 
1277
1283
  @typing.final
1278
1284
  class PrintResult:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cc-transcript"
3
- version = "14.0.0"
3
+ version = "14.1.1"
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"
@@ -46,6 +46,10 @@ pub struct Command {
46
46
  // Parity: command.py Command.span — byte offsets (start, end), None when a redirect absorbs a
47
47
  // trailing word; carries compare=False, repr=False, so it is excluded from PartialEq and Debug.
48
48
  pub span: Option<(usize, usize)>,
49
+ // Native-only provenance for top-level selectors. Substitution commands remain visible in
50
+ // occurrence APIs; this flag is excluded from PartialEq, Debug, and the PyO3 command surface.
51
+ #[doc(hidden)]
52
+ pub substitution: bool,
49
53
  }
50
54
 
51
55
  impl PartialEq for Command {
@@ -145,6 +149,7 @@ impl Command {
145
149
  env: self.env.clone(),
146
150
  redirects: self.redirects.clone(),
147
151
  span: self.span,
152
+ substitution: self.substitution,
148
153
  }
149
154
  }
150
155
 
@@ -183,10 +188,23 @@ pub struct CommandLine {
183
188
  impl CommandLine {
184
189
  // Parity: command.py CommandLine.parse — blank/comment-only input yields empty parts.
185
190
  pub fn parse(raw: &str) -> CommandLine {
186
- let parts = BASH_PARSER.with(|parser| match parser.borrow_mut().parse(raw, None) {
191
+ let mut parts = BASH_PARSER.with(|parser| match parser.borrow_mut().parse(raw, None) {
187
192
  Some(tree) => walk_node(tree.root_node(), raw.as_bytes()),
188
193
  None => Vec::new(),
189
194
  });
195
+ // Nested inside an enumerated host's span: visible but span-less, like an absorbed word.
196
+ let spans: Vec<Option<(usize, usize)>> = parts.iter().map(|(cmd, _)| cmd.span).collect();
197
+ for (i, (cmd, _)) in parts.iter_mut().enumerate() {
198
+ let Some((start, end)) = cmd.span else {
199
+ continue;
200
+ };
201
+ let enclosed = spans.iter().enumerate().any(|(j, other)| {
202
+ j != i && matches!(other, Some((os, oe)) if *os <= start && end <= *oe && (*os, *oe) != (start, end))
203
+ });
204
+ if enclosed {
205
+ cmd.span = None;
206
+ }
207
+ }
190
208
  CommandLine {
191
209
  raw: raw.to_string(),
192
210
  parts,
@@ -198,14 +216,19 @@ impl CommandLine {
198
216
  self.parts.iter().map(|(cmd, _)| cmd).collect()
199
217
  }
200
218
 
201
- // Parity: command.py CommandLine.primary — the final command, or None.
219
+ // Parity: command.py CommandLine.primary — the final top-level command, or None.
202
220
  pub fn primary(&self) -> Option<&Command> {
203
- self.parts.last().map(|(cmd, _)| cmd)
221
+ self.parts
222
+ .iter()
223
+ .rev()
224
+ .find_map(|(cmd, _)| (!cmd.substitution).then_some(cmd))
204
225
  }
205
226
 
206
- // Parity: command.py CommandLine.head — the first command, or None.
227
+ // Parity: command.py CommandLine.head — the first top-level command, or None.
207
228
  pub fn head(&self) -> Option<&Command> {
208
- self.parts.first().map(|(cmd, _)| cmd)
229
+ self.parts
230
+ .iter()
231
+ .find_map(|(cmd, _)| (!cmd.substitution).then_some(cmd))
209
232
  }
210
233
 
211
234
  // Parity: command.py CommandLine.prefixes — each command's prefix, None dropped.
@@ -501,6 +524,7 @@ fn extract_command(node: Node, src: &[u8]) -> Command {
501
524
  env,
502
525
  redirects,
503
526
  span: Some(span),
527
+ substitution: false,
504
528
  }
505
529
  }
506
530
 
@@ -562,13 +586,19 @@ fn walk_redirected(node: Node, src: &[u8]) -> Vec<(Command, Option<String>)> {
562
586
  }
563
587
 
564
588
  // Parity: command.py CommandLine.walk_node — program/list/pipeline split at their ops,
565
- // command extracts, redirected_statement unwraps, everything else recurses in order.
589
+ // command extracts, command substitutions mark their recursive commands, redirected_statement
590
+ // unwraps, and everything else recurses in order.
566
591
  fn walk_node(node: Node, src: &[u8]) -> Vec<(Command, Option<String>)> {
567
592
  match node.kind() {
568
593
  "program" => walk_program(node, src),
569
594
  "list" => collect_parts(node, src, COMPOUND_OPS),
570
595
  "pipeline" => collect_parts(node, src, &["|"]),
571
- "command" => vec![(extract_command(node, src), None)],
596
+ "command" => {
597
+ let mut parts = vec![(extract_command(node, src), None)];
598
+ parts.extend(substitution_parts(node, src));
599
+ parts
600
+ }
601
+ "command_substitution" => walk_substitution(node, src),
572
602
  "redirected_statement" => walk_redirected(node, src),
573
603
  _ => {
574
604
  let mut parts = Vec::new();
@@ -581,6 +611,33 @@ fn walk_node(node: Node, src: &[u8]) -> Vec<(Command, Option<String>)> {
581
611
  }
582
612
  }
583
613
 
614
+ fn walk_substitution(node: Node, src: &[u8]) -> Vec<(Command, Option<String>)> {
615
+ let mut parts = Vec::new();
616
+ let mut cursor = node.walk();
617
+ for child in node.children(&mut cursor) {
618
+ parts.extend(walk_node(child, src));
619
+ }
620
+ for (cmd, _) in &mut parts {
621
+ cmd.substitution = true;
622
+ }
623
+ parts
624
+ }
625
+
626
+ // Word/argument-position `$(…)`/backtick substitutions under a command node, enumerated as parts
627
+ // mirroring assignment position: document order (host first), redirect targets excluded.
628
+ fn substitution_parts(node: Node, src: &[u8]) -> Vec<(Command, Option<String>)> {
629
+ let mut parts = Vec::new();
630
+ let mut cursor = node.walk();
631
+ for child in node.children(&mut cursor) {
632
+ match child.kind() {
633
+ "command_substitution" => parts.extend(walk_node(child, src)),
634
+ "file_redirect" => {}
635
+ _ => parts.extend(substitution_parts(child, src)),
636
+ }
637
+ }
638
+ parts
639
+ }
640
+
584
641
  // Parity: command.py CommandLine.walk_program — collect_parts over `;`, dropping the heredoc body
585
642
  // and delimiter lines that tree-sitter's multi-heredoc ERROR recovery re-parses as sibling commands.
586
643
  fn walk_program(node: Node, src: &[u8]) -> Vec<(Command, Option<String>)> {
@@ -1017,6 +1074,108 @@ mod tests {
1017
1074
  assert_eq!(line.parts[0].0.redirects[0].fd, None);
1018
1075
  }
1019
1076
 
1077
+ fn execs(line: &CommandLine) -> Vec<&str> {
1078
+ line.parts
1079
+ .iter()
1080
+ .map(|(cmd, _)| cmd.executable.as_str())
1081
+ .collect()
1082
+ }
1083
+
1084
+ // Both substitution positions get one treatment: the nested command is a first-class part.
1085
+ #[test]
1086
+ fn substitutions_enumerate_in_word_position_like_assignment_position() {
1087
+ assert_eq!(
1088
+ execs(&CommandLine::parse("x=$(ccx repo overview)")),
1089
+ ["ccx"]
1090
+ );
1091
+ let word = CommandLine::parse("echo $(ccx repo overview)");
1092
+ assert_eq!(execs(&word), ["echo", "ccx"]);
1093
+ assert_eq!(word.parts[1].0.args, ["repo", "overview"]);
1094
+ assert_eq!(
1095
+ execs(&CommandLine::parse("echo `ccx repo overview`")),
1096
+ ["echo", "ccx"]
1097
+ );
1098
+ // command_name, quoted, concatenated, and env-prefix-value positions all enumerate.
1099
+ assert_eq!(
1100
+ execs(&CommandLine::parse("$(which python) --version")),
1101
+ ["$(which python)", "which"]
1102
+ );
1103
+ assert_eq!(execs(&CommandLine::parse("echo \"$(a)\"")), ["echo", "a"]);
1104
+ assert_eq!(
1105
+ execs(&CommandLine::parse("tag=v$(git rev-parse HEAD) make")),
1106
+ ["make", "git"]
1107
+ );
1108
+ // Redirect targets stay out, matching statement-level redirects.
1109
+ assert_eq!(execs(&CommandLine::parse("echo hi > $(target)")), ["echo"]);
1110
+ }
1111
+
1112
+ #[test]
1113
+ fn primary_skips_argument_position_substitution() {
1114
+ let line = CommandLine::parse(r#"gh pr list --json number --search "$(cat q.txt)""#);
1115
+ assert_eq!(execs(&line), ["gh", "cat"]);
1116
+ assert_eq!(line.primary().unwrap().executable, "gh");
1117
+ }
1118
+
1119
+ #[test]
1120
+ fn primary_keeps_word_position_host_command() {
1121
+ let line = CommandLine::parse(r#""$(get-cmd)" --flag"#);
1122
+ assert_eq!(execs(&line), ["\"$(get-cmd)\"", "get-cmd"]);
1123
+ assert_eq!(line.primary().unwrap().executable, "\"$(get-cmd)\"");
1124
+ }
1125
+
1126
+ #[test]
1127
+ fn head_skips_leading_assignment_position_substitution() {
1128
+ let line = CommandLine::parse("x=$(get-cmd); host --flag");
1129
+ assert_eq!(execs(&line), ["get-cmd", "host"]);
1130
+ assert_eq!(line.head().unwrap().executable, "host");
1131
+ }
1132
+
1133
+ // Document order: the host command first, then each outermost substitution left to right,
1134
+ // recursing so nested substitutions follow their host.
1135
+ #[test]
1136
+ fn substitutions_enumerate_nested_in_document_order() {
1137
+ assert_eq!(
1138
+ execs(&CommandLine::parse("diff $(sort a) $(b $(c))")),
1139
+ ["diff", "sort", "b", "c"]
1140
+ );
1141
+ assert_eq!(
1142
+ execs(&CommandLine::parse("echo $(a | b; c)")),
1143
+ ["echo", "a", "b", "c"]
1144
+ );
1145
+ }
1146
+
1147
+ // Operator attachment mirrors assignment position: the op hangs off the last enumerated part.
1148
+ #[test]
1149
+ fn substitution_parts_carry_operators_and_spans_like_assignment_position() {
1150
+ let assign = CommandLine::parse("x=$(a) && foo");
1151
+ assert_eq!(execs(&assign), ["a", "foo"]);
1152
+ assert_eq!(assign.parts[0].1.as_deref(), Some("&&"));
1153
+ let word = CommandLine::parse("echo $(a) && foo");
1154
+ assert_eq!(execs(&word), ["echo", "a", "foo"]);
1155
+ assert_eq!(word.parts[0].1, None);
1156
+ assert_eq!(word.parts[1].1.as_deref(), Some("&&"));
1157
+ assert_eq!(word.next_op(1), Some("&&"));
1158
+ assert_eq!(word.prev_op(2), Some("&&"));
1159
+ assert!(!word.piped(0) && !word.piped(1));
1160
+ // Nested inside the host's span: visible but span-less (splice's non-overlap invariant);
1161
+ // assignment position has no host, so its span survives and splices in place.
1162
+ let line = CommandLine::parse("echo $(ccx repo overview)");
1163
+ assert_eq!(line.parts[0].0.span, Some((0, 25)));
1164
+ assert_eq!(line.parts[1].0.span, None);
1165
+ assert!(matches!(
1166
+ line.splice(&BTreeMap::from([(1, "ls".to_string())])),
1167
+ Err(SpliceError::NoSpan { index: 1 })
1168
+ ));
1169
+ let assign_only = CommandLine::parse("x=$(ccx repo overview)");
1170
+ assert_eq!(assign_only.parts[0].0.span, Some((4, 21)));
1171
+ assert_eq!(
1172
+ assign_only
1173
+ .splice(&BTreeMap::from([(0, "ls".to_string())]))
1174
+ .unwrap(),
1175
+ "x=$(ls)"
1176
+ );
1177
+ }
1178
+
1020
1179
  #[test]
1021
1180
  fn rewrite_occurrences_maps_or_none() {
1022
1181
  let line = CommandLine::parse("git push; ls; git pull");
@@ -699,6 +699,8 @@ fn parse_print_message(element: &Value) -> Result<PrintMessage, ParseError> {
699
699
  };
700
700
  Ok(PrintMessage {
701
701
  body,
702
+ id: field_str(message, "id").map(str::to_string),
703
+ usage: parse_usage(message)?,
702
704
  uuid: field_str(element, "uuid").map(str::to_string),
703
705
  session_id: require_str(element, "session_id")?.to_string(),
704
706
  })
@@ -549,6 +549,8 @@ pub enum PrintBody {
549
549
  #[derive(Debug)]
550
550
  pub struct PrintMessage {
551
551
  pub body: PrintBody,
552
+ pub id: Option<String>,
553
+ pub usage: Option<Usage>,
552
554
  pub uuid: Option<String>,
553
555
  pub session_id: String,
554
556
  }
@@ -34,7 +34,7 @@ process-substitution diff <(sort a.txt) <(sort b.txt) diff
34
34
  process-substitution-in-pipeline cat <(git log) | head -3 cat|head
35
35
  arithmetic-expansion echo $((1 + 2)) echo
36
36
  arithmetic-assignment-prefix x=$((COUNT + 1)) make build make
37
- backtick-substitution echo `git rev-parse HEAD` echo
37
+ backtick-substitution echo `git rev-parse HEAD` echo|git rev-parse
38
38
  function-definition-body-and-call foo() { git status; }; foo git status|foo
39
39
  multiline-quoted-string git commit -m "line1\nline2" git commit
40
40
  newline-separated-commands git add .\ngit commit -m x git add|git commit
@@ -125,6 +125,7 @@ impl CommandView {
125
125
  .map(|rv| rv.r)
126
126
  .collect(),
127
127
  span,
128
+ substitution: false,
128
129
  },
129
130
  }
130
131
  }
@@ -264,6 +264,8 @@ view_dunders!(
264
264
  /// blocks: The structured content blocks of the message.
265
265
  /// uuid: The message's event uuid, when present.
266
266
  /// session_id: The session the message belongs to.
267
+ /// id: The API message id, when present (assistant messages carry one).
268
+ /// usage: The message's token usage, when present (assistant messages carry one).
267
269
  #[pyo3_stub_gen::derive::gen_stub_pyclass]
268
270
  #[pyclass(name = "PrintMessage", module = "cc_transcript.models", frozen)]
269
271
  pub(crate) struct PrintMessageView {
@@ -326,12 +328,24 @@ impl PrintMessageView {
326
328
  fn session_id(&self, _py: Python<'_>) -> PyResult<String> {
327
329
  Ok(self.message().session_id.clone())
328
330
  }
331
+
332
+ #[getter]
333
+ fn id(&self, _py: Python<'_>) -> PyResult<Option<String>> {
334
+ Ok(self.message().id.clone())
335
+ }
336
+
337
+ #[getter]
338
+ fn usage(&self, _py: Python<'_>) -> PyResult<Option<UsageView>> {
339
+ Ok(self.message().usage.as_ref().map(|_| UsageView {
340
+ host: UsageHost::PrintMessage(Arc::clone(&self.p.0), self.msg),
341
+ }))
342
+ }
329
343
  }
330
344
 
331
345
  view_dunders!(
332
346
  PrintMessageView,
333
347
  "PrintMessage",
334
- fields = [role, model, text, blocks, uuid, session_id]
348
+ fields = [role, model, text, blocks, uuid, session_id, id, usage]
335
349
  );
336
350
 
337
351
  /// A parsed 'claude -p --output-format json' result.
@@ -100,12 +100,13 @@ impl BlockRef {
100
100
  }
101
101
  }
102
102
 
103
- /// Where a Usage lives: an assistant entry's optional usage, or the aggregate
104
- /// usage of a ``--print`` result.
103
+ /// Where a Usage lives: an assistant entry's optional usage, the aggregate
104
+ /// usage of a ``--print`` result, or one ``--print`` message's usage.
105
105
  #[derive(Clone)]
106
106
  pub(crate) enum UsageHost {
107
107
  Entry(EventRef),
108
108
  Print(Arc<PrintResult>),
109
+ PrintMessage(Arc<PrintResult>, usize),
109
110
  }
110
111
 
111
112
  impl UsageHost {
@@ -117,6 +118,10 @@ impl UsageHost {
117
118
  .as_ref()
118
119
  .expect("usage view over an assistant entry with usage"),
119
120
  UsageHost::Print(pr) => &pr.usage,
121
+ UsageHost::PrintMessage(pr, msg) => pr.messages[*msg]
122
+ .usage
123
+ .as_ref()
124
+ .expect("usage view over a print message with usage"),
120
125
  }
121
126
  }
122
127
  }
File without changes
File without changes