@camerontaylor/paseo-server 0.11.0-beta.1.fork.1 → 0.11.0-beta.3.fork.1

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 (55) 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 +123 -70
  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 +103 -33
  8. package/dist/server/builtin-plugins/codex-usage-source/shared/input.ts +12 -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 +59 -34
  11. package/dist/server/builtin-plugins/copilot-usage-source/shared/input.ts +7 -1
  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 +115 -71
  48. package/dist/server/web-ui/_expo/static/js/web/{index-5887a06cac5928321e7fd84d0fdf68a4.js → index-980574f58cb292611c985646d625a892.js} +12 -12
  49. package/dist/server/web-ui/_expo/static/js/web/index-980574f58cb292611c985646d625a892.js.br +0 -0
  50. package/dist/server/web-ui/_expo/static/js/web/{index-5887a06cac5928321e7fd84d0fdf68a4.js.gz → index-980574f58cb292611c985646d625a892.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-5887a06cac5928321e7fd84d0fdf68a4.js.br +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,33 +35,82 @@ const responseSchema = z.object({
32
35
  credits: z.object({ balance: number.optional() }).nullish(),
33
36
  });
34
37
 
38
+ interface Auth {
39
+ token: string;
40
+ accountId?: string;
41
+ idToken?: string;
42
+ expires?: number;
43
+ }
44
+
45
+ type RoutedInput = Extract<CodexUsageInput, { route: unknown }>;
46
+
47
+ export async function discover(lookup: StoreLookup = {}): Promise<UsageAccount[]> {
48
+ const env = lookup.env ?? process.env;
49
+ const home = lookup.home ?? homedir();
50
+ const paths = [
51
+ ...(env.CODEX_HOME ? [join(env.CODEX_HOME, "auth.json")] : []),
52
+ join(home, ".codex", "auth.json"),
53
+ ];
54
+ const candidates: RoutedInput[] = [...new Set(paths)].map((path) => ({
55
+ route: { store: "codex", path },
56
+ }));
57
+ candidates.push(
58
+ {
59
+ route: {
60
+ store: "opencode",
61
+ path: join(env.XDG_DATA_HOME || join(home, ".local", "share"), "opencode", "auth.json"),
62
+ },
63
+ },
64
+ { route: { store: "pi", path: piAuthPath(lookup) } },
65
+ ...discoverOmp(lookup).map((route) => ({ route })),
66
+ );
67
+ const present: UsageAccount[] = [];
68
+ for (const input of candidates) {
69
+ const auth = await readAuth(input, lookup);
70
+ if (auth) present.push({ ...accountIdentity(auth, input), input });
71
+ }
72
+ return present;
73
+ }
74
+
35
75
  export async function readAuth(
36
- input: CodexUsageInput,
37
- ): Promise<{ token: string; accountId?: string; idToken?: string } | null> {
38
- let candidates: string[];
76
+ input: CodexUsageInput | Record<string, never>,
77
+ lookup: StoreLookup = {},
78
+ ): Promise<Auth | null> {
39
79
  if ("providerId" in input) {
40
- candidates = input.codexHome ? [join(input.codexHome, "auth.json")] : [];
41
- } else {
42
- candidates = [
43
- ...(process.env["CODEX_HOME"] ? [join(process.env["CODEX_HOME"], "auth.json")] : []),
44
- join(homedir(), ".config", "codex", "auth.json"),
45
- join(homedir(), ".codex", "auth.json"),
46
- ];
80
+ if (!input.codexHome) return null;
81
+ return readAuth(
82
+ { route: { store: "codex", path: join(input.codexHome, "auth.json") } },
83
+ lookup,
84
+ );
47
85
  }
48
- for (const path of candidates) {
49
- try {
50
- const auth = authSchema.parse(JSON.parse(await readFile(path, "utf8")));
51
- if (auth.tokens?.access_token)
52
- return {
53
- token: auth.tokens.access_token,
54
- accountId: auth.tokens.account_id,
55
- idToken: auth.tokens.id_token,
56
- };
57
- } catch {
58
- continue;
86
+ if (!("route" in input)) {
87
+ const env = lookup.env ?? process.env;
88
+ const home = lookup.home ?? homedir();
89
+ const paths = [
90
+ ...(env.CODEX_HOME ? [join(env.CODEX_HOME, "auth.json")] : []),
91
+ join(home, ".config", "codex", "auth.json"),
92
+ join(home, ".codex", "auth.json"),
93
+ ];
94
+ for (const path of new Set(paths)) {
95
+ const auth = await readAuth({ route: { store: "codex", path } }, lookup);
96
+ if (auth) return auth;
59
97
  }
98
+ return null;
99
+ }
100
+ const route = input.route;
101
+ if (route.store !== "codex") {
102
+ const oauth = await readHarness(route, lookup);
103
+ return oauth
104
+ ? { token: oauth.access, accountId: oauth.accountId, expires: oauth.expires }
105
+ : null;
60
106
  }
61
- return null;
107
+ const auth = authSchema.safeParse(await readJson(route.path));
108
+ if (!auth.success || !auth.data.tokens?.access_token) return null;
109
+ return {
110
+ token: auth.data.tokens.access_token,
111
+ accountId: auth.data.tokens.account_id,
112
+ idToken: auth.data.tokens.id_token,
113
+ };
62
114
  }
63
115
 
64
116
  function usageWindow(
@@ -78,9 +130,17 @@ function usageWindow(
78
130
  export async function fetchUsage(
79
131
  input: CodexUsageInput,
80
132
  fetchApi: typeof fetch = fetch,
133
+ lookup: StoreLookup = {},
81
134
  ): Promise<UsageReport> {
82
- const auth = await readAuth(input);
83
- if (!auth) return { status: "unavailable", windows: [] };
135
+ const auth = await readAuth(input, lookup);
136
+ if (!auth) throw new Error("Codex login store no longer exists");
137
+ const refreshedBy = "route" in input ? input.route.store : "codex";
138
+ if (auth.expires !== undefined && auth.expires <= (lookup.now ?? Date.now)())
139
+ return unavailable({
140
+ kind: "expired",
141
+ expiresAt: new Date(auth.expires).toISOString(),
142
+ refreshedBy,
143
+ });
84
144
  const headers: Record<string, string> = {
85
145
  Authorization: `Bearer ${auth.token}`,
86
146
  Accept: "application/json",
@@ -92,10 +152,10 @@ export async function fetchUsage(
92
152
  signal: AbortSignal.timeout(15_000),
93
153
  });
94
154
  if (response.status === 401 || response.status === 403)
95
- return { status: "unavailable", windows: [] };
155
+ return unavailable({ kind: "rejected", status: response.status, refreshedBy });
96
156
  if (!response.ok) throw new Error(`Codex usage API returned ${response.status}`);
97
157
  const text = await response.text();
98
- if (text.trim().startsWith("<")) return { status: "unavailable", windows: [] };
158
+ if (text.trim().startsWith("<")) throw new Error("Codex usage API returned HTML");
99
159
  const usage = responseSchema.parse(JSON.parse(text));
100
160
  const windows = [
101
161
  usageWindow(
@@ -156,10 +216,7 @@ function claimString(value: unknown): string | null {
156
216
  return typeof value === "string" && value.length > 0 ? value : null;
157
217
  }
158
218
 
159
- export async function identify(input: CodexUsageInput) {
160
- if ("providerId" in input) return { key: `provider.${input.providerId}`, label: input.label };
161
- const auth = await readAuth(input);
162
- if (!auth) return null;
219
+ function accountIdentity(auth: Auth, input: RoutedInput): { key: string; label?: string } {
163
220
  const access = jwtClaims(auth.token);
164
221
  const id = jwtClaims(auth.idToken);
165
222
  const accessAuth = claimObject(access, "https://api.openai.com/auth");
@@ -169,11 +226,24 @@ export async function identify(input: CodexUsageInput) {
169
226
  claimString(accessAuth?.["chatgpt_account_id"]) ??
170
227
  claimString(access?.["chatgpt_account_id"]) ??
171
228
  claimString(idAuth?.["chatgpt_account_id"]);
172
- if (!key) return null;
229
+
173
230
  const label =
174
231
  claimString(claimObject(access, "https://api.openai.com/profile")?.["email"]) ??
175
232
  claimString(access?.["email"]) ??
176
233
  claimString(claimObject(id, "https://api.openai.com/profile")?.["email"]) ??
177
234
  claimString(id?.["email"]);
178
- return { key, ...(label ? { label } : {}) };
235
+ return { key: key ?? hashAccountKey(JSON.stringify(input.route)), ...(label ? { label } : {}) };
236
+ }
237
+
238
+ export async function identify(
239
+ input: CodexUsageInput | Record<string, never>,
240
+ lookup: StoreLookup = {},
241
+ ) {
242
+ if ("providerId" in input) return { key: `provider.${input.providerId}`, label: input.label };
243
+ const auth = await readAuth(input, lookup);
244
+ if (!auth) return null;
245
+ if ("route" in input) return accountIdentity(auth, { route: input.route });
246
+ return accountIdentity(auth, {
247
+ route: { store: "codex", path: join(lookup.home ?? homedir(), ".codex", "auth.json") },
248
+ });
179
249
  }
@@ -1,6 +1,17 @@
1
1
  import { z } from "zod";
2
+
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
+ ]);
2
13
  export const inputSchema = z.union([
3
- z.strictObject({}),
14
+ z.object({ route: routeSchema }).strict(),
4
15
  z.strictObject({
5
16
  providerId: z.string().min(1),
6
17
  label: z.string(),
@@ -1,6 +1,6 @@
1
1
  import type { PluginServerContext } from "@camerontaylor/paseo-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 "@camerontaylor/paseo-plugin/server/usage";
@@ -19,39 +20,64 @@ 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
+ type RoutedInput = Extract<UsageInput, { store: string }>;
24
+
25
+ async function readToken(input: RoutedInput): Promise<string | undefined> {
26
+ if (input.store === "env") return process.env[input.locator];
27
+ try {
28
+ const raw = await fs.readFile(input.locator, "utf8");
29
+ return raw.match(/oauth_token:\s*["']?([a-zA-Z0-9_-]+)["']?/)?.[1];
30
+ } catch {
31
+ return undefined;
26
32
  }
27
- candidates.push(join(homedir(), ".config", "gh", "hosts.yml"));
33
+ }
28
34
 
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
- }
35
+ export async function discover(): Promise<UsageAccount[]> {
36
+ const candidates: RoutedInput[] = ["COPILOT_TOKEN", "GITHUB_TOKEN", "GITHUB_PAT"].map(
37
+ (locator) => ({ store: "env", locator }),
38
+ );
39
+ if (process.env.APPDATA)
40
+ candidates.push({
41
+ store: "file",
42
+ locator: join(process.env.APPDATA, "GitHub CLI", "hosts.yml"),
43
+ });
44
+ candidates.push({ store: "file", locator: join(homedir(), ".config", "gh", "hosts.yml") });
45
+ for (const input of candidates) if (await readToken(input)) return [{ key: "default", input }];
46
+ return [];
47
+ }
48
+
49
+ async function readDefaultToken(): Promise<string | undefined> {
50
+ for (const locator of ["COPILOT_TOKEN", "GITHUB_TOKEN", "GITHUB_PAT"]) {
51
+ const token = process.env[locator];
52
+ if (token) return token;
53
+ }
54
+ const paths = [
55
+ ...(process.env.APPDATA ? [join(process.env.APPDATA, "GitHub CLI", "hosts.yml")] : []),
56
+ join(homedir(), ".config", "gh", "hosts.yml"),
57
+ ];
58
+ for (const locator of paths) {
59
+ const token = await readToken({ store: "file", locator });
60
+ if (token) return token;
38
61
  }
39
- return null;
62
+ return undefined;
40
63
  }
41
64
 
42
65
  export async function fetchUsage(
43
66
  input: UsageInput,
44
67
  fetchApi: typeof fetch = fetch,
45
68
  ): Promise<UsageReport> {
46
- const token =
47
- "providerId" in input
48
- ? input.accessToken
49
- : process.env["COPILOT_TOKEN"] ||
50
- process.env["GITHUB_TOKEN"] ||
51
- process.env["GITHUB_PAT"] ||
52
- (await readGithubCliToken());
53
-
54
- if (!token) return unavailableUsage();
69
+ let token: string | undefined;
70
+ if ("providerId" in input) token = input.accessToken;
71
+ else if ("store" in input) token = await readToken(input);
72
+ else token = await readDefaultToken();
73
+ if (!token) {
74
+ if ("providerId" in input)
75
+ return unavailable({
76
+ kind: "no_quota",
77
+ detail: "No Copilot token is configured for this provider account.",
78
+ });
79
+ throw new Error("Copilot login store no longer exists");
80
+ }
55
81
 
56
82
  const res = await fetchApi("https://api.github.com/copilot_internal/user", {
57
83
  signal: AbortSignal.timeout(15_000),
@@ -65,9 +91,9 @@ export async function fetchUsage(
65
91
  },
66
92
  });
67
93
 
68
- if (!res.ok) {
69
- return unavailableUsage();
70
- }
94
+ if (res.status === 401 || res.status === 403)
95
+ return unavailable({ kind: "rejected", status: res.status });
96
+ if (!res.ok) throw new Error(`Copilot usage API returned ${res.status}`);
71
97
 
72
98
  const resp = CopilotUsageResponseSchema.parse(await res.json());
73
99
  const details: UsageDetail[] = resp.quota_reset_date
@@ -83,12 +109,11 @@ export async function fetchUsage(
83
109
  };
84
110
  }
85
111
 
86
- export async function identify(input: UsageInput = {}) {
112
+ export async function identify(input: UsageInput | Record<string, never> = {}) {
87
113
  if ("providerId" in input) return { key: `provider.${input.providerId}`, label: input.label };
88
114
  const token =
89
- process.env["COPILOT_TOKEN"] ||
90
- process.env["GITHUB_TOKEN"] ||
91
- process.env["GITHUB_PAT"] ||
92
- (await readGithubCliToken());
115
+ "store" in input
116
+ ? await readToken({ store: input.store, locator: input.locator })
117
+ : await readDefaultToken();
93
118
  return token ? { key: "default" } : null;
94
119
  }
@@ -1,6 +1,12 @@
1
1
  import { z } from "zod";
2
+
2
3
  export const inputSchema = z.union([
3
- z.strictObject({}),
4
+ z
5
+ .object({
6
+ store: z.enum(["env", "file"]),
7
+ locator: z.string().min(1),
8
+ })
9
+ .strict(),
4
10
  z.strictObject({
5
11
  providerId: z.string().min(1),
6
12
  label: z.string(),
@@ -1,6 +1,6 @@
1
1
  import type { PluginServerContext } from "@camerontaylor/paseo-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 "@camerontaylor/paseo-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 "@camerontaylor/paseo-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>;