@scrylog/cli 0.3.0 → 0.3.1

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,354 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { whichCommand } from "../../shared/platform";
3
- import { UsageError } from "../errors";
4
- import type { CodexOAuthCredentials, CodexUsageSnapshot, RateWindowLegacy } from "../types";
5
-
6
- export interface CodexAppServerProcess {
7
- write(line: string): void;
8
- closeStdin(): void;
9
- onStdout(listener: (chunk: Uint8Array) => void): void;
10
- onStderr(listener: (chunk: Uint8Array) => void): void;
11
- onExit(listener: (code: number | null) => void): void;
12
- onError(listener: (error: Error) => void): void;
13
- kill(signal: "SIGTERM" | "SIGKILL"): void;
14
- }
15
-
16
- export type CodexAppServerTransport = (
17
- executable: string,
18
- args: string[],
19
- ) => CodexAppServerProcess;
20
-
21
- export interface CodexAppServerOptions {
22
- timeoutMs?: number;
23
- killGraceMs?: number;
24
- maxLineBytes?: number;
25
- maxOutputBytes?: number;
26
- resolveExecutable?: () => string | null;
27
- transport?: CodexAppServerTransport;
28
- }
29
-
30
- const defaultTransport: CodexAppServerTransport = (executable, args) => {
31
- const child = spawn(executable, args, {
32
- env: process.env,
33
- stdio: ["pipe", "pipe", "pipe"],
34
- windowsHide: true,
35
- });
36
- return {
37
- write: (line) => child.stdin.write(line),
38
- closeStdin: () => child.stdin.end(),
39
- onStdout: (listener) => child.stdout.on("data", listener),
40
- onStderr: (listener) => child.stderr.on("data", listener),
41
- onExit: (listener) => child.on("close", listener),
42
- onError: (listener) => child.on("error", listener),
43
- kill: (signal) => { child.kill(signal); },
44
- };
45
- };
46
-
47
- const INITIALIZE_ID = 1;
48
- const RATE_LIMITS_ID = 2;
49
-
50
- export class CodexAppServerClient {
51
- private readonly timeoutMs: number;
52
- private readonly killGraceMs: number;
53
- private readonly maxLineBytes: number;
54
- private readonly maxOutputBytes: number;
55
- private readonly resolveExecutable: () => string | null;
56
- private readonly transport: CodexAppServerTransport;
57
- private readonly inFlight = new Map<string, Promise<CodexUsageSnapshot>>();
58
-
59
- constructor(options: CodexAppServerOptions = {}) {
60
- this.timeoutMs = options.timeoutMs ?? 10_000;
61
- this.killGraceMs = options.killGraceMs ?? 250;
62
- this.maxLineBytes = options.maxLineBytes ?? 256 * 1_024;
63
- this.maxOutputBytes = options.maxOutputBytes ?? 1_024 * 1_024;
64
- this.resolveExecutable = options.resolveExecutable ?? (() => whichCommand("codex"));
65
- this.transport = options.transport ?? defaultTransport;
66
- }
67
-
68
- async fetch(
69
- identity?: Pick<CodexOAuthCredentials, "accountEmail" | "accountId">,
70
- options?: { signal?: AbortSignal },
71
- ): Promise<CodexUsageSnapshot> {
72
- const key = identity?.accountId ?? identity?.accountEmail ?? "default";
73
- const existing = this.inFlight.get(key);
74
- if (existing) return this.waitForShared(existing, options?.signal);
75
-
76
- const attempt = this.fetchOnce(identity, options).finally(() => this.inFlight.delete(key));
77
- this.inFlight.set(key, attempt);
78
- return attempt;
79
- }
80
-
81
- private async fetchOnce(
82
- identity?: Pick<CodexOAuthCredentials, "accountEmail" | "accountId">,
83
- options?: { signal?: AbortSignal },
84
- ): Promise<CodexUsageSnapshot> {
85
- if (options?.signal?.aborted) {
86
- throw new UsageError("cancelled", "Codex app-server request cancelled");
87
- }
88
-
89
- const executable = this.resolveExecutable();
90
- if (!executable) {
91
- throw new UsageError("transient", "Codex CLI executable was not found");
92
- }
93
-
94
- let processHandle: CodexAppServerProcess;
95
- try {
96
- processHandle = this.transport(executable, ["-s", "read-only", "-a", "never", "app-server"]);
97
- } catch (error) {
98
- throw new UsageError(
99
- "transient",
100
- `Codex app-server failed to start: ${error instanceof Error ? error.message : String(error)}`,
101
- );
102
- }
103
-
104
- return new Promise<CodexUsageSnapshot>((resolve, reject) => {
105
- let buffer = "";
106
- let outputBytes = 0;
107
- let cleaningUp = false;
108
- let completed = false;
109
- let result: CodexUsageSnapshot | undefined;
110
- let failure: UsageError | undefined;
111
-
112
- const complete = () => {
113
- if (completed) return;
114
- completed = true;
115
- clearTimeout(deadline);
116
- clearTimeout(killTimer);
117
- options?.signal?.removeEventListener("abort", onAbort);
118
- if (failure) reject(failure);
119
- else resolve(result!);
120
- };
121
-
122
- let killTimer: ReturnType<typeof setTimeout> | undefined;
123
- const settle = (nextResult?: CodexUsageSnapshot, nextFailure?: UsageError) => {
124
- if (cleaningUp || completed) return;
125
- cleaningUp = true;
126
- result = nextResult;
127
- failure = nextFailure;
128
- try { processHandle.closeStdin(); } catch {}
129
- try { processHandle.kill("SIGTERM"); } catch {}
130
- killTimer = setTimeout(() => {
131
- try { processHandle.kill("SIGKILL"); } catch {}
132
- complete();
133
- }, this.killGraceMs);
134
- };
135
-
136
- const onAbort = () => settle(undefined, new UsageError("cancelled", "Codex app-server request cancelled"));
137
- const deadline = setTimeout(
138
- () => settle(undefined, new UsageError("transient", `Codex app-server timed out after ${this.timeoutMs}ms`)),
139
- this.timeoutMs,
140
- );
141
-
142
- const send = (message: Record<string, unknown>) => {
143
- processHandle.write(`${JSON.stringify(message)}\n`);
144
- };
145
-
146
- const handleLine = (line: string) => {
147
- if (!line.trim() || cleaningUp) return;
148
- let message: Record<string, unknown>;
149
- try {
150
- message = JSON.parse(line) as Record<string, unknown>;
151
- } catch {
152
- settle(undefined, new UsageError("invalid-response", "Codex app-server emitted invalid JSON"));
153
- return;
154
- }
155
-
156
- if (message["id"] === INITIALIZE_ID) {
157
- if (message["error"] !== undefined) {
158
- settle(undefined, new UsageError("invalid-response", "Codex app-server initialization failed"));
159
- return;
160
- }
161
- send({ jsonrpc: "2.0", method: "initialized", params: {} });
162
- send({ jsonrpc: "2.0", id: RATE_LIMITS_ID, method: "account/rateLimits/read", params: {} });
163
- return;
164
- }
165
-
166
- if (message["id"] === RATE_LIMITS_ID) {
167
- if (message["error"] !== undefined) {
168
- settle(undefined, new UsageError("invalid-response", "Codex app-server rate-limit request failed"));
169
- return;
170
- }
171
- try {
172
- settle(parseRateLimits(message["result"], identity));
173
- } catch (error) {
174
- settle(undefined, error instanceof UsageError ? error : new UsageError("invalid-response", "Codex app-server returned invalid rate limits"));
175
- }
176
- }
177
- };
178
-
179
- processHandle.onStdout((chunk) => {
180
- if (cleaningUp) return;
181
- outputBytes += chunk.byteLength;
182
- if (outputBytes > this.maxOutputBytes) {
183
- settle(undefined, new UsageError("invalid-response", "Codex app-server output exceeded the configured limit"));
184
- return;
185
- }
186
- buffer += Buffer.from(chunk).toString("utf8");
187
- let newline = buffer.indexOf("\n");
188
- while (newline >= 0) {
189
- const line = buffer.slice(0, newline).replace(/\r$/, "");
190
- buffer = buffer.slice(newline + 1);
191
- if (Buffer.byteLength(line) > this.maxLineBytes) {
192
- settle(undefined, new UsageError("invalid-response", "Codex app-server line exceeded the configured limit"));
193
- return;
194
- }
195
- handleLine(line);
196
- newline = buffer.indexOf("\n");
197
- }
198
- if (Buffer.byteLength(buffer) > this.maxLineBytes) {
199
- settle(undefined, new UsageError("invalid-response", "Codex app-server line exceeded the configured limit"));
200
- }
201
- });
202
- processHandle.onStderr((chunk) => {
203
- if (cleaningUp) return;
204
- outputBytes += chunk.byteLength;
205
- if (outputBytes > this.maxOutputBytes) {
206
- settle(undefined, new UsageError("invalid-response", "Codex app-server output exceeded the configured limit"));
207
- }
208
- });
209
- processHandle.onError((error) => {
210
- settle(undefined, new UsageError("transient", `Codex app-server failed to start: ${error.message}`));
211
- });
212
- processHandle.onExit((code) => {
213
- if (cleaningUp) complete();
214
- else settle(undefined, new UsageError("transient", `Codex app-server exited before responding${code === null ? "" : ` (${code})`}`));
215
- });
216
- options?.signal?.addEventListener("abort", onAbort, { once: true });
217
-
218
- send({
219
- jsonrpc: "2.0",
220
- id: INITIALIZE_ID,
221
- method: "initialize",
222
- params: {
223
- clientInfo: { name: "scrylog", version: "0.1.0" },
224
- capabilities: {},
225
- },
226
- });
227
- });
228
- }
229
-
230
- private waitForShared(promise: Promise<CodexUsageSnapshot>, signal?: AbortSignal): Promise<CodexUsageSnapshot> {
231
- if (!signal) return promise;
232
- if (signal.aborted) return Promise.reject(new UsageError("cancelled", "Codex app-server request cancelled"));
233
- return new Promise((resolve, reject) => {
234
- const abort = () => reject(new UsageError("cancelled", "Codex app-server request cancelled"));
235
- signal.addEventListener("abort", abort, { once: true });
236
- promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
237
- });
238
- }
239
- }
240
-
241
- function parseRateLimits(
242
- raw: unknown,
243
- identity?: Pick<CodexOAuthCredentials, "accountEmail" | "accountId">,
244
- ): CodexUsageSnapshot {
245
- if (!isRecord(raw)) throw new UsageError("invalid-response", "Codex app-server response is missing a result");
246
- const limits = pickRecord(raw, "rateLimits", "rate_limits");
247
- if (!limits) throw new UsageError("invalid-response", "Codex app-server response is missing rate limits");
248
-
249
- const primaryWindow = parseWindow(pickRecord(limits, "primary", "primaryWindow", "primary_window"));
250
- const secondaryWindow = parseWindow(pickRecord(limits, "secondary", "secondaryWindow", "secondary_window"));
251
- const creditsRaw = pickRecord(limits, "credits");
252
- const creditBalance = creditsRaw
253
- ? pickNumber(creditsRaw, "balance") ?? pickNumericString(creditsRaw, "balance")
254
- : undefined;
255
- const individualLimit = parseIndividualLimit(pickRecord(limits, "individualLimit", "individual_limit"));
256
-
257
- return {
258
- source: "cli",
259
- planType: pickString(limits, "planType", "plan_type") ?? null,
260
- primaryWindow,
261
- secondaryWindow,
262
- credits: creditsRaw ? {
263
- hasCredits: pickBoolean(creditsRaw, "hasCredits", "has_credits") ?? false,
264
- unlimited: pickBoolean(creditsRaw, "unlimited") ?? false,
265
- balance: creditBalance !== undefined && creditBalance >= 0 ? creditBalance : null,
266
- } : null,
267
- individualLimit,
268
- spendControlPresent: individualLimit !== null,
269
- updatedAt: new Date(),
270
- accountEmail: pickString(raw, "accountEmail", "account_email") ?? identity?.accountEmail ?? null,
271
- accountId: pickString(raw, "accountId", "account_id") ?? identity?.accountId ?? null,
272
- };
273
- }
274
-
275
- function parseIndividualLimit(
276
- raw: Record<string, unknown> | undefined,
277
- ): NonNullable<CodexUsageSnapshot["individualLimit"]> | null {
278
- if (!raw) return null;
279
- const used = pickNumber(raw, "used") ?? pickNumericString(raw, "used");
280
- const limit = pickNumber(raw, "limit") ?? pickNumericString(raw, "limit");
281
- if (used === undefined || limit === undefined || limit <= 0) return null;
282
- const resetValue = raw["resetsAt"] ?? raw["resets_at"];
283
- const resetsAt = typeof resetValue === "number"
284
- ? new Date(resetValue * 1_000)
285
- : typeof resetValue === "string" && /^\d+(?:\.\d+)?$/.test(resetValue.trim())
286
- ? new Date(Number(resetValue) * 1_000)
287
- : typeof resetValue === "string"
288
- ? new Date(resetValue)
289
- : null;
290
- return {
291
- used: Math.max(0, used),
292
- limit,
293
- resetsAt: resetsAt && !Number.isNaN(resetsAt.getTime()) ? resetsAt : null,
294
- };
295
- }
296
-
297
- function parseWindow(raw: Record<string, unknown> | undefined): RateWindowLegacy | null {
298
- if (!raw) return null;
299
- const usedPercent = pickNumber(raw, "usedPercent", "used_percent");
300
- if (usedPercent === undefined) return null;
301
- const windowMinutes = pickNumber(raw, "windowDurationMins", "windowMinutes", "window_minutes") ?? 300;
302
- const resetValue = raw["resetsAt"] ?? raw["resets_at"];
303
- const resetsAt = typeof resetValue === "number"
304
- ? new Date(resetValue * 1_000)
305
- : typeof resetValue === "string"
306
- ? new Date(resetValue)
307
- : null;
308
- const validReset = resetsAt && !Number.isNaN(resetsAt.getTime()) ? resetsAt : null;
309
- return {
310
- usedPercent: Math.max(0, Math.min(100, usedPercent)),
311
- windowMinutes,
312
- resetsAt: validReset,
313
- resetDescription: validReset ? formatDuration(Math.max(0, validReset.getTime() - Date.now())) : null,
314
- };
315
- }
316
-
317
- function formatDuration(ms: number): string {
318
- const minutes = Math.floor(ms / 60_000);
319
- const hours = Math.floor(minutes / 60);
320
- return hours > 0 ? `${hours}h ${minutes % 60}m` : `${minutes}m`;
321
- }
322
-
323
- function isRecord(value: unknown): value is Record<string, unknown> {
324
- return typeof value === "object" && value !== null && !Array.isArray(value);
325
- }
326
-
327
- function pickRecord(raw: Record<string, unknown>, ...keys: string[]): Record<string, unknown> | undefined {
328
- for (const key of keys) if (isRecord(raw[key])) return raw[key];
329
- return undefined;
330
- }
331
-
332
- function pickString(raw: Record<string, unknown>, ...keys: string[]): string | undefined {
333
- for (const key of keys) if (typeof raw[key] === "string") return raw[key];
334
- return undefined;
335
- }
336
-
337
- function pickNumber(raw: Record<string, unknown>, ...keys: string[]): number | undefined {
338
- for (const key of keys) if (typeof raw[key] === "number" && Number.isFinite(raw[key])) return raw[key];
339
- return undefined;
340
- }
341
-
342
- function pickNumericString(raw: Record<string, unknown>, ...keys: string[]): number | undefined {
343
- for (const key of keys) {
344
- if (typeof raw[key] !== "string") continue;
345
- const value = Number(raw[key]);
346
- if (Number.isFinite(value)) return value;
347
- }
348
- return undefined;
349
- }
350
-
351
- function pickBoolean(raw: Record<string, unknown>, ...keys: string[]): boolean | undefined {
352
- for (const key of keys) if (typeof raw[key] === "boolean") return raw[key];
353
- return undefined;
354
- }
@@ -1,240 +0,0 @@
1
- import type { UsageClient } from "../client";
2
- import type { CodexOAuthCredentials, CodexUsageSnapshot, RateWindowLegacy } from "../types";
3
- import { UsageError, classifyUsageError } from "../errors";
4
-
5
- export class AuthenticationError extends UsageError {
6
- constructor(message: string) {
7
- super("invalid-credentials", message);
8
- this.name = "AuthenticationError";
9
- }
10
- }
11
-
12
- export class ScopeError extends UsageError {
13
- constructor(message: string) {
14
- super("forbidden", message);
15
- this.name = "ScopeError";
16
- }
17
- }
18
-
19
- export class ParseError extends UsageError {
20
- constructor(message: string) {
21
- super("invalid-response", message);
22
- this.name = "ParseError";
23
- }
24
- }
25
-
26
- export class CodexOAuthUsageClient implements UsageClient<CodexOAuthCredentials, CodexUsageSnapshot> {
27
- async fetch(creds: CodexOAuthCredentials, options?: { signal?: AbortSignal }): Promise<CodexUsageSnapshot> {
28
- const url = `${backendApiBaseUrl(creds)}/wham/usage`;
29
-
30
- const headers: Record<string, string> = {
31
- Authorization: `Bearer ${creds.accessToken}`,
32
- "Content-Type": "application/json",
33
- };
34
- if (creds.accountId) {
35
- headers["ChatGPT-Account-Id"] = creds.accountId;
36
- }
37
-
38
- let res: Response;
39
- try {
40
- res = await fetch(url, {
41
- method: "GET",
42
- headers,
43
- signal: options?.signal,
44
- });
45
- } catch (error) {
46
- throw classifyUsageError(error);
47
- }
48
-
49
- if (res.status === 401) {
50
- throw new AuthenticationError("Codex OAuth token expired or invalid");
51
- }
52
- if (res.status === 403) {
53
- const body = await res.text();
54
- if (/scope|permission|access denied/i.test(body)) {
55
- throw new ScopeError("Codex OAuth access forbidden: missing scope");
56
- }
57
- throw new ScopeError("Codex OAuth access forbidden");
58
- }
59
- if (res.status === 429) {
60
- const retryAfter = Number.parseFloat(res.headers.get("retry-after") ?? "");
61
- throw new UsageError("rate-limited", "Codex usage request was rate limited", Number.isFinite(retryAfter) ? retryAfter * 1_000 : undefined);
62
- }
63
- if (!res.ok) {
64
- throw new UsageError("transient", `Codex usage server error: ${res.status}`);
65
- }
66
-
67
- let body: Record<string, unknown>;
68
- try {
69
- body = (await res.json()) as Record<string, unknown>;
70
- } catch {
71
- throw new ParseError("Codex usage response was not valid JSON");
72
- }
73
-
74
- const planType = pickString(body, "plan_type", "planType") ?? null;
75
- const responseAccountId = pickString(body, "account_id", "accountId") ?? null;
76
-
77
- // Handle both rate_limit and rate_limits key names
78
- const rateLimitKey = typeof body["rate_limit"] === "object" && body["rate_limit"] !== null
79
- ? "rate_limit"
80
- : typeof body["rate_limits"] === "object" && body["rate_limits"] !== null
81
- ? "rate_limits"
82
- : null;
83
- const rateLimit = rateLimitKey ? (body[rateLimitKey] as Record<string, unknown> | null) : null;
84
-
85
- if (!rateLimit) {
86
- throw new ParseError("Codex usage response missing rate_limit or rate_limits");
87
- }
88
-
89
- const primaryWindow = parseWindow(rateLimit?.["primary_window"] as Record<string, unknown> | undefined);
90
- const secondaryWindow = parseWindow(rateLimit?.["secondary_window"] as Record<string, unknown> | undefined);
91
- const spendControl = pickRecord(body, "spend_control", "spendControl");
92
- const individualLimit = parseIndividualLimit(
93
- pickRecord(body, "individual_limit", "individualLimit")
94
- ?? pickRecord(rateLimit, "individual_limit", "individualLimit")
95
- ?? (spendControl ? pickRecord(spendControl, "individual_limit", "individualLimit") : undefined),
96
- );
97
-
98
- let credits: CodexUsageSnapshot["credits"] = null;
99
- if (typeof body["credits"] === "object" && body["credits"] !== null) {
100
- const c = body["credits"] as Record<string, unknown>;
101
- const balance = pickFiniteNumber(c, "balance");
102
- credits = {
103
- hasCredits: c["has_credits"] === true,
104
- unlimited: c["unlimited"] === true,
105
- balance: balance !== undefined && balance >= 0 ? balance : null,
106
- };
107
- }
108
-
109
- return {
110
- source: "oauth",
111
- planType,
112
- primaryWindow,
113
- secondaryWindow,
114
- credits,
115
- individualLimit,
116
- spendControlPresent: Object.hasOwn(body, "spend_control") || Object.hasOwn(body, "spendControl"),
117
- updatedAt: new Date(),
118
- accountEmail: creds.accountEmail ?? null,
119
- accountId: responseAccountId ?? creds.accountId ?? null,
120
- };
121
- }
122
- }
123
-
124
- export class CodexMonthlySpendClient {
125
- async fetch(
126
- creds: CodexOAuthCredentials,
127
- accountId: string,
128
- options?: { signal?: AbortSignal },
129
- ): Promise<NonNullable<CodexUsageSnapshot["individualLimit"]> | null> {
130
- const encodedAccountId = encodeURIComponent(accountId);
131
- const url = `${backendApiBaseUrl(creds)}/accounts/${encodedAccountId}/spend-controls/current-user/monthly-usage`;
132
- let res: Response;
133
- try {
134
- res = await fetch(url, {
135
- method: "GET",
136
- headers: {
137
- Authorization: `Bearer ${creds.accessToken}`,
138
- Accept: "application/json",
139
- "ChatGPT-Account-Id": accountId,
140
- },
141
- signal: options?.signal,
142
- });
143
- } catch (error) {
144
- throw classifyUsageError(error);
145
- }
146
- if (!res.ok) {
147
- throw new UsageError(
148
- res.status === 401 ? "invalid-credentials" : res.status === 403 ? "forbidden" : "transient",
149
- `Codex monthly spend request failed: ${res.status}`,
150
- );
151
- }
152
-
153
- let body: Record<string, unknown>;
154
- try {
155
- body = (await res.json()) as Record<string, unknown>;
156
- } catch {
157
- throw new ParseError("Codex monthly spend response was not valid JSON");
158
- }
159
- const effectiveLimit = pickRecord(body, "effective_monthly_limit", "effectiveMonthlyLimit");
160
- if (!effectiveLimit) return null;
161
- const enforcement = pickString(effectiveLimit, "enforcement_mode", "enforcementMode")?.toLowerCase();
162
- if (enforcement && ["none", "off", "disabled", "no_limit"].includes(enforcement)) return null;
163
- const used = pickFiniteNumber(body, "current_month_usage", "currentMonthUsage");
164
- const limit = pickFiniteNumber(effectiveLimit, "limit");
165
- if (used === undefined || limit === undefined || limit <= 0) return null;
166
- return { used: Math.max(0, used), limit, resetsAt: null };
167
- }
168
- }
169
-
170
- function parseWindow(raw: Record<string, unknown> | undefined): RateWindowLegacy | null {
171
- if (!raw) return null;
172
- const usedPercent = typeof raw["used_percent"] === "number" ? raw["used_percent"] : 0;
173
- const limitWindowSeconds = typeof raw["limit_window_seconds"] === "number" ? raw["limit_window_seconds"] : undefined;
174
- const resetAtRaw = typeof raw["reset_at"] === "number" ? raw["reset_at"] : typeof raw["resets_at"] === "number" ? raw["resets_at"] : undefined;
175
- const resetsAt = resetAtRaw ? new Date(resetAtRaw * 1000) : null;
176
- const windowMinutes = limitWindowSeconds ? Math.round(limitWindowSeconds / 60) : 300;
177
- const resetDescription = resetsAt ? formatDuration(Math.max(0, resetsAt.getTime() - Date.now())) : null;
178
-
179
- return {
180
- usedPercent,
181
- windowMinutes,
182
- resetsAt,
183
- resetDescription,
184
- };
185
- }
186
-
187
- function parseIndividualLimit(
188
- raw: Record<string, unknown> | undefined,
189
- ): NonNullable<CodexUsageSnapshot["individualLimit"]> | null {
190
- if (!raw) return null;
191
- const used = pickFiniteNumber(raw, "used");
192
- const limit = pickFiniteNumber(raw, "limit");
193
- if (used === undefined || limit === undefined || limit <= 0) return null;
194
- const reset = pickFiniteNumber(raw, "resets_at", "resetsAt", "reset_at");
195
- return {
196
- used: Math.max(0, used),
197
- limit,
198
- resetsAt: reset === undefined ? null : new Date(reset * 1_000),
199
- };
200
- }
201
-
202
- function backendApiBaseUrl(creds: CodexOAuthCredentials): string {
203
- const base = (creds.chatgptBaseUrl ?? "https://chatgpt.com").replace(/\/+$/, "");
204
- return base.endsWith("/backend-api") ? base : `${base}/backend-api`;
205
- }
206
-
207
- function pickRecord(raw: Record<string, unknown>, ...keys: string[]): Record<string, unknown> | undefined {
208
- for (const key of keys) {
209
- const value = raw[key];
210
- if (typeof value === "object" && value !== null && !Array.isArray(value)) {
211
- return value as Record<string, unknown>;
212
- }
213
- }
214
- return undefined;
215
- }
216
-
217
- function pickString(raw: Record<string, unknown>, ...keys: string[]): string | undefined {
218
- for (const key of keys) if (typeof raw[key] === "string") return raw[key];
219
- return undefined;
220
- }
221
-
222
- function pickFiniteNumber(raw: Record<string, unknown>, ...keys: string[]): number | undefined {
223
- for (const key of keys) {
224
- const value = raw[key];
225
- const normalized = typeof value === "string" ? value.trim() : value;
226
- if (normalized === "") continue;
227
- const number = typeof normalized === "number" ? normalized : typeof normalized === "string" ? Number(normalized) : Number.NaN;
228
- if (Number.isFinite(number)) return number;
229
- }
230
- return undefined;
231
- }
232
-
233
- function formatDuration(ms: number): string {
234
- const seconds = Math.floor(ms / 1000);
235
- const minutes = Math.floor(seconds / 60);
236
- const hours = Math.floor(minutes / 60);
237
- if (hours > 0) return `${hours}h ${minutes % 60}m`;
238
- if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
239
- return `${seconds}s`;
240
- }
@@ -1,100 +0,0 @@
1
- import { homedir } from "node:os";
2
- import { join } from "node:path";
3
-
4
- import type { OpenCodeGoUsageSnapshot } from "../types";
5
-
6
- export interface OpenCodeGoUsageApiResponse {
7
- usage?: {
8
- rolling?: UsageApiWindow;
9
- weekly?: UsageApiWindow;
10
- monthly?: UsageApiWindow;
11
- };
12
- }
13
-
14
- interface UsageApiWindow {
15
- status?: string;
16
- percent?: number;
17
- resetsAt?: string;
18
- }
19
-
20
- export const OPENCODE_API_KEY_ENV = "OPENCODE_API_KEY";
21
-
22
- export class OpenCodeGoApiKeyUsageClient {
23
- constructor(
24
- private readonly usageUrl: string = "https://opencode.ai/zen/go/v1/usage",
25
- private readonly authJsonPath: string = join(homedir(), ".local", "share", "opencode", "auth.json"),
26
- private readonly env: Record<string, string | undefined> = process.env,
27
- ) {}
28
-
29
- async resolveApiKey(): Promise<string | null> {
30
- const fromEnv = this.env[OPENCODE_API_KEY_ENV]?.trim();
31
- if (fromEnv) return fromEnv;
32
- try {
33
- const auth = (JSON.parse(await Bun.file(this.authJsonPath).text()) as Record<string, unknown>)["opencode-go"];
34
- if (auth && typeof auth === "object") {
35
- const apiKey = (auth as Record<string, unknown>)["key"];
36
- if (typeof apiKey === "string" && apiKey.trim()) return apiKey.trim();
37
- }
38
- } catch {
39
- // Missing or malformed auth.json simply means no API key available.
40
- }
41
- return null;
42
- }
43
-
44
- async fetch(): Promise<OpenCodeGoUsageSnapshot | null> {
45
- const apiKey = await this.resolveApiKey();
46
- if (!apiKey) return null;
47
-
48
- let response: Response;
49
- try {
50
- response = await fetch(this.usageUrl, {
51
- headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json", "User-Agent": "scrylog" },
52
- redirect: "manual",
53
- });
54
- } catch {
55
- return null;
56
- }
57
- if (!response.ok) return null;
58
-
59
- let body: OpenCodeGoUsageApiResponse;
60
- try {
61
- body = (await response.json()) as OpenCodeGoUsageApiResponse;
62
- } catch {
63
- return null;
64
- }
65
- const usage = body.usage;
66
- if (!usage || typeof usage !== "object") return null;
67
-
68
- const now = new Date();
69
- const rolling = parseUsageWindow(usage.rolling, now);
70
- const weekly = parseUsageWindow(usage.weekly, now);
71
- if (!rolling || !weekly) return null;
72
- const monthly = parseUsageWindow(usage.monthly, now);
73
-
74
- return {
75
- source: "api",
76
- hasMonthlyUsage: monthly !== null,
77
- rollingUsagePercent: rolling.percent,
78
- weeklyUsagePercent: weekly.percent,
79
- monthlyUsagePercent: monthly?.percent ?? 0,
80
- rollingResetInSec: rolling.resetInSec,
81
- weeklyResetInSec: weekly.resetInSec,
82
- monthlyResetInSec: monthly?.resetInSec ?? 0,
83
- updatedAt: now,
84
- workspaceId: null,
85
- workspaceName: null,
86
- };
87
- }
88
- }
89
-
90
- function parseUsageWindow(raw: UsageApiWindow | undefined, now: Date): { percent: number; resetInSec: number } | null {
91
- if (!raw || typeof raw !== "object" || raw.status === "exceeded") return null;
92
- if (typeof raw.percent !== "number" || !Number.isFinite(raw.percent)) return null;
93
- const percent = Math.max(0, Math.min(100, raw.percent));
94
- let resetInSec = 0;
95
- if (typeof raw.resetsAt === "string") {
96
- const at = Date.parse(raw.resetsAt);
97
- if (!Number.isNaN(at)) resetInSec = Math.max(0, Math.round((at - now.getTime()) / 1000));
98
- }
99
- return { percent, resetInSec };
100
- }