@scrylog/cli 0.1.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 +836 -0
- package/package.json +75 -0
- package/src/api/docs/sessions.keys.ts +6 -0
- package/src/api/docs/sessions.ts +81 -0
- package/src/api/docs/system.keys.ts +5 -0
- package/src/api/docs/system.ts +66 -0
- package/src/api/docs/usage.keys.ts +3 -0
- package/src/api/docs/usage.ts +24 -0
- package/src/api/docs/webhooks.keys.ts +5 -0
- package/src/api/docs/webhooks.ts +78 -0
- package/src/api/index.ts +18 -0
- package/src/api/lib/auth.ts +43 -0
- package/src/api/lib/errors.ts +28 -0
- package/src/api/lib/route.ts +18 -0
- package/src/api/models/claude.ts +4 -0
- package/src/api/models/codex.ts +4 -0
- package/src/api/models/opencode.ts +11 -0
- package/src/api/routes/sessions.ts +47 -0
- package/src/api/routes/system.ts +103 -0
- package/src/api/routes/usage.ts +15 -0
- package/src/api/routes/webhooks/claude.ts +161 -0
- package/src/api/routes/webhooks/codex.ts +175 -0
- package/src/api/routes/webhooks/opencode.ts +107 -0
- package/src/api/routes.ts +93 -0
- package/src/api/schemas/common.ts +21 -0
- package/src/api/schemas/sessions.ts +72 -0
- package/src/api/schemas/usage.ts +50 -0
- package/src/api/schemas/webhooks/claude.ts +32 -0
- package/src/api/schemas/webhooks/codex.ts +34 -0
- package/src/api/schemas/webhooks/opencode.ts +29 -0
- package/src/api/services/claude-session.ts +9 -0
- package/src/api/services/codex-session.ts +8 -0
- package/src/api/services/opencode-session.ts +73 -0
- package/src/classifier.ts +206 -0
- package/src/cli/commands.ts +498 -0
- package/src/cli/prompt.ts +44 -0
- package/src/daemon/cli.ts +27 -0
- package/src/daemon/collectors/claude/README.md +108 -0
- package/src/daemon/collectors/claude/collector.ts +413 -0
- package/src/daemon/collectors/claude/discovery.ts +148 -0
- package/src/daemon/collectors/claude/hooks.ts +181 -0
- package/src/daemon/collectors/claude/index.ts +8 -0
- package/src/daemon/collectors/claude/jsonl.ts +85 -0
- package/src/daemon/collectors/claude/transcript.ts +246 -0
- package/src/daemon/collectors/codex/README.md +147 -0
- package/src/daemon/collectors/codex/collector.ts +661 -0
- package/src/daemon/collectors/codex/discovery.ts +104 -0
- package/src/daemon/collectors/codex/hooks.ts +179 -0
- package/src/daemon/collectors/codex/index.ts +7 -0
- package/src/daemon/collectors/codex/jsonl.ts +108 -0
- package/src/daemon/collectors/codex/transcript.ts +299 -0
- package/src/daemon/collectors/git.ts +295 -0
- package/src/daemon/collectors/opencode/README.md +106 -0
- package/src/daemon/collectors/opencode/collector.ts +543 -0
- package/src/daemon/collectors/opencode/discovery.ts +257 -0
- package/src/daemon/collectors/opencode/index.ts +5 -0
- package/src/daemon/collectors/opencode/session-builder.ts +59 -0
- package/src/daemon/collectors/opencode-db.ts +282 -0
- package/src/daemon/collectors/process.ts +201 -0
- package/src/daemon/collectors/tmux/README.md +128 -0
- package/src/daemon/collectors/tmux/collector.ts +472 -0
- package/src/daemon/collectors/tmux/enrichment.ts +241 -0
- package/src/daemon/collectors/tmux/index.ts +4 -0
- package/src/daemon/collectors/tmux/parse.ts +128 -0
- package/src/daemon/collectors/usage.ts +193 -0
- package/src/daemon/index.ts +207 -0
- package/src/daemon/scheduler.ts +86 -0
- package/src/daemon/store.ts +535 -0
- package/src/detect.ts +143 -0
- package/src/hooks/claude.ts +596 -0
- package/src/hooks/codex.ts +517 -0
- package/src/legacy/classifier.ts +215 -0
- package/src/legacy/detect.ts +143 -0
- package/src/mcp/client-server.ts +21 -0
- package/src/mcp/http-handler.ts +126 -0
- package/src/mcp/index.ts +61 -0
- package/src/mcp/resources/config-current.ts +19 -0
- package/src/mcp/resources/index.ts +11 -0
- package/src/mcp/resources/sessions-active.ts +19 -0
- package/src/mcp/resources/sessions-all.ts +19 -0
- package/src/mcp/schemas/sessions.ts +3 -0
- package/src/mcp/server.ts +17 -0
- package/src/mcp/services/api-adapter.ts +48 -0
- package/src/mcp/services/data-source.ts +22 -0
- package/src/mcp/services/store-adapter.ts +40 -0
- package/src/mcp/tools/get-active-sessions.ts +16 -0
- package/src/mcp/tools/get-session-children.ts +24 -0
- package/src/mcp/tools/get-session-messages.ts +24 -0
- package/src/mcp/tools/get-session.ts +23 -0
- package/src/mcp/tools/index.ts +15 -0
- package/src/mcp/tools/list-sessions.ts +16 -0
- package/src/neo-blessed.d.ts +4 -0
- package/src/oc/index.ts +302 -0
- package/src/service/manager.ts +242 -0
- package/src/shared/config.ts +478 -0
- package/src/shared/ipc.ts +80 -0
- package/src/shared/logger.ts +248 -0
- package/src/shared/paths.ts +73 -0
- package/src/shared/platform.ts +110 -0
- package/src/shared/session-view.ts +241 -0
- package/src/shared/types.ts +299 -0
- package/src/shared/utils.ts +339 -0
- package/src/tui/app.ts +920 -0
- package/src/tui/client.ts +149 -0
- package/src/tui/index.ts +181 -0
- package/src/types.ts +113 -0
- package/src/usage/README.md +323 -0
- package/src/usage/api.ts +99 -0
- package/src/usage/client.ts +3 -0
- package/src/usage/clients/anthropic-oauth-usage.ts +178 -0
- package/src/usage/clients/anthropic-web-usage.ts +245 -0
- package/src/usage/clients/claude-cli-probe.ts +187 -0
- package/src/usage/clients/codex-oauth-usage.ts +113 -0
- package/src/usage/clients/opencode-go-usage.ts +441 -0
- package/src/usage/index.ts +19 -0
- package/src/usage/mappers.ts +273 -0
- package/src/usage/planner.ts +54 -0
- package/src/usage/providers/claude.ts +113 -0
- package/src/usage/providers/codex.ts +34 -0
- package/src/usage/providers/opencode-go.ts +37 -0
- package/src/usage/resolver.ts +4 -0
- package/src/usage/resolvers/browser-cookies.ts +198 -0
- package/src/usage/resolvers/claude-auth.ts +99 -0
- package/src/usage/resolvers/claude-cli.ts +24 -0
- package/src/usage/resolvers/claude-web.ts +16 -0
- package/src/usage/resolvers/codex-auth.ts +117 -0
- package/src/usage/resolvers/opencode-go-web.ts +14 -0
- package/src/usage/types.ts +190 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# Usage Limits API
|
|
2
|
+
|
|
3
|
+
The Usage Limits API provides subscription usage data for AI coding agent providers (Claude Code, OpenAI Codex, OpenCode Go) via a single unified endpoint.
|
|
4
|
+
|
|
5
|
+
## Endpoint
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
GET /api/usage
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Returns current usage data for all configured providers, sourced from the daemon's in-memory cache (refreshed every 5 minutes by default).
|
|
12
|
+
|
|
13
|
+
Usage collection is optional. Set `usage.enabled` to `false` in the scrylog config to stop all periodic Claude/Codex/OpenCode Go usage probes while keeping session monitoring enabled:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"usage": {
|
|
18
|
+
"enabled": false
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
When disabled, the daemon does not register the usage collector and does not make external usage requests. This is a normal operational state, not an error.
|
|
24
|
+
|
|
25
|
+
## Response format
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
interface UsageState {
|
|
29
|
+
providers: UsageProviderEntry[];
|
|
30
|
+
lastRefreshedAt: number; // Unix timestamp in milliseconds
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### `UsageProviderEntry`
|
|
35
|
+
|
|
36
|
+
All providers share the same response shape. No discriminated union.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
interface UsageProviderEntry {
|
|
40
|
+
provider: string; // "claude" | "opencode-go"
|
|
41
|
+
accountId: string | null; // stable account identifier for grouping across hosts
|
|
42
|
+
available: boolean; // credentials detected on this machine?
|
|
43
|
+
status: "ok" | "error" | "unavailable" | "pending";
|
|
44
|
+
metadata?: {
|
|
45
|
+
dataSource?: string; // "oauth", "web", "cli"
|
|
46
|
+
accountEmail?: string | null;
|
|
47
|
+
accountOrganization?: string | null;
|
|
48
|
+
subscriptionType?: string | null; // "pro", "free"
|
|
49
|
+
rateLimitTier?: string | null;
|
|
50
|
+
workspaceId?: string | null; // OpenCode Go
|
|
51
|
+
workspaceName?: string | null; // OpenCode Go
|
|
52
|
+
};
|
|
53
|
+
errors?: string[]; // only when status === "error"
|
|
54
|
+
windows?: UsageWindow[]; // standardized usage windows
|
|
55
|
+
cost?: ProviderCostSnapshot | null; // optional overage/cost data
|
|
56
|
+
updatedAt?: string; // ISO 8601
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### `UsageWindow`
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
interface UsageWindow {
|
|
64
|
+
id: string; // "session", "weekly", "monthly", "designs"
|
|
65
|
+
title: string; // "Current Session", "Weekly"
|
|
66
|
+
usedPercent: number; // 0-100
|
|
67
|
+
used?: number; // absolute usage (requests, tokens, credits)
|
|
68
|
+
limit?: number; // absolute limit
|
|
69
|
+
windowMinutes?: number; // window duration
|
|
70
|
+
resetsAt?: string | null; // ISO 8601 timestamp
|
|
71
|
+
resetInSec?: number; // seconds until reset (computed if missing)
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `ProviderCostSnapshot`
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
interface ProviderCostSnapshot {
|
|
79
|
+
used: number;
|
|
80
|
+
limit: number;
|
|
81
|
+
currency: string; // "USD"
|
|
82
|
+
period: string; // "Monthly"
|
|
83
|
+
resetsAt?: string | null;
|
|
84
|
+
updatedAt: string; // ISO 8601
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Account identification (`accountId`)
|
|
89
|
+
|
|
90
|
+
`accountId` is designed to be **stable across hosts** so that a future mobile/web app can group usage by account.
|
|
91
|
+
|
|
92
|
+
| Provider | `accountId` strategy | Example |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| **Claude** | 1. Email (Web API) → 2. `claude-{subscriptionType}-{rateLimitTier}-{scopesHash}` (Keychain fallback) | `"user@acme.com"` or `"claude-pro-default_claude_ai-0db07b08"` |
|
|
95
|
+
| **OpenCode Go** | `workspaceId` from `_server` response | `"wrk_01KQ0X20G68BQG4KV5Q969Z5GV"` |
|
|
96
|
+
| **Codex** | 1. `account_id` from `tokens` object → 2. `sub` claim from access token JWT | `"388604f4-d26c-4167-bb85-13616dc80a39"` |
|
|
97
|
+
|
|
98
|
+
When `status === "unavailable"` or `status === "error"` with no identity data, `accountId` is `null`.
|
|
99
|
+
|
|
100
|
+
## Status semantics
|
|
101
|
+
|
|
102
|
+
| Status | Meaning | `windows` | `errors` | `accountId` |
|
|
103
|
+
|---|---|---|---|---|
|
|
104
|
+
| `ok` | Data fetched successfully | Present | Absent | Computed from best available source |
|
|
105
|
+
| `error` | Credentials found but fetch failed | Absent | Present | From creds metadata if available |
|
|
106
|
+
| `unavailable` | No credentials on this machine | Absent | Absent | `null` |
|
|
107
|
+
| `pending` | First poll not yet complete | Absent | Absent | `null` |
|
|
108
|
+
|
|
109
|
+
## Example responses
|
|
110
|
+
|
|
111
|
+
### All providers available with data
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"providers": [
|
|
116
|
+
{
|
|
117
|
+
"provider": "claude",
|
|
118
|
+
"accountId": "claude-pro-default_claude_ai-0db07b08",
|
|
119
|
+
"available": true,
|
|
120
|
+
"status": "ok",
|
|
121
|
+
"metadata": {
|
|
122
|
+
"dataSource": "oauth",
|
|
123
|
+
"accountEmail": null,
|
|
124
|
+
"subscriptionType": "pro",
|
|
125
|
+
"rateLimitTier": "default_claude_ai"
|
|
126
|
+
},
|
|
127
|
+
"windows": [
|
|
128
|
+
{ "id": "session", "title": "Current Session", "usedPercent": 0, "windowMinutes": 300, "resetsAt": "2026-04-30T09:40:00Z", "resetInSec": 11537 },
|
|
129
|
+
{ "id": "weekly", "title": "Weekly", "usedPercent": 36, "windowMinutes": 10080, "resetsAt": "2026-04-30T17:00:00Z", "resetInSec": 37937 },
|
|
130
|
+
{ "id": "claude-design", "title": "Designs", "usedPercent": 0, "windowMinutes": 10080, "resetsAt": null }
|
|
131
|
+
],
|
|
132
|
+
"cost": null,
|
|
133
|
+
"updatedAt": "2026-04-30T06:27:42Z"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"provider": "opencode-go",
|
|
137
|
+
"accountId": "wrk_01KQ0X20G68BQG4KV5Q969Z5GV",
|
|
138
|
+
"available": true,
|
|
139
|
+
"status": "ok",
|
|
140
|
+
"metadata": {
|
|
141
|
+
"dataSource": "web",
|
|
142
|
+
"workspaceId": "wrk_01KQ0X20G68BQG4KV5Q969Z5GV",
|
|
143
|
+
"workspaceName": "Default"
|
|
144
|
+
},
|
|
145
|
+
"windows": [
|
|
146
|
+
{ "id": "session", "title": "Rolling", "usedPercent": 16, "resetInSec": 12094, "resetsAt": "2026-04-30T09:49:18Z" },
|
|
147
|
+
{ "id": "weekly", "title": "Weekly", "usedPercent": 29, "resetInSec": 322336, "resetsAt": "2026-05-04T00:00:00Z" },
|
|
148
|
+
{ "id": "monthly", "title": "Monthly", "usedPercent": 33, "resetInSec": 2134791, "resetsAt": "2026-05-24T23:27:35Z" }
|
|
149
|
+
],
|
|
150
|
+
"updatedAt": "2026-04-30T06:27:44Z"
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
"lastRefreshedAt": 1777530462243
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Provider unavailable (no credentials)
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"providers": [
|
|
162
|
+
{
|
|
163
|
+
"provider": "claude",
|
|
164
|
+
"accountId": null,
|
|
165
|
+
"available": false,
|
|
166
|
+
"status": "unavailable"
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
"lastRefreshedAt": 1777528387102
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Provider with fetch errors
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"providers": [
|
|
178
|
+
{
|
|
179
|
+
"provider": "claude",
|
|
180
|
+
"accountId": null,
|
|
181
|
+
"available": true,
|
|
182
|
+
"status": "error",
|
|
183
|
+
"errors": ["oauth: Server error: 401", "web: Session cookie invalid"]
|
|
184
|
+
}
|
|
185
|
+
],
|
|
186
|
+
"lastRefreshedAt": 1777528387102
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Architecture
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
DaemonStore
|
|
194
|
+
└── usage: UsageState
|
|
195
|
+
└── providers: UsageProviderEntry[]
|
|
196
|
+
|
|
197
|
+
UsageCollector (polls every 5 min)
|
|
198
|
+
├── ClaudeUsageProvider
|
|
199
|
+
│ ├── ClaudeSourcePlanner (decides oauth/web/cli/auto)
|
|
200
|
+
│ ├── ClaudeKeychainResolver → AnthropicOAuthUsageClient
|
|
201
|
+
│ ├── ClaudeWebResolver → AnthropicWebUsageClient
|
|
202
|
+
│ └── ClaudeCliResolver → ClaudeCliProbeClient
|
|
203
|
+
│ └── mapClaudeToEntry(oauthCreds) → UsageProviderEntry + accountId
|
|
204
|
+
├── CodexUsageProvider
|
|
205
|
+
│ └── CodexAuthResolver → CodexOAuthUsageClient
|
|
206
|
+
│ └── mapCodexToEntry() → UsageProviderEntry + accountId
|
|
207
|
+
└── OpenCodeGoUsageProvider
|
|
208
|
+
├── OpenCodeGoWebResolver
|
|
209
|
+
└── OpenCodeGoUsageClient (resolves workspace id+name)
|
|
210
|
+
└── mapOpenCodeGoToEntry() → UsageProviderEntry + accountId
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Universal API (React Native / Testing)
|
|
214
|
+
|
|
215
|
+
For consumers that already hold credentials (React Native apps, test scripts, external integrations):
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { UsageApi } from "scrylog/usage";
|
|
219
|
+
|
|
220
|
+
// Claude OAuth — token from SecureStore / user input
|
|
221
|
+
const entry = await UsageApi.claude.fromOAuthToken({
|
|
222
|
+
accessToken: "sk-ant-oat01-...",
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Claude OAuth + Web extras (designs, routines, overage cost)
|
|
226
|
+
const entry = await UsageApi.claude.fromOAuthTokenWithWebExtras(
|
|
227
|
+
{ accessToken: "sk-ant-oat01-..." },
|
|
228
|
+
{ cookieHeader: "sessionKey=abc; intercom-id=xyz" }
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Claude Web — session cookies from WebView / cookie jar
|
|
232
|
+
const entry = await UsageApi.claude.fromWebSession({
|
|
233
|
+
cookieHeader: "sessionKey=abc; ...",
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// OpenCode Go — cookies + serverFnId
|
|
237
|
+
const entry = await UsageApi.opencodeGo.fromCookies({
|
|
238
|
+
cookies: "sessionId=abc; csrf=xyz",
|
|
239
|
+
serverFnId: "def39973...",
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Codex OAuth — token from ~/.codex/auth.json
|
|
243
|
+
const entry = await UsageApi.codex.fromOAuthToken({
|
|
244
|
+
accessToken: "eyJhbGciOiJIUzI1NiIs...",
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
All methods return `Promise<UsageProviderEntry>`. They never throw — on failure they return `status: "error"` with `errors` populated.
|
|
249
|
+
|
|
250
|
+
### Raw clients (advanced)
|
|
251
|
+
|
|
252
|
+
If you need the raw provider-specific snapshot before mapping:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import { AnthropicOAuthUsageClient, CodexOAuthUsageClient } from "scrylog/usage";
|
|
256
|
+
|
|
257
|
+
const claudeClient = new AnthropicOAuthUsageClient();
|
|
258
|
+
const claudeSnapshot = await claudeClient.fetch({ accessToken: "..." });
|
|
259
|
+
|
|
260
|
+
const codexClient = new CodexOAuthUsageClient();
|
|
261
|
+
const codexSnapshot = await codexClient.fetch({ accessToken: "...", accountId: "..." });
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
These clients use the global `fetch` and have no Node.js or platform dependencies.
|
|
265
|
+
|
|
266
|
+
## Security
|
|
267
|
+
|
|
268
|
+
- OAuth tokens and browser cookies are read from OS stores (macOS Keychain, Chrome/Safari cookie databases)
|
|
269
|
+
- Credentials are held in memory **only** during the HTTP call
|
|
270
|
+
- Never written to disk, never logged, never included in API responses
|
|
271
|
+
- Chrome cookie decryption uses AES-128-CBC via Keychain "Chrome Safe Storage"
|
|
272
|
+
|
|
273
|
+
## Configuration
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// scrylog.config.json
|
|
277
|
+
{
|
|
278
|
+
"usage": {
|
|
279
|
+
"enabled": true,
|
|
280
|
+
"pollIntervalMs": 300000,
|
|
281
|
+
"providers": ["claude", "opencode-go", "codex"],
|
|
282
|
+
"claude": {
|
|
283
|
+
"dataSource": "auto",
|
|
284
|
+
"webExtrasEnabled": true
|
|
285
|
+
},
|
|
286
|
+
"codex": {
|
|
287
|
+
"dataSource": "auto"
|
|
288
|
+
},
|
|
289
|
+
"opencodeGo": {
|
|
290
|
+
"serverFnId": "def39973159c7f0483d8793a822b8dbb10d067e12c65455fcb4608459ba0234f"
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
| Field | Default | Description |
|
|
297
|
+
|---|---|---|
|
|
298
|
+
| `enabled` | `true` | Enable usage collection |
|
|
299
|
+
| `pollIntervalMs` | `300000` | Polling interval (5 minutes) |
|
|
300
|
+
| `providers` | `["claude", "opencode-go"]` | Which providers to poll |
|
|
301
|
+
| `claude.dataSource` | `"auto"` | `"auto"`, `"oauth"`, `"web"`, or `"cli"` |
|
|
302
|
+
| `claude.webExtrasEnabled` | `true` | Enrich OAuth/CLI with Web API extras |
|
|
303
|
+
| `codex.dataSource` | `"auto"` | `"auto"` or `"oauth"` |
|
|
304
|
+
| `opencodeGo.serverFnId` | hardcoded | Server function ID for workspace resolution |
|
|
305
|
+
|
|
306
|
+
## Platform support
|
|
307
|
+
|
|
308
|
+
| Feature | macOS | Linux | WSL | Windows |
|
|
309
|
+
|---|---|---|---|---|
|
|
310
|
+
| Claude OAuth (Keychain) | ✅ | ❌ | ❌ | ❌ |
|
|
311
|
+
| Claude OAuth (`~/.claude/.credentials.json`) | ⚠️ (fallback only) | ✅ | ✅ | ✅ |
|
|
312
|
+
| Claude Web (browser cookies) | ✅ (Chrome + Safari) | ✅ (Chrome/Chromium) | ✅ (Chrome/Chromium) | ❌ (Chrome: DPAPI not implemented) |
|
|
313
|
+
| Claude CLI (PTY probe) | ✅ (`script` wrapper) | ⚠️ (no PTY wrapper) | ⚠️ (no PTY wrapper) | ⚠️ (no PTY wrapper) |
|
|
314
|
+
| Codex OAuth (`~/.codex/auth.json`) | ✅ | ✅ | ✅ | ✅ (filepath) |
|
|
315
|
+
| OpenCode Go (browser cookies) | ✅ | ✅ | ✅ | ❌ (Chrome: DPAPI not implemented) |
|
|
316
|
+
|
|
317
|
+
## Adding a new provider
|
|
318
|
+
|
|
319
|
+
1. Create resolver + client in `src/usage/resolvers/` and `src/usage/clients/`
|
|
320
|
+
2. Create provider in `src/usage/providers/<name>.ts` that returns `{ entry: UsageProviderEntry, snapshot: any }`
|
|
321
|
+
3. Add mapper in `src/usage/mappers.ts`: `map<Name>ToEntry(snapshot, errors, available)` — include `accountId` computation
|
|
322
|
+
4. Register in `UsageCollector.poll()` with availability pre-check
|
|
323
|
+
5. No changes needed to `UsageState`, Zod schemas, or API route
|
package/src/usage/api.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { AnthropicOAuthUsageClient } from "./clients/anthropic-oauth-usage";
|
|
2
|
+
import { AnthropicWebUsageClient } from "./clients/anthropic-web-usage";
|
|
3
|
+
import { CodexOAuthUsageClient } from "./clients/codex-oauth-usage";
|
|
4
|
+
import { OpenCodeGoUsageClient } from "./clients/opencode-go-usage";
|
|
5
|
+
import { mapClaudeToEntry, mapCodexToEntry, mapOpenCodeGoToEntry } from "./mappers";
|
|
6
|
+
import type { ClaudeOAuthCredentials, ClaudeWebSession, CodexOAuthCredentials, UsageProviderEntry } from "./types";
|
|
7
|
+
|
|
8
|
+
export const UsageApi = {
|
|
9
|
+
claude: {
|
|
10
|
+
async fromOAuthToken(creds: ClaudeOAuthCredentials): Promise<UsageProviderEntry> {
|
|
11
|
+
const client = new AnthropicOAuthUsageClient();
|
|
12
|
+
try {
|
|
13
|
+
const snapshot = await client.fetch(creds);
|
|
14
|
+
return mapClaudeToEntry(snapshot, [], true, creds);
|
|
15
|
+
} catch (err) {
|
|
16
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
17
|
+
return mapClaudeToEntry(null, [message], true, creds);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
async fromWebSession(session: ClaudeWebSession): Promise<UsageProviderEntry> {
|
|
22
|
+
const client = new AnthropicWebUsageClient();
|
|
23
|
+
try {
|
|
24
|
+
const snapshot = await client.fetch(session);
|
|
25
|
+
return mapClaudeToEntry(snapshot, [], true, null);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
28
|
+
return mapClaudeToEntry(null, [message], true, null);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async fromOAuthTokenWithWebExtras(
|
|
33
|
+
oauthCreds: ClaudeOAuthCredentials,
|
|
34
|
+
webSession: ClaudeWebSession,
|
|
35
|
+
): Promise<UsageProviderEntry> {
|
|
36
|
+
const oauthClient = new AnthropicOAuthUsageClient();
|
|
37
|
+
const webClient = new AnthropicWebUsageClient();
|
|
38
|
+
|
|
39
|
+
let snapshot = await oauthClient.fetch(oauthCreds).catch((err: unknown) => {
|
|
40
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
41
|
+
return null;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!snapshot) {
|
|
45
|
+
const message = "OAuth fetch failed; cannot retrieve usage data";
|
|
46
|
+
return mapClaudeToEntry(null, [message], true, oauthCreds);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Merge OAuth credential metadata into snapshot for accountId generation
|
|
50
|
+
if (snapshot.subscriptionType === null && oauthCreds.subscriptionType) {
|
|
51
|
+
snapshot = { ...snapshot, subscriptionType: oauthCreds.subscriptionType };
|
|
52
|
+
}
|
|
53
|
+
if (snapshot.rateLimitTier === null && oauthCreds.rateLimitTier) {
|
|
54
|
+
snapshot = { ...snapshot, rateLimitTier: oauthCreds.rateLimitTier };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Best-effort web extras (designs, routines, cost)
|
|
58
|
+
try {
|
|
59
|
+
const webSnapshot = await webClient.fetch(webSession);
|
|
60
|
+
if (snapshot.extraRateWindows.length === 0 && webSnapshot.extraRateWindows.length > 0) {
|
|
61
|
+
snapshot = { ...snapshot, extraRateWindows: webSnapshot.extraRateWindows };
|
|
62
|
+
}
|
|
63
|
+
if (!snapshot.providerCost && webSnapshot.providerCost) {
|
|
64
|
+
snapshot = { ...snapshot, providerCost: webSnapshot.providerCost };
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Ignore web extras failures — do not change status to error
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return mapClaudeToEntry(snapshot, [], true, oauthCreds);
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
opencodeGo: {
|
|
75
|
+
async fromCookies(creds: { cookies: string; serverFnId: string }): Promise<UsageProviderEntry> {
|
|
76
|
+
const client = new OpenCodeGoUsageClient();
|
|
77
|
+
try {
|
|
78
|
+
const snapshot = await client.fetch(creds);
|
|
79
|
+
return mapOpenCodeGoToEntry(snapshot, [], true);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
82
|
+
return mapOpenCodeGoToEntry(null, [message], true);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
codex: {
|
|
88
|
+
async fromOAuthToken(creds: CodexOAuthCredentials): Promise<UsageProviderEntry> {
|
|
89
|
+
const client = new CodexOAuthUsageClient();
|
|
90
|
+
try {
|
|
91
|
+
const snapshot = await client.fetch(creds);
|
|
92
|
+
return mapCodexToEntry(snapshot, [], true, creds);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
95
|
+
return mapCodexToEntry(null, [message], true, creds);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { UsageClient } from "../client";
|
|
2
|
+
import type { ClaudeOAuthCredentials, ClaudeUsageSnapshot, NamedRateWindowLegacy, ProviderCostSnapshotLegacy, RateWindowLegacy } from "../types";
|
|
3
|
+
|
|
4
|
+
export class AuthenticationError extends Error {
|
|
5
|
+
constructor(message: string) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "AuthenticationError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class ScopeError extends Error {
|
|
12
|
+
constructor(message: string) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "ScopeError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ParseError extends Error {
|
|
19
|
+
constructor(message: string) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "ParseError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface OAuthUsageResponse {
|
|
26
|
+
five_hour?: { utilization?: number | null; resets_at?: string | null } | null;
|
|
27
|
+
seven_day?: { utilization?: number | null; resets_at?: string | null } | null;
|
|
28
|
+
seven_day_sonnet?: { utilization?: number | null; resets_at?: string | null } | null;
|
|
29
|
+
seven_day_opus?: { utilization?: number | null; resets_at?: string | null } | null;
|
|
30
|
+
seven_day_design?: { utilization?: number | null; resets_at?: string | null } | null;
|
|
31
|
+
seven_day_routines?: { utilization?: number | null; resets_at?: string | null } | null;
|
|
32
|
+
extra_usage?: {
|
|
33
|
+
is_enabled?: boolean;
|
|
34
|
+
monthly_limit?: number;
|
|
35
|
+
used_credits?: number;
|
|
36
|
+
currency?: string;
|
|
37
|
+
utilization?: number;
|
|
38
|
+
} | null;
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DESIGN_ALTERNATES = [
|
|
43
|
+
"seven_day_design",
|
|
44
|
+
"seven_day_claude_design",
|
|
45
|
+
"claude_design",
|
|
46
|
+
"design",
|
|
47
|
+
"seven_day_omelette",
|
|
48
|
+
"omelette",
|
|
49
|
+
"omelette_promotional",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const ROUTINES_ALTERNATES = [
|
|
53
|
+
"seven_day_routines",
|
|
54
|
+
"seven_day_claude_routines",
|
|
55
|
+
"claude_routines",
|
|
56
|
+
"routines",
|
|
57
|
+
"routine",
|
|
58
|
+
"seven_day_cowork",
|
|
59
|
+
"cowork",
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
function pickWindow(data: OAuthUsageResponse, keys: string[]): { utilization?: number | null; resets_at?: string | null } | null {
|
|
63
|
+
for (const key of keys) {
|
|
64
|
+
const val = data[key];
|
|
65
|
+
if (val && typeof val === "object" && "utilization" in val) {
|
|
66
|
+
return val as { utilization?: number | null; resets_at?: string | null };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function toRateWindow(
|
|
73
|
+
utilization: number | null | undefined,
|
|
74
|
+
resetsAt: string | null | undefined,
|
|
75
|
+
windowMinutes: number,
|
|
76
|
+
): RateWindowLegacy {
|
|
77
|
+
const usedPercent = typeof utilization === "number" ? Math.max(0, Math.min(100, utilization)) : 0;
|
|
78
|
+
const resetsAtDate = resetsAt ? new Date(resetsAt) : null;
|
|
79
|
+
const resetDescription = resetsAtDate && !Number.isNaN(resetsAtDate.getTime())
|
|
80
|
+
? resetsAtDate.toLocaleString()
|
|
81
|
+
: null;
|
|
82
|
+
return {
|
|
83
|
+
usedPercent,
|
|
84
|
+
windowMinutes,
|
|
85
|
+
resetsAt: resetsAtDate && !Number.isNaN(resetsAtDate.getTime()) ? resetsAtDate : null,
|
|
86
|
+
resetDescription,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class AnthropicOAuthUsageClient implements UsageClient<ClaudeOAuthCredentials, ClaudeUsageSnapshot> {
|
|
91
|
+
async fetch(creds: ClaudeOAuthCredentials, options?: { signal?: AbortSignal }): Promise<ClaudeUsageSnapshot> {
|
|
92
|
+
const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
|
93
|
+
signal: options?.signal,
|
|
94
|
+
headers: {
|
|
95
|
+
Authorization: `Bearer ${creds.accessToken}`,
|
|
96
|
+
Accept: "application/json",
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (res.status === 401) {
|
|
103
|
+
throw new AuthenticationError("OAuth token expired or invalid");
|
|
104
|
+
}
|
|
105
|
+
if (res.status === 403) {
|
|
106
|
+
const body = await res.text();
|
|
107
|
+
if (body.includes("user:profile")) {
|
|
108
|
+
throw new ScopeError("Token missing required scope user:profile");
|
|
109
|
+
}
|
|
110
|
+
throw new Error(`Server error: ${res.status}`);
|
|
111
|
+
}
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
throw new Error(`Server error: ${res.status}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const data = (await res.json()) as OAuthUsageResponse;
|
|
117
|
+
|
|
118
|
+
if (!data.five_hour || typeof data.five_hour.utilization !== "number") {
|
|
119
|
+
throw new ParseError("Missing five_hour.utilization");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const primary = toRateWindow(data.five_hour.utilization, data.five_hour.resets_at, 300);
|
|
123
|
+
const secondary = data.seven_day
|
|
124
|
+
? toRateWindow(data.seven_day.utilization, data.seven_day.resets_at, 10080)
|
|
125
|
+
: null;
|
|
126
|
+
|
|
127
|
+
const opusWindow = pickWindow(data, ["seven_day_sonnet", "seven_day_opus"]);
|
|
128
|
+
const opus = opusWindow ? toRateWindow(opusWindow.utilization, opusWindow.resets_at, 10080) : null;
|
|
129
|
+
|
|
130
|
+
const extraRateWindows: NamedRateWindowLegacy[] = [];
|
|
131
|
+
const designWindow = pickWindow(data, DESIGN_ALTERNATES);
|
|
132
|
+
if (designWindow && typeof designWindow.utilization === "number") {
|
|
133
|
+
extraRateWindows.push({
|
|
134
|
+
id: "claude-design",
|
|
135
|
+
title: "Designs",
|
|
136
|
+
window: toRateWindow(designWindow.utilization, designWindow.resets_at, 10080),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const routinesWindow = pickWindow(data, ROUTINES_ALTERNATES);
|
|
140
|
+
if (routinesWindow && typeof routinesWindow.utilization === "number") {
|
|
141
|
+
extraRateWindows.push({
|
|
142
|
+
id: "claude-routines",
|
|
143
|
+
title: "Daily Routines",
|
|
144
|
+
window: toRateWindow(routinesWindow.utilization, routinesWindow.resets_at, 10080),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let providerCost: ProviderCostSnapshotLegacy | null = null;
|
|
149
|
+
if (data.extra_usage?.is_enabled) {
|
|
150
|
+
const used = (data.extra_usage.used_credits ?? 0) / 100;
|
|
151
|
+
const limit = (data.extra_usage.monthly_limit ?? 0) / 100;
|
|
152
|
+
const currency = (data.extra_usage.currency || "USD").trim();
|
|
153
|
+
providerCost = {
|
|
154
|
+
used,
|
|
155
|
+
limit,
|
|
156
|
+
currencyCode: currency || "USD",
|
|
157
|
+
period: "Monthly",
|
|
158
|
+
resetsAt: null,
|
|
159
|
+
updatedAt: new Date(),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
source: "oauth",
|
|
165
|
+
primary,
|
|
166
|
+
secondary,
|
|
167
|
+
opus,
|
|
168
|
+
extraRateWindows,
|
|
169
|
+
providerCost,
|
|
170
|
+
updatedAt: new Date(),
|
|
171
|
+
accountEmail: null,
|
|
172
|
+
accountOrganization: null,
|
|
173
|
+
loginMethod: null,
|
|
174
|
+
subscriptionType: null,
|
|
175
|
+
rateLimitTier: null,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|