diff --git a/cli.py b/cli.py index 337da08a7e..55372bbb00 100644 --- a/cli.py +++ b/cli.py @@ -5244,9 +5244,33 @@ class HermesCLI: context_length=ctx_len, ) _cprint(" ✨ (◕‿◕)✨ Fresh start! Screen cleared and conversation reset.\n") + # Show a random tip on new session + try: + from hermes_cli.tips import get_random_tip + _tip = get_random_tip() + try: + from hermes_cli.skin_engine import get_active_skin + _tip_color = get_active_skin().get_color("banner_dim", "#B8860B") + except Exception: + _tip_color = "#B8860B" + cc.print(f"[dim {_tip_color}]✦ Tip: {_tip}[/]") + except Exception: + pass else: self.show_banner() print(" ✨ (◕‿◕)✨ Fresh start! Screen cleared and conversation reset.\n") + # Show a random tip on new session + try: + from hermes_cli.tips import get_random_tip + _tip = get_random_tip() + try: + from hermes_cli.skin_engine import get_active_skin + _tip_color = get_active_skin().get_color("banner_dim", "#B8860B") + except Exception: + _tip_color = "#B8860B" + self.console.print(f"[dim {_tip_color}]✦ Tip: {_tip}[/]") + except Exception: + pass elif canonical == "history": self.show_history() elif canonical == "title": @@ -8075,6 +8099,17 @@ class HermesCLI: _welcome_text = "Welcome to Hermes Agent! Type your message or /help for commands." _welcome_color = "#FFF8DC" self.console.print(f"[{_welcome_color}]{_welcome_text}[/]") + # Show a random tip to help users discover features + try: + from hermes_cli.tips import get_random_tip + _tip = get_random_tip() + try: + _tip_color = _welcome_skin.get_color("banner_dim", "#B8860B") + except Exception: + _tip_color = "#B8860B" + self.console.print(f"[dim {_tip_color}]✦ Tip: {_tip}[/]") + except Exception: + pass # Tips are non-critical — never break startup if self.preloaded_skills and not self._startup_skills_line_shown: skills_label = ", ".join(self.preloaded_skills) self.console.print( diff --git a/gateway/run.py b/gateway/run.py index a5876bc147..56573d58ae 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -3965,9 +3965,16 @@ class GatewayRunner: except Exception: pass + # Append a random tip to the reset message + try: + from hermes_cli.tips import get_random_tip + _tip_line = f"\n✦ Tip: {get_random_tip()}" + except Exception: + _tip_line = "" + if session_info: - return f"{header}\n\n{session_info}" - return header + return f"{header}\n\n{session_info}{_tip_line}" + return f"{header}{_tip_line}" async def _handle_profile_command(self, event: MessageEvent) -> str: """Handle /profile — show active profile name and home directory.""" diff --git a/hermes_cli/tips.py b/hermes_cli/tips.py new file mode 100644 index 0000000000..223d8d5402 --- /dev/null +++ b/hermes_cli/tips.py @@ -0,0 +1,280 @@ +"""Random tips shown at CLI session start to help users discover features.""" + +import random +from typing import Optional + +# --------------------------------------------------------------------------- +# Tip corpus — ~200 one-liners covering slash commands, CLI flags, config, +# keybindings, tools, gateway, skills, profiles, and workflow tricks. +# --------------------------------------------------------------------------- + +TIPS = [ + # --- Slash Commands --- + "/btw asks a quick side question without tools or history — great for clarifications.", + "/background runs a task in a separate session while your current one stays free.", + "/branch forks the current session so you can explore a different direction without losing progress.", + "/compress manually compresses conversation context when things get long.", + "/rollback lists filesystem checkpoints — restore files the agent modified to any prior state.", + "/rollback diff 2 previews what changed since checkpoint 2 without restoring anything.", + "/rollback 2 src/file.py restores a single file from a specific checkpoint.", + "/title \"my project\" names your session — resume it later with /resume or hermes -c.", + "/resume picks up where you left off in a previously named session.", + "/queue queues a message for the next turn without interrupting the current one.", + "/undo removes the last user/assistant exchange from the conversation.", + "/retry resends your last message — useful when the agent's response wasn't quite right.", + "/verbose cycles tool progress display: off → new → all → verbose.", + "/reasoning high increases the model's thinking depth. /reasoning show displays the reasoning.", + "/fast toggles priority processing for faster API responses (provider-dependent).", + "/yolo skips all dangerous command approval prompts for the rest of the session.", + "/model lets you switch models mid-session — try /model sonnet or /model gpt-5.", + "/model --global changes your default model permanently.", + "/personality pirate sets a fun personality — 14 built-in options from kawaii to shakespeare.", + "/skin changes the CLI theme — try ares, mono, slate, poseidon, or charizard.", + "/statusbar toggles a persistent bar showing model, tokens, context fill %, cost, and duration.", + "/tools disable browser temporarily removes browser tools for the current session.", + "/browser connect attaches browser tools to your running Chrome instance via CDP.", + "/plugins lists installed plugins and their status.", + "/cron manages scheduled tasks — set up recurring prompts with delivery to any platform.", + "/reload-mcp hot-reloads MCP server configuration without restarting.", + "/usage shows token usage, cost breakdown, and session duration.", + "/insights shows usage analytics for the last 30 days.", + "/paste checks your clipboard for an image and attaches it to your next message.", + "/profile shows which profile is active and its home directory.", + "/config shows your current configuration at a glance.", + "/stop kills all running background processes spawned by the agent.", + + # --- @ Context References --- + "@file:path/to/file.py injects file contents directly into your message.", + "@file:main.py:10-50 injects only lines 10-50 of a file.", + "@folder:src/ injects a directory tree listing.", + "@diff injects your unstaged git changes into the message.", + "@staged injects your staged git changes (git diff --staged).", + "@git:5 injects the last 5 commits with full patches.", + "@url:https://example.com fetches and injects a web page's content.", + "Typing @ triggers filesystem path completion — navigate to any file interactively.", + "Combine multiple references: \"Review @file:main.py and @file:test.py for consistency.\"", + + # --- Keybindings --- + "Alt+Enter (or Ctrl+J) inserts a newline for multi-line input.", + "Ctrl+C interrupts the agent. Double-press within 2 seconds to force exit.", + "Ctrl+Z suspends Hermes to the background — run fg in your shell to resume.", + "Tab accepts auto-suggestion ghost text or autocompletes slash commands.", + "Type a new message while the agent is working to interrupt and redirect it.", + "Alt+V pastes an image from your clipboard into the conversation.", + "Pasting 5+ lines auto-saves to a file and inserts a compact reference instead.", + + # --- CLI Flags --- + "hermes -c resumes your most recent CLI session. hermes -c \"project name\" resumes by title.", + "hermes -w creates an isolated git worktree — perfect for parallel agent workflows.", + "hermes -w -q \"Fix issue #42\" combines worktree isolation with a one-shot query.", + "hermes chat -t web,terminal enables only specific toolsets for a focused session.", + "hermes chat -s github-pr-workflow preloads a skill at launch.", + "hermes chat -q \"query\" runs a single non-interactive query and exits.", + "hermes chat --max-turns 200 overrides the default 90-iteration limit per turn.", + "hermes chat --checkpoints enables filesystem snapshots before every destructive file change.", + "hermes --yolo bypasses all dangerous command approval prompts for the entire session.", + "hermes chat --source telegram tags the session for filtering in hermes sessions list.", + "hermes -p work chat runs under a specific profile without changing your default.", + + # --- CLI Subcommands --- + "hermes doctor --fix diagnoses and auto-repairs config and dependency issues.", + "hermes dump outputs a compact setup summary — great for bug reports.", + "hermes config set KEY VALUE auto-routes secrets to .env and everything else to config.yaml.", + "hermes config edit opens config.yaml in your default editor.", + "hermes config check scans for missing or stale configuration options.", + "hermes sessions browse opens an interactive session picker with search.", + "hermes sessions stats shows session counts by platform and database size.", + "hermes sessions prune --older-than 30 cleans up old sessions.", + "hermes skills search react --source skills-sh searches the skills.sh public directory.", + "hermes skills check scans installed hub skills for upstream updates.", + "hermes skills tap add myorg/skills-repo adds a custom GitHub skill source.", + "hermes skills snapshot export setup.json exports your skill configuration for backup or sharing.", + "hermes mcp add github --command npx adds MCP servers from the command line.", + "hermes mcp serve runs Hermes itself as an MCP server for other agents.", + "hermes auth add lets you add multiple API keys for credential pool rotation.", + "hermes completion bash >> ~/.bashrc enables tab completion for all commands and profiles.", + "hermes logs -f follows agent.log in real time. --level WARNING --since 1h filters output.", + "hermes backup creates a zip backup of your entire Hermes home directory.", + "hermes profile create coder creates an isolated profile that becomes its own command.", + "hermes profile create work --clone copies your current config and keys to a new profile.", + "hermes update syncs new bundled skills to ALL profiles automatically.", + "hermes gateway install sets up Hermes as a system service (systemd/launchd).", + "hermes memory setup lets you configure an external memory provider (Honcho, Mem0, etc.).", + "hermes webhook subscribe creates event-driven webhook routes with HMAC validation.", + + # --- Configuration --- + "Set display.bell_on_complete: true in config.yaml to hear a bell when long tasks finish.", + "Set display.streaming: true to see tokens appear in real time as the model generates.", + "Set display.show_reasoning: true to watch the model's chain-of-thought reasoning.", + "Set display.compact: true to reduce whitespace in output for denser information.", + "Set display.busy_input_mode: queue to queue messages instead of interrupting the agent.", + "Set display.resume_display: minimal to skip the full conversation recap on session resume.", + "Set compression.threshold: 0.50 to control when auto-compression fires (default: 50% of context).", + "Set agent.max_turns: 200 to let the agent take more tool-calling steps per turn.", + "Set file_read_max_chars: 200000 to increase the max content per read_file call.", + "Set approvals.mode: smart to let an LLM auto-approve safe commands and auto-deny dangerous ones.", + "Set fallback_model in config.yaml to automatically fail over to a backup provider.", + "Set privacy.redact_pii: true to hash user IDs and phone numbers before sending to the LLM.", + "Set browser.record_sessions: true to auto-record browser sessions as WebM videos.", + "Set worktree: true in config.yaml to always create a git worktree (same as hermes -w).", + "Set security.website_blocklist.enabled: true to block specific domains from web tools.", + "Set cron.wrap_response: false to deliver raw agent output without the cron header/footer.", + "HERMES_TIMEZONE overrides the server timezone with any IANA timezone string.", + "Environment variable substitution works in config.yaml: use ${VAR_NAME} syntax.", + "Quick commands in config.yaml run shell commands instantly with zero token usage.", + "Custom personalities can be defined in config.yaml under agent.personalities.", + "provider_routing controls OpenRouter provider sorting, whitelisting, and blacklisting.", + + # --- Tools & Capabilities --- + "execute_code runs Python scripts that call Hermes tools programmatically — results stay out of context.", + "delegate_task spawns up to 3 concurrent sub-agents with isolated contexts for parallel work.", + "web_extract works on PDF URLs — pass any PDF link and it converts to markdown.", + "search_files is ripgrep-backed and faster than grep — use it instead of terminal grep.", + "patch uses 9 fuzzy matching strategies so minor whitespace differences won't break edits.", + "patch supports V4A format for bulk multi-file edits in a single call.", + "read_file suggests similar filenames when a file isn't found.", + "read_file auto-deduplicates — re-reading an unchanged file returns a lightweight stub.", + "browser_vision takes a screenshot and analyzes it with AI — works for CAPTCHAs and visual content.", + "browser_console can evaluate JavaScript expressions in the page context.", + "image_generate creates images with FLUX 2 Pro and automatic 2x upscaling.", + "text_to_speech converts text to audio — plays as voice bubbles on Telegram.", + "send_message can reach any connected messaging platform from within a session.", + "The todo tool helps the agent track complex multi-step tasks during a session.", + "session_search performs full-text search across ALL past conversations.", + "The agent automatically saves preferences, corrections, and environment facts to memory.", + "mixture_of_agents routes hard problems through 4 frontier LLMs collaboratively.", + "Terminal commands support background mode with notify_on_complete for long-running tasks.", + "Terminal background processes support watch_patterns to alert on specific output lines.", + "The terminal tool supports 6 backends: local, Docker, SSH, Modal, Daytona, and Singularity.", + + # --- Profiles --- + "Each profile gets its own config, API keys, memory, sessions, skills, and cron jobs.", + "Profile names become shell commands — 'hermes profile create coder' creates the 'coder' command.", + "hermes profile export coder -o backup.tar.gz creates a portable profile archive.", + "If two profiles accidentally share a bot token, the second gateway is blocked with a clear error.", + + # --- Sessions --- + "Sessions auto-generate descriptive titles after the first exchange — no manual naming needed.", + "Session titles support lineage: \"my project\" → \"my project #2\" → \"my project #3\".", + "When exiting, Hermes prints a resume command with session ID and stats.", + "hermes sessions export backup.jsonl exports all sessions for backup or analysis.", + "hermes -r SESSION_ID resumes any specific past session by its ID.", + + # --- Memory --- + "Memory is a frozen snapshot — changes appear in the system prompt only at next session start.", + "Memory entries are automatically scanned for prompt injection and exfiltration patterns.", + "The agent has two memory stores: personal notes (~2200 chars) and user profile (~1375 chars).", + "Corrections you give the agent (\"no, do it this way\") are often auto-saved to memory.", + + # --- Skills --- + "Over 80 bundled skills covering github, creative, mlops, productivity, research, and more.", + "Every installed skill automatically becomes a slash command — type / to see them all.", + "hermes skills install official/security/1password installs optional skills from the repo.", + "Skills can restrict to specific OS platforms — some only load on macOS or Linux.", + "skills.external_dirs in config.yaml lets you load skills from custom directories.", + "The agent can create its own skills as procedural memory using skill_manage.", + "The plan skill saves markdown plans under .hermes/plans/ in the active workspace.", + + # --- Cron & Scheduling --- + "Cron jobs can attach skills: hermes cron add --skill blogwatcher \"Check for new posts\".", + "Cron delivery targets include telegram, discord, slack, email, sms, and 12+ more platforms.", + "If a cron response starts with [SILENT], delivery is suppressed — useful for monitoring-only jobs.", + "Cron supports relative delays (30m), intervals (every 2h), cron expressions, and ISO timestamps.", + "Cron jobs run in completely fresh agent sessions — prompts must be self-contained.", + + # --- Voice --- + "Voice mode works with zero API keys if faster-whisper is installed (free local speech-to-text).", + "Five TTS providers available: Edge TTS (free), ElevenLabs, OpenAI, NeuTTS (free local), MiniMax.", + "/voice on enables voice mode in the CLI. Ctrl+B toggles push-to-talk recording.", + "Streaming TTS plays sentences as they generate — you don't wait for the full response.", + "Voice messages on Telegram, Discord, WhatsApp, and Slack are auto-transcribed.", + + # --- Gateway & Messaging --- + "Hermes runs on 18 platforms: Telegram, Discord, Slack, WhatsApp, Signal, Matrix, email, and more.", + "hermes gateway install sets it up as a system service that starts on boot.", + "DingTalk uses Stream Mode — no webhooks or public URL needed.", + "BlueBubbles brings iMessage to Hermes via a local macOS server.", + "Webhook routes support HMAC validation, rate limiting, and event filtering.", + "The API server exposes an OpenAI-compatible endpoint compatible with Open WebUI and LibreChat.", + "Discord voice channel mode: the bot joins VC, transcribes speech, and talks back.", + "group_sessions_per_user: true gives each person their own session in group chats.", + "/sethome marks a chat as the home channel for cron job deliveries.", + "The gateway supports inactivity-based timeouts — active agents can run indefinitely.", + + # --- Security --- + "Dangerous command approval has 4 tiers: once, session, always (permanent allowlist), deny.", + "Smart approval mode uses an LLM to auto-approve safe commands and flag dangerous ones.", + "SSRF protection blocks private networks, loopback, link-local, and cloud metadata addresses.", + "Tirith pre-exec scanning detects homograph URL spoofing and pipe-to-interpreter patterns.", + "MCP subprocesses receive a filtered environment — only safe system vars pass through.", + "Context files (.hermes.md, AGENTS.md) are security-scanned for prompt injection before loading.", + "command_allowlist in config.yaml permanently approves specific shell command patterns.", + + # --- Context & Compression --- + "Context auto-compresses when it reaches the threshold — memories are flushed and history summarized.", + "The status bar turns yellow, then orange, then red as context fills up.", + "SOUL.md at ~/.hermes/SOUL.md is the agent's primary identity — customize it to shape behavior.", + "Hermes loads project context from .hermes.md, AGENTS.md, CLAUDE.md, or .cursorrules (first match).", + "Subdirectory AGENTS.md files are discovered progressively as the agent navigates into folders.", + "Context files are capped at 20,000 characters with smart head/tail truncation.", + + # --- Browser --- + "Five browser providers: local Chromium, Browserbase, Browser Use, Camofox, and Firecrawl.", + "Camofox is an anti-detection browser — Firefox fork with C++ fingerprint spoofing.", + "browser_navigate returns a page snapshot automatically — no need to call browser_snapshot after.", + "browser_vision with annotate=true overlays numbered labels on interactive elements.", + + # --- MCP --- + "MCP servers are configured in config.yaml — both stdio and HTTP transports supported.", + "Per-server tool filtering: tools.include whitelists and tools.exclude blacklists specific tools.", + "MCP servers auto-generate toolsets at runtime — hermes tools can toggle them per platform.", + "MCP OAuth support: auth: oauth enables browser-based authorization with PKCE.", + + # --- Checkpoints & Rollback --- + "Checkpoints have zero overhead when no files are modified — enabled by default.", + "A pre-rollback snapshot is saved automatically so you can undo the undo.", + "/rollback also undoes the conversation turn, so the agent doesn't remember rolled-back changes.", + "Checkpoints use shadow repos in ~/.hermes/checkpoints/ — your project's .git is never touched.", + + # --- Batch & Data --- + "batch_runner.py processes hundreds of prompts in parallel for training data generation.", + "hermes chat -Q enables quiet mode for programmatic use — suppresses banner and spinner.", + "Trajectory saving (--save-trajectories) captures full tool-use traces for model training.", + + # --- Plugins --- + "Three plugin types: general (tools/hooks), memory providers, and context engines.", + "hermes plugins install owner/repo installs plugins directly from GitHub.", + "8 external memory providers available: Honcho, OpenViking, Mem0, Hindsight, and more.", + "Plugin hooks include pre_tool_call, post_tool_call, pre_llm_call, and post_llm_call.", + + # --- Miscellaneous --- + "Prompt caching (Anthropic) reduces costs by reusing cached system prompt prefixes.", + "The agent auto-generates session titles in a background thread — zero latency impact.", + "Smart model routing can auto-route simple queries to a cheaper model.", + "Slash commands support prefix matching: /h resolves to /help, /mod to /model.", + "Dragging a file path into the terminal auto-attaches images or sends as context.", + ".worktreeinclude in your repo root lists gitignored files to copy into worktrees.", + "hermes acp runs Hermes as an ACP server for VS Code, Zed, and JetBrains integration.", + "Custom providers: save named endpoints in config.yaml under custom_providers.", + "HERMES_EPHEMERAL_SYSTEM_PROMPT injects a system prompt that's never persisted to history.", + "credential_pool_strategies supports fill_first, round_robin, least_used, and random rotation.", + "hermes login supports OAuth-based auth for Nous and OpenAI Codex providers.", + "The API server supports both Chat Completions and Responses API with server-side state.", + "tool_preview_length: 0 in config shows full file paths in the spinner's activity feed.", + "hermes status --deep runs deeper diagnostic checks across all components.", +] + + +def get_random_tip(exclude_recent: int = 0) -> str: + """Return a random tip string. + + Args: + exclude_recent: not used currently; reserved for future + deduplication across sessions. + """ + return random.choice(TIPS) + + +def get_tip_count() -> int: + """Return the total number of tips available.""" + return len(TIPS) diff --git a/tests/hermes_cli/test_tips.py b/tests/hermes_cli/test_tips.py new file mode 100644 index 0000000000..88e00e0ce6 --- /dev/null +++ b/tests/hermes_cli/test_tips.py @@ -0,0 +1,77 @@ +"""Tests for hermes_cli/tips.py — random tip display at session start.""" + +import pytest +from hermes_cli.tips import TIPS, get_random_tip, get_tip_count + + +class TestTipsCorpus: + """Validate the tip corpus itself.""" + + def test_has_at_least_200_tips(self): + assert len(TIPS) >= 200, f"Expected 200+ tips, got {len(TIPS)}" + + def test_no_duplicates(self): + assert len(TIPS) == len(set(TIPS)), "Duplicate tips found" + + def test_all_tips_are_strings(self): + for i, tip in enumerate(TIPS): + assert isinstance(tip, str), f"Tip {i} is not a string: {type(tip)}" + + def test_no_empty_tips(self): + for i, tip in enumerate(TIPS): + assert tip.strip(), f"Tip {i} is empty or whitespace-only" + + def test_max_length_reasonable(self): + """Tips should fit on a single terminal line (~120 chars max).""" + for i, tip in enumerate(TIPS): + assert len(tip) <= 150, ( + f"Tip {i} too long ({len(tip)} chars): {tip[:60]}..." + ) + + def test_no_leading_trailing_whitespace(self): + for i, tip in enumerate(TIPS): + assert tip == tip.strip(), f"Tip {i} has leading/trailing whitespace" + + +class TestGetRandomTip: + """Validate the get_random_tip() function.""" + + def test_returns_string(self): + tip = get_random_tip() + assert isinstance(tip, str) + assert len(tip) > 0 + + def test_returns_tip_from_corpus(self): + tip = get_random_tip() + assert tip in TIPS + + def test_randomness(self): + """Multiple calls should eventually return different tips.""" + seen = set() + for _ in range(50): + seen.add(get_random_tip()) + # With 200+ tips and 50 draws, we should see at least 10 unique + assert len(seen) >= 10, f"Only got {len(seen)} unique tips in 50 draws" + + +class TestGetTipCount: + def test_matches_corpus_length(self): + assert get_tip_count() == len(TIPS) + + +class TestTipIntegrationInCLI: + """Test that the tip display code in cli.py works correctly.""" + + def test_tip_import_works(self): + """The import used in cli.py must succeed.""" + from hermes_cli.tips import get_random_tip + assert callable(get_random_tip) + + def test_tip_display_format(self): + """Verify the Rich markup format doesn't break.""" + tip = get_random_tip() + color = "#B8860B" + markup = f"[dim {color}]✦ Tip: {tip}[/]" + # Should not contain nested/broken Rich tags + assert markup.count("[/]") == 1 + assert "[dim #B8860B]" in markup