Skill: jimliu/baoyu-danger-gemini-web (version 1.56.1)

--- 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. version: 1.56.1 metadata: openclaw: homepage: https://github.com/JimLiu/baoyu-skills#baoyu-danger-gemini-web requires: anyBins: - bun - npx --- # Gemini Web Client Text/image generation via Gemini Web API. Supports reference images and multi-turn conversations. ## User Input Tools When this skill prompts the user, follow this tool-selection rule (priority order): 1. **Prefer built-in user-input tools** exposed by the current agent runtime — e.g., `AskUserQuestion`, `request_user_input`, `clarify`, `ask_user`, or any equivalent. 2. **Fallback**: if no such tool exists, emit a numbered plain-text message and ask the user to reply with the chosen number/answer for each question. 3. **Batching**: if the tool supports multiple questions per call, combine all applicable questions into a single call; if only single-question, ask them one at a time in priority order. Concrete `AskUserQuestion` references below are examples — substitute the local equivalent in other runtimes. ## 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 `{baseDir}` 2. Script path = `{baseDir}/scripts/<script-name>.ts` 3. Resolve `${BUN_X}` runtime: if `bun` installed → `bun`; if `npx` available → `npx -y bun`; else suggest installing bun 4. Replace all `{baseDir}` and `${BUN_X}` in this document with actual values **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) Check EXTEND.md in priority order — the first one found wins: | Priority | Path | Scope | |----------|------|-------| | 1 | `.baoyu-skills/baoyu-danger-gemini-web/EXTEND.md` | Project | | 2 | `${XDG_CONFIG_HOME:-$HOME/.config}/baoyu-skills/baoyu-danger-gemini-web/EXTEND.md` | XDG | | 3 | `$HOME/.baoyu-skills/baoyu-danger-gemini-web/EXTEND.md` | User home | If none found, use defaults. **EXTEND.md supports**: Default model, proxy settings, custom data directory. ## Usage ```bash # Text generation ${BUN_X} {baseDir}/scripts/main.ts "Your prompt" ${BUN_X} {baseDir}/scripts/main.ts --prompt "Your prompt" --model gemini-3-flash # Image generation ${BUN_X} {baseDir}/scripts/main.ts --prompt "A cute cat" --image cat.png ${BUN_X} {baseDir}/scripts/main.ts --promptfiles system.md content.md --image out.png # Vision input (reference images) ${BUN_X} {baseDir}/scripts/main.ts --prompt "Describe this" --reference image.png ${BUN_X} {baseDir}/scripts/main.ts --prompt "Create variation" --reference a.png --image out.png # Multi-turn conversation ${BUN_X} {baseDir}/scripts/main.ts "Remember: 42" --sessionId session-abc ${BUN_X} {baseDir}/scripts/main.ts "What number?" --sessionId session-abc # JSON output ${BUN_X} {baseDir}/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-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. When no explicit profile dir is set, cookie refresh may reuse an already-running local Chrome/Chromium debugging session tied to a standard user-data dir. Set `--profile-dir` or `GEMINI_WEB_CHROME_PROFILE_DIR` to force a dedicated profile and skip existing-session reuse. This is a best-effort CDP session reuse path, not the Chrome DevTools MCP prompt-based `--autoConnect` flow described in Chrome's official docs. 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.

path: scripts/bun.lock
content: {
  "lockfileVersion": 1,
  "configVersion": 1,
  "workspaces": {
    "": {
      "name": "baoyu-danger-gemini-web-scripts",
      "dependencies": {
        "baoyu-chrome-cdp": "^0.1.0",
      },
    },
  },
  "packages": {
    "baoyu-chrome-cdp": ["baoyu-chrome-cdp@0.1.0", "", {}, [REDACTED_SECRET]],
  }
}


path: scripts/gemini-webapi/client.test.ts
content: import assert from "node:assert/strict";
import test from "node:test";

import { collect_generated_image_urls_from_response_parts } from "./client.ts";

