@getpaseo/server 0.11.0-beta.1 → 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 (56) hide show
  1. package/dist/server/builtin-plugins/claude-usage-source/index.server.ts +2 -3
  2. package/dist/server/builtin-plugins/claude-usage-source/server/stores.ts +131 -0
  3. package/dist/server/builtin-plugins/claude-usage-source/server/usage.ts +102 -50
  4. package/dist/server/builtin-plugins/claude-usage-source/shared/input.ts +13 -1
  5. package/dist/server/builtin-plugins/codex-usage-source/index.server.ts +2 -3
  6. package/dist/server/builtin-plugins/codex-usage-source/server/stores.ts +131 -0
  7. package/dist/server/builtin-plugins/codex-usage-source/server/usage.ts +70 -30
  8. package/dist/server/builtin-plugins/codex-usage-source/shared/input.ts +11 -1
  9. package/dist/server/builtin-plugins/copilot-usage-source/index.server.ts +2 -3
  10. package/dist/server/builtin-plugins/copilot-usage-source/server/usage.ts +28 -39
  11. package/dist/server/builtin-plugins/copilot-usage-source/shared/input.ts +6 -2
  12. package/dist/server/builtin-plugins/cursor-usage-source/index.server.ts +2 -3
  13. package/dist/server/builtin-plugins/cursor-usage-source/server/usage.ts +53 -54
  14. package/dist/server/builtin-plugins/cursor-usage-source/shared/input.ts +6 -2
  15. package/dist/server/builtin-plugins/grok-usage-source/index.server.ts +2 -3
  16. package/dist/server/builtin-plugins/grok-usage-source/server/usage.ts +23 -27
  17. package/dist/server/builtin-plugins/grok-usage-source/shared/input.ts +6 -2
  18. package/dist/server/builtin-plugins/kimi-usage-source/index.server.ts +2 -3
  19. package/dist/server/builtin-plugins/kimi-usage-source/server/usage.ts +44 -67
  20. package/dist/server/builtin-plugins/kimi-usage-source/shared/input.ts +6 -2
  21. package/dist/server/builtin-plugins/minimax-usage-source/index.server.ts +2 -3
  22. package/dist/server/builtin-plugins/minimax-usage-source/server/usage.ts +64 -100
  23. package/dist/server/builtin-plugins/minimax-usage-source/shared/input.ts +6 -2
  24. package/dist/server/builtin-plugins/muse-provider/server/usage.ts +9 -9
  25. package/dist/server/builtin-plugins/opencode-go-usage-source/index.server.ts +1 -2
  26. package/dist/server/builtin-plugins/opencode-go-usage-source/server/usage.ts +8 -12
  27. package/dist/server/builtin-plugins/opencode-go-usage-source/shared/input.ts +1 -2
  28. package/dist/server/builtin-plugins/package.json +1 -1
  29. package/dist/server/builtin-plugins/zai-usage-source/index.server.ts +2 -3
  30. package/dist/server/builtin-plugins/zai-usage-source/server/usage.ts +13 -11
  31. package/dist/server/builtin-plugins/zai-usage-source/shared/input.ts +6 -2
  32. package/dist/server/server/orchestration-skills/internal/operations.js +19 -5
  33. package/dist/server/server/orchestration-skills/internal/sync.d.ts +12 -0
  34. package/dist/server/server/orchestration-skills/internal/sync.js +48 -2
  35. package/dist/server/server/plugins/builtin/index.js +6 -0
  36. package/dist/server/server/plugins/compiler.js +4 -0
  37. package/dist/server/server/plugins/index.d.ts +18 -3
  38. package/dist/server/server/plugins/index.js +1 -2
  39. package/dist/server/server/plugins/plugin-process-protocol.d.ts +0 -5
  40. package/dist/server/server/plugins/plugin-process-protocol.js +0 -8
  41. package/dist/server/server/plugins/plugin-process.js +2 -7
  42. package/dist/server/server/plugins/runtime.d.ts +0 -1
  43. package/dist/server/server/plugins/runtime.js +0 -11
  44. package/dist/server/server/plugins/test-fixtures/usage-source/index.server.js +11 -2
  45. package/dist/server/server/plugins/test-fixtures/usage-source-directory/index.server.js +11 -2
  46. package/dist/server/server/plugins/usage-sources/index.d.ts +7 -9
  47. package/dist/server/server/plugins/usage-sources/index.js +98 -71
  48. package/dist/server/web-ui/_expo/static/js/web/{index-87a07caf197cc320e593332ed2d5e20f.js → index-1a3199cb049d45e47bfd4c216ce98838.js} +13 -13
  49. package/dist/server/web-ui/_expo/static/js/web/index-1a3199cb049d45e47bfd4c216ce98838.js.br +0 -0
  50. package/dist/server/web-ui/_expo/static/js/web/index-1a3199cb049d45e47bfd4c216ce98838.js.gz +0 -0
  51. package/dist/server/web-ui/index.html +1 -1
  52. package/dist/server/web-ui/index.html.br +0 -0
  53. package/dist/server/web-ui/index.html.gz +0 -0
  54. package/package.json +6 -6
  55. package/dist/server/web-ui/_expo/static/js/web/index-87a07caf197cc320e593332ed2d5e20f.js.br +0 -0
  56. package/dist/server/web-ui/_expo/static/js/web/index-87a07caf197cc320e593332ed2d5e20f.js.gz +0 -0
