cc-transcript 14.0.0__tar.gz → 14.1.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 (137) hide show
  1. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/PKG-INFO +1 -1
  2. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/_native.pyi +6 -0
  3. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/pyproject.toml +1 -1
  4. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/command.rs +115 -2
  5. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/parse.rs +2 -0
  6. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/types.rs +2 -0
  7. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/data/command_prefix_pins.tsv +1 -1
  8. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/views/print.rs +15 -1
  9. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/views/store.rs +7 -2
  10. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/Cargo.lock +0 -0
  11. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/Cargo.toml +0 -0
  12. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/LICENSE +0 -0
  13. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/README.md +0 -0
  14. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/__init__.py +0 -0
  15. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/activity.py +0 -0
  16. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/activity_probe.py +0 -0
  17. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/builders.py +0 -0
  18. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/command.py +0 -0
  19. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/context.py +0 -0
  20. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/corrections.py +0 -0
  21. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/cost.py +0 -0
  22. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/decisions.py +0 -0
  23. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/discovery.py +0 -0
  24. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/disktruth.py +0 -0
  25. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/evidence.py +0 -0
  26. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/extract/__init__.py +0 -0
  27. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/extract/correct.py +0 -0
  28. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/facts.py +0 -0
  29. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/filterspec.py +0 -0
  30. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/heartbeats.py +0 -0
  31. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/ids.py +0 -0
  32. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/judge/__init__.py +0 -0
  33. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/judge/llm.py +0 -0
  34. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/judge/similar.py +0 -0
  35. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/judge/verdicts.py +0 -0
  36. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/ledger.py +0 -0
  37. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/literals.py +0 -0
  38. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/mining/__init__.py +0 -0
  39. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/mining/candidates.py +0 -0
  40. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/mining/confidence.py +0 -0
  41. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/mining/engine.py +0 -0
  42. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/mining/filterspec.py +0 -0
  43. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/mining/formats.py +0 -0
  44. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/mining/sampling.py +0 -0
  45. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/mining/signals.py +0 -0
  46. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/mining/sourcekind.py +0 -0
  47. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/mining/spec.py +0 -0
  48. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/mining/store.py +0 -0
  49. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/models.py +0 -0
  50. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/nlp.py +0 -0
  51. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/notifications.py +0 -0
  52. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/parser.py +0 -0
  53. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/py.typed +0 -0
  54. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/query.py +0 -0
  55. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/render.py +0 -0
  56. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/sentiment/__init__.py +0 -0
  57. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/sentiment/buckets.py +0 -0
  58. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/sentiment/data/afinn-en-165.tsv +0 -0
  59. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/sentiment/data/domain_overrides.tsv +0 -0
  60. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/sentiment/data/en-ewt.udpipe +0 -0
  61. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/sentiment/engine.py +0 -0
  62. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/sentiment/lexicon.py +0 -0
  63. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/sentiment/scorespec.py +0 -0
  64. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/store.py +0 -0
  65. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/tools.py +0 -0
  66. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/cc_transcript/watch.py +0 -0
  67. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/cli/Cargo.toml +0 -0
  68. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/cli/src/commands/corrections.rs +0 -0
  69. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/cli/src/commands/digest.rs +0 -0
  70. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/cli/src/commands/facts.rs +0 -0
  71. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/cli/src/commands/grep.rs +0 -0
  72. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/cli/src/commands/list.rs +0 -0
  73. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/cli/src/commands/mod.rs +0 -0
  74. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/cli/src/commands/scratchpad.rs +0 -0
  75. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/cli/src/commands/show.rs +0 -0
  76. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/cli/src/commands/slice.rs +0 -0
  77. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/cli/src/commands/stats.rs +0 -0
  78. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/cli/src/commands/watch.rs +0 -0
  79. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/cli/src/lib.rs +0 -0
  80. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/cli/src/main.rs +0 -0
  81. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/cli/src/output.rs +0 -0
  82. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/cli/src/target.rs +0 -0
  83. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/Cargo.toml +0 -0
  84. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/benches/parse.rs +0 -0
  85. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/benches/render.rs +0 -0
  86. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/benches/toolcall.rs +0 -0
  87. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/activity.rs +0 -0
  88. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/buckets.rs +0 -0
  89. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/context.rs +0 -0
  90. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/corrections.rs +0 -0
  91. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/cost.rs +0 -0
  92. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/discovery.rs +0 -0
  93. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/facts.rs +0 -0
  94. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/filter.rs +0 -0
  95. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/ids.rs +0 -0
  96. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/lib.rs +0 -0
  97. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/literals/command.rs +0 -0
  98. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/literals/corrections.rs +0 -0
  99. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/literals/mining.rs +0 -0
  100. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/literals/mod.rs +0 -0
  101. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/literals/protocol.rs +0 -0
  102. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/notifications.rs +0 -0
  103. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/protocol.rs +0 -0
  104. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/pystr.rs +0 -0
  105. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/query.rs +0 -0
  106. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/render.rs +0 -0
  107. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/toolcall.rs +0 -0
  108. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/value.rs +0 -0
  109. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/core/src/watch.rs +0 -0
  110. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/Cargo.toml +0 -0
  111. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/build.rs +0 -0
  112. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/bin/stub_gen.rs +0 -0
  113. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/command.rs +0 -0
  114. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/context.rs +0 -0
  115. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/corrections.rs +0 -0
  116. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/discovery.rs +0 -0
  117. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/lexicon.rs +0 -0
  118. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/lib.rs +0 -0
  119. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/mining.rs +0 -0
  120. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/nlp.rs +0 -0
  121. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/python.rs +0 -0
  122. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/render.rs +0 -0
  123. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/score.rs +0 -0
  124. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/toolcall.rs +0 -0
  125. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/views/attachment.rs +0 -0
  126. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/views/blocks.rs +0 -0
  127. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/views/command.rs +0 -0
  128. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/views/convert.rs +0 -0
  129. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/views/dunder.rs +0 -0
  130. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/views/events.rs +0 -0
  131. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/views/meta.rs +0 -0
  132. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/views/mod.rs +0 -0
  133. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/views/system.rs +0 -0
  134. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/views/toolcall.rs +0 -0
  135. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/views/toolresult.rs +0 -0
  136. {cc_transcript-14.0.0 → cc_transcript-14.1.0}/rust/crates/py/src/views/transcript.rs +0 -0
  137. {cc_transcript-14.0.0 → cc_transcript-14.1.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.0.0
3
+ Version: 14.1.0
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.0"
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"
@@ -183,10 +183,23 @@ pub struct CommandLine {
183
183
  impl CommandLine {
184
184
  // Parity: command.py CommandLine.parse — blank/comment-only input yields empty parts.
185
185
  pub fn parse(raw: &str) -> CommandLine {
186
- let parts = BASH_PARSER.with(|parser| match parser.borrow_mut().parse(raw, None) {
186
+ let mut parts = BASH_PARSER.with(|parser| match parser.borrow_mut().parse(raw, None) {
187
187
  Some(tree) => walk_node(tree.root_node(), raw.as_bytes()),
188
188
  None => Vec::new(),
189
189
  });
190
+ // Nested inside an enumerated host's span: visible but span-less, like an absorbed word.
191
+ let spans: Vec<Option<(usize, usize)>> = parts.iter().map(|(cmd, _)| cmd.span).collect();
192
+ for (i, (cmd, _)) in parts.iter_mut().enumerate() {
193
+ let Some((start, end)) = cmd.span else {
194
+ continue;
195
+ };
196
+ let enclosed = spans.iter().enumerate().any(|(j, other)| {
197
+ j != i && matches!(other, Some((os, oe)) if *os <= start && end <= *oe && (*os, *oe) != (start, end))
198
+ });
199
+ if enclosed {
200
+ cmd.span = None;
201
+ }
202
+ }
190
203
  CommandLine {
191
204
  raw: raw.to_string(),
192
205
  parts,
@@ -568,7 +581,11 @@ fn walk_node(node: Node, src: &[u8]) -> Vec<(Command, Option<String>)> {
568
581
  "program" => walk_program(node, src),
569
582
  "list" => collect_parts(node, src, COMPOUND_OPS),
570
583
  "pipeline" => collect_parts(node, src, &["|"]),
571
- "command" => vec![(extract_command(node, src), None)],
584
+ "command" => {
585
+ let mut parts = vec![(extract_command(node, src), None)];
586
+ parts.extend(substitution_parts(node, src));
587
+ parts
588
+ }
572
589
  "redirected_statement" => walk_redirected(node, src),
573
590
  _ => {
574
591
  let mut parts = Vec::new();
@@ -581,6 +598,21 @@ fn walk_node(node: Node, src: &[u8]) -> Vec<(Command, Option<String>)> {
581
598
  }
582
599
  }
583
600
 
601
+ // Word/argument-position `$(…)`/backtick substitutions under a command node, enumerated as parts
602
+ // mirroring assignment position: document order (host first), redirect targets excluded.
603
+ fn substitution_parts(node: Node, src: &[u8]) -> Vec<(Command, Option<String>)> {
604
+ let mut parts = Vec::new();
605
+ let mut cursor = node.walk();
606
+ for child in node.children(&mut cursor) {
607
+ match child.kind() {
608
+ "command_substitution" => parts.extend(walk_node(child, src)),
609
+ "file_redirect" => {}
610
+ _ => parts.extend(substitution_parts(child, src)),
611
+ }
612
+ }
613
+ parts
614
+ }
615
+
584
616
  // Parity: command.py CommandLine.walk_program — collect_parts over `;`, dropping the heredoc body
585
617
  // and delimiter lines that tree-sitter's multi-heredoc ERROR recovery re-parses as sibling commands.
586
618
  fn walk_program(node: Node, src: &[u8]) -> Vec<(Command, Option<String>)> {
@@ -1017,6 +1049,87 @@ mod tests {
1017
1049
  assert_eq!(line.parts[0].0.redirects[0].fd, None);
1018
1050
  }
1019
1051
 
1052
+ fn execs(line: &CommandLine) -> Vec<&str> {
1053
+ line.parts
1054
+ .iter()
1055
+ .map(|(cmd, _)| cmd.executable.as_str())
1056
+ .collect()
1057
+ }
1058
+
1059
+ // Both substitution positions get one treatment: the nested command is a first-class part.
1060
+ #[test]
1061
+ fn substitutions_enumerate_in_word_position_like_assignment_position() {
1062
+ assert_eq!(
1063
+ execs(&CommandLine::parse("x=$(ccx repo overview)")),
1064
+ ["ccx"]
1065
+ );
1066
+ let word = CommandLine::parse("echo $(ccx repo overview)");
1067
+ assert_eq!(execs(&word), ["echo", "ccx"]);
1068
+ assert_eq!(word.parts[1].0.args, ["repo", "overview"]);
1069
+ assert_eq!(
1070
+ execs(&CommandLine::parse("echo `ccx repo overview`")),
1071
+ ["echo", "ccx"]
1072
+ );
1073
+ // command_name, quoted, concatenated, and env-prefix-value positions all enumerate.
1074
+ assert_eq!(
1075
+ execs(&CommandLine::parse("$(which python) --version")),
1076
+ ["$(which python)", "which"]
1077
+ );
1078
+ assert_eq!(execs(&CommandLine::parse("echo \"$(a)\"")), ["echo", "a"]);
1079
+ assert_eq!(
1080
+ execs(&CommandLine::parse("tag=v$(git rev-parse HEAD) make")),
1081
+ ["make", "git"]
1082
+ );
1083
+ // Redirect targets stay out, matching statement-level redirects.
1084
+ assert_eq!(execs(&CommandLine::parse("echo hi > $(target)")), ["echo"]);
1085
+ }
1086
+
1087
+ // Document order: the host command first, then each outermost substitution left to right,
1088
+ // recursing so nested substitutions follow their host.
1089
+ #[test]
1090
+ fn substitutions_enumerate_nested_in_document_order() {
1091
+ assert_eq!(
1092
+ execs(&CommandLine::parse("diff $(sort a) $(b $(c))")),
1093
+ ["diff", "sort", "b", "c"]
1094
+ );
1095
+ assert_eq!(
1096
+ execs(&CommandLine::parse("echo $(a | b; c)")),
1097
+ ["echo", "a", "b", "c"]
1098
+ );
1099
+ }
1100
+
1101
+ // Operator attachment mirrors assignment position: the op hangs off the last enumerated part.
1102
+ #[test]
1103
+ fn substitution_parts_carry_operators_and_spans_like_assignment_position() {
1104
+ let assign = CommandLine::parse("x=$(a) && foo");
1105
+ assert_eq!(execs(&assign), ["a", "foo"]);
1106
+ assert_eq!(assign.parts[0].1.as_deref(), Some("&&"));
1107
+ let word = CommandLine::parse("echo $(a) && foo");
1108
+ assert_eq!(execs(&word), ["echo", "a", "foo"]);
1109
+ assert_eq!(word.parts[0].1, None);
1110
+ assert_eq!(word.parts[1].1.as_deref(), Some("&&"));
1111
+ assert_eq!(word.next_op(1), Some("&&"));
1112
+ assert_eq!(word.prev_op(2), Some("&&"));
1113
+ assert!(!word.piped(0) && !word.piped(1));
1114
+ // Nested inside the host's span: visible but span-less (splice's non-overlap invariant);
1115
+ // assignment position has no host, so its span survives and splices in place.
1116
+ let line = CommandLine::parse("echo $(ccx repo overview)");
1117
+ assert_eq!(line.parts[0].0.span, Some((0, 25)));
1118
+ assert_eq!(line.parts[1].0.span, None);
1119
+ assert!(matches!(
1120
+ line.splice(&BTreeMap::from([(1, "ls".to_string())])),
1121
+ Err(SpliceError::NoSpan { index: 1 })
1122
+ ));
1123
+ let assign_only = CommandLine::parse("x=$(ccx repo overview)");
1124
+ assert_eq!(assign_only.parts[0].0.span, Some((4, 21)));
1125
+ assert_eq!(
1126
+ assign_only
1127
+ .splice(&BTreeMap::from([(0, "ls".to_string())]))
1128
+ .unwrap(),
1129
+ "x=$(ls)"
1130
+ );
1131
+ }
1132
+
1020
1133
  #[test]
1021
1134
  fn rewrite_occurrences_maps_or_none() {
1022
1135
  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
@@ -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