test("response part fallback finds generated images when legacy generated markers are absent", () => {
  const generatedUrl = "https://lh3.googleusercontent.com/gg-dl/example-generated-image";
  const initialCandidate = ["rcid-1", ["image generated successfully"]];
  const imageCandidate = [
    "rcid-1",
    ["image generated successfully"],
    { nestedPayload: [{ media: generatedUrl }] },
  ];
  const responseJson = [
    ["wrb.fr", null, JSON.stringify([null, [], null, null, [initialCandidate]])],
    ["wrb.fr", null, JSON.stringify([null, [], null, null, [imageCandidate]])],
  ];

  assert.equal(initialCandidate[12], undefined);
  assert.equal(
    /http:\/\/googleusercontent\.com\/image_generation_content\/\d+/.test(String(initialCandidate[1]?.[0])),
    false,
  );
  assert.deepEqual(collect_generated_image_urls_from_response_parts(responseJson, 0, 0), [generatedUrl]);
});


path: scripts/gemini-webapi/client.ts
content: 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 };

const GENERATED_IMAGE_URL_PREFIX = 'https://lh3.googleusercontent.com/gg-dl/';

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

function collect_generated_image_urls(root: unknown, limit: number = 4): string[] {
  return collect_strings(root, (s) => s.startsWith(GENERATED_IMAGE_URL_PREFIX), limit);
}

function parse_response_part_body(part: unknown): unknown[] | null {
  if (!Array.isArray(part)) return null;
  const part_body = get_nested_value<string | null>(part, [2], null);
  if (!part_body) return null;
  try {
    const part_json = JSON.parse(part_body) as unknown;
    return Array.isArray(part_json) ? part_json : null;
  } catch {
    return null;
  }
}

function find_generated_image_part(
  response_json: unknown[],
  body_index: number,
  candidate_index: number,
  limit: number = 4,
): { body: unknown[]; urls: string[] } | null {
  for (let part_index = body_index; part_index < response_json.length; part_index++) {
    const part_json = parse_response_part_body(response_json[part_index]);
    if (!part_json) continue;
    const cand = get_nested_value<unknown>(part_json, [4, candidate_index], null);
    if (!cand) continue;
    const urls = collect_generated_image_urls(cand, limit);
    if (urls.length > 0) return { body: part_json, urls };
  }
  return null;
}

export function collect_generated_image_urls_from_response_parts(
  response_json: unknown[],
  body_index: number,
  candidate_index: number,
  limit: number = 4,
): string[] {
  return find_generated_image_part(response_json, body_index, candidate_index, limit)?.urls ?? [];
}

function push_generated_images(
  generated_images: GeneratedImage[],
  urls: string[],
  proxy: string | null,
  cookies: Record<string, string>,
): void {
  for (const url of urls) {
    generated_images.push(new GeneratedImage(url, '[Generated Image]', '', proxy, cookies));
  }
}

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) {
            const image_part = find_generated_image_part(response_json as unknown[], body_index, candidate_index, 1);
            const img_body = image_part?.body ?? null;

            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) {
              push_generated_images(generated_images, collect_generated_image_urls(img_candidate), this.proxy, this.cookies);
            }
          }

          // Fallback: unconditionally scan all response parts for generated image URLs.
          // The `wants_generated` detection above relies on `candidate[12][7][0]` and an old
          // `googleusercontent.com/image_generation_content/` URL pattern, both of which no
          // longer appear in the current Gemini Web API response format.
          // When Gemini does generate images, their URLs now start with
          // `https://lh3.googleusercontent.com/gg-dl/` and are present somewhere in the
          // response parts — this fallback finds them so images are not silently dropped.
          if (generated_images.length === 0) {
            const urls = collect_generated_image_urls_from_response_parts(response_json as unknown[], body_index, candidate_index);
            push_generated_images(generated_images, urls, 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;
  }
}


path: scripts/gemini-webapi/components/gem-mixin.ts
content: 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);
  }
}



path: scripts/gemini-webapi/components/index.ts
content: export { GemMixin } from './gem-mixin.js';



path: scripts/gemini-webapi/constants.ts
content: 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);
  }
}



path: scripts/gemini-webapi/exceptions.ts
content: 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';
  }
}



