From de59d91add144937933b532938cd205c94235135 Mon Sep 17 00:00:00 2001 From: teknium1 Date: Mon, 2 Mar 2026 22:03:29 -0800 Subject: [PATCH] feat: Windows native support via Git Bash - Add scripts/install.cmd batch wrapper for CMD users (delegates to install.ps1) - Add _find_shell() in local.py: detects Git Bash on Windows via HERMES_GIT_BASH_PATH env var, shutil.which, or common install paths (same pattern as Claude Code's CLAUDE_CODE_GIT_BASH_PATH) - Use _find_shell() in process_registry.py for background processes - Fix hermes_cli/gateway.py: use wmic instead of ps aux on Windows, skip SIGKILL (doesn't exist on Windows), fix venv path (Scripts/python.exe vs bin/python) - Update README with three install commands (Linux/macOS, PowerShell, CMD) and Windows native documentation Requires Git for Windows, which bundles bash.exe. The terminal tool transparently uses Git Bash for shell commands regardless of whether the user launched hermes from PowerShell or CMD. --- README.md | 17 +++++--- hermes_cli/gateway.py | 81 ++++++++++++++++++++++++------------- scripts/install.cmd | 28 +++++++++++++ tools/environments/local.py | 39 +++++++++++++++++- tools/process_registry.py | 5 ++- 5 files changed, 133 insertions(+), 37 deletions(-) create mode 100644 scripts/install.cmd diff --git a/README.md b/README.md index c6891b835..8d101a2eb 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Built by [Nous Research](https://nousresearch.com). Under the hood, the same arc ## Quick Install -**Linux/macOS:** +**Linux / macOS / WSL:** ```bash curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash ``` @@ -42,18 +42,25 @@ curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scri irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1 | iex ``` +**Windows (CMD):** +```cmd +curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.cmd -o install.cmd && install.cmd && del install.cmd +``` + +> **Windows note:** [Git for Windows](https://git-scm.com/download/win) is required. Hermes uses Git Bash internally for shell commands. + The installer will: - Install [uv](https://docs.astral.sh/uv/) (fast Python package manager) if not present - Install Python 3.11 via uv if not already available (no sudo needed) - Clone to `~/.hermes/hermes-agent` (with submodules: mini-swe-agent, tinker-atropos) - Create a virtual environment with Python 3.11 - Install all dependencies and submodule packages -- Symlink `hermes` into `~/.local/bin` so it works globally (no venv activation needed) +- Set up the `hermes` command globally (no venv activation needed) - Run the interactive setup wizard After installation, reload your shell and run: ```bash -source ~/.bashrc # or: source ~/.zshrc +source ~/.bashrc # or: source ~/.zshrc (Windows: restart your terminal) hermes setup # Configure API keys (if you skipped during install) hermes # Start chatting! ``` @@ -1237,8 +1244,8 @@ brew install git brew install ripgrep node ``` -**Windows (WSL recommended):** -Use the [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/install) and follow the Ubuntu instructions above. Alternatively, use the PowerShell quick-install script at the top of this README. +**Windows (native):** +Hermes runs natively on Windows using [Git for Windows](https://git-scm.com/download/win) (which provides Git Bash for shell commands). Install Git for Windows first, then use the PowerShell or CMD quick-install command at the top of this README. WSL also works — follow the Ubuntu instructions above. diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index 30bd85652..525950e9a 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -21,39 +21,59 @@ PROJECT_ROOT = Path(__file__).parent.parent.resolve() def find_gateway_pids() -> list: """Find PIDs of running gateway processes.""" pids = [] + patterns = [ + "hermes_cli.main gateway", + "hermes gateway", + "gateway/run.py", + ] + try: - # Look for gateway processes with multiple patterns - patterns = [ - "hermes_cli.main gateway", - "hermes gateway", - "gateway/run.py", - ] - - result = subprocess.run( - ["ps", "aux"], - capture_output=True, - text=True - ) - - for line in result.stdout.split('\n'): - # Skip grep and current process - if 'grep' in line or str(os.getpid()) in line: - continue - - for pattern in patterns: - if pattern in line: - parts = line.split() - if len(parts) > 1: + if is_windows(): + # Windows: use wmic to search command lines + result = subprocess.run( + ["wmic", "process", "get", "ProcessId,CommandLine", "/FORMAT:LIST"], + capture_output=True, text=True + ) + # Parse WMIC LIST output: blocks of "CommandLine=...\nProcessId=...\n" + current_cmd = "" + for line in result.stdout.split('\n'): + line = line.strip() + if line.startswith("CommandLine="): + current_cmd = line[len("CommandLine="):] + elif line.startswith("ProcessId="): + pid_str = line[len("ProcessId="):] + if any(p in current_cmd for p in patterns): try: - pid = int(parts[1]) - if pid not in pids: + pid = int(pid_str) + if pid != os.getpid() and pid not in pids: pids.append(pid) except ValueError: - continue - break + pass + current_cmd = "" + else: + result = subprocess.run( + ["ps", "aux"], + capture_output=True, + text=True + ) + for line in result.stdout.split('\n'): + # Skip grep and current process + if 'grep' in line or str(os.getpid()) in line: + continue + for pattern in patterns: + if pattern in line: + parts = line.split() + if len(parts) > 1: + try: + pid = int(parts[1]) + if pid not in pids: + pids.append(pid) + except ValueError: + continue + break except Exception: pass - + return pids @@ -64,7 +84,7 @@ def kill_gateway_processes(force: bool = False) -> int: for pid in pids: try: - if force: + if force and not is_windows(): os.kill(pid, signal.SIGKILL) else: os.kill(pid, signal.SIGTERM) @@ -102,7 +122,10 @@ def get_launchd_plist_path() -> Path: return Path.home() / "Library" / "LaunchAgents" / "ai.hermes.gateway.plist" def get_python_path() -> str: - venv_python = PROJECT_ROOT / "venv" / "bin" / "python" + if is_windows(): + venv_python = PROJECT_ROOT / "venv" / "Scripts" / "python.exe" + else: + venv_python = PROJECT_ROOT / "venv" / "bin" / "python" if venv_python.exists(): return str(venv_python) return sys.executable diff --git a/scripts/install.cmd b/scripts/install.cmd new file mode 100644 index 000000000..7c4cf7ef6 --- /dev/null +++ b/scripts/install.cmd @@ -0,0 +1,28 @@ +@echo off +REM ============================================================================ +REM Hermes Agent Installer for Windows (CMD wrapper) +REM ============================================================================ +REM This batch file launches the PowerShell installer for users running CMD. +REM +REM Usage: +REM curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.cmd -o install.cmd && install.cmd && del install.cmd +REM +REM Or if you're already in PowerShell, use the direct command instead: +REM irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1 | iex +REM ============================================================================ + +echo. +echo Hermes Agent Installer +echo Launching PowerShell installer... +echo. + +powershell -ExecutionPolicy ByPass -NoProfile -Command "irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1 | iex" + +if %ERRORLEVEL% NEQ 0 ( + echo. + echo Installation failed. Please try running PowerShell directly: + echo powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1 | iex" + echo. + pause + exit /b 1 +) diff --git a/tools/environments/local.py b/tools/environments/local.py index 3598509b0..702cca49c 100644 --- a/tools/environments/local.py +++ b/tools/environments/local.py @@ -12,6 +12,43 @@ _IS_WINDOWS = platform.system() == "Windows" from tools.environments.base import BaseEnvironment + +def _find_shell() -> str: + """Find the best shell for command execution. + + On Unix: uses $SHELL, falls back to bash. + On Windows: uses Git Bash (bundled with Git for Windows). + Raises RuntimeError if no suitable shell is found on Windows. + """ + if not _IS_WINDOWS: + return os.environ.get("SHELL") or shutil.which("bash") or "/bin/bash" + + # Windows: look for Git Bash (installed with Git for Windows). + # Allow override via env var (same pattern as Claude Code). + custom = os.environ.get("HERMES_GIT_BASH_PATH") + if custom and os.path.isfile(custom): + return custom + + # shutil.which finds bash.exe if Git\bin is on PATH + found = shutil.which("bash") + if found: + return found + + # Check common Git for Windows install locations + for candidate in ( + os.path.join(os.environ.get("ProgramFiles", r"C:\Program Files"), "Git", "bin", "bash.exe"), + os.path.join(os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)"), "Git", "bin", "bash.exe"), + os.path.join(os.environ.get("LOCALAPPDATA", ""), "Programs", "Git", "bin", "bash.exe"), + ): + if candidate and os.path.isfile(candidate): + return candidate + + raise RuntimeError( + "Git Bash not found. Hermes Agent requires Git for Windows on Windows.\n" + "Install it from: https://git-scm.com/download/win\n" + "Or set HERMES_GIT_BASH_PATH to your bash.exe location." + ) + # Noise lines emitted by interactive shells when stdin is not a terminal. # Filtered from output to keep tool results clean. _SHELL_NOISE_SUBSTRINGS = ( @@ -66,7 +103,7 @@ class LocalEnvironment(BaseEnvironment): # tools like nvm, pyenv, and cargo install their init scripts. # -l alone isn't enough: .profile sources .bashrc, but the guard # returns early because the shell isn't interactive. - user_shell = os.environ.get("SHELL") or shutil.which("bash") or "/bin/bash" + user_shell = _find_shell() proc = subprocess.Popen( [user_shell, "-lic", exec_command], text=True, diff --git a/tools/process_registry.py b/tools/process_registry.py index 4653f3ff6..ecf25c08d 100644 --- a/tools/process_registry.py +++ b/tools/process_registry.py @@ -42,6 +42,7 @@ import time import uuid _IS_WINDOWS = platform.system() == "Windows" +from tools.environments.local import _find_shell from dataclasses import dataclass, field from pathlib import Path from typing import Any, Dict, List, Optional @@ -148,7 +149,7 @@ class ProcessRegistry: # Try PTY mode for interactive CLI tools try: import ptyprocess - user_shell = os.environ.get("SHELL") or shutil.which("bash") or "/bin/bash" + user_shell = _find_shell() pty_env = os.environ | (env_vars or {}) pty_env["PYTHONUNBUFFERED"] = "1" pty_proc = ptyprocess.PtyProcess.spawn( @@ -186,7 +187,7 @@ class ProcessRegistry: # Standard Popen path (non-PTY or PTY fallback) # Use the user's login shell for consistency with LocalEnvironment -- # ensures rc files are sourced and user tools are available. - user_shell = os.environ.get("SHELL") or shutil.which("bash") or "/bin/bash" + user_shell = _find_shell() # Force unbuffered output for Python scripts so progress is visible # during background execution (libraries like tqdm/datasets buffer when # stdout is a pipe, hiding output from process(action="poll")).