opencode-gemini-auth 1.1.6 → 1.3.3

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 CHANGED
@@ -1,96 +1,173 @@
1
1
  # Gemini OAuth Plugin for Opencode
2
2
 
3
- Authenticate the Opencode CLI with your Google account so you can use your
4
- existing Gemini plan and its included quota instead of API billing.
3
+ ![License](https://img.shields.io/npm/l/opencode-gemini-auth)
4
+ ![Version](https://img.shields.io/npm/v/opencode-gemini-auth)
5
5
 
6
- ## Setup
6
+ **Authenticate the Opencode CLI with your Google account.** This plugin enables
7
+ you to use your existing Gemini plan and quotas (including the free tier)
8
+ directly within Opencode, bypassing separate API billing.
7
9
 
8
- 1. Add the plugin to your [Opencode config](https://opencode.ai/docs/config/):
10
+ ## Prerequisites
9
11
 
10
- ```json
11
- {
12
- "$schema": "https://opencode.ai/config.json",
13
- "plugin": ["opencode-gemini-auth"]
14
- }
12
+ - [Opencode CLI](https://opencode.ai) installed.
13
+ - A Google account with access to Gemini.
14
+
15
+ ## Installation
16
+
17
+ Add the plugin to your Opencode configuration file
18
+ (`~/.config/opencode/config.json` or similar):
19
+
20
+ ```json
21
+ {
22
+ "$schema": "https://opencode.ai/config.json",
23
+ "plugin": ["opencode-gemini-auth"]
24
+ }
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ 1. **Login**: Run the authentication command in your terminal:
30
+
31
+ ```bash
32
+ opencode auth login
15
33
  ```
16
34
 
17
- 2. Run `opencode auth login`.
18
- 3. Choose the Google provider and select **OAuth with Google (Gemini CLI)**.
35
+ 2. **Select Provider**: Choose **Google** from the list.
36
+ 3. **Authenticate**: Select **OAuth with Google (Gemini CLI)**.
37
+ - A browser window will open for you to approve the access.
38
+ - The plugin spins up a temporary local server to capture the callback.
39
+ - If the local server fails (e.g., port in use or headless environment),
40
+ you can manually copy/paste the callback URL as instructed.
19
41
 
20
- The plugin spins up a local callback listener, so after approving in the
21
- browser you'll land on an "Authentication complete" page with no URL
22
- copy/paste required. If that port is already taken, the CLI automatically
23
- falls back to the classic copy/paste flow and explains what to do.
42
+ Once authenticated, Opencode will use your Google account for Gemini requests.
24
43
 
25
- ## Updating
44
+ ## Configuration
26
45
 
27
- > [!WARNING]
28
- > OpenCode does NOT auto-update plugins
46
+ ### Google Cloud Project
29
47
 
30
- To get the latest version:
48
+ By default, the plugin attempts to provision or find a suitable Google Cloud
49
+ project. To force a specific project, set the `projectId` in your configuration:
31
50
 
32
- ```bash
33
- (cd ~ && sed -i.bak '/"opencode-gemini-auth"/d' .cache/opencode/package.json && \
34
- rm -rf .cache/opencode/node_modules/opencode-gemini-auth && \
35
- echo "Plugin update script finished successfully.")
51
+ ```json
52
+ {
53
+ "provider": {
54
+ "google": {
55
+ "options": {
56
+ "projectId": "your-specific-project-id"
57
+ }
58
+ }
59
+ }
60
+ }
36
61
  ```
37
62
 
38
- ```bash
39
- opencode # Reinstalls latest
40
- ```
63
+ ### Thinking Models
41
64
 
42
- ## Local Development
65
+ Configure "thinking" capabilities for Gemini models using the `thinkingConfig`
66
+ option in your `config.json`.
43
67
 
44
- First, clone the repository and install dependencies:
68
+ **Gemini 3 (Thinking Level)**
69
+ Use `thinkingLevel` (`"low"`, `"high"`) for Gemini 3 models.
45
70
 
46
- ```bash
47
- git clone https://github.com/jenslys/opencode-gemini-auth.git
48
- cd opencode-gemini-auth
49
- bun install
71
+ ```json
72
+ {
73
+ "provider": {
74
+ "google": {
75
+ "models": {
76
+ "gemini-3-pro-preview": {
77
+ "options": {
78
+ "thinkingConfig": {
79
+ "thinkingLevel": "high",
80
+ "includeThoughts": true
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
50
88
  ```
51
89
 
52
- When you want Opencode to use a local checkout of this plugin, point the
53
- `plugin` entry in your config to the folder via a `file://` URL:
90
+ **Gemini 2.5 (Thinking Budget)**
91
+ Use `thinkingBudget` (token count) for Gemini 2.5 models.
54
92
 
55
93
  ```json
56
94
  {
57
- "$schema": "https://opencode.ai/config.json",
58
- "plugin": ["file:///absolute/path/to/opencode-gemini-auth"]
95
+ "provider": {
96
+ "google": {
97
+ "models": {
98
+ "gemini-2.5-flash": {
99
+ "options": {
100
+ "thinkingConfig": {
101
+ "thinkingBudget": 8192,
102
+ "includeThoughts": true
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
59
109
  }
60
110
  ```
61
111
 
62
- Replace `/absolute/path/to/opencode-gemini-auth` with the absolute path to
63
- your local clone.
112
+ ## Troubleshooting
64
113
 
65
- ## Manual Google Cloud Setup
114
+ ### Manual Google Cloud Setup
66
115
 
67
- If automatic provisioning fails, use the console:
116
+ If automatic provisioning fails, you may need to set up the project manually:
68
117
 
69
- 1. Go to the Google Cloud Console and create (or select) a project, e.g. `gemini`.
70
- 2. Select that project.
71
- 3. Enable the **Gemini for Google Cloud API** (`cloudaicompanion.googleapis.com`).
72
- 4. Re-run `opencode auth login` and enter the project **ID** (not the display name).
118
+ 1. Go to the [Google Cloud Console](https://console.cloud.google.com/).
119
+ 2. Create or select a project.
120
+ 3. Enable the **Gemini for Google Cloud API**
121
+ (`cloudaicompanion.googleapis.com`).
122
+ 4. Configure the `projectId` in your Opencode config as shown above.
73
123
 
74
- ## Debugging Gemini Requests
124
+ ### Debugging
75
125
 
76
- Set `OPENCODE_GEMINI_DEBUG=1` in the environment when you run an Opencode
77
- command to capture every Gemini request/response that this plugin issues. When
78
- enabled, the plugin writes to a timestamped `gemini-debug-<ISO>.log` file in
79
- your current working directory so the CLI output stays clean.
126
+ To view detailed logs of Gemini requests and responses, set the
127
+ `OPENCODE_GEMINI_DEBUG` environment variable:
80
128
 
81
129
  ```bash
82
130
  OPENCODE_GEMINI_DEBUG=1 opencode
83
131
  ```
84
132
 
85
- The logger shows the transformed URL, HTTP method, sanitized headers (the
86
- `Authorization` header is redacted), whether the call used streaming, and a
87
- truncated preview (2 KB) of both the request and response bodies. This is handy
88
- when diagnosing "Bad Request" responses from Gemini. Remember that payloads may
89
- still include parts of your prompt or response, so only enable this flag when
90
- you're comfortable keeping that information in the generated log file.
91
-
92
- **404s on `gemini-2.5-flash-image`.** Opencode fires internal
93
- summarization/title requests at `gemini-2.5-flash-image`. The plugin
94
- automatically remaps those payloads to `gemini-2.5-flash`, eliminating the extra
95
- 404s for accounts without image access. If you still see a 404, confirm your
96
- project actually has access to the fallback model.
133
+ This will generate `gemini-debug-<timestamp>.log` files in your working
134
+ directory containing sanitized request/response details.
135
+
136
+ ### Updating
137
+
138
+ Opencode does not automatically update plugins. To update to the latest version,
139
+ you must clear the cached plugin:
140
+
141
+ ```bash
142
+ # Clear the specific plugin cache
143
+ rm -rf ~/.cache/opencode/node_modules/opencode-gemini-auth
144
+
145
+ # Run Opencode to trigger a fresh install
146
+ opencode
147
+ ```
148
+
149
+ ## Development
150
+
151
+ To develop on this plugin locally:
152
+
153
+ 1. **Clone**:
154
+
155
+ ```bash
156
+ git clone https://github.com/jenslys/opencode-gemini-auth.git
157
+ cd opencode-gemini-auth
158
+ bun install
159
+ ```
160
+
161
+ 2. **Link**:
162
+ Update your Opencode config to point to your local directory using a
163
+ `file://` URL:
164
+
165
+ ```json
166
+ {
167
+ "plugin": ["file:///absolute/path/to/opencode-gemini-auth"]
168
+ }
169
+ ```
170
+
171
+ ## License
172
+
173
+ MIT
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-gemini-auth",
3
3
  "module": "index.ts",
4
- "version": "1.1.6",
4
+ "version": "1.3.3",
5
5
  "author": "jenslys",
6
6
  "repository": "https://github.com/jenslys/opencode-gemini-auth",
7
7
  "files": [
@@ -11,11 +11,11 @@
11
11
  "license": "MIT",
12
12
  "type": "module",
13
13
  "devDependencies": {
14
- "@opencode-ai/plugin": "^0.15.30",
14
+ "@opencode-ai/plugin": "^0.15.31",
15
15
  "@types/bun": "latest"
16
16
  },
17
17
  "peerDependencies": {
18
- "typescript": "^5"
18
+ "typescript": "^5.9.3"
19
19
  },
20
20
  "dependencies": {
21
21
  "@openauthjs/openauth": "^0.4.3"
@@ -14,7 +14,6 @@ interface PkcePair {
14
14
 
15
15
  interface GeminiAuthState {
16
16
  verifier: string;
17
- projectId: string;
18
17
  }
19
18
 
20
19
  /**
@@ -23,7 +22,6 @@ interface GeminiAuthState {
23
22
  export interface GeminiAuthorization {
24
23
  url: string;
25
24
  verifier: string;
26
- projectId: string;
27
25
  }
28
26
 
29
27
  interface GeminiTokenExchangeSuccess {
@@ -32,7 +30,6 @@ interface GeminiTokenExchangeSuccess {
32
30
  access: string;
33
31
  expires: number;
34
32
  email?: string;
35
- projectId: string;
36
33
  }
37
34
 
38
35
  interface GeminiTokenExchangeFailure {
@@ -74,14 +71,13 @@ function decodeState(state: string): GeminiAuthState {
74
71
  }
75
72
  return {
76
73
  verifier: parsed.verifier,
77
- projectId: typeof parsed.projectId === "string" ? parsed.projectId : "",
78
74
  };
79
75
  }
80
76
 
81
77
  /**
82
- * Build the Gemini OAuth authorization URL including PKCE and optional project metadata.
78
+ * Build the Gemini OAuth authorization URL including PKCE.
83
79
  */
84
- export async function authorizeGemini(projectId = ""): Promise<GeminiAuthorization> {
80
+ export async function authorizeGemini(): Promise<GeminiAuthorization> {
85
81
  const pkce = (await generatePKCE()) as PkcePair;
86
82
 
87
83
  const url = new URL("https://accounts.google.com/o/oauth2/v2/auth");
@@ -91,17 +87,13 @@ export async function authorizeGemini(projectId = ""): Promise<GeminiAuthorizati
91
87
  url.searchParams.set("scope", GEMINI_SCOPES.join(" "));
92
88
  url.searchParams.set("code_challenge", pkce.challenge);
93
89
  url.searchParams.set("code_challenge_method", "S256");
94
- url.searchParams.set(
95
- "state",
96
- encodeState({ verifier: pkce.verifier, projectId: projectId || "" }),
97
- );
90
+ url.searchParams.set("state", encodeState({ verifier: pkce.verifier }));
98
91
  url.searchParams.set("access_type", "offline");
99
92
  url.searchParams.set("prompt", "consent");
100
93
 
101
94
  return {
102
95
  url: url.toString(),
103
96
  verifier: pkce.verifier,
104
- projectId: projectId || "",
105
97
  };
106
98
  }
107
99
 
@@ -113,7 +105,7 @@ export async function exchangeGemini(
113
105
  state: string,
114
106
  ): Promise<GeminiTokenExchangeResult> {
115
107
  try {
116
- const { verifier, projectId } = decodeState(state);
108
+ const { verifier } = decodeState(state);
117
109
 
118
110
  const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
119
111
  method: "POST",
@@ -155,15 +147,12 @@ export async function exchangeGemini(
155
147
  return { type: "failed", error: "Missing refresh token in response" };
156
148
  }
157
149
 
158
- const storedRefresh = `${refreshToken}|${projectId || ""}`;
159
-
160
150
  return {
161
151
  type: "success",
162
- refresh: storedRefresh,
152
+ refresh: refreshToken,
163
153
  access: tokenPayload.access_token,
164
154
  expires: Date.now() + tokenPayload.expires_in * 1000,
165
155
  email: userInfo.email,
166
- projectId: projectId || "",
167
156
  };
168
157
  } catch (error) {
169
158
  return {
@@ -22,9 +22,17 @@ export function parseRefreshParts(refresh: string): RefreshParts {
22
22
  * Serializes refresh token parts into the stored string format.
23
23
  */
24
24
  export function formatRefreshParts(parts: RefreshParts): string {
25
+ if (!parts.refreshToken) {
26
+ return "";
27
+ }
28
+
29
+ if (!parts.projectId && !parts.managedProjectId) {
30
+ return parts.refreshToken;
31
+ }
32
+
25
33
  const projectSegment = parts.projectId ?? "";
26
- const base = `${parts.refreshToken}|${projectSegment}`;
27
- return parts.managedProjectId ? `${base}|${parts.managedProjectId}` : base;
34
+ const managedSegment = parts.managedProjectId ?? "";
35
+ return `${parts.refreshToken}|${projectSegment}|${managedSegment}`;
28
36
  }
29
37
 
30
38
  /**
@@ -48,7 +48,7 @@ class ProjectIdRequiredError extends Error {
48
48
  */
49
49
  constructor() {
50
50
  super(
51
- "Google Gemini requires a Google Cloud project. Enable the Gemini for Google Cloud API on a project you control, rerun `opencode auth login`, and supply that project ID when prompted.",
51
+ "Google Gemini requires a Google Cloud project. Enable the Gemini for Google Cloud API on a project you control, then set `provider.google.options.projectId` in your Opencode config (or set OPENCODE_GEMINI_PROJECT_ID).",
52
52
  );
53
53
  }
54
54
  }
@@ -109,8 +109,21 @@ export function invalidateProjectContextCache(refresh?: string): void {
109
109
  projectContextResultCache.clear();
110
110
  return;
111
111
  }
112
+
112
113
  projectContextPendingCache.delete(refresh);
113
114
  projectContextResultCache.delete(refresh);
115
+
116
+ const prefix = `${refresh}|cfg:`;
117
+ for (const key of projectContextPendingCache.keys()) {
118
+ if (key.startsWith(prefix)) {
119
+ projectContextPendingCache.delete(key);
120
+ }
121
+ }
122
+ for (const key of projectContextResultCache.keys()) {
123
+ if (key.startsWith(prefix)) {
124
+ projectContextResultCache.delete(key);
125
+ }
126
+ }
114
127
  }
115
128
 
116
129
  /**
@@ -220,13 +233,19 @@ export async function onboardManagedProject(
220
233
  export async function ensureProjectContext(
221
234
  auth: OAuthAuthDetails,
222
235
  client: PluginClient,
236
+ configuredProjectId?: string,
223
237
  ): Promise<ProjectContextResult> {
224
238
  const accessToken = auth.access;
225
239
  if (!accessToken) {
226
240
  return { auth, effectiveProjectId: "" };
227
241
  }
228
242
 
229
- const cacheKey = getCacheKey(auth);
243
+ const cacheKey = (() => {
244
+ const base = getCacheKey(auth);
245
+ if (!base) return undefined;
246
+ const project = configuredProjectId?.trim() ?? "";
247
+ return project ? `${base}|cfg:${project}` : base;
248
+ })();
230
249
  if (cacheKey) {
231
250
  const cached = projectContextResultCache.get(cacheKey);
232
251
  if (cached) {
@@ -240,21 +259,24 @@ export async function ensureProjectContext(
240
259
 
241
260
  const resolveContext = async (): Promise<ProjectContextResult> => {
242
261
  const parts = parseRefreshParts(auth.refresh);
243
- if (parts.projectId || parts.managedProjectId) {
262
+ const effectiveConfiguredProjectId = configuredProjectId?.trim() || undefined;
263
+ const projectId = effectiveConfiguredProjectId ?? parts.projectId;
264
+
265
+ if (projectId || parts.managedProjectId) {
244
266
  return {
245
267
  auth,
246
- effectiveProjectId: parts.projectId || parts.managedProjectId || "",
268
+ effectiveProjectId: projectId || parts.managedProjectId || "",
247
269
  };
248
270
  }
249
271
 
250
- const loadPayload = await loadManagedProject(accessToken, parts.projectId);
272
+ const loadPayload = await loadManagedProject(accessToken, projectId);
251
273
  if (loadPayload?.cloudaicompanionProject) {
252
274
  const managedProjectId = loadPayload.cloudaicompanionProject;
253
275
  const updatedAuth: OAuthAuthDetails = {
254
276
  ...auth,
255
277
  refresh: formatRefreshParts({
256
278
  refreshToken: parts.refreshToken,
257
- projectId: parts.projectId,
279
+ projectId,
258
280
  managedProjectId,
259
281
  }),
260
282
  };
@@ -283,13 +305,13 @@ export async function ensureProjectContext(
283
305
  throw new ProjectIdRequiredError();
284
306
  }
285
307
 
286
- const managedProjectId = await onboardManagedProject(accessToken, tierId, parts.projectId);
308
+ const managedProjectId = await onboardManagedProject(accessToken, tierId, projectId);
287
309
  if (managedProjectId) {
288
310
  const updatedAuth: OAuthAuthDetails = {
289
311
  ...auth,
290
312
  refresh: formatRefreshParts({
291
313
  refreshToken: parts.refreshToken,
292
- projectId: parts.projectId,
314
+ projectId,
293
315
  managedProjectId,
294
316
  }),
295
317
  };
@@ -27,15 +27,19 @@ export interface GeminiUsageMetadata {
27
27
  }
28
28
 
29
29
  /**
30
- * Normalized thinking configuration accepted by Gemini.
30
+ * Thinking configuration accepted by Gemini.
31
+ * - Gemini 3 models use thinkingLevel (string: 'low', 'medium', 'high')
32
+ * - Gemini 2.5 models use thinkingBudget (number)
31
33
  */
32
34
  export interface ThinkingConfig {
33
35
  thinkingBudget?: number;
36
+ thinkingLevel?: string;
34
37
  includeThoughts?: boolean;
35
38
  }
36
39
 
37
40
  /**
38
- * Ensures thinkingConfig is valid: includeThoughts only allowed when budget > 0.
41
+ * Normalizes thinkingConfig - passes through values as-is without mapping.
42
+ * User should use thinkingLevel for Gemini 3 and thinkingBudget for Gemini 2.5.
39
43
  */
40
44
  export function normalizeThinkingConfig(config: unknown): ThinkingConfig | undefined {
41
45
  if (!config || typeof config !== "object") {
@@ -44,15 +48,14 @@ export function normalizeThinkingConfig(config: unknown): ThinkingConfig | undef
44
48
 
45
49
  const record = config as Record<string, unknown>;
46
50
  const budgetRaw = record.thinkingBudget ?? record.thinking_budget;
51
+ const levelRaw = record.thinkingLevel ?? record.thinking_level;
47
52
  const includeRaw = record.includeThoughts ?? record.include_thoughts;
48
53
 
49
54
  const thinkingBudget = typeof budgetRaw === "number" && Number.isFinite(budgetRaw) ? budgetRaw : undefined;
55
+ const thinkingLevel = typeof levelRaw === "string" && levelRaw.length > 0 ? levelRaw.toLowerCase() : undefined;
50
56
  const includeThoughts = typeof includeRaw === "boolean" ? includeRaw : undefined;
51
57
 
52
- const enableThinking = thinkingBudget !== undefined && thinkingBudget > 0;
53
- const finalInclude = enableThinking ? includeThoughts ?? false : false;
54
-
55
- if (!enableThinking && finalInclude === false && thinkingBudget === undefined && includeThoughts === undefined) {
58
+ if (thinkingBudget === undefined && thinkingLevel === undefined && includeThoughts === undefined) {
56
59
  return undefined;
57
60
  }
58
61
 
@@ -60,8 +63,11 @@ export function normalizeThinkingConfig(config: unknown): ThinkingConfig | undef
60
63
  if (thinkingBudget !== undefined) {
61
64
  normalized.thinkingBudget = thinkingBudget;
62
65
  }
63
- if (finalInclude !== undefined) {
64
- normalized.includeThoughts = finalInclude;
66
+ if (thinkingLevel !== undefined) {
67
+ normalized.thinkingLevel = thinkingLevel;
68
+ }
69
+ if (includeThoughts !== undefined) {
70
+ normalized.includeThoughts = includeThoughts;
65
71
  }
66
72
  return normalized;
67
73
  }
@@ -26,6 +26,7 @@ export interface ProviderModel {
26
26
 
27
27
  export interface Provider {
28
28
  models?: Record<string, ProviderModel>;
29
+ options?: Record<string, unknown>;
29
30
  }
30
31
 
31
32
  export interface LoaderResult {
@@ -41,7 +42,7 @@ export interface AuthMethod {
41
42
  url: string;
42
43
  instructions: string;
43
44
  method: string;
44
- callback: (callbackUrl: string) => Promise<GeminiTokenExchangeResult>;
45
+ callback: (() => Promise<GeminiTokenExchangeResult>) | ((callbackUrl: string) => Promise<GeminiTokenExchangeResult>);
45
46
  }>;
46
47
  }
47
48
 
package/src/plugin.ts CHANGED
@@ -1,8 +1,9 @@
1
+ import { spawn } from "node:child_process";
2
+
1
3
  import { GEMINI_PROVIDER_ID, GEMINI_REDIRECT_URI } from "./constants";
2
4
  import { authorizeGemini, exchangeGemini } from "./gemini/oauth";
3
5
  import type { GeminiTokenExchangeResult } from "./gemini/oauth";
4
6
  import { accessTokenExpired, isOAuthAuth } from "./plugin/auth";
5
- import { promptProjectId } from "./plugin/cli";
6
7
  import { ensureProjectContext } from "./plugin/project";
7
8
  import { startGeminiDebugRequest } from "./plugin/debug";
8
9
  import {
@@ -36,6 +37,17 @@ export const GeminiCLIOAuthPlugin = async (
36
37
  return null;
37
38
  }
38
39
 
40
+ const providerOptions =
41
+ provider && typeof provider === "object"
42
+ ? ((provider as { options?: Record<string, unknown> }).options ?? undefined)
43
+ : undefined;
44
+ const projectIdFromConfig =
45
+ providerOptions && typeof providerOptions.projectId === "string"
46
+ ? providerOptions.projectId.trim()
47
+ : "";
48
+ const projectIdFromEnv = process.env.OPENCODE_GEMINI_PROJECT_ID?.trim() ?? "";
49
+ const configuredProjectId = projectIdFromEnv || projectIdFromConfig || undefined;
50
+
39
51
  if (provider.models) {
40
52
  for (const model of Object.values(provider.models)) {
41
53
  if (model) {
@@ -75,7 +87,7 @@ export const GeminiCLIOAuthPlugin = async (
75
87
  */
76
88
  async function resolveProjectContext(): Promise<ProjectContextResult> {
77
89
  try {
78
- return await ensureProjectContext(authRecord, client);
90
+ return await ensureProjectContext(authRecord, client, configuredProjectId);
79
91
  } catch (error) {
80
92
  if (error instanceof Error) {
81
93
  console.error(error.message);
@@ -120,8 +132,6 @@ export const GeminiCLIOAuthPlugin = async (
120
132
  label: "OAuth with Google (Gemini CLI)",
121
133
  type: "oauth",
122
134
  authorize: async () => {
123
- console.log("\n=== Google Gemini OAuth Setup ===");
124
-
125
135
  const isHeadless = !!(
126
136
  process.env.SSH_CONNECTION ||
127
137
  process.env.SSH_CLIENT ||
@@ -133,40 +143,25 @@ export const GeminiCLIOAuthPlugin = async (
133
143
  if (!isHeadless) {
134
144
  try {
135
145
  listener = await startOAuthListener();
136
- const { host } = new URL(GEMINI_REDIRECT_URI);
137
- console.log("1. You'll be asked to sign in to your Google account and grant permission.");
138
- console.log(
139
- `2. We'll automatically capture the browser redirect on http://${host}. No need to paste anything back here.`,
140
- );
141
- console.log("3. Once you see the 'Authentication complete' page in your browser, return to this terminal.");
142
146
  } catch (error) {
143
- console.log("1. You'll be asked to sign in to your Google account and grant permission.");
144
- console.log("2. After you approve, the browser will try to redirect to a 'localhost' page.");
145
- console.log(
146
- "3. This page will show an error like 'This site can't be reached'. This is perfectly normal and means it worked!",
147
- );
148
- console.log(
149
- "4. Once you see that error, copy the entire URL from the address bar, paste it back here, and press Enter.",
150
- );
151
147
  if (error instanceof Error) {
152
- console.log(`\nWarning: Couldn't start the local callback listener (${error.message}). Falling back to manual copy/paste.`);
148
+ console.log(
149
+ `Warning: Couldn't start the local callback listener (${error.message}). You'll need to paste the callback URL.`,
150
+ );
153
151
  } else {
154
- console.log("\nWarning: Couldn't start the local callback listener. Falling back to manual copy/paste.");
152
+ console.log(
153
+ "Warning: Couldn't start the local callback listener. You'll need to paste the callback URL.",
154
+ );
155
155
  }
156
156
  }
157
157
  } else {
158
- console.log("Headless environment detected. Using manual OAuth flow.");
159
- console.log("1. You'll be asked to sign in to your Google account and grant permission.");
160
- console.log("2. After you approve, the browser will redirect to a 'localhost' URL.");
161
- console.log(
162
- "3. Copy the ENTIRE URL from your browser's address bar (it will look like: http://localhost:8085/oauth2callback?code=...&state=...)",
163
- );
164
- console.log("4. Paste the URL back here and press Enter.");
158
+ console.log("Headless environment detected. You'll need to paste the callback URL.");
165
159
  }
166
- console.log("\n");
167
160
 
168
- const projectId = await promptProjectId();
169
- const authorization = await authorizeGemini(projectId);
161
+ const authorization = await authorizeGemini();
162
+ if (!isHeadless) {
163
+ openBrowserUrl(authorization.url);
164
+ }
170
165
 
171
166
  if (listener) {
172
167
  return {
@@ -206,7 +201,7 @@ export const GeminiCLIOAuthPlugin = async (
206
201
  return {
207
202
  url: authorization.url,
208
203
  instructions:
209
- "Visit the URL above, complete OAuth, ignore the localhost connection error, and paste the full redirected URL (e.g., http://localhost:8085/oauth2callback?code=...&state=...): ",
204
+ "Complete OAuth in your browser, then paste the full redirected URL (e.g., http://localhost:8085/oauth2callback?code=...&state=...)",
210
205
  method: "code",
211
206
  callback: async (callbackUrl: string): Promise<GeminiTokenExchangeResult> => {
212
207
  try {
@@ -253,3 +248,20 @@ function toUrlString(value: RequestInfo): string {
253
248
  }
254
249
  return value.toString();
255
250
  }
251
+
252
+ function openBrowserUrl(url: string): void {
253
+ try {
254
+ // Best-effort: don't block auth flow if spawning fails.
255
+ const platform = process.platform;
256
+ const command =
257
+ platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
258
+ const args =
259
+ platform === "win32" ? ["/c", "start", "", url] : [url];
260
+ const child = spawn(command, args, {
261
+ stdio: "ignore",
262
+ detached: true,
263
+ });
264
+ child.unref?.();
265
+ } catch {
266
+ }
267
+ }
package/src/plugin/cli.ts DELETED
@@ -1,15 +0,0 @@
1
- import { createInterface } from "node:readline/promises";
2
- import { stdin as input, stdout as output } from "node:process";
3
-
4
- /**
5
- * Prompts the user for a project ID via stdin/stdout.
6
- */
7
- export async function promptProjectId(): Promise<string> {
8
- const rl = createInterface({ input, output });
9
- try {
10
- const answer = await rl.question("Project ID (leave blank to use your default project): ");
11
- return answer.trim();
12
- } finally {
13
- rl.close();
14
- }
15
- }