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,56 @@
1
+ import { ApiError } from "../types/domain.js";
2
+ import { GitHubCopilotUsageResponse as GitHubCopilotUsageResponseSchema } from "../types/github-copilot.js";
3
+ import { toServiceUsageData } from "./parse-github-copilot-usage.js";
4
+ import { acquireAuthManager, releaseAuthManager, } from "../services/shared-browser-auth-manager.js";
5
+ // Copilot web fetches entitlements from this endpoint (requires GitHub session cookies)
6
+ const API_URL = "https://github.com/github-copilot/chat/entitlement";
7
+ /** Functional core is extracted to ./parse-github-copilot-usage.ts */
8
+ /**
9
+ * GitHub Copilot service adapter
10
+ */
11
+ export const githubCopilotAdapter = {
12
+ name: "GitHub Copilot",
13
+ async fetchUsage() {
14
+ const manager = acquireAuthManager();
15
+ try {
16
+ if (!manager.hasAuth("github-copilot")) {
17
+ return {
18
+ ok: false,
19
+ error: new ApiError("No saved authentication for github-copilot. Run 'axusage auth setup github-copilot' first."),
20
+ };
21
+ }
22
+ const body = await manager.makeAuthenticatedRequest("github-copilot", API_URL);
23
+ const data = JSON.parse(body);
24
+ const parseResult = GitHubCopilotUsageResponseSchema.safeParse(data);
25
+ if (!parseResult.success) {
26
+ return {
27
+ ok: false,
28
+ error: new ApiError(`Invalid response format: ${parseResult.error.message}`, undefined, data),
29
+ };
30
+ }
31
+ try {
32
+ return {
33
+ ok: true,
34
+ value: toServiceUsageData(parseResult.data),
35
+ };
36
+ }
37
+ catch (error) {
38
+ return {
39
+ ok: false,
40
+ error: new ApiError(error instanceof Error
41
+ ? error.message
42
+ : "Unable to parse GitHub Copilot reset date", undefined, parseResult.data.quotas.resetDate),
43
+ };
44
+ }
45
+ }
46
+ catch (error) {
47
+ return {
48
+ ok: false,
49
+ error: new ApiError(`Browser authentication failed: ${error instanceof Error ? error.message : String(error)}`),
50
+ };
51
+ }
52
+ finally {
53
+ await releaseAuthManager();
54
+ }
55
+ },
56
+ };
@@ -0,0 +1,15 @@
1
+ import type { ServiceUsageData } from "../types/domain.js";
2
+ import type { ChatGPTUsageResponse, ChatGPTRateLimitWindow } from "../types/chatgpt.js";
3
+ /**
4
+ * Converts a ChatGPT rate limit window to common usage window
5
+ */
6
+ export declare function toUsageWindow(name: string, window: ChatGPTRateLimitWindow): {
7
+ name: string;
8
+ utilization: number;
9
+ resetsAt: Date;
10
+ periodDurationMs: number;
11
+ };
12
+ /**
13
+ * Converts ChatGPT response to common domain model
14
+ */
15
+ export declare function toServiceUsageData(response: ChatGPTUsageResponse): ServiceUsageData;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Converts a ChatGPT rate limit window to common usage window
3
+ */
4
+ export function toUsageWindow(name, window) {
5
+ return {
6
+ name,
7
+ utilization: window.used_percent,
8
+ resetsAt: new Date(window.reset_at * 1000),
9
+ periodDurationMs: window.limit_window_seconds * 1000,
10
+ };
11
+ }
12
+ /**
13
+ * Converts ChatGPT response to common domain model
14
+ */
15
+ export function toServiceUsageData(response) {
16
+ return {
17
+ service: "ChatGPT",
18
+ planType: response.plan_type,
19
+ windows: [
20
+ toUsageWindow("Primary Window (~5 hours)", response.rate_limit.primary_window),
21
+ toUsageWindow("Secondary Window (~7 days)", response.rate_limit.secondary_window),
22
+ ],
23
+ metadata: {
24
+ allowed: response.rate_limit.allowed,
25
+ limitReached: response.rate_limit.limit_reached,
26
+ },
27
+ };
28
+ }
@@ -0,0 +1,16 @@
1
+ import type { ServiceUsageData } from "../types/domain.js";
2
+ import type { UsageResponse } from "../types/usage.js";
3
+ /**
4
+ * Period durations for Claude usage windows
5
+ */
6
+ export declare const CLAUDE_PERIOD_DURATIONS: {
7
+ readonly five_hour: number;
8
+ readonly seven_day: number;
9
+ readonly seven_day_oauth_apps: number;
10
+ readonly seven_day_opus: number;
11
+ readonly seven_day_sonnet: number;
12
+ };
13
+ /**
14
+ * Converts Claude response to common domain model
15
+ */
16
+ export declare function toServiceUsageData(response: UsageResponse): ServiceUsageData;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Period durations for Claude usage windows
3
+ */
4
+ export const CLAUDE_PERIOD_DURATIONS = {
5
+ five_hour: 5 * 60 * 60 * 1000,
6
+ seven_day: 7 * 24 * 60 * 60 * 1000,
7
+ seven_day_oauth_apps: 7 * 24 * 60 * 60 * 1000,
8
+ seven_day_opus: 7 * 24 * 60 * 60 * 1000,
9
+ seven_day_sonnet: 7 * 24 * 60 * 60 * 1000,
10
+ };
11
+ /**
12
+ * Parses a reset timestamp into a Date while tolerating missing values.
13
+ * Returns undefined for absent or invalid timestamps to signal "not available".
14
+ */
15
+ const parseResetTimestamp = (timestamp) => {
16
+ if (!timestamp)
17
+ return undefined;
18
+ const date = new Date(timestamp);
19
+ // Number.isNaN checks whether getTime() returned NaN (indicating an invalid date).
20
+ return Number.isNaN(date.getTime()) ? undefined : date;
21
+ };
22
+ // Intentionally avoid sentinel timestamps for missing reset dates.
23
+ // Missing values propagate as undefined so the caller can surface "unknown" states.
24
+ /**
25
+ * Converts Claude response to common domain model
26
+ */
27
+ export function toServiceUsageData(response) {
28
+ return {
29
+ service: "Claude",
30
+ windows: [
31
+ {
32
+ name: "5-Hour Usage",
33
+ utilization: response.five_hour.utilization,
34
+ resetsAt: parseResetTimestamp(response.five_hour.resets_at),
35
+ periodDurationMs: CLAUDE_PERIOD_DURATIONS.five_hour,
36
+ },
37
+ {
38
+ name: "7-Day Usage",
39
+ utilization: response.seven_day.utilization,
40
+ resetsAt: parseResetTimestamp(response.seven_day.resets_at),
41
+ periodDurationMs: CLAUDE_PERIOD_DURATIONS.seven_day,
42
+ },
43
+ ...(response.seven_day_oauth_apps
44
+ ? [
45
+ {
46
+ name: "7-Day OAuth Apps",
47
+ utilization: response.seven_day_oauth_apps.utilization,
48
+ resetsAt: parseResetTimestamp(response.seven_day_oauth_apps.resets_at),
49
+ periodDurationMs: CLAUDE_PERIOD_DURATIONS.seven_day_oauth_apps,
50
+ },
51
+ ]
52
+ : []),
53
+ ...(response.seven_day_opus
54
+ ? [
55
+ {
56
+ name: "7-Day Opus Usage",
57
+ utilization: response.seven_day_opus.utilization,
58
+ resetsAt: parseResetTimestamp(response.seven_day_opus.resets_at),
59
+ periodDurationMs: CLAUDE_PERIOD_DURATIONS.seven_day_opus,
60
+ },
61
+ ]
62
+ : []),
63
+ ...(response.seven_day_sonnet
64
+ ? [
65
+ {
66
+ name: "7-Day Sonnet Usage",
67
+ utilization: response.seven_day_sonnet.utilization,
68
+ resetsAt: parseResetTimestamp(response.seven_day_sonnet.resets_at),
69
+ periodDurationMs: CLAUDE_PERIOD_DURATIONS.seven_day_sonnet,
70
+ },
71
+ ]
72
+ : []),
73
+ ],
74
+ };
75
+ }
@@ -0,0 +1,55 @@
1
+ import type { ServiceUsageData, UsageWindow } from "../types/domain.js";
2
+ import type { GeminiQuotaResponse, GeminiQuotaBucket } from "../types/gemini.js";
3
+ /**
4
+ * Model quota after grouping buckets by model
5
+ */
6
+ type ModelQuota = {
7
+ modelId: string;
8
+ lowestRemainingFraction: number;
9
+ resetTime: Date | undefined;
10
+ };
11
+ /**
12
+ * Quota pool containing models that share the same quota
13
+ */
14
+ type QuotaPool = {
15
+ modelIds: string[];
16
+ remainingFraction: number;
17
+ resetTime: Date | undefined;
18
+ };
19
+ /**
20
+ * Parse ISO8601 timestamp to Date
21
+ */
22
+ export declare function parseResetTime(resetTimeString?: string): Date | undefined;
23
+ /**
24
+ * Format model ID for display
25
+ * "gemini-2.5-pro" -> "Gemini 2.5 Pro"
26
+ */
27
+ export declare function formatModelName(modelId: string): string;
28
+ /**
29
+ * Format multiple model names for display
30
+ * Single model: "Gemini 2.5 Pro"
31
+ * Multiple models: "Gemini 2.0 Flash, 2.5 Flash, 2.5 Flash Lite"
32
+ */
33
+ export declare function formatPoolName(modelIds: string[]): string;
34
+ /**
35
+ * Group quota buckets by model, keeping lowest remaining fraction per model
36
+ * (input tokens are usually more restrictive than output)
37
+ */
38
+ export declare function groupBucketsByModel(buckets: GeminiQuotaBucket[]): ModelQuota[];
39
+ /**
40
+ * Group models that share the same quota (same remainingFraction and resetTime)
41
+ */
42
+ export declare function groupByQuotaPool(modelQuotas: ModelQuota[]): QuotaPool[];
43
+ /**
44
+ * Convert quota pool to usage window
45
+ */
46
+ export declare function poolToUsageWindow(pool: QuotaPool): UsageWindow;
47
+ /**
48
+ * Convert model quota to usage window
49
+ */
50
+ export declare function toUsageWindow(quota: ModelQuota): UsageWindow;
51
+ /**
52
+ * Convert Gemini quota response to common domain model
53
+ */
54
+ export declare function toServiceUsageData(response: GeminiQuotaResponse, planType?: string): ServiceUsageData;
55
+ export {};
@@ -0,0 +1,151 @@
1
+ // Default period duration - Gemini quotas typically reset daily
2
+ const DEFAULT_PERIOD_MS = 24 * 60 * 60 * 1000; // 24 hours
3
+ /**
4
+ * Parse ISO8601 timestamp to Date
5
+ */
6
+ export function parseResetTime(resetTimeString) {
7
+ if (!resetTimeString) {
8
+ return undefined;
9
+ }
10
+ const date = new Date(resetTimeString);
11
+ return Number.isNaN(date.getTime()) ? undefined : date;
12
+ }
13
+ // Gemini quotas reset daily, so period is always 24 hours
14
+ const PERIOD_DURATION_MS = DEFAULT_PERIOD_MS;
15
+ /**
16
+ * Format model ID for display
17
+ * "gemini-2.5-pro" -> "Gemini 2.5 Pro"
18
+ */
19
+ export function formatModelName(modelId) {
20
+ return modelId
21
+ .split("-")
22
+ .map((part) => {
23
+ // Capitalize first letter, keep version numbers as-is
24
+ if (/^\d/u.test(part)) {
25
+ return part;
26
+ }
27
+ return part.charAt(0).toUpperCase() + part.slice(1);
28
+ })
29
+ .join(" ");
30
+ }
31
+ /**
32
+ * Format multiple model names for display
33
+ * Single model: "Gemini 2.5 Pro"
34
+ * Multiple models: "Gemini 2.0 Flash, 2.5 Flash, 2.5 Flash Lite"
35
+ */
36
+ export function formatPoolName(modelIds) {
37
+ if (modelIds.length === 0) {
38
+ return "";
39
+ }
40
+ if (modelIds.length === 1) {
41
+ // Length check guarantees element exists, but noUncheckedIndexedAccess requires assertion
42
+ return formatModelName(modelIds[0]);
43
+ }
44
+ // For multiple models, use shortened format: "Gemini 2.0 Flash, 2.5 Flash, 2.5 Flash Lite"
45
+ const formattedNames = modelIds.map((id) => formatModelName(id));
46
+ // Extract common prefix (e.g., "Gemini") if all names share it
47
+ const firstWords = formattedNames.map((name) => name.split(" ")[0] ?? "");
48
+ const firstPrefix = firstWords[0] ?? "";
49
+ const allSamePrefix = firstPrefix !== "" && firstWords.every((word) => word === firstPrefix);
50
+ if (allSamePrefix) {
51
+ const suffixes = formattedNames.map((name) => name.slice(firstPrefix.length + 1));
52
+ // Fallback if any suffix is empty (e.g. ["Gemini", "Gemini Pro"] -> would be "Gemini , Pro")
53
+ if (suffixes.some((s) => !s)) {
54
+ return formattedNames.join(", ");
55
+ }
56
+ return `${firstPrefix} ${suffixes.join(", ")}`;
57
+ }
58
+ return formattedNames.join(", ");
59
+ }
60
+ /**
61
+ * Group quota buckets by model, keeping lowest remaining fraction per model
62
+ * (input tokens are usually more restrictive than output)
63
+ */
64
+ export function groupBucketsByModel(buckets) {
65
+ const modelMap = new Map();
66
+ for (const bucket of buckets) {
67
+ const existing = modelMap.get(bucket.modelId);
68
+ if (!existing ||
69
+ bucket.remainingFraction < existing.lowestRemainingFraction) {
70
+ modelMap.set(bucket.modelId, {
71
+ modelId: bucket.modelId,
72
+ lowestRemainingFraction: bucket.remainingFraction,
73
+ resetTime: parseResetTime(bucket.resetTime),
74
+ });
75
+ }
76
+ }
77
+ return [...modelMap.values()];
78
+ }
79
+ /**
80
+ * Create a unique key for a quota pool based on remaining fraction and reset time
81
+ */
82
+ function createPoolKey(remainingFraction, resetTime) {
83
+ const resetTimeKey = resetTime?.toISOString() ?? "none";
84
+ return `${remainingFraction.toFixed(6)}|${resetTimeKey}`;
85
+ }
86
+ /**
87
+ * Group models that share the same quota (same remainingFraction and resetTime)
88
+ */
89
+ export function groupByQuotaPool(modelQuotas) {
90
+ const poolMap = new Map();
91
+ for (const quota of modelQuotas) {
92
+ const key = createPoolKey(quota.lowestRemainingFraction, quota.resetTime);
93
+ const existing = poolMap.get(key);
94
+ if (existing) {
95
+ existing.modelIds.push(quota.modelId);
96
+ }
97
+ else {
98
+ poolMap.set(key, {
99
+ modelIds: [quota.modelId],
100
+ remainingFraction: quota.lowestRemainingFraction,
101
+ resetTime: quota.resetTime,
102
+ });
103
+ }
104
+ }
105
+ // Sort model IDs within each pool for consistent ordering
106
+ for (const pool of poolMap.values()) {
107
+ pool.modelIds.sort();
108
+ }
109
+ return [...poolMap.values()];
110
+ }
111
+ /**
112
+ * Convert quota pool to usage window
113
+ */
114
+ export function poolToUsageWindow(pool) {
115
+ // Convert remaining fraction (0-1) to utilization percentage (0-100)
116
+ // remaining 0.6 means 40% utilized
117
+ const utilization = (1 - pool.remainingFraction) * 100;
118
+ return {
119
+ name: formatPoolName(pool.modelIds),
120
+ utilization: Math.round(utilization * 100) / 100, // Round to 2 decimal places
121
+ resetsAt: pool.resetTime,
122
+ periodDurationMs: PERIOD_DURATION_MS,
123
+ };
124
+ }
125
+ /**
126
+ * Convert model quota to usage window
127
+ */
128
+ export function toUsageWindow(quota) {
129
+ // Convert remaining fraction (0-1) to utilization percentage (0-100)
130
+ // remaining 0.6 means 40% utilized
131
+ const utilization = (1 - quota.lowestRemainingFraction) * 100;
132
+ return {
133
+ name: formatModelName(quota.modelId),
134
+ utilization: Math.round(utilization * 100) / 100, // Round to 2 decimal places
135
+ resetsAt: quota.resetTime,
136
+ periodDurationMs: PERIOD_DURATION_MS,
137
+ };
138
+ }
139
+ /**
140
+ * Convert Gemini quota response to common domain model
141
+ */
142
+ export function toServiceUsageData(response, planType) {
143
+ const modelQuotas = groupBucketsByModel(response.buckets);
144
+ const quotaPools = groupByQuotaPool(modelQuotas);
145
+ const windows = quotaPools.map((pool) => poolToUsageWindow(pool));
146
+ return {
147
+ service: "Gemini",
148
+ planType,
149
+ windows,
150
+ };
151
+ }
@@ -0,0 +1,23 @@
1
+ import type { ServiceUsageData } from "../types/domain.js";
2
+ import type { GitHubCopilotUsageResponse } from "../types/github-copilot.js";
3
+ /**
4
+ * Parses GitHub reset date (YYYY-MM-DD) into a UTC Date
5
+ */
6
+ export declare function parseResetDate(resetDateString: string): Date;
7
+ /**
8
+ * Calculates monthly period duration ending at the reset date
9
+ *
10
+ * Determines the period start by going back one month from the reset
11
+ * date and clamping the day to the last valid day of that previous
12
+ * month. This handles edge cases like Jan 31 → Feb 28/29 where the
13
+ * target month has fewer days than the reset date's day component.
14
+ *
15
+ * Example:
16
+ * For resetDate = 2025-03-31, calculates duration from 2025-02-28 (clamped)
17
+ * to 2025-03-31.
18
+ */
19
+ export declare function calculatePeriodDuration(resetDate: Date): number;
20
+ /**
21
+ * Converts GitHub Copilot response to common domain model
22
+ */
23
+ export declare function toServiceUsageData(response: GitHubCopilotUsageResponse): ServiceUsageData;
@@ -0,0 +1,78 @@
1
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
2
+ /**
3
+ * Parses GitHub reset date (YYYY-MM-DD) into a UTC Date
4
+ */
5
+ export function parseResetDate(resetDateString) {
6
+ const parts = resetDateString.split("-");
7
+ if (parts.length !== 3) {
8
+ throw new Error(`Invalid reset date format: ${resetDateString}`);
9
+ }
10
+ const [yearString, monthString, dayString] = parts;
11
+ if (!yearString || !monthString || !dayString) {
12
+ throw new Error(`Invalid reset date components: ${resetDateString}`);
13
+ }
14
+ const year = Number(yearString);
15
+ const month = Number(monthString);
16
+ const day = Number(dayString);
17
+ if (Number.isNaN(year) ||
18
+ Number.isNaN(month) ||
19
+ Number.isNaN(day) ||
20
+ month < 1 ||
21
+ month > 12 ||
22
+ day < 1 ||
23
+ day > 31) {
24
+ throw new Error(`Invalid reset date components: ${resetDateString}`);
25
+ }
26
+ return new Date(Date.UTC(year, month - 1, day, 0, 0, 0));
27
+ }
28
+ /**
29
+ * Calculates monthly period duration ending at the reset date
30
+ *
31
+ * Determines the period start by going back one month from the reset
32
+ * date and clamping the day to the last valid day of that previous
33
+ * month. This handles edge cases like Jan 31 → Feb 28/29 where the
34
+ * target month has fewer days than the reset date's day component.
35
+ *
36
+ * Example:
37
+ * For resetDate = 2025-03-31, calculates duration from 2025-02-28 (clamped)
38
+ * to 2025-03-31.
39
+ */
40
+ export function calculatePeriodDuration(resetDate) {
41
+ const periodEnd = resetDate.getTime();
42
+ // Determine previous month and clamp day to its last day
43
+ const year = resetDate.getUTCFullYear();
44
+ const month = resetDate.getUTCMonth(); // 0-based
45
+ const day = resetDate.getUTCDate();
46
+ // First day of current month in UTC
47
+ const firstOfCurrentMonth = Date.UTC(year, month, 1, 0, 0, 0);
48
+ // Last day of previous month: subtract 1 day from first of current month
49
+ const lastPreviousMonthDate = new Date(firstOfCurrentMonth - MS_PER_DAY);
50
+ const lastPreviousMonthDay = lastPreviousMonthDate.getUTCDate();
51
+ const previousMonth = lastPreviousMonthDate.getUTCMonth();
52
+ const previousYear = lastPreviousMonthDate.getUTCFullYear();
53
+ const targetDay = Math.min(day, lastPreviousMonthDay);
54
+ const periodStart = Date.UTC(previousYear, previousMonth, targetDay, 0, 0, 0);
55
+ return Math.max(periodEnd - periodStart, 0);
56
+ }
57
+ /**
58
+ * Converts GitHub Copilot response to common domain model
59
+ */
60
+ export function toServiceUsageData(response) {
61
+ const resetDate = parseResetDate(response.quotas.resetDate);
62
+ const periodDurationMs = calculatePeriodDuration(resetDate);
63
+ const used = response.quotas.limits.premiumInteractions -
64
+ response.quotas.remaining.premiumInteractions;
65
+ const utilization = (used / response.quotas.limits.premiumInteractions) * 100;
66
+ return {
67
+ service: "GitHub Copilot",
68
+ planType: response.plan,
69
+ windows: [
70
+ {
71
+ name: "Monthly Premium Interactions",
72
+ utilization: Math.round(utilization * 100) / 100,
73
+ resetsAt: resetDate,
74
+ periodDurationMs,
75
+ },
76
+ ],
77
+ };
78
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import { Command, Option } from "@commander-js/extra-typings";
3
+ import packageJson from "../package.json" with { type: "json" };
4
+ import { usageCommand } from "./commands/usage-command.js";
5
+ import { authSetupCommand } from "./commands/auth-setup-command.js";
6
+ import { authStatusCommand } from "./commands/auth-status-command.js";
7
+ import { authClearCommand } from "./commands/auth-clear-command.js";
8
+ import { getAvailableServices } from "./services/service-adapter-registry.js";
9
+ import { installAuthManagerCleanup } from "./services/shared-browser-auth-manager.js";
10
+ import { getBrowserContextsDirectory } from "./services/app-paths.js";
11
+ const program = new Command()
12
+ .name(packageJson.name)
13
+ .description(packageJson.description)
14
+ .version(packageJson.version)
15
+ .showHelpAfterError("(add --help for additional information)")
16
+ .showSuggestionAfterError()
17
+ .helpCommand(false)
18
+ .addHelpText("after", `\nExamples:\n # Fetch usage for all services\n ${packageJson.name}\n\n # JSON output for a single service\n ${packageJson.name} --service claude --format=json\n\n # TSV output for piping to cut, awk, sort\n ${packageJson.name} --format=tsv | tail -n +2 | awk -F'\\t' '{print $1, $4"%"}'\n\n # Filter Prometheus metrics with standard tools\n ${packageJson.name} --format=prometheus | grep axusage_utilization_percent\n`);
19
+ // Ensure browser resources are cleaned when process exits
20
+ installAuthManagerCleanup();
21
+ // Usage command (default)
22
+ program
23
+ .command("usage", { isDefault: true })
24
+ .description("Fetch API usage statistics (defaults to all: Claude, ChatGPT, GitHub Copilot)")
25
+ .option("-s, --service <service>", `Service to query (${getAvailableServices().join(", ")}, all) - defaults to all`)
26
+ .option("-i, --interactive", "allow interactive re-authentication during usage fetch")
27
+ .addOption(new Option("-o, --format <format>", "Output format")
28
+ .choices(["text", "tsv", "json", "prometheus"])
29
+ .default("text"))
30
+ .addHelpText("after", `\nExamples:\n # Query all services (default)\n ${packageJson.name}\n\n # Query a single service\n ${packageJson.name} --service claude\n\n # TSV output for piping to Unix tools\n ${packageJson.name} --format=tsv | tail -n +2 | cut -f1,4\n\n # Filter Prometheus metrics\n ${packageJson.name} --format=prometheus | grep claude\n`)
31
+ .action(async (options) => {
32
+ await usageCommand(options);
33
+ });
34
+ // Auth command group
35
+ const auth = program
36
+ .command("auth")
37
+ .description("Manage authentication for services")
38
+ .helpCommand(false)
39
+ .addHelpText("after", `\nStorage: ${getBrowserContextsDirectory()}\n(respects XDG_DATA_HOME and platform defaults)`);
40
+ auth
41
+ .command("setup")
42
+ .description("Set up browser-based authentication for a service")
43
+ .argument("<service>", "Service to authenticate (claude, chatgpt, github-copilot)")
44
+ .action(async (service) => {
45
+ await authSetupCommand({ service });
46
+ });
47
+ auth
48
+ .command("status")
49
+ .description("Check authentication status for services")
50
+ .option("-s, --service <service>", "Check status for specific service")
51
+ .action((options) => {
52
+ authStatusCommand(options);
53
+ });
54
+ auth
55
+ .command("clear")
56
+ .description("Clear saved browser authentication for a service")
57
+ .argument("<service>", "Service to clear (claude, chatgpt, github-copilot)")
58
+ .action(async (service) => {
59
+ await authClearCommand({ service });
60
+ });
61
+ try {
62
+ await program.parseAsync();
63
+ }
64
+ catch (error) {
65
+ const message = error instanceof Error ? error.message : String(error);
66
+ console.error(message);
67
+ if (process.exitCode === undefined)
68
+ process.exitCode = 1;
69
+ }
@@ -0,0 +1,5 @@
1
+ type AuthClearOptions = {
2
+ readonly service?: string;
3
+ };
4
+ export declare function authClearCommand(options: AuthClearOptions): Promise<void>;
5
+ export {};
@@ -0,0 +1,25 @@
1
+ import chalk from "chalk";
2
+ import { existsSync } from "node:fs";
3
+ import trash from "trash";
4
+ import { validateService } from "../services/supported-service.js";
5
+ import { getAuthMetaPathFor, getStorageStatePathFor, } from "../services/auth-storage-path.js";
6
+ import { getBrowserContextsDirectory } from "../services/app-paths.js";
7
+ export async function authClearCommand(options) {
8
+ const service = validateService(options.service);
9
+ const dataDirectory = getBrowserContextsDirectory();
10
+ const storage = getStorageStatePathFor(dataDirectory, service);
11
+ const meta = getAuthMetaPathFor(dataDirectory, service);
12
+ try {
13
+ const targets = [storage, meta].filter((p) => existsSync(p));
14
+ if (targets.length === 0) {
15
+ console.error(chalk.gray(`\nNo saved authentication found for ${service}.`));
16
+ return;
17
+ }
18
+ await trash(targets, { glob: false });
19
+ console.error(chalk.green(`\n✓ Cleared authentication for ${service}`));
20
+ }
21
+ catch (error) {
22
+ console.error(chalk.red(`\n✗ Failed to clear authentication for ${service}: ${error instanceof Error ? error.message : String(error)}`));
23
+ process.exitCode = 1;
24
+ }
25
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Options for the auth setup command
3
+ */
4
+ type AuthSetupOptions = {
5
+ readonly service?: string;
6
+ };
7
+ /**
8
+ * Set up authentication for a service
9
+ */
10
+ export declare function authSetupCommand(options: AuthSetupOptions): Promise<void>;
11
+ export {};
@@ -0,0 +1,45 @@
1
+ import chalk from "chalk";
2
+ import { BrowserAuthManager } from "../services/browser-auth-manager.js";
3
+ import { validateService } from "../services/supported-service.js";
4
+ /**
5
+ * Set up authentication for a service
6
+ */
7
+ export async function authSetupCommand(options) {
8
+ const service = validateService(options.service);
9
+ // CLI-based auth - users should run the native CLI directly
10
+ if (service === "gemini") {
11
+ console.error(chalk.yellow("\nGemini uses CLI-based authentication managed by the Gemini CLI."));
12
+ console.error(chalk.gray("\nTo authenticate, run:"));
13
+ console.error(chalk.cyan(" gemini"));
14
+ console.error(chalk.gray("\nThe Gemini CLI will guide you through the OAuth login process.\n"));
15
+ return;
16
+ }
17
+ if (service === "claude") {
18
+ console.error(chalk.yellow("\nClaude uses CLI-based authentication managed by Claude Code."));
19
+ console.error(chalk.gray("\nTo authenticate, run:"));
20
+ console.error(chalk.cyan(" claude"));
21
+ console.error(chalk.gray("\nClaude Code will guide you through authentication.\n"));
22
+ return;
23
+ }
24
+ if (service === "chatgpt") {
25
+ console.error(chalk.yellow("\nChatGPT uses CLI-based authentication managed by Codex."));
26
+ console.error(chalk.gray("\nTo authenticate, run:"));
27
+ console.error(chalk.cyan(" codex"));
28
+ console.error(chalk.gray("\nCodex will guide you through authentication.\n"));
29
+ return;
30
+ }
31
+ const manager = new BrowserAuthManager({ headless: false });
32
+ try {
33
+ console.error(chalk.blue(`\nSetting up authentication for ${service}...\n`));
34
+ await manager.setupAuth(service);
35
+ console.error(chalk.green(`\n✓ Authentication for ${service} is complete!`));
36
+ console.error(chalk.gray(`\nYou can now run: ${chalk.cyan(`axusage usage --service ${service}`)}`));
37
+ }
38
+ catch (error) {
39
+ console.error(chalk.red(`\n✗ Failed to set up authentication for ${service}: ${error instanceof Error ? error.message : String(error)}`));
40
+ process.exitCode = 1;
41
+ }
42
+ finally {
43
+ await manager.close();
44
+ }
45
+ }
@@ -0,0 +1,5 @@
1
+ type AuthStatusOptions = {
2
+ readonly service?: string;
3
+ };
4
+ export declare function authStatusCommand(options: AuthStatusOptions): void;
5
+ export {};