From 6997fc225dbc23156f6a12ff04f1e121f1d85570 Mon Sep 17 00:00:00 2001 From: Dobidop <67412288+Dobidop@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:19:53 +0100 Subject: [PATCH 1/2] Add usage limit tracking (5-hour and weekly) via Anthropic OAuth API Fetches real-time usage limits from api.anthropic.com/api/oauth/usage using Claude Code's existing OAuth credentials (~/.claude/.credentials.json). Displays 5-hour and weekly usage percentages in the status bar alongside the cost display, with warning highlights at 80%+ utilization. Detailed breakdown with progress bars and reset times shown in the webview panel. - New ClaudeApiClient for OAuth-based API calls with auto token refresh - Status bar shows: $X.XX | 5h:X% | 7d:X% - Warning background when either limit exceeds 80% - Tooltip shows reset times for both limits - Webview cards with color-coded progress bars (normal/warning/critical) - Supports per-model limits (Sonnet/Opus weekly) for Max tier users - Graceful fallback when credentials are unavailable Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 4 +- package.json | 5 ++ src/claudeApiClient.ts | 188 ++++++++++++++++++++++++++++++++++++++++ src/extension.ts | 39 ++++++--- src/statusBar.ts | 64 ++++++++++++-- src/types.ts | 27 +++++- src/webview.ts | 191 ++++++++++++++++++++++++++++++++++++++++- 7 files changed, 494 insertions(+), 24 deletions(-) create mode 100644 src/claudeApiClient.ts diff --git a/package-lock.json b/package-lock.json index 4001b6d..e01e8f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-code-usage", - "version": "1.0.0", + "version": "1.0.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-code-usage", - "version": "1.0.0", + "version": "1.0.8", "license": "MIT", "devDependencies": { "@types/glob": "^9.0.0", diff --git a/package.json b/package.json index d11d246..172ba34 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,11 @@ "minimum": 0, "maximum": 4, "description": "Number of decimal places for cost display" + }, + "claudeCodeUsage.usageLimitTracking": { + "type": "boolean", + "default": true, + "description": "Enable usage limit tracking (requires Claude Code authentication via 'claude auth login')" } } }, diff --git a/src/claudeApiClient.ts b/src/claudeApiClient.ts new file mode 100644 index 0000000..d2d7788 --- /dev/null +++ b/src/claudeApiClient.ts @@ -0,0 +1,188 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { ClaudeCredentials, ClaudeApiUsageResponse } from './types'; + +export class ClaudeApiClient { + private credentialsPath: string; + private credentials: ClaudeCredentials | null = null; + + constructor() { + const homeDir = os.homedir(); + this.credentialsPath = path.join(homeDir, '.claude', '.credentials.json'); + } + + /** + * Load credentials from disk + */ + private async loadCredentials(): Promise { + try { + if (!fs.existsSync(this.credentialsPath)) { + console.warn('[ClaudeAPI] Credentials file not found:', this.credentialsPath); + return null; + } + + const content = await fs.promises.readFile(this.credentialsPath, 'utf-8'); + this.credentials = JSON.parse(content); + return this.credentials; + } catch (error) { + console.error('[ClaudeAPI] Failed to load credentials:', error); + return null; + } + } + + /** + * Save updated credentials to disk + */ + private async saveCredentials(credentials: ClaudeCredentials): Promise { + try { + await fs.promises.writeFile( + this.credentialsPath, + JSON.stringify(credentials), + 'utf-8' + ); + this.credentials = credentials; + } catch (error) { + console.error('[ClaudeAPI] Failed to save credentials:', error); + throw error; + } + } + + /** + * Check if access token is expired or about to expire (within 60 seconds) + */ + private isTokenExpired(credentials: ClaudeCredentials): boolean { + const now = Date.now(); + const expiresAt = credentials.claudeAiOauth.expiresAt; + const bufferTime = 60 * 1000; // 60 seconds buffer + return now >= (expiresAt - bufferTime); + } + + /** + * Refresh the access token using the refresh token + * Uses console.anthropic.com endpoint (same as Claude Code CLI) + */ + private async refreshAccessToken(credentials: ClaudeCredentials): Promise { + console.log('[ClaudeAPI] Refreshing access token...'); + const response = await fetch('https://console.anthropic.com/v1/oauth/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + refresh_token: credentials.claudeAiOauth.refreshToken, + grant_type: 'refresh_token', + }), + }); + + if (!response.ok) { + throw new Error(`Token refresh failed: ${response.status} ${response.statusText}`); + } + + const data = await response.json() as { access_token: string; expires_in: number }; + + const updatedCredentials: ClaudeCredentials = { + ...credentials, + claudeAiOauth: { + ...credentials.claudeAiOauth, + accessToken: data.access_token, + expiresAt: Date.now() + (data.expires_in * 1000), + }, + }; + + await this.saveCredentials(updatedCredentials); + console.log('[ClaudeAPI] Token refreshed successfully'); + return updatedCredentials; + } + + /** + * Get valid credentials, refreshing if necessary + */ + private async getValidCredentials(): Promise { + let credentials = this.credentials || await this.loadCredentials(); + + if (!credentials) { + return null; + } + + if (this.isTokenExpired(credentials)) { + console.log('[ClaudeAPI] Access token expired, refreshing...'); + try { + credentials = await this.refreshAccessToken(credentials); + } catch (error) { + console.error('[ClaudeAPI] Failed to refresh token:', error); + return null; + } + } + + return credentials; + } + + /** + * Fetch usage limits from the Anthropic API + * Uses api.anthropic.com/api/oauth/usage (NOT claude.ai which has Cloudflare) + */ + async fetchUsageLimits(): Promise { + try { + const credentials = await this.getValidCredentials(); + if (!credentials) { + console.warn('[ClaudeAPI] No valid credentials available. Run "claude auth login" first.'); + return null; + } + + console.log('[ClaudeAPI] Fetching usage limits from api.anthropic.com...'); + + const response = await fetch('https://api.anthropic.com/api/oauth/usage', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${credentials.claudeAiOauth.accessToken}`, + 'anthropic-beta': 'oauth-2025-04-20', + 'Content-Type': 'application/json', + }, + }); + + console.log('[ClaudeAPI] Response status:', response.status); + + // If 401, try refreshing token and retry once + if (response.status === 401) { + console.log('[ClaudeAPI] Got 401, attempting token refresh and retry...'); + try { + const refreshed = await this.refreshAccessToken(credentials); + const retryResponse = await fetch('https://api.anthropic.com/api/oauth/usage', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${refreshed.claudeAiOauth.accessToken}`, + 'anthropic-beta': 'oauth-2025-04-20', + 'Content-Type': 'application/json', + }, + }); + + if (!retryResponse.ok) { + console.error('[ClaudeAPI] Retry failed:', retryResponse.status); + return null; + } + + const data = await retryResponse.json() as ClaudeApiUsageResponse; + console.log('[ClaudeAPI] Usage limits fetched successfully (after retry)'); + return data; + } catch (refreshError) { + console.error('[ClaudeAPI] Token refresh failed:', refreshError); + return null; + } + } + + if (!response.ok) { + const errorText = await response.text(); + console.error('[ClaudeAPI] API error:', response.status, errorText); + return null; + } + + const data = await response.json() as ClaudeApiUsageResponse; + console.log('[ClaudeAPI] Usage limits fetched successfully:', JSON.stringify(data, null, 2)); + return data; + } catch (error) { + console.error('[ClaudeAPI] Failed to fetch usage limits:', error); + return null; + } + } +} diff --git a/src/extension.ts b/src/extension.ts index c11ff25..a6592c3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,27 +3,34 @@ import { ClaudeDataLoader } from './dataLoader'; import { StatusBarManager } from './statusBar'; import { UsageWebviewProvider } from './webview'; import { I18n } from './i18n'; -import { ExtensionConfig, UsageData, SessionData } from './types'; +import { ClaudeApiClient } from './claudeApiClient'; +import { ExtensionConfig, UsageData, SessionData, ClaudeApiUsageResponse } from './types'; export class ClaudeCodeUsageExtension { private statusBar: StatusBarManager; private webviewProvider: UsageWebviewProvider; + private apiClient: ClaudeApiClient; private refreshTimer: NodeJS.Timeout | undefined; private cache: { records: any[]; lastUpdate: Date; dataDirectory: string | null; + usageLimits: ClaudeApiUsageResponse | null; + usageLimitsLastUpdate: Date; } = { records: [], lastUpdate: new Date(0), - dataDirectory: null + dataDirectory: null, + usageLimits: null, + usageLimitsLastUpdate: new Date(0) }; constructor(private context: vscode.ExtensionContext) { console.log('Claude Code Usage Extension: Constructor called'); this.statusBar = new StatusBarManager(); this.webviewProvider = new UsageWebviewProvider(context); - + this.apiClient = new ClaudeApiClient(); + this.setupCommands(); this.loadConfiguration(); this.startAutoRefresh(); @@ -107,7 +114,7 @@ export class ClaudeCodeUsageExtension { this.webviewProvider.setLoading(true); const config = this.getConfiguration(); - + // Find Claude data directory const dataDirectory = await ClaudeDataLoader.findClaudeDataDirectory( config.dataDirectory || undefined @@ -122,7 +129,7 @@ export class ClaudeCodeUsageExtension { // Check if we need to reload data const shouldReload = this.shouldReloadData(dataDirectory); - + let records = this.cache.records; if (shouldReload) { records = await ClaudeDataLoader.loadUsageRecords(dataDirectory); @@ -131,10 +138,22 @@ export class ClaudeCodeUsageExtension { this.cache.dataDirectory = dataDirectory; } + // Fetch usage limits from API (cache for 2 minutes) + const shouldReloadUsageLimits = Date.now() - this.cache.usageLimitsLastUpdate.getTime() > 120000; + let usageLimits = this.cache.usageLimits; + if (shouldReloadUsageLimits) { + const fetchedLimits = await this.apiClient.fetchUsageLimits(); + if (fetchedLimits) { + usageLimits = fetchedLimits; + this.cache.usageLimits = usageLimits; + this.cache.usageLimitsLastUpdate = new Date(); + } + } + if (records.length === 0) { const error = 'No usage records found. Make sure Claude Code is running.'; - this.statusBar.updateUsageData(null, error); - this.webviewProvider.updateData(null, null, null, null, [], [], [], error, dataDirectory); + this.statusBar.updateUsageData(null, error, usageLimits); + this.webviewProvider.updateData(null, null, null, null, [], [], [], error, dataDirectory, undefined, usageLimits); return; } @@ -148,13 +167,13 @@ export class ClaudeCodeUsageExtension { const hourlyDataForToday = ClaudeDataLoader.getHourlyDataForToday(records); // Update UI - this.statusBar.updateUsageData(todayData); - this.webviewProvider.updateData(sessionData, todayData, monthData, allTimeData, dailyDataForMonth, dailyDataForAllTime, hourlyDataForToday, undefined, dataDirectory, records); + this.statusBar.updateUsageData(todayData, undefined, usageLimits); + this.webviewProvider.updateData(sessionData, todayData, monthData, allTimeData, dailyDataForMonth, dailyDataForAllTime, hourlyDataForToday, undefined, dataDirectory, records, usageLimits); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; console.error('Error refreshing Claude Code usage data:', error); - + this.statusBar.updateUsageData(null, errorMessage); this.webviewProvider.updateData(null, null, null, null, [], [], [], errorMessage, null); } diff --git a/src/statusBar.ts b/src/statusBar.ts index 608d476..6d32982 100644 --- a/src/statusBar.ts +++ b/src/statusBar.ts @@ -1,10 +1,11 @@ import * as vscode from 'vscode'; -import { UsageData } from './types'; +import { UsageData, ClaudeApiUsageResponse } from './types'; import { I18n } from './i18n'; export class StatusBarManager { private statusBarItem: vscode.StatusBarItem; private isLoading: boolean = false; + private usageLimits: ClaudeApiUsageResponse | null = null; constructor() { this.statusBarItem = vscode.window.createStatusBarItem( @@ -21,9 +22,13 @@ export class StatusBarManager { this.updateStatusBar(); } - updateUsageData(todayData: UsageData | null, error?: string): void { + updateUsageData(todayData: UsageData | null, error?: string, usageLimits?: ClaudeApiUsageResponse | null): void { this.isLoading = false; - + + if (usageLimits !== undefined) { + this.usageLimits = usageLimits; + } + if (error) { this.showError(error); return; @@ -47,11 +52,33 @@ export class StatusBarManager { private showTodayData(todayData: UsageData): void { const cost = I18n.formatCurrency(todayData.totalCost); - this.statusBarItem.text = `$(pulse) ${cost}`; - + let text = `$(pulse) ${cost}`; + + // Append usage limits if available + if (this.usageLimits?.five_hour || this.usageLimits?.seven_day) { + const parts: string[] = []; + if (this.usageLimits.five_hour) { + parts.push(`5h:${Math.round(this.usageLimits.five_hour.utilization)}% |`); + } + if (this.usageLimits.seven_day) { + parts.push(`7d:${Math.round(this.usageLimits.seven_day.utilization)}%`); + } + text += ` | ${parts.join(' ')}`; + } + + this.statusBarItem.text = text; + const tooltip = this.createTooltip(todayData); this.statusBarItem.tooltip = tooltip; - this.statusBarItem.backgroundColor = undefined; + + // Warn if either limit is high + const fiveHourHigh = this.usageLimits?.five_hour && this.usageLimits.five_hour.utilization >= 80; + const weeklyHigh = this.usageLimits?.seven_day && this.usageLimits.seven_day.utilization >= 80; + if (fiveHourHigh || weeklyHigh) { + this.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); + } else { + this.statusBarItem.backgroundColor = undefined; + } } private showNoData(): void { @@ -72,11 +99,30 @@ export class StatusBarManager { `${I18n.t.popup.cost}: ${I18n.formatCurrency(todayData.totalCost)}`, `${I18n.t.popup.inputTokens}: ${I18n.formatNumber(todayData.totalInputTokens)}`, `${I18n.t.popup.outputTokens}: ${I18n.formatNumber(todayData.totalOutputTokens)}`, - `${I18n.t.popup.messages}: ${I18n.formatNumber(todayData.messageCount)}`, - '', - 'Click for detailed breakdown' + `${I18n.t.popup.messages}: ${I18n.formatNumber(todayData.messageCount)}` ]; + // Add usage limits if available + if (this.usageLimits) { + lines.push(''); + lines.push('Usage Limits:'); + + if (this.usageLimits.five_hour) { + const resetDate = new Date(this.usageLimits.five_hour.resets_at); + const hoursUntilReset = Math.max(0, (resetDate.getTime() - Date.now()) / (1000 * 60 * 60)); + lines.push(`5-Hour: ${this.usageLimits.five_hour.utilization.toFixed(1)}% (resets in ${hoursUntilReset.toFixed(1)}h)`); + } + + if (this.usageLimits.seven_day) { + const resetDate = new Date(this.usageLimits.seven_day.resets_at); + const daysUntilReset = Math.max(0, (resetDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)); + lines.push(`Weekly: ${this.usageLimits.seven_day.utilization.toFixed(1)}% (resets in ${daysUntilReset.toFixed(1)}d)`); + } + } + + lines.push(''); + lines.push('Click for detailed breakdown'); + return lines.join('\n'); } diff --git a/src/types.ts b/src/types.ts index eeff010..dc04e58 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,4 +52,29 @@ export interface ModelPricing { cache_read_input_token_cost?: number; } -export type SupportedLanguage = 'en' | "de-DE" | 'zh-TW' | 'zh-CN' | 'ja' | 'ko'; \ No newline at end of file +export type SupportedLanguage = 'en' | "de-DE" | 'zh-TW' | 'zh-CN' | 'ja' | 'ko'; + +// Claude API Usage Limits +export interface UsageLimit { + utilization: number; // percentage (0-100) + resets_at: string; // ISO timestamp +} + +export interface ClaudeApiUsageResponse { + five_hour?: UsageLimit; + seven_day?: UsageLimit; + seven_day_sonnet?: UsageLimit; + seven_day_opus?: UsageLimit; +} + +export interface ClaudeCredentials { + claudeAiOauth: { + accessToken: string; + refreshToken: string; + expiresAt: number; // Unix timestamp in ms + scopes: string[]; + subscriptionType: string | null; + rateLimitTier: string | null; + }; + organizationUuid: string; +} \ No newline at end of file diff --git a/src/webview.ts b/src/webview.ts index c6d9b3e..d615416 100644 --- a/src/webview.ts +++ b/src/webview.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import { I18n } from './i18n'; -import { SessionData, UsageData } from './types'; +import { SessionData, UsageData, ClaudeApiUsageResponse } from './types'; export class UsageWebviewProvider { private panel: vscode.WebviewPanel | undefined; @@ -17,6 +17,7 @@ export class UsageWebviewProvider { private currentTab: string = 'today'; private hourlyDataCache: Map = new Map(); private allRecords: any[] = []; + private usageLimits: ClaudeApiUsageResponse | null = null; constructor(private context: vscode.ExtensionContext) {} @@ -96,7 +97,8 @@ export class UsageWebviewProvider { hourlyDataForToday: { hour: string; data: UsageData }[] = [], error?: string, dataDirectory?: string | null, - allRecords?: any[] + allRecords?: any[], + usageLimits?: ClaudeApiUsageResponse | null ): void { this.currentSessionData = sessionData; this.todayData = todayData; @@ -111,6 +113,9 @@ export class UsageWebviewProvider { if (allRecords) { this.allRecords = allRecords; } + if (usageLimits !== undefined) { + this.usageLimits = usageLimits; + } if (this.panel) { this.updateWebview(); @@ -262,6 +267,10 @@ export class UsageWebviewProvider { + ` + + this.renderUsageLimits() + + ` +
'; + + return html; + } + private renderMonthData(): string { if (!this.monthData) { return `

