@getpaseo/server 0.11.0-beta.2 → 0.11.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/builtin-plugins/claude-usage-source/index.server.ts +1 -2
- package/dist/server/builtin-plugins/claude-usage-source/server/stores.ts +0 -1
- package/dist/server/builtin-plugins/claude-usage-source/server/usage.ts +72 -36
- package/dist/server/builtin-plugins/codex-usage-source/index.server.ts +1 -2
- package/dist/server/builtin-plugins/codex-usage-source/server/stores.ts +0 -1
- package/dist/server/builtin-plugins/codex-usage-source/server/usage.ts +22 -12
- package/dist/server/builtin-plugins/copilot-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/copilot-usage-source/server/usage.ts +28 -39
- package/dist/server/builtin-plugins/copilot-usage-source/shared/input.ts +6 -2
- package/dist/server/builtin-plugins/cursor-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/cursor-usage-source/server/usage.ts +53 -54
- package/dist/server/builtin-plugins/cursor-usage-source/shared/input.ts +6 -2
- package/dist/server/builtin-plugins/grok-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/grok-usage-source/server/usage.ts +23 -27
- package/dist/server/builtin-plugins/grok-usage-source/shared/input.ts +6 -2
- package/dist/server/builtin-plugins/kimi-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/kimi-usage-source/server/usage.ts +44 -67
- package/dist/server/builtin-plugins/kimi-usage-source/shared/input.ts +6 -2
- package/dist/server/builtin-plugins/minimax-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/minimax-usage-source/server/usage.ts +64 -100
- package/dist/server/builtin-plugins/minimax-usage-source/shared/input.ts +6 -2
- package/dist/server/builtin-plugins/muse-provider/server/usage.ts +9 -9
- package/dist/server/builtin-plugins/opencode-go-usage-source/index.server.ts +1 -2
- package/dist/server/builtin-plugins/opencode-go-usage-source/server/usage.ts +8 -12
- package/dist/server/builtin-plugins/opencode-go-usage-source/shared/input.ts +1 -2
- package/dist/server/builtin-plugins/package.json +1 -1
- package/dist/server/builtin-plugins/zai-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/zai-usage-source/server/usage.ts +13 -11
- package/dist/server/builtin-plugins/zai-usage-source/shared/input.ts +6 -2
- package/dist/server/server/plugins/index.d.ts +18 -3
- package/dist/server/server/plugins/index.js +1 -2
- package/dist/server/server/plugins/plugin-process-protocol.d.ts +0 -5
- package/dist/server/server/plugins/plugin-process-protocol.js +0 -8
- package/dist/server/server/plugins/plugin-process.js +2 -7
- package/dist/server/server/plugins/runtime.d.ts +0 -1
- package/dist/server/server/plugins/runtime.js +0 -11
- package/dist/server/server/plugins/test-fixtures/usage-source/index.server.js +11 -2
- package/dist/server/server/plugins/test-fixtures/usage-source-directory/index.server.js +11 -2
- package/dist/server/server/plugins/usage-sources/index.d.ts +6 -8
- package/dist/server/server/plugins/usage-sources/index.js +79 -84
- package/dist/server/web-ui/_expo/static/js/web/{index-a43718759ebeb27df202e8b9fc2fe58a.js → index-1a3199cb049d45e47bfd4c216ce98838.js} +8 -8
- package/dist/server/web-ui/_expo/static/js/web/index-1a3199cb049d45e47bfd4c216ce98838.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-a43718759ebeb27df202e8b9fc2fe58a.js.gz → index-1a3199cb049d45e47bfd4c216ce98838.js.gz} +0 -0
- package/dist/server/web-ui/index.html +1 -1
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/package.json +6 -6
- package/dist/server/web-ui/_expo/static/js/web/index-a43718759ebeb27df202e8b9fc2fe58a.js.br +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
2
|
import { inputSchema } from "./shared/input.js";
|
|
3
|
-
import { discover, fetchUsage
|
|
3
|
+
import { discover, fetchUsage } from "./server/usage.js";
|
|
4
4
|
|
|
5
5
|
export default function contribute(server: PluginServerContext) {
|
|
6
6
|
server.registerUsageSource({
|
|
@@ -9,7 +9,6 @@ export default function contribute(server: PluginServerContext) {
|
|
|
9
9
|
icon: "icon.svg",
|
|
10
10
|
input: inputSchema,
|
|
11
11
|
discover: () => discover(),
|
|
12
|
-
identify,
|
|
13
12
|
fetch: fetchUsage,
|
|
14
13
|
});
|
|
15
14
|
return () => {};
|
|
@@ -97,7 +97,6 @@ function ompRows(path: string, lookup: StoreLookup) {
|
|
|
97
97
|
if (!row.success) return [];
|
|
98
98
|
try {
|
|
99
99
|
const oauth = oauthSchema.parse(JSON.parse(row.data.data));
|
|
100
|
-
if (oauth.expires === undefined || oauth.expires <= (lookup.now ?? Date.now)()) return [];
|
|
101
100
|
return [{ id: row.data.id, oauth }];
|
|
102
101
|
} catch {
|
|
103
102
|
return [];
|
|
@@ -9,7 +9,8 @@ import { z } from "zod";
|
|
|
9
9
|
import {
|
|
10
10
|
hashAccountKey,
|
|
11
11
|
toneFromUsedPct,
|
|
12
|
-
|
|
12
|
+
unavailable,
|
|
13
|
+
type UsageAccount,
|
|
13
14
|
windowFromUsedPct,
|
|
14
15
|
type UsageReport,
|
|
15
16
|
type UsageWindow,
|
|
@@ -27,6 +28,7 @@ const ClaudeCredentialsSchema = z.object({
|
|
|
27
28
|
claudeAiOauth: z
|
|
28
29
|
.object({
|
|
29
30
|
accessToken: z.string().optional(),
|
|
31
|
+
expiresAt: z.number().optional(),
|
|
30
32
|
refreshToken: z.string().optional(),
|
|
31
33
|
subscriptionType: z.string().optional(),
|
|
32
34
|
rateLimitTier: z.string().optional(),
|
|
@@ -356,20 +358,41 @@ async function keychainCredentialRecord(
|
|
|
356
358
|
return parsed.success ? toCredentialRecord(parsed.data) : null;
|
|
357
359
|
}
|
|
358
360
|
|
|
359
|
-
export async function discover(
|
|
361
|
+
export async function discover(
|
|
362
|
+
lookup: ClaudeCredentialLookup = {},
|
|
363
|
+
fetchApi: typeof fetch = fetch,
|
|
364
|
+
): Promise<UsageAccount[]> {
|
|
365
|
+
// Claude Code only reads the file when its Keychain login is empty.
|
|
366
|
+
const primary: UsageInput = (await keychainCredentialRecord(lookup))
|
|
367
|
+
? { route: { store: "keychain" } }
|
|
368
|
+
: { route: { store: "claude", path: claudeCredentialPath(lookup) } };
|
|
360
369
|
const candidates: UsageInput[] = [
|
|
361
|
-
|
|
362
|
-
];
|
|
363
|
-
if ((lookup.platform ?? process.platform) === "darwin")
|
|
364
|
-
candidates.push({ route: { store: "keychain" } });
|
|
365
|
-
candidates.push(
|
|
370
|
+
primary,
|
|
366
371
|
{ route: { store: "pi", path: piAuthPath(lookup) } },
|
|
367
372
|
...discoverOmp(lookup).map((route) => ({ route })),
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
for (const input of candidates)
|
|
371
|
-
|
|
372
|
-
|
|
373
|
+
];
|
|
374
|
+
const accounts: UsageAccount[] = [];
|
|
375
|
+
for (const input of candidates) {
|
|
376
|
+
const credentials = await resolveClaudeCredentials(input, lookup);
|
|
377
|
+
if (!credentials) continue;
|
|
378
|
+
const fallback = { key: hashAccountKey(JSON.stringify(input.route)), input };
|
|
379
|
+
if (credentials.expires !== undefined && credentials.expires <= (lookup.now ?? Date.now)()) {
|
|
380
|
+
accounts.push(fallback);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
const profile = await readProfile(
|
|
385
|
+
credentials.oauth.accessToken,
|
|
386
|
+
fetchApi,
|
|
387
|
+
lookup.now ?? Date.now,
|
|
388
|
+
);
|
|
389
|
+
accounts.push("status" in profile ? fallback : { ...profile, input });
|
|
390
|
+
} catch {
|
|
391
|
+
// Keep the login visible; fetching usage reports the vendor failure.
|
|
392
|
+
accounts.push(fallback);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return accounts;
|
|
373
396
|
}
|
|
374
397
|
|
|
375
398
|
/** Re-read the selected login; the harness owns token refresh. */
|
|
@@ -397,7 +420,9 @@ async function readCredentialFile(path: string): Promise<ClaudeCredentialRecord
|
|
|
397
420
|
|
|
398
421
|
function toCredentialRecord(credentials: ClaudeCredentials): ClaudeCredentialRecord | null {
|
|
399
422
|
const oauth = credentials.claudeAiOauth;
|
|
400
|
-
return oauth?.accessToken
|
|
423
|
+
return oauth?.accessToken
|
|
424
|
+
? { oauth: { ...oauth, accessToken: oauth.accessToken }, expires: oauth.expiresAt }
|
|
425
|
+
: null;
|
|
401
426
|
}
|
|
402
427
|
|
|
403
428
|
export async function fetchUsage(
|
|
@@ -433,7 +458,7 @@ export async function fetchUsage(
|
|
|
433
458
|
return parsed;
|
|
434
459
|
}
|
|
435
460
|
|
|
436
|
-
async function callClaudeApi(token: string): Promise<ClaudeUsageResponse |
|
|
461
|
+
async function callClaudeApi(token: string): Promise<ClaudeUsageResponse | number> {
|
|
437
462
|
const res = await fetchApi("https://api.anthropic.com/api/oauth/usage", {
|
|
438
463
|
signal: AbortSignal.timeout(15_000),
|
|
439
464
|
headers: {
|
|
@@ -442,28 +467,38 @@ export async function fetchUsage(
|
|
|
442
467
|
"anthropic-beta": CLAUDE_OAUTH_BETA,
|
|
443
468
|
},
|
|
444
469
|
});
|
|
445
|
-
if (res.status === 401 || res.status === 403) return
|
|
470
|
+
if (res.status === 401 || res.status === 403) return res.status;
|
|
446
471
|
if (!res.ok) throw new Error(`Claude usage API returned ${res.status}`);
|
|
447
472
|
return ClaudeUsageResponseSchema.parse(await res.json());
|
|
448
473
|
}
|
|
449
474
|
|
|
450
475
|
const credentials = await resolveClaudeCredentials(input, credentialLookup);
|
|
476
|
+
if (!credentials) throw new Error("Claude login store no longer exists");
|
|
477
|
+
const refreshedBy =
|
|
478
|
+
input.route.store === "claude" || input.route.store === "keychain"
|
|
479
|
+
? "claude"
|
|
480
|
+
: input.route.store;
|
|
451
481
|
if (
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
482
|
+
credentials.expires !== undefined &&
|
|
483
|
+
credentials.expires <= (credentialLookup.now ?? Date.now)()
|
|
484
|
+
)
|
|
485
|
+
return unavailable({
|
|
486
|
+
kind: "expired",
|
|
487
|
+
expiresAt: new Date(credentials.expires).toISOString(),
|
|
488
|
+
refreshedBy,
|
|
489
|
+
});
|
|
490
|
+
const profile = await cachedProfile(
|
|
491
|
+
credentials.oauth.accessToken,
|
|
492
|
+
(credentialLookup.now ?? Date.now)(),
|
|
493
|
+
);
|
|
494
|
+
if (profile && "status" in profile)
|
|
495
|
+
return unavailable({ kind: "rejected", status: profile.status, refreshedBy });
|
|
458
496
|
|
|
459
497
|
const { oauth } = credentials;
|
|
460
498
|
const plan = buildClaudePlan(oauth.subscriptionType, oauth.rateLimitTier);
|
|
461
499
|
const resp = await callClaudeApi(oauth.accessToken);
|
|
462
500
|
|
|
463
|
-
if (resp === "
|
|
464
|
-
// Read-only on credentials; the Claude CLI owns refresh. See docs/providers.md.
|
|
465
|
-
return unavailableUsage();
|
|
466
|
-
}
|
|
501
|
+
if (typeof resp === "number") return unavailable({ kind: "rejected", status: resp, refreshedBy });
|
|
467
502
|
|
|
468
503
|
const scoped = reconcileScopedLimits(
|
|
469
504
|
legacyScopedLimits(resp),
|
|
@@ -498,26 +533,27 @@ export async function fetchUsage(
|
|
|
498
533
|
}
|
|
499
534
|
|
|
500
535
|
// The OAuth usage endpoint meters the active organization selected by the token.
|
|
501
|
-
type ClaudeIdentity = { key: string; label?: string } |
|
|
536
|
+
type ClaudeIdentity = { key: string; label?: string } | { status: number };
|
|
502
537
|
const PROFILE_TTL_MS = 300_000;
|
|
503
538
|
const PROFILE_CACHE_LIMIT = 128;
|
|
504
539
|
const profileCache = new Map<string, { at: number; result: Promise<ClaudeIdentity> }>();
|
|
505
540
|
|
|
541
|
+
function cachedProfile(token: string, now: number): Promise<ClaudeIdentity> | undefined {
|
|
542
|
+
pruneProfileCache(now);
|
|
543
|
+
return profileCache.get(hashAccountKey(token))?.result;
|
|
544
|
+
}
|
|
545
|
+
|
|
506
546
|
function pruneProfileCache(now: number): void {
|
|
507
547
|
for (const [key, entry] of profileCache) {
|
|
508
548
|
if (now - entry.at >= PROFILE_TTL_MS) profileCache.delete(key);
|
|
509
549
|
}
|
|
510
550
|
}
|
|
511
551
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
fetchApi: typeof fetch
|
|
515
|
-
now: () => number
|
|
516
|
-
|
|
517
|
-
) {
|
|
518
|
-
const credentials = await resolveClaudeCredentials(input, credentialLookup);
|
|
519
|
-
if (!credentials) return null;
|
|
520
|
-
const token = credentials.oauth.accessToken;
|
|
552
|
+
async function readProfile(
|
|
553
|
+
token: string,
|
|
554
|
+
fetchApi: typeof fetch,
|
|
555
|
+
now: () => number,
|
|
556
|
+
): Promise<ClaudeIdentity> {
|
|
521
557
|
const tokenHash = hashAccountKey(token);
|
|
522
558
|
pruneProfileCache(now());
|
|
523
559
|
const pending = profileCache.get(tokenHash);
|
|
@@ -536,7 +572,7 @@ export async function identify(
|
|
|
536
572
|
"anthropic-beta": CLAUDE_OAUTH_BETA,
|
|
537
573
|
},
|
|
538
574
|
});
|
|
539
|
-
if (response.status === 401 || response.status === 403) return
|
|
575
|
+
if (response.status === 401 || response.status === 403) return { status: response.status };
|
|
540
576
|
if (!response.ok) throw new Error(`Claude profile API returned ${response.status}`);
|
|
541
577
|
const profile = z
|
|
542
578
|
.object({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
2
|
import { inputSchema } from "./shared/input.js";
|
|
3
|
-
import { discover, fetchUsage
|
|
3
|
+
import { discover, fetchUsage } from "./server/usage.js";
|
|
4
4
|
|
|
5
5
|
export default function contribute(server: PluginServerContext) {
|
|
6
6
|
server.registerUsageSource({
|
|
@@ -9,7 +9,6 @@ export default function contribute(server: PluginServerContext) {
|
|
|
9
9
|
icon: "icon.svg",
|
|
10
10
|
input: inputSchema,
|
|
11
11
|
discover: () => discover(),
|
|
12
|
-
identify,
|
|
13
12
|
fetch: fetchUsage,
|
|
14
13
|
});
|
|
15
14
|
return () => {};
|
|
@@ -97,7 +97,6 @@ function ompRows(path: string, lookup: StoreLookup) {
|
|
|
97
97
|
if (!row.success) return [];
|
|
98
98
|
try {
|
|
99
99
|
const oauth = oauthSchema.parse(JSON.parse(row.data.data));
|
|
100
|
-
if (oauth.expires === undefined || oauth.expires <= (lookup.now ?? Date.now)()) return [];
|
|
101
100
|
return [{ id: row.data.id, oauth }];
|
|
102
101
|
} catch {
|
|
103
102
|
return [];
|
|
@@ -3,6 +3,9 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import {
|
|
5
5
|
balanceToneFromRemaining,
|
|
6
|
+
hashAccountKey,
|
|
7
|
+
unavailable,
|
|
8
|
+
type UsageAccount,
|
|
6
9
|
toneFromUsedPct,
|
|
7
10
|
windowFromUsedPct,
|
|
8
11
|
type UsageReport,
|
|
@@ -39,7 +42,7 @@ interface Auth {
|
|
|
39
42
|
expires?: number;
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
export async function discover(lookup: StoreLookup = {}): Promise<
|
|
45
|
+
export async function discover(lookup: StoreLookup = {}): Promise<UsageAccount[]> {
|
|
43
46
|
const env = lookup.env ?? process.env;
|
|
44
47
|
const home = lookup.home ?? homedir();
|
|
45
48
|
const paths = [
|
|
@@ -59,8 +62,11 @@ export async function discover(lookup: StoreLookup = {}): Promise<CodexUsageInpu
|
|
|
59
62
|
{ route: { store: "pi", path: piAuthPath(lookup) } },
|
|
60
63
|
...discoverOmp(lookup).map((route) => ({ route })),
|
|
61
64
|
);
|
|
62
|
-
const present:
|
|
63
|
-
for (const input of candidates)
|
|
65
|
+
const present: UsageAccount[] = [];
|
|
66
|
+
for (const input of candidates) {
|
|
67
|
+
const auth = await readAuth(input, lookup);
|
|
68
|
+
if (auth) present.push({ ...accountIdentity(auth, input), input });
|
|
69
|
+
}
|
|
64
70
|
return present;
|
|
65
71
|
}
|
|
66
72
|
|
|
@@ -104,8 +110,14 @@ export async function fetchUsage(
|
|
|
104
110
|
lookup: StoreLookup = {},
|
|
105
111
|
): Promise<UsageReport> {
|
|
106
112
|
const auth = await readAuth(input, lookup);
|
|
107
|
-
if (!auth
|
|
108
|
-
|
|
113
|
+
if (!auth) throw new Error("Codex login store no longer exists");
|
|
114
|
+
const refreshedBy = input.route.store;
|
|
115
|
+
if (auth.expires !== undefined && auth.expires <= (lookup.now ?? Date.now)())
|
|
116
|
+
return unavailable({
|
|
117
|
+
kind: "expired",
|
|
118
|
+
expiresAt: new Date(auth.expires).toISOString(),
|
|
119
|
+
refreshedBy,
|
|
120
|
+
});
|
|
109
121
|
const headers: Record<string, string> = {
|
|
110
122
|
Authorization: `Bearer ${auth.token}`,
|
|
111
123
|
Accept: "application/json",
|
|
@@ -117,10 +129,10 @@ export async function fetchUsage(
|
|
|
117
129
|
signal: AbortSignal.timeout(15_000),
|
|
118
130
|
});
|
|
119
131
|
if (response.status === 401 || response.status === 403)
|
|
120
|
-
return {
|
|
132
|
+
return unavailable({ kind: "rejected", status: response.status, refreshedBy });
|
|
121
133
|
if (!response.ok) throw new Error(`Codex usage API returned ${response.status}`);
|
|
122
134
|
const text = await response.text();
|
|
123
|
-
if (text.trim().startsWith("<"))
|
|
135
|
+
if (text.trim().startsWith("<")) throw new Error("Codex usage API returned HTML");
|
|
124
136
|
const usage = responseSchema.parse(JSON.parse(text));
|
|
125
137
|
const windows = [
|
|
126
138
|
usageWindow(
|
|
@@ -181,9 +193,7 @@ function claimString(value: unknown): string | null {
|
|
|
181
193
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
182
194
|
}
|
|
183
195
|
|
|
184
|
-
|
|
185
|
-
const auth = await readAuth(input, lookup);
|
|
186
|
-
if (!auth) return null;
|
|
196
|
+
function accountIdentity(auth: Auth, input: CodexUsageInput): { key: string; label?: string } {
|
|
187
197
|
const access = jwtClaims(auth.token);
|
|
188
198
|
const id = jwtClaims(auth.idToken);
|
|
189
199
|
const accessAuth = claimObject(access, "https://api.openai.com/auth");
|
|
@@ -193,11 +203,11 @@ export async function identify(input: CodexUsageInput, lookup: StoreLookup = {})
|
|
|
193
203
|
claimString(accessAuth?.["chatgpt_account_id"]) ??
|
|
194
204
|
claimString(access?.["chatgpt_account_id"]) ??
|
|
195
205
|
claimString(idAuth?.["chatgpt_account_id"]);
|
|
196
|
-
|
|
206
|
+
|
|
197
207
|
const label =
|
|
198
208
|
claimString(claimObject(access, "https://api.openai.com/profile")?.["email"]) ??
|
|
199
209
|
claimString(access?.["email"]) ??
|
|
200
210
|
claimString(claimObject(id, "https://api.openai.com/profile")?.["email"]) ??
|
|
201
211
|
claimString(id?.["email"]);
|
|
202
|
-
return { key, ...(label ? { label } : {}) };
|
|
212
|
+
return { key: key ?? hashAccountKey(JSON.stringify(input.route)), ...(label ? { label } : {}) };
|
|
203
213
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
2
|
import { inputSchema } from "./shared/input.js";
|
|
3
|
-
import { fetchUsage,
|
|
3
|
+
import { fetchUsage, discover } from "./server/usage.js";
|
|
4
4
|
|
|
5
5
|
export default function contribute(server: PluginServerContext) {
|
|
6
6
|
server.registerUsageSource({
|
|
@@ -8,8 +8,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
8
8
|
label: "GitHub Copilot",
|
|
9
9
|
icon: "icon.svg",
|
|
10
10
|
input: inputSchema,
|
|
11
|
-
discover:
|
|
12
|
-
identify,
|
|
11
|
+
discover: () => discover(),
|
|
13
12
|
fetch: fetchUsage,
|
|
14
13
|
});
|
|
15
14
|
return () => {};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { UsageInput } from "../shared/input.js";
|
|
2
|
-
import {
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
unavailable,
|
|
8
|
+
type UsageAccount,
|
|
8
9
|
type UsageReport,
|
|
9
10
|
type UsageDetail,
|
|
10
11
|
} from "@getpaseo/plugin/server/usage";
|
|
@@ -19,39 +20,36 @@ const CopilotUsageResponseSchema = z.object({
|
|
|
19
20
|
quota_reset_date: ApiOptionalStringSchema,
|
|
20
21
|
});
|
|
21
22
|
|
|
22
|
-
async function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
async function readToken(input: UsageInput): Promise<string | undefined> {
|
|
24
|
+
if (input.store === "env") return process.env[input.locator];
|
|
25
|
+
try {
|
|
26
|
+
const raw = await fs.readFile(input.locator, "utf8");
|
|
27
|
+
return raw.match(/oauth_token:\s*["']?([a-zA-Z0-9_-]+)["']?/)?.[1];
|
|
28
|
+
} catch {
|
|
29
|
+
return undefined;
|
|
26
30
|
}
|
|
27
|
-
|
|
31
|
+
}
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return
|
|
33
|
+
export async function discover(): Promise<UsageAccount[]> {
|
|
34
|
+
const candidates: UsageInput[] = ["COPILOT_TOKEN", "GITHUB_TOKEN", "GITHUB_PAT"].map(
|
|
35
|
+
(locator) => ({ store: "env", locator }),
|
|
36
|
+
);
|
|
37
|
+
if (process.env.APPDATA)
|
|
38
|
+
candidates.push({
|
|
39
|
+
store: "file",
|
|
40
|
+
locator: join(process.env.APPDATA, "GitHub CLI", "hosts.yml"),
|
|
41
|
+
});
|
|
42
|
+
candidates.push({ store: "file", locator: join(homedir(), ".config", "gh", "hosts.yml") });
|
|
43
|
+
for (const input of candidates) if (await readToken(input)) return [{ key: "default", input }];
|
|
44
|
+
return [];
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
export async function fetchUsage(
|
|
43
48
|
input: UsageInput,
|
|
44
49
|
fetchApi: typeof fetch = fetch,
|
|
45
50
|
): Promise<UsageReport> {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const token =
|
|
49
|
-
process.env["COPILOT_TOKEN"] ||
|
|
50
|
-
process.env["GITHUB_TOKEN"] ||
|
|
51
|
-
process.env["GITHUB_PAT"] ||
|
|
52
|
-
(await readGithubCliToken());
|
|
53
|
-
|
|
54
|
-
if (!token) return unavailableUsage();
|
|
51
|
+
const token = await readToken(input);
|
|
52
|
+
if (!token) throw new Error("Copilot login store no longer exists");
|
|
55
53
|
|
|
56
54
|
const res = await fetchApi("https://api.github.com/copilot_internal/user", {
|
|
57
55
|
signal: AbortSignal.timeout(15_000),
|
|
@@ -65,9 +63,9 @@ export async function fetchUsage(
|
|
|
65
63
|
},
|
|
66
64
|
});
|
|
67
65
|
|
|
68
|
-
if (
|
|
69
|
-
return
|
|
70
|
-
}
|
|
66
|
+
if (res.status === 401 || res.status === 403)
|
|
67
|
+
return unavailable({ kind: "rejected", status: res.status });
|
|
68
|
+
if (!res.ok) throw new Error(`Copilot usage API returned ${res.status}`);
|
|
71
69
|
|
|
72
70
|
const resp = CopilotUsageResponseSchema.parse(await res.json());
|
|
73
71
|
const details: UsageDetail[] = resp.quota_reset_date
|
|
@@ -82,12 +80,3 @@ export async function fetchUsage(
|
|
|
82
80
|
details,
|
|
83
81
|
};
|
|
84
82
|
}
|
|
85
|
-
|
|
86
|
-
export async function identify() {
|
|
87
|
-
const token =
|
|
88
|
-
process.env["COPILOT_TOKEN"] ||
|
|
89
|
-
process.env["GITHUB_TOKEN"] ||
|
|
90
|
-
process.env["GITHUB_PAT"] ||
|
|
91
|
-
(await readGithubCliToken());
|
|
92
|
-
return token ? { key: "default" } : null;
|
|
93
|
-
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
2
|
import { inputSchema } from "./shared/input.js";
|
|
3
|
-
import { fetchUsage,
|
|
3
|
+
import { fetchUsage, discover } from "./server/usage.js";
|
|
4
4
|
|
|
5
5
|
export default function contribute(server: PluginServerContext) {
|
|
6
6
|
server.registerUsageSource({
|
|
@@ -8,8 +8,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
8
8
|
label: "Cursor",
|
|
9
9
|
icon: "icon.svg",
|
|
10
10
|
input: inputSchema,
|
|
11
|
-
discover:
|
|
12
|
-
identify,
|
|
11
|
+
discover: () => discover(),
|
|
13
12
|
fetch: fetchUsage,
|
|
14
13
|
});
|
|
15
14
|
return () => {};
|
|
@@ -7,7 +7,8 @@ import { z } from "zod";
|
|
|
7
7
|
import {
|
|
8
8
|
toneFromUsedPct,
|
|
9
9
|
usedPctOf,
|
|
10
|
-
|
|
10
|
+
unavailable,
|
|
11
|
+
type UsageAccount,
|
|
11
12
|
type UsageReport,
|
|
12
13
|
type UsageBalance,
|
|
13
14
|
} from "@getpaseo/plugin/server/usage";
|
|
@@ -114,24 +115,7 @@ function cursorTokenFromDb(db: CursorStateDatabase): string | null {
|
|
|
114
115
|
return null;
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
async function readCursorTokenFromSqlite(
|
|
118
|
-
const dbPaths: string[] = [];
|
|
119
|
-
if (process.env["APPDATA"]) {
|
|
120
|
-
dbPaths.push(join(process.env["APPDATA"], "Cursor", "User", "globalStorage", "state.vscdb"));
|
|
121
|
-
}
|
|
122
|
-
dbPaths.push(
|
|
123
|
-
join(
|
|
124
|
-
homeDir,
|
|
125
|
-
"Library",
|
|
126
|
-
"Application Support",
|
|
127
|
-
"Cursor",
|
|
128
|
-
"User",
|
|
129
|
-
"globalStorage",
|
|
130
|
-
"state.vscdb",
|
|
131
|
-
),
|
|
132
|
-
);
|
|
133
|
-
dbPaths.push(join(homeDir, ".config", "Cursor", "User", "globalStorage", "state.vscdb"));
|
|
134
|
-
|
|
118
|
+
async function readCursorTokenFromSqlite(path: string): Promise<string | null> {
|
|
135
119
|
// Held in a variable so TypeScript skips module resolution: @types/node@20 has no
|
|
136
120
|
// node:sqlite typings yet, while the runtime (Node 22+ / Electron) provides it.
|
|
137
121
|
const sqliteSpecifier: string = "node:sqlite";
|
|
@@ -142,24 +126,19 @@ async function readCursorTokenFromSqlite(homeDir: string): Promise<string | null
|
|
|
142
126
|
return null; // runtime without node:sqlite
|
|
143
127
|
}
|
|
144
128
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
} finally {
|
|
155
|
-
db?.close();
|
|
156
|
-
}
|
|
129
|
+
if (!existsSync(path)) return null;
|
|
130
|
+
let db: CursorStateDatabase | undefined;
|
|
131
|
+
try {
|
|
132
|
+
db = new sqlite.DatabaseSync(path, { readOnly: true });
|
|
133
|
+
return cursorTokenFromDb(db);
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
} finally {
|
|
137
|
+
db?.close();
|
|
157
138
|
}
|
|
158
|
-
return null;
|
|
159
139
|
}
|
|
160
140
|
|
|
161
|
-
async function readCursorTokenFromAuthJson(
|
|
162
|
-
const path = join(homeDir, ".config", "cursor", "auth.json");
|
|
141
|
+
async function readCursorTokenFromAuthJson(path: string): Promise<string | null> {
|
|
163
142
|
if (!existsSync(path)) return null;
|
|
164
143
|
try {
|
|
165
144
|
const parsed = CursorAuthStatusSchema.parse(JSON.parse(await readFile(path, "utf8")));
|
|
@@ -173,16 +152,8 @@ export async function fetchUsage(
|
|
|
173
152
|
input: UsageInput,
|
|
174
153
|
fetchApi: typeof fetch = fetch,
|
|
175
154
|
): Promise<UsageReport> {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const token =
|
|
180
|
-
process.env["CURSOR_ACCESS_TOKEN"] ||
|
|
181
|
-
process.env["CURSOR_TOKEN"] ||
|
|
182
|
-
(await readCursorTokenFromSqlite(homeDir)) ||
|
|
183
|
-
(await readCursorTokenFromAuthJson(homeDir));
|
|
184
|
-
|
|
185
|
-
if (!token) return unavailableUsage();
|
|
155
|
+
const token = await readToken(input);
|
|
156
|
+
if (!token) throw new Error("Cursor login store no longer exists");
|
|
186
157
|
|
|
187
158
|
const res = await fetchApi(
|
|
188
159
|
"https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage",
|
|
@@ -198,9 +169,9 @@ export async function fetchUsage(
|
|
|
198
169
|
},
|
|
199
170
|
);
|
|
200
171
|
|
|
201
|
-
if (
|
|
202
|
-
return
|
|
203
|
-
}
|
|
172
|
+
if (res.status === 401 || res.status === 403)
|
|
173
|
+
return unavailable({ kind: "rejected", status: res.status });
|
|
174
|
+
if (!res.ok) throw new Error(`Cursor usage API returned ${res.status}`);
|
|
204
175
|
|
|
205
176
|
const resp = CursorUsageResponseSchema.parse(await res.json());
|
|
206
177
|
const billingCycleEnd = parseCursorBillingCycleTimestamp(resp.billingCycleEnd);
|
|
@@ -230,12 +201,40 @@ export async function fetchUsage(
|
|
|
230
201
|
};
|
|
231
202
|
}
|
|
232
203
|
|
|
233
|
-
|
|
234
|
-
|
|
204
|
+
async function readToken(input: UsageInput): Promise<string | undefined> {
|
|
205
|
+
if (input.store === "env") return process.env[input.locator];
|
|
235
206
|
const token =
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
207
|
+
input.store === "sqlite"
|
|
208
|
+
? await readCursorTokenFromSqlite(input.locator)
|
|
209
|
+
: await readCursorTokenFromAuthJson(input.locator);
|
|
210
|
+
return token ?? undefined;
|
|
211
|
+
}
|
|
212
|
+
export async function discover(): Promise<UsageAccount[]> {
|
|
213
|
+
const home = homedir();
|
|
214
|
+
const candidates: UsageInput[] = ["CURSOR_ACCESS_TOKEN", "CURSOR_TOKEN"].map((locator) => ({
|
|
215
|
+
store: "env",
|
|
216
|
+
locator,
|
|
217
|
+
}));
|
|
218
|
+
if (process.env.APPDATA)
|
|
219
|
+
candidates.push({
|
|
220
|
+
store: "sqlite",
|
|
221
|
+
locator: join(process.env.APPDATA, "Cursor", "User", "globalStorage", "state.vscdb"),
|
|
222
|
+
});
|
|
223
|
+
candidates.push(
|
|
224
|
+
...[
|
|
225
|
+
join(
|
|
226
|
+
home,
|
|
227
|
+
"Library",
|
|
228
|
+
"Application Support",
|
|
229
|
+
"Cursor",
|
|
230
|
+
"User",
|
|
231
|
+
"globalStorage",
|
|
232
|
+
"state.vscdb",
|
|
233
|
+
),
|
|
234
|
+
join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb"),
|
|
235
|
+
].map((locator) => ({ store: "sqlite" as const, locator })),
|
|
236
|
+
{ store: "file", locator: join(home, ".config", "cursor", "auth.json") },
|
|
237
|
+
);
|
|
238
|
+
for (const input of candidates) if (await readToken(input)) return [{ key: "default", input }];
|
|
239
|
+
return [];
|
|
241
240
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
export const inputSchema = z
|
|
3
|
-
|
|
2
|
+
export const inputSchema = z
|
|
3
|
+
.object({
|
|
4
|
+
store: z.enum(["env", "file", "sqlite"]),
|
|
5
|
+
locator: z.string().min(1),
|
|
6
|
+
})
|
|
7
|
+
.strict();
|
|
4
8
|
export type UsageInput = z.infer<typeof inputSchema>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
2
|
import { inputSchema } from "./shared/input.js";
|
|
3
|
-
import { fetchUsage,
|
|
3
|
+
import { fetchUsage, discover } from "./server/usage.js";
|
|
4
4
|
|
|
5
5
|
export default function contribute(server: PluginServerContext) {
|
|
6
6
|
server.registerUsageSource({
|
|
@@ -8,8 +8,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
8
8
|
label: "Grok",
|
|
9
9
|
icon: "icon.svg",
|
|
10
10
|
input: inputSchema,
|
|
11
|
-
discover:
|
|
12
|
-
identify,
|
|
11
|
+
discover: () => discover(),
|
|
13
12
|
fetch: fetchUsage,
|
|
14
13
|
});
|
|
15
14
|
return () => {};
|