@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.
- package/dist/assets/highlights-eq9cgrbb.scm +604 -0
- package/dist/assets/highlights-ghv9g403.scm +205 -0
- package/dist/assets/highlights-hk7bwhj4.scm +284 -0
- package/dist/assets/highlights-r812a2qc.scm +150 -0
- package/dist/assets/highlights-x6tmsnaa.scm +115 -0
- package/dist/assets/injections-73j83es3.scm +27 -0
- package/dist/assets/parser.worker-xyhf83rf.js +4563 -0
- package/dist/assets/tree-sitter-3jzf13jk.wasm +0 -0
- package/dist/assets/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/assets/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/assets/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/assets/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/assets/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/dist/cli/index.js +499 -0
- package/dist/cli/index.js.map +61 -0
- package/dist/daemon/index.js +670 -0
- package/dist/daemon/index.js.map +1475 -0
- package/dist/hooks/claude.js +6 -0
- package/dist/hooks/claude.js.map +11 -0
- package/dist/hooks/codex.js +10 -0
- package/dist/hooks/codex.js.map +11 -0
- package/dist/mcp/index.js +100 -0
- package/dist/mcp/index.js.map +231 -0
- package/dist/oc/index.js +21 -0
- package/dist/oc/index.js.map +10 -0
- package/package.json +16 -13
- package/src/api/docs/sessions.keys.ts +0 -6
- package/src/api/docs/sessions.ts +0 -82
- package/src/api/docs/system.keys.ts +0 -5
- package/src/api/docs/system.ts +0 -69
- package/src/api/docs/usage.keys.ts +0 -3
- package/src/api/docs/usage.ts +0 -24
- package/src/api/docs/webhooks.keys.ts +0 -5
- package/src/api/docs/webhooks.ts +0 -78
- package/src/api/index.ts +0 -18
- package/src/api/lib/auth.ts +0 -43
- package/src/api/lib/errors.ts +0 -28
- package/src/api/lib/route.ts +0 -18
- package/src/api/models/claude.ts +0 -4
- package/src/api/models/codex.ts +0 -4
- package/src/api/models/opencode.ts +0 -11
- package/src/api/routes/sessions.ts +0 -51
- package/src/api/routes/system.ts +0 -106
- package/src/api/routes/usage.ts +0 -15
- package/src/api/routes/webhooks/claude.ts +0 -149
- package/src/api/routes/webhooks/codex.ts +0 -175
- package/src/api/routes/webhooks/opencode.ts +0 -112
- package/src/api/routes.ts +0 -93
- package/src/api/schemas/common.ts +0 -29
- package/src/api/schemas/sessions.ts +0 -86
- package/src/api/schemas/usage.ts +0 -51
- package/src/api/schemas/webhooks/claude.ts +0 -32
- package/src/api/schemas/webhooks/codex.ts +0 -34
- package/src/api/schemas/webhooks/opencode.ts +0 -29
- package/src/api/services/claude-session.ts +0 -9
- package/src/api/services/codex-session.ts +0 -8
- package/src/api/services/opencode-session.ts +0 -73
- package/src/classifier.ts +0 -206
- package/src/cli/commands.ts +0 -714
- package/src/cli/index.ts +0 -18
- package/src/cli/program.ts +0 -175
- package/src/cli/prompt.ts +0 -44
- package/src/daemon/cli.ts +0 -27
- package/src/daemon/collectors/claude/README.md +0 -108
- package/src/daemon/collectors/claude/collector.ts +0 -423
- package/src/daemon/collectors/claude/discovery.ts +0 -127
- package/src/daemon/collectors/claude/hooks.ts +0 -181
- package/src/daemon/collectors/claude/index.ts +0 -8
- package/src/daemon/collectors/claude/jsonl.ts +0 -85
- package/src/daemon/collectors/claude/transcript.ts +0 -246
- package/src/daemon/collectors/codex/README.md +0 -147
- package/src/daemon/collectors/codex/collector.ts +0 -661
- package/src/daemon/collectors/codex/discovery.ts +0 -104
- package/src/daemon/collectors/codex/hooks.ts +0 -179
- package/src/daemon/collectors/codex/index.ts +0 -7
- package/src/daemon/collectors/codex/jsonl.ts +0 -108
- package/src/daemon/collectors/codex/transcript.ts +0 -299
- package/src/daemon/collectors/git.ts +0 -353
- package/src/daemon/collectors/herdr/collector.ts +0 -128
- package/src/daemon/collectors/herdr/index.ts +0 -2
- package/src/daemon/collectors/herdr/normalization.ts +0 -111
- package/src/daemon/collectors/opencode/README.md +0 -106
- package/src/daemon/collectors/opencode/collector.ts +0 -545
- package/src/daemon/collectors/opencode/discovery.ts +0 -257
- package/src/daemon/collectors/opencode/index.ts +0 -5
- package/src/daemon/collectors/opencode/session-builder.ts +0 -59
- package/src/daemon/collectors/opencode-db.ts +0 -294
- package/src/daemon/collectors/process.ts +0 -201
- package/src/daemon/collectors/tmux/README.md +0 -128
- package/src/daemon/collectors/tmux/collector.ts +0 -492
- package/src/daemon/collectors/tmux/enrichment.ts +0 -242
- package/src/daemon/collectors/tmux/index.ts +0 -4
- package/src/daemon/collectors/tmux/parse.ts +0 -128
- package/src/daemon/collectors/usage.ts +0 -259
- package/src/daemon/index.ts +0 -209
- package/src/daemon/scheduler.ts +0 -86
- package/src/daemon/store.ts +0 -630
- package/src/detect.ts +0 -143
- package/src/hooks/claude.ts +0 -596
- package/src/hooks/codex.ts +0 -517
- package/src/legacy/classifier.ts +0 -215
- package/src/legacy/detect.ts +0 -143
- package/src/mcp/client-server.ts +0 -21
- package/src/mcp/http-handler.ts +0 -126
- package/src/mcp/index.ts +0 -61
- package/src/mcp/resources/config-current.ts +0 -19
- package/src/mcp/resources/index.ts +0 -11
- package/src/mcp/resources/sessions-active.ts +0 -19
- package/src/mcp/resources/sessions-all.ts +0 -19
- package/src/mcp/schemas/sessions.ts +0 -3
- package/src/mcp/server.ts +0 -17
- package/src/mcp/services/api-adapter.ts +0 -48
- package/src/mcp/services/data-source.ts +0 -22
- package/src/mcp/services/store-adapter.ts +0 -40
- package/src/mcp/tools/get-active-sessions.ts +0 -16
- package/src/mcp/tools/get-session-children.ts +0 -24
- package/src/mcp/tools/get-session-messages.ts +0 -24
- package/src/mcp/tools/get-session.ts +0 -23
- package/src/mcp/tools/index.ts +0 -15
- package/src/mcp/tools/list-sessions.ts +0 -16
- package/src/oc/index.ts +0 -302
- package/src/service/manager.ts +0 -284
- package/src/shared/config.ts +0 -511
- package/src/shared/ipc.ts +0 -80
- package/src/shared/logger.ts +0 -248
- package/src/shared/paths.ts +0 -73
- package/src/shared/platform.ts +0 -110
- package/src/shared/session-exclusions.ts +0 -5
- package/src/shared/session-view.ts +0 -302
- package/src/shared/terminal-capabilities.ts +0 -130
- package/src/shared/types.ts +0 -330
- package/src/shared/utils.ts +0 -339
- package/src/shared/version.ts +0 -44
- package/src/tui/App.tsx +0 -240
- package/src/tui/client.ts +0 -163
- package/src/tui/components/BindingsModal.tsx +0 -82
- package/src/tui/components/DashboardHeader.tsx +0 -256
- package/src/tui/components/KeyBar.tsx +0 -67
- package/src/tui/components/SessionDetailModal.tsx +0 -169
- package/src/tui/components/SessionsView.tsx +0 -184
- package/src/tui/components/TabBar.tsx +0 -40
- package/src/tui/components/UsageSummaryPane.tsx +0 -272
- package/src/tui/components/UsageView.tsx +0 -242
- package/src/tui/components/tool-icons.ts +0 -24
- package/src/tui/index.tsx +0 -106
- package/src/tui/keybindings.ts +0 -49
- package/src/tui/model.ts +0 -295
- package/src/tui/state.ts +0 -77
- package/src/tui/theme.ts +0 -110
- package/src/types.ts +0 -113
- package/src/usage/README.md +0 -323
- package/src/usage/api.ts +0 -99
- package/src/usage/client.ts +0 -3
- package/src/usage/clients/anthropic-oauth-usage.ts +0 -258
- package/src/usage/clients/anthropic-web-usage.ts +0 -329
- package/src/usage/clients/claude-cli-probe.ts +0 -513
- package/src/usage/clients/codex-app-server.ts +0 -354
- package/src/usage/clients/codex-oauth-usage.ts +0 -240
- package/src/usage/clients/opencode-go-api.ts +0 -100
- package/src/usage/clients/opencode-go-usage.ts +0 -577
- package/src/usage/errors.ts +0 -33
- package/src/usage/index.ts +0 -20
- package/src/usage/local/opencode-go-local.ts +0 -194
- package/src/usage/mappers.ts +0 -282
- package/src/usage/planner.ts +0 -118
- package/src/usage/providers/claude.ts +0 -233
- package/src/usage/providers/codex.ts +0 -115
- package/src/usage/providers/opencode-go.ts +0 -115
- package/src/usage/resolver.ts +0 -4
- package/src/usage/resolvers/browser-cookies.ts +0 -808
- package/src/usage/resolvers/claude-auth.ts +0 -208
- package/src/usage/resolvers/claude-cli.ts +0 -24
- package/src/usage/resolvers/claude-desktop-auth.ts +0 -170
- package/src/usage/resolvers/claude-web.ts +0 -82
- package/src/usage/resolvers/codex-auth.ts +0 -174
- package/src/usage/resolvers/keychain-coordinator.ts +0 -397
- package/src/usage/resolvers/opencode-go-web.ts +0 -55
- package/src/usage/types.ts +0 -225
- package/src/usage/web-session-cache.ts +0 -37
- /package/{src/tui → dist/cli}/assets/providers/SOURCES.md +0 -0
- /package/{src/tui → dist/cli}/assets/providers/claude-terminal.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/claude-terminal.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/claude.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/copilot.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/copilot.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/openai-terminal.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/openai-terminal.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/openai.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/opencode.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/shell.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/shell.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/unknown.png +0 -0
- /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
|
-
}
|
package/src/usage/resolver.ts
DELETED