baoyu-danger-gemini-web
Generates images and text via reverse-engineered Gemini Web API. Supports text generation, image generation from prompts, reference images for vision input, and multi-turn conversations. Use when other skills need image generation backend, or when user requests "generate image with Gemini", "Gemini text generation", or needs vision-capable AI generation.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install jimliu-baoyu-skills-baoyu-danger-gemini-web
Repository
Skill path: skills/baoyu-danger-gemini-web
Generates images and text via reverse-engineered Gemini Web API. Supports text generation, image generation from prompts, reference images for vision input, and multi-turn conversations. Use when other skills need image generation backend, or when user requests "generate image with Gemini", "Gemini text generation", or needs vision-capable AI generation.
Open repositoryBest for
Primary workflow: Analyze Data & AI.
Technical facets: Full Stack, Backend, Data / AI.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: JimLiu.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install baoyu-danger-gemini-web into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/JimLiu/baoyu-skills before adding baoyu-danger-gemini-web to shared team environments
- Use baoyu-danger-gemini-web for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: baoyu-danger-gemini-web
description: Generates images and text via reverse-engineered Gemini Web API. Supports text generation, image generation from prompts, reference images for vision input, and multi-turn conversations. Use when other skills need image generation backend, or when user requests "generate image with Gemini", "Gemini text generation", or needs vision-capable AI generation.
---
# 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):
```bash
# 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
```bash
# 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-2.5-pro
# 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" --json
```
## Options
| Option | Description |
|--------|-------------|
| `--prompt`, `-p` | Prompt text |
| `--promptfiles` | Read prompt from files (concatenated) |
| `--model`, `-m` | Model: gemini-3-pro (default), gemini-2.5-pro, gemini-2.5-flash |
| `--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 |
| `gemini-2.5-pro` | Previous pro |
| `gemini-2.5-flash` | Fast, lightweight |
## 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.
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### scripts/main.ts
```typescript
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-2.5-pro | gemini-2.5-flash (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-2.5-pro') return Model.G_2_5_PRO;
if (k === 'gemini-2.5-flash') return Model.G_2_5_FLASH;
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);
});
```