opencode-gemini-oauth 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +502 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# opencode-gemini-oauth
|
|
2
|
+
|
|
3
|
+
OpenCode plugin for Google Gemini authentication via OAuth. Use your Google AI Pro subscription quota without API billing.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- OAuth PKCE authentication with Google
|
|
8
|
+
- Automatic token refresh
|
|
9
|
+
- Request interception and transformation for Antigravity/Code Assist API
|
|
10
|
+
- Token storage with secure local file
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Add to your `opencode.json`:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"plugins": ["opencode-gemini-oauth@latest"]
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or use the local path for development:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"plugins": ["D:\\git\\opencode-gemini-oauth"]
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
In your `opencode.json`, configure the Google provider:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"provider": {
|
|
37
|
+
"google": {
|
|
38
|
+
"npm": "opencode-gemini-oauth"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
1. Start OpenCode
|
|
47
|
+
2. Select "OAuth with Google (Gemini)" authentication method
|
|
48
|
+
3. Complete the OAuth flow in your browser
|
|
49
|
+
4. Your Google AI Pro quota will be used for requests
|
|
50
|
+
|
|
51
|
+
## Supported Models
|
|
52
|
+
|
|
53
|
+
- `gemini-2.5-pro`
|
|
54
|
+
- `gemini-2.5-flash`
|
|
55
|
+
- `gemini-2.0-flash`
|
|
56
|
+
- And other Gemini models available through your subscription
|
|
57
|
+
|
|
58
|
+
## How It Works
|
|
59
|
+
|
|
60
|
+
This plugin:
|
|
61
|
+
1. Authenticates via Google OAuth using PKCE flow
|
|
62
|
+
2. Stores refresh tokens locally in `%APPDATA%/opencode/gemini-oauth-accounts.json`
|
|
63
|
+
3. Intercepts API requests to `generativelanguage.googleapis.com`
|
|
64
|
+
4. Transforms them to use the Code Assist endpoint with your OAuth token
|
|
65
|
+
5. Automatically refreshes expired tokens
|
|
66
|
+
|
|
67
|
+
## Credits
|
|
68
|
+
|
|
69
|
+
Based on the Antigravity OAuth flow used by Google's AI Studio and Cloud Code extensions.
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
// src/oauth.ts
|
|
2
|
+
import { createServer } from "http";
|
|
3
|
+
import { randomBytes, createHash } from "crypto";
|
|
4
|
+
import { URL as URL2, URLSearchParams as URLSearchParams2 } from "url";
|
|
5
|
+
|
|
6
|
+
// src/constants.ts
|
|
7
|
+
var OAUTH_CONFIG = {
|
|
8
|
+
clientId: "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com",
|
|
9
|
+
clientSecret: "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",
|
|
10
|
+
redirectUri: "http://localhost:36742/oauth-callback",
|
|
11
|
+
port: 36742,
|
|
12
|
+
scopes: [
|
|
13
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
14
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
15
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
16
|
+
"https://www.googleapis.com/auth/cclog",
|
|
17
|
+
"https://www.googleapis.com/auth/experimentsandconfigs"
|
|
18
|
+
],
|
|
19
|
+
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
20
|
+
tokenUrl: "https://oauth2.googleapis.com/token"
|
|
21
|
+
};
|
|
22
|
+
var CODE_ASSIST_ENDPOINTS = [
|
|
23
|
+
"https://autopush-cloudcode-pa.sandbox.googleapis.com",
|
|
24
|
+
"https://cloudcode-pa.googleapis.com"
|
|
25
|
+
];
|
|
26
|
+
var CODE_ASSIST_HEADERS = {
|
|
27
|
+
"User-Agent": "antigravity/1.11.5",
|
|
28
|
+
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1"
|
|
29
|
+
};
|
|
30
|
+
var STORAGE_FILE = "gemini-oauth-accounts.json";
|
|
31
|
+
var TOKEN_REFRESH_BUFFER_MS = 60 * 1e3;
|
|
32
|
+
|
|
33
|
+
// src/storage.ts
|
|
34
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
35
|
+
import { join } from "path";
|
|
36
|
+
import { homedir } from "os";
|
|
37
|
+
function getStoragePath() {
|
|
38
|
+
const configDir = process.env.APPDATA || (process.platform === "darwin" ? join(homedir(), "Library", "Application Support") : join(homedir(), ".config"));
|
|
39
|
+
const opencodeDir = join(configDir, "opencode");
|
|
40
|
+
if (!existsSync(opencodeDir)) {
|
|
41
|
+
mkdirSync(opencodeDir, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
return join(opencodeDir, STORAGE_FILE);
|
|
44
|
+
}
|
|
45
|
+
function loadStorage() {
|
|
46
|
+
const path = getStoragePath();
|
|
47
|
+
if (!existsSync(path)) {
|
|
48
|
+
return { version: 1, accounts: [], activeIndex: 0 };
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
52
|
+
return {
|
|
53
|
+
version: 1,
|
|
54
|
+
accounts: data.accounts || [],
|
|
55
|
+
activeIndex: data.activeIndex || 0
|
|
56
|
+
};
|
|
57
|
+
} catch {
|
|
58
|
+
return { version: 1, accounts: [], activeIndex: 0 };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function saveStorage(data) {
|
|
62
|
+
const path = getStoragePath();
|
|
63
|
+
writeFileSync(path, JSON.stringify(data, null, 2), "utf-8");
|
|
64
|
+
}
|
|
65
|
+
function getActiveAccount() {
|
|
66
|
+
const storage = loadStorage();
|
|
67
|
+
if (storage.accounts.length === 0) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return storage.accounts[storage.activeIndex] || storage.accounts[0] || null;
|
|
71
|
+
}
|
|
72
|
+
function addOrUpdateAccount(account) {
|
|
73
|
+
const storage = loadStorage();
|
|
74
|
+
const existingIndex = storage.accounts.findIndex(
|
|
75
|
+
(a) => a.email === account.email
|
|
76
|
+
);
|
|
77
|
+
if (existingIndex >= 0) {
|
|
78
|
+
storage.accounts[existingIndex] = account;
|
|
79
|
+
storage.activeIndex = existingIndex;
|
|
80
|
+
} else {
|
|
81
|
+
storage.accounts.push(account);
|
|
82
|
+
storage.activeIndex = storage.accounts.length - 1;
|
|
83
|
+
}
|
|
84
|
+
saveStorage(storage);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/oauth.ts
|
|
88
|
+
function generatePKCE() {
|
|
89
|
+
const verifier = randomBytes(32).toString("base64url").replace(/[^a-zA-Z0-9]/g, "").substring(0, 43);
|
|
90
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url").replace(/[^a-zA-Z0-9\-_]/g, "");
|
|
91
|
+
return { verifier, challenge };
|
|
92
|
+
}
|
|
93
|
+
function generateState() {
|
|
94
|
+
return randomBytes(16).toString("hex");
|
|
95
|
+
}
|
|
96
|
+
function buildAuthUrl(state, codeChallenge) {
|
|
97
|
+
const params = new URLSearchParams2({
|
|
98
|
+
client_id: OAUTH_CONFIG.clientId,
|
|
99
|
+
redirect_uri: OAUTH_CONFIG.redirectUri,
|
|
100
|
+
response_type: "code",
|
|
101
|
+
scope: OAUTH_CONFIG.scopes.join(" "),
|
|
102
|
+
state,
|
|
103
|
+
code_challenge: codeChallenge,
|
|
104
|
+
code_challenge_method: "S256",
|
|
105
|
+
access_type: "offline",
|
|
106
|
+
prompt: "consent"
|
|
107
|
+
});
|
|
108
|
+
return `${OAUTH_CONFIG.authUrl}?${params.toString()}`;
|
|
109
|
+
}
|
|
110
|
+
async function exchangeCodeForTokens(code, codeVerifier) {
|
|
111
|
+
const response = await fetch(OAUTH_CONFIG.tokenUrl, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: {
|
|
114
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
115
|
+
},
|
|
116
|
+
body: new URLSearchParams2({
|
|
117
|
+
client_id: OAUTH_CONFIG.clientId,
|
|
118
|
+
client_secret: OAUTH_CONFIG.clientSecret,
|
|
119
|
+
code,
|
|
120
|
+
code_verifier: codeVerifier,
|
|
121
|
+
grant_type: "authorization_code",
|
|
122
|
+
redirect_uri: OAUTH_CONFIG.redirectUri
|
|
123
|
+
}).toString()
|
|
124
|
+
});
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
const error = await response.text();
|
|
127
|
+
throw new Error(`Token exchange failed: ${error}`);
|
|
128
|
+
}
|
|
129
|
+
const data = await response.json();
|
|
130
|
+
return {
|
|
131
|
+
accessToken: data.access_token,
|
|
132
|
+
refreshToken: data.refresh_token,
|
|
133
|
+
expiresIn: data.expires_in
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
async function getUserInfo(accessToken) {
|
|
137
|
+
const response = await fetch(
|
|
138
|
+
"https://www.googleapis.com/oauth2/v2/userinfo",
|
|
139
|
+
{
|
|
140
|
+
headers: {
|
|
141
|
+
Authorization: `Bearer ${accessToken}`
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
throw new Error("Failed to get user info");
|
|
147
|
+
}
|
|
148
|
+
const data = await response.json();
|
|
149
|
+
return { email: data.email, name: data.name };
|
|
150
|
+
}
|
|
151
|
+
async function getProjectId(accessToken) {
|
|
152
|
+
try {
|
|
153
|
+
const response = await fetch(
|
|
154
|
+
"https://cloudcode-pa.googleapis.com/v1/userWorkspace",
|
|
155
|
+
{
|
|
156
|
+
headers: {
|
|
157
|
+
Authorization: `Bearer ${accessToken}`,
|
|
158
|
+
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1"
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
if (response.ok) {
|
|
163
|
+
const data = await response.json();
|
|
164
|
+
return data.managedProjectId;
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
}
|
|
168
|
+
return void 0;
|
|
169
|
+
}
|
|
170
|
+
async function startOAuthFlow() {
|
|
171
|
+
const { verifier, challenge } = generatePKCE();
|
|
172
|
+
const state = generateState();
|
|
173
|
+
const authUrl = buildAuthUrl(state, challenge);
|
|
174
|
+
return new Promise((resolveSetup, rejectSetup) => {
|
|
175
|
+
let callbackResolve;
|
|
176
|
+
let callbackReject;
|
|
177
|
+
const callbackPromise = new Promise((resolve, reject) => {
|
|
178
|
+
callbackResolve = resolve;
|
|
179
|
+
callbackReject = reject;
|
|
180
|
+
});
|
|
181
|
+
const server = createServer(async (req, res) => {
|
|
182
|
+
try {
|
|
183
|
+
const url = new URL2(req.url || "/", `http://localhost:${OAUTH_CONFIG.port}`);
|
|
184
|
+
if (url.pathname === "/oauth-callback") {
|
|
185
|
+
const code = url.searchParams.get("code");
|
|
186
|
+
const returnedState = url.searchParams.get("state");
|
|
187
|
+
const error = url.searchParams.get("error");
|
|
188
|
+
if (error) {
|
|
189
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
190
|
+
res.end(`
|
|
191
|
+
<html>
|
|
192
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
193
|
+
<h1 style="color: #dc2626;">Authentication Failed</h1>
|
|
194
|
+
<p>Error: ${error}</p>
|
|
195
|
+
<p>You can close this window.</p>
|
|
196
|
+
</body>
|
|
197
|
+
</html>
|
|
198
|
+
`);
|
|
199
|
+
callbackReject(new Error(`OAuth error: ${error}`));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (!code || returnedState !== state) {
|
|
203
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
204
|
+
res.end(`
|
|
205
|
+
<html>
|
|
206
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
207
|
+
<h1 style="color: #dc2626;">Invalid Response</h1>
|
|
208
|
+
<p>Missing code or invalid state.</p>
|
|
209
|
+
<p>You can close this window.</p>
|
|
210
|
+
</body>
|
|
211
|
+
</html>
|
|
212
|
+
`);
|
|
213
|
+
callbackReject(new Error("Invalid OAuth response"));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const tokens = await exchangeCodeForTokens(code, verifier);
|
|
217
|
+
const userInfo = await getUserInfo(tokens.accessToken);
|
|
218
|
+
const projectId = await getProjectId(tokens.accessToken);
|
|
219
|
+
const account = {
|
|
220
|
+
email: userInfo.email,
|
|
221
|
+
refreshToken: tokens.refreshToken,
|
|
222
|
+
accessToken: tokens.accessToken,
|
|
223
|
+
expiresAt: Date.now() + tokens.expiresIn * 1e3,
|
|
224
|
+
projectId,
|
|
225
|
+
managedProjectId: projectId
|
|
226
|
+
};
|
|
227
|
+
addOrUpdateAccount(account);
|
|
228
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
229
|
+
res.end(`
|
|
230
|
+
<html>
|
|
231
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
232
|
+
<h1 style="color: #16a34a;">Authentication Successful!</h1>
|
|
233
|
+
<p>Logged in as: <strong>${userInfo.email}</strong></p>
|
|
234
|
+
<p>You can close this window and return to OpenCode.</p>
|
|
235
|
+
<script>setTimeout(() => window.close(), 2000);</script>
|
|
236
|
+
</body>
|
|
237
|
+
</html>
|
|
238
|
+
`);
|
|
239
|
+
callbackResolve(account);
|
|
240
|
+
} else {
|
|
241
|
+
res.writeHead(404);
|
|
242
|
+
res.end("Not found");
|
|
243
|
+
}
|
|
244
|
+
} catch (err) {
|
|
245
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
246
|
+
res.end(`
|
|
247
|
+
<html>
|
|
248
|
+
<body style="font-family: system-ui; padding: 40px; text-align: center;">
|
|
249
|
+
<h1 style="color: #dc2626;">Error</h1>
|
|
250
|
+
<p>${err instanceof Error ? err.message : "Unknown error"}</p>
|
|
251
|
+
<p>You can close this window.</p>
|
|
252
|
+
</body>
|
|
253
|
+
</html>
|
|
254
|
+
`);
|
|
255
|
+
callbackReject(err instanceof Error ? err : new Error("Unknown error"));
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
server.listen(OAUTH_CONFIG.port, async () => {
|
|
259
|
+
try {
|
|
260
|
+
const open = (await import("open")).default;
|
|
261
|
+
await open(authUrl);
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
resolveSetup({
|
|
265
|
+
authUrl,
|
|
266
|
+
server,
|
|
267
|
+
waitForCallback: () => callbackPromise
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
server.on("error", (err) => {
|
|
271
|
+
rejectSetup(new Error(`Failed to start OAuth server: ${err.message}`));
|
|
272
|
+
});
|
|
273
|
+
setTimeout(() => {
|
|
274
|
+
server.close();
|
|
275
|
+
callbackReject(new Error("OAuth flow timed out"));
|
|
276
|
+
}, 5 * 60 * 1e3);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/token.ts
|
|
281
|
+
function isTokenExpired(account) {
|
|
282
|
+
if (!account.accessToken || !account.accessTokenExpiry) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
return Date.now() >= account.accessTokenExpiry - TOKEN_REFRESH_BUFFER_MS;
|
|
286
|
+
}
|
|
287
|
+
async function refreshAccessToken(account) {
|
|
288
|
+
const response = await fetch(OAUTH_CONFIG.tokenUrl, {
|
|
289
|
+
method: "POST",
|
|
290
|
+
headers: {
|
|
291
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
292
|
+
},
|
|
293
|
+
body: new URLSearchParams({
|
|
294
|
+
client_id: OAUTH_CONFIG.clientId,
|
|
295
|
+
client_secret: OAUTH_CONFIG.clientSecret,
|
|
296
|
+
refresh_token: account.refreshToken,
|
|
297
|
+
grant_type: "refresh_token"
|
|
298
|
+
}).toString()
|
|
299
|
+
});
|
|
300
|
+
if (!response.ok) {
|
|
301
|
+
const error = await response.text();
|
|
302
|
+
throw new Error(`Token refresh failed: ${error}`);
|
|
303
|
+
}
|
|
304
|
+
const data = await response.json();
|
|
305
|
+
const expiresAt = Date.now() + data.expires_in * 1e3;
|
|
306
|
+
const updatedAccount = {
|
|
307
|
+
...account,
|
|
308
|
+
accessToken: data.access_token,
|
|
309
|
+
accessTokenExpiry: expiresAt
|
|
310
|
+
};
|
|
311
|
+
addOrUpdateAccount(updatedAccount);
|
|
312
|
+
return {
|
|
313
|
+
accessToken: data.access_token,
|
|
314
|
+
expiresAt
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
async function getValidAccessToken() {
|
|
318
|
+
const account = getActiveAccount();
|
|
319
|
+
if (!account) {
|
|
320
|
+
throw new Error("No authenticated account. Please run OAuth login first.");
|
|
321
|
+
}
|
|
322
|
+
if (isTokenExpired(account)) {
|
|
323
|
+
const { accessToken } = await refreshAccessToken(account);
|
|
324
|
+
return accessToken;
|
|
325
|
+
}
|
|
326
|
+
return account.accessToken;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/fetch-wrapper.ts
|
|
330
|
+
var MODEL_ALIASES = {
|
|
331
|
+
"gemini-3-pro-preview": "gemini-3-pro-high",
|
|
332
|
+
"gemini-3-flash-preview": "gemini-3-flash",
|
|
333
|
+
"gemini-2.5-pro": "gemini-2.5-pro-exp-03-25",
|
|
334
|
+
"gemini-2.5-flash": "gemini-2.5-flash",
|
|
335
|
+
"gemini-2.5-flash-lite": "gemini-2.5-flash-lite-001",
|
|
336
|
+
// Claude models via Antigravity
|
|
337
|
+
"gemini-claude-sonnet-4-5": "claude-sonnet-4-5",
|
|
338
|
+
"gemini-claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
|
|
339
|
+
"gemini-claude-opus-4-5": "claude-opus-4-5",
|
|
340
|
+
"gemini-claude-opus-4-5-thinking": "claude-opus-4-5-thinking"
|
|
341
|
+
};
|
|
342
|
+
function extractAction(url) {
|
|
343
|
+
const match = url.match(/:(\w+)(?:\?|$)/);
|
|
344
|
+
return match ? match[1] : null;
|
|
345
|
+
}
|
|
346
|
+
function transformRequestBody(body) {
|
|
347
|
+
try {
|
|
348
|
+
const parsed = JSON.parse(body);
|
|
349
|
+
if (parsed.model) {
|
|
350
|
+
const modelName = parsed.model.replace(/^models\//, "");
|
|
351
|
+
parsed.model = `models/${MODEL_ALIASES[modelName] || modelName}`;
|
|
352
|
+
}
|
|
353
|
+
return JSON.stringify(parsed);
|
|
354
|
+
} catch {
|
|
355
|
+
return body;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function createAntigravityFetch() {
|
|
359
|
+
let currentEndpointIndex = 0;
|
|
360
|
+
const antigravityFetch = async (input, init) => {
|
|
361
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
362
|
+
if (!url.includes("generativelanguage.googleapis.com")) {
|
|
363
|
+
return fetch(input, init);
|
|
364
|
+
}
|
|
365
|
+
const action = extractAction(url);
|
|
366
|
+
if (!action) {
|
|
367
|
+
return fetch(input, init);
|
|
368
|
+
}
|
|
369
|
+
let accessToken;
|
|
370
|
+
try {
|
|
371
|
+
accessToken = await getValidAccessToken();
|
|
372
|
+
} catch (error) {
|
|
373
|
+
throw new Error(
|
|
374
|
+
`Failed to get access token: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
let body = init?.body;
|
|
378
|
+
if (typeof body === "string") {
|
|
379
|
+
body = transformRequestBody(body);
|
|
380
|
+
}
|
|
381
|
+
let lastError = null;
|
|
382
|
+
for (let i = 0; i < CODE_ASSIST_ENDPOINTS.length; i++) {
|
|
383
|
+
const endpointIndex = (currentEndpointIndex + i) % CODE_ASSIST_ENDPOINTS.length;
|
|
384
|
+
const endpoint = CODE_ASSIST_ENDPOINTS[endpointIndex];
|
|
385
|
+
const targetUrl = `${endpoint}/v1internal:${action}`;
|
|
386
|
+
try {
|
|
387
|
+
const response = await fetch(targetUrl, {
|
|
388
|
+
method: init?.method || "POST",
|
|
389
|
+
headers: {
|
|
390
|
+
...CODE_ASSIST_HEADERS,
|
|
391
|
+
"Content-Type": "application/json",
|
|
392
|
+
Authorization: `Bearer ${accessToken}`,
|
|
393
|
+
...init?.headers
|
|
394
|
+
},
|
|
395
|
+
body,
|
|
396
|
+
signal: init?.signal
|
|
397
|
+
});
|
|
398
|
+
if (response.ok || response.status >= 400 && response.status < 500) {
|
|
399
|
+
currentEndpointIndex = endpointIndex;
|
|
400
|
+
return response;
|
|
401
|
+
}
|
|
402
|
+
if (response.status >= 500) {
|
|
403
|
+
lastError = new Error(`Server error: ${response.status}`);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
return response;
|
|
407
|
+
} catch (error) {
|
|
408
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
throw lastError || new Error("All Code Assist endpoints failed");
|
|
413
|
+
};
|
|
414
|
+
return antigravityFetch;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/index.ts
|
|
418
|
+
var GeminiOAuthPlugin = async (_input) => {
|
|
419
|
+
const authHook = {
|
|
420
|
+
provider: "google",
|
|
421
|
+
loader: async (_auth, _provider) => {
|
|
422
|
+
const account = getActiveAccount();
|
|
423
|
+
if (!account) {
|
|
424
|
+
return {
|
|
425
|
+
apiKey: ""
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
apiKey: "",
|
|
430
|
+
fetch: createAntigravityFetch()
|
|
431
|
+
};
|
|
432
|
+
},
|
|
433
|
+
methods: [
|
|
434
|
+
{
|
|
435
|
+
type: "oauth",
|
|
436
|
+
label: "OAuth with Google (Gemini Pro)",
|
|
437
|
+
authorize: async () => {
|
|
438
|
+
const { authUrl, waitForCallback, server } = await startOAuthFlow();
|
|
439
|
+
return {
|
|
440
|
+
url: authUrl,
|
|
441
|
+
instructions: "Opening browser for Google authentication. Please sign in with your Google AI Pro account.",
|
|
442
|
+
method: "auto",
|
|
443
|
+
callback: async () => {
|
|
444
|
+
try {
|
|
445
|
+
const account = await waitForCallback();
|
|
446
|
+
return {
|
|
447
|
+
type: "success",
|
|
448
|
+
refresh: account.refreshToken,
|
|
449
|
+
access: account.accessToken || "",
|
|
450
|
+
expires: account.expiresAt || Date.now() + 3600 * 1e3
|
|
451
|
+
};
|
|
452
|
+
} catch (error) {
|
|
453
|
+
console.error("OAuth failed:", error);
|
|
454
|
+
return { type: "failed" };
|
|
455
|
+
} finally {
|
|
456
|
+
server.close();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
type: "api",
|
|
464
|
+
label: "Manually enter API Key",
|
|
465
|
+
prompts: [
|
|
466
|
+
{
|
|
467
|
+
type: "text",
|
|
468
|
+
key: "apiKey",
|
|
469
|
+
message: "Enter your Google API Key:",
|
|
470
|
+
placeholder: "AIza...",
|
|
471
|
+
validate: (value) => {
|
|
472
|
+
if (!value || value.trim().length === 0) {
|
|
473
|
+
return "API Key is required";
|
|
474
|
+
}
|
|
475
|
+
if (!value.startsWith("AIza")) {
|
|
476
|
+
return "Invalid Google API Key format";
|
|
477
|
+
}
|
|
478
|
+
return void 0;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
],
|
|
482
|
+
authorize: async (inputs) => {
|
|
483
|
+
const apiKey = inputs?.apiKey;
|
|
484
|
+
if (!apiKey) {
|
|
485
|
+
return { type: "failed" };
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
type: "success",
|
|
489
|
+
key: apiKey
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
};
|
|
495
|
+
return {
|
|
496
|
+
auth: authHook
|
|
497
|
+
};
|
|
498
|
+
};
|
|
499
|
+
var index_default = GeminiOAuthPlugin;
|
|
500
|
+
export {
|
|
501
|
+
index_default as default
|
|
502
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-gemini-oauth",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OpenCode plugin for Google Gemini OAuth authentication (Antigravity)",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
10
|
+
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
11
|
+
"prepublishOnly": "npm run build"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"opencode",
|
|
15
|
+
"opencode-plugin",
|
|
16
|
+
"gemini",
|
|
17
|
+
"google",
|
|
18
|
+
"oauth",
|
|
19
|
+
"antigravity"
|
|
20
|
+
],
|
|
21
|
+
"author": "Marquinho",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/Marquinho/opencode-gemini-oauth"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"open": "^10.1.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@opencode-ai/plugin": "^1.0.182",
|
|
32
|
+
"@types/node": "^22.10.2",
|
|
33
|
+
"tsup": "^8.3.5",
|
|
34
|
+
"typescript": "^5.7.2"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@opencode-ai/plugin": "^1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist"
|
|
44
|
+
]
|
|
45
|
+
}
|