Skip to content

Commit fc08d39

Browse files
authored
Merge pull request #150 from skulidropek/feat/gemini-default-settings
feat(gemini): enable Google Search and set gemini-3.1-pro-preview as default model
2 parents 7149306 + 01eb55b commit fc08d39

File tree

12 files changed

+440
-164
lines changed

12 files changed

+440
-164
lines changed

packages/lib/src/core/templates-entrypoint/gemini.ts

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,63 @@ const renderGeminiAuthConfig = (config: TemplateConfig): string =>
9999
.replaceAll("__GEMINI_AUTH_ROOT__", geminiAuthRootContainerPath(config.sshUser))
100100
.replaceAll("__GEMINI_HOME_DIR__", config.geminiHome)
101101

102+
const geminiSettingsJsonTemplate = `{
103+
"model": {
104+
"name": "gemini-3.1-pro-preview-yolo",
105+
"compressionThreshold": 0.9,
106+
"disableLoopDetection": true
107+
},
108+
"modelConfigs": {
109+
"customAliases": {
110+
"yolo-ultra": {
111+
"modelConfig": {
112+
"model": "gemini-3.1-pro-preview-yolo",
113+
"generateContentConfig": {
114+
"tools": [
115+
{
116+
"googleSearch": {}
117+
},
118+
{
119+
"urlContext": {}
120+
}
121+
]
122+
}
123+
}
124+
}
125+
}
126+
},
127+
"general": {
128+
"defaultApprovalMode": "auto_edit"
129+
},
130+
"tools": {
131+
"allowed": [
132+
"run_shell_command",
133+
"write_file",
134+
"googleSearch",
135+
"urlContext"
136+
]
137+
},
138+
"sandbox": {
139+
"enabled": false
140+
},
141+
"security": {
142+
"folderTrust": {
143+
"enabled": false
144+
},
145+
"auth": {
146+
"selectedType": "oauth-personal"
147+
},
148+
"disableYoloMode": false
149+
},
150+
"mcpServers": {
151+
"playwright": {
152+
"command": "docker-git-playwright-mcp",
153+
"args": [],
154+
"trust": true
155+
}
156+
}
157+
}`
158+
102159
const renderGeminiPermissionSettingsConfig = (config: TemplateConfig): string =>
103160
String.raw`# Gemini CLI: keep trust settings in sync with docker-git defaults
104161
GEMINI_SETTINGS_DIR="${config.geminiHome}"
@@ -111,14 +168,7 @@ mkdir -p "$GEMINI_SETTINGS_DIR" || true
111168
# Disable folder trust prompt and enable auto-approval in settings.json
112169
if [[ ! -f "$GEMINI_CONFIG_SETTINGS_FILE" ]]; then
113170
cat <<'EOF' > "$GEMINI_CONFIG_SETTINGS_FILE"
114-
{
115-
"security": {
116-
"folderTrust": {
117-
"enabled": false
118-
},
119-
"approvalPolicy": "never"
120-
}
121-
}
171+
${geminiSettingsJsonTemplate}
122172
EOF
123173
fi
124174

packages/lib/src/usecases/actions/create-project.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { renderError } from "../errors.js"
2323
import { applyGithubForkConfig } from "../github-fork.js"
2424
import { defaultProjectsRoot } from "../menu-helpers.js"
2525
import { findSshPrivateKey } from "../path-helpers.js"
26-
import { buildSshCommand } from "../projects-core.js"
26+
import { buildSshCommand, getContainerIpIfInsideContainer } from "../projects-core.js"
2727
import { resolveTemplateResourceLimits } from "../resource-limits.js"
2828
import { autoSyncState } from "../state-repo.js"
2929
import { ensureTerminalCursorVisible } from "../terminal-cursor.js"
@@ -97,8 +97,11 @@ const isInteractiveTty = (): boolean => process.stdin.isTTY && process.stdout.is
9797
const buildSshArgs = (
9898
config: CreateCommand["config"],
9999
sshKeyPath: string | null,
100-
remoteCommand?: string
100+
remoteCommand?: string,
101+
ipAddress?: string
101102
): ReadonlyArray<string> => {
103+
const host = ipAddress ?? "localhost"
104+
const port = ipAddress ? 22 : config.sshPort
102105
const args: Array<string> = []
103106
if (sshKeyPath !== null) {
104107
args.push("-i", sshKeyPath)
@@ -113,8 +116,8 @@ const buildSshArgs = (
113116
"-o",
114117
"UserKnownHostsFile=/dev/null",
115118
"-p",
116-
String(config.sshPort),
117-
`${config.sshUser}@localhost`
119+
String(port),
120+
`${config.sshUser}@${host}`
118121
)
119122
if (remoteCommand !== undefined) {
120123
args.push(remoteCommand)
@@ -140,8 +143,14 @@ const openSshBestEffort = (
140143
const fs = yield* _(FileSystem.FileSystem)
141144
const path = yield* _(Path.Path)
142145

146+
const ipAddress = yield* _(
147+
getContainerIpIfInsideContainer(fs, process.cwd(), template.containerName).pipe(
148+
Effect.orElse(() => Effect.succeed<string | undefined>(""))
149+
)
150+
)
151+
143152
const sshKey = yield* _(findSshPrivateKey(fs, path, process.cwd()))
144-
const sshCommand = buildSshCommand(template, sshKey)
153+
const sshCommand = buildSshCommand(template, sshKey, ipAddress)
145154

146155
const remoteCommandLabel = remoteCommand === undefined ? "" : ` (${remoteCommand})`
147156

@@ -152,7 +161,7 @@ const openSshBestEffort = (
152161
{
153162
cwd: process.cwd(),
154163
command: "ssh",
155-
args: buildSshArgs(template, sshKey, remoteCommand)
164+
args: buildSshArgs(template, sshKey, remoteCommand, ipAddress)
156165
},
157166
[0, 130],
158167
(exitCode) => new CommandFailedError({ command: "ssh", exitCode })

packages/lib/src/usecases/actions/docker-up.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
runDockerComposeUpRecreate,
1313
runDockerExecExitCode,
1414
runDockerInspectContainerBridgeIp,
15+
runDockerInspectContainerIp,
1516
runDockerNetworkConnectBridge
1617
} from "../../shell/docker.js"
1718
import type { DockerCommandError } from "../../shell/errors.js"
@@ -31,14 +32,29 @@ const agentFailPath = "/run/docker-git/agent.failed"
3132
const logSshAccess = (
3233
baseDir: string,
3334
config: CreateCommand["config"]
34-
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
35+
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor> =>
3536
Effect.gen(function*(_) {
3637
const fs = yield* _(FileSystem.FileSystem)
3738
const path = yield* _(Path.Path)
39+
40+
const isInsideContainer = yield* _(fs.exists("/.dockerenv"))
41+
let ipAddress: string | undefined
42+
43+
if (isInsideContainer) {
44+
const containerIp = yield* _(
45+
runDockerInspectContainerIp(baseDir, config.containerName).pipe(
46+
Effect.orElse(() => Effect.succeed(""))
47+
)
48+
)
49+
if (containerIp.length > 0) {
50+
ipAddress = containerIp
51+
}
52+
}
53+
3854
const resolvedAuthorizedKeys = resolveAuthorizedKeysPath(path, baseDir, config.authorizedKeysPath)
3955
const authExists = yield* _(fs.exists(resolvedAuthorizedKeys))
4056
const sshKey = yield* _(findSshPrivateKey(fs, path, process.cwd()))
41-
const sshCommand = buildSshCommand(config, sshKey)
57+
const sshCommand = buildSshCommand(config, sshKey, ipAddress)
4258

4359
yield* _(Effect.log(`SSH access: ${sshCommand}`))
4460
if (!authExists) {

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

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -237,35 +237,70 @@ export const prepareGeminiCredentialsDir = (
237237
return credentialsDir
238238
})
239239

240+
export const defaultGeminiSettings = {
241+
model: {
242+
name: "gemini-3.1-pro-preview-yolo",
243+
compressionThreshold: 0.9,
244+
disableLoopDetection: true
245+
},
246+
modelConfigs: {
247+
customAliases: {
248+
"yolo-ultra": {
249+
"modelConfig": {
250+
"model": "gemini-3.1-pro-preview-yolo",
251+
"generateContentConfig": {
252+
"tools": [
253+
{
254+
"googleSearch": {}
255+
},
256+
{
257+
"urlContext": {}
258+
}
259+
]
260+
}
261+
}
262+
}
263+
}
264+
},
265+
general: {
266+
defaultApprovalMode: "auto_edit"
267+
},
268+
tools: {
269+
allowed: [
270+
"run_shell_command",
271+
"write_file",
272+
"googleSearch",
273+
"urlContext"
274+
]
275+
},
276+
sandbox: {
277+
enabled: false
278+
},
279+
security: {
280+
folderTrust: {
281+
enabled: false
282+
},
283+
auth: {
284+
selectedType: "oauth-personal"
285+
},
286+
disableYoloMode: false
287+
},
288+
mcpServers: {
289+
playwright: {
290+
command: "docker-git-playwright-mcp",
291+
args: [],
292+
trust: true
293+
}
294+
}
295+
}
296+
240297
export const writeInitialSettings = (credentialsDir: string, fs: FileSystem.FileSystem) =>
241298
Effect.gen(function*(_) {
242299
const settingsPath = `${credentialsDir}/settings.json`
243300
yield* _(
244301
fs.writeFileString(
245302
settingsPath,
246-
JSON.stringify(
247-
{
248-
model: {
249-
name: "gemini-2.0-flash",
250-
compressionThreshold: 0.9,
251-
disableLoopDetection: true
252-
},
253-
general: {
254-
defaultApprovalMode: "auto_edit"
255-
},
256-
yolo: true,
257-
sandbox: {
258-
enabled: false
259-
},
260-
security: {
261-
folderTrust: { enabled: false },
262-
auth: { selectedType: "oauth-personal" },
263-
approvalPolicy: "never"
264-
}
265-
},
266-
null,
267-
2
268-
)
303+
JSON.stringify(defaultGeminiSettings, null, 2) + "\n"
269304
)
270305
)
271306

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-helpers.js"
7+
import type { GeminiRuntime } from "./auth-gemini-helpers.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-helpers.js"
7+
import type { GeminiRuntime } from "./auth-gemini-helpers.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+
}))

0 commit comments

Comments
 (0)