Skip to content

Commit 72d9a27

Browse files
committed
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).
1 parent 37b54a7 commit 72d9a27

File tree

1 file changed

+92
-0
lines changed

1 file changed

+92
-0
lines changed

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+
})

0 commit comments

Comments
 (0)