import * as crypto from 'crypto'; import { randomUUID } from 'crypto'; import { URLSearchParams, fileURLToPath } from 'url'; import axios from 'axios'; import https from 'https'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { z } from 'zod'; import { logger } from '@exocore/multi'; // ═══════════════════════════════════════════════════════════════════════════ // SECTION 1 — Constants // ═══════════════════════════════════════════════════════════════════════════ export const Endpoint = { GOOGLE: 'https://www.google.com', INIT: 'https://gemini.google.com/app', GENERATE: 'https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate', BATCH_EXEC: 'https://gemini.google.com/_/BardChatUi/data/batchexecute', UPLOAD: 'https://content-push.googleapis.com/upload', ROTATE_COOKIES: 'https://accounts.google.com/RotateCookies', PROCESS_GEM_FILE: 'https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/ProcessFile', }; export const RPC = { LIST_CHATS: 'MaZiqc', READ_CHAT: 'hNvQHb', DELETE_CHAT: 'GzXR5e', DELETE_CHAT_2: 'qWymEb', LIST_GEMS: 'CNgdBe', CREATE_GEM: 'oMH3Zd', UPDATE_GEM: 'kHv0Vd', DELETE_GEM: 'UXcSJb', LIST_NLM_NOTEBOOKS: 'NXpLKc', BARD_ACTIVITY: 'ESY5D', STATUS_POLL: 'aPya6c', ASYNC_POLL: 'kwDCne', POST_PROCESS: 'PCck7e', INIT_CONFIG: 'otAQ7b', INIT_FEATURES: 'GPRiHf', INIT_SETTINGS: 'maGuAc', INIT_STATE: 'cYRIkd', INIT_CAPABILITIES: 'ozz5Z', SYNC_STATE: 'L5adhe', USER_PREFS: 'K4WWud', USAGE_INFO: 'qpEbW', ACCOUNT_INFO: 'o30O0e', HISTORY_STATE: 'DYBcR', NOTIFICATION_STATE: 'ku4Jyf', }; export const BATCH_EXEC_HEADERS = { 'x-goog-ext-525001261-jspb': '[1,null,null,null,null,null,null,null,[4]]', 'x-goog-ext-73010989-jspb': '[0]', }; export const MODEL_HEADER_KEY = 'x-goog-ext-525001261-jspb'; export const TRACKING_HEADER_KEY = 'x-goog-ext-73010989-jspb'; export const SESSION_HEADER_KEY = 'x-goog-ext-525005358-jspb'; export const SAFETY_HEADER_KEY = 'x-goog-ext-73010990-jspb'; export const TEMP_MODEL_HASH = 'fbb127bbb056c959'; export const SYNC_STATE_NULL_COUNT = 87; export class Model { constructor(name, hash = null) { this.name = name; this.hash = hash; } get header() { if (!this.hash) return {}; return { [MODEL_HEADER_KEY]: `[1,null,null,null,"${this.hash}",null,null,0,[4],null,null,2]` }; } toString() { return `Model(name=${JSON.stringify(this.name)}, hash=${JSON.stringify(this.hash)})`; } equals(other) { if (other instanceof Model) return this.name === other.name; return false; } } export const MODELS = { unspecified: new Model('unspecified', null), fast: new Model('fast', '56fdd199312815e2'), thinking: new Model('thinking', 'e051ce1aa80aa576'), pro: new Model('pro', 'e6fa609c3fa255c0'), }; export const MODEL_ALIASES = { flash: 'fast', preview: 'fast', default: 'fast' }; export const DEFAULT_MODEL = MODELS.fast; export const TOKEN_FACTORY_MODELS = new Set(['pro', 'thinking']); export const CHROME_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36'; export const XBV_API_KEY = 'AIzaSyDr2UxVnv_U85AbhhY8XSHSIavUW0DC-sY'; export const DRIVE_PICKER_API_KEY = 'AIzaSyAw-cTyp9Xotzvu3vNDWhDU3E9NConkKxQ'; export const CHROME_HEADERS = { Accept: '*/*', 'Accept-Language': 'en-US,en;q=0.9', 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8', Origin: 'https://gemini.google.com', Referer: 'https://gemini.google.com/', 'User-Agent': CHROME_USER_AGENT, 'X-Same-Domain': '1', 'x-browser-channel': 'stable', 'x-browser-year': '2026', 'x-client-data': 'CJWJywE=', 'Sec-Ch-Ua': '"Chromium";v="145", "Google Chrome";v="145", "Not-A.Brand";v="24"', 'Sec-Ch-Ua-Mobile': '?0', 'Sec-Ch-Ua-Platform': '"macOS"', 'Sec-Fetch-Dest': 'empty', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Site': 'same-origin', }; export function computeXbv(userAgent) { return crypto.createHash('sha1').update(XBV_API_KEY + userAgent).digest('base64'); } export function getModel(name) { if (name in MODELS) return MODELS[name]; if (name in MODEL_ALIASES) return MODELS[MODEL_ALIASES[name]]; const all = Array.from(new Set([...Object.keys(MODELS), ...Object.keys(MODEL_ALIASES)])).sort(); throw new Error(`Unknown model: ${JSON.stringify(name)}. Available: ${all.join(', ')}`); } export const ErrorCode = { TEMPORARY_ERROR: 1013, USAGE_LIMIT_EXCEEDED: 1037, MODEL_INCONSISTENT: 1050, MODEL_HEADER_INVALID: 1052, IP_TEMPORARILY_BLOCKED: 1060, }; export const GEMINI_HEADERS = { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', Host: 'gemini.google.com', Origin: 'https://gemini.google.com', Referer: 'https://gemini.google.com/', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36', 'X-Same-Domain': '1', }; export const ROTATE_COOKIES_HEADERS = { 'Content-Type': 'application/json' }; export const UPLOAD_HEADERS = { 'Push-ID': 'feeds/mcudyrk2a4khkz' }; export const TOKEN_PATTERNS = { snlm0e: /"SNlM0e":\s*"(.*?)"/, cfb2h: /"cfb2h":\s*"(.*?)"/, fdrfje: /"FdrFJe":\s*"(.*?)"/, }; export const EMAIL_PATTERN = /"oPEP7c":\s*"([^"]+@[^"]+)"/; export const COOKIE_1PSID = '__Secure-1PSID'; export const COOKIE_1PSIDTS = '__Secure-1PSIDTS'; export const COOKIE_DOMAIN = '.google.com'; export const COOKIE_REFRESH_INTERVAL = 540; export const COOKIE_REFRESH_COOLDOWN = 60; export const ROTATE_COOKIES_BODY = '[000,"-0000000000000000000"]'; export const CONFIG_DIR_NAME = '.gemini-web-mcp-cli'; export const NLM_CONFIG_DIR_NAME = '.notebooklm-mcp-cli'; export const PROFILES_DIR = 'profiles'; export const CONFIG_FILE = 'config.json'; export const AUTH_FILE = 'auth.json'; export const ENV_PROFILE = 'GEMCLI_PROFILE'; export const DEFAULT_PROFILE_NAME = 'default'; export const REQID_INCREMENT = 100000; export const INNER_REQ_LIST_SIZE = 70; export const VIDEO_TOOL_ID = 11; export const IMAGE_TOOL_ID = 14; export const MUSIC_TOOL_ID = 21; export const RESPONSE_PATHS = { inner_json: [2], metadata: [1], candidates: [4], completion: [25], error_code: [5, 2, 0, 1, 0], candidate_rcid: [0], candidate_text: [1, 0], candidate_thoughts: [37, 0, 0], candidate_web_images: [12, 1], candidate_generated_images: [12, 7, 0], candidate_generated_music: [12, 86], candidate_generated_video: [12, 59], }; // ═══════════════════════════════════════════════════════════════════════════ // SECTION 2 — Exceptions // ═══════════════════════════════════════════════════════════════════════════ export class GeminiError extends Error { constructor(message) { super(message); this.name = new.target.name; Object.setPrototypeOf(this, new.target.prototype); } } export class AuthError extends GeminiError {} export class APIError extends GeminiError { constructor(message, errorCode = null) { super(message); this.errorCode = errorCode; } } export class UsageLimitExceeded extends GeminiError {} export class ModelInvalid extends GeminiError {} export class TemporarilyBlocked extends GeminiError {} export class GeminiTimeoutError extends GeminiError {} export class ImageGenerationError extends APIError {} export class VideoGenerationError extends APIError {} export class MusicGenerationError extends APIError {} export class ResearchError extends APIError {} export class ProfileError extends GeminiError {} export class TokenFactoryError extends GeminiError {} // ═══════════════════════════════════════════════════════════════════════════ // SECTION 3 — Profiles // ═══════════════════════════════════════════════════════════════════════════ function configDir() { return path.join(os.homedir(), CONFIG_DIR_NAME); } function nlmConfigDir() { return path.join(os.homedir(), NLM_CONFIG_DIR_NAME); } function isDir(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } } export class ProfileManager { constructor({ configDir: cd, nlmConfigDir: ncd } = {}) { this.configDir = cd ?? configDir(); this.profilesDir = path.join(this.configDir, PROFILES_DIR); this.configFile = path.join(this.configDir, CONFIG_FILE); this.nlmConfigDir = ncd ?? nlmConfigDir(); this.nlmProfilesDir = path.join(this.nlmConfigDir, PROFILES_DIR); } #ensureDirs() { fs.mkdirSync(this.configDir, { recursive: true }); fs.mkdirSync(this.profilesDir, { recursive: true }); } #profileDir(name) { return path.join(this.profilesDir, name); } #authFilePath(name) { return path.join(this.#profileDir(name), AUTH_FILE); } #nlmProfileDir(name) { return path.join(this.nlmProfilesDir, name); } #readConfig() { if (fs.existsSync(this.configFile)) { try { return JSON.parse(fs.readFileSync(this.configFile, 'utf-8')); } catch { return {}; } } return {}; } #writeConfig(config) { this.#ensureDirs(); fs.writeFileSync(this.configFile, JSON.stringify(config, null, 2)); } getActiveProfile() { const env = process.env[ENV_PROFILE]; if (env) return env; const cfg = this.#readConfig(); return cfg.active_profile ?? DEFAULT_PROFILE_NAME; } setActiveProfile(name) { if (!this.profileExists(name)) throw new ProfileError(`Profile '${name}' does not exist.`); const cfg = this.#readConfig(); cfg.active_profile = name; cfg.version = 1; this.#writeConfig(cfg); logger.info(`Switched to profile: ${name}`); } profileExists(name) { return isDir(this.#profileDir(name)) || isDir(this.#nlmProfileDir(name)); } #listGemcliProfiles() { if (!fs.existsSync(this.profilesDir)) return []; return fs.readdirSync(this.profilesDir, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name).sort(); } #listNlmProfiles() { if (!fs.existsSync(this.nlmProfilesDir)) return []; return fs.readdirSync(this.nlmProfilesDir, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name).sort(); } listProfiles() { return Array.from(new Set([...this.#listGemcliProfiles(), ...this.#listNlmProfiles()])).sort(); } listProfilesDetailed() { const profiles = new Map(); for (const name of this.#listNlmProfiles()) profiles.set(name, { name, source: 'nlm', path: this.#nlmProfileDir(name), displayName: `${name} (from NotebookLM)` }); for (const name of this.#listGemcliProfiles()) profiles.set(name, { name, source: 'gemcli', path: this.#profileDir(name), displayName: name }); return Array.from(profiles.values()).sort((a, b) => a.name.localeCompare(b.name)); } getProfileSource(name) { if (isDir(this.#profileDir(name))) return 'gemcli'; if (isDir(this.#nlmProfileDir(name))) return 'nlm'; throw new ProfileError(`Profile '${name}' does not exist.`); } createProfile(name) { this.#ensureDirs(); const dir = this.#profileDir(name); if (fs.existsSync(dir)) throw new ProfileError(`Profile '${name}' already exists.`); fs.mkdirSync(dir, { recursive: true }); logger.info(`Created profile: ${name}`); return dir; } deleteProfile(name) { const dir = this.#profileDir(name); if (!fs.existsSync(dir)) { if (isDir(this.#nlmProfileDir(name))) throw new ProfileError(`Profile '${name}' is from NotebookLM MCP. Delete it from NotebookLM instead.`); throw new ProfileError(`Profile '${name}' does not exist.`); } if (this.getActiveProfile() === name) throw new ProfileError(`Cannot delete the active profile '${name}'. Switch to another profile first.`); fs.rmSync(dir, { recursive: true, force: true }); logger.info(`Deleted profile: ${name}`); } renameProfile(oldName, newName) { const oldDir = this.#profileDir(oldName); if (!fs.existsSync(oldDir)) { if (isDir(this.#nlmProfileDir(oldName))) throw new ProfileError(`Profile '${oldName}' is from NotebookLM MCP. Rename it from NotebookLM instead.`); throw new ProfileError(`Profile '${oldName}' does not exist.`); } const trimmed = newName.trim(); if (!trimmed) throw new ProfileError('New profile name cannot be empty.'); if (this.profileExists(trimmed)) throw new ProfileError(`Profile '${trimmed}' already exists.`); const newDir = this.#profileDir(trimmed); fs.renameSync(oldDir, newDir); logger.debug(`Renamed profile dir: ${oldName} -> ${trimmed}`); const chromeProfilesDir = path.join(this.configDir, 'chrome-profiles'); const oldChrome = path.join(chromeProfilesDir, oldName); const newChrome = path.join(chromeProfilesDir, trimmed); if (fs.existsSync(oldChrome)) { fs.renameSync(oldChrome, newChrome); logger.debug(`Renamed Chrome profile: ${oldName} -> ${trimmed}`); } const oldTf = path.join(chromeProfilesDir, `${oldName}_tf`); const newTf = path.join(chromeProfilesDir, `${trimmed}_tf`); if (fs.existsSync(oldTf)) { fs.renameSync(oldTf, newTf); logger.debug(`Renamed TF profile: ${oldName}_tf -> ${trimmed}_tf`); } const authFile = this.#authFilePath(trimmed); if (fs.existsSync(authFile)) { try { const data = JSON.parse(fs.readFileSync(authFile, 'utf-8')); const oldPath = data.chrome_profile_path ?? ''; if (oldPath && oldPath.includes(oldName)) { data.chrome_profile_path = oldPath.replace(`/${oldName}`, `/${trimmed}`); fs.writeFileSync(authFile, JSON.stringify(data, null, 2)); } } catch (e) { logger.warn(`Could not update auth.json: ${e.message}`); } } if (this.getActiveProfile() === oldName) { const cfg = this.#readConfig(); cfg.active_profile = trimmed; this.#writeConfig(cfg); logger.debug(`Updated active profile: ${oldName} -> ${trimmed}`); } logger.info(`Renamed profile: ${oldName} -> ${trimmed}`); } loadAuth(name) { const profileName = name ?? this.getActiveProfile(); const gemcliAuth = this.#authFilePath(profileName); if (fs.existsSync(gemcliAuth)) { try { return JSON.parse(fs.readFileSync(gemcliAuth, 'utf-8')); } catch (e) { logger.warn(`Failed to load gemcli auth for '${profileName}': ${e.message}`); } } const nlmChromeDir = path.join(this.nlmConfigDir, 'chrome-profiles', profileName); if (isDir(nlmChromeDir)) { logger.debug(`Using NLM Chrome profile for '${profileName}': ${nlmChromeDir}`); return { chrome_profile_path: nlmChromeDir, _source: 'nlm', _nlm_profile: profileName }; } return {}; } saveAuth(authData, name) { const profileName = name ?? this.getActiveProfile(); this.#ensureDirs(); const dir = this.#profileDir(profileName); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(this.#authFilePath(profileName), JSON.stringify(authData, null, 2)); logger.debug(`Saved auth for profile: ${profileName}`); } getChromeProfilePath(name) { const profileName = name ?? this.getActiveProfile(); const gemcliAuth = this.#authFilePath(profileName); if (fs.existsSync(gemcliAuth)) { try { const data = JSON.parse(fs.readFileSync(gemcliAuth, 'utf-8')); if (data.chrome_profile_path) return data.chrome_profile_path; } catch { } } const nlmChrome = path.join(this.nlmConfigDir, 'chrome-profiles', profileName); if (isDir(nlmChrome)) return nlmChrome; return null; } hasNlmProfiles() { return this.#listNlmProfiles().length > 0; } isFirstRun() { return this.#listGemcliProfiles().length === 0; } } // ═══════════════════════════════════════════════════════════════════════════ // SECTION 4 — Models (data classes) // ═══════════════════════════════════════════════════════════════════════════ function stripPrefix(s, prefix) { return s.startsWith(prefix) ? s.slice(prefix.length) : s; } function ensurePrefix(s, prefix) { if (!s) return s; return s.startsWith(prefix) ? s : prefix + s; } export class RPCPayload { constructor({ rpcId, payload = '[]', identifier = 'generic' }) { this.rpcId = rpcId; this.payload = payload; this.identifier = identifier; } serialize() { return [this.rpcId, this.payload, null, this.identifier]; } } export class ConversationMetadata { constructor({ cid = '', rid = '', rcid = '', extra } = {}) { this.cid = stripPrefix(cid, 'c_'); this.rid = stripPrefix(rid, 'r_'); this.rcid = stripPrefix(rcid, 'rc_'); this.extra = extra ?? new Array(7).fill(null); } toList() { const cid = this.cid ? ensurePrefix(this.cid, 'c_') : ''; const rid = this.rid ? ensurePrefix(this.rid, 'r_') : ''; const rcid = this.rcid ? ensurePrefix(this.rcid, 'rc_') : ''; const tail = [...this.extra]; while (tail.length < 7) tail.push(null); if (tail.length > 7) tail.length = 7; return [cid, rid, rcid, ...tail, '']; } static fromList(data) { if (!Array.isArray(data)) return new ConversationMetadata(); return new ConversationMetadata({ cid: typeof data[0] === 'string' && data[0] ? data[0] : '', rid: typeof data[1] === 'string' && data[1] ? data[1] : '', rcid: typeof data[2] === 'string' && data[2] ? data[2] : '', extra: data.length > 3 ? data.slice(3, 10) : new Array(7).fill(null), }); } } export class Candidate { constructor({ rcid = null, text = null, thoughts = null, webImages = null, generatedImages = null, generatedMusic = null, generatedVideo = null } = {}) { this.rcid = rcid; this.text = text; this.thoughts = thoughts; this.webImages = webImages; this.generatedImages = generatedImages; this.generatedMusic = generatedMusic; this.generatedVideo = generatedVideo; } } export class StreamResponse { constructor({ metadata = null, candidates = [], text = null, thoughts = null, isComplete = false, serverModelHash = null, serverModelLabel = null } = {}) { this.metadata = metadata ?? new ConversationMetadata(); this.candidates = candidates; this.text = text; this.thoughts = thoughts; this.isComplete = isComplete; this.serverModelHash = serverModelHash; this.serverModelLabel = serverModelLabel; } get hasImages() { return this.candidates.some(c => c.generatedImages?.length > 0); } get hasMusic() { return this.candidates.some(c => c.generatedMusic?.length > 0); } get hasVideo() { return this.candidates.some(c => c.generatedVideo?.length > 0); } } export class BatchResult { constructor({ rpcId = null, data = null, raw = null } = {}) { this.rpcId = rpcId; this.data = data; this.raw = raw; } } export const ZRPCPayload = z.object({ rpcId: z.string(), payload: z.string().default('[]'), identifier: z.string().default('generic') }); export const ZConversationMetadata = z.object({ cid: z.string().default(''), rid: z.string().default(''), rcid: z.string().default(''), extra: z.array(z.unknown()).default(() => new Array(7).fill(null)) }); export const ZCandidate = z.object({ rcid: z.string().nullable().optional(), text: z.string().nullable().optional(), thoughts: z.string().nullable().optional(), webImages: z.array(z.unknown()).nullable().optional(), generatedImages: z.array(z.unknown()).nullable().optional(), generatedMusic: z.array(z.unknown()).nullable().optional(), generatedVideo: z.array(z.unknown()).nullable().optional() }); // ═══════════════════════════════════════════════════════════════════════════ // SECTION 5 — Auth // ═══════════════════════════════════════════════════════════════════════════ export class AuthTokens { constructor(snlm0e = '', cfb2h = '', fdrfje = '') { this.snlm0e = snlm0e; this.cfb2h = cfb2h; this.fdrfje = fdrfje; } } export class AuthState { constructor({ cookies = {}, tokens = new AuthTokens(), chromeProfilePath = null, lastRefreshed = 0, email = '' } = {}) { this.cookies = cookies; this.tokens = tokens; this.chromeProfilePath = chromeProfilePath; this.lastRefreshed = lastRefreshed; this.email = email; } toDict() { const d = { cookies: this.cookies, tokens: { snlm0e: this.tokens.snlm0e, cfb2h: this.tokens.cfb2h, fdrfje: this.tokens.fdrfje }, chrome_profile_path: this.chromeProfilePath, last_refreshed: this.lastRefreshed }; if (this.email) d.email = this.email; return d; } static fromDict(data) { const tokensData = data.tokens ?? {}; return new AuthState({ cookies: data.cookies ?? {}, tokens: new AuthTokens(tokensData.snlm0e ?? '', tokensData.cfb2h ?? '', tokensData.fdrfje ?? ''), chromeProfilePath: data.chrome_profile_path ?? null, lastRefreshed: data.last_refreshed ?? 0, email: data.email ?? '', }); } get isValid() { return Boolean(this.cookies[COOKIE_1PSID] && this.tokens.snlm0e); } } export function extractTokens(html) { const tokens = new AuthTokens(); for (const [name, pattern] of Object.entries(TOKEN_PATTERNS)) { const m = html.match(pattern); if (m) tokens[name] = m[1]; } if (!tokens.snlm0e) throw new AuthError('Failed to extract SNlM0e access token from Gemini page. Cookies may be expired or invalid.'); return tokens; } export function extractEmail(html) { const m = html.match(EMAIL_PATTERN); return m ? m[1] : ''; } export class CookieJar { constructor(cookies = {}) { this._store = new Map(); if (cookies && typeof cookies === 'object') { for (const [k, v] of Object.entries(cookies)) { if (typeof v === 'string') this._store.set(k, v); } } } set(name, value, attrs = {}) { this._store.set(name, String(value)); return this; } get(name) { return this._store.get(name) ?? null; } toObject() { const o = {}; for (const [k, v] of this._store) o[k] = v; return o; } toHeader() { return Array.from(this._store.entries()).map(([k, v]) => `${k}=${v}`).join('; '); } toArray() { return Array.from(this._store.entries()).map(([k, v]) => ({ name: k, value: v, domain: COOKIE_DOMAIN, path: '/', secure: true, httpOnly: true, session: false })); } attachTo(client) { const self = this; client.interceptors.request.use(config => { const existing = config.headers.cookie || config.headers.Cookie || ''; const our = self.toHeader(); config.headers.cookie = existing ? `${existing}; ${our}` : our; return config; }); client.interceptors.response.use(response => { const setCookie = response.headers['set-cookie']; if (setCookie) { const arr = Array.isArray(setCookie) ? setCookie : [setCookie]; for (const raw of arr) { const eq = raw.indexOf('='); const semi = raw.indexOf(';'); if (eq > 0) { const name = raw.slice(0, eq).trim(); const value = semi > eq ? raw.slice(eq + 1, semi).trim() : raw.slice(eq + 1).trim(); if (name && value) self._store.set(name, value); } } } return response; }); } } export async function fetchTokens(cookies, proxy = null) { const jar = new CookieJar(cookies); const client = axios.create({ timeout: 30_000, headers: { ...GEMINI_HEADERS }, maxRedirects: 5, proxy: proxy ? parseAxiosProxy(proxy) : false, validateStatus: () => true }); jar.attachTo(client); const res = await client.get(Endpoint.INIT); if (res.status >= 400) throw new AuthError(`Gemini init page returned ${res.status} — cookies may be invalid.`); const html = String(res.data ?? ''); return { tokens: extractTokens(html), cookies: jar.toObject(), email: extractEmail(html) }; } export async function rotateCookies(cookies, proxy = null) { const jar = new CookieJar(cookies); const client = axios.create({ timeout: 30_000, headers: { ...ROTATE_COOKIES_HEADERS }, proxy: proxy ? parseAxiosProxy(proxy) : false, validateStatus: () => true }); jar.attachTo(client); const res = await client.post(Endpoint.ROTATE_COOKIES, ROTATE_COOKIES_BODY); if (res.status === 401) throw new AuthError('Cookie rotation returned 401. Full re-authentication required.'); if (res.status >= 400) throw new AuthError(`Cookie rotation failed: HTTP ${res.status}`); const updated = jar.toObject(); if (updated[COOKIE_1PSIDTS] && updated[COOKIE_1PSIDTS] !== cookies[COOKIE_1PSIDTS]) return updated; return null; } function parseAxiosProxy(proxy) { try { const u = new URL(proxy); const port = u.port ? parseInt(u.port, 10) : (u.protocol === 'https:' ? 443 : 80); const out = { protocol: u.protocol.replace(':', ''), host: u.hostname, port }; if (u.username) out.auth = { username: decodeURIComponent(u.username), password: decodeURIComponent(u.password ?? '') }; return out; } catch { return false; } } export class AuthManager { #state = null; constructor(profileManager, profileName = null, proxy = null) { this.profileManager = profileManager; this.profileName = profileName; this.proxy = proxy; } get state() { if (!this.#state) throw new AuthError('AuthManager not initialized. Call load() first.'); return this.#state; } load() { const data = this.profileManager.loadAuth(this.profileName ?? undefined); this.#state = data && Object.keys(data).length > 0 ? AuthState.fromDict(data) : new AuthState(); return this.#state; } save() { if (this.#state) this.profileManager.saveAuth(this.#state.toDict(), this.profileName ?? undefined); } async refreshTokens() { if (!this.state.cookies[COOKIE_1PSID] && !this.state.cookies['__Secure-3PSID']) throw new AuthError('No cookies available. Provide cookies first.'); logger.debug('auth: refreshing tokens from Gemini app page…'); const { tokens, cookies, email } = await fetchTokens(this.state.cookies, this.proxy); this.state.tokens = tokens; if (email) this.state.email = email; for (const [k, v] of Object.entries(cookies)) this.state.cookies[k] = v; this.state.lastRefreshed = Date.now() / 1000; this.save(); logger.debug('auth: tokens refreshed.'); return tokens; } async refreshCookies() { const elapsed = Date.now() / 1000 - this.state.lastRefreshed; if (elapsed < COOKIE_REFRESH_COOLDOWN) { logger.debug(`auth: cookie-refresh cooldown ${Math.round(COOKIE_REFRESH_COOLDOWN - elapsed)}s remaining`); return false; } try { const updated = await rotateCookies(this.state.cookies, this.proxy); if (updated) { this.state.cookies = updated; this.state.lastRefreshed = Date.now() / 1000; this.save(); logger.debug('auth: cookies refreshed via RotateCookies.'); return true; } } catch (err) { if (err instanceof AuthError) { logger.warn('auth: cookie rotation failed (401). Full re-auth needed.'); throw err; } logger.warn(`auth: cookie rotation failed: ${err.message}`); } return false; } async recover() { try { await this.refreshTokens(); if (this.state.isValid) return this.state; } catch { logger.debug('auth: layer 1 (token refresh) failed.'); } try { this.load(); if (this.state.isValid) { logger.debug('auth: layer 2 (disk reload) succeeded.'); return this.state; } } catch { logger.debug('auth: layer 2 (disk reload) failed.'); } throw new AuthError('All recovery layers exhausted. Re-paste your Gemini cookies via POST /api/gemini/cookies.'); } saveCookies(cookies) { if (!this.#state) this.#state = new AuthState(); this.#state.cookies = { ...cookies }; this.#state.lastRefreshed = Date.now() / 1000; this.save(); } } // ═══════════════════════════════════════════════════════════════════════════ // SECTION 6 — Parser // ═══════════════════════════════════════════════════════════════════════════ const LENGTH_MARKER_RE = /^(\d+)\n/; function utf16CharCount(s, start, utf16Units) { const remaining = s.length - start; if (remaining < utf16Units) return [remaining, remaining]; return [utf16Units, utf16Units]; } export function stripJsonpPrefix(text) { if (text.startsWith(")]}'")) return text.slice(4).replace(/^\n+/, ''); return text; } export function parseFrames(content) { let pos = 0; const total = content.length; const frames = []; while (pos < total) { while (pos < total && /\s/.test(content[pos])) pos++; if (pos >= total) break; const slice = content.slice(pos); const m = slice.match(LENGTH_MARKER_RE); if (!m) break; const frameLength = parseInt(m[1], 10); const contentStart = pos + m[1].length; const [charCount] = utf16CharCount(content, contentStart, frameLength); const endPos = contentStart + charCount; const chunk = content.slice(contentStart, endPos).trim(); pos = endPos; if (!chunk) continue; try { const parsed = JSON.parse(chunk); if (Array.isArray(parsed)) frames.push(...parsed); else frames.push(parsed); } catch { logger.debug(`Failed to parse frame JSON: ${chunk.slice(0, 100)}...`); } } return [frames, content.slice(pos)]; } export function extractJsonFromResponse(text) { let content = stripJsonpPrefix(text).replace(/^\s+/, ''); const [frames] = parseFrames(content); if (frames.length > 0) return frames; try { const parsed = JSON.parse(content.trim()); return Array.isArray(parsed) ? parsed : [parsed]; } catch { } const collected = []; for (const rawLine of content.trim().split(/\r?\n/)) { const line = rawLine.trim(); if (!line) continue; try { const parsed = JSON.parse(line); if (Array.isArray(parsed)) collected.push(...parsed); else if (parsed !== null && typeof parsed === 'object') collected.push(parsed); } catch { continue; } } if (collected.length > 0) return collected; throw new Error('Could not parse any valid JSON from the response.'); } export function getNested(data, path, defaultValue = null) { let current = data; for (const key of path) { try { if (typeof key === 'number') { if (Array.isArray(current)) { if (key >= -current.length && key < current.length) { current = current[key < 0 ? current.length + key : key]; } else if (current.length === 1 && current[0] !== null && typeof current[0] === 'object' && !Array.isArray(current[0])) { const pk = String(key + 1); if (pk in current[0]) current = current[0][pk]; else return defaultValue; } else return defaultValue; } else if (current !== null && typeof current === 'object' && !Array.isArray(current)) { const pk = String(key + 1); if (pk in current) current = current[pk]; else return defaultValue; } else return defaultValue; } else { if (current !== null && typeof current === 'object' && key in current) current = current[key]; else return defaultValue; } } catch { return defaultValue; } } return (current ?? defaultValue); } export function parseBatchexecuteResponse(text) { const frames = extractJsonFromResponse(text); const results = []; for (const frame of frames) { if (!Array.isArray(frame)) continue; if (frame.length >= 3 && frame[0] === 'wrb.fr') { const rawJson = frame[2]; let data = null; if (rawJson && typeof rawJson === 'string') { try { data = JSON.parse(rawJson); } catch { data = rawJson; } } results.push({ rpc_id: frame[1], data, raw: rawJson }); } } return results; } const KNOWN_MODEL_HASHES = new Set(['56fdd199312815e2', 'e051ce1aa80aa576', 'e6fa609c3fa255c0', 'fbb127bbb056c959', '9d8ca3786ebdfbea', '5bf011840784117a']); const KNOWN_MODEL_LABELS = new Set(['Fast', 'Thinking', 'Pro']); function extractServerModel(data, depth = 0) { if (depth > 10 || !Array.isArray(data)) return [null, null]; for (let i = 0; i < data.length; i++) { const item = data[i]; if (typeof item === 'string' && item.length === 16 && KNOWN_MODEL_HASHES.has(item)) { let label = null; for (let off = 1; off <= 5; off++) { if (i + off < data.length) { const v = data[i + off]; if (typeof v === 'string' && KNOWN_MODEL_LABELS.has(v)) { label = v; break; } } } return [item, label]; } } for (const item of data) { if (Array.isArray(item)) { const [h, l] = extractServerModel(item, depth + 1); if (h) return [h, l]; } } return [null, null]; } export function parseStreamResponse(text) { const frames = extractJsonFromResponse(text); const results = []; for (const frame of frames) { if (!Array.isArray(frame)) continue; if (frame.length < 3 || frame[0] !== 'wrb.fr') continue; const rawInner = frame[2]; if (!rawInner || typeof rawInner !== 'string') continue; let inner; try { inner = JSON.parse(rawInner); } catch { continue; } if (!Array.isArray(inner)) continue; const result = { metadata: getNested(inner, [1]), candidates: [], text: null, thoughts: null, completion: getNested(inner, [25]), server_model_hash: null, server_model_label: null }; const candidatesData = getNested(inner, [4]); if (Array.isArray(candidatesData)) { for (const candidate of candidatesData) { if (!Array.isArray(candidate)) continue; result.candidates.push({ rcid: getNested(candidate, [0]), text: getNested(candidate, [1, 0]), thoughts: getNested(candidate, [37, 0, 0]), web_images: getNested(candidate, [12, 1]), generated_images: getNested(candidate, [12, 7, 0]), generated_music: getNested(candidate, [12, 86]), generated_video: getNested(candidate, [12, 59]), }); } if (result.candidates.length > 0) { result.text = result.candidates[0].text; result.thoughts = result.candidates[0].thoughts; } } const [hash, label] = extractServerModel(inner); if (hash) { result.server_model_hash = hash; result.server_model_label = label; } results.push(result); } return results; } // ═══════════════════════════════════════════════════════════════════════════ // SECTION 7 — RPC Transport // ═══════════════════════════════════════════════════════════════════════════ export function buildBatchexecuteBody(payloads, accessToken) { const serialized = [payloads.map(p => p.serialize())]; const params = new URLSearchParams(); params.set('f.req', JSON.stringify(serialized)); params.set('at', accessToken); return params.toString() + '&'; } export function buildBatchexecuteUrl({ rpcIds, reqid, buildLabel, sessionId, sourcePath = '/app' }) { const params = new URLSearchParams(); params.set('rpcids', rpcIds.join(',')); params.set('source-path', sourcePath); if (buildLabel) params.set('bl', buildLabel); if (sessionId) params.set('f.sid', sessionId); params.set('hl', 'en'); params.set('_reqid', String(reqid)); params.set('rt', 'c'); return `${Endpoint.BATCH_EXEC}?${params.toString()}`; } export function buildStreamgenerateBody({ prompt, accessToken, metadata, fileData, gemId, toolId, stylePreset, botguardToken, botguardHash }) { const inner = new Array(INNER_REQ_LIST_SIZE).fill(null); const innerZero = [prompt, 0, null, fileData ?? null, null, null, 0]; if (stylePreset) { try { logger.debug('musicPresets module not yet ported; ignoring stylePreset'); } catch { } } inner[0] = innerZero; inner[1] = ['en']; if (metadata) inner[2] = metadata.toList(); else inner[2] = ['', '', '', null, null, null, null, null, null, null]; if (botguardToken) inner[3] = botguardToken; if (botguardHash) inner[4] = botguardHash; inner[6] = [1]; inner[7] = 1; inner[10] = 1; inner[11] = 0; inner[17] = [[0]]; inner[18] = 0; if (gemId) inner[19] = gemId; inner[27] = 1; inner[30] = [4]; inner[41] = [1]; if (toolId !== null && toolId !== undefined) inner[49] = toolId; inner[53] = 0; inner[59] = randomUUID().toUpperCase(); inner[61] = []; const nowMs = Date.now(); inner[66] = [Math.floor(nowMs / 1000), (nowMs % 1000) * 1_000_000]; inner[68] = 2; const params = new URLSearchParams(); params.set('f.req', JSON.stringify([null, JSON.stringify(inner)])); params.set('at', accessToken); return params.toString() + '&'; } export function buildStreamgenerateUrl({ reqid, buildLabel, sessionId }) { const params = new URLSearchParams(); if (buildLabel) params.set('bl', buildLabel); if (sessionId) params.set('f.sid', sessionId); params.set('hl', 'en'); params.set('_reqid', String(reqid)); params.set('rt', 'c'); return `${Endpoint.GENERATE}?${params.toString()}`; } export function buildHeaders(model, cid, tempSession = null) { const headers = { ...GEMINI_HEADERS }; if (tempSession) { const { clientUUID, sessionUUID } = tempSession; headers[MODEL_HEADER_KEY] = `[1,null,null,null,"${TEMP_MODEL_HASH}",null,null,1,[4],null,null,1,null,null,1,null,"${clientUUID}"]`; headers[SESSION_HEADER_KEY] = `["${sessionUUID}",1]`; headers[TRACKING_HEADER_KEY] = '[0]'; headers[SAFETY_HEADER_KEY] = '[0,0,0]'; } else if (model && Object.keys(model.header).length > 0) { Object.assign(headers, model.header); } if (cid) { const bare = cid.startsWith('c_') ? cid.slice(2) : cid; headers.Referer = `https://gemini.google.com/app/${bare}`; } return headers; } export function buildSyncStateBody(accessToken) { const arr = new Array(SYNC_STATE_NULL_COUNT).fill(null); arr[SYNC_STATE_NULL_COUNT - 1] = 5; const payload = JSON.stringify([arr, [['popup_zs_visits_cooldown']]]); const rpc = new RPCPayload({ rpcId: RPC.SYNC_STATE, payload }); return buildBatchexecuteBody([rpc], accessToken); } export function buildStreamResponse(responseText) { const parsedFrames = parseStreamResponse(responseText); const result = new StreamResponse(); const prevMedia = new Map(); for (const frame of parsedFrames) { if (frame.metadata) result.metadata = ConversationMetadata.fromList(frame.metadata); if (frame.candidates?.length > 0) { const newCandidates = []; for (let i = 0; i < frame.candidates.length; i++) { const c = frame.candidates[i]; const prev = prevMedia.get(i) ?? { images: null, music: null, video: null }; const candidate = new Candidate({ rcid: c.rcid, text: c.text, thoughts: c.thoughts, webImages: c.web_images, generatedImages: c.generated_images || prev.images, generatedMusic: c.generated_music || prev.music, generatedVideo: c.generated_video || prev.video, }); newCandidates.push(candidate); prevMedia.set(i, { images: candidate.generatedImages, music: candidate.generatedMusic, video: candidate.generatedVideo }); } result.candidates = newCandidates; } if (frame.text) result.text = frame.text; if (frame.thoughts) result.thoughts = frame.thoughts; if (frame.completion !== null && frame.completion !== undefined) result.isComplete = true; if (frame.server_model_hash) { result.serverModelHash = frame.server_model_hash; result.serverModelLabel = frame.server_model_label; } } return result; } function randomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } export class RPCTransport { #reqid; constructor({ client, accessToken, buildLabel = null, sessionId = null }) { this.client = client; this.accessToken = accessToken; this.buildLabel = buildLabel; this.sessionId = sessionId; this.#reqid = randomInt(10000, 99999); } #nextReqid() { const c = this.#reqid; this.#reqid += REQID_INCREMENT; return c; } async batchexecute(payloads, sourcePath = '/app') { const rpcIds = payloads.map(p => p.rpcId); const url = buildBatchexecuteUrl({ rpcIds, reqid: this.#nextReqid(), buildLabel: this.buildLabel, sessionId: this.sessionId, sourcePath }); const body = buildBatchexecuteBody(payloads, this.accessToken); const cidFromPath = sourcePath.startsWith('/app/') ? sourcePath.slice('/app/'.length) : null; const headers = { ...buildHeaders(null, cidFromPath), ...BATCH_EXEC_HEADERS }; logger.debug(`batchexecute: ${rpcIds.join(',')}`); const response = await this.client.post(url, body, { headers, responseType: 'text', transformResponse: [d => d] }); if (response.status >= 400) throw new Error(`batchexecute HTTP ${response.status}: ${String(response.data).slice(0, 200)}`); return parseBatchexecuteResponse(String(response.data)).map(r => new BatchResult({ rpcId: r.rpc_id, data: r.data, raw: r.raw })); } async syncState() { const url = buildBatchexecuteUrl({ rpcIds: [RPC.SYNC_STATE], reqid: this.#nextReqid(), buildLabel: this.buildLabel, sessionId: this.sessionId }); const body = buildSyncStateBody(this.accessToken); const headers = { ...GEMINI_HEADERS, [TRACKING_HEADER_KEY]: '[0]' }; logger.debug('syncState: calling L5adhe for temp chat session'); const response = await this.client.post(url, body, { headers, responseType: 'text', transformResponse: [d => d] }); if (response.status >= 400) logger.warn(`syncState: HTTP ${response.status} — continuing anyway`); return response; } async streamGenerate(opts) { const tempSession = opts.temp ?? null; if (tempSession) await this.syncState().catch(e => logger.warn(`syncState failed: ${e.message}`)); const url = buildStreamgenerateUrl({ reqid: this.#nextReqid(), buildLabel: this.buildLabel, sessionId: this.sessionId }); const body = buildStreamgenerateBody({ prompt: opts.prompt, accessToken: this.accessToken, metadata: opts.metadata, fileData: opts.fileData, gemId: opts.gemId, toolId: opts.toolId, stylePreset: opts.stylePreset }); const cid = opts.metadata?.cid ?? null; const headers = buildHeaders(opts.model ?? DEFAULT_MODEL, cid, tempSession); logger.debug(`StreamGenerate${tempSession ? ' [TEMP]' : ''}: ${opts.prompt.slice(0, 50)}...`); const { onDelta } = opts; if (onDelta) { const response = await this.client.post(url, body, { headers, responseType: 'stream', timeout: 120000 }); if (response.status >= 400) throw new Error(`StreamGenerate HTTP ${response.status}`); return this._streamFrames(response.data, onDelta); } const response = await this.client.post(url, body, { headers, responseType: 'text', transformResponse: [d => d] }); if (response.status >= 400) throw new Error(`StreamGenerate HTTP ${response.status}: ${String(response.data).slice(0, 200)}`); return buildStreamResponse(String(response.data)); } async _streamFrames(stream, onDelta) { let buffer = ''; let chunkCount = 0; let frameCount = 0; let totalDeltaChars = 0; let loggedFrames = 0; let prevText = ''; const result = { metadata: null, cid: null, rid: null, rcid: null, serverModelLabel: null }; const emitInner = (inner) => { frameCount++; if (loggedFrames < 3) { const textPreview = JSON.stringify(getNested(inner, [4, 0, 1, 0])).slice(0, 80); const candLen = Array.isArray(getNested(inner, [4])) ? getNested(inner, [4]).length : 0; logger.debug(`_streamFrames: frame #${frameCount} inner_keys=${Object.keys(inner).length} inner[4] len=${candLen} text=${textPreview} inner[25]=${JSON.stringify(getNested(inner, [25]))}`); loggedFrames++; } if (!result.metadata) { const metaList = getNested(inner, [1]); if (Array.isArray(metaList)) { try { result.metadata = ConversationMetadata.fromList(metaList); } catch {} } } const text = getNested(inner, [4, 0, 1, 0]); if (text && text !== prevText) { const delta = text.slice(prevText.length); prevText = text; totalDeltaChars += delta.length; if (delta) onDelta(delta); } if (!result.serverModelLabel) { const [, label] = extractServerModel(inner); if (label) result.serverModelLabel = label; } }; const tryParseWrb = (frame) => { if (!Array.isArray(frame) || frame.length < 3 || frame[0] !== 'wrb.fr') return; const rawInner = frame[2]; if (!rawInner || typeof rawInner !== 'string') return; let inner; try { inner = JSON.parse(rawInner); } catch { return; } if (!Array.isArray(inner)) return; emitInner(inner); }; const consume = () => { buffer = stripJsonpPrefix(buffer); let consumed; // 1) Try length-prefixed frames do { consumed = false; buffer = buffer.replace(/^\s+/, ''); if (!buffer) break; const m = buffer.match(LENGTH_MARKER_RE); if (!m) break; const frameLength = parseInt(m[1], 10); const contentStart = m.index + m[0].length; const remaining = buffer.slice(contentStart); const [charCount] = utf16CharCount(remaining, 0, frameLength); if (remaining.length < charCount) break; let frameJson; try { frameJson = JSON.parse(remaining.slice(0, charCount)); } catch { break; } buffer = remaining.slice(charCount); consumed = true; if (Array.isArray(frameJson)) { for (const f of frameJson) tryParseWrb(f); } } while (consumed); // 2) Try newline-delimited JSON on remainder if (buffer.trim()) { const lines = buffer.split(/\r?\n/); const remaining = []; for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; try { const parsed = JSON.parse(trimmed); if (Array.isArray(parsed)) { for (const f of parsed) tryParseWrb(f); } else { remaining.push(line); } } catch { remaining.push(line); } } buffer = remaining.join('\n'); } }; for await (const chunk of stream) { chunkCount++; const chunkStr = chunk.toString(); if (chunkCount <= 3) logger.debug(`_streamFrames: chunk #${chunkCount} size=${chunkStr.length} first100=${chunkStr.slice(0, 100)}`); buffer += chunkStr; consume(); } consume(); // final pass logger.debug(`_streamFrames: done — ${chunkCount} chunks, ${frameCount} frames, ${totalDeltaChars} chars streamed, prevText="${prevText.slice(0, 100)}"`); if (result.metadata) { result.cid = result.metadata.cid ?? null; result.rid = result.metadata.rid ?? null; result.rcid = result.metadata.rcid ?? null; } return result; } } // ═══════════════════════════════════════════════════════════════════════════ // SECTION 8 — Upload // ═══════════════════════════════════════════════════════════════════════════ const UPLOAD_ENDPOINT = 'https://push.clients6.google.com/upload/'; const PUSH_ID_UPLOAD = 'feeds/mcudyrk2a4khkz'; const X_TENANT_ID = 'bard-storage'; const X_CLIENT_DATA = 'CJvoygE='; const X_CLIENT_PCTX = 'CgcSBWjK7pYx'; function formatCookieHeader(cookies) { return Object.entries(cookies).filter(([, v]) => v !== undefined && v !== null).map(([k, v]) => `${k}=${v}`).join('; '); } function makeUploadClient() { return axios.create({ timeout: 60_000, maxRedirects: 5, validateStatus: () => true }); } export async function uploadToContentPush(_client, cookies, content, mimeType, filename) { const cookieHeader = formatCookieHeader(cookies); if (!cookieHeader) { logger.warn('upload: no cookies available — cannot authenticate upload'); return null; } const http = makeUploadClient(); const step1Headers = { 'accept': '*/*', 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', 'push-id': PUSH_ID_UPLOAD, 'x-goog-upload-command': 'start', 'x-goog-upload-protocol': 'resumable', 'x-goog-upload-header-content-length': content.length.toString(), 'x-tenant-id': X_TENANT_ID, 'x-client-data': X_CLIENT_DATA, 'x-client-pctx': X_CLIENT_PCTX, 'cookie': cookieHeader, 'origin': 'https://gemini.google.com', 'referer': 'https://gemini.google.com/', }; let uploadUrl; try { logger.debug(`upload: step1 start — size=${content.length} mime=${mimeType} file=${filename}`); const r1 = await http.post(UPLOAD_ENDPOINT, `File name: ${filename}`, { headers: step1Headers, responseType: 'text', maxRedirects: 0 }); if (r1.status !== 200) { logger.warn(`upload: step1 failed status=${r1.status} body="${String(r1.data).slice(0, 200)}"`); return null; } uploadUrl = r1.headers['x-goog-upload-url'] ?? ''; if (!uploadUrl) { logger.warn('upload: step1 OK but no x-goog-upload-url header.'); return null; } logger.debug(`upload: step1 OK — upload_url=${uploadUrl.slice(0, 80)}…`); } catch (e) { logger.warn(`upload: step1 threw: ${e.message}`); return null; } const step2Headers = { 'accept': '*/*', 'content-type': mimeType, 'push-id': PUSH_ID_UPLOAD, 'x-goog-upload-command': 'upload, finalize', 'x-goog-upload-offset': '0', 'x-tenant-id': X_TENANT_ID, 'x-client-data': X_CLIENT_DATA, 'x-client-pctx': X_CLIENT_PCTX, 'cookie': cookieHeader, 'origin': 'https://gemini.google.com', 'referer': 'https://gemini.google.com/', }; try { logger.debug(`upload: step2 upload,finalize — url=${uploadUrl.slice(0, 80)}…`); const r2 = await http.post(uploadUrl, content, { headers: step2Headers, responseType: 'text', maxRedirects: 5 }); const body = (r2.data ?? ''); logger.debug(`upload: step2 status=${r2.status} body="${body.slice(0, 120)}"`); if (r2.status === 200 && body.trim().startsWith('/')) { logger.info(`upload: success → ${body.trim().slice(0, 80)}`); return body.trim(); } logger.warn(`upload: step2 failed status=${r2.status} body="${body.slice(0, 200)}"`); return null; } catch (e) { logger.warn(`upload: step2 threw: ${e.message}`); return null; } } export function buildFileDataItem(storagePath, mimeType, filename) { return [[storagePath, 1, null, mimeType], filename, null, null, null, null, null, null, [0]]; } // ═══════════════════════════════════════════════════════════════════════════ // SECTION 9 — Temporary (no-auth) Chat // ═══════════════════════════════════════════════════════════════════════════ const GEMINI_HOST = 'gemini.google.com'; const APP_PATH = '/app'; const STREAM_PATH = '/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate'; const LANG = 'en-US'; function httpsGet(opts) { return new Promise((resolve, reject) => { const req = https.request({ method: 'GET', ...opts }, res => { let body = ''; res.on('data', c => body += c); res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body })); }); req.on('error', reject); req.end(); }); } function httpsPost(opts, bodyStr) { return new Promise((resolve, reject) => { const req = https.request({ method: 'POST', ...opts }, res => { let body = ''; res.on('data', c => body += c); res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body })); }); req.on('error', reject); req.write(bodyStr); req.end(); }); } function extractWiz(html) { const marker = 'WIZ_global_data = '; const start = html.indexOf(marker); if (start < 0) return {}; const jsonStart = start + marker.length; let depth = 0, i = jsonStart, end = -1; while (i < html.length) { if (html[i] === '{') depth++; else if (html[i] === '}') { depth--; if (depth === 0) { end = i + 1; break; } } i++; } try { return end > 0 ? JSON.parse(html.slice(jsonStart, end)) : {}; } catch { return {}; } } function extractCookies(setCookieHeader) { if (!setCookieHeader) return ''; const arr = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader]; return arr.map(c => c.split(';')[0]).join('; '); } async function fetchSession() { logger.debug('tempChat: fetching anonymous session…'); const res = await httpsGet({ hostname: GEMINI_HOST, path: APP_PATH, headers: { 'accept': 'text/html', 'accept-language': LANG, 'user-agent': CHROME_USER_AGENT }, }); const wiz = extractWiz(res.body); const fsid = wiz['FdrFJe']; const bl = wiz['cfb2h']; const cookies = extractCookies(res.headers['set-cookie']); if (!fsid || !bl) logger.warn('tempChat: could not extract f.sid / bl from page — proceeding without'); logger.debug(`tempChat: session acquired (f.sid=${fsid?.slice(0, 8)}…, bl=${bl})`); return { fsid, bl, cookies }; } let _reqid = Math.floor(Math.random() * 90000) + 10000; function nextReqid() { const r = _reqid; _reqid += REQID_INCREMENT; return r; } function buildNoAuthBody(prompt) { const inner = new Array(INNER_REQ_LIST_SIZE).fill(null); inner[0] = [prompt, 0, null, null, null, null, 0]; inner[1] = [LANG]; inner[2] = ['', '', '', null, null, null, null, null, null, '']; inner[6] = [1]; inner[7] = 1; inner[10] = 1; inner[11] = 0; inner[17] = [[0]]; inner[18] = 0; inner[27] = 1; inner[30] = [4]; inner[41] = [1]; inner[53] = 0; inner[59] = randomUUID().toUpperCase(); inner[61] = []; inner[68] = 2; return 'f.req=' + encodeURIComponent(JSON.stringify([null, JSON.stringify(inner)])); } function buildNoAuthHeaders(cookies) { const uuid = randomUUID().toUpperCase(); return { 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8', 'user-agent': CHROME_USER_AGENT, 'accept-language': LANG, 'origin': 'https://gemini.google.com', 'referer': 'https://gemini.google.com/', 'x-same-domain': '1', [MODEL_HEADER_KEY]: `[1,null,null,null,"${TEMP_MODEL_HASH}",null,null,0,[4],null,null,1,null,null,1,null,"${uuid}"]`, [SESSION_HEADER_KEY]: `["${uuid}",1]`, [TRACKING_HEADER_KEY]: '[0]', [SAFETY_HEADER_KEY]: '[0,0,0]', ...(cookies ? { cookie: cookies } : {}), }; } export class TempChat { #session = null; async #getSession() { if (!this.#session) this.#session = await fetchSession(); return this.#session; } async send(prompt) { const { fsid, bl, cookies } = await this.#getSession(); const reqid = nextReqid(); const body = buildNoAuthBody(prompt); const headers = buildNoAuthHeaders(cookies); const params = new URLSearchParams(); if (bl) params.set('bl', bl); if (fsid) params.set('f.sid', fsid); params.set('hl', 'en-US'); params.set('_reqid', String(reqid)); params.set('rt', 'c'); const path = `${STREAM_PATH}?${params.toString()}`; headers['content-length'] = String(Buffer.byteLength(body)); logger.debug(`tempChat: StreamGenerate [no-auth] ${prompt.slice(0, 50)}…`); const res = await httpsPost({ hostname: GEMINI_HOST, path, headers }, body); if (res.status >= 400) { this.#session = null; throw new Error(`tempChat: StreamGenerate HTTP ${res.status}: ${res.body.slice(0, 150)}`); } logger.debug(`tempChat: received ${res.body.length} bytes`); const parsed = buildStreamResponse(res.body); return parsed.text ?? parsed.candidates?.[0]?.text ?? ''; } async refresh() { this.#session = await fetchSession(); } async close() {} } let _tempInstance = null; export function getTempChat() { if (!_tempInstance) _tempInstance = new TempChat(); return _tempInstance; } // ═══════════════════════════════════════════════════════════════════════════ // SECTION 10 — Cookie Manager // ═══════════════════════════════════════════════════════════════════════════ const __filename_cm = fileURLToPath(import.meta.url); const __dirname_cm = path.dirname(__filename_cm); export const COOKIES_JSON_PATH = path.resolve(__dirname_cm, '../..', 'data/gemini.json'); export const LOCAL_COOKIES_PATH = COOKIES_JSON_PATH; if (!fs.existsSync(COOKIES_JSON_PATH)) { fs.writeFileSync(COOKIES_JSON_PATH, '[]', 'utf-8'); } export const AUTH_COOKIE_KEYS = [ '__Secure-1PSID', '__Secure-1PSIDTS', '__Secure-1PSIDCC', '__Secure-1PAPISID', '__Secure-3PSID', '__Secure-3PSIDTS', '__Secure-3PSIDCC', '__Secure-3PAPISID', 'SID', 'HSID', 'SSID', 'APISID', 'SAPISID', 'SIDCC', 'AEC', 'NID', '1P_JAR', '__Secure-ENID', 'SEARCH_SAMESITE', ]; export const CRITICAL_COOKIES = ['__Secure-1PSID', '__Secure-1PSIDTS']; export const ALT_CRITICAL = ['__Secure-3PSID', '__Secure-3PSIDTS']; function activePath() { return COOKIES_JSON_PATH; } export function isCookiesFileEmpty() { if (!fs.existsSync(COOKIES_JSON_PATH)) return true; try { const data = JSON.parse(fs.readFileSync(COOKIES_JSON_PATH, 'utf-8')); if (Array.isArray(data)) return data.length === 0; if (data !== null && typeof data === 'object') return Object.keys(data).length === 0; } catch { /* ignore */ } return true; } export function parseCookies(source) { if (source !== null && typeof source === 'object' && !Array.isArray(source)) { const out = {}; for (const [k, v] of Object.entries(source)) { if (typeof v === 'string') out[k] = v; } return out; } if (Array.isArray(source)) { const result = {}; for (const item of source) { if (item !== null && typeof item === 'object' && !Array.isArray(item)) { if (typeof item.name === 'string' && typeof item.value === 'string') { result[item.name] = item.value; } } } return result; } if (typeof source === 'string') { let s = source.trim() .replace(/^(var|let|const)\s+\w+\s*=\s*/, '') .replace(/;$/, ''); try { return parseCookies(JSON.parse(s)); } catch { /* fall through */ } if (s.startsWith('{') && !s.startsWith('[')) { try { return parseCookies(JSON.parse(`[${s}]`)); } catch { /* fall through */ } } const result = {}; for (const rawLine of s.split(/\r?\n/)) { let line = rawLine.trim().replace(/^,/, '').replace(/,$/, '') .replace(/^["']/, '').replace(/["']$/, ''); if (line.includes(':')) { const idx = line.indexOf(':'); let k = line.slice(0, idx).trim().replace(/^["']/, '').replace(/["']$/, ''); let v = line.slice(idx + 1).trim().replace(/^["']/, '').replace(/["']$/, ''); if (k && v) result[k] = v; } else if (line.includes('=')) { const idx = line.indexOf('='); const k = line.slice(0, idx).trim(); const v = line.slice(idx + 1).trim(); if (k && v) result[k] = v; } } return result; } return {}; } export function loadCookiesJson() { const p = activePath(); if (!fs.existsSync(p)) return []; try { const data = JSON.parse(fs.readFileSync(p, 'utf-8')); if (Array.isArray(data)) return data; if (data !== null && typeof data === 'object') { return Object.entries(data).map(([k, v]) => ({ name: k, value: String(v), domain: '.google.com', path: '/', secure: true, httpOnly: true, session: false, })); } } catch { /* ignore */ } return []; } export function saveCookiesJson(cookies, targetPath) { const target = targetPath ?? COOKIES_JSON_PATH; fs.mkdirSync(path.dirname(target), { recursive: true }); let toWrite; if (!Array.isArray(cookies)) { toWrite = Object.entries(cookies).map(([k, v]) => ({ name: k, value: v, domain: '.google.com', path: '/', secure: true, httpOnly: true, session: false, })); } else { toWrite = cookies; } fs.writeFileSync(target, JSON.stringify(toWrite, null, 2)); } export function mergeCookies(existing, newCookies) { const index = {}; for (const c of existing) { if (c.name) index[c.name] = c; } if (Array.isArray(newCookies)) { for (const item of newCookies) { if (item.name && item.value !== undefined) { if (item.name in index) Object.assign(index[item.name], item); else index[item.name] = item; } } } else { for (const [name, value] of Object.entries(newCookies)) { if (name in index) index[name].value = value; else index[name] = { name, value, domain: '.google.com', path: '/', secure: true, httpOnly: true, session: false }; } } return Object.values(index); } export function getFlatCookies(filterAuth = true) { const raw = loadCookiesJson(); const flat = parseCookies(raw); if (filterAuth) { const filtered = {}; for (const [k, v] of Object.entries(flat)) { if (AUTH_COOKIE_KEYS.includes(k)) filtered[k] = v; } return filtered; } return flat; } export function checkCookies() { const flat = getFlatCookies(false); const p = activePath(); const results = {}; for (const key of CRITICAL_COOKIES) { const val = flat[key] ?? ''; results[key] = { present: !!val, value_preview: val.length > 28 ? val.slice(0, 28) + '...' : val }; } const altResults = {}; for (const key of ALT_CRITICAL) { const val = flat[key] ?? ''; altResults[key] = { present: !!val, value_preview: val.length > 28 ? val.slice(0, 28) + '...' : val }; } const otherFound = AUTH_COOKIE_KEYS.filter( k => !CRITICAL_COOKIES.includes(k) && !ALT_CRITICAL.includes(k) && k in flat ); const primaryValid = Object.values(results).every(r => r.present); const altValid = Object.values(altResults).every(r => r.present); return { critical: results, alt: altResults, other_auth_found: otherFound, total_cookies: Object.keys(flat).length, valid: primaryValid || altValid, psid: flat['__Secure-1PSID'] || flat['__Secure-3PSID'] || '', path: p, jar_size: Object.keys(flat).length, }; } export function importCookiesFromRaw(rawText) { const parsed = parseCookies(rawText); if (Object.keys(parsed).length === 0) return { ok: false, error: 'Could not parse any cookies from input' }; const psid = parsed['__Secure-1PSID'] || parsed['__Secure-3PSID'] || parsed.SID || ''; const psidts = parsed['__Secure-1PSIDTS'] || parsed['__Secure-3PSIDTS'] || ''; const existing = loadCookiesJson(); const merged = mergeCookies(existing, parsed); saveCookiesJson(merged); return { ok: true, psid_found: !!psid, psidts_found: !!psidts, total: Object.keys(parsed).length, path: COOKIES_JSON_PATH, preview: psid ? psid.slice(0, 20) + '...' : '(not found)', }; } export function getCookiesPath() { return activePath(); } export function clearCookiesFile() { const p = activePath(); if (!fs.existsSync(p)) return { ok: true, path: p, message: 'No cookies file found — nothing to delete.' }; fs.writeFileSync(p, '[]', 'utf-8'); return { ok: true, path: p, message: 'Cookies cleared.' }; } export function discoverCookiesPath() { return fs.existsSync(COOKIES_JSON_PATH) ? COOKIES_JSON_PATH : null; } export function importCookiesToProfile(auth) { const flat = getFlatCookies(true); if (Object.keys(flat).length === 0) return false; auth.saveCookies(flat); return true; } function cookiesFromJar(jar) { const c = jar.toObject ? jar.toObject() : jar; return Object.entries(c).map(([name, value]) => ({ name, value, domain: '.google.com', path: '/', secure: true })); } function cookiesFromState(state) { return Object.entries(state.cookies || {}).map(([name, value]) => ({ name, value, domain: '.google.com', path: '/', secure: true })); } // ═══════════════════════════════════════════════════════════════════════════ // SECTION 11 — GeminiClient (main class) // ═══════════════════════════════════════════════════════════════════════════ export class GeminiClient { #httpClient = null; #jar = null; #transport = null; #initialized = false; constructor({ profileName = null, proxy = null, timeoutMs = 300_000 } = {}) { this.profileManager = new ProfileManager(); this.authManager = new AuthManager(this.profileManager, profileName, proxy); this.proxy = proxy; this.timeoutMs = timeoutMs; } get transport() { if (!this.#transport) throw new AuthError('Client not initialized. Call init() first.'); return this.#transport; } get initialized() { return this.#initialized; } async init() { let authState = this.authManager.load(); const localFound = discoverCookiesPath(); const haveAnyCookies = localFound !== null; if (haveAnyCookies && importCookiesToProfile(this.authManager)) { authState = this.authManager.load(); logger.info(`client: auto-synced exocoreCookies.json → profile (${Object.keys(authState.cookies).length} cookies).`); } const hasPsid = !!(authState.cookies['__Secure-1PSID'] || authState.cookies['__Secure-3PSID']); if (!hasPsid) { throw new AuthError('Not signed in yet — tap the login button to paste your cookies.'); } // Don't call refreshTokens() here — hitting gemini.google.com/app from a // datacenter IP with user cookies triggers Google's anti-abuse system and // invalidates the user's browser session (auto-logout). Use whatever cached // SNlM0e token we have; if missing, the API call will fail naturally. if (!authState.tokens.snlm0e) { logger.warn('client: no cached SNlM0e token. Gemini API calls may fail — paste fresh cookies after signing in again.'); } this.#jar = new CookieJar(authState.cookies); this.#httpClient = axios.create({ timeout: this.timeoutMs, headers: { ...GEMINI_HEADERS }, maxRedirects: 5, proxy: this.proxy ? parseAxiosProxy(this.proxy) : false, validateStatus: () => true, }); this.#jar.attachTo(this.#httpClient); this.#transport = new RPCTransport({ client: this.#httpClient, accessToken: authState.tokens.snlm0e, buildLabel: authState.tokens.cfb2h ?? null, sessionId: authState.tokens.fdrfje ?? null, }); this.#initialized = true; logger.info('client: GeminiClient initialized.'); } async send(prompt, { model = null, metadata = null, fileData = null, gemId = null, toolId = null, stylePreset = null, temp = null, onDelta = null } = {}) { if (!this.#initialized) throw new AuthError('GeminiClient not initialized. Call init() first.'); if (toolId !== null && toolId !== undefined) throw new GeminiError('Tool-flagged requests require BotGuard and are not supported in the HTTP-only port.'); if (fileData?.length > 0) logger.warn('client: file_data provided over HTTP path — Google may return 0 candidates.'); const resolvedModel = this.#resolveModel(model); return this.transport.streamGenerate({ prompt, model: resolvedModel, metadata, fileData, gemId, toolId: null, stylePreset, temp, onDelta }); } async executeRpc(rpcId, payload = '[]', sourcePath = '/app') { return this.transport.batchexecute([new RPCPayload({ rpcId, payload })], sourcePath); } async executeRpcs(payloads, sourcePath = '/app') { return this.transport.batchexecute(payloads, sourcePath); } async uploadFile(content, mimeType, filename) { if (!this.#initialized || !this.#httpClient) return null; const buf = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf-8'); const storagePath = await uploadToContentPush(this.#httpClient, this.getCookies(), buf, mimeType, filename); if (!storagePath) return null; return buildFileDataItem(storagePath, mimeType, filename); } getCookies() { return this.#jar ? this.#jar.toObject() : { ...this.authManager.state.cookies }; } async listChats(limit = 50) { if (!this.#initialized) throw new AuthError('GeminiClient not initialized.'); throw new AuthError('Puppeteer module removed; listChats is unavailable.'); try { const { ensurePage, setCookies } = await import('../../puppeteer/chrome/index.js'); const page = await ensurePage(undefined, { fresh: true }); await page.setViewport({ width: 1400, height: 900 }); const cookies = this.#jar ? cookiesFromJar(this.#jar) : cookiesFromState(this.authManager.state); await setCookies(page, cookies); await page.goto('https://gemini.google.com/', { waitUntil: 'domcontentloaded', timeout: 20000 }).catch(() => {}); await page.waitForSelector('[data-test-id="conversation"]', { timeout: 15000 }).catch(() => {}); await new Promise(r => setTimeout(r, 3000)); const chats = await page.evaluate(async () => { const sidebar = document.querySelector('conversations-list')?.parentElement || document.documentElement; const target = sidebar === document.documentElement ? window : sidebar; const sleep = ms => new Promise(r => setTimeout(r, ms)); let prev = -1; for (let i = 0; i < 30; i++) { target.scrollBy(0, 500); await sleep(300); const cur = target.scrollTop || 0; if (cur === prev) break; prev = cur; } return Array.from(document.querySelectorAll('[data-test-id="conversation"]')).map(a => { const href = a.getAttribute('href') || ''; const title = a.querySelector('.conversation-title')?.textContent?.trim() || ''; const match = href.match(/\/app\/([a-z0-9_]+)/); return match ? { id: match[1], title: title || '', pinned: false, timestamp: null } : null; }).filter(Boolean); }); return chats; } catch (e) { logger.debug(`listChats failed: ${e.message}`); return []; } } async deleteChat(cid) { if (!this.#initialized) throw new AuthError('GeminiClient not initialized.'); const cleanId = cid.startsWith('c_') ? cid : `c_${cid}`; const payload1 = JSON.stringify([cleanId]); await this.transport.batchexecute([new RPCPayload({ rpcId: RPC.DELETE_CHAT, payload: payload1 })], `/app/${cleanId.slice(2)}`); const payload2 = JSON.stringify([cleanId, [1, null, 0, 1]]); await this.transport.batchexecute([new RPCPayload({ rpcId: RPC.DELETE_CHAT_2, payload: payload2 })], `/app/${cleanId.slice(2)}`); return { deleted: true, id: cleanId }; } async deleteAllChats() { const chats = await this.listChats(200); const results = { deleted: 0, failed: 0, errors: [] }; for (const chat of chats) { try { await this.deleteChat(chat.id); results.deleted++; } catch (e) { results.failed++; results.errors.push({ id: chat.id, error: e.message }); } } return results; } #resolveModel(model) { if (model instanceof Model) return model; if (typeof model === 'string' && model.length > 0) return getModel(model); return DEFAULT_MODEL; } async close() { this.#httpClient = null; this.#jar = null; this.#transport = null; this.#initialized = false; } }