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