From 72d9a27f874a0df19546e7e70f199f29f180017d Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Tue, 17 Mar 2026 22:35:38 +0000 Subject: [PATCH 1/5] feat(auth): add Gemini OAuth credentials converter script Add a Typescript script that converts Gemini OAuth credentials from ~/.gemini/oauth_creds.json to OpenCode's auth.json format. Supports environment variable overrides for custom paths and handles various expiry date formats (seconds, milliseconds, ISO strings). --- convert-gemini.auth.ts | 92 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 convert-gemini.auth.ts diff --git a/convert-gemini.auth.ts b/convert-gemini.auth.ts new file mode 100644 index 0000000..a744810 --- /dev/null +++ b/convert-gemini.auth.ts @@ -0,0 +1,92 @@ +#!/usr/bin/env bun +import { chmod, mkdir, readFile, writeFile } from "node:fs/promises" +import os from "node:os" +import path from "node:path" + +type Gemini = { + access_token?: string + refresh_token?: string + expiry_date?: number | string +} + +type OAuth = { + type: "oauth" + refresh: string + access: string + expires: number +} + +type Auth = Record> + +function fail(msg: string): never { + console.error(`[convert-gemini.auth] ${msg}`) + process.exit(1) +} + +function home() { + return process.env.HOME || os.homedir() +} + +function data() { + return process.env.XDG_DATA_HOME || path.join(home(), ".local", "share") +} + +function src() { + return process.env.GEMINI_OAUTH_CREDS_PATH || path.join(home(), ".gemini", "oauth_creds.json") +} + +function dst() { + return process.env.OPENCODE_AUTH_PATH || path.join(data(), "opencode", "auth.json") +} + +function ms(value: unknown) { + if (typeof value === "number" && Number.isFinite(value)) { + return value < 1e12 ? value * 1000 : value + } + + if (typeof value === "string" && value.trim()) { + const num = Number(value) + if (Number.isFinite(num)) return num < 1e12 ? num * 1000 : num + const date = Date.parse(value) + if (!Number.isNaN(date)) return date + } + + return Date.now() + 55 * 60 * 1000 +} + +async function json(file: string) { + return JSON.parse(await readFile(file, "utf8")) as Record +} + +async function existing(file: string): Promise { + try { + return (await json(file)) as Auth + } catch { + return {} + } +} + +async function main() { + const source = src() + const target = dst() + const creds = (await json(source)) as Gemini + + if (!creds.refresh_token) fail(`missing refresh_token in ${source}`) + if (!creds.access_token) fail(`missing access_token in ${source}`) + + const auth = await existing(target) + auth.google = { + type: "oauth", + refresh: creds.refresh_token, + access: creds.access_token, + expires: ms(creds.expiry_date), + } + + await mkdir(path.dirname(target), { recursive: true }) + await writeFile(target, `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 }) + await chmod(target, 0o600) +} + +await main().catch((err) => { + fail(err instanceof Error ? err.message : String(err)) +}) From ca33622f5823102dca68116c9cc3e6a9b9adfb56 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Tue, 17 Mar 2026 22:35:56 +0000 Subject: [PATCH 2/5] build(docker): allow convert-gemini.auth.ts in docker context --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index ca7a3a1..35d8188 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,3 +2,4 @@ !Dockerfile !entrypoint.sh !git-export.py +!convert-gemini.auth.ts From 2086810655613642138d09e3b567bdd8b0d4d08f Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Tue, 17 Mar 2026 22:36:14 +0000 Subject: [PATCH 3/5] feat(auth): run Gemini credentials converter at container startup Add logic to entrypoint.sh to automatically detect and convert Gemini OAuth credentials when the container starts. Uses configurable paths for credentials and converter script with sensible defaults. --- entrypoint.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/entrypoint.sh b/entrypoint.sh index 13a129b..c965716 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -4,4 +4,11 @@ # Ensure MISE configuration is loaded source /etc/bash.bashrc +GEMINI_CREDS_PATH="${GEMINI_OAUTH_CREDS_PATH:-$HOME/.gemini/oauth_creds.json}" +CONVERTER_PATH="${CONVERTER_PATH:-/usr/local/bin/convert-gemini.auth.ts}" + +if [[ -f "${GEMINI_CREDS_PATH}" ]]; then + bun "${CONVERTER_PATH}" +fi + exec /usr/local/bin/opencode "$@" From 929e2a1ad1569f0cd7d173c8bda7e69cf9ff2c76 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Tue, 17 Mar 2026 22:36:36 +0000 Subject: [PATCH 4/5] feat(docker): add Gemini auth plugin support Install opencode-gemini-auth plugin globally via bun, add it to the opencode plugin configuration, and copy the credential conversion script for runtime use. --- Dockerfile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index df52273..574b557 100644 --- a/Dockerfile +++ b/Dockerfile @@ -135,6 +135,11 @@ bun install -g "opencode-ai@${OPENCODE_VERSION}" || exit 1 && popd ) || exit 1 +### +# Gemini plugin +# +bun install -g 'opencode-gemini-auth@latest' || exit 1 + ### # agent browser ( @@ -200,7 +205,8 @@ RUN <<'FOE' { "$schema": "https://opencode.ai/config.json", "plugin": [ - "engram" + "engram", + "file:///usr/local/bun/install/global/node_modules/opencode-gemini-auth" ], "mcp": { "engram": { @@ -255,6 +261,7 @@ EOF FOE COPY --chmod=0555 entrypoint.sh /entrypoint.sh +COPY --chmod=0555 convert-gemini.auth.ts /usr/local/bin/convert-gemini.auth.ts USER bun:bun From 226a8336243f8bf2bcad918d99cb787b75fc4187 Mon Sep 17 00:00:00 2001 From: Thomas Meckel Date: Tue, 17 Mar 2026 22:36:50 +0000 Subject: [PATCH 5/5] docs: document Gemini authentication options for container Add documentation for reusing existing Gemini CLI credentials and manual authentication flow inside the container. --- README.md | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/README.md b/README.md index e3206f3..62b8118 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ - [Building the Container Image](#building-the-container-image) - [Build Arguments](#build-arguments) - [Authentication Setup](#authentication-setup) + - [Reusing Existing Gemini CLI Authentication](#reusing-existing-gemini-cli-authentication) + - [Manual Gemini Authentication in the Container](#manual-gemini-authentication-in-the-container) - [Environment File Option](#environment-file-option) - [Important Environment Variables](#important-environment-variables) - [Verifying Authentication](#verifying-authentication) @@ -96,6 +98,76 @@ This gives the container access to: - `/home/bun/.local/share/opencode` through the home mount for persisted local OpenCode and memory-related state - `/work` for the project you want OpenCode to read and modify +### Reusing Existing Gemini CLI Authentication + +If you already authenticated with `gemini-cli` on the host, the container can reuse that login +automatically. + +At startup, `entrypoint.sh` checks whether `~/.gemini/oauth_creds.json` exists inside the +container. If it does, the Bun script `convert-gemini.auth.ts` converts that Gemini OAuth state +into OpenCode's auth store at `~/.local/share/opencode/auth.json`. + +Typical run pattern: + +```bash +docker run -it --rm \ + -v $HOME:/home/bun \ + -v ${PWD}:/work \ + opencode-cli:dev +``` + +With that home-directory mount: + +- host `~/.gemini/oauth_creds.json` becomes available in the container at `/home/bun/.gemini/oauth_creds.json` +- the entrypoint converts it into OpenCode auth automatically before launching `opencode` +- existing entries in `~/.local/share/opencode/auth.json` are preserved and only the `google` provider entry is updated + +Notes: + +- If `~/.gemini/oauth_creds.json` is not present, startup stays silent and OpenCode launches normally +- If the Gemini credentials file exists but is malformed or missing required token fields, container startup fails so the problem is visible +- The converter path inside the image is `/usr/local/bin/convert-gemini.auth.ts` + +### Manual Gemini Authentication in the Container + +If you do not already have reusable Gemini CLI credentials on the host, you can authenticate +manually from inside the container with the `opencode-gemini-auth` plugin. + +Start the container with a bash shell instead of the normal entrypoint: + +```bash +docker run -it --rm \ + --entrypoint bash \ + -v $HOME:/home/bun \ + -v ${PWD}:/work \ + opencode-cli:dev +``` + +Then run the login flow manually inside the container: + +```bash +opencode auth login +``` + +In the OpenCode prompt flow: + +- select `Google` +- select `OAuth with Google (Gemini CLI)` +- complete the browser-based authorization flow + +If you are running the container in an environment where the browser callback cannot be completed +automatically, use the fallback flow described by the plugin and paste the redirected callback URL +or authorization code when prompted. + +After successful login, the credential is stored in your mounted home directory under OpenCode's +data path, so future container runs can reuse it: + +- `/home/bun/.local/share/opencode/auth.json` for provider auth +- `/home/bun/.config/opencode` for config + +Once this has been done once, subsequent normal container starts can use the stored OpenCode auth +directly, without repeating the manual login flow. + ### Environment File Option If your OpenCode setup depends on provider-specific environment variables, keep them in a local