clauderipple 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -0
- package/README.ko.md +18 -2
- package/README.md +27 -5
- package/dist/app/package.json +1 -1
- package/dist/cli/src/claude-auth.js +35 -1
- package/dist/cli/src/codex.js +23 -0
- package/dist/cli/src/hooks/agent-title.js +58 -29
- package/dist/cli/src/index.js +46 -1
- package/dist/cli/src/picker.js +19 -11
- package/dist/cli/src/runtime.js +3 -0
- package/dist/cli/src/settings.js +49 -0
- package/dist/mod/.claude-plugin/plugin.json +1 -0
- package/dist/mod/hooks/hooks.json +1 -0
- package/dist/mod/hooks/register.tsx +309 -0
- package/dist/mod/hooks/worker.ts +49 -0
- package/dist/mod/tsconfig.json +3 -0
- package/dist/mod/types/index.d.ts +30 -0
- package/dist/router/src/admin.js +411 -32
- package/dist/router/src/agents.js +24 -8
- package/dist/router/src/catalog.js +80 -0
- package/dist/router/src/compat.js +27 -1
- package/dist/router/src/config.js +31 -0
- package/dist/router/src/index.js +12 -1
- package/dist/router/src/ingress/server.js +3 -2
- package/dist/router/src/picker.js +33 -2
- package/dist/router/src/presets.js +2 -1
- package/dist/router/src/providers/chatgpt/index.js +77 -18
- package/dist/router/src/providers/chatgpt/translate.js +34 -6
- package/dist/router/src/providers/google/accounts.js +345 -0
- package/dist/router/src/providers/google/antigravity.js +392 -0
- package/dist/router/src/providers/google/index.js +617 -0
- package/dist/router/src/providers/google/login.js +249 -0
- package/dist/router/src/providers/google/thoughtSignatures.js +47 -0
- package/dist/router/src/providers/google/translate.js +539 -0
- package/dist/router/src/providers/google/transport.js +118 -0
- package/dist/router/src/providers/openai/translate.js +3 -3
- package/dist/router/src/proxy.js +116 -9
- package/dist/router/src/requestlog.js +2 -0
- package/dist/router/src/routing.js +22 -0
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +420 -45
- package/dist/ui/i18n.js +76 -4
- package/dist/ui/index.html +5 -0
- package/dist/ui/style.css +4 -0
- package/docs/ARCHITECTURE.md +307 -4
- package/package.json +1 -1
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
// Google Antigravity / Cloud Code Assist (CCA): the wire facts, model-id resolution and the
|
|
2
|
+
// conversation session id this router uses. Kept apart from the account store (accounts.ts) and the
|
|
3
|
+
// OAuth session (login.ts) so each can be read and tested on its own.
|
|
4
|
+
//
|
|
5
|
+
// Every fact here is taken from the reference implementation opencodex (GitHub lidge-jun/opencodex,
|
|
6
|
+
// read 2026-10-01 as a behavioral spec — no code copied; the project rule forbids copying it). Each
|
|
7
|
+
// constant names its source file:line in a comment. **Nothing here is measured against a live
|
|
8
|
+
// Google endpoint** — this environment has no Antigravity subscription, so the whole file is
|
|
9
|
+
// doc/source-derived and unmeasured. That is recorded in docs/ARCHITECTURE.md §4d.
|
|
10
|
+
import crypto from "node:crypto";
|
|
11
|
+
// ── OAuth + Cloud Code Assist endpoints ─────────────────────────────────────────────────────────
|
|
12
|
+
/**
|
|
13
|
+
* The public OAuth client embedded in the Antigravity desktop client. These are client identifiers,
|
|
14
|
+
* not user secrets: opencodex ships them in the clear and they are overridable by environment there.
|
|
15
|
+
* Source: opencodex `src/oauth/google-antigravity.ts:17-38`.
|
|
16
|
+
*/
|
|
17
|
+
export const GOOGLE_OAUTH = {
|
|
18
|
+
clientId: "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com",
|
|
19
|
+
clientSecret: "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",
|
|
20
|
+
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
21
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
22
|
+
userinfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
23
|
+
scopes: [
|
|
24
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
25
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
26
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
27
|
+
"https://www.googleapis.com/auth/cclog",
|
|
28
|
+
"https://www.googleapis.com/auth/experimentsandconfigs",
|
|
29
|
+
],
|
|
30
|
+
/** Project discovery (`loadCodeAssist`) is served by the prod host. */
|
|
31
|
+
prodApi: "https://cloudcode-pa.googleapis.com",
|
|
32
|
+
/** Streaming and `onboardUser` use the daily host. Source: opencodex `src/oauth/google-antigravity.ts:24-25`. */
|
|
33
|
+
dailyApi: "https://daily-cloudcode-pa.googleapis.com",
|
|
34
|
+
apiVersion: "v1internal",
|
|
35
|
+
/** Preferred loopback port; the callback path is fixed. Source: opencodex `src/oauth/google-antigravity.ts:34-35`. */
|
|
36
|
+
port: 51121,
|
|
37
|
+
callbackPath: "/callback",
|
|
38
|
+
/** Refresh this long before the access token expires. Source: opencodex `REFRESH_SKEW_MS`. */
|
|
39
|
+
refreshLeadMs: 5 * 60 * 1000,
|
|
40
|
+
/** How long a sign-in may stay open. Same as the other sign-in flows. */
|
|
41
|
+
timeoutMs: 5 * 60 * 1000,
|
|
42
|
+
requestTimeoutMs: 30_000,
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* The terms warning the CLI prints once and the admin API returns in a `warning` field. Facts only:
|
|
46
|
+
* Google's Antigravity terms (clause 6) and the Gemini CLI FAQ state that using these credentials
|
|
47
|
+
* from a third-party client is a violation, and Google blocked a large number of accounts in
|
|
48
|
+
* February 2026. ClaudeRipple is not affiliated with Google.
|
|
49
|
+
*/
|
|
50
|
+
export const ANTIGRAVITY_WARNING = "Google's Antigravity terms (section 6) and the Gemini CLI FAQ say that using these credentials " +
|
|
51
|
+
"from a client other than Google's own is not permitted, and Google blocked a large number of " +
|
|
52
|
+
"accounts used this way in February 2026. ClaudeRipple is not affiliated with Google. Sign in only " +
|
|
53
|
+
"if you accept that risk.";
|
|
54
|
+
/** Antigravity IDE version, mirrored so the request fingerprint matches the token's client. */
|
|
55
|
+
export const ANTIGRAVITY_IDE_VERSION = "2.5.5";
|
|
56
|
+
/**
|
|
57
|
+
* The User-Agent the real Antigravity IDE sends. The CCA backend gates newer agent models on the
|
|
58
|
+
* `antigravity/ide/...` client family and answers 404 to CLI-shaped UAs even with a valid OAuth
|
|
59
|
+
* token. Source: opencodex `src/adapters/client-fingerprint.ts:44-65`, decompiled from the 2.5.5
|
|
60
|
+
* language server.
|
|
61
|
+
*/
|
|
62
|
+
export function antigravityUserAgent() {
|
|
63
|
+
const override = process.env.CLAUDERIPPLE_ANTIGRAVITY_USER_AGENT?.trim() || process.env.GOOGLE_ANTIGRAVITY_USER_AGENT?.trim();
|
|
64
|
+
if (override)
|
|
65
|
+
return override;
|
|
66
|
+
const [osType, arch] = "windows/amd64".split("/");
|
|
67
|
+
return `antigravity/ide/${ANTIGRAVITY_IDE_VERSION} (os_type=${osType}; arch=${arch}; aidev_client; auth_method=oauth)`;
|
|
68
|
+
}
|
|
69
|
+
// ── Model id resolution ─────────────────────────────────────────────────────────────────────────
|
|
70
|
+
/**
|
|
71
|
+
* Normalise a Claude Code effort onto Cloud Code Assist's three thinking levels. `xhigh`, `max` and
|
|
72
|
+
* `ultra` all clamp to `high`; `none`/`minimal` to `low`. `minimal` is not itself a CCA level.
|
|
73
|
+
* Source: opencodex `src/providers/antigravity-models.ts:resolveAntigravityThinkingLevel`.
|
|
74
|
+
*/
|
|
75
|
+
export function resolveAntigravityThinkingLevel(effort) {
|
|
76
|
+
switch (effort) {
|
|
77
|
+
case "none":
|
|
78
|
+
case "minimal":
|
|
79
|
+
case "low": return "low";
|
|
80
|
+
case "medium": return "medium";
|
|
81
|
+
case "high":
|
|
82
|
+
case "xhigh":
|
|
83
|
+
case "max":
|
|
84
|
+
case "ultra": return "high";
|
|
85
|
+
default: return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Base models whose every effort rides on a different wire id that already names the tier. Here the
|
|
90
|
+
* suffix IS the effort, so no `thinkingConfig` is sent beside it. Source: opencodex
|
|
91
|
+
* `ANTIGRAVITY_EFFORT_WIRE_MAP` / `ANTIGRAVITY_SUFFIX_TIER_MODELS`.
|
|
92
|
+
*/
|
|
93
|
+
const SUFFIX_TIER_MODELS = {
|
|
94
|
+
"gemini-3.8-flash": {
|
|
95
|
+
wire: { low: "gemini-3.8-flash-low", medium: "gemini-3.8-flash-medium", high: "gemini-3.8-flash-high" },
|
|
96
|
+
default: "medium",
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Base models whose efforts map to wire ids but where a `thinkingConfig.thinkingLevel` also travels
|
|
101
|
+
* (the high rung `gemini-pro-agent` carries no tier in its name). Source: opencodex
|
|
102
|
+
* `ANTIGRAVITY_EFFORT_WIRE_MAP` / `ANTIGRAVITY_MODEL_EFFORTS`.
|
|
103
|
+
*/
|
|
104
|
+
const EFFORT_WIRE_MODELS = {
|
|
105
|
+
"gemini-3.1-pro": { wire: { low: "gemini-3.1-pro-low", high: "gemini-pro-agent" }, default: "high" },
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Base models whose effort rides on a single `-tiered` wire id via `thinkingLevel`. Source:
|
|
109
|
+
* opencodex `ANTIGRAVITY_THINKING_LEVEL_MODELS` / `ANTIGRAVITY_PICKER_TO_WIRE`.
|
|
110
|
+
*/
|
|
111
|
+
const THINKING_LEVEL_MODELS = {
|
|
112
|
+
"gemini-3.7-flash": { wire: "gemini-3.7-flash-tiered", default: "medium" },
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Resolve a Claude Code model id onto the Cloud Code Assist wire id and thinking level. A base id
|
|
116
|
+
* (`gemini-3.8-flash`) resolves to its tier wire id; an id already naming a tier is forwarded as it
|
|
117
|
+
* is. Unknown ids pass through untouched so a model Google adds tomorrow is still directly routable.
|
|
118
|
+
*/
|
|
119
|
+
export function resolveAntigravityWireModel(model, effort) {
|
|
120
|
+
const level = resolveAntigravityThinkingLevel(effort);
|
|
121
|
+
const suffix = SUFFIX_TIER_MODELS[model];
|
|
122
|
+
if (suffix) {
|
|
123
|
+
const wire = (level && suffix.wire[level]) || suffix.wire[suffix.default];
|
|
124
|
+
// The suffix names the tier; sending `thinkingLevel` beside it would state the effort twice.
|
|
125
|
+
return { wire, known: true };
|
|
126
|
+
}
|
|
127
|
+
const mapped = EFFORT_WIRE_MODELS[model];
|
|
128
|
+
if (mapped) {
|
|
129
|
+
const wire = (level && mapped.wire[level]) || mapped.wire[mapped.default];
|
|
130
|
+
return { wire, ...(level ? { thinkingLevel: level } : {}), known: true };
|
|
131
|
+
}
|
|
132
|
+
const tiered = THINKING_LEVEL_MODELS[model];
|
|
133
|
+
if (tiered) {
|
|
134
|
+
return { wire: tiered.wire, thinkingLevel: (level ?? tiered.default), known: true };
|
|
135
|
+
}
|
|
136
|
+
// Claude models on Antigravity take effort through thinkingConfig only, no suffix — CCA validates
|
|
137
|
+
// the field against Google's ThinkingLevel enum. Source: opencodex rule 4.
|
|
138
|
+
if (/^claude-/.test(model))
|
|
139
|
+
return { wire: model, ...(level ? { thinkingLevel: level } : {}), known: true };
|
|
140
|
+
return { wire: model, known: false };
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The conversation's Cloud Code Assist session id. The real client puts one stable id on every turn
|
|
144
|
+
* of a conversation so provider-side replay can associate a thoughtSignature with the call that
|
|
145
|
+
* produced it; an unstable id loses that association. We derive it from `conversationKey`, which the
|
|
146
|
+
* router already keeps stable per conversation (and identical to the prompt-cache key), and shape it
|
|
147
|
+
* as the numeric string the wire carries — `sha256(key)` → BigEndian uint64 masked to 63 bits, with a
|
|
148
|
+
* leading "-". Source: opencodex `antigravitySessionId` (CLIProxyAPI `generateStableSessionID`).
|
|
149
|
+
*/
|
|
150
|
+
export function antigravitySessionId(conversationKey) {
|
|
151
|
+
const digest = crypto.createHash("sha256").update(conversationKey, "utf8").digest();
|
|
152
|
+
const masked = digest.readBigUInt64BE(0) & 0x7fffffffffffffffn;
|
|
153
|
+
return `-${masked.toString()}`;
|
|
154
|
+
}
|
|
155
|
+
/** A fresh per-request id, as the envelope's `requestId`. Source: opencodex `agent-${uuid}`. */
|
|
156
|
+
export function antigravityRequestId() {
|
|
157
|
+
return `agent-${crypto.randomUUID()}`;
|
|
158
|
+
}
|
|
159
|
+
// ── Model catalogue ─────────────────────────────────────────────────────────────────────────────
|
|
160
|
+
/**
|
|
161
|
+
* Context windows per model, from CCA's own `maxTokens` for the wire ids. Source: opencodex
|
|
162
|
+
* `ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS`.
|
|
163
|
+
*/
|
|
164
|
+
const CONTEXT_WINDOWS = {
|
|
165
|
+
"gemini-3.8-flash": 1_048_576,
|
|
166
|
+
"gemini-3.7-flash": 1_048_576,
|
|
167
|
+
"gemini-3.1-pro": 1_048_576,
|
|
168
|
+
"gemini-3.1-flash-image": 1_048_576,
|
|
169
|
+
"claude-sonnet-4-6": 250_000,
|
|
170
|
+
"claude-opus-4-6-thinking": 250_000,
|
|
171
|
+
"gpt-oss-120b-medium": 131_072,
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* The effort ladder per collapsed base model, for the GUI. The suffix-tier model's rungs are wire
|
|
175
|
+
* ids, so the GUI shows them as one entry with a ladder rather than three rows (opencodex does the
|
|
176
|
+
* same collapsing). Source: opencodex `ANTIGRAVITY_MODEL_EFFORTS`.
|
|
177
|
+
*/
|
|
178
|
+
const EFFORT_LEVELS = {
|
|
179
|
+
"gemini-3.8-flash": ["low", "medium", "high"],
|
|
180
|
+
"gemini-3.7-flash": ["low", "medium", "high"],
|
|
181
|
+
"gemini-3.1-pro": ["low", "high"],
|
|
182
|
+
"claude-sonnet-4-6": ["low", "medium", "high"],
|
|
183
|
+
"claude-opus-4-6-thinking": ["low", "medium", "high"],
|
|
184
|
+
};
|
|
185
|
+
/** The collapsed base models the CCA backend serves. Source: opencodex `ANTIGRAVITY_MODELS`. */
|
|
186
|
+
export const ANTIGRAVITY_BASE_MODELS = [
|
|
187
|
+
"gemini-3.8-flash",
|
|
188
|
+
"gemini-3.7-flash",
|
|
189
|
+
"gemini-3.1-pro",
|
|
190
|
+
"gemini-3.1-flash-image",
|
|
191
|
+
"claude-sonnet-4-6",
|
|
192
|
+
"claude-opus-4-6-thinking",
|
|
193
|
+
"gpt-oss-120b-medium",
|
|
194
|
+
];
|
|
195
|
+
/** The static fallback list, used when `:fetchAvailableModels` cannot be read. */
|
|
196
|
+
export function staticAntigravityModels() {
|
|
197
|
+
return ANTIGRAVITY_BASE_MODELS.map((id) => ({
|
|
198
|
+
id,
|
|
199
|
+
...(CONTEXT_WINDOWS[id] ? { contextWindow: CONTEXT_WINDOWS[id] } : {}),
|
|
200
|
+
...(EFFORT_LEVELS[id] ? { effortLevels: [...EFFORT_LEVELS[id]] } : {}),
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
function isBaseModel(id) {
|
|
204
|
+
return ANTIGRAVITY_BASE_MODELS.includes(id);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Collapse a wire id back to the collapsed base id the picker shows, when it names a known tier. The
|
|
208
|
+
* collapsed id is what the static catalogue lists and what `resolveAntigravityWireModel` maps back to
|
|
209
|
+
* a wire id, so the picker shows one row per model and routing is the same whether the list came from
|
|
210
|
+
* discovery or the fallback.
|
|
211
|
+
*/
|
|
212
|
+
function collapseWireId(wireId) {
|
|
213
|
+
if (isBaseModel(wireId))
|
|
214
|
+
return wireId;
|
|
215
|
+
const tiered = wireId.endsWith("-tiered") ? wireId.slice(0, -"-tiered".length) : undefined;
|
|
216
|
+
if (tiered && isBaseModel(tiered))
|
|
217
|
+
return tiered;
|
|
218
|
+
const match = /^(.*)-(low|medium|high|max|minimal)$/.exec(wireId);
|
|
219
|
+
if (match && isBaseModel(match[1]))
|
|
220
|
+
return match[1];
|
|
221
|
+
return wireId;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Parse `POST {dailyApi}/v1internal:fetchAvailableModels`. The body groups agent-callable models
|
|
225
|
+
* under `agentModelSorts[].groups[].modelIds`, with tiered Flash ids under `tieredModelIds.flash`, and
|
|
226
|
+
* describes each in `models[wireId]` (`maxTokens`). Source: opencodex
|
|
227
|
+
* `parseAntigravityAvailableModels` (src/providers/antigravity-models.ts:489-560).
|
|
228
|
+
*
|
|
229
|
+
* Returns null when the shape is unreadable, so the caller falls back instead of showing nothing.
|
|
230
|
+
*/
|
|
231
|
+
export function parseAvailableModels(payload, limit = 200) {
|
|
232
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
233
|
+
return null;
|
|
234
|
+
const body = payload;
|
|
235
|
+
const models = body.models;
|
|
236
|
+
if (!models || typeof models !== "object" || Array.isArray(models))
|
|
237
|
+
return null;
|
|
238
|
+
const known = models;
|
|
239
|
+
const available = new Set(Object.keys(known));
|
|
240
|
+
// Collapse each wire id to its base id first, then take the context window and display name from
|
|
241
|
+
// the first wire id that carried that base. A collapsed id is what routing understands.
|
|
242
|
+
const collapsed = new Map();
|
|
243
|
+
const add = (wireId) => {
|
|
244
|
+
if (typeof wireId !== "string" || !available.has(wireId))
|
|
245
|
+
return;
|
|
246
|
+
const id = collapseWireId(wireId);
|
|
247
|
+
if (!collapsed.has(id))
|
|
248
|
+
collapsed.set(id, wireId);
|
|
249
|
+
};
|
|
250
|
+
const sorts = Array.isArray(body.agentModelSorts) ? body.agentModelSorts : [];
|
|
251
|
+
for (const sort of sorts) {
|
|
252
|
+
const groups = sort && typeof sort === "object" ? sort.groups : undefined;
|
|
253
|
+
if (!Array.isArray(groups))
|
|
254
|
+
continue;
|
|
255
|
+
for (const group of groups) {
|
|
256
|
+
const ids = group && typeof group === "object" ? group.modelIds : undefined;
|
|
257
|
+
if (Array.isArray(ids))
|
|
258
|
+
for (const id of ids)
|
|
259
|
+
add(id);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const tiered = body.tieredModelIds && typeof body.tieredModelIds === "object" ? body.tieredModelIds.flash : undefined;
|
|
263
|
+
if (Array.isArray(tiered))
|
|
264
|
+
for (const id of tiered)
|
|
265
|
+
add(id);
|
|
266
|
+
if (collapsed.size === 0)
|
|
267
|
+
return null;
|
|
268
|
+
const out = [];
|
|
269
|
+
for (const [id, wireId] of collapsed) {
|
|
270
|
+
const info = known[wireId] && typeof known[wireId] === "object" ? known[wireId] : undefined;
|
|
271
|
+
const contextWindow = typeof info?.maxTokens === "number" && Number.isFinite(info.maxTokens) && info.maxTokens > 0 ? Math.floor(info.maxTokens) : CONTEXT_WINDOWS[id];
|
|
272
|
+
out.push({
|
|
273
|
+
id,
|
|
274
|
+
...(typeof info?.displayName === "string" && info.displayName ? { name: info.displayName } : {}),
|
|
275
|
+
...(contextWindow ? { contextWindow } : {}),
|
|
276
|
+
...(EFFORT_LEVELS[id] ? { effortLevels: [...EFFORT_LEVELS[id]] } : {}),
|
|
277
|
+
});
|
|
278
|
+
if (out.length >= limit)
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
return out.length > 0 ? out : null;
|
|
282
|
+
}
|
|
283
|
+
function extractProjectId(data) {
|
|
284
|
+
if (!data)
|
|
285
|
+
return undefined;
|
|
286
|
+
for (const key of ["cloudaicompanionProject", "projectId", "project"]) {
|
|
287
|
+
const value = data[key];
|
|
288
|
+
if (typeof value === "string" && value.length > 0)
|
|
289
|
+
return value;
|
|
290
|
+
if (value && typeof value === "object" && typeof value.id === "string")
|
|
291
|
+
return value.id;
|
|
292
|
+
}
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
function signalFor(outer) {
|
|
296
|
+
const timeout = AbortSignal.timeout(GOOGLE_OAUTH.requestTimeoutMs);
|
|
297
|
+
return outer ? AbortSignal.any([outer, timeout]) : timeout;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Discover the account's Cloud Code Assist project: `loadCodeAssist` on the prod host, then
|
|
301
|
+
* `onboardUser` polling on the daily host when the account has none yet. Returns undefined when
|
|
302
|
+
* neither yields one. Source: opencodex `loadCodeAssistProject` / `onboardProject`.
|
|
303
|
+
*/
|
|
304
|
+
export async function discoverProject(accessToken, fetchImpl = fetch, signal, sleep = (ms) => new Promise((r) => setTimeout(r, ms))) {
|
|
305
|
+
const loaded = await loadCodeAssistProject(accessToken, fetchImpl, signal);
|
|
306
|
+
if (loaded)
|
|
307
|
+
return loaded;
|
|
308
|
+
return onboardProject(accessToken, fetchImpl, signal, sleep);
|
|
309
|
+
}
|
|
310
|
+
async function loadCodeAssistProject(accessToken, fetchImpl, signal) {
|
|
311
|
+
try {
|
|
312
|
+
const response = await fetchImpl(`${GOOGLE_OAUTH.prodApi}/${GOOGLE_OAUTH.apiVersion}:loadCodeAssist`, {
|
|
313
|
+
method: "POST",
|
|
314
|
+
headers: { authorization: `Bearer ${accessToken}`, accept: "*/*", "content-type": "application/json", "user-agent": antigravityUserAgent() },
|
|
315
|
+
body: JSON.stringify({ metadata: { ideType: "ANTIGRAVITY" } }),
|
|
316
|
+
signal: signalFor(signal),
|
|
317
|
+
});
|
|
318
|
+
if (!response.ok)
|
|
319
|
+
return undefined;
|
|
320
|
+
return extractProjectId((await response.json().catch(() => undefined)));
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const ONBOARD_ATTEMPTS = 5;
|
|
327
|
+
const ONBOARD_POLL_MS = 2_000;
|
|
328
|
+
async function onboardProject(accessToken, fetchImpl, signal, sleep) {
|
|
329
|
+
for (let attempt = 0; attempt < ONBOARD_ATTEMPTS; attempt++) {
|
|
330
|
+
if (signal?.aborted)
|
|
331
|
+
return undefined;
|
|
332
|
+
let response;
|
|
333
|
+
try {
|
|
334
|
+
response = await fetchImpl(`${GOOGLE_OAUTH.dailyApi}/${GOOGLE_OAUTH.apiVersion}:onboardUser`, {
|
|
335
|
+
method: "POST",
|
|
336
|
+
headers: { authorization: `Bearer ${accessToken}`, accept: "*/*", "content-type": "application/json", "user-agent": antigravityUserAgent() },
|
|
337
|
+
body: JSON.stringify({ tier_id: "free-tier", metadata: { ide_type: "ANTIGRAVITY", ide_name: "antigravity", ide_version: ANTIGRAVITY_IDE_VERSION } }),
|
|
338
|
+
signal: signalFor(signal),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
if (!response.ok) {
|
|
345
|
+
// A transient refusal (429/5xx) is worth polling through; a hard 4xx will not change.
|
|
346
|
+
if (response.status === 429 || response.status >= 500) {
|
|
347
|
+
await sleep(ONBOARD_POLL_MS);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
const data = (await response.json().catch(() => ({})));
|
|
353
|
+
if (data.done === true)
|
|
354
|
+
return extractProjectId(data.response);
|
|
355
|
+
await sleep(ONBOARD_POLL_MS);
|
|
356
|
+
}
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
/** The account email from Google's pinned userinfo endpoint. Never persisted as a token. */
|
|
360
|
+
export async function fetchUserEmail(accessToken, fetchImpl = fetch, signal) {
|
|
361
|
+
try {
|
|
362
|
+
const response = await fetchImpl(GOOGLE_OAUTH.userinfoUrl, {
|
|
363
|
+
method: "GET",
|
|
364
|
+
headers: { authorization: `Bearer ${accessToken}`, accept: "application/json" },
|
|
365
|
+
signal: signalFor(signal),
|
|
366
|
+
});
|
|
367
|
+
if (!response.ok)
|
|
368
|
+
return undefined;
|
|
369
|
+
const body = (await response.json().catch(() => undefined));
|
|
370
|
+
return typeof body?.email === "string" && body.email.length > 0 ? body.email.toLowerCase() : undefined;
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/** The models a CCA account can reach now, or null when the endpoint cannot be read. */
|
|
377
|
+
export async function fetchAvailableModels(accessToken, projectId, fetchImpl = fetch, signal) {
|
|
378
|
+
try {
|
|
379
|
+
const response = await fetchImpl(`${GOOGLE_OAUTH.dailyApi}/${GOOGLE_OAUTH.apiVersion}:fetchAvailableModels`, {
|
|
380
|
+
method: "POST",
|
|
381
|
+
headers: { authorization: `Bearer ${accessToken}`, accept: "*/*", "content-type": "application/json", "user-agent": antigravityUserAgent() },
|
|
382
|
+
body: JSON.stringify({ project: projectId }),
|
|
383
|
+
signal: signalFor(signal),
|
|
384
|
+
});
|
|
385
|
+
if (!response.ok)
|
|
386
|
+
return null;
|
|
387
|
+
return parseAvailableModels(await response.json().catch(() => undefined));
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|