diff --git a/gemini/README.md b/gemini/README.md index 032ae1d..f84b2bb 100755 --- a/gemini/README.md +++ b/gemini/README.md @@ -2,7 +2,7 @@ Patched Gemini CLI for use with custom API endpoints. -Latest: **v0.35.3** (13 patches). +Latest: **v0.39.1** (13 patches). > Node.js v20+ required. One-liner installer подтянет Node.js если его нет. diff --git a/gemini/gemini_config.json b/gemini/gemini_config.json index d99b122..c0c957a 100755 --- a/gemini/gemini_config.json +++ b/gemini/gemini_config.json @@ -15,7 +15,7 @@ "gemini-3.1-pro-low", "gemini-3.1-flash-image" ], - "target_version": "0.35.3", + "target_version": "0.39.1", "telemetry_enabled": false, "npm_package": "@google/gemini-cli", "npm_registry": "https://npm.sensey24.ru" diff --git a/gemini/gemini_patcher.py b/gemini/gemini_patcher.py index 5f43049..2f6cae0 100755 --- a/gemini/gemini_patcher.py +++ b/gemini/gemini_patcher.py @@ -657,44 +657,36 @@ def patch_auto_permissions(gemini_root, config): patched_parts.append("config.js: not found") # --- 9a2: Patch settingsSchema.js to add 'yolo' to valid options --- - # Gemini v0.32.1 used 'auto_edit', v0.35+ uses 'autoEdit' — handle both. 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() - if "{ value: 'yolo'" in schema_content: + 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: - backup_done = False - patched = False - for ae_value in ("autoEdit", "auto_edit"): - old_options = ( - "{ value: 'default', label: 'Default' },\n" - f" {{ value: '{ae_value}', label: 'Auto Edit' }},\n" - " { value: 'plan', label: 'Plan' }," - ) - new_options = ( - "{ value: 'default', label: 'Default' },\n" - f" {{ value: '{ae_value}', label: 'Auto Edit' }},\n" - " { value: 'plan', label: 'Plan' },\n" - " { value: 'yolo', label: 'YOLO' }," - ) - if old_options in schema_content: - if not backup_done: - backup = schema_path + ".backup" - if not os.path.exists(backup): - shutil.copy2(schema_path, backup) - backup_done = True - 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(f"settingsSchema.js: yolo option added ({ae_value} variant)") - patched = True - break - if not patched: - patched_parts.append("settingsSchema.js: pattern not found") + patched_parts.append("settingsSchema.js: pattern not found") else: patched_parts.append("settingsSchema.js: not found") @@ -792,25 +784,12 @@ def patch_auto_permissions(gemini_root, config): '[[safety_checker]]\n' 'toolName = "*"\n' 'priority = 100\n' - 'modes = ["default", "autoEdit", "plan"]\n' + 'modes = ["default", "auto_edit", "plan"]\n' '[safety_checker.checker]\n' 'type = "in-process"\n' 'name = "conseca"' ) - # Self-heal: if older patcher inserted invalid `auto_edit` enum value, fix it - if 'modes = ["default", "auto_edit", "plan"]' in conseca_content: - backup = conseca_path + ".backup" - if not os.path.exists(backup): - shutil.copy2(conseca_path, backup) - conseca_content = conseca_content.replace( - 'modes = ["default", "auto_edit", "plan"]', - 'modes = ["default", "autoEdit", "plan"]', - ) - with open(conseca_path, "w", encoding="utf-8") as f: - f.write(conseca_content) - changes += 1 - if old_conseca in conseca_content and 'modes = ["default"' not in conseca_content: backup = conseca_path + ".backup" if not os.path.exists(backup): @@ -835,8 +814,46 @@ def patch_auto_permissions(gemini_root, config): return True, "; ".join(patched_parts) +# ─── Bundled-mode helpers (0.38+) ─────────────────────────────────────── + +def is_bundled_install(gemini_root): + """True iff this install uses Gemini CLI 0.38+ bundle/chunk-*.js layout.""" + bundle_dir = os.path.join(gemini_root, "bundle") + return os.path.isdir(bundle_dir) + + # ─── Target 10: Patch default model constants in models.js ────────────── +def _patch_default_models_in_text(content, default_model, flash_model): + """Apply Target 10 transformations to a string. Returns (new_content, n_changes).""" + changes = 0 + + if default_model: + # Match both 'export const' (legacy) and 'var' (bundled) plus quote variants + pat = r"((?:export const|var)\s+DEFAULT_GEMINI_MODEL\s*=\s*['\"])gemini-\d+\.\d+[^'\"]*(['\"])" + content, n = re.subn(pat, rf"\g<1>{default_model}\g<2>", content) + changes += n + + if flash_model: + pat = r"((?:export const|var)\s+DEFAULT_GEMINI_FLASH_MODEL\s*=\s*['\"])gemini-\d+\.\d+[^'\"]*(['\"])" + content, n = re.subn(pat, rf"\g<1>{flash_model}\g<2>", content) + changes += n + + pat = r"((?:export const|var)\s+DEFAULT_GEMINI_FLASH_LITE_MODEL\s*=\s*['\"])gemini-\d+\.\d+[^'\"]*(['\"])" + content, n = re.subn(pat, rf"\g<1>{flash_model}\g<2>", content) + changes += n + + pat = r"((?:export const|var)\s+DEFAULT_GEMINI_MODEL_AUTO\s*=\s*['\"])auto-gemini-\d[^'\"]*(['\"])" + content, n = re.subn(pat, r"\g<1>auto-gemini-3\g<2>", content) + changes += n + + pat = r"(return\s*['\"]Auto \(Gemini )\d[^'\"]*(['\"])" + content, n = re.subn(pat, r"\g<1>3\g<2>", content) + changes += n + + return content, changes + + def patch_default_models(gemini_root, config): """ Target 10: Override DEFAULT_GEMINI_MODEL, DEFAULT_GEMINI_FLASH_MODEL, @@ -848,6 +865,68 @@ def patch_default_models(gemini_root, config): if not default_model and not flash_model: return True, "Skipped (no default_model/internal_flash_model in config)" + if is_bundled_install(gemini_root): + from gemini.bundle_finder import find_all_chunks_with_anchors + bundle_dir = os.path.join(gemini_root, "bundle") + # Strict anchor: 'var DEFAULT_GEMINI_MODEL ' (with trailing space) + # only matches DEFINITION sites, not USE sites (which look like + # ', DEFAULT_GEMINI_MODEL,' or 'model: DEFAULT_GEMINI_MODEL'). + anchors = [ + "var DEFAULT_GEMINI_MODEL ", + "var DEFAULT_GEMINI_FLASH_MODEL ", + "var DEFAULT_GEMINI_MODEL_AUTO ", + ] + chunks = find_all_chunks_with_anchors(bundle_dir, anchors) + # Fallback for legacy-format bundle: 'export const' style + if not chunks: + anchors = [ + "export const DEFAULT_GEMINI_MODEL ", + "export const DEFAULT_GEMINI_FLASH_MODEL ", + "export const DEFAULT_GEMINI_MODEL_AUTO ", + ] + chunks = find_all_chunks_with_anchors(bundle_dir, anchors) + if not chunks: + return False, "No bundle chunks found defining DEFAULT_GEMINI_MODEL" + total_changes = 0 + modified = 0 + already = 0 + for path in chunks: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + new_content, n = _patch_default_models_in_text(content, default_model, flash_model) + if new_content == content: + # Verify already-patched: definition must point to expected target. + # If the definition is unchanged from legacy AND we did not + # change anything, it's a true no-match (anchor was wrong). + expected_main = ( + f'DEFAULT_GEMINI_MODEL = "{default_model}"' in content + or f"DEFAULT_GEMINI_MODEL = '{default_model}'" in content + ) if default_model else True + expected_flash = ( + f'DEFAULT_GEMINI_FLASH_MODEL = "{flash_model}"' in content + or f"DEFAULT_GEMINI_FLASH_MODEL = '{flash_model}'" in content + ) if flash_model else True + expected_auto = ( + 'DEFAULT_GEMINI_MODEL_AUTO = "auto-gemini-3"' in content + or "DEFAULT_GEMINI_MODEL_AUTO = 'auto-gemini-3'" in content + ) + if expected_main and expected_flash and expected_auto: + already += 1 + # else: silent skip — not counted as already, not as modified + continue + backup = path + ".backup" + if not os.path.exists(backup): + shutil.copy2(path, backup) + with open(path, "w", encoding="utf-8") as f: + f.write(new_content) + modified += 1 + total_changes += n + if modified == 0 and already == 0: + return False, "No models.js patterns matched in bundled chunks" + if modified == 0: + return True, f"Already patched (models.js, {already} chunk(s))" + return True, f"Patched {total_changes} constant(s) across {modified} bundled chunk(s)" + 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}" @@ -950,6 +1029,46 @@ def patch_model_dialog(gemini_root, config): """ default_model = config.get("default_model", "gemini-2.5-pro") flash_model = config.get("internal_flash_model", "gemini-2.5-flash") + new_desc = f"{default_model}, {flash_model}" + + if is_bundled_install(gemini_root): + from gemini.bundle_finder import find_all_chunks_with_anchors + bundle_dir = os.path.join(gemini_root, "bundle") + # Anchor on the unique-enough literal "dialogDescription" line marker. + chunks = find_all_chunks_with_anchors(bundle_dir, ["dialogDescription"]) + # Match either the legacy pair (X.Y-pro, X.Y-flash) OR an already-patched + # config-derived pair (e.g. gemini-3.1-pro-preview, gemini-3-flash-preview). + legacy_re = re.compile(r"gemini-\d+\.\d+-pro, gemini-\d+\.?\d*-?[a-z]*") + modified = 0 + already = 0 + seen = 0 + for path in chunks: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + if new_desc in content: + seen += 1 + already += 1 + continue + if not legacy_re.search(content): + continue # this chunk has dialogDescription but not the model-pair string + seen += 1 + new_content = legacy_re.sub(new_desc, content, count=1) + if new_content == content: + already += 1 + continue + backup = path + ".backup" + if not os.path.exists(backup): + shutil.copy2(path, backup) + with open(path, "w", encoding="utf-8") as f: + f.write(new_content) + modified += 1 + if seen == 0: + return False, "No bundle chunks found with model dialog description" + if modified == 0 and already == seen: + return True, f"Already patched (ModelDialog, {seen} chunk(s))" + if modified == 0: + return False, "No ModelDialog patterns matched in bundled chunks" + return True, f"Patched ModelDialog in {modified} bundled chunk(s)" dialog_path = os.path.join(gemini_root, MODEL_DIALOG_JS_SUBPATH) if not os.path.isfile(dialog_path): @@ -990,12 +1109,61 @@ def patch_model_dialog(gemini_root, config): # ─── Target 12: Patch chatCompressionService.js compression aliases ──── +_COMPRESSION_REGEX_REPLACEMENTS = [ + (r"(case DEFAULT_GEMINI_MODEL:\s*\n\s*return\s+['\"])chat-compression-\d+\.\d+-pro(['\"])", + r"\g<1>chat-compression-3-pro\g<2>"), + (r"(case DEFAULT_GEMINI_FLASH_MODEL:\s*\n\s*return\s+['\"])chat-compression-\d+\.\d+-flash(['\"])", + r"\g<1>chat-compression-3-flash\g<2>"), + (r"(case DEFAULT_GEMINI_FLASH_LITE_MODEL:\s*\n\s*return\s+['\"])chat-compression-\d+\.\d+-flash[^'\"]*(['\"])", + r"\g<1>chat-compression-3-flash\g<2>"), +] + + +def _patch_compression_in_text(content): + changes = 0 + for pat, repl in _COMPRESSION_REGEX_REPLACEMENTS: + content, n = re.subn(pat, repl, content) + changes += n + return content, changes + + 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. """ + if is_bundled_install(gemini_root): + from gemini.bundle_finder import find_all_chunks_with_anchors + bundle_dir = os.path.join(gemini_root, "bundle") + chunks = find_all_chunks_with_anchors( + bundle_dir, ["case DEFAULT_GEMINI_MODEL:", "chat-compression-"] + ) + if not chunks: + return False, "No bundle chunks found with compression switch" + modified = 0 + already = 0 + total = 0 + for path in chunks: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + new_content, n = _patch_compression_in_text(content) + if new_content == content: + already += 1 + continue + backup = path + ".backup" + if not os.path.exists(backup): + shutil.copy2(path, backup) + with open(path, "w", encoding="utf-8") as f: + f.write(new_content) + modified += 1 + total += n + if modified == 0 and already == len(chunks): + return True, f"Already patched (compression, {len(chunks)} chunk(s))" + if modified == 0: + return False, "No compression patterns matched in bundled chunks" + return True, f"Patched {total} compression alias(es) across {modified} bundled chunk(s)" + 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}" @@ -1057,6 +1225,34 @@ 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") + old_example = "gemini-2.0-flash" + + if is_bundled_install(gemini_root): + from gemini.bundle_finder import find_all_chunks_with_anchors + bundle_dir = os.path.join(gemini_root, "bundle") + chunks = find_all_chunks_with_anchors(bundle_dir, [old_example]) + if not chunks: + # Maybe already patched — search for replacement + patched_chunks = find_all_chunks_with_anchors(bundle_dir, [flash_model]) + if patched_chunks: + return True, f"Already patched (AgentConfigDialog, {len(patched_chunks)} chunk(s))" + return False, "No bundle chunks found with AgentConfigDialog example" + modified = 0 + for path in chunks: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + new_content = content.replace(old_example, flash_model) + if new_content == content: + continue + backup = path + ".backup" + if not os.path.exists(backup): + shutil.copy2(path, backup) + with open(path, "w", encoding="utf-8") as f: + f.write(new_content) + modified += 1 + if modified == 0: + return False, "No AgentConfigDialog patterns matched in bundled chunks" + return True, f"Patched AgentConfigDialog example in {modified} bundled chunk(s)" dialog_path = os.path.join(gemini_root, AGENT_CONFIG_DIALOG_SUBPATH) if not os.path.isfile(dialog_path): @@ -1088,6 +1284,7 @@ def patch_agent_config_dialog(gemini_root, config): def rollback(genai_mjs_path, settings_js_path, gemini_root=None): """Restore backup files.""" + import glob as _glob restored = 0 paths = [genai_mjs_path, settings_js_path] if gemini_root: @@ -1097,11 +1294,16 @@ def rollback(genai_mjs_path, settings_js_path, gemini_root=None): paths.append(config_js) schema_js = os.path.join(gemini_root, SETTINGS_SCHEMA_SUBPATH) paths.append(schema_js) - # Targets 10-13 + # Targets 10-13 — legacy paths (pre-0.38) 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)) + # Targets 10-13 — bundled paths (0.38+): every bundle/*.backup + bundle_dir = os.path.join(gemini_root, "bundle") + if os.path.isdir(bundle_dir): + for backup in _glob.glob(os.path.join(bundle_dir, "*.backup")): + paths.append(backup[:-len(".backup")]) for p in paths: backup = p + ".backup" if os.path.exists(backup): diff --git a/gemini/releases/index.json b/gemini/releases/index.json index e8f2ca6..e33dff0 100755 --- a/gemini/releases/index.json +++ b/gemini/releases/index.json @@ -1,6 +1,12 @@ { - "latest": "0.35.3", + "latest": "0.39.1", "releases": [ + { + "version": "0.39.1", + "date": "2026-04-25", + "patches": 6, + "status": "stable" + }, { "version": "0.35.3", "date": "2026-04-01",