path: scripts/gemini-webapi/index.ts
content: 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';



path: scripts/gemini-webapi/types/candidate.ts
content: 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(/&amp;/g, '&')
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&quot;/g, '"')
    .replace(/&apos;/g, "'")
    .replace(/&#39;/g, "'")
    .replace(/&nbsp;/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];
  }
}


path: scripts/gemini-webapi/types/gem.ts
content: 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);
  }
}



path: scripts/gemini-webapi/types/grpc.ts
content: 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];
  }
}



path: scripts/gemini-webapi/types/image.ts
content: 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);
  }
}


path: scripts/gemini-webapi/types/index.ts
content: 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';



path: scripts/gemini-webapi/types/modeloutput.ts
content: 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 ?? '';
  }
}



path: scripts/gemini-webapi/utils/cookie-file.ts
content: 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;


path: scripts/gemini-webapi/utils/decorators.ts
content: 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;
  };
}



path: scripts/gemini-webapi/utils/get-access-token.ts
content: 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;


path: scripts/gemini-webapi/utils/http.ts
content: 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;


path: scripts/gemini-webapi/utils/index.ts
content: 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>();



path: scripts/gemini-webapi/utils/load-browser-cookies.ts
content: import process from 'node:process';

import {
  CdpConnection,
  discoverRunningChromeDebugPort,
  findChromeExecutable as findChromeExecutableBase,
  findExistingChromeDebugPort,
  gracefulKillChrome,
  getFreePort,
  launchChrome as launchChromeBase,
  openPageSession,
  sleep,
  waitForChromeDebugPort,
  type PlatformCandidates,
} from 'baoyu-chrome-cdp';

import { Endpoint, Headers } from '../constants.js';
import { logger } from './logger.js';
import { cookie_header, fetch_with_timeout } from './http.js';
import { read_cookie_file, type CookieMap, write_cookie_file } from './cookie-file.js';
import { resolveGeminiWebChromeProfileDir, resolveGeminiWebCookiePath } from './paths.js';

const GEMINI_APP_URL = 'https://gemini.google.com/app';

const CHROME_CANDIDATES_FULL: PlatformCandidates = {
  darwin: [
    '/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',
  ],
  win32: [
    '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',
  ],
  default: [
    '/usr/bin/google-chrome',
    '/usr/bin/google-chrome-stable',
    '/usr/bin/chromium',
    '/usr/bin/chromium-browser',
    '/snap/bin/chromium',
    '/usr/bin/microsoft-edge',
  ],
};

async function get_free_port(): Promise<number> {
  return await getFreePort('GEMINI_WEB_DEBUG_PORT');
}

function find_chrome_executable(): string | null {
  return findChromeExecutableBase({
    candidates: CHROME_CANDIDATES_FULL,
    envNames: ['GEMINI_WEB_CHROME_PATH'],
  }) ?? null;
}

async function find_existing_chrome_debug_port(profileDir: string): Promise<number | null> {
  return await findExistingChromeDebugPort({ profileDir });
}

