Merge remote-tracking branch 'origin/main' into codex/align-codex-provider-conventions-mainrepo
# Conflicts: # cron/scheduler.py # gateway/run.py # tools/delegate_tool.py
This commit is contained in:
commit
32070e6bc0
61 changed files with 8482 additions and 244 deletions
|
|
@ -28,19 +28,26 @@ import argparse
|
|||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# Add project root to path
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
# Load .env file
|
||||
# Load .env from ~/.hermes/.env first, then project root as dev fallback
|
||||
from dotenv import load_dotenv
|
||||
env_path = PROJECT_ROOT / '.env'
|
||||
if env_path.exists():
|
||||
from hermes_cli.config import get_env_path, get_hermes_home
|
||||
_user_env = get_env_path()
|
||||
if _user_env.exists():
|
||||
try:
|
||||
load_dotenv(dotenv_path=env_path, encoding="utf-8")
|
||||
load_dotenv(dotenv_path=_user_env, encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
load_dotenv(dotenv_path=env_path, encoding="latin-1")
|
||||
load_dotenv(dotenv_path=_user_env, encoding="latin-1")
|
||||
load_dotenv(dotenv_path=PROJECT_ROOT / '.env', override=False)
|
||||
|
||||
# Point mini-swe-agent at ~/.hermes/ so it shares our config
|
||||
os.environ.setdefault("MSWEA_GLOBAL_CONFIG_DIR", str(get_hermes_home()))
|
||||
os.environ.setdefault("MSWEA_SILENT_STARTUP", "1")
|
||||
|
||||
import logging
|
||||
|
||||
|
|
@ -91,8 +98,31 @@ def _has_any_provider_configured() -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def _resolve_last_cli_session() -> Optional[str]:
|
||||
"""Look up the most recent CLI session ID from SQLite. Returns None if unavailable."""
|
||||
try:
|
||||
from hermes_state import SessionDB
|
||||
db = SessionDB()
|
||||
sessions = db.search_sessions(source="cli", limit=1)
|
||||
db.close()
|
||||
if sessions:
|
||||
return sessions[0]["id"]
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def cmd_chat(args):
|
||||
"""Run interactive chat CLI."""
|
||||
# Resolve --continue into --resume with the latest CLI session
|
||||
if getattr(args, "continue_last", False) and not getattr(args, "resume", None):
|
||||
last_id = _resolve_last_cli_session()
|
||||
if last_id:
|
||||
args.resume = last_id
|
||||
else:
|
||||
print("No previous CLI session found to continue.")
|
||||
sys.exit(1)
|
||||
|
||||
# First-run guard: check if any provider is configured before launching
|
||||
if not _has_any_provider_configured():
|
||||
print()
|
||||
|
|
@ -121,6 +151,7 @@ def cmd_chat(args):
|
|||
"toolsets": args.toolsets,
|
||||
"verbose": args.verbose,
|
||||
"query": args.query,
|
||||
"resume": getattr(args, "resume", None),
|
||||
}
|
||||
# Filter out None values
|
||||
kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
||||
|
|
@ -134,6 +165,116 @@ def cmd_gateway(args):
|
|||
gateway_command(args)
|
||||
|
||||
|
||||
def cmd_whatsapp(args):
|
||||
"""Set up WhatsApp: enable, configure allowed users, install bridge, pair via QR."""
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from hermes_cli.config import get_env_value, save_env_value
|
||||
|
||||
print()
|
||||
print("⚕ WhatsApp Setup")
|
||||
print("=" * 50)
|
||||
print()
|
||||
print("This will link your WhatsApp account to Hermes Agent.")
|
||||
print("The agent will respond to messages sent to your WhatsApp number.")
|
||||
print()
|
||||
|
||||
# Step 1: Enable WhatsApp
|
||||
current = get_env_value("WHATSAPP_ENABLED")
|
||||
if current and current.lower() == "true":
|
||||
print("✓ WhatsApp is already enabled")
|
||||
else:
|
||||
save_env_value("WHATSAPP_ENABLED", "true")
|
||||
print("✓ WhatsApp enabled")
|
||||
|
||||
# Step 2: Allowed users
|
||||
current_users = get_env_value("WHATSAPP_ALLOWED_USERS") or ""
|
||||
if current_users:
|
||||
print(f"✓ Allowed users: {current_users}")
|
||||
response = input("\n Update allowed users? [y/N] ").strip()
|
||||
if response.lower() in ("y", "yes"):
|
||||
phone = input(" Phone number(s) (e.g. 15551234567, comma-separated): ").strip()
|
||||
if phone:
|
||||
save_env_value("WHATSAPP_ALLOWED_USERS", phone.replace(" ", ""))
|
||||
print(f" ✓ Updated to: {phone}")
|
||||
else:
|
||||
print()
|
||||
phone = input(" Your phone number (e.g. 15551234567): ").strip()
|
||||
if phone:
|
||||
save_env_value("WHATSAPP_ALLOWED_USERS", phone.replace(" ", ""))
|
||||
print(f" ✓ Allowed users set: {phone}")
|
||||
else:
|
||||
print(" ⚠ No allowlist — the agent will respond to ALL incoming messages")
|
||||
|
||||
# Step 3: Install bridge deps
|
||||
project_root = Path(__file__).resolve().parents[1]
|
||||
bridge_dir = project_root / "scripts" / "whatsapp-bridge"
|
||||
bridge_script = bridge_dir / "bridge.js"
|
||||
|
||||
if not bridge_script.exists():
|
||||
print(f"\n✗ Bridge script not found at {bridge_script}")
|
||||
return
|
||||
|
||||
if not (bridge_dir / "node_modules").exists():
|
||||
print("\n→ Installing WhatsApp bridge dependencies...")
|
||||
result = subprocess.run(
|
||||
["npm", "install"],
|
||||
cwd=str(bridge_dir),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f" ✗ npm install failed: {result.stderr}")
|
||||
return
|
||||
print(" ✓ Dependencies installed")
|
||||
else:
|
||||
print("✓ Bridge dependencies already installed")
|
||||
|
||||
# Step 4: Check for existing session
|
||||
session_dir = Path.home() / ".hermes" / "whatsapp" / "session"
|
||||
session_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if (session_dir / "creds.json").exists():
|
||||
print("✓ Existing WhatsApp session found")
|
||||
response = input("\n Re-pair? This will clear the existing session. [y/N] ").strip()
|
||||
if response.lower() in ("y", "yes"):
|
||||
import shutil
|
||||
shutil.rmtree(session_dir, ignore_errors=True)
|
||||
session_dir.mkdir(parents=True, exist_ok=True)
|
||||
print(" ✓ Session cleared")
|
||||
else:
|
||||
print("\n✓ WhatsApp is configured and paired!")
|
||||
print(" Start the gateway with: hermes gateway")
|
||||
return
|
||||
|
||||
# Step 5: Run bridge in pair-only mode (no HTTP server, exits after QR scan)
|
||||
print()
|
||||
print("─" * 50)
|
||||
print("📱 Scan the QR code with your phone:")
|
||||
print(" WhatsApp → Settings → Linked Devices → Link a Device")
|
||||
print("─" * 50)
|
||||
print()
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
["node", str(bridge_script), "--pair-only", "--session", str(session_dir)],
|
||||
cwd=str(bridge_dir),
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
print()
|
||||
if (session_dir / "creds.json").exists():
|
||||
print("✓ WhatsApp paired successfully!")
|
||||
print()
|
||||
print("Start the gateway with: hermes gateway")
|
||||
print("Or install as a service: hermes gateway install")
|
||||
else:
|
||||
print("⚠ Pairing may not have completed. Run 'hermes whatsapp' to try again.")
|
||||
|
||||
|
||||
def cmd_setup(args):
|
||||
"""Interactive setup wizard."""
|
||||
from hermes_cli.setup import run_setup_wizard
|
||||
|
|
@ -682,6 +823,8 @@ def main():
|
|||
Examples:
|
||||
hermes Start interactive chat
|
||||
hermes chat -q "Hello" Single query mode
|
||||
hermes --continue Resume the most recent session
|
||||
hermes --resume <session_id> Resume a specific session
|
||||
hermes setup Run setup wizard
|
||||
hermes login Authenticate with an inference provider
|
||||
hermes logout Clear stored authentication
|
||||
|
|
@ -691,6 +834,7 @@ Examples:
|
|||
hermes config set model gpt-4 Set a config value
|
||||
hermes gateway Run messaging gateway
|
||||
hermes gateway install Install as system service
|
||||
hermes sessions list List past sessions
|
||||
hermes update Update to latest version
|
||||
|
||||
For more help on a command:
|
||||
|
|
@ -703,6 +847,19 @@ For more help on a command:
|
|||
action="store_true",
|
||||
help="Show version and exit"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--resume", "-r",
|
||||
metavar="SESSION_ID",
|
||||
default=None,
|
||||
help="Resume a previous session by ID (shortcut for: hermes chat --resume ID)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--continue", "-c",
|
||||
dest="continue_last",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Resume the most recent CLI session"
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
||||
|
||||
|
|
@ -737,6 +894,18 @@ For more help on a command:
|
|||
action="store_true",
|
||||
help="Verbose output"
|
||||
)
|
||||
chat_parser.add_argument(
|
||||
"--resume", "-r",
|
||||
metavar="SESSION_ID",
|
||||
help="Resume a previous session by ID (shown on exit)"
|
||||
)
|
||||
chat_parser.add_argument(
|
||||
"--continue", "-c",
|
||||
dest="continue_last",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Resume the most recent CLI session"
|
||||
)
|
||||
chat_parser.set_defaults(func=cmd_chat)
|
||||
|
||||
# =========================================================================
|
||||
|
|
@ -805,6 +974,16 @@ For more help on a command:
|
|||
)
|
||||
setup_parser.set_defaults(func=cmd_setup)
|
||||
|
||||
# =========================================================================
|
||||
# whatsapp command
|
||||
# =========================================================================
|
||||
whatsapp_parser = subparsers.add_parser(
|
||||
"whatsapp",
|
||||
help="Set up WhatsApp integration",
|
||||
description="Configure WhatsApp and pair via QR code"
|
||||
)
|
||||
whatsapp_parser.set_defaults(func=cmd_whatsapp)
|
||||
|
||||
# =========================================================================
|
||||
# login command
|
||||
# =========================================================================
|
||||
|
|
@ -1233,6 +1412,17 @@ For more help on a command:
|
|||
cmd_version(args)
|
||||
return
|
||||
|
||||
# Handle top-level --resume / --continue as shortcut to chat
|
||||
if (args.resume or args.continue_last) and args.command is None:
|
||||
args.command = "chat"
|
||||
args.query = None
|
||||
args.model = None
|
||||
args.provider = None
|
||||
args.toolsets = None
|
||||
args.verbose = False
|
||||
cmd_chat(args)
|
||||
return
|
||||
|
||||
# Default to chat if no command specified
|
||||
if args.command is None:
|
||||
args.query = None
|
||||
|
|
@ -1240,6 +1430,8 @@ For more help on a command:
|
|||
args.provider = None
|
||||
args.toolsets = None
|
||||
args.verbose = False
|
||||
args.resume = None
|
||||
args.continue_last = False
|
||||
cmd_chat(args)
|
||||
return
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue