@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.
- package/LICENSE +21 -0
- package/README.md +836 -0
- package/package.json +75 -0
- package/src/api/docs/sessions.keys.ts +6 -0
- package/src/api/docs/sessions.ts +81 -0
- package/src/api/docs/system.keys.ts +5 -0
- package/src/api/docs/system.ts +66 -0
- package/src/api/docs/usage.keys.ts +3 -0
- package/src/api/docs/usage.ts +24 -0
- package/src/api/docs/webhooks.keys.ts +5 -0
- package/src/api/docs/webhooks.ts +78 -0
- package/src/api/index.ts +18 -0
- package/src/api/lib/auth.ts +43 -0
- package/src/api/lib/errors.ts +28 -0
- package/src/api/lib/route.ts +18 -0
- package/src/api/models/claude.ts +4 -0
- package/src/api/models/codex.ts +4 -0
- package/src/api/models/opencode.ts +11 -0
- package/src/api/routes/sessions.ts +47 -0
- package/src/api/routes/system.ts +103 -0
- package/src/api/routes/usage.ts +15 -0
- package/src/api/routes/webhooks/claude.ts +161 -0
- package/src/api/routes/webhooks/codex.ts +175 -0
- package/src/api/routes/webhooks/opencode.ts +107 -0
- package/src/api/routes.ts +93 -0
- package/src/api/schemas/common.ts +21 -0
- package/src/api/schemas/sessions.ts +72 -0
- package/src/api/schemas/usage.ts +50 -0
- package/src/api/schemas/webhooks/claude.ts +32 -0
- package/src/api/schemas/webhooks/codex.ts +34 -0
- package/src/api/schemas/webhooks/opencode.ts +29 -0
- package/src/api/services/claude-session.ts +9 -0
- package/src/api/services/codex-session.ts +8 -0
- package/src/api/services/opencode-session.ts +73 -0
- package/src/classifier.ts +206 -0
- package/src/cli/commands.ts +498 -0
- package/src/cli/prompt.ts +44 -0
- package/src/daemon/cli.ts +27 -0
- package/src/daemon/collectors/claude/README.md +108 -0
- package/src/daemon/collectors/claude/collector.ts +413 -0
- package/src/daemon/collectors/claude/discovery.ts +148 -0
- package/src/daemon/collectors/claude/hooks.ts +181 -0
- package/src/daemon/collectors/claude/index.ts +8 -0
- package/src/daemon/collectors/claude/jsonl.ts +85 -0
- package/src/daemon/collectors/claude/transcript.ts +246 -0
- package/src/daemon/collectors/codex/README.md +147 -0
- package/src/daemon/collectors/codex/collector.ts +661 -0
- package/src/daemon/collectors/codex/discovery.ts +104 -0
- package/src/daemon/collectors/codex/hooks.ts +179 -0
- package/src/daemon/collectors/codex/index.ts +7 -0
- package/src/daemon/collectors/codex/jsonl.ts +108 -0
- package/src/daemon/collectors/codex/transcript.ts +299 -0
- package/src/daemon/collectors/git.ts +295 -0
- package/src/daemon/collectors/opencode/README.md +106 -0
- package/src/daemon/collectors/opencode/collector.ts +543 -0
- package/src/daemon/collectors/opencode/discovery.ts +257 -0
- package/src/daemon/collectors/opencode/index.ts +5 -0
- package/src/daemon/collectors/opencode/session-builder.ts +59 -0
- package/src/daemon/collectors/opencode-db.ts +282 -0
- package/src/daemon/collectors/process.ts +201 -0
- package/src/daemon/collectors/tmux/README.md +128 -0
- package/src/daemon/collectors/tmux/collector.ts +472 -0
- package/src/daemon/collectors/tmux/enrichment.ts +241 -0
- package/src/daemon/collectors/tmux/index.ts +4 -0
- package/src/daemon/collectors/tmux/parse.ts +128 -0
- package/src/daemon/collectors/usage.ts +193 -0
- package/src/daemon/index.ts +207 -0
- package/src/daemon/scheduler.ts +86 -0
- package/src/daemon/store.ts +535 -0
- package/src/detect.ts +143 -0
- package/src/hooks/claude.ts +596 -0
- package/src/hooks/codex.ts +517 -0
- package/src/legacy/classifier.ts +215 -0
- package/src/legacy/detect.ts +143 -0
- package/src/mcp/client-server.ts +21 -0
- package/src/mcp/http-handler.ts +126 -0
- package/src/mcp/index.ts +61 -0
- package/src/mcp/resources/config-current.ts +19 -0
- package/src/mcp/resources/index.ts +11 -0
- package/src/mcp/resources/sessions-active.ts +19 -0
- package/src/mcp/resources/sessions-all.ts +19 -0
- package/src/mcp/schemas/sessions.ts +3 -0
- package/src/mcp/server.ts +17 -0
- package/src/mcp/services/api-adapter.ts +48 -0
- package/src/mcp/services/data-source.ts +22 -0
- package/src/mcp/services/store-adapter.ts +40 -0
- package/src/mcp/tools/get-active-sessions.ts +16 -0
- package/src/mcp/tools/get-session-children.ts +24 -0
- package/src/mcp/tools/get-session-messages.ts +24 -0
- package/src/mcp/tools/get-session.ts +23 -0
- package/src/mcp/tools/index.ts +15 -0
- package/src/mcp/tools/list-sessions.ts +16 -0
- package/src/neo-blessed.d.ts +4 -0
- package/src/oc/index.ts +302 -0
- package/src/service/manager.ts +242 -0
- package/src/shared/config.ts +478 -0
- package/src/shared/ipc.ts +80 -0
- package/src/shared/logger.ts +248 -0
- package/src/shared/paths.ts +73 -0
- package/src/shared/platform.ts +110 -0
- package/src/shared/session-view.ts +241 -0
- package/src/shared/types.ts +299 -0
- package/src/shared/utils.ts +339 -0
- package/src/tui/app.ts +920 -0
- package/src/tui/client.ts +149 -0
- package/src/tui/index.ts +181 -0
- package/src/types.ts +113 -0
- package/src/usage/README.md +323 -0
- package/src/usage/api.ts +99 -0
- package/src/usage/client.ts +3 -0
- package/src/usage/clients/anthropic-oauth-usage.ts +178 -0
- package/src/usage/clients/anthropic-web-usage.ts +245 -0
- package/src/usage/clients/claude-cli-probe.ts +187 -0
- package/src/usage/clients/codex-oauth-usage.ts +113 -0
- package/src/usage/clients/opencode-go-usage.ts +441 -0
- package/src/usage/index.ts +19 -0
- package/src/usage/mappers.ts +273 -0
- package/src/usage/planner.ts +54 -0
- package/src/usage/providers/claude.ts +113 -0
- package/src/usage/providers/codex.ts +34 -0
- package/src/usage/providers/opencode-go.ts +37 -0
- package/src/usage/resolver.ts +4 -0
- package/src/usage/resolvers/browser-cookies.ts +198 -0
- package/src/usage/resolvers/claude-auth.ts +99 -0
- package/src/usage/resolvers/claude-cli.ts +24 -0
- package/src/usage/resolvers/claude-web.ts +16 -0
- package/src/usage/resolvers/codex-auth.ts +117 -0
- package/src/usage/resolvers/opencode-go-web.ts +14 -0
- 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";
|