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,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
|
+
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
|
+
}
|