Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
a981e3e89d fix: ship bundled skill scripts executable (#953)
All checks were successful
Lint / lint (pull_request) Successful in 29s
2026-04-22 10:45:10 -04:00
16 changed files with 74 additions and 112 deletions

33
cli.py
View File

@@ -6852,12 +6852,11 @@ class HermesCLI:
self._voice_stop_and_transcribe()
# Audio cue: single beep BEFORE starting stream (avoid CoreAudio conflict)
if self._voice_beeps_enabled():
try:
from tools.voice_mode import play_beep
play_beep(frequency=880, count=1)
except Exception:
pass
try:
from tools.voice_mode import play_beep
play_beep(frequency=880, count=1)
except Exception:
pass
try:
self._voice_recorder.start(on_silence_stop=_on_silence)
@@ -6905,12 +6904,11 @@ class HermesCLI:
wav_path = self._voice_recorder.stop()
# Audio cue: double beep after stream stopped (no CoreAudio conflict)
if self._voice_beeps_enabled():
try:
from tools.voice_mode import play_beep
play_beep(frequency=660, count=2)
except Exception:
pass
try:
from tools.voice_mode import play_beep
play_beep(frequency=660, count=2)
except Exception:
pass
if wav_path is None:
_cprint(f"{_DIM}No speech detected.{_RST}")
@@ -7061,17 +7059,6 @@ class HermesCLI:
_cprint(f"Unknown voice subcommand: {subcommand}")
_cprint("Usage: /voice [on|off|tts|status]")
def _voice_beeps_enabled(self) -> bool:
"""Return whether CLI voice mode should play record start/stop beeps."""
try:
from hermes_cli.config import load_config
voice_cfg = load_config().get("voice", {})
if isinstance(voice_cfg, dict):
return bool(voice_cfg.get("beep_enabled", True))
except Exception:
pass
return True
def _enable_voice_mode(self):
"""Enable voice mode after checking requirements."""
if self._voice_mode:

0
skills/creative/excalidraw/scripts/upload.py Normal file → Executable file
View File

0
skills/leisure/find-nearby/scripts/find_nearby.py Normal file → Executable file
View File

View File

View File

0
skills/productivity/google-workspace/scripts/setup.py Normal file → Executable file
View File

View File

View File

0
skills/red-teaming/godmode/scripts/auto_jailbreak.py Normal file → Executable file
View File

0
skills/red-teaming/godmode/scripts/godmode_race.py Normal file → Executable file
View File

0
skills/red-teaming/godmode/scripts/parseltongue.py Normal file → Executable file
View File

0
skills/research/arxiv/scripts/search_arxiv.py Normal file → Executable file
View File

0
skills/research/polymarket/scripts/polymarket.py Normal file → Executable file
View File

View File

@@ -0,0 +1,63 @@
"""Regression tests for bundled skill scripts and local shell execution.
Issue #953 verifies that bundled skill scripts run out of the box from the
installed ~/.hermes/skills tree without manual chmod or PATH surgery.
"""
import shlex
import shutil
import stat
from pathlib import Path
from tools.environments.local import LocalEnvironment
REPO_ROOT = Path(__file__).resolve().parents[2]
SKILLS_ROOT = REPO_ROOT / "skills"
def _bundled_shebang_scripts() -> list[Path]:
scripts: list[Path] = []
for path in SKILLS_ROOT.rglob("*"):
if not path.is_file() or path.is_symlink() or "scripts" not in path.parts:
continue
first_line = path.read_bytes().splitlines()[:1]
if first_line and first_line[0].startswith(b"#!"):
scripts.append(path)
return sorted(scripts)
def test_bundled_skill_shebang_scripts_are_executable():
missing = []
for path in _bundled_shebang_scripts():
mode = stat.S_IMODE(path.stat().st_mode)
if mode & 0o111 == 0:
missing.append(f"{path.relative_to(REPO_ROOT)} ({oct(mode)})")
assert not missing, (
"Bundled shebang scripts must ship executable so synced skill copies run "
"without manual chmod:\n" + "\n".join(missing)
)
def test_local_environment_executes_installed_skill_script_without_manual_prep(tmp_path):
hermes_home = tmp_path / ".hermes"
installed_skill = hermes_home / "skills" / "research" / "arxiv"
installed_skill.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(SKILLS_ROOT / "research" / "arxiv", installed_skill)
script_path = installed_skill / "scripts" / "search_arxiv.py"
env = LocalEnvironment(
cwd=str(tmp_path),
timeout=15,
env={
"HERMES_HOME": str(hermes_home),
"PATH": "/custom/bin",
},
)
result = env.execute(f"{shlex.quote(str(script_path))} --help")
assert result["returncode"] == 0, result["output"]
assert "Search arXiv and display results in a clean format." in result["output"]
assert "python search_arxiv.py" in result["output"]

View File

@@ -4,31 +4,13 @@ state management, streaming TTS activation, voice message prefix, _vprint."""
import ast
import os
import queue
import sys
import threading
import types
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import pytest
def _ensure_cli_import_shims():
sys.modules.setdefault(
"agent.auxiliary_client",
types.SimpleNamespace(
call_llm=lambda *args, **kwargs: "",
async_call_llm=lambda *args, **kwargs: "",
extract_content_or_reasoning=lambda *args, **kwargs: "",
resolve_provider_client=lambda *args, **kwargs: (None, None, None, None),
get_async_text_auxiliary_client=lambda *args, **kwargs: None,
),
)
_ensure_cli_import_shims()
def _make_voice_cli(**overrides):
"""Create a minimal HermesCLI with only voice-related attrs initialized.
@@ -36,7 +18,6 @@ def _make_voice_cli(**overrides):
needed. Only the voice state attributes (from __init__ lines 3749-3758)
are populated.
"""
_ensure_cli_import_shims()
from cli import HermesCLI
cli = HermesCLI.__new__(HermesCLI)
@@ -952,58 +933,6 @@ class TestEnableVoiceModeReal:
assert cli._voice_mode is True
class TestVoiceBeepConfigReal:
"""Tests the CLI voice beep toggle."""
@patch("hermes_cli.config.load_config", return_value={"voice": {}})
def test_beeps_enabled_by_default(self, _cfg):
cli = _make_voice_cli()
assert cli._voice_beeps_enabled() is True
@patch("hermes_cli.config.load_config", return_value={"voice": {"beep_enabled": False}})
def test_beeps_can_be_disabled(self, _cfg):
cli = _make_voice_cli()
assert cli._voice_beeps_enabled() is False
@patch("cli._cprint")
@patch("cli.threading.Thread")
@patch("tools.voice_mode.play_beep")
@patch("tools.voice_mode.create_audio_recorder")
@patch(
"tools.voice_mode.check_voice_requirements",
return_value={
"available": True,
"audio_available": True,
"stt_available": True,
"details": "OK",
"missing_packages": [],
},
)
@patch(
"hermes_cli.config.load_config",
return_value={
"voice": {
"beep_enabled": False,
"silence_threshold": 200,
"silence_duration": 3.0,
}
},
)
def test_start_recording_skips_beep_when_disabled(
self, _cfg, _req, mock_create, mock_beep, mock_thread, _cp
):
recorder = MagicMock()
recorder.supports_silence_autostop = True
mock_create.return_value = recorder
mock_thread.return_value = MagicMock(start=MagicMock())
cli = _make_voice_cli()
cli._voice_start_recording()
recorder.start.assert_called_once()
mock_beep.assert_not_called()
class TestDisableVoiceModeReal:
"""Tests _disable_voice_mode with real CLI instance."""
@@ -1158,16 +1087,6 @@ class TestVoiceStopAndTranscribeReal:
cli._voice_stop_and_transcribe()
assert cli._pending_input.empty()
@patch("cli._cprint")
@patch("hermes_cli.config.load_config", return_value={"voice": {"beep_enabled": False}})
@patch("tools.voice_mode.play_beep")
def test_no_speech_detected_skips_beep_when_disabled(self, mock_beep, _cfg, _cp):
recorder = MagicMock()
recorder.stop.return_value = None
cli = _make_voice_cli(_voice_recording=True, _voice_recorder=recorder)
cli._voice_stop_and_transcribe()
mock_beep.assert_not_called()
@patch("cli._cprint")
@patch("cli.os.unlink")
@patch("cli.os.path.isfile", return_value=True)
@@ -1237,18 +1156,12 @@ class TestVoiceStopAndTranscribeReal:
@patch("cli._cprint")
@patch("tools.voice_mode.play_beep")
def test_continuous_restarts_on_no_speech(self, _beep, _cp):
import time
recorder = MagicMock()
recorder.stop.return_value = None
cli = _make_voice_cli(_voice_recording=True, _voice_recorder=recorder,
_voice_continuous=True)
cli._voice_start_recording = MagicMock()
cli._voice_stop_and_transcribe()
for _ in range(50):
if cli._voice_start_recording.call_count:
break
time.sleep(0.01)
cli._voice_start_recording.assert_called_once()
@patch("cli._cprint")

View File

@@ -149,7 +149,7 @@ Two-stage algorithm detects when you've finished speaking:
If no speech is detected at all for 15 seconds, recording stops automatically.
Both `silence_threshold` and `silence_duration` are configurable in `config.yaml`. You can also disable the record start/stop beeps with `voice.beep_enabled: false`.
Both `silence_threshold` and `silence_duration` are configurable in `config.yaml`.
### Streaming TTS
@@ -383,7 +383,6 @@ voice:
record_key: "ctrl+b" # Key to start/stop recording
max_recording_seconds: 120 # Maximum recording length
auto_tts: false # Auto-enable TTS when voice mode starts
beep_enabled: true # Play record start/stop beeps
silence_threshold: 200 # RMS level (0-32767) below which counts as silence
silence_duration: 3.0 # Seconds of silence before auto-stop