@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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +836 -0
  3. package/package.json +75 -0
  4. package/src/api/docs/sessions.keys.ts +6 -0
  5. package/src/api/docs/sessions.ts +81 -0
  6. package/src/api/docs/system.keys.ts +5 -0
  7. package/src/api/docs/system.ts +66 -0
  8. package/src/api/docs/usage.keys.ts +3 -0
  9. package/src/api/docs/usage.ts +24 -0
  10. package/src/api/docs/webhooks.keys.ts +5 -0
  11. package/src/api/docs/webhooks.ts +78 -0
  12. package/src/api/index.ts +18 -0
  13. package/src/api/lib/auth.ts +43 -0
  14. package/src/api/lib/errors.ts +28 -0
  15. package/src/api/lib/route.ts +18 -0
  16. package/src/api/models/claude.ts +4 -0
  17. package/src/api/models/codex.ts +4 -0
  18. package/src/api/models/opencode.ts +11 -0
  19. package/src/api/routes/sessions.ts +47 -0
  20. package/src/api/routes/system.ts +103 -0
  21. package/src/api/routes/usage.ts +15 -0
  22. package/src/api/routes/webhooks/claude.ts +161 -0
  23. package/src/api/routes/webhooks/codex.ts +175 -0
  24. package/src/api/routes/webhooks/opencode.ts +107 -0
  25. package/src/api/routes.ts +93 -0
  26. package/src/api/schemas/common.ts +21 -0
  27. package/src/api/schemas/sessions.ts +72 -0
  28. package/src/api/schemas/usage.ts +50 -0
  29. package/src/api/schemas/webhooks/claude.ts +32 -0
  30. package/src/api/schemas/webhooks/codex.ts +34 -0
  31. package/src/api/schemas/webhooks/opencode.ts +29 -0
  32. package/src/api/services/claude-session.ts +9 -0
  33. package/src/api/services/codex-session.ts +8 -0
  34. package/src/api/services/opencode-session.ts +73 -0
  35. package/src/classifier.ts +206 -0
  36. package/src/cli/commands.ts +498 -0
  37. package/src/cli/prompt.ts +44 -0
  38. package/src/daemon/cli.ts +27 -0
  39. package/src/daemon/collectors/claude/README.md +108 -0
  40. package/src/daemon/collectors/claude/collector.ts +413 -0
  41. package/src/daemon/collectors/claude/discovery.ts +148 -0
  42. package/src/daemon/collectors/claude/hooks.ts +181 -0
  43. package/src/daemon/collectors/claude/index.ts +8 -0
  44. package/src/daemon/collectors/claude/jsonl.ts +85 -0
  45. package/src/daemon/collectors/claude/transcript.ts +246 -0
  46. package/src/daemon/collectors/codex/README.md +147 -0
  47. package/src/daemon/collectors/codex/collector.ts +661 -0
  48. package/src/daemon/collectors/codex/discovery.ts +104 -0
  49. package/src/daemon/collectors/codex/hooks.ts +179 -0
  50. package/src/daemon/collectors/codex/index.ts +7 -0
  51. package/src/daemon/collectors/codex/jsonl.ts +108 -0
  52. package/src/daemon/collectors/codex/transcript.ts +299 -0
  53. package/src/daemon/collectors/git.ts +295 -0
  54. package/src/daemon/collectors/opencode/README.md +106 -0
  55. package/src/daemon/collectors/opencode/collector.ts +543 -0
  56. package/src/daemon/collectors/opencode/discovery.ts +257 -0
  57. package/src/daemon/collectors/opencode/index.ts +5 -0
  58. package/src/daemon/collectors/opencode/session-builder.ts +59 -0
  59. package/src/daemon/collectors/opencode-db.ts +282 -0
  60. package/src/daemon/collectors/process.ts +201 -0
  61. package/src/daemon/collectors/tmux/README.md +128 -0
  62. package/src/daemon/collectors/tmux/collector.ts +472 -0
  63. package/src/daemon/collectors/tmux/enrichment.ts +241 -0
  64. package/src/daemon/collectors/tmux/index.ts +4 -0
  65. package/src/daemon/collectors/tmux/parse.ts +128 -0
  66. package/src/daemon/collectors/usage.ts +193 -0
  67. package/src/daemon/index.ts +207 -0
  68. package/src/daemon/scheduler.ts +86 -0
  69. package/src/daemon/store.ts +535 -0
  70. package/src/detect.ts +143 -0
  71. package/src/hooks/claude.ts +596 -0
  72. package/src/hooks/codex.ts +517 -0
  73. package/src/legacy/classifier.ts +215 -0
  74. package/src/legacy/detect.ts +143 -0
  75. package/src/mcp/client-server.ts +21 -0
  76. package/src/mcp/http-handler.ts +126 -0
  77. package/src/mcp/index.ts +61 -0
  78. package/src/mcp/resources/config-current.ts +19 -0
  79. package/src/mcp/resources/index.ts +11 -0
  80. package/src/mcp/resources/sessions-active.ts +19 -0
  81. package/src/mcp/resources/sessions-all.ts +19 -0
  82. package/src/mcp/schemas/sessions.ts +3 -0
  83. package/src/mcp/server.ts +17 -0
  84. package/src/mcp/services/api-adapter.ts +48 -0
  85. package/src/mcp/services/data-source.ts +22 -0
  86. package/src/mcp/services/store-adapter.ts +40 -0
  87. package/src/mcp/tools/get-active-sessions.ts +16 -0
  88. package/src/mcp/tools/get-session-children.ts +24 -0
  89. package/src/mcp/tools/get-session-messages.ts +24 -0
  90. package/src/mcp/tools/get-session.ts +23 -0
  91. package/src/mcp/tools/index.ts +15 -0
  92. package/src/mcp/tools/list-sessions.ts +16 -0
  93. package/src/neo-blessed.d.ts +4 -0
  94. package/src/oc/index.ts +302 -0
  95. package/src/service/manager.ts +242 -0
  96. package/src/shared/config.ts +478 -0
  97. package/src/shared/ipc.ts +80 -0
  98. package/src/shared/logger.ts +248 -0
  99. package/src/shared/paths.ts +73 -0
  100. package/src/shared/platform.ts +110 -0
  101. package/src/shared/session-view.ts +241 -0
  102. package/src/shared/types.ts +299 -0
  103. package/src/shared/utils.ts +339 -0
  104. package/src/tui/app.ts +920 -0
  105. package/src/tui/client.ts +149 -0
  106. package/src/tui/index.ts +181 -0
  107. package/src/types.ts +113 -0
  108. package/src/usage/README.md +323 -0
  109. package/src/usage/api.ts +99 -0
  110. package/src/usage/client.ts +3 -0
  111. package/src/usage/clients/anthropic-oauth-usage.ts +178 -0
  112. package/src/usage/clients/anthropic-web-usage.ts +245 -0
  113. package/src/usage/clients/claude-cli-probe.ts +187 -0
  114. package/src/usage/clients/codex-oauth-usage.ts +113 -0
  115. package/src/usage/clients/opencode-go-usage.ts +441 -0
  116. package/src/usage/index.ts +19 -0
  117. package/src/usage/mappers.ts +273 -0
  118. package/src/usage/planner.ts +54 -0
  119. package/src/usage/providers/claude.ts +113 -0
  120. package/src/usage/providers/codex.ts +34 -0
  121. package/src/usage/providers/opencode-go.ts +37 -0
  122. package/src/usage/resolver.ts +4 -0
  123. package/src/usage/resolvers/browser-cookies.ts +198 -0
  124. package/src/usage/resolvers/claude-auth.ts +99 -0
  125. package/src/usage/resolvers/claude-cli.ts +24 -0
  126. package/src/usage/resolvers/claude-web.ts +16 -0
  127. package/src/usage/resolvers/codex-auth.ts +117 -0
  128. package/src/usage/resolvers/opencode-go-web.ts +14 -0
  129. 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
@@ -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,3 @@
1
+ export interface UsageClient<T, R> {
2
+ fetch(creds: T, options?: { signal?: AbortSignal }): Promise<R>;
3
+ }
@@ -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
+ }