Skip to content

Commit 917310f

Browse files
committed
feat(gemini): enable Google Search and set gemini-3.1-pro-preview as default model
1 parent 017a412 commit 917310f

3 files changed

Lines changed: 110 additions & 67 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { PlatformError } from "@effect/platform/Error"
2+
import { Effect } from "effect"
3+
4+
import type { AuthGeminiLogoutCommand } from "../core/domain.js"
5+
import type { CommandFailedError } from "../shell/errors.js"
6+
import { geminiApiKeyPath, geminiCredentialsPath, geminiEnvFilePath, withGeminiAuth } from "./auth-gemini.js"
7+
import type { GeminiRuntime } from "./auth-gemini.js"
8+
import { normalizeAccountLabel } from "./auth-helpers.js"
9+
import { autoSyncState } from "./state-repo.js"
10+
11+
// CHANGE: logout Gemini CLI by clearing API key and OAuth credentials for a label
12+
// WHY: allow revoking Gemini CLI access deterministically
13+
// QUOTE(ТЗ): "Добавь поддержку gemini CLI"
14+
// REF: issue-146
15+
// SOURCE: https://geminicli.com/docs/get-started/authentication/
16+
// FORMAT THEOREM: forall cmd: authGeminiLogout(cmd) -> credentials_cleared(cmd)
17+
// PURITY: SHELL
18+
// EFFECT: Effect<void, PlatformError | CommandFailedError, GeminiRuntime>
19+
// INVARIANT: all credential files (API key and OAuth) are removed from account directory
20+
// COMPLEXITY: O(1)
21+
export const authGeminiLogout = (
22+
command: AuthGeminiLogoutCommand
23+
): Effect.Effect<void, PlatformError | CommandFailedError, GeminiRuntime> =>
24+
Effect.gen(function*(_) {
25+
const accountLabel = normalizeAccountLabel(command.label, "default")
26+
yield* _(
27+
withGeminiAuth(command, ({ accountPath, fs }) =>
28+
Effect.gen(function*(_) {
29+
// Clear API key
30+
yield* _(fs.remove(geminiApiKeyPath(accountPath), { force: true }))
31+
yield* _(fs.remove(geminiEnvFilePath(accountPath), { force: true }))
32+
// Clear OAuth credentials (entire .gemini directory)
33+
yield* _(fs.remove(geminiCredentialsPath(accountPath), { recursive: true, force: true }))
34+
}))
35+
)
36+
yield* _(autoSyncState(`chore(state): auth gemini logout ${accountLabel}`))
37+
}).pipe(Effect.asVoid)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { PlatformError } from "@effect/platform/Error"
2+
import { Effect } from "effect"
3+
4+
import type { AuthGeminiStatusCommand } from "../core/domain.js"
5+
import type { CommandFailedError } from "../shell/errors.js"
6+
import { resolveGeminiAuthMethod, withGeminiAuth } from "./auth-gemini.js"
7+
import type { GeminiRuntime } from "./auth-gemini.js"
8+
9+
// CHANGE: show Gemini CLI auth status for a given label
10+
// WHY: allow verifying API key/OAuth presence without exposing credentials
11+
// QUOTE(ТЗ): "Добавь поддержку gemini CLI"
12+
// REF: issue-146
13+
// SOURCE: https://geminicli.com/docs/get-started/authentication/
14+
// FORMAT THEOREM: forall cmd: authGeminiStatus(cmd) -> connected(cmd, method) | disconnected(cmd)
15+
// PURITY: SHELL
16+
// EFFECT: Effect<void, PlatformError | CommandFailedError, GeminiRuntime>
17+
// INVARIANT: never logs API keys or OAuth tokens
18+
// COMPLEXITY: O(1)
19+
export const authGeminiStatus = (
20+
command: AuthGeminiStatusCommand
21+
): Effect.Effect<void, PlatformError | CommandFailedError, GeminiRuntime> =>
22+
withGeminiAuth(command, ({ accountLabel, accountPath, fs }) =>
23+
Effect.gen(function*(_) {
24+
const authMethod = yield* _(resolveGeminiAuthMethod(fs, accountPath))
25+
if (authMethod === "none") {
26+
yield* _(Effect.log(`Gemini not connected (${accountLabel}).`))
27+
return
28+
}
29+
yield* _(Effect.log(`Gemini connected (${accountLabel}, ${authMethod}).`))
30+
}))

packages/lib/src/usecases/auth-gemini.ts

Lines changed: 43 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { autoSyncState } from "./state-repo.js"
2727
// INVARIANT: Credentials are stored in isolated account directory
2828
// COMPLEXITY: O(1) for API key, O(user_interaction) for OAuth
2929

30-
type GeminiRuntime = FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor
30+
export type GeminiRuntime = FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor
3131
type GeminiAuthMethod = "none" | "api-key" | "oauth"
3232

3333
const geminiImageName = "docker-git-auth-gemini:latest"
@@ -47,9 +47,9 @@ export const geminiAuthRoot = ".docker-git/.orch/auth/gemini"
4747
const geminiApiKeyFileName = ".api-key"
4848
const geminiEnvFileName = ".env"
4949

50-
const geminiApiKeyPath = (accountPath: string): string => `${accountPath}/${geminiApiKeyFileName}`
51-
const geminiEnvFilePath = (accountPath: string): string => `${accountPath}/${geminiEnvFileName}`
52-
const geminiCredentialsPath = (accountPath: string): string => `${accountPath}/${geminiCredentialsDir}`
50+
export const geminiApiKeyPath = (accountPath: string): string => `${accountPath}/${geminiApiKeyFileName}`
51+
export const geminiEnvFilePath = (accountPath: string): string => `${accountPath}/${geminiEnvFileName}`
52+
export const geminiCredentialsPath = (accountPath: string): string => `${accountPath}/${geminiCredentialsDir}`
5353

5454
// CHANGE: render Dockerfile for Gemini CLI authentication image
5555
// WHY: Gemini CLI OAuth requires running in Docker for headless environments
@@ -96,7 +96,7 @@ const resolveGeminiAccountPath = (path: Path.Path, rootPath: string, label: stri
9696
return { accountLabel, accountPath }
9797
}
9898

