cc-transcript 7.1.0__tar.gz → 8.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 (71) hide show
  1. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/Cargo.lock +53 -1
  2. cc_transcript-8.1.0/PKG-INFO +123 -0
  3. cc_transcript-8.1.0/README.md +83 -0
  4. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/__init__.py +68 -26
  5. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/_parser_rs.pyi +3 -0
  6. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/activity.py +44 -22
  7. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/cli.py +141 -2
  8. cc_transcript-8.1.0/cc_transcript/command.py +516 -0
  9. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/context.py +3 -2
  10. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/discovery.py +15 -0
  11. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/evidence.py +4 -3
  12. cc_transcript-8.1.0/cc_transcript/facts.py +186 -0
  13. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/filterspec.py +32 -4
  14. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/judge/__init__.py +1 -0
  15. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/judge/llm.py +15 -2
  16. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/judge/verdicts.py +19 -5
  17. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/mining/__init__.py +8 -3
  18. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/mining/signals.py +195 -40
  19. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/mining/sourcekind.py +2 -1
  20. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/mining/spec.py +27 -9
  21. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/models.py +10 -0
  22. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/parser.py +2 -7
  23. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/query.py +28 -17
  24. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/render.py +69 -6
  25. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/sentiment/__init__.py +10 -7
  26. cc_transcript-8.1.0/cc_transcript/sentiment/buckets.py +106 -0
  27. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/sentiment/engine.py +2 -1
  28. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/sentiment/lexicon.py +12 -27
  29. cc_transcript-8.1.0/cc_transcript/tools.py +555 -0
  30. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/pyproject.toml +10 -9
  31. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/rust/Cargo.toml +5 -1
  32. cc_transcript-8.1.0/rust/src/command.rs +263 -0
  33. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/rust/src/event.rs +2 -3
  34. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/rust/src/filter.rs +3 -2
  35. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/rust/src/lib.rs +9 -1
  36. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/rust/src/mining.rs +387 -124
  37. cc_transcript-8.1.0/rust/src/protocol.rs +94 -0
  38. cc_transcript-7.1.0/PKG-INFO +0 -149
  39. cc_transcript-7.1.0/README.md +0 -111
  40. cc_transcript-7.1.0/cc_transcript/messages.py +0 -43
  41. cc_transcript-7.1.0/cc_transcript/sentiment/buckets.py +0 -90
  42. cc_transcript-7.1.0/cc_transcript/tools.py +0 -418
  43. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/Cargo.toml +0 -0
  44. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/LICENSE +0 -0
  45. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/__main__.py +0 -0
  46. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/backend.py +0 -0
  47. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/builders.py +0 -0
  48. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/corrections.py +0 -0
  49. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/corrections_cli.py +0 -0
  50. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/cost.py +0 -0
  51. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/decisions.py +0 -0
  52. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/disktruth.py +0 -0
  53. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/extract/__init__.py +0 -0
  54. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/extract/correct.py +0 -0
  55. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/ids.py +0 -0
  56. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/mining/candidates.py +0 -0
  57. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/mining/confidence.py +0 -0
  58. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/mining/engine.py +0 -0
  59. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/mining/filterspec.py +0 -0
  60. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/mining/formats.py +0 -0
  61. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/mining/store.py +0 -0
  62. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/py.typed +0 -0
  63. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/rust.py +0 -0
  64. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/sentiment/scorespec.py +0 -0
  65. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/cc_transcript/store.py +0 -0
  66. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/rust/data/afinn-en-165.tsv +0 -0
  67. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/rust/data/domain_overrides.tsv +0 -0
  68. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/rust/src/lexicon.rs +0 -0
  69. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/rust/src/model.rs +0 -0
  70. {cc_transcript-7.1.0 → cc_transcript-8.1.0}/rust/src/score.rs +0 -0
  71. {cc_transcript-7.1.0 → cc_transcript-8.1.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.1.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.4,<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).
@@ -53,9 +53,20 @@ EXPORTS: dict[str, str] = {
53
53
  "expand_tool_names",
54
54
  "file_path_of",
55
55
  "hunks_of",
56
+ "matches_names",
57
+ "mcp_access",
58
+ "mcp_parts",
56
59
  "parse_tool_call",
57
60
  "tool_name_matches",
58
61
  ),