@@ -1,8 +1,11 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { discoverOmp, piAuthPath, readHarness, readJson, type StoreLookup } from "./stores.js";
2
2
  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,
@@ -32,28 +35,59 @@ const responseSchema = z.object({
32
35
  credits: z.object({ balance: number.optional() }).nullish(),
33
36
  });
34
37
 
35
- export async function readAuth(
36
- _input: CodexUsageInput,
37
- ): Promise<{ token: string; accountId?: string; idToken?: string } | null> {
38
- const candidates = [
39
- ...(process.env["CODEX_HOME"] ? [join(process.env["CODEX_HOME"], "auth.json")] : []),
40
- join(homedir(), ".config", "codex", "auth.json"),
41
- join(homedir(), ".codex", "auth.json"),
38
+ interface Auth {
39
+ token: string;
40
+ accountId?: string;
41
+ idToken?: string;
42
+ expires?: number;
43
+ }
44
+
45
+ export async function discover(lookup: StoreLookup = {}): Promise<UsageAccount[]> {
46
+ const env = lookup.env ?? process.env;
47
+ const home = lookup.home ?? homedir();
48
+ const paths = [
49
+ ...(env.CODEX_HOME ? [join(env.CODEX_HOME, "auth.json")] : []),
50
+ join(home, ".codex", "auth.json"),
42
51
  ];
43
- for (const path of candidates) {
44
- try {
45
- const auth = authSchema.parse(JSON.parse(await readFile(path, "utf8")));
46
- if (auth.tokens?.access_token)
47
- return {
48
- token: auth.tokens.access_token,
49
- accountId: auth.tokens.account_id,
50
- idToken: auth.tokens.id_token,
51
- };
52
- } catch {
53
- continue;
54
- }
52
+ const candidates: CodexUsageInput[] = [...new Set(paths)].map((path) => ({
53
+ route: { store: "codex", path },
54
+ }));
55
+ candidates.push(
56
+ {
57
+ route: {
58
+ store: "opencode",
59
+ path: join(env.XDG_DATA_HOME || join(home, ".local", "share"), "opencode", "auth.json"),
60
+ },
61
+ },
62
+ { route: { store: "pi", path: piAuthPath(lookup) } },
63
+ ...discoverOmp(lookup).map((route) => ({ route })),
64
+ );
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
+ }
70
+ return present;
71
+ }
72
+
73
+ export async function readAuth(
74
+ input: CodexUsageInput,
75
+ lookup: StoreLookup = {},
76
+ ): Promise<Auth | null> {
77
+ const route = input.route;
78
+ if (route.store !== "codex") {
79
+ const oauth = await readHarness(route, lookup);
80
+ return oauth
81
+ ? { token: oauth.access, accountId: oauth.accountId, expires: oauth.expires }
82
+ : null;
55
83
  }
56
- return null;
84
+ const auth = authSchema.safeParse(await readJson(route.path));
85
+ if (!auth.success || !auth.data.tokens?.access_token) return null;
86
+ return {
87
+ token: auth.data.tokens.access_token,
88
+ accountId: auth.data.tokens.account_id,
89
+ idToken: auth.data.tokens.id_token,
90
+ };
57
91
  }
58
92
 
59
93
  function usageWindow(
@@ -73,9 +107,17 @@ function usageWindow(
73
107
  export async function fetchUsage(
74
108
  input: CodexUsageInput,
75
109
  fetchApi: typeof fetch = fetch,
110
+ lookup: StoreLookup = {},
76
111
  ): Promise<UsageReport> {
77
- const auth = await readAuth(input);
78
- if (!auth) return { status: "unavailable", windows: [] };
112
+ const auth = await readAuth(input, lookup);
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
+ });
79
121
  const headers: Record<string, string> = {
80
122
  Authorization: `Bearer ${auth.token}`,
81
123
  Accept: "application/json",
@@ -87,10 +129,10 @@ export async function fetchUsage(
87
129
  signal: AbortSignal.timeout(15_000),
88
130
  });
89
131
  if (response.status === 401 || response.status === 403)
90
- return { status: "unavailable", windows: [] };
132
+ return unavailable({ kind: "rejected", status: response.status, refreshedBy });
91
133
  if (!response.ok) throw new Error(`Codex usage API returned ${response.status}`);
92
134
  const text = await response.text();
93
- if (text.trim().startsWith("<")) return { status: "unavailable", windows: [] };
135
+ if (text.trim().startsWith("<")) throw new Error("Codex usage API returned HTML");
94
136
  const usage = responseSchema.parse(JSON.parse(text));
95
137
  const windows = [
96
138
  usageWindow(
@@ -151,9 +193,7 @@ function claimString(value: unknown): string | null {
151
193
  return typeof value === "string" && value.length > 0 ? value : null;
152
194
  }
153
195
 
154
- export async function identify(input: CodexUsageInput) {
155
- const auth = await readAuth(input);
156
- if (!auth) return null;
196
+ function accountIdentity(auth: Auth, input: CodexUsageInput): { key: string; label?: string } {
157
197
  const access = jwtClaims(auth.token);
158
198
  const id = jwtClaims(auth.idToken);
159
199
  const accessAuth = claimObject(access, "https://api.openai.com/auth");
@@ -163,11 +203,11 @@ export async function identify(input: CodexUsageInput) {
163
203
  claimString(accessAuth?.["chatgpt_account_id"]) ??
164
204
  claimString(access?.["chatgpt_account_id"]) ??
165
205
  claimString(idAuth?.["chatgpt_account_id"]);
166
- if (!key) return null;
206
+
167
207
  const label =
168
208
  claimString(claimObject(access, "https://api.openai.com/profile")?.["email"]) ??
169
209
  claimString(access?.["email"]) ??
170
210
  claimString(claimObject(id, "https://api.openai.com/profile")?.["email"]) ??
171
211
  claimString(id?.["email"]);
172
- return { key, ...(label ? { label } : {}) };
212
+ return { key: key ?? hashAccountKey(JSON.stringify(input.route)), ...(label ? { label } : {}) };
173
213
  }
@@ -1,4 +1,14 @@
1
1
  import { z } from "zod";
2
2
 
3
- export const inputSchema = z.object({}).strict();
3
+ export const routeSchema = z.discriminatedUnion("store", [
4
+ z.object({ store: z.enum(["codex", "opencode", "pi"]), path: z.string().min(1) }).strict(),
5
+ z
6
+ .object({
7
+ store: z.literal("omp"),
8
+ path: z.string().min(1),
9
+ credentialId: z.number().int().positive(),
10
+ })
11
+ .strict(),
12
+ ]);
13
+ export const inputSchema = z.object({ route: routeSchema }).strict();
4
14
  export type CodexUsageInput = 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: "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 () => {};
@@ -1,12 +1,13 @@
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
7
  toneFromUsedPct,
8
8
  usedPctOf,
9
- unavailableUsage,
9
+ unavailable,
10
+ type UsageAccount,
10
11
  windowFromUsedPct,
11
12
  type UsageReport,
12
13
  type UsageWindow,
@@ -109,23 +110,8 @@ export async function fetchUsage(
109
110
  input: UsageInput,
110
111
  fetchApi: typeof fetch = fetch,
111
112
  ): Promise<UsageReport> {
112
- void input;
113
- const homeDir = homedir();
114
-
115
- async function readGrokToken(): Promise<string | null> {
116
- // homeDir override is for tests: Windows os.homedir() ignores $HOME (uses USERPROFILE).
117
- const path = join(homeDir ?? homedir(), ".grok", "auth.json");
118
- if (!existsSync(path)) return null;
119
- try {
120
- return extractGrokTokenFromAuth(JSON.parse(await fs.readFile(path, "utf8")));
121
- } catch {
122
- return null;
123
- }
124
- }
125
-
126
- const token = process.env["GROK_API_KEY"] || process.env["GROK_TOKEN"] || (await readGrokToken());
127
-
128
- if (!token) return unavailableUsage();
113
+ const token = await readToken(input);
114
+ if (!token) throw new Error("Grok login store no longer exists");
129
115
 
130
116
  // The Grok CLI's /usage uses ?format=credits; without it, unified-billing accounts
131
117
  // get a zeroed legacy monthly shape (monthlyLimit.val 0) instead of real usage.
@@ -138,9 +124,9 @@ export async function fetchUsage(
138
124
  },
139
125
  });
140
126
 
141
- if (!res.ok) {
142
- return unavailableUsage();
143
- }
127
+ if (res.status === 401 || res.status === 403)
128
+ return unavailable({ kind: "rejected", status: res.status });
129
+ if (!res.ok) throw new Error(`Grok usage API returned ${res.status}`);
144
130
 
145
131
  const resp = GrokUsageResponseSchema.parse(await res.json());
146
132
  const balance = grokMonthlyCreditBalance(resp);
@@ -155,12 +141,22 @@ export async function fetchUsage(
155
141
  };
156
142
  }
157
143
 
158
- export async function identify() {
159
- if (process.env["GROK_API_KEY"] || process.env["GROK_TOKEN"]) return { key: "default" };
144
+ async function readToken(input: UsageInput): Promise<string | undefined> {
145
+ if (input.store === "env") return process.env[input.locator];
160
146
  try {
161
- const auth = JSON.parse(await fs.readFile(join(homedir(), ".grok", "auth.json"), "utf8"));
162
- return extractGrokTokenFromAuth(auth) ? { key: "default" } : null;
147
+ return (
148
+ extractGrokTokenFromAuth(JSON.parse(await fs.readFile(input.locator, "utf8"))) ?? undefined
149
+ );
163
150
  } catch {
164
- return null;
151
+ return undefined;
165
152
  }
166
153
  }
154
+ export async function discover(): Promise<UsageAccount[]> {
155
+ const candidates: UsageInput[] = ["GROK_API_KEY", "GROK_TOKEN"].map((locator) => ({
156
+ store: "env",
157
+ locator,
158
+ }));
159
+ candidates.push({ store: "file", locator: join(homedir(), ".grok", "auth.json") });
160
+ for (const input of candidates) if (await readToken(input)) return [{ key: "default", input }];
161
+ return [];
162
+ }
@@ -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: "Kimi",
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 () => {};