Skip to content

Latest commit

 

History

History
426 lines (343 loc) · 23.2 KB

usage.md

File metadata and controls

426 lines (343 loc) · 23.2 KB

ccxray usage — Output Schema Reference

ccxray usage summarizes your logged Claude Code / Codex traffic straight from ~/.ccxray/logs/index.ndjson — no running server required. The --json output is an agent-facing contract: small (target <4KB) with a stable shape — the key set and field types below don't change without a deliberate, changelogged update. Field values are derived from the index plus your arguments, with a few inputs resolved at run time: --last is relative to wall-clock now and --cwd ~ expands to the local home dir (see Filter semantics), and skills[].scope is read from the local filesystem (see the skills note). So treat the shape as the contract; expect values to track the index and the runtime environment.

The single source of truth for the shapes below is server/usage.js: analyze() builds the single-scope object, while run() assembles grouped, multi-cwd comparison and error objects. groupTurns() in server/usage-query.js builds each grouped rollup. This doc mirrors them; if they ever disagree, the code wins — and that disagreement is a bug to fix here.

Contract note. Because agents consume --json, the field set, types, and the top-level shapes (single-scope object, multi-cwd object, grouped object, error object) are treated as a contract. Shape changes must be deliberate and noted in the changelog below. The usage --json shape contract block in test/usage.test.js locks every section's exact key set and field types (plus the multi-cwd and error shapes), so an accidental field add/remove fails CI — a deliberate change must update both that test and this doc in the same commit. A separate test guards the <5KB size ceiling (the <4KB above is the working target that sits under that ceiling).


Output modes at a glance

--json emits a single line of compact JSON.stringify output to stdout. The output shape depends on the arguments:

Condition Shape Exit
--by <dimension> Grouped object 0
Default (0 or 1 --cwd, without --by) Single-scope object 0
--cwd a,b (2+ values, without --by) Multi-cwd object 0
No index / no matching entries Error object 1

Rounded numeric fields are rounded to at most the number of decimals noted per field. The value is coerced back to a JSON number (+x.toFixed(n)), so trailing zeros are dropped — you get 0.5, not 0.50, and an exact 0 rather than 0.00. Treat the per-field precision as a maximum, not a fixed width; the rounding cap is the contract.


1. Single-scope object (default)

{
  "meta":     { "totalEntries": 0, "totalSessions": 0, "totalCost": 0,
                "retentionDays": 14, "retentionCutoff": "YYYY-MM-DD",
                "retentionWarning": "older history may have been removed; totals are a lower bound",
                "timeRange": { "from": "ISO|null", "to": "ISO|null" } },
  "sessions": { "count": 0, "byProvider": { "<provider>": 0 },
                "subagentRatio": 0,
                "turnDistribution": { "min": 0, "median": 0, "p75": 0, "max": 0 },
                "topSessions": [ /* ≤10, see below */ ] },
  "models":   [ /* ≤10, see below */ ],
  "tools":    { "totalCalls": 0, "top": [ /* ≤7 (all with --tools) */ ],
                "failRate": 0 },
  "skills":   [ /* see below; may be [] */ ],
  "prompts":  { "hashStability": { "sysHash": {…}, "toolsHash": {…}, "coreHash": {…} } },
  "cache":    { "hitRate": 0, "totalInputTokens": 0,
                "totalOutputTokens": 0, "totalCacheReadTokens": 0 },
  "gapCache": [ /* only non-empty buckets */ ]
}

meta

Field Type Notes
totalEntries number Count of entries (turns) after all filters.
totalSessions number Distinct sessionId count. Entries with no session id collapse into one "unknown" bucket, which counts here.
totalCost number Sum of per-turn cost, USD, 2 dp. Unpriced turns add nothing, so costAgg.unknownCount > 0 makes this a lower bound.
costAgg object Confidence fold for totalCost, same shape and rules as topSessions[].costAgg below.
unpricedModels string[] Every model with at least one unpriced turn (no rate, cost.confidence: "unknown"), sorted, not capped. Empty when the total is fully priced. Human output appends (N model(s) unpriced) to the $X+ total.
retentionDays number | null Active LOG_RETENTION_DAYS value. 0 or below disables retention. null means the setting is not a complete decimal integer (for example, 1e9, 0x10, or 14 days) or is beyond the supported window (magnitude over 36500 days ≈ 100 years, i.e. a "never prune" setting); retention is inactive, so no cutoff or warning is produced.
retentionCutoff string | null Taipei-local YYYY-MM-DD cutoff used by log pruning; null when retention is disabled.
retentionWarning string (conditional) Present only when the requested range starts before retentionCutoff: older history may have been removed, so all totals are a lower bound. An all-time query without a start bound always qualifies when retention is enabled.
timeRange.from string | null Earliest receivedAt as ISO 8601, or null if no timestamps.
timeRange.to string | null Latest receivedAt as ISO 8601, or null.

sessions

Field Type Notes
count number Same value as meta.totalSessions.
byProvider object Map of provider → turn count (e.g. { "anthropic": 42 }). Missing provider → "unknown". Counts turns, not sessions.
subagentRatio number subagent turns / totalEntries, 3 dp, range 0–1.
turnDistribution object { min, median, p75, max } of turns-per-session. Percentiles take the value at zero-based index floor(q × count) (capped at the last element), so e.g. with 2 sessions median is the upper value, not an average.
topSessions array Up to 10 sessions, sorted by cost descending. The synthetic "unknown" and "direct-api" sessions are excluded here (they remain in meta/sessions.count).

Each topSessions[] element:

Field Type Notes
sessionId string Full session id.
turns number Turns in the session.
cost number USD, 2 dp.
costAgg object Confidence fold for cost, always present: {count, fallbackCount, fallbackCost, unknownCount}, all numbers. fallbackCount/fallbackCost cover turns priced from a default rate; unknownCount counts turns that contributed nothing to cost, so a non-zero value means cost is a lower bound. Render cost through this fold (formatAggCost/formatAggCostText, ADR 0017) — a consumer of this JSON cannot see the turns, so it cannot re-derive the fold, and printing cost alone reports a fabricated or under-counted total as an exact one.
durationMin number First→last receivedAt span in minutes, 1 dp. 0 if fewer than 2 timestamped turns.
title string | null Best non-continuation title, truncated to 40 chars. null if none.
branch string | null Git branch of the session's latest-received turn that recorded one (see branch in data-model.md). null when no turn recorded a branch (outside git, detached HEAD, or entries older than branch recording).
model string The session's dominant model (most turns), as a single string — not a map. "unknown" if absent.
provider string Provider of the first turn.

models

Array of up to 10 models, sorted by turns descending.

Field Type Notes
model string Model id. "unknown" if absent.
turns number Turns on this model.
cost number | null USD, 2 dp. null when no turn on this model was priced (costStatus: "unknown"); never 0 for an unpriced model.
costShare number | null model cost / totalCost, 3 dp, range 0–1. The denominator is the priced total, so unpriced turns never enter share math; null when costStatus is "unknown".
costAgg object Confidence fold for cost, same shape as topSessions[].costAgg.
costStatus string "priced" (every turn priced), "partial" (some turns unpriced: cost is a lower bound), or "unknown" (no turn priced). Human output prints unknown for an unknown row and renders the others through formatAggCostText.

tools

Field Type Notes
totalCalls number Total tool invocations across all entries. Per turn, recorded counts (turnToolCalls, else the legacy per-session max of toolCalls) win; a turn with neither (imported transcripts) is counted from its turnToolCallIds map. A null map is unknown and adds nothing; {} is a parsed zero-call turn (ADR 0018).
top array [{ name, count }], sorted by count descending. Capped at 7 by default; --tools lifts the cap to all tools.
failRate number entries with a tool failure / totalEntries, 3 dp. Note the denominator is turns, not tool calls.

skills

Array of skills, sorted by invocations descending. May be [].

Field Type Notes
name string Skill name (may be namespaced, e.g. superpowers:brainstorming).
invocations number Total times invoked.
loads number | null Distinct sessions that invoked it (a proxy for unique "loads"). null only on the synthetic legacy row.
scope string | null "user", "project", "plugin", or null. Resolved by scanning skill directories at analysis time, so a since-deleted skill reads as null.

Legacy row. Entries logged before per-skill tracking (skillCalls) existed contribute a single appended row { "name": "(pre-tracking)", "invocations": N, "loads": null, "scope": null }, where N is the count of generic Skill tool calls from those old entries. It is pushed after the sorted skills, so it is not part of the descending-invocations order.

prompts.hashStability

How often the system prompt / tools / core prompt change between adjacent turns of a session — a window into prompt-cache churn.

"hashStability": {
  "sysHash":   { "changeRate": 0, "pairs": 0, "label": "never" },
  "toolsHash": { "changeRate": 0, "pairs": 0, "label": "never" },
  "coreHash":  { "changeRate": 0, "pairs": 0, "label": "never" }
}
Field Type Notes
changeRate number changed pairs / pairs, 4 dp, range 0–1.
pairs number Adjacent turn pairs (within a session) where both turns carry that hash.
label string Bucketed changeRate: > 0.5 → every-turn, > 0.1 → frequent, > 0.01 → occasional, > 0 → rare, = 0 → never.

cache

Field Type Notes
hitRate number cache-read tokens / all input tokens, 3 dp, range 0–1.
totalInputTokens number All input tokens = input + cache_creation + cache_read, summed. (Not just input_tokens.)
totalOutputTokens number Sum of output_tokens.
totalCacheReadTokens number Sum of cache_read_input_tokens.

gapCache

Cache hit rate bucketed by the idle gap before a turn (time since the previous turn finished). Only buckets that contain at least one measured gap appear — empty buckets are omitted, so this array can have 0–5 elements.

Field Type Notes
gap string One of "<30s", "30s-5m", "5-15m", "15-60m", ">60m".
turns number Inter-turn gaps measured in this bucket (not sessions).
avgHitRate number Mean per-turn cache hit rate in the bucket, 3 dp.
medianHitRate number Median per-turn cache hit rate, 3 dp.

2. Multi-cwd comparison object

When two or more --cwd values are given (e.g. --cwd proj-a,proj-b), the output is a per-project comparison object instead of the single-scope object. projects is sorted by cost descending; meta carries the retention disclosure for the query as a whole:

{
  "projects": [
    { "cwd": "/work/project-alpha", "cost": 0.80, "sessions": 1, "turns": 2, "cacheHit": 0.86 },
    { "cwd": "/work/project-beta",  "cost": 0.30, "sessions": 1, "turns": 2, "cacheHit": 0.78 }
  ],
  "meta": {
    "retentionDays": 14, "retentionCutoff": "YYYY-MM-DD",
    "retentionWarning": "older history may have been removed; totals are a lower bound"
  }
}

meta holds exactly the three retention fields documented for the single-scope meta, with the same semantics. Retention is a property of the query, not of any one project, so it appears once at the top level and never on a projects[] row — these per-project totals sit behind the same window as the single-scope ones and would otherwise be presented as exact.

Field Type Notes
cwd string Working directory (group key). Always a real path — the --cwd filter that triggers this mode drops entries with no cwd, so no "unknown" row appears here.
cost number That group's own single-scope meta.totalCost, 2 dp. Not a field of the top-level meta above, which carries only the retention triple.
sessions number That group's own single-scope meta.totalSessions.
turns number That group's own single-scope meta.totalEntries.
cacheHit number That group's own cache.hitRate, 3 dp.

The grouping is over the entries that already passed --last/--cwd/--session filtering, so every matched cwd that survived appears as one row.


3. Error object

On failure, --json prints a single error object and exits with code 1:

{ "error": "<reason>", "hint": "<actionable hint>" }
error When hint
"no logs found" index.ndjson does not exist under the active CCXRAY_HOME. Names the resolved home so you can fix CCXRAY_HOME.
"no matching entries" The index exists but no entry survived the filters. Suggests loosening a specific filter (or notes the index is empty).

Without --json, the same conditions print a human message to stderr (still exit 1). Invalid --last durations also exit 1 with a stderr message, before any JSON is produced.


Filter semantics

These apply to both human and --json output. Grouped reports use the shared server/usage-query.js read model: responseId copies are merged (ADR 0012), non-Anthropic sessions use one source (proxy if observed, otherwise import), and stored stale costs are repriced in memory through the existing pricing helper. These steps precede attribution and time filters. Grouped reports read the whole index before selection so even an out-of-window proxy observation fixes the Codex session's source. Rows with neither a model nor usage are markers and do not count as turns in grouped reports. Non-grouped reports preserve legacy rows, including cost-only rows, and apply time/cwd filters before repricing and responseId deduplication. No index files are modified.

--agent-id <id> and --branch <name>

Both accept comma-separated values and repeated flags. Values match recorded fields exactly and case-sensitively; values within a flag are ORed, while agent, branch, cwd, session and time constraints are ANDed.

agentId is the reliable key for per-run attribution. cwd is only a fallback when one agent owns the worktree: multiple agents sharing a cwd contribute to its combined total. Missing identities remain visible in nullAgentIdTurns and the null agent group. Attribution reflects recorded identity; incorrectly inherited identity or cwd cannot be corrected by a query.

Branch is the latest recorded per-turn branch field, describing what was recorded at the time. Querying never consults git or the current checkout. A run that switches branches splits across branch groups. Rows without a recorded branch count under key: null and nullBranchTurns; no session-wide branch is substituted.

--from <ISO> / --to <ISO>

Absolute timestamps require a date, time with seconds, and timezone, for example 2026-06-01T10:00:00Z or 2026-06-01T18:00:00+08:00. Fractional seconds are accepted. --since aliases --from; --until aliases --to. Either bound can be omitted. Selection uses from <= receivedAt < to (start inclusive, end exclusive); equal bounds describe an empty range. Invalid timestamps, nonexistent calendar dates, reversed bounds, missing values, and combining any absolute bound with --last exit 1 with a clear stderr error.

ccxray usage --json --by agent --agent-id herdr:w1:p1,herdr:w1:p2 \
  --from 2026-06-01T00:00:00Z --to 2026-06-02T00:00:00Z
ccxray usage --by branch --branch main --branch feature \
  --since 2026-06-01T00:00:00Z --until 2026-06-02T00:00:00Z

Grouped object

--by agent|branch|cwd|session selects a grouped output in both text and JSON, including when multiple cwd values are supplied. JSON emits one compact line:

{
  "by": "agent",
  "groups": [{
    "key": "herdr:w1:p1",
    "turns": 1,
    "tokens": { "input": 10, "cacheRead": 20, "cacheWrite": 30, "output": 40, "reasoning": 5 },
    "cost": 0.01,
    "costFold": { "count": 1, "fallbackCount": 0, "fallbackCost": 0, "unknownCount": 0 },
    "models": { "dummy-model": 1 },
    "firstTs": "2026-06-01T10:00:00.000Z",
    "lastTs": "2026-06-01T10:00:00.000Z"
  }],
  "nullAgentIdTurns": 0,
  "meta": { "retentionDays": 14, "retentionCutoff": "2026-05-19" }
}

key is the recorded agentId, branch, cwd or sessionId string, or JSON null. Groups sort by key ascending with null last. Counts and tokens are sums; models maps model IDs to turn counts. firstTs/lastTs are the earliest/latest selected timestamps as ISO strings, or null when unavailable. Reasoning uses recorded thinkingTokens, then output token details (reasoning/thinking), then reasoning_output_tokens; absent evidence contributes zero. Reasoning can be part of output tokens, so do not add it to output to calculate total tokens. Grouped costs retain numeric precision and use the existing aggregate ADR 0017 costFold; consumers must render cost with this fold. Unpriced calls contribute zero cost and increase unknownCount; human output uses formatAggCostText. nullAgentIdTurns counts selected model calls without agentId for every grouping; nullBranchTurns is also present when by is branch. The meta retention fields follow the existing output rules, including conditional retentionWarning. Null counts reflect the selected scope after all filters. The number of groups is not capped; the compact default-output size target does not apply to grouped output.

