antigravity-claude-proxy 2.2.0 → 2.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antigravity-claude-proxy",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "Proxy server to use Antigravity's Claude models with Claude Code CLI",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -11,11 +11,47 @@ import {
11
11
  LOAD_CODE_ASSIST_HEADERS,
12
12
  DEFAULT_PROJECT_ID
13
13
  } from '../constants.js';
14
- import { refreshAccessToken } from '../auth/oauth.js';
14
+ import { refreshAccessToken, parseRefreshParts, formatRefreshParts } from '../auth/oauth.js';
15
15
  import { getAuthStatus } from '../auth/database.js';
16
16
  import { logger } from '../utils/logger.js';
17
17
  import { isNetworkError } from '../utils/helpers.js';
18
18
  import { onboardUser, getDefaultTierId } from './onboarding.js';
19
+ import { parseTierId } from '../cloudcode/model-api.js';
20
+
21
+ // Track accounts currently fetching subscription to avoid duplicate calls
22
+ const subscriptionFetchInProgress = new Set();
23
+
24
+ /**
25
+ * Fetch subscription tier and save it (blocking)
26
+ * Used when we have a cached project but missing subscription data
27
+ *
28
+ * @param {string} token - OAuth access token
29
+ * @param {Object} account - Account object
30
+ * @param {Function} [onSave] - Callback to save account changes
31
+ */
32
+ async function fetchAndSaveSubscription(token, account, onSave) {
33
+ // Avoid duplicate fetches for the same account
34
+ if (subscriptionFetchInProgress.has(account.email)) {
35
+ return;
36
+ }
37
+ subscriptionFetchInProgress.add(account.email);
38
+
39
+ try {
40
+ // Call discoverProject just to get subscription info
41
+ const { subscription } = await discoverProject(token, account.projectId);
42
+ if (subscription && subscription.tier !== 'unknown') {
43
+ account.subscription = subscription;
44
+ if (onSave) {
45
+ await onSave();
46
+ }
47
+ logger.info(`[AccountManager] Updated subscription tier for ${account.email}: ${subscription.tier}`);
48
+ }
49
+ } catch (e) {
50
+ logger.debug(`[AccountManager] Subscription fetch failed for ${account.email}: ${e.message}`);
51
+ } finally {
52
+ subscriptionFetchInProgress.delete(account.email);
53
+ }
54
+ }
19
55
 
20
56
  /**
21
57
  * Get OAuth token for an account
@@ -82,27 +118,92 @@ export async function getTokenForAccount(account, tokenCache, onInvalid, onSave)
82
118
 
83
119
  /**
84
120
  * Get project ID for an account
121
+ * Aligned with opencode-antigravity-auth: parses refresh token for stored project IDs
85
122
  *
86
123
  * @param {Object} account - Account object
87
124
  * @param {string} token - OAuth access token
88
125
  * @param {Map} projectCache - Project cache map
126
+ * @param {Function} [onSave] - Callback to save account changes
89
127
  * @returns {Promise<string>} Project ID
90
128
  */
