🔭 See how you actually use CodeBuddy Code — every 🧰 built-in tool, 🎯 skill, 🤖 agent, 🧩 plugin and 🔌 MCP tool.
- ✨ Features
- 🤔 Why this exists
- 🏗️ How it works
- 📊 The eight tabs
- 📋 Requirements
- 🚀 Install
- 🧭 Usage
- ⚙️ Configuration
- 🔒 Privacy
- 🧪 Testing
- 📁 Repository layout
⚠️ Limitations & roadmap- 🧹 Uninstall
- 📄 License
- 🧰 Eight views, one command — a dashboard, tools, skills, agents, plugins, MCP, tokens, usage.
- 📜 Full history, instantly — backfills every session already on disk.
- 🔑 Keyed by name — nothing ever disappears — every tab lists what you have ever used and what is installed, keyed by name alone: a renamed, updated or removed entity keeps its history, and the same name from two owners is one merged row.
- 🪶 Passive & read-only — no hook, no plugin, no CodeBuddy source changes.
- 🗃️ One local SQLite file — no network, no upload, no telemetry.
- ⚡ Fast incremental sync — ~5 s to index ~300 sessions / ~420 MB; only new bytes after that.
- 🖥️ TUI + headless — an eight-tab Textual UI for humans, JSON reports for scripts.
- 🔢 Real model tokens — per-model input / output / total plus cache hit / miss / write, straight from
providerData.rawUsage, with a Provider column (Built-in, or a third-party label fromproviderData.requestModelId); total prefers the provider's own reported value. - 📊 Calendar-day windows, per tab — the top-bar range (Today / 2 days / 3 days / 7 days / 30 days / All time) is kept per tab, so changing one never moves another: entity tabs default to 7 days, the Dashboard and the Usage page to Today, and Plugins is always all-time. Windows are whole local calendar days (00:00 → now), never "now minus N hours". A window only shrinks the counts — the list stays full.
- 📅 Custom date range — the top bar's 📅 control (or Custom… in the range list) opens a picker with shortcut chips, an editable START TIME and END TIME (a date plus a 24-hour time), and a month calendar: ‹ › step the month and, at the year's ends, the year. Under each card a quick-time row puts the day's edges one click away — the start offers
00:00/06:00/12:00/18:00/23:59, the end adds now — and a 24-cell hour grid under the calendar sets the hour of whichever card is active, so a date and its hour are picked the same way. Clicking a card aims both grids, so the next day or hour you click lands in that card; ↑/↓ and the wheel nudge the focused field by the step you pick (1m/10m/1h). Dates and times may be typed loosely —10-1is10/01,9:5 AMand21:05are both21:05. The choice is kept per tab, and the top bar always shows the span it is showing. - 🔎 Two-level drill-down — Enter or double-click a row on the Tools, Skills, Agents, Plugins, MCP or Tokens tab: the first screen summarises that entity (its counts and totals in the tab's own window) above its recent calls; Enter a call to see every field of that single call, laid out to fill the screen.
- 🔐 Metadata only — names, timings, statuses, projects. Never message bodies or argument values.
CodeBuddy already writes the raw data you need — it just never shows it. It keeps:
- 📜 session transcripts in
~/.codebuddy/projects/<cwd>/<session>.jsonl(every tool call is afunction_callrecord with name, arguments, status, timestamps); - 🔭 OpenTelemetry spans in
~/.codebuddy/traces/<pid>/trace_*.jsonl(function/agent/generationspans);
…yet only exposes session-level /cost and /context. There is no cross-session,
per-tool / per-skill / per-plugin breakdown. That gap is what cbut fills.
~/.codebuddy/ cbut ~/.local/share/
┌──────────────────┐ ┌─────────────────────┐ ┌────────────────────────┐
│ projects/ │ │ cbut-sync.py │ │ codebuddy-usage- │
│ **/*.jsonl ───┼── read ──▶ parse + classify │ │ tracker/usage.db │
│ │ (only) │ incremental, │ write │ │
│ traces/ │ │ idempotent ├─────────▶│ SQLite (tools · │
│ **/*.jsonl │ └─────────────────────┘ │ skills · agents · │
│ │ ▲ │ plugins · mcp) │
│ mcp.json │ │ └───────────┬────────────┘
│ settings.json │ ┌─────────┴───────────┐ │
│ plugins/ │ │ cbut-tui.py (TUI) │◀────── query ─────────┘
└──────────────────┘ │ cbut-stats.py (CLI)│
untouched └─────────────────────┘
cbut is a passive reader: it parses the logs CodeBuddy already writes and builds an
index. It never touches CodeBuddy's files, installs no hook, and adds no runtime overhead.
The Dashboard opens first: an at-a-glance summary of the important numbers, so you know where to look before drilling in. A shared top bar above the tabs carries the range selector, and each tab keeps its own range — changing one tab's window never moves another's. The entity tabs (Tools, Skills, Agents, MCP, Tokens) default to 7 days; the Dashboard and the Usage page default to Today. The Plugins tab has no window at all: its list is all-time, so the selector is hidden there. A window only changes the counts; it never drops a row, because every tab is keyed by name.
| Tab | What it shows | |
|---|---|---|
| 🧭 | Dashboard | a grid of panels, all for the Dashboard's own window: usage KPIs (tool/skill/agent/MCP/plugin counts), a token summary (requests, input/output, API total, usage total, cache hit rate), absolute cache hit/miss/write, runtime health (completed/incomplete, average tool ms, distinct sessions/projects), Top 5 models and Top 5 tools, and a per-day call sparkline |
| 🧰 | Tools | tool calls by name, completion, average duration, last used |
| 🎯 | Skills | skill invocations by name (merged across owners), last use |
| 🤖 | Agents | subagent types by name, call count, last use |
| 🧩 | Plugins | every plugin — installed and ever used — with its skills/agents/commands; all-time, no version column |
| 🔌 | MCP | MCP servers and tools, invocation counts |
| 🔢 | Tokens | per-model usage total, requests, input, output, API total, cache hit/miss/write |
| 📊 | Usage | request log with summary panels (cc-switch-style columns) |
Press Enter (or double-click) on any row for a two-level drill-down. The first screen summarises the entity — its counts and totals in that tab's own window — above its recent calls; press Enter on a call to open every field of that single call (timestamp, project, session, status, duration, ids, and the token/cache detail for a model response). It covers every entity tab except Dashboard and Usage — tools, skills, agents, plugins, MCP tools and models. Every tab shows an explicit empty-state message when it has no data yet, instead of a blank region.
Every tab lists the union of "ever used" and "installed", keyed by name, sorted by call
count. A deleted skill, an uninstalled plugin or a renamed agent keeps its row and its
count; a name that appears under two plugins is merged into one. Plugins counts every
invocation attributed to a plugin — its skills, agents and slash commands — so a plugin
whose skill was used no longer reads uses = 0. Attribution is evidence-based: a name is
credited to a plugin only when the plugin inventory maps that name to it.
The last column of every entity tab is state — the one thing that separates "still
here" from "never left": in use (installed, and called at least once at any point in
history), installed (on disk right now, never called), removed (gone from disk, kept
for its history, drawn dim). The installed side is a snapshot rebuilt from disk on
every sync, so uninstalling something drops it from the installed column on the next
sync without touching the counts it already earned. "Ever" is deliberately outside the
time window: a tool last used last week still reads in use in a one-day window.
API Total = the provider's reported total when present, otherwise Input + Output. Usage Total is a display-only metric (
Input + Output + cache hit) that re-adds cache hit — which is already inside Input — so it lands at ~2× the API Total. Never read it as the API total. Cache hit/miss/write are reported separately and are never added into the API total. Fields absent from a response are shown as-, never as a fabricated0.
The Usage tab mirrors the style of a session-usage panel, but every number comes from the transcripts on disk:
- The Usage page keeps its own range (default Today), shown in the shared top bar while the Usage tab is active; every other tab keeps its own too, so moving one never moves another. Options are Today / 2 days / 3 days / 7 days / 30 days / All time — whole local calendar days ending at the current time (00:00 → now), not "now minus N hours" and not from when the tracker first ran. Pick Custom… (or the 📅 control) to choose any start/end down to the minute. Its Window / Requests panel shows the dates of the active range.
- One Request Logs list, newest first. The columns follow cc-switch's request records — time · model · usage total · input · output · API total · cache hit (read) · cache miss · cache write · cache hit rate. cc-switch's provider, cost, duration and HTTP-status columns have no transcript source, so they are omitted rather than estimated. The priority columns sit leftmost, and on a narrow terminal a column plan drops the tail instead of cutting it mid-label — the line above the table then names what is hidden and how wide the terminal has to be to show all of it.
- Same accuracy rules as Tokens — API Total = provider total when present, else
Input + Output. A separate Usage Total (Input + Output + cache hit) is shown for reference, explicitly labelled as re-adding cache hit (~2× the API total). Cache hit/miss/write are never folded into the API total, and missing values stay-. - Cache hit rate =
cache hit / (cache hit + cache miss + cache write)— the cacheable input, matching cc-switch. It shows-when a row has no cache data. - The summary panels are height-capped and scroll internally, so the Request Logs table below them stays visible even on a small terminal. Measured at 80×24: the panels go two-up and stay capped, the table keeps 9 rows (a test holds the floor at 6), and the column plan shows the first 5 of its 10 columns with a note above the table naming the other five. Which five depends on the data — the widths are measured from the rows, not assumed — so both counts here are readings from this machine's own database at 80×24, and yours will differ. Nothing is cut mid-label.
- The plan covers every table, not the two that were complained about — the seven tables
that live in the eight tabs (Dashboard draws panels, not a table) and both detail screens
(a row's history, a model's responses): nine in all, counted out of the source rather than
estimated, and a test fails if a table ever appears without a plan. A name long enough to push
a column off the screen therefore costs columns, not legibility. Two columns is the floor: when
even a name and its first number do not fit an 80-cell terminal, the note says
even these are cutrather than implying the table fits. - A cell that is far longer than it earns gets capped, at 28 characters by default — the
number is a setting (
name_cap), and the cap is measured on this machine's data, where every entity name fits (tool 22, model 24, agent 20, plugin 19, skill 28) and the one column that runs past it is the project path (162 of 482 rows over 24, longest 74). A path is capped from the left (…/work/backend-service) because its beginning is the prefix every row shares and its end is what distinguishes them; anything else is capped from the right, keeping the readable head. Nothing is silently shortened: the note prints the number in force (cells capped at 28), and the cap only engages when a column would otherwise be dropped — a wide terminal shows the whole value. Two rows whose names share the visible prefix still open their own detail screen, because a row is keyed by the uncapped value. On this machine the cap buys back three columns on the History screen at 80×24 (2 of 5 became 5 of 5) and changes nothing else.
| 🐧 OS | Linux — developed and verified on Ubuntu; other distributions unverified |
| 🐾 CodeBuddy | CodeBuddy Code — the schema is verified against the logs on your machine, not against a version number: no transcript line carries one, and the CLI here is launched through a shell alias, so its version is not machine-confirmable. See docs/maintenance/compatibility.md |
| 🐍 Python | 3.11+ — the full suite run green on 3.11, 3.12, 3.13 and 3.14 (CI runs 3.11 and 3.14). Headless commands need only the stdlib |
🖥️ textual |
only for the interactive TUI (installed by install.sh) |
git clone https://github.com/SHADE-glitch/codebuddy-usage-tracker.git
cd codebuddy-usage-tracker
./install.sh # creates .venv with textual, links ~/.local/bin/cbut
cbut sync # build the index from your existing CodeBuddy logs
cbut # launch the TUIPrefer no venv? The headless reports work with plain python3:
python3 scripts/cbut-sync.py
python3 scripts/cbut-stats.py statsTwo user units ship in systemd/. Installing them runs
cbut sync --quiet once a day — these are the same four lines install.sh prints at the
end of a successful install:
mkdir -p ~/.config/systemd/user
cp systemd/cbut-sync.* ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now cbut-sync.timerPersistent=true catches up a run missed while the machine was off, the wake-up is
jittered by up to 15 minutes, and the service runs at Nice=10 — so it never competes
with your session for CPU. Check it with systemctl --user status cbut-sync.timer.
cbut # 🖥️ interactive TUI (eight tabs)
cbut sync # 🔄 incremental index of new log data
cbut sync --full # ♻️ rebuild the database from scratch
cbut stats # 📊 overview of all categories
cbut tools|skills|agents|plugins|mcp
cbut models # 🔢 per-model tokens: in/out/total + cache hit/miss/write
cbut show tool Bash # 🔍 recent calls for one entity
cbut recent # 🕒 latest tool calls
cbut inventory # 📦 what is installed, used vs unused
cbut export # 💾 dump every table as JSON
cbut health # 🩺 database + data-source check
cbut backup # 💾 snapshot before a risky step (sync --full does this)
cbut restore # ⏪ list snapshots; `cbut restore NAME` rolls one back
cbut format # 🧾 the CodeBuddy fields and paths this tool depends on| Variable | Default | Purpose |
|---|---|---|
CBUT_DB |
~/.local/share/codebuddy-usage-tracker/usage.db |
database location |
CBUT_CODEBUDDY_DIR |
~/.codebuddy |
source log directory |
CBUT_PRODUCT_JSON |
located from the codebuddy executable on PATH |
the installed package's tool/agent catalogue. Unset, cbut walks PATH for codebuddy and reads product.json above it; with no CodeBuddy installed it falls back to a checked-in list and says so in cbut health. Pin it to test a catalogue, or to read one the PATH cannot find. |
Every knob has the value the app already shipped with, and the file is optional: cbut behaves
exactly as documented until you add one.
~/.config/cbut/config.toml — or point CBUT_CONFIG somewhere else:
top_n = 5 # rows in the Dashboard's Top models / Top tools
log_limit = 100 # request rows on the Usage page
detail_limit = 200 # rows on a history / model-response screen
name_cap = 28 # widest cell a table renders before it cuts the value
refresh_secs = 5 # TUI redraw timer
sync_secs = 30 # TUI auto-sync timer- Precedence is environment > file > default. Each key also reads from
CBUT_TOP_N,CBUT_LOG_LIMIT,CBUT_DETAIL_LIMIT,CBUT_NAME_CAP,CBUT_REFRESH_SECSandCBUT_SYNC_SECS. - A broken file stops the TUI with the path and the reason rather than quietly starting on
defaults — being ignored while you edit it is the worst outcome a settings layer can offer.
cbut healthprints the same verdict, including which file it read. - Unknown keys are refused: a typo'd setting would otherwise do nothing, silently, forever.
Values are type- and range-checked (
top_n = 0is a blank panel, not a preference). - Parsed by the standard library's
tomllib, so this adds no dependency and no network path. Nothing ever writes this file, and no setting is stored in the database.
cbut reads only the structured metadata fields of CodeBuddy's own logs and stores
counts, timings, statuses and identifiers. It does not read or store prompt/response
text, tool argument values, or file contents. 🔒 Nothing leaves the machine.
python3 -m unittest discover -s scripts/tests # Ran 492 tests ... OKThe suites are plain unittest (stdlib only, so pytest discovers them too). Each one
builds its own throwaway database in a temp directory — your real usage.db and your
~/.codebuddy logs are never opened, and no code path in this repository reaches the network
(test_privacy.py holds the import allowlist that keeps it that way).
| Suite | Tests | Covers |
|---|---|---|
test_sync.py |
126 | transcript parsing, tool classification, the route-origin column (requestModelId → built-in / vendor / host / endpoint), incremental vs --full re-sync, crash recovery, unparsed-record accounting |
test_tui.py |
120 | tab wiring and report shapes through Textual's own run_test harness, the per-tab date picker (calendar month/year stepping, the active-card target, preset and quick-time chips that fit their column, the 24-hour clock the fields read and write, the hour grid, the nudge-step selector, the top-bar span surviving a tab switch), the two-level drill-down (the windowed summary header, Enter opening a call's full detail, a long value folding instead of clipping, the plugin path), narrow-terminal layout and the column plan that replaces a clipped tail on every table, the cell cap and the setting behind it, status-line truthfulness |
test_usage.py |
100 | the calendar-day windows (Today / 2 / 3 / 7 / 14 / 30 days / all) and their custom-range labels, the typed date/time normalisation (24-hour display, 12-hour still accepted on input), the token totals and provider grouping, the Dashboard queries, the drill-down queries (windowed per-entity summaries, plugin use counts, a single call's full row), the schema gate, and which indexes earn their keep |
test_stats.py |
28 | the headless report CLI (cbut stats): the overview and the per-category tables, the model/token report and the cache columns it drops on a narrow terminal, show/recent/inventory/export, health (schema version, settings line, unparsed accounting), the backup→restore round trip, the generated format surface, and the empty-state (none) notice |
test_config.py |
23 | the settings layer: defaults unchanged, file, env precedence and bounds, a broken file that stops main() before the TUI starts, the keys the app actually reads, and every key cbut health reports — with the environment hidden, so a configured shell cannot turn these into false failures |
test_dispatcher.py |
12 | bin/cbut: subcommand routing, venv resolution, help without a resolvable install |
test_format_registry.py |
10 | the CodeBuddy format registry and the parser agree, both directions |
test_privacy.py |
11 | import allowlist plus the calls that bypass it (__import__/eval/import_module), no free-text value in any column, CodeBuddy's own files untouched |
test_hermetic.py |
13 | a runtime tripwire: the suite never opens the real usage.db (no in-process sqlite3.connect, no child that would resolve it without CBUT_DB or --db), and never reads the real host footprint (~/.codebuddy — no open, directory walk or child without CBUT_CODEBUDDY_DIR) |
test_snapshots.py |
9 | automatic snapshots before destructive steps, listing, restore, pruning |
test_maintenance_docs.py |
15 | the generated format doc cannot drift from the code, and no document may state a CI version the workflow does not run |
test_readme_counts.py |
5 | the suite table above is the suite table the runner would print, and both pages document every setting the loader knows |
test_record_coverage.py |
6 | every covered commit is recorded in CHANGELOG.md |
test_readme_bilingual.py |
5 | the two READMEs stay one document in two languages, and every table-of-contents link has a heading to land on |
test_conventions.py |
9 | the dimension-A root layer is present at the root, AGENTS.md names its dimension in one line instead of quoting the standard's body, and no runtime state the ignore list covers is tracked |
test_readme_counts.py is what keeps this table honest: a suite added without a row, or a row left
at an old number, fails the suite.
| Path | What it is |
|---|---|
bin/cbut |
bash entry point. Bash on purpose: it must still print a useful error when the TUI venv is missing, so it cannot depend on Python. Prefers ./.venv/bin/python, falls back to python3 |
install.sh |
creates the venv (uv when available) and symlinks ~/.local/bin/cbut. Idempotent; refuses to replace a non-symlink unless --force |
scripts/cbut_db.py |
SQLite schema and the query helpers every entry point shares |
scripts/cbut-sync.py |
log parser and incremental indexer (--full, --quiet) |
scripts/cbut-stats.py |
headless reports: stats · tools · skills · agents · plugins · mcp · models · show · recent · inventory · export · health · backup · restore · format |
scripts/cbut-tui.py |
the eight-tab Textual UI |
scripts/tests/ |
the 334 tests above |
docs/maintenance/ |
what to re-check when CodeBuddy changes: the generated format-dependency surface, the version/compatibility matrix, and the size & performance baseline with the commands that produced it |
systemd/ |
optional daily sync service + timer |
requirements.txt |
textual>=8.2,<9 — TUI only; everything else is stdlib |
Both paths are overridable for a sandboxed install: CBUT_SCRIPTS and CBUT_VENV for the
launcher, CBUT_DB and CBUT_CODEBUDDY_DIR for the data (see Usage).
- 🔌 MCP usage is sparse until you actually invoke an MCP tool; the tab is populated from
real calls (
mcp__<server>__<tool>), including calls wrapped by the deferred-tool mechanism (DeferExecuteTool). - 🤖 Internal agents (
autoModeClassifier,summaryGenerator, …) live in the OTel traces; trace ingestion is planned but not yet wired into v1. - 🏷️ Provider is read, not guessed. The Usage and Tokens tabs carry a Provider column:
Built-in, or a third-party label derived fromproviderData.requestModelId— the vendor name of a known endpoint url, else that host, else the endpoint tag. It is never inferred from the model name, which a custom endpoint's alias can make look built-in. - 🔢
context tokensvs model tokens. The headlesscbut statsprints a session-levelcontext tokensfigure (turn-metricstokenDelta) next to the per-model token totals; the two are deliberately kept apart. Thecontext tokenssum is idempotent — repeated syncs and schema migrations no longer inflate it. - 🔄 Sync is manual (or via the optional systemd timer); there is no live hook.
cbut touches exactly three places outside the repository, and removing them leaves
nothing behind:
systemctl --user disable --now cbut-sync.timer 2>/dev/null # only if you installed it
rm -f ~/.config/systemd/user/cbut-sync.service ~/.config/systemd/user/cbut-sync.timer
systemctl --user daemon-reload
rm -f ~/.local/bin/cbut # the symlink install.sh made
rm -rf ~/.local/share/codebuddy-usage-tracker # the SQLite indexDeleting the repository directory (with its .venv) finishes it. CodeBuddy's own files
were only ever read, so there is nothing to restore there. 🗑️
MIT — see LICENSE. © 2026 SHADE-glitch