diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 38331e02bf5..9439f16dedb 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -5806,6 +5806,16 @@ def _update_via_zip(args): except Exception: pass + # Seed the model-catalog disk cache from the freshly-unpacked checkout + # (same rationale as the git-pull path in _cmd_update_impl). Non-fatal. + try: + from hermes_cli.model_catalog import seed_cache_from_checkout + + if seed_cache_from_checkout(PROJECT_ROOT): + print(" ✓ Model catalog cache refreshed from checkout") + except Exception as e: + logger.debug("Model catalog seed during zip update failed: %s", e) + print() print("✓ Update complete!") try: @@ -8365,6 +8375,22 @@ def _cmd_update_impl(args, gateway_mode: bool): print() print("✓ Code updated!") + # Seed the model-catalog disk cache from the freshly-pulled checkout. + # The repo ships the canonical catalog at + # website/static/api/model-catalog.json, and `git pull` just made it + # current — so copy it straight over ~/.hermes/cache/model_catalog.json + # instead of waiting on a network fetch (which can be bot-gated or hit a + # Portal hiccup). Keeps the model picker's curated/free lists in sync + # with the version the user just installed. Non-fatal on failure: the + # normal network refresh still applies on the next picker open. + try: + from hermes_cli.model_catalog import seed_cache_from_checkout + + if seed_cache_from_checkout(PROJECT_ROOT): + print(" ✓ Model catalog cache refreshed from checkout") + except Exception as e: + logger.debug("Model catalog seed during update failed: %s", e) + # After git pull, source files on disk are newer than cached Python # modules in this process. Reload hermes_constants so that any lazy # import executed below (skills sync, gateway restart) sees new diff --git a/hermes_cli/model_catalog.py b/hermes_cli/model_catalog.py index f69791340dc..40a3a5c00bd 100644 --- a/hermes_cli/model_catalog.py +++ b/hermes_cli/model_catalog.py @@ -356,6 +356,37 @@ def get_curated_nous_models() -> list[str] | None: return out or None +def seed_cache_from_checkout(project_root: "Path | str") -> bool: + """Overwrite the disk cache with the catalog shipped in a local checkout. + + ``hermes update`` pulls the latest repo, so the freshly-pulled + ``website/static/api/model-catalog.json`` IS the newest catalog — no + network round-trip needed. Copying it straight over the disk cache keeps + the model picker current even when the remote manifest fetch is bot-gated + or the Portal hiccups. + + Reads the shipped manifest, validates it against the schema, and writes it + to ``~/.hermes/cache/model_catalog.json`` via the same atomic writer the + network path uses. Returns ``True`` on success, ``False`` if the file is + missing, malformed, or fails validation (caller should treat a ``False`` + as non-fatal — the network fetch path still applies on the next picker + open). + """ + src = Path(project_root) / "website" / "static" / "api" / "model-catalog.json" + try: + with open(src, encoding="utf-8") as fh: + data = json.load(fh) + except (OSError, json.JSONDecodeError) as exc: + logger.debug("model catalog seed from checkout skipped (%s): %s", src, exc) + return False + if not _validate_manifest(data): + logger.debug("model catalog seed from checkout skipped: invalid manifest at %s", src) + return False + _write_disk_cache(data) + reset_cache() # drop the in-process copy so the next read picks up the seed + return True + + def reset_cache() -> None: """Clear the in-process cache. Used by tests and ``hermes model --refresh``.""" global _catalog_cache, _catalog_cache_source_mtime