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.
- package/LICENSE +21 -0
- package/README.md +254 -0
- package/bin/axusage +2 -0
- package/dist/adapters/chatgpt.d.ts +8 -0
- package/dist/adapters/chatgpt.js +68 -0
- package/dist/adapters/claude.d.ts +9 -0
- package/dist/adapters/claude.js +108 -0
- package/dist/adapters/coalesce-claude-usage-response.d.ts +12 -0
- package/dist/adapters/coalesce-claude-usage-response.js +119 -0
- package/dist/adapters/gemini.d.ts +8 -0
- package/dist/adapters/gemini.js +43 -0
- package/dist/adapters/github-copilot.d.ts +6 -0
- package/dist/adapters/github-copilot.js +56 -0
- package/dist/adapters/parse-chatgpt-usage.d.ts +15 -0
- package/dist/adapters/parse-chatgpt-usage.js +28 -0
- package/dist/adapters/parse-claude-usage.d.ts +16 -0
- package/dist/adapters/parse-claude-usage.js +75 -0
- package/dist/adapters/parse-gemini-usage.d.ts +55 -0
- package/dist/adapters/parse-gemini-usage.js +151 -0
- package/dist/adapters/parse-github-copilot-usage.d.ts +23 -0
- package/dist/adapters/parse-github-copilot-usage.js +78 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +69 -0
- package/dist/commands/auth-clear-command.d.ts +5 -0
- package/dist/commands/auth-clear-command.js +25 -0
- package/dist/commands/auth-setup-command.d.ts +11 -0
- package/dist/commands/auth-setup-command.js +45 -0
- package/dist/commands/auth-status-command.d.ts +5 -0
- package/dist/commands/auth-status-command.js +25 -0
- package/dist/commands/fetch-service-usage-with-reauth.d.ts +7 -0
- package/dist/commands/fetch-service-usage-with-reauth.js +45 -0
- package/dist/commands/fetch-service-usage.d.ts +8 -0
- package/dist/commands/fetch-service-usage.js +19 -0
- package/dist/commands/run-auth-setup.d.ts +29 -0
- package/dist/commands/run-auth-setup.js +91 -0
- package/dist/commands/usage-command.d.ts +15 -0
- package/dist/commands/usage-command.js +146 -0
- package/dist/services/app-paths.d.ts +9 -0
- package/dist/services/app-paths.js +39 -0
- package/dist/services/auth-storage-path.d.ts +3 -0
- package/dist/services/auth-storage-path.js +7 -0
- package/dist/services/auth-timeouts.d.ts +4 -0
- package/dist/services/auth-timeouts.js +4 -0
- package/dist/services/browser-auth-manager.d.ts +49 -0
- package/dist/services/browser-auth-manager.js +113 -0
- package/dist/services/create-auth-context.d.ts +8 -0
- package/dist/services/create-auth-context.js +34 -0
- package/dist/services/do-setup-auth.d.ts +3 -0
- package/dist/services/do-setup-auth.js +25 -0
- package/dist/services/fetch-json-with-context.d.ts +5 -0
- package/dist/services/fetch-json-with-context.js +37 -0
- package/dist/services/gemini-api.d.ts +11 -0
- package/dist/services/gemini-api.js +109 -0
- package/dist/services/launch-chromium.d.ts +6 -0
- package/dist/services/launch-chromium.js +20 -0
- package/dist/services/persist-storage-state.d.ts +6 -0
- package/dist/services/persist-storage-state.js +16 -0
- package/dist/services/request-service.d.ts +3 -0
- package/dist/services/request-service.js +4 -0
- package/dist/services/service-adapter-registry.d.ts +18 -0
- package/dist/services/service-adapter-registry.js +26 -0
- package/dist/services/service-auth-configs.d.ts +15 -0
- package/dist/services/service-auth-configs.js +26 -0
- package/dist/services/setup-auth-flow.d.ts +3 -0
- package/dist/services/setup-auth-flow.js +40 -0
- package/dist/services/shared-browser-auth-manager.d.ts +4 -0
- package/dist/services/shared-browser-auth-manager.js +80 -0
- package/dist/services/supported-service.d.ts +6 -0
- package/dist/services/supported-service.js +16 -0
- package/dist/services/verify-session.d.ts +2 -0
- package/dist/services/verify-session.js +25 -0
- package/dist/services/wait-for-login.d.ts +5 -0
- package/dist/services/wait-for-login.js +44 -0
- package/dist/types/chatgpt.d.ts +32 -0
- package/dist/types/chatgpt.js +21 -0
- package/dist/types/domain.d.ts +57 -0
- package/dist/types/domain.js +16 -0
- package/dist/types/gemini.d.ts +31 -0
- package/dist/types/gemini.js +27 -0
- package/dist/types/github-copilot.d.ts +21 -0
- package/dist/types/github-copilot.js +27 -0
- package/dist/types/usage.d.ts +31 -0
- package/dist/types/usage.js +25 -0
- package/dist/utils/calculate-usage-rate.d.ts +9 -0
- package/dist/utils/calculate-usage-rate.js +31 -0
- package/dist/utils/classify-usage-rate.d.ts +6 -0
- package/dist/utils/classify-usage-rate.js +10 -0
- package/dist/utils/format-prometheus-metrics.d.ts +6 -0
- package/dist/utils/format-prometheus-metrics.js +20 -0
- package/dist/utils/format-service-usage.d.ts +18 -0
- package/dist/utils/format-service-usage.js +120 -0
- 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
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,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
|
+
}
|