tau-all-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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -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/telegram/README.md +81 -0
  19. package/extensions/telegram/daemon.mjs +1942 -0
  20. package/extensions/telegram/index.ts +1073 -0
  21. package/extensions/telegram/message-text.d.mts +1 -0
  22. package/extensions/telegram/message-text.mjs +16 -0
  23. package/extensions/usage/anthropic.ts +198 -0
  24. package/extensions/usage/github-copilot.ts +204 -0
  25. package/extensions/usage/google-gemini-cli.ts +232 -0
  26. package/extensions/usage/index.ts +2310 -0
  27. package/extensions/usage/minimax.ts +208 -0
  28. package/extensions/usage/openai-codex.ts +180 -0
  29. package/extensions/usage/openrouter.ts +168 -0
  30. package/extensions/usage/providers.ts +26 -0
  31. package/extensions/usage/shared.ts +156 -0
  32. package/extensions/usage/types.ts +53 -0
  33. package/extensions/usage/zai.ts +186 -0
  34. package/extensions/websearch/README.md +68 -0
  35. package/extensions/websearch/browser/chromium.ts +330 -0
  36. package/extensions/websearch/browser/discovery.ts +75 -0
  37. package/extensions/websearch/browser/firefox.ts +150 -0
  38. package/extensions/websearch/browser/sqlite.ts +38 -0
  39. package/extensions/websearch/config.ts +76 -0
  40. package/extensions/websearch/index.ts +312 -0
  41. package/extensions/websearch/normalize.ts +155 -0
  42. package/extensions/websearch/providers/anthropic.pi.ts +139 -0
  43. package/extensions/websearch/providers/gemini.browser.ts +185 -0
  44. package/extensions/websearch/providers/gemini.pi.ts +108 -0
  45. package/extensions/websearch/providers/openai-codex.browser.ts +77 -0
  46. package/extensions/websearch/providers/openai-codex.pi.ts +49 -0
  47. package/extensions/websearch/providers/openai-codex.shared.ts +123 -0
  48. package/extensions/websearch/providers/pi-model.shared.ts +86 -0
  49. package/extensions/websearch/providers/search-prompt.shared.ts +17 -0
  50. package/extensions/websearch/providers/shared.ts +76 -0
  51. package/extensions/websearch/types.ts +52 -0
  52. package/extensions/worktree.ts +2223 -0
  53. package/package.json +76 -0
  54. package/skills/browser-tools/SKILL.md +253 -0
  55. package/skills/browser-tools/scripts/browser-content.js +100 -0
  56. package/skills/browser-tools/scripts/browser-cookies.js +33 -0
  57. package/skills/browser-tools/scripts/browser-dismiss-cookies.js +455 -0
  58. package/skills/browser-tools/scripts/browser-eval.js +75 -0
  59. package/skills/browser-tools/scripts/browser-logs-tail.js +89 -0
  60. package/skills/browser-tools/scripts/browser-nav.js +28 -0
  61. package/skills/browser-tools/scripts/browser-net-summary.js +115 -0
  62. package/skills/browser-tools/scripts/browser-pick.js +133 -0
  63. package/skills/browser-tools/scripts/browser-screenshot.js +18 -0
  64. package/skills/browser-tools/scripts/browser-start.js +281 -0
  65. package/skills/browser-tools/scripts/browser-watch.js +321 -0
  66. package/skills/browser-tools/scripts/utils.js +60 -0
  67. package/skills/git-clean-history/SKILL.md +45 -0
  68. package/skills/git-commit/SKILL.md +55 -0
  69. package/skills/homeassistant-ops/SKILL.md +184 -0
  70. package/skills/homeassistant-ops/references/api.md +64 -0
  71. package/skills/homeassistant-ops/references/id_conventions.md +71 -0
  72. package/skills/homeassistant-ops/references/playbook.md +150 -0
  73. package/skills/homeassistant-ops/scripts/ha_ops.js +2942 -0
  74. package/skills/openscad/SKILL.md +149 -0
  75. package/skills/openscad/examples/parametric_box.scad +92 -0
  76. package/skills/openscad/examples/phone_stand.scad +95 -0
  77. package/skills/openscad/scripts/common.sh +49 -0
  78. package/skills/openscad/scripts/export-stl.sh +56 -0
  79. package/skills/openscad/scripts/extract-params.sh +147 -0
  80. package/skills/openscad/scripts/multi-preview.sh +68 -0
  81. package/skills/openscad/scripts/preview.sh +74 -0
  82. package/skills/openscad/scripts/render-with-params.sh +91 -0
  83. package/skills/openscad/scripts/validate.sh +46 -0
  84. package/skills/oracle/SKILL.md +64 -0
  85. package/skills/oracle/scripts/oracle +249 -0
  86. package/skills/oracle/scripts/oracle-bundle +227 -0
  87. package/skills/sentry/SKILL.md +198 -0
  88. package/skills/sentry/lib/auth.js +99 -0
  89. package/skills/sentry/scripts/fetch-event.js +325 -0
  90. package/skills/sentry/scripts/fetch-issue.js +368 -0
  91. package/skills/sentry/scripts/list-issues.js +245 -0
  92. package/skills/sentry/scripts/search-events.js +291 -0
  93. package/skills/sentry/scripts/search-logs.js +234 -0
  94. package/skills/update-changelog/SKILL.md +135 -0
  95. package/skills/web-design/SKILL.md +117 -0
