release: Gemini CLI v0.39.1 (6 patches)

This commit is contained in:
delta-cloud-208e
2026-04-25 05:34:40 +00:00
parent b97ac4bb9e
commit 4b70384863
4 changed files with 257 additions and 49 deletions

View File

@@ -2,7 +2,7 @@
<!-- VERSION_BADGE:START -->
Patched Gemini CLI for use with custom API endpoints.
Latest: **v0.35.3** (13 patches).
Latest: **v0.39.1** (13 patches).
<!-- VERSION_BADGE:END -->
> Node.js v20+ required. One-liner installer подтянет Node.js если его нет.

View File

@@ -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"

View File

@@ -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):

View File

@@ -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",