opencode-quotas 0.0.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.
Files changed (136) hide show
  1. package/README.md +344 -0
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +3 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/src/cli.d.ts +3 -0
  7. package/dist/src/cli.d.ts.map +1 -0
  8. package/dist/src/cli.js +42 -0
  9. package/dist/src/cli.js.map +1 -0
  10. package/dist/src/constants.d.ts +9 -0
  11. package/dist/src/constants.d.ts.map +1 -0
  12. package/dist/src/constants.js +15 -0
  13. package/dist/src/constants.js.map +1 -0
  14. package/dist/src/defaults.d.ts +3 -0
  15. package/dist/src/defaults.d.ts.map +1 -0
  16. package/dist/src/defaults.js +52 -0
  17. package/dist/src/defaults.js.map +1 -0
  18. package/dist/src/index.d.ts +6 -0
  19. package/dist/src/index.d.ts.map +1 -0
  20. package/dist/src/index.js +265 -0
  21. package/dist/src/index.js.map +1 -0
  22. package/dist/src/interfaces.d.ts +197 -0
  23. package/dist/src/interfaces.d.ts.map +1 -0
  24. package/dist/src/interfaces.js +2 -0
  25. package/dist/src/interfaces.js.map +1 -0
  26. package/dist/src/logger.d.ts +14 -0
  27. package/dist/src/logger.d.ts.map +1 -0
  28. package/dist/src/logger.js +44 -0
  29. package/dist/src/logger.js.map +1 -0
  30. package/dist/src/plugin-state.d.ts +20 -0
  31. package/dist/src/plugin-state.d.ts.map +1 -0
  32. package/dist/src/plugin-state.js +53 -0
  33. package/dist/src/plugin-state.js.map +1 -0
  34. package/dist/src/providers/antigravity/auth.d.ts +12 -0
  35. package/dist/src/providers/antigravity/auth.d.ts.map +1 -0
  36. package/dist/src/providers/antigravity/auth.js +109 -0
  37. package/dist/src/providers/antigravity/auth.js.map +1 -0
  38. package/dist/src/providers/antigravity/index.d.ts +2 -0
  39. package/dist/src/providers/antigravity/index.d.ts.map +1 -0
  40. package/dist/src/providers/antigravity/index.js +2 -0
  41. package/dist/src/providers/antigravity/index.js.map +1 -0
  42. package/dist/src/providers/antigravity/provider.d.ts +34 -0
  43. package/dist/src/providers/antigravity/provider.d.ts.map +1 -0
  44. package/dist/src/providers/antigravity/provider.js +216 -0
  45. package/dist/src/providers/antigravity/provider.js.map +1 -0
  46. package/dist/src/providers/codex.d.ts +4 -0
  47. package/dist/src/providers/codex.d.ts.map +1 -0
  48. package/dist/src/providers/codex.js +242 -0
  49. package/dist/src/providers/codex.js.map +1 -0
  50. package/dist/src/providers/github.d.ts +4 -0
  51. package/dist/src/providers/github.d.ts.map +1 -0
  52. package/dist/src/providers/github.js +139 -0
  53. package/dist/src/providers/github.js.map +1 -0
  54. package/dist/src/quota-cache.d.ts +26 -0
  55. package/dist/src/quota-cache.d.ts.map +1 -0
  56. package/dist/src/quota-cache.js +107 -0
  57. package/dist/src/quota-cache.js.map +1 -0
  58. package/dist/src/registry.d.ts +3 -0
  59. package/dist/src/registry.d.ts.map +1 -0
  60. package/dist/src/registry.js +23 -0
  61. package/dist/src/registry.js.map +1 -0
  62. package/dist/src/services/aggregation-service.d.ts +34 -0
  63. package/dist/src/services/aggregation-service.d.ts.map +1 -0
  64. package/dist/src/services/aggregation-service.js +89 -0
  65. package/dist/src/services/aggregation-service.js.map +1 -0
  66. package/dist/src/services/config-loader.d.ts +25 -0
  67. package/dist/src/services/config-loader.d.ts.map +1 -0
  68. package/dist/src/services/config-loader.js +105 -0
  69. package/dist/src/services/config-loader.js.map +1 -0
  70. package/dist/src/services/history-service.d.ts +15 -0
  71. package/dist/src/services/history-service.d.ts.map +1 -0
  72. package/dist/src/services/history-service.js +99 -0
  73. package/dist/src/services/history-service.js.map +1 -0
  74. package/dist/src/services/prediction-engine.d.ts +47 -0
  75. package/dist/src/services/prediction-engine.d.ts.map +1 -0
  76. package/dist/src/services/prediction-engine.js +94 -0
  77. package/dist/src/services/prediction-engine.js.map +1 -0
  78. package/dist/src/services/quota-service.d.ts +41 -0
  79. package/dist/src/services/quota-service.d.ts.map +1 -0
  80. package/dist/src/services/quota-service.js +257 -0
  81. package/dist/src/services/quota-service.js.map +1 -0
  82. package/dist/src/tools/quotas.d.ts +11 -0
  83. package/dist/src/tools/quotas.d.ts.map +1 -0
  84. package/dist/src/tools/quotas.js +62 -0
  85. package/dist/src/tools/quotas.js.map +1 -0
  86. package/dist/src/ui/progress-bar.d.ts +20 -0
  87. package/dist/src/ui/progress-bar.d.ts.map +1 -0
  88. package/dist/src/ui/progress-bar.js +150 -0
  89. package/dist/src/ui/progress-bar.js.map +1 -0
  90. package/dist/src/ui/quota-table.d.ts +15 -0
  91. package/dist/src/ui/quota-table.d.ts.map +1 -0
  92. package/dist/src/ui/quota-table.js +136 -0
  93. package/dist/src/ui/quota-table.js.map +1 -0
  94. package/dist/src/utils/debug.d.ts +1 -0
  95. package/dist/src/utils/debug.d.ts.map +1 -0
  96. package/dist/src/utils/debug.js +3 -0
  97. package/dist/src/utils/debug.js.map +1 -0
  98. package/dist/src/utils/paths.d.ts +7 -0
  99. package/dist/src/utils/paths.d.ts.map +1 -0
  100. package/dist/src/utils/paths.js +37 -0
  101. package/dist/src/utils/paths.js.map +1 -0
  102. package/dist/src/utils/time.d.ts +3 -0
  103. package/dist/src/utils/time.d.ts.map +1 -0
  104. package/dist/src/utils/time.js +38 -0
  105. package/dist/src/utils/time.js.map +1 -0
  106. package/dist/src/utils/validation.d.ts +6 -0
  107. package/dist/src/utils/validation.d.ts.map +1 -0
  108. package/dist/src/utils/validation.js +66 -0
  109. package/dist/src/utils/validation.js.map +1 -0
  110. package/package.json +42 -0
  111. package/src/cli.ts +53 -0
  112. package/src/constants.ts +17 -0
  113. package/src/defaults.ts +53 -0
  114. package/src/index.ts +338 -0
  115. package/src/interfaces.ts +258 -0
  116. package/src/logger.ts +55 -0
  117. package/src/plugin-state.ts +65 -0
  118. package/src/providers/antigravity/auth.ts +163 -0
  119. package/src/providers/antigravity/index.ts +1 -0
  120. package/src/providers/antigravity/provider.ts +337 -0
  121. package/src/providers/codex.ts +327 -0
  122. package/src/providers/github.ts +161 -0
  123. package/src/quota-cache.ts +157 -0
  124. package/src/registry.ts +30 -0
  125. package/src/services/aggregation-service.ts +116 -0
  126. package/src/services/config-loader.ts +124 -0
  127. package/src/services/history-service.ts +116 -0
  128. package/src/services/prediction-engine.ts +133 -0
  129. package/src/services/quota-service.ts +343 -0
  130. package/src/tools/quotas.ts +78 -0
  131. package/src/ui/progress-bar.ts +204 -0
  132. package/src/ui/quota-table.ts +173 -0
  133. package/src/utils/debug.ts +1 -0
  134. package/src/utils/paths.ts +41 -0
  135. package/src/utils/time.ts +40 -0
  136. package/src/utils/validation.ts +63 -0
@@ -0,0 +1,258 @@
1
+ export interface QuotaData {
2
+ id: string; // Unique identifier (e.g., "codex-primary", "ag-flash")
3
+ providerName: string; // Display name
4
+ used: number;
5
+ limit: number | null;
6
+ unit: string;
7
+
8
+ /**
9
+ * Reset time description (e.g. "in 2h 41m" or "at 12:00").
10
+ */
11
+ reset?: string;
12
+
13
+ /**
14
+ * Predicted time until limit is reached (e.g. "in 12m (predicted)").
15
+ */
16
+ predictedReset?: string;
17
+
18
+ /**
19
+ * Window or period description (e.g. "5h window" or "Monthly").
20
+ */
21
+ window?: string;
22
+
23
+ /**
24
+ * Extra information or alerts (e.g. "!!" or "unlimited").
25
+ */
26
+ info?: string;
27
+
28
+ /**
29
+ * @deprecated Use reset, window, info instead.
30
+ */
31
+ details?: string;
32
+ }
33
+
34
+ export type QuotaColumn =
35
+ | "name"
36
+ | "bar"
37
+ | "percent"
38
+ | "value"
39
+ | "reset"
40
+ | "window"
41
+ | "info"
42
+ | "status"
43
+ | "ettl";
44
+
45
+ export interface QuotaConfig {
46
+ displayMode: QuotaDisplayMode;
47
+ progressBar?: ProgressBarConfig;
48
+ table?: {
49
+ /**
50
+ * Columns to display in the quota table.
51
+ * Defaults to a smart selection based on data.
52
+ */
53
+ columns?: QuotaColumn[];
54
+ /**
55
+ * Whether to render the table header row (column labels)
56
+ */
57
+ header?: boolean;
58
+ };
59
+ /**
60
+ * Whether to show quotas in the chat footer automatically.
61
+ * Defaults to true.
62
+ */
63
+ footer?: boolean;
64
+ /**
65
+ * Whether to show the plugin title/header (bold line) in the footer.
66
+ * Defaults to true.
67
+ */
68
+ showFooterTitle?: boolean;
69
+ /**
70
+ * List of quota IDs to hide from display.
71
+ */
72
+ disabled?: string[];
73
+ /**
74
+ * Only show quotas relevant to the current model (best-effort matching).
75
+ */
76
+ filterByCurrentModel?: boolean;
77
+ /**
78
+ * Enable debug logging to ~/.local/share/opencode/quotas-debug.log
79
+ */
80
+ debug?: boolean;
81
+ /**
82
+ * Optional aggregation groups.
83
+ */
84
+ aggregatedGroups?: AggregatedGroup[];
85
+ /**
86
+ * Max history age in hours. Defaults to 24.
87
+ */
88
+ historyMaxAgeHours?: number;
89
+ /**
90
+ * Polling interval in milliseconds. Defaults to 60000 (1 minute).
91
+ */
92
+ pollingInterval?: number;
93
+ /**
94
+ * Short time window for regression to capture spikes (minutes). Defaults to 5.
95
+ */
96
+ predictionShortWindowMinutes?: number;
97
+ /**
98
+ * Whether to show quotas that did not match any aggregation group.
99
+ * Defaults to true.
100
+ */
101
+ showUnaggregated?: boolean;
102
+ }
103
+
104
+ export type AggregationStrategy =
105
+ | "most_critical" // Predicted time-to-limit (requires history)
106
+ | "min" // Lowest percentage used
107
+ | "max" // Highest percentage used
108
+ | "mean" // Average percentage used
109
+ | "median"; // Median percentage used
110
+
111
+ export interface AggregatedGroup {
112
+ /**
113
+ * Unique ID for the resulting group (e.g., "ag-flash", "codex-smart").
114
+ */
115
+ id: string;
116
+ /**
117
+ * Display name (e.g., "Antigravity Flash", "Codex Usage").
118
+ */
119
+ name: string;
120
+ /**
121
+ * Explicit IDs of quotas to include in this group.
122
+ * Use this for precise control over which quotas are aggregated.
123
+ */
124
+ sources?: string[];
125
+ /**
126
+ * Regex/Glob patterns to match against raw quota IDs or provider names.
127
+ * The provider will return raw quotas with IDs like "ag-raw-gemini-1-5-flash".
128
+ * Patterns are matched case-insensitively.
129
+ */
130
+ patterns?: string[];
131
+ /**
132
+ * Optional: Limit pattern matching to a specific provider ID.
133
+ * If set, only quotas from this provider will be considered for pattern matching.
134
+ */
135
+ providerId?: string;
136
+ /**
137
+ * Aggregation strategy. Defaults to "most_critical".
138
+ */
139
+ strategy?: AggregationStrategy;
140
+ /**
141
+ * Time window for regression (default: 60 minutes).
142
+ */
143
+ predictionWindowMinutes?: number;
144
+ /**
145
+ * Short time window for spikes (default: 5 minutes).
146
+ */
147
+ predictionShortWindowMinutes?: number;
148
+ }
149
+
150
+ export interface HistoryPoint {
151
+ timestamp: number;
152
+ used: number;
153
+ limit: number | null;
154
+ }
155
+
156
+ export interface IHistoryService {
157
+ init(): Promise<void>;
158
+ append(snapshot: QuotaData[]): Promise<void>;
159
+ getHistory(quotaId: string, windowMs: number): HistoryPoint[];
160
+ setMaxAge(hours: number): void;
161
+ pruneAll(): Promise<void>;
162
+ }
163
+
164
+ export interface IQuotaProvider {
165
+ id: string;
166
+ fetchQuota(): Promise<QuotaData[]>;
167
+ }
168
+
169
+ export interface IQuotaRegistry {
170
+ register(provider: IQuotaProvider): void;
171
+ getAll(): IQuotaProvider[];
172
+ }
173
+
174
+ /**
175
+ * Interface for prediction engines that calculate time-to-limit.
176
+ */
177
+ export interface IPredictionEngine {
178
+ /**
179
+ * Predicts time to limit in milliseconds using historical usage data.
180
+ * @param quotaId - The quota identifier to predict for
181
+ * @param windowMinutes - The long time window for regression (default: 60)
182
+ * @param shortWindowMinutes - The short time window for capturing spikes
183
+ * @returns Time to limit in milliseconds, or Infinity if usage is stable/decreasing
184
+ */
185
+ predictTimeToLimit(
186
+ quotaId: string,
187
+ windowMinutes?: number,
188
+ shortWindowMinutes?: number,
189
+ ): number;
190
+ }
191
+
192
+ /**
193
+ * Interface for aggregation services that combine multiple quotas.
194
+ */
195
+ export interface IAggregationService {
196
+ /**
197
+ * Aggregates quotas using the most critical (shortest time-to-limit) strategy.
198
+ */
199
+ aggregateMostCritical(
200
+ quotas: QuotaData[],
201
+ windowMinutes?: number,
202
+ shortWindowMinutes?: number,
203
+ ): QuotaData | null;
204
+
205
+ /**
206
+ * Aggregates quotas by selecting the one with highest usage ratio.
207
+ */
208
+ aggregateMax(quotas: QuotaData[]): QuotaData;
209
+
210
+ /**
211
+ * Aggregates quotas by selecting the one with lowest usage ratio.
212
+ */
213
+ aggregateMin(quotas: QuotaData[]): QuotaData;
214
+
215
+ /**
216
+ * Aggregates quotas by averaging their usage ratios.
217
+ */
218
+ aggregateAverage(
219
+ quotas: QuotaData[],
220
+ name: string,
221
+ id: string,
222
+ strategy: "mean" | "median",
223
+ ): QuotaData;
224
+ }
225
+
226
+ export type QuotaDisplayMode = "simple" | "detailed" | "hidden";
227
+
228
+ export type AnsiColor =
229
+ | "red"
230
+ | "green"
231
+ | "yellow"
232
+ | "blue"
233
+ | "magenta"
234
+ | "cyan"
235
+ | "white"
236
+ | "gray"
237
+ | "bold"
238
+ | "dim"
239
+ | "reset";
240
+
241
+ export interface GradientLevel {
242
+ threshold: number; // 0 to 1 (e.g., 0.8 for 80%)
243
+ color: AnsiColor;
244
+ }
245
+
246
+ export interface ProgressBarConfig {
247
+ width?: number;
248
+ filledChar?: string;
249
+ emptyChar?: string;
250
+ show?: "used" | "available";
251
+ /**
252
+ * Enable ANSI colors. Defaults to false.
253
+ */
254
+ color?: boolean;
255
+ // Define color levels. The bar will use the color of the first level
256
+ // whose threshold is greater than or equal to the current usage ratio.
257
+ gradients?: GradientLevel[];
258
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,55 @@
1
+ import { appendFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { DEBUG_LOG_FILE } from "./utils/paths";
5
+ import { inspect } from "node:util";
6
+
7
+ export class Logger {
8
+ private static instance: Logger;
9
+ private debugEnabled: boolean = false;
10
+ private logPath: string;
11
+
12
+ private constructor() {
13
+ this.logPath = DEBUG_LOG_FILE();
14
+ if (process.env.OPENCODE_QUOTAS_DEBUG === "1") {
15
+ this.debugEnabled = true;
16
+ }
17
+ }
18
+
19
+ public static getInstance(): Logger {
20
+ if (!Logger.instance) {
21
+ Logger.instance = new Logger();
22
+ }
23
+ return Logger.instance;
24
+ }
25
+
26
+ public setDebug(enabled: boolean): void {
27
+ this.debugEnabled = enabled;
28
+ }
29
+
30
+ public debug(msg: string, data?: any): void {
31
+ this.log(msg, data, true);
32
+ }
33
+
34
+ public info(msg: string, data?: any): void {
35
+ this.log(msg, data, false);
36
+ }
37
+
38
+ public error(msg: string, data?: any): void {
39
+ this.log(msg, data, false);
40
+ }
41
+
42
+ private log(msg: string, data: any, requiresDebug: boolean): void {
43
+ if (requiresDebug && !this.debugEnabled) return;
44
+
45
+ const timestamp = new Date().toISOString();
46
+ const payload = data
47
+ ? ` ${inspect(data, { depth: null, colors: false, breakLength: Infinity })}`
48
+ : "";
49
+ const logLine = `[${timestamp}] ${msg}${payload}`;
50
+
51
+ appendFile(this.logPath, logLine + "\n").catch(() => {});
52
+ }
53
+ }
54
+
55
+ export const logger = Logger.getInstance();
@@ -0,0 +1,65 @@
1
+ const PLUGIN_STATE_KEY = "__OPENCODE_QUOTA_PLUGIN_STATE__";
2
+
3
+ type PluginStateGlobal = {
4
+ [PLUGIN_STATE_KEY]?: PluginState;
5
+ };
6
+
7
+ /**
8
+ * Singleton state manager for the quota plugin.
9
+ * Uses globalThis to ensure only one instance exists across all plugin instantiations.
10
+ */
11
+ export class PluginState {
12
+ private static readonly MAX_TRACKED_MESSAGES = 1000;
13
+ private processedMessages: string[] = [];
14
+ private processedSet = new Set<string>();
15
+ private locks = new Map<string, Promise<void>>();
16
+
17
+ isProcessed(messageId: string): boolean {
18
+ return this.processedSet.has(messageId);
19
+ }
20
+
21
+ markProcessed(messageId: string): void {
22
+ if (this.processedSet.has(messageId)) return;
23
+
24
+ this.processedSet.add(messageId);
25
+ this.processedMessages.push(messageId);
26
+
27
+ while (this.processedMessages.length > PluginState.MAX_TRACKED_MESSAGES) {
28
+ const oldest = this.processedMessages.shift();
29
+ if (oldest) this.processedSet.delete(oldest);
30
+ }
31
+ }
32
+
33
+ async acquireLock(messageId: string): Promise<() => void> {
34
+ const existingLock = this.locks.get(messageId) || Promise.resolve();
35
+
36
+ let resolveLock: () => void;
37
+ const nextLock = new Promise<void>((resolve) => {
38
+ resolveLock = resolve;
39
+ });
40
+
41
+ this.locks.set(messageId, nextLock);
42
+
43
+ await existingLock;
44
+
45
+ return () => {
46
+ resolveLock();
47
+ if (this.locks.get(messageId) === nextLock) {
48
+ this.locks.delete(messageId);
49
+ }
50
+ };
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Returns the global singleton PluginState instance.
56
+ * This ensures all plugin instantiations share the same state,
57
+ * preventing duplicate injection when the plugin is loaded multiple times.
58
+ */
59
+ export function getPluginState(): PluginState {
60
+ const globalRef = globalThis as PluginStateGlobal;
61
+ if (!globalRef[PLUGIN_STATE_KEY]) {
62
+ globalRef[PLUGIN_STATE_KEY] = new PluginState();
63
+ }
64
+ return globalRef[PLUGIN_STATE_KEY];
65
+ }
@@ -0,0 +1,163 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { ANTIGRAVITY_ACCOUNTS_FILE } from "../../utils/paths";
3
+
4
+ /**
5
+ * PUBLIC OAUTH CREDENTIALS - INTENTIONALLY COMMITTED
6
+ *
7
+ * These are "Installed Application" credentials for Google's Native App OAuth flow.
8
+ * Per Google's documentation, the client_secret for native applications is NOT
9
+ * considered confidential. Security relies solely on the user's refresh_token
10
+ * stored locally in ~/.config/opencode/antigravity-accounts.json.
11
+ *
12
+ * See: https://developers.google.com/identity/protocols/oauth2/native-app
13
+ */
14
+ const ANTIGRAVITY_CLIENT_ID =
15
+ "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"; // gitleaks:allow
16
+ const ANTIGRAVITY_CLIENT_SECRET = "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"; // gitleaks:allow
17
+ const TOKEN_URL = "https://oauth2.googleapis.com/token";
18
+
19
+ interface StoredAccount {
20
+ email: string;
21
+ refreshToken: string;
22
+ projectId?: string;
23
+ managedProjectId?: string;
24
+ addedAt: number;
25
+ lastUsed: number;
26
+ }
27
+
28
+ interface AccountsFile {
29
+ version: number;
30
+ accounts: StoredAccount[];
31
+ activeIndex: number;
32
+ }
33
+
34
+ interface TokenResponse {
35
+ access_token: string;
36
+ expires_in: number;
37
+ token_type: string;
38
+ }
39
+
40
+ export interface CloudAuthCredentials {
41
+ accessToken: string;
42
+ projectId?: string;
43
+ email: string;
44
+ }
45
+
46
+ interface CachedCredential extends CloudAuthCredentials {
47
+ expiresAt: number;
48
+ }
49
+
50
+ let cachedCredential: CachedCredential | null = null;
51
+
52
+ function getAccountsFilePath(): string {
53
+ return ANTIGRAVITY_ACCOUNTS_FILE();
54
+ }
55
+
56
+ async function loadAccounts(): Promise<AccountsFile> {
57
+ const accountsPath = getAccountsFilePath();
58
+
59
+ try {
60
+ const content = await readFile(accountsPath, "utf-8");
61
+ const data = JSON.parse(content) as AccountsFile;
62
+
63
+ if (!data.accounts || data.accounts.length === 0) {
64
+ throw new Error("No accounts found in antigravity-accounts.json");
65
+ }
66
+
67
+ return data;
68
+ } catch (error) {
69
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
70
+ throw new Error(
71
+ "Antigravity accounts file not found.\n" +
72
+ "Run 'opencode auth login' first to authenticate with Google.",
73
+ );
74
+ }
75
+ throw error;
76
+ }
77
+ }
78
+
79
+ export async function hasCloudCredentials(): Promise<boolean> {
80
+ try {
81
+ await loadAccounts();
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ async function refreshAccessToken(refreshToken: string): Promise<{ accessToken: string; expiresAt: number }> {
89
+ const response = await fetch(TOKEN_URL, {
90
+ method: "POST",
91
+ headers: {
92
+ "Content-Type": "application/x-www-form-urlencoded",
93
+ },
94
+ body: new URLSearchParams({
95
+ client_id: ANTIGRAVITY_CLIENT_ID,
96
+ client_secret: ANTIGRAVITY_CLIENT_SECRET,
97
+ refresh_token: refreshToken,
98
+ grant_type: "refresh_token",
99
+ }).toString(),
100
+ });
101
+
102
+ if (!response.ok) {
103
+ const errorText = await response.text();
104
+ if (errorText.toLowerCase().includes("invalid_grant")) {
105
+ throw new Error(
106
+ "Refresh token is invalid or expired. Run 'opencode auth login' to re-authenticate.",
107
+ );
108
+ }
109
+ throw new Error(`Token refresh failed: ${response.status} - ${errorText}`);
110
+ }
111
+
112
+ const data = (await response.json()) as TokenResponse;
113
+ return {
114
+ accessToken: data.access_token,
115
+ expiresAt: Date.now() + data.expires_in * 1000,
116
+ };
117
+ }
118
+
119
+ export async function getCloudCredentials(): Promise<CloudAuthCredentials> {
120
+ const accountsFile = await loadAccounts();
121
+ const activeAccount =
122
+ accountsFile.accounts[accountsFile.activeIndex] ?? accountsFile.accounts[0];
123
+
124
+ if (!activeAccount) {
125
+ throw new Error("No active account found in antigravity-accounts.json");
126
+ }
127
+
128
+ // Check cache (5 min buffer)
129
+ const fiveMinutesInMs = 5 * 60 * 1000;
130
+ if (
131
+ cachedCredential &&
132
+ cachedCredential.email === activeAccount.email &&
133
+ cachedCredential.expiresAt > Date.now() + fiveMinutesInMs
134
+ ) {
135
+ return {
136
+ accessToken: cachedCredential.accessToken,
137
+ projectId: cachedCredential.projectId,
138
+ email: cachedCredential.email,
139
+ };
140
+ }
141
+
142
+ const { accessToken, expiresAt } = await refreshAccessToken(activeAccount.refreshToken);
143
+
144
+ cachedCredential = {
145
+ accessToken,
146
+ projectId: activeAccount.projectId,
147
+ email: activeAccount.email,
148
+ expiresAt,
149
+ };
150
+
151
+ return {
152
+ accessToken,
153
+ projectId: activeAccount.projectId,
154
+ email: activeAccount.email,
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Reset the credential cache. Internal use only (primarily for tests).
160
+ */
161
+ export function resetCredentialCache(): void {
162
+ cachedCredential = null;
163
+ }
@@ -0,0 +1 @@
1
+ export * from "./provider";