@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,245 @@
|
|
|
1
|
+
import type { UsageClient } from "../client";
|
|
2
|
+
import type { ClaudeUsageSnapshot, NamedRateWindowLegacy, ProviderCostSnapshotLegacy, RateWindowLegacy, ClaudeWebSession } from "../types";
|
|
3
|
+
|
|
4
|
+
export class InvalidCredentialsError extends Error {
|
|
5
|
+
constructor(message: string) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "InvalidCredentialsError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Org {
|
|
12
|
+
uuid: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
capabilities?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface UsageResponse {
|
|
18
|
+
five_hour?: { utilization?: number | null; resets_at?: string | null } | null;
|
|
19
|
+
seven_day?: { utilization?: number | null; resets_at?: string | null } | null;
|
|
20
|
+
seven_day_sonnet?: { utilization?: number | null; resets_at?: string | null } | null;
|
|
21
|
+
seven_day_opus?: { utilization?: number | null; resets_at?: string | null } | null;
|
|
22
|
+
seven_day_design?: { utilization?: number | null; resets_at?: string | null } | null;
|
|
23
|
+
seven_day_routines?: { utilization?: number | null; resets_at?: string | null } | null;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface OverageResponse {
|
|
28
|
+
monthly_credit_limit?: number;
|
|
29
|
+
currency?: string;
|
|
30
|
+
used_credits?: number;
|
|
31
|
+
is_enabled?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface AccountResponse {
|
|
35
|
+
email_address?: string;
|
|
36
|
+
memberships?: Array<{
|
|
37
|
+
organization?: {
|
|
38
|
+
uuid: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
rate_limit_tier?: string;
|
|
41
|
+
billing_type?: string;
|
|
42
|
+
};
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DESIGN_ALTERNATES = [
|
|
47
|
+
"seven_day_design",
|
|
48
|
+
"seven_day_claude_design",
|
|
49
|
+
"claude_design",
|
|
50
|
+
"design",
|
|
51
|
+
"seven_day_omelette",
|
|
52
|
+
"omelette",
|
|
53
|
+
"omelette_promotional",
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const ROUTINES_ALTERNATES = [
|
|
57
|
+
"seven_day_routines",
|
|
58
|
+
"seven_day_claude_routines",
|
|
59
|
+
"claude_routines",
|
|
60
|
+
"routines",
|
|
61
|
+
"routine",
|
|
62
|
+
"seven_day_cowork",
|
|
63
|
+
"cowork",
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
function pickWindow(data: UsageResponse, keys: string[]): { utilization?: number | null; resets_at?: string | null } | null {
|
|
67
|
+
for (const key of keys) {
|
|
68
|
+
const val = data[key];
|
|
69
|
+
if (val && typeof val === "object" && "utilization" in val) {
|
|
70
|
+
return val as { utilization?: number | null; resets_at?: string | null };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function toRateWindow(
|
|
77
|
+
utilization: number | null | undefined,
|
|
78
|
+
resetsAt: string | null | undefined,
|
|
79
|
+
windowMinutes: number,
|
|
80
|
+
): RateWindowLegacy {
|
|
81
|
+
const usedPercent = typeof utilization === "number" ? Math.max(0, Math.min(100, utilization)) : 0;
|
|
82
|
+
const resetsAtDate = resetsAt ? new Date(resetsAt) : null;
|
|
83
|
+
const resetDescription = resetsAtDate && !Number.isNaN(resetsAtDate.getTime())
|
|
84
|
+
? resetsAtDate.toLocaleString()
|
|
85
|
+
: null;
|
|
86
|
+
return {
|
|
87
|
+
usedPercent,
|
|
88
|
+
windowMinutes,
|
|
89
|
+
resetsAt: resetsAtDate && !Number.isNaN(resetsAtDate.getTime()) ? resetsAtDate : null,
|
|
90
|
+
resetDescription,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class AnthropicWebUsageClient implements UsageClient<ClaudeWebSession, ClaudeUsageSnapshot> {
|
|
95
|
+
async fetch(creds: ClaudeWebSession, options?: { signal?: AbortSignal }): Promise<ClaudeUsageSnapshot> {
|
|
96
|
+
// 1. Fetch organizations
|
|
97
|
+
const orgRes = await fetch("https://claude.ai/api/organizations", {
|
|
98
|
+
signal: options?.signal,
|
|
99
|
+
headers: {
|
|
100
|
+
Cookie: creds.cookieHeader,
|
|
101
|
+
Accept: "application/json",
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (orgRes.status === 401 || orgRes.status === 403) {
|
|
106
|
+
throw new InvalidCredentialsError("Session cookie invalid or expired");
|
|
107
|
+
}
|
|
108
|
+
if (!orgRes.ok) {
|
|
109
|
+
throw new Error(`Organizations fetch failed: ${orgRes.status}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const orgs = (await orgRes.json()) as Org[];
|
|
113
|
+
if (!Array.isArray(orgs) || orgs.length === 0) {
|
|
114
|
+
throw new Error("No organizations found");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Select org
|
|
118
|
+
let org = orgs.find((o) => o.capabilities?.includes("chat"));
|
|
119
|
+
if (!org) {
|
|
120
|
+
org = orgs.find((o) => JSON.stringify(o.capabilities) !== '["api"]');
|
|
121
|
+
}
|
|
122
|
+
if (!org) {
|
|
123
|
+
org = orgs[0];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 2. Fetch usage
|
|
127
|
+
const usageRes = await fetch(`https://claude.ai/api/organizations/${org.uuid}/usage`, {
|
|
128
|
+
signal: options?.signal,
|
|
129
|
+
headers: {
|
|
130
|
+
Cookie: creds.cookieHeader,
|
|
131
|
+
Accept: "application/json",
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (usageRes.status === 401 || usageRes.status === 403) {
|
|
136
|
+
throw new InvalidCredentialsError("Session cookie invalid or expired");
|
|
137
|
+
}
|
|
138
|
+
if (!usageRes.ok) {
|
|
139
|
+
throw new Error(`Usage fetch failed: ${usageRes.status}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const data = (await usageRes.json()) as UsageResponse;
|
|
143
|
+
|
|
144
|
+
if (!data.five_hour || typeof data.five_hour.utilization !== "number") {
|
|
145
|
+
throw new Error("Missing five_hour.utilization");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const primary = toRateWindow(data.five_hour.utilization, data.five_hour.resets_at, 300);
|
|
149
|
+
const secondary = data.seven_day
|
|
150
|
+
? toRateWindow(data.seven_day.utilization, data.seven_day.resets_at, 10080)
|
|
151
|
+
: null;
|
|
152
|
+
|
|
153
|
+
const opusWindow = pickWindow(data, ["seven_day_sonnet", "seven_day_opus"]);
|
|
154
|
+
const opus = opusWindow ? toRateWindow(opusWindow.utilization, opusWindow.resets_at, 10080) : null;
|
|
155
|
+
|
|
156
|
+
const extraRateWindows: NamedRateWindowLegacy[] = [];
|
|
157
|
+
const designWindow = pickWindow(data, DESIGN_ALTERNATES);
|
|
158
|
+
if (designWindow && typeof designWindow.utilization === "number") {
|
|
159
|
+
extraRateWindows.push({
|
|
160
|
+
id: "claude-design",
|
|
161
|
+
title: "Designs",
|
|
162
|
+
window: toRateWindow(designWindow.utilization, designWindow.resets_at, 10080),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const routinesWindow = pickWindow(data, ROUTINES_ALTERNATES);
|
|
166
|
+
if (routinesWindow && typeof routinesWindow.utilization === "number") {
|
|
167
|
+
extraRateWindows.push({
|
|
168
|
+
id: "claude-routines",
|
|
169
|
+
title: "Daily Routines",
|
|
170
|
+
window: toRateWindow(routinesWindow.utilization, routinesWindow.resets_at, 10080),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 3. Best-effort overage
|
|
175
|
+
let providerCost: ProviderCostSnapshotLegacy | null = null;
|
|
176
|
+
try {
|
|
177
|
+
const overageRes = await fetch(`https://claude.ai/api/organizations/${org.uuid}/overage_spend_limit`, {
|
|
178
|
+
signal: options?.signal,
|
|
179
|
+
headers: {
|
|
180
|
+
Cookie: creds.cookieHeader,
|
|
181
|
+
Accept: "application/json",
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
if (overageRes.ok) {
|
|
185
|
+
const overage = (await overageRes.json()) as OverageResponse;
|
|
186
|
+
if (overage.is_enabled) {
|
|
187
|
+
providerCost = {
|
|
188
|
+
used: (overage.used_credits ?? 0) / 100,
|
|
189
|
+
limit: (overage.monthly_credit_limit ?? 0) / 100,
|
|
190
|
+
currencyCode: (overage.currency || "USD").trim() || "USD",
|
|
191
|
+
period: "Monthly",
|
|
192
|
+
resetsAt: null,
|
|
193
|
+
updatedAt: new Date(),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
// Ignore best-effort failure
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 4. Best-effort account info
|
|
202
|
+
let accountEmail: string | null = null;
|
|
203
|
+
let accountOrganization: string | null = null;
|
|
204
|
+
let loginMethod: string | null = null;
|
|
205
|
+
try {
|
|
206
|
+
const accountRes = await fetch("https://claude.ai/api/account", {
|
|
207
|
+
signal: options?.signal,
|
|
208
|
+
headers: {
|
|
209
|
+
Cookie: creds.cookieHeader,
|
|
210
|
+
Accept: "application/json",
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
if (accountRes.ok) {
|
|
214
|
+
const account = (await accountRes.json()) as AccountResponse;
|
|
215
|
+
if (account.email_address) {
|
|
216
|
+
accountEmail = account.email_address.trim();
|
|
217
|
+
}
|
|
218
|
+
const membership = account.memberships?.find((m) => m.organization?.uuid === org.uuid);
|
|
219
|
+
if (membership?.organization?.name) {
|
|
220
|
+
accountOrganization = membership.organization.name;
|
|
221
|
+
if (membership.organization.rate_limit_tier) {
|
|
222
|
+
loginMethod = membership.organization.rate_limit_tier;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
// Ignore best-effort failure
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
source: "web",
|
|
232
|
+
primary,
|
|
233
|
+
secondary,
|
|
234
|
+
opus,
|
|
235
|
+
extraRateWindows,
|
|
236
|
+
providerCost,
|
|
237
|
+
updatedAt: new Date(),
|
|
238
|
+
accountEmail,
|
|
239
|
+
accountOrganization,
|
|
240
|
+
loginMethod,
|
|
241
|
+
subscriptionType: null,
|
|
242
|
+
rateLimitTier: null,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import type { UsageClient } from "../client";
|
|
3
|
+
import type { ClaudeUsageSnapshot, RateWindowLegacy } from "../types";
|
|
4
|
+
|
|
5
|
+
export class TimeoutError extends Error {
|
|
6
|
+
constructor(message: string) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "TimeoutError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseResetDate(text: string): Date | null {
|
|
13
|
+
// Patterns:
|
|
14
|
+
// "Resets at 8:00PM PDT"
|
|
15
|
+
// "Resets at 8:00 PM PDT"
|
|
16
|
+
// "Resets at Apr 28 at 8:00PM PDT"
|
|
17
|
+
// "Resets at Apr 28 at 8:00 PM PDT"
|
|
18
|
+
const match = text.match(/Resets at (.+?)(?:\s*\(([^)]+)\))?$/i);
|
|
19
|
+
if (!match) return null;
|
|
20
|
+
const datePart = match[1]!.trim();
|
|
21
|
+
const tz = match[2] || undefined;
|
|
22
|
+
try {
|
|
23
|
+
const d = new Date(datePart);
|
|
24
|
+
if (!Number.isNaN(d.getTime())) return d;
|
|
25
|
+
// Fallback: try appending current year if no year present
|
|
26
|
+
const withYear = `${datePart} ${new Date().getFullYear()}`;
|
|
27
|
+
const d2 = new Date(withYear);
|
|
28
|
+
if (!Number.isNaN(d2.getTime())) return d2;
|
|
29
|
+
} catch {
|
|
30
|
+
// ignore
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parsePercent(line: string): number | null {
|
|
36
|
+
const match = line.match(/(\d{1,3}(?:\.\d+)?)\s*%/);
|
|
37
|
+
if (!match) return null;
|
|
38
|
+
const val = Number.parseFloat(match[1]!);
|
|
39
|
+
return Number.isNaN(val) ? null : val;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toRateWindow(usedPercent: number, resetText: string | null, windowMinutes: number): RateWindowLegacy {
|
|
43
|
+
const resetsAt = resetText ? parseResetDate(resetText) : null;
|
|
44
|
+
return {
|
|
45
|
+
usedPercent: Math.max(0, Math.min(100, usedPercent)),
|
|
46
|
+
windowMinutes,
|
|
47
|
+
resetsAt,
|
|
48
|
+
resetDescription: resetText,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function probeCli(binaryPath: string, timeoutMs: number): Promise<string> {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
let output = "";
|
|
55
|
+
let killed = false;
|
|
56
|
+
|
|
57
|
+
// Use `script` for PTY on macOS; fallback to direct spawn
|
|
58
|
+
const useScript = process.platform === "darwin";
|
|
59
|
+
const child = useScript
|
|
60
|
+
? spawn("script", ["-q", "/dev/null", binaryPath], {
|
|
61
|
+
env: process.env,
|
|
62
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
63
|
+
})
|
|
64
|
+
: spawn(binaryPath, [], {
|
|
65
|
+
env: process.env,
|
|
66
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const timer = setTimeout(() => {
|
|
70
|
+
killed = true;
|
|
71
|
+
child.kill("SIGTERM");
|
|
72
|
+
reject(new TimeoutError(`CLI probe timed out after ${timeoutMs}ms`));
|
|
73
|
+
}, timeoutMs);
|
|
74
|
+
|
|
75
|
+
child.stdout.on("data", (data: Buffer) => {
|
|
76
|
+
output += data.toString("utf8");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
child.stderr.on("data", (data: Buffer) => {
|
|
80
|
+
output += data.toString("utf8");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
child.on("error", (err) => {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
if (!killed) reject(err);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
child.on("close", () => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
if (!killed) resolve(output);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Send commands
|
|
94
|
+
child.stdin.write("/usage\n");
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
child.stdin.write("/status\n");
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
child.stdin.end();
|
|
99
|
+
}, 500);
|
|
100
|
+
}, 500);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class ClaudeCliProbeClient implements UsageClient<string, ClaudeUsageSnapshot> {
|
|
105
|
+
async fetch(binaryPath: string): Promise<ClaudeUsageSnapshot> {
|
|
106
|
+
let output: string;
|
|
107
|
+
try {
|
|
108
|
+
output = await probeCli(binaryPath, 10_000);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (err instanceof TimeoutError) {
|
|
111
|
+
// Retry with longer timeout
|
|
112
|
+
output = await probeCli(binaryPath, 24_000);
|
|
113
|
+
} else {
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const lines = output.split(/\r?\n/);
|
|
119
|
+
|
|
120
|
+
let primary: RateWindowLegacy | null = null;
|
|
121
|
+
let secondary: RateWindowLegacy | null = null;
|
|
122
|
+
let opus: RateWindowLegacy | null = null;
|
|
123
|
+
let accountEmail: string | null = null;
|
|
124
|
+
let accountOrganization: string | null = null;
|
|
125
|
+
let loginMethod: string | null = null;
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < lines.length; i++) {
|
|
128
|
+
const line = lines[i]!;
|
|
129
|
+
const lower = line.toLowerCase();
|
|
130
|
+
|
|
131
|
+
if (lower.includes("account_email")) {
|
|
132
|
+
const m = line.match(/account_email[:=]\s*(.+)/i);
|
|
133
|
+
if (m) accountEmail = m[1]!.trim();
|
|
134
|
+
}
|
|
135
|
+
if (lower.includes("account_org")) {
|
|
136
|
+
const m = line.match(/account_org[:=]\s*(.+)/i);
|
|
137
|
+
if (m) accountOrganization = m[1]!.trim();
|
|
138
|
+
}
|
|
139
|
+
if (lower.includes("login_method")) {
|
|
140
|
+
const m = line.match(/login_method[:=]\s*(.+)/i);
|
|
141
|
+
if (m) loginMethod = m[1]!.trim();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (lower.includes("current session")) {
|
|
145
|
+
const percent = parsePercent(line);
|
|
146
|
+
if (percent !== null) {
|
|
147
|
+
const used = Math.max(0, Math.min(100, 100 - percent));
|
|
148
|
+
const resetLine = lines[i + 1] || "";
|
|
149
|
+
primary = toRateWindow(used, resetLine.includes("Resets") ? resetLine.trim() : null, 300);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (lower.includes("current week")) {
|
|
154
|
+
const percent = parsePercent(line);
|
|
155
|
+
if (percent !== null) {
|
|
156
|
+
const used = Math.max(0, Math.min(100, 100 - percent));
|
|
157
|
+
const resetLine = lines[i + 1] || "";
|
|
158
|
+
const resetText = resetLine.includes("Resets") ? resetLine.trim() : null;
|
|
159
|
+
if (lower.includes("(all models)") || lower.includes("all models")) {
|
|
160
|
+
secondary = toRateWindow(used, resetText, 10080);
|
|
161
|
+
} else if (lower.includes("opus") || lower.includes("sonnet only")) {
|
|
162
|
+
opus = toRateWindow(used, resetText, 10080);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!primary) {
|
|
169
|
+
throw new Error("Could not parse primary usage window from CLI output");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
source: "cli",
|
|
174
|
+
primary,
|
|
175
|
+
secondary,
|
|
176
|
+
opus,
|
|
177
|
+
extraRateWindows: [],
|
|
178
|
+
providerCost: null,
|
|
179
|
+
updatedAt: new Date(),
|
|
180
|
+
accountEmail,
|
|
181
|
+
accountOrganization,
|
|
182
|
+
loginMethod,
|
|
183
|
+
subscriptionType: null,
|
|
184
|
+
rateLimitTier: null,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { UsageClient } from "../client";
|
|
2
|
+
import type { CodexOAuthCredentials, CodexUsageSnapshot, RateWindowLegacy } from "../types";
|
|
3
|
+
import { AuthenticationError, ScopeError } from "./anthropic-oauth-usage";
|
|
4
|
+
|
|
5
|
+
export { AuthenticationError, ScopeError };
|
|
6
|
+
|
|
7
|
+
export class ParseError extends Error {
|
|
8
|
+
constructor(message: string) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "ParseError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class CodexOAuthUsageClient implements UsageClient<CodexOAuthCredentials, CodexUsageSnapshot> {
|
|
15
|
+
async fetch(creds: CodexOAuthCredentials, options?: { signal?: AbortSignal }): Promise<CodexUsageSnapshot> {
|
|
16
|
+
const baseUrl = creds.chatgptBaseUrl ?? "https://chatgpt.com";
|
|
17
|
+
const url = `${baseUrl}/backend-api/wham/usage`;
|
|
18
|
+
|
|
19
|
+
const headers: Record<string, string> = {
|
|
20
|
+
Authorization: `Bearer ${creds.accessToken}`,
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
};
|
|
23
|
+
if (creds.accountId) {
|
|
24
|
+
headers["ChatGPT-Account-Id"] = creds.accountId;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const res = await fetch(url, {
|
|
28
|
+
method: "GET",
|
|
29
|
+
headers,
|
|
30
|
+
signal: options?.signal,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (res.status === 401) {
|
|
34
|
+
throw new AuthenticationError("Codex OAuth token expired or invalid");
|
|
35
|
+
}
|
|
36
|
+
if (res.status === 403) {
|
|
37
|
+
const body = await res.text();
|
|
38
|
+
if (/scope|permission|access denied/i.test(body)) {
|
|
39
|
+
throw new ScopeError("Codex OAuth access forbidden: missing scope");
|
|
40
|
+
}
|
|
41
|
+
throw new ScopeError("Codex OAuth access forbidden");
|
|
42
|
+
}
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
throw new Error(`Codex usage server error: ${res.status}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const body = (await res.json()) as Record<string, unknown>;
|
|
48
|
+
|
|
49
|
+
const planType = typeof body["plan_type"] === "string" ? body["plan_type"] : null;
|
|
50
|
+
|
|
51
|
+
// Handle both rate_limit and rate_limits key names
|
|
52
|
+
const rateLimitKey = typeof body["rate_limit"] === "object" && body["rate_limit"] !== null
|
|
53
|
+
? "rate_limit"
|
|
54
|
+
: typeof body["rate_limits"] === "object" && body["rate_limits"] !== null
|
|
55
|
+
? "rate_limits"
|
|
56
|
+
: null;
|
|
57
|
+
const rateLimit = rateLimitKey ? (body[rateLimitKey] as Record<string, unknown> | null) : null;
|
|
58
|
+
|
|
59
|
+
if (!rateLimit) {
|
|
60
|
+
throw new ParseError("Codex usage response missing rate_limit or rate_limits");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const primaryWindow = parseWindow(rateLimit?.["primary_window"] as Record<string, unknown> | undefined);
|
|
64
|
+
const secondaryWindow = parseWindow(rateLimit?.["secondary_window"] as Record<string, unknown> | undefined);
|
|
65
|
+
|
|
66
|
+
let credits: CodexUsageSnapshot["credits"] = null;
|
|
67
|
+
if (typeof body["credits"] === "object" && body["credits"] !== null) {
|
|
68
|
+
const c = body["credits"] as Record<string, unknown>;
|
|
69
|
+
credits = {
|
|
70
|
+
hasCredits: c["has_credits"] === true,
|
|
71
|
+
unlimited: c["unlimited"] === true,
|
|
72
|
+
balance: typeof c["balance"] === "number" ? c["balance"] : null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
source: "oauth",
|
|
78
|
+
planType,
|
|
79
|
+
primaryWindow,
|
|
80
|
+
secondaryWindow,
|
|
81
|
+
credits,
|
|
82
|
+
updatedAt: new Date(),
|
|
83
|
+
accountEmail: creds.accountEmail ?? null,
|
|
84
|
+
accountId: creds.accountId ?? null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseWindow(raw: Record<string, unknown> | undefined): RateWindowLegacy | null {
|
|
90
|
+
if (!raw) return null;
|
|
91
|
+
const usedPercent = typeof raw["used_percent"] === "number" ? raw["used_percent"] : 0;
|
|
92
|
+
const limitWindowSeconds = typeof raw["limit_window_seconds"] === "number" ? raw["limit_window_seconds"] : undefined;
|
|
93
|
+
const resetAtRaw = typeof raw["reset_at"] === "number" ? raw["reset_at"] : typeof raw["resets_at"] === "number" ? raw["resets_at"] : undefined;
|
|
94
|
+
const resetsAt = resetAtRaw ? new Date(resetAtRaw * 1000) : null;
|
|
95
|
+
const windowMinutes = limitWindowSeconds ? Math.round(limitWindowSeconds / 60) : 300;
|
|
96
|
+
const resetDescription = resetsAt ? formatDuration(Math.max(0, resetsAt.getTime() - Date.now())) : null;
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
usedPercent,
|
|
100
|
+
windowMinutes,
|
|
101
|
+
resetsAt,
|
|
102
|
+
resetDescription,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatDuration(ms: number): string {
|
|
107
|
+
const seconds = Math.floor(ms / 1000);
|
|
108
|
+
const minutes = Math.floor(seconds / 60);
|
|
109
|
+
const hours = Math.floor(minutes / 60);
|
|
110
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
111
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
|
112
|
+
return `${seconds}s`;
|
|
113
|
+
}
|