Back to skills
SkillHub ClubAnalyze Data & AIFull StackBackendData / AI

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.

Stars
9,533
Hot score
99
Updated
March 20, 2026
Overall rating
C5.5
Composite score
5.5
Best-practice grade
B73.6

Install command

npx @skill-hub/cli install jimliu-baoyu-skills-baoyu-danger-gemini-web

Repository

JimLiu/baoyu-skills

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 repository

Best 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

Claude CodeCodex CLIGemini CLIOpenCode

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

```