62
+ "cc_transcript.command": (
63
+ "Command",
64
+ "CommandLine",
65
+ "CommandLineQuery",
66
+ "Redirect",
67
+ "command_prefixes",
68
+ "parse_command_line",
69
+ ),
59
70
  "cc_transcript.models": (
60
71
  "AssistantEvent",
61
72
  "CacheCreation",
@@ -81,6 +92,8 @@ EXPORTS: dict[str, str] = {
81
92
  "TranscriptEvent",
82
93
  "Usage",
83
94
  "UserEvent",
95
+ "thinking_chars",
96
+ "tool_uses",
84
97
  ),
85
98
  "cc_transcript.parser": (
86
99
  "TranscriptParser",
@@ -114,6 +127,13 @@ EXPORTS: dict[str, str] = {
114
127
  "UserClassifier",
115
128
  "hunk_overlap",
116
129
  "native_user_classifier",
130
+ "result_index",
131
+ ),
132
+ "cc_transcript.facts": (
133
+ "ToolFact",
134
+ "command_prefix_counts",
135
+ "mcp_summary",
136
+ "tool_facts",
117
137
  ),
118
138
  "cc_transcript.evidence": (
119
139
  "CandidatePair",
@@ -153,7 +173,6 @@ EXPORTS: dict[str, str] = {
153
173
  "INTERRUPT_MARKER_RE",
154
174
  "JUNK_USER_MESSAGE_RE",
155
175
  "RESUME_PHRASE_SET",
156
- "STOP_HOOK_RE",
157
176
  "STRUCTURAL_NOISE_RE",
158
177
  "TRIVIAL_ACK_SET",
159
178
  "USERS",
@@ -177,13 +196,6 @@ EXPORTS: dict[str, str] = {
177
196
  "drop_synthetic",
178
197
  "keep_only",
179
198
  ),
180
- "cc_transcript.messages": (
181
- "AssistantMessage",
182
- "BaseMessage",
183
- "MessageToolCall",
184
- "TranscriptMessage",
185
- "UserMessage",
186
- ),
187
199
  "cc_transcript.store": ("FileStateStore",),
188
200
  }.items()
189
201
  for name in names
@@ -222,6 +234,9 @@ if TYPE_CHECKING:
222
234
  from cc_transcript.activity import (
223
235
  native_user_classifier as native_user_classifier,
224
236
  )
237
+ from cc_transcript.activity import (
238
+ result_index as result_index,
239
+ )
225
240
  from cc_transcript.backend import Backend as Backend
226
241
  from cc_transcript.backend import ParsedTranscript as ParsedTranscript
227
242
  from cc_transcript.builders import (
@@ -260,6 +275,24 @@ if TYPE_CHECKING:
260
275
  from cc_transcript.builders import (
261
276
  keep_only as keep_only,
262
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
+ )
263
296
  from cc_transcript.context import (
264
297
  ContextWindow as ContextWindow,
265
298
  )
@@ -374,6 +407,18 @@ if TYPE_CHECKING:
374
407
  from cc_transcript.evidence import (
375
408
  record_harvest as record_harvest,
376
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
+ )
377
422
  from cc_transcript.filterspec import (
378
423
  ASSISTANTS as ASSISTANTS,
379
424
  )
@@ -386,9 +431,6 @@ if TYPE_CHECKING:
386
431
  from cc_transcript.filterspec import (
387
432
  RESUME_PHRASE_SET as RESUME_PHRASE_SET,
388
433
  )
389
- from cc_transcript.filterspec import (
390
- STOP_HOOK_RE as STOP_HOOK_RE,
391
- )
392
434
  from cc_transcript.filterspec import (
393
435
  STRUCTURAL_NOISE_RE as STRUCTURAL_NOISE_RE,
394
436
  )
@@ -434,21 +476,6 @@ if TYPE_CHECKING:
434
476
  from cc_transcript.ids import (
435
477
  tool_digest as tool_digest,
436
478
  )
437
- from cc_transcript.messages import (
438
- AssistantMessage as AssistantMessage,
439
- )
440
- from cc_transcript.messages import (
441
- BaseMessage as BaseMessage,
442
- )
443
- from cc_transcript.messages import (
444
- MessageToolCall as MessageToolCall,
445
- )
446
- from cc_transcript.messages import (
447
- TranscriptMessage as TranscriptMessage,
448
- )
449
- from cc_transcript.messages import (
450
- UserMessage as UserMessage,
451
- )
452
479
  from cc_transcript.models import (
453
480
  AssistantEvent as AssistantEvent,
454
481
  )
@@ -521,6 +548,12 @@ if TYPE_CHECKING:
521
548
  from cc_transcript.models import (
522
549
  UserEvent as UserEvent,
523
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
+ )
524
557
  from cc_transcript.parser import (
525
558
  TranscriptParser as TranscriptParser,
526
559
  )
@@ -636,6 +669,15 @@ if TYPE_CHECKING:
636
669
  from cc_transcript.tools import (
637
670
  hunks_of as hunks_of,
638
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
+ )
639
681
  from cc_transcript.tools import (
640
682
  parse_tool_call as parse_tool_call,
641
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, _: