WIP feat(ccusage): agent command — hierarchical team grouping, resumable session IDs, AI titles#895
WIP feat(ccusage): agent command — hierarchical team grouping, resumable session IDs, AI titles#895thomasttvo wants to merge 27 commits intoryoppippi:mainfrom
Conversation
- mtime pre-filter: skip JSONL files older than --since cutoff (3816 → 40 files for today-only) - lead disambiguation: prefix lead entries with project directory name - shortProjectName: skip known container dirs (IdeaProjects, Projects, Desktop, etc.) - agent column: increase min width to 28 to fit team/agent-name format Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n titles - Insert newline after '/' in agent display names so team/member names wrap at a semantic boundary instead of truncating with '…' - Change session title priority: compaction summary > legacy summary > slug > first user message (was: slug first) - Raise JSONL scan cap from 50 to 500 lines for compaction summaries - Bump title cache version to v3 to force re-derivation - Increase title char limit from 50 to 60 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix compaction title extraction that was looking for wrong data format: - Read `isCompactSummary: true` user entries instead of assistant/acompact - Parse plain text "Primary Request and Intent" section instead of XML tags - Two-phase scan: quick first 50 lines for metadata, then full-file scan with cheap string pre-filter for compaction entries deep in the file - Truncate titles at word boundaries instead of mid-word - Bump cache to v6 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ume IDs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…maries Sessions without compaction data now get meaningful AI-generated titles instead of raw first-user-message text or meaningless 3-word slugs. Uses Claude Haiku via direct API call, authenticated through Claude Code's OAuth credentials (Keychain → credentials file → ANTHROPIC_API_KEY fallback). All uncached sessions are batched into a single API call for speed. Title priority: cached → compaction summary → AI-generated → slug. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace meaningless 3-word slugs (e.g. "bright-wiggling-sky") with truncated session IDs (first 8 chars) as the last-resort fallback when no compaction title, AI title, or user messages are available. - Remove all slug references from resolveSessionTitle() and resolveLeadDisplayNames() - Bump cache version v9 → v10 to invalidate stale slug-based caches - Clean up doc comments and type signatures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…utput Read team configs from ~/.claude/teams/ to map team members to their lead sessions. Display members indented with tree prefix (└─) under their lead's row, sorted by total group cost. - Load teamName → leadSessionId mapping from team config files - Group team members under resolved lead session titles - Show orphan lead headers (lead not in current data range) using cached session titles - Ungrouped members (no team config) keep existing team/member format Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
User messages in JSONL can have content as either a string or an array of content blocks (multimodal format). The title generation filter only handled string content, silently dropping all array-format messages. Also filters out isMeta entries which are system-injected skill content, not real user messages. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a percentage column showing each row's cost as a share of total. Groups team members without a config under their teamName header instead of showing flat team/member rows. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Teams without ~/.claude/teams/ configs (e.g. ephemeral teams spawned
via TeamCreate) were shown as standalone root groups instead of nested
under their parent lead session.
Three-phase discovery chain in discoverOrphanTeams():
- Phase 0: Read from ~/.claude/team-lead-cache/ (populated by
PostToolUse hook at TeamCreate time, survives JSONL compaction)
- Phase 1: Scan {leadSessionId}/subagents/ directories for team member
JSONL files referencing orphan team names
- Phase 2: Scan lead JSONL files for TeamCreate tool_use entries that
reference orphan teams (pre-compaction fallback)
Includes hooks/cache-team-lead.sh PostToolUse hook that captures
team_name → session_id at TeamCreate time. Symlink to
~/.claude/hooks/ for activation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…encing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rows `claude --resume` requires the full UUID — short prefixes open the interactive search UI instead of resuming directly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Raised responsive minimum for column 0 from 28 to 38 in ResponsiveTable and set colWidthOverrides to 46 so UUIDs are never truncated — required for `claude --resume`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a new Changes
Sequence DiagramsequenceDiagram
participant User
participant AgentCmd as Agent Command
participant DataLoader as Data Loader
participant JSONL as JSONL Files
participant Cache as Title Cache
participant AI as Claude AI
participant Output as Formatter
User->>AgentCmd: invoke `agent` with filters/options
AgentCmd->>AgentCmd: merge config & args
AgentCmd->>DataLoader: loadAgentUsageData(filters)
DataLoader->>JSONL: scan Claude paths / read JSONL
DataLoader-->>AgentCmd: return AgentUsage[]
AgentCmd->>Cache: load lead/session title caches
alt missing titles
AgentCmd->>JSONL: parse compaction/summary for titles
JSONL-->>AgentCmd: extracted titles
AgentCmd->>Cache: store extracted titles
alt still missing
AgentCmd->>AI: generateAITitlesBatch(sessions)
AI-->>AgentCmd: AI titles
AgentCmd->>Cache: persist AI-derived titles
end
end
AgentCmd->>AgentCmd: group by team/lead and compute totals
alt json/jq requested
AgentCmd->>Output: emit JSON (optionally filtered by jq)
else
AgentCmd->>Output: render responsive table (colWidthOverrides applied)
end
Output-->>User: display report
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/ccusage/CLAUDE.md`:
- Around line 68-69: Update the note to reflect that the UI now shows full
36-char session UUIDs on lead rows rather than `lead-xxxx`; explicitly state
that lead identifiers should be full UUIDs (36 chars) so designers/testers avoid
truncation and layout changes that would break direct `--resume` usage, and also
clarify that the agent column must support realistic `team/agent-name` values
(e.g., `ccusage-fork/cli-dev`) when rendered alongside full UUIDs so
width/layout tests use full UUID + long agent names together.
In `@apps/ccusage/config-schema.json`:
- Around line 611-615: The "days" property in the JSON schema currently allows
negative and fractional values; update the "days" definition in
config-schema.json to restrict it to positive integers by changing the type to
"integer" and adding a minimum constraint (e.g., "minimum": 1) so only whole
numbers >=1 are valid while preserving the existing
description/markdownDescription.
In `@apps/ccusage/src/agent-id.ts`:
- Around line 82-89: shortProjectName currently loses hyphenated basenames by
splitting on every '-'; change it to only strip leading container-dir tokens and
preserve the remainder joined back with '-' (or use original path metadata if
available): in shortProjectName, iterate from the start removing tokens that are
in CONTAINER_DIRS, then join the remaining tokens with '-' and return that
(fallback to last token or encoded if nothing left); this keeps names like
"ccusage-fork" intact while still removing container prefixes.
In `@apps/ccusage/src/commands/agent.ts`:
- Around line 1299-1301: The percent calculation uses a conditional that still
renders '<0.1' for actual zero values and the totals row always prints '100'
when totals.totalCost is zero; update the pct/pctStr logic to explicitly handle
totals.totalCost === 0 (and likewise in the totals row computation) so that when
totals.totalCost is zero you set the percent string to '0' (or '0.0' to match
formatting) instead of '<0.1', and ensure the totals percent displayed (the code
building the totals row around line 1334) uses the same zero-total guard to
display '0' rather than '100'. Use the existing variables pct, pctStr,
totals.totalCost and data.totalCost to implement this single guarded branch for
both per-row and totals calculations.
- Around line 283-291: The outbound fetch calls (e.g., the POST to
OAUTH_REFRESH_URL that uses refreshToken/OAUTH_CLIENT_ID and the other fetch in
the same file around lines 337-348) lack timeout/abort handling and can hang;
wrap each fetch call with an AbortController, start a timer (e.g., configurable
constant like REQUEST_TIMEOUT_MS) that calls controller.abort() after the
timeout, pass controller.signal to fetch, and clear the timer after the fetch
completes; also catch and handle the abort error separately so the CLI returns a
meaningful error instead of hanging.
- Around line 887-890: The code writes cache files using raw teamName (in the
loop over newlyDiscovered) which allows path traversal; sanitize the filename
before using it with writeFile by encoding or safe-normalizing teamName (e.g.,
use a slug/encode function or replace path separators) and join with cacheDir
when calling writeFile (references: cacheDir, newlyDiscovered loop, writeFile).
Additionally, apply the inverse decode/decoding logic when reading cache
filenames so lookups map back to original team names (ensure whatever
sanitize/encode function used here is reversible or store a mapping file).
- Around line 1113-1140: The code still reads ctx.values directly after calling
mergeConfigWithArgs; update the since/until/days/all logic and all subsequent
option reads to use the merged "values" object returned by mergeConfigWithArgs
instead of ctx.values so merged config overrides are respected. Specifically,
replace references to ctx.values.since, ctx.values.until, ctx.values.days,
ctx.values.all and the parameters passed into loadAgentUsageData (mode, offline,
timezone, locale, teamFilter, sessionFilter) with values.since, values.until,
values.days, values.all and values.mode, values.offline, values.timezone,
values.locale, values.team, values.session; also apply the same switch from
ctx.values to values for the later checks around instances, compact and
breakdown to ensure consistent behavior.
In `@apps/ccusage/src/data-loader.ts`:
- Around line 1530-1549: The current mtime prefilter (using options.since ->
cutoff and stat on projectFilteredWithBase to produce dateFilteredWithBase) can
incorrectly drop valid records; change the logic so we do NOT hard-filter files
by filesystem mtime. Instead, keep all entries from projectFilteredWithBase
(i.e., set dateFilteredWithBase = projectFilteredWithBase) and perform the since
cutoff using each record's internal timestamp during JSONL parsing; if you want
to keep the stat check for perf, only use it as a heuristic (e.g., mark files as
"likely old" but still include them) rather than returning null in the
Promise.all branch. Ensure references: options.since, cutoff, stat,
projectFilteredWithBase, and dateFilteredWithBase are updated accordingly.
In `@hooks/cache-team-lead.sh`:
- Around line 21-23: Replace the raw TEAM_NAME usage as a filename with a safe,
deterministic encoded key (e.g., hex or base64url of sha256(TEAM_NAME) or a
strict whitelist/slugify) when creating and writing to CACHE_DIR so values like
"../settings.local.json" cannot escape; update the write in the hook (where
CACHE_DIR, TEAM_NAME, SESSION_ID are used) to write to
"$CACHE_DIR/<encoded_key>" and mirror the exact same encoding/decoding logic in
the agent read path (the code in apps/ccusage/src/commands/agent.ts) so reads
and writes remain compatible and safe.
In `@packages/terminal/src/table.ts`:
- Around line 231-235: Validate colWidthOverrides before using them: in the
method that computes column width (the block referencing
this.colWidthOverrides[index] and the other occurrence around the same logic),
check that the override is a finite number and at least the minimum allowed
width (e.g., Number.isFinite(override) && override >= 10) before applying
Math.min/Math.max; if invalid (NaN, non-finite, <=0, or less than min), ignore
the override and fall back to the existing width + padding logic so columns
cannot collapse. Ensure you apply the same validation in both places where
this.colWidthOverrides[index] is consumed.
In `@scripts/find-agent-id.mjs`:
- Around line 49-50: getTeamLeadFiles (and the other path-building helpers that
assemble team/config or project scan paths) currently hard-code homedir-based
roots (~/.claude and ~/.config/claude); instead read the configured Claude roots
from the CLAUDE_CONFIG_DIR environment variable (splitting on path.delimiter)
and fall back to the two default roots, then build team and project config paths
by joining each root with the relative subpaths (e.g., 'teams', teamName,
'config.json'). Update getTeamLeadFiles to iterate those configured roots when
locating config.json and apply the same pattern to the other helpers that
construct team and project scan paths so --team and --session respect
CLAUDE_CONFIG_DIR.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: aef64110-5a69-48f5-bc38-93ead378e98f
📒 Files selected for processing (11)
apps/ccusage/CLAUDE.mdapps/ccusage/config-schema.jsonapps/ccusage/package.jsonapps/ccusage/src/agent-id.tsapps/ccusage/src/commands/agent.tsapps/ccusage/src/commands/index.tsapps/ccusage/src/data-loader.tseslint.config.jshooks/cache-team-lead.shpackages/terminal/src/table.tsscripts/find-agent-id.mjs
…es only AI-generated titles were producing bogus results like Hello World Program for sessions without compaction summaries. Now sessions without a compaction or legacy summary show UUID-only with timestamp. Removed: generateAITitlesBatch, getClaudeAuthHeaders, OAuth token refresh, user message collection for AI input, nano-spawn dependency. Bumped cache version to v11 to invalidate stale AI-generated caches. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… summaries only" This reverts commit 31228cd.
Filter out short/trivial user messages (< 10 chars or single words like 'hello', 'ok') before feeding to AI title generation. Also improve the prompt to focus on the user's task/goal. Bump cache version to v11 to invalidate stale titles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sessions with no substantive messages no longer cache a truncated ID as a 'title'. Instead they show just the short ID + timestamp without a trailing separator. Only cache real AI-generated titles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/ccusage/src/commands/agent.ts (1)
646-651: O(n²) complexity fromindexOfin loop.
needsAI.indexOf(item)inside the loop creates O(n²) complexity. While unlikely to matter for typical session counts, this is easily avoidable.♻️ Proposed refactor
-for (const item of needsAI) { +for (let i = 0; i < needsAI.length; i++) { + const item = needsAI[i]!; const agent = agents[item.agentIdx]!; - const aiTitle = aiTitles.get(needsAI.indexOf(item) + 1); + const aiTitle = aiTitles.get(i + 1); const finalTitle = aiTitle ?? agent.sessionId?.slice(0, 8) ?? 'Untitled';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ccusage/src/commands/agent.ts` around lines 646 - 651, The loop over needsAI is using needsAI.indexOf(item) which causes O(n²); change the iteration to use an index-aware loop (e.g., for (let i = 0; i < needsAI.length; i++) or needsAI.forEach((item, i) => ...)) so you can compute aiTitle = aiTitles.get(i + 1) directly and then set agent.agentId (the rest of the logic using agents[item.agentIdx], formatStartTime(item.startTime, timezone), and agent.sessionId stays the same).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/ccusage/src/commands/agent.ts`:
- Around line 1056-1071: loadLeadTitleCache currently loads titles without
validating the cache version, which can return stale data; update
loadLeadTitleCache to read the first line of each cached file and compare it to
the existing CACHE_VERSION constant (same validation used in
resolveSessionTitle) and only set cache.set(sessionId, title) when the version
matches; if the version mismatches or the first line is missing, skip that file
(treat as no cached title) and keep the existing try/catch behavior for IO
errors.
---
Nitpick comments:
In `@apps/ccusage/src/commands/agent.ts`:
- Around line 646-651: The loop over needsAI is using needsAI.indexOf(item)
which causes O(n²); change the iteration to use an index-aware loop (e.g., for
(let i = 0; i < needsAI.length; i++) or needsAI.forEach((item, i) => ...)) so
you can compute aiTitle = aiTitles.get(i + 1) directly and then set
agent.agentId (the rest of the logic using agents[item.agentIdx],
formatStartTime(item.startTime, timezone), and agent.sessionId stays the same).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5d23196b-3686-4312-b7a9-a1dbac8cd3af
📒 Files selected for processing (1)
apps/ccusage/src/commands/agent.ts
truncateAtWord now appends … when text is truncated beyond maxLen. Cache version bumped to v12 to regenerate titles with ellipsis. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
bugbot review |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (5)
apps/ccusage/src/commands/agent.ts (5)
1061-1069:⚠️ Potential issue | 🟡 MinorValidate the cache version before loading orphan lead titles.
resolveSessionTitle()writes a versionedv11payload, but this reader accepts any second line. Old invalidated cache entries can still resurface here and show stale orphan-lead headers.Suggested fix
async function loadLeadTitleCache(teamLeadMap: Map<string, string>): Promise<Map<string, string>> { const cache = new Map<string, string>(); const cacheDir = path.join(os.homedir(), '.claude', 'session-titles'); + const CACHE_VERSION = 'v11'; for (const sessionId of new Set(teamLeadMap.values())) { try { const raw = await readFile(path.join(cacheDir, sessionId), 'utf-8'); const lines = raw.trim().split('\n'); - if (lines.length >= 2 && lines[1] != null && lines[1] !== '') { + if ( + lines[0] === CACHE_VERSION && + lines.length >= 3 && + lines[1] != null && + lines[1] !== '' + ) { cache.set(sessionId, lines[1]); } } catch {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ccusage/src/commands/agent.ts` around lines 1061 - 1069, loadLeadTitleCache is reading the second line of cached files without validating the file version, allowing old/invalid entries to be used; change loadLeadTitleCache to check the file's version header (e.g., verify lines[0] === 'v11' or the expected version emitted by resolveSessionTitle) before using lines[1] as the title, and skip/cache only when the version matches and lines[1] is non-empty (use the existing cacheDir and sessionId variables and preserve the current try/catch behavior).
283-291:⚠️ Potential issue | 🟠 MajorAdd abort timeouts to both outbound
fetchcalls.Both requests can block
ccusage agentindefinitely on a slow or broken connection. Title refresh/generation should fail fast instead of hanging the whole command.Suggested fix
+const REQUEST_TIMEOUT_MS = 15_000; + @@ const response = await fetch(OAUTH_REFRESH_URL, { method: 'POST', + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ const response = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), headers: { 'Content-Type': 'application/json',Also applies to: 337-348
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ccusage/src/commands/agent.ts` around lines 283 - 291, The two outbound fetches (the refresh call to OAUTH_REFRESH_URL and the other fetch in the same file around the token/generation block) must use an AbortController with a short timeout so the agent doesn't hang indefinitely; modify the fetch invocations (the one that builds the body with grant_type:'refresh_token' and the other token/generation fetch) to create an AbortController, pass controller.signal into the fetch options, set a setTimeout to call controller.abort() after a reasonable timeout (e.g., a few seconds), and clear the timeout after the fetch completes/fails to avoid leaks; ensure error handling distinguishes AbortError for a clean fast-fail path.
764-773:⚠️ Potential issue | 🔴 CriticalEncode
teamNamebefore using it as a cache filename.
teamNamecomes from session/tool data and is joined directly into~/.claude/team-lead-cache. A crafted value containing separators or..can escape the cache directory and overwrite arbitrary files writable by the user.Suggested fix
+function toCacheFileName(teamName: string): string { + return encodeURIComponent(teamName); +} + @@ const cacheFiles = await readdir(cacheDir); for (const file of cacheFiles) { - if (!orphanTeams.has(file)) { + const teamName = decodeURIComponent(file); + if (!orphanTeams.has(teamName)) { continue; } try { const leadSessionId = (await readFile(path.join(cacheDir, file), 'utf-8')).trim(); if (leadSessionId !== '') { - teamLeadMap.set(file, leadSessionId); - orphanTeams.delete(file); + teamLeadMap.set(teamName, leadSessionId); + orphanTeams.delete(teamName); } } catch { @@ await mkdir(cacheDir, { recursive: true }); for (const [teamName, leadSessionId] of newlyDiscovered) { - await writeFile(path.join(cacheDir, teamName), leadSessionId, 'utf-8'); + await writeFile( + path.join(cacheDir, toCacheFileName(teamName)), + leadSessionId, + 'utf-8', + ); }Also applies to: 897-899
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ccusage/src/commands/agent.ts` around lines 764 - 773, The code is vulnerable because raw team names are used as cache filenames under cacheDir; ensure you encode/sanitize teamName before using it in path.join so it cannot traverse directories or include path separators. Update all places that build paths for the team-lead cache (where cacheDir, path.join(cacheDir, file) and wherever you write the cache, e.g., the writeFile usage around the later block at lines referenced 897-899) to transform the team name into a safe filename (for example using a deterministic encoding like base64 or a strict whitelist/sanitizer) when creating keys for orphanTeams, teamLeadMap, and when reading/writing files so both readFile and writeFile use the encoded name.
1308-1309:⚠️ Potential issue | 🟡 MinorGuard the
%column when total cost is zero.With an all-zero dataset, rows render
<0.1and the totals row renders100, which is misleading.Suggested fix
- const pct = totals.totalCost > 0 ? (data.totalCost / totals.totalCost) * 100 : 0; - const pctStr = pct < 0.1 ? '<0.1' : pct.toFixed(1); + const hasCostBase = totals.totalCost > 0; + const pct = hasCostBase ? (data.totalCost / totals.totalCost) * 100 : 0; + const pctStr = !hasCostBase ? '0.0' : pct < 0.1 ? '<0.1' : pct.toFixed(1); @@ - pc.yellow('100'), + pc.yellow(totals.totalCost > 0 ? '100' : '0.0'),Also applies to: 1343-1343
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ccusage/src/commands/agent.ts` around lines 1308 - 1309, The percent column displays misleading values when totals.totalCost is zero; update the percent calculation and formatting around pct and pctStr so that when totals.totalCost === 0 you set pct = 0 and render a sensible string (e.g., "0" or "0.0" or "—") instead of applying the "<0.1" rule or showing "100" in the totals row; ensure the same guard is applied where the totals row is rendered (references: pct, pctStr, totals.totalCost, data.totalCost, and the totals row rendering code).
1113-1147:⚠️ Potential issue | 🟠 MajorUse
mergedOptionsconsistently aftermergeConfigWithArgs().After merging config and CLI args, the command falls back to
ctx.valuesfor date filters, loader inputs,instances,compact,breakdown, andjq. Config-backedagent.*values are silently ignored.Suggested fix
const config = loadConfig(ctx.values.config, ctx.values.debug); const mergedOptions: typeof ctx.values = mergeConfigWithArgs(ctx, config, ctx.values.debug); + const values = mergedOptions; @@ - let since = ctx.values.since; - const until = ctx.values.until; + let since = values.since; + const until = values.until; @@ - if (ctx.values.days != null) { + if (values.days != null) { const now = new Date(); const daysAgo = new Date(now); - daysAgo.setDate(daysAgo.getDate() - (ctx.values.days - 1)); - since = formatLocalDateYYYYMMDD(daysAgo, ctx.values.timezone); - } else if (!ctx.values.all) { + daysAgo.setDate(daysAgo.getDate() - (values.days - 1)); + since = formatLocalDateYYYYMMDD(daysAgo, values.timezone); + } else if (!values.all) { // Default: today only - since = formatLocalDateYYYYMMDD(new Date(), ctx.values.timezone); + since = formatLocalDateYYYYMMDD(new Date(), values.timezone); } } @@ - mode: ctx.values.mode, - offline: ctx.values.offline, - timezone: ctx.values.timezone, - locale: ctx.values.locale, - teamFilter: ctx.values.team, - sessionFilter: ctx.values.session, + mode: values.mode, + offline: values.offline, + timezone: values.timezone, + locale: values.locale, + teamFilter: values.team, + sessionFilter: values.session, @@ - const displayData = ctx.values.instances ? agentData : aggregateByRole(agentData); + const displayData = values.instances ? agentData : aggregateByRole(agentData);Also applies to: 1163-1167, 1175-1175, 1227-1228, 1280-1280, 1322-1322
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ccusage/src/commands/agent.ts` around lines 1113 - 1147, The code merges CLI/config into mergedOptions via mergeConfigWithArgs but continues to read from ctx.values; update the date resolution and all subsequent usages to read from mergedOptions instead of ctx.values so config-backed agent.* settings are honored: use mergedOptions.since / mergedOptions.until / mergedOptions.days / mergedOptions.all when computing since (and pass mergedOptions.timezone/locale/mode/offline/team/session), and replace ctx.values.instances, mergedOptions.compact, mergedOptions.breakdown, and mergedOptions.jq (and any other ctx.values references noted around loadAgentUsageData and subsequent blocks) so loadAgentUsageData and later logic use mergedOptions consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/ccusage/src/commands/agent.ts`:
- Around line 1165-1167: The code calls resolveLeadDisplayNames(displayData,
ctx.values.timezone) even when the CLI is in offline mode; change the check so
title resolution is skipped when the effective offline flag is set (e.g.
ctx.values.offline or the repo's effectiveOffline setting) — i.e., only call
resolveLeadDisplayNames if !ctx.values.instances && !ctx.values.offline (or the
equivalent effectiveOffline property) so no Anthropic/claude or network work
runs in offline mode; also ensure resolveLeadDisplayNames itself early-returns
if an offline flag is passed through.
- Around line 667-669: The current no-title fallback truncates the session ID to
8 chars when setting agent.agentId, which prevents direct resume by full UUID;
change the assignment to use the full agent.sessionId (not just slice(0,8)) when
available, e.g. build agent.agentId from agent.sessionId + " · " +
formatStartTime(item.startTime, timezone) while still defaulting to 'Untitled'
only if sessionId is missing; update the code around agent.sessionId,
agent.agentId, formatStartTime, item.startTime and timezone accordingly.
- Around line 620-621: The call to resolveSessionTitle(agent.sessionId,
agent.project, claudePaths) can throw on IO errors and currently aborts the
whole report; wrap that await in a try/catch around the call in the agent report
loop so any thrown errors are caught, log or debug the error (optional) and set
titleInfo to null on error so the code falls back to the existing slug/session
display instead of exiting; specifically modify the block using
resolveSessionTitle to handle exceptions and continue processing the remaining
sessions.
---
Duplicate comments:
In `@apps/ccusage/src/commands/agent.ts`:
- Around line 1061-1069: loadLeadTitleCache is reading the second line of cached
files without validating the file version, allowing old/invalid entries to be
used; change loadLeadTitleCache to check the file's version header (e.g., verify
lines[0] === 'v11' or the expected version emitted by resolveSessionTitle)
before using lines[1] as the title, and skip/cache only when the version matches
and lines[1] is non-empty (use the existing cacheDir and sessionId variables and
preserve the current try/catch behavior).
- Around line 283-291: The two outbound fetches (the refresh call to
OAUTH_REFRESH_URL and the other fetch in the same file around the
token/generation block) must use an AbortController with a short timeout so the
agent doesn't hang indefinitely; modify the fetch invocations (the one that
builds the body with grant_type:'refresh_token' and the other token/generation
fetch) to create an AbortController, pass controller.signal into the fetch
options, set a setTimeout to call controller.abort() after a reasonable timeout
(e.g., a few seconds), and clear the timeout after the fetch completes/fails to
avoid leaks; ensure error handling distinguishes AbortError for a clean
fast-fail path.
- Around line 764-773: The code is vulnerable because raw team names are used as
cache filenames under cacheDir; ensure you encode/sanitize teamName before using
it in path.join so it cannot traverse directories or include path separators.
Update all places that build paths for the team-lead cache (where cacheDir,
path.join(cacheDir, file) and wherever you write the cache, e.g., the writeFile
usage around the later block at lines referenced 897-899) to transform the team
name into a safe filename (for example using a deterministic encoding like
base64 or a strict whitelist/sanitizer) when creating keys for orphanTeams,
teamLeadMap, and when reading/writing files so both readFile and writeFile use
the encoded name.
- Around line 1308-1309: The percent column displays misleading values when
totals.totalCost is zero; update the percent calculation and formatting around
pct and pctStr so that when totals.totalCost === 0 you set pct = 0 and render a
sensible string (e.g., "0" or "0.0" or "—") instead of applying the "<0.1" rule
or showing "100" in the totals row; ensure the same guard is applied where the
totals row is rendered (references: pct, pctStr, totals.totalCost,
data.totalCost, and the totals row rendering code).
- Around line 1113-1147: The code merges CLI/config into mergedOptions via
mergeConfigWithArgs but continues to read from ctx.values; update the date
resolution and all subsequent usages to read from mergedOptions instead of
ctx.values so config-backed agent.* settings are honored: use
mergedOptions.since / mergedOptions.until / mergedOptions.days /
mergedOptions.all when computing since (and pass
mergedOptions.timezone/locale/mode/offline/team/session), and replace
ctx.values.instances, mergedOptions.compact, mergedOptions.breakdown, and
mergedOptions.jq (and any other ctx.values references noted around
loadAgentUsageData and subsequent blocks) so loadAgentUsageData and later logic
use mergedOptions consistently.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5940e311-b06f-49ad-964a-541c5e01b4da
📒 Files selected for processing (1)
apps/ccusage/src/commands/agent.ts
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (7)
apps/ccusage/src/commands/agent.ts (7)
620-621:⚠️ Potential issue | 🟠 MajorWrap
resolveSessionTitlein try-catch to prevent one bad session from aborting the report.If
resolveSessionTitlethrows due to an unreadable JSONL file, the entire command exits. This is a best-effort title decoration — failures should fall back gracefully.💡 Suggested fix
- const titleInfo = await resolveSessionTitle(agent.sessionId, agent.project, claudePaths); + let titleInfo: Awaited<ReturnType<typeof resolveSessionTitle>> = null; + try { + titleInfo = await resolveSessionTitle(agent.sessionId, agent.project, claudePaths); + } catch { + // File became unreadable — fall through to fallback + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ccusage/src/commands/agent.ts` around lines 620 - 621, Wrap the call to resolveSessionTitle(agent.sessionId, agent.project, claudePaths) in a try-catch so a thrown error from unreadable JSONL doesn't abort the whole command: call resolveSessionTitle inside try, assign its result to titleInfo on success, and on catch set titleInfo to a safe fallback (null/undefined or empty object/string), and log the error at debug/verbose level mentioning agent.sessionId and agent.project so the report generation continues for other sessions; update any downstream usage of titleInfo to handle the fallback gracefully.
1113-1150:⚠️ Potential issue | 🟠 MajorMerged config not consistently used after
mergeConfigWithArgs.
mergedOptionsis created at line 1114 but the code continues to read fromctx.valuesfor core options (since,until,days,all,mode,offline,timezone,locale,team,session). This bypasses any config file overrides.💡 Suggested fix
// Resolve date range: explicit --since/--until > --days > --all > default (today) - let since = ctx.values.since; - const until = ctx.values.until; + let since = mergedOptions.since; + const until = mergedOptions.until; if (since == null && until == null) { - if (ctx.values.days != null) { + if (mergedOptions.days != null) { const now = new Date(); const daysAgo = new Date(now); - daysAgo.setDate(daysAgo.getDate() - (ctx.values.days - 1)); - since = formatLocalDateYYYYMMDD(daysAgo, ctx.values.timezone); - } else if (!ctx.values.all) { + daysAgo.setDate(daysAgo.getDate() - (mergedOptions.days - 1)); + since = formatLocalDateYYYYMMDD(daysAgo, mergedOptions.timezone); + } else if (!mergedOptions.all) { // Default: today only - since = formatLocalDateYYYYMMDD(new Date(), ctx.values.timezone); + since = formatLocalDateYYYYMMDD(new Date(), mergedOptions.timezone); } } // ... and similarly for loadAgentUsageData call: - mode: ctx.values.mode, - offline: ctx.values.offline, - timezone: ctx.values.timezone, - locale: ctx.values.locale, - teamFilter: ctx.values.team, - sessionFilter: ctx.values.session, + mode: mergedOptions.mode, + offline: mergedOptions.offline, + timezone: mergedOptions.timezone, + locale: mergedOptions.locale, + teamFilter: mergedOptions.team, + sessionFilter: mergedOptions.session,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ccusage/src/commands/agent.ts` around lines 1113 - 1150, The code creates mergedOptions via mergeConfigWithArgs but continues to read directly from ctx.values, so config overrides are ignored; replace all uses of ctx.values (specifically json, jq, since, until, days, all, mode, offline, timezone, locale, team, session and any other flags read in this block) with mergedOptions equivalents (e.g., use mergedOptions.json / mergedOptions.jq for useJson, compute since/until/days/all from mergedOptions, and pass mergedOptions.mode/offline/timezone/locale/team/session into loadAgentUsageData and onProgress) so the merged configuration is consistently used throughout this function.
894-904:⚠️ Potential issue | 🔴 CriticalUnsanitized
teamNameused as filesystem path — path traversal risk.Line 899 writes cache files using raw
teamName. IfteamNamecontains path separators (e.g.,../) or other special characters, files could be written outside the intended cache directory.💡 Suggested fix: encode team names for safe filesystem use
+function toSafeFileName(name: string): string { + return encodeURIComponent(name); +} + // Persist newly discovered mappings so they survive JSONL compaction if (newlyDiscovered.size > 0) { try { await mkdir(cacheDir, { recursive: true }); for (const [teamName, leadSessionId] of newlyDiscovered) { - await writeFile(path.join(cacheDir, teamName), leadSessionId, 'utf-8'); + await writeFile(path.join(cacheDir, toSafeFileName(teamName)), leadSessionId, 'utf-8'); }Also update the cache reading logic (lines 766-774) to decode filenames:
for (const file of cacheFiles) { - if (!orphanTeams.has(file)) { + const teamName = decodeURIComponent(file); + if (!orphanTeams.has(teamName)) { continue; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ccusage/src/commands/agent.ts` around lines 894 - 904, The cache write uses raw teamName keys from newlyDiscovered to create filenames under cacheDir, which allows path traversal; update the write path to use a safe filename encoding (e.g., base64 or encodeURIComponent) instead of raw teamName before calling path.join and writeFile, and ensure mkdir(cacheDir, { recursive: true }) remains; then mirror the change in the cache reading logic that iterates filenames (the reader that maps filenames back to team names) to decode the stored filenames back to teamName so reads/writes are consistent (refer to newlyDiscovered, cacheDir, mkdir, writeFile to locate the write site and the cache-reading routine to update decoding).
1165-1169:⚠️ Potential issue | 🟠 MajorHonor offline mode before resolving AI titles.
resolveLeadDisplayNamescan invoke the Anthropic API or spawn the Claude CLI, but this call happens even when--offlineis set. Offline mode should skip network-dependent title generation.💡 Suggested fix
// Resolve lead display names (session titles) for aggregated view - if (!ctx.values.instances) { + if (!ctx.values.instances && !mergedOptions.offline) { await resolveLeadDisplayNames(displayData, ctx.values.timezone); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ccusage/src/commands/agent.ts` around lines 1165 - 1169, The code calls resolveLeadDisplayNames(...) even in offline mode; update the conditional to honor the offline flag by skipping title generation when ctx.values.offline is true—i.e., only call resolveLeadDisplayNames(displayData, ctx.values.timezone) if !ctx.values.instances AND !ctx.values.offline (or return early when ctx.values.offline is set) so that no Anthropic/Claude CLI network or subprocess work runs while offline.
1062-1077:⚠️ Potential issue | 🟡 MinorCache version not validated — may load stale titles.
loadLeadTitleCachereads cached titles without checking thev12version prefix, unlikeresolveSessionTitlewhich validates it. This could display stale titles from older cache formats.💡 Suggested fix
async function loadLeadTitleCache(teamLeadMap: Map<string, string>): Promise<Map<string, string>> { const cache = new Map<string, string>(); const cacheDir = path.join(os.homedir(), '.claude', 'session-titles'); + const CACHE_VERSION = 'v12'; for (const sessionId of new Set(teamLeadMap.values())) { try { const raw = await readFile(path.join(cacheDir, sessionId), 'utf-8'); const lines = raw.trim().split('\n'); - if (lines.length >= 2 && lines[1] != null && lines[1] !== '') { + if ( + lines[0] === CACHE_VERSION && + lines.length >= 3 && + lines[1] != null && + lines[1] !== '' + ) { cache.set(sessionId, lines[1]); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ccusage/src/commands/agent.ts` around lines 1062 - 1077, loadLeadTitleCache currently accepts titles from cache without validating the cache version header, which can yield stale/invalid titles; update loadLeadTitleCache to read the cached file header (e.g., check lines[0] === 'v12' or the same version prefix used by resolveSessionTitle) and only set cache.set(sessionId, lines[1]) when the version matches and lines[1] is present, otherwise treat it as a miss (skip/ignore) so behavior matches resolveSessionTitle; ensure you still handle readFile errors the same way.
667-670:⚠️ Potential issue | 🟠 MajorPreserve full session UUID in the no-title fallback for
claude --resume.When AI title generation returns nothing, only the first 8 characters are shown. This breaks the PR objective of enabling direct resume via
claude --resume <uuid>.💡 Suggested fix
} else { // No AI title (no substantive messages) — show truncated ID + time only const shortId = agent.sessionId?.slice(0, 8) ?? 'Untitled'; - agent.agentId = `${shortId} · ${formatStartTime(item.startTime, timezone)}`; + agent.agentId = agent.sessionId != null + ? `${shortId} · ${formatStartTime(item.startTime, timezone)} · ${agent.sessionId}` + : `Untitled · ${formatStartTime(item.startTime, timezone)}`; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ccusage/src/commands/agent.ts` around lines 667 - 670, The fallback that shows only the first 8 chars of the session ID breaks resume because the full UUID is needed; update the no-title branch so agent.agentId uses the full agent.sessionId (falling back to 'Untitled' if missing) instead of agent.sessionId?.slice(0,8), e.g. set agent.agentId = `${agent.sessionId ?? 'Untitled'} · ${formatStartTime(item.startTime, timezone)}` while keeping formatStartTime and timezone usage unchanged.
1309-1311:⚠️ Potential issue | 🟡 MinorPercent column is misleading when total cost is zero.
Line 1310 shows
<0.1even for actual zero costs, and line 1344 always shows100in totals regardless of whether there's any cost data. This is confusing for zero-cost datasets.💡 Suggested fix
- const pct = totals.totalCost > 0 ? (data.totalCost / totals.totalCost) * 100 : 0; - const pctStr = pct < 0.1 ? '<0.1' : pct.toFixed(1); + const hasCostBase = totals.totalCost > 0; + const pct = hasCostBase ? (data.totalCost / totals.totalCost) * 100 : 0; + const pctStr = !hasCostBase ? '0.0' : pct < 0.1 && pct > 0 ? '<0.1' : pct.toFixed(1); // ... and for the totals row: - pc.yellow('100'), + pc.yellow(totals.totalCost > 0 ? '100' : '0.0'),Also applies to: 1344-1344
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/ccusage/src/commands/agent.ts` around lines 1309 - 1311, The percent calculation and totals display are misleading for zero-cost datasets; update the pct/pctStr logic near the lines that compute "const pct = totals.totalCost > 0 ? (data.totalCost / totals.totalCost) * 100 : 0;" and "const pctStr = pct < 0.1 ? '<0.1' : pct.toFixed(1);" so that when totals.totalCost === 0 you emit "0" (or "0.0" per formatting standard) instead of "<0.1" or other values, and likewise change the totals-row percent generation (the code that builds the totals table row around where totals are pushed, currently showing "100") to display "0" when totals.totalCost === 0; ensure you still apply the "<0.1" rule only when totals.totalCost > 0 and pct > 0 but < 0.1.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/ccusage/src/commands/agent.ts`:
- Around line 250-258: The code currently constructs a hardcoded credsPath
(~/.claude/.credentials.json) when creds is null; replace that logic to use the
project's getClaudePaths() helper to resolve the credentials file location
instead. In the block where creds is null (the creds variable check), call
getClaudePaths(), derive the credentials file path from its returned paths (the
same location other code uses), then await readFile on that resolved path and
JSON.parse into creds; preserve the existing try/catch behavior around
readFile/JSON parsing to handle missing/invalid files.
- Around line 360-368: The spawn fallback for the Claude CLI can hang because it
lacks a timeout; wrap the call in an AbortSignal.timeout and pass the signal
into spawn, e.g., create a signal (AbortSignal.timeout(TIMEOUT_MS) or reuse the
module's existing timeout constant), call spawn('claude', ['-p', prompt,
'--model', 'haiku'], { signal }), and handle the abort path in the catch: if
aborted, ensure any started process is killed/cleaned up and continue without
blocking; update references around spawn, proc, and output in the try/catch to
account for the signal and proper cleanup.
---
Duplicate comments:
In `@apps/ccusage/src/commands/agent.ts`:
- Around line 620-621: Wrap the call to resolveSessionTitle(agent.sessionId,
agent.project, claudePaths) in a try-catch so a thrown error from unreadable
JSONL doesn't abort the whole command: call resolveSessionTitle inside try,
assign its result to titleInfo on success, and on catch set titleInfo to a safe
fallback (null/undefined or empty object/string), and log the error at
debug/verbose level mentioning agent.sessionId and agent.project so the report
generation continues for other sessions; update any downstream usage of
titleInfo to handle the fallback gracefully.
- Around line 1113-1150: The code creates mergedOptions via mergeConfigWithArgs
but continues to read directly from ctx.values, so config overrides are ignored;
replace all uses of ctx.values (specifically json, jq, since, until, days, all,
mode, offline, timezone, locale, team, session and any other flags read in this
block) with mergedOptions equivalents (e.g., use mergedOptions.json /
mergedOptions.jq for useJson, compute since/until/days/all from mergedOptions,
and pass mergedOptions.mode/offline/timezone/locale/team/session into
loadAgentUsageData and onProgress) so the merged configuration is consistently
used throughout this function.
- Around line 894-904: The cache write uses raw teamName keys from
newlyDiscovered to create filenames under cacheDir, which allows path traversal;
update the write path to use a safe filename encoding (e.g., base64 or
encodeURIComponent) instead of raw teamName before calling path.join and
writeFile, and ensure mkdir(cacheDir, { recursive: true }) remains; then mirror
the change in the cache reading logic that iterates filenames (the reader that
maps filenames back to team names) to decode the stored filenames back to
teamName so reads/writes are consistent (refer to newlyDiscovered, cacheDir,
mkdir, writeFile to locate the write site and the cache-reading routine to
update decoding).
- Around line 1165-1169: The code calls resolveLeadDisplayNames(...) even in
offline mode; update the conditional to honor the offline flag by skipping title
generation when ctx.values.offline is true—i.e., only call
resolveLeadDisplayNames(displayData, ctx.values.timezone) if
!ctx.values.instances AND !ctx.values.offline (or return early when
ctx.values.offline is set) so that no Anthropic/Claude CLI network or subprocess
work runs while offline.
- Around line 1062-1077: loadLeadTitleCache currently accepts titles from cache
without validating the cache version header, which can yield stale/invalid
titles; update loadLeadTitleCache to read the cached file header (e.g., check
lines[0] === 'v12' or the same version prefix used by resolveSessionTitle) and
only set cache.set(sessionId, lines[1]) when the version matches and lines[1] is
present, otherwise treat it as a miss (skip/ignore) so behavior matches
resolveSessionTitle; ensure you still handle readFile errors the same way.
- Around line 667-670: The fallback that shows only the first 8 chars of the
session ID breaks resume because the full UUID is needed; update the no-title
branch so agent.agentId uses the full agent.sessionId (falling back to
'Untitled' if missing) instead of agent.sessionId?.slice(0,8), e.g. set
agent.agentId = `${agent.sessionId ?? 'Untitled'} ·
${formatStartTime(item.startTime, timezone)}` while keeping formatStartTime and
timezone usage unchanged.
- Around line 1309-1311: The percent calculation and totals display are
misleading for zero-cost datasets; update the pct/pctStr logic near the lines
that compute "const pct = totals.totalCost > 0 ? (data.totalCost /
totals.totalCost) * 100 : 0;" and "const pctStr = pct < 0.1 ? '<0.1' :
pct.toFixed(1);" so that when totals.totalCost === 0 you emit "0" (or "0.0" per
formatting standard) instead of "<0.1" or other values, and likewise change the
totals-row percent generation (the code that builds the totals table row around
where totals are pushed, currently showing "100") to display "0" when
totals.totalCost === 0; ensure you still apply the "<0.1" rule only when
totals.totalCost > 0 and pct > 0 but < 0.1.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 812db9ca-e9c6-4cd0-acbf-343a72d60af2
📒 Files selected for processing (1)
apps/ccusage/src/commands/agent.ts
…ate headers - Filter [No content provided] and other bracketed placeholders from cached and compaction/summary titles using isValidTitle() - Use joinParts() to prevent trailing · when title parts are empty/missing - Deduplicate lead groups by sessionId to prevent duplicate header rows Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nt, fix team grouping - Display full session UUID on line 2 below title for all titled lead rows - Extract inner text from <teammate-message> XML blocks instead of filtering them - Fix lead entries with teamName but no agentName falling to ungrouped - Skip truncation for multiline cells in ResponsiveTable (wordWrap handles them) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove isSubstantive gate for userMessages collection so even short messages like "config" get sent to AI for title generation - Keep isSubstantive gate only for deterministic userMessageTitle fallback - Change AI prompt from "max 8 words" to "max 40 characters" for predictable column width - Tighten post-filter from 80 to 50 chars as safety net - Bump cache version to v13 to regenerate all titles Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ia AI
- Tighten AI prompt to "max 30 chars, 3-5 words, noun phrase only"
- Remove post-filter length check on AI-generated titles
- Extract <command-args> content from slash command messages
- Strip teammate-message preamble ("You are a ... on team ...")
- Feed compaction summaries to AI title generator instead of using directly
- Remove title truncation from display path
- Bump cache to v15 to force regeneration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CodeRabbit Review — All Threads AddressedCritical#1 — Path traversal in cache writes: Added Major#2 — #3 — #4 — mtime prefilter on JSONL files: This is in #5 — Hardcoded #6 — Unhandled #7 — Minor#8 — #9 — Percent column shows #10 — Credentials path hardcoded: Now tries both #11 — #12 — #13 — HTTP calls have no timeout: Added #14 — Comment references old Dismiss#15 — Full UUID shown: Fixed in earlier commit — full 36-char UUID now displayed on second line of titled rows. |
- Sanitize cache path components against traversal attacks - Join last 2 non-container segments in shortProjectName for disambiguation - Use mergedOptions instead of ctx.values throughout run() - Wrap resolveSessionTitle in try/catch for resilience - Skip AI generation in offline mode with fallback IDs - Add CACHE_VERSION check to loadLeadTitleCache (hoisted to module scope) - Show '-' instead of '<0.1' for zero-cost percent column - Try both ~/.claude and ~/.config/claude for credentials - Clamp days param to positive integers - Add AbortSignal timeouts to HTTP fetch calls - Update comments to clarify lead-xxxx format Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
bugbot review |
- Add explicit constraint against verb/gerund starts in AI prompt - Add 3 BAD → GOOD few-shot examples for clarity - Bump cache version v15 → v16 to regenerate stale titles Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a user message starts with a UUID on its own line (e.g. pasting a session ID), the title generator was using that UUID as input instead of the actual descriptive text on subsequent lines. Now skips lines matching bare UUID/hash patterns and uses the first substantive line instead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Increase user message collection from 3 to 5 for richer AI context - Cap per-session AI input at 1000 chars to avoid token waste - Filter bare UUID/hash lines from fallback hint display - Apply UUID filtering to both online and offline fallback paths Sessions with sparse early messages (like 10817cae which started with a pasted UUID) now get enough context for descriptive titles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Problem
When using Claude Code's team/agent system heavily, there's no way to see session relationships, costs, or easily resume past sessions. You have to manually inspect JSONL files to understand which sessions are related or find a session UUID for
claude --resume.Solution
This PR adds a new
ccusage agentcommand that provides a rich, hierarchical view of Claude Code agent sessions.1. Hierarchical team grouping
Team members are nested under their lead session using indentation, making multi-agent work sessions immediately scannable:
2. Full session UUID on lead rows
Lead rows display the full 36-char UUID, enabling direct session resume:
3. AI-generated session titles
Lead rows show a human-readable title generated via Claude Haiku API. The generator uses compaction summaries from the session JSONL as input to produce concise, descriptive titles (≤60 chars). When no compaction summary is available, the title falls back to a truncated session ID. Slugs were removed entirely as a title source. Titles are cached to
~/.claude/session-titles/<sessionId>to avoid re-generating on every invocation.4. Orphan team discovery
Sessions spawned as Claude Code team members are linked back to their lead via a 3-phase chain:
~/.claude/team-lead-cache/): populated by a PostToolUse hook atTeamCreatetime{leadSessionId}/subagents/directories written by Claude CodeTeamCreatetool_use entries in lead session JSONL files as a final fallback5. Cost % column
A
%column shows each session's share of total spend — useful for spotting expensive workers at a glance.Technical details
New files:
agent.ts: the fullccusage agentcommand implementation includingdiscoverOrphanTeams()(3-phase parent discovery) and hierarchical renderingagent-id.ts: public agent identifier utilitiescache-team-lead.sh: PostToolUse hook that captures team→lead mappings atTeamCreatetimefind-agent-id.mjs: script for discovering agent IDs from session filesModified files:
data-loader.ts: new loader to fetch per-agent usage datatable.ts: per-column width overrides to accommodate 36-char UUIDscommands/index.ts: registers the newagentsubcommandconfig-schema.json/package.json: schema and dependency additionsImplementation notes:
peekTeamName()reads only the first 5 lines of a JSONL file to extractteamNameefficientlycontentfield is sometimes a string, sometimes{type:"text", text:"..."}[]with optionalisMeta: trueentries — both forms are handledTest plan
ccusage agentshows team members nested under their lead sessionclaude --resume <uuid-from-ccusage>resumes directly without interactive search%column sums to ~100% across all rowsSummary by CodeRabbit
New Features
Documentation