diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py index 6bdfd1232e..c2bba84548 100644 --- a/hermes_cli/doctor.py +++ b/hermes_cli/doctor.py @@ -543,7 +543,10 @@ def run_doctor(args): if shutil.which("docker"): check_ok("docker", "(optional)") else: - check_warn("docker not found", "(optional)") + if _is_termux(): + check_info("Docker backend is not available inside Termux (expected on Android)") + else: + check_warn("docker not found", "(optional)") # SSH (if using ssh backend) if terminal_env == "ssh": @@ -591,11 +594,17 @@ def run_doctor(args): if agent_browser_path.exists(): check_ok("agent-browser (Node.js)", "(browser automation)") else: - check_warn("agent-browser not installed", "(run: npm install)") + if _is_termux(): + check_info("agent-browser is not installed (expected in the tested Termux path)") + check_info("Install it manually later with: npm install") + else: + check_warn("agent-browser not installed", "(run: npm install)") else: - check_warn("Node.js not found", "(optional, needed for browser tools)") if _is_termux(): + check_info("Node.js not found (browser tools are optional in the tested Termux path)") check_info("Install Node.js on Termux with: pkg install nodejs") + else: + check_warn("Node.js not found", "(optional, needed for browser tools)") # npm audit for all Node.js packages if shutil.which("npm"): diff --git a/hermes_cli/status.py b/hermes_cli/status.py index eed89885d2..a04d570131 100644 --- a/hermes_cli/status.py +++ b/hermes_cli/status.py @@ -79,6 +79,11 @@ def _effective_provider_label() -> str: return provider_label(effective) +def _is_termux() -> bool: + prefix = os.getenv("PREFIX", "") + return bool(os.getenv("TERMUX_VERSION") or "com.termux/files/usr" in prefix) + + def show_status(args): """Show status of all Hermes Agent components.""" show_all = getattr(args, 'all', False) @@ -325,7 +330,25 @@ def show_status(args): print() print(color("◆ Gateway Service", Colors.CYAN, Colors.BOLD)) - if sys.platform.startswith('linux'): + if _is_termux(): + try: + from hermes_cli.gateway import find_gateway_pids + gateway_pids = find_gateway_pids() + except Exception: + gateway_pids = [] + is_running = bool(gateway_pids) + print(f" Status: {check_mark(is_running)} {'running' if is_running else 'stopped'}") + print(" Manager: Termux / manual process") + if gateway_pids: + rendered = ", ".join(str(pid) for pid in gateway_pids[:3]) + if len(gateway_pids) > 3: + rendered += ", ..." + print(f" PID(s): {rendered}") + else: + print(" Start with: hermes gateway") + print(" Note: Android may stop background jobs when Termux is suspended") + + elif sys.platform.startswith('linux'): try: from hermes_cli.gateway import get_service_name _gw_svc = get_service_name() @@ -339,7 +362,7 @@ def show_status(args): timeout=5 ) is_active = result.stdout.strip() == "active" - except subprocess.TimeoutExpired: + except (FileNotFoundError, subprocess.TimeoutExpired): is_active = False print(f" Status: {check_mark(is_active)} {'running' if is_active else 'stopped'}") print(" Manager: systemd (user)") diff --git a/scripts/install.sh b/scripts/install.sh index 2b52b0397a..0bb091baeb 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -121,6 +121,32 @@ is_termux() { [ -n "${TERMUX_VERSION:-}" ] || [[ "${PREFIX:-}" == *"com.termux/files/usr"* ]] } +get_command_link_dir() { + if is_termux && [ -n "${PREFIX:-}" ]; then + echo "$PREFIX/bin" + else + echo "$HOME/.local/bin" + fi +} + +get_command_link_display_dir() { + if is_termux && [ -n "${PREFIX:-}" ]; then + echo '$PREFIX/bin' + else + echo '~/.local/bin' + fi +} + +get_hermes_command_path() { + local link_dir + link_dir="$(get_command_link_dir)" + if [ -x "$link_dir/hermes" ]; then + echo "$link_dir/hermes" + else + echo "hermes" + fi +} + # ============================================================================ # System detection # ============================================================================ @@ -897,15 +923,27 @@ setup_path() { return 0 fi - # Create symlink in ~/.local/bin (standard user binary location, usually on PATH) - mkdir -p "$HOME/.local/bin" - ln -sf "$HERMES_BIN" "$HOME/.local/bin/hermes" - log_success "Symlinked hermes → ~/.local/bin/hermes" + local command_link_dir + local command_link_display_dir + command_link_dir="$(get_command_link_dir)" + command_link_display_dir="$(get_command_link_display_dir)" + + # Create a user-facing shim for the hermes command. + mkdir -p "$command_link_dir" + ln -sf "$HERMES_BIN" "$command_link_dir/hermes" + log_success "Symlinked hermes → $command_link_display_dir/hermes" + + if [ "$DISTRO" = "termux" ]; then + export PATH="$command_link_dir:$PATH" + log_info "$command_link_display_dir is the native Termux command path" + log_success "hermes command ready" + return 0 + fi # Check if ~/.local/bin is on PATH; if not, add it to shell config. # Detect the user's actual login shell (not the shell running this script, # which is always bash when piped from curl). - if ! echo "$PATH" | tr ':' '\n' | grep -q "^$HOME/.local/bin$"; then + if ! echo "$PATH" | tr ':' '\n' | grep -q "^$command_link_dir$"; then SHELL_CONFIGS=() LOGIN_SHELL="$(basename "${SHELL:-/bin/bash}")" case "$LOGIN_SHELL" in @@ -951,7 +989,7 @@ setup_path() { fi # Export for current session so hermes works immediately - export PATH="$HOME/.local/bin:$PATH" + export PATH="$command_link_dir:$PATH" log_success "hermes command ready" } @@ -1149,8 +1187,7 @@ maybe_start_gateway() { read -p "Pair WhatsApp now? [Y/n] " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then - HERMES_CMD="$HOME/.local/bin/hermes" - [ ! -x "$HERMES_CMD" ] && HERMES_CMD="hermes" + HERMES_CMD="$(get_hermes_command_path)" $HERMES_CMD whatsapp || true fi else @@ -1164,16 +1201,17 @@ maybe_start_gateway() { fi echo "" - read -p "Would you like to install the gateway as a background service? [Y/n] " -n 1 -r < /dev/tty + if [ "$DISTRO" = "termux" ]; then + read -p "Would you like to start the gateway in the background? [Y/n] " -n 1 -r < /dev/tty + else + read -p "Would you like to install the gateway as a background service? [Y/n] " -n 1 -r < /dev/tty + fi echo if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then - HERMES_CMD="$HOME/.local/bin/hermes" - if [ ! -x "$HERMES_CMD" ]; then - HERMES_CMD="hermes" - fi + HERMES_CMD="$(get_hermes_command_path)" - if command -v systemctl &> /dev/null; then + if [ "$DISTRO" != "termux" ] && command -v systemctl &> /dev/null; then log_info "Installing systemd service..." if $HERMES_CMD gateway install 2>/dev/null; then log_success "Gateway service installed" @@ -1186,12 +1224,19 @@ maybe_start_gateway() { log_warn "Systemd install failed. You can start manually: hermes gateway" fi else - log_info "systemd not available — starting gateway in background..." + if [ "$DISTRO" = "termux" ]; then + log_info "Termux detected — starting gateway in best-effort background mode..." + else + log_info "systemd not available — starting gateway in background..." + fi nohup $HERMES_CMD gateway > "$HERMES_HOME/logs/gateway.log" 2>&1 & GATEWAY_PID=$! log_success "Gateway started (PID $GATEWAY_PID). Logs: ~/.hermes/logs/gateway.log" log_info "To stop: kill $GATEWAY_PID" log_info "To restart later: hermes gateway" + if [ "$DISTRO" = "termux" ]; then + log_warn "Android may stop background processes when Termux is suspended or the system reclaims resources." + fi fi else log_info "Skipped. Start the gateway later with: hermes gateway" @@ -1230,17 +1275,22 @@ print_success() { echo -e "${CYAN}─────────────────────────────────────────────────────────${NC}" echo "" - echo -e "${YELLOW}⚡ Reload your shell to use 'hermes' command:${NC}" - echo "" - LOGIN_SHELL="$(basename "${SHELL:-/bin/bash}")" - if [ "$LOGIN_SHELL" = "zsh" ]; then - echo " source ~/.zshrc" - elif [ "$LOGIN_SHELL" = "bash" ]; then - echo " source ~/.bashrc" + if [ "$DISTRO" = "termux" ]; then + echo -e "${YELLOW}⚡ 'hermes' was linked into $(get_command_link_display_dir), which is already on PATH in Termux.${NC}" + echo "" else - echo " source ~/.bashrc # or ~/.zshrc" + echo -e "${YELLOW}⚡ Reload your shell to use 'hermes' command:${NC}" + echo "" + LOGIN_SHELL="$(basename "${SHELL:-/bin/bash}")" + if [ "$LOGIN_SHELL" = "zsh" ]; then + echo " source ~/.zshrc" + elif [ "$LOGIN_SHELL" = "bash" ]; then + echo " source ~/.bashrc" + else + echo " source ~/.bashrc # or ~/.zshrc" + fi + echo "" fi - echo "" # Show Node.js warning if auto-install failed if [ "$HAS_NODE" = false ]; then diff --git a/tests/hermes_cli/test_doctor.py b/tests/hermes_cli/test_doctor.py index 1378ad32fa..eb76769099 100644 --- a/tests/hermes_cli/test_doctor.py +++ b/tests/hermes_cli/test_doctor.py @@ -223,3 +223,25 @@ class TestDoctorMemoryProviderSection: out = self._run_doctor_and_capture(monkeypatch, tmp_path, provider="mem0") assert "Memory Provider" in out assert "Built-in memory active" not in out + + +def test_run_doctor_termux_treats_docker_and_browser_warnings_as_expected(monkeypatch, tmp_path): + helper = TestDoctorMemoryProviderSection() + monkeypatch.setenv("TERMUX_VERSION", "0.118.3") + monkeypatch.setenv("PREFIX", "/data/data/com.termux/files/usr") + + real_which = doctor_mod.shutil.which + + def fake_which(cmd): + if cmd in {"docker", "node", "npm"}: + return None + return real_which(cmd) + + monkeypatch.setattr(doctor_mod.shutil, "which", fake_which) + + out = helper._run_doctor_and_capture(monkeypatch, tmp_path, provider="") + + assert "Docker backend is not available inside Termux" in out + assert "Node.js not found (browser tools are optional in the tested Termux path)" in out + assert "Install Node.js on Termux with: pkg install nodejs" in out + assert "docker not found (optional)" not in out diff --git a/tests/hermes_cli/test_status.py b/tests/hermes_cli/test_status.py index 374e57b29e..c24b72dd4c 100644 --- a/tests/hermes_cli/test_status.py +++ b/tests/hermes_cli/test_status.py @@ -12,3 +12,33 @@ def test_show_status_includes_tavily_key(monkeypatch, capsys, tmp_path): output = capsys.readouterr().out assert "Tavily" in output assert "tvly...cdef" in output + + +def test_show_status_termux_gateway_section_skips_systemctl(monkeypatch, capsys, tmp_path): + from hermes_cli import status as status_mod + import hermes_cli.auth as auth_mod + import hermes_cli.gateway as gateway_mod + + monkeypatch.setenv("TERMUX_VERSION", "0.118.3") + monkeypatch.setenv("PREFIX", "/data/data/com.termux/files/usr") + monkeypatch.setattr(status_mod, "get_env_path", lambda: tmp_path / ".env", raising=False) + monkeypatch.setattr(status_mod, "get_hermes_home", lambda: tmp_path, raising=False) + monkeypatch.setattr(status_mod, "load_config", lambda: {"model": "gpt-5.4"}, raising=False) + monkeypatch.setattr(status_mod, "resolve_requested_provider", lambda requested=None: "openai-codex", raising=False) + monkeypatch.setattr(status_mod, "resolve_provider", lambda requested=None, **kwargs: "openai-codex", raising=False) + monkeypatch.setattr(status_mod, "provider_label", lambda provider: "OpenAI Codex", raising=False) + monkeypatch.setattr(auth_mod, "get_nous_auth_status", lambda: {}, raising=False) + monkeypatch.setattr(auth_mod, "get_codex_auth_status", lambda: {}, raising=False) + monkeypatch.setattr(gateway_mod, "find_gateway_pids", lambda exclude_pids=None: [], raising=False) + + def _unexpected_systemctl(*args, **kwargs): + raise AssertionError("systemctl should not be called in the Termux status view") + + monkeypatch.setattr(status_mod.subprocess, "run", _unexpected_systemctl) + + status_mod.show_status(SimpleNamespace(all=False, deep=False)) + + output = capsys.readouterr().out + assert "Manager: Termux / manual process" in output + assert "Start with: hermes gateway" in output + assert "systemd (user)" not in output diff --git a/website/docs/getting-started/termux.md b/website/docs/getting-started/termux.md index e980b507fe..1ad71e5313 100644 --- a/website/docs/getting-started/termux.md +++ b/website/docs/getting-started/termux.md @@ -51,6 +51,7 @@ On Termux, the installer automatically: - uses `pkg` for system packages - creates the venv with `python -m venv` - installs `.[termux]` with `pip` +- links `hermes` into `$PREFIX/bin` so it stays on your Termux PATH - skips the untested browser / WhatsApp bootstrap If you want the explicit commands or need to debug a failed install, use the manual path below. @@ -110,14 +111,22 @@ If you only want the minimal core agent, this also works: python -m pip install -e '.' -c constraints-termux.txt ``` -### 5. Verify the install +### 5. Put `hermes` on your Termux PATH + +```bash +ln -sf "$PWD/venv/bin/hermes" "$PREFIX/bin/hermes" +``` + +`$PREFIX/bin` is already on PATH in Termux, so this makes the `hermes` command persist across new shells without re-activating the venv every time. + +### 6. Verify the install ```bash hermes version hermes doctor ``` -### 6. Start Hermes +### 7. Start Hermes ```bash hermes