chore(quota): drop the Kimi Code quota reader (Kimi already left the catalog)

Redone on v2.8.22 from 3179d25 (feat/grok-usage, never merged).
This commit is contained in:
2026-10-23 13:44:37 +07:00
parent c0832182f2
commit 127da516ac
8 changed files with 14 additions and 137 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Claude Code Working Guide
_Last synced: v2.8.23 (2026-10-26) — OpenCode and Grok Build left the catalog (`opencode-usage.js` gone, a Grok payload on the Claude hook route is dropped)._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session.
_Last synced: v2.8.23 (2026-10-26) — OpenCode and Grok Build left the catalog, the Kimi quota reader too (`opencode-usage.js` gone, a Grok payload on the Claude hook route is dropped)._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session.
## Project mission
- Maintain a reliable local-first dashboard for Claude Code session monitoring.
-1
View File
@@ -50,7 +50,6 @@ const QUOTA_TOOL_LABEL: Record<ToolQuota["id"], string> = {
codex: "Codex",
antigravity: "Antigravity",
cursor: "Cursor",
kimi: "Kimi",
};
function localDate(d = new Date()): string {
+1 -1
View File
@@ -1216,7 +1216,7 @@ export interface CostResult {
/** GET /api/pricing/quota: plan quota spent per native tool (server/lib/quota.js). */
export interface ToolQuota {
id: "claude" | "codex" | "antigravity" | "cursor" | "kimi";
id: "claude" | "codex" | "antigravity" | "cursor";
/** One per plan window: `5h`, `wk`, `mo`, …; `usedPercent` is 0–100.
* `group` names the model pool a window belongs to (Antigravity's). */
windows: Array<{
+1 -1
View File
@@ -2578,7 +2578,7 @@ curl "http://localhost:4820/api/pricing/cost?tz_offset=-420&from=2026-09-01&to=2
GET /api/pricing/quota[?force=1]
```
How much of each native tool's plan quota is spent: `{ tools: [{ id: "claude"|"codex"|"antigravity"|"cursor"|"kimi", active, label?, account?, plan?, expiresAt?, windows: [{ label, usedPercent, resetsAt, group? }], error?, asOf? }], checkedAt }` (`server/lib/quota.js`). **Claude** asks Anthropic's `GET /api/oauth/usage` with the access token the CLI holds right now — never refreshed, so an expired token answers `error: "ELIVEEXPIRED"` until Claude Code refreshes it — cached 5 min (`force=1`, the status bar's click, re-asks at most every 30 s); no OAuth login, no entry. That live account is `active: true`; every OTHER saved auth profile follows as `active: false` with its `label`, asked with its OWN stored token only while that token is valid — never refreshed here, since a refresh rotates the saved credentials (only the Profiles modal's **Check quota** does that) — and, once expired, showing the last figure this server read (`asOf`) or `error: "EPROFILEEXPIRED"`. Each Claude token's owner is also asked (`GET /api/oauth/profile`): the live row carries the label and email of the account its token belongs to (`.claude.json` can name another), and a saved profile whose token belongs to a different account answers `error: "EWRONGACCOUNT"` with no windows, never that other account's usage. **Codex** reads the newest `rate_limits` from its rollout journals, no network (`asOf` = when the journal recorded it; a window past its reset reads 0). **Antigravity** runs the catalog-detected `agy` as `agy --print /usage --output-format json` — the CLI's own `/usage` command, no model call (`num_turns` 0, no conversation) — in a private empty temp directory with a 60 s timeout, only while the tool is enabled in AI Tools and `agy --version` is ≥ 1.1.11 (an older agy may bill `/usage` as a model request); cached like Claude, a failed run shows the last figure (`asOf`) or `error: "EUPSTREAM"`. One window per pool bucket (`5h`, `wk`), `group` naming the pool ("Gemini Models", "Claude and GPT models"); the report names no account and no plan. Running agy may let it update itself; `AGENTHUB_AGY_USAGE=0` never runs it (no entry). **Cursor** reads the Cursor CLI's (`cursor-agent`) own login, `$XDG_CONFIG_HOME/cursor/auth.json` (default `~/.config`), and sends it ONLY to cursor.com's undocumented `GET /api/usage-summary` as the `WorkosCursorSessionToken=<user id>::<token>` cookie (user id from `~/.cursor/cli-config.json` `authInfo`), while its JWT is valid and never refreshed — expired or a 401 is `error: "ETOKENEXPIRED"`; cached like Claude; no file (macOS keeps it in the keychain), no entry. One `mo` window: `individualUsage.plan.totalPercentUsed`, resetting at `billingCycleEnd` (an Enterprise seat without `plan` shows its plan name and no window). **Kimi Code** reads `$KIMI_CODE_HOME/credentials/kimi-code.json` (default `~/.kimi-code`) and sends it only to `api.kimi.com`'s `GET /coding/v1/usages` (`5h`/`wk`/`mo` from `limit_5h`/`limit_7d`/`limit_month_total`). It is used only while valid and never refreshed (`error: "ETOKENEXPIRED"`; a Kimi refresh would rotate the CLI's refresh token and sign it out); no file, no entry. This reader follows Orca's (MIT), cached like Claude. `plan` is the subscription's name as the tool reports it — Claude's `subscriptionType`, Codex's `chatgpt_plan_type` id-token claim, Cursor's `membershipType` — shown as a badge beside the account. `account` is the signed-in email and `expiresAt` when the stored access token runs out — Claude's `claudeAiOauth.expiresAt`, Codex's and Cursor's access-token `exp` (JWT claims read unverified; no token leaves the server but Cursor's own, to cursor.com). Labels: `5h`, `wk`, `mo`, else `<n>d`/`<n>h` (the hover spells `wk`/`mo` out as week/month). The status bar shows one "N% used" chip per tool (its live account) at its right end and, on hover, every account grouped per tool with a row per window — bar, percent, reset time. **Server mode with S3** (one Linux user per account): a developer or analyst gets ONLY their own account's rows, read from their Linux home at the tools' default paths (`~/.claude`, `~/.codex`, `~/.config/cursor` + `~/.cursor`, `~/.kimi-code`) — never the host's — and only from files that account owns (open + `fstat` owner check, a FIFO refused). No saved Claude profiles and no Antigravity row for them (agy's `/usage` needs the self-updating binary, so it stays the host's); an account that is not provisioned gets `tools: []`. An admin gets the host's, as before.
How much of each native tool's plan quota is spent: `{ tools: [{ id: "claude"|"codex"|"antigravity"|"cursor", active, label?, account?, plan?, expiresAt?, windows: [{ label, usedPercent, resetsAt, group? }], error?, asOf? }], checkedAt }` (`server/lib/quota.js`). **Claude** asks Anthropic's `GET /api/oauth/usage` with the access token the CLI holds right now — never refreshed, so an expired token answers `error: "ELIVEEXPIRED"` until Claude Code refreshes it — cached 5 min (`force=1`, the status bar's click, re-asks at most every 30 s); no OAuth login, no entry. That live account is `active: true`; every OTHER saved auth profile follows as `active: false` with its `label`, asked with its OWN stored token only while that token is valid — never refreshed here, since a refresh rotates the saved credentials (only the Profiles modal's **Check quota** does that) — and, once expired, showing the last figure this server read (`asOf`) or `error: "EPROFILEEXPIRED"`. Each Claude token's owner is also asked (`GET /api/oauth/profile`): the live row carries the label and email of the account its token belongs to (`.claude.json` can name another), and a saved profile whose token belongs to a different account answers `error: "EWRONGACCOUNT"` with no windows, never that other account's usage. **Codex** reads the newest `rate_limits` from its rollout journals, no network (`asOf` = when the journal recorded it; a window past its reset reads 0). **Antigravity** runs the catalog-detected `agy` as `agy --print /usage --output-format json` — the CLI's own `/usage` command, no model call (`num_turns` 0, no conversation) — in a private empty temp directory with a 60 s timeout, only while the tool is enabled in AI Tools and `agy --version` is ≥ 1.1.11 (an older agy may bill `/usage` as a model request); cached like Claude, a failed run shows the last figure (`asOf`) or `error: "EUPSTREAM"`. One window per pool bucket (`5h`, `wk`), `group` naming the pool ("Gemini Models", "Claude and GPT models"); the report names no account and no plan. Running agy may let it update itself; `AGENTHUB_AGY_USAGE=0` never runs it (no entry). **Cursor** reads the Cursor CLI's (`cursor-agent`) own login, `$XDG_CONFIG_HOME/cursor/auth.json` (default `~/.config`), and sends it ONLY to cursor.com's undocumented `GET /api/usage-summary` as the `WorkosCursorSessionToken=<user id>::<token>` cookie (user id from `~/.cursor/cli-config.json` `authInfo`), while its JWT is valid and never refreshed — expired or a 401 is `error: "ETOKENEXPIRED"`; cached like Claude; no file (macOS keeps it in the keychain), no entry. One `mo` window: `individualUsage.plan.totalPercentUsed`, resetting at `billingCycleEnd` (an Enterprise seat without `plan` shows its plan name and no window). `plan` is the subscription's name as the tool reports it — Claude's `subscriptionType`, Codex's `chatgpt_plan_type` id-token claim, Cursor's `membershipType` — shown as a badge beside the account. `account` is the signed-in email and `expiresAt` when the stored access token runs out — Claude's `claudeAiOauth.expiresAt`, Codex's and Cursor's access-token `exp` (JWT claims read unverified; no token leaves the server but Cursor's own, to cursor.com). Labels: `5h`, `wk`, `mo`, else `<n>d`/`<n>h` (the hover spells `wk`/`mo` out as week/month). The status bar shows one "N% used" chip per tool (its live account) at its right end and, on hover, every account grouped per tool with a row per window — bar, percent, reset time. **Server mode with S3** (one Linux user per account): a developer or analyst gets ONLY their own account's rows, read from their Linux home at the tools' default paths (`~/.claude`, `~/.codex`, `~/.config/cursor` + `~/.cursor`) — never the host's — and only from files that account owns (open + `fstat` owner check, a FIFO refused). No saved Claude profiles and no Antigravity row for them (agy's `/usage` needs the self-updating binary, so it stays the host's); an account that is not provisioned gets `tools: []`. An admin gets the host's, as before.
#### Analytics
```http
+1 -2
View File
@@ -51,13 +51,12 @@ const env = {
// No suite may fetch LiteLLM's price list (reset-pricing syncs at once).
AGENTHUB_PRICING_SYNC: "0",
// Nor run the developer's real agy (it may update itself) or send their
// real Cursor or Kimi login for the quota.
// real Cursor login for the quota.
AGENTHUB_AGY_USAGE: "0",
// Nor refresh (spend) a real Claude pool login: its refresh token is single use.
DASHBOARD_POOL_REFRESH_MS: "0",
XDG_CONFIG_HOME: tmuxDir,
XDG_DATA_HOME: tmuxDir,
KIMI_CODE_HOME: tmuxDir,
DASHBOARD_PIPELINES_DIR: path.join(dir, "fixtures", "pipelines"),
// Never this machine's server-mode S3 install: its system unit would become
// "the" service manager and its server.conf would send user changes to sudo.
+1 -64
View File
@@ -6,9 +6,7 @@
* with its own token only while it is valid, never refreshed, and never shown
* with the usage of a token that belongs to a different account; each
* tool's plan name; the Cursor CLI's login asked of cursor.com only while
* valid; Antigravity's `agy --print /usage` pools, never on an old agy; and
* the Kimi Code login, sent only to its own host while valid, never
* refreshed.
* valid; and Antigravity's `agy --print /usage` pools, never on an old agy.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
@@ -22,12 +20,6 @@ process.env.XDG_CONFIG_HOME = require("node:fs").mkdtempSync(
);
// Nor run the machine's real agy: only the Antigravity test's fake one runs.
process.env.AGENTHUB_AGY_USAGE = "0";
// Nor its real Kimi login.
for (const k of ["KIMI_CODE_HOME"]) {
process.env[k] = require("node:fs").mkdtempSync(
require("node:path").join(require("node:os").tmpdir(), "quota-home-")
);
}
test("windowLabel names the plan windows", () => {
assert.strictEqual(windowLabel(300), "5h");
@@ -415,58 +407,3 @@ function stubFetch(answers) {
};
return { asked, restore: () => (global.fetch = prev) };
}
test("Kimi Code: the CLI's own token asks api.kimi.com/coding for 5h, week and month", async () => {
const fs = require("node:fs");
const path = require("node:path");
const { quotas, __resetCache } = require("../lib/quota");
const dir = path.join(process.env.KIMI_CODE_HOME, "credentials");
fs.mkdirSync(dir, { recursive: true });
const file = path.join(dir, "kimi-code.json");
const f = stubFetch({
"https://api.kimi.com/coding/v1/usages": [
200,
{
usages: {
limit_5h: { used_ratio: 0.3, reset_time: "2099-01-01T05:00:00Z" },
limit_7d: { used_ratio: "0.2", reset_time: "2099-01-07T00:00:00Z" },
limit_month_total: { used_ratio: 0.4, reset_time: "2099-02-01T00:00:00Z" },
limit_month_code: { used_ratio: 0.25, reset_time: "2099-02-01T00:00:00Z" },
},
},
],
});
const kimi = async () => (await quotas()).tools.find((t) => t.id === "kimi");
try {
__resetCache();
assert.strictEqual(await kimi(), undefined);
const exp = Math.floor(Date.now() / 1000) + 600;
fs.writeFileSync(
file,
JSON.stringify({ access_token: "tok-k", refresh_token: "r", expires_at: exp })
);
__resetCache();
const row = await kimi();
assert.strictEqual(row.expiresAt, new Date(exp * 1000).toISOString());
assert.deepStrictEqual(
row.windows.map((w) => [w.label, Math.round(w.usedPercent)]),
[
["5h", 30],
["wk", 20],
["mo", 40],
]
);
assert.deepStrictEqual(
f.asked.filter((a) => a.includes("kimi")),
["https://api.kimi.com/coding/v1/usages Bearer tok-k"]
);
fs.writeFileSync(file, JSON.stringify({ access_token: "tok-k", expires_at: 1 }));
__resetCache();
assert.strictEqual((await kimi()).error, "ETOKENEXPIRED");
assert.strictEqual(f.asked.filter((a) => a.includes("kimi")).length, 1);
} finally {
f.restore();
fs.rmSync(file, { force: true });
__resetCache();
}
});
+5 -10
View File
@@ -1,7 +1,7 @@
/**
* @file server-s3-quota.test.js
* @description S3 plan, P4: plan quota per member home. `quotas({home})`
* reads Claude, Codex, Cursor and Kimi Code from that
* reads Claude, Codex and Cursor from that
* Linux account's home only — never the host's logins, never agy — and only
* files the member owns (review fix F3: a FIFO or another account's file is
* not read). `GET /api/pricing/quota` gives a developer or an analyst their
@@ -32,7 +32,6 @@ process.env.CLAUDE_HOME = path.join(HOST, ".claude");
process.env.CODEX_HOME = path.join(HOST, ".codex");
process.env.XDG_CONFIG_HOME = path.join(HOST, ".config");
process.env.AGENTHUB_CURSOR_DIR = path.join(HOST, ".cursor");
process.env.KIMI_CODE_HOME = path.join(HOST, ".kimi-code");
const { createApp, startServer } = require("../index");
const auth = require("../lib/auth");
@@ -73,10 +72,6 @@ function seedHome(home, who, { codexPercent }) {
write(path.join(home, ".cursor", "cli-config.json"), {
authInfo: { email: `${who}@cursor`, userId: "u1" },
});
write(path.join(home, ".kimi-code", "credentials", "kimi-code.json"), {
access_token: `${who}-kimi`,
expires_at: Math.floor(LATER / 1000),
});
}
let asked = [];
@@ -136,12 +131,12 @@ describe("quotas({home}) reads a member's own logins", () => {
__resetCache();
asked = [];
const { tools } = await quotas({ home: MIA });
assert.deepEqual(tools.map((t) => t.id).sort(), ["claude", "codex", "cursor", "kimi"]);
assert.deepEqual(tools.map((t) => t.id).sort(), ["claude", "codex", "cursor"]);
const codex = tools.find((t) => t.id === "codex");
assert.equal(codex.account, "mia@codex");
assert.equal(codex.windows[0].usedPercent, 21);
assert.equal(tools.find((t) => t.id === "cursor").account, "mia@cursor");
assert.ok(asked.length >= 4, asked.join(","));
assert.ok(asked.length >= 3, asked.join(","));
assert.ok(
asked.every((a) => a.includes("mia-")),
`only mia's tokens leave: ${asked.join(",")}`
@@ -161,9 +156,9 @@ describe("quotas({home}) reads a member's own logins", () => {
});
it("reads no file the member does not own, and never blocks on a FIFO", async () => {
fs.mkdirSync(path.join(BOB, ".kimi-code", "credentials"), { recursive: true });
fs.mkdirSync(path.join(BOB, ".config", "cursor"), { recursive: true });
require("node:child_process").execFileSync("mkfifo", [
path.join(BOB, ".kimi-code", "credentials", "kimi-code.json"),
path.join(BOB, ".config", "cursor", "auth.json"),
]);
__resetCache();
assert.deepEqual((await quotas({ home: BOB })).tools, []);
+4 -57
View File
@@ -34,12 +34,6 @@
* names no account and no plan. Cached like Claude; a failed run shows
* the last figure with its `asOf`. Running agy may let it update itself;
* `AGENTHUB_AGY_USAGE=0` never runs it.
* - **Kimi Code**: `$KIMI_CODE_HOME/credentials/kimi-code.json` (default
* `~/.kimi-code`), sent only to `api.kimi.com`'s `GET /coding/v1/usages`:
* `5h`, `wk`, `mo` from `limit_5h`/`limit_7d`/`limit_month_total`.
* Kimi's token is used only while valid and never refreshed (Kimi rotates
* its refresh token: a refresh here would sign the CLI out). This reader is
* Orca's (github.com/stablyai/orca, MIT), trimmed.
*
* Each tool also carries its signed-in `account` (an email), its `plan` (the
* subscription's name as the tool reports it: Claude's `subscriptionType`,
@@ -50,8 +44,8 @@
*
* Server mode with S3 (`quotas({home})`): a member's plan is read from THEIR
* Linux account's home only — `.claude`, `.codex`, `.config/cursor` +
* `.cursor`, `.kimi-code`, never the host's env overrides — through `run-as`'s owner-checked reads (F3). No
* saved Claude profiles (the Profiles modal is the host's) and no agy: its
* `.cursor`, never the host's env overrides — through `run-as`'s
* owner-checked reads (F3). No saved Claude profiles (the Profiles modal is the host's) and no agy: its
* `/usage` needs the self-updating binary, so it stays host-only.
*
* Only percentages, window lengths, reset/expiry times, the plan's name and
@@ -79,7 +73,6 @@ const TAIL_BYTES = 512 * 1024;
const CODEX_FILES = 5;
const CURSOR_USAGE_URL = "https://cursor.com/api/usage-summary";
const TIMEOUT_MS = 10_000;
const KIMI_USAGE_URL = "https://api.kimi.com/coding/v1/usages";
const AGY_TIMEOUT_MS = 60_000;
const AGY_MIN_VERSION = [1, 1, 11];
const AGY_WINDOW = { "5h": "5h", weekly: "wk" };
@@ -350,7 +343,6 @@ function toolDirs(home) {
codex: home ? path.join(home, ".codex") : getCodexHome(),
cursorConfig: path.join(env("XDG_CONFIG_HOME") || path.join(h, ".config"), "cursor"),
cursor: home ? path.join(home, ".cursor") : getCursorDir(),
kimi: env("KIMI_CODE_HOME") || path.join(h, ".kimi-code"),
};
}
@@ -464,50 +456,6 @@ async function antigravityQuota() {
}
}
const isoOf = (v) => {
const ms = Date.parse(v);
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
};
/** A Bearer GET to a tool's own host: its windows, or the row's error. */
async function bearerWindows(who, url, token, headers, toWindows) {
try {
const res = await fetch(url, {
headers: { Accept: "application/json", Authorization: `Bearer ${token}`, ...headers },
signal: AbortSignal.timeout(TIMEOUT_MS),
});
if (!res.ok) {
const expired = res.status === 401 || res.status === 403;
return { ...who, windows: [], error: expired ? "ETOKENEXPIRED" : "EUPSTREAM" };
}
return { ...who, ...toWindows(await res.json()) };
} catch {
return { ...who, windows: [], error: "EUPSTREAM" };
}
}
/** Kimi Code's OAuth token and its managed-plan usage. */
async function kimiQuota(dirs = toolDirs()) {
const cred = readJson(path.join(dirs.kimi, "credentials", "kimi-code.json"));
if (typeof cred?.access_token !== "string" || !cred.access_token) return null;
const exp = Number(cred.expires_at) * 1000;
const who = { id: "kimi", active: true, expiresAt: exp ? iso(exp) : null };
if (exp && !fresh(exp)) return { ...who, windows: [], error: "ETOKENEXPIRED" };
return bearerWindows(who, KIMI_USAGE_URL, cred.access_token, {}, (body) => ({
windows: [
["5h", body?.usages?.limit_5h],
["wk", body?.usages?.limit_7d],
["mo", body?.usages?.limit_month_total],
]
.filter(([, u]) => u && Number.isFinite(Number(u.used_ratio)))
.map(([label, u]) => ({
label,
usedPercent: Number(u.used_ratio) * 100,
resetsAt: isoOf(u.reset_time),
})),
}));
}
/**
* Every tool's quota row. `home`: a member's Linux home (S3) — that account's
* logins only, and no agy; omitted, the host's.
@@ -515,13 +463,12 @@ async function kimiQuota(dirs = toolDirs()) {
async function quotas({ force = false, home = null } = {}) {
const dirs = toolDirs(home);
const key = (k) => (home ? `${home}|${k}` : k);
const [claude, cursor, antigravity, kimi] = await Promise.all([
const [claude, cursor, antigravity] = await Promise.all([
claudeQuotas(force, home),
cached(key("cursor"), force, () => cursorQuota(dirs)),
home ? null : cached("antigravity", force, antigravityQuota),
cached(key("kimi"), force, () => kimiQuota(dirs)),
]);
const tools = [...claude, codexQuota(dirs.codex), antigravity, cursor, kimi].filter(Boolean);
const tools = [...claude, codexQuota(dirs.codex), antigravity, cursor].filter(Boolean);
return { tools, checkedAt: new Date().toISOString() };
}