mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
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.
This commit is contained in:
parent
68cc81a74d
commit
de59d91add
5 changed files with 133 additions and 37 deletions
17
README.md
17
README.md
|
|
@ -32,7 +32,7 @@ Built by [Nous Research](https://nousresearch.com). Under the hood, the same arc
|
||||||
|
|
||||||
## Quick Install
|
## Quick Install
|
||||||
|
|
||||||
**Linux/macOS:**
|
**Linux / macOS / WSL:**
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | 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
|
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:
|
The installer will:
|
||||||
- Install [uv](https://docs.astral.sh/uv/) (fast Python package manager) if not present
|
- 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)
|
- 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)
|
- Clone to `~/.hermes/hermes-agent` (with submodules: mini-swe-agent, tinker-atropos)
|
||||||
- Create a virtual environment with Python 3.11
|
- Create a virtual environment with Python 3.11
|
||||||
- Install all dependencies and submodule packages
|
- 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
|
- Run the interactive setup wizard
|
||||||
|
|
||||||
After installation, reload your shell and run:
|
After installation, reload your shell and run:
|
||||||
```bash
|
```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 setup # Configure API keys (if you skipped during install)
|
||||||
hermes # Start chatting!
|
hermes # Start chatting!
|
||||||
```
|
```
|
||||||
|
|
@ -1237,8 +1244,8 @@ brew install git
|
||||||
brew install ripgrep node
|
brew install ripgrep node
|
||||||
```
|
```
|
||||||
|
|
||||||
**Windows (WSL recommended):**
|
**Windows (native):**
|
||||||
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.
|
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.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,36 +21,56 @@ PROJECT_ROOT = Path(__file__).parent.parent.resolve()
|
||||||
def find_gateway_pids() -> list:
|
def find_gateway_pids() -> list:
|
||||||
"""Find PIDs of running gateway processes."""
|
"""Find PIDs of running gateway processes."""
|
||||||
pids = []
|
pids = []
|
||||||
|
patterns = [
|
||||||
|
"hermes_cli.main gateway",
|
||||||
|
"hermes gateway",
|
||||||
|
"gateway/run.py",
|
||||||
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Look for gateway processes with multiple patterns
|
if is_windows():
|
||||||
patterns = [
|
# Windows: use wmic to search command lines
|
||||||
"hermes_cli.main gateway",
|
result = subprocess.run(
|
||||||
"hermes gateway",
|
["wmic", "process", "get", "ProcessId,CommandLine", "/FORMAT:LIST"],
|
||||||
"gateway/run.py",
|
capture_output=True, text=True
|
||||||
]
|
)
|
||||||
|
# Parse WMIC LIST output: blocks of "CommandLine=...\nProcessId=...\n"
|
||||||
result = subprocess.run(
|
current_cmd = ""
|
||||||
["ps", "aux"],
|
for line in result.stdout.split('\n'):
|
||||||
capture_output=True,
|
line = line.strip()
|
||||||
text=True
|
if line.startswith("CommandLine="):
|
||||||
)
|
current_cmd = line[len("CommandLine="):]
|
||||||
|
elif line.startswith("ProcessId="):
|
||||||
for line in result.stdout.split('\n'):
|
pid_str = line[len("ProcessId="):]
|
||||||
# Skip grep and current process
|
if any(p in current_cmd for p in patterns):
|
||||||
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:
|
try:
|
||||||
pid = int(parts[1])
|
pid = int(pid_str)
|
||||||
if pid not in pids:
|
if pid != os.getpid() and pid not in pids:
|
||||||
pids.append(pid)
|
pids.append(pid)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
pass
|
||||||
break
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -64,7 +84,7 @@ def kill_gateway_processes(force: bool = False) -> int:
|
||||||
|
|
||||||
for pid in pids:
|
for pid in pids:
|
||||||
try:
|
try:
|
||||||
if force:
|
if force and not is_windows():
|
||||||
os.kill(pid, signal.SIGKILL)
|
os.kill(pid, signal.SIGKILL)
|
||||||
else:
|
else:
|
||||||
os.kill(pid, signal.SIGTERM)
|
os.kill(pid, signal.SIGTERM)
|
||||||
|
|
@ -102,7 +122,10 @@ def get_launchd_plist_path() -> Path:
|
||||||
return Path.home() / "Library" / "LaunchAgents" / "ai.hermes.gateway.plist"
|
return Path.home() / "Library" / "LaunchAgents" / "ai.hermes.gateway.plist"
|
||||||
|
|
||||||
def get_python_path() -> str:
|
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():
|
if venv_python.exists():
|
||||||
return str(venv_python)
|
return str(venv_python)
|
||||||
return sys.executable
|
return sys.executable
|
||||||
|
|
|
||||||
28
scripts/install.cmd
Normal file
28
scripts/install.cmd
Normal file
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -12,6 +12,43 @@ _IS_WINDOWS = platform.system() == "Windows"
|
||||||
|
|
||||||
from tools.environments.base import BaseEnvironment
|
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.
|
# Noise lines emitted by interactive shells when stdin is not a terminal.
|
||||||
# Filtered from output to keep tool results clean.
|
# Filtered from output to keep tool results clean.
|
||||||
_SHELL_NOISE_SUBSTRINGS = (
|
_SHELL_NOISE_SUBSTRINGS = (
|
||||||
|
|
@ -66,7 +103,7 @@ class LocalEnvironment(BaseEnvironment):
|
||||||
# tools like nvm, pyenv, and cargo install their init scripts.
|
# tools like nvm, pyenv, and cargo install their init scripts.
|
||||||
# -l alone isn't enough: .profile sources .bashrc, but the guard
|
# -l alone isn't enough: .profile sources .bashrc, but the guard
|
||||||
# returns early because the shell isn't interactive.
|
# 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(
|
proc = subprocess.Popen(
|
||||||
[user_shell, "-lic", exec_command],
|
[user_shell, "-lic", exec_command],
|
||||||
text=True,
|
text=True,
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
_IS_WINDOWS = platform.system() == "Windows"
|
_IS_WINDOWS = platform.system() == "Windows"
|
||||||
|
from tools.environments.local import _find_shell
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
@ -148,7 +149,7 @@ class ProcessRegistry:
|
||||||
# Try PTY mode for interactive CLI tools
|
# Try PTY mode for interactive CLI tools
|
||||||
try:
|
try:
|
||||||
import ptyprocess
|
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 = os.environ | (env_vars or {})
|
||||||
pty_env["PYTHONUNBUFFERED"] = "1"
|
pty_env["PYTHONUNBUFFERED"] = "1"
|
||||||
pty_proc = ptyprocess.PtyProcess.spawn(
|
pty_proc = ptyprocess.PtyProcess.spawn(
|
||||||
|
|
@ -186,7 +187,7 @@ class ProcessRegistry:
|
||||||
# Standard Popen path (non-PTY or PTY fallback)
|
# Standard Popen path (non-PTY or PTY fallback)
|
||||||
# Use the user's login shell for consistency with LocalEnvironment --
|
# Use the user's login shell for consistency with LocalEnvironment --
|
||||||
# ensures rc files are sourced and user tools are available.
|
# 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
|
# Force unbuffered output for Python scripts so progress is visible
|
||||||
# during background execution (libraries like tqdm/datasets buffer when
|
# during background execution (libraries like tqdm/datasets buffer when
|
||||||
# stdout is a pipe, hiding output from process(action="poll")).
|
# stdout is a pipe, hiding output from process(action="poll")).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue