cc-transcript 7.1.0__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.
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/Cargo.lock +53 -1
- cc_transcript-8.0.0/PKG-INFO +123 -0
- cc_transcript-8.0.0/README.md +83 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/__init__.py +68 -26
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/_parser_rs.pyi +3 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/activity.py +44 -22
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/cli.py +141 -2
- cc_transcript-8.0.0/cc_transcript/command.py +516 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/context.py +3 -2
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/discovery.py +15 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/evidence.py +4 -3
- cc_transcript-8.0.0/cc_transcript/facts.py +186 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/filterspec.py +28 -3
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/judge/__init__.py +1 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/judge/llm.py +15 -2
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/judge/verdicts.py +19 -5
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/mining/__init__.py +1 -3
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/mining/signals.py +34 -40
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/mining/spec.py +24 -8
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/models.py +10 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/parser.py +2 -7
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/query.py +28 -17
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/render.py +69 -6
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/sentiment/__init__.py +10 -7
- cc_transcript-8.0.0/cc_transcript/sentiment/buckets.py +106 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/sentiment/engine.py +2 -1
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/sentiment/lexicon.py +12 -27
- cc_transcript-8.0.0/cc_transcript/tools.py +555 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/pyproject.toml +9 -8
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/rust/Cargo.toml +5 -1
- cc_transcript-8.0.0/rust/src/command.rs +263 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/rust/src/event.rs +2 -3
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/rust/src/filter.rs +3 -2
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/rust/src/lib.rs +9 -1
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/rust/src/mining.rs +106 -129
- cc_transcript-8.0.0/rust/src/protocol.rs +92 -0
- cc_transcript-7.1.0/PKG-INFO +0 -149
- cc_transcript-7.1.0/README.md +0 -111
- cc_transcript-7.1.0/cc_transcript/messages.py +0 -43
- cc_transcript-7.1.0/cc_transcript/sentiment/buckets.py +0 -90
- cc_transcript-7.1.0/cc_transcript/tools.py +0 -418
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/Cargo.toml +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/LICENSE +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/__main__.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/backend.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/builders.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/corrections.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/corrections_cli.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/cost.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/decisions.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/disktruth.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/extract/__init__.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/extract/correct.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/ids.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/mining/candidates.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/mining/confidence.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/mining/engine.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/mining/filterspec.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/mining/formats.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/mining/sourcekind.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/mining/store.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/py.typed +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/rust.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/sentiment/scorespec.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/cc_transcript/store.py +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/rust/data/afinn-en-165.tsv +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/rust/data/domain_overrides.tsv +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/rust/src/lexicon.rs +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/rust/src/model.rs +0 -0
- {cc_transcript-7.1.0 → cc_transcript-8.0.0}/rust/src/score.rs +0 -0
- {cc_transcript-7.1.0 → 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.
|
|
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
|
+
# 
|
|
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
|
+
[](https://github.com/yasyf/cc-transcript/actions/workflows/ci.yml)
|
|
45
|
+
[](https://pypi.org/project/cc-transcript/)
|
|
46
|
+
[](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
|
+
# 
|
|
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
|
+
[](https://github.com/yasyf/cc-transcript/actions/workflows/ci.yml)
|
|
6
|
+
[](https://pypi.org/project/cc-transcript/)
|
|
7
|
+
[](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,
|
|
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
|
|
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 :=
|
|
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
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
313
|
+
started_at, ended_at = event_stamps(events)
|
|
293
314
|
return Turn(
|
|
294
315
|
index=index,
|
|
295
316
|
prompt=prompt,
|
|
296
|
-
started_at=
|
|
297
|
-
ended_at=
|
|
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 :=
|
|
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, _:
|