Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ def _try_termux_ultrafast_version() -> bool:
from hermes_cli.subcommands.console import build_console_parser
from hermes_cli.subcommands.update import build_update_parser
from hermes_cli.subcommands.uninstall import build_uninstall_parser
from hermes_cli.subcommands.dashboard import build_dashboard_parser
from hermes_cli.subcommands.dashboard import build_dashboard_parser, build_serve_parser
from hermes_cli.subcommands.gui import build_gui_parser
from hermes_cli.subcommands.logs import build_logs_parser
from hermes_cli.subcommands.prompt_size import build_prompt_size_parser
Expand Down Expand Up @@ -12612,6 +12612,47 @@ def _set_chat_arg_defaults(args) -> None:
setattr(args, attr, default)


def _try_fast_serve_launch() -> bool:
"""Dispatch an unambiguous built-in ``serve`` without the full CLI tree.

Desktop launches this exact command on every cold start. Building parsers
for unrelated Hermes commands performs thousands of filesystem-backed
translation lookups on Windows even though none of those commands are
usable in this process. Unknown or globally-scoped arguments fall back to
normal parsing so compatibility and error reporting remain unchanged.
"""
if os.environ.get("HERMES_DISABLE_FAST_SERVE_LAUNCH") == "1":
return False

argv = sys.argv[1:]
if not argv or argv[0] != "serve" or "-h" in argv or "--help" in argv:
return False

# Container routing is top-level policy and must run before host dispatch.
try:
from hermes_cli.config import get_container_exec_info

if get_container_exec_info():
return False
except Exception:
return False

parser = build_serve_parser(
cmd_dashboard=cmd_dashboard,
add_help=False,
exit_on_error=False,
)
try:
args, unknown = parser.parse_known_args(argv[1:])
except (argparse.ArgumentError, ValueError):
return False
if unknown:
return False

cmd_dashboard(args)
return True


def _try_fast_chat_launch() -> bool:
"""Fast path for unambiguous interactive chat launches (all hosts).

Expand Down Expand Up @@ -13159,6 +13200,8 @@ def main():
return
if _try_termux_fast_cli_launch():
return
if _try_fast_serve_launch():
return
if _try_fast_chat_launch():
return

Expand Down
81 changes: 55 additions & 26 deletions hermes_cli/subcommands/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,60 @@ def _add_server_runtime_args(parser) -> None:
)


def _configure_serve_parser(parser, *, cmd_dashboard: Callable) -> None:
"""Attach the canonical ``serve`` arguments to *parser*.

