ccusage 15.10.0 → 16.1.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/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
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, WEEK_DAYS, __commonJSMin, __require, __toESM, inspectError, isFailure, isSuccess, map, pipe, require_usingCtx, succeed, try_ } from "./pricing-fetcher-D3tIkxxO.js";
2
+ import { BLOCKS_COMPACT_WIDTH_THRESHOLD, BLOCKS_DEFAULT_TERMINAL_WIDTH, BLOCKS_WARNING_THRESHOLD, BURN_RATE_THRESHOLDS, CONFIG_FILE_NAME, DEFAULT_RECENT_DAYS, DEFAULT_REFRESH_INTERVAL_SECONDS, MAX_REFRESH_INTERVAL_SECONDS, MCP_DEFAULT_PORT, MIN_REFRESH_INTERVAL_SECONDS, MIN_RENDER_INTERVAL_MS, PricingFetcher, WEEK_DAYS, __commonJSMin, __require, __toESM, inspect, inspectError, isFailure, isSuccess, map, pipe, require_usingCtx, succeed, try_ } from "./pricing-fetcher-5FVKt1XA.js";
3
3
  import { getTotalTokens } from "./_token-utils-WjkbrjKv.js";
4
4
  import { CostModes, SortOrders, filterDateSchema, statuslineHookJsonSchema } from "./_types-BbEk8t2a.js";
5
5
  import { calculateTotals, createTotalsObject } from "./calculate-cost-BDqO4yWA.js";
6
- import { DEFAULT_SESSION_DURATION_HOURS, calculateBurnRate, calculateContextTokens, calculateCostForEntry, createUniqueHash, filterRecentBlocks, formatDateCompact, getClaudePaths, getContextUsageThresholds, getEarliestTimestamp, getUsageLimitResetTime, globUsageFiles, identifySessionBlocks, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData, loadSessionUsageById, loadWeeklyUsageData, projectBlockUsage, sortFilesByTimestamp, uniq, unwrap, usageDataSchema } from "./data-loader-Dhwqb-FE.js";
7
- import { description, log, logger, name, version } from "./logger-yNFB24CE.js";
8
- import { detectMismatches, printMismatchReport } from "./debug-DseOsUbb.js";
9
- import { createMcpHttpApp, createMcpServer, startMcpServerStdio } from "./mcp-Cfnsg0lJ.js";
6
+ import { DEFAULT_SESSION_DURATION_HOURS, calculateBurnRate, calculateContextTokens, calculateCostForEntry, createUniqueHash, filterRecentBlocks, formatDateCompact, getClaudePaths, getContextUsageThresholds, getEarliestTimestamp, getUsageLimitResetTime, globUsageFiles, identifySessionBlocks, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData, loadSessionUsageById, loadWeeklyUsageData, projectBlockUsage, sortFilesByTimestamp, toArray, uniq, unwrap, usageDataSchema } from "./data-loader-D9y22SrV.js";
7
+ import { description, log, logger, name, version } from "./logger-Cyk_YiBe.js";
8
+ import { detectMismatches, printMismatchReport } from "./debug-CSXph9NF.js";
9
+ import { createMcpHttpApp, createMcpServer, startMcpServerStdio } from "./mcp-DRjw50Q4.js";
10
10
  import a, { readFile, stat } from "node:fs/promises";
11
11
  import path, { join } from "node:path";
12
12
  import process$1 from "node:process";