async function launch_chrome(profileDir: string, port: number) {
  const chromePath = find_chrome_executable();
  if (!chromePath) throw new Error('Chrome executable not found.');

  return await launchChromeBase({
    chromePath,
    profileDir,
    port,
    url: GEMINI_APP_URL,
    extraArgs: ['--disable-popup-blocking'],
  });
}

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_cookies_from_existing_chrome(
  timeoutMs: number,
  verbose: boolean,
): Promise<CookieMap | null> {
  const discovered = await discoverRunningChromeDebugPort();
  if (discovered === null) return null;

  if (verbose) logger.info(`Found reusable Chrome debugging session on port ${discovered.port}. Connecting via WebSocket...`);

  let cdp: CdpConnection | null = null;
  let targetId: string | null = null;
  let createdTarget = false;
  try {
    const connectStart = Date.now();
    const connectTimeout = 30_000;
    let lastConnErr: unknown = null;
    while (Date.now() - connectStart < connectTimeout) {
      try {
        cdp = await CdpConnection.connect(discovered.wsUrl, 5_000);
        break;
      } catch (e) {
        lastConnErr = e;
        if (verbose) logger.debug(`WebSocket connect attempt failed: ${e instanceof Error ? e.message : String(e)}, retrying...`);
        await sleep(1000);
      }
    }
    if (!cdp) {
      if (verbose) logger.debug(`Could not connect to Chrome after ${connectTimeout / 1000}s: ${lastConnErr instanceof Error ? lastConnErr.message : String(lastConnErr)}`);
      return null;
    }

    const page = await openPageSession({
      cdp,
      reusing: false,
      url: GEMINI_APP_URL,
      matchTarget: (target) => target.type === 'page' && target.url.includes('gemini.google.com'),
      enableNetwork: true,
      activateTarget: false,
    });
    const { sessionId } = page;
    targetId = page.targetId;
    createdTarget = page.createdTarget;

    if (verbose) logger.debug(createdTarget ? 'No Gemini tab found, creating new tab...' : 'Found existing Gemini tab, attaching...');

    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 cookieMap: CookieMap = {};
      for (const cookie of cookies) {
        if (cookie?.name && typeof cookie.value === 'string') cookieMap[cookie.name] = cookie.value;
      }

      last = cookieMap;
      if (await is_gemini_session_ready(cookieMap, verbose)) return cookieMap;

      await sleep(1000);
    }

    if (verbose) logger.debug(`Existing Chrome did not yield valid cookies. Last keys: ${Object.keys(last).join(', ')}`);
    return null;
  } catch (e) {
    if (verbose) logger.debug(`Failed to connect to existing Chrome debugging session: ${e instanceof Error ? e.message : String(e)}`);
    return null;
  } finally {
    if (cdp) {
      if (createdTarget && targetId) {
        try { await cdp.send('Target.closeTarget', { targetId }, { timeoutMs: 5_000 }); } catch {}
      }
      cdp.close();
    }
  }
}

async function fetch_google_cookies_via_cdp(
  profileDir: string,
  timeoutMs: number,
  verbose: boolean,
): Promise<CookieMap> {
  const existingPort = await find_existing_chrome_debug_port(profileDir);
  const reusing = existingPort !== null;
  const port = existingPort ?? await get_free_port();
  const chrome = reusing ? null : await launch_chrome(profileDir, port);

  let cdp: CdpConnection | null = null;
  let targetId: string | null = null;
  try {
    const wsUrl = await waitForChromeDebugPort(port, 30_000, { includeLastError: true });
    cdp = await CdpConnection.connect(wsUrl, 15_000);

    if (verbose) {
      logger.info(reusing
        ? `Reusing existing Chrome on port ${port}. Waiting for a valid Gemini session...`
        : 'Chrome opened. If needed, complete Google login in the window. Waiting for a valid Gemini session...');
    }

    const page = await openPageSession({
      cdp,
      reusing,
      url: GEMINI_APP_URL,
      matchTarget: (target) => target.type === 'page' && target.url.includes('gemini.google.com'),
      enableNetwork: true,
    });
    const { sessionId } = page;
    targetId = page.targetId;

    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 cookieMap: CookieMap = {};
      for (const cookie of cookies) {
        if (cookie?.name && typeof cookie.value === 'string') cookieMap[cookie.name] = cookie.value;
      }

      last = cookieMap;
      if (await is_gemini_session_ready(cookieMap, verbose)) {
        return cookieMap;
      }

      await sleep(1000);
    }

    throw new Error(`Timed out waiting for a valid Gemini session. Last keys: ${Object.keys(last).join(', ')}`);
  } finally {
    if (cdp) {
      if (reusing && targetId) {
        try {
          await cdp.send('Target.closeTarget', { targetId }, { timeoutMs: 5_000 });
        } catch {}
      }
      cdp.close();
    }

    if (chrome) await gracefulKillChrome(chrome, port);
  }
}

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 hasExplicitProfile = !!(process.env.GEMINI_WEB_CHROME_PROFILE_DIR?.trim() || process.env.BAOYU_CHROME_PROFILE_DIR?.trim());
  const existingCookies = hasExplicitProfile ? null : await fetch_cookies_from_existing_chrome(30_000, verbose);
  if (existingCookies) {
    const filtered: CookieMap = {};
    for (const [key, value] of Object.entries(existingCookies)) {
      if (typeof value === 'string' && value.length > 0) filtered[key] = value;
    }

    await write_cookie_file(filtered, resolveGeminiWebCookiePath(), 'cdp-existing');
    void domain_name;
    return { chrome: filtered };
  }

  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 [key, value] of Object.entries(cookies)) {
    if (typeof value === 'string' && value.length > 0) filtered[key] = value;
  }

  await write_cookie_file(filtered, resolveGeminiWebCookiePath(), 'cdp');
  void domain_name;
  return { chrome: filtered };
}

