
Baoyu Danger Gemini Web
- 169 installs
- 100 repo stars
- Updated March 2, 2026
- xy121718/baoyu-skills
Baoyu-danger-gemini-web is an agent skill that connects workflows to Google Gemini through an unofficial web client for chat, gems, and image generation.
About
Baoyu-danger-gemini-web is an advanced integration skill packaging an unofficial browser-session Gemini web client for agents that need multimodal output without provisioning official API credentials. The embedded implementation covers access-token and cookie lifecycle, file upload helpers, candidate parsing, gem plugins, and defensive error handling for rate limits, temporary blocks, and model invalidation. Solo builders typically reach for it when prototyping generative features, automating content drafts, or experimenting with image generation inside Claude Code–driven pipelines. Because it depends on fragile web auth semantics, it demands careful secret handling and is a poor fit for production compliance-heavy deployments; treat it as a build-time bridge until you migrate to supported APIs.
- TypeScript Gemini web client with GemMixin for custom gems and RPC-style calls
- Handles auth rotation (1PSIDTS), cookies, uploads, and structured error types (auth, usage limits, timeouts)
- Supports generated and web-sourced images with explicit ImageGenerationError paths
- Configurable timeouts, auto-refresh, and verbose logging for long agent sessions
- Marked “danger” tier—unofficial web surface, not the stable Google AI API
Baoyu Danger Gemini Web by the numbers
- 169 all-time installs (skills.sh)
- Ranked #3,085 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 1, 2026 (Skillselion catalog sync)
npx skills add https://github.com/xy121718/baoyu-skills --skill baoyu-danger-gemini-webAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 169 |
|---|---|
| repo stars | ★ 100 |
| Security audit | 1 / 3 scanners passed |
| Last updated | March 2, 2026 |
| Repository | xy121718/baoyu-skills ↗ |
What it does
Wire agent workflows to Google Gemini via the unofficial web client for chat, gems, and image generation when API keys are not an option.
Who is it for?
Best when you're prototyping AI features, content automation, or image experiments inside agent tooling during active development.
Skip if: Production apps requiring SLA-backed APIs, strict data residency, or teams unwilling to manage cookie-based session risk.
When should I use this skill?
When building agent workflows that must call Gemini via web session auth for chat, gems, or images during integration work.
What you get
Your agent can call Gemini web endpoints with rotated session auth and typed errors so you can prototype generative features before moving to a supported API.
- Gemini web API calls from agent workflows
- Parsed model outputs and generated images where supported
By the numbers
- Typed error surface including AuthError, UsageLimitExceeded, ImageGenerationError, and TemporarilyBlocked
Files
Gemini Web Client
Text/image generation via Gemini Web API. Supports reference images and multi-turn conversations.
Script Directory
Important: All scripts are located in the scripts/ subdirectory of this skill.
Agent Execution Instructions: 1. Determine this SKILL.md file's directory path as SKILL_DIR 2. Script path = ${SKILL_DIR}/scripts/<script-name>.ts 3. Replace all ${SKILL_DIR} in this document with the actual path
Script Reference:
| Script | Purpose |
|---|---|
scripts/main.ts | CLI entry point for text/image generation |
scripts/gemini-webapi/* | TypeScript port of gemini_webapi (GeminiClient, types, utils) |
Consent Check (REQUIRED)
Before first use, verify user consent for reverse-engineered API usage.
Consent file locations:
- macOS:
~/Library/Application Support/baoyu-skills/gemini-web/consent.json - Linux:
~/.local/share/baoyu-skills/gemini-web/consent.json - Windows:
%APPDATA%\baoyu-skills\gemini-web\consent.json
Flow: 1. Check if consent file exists with accepted: true and disclaimerVersion: "1.0" 2. If valid consent exists → print warning with acceptedAt date, proceed 3. If no consent → show disclaimer, ask user via AskUserQuestion:
- "Yes, I accept" → create consent file with ISO timestamp, proceed
- "No, I decline" → output decline message, stop
4. Consent file format: {"version":1,"accepted":true,"acceptedAt":"<ISO>","disclaimerVersion":"1.0"}
---
Preferences (EXTEND.md)
Use Bash to check EXTEND.md existence (priority order):
# Check project-level first
test -f .baoyu-skills/baoyu-danger-gemini-web/EXTEND.md && echo "project"
# Then user-level (cross-platform: $HOME works on macOS/Linux/WSL)
test -f "$HOME/.baoyu-skills/baoyu-danger-gemini-web/EXTEND.md" && echo "user"┌──────────────────────────────────────────────────────────┬───────────────────┐ │ Path │ Location │ ├──────────────────────────────────────────────────────────┼───────────────────┤ │ .baoyu-skills/baoyu-danger-gemini-web/EXTEND.md │ Project directory │ ├──────────────────────────────────────────────────────────┼───────────────────┤ │ $HOME/.baoyu-skills/baoyu-danger-gemini-web/EXTEND.md │ User home │ └──────────────────────────────────────────────────────────┴───────────────────┘
┌───────────┬───────────────────────────────────────────────────────────────────────────┐ │ Result │ Action │ ├───────────┼───────────────────────────────────────────────────────────────────────────┤ │ Found │ Read, parse, apply settings │ ├───────────┼───────────────────────────────────────────────────────────────────────────┤ │ Not found │ Use defaults │ └───────────┴───────────────────────────────────────────────────────────────────────────┘
EXTEND.md Supports: Default model | Proxy settings | Custom data directory
Usage
# Text generation
npx -y bun ${SKILL_DIR}/scripts/main.ts "Your prompt"
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Your prompt" --model gemini-3-flash
# Image generation
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cute cat" --image cat.png
npx -y bun ${SKILL_DIR}/scripts/main.ts --promptfiles system.md content.md --image out.png
# Vision input (reference images)
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Describe this" --reference image.png
npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "Create variation" --reference a.png --image out.png
# Multi-turn conversation
npx -y bun ${SKILL_DIR}/scripts/main.ts "Remember: 42" --sessionId session-abc
npx -y bun ${SKILL_DIR}/scripts/main.ts "What number?" --sessionId session-abc
# JSON output
npx -y bun ${SKILL_DIR}/scripts/main.ts "Hello" --jsonOptions
| Option | Description |
|---|---|
--prompt, -p | Prompt text |
--promptfiles | Read prompt from files (concatenated) |
--model, -m | Model: gemini-3-pro (default), gemini-3-flash, gemini-3-flash-thinking, gemini-3.1-pro-preview |
--image [path] | Generate image (default: generated.png) |
--reference, --ref | Reference images for vision input |
--sessionId | Session ID for multi-turn conversation |
--list-sessions | List saved sessions |
--json | Output as JSON |
--login | Refresh cookies, then exit |
--cookie-path | Custom cookie file path |
--profile-dir | Chrome profile directory |
Models
| Model | Description |
|---|---|
gemini-3-pro | Default, latest 3.0 Pro |
gemini-3-flash | Fast, lightweight 3.0 Flash |
gemini-3-flash-thinking | 3.0 Flash with thinking |
gemini-3.1-pro-preview | 3.1 Pro preview (empty header, auto-routed) |
Authentication
First run opens browser for Google auth. Cookies cached automatically.
Supported browsers (auto-detected): Chrome, Chrome Canary/Beta, Chromium, Edge.
Force refresh: --login flag. Override browser: GEMINI_WEB_CHROME_PATH env var.
Environment Variables
| Variable | Description |
|---|---|
GEMINI_WEB_DATA_DIR | Data directory |
GEMINI_WEB_COOKIE_PATH | Cookie file path |
GEMINI_WEB_CHROME_PROFILE_DIR | Chrome profile directory |
GEMINI_WEB_CHROME_PATH | Chrome executable path |
HTTP_PROXY, HTTPS_PROXY | Proxy for Google access (set inline with command) |
Sessions
Session files stored in data directory under sessions/<id>.json.
Contains: id, metadata (Gemini chat state), messages array, timestamps.
Extension Support
Custom configurations via EXTEND.md. See Preferences section for paths and supported options.
import { Endpoint, ErrorCode, Headers, Model } from './constants.js';
import { GemMixin } from './components/gem-mixin.js';
import {
APIError,
AuthError,
GeminiError,
ImageGenerationError,
ModelInvalid,
TemporarilyBlocked,
TimeoutError,
UsageLimitExceeded,
} from './exceptions.js';
import { Candidate, Gem, GeneratedImage, ModelOutput, RPCData, WebImage } from './types/index.js';
import {
extract_json_from_response,
get_access_token,
get_nested_value,
logger,
parse_file_name,
rotate_1psidts,
rotate_tasks,
fetch_with_timeout,
sleep,
upload_file,
write_cookie_file,
resolveGeminiWebCookiePath,
} from './utils/index.js';
type InitOptions = {
timeout?: number;
auto_close?: boolean;
close_delay?: number;
auto_refresh?: boolean;
refresh_interval?: number;
verbose?: boolean;
};
type RequestKwargs = RequestInit & { timeout_ms?: number };
function normalize_headers(h?: HeadersInit): Record<string, string> {
if (!h) return {};
if (Array.isArray(h)) return Object.fromEntries(h.map(([k, v]) => [k, v]));
if (h instanceof Headers) {
const out: Record<string, string> = {};
h.forEach((v, k) => {
out[k] = v;
});
return out;
}
return { ...(h as Record<string, string>) };
}
function collect_strings(root: unknown, accept: (s: string) => boolean, limit: number = 20): string[] {
const out: string[] = [];
const seen = new Set<string>();
const stack: unknown[] = [root];
while (stack.length > 0 && out.length < limit) {
const v = stack.pop();
if (typeof v === 'string') {
if (accept(v) && !seen.has(v)) {
seen.add(v);
out.push(v);
}
continue;
}
if (Array.isArray(v)) {
for (let i = 0; i < v.length; i++) stack.push(v[i]);
continue;
}
if (v && typeof v === 'object') {
for (const val of Object.values(v as Record<string, unknown>)) stack.push(val);
}
}
return out;
}
export class GeminiClient extends GemMixin {
public cookies: Record<string, string> = {};
public proxy: string | null = null;
public _running: boolean = false;
public access_token: string | null = null;
public timeout: number = 300;
public auto_close: boolean = false;
public close_delay: number = 300;
public auto_refresh: boolean = true;
public refresh_interval: number = 540;
public kwargs: RequestInit;
private close_timer: ReturnType<typeof setTimeout> | null = null;
private refresh_abort: AbortController | null = null;
constructor(
secure_1psid: string | null = null,
secure_1psidts: string | null = null,
proxy: string | null = null,
kwargs: RequestInit = {},
) {
super();
this.proxy = proxy;
this.kwargs = kwargs;
if (secure_1psid) {
this.cookies['__Secure-1PSID'] = secure_1psid;
if (secure_1psidts) this.cookies['__Secure-1PSIDTS'] = secure_1psidts;
}
}
async init(
timeoutOrOpts: number | InitOptions = 300,
auto_close: boolean = false,
close_delay: number = 300,
auto_refresh: boolean = true,
refresh_interval: number = 540,
verbose: boolean = true,
): Promise<void> {
const opts: InitOptions =
typeof timeoutOrOpts === 'object'
? timeoutOrOpts
: { timeout: timeoutOrOpts, auto_close, close_delay, auto_refresh, refresh_interval, verbose };
const timeout = opts.timeout ?? 300;
const ac = opts.auto_close ?? false;
const cd = opts.close_delay ?? 300;
const ar = opts.auto_refresh ?? true;
const ri = opts.refresh_interval ?? 540;
const vb = opts.verbose ?? true;
try {
const [token, valid] = await get_access_token(this.cookies, this.proxy, vb);
this.access_token = token;
this.cookies = valid;
this._running = true;
this.timeout = timeout;
this.auto_close = ac;
this.close_delay = cd;
if (this.auto_close) await this.reset_close_task();
this.auto_refresh = ar;
this.refresh_interval = ri;
const sid = this.cookies['__Secure-1PSID'];
if (sid) {
const existing = rotate_tasks.get(sid);
if (existing && existing instanceof AbortController) existing.abort();
rotate_tasks.delete(sid);
}
if (this.auto_refresh && sid) {
const ctl = new AbortController();
this.refresh_abort?.abort();
this.refresh_abort = ctl;
rotate_tasks.set(sid, ctl);
void this.start_auto_refresh(ctl.signal);
}
await write_cookie_file(this.cookies, resolveGeminiWebCookiePath(), 'client').catch(() => {});
if (vb) logger.success('Gemini client initialized successfully.');
} catch (e) {
await this.close();
throw e;
}
}
async close(delay: number = 0): Promise<void> {
if (delay > 0) await sleep(delay * 1000);
this._running = false;
if (this.close_timer) {
clearTimeout(this.close_timer);
this.close_timer = null;
}
this.refresh_abort?.abort();
this.refresh_abort = null;
const sid = this.cookies['__Secure-1PSID'];
const t = sid ? rotate_tasks.get(sid) : null;
if (t && t instanceof AbortController) t.abort();
if (sid) rotate_tasks.delete(sid);
}
async reset_close_task(): Promise<void> {
if (this.close_timer) {
clearTimeout(this.close_timer);
this.close_timer = null;
}
this.close_timer = setTimeout(() => {
void this.close(0);
}, this.close_delay * 1000);
this.close_timer.unref?.();
}
async start_auto_refresh(signal: AbortSignal): Promise<void> {
while (!signal.aborted) {
let newTs: string | null = null;
try {
newTs = await rotate_1psidts(this.cookies, this.proxy);
} catch (e) {
if (e instanceof AuthError) {
logger.warning('AuthError: Failed to refresh cookies. Auto refresh task canceled.');
return;
}
logger.warning(`Unexpected error while refreshing cookies: ${e instanceof Error ? e.message : String(e)}`);
}
if (newTs) {
this.cookies['__Secure-1PSIDTS'] = newTs;
await write_cookie_file(this.cookies, resolveGeminiWebCookiePath(), 'refresh').catch(() => {});
logger.debug('Cookies refreshed. New __Secure-1PSIDTS applied.');
}
await sleep(this.refresh_interval * 1000, signal);
}
}
protected async _run<T>(fn: () => Promise<T>, retry: number): Promise<T> {
try {
if (!this._running) {
await this.init({
timeout: this.timeout,
auto_close: this.auto_close,
close_delay: this.close_delay,
auto_refresh: this.auto_refresh,
refresh_interval: this.refresh_interval,
verbose: false,
});
if (!this._running) {
throw new APIError('Client initialization failed.');
}
}
return await fn();
} catch (e) {
let r = retry;
if (e instanceof ImageGenerationError) r = Math.min(1, r);
if (e instanceof APIError && r > 0) {
await sleep(1000);
return await this._run(fn, r - 1);
}
throw e;
}
}
async generate_content(
prompt: string,
files: string[] | null = null,
model: Model | string | Record<string, unknown> = Model.UNSPECIFIED,
gem: Gem | string | null = null,
chat: ChatSession | null = null,
kwargs: RequestKwargs = {},
): Promise<ModelOutput> {
return await this._run(async () => {
if (!prompt) throw new Error('Prompt cannot be empty.');
let mdl: Model;
if (typeof model === 'string') mdl = Model.from_name(model);
else if (model instanceof Model) mdl = model;
else if (model && typeof model === 'object') mdl = Model.from_dict(model);
else throw new TypeError(`'model' must be a Model instance, string, or dictionary; got ${typeof model}`);
const gem_id = gem instanceof Gem ? gem.id : gem;
if (this.auto_close) await this.reset_close_task();
if (!this.access_token) throw new APIError('Missing access token.');
const f = files?.length ? files : null;
const uploaded =
f &&
(await Promise.all(
f.map(async (p) => [[await upload_file(p, this.proxy)], parse_file_name(p)] as [string[], string]),
));
const first = uploaded ? [prompt, 0, null, uploaded] : [prompt];
const inner: unknown[] = [first, null, chat ? chat.metadata : null];
if (gem_id) {
for (let i = 0; i < 16; i++) inner.push(null);
inner.push(gem_id);
}
const f_req = JSON.stringify([null, JSON.stringify(inner)]);
const body = new URLSearchParams({ at: this.access_token, 'f.req': f_req }).toString();
const h0 = { ...Headers.GEMINI, ...mdl.model_header, Cookie: Object.entries(this.cookies).map(([k, v]) => `${k}=${v}`).join('; ') };
const h1 = { ...h0, ...normalize_headers(kwargs.headers) };
let res: Response;
try {
const timeout_ms = typeof kwargs.timeout_ms === 'number' ? kwargs.timeout_ms : this.timeout * 1000;
const { timeout_ms: _t, ...rest } = kwargs;
res = await fetch_with_timeout(Endpoint.GENERATE, {
method: 'POST',
headers: h1,
body,
redirect: 'follow',
...this.kwargs,
...rest,
timeout_ms,
});
} catch (e) {
throw new TimeoutError(
`Generate content request timed out, please try again. If the problem persists, consider setting a higher 'timeout' value when initializing GeminiClient. (${e instanceof Error ? e.message : String(e)})`,
);
}
if (res.status !== 200) {
await this.close();
throw new APIError(`Failed to generate contents. Request failed with status code ${res.status}`);
}
const txt = await res.text();
const response_json = extract_json_from_response(txt);
let body_json: unknown[] | null = null;
let body_index = 0;
try {
if (!Array.isArray(response_json)) throw new Error('Invalid JSON');
for (let part_index = 0; part_index < response_json.length; part_index++) {
const part = response_json[part_index];
if (!Array.isArray(part)) continue;
const part_body = get_nested_value<string | null>(part, [2], null);
if (!part_body) continue;
try {
const part_json = JSON.parse(part_body) as unknown[];
if (get_nested_value(part_json, [4], null)) {
body_index = part_index;
body_json = part_json;
break;
}
} catch {}
}
if (!body_json) throw new Error('No body');
} catch {
await this.close();
try {
const code = get_nested_value<number>(response_json, [0, 5, 2, 0, 1, 0], -1);
if (code === ErrorCode.USAGE_LIMIT_EXCEEDED) {
throw new UsageLimitExceeded(
`Failed to generate contents. Usage limit of ${mdl.model_name} model has exceeded. Please try switching to another model.`,
);
}
if (code === ErrorCode.MODEL_INCONSISTENT) {
throw new ModelInvalid(
'Failed to generate contents. The specified model is inconsistent with the chat history. Please make sure to pass the same `model` parameter when starting a chat session with previous metadata.',
);
}
if (code === ErrorCode.MODEL_HEADER_INVALID) {
throw new ModelInvalid(
'Failed to generate contents. The specified model is not available. Please update gemini_webapi to the latest version. If the error persists and is caused by the package, please report it on GitHub.',
);
}
if (code === ErrorCode.IP_TEMPORARILY_BLOCKED) {
throw new TemporarilyBlocked(
'Failed to generate contents. Your IP address is temporarily blocked by Google. Please try using a proxy or waiting for a while.',
);
}
} catch (e) {
if (e instanceof GeminiError) throw e;
}
logger.debug(`Invalid response: ${txt.slice(0, 500)}`);
throw new APIError('Failed to generate contents. Invalid response data received. Client will try to re-initialize on next request.');
}
try {
const candidate_list = get_nested_value<unknown[]>(body_json, [4], []);
const out: Candidate[] = [];
for (let candidate_index = 0; candidate_index < candidate_list.length; candidate_index++) {
const candidate = candidate_list[candidate_index];
if (!Array.isArray(candidate)) continue;
const rcid = get_nested_value<string | null>(candidate, [0], null);
if (!rcid) continue;
let text = String(get_nested_value(candidate, [1, 0], ''));
if (/^http:\/\/googleusercontent\.com\/card_content\/\d+/.test(text)) {
text = String(get_nested_value(candidate, [22, 0], text));
}
const thoughts = get_nested_value<string | null>(candidate, [37, 0, 0], null);
const web_images: WebImage[] = [];
for (const w of get_nested_value<unknown[]>(candidate, [12, 1], [])) {
if (!Array.isArray(w)) continue;
const url = get_nested_value<string | null>(w, [0, 0, 0], null);
if (!url) continue;
web_images.push(new WebImage(url, String(get_nested_value(w, [7, 0], '')), String(get_nested_value(w, [0, 4], '')), this.proxy));
}
const generated_images: GeneratedImage[] = [];
const wants_generated =
get_nested_value(candidate, [12, 7, 0], null) != null ||
/http:\/\/googleusercontent\.com\/image_generation_content\/\d+/.test(text);
if (wants_generated) {
let img_body: unknown[] | null = null;
for (let part_index = body_index; part_index < (response_json as unknown[]).length; part_index++) {
const part = (response_json as unknown[])[part_index];
if (!Array.isArray(part)) continue;
const part_body = get_nested_value<string | null>(part, [2], null);
if (!part_body) continue;
try {
const part_json = JSON.parse(part_body) as unknown[];
const cand = get_nested_value<unknown>(part_json, [4, candidate_index], null);
if (!cand) continue;
const urls = collect_strings(cand, (s) => s.startsWith('https://lh3.googleusercontent.com/gg-dl/'), 1);
if (urls.length > 0) {
img_body = part_json;
break;
}
} catch {}
}
if (!img_body) {
throw new ImageGenerationError(
'Failed to parse generated images. Please update gemini_webapi to the latest version. If the error persists and is caused by the package, please report it on GitHub.',
);
}
const img_candidate = get_nested_value<unknown[]>(img_body, [4, candidate_index], []);
const finished = get_nested_value<string | null>(img_candidate, [1, 0], null);
if (finished) {
text = finished.replace(/http:\/\/googleusercontent\.com\/image_generation_content\/\d+/g, '').trimEnd();
}
const gen = get_nested_value<unknown[]>(img_candidate, [12, 7, 0], []);
for (let img_index = 0; img_index < gen.length; img_index++) {
const g = gen[img_index];
if (!Array.isArray(g)) continue;
const url = get_nested_value<string | null>(g, [0, 3, 3], null);
if (!url) continue;
const img_num = get_nested_value<number | null>(g, [3, 6], null);
const title = img_num ? `[Generated Image ${img_num}]` : '[Generated Image]';
const alt_list = get_nested_value<unknown[]>(g, [3, 5], []);
const alt =
(typeof alt_list[img_index] === 'string' ? (alt_list[img_index] as string) : null) ??
(typeof alt_list[0] === 'string' ? (alt_list[0] as string) : '') ??
'';
generated_images.push(new GeneratedImage(url, title, alt, this.proxy, this.cookies));
}
if (generated_images.length === 0) {
const urls = collect_strings(img_candidate, (s) => s.startsWith('https://lh3.googleusercontent.com/gg-dl/'), 4);
for (const url of urls) {
generated_images.push(new GeneratedImage(url, '[Generated Image]', '', this.proxy, this.cookies));
}
}
}
out.push(new Candidate({ rcid, text, thoughts, web_images, generated_images }));
}
if (out.length === 0) {
throw new GeminiError('Failed to generate contents. No output data found in response.');
}
const metadata = get_nested_value<string[]>(body_json, [1], []);
const output = new ModelOutput({ metadata, candidates: out });
if (chat instanceof ChatSession) chat.last_output = output;
return output;
} catch (e) {
if (e instanceof GeminiError || e instanceof APIError) throw e;
throw new APIError('Failed to parse response body. Data structure is invalid.');
}
}, 2);
}
async generateContent(
prompt: string,
files?: string[] | null,
model?: Model | string | Record<string, unknown>,
gem?: Gem | string | null,
chat?: ChatSession | null,
kwargs?: RequestKwargs,
): Promise<ModelOutput> {
return await this.generate_content(prompt, files ?? null, model ?? Model.UNSPECIFIED, gem ?? null, chat ?? null, kwargs ?? {});
}
start_chat(opts?: ConstructorParameters<typeof ChatSession>[1]): ChatSession {
return new ChatSession(this, opts);
}
startChat(opts?: ConstructorParameters<typeof ChatSession>[1]): ChatSession {
return this.start_chat(opts);
}
protected async _batch_execute(payloads: RPCData[], opts: RequestInit = {}): Promise<Response> {
if (!this.access_token) throw new APIError('Missing access token.');
const f_req = JSON.stringify([payloads.map((p) => p.serialize())]);
const body = new URLSearchParams({ at: this.access_token, 'f.req': f_req }).toString();
const h0 = { ...Headers.GEMINI, Cookie: Object.entries(this.cookies).map(([k, v]) => `${k}=${v}`).join('; ') };
const h1 = { ...h0, ...normalize_headers(opts.headers) };
const res = await fetch_with_timeout(Endpoint.BATCH_EXEC, {
method: 'POST',
headers: h1,
body,
redirect: 'follow',
...this.kwargs,
...opts,
timeout_ms: this.timeout * 1000,
});
if (res.status !== 200) {
await this.close();
throw new APIError(`Batch execution failed with status code ${res.status}`);
}
return res;
}
}
export class ChatSession {
private __metadata: Array<string | null> = [null, null, null];
public geminiclient: GeminiClient;
private _last_output: ModelOutput | null = null;
public model: Model | string | Record<string, unknown>;
public gem: Gem | string | null;
constructor(
geminiclient: GeminiClient,
opts: {
metadata?: Array<string | null>;
cid?: string | null;
rid?: string | null;
rcid?: string | null;
model?: Model | string | Record<string, unknown>;
gem?: Gem | string | null;
} = {},
) {
this.geminiclient = geminiclient;
this.model = opts.model ?? Model.UNSPECIFIED;
this.gem = opts.gem ?? null;
if (opts.metadata) this.metadata = opts.metadata;
if (opts.cid) this.cid = opts.cid;
if (opts.rid) this.rid = opts.rid;
if (opts.rcid) this.rcid = opts.rcid;
}
toString(): string {
return `ChatSession(cid='${this.cid}', rid='${this.rid}', rcid='${this.rcid}')`;
}
get last_output(): ModelOutput | null {
return this._last_output;
}
set last_output(v: ModelOutput | null) {
this._last_output = v;
if (v) {
this.metadata = (v.metadata ?? []) as Array<string | null>;
this.rcid = v.rcid;
}
}
async send_message(prompt: string, files: string[] | null = null, kwargs: RequestKwargs = {}): Promise<ModelOutput> {
return await this.geminiclient.generate_content(prompt, files, this.model, this.gem, this, kwargs);
}
async sendMessage(prompt: string, files?: string[] | null, kwargs?: RequestKwargs): Promise<ModelOutput> {
return await this.send_message(prompt, files ?? null, kwargs ?? {});
}
choose_candidate(index: number): ModelOutput {
if (!this.last_output) throw new Error('No previous output data found in this chat session.');
if (index >= this.last_output.candidates.length) {
throw new Error(`Index ${index} exceeds the number of candidates in last model output.`);
}
this.last_output.chosen = index;
this.rcid = this.last_output.rcid;
return this.last_output;
}
chooseCandidate(index: number): ModelOutput {
return this.choose_candidate(index);
}
get metadata(): Array<string | null> {
return this.__metadata;
}
set metadata(v: Array<string | null>) {
if (v.length > 3) throw new Error('metadata cannot exceed 3 elements');
this.__metadata = [null, null, null];
for (let i = 0; i < v.length; i++) this.__metadata[i] = v[i] ?? null;
}
get cid(): string | null {
return this.__metadata[0];
}
set cid(v: string | null) {
this.__metadata[0] = v;
}
get rid(): string | null {
return this.__metadata[1];
}
set rid(v: string | null) {
this.__metadata[1] = v;
}
get rcid(): string | null {
return this.__metadata[2];
}
set rcid(v: string | null) {
this.__metadata[2] = v;
}
}
import { GRPC } from '../constants.js';
import { APIError } from '../exceptions.js';
import { Gem, GemJar, RPCData } from '../types/index.js';
import { logger } from '../utils/logger.js';
import { extract_json_from_response, get_nested_value } from '../utils/parsing.js';
export abstract class GemMixin {
protected _gems: GemJar | null = null;
protected abstract _run<T>(fn: () => Promise<T>, retry: number): Promise<T>;
protected abstract _batch_execute(payloads: RPCData[], opts?: RequestInit): Promise<Response>;
protected abstract close(delay?: number): Promise<void>;
get gems(): GemJar {
if (this._gems == null) {
throw new Error(
'Gems not fetched yet. Call `GeminiClient.fetch_gems()` method to fetch gems from gemini.google.com.',
);
}
return this._gems;
}
async fetch_gems(include_hidden: boolean = false, opts?: RequestInit): Promise<GemJar> {
return await this._run(async () => {
const res = await this._batch_execute(
[
new RPCData(GRPC.LIST_GEMS, include_hidden ? '[4]' : '[3]', 'system'),
new RPCData(GRPC.LIST_GEMS, '[2]', 'custom'),
],
opts,
);
let response_json: unknown;
try {
response_json = extract_json_from_response(await res.text());
if (!Array.isArray(response_json)) throw new Error('Invalid response');
} catch {
await this.close();
throw new APIError('Failed to fetch gems. Invalid response data received. Client will try to re-initialize on next request.');
}
let predefined: unknown[] = [];
let custom: unknown[] = [];
try {
for (const part of response_json as unknown[]) {
if (!Array.isArray(part)) continue;
const ident = part[part.length - 1];
const body = get_nested_value<string | null>(part, [2], null);
if (!body) continue;
if (ident === 'system') {
const parsed = JSON.parse(body) as unknown[];
predefined = (Array.isArray(parsed) ? (parsed[2] as unknown[]) : []) ?? [];
} else if (ident === 'custom') {
const parsed = JSON.parse(body) as unknown[] | null;
if (parsed) custom = (parsed[2] as unknown[]) ?? [];
}
}
if (predefined.length === 0 && custom.length === 0) throw new Error('No gems');
} catch {
await this.close();
logger.debug('Invalid response while parsing gems');
throw new APIError('Failed to fetch gems. Invalid response data received. Client will try to re-initialize on next request.');
}
const entries: [string, Gem][] = [];
for (const gem of predefined) {
if (!Array.isArray(gem)) continue;
const id = String(get_nested_value(gem, [0], ''));
if (!id) continue;
entries.push([
id,
new Gem(
id,
String(get_nested_value(gem, [1, 0], '')),
get_nested_value<string | null>(gem, [1, 1], null),
get_nested_value<string | null>(gem, [2, 0], null),
true,
),
]);
}
for (const gem of custom) {
if (!Array.isArray(gem)) continue;
const id = String(get_nested_value(gem, [0], ''));
if (!id) continue;
entries.push([
id,
new Gem(
id,
String(get_nested_value(gem, [1, 0], '')),
get_nested_value<string | null>(gem, [1, 1], null),
get_nested_value<string | null>(gem, [2, 0], null),
false,
),
]);
}
this._gems = new GemJar(entries);
return this._gems;
}, 2);
}
async create_gem(name: string, prompt: string, description: string = ''): Promise<Gem> {
return await this._run(async () => {
const payload = JSON.stringify([
[
name,
description,
prompt,
null,
null,
null,
null,
null,
0,
null,
1,
null,
null,
null,
[],
],
]);
const res = await this._batch_execute([new RPCData(GRPC.CREATE_GEM, payload)]);
try {
const response_json = extract_json_from_response(await res.text()) as unknown[];
const gem_id = JSON.parse(String((response_json[0] as unknown[])[2]))[0] as string;
return new Gem(gem_id, name, description, prompt, false);
} catch {
await this.close();
throw new APIError('Failed to create gem. Invalid response data received. Client will try to re-initialize on next request.');
}
}, 2);
}
async update_gem(gem: Gem | string, name: string, prompt: string, description: string = ''): Promise<Gem> {
return await this._run(async () => {
const gem_id = typeof gem === 'string' ? gem : gem.id;
const payload = JSON.stringify([
gem_id,
[
name,
description,
prompt,
null,
null,
null,
null,
null,
0,
null,
1,
null,
null,
null,
[],
0,
],
]);
await this._batch_execute([new RPCData(GRPC.UPDATE_GEM, payload)]);
return new Gem(gem_id, name, description, prompt, false);
}, 2);
}
async delete_gem(gem: Gem | string, opts?: RequestInit): Promise<void> {
return await this._run(async () => {
const gem_id = typeof gem === 'string' ? gem : gem.id;
const payload = JSON.stringify([gem_id]);
await this._batch_execute([new RPCData(GRPC.DELETE_GEM, payload)], opts);
}, 2);
}
}
export { GemMixin } from './gem-mixin.js';
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',
ROTATE_COOKIES: 'https://accounts.google.com/RotateCookies',
UPLOAD: 'https://content-push.googleapis.com/upload',
BATCH_EXEC: 'https://gemini.google.com/_/BardChatUi/data/batchexecute',
} as const;
export const GRPC = {
LIST_CHATS: 'MaZiqc',
READ_CHAT: 'hNvQHb',
LIST_GEMS: 'CNgdBe',
CREATE_GEM: 'oMH3Zd',
UPDATE_GEM: 'kHv0Vd',
DELETE_GEM: 'UXcSJb',
} as const;
export const Headers = {
GEMINI: {
'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/120.0.0.0 Safari/537.36',
'X-Same-Domain': '1',
},
ROTATE_COOKIES: {
'Content-Type': 'application/json',
},
UPLOAD: {
'Push-ID': 'feeds/mcudyrk2a4khkz',
},
} as const;
export const ErrorCode = {
TEMPORARY_ERROR_1013: 1013,
USAGE_LIMIT_EXCEEDED: 1037,
MODEL_INCONSISTENT: 1050,
MODEL_HEADER_INVALID: 1052,
IP_TEMPORARILY_BLOCKED: 1060,
} as const;
export class Model {
static readonly UNSPECIFIED = new Model('unspecified', {}, false);
static readonly G_3_0_PRO = new Model(
'gemini-3.0-pro',
{ 'x-goog-ext-525001261-jspb': '[1,null,null,null,"9d8ca3786ebdfbea",null,null,0,[4],null,null,1]' },
false,
);
static readonly G_3_0_FLASH = new Model(
'gemini-3.0-flash',
{ 'x-goog-ext-525001261-jspb': '[1,null,null,null,"fbb127bbb056c959",null,null,0,[4],null,null,1]' },
false,
);
static readonly G_3_0_FLASH_THINKING = new Model(
'gemini-3.0-flash-thinking',
{ 'x-goog-ext-525001261-jspb': '[1,null,null,null,"5bf011840784117a",null,null,0,[4],null,null,1]' },
false,
);
static readonly G_3_1_PRO_PREVIEW = new Model(
'gemini-3.1-pro-preview',
{},
false,
);
constructor(
public readonly model_name: string,
public readonly model_header: Record<string, string>,
public readonly advanced_only: boolean,
) {}
static from_name(name: string): Model {
for (const model of [Model.UNSPECIFIED, Model.G_3_0_PRO, Model.G_3_0_FLASH, Model.G_3_0_FLASH_THINKING, Model.G_3_1_PRO_PREVIEW]) {
if (model.model_name === name) return model;
}
throw new Error(
`Unknown model name: ${name}. Available models: ${[Model.UNSPECIFIED, Model.G_3_0_PRO, Model.G_3_0_FLASH, Model.G_3_0_FLASH_THINKING, Model.G_3_1_PRO_PREVIEW]
.map((m) => m.model_name)
.join(', ')}`,
);
}
static from_dict(model_dict: { model_name?: unknown; model_header?: unknown }): Model {
if (!model_dict || typeof model_dict !== 'object') {
throw new Error("When passing a custom model as a dictionary, 'model_name' and 'model_header' keys must be provided.");
}
if (!('model_name' in model_dict) || !('model_header' in model_dict)) {
throw new Error("When passing a custom model as a dictionary, 'model_name' and 'model_header' keys must be provided.");
}
if (typeof model_dict.model_name !== 'string' || !model_dict.model_name.trim()) {
throw new Error("When passing a custom model as a dictionary, 'model_name' must be a non-empty string.");
}
if (!model_dict.model_header || typeof model_dict.model_header !== 'object') {
throw new Error("When passing a custom model as a dictionary, 'model_header' must be a dictionary containing valid header strings.");
}
const header: Record<string, string> = {};
for (const [k, v] of Object.entries(model_dict.model_header as Record<string, unknown>)) {
if (typeof v === 'string') header[k] = v;
}
return new Model(model_dict.model_name, header, false);
}
}
export class AuthError extends Error {
constructor(message = 'AuthError') {
super(message);
this.name = 'AuthError';
}
}
export class APIError extends Error {
constructor(message = 'APIError') {
super(message);
this.name = 'APIError';
}
}
export class ImageGenerationError extends APIError {
constructor(message = 'ImageGenerationError') {
super(message);
this.name = 'ImageGenerationError';
}
}
export class GeminiError extends Error {
constructor(message = 'GeminiError') {
super(message);
this.name = 'GeminiError';
}
}
export class TimeoutError extends GeminiError {
constructor(message = 'TimeoutError') {
super(message);
this.name = 'TimeoutError';
}
}
export class UsageLimitExceeded extends GeminiError {
constructor(message = 'UsageLimitExceeded') {
super(message);
this.name = 'UsageLimitExceeded';
}
}
export class ModelInvalid extends GeminiError {
constructor(message = 'ModelInvalid') {
super(message);
this.name = 'ModelInvalid';
}
}
export class TemporarilyBlocked extends GeminiError {
constructor(message = 'TemporarilyBlocked') {
super(message);
this.name = 'TemporarilyBlocked';
}
}
export { GeminiClient, ChatSession } from './client.js';
export * from './exceptions.js';
export * from './types/index.js';
export * from './constants.js';
export { logger, set_log_level, setLogLevel } from './utils/logger.js';
export * as utils from './utils/index.js';
import { GeneratedImage, type Image, WebImage } from './image.js';
function decode_html(s: string | null | undefined): string | null | undefined {
if (s == null) return s;
return s
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/'/g, "'")
.replace(/ /g, ' ')
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
.replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(parseInt(dec, 10)));
}
export class Candidate {
public rcid: string;
public text: string;
public thoughts: string | null;
public web_images: WebImage[];
public generated_images: GeneratedImage[];
constructor(params: {
rcid: string;
text: string;
thoughts?: string | null;
web_images?: WebImage[];
generated_images?: GeneratedImage[];
}) {
this.rcid = params.rcid;
this.text = decode_html(params.text) ?? '';
this.thoughts = decode_html(params.thoughts) ?? null;
this.web_images = params.web_images ?? [];
this.generated_images = params.generated_images ?? [];
}
toString(): string {
return this.text;
}
get images(): Image[] {
return [...this.web_images, ...this.generated_images];
}
}
export class Gem {
constructor(
public id: string,
public name: string,
public description: string | null,
public prompt: string | null,
public predefined: boolean,
) {}
toString(): string {
return `Gem(id='${this.id}', name='${this.name}', description='${this.description}', prompt='${this.prompt}', predefined=${this.predefined})`;
}
}
export class GemJar implements Iterable<Gem> {
private m = new Map<string, Gem>();
constructor(entries?: Iterable<[string, Gem]>) {
if (entries) for (const [id, gem] of entries) this.m.set(id, gem);
}
[Symbol.iterator](): Iterator<Gem> {
return this.m.values();
}
entries(): IterableIterator<[string, Gem]> {
return this.m.entries();
}
values(): IterableIterator<Gem> {
return this.m.values();
}
has(id: string): boolean {
return this.m.has(id);
}
set(id: string, gem: Gem): this {
this.m.set(id, gem);
return this;
}
get(id?: string | null, name?: string | null, def: Gem | null = null): Gem | null {
if (id == null && name == null) {
throw new Error('At least one of gem id or name must be provided.');
}
if (id != null) {
const g = this.m.get(id) ?? null;
if (!g) return def;
if (name != null) return g.name === name ? g : def;
return g;
}
if (name != null) {
for (const g of this.m.values()) {
if (g.name === name) return g;
}
return def;
}
return def;
}
filter(predefined: boolean | null = null, name: string | null = null): GemJar {
const out: [string, Gem][] = [];
for (const [id, gem] of this.m.entries()) {
if (predefined != null && gem.predefined !== predefined) continue;
if (name != null && gem.name !== name) continue;
out.push([id, gem]);
}
return new GemJar(out);
}
}
export class RPCData {
constructor(
public rpcid: string,
public payload: string,
public identifier: string = 'generic',
) {}
toString(): string {
return `GRPC(rpcid='${this.rpcid}', payload='${this.payload}', identifier='${this.identifier}')`;
}
serialize(): unknown[] {
return [this.rpcid, this.payload, null, this.identifier];
}
}
import path from 'node:path';
import { mkdir, writeFile } from 'node:fs/promises';
import { logger } from '../utils/logger.js';
import { cookie_header, fetch_with_timeout } from '../utils/http.js';
export class Image {
constructor(
public url: string,
public title = '[Image]',
public alt = '',
public proxy: string | null = null,
) {}
toString(): string {
const u = this.url.length <= 20 ? this.url : `${this.url.slice(0, 8)}...${this.url.slice(-12)}`;
return `Image(title='${this.title}', alt='${this.alt}', url='${u}')`;
}
async save(
p: string = 'temp',
filename: string | null = null,
cookies: Record<string, string> | null = null,
verbose: boolean = false,
skip_invalid_filename: boolean = false,
): Promise<string | null> {
filename = filename ?? this.url.split('/').pop()?.split('?')[0] ?? 'image';
const m = filename.match(/^(.*\.\w+)/);
if (m) filename = m[1]!;
else {
if (verbose) logger.warning(`Invalid filename: ${filename}`);
if (skip_invalid_filename) return null;
}
const headers: Record<string, string> = {
'User-Agent': 'Mozilla/5.0',
Accept: 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8',
Referer: 'https://gemini.google.com/',
};
if (cookies) headers.Cookie = cookie_header(cookies);
let url = this.url;
let res: Response | null = null;
for (let i = 0; i < 10; i++) {
res = await fetch_with_timeout(url, {
method: 'GET',
headers,
redirect: 'manual',
timeout_ms: 30_000,
});
if (res.status >= 300 && res.status < 400) {
const loc = res.headers.get('location');
if (!loc) break;
url = new URL(loc, url).toString();
continue;
}
break;
}
if (!res) throw new Error('Image download failed: no response');
if (!res.ok) {
throw new Error(`Error downloading image: ${res.status} ${res.statusText}`);
}
const ct = res.headers.get('content-type');
if (ct && !ct.includes('image')) {
logger.warning(`Content type of ${filename} is not image, but ${ct}.`);
}
const dir = path.resolve(p);
await mkdir(dir, { recursive: true });
const dest = path.join(dir, filename);
const buf = Buffer.from(await res.arrayBuffer());
await writeFile(dest, buf);
if (verbose) logger.info(`Image saved as ${dest}`);
return dest;
}
}
export class WebImage extends Image {}
export class GeneratedImage extends Image {
constructor(
url: string,
title: string,
alt: string,
proxy: string | null,
public cookies: Record<string, string>,
) {
super(url, title, alt, proxy);
if (!cookies || Object.keys(cookies).length === 0) {
throw new Error('GeneratedImage is designed to be initialized with same cookies as GeminiClient.');
}
}
async save(
p: string = 'temp',
filename: string | null = null,
cookies: Record<string, string> | null = null,
verbose: boolean = false,
skip_invalid_filename: boolean = false,
full_size: boolean = true,
): Promise<string | null> {
const u = full_size ? `${this.url}=s2048` : this.url;
const f = filename ?? `${new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14)}_${u.slice(-10)}.png`;
const img = new Image(u, this.title, this.alt, this.proxy);
return await img.save(p, f, cookies ?? this.cookies, verbose, skip_invalid_filename);
}
}
export { Candidate } from './candidate.js';
export { Gem, GemJar } from './gem.js';
export { RPCData } from './grpc.js';
export { GeneratedImage, Image, WebImage } from './image.js';
export { ModelOutput } from './modeloutput.js';
import type { Image } from './image.js';
import type { Candidate } from './candidate.js';
export class ModelOutput {
public metadata: string[];
public candidates: Candidate[];
public chosen: number;
constructor(params: { metadata: string[]; candidates: Candidate[]; chosen?: number }) {
this.metadata = params.metadata;
this.candidates = params.candidates;
this.chosen = params.chosen ?? 0;
}
toString(): string {
return this.text;
}
get text(): string {
return this.candidates[this.chosen]?.text ?? '';
}
get thoughts(): string | null {
return this.candidates[this.chosen]?.thoughts ?? null;
}
get images(): Image[] {
return this.candidates[this.chosen]?.images ?? [];
}
get rcid(): string {
return this.candidates[this.chosen]?.rcid ?? '';
}
}
import fs from 'node:fs';
import path from 'node:path';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { resolveGeminiWebCookiePath } from './paths.js';
export type CookieMap = Record<string, string>;
export type CookieFileData =
| {
cookies: CookieMap;
updated_at: number;
source?: string;
}
| {
version: number;
updatedAt: string;
cookieMap: CookieMap;
source?: string;
};
export async function read_cookie_file(p: string = resolveGeminiWebCookiePath()): Promise<CookieMap | null> {
try {
if (!fs.existsSync(p) || !fs.statSync(p).isFile()) return null;
const raw = await readFile(p, 'utf8');
const data = JSON.parse(raw) as unknown;
if (data && typeof data === 'object' && 'cookies' in (data as any)) {
const cookies = (data as any).cookies as unknown;
if (cookies && typeof cookies === 'object') {
const out: CookieMap = {};
for (const [k, v] of Object.entries(cookies as Record<string, unknown>)) {
if (typeof v === 'string') out[k] = v;
}
return out;
}
}
if (data && typeof data === 'object' && 'cookieMap' in (data as any)) {
const cookies = (data as any).cookieMap as unknown;
if (cookies && typeof cookies === 'object') {
const out: CookieMap = {};
for (const [k, v] of Object.entries(cookies as Record<string, unknown>)) {
if (typeof v === 'string') out[k] = v;
}
return Object.keys(out).length > 0 ? out : null;
}
}
if (data && typeof data === 'object') {
const out: CookieMap = {};
for (const [k, v] of Object.entries(data as Record<string, unknown>)) {
if (typeof v === 'string') out[k] = v;
}
return Object.keys(out).length > 0 ? out : null;
}
return null;
} catch {
return null;
}
}
export async function write_cookie_file(
cookies: CookieMap,
p: string = resolveGeminiWebCookiePath(),
source?: string,
): Promise<void> {
const dir = path.dirname(p);
await mkdir(dir, { recursive: true });
const payload: CookieFileData = {
version: 1,
updatedAt: new Date().toISOString(),
cookieMap: cookies,
source,
};
await writeFile(p, JSON.stringify(payload, null, 2), 'utf8');
}
export const readCookieFile = read_cookie_file;
export const writeCookieFile = write_cookie_file;
import { APIError, ImageGenerationError } from '../exceptions.js';
import { sleep } from './http.js';
export function running(retry: number = 0) {
return <TArgs extends unknown[], TResult>(
fn: (client: any, ...args: TArgs) => Promise<TResult>,
): ((client: any, ...args: TArgs) => Promise<TResult>) => {
const wrap = async (client: any, ...args: TArgs): Promise<TResult> => {
try {
if (!client?._running) {
await client.init?.({
timeout: client.timeout,
auto_close: client.auto_close,
close_delay: client.close_delay,
auto_refresh: client.auto_refresh,
refresh_interval: client.refresh_interval,
verbose: false,
});
}
return await fn(client, ...args);
} catch (e) {
let r = retry;
if (e instanceof ImageGenerationError) r = Math.min(1, r);
if (e instanceof APIError && r > 0) {
await sleep(1000);
return await running(r - 1)(fn)(client, ...args);
}
throw e;
}
};
return wrap;
};
}
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { Endpoint, Headers } from '../constants.js';
import { AuthError } from '../exceptions.js';
import { cookie_header, extract_set_cookie_value, fetch_with_timeout } from './http.js';
import { logger } from './logger.js';
import { read_cookie_file, write_cookie_file } from './cookie-file.js';
import { resolveGeminiWebDataDir, resolveGeminiWebCookiePath } from './paths.js';
import { load_browser_cookies } from './load-browser-cookies.js';
async function send_request(cookies: Record<string, string>, verbose: boolean): Promise<[string, Record<string, string>]> {
const res = await fetch_with_timeout(Endpoint.INIT, {
method: 'GET',
headers: { ...Headers.GEMINI, Cookie: cookie_header(cookies) },
redirect: 'follow',
timeout_ms: 30_000,
});
if (!res.ok) throw new Error(`Init failed: ${res.status} ${res.statusText}`);
const text = await res.text();
const m = text.match(/\"SNlM0e\":\"(.*?)\"/);
if (!m) throw new Error('Missing SNlM0e in response');
if (verbose) logger.debug('Init succeeded. Initializing client...');
return [m[1]!, cookies];
}
function merge_cookie_maps(...maps: Array<Record<string, string> | null | undefined>): Record<string, string> {
const out: Record<string, string> = {};
for (const m of maps) {
if (!m) continue;
for (const [k, v] of Object.entries(m)) {
if (typeof v === 'string' && v.length > 0) out[k] = v;
}
}
return out;
}
function read_cached_1psidts_file(dir: string, sid: string): string | null {
try {
const p = path.join(dir, `.cached_1psidts_${sid}.txt`);
if (!fs.existsSync(p) || !fs.statSync(p).isFile()) return null;
const v = fs.readFileSync(p, 'utf8').trim();
return v || null;
} catch {
return null;
}
}
function list_cached_1psidts(dir: string): Array<{ sid: string; sidts: string }> {
const out: Array<{ sid: string; sidts: string }> = [];
try {
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return out;
for (const f of fs.readdirSync(dir)) {
if (!f.startsWith('.cached_1psidts_') || !f.endsWith('.txt')) continue;
const sid = f.slice('.cached_1psidts_'.length, -'.txt'.length);
if (!sid) continue;
const sidts = read_cached_1psidts_file(dir, sid);
if (sidts) out.push({ sid, sidts });
}
} catch {}
return out;
}
async function fetch_google_extra_cookies(proxy: string | null, verbose: boolean): Promise<Record<string, string>> {
void proxy;
try {
const res = await fetch_with_timeout(Endpoint.GOOGLE, { timeout_ms: 15_000 });
const setCookie = res.headers.get('set-cookie');
const nid = extract_set_cookie_value(setCookie, 'NID');
if (nid) return { NID: nid };
} catch (e) {
if (verbose) logger.debug(`Skipping google.com preflight: ${e instanceof Error ? e.message : String(e)}`);
}
return {};
}
export async function get_access_token(
base_cookies: Record<string, string>,
proxy: string | null = null,
verbose: boolean = false,
): Promise<[string, Record<string, string>]> {
const extra = await fetch_google_extra_cookies(proxy, verbose);
const cacheDir = resolveGeminiWebDataDir();
const candidates: Record<string, string>[] = [];
const cookieFilePath = resolveGeminiWebCookiePath();
const cachedFile = await read_cookie_file(cookieFilePath);
const forceLogin = !!(process.env.GEMINI_WEB_LOGIN?.trim() || process.env.GEMINI_WEB_FORCE_LOGIN?.trim());
const shouldUseChromeFirst = forceLogin || (!cachedFile && !base_cookies['__Secure-1PSID'] && !base_cookies['__Secure-1PSIDTS']);
if (shouldUseChromeFirst) {
try {
const browser = await load_browser_cookies('google.com', verbose);
for (const cookies of Object.values(browser)) {
candidates.push(merge_cookie_maps(extra, cookies));
}
} catch (e) {
if (verbose) logger.warning(`Failed to load cookies via Chrome CDP: ${e instanceof Error ? e.message : String(e)}`);
}
}
if (base_cookies['__Secure-1PSID'] && base_cookies['__Secure-1PSIDTS']) {
candidates.push(merge_cookie_maps(extra, base_cookies));
} else if (verbose) {
logger.debug('Skipping loading base cookies. Either __Secure-1PSID or __Secure-1PSIDTS is not provided.');
}
if (cachedFile) {
candidates.push(merge_cookie_maps(extra, cachedFile));
}
if (base_cookies['__Secure-1PSID'] && !base_cookies['__Secure-1PSIDTS']) {
const sid = base_cookies['__Secure-1PSID'];
const sidts = read_cached_1psidts_file(cacheDir, sid);
if (sidts) {
candidates.push(merge_cookie_maps(extra, base_cookies, { '__Secure-1PSIDTS': sidts }));
} else if (verbose) {
logger.debug('Skipping loading cached cookies. Cache file not found or empty.');
}
} else if (!base_cookies['__Secure-1PSID']) {
const caches = list_cached_1psidts(cacheDir);
for (const c of caches) {
candidates.push(merge_cookie_maps(extra, { '__Secure-1PSID': c.sid, '__Secure-1PSIDTS': c.sidts }));
}
if (caches.length === 0 && verbose) {
logger.debug('Skipping loading cached cookies. Cookies will be cached after successful initialization.');
}
}
const unique: Record<string, string>[] = [];
const seen = new Set<string>();
for (const c of candidates) {
const key = `${c['__Secure-1PSID'] ?? ''}:${c['__Secure-1PSIDTS'] ?? ''}:${c.NID ?? ''}`;
if (seen.has(key)) continue;
seen.add(key);
unique.push(c);
}
const try_candidates = async (): Promise<[string, Record<string, string>]> => {
if (unique.length === 0) throw new Error('no candidates');
const attempts = unique.map(async (c, i) => {
try {
if (verbose) logger.debug(`Init attempt (${i + 1}/${unique.length})...`);
return await send_request(c, verbose);
} catch (e) {
if (verbose) logger.debug(`Init attempt (${i + 1}/${unique.length}) failed: ${e instanceof Error ? e.message : String(e)}`);
throw e;
}
});
return (await Promise.any(attempts)) as [string, Record<string, string>];
};
try {
const [token, cookies] = await try_candidates();
await write_cookie_file(cookies, resolveGeminiWebCookiePath(), 'init').catch(() => {});
return [token, cookies];
} catch {
if (verbose) logger.debug('Cookie attempts failed. Falling back to Chrome CDP cookie load...');
}
const browser = await load_browser_cookies('google.com', verbose);
let valid = 0;
for (const cookies of Object.values(browser)) {
if (cookies['__Secure-1PSID']) valid++;
if (base_cookies['__Secure-1PSID'] && cookies['__Secure-1PSID'] && cookies['__Secure-1PSID'] !== base_cookies['__Secure-1PSID']) {
if (verbose) logger.debug('Skipping loaded browser cookies: __Secure-1PSID does not match the one provided.');
continue;
}
unique.push(merge_cookie_maps(extra, cookies));
}
if (valid === 0) {
throw new AuthError(
'No valid cookies available for initialization. Please pass __Secure-1PSID and __Secure-1PSIDTS manually.',
);
}
try {
const [token, cookies] = await try_candidates();
await write_cookie_file(cookies, resolveGeminiWebCookiePath(), 'init').catch(() => {});
return [token, cookies];
} catch {
throw new AuthError(
`Failed to initialize client. SECURE_1PSIDTS could get expired frequently, please make sure cookie values are up to date. (Failed initialization attempts: ${unique.length})`,
);
}
}
export const getAccessToken = get_access_token;
export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
return new Promise((resolve) => {
const t = setTimeout(() => {
if (signal) signal.removeEventListener('abort', onAbort);
resolve();
}, ms);
const onAbort = () => {
clearTimeout(t);
if (signal) signal.removeEventListener('abort', onAbort);
resolve();
};
if (signal) {
if (signal.aborted) {
onAbort();
} else {
signal.addEventListener('abort', onAbort, { once: true });
}
}
});
}
export function cookie_header(cookies: Record<string, string>): string {
return Object.entries(cookies)
.filter(([, v]) => typeof v === 'string' && v.length > 0)
.map(([k, v]) => `${k}=${v}`)
.join('; ');
}
export const cookieHeader = cookie_header;
export function extract_set_cookie_value(setCookie: string | null, name: string): string | null {
if (!setCookie) return null;
const re = new RegExp(`(?:^|[;,\\s])${name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}=([^;]+)`, 'i');
const m = setCookie.match(re);
if (!m) return null;
return m[1] ?? null;
}
export async function fetch_with_timeout(
url: string,
init: RequestInit & { timeout_ms?: number } = {},
): Promise<Response> {
const { timeout_ms, ...rest } = init;
if (!timeout_ms || timeout_ms <= 0) return fetch(url, rest);
const ctl = new AbortController();
const t = setTimeout(() => ctl.abort(), timeout_ms);
try {
return await fetch(url, { ...rest, signal: ctl.signal });
} finally {
clearTimeout(t);
}
}
export const fetchWithTimeout = fetch_with_timeout;
export { running } from './decorators.js';
export { get_access_token, getAccessToken } from './get-access-token.js';
export { load_browser_cookies, loadBrowserCookies } from './load-browser-cookies.js';
export { logger, set_log_level, setLogLevel } from './logger.js';
export { extract_json_from_response, extractJsonFromResponse, get_nested_value, getNestedValue } from './parsing.js';
export { rotate_1psidts, rotate1psidts } from './rotate-1psidts.js';
export { upload_file, uploadFile, parse_file_name, parseFileName } from './upload-file.js';
export { read_cookie_file, readCookieFile, write_cookie_file, writeCookieFile } from './cookie-file.js';
export {
resolveUserDataRoot,
resolveGeminiWebChromeProfileDir,
resolveGeminiWebCookiePath,
resolveGeminiWebDataDir,
resolveGeminiWebSessionPath,
resolveGeminiWebSessionsDir,
} from './paths.js';
export { cookie_header, cookieHeader, fetch_with_timeout, fetchWithTimeout, sleep } from './http.js';
export const rotate_tasks = new Map<string, unknown>();
import { spawn, type ChildProcess } from 'node:child_process';
import fs from 'node:fs';
import { mkdir } from 'node:fs/promises';
import net from 'node:net';
import process from 'node:process';
import { Endpoint, Headers } from '../constants.js';
import { logger } from './logger.js';
import { cookie_header, fetch_with_timeout, sleep } from './http.js';
import { read_cookie_file, type CookieMap, write_cookie_file } from './cookie-file.js';
import { resolveGeminiWebChromeProfileDir, resolveGeminiWebCookiePath } from './paths.js';
type CdpSendOptions = { sessionId?: string; timeoutMs?: number };
class CdpConnection {
private ws: WebSocket;
private nextId = 0;
private pending = new Map<
number,
{ resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> | null }
>();
private constructor(ws: WebSocket) {
this.ws = ws;
this.ws.addEventListener('message', (event) => {
try {
const data = typeof event.data === 'string' ? event.data : new TextDecoder().decode(event.data as ArrayBuffer);
const msg = JSON.parse(data) as { id?: number; result?: unknown; error?: { message?: string } };
if (msg.id) {
const p = this.pending.get(msg.id);
if (p) {
this.pending.delete(msg.id);
if (p.timer) clearTimeout(p.timer);
if (msg.error?.message) p.reject(new Error(msg.error.message));
else p.resolve(msg.result);
}
}
} catch {}
});
this.ws.addEventListener('close', () => {
for (const [id, p] of this.pending.entries()) {
this.pending.delete(id);
if (p.timer) clearTimeout(p.timer);
p.reject(new Error('CDP connection closed.'));
}
});
}
static async connect(url: string, timeoutMs: number): Promise<CdpConnection> {
const ws = new WebSocket(url);
await new Promise<void>((resolve, reject) => {
const t = setTimeout(() => reject(new Error('CDP connection timeout.')), timeoutMs);
ws.addEventListener('open', () => {
clearTimeout(t);
resolve();
});
ws.addEventListener('error', () => {
clearTimeout(t);
reject(new Error('CDP connection failed.'));
});
});
return new CdpConnection(ws);
}
async send<T = unknown>(method: string, params?: Record<string, unknown>, opts?: CdpSendOptions): Promise<T> {
const id = ++this.nextId;
const msg: Record<string, unknown> = { id, method };
if (params) msg.params = params;
if (opts?.sessionId) msg.sessionId = opts.sessionId;
const timeoutMs = opts?.timeoutMs ?? 15_000;
const out = await new Promise<unknown>((resolve, reject) => {
const t =
timeoutMs > 0
? setTimeout(() => {
this.pending.delete(id);
reject(new Error(`CDP timeout: ${method}`));
}, timeoutMs)
: null;
this.pending.set(id, { resolve, reject, timer: t });
this.ws.send(JSON.stringify(msg));
});
return out as T;
}
close(): void {
try {
this.ws.close();
} catch {}
}
}
async function get_free_port(): Promise<number> {
return await new Promise((resolve, reject) => {
const srv = net.createServer();
srv.unref();
srv.on('error', reject);
srv.listen(0, '127.0.0.1', () => {
const addr = srv.address();
if (!addr || typeof addr === 'string') {
srv.close(() => reject(new Error('Unable to allocate a free TCP port.')));
return;
}
const port = addr.port;
srv.close((err) => (err ? reject(err) : resolve(port)));
});
});
}
function find_chrome_executable(): string | null {
const override = process.env.GEMINI_WEB_CHROME_PATH?.trim();
if (override && fs.existsSync(override)) return override;
const candidates: string[] = [];
switch (process.platform) {
case 'darwin':
candidates.push(
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
'/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
);
break;
case 'win32':
candidates.push(
'C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe',
'C:\\\\Program Files (x86)\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe',
'C:\\\\Program Files\\\\Microsoft\\\\Edge\\\\Application\\\\msedge.exe',
'C:\\\\Program Files (x86)\\\\Microsoft\\\\Edge\\\\Application\\\\msedge.exe',
);
break;
default:
candidates.push(
'/usr/bin/google-chrome',
'/usr/bin/google-chrome-stable',
'/usr/bin/chromium',
'/usr/bin/chromium-browser',
'/snap/bin/chromium',
'/usr/bin/microsoft-edge',
);
break;
}
for (const p of candidates) {
if (fs.existsSync(p)) return p;
}
return null;
}
async function wait_for_chrome_debug_port(port: number, timeoutMs: number): Promise<string> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const res = await fetch_with_timeout(`http://127.0.0.1:${port}/json/version`, { timeout_ms: 5_000 });
if (!res.ok) throw new Error(`status=${res.status}`);
const j = (await res.json()) as { webSocketDebuggerUrl?: string };
if (j.webSocketDebuggerUrl) return j.webSocketDebuggerUrl;
} catch {}
await sleep(200);
}
throw new Error('Chrome debug port not ready');
}
async function launch_chrome(profileDir: string, port: number): Promise<ChildProcess> {
const chrome = find_chrome_executable();
if (!chrome) throw new Error('Chrome executable not found.');
const args = [
`--remote-debugging-port=${port}`,
`--user-data-dir=${profileDir}`,
'--no-first-run',
'--no-default-browser-check',
'--disable-popup-blocking',
'https://gemini.google.com/app',
];
return spawn(chrome, args, { stdio: 'ignore' });
}
async function is_gemini_session_ready(cookies: CookieMap, verbose: boolean): Promise<boolean> {
if (!cookies['__Secure-1PSID']) return false;
try {
const res = await fetch_with_timeout(Endpoint.INIT, {
method: 'GET',
headers: { ...Headers.GEMINI, Cookie: cookie_header(cookies) },
redirect: 'follow',
timeout_ms: 30_000,
});
if (!res.ok) {
if (verbose) logger.debug(`Gemini init check failed: ${res.status} ${res.statusText}`);
return false;
}
const text = await res.text();
return /\"SNlM0e\":\"(.*?)\"/.test(text);
} catch (e) {
if (verbose) logger.debug(`Gemini init check error: ${e instanceof Error ? e.message : String(e)}`);
return false;
}
}
async function fetch_google_cookies_via_cdp(
profileDir: string,
timeoutMs: number,
verbose: boolean,
): Promise<CookieMap> {
await mkdir(profileDir, { recursive: true });
const port = await get_free_port();
const chrome = await launch_chrome(profileDir, port);
let cdp: CdpConnection | null = null;
try {
const wsUrl = await wait_for_chrome_debug_port(port, 30_000);
cdp = await CdpConnection.connect(wsUrl, 15_000);
const { targetId } = await cdp.send<{ targetId: string }>('Target.createTarget', {
url: 'https://gemini.google.com/app',
newWindow: true,
});
const { sessionId } = await cdp.send<{ sessionId: string }>('Target.attachToTarget', { targetId, flatten: true });
await cdp.send('Network.enable', {}, { sessionId });
if (verbose) {
logger.info('Chrome opened. If needed, complete Google login in the window. Waiting for a valid Gemini session...');
}
const start = Date.now();
let last: CookieMap = {};
while (Date.now() - start < timeoutMs) {
const { cookies } = await cdp.send<{ cookies: Array<{ name: string; value: string }> }>(
'Network.getCookies',
{ urls: ['https://gemini.google.com/', 'https://accounts.google.com/', 'https://www.google.com/'] },
{ sessionId, timeoutMs: 10_000 },
);
const m: CookieMap = {};
for (const c of cookies) {
if (c?.name && typeof c.value === 'string') m[c.name] = c.value;
}
last = m;
if (await is_gemini_session_ready(m, verbose)) {
return m;
}
await sleep(1000);
}
throw new Error(`Timed out waiting for a valid Gemini session. Last keys: ${Object.keys(last).join(', ')}`);
} finally {
if (cdp) {
try {
await cdp.send('Browser.close', {}, { timeoutMs: 5_000 });
} catch {}
cdp.close();
}
try {
chrome.kill('SIGTERM');
} catch {}
setTimeout(() => {
if (!chrome.killed) {
try {
chrome.kill('SIGKILL');
} catch {}
}
}, 2_000).unref?.();
}
}
export async function load_browser_cookies(domain_name: string = '', verbose: boolean = true): Promise<Record<string, CookieMap>> {
const force = process.env.GEMINI_WEB_LOGIN?.trim() || process.env.GEMINI_WEB_FORCE_LOGIN?.trim();
if (!force) {
const cached = await read_cookie_file();
if (cached) return { chrome: cached };
}
const profileDir = process.env.GEMINI_WEB_CHROME_PROFILE_DIR?.trim() || resolveGeminiWebChromeProfileDir();
const cookies = await fetch_google_cookies_via_cdp(profileDir, 120_000, verbose);
const filtered: CookieMap = {};
for (const [k, v] of Object.entries(cookies)) {
if (typeof v === 'string' && v.length > 0) filtered[k] = v;
}
await write_cookie_file(filtered, resolveGeminiWebCookiePath(), 'cdp');
void domain_name;
return { chrome: filtered };
}
export const loadBrowserCookies = load_browser_cookies;
export type LogLevel = 'TRACE' | 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL' | number;
const lvl: Record<Exclude<LogLevel, number>, number> = {
TRACE: 0,
DEBUG: 1,
INFO: 2,
WARNING: 3,
ERROR: 4,
CRITICAL: 5,
};
let cur = lvl.INFO;
function toNum(level: LogLevel): number {
if (typeof level === 'number') return level;
return lvl[level] ?? lvl.INFO;
}
export function set_log_level(level: LogLevel): void {
cur = toNum(level);
}
export const setLogLevel = set_log_level;
function emit(level: Exclude<LogLevel, number>, args: unknown[]): void {
if (lvl[level] < cur) return;
const prefix = `[gemini_webapi] ${level}:`;
if (level === 'WARNING') console.warn(prefix, ...args);
else if (level === 'ERROR' || level === 'CRITICAL') console.error(prefix, ...args);
else console.log(prefix, ...args);
}
export const logger = {
trace: (...args: unknown[]) => emit('TRACE', args),
debug: (...args: unknown[]) => emit('DEBUG', args),
info: (...args: unknown[]) => emit('INFO', args),
warning: (...args: unknown[]) => emit('WARNING', args),
error: (...args: unknown[]) => emit('ERROR', args),
success: (...args: unknown[]) => emit('INFO', args),
};
import { logger } from './logger.js';
export function get_nested_value<T = unknown>(data: unknown, path: number[], def?: T): T {
let cur: unknown = data;
for (let i = 0; i < path.length; i++) {
const k = path[i]!;
if (!Array.isArray(cur)) {
logger.debug(`Safe navigation: path ${JSON.stringify(path)} ended at index ${i} (key '${k}'), returning default.`);
return def as T;
}
cur = cur[k];
if (cur === undefined) {
logger.debug(`Safe navigation: path ${JSON.stringify(path)} ended at index ${i} (key '${k}'), returning default.`);
return def as T;
}
}
if (cur == null && def !== undefined) return def as T;
return cur as T;
}
export function extract_json_from_response(text: string): unknown {
if (typeof text !== 'string') {
throw new TypeError(`Input text is expected to be a string, got ${typeof text} instead.`);
}
let last: unknown = undefined;
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
last = JSON.parse(trimmed) as unknown;
} catch {}
}
if (last === undefined) {
throw new Error('Could not find a valid JSON object or array in the response.');
}
return last;
}
export const extractJsonFromResponse = extract_json_from_response;
export const getNestedValue = get_nested_value;
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
const APP_DATA_DIR = 'baoyu-skills';
const GEMINI_DATA_DIR = 'gemini-web';
const COOKIE_FILE_NAME = 'cookies.json';
const PROFILE_DIR_NAME = 'chrome-profile';
export function resolveUserDataRoot(): string {
if (process.platform === 'win32') {
return process.env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming');
}
if (process.platform === 'darwin') {
return path.join(os.homedir(), 'Library', 'Application Support');
}
return process.env.XDG_DATA_HOME ?? path.join(os.homedir(), '.local', 'share');
}
export function resolveGeminiWebDataDir(): string {
const override = process.env.GEMINI_WEB_DATA_DIR?.trim();
if (override) return path.resolve(override);
return path.join(resolveUserDataRoot(), APP_DATA_DIR, GEMINI_DATA_DIR);
}
export function resolveGeminiWebCookiePath(): string {
const override = process.env.GEMINI_WEB_COOKIE_PATH?.trim();
if (override) return path.resolve(override);
return path.join(resolveGeminiWebDataDir(), COOKIE_FILE_NAME);
}
export function resolveGeminiWebChromeProfileDir(): string {
const override = process.env.GEMINI_WEB_CHROME_PROFILE_DIR?.trim();
if (override) return path.resolve(override);
return path.join(resolveGeminiWebDataDir(), PROFILE_DIR_NAME);
}
export function resolveGeminiWebSessionsDir(): string {
return path.join(resolveGeminiWebDataDir(), 'sessions');
}
export function resolveGeminiWebSessionPath(name: string): string {
const sanitized = name.replace(/[^a-zA-Z0-9_-]/g, '_');
return path.join(resolveGeminiWebSessionsDir(), `${sanitized}.json`);
}
import fs from 'node:fs';
import path from 'node:path';
import { mkdir, writeFile } from 'node:fs/promises';
import { Endpoint, Headers } from '../constants.js';
import { AuthError } from '../exceptions.js';
import { cookie_header, extract_set_cookie_value, fetch_with_timeout } from './http.js';
import { resolveGeminiWebDataDir } from './paths.js';
export async function rotate_1psidts(cookies: Record<string, string>, _proxy?: string | null): Promise<string | null> {
const p = resolveGeminiWebDataDir();
await mkdir(p, { recursive: true });
const sid = cookies['__Secure-1PSID'];
if (!sid) throw new Error('Missing __Secure-1PSID cookie.');
const cachePath = path.join(p, `.cached_1psidts_${sid}.txt`);
try {
const st = fs.statSync(cachePath);
if (Date.now() - st.mtimeMs <= 60_000) return null;
} catch {}
const res = await fetch_with_timeout(Endpoint.ROTATE_COOKIES, {
method: 'POST',
headers: { ...Headers.ROTATE_COOKIES, Cookie: cookie_header(cookies) },
body: '[000,"-0000000000000000000"]',
redirect: 'follow',
timeout_ms: 30_000,
});
if (res.status === 401) throw new AuthError('Failed to refresh cookies (401).');
if (!res.ok) throw new Error(`RotateCookies failed: ${res.status} ${res.statusText}`);
const setCookie = res.headers.get('set-cookie');
const v = extract_set_cookie_value(setCookie, '__Secure-1PSIDTS');
if (v) {
await writeFile(cachePath, v, 'utf8');
return v;
}
return null;
}
export const rotate1psidts = rotate_1psidts;
import fs from 'node:fs';
import path from 'node:path';
import { readFile } from 'node:fs/promises';
import { Endpoint, Headers } from '../constants.js';
export async function upload_file(file: string, _proxy?: string | null): Promise<string> {
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
throw new Error(`${file} is not a valid file.`);
}
const filename = path.basename(file);
const content = await readFile(file);
const form = new FormData();
form.append('file', new Blob([content]), filename);
const res = await fetch(Endpoint.UPLOAD, {
method: 'POST',
headers: { ...Headers.UPLOAD },
body: form,
redirect: 'follow',
});
if (!res.ok) {
throw new Error(`Upload failed: ${res.status} ${res.statusText}`);
}
return await res.text();
}
export function parse_file_name(file: string): string {
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
throw new Error(`${file} is not a valid file.`);
}
return path.basename(file);
}
export const uploadFile = upload_file;
export const parseFileName = parse_file_name;
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
import { GeminiClient, GeneratedImage, Model, type ModelOutput } from './gemini-webapi/index.js';
import { resolveGeminiWebChromeProfileDir, resolveGeminiWebCookiePath, resolveGeminiWebSessionPath, resolveGeminiWebSessionsDir } from './gemini-webapi/utils/index.js';
type CliArgs = {
prompt: string | null;
promptFiles: string[];
modelId: string;
json: boolean;
imagePath: string | null;
referenceImages: string[];
sessionId: string | null;
listSessions: boolean;
login: boolean;
cookiePath: string | null;
profileDir: string | null;
help: boolean;
};
type SessionRecord = {
id: string;
metadata: Array<string | null>;
messages: Array<{ role: 'user' | 'assistant'; content: string; timestamp: string; error?: string }>;
createdAt: string;
updatedAt: string;
};
type LegacySessionV1 = {
version?: number;
sessionId?: string;
updatedAt?: string;
conversationId?: string | null;
responseId?: string | null;
choiceId?: string | null;
chatMetadata?: unknown;
};
function normalizeSessionMetadata(input: unknown): Array<string | null> {
if (Array.isArray(input)) {
const out: Array<string | null> = [];
for (const v of input.slice(0, 3)) out.push(typeof v === 'string' ? v : null);
return out.length > 0 ? out : [null, null, null];
}
if (input && typeof input === 'object') {
const v1 = input as LegacySessionV1;
if (Array.isArray(v1.chatMetadata)) return normalizeSessionMetadata(v1.chatMetadata);
const conv = typeof v1.conversationId === 'string' ? v1.conversationId : null;
const rid = typeof v1.responseId === 'string' ? v1.responseId : null;
const rcid = typeof v1.choiceId === 'string' ? v1.choiceId : null;
if (conv || rid || rcid) return [conv, rid, rcid];
}
return [null, null, null];
}
function printUsage(cookiePath: string, profileDir: string): void {
console.log(`Usage:
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts --prompt "Hello"
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts "Hello"
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts --prompt "A cute cat" --image generated.png
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts --promptfiles system.md content.md --image out.png
Multi-turn conversation (agent generates unique sessionId):
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts "Remember 42" --sessionId abc123
npx -y bun skills/baoyu-danger-gemini-web/scripts/main.ts "What number?" --sessionId abc123
Options:
-p, --prompt <text> Prompt text
--promptfiles <files...> Read prompt from one or more files (concatenated in order)
-m, --model <id> gemini-3-pro | gemini-3-flash | gemini-3-flash-thinking | gemini-3.1-pro-preview (default: gemini-3-pro)
--json Output JSON
--image [path] Generate an image and save it (default: ./generated.png)
--reference <files...> Reference images for vision input
--ref <files...> Alias for --reference
--sessionId <id> Session ID for multi-turn conversation (agent should generate unique ID)
--list-sessions List saved sessions (max 100, sorted by update time)
--login Only refresh cookies, then exit
--cookie-path <path> Cookie file path (default: ${cookiePath})
--profile-dir <path> Chrome profile dir (default: ${profileDir})
-h, --help Show help
Env overrides:
GEMINI_WEB_DATA_DIR, GEMINI_WEB_COOKIE_PATH, GEMINI_WEB_CHROME_PROFILE_DIR, GEMINI_WEB_CHROME_PATH`);
}
function parseArgs(argv: string[]): CliArgs {
const out: CliArgs = {
prompt: null,
promptFiles: [],
modelId: 'gemini-3-pro',
json: false,
imagePath: null,
referenceImages: [],
sessionId: null,
listSessions: false,
login: false,
cookiePath: null,
profileDir: null,
help: false,
};
const positional: string[] = [];
const takeMany = (i: number): { items: string[]; next: number } => {
const items: string[] = [];
let j = i + 1;
while (j < argv.length) {
const v = argv[j]!;
if (v.startsWith('-')) break;
items.push(v);
j++;
}
return { items, next: j - 1 };
};
for (let i = 0; i < argv.length; i++) {
const a = argv[i]!;
if (a === '--help' || a === '-h') {
out.help = true;
continue;
}
if (a === '--json') {
out.json = true;
continue;
}
if (a === '--list-sessions') {
out.listSessions = true;
continue;
}
if (a === '--login') {
out.login = true;
continue;
}
if (a === '--prompt' || a === '-p') {
const v = argv[++i];
if (!v) throw new Error(`Missing value for ${a}`);
out.prompt = v;
continue;
}
if (a === '--promptfiles') {
const { items, next } = takeMany(i);
if (items.length === 0) throw new Error('Missing files for --promptfiles');
out.promptFiles.push(...items);
i = next;
continue;
}
if (a === '--model' || a === '-m') {
const v = argv[++i];
if (!v) throw new Error(`Missing value for ${a}`);
out.modelId = v;
continue;
}
if (a === '--sessionId') {
const v = argv[++i];
if (!v) throw new Error('Missing value for --sessionId');
out.sessionId = v;
continue;
}
if (a === '--cookie-path') {
const v = argv[++i];
if (!v) throw new Error('Missing value for --cookie-path');
out.cookiePath = v;
continue;
}
if (a === '--profile-dir') {
const v = argv[++i];
if (!v) throw new Error('Missing value for --profile-dir');
out.profileDir = v;
continue;
}
if (a === '--image' || a.startsWith('--image=')) {
let v: string | null = null;
if (a.startsWith('--image=')) {
v = a.slice('--image='.length).trim();
} else {
const maybe = argv[i + 1];
if (maybe && !maybe.startsWith('-')) {
v = maybe;
i++;
}
}
out.imagePath = v && v.length > 0 ? v : 'generated.png';
continue;
}
if (a === '--reference' || a === '--ref') {
const { items, next } = takeMany(i);
if (items.length === 0) throw new Error(`Missing files for ${a}`);
out.referenceImages.push(...items);
i = next;
continue;
}
if (a.startsWith('-')) {
throw new Error(`Unknown option: ${a}`);
}
positional.push(a);
}
if (!out.prompt && out.promptFiles.length === 0 && positional.length > 0) {
out.prompt = positional.join(' ');
}
return out;
}
function resolveModel(id: string): Model {
const k = id.trim();
if (k === 'gemini-3-pro') return Model.G_3_0_PRO;
if (k === 'gemini-3.0-pro') return Model.G_3_0_PRO;
if (k === 'gemini-3-flash') return Model.G_3_0_FLASH;
if (k === 'gemini-3.0-flash') return Model.G_3_0_FLASH;
if (k === 'gemini-3-flash-thinking') return Model.G_3_0_FLASH_THINKING;
if (k === 'gemini-3.0-flash-thinking') return Model.G_3_0_FLASH_THINKING;
if (k === 'gemini-3.1-pro-preview') return Model.G_3_1_PRO_PREVIEW;
return Model.from_name(k);
}
async function readPromptFromFiles(files: string[]): Promise<string> {
const parts: string[] = [];
for (const f of files) {
parts.push(await readFile(f, 'utf8'));
}
return parts.join('\n\n');
}
async function readPromptFromStdin(): Promise<string | null> {
if (process.stdin.isTTY) return null;
try {
// Bun provides Bun.stdin; Node-compatible read can be flaky across runtimes.
const t = await Bun.stdin.text();
const v = t.trim();
return v.length > 0 ? v : null;
} catch {
return null;
}
}
function normalizeOutputImagePath(p: string): string {
const full = path.resolve(p);
const ext = path.extname(full);
if (ext) return full;
return `${full}.png`;
}
async function loadSession(id: string): Promise<SessionRecord | null> {
const p = resolveGeminiWebSessionPath(id);
try {
const raw = await readFile(p, 'utf8');
const j = JSON.parse(raw) as unknown;
if (!j || typeof j !== 'object') return null;
const sid = (typeof (j as any).id === 'string' && (j as any).id.trim()) || (typeof (j as any).sessionId === 'string' && (j as any).sessionId.trim()) || id;
const metadata = normalizeSessionMetadata((j as any).metadata ?? (j as any).chatMetadata ?? j);
const messages = Array.isArray((j as any).messages) ? ((j as any).messages as SessionRecord['messages']) : [];
const createdAt =
typeof (j as any).createdAt === 'string'
? ((j as any).createdAt as string)
: typeof (j as any).updatedAt === 'string'
? ((j as any).updatedAt as string)
: new Date().toISOString();
const updatedAt = typeof (j as any).updatedAt === 'string' ? ((j as any).updatedAt as string) : createdAt;
return {
id: sid,
metadata,
messages,
createdAt,
updatedAt,
};
} catch {
return null;
}
}
async function saveSession(rec: SessionRecord): Promise<void> {
const dir = resolveGeminiWebSessionsDir();
await mkdir(dir, { recursive: true });
const p = resolveGeminiWebSessionPath(rec.id);
const tmp = `${p}.tmp.${Date.now()}`;
await writeFile(tmp, JSON.stringify(rec, null, 2), 'utf8');
await fs.promises.rename(tmp, p);
}
async function listSessions(): Promise<SessionRecord[]> {
const dir = resolveGeminiWebSessionsDir();
try {
const names = await readdir(dir);
const items: Array<{ path: string; st: number }> = [];
for (const n of names) {
if (!n.endsWith('.json')) continue;
const p = path.join(dir, n);
try {
const s = await stat(p);
items.push({ path: p, st: s.mtimeMs });
} catch {}
}
items.sort((a, b) => b.st - a.st);
const out: SessionRecord[] = [];
for (const it of items.slice(0, 100)) {
try {
const raw = await readFile(it.path, 'utf8');
const j = JSON.parse(raw) as any;
const id =
(typeof j?.id === 'string' && j.id.trim()) ||
(typeof j?.sessionId === 'string' && j.sessionId.trim()) ||
path.basename(it.path, '.json');
out.push({
id,
metadata: normalizeSessionMetadata(j?.metadata ?? j?.chatMetadata ?? j),
messages: Array.isArray(j?.messages) ? j.messages : [],
createdAt:
typeof j?.createdAt === 'string'
? j.createdAt
: typeof j?.updatedAt === 'string'
? j.updatedAt
: new Date(it.st).toISOString(),
updatedAt: typeof j?.updatedAt === 'string' ? j.updatedAt : new Date(it.st).toISOString(),
});
} catch {}
}
out.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
return out.slice(0, 100);
} catch {
return [];
}
}
function formatJson(out: ModelOutput, extra?: Record<string, unknown>): string {
const candidates = out.candidates.map((c) => ({
rcid: c.rcid,
text: c.text,
thoughts: c.thoughts,
images: c.images.map((img) => ({
url: img.url,
title: img.title,
alt: img.alt,
kind: img instanceof GeneratedImage ? 'generated' : 'web',
})),
}));
return JSON.stringify(
{
text: out.text,
thoughts: out.thoughts,
metadata: out.metadata,
chosen: out.chosen,
candidates,
...extra,
},
null,
2,
);
}
async function main(): Promise<void> {
const args = parseArgs(process.argv.slice(2));
if (args.cookiePath) process.env.GEMINI_WEB_COOKIE_PATH = args.cookiePath;
if (args.profileDir) process.env.GEMINI_WEB_CHROME_PROFILE_DIR = args.profileDir;
const cookiePath = resolveGeminiWebCookiePath();
const profileDir = resolveGeminiWebChromeProfileDir();
if (args.help) {
printUsage(cookiePath, profileDir);
return;
}
if (args.listSessions) {
const ss = await listSessions();
for (const s of ss) {
const n = s.messages.length;
const last = s.messages.slice(-1)[0];
const lastLine = last?.content ? String(last.content).split('\n')[0] : '';
console.log(`${s.id}\t${s.updatedAt}\t${n}\t${lastLine}`);
}
return;
}
if (args.login) {
process.env.GEMINI_WEB_LOGIN = '1';
const c = new GeminiClient();
await c.init({ verbose: true });
await c.close();
if (!args.json) console.log(`Cookie refreshed: ${cookiePath}`);
else console.log(JSON.stringify({ ok: true, cookiePath }, null, 2));
return;
}
let prompt: string | null = args.prompt;
if (!prompt && args.promptFiles.length > 0) prompt = await readPromptFromFiles(args.promptFiles);
if (!prompt) prompt = await readPromptFromStdin();
if (!prompt) {
printUsage(cookiePath, profileDir);
process.exitCode = 1;
return;
}
const model = resolveModel(args.modelId);
const c = new GeminiClient();
await c.init({ verbose: false });
try {
let sess: SessionRecord | null = null;
let chat = null as any;
if (args.sessionId) {
sess = (await loadSession(args.sessionId)) ?? {
id: args.sessionId,
metadata: [null, null, null],
messages: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
chat = c.start_chat({ metadata: sess.metadata, model });
}
const files = args.referenceImages.length > 0 ? args.referenceImages : null;
let out: ModelOutput;
if (chat) out = await chat.send_message(prompt, files);
else out = await c.generate_content(prompt, files, model);
let savedImage: string | null = null;
if (args.imagePath) {
const p = normalizeOutputImagePath(args.imagePath);
const dir = path.dirname(p);
await mkdir(dir, { recursive: true });
const img = out.images[0];
if (!img) {
throw new Error('No image returned in response.');
}
const fn = path.basename(p);
const dp = dir;
if (img instanceof GeneratedImage) {
savedImage = await img.save(dp, fn, undefined, false, false, true);
} else {
savedImage = await img.save(dp, fn, c.cookies, false, false);
}
}
if (sess && args.sessionId) {
const now = new Date().toISOString();
sess.updatedAt = now;
sess.metadata = (chat?.metadata ?? sess.metadata).slice(0, 3);
sess.messages.push({ role: 'user', content: prompt, timestamp: now });
sess.messages.push({ role: 'assistant', content: out.text ?? '', timestamp: now });
await saveSession(sess);
}
if (args.json) {
console.log(formatJson(out, { savedImage, sessionId: args.sessionId, model: model.model_name }));
} else if (args.imagePath) {
console.log(savedImage ?? '');
} else {
console.log(out.text);
}
} finally {
await c.close();
}
}
main().catch((e) => {
const msg = e instanceof Error ? e.message : String(e);
console.error(msg);
process.exit(1);
});
Related skills
How it compares
Integration skill for unofficial Gemini web access—not a first-party Google AI SDK or MCP server.
FAQ
Who is baoyu-danger-gemini-web for?
Developers building agent or content workflows who want Gemini (including gems and images) via web-session automation rather than official API keys.
When should I use baoyu-danger-gemini-web?
During Build/integrations while wiring generative steps into your product or agent—e.g. drafting marketing assets, testing image pipelines, or orchestrating gem-powered tasks before launch.
Is baoyu-danger-gemini-web safe to install?
Treat it as high-risk unofficial access: review the Security Audits panel on this page, never commit live cookies to git, and prefer official APIs for production.