91
- export async function getProjectForAccount(account, token, projectCache) {
129
+ export async function getProjectForAccount(account, token, projectCache, onSave = null) {
92
130
  // Check cache first
93
131
  const cached = projectCache.get(account.email);
94
132
  if (cached) {
95
133
  return cached;
96
134
  }
97
135
 
98
- // OAuth or manual accounts may have projectId specified
136
+ // Parse refresh token to get stored project IDs (aligned with opencode-antigravity-auth)
137
+ const parts = account.refreshToken ? parseRefreshParts(account.refreshToken) : { refreshToken: null, projectId: undefined, managedProjectId: undefined };
138
+
139
+ // If we have a managedProjectId in the refresh token, use it
140
+ if (parts.managedProjectId) {
141
+ projectCache.set(account.email, parts.managedProjectId);
142
+ // If subscription is missing/unknown, fetch it now (blocking)
143
+ if (!account.subscription || account.subscription.tier === 'unknown') {
144
+ await fetchAndSaveSubscription(token, account, onSave);
145
+ }
146
+ return parts.managedProjectId;
147
+ }
148
+
149
+ // Legacy: check account.projectId for backward compatibility
99
150
  if (account.projectId) {
100
151
  projectCache.set(account.email, account.projectId);
152
+ // If subscription is missing/unknown, fetch it now (blocking)
153
+ if (!account.subscription || account.subscription.tier === 'unknown') {
154
+ await fetchAndSaveSubscription(token, account, onSave);
155
+ }
101
156
  return account.projectId;
102
157
  }
103
158
 
104
- // Discover project via loadCodeAssist API
105
- const project = await discoverProject(token);
159
+ // Discover managed project, passing projectId for metadata.duetProject
160
+ // Reference: opencode-antigravity-auth - discoverProject handles fallback internally
161
+ const { project, subscription } = await discoverProject(token, parts.projectId);
162
+
163
+ // Store managedProjectId back in refresh token (if we got a real project)
164
+ if (project && project !== DEFAULT_PROJECT_ID) {
165
+ let needsSave = false;
166
+
167
+ if (account.refreshToken) {
168
+ // OAuth accounts: encode in refresh token
169
+ account.refreshToken = formatRefreshParts({
170
+ refreshToken: parts.refreshToken,
171
+ projectId: parts.projectId,
172
+ managedProjectId: project,
173
+ });
174
+ needsSave = true;
175
+ } else if (account.source === 'database' || account.source === 'manual') {
176
+ // Database/manual accounts: store in projectId field
177
+ account.projectId = project;
178
+ needsSave = true;
179
+ }
180
+
181
+ // Save subscription tier if discovered
182
+ if (subscription) {
183
+ account.subscription = subscription;
184
+ needsSave = true;
185
+ }
186
+
187
+ // Trigger save to persist the updated project and subscription
188
+ if (needsSave && onSave) {
189
+ try {
190
+ await onSave();
191
+ } catch (e) {
192
+ logger.warn(`[AccountManager] Failed to save updated project: ${e.message}`);
193
+ }
194
+ }
195
+ } else if (subscription) {
196
+ // Even if no project discovered, save subscription if we got it
197
+ account.subscription = subscription;
198
+ if (onSave) {
199
+ try {
200
+ await onSave();
201
+ } catch (e) {
202
+ logger.warn(`[AccountManager] Failed to save subscription: ${e.message}`);
203
+ }
204
+ }
205
+ }
206
+
106
207
  projectCache.set(account.email, project);
107
208
  return project;
108
209
  }
@@ -111,13 +212,23 @@ export async function getProjectForAccount(account, token, projectCache) {
111
212
  * Discover project ID via Cloud Code API
112
213
  *
113
214
  * @param {string} token - OAuth access token
114
- * @returns {Promise<string>} Project ID
215
+ * @param {string} [projectId] - Optional project ID from refresh token (for metadata.duetProject)
216
+ * @returns {Promise<{project: string, subscription: {tier: string, projectId: string|null, detectedAt: string}|null}>} Project and subscription info
115
217
  */
116
- export async function discoverProject(token) {
218
+ export async function discoverProject(token, projectId = undefined) {
117
219
  let lastError = null;
118
220
  let gotSuccessfulResponse = false;
119
221
  let loadCodeAssistData = null;
120
222
 
223
+ const metadata = {
224
+ ideType: 'IDE_UNSPECIFIED',
225
+ platform: 'PLATFORM_UNSPECIFIED',
226
+ pluginType: 'GEMINI'
227
+ };
228
+ if (projectId) {
229
+ metadata.duetProject = projectId;
230
+ }
231
+
121
232
  for (const endpoint of LOAD_CODE_ASSIST_ENDPOINTS) {
122
233
  try {
123
234
  const response = await fetch(`${endpoint}/v1internal:loadCodeAssist`, {
@@ -127,14 +238,7 @@ export async function discoverProject(token) {
127
238
  'Content-Type': 'application/json',
128
239
  ...LOAD_CODE_ASSIST_HEADERS
129
240
  },
130
- body: JSON.stringify({
131
- metadata: {
132
- ideType: 'IDE_UNSPECIFIED',
133
- platform: 'PLATFORM_UNSPECIFIED',
134
- pluginType: 'GEMINI',
135
- duetProject: DEFAULT_PROJECT_ID
136
- }
137
- })
241
+ body: JSON.stringify({ metadata })
138
242
  });
139
243
 
140
244
  if (!response.ok) {
@@ -150,13 +254,16 @@ export async function discoverProject(token) {
150
254
 
151
255
  logger.debug(`[AccountManager] loadCodeAssist response from ${endpoint}:`, JSON.stringify(data));
152
256
 
257
+ // Extract subscription tier from response
258
+ const subscription = extractSubscriptionFromResponse(data);
259
+
153
260
  if (typeof data.cloudaicompanionProject === 'string') {
154
261
  logger.success(`[AccountManager] Discovered project: ${data.cloudaicompanionProject}`);
155
- return data.cloudaicompanionProject;
262
+ return { project: data.cloudaicompanionProject, subscription };
156
263
  }
157
264
  if (data.cloudaicompanionProject?.id) {
158
265
  logger.success(`[AccountManager] Discovered project: ${data.cloudaicompanionProject.id}`);
159
- return data.cloudaicompanionProject.id;
266
+ return { project: data.cloudaicompanionProject.id, subscription };
160
267
  }
161
268
 
162
269
  // No project found - log tier data and try to onboard the user
@@ -171,47 +278,72 @@ export async function discoverProject(token) {
171
278
 
172
279
  // If we got a successful response but no project, try onboarding
173
280
  if (gotSuccessfulResponse && loadCodeAssistData) {
174
- // Priority: paidTier > currentTier > allowedTiers (consistent with model-api.js)
175
- let tierId = null;
176
- let tierSource = null;
177
-
178
- if (loadCodeAssistData.paidTier?.id) {
179
- tierId = loadCodeAssistData.paidTier.id;
180
- tierSource = 'paidTier';
181
- } else if (loadCodeAssistData.currentTier?.id) {
182
- tierId = loadCodeAssistData.currentTier.id;
183
- tierSource = 'currentTier';
184
- } else {
185
- tierId = getDefaultTierId(loadCodeAssistData.allowedTiers);
186
- tierSource = 'allowedTiers';
187
- }
281
+ // Only use allowedTiers for onboarding (matching opencode-antigravity-auth and oauth.js)
282
+ // Note: paidTier (g1-pro-tier, g1-ultra-tier) is NOT valid for onboardUser API
283
+ // The paidTier is used for subscription detection only, not for onboarding
284
+ const tierId = getDefaultTierId(loadCodeAssistData.allowedTiers) || 'free-tier';
285
+ logger.info(`[AccountManager] Onboarding user with tier: ${tierId}`);
188
286
 
189
- tierId = tierId || 'free-tier';
190
- logger.info(`[AccountManager] Onboarding user with tier: ${tierId} (source: ${tierSource})`);
191
-
192
- // Check if this is a free tier (raw API values contain 'free')
193
- const isFree = tierId.toLowerCase().includes('free');
194
-
195
- // For non-free tiers, pass DEFAULT_PROJECT_ID as the GCP project
196
- // The API requires a project for paid tier onboarding
287
+ // Pass projectId for metadata.duetProject (without fallback, matching reference)
288
+ // Reference: opencode-antigravity-auth passes parts.projectId (not fallback) to onboardManagedProject
197
289
  const onboardedProject = await onboardUser(
198
290
  token,
199
291
  tierId,
200
- isFree ? null : DEFAULT_PROJECT_ID
292
+ projectId // Original projectId without fallback
201
293
  );
202
294
  if (onboardedProject) {
203
295
  logger.success(`[AccountManager] Successfully onboarded, project: ${onboardedProject}`);
204
- return onboardedProject;
296
+ const subscription = extractSubscriptionFromResponse(loadCodeAssistData);
297
+ return { project: onboardedProject, subscription };
205
298
  }
206
299
 
207
- logger.warn(`[AccountManager] Onboarding failed, using default project: ${DEFAULT_PROJECT_ID}`);
300
+ logger.warn(`[AccountManager] Onboarding failed - account may not work correctly`);
208
301
  }
209
302
 
210
303
  // Only warn if all endpoints failed with errors (not just missing project)
211
304
  if (!gotSuccessfulResponse) {
212
305
  logger.warn(`[AccountManager] loadCodeAssist failed for all endpoints: ${lastError}`);
213
306
  }
214
- return DEFAULT_PROJECT_ID;
307
+
308
+ // Fallback: use projectId if available, otherwise use default
309
+ // Reference: opencode-antigravity-auth/src/plugin/project.ts
310
+ if (projectId) {
311
+ return { project: projectId, subscription: null };
312
+ }
313
+ return { project: DEFAULT_PROJECT_ID, subscription: null };
314
+ }
315
+
316
+ /**
317
+ * Extract subscription tier from loadCodeAssist response
318
+ *
319
+ * @param {Object} data - loadCodeAssist response data
320
+ * @returns {{tier: string, projectId: string|null, detectedAt: string}|null} Subscription info
321
+ */
322
+ function extractSubscriptionFromResponse(data) {
323
+ if (!data) return null;
324
+
325
+ // Priority: paidTier > currentTier (consistent with model-api.js)
326
+ let tier = 'free';
327
+ let cloudProject = null;
328
+
329
+ if (data.paidTier?.id) {
330
+ tier = parseTierId(data.paidTier.id);
331
+ } else if (data.currentTier?.id) {
332
+ tier = parseTierId(data.currentTier.id);
333
+ }
334
+
335
+ // Get project ID
336
+ if (typeof data.cloudaicompanionProject === 'string') {
337
+ cloudProject = data.cloudaicompanionProject;
338
+ } else if (data.cloudaicompanionProject?.id) {
339
+ cloudProject = data.cloudaicompanionProject.id;
340
+ }
341
+
342
+ return {
343
+ tier,
344
+ projectId: cloudProject,
345
+ detectedAt: new Date().toISOString()
346
+ };
215
347
  }
216
348
 
217
349
  /**
@@ -297,7 +297,8 @@ export class AccountManager {
297
297
  * @returns {Promise<string>} Project ID
298
298
  */
299
299
  async getProjectForAccount(account, token) {
300
- return fetchProject(account, token, this.#projectCache);
300
+ // Pass onSave callback to persist managedProjectId in refresh token
301
+ return fetchProject(account, token, this.#projectCache, () => this.saveToDisk());
301
302
  }
302
303
 
303
304
  /**
@@ -43,7 +43,7 @@ export function getDefaultTierId(allowedTiers) {
43
43
  * @param {number} [delayMs=5000] - Delay between polling attempts
44
44
  * @returns {Promise<string|null>} Managed project ID or null if failed
45
45
  */
46
- export async function onboardUser(token, tierId, projectId = null, maxAttempts = 10, delayMs = 5000) {
46
+ export async function onboardUser(token, tierId, projectId = undefined, maxAttempts = 10, delayMs = 5000) {
47
47
  const metadata = {
48
48
  ideType: 'IDE_UNSPECIFIED',
49
49
  platform: 'PLATFORM_UNSPECIFIED',
@@ -58,16 +58,11 @@ export async function onboardUser(token, tierId, projectId = null, maxAttempts =
58
58
  tierId,
59
59
  metadata
60
60
  };
61
+ // Note: Do NOT add cloudaicompanionProject to requestBody
62
+ // Reference implementation only sets metadata.duetProject, not the body field
63
+ // Adding cloudaicompanionProject causes 400 errors for auto-provisioned tiers (g1-pro, g1-ultra)
61
64
 
62
- // Check if this is a free tier (handles raw API values like 'free-tier')
63
- const isFree = tierId.toLowerCase().includes('free');
64
-
65
- // Non-free tiers require a cloudaicompanionProject
66
- if (!isFree && projectId) {
67
- requestBody.cloudaicompanionProject = projectId;
68
- }
69
-
70
- logger.debug(`[Onboarding] Starting onboard with tierId: ${tierId}, projectId: ${projectId}, isFree: ${isFree}`);
65
+ logger.debug(`[Onboarding] Starting onboard with tierId: ${tierId}, projectId: ${projectId}`);
71
66
 
72
67
  for (const endpoint of ONBOARD_USER_ENDPOINTS) {
73
68
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
@@ -10,20 +10,14 @@ import { HybridStrategy } from './hybrid-strategy.js';
10
10
  import { logger } from '../../utils/logger.js';
11
11
  import {
12
12
  SELECTION_STRATEGIES,
13
- DEFAULT_SELECTION_STRATEGY
13
+ DEFAULT_SELECTION_STRATEGY,
14
+ STRATEGY_LABELS
14
15
  } from '../../constants.js';
15
16
 
16
17
  // Re-export strategy constants for convenience
17
18
  export const STRATEGY_NAMES = SELECTION_STRATEGIES;
18
19
  export const DEFAULT_STRATEGY = DEFAULT_SELECTION_STRATEGY;
19
20
 
20
- // Strategy display labels
21
- export const STRATEGY_LABELS = {
22
- 'sticky': 'Sticky (Cache Optimized)',
23
- 'round-robin': 'Round Robin (Load Balanced)',
24
- 'hybrid': 'Hybrid (Smart Distribution)'
25
- };
26
-
27
21
  /**
28
22
  * Create a strategy instance
29
23
  * @param {string} strategyName - Name of the strategy ('sticky', 'round-robin', 'hybrid')
package/src/auth/oauth.js CHANGED
@@ -17,6 +17,34 @@ import {
17
17
  import { logger } from '../utils/logger.js';
18
18
  import { onboardUser, getDefaultTierId } from '../account-manager/onboarding.js';
19
19
 
20
+ /**
21
+ * Parse refresh token parts (aligned with opencode-antigravity-auth)
22
+ * Format: refreshToken|projectId|managedProjectId
23
+ *
24
+ * @param {string} refresh - Composite refresh token string
25
+ * @returns {{refreshToken: string, projectId: string|undefined, managedProjectId: string|undefined}}
26
+ */
27
+ export function parseRefreshParts(refresh) {
28
+ const [refreshToken = '', projectId = '', managedProjectId = ''] = (refresh ?? '').split('|');
29
+ return {
30
+ refreshToken,
31
+ projectId: projectId || undefined,
32
+ managedProjectId: managedProjectId || undefined,
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Format refresh token parts back into composite string
38
+ *
39
+ * @param {{refreshToken: string, projectId?: string|undefined, managedProjectId?: string|undefined}} parts
40
+ * @returns {string} Composite refresh token
41
+ */
42
+ export function formatRefreshParts(parts) {
43
+ const projectSegment = parts.projectId ?? '';
44
+ const base = `${parts.refreshToken}|${projectSegment}`;
45
+ return parts.managedProjectId ? `${base}|${parts.managedProjectId}` : base;
46
+ }
47
+
20
48
  /**
21
49
  * Generate PKCE code verifier and challenge
22
50
  */
@@ -267,11 +295,15 @@ export async function exchangeCode(code, verifier) {
267
295
 
268
296
  /**
269
297
  * Refresh access token using refresh token
298
+ * Handles composite refresh tokens (refreshToken|projectId|managedProjectId)
270
299
  *
271
- * @param {string} refreshToken - OAuth refresh token
300
+ * @param {string} compositeRefresh - OAuth refresh token (may be composite)
272
301
  * @returns {Promise<{accessToken: string, expiresIn: number}>} New access token
273
302
  */
274
- export async function refreshAccessToken(refreshToken) {
303
+ export async function refreshAccessToken(compositeRefresh) {
304
+ // Parse the composite refresh token to extract the actual OAuth token
305
+ const parts = parseRefreshParts(compositeRefresh);
306
+
275
307
  const response = await fetch(OAUTH_CONFIG.tokenUrl, {
276
308
  method: 'POST',
277
309
  headers: {
@@ -280,7 +312,7 @@ export async function refreshAccessToken(refreshToken) {
280
312
  body: new URLSearchParams({
281
313
  client_id: OAUTH_CONFIG.clientId,
282
314
  client_secret: OAUTH_CONFIG.clientSecret,
283
- refresh_token: refreshToken,
315
+ refresh_token: parts.refreshToken, // Use the actual OAuth token
284
316
  grant_type: 'refresh_token'
285
317
  })
286
318
  });
@@ -408,6 +440,8 @@ export async function completeOAuthFlow(code, verifier) {
408
440
  }
409
441
 
410
442
  export default {
443
+ parseRefreshParts,
444
+ formatRefreshParts,
411
445
  getAuthorizationUrl,
412
446
  extractCodeFromInput,
413
447
  startCallbackServer,
@@ -95,7 +95,7 @@ function openBrowser(url) {
95
95
  args = [url];
96
96
  } else if (platform === 'win32') {
97
97
  command = 'cmd';
98
- args = ['/c', 'start', '', url];
98
+ args = ['/c', 'start', '', url.replace(/&/g, '^&')];
99
99
  } else {
100
100
  command = 'xdg-open';
101
101
  args = [url];
@@ -210,20 +210,18 @@ async function addAccount(existingAccounts) {
210
210
  if (existing) {
211
211
  console.log(`\n⚠ Account ${result.email} already exists. Updating tokens.`);
212
212
  existing.refreshToken = result.refreshToken;
213
- existing.projectId = result.projectId;
213
+ // Note: projectId will be discovered and stored in refresh token on first use
214
214
  existing.addedAt = new Date().toISOString();
215
215
  return null; // Don't add duplicate
216
216
  }
217
217
 
218
218
  console.log(`\n✓ Successfully authenticated: ${result.email}`);
219
- if (result.projectId) {
220
- console.log(` Project ID: ${result.projectId}`);
221
- }
219
+ console.log(' Project will be discovered on first API request.');
222
220
 
223
221
  return {
224
222
  email: result.email,
225
223
  refreshToken: result.refreshToken,
226
- projectId: result.projectId,
224
+ // Note: projectId stored in refresh token, not as separate field
227
225
  addedAt: new Date().toISOString(),
228
226
  modelRateLimits: {}
229
227
  };
@@ -267,20 +265,18 @@ async function addAccountNoBrowser(existingAccounts, rl) {
267
265
  if (existing) {
268
266
  console.log(`\n⚠ Account ${result.email} already exists. Updating tokens.`);
269
267
  existing.refreshToken = result.refreshToken;
270
- existing.projectId = result.projectId;
268
+ // Note: projectId will be discovered and stored in refresh token on first use
271
269
  existing.addedAt = new Date().toISOString();
272
270
  return null; // Don't add duplicate
273
271
  }
274
272
 
275
273
  console.log(`\n✓ Successfully authenticated: ${result.email}`);
276
- if (result.projectId) {
277
- console.log(` Project ID: ${result.projectId}`);
278
- }
274
+ console.log(' Project will be discovered on first API request.');
279
275
 
280
276
  return {
281
277
  email: result.email,
282
278
  refreshToken: result.refreshToken,
283
- projectId: result.projectId,
279
+ // Note: projectId stored in refresh token, not as separate field
284
280
  addedAt: new Date().toISOString(),
285
281
  modelRateLimits: {}
286
282
  };
@@ -128,7 +128,7 @@ export async function getModelQuotas(token, projectId = null) {
128
128
  * @param {string} tierId - The tier ID from the API
129
129
  * @returns {'free' | 'pro' | 'ultra' | 'unknown'} The subscription tier
130
130
  */
131
- function parseTierId(tierId) {
131
+ export function parseTierId(tierId) {
132
132
  if (!tierId) return 'unknown';
133
133
  const lower = tierId.toLowerCase();
134
134
 
package/src/constants.js CHANGED
@@ -121,6 +121,13 @@ export const MIN_SIGNATURE_LENGTH = 50; // Minimum valid thinking signature leng
121
121
  export const SELECTION_STRATEGIES = ['sticky', 'round-robin', 'hybrid'];
122
122
  export const DEFAULT_SELECTION_STRATEGY = 'hybrid';
123
123
 
124
+ // Strategy display labels
125
+ export const STRATEGY_LABELS = {
126
+ 'sticky': 'Sticky (Cache Optimized)',
127
+ 'round-robin': 'Round Robin (Load Balanced)',
128
+ 'hybrid': 'Hybrid (Smart Distribution)'
129
+ };
130
+
124
131
  // Gemini-specific limits
125
132
  export const GEMINI_MAX_OUTPUT_TOKENS = 16384;
126
133
 
@@ -263,6 +270,7 @@ export default {
263
270
  isThinkingModel,
264
271
  OAUTH_CONFIG,
265
272
  OAUTH_REDIRECT_URI,
273
+ STRATEGY_LABELS,
266
274
  MODEL_FALLBACK_MAP,
267
275
  TEST_MODELS,
268
276
  DEFAULT_PRESETS,
package/src/index.js CHANGED
@@ -60,8 +60,11 @@ const server = app.listen(PORT, () => {
60
60
  const align4 = (text) => text + ' '.repeat(Math.max(0, 58 - text.length));
61
61
 
62
62
  // Build Control section dynamically
63
+ const strategyOptions = `(${STRATEGY_NAMES.join('/')})`;
64
+ const strategyLine2 = ' ' + strategyOptions;
63
65
  let controlSection = '║ Control: ║\n';
64
- controlSection += '║ --strategy=<s> Set selection strategy (sticky/hybrid) ║\n';
66
+ controlSection += '║ --strategy=<s> Set account selection strategy ║\n';
67
+ controlSection += `${border} ${align(strategyLine2)}${border}\n`;
65
68
  if (!isDebug) {
66
69
  controlSection += '║ --debug Enable debug logging ║\n';
67
70
  }
@@ -668,10 +668,11 @@ export function mountWebUI(app, dirname, accountManager) {
668
668
  const accountData = await completeOAuthFlow(code, verifier);
669
669
 
670
670
  // Add or update the account
671
+ // Note: Don't set projectId here - it will be discovered and stored
672
+ // in the refresh token via getProjectForAccount() on first use
671
673
  await addAccount({
672
674
  email: accountData.email,
673
675
  refreshToken: accountData.refreshToken,
674
- projectId: accountData.projectId,
675
676
  source: 'oauth'
676
677
  });
677
678