@scrylog/cli 0.3.0 → 0.3.2

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 (195) hide show
  1. package/dist/assets/highlights-eq9cgrbb.scm +604 -0
  2. package/dist/assets/highlights-ghv9g403.scm +205 -0
  3. package/dist/assets/highlights-hk7bwhj4.scm +284 -0
  4. package/dist/assets/highlights-r812a2qc.scm +150 -0
  5. package/dist/assets/highlights-x6tmsnaa.scm +115 -0
  6. package/dist/assets/injections-73j83es3.scm +27 -0
  7. package/dist/assets/parser.worker-xyhf83rf.js +4563 -0
  8. package/dist/assets/tree-sitter-3jzf13jk.wasm +0 -0
  9. package/dist/assets/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  10. package/dist/assets/tree-sitter-markdown-411r6y9b.wasm +0 -0
  11. package/dist/assets/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  12. package/dist/assets/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  13. package/dist/assets/tree-sitter-zig-e78zbjpm.wasm +0 -0
  14. package/dist/cli/index.js +499 -0
  15. package/dist/cli/index.js.map +61 -0
  16. package/dist/daemon/index.js +670 -0
  17. package/dist/daemon/index.js.map +1475 -0
  18. package/dist/hooks/claude.js +6 -0
  19. package/dist/hooks/claude.js.map +11 -0
  20. package/dist/hooks/codex.js +10 -0
  21. package/dist/hooks/codex.js.map +11 -0
  22. package/dist/mcp/index.js +100 -0
  23. package/dist/mcp/index.js.map +231 -0
  24. package/dist/oc/index.js +21 -0
  25. package/dist/oc/index.js.map +10 -0
  26. package/package.json +16 -13
  27. package/src/api/docs/sessions.keys.ts +0 -6
  28. package/src/api/docs/sessions.ts +0 -82
  29. package/src/api/docs/system.keys.ts +0 -5
  30. package/src/api/docs/system.ts +0 -69
  31. package/src/api/docs/usage.keys.ts +0 -3
  32. package/src/api/docs/usage.ts +0 -24
  33. package/src/api/docs/webhooks.keys.ts +0 -5
  34. package/src/api/docs/webhooks.ts +0 -78
  35. package/src/api/index.ts +0 -18
  36. package/src/api/lib/auth.ts +0 -43
  37. package/src/api/lib/errors.ts +0 -28
  38. package/src/api/lib/route.ts +0 -18
  39. package/src/api/models/claude.ts +0 -4
  40. package/src/api/models/codex.ts +0 -4
  41. package/src/api/models/opencode.ts +0 -11
  42. package/src/api/routes/sessions.ts +0 -51
  43. package/src/api/routes/system.ts +0 -106
  44. package/src/api/routes/usage.ts +0 -15
  45. package/src/api/routes/webhooks/claude.ts +0 -149
  46. package/src/api/routes/webhooks/codex.ts +0 -175
  47. package/src/api/routes/webhooks/opencode.ts +0 -112
  48. package/src/api/routes.ts +0 -93
  49. package/src/api/schemas/common.ts +0 -29
  50. package/src/api/schemas/sessions.ts +0 -86
  51. package/src/api/schemas/usage.ts +0 -51
  52. package/src/api/schemas/webhooks/claude.ts +0 -32
  53. package/src/api/schemas/webhooks/codex.ts +0 -34
  54. package/src/api/schemas/webhooks/opencode.ts +0 -29
  55. package/src/api/services/claude-session.ts +0 -9
  56. package/src/api/services/codex-session.ts +0 -8
  57. package/src/api/services/opencode-session.ts +0 -73
  58. package/src/classifier.ts +0 -206
  59. package/src/cli/commands.ts +0 -714
  60. package/src/cli/index.ts +0 -18
  61. package/src/cli/program.ts +0 -175
  62. package/src/cli/prompt.ts +0 -44
  63. package/src/daemon/cli.ts +0 -27
  64. package/src/daemon/collectors/claude/README.md +0 -108
  65. package/src/daemon/collectors/claude/collector.ts +0 -423
  66. package/src/daemon/collectors/claude/discovery.ts +0 -127
  67. package/src/daemon/collectors/claude/hooks.ts +0 -181
  68. package/src/daemon/collectors/claude/index.ts +0 -8
  69. package/src/daemon/collectors/claude/jsonl.ts +0 -85
  70. package/src/daemon/collectors/claude/transcript.ts +0 -246
  71. package/src/daemon/collectors/codex/README.md +0 -147
  72. package/src/daemon/collectors/codex/collector.ts +0 -661
  73. package/src/daemon/collectors/codex/discovery.ts +0 -104
  74. package/src/daemon/collectors/codex/hooks.ts +0 -179
  75. package/src/daemon/collectors/codex/index.ts +0 -7
  76. package/src/daemon/collectors/codex/jsonl.ts +0 -108
  77. package/src/daemon/collectors/codex/transcript.ts +0 -299
  78. package/src/daemon/collectors/git.ts +0 -353
  79. package/src/daemon/collectors/herdr/collector.ts +0 -128
  80. package/src/daemon/collectors/herdr/index.ts +0 -2
  81. package/src/daemon/collectors/herdr/normalization.ts +0 -111
  82. package/src/daemon/collectors/opencode/README.md +0 -106
  83. package/src/daemon/collectors/opencode/collector.ts +0 -545
  84. package/src/daemon/collectors/opencode/discovery.ts +0 -257
  85. package/src/daemon/collectors/opencode/index.ts +0 -5
  86. package/src/daemon/collectors/opencode/session-builder.ts +0 -59
  87. package/src/daemon/collectors/opencode-db.ts +0 -294
  88. package/src/daemon/collectors/process.ts +0 -201
  89. package/src/daemon/collectors/tmux/README.md +0 -128
  90. package/src/daemon/collectors/tmux/collector.ts +0 -492
  91. package/src/daemon/collectors/tmux/enrichment.ts +0 -242
  92. package/src/daemon/collectors/tmux/index.ts +0 -4
  93. package/src/daemon/collectors/tmux/parse.ts +0 -128
  94. package/src/daemon/collectors/usage.ts +0 -259
  95. package/src/daemon/index.ts +0 -209
  96. package/src/daemon/scheduler.ts +0 -86
  97. package/src/daemon/store.ts +0 -630
  98. package/src/detect.ts +0 -143
  99. package/src/hooks/claude.ts +0 -596
  100. package/src/hooks/codex.ts +0 -517
  101. package/src/legacy/classifier.ts +0 -215
  102. package/src/legacy/detect.ts +0 -143
  103. package/src/mcp/client-server.ts +0 -21
  104. package/src/mcp/http-handler.ts +0 -126
  105. package/src/mcp/index.ts +0 -61
  106. package/src/mcp/resources/config-current.ts +0 -19
  107. package/src/mcp/resources/index.ts +0 -11
  108. package/src/mcp/resources/sessions-active.ts +0 -19
  109. package/src/mcp/resources/sessions-all.ts +0 -19
  110. package/src/mcp/schemas/sessions.ts +0 -3
  111. package/src/mcp/server.ts +0 -17
  112. package/src/mcp/services/api-adapter.ts +0 -48
  113. package/src/mcp/services/data-source.ts +0 -22
  114. package/src/mcp/services/store-adapter.ts +0 -40
  115. package/src/mcp/tools/get-active-sessions.ts +0 -16
  116. package/src/mcp/tools/get-session-children.ts +0 -24
  117. package/src/mcp/tools/get-session-messages.ts +0 -24
  118. package/src/mcp/tools/get-session.ts +0 -23
  119. package/src/mcp/tools/index.ts +0 -15
  120. package/src/mcp/tools/list-sessions.ts +0 -16
  121. package/src/oc/index.ts +0 -302
  122. package/src/service/manager.ts +0 -284
  123. package/src/shared/config.ts +0 -511
  124. package/src/shared/ipc.ts +0 -80
  125. package/src/shared/logger.ts +0 -248
  126. package/src/shared/paths.ts +0 -73
  127. package/src/shared/platform.ts +0 -110
  128. package/src/shared/session-exclusions.ts +0 -5
  129. package/src/shared/session-view.ts +0 -302
  130. package/src/shared/terminal-capabilities.ts +0 -130
  131. package/src/shared/types.ts +0 -330
  132. package/src/shared/utils.ts +0 -339
  133. package/src/shared/version.ts +0 -44
  134. package/src/tui/App.tsx +0 -240
  135. package/src/tui/client.ts +0 -163
  136. package/src/tui/components/BindingsModal.tsx +0 -82
  137. package/src/tui/components/DashboardHeader.tsx +0 -256
  138. package/src/tui/components/KeyBar.tsx +0 -67
  139. package/src/tui/components/SessionDetailModal.tsx +0 -169
  140. package/src/tui/components/SessionsView.tsx +0 -184
  141. package/src/tui/components/TabBar.tsx +0 -40
  142. package/src/tui/components/UsageSummaryPane.tsx +0 -272
  143. package/src/tui/components/UsageView.tsx +0 -242
  144. package/src/tui/components/tool-icons.ts +0 -24
  145. package/src/tui/index.tsx +0 -106
  146. package/src/tui/keybindings.ts +0 -49
  147. package/src/tui/model.ts +0 -295
  148. package/src/tui/state.ts +0 -77
  149. package/src/tui/theme.ts +0 -110
  150. package/src/types.ts +0 -113
  151. package/src/usage/README.md +0 -323
  152. package/src/usage/api.ts +0 -99
  153. package/src/usage/client.ts +0 -3
  154. package/src/usage/clients/anthropic-oauth-usage.ts +0 -258
  155. package/src/usage/clients/anthropic-web-usage.ts +0 -329
  156. package/src/usage/clients/claude-cli-probe.ts +0 -513
  157. package/src/usage/clients/codex-app-server.ts +0 -354
  158. package/src/usage/clients/codex-oauth-usage.ts +0 -240
  159. package/src/usage/clients/opencode-go-api.ts +0 -100
  160. package/src/usage/clients/opencode-go-usage.ts +0 -577
  161. package/src/usage/errors.ts +0 -33
  162. package/src/usage/index.ts +0 -20
  163. package/src/usage/local/opencode-go-local.ts +0 -194
  164. package/src/usage/mappers.ts +0 -282
  165. package/src/usage/planner.ts +0 -118
  166. package/src/usage/providers/claude.ts +0 -233
  167. package/src/usage/providers/codex.ts +0 -115
  168. package/src/usage/providers/opencode-go.ts +0 -115
  169. package/src/usage/resolver.ts +0 -4
  170. package/src/usage/resolvers/browser-cookies.ts +0 -808
  171. package/src/usage/resolvers/claude-auth.ts +0 -208
  172. package/src/usage/resolvers/claude-cli.ts +0 -24
  173. package/src/usage/resolvers/claude-desktop-auth.ts +0 -170
  174. package/src/usage/resolvers/claude-web.ts +0 -82
  175. package/src/usage/resolvers/codex-auth.ts +0 -174
  176. package/src/usage/resolvers/keychain-coordinator.ts +0 -397
  177. package/src/usage/resolvers/opencode-go-web.ts +0 -55
  178. package/src/usage/types.ts +0 -225
  179. package/src/usage/web-session-cache.ts +0 -37
  180. /package/{src/tui → dist/cli}/assets/providers/SOURCES.md +0 -0
  181. /package/{src/tui → dist/cli}/assets/providers/claude-terminal.png +0 -0
  182. /package/{src/tui → dist/cli}/assets/providers/claude-terminal.svg +0 -0
  183. /package/{src/tui → dist/cli}/assets/providers/claude.png +0 -0
  184. /package/{src/tui → dist/cli}/assets/providers/copilot.png +0 -0
  185. /package/{src/tui → dist/cli}/assets/providers/copilot.svg +0 -0
  186. /package/{src/tui → dist/cli}/assets/providers/openai-terminal.png +0 -0
  187. /package/{src/tui → dist/cli}/assets/providers/openai-terminal.svg +0 -0
  188. /package/{src/tui → dist/cli}/assets/providers/openai.png +0 -0
  189. /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.png +0 -0
  190. /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.svg +0 -0
  191. /package/{src/tui → dist/cli}/assets/providers/opencode.png +0 -0
  192. /package/{src/tui → dist/cli}/assets/providers/shell.png +0 -0
  193. /package/{src/tui → dist/cli}/assets/providers/shell.svg +0 -0
  194. /package/{src/tui → dist/cli}/assets/providers/unknown.png +0 -0
  195. /package/{src/tui → dist/cli}/assets/providers/unknown.svg +0 -0
@@ -1,233 +0,0 @@
1
- import type { UsageClient } from "../client";
2
- import {
3
- AnthropicOAuthUsageClient,
4
- fetchOAuthProfile,
5
- } from "../clients/anthropic-oauth-usage";
6
- import { InvalidCredentialsError, AnthropicWebUsageClient } from "../clients/anthropic-web-usage";
7
- import { ClaudeCliProbeClient } from "../clients/claude-cli-probe";
8
- import { classifyUsageError, UsageError } from "../errors";
9
- import { mapClaudeToEntry } from "../mappers";
10
- import type { ClaudeSourcePlanner } from "../planner";
11
- import type { TokenResolver } from "../resolver";
12
- import type {
13
- ClaudeOAuthCredentials,
14
- ClaudePlannedRoute,
15
- ClaudeUsageSnapshot,
16
- ClaudeWebSession,
17
- UsageProviderEntry,
18
- } from "../types";
19
-
20
- interface ClaudeClients {
21
- oauth: UsageClient<ClaudeOAuthCredentials, ClaudeUsageSnapshot>;
22
- web: UsageClient<ClaudeWebSession, ClaudeUsageSnapshot>;
23
- cli: UsageClient<string, ClaudeUsageSnapshot>;
24
- }
25
-
26
- interface WebExtrasPrimaryIdentity {
27
- email: string | null;
28
- organizationId: string | null;
29
- organizationName: string | null;
30
- }
31
-
32
- interface KeychainDiagnostics {
33
- describeKeychainFailure?(): string | null;
34
- }
35
-
36
- const DEFAULT_PROFILE_IDENTITY_TTL_MS = 30 * 60 * 1_000;
37
-
38
- function nullIdentity(): WebExtrasPrimaryIdentity {
39
- return { email: null, organizationId: null, organizationName: null };
40
- }
41
-
42
- function normalizedIdentity(value: string | null | undefined): string | null {
43
- const trimmed = value?.trim().toLowerCase();
44
- return trimmed ? trimmed : null;
45
- }
46
-
47
- function webExtrasIdentityMatches(primary: WebExtrasPrimaryIdentity, web: ClaudeUsageSnapshot): boolean {
48
- const webEmail = normalizedIdentity(web.accountEmail);
49
- if (primary.email && webEmail && normalizedIdentity(primary.email) === webEmail) return true;
50
- const webOrganizationId = normalizedIdentity(web.accountOrganizationId);
51
- if (primary.organizationId && webOrganizationId && normalizedIdentity(primary.organizationId) === webOrganizationId) return true;
52
- const webOrganizationName = normalizedIdentity(web.accountOrganization);
53
- if (primary.organizationName && webOrganizationName && normalizedIdentity(primary.organizationName) === webOrganizationName) return true;
54
- return false;
55
- }
56
-
57
- export class ClaudeUsageProvider {
58
- private readonly clients: ClaudeClients;
59
- private readonly oauthResolver: TokenResolver<ClaudeOAuthCredentials> & KeychainDiagnostics;
60
- private readonly webResolver: TokenResolver<ClaudeWebSession> & KeychainDiagnostics;
61
- private readonly profileIdentityTtlMs: number;
62
- private profileIdentity?: { token: string; identity: WebExtrasPrimaryIdentity; fetchedAt: number };
63
-
64
- constructor(
65
- private readonly planner: ClaudeSourcePlanner,
66
- oauthResolver: TokenResolver<ClaudeOAuthCredentials> & KeychainDiagnostics,
67
- webResolver: TokenResolver<ClaudeWebSession> & KeychainDiagnostics,
68
- _cliResolver: TokenResolver<string>,
69
- clients?: Partial<ClaudeClients>,
70
- options?: { profileIdentityTtlMs?: number },
71
- ) {
72
- this.oauthResolver = oauthResolver;
73
- this.webResolver = webResolver;
74
- this.profileIdentityTtlMs = options?.profileIdentityTtlMs ?? DEFAULT_PROFILE_IDENTITY_TTL_MS;
75
- this.clients = {
76
- oauth: clients?.oauth ?? new AnthropicOAuthUsageClient(),
77
- web: clients?.web ?? new AnthropicWebUsageClient(),
78
- cli: clients?.cli ?? new ClaudeCliProbeClient(),
79
- };
80
- }
81
-
82
- async fetch(options?: { signal?: AbortSignal }): Promise<{ entry: UsageProviderEntry; snapshot: ClaudeUsageSnapshot | null }> {
83
- const decision = await this.planner.decide();
84
- if (decision.routes.length === 0) {
85
- // Surface actionable keychain diagnostics when a blocking failure (not
86
- // merely missing credentials) explains the empty route plan.
87
- const reason =
88
- this.oauthResolver.describeKeychainFailure?.() ??
89
- this.webResolver.describeKeychainFailure?.() ??
90
- null;
91
- return { entry: mapClaudeToEntry(null, reason ? [reason] : [], false, null), snapshot: null };
92
- }
93
-
94
- const errors: string[] = [];
95
- let sawNonAuthFailure = false;
96
- for (let index = 0; index < decision.routes.length; index++) {
97
- const route = decision.routes[index]!;
98
- try {
99
- const snapshot = await this.fetchRoute(route, options);
100
- const withExtras = await this.mergeWebExtras(snapshot, route, decision.webExtrasCredentials, options);
101
- const winningOAuth = route.type === "oauth" ? route.credentials : null;
102
- return { entry: mapClaudeToEntry(withExtras, [], true, winningOAuth), snapshot: withExtras };
103
- } catch (error) {
104
- const classified = error instanceof InvalidCredentialsError
105
- ? new UsageError("invalid-credentials", error.message)
106
- : classifyUsageError(error);
107
- if (classified.kind === "cancelled") throw classified;
108
- errors.push(`${route.type}: ${classified.message}`);
109
- if (classified.kind !== "invalid-credentials") sawNonAuthFailure = true;
110
- if (classified.kind === "invalid-credentials" && route.type === "oauth") {
111
- // Invalidate the daemon-lifetime credential cache so the next poll
112
- // re-resolves the OAuth source after an authentication rejection.
113
- // The profile identity cache is keyed by token, so dropping it
114
- // forces re-verification once the rotated token arrives.
115
- (this.oauthResolver as Partial<{ invalidate?: () => void }>).invalidate?.();
116
- this.profileIdentity = undefined;
117
- }
118
- const canRecover = classified.kind === "invalid-credentials" && index + 1 < decision.routes.length;
119
- if (!canRecover) break;
120
- }
121
- }
122
-
123
- if (errors.length > 0 && !sawNonAuthFailure) {
124
- // Every attempted route failed with authentication failures: surface a
125
- // single actionable state instead of raw probe errors.
126
- const sessionExpired = errors.some((message) => message.includes("expired") || message.includes("re-login"));
127
- if (sessionExpired) {
128
- return {
129
- entry: mapClaudeToEntry(null, ["Claude session expired — run \"claude\" to re-login"], false, null),
130
- snapshot: null,
131
- };
132
- }
133
- return { entry: mapClaudeToEntry(null, [], false, null), snapshot: null };
134
- }
135
-
136
- return { entry: mapClaudeToEntry(null, errors, true, null), snapshot: null };
137
- }
138
-
139
- private fetchRoute(
140
- route: ClaudePlannedRoute,
141
- options?: { signal?: AbortSignal },
142
- ): Promise<ClaudeUsageSnapshot> {
143
- if (route.type === "oauth") {
144
- if (route.credentials.isStale) {
145
- return Promise.reject(new UsageError("invalid-credentials", "Claude-owned OAuth token is expired or near expiry"));
146
- }
147
- return this.clients.oauth.fetch(route.credentials, options);
148
- }
149
- if (route.type === "web") return this.fetchWebRoute(route.credentials, options);
150
- return this.clients.cli.fetch(route.credentials, options);
151
- }
152
-
153
- private async fetchWebRoute(
154
- session: ClaudeWebSession,
155
- options?: { signal?: AbortSignal },
156
- ): Promise<ClaudeUsageSnapshot> {
157
- let cookieHeader = session.cookieHeader;
158
- if (!cookieHeader && session.importNext) {
159
- cookieHeader = (await session.importNext()) ?? "";
160
- }
161
- if (!cookieHeader) {
162
- // Prefer the resolver's keychain diagnostic when a blocking failure (not
163
- // a transient import miss) is why no web session could be produced. It
164
- // is an environment failure, not rejected credentials, so transient
165
- // classification keeps the reason visible in the reported entry.
166
- const reason = this.webResolver.describeKeychainFailure?.();
167
- throw new UsageError(
168
- reason ? "transient" : "invalid-credentials",
169
- reason
170
- ? `${reason} (no Claude web session available)`
171
- : "No Claude web session available",
172
- );
173
- }
174
- return this.clients.web.fetch({ ...session, cookieHeader }, options);
175
- }
176
-
177
- private async mergeWebExtras(
178
- snapshot: ClaudeUsageSnapshot,
179
- route: ClaudePlannedRoute,
180
- webCredentials: ClaudeWebSession | undefined,
181
- options?: { signal?: AbortSignal },
182
- ): Promise<ClaudeUsageSnapshot> {
183
- if (!webCredentials || snapshot.source === "web") return snapshot;
184
- try {
185
- const web = await this.clients.web.fetch(webCredentials, options);
186
- const primary = await this.primaryWebExtrasIdentity(snapshot, route, options);
187
- if (!webExtrasIdentityMatches(primary, web)) return snapshot;
188
- return {
189
- ...snapshot,
190
- extraRateWindows: snapshot.extraRateWindows.length === 0 ? web.extraRateWindows : snapshot.extraRateWindows,
191
- providerCost: snapshot.providerCost ?? web.providerCost,
192
- };
193
- } catch (error) {
194
- if (classifyUsageError(error).kind === "cancelled") throw error;
195
- return snapshot;
196
- }
197
- }
198
-
199
- private async primaryWebExtrasIdentity(
200
- snapshot: ClaudeUsageSnapshot,
201
- route: ClaudePlannedRoute,
202
- options?: { signal?: AbortSignal },
203
- ): Promise<WebExtrasPrimaryIdentity> {
204
- if (snapshot.accountEmail || snapshot.accountOrganization || snapshot.accountOrganizationId) {
205
- return {
206
- email: snapshot.accountEmail,
207
- organizationId: snapshot.accountOrganizationId,
208
- organizationName: snapshot.accountOrganization,
209
- };
210
- }
211
- if (route.type !== "oauth") return nullIdentity();
212
-
213
- const token = route.credentials.accessToken;
214
- const cached = this.profileIdentity;
215
- if (cached && cached.token === token && Date.now() - cached.fetchedAt < this.profileIdentityTtlMs) {
216
- return cached.identity;
217
- }
218
- try {
219
- const profile = await fetchOAuthProfile(token, options);
220
- const identity: WebExtrasPrimaryIdentity = {
221
- email: profile.email,
222
- organizationId: profile.organizationId,
223
- organizationName: null,
224
- };
225
- this.profileIdentity = { token, identity, fetchedAt: Date.now() };
226
- return identity;
227
- } catch (error) {
228
- if (classifyUsageError(error).kind === "cancelled") throw error;
229
- // Best-effort: do not cache failures so the next poll can retry.
230
- return nullIdentity();
231
- }
232
- }
233
- }
@@ -1,115 +0,0 @@
1
- import type { UsageClient } from "../client";
2
- import type { CodexDataSource, CodexOAuthCredentials, CodexUsageSnapshot, UsageProviderEntry } from "../types";
3
- import type { TokenResolver } from "../resolver";
4
- import { CodexMonthlySpendClient, CodexOAuthUsageClient } from "../clients/codex-oauth-usage";
5
- import { CodexAppServerClient } from "../clients/codex-app-server";
6
- import { classifyUsageError, isRecoverableAuthenticationError } from "../errors";
7
- import { isCodexCredentialFresh } from "../resolvers/codex-auth";
8
- import { mapCodexToEntry } from "../mappers";
9
-
10
- interface CodexAppServerUsageClient {
11
- fetch(
12
- identity?: Pick<CodexOAuthCredentials, "accountEmail" | "accountId">,
13
- options?: { signal?: AbortSignal },
14
- ): Promise<CodexUsageSnapshot>;
15
- }
16
-
17
- interface CodexMonthlySpendUsageClient {
18
- fetch(
19
- credentials: CodexOAuthCredentials,
20
- accountId: string,
21
- options?: { signal?: AbortSignal },
22
- ): Promise<NonNullable<CodexUsageSnapshot["individualLimit"]> | null>;
23
- }
24
-
25
- export interface CodexUsageProviderDependencies {
26
- oauthClient?: UsageClient<CodexOAuthCredentials, CodexUsageSnapshot>;
27
- appServerClient?: CodexAppServerUsageClient;
28
- monthlySpendClient?: CodexMonthlySpendUsageClient;
29
- }
30
-
31
- export class CodexUsageProvider {
32
- private readonly oauthClient: UsageClient<CodexOAuthCredentials, CodexUsageSnapshot>;
33
- private readonly appServerClient: CodexAppServerUsageClient;
34
- private readonly monthlySpendClient: CodexMonthlySpendUsageClient;
35
-
36
- constructor(
37
- private readonly oauthResolver: TokenResolver<CodexOAuthCredentials>,
38
- private readonly dataSource: CodexDataSource = "auto",
39
- dependencies: CodexUsageProviderDependencies = {},
40
- ) {
41
- this.oauthClient = dependencies.oauthClient ?? new CodexOAuthUsageClient();
42
- this.appServerClient = dependencies.appServerClient ?? new CodexAppServerClient();
43
- this.monthlySpendClient = dependencies.monthlySpendClient ?? new CodexMonthlySpendClient();
44
- }
45
-
46
- async fetch(options?: { signal?: AbortSignal }): Promise<{ entry: UsageProviderEntry; snapshot: CodexUsageSnapshot | null }> {
47
- let oauthCreds: CodexOAuthCredentials | null;
48
- try {
49
- oauthCreds = await this.oauthResolver.resolve();
50
- } catch (error) {
51
- const failure = classifyUsageError(error);
52
- return { entry: mapCodexToEntry(null, [`oauth: ${failure.message}`], true, null), snapshot: null };
53
- }
54
-
55
- if (!oauthCreds) {
56
- return { entry: mapCodexToEntry(null, [], false, null), snapshot: null };
57
- }
58
-
59
- if (!isCodexCredentialFresh(oauthCreds)) {
60
- return this.fetchFromCli(oauthCreds, options);
61
- }
62
-
63
- try {
64
- let snapshot = await this.oauthClient.fetch(oauthCreds, options);
65
- snapshot = await this.enrichMonthlySpend(snapshot, oauthCreds, options);
66
- return { entry: mapCodexToEntry(snapshot, [], true, oauthCreds), snapshot };
67
- } catch (error) {
68
- const failure = classifyUsageError(error);
69
- if (failure.kind === "cancelled") throw failure;
70
- if (this.dataSource === "auto" && isRecoverableAuthenticationError(failure)) {
71
- return this.fetchFromCli(oauthCreds, options);
72
- }
73
- return {
74
- entry: mapCodexToEntry(null, [`oauth: ${failure.message}`], true, oauthCreds),
75
- snapshot: null,
76
- };
77
- }
78
- }
79
-
80
- private async enrichMonthlySpend(
81
- snapshot: CodexUsageSnapshot,
82
- oauthCreds: CodexOAuthCredentials,
83
- options?: { signal?: AbortSignal },
84
- ): Promise<CodexUsageSnapshot> {
85
- const accountId = snapshot.accountId ?? oauthCreds.accountId;
86
- if (snapshot.source !== "oauth" || !snapshot.spendControlPresent || snapshot.individualLimit || !accountId) {
87
- return snapshot;
88
- }
89
- try {
90
- const individualLimit = await this.monthlySpendClient.fetch(oauthCreds, accountId, options);
91
- return individualLimit ? { ...snapshot, individualLimit } : snapshot;
92
- } catch (error) {
93
- const failure = classifyUsageError(error);
94
- if (failure.kind === "cancelled") throw failure;
95
- return snapshot;
96
- }
97
- }
98
-
99
- private async fetchFromCli(
100
- oauthCreds: CodexOAuthCredentials,
101
- options?: { signal?: AbortSignal },
102
- ): Promise<{ entry: UsageProviderEntry; snapshot: CodexUsageSnapshot | null }> {
103
- try {
104
- const snapshot = await this.appServerClient.fetch(oauthCreds, options);
105
- return { entry: mapCodexToEntry(snapshot, [], true, null), snapshot };
106
- } catch (error) {
107
- const failure = classifyUsageError(error);
108
- if (failure.kind === "cancelled") throw failure;
109
- return {
110
- entry: mapCodexToEntry(null, [`cli: ${failure.message}`], true, oauthCreds),
111
- snapshot: null,
112
- };
113
- }
114
- }
115
- }
@@ -1,115 +0,0 @@
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
- import { classifyUsageError, isRecoverableAuthenticationError } from "../errors";
7
- import { usageWebSessionCache, type WebSessionCache } from "../web-session-cache";
8
- import { OpenCodeGoApiKeyUsageClient } from "../clients/opencode-go-api";
9
- import { OpenCodeGoLocalUsageReader, type OpenCodeGoLocalUsageFetcher } from "../local/opencode-go-local";
10
- import type { OpenCodeGoWebCredentials } from "../resolvers/opencode-go-web";
11
-
12
- export interface OpenCodeGoApiKeyUsageClientLike {
13
- fetch(): Promise<OpenCodeGoUsageSnapshot | null>;
14
- }
15
-
16
- const CACHE_KEY = "opencode-go";
17
-
18
- export class OpenCodeGoUsageProvider {
19
- constructor(
20
- private readonly resolver: TokenResolver<OpenCodeGoWebCredentials>,
21
- private readonly serverFnId: string,
22
- private readonly client = new OpenCodeGoUsageClient(),
23
- private readonly cache: WebSessionCache = usageWebSessionCache,
24
- private readonly localReader: OpenCodeGoLocalUsageFetcher = new OpenCodeGoLocalUsageReader(),
25
- private readonly apiKeyClient: OpenCodeGoApiKeyUsageClientLike = new OpenCodeGoApiKeyUsageClient(),
26
- ) {}
27
-
28
- async fetch(options?: { signal?: AbortSignal }): Promise<{ entry: UsageProviderEntry; snapshot: OpenCodeGoUsageSnapshot | null }> {
29
- // Official Go usage API is authoritative and independent of browser cookies.
30
- try {
31
- const apiSnapshot = await this.apiKeyClient.fetch();
32
- if (apiSnapshot) {
33
- return { entry: mapOpenCodeGoToEntry(apiSnapshot, [], true), snapshot: apiSnapshot };
34
- }
35
- } catch {
36
- // Fall through to cookie-based collection.
37
- }
38
- return this.fetchWithCookies(options);
39
- }
40
-
41
- private async fetchWithCookies(options?: { signal?: AbortSignal }): Promise<{ entry: UsageProviderEntry; snapshot: OpenCodeGoUsageSnapshot | null }> {
42
- const errors: string[] = [];
43
- const creds = await this.resolver.resolve();
44
- const observed = this.cache.observe(CACHE_KEY);
45
- if (!observed.value && !creds?.candidates.length) {
46
- return { entry: mapOpenCodeGoToEntry(null, [], false), snapshot: null };
47
- }
48
-
49
- const attempts = [
50
- ...(observed.value ? [{ cookies: observed.value, cached: true }] : []),
51
- ...(creds?.candidates ?? [])
52
- .filter((candidate) => candidate.cookies !== observed.value)
53
- .map((candidate) => ({ cookies: candidate.cookies, cached: false })),
54
- ].slice(0, 2);
55
- let expectedRevision = observed.revision;
56
-
57
- for (let index = 0; index < attempts.length; index++) {
58
- const attempt = attempts[index]!;
59
- let rotated = false;
60
- try {
61
- const snapshot = await this.client.fetch(
62
- {
63
- cookies: attempt.cookies,
64
- serverFnId: this.serverFnId,
65
- onCookieRotation: (cookies) => {
66
- rotated = true;
67
- const stored = this.cache.store(CACHE_KEY, cookies, expectedRevision);
68
- if (stored) expectedRevision = stored.revision;
69
- },
70
- },
71
- options,
72
- );
73
- if (!attempt.cached && !rotated && this.cache.observe(CACHE_KEY).revision === expectedRevision) {
74
- const stored = this.cache.store(CACHE_KEY, attempt.cookies, expectedRevision);
75
- if (stored) expectedRevision = stored.revision;
76
- }
77
- return { entry: mapOpenCodeGoToEntry(snapshot, [], true), snapshot };
78
- } catch (error) {
79
- const failure = classifyUsageError(error);
80
- if (failure.kind === "cancelled") throw failure;
81
- if (failure.kind === "invalid-credentials") {
82
- if (this.cache.clear(CACHE_KEY, expectedRevision)) expectedRevision = 0;
83
- // Advance the demand-driven cursor at most once per attempt, keeping
84
- // the two-candidate bound across cached and imported candidates.
85
- const imported = attempts.length < 2 ? (await creds?.importNext?.()) ?? null : null;
86
- if (imported && !attempts.some((candidate) => candidate.cookies === imported.cookies)) {
87
- attempts.push({ cookies: imported.cookies, cached: false });
88
- continue;
89
- }
90
- }
91
- if (!(isRecoverableAuthenticationError(failure) && index + 1 < attempts.length)) {
92
- errors.push(`OpenCode Go: ${failure.message}`);
93
- return this.fetchLocalFallback(errors);
94
- }
95
- }
96
- }
97
-
98
- return this.fetchLocalFallback(errors);
99
- }
100
-
101
- /**
102
- * Last resort after every web attempt fails: report locally accumulated
103
- * opencode-go costs as estimated windows instead of an error. Mirrors the
104
- * CodexBar "local" source so a stale browser session stays usable.
105
- */
106
- private async fetchLocalFallback(errors: string[]): Promise<{ entry: UsageProviderEntry; snapshot: OpenCodeGoUsageSnapshot | null }> {
107
- const local = await this.localReader.fetch();
108
- if (local) {
109
- // Keeping errors here would force status "error" in the mapper; the local
110
- // snapshot is reported as a usable estimate instead.
111
- return { entry: mapOpenCodeGoToEntry(local, [], true), snapshot: local };
112
- }
113
- return { entry: mapOpenCodeGoToEntry(null, errors, true), snapshot: null };
114
- }
115
- }
@@ -1,4 +0,0 @@
1
- export interface TokenResolver<T> {
2
- resolve(): Promise<T | null>;
3
- isAvailable(): Promise<boolean>;
4
- }