From ea089c0c076b0f4b14dd702ffd6135a53af611c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 02:29:36 +0000 Subject: [PATCH 1/2] feat: add Export Usage Data command (CSV / JSON) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New command "Claude Status: Export Usage Data..." exports the daily usage history — the same aggregation that powers the heatmap, covering claudeStatus.heatmap.days days — for expense reports and external analysis. - QuickPick format selection (CSV or JSON) + save dialog - CSV: date,cost_usd,messages,tokens with one row per day - JSON: exportedAt, days, totalCostUsd, and per-day records - Serializers in src/data/exportUsage.ts (pure, unit-tested) - l10n: EN/JA/ZH command title and runtime strings - Docs: SETTINGS.md commands section, README usage tables (EN/JA/ZH), CHANGELOG Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012ZLCLQeudf1LQBnBSyyr4f --- CHANGELOG.md | 7 ++++ README.ja.md | 1 + README.md | 1 + README.zh.md | 1 + docs/SETTINGS.md | 16 +++++++++ l10n/bundle.l10n.ja.json | 8 ++++- l10n/bundle.l10n.zh-cn.json | 8 ++++- package.json | 4 +++ package.nls.ja.json | 1 + package.nls.json | 1 + package.nls.zh-cn.json | 1 + src/data/exportUsage.ts | 31 ++++++++++++++++ src/extension.ts | 48 +++++++++++++++++++++++++ src/test/suite/exportUsage.test.ts | 58 ++++++++++++++++++++++++++++++ 14 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 src/data/exportUsage.ts create mode 100644 src/test/suite/exportUsage.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 64ff5fc..87ff409 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + +- **Export Usage Data command** (`Claude Status: Export Usage Data...`) — export the + daily usage history (date, cost, message count, tokens; same aggregation as the + heatmap, covering `claudeStatus.heatmap.days` days) to a **CSV** or **JSON** file + via a save dialog. Useful for expense reports and external analysis. + --- ## [0.6.2] — 2026-06-13 diff --git a/README.ja.md b/README.ja.md index 715a308..3fd422e 100644 --- a/README.ja.md +++ b/README.ja.md @@ -199,6 +199,7 @@ npm run package # → vscode-claude-status-*.vsix | **Claude Status: Open Dashboard** | ダッシュボードパネルを開く | | **Claude Status: Toggle % / $ Display** | 表示モードの切り替え | | **Claude Status: Set Budget…** | 日次USD予算を設定または無効化 | +| **Claude Status: 使用履歴をエクスポート…** | 日次使用履歴を CSV / JSON にエクスポート | --- diff --git a/README.md b/README.md index b1fe3af..29bdc21 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,7 @@ The extension activates automatically on VS Code startup (`onStartupFinished`). | **Claude Status: Open Dashboard** | Open dashboard panel | | **Claude Status: Toggle % / $ Display** | Switch display mode | | **Claude Status: Set Budget…** | Set or disable daily USD budget | +| **Claude Status: Export Usage Data…** | Export daily usage history to CSV or JSON | --- diff --git a/README.zh.md b/README.zh.md index acc580b..3911e86 100644 --- a/README.zh.md +++ b/README.zh.md @@ -194,6 +194,7 @@ npm run package # → vscode-claude-status-*.vsix | **Claude Status: Open Dashboard** | 打开仪表板面板 | | **Claude Status: Toggle % / $ Display** | 切换显示模式 | | **Claude Status: Set Budget…** | 设置或禁用每日 USD 预算 | +| **Claude Status: 导出使用历史…** | 将每日使用历史导出为 CSV 或 JSON | --- diff --git a/docs/SETTINGS.md b/docs/SETTINGS.md index c7cd670..a2cd6ad 100644 --- a/docs/SETTINGS.md +++ b/docs/SETTINGS.md @@ -251,10 +251,26 @@ Set `claudeStatus.statusBar.format` to a template string to replace the default { "command": "vscode-claude-status.setBudget", "title": "Claude Status: Set Budget..." + }, + { + "command": "vscode-claude-status.exportUsage", + "title": "Claude Status: Export Usage Data..." } ] ``` +### Export Usage Data + +`vscode-claude-status.exportUsage` exports the daily usage history (the same +aggregation that powers the heatmap, covering `claudeStatus.heatmap.days` days) +to a file chosen via a save dialog: + +- **CSV** — `date,cost_usd,messages,tokens`, one row per day (zero-activity days + included), cost with 4 decimal places. +- **JSON** — `{ exportedAt, days, totalCostUsd, daily: [{date, costUsd, messages, tokens}] }`. + +Serializers live in `src/data/exportUsage.ts` (pure functions, unit-tested). + --- ## Keybindings (`package.json` contributes.keybindings) diff --git a/l10n/bundle.l10n.ja.json b/l10n/bundle.l10n.ja.json index 9828276..5fcf1a1 100644 --- a/l10n/bundle.l10n.ja.json +++ b/l10n/bundle.l10n.ja.json @@ -103,5 +103,11 @@ "⚙ Set format": "⚙ 書式を設定", "Format string:": "書式文字列:", "Reset to default": "デフォルトに戻す", - "(default)": "(デフォルト)" + "(default)": "(デフォルト)", + "Spreadsheet-friendly (date, cost, messages, tokens)": "表計算向け(日付、コスト、メッセージ数、トークン数)", + "Machine-readable with export metadata": "エクスポートメタデータ付きの機械可読形式", + "Select export format": "エクスポート形式を選択", + "No usage history to export yet.": "エクスポートできる使用履歴がまだありません。", + "Usage history exported to {0}": "使用履歴を {0} にエクスポートしました", + "Failed to export usage history: {0}": "使用履歴のエクスポートに失敗しました: {0}" } diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index 6e7f944..4515f31 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -103,5 +103,11 @@ "⚙ Set format": "⚙ 设置格式", "Format string:": "格式字符串:", "Reset to default": "恢复默认", - "(default)": "(默认)" + "(default)": "(默认)", + "Spreadsheet-friendly (date, cost, messages, tokens)": "适合电子表格(日期、费用、消息数、令牌数)", + "Machine-readable with export metadata": "带导出元数据的机器可读格式", + "Select export format": "选择导出格式", + "No usage history to export yet.": "暂无可导出的使用历史。", + "Usage history exported to {0}": "使用历史已导出到 {0}", + "Failed to export usage history: {0}": "导出使用历史失败: {0}" } diff --git a/package.json b/package.json index 8a4de64..eb0c862 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,10 @@ { "command": "vscode-claude-status.setBudget", "title": "%cmd.setBudget%" + }, + { + "command": "vscode-claude-status.exportUsage", + "title": "%cmd.exportUsage%" } ], "keybindings": [ diff --git a/package.nls.ja.json b/package.nls.ja.json index 56f4a41..608ffe5 100644 --- a/package.nls.ja.json +++ b/package.nls.ja.json @@ -5,6 +5,7 @@ "cmd.refresh": "Claude Status: 今すぐ更新", "cmd.toggleDisplayMode": "Claude Status: % / $ 表示を切替", "cmd.setBudget": "Claude Status: 予算を設定...", + "cmd.exportUsage": "Claude Status: 使用履歴をエクスポート...", "config.title": "Claude Status", "config.displayMode.desc": "ステータスバーの表示モード: 利用率(%) または USD コスト。", "config.statusBar.alignment.desc": "ステータスバーアイテムの位置。", diff --git a/package.nls.json b/package.nls.json index 9c882f9..4c0a8f6 100644 --- a/package.nls.json +++ b/package.nls.json @@ -5,6 +5,7 @@ "cmd.refresh": "Claude Status: Refresh Now", "cmd.toggleDisplayMode": "Claude Status: Toggle % / $ Display", "cmd.setBudget": "Claude Status: Set Budget...", + "cmd.exportUsage": "Claude Status: Export Usage Data...", "config.title": "Claude Status", "config.displayMode.desc": "Status bar display mode: utilization percentage or USD cost.", "config.statusBar.alignment.desc": "Position of the status bar item.", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index f77b2fc..813d001 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -5,6 +5,7 @@ "cmd.refresh": "Claude Status: 立即刷新", "cmd.toggleDisplayMode": "Claude Status: 切换 % / $ 显示", "cmd.setBudget": "Claude Status: 设置预算...", + "cmd.exportUsage": "Claude Status: 导出使用历史...", "config.title": "Claude Status", "config.displayMode.desc": "状态栏显示模式:利用率百分比或 USD 费用。", "config.statusBar.alignment.desc": "状态栏项目的位置。", diff --git a/src/data/exportUsage.ts b/src/data/exportUsage.ts new file mode 100644 index 0000000..36b34bf --- /dev/null +++ b/src/data/exportUsage.ts @@ -0,0 +1,31 @@ +import type { DailyUsage } from '../webview/heatmap'; + +// Serializers for the "Export Usage Data" command. Pure functions — no vscode +// dependency — so they are unit-testable outside the extension host. + +export function dailyUsageToCsv(daily: DailyUsage[]): string { + const lines = ['date,cost_usd,messages,tokens']; + for (const d of daily) { + lines.push(`${d.date},${d.cost.toFixed(4)},${d.sessionCount},${d.tokensTotal}`); + } + return lines.join('\n') + '\n'; +} + +export function dailyUsageToJson(daily: DailyUsage[]): string { + const totalCostUsd = daily.reduce((sum, d) => sum + d.cost, 0); + return JSON.stringify( + { + exportedAt: new Date().toISOString(), + days: daily.length, + totalCostUsd: Number(totalCostUsd.toFixed(4)), + daily: daily.map(d => ({ + date: d.date, + costUsd: Number(d.cost.toFixed(4)), + messages: d.sessionCount, + tokens: d.tokensTotal, + })), + }, + null, + 2 + ) + '\n'; +} diff --git a/src/extension.ts b/src/extension.ts index 7772685..b8c7ba0 100755 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,5 +1,8 @@ import * as vscode from 'vscode'; +import * as os from 'os'; +import * as path from 'path'; import { DataManager, ClaudeUsageData, PredictionData } from './data/dataManager'; +import { dailyUsageToCsv, dailyUsageToJson } from './data/exportUsage'; import { StatusBarManager } from './statusBar'; import { config } from './config'; @@ -107,6 +110,51 @@ export function activate(context: vscode.ExtensionContext) { : vscode.l10n.t('Daily budget set to ${0}.', value.toFixed(2)) ); }), + vscode.commands.registerCommand('vscode-claude-status.exportUsage', async () => { + type FormatPick = vscode.QuickPickItem & { format: 'csv' | 'json' }; + const picks: FormatPick[] = [ + { + label: 'CSV', + description: vscode.l10n.t('Spreadsheet-friendly (date, cost, messages, tokens)'), + format: 'csv', + }, + { + label: 'JSON', + description: vscode.l10n.t('Machine-readable with export metadata'), + format: 'json', + }, + ]; + const pick = await vscode.window.showQuickPick(picks, { + placeHolder: vscode.l10n.t('Select export format'), + }); + if (!pick) { return; } // cancelled + + const heatmap = await dataManager.getHeatmapData(); + const daily = heatmap?.daily ?? []; + if (daily.every(d => d.sessionCount === 0)) { + vscode.window.showWarningMessage(vscode.l10n.t('No usage history to export yet.')); + return; + } + + const today = new Date().toISOString().slice(0, 10); + const uri = await vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file(path.join(os.homedir(), `claude-usage-${today}.${pick.format}`)), + filters: pick.format === 'csv' ? { CSV: ['csv'] } : { JSON: ['json'] }, + }); + if (!uri) { return; } // cancelled + + const content = pick.format === 'csv' ? dailyUsageToCsv(daily) : dailyUsageToJson(daily); + try { + await vscode.workspace.fs.writeFile(uri, Buffer.from(content, 'utf8')); + vscode.window.showInformationMessage( + vscode.l10n.t('Usage history exported to {0}', uri.fsPath) + ); + } catch (err) { + vscode.window.showErrorMessage( + vscode.l10n.t('Failed to export usage history: {0}', String(err)) + ); + } + }), ); // React to data updates (usage + project costs are refreshed together) diff --git a/src/test/suite/exportUsage.test.ts b/src/test/suite/exportUsage.test.ts new file mode 100644 index 0000000..c7733f3 --- /dev/null +++ b/src/test/suite/exportUsage.test.ts @@ -0,0 +1,58 @@ +import * as assert from 'assert'; +import { dailyUsageToCsv, dailyUsageToJson } from '../../data/exportUsage'; +import type { DailyUsage } from '../../webview/heatmap'; + +const sample: DailyUsage[] = [ + { date: '2026-07-05', cost: 1.23456, sessionCount: 12, tokensTotal: 45000 }, + { date: '2026-07-06', cost: 0, sessionCount: 0, tokensTotal: 0 }, + { date: '2026-07-07', cost: 2.5, sessionCount: 3, tokensTotal: 9000 }, +]; + +suite('ExportUsage', () => { + suite('dailyUsageToCsv', () => { + test('emits header plus one row per day, newline-terminated', () => { + const csv = dailyUsageToCsv(sample); + const lines = csv.split('\n'); + assert.strictEqual(lines[0], 'date,cost_usd,messages,tokens'); + assert.strictEqual(lines.length, 5, 'header + 3 rows + trailing newline'); + assert.strictEqual(lines[4], ''); + }); + + test('formats cost with 4 decimal places', () => { + const csv = dailyUsageToCsv(sample); + assert.ok(csv.includes('2026-07-05,1.2346,12,45000'), csv); + assert.ok(csv.includes('2026-07-06,0.0000,0,0'), csv); + }); + + test('handles empty input', () => { + assert.strictEqual(dailyUsageToCsv([]), 'date,cost_usd,messages,tokens\n'); + }); + }); + + suite('dailyUsageToJson', () => { + test('round-trips through JSON.parse with expected shape', () => { + const parsed = JSON.parse(dailyUsageToJson(sample)); + assert.strictEqual(parsed.days, 3); + assert.strictEqual(parsed.daily.length, 3); + assert.deepStrictEqual(parsed.daily[0], { + date: '2026-07-05', + costUsd: 1.2346, + messages: 12, + tokens: 45000, + }); + assert.ok(!isNaN(new Date(parsed.exportedAt).getTime()), 'exportedAt is a valid date'); + }); + + test('totalCostUsd sums all days', () => { + const parsed = JSON.parse(dailyUsageToJson(sample)); + assert.strictEqual(parsed.totalCostUsd, 3.7346); + }); + + test('handles empty input', () => { + const parsed = JSON.parse(dailyUsageToJson([])); + assert.strictEqual(parsed.days, 0); + assert.strictEqual(parsed.totalCostUsd, 0); + assert.deepStrictEqual(parsed.daily, []); + }); + }); +}); From 7b15707eaf57c8c16d192b5d731b05af7cacf362 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 03:05:56 +0000 Subject: [PATCH 2/2] chore: retrigger CI (time-of-day test flake, fixed in #37) The ubuntu job failed on the pre-existing heatmap test flake that only reproduces between 02:00 and 03:00 local time; the fix ships in #37. Empty commit to re-run CI outside that window. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012ZLCLQeudf1LQBnBSyyr4f