@@ -0,0 +1,208 @@
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import {
3
+ clampPercent,
4
+ formatCount,
5
+ formatPlan,
6
+ formatWindowLabel,
7
+ HttpError,
8
+ LiveUsageUnavailableError,
9
+ makeMeterItem,
10
+ parseEpochDate,
11
+ readArray,
12
+ readNumber,
13
+ readObject,
14
+ readString,
15
+ } from "./shared.js";
16
+ import type { LiveUsageItem, LiveUsageSnapshot, UsageProviderDefinition } from "./types.js";
17
+
18
+ const PROVIDER_ID = "minimax";
19
+ const REMAINS_URLS = [
20
+ "https://api.minimax.io/v1/api/openplatform/coding_plan/remains",
21
+ "https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains",
22
+ ] as const;
23
+
24
+ function readNestedObject(
25
+ input: Record<string, unknown> | undefined,
26
+ ...keys: string[]
27
+ ): Record<string, unknown> | undefined {
28
+ for (const key of keys) {
29
+ const value = readObject(input?.[key]);
30
+ if (value) return value;
31
+ }
32
+ return undefined;
33
+ }
34
+
35
+ function parsePlanName(data: Record<string, unknown> | undefined): string | undefined {
36
+ const comboCard = readObject(data?.current_combo_card);
37
+ return [
38
+ readString(data?.current_subscribe_title),
39
+ readString(data?.plan_name),
40
+ readString(data?.combo_title),
41
+ readString(data?.current_plan_title),
42
+ readString(comboCard?.title),
43
+ ].find(Boolean);
44
+ }
45
+
46
+ function makeUsageMeter(
47
+ totalPrompts: number | undefined,
48
+ usedPrompts: number | undefined,
49
+ windowMinutes: number | undefined,
50
+ resetsAt: Date | undefined,
51
+ ): LiveUsageItem {
52
+ const remainingPrompts =
53
+ totalPrompts !== undefined && usedPrompts !== undefined
54
+ ? Math.max(0, totalPrompts - usedPrompts)
55
+ : undefined;
56
+ const usedPercent =
57
+ totalPrompts !== undefined && totalPrompts > 0 && usedPrompts !== undefined
58
+ ? clampPercent((usedPrompts / totalPrompts) * 100)
59
+ : 0;
60
+
61
+ return makeMeterItem({
62
+ key: "prompts",
63
+ label: "Prompts",
64
+ usedPercent,
65
+ windowLabel: formatWindowLabel(windowMinutes),
66
+ resetsAt,
67
+ detail:
68
+ totalPrompts !== undefined && totalPrompts > 0 && remainingPrompts !== undefined
69
+ ? `${formatCount(remainingPrompts)} / ${formatCount(totalPrompts)} remaining`
70
+ : undefined,
71
+ });
72
+ }
73
+
74
+ async function fetchPayload(
75
+ url: string,
76
+ apiKey: string,
77
+ signal?: AbortSignal,
78
+ ): Promise<Record<string, unknown>> {
79
+ const response = await fetch(url, {
80
+ method: "GET",
81
+ headers: {
82
+ Authorization: `Bearer ${apiKey}`,
83
+ accept: "application/json",
84
+ "Content-Type": "application/json",
85
+ "MM-API-Source": "pi usage",
86
+ },
87
+ signal,
88
+ });
89
+
90
+ if (response.status === 401 || response.status === 403) {
91
+ throw new HttpError(response.status, `MiniMax usage request failed (${response.status}).`);
92
+ }
93
+
94
+ if (!response.ok) {
95
+ const bodyText = await response.text().catch(() => "");
96
+ throw new Error(
97
+ bodyText.trim()
98
+ ? `MiniMax usage request failed (${response.status}): ${bodyText.trim()}`
99
+ : `MiniMax usage request failed (${response.status}).`,
100
+ );
101
+ }
102
+
103
+ return (await response.json()) as Record<string, unknown>;
104
+ }
105
+
106
+ function parseUsageSnapshot(payload: Record<string, unknown>): {
107
+ planName?: string;
108
+ totalPrompts?: number;
109
+ usedPrompts?: number;
110
+ windowMinutes?: number;
111
+ resetsAt?: Date;
112
+ } {
113
+ const data = readNestedObject(payload, "data") ?? payload;
114
+ const baseResp = readNestedObject(data, "base_resp") ?? readNestedObject(payload, "base_resp");
115
+ const statusCode = readNumber(baseResp?.status_code);
116
+ const statusMessage = readString(baseResp?.status_msg);
117
+
118
+ if (statusCode !== undefined && statusCode !== 0) {
119
+ throw new Error(statusMessage ?? `status_code ${statusCode}`);
120
+ }
121
+
122
+ const modelRemains = readArray<Record<string, unknown>>(data.model_remains ?? data.modelRemains);
123
+ const first = modelRemains[0];
124
+ if (!first) {
125
+ throw new Error("MiniMax did not return any coding plan quota data.");
126
+ }
127
+
128
+ const totalPrompts = readNumber(
129
+ first.current_interval_total_count ?? first.currentIntervalTotalCount,
130
+ );
131
+ const usedPrompts = readNumber(
132
+ first.current_interval_usage_count ?? first.currentIntervalUsageCount,
133
+ );
134
+ const startTime = parseEpochDate(readNumber(first.start_time ?? first.startTime));
135
+ const endTime = parseEpochDate(readNumber(first.end_time ?? first.endTime));
136
+ const remainsTime = readNumber(first.remains_time ?? first.remainsTime);
137
+
138
+ const windowMinutes =
139
+ startTime && endTime
140
+ ? Math.max(0, Math.round((endTime.getTime() - startTime.getTime()) / 60_000))
141
+ : undefined;
142
+ const resetsAt =
143
+ endTime && endTime.getTime() > Date.now()
144
+ ? endTime
145
+ : remainsTime !== undefined && remainsTime > 0
146
+ ? new Date(Date.now() + (remainsTime > 1_000_000_000 ? remainsTime : remainsTime * 1000))
147
+ : undefined;
148
+
149
+ return {
150
+ planName: parsePlanName(data),
151
+ totalPrompts,
152
+ usedPrompts,
153
+ windowMinutes: windowMinutes && windowMinutes > 0 ? windowMinutes : undefined,
154
+ resetsAt,
155
+ };
156
+ }
157
+
158
+ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<LiveUsageSnapshot> {
159
+ const apiKey = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER_ID);
160
+ if (!apiKey) throw new LiveUsageUnavailableError("no auth configured");
161
+
162
+ let payload: Record<string, unknown> | undefined;
163
+ let lastError: unknown;
164
+
165
+ for (const url of REMAINS_URLS) {
166
+ try {
167
+ payload = await fetchPayload(url, apiKey, signal);
168
+ break;
169
+ } catch (error) {
170
+ lastError = error;
171
+ if (!(error instanceof HttpError) || (error.status !== 401 && error.status !== 403)) {
172
+ throw error;
173
+ }
174
+ }
175
+ }
176
+
177
+ if (!payload) {
178
+ if (lastError instanceof HttpError && (lastError.status === 401 || lastError.status === 403)) {
179
+ throw new Error("MiniMax API key is invalid or expired.");
180
+ }
181
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
182
+ }
183
+
184
+ const snapshot = parseUsageSnapshot(payload);
185
+
186
+ return {
187
+ plan: formatPlan(snapshot.planName),
188
+ fetchedAt: new Date(),
189
+ items: [
190
+ makeUsageMeter(
191
+ snapshot.totalPrompts,
192
+ snapshot.usedPrompts,
193
+ snapshot.windowMinutes,
194
+ snapshot.resetsAt,
195
+ ),
196
+ ],
197
+ };
198
+ }
199
+
200
+ const minimaxProvider: UsageProviderDefinition = {
201
+ id: PROVIDER_ID,
202
+ label: "MiniMax",
203
+ shortLabel: "mini",
204
+ color: { r: 254, g: 96, b: 60 },
205
+ fetchLiveUsage: fetchUsage,
206
+ };
207
+
208
+ export default minimaxProvider;
@@ -0,0 +1,180 @@
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import {
3
+ clampPercent,
4
+ formatCurrency,
5
+ formatPlan,
6
+ formatWindowLabel,
7
+ makeMeterItem,
8
+ makeStatItem,
9
+ parseEpochDate,
10
+ readNumber,
11
+ readString,
12
+ } from "./shared.js";
13
+ import type { LiveUsageItem, LiveUsageSnapshot, UsageProviderDefinition } from "./types.js";
14
+
15
+ const PROVIDER_ID = "openai-codex";
16
+ const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
17
+ const SESSION_WINDOW_MINUTES = 5 * 60;
18
+ const WEEK_WINDOW_MINUTES = 7 * 24 * 60;
19
+
20
+ interface UsageWindow {
21
+ usedPercent: number;
22
+ windowMinutes: number;
23
+ resetsAt?: Date;
24
+ }
25
+
26
+ function decodeWindow(value: unknown): UsageWindow | undefined {
27
+ const input = value as Record<string, unknown> | null | undefined;
28
+ const usedPercent = readNumber(input?.used_percent);
29
+ const resetAt = readNumber(input?.reset_at);
30
+ const windowSeconds = readNumber(input?.limit_window_seconds);
31
+ if (usedPercent === undefined || windowSeconds === undefined) return undefined;
32
+
33
+ return {
34
+ usedPercent: clampPercent(usedPercent),
35
+ windowMinutes: Math.round(windowSeconds / 60),
36
+ resetsAt: parseEpochDate(resetAt),
37
+ };
38
+ }
39
+
40
+ function windowRole(window: UsageWindow): "session" | "week" | "unknown" {
41
+ if (window.windowMinutes === SESSION_WINDOW_MINUTES) return "session";
42
+ if (window.windowMinutes === WEEK_WINDOW_MINUTES) return "week";
43
+ return "unknown";
44
+ }
45
+
46
+ function normalizeWindows(
47
+ primary?: UsageWindow,
48
+ secondary?: UsageWindow,
49
+ ): { session?: UsageWindow; week?: UsageWindow } {
50
+ const windows = [primary, secondary];
51
+ const normalized: { session?: UsageWindow; week?: UsageWindow } = {};
52
+
53
+ for (const window of windows) {
54
+ if (!window) continue;
55
+
56
+ const role = windowRole(window);
57
+ if (role === "session" && !normalized.session) {
58
+ normalized.session = window;
59
+ continue;
60
+ }
61
+ if (role === "week" && !normalized.week) {
62
+ normalized.week = window;
63
+ }
64
+ }
65
+
66
+ return normalized;
67
+ }
68
+
69
+ function makeWindowMeter(key: string, label: string, window: UsageWindow): LiveUsageItem {
70
+ return makeMeterItem({
71
+ key,
72
+ label,
73
+ usedPercent: window.usedPercent,
74
+ windowLabel: formatWindowLabel(window.windowMinutes),
75
+ resetsAt: window.resetsAt,
76
+ });
77
+ }
78
+
79
+ function extractErrorMessage(status: number, bodyText: string): string {
80
+ const defaultMessage = `OpenAI Codex usage request failed (${status}).`;
81
+ if (status === 401 || status === 403) {
82
+ return "OpenAI Codex login expired. Re-run /login.";
83
+ }
84
+ if (!bodyText.trim()) return defaultMessage;
85
+
86
+ try {
87
+ const parsed = JSON.parse(bodyText) as Record<string, unknown>;
88
+ const error = parsed.error as Record<string, unknown> | undefined;
89
+ const errorMessage = readString(error?.message) ?? readString(parsed.message);
90
+ const errorCode = readString(error?.code) ?? readString(error?.type);
91
+ const resetsAt = readNumber(error?.resets_at);
92
+
93
+ if (
94
+ status === 429 ||
95
+ errorCode === "usage_limit_reached" ||
96
+ errorCode === "rate_limit_exceeded"
97
+ ) {
98
+ if (resetsAt !== undefined) {
99
+ const mins = Math.max(0, Math.round((resetsAt * 1000 - Date.now()) / 60_000));
100
+ return `OpenAI Codex usage limit reached. Try again in ~${mins}m.`;
101
+ }
102
+ return "OpenAI Codex usage limit reached.";
103
+ }
104
+
105
+ return errorMessage ? `${defaultMessage} ${errorMessage}` : defaultMessage;
106
+ } catch {
107
+ return `${defaultMessage} ${bodyText.trim()}`;
108
+ }
109
+ }
110
+
111
+ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<LiveUsageSnapshot> {
112
+ const apiKey = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER_ID);
113
+ if (!apiKey) throw new Error("Not logged in to OpenAI Codex.");
114
+
115
+ const credential = ctx.modelRegistry.authStorage.get(PROVIDER_ID);
116
+ const accountId =
117
+ credential?.type === "oauth" && typeof credential.accountId === "string"
118
+ ? credential.accountId
119
+ : undefined;
120
+
121
+ const headers = new Headers({
122
+ Authorization: `Bearer ${apiKey}`,
123
+ Accept: "application/json",
124
+ "User-Agent": "pi usage",
125
+ });
126
+ if (accountId) headers.set("ChatGPT-Account-Id", accountId);
127
+
128
+ const response = await fetch(USAGE_URL, { headers, signal });
129
+ if (!response.ok) {
130
+ const bodyText = await response.text().catch(() => "");
131
+ throw new Error(extractErrorMessage(response.status, bodyText));
132
+ }
133
+
134
+ const payload = (await response.json()) as Record<string, unknown>;
135
+ const rateLimit = payload.rate_limit as Record<string, unknown> | undefined;
136
+ const credits = payload.credits as Record<string, unknown> | undefined;
137
+ const { session, week } = normalizeWindows(
138
+ decodeWindow(rateLimit?.primary_window),
139
+ decodeWindow(rateLimit?.secondary_window),
140
+ );
141
+
142
+ const items: LiveUsageItem[] = [];
143
+ if (session) items.push(makeWindowMeter("session", "Session", session));
144
+ if (week) items.push(makeWindowMeter("week", "Week", week));
145
+
146
+ const unlimitedCredits = credits?.unlimited === true;
147
+ const creditBalance = readNumber(credits?.balance);
148
+ if (unlimitedCredits) {
149
+ items.push(makeStatItem({ key: "credits", label: "Credits", value: "Unlimited" }));
150
+ } else if (creditBalance !== undefined) {
151
+ items.push(
152
+ makeStatItem({
153
+ key: "credits",
154
+ label: "Credits",
155
+ value: formatCurrency(creditBalance),
156
+ detail: credits?.has_credits === false ? "remaining" : undefined,
157
+ }),
158
+ );
159
+ }
160
+
161
+ if (items.length === 0) {
162
+ throw new Error("OpenAI Codex did not return any session, weekly, or credit usage data.");
163
+ }
164
+
165
+ return {
166
+ plan: formatPlan(readString(payload.plan_type)),
167
+ fetchedAt: new Date(),
168
+ items,
169
+ };
170
+ }
171
+
172
+ const openAICodexProvider: UsageProviderDefinition = {
173
+ id: PROVIDER_ID,
174
+ label: "OpenAI Codex",
175
+ shortLabel: "codex",
176
+ color: { r: 73, g: 163, b: 176 },
177
+ fetchLiveUsage: fetchUsage,
178
+ };
179
+
180
+ export default openAICodexProvider;
@@ -0,0 +1,168 @@
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import {
3
+ formatCurrency,
4
+ HttpError,
5
+ LiveUsageUnavailableError,
6
+ makeMeterItem,
7
+ makeStatItem,
8
+ readNumber,
9
+ readString,
10
+ } from "./shared.js";
11
+ import type { LiveUsageItem, LiveUsageSnapshot, UsageProviderDefinition } from "./types.js";
12
+
13
+ const PROVIDER_ID = "openrouter";
14
+ const API_BASE_URL = "https://openrouter.ai/api/v1";
15
+
16
+ interface CreditsPayload {
17
+ data?: {
18
+ total_credits?: unknown;
19
+ total_usage?: unknown;
20
+ };
21
+ }
22
+
23
+ interface KeyPayload {
24
+ data?: {
25
+ limit?: unknown;
26
+ usage?: unknown;
27
+ rate_limit?: {
28
+ requests?: unknown;
29
+ interval?: unknown;
30
+ };
31
+ };
32
+ }
33
+
34
+ function isAbortError(error: unknown): boolean {
35
+ return error instanceof Error && error.name === "AbortError";
36
+ }
37
+
38
+ async function fetchJson(
39
+ url: string,
40
+ apiKey: string,
41
+ signal?: AbortSignal,
42
+ ): Promise<Record<string, unknown>> {
43
+ const response = await fetch(url, {
44
+ method: "GET",
45
+ headers: {
46
+ Authorization: `Bearer ${apiKey}`,
47
+ Accept: "application/json",
48
+ "X-Title": "pi usage",
49
+ },
50
+ signal,
51
+ });
52
+
53
+ if (response.status === 401 || response.status === 403) {
54
+ throw new HttpError(response.status, "OpenRouter API key is invalid or expired.");
55
+ }
56
+
57
+ if (!response.ok) {
58
+ const bodyText = await response.text().catch(() => "");
59
+ throw new HttpError(
60
+ response.status,
61
+ bodyText.trim()
62
+ ? `OpenRouter usage request failed (${response.status}): ${bodyText.trim()}`
63
+ : `OpenRouter usage request failed (${response.status}).`,
64
+ );
65
+ }
66
+
67
+ return (await response.json()) as Record<string, unknown>;
68
+ }
69
+
70
+ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<LiveUsageSnapshot> {
71
+ const apiKey = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER_ID);
72
+ if (!apiKey) throw new LiveUsageUnavailableError("no auth configured");
73
+
74
+ const [creditsResult, keyResult] = await Promise.allSettled([
75
+ fetchJson(`${API_BASE_URL}/credits`, apiKey, signal),
76
+ fetchJson(`${API_BASE_URL}/key`, apiKey, signal),
77
+ ]);
78
+
79
+ if (creditsResult.status === "rejected") {
80
+ throw creditsResult.reason;
81
+ }
82
+
83
+ const creditsPayload = creditsResult.value as CreditsPayload;
84
+ let keyPayload: KeyPayload | undefined;
85
+ if (keyResult.status === "fulfilled") {
86
+ keyPayload = keyResult.value as KeyPayload;
87
+ } else if (isAbortError(keyResult.reason)) {
88
+ throw keyResult.reason;
89
+ } else if (
90
+ !(keyResult.reason instanceof HttpError) ||
91
+ (keyResult.reason.status !== 403 &&
92
+ keyResult.reason.status !== 404 &&
93
+ keyResult.reason.status !== 405)
94
+ ) {
95
+ throw keyResult.reason;
96
+ }
97
+
98
+ const totalCredits = readNumber(creditsPayload.data?.total_credits) ?? 0;
99
+ const totalUsage = readNumber(creditsPayload.data?.total_usage) ?? 0;
100
+ const balance = Math.max(0, totalCredits - totalUsage);
101
+
102
+ const keyLimit = readNumber(keyPayload?.data?.limit);
103
+ const keyUsage = readNumber(keyPayload?.data?.usage);
104
+ const rateLimitRequests = readNumber(keyPayload?.data?.rate_limit?.requests);
105
+ const rateLimitInterval = readString(keyPayload?.data?.rate_limit?.interval);
106
+
107
+ const items: LiveUsageItem[] = [];
108
+ if (totalCredits > 0) {
109
+ items.push(
110
+ makeMeterItem({
111
+ key: "credits",
112
+ label: "Credits",
113
+ usedPercent: totalCredits > 0 ? (totalUsage / totalCredits) * 100 : 0,
114
+ detail: `${formatCurrency(balance)} / ${formatCurrency(totalCredits)} remaining`,
115
+ }),
116
+ );
117
+ } else {
118
+ items.push(
119
+ makeStatItem({
120
+ key: "balance",
121
+ label: "Balance",
122
+ value: formatCurrency(balance),
123
+ detail: totalUsage > 0 ? `${formatCurrency(totalUsage)} used` : undefined,
124
+ }),
125
+ );
126
+ }
127
+
128
+ if (keyLimit !== undefined && keyLimit > 0 && keyUsage !== undefined && keyUsage >= 0) {
129
+ const keyRemaining = Math.max(0, keyLimit - keyUsage);
130
+ items.push(
131
+ makeMeterItem({
132
+ key: "key-quota",
133
+ label: "Key quota",
134
+ usedPercent: keyLimit > 0 ? (keyUsage / keyLimit) * 100 : 0,
135
+ detail: `${formatCurrency(keyRemaining)} / ${formatCurrency(keyLimit)} remaining`,
136
+ }),
137
+ );
138
+ }
139
+
140
+ if (rateLimitRequests !== undefined && rateLimitInterval) {
141
+ items.push(
142
+ makeStatItem({
143
+ key: "rate-limit",
144
+ label: "Rate limit",
145
+ value: `${Math.round(rateLimitRequests)} / ${rateLimitInterval}`,
146
+ }),
147
+ );
148
+ }
149
+
150
+ if (items.length === 0) {
151
+ throw new Error("OpenRouter did not return any live usage data.");
152
+ }
153
+
154
+ return {
155
+ fetchedAt: new Date(),
156
+ items,
157
+ };
158
+ }
159
+
160
+ const openRouterProvider: UsageProviderDefinition = {
161
+ id: PROVIDER_ID,
162
+ label: "OpenRouter",
163
+ shortLabel: "router",
164
+ color: { r: 100, g: 103, b: 242 },
165
+ fetchLiveUsage: fetchUsage,
166
+ };
167
+
168
+ export default openRouterProvider;
@@ -0,0 +1,26 @@
1
+ import anthropicProvider from "./anthropic.js";
2
+ import githubCopilotProvider from "./github-copilot.js";
3
+ import googleGeminiCliProvider from "./google-gemini-cli.js";
4
+ import minimaxProvider from "./minimax.js";
5
+ import openAICodexProvider from "./openai-codex.js";
6
+ import openRouterProvider from "./openrouter.js";
7
+ import type { UsageProviderDefinition } from "./types.js";
8
+ import zaiProvider from "./zai.js";
9
+
10
+ export const SUPPORTED_PROVIDERS: readonly UsageProviderDefinition[] = [
11
+ openAICodexProvider,
12
+ anthropicProvider,
13
+ githubCopilotProvider,
14
+ googleGeminiCliProvider,
15
+ openRouterProvider,
16
+ zaiProvider,
17
+ minimaxProvider,
18
+ ];
19
+
20
+ const SUPPORTED_PROVIDER_BY_ID = new Map<string, UsageProviderDefinition>(
21
+ SUPPORTED_PROVIDERS.map((provider) => [provider.id, provider]),
22
+ );
23
+
24
+ export function getSupportedProvider(providerId: string): UsageProviderDefinition | undefined {
25
+ return SUPPORTED_PROVIDER_BY_ID.get(providerId);
26
+ }