@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.
- package/dist/server/builtin-plugins/claude-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/claude-usage-source/server/stores.ts +131 -0
- package/dist/server/builtin-plugins/claude-usage-source/server/usage.ts +123 -70
- package/dist/server/builtin-plugins/claude-usage-source/shared/input.ts +13 -1
- package/dist/server/builtin-plugins/codex-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/codex-usage-source/server/stores.ts +131 -0
- package/dist/server/builtin-plugins/codex-usage-source/server/usage.ts +103 -33
- package/dist/server/builtin-plugins/codex-usage-source/shared/input.ts +12 -1
- package/dist/server/builtin-plugins/copilot-usage-source/index.server.ts +2 -3
- package/dist/server/builtin-plugins/copilot-usage-source/server/usage.ts +59 -34
- package/dist/server/builtin-plugins/copilot-usage-source/shared/input.ts +7 -1
- 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/orchestration-skills/internal/operations.js +19 -5
- package/dist/server/server/orchestration-skills/internal/sync.d.ts +12 -0
- package/dist/server/server/orchestration-skills/internal/sync.js +48 -2
- package/dist/server/server/plugins/builtin/index.js +6 -0
- package/dist/server/server/plugins/compiler.js +4 -0
- 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 +7 -9
- package/dist/server/server/plugins/usage-sources/index.js +115 -71
- package/dist/server/web-ui/_expo/static/js/web/{index-5887a06cac5928321e7fd84d0fdf68a4.js → index-980574f58cb292611c985646d625a892.js} +12 -12
- package/dist/server/web-ui/_expo/static/js/web/index-980574f58cb292611c985646d625a892.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-5887a06cac5928321e7fd84d0fdf68a4.js.gz → index-980574f58cb292611c985646d625a892.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-5887a06cac5928321e7fd84d0fdf68a4.js.br +0 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
38
|
-
|
|
76
|
+
input: CodexUsageInput | Record<string, never>,
|
|
77
|
+
lookup: StoreLookup = {},
|
|
78
|
+
): Promise<Auth | null> {
|
|
39
79
|
if ("providerId" in input) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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)
|
|
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 {
|
|
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("<"))
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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 "@camerontaylor/paseo-plugin/server/usage";
|
|
@@ -19,39 +20,64 @@ const CopilotUsageResponseSchema = z.object({
|
|
|
19
20
|
quota_reset_date: ApiOptionalStringSchema,
|
|
20
21
|
});
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
33
|
+
}
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 (
|
|
69
|
-
return
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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,
|
|
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 "@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(
|
|
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 "@camerontaylor/paseo-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 () => {};
|
|
@@ -1,12 +1,13 @@
|
|
|
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
|
toneFromUsedPct,
|
|
8
8
|
usedPctOf,
|
|
9
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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 (
|
|
142
|
-
return
|
|
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
|
-
|
|
159
|
-
if (
|
|
144
|
+
async function readToken(input: UsageInput): Promise<string | undefined> {
|
|
145
|
+
if (input.store === "env") return process.env[input.locator];
|
|
160
146
|
try {
|
|
161
|
-
|
|
162
|
-
|
|
147
|
+
return (
|
|
148
|
+
extractGrokTokenFromAuth(JSON.parse(await fs.readFile(input.locator, "utf8"))) ?? undefined
|
|
149
|
+
);
|
|
163
150
|
} catch {
|
|
164
|
-
return
|
|
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
|
+
}
|