export const loadBrowserCookies = load_browser_cookies;


path: scripts/gemini-webapi/utils/logger.ts
content: 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),
};



path: scripts/gemini-webapi/utils/parsing.ts
content: 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;


path: scripts/gemini-webapi/utils/paths.ts
content: import { execSync } from 'node:child_process';
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);
}

let _wslHome: string | null | undefined;
function getWslWindowsHome(): string | null {
  if (_wslHome !== undefined) return _wslHome;
  if (!process.env.WSL_DISTRO_NAME) { _wslHome = null; return null; }
  try {
    const raw = execSync('cmd.exe /C "echo %USERPROFILE%"', { encoding: 'utf-8', timeout: 5000 }).trim().replace(/\r/g, '');
    _wslHome = execSync(`wslpath -u "${raw}"`, { encoding: 'utf-8', timeout: 5000 }).trim() || null;
  } catch { _wslHome = null; }
  return _wslHome;
}

export function resolveGeminiWebChromeProfileDir(): string {
  const override = process.env.BAOYU_CHROME_PROFILE_DIR?.trim() || process.env.GEMINI_WEB_CHROME_PROFILE_DIR?.trim();
  if (override) return path.resolve(override);
  const wslHome = getWslWindowsHome();
  if (wslHome) return path.join(wslHome, '.local', 'share', APP_DATA_DIR, PROFILE_DIR_NAME);
  return path.join(resolveUserDataRoot(), APP_DATA_DIR, 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`);
}



path: scripts/gemini-webapi/utils/rotate-1psidts.ts
content: 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;



path: scripts/gemini-webapi/utils/upload-file.ts
content: 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;



path: scripts/main.ts
content: 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 formatScriptCommand(fallback: string): string {
  const raw = process.argv[1];
  const displayPath = raw
    ? (() => {
        const relative = path.relative(process.cwd(), raw);
        return relative && !relative.startsWith("..") ? relative : raw;
      })()
    : fallback;
  const quotedPath = displayPath.includes(" ")
    ? `"${displayPath.replace(/"/g, '\\"')}"`
    : displayPath;
  return `npx -y bun ${quotedPath}`;
}

function printUsage(cookiePath: string, profileDir: string): void {
  const cmd = formatScriptCommand("scripts/main.ts");
  console.log(`Usage:
  ${cmd} --prompt "Hello"
  ${cmd} "Hello"
  ${cmd} --prompt "A cute cat" --image generated.png
  ${cmd} --promptfiles system.md content.md --image out.png

Multi-turn conversation (agent generates unique sessionId):
  ${cmd} "Remember 42" --sessionId abc123
  ${cmd} "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

Notes:
  By default cookie refresh may reuse an already-running local Chrome/Chromium debugging session.
  Set --profile-dir or GEMINI_WEB_CHROME_PROFILE_DIR to force a dedicated profile and skip existing-session reuse.
  This reuse path is separate from Chrome DevTools MCP's prompt-based --autoConnect flow.`);
}

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


path: scripts/package.json
content: {
  "name": "baoyu-danger-gemini-web-scripts",
  "private": true,
  "type": "module",
  "dependencies": {
    "baoyu-chrome-cdp": "^0.1.0"
  }
}


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal:  | SkillSpector: suspicious (CRITICAL)