@wenathlan/getry 2.0.165 → 2.0.166
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/accounts.ts +2087 -0
- package/antigravity.ts +2171 -0
- package/auth.ts +265 -0
- package/constants.ts +950 -0
- package/db.ts +810 -0
- package/docs/distributionlanes.md +1 -1
- package/docs/getry.md +33 -0
- package/engine.ts +1535 -0
- package/fingerprint.ts +1461 -0
- package/gateway.ts +1306 -0
- package/gatewayprobe.ts +61 -0
- package/gatewaysmoke.ts +64 -0
- package/intelligence.ts +169 -0
- package/json.ts +932 -0
- package/labels.ts +262 -0
- package/llm.ts +2709 -0
- package/models.ts +4017 -0
- package/nodeshim.ts +265 -0
- package/oauth.ts +2201 -0
- package/package.json +575 -215
- package/project.ts +1293 -0
- package/providers.ts +3238 -0
- package/quota.ts +1891 -0
- package/recovery.ts +2434 -0
- package/request.ts +4496 -0
- package/search.ts +1761 -0
- package/server.ts +766 -0
- package/streaming.ts +4398 -0
- package/thinking.ts +416 -0
- package/types.ts +809 -0
- package/utils.ts +263 -0
- package/validate.ts +61 -0
- package/versionregistry.ts +1325 -0
package/providers.ts
ADDED
|
@@ -0,0 +1,3238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* providers — consolidated providers engine logics of the getry family.
|
|
3
|
+
* @module providers
|
|
4
|
+
* @family wave/logicfold2
|
|
5
|
+
* One keyed file per family: the former variant arms (providercli)
|
|
6
|
+
* retired into this seat with every export preserved under the same name.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { getDefaultAccountManager } from "./accounts.js";
|
|
10
|
+
import { ANTIGRAVITY_VERSION_FALLBACK, ANTIGRAVITY_VERSION_MIN_SUPPORTED, CLIENT_METADATA_STRING, CLOUDCODE_BASE_URL, CLOUDCODE_DAILY_BASE, CODE_ASSIST_PATH_MAP, ENDPOINTS, FALLBACK_PROJECT_ID, FILE_PATHS, GEMINI_CLI_OAUTH_CLIENT_ID, GEMINI_CLI_OAUTH_CLIENT_SECRET, GEMINI_CLI_SCOPES, GEMINI_CLI_USER_AGENT, OAUTH_REVOKE_URL, OAUTH_TOKEN_URL, PLUGIN_VERSION, PROJECT_FALLBACK } from "./constants.js";
|
|
11
|
+
import { X_GOOG_API_CLIENT_GEMINI_CLI } from "./fingerprint.js";
|
|
12
|
+
import { ENDPOINT_ORDER, MODELS_2026_CATALOG, DEFAULT_MODEL_ID, MODEL_BY_ID, ModelCatalogEntry, SEARCH_MODEL_PREVIEW, buildGoogleProviderModelConfigs, buildOpenCodeModelDefinitions, detectTargetProvider } from "./models.js";
|
|
13
|
+
import { ANTIGRAVITY_CLI_CLIENT_ID, ANTIGRAVITY_CLI_CLIENT_SECRET, ANTIGRAVITY_CLI_GRPC_VERSION, ANTIGRAVITY_CLI_OAUTH_SCOPES, ANTIGRAVITY_CLI_REDIRECT_OFFICIAL, ANTIGRAVITY_CLI_VERSION_LATEST, GEMINI_OAUTH_CLIENT_ID, GEMINI_OAUTH_CLIENT_SECRET } from "./oauth.js";
|
|
14
|
+
import { ChatEvent, parseEventStream } from "./streaming.js";
|
|
15
|
+
import { DevThinkConfig, DevThinkPaths, VERSION_FALLBACK, atomicWrite, cfgDir, fetchVersionChain, loadConfigRaw, resolveCredential, saveConfigRaw, validateEmail, validateModelId, validateModelIdCharset, validateProjectId, validateSemver } from "@wenathlan/devthink/config";
|
|
16
|
+
import { FETCH_TIMEOUT_MS } from "@wenathlan/devthink/constants";
|
|
17
|
+
import { debugLogger, logsDir, redactSecrets } from "@wenathlan/devthink/core";
|
|
18
|
+
import { osStringRows } from "@wenathlan/devthink/os";
|
|
19
|
+
|
|
20
|
+
/* ===== block: providers ===== */
|
|
21
|
+
/**
|
|
22
|
+
* providers.ts — the chat provider transport of the workbench (root layer).
|
|
23
|
+
*
|
|
24
|
+
* One responsibility: turn a ChatRequest into a provider-specific streaming
|
|
25
|
+
* HTTP call and turn provider answers into a common ChatEvent stream. the
|
|
26
|
+
* PROVIDERS table carries the eleven known provider ids with their base
|
|
27
|
+
* urls, wire protocols (openai / anthropic / google) and the environment
|
|
28
|
+
* variable that feeds their credential — never a literal key. every wire
|
|
29
|
+
* family gets its own header and body builder; errors come back through
|
|
30
|
+
* cleanError which redacts anything credential-shaped before it surfaces.
|
|
31
|
+
* transport retries ride retryDelay backoff (3 attempts, 5xx only).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/* the ownership seam of the wave/ownership.getry split: the provider
|
|
35
|
+
* protocol surface and the retry/redact helpers are gateway-domain
|
|
36
|
+
* concerns this owner carries beside the provider transport (they moved
|
|
37
|
+
* with the wave — never duplicated inside the os gateway client), while
|
|
38
|
+
* the os config credential resolver stays the devthink seam the adapter
|
|
39
|
+
* imports through the published specifier. */
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
/** Identifies the public wire formats normalized by the gateway. */
|
|
44
|
+
export type ProviderProtocol = "openai" | "anthropic" | "google";
|
|
45
|
+
|
|
46
|
+
/** Returns bounded exponential retry timing for a transient request failure. */
|
|
47
|
+
export function retryDelay(attempt: number, baseMs = 250, maximumMs = 4_000): number {
|
|
48
|
+
return Math.min(maximumMs, baseMs * 2 ** Math.max(0, attempt));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Removes a bearer value from an error message before it reaches a terminal or browser surface. */
|
|
52
|
+
export function redactProviderError(message: string): string {
|
|
53
|
+
return message.replace(/bearer\s+[^\s]+/gi, "bearer [redacted]").slice(0, 500);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type ChatRole = "system" | "user" | "assistant";
|
|
57
|
+
|
|
58
|
+
export type ChatMessage = {
|
|
59
|
+
role: ChatRole;
|
|
60
|
+
content: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type ChatRequest = {
|
|
64
|
+
provider: string;
|
|
65
|
+
model: string;
|
|
66
|
+
messages: ChatMessage[];
|
|
67
|
+
temperature?: number | undefined;
|
|
68
|
+
maxTokens?: number | undefined;
|
|
69
|
+
signal?: AbortSignal | undefined;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type ModelInfo = {
|
|
73
|
+
id: string;
|
|
74
|
+
provider: string;
|
|
75
|
+
contextWindow?: number | undefined;
|
|
76
|
+
supportsStreaming: boolean;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type ProviderDefinition = {
|
|
80
|
+
id: string;
|
|
81
|
+
baseUrl?: string;
|
|
82
|
+
protocol: ProviderProtocol;
|
|
83
|
+
env: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** The known provider table: id → base url, wire protocol and credential env var. */
|
|
87
|
+
export const PROVIDERS: ProviderDefinition[] = [
|
|
88
|
+
{ id: "openai", baseUrl: "https://api.openai.com/v1", protocol: "openai", env: "OPENAI_API_KEY" },
|
|
89
|
+
{ id: "zai", baseUrl: "https://api.z.ai/api/paas/v4", protocol: "openai", env: "ZAI_API_KEY" },
|
|
90
|
+
{ id: "openrouter", baseUrl: "https://openrouter.ai/api/v1", protocol: "openai", env: "OPENROUTER_API_KEY" },
|
|
91
|
+
{ id: "anthropic", baseUrl: "https://api.anthropic.com", protocol: "anthropic", env: "ANTHROPIC_API_KEY" },
|
|
92
|
+
{
|
|
93
|
+
id: "google",
|
|
94
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
95
|
+
protocol: "google",
|
|
96
|
+
env: "GEMINI_API_KEY",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "qwen",
|
|
100
|
+
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
|
101
|
+
protocol: "openai",
|
|
102
|
+
env: "QWEN_API_KEY",
|
|
103
|
+
},
|
|
104
|
+
{ id: "deepseek", baseUrl: "https://api.deepseek.com", protocol: "openai", env: "DEEPSEEK_API_KEY" },
|
|
105
|
+
{ id: "groq", baseUrl: "https://api.groq.com/openai/v1", protocol: "openai", env: "GROQ_API_KEY" },
|
|
106
|
+
{ id: "mistral", baseUrl: "https://api.mistral.ai/v1", protocol: "openai", env: "MISTRAL_API_KEY" },
|
|
107
|
+
{ id: "xai", baseUrl: "https://api.x.ai/v1", protocol: "openai", env: "XAI_API_KEY" },
|
|
108
|
+
{ id: "ollama", protocol: "openai", env: "OLLAMA_API_KEY" },
|
|
109
|
+
{ id: "mimo", baseUrl: "https://api.xiaomimimo.com/v1", protocol: "openai", env: "MIMO_API_KEY" },
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
/** Finds one provider definition by id (case-insensitive).
|
|
113
|
+
* @throws when the provider id is unknown. */
|
|
114
|
+
function findProvider(provider: string): ProviderDefinition {
|
|
115
|
+
const definition = PROVIDERS.find((item) => item.id === provider.toLowerCase());
|
|
116
|
+
if (!definition) throw new Error(`Unknown provider: ${provider}. Use devthink providers.`);
|
|
117
|
+
return definition;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Joins a base url and a path with exactly one slash between them. */
|
|
121
|
+
function joinUrl(baseUrl: string, path: string): string {
|
|
122
|
+
return `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Converts one provider error body into a redacted, single-line Error.
|
|
126
|
+
* @throws never — always returns the Error to throw at the call site. */
|
|
127
|
+
function cleanError(body: string, status: number): Error {
|
|
128
|
+
let detail = body.trim();
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(body) as Record<string, unknown>;
|
|
131
|
+
const error = (parsed.error || parsed.message || parsed.detail) as unknown;
|
|
132
|
+
if (typeof error === "string") detail = error;
|
|
133
|
+
if (error && typeof error === "object") detail = JSON.stringify(error);
|
|
134
|
+
} catch {
|
|
135
|
+
detail = body.trim();
|
|
136
|
+
}
|
|
137
|
+
const safe = redactProviderError(detail);
|
|
138
|
+
return new Error(`${status} ${safe || "Provider request failed."}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Fetches with bounded retry: transparent on 5xx and network faults, immediate otherwise.
|
|
143
|
+
*
|
|
144
|
+
* @param input the absolute request url.
|
|
145
|
+
* @param init the fetch initializer (headers, body, signal ride through untouched).
|
|
146
|
+
* @param attempts total attempts (default 3); waits ride `retryDelay` backoff.
|
|
147
|
+
* @returns the last response — caller decides ok/error handling.
|
|
148
|
+
*/
|
|
149
|
+
async function fetchWithRetry(input: string, init: RequestInit, attempts = 3): Promise<Response> {
|
|
150
|
+
let lastError: unknown;
|
|
151
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
152
|
+
try {
|
|
153
|
+
const response = await fetch(input, init);
|
|
154
|
+
if (response.ok || response.status < 500 || attempt === attempts) return response;
|
|
155
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay(attempt - 1)));
|
|
156
|
+
} catch (error) {
|
|
157
|
+
lastError = error;
|
|
158
|
+
if (attempt === attempts) throw error;
|
|
159
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay(attempt - 1)));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
throw lastError instanceof Error ? lastError : new Error("Provider request failed.");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Bearer-token header set of the openai wire family (accept stays sse for streaming). */
|
|
166
|
+
function buildOpenAiHeaders(credential: string): HeadersInit {
|
|
167
|
+
return { accept: "text/event-stream", "content-type": "application/json", authorization: `Bearer ${credential}` };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** x-api-key header set of the anthropic wire family, pinned to the 2023-06-01 version. */
|
|
171
|
+
function buildAnthropicHeaders(credential: string): HeadersInit {
|
|
172
|
+
return {
|
|
173
|
+
accept: "text/event-stream",
|
|
174
|
+
"content-type": "application/json",
|
|
175
|
+
"x-api-key": credential,
|
|
176
|
+
"anthropic-version": "2023-06-01",
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Maps the common ChatRequest onto the google generateContent body (systemInstruction + contents + generationConfig). */
|
|
181
|
+
function buildGoogleBody(request: ChatRequest): Record<string, unknown> {
|
|
182
|
+
const system = request.messages
|
|
183
|
+
.filter((message) => message.role === "system")
|
|
184
|
+
.map((message) => ({ text: message.content }));
|
|
185
|
+
const contents = request.messages
|
|
186
|
+
.filter((message) => message.role !== "system")
|
|
187
|
+
.map((message) => ({
|
|
188
|
+
role: message.role === "assistant" ? "model" : "user",
|
|
189
|
+
parts: [{ text: message.content }],
|
|
190
|
+
}));
|
|
191
|
+
return {
|
|
192
|
+
...(system.length ? { systemInstruction: { parts: system } } : {}),
|
|
193
|
+
contents,
|
|
194
|
+
generationConfig: {
|
|
195
|
+
...(request.temperature === undefined ? {} : { temperature: request.temperature }),
|
|
196
|
+
...(request.maxTokens === undefined ? {} : { maxOutputTokens: request.maxTokens }),
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Maps the common ChatRequest onto the anthropic messages body (system folded into its own field). */
|
|
202
|
+
function buildAnthropicBody(request: ChatRequest): Record<string, unknown> {
|
|
203
|
+
const system = request.messages
|
|
204
|
+
.filter((message) => message.role === "system")
|
|
205
|
+
.map((message) => message.content)
|
|
206
|
+
.join("\n\n");
|
|
207
|
+
return {
|
|
208
|
+
model: request.model,
|
|
209
|
+
messages: request.messages.filter((message) => message.role !== "system"),
|
|
210
|
+
max_tokens: request.maxTokens ?? 4096,
|
|
211
|
+
stream: true,
|
|
212
|
+
...(system ? { system } : {}),
|
|
213
|
+
...(request.temperature === undefined ? {} : { temperature: request.temperature }),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Maps the common ChatRequest onto the openai chat/completions body. */
|
|
218
|
+
function buildOpenAiBody(request: ChatRequest): Record<string, unknown> {
|
|
219
|
+
return {
|
|
220
|
+
model: request.model,
|
|
221
|
+
messages: request.messages,
|
|
222
|
+
stream: true,
|
|
223
|
+
...(request.temperature === undefined ? {} : { temperature: request.temperature }),
|
|
224
|
+
...(request.maxTokens === undefined ? {} : { max_tokens: request.maxTokens }),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Lists the known providers with their base urls, protocols and credential env names.
|
|
230
|
+
*
|
|
231
|
+
* @returns defensive copies — callers cannot mutate the table.
|
|
232
|
+
*/
|
|
233
|
+
export function listProviders(): ProviderDefinition[] {
|
|
234
|
+
return PROVIDERS.map((provider) => ({ ...provider }));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Resolves one provider to its concrete endpoint and credential.
|
|
239
|
+
*
|
|
240
|
+
* the base url precedence is config `providers.<id>.baseUrl` → active-provider
|
|
241
|
+
* `config.baseUrl` → the table default; the credential comes from
|
|
242
|
+
* `resolveCredential` (config first, then the provider's env var).
|
|
243
|
+
*
|
|
244
|
+
* @returns the definition plus the resolved baseUrl and credential.
|
|
245
|
+
* @throws when no base url can be derived or no credential is present.
|
|
246
|
+
*/
|
|
247
|
+
export function resolveProvider(
|
|
248
|
+
provider: string,
|
|
249
|
+
config: DevThinkConfig,
|
|
250
|
+
paths?: DevThinkPaths,
|
|
251
|
+
): ProviderDefinition & { baseUrl: string; credential: string } {
|
|
252
|
+
const definition = findProvider(provider);
|
|
253
|
+
const isActive = provider === (config.activeProvider || config.provider);
|
|
254
|
+
const baseUrl =
|
|
255
|
+
config.providers?.[provider]?.baseUrl || (isActive ? config.baseUrl : undefined) || definition.baseUrl;
|
|
256
|
+
if (!baseUrl) throw new Error(`Provider ${provider} requires baseUrl in config or --base-url.`);
|
|
257
|
+
const credential = resolveCredential(provider, config, paths);
|
|
258
|
+
if (!credential)
|
|
259
|
+
throw new Error(`No credential found for ${provider}. Set ${definition.env} or configure it explicitly.`);
|
|
260
|
+
return { ...definition, baseUrl, credential };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Streams one chat completion through the provider's native wire protocol.
|
|
265
|
+
*
|
|
266
|
+
* @param request the chat request (provider id, model, messages, sampling knobs, optional abort signal).
|
|
267
|
+
* @param config the resolved DevThink config (active provider, base urls, credentials).
|
|
268
|
+
* @param paths the resolved DevThink paths (credential file lookup).
|
|
269
|
+
* @returns an async generator of normalized ChatEvents (deltas, usage, done).
|
|
270
|
+
* @throws a redacted provider Error on non-ok responses after the retry budget.
|
|
271
|
+
*/
|
|
272
|
+
export async function streamChat(
|
|
273
|
+
request: ChatRequest,
|
|
274
|
+
config: DevThinkConfig,
|
|
275
|
+
paths?: DevThinkPaths,
|
|
276
|
+
): Promise<AsyncGenerator<ChatEvent>> {
|
|
277
|
+
const provider = resolveProvider(request.provider, config, paths);
|
|
278
|
+
const url =
|
|
279
|
+
provider.protocol === "google"
|
|
280
|
+
? `${joinUrl(provider.baseUrl, `models/${encodeURIComponent(request.model)}:streamGenerateContent`)}?alt=sse`
|
|
281
|
+
: joinUrl(provider.baseUrl, provider.protocol === "anthropic" ? "/v1/messages" : "/chat/completions");
|
|
282
|
+
const headers =
|
|
283
|
+
provider.protocol === "google"
|
|
284
|
+
? { accept: "text/event-stream", "content-type": "application/json", "x-goog-api-key": provider.credential }
|
|
285
|
+
: provider.protocol === "anthropic"
|
|
286
|
+
? buildAnthropicHeaders(provider.credential)
|
|
287
|
+
: buildOpenAiHeaders(provider.credential);
|
|
288
|
+
const body =
|
|
289
|
+
provider.protocol === "google"
|
|
290
|
+
? buildGoogleBody(request)
|
|
291
|
+
: provider.protocol === "anthropic"
|
|
292
|
+
? buildAnthropicBody(request)
|
|
293
|
+
: buildOpenAiBody(request);
|
|
294
|
+
const response = await fetchWithRetry(url, {
|
|
295
|
+
method: "POST",
|
|
296
|
+
headers,
|
|
297
|
+
body: JSON.stringify(body),
|
|
298
|
+
...(request.signal ? { signal: request.signal } : {}),
|
|
299
|
+
});
|
|
300
|
+
if (!response.ok) throw cleanError(await response.text(), response.status);
|
|
301
|
+
return parseEventStream(response, { provider: provider.id, model: request.model, signal: request.signal });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Lists the models the provider currently exposes for the resolved credential.
|
|
306
|
+
*
|
|
307
|
+
* @param providerName the provider id from the PROVIDERS table.
|
|
308
|
+
* @param config the resolved DevThink config.
|
|
309
|
+
* @param paths the resolved DevThink paths (credential file lookup).
|
|
310
|
+
* @returns model descriptors (id, provider, context window, streaming support); google reads inputTokenLimit, the openai family reads context_length.
|
|
311
|
+
* @throws a redacted provider Error on non-ok responses after the retry budget.
|
|
312
|
+
*/
|
|
313
|
+
export async function listModels(
|
|
314
|
+
providerName: string,
|
|
315
|
+
config: DevThinkConfig,
|
|
316
|
+
paths?: DevThinkPaths,
|
|
317
|
+
): Promise<ModelInfo[]> {
|
|
318
|
+
const provider = resolveProvider(providerName, config, paths);
|
|
319
|
+
if (provider.protocol === "google") {
|
|
320
|
+
const response = await fetchWithRetry(joinUrl(provider.baseUrl, "models"), {
|
|
321
|
+
headers: { "x-goog-api-key": provider.credential },
|
|
322
|
+
});
|
|
323
|
+
if (!response.ok) throw cleanError(await response.text(), response.status);
|
|
324
|
+
const payload = (await response.json()) as { models?: Array<{ name?: string; inputTokenLimit?: number }> };
|
|
325
|
+
return (payload.models || []).map((model) => ({
|
|
326
|
+
id: (model.name || "").replace(/^models\//, ""),
|
|
327
|
+
provider: provider.id,
|
|
328
|
+
contextWindow: model.inputTokenLimit,
|
|
329
|
+
supportsStreaming: true,
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
const path = provider.protocol === "anthropic" ? "/v1/models" : "/models";
|
|
333
|
+
const headers =
|
|
334
|
+
provider.protocol === "anthropic"
|
|
335
|
+
? buildAnthropicHeaders(provider.credential)
|
|
336
|
+
: buildOpenAiHeaders(provider.credential);
|
|
337
|
+
const response = await fetchWithRetry(joinUrl(provider.baseUrl, path), { headers });
|
|
338
|
+
if (!response.ok) throw cleanError(await response.text(), response.status);
|
|
339
|
+
const payload = (await response.json()) as { data?: Array<{ id?: string; context_length?: number }> };
|
|
340
|
+
return (payload.data || [])
|
|
341
|
+
.filter((model) => model.id)
|
|
342
|
+
.map((model) => ({
|
|
343
|
+
id: model.id as string,
|
|
344
|
+
provider: provider.id,
|
|
345
|
+
contextWindow: model["context_length"],
|
|
346
|
+
supportsStreaming: true,
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* ===== block: providercli ===== */
|
|
351
|
+
/**
|
|
352
|
+
* @file providercli.ts
|
|
353
|
+
* @module gateway/providercli
|
|
354
|
+
* @description
|
|
355
|
+
* The provider account console of the ownership wave (wave/ownership.gateway,
|
|
356
|
+
* step 5 — the final double move): moved verbatim from devthink/server.ts
|
|
357
|
+
* (the grand-merge interned provider account console cli, its library
|
|
358
|
+
* barrel and the opencode server plugin entries), so the gateway owns its
|
|
359
|
+
* provider console family — one owner, the family consumes, and devthink
|
|
360
|
+
* re-exports the public faces for the compat consumers.
|
|
361
|
+
*
|
|
362
|
+
* Unified merge of all duplicate CLI generations into a single modular entry
|
|
363
|
+
* point. Zero external dependencies (only node:* builtins + global fetch).
|
|
364
|
+
*
|
|
365
|
+
* Merge provenance (every unique feature preserved):
|
|
366
|
+
* - BASE cli.ts (=cli(1).ts): modular command CLI with commands
|
|
367
|
+
* login / accounts / quota / config / models / status / doctor /
|
|
368
|
+
* version / interactive menu. Secure atomic writes (chmod 600),
|
|
369
|
+
* multi-account management, quota thresholds via config.ts.
|
|
370
|
+
* - FROM cli(2).ts (=cli(3).ts):
|
|
371
|
+
* logoutFlow + top-level logout command (OAuth token revocation +
|
|
372
|
+
* account removal, --all support, interactive picker);
|
|
373
|
+
* enableAccountFlow / disableAccountFlow as top-level commands;
|
|
374
|
+
* MODELS_2026_CATALOG (2026 antigravity model catalog);
|
|
375
|
+
* buildOpenCodeModelDefinitions() (rich provider model defs with
|
|
376
|
+
* bypass metadata); configureModelsFlow() writing
|
|
377
|
+
* provider.google.models (+ provider.antigravity.models and
|
|
378
|
+
* top-level model shortcuts) into opencode.json with --global /
|
|
379
|
+
* --path resolution; locateOpenCodeJson() +
|
|
380
|
+
* resolveOpenCodeJsonCandidates(); atomicWriteFileAtomic()
|
|
381
|
+
* (fs/promises); resolveAntigravityJsonPath();
|
|
382
|
+
* helpers confirm / redactEmail / fmtDuration / printHeader.
|
|
383
|
+
* v2.1.16: the MODELS_2026_CATALOG data moved to models.ts (single
|
|
384
|
+
* owner); the dead sync atomic writer twin was deleted.
|
|
385
|
+
* - FROM cli(4).ts: richest account listing (store version, activeIndex,
|
|
386
|
+
* activeIndexByFamily, addedAt/createdAt/expiry/projectId,
|
|
387
|
+
* remaining/limit, rateLimitResetTimes per account); quota results
|
|
388
|
+
* persisted back into the accounts file; diagnostics dumping the
|
|
389
|
+
* first 1000 chars of the raw accounts file.
|
|
390
|
+
* - FROM cli(5).ts: manage command (interactive per-account enable/disable
|
|
391
|
+
* toggle via auth.js accountManager.enable(email,bool)); login mode
|
|
392
|
+
* selection "1 add accounts / 2 fresh start (wipe all)";
|
|
393
|
+
* claude-sonnet low/high thinkingLevel variants in configure
|
|
394
|
+
* output; soft-quota (softQuotaUntil) display in account listing;
|
|
395
|
+
* debugLogger audit log on login; diagnostics reporting the logs
|
|
396
|
+
* dir and the default model.
|
|
397
|
+
* - cli(6).ts contained nothing unique (strict subset) - deleted.
|
|
398
|
+
*
|
|
399
|
+
* Sibling modules are consumed from their one owner only:
|
|
400
|
+
* ./oauth.js (the provider oauth family) ./accounts.js ./quota.js ./models.js
|
|
401
|
+
* ./versionregistry.js (in-package relatives since the ownership move — the
|
|
402
|
+
* cross-package hop died with it)
|
|
403
|
+
* @wenathlan/devthink/config (v2.1.14: validate.ts + system.ts consolidated
|
|
404
|
+
* into it) @wenathlan/devthink/constants @wenathlan/devthink/core — the
|
|
405
|
+
* os-local families stay at the os root behind the published specifiers
|
|
406
|
+
*
|
|
407
|
+
* The move's sanctioned changes (the step 5 list):
|
|
408
|
+
* - the getry faces the console consumed through the published specifiers
|
|
409
|
+
* (@wenathlan/getry/oauth and siblings) answer from in-package relatives
|
|
410
|
+
* now (./oauth.js, ./quota.js, ./models.js, ...) — the cross-package hop
|
|
411
|
+
* died with the move
|
|
412
|
+
* - the os-local families (config, core, constants, oslabels) answer from
|
|
413
|
+
* the published devthink specifiers (@wenathlan/devthink/config and
|
|
414
|
+
* siblings) — the same resolution they always had
|
|
415
|
+
* - the getAccountManager() fallback twin was DELETED (~160 lines): every
|
|
416
|
+
* call site answers getDefaultAccountManager({ disableBackgroundTimer:
|
|
417
|
+
* true }) from ./accounts.js — the twin raced the real manager on the
|
|
418
|
+
* same antigravity-accounts.json file
|
|
419
|
+
* - maskToken (the provider one, slice(0,6)) renamed maskProviderToken —
|
|
420
|
+
* the shadow-kill (the getry auth family owns masktoken; behavior kept)
|
|
421
|
+
* - buildGoogleProviderModelConfigs + buildOpenCodeModelDefinitions +
|
|
422
|
+
* detectTargetProvider moved to the models owner (models.ts, the
|
|
423
|
+
* "the opencode model definitions" section); this console imports them
|
|
424
|
+
* from ./models.js
|
|
425
|
+
* - providerServer/googleAuth/antigravityAuth merged into the antigravity
|
|
426
|
+
* owner (antigravity.ts) and re-exported here for compat
|
|
427
|
+
* - the oauth client pair mirror died: GEMINI_OAUTH_CLIENT_ID/SECRET
|
|
428
|
+
* import from ./oauth.js (the owner's CLIENT_ID/CLIENT_SECRET) under
|
|
429
|
+
* the historical call-site names
|
|
430
|
+
* - the dead invokedAsMain autorun block stayed dead in the merged file
|
|
431
|
+
* (its basename test never matched server.ts and its main() reference
|
|
432
|
+
* rode the composed boot) — it was dropped with the move instead of
|
|
433
|
+
* carrying a binding that no longer exists here
|
|
434
|
+
*
|
|
435
|
+
* Bypass constants (documented):
|
|
436
|
+
* client_id=681255809395-oo8f…b135j (.apps.googleusercontent.com)
|
|
437
|
+
* secret=GOCSPX-4uHgMPm…lXFsxl
|
|
438
|
+
* UA=GeminiCLI/0.57.0 (linux; x64; GitHub) google-api-nodejs-client/9.15.1
|
|
439
|
+
* X-Goog-Api-Client=gl-node/22.19.0
|
|
440
|
+
* Client-Metadata=ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI
|
|
441
|
+
* project fallback=rising-fact-p41fc
|
|
442
|
+
* isolated dir=~/.config/opencode (override with OPENCODE_CONFIG_DIR)
|
|
443
|
+
*
|
|
444
|
+
* @author ONDA 5 - Finalization (merge pass)
|
|
445
|
+
* @license MIT
|
|
446
|
+
* @version 6.0.0
|
|
447
|
+
*/
|
|
448
|
+
|
|
449
|
+
import * as crypto from "node:crypto";
|
|
450
|
+
import * as fs from "node:fs";
|
|
451
|
+
import * as fsp from "node:fs/promises";
|
|
452
|
+
import * as os from "node:os";
|
|
453
|
+
import * as path from "node:path";
|
|
454
|
+
|
|
455
|
+
import * as readline from "node:readline/promises";
|
|
456
|
+
import * as accountsMod from "./accounts.js";
|
|
457
|
+
import * as configMod from "@wenathlan/devthink/config";
|
|
458
|
+
import * as constantsMod from "@wenathlan/devthink/constants";
|
|
459
|
+
// v2.1.16 single-owner fix: raw values come from constants.js, identity
|
|
460
|
+
// snapshots from fingerprint.js and model identification from models.js via
|
|
461
|
+
// static named imports (the namespace interop wrappers below stay for the
|
|
462
|
+
// historical CJS/ESM fallback chains — the local literal copies are gone).
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
import * as systemMod from "@wenathlan/devthink/core";
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
import * as modelsMod from "./models.js";
|
|
469
|
+
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
// Local module imports (ESM .js extension for Node resolver, source is .ts).
|
|
472
|
+
// Import from deduplicated base modules ONLY - never from "(N)" copies.
|
|
473
|
+
// (the ownership move: the getry faces answer from in-package relatives)
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
import * as oauthMod from "./oauth.js";
|
|
476
|
+
import * as authMod from "./oauth.js";
|
|
477
|
+
import * as quotaMod from "./quota.js";
|
|
478
|
+
import * as versionMod from "./versionregistry.js";
|
|
479
|
+
// (the twin deletion) the accounts owner's real manager answers every call
|
|
480
|
+
// site the fallback twin used to race
|
|
481
|
+
|
|
482
|
+
// (the model transforms) the opencode model definitions moved to the models
|
|
483
|
+
// owner — the configure flow and the export barrel speak them from there
|
|
484
|
+
|
|
485
|
+
// (the oauth mirror kill) the client pair imports from the oauth owner under
|
|
486
|
+
// the historical call-site names
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
// Resolve interop (CJS/ESM dual): pick default if exists or namespace
|
|
490
|
+
const oauth: any = (oauthMod as any).default ?? oauthMod;
|
|
491
|
+
const accountsPkg: any = (accountsMod as any).default ?? accountsMod;
|
|
492
|
+
const configPkg: any = (configMod as any).default ?? configMod;
|
|
493
|
+
const constantsPkg: any = (constantsMod as any).default ?? constantsMod;
|
|
494
|
+
const quotaPkg: any = (quotaMod as any).default ?? quotaMod;
|
|
495
|
+
const versionPkg: any = (versionMod as any).default ?? versionMod;
|
|
496
|
+
const authPkg: any = (authMod as any).default ?? authMod;
|
|
497
|
+
const modelsPkg: any = (modelsMod as any).default ?? modelsMod;
|
|
498
|
+
const systemPkg: any = (systemMod as any).default ?? systemMod;
|
|
499
|
+
|
|
500
|
+
// Destructure needed APIs with fallback to namespace exports
|
|
501
|
+
const authenticate = oauth.authenticate as typeof import("./oauth.js").authenticate;
|
|
502
|
+
// v2.1.15 Phase A: ALL_MODELS_2026 lives in models.ts (model identification
|
|
503
|
+
// is owned by models.ts alone).
|
|
504
|
+
// v2.1.16 single-owner fix: the last-resort fallback entry is derived from
|
|
505
|
+
// models.js — a MODEL_BY_ID lookup keyed by the models.js DEFAULT_MODEL_ID
|
|
506
|
+
// ("antigravity-gemini-3-pro", whose catalog record carries the very same
|
|
507
|
+
// name/context/output/api/quotaGroup the former local literal spelled out).
|
|
508
|
+
const MODELS_FALLBACK_ENTRY = MODEL_BY_ID[DEFAULT_MODEL_ID];
|
|
509
|
+
const ALL_MODELS_FALLBACK: ReadonlyArray<any> =
|
|
510
|
+
(modelsMod.ALL_MODELS_2026 as any) ??
|
|
511
|
+
(modelsPkg.ALL_MODELS_2026 as any) ??
|
|
512
|
+
(MODELS_FALLBACK_ENTRY ? [MODELS_FALLBACK_ENTRY] : []);
|
|
513
|
+
|
|
514
|
+
const loadConfig = (configMod.loadConfig as any) ?? (configPkg.loadConfig as any);
|
|
515
|
+
// grand-merge rename: the provider saveConfig collided with devthink's root
|
|
516
|
+
// saveConfig in merged config.ts and was renamed agsaveConfig — reference the
|
|
517
|
+
// renamed export (the provider config writer), not devthink's saveConfig.
|
|
518
|
+
const providerSaveConfig = (configMod.agsaveConfig as any) ?? (configPkg.agsaveConfig as any);
|
|
519
|
+
const updateConfig = (configMod.updateConfig as any) ?? (configPkg.updateConfig as any);
|
|
520
|
+
const DEFAULT_CONFIG = (configMod.DEFAULT_CONFIG as any) ?? (configPkg.DEFAULT_CONFIG as any);
|
|
521
|
+
const parseAndValidateConfig = (configMod.parseAndValidateConfig as any) ?? (configPkg.parseAndValidateConfig as any);
|
|
522
|
+
const getConfigPath = (configMod.getConfigPath as any) ?? (() => path.join(resolveBaseDir(), "antigravity.json"));
|
|
523
|
+
|
|
524
|
+
const AccountManagerClass = (accountsMod.AccountManager as any) ?? (accountsPkg as any);
|
|
525
|
+
// constants
|
|
526
|
+
const ALL_MODELS_2026: ReadonlyArray<any> = (modelsMod.ALL_MODELS_2026 as any) ?? ALL_MODELS_FALLBACK;
|
|
527
|
+
// the FILE_PATHS import is the real constants export (the fold alias retired)
|
|
528
|
+
// v2.1.16 single-owner fix: the fallback literal comes from constants.js
|
|
529
|
+
// (PROJECT_FALLBACK owner) instead of a local copy.
|
|
530
|
+
// the FALLBACK_PROJECT_ID import is the real constants export (the fold alias retired)
|
|
531
|
+
const PLUGIN_VERSION_FALLBACK = PLUGIN_VERSION;
|
|
532
|
+
// Ported from cli(2).ts: antigravity UA/version fallbacks (constants first, models.ts backup)
|
|
533
|
+
// v2.1.16 single-owner fix: the "1.19.2" literal comes from constants.js
|
|
534
|
+
// (ANTIGRAVITY_VERSION_FALLBACK owner) instead of a local copy.
|
|
535
|
+
const ANTIGRAVITY_VER_FB: string = (constantsMod as any).ANTIGRAVITY_VERSION_FALLBACK ?? ANTIGRAVITY_VERSION_FALLBACK;
|
|
536
|
+
const ANTIGRAVITY_UA_FB: string =
|
|
537
|
+
(constantsMod as any).ANTIGRAVITY_USER_AGENT_FALLBACK ?? `antigravity/${ANTIGRAVITY_VER_FB}`;
|
|
538
|
+
|
|
539
|
+
// quota
|
|
540
|
+
const QuotaManager = (quotaMod.QuotaManager as any) ?? (quotaPkg.QuotaManager as any);
|
|
541
|
+
const getDefaultQuotaManager = (quotaMod.getDefaultQuotaManager as any) ?? null;
|
|
542
|
+
const checkQuotaFn = (quotaMod.checkQuota as any) ?? null;
|
|
543
|
+
const getQuotaGroupsFn = (quotaMod.getQuotaGroups as any) ?? null;
|
|
544
|
+
|
|
545
|
+
// version
|
|
546
|
+
const getVersion = (versionMod.getVersion as any) ?? (async () => FALLBACK_PROJECT_ID);
|
|
547
|
+
// v2.1.16 single-owner fix: the "1.15.8" hardcode comes from constants.js
|
|
548
|
+
// (ANTIGRAVITY_VERSION_MIN_SUPPORTED owner) instead of a local literal.
|
|
549
|
+
const getVersionInfo =
|
|
550
|
+
(versionMod.getVersionInfo as any) ??
|
|
551
|
+
(async () => ({ version: ANTIGRAVITY_VERSION_MIN_SUPPORTED, source: "hardcoded" }));
|
|
552
|
+
|
|
553
|
+
// auth.js (v3 robin-hood account manager + direct retrieveQuota) - consumers ported from cli(4)/cli(5)
|
|
554
|
+
const AuthAccountManager = (authMod.accountManager as any) ?? (authPkg.accountManager as any) ?? null;
|
|
555
|
+
const authRetrieveQuota = (authMod.retrieveQuota as any) ?? (authPkg.retrieveQuota as any) ?? null;
|
|
556
|
+
|
|
557
|
+
// models.js (flat 2026-08-25 catalog driving provider.google.models config)
|
|
558
|
+
const FLAT_MODELS_2026: any[] = (modelsMod.MODELS_2026_08_25 as any) ?? (modelsPkg.MODELS_2026_08_25 as any) ?? [];
|
|
559
|
+
// v2.1.16 single-owner fix: the endpoint URLs of the defensive fallback
|
|
560
|
+
// object come from constants.js (ENDPOINTS raw host-map owner).
|
|
561
|
+
const CODE_ASSIST = (modelsMod.CODE_ASSIST_ENDPOINTS as any) ??
|
|
562
|
+
(modelsPkg.CODE_ASSIST_ENDPOINTS as any) ?? {
|
|
563
|
+
PROD: ENDPOINTS.PROD,
|
|
564
|
+
DAILY: ENDPOINTS.DAILY,
|
|
565
|
+
SANDBOX: ENDPOINTS.SANDBOX,
|
|
566
|
+
};
|
|
567
|
+
// v2.1.16 single-owner fix: DEFAULT_MODEL_ID answers only from models.js
|
|
568
|
+
// (the owner of the name) — the former local literal retired into the import.
|
|
569
|
+
|
|
570
|
+
// system.js (debug logger singleton + logs dir) - audit trail on login (cli(5) feature)
|
|
571
|
+
const debugLoggerImpl: any = (systemMod.debugLogger as any) ?? (systemPkg.debugLogger as any) ?? null;
|
|
572
|
+
const logsDirImpl: any = (systemMod.logsDir as any) ?? (systemPkg.logsDir as any) ?? null;
|
|
573
|
+
|
|
574
|
+
/** Append a redacted line to the rotating debug audit log (best-effort). */
|
|
575
|
+
function auditLog(msg: string): void {
|
|
576
|
+
try {
|
|
577
|
+
debugLoggerImpl?.get?.().log(msg);
|
|
578
|
+
} catch {
|
|
579
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Resolve the antigravity logs directory (system.js logsDir with local fallback). */
|
|
584
|
+
function resolveLogsDir(): string {
|
|
585
|
+
try {
|
|
586
|
+
if (typeof logsDirImpl === "function") return logsDirImpl();
|
|
587
|
+
} catch {
|
|
588
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
589
|
+
}
|
|
590
|
+
return path.join(resolveBaseDir(), "antigravity-logs");
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// oauth.js extras used by the ported model-definition builder (cli(2))
|
|
594
|
+
const OAUTH_SUPPORTED_MODELS: readonly string[] = (oauthMod.SUPPORTED_MODELS_2026 as any) ?? [];
|
|
595
|
+
|
|
596
|
+
// ===========================================================================
|
|
597
|
+
// 00. ANSI COLORS - zero dep
|
|
598
|
+
// ===========================================================================
|
|
599
|
+
const ANSI = {
|
|
600
|
+
reset: "\x1b[0m",
|
|
601
|
+
bold: "\x1b[1m",
|
|
602
|
+
dim: "\x1b[2m",
|
|
603
|
+
italic: "\x1b[3m",
|
|
604
|
+
underline: "\x1b[4m",
|
|
605
|
+
inverse: "\x1b[7m",
|
|
606
|
+
hidden: "\x1b[8m",
|
|
607
|
+
// foreground
|
|
608
|
+
black: "\x1b[30m",
|
|
609
|
+
red: "\x1b[31m",
|
|
610
|
+
green: "\x1b[32m",
|
|
611
|
+
yellow: "\x1b[33m",
|
|
612
|
+
blue: "\x1b[34m",
|
|
613
|
+
magenta: "\x1b[35m",
|
|
614
|
+
cyan: "\x1b[36m",
|
|
615
|
+
white: "\x1b[37m",
|
|
616
|
+
gray: "\x1b[90m",
|
|
617
|
+
brightRed: "\x1b[91m",
|
|
618
|
+
brightGreen: "\x1b[92m",
|
|
619
|
+
brightYellow: "\x1b[93m",
|
|
620
|
+
brightBlue: "\x1b[94m",
|
|
621
|
+
brightMagenta: "\x1b[95m",
|
|
622
|
+
brightCyan: "\x1b[96m",
|
|
623
|
+
brightWhite: "\x1b[97m",
|
|
624
|
+
// background
|
|
625
|
+
bgBlack: "\x1b[40m",
|
|
626
|
+
bgRed: "\x1b[41m",
|
|
627
|
+
bgGreen: "\x1b[42m",
|
|
628
|
+
bgYellow: "\x1b[43m",
|
|
629
|
+
bgBlue: "\x1b[44m",
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
function colorize(s: string, ...codes: string[]): string {
|
|
633
|
+
if (!s) return s;
|
|
634
|
+
// disable colors if NO_COLOR is set
|
|
635
|
+
if (process.env.NO_COLOR) return s;
|
|
636
|
+
return `${codes.join("")}${s}${ANSI.reset}`;
|
|
637
|
+
}
|
|
638
|
+
const C = {
|
|
639
|
+
bold: (s: string) => colorize(s, ANSI.bold),
|
|
640
|
+
dim: (s: string) => colorize(s, ANSI.dim),
|
|
641
|
+
red: (s: string) => colorize(s, ANSI.red),
|
|
642
|
+
green: (s: string) => colorize(s, ANSI.green),
|
|
643
|
+
yellow: (s: string) => colorize(s, ANSI.yellow),
|
|
644
|
+
cyan: (s: string) => colorize(s, ANSI.cyan),
|
|
645
|
+
magenta: (s: string) => colorize(s, ANSI.magenta),
|
|
646
|
+
blue: (s: string) => colorize(s, ANSI.blue),
|
|
647
|
+
gray: (s: string) => colorize(s, ANSI.gray),
|
|
648
|
+
white: (s: string) => colorize(s, ANSI.white),
|
|
649
|
+
b: (s: string) => colorize(s, ANSI.bold),
|
|
650
|
+
bgGreen: (s: string) => colorize(s, ANSI.bgGreen, ANSI.black),
|
|
651
|
+
bgRed: (s: string) => colorize(s, ANSI.bgRed, ANSI.white),
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
// Unified logger (plain-text markers instead of emoji glyphs)
|
|
655
|
+
function logInfo(msg: string) {
|
|
656
|
+
console.log(`${C.cyan("[i]")} ${msg}`);
|
|
657
|
+
}
|
|
658
|
+
function logOk(msg: string) {
|
|
659
|
+
console.log(`${C.green("[ok]")} ${msg}`);
|
|
660
|
+
}
|
|
661
|
+
function logWarn(msg: string) {
|
|
662
|
+
console.log(`${C.yellow("[!]")} ${msg}`);
|
|
663
|
+
}
|
|
664
|
+
function logErr(msg: string) {
|
|
665
|
+
console.error(`${C.red("[x]")} ${msg}`);
|
|
666
|
+
}
|
|
667
|
+
function logDim(msg: string) {
|
|
668
|
+
console.log(C.dim(msg));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ===========================================================================
|
|
672
|
+
// 00B. SHARED INTERACTION + FORMAT HELPERS (ported from cli(2).ts)
|
|
673
|
+
// ===========================================================================
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Ask one question on a promises-style readline interface and return the
|
|
677
|
+
* trimmed answer.
|
|
678
|
+
*/
|
|
679
|
+
async function askQuestion(rl: readline.Interface, prompt: string): Promise<string> {
|
|
680
|
+
const ans = await rl.question(prompt);
|
|
681
|
+
return String(ans ?? "").trim();
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Yes/no confirmation prompt (ported from cli(2).ts).
|
|
686
|
+
* Empty input returns {@link defaultYes}.
|
|
687
|
+
*/
|
|
688
|
+
async function confirm(rl: readline.Interface, prompt: string, defaultYes = false): Promise<boolean> {
|
|
689
|
+
const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
|
|
690
|
+
const ans = await askQuestion(rl, `${prompt}${suffix}`);
|
|
691
|
+
if (!ans) return defaultYes;
|
|
692
|
+
return /^(y|yes)$/i.test(ans);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Human friendly duration formatter (ported from cli(2).ts).
|
|
697
|
+
* Examples: "now", "42s", "3m 12s", "5h 03m", "2d 7h".
|
|
698
|
+
*/
|
|
699
|
+
function fmtDuration(ms: number): string {
|
|
700
|
+
if (ms <= 0) return "now";
|
|
701
|
+
const s = Math.floor(ms / 1000);
|
|
702
|
+
if (s < 60) return `${s}s`;
|
|
703
|
+
const m = Math.floor(s / 60);
|
|
704
|
+
if (m < 60) return `${m}m ${s % 60}s`;
|
|
705
|
+
const h = Math.floor(m / 60);
|
|
706
|
+
if (h < 24) return `${h}h ${m % 60}m`;
|
|
707
|
+
const d = Math.floor(h / 24);
|
|
708
|
+
return `${d}d ${h % 24}h`;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Email redaction hook for logs/table output (ported from cli(2).ts).
|
|
713
|
+
* The operator-facing CLI intentionally shows the full address for UX;
|
|
714
|
+
* swap the implementation here to redact the local part when needed.
|
|
715
|
+
*/
|
|
716
|
+
function redactEmail(email: string): string {
|
|
717
|
+
return email;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Boxed CLI banner printed by the ported flows (login/logout/configure/
|
|
722
|
+
* diagnostics style headers from cli(2).ts).
|
|
723
|
+
*/
|
|
724
|
+
function printHeader(): void {
|
|
725
|
+
console.log(
|
|
726
|
+
C.bold(C.cyan("\n+---------------------------------------------------------+")) +
|
|
727
|
+
`\n${C.bold(C.cyan("|"))} ${C.bold("devthink")} ${C.dim(`v${CLI_VERSION}`)} - unified CLI ${C.bold(C.cyan("|"))}` +
|
|
728
|
+
`\n${C.bold(C.cyan("|"))} ${C.dim("direct cloudcode-pa.googleapis.com, no localhost v1 proxy")} ${C.bold(C.cyan("|"))}` +
|
|
729
|
+
`\n${C.bold(C.cyan("+---------------------------------------------------------+"))}\n`,
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ===========================================================================
|
|
734
|
+
// 01. PATHS & SECURE FILE UTILS - atomic + 600
|
|
735
|
+
// ===========================================================================
|
|
736
|
+
function resolveBaseDir(): string {
|
|
737
|
+
const env = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
738
|
+
if (env && env.length > 0) return path.resolve(env);
|
|
739
|
+
return path.join(os.homedir(), ".config", "opencode");
|
|
740
|
+
}
|
|
741
|
+
function resolveAccountsPath(): string {
|
|
742
|
+
try {
|
|
743
|
+
const fp = (FILE_PATHS as any).authDir ? path.join((FILE_PATHS as any).baseDir, "antigravity-accounts.json") : null;
|
|
744
|
+
if (fp) return fp;
|
|
745
|
+
} catch {
|
|
746
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
747
|
+
}
|
|
748
|
+
return path.join(resolveBaseDir(), "antigravity-accounts.json");
|
|
749
|
+
}
|
|
750
|
+
function resolveConfigPath(): string {
|
|
751
|
+
try {
|
|
752
|
+
if (typeof getConfigPath === "function") return getConfigPath();
|
|
753
|
+
} catch {
|
|
754
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
755
|
+
}
|
|
756
|
+
return path.join(resolveBaseDir(), "antigravity.json");
|
|
757
|
+
}
|
|
758
|
+
function ensureDirExists(dir: string) {
|
|
759
|
+
try {
|
|
760
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
761
|
+
} catch {
|
|
762
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
763
|
+
}
|
|
764
|
+
try {
|
|
765
|
+
fs.chmodSync(dir, 0o700);
|
|
766
|
+
} catch {
|
|
767
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function atomicWriteFileSecure(filePath: string, content: string, mode = 0o600): void {
|
|
771
|
+
const dir = path.dirname(filePath);
|
|
772
|
+
ensureDirExists(dir);
|
|
773
|
+
const tmp = `${filePath}.tmp.${crypto.randomBytes(6).toString("hex")}.${process.pid}`;
|
|
774
|
+
try {
|
|
775
|
+
fs.writeFileSync(tmp, content, { encoding: "utf8", mode });
|
|
776
|
+
try {
|
|
777
|
+
fs.chmodSync(tmp, mode);
|
|
778
|
+
} catch {
|
|
779
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
780
|
+
}
|
|
781
|
+
fs.renameSync(tmp, filePath);
|
|
782
|
+
try {
|
|
783
|
+
fs.chmodSync(filePath, mode);
|
|
784
|
+
} catch {
|
|
785
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
786
|
+
}
|
|
787
|
+
} catch (e) {
|
|
788
|
+
try {
|
|
789
|
+
fs.unlinkSync(tmp);
|
|
790
|
+
} catch {
|
|
791
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
792
|
+
}
|
|
793
|
+
throw e;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function readJsonSafe<T>(filePath: string, fallback: T): T {
|
|
797
|
+
try {
|
|
798
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
799
|
+
try {
|
|
800
|
+
fs.chmodSync(filePath, 0o600);
|
|
801
|
+
} catch {
|
|
802
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
803
|
+
}
|
|
804
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
805
|
+
return JSON.parse(raw) as T;
|
|
806
|
+
} catch {
|
|
807
|
+
return fallback;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
function maskProviderToken(t: string): string {
|
|
811
|
+
if (!t) return "***";
|
|
812
|
+
if (t.length <= 12) return "***";
|
|
813
|
+
return `${t.slice(0, 6)}...${t.slice(-4)}`;
|
|
814
|
+
}
|
|
815
|
+
function formatDate(ms?: number): string {
|
|
816
|
+
if (!ms) return "-";
|
|
817
|
+
try {
|
|
818
|
+
return new Date(ms).toISOString().replace("T", " ").slice(0, 19);
|
|
819
|
+
} catch {
|
|
820
|
+
return String(ms);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// ---------------------------------------------------------------------------
|
|
825
|
+
// 01B. ASYNC ATOMIC WRITERS (ported from cli(2).ts)
|
|
826
|
+
// ---------------------------------------------------------------------------
|
|
827
|
+
|
|
828
|
+
/** mkdir recursive + chmod, promise flavored (never throws). */
|
|
829
|
+
async function ensureDirAsync(dir: string): Promise<void> {
|
|
830
|
+
try {
|
|
831
|
+
await fsp.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
832
|
+
} catch {
|
|
833
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
await fsp.chmod(dir, 0o700);
|
|
837
|
+
} catch {
|
|
838
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Atomic write using node:fs/promises (ported from cli(2).ts):
|
|
844
|
+
* temp file in the same directory with random suffix, chmod 600, rename.
|
|
845
|
+
*/
|
|
846
|
+
async function atomicWriteFileAtomic(filePath: string, content: string, mode = 0o600): Promise<void> {
|
|
847
|
+
const dir = path.dirname(filePath);
|
|
848
|
+
await ensureDirAsync(dir);
|
|
849
|
+
const rand = crypto.randomBytes(6).toString("hex");
|
|
850
|
+
const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${rand}.tmp`);
|
|
851
|
+
try {
|
|
852
|
+
await fsp.writeFile(tmp, content, { encoding: "utf8", mode });
|
|
853
|
+
try {
|
|
854
|
+
await fsp.chmod(tmp, mode);
|
|
855
|
+
} catch {
|
|
856
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
857
|
+
}
|
|
858
|
+
await fsp.rename(tmp, filePath);
|
|
859
|
+
try {
|
|
860
|
+
await fsp.chmod(filePath, mode);
|
|
861
|
+
} catch {
|
|
862
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
863
|
+
}
|
|
864
|
+
} finally {
|
|
865
|
+
try {
|
|
866
|
+
await fsp.unlink(tmp);
|
|
867
|
+
} catch {
|
|
868
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Sibling of {@link atomicWriteFileAtomic} on the sync fs API
|
|
875
|
+
* (ported from cli(2).ts): tmp + chmod 600 + rename.
|
|
876
|
+
* v2.1.16 single-owner fix: DELETED — a dead twin of the live
|
|
877
|
+
* {@link atomicWriteFileSecure} above with zero callers anywhere in the repo
|
|
878
|
+
* (audit 16-B); the export barrel entry was removed with it.
|
|
879
|
+
*/
|
|
880
|
+
|
|
881
|
+
/** Async JSON reader returning null for missing/invalid files, {} for empty. */
|
|
882
|
+
async function readJsonFileSafe(filePath: string): Promise<any | null> {
|
|
883
|
+
try {
|
|
884
|
+
const txt = await fsp.readFile(filePath, "utf8");
|
|
885
|
+
if (!txt.trim()) return {};
|
|
886
|
+
return JSON.parse(txt);
|
|
887
|
+
} catch {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// ---------------------------------------------------------------------------
|
|
893
|
+
// 01C. OPENCODE.JSON LOCATION (ported from cli(2).ts)
|
|
894
|
+
// ---------------------------------------------------------------------------
|
|
895
|
+
|
|
896
|
+
/** Where opencode.json was found (or where it would be created). */
|
|
897
|
+
interface OpenCodeJsonLocation {
|
|
898
|
+
path: string;
|
|
899
|
+
exists: boolean;
|
|
900
|
+
source: "cwd" | "global" | "env" | "custom";
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/** Deterministic candidate list for opencode.json discovery (env wins first). */
|
|
904
|
+
function resolveOpenCodeJsonCandidates(): string[] {
|
|
905
|
+
const candidates: string[] = [];
|
|
906
|
+
// OPENCODE_CONFIG (opencode >= 1.18) points at the exact config FILE - the
|
|
907
|
+
// strongest signal there is, so it goes first (plus its dir's canonical
|
|
908
|
+
// name, in case the file itself is named differently).
|
|
909
|
+
const envFile = process.env.OPENCODE_CONFIG?.trim();
|
|
910
|
+
if (envFile) {
|
|
911
|
+
const absFile = path.resolve(envFile);
|
|
912
|
+
candidates.push(absFile, path.join(path.dirname(absFile), "opencode.json"));
|
|
913
|
+
}
|
|
914
|
+
const envDir = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
915
|
+
if (envDir) {
|
|
916
|
+
candidates.push(path.join(path.resolve(envDir), "opencode.json"));
|
|
917
|
+
}
|
|
918
|
+
candidates.push(path.join(process.cwd(), "opencode.json"));
|
|
919
|
+
candidates.push(path.join(process.cwd(), ".opencode", "opencode.json"));
|
|
920
|
+
candidates.push(path.join(os.homedir(), ".config", "opencode", "opencode.json"));
|
|
921
|
+
candidates.push(path.join(os.homedir(), ".opencode", "opencode.json"));
|
|
922
|
+
// dedup preserving order
|
|
923
|
+
return Array.from(new Set(candidates));
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/** Best-effort classification of where a located opencode.json came from. */
|
|
927
|
+
function classifyOpenCodeJsonSource(cand: string): OpenCodeJsonLocation["source"] {
|
|
928
|
+
const envFile = process.env.OPENCODE_CONFIG?.trim();
|
|
929
|
+
if (envFile && cand === path.resolve(envFile)) return "env";
|
|
930
|
+
const envDir = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
931
|
+
if (envDir && cand === path.join(path.resolve(envDir), "opencode.json")) return "env";
|
|
932
|
+
if (cand.includes(process.cwd())) return "cwd";
|
|
933
|
+
return "global";
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Locate opencode.json honoring --path overrides and --global preference
|
|
938
|
+
* (ported from cli(2).ts).
|
|
939
|
+
*/
|
|
940
|
+
async function locateOpenCodeJson(customPath?: string, preferGlobal = false): Promise<OpenCodeJsonLocation> {
|
|
941
|
+
if (customPath) {
|
|
942
|
+
const abs = path.resolve(customPath);
|
|
943
|
+
let exists = false;
|
|
944
|
+
try {
|
|
945
|
+
await fsp.access(abs, fs.constants.F_OK);
|
|
946
|
+
exists = true;
|
|
947
|
+
} catch {
|
|
948
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
949
|
+
}
|
|
950
|
+
return { path: abs, exists, source: "custom" };
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const candidates = resolveOpenCodeJsonCandidates();
|
|
954
|
+
const ordered = preferGlobal ? [...candidates].reverse() : candidates;
|
|
955
|
+
|
|
956
|
+
for (const cand of ordered) {
|
|
957
|
+
try {
|
|
958
|
+
await fsp.access(cand, fs.constants.F_OK);
|
|
959
|
+
return { path: cand, exists: true, source: classifyOpenCodeJsonSource(cand) };
|
|
960
|
+
} catch {
|
|
961
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// nothing exists - pick the creation target based on the preference flag
|
|
966
|
+
if (preferGlobal) {
|
|
967
|
+
return { path: path.join(resolveBaseDir(), "opencode.json"), exists: false, source: "global" };
|
|
968
|
+
}
|
|
969
|
+
return { path: path.join(process.cwd(), "opencode.json"), exists: false, source: "cwd" };
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Path of the plugin config file antigravity.json (ported from cli(2).ts).
|
|
974
|
+
* Honors an explicit custom path, otherwise delegates to config.getConfigPath.
|
|
975
|
+
*/
|
|
976
|
+
function resolveAntigravityJsonPath(custom?: string): string {
|
|
977
|
+
if (custom) return path.resolve(custom);
|
|
978
|
+
try {
|
|
979
|
+
if (typeof getConfigPath === "function") {
|
|
980
|
+
const p = getConfigPath();
|
|
981
|
+
if (p) return p;
|
|
982
|
+
}
|
|
983
|
+
} catch {
|
|
984
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
985
|
+
}
|
|
986
|
+
return path.join(resolveBaseDir(), "antigravity.json");
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// ===========================================================================
|
|
990
|
+
// 02. ACCOUNT MANAGER FACTORY (singleton aware)
|
|
991
|
+
// (the twin deletion of the ownership move, step 5: the ~160-line
|
|
992
|
+
// getAccountManager fallback twin — the in-memory manager that raced the
|
|
993
|
+
// real one on the same antigravity-accounts.json file — was DELETED; every
|
|
994
|
+
// call site answers getDefaultAccountManager({ disableBackgroundTimer:
|
|
995
|
+
// true }) from ./accounts.js, the accounts owner's real manager.)
|
|
996
|
+
// ===========================================================================
|
|
997
|
+
|
|
998
|
+
// ===========================================================================
|
|
999
|
+
// 03. CONFIG HELPERS
|
|
1000
|
+
// ===========================================================================
|
|
1001
|
+
function loadAntigravityConfig(): any {
|
|
1002
|
+
try {
|
|
1003
|
+
if (typeof loadConfig === "function") return loadConfig();
|
|
1004
|
+
} catch {
|
|
1005
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
1006
|
+
}
|
|
1007
|
+
return readJsonSafe<any>(
|
|
1008
|
+
resolveConfigPath(),
|
|
1009
|
+
DEFAULT_CONFIG ?? {
|
|
1010
|
+
cli_first: true,
|
|
1011
|
+
toast_scope: "all",
|
|
1012
|
+
soft_quota_threshold_percent: 90,
|
|
1013
|
+
quota_fallback: "auto",
|
|
1014
|
+
quota_refresh_interval_minutes: 15,
|
|
1015
|
+
soft_quota_cache_ttl_minutes: 5,
|
|
1016
|
+
pid_offset_enabled: true,
|
|
1017
|
+
google_search_enabled: false,
|
|
1018
|
+
debug: false,
|
|
1019
|
+
version_cache_ttl: 60,
|
|
1020
|
+
},
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
function saveAntigravityConfigAtomic(cfg: any): any {
|
|
1024
|
+
try {
|
|
1025
|
+
if (typeof providerSaveConfig === "function") return providerSaveConfig(cfg);
|
|
1026
|
+
} catch {
|
|
1027
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
1028
|
+
}
|
|
1029
|
+
// manual atomic
|
|
1030
|
+
const validated = (() => {
|
|
1031
|
+
try {
|
|
1032
|
+
if (typeof parseAndValidateConfig === "function") return parseAndValidateConfig(cfg, { strict: false });
|
|
1033
|
+
} catch {
|
|
1034
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
1035
|
+
}
|
|
1036
|
+
return cfg;
|
|
1037
|
+
})();
|
|
1038
|
+
atomicWriteFileSecure(resolveConfigPath(), JSON.stringify(validated, null, 2) + "\n", 0o600);
|
|
1039
|
+
return validated;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// ===========================================================================
|
|
1043
|
+
// 04. QUOTA HELPERS
|
|
1044
|
+
// ===========================================================================
|
|
1045
|
+
async function fetchQuotaForAccount(email: string | undefined, mgr: any, qManager: any): Promise<any> {
|
|
1046
|
+
const targetEmail = email ?? mgr.getActiveAccount()?.email ?? mgr.listAccounts()?.[0]?.email;
|
|
1047
|
+
if (!targetEmail) throw new Error("No account configured. Run `login` first.");
|
|
1048
|
+
const token = await mgr.getValidAccessToken(targetEmail);
|
|
1049
|
+
const projectId = mgr.findAccount?.(targetEmail)?.projectId ?? FALLBACK_PROJECT_ID;
|
|
1050
|
+
// Prefer quota.ts checkQuota if available
|
|
1051
|
+
if (checkQuotaFn && qManager) {
|
|
1052
|
+
// qManager already bound to token? use checkQuota directly
|
|
1053
|
+
try {
|
|
1054
|
+
if (typeof getQuotaGroupsFn === "function") {
|
|
1055
|
+
const groups = await getQuotaGroupsFn(token, projectId, { source: "antigravity" });
|
|
1056
|
+
return { email: targetEmail, groups, token, projectId };
|
|
1057
|
+
}
|
|
1058
|
+
} catch {
|
|
1059
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
// fallback: direct call to retrieveUserQuotaSummary using bypass headers
|
|
1063
|
+
// v2.1.16 single-owner fix: identity headers from constants.js /
|
|
1064
|
+
// fingerprint.js; base URLs and the v1internal method path from
|
|
1065
|
+
// constants.js (CLOUDCODE_*_BASE + CODE_ASSIST_PATH_MAP) — byte-identical
|
|
1066
|
+
// URLs and header values as the former local literals.
|
|
1067
|
+
const headers: Record<string, string> = {
|
|
1068
|
+
Authorization: `Bearer ${token}`,
|
|
1069
|
+
"User-Agent": GEMINI_CLI_USER_AGENT,
|
|
1070
|
+
"X-Goog-Api-Client": X_GOOG_API_CLIENT_GEMINI_CLI,
|
|
1071
|
+
"Client-Metadata": CLIENT_METADATA_STRING,
|
|
1072
|
+
"Content-Type": "application/json",
|
|
1073
|
+
};
|
|
1074
|
+
const endpoints = [
|
|
1075
|
+
`${CLOUDCODE_DAILY_BASE}${CODE_ASSIST_PATH_MAP.RETRIEVE_USER_QUOTA_SUMMARY}`,
|
|
1076
|
+
`${CLOUDCODE_BASE_URL}${CODE_ASSIST_PATH_MAP.RETRIEVE_USER_QUOTA_SUMMARY}`,
|
|
1077
|
+
];
|
|
1078
|
+
let lastErr: any = null;
|
|
1079
|
+
for (const url of endpoints) {
|
|
1080
|
+
try {
|
|
1081
|
+
const controller = new AbortController();
|
|
1082
|
+
// v2.1.16 single-owner fix: raw 10s timeout literal replaced by the
|
|
1083
|
+
// constants.js FETCH_TIMEOUT_MS owner (same 10_000 value).
|
|
1084
|
+
const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
1085
|
+
const res = await fetch(url, {
|
|
1086
|
+
method: "POST",
|
|
1087
|
+
headers,
|
|
1088
|
+
body: JSON.stringify({ project: projectId }),
|
|
1089
|
+
signal: controller.signal,
|
|
1090
|
+
});
|
|
1091
|
+
clearTimeout(t);
|
|
1092
|
+
if (!res.ok) {
|
|
1093
|
+
lastErr = new Error(`${url} -> ${res.status}`);
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
const j: any = await res.json();
|
|
1097
|
+
return { email: targetEmail, raw: j, token, projectId, endpoint: url };
|
|
1098
|
+
} catch (e) {
|
|
1099
|
+
lastErr = e;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
throw lastErr ?? new Error("Failed to fetch quota");
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Persist fetched quota numbers back into the accounts file
|
|
1107
|
+
* (ported from cli(4).ts: quota writes remaining/limit/lastUsed per account).
|
|
1108
|
+
* Pure best-effort: never throws, preserves every other field atomically.
|
|
1109
|
+
*/
|
|
1110
|
+
async function persistQuotaResultToAccountsFile(
|
|
1111
|
+
email: string,
|
|
1112
|
+
data: { remaining?: number | undefined; limit?: number | undefined },
|
|
1113
|
+
): Promise<void> {
|
|
1114
|
+
try {
|
|
1115
|
+
const p = resolveAccountsPath();
|
|
1116
|
+
const store = readJsonSafe<any>(p, { version: 3, accounts: [] });
|
|
1117
|
+
const arr: any[] = store.accounts ?? [];
|
|
1118
|
+
const idx = arr.findIndex((a: any) => String(a.email).toLowerCase() === email.toLowerCase());
|
|
1119
|
+
if (idx < 0) return;
|
|
1120
|
+
const acc = arr[idx];
|
|
1121
|
+
if (!acc) return;
|
|
1122
|
+
if (typeof data.remaining === "number" && Number.isFinite(data.remaining)) acc.remaining = data.remaining;
|
|
1123
|
+
if (typeof data.limit === "number" && Number.isFinite(data.limit)) acc.limit = data.limit;
|
|
1124
|
+
acc.lastUsed = Date.now();
|
|
1125
|
+
store.updatedAt = Date.now();
|
|
1126
|
+
atomicWriteFileSecure(p, JSON.stringify(store, null, 2) + "\n", 0o600);
|
|
1127
|
+
} catch {
|
|
1128
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// ===========================================================================
|
|
1133
|
+
// 05. COMMAND IMPLEMENTATIONS
|
|
1134
|
+
// ===========================================================================
|
|
1135
|
+
|
|
1136
|
+
async function cmdLogin(args: string[], loginRl?: readline.Interface): Promise<void> {
|
|
1137
|
+
const noBrowser = args.includes("--no-browser") || args.includes("--noBrowser");
|
|
1138
|
+
const timeoutIdx = args.indexOf("--timeout");
|
|
1139
|
+
let timeoutMs = 300000;
|
|
1140
|
+
if (timeoutIdx >= 0 && args[timeoutIdx + 1]) {
|
|
1141
|
+
const v = Number(args[timeoutIdx + 1]);
|
|
1142
|
+
if (!Number.isNaN(v) && v > 0) timeoutMs = v;
|
|
1143
|
+
}
|
|
1144
|
+
// --email option only for hint, real email comes from OAuth userinfo
|
|
1145
|
+
const emailOptIdx = args.findIndex((a) => a === "--email" || a.startsWith("--email="));
|
|
1146
|
+
let hintEmail: string | undefined;
|
|
1147
|
+
if (emailOptIdx >= 0) {
|
|
1148
|
+
const a = args[emailOptIdx]!;
|
|
1149
|
+
if (a.includes("=")) hintEmail = a.split("=")[1];
|
|
1150
|
+
else hintEmail = args[emailOptIdx + 1];
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Login mode selection (ported from cli(5).ts):
|
|
1154
|
+
// 1 = add accounts (merge into store) | 2 = fresh start (wipe all first)
|
|
1155
|
+
let freshStart = args.includes("--fresh") || args.includes("--fresh-start") || args.includes("--wipe-all");
|
|
1156
|
+
if (!freshStart && (Boolean(loginRl) || process.stdin.isTTY === true)) {
|
|
1157
|
+
console.log("[login] mode selection: 1 = add accounts (merge) | 2 = fresh start (wipe all accounts)");
|
|
1158
|
+
const ans = loginRl ? await askQuestion(loginRl, "mode 1/2 [1]: ") : await promptLoginModeChoice();
|
|
1159
|
+
freshStart = ans.trim() === "2";
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
console.log(C.bold("\nAntigravity OAuth Login - Gemini CLI Bypass"));
|
|
1163
|
+
console.log(
|
|
1164
|
+
C.dim(
|
|
1165
|
+
`Client: ${GEMINI_OAUTH_CLIENT_ID.includes("681255809395") ? "gemini-cli default" : "custom override"} (id redacted in logs)`,
|
|
1166
|
+
),
|
|
1167
|
+
);
|
|
1168
|
+
console.log(C.dim(`Project fallback: ${FALLBACK_PROJECT_ID}`));
|
|
1169
|
+
console.log(C.dim(`Headers: GeminiCLI/0.57.0 gl-node/22.19.0 ideType=IDE_UNSPECIFIED`));
|
|
1170
|
+
console.log("");
|
|
1171
|
+
|
|
1172
|
+
if (!authenticate) {
|
|
1173
|
+
logErr("oauth.ts did not expose authenticate. Check the imports.");
|
|
1174
|
+
process.exit(1);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const mgr = getDefaultAccountManager({ disableBackgroundTimer: true });
|
|
1178
|
+
|
|
1179
|
+
try {
|
|
1180
|
+
const result = await authenticate({
|
|
1181
|
+
noBrowser,
|
|
1182
|
+
callbackTimeoutMs: timeoutMs,
|
|
1183
|
+
onLog: (lvl: string, msg: string) => {
|
|
1184
|
+
if (lvl === "info") logDim(`[oauth] ${msg}`);
|
|
1185
|
+
else if (lvl === "warn") logWarn(`[oauth] ${msg}`);
|
|
1186
|
+
else logErr(`[oauth] ${msg}`);
|
|
1187
|
+
},
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
const { tokens, userInfo } = result as any;
|
|
1191
|
+
const email = (userInfo?.email ?? hintEmail ?? "").toLowerCase();
|
|
1192
|
+
if (!email) throw new Error("Could not determine the user email.");
|
|
1193
|
+
|
|
1194
|
+
logOk(`Authenticated as ${C.bold(userInfo?.name ?? email)} <${email}>`);
|
|
1195
|
+
|
|
1196
|
+
// Fresh-start wipe (cli(5) "mode 2"): remove every stored account first
|
|
1197
|
+
if (freshStart) {
|
|
1198
|
+
const previous: any[] = mgr.listAccounts?.() ?? [];
|
|
1199
|
+
for (const oldAcc of previous) {
|
|
1200
|
+
try {
|
|
1201
|
+
await mgr.removeAccount(oldAcc.email);
|
|
1202
|
+
} catch {
|
|
1203
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
logWarn(`Fresh start: removed ${previous.length} previous account(s) before adding.`);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Defensive: Google may not return a refresh_token if the prior consent is still
|
|
1210
|
+
// active on the user's Google account. Pre-empt with prompt=consent in oauth.ts
|
|
1211
|
+
// (auth.ts:265) and revokeGoogleToken() on logout, but still guard here.
|
|
1212
|
+
// Hint: if login keeps failing with a stale consent, review/revoke granted apps at
|
|
1213
|
+
// https://myaccount.google.com/permissions and retry.
|
|
1214
|
+
const refreshToken = tokens.refresh_token ?? tokens.refreshToken ?? result.tokens?.refresh_token;
|
|
1215
|
+
if (!refreshToken) {
|
|
1216
|
+
logErr(
|
|
1217
|
+
"Google did not return a refresh_token. Re-run login, or visit https://myaccount.google.com/permissions to remove the prior consent and try again.",
|
|
1218
|
+
);
|
|
1219
|
+
process.exitCode = 1;
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Add to store
|
|
1224
|
+
const account = mgr.addAccount({
|
|
1225
|
+
email,
|
|
1226
|
+
refreshToken,
|
|
1227
|
+
accessToken: tokens.access_token ?? tokens.accessToken,
|
|
1228
|
+
expiryDate: tokens.expires_at ?? Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
|
1229
|
+
projectId: FALLBACK_PROJECT_ID,
|
|
1230
|
+
enabled: true,
|
|
1231
|
+
oauthClientKey: "gemini-cli",
|
|
1232
|
+
oauthClientConfig: {
|
|
1233
|
+
clientId: GEMINI_OAUTH_CLIENT_ID,
|
|
1234
|
+
clientSecret: GEMINI_OAUTH_CLIENT_SECRET,
|
|
1235
|
+
},
|
|
1236
|
+
scopes: [...GEMINI_CLI_SCOPES],
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
console.log("");
|
|
1240
|
+
logOk(`Account ${C.b(`${email}`)} added/saved successfully.`);
|
|
1241
|
+
console.log(C.dim(` Store: ${resolveAccountsPath()} (chmod 600)`));
|
|
1242
|
+
console.log(
|
|
1243
|
+
C.dim(
|
|
1244
|
+
` AccessToken: ${maskProviderToken(account.accessToken ?? tokens.access_token)} expires ${formatDate(account.expiryDate)}`,
|
|
1245
|
+
),
|
|
1246
|
+
);
|
|
1247
|
+
console.log(C.dim(` RefreshToken: ${maskProviderToken(account.refreshToken)}`));
|
|
1248
|
+
console.log("");
|
|
1249
|
+
|
|
1250
|
+
// Audit trail on successful login (ported from cli(5).ts)
|
|
1251
|
+
auditLog(`login ${email}`);
|
|
1252
|
+
|
|
1253
|
+
// Auto set active if first or none active
|
|
1254
|
+
try {
|
|
1255
|
+
const active = mgr.getActiveAccount?.();
|
|
1256
|
+
if (!active || active.email.toLowerCase() !== email) {
|
|
1257
|
+
mgr.setActive?.(email, { sticky: false });
|
|
1258
|
+
console.log(C.gray(`-> Set as active (use set-active to switch)`));
|
|
1259
|
+
}
|
|
1260
|
+
} catch {
|
|
1261
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Verify quota quickly
|
|
1265
|
+
try {
|
|
1266
|
+
logInfo("Checking initial quota...");
|
|
1267
|
+
const q = await fetchQuotaForAccount(email, mgr, null);
|
|
1268
|
+
if (q.raw?.quotaGroups || q.groups) logOk("Quota reachable via bypass headers.");
|
|
1269
|
+
else logWarn("Quota returned empty, but the account is valid.");
|
|
1270
|
+
} catch (e) {
|
|
1271
|
+
logWarn(`Could not verify quota right now: ${(e as Error).message}`);
|
|
1272
|
+
}
|
|
1273
|
+
} catch (e) {
|
|
1274
|
+
logErr(`Login failed: ${(e as Error).message}`);
|
|
1275
|
+
console.error(C.dim((e as Error).stack ?? ""));
|
|
1276
|
+
process.exit(1);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/** Temp readline used when the login mode question runs outside the menu. */
|
|
1281
|
+
async function promptLoginModeChoice(): Promise<string> {
|
|
1282
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1283
|
+
try {
|
|
1284
|
+
return await askQuestion(rl, "mode 1/2 [1]: ");
|
|
1285
|
+
} finally {
|
|
1286
|
+
rl.close();
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
async function cmdAccounts(args: string[]): Promise<void> {
|
|
1291
|
+
const sub = (args[0] ?? "list").toLowerCase();
|
|
1292
|
+
const mgr = getDefaultAccountManager({ disableBackgroundTimer: true });
|
|
1293
|
+
|
|
1294
|
+
switch (sub) {
|
|
1295
|
+
case "list":
|
|
1296
|
+
case "ls": {
|
|
1297
|
+
const jsonFlag = args.includes("--json");
|
|
1298
|
+
/* the richest listing reads the legacy flat-file fields the v3 store
|
|
1299
|
+
* carries beside its typed face (addedAt, softQuotaUntil, remaining,
|
|
1300
|
+
* rateLimitResetTimes, ...): the deleted getAccountManager twin typed
|
|
1301
|
+
* them loosely, so the listing keeps that loose view of the same rows
|
|
1302
|
+
* (one cast at the binding — every read below stays verbatim). */
|
|
1303
|
+
const accounts = (mgr.listAccounts?.() ?? []) as any[];
|
|
1304
|
+
const activeEmail = (() => {
|
|
1305
|
+
try {
|
|
1306
|
+
return mgr.getActiveAccount?.()?.email ?? null;
|
|
1307
|
+
} catch {
|
|
1308
|
+
return null;
|
|
1309
|
+
}
|
|
1310
|
+
})();
|
|
1311
|
+
if (jsonFlag) {
|
|
1312
|
+
console.log(
|
|
1313
|
+
JSON.stringify(
|
|
1314
|
+
{
|
|
1315
|
+
activeEmail,
|
|
1316
|
+
accounts: accounts.map((a: any) => ({ ...a, refreshToken: maskProviderToken(a.refreshToken) })),
|
|
1317
|
+
count: accounts.length,
|
|
1318
|
+
},
|
|
1319
|
+
null,
|
|
1320
|
+
2,
|
|
1321
|
+
),
|
|
1322
|
+
);
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
console.log(C.bold(`\nAntigravity Accounts (${accounts.length}) - ${resolveAccountsPath()}`));
|
|
1326
|
+
if (accounts.length === 0) {
|
|
1327
|
+
logWarn("No accounts configured. Run: cli.ts login");
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
console.log(
|
|
1331
|
+
C.dim(`Active: ${activeEmail ?? "none"} | Strategy: ${(mgr as any).store?.rotationStrategy ?? "round-robin"}`),
|
|
1332
|
+
);
|
|
1333
|
+
// Richest listing (ported from cli(4).ts): store version + family indexes
|
|
1334
|
+
const storeMeta = readJsonSafe<any>(resolveAccountsPath(), { version: 3 } as any);
|
|
1335
|
+
console.log(
|
|
1336
|
+
C.dim(
|
|
1337
|
+
`Store: version=${storeMeta.version ?? "?"} activeIndex=${storeMeta.activeIndex ?? "?"} activeIndexByFamily=${JSON.stringify(storeMeta.activeIndexByFamily ?? {})}`,
|
|
1338
|
+
),
|
|
1339
|
+
);
|
|
1340
|
+
console.log("");
|
|
1341
|
+
const nowMs = Date.now();
|
|
1342
|
+
for (const acc of accounts) {
|
|
1343
|
+
const isActive = activeEmail && acc.email.toLowerCase() === activeEmail.toLowerCase();
|
|
1344
|
+
const enabled =
|
|
1345
|
+
acc.enabled !== false && acc.disabled !== true && !(acc.disabledUntil && acc.disabledUntil > nowMs);
|
|
1346
|
+
const statusIcon = enabled ? C.green("*") : C.red("o");
|
|
1347
|
+
const activeMark = isActive ? C.bold(C.bgGreen(" ACTIVE ")) + " " : " ";
|
|
1348
|
+
const fail = acc.failureCount ? C.yellow(` fail=${acc.failureCount}`) : "";
|
|
1349
|
+
const disabledReason = acc.disabledReason ? C.dim(` (${acc.disabledReason})`) : "";
|
|
1350
|
+
// Soft-quota marker (ported from cli(5).ts)
|
|
1351
|
+
const softLeft =
|
|
1352
|
+
acc.softQuotaUntil && acc.softQuotaUntil > nowMs
|
|
1353
|
+
? C.yellow(` soft-quota=${fmtDuration(acc.softQuotaUntil - nowMs)} left`)
|
|
1354
|
+
: "";
|
|
1355
|
+
const exhLeft =
|
|
1356
|
+
acc.quotaExhaustedUntil && acc.quotaExhaustedUntil > nowMs
|
|
1357
|
+
? C.yellow(` quota-cooldown=${fmtDuration(acc.quotaExhaustedUntil - nowMs)} left`)
|
|
1358
|
+
: "";
|
|
1359
|
+
// remaining/limit + rate-limit windows (ported from cli(4).ts)
|
|
1360
|
+
const remain =
|
|
1361
|
+
acc.remaining !== undefined && acc.limit !== undefined
|
|
1362
|
+
? C.dim(` remaining=${acc.remaining}/${acc.limit}`)
|
|
1363
|
+
: "";
|
|
1364
|
+
const rateLimits =
|
|
1365
|
+
acc.rateLimitResetTimes && Object.keys(acc.rateLimitResetTimes).length
|
|
1366
|
+
? C.dim(` rateLimits=${JSON.stringify(acc.rateLimitResetTimes)}`)
|
|
1367
|
+
: "";
|
|
1368
|
+
console.log(
|
|
1369
|
+
`${statusIcon} ${activeMark}${C.bold(redactEmail(String(acc.email)))} ${enabled ? C.green("enabled") : C.red("disabled")} ${C.dim(`project=${acc.projectId ?? FALLBACK_PROJECT_ID}`)}${fail}${disabledReason}${softLeft}${exhLeft}${remain}${rateLimits}`,
|
|
1370
|
+
);
|
|
1371
|
+
console.log(
|
|
1372
|
+
` ${C.dim(`addedAt=${formatDate(acc.addedAt)} created=${formatDate(acc.createdAt)} lastUsed=${formatDate(acc.lastUsedAt)} expiry=${formatDate(acc.expiryDate ?? acc.expiry)} token=${maskProviderToken(acc.accessToken ?? "")} refresh=${maskProviderToken(acc.refreshToken)}`)}`,
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
console.log("");
|
|
1376
|
+
break;
|
|
1377
|
+
}
|
|
1378
|
+
case "add": {
|
|
1379
|
+
// manual add via flags --email --refresh-token
|
|
1380
|
+
const emailIdx = args.findIndex((a) => a === "--email" || a.startsWith("--email="));
|
|
1381
|
+
const refreshIdx = args.findIndex(
|
|
1382
|
+
(a) =>
|
|
1383
|
+
a === "--refresh-token" ||
|
|
1384
|
+
a.startsWith("--refresh-token=") ||
|
|
1385
|
+
a === "--refresh" ||
|
|
1386
|
+
a.startsWith("--refresh="),
|
|
1387
|
+
);
|
|
1388
|
+
let email = "";
|
|
1389
|
+
let refresh = "";
|
|
1390
|
+
if (emailIdx >= 0) {
|
|
1391
|
+
const v = args[emailIdx]!;
|
|
1392
|
+
email = v.includes("=") ? v.split("=").slice(1).join("=") : (args[emailIdx + 1] ?? "");
|
|
1393
|
+
}
|
|
1394
|
+
if (refreshIdx >= 0) {
|
|
1395
|
+
const v = args[refreshIdx]!;
|
|
1396
|
+
refresh = v.includes("=") ? v.split("=").slice(1).join("=") : (args[refreshIdx + 1] ?? "");
|
|
1397
|
+
}
|
|
1398
|
+
if (!email || !refresh) {
|
|
1399
|
+
console.log(C.bold("Usage: accounts add --email <email> --refresh-token <token> [--project <id>]"));
|
|
1400
|
+
process.exit(1);
|
|
1401
|
+
}
|
|
1402
|
+
const projIdx = args.findIndex((a) => a === "--project" || a.startsWith("--project="));
|
|
1403
|
+
let project: string = FALLBACK_PROJECT_ID;
|
|
1404
|
+
if (projIdx >= 0) {
|
|
1405
|
+
const v = args[projIdx]!;
|
|
1406
|
+
project = v.includes("=") ? v.split("=").slice(1).join("=") : (args[projIdx + 1] ?? FALLBACK_PROJECT_ID);
|
|
1407
|
+
}
|
|
1408
|
+
const acc = mgr.addAccount({
|
|
1409
|
+
email,
|
|
1410
|
+
refreshToken: refresh,
|
|
1411
|
+
projectId: project,
|
|
1412
|
+
enabled: true,
|
|
1413
|
+
oauthClientKey: "gemini-cli",
|
|
1414
|
+
});
|
|
1415
|
+
logOk(`Account ${email} added. Total: ${mgr.listAccounts().length}`);
|
|
1416
|
+
console.log(C.dim(` ${JSON.stringify({ email: acc.email, projectId: acc.projectId, enabled: acc.enabled })}`));
|
|
1417
|
+
break;
|
|
1418
|
+
}
|
|
1419
|
+
case "remove":
|
|
1420
|
+
case "rm":
|
|
1421
|
+
case "delete":
|
|
1422
|
+
case "del": {
|
|
1423
|
+
const email = args[1];
|
|
1424
|
+
if (!email) {
|
|
1425
|
+
console.log("Usage: accounts remove <email>");
|
|
1426
|
+
process.exit(1);
|
|
1427
|
+
}
|
|
1428
|
+
const ok = mgr.removeAccount(email);
|
|
1429
|
+
if (ok) logOk(`Removed ${email}`);
|
|
1430
|
+
else logErr(`Not found ${email}`);
|
|
1431
|
+
break;
|
|
1432
|
+
}
|
|
1433
|
+
case "enable": {
|
|
1434
|
+
const email = args[1];
|
|
1435
|
+
if (!email) {
|
|
1436
|
+
console.log("Usage: accounts enable <email>");
|
|
1437
|
+
process.exit(1);
|
|
1438
|
+
}
|
|
1439
|
+
try {
|
|
1440
|
+
await enableAccountFlow(email);
|
|
1441
|
+
} catch (e) {
|
|
1442
|
+
logErr((e as Error).message);
|
|
1443
|
+
}
|
|
1444
|
+
break;
|
|
1445
|
+
}
|
|
1446
|
+
case "disable": {
|
|
1447
|
+
const email = args[1];
|
|
1448
|
+
if (!email) {
|
|
1449
|
+
console.log("Usage: accounts disable <email> [--reason <txt>]");
|
|
1450
|
+
process.exit(1);
|
|
1451
|
+
}
|
|
1452
|
+
const reasonIdx = args.indexOf("--reason");
|
|
1453
|
+
const reason = reasonIdx >= 0 ? args[reasonIdx + 1] : "manually disabled via CLI";
|
|
1454
|
+
try {
|
|
1455
|
+
await disableAccountFlow(email, reason ?? "manually disabled via CLI");
|
|
1456
|
+
} catch (e) {
|
|
1457
|
+
logErr((e as Error).message);
|
|
1458
|
+
}
|
|
1459
|
+
break;
|
|
1460
|
+
}
|
|
1461
|
+
case "set-active":
|
|
1462
|
+
case "active":
|
|
1463
|
+
case "use": {
|
|
1464
|
+
const email = args[1];
|
|
1465
|
+
if (!email) {
|
|
1466
|
+
console.log("Usage: accounts set-active <email> [--sticky]");
|
|
1467
|
+
process.exit(1);
|
|
1468
|
+
}
|
|
1469
|
+
const sticky = args.includes("--sticky");
|
|
1470
|
+
try {
|
|
1471
|
+
mgr.setActive(email, { sticky });
|
|
1472
|
+
logOk(`Active now: ${email} ${sticky ? "(sticky)" : "(round-robin)"}`);
|
|
1473
|
+
} catch (e) {
|
|
1474
|
+
logErr((e as Error).message);
|
|
1475
|
+
process.exit(1);
|
|
1476
|
+
}
|
|
1477
|
+
break;
|
|
1478
|
+
}
|
|
1479
|
+
case "import": {
|
|
1480
|
+
const importPathIdx = args.findIndex((a) => a === "--path" || a.startsWith("--path="));
|
|
1481
|
+
let customPath: string | undefined;
|
|
1482
|
+
if (importPathIdx >= 0) {
|
|
1483
|
+
const v = args[importPathIdx]!;
|
|
1484
|
+
customPath = v.includes("=") ? v.split("=").slice(1).join("=") : args[importPathIdx + 1];
|
|
1485
|
+
}
|
|
1486
|
+
try {
|
|
1487
|
+
if (typeof (mgr as any).importFromAntigravityManager === "function") {
|
|
1488
|
+
const res = await (mgr as any).importFromAntigravityManager(customPath ? [customPath] : undefined);
|
|
1489
|
+
console.log(
|
|
1490
|
+
C.bold(
|
|
1491
|
+
`\nImport result: imported=${(res as any).imported} updated=${(res as any).updated} skipped=${(res as any).skipped}`,
|
|
1492
|
+
),
|
|
1493
|
+
);
|
|
1494
|
+
if ((res as any).errors?.length) console.log(C.yellow(`Errors: ${(res as any).errors.join("; ")}`));
|
|
1495
|
+
if ((res as any).sources?.length) console.log(C.dim(`Sources: ${(res as any).sources.join(", ")}`));
|
|
1496
|
+
} else if (typeof (mgr as any).importFromAntigravity === "function") {
|
|
1497
|
+
const res = await (mgr as any).importFromAntigravity();
|
|
1498
|
+
console.log(JSON.stringify(res, null, 2));
|
|
1499
|
+
} else {
|
|
1500
|
+
logWarn(
|
|
1501
|
+
"importFromAntigravityManager is not available in this AccountManager version. Trying manual import...",
|
|
1502
|
+
);
|
|
1503
|
+
// manual fallback: scan known paths
|
|
1504
|
+
const candidates = customPath
|
|
1505
|
+
? [customPath]
|
|
1506
|
+
: [
|
|
1507
|
+
path.join(os.homedir(), ".config", "antigravity", "accounts.json"),
|
|
1508
|
+
path.join(os.homedir(), ".antigravity", "accounts.json"),
|
|
1509
|
+
path.join(os.homedir(), ".config", "opencode", "antigravity-manager.json"),
|
|
1510
|
+
];
|
|
1511
|
+
let count = 0;
|
|
1512
|
+
for (const p of candidates) {
|
|
1513
|
+
if (!fs.existsSync(p)) continue;
|
|
1514
|
+
try {
|
|
1515
|
+
const data = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1516
|
+
const arr = Array.isArray(data) ? data : (data.accounts ?? []);
|
|
1517
|
+
for (const raw of arr) {
|
|
1518
|
+
const email = (raw.email ?? raw.user_email)?.toLowerCase();
|
|
1519
|
+
const rt = raw.refreshToken ?? raw.refresh_token;
|
|
1520
|
+
if (email && rt) {
|
|
1521
|
+
mgr.addAccount({ email, refreshToken: rt, projectId: raw.projectId ?? FALLBACK_PROJECT_ID });
|
|
1522
|
+
count++;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
logOk(`Imported ${arr.length} from ${p}`);
|
|
1526
|
+
} catch (e) {
|
|
1527
|
+
logWarn(`Failed ${p}: ${(e as Error).message}`);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
console.log(C.dim(`Manual import total: ${count}`));
|
|
1531
|
+
}
|
|
1532
|
+
} catch (e) {
|
|
1533
|
+
logErr(`Import failed: ${(e as Error).message}`);
|
|
1534
|
+
}
|
|
1535
|
+
break;
|
|
1536
|
+
}
|
|
1537
|
+
default:
|
|
1538
|
+
console.log(C.bold("\naccounts subcommands:"));
|
|
1539
|
+
console.log(" list [--json] List accounts");
|
|
1540
|
+
console.log(" add --email --refresh-token Add manually");
|
|
1541
|
+
console.log(" remove <email> Remove account");
|
|
1542
|
+
console.log(" enable <email> Enable account");
|
|
1543
|
+
console.log(" disable <email> Disable account");
|
|
1544
|
+
console.log(" set-active <email> [--sticky] Set active + strategy");
|
|
1545
|
+
console.log(" import [--path <file>] Import from antigravity-manager");
|
|
1546
|
+
break;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function formatQuotaBar(remaining: number, limit: number): string {
|
|
1551
|
+
const cfg = loadAntigravityConfig();
|
|
1552
|
+
const softPct = cfg.soft_quota_threshold_percent ?? 90;
|
|
1553
|
+
const used = limit - remaining;
|
|
1554
|
+
const pctUsed = limit > 0 ? (used / limit) * 100 : 0;
|
|
1555
|
+
const barLen = 20;
|
|
1556
|
+
const filled = Math.round((pctUsed / 100) * barLen);
|
|
1557
|
+
const bar = "#".repeat(filled) + ".".repeat(barLen - filled);
|
|
1558
|
+
let col = C.green;
|
|
1559
|
+
if (pctUsed >= softPct) col = C.yellow;
|
|
1560
|
+
if (pctUsed >= 98) col = C.red;
|
|
1561
|
+
return `${col(bar)} ${pctUsed.toFixed(1)}% used (${remaining}/${limit} remaining)`;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
async function cmdQuota(args: string[]): Promise<void> {
|
|
1565
|
+
const jsonFlag = args.includes("--json");
|
|
1566
|
+
const allFlag = args.includes("--all");
|
|
1567
|
+
let emailArg: string | undefined;
|
|
1568
|
+
// positional email unless flag
|
|
1569
|
+
for (const a of args) {
|
|
1570
|
+
if (!a.startsWith("-") && a.includes("@")) {
|
|
1571
|
+
emailArg = a;
|
|
1572
|
+
break;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
const emailIdx = args.findIndex((a) => a === "--email" || a.startsWith("--email="));
|
|
1576
|
+
if (emailIdx >= 0) {
|
|
1577
|
+
const v = args[emailIdx]!;
|
|
1578
|
+
emailArg = v.includes("=") ? v.split("=").slice(1).join("=") : args[emailIdx + 1];
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
const mgr = getDefaultAccountManager({ disableBackgroundTimer: true });
|
|
1582
|
+
const stores = mgr.listAccounts?.() ?? [];
|
|
1583
|
+
if (stores.length === 0) {
|
|
1584
|
+
logWarn("No accounts. Run login first.");
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
const targets = allFlag ? stores.map((s: any) => s.email) : [emailArg ?? stores[0]?.email];
|
|
1589
|
+
const cfg = loadAntigravityConfig();
|
|
1590
|
+
const softThresh = (cfg.soft_quota_threshold_percent ?? 90) / 100;
|
|
1591
|
+
|
|
1592
|
+
console.log(
|
|
1593
|
+
C.bold(
|
|
1594
|
+
`\nQuota Check - soft threshold ${cfg.soft_quota_threshold_percent}% | refresh ${cfg.quota_refresh_interval_minutes}min`,
|
|
1595
|
+
),
|
|
1596
|
+
);
|
|
1597
|
+
console.log(C.dim(`Bypass: GeminiCLI/0.57.0 gl-node/22.19.0 Client-Metadata=IDE_UNSPECIFIED`));
|
|
1598
|
+
console.log("");
|
|
1599
|
+
|
|
1600
|
+
let qManagerInst: any = null;
|
|
1601
|
+
try {
|
|
1602
|
+
if (QuotaManager)
|
|
1603
|
+
qManagerInst = new QuotaManager({ projectFallback: FALLBACK_PROJECT_ID, softThreshold: softThresh });
|
|
1604
|
+
else if (getDefaultQuotaManager) qManagerInst = getDefaultQuotaManager({ softThreshold: softThresh });
|
|
1605
|
+
} catch {
|
|
1606
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
for (const email of targets) {
|
|
1610
|
+
if (!email) continue;
|
|
1611
|
+
console.log(C.bold(`- ${email} -`));
|
|
1612
|
+
try {
|
|
1613
|
+
const token = await mgr.getValidAccessToken(email);
|
|
1614
|
+
const quotaResult = await fetchQuotaForAccount(email, mgr, qManagerInst);
|
|
1615
|
+
const raw = (quotaResult as any).raw ?? {};
|
|
1616
|
+
const groups = (quotaResult as any).groups ?? [];
|
|
1617
|
+
|
|
1618
|
+
// Persist results back to the accounts file (ported from cli(4).ts):
|
|
1619
|
+
// sum numeric bucket values, otherwise fall back to auth.js retrieveQuota.
|
|
1620
|
+
let persistedRemaining: number | undefined;
|
|
1621
|
+
let persistedLimit: number | undefined;
|
|
1622
|
+
const bucketArr: any[] = Array.isArray(raw?.quotaBuckets)
|
|
1623
|
+
? raw.quotaBuckets
|
|
1624
|
+
: Array.isArray(raw?.buckets)
|
|
1625
|
+
? raw.buckets
|
|
1626
|
+
: [];
|
|
1627
|
+
if (bucketArr.length > 0) {
|
|
1628
|
+
let rSum = 0;
|
|
1629
|
+
let lSum = 0;
|
|
1630
|
+
let numeric = true;
|
|
1631
|
+
for (const b of bucketArr) {
|
|
1632
|
+
const r = Number(b?.remaining ?? b?.remainingQuota);
|
|
1633
|
+
const l = Number(b?.limit ?? b?.quotaLimit ?? b?.max);
|
|
1634
|
+
if (!Number.isFinite(r) || !Number.isFinite(l)) {
|
|
1635
|
+
numeric = false;
|
|
1636
|
+
break;
|
|
1637
|
+
}
|
|
1638
|
+
rSum += r;
|
|
1639
|
+
lSum += l;
|
|
1640
|
+
}
|
|
1641
|
+
if (numeric) {
|
|
1642
|
+
persistedRemaining = rSum;
|
|
1643
|
+
persistedLimit = lSum;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
if (
|
|
1647
|
+
(persistedRemaining === undefined || persistedLimit === undefined) &&
|
|
1648
|
+
typeof authRetrieveQuota === "function"
|
|
1649
|
+
) {
|
|
1650
|
+
try {
|
|
1651
|
+
const qq = await authRetrieveQuota(token, (quotaResult as any).projectId ?? FALLBACK_PROJECT_ID);
|
|
1652
|
+
if (qq) {
|
|
1653
|
+
persistedRemaining = Number(qq.remaining);
|
|
1654
|
+
persistedLimit = Number(qq.limit);
|
|
1655
|
+
}
|
|
1656
|
+
} catch {
|
|
1657
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
if (persistedRemaining !== undefined || persistedLimit !== undefined) {
|
|
1661
|
+
await persistQuotaResultToAccountsFile(email, { remaining: persistedRemaining, limit: persistedLimit });
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
if (jsonFlag) {
|
|
1665
|
+
console.log(
|
|
1666
|
+
JSON.stringify(
|
|
1667
|
+
{
|
|
1668
|
+
email,
|
|
1669
|
+
raw,
|
|
1670
|
+
groups,
|
|
1671
|
+
endpoint: (quotaResult as any).endpoint,
|
|
1672
|
+
projectId: (quotaResult as any).projectId,
|
|
1673
|
+
remaining: persistedRemaining,
|
|
1674
|
+
limit: persistedLimit,
|
|
1675
|
+
},
|
|
1676
|
+
null,
|
|
1677
|
+
2,
|
|
1678
|
+
),
|
|
1679
|
+
);
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// Parse raw if coming from API
|
|
1684
|
+
if (raw && typeof raw === "object") {
|
|
1685
|
+
// raw may have structure with quotaBuckets or similar
|
|
1686
|
+
const buckets = raw.quotaBuckets ?? raw.buckets ?? raw.quota ?? [];
|
|
1687
|
+
if (Array.isArray(buckets) && buckets.length) {
|
|
1688
|
+
for (const b of buckets) {
|
|
1689
|
+
const name = b.displayName ?? b.group ?? b.name ?? b.id ?? "unknown";
|
|
1690
|
+
const remaining = b.remaining ?? b.remainingQuota ?? 0;
|
|
1691
|
+
const limit = b.limit ?? b.quotaLimit ?? b.max ?? remaining + (b.used ?? 0);
|
|
1692
|
+
const reset = b.resetTime ?? b.reset_at ?? b.resetAt;
|
|
1693
|
+
console.log(
|
|
1694
|
+
` ${C.cyan(name)}: ${formatQuotaBar(remaining, limit)} ${reset ? C.dim(`reset=${formatDate(new Date(reset).getTime())}`) : ""}`,
|
|
1695
|
+
);
|
|
1696
|
+
if (b.models?.length)
|
|
1697
|
+
console.log(
|
|
1698
|
+
` ${C.dim(`models: ${b.models.slice(0, 5).join(", ")}${b.models.length > 5 ? "..." : ""}`)}`,
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
} else if (Object.keys(raw).length) {
|
|
1702
|
+
// pretty print all top-level keys
|
|
1703
|
+
console.log(C.dim(` raw keys: ${Object.keys(raw).join(", ")}`));
|
|
1704
|
+
// try to interpret known fields
|
|
1705
|
+
for (const k of Object.keys(raw)) {
|
|
1706
|
+
const v = (raw as any)[k];
|
|
1707
|
+
if (v && typeof v === "object" && "remaining" in v) {
|
|
1708
|
+
console.log(` ${C.cyan(k)}: ${formatQuotaBar((v as any).remaining ?? 0, (v as any).limit ?? 1000)}`);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
if (Array.isArray((raw as any).quotaGroups)) {
|
|
1712
|
+
for (const g of (raw as any).quotaGroups) {
|
|
1713
|
+
console.log(` ${C.cyan(g.displayName ?? g.id)} remaining=${g.remaining ?? "?"} limit=${g.limit ?? "?"}`);
|
|
1714
|
+
}
|
|
1715
|
+
} else {
|
|
1716
|
+
console.log(` ${C.dim(JSON.stringify(raw).slice(0, 800))}`);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
if (groups.length) {
|
|
1722
|
+
for (const g of groups) {
|
|
1723
|
+
console.log(
|
|
1724
|
+
` ${C.cyan(g.id ?? g.group)}: remaining=${(g as any).remaining ?? "?"} used=${(g as any).used ?? "?"} limit=${(g as any).limit ?? "?"}`,
|
|
1725
|
+
);
|
|
1726
|
+
if ((g as any).models) console.log(` ${C.dim(`models: ${(g as any).models.join(", ")}`)}`);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
if (!raw || (Object.keys(raw).length === 0 && (!groups || groups.length === 0))) {
|
|
1731
|
+
logWarn(" Empty quota - endpoint may have returned 1.0 (expected for gemini-cli), trying daily endpoint...");
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
if (persistedRemaining !== undefined && persistedLimit !== undefined) {
|
|
1735
|
+
console.log(
|
|
1736
|
+
` ${C.green("[saved]")} remaining=${persistedRemaining}/${persistedLimit} -> ${path.basename(resolveAccountsPath())}`,
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// Evaluate threshold warnings
|
|
1741
|
+
const thresholdPct = cfg.soft_quota_threshold_percent ?? 90;
|
|
1742
|
+
console.log(
|
|
1743
|
+
C.dim(
|
|
1744
|
+
` soft_threshold=${thresholdPct}% | project=${(quotaResult as any).projectId ?? FALLBACK_PROJECT_ID} | endpoint=${(quotaResult as any).endpoint ?? "quota_manager"}`,
|
|
1745
|
+
),
|
|
1746
|
+
);
|
|
1747
|
+
} catch (e) {
|
|
1748
|
+
logErr(` ${email}: ${(e as Error).message}`);
|
|
1749
|
+
if (jsonFlag) console.log(JSON.stringify({ email, error: (e as Error).message }, null, 2));
|
|
1750
|
+
}
|
|
1751
|
+
console.log("");
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
async function cmdConfig(args: string[]): Promise<void> {
|
|
1756
|
+
const sub = (args[0] ?? "list").toLowerCase();
|
|
1757
|
+
const cfg = loadAntigravityConfig();
|
|
1758
|
+
|
|
1759
|
+
switch (sub) {
|
|
1760
|
+
case "list":
|
|
1761
|
+
case "ls":
|
|
1762
|
+
case "show": {
|
|
1763
|
+
const jsonFlag = args.includes("--json");
|
|
1764
|
+
if (jsonFlag) {
|
|
1765
|
+
console.log(JSON.stringify(cfg, null, 2));
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
console.log(C.bold(`\nAntigravity Config - ${resolveConfigPath()} (chmod 600)`));
|
|
1769
|
+
console.log("");
|
|
1770
|
+
for (const [k, v] of Object.entries(cfg)) {
|
|
1771
|
+
console.log(
|
|
1772
|
+
` ${C.cyan(k)}: ${C.bold(String(v))} ${C.dim(`(default: ${String((DEFAULT_CONFIG as any)?.[k] ?? "-")})`)}`,
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1775
|
+
console.log("");
|
|
1776
|
+
console.log(C.dim("Tunable keys:"));
|
|
1777
|
+
console.log(
|
|
1778
|
+
C.dim(" cli_first (bool), toast_scope (all|project|minimal|none), soft_quota_threshold_percent (0-100)"),
|
|
1779
|
+
);
|
|
1780
|
+
console.log(C.dim(" quota_fallback (true|false|auto|<model>), quota_refresh_interval_minutes (1-1440)"));
|
|
1781
|
+
console.log(
|
|
1782
|
+
C.dim(
|
|
1783
|
+
" soft_quota_cache_ttl_minutes, pid_offset_enabled (bool), google_search_enabled (bool), debug (bool), version_cache_ttl",
|
|
1784
|
+
),
|
|
1785
|
+
);
|
|
1786
|
+
break;
|
|
1787
|
+
}
|
|
1788
|
+
case "get": {
|
|
1789
|
+
const key = args[1];
|
|
1790
|
+
if (!key) {
|
|
1791
|
+
console.log("Usage: config get <key>");
|
|
1792
|
+
process.exit(1);
|
|
1793
|
+
}
|
|
1794
|
+
const val = (cfg as any)[key];
|
|
1795
|
+
if (val === undefined) {
|
|
1796
|
+
logWarn(`Key ${key} not found`);
|
|
1797
|
+
process.exit(1);
|
|
1798
|
+
}
|
|
1799
|
+
console.log(`${key}=${JSON.stringify(val)}`);
|
|
1800
|
+
break;
|
|
1801
|
+
}
|
|
1802
|
+
case "set": {
|
|
1803
|
+
const key = args[1];
|
|
1804
|
+
const rawVal = args[2];
|
|
1805
|
+
if (!key || rawVal === undefined) {
|
|
1806
|
+
console.log(
|
|
1807
|
+
"Usage: config set <key> <value>\nExample: config set soft_quota_threshold_percent 85\n config set cli_first false",
|
|
1808
|
+
);
|
|
1809
|
+
process.exit(1);
|
|
1810
|
+
}
|
|
1811
|
+
// parse value smartly
|
|
1812
|
+
let parsed: any = rawVal;
|
|
1813
|
+
if (rawVal === "true") parsed = true;
|
|
1814
|
+
else if (rawVal === "false") parsed = false;
|
|
1815
|
+
else if (!Number.isNaN(Number(rawVal)) && rawVal.trim() !== "") {
|
|
1816
|
+
const n = Number(rawVal);
|
|
1817
|
+
if (Number.isFinite(n)) parsed = n;
|
|
1818
|
+
} else {
|
|
1819
|
+
// strip quotes
|
|
1820
|
+
parsed = rawVal.replace(/^['"]|['"]$/g, "");
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// Special handling: model catalog validation for quota_fallback when a non-auto/non-bool string
|
|
1824
|
+
if (key === "quota_fallback" && typeof parsed === "string" && !["true", "false", "auto"].includes(parsed)) {
|
|
1825
|
+
// allow model ids
|
|
1826
|
+
const isModel = ALL_MODELS_2026.some((m: any) => m.id === parsed);
|
|
1827
|
+
if (!isModel) {
|
|
1828
|
+
logWarn(
|
|
1829
|
+
`Value ${parsed} is not a known model. Allowed: ${ALL_MODELS_2026.map((m: any) => m.id).join(", ")} or auto/true/false`,
|
|
1830
|
+
);
|
|
1831
|
+
// continue anyway
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
const partial: any = { [key]: parsed };
|
|
1836
|
+
try {
|
|
1837
|
+
const updated = (() => {
|
|
1838
|
+
try {
|
|
1839
|
+
if (typeof updateConfig === "function") return updateConfig(partial);
|
|
1840
|
+
} catch {
|
|
1841
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
1842
|
+
}
|
|
1843
|
+
// manual merge atomic
|
|
1844
|
+
const current = loadAntigravityConfig();
|
|
1845
|
+
const mergedCfg = { ...current, ...partial };
|
|
1846
|
+
// validate
|
|
1847
|
+
let validated = mergedCfg;
|
|
1848
|
+
try {
|
|
1849
|
+
if (typeof parseAndValidateConfig === "function")
|
|
1850
|
+
validated = parseAndValidateConfig(mergedCfg, { strict: false });
|
|
1851
|
+
} catch {
|
|
1852
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
1853
|
+
}
|
|
1854
|
+
saveAntigravityConfigAtomic(validated);
|
|
1855
|
+
return validated;
|
|
1856
|
+
})();
|
|
1857
|
+
logOk(`Config ${C.bold(key)}=${C.bold(String(parsed))} saved.`);
|
|
1858
|
+
console.log(C.dim(JSON.stringify(updated, null, 2)));
|
|
1859
|
+
} catch (e) {
|
|
1860
|
+
logErr(`Failed to save config: ${(e as Error).message}`);
|
|
1861
|
+
process.exit(1);
|
|
1862
|
+
}
|
|
1863
|
+
break;
|
|
1864
|
+
}
|
|
1865
|
+
case "reset":
|
|
1866
|
+
case "defaults": {
|
|
1867
|
+
try {
|
|
1868
|
+
saveAntigravityConfigAtomic(DEFAULT_CONFIG ?? cfg);
|
|
1869
|
+
logOk("Config reset to defaults.");
|
|
1870
|
+
console.log(C.dim(JSON.stringify(DEFAULT_CONFIG, null, 2)));
|
|
1871
|
+
} catch (e) {
|
|
1872
|
+
logErr((e as Error).message);
|
|
1873
|
+
}
|
|
1874
|
+
break;
|
|
1875
|
+
}
|
|
1876
|
+
default:
|
|
1877
|
+
console.log(C.bold("config subcommands: list [--json], get <key>, set <key> <value>, reset"));
|
|
1878
|
+
console.log(C.dim("Examples:"));
|
|
1879
|
+
console.log(C.dim(" config set soft_quota_threshold_percent 85"));
|
|
1880
|
+
console.log(C.dim(" config set quota_refresh_interval_minutes 30"));
|
|
1881
|
+
console.log(C.dim(" config set toast_scope minimal"));
|
|
1882
|
+
break;
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
async function cmdModels(args: string[]): Promise<void> {
|
|
1887
|
+
const sub = (args[0] ?? "list").toLowerCase();
|
|
1888
|
+
const jsonFlag = args.includes("--json");
|
|
1889
|
+
const apiFilter = (() => {
|
|
1890
|
+
const idx = args.indexOf("--api");
|
|
1891
|
+
if (idx >= 0) return args[idx + 1];
|
|
1892
|
+
const apiEq = args.find((a) => a.startsWith("--api="));
|
|
1893
|
+
if (apiEq) return apiEq.split("=")[1];
|
|
1894
|
+
return undefined;
|
|
1895
|
+
})();
|
|
1896
|
+
|
|
1897
|
+
if (sub === "info" || sub === "show") {
|
|
1898
|
+
const id = args[1];
|
|
1899
|
+
if (!id) {
|
|
1900
|
+
console.log("Usage: models info <model-id>");
|
|
1901
|
+
process.exit(1);
|
|
1902
|
+
}
|
|
1903
|
+
const model = (ALL_MODELS_2026 as any[]).find((m: any) => m.id === id);
|
|
1904
|
+
if (!model) {
|
|
1905
|
+
logErr(`Model ${id} not found. Use models list`);
|
|
1906
|
+
process.exit(1);
|
|
1907
|
+
}
|
|
1908
|
+
if (jsonFlag) {
|
|
1909
|
+
console.log(JSON.stringify(model, null, 2));
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
console.log(C.bold(`\n${model.name} (${model.id})`));
|
|
1913
|
+
console.log(` ${C.cyan("API")}: ${model.api} | ${C.cyan("Provider")}: ${model.provider ?? "google"}`);
|
|
1914
|
+
console.log(` ${C.cyan("Context")}: ${model.context} | Output: ${model.output}`);
|
|
1915
|
+
console.log(` ${C.cyan("Family")}: ${model.family} | QuotaGroup: ${model.quotaGroup}`);
|
|
1916
|
+
if (model.preview) console.log(` ${C.yellow("preview=true")}`);
|
|
1917
|
+
if (model.thinking) console.log(` ${C.magenta("thinking=true")}`);
|
|
1918
|
+
if (model.customTools) console.log(` ${C.blue("customTools=true")}`);
|
|
1919
|
+
// routing info if available
|
|
1920
|
+
try {
|
|
1921
|
+
// v2.1.15 Phase A: MODEL_ROUTING lives in models.ts (model identification
|
|
1922
|
+
// is owned by models.ts alone).
|
|
1923
|
+
const routing = (modelsMod.MODEL_ROUTING as any)?.[id] ?? (modelsPkg.MODEL_ROUTING as any)?.[id];
|
|
1924
|
+
if (routing) {
|
|
1925
|
+
console.log(
|
|
1926
|
+
` ${C.cyan("Routing")}: endpoint=${routing.endpoint} cloudModelId=${routing.cloudModelId} stream=${routing.stream}`,
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
} catch {
|
|
1930
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
1931
|
+
}
|
|
1932
|
+
console.log("");
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
// list
|
|
1937
|
+
let list = ALL_MODELS_2026 as any[];
|
|
1938
|
+
if (apiFilter) list = list.filter((m) => m.api === apiFilter);
|
|
1939
|
+
if (jsonFlag) {
|
|
1940
|
+
console.log(JSON.stringify(list, null, 2));
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
console.log(C.bold(`\nCatalog ALL_MODELS_2026 (${list.length} models)`));
|
|
1945
|
+
console.log(C.dim(`Fallback project: ${FALLBACK_PROJECT_ID} | Client: GeminiCLI/0.57.0`));
|
|
1946
|
+
console.log("");
|
|
1947
|
+
console.log(C.dim(` ${"ID".padEnd(38)} ${"API".padEnd(14)} ${"CTX".padEnd(8)} QUOTA-GROUP`));
|
|
1948
|
+
console.log(C.dim(" " + "-".repeat(80)));
|
|
1949
|
+
for (const m of list) {
|
|
1950
|
+
const idPad = String(m.id).padEnd(38);
|
|
1951
|
+
const apiPad = String(m.api ?? "").padEnd(14);
|
|
1952
|
+
const ctx = String(m.context).padEnd(8);
|
|
1953
|
+
const color = m.api === "antigravity" ? C.cyan : C.green;
|
|
1954
|
+
const preview = m.preview ? C.yellow(" preview") : "";
|
|
1955
|
+
const think = m.thinking ? C.magenta(" thinking") : "";
|
|
1956
|
+
console.log(` ${color(idPad)} ${apiPad} ${ctx} ${m.quotaGroup}${preview}${think}`);
|
|
1957
|
+
}
|
|
1958
|
+
console.log("");
|
|
1959
|
+
console.log(C.dim("Use: models info <id> for details"));
|
|
1960
|
+
console.log(C.dim("Filter: --api antigravity | --api gemini-cli"));
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
async function cmdStatus(_args: string[]): Promise<void> {
|
|
1964
|
+
const mgr = getDefaultAccountManager({ disableBackgroundTimer: true });
|
|
1965
|
+
const cfg = loadAntigravityConfig();
|
|
1966
|
+
const accounts = mgr.listAccounts?.() ?? [];
|
|
1967
|
+
const active = (() => {
|
|
1968
|
+
try {
|
|
1969
|
+
return mgr.getActiveAccount?.();
|
|
1970
|
+
} catch {
|
|
1971
|
+
return null;
|
|
1972
|
+
}
|
|
1973
|
+
})();
|
|
1974
|
+
console.log(C.bold("\nAntigravity Status"));
|
|
1975
|
+
console.log(
|
|
1976
|
+
C.dim(`BaseDir: ${resolveBaseDir()} | AccountsFile: ${resolveAccountsPath()} | ConfigFile: ${resolveConfigPath()}`),
|
|
1977
|
+
);
|
|
1978
|
+
console.log("");
|
|
1979
|
+
console.log(
|
|
1980
|
+
` ${C.cyan("Accounts")}: ${accounts.length} total, ${accounts.filter((a: any) => a.enabled !== false).length} enabled`,
|
|
1981
|
+
);
|
|
1982
|
+
if (active)
|
|
1983
|
+
console.log(` ${C.cyan("Active")}: ${C.bold(active.email)} (project=${active.projectId ?? FALLBACK_PROJECT_ID})`);
|
|
1984
|
+
else console.log(` ${C.cyan("Active")}: ${C.yellow("none")}`);
|
|
1985
|
+
console.log(
|
|
1986
|
+
` ${C.cyan("Config")}: cli_first=${cfg.cli_first} toast_scope=${cfg.toast_scope} soft_quota=${cfg.soft_quota_threshold_percent}% refresh=${cfg.quota_refresh_interval_minutes}min fallback=${cfg.quota_fallback}`,
|
|
1987
|
+
);
|
|
1988
|
+
// file perms
|
|
1989
|
+
for (const p of [resolveAccountsPath(), resolveConfigPath()]) {
|
|
1990
|
+
try {
|
|
1991
|
+
if (!fs.existsSync(p)) {
|
|
1992
|
+
console.log(` ${C.dim(p)}: ${C.yellow("does not exist")}`);
|
|
1993
|
+
continue;
|
|
1994
|
+
}
|
|
1995
|
+
const stat = fs.statSync(p);
|
|
1996
|
+
const mode = (stat.mode & 0o777).toString(8);
|
|
1997
|
+
const ok = mode === "600" || mode === "700";
|
|
1998
|
+
console.log(
|
|
1999
|
+
` ${C.dim(p)}: mode=${ok ? C.green(mode) : C.red(mode)} size=${stat.size}b mtime=${formatDate(stat.mtimeMs)}`,
|
|
2000
|
+
);
|
|
2001
|
+
} catch (e) {
|
|
2002
|
+
console.log(` ${C.dim(p)}: ${C.red((e as Error).message)}`);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
// version cache
|
|
2006
|
+
try {
|
|
2007
|
+
const vInfo = await getVersionInfo();
|
|
2008
|
+
console.log(` ${C.cyan("Antigravity Version")}: ${vInfo.version} (source=${vInfo.source})`);
|
|
2009
|
+
} catch {
|
|
2010
|
+
console.log(` ${C.cyan("Antigravity Version")}: ${C.yellow("failed to fetch - using fallback")}`);
|
|
2011
|
+
}
|
|
2012
|
+
console.log("");
|
|
2013
|
+
// quick quota summary for active
|
|
2014
|
+
if (active) {
|
|
2015
|
+
try {
|
|
2016
|
+
console.log(C.dim("Checking quick quota for the active account..."));
|
|
2017
|
+
const q = await fetchQuotaForAccount(active.email, mgr, null);
|
|
2018
|
+
const buckets = (q.raw?.quotaBuckets ?? q.raw?.buckets ?? []) as any[];
|
|
2019
|
+
if (buckets.length) {
|
|
2020
|
+
for (const b of buckets.slice(0, 5)) {
|
|
2021
|
+
const name = b.displayName ?? b.group ?? "quota";
|
|
2022
|
+
console.log(` ${C.cyan(name)} remaining=${b.remaining ?? "?"} limit=${b.limit ?? "?"}`);
|
|
2023
|
+
}
|
|
2024
|
+
} else {
|
|
2025
|
+
console.log(C.dim(" Raw quota available, but without detailed buckets (daily endpoint vs base)"));
|
|
2026
|
+
}
|
|
2027
|
+
} catch (e) {
|
|
2028
|
+
logWarn(` Quota check failed: ${(e as Error).message}`);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
console.log("");
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
async function cmdDoctor(_args: string[]): Promise<void> {
|
|
2035
|
+
console.log(C.bold("\nDoctor - Antigravity Diagnostics"));
|
|
2036
|
+
console.log("");
|
|
2037
|
+
const checks: Array<{ name: string; ok: boolean; msg: string }> = [];
|
|
2038
|
+
// dir
|
|
2039
|
+
try {
|
|
2040
|
+
const base = resolveBaseDir();
|
|
2041
|
+
ensureDirExists(base);
|
|
2042
|
+
const stat = fs.statSync(base);
|
|
2043
|
+
checks.push({ name: "baseDir exists", ok: true, msg: `${base} mode=${(stat.mode & 0o777).toString(8)}` });
|
|
2044
|
+
} catch (e) {
|
|
2045
|
+
checks.push({ name: "baseDir exists", ok: false, msg: (e as Error).message });
|
|
2046
|
+
}
|
|
2047
|
+
// accounts file
|
|
2048
|
+
try {
|
|
2049
|
+
const p = resolveAccountsPath();
|
|
2050
|
+
if (fs.existsSync(p)) {
|
|
2051
|
+
const st = fs.statSync(p);
|
|
2052
|
+
const mode = (st.mode & 0o777).toString(8);
|
|
2053
|
+
checks.push({
|
|
2054
|
+
name: "accounts file",
|
|
2055
|
+
ok: mode === "600",
|
|
2056
|
+
msg: `${p} mode=${mode} ${mode !== "600" ? "(must be 600)" : ""}`,
|
|
2057
|
+
});
|
|
2058
|
+
// try to chmod fix
|
|
2059
|
+
if (mode !== "600") {
|
|
2060
|
+
try {
|
|
2061
|
+
fs.chmodSync(p, 0o600);
|
|
2062
|
+
checks.push({ name: "chmod fix accounts", ok: true, msg: "fixed to 600" });
|
|
2063
|
+
} catch {
|
|
2064
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
const data = readJsonSafe<any>(p, { accounts: [] });
|
|
2068
|
+
checks.push({ name: "accounts valid JSON", ok: true, msg: `${data.accounts?.length ?? 0} accounts` });
|
|
2069
|
+
// check tokens expiring
|
|
2070
|
+
const now = Date.now();
|
|
2071
|
+
const expiring = (data.accounts ?? []).filter((a: any) => a.expiryDate && a.expiryDate < now + 600000);
|
|
2072
|
+
checks.push({
|
|
2073
|
+
name: "token expiry",
|
|
2074
|
+
ok: expiring.length === 0,
|
|
2075
|
+
msg: expiring.length ? `${expiring.length} token(s) expiring <10min` : "all valid",
|
|
2076
|
+
});
|
|
2077
|
+
} else {
|
|
2078
|
+
checks.push({ name: "accounts file", ok: false, msg: `${p} does not exist - run login` });
|
|
2079
|
+
}
|
|
2080
|
+
} catch (e) {
|
|
2081
|
+
checks.push({ name: "accounts file", ok: false, msg: (e as Error).message });
|
|
2082
|
+
}
|
|
2083
|
+
// config file
|
|
2084
|
+
try {
|
|
2085
|
+
const p = resolveConfigPath();
|
|
2086
|
+
if (fs.existsSync(p)) {
|
|
2087
|
+
const st = fs.statSync(p);
|
|
2088
|
+
checks.push({ name: "config file", ok: true, msg: `${p} mode=${(st.mode & 0o777).toString(8)}` });
|
|
2089
|
+
const cfgRaw = readJsonSafe<any>(p, {});
|
|
2090
|
+
checks.push({
|
|
2091
|
+
name: "config valid",
|
|
2092
|
+
ok: Object.keys(cfgRaw).length > 0,
|
|
2093
|
+
msg: `${Object.keys(cfgRaw).length} keys`,
|
|
2094
|
+
});
|
|
2095
|
+
} else {
|
|
2096
|
+
checks.push({ name: "config file", ok: false, msg: `${p} does not exist - defaults will be created` });
|
|
2097
|
+
saveAntigravityConfigAtomic(DEFAULT_CONFIG ?? { cli_first: true });
|
|
2098
|
+
checks.push({ name: "config created", ok: true, msg: "defaults created" });
|
|
2099
|
+
}
|
|
2100
|
+
} catch (e) {
|
|
2101
|
+
checks.push({ name: "config file", ok: false, msg: (e as Error).message });
|
|
2102
|
+
}
|
|
2103
|
+
// bypass headers constants
|
|
2104
|
+
checks.push({
|
|
2105
|
+
name: "bypass constants",
|
|
2106
|
+
ok: GEMINI_OAUTH_CLIENT_ID.includes("681255809395"),
|
|
2107
|
+
msg: `client_id=${GEMINI_OAUTH_CLIENT_ID.includes("681255809395") ? "gemini-cli-default" : "custom"} UA=GeminiCLI/0.57.0 X-Goog=gl-node/22.19.0`,
|
|
2108
|
+
});
|
|
2109
|
+
// fetch project?
|
|
2110
|
+
checks.push({ name: "fallback project", ok: !!FALLBACK_PROJECT_ID, msg: FALLBACK_PROJECT_ID });
|
|
2111
|
+
|
|
2112
|
+
// Logs dir reporting (ported from cli(5).ts diagnostics)
|
|
2113
|
+
try {
|
|
2114
|
+
const ld = resolveLogsDir();
|
|
2115
|
+
checks.push({
|
|
2116
|
+
name: "logs dir",
|
|
2117
|
+
ok: fs.existsSync(ld),
|
|
2118
|
+
msg: `${ld}${fs.existsSync(ld) ? "" : " (not created yet)"}`,
|
|
2119
|
+
});
|
|
2120
|
+
} catch (e) {
|
|
2121
|
+
checks.push({ name: "logs dir", ok: false, msg: (e as Error).message });
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// Default model reporting (ported from cli(5).ts diagnostics)
|
|
2125
|
+
try {
|
|
2126
|
+
const def = FLAT_MODELS_2026.find((m: any) => m.id === DEFAULT_MODEL_ID);
|
|
2127
|
+
checks.push({
|
|
2128
|
+
name: "default model",
|
|
2129
|
+
ok: Boolean(def),
|
|
2130
|
+
msg: def ? `${def.id} (${def.name}) release=${def.release ?? "?"}` : DEFAULT_MODEL_ID,
|
|
2131
|
+
});
|
|
2132
|
+
} catch (e) {
|
|
2133
|
+
checks.push({ name: "default model", ok: false, msg: (e as Error).message });
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
// opencode.json location (via the ported locateOpenCodeJson resolver)
|
|
2137
|
+
try {
|
|
2138
|
+
const loc = await locateOpenCodeJson();
|
|
2139
|
+
checks.push({
|
|
2140
|
+
name: "opencode.json",
|
|
2141
|
+
ok: loc.exists,
|
|
2142
|
+
msg: `${loc.path}${loc.exists ? ` (found via ${loc.source})` : ` (missing, would create via ${loc.source})`}`,
|
|
2143
|
+
});
|
|
2144
|
+
} catch (e) {
|
|
2145
|
+
checks.push({ name: "opencode.json", ok: false, msg: (e as Error).message });
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
for (const ch of checks) {
|
|
2149
|
+
console.log(` ${ch.ok ? C.green("[ok]") : C.red("[x]")} ${C.bold(ch.name)}: ${ch.msg}`);
|
|
2150
|
+
}
|
|
2151
|
+
console.log("");
|
|
2152
|
+
// Raw accounts file dump - first 1000 chars (ported from cli(4).ts diagnostics)
|
|
2153
|
+
try {
|
|
2154
|
+
const ap = resolveAccountsPath();
|
|
2155
|
+
if (fs.existsSync(ap)) {
|
|
2156
|
+
const rawAcc = fs.readFileSync(ap, "utf8");
|
|
2157
|
+
console.log(C.dim("Accounts file raw (first 1000 chars):"));
|
|
2158
|
+
console.log(C.dim(rawAcc.slice(0, 1000)));
|
|
2159
|
+
console.log("");
|
|
2160
|
+
}
|
|
2161
|
+
} catch {
|
|
2162
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
2163
|
+
}
|
|
2164
|
+
// suggestions
|
|
2165
|
+
console.log(C.dim("Suggestions:"));
|
|
2166
|
+
console.log(C.dim(" - If a token is expiring: AccountManager auto-refreshes on use, or run `login` again"));
|
|
2167
|
+
console.log(C.dim(" - If mode != 600: the CLI fixes permissions automatically on the next write"));
|
|
2168
|
+
console.log(C.dim(" - To force bypass headers: always use gl-node/22.19.0 and Client-Metadata IDE_UNSPECIFIED"));
|
|
2169
|
+
console.log("");
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
async function cmdVersion(_args: string[]): Promise<void> {
|
|
2173
|
+
const jsonFlag = _args.includes("--json");
|
|
2174
|
+
try {
|
|
2175
|
+
const info = await getVersionInfo();
|
|
2176
|
+
if (jsonFlag) {
|
|
2177
|
+
console.log(
|
|
2178
|
+
JSON.stringify(
|
|
2179
|
+
{
|
|
2180
|
+
plugin: PLUGIN_VERSION_FALLBACK,
|
|
2181
|
+
antigravity: info.version,
|
|
2182
|
+
full: info,
|
|
2183
|
+
client_id: GEMINI_OAUTH_CLIENT_ID.includes("681255809395") ? "gemini-cli-default" : "custom",
|
|
2184
|
+
project: FALLBACK_PROJECT_ID,
|
|
2185
|
+
models: ALL_MODELS_2026.length,
|
|
2186
|
+
},
|
|
2187
|
+
null,
|
|
2188
|
+
2,
|
|
2189
|
+
),
|
|
2190
|
+
);
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
console.log(C.bold("\nVersions"));
|
|
2194
|
+
console.log(` Plugin devthink: ${C.bold(PLUGIN_VERSION_FALLBACK)}`);
|
|
2195
|
+
console.log(
|
|
2196
|
+
` Antigravity: ${C.bold(info.version)} full=${(info as any).fullVersion ?? "-"} source=${info.source}`,
|
|
2197
|
+
);
|
|
2198
|
+
console.log(
|
|
2199
|
+
` ClientID: ${C.dim(GEMINI_OAUTH_CLIENT_ID.includes("681255809395") ? "gemini-cli-default (redacted)" : "custom (redacted)")} (Gemini CLI 0.57.0)`,
|
|
2200
|
+
);
|
|
2201
|
+
console.log(` Project fallback: ${FALLBACK_PROJECT_ID}`);
|
|
2202
|
+
console.log(` Models catalog: ${ALL_MODELS_2026.length} (antigravity-first)`);
|
|
2203
|
+
console.log(` BaseDir: ${resolveBaseDir()}`);
|
|
2204
|
+
} catch (e) {
|
|
2205
|
+
console.log(C.bold(`\nFallback version: ${PLUGIN_VERSION_FALLBACK}`));
|
|
2206
|
+
console.log(C.dim(`Error fetching remote version: ${(e as Error).message}`));
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
// ===========================================================================
|
|
2211
|
+
// 05B. LOGOUT / ENABLE / DISABLE / MANAGE (ported from cli(2).ts + cli(5).ts)
|
|
2212
|
+
// ===========================================================================
|
|
2213
|
+
|
|
2214
|
+
/**
|
|
2215
|
+
* Best-effort OAuth token revocation against Google's revoke endpoint.
|
|
2216
|
+
* A 400 response means the token was already invalid/revoked - counted as OK.
|
|
2217
|
+
*/
|
|
2218
|
+
async function revokeGoogleToken(token?: string | null): Promise<boolean> {
|
|
2219
|
+
if (!token) return false;
|
|
2220
|
+
try {
|
|
2221
|
+
// v2.1.16 single-owner fix: revoke endpoint from constants.js
|
|
2222
|
+
// (OAUTH_REVOKE_URL owner) instead of a local literal.
|
|
2223
|
+
const res = await fetch(OAUTH_REVOKE_URL, {
|
|
2224
|
+
method: "POST",
|
|
2225
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2226
|
+
body: new URLSearchParams({ token }).toString(),
|
|
2227
|
+
});
|
|
2228
|
+
return res.ok || res.status === 400;
|
|
2229
|
+
} catch {
|
|
2230
|
+
return false;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
/**
|
|
2235
|
+
* Logout flow (ported from cli(2).ts, extended with token revocation):
|
|
2236
|
+
* revokes the Google refresh/access token and removes the account from the
|
|
2237
|
+
* store. Supports a single email, --all, and an interactive picker when no
|
|
2238
|
+
* email is given. Pass {@link sharedRl} when called from the interactive menu.
|
|
2239
|
+
*/
|
|
2240
|
+
async function logoutFlow(emailArg?: string, all = false, sharedRl?: readline.Interface): Promise<void> {
|
|
2241
|
+
printHeader();
|
|
2242
|
+
const rl = sharedRl ?? readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2243
|
+
const ownedRl = !sharedRl;
|
|
2244
|
+
try {
|
|
2245
|
+
const mgr: any = getDefaultAccountManager({ disableBackgroundTimer: true });
|
|
2246
|
+
const accounts: any[] = mgr.listAccounts?.() ?? [];
|
|
2247
|
+
|
|
2248
|
+
if (accounts.length === 0) {
|
|
2249
|
+
logWarn("No accounts registered.");
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
if (all) {
|
|
2254
|
+
console.log(C.bold(`Logout --all: removing ${accounts.length} account(s)`));
|
|
2255
|
+
const okAll = await confirm(
|
|
2256
|
+
rl,
|
|
2257
|
+
C.red(`Confirm removal of ALL ${accounts.length} accounts (tokens will be revoked)?`),
|
|
2258
|
+
false,
|
|
2259
|
+
);
|
|
2260
|
+
if (!okAll) {
|
|
2261
|
+
console.log(C.dim("Cancelled."));
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
let removedCount = 0;
|
|
2265
|
+
for (const acc of accounts) {
|
|
2266
|
+
await revokeGoogleToken(acc.refreshToken ?? acc.refresh_token ?? acc.accessToken);
|
|
2267
|
+
try {
|
|
2268
|
+
await mgr.removeAccount(acc.email);
|
|
2269
|
+
removedCount++;
|
|
2270
|
+
console.log(C.dim(` removed ${acc.email}`));
|
|
2271
|
+
} catch (e) {
|
|
2272
|
+
console.log(C.red(` failed ${acc.email}: ${(e as Error).message}`));
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
logOk(`All accounts removed (${removedCount}). Store: ${resolveAccountsPath()}`);
|
|
2276
|
+
auditLog(`logout --all removed=${removedCount}`);
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
let targetEmail = emailArg?.toLowerCase().trim();
|
|
2281
|
+
|
|
2282
|
+
if (!targetEmail) {
|
|
2283
|
+
if (accounts.length === 1) {
|
|
2284
|
+
targetEmail = String(accounts[0]?.email ?? "").toLowerCase();
|
|
2285
|
+
} else {
|
|
2286
|
+
// interactive choose
|
|
2287
|
+
console.log(C.bold("Logout - choose an account to remove:\n"));
|
|
2288
|
+
accounts.forEach((acc, i) => {
|
|
2289
|
+
const mark = acc.disabled === true || acc.enabled === false ? C.red("[disabled]") : C.green("[active]");
|
|
2290
|
+
console.log(
|
|
2291
|
+
` ${C.cyan(String(i + 1))}. ${redactEmail(String(acc.email))} ${mark} project=${acc.projectId ?? FALLBACK_PROJECT_ID}`,
|
|
2292
|
+
);
|
|
2293
|
+
});
|
|
2294
|
+
const ans = await askQuestion(rl, `\nEnter number (1-${accounts.length}) or email: `);
|
|
2295
|
+
const num = Number(ans);
|
|
2296
|
+
if (Number.isFinite(num) && num >= 1 && num <= accounts.length) {
|
|
2297
|
+
targetEmail = String(accounts[num - 1]?.email ?? "").toLowerCase();
|
|
2298
|
+
} else if (ans.includes("@")) {
|
|
2299
|
+
targetEmail = ans.toLowerCase();
|
|
2300
|
+
} else {
|
|
2301
|
+
logWarn("Invalid input, cancelled.");
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
if (!targetEmail) {
|
|
2308
|
+
logWarn("No email provided, cancelled.");
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
const chosen: string = targetEmail;
|
|
2313
|
+
const existing = mgr.findAccount?.(chosen) ?? accounts.find((a) => String(a.email).toLowerCase() === chosen);
|
|
2314
|
+
if (!existing) {
|
|
2315
|
+
logErr(`Account not found: ${chosen}`);
|
|
2316
|
+
console.log(C.dim(`Existing accounts: ${accounts.map((a) => a.email).join(", ")}`));
|
|
2317
|
+
process.exitCode = 1;
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
const okOne = await confirm(
|
|
2322
|
+
rl,
|
|
2323
|
+
`Remove account ${C.bold(existing.email)} (its refresh token will be revoked)?`,
|
|
2324
|
+
false,
|
|
2325
|
+
);
|
|
2326
|
+
if (!okOne) {
|
|
2327
|
+
console.log(C.dim("Cancelled."));
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
const revoked = await revokeGoogleToken(existing.refreshToken ?? existing.refresh_token ?? existing.accessToken);
|
|
2332
|
+
console.log(
|
|
2333
|
+
revoked
|
|
2334
|
+
? C.dim(" token revoked via oauth2.googleapis.com/revoke")
|
|
2335
|
+
: C.dim(" token revocation skipped/failed (removing the account locally anyway)"),
|
|
2336
|
+
);
|
|
2337
|
+
|
|
2338
|
+
const removed = await mgr.removeAccount(existing.email);
|
|
2339
|
+
if (removed) {
|
|
2340
|
+
logOk(`Account removed: ${existing.email}`);
|
|
2341
|
+
} else {
|
|
2342
|
+
logErr(`Failed to remove: ${existing.email}`);
|
|
2343
|
+
process.exitCode = 1;
|
|
2344
|
+
}
|
|
2345
|
+
auditLog(`logout ${existing.email} revoked=${revoked}`);
|
|
2346
|
+
} finally {
|
|
2347
|
+
if (ownedRl) rl.close();
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
/** Top-level enable command (ported from cli(2).ts enableAccountFlow). */
|
|
2352
|
+
async function enableAccountFlow(email: string): Promise<void> {
|
|
2353
|
+
printHeader();
|
|
2354
|
+
const mgr: any = getDefaultAccountManager({ disableBackgroundTimer: true });
|
|
2355
|
+
try {
|
|
2356
|
+
if (typeof mgr.enableAccount === "function") mgr.enableAccount(email);
|
|
2357
|
+
else if (typeof mgr.enable === "function") await mgr.enable(email);
|
|
2358
|
+
else throw new Error("account manager does not support enable");
|
|
2359
|
+
logOk(`Account enabled: ${email}`);
|
|
2360
|
+
} catch (e) {
|
|
2361
|
+
logErr(`Account not found or enable failed: ${email} (${(e as Error).message})`);
|
|
2362
|
+
process.exitCode = 1;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
/** Top-level disable command (ported from cli(2).ts disableAccountFlow). */
|
|
2367
|
+
async function disableAccountFlow(email: string, reason = "manually disabled via CLI"): Promise<void> {
|
|
2368
|
+
printHeader();
|
|
2369
|
+
const mgr: any = getDefaultAccountManager({ disableBackgroundTimer: true });
|
|
2370
|
+
try {
|
|
2371
|
+
if (typeof mgr.disableAccount === "function") mgr.disableAccount(email, reason);
|
|
2372
|
+
else if (typeof mgr.disable === "function") await mgr.disable(email);
|
|
2373
|
+
else throw new Error("account manager does not support disable");
|
|
2374
|
+
logOk(`Account disabled: ${email} (skipped in rotation)`);
|
|
2375
|
+
} catch (e) {
|
|
2376
|
+
logErr(`Account not found or disable failed: ${email} (${(e as Error).message})`);
|
|
2377
|
+
process.exitCode = 1;
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
/**
|
|
2382
|
+
* Interactive per-account enable/disable toggle
|
|
2383
|
+
* (ported from cli(5).ts manage command, backed by auth.js accountManager).
|
|
2384
|
+
* Pass {@link sharedRl} when invoked from the interactive menu.
|
|
2385
|
+
*/
|
|
2386
|
+
async function cmdManage(_args: string[], sharedRl?: readline.Interface): Promise<void> {
|
|
2387
|
+
const rl = sharedRl ?? readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2388
|
+
const ownedRl = !sharedRl;
|
|
2389
|
+
try {
|
|
2390
|
+
await cmdAccounts(["list"]);
|
|
2391
|
+
const email = await askQuestion(rl, "Email to toggle enable/disable: ");
|
|
2392
|
+
if (!email) {
|
|
2393
|
+
logWarn("Cancelled.");
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
const enStr = await askQuestion(rl, "Enable true/false [true]: ");
|
|
2397
|
+
const en = enStr.trim().toLowerCase() !== "false";
|
|
2398
|
+
|
|
2399
|
+
let applied = false;
|
|
2400
|
+
// Preferred path: auth.js accountManager.enable(email, bool) on the v3 store
|
|
2401
|
+
if (typeof AuthAccountManager === "function") {
|
|
2402
|
+
try {
|
|
2403
|
+
const m = new AuthAccountManager();
|
|
2404
|
+
if (typeof m.load === "function") {
|
|
2405
|
+
try {
|
|
2406
|
+
await m.load();
|
|
2407
|
+
} catch {
|
|
2408
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
if (typeof m.enable === "function") {
|
|
2412
|
+
await m.enable(email, en);
|
|
2413
|
+
applied = true;
|
|
2414
|
+
}
|
|
2415
|
+
} catch (e) {
|
|
2416
|
+
logWarn(`auth.js manager toggle failed: ${(e as Error).message}`);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
if (!applied) {
|
|
2420
|
+
const mgr: any = getDefaultAccountManager({ disableBackgroundTimer: true });
|
|
2421
|
+
if (en) mgr.enableAccount(email);
|
|
2422
|
+
else mgr.disableAccount(email, "disabled via manage command");
|
|
2423
|
+
}
|
|
2424
|
+
auditLog(`manage ${en ? "enable" : "disable"} ${email}`);
|
|
2425
|
+
console.log(`${en ? "enabled" : "disabled"} ${email}`);
|
|
2426
|
+
} finally {
|
|
2427
|
+
if (ownedRl) rl.close();
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
// ===========================================================================
|
|
2432
|
+
// 05C. MODELS 2026 CATALOG + OPENCODE.JSON CONFIGURE (ported from cli(2)/(4)/(5))
|
|
2433
|
+
// (the model transforms of the ownership move: buildGoogleProviderModelConfigs
|
|
2434
|
+
// + buildOpenCodeModelDefinitions + detectTargetProvider moved to the models
|
|
2435
|
+
// owner — the imports at the top answer them)
|
|
2436
|
+
// ===========================================================================
|
|
2437
|
+
|
|
2438
|
+
const CLI_VERSION = "6.0.0" as const;
|
|
2439
|
+
|
|
2440
|
+
/**
|
|
2441
|
+
* Configure models flow (merge of cli(2).ts configureModelsFlow with the
|
|
2442
|
+
* cli(4)/cli(5) provider.google.models writer):
|
|
2443
|
+
* - locates opencode.json via locateOpenCodeJson (--global / --path aware);
|
|
2444
|
+
* - backs up an existing file with invalid JSON before overwriting;
|
|
2445
|
+
* - detects the TARGET provider by MODEL IDS ONLY (any user-chosen provider
|
|
2446
|
+
* name carrying antigravity-* / gemini-* models, e.g. "casas-bahia"),
|
|
2447
|
+
* falling back to "google" when none exists yet;
|
|
2448
|
+
* - ADD-ONLY merge: user model entries are NEVER overwritten (custom name /
|
|
2449
|
+
* limit / options / thinking variants are preserved verbatim), catalog
|
|
2450
|
+
* models are appended only when the id is missing;
|
|
2451
|
+
* - user provider options/npm/name are never touched (no forced baseUrl,
|
|
2452
|
+
* no __comment pollution); minimal block fields are set only when
|
|
2453
|
+
* creating a brand-new provider;
|
|
2454
|
+
* - preserves the file's existing indentation and trailing newline, and
|
|
2455
|
+
* skips the write entirely when nothing changed;
|
|
2456
|
+
* - writes everything with atomicWriteFileAtomic (tmp + chmod 600 + rename).
|
|
2457
|
+
*/
|
|
2458
|
+
async function configureModelsFlow(
|
|
2459
|
+
opts: { global?: boolean | undefined; customPath?: string | undefined } = {},
|
|
2460
|
+
): Promise<void> {
|
|
2461
|
+
printHeader();
|
|
2462
|
+
console.log(C.bold("Configure models - writing plugin model definitions into opencode.json\n"));
|
|
2463
|
+
|
|
2464
|
+
const loc = await locateOpenCodeJson(opts.customPath, opts.global === true);
|
|
2465
|
+
console.log(C.dim(`Target candidate: ${loc.path} (exists=${loc.exists}, source=${loc.source})`));
|
|
2466
|
+
console.log(C.dim(`Global config dir: ${resolveBaseDir()}`));
|
|
2467
|
+
console.log("");
|
|
2468
|
+
|
|
2469
|
+
let existing: any = {};
|
|
2470
|
+
let rawOriginal = "";
|
|
2471
|
+
if (loc.exists) {
|
|
2472
|
+
rawOriginal = await fsp.readFile(loc.path, "utf8").catch(() => "");
|
|
2473
|
+
const parsed = await readJsonFileSafe(loc.path);
|
|
2474
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2475
|
+
existing = parsed;
|
|
2476
|
+
logOk(`Existing opencode.json loaded (${Object.keys(parsed).length} top-level keys)`);
|
|
2477
|
+
} else {
|
|
2478
|
+
logWarn("Existing opencode.json contains invalid JSON - backing it up and starting fresh");
|
|
2479
|
+
try {
|
|
2480
|
+
const backupPath = `${loc.path}.backup-${Date.now()}.json`;
|
|
2481
|
+
await fsp.copyFile(loc.path, backupPath);
|
|
2482
|
+
console.log(C.dim(` backup -> ${backupPath}`));
|
|
2483
|
+
} catch {
|
|
2484
|
+
/* the guarded best-effort operation falls through: the outer flow owns the failure */
|
|
2485
|
+
}
|
|
2486
|
+
existing = {};
|
|
2487
|
+
rawOriginal = "";
|
|
2488
|
+
}
|
|
2489
|
+
} else {
|
|
2490
|
+
console.log(C.dim("opencode.json does not exist yet - a new one will be created"));
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
const target = detectTargetProvider(existing);
|
|
2494
|
+
const defs = buildOpenCodeModelDefinitions();
|
|
2495
|
+
const googleModels = buildGoogleProviderModelConfigs();
|
|
2496
|
+
|
|
2497
|
+
// Merge strategy - ADD-ONLY, user wins:
|
|
2498
|
+
// - plugin: unique array merge; never duplicates devthink when a variant
|
|
2499
|
+
// (@wenathlan/devthink, devthink@latest, ...) is already listed
|
|
2500
|
+
// - provider.<target>.models: catalog ids appended ONLY when missing;
|
|
2501
|
+
// existing user entries (custom limits/options/variants) untouched
|
|
2502
|
+
// - provider npm/name/options: set only when creating a brand-new block
|
|
2503
|
+
// - top-level models: additive merge keeping user overrides
|
|
2504
|
+
const merged: any = { ...existing };
|
|
2505
|
+
|
|
2506
|
+
if (!merged.$schema) merged.$schema = "https://opencode.ai/config.json";
|
|
2507
|
+
|
|
2508
|
+
const existingPlugins: any[] = Array.isArray(merged.plugin)
|
|
2509
|
+
? merged.plugin
|
|
2510
|
+
: Array.isArray(merged.plugins)
|
|
2511
|
+
? merged.plugins
|
|
2512
|
+
: [];
|
|
2513
|
+
const pluginsSet = new Set<string>(existingPlugins.map((p: any) => String(p)));
|
|
2514
|
+
// v2.1.21 — the plugin reference is name-agnostic: any variant already
|
|
2515
|
+
// listed counts ("@wenathlan/devthink", "devthink@npm:@wenathlan/devthink@latest"
|
|
2516
|
+
// — the scope-free npm alias —, "file:.../devthink", ...) and is never
|
|
2517
|
+
// duplicated. When nothing exists we add the canonical npm name.
|
|
2518
|
+
// (A package literally named "devthink" outside the @wenathlan scope cannot exist on npmjs.org — the
|
|
2519
|
+
// registry blocks the name with 403 "too similar to madge/panene/sane" —
|
|
2520
|
+
// so the bare spelling is the npm ALIAS, documented in the README.)
|
|
2521
|
+
const hasdevthinkvariant = [...pluginsSet].some((p) => /(^|[/@])devthink([@/]|$)/.test(p));
|
|
2522
|
+
if (!hasdevthinkvariant) {
|
|
2523
|
+
pluginsSet.add("@wenathlan/devthink");
|
|
2524
|
+
}
|
|
2525
|
+
merged.plugin = Array.from(pluginsSet);
|
|
2526
|
+
|
|
2527
|
+
if (!merged.provider || typeof merged.provider !== "object") merged.provider = {};
|
|
2528
|
+
if (!merged.provider[target.id] || typeof merged.provider[target.id] !== "object") merged.provider[target.id] = {};
|
|
2529
|
+
const targetBlock = merged.provider[target.id];
|
|
2530
|
+
const existingModels = targetBlock.models && typeof targetBlock.models === "object" ? targetBlock.models : {};
|
|
2531
|
+
const addedModels: Record<string, any> = {};
|
|
2532
|
+
for (const [id, def] of Object.entries(googleModels)) {
|
|
2533
|
+
if (!existingModels[id]) addedModels[id] = def;
|
|
2534
|
+
}
|
|
2535
|
+
targetBlock.models = { ...existingModels, ...addedModels };
|
|
2536
|
+
if (!target.existed) {
|
|
2537
|
+
// brand-new provider: minimal fields only; options the user later adds
|
|
2538
|
+
// (or loader-injected credentials) always win - nothing is forced
|
|
2539
|
+
if (!targetBlock.name) targetBlock.name = target.id;
|
|
2540
|
+
if (!targetBlock.npm) targetBlock.npm = "@ai-sdk/openai-compatible";
|
|
2541
|
+
if (targetBlock.options === undefined) targetBlock.options = {};
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
if (!merged.models || typeof merged.models !== "object") merged.models = {};
|
|
2545
|
+
const topModels = merged.models;
|
|
2546
|
+
for (const k of Object.keys(defs.topLevelModels)) {
|
|
2547
|
+
const v = defs.topLevelModels[k];
|
|
2548
|
+
if (!topModels[k]) {
|
|
2549
|
+
topModels[k] = v;
|
|
2550
|
+
} else {
|
|
2551
|
+
topModels[k] = {
|
|
2552
|
+
...(v as any),
|
|
2553
|
+
...topModels[k],
|
|
2554
|
+
provider: topModels[k].provider ?? target.id,
|
|
2555
|
+
model: topModels[k].model ?? (v as any).model,
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
merged.models = topModels;
|
|
2560
|
+
|
|
2561
|
+
// Preserve the user's formatting: detect indentation + trailing newline
|
|
2562
|
+
const detectIndent = (raw: string): { indent: string; trailingNewline: boolean } => {
|
|
2563
|
+
let indent = " ";
|
|
2564
|
+
const m = raw.match(/\n([ \t]+)"/);
|
|
2565
|
+
if (m?.[1]) indent = m[1];
|
|
2566
|
+
return { indent, trailingNewline: raw === "" ? true : raw.endsWith("\n") };
|
|
2567
|
+
};
|
|
2568
|
+
const fmt = detectIndent(rawOriginal);
|
|
2569
|
+
const indentArg: number | "\t" = fmt.indent.includes("\t") ? "\t" : Math.min(Math.max(fmt.indent.length, 1), 8);
|
|
2570
|
+
const content = JSON.stringify(merged, null, indentArg) + (fmt.trailingNewline ? "\n" : "");
|
|
2571
|
+
|
|
2572
|
+
if (rawOriginal !== "" && content === rawOriginal) {
|
|
2573
|
+
logOk(`opencode.json already up to date - nothing to add (target provider "${target.id}")`);
|
|
2574
|
+
console.log(
|
|
2575
|
+
C.dim(
|
|
2576
|
+
` provider.${target.id}.models: ${Object.keys(targetBlock.models).length} definitions (all present, user entries preserved)`,
|
|
2577
|
+
),
|
|
2578
|
+
);
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
try {
|
|
2583
|
+
await atomicWriteFileAtomic(loc.path, content, 0o600);
|
|
2584
|
+
logOk(`opencode.json written atomically (chmod 600) -> ${loc.path}`);
|
|
2585
|
+
console.log(
|
|
2586
|
+
C.dim(
|
|
2587
|
+
` target provider: ${target.id} (${target.existed ? "detected by model ids, user block preserved" : "created (nothing existed yet)"})`,
|
|
2588
|
+
),
|
|
2589
|
+
);
|
|
2590
|
+
console.log(C.dim(` plugin: ${JSON.stringify(merged.plugin)}`));
|
|
2591
|
+
console.log(
|
|
2592
|
+
C.dim(
|
|
2593
|
+
` provider.${target.id}.models: +${Object.keys(addedModels).length} added, ${Object.keys(existingModels).length} existing preserved (user customizations untouched)`,
|
|
2594
|
+
),
|
|
2595
|
+
);
|
|
2596
|
+
console.log(C.dim(` top-level models: ${Object.keys(topModels).length} entries (additive)`));
|
|
2597
|
+
console.log(C.dim(` indentation preserved: ${JSON.stringify(fmt.indent)}`));
|
|
2598
|
+
console.log(`\n${C.bold("Verify:")} ${C.cyan(`cat ${loc.path}`)}`);
|
|
2599
|
+
} catch (e) {
|
|
2600
|
+
logErr(`Failed to write opencode.json: ${(e as Error).message}`);
|
|
2601
|
+
process.exitCode = 1;
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
// ===========================================================================
|
|
2606
|
+
// 06. INTERACTIVE MENU
|
|
2607
|
+
// ===========================================================================
|
|
2608
|
+
async function interactiveMenu(): Promise<void> {
|
|
2609
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2610
|
+
let exit = false;
|
|
2611
|
+
const mgr = getDefaultAccountManager({ disableBackgroundTimer: true });
|
|
2612
|
+
|
|
2613
|
+
const menu = `
|
|
2614
|
+
${C.bold("============================================================")}
|
|
2615
|
+
${C.bold(" Opencode Antigravity Auth - Interactive CLI (unified) ")}
|
|
2616
|
+
${C.bold("============================================================")}
|
|
2617
|
+
${C.dim(`Dir: ${resolveBaseDir()} | Client: GeminiCLI/0.57.0 | gl-node/22.19.0 | project=${FALLBACK_PROJECT_ID}`)}
|
|
2618
|
+
|
|
2619
|
+
${C.cyan("1")} Login OAuth (PKCE + browser) - add or fresh start
|
|
2620
|
+
${C.cyan("2")} List accounts (rich v3 format)
|
|
2621
|
+
${C.cyan("3")} Add account manually
|
|
2622
|
+
${C.cyan("4")} Set active account / rotation strategy
|
|
2623
|
+
${C.cyan("5")} Enable / Disable account
|
|
2624
|
+
${C.cyan("6")} Remove account
|
|
2625
|
+
${C.cyan("7")} Check quotas (active or all) - persists results
|
|
2626
|
+
${C.cyan("8")} Configure thresholds / catalog
|
|
2627
|
+
${C.cyan("9")} Models (catalog ALL_MODELS_2026)
|
|
2628
|
+
${C.cyan("10")} Status + Doctor/Diagnostics
|
|
2629
|
+
${C.cyan("11")} Version
|
|
2630
|
+
${C.cyan("12")} Logout (revoke token + remove account)
|
|
2631
|
+
${C.cyan("13")} Manage accounts (toggle enable/disable)
|
|
2632
|
+
${C.cyan("14")} Configure models -> opencode.json (--global/custom)
|
|
2633
|
+
${C.cyan("0")} Exit
|
|
2634
|
+
`;
|
|
2635
|
+
|
|
2636
|
+
while (!exit) {
|
|
2637
|
+
console.log(menu);
|
|
2638
|
+
const choice = (await rl.question(`${C.bold("Choose [0-14]: ")}`)).trim();
|
|
2639
|
+
switch (choice) {
|
|
2640
|
+
case "1": {
|
|
2641
|
+
const noB = (await askQuestion(rl, "No-browser? (y/N): ")).toLowerCase();
|
|
2642
|
+
await cmdLogin(noB ? ["--no-browser"] : [], rl);
|
|
2643
|
+
break;
|
|
2644
|
+
}
|
|
2645
|
+
case "2": {
|
|
2646
|
+
await cmdAccounts(["list"]);
|
|
2647
|
+
break;
|
|
2648
|
+
}
|
|
2649
|
+
case "3": {
|
|
2650
|
+
const email = await askQuestion(rl, "Email: ");
|
|
2651
|
+
const rt = await askQuestion(rl, "Refresh token: ");
|
|
2652
|
+
if (!email || !rt) {
|
|
2653
|
+
logWarn("Cancelled");
|
|
2654
|
+
break;
|
|
2655
|
+
}
|
|
2656
|
+
const proj = (await askQuestion(rl, `Project ID [${FALLBACK_PROJECT_ID}]: `)) || FALLBACK_PROJECT_ID;
|
|
2657
|
+
try {
|
|
2658
|
+
mgr.addAccount({ email, refreshToken: rt, projectId: proj, enabled: true });
|
|
2659
|
+
logOk(`Added ${email}`);
|
|
2660
|
+
} catch (e) {
|
|
2661
|
+
logErr((e as Error).message);
|
|
2662
|
+
}
|
|
2663
|
+
break;
|
|
2664
|
+
}
|
|
2665
|
+
case "4": {
|
|
2666
|
+
await cmdAccounts(["list"]);
|
|
2667
|
+
const email = await askQuestion(rl, "Email to activate: ");
|
|
2668
|
+
if (!email) break;
|
|
2669
|
+
const sticky = (await askQuestion(rl, "Sticky mode? (y/N): ")).toLowerCase();
|
|
2670
|
+
const stickyOn = /^(y|yes)$/.test(sticky);
|
|
2671
|
+
try {
|
|
2672
|
+
mgr.setActive(email, { sticky: stickyOn });
|
|
2673
|
+
logOk(`Active ${email} sticky=${stickyOn}`);
|
|
2674
|
+
} catch (e) {
|
|
2675
|
+
logErr((e as Error).message);
|
|
2676
|
+
}
|
|
2677
|
+
break;
|
|
2678
|
+
}
|
|
2679
|
+
case "5": {
|
|
2680
|
+
await cmdAccounts(["list"]);
|
|
2681
|
+
const email = await askQuestion(rl, "Email to toggle enable/disable: ");
|
|
2682
|
+
if (!email) break;
|
|
2683
|
+
const act = (await askQuestion(rl, "Enable (e) / Disable (d)? [e/d]: ")).toLowerCase();
|
|
2684
|
+
try {
|
|
2685
|
+
if (act === "d") {
|
|
2686
|
+
mgr.disableAccount(email, "manual interactive");
|
|
2687
|
+
logOk(`Disabled ${email}`);
|
|
2688
|
+
} else {
|
|
2689
|
+
mgr.enableAccount(email);
|
|
2690
|
+
logOk(`Enabled ${email}`);
|
|
2691
|
+
}
|
|
2692
|
+
} catch (e) {
|
|
2693
|
+
logErr((e as Error).message);
|
|
2694
|
+
}
|
|
2695
|
+
break;
|
|
2696
|
+
}
|
|
2697
|
+
case "6": {
|
|
2698
|
+
await cmdAccounts(["list"]);
|
|
2699
|
+
const email = await askQuestion(rl, "Email to remove: ");
|
|
2700
|
+
if (!email) break;
|
|
2701
|
+
const conf = await askQuestion(rl, `Confirm removal of ${email}? Type YES: `);
|
|
2702
|
+
if (conf !== "YES") {
|
|
2703
|
+
logWarn("Cancelled");
|
|
2704
|
+
break;
|
|
2705
|
+
}
|
|
2706
|
+
const ok = mgr.removeAccount(email);
|
|
2707
|
+
if (ok) logOk(`Removed ${email}`);
|
|
2708
|
+
else logErr("Not found");
|
|
2709
|
+
break;
|
|
2710
|
+
}
|
|
2711
|
+
case "7": {
|
|
2712
|
+
const all = (await askQuestion(rl, "All accounts? (y/N): ")).toLowerCase();
|
|
2713
|
+
const json = (await askQuestion(rl, "JSON output? (y/N): ")).toLowerCase();
|
|
2714
|
+
const args: string[] = [];
|
|
2715
|
+
if (/^(y|yes)$/.test(all)) args.push("--all");
|
|
2716
|
+
if (/^(y|yes)$/.test(json)) args.push("--json");
|
|
2717
|
+
await cmdQuota(args);
|
|
2718
|
+
break;
|
|
2719
|
+
}
|
|
2720
|
+
case "8": {
|
|
2721
|
+
await cmdConfig(["list"]);
|
|
2722
|
+
const k = await askQuestion(rl, "Key to change (or enter to go back): ");
|
|
2723
|
+
if (!k) break;
|
|
2724
|
+
const v = await askQuestion(rl, `New value for ${k}: `);
|
|
2725
|
+
if (!v) break;
|
|
2726
|
+
await cmdConfig(["set", k, v]);
|
|
2727
|
+
break;
|
|
2728
|
+
}
|
|
2729
|
+
case "9": {
|
|
2730
|
+
const api = await askQuestion(rl, "Filter API (antigravity/gemini-cli/enter for all): ");
|
|
2731
|
+
if (api) await cmdModels(["list", "--api", api]);
|
|
2732
|
+
else await cmdModels(["list"]);
|
|
2733
|
+
const mid = await askQuestion(rl, "Show model info (id) or enter to go back: ");
|
|
2734
|
+
if (mid) await cmdModels(["info", mid]);
|
|
2735
|
+
break;
|
|
2736
|
+
}
|
|
2737
|
+
case "10": {
|
|
2738
|
+
await cmdStatus([]);
|
|
2739
|
+
await cmdDoctor([]);
|
|
2740
|
+
break;
|
|
2741
|
+
}
|
|
2742
|
+
case "11": {
|
|
2743
|
+
await cmdVersion([]);
|
|
2744
|
+
break;
|
|
2745
|
+
}
|
|
2746
|
+
case "12": {
|
|
2747
|
+
const allAns = (await askQuestion(rl, "Logout ALL accounts? (y/N): ")).toLowerCase();
|
|
2748
|
+
await logoutFlow(undefined, /^(y|yes)$/.test(allAns), rl);
|
|
2749
|
+
break;
|
|
2750
|
+
}
|
|
2751
|
+
case "13": {
|
|
2752
|
+
await cmdManage([], rl);
|
|
2753
|
+
break;
|
|
2754
|
+
}
|
|
2755
|
+
case "14": {
|
|
2756
|
+
const gAns = (
|
|
2757
|
+
await askQuestion(rl, "Use global ~/.config/opencode/opencode.json instead of cwd/opencode.json? (y/N): ")
|
|
2758
|
+
).toLowerCase();
|
|
2759
|
+
await configureModelsFlow({ global: /^(y|yes)$/.test(gAns) });
|
|
2760
|
+
break;
|
|
2761
|
+
}
|
|
2762
|
+
case "0":
|
|
2763
|
+
case "q":
|
|
2764
|
+
case "quit":
|
|
2765
|
+
case "exit": {
|
|
2766
|
+
exit = true;
|
|
2767
|
+
break;
|
|
2768
|
+
}
|
|
2769
|
+
default:
|
|
2770
|
+
logWarn("Invalid option");
|
|
2771
|
+
break;
|
|
2772
|
+
}
|
|
2773
|
+
if (!exit) {
|
|
2774
|
+
await askQuestion(rl, C.dim("\n[press enter to continue]"));
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
rl.close();
|
|
2778
|
+
console.log(C.green("\nGoodbye!"));
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
// ===========================================================================
|
|
2782
|
+
// 07. HELP & MAIN ROUTER
|
|
2783
|
+
// ===========================================================================
|
|
2784
|
+
function printHelp(): void {
|
|
2785
|
+
console.log(
|
|
2786
|
+
C.bold(`
|
|
2787
|
+
devthink provider CLI (unified merge) - production-ready
|
|
2788
|
+
Writes to ~/.config/opencode/ (600) atomically | honors OPENCODE_CONFIG_DIR
|
|
2789
|
+
Bypass: client_id 681255809395-oo8f…b135j / UA GeminiCLI/0.57.0 / gl-node/22.19.0 / Client-Metadata IDE_UNSPECIFIED / project rising-fact-p41fc
|
|
2790
|
+
|
|
2791
|
+
Usage:
|
|
2792
|
+
cli.ts <command> [subcommand] [flags]
|
|
2793
|
+
|
|
2794
|
+
Commands:
|
|
2795
|
+
login [--no-browser] [--timeout <ms>] [--email <email>] [--fresh]
|
|
2796
|
+
OAuth PKCE Antigravity via oauth.ts (local browser callback on 127.0.0.1).
|
|
2797
|
+
Interactive mode selection: 1 = add accounts, 2 = fresh start (wipe all).
|
|
2798
|
+
Writes a debugLogger audit line on success.
|
|
2799
|
+
|
|
2800
|
+
logout [<email>] [--all]
|
|
2801
|
+
Revoke the Google refresh token (oauth2.googleapis.com/revoke) and remove
|
|
2802
|
+
the account. Without an email: single-account auto pick or interactive
|
|
2803
|
+
chooser. --all removes every account after confirmation.
|
|
2804
|
+
|
|
2805
|
+
accounts list [--json]
|
|
2806
|
+
accounts add --email <email> --refresh-token <token> [--project <id>]
|
|
2807
|
+
accounts remove <email>
|
|
2808
|
+
accounts enable <email>
|
|
2809
|
+
accounts disable <email> [--reason <text>]
|
|
2810
|
+
accounts set-active <email> [--sticky]
|
|
2811
|
+
accounts import [--path <file>]
|
|
2812
|
+
Rich listing: store version, activeIndexByFamily, addedAt/created/expiry,
|
|
2813
|
+
remaining/limit, rateLimits, soft-quota cooldowns.
|
|
2814
|
+
|
|
2815
|
+
enable <email>
|
|
2816
|
+
disable <email>
|
|
2817
|
+
Top-level single-account toggles.
|
|
2818
|
+
|
|
2819
|
+
manage
|
|
2820
|
+
Interactive per-account enable/disable toggle (robin-hood v3 store).
|
|
2821
|
+
|
|
2822
|
+
quota [email] [--all] [--json] [--email <email>]
|
|
2823
|
+
Checks quotas via quota.ts (daily-cloudcode-pa + prod) with config
|
|
2824
|
+
thresholds; persists remaining/limit back into the accounts file.
|
|
2825
|
+
|
|
2826
|
+
config list [--json]
|
|
2827
|
+
config get <key>
|
|
2828
|
+
config set <key> <value>
|
|
2829
|
+
config reset
|
|
2830
|
+
Tunables: soft_quota_threshold_percent (0-100), quota_refresh_interval_minutes
|
|
2831
|
+
(1-1440), toast_scope, cli_first, quota_fallback, etc. Model catalog
|
|
2832
|
+
ALL_MODELS_2026 from constants.ts.
|
|
2833
|
+
|
|
2834
|
+
models list [--api antigravity|gemini-cli] [--json]
|
|
2835
|
+
models info <model-id>
|
|
2836
|
+
|
|
2837
|
+
configure [--global] [--path <file>]
|
|
2838
|
+
Writes model definitions into opencode.json: provider.google.models
|
|
2839
|
+
(2026-08-25 catalog with thinkingLevel variants incl claude-sonnet
|
|
2840
|
+
low/high and claude-opus budgets), provider.antigravity.models (rich
|
|
2841
|
+
bypass metadata) and top-level model shortcuts. Atomic chmod 600 write.
|
|
2842
|
+
|
|
2843
|
+
status
|
|
2844
|
+
Accounts summary + config + permissions + quick quota
|
|
2845
|
+
|
|
2846
|
+
doctor | diagnostics
|
|
2847
|
+
File/permission/token-expiry diagnostics, logs dir, default model,
|
|
2848
|
+
opencode.json location, raw accounts file head (first 1000 chars)
|
|
2849
|
+
|
|
2850
|
+
version [--json]
|
|
2851
|
+
Plugin + remote antigravity manifest versions
|
|
2852
|
+
|
|
2853
|
+
menu | interactive
|
|
2854
|
+
help | --help | -h
|
|
2855
|
+
|
|
2856
|
+
Env:
|
|
2857
|
+
OPENCODE_CONFIG_DIR Override ~/.config/opencode
|
|
2858
|
+
ANTIGRAVITY_DEBUG Debug logging (1/true/debug)
|
|
2859
|
+
ANTIGRAVITY_CLI_AUTORUN Set to 0 to disable auto-run when executed directly
|
|
2860
|
+
NO_COLOR Disable ANSI colors
|
|
2861
|
+
|
|
2862
|
+
No args => interactive menu (readline/promises), zero deps.
|
|
2863
|
+
|
|
2864
|
+
Examples:
|
|
2865
|
+
npx tsx cli.ts login --no-browser
|
|
2866
|
+
npx tsx cli.ts logout --all
|
|
2867
|
+
npx tsx cli.ts accounts list
|
|
2868
|
+
npx tsx cli.ts enable user@example.com
|
|
2869
|
+
npx tsx cli.ts quota --all
|
|
2870
|
+
npx tsx cli.ts config set soft_quota_threshold_percent 85
|
|
2871
|
+
npx tsx cli.ts configure --global
|
|
2872
|
+
npx tsx cli.ts models list --api antigravity
|
|
2873
|
+
npx tsx cli.ts status
|
|
2874
|
+
`),
|
|
2875
|
+
);
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
/**
|
|
2879
|
+
* Family entry of the grand merge: the single devthink binary routes the
|
|
2880
|
+
* extension command family here (`devthink ext <command>`); the argv is
|
|
2881
|
+
* spliced into the process position the original entry reading expects,
|
|
2882
|
+
* the family main runs untouched and the caller argv is restored.
|
|
2883
|
+
*
|
|
2884
|
+
* @param argv the family arguments (without the family prefix).
|
|
2885
|
+
*/
|
|
2886
|
+
export async function runproviderfamily(argv: string[]): Promise<void> {
|
|
2887
|
+
const saved = process.argv.slice();
|
|
2888
|
+
process.argv = [saved[0], saved[1], ...argv];
|
|
2889
|
+
try {
|
|
2890
|
+
await providerconsolemain();
|
|
2891
|
+
} finally {
|
|
2892
|
+
process.argv = saved;
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
async function providerconsolemain(): Promise<void> {
|
|
2897
|
+
const argv = process.argv.slice(2);
|
|
2898
|
+
const cmd = (argv[0] ?? "").toLowerCase();
|
|
2899
|
+
|
|
2900
|
+
/** Read the value of a "--name value" / "--name=value" style flag. */
|
|
2901
|
+
const getFlagValue = (name: string): string | undefined => {
|
|
2902
|
+
const idx = argv.findIndex((a) => a === `--${name}` || a.startsWith(`--${name}=`));
|
|
2903
|
+
if (idx === -1) return undefined;
|
|
2904
|
+
const a = argv[idx];
|
|
2905
|
+
if (a && a.includes("=")) return a.split("=").slice(1).join("=").trim();
|
|
2906
|
+
return argv[idx + 1]?.trim();
|
|
2907
|
+
};
|
|
2908
|
+
|
|
2909
|
+
// If no command, interactive
|
|
2910
|
+
if (!cmd || cmd === "menu" || cmd === "interactive" || cmd === "i") {
|
|
2911
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
2912
|
+
printHelp();
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
// check if TTY; if not TTY and no args, show help
|
|
2916
|
+
if (!process.stdout.isTTY && !argv.length) {
|
|
2917
|
+
printHelp();
|
|
2918
|
+
return;
|
|
2919
|
+
}
|
|
2920
|
+
if (!argv.length) {
|
|
2921
|
+
await interactiveMenu();
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
switch (cmd) {
|
|
2927
|
+
case "help":
|
|
2928
|
+
case "--help":
|
|
2929
|
+
case "-h":
|
|
2930
|
+
printHelp();
|
|
2931
|
+
break;
|
|
2932
|
+
case "login":
|
|
2933
|
+
case "auth":
|
|
2934
|
+
case "signin":
|
|
2935
|
+
await cmdLogin(argv.slice(1));
|
|
2936
|
+
break;
|
|
2937
|
+
case "logout":
|
|
2938
|
+
case "signout": {
|
|
2939
|
+
const restArgs = argv.slice(1);
|
|
2940
|
+
const emailArg = restArgs.find((a) => !a.startsWith("-") && a.includes("@"));
|
|
2941
|
+
const allFlag = restArgs.some((a) => a === "--all" || a === "-a");
|
|
2942
|
+
await logoutFlow(emailArg, allFlag);
|
|
2943
|
+
break;
|
|
2944
|
+
}
|
|
2945
|
+
case "accounts":
|
|
2946
|
+
case "account":
|
|
2947
|
+
case "acc":
|
|
2948
|
+
await cmdAccounts(argv.slice(1));
|
|
2949
|
+
break;
|
|
2950
|
+
case "enable": {
|
|
2951
|
+
const email = argv.slice(1).find((a) => !a.startsWith("-") && a.includes("@")) ?? getFlagValue("email");
|
|
2952
|
+
if (!email) {
|
|
2953
|
+
logErr("enable requires an email: enable <email> [--email <email>]");
|
|
2954
|
+
process.exitCode = 2;
|
|
2955
|
+
break;
|
|
2956
|
+
}
|
|
2957
|
+
await enableAccountFlow(email);
|
|
2958
|
+
break;
|
|
2959
|
+
}
|
|
2960
|
+
case "disable": {
|
|
2961
|
+
const email = argv.slice(1).find((a) => !a.startsWith("-") && a.includes("@")) ?? getFlagValue("email");
|
|
2962
|
+
if (!email) {
|
|
2963
|
+
logErr("disable requires an email: disable <email> [--email <email>]");
|
|
2964
|
+
process.exitCode = 2;
|
|
2965
|
+
break;
|
|
2966
|
+
}
|
|
2967
|
+
await disableAccountFlow(email);
|
|
2968
|
+
break;
|
|
2969
|
+
}
|
|
2970
|
+
case "manage":
|
|
2971
|
+
case "accounts-manage":
|
|
2972
|
+
await cmdManage(argv.slice(1));
|
|
2973
|
+
break;
|
|
2974
|
+
case "quota":
|
|
2975
|
+
case "quotas":
|
|
2976
|
+
case "q":
|
|
2977
|
+
await cmdQuota(argv.slice(1));
|
|
2978
|
+
break;
|
|
2979
|
+
case "config":
|
|
2980
|
+
case "cfg":
|
|
2981
|
+
case "settings":
|
|
2982
|
+
await cmdConfig(argv.slice(1));
|
|
2983
|
+
break;
|
|
2984
|
+
case "configure":
|
|
2985
|
+
case "configure-models":
|
|
2986
|
+
case "config-models":
|
|
2987
|
+
case "setup": {
|
|
2988
|
+
const globalFlag = argv.includes("--global") || argv.includes("-g");
|
|
2989
|
+
const customPath = getFlagValue("path") ?? getFlagValue("file") ?? argv.slice(1).find((a) => a.endsWith(".json"));
|
|
2990
|
+
await configureModelsFlow({ global: globalFlag, customPath });
|
|
2991
|
+
break;
|
|
2992
|
+
}
|
|
2993
|
+
case "models":
|
|
2994
|
+
case "model":
|
|
2995
|
+
case "m":
|
|
2996
|
+
await cmdModels(argv.slice(1));
|
|
2997
|
+
break;
|
|
2998
|
+
case "status":
|
|
2999
|
+
case "st":
|
|
3000
|
+
await cmdStatus(argv.slice(1));
|
|
3001
|
+
break;
|
|
3002
|
+
case "doctor":
|
|
3003
|
+
case "doc":
|
|
3004
|
+
case "check":
|
|
3005
|
+
case "diagnostics":
|
|
3006
|
+
await cmdDoctor(argv.slice(1));
|
|
3007
|
+
break;
|
|
3008
|
+
case "version":
|
|
3009
|
+
case "ver":
|
|
3010
|
+
case "v":
|
|
3011
|
+
await cmdVersion(argv.slice(1));
|
|
3012
|
+
break;
|
|
3013
|
+
default:
|
|
3014
|
+
logErr(`Unknown command: ${cmd}`);
|
|
3015
|
+
printHelp();
|
|
3016
|
+
process.exit(1);
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
export type { ModelCatalogEntry, OpenCodeJsonLocation };
|
|
3021
|
+
// ---------------------------------------------------------------------------
|
|
3022
|
+
// Public API (~30 exports incl runCLI + default { runCLI, interactiveMenu })
|
|
3023
|
+
// ---------------------------------------------------------------------------
|
|
3024
|
+
export {
|
|
3025
|
+
ANSI,
|
|
3026
|
+
atomicWriteFileAtomic,
|
|
3027
|
+
atomicWriteFileSecure,
|
|
3028
|
+
buildOpenCodeModelDefinitions,
|
|
3029
|
+
C,
|
|
3030
|
+
CLI_VERSION,
|
|
3031
|
+
cmdAccounts,
|
|
3032
|
+
cmdConfig,
|
|
3033
|
+
cmdDoctor,
|
|
3034
|
+
cmdLogin,
|
|
3035
|
+
cmdManage,
|
|
3036
|
+
cmdModels,
|
|
3037
|
+
cmdQuota,
|
|
3038
|
+
cmdStatus,
|
|
3039
|
+
cmdVersion,
|
|
3040
|
+
configureModelsFlow,
|
|
3041
|
+
confirm,
|
|
3042
|
+
disableAccountFlow,
|
|
3043
|
+
enableAccountFlow,
|
|
3044
|
+
fetchQuotaForAccount,
|
|
3045
|
+
fmtDuration,
|
|
3046
|
+
getDefaultAccountManager as getAccountManager,
|
|
3047
|
+
interactiveMenu,
|
|
3048
|
+
locateOpenCodeJson,
|
|
3049
|
+
logoutFlow,
|
|
3050
|
+
printHeader,
|
|
3051
|
+
printHelp,
|
|
3052
|
+
providerconsolemain as runCLI,
|
|
3053
|
+
redactEmail,
|
|
3054
|
+
resolveAccountsPath,
|
|
3055
|
+
resolveAntigravityJsonPath,
|
|
3056
|
+
resolveBaseDir,
|
|
3057
|
+
resolveConfigPath,
|
|
3058
|
+
resolveOpenCodeJsonCandidates,
|
|
3059
|
+
};
|
|
3060
|
+
|
|
3061
|
+
export const providercli = {
|
|
3062
|
+
runCLI: providerconsolemain,
|
|
3063
|
+
interactiveMenu,
|
|
3064
|
+
};
|
|
3065
|
+
|
|
3066
|
+
/* ── Merged: the grand merge section ── the correlated provider family library barrel logics of the merged repository interned here, one surface without duplicate variations ── */
|
|
3067
|
+
/**
|
|
3068
|
+
* @file providercli.ts — the barrel section of the provider family.
|
|
3069
|
+
* @description The library barrel of the merged provider family —
|
|
3070
|
+
* library-first, root-first. The provider family modules of the merged
|
|
3071
|
+
* repository live as the flat root modules (constants, config, models,
|
|
3072
|
+
* accounts, fingerprint, project, quota, recovery, search, core, request,
|
|
3073
|
+
* streaming, oauth, antigravity, devthink, server) and every import path
|
|
3074
|
+
* of the provider surface resolves through this barrel.
|
|
3075
|
+
*
|
|
3076
|
+
* The namespace re-exports keep the historical surface: several siblings
|
|
3077
|
+
* (constants/config/oauth/accounts/fingerprint/search) intentionally share
|
|
3078
|
+
* exported names such as PROJECT_FALLBACK, MODELS_2026 or OAUTH_SCOPES —
|
|
3079
|
+
* every feature stays reachable through its module namespace below. The
|
|
3080
|
+
* consolidation history rides the mapping: the provider oauth family lives
|
|
3081
|
+
* in oauth.ts, request-helpers merged into request.ts, system merged into
|
|
3082
|
+
* config.ts (with the debug utilities in debug.ts) — the OAuth,
|
|
3083
|
+
* RequestHelpers and System namespaces below alias the consolidated modules
|
|
3084
|
+
* so every historical import path keeps resolving.
|
|
3085
|
+
*/
|
|
3086
|
+
|
|
3087
|
+
export * as Accounts from "./accounts.js";
|
|
3088
|
+
export * as Constants from "@wenathlan/devthink/constants";
|
|
3089
|
+
export * as Core from "@wenathlan/devthink/core";
|
|
3090
|
+
export * as Fingerprint from "./fingerprint.js";
|
|
3091
|
+
// the provider oauth family answers the OAuth namespace (the grand merge
|
|
3092
|
+
// consolidation interned the family in oauth.ts — node surface).
|
|
3093
|
+
export * as OAuth from "./oauth.js";
|
|
3094
|
+
// the provider oauth family answers the AntigravityCli compat namespace.
|
|
3095
|
+
export * as AntigravityCli from "./oauth.js";
|
|
3096
|
+
// merged group additions: auth/core/system/models namespaces
|
|
3097
|
+
export * as Auth from "./oauth.js";
|
|
3098
|
+
|
|
3099
|
+
/** System — the v2.1.14 system utilities surface: config.ts absorbed the
|
|
3100
|
+
* system module (cfgDir, the version fallback chain, atomic writes, the
|
|
3101
|
+
* legacy validator aliases and the raw JSON IO helpers) while the debug
|
|
3102
|
+
* utilities (logsDir, redactSecrets, the debugLogger singleton) live in
|
|
3103
|
+
* debug.ts, so the namespace carries exactly the exports the system
|
|
3104
|
+
* shim used to publish. */
|
|
3105
|
+
|
|
3106
|
+
|
|
3107
|
+
|
|
3108
|
+
export const System = {
|
|
3109
|
+
atomicWrite,
|
|
3110
|
+
cfgDir,
|
|
3111
|
+
fetchVersionChain,
|
|
3112
|
+
loadConfigRaw,
|
|
3113
|
+
saveConfigRaw,
|
|
3114
|
+
validateEmail,
|
|
3115
|
+
validateModelId,
|
|
3116
|
+
validateModelIdCharset,
|
|
3117
|
+
validateProjectId,
|
|
3118
|
+
validateSemver,
|
|
3119
|
+
VERSION_FALLBACK,
|
|
3120
|
+
debugLogger,
|
|
3121
|
+
logsDir,
|
|
3122
|
+
redactSecrets,
|
|
3123
|
+
};
|
|
3124
|
+
export * as PluginModule from "./antigravity.js";
|
|
3125
|
+
// default plugin export for opencode compatibility (the antigravity plugin family)
|
|
3126
|
+
export { default as opencodePluginModule, getPlugin, plugin } from "./antigravity.js";
|
|
3127
|
+
export * as Config from "@wenathlan/devthink/config";
|
|
3128
|
+
export * as Debug from "@wenathlan/devthink/core";
|
|
3129
|
+
export * as Models from "./models.js";
|
|
3130
|
+
export * as Models2026 from "./models.js";
|
|
3131
|
+
export * as Project from "./project.js";
|
|
3132
|
+
export * as Quota from "./quota.js";
|
|
3133
|
+
export * as Recovery from "./recovery.js";
|
|
3134
|
+
// v2.1.14: request-helpers.ts was consolidated into request.ts; namespace kept for compat.
|
|
3135
|
+
export * as RequestHelpers from "./request.js";
|
|
3136
|
+
export * as Request from "./request.js";
|
|
3137
|
+
export * as Search from "./search.js";
|
|
3138
|
+
// the CLI namespace left the providers barrel (the browser purity law):
|
|
3139
|
+
// cli.js value-imports node:process (stdin, stdout) and ./server.js, so the
|
|
3140
|
+
// namespace re-export dragged the whole hono server stack into every
|
|
3141
|
+
// browser consumer of this barrel — the dev boot died on the externalized
|
|
3142
|
+
// node:process stub and the serve-static module. The CLI keeps its own
|
|
3143
|
+
// published face (@wenathlan/getry/cli) and the os server re-exports it
|
|
3144
|
+
// directly from the owner; nothing in the tree consumed providers.CLI.
|
|
3145
|
+
export * as Streaming from "./streaming.js";
|
|
3146
|
+
export * as Version from "./versionregistry.js";
|
|
3147
|
+
|
|
3148
|
+
// v2.1.16 single-owner doctrine: VERSION derives from constants.js
|
|
3149
|
+
// PLUGIN_VERSION (raw value owner) and the two bypass aggregates assemble
|
|
3150
|
+
// every value from its owner module instead of locally-reassembled literals.
|
|
3151
|
+
|
|
3152
|
+
|
|
3153
|
+
/* the identity snapshot import rides the account console cli section above — one declaration per binding */
|
|
3154
|
+
|
|
3155
|
+
// version / identity
|
|
3156
|
+
export const VERSION: string = PLUGIN_VERSION;
|
|
3157
|
+
export const PLUGIN_ID = "devthink" as const;
|
|
3158
|
+
|
|
3159
|
+
// headline model catalog re-exports
|
|
3160
|
+
export { ALL_MODELS_2026, MODEL_BY_ID, MODEL_ROUTING } from "./models.js";
|
|
3161
|
+
|
|
3162
|
+
/**
|
|
3163
|
+
* Gemini CLI bypass aggregate: exact identification the official Gemini CLI
|
|
3164
|
+
* sends when talking to cloudcode-pa. Consumed by bypass flows that must not
|
|
3165
|
+
* require a user project id. Every value is imported from its owner — the
|
|
3166
|
+
* OAuth pair, UA, Client-Metadata string, project fallback and base URL from
|
|
3167
|
+
* constants.js, the gl-node snapshot from fingerprint.js and the v1internal
|
|
3168
|
+
* method paths via constants.js CODE_ASSIST_PATH_MAP.
|
|
3169
|
+
*/
|
|
3170
|
+
export const GEMINI_CLI_BYPASS = {
|
|
3171
|
+
clientId: GEMINI_CLI_OAUTH_CLIENT_ID,
|
|
3172
|
+
clientSecret: GEMINI_CLI_OAUTH_CLIENT_SECRET,
|
|
3173
|
+
userAgent: GEMINI_CLI_USER_AGENT,
|
|
3174
|
+
xGoogApiClient: X_GOOG_API_CLIENT_GEMINI_CLI,
|
|
3175
|
+
clientMetadata: CLIENT_METADATA_STRING,
|
|
3176
|
+
fallbackProject: PROJECT_FALLBACK,
|
|
3177
|
+
endpoints: {
|
|
3178
|
+
loadCodeAssist: `${CLOUDCODE_BASE_URL}${CODE_ASSIST_PATH_MAP.LOAD_CODE_ASSIST}`,
|
|
3179
|
+
generateContent: `${CLOUDCODE_BASE_URL}${CODE_ASSIST_PATH_MAP.GENERATE_CONTENT}`,
|
|
3180
|
+
streamGenerateContent: `${CLOUDCODE_BASE_URL}${CODE_ASSIST_PATH_MAP.STREAM_GENERATE_CONTENT}`,
|
|
3181
|
+
},
|
|
3182
|
+
/** Isolated credential dir — resolved per-OS at runtime, never hardcoded. */
|
|
3183
|
+
isolatedDir: "MAENE_ISOLATED_DIR | <opencode-base>/auth",
|
|
3184
|
+
} as const;
|
|
3185
|
+
|
|
3186
|
+
/**
|
|
3187
|
+
* Antigravity CLI bypass aggregate: identification of the official `agy`
|
|
3188
|
+
* binary (extracted from the distributed CLI and live-validated 2026-08-26).
|
|
3189
|
+
* The client the plugin presents for NEW logins since 2.1.8: the client pair,
|
|
3190
|
+
* scopes, official redirect URI and version parts come from oauth.js
|
|
3191
|
+
* (the authentication owner — CLIENT_ID/CLIENT_SECRET are env-overridable
|
|
3192
|
+
* there, so the aggregate tracks the same override); the UA and
|
|
3193
|
+
* X-Goog-Api-Client keep their static linux/amd64 snapshot shape, composed
|
|
3194
|
+
* from the oauth.js version constants.
|
|
3195
|
+
*/
|
|
3196
|
+
export const ANTIGRAVITY_CLI_BYPASS = {
|
|
3197
|
+
clientId: ANTIGRAVITY_CLI_CLIENT_ID,
|
|
3198
|
+
clientSecret: ANTIGRAVITY_CLI_CLIENT_SECRET,
|
|
3199
|
+
userAgent: `antigravity-cli/${ANTIGRAVITY_CLI_VERSION_LATEST} (linux; amd64)`,
|
|
3200
|
+
xGoogApiClient: `antigravity-cli/${ANTIGRAVITY_CLI_VERSION_LATEST} grpc-go/${ANTIGRAVITY_CLI_GRPC_VERSION}`,
|
|
3201
|
+
redirectUriOfficial: ANTIGRAVITY_CLI_REDIRECT_OFFICIAL,
|
|
3202
|
+
scopes: ANTIGRAVITY_CLI_OAUTH_SCOPES,
|
|
3203
|
+
usesPKCE: true,
|
|
3204
|
+
appLabel: "Google Antigravity",
|
|
3205
|
+
} as const;
|
|
3206
|
+
|
|
3207
|
+
/* ── Merged: the grand merge section ── the correlated opencode server plugin entry logics of the merged repository interned here, one surface without duplicate variations ── */
|
|
3208
|
+
/**
|
|
3209
|
+
* @file providercli.ts — the opencode `./server` package entry (v2.1.20).
|
|
3210
|
+
*
|
|
3211
|
+
* opencode >= 1.18 resolves package.json exports["./server"] FIRST when
|
|
3212
|
+
* loading a server plugin. This entry uses the multi-export plugin shape so
|
|
3213
|
+
* that a SINGLE plugin reference registers THREE auth entries in
|
|
3214
|
+
* `opencode auth login` (and in the TUI "Connect a provider" dialog):
|
|
3215
|
+
*
|
|
3216
|
+
* - provider "google" → the standard Google entry, OAuth method
|
|
3217
|
+
* - provider "antigravity" → the standard Antigravity entry, OAuth method
|
|
3218
|
+
* - the CUSTOM provider detected by MODEL IDS in opencode.json
|
|
3219
|
+
* ("casasbahia", "my-google", ... — whatever the user named it)
|
|
3220
|
+
*
|
|
3221
|
+
* opencode's loader treats every exported FUNCTION as a plugin instance and
|
|
3222
|
+
* merges the returned hooks, which is exactly what we need: one instance
|
|
3223
|
+
* returns the main hooks (identification headers, config injection, the
|
|
3224
|
+
* custom-provider auth) and two more instances return the standard google /
|
|
3225
|
+
* antigravity auth hooks.
|
|
3226
|
+
*
|
|
3227
|
+
* IMPORTANT CONTRACT: every export in this file MUST be a function —
|
|
3228
|
+
* opencode's legacy loader throws "Plugin export is not a function" for any
|
|
3229
|
+
* other export shape. Never re-export constants, objects or types from here;
|
|
3230
|
+
* library consumers keep importing from the package root (plugin.ts).
|
|
3231
|
+
*
|
|
3232
|
+
* (the ownership move of the gateway ownership wave, step 5: the three
|
|
3233
|
+
* entries merged into the antigravity owner — providerServer/googleAuth/
|
|
3234
|
+
* antigravityAuth live in antigravity.ts beside opencodeServer and
|
|
3235
|
+
* buildOpencodeAuthHook — and re-exported here for compat.)
|
|
3236
|
+
*/
|
|
3237
|
+
|
|
3238
|
+
export { providerServer, googleAuth, antigravityAuth } from "./antigravity.js";
|