@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.
Files changed (48) hide show
  1. package/dist/server/builtin-plugins/claude-usage-source/index.server.ts +1 -2
  2. package/dist/server/builtin-plugins/claude-usage-source/server/stores.ts +0 -1
  3. package/dist/server/builtin-plugins/claude-usage-source/server/usage.ts +72 -36
  4. package/dist/server/builtin-plugins/codex-usage-source/index.server.ts +1 -2
  5. package/dist/server/builtin-plugins/codex-usage-source/server/stores.ts +0 -1
  6. package/dist/server/builtin-plugins/codex-usage-source/server/usage.ts +22 -12
  7. package/dist/server/builtin-plugins/copilot-usage-source/index.server.ts +2 -3
  8. package/dist/server/builtin-plugins/copilot-usage-source/server/usage.ts +28 -39
  9. package/dist/server/builtin-plugins/copilot-usage-source/shared/input.ts +6 -2
  10. package/dist/server/builtin-plugins/cursor-usage-source/index.server.ts +2 -3
  11. package/dist/server/builtin-plugins/cursor-usage-source/server/usage.ts +53 -54
  12. package/dist/server/builtin-plugins/cursor-usage-source/shared/input.ts +6 -2
  13. package/dist/server/builtin-plugins/grok-usage-source/index.server.ts +2 -3
  14. package/dist/server/builtin-plugins/grok-usage-source/server/usage.ts +23 -27
  15. package/dist/server/builtin-plugins/grok-usage-source/shared/input.ts +6 -2
  16. package/dist/server/builtin-plugins/kimi-usage-source/index.server.ts +2 -3
  17. package/dist/server/builtin-plugins/kimi-usage-source/server/usage.ts +44 -67
  18. package/dist/server/builtin-plugins/kimi-usage-source/shared/input.ts +6 -2
  19. package/dist/server/builtin-plugins/minimax-usage-source/index.server.ts +2 -3
  20. package/dist/server/builtin-plugins/minimax-usage-source/server/usage.ts +64 -100
  21. package/dist/server/builtin-plugins/minimax-usage-source/shared/input.ts +6 -2
  22. package/dist/server/builtin-plugins/muse-provider/server/usage.ts +9 -9
  23. package/dist/server/builtin-plugins/opencode-go-usage-source/index.server.ts +1 -2
  24. package/dist/server/builtin-plugins/opencode-go-usage-source/server/usage.ts +8 -12
  25. package/dist/server/builtin-plugins/opencode-go-usage-source/shared/input.ts +1 -2
  26. package/dist/server/builtin-plugins/package.json +1 -1
  27. package/dist/server/builtin-plugins/zai-usage-source/index.server.ts +2 -3
  28. package/dist/server/builtin-plugins/zai-usage-source/server/usage.ts +13 -11
  29. package/dist/server/builtin-plugins/zai-usage-source/shared/input.ts +6 -2
  30. package/dist/server/server/plugins/index.d.ts +18 -3
  31. package/dist/server/server/plugins/index.js +1 -2
  32. package/dist/server/server/plugins/plugin-process-protocol.d.ts +0 -5
  33. package/dist/server/server/plugins/plugin-process-protocol.js +0 -8
  34. package/dist/server/server/plugins/plugin-process.js +2 -7
  35. package/dist/server/server/plugins/runtime.d.ts +0 -1
  36. package/dist/server/server/plugins/runtime.js +0 -11
  37. package/dist/server/server/plugins/test-fixtures/usage-source/index.server.js +11 -2
  38. package/dist/server/server/plugins/test-fixtures/usage-source-directory/index.server.js +11 -2
  39. package/dist/server/server/plugins/usage-sources/index.d.ts +6 -8
  40. package/dist/server/server/plugins/usage-sources/index.js +79 -84
  41. package/dist/server/web-ui/_expo/static/js/web/{index-a43718759ebeb27df202e8b9fc2fe58a.js → index-1a3199cb049d45e47bfd4c216ce98838.js} +8 -8
  42. package/dist/server/web-ui/_expo/static/js/web/index-1a3199cb049d45e47bfd4c216ce98838.js.br +0 -0
  43. package/dist/server/web-ui/_expo/static/js/web/{index-a43718759ebeb27df202e8b9fc2fe58a.js.gz → index-1a3199cb049d45e47bfd4c216ce98838.js.gz} +0 -0
  44. package/dist/server/web-ui/index.html +1 -1
  45. package/dist/server/web-ui/index.html.br +0 -0
  46. package/dist/server/web-ui/index.html.gz +0 -0
  47. package/package.json +6 -6
  48. 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, identify } from "./server/usage.js";
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
- unavailableUsage,
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(lookup: ClaudeCredentialLookup = {}): Promise<UsageInput[]> {
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
- { route: { store: "claude", path: claudeCredentialPath(lookup) } },
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 present: UsageInput[] = [];
370
- for (const input of candidates)
371
- if (await resolveClaudeCredentials(input, lookup)) present.push(input);
372
- return present;
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 ? { oauth: { ...oauth, accessToken: oauth.accessToken } } : null;
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 | "NEEDS_AUTH"> {
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 "NEEDS_AUTH";
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
- !credentials ||
453
- (credentials.expires !== undefined &&
454
- credentials.expires <= (credentialLookup.now ?? Date.now)())
455
- ) {
456
- return unavailableUsage();
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 === "NEEDS_AUTH") {
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 } | null;
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
- export async function identify(
513
- input: UsageInput,
514
- fetchApi: typeof fetch = fetch,
515
- now: () => number = Date.now,
516
- credentialLookup: ClaudeCredentialLookup = {},
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 null;
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, identify } from "./server/usage.js";
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<CodexUsageInput[]> {
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: CodexUsageInput[] = [];
63
- for (const input of candidates) if (await readAuth(input, lookup)) present.push(input);
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 || (auth.expires !== undefined && auth.expires <= (lookup.now ?? Date.now)()))
108
- return { status: "unavailable", windows: [] };
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 { status: "unavailable", windows: [] };
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("<")) return { status: "unavailable", windows: [] };
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
- export async function identify(input: CodexUsageInput, lookup: StoreLookup = {}) {
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
- if (!key) return null;
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, identify } from "./server/usage.js";
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: async () => [{}],
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 { existsSync, promises as fs } from "node:fs";
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
- unavailableUsage,
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 readGithubCliToken(): Promise<string | null> {
23
- const candidates: string[] = [];
24
- if (process.env["APPDATA"]) {
25
- candidates.push(join(process.env["APPDATA"], "GitHub CLI", "hosts.yml"));
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
- candidates.push(join(homedir(), ".config", "gh", "hosts.yml"));
31
+ }
28
32
 
29
- for (const path of candidates) {
30
- if (!existsSync(path)) continue;
31
- try {
32
- const raw = await fs.readFile(path, "utf8");
33
- const match = raw.match(/oauth_token:\s*["']?([a-zA-Z0-9_-]+)["']?/);
34
- if (match?.[1]) return match[1];
35
- } catch {
36
- continue;
37
- }
38
- }
39
- return null;
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
- void input;
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 (!res.ok) {
69
- return unavailableUsage();
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,4 +1,8 @@
1
1
  import { z } from "zod";
2
- export const inputSchema = z.object({}).strict();
3
-
2
+ export const inputSchema = z
3
+ .object({
4
+ store: z.enum(["env", "file"]),
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, identify } from "./server/usage.js";
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: async () => [{}],
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
- unavailableUsage,
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(homeDir: string): Promise<string | null> {
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
- for (const path of dbPaths) {
146
- if (!existsSync(path)) continue;
147
- let db: CursorStateDatabase | undefined;
148
- try {
149
- db = new sqlite.DatabaseSync(path, { readOnly: true });
150
- const token = cursorTokenFromDb(db);
151
- if (token) return token;
152
- } catch {
153
- // Locked, permission, corrupt, or schema failures try the next candidate.
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(homeDir: string): Promise<string | null> {
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
- void input;
177
- const homeDir = homedir();
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 (!res.ok) {
202
- return unavailableUsage();
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
- export async function identify() {
234
- const home = homedir();
204
+ async function readToken(input: UsageInput): Promise<string | undefined> {
205
+ if (input.store === "env") return process.env[input.locator];
235
206
  const token =
236
- process.env["CURSOR_ACCESS_TOKEN"] ||
237
- process.env["CURSOR_TOKEN"] ||
238
- (await readCursorTokenFromSqlite(home)) ||
239
- (await readCursorTokenFromAuthJson(home));
240
- return token ? { key: "default" } : null;
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.object({}).strict();
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, identify } from "./server/usage.js";
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: async () => [{}],
12
- identify,
11
+ discover: () => discover(),
13
12
  fetch: fetchUsage,
14
13
  });
15
14
  return () => {};