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")).