Skip to content

Commit 60e1571

Browse files
authored
feat: ✨ add Gemini auth plugin (#5)
2 parents 37b54a7 + 226a833 commit 60e1571

File tree

5 files changed

+180
-1
lines changed

5 files changed

+180
-1
lines changed

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
!Dockerfile
33
!entrypoint.sh
44
!git-export.py
5+
!convert-gemini.auth.ts

Dockerfile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ bun install -g "opencode-ai@${OPENCODE_VERSION}" || exit 1
135135
&& popd
136136
) || exit 1
137137

138+
###
139+
# Gemini plugin
140+
#
141+
bun install -g 'opencode-gemini-auth@latest' || exit 1
142+
138143
###
139144
# agent browser
140145
(
@@ -200,7 +205,8 @@ RUN <<'FOE'
200205
{
201206
"$schema": "https://opencode.ai/config.json",
202207
"plugin": [
203-
"engram"
208+
"engram",
209+
"file:///usr/local/bun/install/global/node_modules/opencode-gemini-auth"
204210
],
205211
"mcp": {
206212
"engram": {
@@ -255,6 +261,7 @@ EOF
255261
FOE
256262

257263
COPY --chmod=0555 entrypoint.sh /entrypoint.sh
264+
COPY --chmod=0555 convert-gemini.auth.ts /usr/local/bin/convert-gemini.auth.ts
258265

259266
USER bun:bun
260267

README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
- [Building the Container Image](#building-the-container-image)
55
- [Build Arguments](#build-arguments)
66
- [Authentication Setup](#authentication-setup)
7+
- [Reusing Existing Gemini CLI Authentication](#reusing-existing-gemini-cli-authentication)
8+
- [Manual Gemini Authentication in the Container](#manual-gemini-authentication-in-the-container)
79
- [Environment File Option](#environment-file-option)
810
- [Important Environment Variables](#important-environment-variables)
911
- [Verifying Authentication](#verifying-authentication)
@@ -96,6 +98,76 @@ This gives the container access to:
9698
- `/home/bun/.local/share/opencode` through the home mount for persisted local OpenCode and memory-related state
9799
- `/work` for the project you want OpenCode to read and modify
98100

101+
### Reusing Existing Gemini CLI Authentication
102+
103+
If you already authenticated with `gemini-cli` on the host, the container can reuse that login
104+
automatically.
105+
106+
At startup, `entrypoint.sh` checks whether `~/.gemini/oauth_creds.json` exists inside the
107+
container. If it does, the Bun script `convert-gemini.auth.ts` converts that Gemini OAuth state
108+
into OpenCode's auth store at `~/.local/share/opencode/auth.json`.
109+
110+
Typical run pattern:
111+
112+
```bash
113+
docker run -it --rm \
114+
-v $HOME:/home/bun \
115+
-v ${PWD}:/work \
116+
opencode-cli:dev
117+
```
118+
119+
With that home-directory mount:
120+
121+
- host `~/.gemini/oauth_creds.json` becomes available in the container at `/home/bun/.gemini/oauth_creds.json`
122+
- the entrypoint converts it into OpenCode auth automatically before launching `opencode`
123+
- existing entries in `~/.local/share/opencode/auth.json` are preserved and only the `google` provider entry is updated
124+
125+
Notes:
126+
127+
- If `~/.gemini/oauth_creds.json` is not present, startup stays silent and OpenCode launches normally
128+
- If the Gemini credentials file exists but is malformed or missing required token fields, container startup fails so the problem is visible
129+
- The converter path inside the image is `/usr/local/bin/convert-gemini.auth.ts`
130+
131+
### Manual Gemini Authentication in the Container
132+
133+
If you do not already have reusable Gemini CLI credentials on the host, you can authenticate
134+
manually from inside the container with the `opencode-gemini-auth` plugin.
135+
136+
Start the container with a bash shell instead of the normal entrypoint:
137+
138+
```bash
139+
docker run -it --rm \
140+
--entrypoint bash \
141+
-v $HOME:/home/bun \
142+
-v ${PWD}:/work \
143+
opencode-cli:dev
144+
```
145+
146+
Then run the login flow manually inside the container:
147+
148+
```bash
149+
opencode auth login
150+
```
151+
152+
In the OpenCode prompt flow:
153+
154+
- select `Google`
155+
- select `OAuth with Google (Gemini CLI)`
156+
- complete the browser-based authorization flow
157+
158+
If you are running the container in an environment where the browser callback cannot be completed
159+
automatically, use the fallback flow described by the plugin and paste the redirected callback URL
160+
or authorization code when prompted.
161+
162+
After successful login, the credential is stored in your mounted home directory under OpenCode's
163+
data path, so future container runs can reuse it:
164+
165+
- `/home/bun/.local/share/opencode/auth.json` for provider auth
166+
- `/home/bun/.config/opencode` for config
167+
168+
Once this has been done once, subsequent normal container starts can use the stored OpenCode auth
169+
directly, without repeating the manual login flow.
170+
99171
### Environment File Option
100172

101173
If your OpenCode setup depends on provider-specific environment variables, keep them in a local

convert-gemini.auth.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#!/usr/bin/env bun
2+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises"
3+
import os from "node:os"
4+
import path from "node:path"
5+
6+
type Gemini = {
7+
access_token?: string
8+
refresh_token?: string
9+
expiry_date?: number | string
10+
}
11+
12+
type OAuth = {
13+
type: "oauth"
14+
refresh: string
15+
access: string
16+
expires: number
17+
}
18+
19+
type Auth = Record<string, OAuth | Record<string, unknown>>
20+
21+
function fail(msg: string): never {
22+
console.error(`[convert-gemini.auth] ${msg}`)
23+
process.exit(1)
24+
}
25+
26+
function home() {
27+
return process.env.HOME || os.homedir()
28+
}
29+
30+
function data() {
31+
return process.env.XDG_DATA_HOME || path.join(home(), ".local", "share")
32+
}
33+
34+
function src() {
35+
return process.env.GEMINI_OAUTH_CREDS_PATH || path.join(home(), ".gemini", "oauth_creds.json")
36+
}
37+
38+
function dst() {
39+
return process.env.OPENCODE_AUTH_PATH || path.join(data(), "opencode", "auth.json")
40+
}
41+
42+
function ms(value: unknown) {
43+
if (typeof value === "number" && Number.isFinite(value)) {
44+
return value < 1e12 ? value * 1000 : value
45+
}
46+
47+
if (typeof value === "string" && value.trim()) {
48+
const num = Number(value)
49+
if (Number.isFinite(num)) return num < 1e12 ? num * 1000 : num
50+
const date = Date.parse(value)
51+
if (!Number.isNaN(date)) return date
52+
}
53+
54+
return Date.now() + 55 * 60 * 1000
55+
}
56+
57+
async function json(file: string) {
58+
return JSON.parse(await readFile(file, "utf8")) as Record<string, unknown>
59+
}
60+
61+
async function existing(file: string): Promise<Auth> {
62+
try {
63+
return (await json(file)) as Auth
64+
} catch {
65+
return {}
66+
}
67+
}
68+
69+
async function main() {
70+
const source = src()
71+
const target = dst()
72+
const creds = (await json(source)) as Gemini
73+
74+
if (!creds.refresh_token) fail(`missing refresh_token in ${source}`)
75+
if (!creds.access_token) fail(`missing access_token in ${source}`)
76+
77+
const auth = await existing(target)
78+
auth.google = {
79+
type: "oauth",
80+
refresh: creds.refresh_token,
81+
access: creds.access_token,
82+
expires: ms(creds.expiry_date),
83+
}
84+
85+
await mkdir(path.dirname(target), { recursive: true })
86+
await writeFile(target, `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 })
87+
await chmod(target, 0o600)
88+
}
89+
90+
await main().catch((err) => {
91+
fail(err instanceof Error ? err.message : String(err))
92+
})

entrypoint.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,11 @@
44
# Ensure MISE configuration is loaded
55
source /etc/bash.bashrc
66

7+
GEMINI_CREDS_PATH="${GEMINI_OAUTH_CREDS_PATH:-$HOME/.gemini/oauth_creds.json}"
8+
CONVERTER_PATH="${CONVERTER_PATH:-/usr/local/bin/convert-gemini.auth.ts}"
9+
10+
if [[ -f "${GEMINI_CREDS_PATH}" ]]; then
11+
bun "${CONVERTER_PATH}"
12+
fi
13+
714
exec /usr/local/bin/opencode "$@"

0 commit comments

Comments
 (0)