skiagram-core 0.1.1

Domain model, agent adapters, and token-accounting analysis for skiagram (no UI, no network).
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
<a id="readme-top"></a>

<div align="center">

<img src="readme_banner.png" alt="skiagram: the flamegraph for your AI agent's token spend" width="100%">

<h3>๐Ÿ”ฅ The flamegraph for your AI agent's token spend ๐Ÿ”ฅ</h3>

<p>
Profile <strong>where your AI coding agent's tokens actually went</strong>, and <em>why your context window is full</em>.<br>
Local-first. Single static binary. <strong>Nothing ever leaves your machine.</strong>
</p>

<p>
<a href="https://github.com/TanvirAnjumApurbo/skiagram/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/TanvirAnjumApurbo/skiagram/ci.yml?branch=main&style=flat-square&logo=githubactions&logoColor=white&label=CI"></a>
<a href="https://crates.io/crates/skiagram"><img alt="crates.io" src="https://img.shields.io/crates/v/skiagram?style=flat-square&logo=rust&logoColor=white"></a>
<a href="https://crates.io/crates/skiagram"><img alt="Downloads" src="https://img.shields.io/crates/d/skiagram?style=flat-square&label=downloads"></a>
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue?style=flat-square"></a>
<a href="https://www.rust-lang.org"><img alt="Rust 1.85+" src="https://img.shields.io/badge/Rust-1.85%2B-CE412B?style=flat-square&logo=rust&logoColor=white"></a>
<img alt="Platforms" src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-555?style=flat-square">
<img alt="Network: zero by default" src="https://img.shields.io/badge/network-zero%20by%20default-3fb950?style=flat-square">
<a href="https://github.com/TanvirAnjumApurbo/skiagram/stargazers"><img alt="Stars" src="https://img.shields.io/github/stars/TanvirAnjumApurbo/skiagram?style=flat-square&logo=github&label=stars"></a>
</p>

<p>
<a href="#features"><strong>Features</strong></a> ยท
<a href="#install"><strong>Install</strong></a> ยท
<a href="#usage"><strong>Usage</strong></a> ยท
<a href="#how-it-works"><strong>How it works</strong></a> ยท
<a href="#agents"><strong>Agents</strong></a> ยท
<a href="#contributing"><strong>Contributing</strong></a>
</p>

</div>

---

## ๐Ÿค” What is this?

Your AI coding agent (Claude Code, Codex, Gemini CLI, Copilot CLI) quietly writes a session log
of everything it does. **skiagram** reads those logs (read-only, fully offline) and tells you the
two things they never show you directly:

1. **Where your tokens actually went**, broken down by project, session, model, and token type,
   with the numbers *deduplicated and priced correctly*.
2. **Why your context window is full**: which MCP server, tool-definition set, or giant tool
   result is eating the window before you type a word.

Think *flamegraph for agent token spend*, not "daily usage table".

```bash
skiagram            # spend summary (auto-detects your agent)
skiagram tui        # interactive drill-down browser
skiagram context    # what's filling your context window, and why
skiagram flame      # export a flamegraph SVG of where the tokens went
```

---

<a id="features"></a>

## โœจ Features

Plain per-day usage tracking is a solved problem. skiagram exists because **the numbers themselves
are usually wrong**, and because nobody tells you **where the context window went**.

### ๐ŸŽฏ Correct accounting (getting the number right is the whole point)

On-disk logs are unreliable. Agents write **one JSONL line per content block**, and every line
repeats the request's token usage, so a single API request shows up as **2 to 10 lines sharing one
`requestId`** (real data we measured: 642 lines for 262 requests). Naive summation multiplies your
spend by that factor. skiagram:

- **Deduplicates per request** before summing anything (the core accounting step).
- **Prices cache reads, 5-minute cache writes, and 1-hour cache writes separately.** They differ by
  up to ~10ร—, and lumping them quietly inflates or deflates your bill.
- **Attributes extended-thinking tokens** as a *measured* share of output (they're already inside
  `output_tokens`, verified on 2,268 real requests, so we never invent a phantom undercount).
- **Treats absence as unknown, not zero.** A missing usage field becomes a stated *lower bound*; a
  model missing from the price snapshot is listed as *unpriced*, never guessed.

### ๐Ÿงฑ Context-bloat attribution

`skiagram context` breaks down what fills your window **by source** (system prompt, tool/MCP
definitions, history, attachments), **by MCP server**, and surfaces the **heaviest individual
items**, the one giant tool result that quietly dominates. It separates *measured, billed* tokens
from *estimated* composition and never blurs the two.

