Compare commits

...

30 Commits

Author SHA1 Message Date
kshitij
b9ff134f73 feat: add Tavily setup support (#1113)
Register TAVILY_API_KEY across the configuration and setup flow:
- OPTIONAL_ENV_VARS metadata (config.py)
- ENV_VARS_BY_VERSION migration at version 10
- hermes config / hermes status display
- hermes tools web provider selection
- Setup summary
- Config version bump 9 → 10

Cherry-picked from PR #1113 by kshitijk4poor. Fixed migration version
(7 → 10) and resolved merge conflicts with current main.

Closes #1069
2026-03-17 02:57:18 -07:00
teknium1
77da011b25 Merge remote-tracking branch 'origin/main' into hermes/hermes-6bb9911e 2026-03-17 02:54:29 -07:00
teknium1
46894c4486 fix: detect Claude Code version dynamically for OAuth user-agent
The _CLAUDE_CODE_VERSION was hardcoded to '2.1.2' but Anthropic
rejects OAuth requests when the spoofed user-agent version is too
far behind the current Claude Code release. The error is a generic
400 with just 'Error' as the message, making it very hard to diagnose.

Fix: detect the installed version via 'claude --version' at import
time, falling back to a bumped static constant (2.1.74) when Claude
Code isn't installed. This means users who keep Claude Code updated
never hit stale-version rejections.

Reported by Jack — changing the version string to match the installed
claude binary fixed persistent OAuth 400 errors immediately.
2026-03-17 02:47:37 -07:00
teknium1
f2d9e409bf Merge remote-tracking branch 'origin/main' into hermes/hermes-6bb9911e 2026-03-17 02:46:10 -07:00
jcorrego
87048ba541 fix(whatsapp): support LID format in self-chat mode (#1556)
WhatsApp now uses LID (Linked Identity Device) format alongside classic
@s.whatsapp.net. Self-chat detection checked only the classic format,
breaking self-chat mode for users on newer WhatsApp versions.

- Check both sock.user.id and sock.user.lid for self-chat detection
- Accept 'append' message type in addition to 'notify' (self-chat
  messages arrive as 'append')
- Track sent message IDs to prevent echo-back loops with media
- Add WHATSAPP_DEBUG env var for troubleshooting

Based on PR #1556 by jcorrego (manually applied due to cherry-pick
conflicts).
2026-03-17 02:40:40 -07:00
teknium1
6229e48000 fix: email send_typing metadata param + ☤ Hermes staff symbol
- email.py: add missing metadata parameter to send_typing() to match
  BasePlatformAdapter signature (PR #1431 by @ItsChoudhry)
- README.md: ⚕ → ☤ — the caduceus is Hermes's staff, not the
  medical Staff of Asclepius (PR #1420 by @rianczerwinski)
2026-03-17 02:37:24 -07:00
Teknium
926afb4685 fix(docker): add explicit env allowlist for container credentials (#1436)
Docker terminal sessions are secret-dark by default. This adds
terminal.docker_forward_env as an explicit allowlist for env vars
that may be forwarded into Docker containers.

Values resolve from the current shell first, then fall back to
~/.hermes/.env. Only variables the user explicitly lists are
forwarded — nothing is auto-exposed.

Cherry-picked from PR #1449 by @teknium1, conflict-resolved onto
current main.

Fixes #1436
Supersedes #1439
2026-03-17 02:37:24 -07:00
Teknium
e6083209f2 refactor(tts): replace NeuTTS optional skill with built-in provider + setup flow
Remove the optional skill (redundant now that NeuTTS is a built-in TTS
provider). Replace neutts_cli dependency with a standalone synthesis
helper (tools/neutts_synth.py) that calls the neutts Python API directly
in a subprocess.

Add TTS provider selection to hermes setup:
- 'hermes setup' now prompts for TTS provider after model selection
- 'hermes setup tts' available as standalone section
- Selecting NeuTTS checks for deps and offers to install:
  espeak-ng (system) + neutts[all] (pip)
- ElevenLabs/OpenAI selections prompt for API keys
- Tool status display shows NeuTTS install state

Changes:
- Remove optional-skills/mlops/models/neutts/ (skill + CLI scaffold)
- Add tools/neutts_synth.py (standalone synthesis subprocess helper)
- Move jo.wav/jo.txt to tools/neutts_samples/ (bundled default voice)
- Refactor _generate_neutts() — uses neutts API via subprocess, no
  neutts_cli dependency, config-driven ref_audio/ref_text/model/device
- Add TTS setup to hermes_cli/setup.py (SETUP_SECTIONS, tool status)
- Update config.py defaults (ref_audio, ref_text, model, device)
2026-03-17 02:37:24 -07:00
teknium1
8e8b35e868 perf: use ripgrep for file search (200x faster than find)
search_files(target='files') now uses rg --files -g instead of find.
Ripgrep respects .gitignore, excludes hidden dirs by default, and has
parallel directory traversal — ~200x faster on wide trees (0.14s vs 34s
benchmarked on 164-repo tree).

Falls back to find when rg is unavailable, preserving hidden-dir
exclusion and BSD find compatibility.

Salvaged from PR #1464 by @light-merlin-dark (Merlin) — adapted to
preserve hidden-dir exclusion added since the original PR.
2026-03-17 02:37:24 -07:00
kshitij
83f3dfc7c2 fix: recognize Claude Code OAuth credentials in startup gate (#1455)
The _has_any_provider_configured() startup check didn't look for
Claude Code OAuth credentials (~/.claude/.credentials.json). Users
with only Claude Code auth got the setup wizard instead of starting.

Cherry-picked from PR #1455 by kshitijk4poor.
2026-03-17 02:32:01 -07:00
teknium1
fe01f0b122 Merge remote-tracking branch 'origin/main' into hermes/hermes-6bb9911e 2026-03-17 02:31:40 -07:00
Angello Picasso
d4810d8c4b feat(gateway): inject reply-to message context for out-of-session replies (#1594)
When a user replies to a Telegram message, check if the quoted text
exists in the current session transcript. If missing (from cron jobs,
background tasks, or old sessions), prepend [Replying to: "..."] to
the message so the agent has context about what's being referenced.

- Add reply_to_text field to MessageEvent (base.py)
- Populate from Telegram's reply_to_message (text or caption)
- Inject context in _handle_message when not found in history

Based on PR #1596 by anpicasso (cherry-picked reply-to feature only,
excluded unrelated /server command and background delegation changes).
2026-03-17 02:31:09 -07:00
teknium1
fb83d5d86d Merge remote-tracking branch 'origin/main' into hermes/hermes-6bb9911e 2026-03-17 02:29:53 -07:00
Max K
753f94a44d fix(gateway): /model shows active fallback model instead of config default (#1615)
When the agent falls back to a different model (e.g. due to rate
limiting), /model still showed the config default. Now tracks the
effective model/provider after each agent run and displays it.

Cleared when the primary model succeeds again or the user explicitly
switches via /model.

Cherry-picked from PR #1616 by MaxKerkula. Added hasattr guard for
test compatibility.
2026-03-17 02:26:35 -07:00
teknium1
b76f4b2110 Merge remote-tracking branch 'origin/main' into hermes/hermes-6bb9911e 2026-03-17 02:25:26 -07:00
teknium1
ef7c41b58f fix(gateway): cap interrupt recursion depth to prevent resource exhaustion (#816)
When a user sends multiple messages while the agent keeps failing,
_run_agent() calls itself recursively with no depth limit. This can
exhaust stack/memory if the agent is in a failure loop.

Add _MAX_INTERRUPT_DEPTH = 3. When exceeded, the pending message is
logged and the current result is returned instead of recursing deeper.

The log handler duplication bug described in #816 was already fixed
separately (AIAgent.__init__ deduplicates handlers).
2026-03-17 02:22:51 -07:00
teknium1
6da65dc9f0 Merge remote-tracking branch 'origin/main' into hermes/hermes-6bb9911e 2026-03-17 02:21:09 -07:00
buray
95076d7e57 fix(security): block sandbox backend creds from subprocess env (#1264)
Add Modal and Daytona sandbox credentials to the subprocess env
blocklist so they're not leaked to agent terminal sessions via
printenv/env.

Cherry-picked from PR #1571 by ygd58.
2026-03-17 02:20:25 -07:00
teknium1
3bd0fa2bab Merge remote-tracking branch 'origin/main' into hermes/hermes-6bb9911e 2026-03-17 02:19:04 -07:00
buray
f824116ff9 fix(claw): warn when API keys are skipped during OpenClaw migration (#1580)
When --migrate-secrets is not passed (the default), API keys like
OPENROUTER_API_KEY are silently skipped with no warning. Users don't
realize their keys weren't migrated until the agent fails to connect.

Add a post-migration warning with actionable instructions: either
re-run with --migrate-secrets or add the key manually via
hermes config set.

Cherry-picked from PR #1593 by ygd58.
2026-03-17 02:10:19 -07:00
teknium1
1b48ce9578 Merge remote-tracking branch 'origin/main' into hermes/hermes-6bb9911e 2026-03-17 02:09:59 -07:00
teknium1
4cfecd41a6 fix(cli): flush stdout during agent loop to prevent macOS display freeze (#1624)
The interrupt polling loop in chat() waited on the queue without
invalidating the prompt_toolkit renderer. On macOS, the StdoutProxy
buffer only flushed on input events, causing the CLI to appear frozen
during tool execution until the user typed a key.

Fix: call _invalidate() on each queue timeout (every ~100ms, throttled
to 150ms) to force the renderer to flush buffered agent output.
2026-03-17 02:09:01 -07:00
teknium1
afc9ad0b31 Merge remote-tracking branch 'origin/main' into hermes/hermes-6bb9911e 2026-03-17 02:06:05 -07:00
crazywriter1
667395ddd3 fix(approval): show full command in dangerous command approval (#1553)
Previously the command was truncated to 80 chars in CLI (with a
[v]iew full option), 500 chars in Discord embeds, and missing entirely
in Telegram/Slack approval messages. Now the full command is always
displayed everywhere:

- CLI: removed 80-char truncation and [v]iew full menu option
- Gateway (TG/Slack): approval_required message includes full command
  in a code block
- Discord: embed shows full command up to 4096-char limit
- Windows: skip SIGALRM-based test timeout (Unix-only)
- Updated tests: replaced view-flow tests with direct approval tests

Cherry-picked from PR #1566 by crazywriter1.
2026-03-17 02:02:16 -07:00
teknium1
9b7dbb1a49 Merge remote-tracking branch 'origin/main' into hermes/hermes-6bb9911e 2026-03-17 02:00:51 -07:00
lbn
993abca05c fix(tools): chunk long messages in send_message_tool before dispatch (#1552)
Long messages sent via send_message tool or cron delivery silently
failed when exceeding platform limits. Gateway adapters handle this
via truncate_message(), but the standalone senders in send_message_tool
bypassed that entirely.

- Apply truncate_message() chunking in _send_to_platform() before
  dispatching to individual platform senders
- Remove naive message[i:i+2000] character split in _send_discord()
  in favor of centralized smart splitting
- Attach media files to last chunk only for Telegram
- Add regression tests for chunking and media placement

Cherry-picked from PR #1557 by llbn.
2026-03-17 01:52:21 -07:00
teknium1
a1425d7fb5 Merge remote-tracking branch 'origin/main' into hermes/hermes-6bb9911e 2026-03-17 01:51:44 -07:00
buray
26b2fc360f fix(skills): detect prompt injection patterns and block cache file reads
Adds two security layers to prevent prompt injection via skills hub
cache files (#1558):

1. read_file: blocks direct reads of ~/.hermes/skills/.hub/ directory
   (index-cache, catalog files). The 3.5MB clawhub_catalog_v1.json
   was the original injection vector — untrusted skill descriptions
   in the catalog contained adversarial text that the model executed.

2. skill_view: warns when skills are loaded from outside the trusted
   ~/.hermes/skills/ directory, and detects common injection patterns
   in skill content ("ignore previous instructions", "<system>", etc.).

Cherry-picked from PR #1562 by ygd58.
2026-03-17 01:49:49 -07:00
teknium1
e1e702abc5 Merge remote-tracking branch 'origin/main' into hermes/hermes-6bb9911e 2026-03-17 01:48:06 -07:00
teknium1
75a2b77b0d fix: prevent infinite 400 failure loop on context overflow (#1630)
When a gateway session exceeds the model's context window, Anthropic may
return a generic 400 invalid_request_error with just 'Error' as the
message.  This bypassed the phrase-based context-length detection,
causing the agent to treat it as a non-retryable client error.  Worse,
the failed user message was still persisted to the transcript, making
the session even larger on each attempt — creating an infinite loop.

Three-layer fix:

1. run_agent.py — Fallback heuristic: when a 400 error has a very short
   generic message AND the session is large (>40% of context or >80
   messages), treat it as a probable context overflow and trigger
   compression instead of aborting.

2. run_agent.py + gateway/run.py — Don't persist failed messages:
   when the agent returns failed=True before generating any response,
   skip writing the user's message to the transcript/DB. This prevents
   the session from growing on each failure.

3. gateway/run.py — Smarter error messages: detect context-overflow
   failures and suggest /compact or /reset specifically, instead of a
   generic 'try again' that will fail identically.
2026-03-17 01:45:39 -07:00
6 changed files with 52 additions and 3 deletions

View File

@@ -357,7 +357,7 @@ DEFAULT_CONFIG = {
},
# Config schema version - bump this when adding new required fields
"_config_version": 9,
"_config_version": 10,
}
# =============================================================================
@@ -371,6 +371,7 @@ ENV_VARS_BY_VERSION: Dict[int, List[str]] = {
4: ["VOICE_TOOLS_OPENAI_KEY", "ELEVENLABS_API_KEY"],
5: ["WHATSAPP_ENABLED", "WHATSAPP_MODE", "WHATSAPP_ALLOWED_USERS",
"SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "SLACK_ALLOWED_USERS"],
10: ["TAVILY_API_KEY"],
}
# Required environment variables with metadata for migration prompts.
@@ -550,6 +551,14 @@ OPTIONAL_ENV_VARS = {
"password": True,
"category": "tool",
},
"TAVILY_API_KEY": {
"description": "Tavily API key for Tavily-based research skills and integrations",
"prompt": "Tavily API key",
"url": "https://app.tavily.com/home",
"tools": ["tavily skills", "research integrations"],
"password": True,
"category": "tool",
},
"FIRECRAWL_API_URL": {
"description": "Firecrawl API URL for self-hosted instances (optional)",
"prompt": "Firecrawl API URL (leave empty for cloud)",
@@ -1450,6 +1459,7 @@ def show_config():
("OPENROUTER_API_KEY", "OpenRouter"),
("VOICE_TOOLS_OPENAI_KEY", "OpenAI (STT/TTS)"),
("FIRECRAWL_API_KEY", "Firecrawl"),
("TAVILY_API_KEY", "Tavily"),
("BROWSERBASE_API_KEY", "Browserbase"),
("BROWSER_USE_API_KEY", "Browser Use"),
("FAL_KEY", "FAL"),
@@ -1598,7 +1608,7 @@ def set_config_value(key: str, value: str):
# Check if it's an API key (goes to .env)
api_keys = [
'OPENROUTER_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'VOICE_TOOLS_OPENAI_KEY',
'FIRECRAWL_API_KEY', 'FIRECRAWL_API_URL', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID', 'BROWSER_USE_API_KEY',
'FIRECRAWL_API_KEY', 'TAVILY_API_KEY', 'FIRECRAWL_API_URL', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID', 'BROWSER_USE_API_KEY',
'FAL_KEY', 'TELEGRAM_BOT_TOKEN', 'DISCORD_BOT_TOKEN',
'TERMINAL_SSH_HOST', 'TERMINAL_SSH_USER', 'TERMINAL_SSH_KEY',
'SUDO_PASSWORD', 'SLACK_BOT_TOKEN', 'SLACK_APP_TOKEN',

View File

@@ -450,6 +450,12 @@ def _print_setup_summary(config: dict, hermes_home):
else:
tool_status.append(("Web Search & Extract", False, "FIRECRAWL_API_KEY"))
# Tavily (skills / integrations)
if get_env_value("TAVILY_API_KEY"):
tool_status.append(("Tavily Research Integrations", True, None))
else:
tool_status.append(("Tavily Research Integrations", False, "TAVILY_API_KEY"))
# Browser tools (local Chromium or Browserbase cloud)
import shutil

View File

@@ -120,6 +120,7 @@ def show_status(args):
"MiniMax": "MINIMAX_API_KEY",
"MiniMax-CN": "MINIMAX_CN_API_KEY",
"Firecrawl": "FIRECRAWL_API_KEY",
"Tavily": "TAVILY_API_KEY",
"Browserbase": "BROWSERBASE_API_KEY", # Optional — local browser works without this
"FAL": "FAL_KEY",
"Tinker": "TINKER_API_KEY",

View File

@@ -150,9 +150,16 @@ TOOL_CATEGORIES = {
"web": {
"name": "Web Search & Extract",
"setup_title": "Select Search Provider",
"setup_note": "A free DuckDuckGo search skill is also included — skip this if you don't need Firecrawl.",
"setup_note": "A free DuckDuckGo search skill is also included, and Tavily-based skills/integrations can be configured here too.",
"icon": "🔍",
"providers": [
{
"name": "Tavily",
"tag": "Search-first research for Tavily-based skills and integrations",
"env_vars": [
{"key": "TAVILY_API_KEY", "prompt": "Tavily API key", "url": "https://app.tavily.com/home"},
],
},
{
"name": "Firecrawl Cloud",
"tag": "Recommended - hosted service",

View File

@@ -8,6 +8,8 @@ import yaml
from hermes_cli.config import (
DEFAULT_CONFIG,
ENV_VARS_BY_VERSION,
OPTIONAL_ENV_VARS,
get_hermes_home,
ensure_hermes_home,
load_config,
@@ -345,3 +347,12 @@ class TestAnthropicTokenMigration:
}):
migrate_config(interactive=False, quiet=True)
assert load_env().get("ANTHROPIC_TOKEN") == "current-token"
class TestOptionalToolKeys:
def test_tavily_api_key_is_registered_for_setup_and_migration(self):
tavily = OPTIONAL_ENV_VARS["TAVILY_API_KEY"]
assert tavily["prompt"] == "Tavily API key"
assert tavily["category"] == "tool"
assert "research" in tavily["description"].lower()
assert "TAVILY_API_KEY" in ENV_VARS_BY_VERSION[10]

View File

@@ -0,0 +1,14 @@
from types import SimpleNamespace
from hermes_cli.status import show_status
def test_show_status_includes_tavily_key(monkeypatch, capsys, tmp_path):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
monkeypatch.setenv("TAVILY_API_KEY", "tvly-1234567890abcdef")
show_status(SimpleNamespace(all=False, deep=False))
output = capsys.readouterr().out
assert "Tavily" in output
assert "tvly...cdef" in output