@juanbenjumea/pi-provider-usage-fetcher 0.0.0-stage → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -2
  3. package/dist/index.d.ts +24 -0
  4. package/dist/index.js +24 -0
  5. package/dist/lib/auth/auth.d.ts +12 -0
  6. package/dist/lib/auth/auth.js +67 -0
  7. package/dist/lib/auth/codexbar-config.d.ts +2 -0
  8. package/dist/lib/auth/codexbar-config.js +38 -0
  9. package/dist/lib/auth/web-sessions.d.ts +6 -0
  10. package/dist/lib/auth/web-sessions.js +36 -0
  11. package/dist/lib/claude/claude.d.ts +2 -0
  12. package/dist/lib/claude/claude.js +51 -0
  13. package/dist/lib/cline/cline.d.ts +7 -0
  14. package/dist/lib/cline/cline.js +94 -0
  15. package/dist/lib/codex/codex.d.ts +2 -0
  16. package/dist/lib/codex/codex.js +56 -0
  17. package/dist/lib/commandcode/commandcode.d.ts +13 -0
  18. package/dist/lib/commandcode/commandcode.js +205 -0
  19. package/dist/lib/copilot/copilot.d.ts +2 -0
  20. package/dist/lib/copilot/copilot.js +44 -0
  21. package/dist/lib/cursor/cursor-auth.d.ts +2 -0
  22. package/dist/lib/cursor/cursor-auth.js +83 -0
  23. package/dist/lib/cursor/cursor.d.ts +38 -0
  24. package/dist/lib/cursor/cursor.js +147 -0
  25. package/dist/lib/dashboard.d.ts +1 -0
  26. package/dist/lib/dashboard.js +1 -0
  27. package/dist/lib/fetch.d.ts +1 -0
  28. package/dist/lib/fetch.js +1 -0
  29. package/dist/lib/gemini/gemini.d.ts +2 -0
  30. package/dist/lib/gemini/gemini.js +85 -0
  31. package/dist/lib/kimi/kimi.d.ts +2 -0
  32. package/dist/lib/kimi/kimi.js +54 -0
  33. package/dist/lib/opencode-go/dashboard.d.ts +4 -0
  34. package/dist/lib/opencode-go/dashboard.js +47 -0
  35. package/dist/lib/opencode-go/fetch.d.ts +16 -0
  36. package/dist/lib/opencode-go/fetch.js +42 -0
  37. package/dist/lib/opencode-go/opencode-go.d.ts +11 -0
  38. package/dist/lib/opencode-go/opencode-go.js +63 -0
  39. package/dist/lib/opencode-go/types.d.ts +32 -0
  40. package/dist/lib/opencode-go/types.js +1 -0
  41. package/dist/lib/opencode-go/usage-api.d.ts +22 -0
  42. package/dist/lib/opencode-go/usage-api.js +113 -0
  43. package/dist/lib/openference/openference.d.ts +14 -0
  44. package/dist/lib/openference/openference.js +180 -0
  45. package/dist/lib/provider/dispatch.d.ts +9 -0
  46. package/dist/lib/provider/dispatch.js +86 -0
  47. package/dist/lib/provider/index.d.ts +14 -0
  48. package/dist/lib/provider/index.js +14 -0
  49. package/dist/lib/shared/format.d.ts +5 -0
  50. package/dist/lib/shared/format.js +42 -0
  51. package/dist/lib/shared/http.d.ts +5 -0
  52. package/dist/lib/shared/http.js +58 -0
  53. package/dist/lib/shared/quota-types.d.ts +26 -0
  54. package/dist/lib/shared/quota-types.js +1 -0
  55. package/dist/lib/synthetic/synthetic.d.ts +4 -0
  56. package/dist/lib/synthetic/synthetic.js +130 -0
  57. package/dist/lib/types.d.ts +1 -0
  58. package/dist/lib/types.js +1 -0
  59. package/dist/lib/umans/umans.d.ts +2 -0
  60. package/dist/lib/umans/umans.js +32 -0
  61. package/dist/lib/usage-api.d.ts +1 -0
  62. package/dist/lib/usage-api.js +1 -0
  63. package/dist/lib/zai/zai.d.ts +4 -0
  64. package/dist/lib/zai/zai.js +65 -0
  65. package/package.json +73 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Juan Benjumea
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,89 @@
1
- # Temporary Holding Version
1
+ # @juanbenjumea/pi-provider-usage-fetcher
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Shared **provider usage** library for Pi extensions, agentq, and other tooling.
4
+
5
+ Despite the name (historical), this package covers more than OpenCode Go:
6
+
7
+ | Provider | Auth | Source |
8
+ |---|---|---|
9
+ | **OpenCode Go** | API key (same as chat) | `GET https://opencode.ai/zen/go/v1/usage` |
10
+ | **Claude** | macOS keychain, `auth.json`, explicit apiKey | `GET https://api.anthropic.com/api/oauth/usage` |
11
+ | **Codex** | `auth.json`, `~/.codex/auth.json`, explicit apiKey | `GET https://chatgpt.com/backend-api/wham/usage` |
12
+ | **ClinePass** | `CLINE_API_KEY`, `auth.json`, explicit apiKey | `api.cline.bot/api/v1/*` |
13
+ | **Umans** | `UMANS_API_KEY`, `auth.json`, explicit apiKey | `GET https://api.code.umans.ai/v1/usage` |
14
+ | **Copilot** | `auth.json` (`github-copilot`), explicit apiKey | `GET https://api.github.com/copilot_internal/user` |
15
+ | **Gemini** | `auth.json`, `~/.gemini/oauth_creds.json`, explicit apiKey | `POST https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota` |
16
+ | **Kimi Coding** | `KIMI_API_KEY`, `auth.json`, explicit apiKey | `GET https://api.kimi.com/coding/v1/usages` |
17
+ | **Synthetic** | `SYNTHETIC_API_KEY`, `auth.json`, explicit apiKey | `GET https://api.synthetic.new/v2/quotas` |
18
+ | **z.ai / GLM** | Region-specific environment key, `auth.json`, explicit apiKey | `GET {api.z.ai or open.bigmodel.cn}/api/monitor/usage/quota/limit` |
19
+ | **Cursor** | `quota-sessions.json`, env, CodexBar, Cursor app DB | `cursor.com/api/usage-summary` |
20
+ | **CommandCode** | `quota-sessions.json`, env, CodexBar | `api.commandcode.ai/internal/billing/*` |
21
+ | **Openference** | API key from caller, `OPENFERENCE_API_KEY`, or `auth.json` | `GET https://openference.com/api/user/me` |
22
+
23
+ Legacy OpenCode Go dashboard cookie scraping remains as a fallback when no API key is configured.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install @juanbenjumea/pi-provider-usage-fetcher
29
+ ```
30
+
31
+ Monorepo consumers: `"@juanbenjumea/pi-provider-usage-fetcher": "file:../opencode-go-usage"`.
32
+
33
+ ## Unified Dispatch
34
+
35
+ ```ts
36
+ import { detectProvider, fetchProviderUsage, fetchAllProviderUsage } from "@juanbenjumea/pi-provider-usage-fetcher";
37
+
38
+ // Automatically detects alias ("anthropic", "github-copilot", "opencode", etc.)
39
+ const canonical = detectProvider("anthropic"); // "claude"
40
+
41
+ // Fetches and normalizes quota snapshot across any supported provider
42
+ const snapshot = await fetchProviderUsage("anthropic", { apiKey: "..." });
43
+ // { provider: "Claude", windows: [ { label: "5h", usedPercent: 45, resetsIn: "1h30m" }, ... ], fetchedAt }
44
+
45
+ // Or collect several providers concurrently; unknown providers are omitted.
46
+ const snapshots = await fetchAllProviderUsage(["anthropic", "openai-codex", "cursor"]);
47
+ ```
48
+
49
+ ## OpenCode Go (API-first)
50
+
51
+ ```ts
52
+ import { fetchUsageApi } from "@juanbenjumea/pi-provider-usage-fetcher";
53
+
54
+ const usage = await fetchUsageApi(process.env.OPENCODE_GO_KEY!);
55
+ // { rolling, weekly, monthly, error? }
56
+ ```
57
+
58
+ ## Cursor / CommandCode
59
+
60
+ ```ts
61
+ import { fetchCursorUsage } from "@juanbenjumea/pi-provider-usage-fetcher/cursor.ts";
62
+ import { fetchCommandCodeUsage } from "@juanbenjumea/pi-provider-usage-fetcher/commandcode.ts";
63
+ import { fetchOpenferenceUsage } from "@juanbenjumea/pi-provider-usage-fetcher/openference.ts";
64
+
65
+ // Footer labels (Plan / Auto / API)
66
+ await fetchCursorUsage();
67
+
68
+ // agentq labels (total / auto-composer / api-models)
69
+ await fetchCursorUsage({ labelStyle: "agentq" });
70
+
71
+ await fetchCommandCodeUsage();
72
+
73
+ // Uses the dashboard profile endpoint (openference.com/api/user/me), which the
74
+ // inference API key authenticates via Bearer. window = usage.windowQuotaUsed /
75
+ // plan.requestsPerWindow, week = usage.weekQuotaUsed / plan.requestsPerWeek.
76
+ const openference = await fetchOpenferenceUsage(process.env.OPENFERENCE_API_KEY);
77
+ ```
78
+
79
+ Auth resolution order: provider-specific explicit key → environment variable → `auth.json` credential. For Cursor and CommandCode cookies, the existing order is `quota-sessions.json` → env vars → legacy `auth.json` cookies → CodexBar manual cookies → (Cursor only) desktop app token.
80
+
81
+ ## Consumers
82
+
83
+ - `@juanbenjumea/pi-dynamic-footer` — subscription quota bars
84
+ - `@juanbenjumea/pi-multi-opencode-go` — failover usage polling
85
+ - **agentq** — `data/quota.json` collector
86
+
87
+ ## Package rename
88
+
89
+ This package is published as `@juanbenjumea/pi-provider-usage-fetcher`. Existing consumers of the former `@juanbenjumea/opencode-go-usage` npm package must switch their dependency and imports when adopting this package; the old package identity remains separate.
@@ -0,0 +1,24 @@
1
+ export * from "./lib/opencode-go/types.ts";
2
+ export * from "./lib/opencode-go/dashboard.ts";
3
+ export * from "./lib/opencode-go/fetch.ts";
4
+ export * from "./lib/opencode-go/usage-api.ts";
5
+ export * from "./lib/shared/quota-types.ts";
6
+ export * from "./lib/shared/format.ts";
7
+ export * from "./lib/auth/auth.ts";
8
+ export * from "./lib/auth/web-sessions.ts";
9
+ export * from "./lib/auth/codexbar-config.ts";
10
+ export * from "./lib/cursor/cursor-auth.ts";
11
+ export * from "./lib/cursor/cursor.ts";
12
+ export * from "./lib/commandcode/commandcode.ts";
13
+ export * from "./lib/openference/openference.ts";
14
+ export * from "./lib/opencode-go/opencode-go.ts";
15
+ export * from "./lib/claude/claude.ts";
16
+ export * from "./lib/codex/codex.ts";
17
+ export * from "./lib/cline/cline.ts";
18
+ export * from "./lib/umans/umans.ts";
19
+ export * from "./lib/copilot/copilot.ts";
20
+ export * from "./lib/gemini/gemini.ts";
21
+ export * from "./lib/kimi/kimi.ts";
22
+ export * from "./lib/synthetic/synthetic.ts";
23
+ export * from "./lib/zai/zai.ts";
24
+ export * from "./lib/provider/dispatch.ts";
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ export * from "./lib/opencode-go/types.js";
2
+ export * from "./lib/opencode-go/dashboard.js";
3
+ export * from "./lib/opencode-go/fetch.js";
4
+ export * from "./lib/opencode-go/usage-api.js";
5
+ export * from "./lib/shared/quota-types.js";
6
+ export * from "./lib/shared/format.js";
7
+ export * from "./lib/auth/auth.js";
8
+ export * from "./lib/auth/web-sessions.js";
9
+ export * from "./lib/auth/codexbar-config.js";
10
+ export * from "./lib/cursor/cursor-auth.js";
11
+ export * from "./lib/cursor/cursor.js";
12
+ export * from "./lib/commandcode/commandcode.js";
13
+ export * from "./lib/openference/openference.js";
14
+ export * from "./lib/opencode-go/opencode-go.js";
15
+ export * from "./lib/claude/claude.js";
16
+ export * from "./lib/codex/codex.js";
17
+ export * from "./lib/cline/cline.js";
18
+ export * from "./lib/umans/umans.js";
19
+ export * from "./lib/copilot/copilot.js";
20
+ export * from "./lib/gemini/gemini.js";
21
+ export * from "./lib/kimi/kimi.js";
22
+ export * from "./lib/synthetic/synthetic.js";
23
+ export * from "./lib/zai/zai.js";
24
+ export * from "./lib/provider/dispatch.js";
@@ -0,0 +1,12 @@
1
+ type JsonObject = Record<string, unknown>;
2
+ export declare function getAgentDir(): string;
3
+ export declare function loadAuthJson(): JsonObject;
4
+ /**
5
+ * Extract a provider block from auth.json as a plain object, when present.
6
+ * The raw auth entry narrows to `object` after a typeof check, which has no
7
+ * index signature, so this casts once to keep callers typed.
8
+ */
9
+ export declare function authProviderBlock(auth: JsonObject, key: string): JsonObject | undefined;
10
+ export declare function resolveAuthValue(value: unknown): string | undefined;
11
+ export declare function authCredential(...keys: string[]): string | undefined;
12
+ export {};
@@ -0,0 +1,67 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ export function getAgentDir() {
5
+ return process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent");
6
+ }
7
+ export function loadAuthJson() {
8
+ const authPath = join(getAgentDir(), "auth.json");
9
+ try {
10
+ if (existsSync(authPath)) {
11
+ return JSON.parse(readFileSync(authPath, "utf-8"));
12
+ }
13
+ }
14
+ catch {
15
+ // Missing or malformed auth should render as unavailable, not break pi.
16
+ }
17
+ return {};
18
+ }
19
+ /**
20
+ * Extract a provider block from auth.json as a plain object, when present.
21
+ * The raw auth entry narrows to `object` after a typeof check, which has no
22
+ * index signature, so this casts once to keep callers typed.
23
+ */
24
+ export function authProviderBlock(auth, key) {
25
+ const entry = auth[key];
26
+ return entry && typeof entry === "object" ? entry : undefined;
27
+ }
28
+ export function resolveAuthValue(value) {
29
+ if (typeof value !== "string")
30
+ return undefined;
31
+ const trimmed = value.trim();
32
+ if (!trimmed)
33
+ return undefined;
34
+ if (trimmed.startsWith("!"))
35
+ return undefined;
36
+ if (/^[A-Z][A-Z0-9_]*$/.test(trimmed))
37
+ return process.env[trimmed] || undefined;
38
+ if (trimmed.startsWith("$$"))
39
+ return trimmed.slice(1);
40
+ if (trimmed.startsWith("$!"))
41
+ return trimmed.slice(1);
42
+ if (trimmed.startsWith("$")) {
43
+ const name = trimmed.slice(1).replace(/^\{(.*)\}$/, "$1");
44
+ return process.env[name] || undefined;
45
+ }
46
+ return trimmed;
47
+ }
48
+ export function authCredential(...keys) {
49
+ const auth = loadAuthJson();
50
+ for (const key of keys) {
51
+ const entry = auth[key];
52
+ if (typeof entry === "string") {
53
+ const value = resolveAuthValue(entry);
54
+ if (value)
55
+ return value;
56
+ continue;
57
+ }
58
+ if (entry && typeof entry === "object") {
59
+ for (const field of ["access", "key", "token", "refresh", "cookie"]) {
60
+ const value = resolveAuthValue(entry[field]);
61
+ if (value)
62
+ return value;
63
+ }
64
+ }
65
+ }
66
+ return undefined;
67
+ }
@@ -0,0 +1,2 @@
1
+ export declare function codexBarProviderCookie(providerId: string): string | undefined;
2
+ export declare function envCookie(name: string): string | undefined;
@@ -0,0 +1,38 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ function codexBarConfigPath() {
5
+ const explicit = process.env.CODEXBAR_CONFIG?.trim();
6
+ if (explicit)
7
+ return explicit;
8
+ const xdg = process.env.XDG_CONFIG_HOME?.trim();
9
+ const xdgPath = xdg && xdg.startsWith("/") ? join(xdg, "codexbar", "config.json") : undefined;
10
+ const defaultPath = join(homedir(), ".config", "codexbar", "config.json");
11
+ const legacyPath = join(homedir(), ".codexbar", "config.json");
12
+ if (xdgPath && existsSync(xdgPath))
13
+ return xdgPath;
14
+ if (existsSync(defaultPath))
15
+ return defaultPath;
16
+ if (existsSync(legacyPath))
17
+ return legacyPath;
18
+ return undefined;
19
+ }
20
+ export function codexBarProviderCookie(providerId) {
21
+ const configPath = codexBarConfigPath();
22
+ if (!configPath)
23
+ return undefined;
24
+ try {
25
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
26
+ const providers = Array.isArray(config.providers) ? config.providers : [];
27
+ const entry = providers.find((provider) => provider?.id === providerId);
28
+ const cookieHeader = entry?.cookieHeader;
29
+ return typeof cookieHeader === "string" && cookieHeader.trim() ? cookieHeader.trim() : undefined;
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ }
35
+ export function envCookie(name) {
36
+ const value = process.env[name]?.trim();
37
+ return value || undefined;
38
+ }
@@ -0,0 +1,6 @@
1
+ export declare const DEFAULT_QUOTA_SESSIONS_FILE = "quota-sessions.json";
2
+ type JsonObject = Record<string, unknown>;
3
+ export declare function loadQuotaSessions(): JsonObject;
4
+ /** Cookie header for a provider id in quota-sessions.json (string or { cookie }). */
5
+ export declare function quotaSessionCookie(providerId: string): string | undefined;
6
+ export {};
@@ -0,0 +1,36 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getAgentDir, resolveAuthValue } from "./auth.js";
4
+ export const DEFAULT_QUOTA_SESSIONS_FILE = "quota-sessions.json";
5
+ function quotaSessionsPath() {
6
+ const override = process.env.PI_QUOTA_SESSIONS_FILE?.trim();
7
+ if (override)
8
+ return override;
9
+ return join(getAgentDir(), DEFAULT_QUOTA_SESSIONS_FILE);
10
+ }
11
+ export function loadQuotaSessions() {
12
+ const path = quotaSessionsPath();
13
+ try {
14
+ if (existsSync(path)) {
15
+ return JSON.parse(readFileSync(path, "utf-8"));
16
+ }
17
+ }
18
+ catch {
19
+ // Missing or malformed sessions should render as unavailable, not break pi.
20
+ }
21
+ return {};
22
+ }
23
+ /** Cookie header for a provider id in quota-sessions.json (string or { cookie }). */
24
+ export function quotaSessionCookie(providerId) {
25
+ const sessions = loadQuotaSessions();
26
+ const entry = sessions[providerId];
27
+ if (typeof entry === "string") {
28
+ return resolveAuthValue(entry)?.trim();
29
+ }
30
+ if (entry && typeof entry === "object") {
31
+ const cookie = resolveAuthValue(entry.cookie);
32
+ if (cookie)
33
+ return cookie.trim();
34
+ }
35
+ return undefined;
36
+ }
@@ -0,0 +1,2 @@
1
+ import type { QuotaFetchOptions, QuotaSnapshot } from "../shared/quota-types.ts";
2
+ export declare function fetchClaudeUsage(options?: QuotaFetchOptions): Promise<QuotaSnapshot>;
@@ -0,0 +1,51 @@
1
+ import { execFile } from "node:child_process";
2
+ import { authCredential } from "../auth/auth.js";
3
+ import { formatResetTime, normalizePercent, safeError } from "../shared/format.js";
4
+ import { fetchJson } from "../shared/http.js";
5
+ export async function fetchClaudeUsage(options = {}) {
6
+ let token = options.apiKey || authCredential("anthropic");
7
+ if (!token) {
8
+ try {
9
+ // Resolve the macOS keychain credential asynchronously so a slow
10
+ // keychain lookup never blocks the TUI thread.
11
+ const keychain = await new Promise((resolve, reject) => {
12
+ execFile("/usr/bin/security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"], { encoding: "utf-8", timeout: 2000 }, (err, stdout) => (err ? reject(err) : resolve(stdout)));
13
+ }).then((s) => s.trim());
14
+ const parsed = JSON.parse(keychain);
15
+ token = parsed.claudeAiOauth?.accessToken;
16
+ }
17
+ catch {
18
+ // No Claude CLI credential.
19
+ }
20
+ }
21
+ if (!token)
22
+ return { provider: "Claude", windows: [], error: "no-auth", fetchedAt: Date.now() };
23
+ try {
24
+ const { data } = await fetchJson("https://api.anthropic.com/api/oauth/usage", {
25
+ headers: {
26
+ Authorization: `Bearer ${token}`,
27
+ "anthropic-beta": "oauth-2025-04-20",
28
+ },
29
+ }, options.timeoutMs ?? 10_000, options.fetchImpl ?? fetch);
30
+ const windows = [];
31
+ const addWindow = (source, label) => {
32
+ const utilization = Number(source?.utilization);
33
+ if (!Number.isFinite(utilization))
34
+ return;
35
+ windows.push({
36
+ label,
37
+ usedPercent: normalizePercent(utilization),
38
+ resetsIn: source.resets_at ? formatResetTime(new Date(source.resets_at)) : undefined,
39
+ });
40
+ };
41
+ addWindow(data.five_hour, "5h");
42
+ addWindow(data.seven_day, "Week");
43
+ // Some subscription variants expose a monthly bucket; do not synthesize
44
+ // one when the endpoint omits it.
45
+ addWindow(data.monthly ?? data.thirty_day ?? data.thirty_day_window, "Month");
46
+ return { provider: "Claude", windows, fetchedAt: Date.now() };
47
+ }
48
+ catch (error) {
49
+ return { provider: "Claude", windows: [], error: safeError(error), fetchedAt: Date.now() };
50
+ }
51
+ }
@@ -0,0 +1,7 @@
1
+ import type { QuotaFetchOptions, QuotaSnapshot } from "../shared/quota-types.ts";
2
+ export interface ClineUsageItem {
3
+ createdAt?: string;
4
+ costUsd?: number;
5
+ }
6
+ export declare function clineApiKey(explicit?: string): string | undefined;
7
+ export declare function fetchClineUsage(apiKeyOrOptions?: string | QuotaFetchOptions, maybeOptions?: QuotaFetchOptions): Promise<QuotaSnapshot>;
@@ -0,0 +1,94 @@
1
+ import { authCredential, resolveAuthValue } from "../auth/auth.js";
2
+ import { clampPercent, formatResetTime, safeError } from "../shared/format.js";
3
+ import { fetchJson } from "../shared/http.js";
4
+ const CLINE_BASE_URL = "https://api.cline.bot";
5
+ const CLINE_PAGE_LIMIT = 100;
6
+ const CLINE_MAX_PAGES = 100;
7
+ function clineUrl(path) {
8
+ return new URL(path, CLINE_BASE_URL).toString();
9
+ }
10
+ export function clineApiKey(explicit) {
11
+ return explicit || resolveAuthValue(process.env.CLINE_API_KEY) || authCredential("cline-pass", "clinepass");
12
+ }
13
+ function parseClineDate(value) {
14
+ if (typeof value !== "string" || !value.trim())
15
+ return undefined;
16
+ const date = new Date(value);
17
+ return Number.isFinite(date.getTime()) ? date : undefined;
18
+ }
19
+ function clineWindow(now, label, durationMs, limitUnits, items) {
20
+ if (!Number.isFinite(limitUnits) || limitUnits <= 0)
21
+ return null;
22
+ const start = now - durationMs;
23
+ let usedUnits = 0;
24
+ let oldest;
25
+ for (const item of items) {
26
+ const createdAt = parseClineDate(item.createdAt)?.getTime();
27
+ if (createdAt === undefined || createdAt < start || createdAt > now)
28
+ continue;
29
+ const cost = Number(item.costUsd);
30
+ if (Number.isFinite(cost))
31
+ usedUnits += cost;
32
+ oldest = oldest === undefined ? createdAt : Math.min(oldest, createdAt);
33
+ }
34
+ const usedPercent = limitUnits > 0 ? clampPercent((usedUnits / limitUnits) * 100) : 0;
35
+ const resetAt = oldest === undefined ? undefined : new Date(oldest + durationMs);
36
+ return { label, usedPercent, resetsIn: resetAt ? formatResetTime(resetAt) : undefined };
37
+ }
38
+ export async function fetchClineUsage(apiKeyOrOptions, maybeOptions) {
39
+ const options = typeof apiKeyOrOptions === "object" && apiKeyOrOptions !== null
40
+ ? apiKeyOrOptions
41
+ : (maybeOptions ?? {});
42
+ const explicitKey = typeof apiKeyOrOptions === "string" ? apiKeyOrOptions : options.apiKey;
43
+ const apiKey = clineApiKey(explicitKey);
44
+ if (!apiKey) {
45
+ return { provider: "ClinePass", windows: [], error: "no-auth", fetchedAt: Date.now() };
46
+ }
47
+ const timeoutMs = options.timeoutMs ?? 10_000;
48
+ const fetchImpl = options.fetchImpl ?? fetch;
49
+ try {
50
+ const headers = { Authorization: `Bearer ${apiKey}`, Accept: "application/json", "User-Agent": "pi-obs-footer" };
51
+ const me = await fetchJson(clineUrl("/api/v1/users/me"), { headers }, timeoutMs, fetchImpl);
52
+ const userId = typeof me.data?.data?.id === "string" ? me.data.data.id : "";
53
+ if (!userId)
54
+ return { provider: "ClinePass", windows: [], error: "invalid-response", fetchedAt: Date.now() };
55
+ const plans = await fetchJson(clineUrl("/api/v1/plans"), { headers }, timeoutMs, fetchImpl);
56
+ const planList = Array.isArray(plans.data?.data) ? plans.data.data : [];
57
+ const threshold = planList.find((plan) => plan?.isActive && plan?.entitlements?.cline_pass?.enabled && plan?.entitlements?.cline_pass?.inferenceCapThreshold)?.entitlements?.cline_pass?.inferenceCapThreshold;
58
+ if (!threshold)
59
+ return { provider: "ClinePass", windows: [], error: "no-active-plan", fetchedAt: Date.now() };
60
+ const items = [];
61
+ let cursor = "";
62
+ const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
63
+ for (let page = 0; page < CLINE_MAX_PAGES; page++) {
64
+ const url = new URL(clineUrl(`/api/v1/users/${encodeURIComponent(userId)}/usages`));
65
+ url.searchParams.set("limit", String(CLINE_PAGE_LIMIT));
66
+ if (cursor)
67
+ url.searchParams.set("cursor", cursor);
68
+ const response = await fetchJson(url.toString(), { headers }, timeoutMs, fetchImpl);
69
+ const data = response.data?.data;
70
+ const pageItems = Array.isArray(data?.items) ? data.items : [];
71
+ items.push(...pageItems);
72
+ const oldest = pageItems
73
+ .map((item) => parseClineDate(item.createdAt)?.getTime())
74
+ .filter((value) => value !== undefined)
75
+ .reduce((min, value) => min === undefined ? value : Math.min(min, value), undefined);
76
+ cursor = typeof data?.nextToken === "string" ? data.nextToken.trim() : "";
77
+ if (!cursor || pageItems.length === 0 || (oldest !== undefined && oldest < cutoff))
78
+ break;
79
+ }
80
+ const now = Date.now();
81
+ return {
82
+ provider: "ClinePass",
83
+ windows: [
84
+ clineWindow(now, "5h", 5 * 60 * 60 * 1000, Number(threshold.last5HoursUsageCostUSDPerUser), items),
85
+ clineWindow(now, "Week", 7 * 24 * 60 * 60 * 1000, Number(threshold.last7daysUsageCostUSDPerUser), items),
86
+ clineWindow(now, "Month", 30 * 24 * 60 * 60 * 1000, Number(threshold.last30daysUsageCostUSDPerUser), items),
87
+ ].filter((window) => window !== null),
88
+ fetchedAt: now,
89
+ };
90
+ }
91
+ catch (error) {
92
+ return { provider: "ClinePass", windows: [], error: safeError(error), fetchedAt: Date.now() };
93
+ }
94
+ }
@@ -0,0 +1,2 @@
1
+ import type { QuotaFetchOptions, QuotaSnapshot } from "../shared/quota-types.ts";
2
+ export declare function fetchCodexUsage(options?: QuotaFetchOptions): Promise<QuotaSnapshot>;
@@ -0,0 +1,56 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { authCredential, loadAuthJson } from "../auth/auth.js";
5
+ import { clampPercent, formatResetSeconds, formatResetTime, safeError } from "../shared/format.js";
6
+ import { fetchJson } from "../shared/http.js";
7
+ export async function fetchCodexUsage(options = {}) {
8
+ let token = options.apiKey || authCredential("openai-codex");
9
+ let accountId = loadAuthJson()["openai-codex"]?.accountId;
10
+ if (!token) {
11
+ const codexPath = join(process.env.CODEX_HOME || join(homedir(), ".codex"), "auth.json");
12
+ try {
13
+ const data = JSON.parse(readFileSync(codexPath, "utf-8"));
14
+ token = data.OPENAI_API_KEY || data.tokens?.access_token;
15
+ accountId ||= data.tokens?.account_id;
16
+ }
17
+ catch {
18
+ // No Codex credential.
19
+ }
20
+ }
21
+ if (!token)
22
+ return { provider: "Codex", windows: [], error: "no-auth", fetchedAt: Date.now() };
23
+ try {
24
+ const headers = {
25
+ Authorization: `Bearer ${token}`,
26
+ "User-Agent": "pi-agent",
27
+ Accept: "application/json",
28
+ };
29
+ if (accountId)
30
+ headers["ChatGPT-Account-Id"] = accountId;
31
+ const { data } = await fetchJson("https://chatgpt.com/backend-api/wham/usage", { headers }, options.timeoutMs ?? 10_000, options.fetchImpl ?? fetch);
32
+ const windows = [];
33
+ const rateLimit = data.rate_limit ?? {};
34
+ for (const [label, window] of [
35
+ ["5h", rateLimit.primary_window],
36
+ ["Week", rateLimit.secondary_window],
37
+ ["Month", rateLimit.monthly_window ?? rateLimit.tertiary_window],
38
+ ]) {
39
+ if (!window)
40
+ continue;
41
+ const resetAt = typeof window.reset_at === "number" ? new Date(window.reset_at * 1000) : undefined;
42
+ const usedPercent = Number(window.used_percent);
43
+ if (!Number.isFinite(usedPercent))
44
+ continue;
45
+ windows.push({
46
+ label,
47
+ usedPercent: clampPercent(usedPercent),
48
+ resetsIn: resetAt ? formatResetTime(resetAt) : formatResetSeconds(Number(window.reset_after_seconds)),
49
+ });
50
+ }
51
+ return { provider: "Codex", windows, fetchedAt: Date.now() };
52
+ }
53
+ catch (error) {
54
+ return { provider: "Codex", windows: [], error: safeError(error), fetchedAt: Date.now() };
55
+ }
56
+ }
@@ -0,0 +1,13 @@
1
+ import type { QuotaSnapshot, QuotaWindow } from "../shared/quota-types.ts";
2
+ export declare function resolveCommandCodeCookieHeader(): string | undefined;
3
+ export declare function parseCommandCodeCredits(data: any): {
4
+ monthlyRemaining: number;
5
+ fiveHour?: QuotaWindow;
6
+ weekly?: QuotaWindow;
7
+ };
8
+ export declare function parseCommandCodeSubscription(data: any): {
9
+ planId?: string;
10
+ periodEnd?: Date;
11
+ };
12
+ export declare function buildCommandCodeWindows(credits: ReturnType<typeof parseCommandCodeCredits>, subscription: ReturnType<typeof parseCommandCodeSubscription>): QuotaWindow[];
13
+ export declare function fetchCommandCodeUsage(): Promise<QuotaSnapshot>;