ccusage 15.3.1 → 15.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,6 +62,11 @@ ccusage blocks --live # Real-time usage dashboard
62
62
  ccusage daily --since 20250525 --until 20250530
63
63
  ccusage daily --json # JSON output
64
64
  ccusage daily --breakdown # Per-model cost breakdown
65
+
66
+ # Project analysis
67
+ ccusage daily --instances # Group by project/instance
68
+ ccusage daily --project myproject # Filter to specific project
69
+ ccusage daily --instances --project myproject --json # Combined usage
65
70
  ```
66
71
 
67
72
  ## Features
@@ -83,6 +88,7 @@ ccusage daily --breakdown # Per-model cost breakdown
83
88
  - 🔄 **Cache Token Support**: Tracks and displays cache creation and cache read tokens separately
84
89
  - 🌐 **Offline Mode**: Use pre-cached pricing data without network connectivity with `--offline` (Claude models only)
85
90
  - 🔌 **MCP Integration**: Built-in Model Context Protocol server for integration with other tools
91
+ - 🏗️ **Multi-Instance Support**: Group usage by project with `--instances` flag and filter by specific projects
86
92
  - 🚀 **Ultra-Small Bundle**: Unlike other CLI tools, we pay extreme attention to bundle size - incredibly small even without minification!
87
93
 
88
94
  ## Documentation
@@ -1,4 +1,4 @@
1
- import { DailyUsage, MonthlyUsage, SessionUsage } from "./data-loader-a9CiVyT5.js";
1
+ import { DailyUsage, MonthlyUsage, SessionUsage } from "./data-loader-CgvyDaQD.js";
2
2
  import "./pricing-fetcher-B3SvKOod.js";
3
3
 
4
4
  //#region src/_token-utils.d.ts
@@ -1,7 +1,7 @@
1
- import { CLAUDE_CONFIG_DIR_ENV, CLAUDE_PROJECTS_DIR_NAME, DEFAULT_CLAUDE_CODE_PATH, DEFAULT_CLAUDE_CONFIG_PATH, DEFAULT_RECENT_DAYS, PricingFetcher, USAGE_DATA_GLOB_PATTERN, USER_HOME_DIR, __commonJSMin, __require, __toESM, isFailure, isPromise, require_usingCtx } from "./pricing-fetcher-DpoTR8Md.js";
1
+ import { CLAUDE_CONFIG_DIR_ENV, CLAUDE_PROJECTS_DIR_NAME, DEFAULT_CLAUDE_CODE_PATH, DEFAULT_CLAUDE_CONFIG_PATH, DEFAULT_RECENT_DAYS, PricingFetcher, USAGE_DATA_GLOB_PATTERN, USER_HOME_DIR, __commonJSMin, __require, __toESM, isFailure, isPromise, require_usingCtx } from "./pricing-fetcher-B2yEbqCO.js";
2
2
  import { getTotalTokens } from "./_token-utils-WjkbrjKv.js";
3
3
  import { activityDateSchema, arrayType, booleanType, createDailyDate, createMonthlyDate, createProjectPath, createSessionId, dailyDateSchema, isoTimestampSchema, messageIdSchema, modelNameSchema, monthlyDateSchema, numberType, objectType, projectPathSchema, requestIdSchema, sessionIdSchema, stringType, versionSchema } from "./_types-BHFM59hI.js";
4
- import { logger } from "./logger-D7tlrIfv.js";
4
+ import { logger } from "./logger-CZzaUNvP.js";
5
5
  import a, { readFile } from "node:fs/promises";
6
6
  import path, { posix } from "node:path";
7
7
  import process$1 from "node:process";
@@ -251,7 +251,7 @@ const unwrap = (...args) => {
251
251
  const apply = (r) => {
252
252
  if (isFailure(r)) {
253
253
  if (hasDefault$1) return defaultValue$1;
254
- throw new Error(String(r.error));
254
+ throw r.error;
255
255
  }
256
256
  return r.value;
257
257
  };
@@ -263,7 +263,7 @@ const unwrap = (...args) => {
263
263
  const apply = (r) => {
264
264
  if (isFailure(r)) {
265
265
  if (hasDefault) return defaultValue;
266
- throw new Error(String(r.error));
266
+ throw r.error;
267
267
  }
268
268
  return r.value;
269
269
  };
@@ -3327,6 +3327,19 @@ function getClaudePaths() {
3327
3327
  return paths;
3328
3328
  }
3329
3329
  /**
3330
+ * Extract project name from Claude JSONL file path
3331
+ * @param jsonlPath - Absolute path to JSONL file
3332
+ * @returns Project name extracted from path, or "unknown" if malformed
3333
+ */
3334
+ function extractProjectFromPath(jsonlPath) {
3335
+ const normalizedPath = jsonlPath.replace(/[/\\]/g, path.sep);
3336
+ const segments = normalizedPath.split(path.sep);
3337
+ const projectsIndex = segments.findIndex((segment) => segment === CLAUDE_PROJECTS_DIR_NAME);
3338
+ if (projectsIndex === -1 || projectsIndex + 1 >= segments.length) return "unknown";
3339
+ const projectName = segments[projectsIndex + 1];
3340
+ return projectName != null && projectName.trim() !== "" ? projectName : "unknown";
3341
+ }
3342
+ /**
3330
3343
  * Zod schema for validating Claude usage data from JSONL files
3331
3344
  */
3332
3345
  const usageDataSchema = objectType({
@@ -3369,7 +3382,8 @@ const dailyUsageSchema = objectType({
3369
3382
  cacheReadTokens: numberType(),
3370
3383
  totalCost: numberType(),
3371
3384
  modelsUsed: arrayType(modelNameSchema),
3372
- modelBreakdowns: arrayType(modelBreakdownSchema)
3385
+ modelBreakdowns: arrayType(modelBreakdownSchema),
3386
+ project: stringType().optional()
3373
3387
  });
3374
3388
  /**
3375
3389
  * Zod schema for session-based usage aggregation data
@@ -3398,7 +3412,8 @@ const monthlyUsageSchema = objectType({
3398
3412
  cacheReadTokens: numberType(),
3399
3413
  totalCost: numberType(),
3400
3414
  modelsUsed: arrayType(modelNameSchema),
3401
- modelBreakdowns: arrayType(modelBreakdownSchema)
3415
+ modelBreakdowns: arrayType(modelBreakdownSchema),
3416
+ project: stringType().optional()
3402
3417
  });
3403
3418
  /**
3404
3419
  * Aggregates token counts and costs by model name
@@ -3499,6 +3514,16 @@ function filterByDateRange(items, getDate, since, until) {
3499
3514
  });
3500
3515
  }
3501
3516
  /**
3517
+ * Filters items by project name
3518
+ */
3519
+ function filterByProject(items, getProject, projectFilter) {
3520
+ if (projectFilter == null) return items;
3521
+ return items.filter((item) => {
3522
+ const projectName = getProject(item);
3523
+ return projectName === projectFilter;
3524
+ });
3525
+ }
3526
+ /**
3502
3527
  * Checks if an entry is a duplicate based on hash
3503
3528
  */
3504
3529
  function isDuplicateEntry(uniqueHash, processedHashes) {
@@ -3518,16 +3543,26 @@ function extractUniqueModels(entries, getModel) {
3518
3543
  return uniq(entries.map(getModel).filter((m$1) => m$1 != null && m$1 !== "<synthetic>"));
3519
3544
  }
3520
3545
  /**
3546
+ * Shared method for formatting dates with proper timezone handling
3547
+ * @param dateStr - Input date string
3548
+ * @param twoLine - Whether to format as two lines (true) or single line (false)
3549
+ * @returns Formatted date string
3550
+ */
3551
+ function formatDateInternal(dateStr, twoLine) {
3552
+ const date = new Date(dateStr);
3553
+ const hasTimezone = /Z|[+-]\d{2}:\d{2}/.test(dateStr);
3554
+ const year = hasTimezone ? date.getUTCFullYear() : date.getFullYear();
3555
+ const month = String(hasTimezone ? date.getUTCMonth() + 1 : date.getMonth() + 1).padStart(2, "0");
3556
+ const day = String(hasTimezone ? date.getUTCDate() : date.getDate()).padStart(2, "0");
3557
+ return twoLine ? `${year}\n${month}-${day}` : `${year}-${month}-${day}`;
3558
+ }
3559
+ /**
3521
3560
  * Formats a date string to YYYY-MM-DD format
3522
3561
  * @param dateStr - Input date string
3523
3562
  * @returns Formatted date string in YYYY-MM-DD format
3524
3563
  */
3525
3564
  function formatDate(dateStr) {
3526
- const date = new Date(dateStr);
3527
- const year = date.getFullYear();
3528
- const month = String(date.getMonth() + 1).padStart(2, "0");
3529
- const day = String(date.getDate()).padStart(2, "0");
3530
- return `${year}-${month}-${day}`;
3565
+ return formatDateInternal(dateStr, false);
3531
3566
  }
3532
3567
  /**
3533
3568
  * Formats a date string to compact format with year on first line and month-day on second
@@ -3535,11 +3570,7 @@ function formatDate(dateStr) {
3535
3570
  * @returns Formatted date string with newline separator (YYYY\nMM-DD)
3536
3571
  */
3537
3572
  function formatDateCompact(dateStr) {
3538
- const date = new Date(dateStr);
3539
- const year = date.getFullYear();
3540
- const month = String(date.getMonth() + 1).padStart(2, "0");
3541
- const day = String(date.getDate()).padStart(2, "0");
3542
- return `${year}\n${month}-${day}`;
3573
+ return formatDateInternal(dateStr, true);
3543
3574
  }
3544
3575
  /**
3545
3576
  * Generic function to sort items by date based on sort order
@@ -3647,6 +3678,25 @@ function getUsageLimitResetTime(data) {
3647
3678
  return resetTime;
3648
3679
  }
3649
3680
  /**
3681
+ * Glob files from multiple Claude paths in parallel
3682
+ * @param claudePaths - Array of Claude base paths
3683
+ * @returns Array of file paths with their base directories
3684
+ */
3685
+ async function globUsageFiles(claudePaths) {
3686
+ const filePromises = claudePaths.map(async (claudePath) => {
3687
+ const claudeDir = path.join(claudePath, CLAUDE_PROJECTS_DIR_NAME);
3688
+ const files = await glob([USAGE_DATA_GLOB_PATTERN], {
3689
+ cwd: claudeDir,
3690
+ absolute: true
3691
+ }).catch(() => []);
3692
+ return files.map((file) => ({
3693
+ file,
3694
+ baseDir: claudeDir
3695
+ }));
3696
+ });
3697
+ return (await Promise.all(filePromises)).flat();
3698
+ }
3699
+ /**
3650
3700
  * Loads and aggregates Claude usage data by day
3651
3701
  * Processes all JSONL files in the Claude projects directory and groups usage by date
3652
3702
  * @param options - Optional configuration for loading and filtering data
@@ -3656,17 +3706,11 @@ async function loadDailyUsageData(options) {
3656
3706
  try {
3657
3707
  var _usingCtx = (0, import_usingCtx.default)();
3658
3708
  const claudePaths = toArray(options?.claudePath ?? getClaudePaths());
3659
- const allFiles = [];
3660
- for (const claudePath of claudePaths) {
3661
- const claudeDir = path.join(claudePath, CLAUDE_PROJECTS_DIR_NAME);
3662
- const files = await glob([USAGE_DATA_GLOB_PATTERN], {
3663
- cwd: claudeDir,
3664
- absolute: true
3665
- });
3666
- allFiles.push(...files);
3667
- }
3668
- if (allFiles.length === 0) return [];
3669
- const sortedFiles = await sortFilesByTimestamp(allFiles);
3709
+ const allFiles = await globUsageFiles(claudePaths);
3710
+ const fileList = allFiles.map((f$1) => f$1.file);
3711
+ if (fileList.length === 0) return [];
3712
+ const projectFilteredFiles = filterByProject(fileList, (filePath) => extractProjectFromPath(filePath), options?.project);
3713
+ const sortedFiles = await sortFilesByTimestamp(projectFilteredFiles);
3670
3714
  const mode = options?.mode ?? "auto";
3671
3715
  const fetcher = _usingCtx.u(mode === "display" ? null : new PricingFetcher(options?.offline));
3672
3716
  const processedHashes = /* @__PURE__ */ new Set();
@@ -3684,17 +3728,24 @@ async function loadDailyUsageData(options) {
3684
3728
  markAsProcessed(uniqueHash, processedHashes);
3685
3729
  const date = formatDate(data.timestamp);
3686
3730
  const cost = fetcher != null ? await calculateCostForEntry(data, mode, fetcher) : data.costUSD ?? 0;
3731
+ const project = extractProjectFromPath(file);
3687
3732
  allEntries.push({
3688
3733
  data,
3689
3734
  date,
3690
3735
  cost,
3691
- model: data.message.model
3736
+ model: data.message.model,
3737
+ project
3692
3738
  });
3693
3739
  } catch {}
3694
3740
  }
3695
- const groupedByDate = groupBy(allEntries, (entry) => entry.date);
3696
- const results = Object.entries(groupedByDate).map(([date, entries]) => {
3741
+ const needsProjectGrouping = options?.groupByProject === true || options?.project != null;
3742
+ const groupingKey = needsProjectGrouping ? (entry) => `${entry.date}\x00${entry.project}` : (entry) => entry.date;
3743
+ const groupedData = groupBy(allEntries, groupingKey);
3744
+ const results = Object.entries(groupedData).map(([groupKey, entries]) => {
3697
3745
  if (entries == null) return void 0;
3746
+ const parts = groupKey.split("\0");
3747
+ const date = parts[0] ?? groupKey;
3748
+ const project = parts.length > 1 ? parts[1] : void 0;
3698
3749
  const modelAggregates = aggregateByModel(entries, (entry) => entry.model, (entry) => entry.data.message.usage, (entry) => entry.cost);
3699
3750
  const modelBreakdowns = createModelBreakdowns(modelAggregates);
3700
3751
  const totals = calculateTotals(entries, (entry) => entry.data.message.usage, (entry) => entry.cost);
@@ -3703,11 +3754,13 @@ async function loadDailyUsageData(options) {
3703
3754
  date: createDailyDate(date),
3704
3755
  ...totals,
3705
3756
  modelsUsed,
3706
- modelBreakdowns
3757
+ modelBreakdowns,
3758
+ ...project != null && { project }
3707
3759
  };
3708
3760
  }).filter((item) => item != null);
3709
- const filtered = filterByDateRange(results, (item) => item.date, options?.since, options?.until);
3710
- return sortByDate(filtered, (item) => item.date, options?.order);
3761
+ const dateFiltered = filterByDateRange(results, (item) => item.date, options?.since, options?.until);
3762
+ const finalFiltered = filterByProject(dateFiltered, (item) => item.project, options?.project);
3763
+ return sortByDate(finalFiltered, (item) => item.date, options?.order);
3711
3764
  } catch (_) {
3712
3765
  _usingCtx.e = _;
3713
3766
  } finally {
@@ -3724,21 +3777,11 @@ async function loadSessionData(options) {
3724
3777
  try {
3725
3778
  var _usingCtx3 = (0, import_usingCtx.default)();
3726
3779
  const claudePaths = toArray(options?.claudePath ?? getClaudePaths());
3727
- const filesWithBase = [];
3728
- for (const claudePath of claudePaths) {
3729
- const claudeDir = path.join(claudePath, CLAUDE_PROJECTS_DIR_NAME);
3730
- const files = await glob([USAGE_DATA_GLOB_PATTERN], {
3731
- cwd: claudeDir,
3732
- absolute: true
3733
- });
3734
- for (const file of files) filesWithBase.push({
3735
- file,
3736
- baseDir: claudeDir
3737
- });
3738
- }
3780
+ const filesWithBase = await globUsageFiles(claudePaths);
3739
3781
  if (filesWithBase.length === 0) return [];
3740
- const fileToBaseMap = new Map(filesWithBase.map((f$1) => [f$1.file, f$1.baseDir]));
3741
- const sortedFilesWithBase = await sortFilesByTimestamp(filesWithBase.map((f$1) => f$1.file)).then((sortedFiles) => sortedFiles.map((file) => ({
3782
+ const projectFilteredWithBase = filterByProject(filesWithBase, (item) => extractProjectFromPath(item.file), options?.project);
3783
+ const fileToBaseMap = new Map(projectFilteredWithBase.map((f$1) => [f$1.file, f$1.baseDir]));
3784
+ const sortedFilesWithBase = await sortFilesByTimestamp(projectFilteredWithBase.map((f$1) => f$1.file)).then((sortedFiles) => sortedFiles.map((file) => ({
3742
3785
  file,
3743
3786
  baseDir: fileToBaseMap.get(file) ?? ""
3744
3787
  })));
@@ -3795,8 +3838,9 @@ async function loadSessionData(options) {
3795
3838
  modelBreakdowns
3796
3839
  };
3797
3840
  }).filter((item) => item != null);
3798
- const filtered = filterByDateRange(results, (item) => item.lastActivity, options?.since, options?.until);
3799
- return sortByDate(filtered, (item) => item.lastActivity, options?.order);
3841
+ const dateFiltered = filterByDateRange(results, (item) => item.lastActivity, options?.since, options?.until);
3842
+ const sessionFiltered = filterByProject(dateFiltered, (item) => item.projectPath, options?.project);
3843
+ return sortByDate(sessionFiltered, (item) => item.lastActivity, options?.order);
3800
3844
  } catch (_) {
3801
3845
  _usingCtx3.e = _;
3802
3846
  } finally {
@@ -3811,10 +3855,15 @@ async function loadSessionData(options) {
3811
3855
  */
3812
3856
  async function loadMonthlyUsageData(options) {
3813
3857
  const dailyData = await loadDailyUsageData(options);
3814
- const groupedByMonth = groupBy(dailyData, (data) => data.date.substring(0, 7));
3858
+ const needsProjectGrouping = options?.groupByProject === true || options?.project != null;
3859
+ const groupingKey = needsProjectGrouping ? (data) => `${data.date.substring(0, 7)}\x00${data.project ?? "unknown"}` : (data) => data.date.substring(0, 7);
3860
+ const groupedByMonth = groupBy(dailyData, groupingKey);
3815
3861
  const monthlyArray = [];
3816
- for (const [month, dailyEntries] of Object.entries(groupedByMonth)) {
3862
+ for (const [groupKey, dailyEntries] of Object.entries(groupedByMonth)) {
3817
3863
  if (dailyEntries == null) continue;
3864
+ const parts = groupKey.split("\0");
3865
+ const month = parts[0] ?? groupKey;
3866
+ const project = parts.length > 1 ? parts[1] : void 0;
3818
3867
  const allBreakdowns = dailyEntries.flatMap((daily) => daily.modelBreakdowns);
3819
3868
  const modelAggregates = aggregateModelBreakdowns(allBreakdowns);
3820
3869
  const modelBreakdowns = createModelBreakdowns(modelAggregates);
@@ -3840,7 +3889,8 @@ async function loadMonthlyUsageData(options) {
3840
3889
  cacheReadTokens: totalCacheReadTokens,
3841
3890
  totalCost,
3842
3891
  modelsUsed: uniq(models),
3843
- modelBreakdowns
3892
+ modelBreakdowns,
3893
+ ...project != null && { project }
3844
3894
  };
3845
3895
  monthlyArray.push(monthlyUsage);
3846
3896
  }
@@ -3866,7 +3916,8 @@ async function loadSessionBlockData(options) {
3866
3916
  allFiles.push(...files);
3867
3917
  }
3868
3918
  if (allFiles.length === 0) return [];
3869
- const sortedFiles = await sortFilesByTimestamp(allFiles);
3919
+ const blocksFilteredFiles = filterByProject(allFiles, (filePath) => extractProjectFromPath(filePath), options?.project);
3920
+ const sortedFiles = await sortFilesByTimestamp(blocksFilteredFiles);
3870
3921
  const mode = options?.mode ?? "auto";
3871
3922
  const fetcher = _usingCtx4.u(mode === "display" ? null : new PricingFetcher(options?.offline));
3872
3923
  const processedHashes = /* @__PURE__ */ new Set();
@@ -3902,17 +3953,17 @@ async function loadSessionBlockData(options) {
3902
3953
  }
3903
3954
  }
3904
3955
  const blocks = identifySessionBlocks(allEntries, options?.sessionDurationHours);
3905
- const filtered = options?.since != null && options.since !== "" || options?.until != null && options.until !== "" ? blocks.filter((block) => {
3956
+ const dateFiltered = options?.since != null && options.since !== "" || options?.until != null && options.until !== "" ? blocks.filter((block) => {
3906
3957
  const blockDateStr = formatDate(block.startTime.toISOString()).replace(/-/g, "");
3907
3958
  if (options.since != null && options.since !== "" && blockDateStr < options.since) return false;
3908
3959
  if (options.until != null && options.until !== "" && blockDateStr > options.until) return false;
3909
3960
  return true;
3910
3961
  }) : blocks;
3911
- return sortByDate(filtered, (block) => block.startTime, options?.order);
3962
+ return sortByDate(dateFiltered, (block) => block.startTime, options?.order);
3912
3963
  } catch (_) {
3913
3964
  _usingCtx4.e = _;
3914
3965
  } finally {
3915
3966
  _usingCtx4.d();
3916
3967
  }
3917
3968
  }
3918
- export { DEFAULT_SESSION_DURATION_HOURS, calculateBurnRate, calculateCostForEntry, createUniqueHash, dailyUsageSchema, filterRecentBlocks, formatDate, formatDateCompact, getClaudePaths, getEarliestTimestamp, getUsageLimitResetTime, glob, identifySessionBlocks, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData, modelBreakdownSchema, monthlyUsageSchema, projectBlockUsage, sessionUsageSchema, sortFilesByTimestamp, uniq, unwrap, usageDataSchema };
3969
+ export { DEFAULT_SESSION_DURATION_HOURS, calculateBurnRate, calculateCostForEntry, createUniqueHash, dailyUsageSchema, extractProjectFromPath, filterRecentBlocks, formatDate, formatDateCompact, getClaudePaths, getEarliestTimestamp, getUsageLimitResetTime, glob, globUsageFiles, identifySessionBlocks, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData, modelBreakdownSchema, monthlyUsageSchema, projectBlockUsage, sessionUsageSchema, sortFilesByTimestamp, uniq, unwrap, usageDataSchema };
@@ -55,6 +55,12 @@ type SessionBlock = {
55
55
  * @returns Array of valid Claude data directory paths
56
56
  */
57
57
  declare function getClaudePaths(): string[];
58
+ /**
59
+ * Extract project name from Claude JSONL file path
60
+ * @param jsonlPath - Absolute path to JSONL file
61
+ * @returns Project name extracted from path, or "unknown" if malformed
62
+ */
63
+ declare function extractProjectFromPath(jsonlPath: string): string;
58
64
  /**
59
65
  * Zod schema for validating Claude usage data from JSONL files
60
66
  */
@@ -220,6 +226,7 @@ declare const dailyUsageSchema: z.ZodObject<{
220
226
  cacheReadTokens: number;
221
227
  cost: number;
222
228
  }>, "many">;
229
+ project: z.ZodOptional<z.ZodString>;
223
230
  }, "strip", z.ZodTypeAny, {
224
231
  date: string & z.BRAND<"DailyDate">;
225
232
  inputTokens: number;
@@ -236,6 +243,7 @@ declare const dailyUsageSchema: z.ZodObject<{
236
243
  cacheReadTokens: number;
237
244
  cost: number;
238
245
  }[];
246
+ project?: string | undefined;
239
247
  }, {
240
248
  date: string;
241
249
  inputTokens: number;
@@ -252,6 +260,7 @@ declare const dailyUsageSchema: z.ZodObject<{
252
260
  cacheReadTokens: number;
253
261
  cost: number;
254
262
  }[];
263
+ project?: string | undefined;
255
264
  }>;
256
265
  /**
257
266
  * Type definition for daily usage aggregation
@@ -369,6 +378,7 @@ declare const monthlyUsageSchema: z.ZodObject<{
369
378
  cacheReadTokens: number;
370
379
  cost: number;
371
380
  }>, "many">;
381
+ project: z.ZodOptional<z.ZodString>;
372
382
  }, "strip", z.ZodTypeAny, {
373
383
  month: string & z.BRAND<"MonthlyDate">;
374
384
  inputTokens: number;
@@ -385,6 +395,7 @@ declare const monthlyUsageSchema: z.ZodObject<{
385
395
  cacheReadTokens: number;
386
396
  cost: number;
387
397
  }[];
398
+ project?: string | undefined;
388
399
  }, {
389
400
  month: string;
390
401
  inputTokens: number;
@@ -401,6 +412,7 @@ declare const monthlyUsageSchema: z.ZodObject<{
401
412
  cacheReadTokens: number;
402
413
  cost: number;
403
414
  }[];
415
+ project?: string | undefined;
404
416
  }>;
405
417
  /**
406
418
  * Type definition for monthly usage aggregation
@@ -446,6 +458,19 @@ declare function calculateCostForEntry(data: UsageData, mode: CostMode, fetcher:
446
458
  * @returns Usage limit expiration date
447
459
  */
448
460
  declare function getUsageLimitResetTime(data: UsageData): Date | null;
461
+ /**
462
+ * Result of glob operation with base directory information
463
+ */
464
+ type GlobResult = {
465
+ file: string;
466
+ baseDir: string;
467
+ };
468
+ /**
469
+ * Glob files from multiple Claude paths in parallel
470
+ * @param claudePaths - Array of Claude base paths
471
+ * @returns Array of file paths with their base directories
472
+ */
473
+ declare function globUsageFiles(claudePaths: string[]): Promise<GlobResult[]>;
449
474
  /**
450
475
  * Date range filter for limiting usage data by date
451
476
  */
@@ -462,6 +487,8 @@ type LoadOptions = {
462
487
  order?: SortOrder; // Sort order for dates
463
488
  offline?: boolean; // Use offline mode for pricing
464
489
  sessionDurationHours?: number; // Session block duration in hours
490
+ groupByProject?: boolean; // Group data by project instead of aggregating
491
+ project?: string; // Filter to specific project name
465
492
  } & DateFilter;
466
493
  /**
467
494
  * Loads and aggregates Claude usage data by day
@@ -492,4 +519,4 @@ declare function loadMonthlyUsageData(options?: LoadOptions): Promise<MonthlyUsa
492
519
  */
493
520
  declare function loadSessionBlockData(options?: LoadOptions): Promise<SessionBlock[]>;
494
521
  //#endregion
495
- export { DailyUsage, DateFilter, LoadOptions, ModelBreakdown, MonthlyUsage, SessionUsage, UsageData, calculateCostForEntry, createUniqueHash, dailyUsageSchema, formatDate, formatDateCompact, getClaudePaths, getEarliestTimestamp, getUsageLimitResetTime, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData, modelBreakdownSchema, monthlyUsageSchema, sessionUsageSchema, sortFilesByTimestamp, usageDataSchema };
522
+ export { DailyUsage, DateFilter, GlobResult, LoadOptions, ModelBreakdown, MonthlyUsage, SessionUsage, UsageData, calculateCostForEntry, createUniqueHash, dailyUsageSchema, extractProjectFromPath, formatDate, formatDateCompact, getClaudePaths, getEarliestTimestamp, getUsageLimitResetTime, globUsageFiles, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData, modelBreakdownSchema, monthlyUsageSchema, sessionUsageSchema, sortFilesByTimestamp, usageDataSchema };
@@ -1,3 +1,3 @@
1
- import { DailyUsage, DateFilter, LoadOptions, ModelBreakdown, MonthlyUsage, SessionUsage, UsageData, calculateCostForEntry, createUniqueHash, dailyUsageSchema, formatDate, formatDateCompact, getClaudePaths, getEarliestTimestamp, getUsageLimitResetTime, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData, modelBreakdownSchema, monthlyUsageSchema, sessionUsageSchema, sortFilesByTimestamp, usageDataSchema } from "./data-loader-a9CiVyT5.js";
1
+ import { DailyUsage, DateFilter, GlobResult, LoadOptions, ModelBreakdown, MonthlyUsage, SessionUsage, UsageData, calculateCostForEntry, createUniqueHash, dailyUsageSchema, extractProjectFromPath, formatDate, formatDateCompact, getClaudePaths, getEarliestTimestamp, getUsageLimitResetTime, globUsageFiles, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData, modelBreakdownSchema, monthlyUsageSchema, sessionUsageSchema, sortFilesByTimestamp, usageDataSchema } from "./data-loader-CgvyDaQD.js";
2
2
  import "./pricing-fetcher-B3SvKOod.js";
3
- export { DailyUsage, DateFilter, LoadOptions, ModelBreakdown, MonthlyUsage, SessionUsage, UsageData, calculateCostForEntry, createUniqueHash, dailyUsageSchema, formatDate, formatDateCompact, getClaudePaths, getEarliestTimestamp, getUsageLimitResetTime, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData, modelBreakdownSchema, monthlyUsageSchema, sessionUsageSchema, sortFilesByTimestamp, usageDataSchema };
3
+ export { DailyUsage, DateFilter, GlobResult, LoadOptions, ModelBreakdown, MonthlyUsage, SessionUsage, UsageData, calculateCostForEntry, createUniqueHash, dailyUsageSchema, extractProjectFromPath, formatDate, formatDateCompact, getClaudePaths, getEarliestTimestamp, getUsageLimitResetTime, globUsageFiles, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData, modelBreakdownSchema, monthlyUsageSchema, sessionUsageSchema, sortFilesByTimestamp, usageDataSchema };
@@ -1,6 +1,6 @@
1
- import "./pricing-fetcher-DpoTR8Md.js";
1
+ import "./pricing-fetcher-B2yEbqCO.js";
2
2
  import "./_token-utils-WjkbrjKv.js";
3
3
  import "./_types-BHFM59hI.js";
4
- import { calculateCostForEntry, createUniqueHash, dailyUsageSchema, formatDate, formatDateCompact, getClaudePaths, getEarliestTimestamp, getUsageLimitResetTime, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData, modelBreakdownSchema, monthlyUsageSchema, sessionUsageSchema, sortFilesByTimestamp, usageDataSchema } from "./data-loader-DqK3z1AK.js";
5
- import "./logger-D7tlrIfv.js";
6
- export { calculateCostForEntry, createUniqueHash, dailyUsageSchema, formatDate, formatDateCompact, getClaudePaths, getEarliestTimestamp, getUsageLimitResetTime, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData, modelBreakdownSchema, monthlyUsageSchema, sessionUsageSchema, sortFilesByTimestamp, usageDataSchema };
4
+ import { calculateCostForEntry, createUniqueHash, dailyUsageSchema, extractProjectFromPath, formatDate, formatDateCompact, getClaudePaths, getEarliestTimestamp, getUsageLimitResetTime, globUsageFiles, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData, modelBreakdownSchema, monthlyUsageSchema, sessionUsageSchema, sortFilesByTimestamp, usageDataSchema } from "./data-loader-BVwVxx4T.js";
5
+ import "./logger-CZzaUNvP.js";
6
+ export { calculateCostForEntry, createUniqueHash, dailyUsageSchema, extractProjectFromPath, formatDate, formatDateCompact, getClaudePaths, getEarliestTimestamp, getUsageLimitResetTime, globUsageFiles, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData, modelBreakdownSchema, monthlyUsageSchema, sessionUsageSchema, sortFilesByTimestamp, usageDataSchema };
@@ -1,6 +1,6 @@
1
- import { CLAUDE_PROJECTS_DIR_NAME, DEBUG_MATCH_THRESHOLD_PERCENT, PricingFetcher, USAGE_DATA_GLOB_PATTERN, __toESM, isFailure, require_usingCtx, try_ } from "./pricing-fetcher-DpoTR8Md.js";
2
- import { getClaudePaths, glob, unwrap, usageDataSchema } from "./data-loader-DqK3z1AK.js";
3
- import { logger } from "./logger-D7tlrIfv.js";
1
+ import { CLAUDE_PROJECTS_DIR_NAME, DEBUG_MATCH_THRESHOLD_PERCENT, PricingFetcher, USAGE_DATA_GLOB_PATTERN, __toESM, isFailure, require_usingCtx, try_ } from "./pricing-fetcher-B2yEbqCO.js";
2
+ import { getClaudePaths, glob, unwrap, usageDataSchema } from "./data-loader-BVwVxx4T.js";
3
+ import { logger } from "./logger-CZzaUNvP.js";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  var import_usingCtx = __toESM(require_usingCtx(), 1);
package/dist/debug.js CHANGED
@@ -1,7 +1,7 @@
1
- import "./pricing-fetcher-DpoTR8Md.js";
1
+ import "./pricing-fetcher-B2yEbqCO.js";
2
2
  import "./_token-utils-WjkbrjKv.js";
3
3
  import "./_types-BHFM59hI.js";
4
- import "./data-loader-DqK3z1AK.js";
5
- import "./logger-D7tlrIfv.js";
6
- import { detectMismatches, printMismatchReport } from "./debug-Cby_QhQQ.js";
4
+ import "./data-loader-BVwVxx4T.js";
5
+ import "./logger-CZzaUNvP.js";
6
+ import { detectMismatches, printMismatchReport } from "./debug-DtmspKbf.js";
7
7
  export { detectMismatches, printMismatchReport };
package/dist/index.js CHANGED
@@ -1,14 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { BLOCKS_COMPACT_WIDTH_THRESHOLD, BLOCKS_DEFAULT_TERMINAL_WIDTH, BLOCKS_WARNING_THRESHOLD, BURN_RATE_THRESHOLDS, CLAUDE_PROJECTS_DIR_NAME, DEFAULT_RECENT_DAYS, DEFAULT_REFRESH_INTERVAL_SECONDS, MAX_REFRESH_INTERVAL_SECONDS, MCP_DEFAULT_PORT, MIN_REFRESH_INTERVAL_SECONDS, MIN_RENDER_INTERVAL_MS, PricingFetcher, USAGE_DATA_GLOB_PATTERN, __commonJSMin, __require, __toESM, isFailure, require_usingCtx, try_ } from "./pricing-fetcher-DpoTR8Md.js";
2
+ import { BLOCKS_COMPACT_WIDTH_THRESHOLD, BLOCKS_DEFAULT_TERMINAL_WIDTH, BLOCKS_WARNING_THRESHOLD, BURN_RATE_THRESHOLDS, DEFAULT_RECENT_DAYS, DEFAULT_REFRESH_INTERVAL_SECONDS, MAX_REFRESH_INTERVAL_SECONDS, MCP_DEFAULT_PORT, MIN_REFRESH_INTERVAL_SECONDS, MIN_RENDER_INTERVAL_MS, PROJECT_ALIASES_ENV, PricingFetcher, __commonJSMin, __require, __toESM, isFailure, require_usingCtx, try_ } from "./pricing-fetcher-B2yEbqCO.js";
3
3
  import { getTotalTokens } from "./_token-utils-WjkbrjKv.js";
4
4
  import { CostModes, SortOrders, filterDateSchema } from "./_types-BHFM59hI.js";
5
5
  import { calculateTotals, createTotalsObject } from "./calculate-cost-BDqO4yWA.js";
6
- import { DEFAULT_SESSION_DURATION_HOURS, calculateBurnRate, calculateCostForEntry, createUniqueHash, filterRecentBlocks, formatDateCompact, getClaudePaths, getEarliestTimestamp, getUsageLimitResetTime, glob, identifySessionBlocks, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData, projectBlockUsage, sortFilesByTimestamp, uniq, usageDataSchema } from "./data-loader-DqK3z1AK.js";
7
- import { description, log, logger, name, version } from "./logger-D7tlrIfv.js";
8
- import { detectMismatches, printMismatchReport } from "./debug-Cby_QhQQ.js";
9
- import { createMcpHttpApp, createMcpServer, startMcpServerStdio } from "./mcp-CFT0dcvs.js";
6
+ import { DEFAULT_SESSION_DURATION_HOURS, calculateBurnRate, calculateCostForEntry, createUniqueHash, filterRecentBlocks, formatDateCompact, getClaudePaths, getEarliestTimestamp, getUsageLimitResetTime, globUsageFiles, identifySessionBlocks, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData, projectBlockUsage, sortFilesByTimestamp, uniq, usageDataSchema } from "./data-loader-BVwVxx4T.js";
7
+ import { description, log, logger, name, version } from "./logger-CZzaUNvP.js";
8
+ import { detectMismatches, printMismatchReport } from "./debug-DtmspKbf.js";
9
+ import { createMcpHttpApp, createMcpServer, startMcpServerStdio } from "./mcp-DRNUcHZe.js";
10
10
  import { readFile } from "node:fs/promises";
11
- import path from "node:path";
12
11
  import process$1 from "node:process";
13
12
  import { createServer } from "node:http";
14
13
  import { Http2ServerRequest } from "node:http2";
@@ -3205,14 +3204,11 @@ var LiveMonitor = class {
3205
3204
  * Only reads new or modified files since last check
3206
3205
  */
3207
3206
  async getActiveBlock() {
3208
- const claudeDir = path.join(this.config.claudePath, CLAUDE_PROJECTS_DIR_NAME);
3209
- const files = await glob([USAGE_DATA_GLOB_PATTERN], {
3210
- cwd: claudeDir,
3211
- absolute: true
3212
- });
3213
- if (files.length === 0) return null;
3207
+ const results = await globUsageFiles(this.config.claudePaths);
3208
+ const allFiles = results.map((r) => r.file);
3209
+ if (allFiles.length === 0) return null;
3214
3210
  const filesToRead = [];
3215
- for (const file of files) {
3211
+ for (const file of allFiles) {
3216
3212
  const timestamp = await getEarliestTimestamp(file);
3217
3213
  const lastTimestamp = this.lastFileTimestamps.get(file);
3218
3214
  if (timestamp != null && (lastTimestamp == null || timestamp.getTime() > lastTimestamp)) {
@@ -3745,7 +3741,7 @@ const DETAIL_COLUMN_WIDTHS = {
3745
3741
  function renderLiveDisplay(terminal, block, config) {
3746
3742
  const width = terminal.width;
3747
3743
  const now = /* @__PURE__ */ new Date();
3748
- const totalTokens = block.tokenCounts.inputTokens + block.tokenCounts.outputTokens;
3744
+ const totalTokens = getTotalTokens(block.tokenCounts);
3749
3745
  const elapsed = (now.getTime() - block.startTime.getTime()) / (1e3 * 60);
3750
3746
  const remaining = (block.endTime.getTime() - now.getTime()) / (1e3 * 60);
3751
3747
  if (width < 60) {
@@ -3952,7 +3948,7 @@ async function startLiveMonitoring(config) {
3952
3948
  terminal.clearScreen();
3953
3949
  terminal.hideCursor();
3954
3950
  const monitor = _usingCtx.u(new LiveMonitor({
3955
- claudePath: config.claudePath,
3951
+ claudePaths: config.claudePaths,
3956
3952
  sessionDurationHours: config.sessionDurationHours,
3957
3953
  mode: config.mode,
3958
3954
  order: config.order
@@ -4052,8 +4048,7 @@ function formatModels(models) {
4052
4048
  * @returns Parsed token limit or undefined if invalid
4053
4049
  */
4054
4050
  function parseTokenLimit(value, maxFromAll) {
4055
- if (value == null || value === "") return void 0;
4056
- if (value === "max") return maxFromAll > 0 ? maxFromAll : void 0;
4051
+ if (value == null || value === "" || value === "max") return maxFromAll > 0 ? maxFromAll : void 0;
4057
4052
  const limit = Number.parseInt(value, 10);
4058
4053
  return Number.isNaN(limit) ? void 0 : limit;
4059
4054
  }
@@ -4117,7 +4112,7 @@ const blocksCommand = define({
4117
4112
  process$1.exit(0);
4118
4113
  }
4119
4114
  let maxTokensFromAll = 0;
4120
- if (ctx.values.tokenLimit === "max") {
4115
+ if (ctx.values.tokenLimit === "max" || ctx.values.tokenLimit == null || ctx.values.tokenLimit === "") {
4121
4116
  for (const block of blocks) if (!(block.isGap ?? false) && !block.isActive) {
4122
4117
  const blockTokens = getTotalTokens(block.tokenCounts);
4123
4118
  if (blockTokens > maxTokensFromAll) maxTokensFromAll = blockTokens;
@@ -4151,7 +4146,7 @@ const blocksCommand = define({
4151
4146
  throw new Error("No valid Claude data directory found");
4152
4147
  }
4153
4148
  await startLiveMonitoring({
4154
- claudePath: paths[0],
4149
+ claudePaths: paths,
4155
4150
  tokenLimit: parseTokenLimit(tokenLimitValue, maxTokensFromAll),
4156
4151
  refreshInterval: refreshInterval * 1e3,
4157
4152
  sessionDurationHours: ctx.values.sessionLength,
@@ -4173,7 +4168,7 @@ const blocksCommand = define({
4173
4168
  isGap: block.isGap ?? false,
4174
4169
  entries: block.entries.length,
4175
4170
  tokenCounts: block.tokenCounts,
4176
- totalTokens: block.tokenCounts.inputTokens + block.tokenCounts.outputTokens,
4171
+ totalTokens: getTotalTokens(block.tokenCounts),
4177
4172
  costUSD: block.costUSD,
4178
4173
  models: block.models,
4179
4174
  burnRate,
@@ -4334,11 +4329,162 @@ const blocksCommand = define({
4334
4329
  }
4335
4330
  }
4336
4331
  });
4332
+ /**
4333
+ * Group daily usage data by project for JSON output
4334
+ */
4335
+ function groupByProject(dailyData) {
4336
+ const projects = {};
4337
+ for (const data of dailyData) {
4338
+ const projectName = data.project ?? "unknown";
4339
+ if (projects[projectName] == null) projects[projectName] = [];
4340
+ projects[projectName].push({
4341
+ date: data.date,
4342
+ inputTokens: data.inputTokens,
4343
+ outputTokens: data.outputTokens,
4344
+ cacheCreationTokens: data.cacheCreationTokens,
4345
+ cacheReadTokens: data.cacheReadTokens,
4346
+ totalTokens: getTotalTokens(data),
4347
+ totalCost: data.totalCost,
4348
+ modelsUsed: data.modelsUsed,
4349
+ modelBreakdowns: data.modelBreakdowns
4350
+ });
4351
+ }
4352
+ return projects;
4353
+ }
4354
+ /**
4355
+ * Group daily usage data by project for table display
4356
+ */
4357
+ function groupDataByProject(dailyData) {
4358
+ const projects = {};
4359
+ for (const data of dailyData) {
4360
+ const projectName = data.project ?? "unknown";
4361
+ if (projects[projectName] == null) projects[projectName] = [];
4362
+ projects[projectName].push(data);
4363
+ }
4364
+ return projects;
4365
+ }
4366
+ /**
4367
+ * Cache for parsed aliases to avoid repeated parsing
4368
+ */
4369
+ let aliasCache = null;
4370
+ /**
4371
+ * Parse project aliases from environment variable
4372
+ * @returns Map of raw project names to their aliases
4373
+ */
4374
+ function getProjectAliases() {
4375
+ if (aliasCache !== null) return aliasCache;
4376
+ aliasCache = /* @__PURE__ */ new Map();
4377
+ const aliasEnv = (process$1.env[PROJECT_ALIASES_ENV] ?? "").trim();
4378
+ if (aliasEnv === "") return aliasCache;
4379
+ const pairs = aliasEnv.split(",").map((pair) => pair.trim()).filter((pair) => pair !== "");
4380
+ for (const pair of pairs) {
4381
+ const parts = pair.split("=").map((s) => s.trim());
4382
+ const rawName = parts[0];
4383
+ const alias = parts[1];
4384
+ if (rawName != null && alias != null && rawName !== "" && alias !== "") aliasCache.set(rawName, alias);
4385
+ }
4386
+ return aliasCache;
4387
+ }
4388
+ /**
4389
+ * Extract meaningful project name from directory-style project paths
4390
+ * Uses improved heuristics to handle complex project structures
4391
+ *
4392
+ * @param projectName - Raw project name from directory path
4393
+ * @returns Cleaned and formatted project name
4394
+ *
4395
+ * @example
4396
+ * ```typescript
4397
+ * // Basic cleanup
4398
+ * parseProjectName('-Users-phaedrus-Development-ccusage')
4399
+ * // → 'ccusage'
4400
+ *
4401
+ * // Complex project with feature branch
4402
+ * parseProjectName('-Users-phaedrus-Development-adminifi-edugakko-api--feature-ticket-002-configure-dependabot')
4403
+ * // → 'configure-dependabot'
4404
+ *
4405
+ * // Handle unknown projects
4406
+ * parseProjectName('unknown')
4407
+ * // → 'Unknown Project'
4408
+ * ```
4409
+ */
4410
+ function parseProjectName(projectName) {
4411
+ if (projectName === "unknown" || projectName === "") return "Unknown Project";
4412
+ let cleaned = projectName;
4413
+ if (cleaned.match(/^[A-Z]:\\Users\\|^\\Users\\/) != null) {
4414
+ const segments = cleaned.split("\\");
4415
+ const userIndex = segments.findIndex((seg) => seg === "Users");
4416
+ if (userIndex !== -1 && userIndex + 3 < segments.length) cleaned = segments.slice(userIndex + 3).join("-");
4417
+ }
4418
+ if (cleaned.startsWith("-Users-") || cleaned.startsWith("/Users/")) {
4419
+ const separator = cleaned.startsWith("-Users-") ? "-" : "/";
4420
+ const segments = cleaned.split(separator).filter((s) => s.length > 0);
4421
+ const userIndex = segments.findIndex((seg) => seg === "Users");
4422
+ if (userIndex !== -1 && userIndex + 3 < segments.length) cleaned = segments.slice(userIndex + 3).join("-");
4423
+ }
4424
+ if (cleaned === projectName) cleaned = projectName.replace(/^[/\\-]+|[/\\-]+$/g, "");
4425
+ if (cleaned.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i) != null) {
4426
+ const parts = cleaned.split("-");
4427
+ if (parts.length >= 5) cleaned = parts.slice(-2).join("-");
4428
+ }
4429
+ if (cleaned.includes("--")) {
4430
+ const parts = cleaned.split("--");
4431
+ if (parts.length >= 2 && parts[0] != null) cleaned = parts[0];
4432
+ }
4433
+ if (cleaned.includes("-") && cleaned.length > 20) {
4434
+ const segments = cleaned.split("-");
4435
+ const meaningfulSegments = segments.filter((seg) => seg.length > 2 && seg.match(/^(?:dev|development|feat|feature|fix|bug|test|staging|prod|production|main|master|branch)$/i) == null);
4436
+ if (meaningfulSegments.length >= 2) {
4437
+ const lastSegments = meaningfulSegments.slice(-2);
4438
+ if (lastSegments.join("-").length >= 6) cleaned = lastSegments.join("-");
4439
+ else if (meaningfulSegments.length >= 3) cleaned = meaningfulSegments.slice(-3).join("-");
4440
+ }
4441
+ }
4442
+ cleaned = cleaned.replace(/^[/\\-]+|[/\\-]+$/g, "");
4443
+ return cleaned !== "" ? cleaned : projectName !== "" ? projectName : "Unknown Project";
4444
+ }
4445
+ /**
4446
+ * Format project name for display with custom alias support
4447
+ *
4448
+ * @param projectName - Raw project name from directory path
4449
+ * @returns User-friendly project name with alias support
4450
+ *
4451
+ * @example
4452
+ * ```typescript
4453
+ * // Without aliases
4454
+ * formatProjectName('-Users-phaedrus-Development-ccusage')
4455
+ * // → 'ccusage'
4456
+ *
4457
+ * // With alias (when CCUSAGE_PROJECT_ALIASES="ccusage=Usage Tracker")
4458
+ * formatProjectName('-Users-phaedrus-Development-ccusage')
4459
+ * // → 'Usage Tracker'
4460
+ * ```
4461
+ */
4462
+ function formatProjectName(projectName) {
4463
+ const aliases = getProjectAliases();
4464
+ if (aliases.has(projectName)) return aliases.get(projectName);
4465
+ const parsed = parseProjectName(projectName);
4466
+ if (aliases.has(parsed)) return aliases.get(parsed);
4467
+ return parsed;
4468
+ }
4337
4469
  var import_picocolors$2 = __toESM(require_picocolors(), 1);
4338
4470
  const dailyCommand = define({
4339
4471
  name: "daily",
4340
4472
  description: "Show usage report grouped by date",
4341
4473
  ...sharedCommandConfig,
4474
+ args: {
4475
+ ...sharedCommandConfig.args,
4476
+ instances: {
4477
+ type: "boolean",
4478
+ short: "i",
4479
+ description: "Show usage breakdown by project/instance",
4480
+ default: false
4481
+ },
4482
+ project: {
4483
+ type: "string",
4484
+ short: "p",
4485
+ description: "Filter to specific project name"
4486
+ }
4487
+ },
4342
4488
  async run(ctx) {
4343
4489
  if (ctx.values.json) logger.level = 0;
4344
4490
  const dailyData = await loadDailyUsageData({
@@ -4346,7 +4492,9 @@ const dailyCommand = define({
4346
4492
  until: ctx.values.until,
4347
4493
  mode: ctx.values.mode,
4348
4494
  order: ctx.values.order,
4349
- offline: ctx.values.offline
4495
+ offline: ctx.values.offline,
4496
+ groupByProject: ctx.values.instances,
4497
+ project: ctx.values.project
4350
4498
  });
4351
4499
  if (dailyData.length === 0) {
4352
4500
  if (ctx.values.json) log(JSON.stringify([]));
@@ -4359,7 +4507,10 @@ const dailyCommand = define({
4359
4507
  printMismatchReport(mismatchStats, ctx.values.debugSamples);
4360
4508
  }
4361
4509
  if (ctx.values.json) {
4362
- const jsonOutput = {
4510
+ const jsonOutput = ctx.values.instances && dailyData.some((d) => d.project != null) ? {
4511
+ projects: groupByProject(dailyData),
4512
+ totals: createTotalsObject(totals)
4513
+ } : {
4363
4514
  daily: dailyData.map((data) => ({
4364
4515
  date: data.date,
4365
4516
  inputTokens: data.inputTokens,
@@ -4369,7 +4520,8 @@ const dailyCommand = define({
4369
4520
  totalTokens: getTotalTokens(data),
4370
4521
  totalCost: data.totalCost,
4371
4522
  modelsUsed: data.modelsUsed,
4372
- modelBreakdowns: data.modelBreakdowns
4523
+ modelBreakdowns: data.modelBreakdowns,
4524
+ ...data.project != null && { project: data.project }
4373
4525
  })),
4374
4526
  totals: createTotalsObject(totals)
4375
4527
  };
@@ -4415,7 +4567,46 @@ const dailyCommand = define({
4415
4567
  ],
4416
4568
  compactThreshold: 100
4417
4569
  });
4418
- for (const data of dailyData) {
4570
+ if (ctx.values.instances && dailyData.some((d) => d.project != null)) {
4571
+ const projectGroups = groupDataByProject(dailyData);
4572
+ let isFirstProject = true;
4573
+ for (const [projectName, projectData] of Object.entries(projectGroups)) {
4574
+ if (!isFirstProject) table.push([
4575
+ "",
4576
+ "",
4577
+ "",
4578
+ "",
4579
+ "",
4580
+ "",
4581
+ "",
4582
+ ""
4583
+ ]);
4584
+ table.push([
4585
+ import_picocolors$2.default.cyan(`Project: ${formatProjectName(projectName)}`),
4586
+ "",
4587
+ "",
4588
+ "",
4589
+ "",
4590
+ "",
4591
+ "",
4592
+ ""
4593
+ ]);
4594
+ for (const data of projectData) {
4595
+ table.push([
4596
+ data.date,
4597
+ formatModelsDisplayMultiline(data.modelsUsed),
4598
+ formatNumber(data.inputTokens),
4599
+ formatNumber(data.outputTokens),
4600
+ formatNumber(data.cacheCreationTokens),
4601
+ formatNumber(data.cacheReadTokens),
4602
+ formatNumber(getTotalTokens(data)),
4603
+ formatCurrency(data.totalCost)
4604
+ ]);
4605
+ if (ctx.values.breakdown) pushBreakdownRows(table, data.modelBreakdowns);
4606
+ }
4607
+ isFirstProject = false;
4608
+ }
4609
+ } else for (const data of dailyData) {
4419
4610
  table.push([
4420
4611
  data.date,
4421
4612
  formatModelsDisplayMultiline(data.modelsUsed),
@@ -4474,6 +4665,7 @@ var Request = class extends GlobalRequest {
4474
4665
  super(input, options);
4475
4666
  }
4476
4667
  };
4668
+ var wrapBodyStream = Symbol("wrapBodyStream");
4477
4669
  var newRequestFromIncoming = (method, url, incoming, abortController) => {
4478
4670
  const headerRecord = [];
4479
4671
  const rawHeaders = incoming.rawHeaders;
@@ -4498,7 +4690,19 @@ var newRequestFromIncoming = (method, url, incoming, abortController) => {
4498
4690
  controller.enqueue(incoming.rawBody);
4499
4691
  controller.close();
4500
4692
  } });
4501
- else init$1.body = Readable.toWeb(incoming);
4693
+ else if (incoming[wrapBodyStream]) {
4694
+ let reader;
4695
+ init$1.body = new ReadableStream({ async pull(controller) {
4696
+ try {
4697
+ reader ||= Readable.toWeb(incoming).getReader();
4698
+ const { done, value } = await reader.read();
4699
+ if (done) controller.close();
4700
+ else controller.enqueue(value);
4701
+ } catch (error) {
4702
+ controller.error(error);
4703
+ }
4704
+ } });
4705
+ } else init$1.body = Readable.toWeb(incoming);
4502
4706
  return new Request(url, init$1);
4503
4707
  };
4504
4708
  var getRequestCache = Symbol("getRequestCache");
@@ -4709,6 +4913,7 @@ global.fetch = (info$1, init$1) => {
4709
4913
  };
4710
4914
  return webFetch(info$1, init$1);
4711
4915
  };
4916
+ var outgoingEnded = Symbol("outgoingEnded");
4712
4917
  var regBuffer = /^no$/i;
4713
4918
  var regContentType = /^(application\/json\b|text\/(?!event-stream\b))/i;
4714
4919
  var handleRequestError = () => new Response(null, { status: 400 });
@@ -4737,8 +4942,9 @@ var responseViaCache = async (res, outgoing) => {
4737
4942
  else if (body instanceof Blob) outgoing.end(new Uint8Array(await body.arrayBuffer()));
4738
4943
  else {
4739
4944
  flushHeaders(outgoing);
4740
- return writeFromReadableStream(body, outgoing)?.catch((e) => handleResponseError(e, outgoing));
4945
+ await writeFromReadableStream(body, outgoing)?.catch((e) => handleResponseError(e, outgoing));
4741
4946
  }
4947
+ outgoing[outgoingEnded]?.();
4742
4948
  };
4743
4949
  var responseViaResponseObject = async (res, outgoing, options = {}) => {
4744
4950
  if (res instanceof Promise) if (options.errorHandler) try {
@@ -4767,8 +4973,10 @@ var responseViaResponseObject = async (res, outgoing, options = {}) => {
4767
4973
  outgoing.writeHead(res.status, resHeaderRecord);
4768
4974
  outgoing.end();
4769
4975
  }
4976
+ outgoing[outgoingEnded]?.();
4770
4977
  };
4771
4978
  var getRequestListener = (fetchCallback, options = {}) => {
4979
+ const autoCleanupIncoming = options.autoCleanupIncoming ?? true;
4772
4980
  if (options.overrideGlobalObjects !== false && global.Request !== Request) {
4773
4981
  Object.defineProperty(global, "Request", { value: Request });
4774
4982
  Object.defineProperty(global, "Response", { value: Response2 });
@@ -4777,11 +4985,32 @@ var getRequestListener = (fetchCallback, options = {}) => {
4777
4985
  let res, req;
4778
4986
  try {
4779
4987
  req = newRequest(incoming, options.hostname);
4988
+ let incomingEnded = !autoCleanupIncoming || incoming.method === "GET" || incoming.method === "HEAD";
4989
+ if (!incomingEnded) {
4990
+ incoming[wrapBodyStream] = true;
4991
+ incoming.on("end", () => {
4992
+ incomingEnded = true;
4993
+ });
4994
+ if (incoming instanceof Http2ServerRequest) outgoing[outgoingEnded] = () => {
4995
+ if (!incomingEnded) setTimeout(() => {
4996
+ if (!incomingEnded) setTimeout(() => {
4997
+ incoming.destroy();
4998
+ outgoing.destroy();
4999
+ });
5000
+ });
5001
+ };
5002
+ }
4780
5003
  outgoing.on("close", () => {
4781
5004
  const abortController = req[abortControllerKey];
4782
- if (!abortController) return;
4783
- if (incoming.errored) req[abortControllerKey].abort(incoming.errored.toString());
4784
- else if (!outgoing.writableFinished) req[abortControllerKey].abort("Client connection prematurely closed.");
5005
+ if (abortController) {
5006
+ if (incoming.errored) req[abortControllerKey].abort(incoming.errored.toString());
5007
+ else if (!outgoing.writableFinished) req[abortControllerKey].abort("Client connection prematurely closed.");
5008
+ }
5009
+ if (!incomingEnded) setTimeout(() => {
5010
+ if (!incomingEnded) setTimeout(() => {
5011
+ incoming.destroy();
5012
+ });
5013
+ });
4785
5014
  });
4786
5015
  res = fetchCallback(req, {
4787
5016
  incoming,
@@ -4807,7 +5036,8 @@ var createAdaptorServer = (options) => {
4807
5036
  const fetchCallback = options.fetch;
4808
5037
  const requestListener = getRequestListener(fetchCallback, {
4809
5038
  hostname: options.hostname,
4810
- overrideGlobalObjects: options.overrideGlobalObjects
5039
+ overrideGlobalObjects: options.overrideGlobalObjects,
5040
+ autoCleanupIncoming: options.autoCleanupIncoming
4811
5041
  });
4812
5042
  const createServer$1 = options.createServer || createServer;
4813
5043
  const server = createServer$1(options.serverOptions || {}, requestListener);
@@ -5038,7 +5268,8 @@ const sessionCommand = define({
5038
5268
  totalCost: data.totalCost,
5039
5269
  lastActivity: data.lastActivity,
5040
5270
  modelsUsed: data.modelsUsed,
5041
- modelBreakdowns: data.modelBreakdowns
5271
+ modelBreakdowns: data.modelBreakdowns,
5272
+ projectPath: data.projectPath
5042
5273
  })),
5043
5274
  totals: createTotalsObject(totals)
5044
5275
  };
@@ -951,7 +951,7 @@ function _getDefaultLogLevel() {
951
951
  }
952
952
  const consola = createConsola$1();
953
953
  var name = "ccusage";
954
- var version = "15.3.1";
954
+ var version = "15.5.0";
955
955
  var description = "Usage analysis tool for Claude Code";
956
956
  /**
957
957
  * Application logger instance with package name tag
package/dist/logger.js CHANGED
@@ -1,2 +1,2 @@
1
- import { log, logger } from "./logger-D7tlrIfv.js";
1
+ import { log, logger } from "./logger-CZzaUNvP.js";
2
2
  export { log, logger };
@@ -1,9 +1,9 @@
1
- import { __commonJSMin, __toESM, require_usingCtx } from "./pricing-fetcher-DpoTR8Md.js";
1
+ import { __commonJSMin, __toESM, require_usingCtx } from "./pricing-fetcher-B2yEbqCO.js";
2
2
  import { getTotalTokens } from "./_token-utils-WjkbrjKv.js";
3
3
  import { ZodFirstPartyTypeKind, ZodOptional, ZodType, arrayType, booleanType, discriminatedUnionType, enumType, filterDateSchema, literalType, numberType, objectType, optionalType, recordType, stringType, unionType, unknownType } from "./_types-BHFM59hI.js";
4
4
  import { calculateTotals, createTotalsObject } from "./calculate-cost-BDqO4yWA.js";
5
- import { getClaudePaths, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData } from "./data-loader-DqK3z1AK.js";
6
- import { name, version } from "./logger-D7tlrIfv.js";
5
+ import { getClaudePaths, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData } from "./data-loader-BVwVxx4T.js";
6
+ import { name, version } from "./logger-CZzaUNvP.js";
7
7
  import process from "node:process";
8
8
  const LATEST_PROTOCOL_VERSION = "2025-06-18";
9
9
  const SUPPORTED_PROTOCOL_VERSIONS = [
@@ -606,7 +606,7 @@ const ElicitRequestSchema = RequestSchema.extend({
606
606
  const ElicitResultSchema = ResultSchema.extend({
607
607
  action: enumType([
608
608
  "accept",
609
- "reject",
609
+ "decline",
610
610
  "cancel"
611
611
  ]),
612
612
  content: optionalType(recordType(stringType(), unknownType()))
@@ -1054,7 +1054,7 @@ var HonoRequest = class {
1054
1054
  return bodyCache[key] = raw$1[key]();
1055
1055
  };
1056
1056
  json() {
1057
- return this.#cachedBody("json");
1057
+ return this.#cachedBody("text").then((text) => JSON.parse(text));
1058
1058
  }
1059
1059
  text() {
1060
1060
  return this.#cachedBody("text");
@@ -8561,10 +8561,6 @@ var McpServer = class {
8561
8561
  return registeredResourceTemplate;
8562
8562
  }
8563
8563
  }
8564
- /**
8565
- * Registers a resource with a config object and callback.
8566
- * For static resources, use a URI string. For dynamic resources, use a ResourceTemplate.
8567
- */
8568
8564
  registerResource(name$1, uriOrTemplate, config, readCallback) {
8569
8565
  if (typeof uriOrTemplate === "string") {
8570
8566
  if (this._registeredResources[uriOrTemplate]) throw new Error(`Resource ${uriOrTemplate} is already registered`);
@@ -8766,7 +8762,10 @@ var McpServer = class {
8766
8762
  if (this.isConnected()) this.server.sendPromptListChanged();
8767
8763
  }
8768
8764
  };
8769
- const EMPTY_OBJECT_JSON_SCHEMA = { type: "object" };
8765
+ const EMPTY_OBJECT_JSON_SCHEMA = {
8766
+ type: "object",
8767
+ properties: {}
8768
+ };
8770
8769
  function isZodRawShape(obj) {
8771
8770
  if (typeof obj !== "object" || obj === null) return false;
8772
8771
  const isEmptyObject = Object.keys(obj).length === 0;
package/dist/mcp.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { LoadOptions } from "./data-loader-a9CiVyT5.js";
1
+ import { LoadOptions } from "./data-loader-CgvyDaQD.js";
2
2
  import "./pricing-fetcher-B3SvKOod.js";
3
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { Hono } from "hono/tiny";
package/dist/mcp.js CHANGED
@@ -1,8 +1,8 @@
1
- import "./pricing-fetcher-DpoTR8Md.js";
1
+ import "./pricing-fetcher-B2yEbqCO.js";
2
2
  import "./_token-utils-WjkbrjKv.js";
3
3
  import "./_types-BHFM59hI.js";
4
4
  import "./calculate-cost-BDqO4yWA.js";
5
- import "./data-loader-DqK3z1AK.js";
6
- import "./logger-D7tlrIfv.js";
7
- import { createMcpHttpApp, createMcpServer, startMcpServerStdio } from "./mcp-CFT0dcvs.js";
5
+ import "./data-loader-BVwVxx4T.js";
6
+ import "./logger-CZzaUNvP.js";
7
+ import { createMcpHttpApp, createMcpServer, startMcpServerStdio } from "./mcp-DRNUcHZe.js";
8
8
  export { createMcpHttpApp, createMcpServer, startMcpServerStdio };
@@ -1,5 +1,5 @@
1
1
  import { modelPricingSchema } from "./_types-BHFM59hI.js";
2
- import { logger } from "./logger-D7tlrIfv.js";
2
+ import { logger } from "./logger-CZzaUNvP.js";
3
3
  import { createRequire } from "node:module";
4
4
  import path from "node:path";
5
5
  import F, { homedir } from "node:os";
@@ -47,6 +47,7 @@ const andThrough = (fn) => (result) => {
47
47
  };
48
48
  return isPromise(result) ? result.then(apply) : apply(result);
49
49
  };
50
+ const isSuccess = (result) => "Success" === result.type;
50
51
  const succeed = (...args) => {
51
52
  const value = args[0];
52
53
  if (void 0 === value) return { type: "Success" };
@@ -71,7 +72,6 @@ const fail = (...args) => {
71
72
  error
72
73
  };
73
74
  };
74
- const isSuccess = (result) => "Success" === result.type;
75
75
  const inspect = (fn) => (result) => {
76
76
  const apply = (r) => {
77
77
  if (isSuccess(r)) fn(r.value);
@@ -192,6 +192,28 @@ const DEFAULT_CLAUDE_CONFIG_PATH = `${XDG_CONFIG_DIR}/claude`;
192
192
  */
193
193
  const CLAUDE_CONFIG_DIR_ENV = "CLAUDE_CONFIG_DIR";
194
194
  /**
195
+ * Environment variable for custom project name aliases
196
+ *
197
+ * Allows users to configure readable display names for cryptic or long project directory names.
198
+ * This is particularly useful for:
199
+ * - UUID-based project directories generated by Claude Code
200
+ * - Long path-based project names that are hard to read in tables
201
+ * - Custom branding of project names for organizational consistency
202
+ *
203
+ * Format: "raw-name=alias,another-name=other-alias"
204
+ * Example: "a2cd99ed-a586-4fe4-8f59-b0026409ec09=My Project,long-project-name=Short"
205
+ *
206
+ * Environment variable approach is used because:
207
+ * - No config file needed - simple and lightweight configuration
208
+ * - Works in any shell/environment without file management
209
+ * - Easy to set temporarily for specific invocations
210
+ * - Follows Unix conventions for tool configuration
211
+ * - Doesn't require file system permissions or config directory setup
212
+ *
213
+ * Used to override display names for project directories in all output formats.
214
+ */
215
+ const PROJECT_ALIASES_ENV = "CCUSAGE_PROJECT_ALIASES";
216
+ /**
195
217
  * Claude projects directory name within the data directory
196
218
  * Contains subdirectories for each project with usage data
197
219
  */
@@ -324,14 +346,6 @@ var PricingFetcher = class {
324
346
  loadOfflinePricing = try_({
325
347
  try: async () => {
326
348
  const pricing = new Map(Object.entries({
327
- "claude-instant-1": {
328
- "input_cost_per_token": 163e-8,
329
- "output_cost_per_token": 551e-8
330
- },
331
- "claude-instant-1.2": {
332
- "input_cost_per_token": 163e-9,
333
- "output_cost_per_token": 551e-9
334
- },
335
349
  "claude-2": {
336
350
  "input_cost_per_token": 8e-6,
337
351
  "output_cost_per_token": 24e-6
@@ -546,4 +560,4 @@ var PricingFetcher = class {
546
560
  return cost;
547
561
  }
548
562
  };
549
- export { BLOCKS_COMPACT_WIDTH_THRESHOLD, BLOCKS_DEFAULT_TERMINAL_WIDTH, BLOCKS_WARNING_THRESHOLD, BURN_RATE_THRESHOLDS, CLAUDE_CONFIG_DIR_ENV, CLAUDE_PROJECTS_DIR_NAME, DEBUG_MATCH_THRESHOLD_PERCENT, DEFAULT_CLAUDE_CODE_PATH, DEFAULT_CLAUDE_CONFIG_PATH, DEFAULT_RECENT_DAYS, DEFAULT_REFRESH_INTERVAL_SECONDS, MAX_REFRESH_INTERVAL_SECONDS, MCP_DEFAULT_PORT, MIN_REFRESH_INTERVAL_SECONDS, MIN_RENDER_INTERVAL_MS, PricingFetcher, USAGE_DATA_GLOB_PATTERN, USER_HOME_DIR, __commonJSMin, __require, __toESM, isFailure, isPromise, require_usingCtx, try_ };
563
+ export { BLOCKS_COMPACT_WIDTH_THRESHOLD, BLOCKS_DEFAULT_TERMINAL_WIDTH, BLOCKS_WARNING_THRESHOLD, BURN_RATE_THRESHOLDS, CLAUDE_CONFIG_DIR_ENV, CLAUDE_PROJECTS_DIR_NAME, DEBUG_MATCH_THRESHOLD_PERCENT, DEFAULT_CLAUDE_CODE_PATH, DEFAULT_CLAUDE_CONFIG_PATH, DEFAULT_RECENT_DAYS, DEFAULT_REFRESH_INTERVAL_SECONDS, MAX_REFRESH_INTERVAL_SECONDS, MCP_DEFAULT_PORT, MIN_REFRESH_INTERVAL_SECONDS, MIN_RENDER_INTERVAL_MS, PROJECT_ALIASES_ENV, PricingFetcher, USAGE_DATA_GLOB_PATTERN, USER_HOME_DIR, __commonJSMin, __require, __toESM, isFailure, isPromise, require_usingCtx, try_ };
@@ -1,4 +1,4 @@
1
- import { PricingFetcher } from "./pricing-fetcher-DpoTR8Md.js";
1
+ import { PricingFetcher } from "./pricing-fetcher-B2yEbqCO.js";
2
2
  import "./_types-BHFM59hI.js";
3
- import "./logger-D7tlrIfv.js";
3
+ import "./logger-CZzaUNvP.js";
4
4
  export { PricingFetcher };
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "ccusage",
3
- "type": "module",
4
- "version": "15.3.1",
3
+ "version": "15.5.0",
5
4
  "description": "Usage analysis tool for Claude Code",
6
- "author": "ryoppippi",
7
- "license": "MIT",
8
- "funding": "https://github.com/ryoppippi/ccusage?sponsor=1",
9
5
  "homepage": "https://github.com/ryoppippi/ccusage#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/ryoppippi/ccusage/issues"
8
+ },
10
9
  "repository": {
11
10
  "type": "git",
12
11
  "url": "git+https://github.com/ryoppippi/ccusage.git"
13
12
  },
14
- "bugs": {
15
- "url": "https://github.com/ryoppippi/ccusage/issues"
16
- },
13
+ "funding": "https://github.com/ryoppippi/ccusage?sponsor=1",
14
+ "license": "MIT",
15
+ "author": "ryoppippi",
16
+ "type": "module",
17
17
  "exports": {
18
18
  ".": "./dist/index.js",
19
19
  "./calculate-cost": "./dist/calculate-cost.js",
@@ -32,6 +32,6 @@
32
32
  "dist"
33
33
  ],
34
34
  "engines": {
35
- "node": ">=20.19.3"
35
+ "node": ">=20.19.4"
36
36
  }
37
37
  }