--last <d/h/m>

Keep entries with receivedAt >= now - duration. Forms: 7d, 24h, 30m (days / hours / minutes). Anything else exits 1 with an error. Applied before session aliases; grouped reports merge and select sources before filtering.

--cwd <path> (matching)

Comma-separated or repeated. Each value matches one of two ways:

  • Absolute (/…) or ~-rooted path → path-bound prefix. ~ expands to your home dir first. The match is subtree-aware: /work/proj matches /work/proj and /work/proj/sub, but not the sibling /work/proj-other.
  • Anything else → case-insensitive substring against the full cwd. A leading ./ is stripped, so ./foo behaves like foo.

Without --by, giving 2+ cwd values switches the output to the multi-cwd comparison object.

--session <id> (matching)

Comma-separated or repeated. Resolved after time, agentId, branch and cwd filters, so the aliases operate on the already-filtered scope. Each value is one of:

  • Alias — latest (newest by receivedAt in scope) or costliest (highest summed cost in scope).
  • UUID prefix — e.sessionId.startsWith(id).
  • Title substring — case-insensitive match against the session title.

A value that isn't latest/costliest is tried as both a UUID prefix and a title substring (either match keeps the session).

--tools

Lifts the tools.top cap from 7 to all tools (and shows all in human output).

--open

After printing, opens the dashboard to the resolved session — only valid when exactly one session matched. With 2+ matches it prints a stderr note and skips opening; with 0 matched sessions it silently does nothing. Either way it never changes the JSON. In multi-cwd comparison mode (--cwd a,b) --open is ignored entirely.


See also

  • docs/data-model.md — the per-entry summary fields (cost, usage, toolCalls, skillCalls, sysHash/toolsHash/coreHash, isSubagent, cwd, …) that usage aggregates from index.ndjson.
  • server/usage.js — the implementation.
  • README → Usage Analytics CLI — task-oriented command examples.

Changelog

  • 2026-11-01 — Added agentId/branch filters, absolute windows and aliases, and opt-in grouped text/JSON output. Default JSON shapes remain unchanged. Grouped reports share merge-before-filter and one-source selection with usage queries and exclude marker rows. Non-grouped reports retain legacy behavior.

  • 2026-10-17 — Added topSessions[].branch (string | null): the git branch of the session's latest-received turn that recorded one. Additive; the multi-cwd and error shapes are unchanged.

  • 2026-09-04 — Added meta.retentionDays, meta.retentionCutoff, and the conditional meta.retentionWarning. The human time-range line now gives the same lower-bound warning when the requested range reaches before retention. A non-numeric LOG_RETENTION_DAYS is represented as retentionDays: null. Breaking: multi-cwd comparison output changed from a bare array to { projects, meta } so the same disclosure reaches that surface — its totals sit behind the same retention window and were previously presented as exact. --last is now forwarded into the comparison's retention verdict, which previously judged every window as all-time. A LOG_RETENTION_DAYS beyond the supported window is now treated as retention off; previously such a value fed an out-of-range date into the cutoff and pruned every log file. The cutoff is also now computed in the Taipei calendar rather than the host's, so it no longer depends on where the process runs (unchanged on a Taipei or UTC host; a DST-observing host previously drifted by a day around its transitions).

  • 2026-06-21 (Claude, Opus 4.8) — Initial schema reference for ccxray usage --json as shipped in PR #94. Documents the single-scope object, multi-cwd comparison shape, and error object, plus filter semantics and per-field precision. Backed by the usage --json shape contract test that locks the key set and field types of every section.