cc-transcript 7.0.2__tar.gz → 8.0.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 (71) hide show
  1. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/Cargo.lock +53 -1
  2. cc_transcript-8.0.0/PKG-INFO +123 -0
  3. cc_transcript-8.0.0/README.md +83 -0
  4. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/__init__.py +72 -26
  5. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/_parser_rs.pyi +3 -0
  6. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/activity.py +44 -22
  7. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/cli.py +141 -2
  8. cc_transcript-8.0.0/cc_transcript/command.py +516 -0
  9. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/context.py +3 -2
  10. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/discovery.py +15 -0
  11. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/evidence.py +4 -3
  12. cc_transcript-8.0.0/cc_transcript/facts.py +186 -0
  13. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/filterspec.py +28 -3
  14. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/judge/__init__.py +1 -0
  15. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/judge/llm.py +15 -2
  16. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/judge/verdicts.py +19 -5
  17. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/mining/__init__.py +1 -3
  18. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/mining/signals.py +34 -40
  19. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/mining/spec.py +24 -8
  20. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/models.py +10 -0
  21. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/parser.py +2 -7
  22. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/query.py +28 -17
  23. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/render.py +69 -6
  24. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/sentiment/__init__.py +10 -7
  25. cc_transcript-8.0.0/cc_transcript/sentiment/buckets.py +106 -0
  26. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/sentiment/engine.py +2 -1
  27. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/sentiment/lexicon.py +12 -27
  28. cc_transcript-8.0.0/cc_transcript/tools.py +555 -0
  29. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/pyproject.toml +9 -8
  30. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/rust/Cargo.toml +5 -1
  31. cc_transcript-8.0.0/rust/src/command.rs +263 -0
  32. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/rust/src/event.rs +2 -3
  33. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/rust/src/filter.rs +3 -2
  34. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/rust/src/lib.rs +9 -1
  35. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/rust/src/mining.rs +106 -129
  36. cc_transcript-8.0.0/rust/src/protocol.rs +92 -0
  37. cc_transcript-7.0.2/PKG-INFO +0 -149
  38. cc_transcript-7.0.2/README.md +0 -111
  39. cc_transcript-7.0.2/cc_transcript/messages.py +0 -43
  40. cc_transcript-7.0.2/cc_transcript/sentiment/buckets.py +0 -90
  41. cc_transcript-7.0.2/cc_transcript/tools.py +0 -386
  42. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/Cargo.toml +0 -0
  43. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/LICENSE +0 -0
  44. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/__main__.py +0 -0
  45. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/backend.py +0 -0
  46. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/builders.py +0 -0
  47. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/corrections.py +0 -0
  48. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/corrections_cli.py +0 -0
  49. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/cost.py +0 -0
  50. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/decisions.py +0 -0
  51. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/disktruth.py +0 -0
  52. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/extract/__init__.py +0 -0
  53. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/extract/correct.py +0 -0
  54. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/ids.py +0 -0
  55. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/mining/candidates.py +0 -0
  56. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/mining/confidence.py +0 -0
  57. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/mining/engine.py +0 -0
  58. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/mining/filterspec.py +0 -0
  59. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/mining/formats.py +0 -0
  60. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/mining/sourcekind.py +0 -0
  61. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/mining/store.py +0 -0
  62. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/py.typed +0 -0
  63. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/rust.py +0 -0
  64. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/sentiment/scorespec.py +0 -0
  65. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/cc_transcript/store.py +0 -0
  66. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/rust/data/afinn-en-165.tsv +0 -0
  67. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/rust/data/domain_overrides.tsv +0 -0
  68. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/rust/src/lexicon.rs +0 -0
  69. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/rust/src/model.rs +0 -0
  70. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/rust/src/score.rs +0 -0
  71. {cc_transcript-7.0.2 → cc_transcript-8.0.0}/rust/src/value.rs +0 -0
