2026-02-02 19:01:51 -08:00
|
|
|
"""
|
|
|
|
|
Status command for hermes CLI.
|
|
|
|
|
|
|
|
|
|
Shows the status of all Hermes Agent components.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
import subprocess
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
|
|
|
|
|
|
2026-03-11 19:37:42 +03:00
|
|
|
from hermes_cli.auth import AuthError, resolve_provider
|
2026-02-20 23:23:32 -08:00
|
|
|
from hermes_cli.colors import Colors, color
|
2026-03-11 19:37:42 +03:00
|
|
|
from hermes_cli.config import get_env_path, get_env_value, get_hermes_home, load_config
|
|
|
|
|
from hermes_cli.models import provider_label
|
|
|
|
|
from hermes_cli.runtime_provider import resolve_requested_provider
|
2026-02-20 23:23:32 -08:00
|
|
|
from hermes_constants import OPENROUTER_MODELS_URL
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
def check_mark(ok: bool) -> str:
|
|
|
|
|
if ok:
|
|
|
|
|
return color("✓", Colors.GREEN)
|
|
|
|
|
return color("✗", Colors.RED)
|
|
|
|
|
|
|
|
|
|
def redact_key(key: str) -> str:
|
|
|
|
|
"""Redact an API key for display."""
|
|
|
|
|
if not key:
|
|
|
|
|
return "(not set)"
|
|
|
|
|
if len(key) < 12:
|
|
|
|
|
return "***"
|
|
|
|
|
return key[:4] + "..." + key[-4:]
|
|
|
|
|
|
|
|
|
|
|
2026-02-20 17:24:00 -08:00
|
|
|
def _format_iso_timestamp(value) -> str:
|
|
|
|
|
"""Format ISO timestamps for status output, converting to local timezone."""
|
|
|
|
|
if not value or not isinstance(value, str):
|
|
|
|
|
return "(unknown)"
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
text = value.strip()
|
|
|
|
|
if not text:
|
|
|
|
|
return "(unknown)"
|
|
|
|
|
if text.endswith("Z"):
|
|
|
|
|
text = text[:-1] + "+00:00"
|
|
|
|
|
try:
|
|
|
|
|
parsed = datetime.fromisoformat(text)
|
|
|
|
|
if parsed.tzinfo is None:
|
|
|
|
|
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
|
|
|
except Exception:
|
|
|
|
|
return value
|
|
|
|
|
return parsed.astimezone().strftime("%Y-%m-%d %H:%M:%S %Z")
|
|
|
|
|
|
|
|
|
|
|
2026-03-11 19:37:42 +03:00
|
|
|
def _configured_model_label(config: dict) -> str:
|
|
|
|
|
"""Return the configured default model from config.yaml."""
|
|
|
|
|
model_cfg = config.get("model")
|
|
|
|
|
if isinstance(model_cfg, dict):
|
|
|
|
|
model = (model_cfg.get("default") or model_cfg.get("name") or "").strip()
|
|
|
|
|
elif isinstance(model_cfg, str):
|
|
|
|
|
model = model_cfg.strip()
|
|
|
|
|
else:
|
|
|
|
|
model = ""
|
|
|
|
|
return model or "(not set)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _effective_provider_label() -> str:
|
|
|
|
|
"""Return the provider label matching current CLI runtime resolution."""
|
|
|
|
|
requested = resolve_requested_provider()
|
|
|
|
|
try:
|
|
|
|
|
effective = resolve_provider(requested)
|
|
|
|
|
except AuthError:
|
|
|
|
|
effective = requested or "auto"
|
|
|
|
|
|
|
|
|
|
if effective == "openrouter" and get_env_value("OPENAI_BASE_URL"):
|
|
|
|
|
effective = "custom"
|
|
|
|
|
|
|
|
|
|
return provider_label(effective)
|
|
|
|
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
def show_status(args):
|
|
|
|
|
"""Show status of all Hermes Agent components."""
|
|
|
|
|
show_all = getattr(args, 'all', False)
|
|
|
|
|
deep = getattr(args, 'deep', False)
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
print(color("┌─────────────────────────────────────────────────────────┐", Colors.CYAN))
|
2026-02-20 21:25:04 -08:00
|
|
|
print(color("│ ⚕ Hermes Agent Status │", Colors.CYAN))
|
2026-02-02 19:01:51 -08:00
|
|
|
print(color("└─────────────────────────────────────────────────────────┘", Colors.CYAN))
|
|
|
|
|
|
|
|
|
|
# =========================================================================
|
|
|
|
|
# Environment
|
|
|
|
|
# =========================================================================
|
|
|
|
|
print()
|
|
|
|
|
print(color("◆ Environment", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
print(f" Project: {PROJECT_ROOT}")
|
|
|
|
|
print(f" Python: {sys.version.split()[0]}")
|
|
|
|
|
|
2026-02-26 16:49:14 +11:00
|
|
|
env_path = get_env_path()
|
2026-02-02 19:01:51 -08:00
|
|
|
print(f" .env file: {check_mark(env_path.exists())} {'exists' if env_path.exists() else 'not found'}")
|
2026-03-11 19:37:42 +03:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
config = load_config()
|
|
|
|
|
except Exception:
|
|
|
|
|
config = {}
|
|
|
|
|
|
|
|
|
|
print(f" Model: {_configured_model_label(config)}")
|
|
|
|
|
print(f" Provider: {_effective_provider_label()}")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
# =========================================================================
|
|
|
|
|
# API Keys
|
|
|
|
|
# =========================================================================
|
|
|
|
|
print()
|
|
|
|
|
print(color("◆ API Keys", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
|
|
|
|
keys = {
|
|
|
|
|
"OpenRouter": "OPENROUTER_API_KEY",
|
|
|
|
|
"OpenAI": "OPENAI_API_KEY",
|
feat: add z.ai/GLM, Kimi/Moonshot, MiniMax as first-class providers
Adds 4 new direct API-key providers (zai, kimi-coding, minimax, minimax-cn)
to the inference provider system. All use standard OpenAI-compatible
chat/completions endpoints with Bearer token auth.
Core changes:
- auth.py: Extended ProviderConfig with api_key_env_vars and base_url_env_var
fields. Added providers to PROVIDER_REGISTRY. Added provider aliases
(glm, z-ai, zhipu, kimi, moonshot). Added auto-detection of API-key
providers in resolve_provider(). Added resolve_api_key_provider_credentials()
and get_api_key_provider_status() helpers.
- runtime_provider.py: Added generic API-key provider branch in
resolve_runtime_provider() — any provider with auth_type='api_key'
is automatically handled.
- main.py: Added providers to hermes model menu with generic
_model_flow_api_key_provider() flow. Updated _has_any_provider_configured()
to check all provider env vars. Updated argparse --provider choices.
- setup.py: Added providers to setup wizard with API key prompts and
curated model lists.
- config.py: Added env vars (GLM_API_KEY, KIMI_API_KEY, MINIMAX_API_KEY,
etc.) to OPTIONAL_ENV_VARS.
- status.py: Added API key display and provider status section.
- doctor.py: Added connectivity checks for each provider endpoint.
- cli.py: Updated provider docstrings.
Docs: Updated README.md, .env.example, cli-config.yaml.example,
cli-commands.md, environment-variables.md, configuration.md.
Tests: 50 new tests covering registry, aliases, resolution, auto-detection,
credential resolution, and runtime provider dispatch.
Inspired by PR #33 (numman-ali) which proposed a provider registry approach.
Credit to tars90percent (PR #473) and manuelschipper (PR #420) for related
provider improvements merged earlier in this changeset.
2026-03-06 18:55:12 -08:00
|
|
|
"Z.AI/GLM": "GLM_API_KEY",
|
|
|
|
|
"Kimi": "KIMI_API_KEY",
|
|
|
|
|
"MiniMax": "MINIMAX_API_KEY",
|
|
|
|
|
"MiniMax-CN": "MINIMAX_CN_API_KEY",
|
2026-02-02 19:01:51 -08:00
|
|
|
"Firecrawl": "FIRECRAWL_API_KEY",
|
2026-03-17 04:28:03 -07:00
|
|
|
"Tavily": "TAVILY_API_KEY",
|
2026-03-07 01:23:27 -08:00
|
|
|
"Browserbase": "BROWSERBASE_API_KEY", # Optional — local browser works without this
|
2026-02-02 19:01:51 -08:00
|
|
|
"FAL": "FAL_KEY",
|
2026-02-04 09:36:51 -08:00
|
|
|
"Tinker": "TINKER_API_KEY",
|
|
|
|
|
"WandB": "WANDB_API_KEY",
|
2026-02-12 10:05:08 -08:00
|
|
|
"ElevenLabs": "ELEVENLABS_API_KEY",
|
Add Skills Hub — universal skill search, install, and management from online registries
Implements the Hermes Skills Hub with agentskills.io spec compliance,
multi-registry skill discovery, security scanning, and user-driven
management via CLI and /skills slash command.
Core features:
- Security scanner (tools/skills_guard.py): 120 threat patterns across
12 categories, trust-aware install policy (builtin/trusted/community),
structural checks, unicode injection detection, LLM audit pass
- Hub client (tools/skills_hub.py): GitHub, ClawHub, Claude Code
marketplace, and LobeHub source adapters with shared GitHubAuth
(PAT + gh CLI + GitHub App), lock file provenance tracking, quarantine
flow, and unified search across all sources
- CLI interface (hermes_cli/skills_hub.py): search, install, inspect,
list, audit, uninstall, publish (GitHub PR), snapshot export/import,
and tap management — powers both `hermes skills` and `/skills`
Spec conformance (Phase 0):
- Upgraded frontmatter parser to yaml.safe_load with fallback
- Migrated 39 SKILL.md files: tags/related_skills to metadata.hermes.*
- Added assets/ directory support and compatibility/metadata fields
- Excluded .hub/ from skill discovery in skills_tool.py
Updated 13 config/doc files including README, AGENTS.md, .env.example,
setup wizard, doctor, status, pyproject.toml, and docs.
2026-02-18 16:09:05 -08:00
|
|
|
"GitHub": "GITHUB_TOKEN",
|
2026-02-02 19:01:51 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for name, env_var in keys.items():
|
2026-02-26 16:49:14 +11:00
|
|
|
value = get_env_value(env_var) or ""
|
2026-02-02 19:01:51 -08:00
|
|
|
has_key = bool(value)
|
|
|
|
|
display = redact_key(value) if not show_all else value
|
|
|
|
|
print(f" {name:<12} {check_mark(has_key)} {display}")
|
2026-02-20 17:24:00 -08:00
|
|
|
|
2026-03-13 02:09:52 -07:00
|
|
|
anthropic_value = (
|
|
|
|
|
get_env_value("ANTHROPIC_TOKEN")
|
|
|
|
|
or get_env_value("ANTHROPIC_API_KEY")
|
|
|
|
|
or ""
|
|
|
|
|
)
|
|
|
|
|
anthropic_display = redact_key(anthropic_value) if not show_all else anthropic_value
|
|
|
|
|
print(f" {'Anthropic':<12} {check_mark(bool(anthropic_value))} {anthropic_display}")
|
|
|
|
|
|
2026-02-20 17:24:00 -08:00
|
|
|
# =========================================================================
|
|
|
|
|
# Auth Providers (OAuth)
|
|
|
|
|
# =========================================================================
|
|
|
|
|
print()
|
|
|
|
|
print(color("◆ Auth Providers", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
|
|
|
|
try:
|
2026-02-25 18:20:38 -08:00
|
|
|
from hermes_cli.auth import get_nous_auth_status, get_codex_auth_status
|
2026-02-20 17:24:00 -08:00
|
|
|
nous_status = get_nous_auth_status()
|
2026-02-25 18:20:38 -08:00
|
|
|
codex_status = get_codex_auth_status()
|
2026-02-20 17:24:00 -08:00
|
|
|
except Exception:
|
|
|
|
|
nous_status = {}
|
2026-02-25 18:20:38 -08:00
|
|
|
codex_status = {}
|
2026-02-20 17:24:00 -08:00
|
|
|
|
|
|
|
|
nous_logged_in = bool(nous_status.get("logged_in"))
|
|
|
|
|
print(
|
|
|
|
|
f" {'Nous Portal':<12} {check_mark(nous_logged_in)} "
|
2026-02-28 21:47:51 -08:00
|
|
|
f"{'logged in' if nous_logged_in else 'not logged in (run: hermes model)'}"
|
2026-02-20 17:24:00 -08:00
|
|
|
)
|
|
|
|
|
if nous_logged_in:
|
|
|
|
|
portal_url = nous_status.get("portal_base_url") or "(unknown)"
|
|
|
|
|
access_exp = _format_iso_timestamp(nous_status.get("access_expires_at"))
|
|
|
|
|
key_exp = _format_iso_timestamp(nous_status.get("agent_key_expires_at"))
|
|
|
|
|
refresh_label = "yes" if nous_status.get("has_refresh_token") else "no"
|
|
|
|
|
print(f" Portal URL: {portal_url}")
|
|
|
|
|
print(f" Access exp: {access_exp}")
|
|
|
|
|
print(f" Key exp: {key_exp}")
|
|
|
|
|
print(f" Refresh: {refresh_label}")
|
|
|
|
|
|
2026-02-25 18:20:38 -08:00
|
|
|
codex_logged_in = bool(codex_status.get("logged_in"))
|
|
|
|
|
print(
|
|
|
|
|
f" {'OpenAI Codex':<12} {check_mark(codex_logged_in)} "
|
2026-02-28 21:47:51 -08:00
|
|
|
f"{'logged in' if codex_logged_in else 'not logged in (run: hermes model)'}"
|
2026-02-25 18:20:38 -08:00
|
|
|
)
|
2026-03-05 21:27:12 +03:00
|
|
|
codex_auth_file = codex_status.get("auth_store")
|
2026-02-25 18:20:38 -08:00
|
|
|
if codex_auth_file:
|
|
|
|
|
print(f" Auth file: {codex_auth_file}")
|
|
|
|
|
codex_last_refresh = _format_iso_timestamp(codex_status.get("last_refresh"))
|
|
|
|
|
if codex_status.get("last_refresh"):
|
|
|
|
|
print(f" Refreshed: {codex_last_refresh}")
|
|
|
|
|
if codex_status.get("error") and not codex_logged_in:
|
|
|
|
|
print(f" Error: {codex_status.get('error')}")
|
|
|
|
|
|
feat: add z.ai/GLM, Kimi/Moonshot, MiniMax as first-class providers
Adds 4 new direct API-key providers (zai, kimi-coding, minimax, minimax-cn)
to the inference provider system. All use standard OpenAI-compatible
chat/completions endpoints with Bearer token auth.
Core changes:
- auth.py: Extended ProviderConfig with api_key_env_vars and base_url_env_var
fields. Added providers to PROVIDER_REGISTRY. Added provider aliases
(glm, z-ai, zhipu, kimi, moonshot). Added auto-detection of API-key
providers in resolve_provider(). Added resolve_api_key_provider_credentials()
and get_api_key_provider_status() helpers.
- runtime_provider.py: Added generic API-key provider branch in
resolve_runtime_provider() — any provider with auth_type='api_key'
is automatically handled.
- main.py: Added providers to hermes model menu with generic
_model_flow_api_key_provider() flow. Updated _has_any_provider_configured()
to check all provider env vars. Updated argparse --provider choices.
- setup.py: Added providers to setup wizard with API key prompts and
curated model lists.
- config.py: Added env vars (GLM_API_KEY, KIMI_API_KEY, MINIMAX_API_KEY,
etc.) to OPTIONAL_ENV_VARS.
- status.py: Added API key display and provider status section.
- doctor.py: Added connectivity checks for each provider endpoint.
- cli.py: Updated provider docstrings.
Docs: Updated README.md, .env.example, cli-config.yaml.example,
cli-commands.md, environment-variables.md, configuration.md.
Tests: 50 new tests covering registry, aliases, resolution, auto-detection,
credential resolution, and runtime provider dispatch.
Inspired by PR #33 (numman-ali) which proposed a provider registry approach.
Credit to tars90percent (PR #473) and manuelschipper (PR #420) for related
provider improvements merged earlier in this changeset.
2026-03-06 18:55:12 -08:00
|
|
|
# =========================================================================
|
|
|
|
|
# API-Key Providers
|
|
|
|
|
# =========================================================================
|
|
|
|
|
print()
|
|
|
|
|
print(color("◆ API-Key Providers", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
|
|
|
|
apikey_providers = {
|
|
|
|
|
"Z.AI / GLM": ("GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY"),
|
|
|
|
|
"Kimi / Moonshot": ("KIMI_API_KEY",),
|
|
|
|
|
"MiniMax": ("MINIMAX_API_KEY",),
|
|
|
|
|
"MiniMax (China)": ("MINIMAX_CN_API_KEY",),
|
|
|
|
|
}
|
|
|
|
|
for pname, env_vars in apikey_providers.items():
|
|
|
|
|
key_val = ""
|
|
|
|
|
for ev in env_vars:
|
|
|
|
|
key_val = get_env_value(ev) or ""
|
|
|
|
|
if key_val:
|
|
|
|
|
break
|
|
|
|
|
configured = bool(key_val)
|
|
|
|
|
label = "configured" if configured else "not configured (run: hermes model)"
|
|
|
|
|
print(f" {pname:<16} {check_mark(configured)} {label}")
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
# =========================================================================
|
|
|
|
|
# Terminal Configuration
|
|
|
|
|
# =========================================================================
|
|
|
|
|
print()
|
|
|
|
|
print(color("◆ Terminal Backend", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
2026-02-16 19:47:23 -08:00
|
|
|
terminal_env = os.getenv("TERMINAL_ENV", "")
|
|
|
|
|
if not terminal_env:
|
|
|
|
|
# Fall back to config file value when env var isn't set
|
|
|
|
|
# (hermes status doesn't go through cli.py's config loading)
|
|
|
|
|
try:
|
|
|
|
|
_cfg = load_config()
|
|
|
|
|
terminal_env = _cfg.get("terminal", {}).get("backend", "local")
|
|
|
|
|
except Exception:
|
|
|
|
|
terminal_env = "local"
|
2026-02-02 19:01:51 -08:00
|
|
|
print(f" Backend: {terminal_env}")
|
|
|
|
|
|
|
|
|
|
if terminal_env == "ssh":
|
|
|
|
|
ssh_host = os.getenv("TERMINAL_SSH_HOST", "")
|
|
|
|
|
ssh_user = os.getenv("TERMINAL_SSH_USER", "")
|
|
|
|
|
print(f" SSH Host: {ssh_host or '(not set)'}")
|
|
|
|
|
print(f" SSH User: {ssh_user or '(not set)'}")
|
|
|
|
|
elif terminal_env == "docker":
|
|
|
|
|
docker_image = os.getenv("TERMINAL_DOCKER_IMAGE", "python:3.11-slim")
|
|
|
|
|
print(f" Docker Image: {docker_image}")
|
2026-03-05 00:44:39 -08:00
|
|
|
elif terminal_env == "daytona":
|
|
|
|
|
daytona_image = os.getenv("TERMINAL_DAYTONA_IMAGE", "nikolaik/python-nodejs:python3.11-nodejs20")
|
|
|
|
|
print(f" Daytona Image: {daytona_image}")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
sudo_password = os.getenv("SUDO_PASSWORD", "")
|
|
|
|
|
print(f" Sudo: {check_mark(bool(sudo_password))} {'enabled' if sudo_password else 'disabled'}")
|
|
|
|
|
|
|
|
|
|
# =========================================================================
|
|
|
|
|
# Messaging Platforms
|
|
|
|
|
# =========================================================================
|
|
|
|
|
print()
|
|
|
|
|
print(color("◆ Messaging Platforms", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
|
|
|
|
platforms = {
|
|
|
|
|
"Telegram": ("TELEGRAM_BOT_TOKEN", "TELEGRAM_HOME_CHANNEL"),
|
|
|
|
|
"Discord": ("DISCORD_BOT_TOKEN", "DISCORD_HOME_CHANNEL"),
|
|
|
|
|
"WhatsApp": ("WHATSAPP_ENABLED", None),
|
fix: Signal adapter parity pass — integration gaps, clawdbot features, env var simplification
Integration gaps fixed (7 files missing Signal):
- cron/scheduler.py: Signal in platform_map (cron delivery was broken)
- agent/prompt_builder.py: PLATFORM_HINTS for Signal (agent knows it's on Signal)
- toolsets.py: hermes-signal toolset + added to hermes-gateway composite
- hermes_cli/status.py: Signal + Slack in platform status display
- tools/send_message_tool.py: Signal example in target description
- tools/cronjob_tools.py: Signal in delivery option docs + schema
- gateway/channel_directory.py: Signal in session-based channel discovery
Clawdbot parity features added to signal.py:
- Self-message filtering: prevents reply loops by checking sender != account
- SyncMessage filtering: ignores sync envelopes (sent transcripts, read receipts)
- Edit message support: reads dataMessage from editMessage envelope
- Mention rendering: replaces \uFFFC placeholders with @identifier text
- Jitter in SSE reconnection backoff (20% randomization, prevents thundering herd)
Env var simplification (7 → 4):
- Removed SIGNAL_DM_POLICY (DM auth follows standard platform pattern via
SIGNAL_ALLOWED_USERS + DM pairing, same as Telegram/Discord)
- Removed SIGNAL_GROUP_POLICY (derived from SIGNAL_GROUP_ALLOWED_USERS:
not set = disabled, set with IDs = allowlist, set with * = open)
- Removed SIGNAL_DEBUG (was setting root logger, removed entirely)
- Remaining: SIGNAL_HTTP_URL, SIGNAL_ACCOUNT (required),
SIGNAL_ALLOWED_USERS, SIGNAL_GROUP_ALLOWED_USERS (optional)
Updated all docs (website, AGENTS.md, signal.md) to match.
2026-03-08 21:00:21 -07:00
|
|
|
"Signal": ("SIGNAL_HTTP_URL", "SIGNAL_HOME_CHANNEL"),
|
|
|
|
|
"Slack": ("SLACK_BOT_TOKEN", None),
|
feat: add email gateway platform (IMAP/SMTP)
Allow users to interact with Hermes by sending and receiving emails.
Uses IMAP polling for incoming messages and SMTP for replies with
proper threading (In-Reply-To, References headers).
Integrates with all 14 gateway extension points: config, adapter
factory, authorization, send_message tool, cron delivery, toolsets,
prompt hints, channel directory, setup wizard, status display, and
env example.
65 tests covering config, parsing, dispatch, threading, IMAP fetch,
SMTP send, attachments, and all integration points.
2026-03-10 03:15:38 +03:00
|
|
|
"Email": ("EMAIL_ADDRESS", "EMAIL_HOME_ADDRESS"),
|
feat: add SMS (Twilio) platform adapter
Add SMS as a first-class messaging platform via the Twilio API.
Shares credentials with the existing telephony skill — same
TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER env vars.
Adapter (gateway/platforms/sms.py):
- aiohttp webhook server for inbound (Twilio form-encoded POSTs)
- Twilio REST API with Basic auth for outbound
- Markdown stripping, smart chunking at 1600 chars
- Echo loop prevention, phone number redaction in logs
Integration (13 files):
- gateway config, run, channel_directory
- agent prompt_builder (SMS platform hint)
- cron scheduler, cronjob tools
- send_message_tool (_send_sms via Twilio API)
- toolsets (hermes-sms + hermes-gateway)
- gateway setup wizard, status display
- pyproject.toml (sms optional extra)
- 21 tests
Docs:
- website/docs/user-guide/messaging/sms.md (full setup guide)
- Updated messaging index (architecture, toolsets, security, links)
- Updated environment-variables.md reference
Inspired by PR #1575 (@sunsakis), rewritten for Twilio.
2026-03-17 03:14:53 -07:00
|
|
|
"SMS": ("TWILIO_ACCOUNT_SID", "SMS_HOME_CHANNEL"),
|
2026-02-02 19:01:51 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for name, (token_var, home_var) in platforms.items():
|
|
|
|
|
token = os.getenv(token_var, "")
|
|
|
|
|
has_token = bool(token)
|
|
|
|
|
|
|
|
|
|
home_channel = ""
|
|
|
|
|
if home_var:
|
|
|
|
|
home_channel = os.getenv(home_var, "")
|
|
|
|
|
|
|
|
|
|
status = "configured" if has_token else "not configured"
|
|
|
|
|
if home_channel:
|
|
|
|
|
status += f" (home: {home_channel})"
|
|
|
|
|
|
|
|
|
|
print(f" {name:<12} {check_mark(has_token)} {status}")
|
|
|
|
|
|
|
|
|
|
# =========================================================================
|
|
|
|
|
# Gateway Status
|
|
|
|
|
# =========================================================================
|
|
|
|
|
print()
|
|
|
|
|
print(color("◆ Gateway Service", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
|
|
|
|
if sys.platform.startswith('linux'):
|
2026-03-16 04:42:46 -07:00
|
|
|
try:
|
|
|
|
|
from hermes_cli.gateway import get_service_name
|
|
|
|
|
_gw_svc = get_service_name()
|
|
|
|
|
except Exception:
|
|
|
|
|
_gw_svc = "hermes-gateway"
|
2026-02-02 19:01:51 -08:00
|
|
|
result = subprocess.run(
|
2026-03-16 04:42:46 -07:00
|
|
|
["systemctl", "--user", "is-active", _gw_svc],
|
2026-02-02 19:01:51 -08:00
|
|
|
capture_output=True,
|
|
|
|
|
text=True
|
|
|
|
|
)
|
|
|
|
|
is_active = result.stdout.strip() == "active"
|
|
|
|
|
print(f" Status: {check_mark(is_active)} {'running' if is_active else 'stopped'}")
|
|
|
|
|
print(f" Manager: systemd (user)")
|
|
|
|
|
|
|
|
|
|
elif sys.platform == 'darwin':
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
["launchctl", "list", "ai.hermes.gateway"],
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True
|
|
|
|
|
)
|
|
|
|
|
is_loaded = result.returncode == 0
|
|
|
|
|
print(f" Status: {check_mark(is_loaded)} {'loaded' if is_loaded else 'not loaded'}")
|
|
|
|
|
print(f" Manager: launchd")
|
|
|
|
|
else:
|
|
|
|
|
print(f" Status: {color('N/A', Colors.DIM)}")
|
|
|
|
|
print(f" Manager: (not supported on this platform)")
|
|
|
|
|
|
|
|
|
|
# =========================================================================
|
|
|
|
|
# Cron Jobs
|
|
|
|
|
# =========================================================================
|
|
|
|
|
print()
|
|
|
|
|
print(color("◆ Scheduled Jobs", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
fix(cli): respect HERMES_HOME in all remaining hardcoded ~/.hermes paths
Several files resolved paths via Path.home() / ".hermes" or
os.path.expanduser("~/.hermes/..."), bypassing the HERMES_HOME
environment variable. This broke isolation when running multiple
Hermes instances with distinct HERMES_HOME directories.
Replace all hardcoded paths with calls to get_hermes_home() from
hermes_cli.config, consistent with the rest of the codebase.
Files fixed:
- tools/process_registry.py (processes.json)
- gateway/pairing.py (pairing/)
- gateway/sticker_cache.py (sticker_cache.json)
- gateway/channel_directory.py (channel_directory.json, sessions.json)
- gateway/config.py (gateway.json, config.yaml, sessions_dir)
- gateway/mirror.py (sessions/)
- gateway/hooks.py (hooks/)
- gateway/platforms/base.py (image_cache/, audio_cache/, document_cache/)
- gateway/platforms/whatsapp.py (whatsapp/session)
- gateway/delivery.py (cron/output)
- agent/auxiliary_client.py (auth.json)
- agent/prompt_builder.py (SOUL.md)
- cli.py (config.yaml, images/, pastes/, history)
- run_agent.py (logs/)
- tools/environments/base.py (sandboxes/)
- tools/environments/modal.py (modal_snapshots.json)
- tools/environments/singularity.py (singularity_snapshots.json)
- tools/tts_tool.py (audio_cache)
- hermes_cli/status.py (cron/jobs.json, sessions.json)
- hermes_cli/gateway.py (logs/, whatsapp session)
- hermes_cli/main.py (whatsapp/session)
Tests updated to use HERMES_HOME env var instead of patching Path.home().
Closes #892
(cherry picked from commit 78ac1bba43b8b74a934c6172f2c29bb4d03164b9)
2026-03-11 07:31:41 +01:00
|
|
|
jobs_file = get_hermes_home() / "cron" / "jobs.json"
|
2026-02-02 19:01:51 -08:00
|
|
|
if jobs_file.exists():
|
|
|
|
|
import json
|
|
|
|
|
try:
|
2026-03-05 17:04:33 -05:00
|
|
|
with open(jobs_file, encoding="utf-8") as f:
|
2026-02-02 19:01:51 -08:00
|
|
|
data = json.load(f)
|
|
|
|
|
jobs = data.get("jobs", [])
|
|
|
|
|
enabled_jobs = [j for j in jobs if j.get("enabled", True)]
|
|
|
|
|
print(f" Jobs: {len(enabled_jobs)} active, {len(jobs)} total")
|
2026-02-20 23:23:32 -08:00
|
|
|
except Exception:
|
2026-02-02 19:01:51 -08:00
|
|
|
print(f" Jobs: (error reading jobs file)")
|
|
|
|
|
else:
|
|
|
|
|
print(f" Jobs: 0")
|
|
|
|
|
|
|
|
|
|
# =========================================================================
|
|
|
|
|
# Sessions
|
|
|
|
|
# =========================================================================
|
|
|
|
|
print()
|
|
|
|
|
print(color("◆ Sessions", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
fix(cli): respect HERMES_HOME in all remaining hardcoded ~/.hermes paths
Several files resolved paths via Path.home() / ".hermes" or
os.path.expanduser("~/.hermes/..."), bypassing the HERMES_HOME
environment variable. This broke isolation when running multiple
Hermes instances with distinct HERMES_HOME directories.
Replace all hardcoded paths with calls to get_hermes_home() from
hermes_cli.config, consistent with the rest of the codebase.
Files fixed:
- tools/process_registry.py (processes.json)
- gateway/pairing.py (pairing/)
- gateway/sticker_cache.py (sticker_cache.json)
- gateway/channel_directory.py (channel_directory.json, sessions.json)
- gateway/config.py (gateway.json, config.yaml, sessions_dir)
- gateway/mirror.py (sessions/)
- gateway/hooks.py (hooks/)
- gateway/platforms/base.py (image_cache/, audio_cache/, document_cache/)
- gateway/platforms/whatsapp.py (whatsapp/session)
- gateway/delivery.py (cron/output)
- agent/auxiliary_client.py (auth.json)
- agent/prompt_builder.py (SOUL.md)
- cli.py (config.yaml, images/, pastes/, history)
- run_agent.py (logs/)
- tools/environments/base.py (sandboxes/)
- tools/environments/modal.py (modal_snapshots.json)
- tools/environments/singularity.py (singularity_snapshots.json)
- tools/tts_tool.py (audio_cache)
- hermes_cli/status.py (cron/jobs.json, sessions.json)
- hermes_cli/gateway.py (logs/, whatsapp session)
- hermes_cli/main.py (whatsapp/session)
Tests updated to use HERMES_HOME env var instead of patching Path.home().
Closes #892
(cherry picked from commit 78ac1bba43b8b74a934c6172f2c29bb4d03164b9)
2026-03-11 07:31:41 +01:00
|
|
|
sessions_file = get_hermes_home() / "sessions" / "sessions.json"
|
2026-02-02 19:01:51 -08:00
|
|
|
if sessions_file.exists():
|
|
|
|
|
import json
|
|
|
|
|
try:
|
2026-03-05 17:04:33 -05:00
|
|
|
with open(sessions_file, encoding="utf-8") as f:
|
2026-02-02 19:01:51 -08:00
|
|
|
data = json.load(f)
|
|
|
|
|
print(f" Active: {len(data)} session(s)")
|
2026-02-20 23:23:32 -08:00
|
|
|
except Exception:
|
2026-02-02 19:01:51 -08:00
|
|
|
print(f" Active: (error reading sessions file)")
|
|
|
|
|
else:
|
|
|
|
|
print(f" Active: 0")
|
|
|
|
|
|
|
|
|
|
# =========================================================================
|
|
|
|
|
# Deep checks
|
|
|
|
|
# =========================================================================
|
|
|
|
|
if deep:
|
|
|
|
|
print()
|
|
|
|
|
print(color("◆ Deep Checks", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
|
|
|
|
# Check OpenRouter connectivity
|
|
|
|
|
openrouter_key = os.getenv("OPENROUTER_API_KEY", "")
|
|
|
|
|
if openrouter_key:
|
|
|
|
|
try:
|
|
|
|
|
import httpx
|
|
|
|
|
response = httpx.get(
|
2026-02-20 23:23:32 -08:00
|
|
|
OPENROUTER_MODELS_URL,
|
2026-02-02 19:01:51 -08:00
|
|
|
headers={"Authorization": f"Bearer {openrouter_key}"},
|
|
|
|
|
timeout=10
|
|
|
|
|
)
|
|
|
|
|
ok = response.status_code == 200
|
|
|
|
|
print(f" OpenRouter: {check_mark(ok)} {'reachable' if ok else f'error ({response.status_code})'}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f" OpenRouter: {check_mark(False)} error: {e}")
|
|
|
|
|
|
|
|
|
|
# Check gateway port
|
|
|
|
|
try:
|
|
|
|
|
import socket
|
|
|
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
|
sock.settimeout(1)
|
|
|
|
|
result = sock.connect_ex(('127.0.0.1', 18789))
|
|
|
|
|
sock.close()
|
|
|
|
|
# Port in use = gateway likely running
|
|
|
|
|
port_in_use = result == 0
|
|
|
|
|
# This is informational, not necessarily bad
|
|
|
|
|
print(f" Port 18789: {'in use' if port_in_use else 'available'}")
|
2026-02-20 23:23:32 -08:00
|
|
|
except OSError:
|
2026-02-02 19:01:51 -08:00
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
print(color("─" * 60, Colors.DIM))
|
|
|
|
|
print(color(" Run 'hermes doctor' for detailed diagnostics", Colors.DIM))
|
|
|
|
|
print(color(" Run 'hermes setup' to configure", Colors.DIM))
|
|
|
|
|
print()
|