tau-coding-agent 0.1.0

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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/extensions/answer.ts +601 -0
  4. package/extensions/branch-term.ts +405 -0
  5. package/extensions/btw.ts +444 -0
  6. package/extensions/ghostty.ts +301 -0
  7. package/extensions/git-diff-stats.ts +277 -0
  8. package/extensions/git-pr-status.ts +286 -0
  9. package/extensions/insights.ts +2367 -0
  10. package/extensions/interlude.ts +144 -0
  11. package/extensions/loop.ts +529 -0
  12. package/extensions/memory.ts +1889 -0
  13. package/extensions/notify.ts +161 -0
  14. package/extensions/openai-fast.ts +227 -0
  15. package/extensions/openai-verbosity.ts +223 -0
  16. package/extensions/review.ts +4347 -0
  17. package/extensions/sandbox/index.ts +2578 -0
  18. package/extensions/usage/anthropic.ts +198 -0
  19. package/extensions/usage/github-copilot.ts +204 -0
  20. package/extensions/usage/google-gemini-cli.ts +232 -0
  21. package/extensions/usage/index.ts +2310 -0
  22. package/extensions/usage/minimax.ts +208 -0
  23. package/extensions/usage/openai-codex.ts +180 -0
  24. package/extensions/usage/openrouter.ts +168 -0
  25. package/extensions/usage/providers.ts +26 -0
  26. package/extensions/usage/shared.ts +156 -0
  27. package/extensions/usage/types.ts +53 -0
  28. package/extensions/usage/zai.ts +186 -0
  29. package/extensions/websearch/README.md +68 -0
  30. package/extensions/websearch/browser/chromium.ts +330 -0
  31. package/extensions/websearch/browser/discovery.ts +75 -0
  32. package/extensions/websearch/browser/firefox.ts +150 -0
  33. package/extensions/websearch/browser/sqlite.ts +38 -0
  34. package/extensions/websearch/config.ts +76 -0
  35. package/extensions/websearch/index.ts +312 -0
  36. package/extensions/websearch/normalize.ts +155 -0
  37. package/extensions/websearch/providers/anthropic.pi.ts +139 -0
  38. package/extensions/websearch/providers/gemini.browser.ts +185 -0
  39. package/extensions/websearch/providers/gemini.pi.ts +108 -0
  40. package/extensions/websearch/providers/openai-codex.browser.ts +77 -0
  41. package/extensions/websearch/providers/openai-codex.pi.ts +49 -0
  42. package/extensions/websearch/providers/openai-codex.shared.ts +123 -0
  43. package/extensions/websearch/providers/pi-model.shared.ts +86 -0
  44. package/extensions/websearch/providers/search-prompt.shared.ts +17 -0
  45. package/extensions/websearch/providers/shared.ts +76 -0
  46. package/extensions/websearch/types.ts +52 -0
  47. package/extensions/worktree.ts +2223 -0
  48. package/package.json +72 -0
  49. package/skills/browser-tools/SKILL.md +253 -0
  50. package/skills/browser-tools/scripts/browser-content.js +100 -0
  51. package/skills/browser-tools/scripts/browser-cookies.js +33 -0
  52. package/skills/browser-tools/scripts/browser-dismiss-cookies.js +455 -0
  53. package/skills/browser-tools/scripts/browser-eval.js +75 -0
  54. package/skills/browser-tools/scripts/browser-logs-tail.js +89 -0
  55. package/skills/browser-tools/scripts/browser-nav.js +28 -0
  56. package/skills/browser-tools/scripts/browser-net-summary.js +115 -0
  57. package/skills/browser-tools/scripts/browser-pick.js +133 -0
  58. package/skills/browser-tools/scripts/browser-screenshot.js +18 -0
  59. package/skills/browser-tools/scripts/browser-start.js +281 -0
  60. package/skills/browser-tools/scripts/browser-watch.js +321 -0
  61. package/skills/browser-tools/scripts/utils.js +60 -0
  62. package/skills/git-clean-history/SKILL.md +45 -0
  63. package/skills/git-commit/SKILL.md +55 -0
  64. package/skills/oracle/SKILL.md +64 -0
  65. package/skills/oracle/scripts/oracle +249 -0
  66. package/skills/oracle/scripts/oracle-bundle +227 -0
  67. package/skills/sentry/SKILL.md +198 -0
  68. package/skills/sentry/lib/auth.js +99 -0
  69. package/skills/sentry/scripts/fetch-event.js +325 -0
  70. package/skills/sentry/scripts/fetch-issue.js +368 -0
  71. package/skills/sentry/scripts/list-issues.js +245 -0
  72. package/skills/sentry/scripts/search-events.js +291 -0
  73. package/skills/sentry/scripts/search-logs.js +234 -0
  74. package/skills/update-changelog/SKILL.md +135 -0
  75. package/skills/web-design/SKILL.md +117 -0
