cc-transcript 4.1.0__tar.gz → 6.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-4.1.0 → cc_transcript-6.0.0}/Cargo.lock +1 -1
- cc_transcript-6.0.0/PKG-INFO +149 -0
- cc_transcript-6.0.0/README.md +111 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/__init__.py +66 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/__main__.py +2 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/_parser_rs.pyi +4 -1
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/corrections_cli.py +2 -3
- cc_transcript-6.0.0/cc_transcript/cost.py +138 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/filterspec.py +12 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/judge/llm.py +20 -27
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/judge/verdicts.py +4 -8
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/mining/__init__.py +39 -22
- cc_transcript-6.0.0/cc_transcript/mining/engine.py +91 -0
- cc_transcript-6.0.0/cc_transcript/mining/formats.py +142 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/mining/signals.py +150 -100
- cc_transcript-6.0.0/cc_transcript/mining/spec.py +524 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/mining/store.py +2 -2
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/models.py +190 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/parser.py +112 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/render.py +2 -2
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/sentiment/engine.py +3 -1
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/store.py +2 -9
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/tools.py +4 -7
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/pyproject.toml +2 -2
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/rust/Cargo.toml +1 -1
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/rust/src/event.rs +198 -6
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/rust/src/lib.rs +23 -2
- cc_transcript-6.0.0/rust/src/mining.rs +1250 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/rust/src/model.rs +9 -0
- cc_transcript-4.1.0/PKG-INFO +0 -184
- cc_transcript-4.1.0/README.md +0 -146
- cc_transcript-4.1.0/cc_transcript/mining/formats.py +0 -64
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/Cargo.toml +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/LICENSE +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/activity.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/backend.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/builders.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/cli.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/context.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/corrections.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/decisions.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/discovery.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/disktruth.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/evidence.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/extract/__init__.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/extract/correct.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/ids.py +2 -2
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/judge/__init__.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/messages.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/mining/candidates.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/mining/confidence.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/mining/filterspec.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/mining/sourcekind.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/py.typed +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/query.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/rust.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/sentiment/__init__.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/sentiment/buckets.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/sentiment/lexicon.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/cc_transcript/sentiment/scorespec.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/rust/data/afinn-en-165.tsv +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/rust/data/domain_overrides.tsv +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/rust/src/filter.rs +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/rust/src/lexicon.rs +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/rust/src/score.rs +0 -0
- {cc_transcript-4.1.0 → cc_transcript-6.0.0}/rust/src/value.rs +0 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cc-transcript
|
|
3
|
+
Version: 6.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: pytest>=8.0 ; extra == 'dev'
|
|
17
|
+
Requires-Dist: ty>=0.0.44 ; extra == 'dev'
|
|
18
|
+
Requires-Dist: ruff>=0.8 ; extra == 'dev'
|
|
19
|
+
Requires-Dist: spawnllm>=0.4.0 ; extra == 'llm'
|
|
20
|
+
Requires-Dist: spacy>=3.8 ; extra == 'sentiment'
|
|
21
|
+
Requires-Dist: afinn>=0.1 ; extra == 'sentiment'
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Provides-Extra: llm
|
|
24
|
+
Provides-Extra: sentiment
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Summary: The Claude Code session-activity platform: a superset JSONL parser (Rust fast path, Python reference), typed tool calls with a cross-language content digest, turns/edits/evidence over one spine, context windows with labeled fidelity, unified decision and correction ledgers, LLM judging, and a transcript-investigation CLI.
|
|
27
|
+
Keywords: claude-code,claude,anthropic,transcripts,jsonl,parser,cli,agents,sentiment,feedback-mining
|
|
28
|
+
Author-email: Yasyf Mohamedali <yasyfm@gmail.com>
|
|
29
|
+
License-Expression: PolyForm-Noncommercial-1.0.0
|
|
30
|
+
Requires-Python: >=3.13
|
|
31
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
32
|
+
Project-URL: Changelog, https://github.com/yasyf/cc-transcript/blob/main/CHANGELOG.md
|
|
33
|
+
Project-URL: Documentation, https://yasyf.github.io/cc-transcript/
|
|
34
|
+
Project-URL: Homepage, https://github.com/yasyf/cc-transcript
|
|
35
|
+
Project-URL: Issues, https://github.com/yasyf/cc-transcript/issues
|
|
36
|
+
Project-URL: Repository, https://github.com/yasyf/cc-transcript
|
|
37
|
+
|
|
38
|
+
# cc-transcript
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
[](https://pypi.org/project/cc-transcript/)
|
|
43
|
+
[](https://pypi.org/project/cc-transcript/)
|
|
44
|
+
[](https://yasyf.github.io/cc-transcript/)
|
|
45
|
+
[](https://github.com/yasyf/cc-transcript/blob/main/LICENSE)
|
|
46
|
+
|
|
47
|
+
A non-lossy parser for Claude Code's on-disk JSONL transcripts, plus a CLI to investigate sessions.
|
|
48
|
+
|
|
49
|
+
`cc-transcript` parses Claude Code's JSONL transcripts into a typed superset event model — every entry type preserved, nothing dropped. The parser never silently discards sidechains, synthetic turns, tool results, or unrecognized entries; filtering is opt-in and lives in your code, not buried in the parser. It ships as a Python library, a `uvx`-runnable CLI, and a Claude Code plugin.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uvx cc-transcript --help # run the CLI
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quickstart
|
|
58
|
+
|
|
59
|
+
Discover the transcripts on disk, parse one, and walk the events:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import anyio
|
|
63
|
+
|
|
64
|
+
from cc_transcript import AssistantEvent, TranscriptDiscovery, UserEvent, parse_events_from_bytes
|
|
65
|
+
|
|
66
|
+
path = anyio.run(TranscriptDiscovery.find_transcripts)[0]
|
|
67
|
+
events = parse_events_from_bytes(path.read_bytes())
|
|
68
|
+
|
|
69
|
+
for event in events:
|
|
70
|
+
match event:
|
|
71
|
+
case UserEvent(text=text):
|
|
72
|
+
print("user:", text[:80])
|
|
73
|
+
case AssistantEvent(model=model, text=text):
|
|
74
|
+
print(f"assistant ({model}):", text[:80])
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Filtering is opt-in. Compose a spec from small builders and apply it — `build_spec` assembles clauses, `apply_spec` yields the survivors. `NOISE_SPEC` is a ready-made spec for universal structural noise (system reminders, local-command output, skill banners):
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from cc_transcript import apply_spec, build_spec, keep_only, drop_junk, drop_short
|
|
81
|
+
|
|
82
|
+
spec = build_spec(keep_only("user", "assistant"), drop_junk("structural"), drop_short(2))
|
|
83
|
+
clean = list(apply_spec(events, spec))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## CLI
|
|
87
|
+
|
|
88
|
+
Every command runs as `uvx cc-transcript <command> ...`; see `--help` on any command for its flags.
|
|
89
|
+
|
|
90
|
+
| Command | What it does |
|
|
91
|
+
| -------- | ------------ |
|
|
92
|
+
| `list` | Find transcripts on disk, newest first |
|
|
93
|
+
| `stats` | Summarize a session — counts, kinds, models, tools, token sizes, span |
|
|
94
|
+
| `show` | Render one compact line per event (`--signal` keeps the conversational spine) |
|
|
95
|
+
| `grep` | Search event content; hit indexes feed back into `show --range` |
|
|
96
|
+
| `slice` | Emit per-tool-call JSONL for a session UUID and time window (the bridge cc-review consumes) |
|
|
97
|
+
| `digest` | Generate and check the cross-language fixture corpus for the tool-digest contract |
|
|
98
|
+
|
|
99
|
+
The output is compact by design — one line per event, hard truncation — so an agent triages a session in a few hundred tokens instead of paging through megabytes of JSONL:
|
|
100
|
+
|
|
101
|
+
```console
|
|
102
|
+
$ uvx cc-transcript stats ~/.claude/projects/-Users-yasyf-Code-cc-transcript/4c77d556-8694-4613-8f50-253d905da68e.jsonl
|
|
103
|
+
files 1
|
|
104
|
+
events 181
|
|
105
|
+
kinds other 68 · assistant 53 · user 33 · mode 22 · system 5
|
|
106
|
+
models claude-fable-5 53
|
|
107
|
+
tools TaskCreate 10 · Agent 5 · Read 5 · TaskUpdate 5 · Bash 2 · ToolSearch 2 · AskUserQuestion 1 · ExitPlanMode 1
|
|
108
|
+
text 14.8KB
|
|
109
|
+
thinking 8.7KB
|
|
110
|
+
tool io 89.0KB
|
|
111
|
+
sessions 1
|
|
112
|
+
span 2026-06-12 01:07:55 → 2026-06-12 02:28:03
|
|
113
|
+
interrupts 0
|
|
114
|
+
tool errors 0
|
|
115
|
+
sidechain 0
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Claude Code plugin
|
|
119
|
+
|
|
120
|
+
Install the bundled plugin from inside Claude Code:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
/plugin marketplace add yasyf/cc-transcript
|
|
124
|
+
/plugin install cc-transcript@cc-transcript
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The plugin's skill teaches Claude to answer questions about its own history — "what did I ask yesterday", "find the session where we fixed the parser" — by funneling through the CLI's `list`, `stats`, `grep`, and `show` commands instead of reading raw JSONL.
|
|
128
|
+
|
|
129
|
+
## How it works
|
|
130
|
+
|
|
131
|
+
The event model is a superset: sidechains, `<synthetic>` turns, thinking blocks, and unrecognized entry types all survive parsing, so you decide what to drop via composable filter specs. Two engines sit behind one `Backend` protocol — `RustBackend` (PyO3 + rayon) is the default fast path, `PythonBackend` is the readable reference, parity-asserted against each other. On top of the spine, `SessionActivity` lifts events into turns and typed tool calls, `FileStateStore` tracks per-file mtimes in SQLite for incremental re-parses, and `cc_transcript.mining`/`judge`/`sentiment` extract feedback, run LLM verdict passes, and score conversational sentiment. The [docs site](https://yasyf.github.io/cc-transcript/) covers each of these in depth.
|
|
132
|
+
|
|
133
|
+
## Docs
|
|
134
|
+
|
|
135
|
+
Each section of [the docs site](https://yasyf.github.io/cc-transcript/) is a focused guide:
|
|
136
|
+
|
|
137
|
+
- [Getting Started](https://yasyf.github.io/cc-transcript/docs/getting-started/index.html) — install, parse, filter, persist.
|
|
138
|
+
- [Filtering events](https://yasyf.github.io/cc-transcript/docs/guide/filtering-events.html) — clauses, specs, and `NOISE_SPEC`.
|
|
139
|
+
- [Scoring sentiment](https://yasyf.github.io/cc-transcript/docs/guide/scoring-sentiment.html) — the lexicon engine and score specs.
|
|
140
|
+
- [Rust/Python backends & parity](https://yasyf.github.io/cc-transcript/docs/guide/backends-and-parity.html) — the `Backend` protocol and parity testing.
|
|
141
|
+
- [Compose your own policy](https://yasyf.github.io/cc-transcript/docs/guide/compose-your-own-policy.html) — building a bespoke filtering policy.
|
|
142
|
+
- [Mining feedback](https://yasyf.github.io/cc-transcript/docs/guide/mining-feedback.html) — detectors, confidence, candidates, and verdicts.
|
|
143
|
+
- [The transcript CLI](https://yasyf.github.io/cc-transcript/docs/guide/transcript-cli.html) — `list`/`show`/`grep`/`stats` end to end.
|
|
144
|
+
- [API reference](https://yasyf.github.io/cc-transcript/reference/index.html) — the complete typed surface.
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
[PolyForm Noncommercial 1.0.0](LICENSE).
|
|
149
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# cc-transcript
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/cc-transcript/)
|
|
6
|
+
[](https://pypi.org/project/cc-transcript/)
|
|
7
|
+
[](https://yasyf.github.io/cc-transcript/)
|
|
8
|
+
[](https://github.com/yasyf/cc-transcript/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
A non-lossy parser for Claude Code's on-disk JSONL transcripts, plus a CLI to investigate sessions.
|
|
11
|
+
|
|
12
|
+
`cc-transcript` parses Claude Code's JSONL transcripts into a typed superset event model — every entry type preserved, nothing dropped. The parser never silently discards sidechains, synthetic turns, tool results, or unrecognized entries; filtering is opt-in and lives in your code, not buried in the parser. It ships as a Python library, a `uvx`-runnable CLI, and a Claude Code plugin.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uvx cc-transcript --help # run the CLI
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
Discover the transcripts on disk, parse one, and walk the events:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import anyio
|
|
26
|
+
|
|
27
|
+
from cc_transcript import AssistantEvent, TranscriptDiscovery, UserEvent, parse_events_from_bytes
|
|
28
|
+
|
|
29
|
+
path = anyio.run(TranscriptDiscovery.find_transcripts)[0]
|
|
30
|
+
events = parse_events_from_bytes(path.read_bytes())
|
|
31
|
+
|
|
32
|
+
for event in events:
|
|
33
|
+
match event:
|
|
34
|
+
case UserEvent(text=text):
|
|
35
|
+
print("user:", text[:80])
|
|
36
|
+
case AssistantEvent(model=model, text=text):
|
|
37
|
+
print(f"assistant ({model}):", text[:80])
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Filtering is opt-in. Compose a spec from small builders and apply it — `build_spec` assembles clauses, `apply_spec` yields the survivors. `NOISE_SPEC` is a ready-made spec for universal structural noise (system reminders, local-command output, skill banners):
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from cc_transcript import apply_spec, build_spec, keep_only, drop_junk, drop_short
|
|
44
|
+
|
|
45
|
+
spec = build_spec(keep_only("user", "assistant"), drop_junk("structural"), drop_short(2))
|
|
46
|
+
clean = list(apply_spec(events, spec))
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## CLI
|
|
50
|
+
|
|
51
|
+
Every command runs as `uvx cc-transcript <command> ...`; see `--help` on any command for its flags.
|
|
52
|
+
|
|
53
|
+
| Command | What it does |
|
|
54
|
+
| -------- | ------------ |
|
|
55
|
+
| `list` | Find transcripts on disk, newest first |
|
|
56
|
+
| `stats` | Summarize a session — counts, kinds, models, tools, token sizes, span |
|
|
57
|
+
| `show` | Render one compact line per event (`--signal` keeps the conversational spine) |
|
|
58
|
+
| `grep` | Search event content; hit indexes feed back into `show --range` |
|
|
59
|
+
| `slice` | Emit per-tool-call JSONL for a session UUID and time window (the bridge cc-review consumes) |
|
|
60
|
+
| `digest` | Generate and check the cross-language fixture corpus for the tool-digest contract |
|
|
61
|
+
|
|
62
|
+
The output is compact by design — one line per event, hard truncation — so an agent triages a session in a few hundred tokens instead of paging through megabytes of JSONL:
|
|
63
|
+
|
|
64
|
+
```console
|
|
65
|
+
$ uvx cc-transcript stats ~/.claude/projects/-Users-yasyf-Code-cc-transcript/4c77d556-8694-4613-8f50-253d905da68e.jsonl
|
|
66
|
+
files 1
|
|
67
|
+
events 181
|
|
68
|
+
kinds other 68 · assistant 53 · user 33 · mode 22 · system 5
|
|
69
|
+
models claude-fable-5 53
|
|
70
|
+
tools TaskCreate 10 · Agent 5 · Read 5 · TaskUpdate 5 · Bash 2 · ToolSearch 2 · AskUserQuestion 1 · ExitPlanMode 1
|
|
71
|
+
text 14.8KB
|
|
72
|
+
thinking 8.7KB
|
|
73
|
+
tool io 89.0KB
|
|
74
|
+
sessions 1
|
|
75
|
+
span 2026-06-12 01:07:55 → 2026-06-12 02:28:03
|
|
76
|
+
interrupts 0
|
|
77
|
+
tool errors 0
|
|
78
|
+
sidechain 0
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Claude Code plugin
|
|
82
|
+
|
|
83
|
+
Install the bundled plugin from inside Claude Code:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
/plugin marketplace add yasyf/cc-transcript
|
|
87
|
+
/plugin install cc-transcript@cc-transcript
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The plugin's skill teaches Claude to answer questions about its own history — "what did I ask yesterday", "find the session where we fixed the parser" — by funneling through the CLI's `list`, `stats`, `grep`, and `show` commands instead of reading raw JSONL.
|
|
91
|
+
|
|
92
|
+
## How it works
|
|
93
|
+
|
|
94
|
+
The event model is a superset: sidechains, `<synthetic>` turns, thinking blocks, and unrecognized entry types all survive parsing, so you decide what to drop via composable filter specs. Two engines sit behind one `Backend` protocol — `RustBackend` (PyO3 + rayon) is the default fast path, `PythonBackend` is the readable reference, parity-asserted against each other. On top of the spine, `SessionActivity` lifts events into turns and typed tool calls, `FileStateStore` tracks per-file mtimes in SQLite for incremental re-parses, and `cc_transcript.mining`/`judge`/`sentiment` extract feedback, run LLM verdict passes, and score conversational sentiment. The [docs site](https://yasyf.github.io/cc-transcript/) covers each of these in depth.
|
|
95
|
+
|
|
96
|
+
## Docs
|
|
97
|
+
|
|
98
|
+
Each section of [the docs site](https://yasyf.github.io/cc-transcript/) is a focused guide:
|
|
99
|
+
|
|
100
|
+
- [Getting Started](https://yasyf.github.io/cc-transcript/docs/getting-started/index.html) — install, parse, filter, persist.
|
|
101
|
+
- [Filtering events](https://yasyf.github.io/cc-transcript/docs/guide/filtering-events.html) — clauses, specs, and `NOISE_SPEC`.
|
|
102
|
+
- [Scoring sentiment](https://yasyf.github.io/cc-transcript/docs/guide/scoring-sentiment.html) — the lexicon engine and score specs.
|
|
103
|
+
- [Rust/Python backends & parity](https://yasyf.github.io/cc-transcript/docs/guide/backends-and-parity.html) — the `Backend` protocol and parity testing.
|
|
104
|
+
- [Compose your own policy](https://yasyf.github.io/cc-transcript/docs/guide/compose-your-own-policy.html) — building a bespoke filtering policy.
|
|
105
|
+
- [Mining feedback](https://yasyf.github.io/cc-transcript/docs/guide/mining-feedback.html) — detectors, confidence, candidates, and verdicts.
|
|
106
|
+
- [The transcript CLI](https://yasyf.github.io/cc-transcript/docs/guide/transcript-cli.html) — `list`/`show`/`grep`/`stats` end to end.
|
|
107
|
+
- [API reference](https://yasyf.github.io/cc-transcript/reference/index.html) — the complete typed surface.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
[PolyForm Noncommercial 1.0.0](LICENSE).
|
|
@@ -57,19 +57,28 @@ EXPORTS: dict[str, str] = {
|
|
|
57
57
|
),
|
|
58
58
|
"cc_transcript.models": (
|
|
59
59
|
"AssistantEvent",
|
|
60
|
+
"CacheCreation",
|
|
60
61
|
"CcVersion",
|
|
61
62
|
"ContentBlock",
|
|
62
63
|
"EntryMeta",
|
|
63
64
|
"FallbackBlock",
|
|
65
|
+
"InitInfo",
|
|
66
|
+
"McpServer",
|
|
64
67
|
"ModeEvent",
|
|
68
|
+
"ModelUsage",
|
|
65
69
|
"OtherBlock",
|
|
66
70
|
"OtherEvent",
|
|
71
|
+
"Plugin",
|
|
72
|
+
"PrintMessage",
|
|
73
|
+
"PrintResult",
|
|
74
|
+
"ServerToolUse",
|
|
67
75
|
"SystemEvent",
|
|
68
76
|
"TextBlock",
|
|
69
77
|
"ThinkingBlock",
|
|
70
78
|
"ToolResultBlock",
|
|
71
79
|
"ToolUseBlock",
|
|
72
80
|
"TranscriptEvent",
|
|
81
|
+
"Usage",
|
|
73
82
|
"UserEvent",
|
|
74
83
|
),
|
|
75
84
|
"cc_transcript.parser": (
|
|
@@ -77,6 +86,15 @@ EXPORTS: dict[str, str] = {
|
|
|
77
86
|
"parse_event",
|
|
78
87
|
"parse_events_async",
|
|
79
88
|
"parse_events_from_bytes",
|
|
89
|
+
"parse_print_result",
|
|
90
|
+
),
|
|
91
|
+
"cc_transcript.cost": (
|
|
92
|
+
"PRICING",
|
|
93
|
+
"CostBreakdown",
|
|
94
|
+
"ModelPricing",
|
|
95
|
+
"cost_of",
|
|
96
|
+
"cost_of_assistant",
|
|
97
|
+
"resolve_pricing",
|
|
80
98
|
),
|
|
81
99
|
"cc_transcript.backend": ("Backend", "ParsedTranscript"),
|
|
82
100
|
"cc_transcript.discovery": (
|
|
@@ -271,6 +289,24 @@ if TYPE_CHECKING:
|
|
|
271
289
|
from cc_transcript.corrections import (
|
|
272
290
|
Origin as Origin,
|
|
273
291
|
)
|
|
292
|
+
from cc_transcript.cost import (
|
|
293
|
+
PRICING as PRICING,
|
|
294
|
+
)
|
|
295
|
+
from cc_transcript.cost import (
|
|
296
|
+
CostBreakdown as CostBreakdown,
|
|
297
|
+
)
|
|
298
|
+
from cc_transcript.cost import (
|
|
299
|
+
ModelPricing as ModelPricing,
|
|
300
|
+
)
|
|
301
|
+
from cc_transcript.cost import (
|
|
302
|
+
cost_of as cost_of,
|
|
303
|
+
)
|
|
304
|
+
from cc_transcript.cost import (
|
|
305
|
+
cost_of_assistant as cost_of_assistant,
|
|
306
|
+
)
|
|
307
|
+
from cc_transcript.cost import (
|
|
308
|
+
resolve_pricing as resolve_pricing,
|
|
309
|
+
)
|
|
274
310
|
from cc_transcript.decisions import (
|
|
275
311
|
DECISIONS_DDL as DECISIONS_DDL,
|
|
276
312
|
)
|
|
@@ -415,6 +451,9 @@ if TYPE_CHECKING:
|
|
|
415
451
|
from cc_transcript.models import (
|
|
416
452
|
AssistantEvent as AssistantEvent,
|
|
417
453
|
)
|
|
454
|
+
from cc_transcript.models import (
|
|
455
|
+
CacheCreation as CacheCreation,
|
|
456
|
+
)
|
|
418
457
|
from cc_transcript.models import (
|
|
419
458
|
CcVersion as CcVersion,
|
|
420
459
|
)
|
|
@@ -427,15 +466,36 @@ if TYPE_CHECKING:
|
|
|
427
466
|
from cc_transcript.models import (
|
|
428
467
|
FallbackBlock as FallbackBlock,
|
|
429
468
|
)
|
|
469
|
+
from cc_transcript.models import (
|
|
470
|
+
InitInfo as InitInfo,
|
|
471
|
+
)
|
|
472
|
+
from cc_transcript.models import (
|
|
473
|
+
McpServer as McpServer,
|
|
474
|
+
)
|
|
430
475
|
from cc_transcript.models import (
|
|
431
476
|
ModeEvent as ModeEvent,
|
|
432
477
|
)
|
|
478
|
+
from cc_transcript.models import (
|
|
479
|
+
ModelUsage as ModelUsage,
|
|
480
|
+
)
|
|
433
481
|
from cc_transcript.models import (
|
|
434
482
|
OtherBlock as OtherBlock,
|
|
435
483
|
)
|
|
436
484
|
from cc_transcript.models import (
|
|
437
485
|
OtherEvent as OtherEvent,
|
|
438
486
|
)
|
|
487
|
+
from cc_transcript.models import (
|
|
488
|
+
Plugin as Plugin,
|
|
489
|
+
)
|
|
490
|
+
from cc_transcript.models import (
|
|
491
|
+
PrintMessage as PrintMessage,
|
|
492
|
+
)
|
|
493
|
+
from cc_transcript.models import (
|
|
494
|
+
PrintResult as PrintResult,
|
|
495
|
+
)
|
|
496
|
+
from cc_transcript.models import (
|
|
497
|
+
ServerToolUse as ServerToolUse,
|
|
498
|
+
)
|
|
439
499
|
from cc_transcript.models import (
|
|
440
500
|
SystemEvent as SystemEvent,
|
|
441
501
|
)
|
|
@@ -454,6 +514,9 @@ if TYPE_CHECKING:
|
|
|
454
514
|
from cc_transcript.models import (
|
|
455
515
|
TranscriptEvent as TranscriptEvent,
|
|
456
516
|
)
|
|
517
|
+
from cc_transcript.models import (
|
|
518
|
+
Usage as Usage,
|
|
519
|
+
)
|
|
457
520
|
from cc_transcript.models import (
|
|
458
521
|
UserEvent as UserEvent,
|
|
459
522
|
)
|
|
@@ -469,6 +532,9 @@ if TYPE_CHECKING:
|
|
|
469
532
|
from cc_transcript.parser import (
|
|
470
533
|
parse_events_from_bytes as parse_events_from_bytes,
|
|
471
534
|
)
|
|
535
|
+
from cc_transcript.parser import (
|
|
536
|
+
parse_print_result as parse_print_result,
|
|
537
|
+
)
|
|
472
538
|
from cc_transcript.query import (
|
|
473
539
|
FileRef as FileRef,
|
|
474
540
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from cc_transcript.models import TranscriptEvent
|
|
3
|
+
from cc_transcript.models import PrintResult, TranscriptEvent
|
|
4
4
|
|
|
5
5
|
class ParseStream:
|
|
6
6
|
"""A streaming handle over a rayon-parsed batch of transcript files."""
|
|
@@ -18,6 +18,9 @@ def stream_parse(paths: list[tuple[str, float]], prefetch: int, spec_json: str |
|
|
|
18
18
|
are dropped during parsing, before any Python object is built.
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
+
def parse_print_result(raw: bytes, /) -> PrintResult:
|
|
22
|
+
"""Parses a 'claude -p --output-format json' result from raw JSON bytes."""
|
|
23
|
+
|
|
21
24
|
def lexicon_available() -> bool:
|
|
22
25
|
"""Whether the UDPipe English model loaded (downloading + caching on first call)."""
|
|
23
26
|
|
|
@@ -11,7 +11,6 @@ from __future__ import annotations
|
|
|
11
11
|
import json
|
|
12
12
|
import time
|
|
13
13
|
from dataclasses import asdict
|
|
14
|
-
from typing import cast
|
|
15
14
|
|
|
16
15
|
import click
|
|
17
16
|
import orjson
|
|
@@ -54,7 +53,7 @@ def add(
|
|
|
54
53
|
anchor: str,
|
|
55
54
|
incorrect_file: str,
|
|
56
55
|
ts_ms: int | None,
|
|
57
|
-
origin:
|
|
56
|
+
origin: Origin | None,
|
|
58
57
|
incorrect_old: str,
|
|
59
58
|
incorrect_new: str,
|
|
60
59
|
incorrect_digest: str | None,
|
|
@@ -79,7 +78,7 @@ def add(
|
|
|
79
78
|
incorrect_file=incorrect_file,
|
|
80
79
|
incorrect_old=incorrect_old,
|
|
81
80
|
incorrect_new=incorrect_new,
|
|
82
|
-
correction_origin=
|
|
81
|
+
correction_origin=origin,
|
|
83
82
|
correction_file=correction_file,
|
|
84
83
|
correction_old=correction_old,
|
|
85
84
|
correction_new=correction_new,
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
|
|
9
|
+
from cc_transcript.models import AssistantEvent, Usage
|
|
10
|
+
|
|
11
|
+
MTOK = 1_000_000
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class ModelPricing:
|
|
16
|
+
"""USD-per-million-token rates for a model family.
|
|
17
|
+
|
|
18
|
+
Standard service tier, standard (non-long-context) pricing — current Claude
|
|
19
|
+
4.x models serve their 1M context at standard rates with no long-context
|
|
20
|
+
premium. Override via the `pricing` argument to cost_of for other tiers.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
input: USD per million uncached input tokens.
|
|
24
|
+
output: USD per million output tokens.
|
|
25
|
+
cache_read: USD per million tokens served from the cache.
|
|
26
|
+
cache_write_5m: USD per million tokens written to the 5-minute cache.
|
|
27
|
+
cache_write_1h: USD per million tokens written to the 1-hour cache.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
input: float
|
|
31
|
+
output: float
|
|
32
|
+
cache_read: float
|
|
33
|
+
cache_write_5m: float
|
|
34
|
+
cache_write_1h: float
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class CostBreakdown:
|
|
39
|
+
"""The per-component and total USD cost of a single turn's token usage.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
input_cost: USD cost of the uncached input tokens.
|
|
43
|
+
output_cost: USD cost of the output tokens.
|
|
44
|
+
cache_read_cost: USD cost of the cache-read tokens.
|
|
45
|
+
cache_write_cost: USD cost of the cache-creation tokens, summed across TTL buckets.
|
|
46
|
+
total: The sum of the four component costs.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
input_cost: float
|
|
50
|
+
output_cost: float
|
|
51
|
+
cache_read_cost: float
|
|
52
|
+
cache_write_cost: float
|
|
53
|
+
total: float
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
PRICING: Mapping[str, ModelPricing] = {
|
|
57
|
+
"fable": ModelPricing(input=10.0, output=50.0, cache_read=1.0, cache_write_5m=12.5, cache_write_1h=20.0),
|
|
58
|
+
"opus": ModelPricing(input=5.0, output=25.0, cache_read=0.5, cache_write_5m=6.25, cache_write_1h=10.0),
|
|
59
|
+
"sonnet": ModelPricing(input=3.0, output=15.0, cache_read=0.3, cache_write_5m=3.75, cache_write_1h=6.0),
|
|
60
|
+
"haiku": ModelPricing(input=1.0, output=5.0, cache_read=0.1, cache_write_5m=1.25, cache_write_1h=2.0),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def resolve_pricing(model: str, *, pricing: Mapping[str, ModelPricing] = PRICING) -> ModelPricing:
|
|
65
|
+
"""Return the pricing row whose family key is a substring of `model`.
|
|
66
|
+
|
|
67
|
+
Family keys are mutually exclusive substrings, so at most one matches —
|
|
68
|
+
``"claude-haiku-4-5-20251001"`` resolves to haiku, ``"claude-opus-4-8"`` to
|
|
69
|
+
opus, and a bare ``"sonnet"`` to sonnet.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
model: The model id to resolve.
|
|
73
|
+
pricing: The family-keyed pricing table to search.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
KeyError: If no family key is a substring of `model`.
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
>>> resolve_pricing("claude-opus-4-8").input
|
|
80
|
+
5.0
|
|
81
|
+
"""
|
|
82
|
+
for family, row in pricing.items():
|
|
83
|
+
if family in model:
|
|
84
|
+
return row
|
|
85
|
+
raise KeyError(f"no pricing for model: {model}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cost_of(usage: Usage, model: str, *, pricing: Mapping[str, ModelPricing] = PRICING) -> CostBreakdown:
|
|
89
|
+
"""Compute the USD cost of a turn's token usage under a model's rates.
|
|
90
|
+
|
|
91
|
+
The cache-creation split prefers the per-TTL ``usage.cache_creation`` when
|
|
92
|
+
present, falling back to the flat ``usage.cache_creation_input_tokens`` as
|
|
93
|
+
5-minute writes with no 1-hour share.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
usage: The turn's token usage and cache accounting.
|
|
97
|
+
model: The model id whose rates apply.
|
|
98
|
+
pricing: The family-keyed pricing table to resolve against.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The per-component and total cost.
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
>>> cost_of(usage, "claude-haiku-4-5-20251001").total
|
|
105
|
+
0.05759
|
|
106
|
+
"""
|
|
107
|
+
row = resolve_pricing(model, pricing=pricing)
|
|
108
|
+
write_5m, write_1h = (
|
|
109
|
+
(usage.cache_creation.ephemeral_5m_input_tokens, usage.cache_creation.ephemeral_1h_input_tokens)
|
|
110
|
+
if usage.cache_creation is not None
|
|
111
|
+
else (usage.cache_creation_input_tokens, 0)
|
|
112
|
+
)
|
|
113
|
+
input_cost = usage.input_tokens / MTOK * row.input
|
|
114
|
+
output_cost = usage.output_tokens / MTOK * row.output
|
|
115
|
+
cache_read_cost = usage.cache_read_input_tokens / MTOK * row.cache_read
|
|
116
|
+
cache_write_cost = write_5m / MTOK * row.cache_write_5m + write_1h / MTOK * row.cache_write_1h
|
|
117
|
+
return CostBreakdown(
|
|
118
|
+
input_cost=input_cost,
|
|
119
|
+
output_cost=output_cost,
|
|
120
|
+
cache_read_cost=cache_read_cost,
|
|
121
|
+
cache_write_cost=cache_write_cost,
|
|
122
|
+
total=input_cost + output_cost + cache_read_cost + cache_write_cost,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def cost_of_assistant(event: AssistantEvent, *, pricing: Mapping[str, ModelPricing] = PRICING) -> CostBreakdown | None:
|
|
127
|
+
"""Compute the cost of an assistant turn, or None when it carries no usage.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
event: The assistant turn whose usage is priced.
|
|
131
|
+
pricing: The family-keyed pricing table to resolve against.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The cost breakdown, or None when ``event.usage`` is None.
|
|
135
|
+
"""
|
|
136
|
+
if event.usage is None:
|
|
137
|
+
return None
|
|
138
|
+
return cost_of(event.usage, event.model, pricing=pricing)
|
|
@@ -69,6 +69,12 @@ AGENT_INJECTION_GROUPS: tuple[tuple[str, str], ...] = (
|
|
|
69
69
|
INTERRUPT_MARKER_GROUPS: tuple[tuple[str, str], ...] = (("interrupt", r"\[Request interrupted by user"),)
|
|
70
70
|
STOP_HOOK_GROUPS: tuple[tuple[str, str], ...] = (("stop_hook", r"Stop hook feedback:"),)
|
|
71
71
|
|
|
72
|
+
# Raw CC-injected protocol strings carried in tool-result content: the denial banner
|
|
73
|
+
# and the markers that wrap the user's verbatim instruction in a rejected tool use.
|
|
74
|
+
DENIAL_PREFIX = "The user doesn't want to proceed with this tool use. The tool use was rejected"
|
|
75
|
+
USER_SAID_MARKER = "To tell you how to proceed, the user said:\n"
|
|
76
|
+
USER_SAID_TRAILER = "Note: The user's next message"
|
|
77
|
+
|
|
72
78
|
# Approve-and-advance directives: a user telling the agent to proceed/commit/push or
|
|
73
79
|
# to resume killed work. They follow an assistant turn but advance it rather than
|
|
74
80
|
# correcting it — the opposite of pushback — so a pushback consumer drops them. The
|
|
@@ -142,6 +148,11 @@ MILD_IMPATIENCE_GROUPS: tuple[tuple[str, str], ...] = (
|
|
|
142
148
|
),
|
|
143
149
|
)
|
|
144
150
|
|
|
151
|
+
# Hedging cues that soften a correction; a mining demote stage drops confidence on a hit.
|
|
152
|
+
HEDGE_GROUPS: tuple[tuple[str, str], ...] = (
|
|
153
|
+
("hedged", r"\b(?:maybe|perhaps|possibly|might|not sure|i think|i guess|if you (?:want|prefer)|up to you)\b"),
|
|
154
|
+
)
|
|
155
|
+
|
|
145
156
|
PORTABLE_GROUP_NAMES: frozenset[str] = frozenset(
|
|
146
157
|
name
|
|
147
158
|
for name, _ in (
|
|
@@ -152,6 +163,7 @@ PORTABLE_GROUP_NAMES: frozenset[str] = frozenset(
|
|
|
152
163
|
*COMMAND_ECHO_GROUPS,
|
|
153
164
|
*FRUSTRATION_GROUPS,
|
|
154
165
|
*MILD_IMPATIENCE_GROUPS,
|
|
166
|
+
*HEDGE_GROUPS,
|
|
155
167
|
)
|
|
156
168
|
)
|
|
157
169
|
|