cc-transcript 14.2.0__tar.gz → 14.4.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 (146) hide show
  1. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/PKG-INFO +1 -1
  2. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/__init__.py +18 -0
  3. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/__init__.pyi +35 -0
  4. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/_native.pyi +52 -9
  5. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/mining/spec.py +23 -8
  6. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/parser.py +30 -0
  7. cc_transcript-14.4.0/cc_transcript/synthetic.py +219 -0
  8. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/tools.py +27 -8
  9. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/pyproject.toml +1 -1
  10. cc_transcript-14.4.0/rust/crates/core/src/codex/mod.rs +10 -0
  11. cc_transcript-14.4.0/rust/crates/core/src/codex/parse.rs +1270 -0
  12. cc_transcript-14.4.0/rust/crates/core/src/codex/protocol.rs +129 -0
  13. cc_transcript-14.4.0/rust/crates/core/src/codex/types.rs +260 -0
  14. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/lib.rs +1 -0
  15. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/toolcall.rs +227 -18
  16. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/types.rs +4 -4
  17. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/mining.rs +3 -3
  18. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/toolcall.rs +1 -8
  19. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/views/toolcall.rs +110 -11
  20. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/Cargo.lock +0 -0
  21. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/Cargo.toml +0 -0
  22. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/LICENSE +0 -0
  23. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/README.md +0 -0
  24. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/activity.py +0 -0
  25. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/activity_probe.py +0 -0
  26. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/builders.py +0 -0
  27. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/command.py +0 -0
  28. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/context.py +0 -0
  29. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/corrections.py +0 -0
  30. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/cost.py +0 -0
  31. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/decisions.py +0 -0
  32. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/discovery.py +0 -0
  33. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/disktruth.py +0 -0
  34. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/evidence.py +0 -0
  35. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/extract/__init__.py +0 -0
  36. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/extract/correct.py +0 -0
  37. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/facts.py +0 -0
  38. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/filterspec.py +0 -0
  39. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/heartbeats.py +0 -0
  40. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/ids.py +0 -0
  41. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/judge/__init__.py +0 -0
  42. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/judge/llm.py +0 -0
  43. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/judge/similar.py +0 -0
  44. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/judge/verdicts.py +0 -0
  45. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/ledger.py +0 -0
  46. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/literals.py +0 -0
  47. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/mining/__init__.py +0 -0
  48. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/mining/candidates.py +0 -0
  49. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/mining/confidence.py +0 -0
  50. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/mining/engine.py +0 -0
  51. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/mining/formats.py +0 -0
  52. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/mining/sampling.py +0 -0
  53. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/mining/signals.py +0 -0
  54. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/mining/sourcekind.py +0 -0
  55. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/mining/store.py +0 -0
  56. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/models.py +0 -0
  57. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/nlp.py +0 -0
  58. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/notifications.py +0 -0
  59. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/py.typed +0 -0
  60. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/query.py +0 -0
  61. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/render.py +0 -0
  62. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/sentiment/__init__.py +0 -0
  63. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/sentiment/buckets.py +0 -0
  64. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/sentiment/data/afinn-en-165.tsv +0 -0
  65. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/sentiment/data/domain_overrides.tsv +0 -0
  66. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/sentiment/data/en-ewt.udpipe +0 -0
  67. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/sentiment/engine.py +0 -0
  68. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/sentiment/lexicon.py +0 -0
  69. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/sentiment/scorespec.py +0 -0
  70. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/cc_transcript/watch.py +0 -0
  71. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/cli/Cargo.toml +0 -0
  72. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/cli/src/commands/corrections.rs +0 -0
  73. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/cli/src/commands/digest.rs +0 -0
  74. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/cli/src/commands/facts.rs +0 -0
  75. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/cli/src/commands/grep.rs +0 -0
  76. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/cli/src/commands/list.rs +0 -0
  77. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/cli/src/commands/mod.rs +0 -0
  78. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/cli/src/commands/scratchpad.rs +0 -0
  79. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/cli/src/commands/show.rs +0 -0
  80. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/cli/src/commands/slice.rs +0 -0
  81. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/cli/src/commands/stats.rs +0 -0
  82. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/cli/src/commands/watch.rs +0 -0
  83. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/cli/src/lib.rs +0 -0
  84. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/cli/src/main.rs +0 -0
  85. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/cli/src/output.rs +0 -0
  86. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/cli/src/target.rs +0 -0
  87. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/Cargo.toml +0 -0
  88. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/benches/parse.rs +0 -0
  89. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/benches/render.rs +0 -0
  90. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/benches/toolcall.rs +0 -0
  91. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/activity.rs +0 -0
  92. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/buckets.rs +0 -0
  93. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/command.rs +0 -0
  94. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/context.rs +0 -0
  95. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/corrections.rs +0 -0
  96. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/cost.rs +0 -0
  97. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/discovery.rs +0 -0
  98. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/facts.rs +0 -0
  99. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/feedback.rs +0 -0
  100. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/filter.rs +0 -0
  101. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/ids.rs +0 -0
  102. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/literals/command.rs +0 -0
  103. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/literals/corrections.rs +0 -0
  104. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/literals/feedback.rs +0 -0
  105. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/literals/mining.rs +0 -0
  106. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/literals/mod.rs +0 -0
  107. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/literals/protocol.rs +0 -0
  108. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/notifications.rs +0 -0
  109. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/parse.rs +0 -0
  110. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/protocol.rs +0 -0
  111. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/pystr.rs +0 -0
  112. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/query.rs +0 -0
  113. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/render.rs +0 -0
  114. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/sqlite.rs +0 -0
  115. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/value.rs +0 -0
  116. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/core/src/watch.rs +0 -0
  117. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/Cargo.toml +0 -0
  118. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/build.rs +0 -0
  119. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/data/command_prefix_pins.tsv +0 -0
  120. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/bin/stub_gen.rs +0 -0
  121. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/command.rs +0 -0
  122. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/context.rs +0 -0
  123. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/corrections.rs +0 -0
  124. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/discovery.rs +0 -0
  125. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/feedback.rs +0 -0
  126. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/lexicon.rs +0 -0
  127. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/lib.rs +0 -0
  128. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/nlp.rs +0 -0
  129. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/python.rs +0 -0
  130. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/render.rs +0 -0
  131. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/score.rs +0 -0
  132. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/sqlite.rs +0 -0
  133. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/views/attachment.rs +0 -0
  134. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/views/blocks.rs +0 -0
  135. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/views/command.rs +0 -0
  136. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/views/convert.rs +0 -0
  137. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/views/dunder.rs +0 -0
  138. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/views/events.rs +0 -0
  139. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/views/meta.rs +0 -0
  140. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/views/mod.rs +0 -0
  141. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/views/print.rs +0 -0
  142. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/views/store.rs +0 -0
  143. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/views/system.rs +0 -0
  144. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/views/toolresult.rs +0 -0
  145. {cc_transcript-14.2.0 → cc_transcript-14.4.0}/rust/crates/py/src/views/transcript.rs +0 -0
  146. {cc_transcript-14.2.0 → cc_transcript-14.4.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.2.0
3
+ Version: 14.4.0
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -53,6 +53,7 @@ EXPORTS: dict[str, str] = {
53
53
  "ReadResult",
54
54
  "SkillCall",
55
55
  "SkillResult",
56
+ "SpanEditCall",
56
57
  "TaskCall",
57
58
  "TaskCreateCall",
58
59
  "TaskLaunchResult",
@@ -76,7 +77,9 @@ EXPORTS: dict[str, str] = {
76
77
  "mcp_parts",
77
78
  "parse_tool_call",
78
79
  "parse_tool_result",
80
+ "register_mcp_tool",
79
81
  "tool_name_matches",
82
+ "unregister_mcp_tool",
80
83
  ),
81
84
  "cc_transcript.command": (
82
85
  "Command",
@@ -142,10 +145,25 @@ EXPORTS: dict[str, str] = {
142
145
  "cc_transcript.parser": (
143
146
  "parse",
144
147
  "parse_event",
148
+ "parse_events",
145
149
  "parse_events_from_bytes",
146
150
  "parse_print_result",
147
151
  "stream",
148
152
  ),
153
+ "cc_transcript.synthetic": (
154
+ "assistant_line",
155
+ "meta_fields",
156
+ "mode_line",
157
+ "other_line",
158
+ "synthetic_assistant_event",
159
+ "synthetic_user_event",
160
+ "system_line",
161
+ "text_block",
162
+ "thinking_block",
163
+ "tool_result",
164
+ "tool_use",
165
+ "user_line",
166
+ ),
149
167
  "cc_transcript.cost": (
150
168
  "PRICING",
151
169
  "CostBreakdown",
@@ -187,6 +187,7 @@ from cc_transcript.notifications import (
187
187
  from cc_transcript.parser import (
188
188
  parse as parse,
189
189
  parse_event as parse_event,
190
+ parse_events as parse_events,
190
191
  parse_events_from_bytes as parse_events_from_bytes,
191
192
  parse_print_result as parse_print_result,
192
193
  stream as stream,
@@ -207,6 +208,21 @@ from cc_transcript.render import (
207
208
  render_turn as render_turn,
208
209
  )
209
210
 
211
+ from cc_transcript.synthetic import (
212
+ assistant_line as assistant_line,
213
+ meta_fields as meta_fields,
214
+ mode_line as mode_line,
215
+ other_line as other_line,
216
+ synthetic_assistant_event as synthetic_assistant_event,
217
+ synthetic_user_event as synthetic_user_event,
218
+ system_line as system_line,
219
+ text_block as text_block,
220
+ thinking_block as thinking_block,
221
+ tool_result as tool_result,
222
+ tool_use as tool_use,
223
+ user_line as user_line,
224
+ )
225
+
210
226
  from cc_transcript.tools import (
211
227
  AskUserQuestionResult as AskUserQuestionResult,
212
228
  BashCall as BashCall,
@@ -227,6 +243,7 @@ from cc_transcript.tools import (
227
243
  ReadResult as ReadResult,
228
244
  SkillCall as SkillCall,
229
245
  SkillResult as SkillResult,
246
+ SpanEditCall as SpanEditCall,
230
247
  TOOL_ALIASES as TOOL_ALIASES,
231
248
  TaskCall as TaskCall,
232
249
  TaskCreateCall as TaskCreateCall,
@@ -251,7 +268,9 @@ from cc_transcript.tools import (
251
268
  mcp_parts as mcp_parts,
252
269
  parse_tool_call as parse_tool_call,
253
270
  parse_tool_result as parse_tool_result,
271
+ register_mcp_tool as register_mcp_tool,
254
272
  tool_name_matches as tool_name_matches,
273
+ unregister_mcp_tool as unregister_mcp_tool,
255
274
  )
256
275
 
257
276
  from cc_transcript.watch import (
@@ -360,6 +379,7 @@ __all__ = [
360
379
  "SessionId",
361
380
  "SkillCall",
362
381
  "SkillResult",
382
+ "SpanEditCall",
363
383
  "StopHookSummary",
364
384
  "SubagentIndex",
365
385
  "SubagentSession",
@@ -406,6 +426,7 @@ __all__ = [
406
426
  "WriteResult",
407
427
  "annotate_spec",
408
428
  "apply_spec",
429
+ "assistant_line",
409
430
  "build_spec",
410
431
  "canonical_json",
411
432
  "capture_window",
@@ -440,15 +461,20 @@ __all__ = [
440
461
  "mcp_access",
441
462
  "mcp_parts",
442
463
  "mcp_summary",
464
+ "meta_fields",
465
+ "mode_line",
443
466
  "native_user_classifier",
467
+ "other_line",
444
468
  "parse",
445
469
  "parse_command_line",
446
470
  "parse_event",
471
+ "parse_events",
447
472
  "parse_events_from_bytes",
448
473
  "parse_print_result",
449
474
  "parse_tool_call",
450
475
  "parse_tool_result",
451
476
  "record_harvest",
477
+ "register_mcp_tool",
452
478
  "render_session",
453
479
  "render_tool_call",
454
480
  "render_turn",
@@ -457,9 +483,18 @@ __all__ = [
457
483
  "result_index",
458
484
  "stream",
459
485
  "subagent_paths",
486
+ "synthetic_assistant_event",
487
+ "synthetic_user_event",
488
+ "system_line",
489
+ "text_block",
490
+ "thinking_block",
460
491
  "thinking_chars",
461
492
  "tool_digest",
462
493
  "tool_facts",
463
494
  "tool_name_matches",
495
+ "tool_result",
496
+ "tool_use",
464
497
  "tool_uses",
498
+ "unregister_mcp_tool",
499
+ "user_line",
465
500
  ]
@@ -72,6 +72,7 @@ __all__ = [
72
72
  "ServerToolUse",
73
73
  "SkillCall",
74
74
  "SkillResult",
75
+ "SpanEditCall",
75
76
  "SpecMatcher",
76
77
  "StopHookSummary",
77
78
  "SystemEvent",
@@ -133,6 +134,7 @@ __all__ = [
133
134
  "parse_bytes",
134
135
  "parse_print_result",
135
136
  "query_session",
137
+ "register_mcp_tool",
136
138
  "render_compact_lines",
137
139
  "render_haystacks",
138
140
  "render_stats",
@@ -147,6 +149,7 @@ __all__ = [
147
149
  "tool_name_matches",
148
150
  "toolcall_parse_view",
149
151
  "toolresult_parse_view",
152
+ "unregister_mcp_tool",
150
153
  ]
151
154
 
152
155
  @typing.final
@@ -1131,6 +1134,11 @@ class OtherCall(ToolCallBase):
1131
1134
  __match_args__: typing.ClassVar[tuple[str, ...]]
1132
1135
  @property
1133
1136
  def name(self) -> builtins.str: ...
1137
+ @property
1138
+ def error(self) -> typing.Optional[builtins.str]:
1139
+ r"""
1140
+ The strict-parse failure this call degraded from; None when the payload was well-formed but the tool has no typed model.
1141
+ """
1134
1142
 
1135
1143
  @typing.final
1136
1144
  class OtherEvent:
@@ -1514,6 +1522,27 @@ class SkillResult(ToolResultBase):
1514
1522
  @property
1515
1523
  def allowed_tools(self) -> tuple[str, ...] | None: ...
1516
1524
 
1525
+ @typing.final
1526
+ class SpanEditCall(ToolCallBase):
1527
+ r"""
1528
+ An in-place file edit addressed by an opaque locator.
1529
+
1530
+ A registered MCP tool whose spec carries a span-edit lowering. The payload
1531
+ carries no pre-image, so :func:`hunks_of` yields no hunk; ``new`` is ``None``
1532
+ when the call deletes rather than writes. Registrations are process-local.
1533
+
1534
+ Attributes:
1535
+ file_path: The file the edit targets.
1536
+ new: The written content, or ``None`` when the call is a deletion.
1537
+ """
1538
+ __match_args__: typing.ClassVar[tuple[str, ...]]
1539
+ @property
1540
+ def name(self) -> builtins.str: ...
1541
+ @property
1542
+ def file_path(self) -> builtins.str: ...
1543
+ @property
1544
+ def new(self) -> typing.Optional[builtins.str]: ...
1545
+
1517
1546
  @typing.final
1518
1547
  class SpecMatcher:
1519
1548
  def __new__(cls, spec_json: builtins.str) -> SpecMatcher: ...
@@ -2147,6 +2176,8 @@ def hunks_of(call: tools.ToolCall | tools.FallbackCall) -> tuple[Hunk, ...]:
2147
2176
 
2148
2177
  MultiEdit yields one hunk per span in application order — never just the
2149
2178
  first. Write and NotebookEdit are pure additions with an empty old side.
2179
+ :class:`~SpanEditCall` yields none: its payload carries no
2180
+ pre-image, so no honest hunk can be derived.
2150
2181
  """
2151
2182
 
2152
2183
  def ids_canonical_json(value_json: builtins.str) -> builtins.str: ...
@@ -2166,19 +2197,16 @@ def matches_names(actual: builtins.str, names: collections.abc.Container[str]) -
2166
2197
  Whether ``actual`` is one of ``names``, exactly or as an MCP tool suffix.
2167
2198
 
2168
2199
  True when ``actual`` is in ``names``, or when it splits as
2169
- ``mcp__<server>__<tool>`` on the first two ``__`` and ``<tool>`` — or its
2170
- native built-in edit-gate equivalent — is in ``names``. That alias closes
2171
- the edit-gate bypass where cc-context's ``ccx_code_edit`` /
2172
- ``ccx_code_replace`` write through the MCP under names no ``Edit``/``Write``
2173
- gate would catch. Display-name aliases from
2174
- :data:`~tools.TOOL_ALIASES` are not closed over ``names`` is
2175
- taken verbatim; pre-expand with :func:`expand_tool_names` for those.
2200
+ ``mcp__<server>__<tool>`` on the first two ``__`` and ``<tool>`` — or the
2201
+ built-in gate its registered spec ``behaves_like`` — is in ``names``. That
2202
+ closure lets a registered MCP tool (e.g. one that gates as ``Edit``) match an
2203
+ ``Edit`` gate even under a name no built-in gate would catch. Display-name
2204
+ aliases from :data:`~tools.TOOL_ALIASES` are not closed over —
2205
+ ``names`` is taken verbatim; pre-expand with :func:`expand_tool_names` for those.
2176
2206
 
2177
2207
  Example:
2178
2208
  >>> matches_names("mcp__github__Grep", {"Grep"})
2179
2209
  True
2180
- >>> matches_names("mcp__cc-context__ccx_code_edit", {"Edit"})
2181
- True
2182
2210
  >>> matches_names("Execute", {"Bash"})
2183
2211
  False
2184
2212
  """
@@ -2230,6 +2258,16 @@ def parse_print_result(raw: bytes) -> PrintResult: ...
2230
2258
 
2231
2259
  def query_session(path: builtins.str, max_events: builtins.int) -> dict[str, typing.Any]: ...
2232
2260
 
2261
+ def register_mcp_tool(tool: builtins.str, behaves_like: builtins.str, span_edit: collections.abc.Mapping[str, str] | None = None) -> None:
2262
+ r"""
2263
+ Register an MCP tool spec, keyed by its bare segment; last write wins.
2264
+
2265
+ ``behaves_like`` names the built-in gate the tool matches. ``span_edit``, when
2266
+ given, maps the payload key names — ``path``, ``content``, and optionally
2267
+ ``delete`` — the tool's :class:`~SpanEditCall` lowering
2268
+ reads. Registrations are process-local.
2269
+ """
2270
+
2233
2271
  def render_compact_lines(raw: bytes, width: builtins.int, thinking: builtins.bool, uuids: builtins.bool) -> builtins.list[builtins.str]: ...
2234
2272
 
2235
2273
  def render_haystacks(raw: bytes, wheres: typing.Sequence[builtins.str]) -> builtins.list[builtins.str]: ...
@@ -2275,3 +2313,8 @@ def toolresult_parse_view(name: builtins.str, payload_json: builtins.str) -> too
2275
2313
  the typed view hierarchy; the ``cc_transcript.tools`` facade owns the public
2276
2314
  signature.
2277
2315
  """
2316
+
2317
+ def unregister_mcp_tool(tool: builtins.str) -> builtins.bool:
2318
+ r"""
2319
+ Unregister an MCP tool by its bare segment; returns whether it was registered.
2320
+ """
@@ -65,10 +65,23 @@ ALL_DETECTORS: frozenset[DetectorName] = frozenset(
65
65
  }
66
66
  )
67
67
 
68
- EDIT_TOOLS: frozenset[str] = expand_tool_names("Edit|Write|MultiEdit|NotebookEdit")
69
- SUBAGENT_TOOLS: frozenset[str] = expand_tool_names("Agent|Task")
70
- PLAN_TOOLS: frozenset[str] = expand_tool_names("ExitPlanMode")
71
- DENIAL_EXCLUDED_TOOLS: frozenset[str] = expand_tool_names("ExitPlanMode|AskUserQuestion")
68
+ # Expanded lazily (module __getattr__ and the spec fields' default_factory), so an
69
+ # MCP tool registered after import but before spec build lands in the name sets.
70
+ _TOOL_SETS: dict[str, str] = {
71
+ "EDIT_TOOLS": "Edit|Write|MultiEdit|NotebookEdit",
72
+ "SUBAGENT_TOOLS": "Agent|Task",
73
+ "PLAN_TOOLS": "ExitPlanMode",
74
+ "DENIAL_EXCLUDED_TOOLS": "ExitPlanMode|AskUserQuestion",
75
+ }
76
+
77
+
78
+ def __getattr__(name: str) -> frozenset[str]:
79
+ try:
80
+ return expand_tool_names(_TOOL_SETS[name])
81
+ except KeyError:
82
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
83
+
84
+
72
85
  REENTRY_LOOKBACK = 40
73
86
  CONFIDENCE_STEP = 0.25
74
87
  SHORT_FOLLOWUP_MAX_WORDS = 2
@@ -168,7 +181,7 @@ USER_MESSAGE_SPEC = ConfidenceSpec(
168
181
  class ProvenanceSpec:
169
182
  """Names the subagent tools whose tool results classify as ``claude`` provenance."""
170
183
 
171
- subagent_tools: frozenset[str] = SUBAGENT_TOOLS
184
+ subagent_tools: frozenset[str] = field(default_factory=lambda: expand_tool_names(_TOOL_SETS["SUBAGENT_TOOLS"]))
172
185
 
173
186
 
174
187
  @dataclass(frozen=True, slots=True)
@@ -259,9 +272,11 @@ class MiningSpec:
259
272
  provenance: ProvenanceSpec = field(default_factory=ProvenanceSpec)
260
273
  review: ReviewSpec = field(default_factory=ReviewSpec)
261
274
  reentry_lookback: int = REENTRY_LOOKBACK
262
- edit_tools: frozenset[str] = EDIT_TOOLS
263
- plan_tools: frozenset[str] = PLAN_TOOLS
264
- denial_excluded_tools: frozenset[str] = DENIAL_EXCLUDED_TOOLS
275
+ edit_tools: frozenset[str] = field(default_factory=lambda: expand_tool_names(_TOOL_SETS["EDIT_TOOLS"]))
276
+ plan_tools: frozenset[str] = field(default_factory=lambda: expand_tool_names(_TOOL_SETS["PLAN_TOOLS"]))
277
+ denial_excluded_tools: frozenset[str] = field(
278
+ default_factory=lambda: expand_tool_names(_TOOL_SETS["DENIAL_EXCLUDED_TOOLS"])
279
+ )
265
280
 
266
281
 
267
282
  def bump(signal: CandidateSignal, delta: float, reason: str) -> CandidateSignal:
@@ -108,9 +108,39 @@ def parse_event(data: Mapping[str, Any]) -> TranscriptEvent | None:
108
108
 
109
109
 
110
110
  def parse_events_from_bytes(raw: bytes) -> list[TranscriptEvent]:
111
+ """Parse a JSONL transcript byte buffer into typed native events.
112
+
113
+ Compositionality contract: parsing splits on newlines and folds each line
114
+ independently, so for any split of ``raw`` on a line boundary,
115
+ ``parse_events_from_bytes(prefix) + parse_events_from_bytes(suffix)`` equals
116
+ ``parse_events_from_bytes(prefix + suffix)`` — value equality on the views'
117
+ structural ``__eq__``. Each call owns its buffer; no state carries across calls.
118
+ Blank, undecodable, and non-mapping lines are skipped identically wherever the
119
+ split falls, and an unterminated final line parses the same as a terminated one.
120
+ A line that fails typed parsing — a JSON object missing a required field —
121
+ raises identically wherever the split falls: the side containing that line
122
+ raises, so the equation above applies to inputs that parse. Splits inside a
123
+ line are out of contract: a mid-line split is not a line boundary, and the
124
+ fragments may parse differently than the whole line.
125
+
126
+ Incremental consumers — tail parsers re-parsing an appended slice cut on a
127
+ newline boundary — may rely on this contract; it is pinned by the line-boundary
128
+ sweep in ``tests/test_parser.py``.
129
+ """
111
130
  return list(_native.parse_bytes(raw).events)
112
131
 
113
132
 
133
+ def parse_events(*lines: Mapping[str, Any]) -> list[TranscriptEvent]:
134
+ """Parse raw transcript-line mappings into typed native events.
135
+
136
+ Serializes each mapping to a JSONL line and folds them through the native
137
+ backend — the same path :func:`parse_events_from_bytes` and production use — so
138
+ the result is a list of lazy views over a multi-line parse. Lines the tolerant
139
+ parser drops (a below-``MINYEAR`` timestamp) never materialize.
140
+ """
141
+ return parse_events_from_bytes(b"\n".join(orjson.dumps(dict(line)) for line in lines))
142
+
143
+
114
144
  def parse_print_result(raw: bytes) -> PrintResult:
115
145
  """Parse a 'claude -p --output-format json' payload into a :class:`~cc_transcript.models.PrintResult`.
116
146
 
@@ -0,0 +1,219 @@
1
+ """Synthetic transcript builders — hand-assembled JSONL that parses into real events.
2
+
3
+ The v14 view model has no Python constructors, so tests and tools can no longer
4
+ hand-build ``UserEvent``/``ToolUseBlock``/… objects. Instead they synthesize the
5
+ JSONL a real transcript carries and parse it through the native backend — the exact
6
+ path production uses. The ``*_line`` builders assemble the raw envelope dicts
7
+ (mirroring the fields Claude Code emits), the block builders emit the
8
+ ``message.content`` entries the parser lifts into block views, and
9
+ :func:`synthetic_user_event`/:func:`synthetic_assistant_event` round-trip a single
10
+ envelope through :func:`~cc_transcript.parser.parse_events` into one native event.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from datetime import UTC, datetime, timedelta
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ from cc_transcript.models import AssistantEvent, UserEvent
19
+ from cc_transcript.parser import parse_events
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Mapping, Sequence
23
+
24
+ BASE = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
25
+
26
+ Content = str | list[dict[str, Any]]
27
+
28
+
29
+ def text_block(text: str) -> dict[str, Any]:
30
+ """A ``text`` content block carrying ``text``."""
31
+ return {"type": "text", "text": text}
32
+
33
+
34
+ def thinking_block(thinking: str) -> dict[str, Any]:
35
+ """A ``thinking`` content block carrying ``thinking``."""
36
+ return {"type": "thinking", "thinking": thinking}
37
+
38
+
39
+ def tool_use(id: str, name: str, input: Mapping[str, Any]) -> dict[str, Any]:
40
+ """A ``tool_use`` content block invoking ``name`` with ``input`` under id ``id``."""
41
+ return {"type": "tool_use", "id": id, "name": name, "input": dict(input)}
42
+
43
+
44
+ def tool_result(tool_use_id: str, content: Content = "ok", *, is_error: bool = False) -> dict[str, Any]:
45
+ """A ``tool_result`` content block for ``tool_use_id``; ``is_error`` marks a failed call."""
46
+ return {"type": "tool_result", "tool_use_id": tool_use_id, "content": content, "is_error": is_error}
47
+
48
+
49
+ def meta_fields(
50
+ uuid: str,
51
+ *,
52
+ session_id: str = "s",
53
+ timestamp: datetime = BASE,
54
+ secs: int = 0,
55
+ parent_uuid: str | None = None,
56
+ cwd: str | None = "/repo",
57
+ git_branch: str | None = "main",
58
+ version: str | None = "1.2.3",
59
+ is_sidechain: bool = False,
60
+ is_meta: bool = False,
61
+ entrypoint: str | None = "cli",
62
+ is_compact_summary: bool = False,
63
+ is_visible_in_transcript_only: bool = False,
64
+ user_type: str | None = None,
65
+ slug: str | None = None,
66
+ ) -> dict[str, Any]:
67
+ """The envelope-level fields the parser lifts into :class:`~cc_transcript.models.EntryMeta`.
68
+
69
+ Optional string fields are omitted when ``None`` so the parser's ``.get`` chains
70
+ reproduce a ``None`` attribute; booleans always serialize.
71
+ """
72
+ fields: dict[str, Any] = {
73
+ "uuid": uuid,
74
+ "sessionId": session_id,
75
+ "timestamp": (timestamp + timedelta(seconds=secs)).isoformat(),
76
+ "isSidechain": is_sidechain,
77
+ "isMeta": is_meta,
78
+ "isCompactSummary": is_compact_summary,
79
+ "isVisibleInTranscriptOnly": is_visible_in_transcript_only,
80
+ }
81
+ for key, value in (
82
+ ("parentUuid", parent_uuid),
83
+ ("cwd", cwd),
84
+ ("gitBranch", git_branch),
85
+ ("version", version),
86
+ ("entrypoint", entrypoint),
87
+ ("userType", user_type),
88
+ ("slug", slug),
89
+ ):
90
+ if value is not None:
91
+ fields[key] = value
92
+ return fields
93
+
94
+
95
+ def user_line(
96
+ uuid: str,
97
+ text: str = "",
98
+ *,
99
+ blocks: Sequence[dict[str, Any]] = (),
100
+ tool_use_result: Mapping[str, Any] | str | None = None,
101
+ tool_denial_kind: str | None = None,
102
+ interrupted: bool = False,
103
+ **meta_kw: Any,
104
+ ) -> dict[str, Any]:
105
+ """A ``type: user`` envelope; ``blocks`` are ``message.content`` entries.
106
+
107
+ With ``blocks`` the content is a list — ``text`` (when non-empty) becomes a
108
+ leading text block, exactly as a real transcript carries it. ``interrupted``
109
+ sets ``interruptedMessageId`` so the parser derives ``interrupted=True``.
110
+ """
111
+ content: Content = ([text_block(text)] if text else []) + list(blocks) if blocks else text
112
+ line: dict[str, Any] = {"type": "user", "message": {"role": "user", "content": content}} | meta_fields(
113
+ uuid, **meta_kw
114
+ )
115
+ if tool_use_result is not None:
116
+ line["toolUseResult"] = tool_use_result
117
+ if tool_denial_kind is not None:
118
+ line["toolDenialKind"] = tool_denial_kind
119
+ if interrupted:
120
+ line["interruptedMessageId"] = f"imid-{uuid}"
121
+ return line
122
+
123
+
124
+ def assistant_line(
125
+ uuid: str,
126
+ text: str = "",
127
+ *,
128
+ model: str = "claude-opus-4-7",
129
+ blocks: Sequence[dict[str, Any]] = (),
130
+ stop_reason: str | None = None,
131
+ usage: Mapping[str, Any] | None = None,
132
+ **meta_kw: Any,
133
+ ) -> dict[str, Any]:
134
+ """An ``type: assistant`` envelope; ``blocks`` are ``message.content`` entries."""
135
+ content = ([text_block(text)] if text else []) + list(blocks)
136
+ message: dict[str, Any] = {"role": "assistant", "model": model, "content": content}
137
+ if stop_reason is not None:
138
+ message["stop_reason"] = stop_reason
139
+ if usage is not None:
140
+ message["usage"] = dict(usage)
141
+ return {"type": "assistant", "message": message} | meta_fields(uuid, **meta_kw)
142
+
143
+
144
+ def mode_line(value: str, *, session_id: str = "s", channel: str = "mode") -> dict[str, Any]:
145
+ """A ``mode``/``permission-mode`` envelope the parser lifts into :class:`~cc_transcript.models.ModeEvent`."""
146
+ key = "permissionMode" if channel == "permission-mode" else "mode"
147
+ return {"type": channel, "sessionId": session_id, key: value}
148
+
149
+
150
+ def system_line(subtype: str, *, content: str | None = None, level: str | None = None, **fields: Any) -> dict[str, Any]:
151
+ """A ``type: system`` envelope; ``fields`` carry the subtype-specific detail keys."""
152
+ line: dict[str, Any] = {"type": "system", "subtype": subtype} | meta_fields(fields.pop("uuid", "sys")) | fields
153
+ if content is not None:
154
+ line["content"] = content
155
+ if level is not None:
156
+ line["level"] = level
157
+ return line
158
+
159
+
160
+ def other_line(type: str, **fields: Any) -> dict[str, Any]:
161
+ """An unmodeled-``type`` envelope lifted into :class:`~cc_transcript.models.OtherEvent`; ``raw`` is the line."""
162
+ return {"type": type} | fields
163
+
164
+
165
+ def synthetic_user_event(
166
+ text: str = "",
167
+ *,
168
+ blocks: Sequence[Mapping[str, Any]] = (),
169
+ uuid: str = "synthetic",
170
+ **meta: Any,
171
+ ) -> UserEvent:
172
+ """Builds a native :class:`~cc_transcript.models.UserEvent` from text or content blocks.
173
+
174
+ Serializes a ``user`` envelope and parses it through the native backend, so the
175
+ result is a real view accepted anywhere parsed events are — including
176
+ :meth:`~cc_transcript.activity.SessionActivity.from_events`. Extra keywords forward
177
+ to the envelope metadata (``session_id``, ``timestamp``, …).
178
+
179
+ Example:
180
+ >>> synthetic_user_event("fix the bug", session_id="s1").text
181
+ 'fix the bug'
182
+
183
+ Raises:
184
+ ValueError: When the envelope does not parse to exactly one ``UserEvent``.
185
+ """
186
+ events = parse_events(user_line(uuid, text, blocks=[dict(block) for block in blocks], **meta))
187
+ match events:
188
+ case [UserEvent() as event]:
189
+ return event
190
+ raise ValueError(f"expected one UserEvent, got {[type(event).__name__ for event in events]}")
191
+
192
+
193
+ def synthetic_assistant_event(
194
+ text: str = "",
195
+ *,
196
+ blocks: Sequence[Mapping[str, Any]] = (),
197
+ uuid: str = "synthetic",
198
+ **meta: Any,
199
+ ) -> AssistantEvent:
200
+ """Builds a native :class:`~cc_transcript.models.AssistantEvent` from text or content blocks.
201
+
202
+ Serializes an ``assistant`` envelope and parses it through the native backend, so
203
+ the result is a real view accepted anywhere parsed events are — including
204
+ :meth:`~cc_transcript.activity.SessionActivity.from_events`. Extra keywords forward
205
+ to the envelope metadata (``session_id``, ``timestamp``, …) and message fields
206
+ (``stop_reason``, ``usage``, ``model``).
207
+
208
+ Example:
209
+ >>> synthetic_assistant_event("on it", stop_reason="end_turn").text
210
+ 'on it'
211
+
212
+ Raises:
213
+ ValueError: When the envelope does not parse to exactly one ``AssistantEvent``.
214
+ """
215
+ events = parse_events(assistant_line(uuid, text, blocks=[dict(block) for block in blocks], **meta))
216
+ match events:
217
+ case [AssistantEvent() as event]:
218
+ return event
219
+ raise ValueError(f"expected one AssistantEvent, got {[type(event).__name__ for event in events]}")