@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,273 @@
1
+ import type {
2
+ UsageProviderEntry,
3
+ UsageWindow,
4
+ ProviderCostSnapshot,
5
+ ClaudeUsageSnapshot,
6
+ CodexUsageSnapshot,
7
+ OpenCodeGoUsageSnapshot,
8
+ RateWindowLegacy,
9
+ ClaudeOAuthCredentials,
10
+ CodexOAuthCredentials,
11
+ } from "./types";
12
+
13
+
14
+ function toIsoString(d: Date | null): string | null {
15
+ return d && !Number.isNaN(d.getTime()) ? d.toISOString() : null;
16
+ }
17
+
18
+ function computeResetInSec(resetsAt: string | null | undefined): number | undefined {
19
+ if (!resetsAt) return undefined;
20
+ const d = new Date(resetsAt);
21
+ if (Number.isNaN(d.getTime())) return undefined;
22
+ return Math.max(0, Math.round(d.getTime() / 1000 - Date.now() / 1000));
23
+ }
24
+
25
+ function computeResetsAt(resetInSec: number | undefined): string | undefined {
26
+ if (resetInSec === undefined || resetInSec < 0) return undefined;
27
+ return new Date(Date.now() + resetInSec * 1000).toISOString();
28
+ }
29
+
30
+ function mapRateWindowLegacyToWindow(id: string, title: string, rw: RateWindowLegacy | null): UsageWindow | undefined {
31
+ if (!rw) return undefined;
32
+ const resetsAt = toIsoString(rw.resetsAt);
33
+ return {
34
+ id,
35
+ title,
36
+ usedPercent: rw.usedPercent,
37
+ windowMinutes: rw.windowMinutes,
38
+ resetsAt,
39
+ resetInSec: computeResetInSec(resetsAt),
40
+ };
41
+ }
42
+
43
+ function simpleHash(input: string): string {
44
+ let hash = 0;
45
+ for (let i = 0; i < input.length; i++) {
46
+ const char = input.charCodeAt(i);
47
+ hash = ((hash << 5) - hash + char) | 0;
48
+ }
49
+ return Math.abs(hash).toString(16).padStart(8, "0");
50
+ }
51
+
52
+ export function buildClaudeAccountId(
53
+ snapshot: ClaudeUsageSnapshot | null,
54
+ oauthCreds: ClaudeOAuthCredentials | null,
55
+ ): string | null {
56
+ // Best source: email from Web API
57
+ if (snapshot?.accountEmail) {
58
+ return snapshot.accountEmail;
59
+ }
60
+ // Second best: org UUID from Web API (not in snapshot currently, but we can add if needed)
61
+ // For now, use Keychain metadata as fallback
62
+ if (oauthCreds?.subscriptionType || oauthCreds?.rateLimitTier) {
63
+ const parts = ["claude"];
64
+ if (oauthCreds.subscriptionType) parts.push(oauthCreds.subscriptionType);
65
+ if (oauthCreds.rateLimitTier) parts.push(oauthCreds.rateLimitTier);
66
+ if (oauthCreds.scopes && oauthCreds.scopes.length > 0) {
67
+ // Add a short hash of scopes to distinguish accounts with same tier
68
+ const scopesHash = simpleHash(oauthCreds.scopes.join(","));
69
+ parts.push(scopesHash);
70
+ }
71
+ return parts.join("-");
72
+ }
73
+ return null;
74
+ }
75
+
76
+ export function mapClaudeToEntry(
77
+ snapshot: ClaudeUsageSnapshot | null,
78
+ errors: string[] = [],
79
+ available: boolean,
80
+ oauthCreds: ClaudeOAuthCredentials | null,
81
+ ): UsageProviderEntry {
82
+ const base: UsageProviderEntry = {
83
+ provider: "claude",
84
+ accountId: buildClaudeAccountId(snapshot, oauthCreds),
85
+ available,
86
+ status: snapshot ? (errors.length > 0 ? "error" : "ok") : available ? "error" : "unavailable",
87
+ };
88
+
89
+ if (!snapshot) {
90
+ if (errors.length > 0) {
91
+ base.errors = errors;
92
+ }
93
+ return base;
94
+ }
95
+
96
+ const windows: UsageWindow[] = [];
97
+
98
+ const primary = mapRateWindowLegacyToWindow("session", "Current Session", snapshot.primary);
99
+ if (primary) windows.push(primary);
100
+
101
+ const secondary = mapRateWindowLegacyToWindow("weekly", "Weekly", snapshot.secondary);
102
+ if (secondary) windows.push(secondary);
103
+
104
+ const opus = mapRateWindowLegacyToWindow("opus", "Opus", snapshot.opus);
105
+ if (opus) windows.push(opus);
106
+
107
+ for (const extra of snapshot.extraRateWindows) {
108
+ const w = mapRateWindowLegacyToWindow(extra.id, extra.title, extra.window);
109
+ if (w) windows.push(w);
110
+ }
111
+
112
+ let cost: ProviderCostSnapshot | null = null;
113
+ if (snapshot.providerCost) {
114
+ cost = {
115
+ used: snapshot.providerCost.used,
116
+ limit: snapshot.providerCost.limit,
117
+ currency: snapshot.providerCost.currencyCode,
118
+ period: snapshot.providerCost.period,
119
+ resetsAt: toIsoString(snapshot.providerCost.resetsAt),
120
+ updatedAt: toIsoString(snapshot.providerCost.updatedAt) ?? new Date().toISOString(),
121
+ };
122
+ }
123
+
124
+ base.metadata = {
125
+ dataSource: snapshot.source,
126
+ accountEmail: snapshot.accountEmail,
127
+ accountOrganization: snapshot.accountOrganization,
128
+ subscriptionType: snapshot.subscriptionType,
129
+ rateLimitTier: snapshot.rateLimitTier,
130
+ };
131
+
132
+ if (errors.length > 0) {
133
+ base.errors = errors;
134
+ }
135
+
136
+ base.windows = windows;
137
+ base.cost = cost;
138
+ base.updatedAt = toIsoString(snapshot.updatedAt) ?? new Date().toISOString();
139
+
140
+ return base;
141
+ }
142
+
143
+ export function buildCodexAccountId(
144
+ snapshot: CodexUsageSnapshot | null,
145
+ oauthCreds: CodexOAuthCredentials | null,
146
+ ): string | null {
147
+ if (oauthCreds?.accountEmail) {
148
+ return oauthCreds.accountEmail;
149
+ }
150
+ if (oauthCreds?.accountId) {
151
+ return oauthCreds.accountId;
152
+ }
153
+ if (snapshot?.accountEmail) {
154
+ return snapshot.accountEmail;
155
+ }
156
+ if (snapshot?.planType) {
157
+ return `codex-${snapshot.planType}`;
158
+ }
159
+ return null;
160
+ }
161
+
162
+ export function mapCodexToEntry(
163
+ snapshot: CodexUsageSnapshot | null,
164
+ errors: string[] = [],
165
+ available: boolean,
166
+ oauthCreds: CodexOAuthCredentials | null,
167
+ ): UsageProviderEntry {
168
+ const base: UsageProviderEntry = {
169
+ provider: "codex",
170
+ accountId: buildCodexAccountId(snapshot, oauthCreds),
171
+ available,
172
+ status: snapshot ? (errors.length > 0 ? "error" : "ok") : available ? "error" : "unavailable",
173
+ };
174
+
175
+ if (!snapshot) {
176
+ if (errors.length > 0) {
177
+ base.errors = errors;
178
+ }
179
+ return base;
180
+ }
181
+
182
+ const windows: UsageWindow[] = [];
183
+
184
+ const primary = mapRateWindowLegacyToWindow("session", "Current Session", snapshot.primaryWindow);
185
+ if (primary) windows.push(primary);
186
+
187
+ const secondary = mapRateWindowLegacyToWindow("weekly", "Weekly", snapshot.secondaryWindow);
188
+ if (secondary) windows.push(secondary);
189
+
190
+ // Codex usage API provides rate windows (percentages) and credits (balance/remaining).
191
+ // It does not expose separate "used" vs "limit" absolute amounts, so a
192
+ // ProviderCostSnapshot cannot be accurately constructed. Only rate windows are
193
+ // mapped; credit balance is surfaced via metadata instead of cost.
194
+ const cost: ProviderCostSnapshot | null = null;
195
+
196
+ base.metadata = {
197
+ dataSource: snapshot.source,
198
+ accountEmail: snapshot.accountEmail ?? oauthCreds?.accountEmail,
199
+ subscriptionType: snapshot.planType,
200
+ };
201
+
202
+ if (errors.length > 0) {
203
+ base.errors = errors;
204
+ }
205
+
206
+ base.windows = windows;
207
+ base.cost = cost;
208
+ base.updatedAt = toIsoString(snapshot.updatedAt) ?? new Date().toISOString();
209
+
210
+ return base;
211
+ }
212
+
213
+ export function mapOpenCodeGoToEntry(
214
+ snapshot: OpenCodeGoUsageSnapshot | null,
215
+ errors: string[] = [],
216
+ available: boolean,
217
+ ): UsageProviderEntry {
218
+ const base: UsageProviderEntry = {
219
+ provider: "opencode-go",
220
+ accountId: snapshot?.workspaceId ?? null,
221
+ available,
222
+ status: snapshot ? (errors.length > 0 ? "error" : "ok") : available ? "error" : "unavailable",
223
+ };
224
+
225
+ if (!snapshot) {
226
+ if (errors.length > 0) {
227
+ base.errors = errors;
228
+ }
229
+ return base;
230
+ }
231
+
232
+ const windows: UsageWindow[] = [
233
+ {
234
+ id: "session",
235
+ title: "Rolling",
236
+ usedPercent: snapshot.rollingUsagePercent,
237
+ resetInSec: snapshot.rollingResetInSec,
238
+ resetsAt: computeResetsAt(snapshot.rollingResetInSec),
239
+ },
240
+ {
241
+ id: "weekly",
242
+ title: "Weekly",
243
+ usedPercent: snapshot.weeklyUsagePercent,
244
+ resetInSec: snapshot.weeklyResetInSec,
245
+ resetsAt: computeResetsAt(snapshot.weeklyResetInSec),
246
+ },
247
+ ];
248
+
249
+ if (snapshot.hasMonthlyUsage) {
250
+ windows.push({
251
+ id: "monthly",
252
+ title: "Monthly",
253
+ usedPercent: snapshot.monthlyUsagePercent,
254
+ resetInSec: snapshot.monthlyResetInSec,
255
+ resetsAt: computeResetsAt(snapshot.monthlyResetInSec),
256
+ });
257
+ }
258
+
259
+ base.metadata = {
260
+ dataSource: snapshot.source,
261
+ workspaceId: snapshot.workspaceId,
262
+ workspaceName: snapshot.workspaceName,
263
+ };
264
+
265
+ if (errors.length > 0) {
266
+ base.errors = errors;
267
+ }
268
+
269
+ base.windows = windows;
270
+ base.updatedAt = toIsoString(snapshot.updatedAt) ?? new Date().toISOString();
271
+
272
+ return base;
273
+ }
@@ -0,0 +1,54 @@
1
+ import type { TokenResolver } from "./resolver";
2
+ import type { ClaudePlannerDecision, ClaudeDataSource, ClaudeOAuthCredentials, ClaudeWebSession } from "./types";
3
+
4
+ export interface ClaudeSourcePlannerConfig {
5
+ dataSource: ClaudeDataSource;
6
+ webExtrasEnabled: boolean;
7
+ }
8
+
9
+ export class ClaudeSourcePlanner {
10
+ constructor(
11
+ private readonly config: ClaudeSourcePlannerConfig,
12
+ private readonly oauthResolver: TokenResolver<ClaudeOAuthCredentials>,
13
+ private readonly webResolver: TokenResolver<ClaudeWebSession>,
14
+ private readonly cliResolver: TokenResolver<string>,
15
+ ) {}
16
+
17
+ async decide(): Promise<ClaudePlannerDecision> {
18
+ const [hasOAuth, hasWeb, hasCli] = await Promise.all([
19
+ this.oauthResolver.isAvailable(),
20
+ this.webResolver.isAvailable(),
21
+ this.cliResolver.isAvailable(),
22
+ ]);
23
+
24
+ const selected = this.config.dataSource;
25
+ const webExtras = this.config.webExtrasEnabled;
26
+
27
+ if (selected === "oauth") {
28
+ return { primary: "oauth", fallback: [], webExtras };
29
+ }
30
+ if (selected === "web") {
31
+ return { primary: "web", fallback: [], webExtras };
32
+ }
33
+ if (selected === "cli") {
34
+ return { primary: "cli", fallback: [], webExtras };
35
+ }
36
+
37
+ // Auto mode
38
+ const fallback: ClaudeDataSource[] = [];
39
+ if (hasOAuth) {
40
+ if (hasWeb) fallback.push("web");
41
+ if (hasCli) fallback.push("cli");
42
+ return { primary: "oauth", fallback, webExtras };
43
+ }
44
+ if (hasWeb) {
45
+ if (hasCli) fallback.push("cli");
46
+ return { primary: "web", fallback, webExtras };
47
+ }
48
+ if (hasCli) {
49
+ return { primary: "cli", fallback: [], webExtras };
50
+ }
51
+
52
+ return { primary: "auto", fallback: [], webExtras };
53
+ }
54
+ }
@@ -0,0 +1,113 @@
1
+ import type { UsageClient } from "../client";
2
+ import type { ClaudeOAuthCredentials, ClaudeUsageSnapshot, ClaudeWebSession, UsageProviderEntry } from "../types";
3
+ import { ClaudeSourcePlanner } from "../planner";
4
+ import { AnthropicOAuthUsageClient, AuthenticationError, ScopeError } from "../clients/anthropic-oauth-usage";
5
+ import { AnthropicWebUsageClient, InvalidCredentialsError } from "../clients/anthropic-web-usage";
6
+ import { ClaudeCliProbeClient } from "../clients/claude-cli-probe";
7
+ import type { TokenResolver } from "../resolver";
8
+ import { mapClaudeToEntry } from "../mappers";
9
+
10
+ export class ClaudeUsageProvider {
11
+ private readonly oauthClient: UsageClient<ClaudeOAuthCredentials, ClaudeUsageSnapshot>;
12
+ private readonly webClient: UsageClient<ClaudeWebSession, ClaudeUsageSnapshot>;
13
+ private readonly cliClient: UsageClient<string, ClaudeUsageSnapshot>;
14
+
15
+ constructor(
16
+ private readonly planner: ClaudeSourcePlanner,
17
+ private readonly oauthResolver: TokenResolver<ClaudeOAuthCredentials>,
18
+ private readonly webResolver: TokenResolver<ClaudeWebSession>,
19
+ private readonly cliResolver: TokenResolver<string>,
20
+ ) {
21
+ this.oauthClient = new AnthropicOAuthUsageClient();
22
+ this.webClient = new AnthropicWebUsageClient();
23
+ this.cliClient = new ClaudeCliProbeClient();
24
+ }
25
+
26
+ async fetch(options?: { signal?: AbortSignal }): Promise<{ entry: UsageProviderEntry; snapshot: ClaudeUsageSnapshot | null }> {
27
+ const decision = await this.planner.decide();
28
+ const errors: string[] = [];
29
+
30
+ if (decision.primary === "auto") {
31
+ // No credentials available on this machine
32
+ return { entry: mapClaudeToEntry(null, [], false, null), snapshot: null };
33
+ }
34
+
35
+ // Resolve all credentials upfront for both fetch and accountId generation
36
+ const [oauthCreds, webCreds, cliCreds] = await Promise.all([
37
+ this.oauthResolver.resolve(),
38
+ this.webResolver.resolve(),
39
+ this.cliResolver.resolve(),
40
+ ]);
41
+
42
+ const sources: Array<{ type: "oauth" | "web" | "cli"; creds: unknown }> = [];
43
+
44
+ // Primary
45
+ if (decision.primary === "oauth" && oauthCreds) {
46
+ sources.push({ type: "oauth", creds: oauthCreds });
47
+ } else if (decision.primary === "web" && webCreds) {
48
+ sources.push({ type: "web", creds: webCreds });
49
+ } else if (decision.primary === "cli" && cliCreds) {
50
+ sources.push({ type: "cli", creds: cliCreds });
51
+ }
52
+
53
+ // Fallbacks
54
+ for (const fb of decision.fallback) {
55
+ if (fb === "oauth" && oauthCreds) {
56
+ sources.push({ type: "oauth", creds: oauthCreds });
57
+ } else if (fb === "web" && webCreds) {
58
+ sources.push({ type: "web", creds: webCreds });
59
+ } else if (fb === "cli" && cliCreds) {
60
+ sources.push({ type: "cli", creds: cliCreds });
61
+ }
62
+ }
63
+
64
+ let snapshot: ClaudeUsageSnapshot | null = null;
65
+
66
+ for (const source of sources) {
67
+ try {
68
+ if (source.type === "oauth") {
69
+ snapshot = await this.oauthClient.fetch(source.creds as ClaudeOAuthCredentials, options);
70
+ } else if (source.type === "web") {
71
+ snapshot = await this.webClient.fetch(source.creds as ClaudeWebSession, options);
72
+ } else if (source.type === "cli") {
73
+ snapshot = await this.cliClient.fetch(source.creds as string, options);
74
+ }
75
+ break;
76
+ } catch (err) {
77
+ const message = err instanceof Error ? err.message : String(err);
78
+ errors.push(`${source.type}: ${message}`);
79
+ if (err instanceof InvalidCredentialsError || err instanceof AuthenticationError || err instanceof ScopeError) {
80
+ continue;
81
+ }
82
+ // Continue to fallback for other errors too
83
+ }
84
+ }
85
+
86
+ // Merge OAuth credential metadata into snapshot for accountId generation
87
+ if (snapshot && oauthCreds) {
88
+ if (snapshot.subscriptionType === null && oauthCreds.subscriptionType) {
89
+ snapshot = { ...snapshot, subscriptionType: oauthCreds.subscriptionType };
90
+ }
91
+ if (snapshot.rateLimitTier === null && oauthCreds.rateLimitTier) {
92
+ snapshot = { ...snapshot, rateLimitTier: oauthCreds.rateLimitTier };
93
+ }
94
+ }
95
+
96
+ // Web extras merge
97
+ if (snapshot && decision.webExtras && snapshot.source !== "web" && webCreds) {
98
+ try {
99
+ const webSnapshot = await this.webClient.fetch(webCreds, options);
100
+ if (snapshot.extraRateWindows.length === 0 && webSnapshot.extraRateWindows.length > 0) {
101
+ snapshot = { ...snapshot, extraRateWindows: webSnapshot.extraRateWindows };
102
+ }
103
+ if (!snapshot.providerCost && webSnapshot.providerCost) {
104
+ snapshot = { ...snapshot, providerCost: webSnapshot.providerCost };
105
+ }
106
+ } catch {
107
+ // Ignore web extras failures
108
+ }
109
+ }
110
+
111
+ return { entry: mapClaudeToEntry(snapshot, errors, true, oauthCreds), snapshot };
112
+ }
113
+ }
@@ -0,0 +1,34 @@
1
+ import type { UsageClient } from "../client";
2
+ import type { CodexOAuthCredentials, CodexUsageSnapshot, UsageProviderEntry } from "../types";
3
+ import type { TokenResolver } from "../resolver";
4
+ import { CodexOAuthUsageClient } from "../clients/codex-oauth-usage";
5
+ import { mapCodexToEntry } from "../mappers";
6
+
7
+ export class CodexUsageProvider {
8
+ private readonly oauthClient: UsageClient<CodexOAuthCredentials, CodexUsageSnapshot>;
9
+
10
+ constructor(
11
+ private readonly oauthResolver: TokenResolver<CodexOAuthCredentials>,
12
+ ) {
13
+ this.oauthClient = new CodexOAuthUsageClient();
14
+ }
15
+
16
+ async fetch(options?: { signal?: AbortSignal }): Promise<{ entry: UsageProviderEntry; snapshot: CodexUsageSnapshot | null }> {
17
+ const errors: string[] = [];
18
+ const oauthCreds = await this.oauthResolver.resolve();
19
+
20
+ if (!oauthCreds) {
21
+ return { entry: mapCodexToEntry(null, [], false, null), snapshot: null };
22
+ }
23
+
24
+ let snapshot: CodexUsageSnapshot | null = null;
25
+ try {
26
+ snapshot = await this.oauthClient.fetch(oauthCreds, options);
27
+ } catch (err) {
28
+ const message = err instanceof Error ? err.message : String(err);
29
+ errors.push(`oauth: ${message}`);
30
+ }
31
+
32
+ return { entry: mapCodexToEntry(snapshot, errors, true, oauthCreds), snapshot };
33
+ }
34
+ }
@@ -0,0 +1,37 @@
1
+ import type { TokenResolver } from "../resolver";
2
+ import type { OpenCodeGoUsageSnapshot } from "../types";
3
+ import type { UsageProviderEntry } from "../types";
4
+ import { OpenCodeGoUsageClient } from "../clients/opencode-go-usage";
5
+ import { mapOpenCodeGoToEntry } from "../mappers";
6
+
7
+ export class OpenCodeGoUsageProvider {
8
+ private readonly client = new OpenCodeGoUsageClient();
9
+
10
+ constructor(
11
+ private readonly resolver: TokenResolver<{ cookies: string }>,
12
+ private readonly serverFnId: string,
13
+ ) {}
14
+
15
+ async fetch(options?: { signal?: AbortSignal }): Promise<{ entry: UsageProviderEntry; snapshot: OpenCodeGoUsageSnapshot | null }> {
16
+ const errors: string[] = [];
17
+ const available = await this.resolver.isAvailable();
18
+ if (!available) {
19
+ return { entry: mapOpenCodeGoToEntry(null, [], false), snapshot: null };
20
+ }
21
+
22
+ const creds = await this.resolver.resolve();
23
+ if (!creds) {
24
+ // Should not happen if isAvailable() is correct
25
+ return { entry: mapOpenCodeGoToEntry(null, [], false), snapshot: null };
26
+ }
27
+
28
+ try {
29
+ const snapshot = await this.client.fetch({ cookies: creds.cookies, serverFnId: this.serverFnId }, options);
30
+ return { entry: mapOpenCodeGoToEntry(snapshot, errors, true), snapshot };
31
+ } catch (err) {
32
+ const message = err instanceof Error ? err.message : String(err);
33
+ errors.push(`OpenCode Go: ${message}`);
34
+ return { entry: mapOpenCodeGoToEntry(null, errors, true), snapshot: null };
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,4 @@
1
+ export interface TokenResolver<T> {
2
+ resolve(): Promise<T | null>;
3
+ isAvailable(): Promise<boolean>;
4
+ }