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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Łukasz Jerciński
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,254 @@
1
+ # axusage
2
+
3
+ Monitor AI usage across Claude, ChatGPT, GitHub Copilot, and Gemini from a single command.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Install globally
9
+ npm install -g axusage
10
+
11
+ # Set up authentication (one-time setup per service)
12
+ claude
13
+ codex
14
+ gemini
15
+ axusage auth setup github-copilot
16
+
17
+ # Check authentication status
18
+ axusage auth status
19
+
20
+ # Fetch usage
21
+ axusage
22
+ ```
23
+
24
+ ## Authentication
25
+
26
+ Claude, ChatGPT, and Gemini use their respective CLI OAuth sessions. GitHub
27
+ Copilot uses browser-based authentication for persistent, long-lived sessions.
28
+
29
+ **Setup (one-time per service):**
30
+
31
+ ```bash
32
+ # Set up authentication for each service
33
+ claude
34
+ codex
35
+ gemini
36
+ axusage auth setup github-copilot
37
+
38
+ # Check authentication status
39
+ axusage auth status
40
+ ```
41
+
42
+ When you run `auth setup` for GitHub Copilot, a browser window will open.
43
+ Simply log in to GitHub as you normally would. Your authentication will be
44
+ saved and automatically used for future requests.
45
+
46
+ **Authenticated sessions directory (via [`env-paths`](https://github.com/sindresorhus/env-paths)):**
47
+
48
+ - Linux: `~/.local/share/axusage/browser-contexts/` (or `$XDG_DATA_HOME/axusage/browser-contexts/`)
49
+ - macOS: `~/Library/Application Support/axusage/browser-contexts/`
50
+ - Windows: `%LOCALAPPDATA%\axusage\Data\browser-contexts\`
51
+
52
+ You can override the location by providing `BrowserAuthConfig.dataDir`, but the CLI defaults to these platform-appropriate directories.
53
+
54
+ > **Migration from `agent-usage`:** If upgrading from the old `agent-usage` package, copy your authentication contexts:
55
+ >
56
+ > - Linux/macOS: `mkdir -p ~/.local/share/axusage/ && cp -r ~/.local/share/agent-usage/* ~/.local/share/axusage/`
57
+ > - Windows: Copy from `%LOCALAPPDATA%\agent-usage\` to `%LOCALAPPDATA%\axusage\`
58
+
59
+ Security notes:
60
+
61
+ - Files in this directory contain sensitive session data. They are created with owner-only permissions (0600 for files, 0700 for the directory) where possible.
62
+ - To revoke access for GitHub Copilot, clear saved browser auth:
63
+
64
+ ```bash
65
+ axusage auth clear github-copilot
66
+ ```
67
+
68
+ Browser installation:
69
+
70
+ - Playwright Chromium is installed automatically on `pnpm install` via a postinstall script. If this fails in your environment, install manually:
71
+
72
+ ```bash
73
+ pnpm exec playwright install chromium --with-deps
74
+ ```
75
+
76
+ **Global installation with pnpm:**
77
+
78
+ pnpm blocks postinstall scripts for global packages by default (npm runs them automatically). After installing globally, approve and run the postinstall script:
79
+
80
+ ```bash
81
+ pnpm add -g axusage
82
+ pnpm approve-builds -g # Select axusage when prompted
83
+ pnpm add -g axusage # Reinstall to run postinstall
84
+ ```
85
+
86
+ Alternatively, install the browser manually after global installation. Use the Playwright binary that ships with the global package so the browser is installed in the right location:
87
+
88
+ ```bash
89
+ pnpm add -g axusage
90
+ PLAYWRIGHT_BIN="$(pnpm root -g)/axusage/node_modules/.bin/playwright"
91
+ "$PLAYWRIGHT_BIN" install chromium --with-deps
92
+ ```
93
+
94
+ ## Usage
95
+
96
+ ```bash
97
+ # Query all services
98
+ axusage
99
+
100
+ # Allow interactive re-authentication during usage fetch
101
+ axusage --interactive
102
+
103
+ # Single service
104
+ axusage --service claude
105
+ axusage --service chatgpt
106
+ axusage --service github-copilot
107
+
108
+ # JSON output
109
+ axusage --format=json
110
+ axusage --service claude --format=json
111
+
112
+ # TSV output (parseable with cut, awk, sort)
113
+ axusage --format=tsv
114
+ ```
115
+
116
+ ## Examples
117
+
118
+ ### Extract service and utilization (TSV + awk)
119
+
120
+ ```bash
121
+ axusage --format=tsv | tail -n +2 | awk -F'\t' '{print $1, $4"%"}'
122
+ ```
123
+
124
+ ### Count windows by service (TSV + cut/sort/uniq)
125
+
126
+ ```bash
127
+ axusage --format=tsv | tail -n +2 | cut -f1 | sort | uniq -c
128
+ ```
129
+
130
+ ### Filter by utilization threshold (TSV + awk)
131
+
132
+ ```bash
133
+ axusage --format=tsv | tail -n +2 | awk -F'\t' '$4 > 50 {print $1, $3, $4"%"}'
134
+ ```
135
+
136
+ ### Extract utilization as JSON (JSON + jq)
137
+
138
+ ```bash
139
+ axusage --format=json \
140
+ | jq -r '(.results? // .) | (if type=="array" then . else [.] end) | .[] | .windows[] | [.name, (.utilization|tostring)] | @tsv'
141
+ ```
142
+
143
+ ## Output
144
+
145
+ Human-readable format shows:
146
+
147
+ - Utilization percentage per window (5-hour, 7-day, monthly)
148
+ - Usage rate vs expected rate
149
+ - Reset times
150
+ - Color coding: 🟢 on track | 🟡 over budget | 🔴 significantly over
151
+
152
+ JSON format returns structured data for programmatic use.
153
+
154
+ ## Agent Rule
155
+
156
+ Add to your `CLAUDE.md` or `AGENTS.md`:
157
+
158
+ ```markdown
159
+ # Rule: `axusage` Usage
160
+
161
+ Run `npx -y axusage --help` to learn available options.
162
+
163
+ Use `axusage` when you need a quick, scriptable snapshot of API usage across Claude, ChatGPT, GitHub Copilot, and Gemini. It standardizes output (text, JSON, Prometheus) so you can alert, dashboard, or pipe it into other Unix tools.
164
+ ```
165
+
166
+ ## Troubleshooting
167
+
168
+ ### Authentication setup hangs
169
+
170
+ - The CLI shows a countdown while waiting for login.
171
+ - If you have completed login, press Enter in the terminal to continue.
172
+ - If it still fails, run `axusage auth clear <service>` and retry.
173
+
174
+ ### "No saved authentication" error
175
+
176
+ - Check which services are authenticated: `axusage auth status`.
177
+ - Set up the missing service: `axusage auth setup <service>`.
178
+
179
+ ### Sessions expire
180
+
181
+ - Browser sessions can expire based on provider policy. Re-run `auth setup` for the affected service when you see authentication errors.
182
+
183
+ ## Remote authentication and Prometheus export
184
+
185
+ You can perform the interactive login flow on a workstation (for example, a local macOS laptop) and reuse the resulting browser session on a headless Linux server that collects usage and exports it for Prometheus.
186
+
187
+ ### 1. Authenticate on a workstation
188
+
189
+ 1. Install globally and authenticate the CLIs you need, then set up browser
190
+ auth for GitHub Copilot:
191
+
192
+ ```bash
193
+ npm install -g axusage
194
+
195
+ claude
196
+ codex
197
+ gemini
198
+ axusage auth setup github-copilot
199
+ ```
200
+
201
+ 2. Confirm the workstation has valid sessions:
202
+
203
+ ```bash
204
+ axusage auth status
205
+ ```
206
+
207
+ 3. Package the saved contexts so they can be transferred. Set `CONTEXT_DIR` to the path for your platform (see the table above):
208
+
209
+ ```bash
210
+ CONTEXT_DIR="$HOME/.local/share/axusage/browser-contexts" # Linux default; adjust on macOS/Windows
211
+ tar czf axusage-contexts.tgz -C "$(dirname "$CONTEXT_DIR")" "$(basename "$CONTEXT_DIR")"
212
+ ```
213
+
214
+ Archive structure: `browser-contexts/claude/`, `browser-contexts/chatgpt/`, etc.
215
+
216
+ ### 2. Transfer the browser contexts to the Linux server
217
+
218
+ 1. Copy the archive to the server with `scp` (replace `user@server` with your login):
219
+
220
+ ```bash
221
+ scp axusage-contexts.tgz user@server:~/
222
+ ```
223
+
224
+ 2. On the server, create the target directory if it does not already exist, unpack the archive, and lock down the permissions:
225
+
226
+ ```bash
227
+ ssh user@server
228
+ CONTEXT_DIR="$HOME/.local/share/axusage/browser-contexts" # Linux default; adjust per platform
229
+ AXUSAGE_DIR="$(dirname "$CONTEXT_DIR")"
230
+ mkdir -p "$CONTEXT_DIR"
231
+ tar xzf ~/axusage-contexts.tgz -C "$AXUSAGE_DIR"
232
+ # Directories 700, files 600
233
+ find "$AXUSAGE_DIR" -type d -exec chmod 700 {} +
234
+ find "$CONTEXT_DIR" -type f -exec chmod 600 {} +
235
+ ```
236
+
237
+ 3. Verify that the sessions are available on the server:
238
+
239
+ ```bash
240
+ axusage auth status
241
+ ```
242
+
243
+ If the server does not yet have the tool installed, run `npm install -g axusage` before checking the status.
244
+
245
+ Notes:
246
+
247
+ - Use `--service <name>` to restrict services.
248
+ - Sessions may expire or become invalid if you change your password or log out of the service in another browser. Re-run `auth setup` as needed.
249
+ - If you transfer browser contexts between machines, ensure the target system is secure and permissions are restricted to the intended user.
250
+ - The CLI stores authentication data in the platform-specific directories listed above; protect that directory to prevent unauthorized access.
251
+
252
+ ## Development
253
+
254
+ For local development in this repository, `pnpm run start` triggers a clean rebuild before executing the CLI. Use `pnpm run usage` only when `dist/` is already up to date. End users installing globally should run the `axusage` binary directly.
package/bin/axusage ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import("../dist/cli.js");
@@ -0,0 +1,8 @@
1
+ import type { ServiceAdapter } from "../types/domain.js";
2
+ /**
3
+ * ChatGPT service adapter using direct API access.
4
+ *
5
+ * Uses the OAuth token from Codex CLI's credential store (~/.codex/auth.json)
6
+ * to make direct API calls to ChatGPT's usage endpoint.
7
+ */
8
+ export declare const chatGPTAdapter: ServiceAdapter;
@@ -0,0 +1,68 @@
1
+ import { extractRawCredentials, getAccessToken } from "axauth";
2
+ import { ApiError } from "../types/domain.js";
3
+ import { ChatGPTUsageResponse as ChatGPTUsageResponseSchema } from "../types/chatgpt.js";
4
+ import { toServiceUsageData } from "./parse-chatgpt-usage.js";
5
+ const API_URL = "https://chatgpt.com/backend-api/wham/usage";
6
+ /**
7
+ * ChatGPT service adapter using direct API access.
8
+ *
9
+ * Uses the OAuth token from Codex CLI's credential store (~/.codex/auth.json)
10
+ * to make direct API calls to ChatGPT's usage endpoint.
11
+ */
12
+ export const chatGPTAdapter = {
13
+ name: "ChatGPT",
14
+ async fetchUsage() {
15
+ const credentials = extractRawCredentials("codex");
16
+ if (!credentials) {
17
+ return {
18
+ ok: false,
19
+ error: new ApiError("No Codex credentials found. Run 'codex' to authenticate."),
20
+ };
21
+ }
22
+ if (credentials.type !== "oauth") {
23
+ return {
24
+ ok: false,
25
+ error: new ApiError("ChatGPT usage API requires OAuth authentication. API key authentication is not supported for usage data."),
26
+ };
27
+ }
28
+ const accessToken = getAccessToken(credentials);
29
+ if (!accessToken) {
30
+ return {
31
+ ok: false,
32
+ error: new ApiError("Invalid OAuth credentials: missing access token."),
33
+ };
34
+ }
35
+ try {
36
+ const response = await fetch(API_URL, {
37
+ headers: {
38
+ Authorization: `Bearer ${accessToken}`,
39
+ },
40
+ });
41
+ if (!response.ok) {
42
+ const errorText = await response.text().catch(() => "");
43
+ return {
44
+ ok: false,
45
+ error: new ApiError(`ChatGPT API request failed: ${String(response.status)} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`, response.status),
46
+ };
47
+ }
48
+ const data = await response.json();
49
+ const parseResult = ChatGPTUsageResponseSchema.safeParse(data);
50
+ if (!parseResult.success) {
51
+ return {
52
+ ok: false,
53
+ error: new ApiError(`Invalid response format: ${parseResult.error.message}`, undefined, data),
54
+ };
55
+ }
56
+ return {
57
+ ok: true,
58
+ value: toServiceUsageData(parseResult.data),
59
+ };
60
+ }
61
+ catch (error) {
62
+ return {
63
+ ok: false,
64
+ error: new ApiError(`Failed to fetch ChatGPT usage: ${error instanceof Error ? error.message : String(error)}`),
65
+ };
66
+ }
67
+ },
68
+ };
@@ -0,0 +1,9 @@
1
+ import type { ServiceAdapter } from "../types/domain.js";
2
+ /**
3
+ * Claude service adapter using direct API access.
4
+ *
5
+ * This adapter uses the OAuth token from Claude Code's credential store
6
+ * (Keychain on macOS, credentials file elsewhere) to make direct API calls
7
+ * to the Anthropic usage endpoint.
8
+ */
9
+ export declare const claudeAdapter: ServiceAdapter;
@@ -0,0 +1,108 @@
1
+ import { extractRawCredentials, getAccessToken } from "axauth";
2
+ import { z } from "zod";
3
+ import { ApiError } from "../types/domain.js";
4
+ import { UsageResponse as UsageResponseSchema } from "../types/usage.js";
5
+ import { coalesceClaudeUsageResponse } from "./coalesce-claude-usage-response.js";
6
+ import { toServiceUsageData } from "./parse-claude-usage.js";
7
+ const USAGE_API_URL = "https://api.anthropic.com/api/oauth/usage";
8
+ const PROFILE_API_URL = "https://api.anthropic.com/api/oauth/profile";
9
+ const ANTHROPIC_BETA_HEADER = "oauth-2025-04-20";
10
+ /** Map organization_type to display name */
11
+ const PLAN_TYPE_MAP = {
12
+ claude_max: "Max",
13
+ claude_pro: "Pro",
14
+ claude_enterprise: "Enterprise",
15
+ claude_team: "Team",
16
+ };
17
+ /** Fetch plan type from profile endpoint (best effort) */
18
+ async function fetchPlanType(accessToken) {
19
+ try {
20
+ const response = await fetch(PROFILE_API_URL, {
21
+ headers: {
22
+ Authorization: `Bearer ${accessToken}`,
23
+ "anthropic-beta": ANTHROPIC_BETA_HEADER,
24
+ },
25
+ });
26
+ if (!response.ok)
27
+ return undefined;
28
+ const data = (await response.json());
29
+ const orgType = data.organization?.organization_type;
30
+ return orgType ? (PLAN_TYPE_MAP[orgType] ?? orgType) : undefined;
31
+ }
32
+ catch {
33
+ return undefined;
34
+ }
35
+ }
36
+ /**
37
+ * Claude service adapter using direct API access.
38
+ *
39
+ * This adapter uses the OAuth token from Claude Code's credential store
40
+ * (Keychain on macOS, credentials file elsewhere) to make direct API calls
41
+ * to the Anthropic usage endpoint.
42
+ */
43
+ export const claudeAdapter = {
44
+ name: "Claude",
45
+ async fetchUsage() {
46
+ const credentials = extractRawCredentials("claude");
47
+ if (!credentials) {
48
+ return {
49
+ ok: false,
50
+ error: new ApiError("No Claude Code credentials found. Ensure Claude Code is installed and authenticated."),
51
+ };
52
+ }
53
+ if (credentials.type !== "oauth") {
54
+ return {
55
+ ok: false,
56
+ error: new ApiError("Claude Code usage API requires OAuth authentication. API key authentication is not supported for usage data."),
57
+ };
58
+ }
59
+ const accessToken = getAccessToken(credentials);
60
+ if (!accessToken) {
61
+ return {
62
+ ok: false,
63
+ error: new ApiError("Invalid OAuth credentials: missing access token."),
64
+ };
65
+ }
66
+ try {
67
+ const response = await fetch(USAGE_API_URL, {
68
+ headers: {
69
+ Authorization: `Bearer ${accessToken}`,
70
+ "anthropic-beta": ANTHROPIC_BETA_HEADER,
71
+ },
72
+ });
73
+ if (!response.ok) {
74
+ const errorText = await response.text().catch(() => "");
75
+ return {
76
+ ok: false,
77
+ error: new ApiError(`Claude API request failed: ${String(response.status)} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`, response.status),
78
+ };
79
+ }
80
+ const data = await response.json();
81
+ const parseResult = UsageResponseSchema.safeParse(coalesceClaudeUsageResponse(data) ?? data);
82
+ if (!parseResult.success) {
83
+ /* eslint-disable unicorn/no-null -- JSON.stringify requires null for no replacer */
84
+ console.error("Raw API response:", JSON.stringify(data, null, 2));
85
+ console.error("Validation errors:", JSON.stringify(z.treeifyError(parseResult.error), null, 2));
86
+ /* eslint-enable unicorn/no-null */
87
+ return {
88
+ ok: false,
89
+ error: new ApiError(`Invalid response format: ${parseResult.error.message}`, undefined, data),
90
+ };
91
+ }
92
+ // Fetch plan type (best effort, don't fail if unavailable)
93
+ const planType = await fetchPlanType(accessToken);
94
+ const usageData = toServiceUsageData(parseResult.data);
95
+ return {
96
+ ok: true,
97
+ value: planType ? { ...usageData, planType } : usageData,
98
+ };
99
+ }
100
+ catch (error) {
101
+ const message = error instanceof Error ? error.message : String(error);
102
+ return {
103
+ ok: false,
104
+ error: new ApiError(`Failed to fetch Claude usage: ${message}`),
105
+ };
106
+ }
107
+ },
108
+ };
@@ -0,0 +1,12 @@
1
+ import type { UsageResponseInput } from "../types/usage.js";
2
+ /**
3
+ * Best-effort coalescing for array-shaped Claude usage responses.
4
+ *
5
+ * Notes:
6
+ * - Requires 5-hour and 7-day windows; otherwise returns undefined.
7
+ * - Windows with missing reset timestamps are retained with `resets_at: null`,
8
+ * which the schema transforms to undefined for consumers.
9
+ * - Opus, Sonnet, and OAuth apps windows are optional and will only be included
10
+ * when present in the source data.
11
+ */
12
+ export declare function coalesceClaudeUsageResponse(data: unknown): UsageResponseInput | undefined;
@@ -0,0 +1,119 @@
1
+ import { z } from "zod";
2
+ const UsageWindowCandidate = z.object({
3
+ window: z.string().optional(),
4
+ period: z.string().optional(),
5
+ name: z.string().optional(),
6
+ key: z.string().optional(),
7
+ utilization: z.number().optional(),
8
+ percentage: z.number().optional(),
9
+ percent: z.number().optional(),
10
+ resets_at: z.union([z.string(), z.null()]).optional(),
11
+ reset_at: z.union([z.string(), z.null()]).optional(),
12
+ resetsAt: z.union([z.string(), z.null()]).optional(),
13
+ resetAt: z.union([z.string(), z.null()]).optional(),
14
+ });
15
+ const UsageWindowCandidates = z.array(UsageWindowCandidate);
16
+ /**
17
+ * Tokenizes labels so we can match windows even when punctuation varies,
18
+ * e.g. "7-day" vs "seven_day".
19
+ * Splits on any non-alphanumeric characters (hyphen, underscore, space, etc.).
20
+ *
21
+ * Note: labels are normalized to lowercase in {@link normalizeLabel}, so the
22
+ * character class here only needs to handle lowercase a–z and digits.
23
+ */
24
+ const tokenizeLabel = (label) => new Set(label.split(/[^a-z0-9]+/gu).filter(Boolean));
25
+ const normalizeLabel = (candidate) => (candidate.window ||
26
+ candidate.period ||
27
+ candidate.name ||
28
+ candidate.key ||
29
+ "").toLowerCase();
30
+ const resolveResetTimestamp = (candidate) => {
31
+ const values = [
32
+ candidate.resets_at,
33
+ candidate.reset_at,
34
+ candidate.resetsAt,
35
+ candidate.resetAt,
36
+ ];
37
+ for (const value of values) {
38
+ if (value === undefined)
39
+ continue;
40
+ return value;
41
+ }
42
+ // eslint-disable-next-line unicorn/no-null -- Schema expects explicit null for absent timestamps
43
+ return null;
44
+ };
45
+ const resolveUtilization = (candidate) => {
46
+ if (candidate.utilization !== undefined)
47
+ return candidate.utilization;
48
+ return candidate.percentage ?? candidate.percent ?? 0;
49
+ };
50
+ const selectMetric = (candidates, matchers) => {
51
+ for (const candidate of candidates) {
52
+ const label = normalizeLabel(candidate);
53
+ if (!label)
54
+ continue;
55
+ const tokens = tokenizeLabel(label);
56
+ const matches = matchers.includes(label) ||
57
+ matchers.some((matcher) => tokens.has(matcher));
58
+ if (!matches)
59
+ continue;
60
+ const utilization = resolveUtilization(candidate);
61
+ return {
62
+ utilization,
63
+ resets_at: resolveResetTimestamp(candidate),
64
+ };
65
+ }
66
+ return undefined;
67
+ };
68
+ /**
69
+ * Best-effort coalescing for array-shaped Claude usage responses.
70
+ *
71
+ * Notes:
72
+ * - Requires 5-hour and 7-day windows; otherwise returns undefined.
73
+ * - Windows with missing reset timestamps are retained with `resets_at: null`,
74
+ * which the schema transforms to undefined for consumers.
75
+ * - Opus, Sonnet, and OAuth apps windows are optional and will only be included
76
+ * when present in the source data.
77
+ */
78
+ export function coalesceClaudeUsageResponse(data) {
79
+ if (!Array.isArray(data))
80
+ return undefined;
81
+ const parsed = UsageWindowCandidates.safeParse(data);
82
+ if (!parsed.success)
83
+ return undefined;
84
+ const candidates = parsed.data;
85
+ const fiveHour = selectMetric(candidates, [
86
+ "five_hour",
87
+ "five",
88
+ "5",
89
+ "5hour",
90
+ ]);
91
+ const sevenDay = selectMetric(candidates, [
92
+ "seven_day",
93
+ "seven",
94
+ "7",
95
+ "week",
96
+ ]);
97
+ const sevenDayOpus = selectMetric(candidates, ["seven_day_opus", "opus"]);
98
+ const sevenDaySonnet = selectMetric(candidates, [
99
+ "seven_day_sonnet",
100
+ "sonnet",
101
+ ]);
102
+ const sevenDayOauth = selectMetric(candidates, [
103
+ "seven_day_oauth_apps",
104
+ "oauth",
105
+ ]);
106
+ if (!fiveHour || !sevenDay)
107
+ return undefined;
108
+ const result = {
109
+ five_hour: fiveHour,
110
+ seven_day: sevenDay,
111
+ };
112
+ if (sevenDayOpus)
113
+ result.seven_day_opus = sevenDayOpus;
114
+ if (sevenDaySonnet)
115
+ result.seven_day_sonnet = sevenDaySonnet;
116
+ if (sevenDayOauth)
117
+ result.seven_day_oauth_apps = sevenDayOauth;
118
+ return result;
119
+ }
@@ -0,0 +1,8 @@
1
+ import type { ServiceAdapter } from "../types/domain.js";
2
+ /**
3
+ * Gemini service adapter using direct API access.
4
+ *
5
+ * Uses the OAuth token from Gemini CLI's credential store (~/.gemini/oauth_creds.json)
6
+ * to make direct API calls to Google's quota endpoint.
7
+ */
8
+ export declare const geminiAdapter: ServiceAdapter;
@@ -0,0 +1,43 @@
1
+ import { getAgentAccessToken } from "axauth";
2
+ import { ApiError } from "../types/domain.js";
3
+ import { fetchGeminiQuota, fetchGeminiProject, } from "../services/gemini-api.js";
4
+ import { toServiceUsageData } from "./parse-gemini-usage.js";
5
+ /**
6
+ * Gemini service adapter using direct API access.
7
+ *
8
+ * Uses the OAuth token from Gemini CLI's credential store (~/.gemini/oauth_creds.json)
9
+ * to make direct API calls to Google's quota endpoint.
10
+ */
11
+ export const geminiAdapter = {
12
+ name: "Gemini",
13
+ async fetchUsage() {
14
+ const accessToken = getAgentAccessToken("gemini");
15
+ if (!accessToken) {
16
+ return {
17
+ ok: false,
18
+ error: new ApiError("No Gemini credentials found. Run 'gemini' to authenticate."),
19
+ };
20
+ }
21
+ try {
22
+ // Discover project ID for more accurate quotas (best effort)
23
+ const projectId = await fetchGeminiProject(accessToken);
24
+ // Fetch quota data
25
+ const quotaResult = await fetchGeminiQuota(accessToken, projectId);
26
+ if (!quotaResult.ok) {
27
+ return quotaResult;
28
+ }
29
+ return {
30
+ ok: true,
31
+ value: toServiceUsageData(quotaResult.value),
32
+ };
33
+ }
34
+ catch (error) {
35
+ return {
36
+ ok: false,
37
+ error: error instanceof ApiError
38
+ ? error
39
+ : new ApiError(`Failed to fetch Gemini usage: ${error instanceof Error ? error.message : String(error)}`),
40
+ };
41
+ }
42
+ },
43
+ };
@@ -0,0 +1,6 @@
1
+ import type { ServiceAdapter } from "../types/domain.js";
2
+ /** Functional core is extracted to ./parse-github-copilot-usage.ts */
3
+ /**
4
+ * GitHub Copilot service adapter
5
+ */
6
+ export declare const githubCopilotAdapter: ServiceAdapter;