### Bootstrap development environment with standard installer Source: https://hermes-agent.nousresearch.com/docs/developer-guide/contributing Use the standard installer to clone the repository and install development dependencies in the recommended layout. ```bash curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash cd "${HERMES_HOME:-$HOME/.hermes}/hermes-agent" # Add dev/test extras on top of the standard install. uv pip install -e ".[all,dev]" # Optional: browser tools / docs site dependencies. npm install ``` -------------------------------- ### Verify installation and run commands Source: https://hermes-agent.nousresearch.com/docs/developer-guide/contributing Check the installation status and execute basic chat commands. ```bash # The standard installer already put `hermes` on PATH. hermes doctor hermes chat -q "Hello" ``` -------------------------------- ### hermes egress lifecycle: install, setup, start, stop Source: https://hermes-agent.nousresearch.com/docs/developer-guide/egress-internals Lifecycle overview of the hermes egress CLI commands (install, setup, start, stop) and the internal functions they invoke. Shows the security measures: SHA-256 verification, atomic file writes, O_NOFOLLOW, nonce planting, and PID file handling. The stop command avoids SIGKILL if the process was recycled. ```text hermes egress install -> agent.proxy_sources.iron_proxy.install_iron_proxy(force=...) Downloads pinned tarball + checksums.txt from GitHub Releases. SHA-256 verification before extraction. tarfile.extract(..., filter="data") on Python 3.12+ (PEP 706); falls back to plain extract on older Python with member-name sanitisation via _pick_tar_member. Stage into ~/.hermes/bin/.iron-proxy_XXXX, chmod 755, os.replace to ~/.hermes/bin/iron-proxy (atomic). _VERSION_CACHE.pop(target) so a forced reinstall re-probes --version on next call. hermes egress setup [--from-bitwarden | --no-bitwarden] [--rotate-tokens] -> proxy_cli.cmd_setup Step 1. find_iron_proxy(install_if_missing=False) -> install if absent. Step 2. ensure_ca_cert() Run openssl genrsa + req via subprocess. Write CA key via os.open(O_WRONLY|O_CREAT|O_TRUNC|O_NOFOLLOW, 0o600) + os.replace. Never exists on disk under default umask. Write CA cert with 0o644 (public). Step 3. discover_provider_mappings() or pull names from BWS via fetch_bitwarden_secrets() when --from-bitwarden. merge_mappings(existing=load_mappings(), discovered, rotate=args.rotate_tokens) preserves prior tokens unless --rotate-tokens is passed. discover_uncovered_providers() and surface warnings. Step 4. ensure_audit_log(audit_log_path) # raises on OSError build_proxy_config(...) with defaults applied at the call site (deny CIDRs default, bind policy from _default_http_listen). write_proxy_config(cfg) # atomic via .tmp + os.replace, 0o600 write_mappings(mappings) # atomic, 0o600 Step 5. proxy_cfg["enabled"] = True; credential_source preservation logic (do NOT silently downgrade bitwarden -> env on re-run); save_config(cfg). hermes egress start -> proxy_cli.cmd_start Pre-checks (refuse-start path): - credential_source=bitwarden? -> pre-validate access_token_env + project_id -> iron_proxy.start_proxy( refresh_secrets_from_bitwarden=..., bitwarden_config=..., ) existing=_read_pid(); if alive, idempotent return. _build_proxy_subprocess_env(...): ALLOWLIST + mapped real_env_names, strip HTTPS_PROXY/etc. to avoid recursion, optional BWS refresh (raises on missing values unless allow_env_fallback=true). Plant nonce: _proxy_nonce = sha256(urandom(16)); env[NONCE_ENV] = ... Open log_path via O_NOFOLLOW + 0o600 + st_uid check. Popen with stdin=DEVNULL, stdout=log_fd, stderr=STDOUT, start_new_session=True (POSIX). Close parent's log_fd in finally. _write_pidfile_safely(pidfile, proc.pid) O_EXCL + O_NOFOLLOW + uid check + persisted nonce sidecar. FileExistsError -> discriminate live vs stale, retry once if stale. Install SIGINT/SIGTERM handlers (main-thread only). Poll loop (do-while shape): while True: if proc.poll() is not None: tail log + unlink pidfile + raise if _port_listening(probe_host, tunnel_port): break # probe_host = configured bind host if time.time() >= deadline: break (do-while: checked AFTER first probe) time.sleep(0.1) If not listening at exit: _kill_and_wait(proc) + unlink pidfile + raise. hermes egress stop -> iron_proxy.stop_proxy _read_pid + _pid_alive guard. starttime_before = _pid_proc_starttime(pid) # Linux only; None elsewhere os.kill(pid, SIGTERM) Wait up to 5s for graceful exit. After grace: re-check starttime + _pid_alive. If recycled (starttime drift OR _pid_alive False), DO NOT SIGKILL. Otherwise os.kill(pid, _KILL_SIGNAL). _cleanup_state_files: unlink pidfile + nonce sibling. ``` -------------------------------- ### get_config_schema() example Source: https://hermes-agent.nousresearch.com/docs/developer-guide/memory-provider-plugin Python method returning a list of field descriptors for the setup wizard. Fields with 'secret': True and 'env_var' are written to .env; non-secret fields are passed to save_config(). ```python def get_config_schema(self): return [ { "key": "api_key", "description": "My Provider API key", "secret": True, # → written to .env "required": True, "env_var": "MY_API_KEY", # explicit env var name "url": "https://my-provider.com/keys", # where to get it }, { "key": "region", "description": "Server region", "default": "us-east", "choices": ["us-east", "eu-west", "ap-south"], }, { "key": "project", "description": "Project identifier", "default": "hermes", }, ] ``` -------------------------------- ### Minimal API-key provider: __init__.py and plugin.yaml Source: https://hermes-agent.nousresearch.com/docs/developer-guide/model-provider-plugin Minimal provider example: __init__.py defines a ProviderProfile and registers it; plugin.yaml declares the plugin metadata. After dropping these two files, the provider is auto-wired into credential resolution, the --provider CLI flag, the hermes model picker, hermes doctor, hermes setup, URL reverse-mapping, auxiliary model selection, runtime resolution, and the chat_completions transport. ```python # plugins/model-providers/acme-inference/__init__.py from providers import register_provider from providers.base import ProviderProfile acme = ProviderProfile( name="acme-inference", aliases=("acme",), display_name="Acme Inference", description="Acme — OpenAI-compatible direct API", signup_url="https://acme.example.com/keys", env_vars=("ACME_API_KEY", "ACME_BASE_URL"), base_url="https://api.acme.example.com/v1", auth_type="api_key", default_aux_model="acme-small-fast", fallback_models=( "acme-large-v3", "acme-medium-v3", "acme-small-fast", ), ) register_provider(acme) ``` ```yaml # plugins/model-providers/acme-inference/plugin.yaml name: acme-inference kind: model-provider version: 1.0.0 description: Acme Inference — OpenAI-compatible direct API author: Your Name ``` -------------------------------- ### Run commands for the wrapper CLI Source: https://hermes-agent.nousresearch.com/docs/developer-guide/extending-the-cli Shell commands to activate the virtual environment and run the example wrapper CLI. Assumes the Hermes agent is installed in ~/.hermes/hermes-agent. ```bash cd ~/.hermes/hermes-agent source .venv/bin/activate python my_cli.py ``` -------------------------------- ### Install and enable a portable plugin Source: https://hermes-agent.nousresearch.com/docs/developer-guide/plugins Commands to install a portable package without enabling it, list installed plugins, and then enable the desired plugin. ```bash hermes plugins install owner/repository --no-enable hermes plugins list hermes plugins enable ``` -------------------------------- ### Hermes install link with hermes:// scheme Source: https://hermes-agent.nousresearch.com/docs/developer-guide/desktop-plugin-sdk Install link using the hermes:// scheme. The user gets a confirmation dialog and picks components before anything is installed; deep links never auto-install. force=1 replaces an existing install; dev builds use hermes-dev://. ```html Install in Hermes ``` -------------------------------- ### NixOS entry-point plugin configuration Source: https://hermes-agent.nousresearch.com/docs/developer-guide/plugins Declarative NixOS installation for entry-point plugins. Add this to your configuration.nix to build and install a plugin package from a GitHub source. Requires a pyproject.toml with entry points. The hash placeholder should be replaced with the output of nix-prefetch-url --unpack. ```nix # User's configuration.nix services.hermes-agent.extraPythonPackages = [ (pkgs.python312Packages.buildPythonPackage { pname = "my-plugin"; version = "1.0.0"; src = pkgs.fetchFromGitHub { owner = "you"; repo = "hermes-my-plugin"; rev = "v1.0.0"; hash = "sha256-..."; # nix-prefetch-url --unpack }; format = "pyproject"; build-system = [ pkgs.python312Packages.setuptools ]; }) ]; ``` -------------------------------- ### Symlink manual installation entrypoint Source: https://hermes-agent.nousresearch.com/docs/developer-guide/contributing Create a symlink to the local hermes binary if using the manual clone fallback. ```bash mkdir -p ~/.local/bin ln -sf "$(pwd)/venv/bin/hermes" ~/.local/bin/hermes ``` -------------------------------- ### Defining Setup Schema Source: https://hermes-agent.nousresearch.com/docs/developer-guide/browser-provider-plugin Override get_setup_schema to provide configuration prompts and installation hooks within the hermes tools interface. ```python def get_setup_schema(self) -> dict: return { "name": "My Backend", "badge": "paid", "tag": "Cloud browser with stealth and proxies", "env_vars": [ {"key": "MY_BACKEND_API_KEY", "prompt": "My Backend API key", "url": "https://mybackend.example"}, ], "post_setup": "agent_browser", # ensures local Chromium is installed (agent-browser itself resolves via npx) } ``` -------------------------------- ### Perform manual clone and environment setup Source: https://hermes-agent.nousresearch.com/docs/developer-guide/contributing Manually clone the repository and create a virtual environment outside the source tree to prevent accidental deletion. ```bash git clone https://github.com/NousResearch/hermes-agent.git cd hermes-agent # Create venv with Python 3.11, OUTSIDE the source tree uv venv ~/.hermes/venvs/hermes-dev --python 3.11 export VIRTUAL_ENV="$HOME/.hermes/venvs/hermes-dev" export PATH="$VIRTUAL_ENV/bin:$PATH" # Install with all extras (messaging, cron, CLI menus, dev tools) uv pip install -e ".[all,dev]" # Optional: browser tools npm install ``` -------------------------------- ### Manual Configuration Setup Source: https://hermes-agent.nousresearch.com/docs/developer-guide/creating-skills Command-line interface command to manually set a configuration value for a skill. ```bash hermes config set skills.config.myplugin.path ~/my-data ``` -------------------------------- ### SessionDB Usage Examples Source: https://hermes-agent.nousresearch.com/docs/developer-guide/session-storage Code examples for initializing SessionDB, managing sessions, storing and retrieving messages, and handling session titles. ```APIDOC ### Initialize ```python from hermes_state import SessionDB db = SessionDB() # Default: ~/.hermes/state.db db = SessionDB(db_path=Path("/tmp/test.db")) # Custom path ``` ### Create and Manage Sessions ```python # Create a new session db.create_session( session_id="sess_abc123", source="cli", model="anthropic/claude-sonnet-4.6", user_id="user_1", parent_session_id=None, # or previous session ID for lineage ) # End a session db.end_session("sess_abc123", end_reason="user_exit") # Reopen a session (clear ended_at/end_reason) db.reopen_session("sess_abc123") ``` ### Store Messages ```python msg_id = db.append_message( session_id="sess_abc123", role="assistant", content="Here's the answer...", tool_calls=[{"id": "call_1", "function": {"name": "terminal", "arguments": "{}"}}], token_count=150, finish_reason="stop", reasoning="Let me think about this...", ) ``` ### Retrieve Messages ```python # Raw messages with all metadata messages = db.get_messages("sess_abc123") # OpenAI conversation format (for API replay) conversation = db.get_messages_as_conversation("sess_abc123") # Returns: [{"role": "user", "content": "..."}, {"role": "assistant", ...}] ``` ### Session Titles ```python # Set a title (must be unique among non-NULL titles) db.set_session_title("sess_abc123", "Fix Docker Build") # Resolve by title (returns most recent in lineage) session_id = db.resolve_session_by_title("Fix Docker Build") # Auto-generate next title in lineage next_title = db.get_next_title_in_lineage("Fix Docker Build") # Returns: "Fix Docker Build #2" ``` ``` -------------------------------- ### Multi-provider config for web search and extract Source: https://hermes-agent.nousresearch.com/docs/developer-guide/web-search-provider-plugin Example configuration for a multi-provider setup. Sets search_backend to brave-free and extract_backend to firecrawl. When these are unset, both fall through to web.backend, and if that is also unset, Hermes picks the first available provider based on env-var presence. ```yaml # ~/.hermes/config.yaml web: search_backend: "brave-free" # search-only, fast, free 2k/mo extract_backend: "firecrawl" # extract + crawl, paid quota ``` -------------------------------- ### plugin.yaml Example Source: https://hermes-agent.nousresearch.com/docs/developer-guide/adding-platform-adapters Example plugin.yaml with metadata, requires_env, and optional_env blocks. The requires_env and optional_env auto-populate hermes config UI entries. ```yaml name: my-platform label: My Platform kind: platform version: 1.0.0 description: My custom messaging platform adapter author: Your Name requires_env: - MY_PLATFORM_TOKEN # bare string works - name: MY_PLATFORM_CHANNEL # or rich dict for better UX description: "Channel to join" prompt: "Channel" password: false optional_env: - name: MY_PLATFORM_HOME_CHANNEL description: "Default channel for cron delivery" password: false ``` -------------------------------- ### Test Hermes plugin: start, prompts, /plugins, output Source: https://hermes-agent.nousresearch.com/docs/developer-guide/plugins Start Hermes, try calculator prompts, check plugin status with /plugins, and see the expected output showing the calculator plugin loaded. ```bash hermes ``` ```text What's 2 to the power of 16? Convert 100 fahrenheit to celsius What's the square root of 2 times pi? How many gigabytes is 1.5 terabytes? ``` ```text /plugins ``` ```text Plugins (1): ✓ calculator v1.0.0 (2 tools, 1 hooks) ``` -------------------------------- ### Conventional Commit Examples Source: https://hermes-agent.nousresearch.com/docs/developer-guide/contributing Examples of properly formatted commit messages using defined types and scopes. ```text fix(cli): prevent crash in save_config_value when model is a string feat(gateway): add WhatsApp multi-user session isolation fix(security): prevent shell injection in sudo password piping ``` -------------------------------- ### Install and Manage Blueprint Automations Source: https://hermes-agent.nousresearch.com/docs/developer-guide/creating-skills Use the Hermes CLI to install a blueprint and manage its suggested cron job status. ```bash hermes skills install owner/morning-brief # → Blueprint: 'morning-brief' is an automation (schedule 0 8 * * *). # Added to your suggestions — run /suggestions to schedule or dismiss it. # then, in a session: /suggestions # lists pending suggestions, numbered /suggestions accept 1 # creates the cron job /suggestions dismiss 1 # never offer it again ``` -------------------------------- ### Configure Browser Provider Source: https://hermes-agent.nousresearch.com/docs/developer-guide/browser-provider-plugin Example YAML configuration for specifying a cloud provider backend. ```yaml browser: cloud_provider: my-backend ``` -------------------------------- ### Install plugin with pip Source: https://hermes-agent.nousresearch.com/docs/developer-guide/plugins Install a plugin package with pip. The plugin is automatically discovered on the next Hermes startup. ```bash pip install hermes-plugin-calculator # Plugin auto-discovered on next hermes startup ``` -------------------------------- ### config.yaml - Platform configuration Source: https://hermes-agent.nousresearch.com/docs/developer-guide/adding-platform-adapters Example YAML configuration for enabling the my_platform adapter and setting extra options like token and channel. Users can also configure via environment variables that the adapter reads in __init__. ```yaml gateway: platforms: my_platform: enabled: true extra: token: "..." channel: "#general" ``` -------------------------------- ### Add and use a custom skills tap Source: https://hermes-agent.nousresearch.com/docs/developer-guide/plugins Add a custom skill registry as a tap, then search and install skills from it. A tap is just a GitHub repo with skills//SKILL.md directories. ```bash hermes skills tap add myorg/skills-repo hermes skills search my-workflow --source myorg/skills-repo hermes skills install myorg/skills-repo/my-workflow ``` -------------------------------- ### NixOS directory plugin configuration Source: https://hermes-agent.nousresearch.com/docs/developer-guide/plugins Declarative NixOS installation for directory plugins that do not require a pyproject.toml. Add this to your configuration.nix to fetch and install a plugin directly from GitHub. ```nix services.hermes-agent.extraPlugins = [ (pkgs.fetchFromGitHub { owner = "you"; repo = "hermes-my-plugin"; rev = "v1.0.0"; hash = "sha256-..."; }) ]; ``` -------------------------------- ### Add a custom repository as a tap Source: https://hermes-agent.nousresearch.com/docs/developer-guide/creating-skills Register a custom repository so that users can search and install skills from it. ```bash hermes skills tap add owner/repo ``` -------------------------------- ### plugin.yaml manifest v2 example Source: https://hermes-agent.nousresearch.com/docs/developer-guide/plugins A complete v2 manifest example showing all optional fields: manifest_version, api_version, license, homepage, tags, requires_plugins, python_dependencies, and config_schema. Use it as a template for a plugin that targets the v2 schema. Note that python_dependencies are only surfaced and never auto-installed, and requires_plugins entries are advisory. ```yaml # plugin.yaml — manifest v2 example name: my-plugin version: 1.2.0 manifest_version: 2 api_version: 1 license: MIT homepage: https://github.com/owner/my-plugin tags: [gateway, demo] requires_plugins: - id: other-plugin version_range: ">=1.0,<2" python_dependencies: - "somepkg>=1.0,<2" # surfaced, never auto-installed config_schema: api_url: {type: str, default: "", description: "Service endpoint"} ``` -------------------------------- ### Packaged Provider Entry Point Configuration Source: https://hermes-agent.nousresearch.com/docs/developer-guide/memory-provider-plugin Example configuration for a pyproject.toml file to register a memory provider via the hermes_agent.memory_providers entry point. ```toml [project.entry-points."hermes_agent.memory_providers"] my-provider = "my_provider:register" ``` -------------------------------- ### CLI startup caching status Source: https://hermes-agent.nousresearch.com/docs/developer-guide/context-compression-and-caching Example CLI output showing that prompt caching is enabled, including the model provider and TTL. ```text 💾 Prompt caching: ENABLED (Claude via OpenRouter, 5m TTL) ``` -------------------------------- ### Provider:model syntax examples Source: https://hermes-agent.nousresearch.com/docs/developer-guide/adding-providers Shows the provider:model syntax that must be supported by the catalog in hermes_cli/models.py. If aliases are missing here, the provider may authenticate correctly but still fail in /model parsing. ```text anthropic:claude-sonnet-4-6 kimi:model-name ``` -------------------------------- ### Create a platform plugin adapter (myplatform) Source: https://hermes-agent.nousresearch.com/docs/developer-guide/plugins Create a platform plugin adapter and its plugin.yaml manifest. The adapter implements the BasePlatformAdapter contract and registers via ctx.register_platform; the manifest declares required and optional environment variables. The env_enablement_fn auto-populates PlatformConfig.extra so env-only setups appear in `hermes gateway status`. ```python # plugins/platforms/myplatform/adapter.py from gateway.platforms.base import BasePlatformAdapter class MyPlatformAdapter(BasePlatformAdapter): async def connect(self): ... async def send(self, chat_id, text): ... async def disconnect(self): ... def check_requirements(): import os return bool(os.environ.get("MYPLATFORM_TOKEN")) def _env_enablement(): import os tok = os.getenv("MYPLATFORM_TOKEN", "").strip() if not tok: return None return {"token": tok} def register(ctx): ctx.register_platform( name="myplatform", label="MyPlatform", adapter_factory=lambda cfg: MyPlatformAdapter(cfg), check_fn=check_requirements, required_env=["MYPLATFORM_TOKEN"], # Auto-populate PlatformConfig.extra from env so env-only setups # show up in `hermes gateway status` without SDK instantiation. env_enablement_fn=_env_enablement, # Opt in to cron delivery: `deliver=myplatform` routes to this var. cron_deliver_env_var="MYPLATFORM_HOME_CHANNEL", emoji="💬", platform_hint="You are chatting via MyPlatform. Keep responses concise.", ) ``` ```yaml # plugins/platforms/myplatform/plugin.yaml name: myplatform-platform label: MyPlatform kind: platform version: 1.0.0 description: MyPlatform gateway adapter requires_env: - name: MYPLATFORM_TOKEN description: "Bot token from the MyPlatform console" password: true optional_env: - name: MYPLATFORM_HOME_CHANNEL description: "Default channel for cron delivery" password: false ``` -------------------------------- ### Skill Configuration Storage Format Source: https://hermes-agent.nousresearch.com/docs/developer-guide/creating-skills Example of how configuration values are persisted within the skills.config namespace in config.yaml. ```yaml skills: config: myplugin: path: ~/my-data ``` -------------------------------- ### Define tool schemas in schemas.py Source: https://hermes-agent.nousresearch.com/docs/developer-guide/plugins Create schemas.py with tool schemas for the LLM. The example defines CALCULATE and UNIT_CONVERT schemas, each with a name, description, and parameters. The description field is critical for the LLM to decide when to use the tool. ```python """Tool schemas — what the LLM sees.""" CALCULATE = { "name": "calculate", "description": ( "Evaluate a mathematical expression and return the result. " "Supports arithmetic (+, -, *, /, **), functions (sqrt, sin, cos, " "log, abs, round, floor, ceil), and constants (pi, e). " "Use this for any math the user asks about." ), "parameters": { "type": "object", "properties": { "expression": { "type": "string", "description": "Math expression to evaluate (e.g., '2**10', 'sqrt(144)')", }, }, "required": ["expression"], }, } UNIT_CONVERT = { "name": "unit_convert", "description": ( "Convert a value between units. Supports length (m, km, mi, ft, in), " "weight (kg, lb, oz, g), temperature (C, F, K), data (B, KB, MB, GB, TB), " "and time (s, min, hr, day)." ), "parameters": { "type": "object", "properties": { "value": { "type": "number", "description": "The numeric value to convert", }, "from_unit": { "type": "string", "description": "Source unit (e.g., 'km', 'lb', 'F', 'GB')", }, "to_unit": { "type": "string", "description": "Target unit (e.g., 'mi', 'kg', 'C', 'MB')", }, }, "required": ["value", "from_unit", "to_unit"], }, } ``` -------------------------------- ### Lazy-install optional dependencies with tools.lazy_deps.ensure Source: https://hermes-agent.nousresearch.com/docs/developer-guide/plugins Use this pattern inside a tool handler to lazily install an optional SDK on first use. The feature key must be in the in-tree LAZY_DEPS allowlist, and specs are PyPI-by-name only. If security.allow_lazy_installs is false, ensure() raises FeatureUnavailable; catch it and return an error result. ```python # tools.py from tools.lazy_deps import ensure, FeatureUnavailable def my_tool_handler(args, **kwargs): try: ensure("my-plugin.my-backend") # key must be in LAZY_DEPS except FeatureUnavailable as exc: return {"error": str(exc)} import my_backend_sdk # safe now ... ``` -------------------------------- ### CLI registration for memory provider plugin Source: https://hermes-agent.nousresearch.com/docs/developer-guide/memory-provider-plugin Example `cli.py` for a memory provider plugin. It defines a `register_cli(subparser)` function that builds an argparse tree with `status` and `config` subcommands, and a handler that prints appropriate messages. The CLI is discovered at startup via `discover_plugin_cli_commands()` and only appears when the provider is active. ```python # plugins/memory/my-provider/cli.py def my_command(args): """Handler dispatched by argparse.""" sub = getattr(args, "my_command", None) if sub == "status": print("Provider is active and connected.") elif sub == "config": print("Showing config...") else: print("Usage: hermes my-provider ") def register_cli(subparser) -> None: """Build the hermes my-provider argparse tree. Called by discover_plugin_cli_commands() at argparse setup time. """ subs = subparser.add_subparsers(dest="my_command") subs.add_parser("status", help="Show provider status") subs.add_parser("config", help="Show provider config") subparser.set_defaults(func=my_command) ``` -------------------------------- ### Directory structure for memory provider plugin Source: https://hermes-agent.nousresearch.com/docs/developer-guide/memory-provider-plugin Directory structure for a memory provider plugin with CLI support. It shows the required files: `__init__.py` for the provider implementation, `plugin.yaml` for metadata, `cli.py` for CLI commands, and `README.md` for setup instructions. ```text plugins/memory/my-provider/ ├── __init__.py # MemoryProvider implementation + register() ├── plugin.yaml # Metadata ├── cli.py # register_cli(subparser) — CLI commands └── README.md # Setup instructions ``` -------------------------------- ### DM Pairing Flow Example Source: https://hermes-agent.nousresearch.com/docs/developer-guide/gateway-internals Example multi-line exchange showing the DM pairing flow: Admin invokes /pair to get a pairing code, which is shared with the new user; the new user submits the code and is authorized. ```text Admin: /pair Gateway: "Pairing code: ABC123. Share with the user." New user: ABC123 Gateway: "Paired! You're now authorized." ``` -------------------------------- ### ACP CLI commands Source: https://hermes-agent.nousresearch.com/docs/developer-guide/programmatic-integration Commands to start the ACP server, verify dependencies, and run interactive setup for terminal authentication. ```bash hermes acp # serve ACP on stdio hermes acp --check # verify ACP dependencies and adapter imports hermes acp --setup # interactive provider/model setup for ACP terminal auth ``` -------------------------------- ### Plugin Registration Entry Point Source: https://hermes-agent.nousresearch.com/docs/developer-guide/browser-provider-plugin The __init__.py file serves as the registration entry point for the plugin. ```python from plugins.browser.my_backend.provider import MyBackendProvider def register(ctx) -> None: ctx.register_browser_provider(MyBackendProvider()) ``` -------------------------------- ### Create plugin directory with mkdir and cd Source: https://hermes-agent.nousresearch.com/docs/developer-guide/plugins Creates the plugin directory and changes into it. Run these commands before writing the manifest and other plugin files. ```bash mkdir -p ~/.hermes/plugins/calculator cd ~/.hermes/plugins/calculator ``` -------------------------------- ### plugin_api.py backend routes with APIRouter Source: https://hermes-agent.nousresearch.com/docs/developer-guide/desktop-plugin-sdk Example plugin_api.py backend for a desktop plugin. Routes mount under /api/plugins// (e.g., GET /api/plugins//board). Backend code runs inside the gateway process and can import from the hermes-agent codebase directly. The Python backend is gated separately: it is imported only when the plugin is in the plugins.enabled allow-list in config.yaml (and not in plugins.disabled). ```python # plugin_api.py from fastapi import APIRouter router = APIRouter() @router.get("/board") async def board(): return {"items": ["one", "two", "three"]} @router.post("/action") async def action(body: dict): return {"ok": True, "received": body} ``` -------------------------------- ### Configure development environment Source: https://hermes-agent.nousresearch.com/docs/developer-guide/contributing Initialize the configuration directory and set up the environment variables required for operation. ```bash mkdir -p ~/.hermes/{cron,sessions,logs,memories,skills} cp cli-config.yaml.example ~/.hermes/config.yaml touch ~/.hermes/.env # Add at minimum an LLM provider key: echo 'OPENROUTER_API_KEY=sk-or-v1-your-key' >> ~/.hermes/.env ``` -------------------------------- ### Initialize SessionDB (default and custom path) Source: https://hermes-agent.nousresearch.com/docs/developer-guide/session-storage Initializes a SessionDB instance. The default path is ~/.hermes/state.db; pass a Path to use a custom location. ```python from hermes_state import SessionDB db = SessionDB() # Default: ~/.hermes/state.db db = SessionDB(db_path=Path("/tmp/test.db")) # Custom path ``` -------------------------------- ### Create hello plugin.js with jsx()/jsxs() and register pane and chip Source: https://hermes-agent.nousresearch.com/docs/developer-guide/desktop-plugin-sdk Create this file at $HERMES_HOME/desktop-plugins/hello/plugin.js (default ~/.hermes/desktop-plugins/hello/plugin.js). The folder name must equal the plugin id ('hello'). The file is loaded uncompiled, so JSX syntax will not parse; use jsx()/jsxs() from react/jsx-runtime. Only @hermes/plugin-sdk, react, and react/jsx-runtime are importable. The app watches desktop-plugins/, loads within a few seconds, and hot-reloads on save; if it doesn't appear, run ⌘K → Reload desktop plugins, and if loading fails a toast names the error. ```javascript // ~/.hermes/desktop-plugins/hello/plugin.js import { host, haptic, useValue } from '@hermes/plugin-sdk' import { jsx, jsxs } from 'react/jsx-runtime' function HelloPane() { const gateway = useValue(host.state.gateway) return jsxs('div', { className: 'flex h-full flex-col gap-2 p-3 text-sm', children: [ jsx('div', { className: 'font-medium', children: 'Hello, Hermes' }), jsx('div', { className: 'text-(--ui-text-tertiary)', children: `gateway: ${gateway}` }) ] }) } export default { id: 'hello', // must match the folder name name: 'Hello', register(ctx) { ctx.register({ id: 'pane', area: 'panes', title: 'hello', data: { placement: 'right', width: '260px' }, render: () => jsx(HelloPane, {}) }) ctx.register({ id: 'chip', area: 'statusBar.right', order: 130, render: () => jsx('button', { type: 'button', className: 'px-1.5 text-[0.6875rem] text-(--ui-text-tertiary)', onClick: () => { haptic('tap') host.notify({ kind: 'info', message: 'Hello from my plugin!' }) }, children: 'hello' }) }) } } ``` -------------------------------- ### GET /v1/runs/{id} Source: https://hermes-agent.nousresearch.com/docs/developer-guide/programmatic-integration Get the status of a run by its ID. ```APIDOC ## GET /v1/runs/{id} ### Description Get the status of a run by its ID. ### Method GET ### Endpoint /v1/runs/{id} ### Parameters #### Path Parameters - **id** (string) - Required - The run ID. ### Response #### Success Response (200) - **status** (string) - The current status of the run. ``` -------------------------------- ### _get_extra_tui_widgets() custom widget example Source: https://hermes-agent.nousresearch.com/docs/developer-guide/extending-the-cli An example of using ConditionalContainer to add a toggleable status widget to the TUI. ```python from prompt_toolkit.layout import ConditionalContainer, Window, FormattedTextControl from prompt_toolkit.filters import Condition def _get_extra_tui_widgets(self): return [ ConditionalContainer( Window(FormattedTextControl("Status: connected"), height=1), filter=Condition(lambda: self._show_status), ), ] ``` -------------------------------- ### Implement MemoryProvider plugin (plugins/memory/my-memory/__init__.py) Source: https://hermes-agent.nousresearch.com/docs/developer-guide/plugins Drop this implementation into plugins/memory// to add a cross-session memory backend. Memory providers are single-select; only one is active at a time, chosen via memory.provider in config.yaml. If the provider also loads as a general plugin, general discovery owns its lifecycle hooks and the memory loader supplies hooks only as a fallback until that plugin source loads successfully. ```python # plugins/memory/my-memory/__init__.py from agent.memory_provider import MemoryProvider class MyMemoryProvider(MemoryProvider): @property def name(self) -> str: return "my-memory" def is_available(self) -> bool: import os return bool(os.environ.get("MY_MEMORY_API_KEY")) def initialize(self, session_id: str, **kwargs) -> None: self._session_id = session_id def sync_turn(self, user_content, assistant_content, *, session_id="", messages=None) -> None: ... def prefetch(self, query, *, session_id="") -> str: ... def get_tool_schemas(self) -> list[dict]: return [] # required @abstractmethod — see full guide def register(ctx): ctx.register_memory_provider(MyMemoryProvider()) ``` -------------------------------- ### Assembled system prompt with all layers Source: https://hermes-agent.nousresearch.com/docs/developer-guide/prompt-assembly Shows the assembled system prompt with all ten layers present. Each layer is labeled with its source (e.g., ~/.hermes/SOUL.md, config, API, memory, user profile, skills index, project context, timestamp, platform hint). The example includes placeholder content for Honcho and user-configured overrides, and notes that tool-use enforcement applies only to GPT/Codex models. ```text # Layer 1: Agent Identity (from ~/.hermes/SOUL.md) You are Hermes, an AI assistant created by Nous Research. You are an expert software engineer and researcher. You value correctness, clarity, and efficiency. ... # Layer 2: Tool-aware behavior guidance Task-learned procedures, pitfalls, and task-specific preferences belong in skills. Memory is the narrow exception for facts that apply to EVERY session regardless of task. Skill-writing instructions appear here only when skill_manage is available; its absence does not widen memory's scope. ... When the user references something from a past conversation or you suspect relevant cross-session context exists, use session_search to recall it before asking them to repeat themselves. # Tool-use enforcement (for GPT/Codex models only) You MUST use your tools to take action — do not describe what you would do or plan to do without actually doing it. ... # Layer 3: Honcho static block (when active) [Honcho personality/context data] # Layer 4: Optional system message (from config or API) [User-configured system message override] # Layer 5: Frozen MEMORY snapshot ## Persistent Memory - User prefers Python 3.12, uses pyproject.toml - Default editor is nvim - Working on project "atlas" in ~/code/atlas - Timezone: US/Pacific # Layer 6: Frozen USER profile snapshot ## User Profile - Name: Alice - GitHub: alice-dev # Layer 7: Skills index ## Skills (mandatory) Before replying, scan the skills below. If one clearly matches your task, load it with skill_view(name) and follow its instructions. ... software-development: - code-review: Structured code review workflow - test-driven-development: TDD methodology research: - arxiv: Search and summarize arXiv papers # Layer 8: Context files (from project directory) # Project Context The following project context files have been loaded and should be followed: ## AGENTS.md This is the atlas project. Use pytest for testing. The main entry point is src/atlas/main.py. Always run `make lint` before committing. # Layer 9: Timestamp + session Current time: 2026-03-30T14:30:00-07:00 Session: abc123 # Layer 10: Platform hint You are a CLI AI Agent. Try not to use markdown but simple text renderable inside a terminal. ``` -------------------------------- ### Test plugin with temporary HERMES_HOME Source: https://hermes-agent.nousresearch.com/docs/developer-guide/model-provider-plugin Sets up a temporary HERMES_HOME, creates a plugin that registers a provider, and runs a test command. Requires setting MY_API_KEY. ```bash export HERMES_HOME=/tmp/hermes-plugin-test mkdir -p $HERMES_HOME/plugins/model-providers/my-provider cat > $HERMES_HOME/plugins/model-providers/my-provider/__init__.py <<'EOF' from providers import register_provider from providers.base import ProviderProfile register_provider(ProviderProfile( name="my-provider", env_vars=("MY_API_KEY",), base_url="https://api.my-provider.example.com/v1", auth_type="api_key", )) EOF export MY_API_KEY=your-test-key hermes -z "hello" --provider my-provider -m some-model ``` -------------------------------- ### Initialize hgui desktop environment Source: https://hermes-agent.nousresearch.com/docs/developer-guide/worktree-ui-dev This shell function configures the environment, manages dependency linking, and launches the Vite development server for the Hermes desktop app. ```bash hgui() { local root deps desktop root="$(_hermes_root)" || { echo "hgui: not in a Hermes checkout" >&2; return 1; } deps="${HERMES_GUI_DEPS_CHECKOUT:-$HERMES_MAIN_CHECKOUT}" desktop="$root/apps/desktop" # Borrow deps when locks match; otherwise install locally in the worktree. if cmp -s "$root/package-lock.json" "$deps/package-lock.json"; then _hermes_link_deps "$desktop" "$deps/apps/desktop" _hermes_link_deps "$root" "$deps" else ( cd "$root" && npm ci ) || return 1 fi # Vite is fixed at 5174 — evict a stale session from another hgui. lsof -t -i:5174 >/dev/null 2>&1 && killport 5174 # Electron often survives Ctrl+C without reaping its ephemeral backends. trap '_hermes_gui_cleanup "$root"' INT TERM EXIT ( cd "$desktop" export PATH="$root/node_modules/.bin:$PATH" HERMES_DESKTOP_HERMES_ROOT="$root" \ HERMES_DESKTOP_PYTHON="$HERMES_MAIN_CHECKOUT/.venv/bin/python" \ HERMES_DESKTOP_IGNORE_EXISTING=1 \ HERMES_DESKTOP_CWD="$root" \ npm run dev ) } ``` -------------------------------- ### Enable and Configure the Plugin via CLI Source: https://hermes-agent.nousresearch.com/docs/developer-guide/terminal-environment-plugin Commands to activate the plugin and set it as the active terminal backend. ```bash hermes plugins enable acmebox hermes config set terminal.backend acmebox ``` -------------------------------- ### __init__.py - register() entry point Source: https://hermes-agent.nousresearch.com/docs/developer-guide/web-search-provider-plugin Plugin entry point that registers the provider. The register function is called once at load time and uses ctx.register_web_search_provider to add the provider instance. ```python # plugins/web/my-backend/__init__.py from plugins.web.my_backend.provider import MyBackendWebSearchProvider def register(ctx) -> None: """Plugin entry point — called once at load time.""" ctx.register_web_search_provider(MyBackendWebSearchProvider()) ``` -------------------------------- ### GET /health Source: https://hermes-agent.nousresearch.com/docs/developer-guide/programmatic-integration Health check endpoint. ```APIDOC ## GET /health ### Description Health check endpoint. ### Method GET ### Endpoint /health ``` -------------------------------- ### GET /v1/browser-control/ws Source: https://hermes-agent.nousresearch.com/docs/developer-guide/programmatic-integration Browser-controller WebSocket endpoint. ```APIDOC ## GET /v1/browser-control/ws ### Description Browser-controller WebSocket endpoint. ### Method GET ### Endpoint /v1/browser-control/ws ``` -------------------------------- ### GET /v1/capabilities Source: https://hermes-agent.nousresearch.com/docs/developer-guide/programmatic-integration Machine-readable feature flags. ```APIDOC ## GET /v1/capabilities ### Description Machine-readable feature flags. ### Method GET ### Endpoint /v1/capabilities ``` -------------------------------- ### Launch and Wait for a Subagent Source: https://hermes-agent.nousresearch.com/docs/developer-guide/subagent-lifecycle-api Demonstrates initializing a subagent request, launching it within an active agent context, and waiting for the result. ```python from agent.subagent_lifecycle import SubagentLaunchRequest def launch_review(ctx): # Call from a plugin tool or hook while an agent turn is active. service = ctx.subagent_lifecycle handle = service.launch(SubagentLaunchRequest( goal="Review this change for regressions.", context="Only inspect the supplied repository.", role="leaf", correlation_id="review-42", allowed_toolsets=("file",), )) # Persist handle.to_dict() if desired. if service.wait(handle, timeout_seconds=2).timed_out: return handle.to_dict() return service.result(handle) ``` -------------------------------- ### Implement ContextEngine plugin (plugins/context_engine/my-engine/__init__.py) Source: https://hermes-agent.nousresearch.com/docs/developer-guide/plugins Drop this implementation into plugins/context_engine// to replace the context compressor. Context engines are single-select, chosen via context.engine in config.yaml. ```python # plugins/context_engine/my-engine/__init__.py from agent.context_engine import ContextEngine class MyContextEngine(ContextEngine): @property def name(self) -> str: return "my-engine" def update_from_response(self, usage) -> None: ... def should_compress(self, prompt_tokens: int = None) -> bool: ... def compress(self, messages, current_tokens=None, focus_topic=None, force=False, memory_context="") -> list: ... def register(ctx): ctx.register_context_engine(MyContextEngine()) ``` -------------------------------- ### Define env vars in plugin.yaml for hermes config Source: https://hermes-agent.nousresearch.com/docs/developer-guide/adding-platform-adapters Define required and optional environment variables for a platform adapter in plugin.yaml. The CLI setup UI picks up descriptions, prompts, password flags, and URLs automatically. Supported dict keys: name (required), description, prompt, url, password (bool; auto-detected from *_TOKEN / *_SECRET / *_KEY / *_PASSWORD / *_JSON suffix when omitted), category (defaults to "messaging"). Bare-string entries still work but get a generic description auto-derived from the plugin's label; hardcoded entries in OPTIONAL_ENV_VARS take precedence. ```yaml # plugins/platforms/my_platform/plugin.yaml name: my_platform-platform label: My Platform kind: platform version: 1.0.0 description: > My Platform gateway adapter for Hermes Agent. author: Your Name requires_env: - name: MY_PLATFORM_TOKEN description: "Bot API token from the My Platform console" prompt: "My Platform bot token" url: "https://my-platform.example.com/bots" password: true - name: MY_PLATFORM_CHANNEL description: "Channel to join (e.g. #hermes)" prompt: "Channel" password: false optional_env: - name: MY_PLATFORM_HOME_CHANNEL description: "Default channel for cron delivery (defaults to MY_PLATFORM_CHANNEL)" prompt: "Home channel (or empty)" password: false - name: MY_PLATFORM_ALLOWED_USERS description: "Comma-separated user IDs allowed to talk to the bot" prompt: "Allowed users (comma-separated)" password: false ``` -------------------------------- ### GET /health/detailed Source: https://hermes-agent.nousresearch.com/docs/developer-guide/programmatic-integration Detailed health check endpoint. ```APIDOC ## GET /health/detailed ### Description Detailed health check endpoint. ### Method GET ### Endpoint /health/detailed ``` -------------------------------- ### GET /v1/runs/{id}/events Source: https://hermes-agent.nousresearch.com/docs/developer-guide/programmatic-integration SSE stream of lifecycle events for a run. ```APIDOC ## GET /v1/runs/{id}/events ### Description SSE stream of lifecycle events for a run. ### Method GET ### Endpoint /v1/runs/{id}/events ### Parameters #### Path Parameters - **id** (string) - Required - The run ID. ### Response #### Success Response (200) - SSE stream of lifecycle events. ``` -------------------------------- ### POST /v1/runs Source: https://hermes-agent.nousresearch.com/docs/developer-guide/programmatic-integration Start a run and receive a run_id. Returns 202 Accepted. ```APIDOC ## POST /v1/runs ### Description Start a run and receive a run_id. Returns 202 Accepted. ### Method POST ### Endpoint /v1/runs ### Response #### Success Response (202) - **run_id** (string) - The identifier of the started run. ```