From 6cfeb921e6edf7410006ac9b02a11b1c3f537b44 Mon Sep 17 00:00:00 2001 From: mgiovani Date: Mon, 29 Sep 2025 17:27:01 -0300 Subject: [PATCH] fix(ccusage): handle case-insensitive session ID lookup Session cost calculation was breaking when users provided uppercase session IDs because the file lookup used case-sensitive glob matching. This fix enables case-insensitive matching by setting the caseSensitiveMatch option to false in the glob call, allowing session IDs to be looked up regardless of case. Fixes #675 --- apps/ccusage/src/data-loader.ts | 95 ++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/apps/ccusage/src/data-loader.ts b/apps/ccusage/src/data-loader.ts index efa28ee6..73f48c2a 100644 --- a/apps/ccusage/src/data-loader.ts +++ b/apps/ccusage/src/data-loader.ts @@ -1093,7 +1093,7 @@ export async function loadSessionUsageById( // Find the JSONL file for this session ID // On Windows, replace backslashes from path.join with forward slashes for tinyglobby compatibility const patterns = claudePaths.map(p => path.join(p, 'projects', '**', `${sessionId}.jsonl`).replace(/\\/g, '/')); - const jsonlFiles = await glob(patterns); + const jsonlFiles = await glob(patterns, { caseSensitiveMatch: false }); if (jsonlFiles.length === 0) { return null; @@ -1576,6 +1576,99 @@ if (import.meta.vitest != null) { expect(result).toBeNull(); }); + + it('loads session with uppercase ID when file is lowercase', async () => { + await using fixture = await createFixture({ + '.claude': { + projects: { + 'test-project': { + 'session-abc.jsonl': JSON.stringify({ + timestamp: '2024-01-01T00:00:00Z', + sessionId: 'session-abc', + message: { + usage: { + input_tokens: 100, + output_tokens: 50, + }, + model: 'claude-sonnet-4-20250514', + }, + costUSD: 0.5, + }), + }, + }, + }, + }); + + vi.stubEnv('CLAUDE_CONFIG_DIR', fixture.getPath('.claude')); + + const result = await loadSessionUsageById('SESSION-ABC', { mode: 'display' }); + + expect(result).not.toBeNull(); + expect(result?.totalCost).toBe(0.5); + expect(result?.entries).toHaveLength(1); + }); + + it('loads session with lowercase ID when file is uppercase', async () => { + await using fixture = await createFixture({ + '.claude': { + projects: { + 'test-project': { + 'SESSION-XYZ.jsonl': JSON.stringify({ + timestamp: '2024-01-01T00:00:00Z', + sessionId: 'SESSION-XYZ', + message: { + usage: { + input_tokens: 200, + output_tokens: 100, + }, + model: 'claude-sonnet-4-20250514', + }, + costUSD: 1.0, + }), + }, + }, + }, + }); + + vi.stubEnv('CLAUDE_CONFIG_DIR', fixture.getPath('.claude')); + + const result = await loadSessionUsageById('session-xyz', { mode: 'display' }); + + expect(result).not.toBeNull(); + expect(result?.totalCost).toBe(1.0); + expect(result?.entries).toHaveLength(1); + }); + + it('loads session with mixed-case ID', async () => { + await using fixture = await createFixture({ + '.claude': { + projects: { + 'test-project': { + 'MixedCase-123.jsonl': JSON.stringify({ + timestamp: '2024-01-01T00:00:00Z', + sessionId: 'MixedCase-123', + message: { + usage: { + input_tokens: 150, + output_tokens: 75, + }, + model: 'claude-sonnet-4-20250514', + }, + costUSD: 0.75, + }), + }, + }, + }, + }); + + vi.stubEnv('CLAUDE_CONFIG_DIR', fixture.getPath('.claude')); + + const result = await loadSessionUsageById('mixedcase-123', { mode: 'display' }); + + expect(result).not.toBeNull(); + expect(result?.totalCost).toBe(0.75); + expect(result?.entries).toHaveLength(1); + }); }); describe('formatDateCompact', () => {