Merge branch 'main' of github.com:NousResearch/hermes-agent into bb/gui

This commit is contained in:
Brooklyn Nicholson
2026-05-04 12:47:53 -05:00
182 changed files with 9843 additions and 974 deletions
+64 -13
View File
@@ -114,6 +114,16 @@ def _apply_profile_override() -> None:
consume = 1
break
# 1b. Reject values that can't be valid profile names (e.g. pytest's
# "-p no:xdist" would be misread as profile "no:xdist" otherwise).
# Mirrors hermes_cli.profiles._PROFILE_ID_RE so we never call
# resolve_profile_env() with a value it must reject + sys.exit on.
if profile_name is not None and consume == 2:
import re as _re
if not _re.match(r"^[a-z0-9][a-z0-9_-]{0,63}$", profile_name):
profile_name = None
consume = 0
# 1.5 If HERMES_HOME is already set and no explicit flag was given, trust it.
# This lets child processes (relaunch, subprocess) inherit the parent's
# profile choice without having to pass --profile again.
@@ -837,7 +847,17 @@ def _print_tui_exit_summary(session_id: Optional[str], active_session_file: Opti
)
_NPM_LOCK_RUNTIME_KEYS = frozenset({"ideallyInert"})
_NPM_LOCK_RUNTIME_KEYS = frozenset({"ideallyInert", "peer"})
"""Lockfile fields npm writes non-deterministically at install time.
``ideallyInert`` is npm's runtime annotation for packages it skipped installing
(per-platform opt-outs). ``peer`` is dropped from the hidden ``.package-lock.json``
on dev-dependencies that are *also* declared as peers the canonical
``package-lock.json`` records the dual role, but npm 9's actualized tree strips
it. Neither key represents a real skew between what was declared and what was
installed, so we exclude them from the comparison in :func:`_tui_need_npm_install`
to avoid false-positive reinstalls on every launch.
"""
def _tui_need_npm_install(root: Path) -> bool:
@@ -1042,17 +1062,21 @@ def _make_tui_argv(tui_dir: Path, tui_dev: bool) -> tuple[list[str], Path]:
if _tui_need_npm_install(tui_dir):
if not os.environ.get("HERMES_QUIET"):
print("Installing TUI dependencies…")
# Capture stdout as well as stderr — some npm errors (notably EACCES on a
# root-owned node_modules in containers) are emitted on stdout, and a
# bare "npm install failed." with no preview defeats debugging. We keep
# the failure-only print path so a successful install stays silent.
result = subprocess.run(
[npm, "install", "--silent", "--no-fund", "--no-audit", "--progress=false"],
cwd=str(tui_dir),
stdout=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
env={**os.environ, "CI": "1"},
)
if result.returncode != 0:
err = (result.stderr or "").strip()
preview = "\n".join(err.splitlines()[-30:])
combined = f"{result.stdout or ''}\n{result.stderr or ''}".strip()
preview = "\n".join(combined.splitlines()[-30:])
print("npm install failed.")
if preview:
print(preview)
@@ -3399,10 +3423,10 @@ def _model_flow_named_custom(config, provider_info):
print()
print("Fetching available models...")
models = fetch_api_models(
api_key, base_url, timeout=8.0,
api_mode=api_mode or None,
)
fetch_kwargs = {"timeout": 8.0}
if api_mode:
fetch_kwargs["api_mode"] = api_mode
models = fetch_api_models(api_key, base_url, **fetch_kwargs)
if models:
default_idx = 0
@@ -6477,13 +6501,29 @@ def _cmd_update_check():
if sys.platform == "win32":
git_cmd = ["git", "-c", "windows.appendAtomically=false"]
print("→ Fetching from origin...")
# Fetch both origin and upstream; prefer upstream as the canonical reference
print("→ Fetching from upstream...")
fetch_result = subprocess.run(
git_cmd + ["fetch", "origin"],
git_cmd + ["fetch", "upstream"],
cwd=PROJECT_ROOT,
capture_output=True,
text=True,
)
if fetch_result.returncode != 0:
# Fallback to origin if upstream doesn't exist
print("→ Fetching from origin...")
fetch_result = subprocess.run(
git_cmd + ["fetch", "origin"],
cwd=PROJECT_ROOT,
capture_output=True,
text=True,
)
upstream_exists = False
compare_branch = "origin/main"
else:
upstream_exists = True
compare_branch = "upstream/main"
if fetch_result.returncode != 0:
stderr = fetch_result.stderr.strip()
if "Could not resolve host" in stderr or "unable to access" in stderr:
@@ -6491,13 +6531,13 @@ def _cmd_update_check():
elif "Authentication failed" in stderr or "could not read Username" in stderr:
print("✗ Authentication failed — check your git credentials or SSH key.")
else:
print("✗ Failed to fetch from origin.")
print("✗ Failed to fetch.")
if stderr:
print(f" {stderr.splitlines()[0]}")
sys.exit(1)
rev_result = subprocess.run(
git_cmd + ["rev-list", "HEAD..origin/main", "--count"],
git_cmd + ["rev-list", f"HEAD..{compare_branch}", "--count"],
cwd=PROJECT_ROOT,
capture_output=True,
text=True,
@@ -6509,7 +6549,7 @@ def _cmd_update_check():
print("✓ Already up to date.")
else:
commits_word = "commit" if behind == 1 else "commits"
print(f"⚕ Update available: {behind} {commits_word} behind origin/main.")
print(f"⚕ Update available: {behind} {commits_word} behind {compare_branch}.")
from hermes_cli.config import recommended_update_command
print(f" Run '{recommended_update_command()}' to install.")
@@ -8897,6 +8937,7 @@ Examples:
hermes debug share --lines 500 Include more log lines
hermes debug share --expire 30 Keep paste for 30 days
hermes debug share --local Print report locally (no upload)
hermes debug share --no-redact Disable upload-time secret redaction
hermes debug delete <url> Delete a previously uploaded paste
""",
)
@@ -8922,6 +8963,16 @@ Examples:
action="store_true",
help="Print the report locally instead of uploading",
)
share_parser.add_argument(
"--no-redact",
action="store_true",
help=(
"Disable upload-time secret redaction (default: redact). Logs "
"are normally run through agent.redact.redact_sensitive_text "
"with force=True before upload so credentials are not leaked "
"into the public paste service."
),
)
delete_parser = debug_sub.add_parser(
"delete",
help="Delete a paste uploaded by 'hermes debug share'",