@eiei114/pi-sub-core 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +190 -0
- package/README.md +178 -0
- package/index.ts +540 -0
- package/package.json +35 -0
- package/src/cache.ts +546 -0
- package/src/config.ts +35 -0
- package/src/dependencies.ts +37 -0
- package/src/errors.ts +71 -0
- package/src/paths.ts +55 -0
- package/src/provider.ts +66 -0
- package/src/providers/detection.ts +51 -0
- package/src/providers/impl/anthropic.ts +174 -0
- package/src/providers/impl/antigravity.ts +226 -0
- package/src/providers/impl/codex.ts +186 -0
- package/src/providers/impl/copilot.ts +176 -0
- package/src/providers/impl/gemini.ts +130 -0
- package/src/providers/impl/kiro.ts +92 -0
- package/src/providers/impl/zai.ts +120 -0
- package/src/providers/index.ts +5 -0
- package/src/providers/metadata.ts +16 -0
- package/src/providers/registry.ts +54 -0
- package/src/providers/settings.ts +109 -0
- package/src/providers/status.ts +25 -0
- package/src/settings/behavior.ts +58 -0
- package/src/settings/menu.ts +83 -0
- package/src/settings/tools.ts +38 -0
- package/src/settings/ui.ts +450 -0
- package/src/settings-types.ts +95 -0
- package/src/settings-ui.ts +1 -0
- package/src/settings.ts +137 -0
- package/src/status.ts +245 -0
- package/src/storage/lock.ts +150 -0
- package/src/storage.ts +61 -0
- package/src/types.ts +33 -0
- package/src/ui/keybindings.ts +92 -0
- package/src/ui/settings-list.ts +290 -0
- package/src/usage/controller.ts +250 -0
- package/src/usage/fetch.ts +215 -0
- package/src/usage/types.ts +5 -0
- package/src/utils.ts +158 -0
- package/test/all.test.ts +9 -0
- package/test/cache.test.ts +157 -0
- package/test/controller.test.ts +101 -0
- package/test/detection.test.ts +24 -0
- package/test/extension.test.ts +233 -0
- package/test/helpers.ts +48 -0
- package/test/keybindings.test.ts +59 -0
- package/test/lock.test.ts +49 -0
- package/test/prioritize.test.ts +81 -0
- package/test/providers.test.ts +385 -0
- package/test/status.test.ts +70 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Codex usage provider
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import type { Dependencies, RateWindow, UsageSnapshot } from "../../types.js";
|
|
7
|
+
import { BaseProvider } from "../../provider.js";
|
|
8
|
+
import { noCredentials, fetchFailed, httpError } from "../../errors.js";
|
|
9
|
+
import { formatReset, createTimeoutController } from "../../utils.js";
|
|
10
|
+
import { API_TIMEOUT_MS } from "../../config.js";
|
|
11
|
+
|
|
12
|
+
interface CodexRateWindow {
|
|
13
|
+
reset_at?: number;
|
|
14
|
+
limit_window_seconds?: number;
|
|
15
|
+
used_percent?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface CodexRateLimit {
|
|
19
|
+
primary_window?: CodexRateWindow;
|
|
20
|
+
secondary_window?: CodexRateWindow;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface CodexAdditionalRateLimit {
|
|
24
|
+
limit_name?: string;
|
|
25
|
+
metered_feature?: string;
|
|
26
|
+
rate_limit?: CodexRateLimit;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load Codex credentials from auth.json
|
|
31
|
+
* First tries pi's auth.json, then falls back to legacy codex location
|
|
32
|
+
*/
|
|
33
|
+
function loadCodexCredentials(deps: Dependencies): { accessToken?: string; accountId?: string } {
|
|
34
|
+
// Explicit override via env var
|
|
35
|
+
const envAccessToken = (
|
|
36
|
+
deps.env.OPENAI_CODEX_OAUTH_TOKEN ||
|
|
37
|
+
deps.env.OPENAI_CODEX_ACCESS_TOKEN ||
|
|
38
|
+
deps.env.CODEX_OAUTH_TOKEN ||
|
|
39
|
+
deps.env.CODEX_ACCESS_TOKEN
|
|
40
|
+
)?.trim();
|
|
41
|
+
const envAccountId = (deps.env.OPENAI_CODEX_ACCOUNT_ID || deps.env.CHATGPT_ACCOUNT_ID)?.trim();
|
|
42
|
+
if (envAccessToken) {
|
|
43
|
+
return { accessToken: envAccessToken, accountId: envAccountId || undefined };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Try pi's auth.json first
|
|
47
|
+
const piAuthPath = path.join(deps.homedir(), ".pi", "agent", "auth.json");
|
|
48
|
+
try {
|
|
49
|
+
if (deps.fileExists(piAuthPath)) {
|
|
50
|
+
const data = JSON.parse(deps.readFile(piAuthPath) ?? "{}");
|
|
51
|
+
if (data["openai-codex"]?.access) {
|
|
52
|
+
return {
|
|
53
|
+
accessToken: data["openai-codex"].access,
|
|
54
|
+
accountId: data["openai-codex"].accountId,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// Ignore parse errors, try legacy location
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Fall back to legacy codex location
|
|
63
|
+
const codexHome = deps.env.CODEX_HOME || path.join(deps.homedir(), ".codex");
|
|
64
|
+
const authPath = path.join(codexHome, "auth.json");
|
|
65
|
+
try {
|
|
66
|
+
if (deps.fileExists(authPath)) {
|
|
67
|
+
const data = JSON.parse(deps.readFile(authPath) ?? "{}");
|
|
68
|
+
if (data.OPENAI_API_KEY) {
|
|
69
|
+
return { accessToken: data.OPENAI_API_KEY };
|
|
70
|
+
} else if (data.tokens?.access_token) {
|
|
71
|
+
return {
|
|
72
|
+
accessToken: data.tokens.access_token,
|
|
73
|
+
accountId: data.tokens.account_id,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// Ignore parse errors
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getWindowLabel(windowSeconds?: number, fallbackWindowSeconds?: number): string {
|
|
85
|
+
const safeWindowSeconds =
|
|
86
|
+
typeof windowSeconds === "number" && windowSeconds > 0
|
|
87
|
+
? windowSeconds
|
|
88
|
+
: typeof fallbackWindowSeconds === "number" && fallbackWindowSeconds > 0
|
|
89
|
+
? fallbackWindowSeconds
|
|
90
|
+
: 0;
|
|
91
|
+
if (!safeWindowSeconds) {
|
|
92
|
+
return "0h";
|
|
93
|
+
}
|
|
94
|
+
const windowHours = Math.round(safeWindowSeconds / 3600);
|
|
95
|
+
if (windowHours >= 144) return "Week";
|
|
96
|
+
if (windowHours >= 24) return "Day";
|
|
97
|
+
return `${windowHours}h`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function pushWindow(
|
|
101
|
+
windows: RateWindow[],
|
|
102
|
+
prefix: string | undefined,
|
|
103
|
+
window: CodexRateWindow | undefined,
|
|
104
|
+
fallbackWindowSeconds?: number
|
|
105
|
+
): void {
|
|
106
|
+
if (!window) return;
|
|
107
|
+
const resetDate = window.reset_at ? new Date(window.reset_at * 1000) : undefined;
|
|
108
|
+
const label = getWindowLabel(window.limit_window_seconds, fallbackWindowSeconds);
|
|
109
|
+
const windowLabel = prefix ? `${prefix} ${label}` : label;
|
|
110
|
+
windows.push({
|
|
111
|
+
label: windowLabel,
|
|
112
|
+
usedPercent: window.used_percent || 0,
|
|
113
|
+
resetDescription: resetDate ? formatReset(resetDate) : undefined,
|
|
114
|
+
resetAt: resetDate?.toISOString(),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function addRateWindows(windows: RateWindow[], rateLimit: CodexRateLimit | undefined, prefix?: string): void {
|
|
119
|
+
pushWindow(windows, prefix, rateLimit?.primary_window, 10800);
|
|
120
|
+
pushWindow(windows, prefix, rateLimit?.secondary_window, 86400);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class CodexProvider extends BaseProvider {
|
|
124
|
+
readonly name = "codex" as const;
|
|
125
|
+
readonly displayName = "Codex Plan";
|
|
126
|
+
|
|
127
|
+
hasCredentials(deps: Dependencies): boolean {
|
|
128
|
+
return Boolean(loadCodexCredentials(deps).accessToken);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async fetchUsage(deps: Dependencies): Promise<UsageSnapshot> {
|
|
132
|
+
const { accessToken, accountId } = loadCodexCredentials(deps);
|
|
133
|
+
if (!accessToken) {
|
|
134
|
+
return this.emptySnapshot(noCredentials());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { controller, clear } = createTimeoutController(API_TIMEOUT_MS);
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const headers: Record<string, string> = {
|
|
141
|
+
Authorization: `Bearer ${accessToken}`,
|
|
142
|
+
Accept: "application/json",
|
|
143
|
+
};
|
|
144
|
+
if (accountId) {
|
|
145
|
+
headers["ChatGPT-Account-Id"] = accountId;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const res = await deps.fetch("https://chatgpt.com/backend-api/wham/usage", {
|
|
149
|
+
headers,
|
|
150
|
+
signal: controller.signal,
|
|
151
|
+
});
|
|
152
|
+
clear();
|
|
153
|
+
|
|
154
|
+
if (!res.ok) {
|
|
155
|
+
return this.emptySnapshot(httpError(res.status));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const data = (await res.json()) as {
|
|
159
|
+
rate_limit?: CodexRateLimit;
|
|
160
|
+
additional_rate_limits?: CodexAdditionalRateLimit[];
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const windows: RateWindow[] = [];
|
|
164
|
+
addRateWindows(windows, data.rate_limit);
|
|
165
|
+
|
|
166
|
+
if (Array.isArray(data.additional_rate_limits)) {
|
|
167
|
+
for (const entry of data.additional_rate_limits) {
|
|
168
|
+
if (!entry || typeof entry !== "object") continue;
|
|
169
|
+
const prefix =
|
|
170
|
+
typeof entry.limit_name === "string" && entry.limit_name.trim().length > 0
|
|
171
|
+
? entry.limit_name.trim()
|
|
172
|
+
: typeof entry.metered_feature === "string" && entry.metered_feature.trim().length > 0
|
|
173
|
+
? entry.metered_feature.trim()
|
|
174
|
+
: "Additional";
|
|
175
|
+
addRateWindows(windows, entry.rate_limit, prefix);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return this.snapshot({ windows });
|
|
180
|
+
} catch {
|
|
181
|
+
clear();
|
|
182
|
+
return this.emptySnapshot(fetchFailed());
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Copilot usage provider
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import type { Dependencies, RateWindow, UsageSnapshot } from "../../types.js";
|
|
7
|
+
import { BaseProvider } from "../../provider.js";
|
|
8
|
+
import { noCredentials, fetchFailed, httpError } from "../../errors.js";
|
|
9
|
+
import { formatReset, createTimeoutController } from "../../utils.js";
|
|
10
|
+
import { API_TIMEOUT_MS } from "../../config.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Copilot token entries stored by legacy GitHub Copilot CLI
|
|
14
|
+
*/
|
|
15
|
+
type CopilotHostEntry = {
|
|
16
|
+
oauth_token?: string;
|
|
17
|
+
user_token?: string;
|
|
18
|
+
github_token?: string;
|
|
19
|
+
token?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const COPILOT_TOKEN_KEYS: Array<keyof CopilotHostEntry> = [
|
|
23
|
+
"oauth_token",
|
|
24
|
+
"user_token",
|
|
25
|
+
"github_token",
|
|
26
|
+
"token",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function getTokenFromHostEntry(entry: CopilotHostEntry | undefined): string | undefined {
|
|
30
|
+
if (!entry) return undefined;
|
|
31
|
+
for (const key of COPILOT_TOKEN_KEYS) {
|
|
32
|
+
const value = entry[key];
|
|
33
|
+
if (typeof value === "string" && value.length > 0) {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function loadLegacyCopilotToken(deps: Dependencies): string | undefined {
|
|
41
|
+
const configHome = deps.env.XDG_CONFIG_HOME || path.join(deps.homedir(), ".config");
|
|
42
|
+
const legacyPaths = [
|
|
43
|
+
path.join(configHome, "github-copilot", "hosts.json"),
|
|
44
|
+
path.join(deps.homedir(), ".github-copilot", "hosts.json"),
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
for (const hostsPath of legacyPaths) {
|
|
48
|
+
try {
|
|
49
|
+
if (!deps.fileExists(hostsPath)) continue;
|
|
50
|
+
const data = JSON.parse(deps.readFile(hostsPath) ?? "{}");
|
|
51
|
+
if (!data || typeof data !== "object") continue;
|
|
52
|
+
|
|
53
|
+
const normalizedHosts: Record<string, CopilotHostEntry> = {};
|
|
54
|
+
for (const [host, entry] of Object.entries(data as Record<string, CopilotHostEntry>)) {
|
|
55
|
+
normalizedHosts[host.toLowerCase()] = entry;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const preferredToken =
|
|
59
|
+
getTokenFromHostEntry(normalizedHosts["github.com"]) ||
|
|
60
|
+
getTokenFromHostEntry(normalizedHosts["api.github.com"]);
|
|
61
|
+
if (preferredToken) return preferredToken;
|
|
62
|
+
|
|
63
|
+
for (const entry of Object.values(normalizedHosts)) {
|
|
64
|
+
const token = getTokenFromHostEntry(entry);
|
|
65
|
+
if (token) return token;
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Ignore parse errors
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Load Copilot token from pi auth.json first, then fallback to legacy locations.
|
|
77
|
+
*/
|
|
78
|
+
function loadCopilotToken(deps: Dependencies): string | undefined {
|
|
79
|
+
// Explicit override via env var
|
|
80
|
+
const envToken = (deps.env.COPILOT_GITHUB_TOKEN || deps.env.GH_TOKEN || deps.env.GITHUB_TOKEN || deps.env.COPILOT_TOKEN)?.trim();
|
|
81
|
+
if (envToken) return envToken;
|
|
82
|
+
|
|
83
|
+
const authPath = path.join(deps.homedir(), ".pi", "agent", "auth.json");
|
|
84
|
+
try {
|
|
85
|
+
if (deps.fileExists(authPath)) {
|
|
86
|
+
const data = JSON.parse(deps.readFile(authPath) ?? "{}");
|
|
87
|
+
// Prefer refresh token (GitHub access token) for GitHub API endpoints.
|
|
88
|
+
const piToken = data["github-copilot"]?.refresh || data["github-copilot"]?.access;
|
|
89
|
+
if (typeof piToken === "string" && piToken.length > 0) return piToken;
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// Ignore parse errors
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return loadLegacyCopilotToken(deps);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class CopilotProvider extends BaseProvider {
|
|
99
|
+
readonly name = "copilot" as const;
|
|
100
|
+
readonly displayName = "Copilot Plan";
|
|
101
|
+
|
|
102
|
+
hasCredentials(deps: Dependencies): boolean {
|
|
103
|
+
return Boolean(loadCopilotToken(deps));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async fetchUsage(deps: Dependencies): Promise<UsageSnapshot> {
|
|
107
|
+
const token = loadCopilotToken(deps);
|
|
108
|
+
if (!token) {
|
|
109
|
+
return this.emptySnapshot(noCredentials());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const { controller, clear } = createTimeoutController(API_TIMEOUT_MS);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const res = await deps.fetch("https://api.github.com/copilot_internal/user", {
|
|
116
|
+
headers: {
|
|
117
|
+
"Editor-Version": "vscode/1.96.2",
|
|
118
|
+
"User-Agent": "GitHubCopilotChat/0.26.7",
|
|
119
|
+
"X-Github-Api-Version": "2025-04-01",
|
|
120
|
+
Accept: "application/json",
|
|
121
|
+
Authorization: `token ${token}`,
|
|
122
|
+
},
|
|
123
|
+
signal: controller.signal,
|
|
124
|
+
});
|
|
125
|
+
clear();
|
|
126
|
+
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
return this.emptySnapshot(httpError(res.status));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const data = (await res.json()) as {
|
|
132
|
+
quota_reset_date_utc?: string;
|
|
133
|
+
quota_snapshots?: {
|
|
134
|
+
premium_interactions?: {
|
|
135
|
+
percent_remaining?: number;
|
|
136
|
+
remaining?: number;
|
|
137
|
+
entitlement?: number;
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const windows: RateWindow[] = [];
|
|
143
|
+
const resetDate = data.quota_reset_date_utc ? new Date(data.quota_reset_date_utc) : undefined;
|
|
144
|
+
const resetDesc = resetDate ? formatReset(resetDate) : undefined;
|
|
145
|
+
|
|
146
|
+
let requestsRemaining: number | undefined;
|
|
147
|
+
let requestsEntitlement: number | undefined;
|
|
148
|
+
|
|
149
|
+
if (data.quota_snapshots?.premium_interactions) {
|
|
150
|
+
const pi = data.quota_snapshots.premium_interactions;
|
|
151
|
+
const monthUsedPercent = Math.max(0, 100 - (pi.percent_remaining || 0));
|
|
152
|
+
windows.push({
|
|
153
|
+
label: "Month",
|
|
154
|
+
usedPercent: monthUsedPercent,
|
|
155
|
+
resetDescription: resetDesc,
|
|
156
|
+
resetAt: resetDate?.toISOString(),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const remaining = pi.remaining ?? 0;
|
|
160
|
+
const entitlement = pi.entitlement ?? 0;
|
|
161
|
+
requestsRemaining = remaining;
|
|
162
|
+
requestsEntitlement = entitlement;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return this.snapshot({
|
|
166
|
+
windows,
|
|
167
|
+
requestsRemaining,
|
|
168
|
+
requestsEntitlement,
|
|
169
|
+
});
|
|
170
|
+
} catch {
|
|
171
|
+
clear();
|
|
172
|
+
return this.emptySnapshot(fetchFailed());
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Gemini usage provider
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import type { Dependencies, RateWindow, UsageSnapshot } from "../../types.js";
|
|
7
|
+
import { BaseProvider } from "../../provider.js";
|
|
8
|
+
import { noCredentials, fetchFailed, httpError } from "../../errors.js";
|
|
9
|
+
import { createTimeoutController } from "../../utils.js";
|
|
10
|
+
import { API_TIMEOUT_MS } from "../../config.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load Gemini access token from various sources
|
|
14
|
+
*/
|
|
15
|
+
function loadGeminiToken(deps: Dependencies): string | undefined {
|
|
16
|
+
// Explicit override via env var
|
|
17
|
+
const envToken = (
|
|
18
|
+
deps.env.GOOGLE_GEMINI_CLI_OAUTH_TOKEN ||
|
|
19
|
+
deps.env.GOOGLE_GEMINI_CLI_ACCESS_TOKEN ||
|
|
20
|
+
deps.env.GEMINI_OAUTH_TOKEN ||
|
|
21
|
+
deps.env.GOOGLE_GEMINI_OAUTH_TOKEN
|
|
22
|
+
)?.trim();
|
|
23
|
+
if (envToken) return envToken;
|
|
24
|
+
|
|
25
|
+
// Try pi auth.json first
|
|
26
|
+
const piAuthPath = path.join(deps.homedir(), ".pi", "agent", "auth.json");
|
|
27
|
+
try {
|
|
28
|
+
if (deps.fileExists(piAuthPath)) {
|
|
29
|
+
const data = JSON.parse(deps.readFile(piAuthPath) ?? "{}");
|
|
30
|
+
if (data["google-gemini-cli"]?.access) return data["google-gemini-cli"].access;
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// Ignore parse errors
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Try ~/.gemini/oauth_creds.json
|
|
37
|
+
const credPath = path.join(deps.homedir(), ".gemini", "oauth_creds.json");
|
|
38
|
+
try {
|
|
39
|
+
if (deps.fileExists(credPath)) {
|
|
40
|
+
const data = JSON.parse(deps.readFile(credPath) ?? "{}");
|
|
41
|
+
if (data.access_token) return data.access_token;
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// Ignore parse errors
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class GeminiProvider extends BaseProvider {
|
|
51
|
+
readonly name = "gemini" as const;
|
|
52
|
+
readonly displayName = "Gemini Plan";
|
|
53
|
+
|
|
54
|
+
hasCredentials(deps: Dependencies): boolean {
|
|
55
|
+
return Boolean(loadGeminiToken(deps));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async fetchUsage(deps: Dependencies): Promise<UsageSnapshot> {
|
|
59
|
+
const token = loadGeminiToken(deps);
|
|
60
|
+
if (!token) {
|
|
61
|
+
return this.emptySnapshot(noCredentials());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { controller, clear } = createTimeoutController(API_TIMEOUT_MS);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const res = await deps.fetch("https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota", {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: {
|
|
70
|
+
Authorization: `Bearer ${token}`,
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
},
|
|
73
|
+
body: "{}",
|
|
74
|
+
signal: controller.signal,
|
|
75
|
+
});
|
|
76
|
+
clear();
|
|
77
|
+
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
return this.emptySnapshot(httpError(res.status));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const data = (await res.json()) as {
|
|
83
|
+
buckets?: Array<{
|
|
84
|
+
modelId?: string;
|
|
85
|
+
remainingFraction?: number;
|
|
86
|
+
}>;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Aggregate quotas by model type
|
|
90
|
+
const quotas: Record<string, number> = {};
|
|
91
|
+
for (const bucket of data.buckets || []) {
|
|
92
|
+
const model = bucket.modelId || "unknown";
|
|
93
|
+
const frac = bucket.remainingFraction ?? 1;
|
|
94
|
+
if (!quotas[model] || frac < quotas[model]) {
|
|
95
|
+
quotas[model] = frac;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const windows: RateWindow[] = [];
|
|
100
|
+
let proMin = 1;
|
|
101
|
+
let flashMin = 1;
|
|
102
|
+
let hasProModel = false;
|
|
103
|
+
let hasFlashModel = false;
|
|
104
|
+
|
|
105
|
+
for (const [model, frac] of Object.entries(quotas)) {
|
|
106
|
+
if (model.toLowerCase().includes("pro")) {
|
|
107
|
+
hasProModel = true;
|
|
108
|
+
if (frac < proMin) proMin = frac;
|
|
109
|
+
}
|
|
110
|
+
if (model.toLowerCase().includes("flash")) {
|
|
111
|
+
hasFlashModel = true;
|
|
112
|
+
if (frac < flashMin) flashMin = frac;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (hasProModel) {
|
|
117
|
+
windows.push({ label: "Pro", usedPercent: (1 - proMin) * 100 });
|
|
118
|
+
}
|
|
119
|
+
if (hasFlashModel) {
|
|
120
|
+
windows.push({ label: "Flash", usedPercent: (1 - flashMin) * 100 });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return this.snapshot({ windows });
|
|
124
|
+
} catch {
|
|
125
|
+
clear();
|
|
126
|
+
return this.emptySnapshot(fetchFailed());
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS Kiro usage provider
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Dependencies, RateWindow, UsageSnapshot } from "../../types.js";
|
|
6
|
+
import { BaseProvider } from "../../provider.js";
|
|
7
|
+
import { noCli, notLoggedIn, fetchFailed } from "../../errors.js";
|
|
8
|
+
import { formatReset, stripAnsi, whichSync } from "../../utils.js";
|
|
9
|
+
import { CLI_TIMEOUT_MS } from "../../config.js";
|
|
10
|
+
|
|
11
|
+
export class KiroProvider extends BaseProvider {
|
|
12
|
+
readonly name = "kiro" as const;
|
|
13
|
+
readonly displayName = "Kiro Plan";
|
|
14
|
+
|
|
15
|
+
hasCredentials(deps: Dependencies): boolean {
|
|
16
|
+
return Boolean(whichSync("kiro-cli", deps));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async fetchUsage(deps: Dependencies): Promise<UsageSnapshot> {
|
|
20
|
+
const kiroBinary = whichSync("kiro-cli", deps);
|
|
21
|
+
if (!kiroBinary) {
|
|
22
|
+
return this.emptySnapshot(noCli("kiro-cli"));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Check if logged in
|
|
27
|
+
try {
|
|
28
|
+
deps.execFileSync(kiroBinary, ["whoami"], {
|
|
29
|
+
encoding: "utf-8",
|
|
30
|
+
timeout: API_TIMEOUT_MS,
|
|
31
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
32
|
+
});
|
|
33
|
+
} catch {
|
|
34
|
+
return this.emptySnapshot(notLoggedIn());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Get usage
|
|
38
|
+
const output = deps.execFileSync(kiroBinary, ["chat", "--no-interactive", "/usage"], {
|
|
39
|
+
encoding: "utf-8",
|
|
40
|
+
timeout: CLI_TIMEOUT_MS,
|
|
41
|
+
env: { ...deps.env, TERM: "xterm-256color" },
|
|
42
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const stripped = stripAnsi(output);
|
|
46
|
+
const windows: RateWindow[] = [];
|
|
47
|
+
|
|
48
|
+
// Parse credits percentage from "████...█ X%"
|
|
49
|
+
let creditsPercent = 0;
|
|
50
|
+
const percentMatch = stripped.match(/█+\s*(\d+)%/);
|
|
51
|
+
if (percentMatch) {
|
|
52
|
+
creditsPercent = parseInt(percentMatch[1], 10);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Parse credits used/total from "(X.XX of Y covered in plan)"
|
|
56
|
+
const creditsMatch = stripped.match(/\((\d+\.?\d*)\s+of\s+(\d+)\s+covered/);
|
|
57
|
+
if (creditsMatch && !percentMatch) {
|
|
58
|
+
const creditsUsed = parseFloat(creditsMatch[1]);
|
|
59
|
+
const creditsTotal = parseFloat(creditsMatch[2]);
|
|
60
|
+
if (creditsTotal > 0) {
|
|
61
|
+
creditsPercent = (creditsUsed / creditsTotal) * 100;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Parse reset date from "resets on 01/01"
|
|
66
|
+
let resetsAt: Date | undefined;
|
|
67
|
+
const resetMatch = stripped.match(/resets on (\d{2}\/\d{2})/);
|
|
68
|
+
if (resetMatch) {
|
|
69
|
+
const [month, day] = resetMatch[1].split("/").map(Number);
|
|
70
|
+
const now = new Date();
|
|
71
|
+
const year = now.getFullYear();
|
|
72
|
+
resetsAt = new Date(year, month - 1, day);
|
|
73
|
+
if (resetsAt < now) resetsAt.setFullYear(year + 1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
windows.push({
|
|
77
|
+
label: "Credits",
|
|
78
|
+
usedPercent: creditsPercent,
|
|
79
|
+
resetDescription: resetsAt ? formatReset(resetsAt) : undefined,
|
|
80
|
+
resetAt: resetsAt?.toISOString(),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return this.snapshot({ windows });
|
|
84
|
+
} catch {
|
|
85
|
+
return this.emptySnapshot(fetchFailed());
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Kiro doesn't have a public status page
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const API_TIMEOUT_MS = 5000;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* z.ai usage provider
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import type { Dependencies, RateWindow, UsageSnapshot } from "../../types.js";
|
|
7
|
+
import { BaseProvider } from "../../provider.js";
|
|
8
|
+
import { noCredentials, fetchFailed, httpError, apiError } from "../../errors.js";
|
|
9
|
+
import { formatReset, createTimeoutController } from "../../utils.js";
|
|
10
|
+
import { API_TIMEOUT_MS } from "../../config.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load z.ai API key from environment or auth.json
|
|
14
|
+
*/
|
|
15
|
+
function loadZaiApiKey(deps: Dependencies): string | undefined {
|
|
16
|
+
// Try environment variable first
|
|
17
|
+
if (deps.env.ZAI_API_KEY) {
|
|
18
|
+
return deps.env.ZAI_API_KEY;
|
|
19
|
+
}
|
|
20
|
+
if (deps.env.Z_AI_API_KEY) {
|
|
21
|
+
return deps.env.Z_AI_API_KEY;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Try pi auth.json
|
|
25
|
+
const authPath = path.join(deps.homedir(), ".pi", "agent", "auth.json");
|
|
26
|
+
try {
|
|
27
|
+
if (deps.fileExists(authPath)) {
|
|
28
|
+
const auth = JSON.parse(deps.readFile(authPath) ?? "{}");
|
|
29
|
+
return auth["z-ai"]?.access || auth["z-ai"]?.key || auth["zai"]?.access || auth["zai"]?.key;
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Ignore parse errors
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class ZaiProvider extends BaseProvider {
|
|
39
|
+
readonly name = "zai" as const;
|
|
40
|
+
readonly displayName = "z.ai Plan";
|
|
41
|
+
|
|
42
|
+
hasCredentials(deps: Dependencies): boolean {
|
|
43
|
+
return Boolean(loadZaiApiKey(deps));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async fetchUsage(deps: Dependencies): Promise<UsageSnapshot> {
|
|
47
|
+
const apiKey = loadZaiApiKey(deps);
|
|
48
|
+
if (!apiKey) {
|
|
49
|
+
return this.emptySnapshot(noCredentials());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const { controller, clear } = createTimeoutController(API_TIMEOUT_MS);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const res = await deps.fetch("https://api.z.ai/api/monitor/usage/quota/limit", {
|
|
56
|
+
method: "GET",
|
|
57
|
+
headers: {
|
|
58
|
+
Authorization: `Bearer ${apiKey}`,
|
|
59
|
+
Accept: "application/json",
|
|
60
|
+
},
|
|
61
|
+
signal: controller.signal,
|
|
62
|
+
});
|
|
63
|
+
clear();
|
|
64
|
+
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
return this.emptySnapshot(httpError(res.status));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const data = (await res.json()) as {
|
|
70
|
+
success?: boolean;
|
|
71
|
+
code?: number;
|
|
72
|
+
msg?: string;
|
|
73
|
+
data?: {
|
|
74
|
+
limits?: Array<{
|
|
75
|
+
type?: string;
|
|
76
|
+
unit?: number;
|
|
77
|
+
number?: number;
|
|
78
|
+
percentage?: number;
|
|
79
|
+
nextResetTime?: string;
|
|
80
|
+
}>;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (!data.success || data.code !== 200) {
|
|
85
|
+
return this.emptySnapshot(apiError(data.msg || "API error"));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const windows: RateWindow[] = [];
|
|
89
|
+
const limits = data.data?.limits || [];
|
|
90
|
+
|
|
91
|
+
for (const limit of limits) {
|
|
92
|
+
const percent = limit.percentage || 0;
|
|
93
|
+
const nextReset = limit.nextResetTime ? new Date(limit.nextResetTime) : undefined;
|
|
94
|
+
|
|
95
|
+
if (limit.type === "TOKENS_LIMIT") {
|
|
96
|
+
windows.push({
|
|
97
|
+
label: "Tokens",
|
|
98
|
+
usedPercent: percent,
|
|
99
|
+
resetDescription: nextReset ? formatReset(nextReset) : undefined,
|
|
100
|
+
resetAt: nextReset?.toISOString(),
|
|
101
|
+
});
|
|
102
|
+
} else if (limit.type === "TIME_LIMIT") {
|
|
103
|
+
windows.push({
|
|
104
|
+
label: "Monthly",
|
|
105
|
+
usedPercent: percent,
|
|
106
|
+
resetDescription: nextReset ? formatReset(nextReset) : undefined,
|
|
107
|
+
resetAt: nextReset?.toISOString(),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return this.snapshot({ windows });
|
|
113
|
+
} catch {
|
|
114
|
+
clear();
|
|
115
|
+
return this.emptySnapshot(fetchFailed());
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// z.ai doesn't have a public status page
|
|
120
|
+
}
|