99-
const withGeminiAuth = <A, E>(
99+
export const withGeminiAuth = <A, E>(
100100
command: AuthGeminiLoginCommand | AuthGeminiLogoutCommand | AuthGeminiStatusCommand,
101101
run: (
102102
context: GeminiAccountContext
@@ -200,7 +200,7 @@ const hasOauthCredentials = (
200200
// PURITY: SHELL
201201
// INVARIANT: API key takes precedence over OAuth credentials
202202
// COMPLEXITY: O(1)
203-
const resolveGeminiAuthMethod = (
203+
export const resolveGeminiAuthMethod = (
204204
fs: FileSystem.FileSystem,
205205
accountPath: string
206206
): Effect.Effect<GeminiAuthMethod, PlatformError> =>
@@ -234,6 +234,24 @@ export const authGeminiLogin = (
234234
const apiKeyFilePath = geminiApiKeyPath(accountPath)
235235
yield* _(fs.writeFileString(apiKeyFilePath, `${apiKey.trim()}\n`))
236236
yield* _(fs.chmod(apiKeyFilePath, 0o600), Effect.orElseSucceed(() => void 0))
237+
238+
const credentialsDir = geminiCredentialsPath(accountPath)
239+
yield* _(fs.makeDirectory(credentialsDir, { recursive: true }))
240+
const settingsPath = `${credentialsDir}/settings.json`
241+
yield* _(
242+
fs.writeFileString(
243+
settingsPath,
244+
JSON.stringify(
245+
{
246+
model: "gemini-3.1-pro-preview",
247+
web_search: true,
248+
security: { folderTrust: { enabled: false }, approvalPolicy: "never" }
249+
},
250+
null,
251+
2
252+
) + "\n"
253+
)
254+
)
237255
})).pipe(
238256
Effect.zipRight(autoSyncState(`chore(state): auth gemini ${accountLabel}`))
239257
)
@@ -253,16 +271,11 @@ export const authGeminiLoginCli = (
253271
_command: AuthGeminiLoginCommand
254272
): Effect.Effect<void, PlatformError | CommandFailedError, GeminiRuntime> =>
255273
Effect.gen(function*(_) {
256-
yield* _(Effect.log("Gemini CLI supports two authentication methods:"))
257-
yield* _(Effect.log(""))
258-
yield* _(Effect.log("1. API Key (recommended for simplicity):"))
259-
yield* _(Effect.log(" - Go to https://ai.google.dev/aistudio"))
260-
yield* _(Effect.log(" - Create or retrieve your API key"))
261-
yield* _(Effect.log(" - Use: docker-git menu -> Auth profiles -> Gemini CLI: set API key"))
262-
yield* _(Effect.log(""))
263-
yield* _(Effect.log("2. OAuth (Sign in with Google):"))
264-
yield* _(Effect.log(" - Use: docker-git menu -> Auth profiles -> Gemini CLI: login via OAuth"))
265-
yield* _(Effect.log(" - Follow the prompts to authenticate with your Google account"))
274+
yield* _(
275+
Effect.log(
276+
"Gemini CLI supports two authentication methods:\n\n1. API Key (recommended for simplicity):\n - Go to https://ai.google.dev/aistudio\n - Create or retrieve your API key\n - Use: docker-git menu -> Auth profiles -> Gemini CLI: set API key\n\n2. OAuth (Sign in with Google):\n - Use: docker-git menu -> Auth profiles -> Gemini CLI: login via OAuth\n - Follow the prompts to authenticate with your Google account"
277+
)
278+
)
266279
})
267280

268281
// CHANGE: login to Gemini CLI via OAuth in Docker container
@@ -299,7 +312,16 @@ const prepareGeminiCredentialsDir = (
299312
const writeInitialSettings = (credentialsDir: string, fs: FileSystem.FileSystem) =>
300313
Effect.gen(function*(_) {
301314
const settingsPath = `${credentialsDir}/settings.json`
302-
yield* _(fs.writeFileString(settingsPath, JSON.stringify({ security: { folderTrust: { enabled: false } } })))
315+
yield* _(
316+
fs.writeFileString(
317+
settingsPath,
318+
JSON.stringify({
319+
model: "gemini-3.1-pro-preview",
320+
web_search: true,
321+
security: { folderTrust: { enabled: false } }
322+
})
323+
)
324+
)
303325

304326
const trustedFoldersPath = `${credentialsDir}/trustedFolders.json`
305327
yield* _(
@@ -340,6 +362,8 @@ export const authGeminiLoginOauth = (
340362
settingsPath,
341363
JSON.stringify(
342364
{
365+
model: "gemini-3.1-pro-preview",
366+
web_search: true,
343367
security: {
344368
folderTrust: { enabled: false },
345369
auth: { selectedType: "oauth-personal" },
@@ -358,53 +382,5 @@ export const authGeminiLoginOauth = (
358382
)
359383
}
360384

361-
// CHANGE: show Gemini CLI auth status for a given label
362-
// WHY: allow verifying API key/OAuth presence without exposing credentials
363-
// QUOTE(ТЗ): "Добавь поддержку gemini CLI"
364-
// REF: issue-146
365-
// SOURCE: https://geminicli.com/docs/get-started/authentication/
366-
// FORMAT THEOREM: forall cmd: authGeminiStatus(cmd) -> connected(cmd, method) | disconnected(cmd)
367-
// PURITY: SHELL
368-
// EFFECT: Effect<void, PlatformError | CommandFailedError, GeminiRuntime>
369-
// INVARIANT: never logs API keys or OAuth tokens
370-
// COMPLEXITY: O(1)
371-
export const authGeminiStatus = (
372-
command: AuthGeminiStatusCommand
373-
): Effect.Effect<void, PlatformError | CommandFailedError, GeminiRuntime> =>
374-
withGeminiAuth(command, ({ accountLabel, accountPath, fs }) =>
375-
Effect.gen(function*(_) {
376-
const authMethod = yield* _(resolveGeminiAuthMethod(fs, accountPath))
377-
if (authMethod === "none") {
378-
yield* _(Effect.log(`Gemini not connected (${accountLabel}).`))
379-
return
380-
}
381-
yield* _(Effect.log(`Gemini connected (${accountLabel}, ${authMethod}).`))
382-
}))
383-
384-
// CHANGE: logout Gemini CLI by clearing API key and OAuth credentials for a label
385-
// WHY: allow revoking Gemini CLI access deterministically
386-
// QUOTE(ТЗ): "Добавь поддержку gemini CLI"
387-
// REF: issue-146
388-
// SOURCE: https://geminicli.com/docs/get-started/authentication/
389-
// FORMAT THEOREM: forall cmd: authGeminiLogout(cmd) -> credentials_cleared(cmd)
390-
// PURITY: SHELL
391-
// EFFECT: Effect<void, PlatformError | CommandFailedError, GeminiRuntime>
392-
// INVARIANT: all credential files (API key and OAuth) are removed from account directory
393-
// COMPLEXITY: O(1)
394-
export const authGeminiLogout = (
395-
command: AuthGeminiLogoutCommand
396-
): Effect.Effect<void, PlatformError | CommandFailedError, GeminiRuntime> =>
397-
Effect.gen(function*(_) {
398-
const accountLabel = normalizeAccountLabel(command.label, "default")
399-
yield* _(
400-
withGeminiAuth(command, ({ accountPath, fs }) =>
401-
Effect.gen(function*(_) {
402-
// Clear API key
403-
yield* _(fs.remove(geminiApiKeyPath(accountPath), { force: true }))
404-
yield* _(fs.remove(geminiEnvFilePath(accountPath), { force: true }))
405-
// Clear OAuth credentials (entire .gemini directory)
406-
yield* _(fs.remove(geminiCredentialsPath(accountPath), { recursive: true, force: true }))
407-
}))
408-
)
409-
yield* _(autoSyncState(`chore(state): auth gemini logout ${accountLabel}`))
410-
}).pipe(Effect.asVoid)
385+
export { authGeminiLogout } from "./auth-gemini-logout.js"
386+
export { authGeminiStatus } from "./auth-gemini-status.js"

0 commit comments

Comments
 (0)