Kept separate from the full subcommand tree so Desktop's hot path can parse
only the command it launches. Both callers use this exact function, keeping
the lean parser and normal CLI semantics in lockstep.
"""
_add_server_runtime_args(parser)
# Accepted but redundant: ``serve`` is always headless. Kept so callers
# using the legacy flag do not trip an argparse error.
parser.add_argument("--no-open", action="store_true", help=argparse.SUPPRESS)
parser.add_argument(
"--ssh-session-token-file",
dest="ssh_session_token_file",
metavar="PATH",
default=None,
help="Read a one-shot Desktop SSH session token from PATH",
)
parser.add_argument(
"--ssh-owner-nonce",
dest="ssh_owner_nonce",
metavar="NONCE",
default=None,
help="Identify a Desktop-owned SSH backend process",
)
parser.set_defaults(
func=cmd_dashboard,
no_open=True,
headless_backend=True,
command="serve",
)


def build_serve_parser(
*,
cmd_dashboard: Callable,
add_help: bool = True,
exit_on_error: bool = True,
) -> argparse.ArgumentParser:
"""Build the standalone parser used by the lean ``serve`` dispatch path."""
parser = argparse.ArgumentParser(
prog="hermes serve",
description=(
"Run the Hermes backend server - the JSON-RPC/WebSocket gateway the "
"desktop app and remote clients connect to. Headless: it never opens "
"a browser UI."
),
add_help=add_help,
exit_on_error=exit_on_error,
)
_configure_serve_parser(parser, cmd_dashboard=cmd_dashboard)
return parser


def build_dashboard_parser(
subparsers, *, cmd_dashboard: Callable, cmd_dashboard_register: Callable
) -> None:
Expand Down Expand Up @@ -142,32 +196,7 @@ def build_dashboard_parser(
"a browser UI."
),
)
_add_server_runtime_args(serve_parser)
# Accepted but redundant: `serve` is always headless (see set_defaults
# below). Kept so callers that pass the legacy `--no-open` flag (e.g. the
# desktop backend spawn) don't trip "unrecognized arguments".
serve_parser.add_argument(
"--no-open", action="store_true", help=argparse.SUPPRESS
)
serve_parser.add_argument(
"--ssh-session-token-file",
dest="ssh_session_token_file",
metavar="PATH",
default=None,
help="Read a one-shot Desktop SSH session token from PATH",
)
serve_parser.add_argument(
"--ssh-owner-nonce",
dest="ssh_owner_nonce",
metavar="NONCE",
default=None,
help="Identify a Desktop-owned SSH backend process",
)
# `headless_backend` marks the lean path: desktop/remote clients speak pure
# JSON-RPC/WS, so `serve` skips the web UI build AND never serves the SPA
# (cmd_dashboard exports HERMES_SERVE_HEADLESS=1). `dashboard` leaves it
# unset and serves the browser UI as before.
serve_parser.set_defaults(func=cmd_dashboard, no_open=True, headless_backend=True)
_configure_serve_parser(serve_parser, cmd_dashboard=cmd_dashboard)

# `hermes dashboard register` β€” register a self-hosted dashboard OAuth
# client with Nous Portal and write the client_id into ~/.hermes/.env.
Expand Down
89 changes: 89 additions & 0 deletions tests/hermes_cli/test_fast_serve_launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from __future__ import annotations

import argparse
import sys

import hermes_cli.config as config_mod
import hermes_cli.main as main_mod
from hermes_cli.subcommands.dashboard import build_dashboard_parser, build_serve_parser


def _capture(_args) -> None:
return None


def test_standalone_serve_parser_matches_full_subcommand_parser() -> None:
root = argparse.ArgumentParser()
subparsers = root.add_subparsers(dest="command")
build_dashboard_parser(
subparsers,
cmd_dashboard=_capture,
cmd_dashboard_register=_capture,
)
lean = build_serve_parser(cmd_dashboard=_capture)

argv = [
"--host",
"127.0.0.1",
"--port",
"0",
"--no-open",
"--ssh-session-token-file",
"token.txt",
"--ssh-owner-nonce",
"0123456789abcdef",
]

assert vars(lean.parse_args(argv)) == vars(root.parse_args(["serve", *argv]))


def test_fast_serve_launch_dispatches_canonical_arguments(monkeypatch) -> None:
captured = []
monkeypatch.setattr(config_mod, "get_container_exec_info", lambda: None)
monkeypatch.setattr(main_mod, "cmd_dashboard", captured.append)
monkeypatch.setattr(
sys,
"argv",
[
"hermes",
"serve",
"--host",
"127.0.0.1",
"--port",
"0",
"--ssh-owner-nonce",
"0123456789abcdef",
],
)

assert main_mod._try_fast_serve_launch() is True
assert len(captured) == 1
assert captured[0].command == "serve"
assert captured[0].headless_backend is True
assert captured[0].no_open is True
assert captured[0].host == "127.0.0.1"
assert captured[0].port == 0
assert captured[0].ssh_owner_nonce == "0123456789abcdef"


def test_fast_serve_launch_falls_back_for_unknown_arguments(monkeypatch) -> None:
monkeypatch.setattr(config_mod, "get_container_exec_info", lambda: None)
monkeypatch.setattr(sys, "argv", ["hermes", "serve", "--future-flag"])

assert main_mod._try_fast_serve_launch() is False


def test_fast_serve_launch_preserves_container_routing(monkeypatch) -> None:
monkeypatch.setattr(config_mod, "get_container_exec_info", lambda: {"name": "managed"})
monkeypatch.setattr(sys, "argv", ["hermes", "serve"])

assert main_mod._try_fast_serve_launch() is False


def test_fast_serve_launch_preserves_help_and_opt_out(monkeypatch) -> None:
monkeypatch.setattr(sys, "argv", ["hermes", "serve", "--help"])
assert main_mod._try_fast_serve_launch() is False

monkeypatch.setenv("HERMES_DISABLE_FAST_SERVE_LAUNCH", "1")
monkeypatch.setattr(sys, "argv", ["hermes", "serve"])
assert main_mod._try_fast_serve_launch() is False
Loading