@@ -0,0 +1,198 @@
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import {
3
+ clampPercent,
4
+ formatCurrency,
5
+ formatPlan,
6
+ formatWindowLabel,
7
+ LiveUsageUnavailableError,
8
+ makeMeterItem,
9
+ parseISODate,
10
+ readNumber,
11
+ readString,
12
+ } from "./shared.js";
13
+ import type { LiveUsageItem, LiveUsageSnapshot, UsageProviderDefinition } from "./types.js";
14
+
15
+ const PROVIDER_ID = "anthropic";
16
+ const USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
17
+ const BETA_HEADER = "oauth-2025-04-20";
18
+ const USER_AGENT = "claude-code/2.1.0";
19
+ const SESSION_WINDOW_MINUTES = 5 * 60;
20
+ const WEEK_WINDOW_MINUTES = 7 * 24 * 60;
21
+
22
+ interface UsageWindow {
23
+ usedPercent: number;
24
+ resetsAt?: Date;
25
+ }
26
+
27
+ function makeWindowMeter(
28
+ key: string,
29
+ label: string,
30
+ window: UsageWindow,
31
+ windowMinutes: number,
32
+ ): LiveUsageItem {
33
+ return makeMeterItem({
34
+ key,
35
+ label,
36
+ usedPercent: window.usedPercent,
37
+ windowLabel: formatWindowLabel(windowMinutes),
38
+ resetsAt: window.resetsAt,
39
+ });
40
+ }
41
+
42
+ function parseWindow(value: unknown): UsageWindow | undefined {
43
+ const input = value as Record<string, unknown> | null | undefined;
44
+ const utilization = readNumber(input?.utilization);
45
+ if (utilization === undefined) return undefined;
46
+
47
+ const resetsAt = parseISODate(readString(input?.resets_at));
48
+ return {
49
+ usedPercent: clampPercent(utilization),
50
+ resetsAt,
51
+ };
52
+ }
53
+
54
+ function parseExtraUsage(value: unknown): LiveUsageItem | undefined {
55
+ const input = value as Record<string, unknown> | null | undefined;
56
+ if (!input || input.is_enabled !== true) return undefined;
57
+
58
+ const monthlyLimitMinor = readNumber(input.monthly_limit);
59
+ const usedCreditsMinor = readNumber(input.used_credits);
60
+ const utilization = readNumber(input.utilization);
61
+ const currency = readString(input.currency) ?? "USD";
62
+
63
+ if (
64
+ monthlyLimitMinor === undefined &&
65
+ usedCreditsMinor === undefined &&
66
+ utilization === undefined
67
+ ) {
68
+ return undefined;
69
+ }
70
+
71
+ const usedMajor = (usedCreditsMinor ?? 0) / 100;
72
+ const limitMajor = (monthlyLimitMinor ?? 0) / 100;
73
+ const usedPercent = clampPercent(
74
+ utilization ??
75
+ (monthlyLimitMinor && monthlyLimitMinor > 0
76
+ ? ((usedCreditsMinor ?? 0) / monthlyLimitMinor) * 100
77
+ : 0),
78
+ );
79
+
80
+ let detail: string | undefined;
81
+ if (monthlyLimitMinor !== undefined && monthlyLimitMinor > 0) {
82
+ detail = `${formatCurrency(usedMajor, currency)} / ${formatCurrency(limitMajor, currency)} used · monthly`;
83
+ }
84
+
85
+ return makeMeterItem({
86
+ key: "extra-usage",
87
+ label: "Extra usage",
88
+ usedPercent,
89
+ detail,
90
+ });
91
+ }
92
+
93
+ function extractErrorMessage(status: number, bodyText: string): string {
94
+ const defaultMessage = `Claude usage request failed (${status}).`;
95
+ if (status === 401 || status === 403) {
96
+ return "Claude login expired. Re-run /login anthropic.";
97
+ }
98
+ if (!bodyText.trim()) return defaultMessage;
99
+
100
+ try {
101
+ const parsed = JSON.parse(bodyText) as Record<string, unknown>;
102
+ const error = parsed.error as Record<string, unknown> | undefined;
103
+ const message = readString(error?.message) ?? readString(parsed.message);
104
+ return message ? `${defaultMessage} ${message}` : defaultMessage;
105
+ } catch {
106
+ return `${defaultMessage} ${bodyText.trim()}`;
107
+ }
108
+ }
109
+
110
+ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<LiveUsageSnapshot> {
111
+ const token = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER_ID);
112
+ if (!token) throw new LiveUsageUnavailableError("no auth configured");
113
+
114
+ const credential = ctx.modelRegistry.authStorage.get(PROVIDER_ID);
115
+ if (credential?.type === "api_key") {
116
+ throw new LiveUsageUnavailableError("API-key auth does not expose Claude subscription quota");
117
+ }
118
+
119
+ const response = await fetch(USAGE_URL, {
120
+ method: "GET",
121
+ headers: {
122
+ Authorization: `Bearer ${token}`,
123
+ Accept: "application/json",
124
+ "Content-Type": "application/json",
125
+ "anthropic-beta": BETA_HEADER,
126
+ "User-Agent": USER_AGENT,
127
+ },
128
+ signal,
129
+ });
130
+
131
+ if (!response.ok) {
132
+ const bodyText = await response.text().catch(() => "");
133
+ throw new Error(extractErrorMessage(response.status, bodyText));
134
+ }
135
+
136
+ const payload = (await response.json()) as Record<string, unknown>;
137
+ const fiveHour = parseWindow(payload.five_hour);
138
+ const sevenDay = parseWindow(payload.seven_day);
139
+ const sevenDaySonnet = parseWindow(payload.seven_day_sonnet);
140
+ const sevenDayOpus = parseWindow(payload.seven_day_opus);
141
+ const sevenDayOAuthApps = parseWindow(payload.seven_day_oauth_apps);
142
+
143
+ const items: LiveUsageItem[] = [];
144
+ if (fiveHour) items.push(makeWindowMeter("session", "Session", fiveHour, SESSION_WINDOW_MINUTES));
145
+ if (sevenDay) items.push(makeWindowMeter("week", "Week", sevenDay, WEEK_WINDOW_MINUTES));
146
+ if (sevenDaySonnet) {
147
+ items.push(makeWindowMeter("sonnet", "Sonnet", sevenDaySonnet, WEEK_WINDOW_MINUTES));
148
+ } else if (sevenDayOpus) {
149
+ items.push(makeWindowMeter("opus", "Opus", sevenDayOpus, WEEK_WINDOW_MINUTES));
150
+ } else if (sevenDayOAuthApps) {
151
+ items.push(makeWindowMeter("apps", "Apps", sevenDayOAuthApps, WEEK_WINDOW_MINUTES));
152
+ }
153
+
154
+ const extraUsage = parseExtraUsage(payload.extra_usage);
155
+ if (extraUsage) items.push(extraUsage);
156
+
157
+ if (items.length === 0) {
158
+ throw new Error("Claude did not return any live quota data.");
159
+ }
160
+
161
+ const rateLimitTier =
162
+ credential?.type === "oauth" && typeof credential.rateLimitTier === "string"
163
+ ? credential.rateLimitTier
164
+ : undefined;
165
+
166
+ return {
167
+ plan: formatPlan(rateLimitTier),
168
+ fetchedAt: new Date(),
169
+ items,
170
+ };
171
+ }
172
+
173
+ async function getLiveUsageAvailability(ctx: ExtensionContext) {
174
+ if (!ctx.modelRegistry.authStorage.hasAuth(PROVIDER_ID)) {
175
+ return { available: false as const, reason: "no auth configured" };
176
+ }
177
+
178
+ const credential = ctx.modelRegistry.authStorage.get(PROVIDER_ID);
179
+ if (credential?.type === "oauth") {
180
+ return { available: true as const };
181
+ }
182
+
183
+ return {
184
+ available: false as const,
185
+ reason: "API-key auth does not expose Claude subscription quota",
186
+ };
187
+ }
188
+
189
+ const anthropicProvider: UsageProviderDefinition = {
190
+ id: PROVIDER_ID,
191
+ label: "Claude",
192
+ shortLabel: "claude",
193
+ color: { r: 204, g: 124, b: 94 },
194
+ getLiveUsageAvailability,
195
+ fetchLiveUsage: fetchUsage,
196
+ };
197
+
198
+ export default anthropicProvider;
@@ -0,0 +1,204 @@
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import {
3
+ clampPercent,
4
+ formatCount,
5
+ formatPlan,
6
+ makeMeterItem,
7
+ parseISODate,
8
+ readNumber,
9
+ readString,
10
+ } from "./shared.js";
11
+ import type { LiveUsageItem, LiveUsageSnapshot, UsageProviderDefinition } from "./types.js";
12
+
13
+ const PROVIDER_ID = "github-copilot";
14
+
15
+ interface QuotaSnapshot {
16
+ entitlement: number;
17
+ remaining: number;
18
+ percentRemaining: number;
19
+ }
20
+
21
+ function normalizeDomain(input: string): string | undefined {
22
+ const value = input.trim();
23
+ if (!value) return undefined;
24
+ try {
25
+ const url = value.includes("://") ? new URL(value) : new URL(`https://${value}`);
26
+ return url.hostname;
27
+ } catch {
28
+ return undefined;
29
+ }
30
+ }
31
+
32
+ function parseQuotaSnapshot(value: unknown): QuotaSnapshot | undefined {
33
+ const input = value as Record<string, unknown> | null | undefined;
34
+ if (!input || typeof input !== "object") return undefined;
35
+
36
+ const entitlement = readNumber(input.entitlement) ?? 0;
37
+ const remaining = readNumber(input.remaining) ?? 0;
38
+ const explicitPercent = readNumber(input.percent_remaining);
39
+ const derivedPercent = entitlement > 0 ? (remaining / entitlement) * 100 : undefined;
40
+ const percentRemaining = explicitPercent ?? derivedPercent ?? 0;
41
+ const quotaId = readString(input.quota_id) ?? "";
42
+
43
+ if (explicitPercent === undefined && derivedPercent === undefined) return undefined;
44
+ if (entitlement === 0 && remaining === 0 && percentRemaining === 0 && !quotaId) return undefined;
45
+
46
+ return {
47
+ entitlement,
48
+ remaining,
49
+ percentRemaining: clampPercent(percentRemaining),
50
+ };
51
+ }
52
+
53
+ function parseQuotaSnapshots(payload: Record<string, unknown>): {
54
+ premiumInteractions?: QuotaSnapshot;
55
+ chat?: QuotaSnapshot;
56
+ } {
57
+ const quotaSnapshots = payload.quota_snapshots as Record<string, unknown> | undefined;
58
+ let premiumInteractions = parseQuotaSnapshot(quotaSnapshots?.premium_interactions);
59
+ let chat = parseQuotaSnapshot(quotaSnapshots?.chat);
60
+
61
+ if ((!premiumInteractions || !chat) && quotaSnapshots && typeof quotaSnapshots === "object") {
62
+ let firstUsable: QuotaSnapshot | undefined;
63
+ for (const [key, rawValue] of Object.entries(quotaSnapshots)) {
64
+ const snapshot = parseQuotaSnapshot(rawValue);
65
+ if (!snapshot) continue;
66
+ const lowerKey = key.toLowerCase();
67
+ if (!firstUsable) firstUsable = snapshot;
68
+ if (!chat && lowerKey.includes("chat")) {
69
+ chat = snapshot;
70
+ continue;
71
+ }
72
+ if (
73
+ !premiumInteractions &&
74
+ (lowerKey.includes("premium") ||
75
+ lowerKey.includes("completion") ||
76
+ lowerKey.includes("code"))
77
+ ) {
78
+ premiumInteractions = snapshot;
79
+ }
80
+ }
81
+ if (!premiumInteractions && !chat) chat = firstUsable;
82
+ }
83
+
84
+ const monthlyQuotas = payload.monthly_quotas as Record<string, unknown> | undefined;
85
+ const limitedUserQuotas = payload.limited_user_quotas as Record<string, unknown> | undefined;
86
+
87
+ if (!premiumInteractions) {
88
+ const entitlement = readNumber(monthlyQuotas?.completions);
89
+ const remaining = readNumber(limitedUserQuotas?.completions);
90
+ if (entitlement !== undefined && entitlement > 0 && remaining !== undefined) {
91
+ premiumInteractions = {
92
+ entitlement,
93
+ remaining,
94
+ percentRemaining: clampPercent((remaining / entitlement) * 100),
95
+ };
96
+ }
97
+ }
98
+
99
+ if (!chat) {
100
+ const entitlement = readNumber(monthlyQuotas?.chat);
101
+ const remaining = readNumber(limitedUserQuotas?.chat);
102
+ if (entitlement !== undefined && entitlement > 0 && remaining !== undefined) {
103
+ chat = {
104
+ entitlement,
105
+ remaining,
106
+ percentRemaining: clampPercent((remaining / entitlement) * 100),
107
+ };
108
+ }
109
+ }
110
+
111
+ return { premiumInteractions, chat };
112
+ }
113
+
114
+ function makeQuotaMeter(
115
+ key: string,
116
+ label: string,
117
+ quota: QuotaSnapshot,
118
+ resetsAt?: Date,
119
+ ): LiveUsageItem {
120
+ const usedPercent = clampPercent(100 - quota.percentRemaining);
121
+ return makeMeterItem({
122
+ key,
123
+ label,
124
+ usedPercent,
125
+ resetsAt,
126
+ detail:
127
+ quota.entitlement > 0
128
+ ? `${formatCount(quota.remaining)} / ${formatCount(quota.entitlement)} remaining`
129
+ : undefined,
130
+ });
131
+ }
132
+
133
+ function buildUsageUrl(enterpriseUrl?: string): string {
134
+ const domain = enterpriseUrl ? normalizeDomain(enterpriseUrl) : undefined;
135
+ const host = domain && domain !== "github.com" ? `api.${domain}` : "api.github.com";
136
+ return `https://${host}/copilot_internal/user`;
137
+ }
138
+
139
+ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<LiveUsageSnapshot> {
140
+ const credential = ctx.modelRegistry.authStorage.get(PROVIDER_ID);
141
+ if (
142
+ credential?.type !== "oauth" ||
143
+ typeof credential.refresh !== "string" ||
144
+ !credential.refresh.trim()
145
+ ) {
146
+ throw new Error("Not logged in to GitHub Copilot.");
147
+ }
148
+
149
+ const enterpriseUrl =
150
+ typeof credential.enterpriseUrl === "string" ? credential.enterpriseUrl : undefined;
151
+ const response = await fetch(buildUsageUrl(enterpriseUrl), {
152
+ method: "GET",
153
+ headers: {
154
+ Authorization: `token ${credential.refresh}`,
155
+ Accept: "application/json",
156
+ "Editor-Version": "vscode/1.107.0",
157
+ "Editor-Plugin-Version": "copilot-chat/0.35.0",
158
+ "User-Agent": "GitHubCopilotChat/0.35.0",
159
+ "X-Github-Api-Version": "2025-04-01",
160
+ },
161
+ signal,
162
+ });
163
+
164
+ if (response.status === 401 || response.status === 403) {
165
+ throw new Error("GitHub Copilot login expired. Re-run /login.");
166
+ }
167
+ if (!response.ok) {
168
+ const bodyText = await response.text().catch(() => "");
169
+ throw new Error(
170
+ bodyText.trim()
171
+ ? `GitHub Copilot usage request failed (${response.status}): ${bodyText.trim()}`
172
+ : `GitHub Copilot usage request failed (${response.status}).`,
173
+ );
174
+ }
175
+
176
+ const payload = (await response.json()) as Record<string, unknown>;
177
+ const { premiumInteractions, chat } = parseQuotaSnapshots(payload);
178
+ const resetsAt = parseISODate(readString(payload.quota_reset_date));
179
+
180
+ const items: LiveUsageItem[] = [];
181
+ if (premiumInteractions)
182
+ items.push(makeQuotaMeter("premium", "Premium", premiumInteractions, resetsAt));
183
+ if (chat) items.push(makeQuotaMeter("chat", "Chat", chat, resetsAt));
184
+
185
+ if (items.length === 0) {
186
+ throw new Error("GitHub Copilot did not return premium or chat quota data.");
187
+ }
188
+
189
+ return {
190
+ plan: formatPlan(readString(payload.copilot_plan)),
191
+ fetchedAt: new Date(),
192
+ items,
193
+ };
194
+ }
195
+
196
+ const githubCopilotProvider: UsageProviderDefinition = {
197
+ id: PROVIDER_ID,
198
+ label: "GitHub Copilot",
199
+ shortLabel: "copilot",
200
+ color: { r: 168, g: 85, b: 247 },
201
+ fetchLiveUsage: fetchUsage,
202
+ };
203
+
204
+ export default githubCopilotProvider;
@@ -0,0 +1,232 @@
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import {
3
+ clampPercent,
4
+ LiveUsageUnavailableError,
5
+ makeMeterItem,
6
+ parseISODate,
7
+ readArray,
8
+ readString,
9
+ } from "./shared.js";
10
+ import type { LiveUsageItem, LiveUsageSnapshot, UsageProviderDefinition } from "./types.js";
11
+
12
+ const PROVIDER_ID = "google-gemini-cli";
13
+ const QUOTA_URL = "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota";
14
+ const LOAD_CODE_ASSIST_URL = "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist";
15
+
16
+ type GeminiTier = "free-tier" | "legacy-tier" | "standard-tier";
17
+
18
+ interface GeminiAuthPayload {
19
+ token?: string;
20
+ projectId?: string;
21
+ }
22
+
23
+ interface GeminiQuota {
24
+ modelId: string;
25
+ percentLeft: number;
26
+ resetTime?: Date;
27
+ }
28
+
29
+ function readAuthPayload(value: string): GeminiAuthPayload {
30
+ try {
31
+ return JSON.parse(value) as GeminiAuthPayload;
32
+ } catch {
33
+ throw new LiveUsageUnavailableError("Gemini CLI OAuth login is required for live quota data");
34
+ }
35
+ }
36
+
37
+ function parsePlan(tier: GeminiTier | undefined): string | undefined {
38
+ switch (tier) {
39
+ case "standard-tier":
40
+ return "Paid";
41
+ case "free-tier":
42
+ return "Free";
43
+ case "legacy-tier":
44
+ return "Legacy";
45
+ default:
46
+ return undefined;
47
+ }
48
+ }
49
+
50
+ function parseModelLabel(modelId: string): string {
51
+ const value = modelId.trim();
52
+ const slash = value.lastIndexOf("/");
53
+ return slash === -1 ? value : value.slice(slash + 1);
54
+ }
55
+
56
+ function isFlashLiteModel(id: string): boolean {
57
+ return id.includes("flash-lite");
58
+ }
59
+
60
+ function isFlashModel(id: string): boolean {
61
+ return id.includes("flash") && !isFlashLiteModel(id);
62
+ }
63
+
64
+ function isProModel(id: string): boolean {
65
+ return id.includes("pro");
66
+ }
67
+
68
+ function makeMeter(key: string, label: string, quota: GeminiQuota): LiveUsageItem {
69
+ return makeMeterItem({
70
+ key,
71
+ label,
72
+ usedPercent: clampPercent(100 - quota.percentLeft),
73
+ windowLabel: "24h",
74
+ resetsAt: quota.resetTime,
75
+ });
76
+ }
77
+
78
+ async function fetchJson(
79
+ url: string,
80
+ token: string,
81
+ body: Record<string, unknown>,
82
+ signal?: AbortSignal,
83
+ ): Promise<Record<string, unknown>> {
84
+ const response = await fetch(url, {
85
+ method: "POST",
86
+ headers: {
87
+ Authorization: `Bearer ${token}`,
88
+ "Content-Type": "application/json",
89
+ },
90
+ body: JSON.stringify(body),
91
+ signal,
92
+ });
93
+
94
+ if (response.status === 401 || response.status === 403) {
95
+ throw new Error("Gemini CLI login expired. Re-run /login google-gemini-cli.");
96
+ }
97
+
98
+ if (!response.ok) {
99
+ const bodyText = await response.text().catch(() => "");
100
+ throw new Error(
101
+ bodyText.trim()
102
+ ? `Gemini usage request failed (${response.status}): ${bodyText.trim()}`
103
+ : `Gemini usage request failed (${response.status}).`,
104
+ );
105
+ }
106
+
107
+ return (await response.json()) as Record<string, unknown>;
108
+ }
109
+
110
+ async function loadCodeAssistStatus(
111
+ token: string,
112
+ signal?: AbortSignal,
113
+ ): Promise<{ tier?: GeminiTier }> {
114
+ try {
115
+ const payload = await fetchJson(
116
+ LOAD_CODE_ASSIST_URL,
117
+ token,
118
+ { metadata: { ideType: "GEMINI_CLI", pluginType: "GEMINI" } },
119
+ signal,
120
+ );
121
+
122
+ const currentTier = payload.currentTier as Record<string, unknown> | undefined;
123
+ const tierId = readString(currentTier?.id);
124
+
125
+ return {
126
+ tier:
127
+ tierId === "free-tier" || tierId === "legacy-tier" || tierId === "standard-tier"
128
+ ? tierId
129
+ : undefined,
130
+ };
131
+ } catch {
132
+ return {};
133
+ }
134
+ }
135
+
136
+ function parseQuotas(payload: Record<string, unknown>): GeminiQuota[] {
137
+ const buckets = readArray<Record<string, unknown>>(payload.buckets);
138
+ const quotaByModel = new Map<string, GeminiQuota>();
139
+
140
+ for (const bucket of buckets) {
141
+ const input = bucket as Record<string, unknown> | null | undefined;
142
+ const modelId = readString(input?.modelId);
143
+ const remainingFraction =
144
+ typeof input?.remainingFraction === "number" ? input.remainingFraction : undefined;
145
+ if (!modelId || remainingFraction === undefined) continue;
146
+
147
+ const percentLeft = clampPercent(remainingFraction * 100);
148
+ const resetTime = parseISODate(readString(input?.resetTime));
149
+ const quota: GeminiQuota = {
150
+ modelId,
151
+ percentLeft,
152
+ resetTime,
153
+ };
154
+
155
+ const existing = quotaByModel.get(modelId);
156
+ if (!existing || quota.percentLeft < existing.percentLeft) {
157
+ quotaByModel.set(modelId, quota);
158
+ }
159
+ }
160
+
161
+ return [...quotaByModel.values()].sort((left, right) =>
162
+ left.modelId.localeCompare(right.modelId),
163
+ );
164
+ }
165
+
166
+ function buildItems(quotas: GeminiQuota[]): LiveUsageItem[] {
167
+ const lower = quotas.map((quota) => ({ quota, modelId: quota.modelId.toLowerCase() }));
168
+ const pro = lower.filter((entry) => isProModel(entry.modelId)).map((entry) => entry.quota);
169
+ const flash = lower.filter((entry) => isFlashModel(entry.modelId)).map((entry) => entry.quota);
170
+ const flashLite = lower
171
+ .filter((entry) => isFlashLiteModel(entry.modelId))
172
+ .map((entry) => entry.quota);
173
+
174
+ const items: LiveUsageItem[] = [];
175
+ const proMin = pro.sort((left, right) => left.percentLeft - right.percentLeft)[0];
176
+ const flashMin = flash.sort((left, right) => left.percentLeft - right.percentLeft)[0];
177
+ const flashLiteMin = flashLite.sort((left, right) => left.percentLeft - right.percentLeft)[0];
178
+
179
+ if (proMin) items.push(makeMeter("pro", "Pro", proMin));
180
+ if (flashMin) items.push(makeMeter("flash", "Flash", flashMin));
181
+ if (flashLiteMin) items.push(makeMeter("flash-lite", "Flash Lite", flashLiteMin));
182
+
183
+ if (items.length > 0) return items;
184
+
185
+ for (const quota of quotas.slice(0, 3)) {
186
+ items.push(makeMeter(quota.modelId, parseModelLabel(quota.modelId), quota));
187
+ }
188
+ return items;
189
+ }
190
+
191
+ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<LiveUsageSnapshot> {
192
+ const apiKey = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER_ID);
193
+ if (!apiKey) throw new LiveUsageUnavailableError("no auth configured");
194
+
195
+ const auth = readAuthPayload(apiKey);
196
+ const token = typeof auth.token === "string" ? auth.token.trim() : "";
197
+ if (!token) {
198
+ throw new LiveUsageUnavailableError("Gemini CLI OAuth login is required for live quota data");
199
+ }
200
+
201
+ const authProjectId = typeof auth.projectId === "string" ? auth.projectId.trim() : undefined;
202
+ const statusPromise = loadCodeAssistStatus(token, signal);
203
+ const quotaPromise = fetchJson(
204
+ QUOTA_URL,
205
+ token,
206
+ authProjectId ? { project: authProjectId } : {},
207
+ signal,
208
+ );
209
+
210
+ const [status, quotaPayload] = await Promise.all([statusPromise, quotaPromise]);
211
+ const quotas = parseQuotas(quotaPayload);
212
+
213
+ if (quotas.length === 0) {
214
+ throw new Error("Gemini did not return any live quota data.");
215
+ }
216
+
217
+ return {
218
+ plan: parsePlan(status.tier),
219
+ fetchedAt: new Date(),
220
+ items: buildItems(quotas),
221
+ };
222
+ }
223
+
224
+ const googleGeminiCliProvider: UsageProviderDefinition = {
225
+ id: PROVIDER_ID,
226
+ label: "Gemini CLI",
227
+ shortLabel: "gemini",
228
+ color: { r: 171, g: 135, b: 234 },
229
+ fetchLiveUsage: fetchUsage,
230
+ };
231
+
232
+ export default googleGeminiCliProvider;