refactor(usage): shape sources as fetch functions

Keep the plugin usage API limited to stable report and presentation helpers. Source-local parsers and timeout handling preserve credential and response parity while the cases move alongside each source.
This commit is contained in:
Mohamed Boudra
2026-09-26 20:52:37 +02:00
parent 0f2f6a3b89
commit 2e53a5b9b5
40 changed files with 2628 additions and 2396 deletions
@@ -1,6 +1,6 @@
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { inputSchema } from "./shared/input.js";
import { ClaudeQuotaProvider } from "./server/usage.js";
import { fetchUsage } from "./server/usage.js";
export default function contribute(server: PluginServerContext) {
server.registerUsageSource({
@@ -9,14 +9,7 @@ export default function contribute(server: PluginServerContext) {
icon: "icon.svg",
input: inputSchema,
discover: async () => [{}],
fetch: async (input) => {
const parsed = inputSchema.parse(input);
return new ClaudeQuotaProvider({
logger: console,
...("configDir" in parsed ? { configDir: parsed.configDir } : {}),
...("accessToken" in parsed ? { accessToken: parsed.accessToken } : {}),
}).fetchUsage();
},
fetch: fetchUsage,
});
return () => {};
}
@@ -1 +1 @@
{ "id": "claude-usage-source", "requirements": { "paseo": ">=0.9.2" } }
{ "id": "claude-usage-source" }
@@ -0,0 +1,164 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { claudeKeychainAccount, readClaudeKeychainCredentials } from "./usage.js";
const SERVICE = "Claude Code-credentials";
afterEach(() => {
vi.unstubAllEnvs();
});
describe("claudeKeychainAccount", () => {
it("uses a supported username verbatim", () => {
expect(claudeKeychainAccount("thomas.benoit")).toBe("thomas.benoit");
});
it("falls back to claude-code-user when the username carries a rejected character", () => {
expect(claudeKeychainAccount("first.last@example.com")).toBe("claude-code-user");
});
it("reads the username from $USER when no account is given", () => {
vi.stubEnv("USER", "ci-runner");
expect(claudeKeychainAccount()).toBe("ci-runner");
vi.stubEnv("USER", "first.last@example.com");
expect(claudeKeychainAccount()).toBe("claude-code-user");
});
});
describe("readClaudeKeychainCredentials", () => {
it("looks the item up by account and stops there when it exists", async () => {
const run = vi.fn(async () => JSON.stringify({ claudeAiOauth: { accessToken: "at_fresh" } }));
const credentials = await readClaudeKeychainCredentials(run, "claude-code-user");
expect(credentials).toEqual({ claudeAiOauth: { accessToken: "at_fresh" } });
expect(run).toHaveBeenCalledTimes(1);
expect(run).toHaveBeenCalledWith([
"find-generic-password",
"-a",
"claude-code-user",
"-w",
"-s",
SERVICE,
]);
});
it("falls back to the account-less lookup when no item matches the account", async () => {
const run = vi.fn(async (args: string[]) =>
args.includes("-a") ? null : JSON.stringify({ claudeAiOauth: { accessToken: "at_legacy" } }),
);
const credentials = await readClaudeKeychainCredentials(run, "claude-code-user");
expect(credentials).toEqual({ claudeAiOauth: { accessToken: "at_legacy" } });
expect(run).toHaveBeenNthCalledWith(2, ["find-generic-password", "-w", "-s", SERVICE]);
});
it("falls through when the account item carries no access token", async () => {
const run = vi.fn(async (args: string[]) =>
args.includes("-a")
? JSON.stringify({ claudeAiOauth: { subscriptionType: "team" } })
: JSON.stringify({ claudeAiOauth: { accessToken: "at_legacy" } }),
);
const credentials = await readClaudeKeychainCredentials(run, "claude-code-user");
expect(credentials).toEqual({ claudeAiOauth: { accessToken: "at_legacy" } });
expect(run).toHaveBeenCalledTimes(2);
});
it("is null when the Keychain holds no item at all", async () => {
const run = vi.fn(async () => null);
expect(await readClaudeKeychainCredentials(run, "claude-code-user")).toBeNull();
});
it("tries the next lookup when the item does not parse", async () => {
const run = vi.fn(async (args: string[]) =>
args.includes("-a")
? "not-json"
: JSON.stringify({ claudeAiOauth: { accessToken: "at_legacy" } }),
);
expect(await readClaudeKeychainCredentials(run, "claude-code-user")).toEqual({
claudeAiOauth: { accessToken: "at_legacy" },
});
});
});
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fetchUsage } from "./usage.js";
function fixtureFetch(expectedToken: string): typeof fetch {
return vi.fn(async (_url: RequestInfo | URL, init?: RequestInit) => {
expect(init?.headers).toMatchObject({ Authorization: `Bearer ${expectedToken}` });
return new Response(JSON.stringify({ five_hour: { utilization: 12 } }), { status: 200 });
}) as unknown as typeof fetch;
}
describe("Claude usage input forms", () => {
it("reads only the selected configDir file", async () => {
const dir = mkdtempSync(join(tmpdir(), "claude-usage-input-"));
try {
writeFileSync(
join(dir, ".credentials.json"),
JSON.stringify({ claudeAiOauth: { accessToken: "fixture-file" } }),
);
expect((await fetchUsage({ configDir: dir }, fixtureFetch("fixture-file"))).status).toBe(
"available",
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("uses an accessToken without reading a file", async () => {
expect(
(await fetchUsage({ accessToken: "fixture-direct" }, fixtureFetch("fixture-direct"))).status,
).toBe("available");
});
it("default discovery reads the credential file", async () => {
const dir = mkdtempSync(join(tmpdir(), "claude-usage-default-"));
try {
writeFileSync(
join(dir, ".credentials.json"),
JSON.stringify({ claudeAiOauth: { accessToken: "fixture-default" } }),
);
vi.stubEnv("CLAUDE_HOME", dir);
expect((await fetchUsage({}, fixtureFetch("fixture-default"))).status).toBe("available");
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("stays unavailable when non-default configDir has no credentials", async () => {
const dir = mkdtempSync(join(tmpdir(), "claude-empty-config-"));
try {
const fetchApi = vi.fn() as unknown as typeof fetch;
expect((await fetchUsage({ configDir: dir }, fetchApi)).status).toBe("unavailable");
expect(fetchApi).not.toHaveBeenCalled();
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
describe("Claude Keychain token usage", () => {
it("returns unavailable on 401 without attempting refresh", async () => {
const run = vi.fn(async () =>
JSON.stringify({
claudeAiOauth: { accessToken: "fixture-expired", refreshToken: "fixture-refresh" },
}),
);
const credentials = await readClaudeKeychainCredentials(run, "fixture-account");
const token = (credentials as { claudeAiOauth: { accessToken: string } }).claudeAiOauth
.accessToken;
const fetchApi = vi.fn(
async () => new Response(null, { status: 401 }),
) as unknown as typeof fetch;
const report = await fetchUsage({ accessToken: token }, fetchApi);
expect(report.status).toBe("unavailable");
expect(fetchApi).toHaveBeenCalledTimes(1);
expect(run).toHaveBeenCalledTimes(1);
});
});
@@ -1,194 +1,601 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { claudeKeychainAccount, readClaudeKeychainCredentials } from "./usage.js";
import { readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { fetchUsage } from "./usage.js";
import type { UsageReport } from "@getpaseo/plugin/server/usage";
const SERVICE = "Claude Code-credentials";
function writeClaudeCredentials(
dir: string,
accessToken: string,
refreshToken = "rt_test",
subscriptionType = "pro",
rateLimitTier = "default_1x",
): void {
writeFileSync(
join(dir, ".credentials.json"),
JSON.stringify({
claudeAiOauth: { accessToken, refreshToken, subscriptionType, rateLimitTier },
}),
);
}
afterEach(() => {
vi.unstubAllEnvs();
});
function makeClaudeResponse(
overrides: Partial<{
five_hour: { utilization: number | string; resets_at: string };
seven_day: { utilization: number | string; resets_at: string };
seven_day_opus: { utilization: number | string; resets_at: string };
}> = {},
) {
return {
five_hour: { utilization: 11, resets_at: "2026-06-01T21:00:00Z" },
seven_day: { utilization: 1, resets_at: "2026-06-04T00:00:00Z" },
seven_day_opus: { utilization: 0.5, resets_at: "2026-06-04T00:00:00Z" },
...overrides,
};
}
describe("claudeKeychainAccount", () => {
it("uses a supported username verbatim", () => {
expect(claudeKeychainAccount("thomas.benoit")).toBe("thomas.benoit");
function mockFetch(handlers: Map<string, () => Response>): typeof fetch {
return vi.fn(async (url: RequestInfo | URL) => {
const key = url.toString();
const handler = handlers.get(key);
if (!handler) throw new Error(`Unmocked fetch: ${key}`);
return handler();
}) as unknown as typeof fetch;
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
describe("claude usage source", () => {
let claudeHome: string;
let homeDir: string;
let fetchApi: typeof fetch;
let originalEnv: Record<string, string | undefined>;
beforeEach(() => {
claudeHome = mkdtempSync(join(tmpdir(), "usage-claude-"));
homeDir = mkdtempSync(join(tmpdir(), "usage-home-"));
originalEnv = { ...process.env };
process.env["HOME"] = homeDir;
process.env["USERPROFILE"] = homeDir;
process.env["CLAUDE_HOME"] = claudeHome;
for (const key of [
"APPDATA",
"COPILOT_TOKEN",
"GITHUB_TOKEN",
"GITHUB_PAT",
"CURSOR_ACCESS_TOKEN",
"CURSOR_TOKEN",
"ZAI_API_KEY",
"GLM_API_KEY",
"GROK_API_KEY",
"GROK_TOKEN",
"KIMI_TOKEN",
"KIMI_API_KEY",
"KIMI_CODE_HOME",
"MINIMAX_API_KEY",
"MINIMAX_BASE_URL",
])
delete process.env[key];
fetchApi = mockFetch(new Map());
});
afterEach(() => {
rmSync(claudeHome, { recursive: true, force: true });
rmSync(homeDir, { recursive: true, force: true });
for (const key of Object.keys(process.env)) if (!(key in originalEnv)) delete process.env[key];
for (const key in originalEnv) process.env[key] = originalEnv[key];
});
function service(
_options: {
platform?: typeof process.platform;
keychain?: () => Promise<unknown | null>;
cursorHomeDir?: string;
kimiHomeDir?: string;
} = {},
) {
return {
listUsage: async () => {
const report = await fetchUsage({}, (url, init) => fetchApi(url, init));
return {
providers: [
{
providerId: "claude",
...report,
error: report.error ?? null,
planLabel: report.planLabel ?? null,
},
],
};
},
};
}
function findProvider(
result: { providers: Array<{ providerId: string } & UsageReport> },
id: string,
) {
const report = result.providers.find((item) => item.providerId === id);
if (!report) throw new Error(`Missing usage source ${id}`);
return report;
}
it("fetches Claude usage, coerces API numbers, and attaches HTTP timeout signals", async () => {
writeClaudeCredentials(claudeHome, "at_valid");
fetchApi = mockFetch(
new Map([
[
"https://api.anthropic.com/api/oauth/usage",
() =>
jsonResponse(
makeClaudeResponse({
five_hour: { utilization: "11", resets_at: "2026-06-01T21:00:00Z" },
}),
),
],
]),
);
const result = await service().listUsage();
const claude = findProvider(result, "claude");
expect(claude).toMatchObject({
status: "available",
planLabel: "Pro 1x",
windows: expect.arrayContaining([
expect.objectContaining({ id: "five_hour", usedPct: 11 }),
expect.objectContaining({ id: "weekly", usedPct: 1 }),
expect.objectContaining({ id: "weekly_model_opus", usedPct: 0.5 }),
]),
});
expect(fetchApi).toHaveBeenCalledWith(
"https://api.anthropic.com/api/oauth/usage",
expect.objectContaining({ signal: expect.any(AbortSignal) }),
);
});
it("falls back to claude-code-user when the username carries a rejected character", () => {
expect(claudeKeychainAccount("first.last@example.com")).toBe("claude-code-user");
it("accepts a null Claude resets_at when a window has no scheduled reset", async () => {
writeClaudeCredentials(claudeHome, "at_valid");
fetchApi = mockFetch(
new Map([
[
"https://api.anthropic.com/api/oauth/usage",
() =>
jsonResponse({
five_hour: { utilization: 0, resets_at: null },
seven_day: { utilization: 1, resets_at: "2026-06-04T00:00:00Z" },
}),
],
]),
);
const result = await service().listUsage();
const claude = findProvider(result, "claude");
expect(claude).toMatchObject({
status: "available",
windows: expect.arrayContaining([
expect.objectContaining({ id: "five_hour", usedPct: 0, resetsAt: null }),
expect.objectContaining({ id: "weekly", usedPct: 1 }),
]),
});
});
it("reads the username from $USER when no account is given", () => {
vi.stubEnv("USER", "ci-runner");
expect(claudeKeychainAccount()).toBe("ci-runner");
it("returns unavailable Claude usage when credentials are missing", async () => {
fetchApi = vi.fn() as never;
vi.stubEnv("USER", "first.last@example.com");
expect(claudeKeychainAccount()).toBe("claude-code-user");
const result = await service().listUsage();
const claude = findProvider(result, "claude");
expect(claude.status).toBe("unavailable");
expect(fetchApi).not.toHaveBeenCalled();
});
it("returns unavailable on 401 without refreshing or rewriting credentials", async () => {
writeClaudeCredentials(claudeHome, "at_expired", "rt_valid");
const credPath = join(claudeHome, ".credentials.json");
const before = readFileSync(credPath, "utf8");
let usageCalls = 0;
fetchApi = vi.fn(async (url: RequestInfo | URL) => {
const endpoint = url.toString();
if (endpoint === "https://api.anthropic.com/api/oauth/usage") {
usageCalls += 1;
return new Response(null, { status: 401 });
}
// The read-only fetcher must never hit the OAuth token endpoint.
throw new Error(`Unmocked: ${endpoint}`);
}) as never;
const result = await service().listUsage();
expect(findProvider(result, "claude").status).toBe("unavailable");
expect(usageCalls).toBe(1);
// The credentials file must be left untouched for the Claude CLI to own.
expect(readFileSync(credPath, "utf8")).toBe(before);
});
});
describe("readClaudeKeychainCredentials", () => {
it("looks the item up by account and stops there when it exists", async () => {
const run = vi.fn(async () => JSON.stringify({ claudeAiOauth: { accessToken: "at_fresh" } }));
describe("usage bars escalate as they fill", () => {
let claudeHome: string;
const credentials = await readClaudeKeychainCredentials(run, "claude-code-user");
beforeEach(() => {
claudeHome = mkdtempSync(join(tmpdir(), "paseo-tone-claude-"));
});
expect(credentials).toEqual({ claudeAiOauth: { accessToken: "at_fresh" } });
expect(run).toHaveBeenCalledTimes(1);
expect(run).toHaveBeenCalledWith([
"find-generic-password",
"-a",
"claude-code-user",
"-w",
"-s",
SERVICE,
afterEach(() => {
rmSync(claudeHome, { recursive: true, force: true });
});
function claudeAt(utilization: number) {
writeClaudeCredentials(claudeHome, "at_valid");
process.env["CLAUDE_HOME"] = claudeHome;
return fetchUsage(
{},
mockFetch(
new Map([
[
"https://api.anthropic.com/api/oauth/usage",
() => jsonResponse({ seven_day: { utilization, resets_at: "2026-06-04T00:00:00Z" } }),
],
]),
),
);
}
it.each([
[10, "ok"],
[75, "warning"],
[99, "danger"],
])("a Claude window at %s%% is %s", async (utilization, tone) => {
const usage = await claudeAt(utilization);
expect(usage.windows).toEqual([expect.objectContaining({ id: "weekly", tone })]);
});
});
describe("Claude usage source scoped weekly limits", () => {
beforeEach(() => {
vi.spyOn(console, "warn").mockImplementation(() => {});
});
afterEach(() => {
vi.restoreAllMocks();
});
let claudeHome: string;
beforeEach(() => {
claudeHome = mkdtempSync(join(tmpdir(), "paseo-claude-limits-"));
});
afterEach(() => {
rmSync(claudeHome, { recursive: true, force: true });
});
function fableLimit(overrides: Record<string, unknown> = {}) {
return {
kind: "weekly_scoped",
group: "weekly",
percent: 0,
severity: "normal",
resets_at: "2026-06-04T00:00:00Z",
is_active: false,
scope: { model: { id: null, display_name: "Fable" }, surface: null },
...overrides,
};
}
function claudeProvider(body: unknown) {
writeClaudeCredentials(claudeHome, "at_valid");
process.env["CLAUDE_HOME"] = claudeHome;
const fetchApi = mockFetch(
new Map([["https://api.anthropic.com/api/oauth/usage", () => jsonResponse(body)]]),
);
return { provider: { fetchUsage: () => fetchUsage({}, fetchApi) } };
}
it("renders a scoped weekly limit as its own window", async () => {
const { provider } = claudeProvider({
five_hour: { utilization: 6, resets_at: "2026-06-01T21:00:00Z" },
seven_day: { utilization: 23, resets_at: "2026-06-04T00:00:00Z" },
limits: [fableLimit()],
});
const usage = await provider.fetchUsage();
expect(usage.windows).toContainEqual(
expect.objectContaining({ id: "weekly_model_fable", label: "Weekly · Fable" }),
);
});
it("renders a scoped window that is at zero and inactive", async () => {
const { provider } = claudeProvider({
seven_day: { utilization: 23, resets_at: "2026-06-04T00:00:00Z" },
limits: [fableLimit({ percent: 0, is_active: false })],
});
const usage = await provider.fetchUsage();
expect(usage.windows).toContainEqual(
expect.objectContaining({ id: "weekly_model_fable", usedPct: 0, remainingPct: 100 }),
);
});
it("ignores session and all-models entries so they do not duplicate the top-level windows", async () => {
const { provider } = claudeProvider({
five_hour: { utilization: 6, resets_at: "2026-06-01T21:00:00Z" },
seven_day: { utilization: 23, resets_at: "2026-06-04T00:00:00Z" },
limits: [
{ kind: "session", percent: 6, resets_at: "2026-06-01T21:00:00Z", scope: null },
{ kind: "weekly_all", percent: 23, resets_at: "2026-06-04T00:00:00Z", scope: null },
fableLimit(),
],
});
const usage = await provider.fetchUsage();
expect(usage.windows.map((window) => window.id)).toEqual([
"five_hour",
"weekly",
"weekly_model_fable",
]);
});
it("falls back to the account-less lookup when no item matches the account", async () => {
const run = vi.fn(async (args: string[]) =>
args.includes("-a") ? null : JSON.stringify({ claudeAiOauth: { accessToken: "at_legacy" } }),
);
const credentials = await readClaudeKeychainCredentials(run, "claude-code-user");
expect(credentials).toEqual({ claudeAiOauth: { accessToken: "at_legacy" } });
expect(run).toHaveBeenNthCalledWith(2, ["find-generic-password", "-w", "-s", SERVICE]);
});
it("falls through when the account item carries no access token", async () => {
const run = vi.fn(async (args: string[]) =>
args.includes("-a")
? JSON.stringify({ claudeAiOauth: { subscriptionType: "team" } })
: JSON.stringify({ claudeAiOauth: { accessToken: "at_legacy" } }),
);
const credentials = await readClaudeKeychainCredentials(run, "claude-code-user");
expect(credentials).toEqual({ claudeAiOauth: { accessToken: "at_legacy" } });
expect(run).toHaveBeenCalledTimes(2);
});
it("is null when the Keychain holds no item at all", async () => {
const run = vi.fn(async () => null);
expect(await readClaudeKeychainCredentials(run, "claude-code-user")).toBeNull();
});
it("tries the next lookup when the item does not parse", async () => {
const run = vi.fn(async (args: string[]) =>
args.includes("-a")
? "not-json"
: JSON.stringify({ claudeAiOauth: { accessToken: "at_legacy" } }),
);
expect(await readClaudeKeychainCredentials(run, "claude-code-user")).toEqual({
claudeAiOauth: { accessToken: "at_legacy" },
it("labels a surface-scoped limit from its surface name", async () => {
const { provider } = claudeProvider({
seven_day: { utilization: 23, resets_at: "2026-06-04T00:00:00Z" },
limits: [
fableLimit({ scope: { model: null, surface: { id: "code", display_name: "Code" } } }),
],
});
});
});
describe("Claude usage input forms", () => {
it("reads only the selected configDir file", async () => {
const { mkdtempSync, writeFileSync, rmSync } = await import("node:fs");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const { ClaudeQuotaProvider } = await import("./usage.js");
const dir = mkdtempSync(join(tmpdir(), "claude-usage-input-"));
try {
writeFileSync(
join(dir, ".credentials.json"),
JSON.stringify({ claudeAiOauth: { accessToken: "fixture-file" } }),
);
const keychain = vi.fn(async () => ({ claudeAiOauth: { accessToken: "fixture-keychain" } }));
const fetchApi = vi.fn(async (_url: unknown, init: RequestInit) => {
expect(init.headers).toMatchObject({ Authorization: "Bearer fixture-file" });
return new Response(JSON.stringify({ five_hour: { utilization: 12 } }), { status: 200 });
});
const report = await new ClaudeQuotaProvider({
logger: console,
configDir: dir,
platform: "darwin",
claudeKeychainReader: keychain,
fetch: fetchApi as never,
}).fetchUsage();
expect(report.status).toBe("available");
expect(keychain).not.toHaveBeenCalled();
} finally {
rmSync(dir, { recursive: true, force: true });
}
const usage = await provider.fetchUsage();
expect(usage.windows).toContainEqual(
expect.objectContaining({ id: "weekly_surface_code", label: "Weekly · Code" }),
);
});
it("uses an accessToken without reading a file or keychain", async () => {
const { ClaudeQuotaProvider } = await import("./usage.js");
const keychain = vi.fn(async () => null);
const fetchApi = vi.fn(async (_url: unknown, init: RequestInit) => {
expect(init.headers).toMatchObject({ Authorization: "Bearer fixture-direct" });
return new Response(JSON.stringify({ seven_day: { utilization: 25 } }), { status: 200 });
it("skips a scoped limit with no resolvable label rather than rendering an unlabelled bar", async () => {
const { provider } = claudeProvider({
seven_day: { utilization: 23, resets_at: "2026-06-04T00:00:00Z" },
limits: [fableLimit({ scope: { model: { id: null, display_name: null }, surface: null } })],
});
const report = await new ClaudeQuotaProvider({
logger: console,
accessToken: "fixture-direct",
configDir: "/nonexistent",
platform: "darwin",
claudeKeychainReader: keychain,
fetch: fetchApi as never,
}).fetchUsage();
expect(report.status).toBe("available");
expect(keychain).not.toHaveBeenCalled();
const usage = await provider.fetchUsage();
expect(usage.windows.map((window) => window.id)).toEqual(["weekly"]);
expect(console.warn).toHaveBeenCalled();
});
it("default discovery reads the credential file before keychain", async () => {
const { mkdtempSync, writeFileSync, rmSync } = await import("node:fs");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const { ClaudeQuotaProvider } = await import("./usage.js");
const dir = mkdtempSync(join(tmpdir(), "claude-usage-default-"));
try {
writeFileSync(
join(dir, ".credentials.json"),
JSON.stringify({ claudeAiOauth: { accessToken: "fixture-default" } }),
);
const keychain = vi.fn(async () => ({ claudeAiOauth: { accessToken: "fixture-keychain" } }));
const fetchApi = vi.fn(async (_url: unknown, init: RequestInit) => {
expect(init.headers).toMatchObject({ Authorization: "Bearer fixture-default" });
return new Response(JSON.stringify({ five_hour: { utilization: 7 } }), { status: 200 });
});
const report = await new ClaudeQuotaProvider({
logger: console,
claudeHome: dir,
platform: "darwin",
claudeKeychainReader: keychain,
fetch: fetchApi as never,
}).fetchUsage();
expect(report.status).toBe("available");
expect(keychain).not.toHaveBeenCalled();
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
// Regression: an additive section must never take down data that already parsed.
it("keeps the top-level windows when a limits entry is malformed", async () => {
const { provider } = claudeProvider({
five_hour: { utilization: 6, resets_at: "2026-06-01T21:00:00Z" },
seven_day: { utilization: 23, resets_at: "2026-06-04T00:00:00Z" },
limits: [{ percent: "not-a-kind" }, fableLimit()],
});
describe("Claude non-default config directory", () => {
it("stays unavailable when its credential file is missing without probing keychain", async () => {
const { mkdtempSync, rmSync } = await import("node:fs");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const { ClaudeQuotaProvider } = await import("./usage.js");
const dir = mkdtempSync(join(tmpdir(), "claude-empty-config-"));
try {
const keychain = vi.fn(async () => ({ claudeAiOauth: { accessToken: "fixture-keychain" } }));
const fetchApi = vi.fn();
const report = await new ClaudeQuotaProvider({
logger: console,
configDir: dir,
platform: "darwin",
claudeKeychainReader: keychain,
fetch: fetchApi as never,
}).fetchUsage();
expect(report.status).toBe("unavailable");
expect(keychain).not.toHaveBeenCalled();
expect(fetchApi).not.toHaveBeenCalled();
} finally {
rmSync(dir, { recursive: true, force: true });
}
const usage = await provider.fetchUsage();
expect(usage.status).toBe("available");
expect(usage.windows.map((window) => window.id)).toEqual([
"five_hour",
"weekly",
"weekly_model_fable",
]);
expect(console.warn).toHaveBeenCalled();
});
it("warns when a successful response describes no windows at all", async () => {
const { provider } = claudeProvider({ limits: [] });
const usage = await provider.fetchUsage();
expect(usage.windows).toEqual([]);
expect(console.warn).toHaveBeenCalledWith(
"Claude usage response parsed but produced no windows",
);
});
});
/**
* The reconciliation matrix.
*
* Four review rounds on PR #2303 each found a different hole in how the two
* representations of one scoped limit get combined, because each fix was tested against
* the case that was reported rather than the space of cases. This walks the space:
* every combination of which representation carries the limit, whether the `limits[]`
* entry supplies values, and whether the two descriptions denote the same limit at all.
*/
describe("Claude usage source scoped limit reconciliation", () => {
let claudeHome: string;
beforeEach(() => {
claudeHome = mkdtempSync(join(tmpdir(), "paseo-claude-matrix-"));
});
afterEach(() => {
rmSync(claudeHome, { recursive: true, force: true });
});
const RESETS = "2026-06-04T00:00:00Z";
function scoped(scope: unknown, percent: number | null = 30, resetsAt: string | null = RESETS) {
return { kind: "weekly_scoped", percent, resets_at: resetsAt, scope };
}
const model = (name: string | null, id: string | null = null) => ({
model: { id, display_name: name },
surface: null,
});
const surface = (name: string | null, id: string | null = null) => ({
model: null,
surface: { id, display_name: name },
});
async function windowsFor(body: Record<string, unknown>) {
writeClaudeCredentials(claudeHome, "at_valid");
process.env["CLAUDE_HOME"] = claudeHome;
const usage = await fetchUsage(
{},
mockFetch(new Map([["https://api.anthropic.com/api/oauth/usage", () => jsonResponse(body)]])),
);
return usage.windows;
}
it("which representation carries the limit: legacy only", async () => {
const windows = await windowsFor({
seven_day_omelette: { utilization: 12, resets_at: RESETS },
});
expect(windows).toEqual([
expect.objectContaining({
id: "weekly_model_omelette",
label: "Weekly · Omelette",
usedPct: 12,
}),
]);
});
it("which representation carries the limit: limits[] only", async () => {
const windows = await windowsFor({ limits: [scoped(model("Fable"), 2)] });
expect(windows).toEqual([
expect.objectContaining({
id: "weekly_model_fable",
label: "Weekly · Fable",
usedPct: 2,
}),
]);
});
it("which representation carries the limit: both, same limit — one bar, scoped identity", async () => {
const windows = await windowsFor({
seven_day_omelette: { utilization: 12, resets_at: RESETS },
limits: [scoped(model("Omelette"), 30)],
});
expect(windows).toEqual([
expect.objectContaining({ id: "weekly_model_omelette", usedPct: 30 }),
]);
});
it("which representation carries the limit: both, different limits — two bars", async () => {
const windows = await windowsFor({
seven_day_opus: { utilization: 8, resets_at: RESETS },
limits: [scoped(model("Fable"), 2)],
});
expect(windows.map((w) => w.id)).toEqual(["weekly_model_opus", "weekly_model_fable"]);
});
it("which representation carries the limit: neither", async () => {
const windows = await windowsFor({ seven_day: { utilization: 23, resets_at: RESETS } });
expect(windows.map((w) => w.id)).toEqual(["weekly"]);
});
const legacy = { seven_day_omelette: { utilization: 12, resets_at: RESETS } };
it("value fallback when the scoped entry is sparse: scoped values win when present", async () => {
const windows = await windowsFor({
...legacy,
limits: [scoped(model("Omelette"), 30, "2026-06-09T00:00:00Z")],
});
expect(windows[0]).toMatchObject({ usedPct: 30, resetsAt: "2026-06-09T00:00:00Z" });
});
it("value fallback when the scoped entry is sparse: percentage falls back per field", async () => {
const windows = await windowsFor({
...legacy,
limits: [scoped(model("Omelette"), null, "2026-06-09T00:00:00Z")],
});
expect(windows[0]).toMatchObject({ usedPct: 12, resetsAt: "2026-06-09T00:00:00Z" });
});
it("value fallback when the scoped entry is sparse: reset time falls back per field", async () => {
const windows = await windowsFor({
...legacy,
limits: [scoped(model("Omelette"), 30, null)],
});
expect(windows[0]).toMatchObject({ usedPct: 30, resetsAt: RESETS });
});
it("value fallback when the scoped entry is sparse: both fall back when the scoped entry only names the limit", async () => {
const windows = await windowsFor({
...legacy,
limits: [scoped(model("Omelette"), null, null)],
});
expect(windows[0]).toMatchObject({ usedPct: 12, resetsAt: RESETS });
});
it("value fallback when the scoped entry is sparse: stays empty when neither side has a value", async () => {
const windows = await windowsFor({ limits: [scoped(model("Fable"), null, null)] });
expect(windows[0]).toMatchObject({ id: "weekly_model_fable", usedPct: null });
});
it("identity: a surface never matches a legacy model window of the same name", async () => {
const windows = await windowsFor({
seven_day_omelette: { utilization: 12, resets_at: RESETS },
limits: [scoped(surface("Omelette"), 30)],
});
expect(windows.map((w) => w.id)).toEqual(["weekly_model_omelette", "weekly_surface_omelette"]);
expect(windows[0]).toMatchObject({ usedPct: 12 });
expect(windows[1]).toMatchObject({ usedPct: 30 });
});
it("identity: a model and a surface of the same name stay apart", async () => {
const windows = await windowsFor({
limits: [scoped(model("Code"), 4), scoped(surface("Code"), 9)],
});
expect(windows.map((w) => w.id)).toEqual(["weekly_model_code", "weekly_surface_code"]);
});
it("identity: ids decide when both sides have one", async () => {
const windows = await windowsFor({
limits: [
scoped(model("Fable-Pro", "fable-pro"), 4),
scoped(model("Fable_Pro", "fable_pro"), 9),
],
});
expect(windows.map((w) => w.id)).toEqual(["weekly_model_fable-pro", "weekly_model_fable_pro"]);
});
it("identity: names decide when ids are absent, so indistinguishable entries merge", async () => {
const windows = await windowsFor({
limits: [scoped(model("Fable Pro"), 4), scoped(model("Fable-Pro"), 9)],
});
expect(windows).toEqual([
expect.objectContaining({ id: "weekly_model_fable_pro", usedPct: 9 }),
]);
});
it("identity: a renamed scope keeps its id when the API supplies one", async () => {
const before = await windowsFor({ limits: [scoped(model("Fable", "fable"), 2)] });
const after = await windowsFor({ limits: [scoped(model("Fable 5", "fable"), 2)] });
expect(before[0]?.id).toBe("weekly_model_fable");
expect(after[0]?.id).toBe("weekly_model_fable");
expect(after[0]?.label).toBe("Weekly · Fable 5");
});
it("identity: a limit keeps one id whichever representation carries it", async () => {
const viaLegacy = await windowsFor({
seven_day_omelette: { utilization: 12, resets_at: RESETS },
});
const viaLimits = await windowsFor({ limits: [scoped(model("Omelette"), 12)] });
expect(viaLegacy[0]?.id).toBe(viaLimits[0]?.id);
});
it("ordering and unscoped windows: puts session and weekly ahead of the scoped bars", async () => {
const windows = await windowsFor({
five_hour: { utilization: 6, resets_at: RESETS },
seven_day: { utilization: 23, resets_at: RESETS },
seven_day_opus: { utilization: 8, resets_at: RESETS },
limits: [
{ kind: "session", percent: 6, resets_at: RESETS, scope: null },
{ kind: "weekly_all", percent: 23, resets_at: RESETS, scope: null },
scoped(model("Fable"), 2),
],
});
expect(windows.map((w) => w.id)).toEqual([
"five_hour",
"weekly",
"weekly_model_opus",
"weekly_model_fable",
]);
});
});
@@ -1,3 +1,4 @@
import type { UsageInput } from "../shared/input.js";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, promises as fs } from "node:fs";
@@ -6,17 +7,16 @@ import { join } from "node:path";
import { promisify } from "node:util";
import { z } from "zod";
import {
ApiNumberSchema,
fetchProviderApi,
toneFromUsedPct,
unavailableUsage,
windowFromUsedPct,
type UsageReport,
type UsageWindow,
type UsageDetail,
type UsageApiFetch,
} from "@getpaseo/plugin/server/usage";
const ApiNumberSchema = z.coerce.number().finite();
const execFileAsync = promisify(execFile);
const CLAUDE_KEYCHAIN_TIMEOUT_MS = 2_000;
const CLAUDE_OAUTH_BETA = "oauth-2025-04-20";
@@ -78,16 +78,6 @@ interface ClaudeCredentialRecord {
oauth: { accessToken: string } & NonNullable<ClaudeCredentials["claudeAiOauth"]>;
}
interface ClaudeQuotaProviderOptions {
logger: Console;
claudeHome?: string;
configDir?: string;
accessToken?: string;
claudeKeychainReader?: () => Promise<unknown | null>;
platform?: typeof process.platform;
fetch?: UsageApiFetch;
}
function buildClaudePlan(
subscriptionType: string | undefined,
rateLimitTier: string | undefined,
@@ -337,75 +327,15 @@ export async function readClaudeKeychainCredentials(
return null;
}
export class ClaudeQuotaProvider {
private readonly logger: Console;
private readonly claudeHome: string;
private readonly configDir: string | undefined;
private readonly accessToken: string | undefined;
private readonly readKeychainCredentials: () => Promise<unknown | null>;
private readonly platform: typeof process.platform;
private readonly fetchApi: UsageApiFetch;
constructor(options: ClaudeQuotaProviderOptions) {
this.logger = options.logger;
this.claudeHome =
options.claudeHome || process.env["CLAUDE_HOME"] || join(homedir(), ".claude");
this.configDir = options.configDir;
this.accessToken = options.accessToken;
this.readKeychainCredentials = options.claudeKeychainReader ?? readClaudeKeychainCredentials;
this.platform = options.platform ?? process.platform;
this.fetchApi = options.fetch ?? fetch;
}
async fetchUsage(): Promise<UsageReport> {
const credentials = await this.readCredentials();
if (!credentials) {
return unavailableUsage();
}
const { oauth } = credentials;
const accountKey = createHash("sha256").update(oauth.accessToken).digest("hex");
const plan = buildClaudePlan(oauth.subscriptionType, oauth.rateLimitTier);
const resp = await this.callClaudeApi(oauth.accessToken);
if (resp === "NEEDS_AUTH") {
// Read-only on credentials; the Claude CLI owns refresh. See docs/providers.md.
return unavailableUsage(accountKey);
}
const scoped = reconcileScopedLimits(
legacyScopedLimits(resp),
this.scopedLimitsFromResponse(resp.limits),
);
const windows = [...unscopedWindows(resp), ...scopedWindows(scoped)];
if (windows[0]) windows[0].headline = true;
if (windows.length === 0) {
// The response parsed but described nothing. That silence is how the previous
// shape change went unnoticed, so make it greppable. `warn` and not `debug`
// because file logging defaults to `info`.
this.logger.warn("Claude usage response parsed but produced no windows");
}
const details: UsageDetail[] = [];
const extraUsageEnabled = resp.extra_usage?.is_enabled;
if (extraUsageEnabled !== undefined) {
details.push({
id: "extra_usage",
label: "Extra usage",
value: extraUsageEnabled ? "Enabled" : "Disabled",
});
}
return {
account: { key: accountKey },
status: "available",
planLabel: plan ?? undefined,
windows,
balances: [],
details,
};
}
export async function fetchUsage(
input: UsageInput,
fetchApi: typeof fetch = fetch,
): Promise<UsageReport> {
const claudeHome = process.env["CLAUDE_HOME"] || join(homedir(), ".claude");
const configDir = "configDir" in input ? input.configDir : undefined;
const accessToken = "accessToken" in input ? input.accessToken : undefined;
const readKeychainCredentials = readClaudeKeychainCredentials;
const platform = process.platform;
/**
* Scoped limits carried by `limits[]`.
@@ -413,21 +343,21 @@ export class ClaudeQuotaProvider {
* Entries are validated one at a time so a single malformed or newly-shaped entry
* cannot fail the whole response and take the windows that already parsed with it.
*/
private scopedLimitsFromResponse(limits: ClaudeUsageResponse["limits"]): ScopedLimit[] {
function scopedLimitsFromResponse(limits: ClaudeUsageResponse["limits"]): ScopedLimit[] {
if (!limits) return [];
const parsed: ScopedLimit[] = [];
for (const entry of limits) {
const result = ClaudeLimitSchema.safeParse(entry);
if (!result.success) {
this.logger.warn({ err: result.error }, "Skipping unparseable Claude usage limit entry");
console.warn({ err: result.error }, "Skipping unparseable Claude usage limit entry");
continue;
}
if (result.data.kind !== SCOPED_WEEKLY_KIND) continue;
const limit = scopedLimitFromEntry(result.data);
if (!limit) {
this.logger.warn("Skipping scoped Claude usage limit with no resolvable scope name");
console.warn("Skipping scoped Claude usage limit with no resolvable scope name");
continue;
}
parsed.push(limit);
@@ -435,20 +365,18 @@ export class ClaudeQuotaProvider {
return parsed;
}
private async readCredentials(): Promise<ClaudeCredentialRecord | null> {
if (this.accessToken) return { oauth: { accessToken: this.accessToken } };
if (this.configDir) return this.readCredentialFile(join(this.configDir, ".credentials.json"));
const credPath = join(this.claudeHome, ".credentials.json");
const fileCredentials = await this.readCredentialFile(credPath);
return (
fileCredentials ?? (this.platform === "darwin" ? await this.readKeychainCredential() : null)
);
async function readCredentials(): Promise<ClaudeCredentialRecord | null> {
if (accessToken) return { oauth: { accessToken: accessToken } };
if (configDir) return readCredentialFile(join(configDir, ".credentials.json"));
const credPath = join(claudeHome, ".credentials.json");
const fileCredentials = await readCredentialFile(credPath);
return fileCredentials ?? (platform === "darwin" ? await readKeychainCredential() : null);
}
private async readCredentialFile(path: string): Promise<ClaudeCredentialRecord | null> {
async function readCredentialFile(path: string): Promise<ClaudeCredentialRecord | null> {
if (!existsSync(path)) return null;
try {
return this.toCredentialRecord(
return toCredentialRecord(
ClaudeCredentialsSchema.parse(JSON.parse(await fs.readFile(path, "utf8"))),
);
} catch {
@@ -456,20 +384,21 @@ export class ClaudeQuotaProvider {
}
}
private async readKeychainCredential(): Promise<ClaudeCredentialRecord | null> {
const parsed = ClaudeCredentialsSchema.safeParse(await this.readKeychainCredentials());
return parsed.success ? this.toCredentialRecord(parsed.data) : null;
async function readKeychainCredential(): Promise<ClaudeCredentialRecord | null> {
const parsed = ClaudeCredentialsSchema.safeParse(await readKeychainCredentials());
return parsed.success ? toCredentialRecord(parsed.data) : null;
}
private toCredentialRecord(
function toCredentialRecord(
credentials: z.infer<typeof ClaudeCredentialsSchema>,
): ClaudeCredentialRecord | null {
const oauth = credentials.claudeAiOauth;
return oauth?.accessToken ? { oauth: { ...oauth, accessToken: oauth.accessToken } } : null;
}
private async callClaudeApi(token: string): Promise<ClaudeUsageResponse | "NEEDS_AUTH"> {
const res = await fetchProviderApi(this.fetchApi, "https://api.anthropic.com/api/oauth/usage", {
async function callClaudeApi(token: string): Promise<ClaudeUsageResponse | "NEEDS_AUTH"> {
const res = await fetchApi("https://api.anthropic.com/api/oauth/usage", {
signal: AbortSignal.timeout(15_000),
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
@@ -480,4 +409,52 @@ export class ClaudeQuotaProvider {
if (!res.ok) throw new Error(`Claude usage API returned ${res.status}`);
return ClaudeUsageResponseSchema.parse(await res.json());
}
const credentials = await readCredentials();
if (!credentials) {
return unavailableUsage();
}
const { oauth } = credentials;
const accountKey = createHash("sha256").update(oauth.accessToken).digest("hex");
const plan = buildClaudePlan(oauth.subscriptionType, oauth.rateLimitTier);
const resp = await callClaudeApi(oauth.accessToken);
if (resp === "NEEDS_AUTH") {
// Read-only on credentials; the Claude CLI owns refresh. See docs/providers.md.
return unavailableUsage(accountKey);
}
const scoped = reconcileScopedLimits(
legacyScopedLimits(resp),
scopedLimitsFromResponse(resp.limits),
);
const windows = [...unscopedWindows(resp), ...scopedWindows(scoped)];
if (windows[0]) windows[0].headline = true;
if (windows.length === 0) {
// The response parsed but described nothing. That silence is how the previous
// shape change went unnoticed, so make it greppable. `warn` and not `debug`
// because file logging defaults to `info`.
console.warn("Claude usage response parsed but produced no windows");
}
const details: UsageDetail[] = [];
const extraUsageEnabled = resp.extra_usage?.is_enabled;
if (extraUsageEnabled !== undefined) {
details.push({
id: "extra_usage",
label: "Extra usage",
value: extraUsageEnabled ? "Enabled" : "Disabled",
});
}
return {
account: { key: accountKey },
status: "available",
planLabel: plan ?? undefined,
windows,
balances: [],
details,
};
}
@@ -1,6 +1,6 @@
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { inputSchema } from "./shared/input.js";
import { CopilotQuotaProvider } from "./server/usage.js";
import { fetchUsage } from "./server/usage.js";
export default function contribute(server: PluginServerContext) {
server.registerUsageSource({
@@ -9,7 +9,7 @@ export default function contribute(server: PluginServerContext) {
icon: "icon.svg",
input: inputSchema,
discover: async () => [{}],
fetch: async (_input) => new CopilotQuotaProvider({ logger: console }).fetchUsage(),
fetch: fetchUsage,
});
return () => {};
}
@@ -1 +1 @@
{ "id": "copilot-usage-source", "requirements": { "paseo": ">=0.9.2" } }
{ "id": "copilot-usage-source" }
@@ -0,0 +1,113 @@
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { fetchUsage } from "./usage.js";
import type { UsageReport } from "@getpaseo/plugin/server/usage";
function mockFetch(handlers: Map<string, () => Response>): typeof fetch {
return vi.fn(async (url: RequestInfo | URL) => {
const key = url.toString();
const handler = handlers.get(key);
if (!handler) throw new Error(`Unmocked fetch: ${key}`);
return handler();
}) as unknown as typeof fetch;
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
describe("copilot usage source", () => {
let homeDir: string;
let fetchApi: typeof fetch;
let originalEnv: Record<string, string | undefined>;
beforeEach(() => {
homeDir = mkdtempSync(join(tmpdir(), "usage-home-"));
originalEnv = { ...process.env };
process.env["HOME"] = homeDir;
process.env["USERPROFILE"] = homeDir;
for (const key of [
"APPDATA",
"COPILOT_TOKEN",
"GITHUB_TOKEN",
"GITHUB_PAT",
"CURSOR_ACCESS_TOKEN",
"CURSOR_TOKEN",
"ZAI_API_KEY",
"GLM_API_KEY",
"GROK_API_KEY",
"GROK_TOKEN",
"KIMI_TOKEN",
"KIMI_API_KEY",
"KIMI_CODE_HOME",
"MINIMAX_API_KEY",
"MINIMAX_BASE_URL",
])
delete process.env[key];
fetchApi = mockFetch(new Map());
});
afterEach(() => {
rmSync(homeDir, { recursive: true, force: true });
for (const key of Object.keys(process.env)) if (!(key in originalEnv)) delete process.env[key];
for (const key in originalEnv) process.env[key] = originalEnv[key];
});
function service(
_options: {
platform?: typeof process.platform;
keychain?: () => Promise<unknown | null>;
cursorHomeDir?: string;
kimiHomeDir?: string;
} = {},
) {
return {
listUsage: async () => {
const report = await fetchUsage({}, (url, init) => fetchApi(url, init));
return {
providers: [
{
providerId: "copilot",
...report,
error: report.error ?? null,
planLabel: report.planLabel ?? null,
},
],
};
},
};
}
function findProvider(
result: { providers: Array<{ providerId: string } & UsageReport> },
id: string,
) {
const report = result.providers.find((item) => item.providerId === id);
if (!report) throw new Error(`Missing usage source ${id}`);
return report;
}
it("fetches Copilot usage from COPILOT_TOKEN", async () => {
process.env["COPILOT_TOKEN"] = "copilot_test_token";
fetchApi = mockFetch(
new Map([
[
"https://api.github.com/copilot_internal/user",
() =>
jsonResponse({
copilot_plan: "business",
quota_reset_date: "2026-07-01T00:00:00Z",
}),
],
]),
);
const copilot = findProvider(await service().listUsage(), "copilot");
expect(copilot).toMatchObject({
status: "available",
planLabel: "business",
details: [{ id: "reset", label: "Quota reset", value: "2026-07-01T00:00:00Z" }],
});
});
});
@@ -1,26 +1,24 @@
import type { UsageInput } from "../shared/input.js";
import { existsSync, promises as fs } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { z } from "zod";
import {
ApiOptionalStringSchema,
fetchProviderApi,
unavailableUsage,
type UsageReport,
type UsageDetail,
type UsageApiFetch,
} from "@getpaseo/plugin/server/usage";
const ApiOptionalStringSchema = z.preprocess(
(value) => (value == null ? undefined : value),
z.coerce.string().optional(),
);
const CopilotUsageResponseSchema = z.object({
copilot_plan: ApiOptionalStringSchema,
quota_reset_date: ApiOptionalStringSchema,
});
interface CopilotQuotaProviderOptions {
logger: Console;
fetch?: UsageApiFetch;
}
async function readGithubCliToken(): Promise<string | null> {
const candidates: string[] = [];
if (process.env["APPDATA"]) {
@@ -41,56 +39,47 @@ async function readGithubCliToken(): Promise<string | null> {
return null;
}
export class CopilotQuotaProvider {
private readonly logger: Console;
private readonly fetchApi: UsageApiFetch;
export async function fetchUsage(
input: UsageInput,
fetchApi: typeof fetch = fetch,
): Promise<UsageReport> {
void input;
constructor(options: CopilotQuotaProviderOptions) {
this.logger = options.logger;
this.fetchApi = options.fetch ?? fetch;
const token =
process.env["COPILOT_TOKEN"] ||
process.env["GITHUB_TOKEN"] ||
process.env["GITHUB_PAT"] ||
(await readGithubCliToken());
if (!token) return unavailableUsage();
const res = await fetchApi("https://api.github.com/copilot_internal/user", {
signal: AbortSignal.timeout(15_000),
headers: {
Authorization: `token ${token}`,
Accept: "application/json",
"Editor-Version": "vscode/1.96.2",
"Editor-Plugin-Version": "copilot-chat/0.26.7",
"User-Agent": "GitHubCopilotChat/0.26.7",
"X-Github-Api-Version": "2025-04-01",
},
});
if (!res.ok) {
return unavailableUsage();
}
async fetchUsage(): Promise<UsageReport> {
const token =
process.env["COPILOT_TOKEN"] ||
process.env["GITHUB_TOKEN"] ||
process.env["GITHUB_PAT"] ||
(await readGithubCliToken());
const resp = CopilotUsageResponseSchema.parse(await res.json());
const details: UsageDetail[] = resp.quota_reset_date
? [{ id: "reset", label: "Quota reset", value: resp.quota_reset_date }]
: [];
if (!token) return unavailableUsage();
const res = await fetchProviderApi(
this.fetchApi,
"https://api.github.com/copilot_internal/user",
{
headers: {
Authorization: `token ${token}`,
Accept: "application/json",
"Editor-Version": "vscode/1.96.2",
"Editor-Plugin-Version": "copilot-chat/0.26.7",
"User-Agent": "GitHubCopilotChat/0.26.7",
"X-Github-Api-Version": "2025-04-01",
},
},
);
if (!res.ok) {
this.logger.debug({ status: res.status }, "Copilot usage fetch failed");
return unavailableUsage();
}
const resp = CopilotUsageResponseSchema.parse(await res.json());
const details: UsageDetail[] = resp.quota_reset_date
? [{ id: "reset", label: "Quota reset", value: resp.quota_reset_date }]
: [];
return {
account: { key: "default" },
status: "available",
planLabel: resp.copilot_plan || undefined,
windows: [],
balances: [],
details,
};
}
return {
account: { key: "default" },
status: "available",
planLabel: resp.copilot_plan || undefined,
windows: [],
balances: [],
details,
};
}
@@ -1,2 +1,4 @@
import { z } from "zod";
export const inputSchema = z.object({}).strict();
export type UsageInput = z.infer<typeof inputSchema>;
@@ -1,6 +1,6 @@
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { inputSchema } from "./shared/input.js";
import { CursorQuotaProvider } from "./server/usage.js";
import { fetchUsage } from "./server/usage.js";
export default function contribute(server: PluginServerContext) {
server.registerUsageSource({
@@ -9,7 +9,7 @@ export default function contribute(server: PluginServerContext) {
icon: "icon.svg",
input: inputSchema,
discover: async () => [{}],
fetch: async (_input) => new CursorQuotaProvider({ logger: console }).fetchUsage(),
fetch: fetchUsage,
});
return () => {};
}
@@ -1 +1 @@
{ "id": "cursor-usage-source", "requirements": { "paseo": ">=0.9.2" } }
{ "id": "cursor-usage-source" }
@@ -0,0 +1,260 @@
import { mkdirSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { fetchUsage } from "./usage.js";
import type { UsageReport } from "@getpaseo/plugin/server/usage";
// node:sqlite has no @types/node@20 typings; require it with a narrow local type.
const testRequire = createRequire(import.meta.url);
interface TestSqliteDb {
exec(sql: string): void;
prepare(sql: string): { run(...params: unknown[]): void };
close(): void;
}
// Cursor builds have stored ItemTable values as both TEXT and BLOB. Keys map to the
// real layouts: a plain modern token or the legacy JSON object.
function writeCursorStateDb(homeDir: string, rows: Record<string, string | Uint8Array>): void {
const dir = join(homeDir, ".config", "Cursor", "User", "globalStorage");
mkdirSync(dir, { recursive: true });
const { DatabaseSync } = testRequire("node:sqlite") as {
DatabaseSync: new (path: string) => TestSqliteDb;
};
const db = new DatabaseSync(join(dir, "state.vscdb"));
db.exec("CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB)");
const insert = db.prepare("INSERT INTO ItemTable (key, value) VALUES (?, ?)");
for (const [key, value] of Object.entries(rows)) {
insert.run(key, value);
}
db.close();
}
function writeCursorAuthJson(homeDir: string, accessToken: string): void {
const dir = join(homeDir, ".config", "cursor");
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "auth.json"), JSON.stringify({ accessToken }));
}
function mockFetch(handlers: Map<string, () => Response>): typeof fetch {
return vi.fn(async (url: RequestInfo | URL) => {
const key = url.toString();
const handler = handlers.get(key);
if (!handler) throw new Error(`Unmocked fetch: ${key}`);
return handler();
}) as unknown as typeof fetch;
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
describe("cursor usage source", () => {
let homeDir: string;
let fetchApi: typeof fetch;
let originalEnv: Record<string, string | undefined>;
beforeEach(() => {
homeDir = mkdtempSync(join(tmpdir(), "usage-home-"));
originalEnv = { ...process.env };
process.env["HOME"] = homeDir;
process.env["USERPROFILE"] = homeDir;
for (const key of [
"APPDATA",
"COPILOT_TOKEN",
"GITHUB_TOKEN",
"GITHUB_PAT",
"CURSOR_ACCESS_TOKEN",
"CURSOR_TOKEN",
"ZAI_API_KEY",
"GLM_API_KEY",
"GROK_API_KEY",
"GROK_TOKEN",
"KIMI_TOKEN",
"KIMI_API_KEY",
"KIMI_CODE_HOME",
"MINIMAX_API_KEY",
"MINIMAX_BASE_URL",
])
delete process.env[key];
fetchApi = mockFetch(new Map());
});
afterEach(() => {
rmSync(homeDir, { recursive: true, force: true });
for (const key of Object.keys(process.env)) if (!(key in originalEnv)) delete process.env[key];
for (const key in originalEnv) process.env[key] = originalEnv[key];
});
function service(
_options: {
platform?: typeof process.platform;
keychain?: () => Promise<unknown | null>;
cursorHomeDir?: string;
kimiHomeDir?: string;
} = {},
) {
return {
listUsage: async () => {
const report = await fetchUsage({}, (url, init) => fetchApi(url, init));
return {
providers: [
{
providerId: "cursor",
...report,
error: report.error ?? null,
planLabel: report.planLabel ?? null,
},
],
};
},
};
}
function findProvider(
result: { providers: Array<{ providerId: string } & UsageReport> },
id: string,
) {
const report = result.providers.find((item) => item.providerId === id);
if (!report) throw new Error(`Missing usage source ${id}`);
return report;
}
it("fetches Cursor usage and normalizes malformed billing dates to null", async () => {
process.env["CURSOR_ACCESS_TOKEN"] = "cursor_test_token";
fetchApi = mockFetch(
new Map([
[
"https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage",
() =>
jsonResponse({
planUsage: {
totalSpend: "1500",
includedSpend: "1000",
bonusSpend: "500",
remaining: "2500",
limit: "4000",
},
billingCycleStart: "2026-01-14T12:42:14.000Z",
billingCycleEnd: "not-a-date",
}),
],
]),
);
const cursor = findProvider(await service().listUsage(), "cursor");
expect(cursor).toMatchObject({
status: "available",
balances: [
expect.objectContaining({
id: "plan_usage",
used: 15,
remaining: 25,
limit: 40,
resetsAt: null,
}),
],
});
});
it("reads the Cursor token from the modern cursorAuth/accessToken key in state.vscdb", async () => {
writeCursorStateDb(homeDir, { "cursorAuth/accessToken": "cursor_state_jwt" });
let authorization: string | null = null;
fetchApi = (async (url: RequestInfo | URL, init?: RequestInit) => {
authorization = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null;
return jsonResponse({
planUsage: {
totalSpend: "1500",
includedSpend: "1000",
bonusSpend: "500",
remaining: "2500",
limit: "4000",
},
billingCycleStart: "2026-01-14T12:42:14.000Z",
billingCycleEnd: "2026-02-14T12:42:14.000Z",
});
}) as unknown as typeof fetch;
const cursor = findProvider(await service({ cursorHomeDir: homeDir }).listUsage(), "cursor");
expect(authorization).toBe("Bearer cursor_state_jwt");
expect(cursor).toMatchObject({
status: "available",
balances: [expect.objectContaining({ id: "plan_usage", used: 15, remaining: 25, limit: 40 })],
});
});
it("falls back to the legacy cursorAuthStatus JSON blob when the modern key is absent", async () => {
writeCursorStateDb(homeDir, {
cursorAuthStatus: Buffer.from(JSON.stringify({ accessToken: "cursor_legacy_jwt" }), "utf8"),
});
let authorization: string | null = null;
fetchApi = (async (url: RequestInfo | URL, init?: RequestInit) => {
authorization = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null;
return jsonResponse({
planUsage: { totalSpend: "0", remaining: "100", limit: "100" },
billingCycleStart: null,
billingCycleEnd: null,
});
}) as unknown as typeof fetch;
const cursor = findProvider(await service({ cursorHomeDir: homeDir }).listUsage(), "cursor");
expect(authorization).toBe("Bearer cursor_legacy_jwt");
expect(cursor.status).toBe("available");
});
it("reads the Cursor token from cursor-agent ~/.config/cursor/auth.json when desktop state is absent", async () => {
writeCursorAuthJson(homeDir, "cursor_cli_jwt");
let authorization: string | null = null;
fetchApi = (async (_url: RequestInfo | URL, init?: RequestInit) => {
authorization = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null;
return jsonResponse({
planUsage: {
totalSpend: "1500",
includedSpend: "1000",
bonusSpend: "500",
remaining: "2500",
limit: "4000",
},
billingCycleStart: "2026-01-14T12:42:14.000Z",
billingCycleEnd: "2026-02-14T12:42:14.000Z",
});
}) as unknown as typeof fetch;
const cursor = findProvider(await service({ cursorHomeDir: homeDir }).listUsage(), "cursor");
expect(authorization).toBe("Bearer cursor_cli_jwt");
expect(cursor).toMatchObject({
status: "available",
balances: [expect.objectContaining({ id: "plan_usage", used: 15, remaining: 25, limit: 40 })],
});
});
it("prefers the desktop state.vscdb token over cursor-agent auth.json", async () => {
writeCursorStateDb(homeDir, { "cursorAuth/accessToken": "cursor_desktop_jwt" });
writeCursorAuthJson(homeDir, "cursor_cli_jwt");
let authorization: string | null = null;
fetchApi = (async (_url: RequestInfo | URL, init?: RequestInit) => {
authorization = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null;
return jsonResponse({
planUsage: { totalSpend: "0", remaining: "100", limit: "100" },
billingCycleStart: null,
billingCycleEnd: null,
});
}) as unknown as typeof fetch;
const cursor = findProvider(await service({ cursorHomeDir: homeDir }).listUsage(), "cursor");
expect(authorization).toBe("Bearer cursor_desktop_jwt");
expect(cursor.status).toBe("available");
});
it("stays unavailable when state.vscdb is unreadable", async () => {
const path = join(homeDir, ".config", "Cursor", "User", "globalStorage");
mkdirSync(path, { recursive: true });
writeFileSync(join(path, "state.vscdb"), "not a database");
const cursor = findProvider(await service().listUsage(), "cursor");
expect(cursor.status).toBe("unavailable");
});
});
@@ -1,20 +1,27 @@
import type { UsageInput } from "../shared/input.js";
import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import { z } from "zod";
import {
ApiNullableNumberSchema,
toneFromUsedPct,
usedPctOf,
fetchProviderApi,
toIsoStringOrNull,
unavailableUsage,
type UsageReport,
type UsageBalance,
type UsageApiFetch,
} from "@getpaseo/plugin/server/usage";
const ApiNumberSchema = z.coerce.number().finite();
const ApiNullableNumberSchema = z.preprocess(
(value) => (value == null ? null : value),
ApiNumberSchema.nullable(),
);
function toIsoStringOrNull(timestampMs: number): string | null {
const date = new Date(timestampMs);
return Number.isFinite(date.getTime()) ? date.toISOString() : null;
}
// Cursor desktop stores auth in VS Code's ItemTable (state.vscdb). Modern builds keep
// the access token as a plain JWT string under `cursorAuth/accessToken`; older builds
// kept a JSON blob under `cursorAuthStatus`. Read it with node:sqlite so we don't
@@ -62,12 +69,6 @@ const CursorAuthStatusSchema = z.object({
type CursorUsageResponse = z.infer<typeof CursorUsageResponseSchema>;
interface CursorQuotaProviderOptions {
logger: Console;
fetch?: UsageApiFetch;
homeDir?: string;
}
function parseCursorBillingCycleTimestamp(
value: CursorUsageResponse["billingCycleStart"],
): string | null {
@@ -113,7 +114,7 @@ function cursorTokenFromDb(db: CursorStateDatabase): string | null {
return null;
}
async function readCursorTokenFromSqlite(homeDir: string, logger: Console): Promise<string | null> {
async function readCursorTokenFromSqlite(homeDir: string): Promise<string | null> {
const dbPaths: string[] = [];
if (process.env["APPDATA"]) {
dbPaths.push(join(process.env["APPDATA"], "Cursor", "User", "globalStorage", "state.vscdb"));
@@ -137,8 +138,7 @@ async function readCursorTokenFromSqlite(homeDir: string, logger: Console): Prom
let sqlite: NodeSqliteModule;
try {
sqlite = (await import(sqliteSpecifier)) as unknown as NodeSqliteModule;
} catch (err) {
logger.debug({ err }, "node:sqlite unavailable; cannot read Cursor state.vscdb");
} catch {
return null; // runtime without node:sqlite
}
@@ -149,10 +149,8 @@ async function readCursorTokenFromSqlite(homeDir: string, logger: Console): Prom
db = new sqlite.DatabaseSync(path, { readOnly: true });
const token = cursorTokenFromDb(db);
if (token) return token;
} catch (err) {
// Locked/permission/corrupt/schema failures all land here; log so an
// unavailable Cursor card is diagnosable, then try the next candidate.
logger.debug({ err, path }, "Failed to read Cursor token from state.vscdb");
} catch {
// Locked, permission, corrupt, or schema failures try the next candidate.
} finally {
db?.close();
}
@@ -160,86 +158,75 @@ async function readCursorTokenFromSqlite(homeDir: string, logger: Console): Prom
return null;
}
async function readCursorTokenFromAuthJson(
homeDir: string,
logger: Console,
): Promise<string | null> {
async function readCursorTokenFromAuthJson(homeDir: string): Promise<string | null> {
const path = join(homeDir, ".config", "cursor", "auth.json");
if (!existsSync(path)) return null;
try {
const parsed = CursorAuthStatusSchema.parse(JSON.parse(await readFile(path, "utf8")));
return parsed.accessToken?.trim() || null;
} catch (err) {
logger.debug({ err, path }, "Failed to read Cursor token from auth.json");
} catch {
return null;
}
}
export class CursorQuotaProvider {
private readonly logger: Console;
private readonly fetchApi: UsageApiFetch;
private readonly homeDir: string;
export async function fetchUsage(
input: UsageInput,
fetchApi: typeof fetch = fetch,
): Promise<UsageReport> {
void input;
const homeDir = homedir();
constructor(options: CursorQuotaProviderOptions) {
this.logger = options.logger;
this.fetchApi = options.fetch ?? fetch;
this.homeDir = options.homeDir ?? homedir();
}
const token =
process.env["CURSOR_ACCESS_TOKEN"] ||
process.env["CURSOR_TOKEN"] ||
(await readCursorTokenFromSqlite(homeDir)) ||
(await readCursorTokenFromAuthJson(homeDir));
async fetchUsage(): Promise<UsageReport> {
const token =
process.env["CURSOR_ACCESS_TOKEN"] ||
process.env["CURSOR_TOKEN"] ||
(await readCursorTokenFromSqlite(this.homeDir, this.logger)) ||
(await readCursorTokenFromAuthJson(this.homeDir, this.logger));
if (!token) return unavailableUsage();
if (!token) return unavailableUsage();
const res = await fetchProviderApi(
this.fetchApi,
"https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage",
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"Connect-Protocol-Version": "1",
},
body: JSON.stringify({}),
const res = await fetchApi(
"https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage",
{
signal: AbortSignal.timeout(15_000),
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"Connect-Protocol-Version": "1",
},
);
body: JSON.stringify({}),
},
);
if (!res.ok) {
this.logger.debug({ status: res.status }, "Cursor usage fetch failed");
return unavailableUsage();
}
const resp = CursorUsageResponseSchema.parse(await res.json());
const billingCycleEnd = parseCursorBillingCycleTimestamp(resp.billingCycleEnd);
const balances: UsageBalance[] = [];
if (resp.planUsage) {
const totalSpend = centsToDollars(resp.planUsage.totalSpend);
const remaining = centsToDollars(resp.planUsage.remaining);
const limit = centsToDollars(resp.planUsage.limit);
balances.push({
id: "plan_usage",
label: "Plan usage",
used: totalSpend,
remaining,
limit,
unit: "usd",
resetsAt: billingCycleEnd,
tone: toneFromUsedPct(usedPctOf(totalSpend, limit)),
});
}
return {
account: { key: "default" },
status: "available",
planLabel: undefined,
windows: [],
balances,
details: [],
};
if (!res.ok) {
return unavailableUsage();
}
const resp = CursorUsageResponseSchema.parse(await res.json());
const billingCycleEnd = parseCursorBillingCycleTimestamp(resp.billingCycleEnd);
const balances: UsageBalance[] = [];
if (resp.planUsage) {
const totalSpend = centsToDollars(resp.planUsage.totalSpend);
const remaining = centsToDollars(resp.planUsage.remaining);
const limit = centsToDollars(resp.planUsage.limit);
balances.push({
id: "plan_usage",
label: "Plan usage",
used: totalSpend,
remaining,
limit,
unit: "usd",
resetsAt: billingCycleEnd,
tone: toneFromUsedPct(usedPctOf(totalSpend, limit)),
});
}
return {
account: { key: "default" },
status: "available",
planLabel: undefined,
windows: [],
balances,
details: [],
};
}
@@ -1,2 +1,4 @@
import { z } from "zod";
export const inputSchema = z.object({}).strict();
export type UsageInput = z.infer<typeof inputSchema>;
@@ -1,6 +1,6 @@
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { inputSchema } from "./shared/input.js";
import { GrokQuotaProvider } from "./server/usage.js";
import { fetchUsage } from "./server/usage.js";
export default function contribute(server: PluginServerContext) {
server.registerUsageSource({
@@ -9,7 +9,7 @@ export default function contribute(server: PluginServerContext) {
icon: "icon.svg",
input: inputSchema,
discover: async () => [{}],
fetch: async (_input) => new GrokQuotaProvider({ logger: console }).fetchUsage(),
fetch: fetchUsage,
});
return () => {};
}
@@ -1 +1 @@
{ "id": "grok-usage-source", "requirements": { "paseo": ">=0.9.2" } }
{ "id": "grok-usage-source" }
@@ -0,0 +1,268 @@
import { mkdirSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { fetchUsage } from "./usage.js";
import type { UsageReport } from "@getpaseo/plugin/server/usage";
function writeGrokAuth(home: string, auth: Record<string, unknown>): void {
mkdirSync(join(home, ".grok"), { recursive: true });
writeFileSync(join(home, ".grok", "auth.json"), JSON.stringify(auth));
}
function mockFetch(handlers: Map<string, () => Response>): typeof fetch {
return vi.fn(async (url: RequestInfo | URL) => {
const key = url.toString();
const handler = handlers.get(key);
if (!handler) throw new Error(`Unmocked fetch: ${key}`);
return handler();
}) as unknown as typeof fetch;
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
describe("grok usage source", () => {
let homeDir: string;
let fetchApi: typeof fetch;
let originalEnv: Record<string, string | undefined>;
beforeEach(() => {
homeDir = mkdtempSync(join(tmpdir(), "usage-home-"));
originalEnv = { ...process.env };
process.env["HOME"] = homeDir;
process.env["USERPROFILE"] = homeDir;
for (const key of [
"APPDATA",
"COPILOT_TOKEN",
"GITHUB_TOKEN",
"GITHUB_PAT",
"CURSOR_ACCESS_TOKEN",
"CURSOR_TOKEN",
"ZAI_API_KEY",
"GLM_API_KEY",
"GROK_API_KEY",
"GROK_TOKEN",
"KIMI_TOKEN",
"KIMI_API_KEY",
"KIMI_CODE_HOME",
"MINIMAX_API_KEY",
"MINIMAX_BASE_URL",
])
delete process.env[key];
fetchApi = mockFetch(new Map());
});
afterEach(() => {
rmSync(homeDir, { recursive: true, force: true });
for (const key of Object.keys(process.env)) if (!(key in originalEnv)) delete process.env[key];
for (const key in originalEnv) process.env[key] = originalEnv[key];
});
function service(
_options: {
platform?: typeof process.platform;
keychain?: () => Promise<unknown | null>;
cursorHomeDir?: string;
kimiHomeDir?: string;
} = {},
) {
return {
listUsage: async () => {
const report = await fetchUsage({}, (url, init) => fetchApi(url, init));
return {
providers: [
{
providerId: "grok",
...report,
error: report.error ?? null,
planLabel: report.planLabel ?? null,
},
],
};
},
};
}
function findProvider(
result: { providers: Array<{ providerId: string } & UsageReport> },
id: string,
) {
const report = result.providers.find((item) => item.providerId === id);
if (!report) throw new Error(`Missing usage source ${id}`);
return report;
}
it("fetches Grok usage and preserves zero values", async () => {
process.env["GROK_API_KEY"] = "grok_test_token";
fetchApi = mockFetch(
new Map([
[
"https://cli-chat-proxy.grok.com/v1/billing?format=credits",
() =>
jsonResponse({
config: { monthlyLimit: { val: 0 }, used: { val: 0 } },
}),
],
]),
);
const grok = findProvider(await service().listUsage(), "grok");
expect(grok).toMatchObject({
status: "available",
balances: [
expect.objectContaining({
id: "monthly_credits",
used: 0,
remaining: 0,
limit: 0,
}),
],
});
});
it("fetches Grok usage from live billing shape (config.used.val)", async () => {
process.env["GROK_API_KEY"] = "grok_test_token";
fetchApi = mockFetch(
new Map([
[
"https://cli-chat-proxy.grok.com/v1/billing?format=credits",
() =>
jsonResponse({
config: {
monthlyLimit: { val: 150000 },
used: { val: 37886 },
billingPeriodStart: "2026-07-01T00:00:00+00:00",
billingPeriodEnd: "2026-08-01T00:00:00+00:00",
},
}),
],
]),
);
const grok = findProvider(await service().listUsage(), "grok");
expect(grok).toMatchObject({
status: "available",
balances: [
expect.objectContaining({
id: "monthly_credits",
used: 37886,
remaining: 112114,
limit: 150000,
unit: "credits",
}),
],
});
});
it("fetches Grok usage with nested ~/.grok/auth.json key token", async () => {
writeGrokAuth(homeDir, {
"https://auth.x.ai::test-user-id": {
key: "nested_jwt_token",
refresh_token: "rt_nested",
expires_at: "2026-08-01T00:00:00Z",
user_id: "test-user-id",
email: "user@example.com",
},
});
let authorization: string | null = null;
fetchApi = (async (_url: RequestInfo | URL, init?: RequestInit) => {
authorization = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null;
return jsonResponse({
config: {
monthlyLimit: { val: 100 },
used: { val: 25 },
},
});
}) as typeof fetch;
const grok = findProvider(await service().listUsage(), "grok");
expect(authorization).toBe("Bearer nested_jwt_token");
expect(grok).toMatchObject({
status: "available",
balances: [
expect.objectContaining({
id: "monthly_credits",
used: 25,
remaining: 75,
limit: 100,
}),
],
});
});
it("still accepts legacy Grok usage.creditUsage when config.used is absent", async () => {
process.env["GROK_API_KEY"] = "grok_test_token";
fetchApi = mockFetch(
new Map([
[
"https://cli-chat-proxy.grok.com/v1/billing?format=credits",
() =>
jsonResponse({
config: { monthlyLimit: { val: 50 } },
usage: { creditUsage: 10 },
}),
],
]),
);
const grok = findProvider(await service().listUsage(), "grok");
expect(grok).toMatchObject({
status: "available",
balances: [
expect.objectContaining({
id: "monthly_credits",
used: 10,
remaining: 40,
limit: 50,
}),
],
});
});
it("fetches Grok unified-billing usage as a weekly window", async () => {
process.env["GROK_API_KEY"] = "grok_test_token";
fetchApi = mockFetch(
new Map([
[
"https://cli-chat-proxy.grok.com/v1/billing?format=credits",
() =>
jsonResponse({
config: {
currentPeriod: {
type: "USAGE_PERIOD_TYPE_WEEKLY",
start: "2026-08-24T09:41:40.001370+00:00",
end: "2026-08-31T09:41:40.001370+00:00",
},
creditUsagePercent: 76.0,
isUnifiedBillingUser: true,
billingPeriodStart: "2026-08-24T09:41:40.001370+00:00",
billingPeriodEnd: "2026-08-31T09:41:40.001370+00:00",
},
}),
],
]),
);
const grok = findProvider(await service().listUsage(), "grok");
expect(grok).toMatchObject({
status: "available",
windows: [
{
id: "weekly",
label: "Weekly",
usedPct: 76,
remainingPct: 24,
resetsAt: "2026-08-31T09:41:40.001370+00:00",
tone: "warning",
},
],
balances: [],
});
});
});
@@ -1,21 +1,24 @@
import type { UsageInput } from "../shared/input.js";
import { existsSync, promises as fs } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { z } from "zod";
import {
ApiNumberSchema,
ApiOptionalStringSchema,
toneFromUsedPct,
usedPctOf,
fetchProviderApi,
unavailableUsage,
windowFromUsedPct,
type UsageReport,
type UsageWindow,
type UsageBalance,
type UsageApiFetch,
} from "@getpaseo/plugin/server/usage";
const ApiNumberSchema = z.coerce.number().finite();
const ApiOptionalStringSchema = z.preprocess(
(value) => (value == null ? undefined : value),
z.coerce.string().optional(),
);
const GrokUsageResponseSchema = z.object({
config: z
.object({
@@ -45,13 +48,6 @@ const GrokUsageResponseSchema = z.object({
.nullish(),
});
interface GrokQuotaProviderOptions {
logger: Console;
fetch?: UsageApiFetch;
/** Override home directory (tests). Production uses os.homedir(). */
homeDir?: string;
}
/** Resolve a Grok CLI token from ~/.grok/auth.json (legacy or current nested shape). */
export function extractGrokTokenFromAuth(auth: unknown): string | null {
if (auth == null || typeof auth !== "object" || Array.isArray(auth)) return null;
@@ -108,60 +104,16 @@ function grokUsageWindow(response: z.infer<typeof GrokUsageResponseSchema>): Usa
});
}
export class GrokQuotaProvider {
private readonly logger: Console;
private readonly fetchApi: UsageApiFetch;
private readonly homeDir: string | undefined;
export async function fetchUsage(
input: UsageInput,
fetchApi: typeof fetch = fetch,
): Promise<UsageReport> {
void input;
const homeDir = homedir();
constructor(options: GrokQuotaProviderOptions) {
this.logger = options.logger;
this.fetchApi = options.fetch ?? fetch;
this.homeDir = options.homeDir;
}
async fetchUsage(): Promise<UsageReport> {
const token =
process.env["GROK_API_KEY"] || process.env["GROK_TOKEN"] || (await this.readGrokToken());
if (!token) return unavailableUsage();
// The Grok CLI's /usage uses ?format=credits; without it, unified-billing accounts
// get a zeroed legacy monthly shape (monthlyLimit.val 0) instead of real usage.
const res = await fetchProviderApi(
this.fetchApi,
"https://cli-chat-proxy.grok.com/v1/billing?format=credits",
{
headers: {
Authorization: `Bearer ${token}`,
"X-XAI-Token-Auth": "xai-grok-cli",
Accept: "application/json",
},
},
);
if (!res.ok) {
this.logger.debug({ status: res.status }, "Grok usage fetch failed");
return unavailableUsage();
}
const resp = GrokUsageResponseSchema.parse(await res.json());
const balance = grokMonthlyCreditBalance(resp);
const window = grokUsageWindow(resp);
if (window) window.headline = true;
return {
account: { key: "default" },
status: "available",
planLabel: undefined,
windows: window ? [window] : [],
balances: balance ? [balance] : [],
details: [],
};
}
private async readGrokToken(): Promise<string | null> {
async function readGrokToken(): Promise<string | null> {
// homeDir override is for tests: Windows os.homedir() ignores $HOME (uses USERPROFILE).
const path = join(this.homeDir ?? homedir(), ".grok", "auth.json");
const path = join(homeDir ?? homedir(), ".grok", "auth.json");
if (!existsSync(path)) return null;
try {
return extractGrokTokenFromAuth(JSON.parse(await fs.readFile(path, "utf8")));
@@ -169,4 +121,37 @@ export class GrokQuotaProvider {
return null;
}
}
const token = process.env["GROK_API_KEY"] || process.env["GROK_TOKEN"] || (await readGrokToken());
if (!token) return unavailableUsage();
// The Grok CLI's /usage uses ?format=credits; without it, unified-billing accounts
// get a zeroed legacy monthly shape (monthlyLimit.val 0) instead of real usage.
const res = await fetchApi("https://cli-chat-proxy.grok.com/v1/billing?format=credits", {
signal: AbortSignal.timeout(15_000),
headers: {
Authorization: `Bearer ${token}`,
"X-XAI-Token-Auth": "xai-grok-cli",
Accept: "application/json",
},
});
if (!res.ok) {
return unavailableUsage();
}
const resp = GrokUsageResponseSchema.parse(await res.json());
const balance = grokMonthlyCreditBalance(resp);
const window = grokUsageWindow(resp);
if (window) window.headline = true;
return {
account: { key: "default" },
status: "available",
planLabel: undefined,
windows: window ? [window] : [],
balances: balance ? [balance] : [],
details: [],
};
}
@@ -1,2 +1,4 @@
import { z } from "zod";
export const inputSchema = z.object({}).strict();
export type UsageInput = z.infer<typeof inputSchema>;
@@ -1,6 +1,6 @@
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { inputSchema } from "./shared/input.js";
import { KimiQuotaProvider } from "./server/usage.js";
import { fetchUsage } from "./server/usage.js";
export default function contribute(server: PluginServerContext) {
server.registerUsageSource({
@@ -9,7 +9,7 @@ export default function contribute(server: PluginServerContext) {
icon: "icon.svg",
input: inputSchema,
discover: async () => [{}],
fetch: async (_input) => new KimiQuotaProvider({ logger: console }).fetchUsage(),
fetch: fetchUsage,
});
return () => {};
}
@@ -1 +1 @@
{ "id": "kimi-usage-source", "requirements": { "paseo": ">=0.9.2" } }
{ "id": "kimi-usage-source" }
@@ -0,0 +1,370 @@
import { mkdirSync, readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { fetchUsage } from "./usage.js";
import type { UsageReport } from "@getpaseo/plugin/server/usage";
function kimiCredentialPath(dir: string): string {
return join(dir, "credentials", "kimi-code.json");
}
function writeKimiCredentials(dir: string, accessToken: string, overrides: object = {}): void {
mkdirSync(join(dir, "credentials"), { recursive: true });
writeFileSync(
kimiCredentialPath(dir),
JSON.stringify({
access_token: accessToken,
refresh_token: "rt_kimi",
expires_at: 1_798_812_800,
expires_in: 900,
scope: "kimi-code",
token_type: "Bearer",
...overrides,
}),
);
}
function mockFetch(handlers: Map<string, () => Response>): typeof fetch {
return vi.fn(async (url: RequestInfo | URL) => {
const key = url.toString();
const handler = handlers.get(key);
if (!handler) throw new Error(`Unmocked fetch: ${key}`);
return handler();
}) as unknown as typeof fetch;
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
describe("kimi usage source", () => {
let homeDir: string;
let fetchApi: typeof fetch;
let originalEnv: Record<string, string | undefined>;
beforeEach(() => {
homeDir = mkdtempSync(join(tmpdir(), "usage-home-"));
originalEnv = { ...process.env };
process.env["HOME"] = homeDir;
process.env["USERPROFILE"] = homeDir;
for (const key of [
"APPDATA",
"COPILOT_TOKEN",
"GITHUB_TOKEN",
"GITHUB_PAT",
"CURSOR_ACCESS_TOKEN",
"CURSOR_TOKEN",
"ZAI_API_KEY",
"GLM_API_KEY",
"GROK_API_KEY",
"GROK_TOKEN",
"KIMI_TOKEN",
"KIMI_API_KEY",
"KIMI_CODE_HOME",
"MINIMAX_API_KEY",
"MINIMAX_BASE_URL",
])
delete process.env[key];
fetchApi = mockFetch(new Map());
});
afterEach(() => {
rmSync(homeDir, { recursive: true, force: true });
for (const key of Object.keys(process.env)) if (!(key in originalEnv)) delete process.env[key];
for (const key in originalEnv) process.env[key] = originalEnv[key];
});
function service(
_options: {
platform?: typeof process.platform;
keychain?: () => Promise<unknown | null>;
cursorHomeDir?: string;
kimiHomeDir?: string;
} = {},
) {
return {
listUsage: async () => {
const report = await fetchUsage({}, (url, init) => fetchApi(url, init));
return {
providers: [
{
providerId: "kimi",
...report,
error: report.error ?? null,
planLabel: report.planLabel ?? null,
},
],
};
},
};
}
function findProvider(
result: { providers: Array<{ providerId: string } & UsageReport> },
id: string,
) {
const report = result.providers.find((item) => item.providerId === id);
if (!report) throw new Error(`Missing usage source ${id}`);
return report;
}
it("fetches Kimi usage from KIMI_TOKEN", async () => {
process.env["KIMI_TOKEN"] = "kimi_test_token";
fetchApi = mockFetch(
new Map([
[
"https://api.kimi.com/coding/v1/usages",
() =>
jsonResponse({
usage: {
limit: "100",
remaining: "74",
resetTime: "2026-02-11T17:32:50Z",
},
}),
],
]),
);
const kimi = findProvider(await service().listUsage(), "kimi");
expect(kimi).toMatchObject({
status: "available",
windows: [
expect.objectContaining({
id: "coding_usage",
usedPct: 26,
remainingPct: 74,
resetsAt: "2026-02-11T17:32:50Z",
}),
],
});
});
it("fetches Kimi usage from the CLI credential home", async () => {
writeKimiCredentials(join(homeDir, ".kimi-code"), "kimi_cli_token");
let requestedUrl: string | null = null;
let authorization: string | null = null;
fetchApi = (async (url: RequestInfo | URL, init?: RequestInit) => {
requestedUrl = url.toString();
authorization = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null;
return jsonResponse({
usage: {
limit: "200",
remaining: "150",
resetTime: "2026-06-23T05:12:17Z",
},
});
}) as unknown as typeof fetch;
const kimi = findProvider(await service({ kimiHomeDir: homeDir }).listUsage(), "kimi");
expect(requestedUrl).toBe("https://api.kimi.com/coding/v1/usages");
expect(authorization).toBe("Bearer kimi_cli_token");
expect(kimi).toMatchObject({
status: "available",
windows: [
expect.objectContaining({
id: "coding_usage",
usedPct: 25,
remainingPct: 75,
resetsAt: "2026-06-23T05:12:17Z",
}),
],
});
});
it("reads Kimi credentials whose optional fields are null", async () => {
writeKimiCredentials(join(homeDir, ".kimi-code"), "kimi_cli_token", {
expires_at: null,
expires_in: null,
scope: null,
token_type: null,
});
fetchApi = mockFetch(
new Map([
[
"https://api.kimi.com/coding/v1/usages",
() => jsonResponse({ usage: { limit: "100", remaining: "60" } }),
],
]),
);
const kimi = findProvider(await service({ kimiHomeDir: homeDir }).listUsage(), "kimi");
expect(kimi.status).toBe("available");
});
it("returns unavailable on 401 without refreshing or rewriting the credential file", async () => {
writeKimiCredentials(join(homeDir, ".kimi-code"), "at_kimi_expired");
const credPath = kimiCredentialPath(join(homeDir, ".kimi-code"));
const before = readFileSync(credPath, "utf8");
let usageCalls = 0;
fetchApi = vi.fn(async (url: RequestInfo | URL) => {
const endpoint = url.toString();
if (endpoint === "https://api.kimi.com/coding/v1/usages") {
usageCalls += 1;
return new Response(null, { status: 401 });
}
// The read-only fetcher must never hit the OAuth token endpoint.
throw new Error(`Unmocked: ${endpoint}`);
}) as never;
const result = await service({ kimiHomeDir: homeDir }).listUsage();
expect(findProvider(result, "kimi").status).toBe("unavailable");
expect(usageCalls).toBe(1);
// The credentials file must be left untouched for the Kimi CLI to own.
expect(readFileSync(credPath, "utf8")).toBe(before);
});
it("does not refresh Kimi tokens read from the environment", async () => {
process.env["KIMI_TOKEN"] = "kimi_test_token";
const usageFetch = vi.fn(async () => new Response(null, { status: 401 }));
fetchApi = usageFetch as never;
const result = await service().listUsage();
expect(findProvider(result, "kimi").status).toBe("unavailable");
expect(usageFetch).toHaveBeenCalledTimes(1);
});
it("does not refresh Kimi tokens on a 403", async () => {
writeKimiCredentials(join(homeDir, ".kimi-code"), "at_kimi_forbidden");
const usageFetch = vi.fn(async () => new Response(null, { status: 403 }));
fetchApi = usageFetch as never;
const result = await service({ kimiHomeDir: homeDir }).listUsage();
expect(findProvider(result, "kimi").status).toBe("unavailable");
expect(usageFetch).toHaveBeenCalledTimes(1);
});
});
describe("Kimi usage source usage windows", () => {
afterEach(() => {
delete process.env["KIMI_TOKEN"];
vi.restoreAllMocks();
});
it("normalizes weekly and enforced rolling usage windows", async () => {
process.env["KIMI_TOKEN"] = "kimi_test_token";
const fetchApi = vi.fn(async () =>
jsonResponse({
limited: true,
usage: {
limit: "100",
used: "61",
remaining: "39",
resetTime: "2026-08-05T00:01:45Z",
},
limits: [
{
window: {
duration: 300,
timeUnit: "TIME_UNIT_MINUTE",
},
detail: {
limit: "100",
used: "100",
resetTime: "2026-07-31T17:01:45Z",
},
},
],
}),
);
const usage = await fetchUsage({}, fetchApi);
expect(usage).toMatchObject({
status: "available",
windows: [
{
id: "coding_usage",
label: "Weekly limit",
usedPct: 61,
remainingPct: 39,
resetsAt: "2026-08-05T00:01:45Z",
tone: "ok",
},
{
id: "coding_limit_300_time_unit_minute",
label: "5-hour limit",
usedPct: 100,
remainingPct: 0,
resetsAt: "2026-07-31T17:01:45Z",
tone: "danger",
},
],
});
});
it("keeps valid windows when another limits entry is malformed", async () => {
process.env["KIMI_TOKEN"] = "kimi_test_token";
const fetchApi = vi.fn(async () =>
jsonResponse({
usage: {
limit: "100",
remaining: "75",
resetTime: "2026-08-05T00:01:45Z",
},
limits: [
{ window: { duration: "invalid" }, detail: {} },
{
window: { duration: 300, timeUnit: "TIME_UNIT_MINUTE" },
detail: { limit: "100", remaining: "50" },
},
],
}),
);
const usage = await fetchUsage({}, fetchApi);
expect(usage.windows).toHaveLength(2);
expect(usage.windows[1]).toMatchObject({
label: "5-hour limit",
usedPct: 50,
remainingPct: 50,
});
});
it("accepts direct limit fields, alternate reset keys, and provider labels", async () => {
process.env["KIMI_TOKEN"] = "kimi_test_token";
const fetchApi = vi.fn(async () =>
jsonResponse({
usage: null,
limits: [
{
name: "Burst quota",
limit: "80",
remaining: "20",
reset_at: "2026-08-01T00:00:00Z",
},
],
}),
);
const usage = await fetchUsage({}, fetchApi);
expect(usage.windows).toEqual([
expect.objectContaining({
id: "coding_limit_burst_quota",
label: "Burst quota",
usedPct: 75,
remainingPct: 25,
resetsAt: "2026-08-01T00:00:00Z",
}),
]);
});
it("keeps window ids unique when Kimi returns duplicate limit descriptors", async () => {
process.env["KIMI_TOKEN"] = "kimi_test_token";
const duplicate = {
window: { duration: 300, timeUnit: "TIME_UNIT_MINUTE" },
detail: { limit: "100", used: "10" },
};
const fetchApi = vi.fn(async () => jsonResponse({ limits: [duplicate, duplicate] }));
const usage = await fetchUsage({}, fetchApi);
expect(usage.windows.map((window) => window.id)).toEqual([
"coding_limit_300_time_unit_minute",
"coding_limit_300_time_unit_minute_2",
]);
});
});
@@ -1,19 +1,22 @@
import type { UsageInput } from "../shared/input.js";
import { existsSync, promises as fs } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { z } from "zod";
import {
ApiNumberSchema,
ApiOptionalStringSchema,
fetchProviderApi,
toneFromUsedPct,
unavailableUsage,
windowFromUsedPct,
type UsageReport,
type UsageWindow,
type UsageApiFetch,
} from "@getpaseo/plugin/server/usage";
const ApiNumberSchema = z.coerce.number().finite();
const ApiOptionalStringSchema = z.preprocess(
(value) => (value == null ? undefined : value),
z.coerce.string().optional(),
);
const KIMI_USAGE_URL = "https://api.kimi.com/coding/v1/usages";
const KimiUsageFieldsSchema = z.object({
@@ -187,10 +190,7 @@ function uniqueWindowId(baseId: string, seenIds: Set<string>): string {
return id;
}
function kimiUsageWindowsFromPayload(
payload: unknown,
logger: Pick<Console, "debug">,
): UsageWindow[] {
function kimiUsageWindowsFromPayload(payload: unknown): UsageWindow[] {
const response = KimiUsageResponseSchema.parse(payload);
const windows: UsageWindow[] = [];
const seenWindowIds = new Set<string>();
@@ -210,14 +210,12 @@ function kimiUsageWindowsFromPayload(
for (const [index, rawLimit] of limits.entries()) {
const parsedLimit = KimiUsageLimitSchema.safeParse(rawLimit);
if (!parsedLimit.success) {
logger.debug({ index }, "Ignoring malformed Kimi usage limit window");
continue;
}
const limit = parsedLimit.data;
const { fields, metadata } = limitFields(limit);
if (!hasUsageData(fields)) {
logger.debug({ index }, "Ignoring malformed Kimi usage limit window");
continue;
}
@@ -249,50 +247,16 @@ const KimiAuthSchema = z
type KimiAuth = z.infer<typeof KimiAuthSchema>;
type KimiCredentials = KimiAuth & { access_token: string };
interface KimiQuotaProviderOptions {
logger: Console;
fetch?: UsageApiFetch;
homeDir?: string;
}
export async function fetchUsage(
input: UsageInput,
fetchApi: typeof fetch = fetch,
): Promise<UsageReport> {
void input;
const homeDir = homedir();
export class KimiQuotaProvider {
private readonly logger: Console;
private readonly fetchApi: UsageApiFetch;
private readonly homeDir?: string;
constructor(options: KimiQuotaProviderOptions) {
this.logger = options.logger;
this.fetchApi = options.fetch ?? fetch;
this.homeDir = options.homeDir;
}
async fetchUsage(): Promise<UsageReport> {
const credentials = await this.readCredentials();
if (!credentials) return unavailableUsage();
const res = await this.callUsageApi(credentials.access_token);
if (!res.ok) {
// Read-only on credentials; the Kimi CLI owns refresh. See docs/providers.md.
this.logger.debug({ status: res.status }, "Kimi usage fetch failed");
return unavailableUsage();
}
const windows = kimiUsageWindowsFromPayload(await res.json(), this.logger);
if (windows[0]) windows[0].headline = true;
return {
account: { key: "default" },
status: "available",
planLabel: undefined,
windows,
balances: [],
details: [],
};
}
private async callUsageApi(token: string): Promise<Response> {
return fetchProviderApi(this.fetchApi, KIMI_USAGE_URL, {
async function callUsageApi(token: string): Promise<Response> {
return fetchApi(KIMI_USAGE_URL, {
signal: AbortSignal.timeout(15_000),
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
@@ -300,14 +264,14 @@ export class KimiQuotaProvider {
});
}
private async readCredentials(): Promise<KimiCredentials | null> {
async function readCredentials(): Promise<KimiCredentials | null> {
const environmentToken = process.env["KIMI_TOKEN"] || process.env["KIMI_API_KEY"];
if (environmentToken) {
return { access_token: environmentToken };
}
for (const path of this.credentialPaths()) {
const credentials = await this.readCredentialFile(path);
for (const path of credentialPaths()) {
const credentials = await readCredentialFile(path);
if (credentials?.access_token) {
return { ...credentials, access_token: credentials.access_token };
}
@@ -315,19 +279,19 @@ export class KimiQuotaProvider {
return null;
}
private credentialPaths(): string[] {
const homeDir = this.homeDir ?? homedir();
function credentialPaths(): string[] {
const home = homeDir;
return [
join(
process.env["KIMI_CODE_HOME"] || join(homeDir, ".kimi-code"),
process.env["KIMI_CODE_HOME"] || join(home, ".kimi-code"),
"credentials",
"kimi-code.json",
),
join(homeDir, ".kimi", "credentials", "kimi-code.json"),
join(home, ".kimi", "credentials", "kimi-code.json"),
];
}
private async readCredentialFile(path: string): Promise<KimiAuth | null> {
async function readCredentialFile(path: string): Promise<KimiAuth | null> {
if (!existsSync(path)) return null;
try {
return KimiAuthSchema.parse(JSON.parse(await fs.readFile(path, "utf8")));
@@ -335,4 +299,27 @@ export class KimiQuotaProvider {
return null;
}
}
const credentials = await readCredentials();
if (!credentials) return unavailableUsage();
const res = await callUsageApi(credentials.access_token);
if (!res.ok) {
// Read-only on credentials; the Kimi CLI owns refresh. See docs/providers.md.
return unavailableUsage();
}
const windows = kimiUsageWindowsFromPayload(await res.json());
if (windows[0]) windows[0].headline = true;
return {
account: { key: "default" },
status: "available",
planLabel: undefined,
windows,
balances: [],
details: [],
};
}
@@ -1,2 +1,4 @@
import { z } from "zod";
export const inputSchema = z.object({}).strict();
export type UsageInput = z.infer<typeof inputSchema>;
@@ -1,6 +1,6 @@
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { inputSchema } from "./shared/input.js";
import { MiniMaxQuotaProvider } from "./server/usage.js";
import { fetchUsage } from "./server/usage.js";
export default function contribute(server: PluginServerContext) {
server.registerUsageSource({
@@ -9,7 +9,7 @@ export default function contribute(server: PluginServerContext) {
icon: "icon.svg",
input: inputSchema,
discover: async () => [{}],
fetch: async (_input) => new MiniMaxQuotaProvider({ logger: console }).fetchUsage(),
fetch: fetchUsage,
});
return () => {};
}
@@ -1 +1 @@
{ "id": "minimax-usage-source", "requirements": { "paseo": ">=0.9.2" } }
{ "id": "minimax-usage-source" }
@@ -0,0 +1,315 @@
import { mkdirSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { fetchUsage } from "./usage.js";
import type { UsageReport } from "@getpaseo/plugin/server/usage";
function writeMiniMaxConfig(dir: string, payload: Record<string, unknown>): void {
mkdirSync(join(dir, ".mmx"), { recursive: true });
writeFileSync(join(dir, ".mmx", "config.json"), JSON.stringify(payload));
}
function writeMiniMaxCredentials(
dir: string,
accessToken: string,
expiresAt?: string,
resourceUrl?: string,
): void {
mkdirSync(join(dir, ".mmx"), { recursive: true });
const payload: Record<string, unknown> = { access_token: accessToken };
if (expiresAt !== undefined) payload["expires_at"] = expiresAt;
if (resourceUrl !== undefined) payload["resource_url"] = resourceUrl;
writeFileSync(join(dir, ".mmx", "credentials.json"), JSON.stringify(payload));
}
function mockFetch(handlers: Map<string, () => Response>): typeof fetch {
return vi.fn(async (url: RequestInfo | URL) => {
const key = url.toString();
const handler = handlers.get(key);
if (!handler) throw new Error(`Unmocked fetch: ${key}`);
return handler();
}) as unknown as typeof fetch;
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
describe("minimax usage source", () => {
let homeDir: string;
let fetchApi: typeof fetch;
let originalEnv: Record<string, string | undefined>;
beforeEach(() => {
homeDir = mkdtempSync(join(tmpdir(), "usage-home-"));
originalEnv = { ...process.env };
process.env["HOME"] = homeDir;
process.env["USERPROFILE"] = homeDir;
for (const key of [
"APPDATA",
"COPILOT_TOKEN",
"GITHUB_TOKEN",
"GITHUB_PAT",
"CURSOR_ACCESS_TOKEN",
"CURSOR_TOKEN",
"ZAI_API_KEY",
"GLM_API_KEY",
"GROK_API_KEY",
"GROK_TOKEN",
"KIMI_TOKEN",
"KIMI_API_KEY",
"KIMI_CODE_HOME",
"MINIMAX_API_KEY",
"MINIMAX_BASE_URL",
])
delete process.env[key];
fetchApi = mockFetch(new Map());
});
afterEach(() => {
rmSync(homeDir, { recursive: true, force: true });
for (const key of Object.keys(process.env)) if (!(key in originalEnv)) delete process.env[key];
for (const key in originalEnv) process.env[key] = originalEnv[key];
});
function service(
_options: {
platform?: typeof process.platform;
keychain?: () => Promise<unknown | null>;
cursorHomeDir?: string;
kimiHomeDir?: string;
} = {},
) {
return {
listUsage: async () => {
const report = await fetchUsage({}, (url, init) => fetchApi(url, init));
return {
providers: [
{
providerId: "minimax",
...report,
error: report.error ?? null,
planLabel: report.planLabel ?? null,
},
],
};
},
};
}
function findProvider(
result: { providers: Array<{ providerId: string } & UsageReport> },
id: string,
) {
const report = result.providers.find((item) => item.providerId === id);
if (!report) throw new Error(`Missing usage source ${id}`);
return report;
}
it("fetches MiniMax usage from MINIMAX_API_KEY against the global endpoint", async () => {
process.env["MINIMAX_API_KEY"] = "minimax_test_token";
let requestedUrl: string | null = null;
let authorization: string | null = null;
fetchApi = (async (url: RequestInfo | URL, init?: RequestInit) => {
requestedUrl = url.toString();
authorization = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null;
return jsonResponse({
model_remains: [
{
model_name: "MiniMax-M2.7",
end_time: Date.parse("2026-06-19T05:00:00.000Z"),
weekly_end_time: Date.parse("2026-06-26T00:00:00.000Z"),
current_interval_total_count: 1000,
current_interval_usage_count: 250,
current_interval_remaining_percent: 75,
current_weekly_total_count: 5000,
current_weekly_usage_count: 1200,
current_weekly_remaining_percent: 76,
},
],
});
}) as unknown as typeof fetch;
const miniMax = findProvider(await service().listUsage(), "minimax");
expect(requestedUrl).toBe("https://api.minimax.io/v1/token_plan/remains");
expect(authorization).toBe("Bearer minimax_test_token");
expect(miniMax).toMatchObject({
status: "available",
windows: expect.arrayContaining([
expect.objectContaining({
id: "interval_MiniMax-M2.7",
label: "MiniMax-M2.7 · Interval",
usedPct: 25,
remainingPct: 75,
resetsAt: "2026-06-19T05:00:00.000Z",
}),
expect.objectContaining({
id: "weekly_MiniMax-M2.7",
label: "MiniMax-M2.7 · Weekly",
usedPct: 24,
remainingPct: 76,
resetsAt: "2026-06-26T00:00:00.000Z",
}),
]),
});
});
it("returns unavailable MiniMax usage when no credentials are configured", async () => {
fetchApi = vi.fn() as never;
const miniMax = findProvider(await service().listUsage(), "minimax");
expect(miniMax.status).toBe("unavailable");
expect(fetchApi).not.toHaveBeenCalled();
});
it("reads MiniMax OAuth credentials from the CLI credentials file", async () => {
writeMiniMaxCredentials(
homeDir,
"minimax_oauth_token",
"2030-01-01T00:00:00.000Z",
"https://account.example.com",
);
let requestedUrl: string | null = null;
fetchApi = (async (url: RequestInfo | URL) => {
requestedUrl = url.toString();
return jsonResponse({ model_remains: [] });
}) as unknown as typeof fetch;
await service().listUsage();
expect(requestedUrl).toBe("https://account.example.com/v1/token_plan/remains");
});
it("falls back to MiniMax api_key in the CLI config file", async () => {
writeMiniMaxConfig(homeDir, {
api_key: "minimax_config_key",
region: "cn",
});
let requestedUrl: string | null = null;
fetchApi = (async (url: RequestInfo | URL) => {
requestedUrl = url.toString();
return jsonResponse({ model_remains: [] });
}) as unknown as typeof fetch;
const miniMax = findProvider(await service().listUsage(), "minimax");
expect(requestedUrl).toBe("https://api.minimaxi.com/v1/token_plan/remains");
expect(miniMax.status).toBe("unavailable");
});
it("reports MiniMax accounts with no active token plan as unavailable", async () => {
writeMiniMaxConfig(homeDir, { api_key: "minimax_config_key", region: "cn" });
fetchApi = mockFetch(
new Map([
[
"https://api.minimaxi.com/v1/token_plan/remains",
() =>
jsonResponse({
model_remains: null,
base_resp: { status_code: 2062, status_msg: "no active token plan subscription" },
}),
],
]),
);
const miniMax = findProvider(await service().listUsage(), "minimax");
expect(miniMax).toMatchObject({ status: "unavailable", windows: [], error: null });
});
it("reports a null MiniMax token plan as unavailable even without base_resp", async () => {
process.env["MINIMAX_API_KEY"] = "minimax_test_token";
fetchApi = mockFetch(
new Map([
[
"https://api.minimax.io/v1/token_plan/remains",
() => jsonResponse({ model_remains: null }),
],
]),
);
const miniMax = findProvider(await service().listUsage(), "minimax");
expect(miniMax).toMatchObject({ status: "unavailable", windows: [], error: null });
});
it("still reads MiniMax windows when base_resp reports success", async () => {
process.env["MINIMAX_API_KEY"] = "minimax_test_token";
fetchApi = mockFetch(
new Map([
[
"https://api.minimax.io/v1/token_plan/remains",
() =>
jsonResponse({
base_resp: { status_code: 0, status_msg: "success" },
model_remains: [
{
model_name: "MiniMax-M2.7",
end_time: Date.parse("2026-06-19T05:00:00.000Z"),
current_interval_total_count: 100,
current_interval_usage_count: 25,
current_interval_remaining_percent: 75,
},
],
}),
],
]),
);
const miniMax = findProvider(await service().listUsage(), "minimax");
expect(miniMax).toMatchObject({
status: "available",
windows: expect.arrayContaining([
expect.objectContaining({ id: "interval_MiniMax-M2.7", usedPct: 25 }),
]),
});
});
it("marks exhausted MiniMax interval windows with a danger tone", async () => {
process.env["MINIMAX_API_KEY"] = "minimax_test_token";
fetchApi = mockFetch(
new Map([
[
"https://api.minimax.io/v1/token_plan/remains",
() =>
jsonResponse({
model_remains: [
{
model_name: "MiniMax-M2.7",
end_time: Date.parse("2026-06-19T05:00:00.000Z"),
weekly_end_time: Date.parse("2026-06-26T00:00:00.000Z"),
current_interval_total_count: 100,
current_interval_usage_count: 100,
current_interval_remaining_percent: 0,
current_interval_status: 2,
current_weekly_total_count: 100,
current_weekly_usage_count: 10,
current_weekly_remaining_percent: 90,
current_weekly_status: 1,
},
],
}),
],
]),
);
const miniMax = findProvider(await service().listUsage(), "minimax");
expect(miniMax).toMatchObject({
status: "available",
windows: expect.arrayContaining([
expect.objectContaining({
id: "interval_MiniMax-M2.7",
usedPct: 100,
tone: "danger",
}),
expect.objectContaining({
id: "weekly_MiniMax-M2.7",
tone: "ok",
}),
]),
});
});
});
@@ -1,22 +1,23 @@
import type { UsageInput } from "../shared/input.js";
import { existsSync, promises as fs } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { z } from "zod";
import {
ApiNumberSchema,
ApiOptionalStringSchema,
fetchProviderApi,
unavailableUsage,
windowFromUsedPct,
type UsageReport,
type UsageWindow,
type UsageApiFetch,
} from "@getpaseo/plugin/server/usage";
const ApiNumberSchema = z.coerce.number().finite();
const ApiOptionalStringSchema = z.preprocess(
(value) => (value == null ? undefined : value),
z.coerce.string().optional(),
);
const MINIMAX_GLOBAL_BASE_URL = "https://api.minimax.io";
const MINIMAX_CN_BASE_URL = "https://api.minimaxi.com";
const MINIMAX_CREDENTIALS_PATH = join(homedir(), ".mmx", "credentials.json");
const MINIMAX_CONFIG_PATH = join(homedir(), ".mmx", "config.json");
const MiniMaxModelRemainSchema = z.object({
model_name: ApiOptionalStringSchema,
@@ -73,15 +74,6 @@ interface MiniMaxResolvedAuth {
baseUrl: string;
}
interface MiniMaxQuotaProviderOptions {
logger: Console;
fetch?: UsageApiFetch;
configPath?: string;
credentialsPath?: string;
env?: NodeJS.ProcessEnv;
now?: () => number;
}
function resolveBaseUrl(input: { baseUrl?: string; region?: string }): string {
const explicit = input.baseUrl;
if (explicit && explicit.startsWith("http")) return explicit;
@@ -152,91 +144,35 @@ function toWeeklyWindow(modelName: string, model: MiniMaxModelRemain): UsageWind
});
}
export class MiniMaxQuotaProvider {
private readonly logger: Console;
private readonly fetchApi: UsageApiFetch;
private readonly configPath: string;
private readonly credentialsPath: string;
private readonly env: NodeJS.ProcessEnv;
private readonly now: () => number;
export async function fetchUsage(
input: UsageInput,
fetchApi: typeof fetch = fetch,
): Promise<UsageReport> {
void input;
const configPath = join(homedir(), ".mmx", "config.json");
const credentialsPath = join(homedir(), ".mmx", "credentials.json");
const env = process.env;
const now = Date.now;
constructor(options: MiniMaxQuotaProviderOptions) {
this.logger = options.logger;
this.fetchApi = options.fetch ?? fetch;
this.configPath = options.configPath ?? MINIMAX_CONFIG_PATH;
this.credentialsPath = options.credentialsPath ?? MINIMAX_CREDENTIALS_PATH;
this.env = options.env ?? process.env;
this.now = options.now ?? Date.now;
}
async fetchUsage(): Promise<UsageReport> {
const auth = await this.resolveAuth();
if (!auth) return unavailableUsage();
const res = await fetchProviderApi(this.fetchApi, `${auth.baseUrl}/v1/token_plan/remains`, {
headers: {
Authorization: `Bearer ${auth.token}`,
Accept: "application/json",
},
});
if (!res.ok) {
this.logger.debug({ status: res.status }, "MiniMax usage fetch failed");
return unavailableUsage();
}
const resp = MiniMaxQuotaResponseSchema.parse(await res.json());
const statusCode = resp.base_resp?.status_code;
if (typeof statusCode === "number" && statusCode !== 0) {
this.logger.debug(
{ statusCode, statusMsg: resp.base_resp?.status_msg },
"MiniMax usage unavailable",
);
return unavailableUsage();
}
const models = resp.model_remains ?? [];
const windows: UsageWindow[] = [];
for (const model of models) {
const name = model.model_name ?? "token-plan";
const intervalWindow = toIntervalWindow(name, model);
if (intervalWindow) windows.push(intervalWindow);
const weeklyWindow = toWeeklyWindow(name, model);
if (weeklyWindow) windows.push(weeklyWindow);
}
if (windows[0]) windows[0].headline = true;
return {
account: { key: "default" },
status: windows.length > 0 ? "available" : "unavailable",
planLabel: undefined,
windows,
balances: [],
details: [],
};
}
private async resolveAuth(): Promise<MiniMaxResolvedAuth | null> {
const envToken = this.env["MINIMAX_API_KEY"];
async function resolveAuth(): Promise<MiniMaxResolvedAuth | null> {
const envToken = env["MINIMAX_API_KEY"];
if (envToken) {
const envBase = this.env["MINIMAX_BASE_URL"];
const envBase = env["MINIMAX_BASE_URL"];
return {
token: envToken,
baseUrl: resolveBaseUrl({ baseUrl: envBase }),
};
}
const credentials = await this.readCredentials();
if (credentials?.access_token && !this.isExpired(credentials.expires_at)) {
const credentials = await readCredentials();
if (credentials?.access_token && !isExpired(credentials.expires_at)) {
return {
token: credentials.access_token,
baseUrl: resolveBaseUrl({ baseUrl: credentials.resource_url }),
};
}
const config = await this.readConfig();
const config = await readConfig();
if (config?.api_key) {
return {
token: config.api_key,
@@ -247,7 +183,7 @@ export class MiniMaxQuotaProvider {
};
}
if (config?.oauth?.access_token && !this.isExpired(config.oauth.expires_at)) {
if (config?.oauth?.access_token && !isExpired(config.oauth.expires_at)) {
return {
token: config.oauth.access_token,
baseUrl: resolveBaseUrl({
@@ -260,30 +196,73 @@ export class MiniMaxQuotaProvider {
return null;
}
private isExpired(expiresAt: string | null | undefined): boolean {
function isExpired(expiresAt: string | null | undefined): boolean {
if (!expiresAt) return false;
const parsed = Date.parse(expiresAt);
if (!Number.isFinite(parsed)) return false;
return parsed <= this.now();
return parsed <= now();
}
private async readCredentials(): Promise<z.infer<typeof MiniMaxCredentialsSchema> | null> {
if (!existsSync(this.credentialsPath)) return null;
async function readCredentials(): Promise<z.infer<typeof MiniMaxCredentialsSchema> | null> {
if (!existsSync(credentialsPath)) return null;
try {
const raw = JSON.parse(await fs.readFile(this.credentialsPath, "utf8"));
const raw = JSON.parse(await fs.readFile(credentialsPath, "utf8"));
return MiniMaxCredentialsSchema.parse(raw);
} catch {
return null;
}
}
private async readConfig(): Promise<z.infer<typeof MiniMaxConfigSchema> | null> {
if (!existsSync(this.configPath)) return null;
async function readConfig(): Promise<z.infer<typeof MiniMaxConfigSchema> | null> {
if (!existsSync(configPath)) return null;
try {
const raw = JSON.parse(await fs.readFile(this.configPath, "utf8"));
const raw = JSON.parse(await fs.readFile(configPath, "utf8"));
return MiniMaxConfigSchema.parse(raw);
} catch {
return null;
}
}
const auth = await resolveAuth();
if (!auth) return unavailableUsage();
const res = await fetchApi(`${auth.baseUrl}/v1/token_plan/remains`, {
signal: AbortSignal.timeout(15_000),
headers: {
Authorization: `Bearer ${auth.token}`,
Accept: "application/json",
},
});
if (!res.ok) {
return unavailableUsage();
}
const resp = MiniMaxQuotaResponseSchema.parse(await res.json());
const statusCode = resp.base_resp?.status_code;
if (typeof statusCode === "number" && statusCode !== 0) {
return unavailableUsage();
}
const models = resp.model_remains ?? [];
const windows: UsageWindow[] = [];
for (const model of models) {
const name = model.model_name ?? "token-plan";
const intervalWindow = toIntervalWindow(name, model);
if (intervalWindow) windows.push(intervalWindow);
const weeklyWindow = toWeeklyWindow(name, model);
if (weeklyWindow) windows.push(weeklyWindow);
}
if (windows[0]) windows[0].headline = true;
return {
account: { key: "default" },
status: windows.length > 0 ? "available" : "unavailable",
planLabel: undefined,
windows,
balances: [],
details: [],
};
}
@@ -1,2 +1,4 @@
import { z } from "zod";
export const inputSchema = z.object({}).strict();
export type UsageInput = z.infer<typeof inputSchema>;
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { inputSchema } from "./shared/input.js";
import { ZaiQuotaProvider } from "./server/usage.js";
import { fetchUsage } from "./server/usage.js";
export default function contribute(server: PluginServerContext) {
server.registerUsageSource({
@@ -9,7 +9,7 @@ export default function contribute(server: PluginServerContext) {
icon: "icon.svg",
input: inputSchema,
discover: async () => [{}],
fetch: async (_input) => new ZaiQuotaProvider({ logger: console }).fetchUsage(),
fetch: fetchUsage,
});
return () => {};
}
@@ -1 +1 @@
{ "id": "zai-usage-source", "requirements": { "paseo": ">=0.9.2" } }
{ "id": "zai-usage-source" }
@@ -0,0 +1,119 @@
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { fetchUsage } from "./usage.js";
import type { UsageReport } from "@getpaseo/plugin/server/usage";
function mockFetch(handlers: Map<string, () => Response>): typeof fetch {
return vi.fn(async (url: RequestInfo | URL) => {
const key = url.toString();
const handler = handlers.get(key);
if (!handler) throw new Error(`Unmocked fetch: ${key}`);
return handler();
}) as unknown as typeof fetch;
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
describe("zai usage source", () => {
let homeDir: string;
let fetchApi: typeof fetch;
let originalEnv: Record<string, string | undefined>;
beforeEach(() => {
homeDir = mkdtempSync(join(tmpdir(), "usage-home-"));
originalEnv = { ...process.env };
process.env["HOME"] = homeDir;
process.env["USERPROFILE"] = homeDir;
for (const key of [
"APPDATA",
"COPILOT_TOKEN",
"GITHUB_TOKEN",
"GITHUB_PAT",
"CURSOR_ACCESS_TOKEN",
"CURSOR_TOKEN",
"ZAI_API_KEY",
"GLM_API_KEY",
"GROK_API_KEY",
"GROK_TOKEN",
"KIMI_TOKEN",
"KIMI_API_KEY",
"KIMI_CODE_HOME",
"MINIMAX_API_KEY",
"MINIMAX_BASE_URL",
])
delete process.env[key];
fetchApi = mockFetch(new Map());
});
afterEach(() => {
rmSync(homeDir, { recursive: true, force: true });
for (const key of Object.keys(process.env)) if (!(key in originalEnv)) delete process.env[key];
for (const key in originalEnv) process.env[key] = originalEnv[key];
});
function service(
_options: {
platform?: typeof process.platform;
keychain?: () => Promise<unknown | null>;
cursorHomeDir?: string;
kimiHomeDir?: string;
} = {},
) {
return {
listUsage: async () => {
const report = await fetchUsage({}, (url, init) => fetchApi(url, init));
return {
providers: [
{
providerId: "zai",
...report,
error: report.error ?? null,
planLabel: report.planLabel ?? null,
},
],
};
},
};
}
function findProvider(
result: { providers: Array<{ providerId: string } & UsageReport> },
id: string,
) {
const report = result.providers.find((item) => item.providerId === id);
if (!report) throw new Error(`Missing usage source ${id}`);
return report;
}
it("fetches Z.ai usage from ZAI_API_KEY", async () => {
process.env["ZAI_API_KEY"] = "zai_test_token";
fetchApi = mockFetch(
new Map([
[
"https://api.z.ai/api/biz/subscription/list",
() =>
jsonResponse({
data: [
{
productName: "GLM Coding Max",
status: "VALID",
purchaseTime: "2026-01-12 16:55:13",
valid: "2026-02-12 16:55:13-2026-03-12 16:55:13",
},
],
}),
],
]),
);
const zai = findProvider(await service().listUsage(), "zai");
expect(zai).toMatchObject({
status: "available",
planLabel: "GLM Coding Max",
details: expect.arrayContaining([{ id: "status", label: "Status", value: "VALID" }]),
});
});
});
@@ -1,13 +1,16 @@
import type { UsageInput } from "../shared/input.js";
import { z } from "zod";
import {
ApiOptionalStringSchema,
fetchProviderApi,
unavailableUsage,
type UsageReport,
type UsageDetail,
type UsageApiFetch,
} from "@getpaseo/plugin/server/usage";
const ApiOptionalStringSchema = z.preprocess(
(value) => (value == null ? undefined : value),
z.coerce.string().optional(),
);
const ZaiUsageResponseSchema = z.object({
data: z
.array(
@@ -21,58 +24,44 @@ const ZaiUsageResponseSchema = z.object({
.optional(),
});
interface ZaiQuotaProviderOptions {
logger: Console;
fetch?: UsageApiFetch;
}
export async function fetchUsage(
input: UsageInput,
fetchApi: typeof fetch = fetch,
): Promise<UsageReport> {
void input;
export class ZaiQuotaProvider {
private readonly logger: Console;
private readonly fetchApi: UsageApiFetch;
const token = process.env["ZAI_API_KEY"] || process.env["GLM_API_KEY"];
if (!token) return unavailableUsage();
constructor(options: ZaiQuotaProviderOptions) {
this.logger = options.logger;
this.fetchApi = options.fetch ?? fetch;
const res = await fetchApi("https://api.z.ai/api/biz/subscription/list", {
signal: AbortSignal.timeout(15_000),
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
});
if (!res.ok) {
return unavailableUsage();
}
async fetchUsage(): Promise<UsageReport> {
const token = process.env["ZAI_API_KEY"] || process.env["GLM_API_KEY"];
if (!token) return unavailableUsage();
const resp = ZaiUsageResponseSchema.parse(await res.json());
const sub = resp.data?.[0];
if (!sub) return unavailableUsage();
const res = await fetchProviderApi(
this.fetchApi,
"https://api.z.ai/api/biz/subscription/list",
{
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
},
);
if (!res.ok) {
this.logger.debug({ status: res.status }, "Z.ai usage fetch failed");
return unavailableUsage();
}
const resp = ZaiUsageResponseSchema.parse(await res.json());
const sub = resp.data?.[0];
if (!sub) return unavailableUsage();
const details: UsageDetail[] = [];
if (sub.status) details.push({ id: "status", label: "Status", value: sub.status });
if (sub.valid) details.push({ id: "valid", label: "Valid", value: sub.valid });
if (sub.purchaseTime) {
details.push({ id: "purchase_time", label: "Purchased", value: sub.purchaseTime });
}
return {
account: { key: "default" },
status: "available",
planLabel: sub.productName || undefined,
windows: [],
balances: [],
details,
};
const details: UsageDetail[] = [];
if (sub.status) details.push({ id: "status", label: "Status", value: sub.status });
if (sub.valid) details.push({ id: "valid", label: "Valid", value: sub.valid });
if (sub.purchaseTime) {
details.push({ id: "purchase_time", label: "Purchased", value: sub.purchaseTime });
}
return {
account: { key: "default" },
status: "available",
planLabel: sub.productName || undefined,
windows: [],
balances: [],
details,
};
}
@@ -1,2 +1,4 @@
import { z } from "zod";
export const inputSchema = z.object({}).strict();
export type UsageInput = z.infer<typeof inputSchema>;
@@ -52,7 +52,7 @@ export class UsageSourceRegistry {
return [...unique.values()];
}
// COMPAT(providerUsageList): added in v0.9.2, remove after 2027-03-26.
// COMPAT(providerUsageList): added in v0.9.3, remove after 2027-03-26.
async listLegacyUsage(): Promise<{ fetchedAt: string; providers: ProviderUsage[] }> {
const reports = await this.listReports();
return {
@@ -782,8 +782,8 @@ describe("relative typed-entry configuration", () => {
"packages",
"server",
"src",
"services",
"quota-fetcher",
"plugins",
"usage-sources",
"providers",
"local.ts",
);
@@ -792,7 +792,7 @@ describe("relative typed-entry configuration", () => {
const results = await searchRelativeDirectoryEntries({
cwd: workspaceDir,
query: "packages/server/src/services/quota-fetcher/providers/local.ts",
query: "packages/server/src/plugins/usage-sources/providers/local.ts",
limit: 20,
includeFiles: true,
includeDirectories: false,
@@ -802,7 +802,7 @@ describe("relative typed-entry configuration", () => {
expect(results).toEqual([
{
path: "packages/server/src/services/quota-fetcher/providers/local.ts",
path: "packages/server/src/plugins/usage-sources/providers/local.ts",
kind: "file",
},
]);