axusage 2.0.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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +254 -0
  3. package/bin/axusage +2 -0
  4. package/dist/adapters/chatgpt.d.ts +8 -0
  5. package/dist/adapters/chatgpt.js +68 -0
  6. package/dist/adapters/claude.d.ts +9 -0
  7. package/dist/adapters/claude.js +108 -0
  8. package/dist/adapters/coalesce-claude-usage-response.d.ts +12 -0
  9. package/dist/adapters/coalesce-claude-usage-response.js +119 -0
  10. package/dist/adapters/gemini.d.ts +8 -0
  11. package/dist/adapters/gemini.js +43 -0
  12. package/dist/adapters/github-copilot.d.ts +6 -0
  13. package/dist/adapters/github-copilot.js +56 -0
  14. package/dist/adapters/parse-chatgpt-usage.d.ts +15 -0
  15. package/dist/adapters/parse-chatgpt-usage.js +28 -0
  16. package/dist/adapters/parse-claude-usage.d.ts +16 -0
  17. package/dist/adapters/parse-claude-usage.js +75 -0
  18. package/dist/adapters/parse-gemini-usage.d.ts +55 -0
  19. package/dist/adapters/parse-gemini-usage.js +151 -0
  20. package/dist/adapters/parse-github-copilot-usage.d.ts +23 -0
  21. package/dist/adapters/parse-github-copilot-usage.js +78 -0
  22. package/dist/cli.d.ts +2 -0
  23. package/dist/cli.js +69 -0
  24. package/dist/commands/auth-clear-command.d.ts +5 -0
  25. package/dist/commands/auth-clear-command.js +25 -0
  26. package/dist/commands/auth-setup-command.d.ts +11 -0
  27. package/dist/commands/auth-setup-command.js +45 -0
  28. package/dist/commands/auth-status-command.d.ts +5 -0
  29. package/dist/commands/auth-status-command.js +25 -0
  30. package/dist/commands/fetch-service-usage-with-reauth.d.ts +7 -0
  31. package/dist/commands/fetch-service-usage-with-reauth.js +45 -0
  32. package/dist/commands/fetch-service-usage.d.ts +8 -0
  33. package/dist/commands/fetch-service-usage.js +19 -0
  34. package/dist/commands/run-auth-setup.d.ts +29 -0
  35. package/dist/commands/run-auth-setup.js +91 -0
  36. package/dist/commands/usage-command.d.ts +15 -0
  37. package/dist/commands/usage-command.js +146 -0
  38. package/dist/services/app-paths.d.ts +9 -0
  39. package/dist/services/app-paths.js +39 -0
  40. package/dist/services/auth-storage-path.d.ts +3 -0
  41. package/dist/services/auth-storage-path.js +7 -0
  42. package/dist/services/auth-timeouts.d.ts +4 -0
  43. package/dist/services/auth-timeouts.js +4 -0
  44. package/dist/services/browser-auth-manager.d.ts +49 -0
  45. package/dist/services/browser-auth-manager.js +113 -0
  46. package/dist/services/create-auth-context.d.ts +8 -0
  47. package/dist/services/create-auth-context.js +34 -0
  48. package/dist/services/do-setup-auth.d.ts +3 -0
  49. package/dist/services/do-setup-auth.js +25 -0
  50. package/dist/services/fetch-json-with-context.d.ts +5 -0
  51. package/dist/services/fetch-json-with-context.js +37 -0
  52. package/dist/services/gemini-api.d.ts +11 -0
  53. package/dist/services/gemini-api.js +109 -0
  54. package/dist/services/launch-chromium.d.ts +6 -0
  55. package/dist/services/launch-chromium.js +20 -0
  56. package/dist/services/persist-storage-state.d.ts +6 -0
  57. package/dist/services/persist-storage-state.js +16 -0
  58. package/dist/services/request-service.d.ts +3 -0
  59. package/dist/services/request-service.js +4 -0
  60. package/dist/services/service-adapter-registry.d.ts +18 -0
  61. package/dist/services/service-adapter-registry.js +26 -0
  62. package/dist/services/service-auth-configs.d.ts +15 -0
  63. package/dist/services/service-auth-configs.js +26 -0
  64. package/dist/services/setup-auth-flow.d.ts +3 -0
  65. package/dist/services/setup-auth-flow.js +40 -0
  66. package/dist/services/shared-browser-auth-manager.d.ts +4 -0
  67. package/dist/services/shared-browser-auth-manager.js +80 -0
  68. package/dist/services/supported-service.d.ts +6 -0
  69. package/dist/services/supported-service.js +16 -0
  70. package/dist/services/verify-session.d.ts +2 -0
  71. package/dist/services/verify-session.js +25 -0
  72. package/dist/services/wait-for-login.d.ts +5 -0
  73. package/dist/services/wait-for-login.js +44 -0
  74. package/dist/types/chatgpt.d.ts +32 -0
  75. package/dist/types/chatgpt.js +21 -0
  76. package/dist/types/domain.d.ts +57 -0
  77. package/dist/types/domain.js +16 -0
  78. package/dist/types/gemini.d.ts +31 -0
  79. package/dist/types/gemini.js +27 -0
  80. package/dist/types/github-copilot.d.ts +21 -0
  81. package/dist/types/github-copilot.js +27 -0
  82. package/dist/types/usage.d.ts +31 -0
  83. package/dist/types/usage.js +25 -0
  84. package/dist/utils/calculate-usage-rate.d.ts +9 -0
  85. package/dist/utils/calculate-usage-rate.js +31 -0
  86. package/dist/utils/classify-usage-rate.d.ts +6 -0
  87. package/dist/utils/classify-usage-rate.js +10 -0
  88. package/dist/utils/format-prometheus-metrics.d.ts +6 -0
  89. package/dist/utils/format-prometheus-metrics.js +20 -0
  90. package/dist/utils/format-service-usage.d.ts +18 -0
  91. package/dist/utils/format-service-usage.js +120 -0
  92. package/package.json +83 -0
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * ChatGPT API response schemas
4
+ */
5
+ export const ChatGPTRateLimitWindow = z.object({
6
+ used_percent: z.number(),
7
+ limit_window_seconds: z.number(),
8
+ reset_after_seconds: z.number(),
9
+ reset_at: z.number(), // Unix timestamp
10
+ });
11
+ const ChatGPTRateLimit = z.object({
12
+ allowed: z.boolean(),
13
+ limit_reached: z.boolean(),
14
+ primary_window: ChatGPTRateLimitWindow,
15
+ secondary_window: ChatGPTRateLimitWindow,
16
+ });
17
+ export const ChatGPTUsageResponse = z.object({
18
+ plan_type: z.string(),
19
+ rate_limit: ChatGPTRateLimit,
20
+ credits: z.unknown().nullable(),
21
+ });
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Common domain types for representing usage data across different services
3
+ */
4
+ /**
5
+ * A single usage window/period with utilization data
6
+ */
7
+ export type UsageWindow = {
8
+ readonly name: string;
9
+ readonly utilization: number;
10
+ readonly resetsAt: Date | undefined;
11
+ readonly periodDurationMs: number;
12
+ };
13
+ /**
14
+ * Complete usage data for a service
15
+ */
16
+ export type ServiceUsageData = {
17
+ readonly service: string;
18
+ readonly planType?: string;
19
+ readonly windows: readonly UsageWindow[];
20
+ readonly metadata?: {
21
+ readonly allowed?: boolean;
22
+ readonly limitReached?: boolean;
23
+ };
24
+ };
25
+ /**
26
+ * Result type for API operations
27
+ */
28
+ export type Result<T, E extends Error> = {
29
+ readonly ok: true;
30
+ readonly value: T;
31
+ } | {
32
+ readonly ok: false;
33
+ readonly error: E;
34
+ };
35
+ /**
36
+ * Base error class for API errors
37
+ */
38
+ export declare class ApiError extends Error {
39
+ readonly status?: number;
40
+ readonly body?: unknown;
41
+ constructor(message: string, status?: number, body?: unknown);
42
+ }
43
+ /**
44
+ * Service adapter interface
45
+ */
46
+ export interface ServiceAdapter {
47
+ readonly name: string;
48
+ fetchUsage(): Promise<Result<ServiceUsageData, ApiError>>;
49
+ }
50
+ /**
51
+ * Result of fetching usage for a single service.
52
+ * Wraps the service name with its usage data result or error.
53
+ */
54
+ export type ServiceResult = {
55
+ readonly service: string;
56
+ readonly result: Result<ServiceUsageData, ApiError>;
57
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Common domain types for representing usage data across different services
3
+ */
4
+ /**
5
+ * Base error class for API errors
6
+ */
7
+ export class ApiError extends Error {
8
+ status;
9
+ body;
10
+ constructor(message, status, body) {
11
+ super(message);
12
+ this.name = "ApiError";
13
+ this.status = status;
14
+ this.body = body;
15
+ }
16
+ }
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Quota API bucket schema
4
+ */
5
+ export declare const GeminiQuotaBucket: z.ZodObject<{
6
+ modelId: z.ZodString;
7
+ remainingFraction: z.ZodNumber;
8
+ resetTime: z.ZodOptional<z.ZodString>;
9
+ tokenType: z.ZodOptional<z.ZodString>;
10
+ }, z.core.$strip>;
11
+ export type GeminiQuotaBucket = z.infer<typeof GeminiQuotaBucket>;
12
+ /**
13
+ * Quota API response schema
14
+ */
15
+ export declare const GeminiQuotaResponse: z.ZodObject<{
16
+ buckets: z.ZodArray<z.ZodObject<{
17
+ modelId: z.ZodString;
18
+ remainingFraction: z.ZodNumber;
19
+ resetTime: z.ZodOptional<z.ZodString>;
20
+ tokenType: z.ZodOptional<z.ZodString>;
21
+ }, z.core.$strip>>;
22
+ }, z.core.$strip>;
23
+ export type GeminiQuotaResponse = z.infer<typeof GeminiQuotaResponse>;
24
+ export declare const GeminiProjectsResponse: z.ZodObject<{
25
+ projects: z.ZodOptional<z.ZodArray<z.ZodObject<{
26
+ projectId: z.ZodString;
27
+ projectNumber: z.ZodOptional<z.ZodString>;
28
+ labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
29
+ }, z.core.$strip>>>;
30
+ }, z.core.$strip>;
31
+ export type GeminiProjectsResponse = z.infer<typeof GeminiProjectsResponse>;
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Quota API bucket schema
4
+ */
5
+ export const GeminiQuotaBucket = z.object({
6
+ modelId: z.string(),
7
+ remainingFraction: z.number(), // 0-1
8
+ resetTime: z.string().optional(), // ISO8601
9
+ tokenType: z.string().optional(), // "input" | "output"
10
+ });
11
+ /**
12
+ * Quota API response schema
13
+ */
14
+ export const GeminiQuotaResponse = z.object({
15
+ buckets: z.array(GeminiQuotaBucket),
16
+ });
17
+ /**
18
+ * Cloud Resource Manager projects response
19
+ */
20
+ const GeminiProject = z.object({
21
+ projectId: z.string(),
22
+ projectNumber: z.string().optional(),
23
+ labels: z.record(z.string(), z.string()).optional(),
24
+ });
25
+ export const GeminiProjectsResponse = z.object({
26
+ projects: z.array(GeminiProject).optional(),
27
+ });
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ export declare const GitHubCopilotUsageResponse: z.ZodObject<{
3
+ licenseType: z.ZodString;
4
+ quotas: z.ZodObject<{
5
+ limits: z.ZodObject<{
6
+ premiumInteractions: z.ZodNumber;
7
+ }, z.core.$strip>;
8
+ remaining: z.ZodObject<{
9
+ premiumInteractions: z.ZodNumber;
10
+ chatPercentage: z.ZodOptional<z.ZodNumber>;
11
+ premiumInteractionsPercentage: z.ZodNumber;
12
+ }, z.core.$strip>;
13
+ resetDate: z.ZodString;
14
+ overagesEnabled: z.ZodOptional<z.ZodBoolean>;
15
+ }, z.core.$strip>;
16
+ plan: z.ZodString;
17
+ trial: z.ZodOptional<z.ZodObject<{
18
+ eligible: z.ZodBoolean;
19
+ }, z.core.$strip>>;
20
+ }, z.core.$strip>;
21
+ export type GitHubCopilotUsageResponse = z.infer<typeof GitHubCopilotUsageResponse>;
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * GitHub Copilot API response schemas
4
+ */
5
+ const GitHubCopilotQuotaLimits = z.object({
6
+ premiumInteractions: z.number(),
7
+ });
8
+ const GitHubCopilotQuotaRemaining = z.object({
9
+ premiumInteractions: z.number(),
10
+ chatPercentage: z.number().optional(),
11
+ premiumInteractionsPercentage: z.number(),
12
+ });
13
+ const GitHubCopilotQuotas = z.object({
14
+ limits: GitHubCopilotQuotaLimits,
15
+ remaining: GitHubCopilotQuotaRemaining,
16
+ resetDate: z.string(), // Format: "YYYY-MM-DD"
17
+ overagesEnabled: z.boolean().optional(),
18
+ });
19
+ const GitHubCopilotTrial = z.object({
20
+ eligible: z.boolean(),
21
+ });
22
+ export const GitHubCopilotUsageResponse = z.object({
23
+ licenseType: z.string(),
24
+ quotas: GitHubCopilotQuotas,
25
+ plan: z.string(),
26
+ trial: GitHubCopilotTrial.optional(),
27
+ });
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Complete usage response from the Anthropic API
4
+ *
5
+ * Note: `seven_day_opus` and `seven_day_sonnet` can be null depending on the
6
+ * user's plan. The API may return either, both, or neither.
7
+ */
8
+ export declare const UsageResponse: z.ZodObject<{
9
+ five_hour: z.ZodObject<{
10
+ utilization: z.ZodNumber;
11
+ resets_at: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>, z.ZodTransform<string | undefined, string | null | undefined>>;
12
+ }, z.core.$strip>;
13
+ seven_day: z.ZodObject<{
14
+ utilization: z.ZodNumber;
15
+ resets_at: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>, z.ZodTransform<string | undefined, string | null | undefined>>;
16
+ }, z.core.$strip>;
17
+ seven_day_oauth_apps: z.ZodOptional<z.ZodNullable<z.ZodObject<{
18
+ utilization: z.ZodNumber;
19
+ resets_at: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>, z.ZodTransform<string | undefined, string | null | undefined>>;
20
+ }, z.core.$strip>>>;
21
+ seven_day_opus: z.ZodOptional<z.ZodNullable<z.ZodObject<{
22
+ utilization: z.ZodNumber;
23
+ resets_at: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>, z.ZodTransform<string | undefined, string | null | undefined>>;
24
+ }, z.core.$strip>>>;
25
+ seven_day_sonnet: z.ZodOptional<z.ZodNullable<z.ZodObject<{
26
+ utilization: z.ZodNumber;
27
+ resets_at: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>, z.ZodTransform<string | undefined, string | null | undefined>>;
28
+ }, z.core.$strip>>>;
29
+ }, z.core.$strip>;
30
+ export type UsageResponse = z.infer<typeof UsageResponse>;
31
+ export type UsageResponseInput = z.input<typeof UsageResponse>;
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Usage metric with utilization percentage and reset timestamp
4
+ */
5
+ const UsageMetric = z.object({
6
+ utilization: z.number(),
7
+ // Transform API null to undefined so the rest of the app only handles string | undefined.
8
+ resets_at: z.iso
9
+ .datetime({ offset: true })
10
+ .nullish()
11
+ .transform((value) => value === null ? undefined : value),
12
+ });
13
+ /**
14
+ * Complete usage response from the Anthropic API
15
+ *
16
+ * Note: `seven_day_opus` and `seven_day_sonnet` can be null depending on the
17
+ * user's plan. The API may return either, both, or neither.
18
+ */
19
+ export const UsageResponse = z.object({
20
+ five_hour: UsageMetric,
21
+ seven_day: UsageMetric,
22
+ seven_day_oauth_apps: UsageMetric.nullable().optional(),
23
+ seven_day_opus: UsageMetric.nullable().optional(),
24
+ seven_day_sonnet: UsageMetric.nullable().optional(),
25
+ });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Calculates usage rate based on time elapsed vs usage consumed
3
+ * Rate = actual_usage / expected_usage
4
+ *
5
+ * Returns undefined if insufficient time has elapsed for accurate rate calculation.
6
+ * Rate is shown after either {@link MIN_ELAPSED_FRACTION} of the period OR
7
+ * {@link MIN_ELAPSED_TIME_MS} has passed (whichever comes first).
8
+ */
9
+ export declare function calculateUsageRate(utilization: number, resetsAt: Date | undefined, periodDurationMs: number): number | undefined;
@@ -0,0 +1,31 @@
1
+ /** Minimum fraction of period that must elapse before showing rate */
2
+ const MIN_ELAPSED_FRACTION = 0.05; // 5%
3
+ /** Minimum time that must elapse before showing rate (2 hours in ms) */
4
+ const MIN_ELAPSED_TIME_MS = 2 * 60 * 60 * 1000;
5
+ /**
6
+ * Calculates usage rate based on time elapsed vs usage consumed
7
+ * Rate = actual_usage / expected_usage
8
+ *
9
+ * Returns undefined if insufficient time has elapsed for accurate rate calculation.
10
+ * Rate is shown after either {@link MIN_ELAPSED_FRACTION} of the period OR
11
+ * {@link MIN_ELAPSED_TIME_MS} has passed (whichever comes first).
12
+ */
13
+ export function calculateUsageRate(utilization, resetsAt, periodDurationMs) {
14
+ if (!resetsAt)
15
+ return undefined;
16
+ if (periodDurationMs <= 0)
17
+ return 0;
18
+ const now = Date.now();
19
+ const resetTime = resetsAt.getTime();
20
+ const periodStart = resetTime - periodDurationMs;
21
+ const elapsedTime = now - periodStart;
22
+ if (elapsedTime <= 0)
23
+ return undefined;
24
+ // Avoid inaccurate rates early in the period
25
+ // Show rate after 5% of period OR 2 hours elapsed (whichever first)
26
+ const minElapsedTime = Math.min(periodDurationMs * MIN_ELAPSED_FRACTION, MIN_ELAPSED_TIME_MS);
27
+ if (elapsedTime < minElapsedTime)
28
+ return undefined;
29
+ const elapsedPercentage = (elapsedTime / periodDurationMs) * 100;
30
+ return utilization / elapsedPercentage;
31
+ }
@@ -0,0 +1,6 @@
1
+ type UsageRateCategory = "green" | "yellow" | "red";
2
+ /**
3
+ * Classifies a usage rate into a severity bucket
4
+ */
5
+ export declare function classifyUsageRate(rate: number): UsageRateCategory;
6
+ export {};
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Classifies a usage rate into a severity bucket
3
+ */
4
+ export function classifyUsageRate(rate) {
5
+ if (rate > 1.5)
6
+ return "red";
7
+ if (rate > 1)
8
+ return "yellow";
9
+ return "green";
10
+ }
@@ -0,0 +1,6 @@
1
+ import type { ServiceUsageData } from "../types/domain.js";
2
+ /**
3
+ * Formats service usage data as Prometheus text exposition using prom-client.
4
+ * Emits a gauge `axusage_utilization_percent{service,window}` per window.
5
+ */
6
+ export declare function formatPrometheusMetrics(data: readonly ServiceUsageData[]): Promise<string>;
@@ -0,0 +1,20 @@
1
+ import { Gauge, Registry } from "prom-client";
2
+ /**
3
+ * Formats service usage data as Prometheus text exposition using prom-client.
4
+ * Emits a gauge `axusage_utilization_percent{service,window}` per window.
5
+ */
6
+ export async function formatPrometheusMetrics(data) {
7
+ const registry = new Registry();
8
+ const gauge = new Gauge({
9
+ name: "axusage_utilization_percent",
10
+ help: "Current utilization percentage by service/window",
11
+ labelNames: ["service", "window"],
12
+ registers: [registry],
13
+ });
14
+ for (const entry of data) {
15
+ for (const w of entry.windows) {
16
+ gauge.set({ service: entry.service, window: w.name }, w.utilization);
17
+ }
18
+ }
19
+ return registry.metrics();
20
+ }
@@ -0,0 +1,18 @@
1
+ import type { ServiceUsageData } from "../types/domain.js";
2
+ /**
3
+ * Formats complete service usage data for human-readable display
4
+ */
5
+ export declare function formatServiceUsageData(data: ServiceUsageData): string;
6
+ /**
7
+ * Converts service usage data to a plain JSON-serializable object
8
+ */
9
+ export declare function toJsonObject(data: ServiceUsageData): unknown;
10
+ /**
11
+ * Formats service usage data as JSON string
12
+ */
13
+ export declare function formatServiceUsageDataAsJson(data: ServiceUsageData): string;
14
+ /**
15
+ * Formats multiple services' usage data as TSV with header.
16
+ * One row per usage window, tab-delimited, UPPERCASE headers.
17
+ */
18
+ export declare function formatServiceUsageAsTsv(data: ServiceUsageData[]): string;
@@ -0,0 +1,120 @@
1
+ import chalk from "chalk";
2
+ import { calculateUsageRate } from "./calculate-usage-rate.js";
3
+ import { classifyUsageRate } from "./classify-usage-rate.js";
4
+ /**
5
+ * Formats a utilization value as a percentage string
6
+ */
7
+ function formatUtilization(utilization) {
8
+ return `${utilization.toFixed(2)}%`;
9
+ }
10
+ /**
11
+ * Formats a Date as a human-readable date string
12
+ */
13
+ function formatResetTime(date) {
14
+ return date ? date.toLocaleString() : "Not available";
15
+ }
16
+ /**
17
+ * Gets color for utilization based on usage rate
18
+ * Green: on track or under (rate ≤ 1.0)
19
+ * Yellow: slightly over budget (1.0 < rate ≤ 1.5)
20
+ * Red: significantly over budget (rate > 1.5)
21
+ */
22
+ function getUtilizationColor(rate) {
23
+ if (rate === undefined)
24
+ return chalk.gray;
25
+ const bucket = classifyUsageRate(rate);
26
+ if (bucket === "red")
27
+ return chalk.red;
28
+ if (bucket === "yellow")
29
+ return chalk.yellow;
30
+ return chalk.green;
31
+ }
32
+ /**
33
+ * Formats a single usage window for display
34
+ */
35
+ function formatUsageWindow(window) {
36
+ const utilizationString = formatUtilization(window.utilization);
37
+ const rate = calculateUsageRate(window.utilization, window.resetsAt, window.periodDurationMs);
38
+ const coloredUtilization = getUtilizationColor(rate)(utilizationString);
39
+ const resetTime = formatResetTime(window.resetsAt);
40
+ // Build full display string for rate to keep formatting consistent
41
+ const rateDisplay = rate === undefined ? "Not available" : `${rate.toFixed(2)}x rate`;
42
+ return `${chalk.bold(window.name)}:
43
+ Utilization: ${coloredUtilization} (${rateDisplay})
44
+ Resets at: ${resetTime}`;
45
+ }
46
+ /**
47
+ * Formats complete service usage data for human-readable display
48
+ */
49
+ export function formatServiceUsageData(data) {
50
+ const header = [
51
+ chalk.cyan.bold(`=== ${data.service} Usage ===`),
52
+ data.planType ? chalk.gray(`Plan: ${data.planType}`) : undefined,
53
+ data.metadata?.limitReached === true
54
+ ? chalk.red("⚠ Rate limit reached")
55
+ : data.metadata?.allowed === false
56
+ ? chalk.red("⚠ Usage not allowed")
57
+ : undefined,
58
+ ]
59
+ .filter(Boolean)
60
+ .join("\n");
61
+ const windows = data.windows
62
+ .map((window) => formatUsageWindow(window))
63
+ .join("\n\n");
64
+ return `${header}\n\n${windows}`;
65
+ }
66
+ /**
67
+ * Converts service usage data to a plain JSON-serializable object
68
+ */
69
+ export function toJsonObject(data) {
70
+ return {
71
+ service: data.service,
72
+ planType: data.planType,
73
+ windows: data.windows.map((w) => ({
74
+ name: w.name,
75
+ utilization: w.utilization,
76
+ resetsAt: w.resetsAt?.toISOString(),
77
+ periodDurationMs: w.periodDurationMs,
78
+ })),
79
+ metadata: data.metadata,
80
+ };
81
+ }
82
+ /**
83
+ * Formats service usage data as JSON string
84
+ */
85
+ export function formatServiceUsageDataAsJson(data) {
86
+ // eslint-disable-next-line unicorn/no-null -- JSON.stringify requires null for no replacer
87
+ return JSON.stringify(toJsonObject(data), null, 2);
88
+ }
89
+ const TSV_HEADER = "SERVICE\tPLAN\tWINDOW\tUTILIZATION\tRATE\tRESETS_AT";
90
+ /**
91
+ * Sanitizes a string for TSV output by replacing tabs and newlines with spaces.
92
+ */
93
+ function sanitizeForTsv(value) {
94
+ return value.replaceAll(/[\t\n\r]/gu, " ");
95
+ }
96
+ /**
97
+ * Formats a single service's usage data as TSV rows (no header).
98
+ * One row per usage window.
99
+ */
100
+ function formatServiceUsageRowsAsTsv(data) {
101
+ return data.windows.map((w) => {
102
+ const rate = calculateUsageRate(w.utilization, w.resetsAt, w.periodDurationMs);
103
+ return [
104
+ sanitizeForTsv(data.service),
105
+ sanitizeForTsv(data.planType ?? "-"),
106
+ sanitizeForTsv(w.name),
107
+ w.utilization.toFixed(2),
108
+ rate?.toFixed(2) ?? "-",
109
+ w.resetsAt?.toISOString() ?? "-",
110
+ ].join("\t");
111
+ });
112
+ }
113
+ /**
114
+ * Formats multiple services' usage data as TSV with header.
115
+ * One row per usage window, tab-delimited, UPPERCASE headers.
116
+ */
117
+ export function formatServiceUsageAsTsv(data) {
118
+ const rows = data.flatMap((d) => formatServiceUsageRowsAsTsv(d));
119
+ return [TSV_HEADER, ...rows].join("\n");
120
+ }
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "axusage",
3
+ "author": "Łukasz Jerciński",
4
+ "license": "MIT",
5
+ "version": "2.0.0",
6
+ "description": "Monitor API usage across Claude, ChatGPT, GitHub Copilot, and Gemini from a single CLI",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Jercik/axusage.git"
10
+ },
11
+ "type": "module",
12
+ "bin": {
13
+ "axusage": "bin/axusage"
14
+ },
15
+ "files": [
16
+ "bin/",
17
+ "dist/",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "keywords": [
22
+ "ai",
23
+ "usage",
24
+ "analytics",
25
+ "cli",
26
+ "claude",
27
+ "chatgpt",
28
+ "gemini",
29
+ "copilot",
30
+ "llm",
31
+ "monitoring"
32
+ ],
33
+ "engines": {
34
+ "node": ">=22.14.0"
35
+ },
36
+ "dependencies": {
37
+ "@commander-js/extra-typings": "^14.0.0",
38
+ "axauth": "^1.0.0",
39
+ "chalk": "^5.6.2",
40
+ "commander": "^14.0.2",
41
+ "env-paths": "^3.0.0",
42
+ "playwright": "^1.57.0",
43
+ "prom-client": "^15.1.3",
44
+ "trash": "^10.0.1",
45
+ "zod": "^4.2.0"
46
+ },
47
+ "devDependencies": {
48
+ "@eslint/compat": "^2.0.0",
49
+ "@eslint/js": "^9.39.2",
50
+ "@total-typescript/ts-reset": "^0.6.1",
51
+ "@types/node": "^25.0.2",
52
+ "@vitest/coverage-v8": "^4.0.15",
53
+ "@vitest/eslint-plugin": "^1.5.2",
54
+ "eslint": "^9.39.2",
55
+ "eslint-config-prettier": "^10.1.8",
56
+ "eslint-plugin-unicorn": "^62.0.0",
57
+ "fta-check": "^1.5.0",
58
+ "fta-cli": "^3.0.0",
59
+ "globals": "^16.5.0",
60
+ "knip": "^5.73.4",
61
+ "prettier": "3.7.4",
62
+ "semantic-release": "^25.0.2",
63
+ "typescript": "^5.9.3",
64
+ "typescript-eslint": "^8.49.0",
65
+ "vitest": "^4.0.15"
66
+ },
67
+ "scripts": {
68
+ "postinstall": "playwright install chromium",
69
+ "build": "tsc -p tsconfig.app.json",
70
+ "clean": "rm -rf dist *.tsbuildinfo",
71
+ "format": "prettier --write .",
72
+ "format:check": "prettier --check .",
73
+ "fta": "fta-check",
74
+ "knip": "knip",
75
+ "lint": "eslint",
76
+ "rebuild": "pnpm run clean && pnpm run build",
77
+ "start": "pnpm run rebuild && node bin/axusage",
78
+ "test": "vitest run",
79
+ "test:coverage": "vitest run --coverage",
80
+ "test:watch": "vitest",
81
+ "typecheck": "tsc -b --noEmit"
82
+ }
83
+ }