### ๐ŸŒณ Sub-agent attribution

Spawned sub-agents (the `Task` / `Agent` tool) write their **own** transcripts; most tools drop or
misattribute that spend. skiagram folds it back into the parent session and shows the sub-agent share.

### ๐Ÿ”ฅ Drill-down UX (TUI + flamegraph)

A navigable **TUI** (sessions โ†’ turns โ†’ context breakdown) and a literal **flamegraph SVG** export,
color-coded by token type with a legend, regroupable with `--group-by`.

### ๐Ÿ”’ Local-first ยท ๐Ÿ“ฆ single binary

No daemon, no proxy in the request path, no telemetry, **no network calls in the default build**.
One static binary you can drop anywhere.

---

<a id="install"></a>

## ๐Ÿ“ฆ Install

skiagram is a single static binary. Pick whichever channel fits your setup; every command is one line.

**Cargo** (any OS with Rust โ‰ฅ 1.85):

```bash
cargo install skiagram
```

**Prebuilt binary via [cargo-binstall](https://github.com/cargo-bins/cargo-binstall)** (no compile):

```bash
cargo binstall skiagram
```

**macOS and Linux (Homebrew):**

```bash
brew install TanvirAnjumApurbo/tap/skiagram
```

**macOS and Linux (shell installer):**

```bash
curl -LsSf https://github.com/TanvirAnjumApurbo/skiagram/releases/latest/download/skiagram-installer.sh | sh
```

**Windows (Scoop):**

```bash
scoop bucket add skiagram https://github.com/TanvirAnjumApurbo/scoop-bucket && scoop install skiagram
```

**Windows (winget):**

```bash
winget install TanvirAnjumApurbo.skiagram
```

**Windows (PowerShell installer):**

```powershell
irm https://github.com/TanvirAnjumApurbo/skiagram/releases/latest/download/skiagram-installer.ps1 | iex
```

**npm** (install globally, or run on demand with `npx`):

```bash
npm install -g skiagram
```

```bash
npx skiagram
```

**From source:**

```bash
git clone https://github.com/TanvirAnjumApurbo/skiagram && cd skiagram && cargo install --path crates/skiagram
```

Prebuilt archives for **macOS ยท Linux ยท Windows** are attached to every
[GitHub Release](https://github.com/TanvirAnjumApurbo/skiagram/releases).

---

<a id="usage"></a>

## ๐Ÿš€ Usage

Run it with no arguments to auto-detect your agent and print a deduplicated spend summary:

```bash
skiagram
```

### Commands

| Command | What it does |
| --- | --- |
| `skiagram summary` | ๐Ÿงพ Token + cost summary, deduplicated (the default command) |
| `skiagram context` | ๐Ÿงฑ What's filling your context window, by source, by MCP server, fat tail |
| `skiagram anomalies` | ๐Ÿšจ Fat-tail requests that dominate spend, plus retry storms |
| `skiagram classify` | ๐Ÿท๏ธ Spend broken down by task type (debugging / features / refactor / โ€ฆ) |
| `skiagram flame` | ๐Ÿ”ฅ Export a flamegraph SVG of token spend |
| `skiagram tui` | ๐Ÿ–ฅ๏ธ Interactive drill-down browser (arrow keys / `j` `k`, `q` to quit) |
| `skiagram watch` | ๐Ÿ“ก Live-tail: re-render the summary whenever session files change |

### Global flags

| Flag | Effect |
| --- | --- |
| `--agent <id>` | `claude-code`, `codex`, `gemini`, `copilot`, or `cursor`. Default: **auto-detect** |
| `--since YYYY-MM-DD` | Only count usage on/after this UTC date |
| `--refresh-pricing` | Refresh model prices from LiteLLM first (needs the `network` build feature) |

Most commands also accept `--json` for machine-readable output.

### Examples

```bash
skiagram summary --json --since 2026-06-01
```

```bash
skiagram --agent codex context
```

```bash
skiagram flame --metric cost --out spend.svg
```

```bash
skiagram flame --group-by project,model,type
```

```bash
skiagram flame --fold | flamegraph.pl > spend.svg
```

<details>
<summary><strong>Sample <code>summary</code> output</strong> (from the bundled synthetic fixtures)</summary>

```text
TOTALS (deduplicated)
 Requests   Input   Output   Cache read   Cache write   Total tokens   Est. cost
        6   5,600      980       11,000           500         18,080     $0.0319

requestId dedup: collapsed 2 duplicate line(s) into 6 request(s); naive per-line
summing would report 30,850 tokens (+71% overcount avoided)
extended thinking: used in 1 of 6 request(s); already counted inside Output above; visible thinking ~414 est. token(s)
sub-agent share: 1,300 tokens across 1 request(s) ($0.0087), attributed to parent sessions
```

Every number is traceable to `(model, token type, unit price)`, with no magic constants.

</details>

---

## ๐Ÿ”ฅ Flamegraph

`skiagram flame` turns your spend into a navigable flamegraph SVG. Frame **width = tokens** (or cost,
with `--metric cost`), and the default hierarchy is **project โ†’ session โ†’ model โ†’ token-type**:

```bash
skiagram flame --out spend.svg
```

- **Colored by token type.** `input`, `output`, `cache-read`, `cache-write`, and `thinking` each get
  a fixed swatch with a legend, so you read the graph by color instead of squinting at labels.
- **Readable sessions.** The opaque session UUID is shortened to an 8-char prefix.
- **Regroupable.** `--group-by project,model,type` drops or reorders levels. Regrouping never changes
  the totals; it only changes how the same spend is sliced.
- **Pipe-friendly.** `--fold` prints the raw folded stacks to stdout for any flamegraph tool.

Frame widths agree with `summary` **by construction**: the same request-level dedup feeds both, so
the picture and the table can never disagree.

---

<a id="how-it-works"></a>

## ๐Ÿงฎ How the accounting works

> Getting the number right is a **core feature**, not a footnote. Here is exactly what skiagram does,
> so you can trust (and challenge) every figure.

- **Dedup rule.** Assistant lines are grouped by `requestId`; usage counters take the field-wise
  **MAX** (lines either repeat identical usage or grow monotonically while streaming). Lines without
  a `requestId` are never merged.
- **Cache pricing.** cache-read ~0.1ร— input, 5-minute cache-write ~1.25ร— input, 1-hour cache-write
  ~2ร— input. Each is priced separately from an embedded snapshot of public prices.
- **Thinking tokens.** Claude Code's `output_tokens` *already includes* extended-thinking tokens
  (verified), so we never add an estimate on top. When an agent reports thinking as a *separate*
  count (Codex, Gemini), we keep it disjoint so the sum still balances.
- **Absence is not zero.** A missing usage field makes that total a stated lower bound; unpriced
  models are listed, not guessed.
- **Estimates, not invoices.** Costs come from public pricing and are labeled as estimates.

The embedded price snapshot keeps the default build fully **offline**. `--refresh-pricing` (behind the
opt-in `network` feature) updates it from LiteLLM and caches the result for later offline runs.

---

## ๐Ÿงฑ Context: what's filling your window?

```bash
skiagram context
```

```bash
skiagram context --json
```

Two kinds of number, kept strictly apart:

- **MEASURED** (real, billed tokens): your **startup overhead** (system prompt + tool definitions +
  memory files + first turn) that's already in the window on a fresh session before you type
  anything, plus the **peak/final window fill** across sessions.
- **ESTIMATED** (`~`-prefixed, from transcript sizes, *never billed*): the relative composition **by
  source**, **by MCP server**, and the **heaviest individual items**, the context "fat tail" where one
  giant tool result dominates.

Plus an exact **inventory**: which MCP servers are in play, how many tools were *deferred* (available
but not loaded, so they *don't* bloat the window, a common misconception), how many skills were listed,
and how many times the window filled up and got compacted.

---

<a id="agents"></a>

## ๐Ÿค– Supported agents

| Agent | Status | Notes |
| --- | :---: | --- |
| **Claude Code** | โœ… | discover + parse + dedup + sub-agent folding + thinking attribution |
| **Codex CLI** | โœ… | real token reconciliation (cumulative vs per-request delta) |
| **Gemini CLI** | โœ… | real per-message tokens, dedup by message id, disjoint thoughts |
| **Copilot CLI** | โœ… | structural (Copilot logs no per-request billing tokens) |
| **Cursor** | โฌœ | deferred: per-request `tokenCount` is ~99% zeroed; needs bundled `rusqlite` |

Adding a new agent is one trait implementation. See [Contributing](#contributing).

---

## โš™๏ธ Configuration

**Config file:** `config.toml` in your platform's config dir (resolved via `directories`; override the
whole path with `$SKIAGRAM_CONFIG`). Unknown keys are ignored, so it stays forward-compatible:

```toml
# Skip auto-detect and always read this agent unless --agent is passed.
default_agent = "claude-code"
```

Agent precedence: `--agent` flag โ€บ `default_agent` in config โ€บ auto-detect.

**Environment variables**

| Variable | Effect |
| --- | --- |
| `SKIAGRAM_LOG=debug` | See which lines were skipped leniently, and why |
| `SKIAGRAM_CONFIG` | Path to an alternate `config.toml` |
| `CLAUDE_CONFIG_DIR` | Override the Claude Code data root (default `~/.claude`) |
| `CODEX_HOME` / `GEMINI_HOME` / `COPILOT_HOME` | Override each agent's data root |

---

## ๐Ÿ”’ Privacy

Session files contain your prompts and source code. skiagram:

- opens them **read-only** and processes everything **in-memory, on your machine**;
- has **no network code in the default build**: no telemetry, no uploads, ever;
- ships only **fully synthetic** test fixtures (no real prompts, paths, or secrets).

It reads files your agents already write; it is **not** a proxy or interceptor in the request path.

---

<a id="contributing"></a>

## ๐Ÿ› ๏ธ Contributing

PRs welcome, especially **new agent adapters**. Each agent is one implementation of the `Adapter`
trait in `crates/skiagram-core/src/adapters/`:

```rust
pub trait Adapter {
    fn id(&self) -> &'static str;                               // "claude-code"
    fn detect(&self) -> bool;                                   // files present?
    fn discover(&self) -> anyhow::Result<Vec<SessionRef>>;      // find session files
    fn parse(&self, r: &SessionRef) -> anyhow::Result<Session>; // file -> normalized model
}
```

1. Implement the trait. Parsers must be **lenient**: skip unknown lines with a `tracing::debug`, never
   panic. A corrupt line must not abort a whole session parse.
2. Register it in `adapters::all()`.
3. Add **redacted/synthetic** fixtures under `fixtures/<agent>/` (no real prompts, paths, or secrets),
   plus an `insta` snapshot test and an `assert_cmd` CLI test. **Required.**
4. `cargo fmt && cargo clippy -- -D warnings && cargo test` must all pass.

The `skiagram-core` crate's module docs explain the architecture, the data-format notes, and the
correctness rules every change must honor.

---

## ๐Ÿง‘โ€๐Ÿ’ป Development

```bash
cargo build                          # debug build
cargo test                           # unit + snapshot + integration tests
cargo run -p skiagram -- summary     # run the CLI
cargo run -p skiagram -- tui         # run the TUI
cargo clippy -- -D warnings          # lint (CI-enforced)
cargo fmt --check                    # format check (CI-enforced)
```

The workspace is two crates: **`skiagram-core`** (pure domain logic: model, adapters, analysis,
pricing; no terminal I/O, no network) and **`skiagram`** (the CLI + TUI binary that owns all I/O).

---

## ๐Ÿ—บ๏ธ Project status

**skiagram `v0.1.0` is the first public release.** It ships:

- Correct, deduplicated token + cost accounting
- Adapters for **Claude Code**, **Codex CLI**, **Gemini CLI**, and **Copilot CLI**
- Context-window bloat attribution, sub-agent attribution, anomaly detection, and task classification
- Flamegraph SVG export, an interactive TUI, and live-tail (`watch`)
- A config file, optional online pricing refresh, and a fully offline default build

**Planned next:** a Cursor adapter (waiting on usable per-request token data), refreshable-pricing UX
polish, and a homebrew-core submission.

---

## ๐Ÿ“„ License

[MIT](LICENSE) ยฉ skiagram contributors.

---

<div align="center">

Built with ๐Ÿฆ€ **Rust** ยท Local-first ยท Offline by default

<a href="https://github.com/TanvirAnjumApurbo/skiagram/issues/new">๐Ÿ› Report a bug</a> ยท
<a href="https://github.com/TanvirAnjumApurbo/skiagram/issues/new">๐Ÿ’ก Request a feature</a> ยท
<a href="https://github.com/TanvirAnjumApurbo/skiagram/stargazers">โญ Star the repo</a>

<sub>If skiagram saved you some tokens, a โญ helps other people find it. Thank you!</sub>

<br><br>

<a href="#readme-top"><sub>โ†‘ Back to top</sub></a>

</div>