@@ -66,7 +66,7 @@ dependencies = [
66
66
 
67
67
  [[package]]
68
68
  name = "cc_transcript_parser"
69
- version = "0.7.0"
69
+ version = "0.8.0"
70
70
  dependencies = [
71
71
  "chrono",
72
72
  "crossbeam-channel",
@@ -79,6 +79,8 @@ dependencies = [
79
79
  "regex",
80
80
  "serde",
81
81
  "sonic-rs",
82
+ "tree-sitter",
83
+ "tree-sitter-bash",
82
84
  "udpipe-rs",
83
85
  ]
84
86
 
@@ -728,6 +730,20 @@ dependencies = [
728
730
  "syn",
729
731
  ]
730
732
 
733
+ [[package]]
734
+ name = "serde_json"
735
+ version = "1.0.150"
736
+ source = "registry+https://github.com/rust-lang/crates.io-index"
737
+ checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
738
+ dependencies = [
739
+ "indexmap",
740
+ "itoa",
741
+ "memchr",
742
+ "serde",
743
+ "serde_core",
744
+ "zmij",
745
+ ]
746
+
731
747
  [[package]]
732
748
  name = "shlex"
733
749
  version = "2.0.1"
@@ -791,6 +807,12 @@ dependencies = [
791
807
  "cfg-if",
792
808
  ]
793
809
 
810
+ [[package]]
811
+ name = "streaming-iterator"
812
+ version = "0.1.9"
813
+ source = "registry+https://github.com/rust-lang/crates.io-index"
814
+ checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
815
+
794
816
  [[package]]
795
817
  name = "subtle"
796
818
  version = "2.6.1"
@@ -869,6 +891,36 @@ version = "0.1.1"
869
891
  source = "registry+https://github.com/rust-lang/crates.io-index"
870
892
  checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
871
893
 
894
+ [[package]]
895
+ name = "tree-sitter"
896
+ version = "0.26.10"
897
+ source = "registry+https://github.com/rust-lang/crates.io-index"
898
+ checksum = "3c343ed63e3f5c64d1acdecb5d2c13d4e169cb5fde0052106ebaa6c6f27f9e55"
899
+ dependencies = [
900
+ "cc",
901
+ "regex",
902
+ "regex-syntax",
903
+ "serde_json",
904
+ "streaming-iterator",
905
+ "tree-sitter-language",
906
+ ]
907
+
908
+ [[package]]
909
+ name = "tree-sitter-bash"
910
+ version = "0.25.1"
911
+ source = "registry+https://github.com/rust-lang/crates.io-index"
912
+ checksum = "9e5ec769279cc91b561d3df0d8a5deb26b0ad40d183127f409494d6d8fc53062"
913
+ dependencies = [
914
+ "cc",
915
+ "tree-sitter-language",
916
+ ]
917
+
918
+ [[package]]
919
+ name = "tree-sitter-language"
920
+ version = "0.1.7"
921
+ source = "registry+https://github.com/rust-lang/crates.io-index"
922
+ checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782"
923
+
872
924
  [[package]]
873
925
  name = "udpipe-rs"
874
926
  version = "0.2.0"
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: cc-transcript
3
+ Version: 8.0.0
4
+ Classifier: Development Status :: 3 - Alpha
5
+ Classifier: Environment :: Console
6
+ Classifier: Intended Audience :: Developers
7
+ Classifier: Operating System :: OS Independent
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Programming Language :: Rust
11
+ Classifier: Typing :: Typed
12
+ Requires-Dist: aiosqlite>=0.20
13
+ Requires-Dist: anyio>=4.4
14
+ Requires-Dist: click>=8.1
15
+ Requires-Dist: orjson>=3.10
16
+ Requires-Dist: tree-sitter>=0.26
17
+ Requires-Dist: tree-sitter-bash>=0.25
18
+ Requires-Dist: pytest>=8.0 ; extra == 'dev'
19
+ Requires-Dist: ty>=0.0.44 ; extra == 'dev'
20
+ Requires-Dist: ruff>=0.8 ; extra == 'dev'
21
+ Requires-Dist: spawnllm>=0.5.1,<0.6 ; extra == 'llm'
22
+ Requires-Dist: spacy>=3.8 ; extra == 'sentiment'
23
+ Requires-Dist: afinn>=0.1 ; extra == 'sentiment'
24
+ Provides-Extra: dev
25
+ Provides-Extra: llm
26
+ Provides-Extra: sentiment
27
+ License-File: LICENSE
28
+ Summary: Grep every Claude Code session you've ever run.
29
+ Keywords: claude-code,claude,anthropic,transcripts,jsonl,parser,cli,agents,sentiment,feedback-mining
30
+ Author-email: Yasyf Mohamedali <yasyfm@gmail.com>
31
+ License-Expression: PolyForm-Noncommercial-1.0.0
32
+ Requires-Python: >=3.13
33
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
34
+ Project-URL: Changelog, https://github.com/yasyf/cc-transcript/blob/main/CHANGELOG.md
35
+ Project-URL: Documentation, https://yasyf.github.io/cc-transcript/
36
+ Project-URL: Homepage, https://github.com/yasyf/cc-transcript
37
+ Project-URL: Issues, https://github.com/yasyf/cc-transcript/issues
38
+ Project-URL: Repository, https://github.com/yasyf/cc-transcript
39
+
40
+ # ![cc-transcript](https://github.com/yasyf/cc-transcript/raw/main/docs/assets/readme-banner.webp)
41
+
42
+ **Grep every Claude Code session you've ever run.** cc-transcript's lossless parser reads Claude Code's on-disk JSONL; the CLI lists transcripts, greps their content, and renders one compact line per event.
43
+
44
+ [![CI](https://github.com/yasyf/cc-transcript/actions/workflows/ci.yml/badge.svg)](https://github.com/yasyf/cc-transcript/actions/workflows/ci.yml)
45
+ [![PyPI](https://img.shields.io/pypi/v/cc-transcript.svg)](https://pypi.org/project/cc-transcript/)
46
+ [![License: PolyForm Noncommercial](https://img.shields.io/badge/license-PolyForm--Noncommercial--1.0.0-blue)](https://github.com/yasyf/cc-transcript/blob/main/LICENSE)
47
+
48
+ ## Get started
49
+
50
+ ```bash
51
+ uvx cc-transcript list
52
+ ```
53
+
54
+ Every session is already on disk under `~/.claude/projects` — `list` finds them newest first, and `stats` collapses one into a screenful:
55
+
56
+ <img src="https://github.com/yasyf/cc-transcript/raw/main/docs/assets/demo.png" alt="Terminal running 'uvx cc-transcript stats' — a 1,023-event session summarized as counts, kinds, models, and tool names" width="700">
57
+
58
+ Driving with an agent? Paste this:
59
+
60
+ ```text
61
+ /plugin marketplace add yasyf/cc-transcript
62
+ /plugin install cc-transcript@cc-transcript
63
+ ```
64
+
65
+ The plugin's skill teaches Claude to answer "what did I ask you yesterday?" from its own history, funneling through the CLI instead of reading raw JSONL.
66
+
67
+ <details>
68
+ <summary>No Claude Code? Point any agent at the CLI</summary>
69
+
70
+ ```text
71
+ Use `uvx cc-transcript` to investigate my Claude Code sessions: `list` finds
72
+ transcripts on disk, `stats` summarizes one, `grep` searches content, and `show`
73
+ renders one compact line per event. Start by listing my newest sessions and
74
+ summarizing the most recent one. Docs: https://yasyf.github.io/cc-transcript/
75
+ ```
76
+
77
+ </details>
78
+
79
+ ---
80
+
81
+ ## Use cases
82
+
83
+ ### Ask Claude what you asked Claude yesterday
84
+
85
+ Yesterday's session is a megabyte of JSONL, and pasting it into a chat blows the context window. Render the spine instead:
86
+
87
+ ```bash
88
+ uvx cc-transcript show --signal --tail 20 ~/.claude/projects/<project>/<session>.jsonl
89
+ ```
90
+
91
+ `--signal` keeps only the substantive user and assistant turns — one line per event, a few hundred tokens for the whole exchange. With the plugin installed, skip the command: ask Claude directly and its skill runs this funnel for you.
92
+
93
+ ### Find the session where you fixed that bug
94
+
95
+ The fix happened weeks ago, in one of a few hundred transcripts. Search them all:
96
+
97
+ ```bash
98
+ uvx cc-transcript grep "TranscriptExpiredError" --project cc-transcript
99
+ ```
100
+
101
+ Hits print under their transcript's path, each line carrying its raw event index — feed an index back into `show --range` to re-read the conversation around the fix. `grep` searches your newest 50 transcripts by default; `--all` takes it to every session on disk, and a no-match run exits 1 so it scripts cleanly.
102
+
103
+ ### Debug a token blowup from transcript evidence
104
+
105
+ A session burned through the context window and you want the culprit, not a guess. Rank the evidence:
106
+
107
+ ```bash
108
+ uvx cc-transcript stats --per-file --project myapp
109
+ ```
110
+
111
+ Each block reports text, thinking, and tool-io bytes plus per-tool call counts — the blowup stands out as an outsized `tool io` line. Then `grep --tool <name> --with-result` pins it to the exact calls.
112
+
113
+ ## More in the docs
114
+
115
+ - **The Python library** — raw JSONL bytes to typed events, a composed filter, and a score in a dozen lines — [getting started](https://yasyf.github.io/cc-transcript/docs/getting-started/index.html)
116
+ - **Filtering events** — composable clauses, specs, and the ready-made `NOISE_SPEC` — [builder catalog](https://yasyf.github.io/cc-transcript/docs/guide/filtering-events.html)
117
+ - **Sentiment scoring** — bucket conversations and score them around any inference engine — [score specs](https://yasyf.github.io/cc-transcript/docs/guide/scoring-sentiment.html)
118
+ - **Feedback mining** — detectors, confidence calibration, and LLM verdict passes over your corpus — [mine your sessions](https://yasyf.github.io/cc-transcript/docs/guide/mining-feedback.html)
119
+ - **Two backends, one protocol** — a Rust fast path and a Python reference, parity-asserted against each other — [how parity works](https://yasyf.github.io/cc-transcript/docs/guide/backends-and-parity.html)
120
+ - **API reference** — the complete typed surface, from `TranscriptEvent` to `SessionActivity` — [reference](https://yasyf.github.io/cc-transcript/reference/index.html)
121
+
122
+ Read the [docs](https://yasyf.github.io/cc-transcript/) for the full guide. Licensed under [PolyForm Noncommercial 1.0.0](https://github.com/yasyf/cc-transcript/blob/main/LICENSE).
123
+
@@ -0,0 +1,83 @@
1
+ # ![cc-transcript](https://github.com/yasyf/cc-transcript/raw/main/docs/assets/readme-banner.webp)
2
+
3
+ **Grep every Claude Code session you've ever run.** cc-transcript's lossless parser reads Claude Code's on-disk JSONL; the CLI lists transcripts, greps their content, and renders one compact line per event.
4
+
5
+ [![CI](https://github.com/yasyf/cc-transcript/actions/workflows/ci.yml/badge.svg)](https://github.com/yasyf/cc-transcript/actions/workflows/ci.yml)
6
+ [![PyPI](https://img.shields.io/pypi/v/cc-transcript.svg)](https://pypi.org/project/cc-transcript/)
7
+ [![License: PolyForm Noncommercial](https://img.shields.io/badge/license-PolyForm--Noncommercial--1.0.0-blue)](https://github.com/yasyf/cc-transcript/blob/main/LICENSE)
8
+
9
+ ## Get started
10
+
11
+ ```bash
12
+ uvx cc-transcript list
13
+ ```
14
+
15
+ Every session is already on disk under `~/.claude/projects` — `list` finds them newest first, and `stats` collapses one into a screenful:
16
+
17
+ <img src="https://github.com/yasyf/cc-transcript/raw/main/docs/assets/demo.png" alt="Terminal running 'uvx cc-transcript stats' — a 1,023-event session summarized as counts, kinds, models, and tool names" width="700">
18
+
19
+ Driving with an agent? Paste this:
20
+
21
+ ```text
22
+ /plugin marketplace add yasyf/cc-transcript
23
+ /plugin install cc-transcript@cc-transcript
24
+ ```
25
+
26
+ The plugin's skill teaches Claude to answer "what did I ask you yesterday?" from its own history, funneling through the CLI instead of reading raw JSONL.
27
+
28
+ <details>
29
+ <summary>No Claude Code? Point any agent at the CLI</summary>
30
+
31
+ ```text
32
+ Use `uvx cc-transcript` to investigate my Claude Code sessions: `list` finds
33
+ transcripts on disk, `stats` summarizes one, `grep` searches content, and `show`
34
+ renders one compact line per event. Start by listing my newest sessions and
35
+ summarizing the most recent one. Docs: https://yasyf.github.io/cc-transcript/
36
+ ```
37
+
38
+ </details>
39
+
40
+ ---
41
+
42
+ ## Use cases
43
+
44
+ ### Ask Claude what you asked Claude yesterday
45
+
46
+ Yesterday's session is a megabyte of JSONL, and pasting it into a chat blows the context window. Render the spine instead:
47
+
48
+ ```bash
49
+ uvx cc-transcript show --signal --tail 20 ~/.claude/projects/<project>/<session>.jsonl
50
+ ```
51
+
52
+ `--signal` keeps only the substantive user and assistant turns — one line per event, a few hundred tokens for the whole exchange. With the plugin installed, skip the command: ask Claude directly and its skill runs this funnel for you.
53
+
54
+ ### Find the session where you fixed that bug
55
+
56
+ The fix happened weeks ago, in one of a few hundred transcripts. Search them all:
57
+
58
+ ```bash
59
+ uvx cc-transcript grep "TranscriptExpiredError" --project cc-transcript
60
+ ```
61
+
62
+ Hits print under their transcript's path, each line carrying its raw event index — feed an index back into `show --range` to re-read the conversation around the fix. `grep` searches your newest 50 transcripts by default; `--all` takes it to every session on disk, and a no-match run exits 1 so it scripts cleanly.
63
+
64
+ ### Debug a token blowup from transcript evidence
65
+
66
+ A session burned through the context window and you want the culprit, not a guess. Rank the evidence:
67
+
68
+ ```bash
69
+ uvx cc-transcript stats --per-file --project myapp
70
+ ```
71
+
72
+ Each block reports text, thinking, and tool-io bytes plus per-tool call counts — the blowup stands out as an outsized `tool io` line. Then `grep --tool <name> --with-result` pins it to the exact calls.
73
+
74
+ ## More in the docs
75
+
76
+ - **The Python library** — raw JSONL bytes to typed events, a composed filter, and a score in a dozen lines — [getting started](https://yasyf.github.io/cc-transcript/docs/getting-started/index.html)
77
+ - **Filtering events** — composable clauses, specs, and the ready-made `NOISE_SPEC` — [builder catalog](https://yasyf.github.io/cc-transcript/docs/guide/filtering-events.html)
78
+ - **Sentiment scoring** — bucket conversations and score them around any inference engine — [score specs](https://yasyf.github.io/cc-transcript/docs/guide/scoring-sentiment.html)
79
+ - **Feedback mining** — detectors, confidence calibration, and LLM verdict passes over your corpus — [mine your sessions](https://yasyf.github.io/cc-transcript/docs/guide/mining-feedback.html)
80
+ - **Two backends, one protocol** — a Rust fast path and a Python reference, parity-asserted against each other — [how parity works](https://yasyf.github.io/cc-transcript/docs/guide/backends-and-parity.html)
81
+ - **API reference** — the complete typed surface, from `TranscriptEvent` to `SessionActivity` — [reference](https://yasyf.github.io/cc-transcript/reference/index.html)
82
+
83
+ Read the [docs](https://yasyf.github.io/cc-transcript/) for the full guide. Licensed under [PolyForm Noncommercial 1.0.0](https://github.com/yasyf/cc-transcript/blob/main/LICENSE).
@@ -48,13 +48,25 @@ EXPORTS: dict[str, str] = {
48
48
  "ToolCall",
49
49
  "ToolCallBase",
50
50
  "ToolInputError",
51
+ "WorkflowCall",
51
52
  "WriteCall",
52
53
  "expand_tool_names",
53
54
  "file_path_of",
54
55
  "hunks_of",
56
+ "matches_names",
57
+ "mcp_access",
58
+ "mcp_parts",
55
59
  "parse_tool_call",
56
60
  "tool_name_matches",
57
61
  ),
62
+ "cc_transcript.command": (
63
+ "Command",
64
+ "CommandLine",
65
+ "CommandLineQuery",
66
+ "Redirect",
67
+ "command_prefixes",
68
+ "parse_command_line",
69
+ ),
58
70
  "cc_transcript.models": (
59
71
  "AssistantEvent",
60
72
  "CacheCreation",
@@ -80,6 +92,8 @@ EXPORTS: dict[str, str] = {
80
92
  "TranscriptEvent",
81
93
  "Usage",
82
94
  "UserEvent",
95
+ "thinking_chars",
96
+ "tool_uses",
83
97
  ),
84
98
  "cc_transcript.parser": (
85
99
  "TranscriptParser",
@@ -113,6 +127,13 @@ EXPORTS: dict[str, str] = {
113
127
  "UserClassifier",
114
128
  "hunk_overlap",
115
129
  "native_user_classifier",
130
+ "result_index",
131
+ ),
132
+ "cc_transcript.facts": (
133
+ "ToolFact",
134
+ "command_prefix_counts",
135
+ "mcp_summary",
136
+ "tool_facts",
116
137
  ),
117
138
  "cc_transcript.evidence": (
118
139
  "CandidatePair",
@@ -152,7 +173,6 @@ EXPORTS: dict[str, str] = {
152
173
  "INTERRUPT_MARKER_RE",
153
174
  "JUNK_USER_MESSAGE_RE",
154
175
  "RESUME_PHRASE_SET",
155
- "STOP_HOOK_RE",
156
176
  "STRUCTURAL_NOISE_RE",
157
177
  "TRIVIAL_ACK_SET",
158
178
  "USERS",
@@ -176,13 +196,6 @@ EXPORTS: dict[str, str] = {
176
196
  "drop_synthetic",
177
197
  "keep_only",
178
198
  ),
179
- "cc_transcript.messages": (
180
- "AssistantMessage",
181
- "BaseMessage",
182
- "MessageToolCall",
183
- "TranscriptMessage",
184
- "UserMessage",
185
- ),
186
199
  "cc_transcript.store": ("FileStateStore",),
187
200
  }.items()
188
201
  for name in names
@@ -221,6 +234,9 @@ if TYPE_CHECKING:
221
234
  from cc_transcript.activity import (
222
235
  native_user_classifier as native_user_classifier,
223
236
  )
237
+ from cc_transcript.activity import (
238
+ result_index as result_index,
239
+ )
224
240
  from cc_transcript.backend import Backend as Backend
225
241
  from cc_transcript.backend import ParsedTranscript as ParsedTranscript
226
242
  from cc_transcript.builders import (
@@ -259,6 +275,24 @@ if TYPE_CHECKING:
259
275
  from cc_transcript.builders import (
260
276
  keep_only as keep_only,
261
277
  )
278
+ from cc_transcript.command import (
279
+ Command as Command,
280
+ )
281
+ from cc_transcript.command import (
282
+ CommandLine as CommandLine,
283
+ )
284
+ from cc_transcript.command import (
285
+ CommandLineQuery as CommandLineQuery,
286
+ )
287
+ from cc_transcript.command import (
288
+ Redirect as Redirect,
289
+ )
290
+ from cc_transcript.command import (
291
+ command_prefixes as command_prefixes,
292
+ )
293
+ from cc_transcript.command import (
294
+ parse_command_line as parse_command_line,
295
+ )
262
296
  from cc_transcript.context import (
263
297
  ContextWindow as ContextWindow,
264
298
  )
@@ -373,6 +407,18 @@ if TYPE_CHECKING:
373
407
  from cc_transcript.evidence import (
374
408
  record_harvest as record_harvest,
375
409
  )
410
+ from cc_transcript.facts import (
411
+ ToolFact as ToolFact,
412
+ )
413
+ from cc_transcript.facts import (
414
+ command_prefix_counts as command_prefix_counts,
415
+ )
416
+ from cc_transcript.facts import (
417
+ mcp_summary as mcp_summary,
418
+ )
419
+ from cc_transcript.facts import (
420
+ tool_facts as tool_facts,
421
+ )
376
422
  from cc_transcript.filterspec import (
377
423
  ASSISTANTS as ASSISTANTS,
378
424
  )
@@ -385,9 +431,6 @@ if TYPE_CHECKING:
385
431
  from cc_transcript.filterspec import (
386
432
  RESUME_PHRASE_SET as RESUME_PHRASE_SET,
387
433
  )
388
- from cc_transcript.filterspec import (
389
- STOP_HOOK_RE as STOP_HOOK_RE,
390
- )
391
434
  from cc_transcript.filterspec import (
392
435
  STRUCTURAL_NOISE_RE as STRUCTURAL_NOISE_RE,
393
436
  )
@@ -433,21 +476,6 @@ if TYPE_CHECKING:
433
476
  from cc_transcript.ids import (
434
477
  tool_digest as tool_digest,
435
478
  )
436
- from cc_transcript.messages import (
437
- AssistantMessage as AssistantMessage,
438
- )
439
- from cc_transcript.messages import (
440
- BaseMessage as BaseMessage,
441
- )
442
- from cc_transcript.messages import (
443
- MessageToolCall as MessageToolCall,
444
- )
445
- from cc_transcript.messages import (
446
- TranscriptMessage as TranscriptMessage,
447
- )
448
- from cc_transcript.messages import (
449
- UserMessage as UserMessage,
450
- )
451
479
  from cc_transcript.models import (
452
480
  AssistantEvent as AssistantEvent,
453
481
  )
@@ -520,6 +548,12 @@ if TYPE_CHECKING:
520
548
  from cc_transcript.models import (
521
549
  UserEvent as UserEvent,
522
550
  )
551
+ from cc_transcript.models import (
552
+ thinking_chars as thinking_chars,
553
+ )
554
+ from cc_transcript.models import (
555
+ tool_uses as tool_uses,
556
+ )
523
557
  from cc_transcript.parser import (
524
558
  TranscriptParser as TranscriptParser,
525
559
  )
@@ -620,6 +654,9 @@ if TYPE_CHECKING:
620
654
  from cc_transcript.tools import (
621
655
  ToolInputError as ToolInputError,
622
656
  )
657
+ from cc_transcript.tools import (
658
+ WorkflowCall as WorkflowCall,
659
+ )
623
660
  from cc_transcript.tools import (
624
661
  WriteCall as WriteCall,
625
662
  )
@@ -632,6 +669,15 @@ if TYPE_CHECKING:
632
669
  from cc_transcript.tools import (
633
670
  hunks_of as hunks_of,
634
671
  )
672
+ from cc_transcript.tools import (
673
+ matches_names as matches_names,
674
+ )
675
+ from cc_transcript.tools import (
676
+ mcp_access as mcp_access,
677
+ )
678
+ from cc_transcript.tools import (
679
+ mcp_parts as mcp_parts,
680
+ )
635
681
  from cc_transcript.tools import (
636
682
  parse_tool_call as parse_tool_call,
637
683
  )
@@ -38,3 +38,6 @@ def score_short_circuit(spec_json: str, buckets: list[list[str]], /) -> list[int
38
38
 
39
39
  def score_post_process(spec_json: str, buckets: list[list[str]], raw: list[int], /) -> list[int]:
40
40
  """Folds each bucket's raw score through the post-process stages in order."""
41
+
42
+ def command_prefixes(commands: list[str], /) -> list[list[str]]:
43
+ """Permission-style prefixes per command line, parsed in parallel off the GIL."""
@@ -17,8 +17,9 @@ from dataclasses import dataclass
17
17
  from typing import TYPE_CHECKING
18
18
 
19
19
  from cc_transcript.discovery import TranscriptDiscovery, TranscriptExpiredError, find_transcript
20
+ from cc_transcript.filterspec import event_meta
20
21
  from cc_transcript.ids import EventRef
21
- from cc_transcript.models import AssistantEvent, SystemEvent, ToolResultBlock, ToolUseBlock, UserEvent
22
+ from cc_transcript.models import AssistantEvent, ToolResultBlock, ToolUseBlock, UserEvent
22
23
  from cc_transcript.parser import TranscriptParser
23
24
  from cc_transcript.tools import file_path_of, hunks_of, parse_tool_call
24
25
 
@@ -28,7 +29,7 @@ if TYPE_CHECKING:
28
29
  from pathlib import Path
29
30
 
30
31
  from cc_transcript.ids import SessionId, ToolUseId
31
- from cc_transcript.models import EntryMeta, TranscriptEvent
32
+ from cc_transcript.models import TranscriptEvent
32
33
  from cc_transcript.tools import Hunk, ToolCall
33
34
 
34
35
  UserClassifier = Callable[["UserEvent"], bool]
@@ -52,6 +53,8 @@ class ToolUse:
52
53
  ref: The resolvable reference to the tool-use block.
53
54
  call: The typed tool call, constructed once at lift time.
54
55
  result: The matching result block, or None when none ever arrived.
56
+ result_ts: The timestamp of the user entry carrying the result, or
57
+ None when no result ever arrived.
55
58
  turn_index: The index of the turn the call fired in.
56
59
  ts: The timestamp of the assistant entry carrying the call.
57
60
  """
@@ -59,9 +62,15 @@ class ToolUse:
59
62
  ref: EventRef
60
63
  call: ToolCall
61
64
  result: ToolResultBlock | None
65
+ result_ts: datetime | None
62
66
  turn_index: int
63
67
  ts: datetime
64
68
 
69
+ @property
70
+ def duration_ms(self) -> int | None:
71
+ """Milliseconds from the call to its result, or None without a result."""
72
+ return None if self.result_ts is None else round((self.result_ts - self.ts).total_seconds() * 1000)
73
+
65
74
 
66
75
  @dataclass(frozen=True, slots=True)
67
76
  class Edit:
@@ -157,13 +166,7 @@ class SessionActivity:
157
166
  segments[-1][1].append(event)
158
167
  case _:
159
168
  segments.append(("", [event]))
160
- results = {
161
- block.tool_use_id: block
162
- for event in events
163
- if isinstance(event, UserEvent)
164
- for block in event.blocks
165
- if isinstance(block, ToolResultBlock)
166
- }
169
+ results = result_index(events)
167
170
  return cls(
168
171
  session_id=session_id,
169
172
  turns=tuple(
@@ -218,7 +221,7 @@ class SessionActivity:
218
221
  turn
219
222
  for turn in self.turns
220
223
  for event in turn.events
221
- if (meta := meta_of(event)) is not None and meta.uuid == ref.event_uuid
224
+ if (meta := event_meta(event)) is not None and meta.uuid == ref.event_uuid
222
225
  ),
223
226
  None,
224
227
  )
@@ -274,12 +277,30 @@ def hunk_overlap(a: Hunk, b: Hunk) -> float:
274
277
  return sum(line in olds for line in lines) / len(lines)
275
278
 
276
279
 
277
- def meta_of(event: TranscriptEvent) -> EntryMeta | None:
278
- match event:
279
- case UserEvent() | AssistantEvent() | SystemEvent():
280
- return event.meta
281
- case _:
282
- return None
280
+ def result_index(events: Sequence[TranscriptEvent]) -> dict[ToolUseId, tuple[ToolResultBlock, datetime | None]]:
281
+ """Indexes tool results by the id of the tool use they answer.
282
+
283
+ Pairs each :class:`~cc_transcript.models.ToolResultBlock` with the
284
+ timestamp of the ``UserEvent`` carrying it, keyed by the block's
285
+ ``tool_use_id`` — the single source for joining a call to its result and
286
+ deriving the call's duration.
287
+
288
+ Returns:
289
+ A mapping from tool-use id to a ``(result block, result timestamp)``
290
+ pair.
291
+ """
292
+ return {
293
+ block.tool_use_id: (block, event.meta.timestamp)
294
+ for event in events
295
+ if isinstance(event, UserEvent)
296
+ for block in event.blocks
297
+ if isinstance(block, ToolResultBlock)
298
+ }
299
+
300
+
301
+ def event_stamps(events: Sequence[TranscriptEvent]) -> tuple[datetime | None, datetime | None]:
302
+ stamps = [meta.timestamp for event in events if (meta := event_meta(event)) is not None]
303
+ return (stamps[0], stamps[-1]) if stamps else (None, None)
283
304
 
284
305
 
285
306
  def lift_turn(
@@ -287,20 +308,21 @@ def lift_turn(
287
308
  index: int,
288
309
  prompt: str,
289
310
  events: tuple[TranscriptEvent, ...],
290
- results: Mapping[ToolUseId, ToolResultBlock],
311
+ results: Mapping[ToolUseId, tuple[ToolResultBlock, datetime | None]],
291
312
  ) -> Turn:
292
- stamps = [meta.timestamp for event in events if (meta := meta_of(event)) is not None]
313
+ started_at, ended_at = event_stamps(events)
293
314
  return Turn(
294
315
  index=index,
295
316
  prompt=prompt,
296
- started_at=stamps[0] if stamps else None,
297
- ended_at=stamps[-1] if stamps else None,
317
+ started_at=started_at,
318
+ ended_at=ended_at,
298
319
  events=events,
299
320
  tool_uses=tuple(
300
321
  ToolUse(
301
322
  ref=EventRef(session_id, event.meta.uuid, block.id),
302
323
  call=parse_tool_call(block.name, block.input, on_error="other"),
303
- result=results.get(block.id),
324
+ result=pair[0] if (pair := results.get(block.id)) is not None else None,
325
+ result_ts=pair[1] if pair is not None else None,
304
326
  turn_index=index,
305
327
  ts=event.meta.timestamp,
306
328
  )
@@ -316,7 +338,7 @@ def position_in(turn: Turn, ref: EventRef) -> tuple[int, int]:
316
338
  event_pos, event = next(
317
339
  (i, event)
318
340
  for i, event in enumerate(turn.events)
319
- if (meta := meta_of(event)) is not None and meta.uuid == ref.event_uuid
341
+ if (meta := event_meta(event)) is not None and meta.uuid == ref.event_uuid
320
342
  )
321
343
  match ref.tool_use_id, event:
322
344
  case None, _: