@scrylog/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +836 -0
  3. package/package.json +75 -0
  4. package/src/api/docs/sessions.keys.ts +6 -0
  5. package/src/api/docs/sessions.ts +81 -0
  6. package/src/api/docs/system.keys.ts +5 -0
  7. package/src/api/docs/system.ts +66 -0
  8. package/src/api/docs/usage.keys.ts +3 -0
  9. package/src/api/docs/usage.ts +24 -0
  10. package/src/api/docs/webhooks.keys.ts +5 -0
  11. package/src/api/docs/webhooks.ts +78 -0
  12. package/src/api/index.ts +18 -0
  13. package/src/api/lib/auth.ts +43 -0
  14. package/src/api/lib/errors.ts +28 -0
  15. package/src/api/lib/route.ts +18 -0
  16. package/src/api/models/claude.ts +4 -0
  17. package/src/api/models/codex.ts +4 -0
  18. package/src/api/models/opencode.ts +11 -0
  19. package/src/api/routes/sessions.ts +47 -0
  20. package/src/api/routes/system.ts +103 -0
  21. package/src/api/routes/usage.ts +15 -0
  22. package/src/api/routes/webhooks/claude.ts +161 -0
  23. package/src/api/routes/webhooks/codex.ts +175 -0
  24. package/src/api/routes/webhooks/opencode.ts +107 -0
  25. package/src/api/routes.ts +93 -0
  26. package/src/api/schemas/common.ts +21 -0
  27. package/src/api/schemas/sessions.ts +72 -0
  28. package/src/api/schemas/usage.ts +50 -0
  29. package/src/api/schemas/webhooks/claude.ts +32 -0
  30. package/src/api/schemas/webhooks/codex.ts +34 -0
  31. package/src/api/schemas/webhooks/opencode.ts +29 -0
  32. package/src/api/services/claude-session.ts +9 -0
  33. package/src/api/services/codex-session.ts +8 -0
  34. package/src/api/services/opencode-session.ts +73 -0
  35. package/src/classifier.ts +206 -0
  36. package/src/cli/commands.ts +498 -0
  37. package/src/cli/prompt.ts +44 -0
  38. package/src/daemon/cli.ts +27 -0
  39. package/src/daemon/collectors/claude/README.md +108 -0
  40. package/src/daemon/collectors/claude/collector.ts +413 -0
  41. package/src/daemon/collectors/claude/discovery.ts +148 -0
  42. package/src/daemon/collectors/claude/hooks.ts +181 -0
  43. package/src/daemon/collectors/claude/index.ts +8 -0
  44. package/src/daemon/collectors/claude/jsonl.ts +85 -0
  45. package/src/daemon/collectors/claude/transcript.ts +246 -0
  46. package/src/daemon/collectors/codex/README.md +147 -0
  47. package/src/daemon/collectors/codex/collector.ts +661 -0
  48. package/src/daemon/collectors/codex/discovery.ts +104 -0
  49. package/src/daemon/collectors/codex/hooks.ts +179 -0
  50. package/src/daemon/collectors/codex/index.ts +7 -0
  51. package/src/daemon/collectors/codex/jsonl.ts +108 -0
  52. package/src/daemon/collectors/codex/transcript.ts +299 -0
  53. package/src/daemon/collectors/git.ts +295 -0
  54. package/src/daemon/collectors/opencode/README.md +106 -0
  55. package/src/daemon/collectors/opencode/collector.ts +543 -0
  56. package/src/daemon/collectors/opencode/discovery.ts +257 -0
  57. package/src/daemon/collectors/opencode/index.ts +5 -0
  58. package/src/daemon/collectors/opencode/session-builder.ts +59 -0
  59. package/src/daemon/collectors/opencode-db.ts +282 -0
  60. package/src/daemon/collectors/process.ts +201 -0
  61. package/src/daemon/collectors/tmux/README.md +128 -0
  62. package/src/daemon/collectors/tmux/collector.ts +472 -0
  63. package/src/daemon/collectors/tmux/enrichment.ts +241 -0
  64. package/src/daemon/collectors/tmux/index.ts +4 -0
  65. package/src/daemon/collectors/tmux/parse.ts +128 -0
  66. package/src/daemon/collectors/usage.ts +193 -0
  67. package/src/daemon/index.ts +207 -0
  68. package/src/daemon/scheduler.ts +86 -0
  69. package/src/daemon/store.ts +535 -0
  70. package/src/detect.ts +143 -0
  71. package/src/hooks/claude.ts +596 -0
  72. package/src/hooks/codex.ts +517 -0
  73. package/src/legacy/classifier.ts +215 -0
  74. package/src/legacy/detect.ts +143 -0
  75. package/src/mcp/client-server.ts +21 -0
  76. package/src/mcp/http-handler.ts +126 -0
  77. package/src/mcp/index.ts +61 -0
  78. package/src/mcp/resources/config-current.ts +19 -0
  79. package/src/mcp/resources/index.ts +11 -0
  80. package/src/mcp/resources/sessions-active.ts +19 -0
  81. package/src/mcp/resources/sessions-all.ts +19 -0
  82. package/src/mcp/schemas/sessions.ts +3 -0
  83. package/src/mcp/server.ts +17 -0
  84. package/src/mcp/services/api-adapter.ts +48 -0
  85. package/src/mcp/services/data-source.ts +22 -0
  86. package/src/mcp/services/store-adapter.ts +40 -0
  87. package/src/mcp/tools/get-active-sessions.ts +16 -0
  88. package/src/mcp/tools/get-session-children.ts +24 -0
  89. package/src/mcp/tools/get-session-messages.ts +24 -0
  90. package/src/mcp/tools/get-session.ts +23 -0
  91. package/src/mcp/tools/index.ts +15 -0
  92. package/src/mcp/tools/list-sessions.ts +16 -0
  93. package/src/neo-blessed.d.ts +4 -0
  94. package/src/oc/index.ts +302 -0
  95. package/src/service/manager.ts +242 -0
  96. package/src/shared/config.ts +478 -0
  97. package/src/shared/ipc.ts +80 -0
  98. package/src/shared/logger.ts +248 -0
  99. package/src/shared/paths.ts +73 -0
  100. package/src/shared/platform.ts +110 -0
  101. package/src/shared/session-view.ts +241 -0
  102. package/src/shared/types.ts +299 -0
  103. package/src/shared/utils.ts +339 -0
  104. package/src/tui/app.ts +920 -0
  105. package/src/tui/client.ts +149 -0
  106. package/src/tui/index.ts +181 -0
  107. package/src/types.ts +113 -0
  108. package/src/usage/README.md +323 -0
  109. package/src/usage/api.ts +99 -0
  110. package/src/usage/client.ts +3 -0
  111. package/src/usage/clients/anthropic-oauth-usage.ts +178 -0
  112. package/src/usage/clients/anthropic-web-usage.ts +245 -0
  113. package/src/usage/clients/claude-cli-probe.ts +187 -0
  114. package/src/usage/clients/codex-oauth-usage.ts +113 -0
  115. package/src/usage/clients/opencode-go-usage.ts +441 -0
  116. package/src/usage/index.ts +19 -0
  117. package/src/usage/mappers.ts +273 -0
  118. package/src/usage/planner.ts +54 -0
  119. package/src/usage/providers/claude.ts +113 -0
  120. package/src/usage/providers/codex.ts +34 -0
  121. package/src/usage/providers/opencode-go.ts +37 -0
  122. package/src/usage/resolver.ts +4 -0
  123. package/src/usage/resolvers/browser-cookies.ts +198 -0
  124. package/src/usage/resolvers/claude-auth.ts +99 -0
  125. package/src/usage/resolvers/claude-cli.ts +24 -0
  126. package/src/usage/resolvers/claude-web.ts +16 -0
  127. package/src/usage/resolvers/codex-auth.ts +117 -0
  128. package/src/usage/resolvers/opencode-go-web.ts +14 -0
  129. package/src/usage/types.ts +190 -0
@@ -0,0 +1,441 @@
1
+ import type { UsageClient } from "../client";
2
+ import type { OpenCodeGoUsageSnapshot } from "../types";
3
+ import { InvalidCredentialsError } from "../clients/anthropic-web-usage";
4
+ import { ParseError } from "../clients/anthropic-oauth-usage";
5
+
6
+ function randomUuid(): string {
7
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
8
+ const r = (Math.random() * 16) | 0;
9
+ const v = c === "x" ? r : (r & 0x3) | 0x8;
10
+ return v.toString(16);
11
+ });
12
+ }
13
+
14
+ function isSignedOut(text: string): boolean {
15
+ const lower = text.toLowerCase();
16
+ const indicators = ["login", "sign in", "auth/authorize", "not associated with an account", 'actor of type "public"'];
17
+ return indicators.some((ind) => lower.includes(ind.toLowerCase()));
18
+ }
19
+
20
+ function extractWorkspaceIds(text: string): string[] {
21
+ const ids: string[] = [];
22
+ // Strategy 1: regex
23
+ const regex = /id\s*:\s*"(wrk_[^"]+)"/g;
24
+ let m: RegExpExecArray | null;
25
+ while ((m = regex.exec(text))) {
26
+ ids.push(m[1]!);
27
+ }
28
+ if (ids.length > 0) return ids;
29
+
30
+ // Strategy 2: JSON traversal
31
+ try {
32
+ const data = JSON.parse(text);
33
+ function walk(obj: unknown): void {
34
+ if (typeof obj === "string" && obj.startsWith("wrk_") && obj.length > 4) {
35
+ ids.push(obj);
36
+ } else if (Array.isArray(obj)) {
37
+ for (const item of obj) walk(item);
38
+ } else if (obj && typeof obj === "object") {
39
+ for (const v of Object.values(obj)) walk(v);
40
+ }
41
+ }
42
+ walk(data);
43
+ } catch {
44
+ // ignore
45
+ }
46
+
47
+ return ids;
48
+ }
49
+
50
+ function extractWorkspace(text: string): { id: string; name: string | null } | null {
51
+ // Try to extract id + name from the same object pattern
52
+ const regex = /id\s*:\s*"(wrk_[^"]+)"(?:\s*,\s*name\s*:\s*"([^"]*)")?/;
53
+ const m = regex.exec(text);
54
+ if (m) {
55
+ return { id: m[1]!, name: m[2] || null };
56
+ }
57
+
58
+ // Fallback to ids only
59
+ const ids = extractWorkspaceIds(text);
60
+ if (ids.length > 0) {
61
+ return { id: ids[0]!, name: null };
62
+ }
63
+
64
+ return null;
65
+ }
66
+
67
+ interface WindowCandidate {
68
+ percent: number;
69
+ resetInSec: number;
70
+ path: string;
71
+ }
72
+
73
+ function findCandidates(obj: unknown, path = "", depth = 0): WindowCandidate[] {
74
+ if (depth > 10 || !obj || typeof obj !== "object") return [];
75
+ const candidates: WindowCandidate[] = [];
76
+
77
+ if (Array.isArray(obj)) {
78
+ for (let i = 0; i < obj.length; i++) {
79
+ candidates.push(...findCandidates(obj[i], `${path}[${i}]`, depth + 1));
80
+ }
81
+ return candidates;
82
+ }
83
+
84
+ const record = obj as Record<string, unknown>;
85
+ const keys = Object.keys(record);
86
+
87
+ // Check if this object is a candidate
88
+ const percentKeys = [
89
+ "usagePercent",
90
+ "usedPercent",
91
+ "percentUsed",
92
+ "percent",
93
+ "usage_percent",
94
+ "used_percent",
95
+ "utilization",
96
+ "utilizationPercent",
97
+ "utilization_percent",
98
+ "usage",
99
+ ];
100
+ const resetSecKeys = [
101
+ "resetInSec",
102
+ "resetInSeconds",
103
+ "resetSeconds",
104
+ "reset_sec",
105
+ "reset_in_sec",
106
+ "resetsInSec",
107
+ "resetsInSeconds",
108
+ "resetIn",
109
+ "resetSec",
110
+ ];
111
+ const resetTsKeys = [
112
+ "resetAt",
113
+ "resetsAt",
114
+ "reset_at",
115
+ "resets_at",
116
+ "nextReset",
117
+ "next_reset",
118
+ "renewAt",
119
+ "renew_at",
120
+ ];
121
+ const usedKeys = ["used", "usage", "consumed", "count", "usedTokens"];
122
+ const limitKeys = ["limit", "total", "quota", "max", "cap", "tokenLimit"];
123
+
124
+ let percent: number | null = null;
125
+ let resetInSec = 0;
126
+
127
+ for (const key of keys) {
128
+ const val = record[key];
129
+ if (percentKeys.includes(key) && (typeof val === "number" || typeof val === "string")) {
130
+ const n = typeof val === "string" ? Number.parseFloat(val) : val;
131
+ if (!Number.isNaN(n)) percent = n;
132
+ }
133
+ if (resetSecKeys.includes(key) && typeof val === "number") {
134
+ resetInSec = val;
135
+ }
136
+ if (resetTsKeys.includes(key) && (typeof val === "number" || typeof val === "string")) {
137
+ if (typeof val === "number") {
138
+ const ts = val > 1_000_000_000_000 ? val / 1000 : val;
139
+ resetInSec = Math.max(0, Math.round(ts - Date.now() / 1000));
140
+ } else {
141
+ const d = new Date(val);
142
+ if (!Number.isNaN(d.getTime())) {
143
+ resetInSec = Math.max(0, Math.round(d.getTime() / 1000 - Date.now() / 1000));
144
+ }
145
+ }
146
+ }
147
+ if (usedKeys.includes(key) && typeof val === "number" && percent === null) {
148
+ const limitVal = keys.find((k) => limitKeys.includes(k));
149
+ if (limitVal && typeof record[limitVal] === "number" && record[limitVal]! > 0) {
150
+ percent = (val / (record[limitVal] as number)) * 100;
151
+ }
152
+ }
153
+ }
154
+
155
+ if (percent !== null) {
156
+ let p = percent <= 1 && percent >= 0 ? percent * 100 : percent;
157
+ p = Math.max(0, Math.min(100, p));
158
+ candidates.push({ percent: p, resetInSec, path });
159
+ }
160
+
161
+ // Recurse
162
+ for (const [key, val] of Object.entries(record)) {
163
+ candidates.push(...findCandidates(val, path ? `${path}.${key}` : key, depth + 1));
164
+ }
165
+
166
+ return candidates;
167
+ }
168
+
169
+ function classifyCandidates(candidates: WindowCandidate[]): {
170
+ rolling: WindowCandidate | null;
171
+ weekly: WindowCandidate | null;
172
+ monthly: WindowCandidate | null;
173
+ } {
174
+ let rolling: WindowCandidate | null = null;
175
+ let weekly: WindowCandidate | null = null;
176
+ let monthly: WindowCandidate | null = null;
177
+ const used = new Set<number>();
178
+
179
+ for (let i = 0; i < candidates.length; i++) {
180
+ if (used.has(i)) continue;
181
+ const c = candidates[i]!;
182
+ const lowerPath = c.path.toLowerCase();
183
+ if (
184
+ lowerPath.includes("rolling") ||
185
+ lowerPath.includes("hour") ||
186
+ lowerPath.includes("5h") ||
187
+ lowerPath.includes("5-hour")
188
+ ) {
189
+ if (!rolling || c.resetInSec < rolling.resetInSec || (c.resetInSec === rolling.resetInSec && c.percent > rolling.percent)) {
190
+ if (rolling) used.add(candidates.indexOf(rolling));
191
+ rolling = c;
192
+ used.add(i);
193
+ }
194
+ } else if (lowerPath.includes("weekly") || lowerPath.includes("week")) {
195
+ if (!weekly || c.resetInSec > weekly.resetInSec || (c.resetInSec === weekly.resetInSec && c.percent > weekly.percent)) {
196
+ if (weekly) used.add(candidates.indexOf(weekly));
197
+ weekly = c;
198
+ used.add(i);
199
+ }
200
+ } else if (lowerPath.includes("monthly") || lowerPath.includes("month")) {
201
+ if (!monthly || c.resetInSec > monthly.resetInSec || (c.resetInSec === monthly.resetInSec && c.percent > monthly.percent)) {
202
+ if (monthly) used.add(candidates.indexOf(monthly));
203
+ monthly = c;
204
+ used.add(i);
205
+ }
206
+ }
207
+ }
208
+
209
+ // If still missing, assign remaining candidates by resetInSec heuristic
210
+ const remaining = candidates.filter((_, i) => !used.has(i));
211
+ remaining.sort((a, b) => a.resetInSec - b.resetInSec);
212
+ if (!rolling && remaining.length > 0) {
213
+ rolling = remaining.shift()!;
214
+ }
215
+ if (!weekly && remaining.length > 0) {
216
+ weekly = remaining.pop()!;
217
+ }
218
+ if (!monthly && remaining.length > 0) {
219
+ monthly = remaining.pop()!;
220
+ }
221
+
222
+ return { rolling, weekly, monthly };
223
+ }
224
+
225
+ function parseSubscriptionPage(text: string): {
226
+ rollingPercent: number;
227
+ weeklyPercent: number;
228
+ monthlyPercent: number;
229
+ rollingReset: number;
230
+ weeklyReset: number;
231
+ monthlyReset: number;
232
+ hasMonthly: boolean;
233
+ } {
234
+ if (isSignedOut(text)) {
235
+ throw new InvalidCredentialsError("Session is signed out");
236
+ }
237
+
238
+ // Strategy 1: JSON direct lookup
239
+ let data: unknown;
240
+ try {
241
+ data = JSON.parse(text);
242
+ } catch {
243
+ data = null;
244
+ }
245
+
246
+ if (data && typeof data === "object") {
247
+ const record = data as Record<string, unknown>;
248
+ const nestedKeys = ["data", "result", "usage", "billing", "payload"];
249
+
250
+ const rollingKeys = ["rollingUsage", "rolling", "rolling_usage", "rollingWindow", "rolling_window"];
251
+ const weeklyKeys = ["weeklyUsage", "weekly", "weekly_usage", "weeklyWindow", "weekly_window"];
252
+ const monthlyKeys = ["monthlyUsage", "monthly", "monthly_usage", "monthlyWindow", "monthly_window"];
253
+
254
+ function getVal(root: Record<string, unknown>, keys: string[]): unknown {
255
+ for (const k of keys) {
256
+ if (k in root) return root[k];
257
+ }
258
+ for (const nk of nestedKeys) {
259
+ const nested = root[nk];
260
+ if (nested && typeof nested === "object") {
261
+ for (const k of keys) {
262
+ if (k in (nested as Record<string, unknown>)) return (nested as Record<string, unknown>)[k];
263
+ }
264
+ }
265
+ }
266
+ return undefined;
267
+ }
268
+
269
+ const rollingVal = getVal(record, rollingKeys);
270
+ const weeklyVal = getVal(record, weeklyKeys);
271
+ const monthlyVal = getVal(record, monthlyKeys);
272
+
273
+ if (rollingVal !== undefined && weeklyVal !== undefined) {
274
+ function extractPercent(v: unknown): number {
275
+ if (typeof v === "number") return v <= 1 && v >= 0 ? v * 100 : v;
276
+ if (typeof v === "object" && v !== null) {
277
+ const r = v as Record<string, unknown>;
278
+ for (const k of ["usagePercent", "usedPercent", "percentUsed", "percent", "utilization"]) {
279
+ if (typeof r[k] === "number") {
280
+ const n = r[k] as number;
281
+ return n <= 1 && n >= 0 ? n * 100 : n;
282
+ }
283
+ }
284
+ }
285
+ return 0;
286
+ }
287
+ function extractReset(v: unknown): number {
288
+ if (typeof v === "number") return v;
289
+ if (typeof v === "object" && v !== null) {
290
+ const r = v as Record<string, unknown>;
291
+ for (const k of ["resetInSec", "resetInSeconds", "resetSeconds"]) {
292
+ if (typeof r[k] === "number") return r[k] as number;
293
+ }
294
+ for (const k of ["resetAt", "resetsAt", "reset_at"]) {
295
+ if (typeof r[k] === "string") {
296
+ const d = new Date(r[k] as string);
297
+ if (!Number.isNaN(d.getTime())) return Math.max(0, Math.round(d.getTime() / 1000 - Date.now() / 1000));
298
+ }
299
+ }
300
+ }
301
+ return 0;
302
+ }
303
+
304
+ const rollingPercent = extractPercent(rollingVal);
305
+ const weeklyPercent = extractPercent(weeklyVal);
306
+ const monthlyPercent = monthlyVal !== undefined ? extractPercent(monthlyVal) : 0;
307
+ return {
308
+ rollingPercent: Math.max(0, Math.min(100, rollingPercent)),
309
+ weeklyPercent: Math.max(0, Math.min(100, weeklyPercent)),
310
+ monthlyPercent: Math.max(0, Math.min(100, monthlyPercent)),
311
+ rollingReset: extractReset(rollingVal),
312
+ weeklyReset: extractReset(weeklyVal),
313
+ monthlyReset: monthlyVal !== undefined ? extractReset(monthlyVal) : 0,
314
+ hasMonthly: monthlyVal !== undefined,
315
+ };
316
+ }
317
+
318
+ // Strategy 1b + 1c: nested search and candidate search
319
+ const candidates = findCandidates(data);
320
+ const classified = classifyCandidates(candidates);
321
+ if (classified.rolling && classified.weekly) {
322
+ return {
323
+ rollingPercent: classified.rolling.percent,
324
+ weeklyPercent: classified.weekly.percent,
325
+ monthlyPercent: classified.monthly?.percent ?? 0,
326
+ rollingReset: classified.rolling.resetInSec,
327
+ weeklyReset: classified.weekly.resetInSec,
328
+ monthlyReset: classified.monthly?.resetInSec ?? 0,
329
+ hasMonthly: !!classified.monthly,
330
+ };
331
+ }
332
+ }
333
+
334
+ // Strategy 2: Regex fallback
335
+ const rollingPercentMatch = text.match(/rollingUsage[^}]*?usagePercent\s*:\s*([0-9]+(?:\.[0-9]+)?)/);
336
+ const rollingResetMatch = text.match(/rollingUsage[^}]*?resetInSec\s*:\s*([0-9]+)/);
337
+ const weeklyPercentMatch = text.match(/weeklyUsage[^}]*?usagePercent\s*:\s*([0-9]+(?:\.[0-9]+)?)/);
338
+ const weeklyResetMatch = text.match(/weeklyUsage[^}]*?resetInSec\s*:\s*([0-9]+)/);
339
+ const monthlyPercentMatch = text.match(/monthlyUsage[^}]*?usagePercent\s*:\s*([0-9]+(?:\.[0-9]+)?)/);
340
+ const monthlyResetMatch = text.match(/monthlyUsage[^}]*?resetInSec\s*:\s*([0-9]+)/);
341
+
342
+ const rollingPercent = rollingPercentMatch ? Number.parseFloat(rollingPercentMatch[1]!) : null;
343
+ const weeklyPercent = weeklyPercentMatch ? Number.parseFloat(weeklyPercentMatch[1]!) : null;
344
+
345
+ if (rollingPercent === null || weeklyPercent === null) {
346
+ throw new ParseError("Missing usage fields.");
347
+ }
348
+
349
+ return {
350
+ rollingPercent: Math.max(0, Math.min(100, rollingPercent)),
351
+ weeklyPercent: Math.max(0, Math.min(100, weeklyPercent)),
352
+ monthlyPercent: monthlyPercentMatch ? Math.max(0, Math.min(100, Number.parseFloat(monthlyPercentMatch[1]!))) : 0,
353
+ rollingReset: rollingResetMatch ? Number.parseInt(rollingResetMatch[1]!, 10) : 0,
354
+ weeklyReset: weeklyResetMatch ? Number.parseInt(weeklyResetMatch[1]!, 10) : 0,
355
+ monthlyReset: monthlyResetMatch ? Number.parseInt(monthlyResetMatch[1]!, 10) : 0,
356
+ hasMonthly: monthlyPercentMatch !== null,
357
+ };
358
+ }
359
+
360
+ export class OpenCodeGoUsageClient implements UsageClient<{ cookies: string; serverFnId: string }, OpenCodeGoUsageSnapshot> {
361
+ async fetch(creds: { cookies: string; serverFnId: string }, options?: { signal?: AbortSignal }): Promise<OpenCodeGoUsageSnapshot> {
362
+ const workspace = await this.resolveWorkspace(creds.cookies, creds.serverFnId, options?.signal);
363
+ const url = `https://opencode.ai/workspace/${workspace.id}/go`;
364
+
365
+ const res = await fetch(url, {
366
+ signal: options?.signal,
367
+ headers: {
368
+ Cookie: creds.cookies,
369
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
370
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
371
+ },
372
+ });
373
+
374
+ if (res.status === 401 || res.status === 403) {
375
+ throw new InvalidCredentialsError("OpenCode Go session invalid");
376
+ }
377
+
378
+ const text = await res.text();
379
+ if (isSignedOut(text)) {
380
+ throw new InvalidCredentialsError("OpenCode Go session is signed out");
381
+ }
382
+
383
+ const parsed = parseSubscriptionPage(text);
384
+
385
+ return {
386
+ source: "web",
387
+ hasMonthlyUsage: parsed.hasMonthly,
388
+ rollingUsagePercent: parsed.rollingPercent,
389
+ weeklyUsagePercent: parsed.weeklyPercent,
390
+ monthlyUsagePercent: parsed.monthlyPercent,
391
+ rollingResetInSec: parsed.rollingReset,
392
+ weeklyResetInSec: parsed.weeklyReset,
393
+ monthlyResetInSec: parsed.monthlyReset,
394
+ updatedAt: new Date(),
395
+ workspaceId: workspace.id,
396
+ workspaceName: workspace.name,
397
+ };
398
+ }
399
+
400
+ private async resolveWorkspace(cookies: string, serverFnId: string, signal?: AbortSignal): Promise<{ id: string; name: string | null }> {
401
+ const headers: Record<string, string> = {
402
+ Cookie: cookies,
403
+ "X-Server-Id": serverFnId,
404
+ "X-Server-Instance": `server-fn:${randomUuid()}`,
405
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
406
+ Origin: "https://opencode.ai",
407
+ Referer: "https://opencode.ai",
408
+ Accept: "text/javascript, application/json;q=0.9, */*;q=0.8",
409
+ };
410
+
411
+ // GET attempt
412
+ let res = await fetch(`https://opencode.ai/_server?id=${serverFnId}`, { headers, signal });
413
+ let text = await res.text();
414
+
415
+ if (isSignedOut(text)) {
416
+ throw new InvalidCredentialsError("OpenCode Go session is signed out");
417
+ }
418
+
419
+ let workspace = extractWorkspace(text);
420
+ if (!workspace) {
421
+ // POST fallback
422
+ res = await fetch("https://opencode.ai/_server", {
423
+ method: "POST",
424
+ signal,
425
+ headers: { ...headers, "Content-Type": "application/json" },
426
+ body: "[]",
427
+ });
428
+ text = await res.text();
429
+ if (isSignedOut(text)) {
430
+ throw new InvalidCredentialsError("OpenCode Go session is signed out");
431
+ }
432
+ workspace = extractWorkspace(text);
433
+ }
434
+
435
+ if (!workspace) {
436
+ throw new ParseError("Missing workspace id.");
437
+ }
438
+
439
+ return workspace;
440
+ }
441
+ }
@@ -0,0 +1,19 @@
1
+ export * from "./types";
2
+ export * from "./resolver";
3
+ export * from "./client";
4
+ export * from "./planner";
5
+ export { UsageApi } from "./api";
6
+ export { mapClaudeToEntry, mapOpenCodeGoToEntry, mapCodexToEntry, buildClaudeAccountId, buildCodexAccountId } from "./mappers";
7
+ export { ClaudeUsageProvider } from "./providers/claude";
8
+ export { CodexUsageProvider } from "./providers/codex";
9
+ export { OpenCodeGoUsageProvider } from "./providers/opencode-go";
10
+ export { ClaudeAuthResolver } from "./resolvers/claude-auth";
11
+ export { ClaudeWebResolver } from "./resolvers/claude-web";
12
+ export { ClaudeCliResolver } from "./resolvers/claude-cli";
13
+ export { CodexAuthResolver } from "./resolvers/codex-auth";
14
+ export { OpenCodeGoWebResolver } from "./resolvers/opencode-go-web";
15
+ export { extractCookies, buildCookieHeader } from "./resolvers/browser-cookies";
16
+ export { AnthropicOAuthUsageClient } from "./clients/anthropic-oauth-usage";
17
+ export { AnthropicWebUsageClient } from "./clients/anthropic-web-usage";
18
+ export { CodexOAuthUsageClient } from "./clients/codex-oauth-usage";
19
+ export { OpenCodeGoUsageClient } from "./clients/opencode-go-usage";