${I18n.t.popup.noDataMessage}

`; @@ -1269,6 +1362,100 @@ export class UsageWebviewProvider { color: var(--vscode-descriptionForeground); padding: 20px; } + + .usage-limits-section { + margin-bottom: 24px; + padding: 16px; + background: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 8px; + } + + .usage-limits-section h2 { + margin: 0 0 16px 0; + font-size: 16px; + color: var(--vscode-foreground); + } + + .usage-limits-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; + } + + .limit-card { + padding: 16px; + background: var(--vscode-input-background); + border-radius: 8px; + border: 2px solid var(--vscode-input-border); + } + + .limit-card.warning { + border-color: var(--vscode-editorWarning-foreground); + background: rgba(255, 165, 0, 0.1); + } + + .limit-card.critical { + border-color: var(--vscode-editorError-foreground); + background: rgba(255, 0, 0, 0.1); + } + + .limit-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + } + + .limit-header h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: var(--vscode-foreground); + } + + .limit-percentage { + font-size: 18px; + font-weight: bold; + color: var(--vscode-charts-blue); + } + + .limit-card.warning .limit-percentage { + color: var(--vscode-editorWarning-foreground); + } + + .limit-card.critical .limit-percentage { + color: var(--vscode-editorError-foreground); + } + + .limit-bar { + width: 100%; + height: 8px; + background: var(--vscode-progressBar-background); + border-radius: 4px; + overflow: hidden; + margin-bottom: 8px; + } + + .limit-progress { + height: 100%; + background: var(--vscode-charts-blue); + border-radius: 4px; + transition: width 0.3s ease; + } + + .limit-card.warning .limit-progress { + background: var(--vscode-editorWarning-foreground); + } + + .limit-card.critical .limit-progress { + background: var(--vscode-editorError-foreground); + } + + .limit-footer { + font-size: 12px; + color: var(--vscode-descriptionForeground); + } `; } From 194cdde2786a0be3278de2d9406d0fd8f9889887 Mon Sep 17 00:00:00 2001 From: Dobidop <67412288+Dobidop@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:24:16 +0100 Subject: [PATCH 2/2] Enhance usage limit tracking and error handling in Claude API client and data loader --- src/claudeApiClient.ts | 47 ++++++++++++++++++++++++++---------------- src/dataLoader.ts | 13 +++++++++--- src/extension.ts | 17 ++++++++++++++- src/statusBar.ts | 3 ++- 4 files changed, 57 insertions(+), 23 deletions(-) diff --git a/src/claudeApiClient.ts b/src/claudeApiClient.ts index d2d7788..9ffaf8a 100644 --- a/src/claudeApiClient.ts +++ b/src/claudeApiClient.ts @@ -6,6 +6,7 @@ import { ClaudeCredentials, ClaudeApiUsageResponse } from './types'; export class ClaudeApiClient { private credentialsPath: string; private credentials: ClaudeCredentials | null = null; + private rateLimitedUntil: number = 0; constructor() { const homeDir = os.homedir(); @@ -122,8 +123,25 @@ export class ClaudeApiClient { * Fetch usage limits from the Anthropic API * Uses api.anthropic.com/api/oauth/usage (NOT claude.ai which has Cloudflare) */ + private async callUsageApi(accessToken: string): Promise { + return fetch('https://api.anthropic.com/api/oauth/usage', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'anthropic-beta': 'oauth-2025-04-20', + 'Content-Type': 'application/json', + }, + }); + } + async fetchUsageLimits(): Promise { try { + // Respect rate limit backoff + if (Date.now() < this.rateLimitedUntil) { + console.log('[ClaudeAPI] Rate limited, skipping until', new Date(this.rateLimitedUntil).toISOString()); + return null; + } + const credentials = await this.getValidCredentials(); if (!credentials) { console.warn('[ClaudeAPI] No valid credentials available. Run "claude auth login" first.'); @@ -131,31 +149,24 @@ export class ClaudeApiClient { } console.log('[ClaudeAPI] Fetching usage limits from api.anthropic.com...'); - - const response = await fetch('https://api.anthropic.com/api/oauth/usage', { - method: 'GET', - headers: { - 'Authorization': `Bearer ${credentials.claudeAiOauth.accessToken}`, - 'anthropic-beta': 'oauth-2025-04-20', - 'Content-Type': 'application/json', - }, - }); - + const response = await this.callUsageApi(credentials.claudeAiOauth.accessToken); console.log('[ClaudeAPI] Response status:', response.status); + // Handle rate limiting with backoff + if (response.status === 429) { + const retryAfter = response.headers.get('retry-after'); + const backoffMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : 5 * 60 * 1000; + this.rateLimitedUntil = Date.now() + backoffMs; + console.warn(`[ClaudeAPI] Rate limited, backing off for ${backoffMs / 1000}s`); + return null; + } + // If 401, try refreshing token and retry once if (response.status === 401) { console.log('[ClaudeAPI] Got 401, attempting token refresh and retry...'); try { const refreshed = await this.refreshAccessToken(credentials); - const retryResponse = await fetch('https://api.anthropic.com/api/oauth/usage', { - method: 'GET', - headers: { - 'Authorization': `Bearer ${refreshed.claudeAiOauth.accessToken}`, - 'anthropic-beta': 'oauth-2025-04-20', - 'Content-Type': 'application/json', - }, - }); + const retryResponse = await this.callUsageApi(refreshed.claudeAiOauth.accessToken); if (!retryResponse.ok) { console.error('[ClaudeAPI] Retry failed:', retryResponse.status); diff --git a/src/dataLoader.ts b/src/dataLoader.ts index 3723417..ca93636 100644 --- a/src/dataLoader.ts +++ b/src/dataLoader.ts @@ -138,16 +138,20 @@ export class ClaudeDataLoader { static async loadUsageRecords(dataDirectory?: string): Promise { try { const claudePaths = dataDirectory ? [dataDirectory] : this.getClaudePaths(); + console.log('[DataLoader] Loading records from paths:', claudePaths); const allFiles: string[] = []; for (const claudePath of claudePaths) { const claudeDir = path.join(claudePath, CLAUDE_PROJECTS_DIR_NAME); + console.log('[DataLoader] Checking directory:', claudeDir, 'exists:', fs.existsSync(claudeDir)); if (fs.existsSync(claudeDir)) { const files = await findJsonlFiles(claudeDir); + console.log('[DataLoader] Found JSONL files:', files.length); allFiles.push(...files); } } + console.log('[DataLoader] Total files found:', allFiles.length); const sortedFiles = await this.sortFilesByTimestamp(allFiles); const processedHashes = new Set(); const records: ClaudeUsageRecord[] = []; @@ -159,12 +163,14 @@ export class ClaudeDataLoader { .trim() .split('\n') .filter((line) => line.trim() !== ''); + console.log('[DataLoader] Processing file:', file, 'lines:', lines.length); for (const line of lines) { try { const parsed = JSON.parse(line) as unknown; if (!validateUsageRecord(parsed)) { + console.log('[DataLoader] Record validation failed for line in', file); continue; } @@ -181,17 +187,18 @@ export class ClaudeDataLoader { records.push(data as ClaudeUsageRecord); } catch (parseError) { - console.warn(`Failed to parse line in ${file}:`, parseError); + console.warn('[DataLoader] Failed to parse line in', file, ':', parseError); } } } catch (fileError) { - console.warn(`Failed to read file ${file}:`, fileError); + console.warn('[DataLoader] Failed to read file', file, ':', fileError); } } + console.log('[DataLoader] Loaded total records:', records.length); return records; } catch (error) { - console.error('Error loading usage records:', error); + console.error('[DataLoader] Error loading usage records:', error); return []; } } diff --git a/src/extension.ts b/src/extension.ts index a6592c3..1e05124 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -110,18 +110,23 @@ export class ClaudeCodeUsageExtension { private async refreshData(): Promise { try { + console.log('[Extension] refreshData started'); this.statusBar.setLoading(true); this.webviewProvider.setLoading(true); const config = this.getConfiguration(); + console.log('[Extension] Config:', { refreshInterval: config.refreshInterval, dataDirectory: config.dataDirectory }); // Find Claude data directory + console.log('[Extension] Finding Claude data directory...'); const dataDirectory = await ClaudeDataLoader.findClaudeDataDirectory( config.dataDirectory || undefined ); + console.log('[Extension] Data directory found:', dataDirectory); if (!dataDirectory) { const error = 'Claude data directory not found. Please check your configuration.'; + console.error('[Extension]', error); this.statusBar.updateUsageData(null, error); this.webviewProvider.updateData(null, null, null, null, [], [], [], error, null); return; @@ -129,13 +134,18 @@ export class ClaudeCodeUsageExtension { // Check if we need to reload data const shouldReload = this.shouldReloadData(dataDirectory); + console.log('[Extension] Should reload data:', shouldReload); let records = this.cache.records; if (shouldReload) { + console.log('[Extension] Loading usage records...'); records = await ClaudeDataLoader.loadUsageRecords(dataDirectory); + console.log('[Extension] Loaded records:', records.length); this.cache.records = records; this.cache.lastUpdate = new Date(); this.cache.dataDirectory = dataDirectory; + } else { + console.log('[Extension] Using cached records:', records.length); } // Fetch usage limits from API (cache for 2 minutes) @@ -152,10 +162,12 @@ export class ClaudeCodeUsageExtension { if (records.length === 0) { const error = 'No usage records found. Make sure Claude Code is running.'; + console.warn('[Extension]', error); this.statusBar.updateUsageData(null, error, usageLimits); this.webviewProvider.updateData(null, null, null, null, [], [], [], error, dataDirectory, undefined, usageLimits); return; } + console.log('[Extension] Processing', records.length, 'records'); // Calculate usage data const sessionData = ClaudeDataLoader.getCurrentSessionData(records); @@ -167,12 +179,15 @@ export class ClaudeCodeUsageExtension { const hourlyDataForToday = ClaudeDataLoader.getHourlyDataForToday(records); // Update UI + console.log('[Extension] Updating UI with data. Today cost:', todayData?.totalCost || 0); this.statusBar.updateUsageData(todayData, undefined, usageLimits); this.webviewProvider.updateData(sessionData, todayData, monthData, allTimeData, dailyDataForMonth, dailyDataForAllTime, hourlyDataForToday, undefined, dataDirectory, records, usageLimits); + console.log('[Extension] refreshData completed successfully'); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; - console.error('Error refreshing Claude Code usage data:', error); + console.error('[Extension] Error refreshing Claude Code usage data:', errorMessage); + console.error('[Extension] Full error:', error); this.statusBar.updateUsageData(null, errorMessage); this.webviewProvider.updateData(null, null, null, null, [], [], [], errorMessage, null); diff --git a/src/statusBar.ts b/src/statusBar.ts index 6d32982..beb8380 100644 --- a/src/statusBar.ts +++ b/src/statusBar.ts @@ -14,7 +14,8 @@ export class StatusBarManager { ); this.statusBarItem.command = 'claudeCodeUsage.showDetails'; this.statusBarItem.show(); - this.updateStatusBar(); + // Initialize with loading state + this.setLoading(true); } setLoading(loading: boolean): void {