@@ -817,7 +817,94 @@ var require_picocolors = /* @__PURE__ */ __commonJSMin(((exports, module) => {
817
817
  };
818
818
  };
819
819
  module.exports = createColors(), module.exports.createColors = createColors;
820
- })), import_picocolors$8 = /* @__PURE__ */ __toESM(require_picocolors(), 1);
820
+ })), import_picocolors$8 = /* @__PURE__ */ __toESM(require_picocolors(), 1), import_usingCtx$3 = /* @__PURE__ */ __toESM(require_usingCtx(), 1);
821
+ function extractExplicitArgs(tokens) {
822
+ const explicit = {};
823
+ for (const token of tokens) if (typeof token === "object" && token !== null) {
824
+ const t = token;
825
+ if (t.kind === "option" && typeof t.name === "string") explicit[t.name] = true;
826
+ }
827
+ return explicit;
828
+ }
829
+ function getConfigSearchPaths() {
830
+ const claudeConfigDirs = [join(process$1.cwd(), ".ccusage"), ...toArray(getClaudePaths())];
831
+ return claudeConfigDirs.map((dir) => join(dir, CONFIG_FILE_NAME));
832
+ }
833
+ function validateConfigJson(data) {
834
+ if (typeof data !== "object" || data === null) return false;
835
+ const config = data;
836
+ return !(config.$schema != null && typeof config.$schema !== "string" || config.defaults != null && (typeof config.defaults !== "object" || config.defaults === null) || config.commands != null && (typeof config.commands !== "object" || config.commands === null));
837
+ }
838
+ function loadConfigFile(filePath, debug$4 = false) {
839
+ if (!existsSync(filePath)) {
840
+ if (debug$4) logger.info(` • Checking: ${filePath} (not found)`);
841
+ return void 0;
842
+ }
843
+ const parseConfigFileResult = pipe(try_({
844
+ try: () => {
845
+ const content = readFileSync(filePath, "utf-8"), data = JSON.parse(content);
846
+ if (!validateConfigJson(data)) throw new Error("Invalid configuration structure");
847
+ return data.source = filePath, data;
848
+ },
849
+ catch: (error) => error
850
+ })(), inspect(() => {
851
+ if (logger.debug(`Parsed configuration file: ${filePath}`), debug$4) logger.info(` • Checking: ${filePath} (found ✓)`);
852
+ }), inspectError((error) => {
853
+ const errorMessage = error instanceof Error ? error.message : String(error);
854
+ if (logger.warn(`Error parsing configuration file at ${filePath}: ${errorMessage}`), debug$4) logger.info(` • Checking: ${filePath} (error: ${errorMessage})`);
855
+ }), unwrap(void 0));
856
+ return parseConfigFileResult;
857
+ }
858
+ function loadConfig(configPath, debug$4 = false) {
859
+ if (debug$4) logger.info("Debug mode enabled - showing config loading details\n");
860
+ if (configPath != null) {
861
+ if (debug$4) logger.info("Using specified config file:"), logger.info(` • Path: ${configPath}`);
862
+ const config = loadConfigFile(configPath, debug$4);
863
+ if (config == null) logger.warn(`Configuration file not found or invalid: ${configPath}`);
864
+ else if (debug$4) logger.info(""), logger.info(`Loaded config from: ${configPath}`), logger.info(` • Schema: ${config.$schema ?? "none"}`), logger.info(` • Has defaults: ${config.defaults != null ? "yes" : "no"}${config.defaults != null ? ` (${Object.keys(config.defaults).length} options)` : ""}`), logger.info(` • Has command configs: ${config.commands != null ? "yes" : "no"}${config.commands != null ? ` (${Object.keys(config.commands).join(", ")})` : ""}`);
865
+ return config;
866
+ }
867
+ if (debug$4) logger.info("Searching for config files:");
868
+ for (const searchPath of getConfigSearchPaths()) {
869
+ const config = loadConfigFile(searchPath, debug$4);
870
+ if (config != null) {
871
+ if (debug$4) logger.info(""), logger.info(`Loaded config from: ${searchPath}`), logger.info(` • Schema: ${config.$schema ?? "none"}`), logger.info(` • Has defaults: ${config.defaults != null ? "yes" : "no"}${config.defaults != null ? ` (${Object.keys(config.defaults).length} options)` : ""}`), logger.info(` • Has command configs: ${config.commands != null ? "yes" : "no"}${config.commands != null ? ` (${Object.keys(config.commands).join(", ")})` : ""}`);
872
+ return config;
873
+ }
874
+ }
875
+ if (logger.debug("No valid configuration file found"), debug$4) logger.info(""), logger.info("No valid configuration file found");
876
+ return void 0;
877
+ }
878
+ function mergeConfigWithArgs(ctx, config, debug$4 = false) {
879
+ if (config == null) {
880
+ if (debug$4) logger.info(""), logger.info(`No config file loaded, using CLI args only for '${ctx.name ?? "unknown"}' command`);
881
+ return ctx.values;
882
+ }
883
+ const merged = {}, commandName = ctx.name, sources = {};
884
+ if (config.defaults != null) for (const [key, value$1] of Object.entries(config.defaults)) merged[key] = value$1, sources[key] = "defaults";
885
+ if (commandName != null && config.commands?.[commandName] != null) for (const [key, value$1] of Object.entries(config.commands[commandName])) merged[key] = value$1, sources[key] = "command config";
886
+ const explicit = extractExplicitArgs(ctx.tokens);
887
+ for (const [key, value$1] of Object.entries(ctx.values)) if (value$1 != null && explicit[key] === true) merged[key] = value$1, sources[key] = "CLI";
888
+ if (logger.debug(`Merged config for ${commandName ?? "unknown"}:`, merged), debug$4) {
889
+ logger.info(""), logger.info(`Merging options for '${commandName ?? "unknown"}' command:`);
890
+ const bySource = {
891
+ "defaults": [],
892
+ "command config": [],
893
+ "CLI": []
894
+ };
895
+ for (const [key, source] of Object.entries(sources)) if (bySource[source] != null) bySource[source].push(`${key}=${JSON.stringify(merged[key])}`);
896
+ if (bySource.defaults.length > 0) logger.info(` • From defaults: ${bySource.defaults.join(", ")}`);
897
+ if (bySource["command config"].length > 0) logger.info(` • From command config: ${bySource["command config"].join(", ")}`);
898
+ if (bySource.CLI.length > 0) logger.info(` • From CLI args: ${bySource.CLI.join(", ")}`);
899
+ logger.info(" • Final merged options: {");
900
+ for (const [key, value$1] of Object.entries(merged)) {
901
+ const source = sources[key] ?? "unknown";
902
+ logger.info(` ${key}: ${JSON.stringify(value$1)} (from ${source}),`);
903
+ }
904
+ logger.info(" }");
905
+ }
906
+ return merged;
907
+ }
821
908
  const getContext = (raw) => ({
822
909
  start: process$1.hrtime.bigint(),
823
910
  command: raw.map((part) => getCommandPart(stripVTControlCharacters(part))).join(" "),
@@ -1077,6 +1164,10 @@ const sharedArgs = {
1077
1164
  short: "q",
1078
1165
  description: "Process JSON output with jq command (requires jq binary, implies --json)"
1079
1166
  },
1167
+ config: {
1168
+ type: "string",
1169
+ description: "Path to configuration file (default: auto-discovery)"
1170
+ },
1080
1171
  compact: {
1081
1172
  type: "boolean",
1082
1173
  description: "Force compact mode for narrow displays (better for screenshots)",
@@ -3072,7 +3163,7 @@ const blocksCommand = define({
3072
3163
  },
3073
3164
  toKebab: true,
3074
3165
  async run(ctx) {
3075
- const useJson = ctx.values.json || ctx.values.jq != null;
3166
+ const config = loadConfig(ctx.values.config, ctx.values.debug), mergedOptions = mergeConfigWithArgs(ctx, config, ctx.values.debug), useJson = mergedOptions.json || mergedOptions.jq != null;
3076
3167
  if (useJson) logger.level = 0;
3077
3168
  if (ctx.values.sessionLength <= 0) logger.error("Session length must be a positive number"), process$1.exit(1);
3078
3169
  let blocks = await loadSessionBlockData({
@@ -3285,19 +3376,6 @@ function groupDataByProject(dailyData) {
3285
3376
  }
3286
3377
  return projects;
3287
3378
  }
3288
- let aliasCache = null;
3289
- function getProjectAliases() {
3290
- if (aliasCache !== null) return aliasCache;
3291
- aliasCache = /* @__PURE__ */ new Map();
3292
- const aliasEnv = (process$1.env[PROJECT_ALIASES_ENV] ?? "").trim();
3293
- if (aliasEnv === "") return aliasCache;
3294
- const pairs$1 = aliasEnv.split(",").map((pair$1) => pair$1.trim()).filter((pair$1) => pair$1 !== "");
3295
- for (const pair$1 of pairs$1) {
3296
- const parts = pair$1.split("=").map((s) => s.trim()), rawName = parts[0], alias = parts[1];
3297
- if (rawName != null && alias != null && rawName !== "" && alias !== "") aliasCache.set(rawName, alias);
3298
- }
3299
- return aliasCache;
3300
- }
3301
3379
  function parseProjectName(projectName) {
3302
3380
  if (projectName === "unknown" || projectName === "") return "Unknown Project";
3303
3381
  let cleaned = projectName;
@@ -3328,11 +3406,10 @@ function parseProjectName(projectName) {
3328
3406
  }
3329
3407
  return cleaned = cleaned.replace(/^[/\\-]+|[/\\-]+$/g, ""), cleaned !== "" ? cleaned : projectName !== "" ? projectName : "Unknown Project";
3330
3408
  }
3331
- function formatProjectName(projectName) {
3332
- const aliases = getProjectAliases();
3333
- if (aliases.has(projectName)) return aliases.get(projectName);
3409
+ function formatProjectName(projectName, aliases) {
3410
+ if (aliases != null && aliases.has(projectName)) return aliases.get(projectName);
3334
3411
  const parsed = parseProjectName(projectName);
3335
- return aliases.has(parsed) ? aliases.get(parsed) : parsed;
3412
+ return aliases != null && aliases.has(parsed) ? aliases.get(parsed) : parsed;
3336
3413
  }
3337
3414
  var import_picocolors$4 = /* @__PURE__ */ __toESM(require_picocolors(), 1);
3338
3415
  const dailyCommand = define({
@@ -3351,21 +3428,29 @@ const dailyCommand = define({
3351
3428
  type: "string",
3352
3429
  short: "p",
3353
3430
  description: "Filter to specific project name"
3431
+ },
3432
+ projectAliases: {
3433
+ type: "string",
3434
+ description: "Comma-separated project aliases (e.g., 'ccusage=Usage Tracker,myproject=My Project')",
3435
+ hidden: true
3354
3436
  }
3355
3437
  },
3356
3438
  async run(ctx) {
3357
- const useJson = ctx.values.json || ctx.values.jq != null;
3439
+ const config = loadConfig(ctx.values.config, ctx.values.debug), mergedOptions = mergeConfigWithArgs(ctx, config, ctx.values.debug);
3440
+ let projectAliases;
3441
+ if (mergedOptions.projectAliases != null && typeof mergedOptions.projectAliases === "string") {
3442
+ projectAliases = /* @__PURE__ */ new Map();
3443
+ const pairs$1 = mergedOptions.projectAliases.split(",").map((pair$1) => pair$1.trim()).filter((pair$1) => pair$1 !== "");
3444
+ for (const pair$1 of pairs$1) {
3445
+ const parts = pair$1.split("=").map((s) => s.trim()), rawName = parts[0], alias = parts[1];
3446
+ if (rawName != null && alias != null && rawName !== "" && alias !== "") projectAliases.set(rawName, alias);
3447
+ }
3448
+ }
3449
+ const useJson = Boolean(mergedOptions.json) || mergedOptions.jq != null;
3358
3450
  if (useJson) logger.level = 0;
3359
3451
  const dailyData = await loadDailyUsageData({
3360
- since: ctx.values.since,
3361
- until: ctx.values.until,
3362
- mode: ctx.values.mode,
3363
- order: ctx.values.order,
3364
- offline: ctx.values.offline,
3365
- groupByProject: ctx.values.instances,
3366
- project: ctx.values.project,
3367
- timezone: ctx.values.timezone,
3368
- locale: ctx.values.locale
3452
+ ...mergedOptions,
3453
+ groupByProject: mergedOptions.instances
3369
3454
  });
3370
3455
  if (dailyData.length === 0) {
3371
3456
  if (useJson) log(JSON.stringify([]));
@@ -3373,12 +3458,12 @@ const dailyCommand = define({
3373
3458
  process$1.exit(0);
3374
3459
  }
3375
3460
  const totals = calculateTotals(dailyData);
3376
- if (ctx.values.debug && !useJson) {
3461
+ if (mergedOptions.debug && !useJson) {
3377
3462
  const mismatchStats = await detectMismatches(void 0);
3378
- printMismatchReport(mismatchStats, ctx.values.debugSamples);
3463
+ printMismatchReport(mismatchStats, mergedOptions.debugSamples);
3379
3464
  }
3380
3465
  if (useJson) {
3381
- const jsonOutput = ctx.values.instances && dailyData.some((d) => d.project != null) ? {
3466
+ const jsonOutput = Boolean(mergedOptions.instances) && dailyData.some((d) => d.project != null) ? {
3382
3467
  projects: groupByProject(dailyData),
3383
3468
  totals: createTotalsObject(totals)
3384
3469
  } : {
@@ -3396,8 +3481,8 @@ const dailyCommand = define({
3396
3481
  })),
3397
3482
  totals: createTotalsObject(totals)
3398
3483
  };
3399
- if (ctx.values.jq != null) {
3400
- const jqResult = await processWithJq(jsonOutput, ctx.values.jq);
3484
+ if (mergedOptions.jq != null) {
3485
+ const jqResult = await processWithJq(jsonOutput, mergedOptions.jq);
3401
3486
  if (isFailure(jqResult)) logger.error(jqResult.error.message), process$1.exit(1);
3402
3487
  log(jqResult.value);
3403
3488
  } else log(JSON.stringify(jsonOutput, null, 2));
@@ -3425,7 +3510,7 @@ const dailyCommand = define({
3425
3510
  "right",
3426
3511
  "right"
3427
3512
  ],
3428
- dateFormatter: (dateStr) => formatDateCompact(dateStr, ctx.values.timezone, ctx.values.locale),
3513
+ dateFormatter: (dateStr) => formatDateCompact(dateStr, mergedOptions.timezone, mergedOptions.locale ?? void 0),
3429
3514
  compactHead: [
3430
3515
  "Date",
3431
3516
  "Models",
@@ -3443,7 +3528,7 @@ const dailyCommand = define({
3443
3528
  compactThreshold: 100,
3444
3529
  forceCompact: ctx.values.compact
3445
3530
  });
3446
- if (ctx.values.instances && dailyData.some((d) => d.project != null)) {
3531
+ if (Boolean(mergedOptions.instances) && dailyData.some((d) => d.project != null)) {
3447
3532
  const projectGroups = groupDataByProject(dailyData);
3448
3533
  let isFirstProject = true;
3449
3534
  for (const [projectName, projectData] of Object.entries(projectGroups)) {
@@ -3458,7 +3543,7 @@ const dailyCommand = define({
3458
3543
  ""
3459
3544
  ]);
3460
3545
  table.push([
3461
- import_picocolors$4.default.cyan(`Project: ${formatProjectName(projectName)}`),
3546
+ import_picocolors$4.default.cyan(`Project: ${formatProjectName(projectName, projectAliases)}`),
3462
3547
  "",
3463
3548
  "",
3464
3549
  "",
@@ -3476,7 +3561,7 @@ const dailyCommand = define({
3476
3561
  formatNumber(data.cacheReadTokens),
3477
3562
  formatNumber(getTotalTokens(data)),
3478
3563
  formatCurrency(data.totalCost)
3479
- ]), ctx.values.breakdown) pushBreakdownRows(table, data.modelBreakdowns);
3564
+ ]), mergedOptions.breakdown) pushBreakdownRows(table, data.modelBreakdowns);
3480
3565
  isFirstProject = false;
3481
3566
  }
3482
3567
  } else for (const data of dailyData) if (table.push([
@@ -3488,7 +3573,7 @@ const dailyCommand = define({
3488
3573
  formatNumber(data.cacheReadTokens),
3489
3574
  formatNumber(getTotalTokens(data)),
3490
3575
  formatCurrency(data.totalCost)
3491
- ]), ctx.values.breakdown) pushBreakdownRows(table, data.modelBreakdowns);
3576
+ ]), mergedOptions.breakdown) pushBreakdownRows(table, data.modelBreakdowns);
3492
3577
  if (table.push([
3493
3578
  "",
3494
3579
  "",
@@ -3900,17 +3985,9 @@ const monthlyCommand = define({
3900
3985
  description: "Show usage report grouped by month",
3901
3986
  ...sharedCommandConfig,
3902
3987
  async run(ctx) {
3903
- const useJson = ctx.values.json || ctx.values.jq != null;
3988
+ const config = loadConfig(ctx.values.config, ctx.values.debug), mergedOptions = mergeConfigWithArgs(ctx, config, ctx.values.debug), useJson = Boolean(mergedOptions.json) || mergedOptions.jq != null;
3904
3989
  if (useJson) logger.level = 0;
3905
- const monthlyData = await loadMonthlyUsageData({
3906
- since: ctx.values.since,
3907
- until: ctx.values.until,
3908
- mode: ctx.values.mode,
3909
- order: ctx.values.order,
3910
- offline: ctx.values.offline,
3911
- timezone: ctx.values.timezone,
3912
- locale: ctx.values.locale
3913
- });
3990
+ const monthlyData = await loadMonthlyUsageData(mergedOptions);
3914
3991
  if (monthlyData.length === 0) {
3915
3992
  if (useJson) {
3916
3993
  const emptyOutput = {
@@ -3929,9 +4006,9 @@ const monthlyCommand = define({
3929
4006
  process$1.exit(0);
3930
4007
  }
3931
4008
  const totals = calculateTotals(monthlyData);
3932
- if (ctx.values.debug && !useJson) {
4009
+ if (mergedOptions.debug && !useJson) {
3933
4010
  const mismatchStats = await detectMismatches(void 0);
3934
- printMismatchReport(mismatchStats, ctx.values.debugSamples);
4011
+ printMismatchReport(mismatchStats, mergedOptions.debugSamples);
3935
4012
  }
3936
4013
  if (useJson) {
3937
4014
  const jsonOutput = {
@@ -3948,8 +4025,8 @@ const monthlyCommand = define({
3948
4025
  })),
3949
4026
  totals: createTotalsObject(totals)
3950
4027
  };
3951
- if (ctx.values.jq != null) {
3952
- const jqResult = await processWithJq(jsonOutput, ctx.values.jq);
4028
+ if (mergedOptions.jq != null) {
4029
+ const jqResult = await processWithJq(jsonOutput, mergedOptions.jq);
3953
4030
  if (isFailure(jqResult)) logger.error(jqResult.error.message), process$1.exit(1);
3954
4031
  log(jqResult.value);
3955
4032
  } else log(JSON.stringify(jsonOutput, null, 2));
@@ -3977,7 +4054,7 @@ const monthlyCommand = define({
3977
4054
  "right",
3978
4055
  "right"
3979
4056
  ],
3980
- dateFormatter: (dateStr) => formatDateCompact(dateStr, ctx.values.timezone, ctx.values.locale),
4057
+ dateFormatter: (dateStr) => formatDateCompact(dateStr, mergedOptions.timezone, mergedOptions.locale ?? "en-CA"),
3981
4058
  compactHead: [
3982
4059
  "Month",
3983
4060
  "Models",
@@ -4004,7 +4081,7 @@ const monthlyCommand = define({
4004
4081
  formatNumber(data.cacheReadTokens),
4005
4082
  formatNumber(getTotalTokens(data)),
4006
4083
  formatCurrency(data.totalCost)
4007
- ]), ctx.values.breakdown) pushBreakdownRows(table, data.modelBreakdowns);
4084
+ ]), mergedOptions.breakdown) pushBreakdownRows(table, data.modelBreakdowns);
4008
4085
  if (table.push([
4009
4086
  "",
4010
4087
  "",
@@ -4116,15 +4193,15 @@ const sessionCommand = define({
4116
4193
  },
4117
4194
  toKebab: true,
4118
4195
  async run(ctx) {
4119
- const useJson = ctx.values.json || ctx.values.jq != null;
4196
+ const config = loadConfig(ctx.values.config, ctx.values.debug), mergedOptions = mergeConfigWithArgs(ctx, config, ctx.values.debug), useJson = mergedOptions.json || mergedOptions.jq != null;
4120
4197
  if (useJson) logger.level = 0;
4121
- if (ctx.values.id != null) return handleSessionIdLookup({ values: {
4122
- id: ctx.values.id,
4123
- mode: ctx.values.mode,
4124
- offline: ctx.values.offline,
4125
- jq: ctx.values.jq,
4126
- timezone: ctx.values.timezone,
4127
- locale: ctx.values.locale ?? "en-CA"
4198
+ if (mergedOptions.id != null) return handleSessionIdLookup({ values: {
4199
+ id: mergedOptions.id,
4200
+ mode: mergedOptions.mode,
4201
+ offline: mergedOptions.offline,
4202
+ jq: mergedOptions.jq,
4203
+ timezone: mergedOptions.timezone,
4204
+ locale: mergedOptions.locale ?? "en-CA"
4128
4205
  } }, useJson);
4129
4206
  const sessionData = await loadSessionData({
4130
4207
  since: ctx.values.since,
@@ -5132,7 +5209,12 @@ function getSemaphore(sessionId) {
5132
5209
  const semaphore = createLimoJson(semaphorePath);
5133
5210
  return semaphore;
5134
5211
  }
5135
- const statuslineCommand = define({
5212
+ const visualBurnRateChoices = [
5213
+ "off",
5214
+ "emoji",
5215
+ "text",
5216
+ "emoji-text"
5217
+ ], statuslineCommand = define({
5136
5218
  name: "statusline",
5137
5219
  description: "Display compact status line for Claude Code hooks with hybrid time+file caching (Beta)",
5138
5220
  toKebab: true,
@@ -5141,6 +5223,15 @@ const statuslineCommand = define({
5141
5223
  ...sharedArgs.offline,
5142
5224
  default: true
5143
5225
  },
5226
+ visualBurnRate: {
5227
+ type: "enum",
5228
+ choices: visualBurnRateChoices,
5229
+ description: "Controls the visualization of the burn rate status",
5230
+ default: "off",
5231
+ short: "vb",
5232
+ negatable: false,
5233
+ toKebab: true
5234
+ },
5144
5235
  cache: {
5145
5236
  type: "boolean",
5146
5237
  description: "Enable cache for status line output (default: true)",
@@ -5150,16 +5241,18 @@ const statuslineCommand = define({
5150
5241
  type: "number",
5151
5242
  description: `Refresh interval in seconds for cache expiry (default: ${DEFAULT_REFRESH_INTERVAL_SECONDS})`,
5152
5243
  default: DEFAULT_REFRESH_INTERVAL_SECONDS
5153
- }
5244
+ },
5245
+ config: sharedArgs.config,
5246
+ debug: sharedArgs.debug
5154
5247
  },
5155
5248
  async run(ctx) {
5156
5249
  logger.level = 0;
5157
- const refreshInterval = ctx.values.refreshInterval, stdin$2 = await getStdin();
5250
+ const config = loadConfig(ctx.values.config, ctx.values.debug), mergedOptions = mergeConfigWithArgs(ctx, config, ctx.values.debug), refreshInterval = mergedOptions.refreshInterval, stdin$2 = await getStdin();
5158
5251
  if (stdin$2.length === 0) log("❌ No input provided"), process$1.exit(1);
5159
5252
  const hookDataJson = JSON.parse(stdin$2.trim()), hookDataParseResult = statuslineHookJsonSchema.safeParse(hookDataJson);
5160
5253
  if (!hookDataParseResult.success) log("❌ Invalid input format:", hookDataParseResult.error.message), process$1.exit(1);
5161
5254
  const hookData = hookDataParseResult.data, sessionId = hookData.session_id, initialSemaphoreState = pipe(succeed(getSemaphore(sessionId)), map((semaphore) => semaphore.data), unwrap(void 0)), currentMtime = await getFileModifiedTime(hookData.transcript_path);
5162
- if (ctx.values.cache && initialSemaphoreState != null) {
5255
+ if (mergedOptions.cache && initialSemaphoreState != null) {
5163
5256
  const now = Date.now(), timeElapsed = now - (initialSemaphoreState.lastUpdateTime ?? 0), isExpired = timeElapsed >= refreshInterval * 1e3, isFileModified = initialSemaphoreState.transcriptMtime !== currentMtime;
5164
5257
  if (!isExpired && !isFileModified) {
5165
5258
  log(initialSemaphoreState.lastOutput);
@@ -5209,7 +5302,7 @@ const statuslineCommand = define({
5209
5302
  const sessionCost = await pipe(try_({
5210
5303
  try: loadSessionUsageById(sessionId, {
5211
5304
  mode: "auto",
5212
- offline: ctx.values.offline
5305
+ offline: mergedOptions.offline
5213
5306
  }),
5214
5307
  catch: (error) => error
5215
5308
  }), map((sessionCost$1) => sessionCost$1?.totalCost), inspectError((error) => logger.error("Failed to load session data:", error)), unwrap(void 0)), today = /* @__PURE__ */ new Date(), todayStr = today.toISOString().split("T")[0]?.replace(/-/g, "") ?? "", todayCost = await pipe(try_({
@@ -5217,7 +5310,7 @@ const statuslineCommand = define({
5217
5310
  since: todayStr,
5218
5311
  until: todayStr,
5219
5312
  mode: "auto",
5220
- offline: ctx.values.offline
5313
+ offline: mergedOptions.offline
5221
5314
  }),
5222
5315
  catch: (error) => error
5223
5316
  }), map((dailyData) => {
@@ -5229,7 +5322,7 @@ const statuslineCommand = define({
5229
5322
  }), inspectError((error) => logger.error("Failed to load daily data:", error)), unwrap(0)), { blockInfo, burnRateInfo } = await pipe(try_({
5230
5323
  try: loadSessionBlockData({
5231
5324
  mode: "auto",
5232
- offline: ctx.values.offline
5325
+ offline: mergedOptions.offline
5233
5326
  }),
5234
5327
  catch: (error) => error
5235
5328
  }), map((blocks) => {
@@ -5242,8 +5335,26 @@ const statuslineCommand = define({
5242
5335
  });
5243
5336
  if (activeBlock != null) {
5244
5337
  const now = /* @__PURE__ */ new Date(), remaining = Math.round((activeBlock.endTime.getTime() - now.getTime()) / (1e3 * 60)), blockCost = activeBlock.costUSD, blockInfo$1 = `${formatCurrency(blockCost)} block (${formatRemainingTime(remaining)})`, burnRate = calculateBurnRate(activeBlock), burnRateInfo$1 = burnRate != null ? (() => {
5245
- const costPerHour = burnRate.costPerHour, costPerHourStr = `${formatCurrency(costPerHour)}/hr`, coloredBurnRate = burnRate.tokensPerMinuteForIndicator < 2e3 ? import_picocolors$1.default.green(costPerHourStr) : burnRate.tokensPerMinuteForIndicator < 5e3 ? import_picocolors$1.default.yellow(costPerHourStr) : import_picocolors$1.default.red(costPerHourStr);
5246
- return ` | 🔥 ${coloredBurnRate}`;
5338
+ const renderEmojiStatus = ctx.values.visualBurnRate === "emoji" || ctx.values.visualBurnRate === "emoji-text", renderTextStatus = ctx.values.visualBurnRate === "text" || ctx.values.visualBurnRate === "emoji-text", costPerHour = burnRate.costPerHour, costPerHourStr = `${formatCurrency(costPerHour)}/hr`, burnStatus = burnRate.tokensPerMinuteForIndicator < 2e3 ? "normal" : burnRate.tokensPerMinuteForIndicator < 5e3 ? "moderate" : "high", burnStatusMappings = {
5339
+ normal: {
5340
+ emoji: "🟢",
5341
+ textValue: "Normal",
5342
+ coloredString: import_picocolors$1.default.green
5343
+ },
5344
+ moderate: {
5345
+ emoji: "⚠️",
5346
+ textValue: "Moderate",
5347
+ coloredString: import_picocolors$1.default.yellow
5348
+ },
5349
+ high: {
5350
+ emoji: "🚨",
5351
+ textValue: "High",
5352
+ coloredString: import_picocolors$1.default.red
5353
+ }
5354
+ }, { emoji, textValue, coloredString } = burnStatusMappings[burnStatus], burnRateOutputSegments = [coloredString(costPerHourStr)];
5355
+ if (renderEmojiStatus) burnRateOutputSegments.push(emoji);
5356
+ if (renderTextStatus) burnRateOutputSegments.push(coloredString(`(${textValue})`));
5357
+ return ` | 🔥 ${burnRateOutputSegments.join(" ")}`;
5247
5358
  })() : "";
5248
5359
  return {
5249
5360
  blockInfo: blockInfo$1,
@@ -5258,7 +5369,7 @@ const statuslineCommand = define({
5258
5369
  blockInfo: "No active block",
5259
5370
  burnRateInfo: ""
5260
5371
  })), contextInfo = await pipe(try_({
5261
- try: calculateContextTokens(hookData.transcript_path, hookData.model.id, ctx.values.offline),
5372
+ try: calculateContextTokens(hookData.transcript_path, hookData.model.id, mergedOptions.offline),
5262
5373
  catch: (error) => error
5263
5374
  }), inspectError((error) => logger.debug(`Failed to calculate context tokens: ${error instanceof Error ? error.message : String(error)}`)), map((ctx$1) => {
5264
5375
  if (ctx$1 == null) return void 0;
@@ -5272,7 +5383,7 @@ const statuslineCommand = define({
5272
5383
  if (isSuccess(mainProcessingResult)) try {
5273
5384
  var _usingCtx3 = (0, import_usingCtx.default)();
5274
5385
  const statusLine = mainProcessingResult.value;
5275
- if (log(statusLine), !ctx.values.cache) return;
5386
+ if (log(statusLine), !mergedOptions.cache) return;
5276
5387
  const semaphore = _usingCtx3.u(getSemaphore(sessionId));
5277
5388
  semaphore.data = {
5278
5389
  date: (/* @__PURE__ */ new Date()).toISOString(),
@@ -5293,7 +5404,7 @@ const statuslineCommand = define({
5293
5404
  var _usingCtx4 = (0, import_usingCtx.default)();
5294
5405
  if (initialSemaphoreState?.lastOutput != null && initialSemaphoreState.lastOutput !== "") log(initialSemaphoreState.lastOutput);
5295
5406
  else log("❌ Error generating status");
5296
- if (logger.error("Error in statusline command:", mainProcessingResult.error), !ctx.values.cache) return;
5407
+ if (logger.error("Error in statusline command:", mainProcessingResult.error), !mergedOptions.cache) return;
5297
5408
  const semaphore = _usingCtx4.u(getSemaphore(sessionId));
5298
5409
  if (semaphore.data != null) semaphore.data.isUpdating = false, semaphore.data.pid = void 0;
5299
5410
  } catch (_) {
@@ -5319,18 +5430,9 @@ const weeklyCommand = define({
5319
5430
  },
5320
5431
  toKebab: true,
5321
5432
  async run(ctx) {
5322
- const useJson = ctx.values.json || ctx.values.jq != null;
5433
+ const config = loadConfig(ctx.values.config, ctx.values.debug), mergedOptions = mergeConfigWithArgs(ctx, config, ctx.values.debug), useJson = Boolean(mergedOptions.json) || mergedOptions.jq != null;
5323
5434
  if (useJson) logger.level = 0;
5324
- const weeklyData = await loadWeeklyUsageData({
5325
- since: ctx.values.since,
5326
- until: ctx.values.until,
5327
- timezone: ctx.values.timezone,
5328
- mode: ctx.values.mode,
5329
- order: ctx.values.order,
5330
- offline: ctx.values.offline,
5331
- startOfWeek: ctx.values.startOfWeek,
5332
- locale: ctx.values.locale
5333
- });
5435
+ const weeklyData = await loadWeeklyUsageData(mergedOptions);
5334
5436
  if (weeklyData.length === 0) {
5335
5437
  if (useJson) {
5336
5438
  const emptyOutput = {
@@ -5349,9 +5451,9 @@ const weeklyCommand = define({
5349
5451
  process$1.exit(0);
5350
5452
  }
5351
5453
  const totals = calculateTotals(weeklyData);
5352
- if (ctx.values.debug && !useJson) {
5454
+ if (mergedOptions.debug && !useJson) {
5353
5455
  const mismatchStats = await detectMismatches(void 0);
5354
- printMismatchReport(mismatchStats, ctx.values.debugSamples);
5456
+ printMismatchReport(mismatchStats, mergedOptions.debugSamples);
5355
5457
  }
5356
5458
  if (useJson) {
5357
5459
  const jsonOutput = {
@@ -5368,8 +5470,8 @@ const weeklyCommand = define({
5368
5470
  })),
5369
5471
  totals: createTotalsObject(totals)
5370
5472
  };
5371
- if (ctx.values.jq != null) {
5372
- const jqResult = await processWithJq(jsonOutput, ctx.values.jq);
5473
+ if (mergedOptions.jq != null) {
5474
+ const jqResult = await processWithJq(jsonOutput, mergedOptions.jq);
5373
5475
  if (isFailure(jqResult)) logger.error(jqResult.error.message), process$1.exit(1);
5374
5476
  log(jqResult.value);
5375
5477
  } else log(JSON.stringify(jsonOutput, null, 2));
@@ -5397,7 +5499,7 @@ const weeklyCommand = define({
5397
5499
  "right",
5398
5500
  "right"
5399
5501
  ],
5400
- dateFormatter: (dateStr) => formatDateCompact(dateStr, ctx.values.timezone, ctx.values.locale),
5502
+ dateFormatter: (dateStr) => formatDateCompact(dateStr, mergedOptions.timezone, mergedOptions.locale ?? void 0),
5401
5503
  compactHead: [
5402
5504
  "Week",
5403
5505
  "Models",
@@ -5424,7 +5526,7 @@ const weeklyCommand = define({
5424
5526
  formatNumber(data.cacheReadTokens),
5425
5527
  formatNumber(getTotalTokens(data)),
5426
5528
  formatCurrency(data.totalCost)
5427
- ]), ctx.values.breakdown) pushBreakdownRows(table, data.modelBreakdowns);
5529
+ ]), mergedOptions.breakdown) pushBreakdownRows(table, data.modelBreakdowns);
5428
5530
  if (table.push([
5429
5531
  "",
5430
5532
  "",
@@ -5446,14 +5548,25 @@ const weeklyCommand = define({
5446
5548
  ]), log(table.toString()), table.isCompactMode()) logger.info("\nRunning in Compact Mode"), logger.info("Expand terminal width to see cache metrics and total tokens");
5447
5549
  }
5448
5550
  }
5449
- }), subCommands = /* @__PURE__ */ new Map();
5450
- subCommands.set("daily", dailyCommand), subCommands.set("monthly", monthlyCommand), subCommands.set("weekly", weeklyCommand), subCommands.set("session", sessionCommand), subCommands.set("blocks", blocksCommand), subCommands.set("mcp", mcpCommand), subCommands.set("statusline", statuslineCommand);
5551
+ }), subCommandUnion = [
5552
+ ["daily", dailyCommand],
5553
+ ["monthly", monthlyCommand],
5554
+ ["weekly", weeklyCommand],
5555
+ ["session", sessionCommand],
5556
+ ["blocks", blocksCommand],
5557
+ ["mcp", mcpCommand],
5558
+ ["statusline", statuslineCommand]
5559
+ ], subCommands = /* @__PURE__ */ new Map();
5560
+ for (const [name$1, command] of subCommandUnion) subCommands.set(name$1, command);
5451
5561
  const mainCommand = dailyCommand;
5452
- await cli(process$1.argv.slice(2), mainCommand, {
5453
- name,
5454
- version,
5455
- description,
5456
- subCommands,
5457
- renderHeader: null
5458
- });
5562
+ async function run() {
5563
+ await cli(process$1.argv.slice(2), mainCommand, {
5564
+ name,
5565
+ version,
5566
+ description,
5567
+ subCommands,
5568
+ renderHeader: null
5569
+ });
5570
+ }
5571
+ await run();
5459
5572
  export {};
@@ -761,7 +761,7 @@ function _getDefaultLogLevel() {
761
761
  return g ? LogLevels.debug : R ? LogLevels.warn : LogLevels.info;
762
762
  }
763
763
  const consola = createConsola$1();
764
- var name = "ccusage", version = "15.10.0", description = "Usage analysis tool for Claude Code";
764
+ var name = "ccusage", version = "16.1.0", description = "Usage analysis tool for Claude Code";
765
765
  const logger = consola.withTag(name);
766
766
  if (process$1.env.LOG_LEVEL != null) {
767
767
  const level = Number.parseInt(process$1.env.LOG_LEVEL, 10);
package/dist/logger.js CHANGED
@@ -1,2 +1,2 @@
1
- import { log, logger } from "./logger-yNFB24CE.js";
1
+ import { log, logger } from "./logger-Cyk_YiBe.js";
2
2
  export { log, logger };
@@ -1,9 +1,9 @@
1
- import { __commonJSMin, __toESM, require_usingCtx } from "./pricing-fetcher-D3tIkxxO.js";
1
+ import { __commonJSMin, __toESM, require_usingCtx } from "./pricing-fetcher-5FVKt1XA.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-BbEk8t2a.js";
4
4
  import { calculateTotals, createTotalsObject } from "./calculate-cost-BDqO4yWA.js";
5
- import { getClaudePaths, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData } from "./data-loader-Dhwqb-FE.js";
6
- import { name, version } from "./logger-yNFB24CE.js";
5
+ import { getClaudePaths, loadDailyUsageData, loadMonthlyUsageData, loadSessionBlockData, loadSessionData } from "./data-loader-D9y22SrV.js";
6
+ import { name, version } from "./logger-Cyk_YiBe.js";
7
7
  import process from "node:process";
8
8
  const LATEST_PROTOCOL_VERSION = "2025-06-18", SUPPORTED_PROTOCOL_VERSIONS = [
9
9
  LATEST_PROTOCOL_VERSION,
@@ -6670,12 +6670,12 @@ function transformUsageDataWithTotals(data, totals, mapper, key) {
6670
6670
  totals: createTotalsObject(totals)
6671
6671
  };
6672
6672
  }
6673
- const defaultOptions = { claudePath: (() => {
6673
+ function defaultOptions() {
6674
6674
  const paths = getClaudePaths();
6675
6675
  if (paths.length === 0) throw new Error("No valid Claude path found. Ensure getClaudePaths() returns at least one valid path.");
6676
- return paths[0];
6677
- })() };
6678
- function createMcpServer({ claudePath } = defaultOptions) {
6676
+ return { claudePath: paths[0] };
6677
+ }
6678
+ function createMcpServer(options) {
6679
6679
  const server = new McpServer({
6680
6680
  name,
6681
6681
  version
@@ -6689,7 +6689,7 @@ function createMcpServer({ claudePath } = defaultOptions) {
6689
6689
  ]).default("auto").optional(),
6690
6690
  timezone: stringType().optional(),
6691
6691
  locale: stringType().default("en-CA").optional()
6692
- };
6692
+ }, { claudePath } = options ?? defaultOptions();
6693
6693
  return server.registerTool("daily", {
6694
6694
  description: "Show usage report grouped by date",
6695
6695
  inputSchema: parametersZodSchema,
@@ -6807,8 +6807,8 @@ async function startMcpServerStdio(server) {
6807
6807
  const transport = new StdioServerTransport();
6808
6808
  await server.connect(transport);
6809
6809
  }
6810
- function createMcpHttpApp(options = defaultOptions) {
6811
- const app = new Hono(), mcpServer = createMcpServer(options);
6810
+ function createMcpHttpApp(options) {
6811
+ const app = new Hono(), mcpServer = createMcpServer(options ?? defaultOptions());
6812
6812
  return app.all("/", async (c) => {
6813
6813
  const transport = new StreamableHTTPTransport();
6814
6814
  return await mcpServer.connect(transport), transport.handleRequest(c);