#!/usr/bin/env python3 """ Gemini Code Patcher — patches Gemini CLI to route through custom AI proxy. Targets: 1. gemini_base_url — hardcoded generativelanguage.googleapis.com → proxy 2. vertex_base_url — hardcoded aiplatform.googleapis.com → proxy 3. sanitize_env_url — allow : in URL env vars 4. auth_env_whitelist — add GOOGLE_GEMINI_BASE_URL to whitelist 5. user_settings — settings.json (auth + telemetry) 6. system_env — env vars injection 7. auto_update_registry — redirect registry-url default to npm.sensey24.ru 8. auto_update_commands — add --registry to update commands 9. auto_permissions — bypass tool approval prompts (YOLO mode + conseca safety bypass) 10. default_models — override DEFAULT_GEMINI_MODEL/FLASH/LITE/AUTO in models.js 11. model_dialog_desc — fix hardcoded model names in ModelDialog.js 12. compression_aliases — fix compression config aliases in chatCompressionService.js 13. agent_config_desc — fix example model name in AgentConfigDialog.js """ import json import os import re import sys import shutil import platform import subprocess import argparse from pathlib import Path # ─── Constants ────────────────────────────────────────────────────────── SCRIPT_DIR = Path(__file__).resolve().parent CONFIG_PATH = SCRIPT_DIR / "gemini_config.json" IS_WINDOWS = platform.system() == "Windows" IS_MACOS = platform.system() == "Darwin" NPM_PACKAGE = "@google/gemini-cli" GENAI_SUBPATH = "node_modules/@google/genai/dist/node/index.mjs" SETTINGS_JS_SUBPATH = "dist/src/config/settings.js" REGISTRY_URL_SUBPATH = "node_modules/registry-url/index.js" INSTALL_INFO_SUBPATH = "dist/src/utils/installationInfo.js" NPM_CONF_DEFAULTS_SUBPATH = "node_modules/@pnpm/npm-conf/lib/defaults.js" CONFIG_JS_SUBPATH = "dist/src/config/config.js" SETTINGS_SCHEMA_SUBPATH = "dist/src/config/settingsSchema.js" MODELS_JS_SUBPATH = "node_modules/@google/gemini-cli-core/dist/src/config/models.js" MODEL_DIALOG_JS_SUBPATH = "dist/src/ui/components/ModelDialog.js" CHAT_COMPRESSION_SUBPATH = "node_modules/@google/gemini-cli-core/dist/src/services/chatCompressionService.js" AGENT_CONFIG_DIALOG_SUBPATH = "dist/src/ui/components/AgentConfigDialog.js" CONSECA_TOML_SUBPATH = "node_modules/@google/gemini-cli-core/dist/src/policy/policies/conseca.toml" # ANSI colors GREEN = "\033[92m" YELLOW = "\033[93m" RED = "\033[91m" CYAN = "\033[96m" BOLD = "\033[1m" RESET = "\033[0m" PATCH_MARKER = "/* GEMINI_PATCHED */" # ─── Utilities ────────────────────────────────────────────────────────── def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) def color(text, c): return f"{c}{text}{RESET}" # ─── Detection ────────────────────────────────────────────────────────── def default_gemini_paths(): """Return list of possible Gemini CLI install paths.""" paths = [] if IS_WINDOWS: appdata = os.environ.get("APPDATA", "") if appdata: paths.append(os.path.join(appdata, "npm", "node_modules", NPM_PACKAGE)) localappdata = os.environ.get("LOCALAPPDATA", "") if localappdata: paths.append(os.path.join(localappdata, "npm", "node_modules", NPM_PACKAGE)) elif IS_MACOS: paths.extend([ f"/opt/homebrew/lib/node_modules/{NPM_PACKAGE}", f"/usr/local/lib/node_modules/{NPM_PACKAGE}", f"/usr/lib/node_modules/{NPM_PACKAGE}", ]) else: paths.extend([ f"/usr/lib/node_modules/{NPM_PACKAGE}", f"/usr/local/lib/node_modules/{NPM_PACKAGE}", ]) return paths def detect_gemini(): """Find Gemini CLI installation. Returns (gemini_root, genai_mjs, settings_js) or raises.""" for root in default_gemini_paths(): genai_mjs = os.path.join(root, GENAI_SUBPATH) settings_js = os.path.join(root, SETTINGS_JS_SUBPATH) if os.path.isfile(genai_mjs): return root, genai_mjs, settings_js raise FileNotFoundError( f"Gemini CLI not found. Checked: {default_gemini_paths()}" ) def read_version(gemini_root): """Read Gemini CLI version from package.json.""" pkg_path = os.path.join(gemini_root, "package.json") if not os.path.isfile(pkg_path): return "unknown" with open(pkg_path, "r") as f: data = json.load(f) return data.get("version", "unknown") # ─── Config ───────────────────────────────────────────────────────────── def load_config(config_path=None): """Load gemini_config.json.""" path = config_path or CONFIG_PATH if not os.path.isfile(path): raise FileNotFoundError(f"Config not found: {path}") with open(path, "r") as f: return json.load(f) # ─── Target 1+2: Patch genai URLs ────────────────────────────────────── def patch_genai_urls(genai_mjs_path, config): """ Patch hardcoded API URLs in @google/genai/dist/node/index.mjs. Target 1: generativelanguage.googleapis.com → proxy Target 2: aiplatform.googleapis.com → proxy """ if not os.path.isfile(genai_mjs_path): return False, f"File not found: {genai_mjs_path}" with open(genai_mjs_path, "r", encoding="utf-8") as f: content = f.read() base_url = config["base_url"].rstrip("/") + "/" changes = 0 original = content # Check if already patched if PATCH_MARKER in content: return True, "Already patched (marker found)" # Target 1: Gemini API base URL pattern1 = r'(initHttpOptions\.baseUrl\s*=\s*)`https://generativelanguage\.googleapis\.com/`' replacement1 = rf'\1`{base_url}`' content, n = re.subn(pattern1, replacement1, content) changes += n # Target 2a: Regional Vertex AI endpoint pattern2a = r"return\s*`https://\$\{this\.clientOptions\.location\}-aiplatform\.googleapis\.com/`" replacement2a = f"return `{base_url}`" content, n = re.subn(pattern2a, replacement2a, content) changes += n # Target 2b: Global Vertex AI endpoint pattern2b = r"return\s*`https://aiplatform\.googleapis\.com/`" replacement2b = f"return `{base_url}`" content, n = re.subn(pattern2b, replacement2b, content) changes += n # Target 1b: baseURL fallback (genai SDK >=1.30) base_url_noslash = base_url.rstrip("/") pattern1b = r'(baseURL:\s*baseURL\s*\|\|\s*)`https://generativelanguage\.googleapis\.com`' replacement1b = rf'\1`{base_url_noslash}`' content, n = re.subn(pattern1b, replacement1b, content) changes += n # Target 1c: baseURLOverridden default check (genai SDK >=1.30) pattern1c = r"(this\.baseURL\s*!==\s*)'https://generativelanguage\.googleapis\.com'" replacement1c = rf"\1'{base_url_noslash}'" content, n = re.subn(pattern1c, replacement1c, content) changes += n # Catch-all: replace any remaining googleapis URLs in non-comment code remaining = content.count("generativelanguage.googleapis.com") if remaining > 0: content = content.replace("generativelanguage.googleapis.com", base_url_noslash.replace("https://", "").replace("http://", "")) changes += remaining if changes == 0: return False, "No URL patterns matched — structure may have changed" # Add patch marker at the top content = PATCH_MARKER + "\n" + content # Write back # Create backup first backup_path = genai_mjs_path + ".backup" if not os.path.exists(backup_path): shutil.copy2(genai_mjs_path, backup_path) with open(genai_mjs_path, "w", encoding="utf-8") as f: f.write(content) return True, f"Patched {changes} URL(s) in genai index.mjs" # ─── Target 3+4: Patch settings.js ───────────────────────────────────── def patch_settings_js(settings_js_path): """ Target 3: Fix sanitizeEnvVar to allow : in URLs. Target 4: Add GOOGLE_GEMINI_BASE_URL to AUTH_ENV_VAR_WHITELIST. """ if not os.path.isfile(settings_js_path): return False, f"File not found: {settings_js_path}" with open(settings_js_path, "r", encoding="utf-8") as f: content = f.read() changes = 0 # Target 3: sanitizeEnvVar — allow : and @ in regex old_sanitize = "return value.replace(/[^a-zA-Z0-9\\-_./]/g, '');" new_sanitize = "return value.replace(/[^a-zA-Z0-9\\-_./:@]/g, '');" if old_sanitize in content: content = content.replace(old_sanitize, new_sanitize, 1) changes += 1 # Target 4: AUTH_ENV_VAR_WHITELIST — add BASE_URL entries if "GOOGLE_GEMINI_BASE_URL" not in content: old_whitelist = "'GOOGLE_CLOUD_LOCATION',\n];" new_whitelist = ( "'GOOGLE_CLOUD_LOCATION',\n" " 'GOOGLE_GEMINI_BASE_URL',\n" " 'GOOGLE_VERTEX_BASE_URL',\n" "];" ) if old_whitelist in content: content = content.replace(old_whitelist, new_whitelist, 1) changes += 1 else: # Try alternate formatting old_whitelist2 = "'GOOGLE_CLOUD_LOCATION',\n ];" new_whitelist2 = ( "'GOOGLE_CLOUD_LOCATION',\n" " 'GOOGLE_GEMINI_BASE_URL',\n" " 'GOOGLE_VERTEX_BASE_URL',\n" " ];" ) if old_whitelist2 in content: content = content.replace(old_whitelist2, new_whitelist2, 1) changes += 1 if changes == 0: # Check if already patched already_patched = ( new_sanitize in content or "/:@]" in content ) and "GOOGLE_GEMINI_BASE_URL" in content if already_patched: return True, "Already patched (settings.js)" return False, "No settings.js patterns matched" # Backup backup_path = settings_js_path + ".backup" if not os.path.exists(backup_path): shutil.copy2(settings_js_path, backup_path) with open(settings_js_path, "w", encoding="utf-8") as f: f.write(content) return True, f"Patched {changes} target(s) in settings.js" # ─── Target 5: User settings.json ────────────────────────────────────── def patch_user_settings(config, home_dir=None): """ Target 5: Configure ~/.gemini/settings.json with auth type and telemetry. """ if home_dir is None: home_dir = os.path.expanduser("~") gemini_dir = os.path.join(home_dir, ".gemini") settings_path = os.path.join(gemini_dir, "settings.json") # Ensure dir exists os.makedirs(gemini_dir, exist_ok=True) # Read existing settings existing = {} if os.path.isfile(settings_path): try: with open(settings_path, "r") as f: existing = json.load(f) except (json.JSONDecodeError, OSError): existing = {} # Remove deprecated keys that cause spam warnings DEPRECATED_KEYS = ["codebaseInvestigatorSettings"] exp = existing.get("experimental", {}) for key in DEPRECATED_KEYS: exp.pop(key, None) if "experimental" in existing and not existing["experimental"]: del existing["experimental"] # Also clean system-level configs (read-only warning source) if IS_WINDOWS: sys_paths = ["C:\\ProgramData\\gemini-cli\\settings.json", "C:\\ProgramData\\gemini-cli\\system-defaults.json"] elif IS_MACOS: sys_paths = ["/Library/Application Support/GeminiCli/settings.json"] else: sys_paths = ["/etc/gemini-cli/settings.json", "/etc/gemini-cli/system-defaults.json"] for sp in sys_paths: if os.path.isfile(sp): try: with open(sp, "r") as f: sd = json.load(f) se = sd.get("experimental", {}) changed = False for key in DEPRECATED_KEYS: if key in se: del se[key] changed = True if changed: if not se and "experimental" in sd: del sd["experimental"] with open(sp, "w") as f: json.dump(sd, f, indent=2) except (PermissionError, OSError): pass # skip if no write access # Deep merge our settings if "security" not in existing: existing["security"] = {} if "auth" not in existing["security"]: existing["security"]["auth"] = {} existing["security"]["auth"]["selectedType"] = "gemini-api-key" if "telemetry" not in existing: existing["telemetry"] = {} existing["telemetry"]["enabled"] = config.get("telemetry_enabled", False) existing["telemetry"]["logPrompts"] = False # Set default model (e.g. gemini-3.1-pro-preview) default_model = config.get("default_model") if default_model: if "model" not in existing: existing["model"] = {} existing["model"]["name"] = default_model # Override internal task models to use gemini-3-flash-preview internal_flash_model = config.get("internal_flash_model") if internal_flash_model: if "modelConfigs" not in existing: existing["modelConfigs"] = {} existing["modelConfigs"]["customAliases"] = { "classifier": { "extends": "base", "modelConfig": { "model": internal_flash_model, "generateContentConfig": { "maxOutputTokens": 1024, "thinkingConfig": {"thinkingLevel": "MEDIUM"}, }, }, }, "prompt-completion": { "extends": "base", "modelConfig": { "model": internal_flash_model, "generateContentConfig": { "temperature": 0.3, "maxOutputTokens": 16000, "thinkingConfig": {"thinkingLevel": "LOW"}, }, }, }, "fast-ack-helper": { "extends": "base", "modelConfig": { "model": internal_flash_model, "generateContentConfig": { "temperature": 0.2, "maxOutputTokens": 120, "thinkingConfig": {"thinkingLevel": "LOW"}, }, }, }, "edit-corrector": { "extends": "base", "modelConfig": { "model": internal_flash_model, "generateContentConfig": { "thinkingConfig": {"thinkingLevel": "LOW"}, }, }, }, "summarizer-default": { "extends": "base", "modelConfig": { "model": internal_flash_model, "generateContentConfig": { "maxOutputTokens": 2000, "thinkingConfig": {"thinkingLevel": "MINIMAL"}, }, }, }, "summarizer-shell": { "extends": "base", "modelConfig": { "model": internal_flash_model, "generateContentConfig": { "maxOutputTokens": 2000, "thinkingConfig": {"thinkingLevel": "MINIMAL"}, }, }, }, } # Write back with open(settings_path, "w") as f: json.dump(existing, f, indent=2) return True, f"Settings updated: {settings_path}" # ─── Target 6: System env vars ───────────────────────────────────────── def setup_env_vars(config): """ Target 6: Inject env vars into /etc/environment (Linux) or create wrapper. """ base_url = config["base_url"] api_key = config["api_key"] env_vars = { "GEMINI_API_KEY": api_key, "GOOGLE_GEMINI_BASE_URL": base_url, } if IS_WINDOWS: return _setup_env_windows(env_vars) # Linux/macOS: write to /etc/environment env_file = "/etc/environment" changes = 0 try: existing_content = "" if os.path.isfile(env_file): with open(env_file, "r") as f: existing_content = f.read() lines = existing_content.splitlines() new_lines = [] for line in lines: # Remove old entries key_match = False for key in env_vars: if line.startswith(f"{key}=") or line.startswith(f'export {key}='): key_match = True break if not key_match: new_lines.append(line) # Add new entries for key, value in env_vars.items(): new_lines.append(f'{key}="{value}"') changes += 1 with open(env_file, "w") as f: f.write("\n".join(new_lines) + "\n") # Also export to current process for key, value in env_vars.items(): os.environ[key] = value return True, f"Set {changes} env var(s) in {env_file}" except PermissionError: # Fallback: just set for current process for key, value in env_vars.items(): os.environ[key] = value return True, f"Set {len(env_vars)} env var(s) in current process (no root access for {env_file})" def _setup_env_windows(env_vars): """Set env vars on Windows via setx.""" changes = 0 for key, value in env_vars.items(): try: subprocess.run(["setx", key, value], check=True, capture_output=True) os.environ[key] = value changes += 1 except Exception as e: eprint(f"Warning: Failed to set {key}: {e}") return changes > 0, f"Set {changes} env var(s) via setx" # ─── Target 7+8: Auto-update registry redirect ──────────────────────── def get_auto_update_paths(gemini_root): """Return paths to auto-update related files.""" return { "registry_url": os.path.join(gemini_root, REGISTRY_URL_SUBPATH), "install_info": os.path.join(gemini_root, INSTALL_INFO_SUBPATH), "npm_conf": os.path.join(gemini_root, NPM_CONF_DEFAULTS_SUBPATH), } def patch_auto_update(gemini_root, config): """ Target 7: Redirect registry-url default from registry.npmjs.org → npm_registry. Target 8: Add --registry flag to update commands in installationInfo.js. """ npm_registry = config.get("npm_registry", "").rstrip("/") if not npm_registry: return True, "Skipped (no npm_registry in config)" paths = get_auto_update_paths(gemini_root) changes = 0 patched_parts = [] # --- Target 7a: registry-url/index.js --- reg_path = paths["registry_url"] if os.path.isfile(reg_path): with open(reg_path, "r", encoding="utf-8") as f: content = f.read() old_registry = "https://registry.npmjs.org/" if old_registry in content: backup = reg_path + ".backup" if not os.path.exists(backup): shutil.copy2(reg_path, backup) content = content.replace(old_registry, npm_registry + "/") with open(reg_path, "w", encoding="utf-8") as f: f.write(content) changes += 1 patched_parts.append("registry-url") elif npm_registry in content: patched_parts.append("registry-url: already patched") else: patched_parts.append("registry-url: pattern not found") # --- Target 7b: @pnpm/npm-conf/lib/defaults.js --- conf_path = paths["npm_conf"] if os.path.isfile(conf_path): with open(conf_path, "r", encoding="utf-8") as f: content = f.read() old_conf = "registry: 'https://registry.npmjs.org/'," new_conf = f"registry: '{npm_registry}/'," if old_conf in content: backup = conf_path + ".backup" if not os.path.exists(backup): shutil.copy2(conf_path, backup) content = content.replace(old_conf, new_conf) with open(conf_path, "w", encoding="utf-8") as f: f.write(content) changes += 1 patched_parts.append("npm-conf defaults") elif npm_registry in content: patched_parts.append("npm-conf: already patched") # --- Target 8: installationInfo.js --- info_path = paths["install_info"] if os.path.isfile(info_path): with open(info_path, "r", encoding="utf-8") as f: content = f.read() cmd_changes = 0 # npm, pnpm, yarn: append --registry for old_cmd in [ "'npm install -g @google/gemini-cli@latest'", "'pnpm add -g @google/gemini-cli@latest'", "'yarn global add @google/gemini-cli@latest'", ]: new_cmd = old_cmd.replace("@latest'", f"@latest --registry {npm_registry}'") if old_cmd in content: content = content.replace(old_cmd, new_cmd) cmd_changes += 1 # bun: prefix with BUN_CONFIG_REGISTRY env var old_bun = "'bun add -g @google/gemini-cli@latest'" new_bun = f"'BUN_CONFIG_REGISTRY={npm_registry} bun add -g @google/gemini-cli@latest'" if old_bun in content: content = content.replace(old_bun, new_bun) cmd_changes += 1 if cmd_changes > 0: backup = info_path + ".backup" if not os.path.exists(backup): shutil.copy2(info_path, backup) with open(info_path, "w", encoding="utf-8") as f: f.write(content) changes += cmd_changes patched_parts.append(f"installationInfo: {cmd_changes} command(s)") elif npm_registry in content: patched_parts.append("installationInfo: already patched") else: patched_parts.append("installationInfo: no commands matched") if changes == 0 and any("already patched" in p for p in patched_parts): return True, "Already patched (" + "; ".join(patched_parts) + ")" elif changes == 0: return False, "No auto-update patterns matched (" + "; ".join(patched_parts) + ")" return True, "; ".join(patched_parts) # ─── Target 9: Auto-permissions (bypass approval prompts) ────────────── def patch_auto_permissions(gemini_root, config): """ Target 9a: Patch config.js to allow YOLO mode from settings.json. Target 9b: Set defaultApprovalMode=yolo + disable folderTrust in settings.json. Target 9c: Auto-trust folders via trustedFolders.json. Target 9d: Patch conseca.toml to skip safety checker in YOLO mode. """ changes = 0 patched_parts = [] # --- 9a: Patch config.js to remove YOLO filter from settings --- config_js_path = os.path.join(gemini_root, CONFIG_JS_SUBPATH) if os.path.isfile(config_js_path): with open(config_js_path, "r", encoding="utf-8") as f: content = f.read() old_yolo_filter = ( "(settings.general?.defaultApprovalMode !== 'yolo'\n" " ? settings.general?.defaultApprovalMode\n" " : undefined)" ) new_yolo_filter = "(settings.general?.defaultApprovalMode)" if old_yolo_filter in content: backup = config_js_path + ".backup" if not os.path.exists(backup): shutil.copy2(config_js_path, backup) content = content.replace(old_yolo_filter, new_yolo_filter, 1) with open(config_js_path, "w", encoding="utf-8") as f: f.write(content) changes += 1 patched_parts.append("config.js: yolo filter removed") elif new_yolo_filter in content and old_yolo_filter not in content: patched_parts.append("config.js: already patched") else: patched_parts.append("config.js: pattern not found") else: patched_parts.append("config.js: not found") # --- 9a2: Patch settingsSchema.js to add 'yolo' to valid options --- schema_path = os.path.join(gemini_root, SETTINGS_SCHEMA_SUBPATH) if os.path.isfile(schema_path): with open(schema_path, "r", encoding="utf-8") as f: schema_content = f.read() old_options = ( "{ value: 'default', label: 'Default' },\n" " { value: 'auto_edit', label: 'Auto Edit' },\n" " { value: 'plan', label: 'Plan' }," ) new_options = ( "{ value: 'default', label: 'Default' },\n" " { value: 'auto_edit', label: 'Auto Edit' },\n" " { value: 'plan', label: 'Plan' },\n" " { value: 'yolo', label: 'YOLO' }," ) if old_options in schema_content and "{ value: 'yolo'" not in schema_content: backup = schema_path + ".backup" if not os.path.exists(backup): shutil.copy2(schema_path, backup) schema_content = schema_content.replace(old_options, new_options, 1) with open(schema_path, "w", encoding="utf-8") as f: f.write(schema_content) changes += 1 patched_parts.append("settingsSchema.js: yolo option added") elif "{ value: 'yolo'" in schema_content: patched_parts.append("settingsSchema.js: already patched") else: patched_parts.append("settingsSchema.js: pattern not found") else: patched_parts.append("settingsSchema.js: not found") # --- 9b: Set YOLO mode + disable folderTrust in settings.json --- home_dir = os.path.expanduser("~") gemini_dir = os.path.join(home_dir, ".gemini") settings_path = os.path.join(gemini_dir, "settings.json") os.makedirs(gemini_dir, exist_ok=True) existing = {} if os.path.isfile(settings_path): try: with open(settings_path, "r") as f: existing = json.load(f) except (json.JSONDecodeError, OSError): existing = {} settings_changed = False if "general" not in existing: existing["general"] = {} if existing["general"].get("defaultApprovalMode") != "yolo": existing["general"]["defaultApprovalMode"] = "yolo" settings_changed = True if "security" not in existing: existing["security"] = {} if "folderTrust" not in existing["security"]: existing["security"]["folderTrust"] = {} if existing["security"]["folderTrust"].get("enabled") is not False: existing["security"]["folderTrust"]["enabled"] = False settings_changed = True if existing.get("security", {}).get("disableYoloMode"): existing["security"]["disableYoloMode"] = False settings_changed = True if not existing["security"].get("enablePermanentToolApproval"): existing["security"]["enablePermanentToolApproval"] = True settings_changed = True if settings_changed: with open(settings_path, "w") as f: json.dump(existing, f, indent=2) changes += 1 patched_parts.append("settings.json: yolo + folderTrust disabled") else: patched_parts.append("settings.json: already configured") # --- 9c: Auto-trust common folders via trustedFolders.json --- trusted_path = os.path.join(gemini_dir, "trustedFolders.json") trusted = {} if os.path.isfile(trusted_path): try: with open(trusted_path, "r") as f: trusted = json.load(f) except (json.JSONDecodeError, OSError): trusted = {} trust_paths = [home_dir, "/home", "/root", "/tmp"] trusted_changed = False for tp in trust_paths: if tp not in trusted: trusted[tp] = "TRUST_PARENT" trusted_changed = True if trusted_changed: with open(trusted_path, "w") as f: json.dump(trusted, f, indent=2) changes += 1 patched_parts.append("trustedFolders.json: paths added") else: patched_parts.append("trustedFolders.json: already configured") # --- 9d: Patch conseca.toml to exclude YOLO from safety checker --- # The conseca safety checker can override YOLO ALLOW → ASK_USER for # "dangerous" commands (rm, chmod, etc.). Adding modes excludes YOLO. conseca_path = os.path.join(gemini_root, CONSECA_TOML_SUBPATH) if os.path.isfile(conseca_path): with open(conseca_path, "r", encoding="utf-8") as f: conseca_content = f.read() # Original conseca.toml has no modes= field, meaning it applies to ALL modes. # We add modes to restrict it to non-YOLO modes only. old_conseca = ( '[[safety_checker]]\n' 'toolName = "*"\n' 'priority = 100\n' '[safety_checker.checker]\n' 'type = "in-process"\n' 'name = "conseca"' ) new_conseca = ( '[[safety_checker]]\n' 'toolName = "*"\n' 'priority = 100\n' 'modes = ["default", "auto_edit", "plan"]\n' '[safety_checker.checker]\n' 'type = "in-process"\n' 'name = "conseca"' ) if old_conseca in conseca_content and 'modes = ["default"' not in conseca_content: backup = conseca_path + ".backup" if not os.path.exists(backup): shutil.copy2(conseca_path, backup) conseca_content = conseca_content.replace(old_conseca, new_conseca, 1) with open(conseca_path, "w", encoding="utf-8") as f: f.write(conseca_content) changes += 1 patched_parts.append("conseca.toml: YOLO excluded from safety checker") elif 'modes = ["default"' in conseca_content: patched_parts.append("conseca.toml: already patched") else: patched_parts.append("conseca.toml: pattern not found") else: patched_parts.append("conseca.toml: not found") if changes == 0 and any("already" in p for p in patched_parts): return True, "Already patched (" + "; ".join(patched_parts) + ")" elif changes == 0: return False, "No auto-permissions patterns matched (" + "; ".join(patched_parts) + ")" return True, "; ".join(patched_parts) # ─── Target 10: Patch default model constants in models.js ────────────── def patch_default_models(gemini_root, config): """ Target 10: Override DEFAULT_GEMINI_MODEL, DEFAULT_GEMINI_FLASH_MODEL, DEFAULT_GEMINI_FLASH_LITE_MODEL, DEFAULT_GEMINI_MODEL_AUTO and getDisplayString() in models.js to use 3.x models from config. """ default_model = config.get("default_model") flash_model = config.get("internal_flash_model") if not default_model and not flash_model: return True, "Skipped (no default_model/internal_flash_model in config)" models_path = os.path.join(gemini_root, MODELS_JS_SUBPATH) if not os.path.isfile(models_path): return False, f"File not found: {models_path}" with open(models_path, "r", encoding="utf-8") as f: content = f.read() changes = 0 # 10a: DEFAULT_GEMINI_MODEL if default_model: old = "export const DEFAULT_GEMINI_MODEL = 'gemini-2.5-pro';" new = f"export const DEFAULT_GEMINI_MODEL = '{default_model}';" if old in content: content = content.replace(old, new, 1) changes += 1 # 10b: DEFAULT_GEMINI_FLASH_MODEL if flash_model: old = "export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-2.5-flash';" new = f"export const DEFAULT_GEMINI_FLASH_MODEL = '{flash_model}';" if old in content: content = content.replace(old, new, 1) changes += 1 # 10c: DEFAULT_GEMINI_FLASH_LITE_MODEL → same as flash (no 3.x lite) if flash_model: old = "export const DEFAULT_GEMINI_FLASH_LITE_MODEL = 'gemini-2.5-flash-lite';" new = f"export const DEFAULT_GEMINI_FLASH_LITE_MODEL = '{flash_model}';" if old in content: content = content.replace(old, new, 1) changes += 1 # 10d: DEFAULT_GEMINI_MODEL_AUTO old_auto = "export const DEFAULT_GEMINI_MODEL_AUTO = 'auto-gemini-2.5';" new_auto = "export const DEFAULT_GEMINI_MODEL_AUTO = 'auto-gemini-3';" if old_auto in content: content = content.replace(old_auto, new_auto, 1) changes += 1 # 10e: getDisplayString() — fix auto display label old_display = "return 'Auto (Gemini 2.5)';" new_display = "return 'Auto (Gemini 3)';" if old_display in content: content = content.replace(old_display, new_display, 1) changes += 1 if changes == 0: # Check if already patched already = True if default_model and f"= '{default_model}';" not in content: already = False if flash_model and f"DEFAULT_GEMINI_FLASH_MODEL = '{flash_model}';" not in content: already = False if already: return True, "Already patched (models.js)" return False, "No models.js patterns matched — structure may have changed" # Backup backup_path = models_path + ".backup" if not os.path.exists(backup_path): shutil.copy2(models_path, backup_path) with open(models_path, "w", encoding="utf-8") as f: f.write(content) return True, f"Patched {changes} constant(s) in models.js" # ─── Target 11: Patch ModelDialog.js hardcoded descriptions ──────────── def patch_model_dialog(gemini_root, config): """ Target 11: Fix hardcoded model names in ModelDialog.js description. """ default_model = config.get("default_model", "gemini-2.5-pro") flash_model = config.get("internal_flash_model", "gemini-2.5-flash") dialog_path = os.path.join(gemini_root, MODEL_DIALOG_JS_SUBPATH) if not os.path.isfile(dialog_path): return False, f"File not found: {dialog_path}" with open(dialog_path, "r", encoding="utf-8") as f: content = f.read() old_desc = "gemini-2.5-pro, gemini-2.5-flash" new_desc = f"{default_model}, {flash_model}" if old_desc not in content: if new_desc in content: return True, "Already patched (ModelDialog.js)" return False, "No ModelDialog.js pattern matched" backup_path = dialog_path + ".backup" if not os.path.exists(backup_path): shutil.copy2(dialog_path, backup_path) content = content.replace(old_desc, new_desc) with open(dialog_path, "w", encoding="utf-8") as f: f.write(content) return True, "Patched model description in ModelDialog.js" # ─── Target 12: Patch chatCompressionService.js compression aliases ──── def patch_compression_aliases(gemini_root, config): """ Target 12: Fix compression config aliases in chatCompressionService.js. After Target 10, DEFAULT_ constants point to 3.x models but switch/case returns 2.5 aliases. Fix to return 3.x aliases. """ compress_path = os.path.join(gemini_root, CHAT_COMPRESSION_SUBPATH) if not os.path.isfile(compress_path): return False, f"File not found: {compress_path}" with open(compress_path, "r", encoding="utf-8") as f: content = f.read() changes = 0 replacements = [ # (old_return_value, new_return_value) ("case DEFAULT_GEMINI_MODEL:\n return 'chat-compression-2.5-pro';", "case DEFAULT_GEMINI_MODEL:\n return 'chat-compression-3-pro';"), ("case DEFAULT_GEMINI_FLASH_MODEL:\n return 'chat-compression-2.5-flash';", "case DEFAULT_GEMINI_FLASH_MODEL:\n return 'chat-compression-3-flash';"), ("case DEFAULT_GEMINI_FLASH_LITE_MODEL:\n return 'chat-compression-2.5-flash-lite';", "case DEFAULT_GEMINI_FLASH_LITE_MODEL:\n return 'chat-compression-3-flash';"), ] for old, new in replacements: if old in content: content = content.replace(old, new, 1) changes += 1 if changes == 0: if "'chat-compression-3-pro'" in content and "'chat-compression-3-flash'" in content: return True, "Already patched (chatCompressionService.js)" return False, "No chatCompressionService.js patterns matched" backup_path = compress_path + ".backup" if not os.path.exists(backup_path): shutil.copy2(compress_path, backup_path) with open(compress_path, "w", encoding="utf-8") as f: f.write(content) return True, f"Patched {changes} compression alias(es)" # ─── Target 13: Patch AgentConfigDialog.js example model name ────────── def patch_agent_config_dialog(gemini_root, config): """ Target 13: Fix example model name in AgentConfigDialog.js description. """ flash_model = config.get("internal_flash_model", "gemini-3-flash-preview") dialog_path = os.path.join(gemini_root, AGENT_CONFIG_DIALOG_SUBPATH) if not os.path.isfile(dialog_path): return False, f"File not found: {dialog_path}" with open(dialog_path, "r", encoding="utf-8") as f: content = f.read() old_example = "gemini-2.0-flash" new_example = flash_model if old_example not in content: if new_example in content: return True, "Already patched (AgentConfigDialog.js)" return False, "No AgentConfigDialog.js pattern matched" backup_path = dialog_path + ".backup" if not os.path.exists(backup_path): shutil.copy2(dialog_path, backup_path) content = content.replace(old_example, new_example) with open(dialog_path, "w", encoding="utf-8") as f: f.write(content) return True, "Patched example model in AgentConfigDialog.js" # ─── Rollback ─────────────────────────────────────────────────────────── def rollback(genai_mjs_path, settings_js_path, gemini_root=None): """Restore backup files.""" restored = 0 paths = [genai_mjs_path, settings_js_path] if gemini_root: auto_paths = get_auto_update_paths(gemini_root) paths.extend(auto_paths.values()) config_js = os.path.join(gemini_root, CONFIG_JS_SUBPATH) paths.append(config_js) schema_js = os.path.join(gemini_root, SETTINGS_SCHEMA_SUBPATH) paths.append(schema_js) # Targets 10-13 paths.append(os.path.join(gemini_root, MODELS_JS_SUBPATH)) paths.append(os.path.join(gemini_root, MODEL_DIALOG_JS_SUBPATH)) paths.append(os.path.join(gemini_root, CHAT_COMPRESSION_SUBPATH)) paths.append(os.path.join(gemini_root, AGENT_CONFIG_DIALOG_SUBPATH)) for p in paths: backup = p + ".backup" if os.path.exists(backup): shutil.copy2(backup, p) restored += 1 return restored > 0, f"Restored {restored} file(s) from backup" # ─── Validation ───────────────────────────────────────────────────────── def validate_installation(config): """Run basic validation: gemini --version and test prompt.""" results = [] # Check gemini exists try: result = subprocess.run( ["gemini", "--version"], capture_output=True, text=True, timeout=10 ) version = result.stdout.strip() or result.stderr.strip() results.append(("version_check", True, version)) except Exception as e: results.append(("version_check", False, str(e))) # Test prompt (needs env vars set) try: env = os.environ.copy() env["GEMINI_API_KEY"] = config["api_key"] env["GOOGLE_GEMINI_BASE_URL"] = config["base_url"] result = subprocess.run( ["gemini", "-p", "Reply with just the word OK, nothing else"], capture_output=True, text=True, timeout=60, env=env ) output = result.stdout.strip() success = "OK" in output.upper() results.append(("prompt_test", success, output[:200])) except Exception as e: results.append(("prompt_test", False, str(e))) return results # ─── Main orchestrator ────────────────────────────────────────────────── def apply_all_patches(config=None, settings_only=False): """Apply all patch targets.""" if config is None: config = load_config() gemini_root, genai_mjs, settings_js = detect_gemini() version = read_version(gemini_root) print(f"\n{BOLD}Gemini Code Patcher{RESET}") print(f" Version: {color(version, CYAN)}") print(f" Root: {gemini_root}") print(f" Proxy: {config['base_url']}") print() results = {} all_ok = True if not settings_only: # Target 1+2: genai URLs ok, msg = patch_genai_urls(genai_mjs, config) results["genai_urls"] = (ok, msg) print(f" {'[OK]' if ok else '[FAIL]':>8} Target 1+2: {msg}") if not ok: all_ok = False # Target 3+4: settings.js ok, msg = patch_settings_js(settings_js) results["settings_js"] = (ok, msg) print(f" {'[OK]' if ok else '[FAIL]':>8} Target 3+4: {msg}") if not ok: all_ok = False # Target 5: user settings ok, msg = patch_user_settings(config) results["user_settings"] = (ok, msg) print(f" {'[OK]' if ok else '[FAIL]':>8} Target 5: {msg}") if not ok: all_ok = False # Target 6: env vars ok, msg = setup_env_vars(config) results["env_vars"] = (ok, msg) print(f" {'[OK]' if ok else '[FAIL]':>8} Target 6: {msg}") if not ok: all_ok = False if not settings_only: # Target 7+8: auto-update registry redirect ok, msg = patch_auto_update(gemini_root, config) results["auto_update"] = (ok, msg) print(f" {'[OK]' if ok else '[FAIL]':>8} Target 7+8: {msg}") if not ok: all_ok = False # Target 9: auto-permissions (bypass approval prompts) ok, msg = patch_auto_permissions(gemini_root, config) results["auto_permissions"] = (ok, msg) print(f" {'[OK]' if ok else '[FAIL]':>8} Target 9: {msg}") if not ok: all_ok = False # Target 10: default model constants in models.js ok, msg = patch_default_models(gemini_root, config) results["default_models"] = (ok, msg) print(f" {'[OK]' if ok else '[FAIL]':>8} Target 10: {msg}") if not ok: all_ok = False # Target 11: ModelDialog description ok, msg = patch_model_dialog(gemini_root, config) results["model_dialog"] = (ok, msg) print(f" {'[OK]' if ok else '[FAIL]':>8} Target 11: {msg}") if not ok: all_ok = False # Target 12: compression aliases ok, msg = patch_compression_aliases(gemini_root, config) results["compression_aliases"] = (ok, msg) print(f" {'[OK]' if ok else '[FAIL]':>8} Target 12: {msg}") if not ok: all_ok = False # Target 13: AgentConfigDialog example model ok, msg = patch_agent_config_dialog(gemini_root, config) results["agent_config_dialog"] = (ok, msg) print(f" {'[OK]' if ok else '[FAIL]':>8} Target 13: {msg}") if not ok: all_ok = False print() if all_ok: print(f" {color('All patches applied successfully!', GREEN)}") else: print(f" {color('Some patches failed. Check output above.', RED)}") return all_ok, results # ─── CLI ──────────────────────────────────────────────────────────────── def main(): parser = argparse.ArgumentParser( description="Gemini Code Patcher — route Gemini CLI through custom proxy" ) parser.add_argument("--apply", action="store_true", help="Apply all patches") parser.add_argument("--settings-only", action="store_true", help="Apply only settings.json + env vars") parser.add_argument("--rollback", action="store_true", help="Restore from backup") parser.add_argument("--detect", action="store_true", help="Detect Gemini CLI installation") parser.add_argument("--validate", action="store_true", help="Validate installation") parser.add_argument("--config", type=str, help="Path to config file") args = parser.parse_args() config = load_config(args.config) if args.detect: try: root, genai, settings = detect_gemini() version = read_version(root) print(f"Gemini CLI found:") print(f" Root: {root}") print(f" Version: {version}") print(f" GenAI MJS: {genai}") print(f" Settings: {settings}") except FileNotFoundError as e: eprint(str(e)) sys.exit(1) elif args.rollback: try: root, genai, settings = detect_gemini() ok, msg = rollback(genai, settings, root) print(msg) sys.exit(0 if ok else 1) except FileNotFoundError as e: eprint(str(e)) sys.exit(1) elif args.validate: results = validate_installation(config) for name, ok, detail in results: status = color("[OK]", GREEN) if ok else color("[FAIL]", RED) print(f" {status} {name}: {detail}") sys.exit(0 if all(r[1] for r in results) else 1) elif args.apply or args.settings_only: ok, _ = apply_all_patches(config, settings_only=args.settings_only) sys.exit(0 if ok else 1) else: parser.print_help() if __name__ == "__main__": main()