commit 51c86df69007de1fd1e3f4959e0f9424764f9a58 Author: wehub-resource-sync Date: Mon Jul 13 13:36:53 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.agents/skills/electron-pro/SKILL.md b/.agents/skills/electron-pro/SKILL.md new file mode 100644 index 0000000..76b857c --- /dev/null +++ b/.agents/skills/electron-pro/SKILL.md @@ -0,0 +1,163 @@ +--- +name: electron-pro +description: Expert in building cross-platform desktop applications using web technologies (HTML/CSS/JS) with the Electron framework. +--- + +# Electron Desktop Developer + +## Purpose + +Provides cross-platform desktop application development expertise specializing in Electron, IPC architecture, and OS-level integration. Builds secure, performant desktop applications using web technologies with native capabilities for Windows, macOS, and Linux. + +## When to Use + +- Building cross-platform desktop apps (VS Code, Discord style) +- Migrating web apps to desktop with native capabilities (File system, Notifications) +- Implementing secure IPC (Main ↔ Renderer communication) +- Optimizing Electron memory usage and startup time +- Configuring auto-updaters (electron-updater) +- Signing and notarizing apps for app stores + +--- + +--- + +## 2. Decision Framework + +### Architecture Selection + +``` +How to structure the app? +│ +├─ **Security First (Recommended)** +│ ├─ Context Isolation? → **Yes** (Standard since v12) +│ ├─ Node Integration? → **No** (Never in Renderer) +│ └─ Preload Scripts? → **Yes** (Bridge API) +│ +├─ **Data Persistence** +│ ├─ Simple Settings? → **electron-store** (JSON) +│ ├─ Large Datasets? → **SQLite** (`better-sqlite3` in Main process) +│ └─ User Files? → **Native File System API** +│ +└─ **UI Framework** + ├─ React/Vue/Svelte? → **Yes** (Standard SPA approach) + ├─ Multiple Windows? → **Window Manager Pattern** + └─ System Tray App? → **Hidden Window Pattern** +``` + +### IPC Communication Patterns + +| Pattern | Method | Use Case | +| ------------------------------ | -------------------- | ----------------------------------------------- | +| **One-Way (Renderer → Main)** | `ipcRenderer.send` | logging, analytics, minimizing window | +| **Two-Way (Request/Response)** | `ipcRenderer.invoke` | DB queries, file reads, heavy computations | +| **Main → Renderer** | `webContents.send` | Menu actions, system events, push notifications | + +**Red Flags → Escalate to `security-auditor`:** + +- Enabling `nodeIntegration: true` in production +- Disabling `contextIsolation` +- Loading remote content (`https://`) without strict CSP +- Using `remote` module (Deprecated & insecure) + +--- + +--- + +### Workflow 2: Performance Optimization (Startup) + +**Goal:** Reduce launch time to < 2s. + +**Steps:** + +1. **V8 Snapshot** + - Use `electron-link` or `v8-compile-cache` to pre-compile JS. + +2. **Lazy Loading Modules** + - Don't `require()` everything at top of `main.ts`. + + ```javascript + // Bad + import { heavyLib } from "heavy-lib"; + + // Good + ipcMain.handle("do-work", () => { + const heavyLib = require("heavy-lib"); + heavyLib.process(); + }); + ``` + +3. **Bundle Main Process** + - Use `esbuild` or `webpack` for Main process (not just Renderer) to tree-shake unused code and minify. + +--- + +--- + +## 4. Patterns & Templates + +### Pattern 1: Worker Threads (CPU Intensive Tasks) + +**Use case:** Image processing or parsing large files without freezing the UI. + +```typescript +// main.ts +import { Worker } from "worker_threads"; + +ipcMain.handle("process-image", (event, data) => { + return new Promise((resolve, reject) => { + const worker = new Worker("./worker.js", { workerData: data }); + worker.on("message", resolve); + worker.on("error", reject); + }); +}); +``` + +### Pattern 2: Deep Linking (Protocol Handler) + +**Use case:** Opening app from browser (`myapp://open?id=123`). + +```typescript +// main.ts +if (process.defaultApp) { + if (process.argv.length >= 2) { + app.setAsDefaultProtocolClient("myapp", process.execPath, [ + path.resolve(process.argv[1]), + ]); + } +} else { + app.setAsDefaultProtocolClient("myapp"); +} + +app.on("open-url", (event, url) => { + event.preventDefault(); + // Parse url 'myapp://...' and navigate renderer + mainWindow.webContents.send("navigate", url); +}); +``` + +--- + +--- + +## 6. Integration Patterns + +### **frontend-ui-ux-engineer:** + +- **Handoff**: UI Dev builds the React/Vue app → Electron Dev wraps it. +- **Collaboration**: Handling window controls (custom title bar), vibrancy/acrylic effects. +- **Tools**: CSS `app-region: drag`. + +### **devops-engineer:** + +- **Handoff**: Electron Dev provides build config → DevOps sets up CI pipeline. +- **Collaboration**: Code signing certificates (Apple Developer ID, Windows EV). +- **Tools**: Electron Builder, Notarization scripts. + +### **security-engineer:** + +- **Handoff**: Electron Dev implements feature → Security Dev audits IPC surface. +- **Collaboration**: Defining Content Security Policy (CSP) headers. +- **Tools**: Electronegativity (Scanner). + +--- diff --git a/.agents/skills/hermes-agent/SKILL.md b/.agents/skills/hermes-agent/SKILL.md new file mode 100644 index 0000000..c5b8b50 --- /dev/null +++ b/.agents/skills/hermes-agent/SKILL.md @@ -0,0 +1,1765 @@ +--- +name: hermes-agent +description: Expert in building self-improving AI agents with tool use, multi-platform messaging, and a closed learning loop. Proficient in LLM orchestration, tool integration, session management, and agent autonomy. +--- + +# Hermes Agent - Complete Project Guide (A-Z) + +> **Purpose of this document:** A single, comprehensive reference that explains everything about the Hermes Agent project — its architecture, source code, features, release history, and design patterns — so that any AI or developer can fully understand the system. + +--- + +## Table of Contents + +1. [Project Overview](#1-project-overview) +2. [Key Features Summary](#2-key-features-summary) +3. [Installation & Getting Started](#3-installation--getting-started) +4. [Project Structure](#4-project-structure) +5. [Core Architecture](#5-core-architecture) + - 5.1 [AIAgent Class (run_agent.py)](#51-aiagent-class-run_agentpy) + - 5.2 [Tool Orchestration (model_tools.py)](#52-tool-orchestration-model_toolspy) + - 5.3 [Toolset System (toolsets.py)](#53-toolset-system-toolsetspy) + - 5.4 [Tool Registry (tools/registry.py)](#54-tool-registry-toolsregistrypy) + - 5.5 [Session Database (hermes_state.py)](#55-session-database-hermes_statepy) + - 5.6 [Constants & Home Directory (hermes_constants.py)](#56-constants--home-directory-hermes_constantspy) +6. [CLI System](#6-cli-system) + - 6.1 [Interactive CLI (cli.py)](#61-interactive-cli-clipy) + - 6.2 [CLI Entry Point (hermes_cli/main.py)](#62-cli-entry-point-hermes_climainpy) + - 6.3 [Configuration System (hermes_cli/config.py)](#63-configuration-system-hermes_cliconfigpy) + - 6.4 [Slash Command Registry (hermes_cli/commands.py)](#64-slash-command-registry-hermes_clicommandspy) + - 6.5 [Setup Wizard (hermes_cli/setup.py)](#65-setup-wizard-hermes_clisetupy) + - 6.6 [Model Catalog (hermes_cli/models.py)](#66-model-catalog-hermes_climodelspy) + - 6.7 [Skin/Theme Engine (hermes_cli/skin_engine.py)](#67-skintheme-engine-hermes_cliskin_enginepy) +7. [Tool System](#7-tool-system) + - 7.1 [Terminal Tool (tools/terminal_tool.py)](#71-terminal-tool-toolsterminal_toolpy) + - 7.2 [File Tools (tools/file_tools.py)](#72-file-tools-toolsfile_toolspy) + - 7.3 [Web Tools (tools/web_tools.py)](#73-web-tools-toolsweb_toolspy) + - 7.4 [Browser Tool (tools/browser_tool.py)](#74-browser-tool-toolsbrowser_toolpy) + - 7.5 [Delegate Tool (tools/delegate_tool.py)](#75-delegate-tool-toolsdelegate_toolpy) + - 7.6 [MCP Tool (tools/mcp_tool.py)](#76-mcp-tool-toolsmcp_toolpy) + - 7.7 [Approval System (tools/approval.py)](#77-approval-system-toolsapprovalpy) + - 7.8 [Terminal Backends (tools/environments/)](#78-terminal-backends-toolsenvironments) +8. [Agent Internals](#8-agent-internals) + - 8.1 [Prompt Builder (agent/prompt_builder.py)](#81-prompt-builder-agentprompt_builderpy) + - 8.2 [Context Compressor (agent/context_compressor.py)](#82-context-compressor-agentcontext_compressorpy) + - 8.3 [Prompt Caching (agent/prompt_caching.py)](#83-prompt-caching-agentprompt_cachingpy) + - 8.4 [Auxiliary Client (agent/auxiliary_client.py)](#84-auxiliary-client-agentauxiliary_clientpy) + - 8.5 [Display & Spinner (agent/display.py)](#85-display--spinner-agentdisplaypy) + - 8.6 [Skill Commands (agent/skill_commands.py)](#86-skill-commands-agentskill_commandspy) +9. [Messaging Gateway](#9-messaging-gateway) + - 9.1 [GatewayRunner (gateway/run.py)](#91-gatewayrunner-gatewayrunpy) + - 9.2 [Session Store (gateway/session.py)](#92-session-store-gatewaysessionpy) + - 9.3 [Platform Adapters (gateway/platforms/)](#93-platform-adapters-gatewayplatforms) +10. [Cron Scheduling](#10-cron-scheduling) +11. [Skills System](#11-skills-system) +12. [Plugin System](#12-plugin-system) +13. [Memory System](#13-memory-system) +14. [ACP Server (IDE Integration)](#14-acp-server-ide-integration) +15. [API Server](#15-api-server) +16. [MCP Server Mode](#16-mcp-server-mode) +17. [RL Training Environments](#17-rl-training-environments) +18. [Profiles (Multi-Instance)](#18-profiles-multi-instance) +19. [Security Model](#19-security-model) +20. [Provider & Model System](#20-provider--model-system) +21. [Streaming & Reasoning](#21-streaming--reasoning) +22. [Release History](#22-release-history) +23. [File Dependency Chain](#23-file-dependency-chain) +24. [Key Design Patterns](#24-key-design-patterns) +25. [Configuration Reference](#25-configuration-reference) +26. [Known Pitfalls](#26-known-pitfalls) + +--- + +## 1. Project Overview + +**Hermes Agent** is a self-improving AI agent built by [Nous Research](https://nousresearch.com). It is an open-source (MIT licensed), Python-based project that provides: + +- A **full interactive terminal UI** (CLI) for conversing with LLMs +- A **messaging gateway** supporting 16+ platforms (Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Email, etc.) +- A **closed learning loop** — the agent creates skills from experience, improves them during use, nudges itself to persist knowledge, searches past conversations, and builds a deepening model of who you are +- **40+ built-in tools** — terminal execution, file manipulation, web search, browser automation, code execution, image generation, TTS/STT, and more +- **Any LLM provider** — OpenRouter (200+ models), Nous Portal (400+ models), OpenAI, Anthropic, Hugging Face, GitHub Copilot, z.ai/GLM, Kimi/Moonshot, MiniMax, Alibaba/DashScope, custom endpoints +- **Six terminal backends** — local, Docker, SSH, Modal (serverless), Daytona (serverless), Singularity (HPC) +- **Scheduled automations** via built-in cron scheduler +- **IDE integration** via ACP (Agent Communication Protocol) for VS Code, Zed, JetBrains +- **MCP integration** — both client (connect to any MCP server) and server (expose Hermes to MCP clients) +- **RL training** via Atropos environments for training the next generation of tool-calling models + +**Tech Stack:** + +- Python 3.11+ (core agent, tools, gateway, cron) +- Node.js (browser automation via agent-browser) +- SQLite with WAL mode and FTS5 (session storage, full-text search) +- OpenAI-compatible API (primary inference interface) +- Anthropic SDK (native Anthropic support) +- Rich + prompt_toolkit (CLI rendering) + +**Repository:** `github.com/NousResearch/hermes-agent` +**Version:** 0.7.0 (as of April 2026) +**License:** MIT + +--- + +## 2. Key Features Summary + +| Feature | Description | +| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Terminal UI** | Full TUI with multiline editing, slash-command autocomplete, conversation history, interrupt-and-redirect, streaming tool output | +| **Multi-Platform Messaging** | Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Email, Home Assistant, DingTalk, Feishu/Lark, WeCom, Mattermost, SMS, Webhook — all from a single gateway process | +| **Learning Loop** | Agent-curated memory with periodic nudges, autonomous skill creation, skills self-improve during use, FTS5 session search with LLM summarization, Honcho dialectic user modeling | +| **Scheduled Tasks** | Built-in cron scheduler with delivery to any platform (daily reports, nightly backups, weekly audits) | +| **Subagent Delegation** | Spawn isolated subagents for parallel workstreams with restricted toolsets | +| **Execute Code** | Python scripts that call tools via RPC, collapsing multi-step pipelines into zero-context-cost turns | +| **Terminal Backends** | Local, Docker, SSH, Modal, Daytona, Singularity — run on a $5 VPS or a GPU cluster | +| **Skills** | 70+ bundled skills across 28 categories, Skills Hub for community discovery, agentskills.io compatibility | +| **Plugins** | Drop-in Python plugins with lifecycle hooks (pre_llm_call, post_llm_call, on_session_start, on_session_end) | +| **MCP** | Client (connect to MCP servers for extended tools) and Server (expose conversations to MCP clients) | +| **IDE Integration** | VS Code, Zed, JetBrains via ACP server with session management and tool streaming | +| **API Server** | OpenAI-compatible `/v1/chat/completions` endpoint for headless integrations | +| **Profiles** | Multi-instance support — each profile gets isolated config, memory, sessions, skills, gateway | +| **Security** | Command approval system, secret redaction, SSRF protection, PII redaction, injection detection, credential directory protection | +| **RL Training** | Atropos environments for batch trajectory generation and agent policy optimization | + +--- + +## 3. Installation & Getting Started + +```bash +# One-line install (Linux, macOS, WSL2) +curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash + +# After install +source ~/.bashrc # or: source ~/.zshrc +hermes # start chatting + +# Key commands +hermes model # Choose LLM provider and model +hermes tools # Configure which tools are enabled +hermes config set # Set individual config values +hermes gateway # Start the messaging gateway +hermes setup # Run the full setup wizard +hermes update # Update to latest version +hermes doctor # Diagnose any issues +``` + +**For development:** + +```bash +git clone https://github.com/NousResearch/hermes-agent.git +cd hermes-agent +curl -LsSf https://astral.sh/uv/install.sh | sh +uv venv venv --python 3.11 +source venv/bin/activate +uv pip install -e ".[all,dev]" +python -m pytest tests/ -q # ~3000 tests +``` + +--- + +## 4. Project Structure + +``` +hermes-agent/ +├── run_agent.py # AIAgent class — core conversation loop +├── model_tools.py # Tool orchestration, _discover_tools(), handle_function_call() +├── toolsets.py # Toolset definitions, _HERMES_CORE_TOOLS list +├── toolset_distributions.py # Toolset sampling distributions for RL +├── cli.py # HermesCLI class — interactive CLI orchestrator +├── hermes_state.py # SessionDB — SQLite session store (FTS5 search) +├── hermes_constants.py # Shared constants, get_hermes_home() +├── hermes_time.py # Timezone handling +├── utils.py # Shared utility functions +├── batch_runner.py # Parallel batch processing +├── trajectory_compressor.py # Trajectory compression for RL training +├── mcp_serve.py # MCP server mode entry point +├── mini_swe_runner.py # Minimal SWE benchmark runner +├── rl_cli.py # RL CLI commands +│ +├── agent/ # Agent internals +│ ├── prompt_builder.py # System prompt assembly +│ ├── context_compressor.py # Auto context compression +│ ├── prompt_caching.py # Anthropic prompt caching +│ ├── auxiliary_client.py # Auxiliary LLM client (vision, summarization) +│ ├── model_metadata.py # Model context lengths, token estimation +│ ├── models_dev.py # models.dev registry integration +│ ├── display.py # KawaiiSpinner, tool preview formatting +│ ├── skill_commands.py # Skill slash commands (shared CLI/gateway) +│ └── trajectory.py # Trajectory saving helpers +│ +├── hermes_cli/ # CLI subcommands and setup +│ ├── main.py # Entry point — all `hermes` subcommands +│ ├── config.py # DEFAULT_CONFIG, OPTIONAL_ENV_VARS, migration +│ ├── commands.py # Slash command definitions + SlashCommandCompleter +│ ├── callbacks.py # Terminal callbacks (clarify, sudo, approval) +│ ├── setup.py # Interactive setup wizard +│ ├── skin_engine.py # Skin/theme engine +│ ├── skills_config.py # `hermes skills` — skill management +│ ├── tools_config.py # `hermes tools` — tool management +│ ├── skills_hub.py # Skills Hub integration +│ ├── models.py # Model catalog, provider model lists +│ ├── model_switch.py # Shared /model switch pipeline +│ └── auth.py # Provider credential resolution +│ +├── tools/ # Tool implementations (one file per tool) +│ ├── registry.py # Central tool registry +│ ├── approval.py # Dangerous command detection +│ ├── terminal_tool.py # Terminal/shell execution +│ ├── process_registry.py # Background process management +│ ├── file_tools.py # File read/write/search/patch +│ ├── web_tools.py # Web search/extract +│ ├── browser_tool.py # Browser automation +│ ├── code_execution_tool.py # execute_code sandbox +│ ├── delegate_tool.py # Subagent delegation +│ ├── mcp_tool.py # MCP client integration +│ ├── skills_tool.py # Skill management tool +│ ├── todo_tool.py # Todo/task tracking tool +│ ├── memory_tool.py # Memory read/write tool +│ ├── tts_tool.py # Text-to-speech +│ ├── vision_tool.py # Image analysis +│ ├── image_gen_tool.py # Image generation +│ └── environments/ # Terminal backends +│ ├── base.py # BaseEnvironment ABC +│ ├── local.py # Local execution +│ ├── docker.py # Docker containers +│ ├── ssh.py # SSH remote execution +│ ├── modal.py # Modal serverless +│ ├── managed_modal.py # Nous-hosted Modal +│ ├── daytona.py # Daytona serverless +│ ├── singularity.py # Singularity HPC containers +│ └── persistent_shell.py # Persistent shell mixin +│ +├── gateway/ # Messaging platform gateway +│ ├── run.py # GatewayRunner — main message loop +│ ├── session.py # SessionStore — conversation persistence +│ ├── status.py # Gateway status, token locks +│ └── platforms/ # 16 platform adapters +│ ├── base.py # BasePlatformAdapter ABC +│ ├── telegram.py # Telegram (polling + webhook) +│ ├── discord.py # Discord +│ ├── slack.py # Slack +│ ├── whatsapp.py # WhatsApp +│ ├── matrix.py # Matrix (E2EE) +│ ├── signal.py # Signal +│ ├── email.py # Email (IMAP/SMTP) +│ ├── homeassistant.py # Home Assistant +│ ├── sms.py # SMS (Twilio) +│ ├── mattermost.py # Mattermost +│ ├── dingtalk.py # DingTalk +│ ├── feishu.py # Feishu/Lark +│ ├── wecom.py # WeCom (Enterprise WeChat) +│ ├── webhook.py # Generic webhook +│ └── api_server.py # OpenAI-compatible API server +│ +├── acp_adapter/ # ACP server (IDE integration) +│ ├── server.py # HermesACPAgent class +│ ├── session.py # SessionManager +│ ├── events.py # Streaming callbacks +│ ├── permissions.py # Approval callbacks +│ └── entry.py # Entry point +│ +├── cron/ # Scheduler +│ ├── scheduler.py # tick() — job execution engine +│ └── jobs.py # Job storage and CRUD +│ +├── plugins/ # Plugin system +│ └── memory/ # 8 memory provider plugins +│ ├── openviking/ +│ ├── mem0/ +│ ├── hindsight/ +│ ├── holographic/ +│ ├── honcho/ +│ ├── retaindb/ +│ └── byterover/ +│ +├── environments/ # RL training environments (Atropos) +│ ├── hermes_base_env.py # Abstract base RL environment +│ ├── agent_loop.py # HermesAgentLoop — rollout execution +│ ├── tool_context.py # ToolContext — sandbox for RL +│ ├── web_research_env.py # Web research tasks +│ └── agentic_opd_env.py # Observation-Prediction-Demo env +│ +├── skills/ # 70+ bundled skills across 28 categories +├── optional-skills/ # Additional optional skills +├── tests/ # ~3000 pytest tests +├── scripts/ # Install, update, packaging scripts +├── docker/ # Docker build files +├── docs/ # Documentation source (Docusaurus) +├── website/ # Landing page +├── desktop/ # Desktop app (Electron, separate repo) +├── tinker-atropos/ # RL submodule +│ +├── pyproject.toml # Python package config +├── AGENTS.md # Developer guide for AI assistants +├── RELEASE_v0.2.0.md → v0.7.0.md # Release notes +└── cli-config.yaml.example # Example config +``` + +**User config directory:** `~/.hermes/` + +``` +~/.hermes/ +├── config.yaml # User settings +├── .env # API keys and secrets +├── MEMORY.md # Persistent agent memory +├── USER.md # User profile +├── SOUL.md # Agent personality/identity +├── sessions.db # SQLite session database +├── skills/ # User-installed skills +├── skins/ # Custom CLI themes +├── plugins/ # User plugins +├── cron/ # Cron jobs and output +│ ├── jobs.json +│ └── output/ +├── cache/ # Image/audio cache +├── plans/ # Generated plans +├── profiles/ # Multi-instance profiles +└── mcp/ # MCP server configs +``` + +--- + +## 5. Core Architecture + +### 5.1 AIAgent Class (run_agent.py) + +The `AIAgent` class is the heart of the system — the core conversation loop that orchestrates LLM calls, tool execution, context management, and response delivery. + +**Constructor (~60 parameters):** + +```python +class AIAgent: + def __init__(self, + model: str = "anthropic/claude-opus-4.6", + max_iterations: int = 90, + enabled_toolsets: list = None, + disabled_toolsets: list = None, + quiet_mode: bool = False, + save_trajectories: bool = False, + platform: str = None, # "cli", "telegram", etc. + session_id: str = None, + session_db: SessionDB = None, + skip_context_files: bool = False, + skip_memory: bool = False, + base_url: str = None, + api_key: str = None, + provider: str = None, + api_mode: str = "chat_completions", # or "anthropic_messages" or "codex_responses" + tool_progress_callback = None, + stream_delta_callback = None, + thinking_callback = None, + status_callback = None, + iteration_budget: IterationBudget = None, + credential_pool = None, + checkpoints_enabled: bool = False, + # ... plus provider, routing, callback params + ) +``` + +**Main Methods:** + +| Method | Returns | Purpose | +| ------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------- | +| `chat(message, stream_callback)` | `str` | Simple interface — returns final response text | +| `run_conversation(user_message, system_message, conversation_history, task_id)` | `dict` | Full interface — returns `{final_response, messages, completed, api_calls, error}` | +| `_interruptible_api_call(api_kwargs)` | Response | Runs API request in background thread with interrupt support | +| `_interruptible_streaming_api_call(api_kwargs, on_first_delta)` | Response | Streaming variant with delta callbacks | + +**The Core Agent Loop** (inside `run_conversation()`): + +```python +while api_call_count < self.max_iterations and self.iteration_budget.remaining > 0: + response = client.chat.completions.create( + model=model, messages=messages, tools=tool_schemas + ) + if response.tool_calls: + for tool_call in response.tool_calls: + result = handle_function_call(tool_call.name, tool_call.args, task_id) + messages.append(tool_result_message(result)) + api_call_count += 1 + else: + return response.content # Final text response +``` + +**Key Behaviors:** + +- **Three API modes:** `chat_completions` (OpenAI-compatible), `anthropic_messages` (Anthropic SDK), `codex_responses` (OpenAI Codex) +- **Parallel tool execution:** Independent tool calls run concurrently via ThreadPoolExecutor (unless they share file paths or are in the never-parallel list) +- **Interrupt support:** Background threads allow interrupt detection without blocking on HTTP +- **Error recovery:** Automatic fallback chain (primary → fallback model), retry with exponential backoff, context compression on token overflow +- **Budget pressure:** Warnings at 70% (caution) and 90% (urgent) of iteration budget +- **Oversized results:** Tool results >100K chars are saved to temp files with a preview +- **Stale connection detection:** 90s timeout for streaming, 60s read timeout + +**IterationBudget** (thread-safe): + +```python +class IterationBudget: + def consume() -> bool # Check and consume one iteration + def refund() # Give back iteration (for execute_code turns) + @property remaining # Remaining iterations +``` + +--- + +### 5.2 Tool Orchestration (model_tools.py) + +Bridges the agent and tool registry — handles discovery, schema generation, and dispatch. + +**Key Functions:** + +| Function | Purpose | +| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| `_discover_tools()` | Imports all tool modules (each calls `registry.register()` on import) | +| `get_tool_definitions(enabled_toolsets, disabled_toolsets, quiet_mode)` | Returns OpenAI-format tool schemas filtered by toolset | +| `handle_function_call(function_name, function_args, task_id, user_task, enabled_tools)` | Main dispatcher — routes calls to registry with arg coercion | +| `coerce_tool_args(tool_name, args)` | Type coercion for LLM-generated arguments (string→int, string→bool, etc.) | + +**Tool Discovery Order:** + +1. Static tools (web_tools, terminal_tool, file_tools, browser_tool, etc.) +2. Optional tools (fal_client for image gen, honcho, etc.) — graceful fallback if missing +3. Plugin-registered tools +4. MCP server tools (dynamic, via tools/list_changed notifications) + +**Special Tool Handling:** + +- **Agent-level tools** (todo, memory, session_search, delegate_task): Intercepted by `run_agent.py` before `handle_function_call()` +- **execute_code**: Passes `enabled_tools` for sandbox tool list +- **Dynamic schema adjustments**: `browser_navigate` strips web_search reference if tools unavailable + +**Async Bridging:** + +- Persistent event loops (not `asyncio.run()`) to prevent "Event loop is closed" errors +- Main thread uses shared loop; worker threads get per-thread loops +- `_run_async()` detects running loop and spins up disposable thread if needed + +--- + +### 5.3 Toolset System (toolsets.py) + +Provides flexible tool grouping and composition. + +**Core Toolsets:** + +| Toolset | Tools Included | +| ---------------- | ------------------------------------------------------------------------------------------------ | +| `web` | web_search, web_extract, web_crawl | +| `terminal` | terminal | +| `file` | read_file, write_file, edit_file, list_files, search_files | +| `browser` | browser_navigate, browser_snapshot, browser_click, browser_type, browser_scroll, browser_extract | +| `vision` | analyze_image | +| `image_gen` | generate_image | +| `tts` | text_to_speech | +| `todo` | todo_read, todo_write | +| `memory` | memory_read, memory_write | +| `session_search` | session_search | +| `delegation` | delegate_task | +| `code_execution` | execute_code | +| `cronjob` | create_job, list_jobs, delete_job | +| `messaging` | send_message | +| `homeassistant` | ha_get_states, ha_call_service, ... | + +**Composite Toolsets:** + +- `hermes-cli` — All core tools for CLI platform +- `hermes-telegram`, `hermes-discord`, etc. — Platform-specific tool sets +- `hermes-gateway` — Union of all platform tools +- `debugging` — terminal + file + web +- `safe` — Everything except terminal + +**Resolution:** + +```python +resolve_toolset(name, visited=None) → List[str] +# Recursively resolves toolset to tool names +# Handles composition (includes) and cycle detection +# Special aliases: "all" or "*" = all tools +``` + +--- + +### 5.4 Tool Registry (tools/registry.py) + +Singleton managing all tool schemas and handlers. Circular-import safe — has no tool dependencies. + +**ToolEntry** (per-tool metadata): + +```python +@dataclass(slots=True) +class ToolEntry: + name: str + toolset: str + schema: dict # OpenAI-format tool definition + handler: Callable # Sync or async handler function + check_fn: Callable # Returns True if tool is available + requires_env: list # Required environment variables + is_async: bool + description: str + emoji: str +``` + +**Key Methods:** + +```python +registry.register(name, toolset, schema, handler, check_fn, requires_env) +registry.get_definitions(tool_names, quiet) # Returns filtered schemas +registry.dispatch(name, args, **kwargs) # Execute with async bridging +registry.deregister(name) # Remove (for MCP tool refresh) +registry.check_tool_availability() # Returns (available, unavailable) +``` + +--- + +### 5.5 Session Database (hermes_state.py) + +SQLite-based persistent session storage with FTS5 full-text search. + +**Schema (v6):** + +```sql +-- Sessions table +sessions ( + id TEXT PRIMARY KEY, + source TEXT, user_id TEXT, model TEXT, model_config TEXT, + system_prompt TEXT, parent_session_id TEXT, + started_at TEXT, ended_at TEXT, end_reason TEXT, + message_count INTEGER, tool_call_count INTEGER, + input_tokens INTEGER, output_tokens INTEGER, + cache_read_tokens INTEGER, cache_write_tokens INTEGER, reasoning_tokens INTEGER, + estimated_cost_usd REAL, actual_cost_usd REAL, + title TEXT -- UNIQUE INDEX +) + +-- Messages table +messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT, role TEXT, content TEXT, + tool_call_id TEXT, tool_calls TEXT, -- JSON + tool_name TEXT, timestamp TEXT, + token_count INTEGER, finish_reason TEXT, + reasoning TEXT, reasoning_details TEXT, codex_reasoning_items TEXT +) + +-- FTS5 virtual table (auto-synced via triggers) +messages_fts (content) +``` + +**Concurrency Model:** + +- WAL (Write-Ahead Logging) for concurrent readers + single writer +- `BEGIN IMMEDIATE` for write transactions (lock at start, not commit) +- Jitter retry on lock: 20-150ms random backoff, max 15 retries +- Periodic WAL checkpoint every 50 writes + +**Key Operations:** + +- `create_session()`, `end_session()`, `reopen_session()` +- `add_message()`, `get_messages()` +- `search_sessions(query)` — FTS5 full-text search +- `update_token_counts()` — Supports both incremental (CLI) and absolute (gateway) modes + +--- + +### 5.6 Constants & Home Directory (hermes_constants.py) + +Import-safe constants module with no circular dependencies. + +```python +get_hermes_home() → Path # HERMES_HOME env var or ~/.hermes +display_hermes_home() → str # User-friendly display: "~/.hermes" +get_optional_skills_dir() → Path # HERMES_OPTIONAL_SKILLS env var +parse_reasoning_effort(str) → Dict # "high" → {"enabled": True, "effort": "high"} + +# Key constants +OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1" +NOUS_API_BASE_URL = "https://inference-api.nousresearch.com/v1" +AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh/v1" +VALID_REASONING_EFFORTS = ("xhigh", "high", "medium", "low", "minimal") +``` + +--- + +## 6. CLI System + +### 6.1 Interactive CLI (cli.py) + +The `HermesCLI` class provides the interactive terminal interface. + +**Features:** + +- **Rich** for banner/panels, **prompt_toolkit** for input with autocomplete +- **KawaiiSpinner** — animated faces during API calls, `┊` activity feed for tool results +- Multiline editing with Shift+Enter +- Slash-command autocomplete +- Session history with up/down arrow navigation +- Clipboard image paste (Alt+V / Ctrl+V) +- Status bar showing model, provider, and token counts +- Inline diff previews for file write/patch operations + +**Configuration Loading:** + +```python +load_cli_config() → dict +# Loads from ~/.hermes/config.yaml (or ./cli-config.yaml fallback) +# Merges with hardcoded defaults +# Expands ${ENV_VAR} references +# Maps terminal config → env vars +``` + +--- + +### 6.2 CLI Entry Point (hermes_cli/main.py) + +All `hermes` subcommands are dispatched from here: + +``` +hermes # Default: interactive chat +hermes chat # Explicit interactive mode +hermes gateway start|stop|status|install|uninstall +hermes setup # Setup wizard +hermes model # Select model/provider +hermes tools # Configure tools +hermes skills # Manage skills +hermes config set|get # Direct config manipulation +hermes cron list|delete # Cron job management +hermes doctor # Diagnose issues +hermes sessions browse # Session picker +hermes profile create|list|switch|delete|export|import +hermes mcp serve|add|remove # MCP management +hermes acp # Start ACP server +hermes update|uninstall|version +``` + +**Profile System:** + +- `_apply_profile_override()` runs BEFORE any imports to set `HERMES_HOME` +- Pre-parses `--profile/-p` from argv +- Allows fully isolated agent instances with separate config, memory, sessions, skills + +--- + +### 6.3 Configuration System (hermes_cli/config.py) + +**Key Configuration Sections:** + +```yaml +model: "anthropic/claude-opus-4.6" # or dict with provider/base_url/api_key +providers: {} # Provider-specific configs +fallback_providers: [] # Ordered failover list +credential_pool: {} # Multiple API keys per provider + +agent: + max_turns: 90 + gateway_timeout: 1800 + tool_use_enforcement: "auto" + +terminal: + backend: "local" # local|docker|modal|daytona|ssh|singularity + timeout: 180 + persistent_shell: true + docker_image: "nikolaik/python-nodejs:..." + +compression: + enabled: true + threshold: 0.50 # Compress when 50% of context used + target_ratio: 0.20 # Summary = 20% of compressed content + protect_last_n: 20 + +auxiliary: + vision: { provider, model } + web_extract: { provider, model } + compression: { provider, model } + +memory: + memory_enabled: true + provider: "" # "" | "honcho" | "mem0" | etc. + memory_char_limit: 2200 + +display: + personality: "kawaii" + show_reasoning: false + inline_diffs: true + skin: "default" + streaming: true + +tts: + provider: "edge" # edge|elevenlabs|openai|neutts + +stt: + enabled: true + provider: "local" # local|groq|openai + +privacy: + redact_pii: false + +mcp_servers: {} # MCP server configurations + +skills: + external_dirs: [] # Additional skill directories + +approvals: + mode: "smart" # smart|always|off +``` + +**Config Files:** + +- `~/.hermes/config.yaml` — User settings (authoritative) +- `~/.hermes/.env` — API keys and secrets +- Config version migration system (currently v5) + +--- + +### 6.4 Slash Command Registry (hermes_cli/commands.py) + +All slash commands defined centrally in `COMMAND_REGISTRY`: + +```python +CommandDef(name, description, category, aliases, args_hint, cli_only, gateway_only) +``` + +**Derived automatically by:** + +- CLI `process_command()` — dispatch on canonical name +- Gateway dispatch + help +- Telegram BotCommand menu +- Slack `/hermes` subcommands +- Autocomplete + help text + +**Key Commands:** + +| Command | Aliases | Description | +| -------------- | ---------- | ----------------------------- | +| `/new` | `/reset` | Start fresh conversation | +| `/model` | | Show/switch model | +| `/personality` | | Set agent personality | +| `/retry` | | Retry last turn | +| `/undo` | | Remove last turn | +| `/compress` | `/compact` | Compress context | +| `/usage` | `/cost` | Show token usage | +| `/insights` | | Usage analytics | +| `/skills` | | Browse/install skills | +| `/background` | `/bg` | Manage background processes | +| `/plan` | | Generate implementation plan | +| `/rollback` | | Restore filesystem checkpoint | +| `/verbose` | | Toggle debug output | +| `/reasoning` | | Set reasoning effort | +| `/yolo` | | Toggle approval bypass | +| `/btw` | | Ephemeral side question | +| `/stop` | | Kill current agent run | +| `/queue` | | Queue next prompt | +| `/browser` | | Interactive browser session | +| `/history` | `/resume` | Session browser | +| `/skin` | | Switch CLI theme | + +--- + +### 6.5 Setup Wizard (hermes_cli/setup.py) + +Modular interactive wizard with independent sections: + +1. **Model & Provider** — Select AI provider, enter API keys, choose model +2. **Terminal Backend** — Choose execution environment +3. **Agent Settings** — Max iterations, compression, session policies +4. **Messaging Platforms** — Configure Telegram, Discord, Slack, etc. +5. **Tools** — TTS, STT, web search, image generation, browser + +Features: + +- Live credential validation +- Real-time model list fetching from provider APIs +- Automatic OpenClaw migration detection +- Atomic config file writes + +--- + +### 6.6 Model Catalog (hermes_cli/models.py) + +Provider-specific model lists: + +```python +_PROVIDER_MODELS = { + "nous": ["anthropic/claude-opus-4.6", "anthropic/claude-sonnet-4.6", ...], # 25+ + "openrouter": ["anthropic/claude-opus-4.6", "google/gemini-3-flash", ...], # 30+ + "anthropic": ["claude-opus-4-6", "claude-sonnet-4-6", ...], + "openai": ["gpt-5", "gpt-5.4-mini", "gpt-4.1", "gpt-4o", ...], + "copilot": ["gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex", ...], + "huggingface": [...], + "minimax": [...], + "kimi-coding": [...], + "alibaba": [...], + "deepseek": [...], + # ... more providers +} +``` + +Features: + +- Dynamic fetching via provider `/models` endpoints +- Curated lists used when live probe returns fewer models +- Fuzzy matching for typo correction +- Validation against provider catalog + +--- + +### 6.7 Skin/Theme Engine (hermes_cli/skin_engine.py) + +Data-driven CLI visual customization — no code changes needed. + +**Customizable Elements:** + +| Element | Key | Used By | +| -------------------------------- | ------------------------ | ----------------- | +| Banner border/title/accent | `colors.*` | banner.py | +| Response box border | `colors.response_border` | cli.py | +| Spinner faces (waiting/thinking) | `spinner.*` | display.py | +| Spinner verbs/wings | `spinner.*` | display.py | +| Tool output prefix | `tool_prefix` | display.py | +| Per-tool emojis | `tool_emojis` | display.py | +| Agent name/welcome/prompt | `branding.*` | banner.py, cli.py | + +**Built-in Skins:** default, ares, mono, slate, poseidon, sisyphus, charizard + +**User Skins:** Drop `~/.hermes/skins/.yaml` and activate with `/skin ` + +--- + +## 7. Tool System + +### 7.1 Terminal Tool (tools/terminal_tool.py) + +Shell command execution across multiple backends. + +```python +def terminal_tool( + command: str, + background: bool = False, + timeout: Optional[int] = None, + task_id: Optional[str] = None, + force: bool = False, # Skip approval for dangerous commands + workdir: Optional[str] = None, + check_interval: Optional[int] = None, # Background task polling + pty: bool = False, +) -> str # JSON result +``` + +**Features:** + +- Multi-backend: Selects based on `TERMINAL_ENV` (local/docker/ssh/modal/daytona/singularity) +- Per-task_id sandboxes with thread-safe creation locks +- Dangerous command routing through approval system +- Background task support with file-based IPC +- Interrupt handling — polls `is_interrupted()` during execution +- Auto-cleanup daemon thread for idle environments (>300s) +- Disk usage warnings at configurable threshold + +--- + +### 7.2 File Tools (tools/file_tools.py) + +Safe file operations with size guards and sensitive path protection. + +- `read_file_tool(path, offset, limit)` — Read with pagination (default 100K char limit) +- `write_file_tool(path, content)` — Write with approval for sensitive paths +- `edit_file_tool(path, old_text, new_text)` — String replacement editing +- `list_files_tool(path)` — Directory listing +- `search_files_tool(pattern, path)` — Glob/regex file search + +**Safety:** + +- Device path blocklist (`/dev/zero`, `/dev/stdin`, etc.) +- Read dedup tracking — returns stub on re-read if mtime unchanged +- Sensitive path blocking: `/etc/`, `/boot/`, `~/.ssh` without approval +- Prompt injection protection for known dangerous paths + +--- + +### 7.3 Web Tools (tools/web_tools.py) + +Web search and content extraction. + +- `web_search_tool(query, limit)` — Search via configurable backend +- `web_extract_tool(urls, format)` — Extract content from URLs +- `web_crawl_tool(url, instructions)` — LLM-guided web crawling + +**Backends:** Firecrawl, Parallel Web, Tavily, Exa, DuckDuckGo + +- Fallback detection by highest-priority available API key +- LLM processing via auxiliary client (Gemini 3 Flash) for intelligent extraction +- SSRF protection and URL safety checks + +--- + +### 7.4 Browser Tool (tools/browser_tool.py) + +Headless browser automation with accessibility tree. + +```python +browser_navigate(url, task_id) → str +browser_snapshot(task_id, max_chars) → str +browser_click(ref, task_id) → str # Element refs like @e1, @e2 +browser_type(ref, text, task_id) → str +browser_scroll(ref, direction, task_id) → str +browser_extract(task, max_chars, task_id) → str +``` + +**Backends:** + +- **Local:** Headless Chromium via `agent-browser` CLI +- **Cloud:** Browserbase (stealth, proxies, CAPTCHA solving) +- **Camofox:** Anti-detection browser using Camoufox + +**Features:** + +- Accessibility tree for text-based snapshots (no vision required) +- Session isolation per task_id with inactivity cleanup +- API key detection in URLs (prevents exfiltration) +- SSRF protection for private/internal addresses + +--- + +### 7.5 Delegate Tool (tools/delegate_tool.py) + +Spawn isolated subagents for parallel workstreams. + +```python +def delegate_task( + goal: str, + context: str = None, + toolsets: List[str] = None, # Default: ["terminal", "file", "web"] + tasks: List[Dict] = None, # Batch mode: up to 3 concurrent + max_iterations: int = 50, + parent_agent = None, +) → str +``` + +**Isolation:** + +- Fresh conversation (no parent history) +- Own task_id (separate terminal session, file ops) +- Restricted toolset (configurable, blocked tools always stripped) +- Focused system prompt from goal + context +- Parent only sees delegation call and summary result + +**Blocked Tools:** delegate_task, clarify, memory, send_message, execute_code (no recursion, no user interaction) + +**Constraints:** MAX_DEPTH=2, MAX_CONCURRENT_CHILDREN=3 + +--- + +### 7.6 MCP Tool (tools/mcp_tool.py) + +Model Context Protocol client integration. + +**Configuration (config.yaml):** + +```yaml +mcp_servers: + filesystem: + command: "npx" + args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] + timeout: 120 + remote_api: + url: "https://my-mcp-server.example.com/mcp" + headers: + Authorization: "Bearer sk-..." + sampling: + enabled: true + model: "gemini-3-flash" +``` + +**Features:** + +- **Transports:** Stdio (local processes) and HTTP/StreamableHTTP (remote) +- **Dynamic tool discovery:** Listens for `tools/list_changed` notifications +- **Sampling support:** MCP servers can request LLM completions +- **Automatic reconnection** with exponential backoff (5 retries) +- Dedicated background event loop in daemon thread +- Thread-safe via lock protecting server dict + +--- + +### 7.7 Approval System (tools/approval.py) + +Dangerous command detection and approval flow. + +```python +detect_dangerous_command(command: str) → (is_dangerous, pattern_key, description) +``` + +**106 patterns covering:** + +- Destructive operations (`rm -r /`, `mkfs`, `dd if=`) +- Privilege escalation (`chmod 777`, `chown -R root`) +- SQL injection (`DROP TABLE`, `DELETE FROM` without WHERE) +- System targeting (`/etc/` writes, `systemctl stop`, fork bombs) +- Shell injection (pipe to sh, wget to sh, process substitution) +- Network exfiltration (curl with embedded API keys) +- Secret access (`cat ~/.env`, `cat ~/.netrc`) + +**Normalization:** Strips ANSI escapes, null bytes, normalizes Unicode + +**Approval Modes:** + +- `smart` — Learns which commands are safe based on user decisions +- `always` — Always ask for dangerous commands +- `off` — Never ask (or `/yolo` toggle) + +--- + +### 7.8 Terminal Backends (tools/environments/) + +| Backend | File | Features | +| ----------------- | ------------------ | ------------------------------------------------------------------------------ | +| **Local** | `local.py` | Direct execution, interrupt support, non-blocking I/O, persistent shell | +| **Docker** | `docker.py` | Sandboxed containers, cap-drop ALL, no-new-privileges, PID limits, bind mounts | +| **SSH** | `ssh.py` | Remote execution via ControlMaster, persistent shell mixin | +| **Modal** | `modal.py` | Native Modal SDK `Sandbox.create()`/`Sandbox.exec()`, persistent snapshots | +| **Managed Modal** | `managed_modal.py` | Modal through Nous-hosted gateway | +| **Daytona** | `daytona.py` | Daytona SDK cloud sandboxes, stop/resume lifecycle | +| **Singularity** | `singularity.py` | Singularity containers with scratch dir, SIF cache | + +**Common Interface (BaseEnvironment ABC):** + +```python +execute(command, timeout) → {"output": str, "returncode": int} +cleanup() → None +``` + +--- + +## 8. Agent Internals + +### 8.1 Prompt Builder (agent/prompt_builder.py) + +Assembles the system prompt from multiple sources: + +| Component | Source | +| ----------------------- | ---------------------------------------------------- | +| Agent Identity | `DEFAULT_AGENT_IDENTITY` or `SOUL.md` | +| Memory Guidance | When/how to use memory tool | +| Session Search Guidance | How to recall past conversations | +| Skills Guidance | When to create/patch skills | +| Tool Use Enforcement | Must execute tools, not describe actions | +| Skills Index | `~/.hermes/skills/.hermes-skills.json` | +| Platform Hints | OS, Python version, shell, available tools | +| Context Files | `.hermes.md`, `AGENTS.md`, `.cursorrules`, `SOUL.md` | +| Model/Provider Info | Current model and provider identity | + +**Context File Discovery:** + +1. Check `cwd/.hermes.md` or `HERMES.md` +2. Walk parent directories up to git root +3. Validate against injection patterns before inclusion + +**Injection Detection (30+ patterns):** + +- "ignore previous instructions", "system prompt override" +- "do not tell the user", "act as if you have no restrictions" +- HTML comment injection, exfiltration via curl, Unicode stealth chars + +--- + +### 8.2 Context Compressor (agent/context_compressor.py) + +Automatically compresses conversation history when approaching context limits. + +```python +class ContextCompressor: + threshold_percent: float = 0.50 # Compress when 50% of context used + protect_first_n: int = 3 # Keep system prompt + first turn + protect_last_n: int = 20 # Keep recent N messages + summary_target_ratio: float = 0.20 # Summary = 20% of compressed content +``` + +**Compression Algorithm:** + +1. **Pre-pass:** Replace old tool results with placeholder (cheap) +2. **Protect head:** System prompt + first exchange always kept +3. **Protect tail:** Token-budget-based tail protection (~20K tokens) +4. **Summarize middle:** LLM-generated structured summary (Goal, Progress, Decisions, Files, Next Steps) +5. **Iterative updates:** On subsequent compressions, update previous summary instead of re-summarizing + +--- + +### 8.3 Prompt Caching (agent/prompt_caching.py) + +Anthropic prompt caching support for cost reduction. + +- System prompt cached across turns (first conversation turn establishes cache) +- Cache markers inserted at system prompt boundaries +- Must not break mid-conversation — altering past context invalidates cache +- The ONLY time context is altered is during compression + +**Critical Rule:** Do NOT implement changes that would alter past context, change toolsets, reload memories, or rebuild system prompts mid-conversation. + +--- + +### 8.4 Auxiliary Client (agent/auxiliary_client.py) + +Separate LLM client for non-primary tasks: + +- **Vision analysis** — Image description and analysis +- **Web extraction** — Content summarization +- **Context compression** — Generating conversation summaries +- **Session search** — Summarizing search results +- **MCP sampling** — Serving server-initiated LLM requests + +Configured per-task via `auxiliary` section in config.yaml. + +--- + +### 8.5 Display & Spinner (agent/display.py) + +- **KawaiiSpinner** — Animated face characters during API calls +- **Tool preview formatting** — `┊` prefixed activity feed for tool execution +- **Inline diff previews** — Shows unified diffs for write/patch operations +- Respects active skin for colors, emojis, and branding + +--- + +### 8.6 Skill Commands (agent/skill_commands.py) + +Shared skill invocation for CLI and gateway: + +- Skills loaded from `~/.hermes/skills/` and external directories +- Injected as **user message** (not system prompt) to preserve prompt caching +- `/plan` command generates implementation plans stored in `.hermes/plans/` +- Skill content includes setup instructions, tool options, usage examples + +--- + +## 9. Messaging Gateway + +### 9.1 GatewayRunner (gateway/run.py) + +Main controller managing all platform adapters and routing messages. + +**Key Attributes:** + +- `adapters: Dict[Platform, BasePlatformAdapter]` — Active platform instances +- `session_store: SessionStore` — Conversation persistence +- `_running_agents: Dict[str, AIAgent]` — Per-session cached agents (preserves prompt caching) +- `_pending_approvals: Dict[str, Dict]` — Dangerous command approval tracking +- `pairing_store: PairingStore` — DM code-based user authorization + +**Message Flow:** + +1. Platform adapter queues `MessageEvent` +2. GatewayRunner dequeues, calls `_handle_message()` +3. Slash command? → dispatch to handler +4. Regular message? → `_handle_message_with_agent()` (async, with per-session locking) +5. Response delivered back via platform adapter + +**Features:** + +- Agent caching per session (preserves Anthropic prompt cache across turns) +- Session reset policies (inactivity timeout, hard reset) +- Per-session model overrides via `/model` +- Background memory flush on session expiry +- Approval routing (`/approve`, `/deny`) with interactive buttons (Discord) +- 30+ slash command handlers + +--- + +### 9.2 Session Store (gateway/session.py) + +**SessionSource** — Where a message originated (platform, chat_id, user info) +**SessionContext** — Full context for system prompt injection (platforms, home channels, metadata) +**SessionStore** — Loads/saves conversation transcripts as JSON files + +``` +~/.hermes/sessions/{session_key}.json +Format: [{role, content, timestamp}, ...] +``` + +**Features:** + +- Session key computed from platform + chat_id + thread_id (deterministic hash) +- Inactivity timeout for session resets +- PII redaction (phone numbers hashed) +- Survives gateway restarts + +--- + +### 9.3 Platform Adapters (gateway/platforms/) + +**16 adapters sharing `BasePlatformAdapter` interface:** + +| Platform | Key Features | +| ------------------ | -------------------------------------------------------------------------------------------------------- | +| **Telegram** | Polling + webhook mode, media handling, inline keyboards, forum topic isolation, group mention gating | +| **Discord** | Server channels, threads, reactions (processing/done/error), button-based approval, @mention requirement | +| **Slack** | Multi-workspace OAuth, thread handling, app_mention, `/hermes` subcommands | +| **WhatsApp** | Group & DM support, media captions, LID↔phone alias resolution | +| **Matrix** | E2EE room encryption, threaded messages, trusted device flow, native voice messages | +| **Signal** | Encrypted DMs, group membership, SSE keepalive, phone URL encoding | +| **Email** | IMAP/SMTP, multi-recipient, skip_attachments option | +| **Home Assistant** | REST tools + WebSocket, service discovery, smart home automation | +| **SMS** | Twilio integration | +| **Mattermost** | Self-hosted Slack alternative, configurable mention behavior | +| **DingTalk** | Alibaba enterprise messaging | +| **Feishu/Lark** | Enterprise messaging, message cards, approval workflows | +| **WeCom** | Enterprise WeChat, department management | +| **Webhook** | Generic HTTP POST for custom integrations | +| **API Server** | OpenAI-compatible `/v1/chat/completions` endpoint | + +**Common Features:** + +- Image/audio caching for vision and STT tools +- Rate limiting with exponential backoff +- Session routing with authorization rules +- Cross-platform conversation continuity + +--- + +## 10. Cron Scheduling + +Built-in job scheduler running in the gateway background thread. + +**Schedule Types:** + +- `"once in 5m"` — One-shot after duration +- `"every 30m"` — Recurring interval +- `"0 9 * * *"` — Standard cron expression +- `"2026-04-06T14:00"` — Absolute datetime + +**Job Storage:** `~/.hermes/cron/jobs.json` + +**Execution Flow:** + +1. `tick()` called every 60s from gateway background thread +2. Fetch due jobs past `next_run_at` +3. Spawn `hermes` CLI subprocess with job prompt + skills +4. Capture output → save to `~/.hermes/cron/output/{job_id}/{timestamp}.md` +5. Deliver to target platform (or stay local) + +**Delivery Targets:** + +- `"local"` — Output saved locally only +- `"origin"` — Send to originating chat +- `"telegram:"` — Explicit platform/chat routing +- `[SILENT]` prefix — Suppress delivery but keep logs + +**Grace Windows:** Based on schedule frequency (120s–2hrs) to handle missed jobs + +--- + +## 11. Skills System + +**Skills are composable, agent-invokable knowledge units.** + +**Structure:** + +``` +skills/ +├── creative/ # ASCII art, diagrams, music +├── software-development/ # Debugging, testing, docs +├── github/ # Codebase inspection, PR workflow +├── research/ # Literature, web scraping +├── productivity/ # Task management +├── media/ # Image, video, audio processing +├── mlops/ # ML experiment tracking +├── autonomous-ai-agents/ # Multi-agent orchestration +└── [20+ more categories] +``` + +**Per-Skill Structure:** + +``` +skill-name/ +├── SKILL.md # Metadata (YAML frontmatter) + implementation instructions +└── [sub-skills]/ +``` + +**SKILL.md Example:** + +```yaml +--- +name: ascii-art +description: Generate ASCII art using multiple tools +version: 4.0.0 +dependencies: [] +metadata: + hermes: + tags: [ASCII, Art, Banners, Creative] + related_skills: [excalidraw] +--- +[Implementation instructions, tool options, examples...] +``` + +**Discovery:** + +- Auto-discovered from `~/.hermes/skills/` + external dirs +- Skills index built at startup (`.hermes-skills.json`) +- Loaded as user messages to preserve prompt caching +- Per-platform enable/disable via `hermes skills` +- Skills Hub (`agentskills.io`) for community sharing + +--- + +## 12. Plugin System + +Drop Python files into `~/.hermes/plugins/` to extend Hermes. + +**Plugin Capabilities:** + +- Register custom tools and toolsets +- Inject messages into conversation +- Lifecycle hooks: `pre_llm_call`, `post_llm_call`, `on_session_start`, `on_session_end` +- Enable/disable via `hermes plugins enable/disable ` + +**Memory Provider Plugins (plugins/memory/):** +8 implementations: openviking, mem0, hindsight, holographic, honcho, retaindb, byterover + +Each implements: + +```python +class MemoryProvider: + def is_available() → bool + def store(key, value) + def retrieve(query) → list + def clear() +``` + +--- + +## 13. Memory System + +Hermes has a pluggable memory provider interface: + +**Built-in Memory:** + +- `MEMORY.md` — Markdown file with persistent facts +- `USER.md` — User profile information +- Memory read/write tools called by agent during conversation +- Periodic nudges prompt agent to save important information +- FTS5 session search for cross-session recall + +**Honcho Integration:** + +- AI-native dialectic user modeling +- Async memory writes +- Profile-scoped host/peer resolution +- Multi-user isolation in gateway mode + +**Configuration:** + +```yaml +memory: + memory_enabled: true + provider: "" # "" for built-in, "honcho", "mem0", etc. + memory_char_limit: 2200 +``` + +--- + +## 14. ACP Server (IDE Integration) + +Agent Communication Protocol server for VS Code, Zed, JetBrains. + +**Entry:** `hermes acp` → `acp_adapter/server.py` + +**HermesACPAgent Class:** + +```python +initialize() # Handshake with IDE client +authenticate(method_id) # Validate credentials +new_session(cwd, mcp_servers) # Create isolated session +load_session(session_id) # Resume session +fork_session(session_id) # Branch for parallel work +list_sessions() # Browse all sessions +cancel(session_id) # Interrupt running agent +``` + +**Features:** + +- Slash command support (`/model`, `/tools`, `/reset`, `/compact`) +- Client-provided MCP servers (IDE's MCP ecosystem flows into agent) +- Streaming callbacks for messages, thinking, tool progress +- Dangerous command approval via IDE UI + +--- + +## 15. API Server + +OpenAI-compatible API endpoint for headless integrations (e.g., Open WebUI). + +**Endpoint:** `POST /v1/chat/completions` + +**Features:** + +- `X-Hermes-Session-Id` header for persistent sessions +- Tool progress streaming via SSE events +- `/api/jobs` REST API for cron management +- Input limits, field whitelists, SQLite-backed response persistence +- CORS origin protection + +--- + +## 16. MCP Server Mode + +Expose Hermes conversations to MCP-compatible clients. + +**Entry:** `hermes mcp serve` + +**Features:** + +- Browse conversations and sessions +- Read messages and search across sessions +- Manage attachments +- Supports both stdio and Streamable HTTP transports +- Compatible with Claude Desktop, Cursor, VS Code, etc. + +--- + +## 17. RL Training Environments + +Atropos-based RL training framework for agent policy optimization. + +**Base Class:** `HermesAgentBaseEnv` (extends `atroposlib.BaseEnv`) + +**Configuration:** + +```python +HermesAgentEnvConfig: + enabled_toolsets: ["terminal", "file", "web"] + max_agent_turns: 30 + agent_temperature: 1.0 + terminal_backend: "local" # or docker/modal for isolation + dataset_name: str +``` + +**Subclass Requirements:** + +- `setup()` — Load dataset +- `get_next_item()` — Return next task +- `format_prompt()` — Convert item → user message +- `compute_reward()` — Score rollout +- `evaluate()` — Periodic eval on test set + +**Example Environments:** + +- `web_research_env.py` — Web research tasks +- `agentic_opd_env.py` — Observation-Prediction-Demonstration +- `hermes_swe_env.py` — Software engineering tasks + +**Supporting:** + +- `HermesAgentLoop` — Orchestrates step-by-step rollouts +- `ToolContext` — Sandbox for tool execution, records side effects +- `trajectory_compressor.py` — Compresses trajectories for training data + +--- + +## 18. Profiles (Multi-Instance) + +Run multiple fully isolated Hermes instances from the same installation. + +**Commands:** + +```bash +hermes profile create +hermes profile list +hermes profile switch +hermes profile delete +hermes profile export +hermes profile import +hermes -p # Launch with specific profile +``` + +**Each profile gets:** + +- Own `HERMES_HOME` directory (`~/.hermes/profiles//`) +- Own config.yaml, .env, memory, sessions, skills, gateway service +- Token-lock isolation (prevents two profiles sharing bot credentials) + +**Implementation:** + +- `_apply_profile_override()` sets `HERMES_HOME` env var before any imports +- All 119+ references to `get_hermes_home()` automatically scope to active profile +- Profile operations are HOME-anchored (`~/.hermes/profiles/`) for cross-profile visibility + +--- + +## 19. Security Model + +### Command Approval + +- 106 dangerous command patterns (destructive, privilege escalation, SQL, exfiltration) +- Smart approval mode learns from user decisions +- Session-based approval state (per-session tracking) +- `--fuck-it-ship-it` flag or `/yolo` toggle to bypass + +### Secret Protection + +- Secret redaction in logs and tool output (API keys, tokens) +- Browser URL scanning for embedded secrets +- LLM response scanning for exfiltration attempts +- Credential directory protection (`.docker`, `.azure`, `.config/gh`, `.ssh`) +- `execute_code` sandbox output redaction + +### Input Safety + +- Prompt injection detection in context files (30+ patterns) +- Unicode stealth character detection +- ANSI escape sequence normalization +- Device path blocklist for file reads +- SSRF protection in web/browser tools + +### PII Handling + +- Optional `privacy.redact_pii` mode +- Phone number hashing in session metadata +- Sender ID anonymization in gateway logs + +### Supply Chain + +- All dependency version ranges pinned +- `uv.lock` with hashes for reproducible builds +- CI workflow scanning PRs for supply chain attack patterns +- Compromised `litellm` dependency removed + +--- + +## 20. Provider & Model System + +### Supported Providers + +| Provider | API Mode | Key Features | +| --------------------- | ------------------ | ----------------------------------------- | +| **Nous Portal** | chat_completions | 400+ models, first-class setup | +| **OpenRouter** | chat_completions | 200+ models, provider routing preferences | +| **Anthropic** | anthropic_messages | Native prompt caching, OAuth PKCE | +| **OpenAI** | chat_completions | GPT-5, Codex | +| **GitHub Copilot** | chat_completions | OAuth, 400k context | +| **Hugging Face** | chat_completions | Curated agentic model picker | +| **Google (Direct)** | chat_completions | Full Gemini context lengths | +| **z.ai/GLM** | chat_completions | Chinese LLM models | +| **Kimi/Moonshot** | chat_completions | Kimi Code API | +| **MiniMax** | anthropic_messages | M2.7 models | +| **Alibaba/DashScope** | chat_completions | Qwen models | +| **DeepSeek** | chat_completions | V3 models | +| **Vercel AI Gateway** | chat_completions | Routing through Vercel | +| **Kilo Code** | chat_completions | Custom provider | +| **OpenCode Zen/Go** | chat_completions | Custom provider | +| **Custom Endpoint** | configurable | Any OpenAI-compatible API | + +### Provider Features + +- **Ordered fallback chain:** Auto-failover across configured providers on errors +- **Credential pools:** Multiple API keys per provider with `least_used` rotation +- **Per-turn primary restoration:** After fallback use, restore primary on next turn +- **Context length detection:** models.dev integration, provider-aware resolution, `/v1/props` for llama.cpp +- **Rate limit handling:** User-friendly 429 messages with Retry-After countdown +- **Anthropic long-context tier:** Auto-reduces to 200k on tier limit 429 + +--- + +## 21. Streaming & Reasoning + +### Streaming + +- Enabled by default in CLI and gateway +- Token-by-token delivery via `stream_delta_callback` +- Proper spinner/tool progress display during streaming +- Stale connection detection (90s timeout) +- Fallback to non-streaming if provider doesn't support it + +### Reasoning/Thinking + +- Configurable effort: xhigh, high, medium, low, minimal, none +- `/reasoning` command to toggle display and effort level +- Anthropic thinking blocks preserved across multi-turn conversations +- `` tag extraction for compatible models +- Reasoning persisted to SessionDB (v6 schema) for cross-session continuity +- Thinking-budget exhaustion detection to skip useless continuation retries + +--- + +## 22. Release History + +### v0.2.0 (March 12, 2026) — The Foundation Release + +> 216 merged PRs from 63 contributors, resolving 119 issues + +- Multi-platform messaging gateway (Telegram, Discord, Slack, WhatsApp, Signal, Email, Home Assistant) +- MCP client support (stdio + HTTP) +- 70+ skills across 15+ categories +- Centralized provider router (`call_llm()` API) +- ACP server for IDE integration +- CLI skin/theme engine +- Git worktree isolation (`hermes -w`) +- Filesystem checkpoints and `/rollback` +- 3,289 tests + +### v0.3.0 (March 17, 2026) — Streaming, Plugins, Providers + +- Unified streaming infrastructure (token-by-token delivery) +- First-class plugin architecture (`~/.hermes/plugins/`) +- Native Anthropic provider with prompt caching +- Smart approvals + `/stop` command +- Honcho memory integration +- Voice mode (CLI push-to-talk, Telegram/Discord voice) +- Concurrent tool execution (ThreadPoolExecutor) +- PII redaction +- `/browser connect` via Chrome DevTools Protocol +- Vercel AI Gateway provider +- Persistent shell mode for local/SSH backends +- Agentic On-Policy Distillation RL environment + +### v0.4.0 (March 23, 2026) — Platform Expansion + +- OpenAI-compatible API server (`/v1/chat/completions`) +- 6 new messaging adapters (Signal rewrite, DingTalk, SMS, Mattermost, Matrix, Webhook) +- `@file` and `@url` context references with tab completion +- 4 new providers (GitHub Copilot, Alibaba, Kilo Code, OpenCode) +- MCP server management CLI with OAuth 2.1 PKCE +- Gateway prompt caching (dramatic cost reduction) +- Context compression overhaul (structured summaries, iterative updates) +- Streaming enabled by default +- 200+ bug fixes + +### v0.5.0 (March 28, 2026) — Hardening + +- Nous Portal expanded to 400+ models +- Hugging Face as first-class provider +- Telegram Private Chat Topics +- Native Modal SDK backend (replaced swe-rex) +- Plugin lifecycle hooks activated +- GPT model tool-use enforcement +- Nix flake with NixOS module +- Supply chain hardening (removed litellm, pinned deps, CI scanning) +- Anthropic per-model output limits (128K for Opus 4.6) + +### v0.6.0 (March 30, 2026) — Multi-Instance + +> 95 PRs and 16 resolved issues in 2 days + +- Profiles for multiple isolated agent instances +- MCP Server Mode (`hermes mcp serve`) +- Official Docker container +- Ordered fallback provider chain +- Feishu/Lark platform adapter +- WeCom (Enterprise WeChat) adapter +- Slack multi-workspace OAuth +- Telegram webhook mode + group controls +- Exa search backend +- Skills & credentials on remote backends + +### v0.7.0 (April 3, 2026) — Resilience + +> 168 PRs and 46 resolved issues + +- Pluggable memory provider interface (ABC-based plugin system) +- Same-provider credential pools with automatic rotation +- Camofox anti-detection browser backend +- Inline diff previews in tool activity feed +- API server session continuity + tool streaming +- ACP client-provided MCP servers +- Gateway hardening (race conditions, approval routing, compression death spirals) +- Secret exfiltration blocking (URL scanning, base64 detection) + +--- + +## 23. File Dependency Chain + +``` +hermes_constants.py (no deps — imported by everything) + ↑ +tools/registry.py (no tool deps — imported by all tool files) + ↑ +tools/*.py (each calls registry.register() at import time) + ↑ +model_tools.py (imports tools/registry + triggers tool discovery) + ↑ +run_agent.py (AIAgent), cli.py (HermesCLI), gateway/run.py (GatewayRunner) + ↑ +hermes_cli/main.py (entry point — dispatches to all subsystems) +``` + +**Key Principle:** `tools/registry.py` is circular-import safe. It has no tool dependencies. Tool files import the registry; `model_tools.py` imports both. + +--- + +## 24. Key Design Patterns + +| Pattern | Description | +| ------------------------------ | -------------------------------------------------------------------------------------------------- | +| **Registry-based Tool System** | Single source of truth; plugins register at import time; dynamic (MCP) and static tools coexist | +| **Toolset Composition** | Recursive resolution with cycle detection; platform-specific composites | +| **Iteration Budget** | Thread-safe shared budget across parent + subagents | +| **Streaming First** | Preferred over non-streaming for health checking (stale connection detection) | +| **Prefix Caching** | System prompt cached across turns (Anthropic optimization); context never altered mid-conversation | +| **Proactive Compression** | Triggered at 50% context usage; structured summaries with iterative updates | +| **Async Bridging** | Persistent event loops prevent "Event loop is closed"; per-thread loops for workers | +| **Profile Isolation** | HERMES_HOME env var set before imports; all state functions route through `get_hermes_home()` | +| **Agent Caching** | Gateway caches AIAgent per session to preserve prompt cache across turns | +| **WAL Concurrency** | SQLite WAL mode + jitter retry for concurrent readers + single writer | +| **Plugin Architecture** | Tools, toolsets, hooks, memory providers extensible via plugins | +| **Multi-Backend Execution** | Pluggable terminal backends with unified BaseEnvironment interface | +| **Safety Layers** | Approval system → sensitive path guards → injection detection → capability filtering | + +--- + +## 25. Configuration Reference + +### Environment Variables (key ones) + +| Variable | Purpose | +| --------------------- | --------------------------------------------------------- | +| `HERMES_HOME` | Override home directory (profiles set this automatically) | +| `OPENROUTER_API_KEY` | OpenRouter provider key | +| `ANTHROPIC_API_KEY` | Anthropic provider key | +| `OPENAI_API_KEY` | OpenAI provider key | +| `NOUS_API_KEY` | Nous Portal key | +| `FIRECRAWL_API_KEY` | Web search/extraction | +| `EXA_API_KEY` | Exa search backend | +| `BROWSERBASE_API_KEY` | Cloud browser | +| `FAL_KEY` | Image generation | +| `ELEVENLABS_API_KEY` | Premium TTS | +| `HONCHO_API_KEY` | Honcho memory | +| `TERMINAL_ENV` | Terminal backend override | +| `TERMINAL_CWD` | Terminal working directory | +| `MESSAGING_CWD` | Gateway working directory | + +### Config File Locations + +| File | Purpose | +| ----------------------------- | -------------------------------- | +| `~/.hermes/config.yaml` | Main configuration (YAML) | +| `~/.hermes/.env` | API keys and secrets | +| `~/.hermes/MEMORY.md` | Persistent agent memory | +| `~/.hermes/USER.md` | User profile | +| `~/.hermes/SOUL.md` | Agent persona/identity | +| `~/.hermes/sessions.db` | SQLite session database | +| `~/.hermes/cron/jobs.json` | Cron job definitions | +| `.hermes.md` (in project dir) | Per-project context file | +| `AGENTS.md` (in project dir) | Developer instructions for agent | + +--- + +## 26. Known Pitfalls + +1. **DO NOT hardcode `~/.hermes` paths** — Use `get_hermes_home()` from `hermes_constants`. Hardcoding breaks profiles. + +2. **DO NOT use `simple_term_menu`** — Rendering bugs in tmux/iTerm2 (ghosting). Use `curses` instead. + +3. **DO NOT use `\033[K`** (ANSI erase-to-EOL) — Leaks as literal text under prompt_toolkit's `patch_stdout`. Use space-padding. + +4. **`_last_resolved_tool_names` is process-global** — Saved/restored around subagent execution in `delegate_tool.py`. + +5. **DO NOT hardcode cross-tool references in schemas** — Tool may be unavailable. Add dynamic references in `get_tool_definitions()`. + +6. **Tests must not write to `~/.hermes/`** — `_isolate_hermes_home` autouse fixture redirects to temp dir. + +7. **Prompt caching must not break** — Do NOT alter past context, change toolsets, reload memories, or rebuild system prompts mid-conversation. + +8. **Working directory behavior differs:** CLI uses `os.getcwd()`, gateway uses `MESSAGING_CWD` env var. + +9. **Config has three loaders:** `load_cli_config()` (CLI), `load_config()` (hermes tools/setup), direct YAML (gateway). They have different merge behaviors. + +10. **Profile operations are HOME-anchored** — `_get_profiles_root()` returns `Path.home() / ".hermes" / "profiles"`, NOT `get_hermes_home() / "profiles"`. This is intentional for cross-profile visibility. + +--- + +_This document covers Hermes Agent v0.7.0 as of April 2026. For the latest information, refer to the [official documentation](https://hermes-agent.nousresearch.com/docs/) and the [GitHub repository](https://github.com/NousResearch/hermes-agent)._ diff --git a/.agents/skills/lat-md/SKILL.md b/.agents/skills/lat-md/SKILL.md new file mode 100644 index 0000000..4208385 --- /dev/null +++ b/.agents/skills/lat-md/SKILL.md @@ -0,0 +1,169 @@ +--- +name: lat-md +description: >- + Writing and maintaining lat.md documentation files — structured markdown that + describes a project's architecture, design decisions, and test specs. Use when + creating, editing, or reviewing files in the lat.md/ directory. +--- + +# lat.md Authoring Guide + +This skill covers the syntax, structure rules, and conventions for writing `lat.md/` files. Load it whenever you need to create or edit sections in the `lat.md/` directory. + +## What belongs in lat.md + +`lat.md/` files describe **what** the project does and **why** — domain concepts, key design decisions, business logic, and test specifications. They do NOT duplicate source code. Think of each section as an anchor that source code references back to. + +Good candidates for sections: +- Architecture decisions and their rationale +- Domain concepts and business rules +- API contracts and protocols +- Test specifications (what is tested and why) +- Non-obvious constraints or invariants + +Bad candidates: +- Step-by-step code walkthroughs (the code itself is the walkthrough) +- Auto-generated API docs (use tools for that) +- Temporary notes or TODOs + +## Section structure + +Every section **must** have a leading paragraph — at least one sentence immediately after the heading, before any child headings or other block content. + +The first paragraph must be ≤250 characters (excluding `[[wiki link]]` content). This paragraph is the section's identity — it appears in search results, command output, and RAG context. + +```markdown +# Good Section + +Brief overview of what this section documents and why it matters. + +More detail can go in subsequent paragraphs, code blocks, or lists. + +## Child heading + +Details about this child topic. +``` + +```markdown +# Bad Section + +## Child heading + +This is invalid — "Bad Section" has no leading paragraph. +``` + +`lat check` enforces this rule. + +## Section IDs + +Sections are addressed by file path and heading chain: + +- **Full form**: `lat.md/path/to/file#Heading#SubHeading` +- **Short form**: `file#Heading#SubHeading` (when the file stem is unique) + +Examples: `lat.md/tests/search#RAG Replay Tests`, `cli#init`, `parser#Wiki Links`. + +## Wiki links + +Cross-reference other sections or source code with `[[target]]` or `[[target|alias]]`. + +### Section links + +```markdown +See [[cli#init]] for setup details. +The parser validates [[parser#Wiki Links|wiki link syntax]]. +``` + +### Source code links + +Reference functions, classes, constants, and methods in source files: + +```markdown +[[src/config.ts#getConfigDir]] — function +[[src/server.ts#App#listen]] — class method +[[lib/utils.py#parse_args]] — Python function +[[src/lib.rs#Greeter#greet]] — Rust impl method +[[src/app.go#Greeter#Greet]] — Go method +[[src/app.h#Greeter]] — C struct +``` + +`lat check` validates that all targets exist. + +## Code refs + +Tie source code back to `lat.md/` sections with `@lat:` comments: + +```typescript +// @lat: [[cli#init]] +export function init() { ... } +``` + +```python +# @lat: [[cli#init]] +def init(): + ... +``` + +Supported comment styles: `//` (JS/TS/Rust/Go/C) and `#` (Python). + +Place one `@lat:` comment per section, at the relevant code — not at the top of the file. + +## Test specs + +Describe tests as sections in `lat.md/` files. Add frontmatter to require that every leaf section has a matching `@lat:` comment in test code: + +```markdown +--- +lat: + require-code-mention: true +--- +# Tests + +Authentication test specifications. + +## User login + +Verify credential validation and error handling. + +### Rejects expired tokens + +Tokens past their expiry timestamp are rejected with 401, even if otherwise valid. + +### Handles missing password + +Login request without a password field returns 400 with a descriptive error. +``` + +Each test references its spec: + +```python +# @lat: [[tests#User login#Rejects expired tokens]] +def test_rejects_expired_tokens(): + ... +``` + +Rules: +- Every leaf section under `require-code-mention: true` must be referenced by exactly one `@lat:` comment +- Every section MUST have a description — at least one sentence explaining what the test verifies and why +- `lat check` flags unreferenced specs and dangling code refs + +## Frontmatter + +Optional YAML frontmatter at the top of `lat.md/` files: + +```yaml +--- +lat: + require-code-mention: true +--- +``` + +Currently the only supported field is `require-code-mention` for test spec enforcement. + +## Validation + +Always run `lat check` after editing `lat.md/` files. It validates: +- All wiki links point to existing sections or source code symbols +- All `@lat:` code refs point to existing sections +- Every section has a leading paragraph (≤250 chars) +- All `require-code-mention` leaf sections are referenced in code diff --git a/.agents/skills/typescript-expert/SKILL.md b/.agents/skills/typescript-expert/SKILL.md new file mode 100644 index 0000000..8dc79c1 --- /dev/null +++ b/.agents/skills/typescript-expert/SKILL.md @@ -0,0 +1,470 @@ +--- +name: typescript-expert +description: TypeScript and JavaScript expert with deep knowledge of type-level programming, performance optimization, monorepo management, migration strategies, and modern tooling. +category: framework +risk: critical +source: community +date_added: "2026-02-27" +--- + +# TypeScript Expert + +You are an advanced TypeScript expert with deep, practical knowledge of type-level programming, performance optimization, and real-world problem solving based on current best practices. + +## When invoked: + +0. If the issue requires ultra-specific expertise, recommend switching and stop: + - Deep webpack/vite/rollup bundler internals → typescript-build-expert + - Complex ESM/CJS migration or circular dependency analysis → typescript-module-expert + - Type performance profiling or compiler internals → typescript-type-expert + + Example to output: + "This requires deep bundler expertise. Please invoke: 'Use the typescript-build-expert subagent.' Stopping here." + +1. Analyze project setup comprehensively: + + **Use internal tools first (Read, Grep, Glob) for better performance. Shell commands are fallbacks.** + + ```bash + # Core versions and configuration + npx tsc --version + node -v + # Detect tooling ecosystem (prefer parsing package.json) + node -e "const p=require('./package.json');console.log(Object.keys({...p.devDependencies,...p.dependencies}||{}).join('\n'))" 2>/dev/null | grep -E 'biome|eslint|prettier|vitest|jest|turborepo|nx' || echo "No tooling detected" + # Check for monorepo (fixed precedence) + (test -f pnpm-workspace.yaml || test -f lerna.json || test -f nx.json || test -f turbo.json) && echo "Monorepo detected" + ``` + + **After detection, adapt approach:** + - Match import style (absolute vs relative) + - Respect existing baseUrl/paths configuration + - Prefer existing project scripts over raw tools + - In monorepos, consider project references before broad tsconfig changes + +2. Identify the specific problem category and complexity level + +3. Apply the appropriate solution strategy from my expertise + +4. Validate thoroughly: + + ```bash + # Fast fail approach (avoid long-lived processes) + npm run -s typecheck || npx tsc --noEmit + npm test -s || npx vitest run --reporter=basic --no-watch + # Only if needed and build affects outputs/config + npm run -s build + ``` + + **Safety note:** Avoid watch/serve processes in validation. Use one-shot diagnostics only. + +## Advanced Type System Expertise + +### Type-Level Programming Patterns + +**Branded Types for Domain Modeling** + +```typescript +// Create nominal types to prevent primitive obsession +type Brand = K & { __brand: T }; +type UserId = Brand; +type OrderId = Brand; + +// Prevents accidental mixing of domain primitives +function processOrder(orderId: OrderId, userId: UserId) {} +``` + +- Use for: Critical domain primitives, API boundaries, currency/units +- Resource: https://egghead.io/blog/using-branded-types-in-typescript + +**Advanced Conditional Types** + +```typescript +// Recursive type manipulation +type DeepReadonly = T extends (...args: any[]) => any + ? T + : T extends object + ? { readonly [K in keyof T]: DeepReadonly } + : T; + +// Template literal type magic +type PropEventSource = { + on( + eventName: `${Key}Changed`, + callback: (newValue: Type[Key]) => void, + ): void; +}; +``` + +- Use for: Library APIs, type-safe event systems, compile-time validation +- Watch for: Type instantiation depth errors (limit recursion to 10 levels) + +**Type Inference Techniques** + +```typescript +// Use 'satisfies' for constraint validation (TS 5.0+) +const config = { + api: "https://api.example.com", + timeout: 5000, +} satisfies Record; +// Preserves literal types while ensuring constraints + +// Const assertions for maximum inference +const routes = ["/home", "/about", "/contact"] as const; +type Route = (typeof routes)[number]; // '/home' | '/about' | '/contact' +``` + +### Performance Optimization Strategies + +**Type Checking Performance** + +```bash +# Diagnose slow type checking +npx tsc --extendedDiagnostics --incremental false | grep -E "Check time|Files:|Lines:|Nodes:" + +# Common fixes for "Type instantiation is excessively deep" +# 1. Replace type intersections with interfaces +# 2. Split large union types (>100 members) +# 3. Avoid circular generic constraints +# 4. Use type aliases to break recursion +``` + +**Build Performance Patterns** + +- Enable `skipLibCheck: true` for library type checking only (often significantly improves performance on large projects, but avoid masking app typing issues) +- Use `incremental: true` with `.tsbuildinfo` cache +- Configure `include`/`exclude` precisely +- For monorepos: Use project references with `composite: true` + +## Real-World Problem Resolution + +### Complex Error Patterns + +**"The inferred type of X cannot be named"** + +- Cause: Missing type export or circular dependency +- Fix priority: + 1. Export the required type explicitly + 2. Use `ReturnType` helper + 3. Break circular dependencies with type-only imports +- Resource: https://github.com/microsoft/TypeScript/issues/47663 + +**Missing type declarations** + +- Quick fix with ambient declarations: + +```typescript +// types/ambient.d.ts +declare module "some-untyped-package" { + const value: unknown; + export default value; + export = value; // if CJS interop is needed +} +``` + +- For more details: [Declaration Files Guide](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html) + +**"Excessive stack depth comparing types"** + +- Cause: Circular or deeply recursive types +- Fix priority: + 1. Limit recursion depth with conditional types + 2. Use `interface` extends instead of type intersection + 3. Simplify generic constraints + +```typescript +// Bad: Infinite recursion +type InfiniteArray = T | InfiniteArray[]; + +// Good: Limited recursion +type NestedArray = D extends 0 + ? T + : T | NestedArray[]; +``` + +**Module Resolution Mysteries** + +- "Cannot find module" despite file existing: + 1. Check `moduleResolution` matches your bundler + 2. Verify `baseUrl` and `paths` alignment + 3. For monorepos: Ensure workspace protocol (workspace:\*) + 4. Try clearing cache: `rm -rf node_modules/.cache .tsbuildinfo` + +**Path Mapping at Runtime** + +- TypeScript paths only work at compile time, not runtime +- Node.js runtime solutions: + - ts-node: Use `ts-node -r tsconfig-paths/register` + - Node ESM: Use loader alternatives or avoid TS paths at runtime + - Production: Pre-compile with resolved paths + +### Migration Expertise + +**JavaScript to TypeScript Migration** + +```bash +# Incremental migration strategy +# 1. Enable allowJs and checkJs (merge into existing tsconfig.json): +# Add to existing tsconfig.json: +# { +# "compilerOptions": { +# "allowJs": true, +# "checkJs": true +# } +# } + +# 2. Rename files gradually (.js → .ts) +# 3. Add types file by file using AI assistance +# 4. Enable strict mode features one by one + +# Automated helpers (if installed/needed) +command -v ts-migrate >/dev/null 2>&1 && npx ts-migrate migrate . --sources 'src/**/*.js' +command -v typesync >/dev/null 2>&1 && npx typesync # Install missing @types packages +``` + +**Tool Migration Decisions** + +| From | To | When | Migration Effort | +| ----------------- | --------------- | --------------------------------------------- | ----------------- | +| ESLint + Prettier | Biome | Need much faster speed, okay with fewer rules | Low (1 day) | +| TSC for linting | Type-check only | Have 100+ files, need faster feedback | Medium (2-3 days) | +| Lerna | Nx/Turborepo | Need caching, parallel builds | High (1 week) | +| CJS | ESM | Node 18+, modern tooling | High (varies) | + +### Monorepo Management + +**Nx vs Turborepo Decision Matrix** + +- Choose **Turborepo** if: Simple structure, need speed, <20 packages +- Choose **Nx** if: Complex dependencies, need visualization, plugins required +- Performance: Nx often performs better on large monorepos (>50 packages) + +**TypeScript Monorepo Configuration** + +```json +// Root tsconfig.json +{ + "references": [ + { "path": "./packages/core" }, + { "path": "./packages/ui" }, + { "path": "./apps/web" } + ], + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true + } +} +``` + +## Modern Tooling Expertise + +### Biome vs ESLint + +**Use Biome when:** + +- Speed is critical (often faster than traditional setups) +- Want single tool for lint + format +- TypeScript-first project +- Okay with 64 TS rules vs 100+ in typescript-eslint + +**Stay with ESLint when:** + +- Need specific rules/plugins +- Have complex custom rules +- Working with Vue/Angular (limited Biome support) +- Need type-aware linting (Biome doesn't have this yet) + +### Type Testing Strategies + +**Vitest Type Testing (Recommended)** + +```typescript +// in avatar.test-d.ts +import { expectTypeOf } from "vitest"; +import type { Avatar } from "./avatar"; + +test("Avatar props are correctly typed", () => { + expectTypeOf().toHaveProperty("size"); + expectTypeOf().toEqualTypeOf<"sm" | "md" | "lg">(); +}); +``` + +**When to Test Types:** + +- Publishing libraries +- Complex generic functions +- Type-level utilities +- API contracts + +## Debugging Mastery + +### CLI Debugging Tools + +```bash +# Debug TypeScript files directly (if tools installed) +command -v tsx >/dev/null 2>&1 && npx tsx --inspect src/file.ts +command -v ts-node >/dev/null 2>&1 && npx ts-node --inspect-brk src/file.ts + +# Trace module resolution issues +npx tsc --traceResolution > resolution.log 2>&1 +grep "Module resolution" resolution.log + +# Debug type checking performance (use --incremental false for clean trace) +npx tsc --generateTrace trace --incremental false +# Analyze trace (if installed) +command -v @typescript/analyze-trace >/dev/null 2>&1 && npx @typescript/analyze-trace trace + +# Memory usage analysis +node --max-old-space-size=8192 node_modules/typescript/lib/tsc.js +``` + +### Custom Error Classes + +```typescript +// Proper error class with stack preservation +class DomainError extends Error { + constructor( + message: string, + public code: string, + public statusCode: number, + ) { + super(message); + this.name = "DomainError"; + Error.captureStackTrace(this, this.constructor); + } +} +``` + +## Current Best Practices + +### Strict by Default + +```json +{ + "compilerOptions": { + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "exactOptionalPropertyTypes": true, + "noPropertyAccessFromIndexSignature": true + } +} +``` + +### ESM-First Approach + +- Set `"type": "module"` in package.json +- Use `.mts` for TypeScript ESM files if needed +- Configure `"moduleResolution": "bundler"` for modern tools +- Use dynamic imports for CJS: `const pkg = await import('cjs-package')` + - Note: `await import()` requires async function or top-level await in ESM + - For CJS packages in ESM: May need `(await import('pkg')).default` depending on the package's export structure and your compiler settings + +### AI-Assisted Development + +- GitHub Copilot excels at TypeScript generics +- Use AI for boilerplate type definitions +- Validate AI-generated types with type tests +- Document complex types for AI context + +## Code Review Checklist + +When reviewing TypeScript/JavaScript code, focus on these domain-specific aspects: + +### Type Safety + +- [ ] No implicit `any` types (use `unknown` or proper types) +- [ ] Strict null checks enabled and properly handled +- [ ] Type assertions (`as`) justified and minimal +- [ ] Generic constraints properly defined +- [ ] Discriminated unions for error handling +- [ ] Return types explicitly declared for public APIs + +### TypeScript Best Practices + +- [ ] Prefer `interface` over `type` for object shapes (better error messages) +- [ ] Use const assertions for literal types +- [ ] Leverage type guards and predicates +- [ ] Avoid type gymnastics when simpler solution exists +- [ ] Template literal types used appropriately +- [ ] Branded types for domain primitives + +### Performance Considerations + +- [ ] Type complexity doesn't cause slow compilation +- [ ] No excessive type instantiation depth +- [ ] Avoid complex mapped types in hot paths +- [ ] Use `skipLibCheck: true` in tsconfig +- [ ] Project references configured for monorepos + +### Module System + +- [ ] Consistent import/export patterns +- [ ] No circular dependencies +- [ ] Proper use of barrel exports (avoid over-bundling) +- [ ] ESM/CJS compatibility handled correctly +- [ ] Dynamic imports for code splitting + +### Error Handling Patterns + +- [ ] Result types or discriminated unions for errors +- [ ] Custom error classes with proper inheritance +- [ ] Type-safe error boundaries +- [ ] Exhaustive switch cases with `never` type + +### Code Organization + +- [ ] Types co-located with implementation +- [ ] Shared types in dedicated modules +- [ ] Avoid global type augmentation when possible +- [ ] Proper use of declaration files (.d.ts) + +## Quick Decision Trees + +### "Which tool should I use?" + +``` +Type checking only? → tsc +Type checking + linting speed critical? → Biome +Type checking + comprehensive linting? → ESLint + typescript-eslint +Type testing? → Vitest expectTypeOf +Build tool? → Project size <10 packages? Turborepo. Else? Nx +``` + +### "How do I fix this performance issue?" + +``` +Slow type checking? → skipLibCheck, incremental, project references +Slow builds? → Check bundler config, enable caching +Slow tests? → Vitest with threads, avoid type checking in tests +Slow language server? → Exclude node_modules, limit files in tsconfig +``` + +## Expert Resources + +### Performance + +- [TypeScript Wiki Performance](https://github.com/microsoft/TypeScript/wiki/Performance) +- [Type instantiation tracking](https://github.com/microsoft/TypeScript/pull/48077) + +### Advanced Patterns + +- [Type Challenges](https://github.com/type-challenges/type-challenges) +- [Type-Level TypeScript Course](https://type-level-typescript.com) + +### Tools + +- [Biome](https://biomejs.dev) - Fast linter/formatter +- [TypeStat](https://github.com/JoshuaKGoldberg/TypeStat) - Auto-fix TypeScript types +- [ts-migrate](https://github.com/airbnb/ts-migrate) - Migration toolkit + +### Testing + +- [Vitest Type Testing](https://vitest.dev/guide/testing-types) +- [tsd](https://github.com/tsdjs/tsd) - Standalone type testing + +Always validate changes don't break existing functionality before considering the issue resolved. + +## When to Use + +This skill is applicable to execute the workflow or actions described in the overview. diff --git a/.agents/skills/typescript-expert/references/tsconfig-strict.json b/.agents/skills/typescript-expert/references/tsconfig-strict.json new file mode 100644 index 0000000..36da566 --- /dev/null +++ b/.agents/skills/typescript-expert/references/tsconfig-strict.json @@ -0,0 +1,74 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Strict TypeScript 5.x", + "compilerOptions": { + // ========================================================================= + // STRICTNESS (Maximum Type Safety) + // ========================================================================= + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "exactOptionalPropertyTypes": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true, + // ========================================================================= + // MODULE SYSTEM (Modern ESM) + // ========================================================================= + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + // ========================================================================= + // OUTPUT + // ========================================================================= + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + // ========================================================================= + // PERFORMANCE + // ========================================================================= + "skipLibCheck": true, + "incremental": true, + // ========================================================================= + // PATH ALIASES + // ========================================================================= + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@/components/*": ["./src/components/*"], + "@/lib/*": ["./src/lib/*"], + "@/types/*": ["./src/types/*"], + "@/utils/*": ["./src/utils/*"] + }, + // ========================================================================= + // JSX (for React projects) + // ========================================================================= + // "jsx": "react-jsx", + // ========================================================================= + // EMIT + // ========================================================================= + "noEmit": true // Let bundler handle emit + // "outDir": "./dist", + // "rootDir": "./src", + // ========================================================================= + // DECORATORS (if needed) + // ========================================================================= + // "experimentalDecorators": true, + // "emitDecoratorMetadata": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts"], + "exclude": [ + "node_modules", + "dist", + "build", + "coverage", + "**/*.test.ts", + "**/*.spec.ts" + ] +} diff --git a/.agents/skills/typescript-expert/references/typescript-cheatsheet.md b/.agents/skills/typescript-expert/references/typescript-cheatsheet.md new file mode 100644 index 0000000..fb08eb6 --- /dev/null +++ b/.agents/skills/typescript-expert/references/typescript-cheatsheet.md @@ -0,0 +1,393 @@ +# TypeScript Cheatsheet + +## Type Basics + +```typescript +// Primitives +const name: string = "John"; +const age: number = 30; +const isActive: boolean = true; +const nothing: null = null; +const notDefined: undefined = undefined; + +// Arrays +const numbers: number[] = [1, 2, 3]; +const strings: Array = ["a", "b", "c"]; + +// Tuple +const tuple: [string, number] = ["hello", 42]; + +// Object +const user: { name: string; age: number } = { name: "John", age: 30 }; + +// Union +const value: string | number = "hello"; + +// Literal +const direction: "up" | "down" | "left" | "right" = "up"; + +// Any vs Unknown +const anyValue: any = "anything"; // ❌ Avoid +const unknownValue: unknown = "safe"; // ✅ Prefer, requires narrowing +``` + +## Type Aliases & Interfaces + +```typescript +// Type Alias +type Point = { + x: number; + y: number; +}; + +// Interface (preferred for objects) +interface User { + id: string; + name: string; + email?: string; // Optional + readonly createdAt: Date; // Readonly +} + +// Extending +interface Admin extends User { + permissions: string[]; +} + +// Intersection +type AdminUser = User & { permissions: string[] }; +``` + +## Generics + +```typescript +// Generic function +function identity(value: T): T { + return value; +} + +// Generic with constraint +function getLength(item: T): number { + return item.length; +} + +// Generic interface +interface ApiResponse { + data: T; + status: number; + message: string; +} + +// Generic with default +type Container = { + value: T; +}; + +// Multiple generics +function merge(obj1: T, obj2: U): T & U { + return { ...obj1, ...obj2 }; +} +``` + +## Utility Types + +```typescript +interface User { + id: string; + name: string; + email: string; + age: number; +} + +// Partial - all optional +type PartialUser = Partial; + +// Required - all required +type RequiredUser = Required; + +// Readonly - all readonly +type ReadonlyUser = Readonly; + +// Pick - select properties +type UserName = Pick; + +// Omit - exclude properties +type UserWithoutEmail = Omit; + +// Record - key-value map +type UserMap = Record; + +// Extract - extract from union +type StringOrNumber = string | number | boolean; +type OnlyStrings = Extract; + +// Exclude - exclude from union +type NotString = Exclude; + +// NonNullable - remove null/undefined +type MaybeString = string | null | undefined; +type DefinitelyString = NonNullable; + +// ReturnType - get function return type +function getUser() { + return { name: "John" }; +} +type UserReturn = ReturnType; + +// Parameters - get function parameters +type GetUserParams = Parameters; + +// Awaited - unwrap Promise +type ResolvedUser = Awaited>; +``` + +## Conditional Types + +```typescript +// Basic conditional +type IsString = T extends string ? true : false; + +// Infer keyword +type UnwrapPromise = T extends Promise ? U : T; + +// Distributive conditional +type ToArray = T extends any ? T[] : never; +type Result = ToArray; // string[] | number[] + +// NonDistributive +type ToArrayNonDist = [T] extends [any] ? T[] : never; +``` + +## Template Literal Types + +```typescript +type Color = "red" | "green" | "blue"; +type Size = "small" | "medium" | "large"; + +// Combine +type ColorSize = `${Color}-${Size}`; +// 'red-small' | 'red-medium' | 'red-large' | ... + +// Event handlers +type EventName = "click" | "focus" | "blur"; +type EventHandler = `on${Capitalize}`; +// 'onClick' | 'onFocus' | 'onBlur' +``` + +## Mapped Types + +```typescript +// Basic mapped type +type Optional = { + [K in keyof T]?: T[K]; +}; + +// With key remapping +type Getters = { + [K in keyof T as `get${Capitalize}`]: () => T[K]; +}; + +// Filter keys +type OnlyStrings = { + [K in keyof T as T[K] extends string ? K : never]: T[K]; +}; +``` + +## Type Guards + +```typescript +// typeof guard +function process(value: string | number) { + if (typeof value === "string") { + return value.toUpperCase(); // string + } + return value.toFixed(2); // number +} + +// instanceof guard +class Dog { + bark() {} +} +class Cat { + meow() {} +} + +function makeSound(animal: Dog | Cat) { + if (animal instanceof Dog) { + animal.bark(); + } else { + animal.meow(); + } +} + +// in guard +interface Bird { + fly(): void; +} +interface Fish { + swim(): void; +} + +function move(animal: Bird | Fish) { + if ("fly" in animal) { + animal.fly(); + } else { + animal.swim(); + } +} + +// Custom type guard +function isString(value: unknown): value is string { + return typeof value === "string"; +} + +// Assertion function +function assertIsString(value: unknown): asserts value is string { + if (typeof value !== "string") { + throw new Error("Not a string"); + } +} +``` + +## Discriminated Unions + +```typescript +// With type discriminant +type Success = { type: "success"; data: T }; +type Error = { type: "error"; message: string }; +type Loading = { type: "loading" }; + +type State = Success | Error | Loading; + +function handle(state: State) { + switch (state.type) { + case "success": + return state.data; // T + case "error": + return state.message; // string + case "loading": + return null; + } +} + +// Exhaustive check +function assertNever(value: never): never { + throw new Error(`Unexpected value: ${value}`); +} +``` + +## Branded Types + +```typescript +// Create branded type +type Brand = K & { __brand: T }; + +type UserId = Brand; +type OrderId = Brand; + +// Constructor functions +function createUserId(id: string): UserId { + return id as UserId; +} + +function createOrderId(id: string): OrderId { + return id as OrderId; +} + +// Usage - prevents mixing +function getOrder(orderId: OrderId, userId: UserId) {} + +const userId = createUserId("user-123"); +const orderId = createOrderId("order-456"); + +getOrder(orderId, userId); // ✅ OK +// getOrder(userId, orderId) // ❌ Error - types don't match +``` + +## Module Declarations + +```typescript +// Declare module for untyped package +declare module "untyped-package" { + export function doSomething(): void; + export const value: string; +} + +// Augment existing module +declare module "express" { + interface Request { + user?: { id: string }; + } +} + +// Declare global +declare global { + interface Window { + myGlobal: string; + } +} +``` + +## TSConfig Essentials + +```json +{ + "compilerOptions": { + // Strictness + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Modules + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + + // Output + "target": "ES2022", + "lib": ["ES2022", "DOM"], + + // Performance + "skipLibCheck": true, + "incremental": true, + + // Paths + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} +``` + +## Best Practices + +```typescript +// ✅ Prefer interface for objects +interface User { + name: string; +} + +// ✅ Use const assertions +const routes = ["home", "about"] as const; + +// ✅ Use satisfies for validation +const config = { + api: "https://api.example.com", +} satisfies Record; + +// ✅ Use unknown over any +function parse(input: unknown) { + if (typeof input === "string") { + return JSON.parse(input); + } +} + +// ✅ Explicit return types for public APIs +export function getUser(id: string): User | null { + // ... +} + +// ❌ Avoid +const data: any = fetchData(); +data.anything.goes.wrong; // No type safety +``` diff --git a/.agents/skills/typescript-expert/references/utility-types.ts b/.agents/skills/typescript-expert/references/utility-types.ts new file mode 100644 index 0000000..b0a93bc --- /dev/null +++ b/.agents/skills/typescript-expert/references/utility-types.ts @@ -0,0 +1,332 @@ +/** + * TypeScript Utility Types Library + * + * A collection of commonly used utility types for TypeScript projects. + * Copy and use as needed in your projects. + */ + +// ============================================================================= +// BRANDED TYPES +// ============================================================================= + +/** + * Create nominal/branded types to prevent primitive obsession. + * + * @example + * type UserId = Brand + * type OrderId = Brand + */ +export type Brand = K & { readonly __brand: T }; + +// Branded type constructors +export type UserId = Brand; +export type Email = Brand; +export type UUID = Brand; +export type Timestamp = Brand; +export type PositiveNumber = Brand; + +// ============================================================================= +// RESULT TYPE (Error Handling) +// ============================================================================= + +/** + * Type-safe error handling without exceptions. + */ +export type Result = + | { success: true; data: T } + | { success: false; error: E }; + +export const ok = (data: T): Result => ({ + success: true, + data, +}); + +export const err = (error: E): Result => ({ + success: false, + error, +}); + +// ============================================================================= +// OPTION TYPE (Nullable Handling) +// ============================================================================= + +/** + * Explicit optional value handling. + */ +export type Option = Some | None; + +export type Some = { type: "some"; value: T }; +export type None = { type: "none" }; + +export const some = (value: T): Some => ({ type: "some", value }); +export const none: None = { type: "none" }; + +// ============================================================================= +// DEEP UTILITIES +// ============================================================================= + +/** + * Make all properties deeply readonly. + */ +export type DeepReadonly = T extends (...args: any[]) => any + ? T + : T extends object + ? { readonly [K in keyof T]: DeepReadonly } + : T; + +/** + * Make all properties deeply optional. + */ +export type DeepPartial = T extends object + ? { [K in keyof T]?: DeepPartial } + : T; + +/** + * Make all properties deeply required. + */ +export type DeepRequired = T extends object + ? { [K in keyof T]-?: DeepRequired } + : T; + +/** + * Make all properties deeply mutable (remove readonly). + */ +export type DeepMutable = T extends object + ? { -readonly [K in keyof T]: DeepMutable } + : T; + +// ============================================================================= +// OBJECT UTILITIES +// ============================================================================= + +/** + * Get keys of object where value matches type. + */ +export type KeysOfType = { + [K in keyof T]: T[K] extends V ? K : never; +}[keyof T]; + +/** + * Pick properties by value type. + */ +export type PickByType = Pick>; + +/** + * Omit properties by value type. + */ +export type OmitByType = Omit>; + +/** + * Make specific keys optional. + */ +export type PartialBy = Omit & Partial>; + +/** + * Make specific keys required. + */ +export type RequiredBy = Omit & + Required>; + +/** + * Make specific keys readonly. + */ +export type ReadonlyBy = Omit & + Readonly>; + +/** + * Merge two types (second overrides first). + */ +export type Merge = Omit & U; + +// ============================================================================= +// ARRAY UTILITIES +// ============================================================================= + +/** + * Get element type from array. + */ +export type ElementOf = T extends (infer E)[] ? E : never; + +/** + * Tuple of specific length. + */ +export type Tuple = N extends N + ? number extends N + ? T[] + : _TupleOf + : never; + +type _TupleOf = R["length"] extends N + ? R + : _TupleOf; + +/** + * Non-empty array. + */ +export type NonEmptyArray = [T, ...T[]]; + +/** + * At least N elements. + */ +export type AtLeast = [...Tuple, ...T[]]; + +// ============================================================================= +// FUNCTION UTILITIES +// ============================================================================= + +/** + * Get function arguments as tuple. + */ +export type Arguments = T extends (...args: infer A) => any ? A : never; + +/** + * Get first argument of function. + */ +export type FirstArgument = T extends (first: infer F, ...args: any[]) => any + ? F + : never; + +/** + * Async version of function. + */ +export type AsyncFunction any> = ( + ...args: Parameters +) => Promise>>; + +/** + * Promisify return type. + */ +export type Promisify = T extends (...args: infer A) => infer R + ? (...args: A) => Promise> + : never; + +// ============================================================================= +// STRING UTILITIES +// ============================================================================= + +/** + * Split string by delimiter. + */ +export type Split< + S extends string, + D extends string, +> = S extends `${infer T}${D}${infer U}` ? [T, ...Split] : [S]; + +/** + * Join tuple to string. + */ +export type Join = T extends [] + ? "" + : T extends [infer F extends string] + ? F + : T extends [infer F extends string, ...infer R extends string[]] + ? `${F}${D}${Join}` + : never; + +/** + * Path to nested object. + */ +export type PathOf = K extends string + ? T[K] extends object + ? K | `${K}.${PathOf}` + : K + : never; + +// ============================================================================= +// UNION UTILITIES +// ============================================================================= + +/** + * Last element of union. + */ +export type UnionLast = + UnionToIntersection T : never> extends () => infer R + ? R + : never; + +/** + * Union to intersection. + */ +export type UnionToIntersection = ( + U extends any ? (k: U) => void : never +) extends (k: infer I) => void + ? I + : never; + +/** + * Union to tuple. + */ +export type UnionToTuple> = [T] extends [never] + ? [] + : [...UnionToTuple>, L]; + +// ============================================================================= +// VALIDATION UTILITIES +// ============================================================================= + +/** + * Assert type at compile time. + */ +export type AssertEqual = + (() => V extends T ? 1 : 2) extends () => V extends U ? 1 : 2 + ? true + : false; + +/** + * Ensure type is not never. + */ +export type IsNever = [T] extends [never] ? true : false; + +/** + * Ensure type is any. + */ +export type IsAny = 0 extends 1 & T ? true : false; + +/** + * Ensure type is unknown. + */ +export type IsUnknown = + IsAny extends true ? false : unknown extends T ? true : false; + +// ============================================================================= +// JSON UTILITIES +// ============================================================================= + +/** + * JSON-safe types. + */ +export type JsonPrimitive = string | number | boolean | null; +export type JsonArray = JsonValue[]; +export type JsonObject = { [key: string]: JsonValue }; +export type JsonValue = JsonPrimitive | JsonArray | JsonObject; + +/** + * Make type JSON-serializable. + */ +export type Jsonify = T extends JsonPrimitive + ? T + : T extends undefined | ((...args: any[]) => any) | symbol + ? never + : T extends { toJSON(): infer R } + ? R + : T extends object + ? { [K in keyof T]: Jsonify } + : never; + +// ============================================================================= +// EXHAUSTIVE CHECK +// ============================================================================= + +/** + * Ensure all cases are handled in switch/if. + */ +export function assertNever(value: never, message?: string): never { + throw new Error(message ?? `Unexpected value: ${value}`); +} + +/** + * Exhaustive check without throwing. + */ +export function exhaustiveCheck(_value: never): void { + // This function should never be called +} diff --git a/.agents/skills/typescript-expert/scripts/ts_diagnostic.py b/.agents/skills/typescript-expert/scripts/ts_diagnostic.py new file mode 100644 index 0000000..3d42e90 --- /dev/null +++ b/.agents/skills/typescript-expert/scripts/ts_diagnostic.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +""" +TypeScript Project Diagnostic Script +Analyzes TypeScript projects for configuration, performance, and common issues. +""" + +import subprocess +import sys +import os +import json +from pathlib import Path + +def run_cmd(cmd: str) -> str: + """Run shell command and return output.""" + try: + result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + return result.stdout + result.stderr + except Exception as e: + return str(e) + +def check_versions(): + """Check TypeScript and Node versions.""" + print("\n📦 Versions:") + print("-" * 40) + + ts_version = run_cmd("npx tsc --version 2>/dev/null").strip() + node_version = run_cmd("node -v 2>/dev/null").strip() + + print(f" TypeScript: {ts_version or 'Not found'}") + print(f" Node.js: {node_version or 'Not found'}") + +def check_tsconfig(): + """Analyze tsconfig.json settings.""" + print("\n⚙️ TSConfig Analysis:") + print("-" * 40) + + tsconfig_path = Path("tsconfig.json") + if not tsconfig_path.exists(): + print("⚠️ tsconfig.json not found") + return + + try: + with open(tsconfig_path) as f: + config = json.load(f) + + compiler_opts = config.get("compilerOptions", {}) + + # Check strict mode + if compiler_opts.get("strict"): + print("✅ Strict mode enabled") + else: + print("⚠️ Strict mode NOT enabled") + + # Check important flags + flags = { + "noUncheckedIndexedAccess": "Unchecked index access protection", + "noImplicitOverride": "Implicit override protection", + "skipLibCheck": "Skip lib check (performance)", + "incremental": "Incremental compilation" + } + + for flag, desc in flags.items(): + status = "✅" if compiler_opts.get(flag) else "⚪" + print(f" {status} {desc}: {compiler_opts.get(flag, 'not set')}") + + # Check module settings + print(f"\n Module: {compiler_opts.get('module', 'not set')}") + print(f" Module Resolution: {compiler_opts.get('moduleResolution', 'not set')}") + print(f" Target: {compiler_opts.get('target', 'not set')}") + + except json.JSONDecodeError: + print("❌ Invalid JSON in tsconfig.json") + +def check_tooling(): + """Detect TypeScript tooling ecosystem.""" + print("\n🛠️ Tooling Detection:") + print("-" * 40) + + pkg_path = Path("package.json") + if not pkg_path.exists(): + print("⚠️ package.json not found") + return + + try: + with open(pkg_path) as f: + pkg = json.load(f) + + all_deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})} + + tools = { + "biome": "Biome (linter/formatter)", + "eslint": "ESLint", + "prettier": "Prettier", + "vitest": "Vitest (testing)", + "jest": "Jest (testing)", + "turborepo": "Turborepo (monorepo)", + "turbo": "Turbo (monorepo)", + "nx": "Nx (monorepo)", + "lerna": "Lerna (monorepo)" + } + + for tool, desc in tools.items(): + for dep in all_deps: + if tool in dep.lower(): + print(f" ✅ {desc}") + break + + except json.JSONDecodeError: + print("❌ Invalid JSON in package.json") + +def check_monorepo(): + """Check for monorepo configuration.""" + print("\n📦 Monorepo Check:") + print("-" * 40) + + indicators = [ + ("pnpm-workspace.yaml", "PNPM Workspace"), + ("lerna.json", "Lerna"), + ("nx.json", "Nx"), + ("turbo.json", "Turborepo") + ] + + found = False + for file, name in indicators: + if Path(file).exists(): + print(f" ✅ {name} detected") + found = True + + if not found: + print(" ⚪ No monorepo configuration detected") + +def check_type_errors(): + """Run quick type check.""" + print("\n🔍 Type Check:") + print("-" * 40) + + result = run_cmd("npx tsc --noEmit 2>&1 | head -20") + if "error TS" in result: + errors = result.count("error TS") + print(f" ❌ {errors}+ type errors found") + print(result[:500]) + else: + print(" ✅ No type errors") + +def check_any_usage(): + """Check for any type usage.""" + print("\n⚠️ 'any' Type Usage:") + print("-" * 40) + + result = run_cmd("grep -r ': any' --include='*.ts' --include='*.tsx' src/ 2>/dev/null | wc -l") + count = result.strip() + if count and count != "0": + print(f" ⚠️ Found {count} occurrences of ': any'") + sample = run_cmd("grep -rn ': any' --include='*.ts' --include='*.tsx' src/ 2>/dev/null | head -5") + if sample: + print(sample) + else: + print(" ✅ No explicit 'any' types found") + +def check_type_assertions(): + """Check for type assertions.""" + print("\n⚠️ Type Assertions (as):") + print("-" * 40) + + result = run_cmd("grep -r ' as ' --include='*.ts' --include='*.tsx' src/ 2>/dev/null | grep -v 'import' | wc -l") + count = result.strip() + if count and count != "0": + print(f" ⚠️ Found {count} type assertions") + else: + print(" ✅ No type assertions found") + +def check_performance(): + """Check type checking performance.""" + print("\n⏱️ Type Check Performance:") + print("-" * 40) + + result = run_cmd("npx tsc --extendedDiagnostics --noEmit 2>&1 | grep -E 'Check time|Files:|Lines:|Nodes:'") + if result.strip(): + for line in result.strip().split('\n'): + print(f" {line}") + else: + print(" ⚠️ Could not measure performance") + +def main(): + print("=" * 50) + print("🔍 TypeScript Project Diagnostic Report") + print("=" * 50) + + check_versions() + check_tsconfig() + check_tooling() + check_monorepo() + check_any_usage() + check_type_assertions() + check_type_errors() + check_performance() + + print("\n" + "=" * 50) + print("✅ Diagnostic Complete") + print("=" * 50) + +if __name__ == "__main__": + main() diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 0000000..aee54d8 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,7 @@ +# Target the Chromium version that the bundled Electron ships. Tooling that +# reads browserslist (PostCSS / Microsoft Edge Tools / linters) will stop +# warning about features that are only "missing" on engines we never ship to. +# +# Electron 39.x bundles Chromium 130+. Bumping this when we upgrade Electron +# keeps the lint signal honest without silencing legitimate compat issues. +last 2 Chrome versions diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..36f1e57 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,24 @@ +{ + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "lat hook claude UserPromptSubmit" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "lat hook claude Stop" + } + ] + } + ] + } +} diff --git a/.claude/skills/electron-pro b/.claude/skills/electron-pro new file mode 120000 index 0000000..8a29704 --- /dev/null +++ b/.claude/skills/electron-pro @@ -0,0 +1 @@ +../../.agents/skills/electron-pro \ No newline at end of file diff --git a/.claude/skills/hermes-agent/SKILL.md b/.claude/skills/hermes-agent/SKILL.md new file mode 100644 index 0000000..c5b8b50 --- /dev/null +++ b/.claude/skills/hermes-agent/SKILL.md @@ -0,0 +1,1765 @@ +--- +name: hermes-agent +description: Expert in building self-improving AI agents with tool use, multi-platform messaging, and a closed learning loop. Proficient in LLM orchestration, tool integration, session management, and agent autonomy. +--- + +# Hermes Agent - Complete Project Guide (A-Z) + +> **Purpose of this document:** A single, comprehensive reference that explains everything about the Hermes Agent project — its architecture, source code, features, release history, and design patterns — so that any AI or developer can fully understand the system. + +--- + +## Table of Contents + +1. [Project Overview](#1-project-overview) +2. [Key Features Summary](#2-key-features-summary) +3. [Installation & Getting Started](#3-installation--getting-started) +4. [Project Structure](#4-project-structure) +5. [Core Architecture](#5-core-architecture) + - 5.1 [AIAgent Class (run_agent.py)](#51-aiagent-class-run_agentpy) + - 5.2 [Tool Orchestration (model_tools.py)](#52-tool-orchestration-model_toolspy) + - 5.3 [Toolset System (toolsets.py)](#53-toolset-system-toolsetspy) + - 5.4 [Tool Registry (tools/registry.py)](#54-tool-registry-toolsregistrypy) + - 5.5 [Session Database (hermes_state.py)](#55-session-database-hermes_statepy) + - 5.6 [Constants & Home Directory (hermes_constants.py)](#56-constants--home-directory-hermes_constantspy) +6. [CLI System](#6-cli-system) + - 6.1 [Interactive CLI (cli.py)](#61-interactive-cli-clipy) + - 6.2 [CLI Entry Point (hermes_cli/main.py)](#62-cli-entry-point-hermes_climainpy) + - 6.3 [Configuration System (hermes_cli/config.py)](#63-configuration-system-hermes_cliconfigpy) + - 6.4 [Slash Command Registry (hermes_cli/commands.py)](#64-slash-command-registry-hermes_clicommandspy) + - 6.5 [Setup Wizard (hermes_cli/setup.py)](#65-setup-wizard-hermes_clisetupy) + - 6.6 [Model Catalog (hermes_cli/models.py)](#66-model-catalog-hermes_climodelspy) + - 6.7 [Skin/Theme Engine (hermes_cli/skin_engine.py)](#67-skintheme-engine-hermes_cliskin_enginepy) +7. [Tool System](#7-tool-system) + - 7.1 [Terminal Tool (tools/terminal_tool.py)](#71-terminal-tool-toolsterminal_toolpy) + - 7.2 [File Tools (tools/file_tools.py)](#72-file-tools-toolsfile_toolspy) + - 7.3 [Web Tools (tools/web_tools.py)](#73-web-tools-toolsweb_toolspy) + - 7.4 [Browser Tool (tools/browser_tool.py)](#74-browser-tool-toolsbrowser_toolpy) + - 7.5 [Delegate Tool (tools/delegate_tool.py)](#75-delegate-tool-toolsdelegate_toolpy) + - 7.6 [MCP Tool (tools/mcp_tool.py)](#76-mcp-tool-toolsmcp_toolpy) + - 7.7 [Approval System (tools/approval.py)](#77-approval-system-toolsapprovalpy) + - 7.8 [Terminal Backends (tools/environments/)](#78-terminal-backends-toolsenvironments) +8. [Agent Internals](#8-agent-internals) + - 8.1 [Prompt Builder (agent/prompt_builder.py)](#81-prompt-builder-agentprompt_builderpy) + - 8.2 [Context Compressor (agent/context_compressor.py)](#82-context-compressor-agentcontext_compressorpy) + - 8.3 [Prompt Caching (agent/prompt_caching.py)](#83-prompt-caching-agentprompt_cachingpy) + - 8.4 [Auxiliary Client (agent/auxiliary_client.py)](#84-auxiliary-client-agentauxiliary_clientpy) + - 8.5 [Display & Spinner (agent/display.py)](#85-display--spinner-agentdisplaypy) + - 8.6 [Skill Commands (agent/skill_commands.py)](#86-skill-commands-agentskill_commandspy) +9. [Messaging Gateway](#9-messaging-gateway) + - 9.1 [GatewayRunner (gateway/run.py)](#91-gatewayrunner-gatewayrunpy) + - 9.2 [Session Store (gateway/session.py)](#92-session-store-gatewaysessionpy) + - 9.3 [Platform Adapters (gateway/platforms/)](#93-platform-adapters-gatewayplatforms) +10. [Cron Scheduling](#10-cron-scheduling) +11. [Skills System](#11-skills-system) +12. [Plugin System](#12-plugin-system) +13. [Memory System](#13-memory-system) +14. [ACP Server (IDE Integration)](#14-acp-server-ide-integration) +15. [API Server](#15-api-server) +16. [MCP Server Mode](#16-mcp-server-mode) +17. [RL Training Environments](#17-rl-training-environments) +18. [Profiles (Multi-Instance)](#18-profiles-multi-instance) +19. [Security Model](#19-security-model) +20. [Provider & Model System](#20-provider--model-system) +21. [Streaming & Reasoning](#21-streaming--reasoning) +22. [Release History](#22-release-history) +23. [File Dependency Chain](#23-file-dependency-chain) +24. [Key Design Patterns](#24-key-design-patterns) +25. [Configuration Reference](#25-configuration-reference) +26. [Known Pitfalls](#26-known-pitfalls) + +--- + +## 1. Project Overview + +**Hermes Agent** is a self-improving AI agent built by [Nous Research](https://nousresearch.com). It is an open-source (MIT licensed), Python-based project that provides: + +- A **full interactive terminal UI** (CLI) for conversing with LLMs +- A **messaging gateway** supporting 16+ platforms (Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Email, etc.) +- A **closed learning loop** — the agent creates skills from experience, improves them during use, nudges itself to persist knowledge, searches past conversations, and builds a deepening model of who you are +- **40+ built-in tools** — terminal execution, file manipulation, web search, browser automation, code execution, image generation, TTS/STT, and more +- **Any LLM provider** — OpenRouter (200+ models), Nous Portal (400+ models), OpenAI, Anthropic, Hugging Face, GitHub Copilot, z.ai/GLM, Kimi/Moonshot, MiniMax, Alibaba/DashScope, custom endpoints +- **Six terminal backends** — local, Docker, SSH, Modal (serverless), Daytona (serverless), Singularity (HPC) +- **Scheduled automations** via built-in cron scheduler +- **IDE integration** via ACP (Agent Communication Protocol) for VS Code, Zed, JetBrains +- **MCP integration** — both client (connect to any MCP server) and server (expose Hermes to MCP clients) +- **RL training** via Atropos environments for training the next generation of tool-calling models + +**Tech Stack:** + +- Python 3.11+ (core agent, tools, gateway, cron) +- Node.js (browser automation via agent-browser) +- SQLite with WAL mode and FTS5 (session storage, full-text search) +- OpenAI-compatible API (primary inference interface) +- Anthropic SDK (native Anthropic support) +- Rich + prompt_toolkit (CLI rendering) + +**Repository:** `github.com/NousResearch/hermes-agent` +**Version:** 0.7.0 (as of April 2026) +**License:** MIT + +--- + +## 2. Key Features Summary + +| Feature | Description | +| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Terminal UI** | Full TUI with multiline editing, slash-command autocomplete, conversation history, interrupt-and-redirect, streaming tool output | +| **Multi-Platform Messaging** | Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Email, Home Assistant, DingTalk, Feishu/Lark, WeCom, Mattermost, SMS, Webhook — all from a single gateway process | +| **Learning Loop** | Agent-curated memory with periodic nudges, autonomous skill creation, skills self-improve during use, FTS5 session search with LLM summarization, Honcho dialectic user modeling | +| **Scheduled Tasks** | Built-in cron scheduler with delivery to any platform (daily reports, nightly backups, weekly audits) | +| **Subagent Delegation** | Spawn isolated subagents for parallel workstreams with restricted toolsets | +| **Execute Code** | Python scripts that call tools via RPC, collapsing multi-step pipelines into zero-context-cost turns | +| **Terminal Backends** | Local, Docker, SSH, Modal, Daytona, Singularity — run on a $5 VPS or a GPU cluster | +| **Skills** | 70+ bundled skills across 28 categories, Skills Hub for community discovery, agentskills.io compatibility | +| **Plugins** | Drop-in Python plugins with lifecycle hooks (pre_llm_call, post_llm_call, on_session_start, on_session_end) | +| **MCP** | Client (connect to MCP servers for extended tools) and Server (expose conversations to MCP clients) | +| **IDE Integration** | VS Code, Zed, JetBrains via ACP server with session management and tool streaming | +| **API Server** | OpenAI-compatible `/v1/chat/completions` endpoint for headless integrations | +| **Profiles** | Multi-instance support — each profile gets isolated config, memory, sessions, skills, gateway | +| **Security** | Command approval system, secret redaction, SSRF protection, PII redaction, injection detection, credential directory protection | +| **RL Training** | Atropos environments for batch trajectory generation and agent policy optimization | + +--- + +## 3. Installation & Getting Started + +```bash +# One-line install (Linux, macOS, WSL2) +curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash + +# After install +source ~/.bashrc # or: source ~/.zshrc +hermes # start chatting + +# Key commands +hermes model # Choose LLM provider and model +hermes tools # Configure which tools are enabled +hermes config set # Set individual config values +hermes gateway # Start the messaging gateway +hermes setup # Run the full setup wizard +hermes update # Update to latest version +hermes doctor # Diagnose any issues +``` + +**For development:** + +```bash +git clone https://github.com/NousResearch/hermes-agent.git +cd hermes-agent +curl -LsSf https://astral.sh/uv/install.sh | sh +uv venv venv --python 3.11 +source venv/bin/activate +uv pip install -e ".[all,dev]" +python -m pytest tests/ -q # ~3000 tests +``` + +--- + +## 4. Project Structure + +``` +hermes-agent/ +├── run_agent.py # AIAgent class — core conversation loop +├── model_tools.py # Tool orchestration, _discover_tools(), handle_function_call() +├── toolsets.py # Toolset definitions, _HERMES_CORE_TOOLS list +├── toolset_distributions.py # Toolset sampling distributions for RL +├── cli.py # HermesCLI class — interactive CLI orchestrator +├── hermes_state.py # SessionDB — SQLite session store (FTS5 search) +├── hermes_constants.py # Shared constants, get_hermes_home() +├── hermes_time.py # Timezone handling +├── utils.py # Shared utility functions +├── batch_runner.py # Parallel batch processing +├── trajectory_compressor.py # Trajectory compression for RL training +├── mcp_serve.py # MCP server mode entry point +├── mini_swe_runner.py # Minimal SWE benchmark runner +├── rl_cli.py # RL CLI commands +│ +├── agent/ # Agent internals +│ ├── prompt_builder.py # System prompt assembly +│ ├── context_compressor.py # Auto context compression +│ ├── prompt_caching.py # Anthropic prompt caching +│ ├── auxiliary_client.py # Auxiliary LLM client (vision, summarization) +│ ├── model_metadata.py # Model context lengths, token estimation +│ ├── models_dev.py # models.dev registry integration +│ ├── display.py # KawaiiSpinner, tool preview formatting +│ ├── skill_commands.py # Skill slash commands (shared CLI/gateway) +│ └── trajectory.py # Trajectory saving helpers +│ +├── hermes_cli/ # CLI subcommands and setup +│ ├── main.py # Entry point — all `hermes` subcommands +│ ├── config.py # DEFAULT_CONFIG, OPTIONAL_ENV_VARS, migration +│ ├── commands.py # Slash command definitions + SlashCommandCompleter +│ ├── callbacks.py # Terminal callbacks (clarify, sudo, approval) +│ ├── setup.py # Interactive setup wizard +│ ├── skin_engine.py # Skin/theme engine +│ ├── skills_config.py # `hermes skills` — skill management +│ ├── tools_config.py # `hermes tools` — tool management +│ ├── skills_hub.py # Skills Hub integration +│ ├── models.py # Model catalog, provider model lists +│ ├── model_switch.py # Shared /model switch pipeline +│ └── auth.py # Provider credential resolution +│ +├── tools/ # Tool implementations (one file per tool) +│ ├── registry.py # Central tool registry +│ ├── approval.py # Dangerous command detection +│ ├── terminal_tool.py # Terminal/shell execution +│ ├── process_registry.py # Background process management +│ ├── file_tools.py # File read/write/search/patch +│ ├── web_tools.py # Web search/extract +│ ├── browser_tool.py # Browser automation +│ ├── code_execution_tool.py # execute_code sandbox +│ ├── delegate_tool.py # Subagent delegation +│ ├── mcp_tool.py # MCP client integration +│ ├── skills_tool.py # Skill management tool +│ ├── todo_tool.py # Todo/task tracking tool +│ ├── memory_tool.py # Memory read/write tool +│ ├── tts_tool.py # Text-to-speech +│ ├── vision_tool.py # Image analysis +│ ├── image_gen_tool.py # Image generation +│ └── environments/ # Terminal backends +│ ├── base.py # BaseEnvironment ABC +│ ├── local.py # Local execution +│ ├── docker.py # Docker containers +│ ├── ssh.py # SSH remote execution +│ ├── modal.py # Modal serverless +│ ├── managed_modal.py # Nous-hosted Modal +│ ├── daytona.py # Daytona serverless +│ ├── singularity.py # Singularity HPC containers +│ └── persistent_shell.py # Persistent shell mixin +│ +├── gateway/ # Messaging platform gateway +│ ├── run.py # GatewayRunner — main message loop +│ ├── session.py # SessionStore — conversation persistence +│ ├── status.py # Gateway status, token locks +│ └── platforms/ # 16 platform adapters +│ ├── base.py # BasePlatformAdapter ABC +│ ├── telegram.py # Telegram (polling + webhook) +│ ├── discord.py # Discord +│ ├── slack.py # Slack +│ ├── whatsapp.py # WhatsApp +│ ├── matrix.py # Matrix (E2EE) +│ ├── signal.py # Signal +│ ├── email.py # Email (IMAP/SMTP) +│ ├── homeassistant.py # Home Assistant +│ ├── sms.py # SMS (Twilio) +│ ├── mattermost.py # Mattermost +│ ├── dingtalk.py # DingTalk +│ ├── feishu.py # Feishu/Lark +│ ├── wecom.py # WeCom (Enterprise WeChat) +│ ├── webhook.py # Generic webhook +│ └── api_server.py # OpenAI-compatible API server +│ +├── acp_adapter/ # ACP server (IDE integration) +│ ├── server.py # HermesACPAgent class +│ ├── session.py # SessionManager +│ ├── events.py # Streaming callbacks +│ ├── permissions.py # Approval callbacks +│ └── entry.py # Entry point +│ +├── cron/ # Scheduler +│ ├── scheduler.py # tick() — job execution engine +│ └── jobs.py # Job storage and CRUD +│ +├── plugins/ # Plugin system +│ └── memory/ # 8 memory provider plugins +│ ├── openviking/ +│ ├── mem0/ +│ ├── hindsight/ +│ ├── holographic/ +│ ├── honcho/ +│ ├── retaindb/ +│ └── byterover/ +│ +├── environments/ # RL training environments (Atropos) +│ ├── hermes_base_env.py # Abstract base RL environment +│ ├── agent_loop.py # HermesAgentLoop — rollout execution +│ ├── tool_context.py # ToolContext — sandbox for RL +│ ├── web_research_env.py # Web research tasks +│ └── agentic_opd_env.py # Observation-Prediction-Demo env +│ +├── skills/ # 70+ bundled skills across 28 categories +├── optional-skills/ # Additional optional skills +├── tests/ # ~3000 pytest tests +├── scripts/ # Install, update, packaging scripts +├── docker/ # Docker build files +├── docs/ # Documentation source (Docusaurus) +├── website/ # Landing page +├── desktop/ # Desktop app (Electron, separate repo) +├── tinker-atropos/ # RL submodule +│ +├── pyproject.toml # Python package config +├── AGENTS.md # Developer guide for AI assistants +├── RELEASE_v0.2.0.md → v0.7.0.md # Release notes +└── cli-config.yaml.example # Example config +``` + +**User config directory:** `~/.hermes/` + +``` +~/.hermes/ +├── config.yaml # User settings +├── .env # API keys and secrets +├── MEMORY.md # Persistent agent memory +├── USER.md # User profile +├── SOUL.md # Agent personality/identity +├── sessions.db # SQLite session database +├── skills/ # User-installed skills +├── skins/ # Custom CLI themes +├── plugins/ # User plugins +├── cron/ # Cron jobs and output +│ ├── jobs.json +│ └── output/ +├── cache/ # Image/audio cache +├── plans/ # Generated plans +├── profiles/ # Multi-instance profiles +└── mcp/ # MCP server configs +``` + +--- + +## 5. Core Architecture + +### 5.1 AIAgent Class (run_agent.py) + +The `AIAgent` class is the heart of the system — the core conversation loop that orchestrates LLM calls, tool execution, context management, and response delivery. + +**Constructor (~60 parameters):** + +```python +class AIAgent: + def __init__(self, + model: str = "anthropic/claude-opus-4.6", + max_iterations: int = 90, + enabled_toolsets: list = None, + disabled_toolsets: list = None, + quiet_mode: bool = False, + save_trajectories: bool = False, + platform: str = None, # "cli", "telegram", etc. + session_id: str = None, + session_db: SessionDB = None, + skip_context_files: bool = False, + skip_memory: bool = False, + base_url: str = None, + api_key: str = None, + provider: str = None, + api_mode: str = "chat_completions", # or "anthropic_messages" or "codex_responses" + tool_progress_callback = None, + stream_delta_callback = None, + thinking_callback = None, + status_callback = None, + iteration_budget: IterationBudget = None, + credential_pool = None, + checkpoints_enabled: bool = False, + # ... plus provider, routing, callback params + ) +``` + +**Main Methods:** + +| Method | Returns | Purpose | +| ------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------- | +| `chat(message, stream_callback)` | `str` | Simple interface — returns final response text | +| `run_conversation(user_message, system_message, conversation_history, task_id)` | `dict` | Full interface — returns `{final_response, messages, completed, api_calls, error}` | +| `_interruptible_api_call(api_kwargs)` | Response | Runs API request in background thread with interrupt support | +| `_interruptible_streaming_api_call(api_kwargs, on_first_delta)` | Response | Streaming variant with delta callbacks | + +**The Core Agent Loop** (inside `run_conversation()`): + +```python +while api_call_count < self.max_iterations and self.iteration_budget.remaining > 0: + response = client.chat.completions.create( + model=model, messages=messages, tools=tool_schemas + ) + if response.tool_calls: + for tool_call in response.tool_calls: + result = handle_function_call(tool_call.name, tool_call.args, task_id) + messages.append(tool_result_message(result)) + api_call_count += 1 + else: + return response.content # Final text response +``` + +**Key Behaviors:** + +- **Three API modes:** `chat_completions` (OpenAI-compatible), `anthropic_messages` (Anthropic SDK), `codex_responses` (OpenAI Codex) +- **Parallel tool execution:** Independent tool calls run concurrently via ThreadPoolExecutor (unless they share file paths or are in the never-parallel list) +- **Interrupt support:** Background threads allow interrupt detection without blocking on HTTP +- **Error recovery:** Automatic fallback chain (primary → fallback model), retry with exponential backoff, context compression on token overflow +- **Budget pressure:** Warnings at 70% (caution) and 90% (urgent) of iteration budget +- **Oversized results:** Tool results >100K chars are saved to temp files with a preview +- **Stale connection detection:** 90s timeout for streaming, 60s read timeout + +**IterationBudget** (thread-safe): + +```python +class IterationBudget: + def consume() -> bool # Check and consume one iteration + def refund() # Give back iteration (for execute_code turns) + @property remaining # Remaining iterations +``` + +--- + +### 5.2 Tool Orchestration (model_tools.py) + +Bridges the agent and tool registry — handles discovery, schema generation, and dispatch. + +**Key Functions:** + +| Function | Purpose | +| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| `_discover_tools()` | Imports all tool modules (each calls `registry.register()` on import) | +| `get_tool_definitions(enabled_toolsets, disabled_toolsets, quiet_mode)` | Returns OpenAI-format tool schemas filtered by toolset | +| `handle_function_call(function_name, function_args, task_id, user_task, enabled_tools)` | Main dispatcher — routes calls to registry with arg coercion | +| `coerce_tool_args(tool_name, args)` | Type coercion for LLM-generated arguments (string→int, string→bool, etc.) | + +**Tool Discovery Order:** + +1. Static tools (web_tools, terminal_tool, file_tools, browser_tool, etc.) +2. Optional tools (fal_client for image gen, honcho, etc.) — graceful fallback if missing +3. Plugin-registered tools +4. MCP server tools (dynamic, via tools/list_changed notifications) + +**Special Tool Handling:** + +- **Agent-level tools** (todo, memory, session_search, delegate_task): Intercepted by `run_agent.py` before `handle_function_call()` +- **execute_code**: Passes `enabled_tools` for sandbox tool list +- **Dynamic schema adjustments**: `browser_navigate` strips web_search reference if tools unavailable + +**Async Bridging:** + +- Persistent event loops (not `asyncio.run()`) to prevent "Event loop is closed" errors +- Main thread uses shared loop; worker threads get per-thread loops +- `_run_async()` detects running loop and spins up disposable thread if needed + +--- + +### 5.3 Toolset System (toolsets.py) + +Provides flexible tool grouping and composition. + +**Core Toolsets:** + +| Toolset | Tools Included | +| ---------------- | ------------------------------------------------------------------------------------------------ | +| `web` | web_search, web_extract, web_crawl | +| `terminal` | terminal | +| `file` | read_file, write_file, edit_file, list_files, search_files | +| `browser` | browser_navigate, browser_snapshot, browser_click, browser_type, browser_scroll, browser_extract | +| `vision` | analyze_image | +| `image_gen` | generate_image | +| `tts` | text_to_speech | +| `todo` | todo_read, todo_write | +| `memory` | memory_read, memory_write | +| `session_search` | session_search | +| `delegation` | delegate_task | +| `code_execution` | execute_code | +| `cronjob` | create_job, list_jobs, delete_job | +| `messaging` | send_message | +| `homeassistant` | ha_get_states, ha_call_service, ... | + +**Composite Toolsets:** + +- `hermes-cli` — All core tools for CLI platform +- `hermes-telegram`, `hermes-discord`, etc. — Platform-specific tool sets +- `hermes-gateway` — Union of all platform tools +- `debugging` — terminal + file + web +- `safe` — Everything except terminal + +**Resolution:** + +```python +resolve_toolset(name, visited=None) → List[str] +# Recursively resolves toolset to tool names +# Handles composition (includes) and cycle detection +# Special aliases: "all" or "*" = all tools +``` + +--- + +### 5.4 Tool Registry (tools/registry.py) + +Singleton managing all tool schemas and handlers. Circular-import safe — has no tool dependencies. + +**ToolEntry** (per-tool metadata): + +```python +@dataclass(slots=True) +class ToolEntry: + name: str + toolset: str + schema: dict # OpenAI-format tool definition + handler: Callable # Sync or async handler function + check_fn: Callable # Returns True if tool is available + requires_env: list # Required environment variables + is_async: bool + description: str + emoji: str +``` + +**Key Methods:** + +```python +registry.register(name, toolset, schema, handler, check_fn, requires_env) +registry.get_definitions(tool_names, quiet) # Returns filtered schemas +registry.dispatch(name, args, **kwargs) # Execute with async bridging +registry.deregister(name) # Remove (for MCP tool refresh) +registry.check_tool_availability() # Returns (available, unavailable) +``` + +--- + +### 5.5 Session Database (hermes_state.py) + +SQLite-based persistent session storage with FTS5 full-text search. + +**Schema (v6):** + +```sql +-- Sessions table +sessions ( + id TEXT PRIMARY KEY, + source TEXT, user_id TEXT, model TEXT, model_config TEXT, + system_prompt TEXT, parent_session_id TEXT, + started_at TEXT, ended_at TEXT, end_reason TEXT, + message_count INTEGER, tool_call_count INTEGER, + input_tokens INTEGER, output_tokens INTEGER, + cache_read_tokens INTEGER, cache_write_tokens INTEGER, reasoning_tokens INTEGER, + estimated_cost_usd REAL, actual_cost_usd REAL, + title TEXT -- UNIQUE INDEX +) + +-- Messages table +messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT, role TEXT, content TEXT, + tool_call_id TEXT, tool_calls TEXT, -- JSON + tool_name TEXT, timestamp TEXT, + token_count INTEGER, finish_reason TEXT, + reasoning TEXT, reasoning_details TEXT, codex_reasoning_items TEXT +) + +-- FTS5 virtual table (auto-synced via triggers) +messages_fts (content) +``` + +**Concurrency Model:** + +- WAL (Write-Ahead Logging) for concurrent readers + single writer +- `BEGIN IMMEDIATE` for write transactions (lock at start, not commit) +- Jitter retry on lock: 20-150ms random backoff, max 15 retries +- Periodic WAL checkpoint every 50 writes + +**Key Operations:** + +- `create_session()`, `end_session()`, `reopen_session()` +- `add_message()`, `get_messages()` +- `search_sessions(query)` — FTS5 full-text search +- `update_token_counts()` — Supports both incremental (CLI) and absolute (gateway) modes + +--- + +### 5.6 Constants & Home Directory (hermes_constants.py) + +Import-safe constants module with no circular dependencies. + +```python +get_hermes_home() → Path # HERMES_HOME env var or ~/.hermes +display_hermes_home() → str # User-friendly display: "~/.hermes" +get_optional_skills_dir() → Path # HERMES_OPTIONAL_SKILLS env var +parse_reasoning_effort(str) → Dict # "high" → {"enabled": True, "effort": "high"} + +# Key constants +OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1" +NOUS_API_BASE_URL = "https://inference-api.nousresearch.com/v1" +AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh/v1" +VALID_REASONING_EFFORTS = ("xhigh", "high", "medium", "low", "minimal") +``` + +--- + +## 6. CLI System + +### 6.1 Interactive CLI (cli.py) + +The `HermesCLI` class provides the interactive terminal interface. + +**Features:** + +- **Rich** for banner/panels, **prompt_toolkit** for input with autocomplete +- **KawaiiSpinner** — animated faces during API calls, `┊` activity feed for tool results +- Multiline editing with Shift+Enter +- Slash-command autocomplete +- Session history with up/down arrow navigation +- Clipboard image paste (Alt+V / Ctrl+V) +- Status bar showing model, provider, and token counts +- Inline diff previews for file write/patch operations + +**Configuration Loading:** + +```python +load_cli_config() → dict +# Loads from ~/.hermes/config.yaml (or ./cli-config.yaml fallback) +# Merges with hardcoded defaults +# Expands ${ENV_VAR} references +# Maps terminal config → env vars +``` + +--- + +### 6.2 CLI Entry Point (hermes_cli/main.py) + +All `hermes` subcommands are dispatched from here: + +``` +hermes # Default: interactive chat +hermes chat # Explicit interactive mode +hermes gateway start|stop|status|install|uninstall +hermes setup # Setup wizard +hermes model # Select model/provider +hermes tools # Configure tools +hermes skills # Manage skills +hermes config set|get # Direct config manipulation +hermes cron list|delete # Cron job management +hermes doctor # Diagnose issues +hermes sessions browse # Session picker +hermes profile create|list|switch|delete|export|import +hermes mcp serve|add|remove # MCP management +hermes acp # Start ACP server +hermes update|uninstall|version +``` + +**Profile System:** + +- `_apply_profile_override()` runs BEFORE any imports to set `HERMES_HOME` +- Pre-parses `--profile/-p` from argv +- Allows fully isolated agent instances with separate config, memory, sessions, skills + +--- + +### 6.3 Configuration System (hermes_cli/config.py) + +**Key Configuration Sections:** + +```yaml +model: "anthropic/claude-opus-4.6" # or dict with provider/base_url/api_key +providers: {} # Provider-specific configs +fallback_providers: [] # Ordered failover list +credential_pool: {} # Multiple API keys per provider + +agent: + max_turns: 90 + gateway_timeout: 1800 + tool_use_enforcement: "auto" + +terminal: + backend: "local" # local|docker|modal|daytona|ssh|singularity + timeout: 180 + persistent_shell: true + docker_image: "nikolaik/python-nodejs:..." + +compression: + enabled: true + threshold: 0.50 # Compress when 50% of context used + target_ratio: 0.20 # Summary = 20% of compressed content + protect_last_n: 20 + +auxiliary: + vision: { provider, model } + web_extract: { provider, model } + compression: { provider, model } + +memory: + memory_enabled: true + provider: "" # "" | "honcho" | "mem0" | etc. + memory_char_limit: 2200 + +display: + personality: "kawaii" + show_reasoning: false + inline_diffs: true + skin: "default" + streaming: true + +tts: + provider: "edge" # edge|elevenlabs|openai|neutts + +stt: + enabled: true + provider: "local" # local|groq|openai + +privacy: + redact_pii: false + +mcp_servers: {} # MCP server configurations + +skills: + external_dirs: [] # Additional skill directories + +approvals: + mode: "smart" # smart|always|off +``` + +**Config Files:** + +- `~/.hermes/config.yaml` — User settings (authoritative) +- `~/.hermes/.env` — API keys and secrets +- Config version migration system (currently v5) + +--- + +### 6.4 Slash Command Registry (hermes_cli/commands.py) + +All slash commands defined centrally in `COMMAND_REGISTRY`: + +```python +CommandDef(name, description, category, aliases, args_hint, cli_only, gateway_only) +``` + +**Derived automatically by:** + +- CLI `process_command()` — dispatch on canonical name +- Gateway dispatch + help +- Telegram BotCommand menu +- Slack `/hermes` subcommands +- Autocomplete + help text + +**Key Commands:** + +| Command | Aliases | Description | +| -------------- | ---------- | ----------------------------- | +| `/new` | `/reset` | Start fresh conversation | +| `/model` | | Show/switch model | +| `/personality` | | Set agent personality | +| `/retry` | | Retry last turn | +| `/undo` | | Remove last turn | +| `/compress` | `/compact` | Compress context | +| `/usage` | `/cost` | Show token usage | +| `/insights` | | Usage analytics | +| `/skills` | | Browse/install skills | +| `/background` | `/bg` | Manage background processes | +| `/plan` | | Generate implementation plan | +| `/rollback` | | Restore filesystem checkpoint | +| `/verbose` | | Toggle debug output | +| `/reasoning` | | Set reasoning effort | +| `/yolo` | | Toggle approval bypass | +| `/btw` | | Ephemeral side question | +| `/stop` | | Kill current agent run | +| `/queue` | | Queue next prompt | +| `/browser` | | Interactive browser session | +| `/history` | `/resume` | Session browser | +| `/skin` | | Switch CLI theme | + +--- + +### 6.5 Setup Wizard (hermes_cli/setup.py) + +Modular interactive wizard with independent sections: + +1. **Model & Provider** — Select AI provider, enter API keys, choose model +2. **Terminal Backend** — Choose execution environment +3. **Agent Settings** — Max iterations, compression, session policies +4. **Messaging Platforms** — Configure Telegram, Discord, Slack, etc. +5. **Tools** — TTS, STT, web search, image generation, browser + +Features: + +- Live credential validation +- Real-time model list fetching from provider APIs +- Automatic OpenClaw migration detection +- Atomic config file writes + +--- + +### 6.6 Model Catalog (hermes_cli/models.py) + +Provider-specific model lists: + +```python +_PROVIDER_MODELS = { + "nous": ["anthropic/claude-opus-4.6", "anthropic/claude-sonnet-4.6", ...], # 25+ + "openrouter": ["anthropic/claude-opus-4.6", "google/gemini-3-flash", ...], # 30+ + "anthropic": ["claude-opus-4-6", "claude-sonnet-4-6", ...], + "openai": ["gpt-5", "gpt-5.4-mini", "gpt-4.1", "gpt-4o", ...], + "copilot": ["gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex", ...], + "huggingface": [...], + "minimax": [...], + "kimi-coding": [...], + "alibaba": [...], + "deepseek": [...], + # ... more providers +} +``` + +Features: + +- Dynamic fetching via provider `/models` endpoints +- Curated lists used when live probe returns fewer models +- Fuzzy matching for typo correction +- Validation against provider catalog + +--- + +### 6.7 Skin/Theme Engine (hermes_cli/skin_engine.py) + +Data-driven CLI visual customization — no code changes needed. + +**Customizable Elements:** + +| Element | Key | Used By | +| -------------------------------- | ------------------------ | ----------------- | +| Banner border/title/accent | `colors.*` | banner.py | +| Response box border | `colors.response_border` | cli.py | +| Spinner faces (waiting/thinking) | `spinner.*` | display.py | +| Spinner verbs/wings | `spinner.*` | display.py | +| Tool output prefix | `tool_prefix` | display.py | +| Per-tool emojis | `tool_emojis` | display.py | +| Agent name/welcome/prompt | `branding.*` | banner.py, cli.py | + +**Built-in Skins:** default, ares, mono, slate, poseidon, sisyphus, charizard + +**User Skins:** Drop `~/.hermes/skins/.yaml` and activate with `/skin ` + +--- + +## 7. Tool System + +### 7.1 Terminal Tool (tools/terminal_tool.py) + +Shell command execution across multiple backends. + +```python +def terminal_tool( + command: str, + background: bool = False, + timeout: Optional[int] = None, + task_id: Optional[str] = None, + force: bool = False, # Skip approval for dangerous commands + workdir: Optional[str] = None, + check_interval: Optional[int] = None, # Background task polling + pty: bool = False, +) -> str # JSON result +``` + +**Features:** + +- Multi-backend: Selects based on `TERMINAL_ENV` (local/docker/ssh/modal/daytona/singularity) +- Per-task_id sandboxes with thread-safe creation locks +- Dangerous command routing through approval system +- Background task support with file-based IPC +- Interrupt handling — polls `is_interrupted()` during execution +- Auto-cleanup daemon thread for idle environments (>300s) +- Disk usage warnings at configurable threshold + +--- + +### 7.2 File Tools (tools/file_tools.py) + +Safe file operations with size guards and sensitive path protection. + +- `read_file_tool(path, offset, limit)` — Read with pagination (default 100K char limit) +- `write_file_tool(path, content)` — Write with approval for sensitive paths +- `edit_file_tool(path, old_text, new_text)` — String replacement editing +- `list_files_tool(path)` — Directory listing +- `search_files_tool(pattern, path)` — Glob/regex file search + +**Safety:** + +- Device path blocklist (`/dev/zero`, `/dev/stdin`, etc.) +- Read dedup tracking — returns stub on re-read if mtime unchanged +- Sensitive path blocking: `/etc/`, `/boot/`, `~/.ssh` without approval +- Prompt injection protection for known dangerous paths + +--- + +### 7.3 Web Tools (tools/web_tools.py) + +Web search and content extraction. + +- `web_search_tool(query, limit)` — Search via configurable backend +- `web_extract_tool(urls, format)` — Extract content from URLs +- `web_crawl_tool(url, instructions)` — LLM-guided web crawling + +**Backends:** Firecrawl, Parallel Web, Tavily, Exa, DuckDuckGo + +- Fallback detection by highest-priority available API key +- LLM processing via auxiliary client (Gemini 3 Flash) for intelligent extraction +- SSRF protection and URL safety checks + +--- + +### 7.4 Browser Tool (tools/browser_tool.py) + +Headless browser automation with accessibility tree. + +```python +browser_navigate(url, task_id) → str +browser_snapshot(task_id, max_chars) → str +browser_click(ref, task_id) → str # Element refs like @e1, @e2 +browser_type(ref, text, task_id) → str +browser_scroll(ref, direction, task_id) → str +browser_extract(task, max_chars, task_id) → str +``` + +**Backends:** + +- **Local:** Headless Chromium via `agent-browser` CLI +- **Cloud:** Browserbase (stealth, proxies, CAPTCHA solving) +- **Camofox:** Anti-detection browser using Camoufox + +**Features:** + +- Accessibility tree for text-based snapshots (no vision required) +- Session isolation per task_id with inactivity cleanup +- API key detection in URLs (prevents exfiltration) +- SSRF protection for private/internal addresses + +--- + +### 7.5 Delegate Tool (tools/delegate_tool.py) + +Spawn isolated subagents for parallel workstreams. + +```python +def delegate_task( + goal: str, + context: str = None, + toolsets: List[str] = None, # Default: ["terminal", "file", "web"] + tasks: List[Dict] = None, # Batch mode: up to 3 concurrent + max_iterations: int = 50, + parent_agent = None, +) → str +``` + +**Isolation:** + +- Fresh conversation (no parent history) +- Own task_id (separate terminal session, file ops) +- Restricted toolset (configurable, blocked tools always stripped) +- Focused system prompt from goal + context +- Parent only sees delegation call and summary result + +**Blocked Tools:** delegate_task, clarify, memory, send_message, execute_code (no recursion, no user interaction) + +**Constraints:** MAX_DEPTH=2, MAX_CONCURRENT_CHILDREN=3 + +--- + +### 7.6 MCP Tool (tools/mcp_tool.py) + +Model Context Protocol client integration. + +**Configuration (config.yaml):** + +```yaml +mcp_servers: + filesystem: + command: "npx" + args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] + timeout: 120 + remote_api: + url: "https://my-mcp-server.example.com/mcp" + headers: + Authorization: "Bearer sk-..." + sampling: + enabled: true + model: "gemini-3-flash" +``` + +**Features:** + +- **Transports:** Stdio (local processes) and HTTP/StreamableHTTP (remote) +- **Dynamic tool discovery:** Listens for `tools/list_changed` notifications +- **Sampling support:** MCP servers can request LLM completions +- **Automatic reconnection** with exponential backoff (5 retries) +- Dedicated background event loop in daemon thread +- Thread-safe via lock protecting server dict + +--- + +### 7.7 Approval System (tools/approval.py) + +Dangerous command detection and approval flow. + +```python +detect_dangerous_command(command: str) → (is_dangerous, pattern_key, description) +``` + +**106 patterns covering:** + +- Destructive operations (`rm -r /`, `mkfs`, `dd if=`) +- Privilege escalation (`chmod 777`, `chown -R root`) +- SQL injection (`DROP TABLE`, `DELETE FROM` without WHERE) +- System targeting (`/etc/` writes, `systemctl stop`, fork bombs) +- Shell injection (pipe to sh, wget to sh, process substitution) +- Network exfiltration (curl with embedded API keys) +- Secret access (`cat ~/.env`, `cat ~/.netrc`) + +**Normalization:** Strips ANSI escapes, null bytes, normalizes Unicode + +**Approval Modes:** + +- `smart` — Learns which commands are safe based on user decisions +- `always` — Always ask for dangerous commands +- `off` — Never ask (or `/yolo` toggle) + +--- + +### 7.8 Terminal Backends (tools/environments/) + +| Backend | File | Features | +| ----------------- | ------------------ | ------------------------------------------------------------------------------ | +| **Local** | `local.py` | Direct execution, interrupt support, non-blocking I/O, persistent shell | +| **Docker** | `docker.py` | Sandboxed containers, cap-drop ALL, no-new-privileges, PID limits, bind mounts | +| **SSH** | `ssh.py` | Remote execution via ControlMaster, persistent shell mixin | +| **Modal** | `modal.py` | Native Modal SDK `Sandbox.create()`/`Sandbox.exec()`, persistent snapshots | +| **Managed Modal** | `managed_modal.py` | Modal through Nous-hosted gateway | +| **Daytona** | `daytona.py` | Daytona SDK cloud sandboxes, stop/resume lifecycle | +| **Singularity** | `singularity.py` | Singularity containers with scratch dir, SIF cache | + +**Common Interface (BaseEnvironment ABC):** + +```python +execute(command, timeout) → {"output": str, "returncode": int} +cleanup() → None +``` + +--- + +## 8. Agent Internals + +### 8.1 Prompt Builder (agent/prompt_builder.py) + +Assembles the system prompt from multiple sources: + +| Component | Source | +| ----------------------- | ---------------------------------------------------- | +| Agent Identity | `DEFAULT_AGENT_IDENTITY` or `SOUL.md` | +| Memory Guidance | When/how to use memory tool | +| Session Search Guidance | How to recall past conversations | +| Skills Guidance | When to create/patch skills | +| Tool Use Enforcement | Must execute tools, not describe actions | +| Skills Index | `~/.hermes/skills/.hermes-skills.json` | +| Platform Hints | OS, Python version, shell, available tools | +| Context Files | `.hermes.md`, `AGENTS.md`, `.cursorrules`, `SOUL.md` | +| Model/Provider Info | Current model and provider identity | + +**Context File Discovery:** + +1. Check `cwd/.hermes.md` or `HERMES.md` +2. Walk parent directories up to git root +3. Validate against injection patterns before inclusion + +**Injection Detection (30+ patterns):** + +- "ignore previous instructions", "system prompt override" +- "do not tell the user", "act as if you have no restrictions" +- HTML comment injection, exfiltration via curl, Unicode stealth chars + +--- + +### 8.2 Context Compressor (agent/context_compressor.py) + +Automatically compresses conversation history when approaching context limits. + +```python +class ContextCompressor: + threshold_percent: float = 0.50 # Compress when 50% of context used + protect_first_n: int = 3 # Keep system prompt + first turn + protect_last_n: int = 20 # Keep recent N messages + summary_target_ratio: float = 0.20 # Summary = 20% of compressed content +``` + +**Compression Algorithm:** + +1. **Pre-pass:** Replace old tool results with placeholder (cheap) +2. **Protect head:** System prompt + first exchange always kept +3. **Protect tail:** Token-budget-based tail protection (~20K tokens) +4. **Summarize middle:** LLM-generated structured summary (Goal, Progress, Decisions, Files, Next Steps) +5. **Iterative updates:** On subsequent compressions, update previous summary instead of re-summarizing + +--- + +### 8.3 Prompt Caching (agent/prompt_caching.py) + +Anthropic prompt caching support for cost reduction. + +- System prompt cached across turns (first conversation turn establishes cache) +- Cache markers inserted at system prompt boundaries +- Must not break mid-conversation — altering past context invalidates cache +- The ONLY time context is altered is during compression + +**Critical Rule:** Do NOT implement changes that would alter past context, change toolsets, reload memories, or rebuild system prompts mid-conversation. + +--- + +### 8.4 Auxiliary Client (agent/auxiliary_client.py) + +Separate LLM client for non-primary tasks: + +- **Vision analysis** — Image description and analysis +- **Web extraction** — Content summarization +- **Context compression** — Generating conversation summaries +- **Session search** — Summarizing search results +- **MCP sampling** — Serving server-initiated LLM requests + +Configured per-task via `auxiliary` section in config.yaml. + +--- + +### 8.5 Display & Spinner (agent/display.py) + +- **KawaiiSpinner** — Animated face characters during API calls +- **Tool preview formatting** — `┊` prefixed activity feed for tool execution +- **Inline diff previews** — Shows unified diffs for write/patch operations +- Respects active skin for colors, emojis, and branding + +--- + +### 8.6 Skill Commands (agent/skill_commands.py) + +Shared skill invocation for CLI and gateway: + +- Skills loaded from `~/.hermes/skills/` and external directories +- Injected as **user message** (not system prompt) to preserve prompt caching +- `/plan` command generates implementation plans stored in `.hermes/plans/` +- Skill content includes setup instructions, tool options, usage examples + +--- + +## 9. Messaging Gateway + +### 9.1 GatewayRunner (gateway/run.py) + +Main controller managing all platform adapters and routing messages. + +**Key Attributes:** + +- `adapters: Dict[Platform, BasePlatformAdapter]` — Active platform instances +- `session_store: SessionStore` — Conversation persistence +- `_running_agents: Dict[str, AIAgent]` — Per-session cached agents (preserves prompt caching) +- `_pending_approvals: Dict[str, Dict]` — Dangerous command approval tracking +- `pairing_store: PairingStore` — DM code-based user authorization + +**Message Flow:** + +1. Platform adapter queues `MessageEvent` +2. GatewayRunner dequeues, calls `_handle_message()` +3. Slash command? → dispatch to handler +4. Regular message? → `_handle_message_with_agent()` (async, with per-session locking) +5. Response delivered back via platform adapter + +**Features:** + +- Agent caching per session (preserves Anthropic prompt cache across turns) +- Session reset policies (inactivity timeout, hard reset) +- Per-session model overrides via `/model` +- Background memory flush on session expiry +- Approval routing (`/approve`, `/deny`) with interactive buttons (Discord) +- 30+ slash command handlers + +--- + +### 9.2 Session Store (gateway/session.py) + +**SessionSource** — Where a message originated (platform, chat_id, user info) +**SessionContext** — Full context for system prompt injection (platforms, home channels, metadata) +**SessionStore** — Loads/saves conversation transcripts as JSON files + +``` +~/.hermes/sessions/{session_key}.json +Format: [{role, content, timestamp}, ...] +``` + +**Features:** + +- Session key computed from platform + chat_id + thread_id (deterministic hash) +- Inactivity timeout for session resets +- PII redaction (phone numbers hashed) +- Survives gateway restarts + +--- + +### 9.3 Platform Adapters (gateway/platforms/) + +**16 adapters sharing `BasePlatformAdapter` interface:** + +| Platform | Key Features | +| ------------------ | -------------------------------------------------------------------------------------------------------- | +| **Telegram** | Polling + webhook mode, media handling, inline keyboards, forum topic isolation, group mention gating | +| **Discord** | Server channels, threads, reactions (processing/done/error), button-based approval, @mention requirement | +| **Slack** | Multi-workspace OAuth, thread handling, app_mention, `/hermes` subcommands | +| **WhatsApp** | Group & DM support, media captions, LID↔phone alias resolution | +| **Matrix** | E2EE room encryption, threaded messages, trusted device flow, native voice messages | +| **Signal** | Encrypted DMs, group membership, SSE keepalive, phone URL encoding | +| **Email** | IMAP/SMTP, multi-recipient, skip_attachments option | +| **Home Assistant** | REST tools + WebSocket, service discovery, smart home automation | +| **SMS** | Twilio integration | +| **Mattermost** | Self-hosted Slack alternative, configurable mention behavior | +| **DingTalk** | Alibaba enterprise messaging | +| **Feishu/Lark** | Enterprise messaging, message cards, approval workflows | +| **WeCom** | Enterprise WeChat, department management | +| **Webhook** | Generic HTTP POST for custom integrations | +| **API Server** | OpenAI-compatible `/v1/chat/completions` endpoint | + +**Common Features:** + +- Image/audio caching for vision and STT tools +- Rate limiting with exponential backoff +- Session routing with authorization rules +- Cross-platform conversation continuity + +--- + +## 10. Cron Scheduling + +Built-in job scheduler running in the gateway background thread. + +**Schedule Types:** + +- `"once in 5m"` — One-shot after duration +- `"every 30m"` — Recurring interval +- `"0 9 * * *"` — Standard cron expression +- `"2026-04-06T14:00"` — Absolute datetime + +**Job Storage:** `~/.hermes/cron/jobs.json` + +**Execution Flow:** + +1. `tick()` called every 60s from gateway background thread +2. Fetch due jobs past `next_run_at` +3. Spawn `hermes` CLI subprocess with job prompt + skills +4. Capture output → save to `~/.hermes/cron/output/{job_id}/{timestamp}.md` +5. Deliver to target platform (or stay local) + +**Delivery Targets:** + +- `"local"` — Output saved locally only +- `"origin"` — Send to originating chat +- `"telegram:"` — Explicit platform/chat routing +- `[SILENT]` prefix — Suppress delivery but keep logs + +**Grace Windows:** Based on schedule frequency (120s–2hrs) to handle missed jobs + +--- + +## 11. Skills System + +**Skills are composable, agent-invokable knowledge units.** + +**Structure:** + +``` +skills/ +├── creative/ # ASCII art, diagrams, music +├── software-development/ # Debugging, testing, docs +├── github/ # Codebase inspection, PR workflow +├── research/ # Literature, web scraping +├── productivity/ # Task management +├── media/ # Image, video, audio processing +├── mlops/ # ML experiment tracking +├── autonomous-ai-agents/ # Multi-agent orchestration +└── [20+ more categories] +``` + +**Per-Skill Structure:** + +``` +skill-name/ +├── SKILL.md # Metadata (YAML frontmatter) + implementation instructions +└── [sub-skills]/ +``` + +**SKILL.md Example:** + +```yaml +--- +name: ascii-art +description: Generate ASCII art using multiple tools +version: 4.0.0 +dependencies: [] +metadata: + hermes: + tags: [ASCII, Art, Banners, Creative] + related_skills: [excalidraw] +--- +[Implementation instructions, tool options, examples...] +``` + +**Discovery:** + +- Auto-discovered from `~/.hermes/skills/` + external dirs +- Skills index built at startup (`.hermes-skills.json`) +- Loaded as user messages to preserve prompt caching +- Per-platform enable/disable via `hermes skills` +- Skills Hub (`agentskills.io`) for community sharing + +--- + +## 12. Plugin System + +Drop Python files into `~/.hermes/plugins/` to extend Hermes. + +**Plugin Capabilities:** + +- Register custom tools and toolsets +- Inject messages into conversation +- Lifecycle hooks: `pre_llm_call`, `post_llm_call`, `on_session_start`, `on_session_end` +- Enable/disable via `hermes plugins enable/disable ` + +**Memory Provider Plugins (plugins/memory/):** +8 implementations: openviking, mem0, hindsight, holographic, honcho, retaindb, byterover + +Each implements: + +```python +class MemoryProvider: + def is_available() → bool + def store(key, value) + def retrieve(query) → list + def clear() +``` + +--- + +## 13. Memory System + +Hermes has a pluggable memory provider interface: + +**Built-in Memory:** + +- `MEMORY.md` — Markdown file with persistent facts +- `USER.md` — User profile information +- Memory read/write tools called by agent during conversation +- Periodic nudges prompt agent to save important information +- FTS5 session search for cross-session recall + +**Honcho Integration:** + +- AI-native dialectic user modeling +- Async memory writes +- Profile-scoped host/peer resolution +- Multi-user isolation in gateway mode + +**Configuration:** + +```yaml +memory: + memory_enabled: true + provider: "" # "" for built-in, "honcho", "mem0", etc. + memory_char_limit: 2200 +``` + +--- + +## 14. ACP Server (IDE Integration) + +Agent Communication Protocol server for VS Code, Zed, JetBrains. + +**Entry:** `hermes acp` → `acp_adapter/server.py` + +**HermesACPAgent Class:** + +```python +initialize() # Handshake with IDE client +authenticate(method_id) # Validate credentials +new_session(cwd, mcp_servers) # Create isolated session +load_session(session_id) # Resume session +fork_session(session_id) # Branch for parallel work +list_sessions() # Browse all sessions +cancel(session_id) # Interrupt running agent +``` + +**Features:** + +- Slash command support (`/model`, `/tools`, `/reset`, `/compact`) +- Client-provided MCP servers (IDE's MCP ecosystem flows into agent) +- Streaming callbacks for messages, thinking, tool progress +- Dangerous command approval via IDE UI + +--- + +## 15. API Server + +OpenAI-compatible API endpoint for headless integrations (e.g., Open WebUI). + +**Endpoint:** `POST /v1/chat/completions` + +**Features:** + +- `X-Hermes-Session-Id` header for persistent sessions +- Tool progress streaming via SSE events +- `/api/jobs` REST API for cron management +- Input limits, field whitelists, SQLite-backed response persistence +- CORS origin protection + +--- + +## 16. MCP Server Mode + +Expose Hermes conversations to MCP-compatible clients. + +**Entry:** `hermes mcp serve` + +**Features:** + +- Browse conversations and sessions +- Read messages and search across sessions +- Manage attachments +- Supports both stdio and Streamable HTTP transports +- Compatible with Claude Desktop, Cursor, VS Code, etc. + +--- + +## 17. RL Training Environments + +Atropos-based RL training framework for agent policy optimization. + +**Base Class:** `HermesAgentBaseEnv` (extends `atroposlib.BaseEnv`) + +**Configuration:** + +```python +HermesAgentEnvConfig: + enabled_toolsets: ["terminal", "file", "web"] + max_agent_turns: 30 + agent_temperature: 1.0 + terminal_backend: "local" # or docker/modal for isolation + dataset_name: str +``` + +**Subclass Requirements:** + +- `setup()` — Load dataset +- `get_next_item()` — Return next task +- `format_prompt()` — Convert item → user message +- `compute_reward()` — Score rollout +- `evaluate()` — Periodic eval on test set + +**Example Environments:** + +- `web_research_env.py` — Web research tasks +- `agentic_opd_env.py` — Observation-Prediction-Demonstration +- `hermes_swe_env.py` — Software engineering tasks + +**Supporting:** + +- `HermesAgentLoop` — Orchestrates step-by-step rollouts +- `ToolContext` — Sandbox for tool execution, records side effects +- `trajectory_compressor.py` — Compresses trajectories for training data + +--- + +## 18. Profiles (Multi-Instance) + +Run multiple fully isolated Hermes instances from the same installation. + +**Commands:** + +```bash +hermes profile create +hermes profile list +hermes profile switch +hermes profile delete +hermes profile export +hermes profile import +hermes -p # Launch with specific profile +``` + +**Each profile gets:** + +- Own `HERMES_HOME` directory (`~/.hermes/profiles//`) +- Own config.yaml, .env, memory, sessions, skills, gateway service +- Token-lock isolation (prevents two profiles sharing bot credentials) + +**Implementation:** + +- `_apply_profile_override()` sets `HERMES_HOME` env var before any imports +- All 119+ references to `get_hermes_home()` automatically scope to active profile +- Profile operations are HOME-anchored (`~/.hermes/profiles/`) for cross-profile visibility + +--- + +## 19. Security Model + +### Command Approval + +- 106 dangerous command patterns (destructive, privilege escalation, SQL, exfiltration) +- Smart approval mode learns from user decisions +- Session-based approval state (per-session tracking) +- `--fuck-it-ship-it` flag or `/yolo` toggle to bypass + +### Secret Protection + +- Secret redaction in logs and tool output (API keys, tokens) +- Browser URL scanning for embedded secrets +- LLM response scanning for exfiltration attempts +- Credential directory protection (`.docker`, `.azure`, `.config/gh`, `.ssh`) +- `execute_code` sandbox output redaction + +### Input Safety + +- Prompt injection detection in context files (30+ patterns) +- Unicode stealth character detection +- ANSI escape sequence normalization +- Device path blocklist for file reads +- SSRF protection in web/browser tools + +### PII Handling + +- Optional `privacy.redact_pii` mode +- Phone number hashing in session metadata +- Sender ID anonymization in gateway logs + +### Supply Chain + +- All dependency version ranges pinned +- `uv.lock` with hashes for reproducible builds +- CI workflow scanning PRs for supply chain attack patterns +- Compromised `litellm` dependency removed + +--- + +## 20. Provider & Model System + +### Supported Providers + +| Provider | API Mode | Key Features | +| --------------------- | ------------------ | ----------------------------------------- | +| **Nous Portal** | chat_completions | 400+ models, first-class setup | +| **OpenRouter** | chat_completions | 200+ models, provider routing preferences | +| **Anthropic** | anthropic_messages | Native prompt caching, OAuth PKCE | +| **OpenAI** | chat_completions | GPT-5, Codex | +| **GitHub Copilot** | chat_completions | OAuth, 400k context | +| **Hugging Face** | chat_completions | Curated agentic model picker | +| **Google (Direct)** | chat_completions | Full Gemini context lengths | +| **z.ai/GLM** | chat_completions | Chinese LLM models | +| **Kimi/Moonshot** | chat_completions | Kimi Code API | +| **MiniMax** | anthropic_messages | M2.7 models | +| **Alibaba/DashScope** | chat_completions | Qwen models | +| **DeepSeek** | chat_completions | V3 models | +| **Vercel AI Gateway** | chat_completions | Routing through Vercel | +| **Kilo Code** | chat_completions | Custom provider | +| **OpenCode Zen/Go** | chat_completions | Custom provider | +| **Custom Endpoint** | configurable | Any OpenAI-compatible API | + +### Provider Features + +- **Ordered fallback chain:** Auto-failover across configured providers on errors +- **Credential pools:** Multiple API keys per provider with `least_used` rotation +- **Per-turn primary restoration:** After fallback use, restore primary on next turn +- **Context length detection:** models.dev integration, provider-aware resolution, `/v1/props` for llama.cpp +- **Rate limit handling:** User-friendly 429 messages with Retry-After countdown +- **Anthropic long-context tier:** Auto-reduces to 200k on tier limit 429 + +--- + +## 21. Streaming & Reasoning + +### Streaming + +- Enabled by default in CLI and gateway +- Token-by-token delivery via `stream_delta_callback` +- Proper spinner/tool progress display during streaming +- Stale connection detection (90s timeout) +- Fallback to non-streaming if provider doesn't support it + +### Reasoning/Thinking + +- Configurable effort: xhigh, high, medium, low, minimal, none +- `/reasoning` command to toggle display and effort level +- Anthropic thinking blocks preserved across multi-turn conversations +- `` tag extraction for compatible models +- Reasoning persisted to SessionDB (v6 schema) for cross-session continuity +- Thinking-budget exhaustion detection to skip useless continuation retries + +--- + +## 22. Release History + +### v0.2.0 (March 12, 2026) — The Foundation Release + +> 216 merged PRs from 63 contributors, resolving 119 issues + +- Multi-platform messaging gateway (Telegram, Discord, Slack, WhatsApp, Signal, Email, Home Assistant) +- MCP client support (stdio + HTTP) +- 70+ skills across 15+ categories +- Centralized provider router (`call_llm()` API) +- ACP server for IDE integration +- CLI skin/theme engine +- Git worktree isolation (`hermes -w`) +- Filesystem checkpoints and `/rollback` +- 3,289 tests + +### v0.3.0 (March 17, 2026) — Streaming, Plugins, Providers + +- Unified streaming infrastructure (token-by-token delivery) +- First-class plugin architecture (`~/.hermes/plugins/`) +- Native Anthropic provider with prompt caching +- Smart approvals + `/stop` command +- Honcho memory integration +- Voice mode (CLI push-to-talk, Telegram/Discord voice) +- Concurrent tool execution (ThreadPoolExecutor) +- PII redaction +- `/browser connect` via Chrome DevTools Protocol +- Vercel AI Gateway provider +- Persistent shell mode for local/SSH backends +- Agentic On-Policy Distillation RL environment + +### v0.4.0 (March 23, 2026) — Platform Expansion + +- OpenAI-compatible API server (`/v1/chat/completions`) +- 6 new messaging adapters (Signal rewrite, DingTalk, SMS, Mattermost, Matrix, Webhook) +- `@file` and `@url` context references with tab completion +- 4 new providers (GitHub Copilot, Alibaba, Kilo Code, OpenCode) +- MCP server management CLI with OAuth 2.1 PKCE +- Gateway prompt caching (dramatic cost reduction) +- Context compression overhaul (structured summaries, iterative updates) +- Streaming enabled by default +- 200+ bug fixes + +### v0.5.0 (March 28, 2026) — Hardening + +- Nous Portal expanded to 400+ models +- Hugging Face as first-class provider +- Telegram Private Chat Topics +- Native Modal SDK backend (replaced swe-rex) +- Plugin lifecycle hooks activated +- GPT model tool-use enforcement +- Nix flake with NixOS module +- Supply chain hardening (removed litellm, pinned deps, CI scanning) +- Anthropic per-model output limits (128K for Opus 4.6) + +### v0.6.0 (March 30, 2026) — Multi-Instance + +> 95 PRs and 16 resolved issues in 2 days + +- Profiles for multiple isolated agent instances +- MCP Server Mode (`hermes mcp serve`) +- Official Docker container +- Ordered fallback provider chain +- Feishu/Lark platform adapter +- WeCom (Enterprise WeChat) adapter +- Slack multi-workspace OAuth +- Telegram webhook mode + group controls +- Exa search backend +- Skills & credentials on remote backends + +### v0.7.0 (April 3, 2026) — Resilience + +> 168 PRs and 46 resolved issues + +- Pluggable memory provider interface (ABC-based plugin system) +- Same-provider credential pools with automatic rotation +- Camofox anti-detection browser backend +- Inline diff previews in tool activity feed +- API server session continuity + tool streaming +- ACP client-provided MCP servers +- Gateway hardening (race conditions, approval routing, compression death spirals) +- Secret exfiltration blocking (URL scanning, base64 detection) + +--- + +## 23. File Dependency Chain + +``` +hermes_constants.py (no deps — imported by everything) + ↑ +tools/registry.py (no tool deps — imported by all tool files) + ↑ +tools/*.py (each calls registry.register() at import time) + ↑ +model_tools.py (imports tools/registry + triggers tool discovery) + ↑ +run_agent.py (AIAgent), cli.py (HermesCLI), gateway/run.py (GatewayRunner) + ↑ +hermes_cli/main.py (entry point — dispatches to all subsystems) +``` + +**Key Principle:** `tools/registry.py` is circular-import safe. It has no tool dependencies. Tool files import the registry; `model_tools.py` imports both. + +--- + +## 24. Key Design Patterns + +| Pattern | Description | +| ------------------------------ | -------------------------------------------------------------------------------------------------- | +| **Registry-based Tool System** | Single source of truth; plugins register at import time; dynamic (MCP) and static tools coexist | +| **Toolset Composition** | Recursive resolution with cycle detection; platform-specific composites | +| **Iteration Budget** | Thread-safe shared budget across parent + subagents | +| **Streaming First** | Preferred over non-streaming for health checking (stale connection detection) | +| **Prefix Caching** | System prompt cached across turns (Anthropic optimization); context never altered mid-conversation | +| **Proactive Compression** | Triggered at 50% context usage; structured summaries with iterative updates | +| **Async Bridging** | Persistent event loops prevent "Event loop is closed"; per-thread loops for workers | +| **Profile Isolation** | HERMES_HOME env var set before imports; all state functions route through `get_hermes_home()` | +| **Agent Caching** | Gateway caches AIAgent per session to preserve prompt cache across turns | +| **WAL Concurrency** | SQLite WAL mode + jitter retry for concurrent readers + single writer | +| **Plugin Architecture** | Tools, toolsets, hooks, memory providers extensible via plugins | +| **Multi-Backend Execution** | Pluggable terminal backends with unified BaseEnvironment interface | +| **Safety Layers** | Approval system → sensitive path guards → injection detection → capability filtering | + +--- + +## 25. Configuration Reference + +### Environment Variables (key ones) + +| Variable | Purpose | +| --------------------- | --------------------------------------------------------- | +| `HERMES_HOME` | Override home directory (profiles set this automatically) | +| `OPENROUTER_API_KEY` | OpenRouter provider key | +| `ANTHROPIC_API_KEY` | Anthropic provider key | +| `OPENAI_API_KEY` | OpenAI provider key | +| `NOUS_API_KEY` | Nous Portal key | +| `FIRECRAWL_API_KEY` | Web search/extraction | +| `EXA_API_KEY` | Exa search backend | +| `BROWSERBASE_API_KEY` | Cloud browser | +| `FAL_KEY` | Image generation | +| `ELEVENLABS_API_KEY` | Premium TTS | +| `HONCHO_API_KEY` | Honcho memory | +| `TERMINAL_ENV` | Terminal backend override | +| `TERMINAL_CWD` | Terminal working directory | +| `MESSAGING_CWD` | Gateway working directory | + +### Config File Locations + +| File | Purpose | +| ----------------------------- | -------------------------------- | +| `~/.hermes/config.yaml` | Main configuration (YAML) | +| `~/.hermes/.env` | API keys and secrets | +| `~/.hermes/MEMORY.md` | Persistent agent memory | +| `~/.hermes/USER.md` | User profile | +| `~/.hermes/SOUL.md` | Agent persona/identity | +| `~/.hermes/sessions.db` | SQLite session database | +| `~/.hermes/cron/jobs.json` | Cron job definitions | +| `.hermes.md` (in project dir) | Per-project context file | +| `AGENTS.md` (in project dir) | Developer instructions for agent | + +--- + +## 26. Known Pitfalls + +1. **DO NOT hardcode `~/.hermes` paths** — Use `get_hermes_home()` from `hermes_constants`. Hardcoding breaks profiles. + +2. **DO NOT use `simple_term_menu`** — Rendering bugs in tmux/iTerm2 (ghosting). Use `curses` instead. + +3. **DO NOT use `\033[K`** (ANSI erase-to-EOL) — Leaks as literal text under prompt_toolkit's `patch_stdout`. Use space-padding. + +4. **`_last_resolved_tool_names` is process-global** — Saved/restored around subagent execution in `delegate_tool.py`. + +5. **DO NOT hardcode cross-tool references in schemas** — Tool may be unavailable. Add dynamic references in `get_tool_definitions()`. + +6. **Tests must not write to `~/.hermes/`** — `_isolate_hermes_home` autouse fixture redirects to temp dir. + +7. **Prompt caching must not break** — Do NOT alter past context, change toolsets, reload memories, or rebuild system prompts mid-conversation. + +8. **Working directory behavior differs:** CLI uses `os.getcwd()`, gateway uses `MESSAGING_CWD` env var. + +9. **Config has three loaders:** `load_cli_config()` (CLI), `load_config()` (hermes tools/setup), direct YAML (gateway). They have different merge behaviors. + +10. **Profile operations are HOME-anchored** — `_get_profiles_root()` returns `Path.home() / ".hermes" / "profiles"`, NOT `get_hermes_home() / "profiles"`. This is intentional for cross-profile visibility. + +--- + +_This document covers Hermes Agent v0.7.0 as of April 2026. For the latest information, refer to the [official documentation](https://hermes-agent.nousresearch.com/docs/) and the [GitHub repository](https://github.com/NousResearch/hermes-agent)._ diff --git a/.claude/skills/lat-md/SKILL.md b/.claude/skills/lat-md/SKILL.md new file mode 100644 index 0000000..4208385 --- /dev/null +++ b/.claude/skills/lat-md/SKILL.md @@ -0,0 +1,169 @@ +--- +name: lat-md +description: >- + Writing and maintaining lat.md documentation files — structured markdown that + describes a project's architecture, design decisions, and test specs. Use when + creating, editing, or reviewing files in the lat.md/ directory. +--- + +# lat.md Authoring Guide + +This skill covers the syntax, structure rules, and conventions for writing `lat.md/` files. Load it whenever you need to create or edit sections in the `lat.md/` directory. + +## What belongs in lat.md + +`lat.md/` files describe **what** the project does and **why** — domain concepts, key design decisions, business logic, and test specifications. They do NOT duplicate source code. Think of each section as an anchor that source code references back to. + +Good candidates for sections: +- Architecture decisions and their rationale +- Domain concepts and business rules +- API contracts and protocols +- Test specifications (what is tested and why) +- Non-obvious constraints or invariants + +Bad candidates: +- Step-by-step code walkthroughs (the code itself is the walkthrough) +- Auto-generated API docs (use tools for that) +- Temporary notes or TODOs + +## Section structure + +Every section **must** have a leading paragraph — at least one sentence immediately after the heading, before any child headings or other block content. + +The first paragraph must be ≤250 characters (excluding `[[wiki link]]` content). This paragraph is the section's identity — it appears in search results, command output, and RAG context. + +```markdown +# Good Section + +Brief overview of what this section documents and why it matters. + +More detail can go in subsequent paragraphs, code blocks, or lists. + +## Child heading + +Details about this child topic. +``` + +```markdown +# Bad Section + +## Child heading + +This is invalid — "Bad Section" has no leading paragraph. +``` + +`lat check` enforces this rule. + +## Section IDs + +Sections are addressed by file path and heading chain: + +- **Full form**: `lat.md/path/to/file#Heading#SubHeading` +- **Short form**: `file#Heading#SubHeading` (when the file stem is unique) + +Examples: `lat.md/tests/search#RAG Replay Tests`, `cli#init`, `parser#Wiki Links`. + +## Wiki links + +Cross-reference other sections or source code with `[[target]]` or `[[target|alias]]`. + +### Section links + +```markdown +See [[cli#init]] for setup details. +The parser validates [[parser#Wiki Links|wiki link syntax]]. +``` + +### Source code links + +Reference functions, classes, constants, and methods in source files: + +```markdown +[[src/config.ts#getConfigDir]] — function +[[src/server.ts#App#listen]] — class method +[[lib/utils.py#parse_args]] — Python function +[[src/lib.rs#Greeter#greet]] — Rust impl method +[[src/app.go#Greeter#Greet]] — Go method +[[src/app.h#Greeter]] — C struct +``` + +`lat check` validates that all targets exist. + +## Code refs + +Tie source code back to `lat.md/` sections with `@lat:` comments: + +```typescript +// @lat: [[cli#init]] +export function init() { ... } +``` + +```python +# @lat: [[cli#init]] +def init(): + ... +``` + +Supported comment styles: `//` (JS/TS/Rust/Go/C) and `#` (Python). + +Place one `@lat:` comment per section, at the relevant code — not at the top of the file. + +## Test specs + +Describe tests as sections in `lat.md/` files. Add frontmatter to require that every leaf section has a matching `@lat:` comment in test code: + +```markdown +--- +lat: + require-code-mention: true +--- +# Tests + +Authentication test specifications. + +## User login + +Verify credential validation and error handling. + +### Rejects expired tokens + +Tokens past their expiry timestamp are rejected with 401, even if otherwise valid. + +### Handles missing password + +Login request without a password field returns 400 with a descriptive error. +``` + +Each test references its spec: + +```python +# @lat: [[tests#User login#Rejects expired tokens]] +def test_rejects_expired_tokens(): + ... +``` + +Rules: +- Every leaf section under `require-code-mention: true` must be referenced by exactly one `@lat:` comment +- Every section MUST have a description — at least one sentence explaining what the test verifies and why +- `lat check` flags unreferenced specs and dangling code refs + +## Frontmatter + +Optional YAML frontmatter at the top of `lat.md/` files: + +```yaml +--- +lat: + require-code-mention: true +--- +``` + +Currently the only supported field is `require-code-mention` for test spec enforcement. + +## Validation + +Always run `lat check` after editing `lat.md/` files. It validates: +- All wiki links point to existing sections or source code symbols +- All `@lat:` code refs point to existing sections +- Every section has a leading paragraph (≤250 chars) +- All `require-code-mention` leaf sections are referenced in code diff --git a/.claude/skills/typescript-expert b/.claude/skills/typescript-expert new file mode 120000 index 0000000..639dcbb --- /dev/null +++ b/.claude/skills/typescript-expert @@ -0,0 +1 @@ +../../.agents/skills/typescript-expert \ No newline at end of file diff --git a/.codex/hooks.json b/.codex/hooks.json new file mode 100644 index 0000000..36f1e57 --- /dev/null +++ b/.codex/hooks.json @@ -0,0 +1,24 @@ +{ + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "lat hook claude UserPromptSubmit" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "lat hook claude Stop" + } + ] + } + ] + } +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5db600c --- /dev/null +++ b/.env.example @@ -0,0 +1,33 @@ +# Hermes Analytics Configuration (optional) +# Set these in your build environment or CI to enable analytics. +# These values are NOT exposed to users — analytics only works in official builds. +# +# GITHUB SECRETS SETUP: +# 1. Go to Settings → Secrets and variables → Actions +# 2. Add repository secrets: +# - VITE_ANALYTICS_BASE_URL - Base URL of the analytics service +# (e.g. https://analytics.hermesone.org) +# - VITE_ANALYTICS_API_KEY - API key sent as the x-api-key header +# 3. These are automatically injected during GitHub Actions builds. +# 4. Forks without these secrets will have analytics disabled. + +VITE_ANALYTICS_BASE_URL=https://analytics.hermesone.org + +VITE_ANALYTICS_API_KEY=your_analytics_api_key_here + +# Hermes One Backend Configuration (optional) +# Base URL + client key for device login and desktop↔backend agent sync. +# The MAIN_VITE_ prefix scopes these to the main process (where the API calls +# run) so the key isn't also copied into the renderer bundle. +# +# GITHUB SETUP (injected during GitHub Actions builds): +# - Repository VARIABLE HERMES_API_URL → MAIN_VITE_HERMES_API_URL +# - Repository SECRET HERMES_API_KEY → MAIN_VITE_HERMES_API_KEY +# +# Local dev: leave the URL at the Nitro dev server and the key empty (the +# backend doesn't enforce the key yet). Runtime HERMES_API_URL / HERMES_API_KEY +# override these baked values. + +MAIN_VITE_HERMES_API_URL=http://localhost:3002 + +MAIN_VITE_HERMES_API_KEY=your_hermes_api_key_here diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..eb8707c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Shell scripts must always be LF — CRLF breaks the shebang line on +# Linux ("bad interpreter: /bin/bash\r: No such file or directory"), +# which silently fails the .deb / .rpm postinst hook (#395). +*.sh text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bf8be75 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +# Runs the type checker and test suite on every PR and on pushes to main, +# so broken code is caught before it merges. Mirrors the Node version and +# install step already used by release.yml. + +on: + pull_request: + push: + branches: [main] + +# A newer commit on the same ref supersedes an in-flight run. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Test + run: npm test + + # Lint is informational for now — the codebase has a large backlog of + # prettier/line-ending warnings (no errors). Surfaced but not gating, + # so a warning backlog doesn't block merges. + - name: Lint + run: npm run lint + continue-on-error: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d1dfb96 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,403 @@ +name: Release + +on: + push: + branches: + - release + workflow_dispatch: + inputs: + dry_run: + description: "Run all build jobs but skip publish (no tag, no GitHub Release)" + type: boolean + default: true + +permissions: + contents: write + +concurrency: + group: release + cancel-in-progress: true + +jobs: + prepare: + name: Prepare Release + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + tag: ${{ steps.version.outputs.tag }} + tag_exists: ${{ steps.check.outputs.exists }} + is_dry_run: ${{ steps.mode.outputs.is_dry_run }} + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Read version from package.json + id: version + run: | + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" + echo "Release version: v$VERSION" + + - name: Check if tag already exists + id: check + run: | + if git ls-remote --tags origin "refs/tags/v${{ steps.version.outputs.version }}" | grep -q .; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "Tag v${{ steps.version.outputs.version }} already exists — skipping." + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Compute dry-run flag + id: mode + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.dry_run }}" = "true" ]; then + echo "is_dry_run=true" >> "$GITHUB_OUTPUT" + echo "Dry run: builds will run, publish will be skipped." + else + echo "is_dry_run=false" >> "$GITHUB_OUTPUT" + echo "Real release: publish will run if tag does not exist." + fi + + release_mac: + name: Build macOS (${{ matrix.arch }}) + needs: prepare + if: needs.prepare.outputs.tag_exists == 'false' + runs-on: macos-latest + strategy: + matrix: + arch: [x64, arm64] + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Rebuild native dependencies for target arch + run: npx electron-builder install-app-deps --arch=${{ matrix.arch }} + + - name: Build app + env: + VITE_ANALYTICS_BASE_URL: ${{ vars.VITE_ANALYTICS_BASE_URL }} + VITE_ANALYTICS_API_KEY: ${{ secrets.VITE_ANALYTICS_API_KEY }} + MAIN_VITE_HERMES_API_URL: ${{ vars.HERMES_API_URL }} + MAIN_VITE_HERMES_API_KEY: ${{ secrets.HERMES_API_KEY }} + run: npm run build + + - name: Stage App Store Connect API key + env: + ASC_API_KEY_BASE64: ${{ secrets.ASC_API_KEY }} + ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} + run: | + set -euo pipefail + if [ -z "${ASC_API_KEY_BASE64:-}" ]; then + echo "ASC_API_KEY secret is missing — notarization will fail." >&2 + exit 1 + fi + KEY_DIR="$RUNNER_TEMP/appstore" + mkdir -p "$KEY_DIR" + KEY_PATH="$KEY_DIR/AuthKey_${ASC_KEY_ID}.p8" + printf '%s' "$ASC_API_KEY_BASE64" | base64 --decode > "$KEY_PATH" + chmod 600 "$KEY_PATH" + echo "APPLE_API_KEY=$KEY_PATH" >> "$GITHUB_ENV" + + - name: Package macOS artifacts + env: + CSC_LINK: ${{ secrets.CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + APPLE_API_KEY_ID: ${{ secrets.ASC_KEY_ID }} + APPLE_API_ISSUER: ${{ secrets.ASC_ISSUER_ID }} + run: npx electron-builder --mac dmg zip --${{ matrix.arch }} --publish never + + - name: Verify native module architecture + run: | + set -euo pipefail + NODE_FILE="dist/mac-${{ matrix.arch }}/Hermes One.app/Contents/Resources/app.asar.unpacked/node_modules/better-sqlite3/build/Release/better_sqlite3.node" + if [ ! -f "$NODE_FILE" ]; then + NODE_FILE="dist/mac/Hermes One.app/Contents/Resources/app.asar.unpacked/node_modules/better-sqlite3/build/Release/better_sqlite3.node" + fi + file "$NODE_FILE" + case "${{ matrix.arch }}" in + x64) file "$NODE_FILE" | grep -q "x86_64" ;; + arm64) file "$NODE_FILE" | grep -q "arm64" ;; + esac + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: mac-${{ matrix.arch }}-artifacts + path: | + dist/*.dmg + dist/*.zip + dist/*.blockmap + + release_linux: + name: Build Linux (${{ matrix.arch }}) + needs: prepare + if: needs.prepare.outputs.tag_exists == 'false' + runs-on: ${{ matrix.runs-on }} + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + runs-on: ubuntu-latest + targets: AppImage deb rpm + - arch: arm64 + runs-on: ubuntu-24.04-arm + targets: AppImage + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Rebuild native dependencies for target arch + run: npx electron-builder install-app-deps --arch=${{ matrix.arch }} + + - name: Build app + env: + VITE_ANALYTICS_BASE_URL: ${{ vars.VITE_ANALYTICS_BASE_URL }} + VITE_ANALYTICS_API_KEY: ${{ secrets.VITE_ANALYTICS_API_KEY }} + MAIN_VITE_HERMES_API_URL: ${{ vars.HERMES_API_URL }} + MAIN_VITE_HERMES_API_KEY: ${{ secrets.HERMES_API_KEY }} + run: npm run build + + - name: Install rpmbuild + if: contains(matrix.targets, 'rpm') + run: sudo apt-get update && sudo apt-get install -y rpm + + - name: Package Linux artifacts + run: npx electron-builder --linux ${{ matrix.targets }} --${{ matrix.arch }} --publish never + + - name: Upload x64 artifacts + if: matrix.arch == 'x64' + uses: actions/upload-artifact@v4 + with: + name: linux-x64-artifacts + path: | + dist/*.AppImage + dist/*.deb + dist/*.rpm + dist/latest-linux.yml + + - name: Upload arm64 artifacts + if: matrix.arch == 'arm64' + uses: actions/upload-artifact@v4 + with: + name: linux-arm64-artifacts + path: | + dist/*.AppImage + + release_windows: + name: Build Windows + needs: prepare + if: needs.prepare.outputs.tag_exists == 'false' + runs-on: windows-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build app + env: + VITE_ANALYTICS_BASE_URL: ${{ vars.VITE_ANALYTICS_BASE_URL }} + VITE_ANALYTICS_API_KEY: ${{ secrets.VITE_ANALYTICS_API_KEY }} + MAIN_VITE_HERMES_API_URL: ${{ vars.HERMES_API_URL }} + MAIN_VITE_HERMES_API_KEY: ${{ secrets.HERMES_API_KEY }} + run: npm run build + + - name: Package Windows artifacts + run: npx electron-builder --win nsis portable --x64 --publish never + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: windows-artifacts + path: | + dist/*.exe + dist/*.exe.blockmap + dist/latest.yml + + generate_winget: + name: Generate winget manifests + needs: [prepare, release_windows] + if: needs.prepare.outputs.tag_exists == 'false' + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Download Windows installer artifact + uses: actions/download-artifact@v4 + with: + name: windows-artifacts + path: dist/ + + - name: Generate winget manifests + env: + VERSION: ${{ needs.prepare.outputs.version }} + PUBLISH_OWNER: fathah + run: node scripts/generate-winget-manifests.mjs + + - name: Upload winget manifests artifact + uses: actions/upload-artifact@v4 + with: + name: winget-manifests-${{ needs.prepare.outputs.version }} + path: dist/winget/ + + publish: + name: Publish Release + needs: + [prepare, release_mac, release_linux, release_windows, generate_winget] + if: needs.prepare.outputs.is_dry_run == 'false' && needs.prepare.outputs.tag_exists == 'false' + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Create tag + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag ${{ needs.prepare.outputs.tag }} + git push origin ${{ needs.prepare.outputs.tag }} + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + + - name: Generate macOS update metadata + env: + VERSION: ${{ needs.prepare.outputs.version }} + run: | + node <<'NODE' + const crypto = require("crypto"); + const fs = require("fs"); + const path = require("path"); + + const artifactsDir = path.join(process.cwd(), "artifacts"); + const version = process.env.VERSION; + const zipNames = fs + .readdirSync(artifactsDir) + .filter( + (name) => + name.startsWith(`hermes-desktop-${version}-`) && + name.endsWith("-mac.zip"), + ) + .sort((a, b) => { + if (a.includes("-x64-")) return -1; + if (b.includes("-x64-")) return 1; + return a.localeCompare(b); + }); + + if ( + zipNames.length < 2 || + !zipNames.some((name) => name.includes("-x64-")) || + !zipNames.some((name) => name.includes("-arm64-")) + ) { + throw new Error( + `Expected x64 and arm64 macOS zips, found: ${zipNames.join(", ")}`, + ); + } + + const files = zipNames.map((name) => { + const filePath = path.join(artifactsDir, name); + return { + url: name, + sha512: crypto + .createHash("sha512") + .update(fs.readFileSync(filePath)) + .digest("base64"), + size: fs.statSync(filePath).size, + }; + }); + const primary = files.find((file) => file.url.includes("-x64-")) || files[0]; + const releaseDate = new Date().toISOString(); + const lines = [ + `version: ${version}`, + "files:", + ...files.flatMap((file) => [ + ` - url: ${file.url}`, + ` sha512: ${file.sha512}`, + ` size: ${file.size}`, + ]), + `path: ${primary.url}`, + `sha512: ${primary.sha512}`, + `releaseDate: '${releaseDate}'`, + "", + ]; + + fs.writeFileSync(path.join(artifactsDir, "latest-mac.yml"), lines.join("\n")); + NODE + + - name: Publish GitHub release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.prepare.outputs.tag }} + name: Hermes Desktop ${{ needs.prepare.outputs.tag }} + generate_release_notes: true + files: | + artifacts/*.dmg + artifacts/*.zip + artifacts/*.AppImage + artifacts/*.deb + artifacts/*.rpm + artifacts/*.exe + artifacts/*.blockmap + artifacts/latest.yml + artifacts/latest-linux.yml + artifacts/latest-mac.yml + + rebuild_landing_page: + name: Rebuild landing page + needs: [prepare, publish] + # Only after a real release that actually published a new tag. + if: needs.prepare.outputs.is_dry_run == 'false' && needs.prepare.outputs.tag_exists == 'false' + runs-on: ubuntu-latest + steps: + - name: Wait for release to propagate + # Give GitHub's releases/latest API time to settle before the landing + # page rebuilds and re-fetches the latest version. + run: sleep 300 + + - name: Trigger landing page deploy + # SITE_DEPLOY_TOKEN is a fine-grained PAT with "Actions: read and write" + # on fathah/hermes-landing-page. The default GITHUB_TOKEN cannot dispatch + # workflows in another repository. + env: + GH_TOKEN: ${{ secrets.SITE_DEPLOY_TOKEN }} + run: | + gh workflow run deploy.yml \ + --repo fathah/hermes-landing-page \ + --ref main diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6bba941 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +.DS_Store +.idea/ + +# Dependencies +node_modules/ + +# Build output +dist/ +out/ + +# TypeScript incremental build info +*.tsbuildinfo + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Environment files +.env +.env.* +!.env.example + +# ESLint cache +.eslintcache + +# Local isolated Hermes homes for development/tests +.sandbox/ + +# Electron packaging artifacts +release/ +.claude/worktrees + +# Tauri +**/src-tauri/target/ +**/src-tauri/gen/ +**/src-tauri/WixTools/ + +# Local launcher scripts (user-specific, do not commit) +/*.bat +/*.ps1 +.mcp.json diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..7556de4 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,25 @@ +BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null) +if [[ "$BRANCH" != "release" && "$BRANCH" != release/* ]]; then + exit 0 +fi + +# Git hooks run with a minimal PATH that omits user-local tool dirs, so bun +# (installed under ~/.bun/bin) isn't found. Add common locations, then prefer +# bun and fall back to npm. +export PATH="$HOME/.bun/bin:$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH" + +if command -v bun >/dev/null 2>&1; then + RUN="bun run" +elif command -v npm >/dev/null 2>&1; then + RUN="npm run" +else + echo "✗ Neither bun nor npm found on PATH. Commit aborted."; exit 1 +fi + +echo "→ [pre-commit] Release branch — running lint..." +$RUN lint || { echo "✗ Lint failed. Commit aborted."; exit 1; } + +echo "→ [pre-commit] Running tests..." +$RUN test || { echo "✗ Tests failed. Commit aborted."; exit 1; } + +echo "✓ [pre-commit] Lint and tests passed." diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 0000000..e5ff5ae --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,22 @@ +BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null) +if [[ "$BRANCH" != "release" && "$BRANCH" != release/* ]]; then + exit 0 +fi + +# Git hooks run with a minimal PATH that omits user-local tool dirs, so bun +# (installed under ~/.bun/bin) isn't found. Add common locations, then prefer +# bun and fall back to npm. +export PATH="$HOME/.bun/bin:$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH" + +if command -v bun >/dev/null 2>&1; then + RUN="bun run" +elif command -v npm >/dev/null 2>&1; then + RUN="npm run" +else + echo "✗ Neither bun nor npm found on PATH. Push aborted."; exit 1 +fi + +echo "→ [pre-push] Release branch — running build..." +$RUN build || { echo "✗ Build failed. Push aborted."; exit 1; } + +echo "✓ [pre-push] Build passed." diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c544150 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,105 @@ +# Before starting work + +- Run `lat search` to find sections relevant to your task. Read them to understand the design intent before writing code. +- Run `lat expand` on user prompts to expand any `[[refs]]` — this resolves section names to file locations and provides context. + +# Post-task checklist (REQUIRED — do not skip) + +After EVERY task, before responding to the user: + +- [ ] Update `lat.md/` if you added or changed any functionality, architecture, tests, or behavior +- [ ] Run `lat check` — all wiki links and code refs must pass +- [ ] Do not skip these steps. Do not consider your task done until both are complete. + +--- + +# What is lat.md? + +This project uses [lat.md](https://www.npmjs.com/package/lat.md) to maintain a structured knowledge graph of its architecture, design decisions, and test specs in the `lat.md/` directory. It is a set of cross-linked markdown files that describe **what** this project does and **why** — the domain concepts, key design decisions, business logic, and test specifications. Use it to ground your work in the actual architecture rather than guessing. + +# Commands + +```bash +lat locate "Section Name" # find a section by name (exact, fuzzy) +lat refs "file#Section" # find what references a section +lat search "natural language" # semantic search across all sections +lat expand "user prompt text" # expand [[refs]] to resolved locations +lat check # validate all links and code refs +``` + +Run `lat --help` when in doubt about available commands or options. + +If `lat search` fails because no API key is configured, explain to the user that semantic search requires a key provided via `LAT_LLM_KEY` (direct value), `LAT_LLM_KEY_FILE` (path to key file), or `LAT_LLM_KEY_HELPER` (command that prints the key). Supported key prefixes: `sk-...` (OpenAI) or `vck_...` (Vercel). If the user doesn't want to set it up, use `lat locate` for direct lookups instead. + +# Syntax primer + +- **Section ids**: `lat.md/path/to/file#Heading#SubHeading` — full form uses project-root-relative path (e.g. `lat.md/tests/search#RAG Replay Tests`). Short form uses bare file name when unique (e.g. `search#RAG Replay Tests`, `cli#search#Indexing`). +- **Wiki links**: `[[target]]` or `[[target|alias]]` — cross-references between sections. Can also reference source code: `[[src/foo.ts#myFunction]]`. +- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct). `lat check` validates these exist. +- **Code refs**: `// @lat: [[section-id]]` (JS/TS/Rust/Go/C) or `# @lat: [[section-id]]` (Python) — ties source code to concepts + +# Test specs + +Key tests can be described as sections in `lat.md/` files (e.g. `tests.md`). Add frontmatter to require that every leaf section is referenced by a `// @lat:` or `# @lat:` comment in test code: + +```markdown +--- +lat: + require-code-mention: true +--- +# Tests + +Authentication and authorization test specifications. + +## User login + +Verify credential validation and error handling for the login endpoint. + +### Rejects expired tokens +Tokens past their expiry timestamp are rejected with 401, even if otherwise valid. + +### Handles missing password +Login request without a password field returns 400 with a descriptive error. +``` + +Every section MUST have a description — at least one sentence explaining what the test verifies and why. Empty sections with just a heading are not acceptable. (This is a specific case of the general leading paragraph rule below.) + +Each test in code should reference its spec with exactly one comment placed next to the relevant test — not at the top of the file: + +```python +# @lat: [[tests#User login#Rejects expired tokens]] +def test_rejects_expired_tokens(): + ... + +# @lat: [[tests#User login#Handles missing password]] +def test_handles_missing_password(): + ... +``` + +Do not duplicate refs. One `@lat:` comment per spec section, placed at the test that covers it. `lat check` will flag any spec section not covered by a code reference, and any code reference pointing to a nonexistent section. + +# Section structure + +Every section in `lat.md/` **must** have a leading paragraph — at least one sentence immediately after the heading, before any child headings or other block content. The first paragraph must be ≤250 characters (excluding `[[wiki link]]` content). This paragraph serves as the section's overview and is used in search results, command output, and RAG context — keeping it concise guarantees the section's essence is always captured. + +```markdown +# Good Section + +Brief overview of what this section documents and why it matters. + +More detail can go in subsequent paragraphs, code blocks, or lists. + +## Child heading + +Details about this child topic. +``` + +```markdown +# Bad Section + +## Child heading + +Details about this child topic. +``` + +The second example is invalid because `Bad Section` has no leading paragraph. `lat check` validates this rule and reports errors for missing or overly long leading paragraphs. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c544150 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,105 @@ +# Before starting work + +- Run `lat search` to find sections relevant to your task. Read them to understand the design intent before writing code. +- Run `lat expand` on user prompts to expand any `[[refs]]` — this resolves section names to file locations and provides context. + +# Post-task checklist (REQUIRED — do not skip) + +After EVERY task, before responding to the user: + +- [ ] Update `lat.md/` if you added or changed any functionality, architecture, tests, or behavior +- [ ] Run `lat check` — all wiki links and code refs must pass +- [ ] Do not skip these steps. Do not consider your task done until both are complete. + +--- + +# What is lat.md? + +This project uses [lat.md](https://www.npmjs.com/package/lat.md) to maintain a structured knowledge graph of its architecture, design decisions, and test specs in the `lat.md/` directory. It is a set of cross-linked markdown files that describe **what** this project does and **why** — the domain concepts, key design decisions, business logic, and test specifications. Use it to ground your work in the actual architecture rather than guessing. + +# Commands + +```bash +lat locate "Section Name" # find a section by name (exact, fuzzy) +lat refs "file#Section" # find what references a section +lat search "natural language" # semantic search across all sections +lat expand "user prompt text" # expand [[refs]] to resolved locations +lat check # validate all links and code refs +``` + +Run `lat --help` when in doubt about available commands or options. + +If `lat search` fails because no API key is configured, explain to the user that semantic search requires a key provided via `LAT_LLM_KEY` (direct value), `LAT_LLM_KEY_FILE` (path to key file), or `LAT_LLM_KEY_HELPER` (command that prints the key). Supported key prefixes: `sk-...` (OpenAI) or `vck_...` (Vercel). If the user doesn't want to set it up, use `lat locate` for direct lookups instead. + +# Syntax primer + +- **Section ids**: `lat.md/path/to/file#Heading#SubHeading` — full form uses project-root-relative path (e.g. `lat.md/tests/search#RAG Replay Tests`). Short form uses bare file name when unique (e.g. `search#RAG Replay Tests`, `cli#search#Indexing`). +- **Wiki links**: `[[target]]` or `[[target|alias]]` — cross-references between sections. Can also reference source code: `[[src/foo.ts#myFunction]]`. +- **Source code links**: Wiki links in `lat.md/` files can reference functions, classes, constants, and methods in TypeScript/JavaScript/Python/Rust/Go/C files. Use the full path: `[[src/config.ts#getConfigDir]]`, `[[src/server.ts#App#listen]]` (class method), `[[lib/utils.py#parse_args]]`, `[[src/lib.rs#Greeter#greet]]` (Rust impl method), `[[src/app.go#Greeter#Greet]]` (Go method), `[[src/app.h#Greeter]]` (C struct). `lat check` validates these exist. +- **Code refs**: `// @lat: [[section-id]]` (JS/TS/Rust/Go/C) or `# @lat: [[section-id]]` (Python) — ties source code to concepts + +# Test specs + +Key tests can be described as sections in `lat.md/` files (e.g. `tests.md`). Add frontmatter to require that every leaf section is referenced by a `// @lat:` or `# @lat:` comment in test code: + +```markdown +--- +lat: + require-code-mention: true +--- +# Tests + +Authentication and authorization test specifications. + +## User login + +Verify credential validation and error handling for the login endpoint. + +### Rejects expired tokens +Tokens past their expiry timestamp are rejected with 401, even if otherwise valid. + +### Handles missing password +Login request without a password field returns 400 with a descriptive error. +``` + +Every section MUST have a description — at least one sentence explaining what the test verifies and why. Empty sections with just a heading are not acceptable. (This is a specific case of the general leading paragraph rule below.) + +Each test in code should reference its spec with exactly one comment placed next to the relevant test — not at the top of the file: + +```python +# @lat: [[tests#User login#Rejects expired tokens]] +def test_rejects_expired_tokens(): + ... + +# @lat: [[tests#User login#Handles missing password]] +def test_handles_missing_password(): + ... +``` + +Do not duplicate refs. One `@lat:` comment per spec section, placed at the test that covers it. `lat check` will flag any spec section not covered by a code reference, and any code reference pointing to a nonexistent section. + +# Section structure + +Every section in `lat.md/` **must** have a leading paragraph — at least one sentence immediately after the heading, before any child headings or other block content. The first paragraph must be ≤250 characters (excluding `[[wiki link]]` content). This paragraph serves as the section's overview and is used in search results, command output, and RAG context — keeping it concise guarantees the section's essence is always captured. + +```markdown +# Good Section + +Brief overview of what this section documents and why it matters. + +More detail can go in subsequent paragraphs, code blocks, or lists. + +## Child heading + +Details about this child topic. +``` + +```markdown +# Bad Section + +## Child heading + +Details about this child topic. +``` + +The second example is invalid because `Bad Section` has no leading paragraph. `lat check` validates this rule and reports errors for missing or overly long leading paragraphs. diff --git a/CONTRIBUTING.ja-JP.md b/CONTRIBUTING.ja-JP.md new file mode 100644 index 0000000..75379b2 --- /dev/null +++ b/CONTRIBUTING.ja-JP.md @@ -0,0 +1,104 @@ +# Hermes Desktop へのコントリビューション + +Hermes Desktop へのコントリビューションに興味を持っていただきありがとうございます!バグ修正、新機能、ドキュメント改善、ちょっとしたタイポ修正まで、どんな貢献も歓迎します。 + +## 言語 + +- English: `CONTRIBUTING.md` +- 简体中文: `CONTRIBUTING.zh-CN.md` +- 日本語: `CONTRIBUTING.ja-JP.md` + +## はじめに + +1. リポジトリを **Fork** し、ローカルにクローンします。 +2. **依存関係をインストール:** + + ```bash + npm install + ``` + +3. **開発モードでアプリを起動:** + + ```bash + npm run dev + ``` + +## 変更を加える + +1. `main` から新しいブランチを作成します。 + + ```bash + git checkout -b your-branch-name + ``` + +2. 変更を加えます。コミットは目的を絞って — 1 つの論理的な変更につき 1 つのコミットを心掛けてください。 + +3. 提出前にチェックを実行します。 + + ```bash + npm run lint + npm run typecheck + ``` + +4. `npm run dev` でローカル動作確認を行い、期待通りに動くことを確認してください。 + +## プルリクエストの提出 + +1. ブランチを自分の fork に push します。 +2. 上流リポジトリの `main` に対してプルリクエストを開きます。 +3. 変更内容とその理由を明確に記述してください。 +4. PR が Open Issue に対応する場合は、参照を記述してください(例: `Fixes #42`)。 + +### プルリクエストは小さく保つ + +PR は小さく目的を絞ってください — その方がレビューもマージもずっと容易になります。多くのファイルに触れる PR や、無関係な変更をまとめた PR は、分割を依頼されたり、受け入れられない場合があります。 + +- 1 つの PR につき 1 つの論理的な変更(1 つの修正、1 つの機能、1 つのリファクタリング)に絞ってください。 +- 無関係なファイルを多数触っていることに気づいたら、作業を複数の PR に分割してください。 +- フォーマット / スタイルの一括変更を機能変更と混在させないでください。 +- 小さな PR ほど、レビューとマージが速くなります。 + +メンテナが PR をレビューし、変更を依頼する場合があります。承認されるとマージされます。 + +## バグ報告 + +バグを見つけた場合は、以下の情報を添えて [Issue を作成してください](https://github.com/fathah/hermes-desktop/issues/new)。 + +- 明確なタイトルと説明 +- 再現手順 +- 期待される動作と実際の動作 +- 必要に応じて OS とアプリのバージョン + +## 機能リクエスト + +アイデアがある場合は、[Issue を作成して](https://github.com/fathah/hermes-desktop/issues/new)以下を記述してください。 + +- 解決したい問題 +- どのように動作してほしいか +- 検討した代替案 + +## プロジェクト構成 + +```text +src/main/ Electron メインプロセス、IPC ハンドラ、Hermes 統合 +src/preload/ セキュアな renderer ブリッジ +src/renderer/src/ React アプリと UI コンポーネント +resources/ アプリアイコンとパッケージ用アセット +build/ パッケージング用リソース +``` + +## コードスタイル + +- 本プロジェクトでは TypeScript、React、Electron を使用しています。 +- Lint エラーを確認するには `npm run lint` を実行してください。 +- 型の安全性を検証するには `npm run typecheck` を実行してください。 +- コードベース内の既存のパターンや慣習に従ってください。 + +## コミュニティ + +- 他のコントリビューターと話すには [Nous Research Discord](https://discord.gg/NousResearch) に参加してください。 +- Hermes の動作についての詳細は[ドキュメント](https://hermes-agent.nousresearch.com/docs/)を参照してください。 + +## ライセンス + +コントリビューションをいただいた時点で、その内容が [MIT License](LICENSE) の下でライセンスされることに同意したものとみなします。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0d4a582 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,104 @@ +# Contributing to Hermes Desktop + +Thanks for your interest in contributing to Hermes Desktop! Whether it's a bug fix, a new feature, improved docs, or just a typo — every contribution helps. + +## Languages + +- English: `CONTRIBUTING.md` +- 简体中文: `CONTRIBUTING.zh-CN.md` +- 日本語: `CONTRIBUTING.ja-JP.md` + +## Getting Started + +1. **Fork** the repository and clone your fork locally. +2. **Install dependencies:** + + ```bash + npm install + ``` + +3. **Start the app in development mode:** + + ```bash + npm run dev + ``` + +## Making Changes + +1. Create a new branch from `main`: + + ```bash + git checkout -b your-branch-name + ``` + +2. Make your changes. Keep commits focused — one logical change per commit. + +3. Run checks before submitting: + + ```bash + npm run lint + npm run typecheck + ``` + +4. Test your changes locally with `npm run dev` to make sure everything works as expected. + +## Submitting a Pull Request + +1. Push your branch to your fork. +2. Open a pull request against `main` on the upstream repo. +3. Write a clear description of what you changed and why. +4. If your PR addresses an open issue, reference it (e.g., `Fixes #42`). + +### Keep Pull Requests Small + +Please keep PRs small and focused — they are much easier to review and merge. PRs that touch too many files or bundle unrelated changes will likely be asked for splitting up or may not be accepted. + +- Stick to one logical change per PR (one fix, one feature, one refactor). +- If you find yourself touching many unrelated files, split the work into multiple PRs. +- Avoid bundling formatting/style sweeps with functional changes. +- Smaller PRs get reviewed and merged faster. + +A maintainer will review your PR and may request changes. Once approved, it will be merged. + +## Reporting Bugs + +Found a bug? [Open an issue](https://github.com/fathah/hermes-desktop/issues/new) with: + +- A clear title and description. +- Steps to reproduce the issue. +- What you expected to happen vs. what actually happened. +- Your OS and app version, if relevant. + +## Requesting Features + +Have an idea? [Open an issue](https://github.com/fathah/hermes-desktop/issues/new) and describe: + +- The problem you're trying to solve. +- How you'd like it to work. +- Any alternatives you've considered. + +## Project Structure + +```text +src/main/ Electron main process, IPC handlers, Hermes integration +src/preload/ Secure renderer bridge +src/renderer/src/ React app and UI components +resources/ App icons and packaged assets +build/ Packaging resources +``` + +## Code Style + +- The project uses TypeScript, React, and Electron. +- Run `npm run lint` to check for lint errors. +- Run `npm run typecheck` to verify type safety. +- Follow existing patterns and conventions in the codebase. + +## Community + +- Join the [Nous Research Discord](https://discord.gg/NousResearch) to chat with other contributors. +- Check the [documentation](https://hermes-agent.nousresearch.com/docs/) for more context on how Hermes works. + +## License + +By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE). diff --git a/CONTRIBUTING.zh-CN.md b/CONTRIBUTING.zh-CN.md new file mode 100644 index 0000000..efda94d --- /dev/null +++ b/CONTRIBUTING.zh-CN.md @@ -0,0 +1,104 @@ +# 为 Hermes Desktop 做贡献 + +感谢你愿意为 Hermes Desktop 做出贡献。无论是修复 bug、添加新功能、完善文档,还是修正一个拼写错误,每一份贡献都很有价值。 + +## 语言 + +- 英文:`CONTRIBUTING.md` +- 简体中文:`CONTRIBUTING.zh-CN.md` +- 日本語:`CONTRIBUTING.ja-JP.md` + +## 快速开始 + +1. **Fork** 本仓库,并将你的 fork 克隆到本地。 +2. **安装依赖:** + + ```bash + npm install + ``` + +3. **以开发模式启动应用:** + + ```bash + npm run dev + ``` + +## 修改代码 + +1. 从 `main` 创建新分支: + + ```bash + git checkout -b your-branch-name + ``` + +2. 完成你的改动。请保持提交聚焦,每个 commit 只做一类逻辑改动。 + +3. 提交前先运行检查: + + ```bash + npm run lint + npm run typecheck + ``` + +4. 使用 `npm run dev` 在本地测试改动,确保行为符合预期。 + +## 提交 Pull Request + +1. 将分支推送到你的 fork。 +2. 在上游仓库中向 `main` 发起 Pull Request。 +3. 清楚描述你改了什么,以及为什么这样改。 +4. 如果你的 PR 解决了某个已有 issue,请在描述中引用它(例如:`Fixes #42`)。 + +### 保持 Pull Request 精简 + +请保持 PR 小而聚焦——这样更容易审核和合并。触及过多文件或捆绑了不相关改动的 PR 可能会被要求拆分,甚至可能不被接受。 + +- 每个 PR 只做一类逻辑改动(一个修复、一个功能、一次重构)。 +- 如果你发现自己改了很多不相关的文件,请将工作拆分成多个 PR。 +- 避免将格式化/样式改动与功能改动混在一起提交。 +- 更小的 PR 能更快得到审核和合并。 + +维护者会审核你的 PR,并可能提出修改建议。审核通过后,PR 会被合并。 + +## 报告 Bug + +如果你发现了 bug,请在 GitHub 上 [提交 issue](https://github.com/fathah/hermes-desktop/issues/new),并尽量包含: + +- 清晰的标题和描述 +- 复现步骤 +- 预期行为与实际行为 +- 你的操作系统和应用版本(如果相关) + +## 功能请求 + +如果你有新想法,也欢迎 [提交 issue](https://github.com/fathah/hermes-desktop/issues/new),并描述: + +- 你想解决的问题 +- 你希望它如何工作 +- 你考虑过的替代方案 + +## 项目结构 + +```text +src/main/ Electron 主进程、IPC 处理器、Hermes 集成 +src/preload/ 安全的 renderer bridge +src/renderer/src/ React 应用和 UI 组件 +resources/ 应用图标和打包资源 +build/ 打包配置资源 +``` + +## 代码风格 + +- 项目使用 TypeScript、React 和 Electron。 +- 运行 `npm run lint` 检查 lint 错误。 +- 运行 `npm run typecheck` 验证类型安全。 +- 尽量遵循当前仓库现有模式和约定。 + +## 社区 + +- 欢迎加入 [Nous Research Discord](https://discord.gg/NousResearch),与其他贡献者交流。 +- 也可以查看 [文档](https://hermes-agent.nousresearch.com/docs/) 了解 Hermes 的整体工作方式。 + +## 许可证 + +通过提交贡献,即表示你同意你的贡献将按照 [MIT License](LICENSE) 授权。 diff --git a/Development.md b/Development.md new file mode 100644 index 0000000..4230daa --- /dev/null +++ b/Development.md @@ -0,0 +1,48 @@ +## Development + +### Prerequisites + +- Node.js and npm +- A Unix-like shell environment for the Hermes installer +- Network access for downloading Hermes during first-run install + +### Install dependencies + +```bash +npm install +``` + +### Start the app in development + +```bash +npm run dev +``` + +### Run checks + +```bash +npm run lint +npm run typecheck +``` + +### Run tests + +```bash +npm run test +npm run test:watch +``` + +### Build the desktop app + +```bash +npm run build +``` + +Platform packaging: + +```bash +npm run build:mac +npm run build:win +npm run build:linux +npm run build:rpm # Fedora/RHEL .rpm only +``` diff --git a/KANBAN_GAP_REPORT.md b/KANBAN_GAP_REPORT.md new file mode 100644 index 0000000..29f5069 --- /dev/null +++ b/KANBAN_GAP_REPORT.md @@ -0,0 +1,113 @@ +# Kanban — Reference vs. hermes-desktop: Report & Plan + +_Generated 2026-06-21. Sources: `hermes-agent` docs + `kanban_db.py` + `plugins/kanban/dashboard/plugin_api.py`; our `src/main/kanban.ts` + `src/renderer/src/screens/Kanban/Kanban.tsx`._ + +## 1. What Kanban is (in hermes-agent) + +Hermes Kanban is a **durable, SQLite-backed task board** (`~/.hermes/kanban.db`, WAL mode) for coordinating multiple named agent profiles. It is the heavier sibling of `delegate_task`: + +| | `delegate_task` | Kanban | +|---|---|---| +| Shape | RPC (fork→join) | Durable queue + state machine | +| Parent | Blocks | Fire-and-forget | +| Child | Anonymous subagent | Named profile w/ persistent memory | +| Resumable | No | Block→unblock→re-run; crash→reclaim | +| Human-in-loop | No | Comment / unblock anytime | +| Audit | Lost on compression | Durable SQLite rows | + +**Three surfaces, one `kanban_db` core (cannot drift):** +1. **Agents** drive it via `kanban_*` tools (`kanban_show/list/complete/block/heartbeat/comment/create/link/unblock`). +2. **Humans/scripts/cron** drive it via `hermes kanban …` CLI and `/kanban` slash command. +3. **Dashboard plugin** (FastAPI + React SPA) at `plugins/kanban/` — REST under `/api/plugins/kanban/`, live `task_events` WebSocket. + +**Key concepts:** Board (isolated queue, multi-project) · Task · Link (parent→child dependency, promotes `todo→ready` when all parents `done`) · Comment (inter-agent protocol) · Workspace (`scratch` / `dir:` / `worktree`) · Dispatcher (gateway-embedded loop, 60s tick: reclaim stale/crashed, promote, claim, spawn) · Tenant (soft namespace within a board). + +**Notable mechanics:** auto-decompose of triage tasks, goal-mode cards (Ralph loop + judge), circuit-breaker (`failure_limit`, auto-block after N spawn failures), respawn guard (`blocker_auth`/`recent_success`/`active_pr`), `scheduled_at` delayed dispatch, diagnostics rule-engine, attachments (25 MB cap), Kanban Swarm topology helper. + +## 2. Canonical statuses (the "kanban words") + +From `kanban_db.VALID_STATUSES`: + +``` +triage · todo · scheduled · ready · running · blocked · review · done · archived +``` + +Dashboard `BOARD_COLUMNS` (plugin_api.py): `triage, todo, scheduled, ready, running, blocked, review, done` (+ `archived` via toggle). + +`VALID_INITIAL_STATUSES = {running, blocked}`. + +### Allowed status transitions (from plugin PATCH `/tasks/:id`) +- `done` → `complete_task(result, summary, metadata)` +- `blocked` → `block_task(reason)` +- `scheduled` → `schedule_task(reason)` +- `ready` → if currently `blocked`/`scheduled` → `unblock_task`; else direct set (drag-drop `todo→ready`); **refused if any parent not `done`** (409 names the blocking parent) +- `archived` → `archive_task` +- `running` → **rejected** ("use the dispatcher/claim path") +- `todo` / `triage` / `scheduled` → direct set +- Reopening a `done`/`archived` parent demotes stale-`ready` children back to `todo`. + +## 3. Actions / API surface + +### Canonical CLI verbs (`hermes_cli/kanban.py`) +`init, create, list, show, assign, reassign, edit, promote, schedule, diagnostics, link, unlink, claim, comment, complete, block, unblock, archive, tail, watch, heartbeat, runs, assignees, dispatch, daemon, stats, log, notify-subscribe, notify-list, notify-unsubscribe, context, specify, decompose, swarm, gc`, plus `boards {list,create,rm,switch,show,rename,set-workdir}`. + +### Dashboard REST (representative) +`GET /board` · `GET/POST/PATCH/DELETE /tasks[/:id]` · `POST /tasks/bulk` · attachments CRUD · `POST /tasks/:id/comments` · `POST /tasks/:id/{specify,decompose}` · `GET/PATCH /profiles[/:name]` + `/describe-auto` · `GET/PUT /orchestration` · `POST/DELETE /links` · `POST /dispatch` · `GET /diagnostics` · `GET /workers/active` · `GET /runs/:id[/inspect]` · `POST /runs/:id/terminate` · `GET /config` · `WS /events`. + +> Note: the **reference desktop app** (`hermes-agent/apps/desktop`) has **no kanban board UI** — only `/kanban` as a slash-command string and a notifications comment. The rich UI reference is the **dashboard plugin**, not that app. + +## 4. What our hermes-desktop has today + +**Main (`src/main/kanban.ts`)** — execs `hermes kanban` (local) or SSH-tunnels (`sshRunKanban`); blocks plain-remote mode. Exposes: +`listBoards, currentBoard, switchBoard, createBoard, removeBoard, listTasks, getTask, createTask, assignTask, completeTask, blockTask, unblockTask, archiveTask, specifyTask, reclaimTask, commentTask, listClaw3dHqTasks, dispatchOnce`. + +**Renderer (`Kanban.tsx`)** — 6 columns `triage, todo, ready, running, blocked, done`; 6s poll; board switcher (+ read-only Claw3D HQ virtual board); create-task modal; create-board modal; read-only detail modal (body/summary/result/comments/events). Card actions: specify, mark-done, reclaim, unblock, block, archive. Drag-drop transitions: `→done`, `→blocked` (from todo/ready/running), `blocked→ready`. + +## 5. Gap analysis (reference → ours) + +### A. Status coverage +| Status | Canonical | Our columns | Gap | +|---|---|---|---| +| triage,todo,ready,running,blocked,done | ✓ | ✓ | — | +| **scheduled** | ✓ | ✗ | **Missing column** — `scheduled` tasks fall through to `todo` bucket (`Kanban.tsx:322`). | +| **review** | ✓ | ✗ | **Missing column** — `review` tasks mis-bucket to `todo`. | +| archived | ✓ (toggle) | ✗ | No "show archived" toggle in UI (main supports `includeArchived`). | + +### B. Actions defined in main but NOT surfaced in UI +- `assignTask` / reassign — no reassign control in detail modal. +- `commentTask` — detail modal shows comments **read-only**; no compose box. +- `removeBoard` — no delete-board affordance. + +### C. Canonical actions NOT wired at all (no main fn, no UI) +- **`decompose`** — the headline triage flow (fan-out to child graph). We only have `specify`. +- **`promote`** (todo/blocked→ready recovery), **`schedule`** (`scheduled_at`), **`edit`** (title/body/priority in place — we can create but not edit), **`link`/`unlink`** (dependency editing), **`diagnostics`**, **`runs`** (attempt history — `KanbanRun` typed but not fetched standalone), **`assignees`/`stats`**, **`notify-*`**, **`swarm`**, **`gc`**, **`tail`/`watch`**, **`heartbeat`**, **boards `rename`/`set-workdir`**, **attachments** (upload/list/download/delete). + +### D. UX / behavior gaps vs dashboard plugin +- **Polling vs live** — we poll every 6s; plugin tails `task_events` over WebSocket (instant, debounced). No WS bridge in our main. +- **No dependency / progress UI** — no parent/child chips, no `N/M` progress pill, no link editor. +- **No diagnostics surfacing** — no distress badges (hallucination/crash/stuck-blocked) the plugin renders. +- **No bulk multi-select** actions. +- **No orchestration controls** — Auto/Manual decompose pill, profile-description editor, orchestrator settings. +- **Transition rules are re-implemented client-side** (`isValidDragTransition`) and narrower than the backend — risk of drift; e.g. no `→scheduled`, no `→review`, no `→ready` from `todo` via drag. +- **Detail modal is read-only** — can't edit title/body/priority/assignee or add comments/links inline. + +## 6. Recommended plan (phased) + +### Phase 1 — Correctness (low effort, high value) +1. Add `scheduled` + `review` to `COLUMNS` and `en/kanban.ts` `status.*` (and other locales). Fixes silent mis-bucketing. +2. Add a **"show archived"** toggle (main already supports `includeArchived`). +3. Wire **comment compose** in the detail modal (`commentTask` already in main). +4. Wire **reassign** in the detail modal (`assignTask` already in main). + +### Phase 2 — Parity actions +5. Add main fns + UI for **`decompose`**, **`edit`** (title/body/priority), **`promote`**, **`schedule`** (`--at`), **`link`/`unlink`** (dependency editor with parent/child chips + progress pill). +6. Add **`runs`/diagnostics`** read views in the detail drawer (attempt history, distress badges). + +### Phase 3 — Live + richer UX +7. Replace 6s poll with a **`task_events` tail** (SSH `kanban watch --json` stream, or local tail) for instant updates. +8. **Bulk multi-select** actions; **orchestration** Auto/Manual + profile descriptions; **attachments**. + +### Cross-cutting +- Prefer routing transitions through backend verbs rather than widening the client-side `isValidDragTransition`, to avoid drift from `kanban_db` rules. +- Keep SSH-tunnel + remote-unsupported guards on every new verb (existing pattern in `kanban.ts`). +- Each new IPC verb needs: `kanban.ts` fn → `ipc/register.ts` → `preload/index.ts` (+ `.d.ts`) → renderer. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5f9f00a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 github.com/fathah + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PROFILE_MODAL_HANDOFF.md b/PROFILE_MODAL_HANDOFF.md new file mode 100644 index 0000000..b12925a --- /dev/null +++ b/PROFILE_MODAL_HANDOFF.md @@ -0,0 +1,102 @@ +# Profile Modal — Handoff + +A global, reusable profile detail/settings modal for Hermes Desktop (Electron + React renderer). Built on branch `pr/746`. This doc is a self-contained handoff so another agent can continue the work. + +## What it is + +A single **80vw × 80vh** modal, mounted once at the app root, opened from anywhere via a context hook. It has a **left section-nav** (Profile / Wallet / Advanced) and a scrollable right content pane. It replaces the old inline "appearance" modal that used to live inside the Agents screen. + +It was built to grow — the user has "more plans with profile" (e.g. a real Wallet screen). + +## Files + +| File | Role | +| --- | --- | +| `src/renderer/src/components/profile/ProfileModal.tsx` | The modal UI (header, left nav, panes, mutations). | +| `src/renderer/src/components/profile/ProfileModalProvider.tsx` | Mounts the modal at app root; holds open state. | +| `src/renderer/src/components/profile/ProfileModalContext.ts` | Context + `useProfileModal()` hook + `OpenProfileOptions` type. | +| `src/renderer/src/App.tsx` | `` wraps the app (inside `FontProvider`). | +| `src/renderer/src/screens/Layout/ProfileSwitcher.tsx` | Sidebar popover; active profile button calls `openProfile`. | +| `src/renderer/src/screens/Agents/Agents.tsx` | Profiles screen; pencil/card edit calls `openProfile` (inline modal removed). | +| `src/renderer/src/assets/main.css` | All `.profile-modal-*` styles (search that prefix). | +| `src/renderer/src/assets/icons/index.tsx` | Re-exports lucide icons; added `User`, `Wallet`. | +| `src/shared/i18n/locales/en/agents.ts` | i18n keys (English source; see i18n note). | +| `lat.md/sidebar-navigation.md` | Architecture doc, section "Profile detail modal" (keep in sync). | + +## How to open it + +```ts +import { useProfileModal } from "../../components/profile/ProfileModalContext"; + +const { openProfile } = useProfileModal(); + +openProfile("fatha", { + onChanged: reloadMyList, // called after every successful mutation + onDeleted: (name) => { /* e.g. fall back to "default" if it was active */ }, +}); +``` + +The modal **self-loads** its data via `window.hermesAPI.listProfiles()` (there is no single-profile IPC) and re-reads after every mutation, so callers only need `onChanged`/`onDeleted` to refresh their own lists. + +## Current structure + +**Header:** small `ProfileAvatar` (28px) + the profile **name only** (no "Edit {name}") + close (X). + +**Left nav** (`PROFILE_SECTIONS` in `ProfileModal.tsx`), each item = icon + label: +- **Profile** (`User` icon) — large avatar + gateway dot, name + `default` tag, Upload/Remove image, the provider/model/skills/gateway chips, and the color swatches. +- **Wallet** (`Wallet` icon) — **placeholder only**: centered "Coming soon" (`.profile-modal-coming-soon`). Not yet implemented. +- **Advanced** (`Settings` icon) — the Delete Profile danger zone. + +**Behaviors:** +- Every profile is editable (avatar + color), **including `default`**. +- Only `default` **cannot be deleted** — its Advanced pane shows `agents.defaultNotDeletable` instead of the delete button. +- Dismiss: overlay click, Escape, close (X), or Done button. + +## Profile data shape (`listProfiles()`) + +```ts +{ name, path, isDefault, isActive, model, provider, hasEnv, hasSoul, + skillCount, gatewayRunning, color?, avatar? } +``` + +## IPC available on `window.hermesAPI` (no new IPC was added) + +- `listProfiles()` → `ProfileInfo[]` +- `setProfileColor(name, color)` → `{ success, error? }` +- `setProfileAvatar(name, dataUrl)` → `{ success, error? }` +- `removeProfileAvatar(name)` → `{ success, error? }` +- `deleteProfile(name)` → `{ success, error? }` +- `createProfile(name, clone)`, `setActiveProfile(name)` (used elsewhere) + +Avatar files are converted with `fileToAvatarDataUrl` (`src/renderer/src/utils/imageResize.ts`). Colors come from `PROFILE_COLORS` (`src/shared/profileColors.ts`). + +## How to add a new section (e.g. build out Wallet) + +1. Add the id to the `ProfileSection` union and an entry to `PROFILE_SECTIONS` (`{ id, labelKey, Icon }`) in `ProfileModal.tsx`. +2. Add a `{section === "" && (
)}` block in the content area. +3. Add the nav label key to `src/shared/i18n/locales/en/agents.ts` (e.g. `sectionWallet`). +4. Style with existing `.profile-modal-*` classes or add new ones in `main.css`. + +## Project conventions (important) + +- **i18n**: source/fallback locale is `en`. New UI strings go in `src/shared/i18n/locales/en/agents.ts`; the other 10 locales (`es, he, id, ja, pl, pt-BR, pt-PT, tr, zh-CN, zh-TW`) **fall back to en automatically** (`FALLBACK_LOCALE` in `src/shared/i18n/index.ts`), so you don't have to edit them to ship — translate later. +- **lat.md sync (required)**: this repo uses [lat.md]. After any code change, update the relevant section in `lat.md/` and run `lat check` (a Stop hook enforces this). The profile modal is documented in `lat.md/sidebar-navigation.md` → "Profile detail modal". Wiki links like `[[src/.../ProfileModal.tsx#ProfileModal]]` must resolve. +- **Node for tooling**: the repo's `.nvmrc` pins Node 21, but `vitest`/typecheck need **Node ≥22** (`nvm use 22.19.0`) — Node 21 fails to load `vitest.config.ts` (`ERR_REQUIRE_ESM`). +- **react-refresh**: keep hooks/context in `.ts` files separate from the provider component (`.tsx`) — that's why context/hook and provider are split. + +## Verify + +```bash +nvm use 22.19.0 +npm run typecheck:web +npx eslint src/renderer/src/components/profile/*.ts src/renderer/src/components/profile/*.tsx +lat check +``` + +Manual (`npm run dev`, Node ≥22): sidebar profile popover → click the active profile → modal opens; switch nav sections; edit color/avatar (reflects live in sidebar + Agents); Wallet shows "Coming soon"; Advanced deletes non-default profiles (default shows the not-deletable note). + +## Known open items / ideas + +- **Wallet** is a stub — needs the real screen + likely new IPC for wallet/balance. +- Advanced currently holds only Delete; the user mentioned "wallet etc. in advanced … add later," so more settings can live there. +- No automated tests yet for `ProfileModal` (the dashboard adapter has tests as a pattern to follow under `src/renderer/src/screens/Chat/dashboardEventAdapter.test.ts`). diff --git a/README.es-LATAM.md b/README.es-LATAM.md new file mode 100644 index 0000000..d07b69d --- /dev/null +++ b/README.es-LATAM.md @@ -0,0 +1,350 @@ +HERMES DESKTOP + +
+

+ Twitter/X + Discord + Licencia: MIT + Releases + + Estrellas + + + Descargas + + + Token $HD + +

+ +

+ English · + 简体中文 · + 日本語 · + Español (LATAM) +

+ +

+ + + + + Star History Rank + + +

+ +> **Este proyecto está en desarrollo activo.** Las funciones pueden cambiar y algunas cosas podrían no funcionar perfectamente. Si encuentras un problema o tienes una idea, [abre un issue](https://github.com/fathah/hermes-desktop/issues). ¡Las contribuciones son bienvenidas! + +Hermes Desktop es una aplicación nativa de escritorio para instalar, configurar y chatear con [Hermes Agent](https://github.com/NousResearch/hermes-agent) — un asistente de IA con autoaprendizaje, uso de herramientas, mensajería multiplataforma y un ciclo de aprendizaje cerrado. + +En lugar de manejar el CLI a mano, la app guía todo el proceso de instalación, configuración de proveedores y uso diario en un solo lugar. Usa el script oficial de instalación de Hermes, guarda los archivos en `~/.hermes` y te da una GUI para chat, sesiones, perfiles, memoria, habilidades, herramientas, tareas programadas, gateways de mensajería y más. + +## Patrocinadores + + + Atlas Cloud + + +> **[Atlas Cloud](https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=hermes-desktop)** es una plataforma de inferencia de IA full-modal compatible con OpenAI (DeepSeek, Qwen, GLM, Kimi, MiniMax y más). Úsala en Hermes Desktop seleccionando **Atlas Cloud** como tu proveedor — la URL base se configura automáticamente. + +## Instalación + +Descargar ahora + +### Windows + +> **Usuarios de Windows:** El instalador no tiene firma de código. Windows SmartScreen mostrará una advertencia al primer lanzamiento — haz clic en "Más información" → "Ejecutar de todas formas". + +> **Usuarios de WSL:** Si el instalador se queda colgado en `Switching to root user to install dependencies...`, Playwright está esperando una contraseña de sudo que no tiene TTY para leerla. Otorga sudo sin contraseña para la instalación y reviértelo al terminar: +> +> ```bash +> echo "$USER ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/hermes-install +> # …vuelve a ejecutar el instalador; cuando termine: +> sudo rm /etc/sudoers.d/hermes-install +> ``` +> +> Seguimiento en [#109](https://github.com/fathah/hermes-desktop/issues/109). + +### Fedora (RPM) + +```bash +sudo dnf install ./hermes-desktop-.rpm +``` + +> **Usuarios de Fedora:** El `.rpm` no tiene firma GPG. Si tu sistema exige verificación de firma, agrega `--nogpgcheck` al comando de instalación. La actualización automática no está disponible para builds `.rpm` (limitación de `electron-updater`); reinstala el nuevo `.rpm` para actualizar. + +## Vista previa + + + + + + + + + + + + + + + + + + + + + + + + + + +
Chat
Chat
Perfiles
Profiles
Modelos
Models
Proveedores
Providers
Herramientas
Tools
Habilidades
Skills
Tareas programadas
Schedules
Gateway
Gateway
Persona
Persona
Kanban
Kanban
Oficina
Office
Configuración
Settings
+ +## Funcionalidades + +- **Instalación guiada en el primer uso** de Hermes Agent con seguimiento de progreso y resolución de dependencias +- **Backend local o remoto** — ejecuta Hermes localmente en `127.0.0.1:8642`, o conecta la app a un servidor remoto con URL + clave API +- **Soporte multi-proveedor** — OpenRouter, Anthropic, OpenAI, Google (Gemini), xAI (Grok), Nous Portal, Qwen, MiniMax, Hugging Face, Groq y endpoints compatibles con OpenAI (LM Studio, Atomic Chat, Ollama, vLLM, llama.cpp) +- **UI de chat con streaming** con SSE, indicadores de progreso de herramientas, renderizado de Markdown y resaltado de sintaxis +- **Seguimiento de tokens** — conteo en tiempo real de tokens de entrada/salida y costo en el pie del chat, más el comando `/usage` +- **22 comandos slash** — `/new`, `/clear`, `/fast`, `/web`, `/image`, `/browse`, `/code`, `/shell`, `/usage`, `/help`, `/tools`, `/skills`, `/model`, `/memory`, `/persona`, `/version`, `/compact`, `/compress`, `/undo`, `/retry`, `/debug`, `/status` y más +- **Gestión de sesiones** — búsqueda de texto completo (SQLite FTS5), historial agrupado por fecha, reanudar y buscar conversaciones +- **Cambio de perfiles** — crea, elimina y cambia entre entornos Hermes con configuración aislada +- **14 conjuntos de herramientas** — web, navegador, terminal, archivos, ejecución de código, visión, generación de imágenes, TTS, habilidades, memoria, búsqueda de sesiones, clarificación, delegación, MoA y planificación de tareas +- **Sistema de memoria** — ver/editar entradas de memoria, perfil de usuario, seguimiento de capacidad y proveedores de memoria (Honcho, Hindsight, Mem0, RetainDB, Supermemory, ByteRover) +- **Editor de Persona** — edita y restablece el archivo SOUL.md de personalidad de tu agente +- **Modelos guardados** — gestión CRUD de configuraciones de modelos por proveedor +- **Tareas programadas** — constructor de cron jobs (minutos, cada hora, diario, semanal, cron personalizado) con 15 destinos de entrega +- **16 gateways de mensajería** — Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Mattermost, Email (IMAP/SMTP), SMS (Twilio/Vonage), iMessage (BlueBubbles), DingTalk, Feishu/Lark, WeCom, WeChat (iLink Bot), Webhooks, Home Assistant +- **Hermes Office (Claw3d)** — interfaz visual 3D con servidor de desarrollo y gestión de adaptadores +- **Backup, importar y diagnóstico** — respaldo/restauración completa y diagnóstico del sistema desde Configuración +- **Visor de logs** — visualiza logs de gateway y agente directamente desde la pantalla de Configuración +- **Actualizador automático** — verifica e instala actualizaciones vía electron-updater +- **Listo para i18n** — framework de internacionalización con localización en inglés para todas las pantallas, listo para traducciones de la comunidad +- **Suite de pruebas** — parser SSE, handlers IPC, superficie de API preload, utilidades del instalador y validación de constantes con Vitest + +## Cómo funciona + +Al primer lanzamiento, la app: + +1. Pregunta si deseas ejecutar Hermes **localmente** o conectarte a un **servidor remoto**. +2. **Modo local:** verifica si Hermes ya está instalado en `~/.hermes`; si no, ejecuta el instalador oficial con resolución de dependencias (Git, uv, Python 3.11+). +3. **Modo remoto:** solicita la URL de la API remota y la clave API, valida la conexión y omite la instalación local. +4. Solicita un proveedor de API o endpoint de modelo local. +5. Guarda la configuración del proveedor y las claves API en los archivos de configuración de Hermes. +6. Lanza el workspace principal una vez completada la configuración. + +En modo local, las solicitudes de chat van por `http://127.0.0.1:8642` con streaming SSE. En modo remoto, la app se comunica con tu URL remota configurada con el mismo protocolo de streaming. La app parsea el stream en tiempo real, renderizando progreso de herramientas, contenido Markdown y uso de tokens a medida que llegan. + +## Pantallas + +| Pantalla | Descripción | +| ----------------- | -------------------------------------------------------------------------------------------------- | +| **Chat** | UI de conversación con streaming, comandos slash, progreso de herramientas y seguimiento de tokens | +| **Sesiones** | Navega, busca y reanuda conversaciones pasadas | +| **Agentes** | Crea, elimina y cambia entre perfiles de Hermes | +| **Habilidades** | Navega, instala y gestiona habilidades incluidas e instaladas | +| **Modelos** | Gestiona configuraciones de modelos guardadas por proveedor | +| **Memoria** | Ver/editar entradas de memoria, perfil de usuario y configurar proveedores de memoria | +| **Soul** | Edita la persona del perfil activo (SOUL.md) | +| **Herramientas** | Activa o desactiva conjuntos de herramientas individuales | +| **Programadas** | Crea y gestiona cron jobs con destinos de entrega | +| **Gateway** | Configura y controla integraciones de plataformas de mensajería | +| **Oficina** | Configuración y gestión de la interfaz visual Claw3d | +| **Configuración** | Config de proveedor, pools de credenciales, backup/importar, visor de logs, red, tema | + +## Proveedores soportados + +### Patrocinadores + +| Proveedor | Notas | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Atlas Cloud** | Gateway compatible con OpenAI — DeepSeek, Qwen, GLM, Kimi, MiniMax y más ([atlascloud.ai](https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=hermes-desktop)) | + +### Proveedores de LLM + +| Proveedor | Notas | +| ------------------- | ---------------------------------------- | +| **OpenRouter** | 200+ modelos vía API única (recomendado) | +| **Anthropic** | Acceso directo a Claude | +| **OpenAI** | Acceso directo a GPT | +| **Google (Gemini)** | Google AI Studio | +| **xAI (Grok)** | Modelos Grok | +| **Nous Portal** | Capa gratuita disponible | +| **Qwen** | Modelos QwenAI | +| **MiniMax** | Endpoints globales y de China | +| **Hugging Face** | 20+ modelos abiertos vía HF Inference | +| **Groq** | Inferencia rápida (voz/STT) | +| **Local/Custom** | Cualquier endpoint compatible con OpenAI | + +Los presets locales incluyen LM Studio, Atomic Chat, Ollama, vLLM y llama.cpp. + +### Plataformas de mensajería + +Telegram, Discord, Slack, WhatsApp, Signal, Matrix/Element, Mattermost, Email (IMAP/SMTP), SMS (Twilio y Vonage), iMessage (BlueBubbles), DingTalk, Feishu/Lark, WeCom, WeChat (iLink Bot), Webhooks y Home Assistant. + +### Integraciones de herramientas + +Exa Search, Parallel API, Tavily, Firecrawl, FAL.ai (generación de imágenes), Honcho, Browserbase, Weights & Biases y Tinker. + +## Desarrollo + +### Requisitos previos + +- Node.js y npm +- Un entorno tipo Unix para el instalador de Hermes +- Acceso a internet para descargar Hermes en la primera instalación + +### Instalar dependencias + +```bash +npm install +``` + +### Iniciar la app en desarrollo + +```bash +npm run dev +``` + +### Ejecutar verificaciones + +```bash +npm run lint +npm run typecheck +``` + +### Ejecutar pruebas + +```bash +npm run test +npm run test:watch +``` + +### Construir la app de escritorio + +```bash +npm run build +``` + +Empaquetado por plataforma: + +```bash +npm run build:mac +npm run build:win +npm run build:linux +npm run build:rpm # Solo .rpm para Fedora/RHEL +``` + +## Configuración inicial + +Cuando la app se abre por primera vez, detectará una instalación existente de Hermes o se ofrecerá a instalarla. + +Rutas de configuración soportadas en la UI: + +- `OpenRouter` +- `Anthropic` +- `OpenAI` +- `Local LLM` vía URL base compatible con OpenAI + +Los presets locales incluyen: + +- LM Studio +- Atomic Chat +- Ollama +- vLLM +- llama.cpp + +Los archivos de Hermes se gestionan en: + +- `~/.hermes` +- `~/.hermes/.env` +- `~/.hermes/config.yaml` +- `~/.hermes/hermes-agent` +- `~/.hermes/profiles/` — directorios de perfiles con nombre +- `~/.hermes/state.db` — base de datos del historial de sesiones +- `~/.hermes/cron/jobs.json` — tareas programadas + +## Proveedor de secretos + +Por defecto, las claves API viven en `~/.hermes/.env` (el proveedor **env**). No se necesita configuración — este es el comportamiento histórico y nada cambia para ti. + +Si prefieres no guardar claves en un `.env` en texto plano, el proveedor **command** (de activación opcional) las resuelve ejecutando un comando auxiliar que tú configuras. El orden de resolución en todos lados es: `process.env` → `.env` → proveedor → no definida. + +Helper por clave (el nombre de la clave solicitada llega como `$HERMES_SECRET_KEY`): + +```yaml +# ~/.hermes/config.yaml +secrets: + provider: command + command: secret-tool lookup hermes "$HERMES_SECRET_KEY" +``` + +O un helper que vuelca un bloque dotenv (por ejemplo, un vault que se descifra en tmpfs): + +```yaml +secrets: + provider: command + command: "cat /run/user/1000/hermes-secrets.env" +``` + +La salida del helper puede ser un valor único (helpers por clave) o líneas `KEY=VALUE` (volcados dotenv); ambos formatos se detectan automáticamente. + +### Integración con vault / gestor de secretos (sin TPM requerido) + +El proveedor `command` es **agnóstico al vault** — ejecuta el helper que configures y lee su stdout. El helper es lo único que necesita comunicarse con tu almacén de secretos. Si no tienes un keyfile sellado con TPM, cualquiera de estas opciones funciona sin cambios en el código de Hermes: + +- **KeePassXC (BD solo con contraseña, sin keyfile):** apunta `secrets.command` a un pequeño script `kpxc-export.sh` que hace `keepassxc-cli ls ~/secrets/hermes.kdbx <<<"$KPXC_PASSWORD"` y vuelca el grupo relevante como dotenv. Pide la contraseña maestra una vez por sesión. +- **GnuPG con clave solo de contraseña:** `gpg --batch --passphrase-fd 0 --decrypt ~/.keys/api-keys.gpg` funciona directamente como valor de `command`. Pasa la contraseña vía descriptor de archivo o variable de entorno, nunca como argumento. +- **`pass` (el gestor de contraseñas unix estándar):** `command: "pass show hermes/$HERMES_SECRET_KEY"` para helper por clave, o un script wrapper para volcado dotenv. +- **`secret-tool` (libsecret/Gnome Keyring):** `command: "secret-tool lookup hermes $HERMES_SECRET_KEY"` (ya mostrado arriba como ejemplo canónico por clave). +- **Bitwarden CLI:** `bw get item "$HERMES_SECRET_KEY" | jq -r .notes` (después de `bw unlock` en la sesión). +- **1Password CLI:** `op read "op://vault/$HERMES_SECRET_KEY/credential"`. +- **Archivo env plano con permisos gestionados:** `command: "cat ~/.config/hermes/secrets.env"` con `chmod 600` y el archivo propiedad de tu usuario. No tan seguro como un vault, pero mejor que un `.env` legible por todos. + +El punto: **cualquier helper que imprima un valor (por clave) o un bloque dotenv (modo lista) en stdout funcionará**, y Hermes impone un timeout de 3 segundos y un límite de 1 MiB de salida al helper. El proveedor no hace suposiciones sobre TPM, FIDO2, tarjetas inteligentes ni keychains de plataforma. + +Modelo de seguridad: + +- El string de comando es tu propia configuración — mismo nivel de confianza que `.env`. Se ejecuta vía `/bin/sh -c`, por lo que el proveedor command es solo POSIX (Linux/macOS); Windows se mantiene en el proveedor env. +- El helper hereda el entorno del proceso más `HERMES_SECRET_KEY`; el nombre de la clave se pasa como dato, nunca interpolado en el string del shell. +- Timeout fijo de 3 segundos, límite de 1 MiB de salida, y stderr se descarta. +- Los valores resueltos nunca se registran ni se escriben en disco; los fallos degradan a "clave no definida", registrando solo el código de salida/señal. +- El broadcast de inicio de gateway usa una única llamada `list()`, nunca un loop de helper por clave. + +Fuente de verdad: [`src/main/secrets/`](src/main/secrets/). + +## Stack tecnológico + +- **Electron** 39 — shell de escritorio multiplataforma +- **React** 19 — framework de UI +- **TypeScript** 5.9 — tipado seguro en procesos main y renderer +- **Tailwind CSS** 4 — estilos utility-first +- **Vite** 7 + electron-vite — servidor de desarrollo rápido y herramientas de build +- **better-sqlite3** — almacenamiento local de sesiones con búsqueda FTS5 +- **i18next** — framework de internacionalización +- **Vitest** — test runner + +## Notas + +- La app de escritorio depende del proyecto upstream Hermes Agent para el comportamiento del agente y la ejecución de herramientas. +- El instalador integrado ejecuta el script oficial de instalación de Hermes con `--skip-setup`, luego completa la configuración del proveedor en la GUI. +- Los proveedores de modelos locales no requieren clave API, pero el servidor compatible debe estar ejecutándose. +- Las rutas alternativas de registro npm son compatibles para entornos con acceso restringido a internet. + +## Contribuir + +¡Las contribuciones son bienvenidas! Consulta la [Guía de contribución](CONTRIBUTING.md) para comenzar. Si no sabes por dónde empezar, mira los [issues abiertos](https://github.com/fathah/hermes-desktop/issues). ¿Encontraste un bug o tienes una solicitud de funcionalidad? [Abre un issue](https://github.com/fathah/hermes-desktop/issues/new). + +## Proyecto relacionado + +Para el agente central, documentación y flujos de trabajo CLI, consulta el repositorio principal de Hermes Agent: + +- https://github.com/NousResearch/hermes-agent + +--- + +_Traducción al español LATAM por [Nanoboy](https://github.com/365diascollaboration-prog)._ diff --git a/README.ja-JP.md b/README.ja-JP.md new file mode 100644 index 0000000..b0a735d --- /dev/null +++ b/README.ja-JP.md @@ -0,0 +1,268 @@ +HERMES DESKTOP + +
+

+ Documentation + Telegram + License: MIT + Releases + + Stars + + + Downloads + +

+ +> **本プロジェクトは現在も活発に開発中です。** 機能は変更される可能性があり、一部が動作しなくなることもあります。問題に遭遇した場合や、アイデアがある場合は[Issue を作成してください](https://github.com/fathah/hermes-desktop/issues)。コントリビューションも歓迎しています! + +## 言語 + +- English: `README.md` +- 简体中文: `README.zh-CN.md` +- 日本語: `README.ja-JP.md` + +Hermes Desktop は、[Hermes Agent](https://github.com/NousResearch/hermes-agent)(ツール使用、マルチプラットフォームメッセージング、クローズドな学習ループを備えた、自己改善型 AI アシスタント)のインストール・設定・チャットを行うためのネイティブデスクトップアプリです。 + +CLI を手作業で管理する代わりに、本アプリではインストール、プロバイダのセットアップ、日常的な利用までを一箇所でガイドします。公式の Hermes インストールスクリプトを使用し、Hermes を `~/.hermes` に保存し、チャット、セッション、プロファイル、メモリ、スキル、ツール、スケジューリング、メッセージングゲートウェイなどを GUI で操作できます。 + +## インストール + +Download Now + +### Windows + +> **Windows ユーザーへ:** インストーラはコード署名されていません。初回起動時に Windows SmartScreen の警告が表示されます。「詳細情報」→「実行」をクリックしてください。 + +> **WSL ユーザーへ:** インストーラが `Switching to root user to install dependencies...` で停止する場合、Playwright が sudo パスワードを待っていますが、読み取るための TTY がありません。インストール中だけパスワードなしの sudo を許可し、完了後に元へ戻してください。 +> +> ```bash +> echo "$USER ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/hermes-install +> # …インストーラを再実行し、完了したら: +> sudo rm /etc/sudoers.d/hermes-install +> ``` +> +> [#109](https://github.com/fathah/hermes-desktop/issues/109) で追跡しています。 + +### Fedora (RPM) + +```bash +sudo dnf install ./hermes-desktop-.rpm +``` + +> **Fedora ユーザーへ:** `.rpm` は GPG 署名されていません。署名検証を強制する設定の場合は、インストールコマンドに `--nogpgcheck` を追加してください。`.rpm` ビルドは自動アップデートに対応していません(`electron-updater` の制約)。アップデートする場合は新しい `.rpm` を再インストールしてください。 + +## プレビュー + + + + + + + + + + + + + + + + + + + + + + + + + + +
Chat
Chat
Profiles
Profiles
Models
Models
Providers
Providers
Tools
Tools
Skills
Skills
Schedules
Schedules
Gateway
Gateway
Persona
Persona
Kanban
Kanban
Office
Office
Settings
Settings
+ +## 機能 + +- **初回起動時のガイド付きインストール** — Hermes Agent のインストールを進捗表示と依存関係解決付きで案内します +- **ローカル / リモートバックエンド** — Hermes をローカル (`127.0.0.1:8642`) で実行するか、URL + API キーを使ってリモートの Hermes API サーバーに接続できます +- **マルチプロバイダ対応** — OpenRouter, Anthropic, OpenAI, Google (Gemini), xAI (Grok), Nous Portal, Qwen, MiniMax, Hugging Face, Groq、そしてローカルの OpenAI 互換エンドポイント (LM Studio, Ollama, vLLM, llama.cpp) +- **ストリーミングチャット UI** — SSE ストリーミング、ツール進捗インジケータ、Markdown レンダリング、シンタックスハイライト対応 +- **トークン使用量のトラッキング** — プロンプト / 出力トークン数とコストをチャットフッターにリアルタイム表示。`/usage` スラッシュコマンドも利用可能 +- **22 種類のスラッシュコマンド** — `/new`, `/clear`, `/fast`, `/web`, `/image`, `/browse`, `/code`, `/shell`, `/usage`, `/help`, `/tools`, `/skills`, `/model`, `/memory`, `/persona`, `/version`, `/compact`, `/compress`, `/undo`, `/retry`, `/debug`, `/status` など +- **セッション管理** — 全文検索 (SQLite FTS5)、日付別の履歴表示、会話の再開と横断検索 +- **プロファイル切り替え** — Hermes 環境を分離した状態で作成・削除・切り替え可能 +- **14 のツールセット** — Web、ブラウザ、ターミナル、ファイル、コード実行、ビジョン、画像生成、TTS、スキル、メモリ、セッション検索、Clarify、Delegation、MoA、タスクプランニング +- **メモリシステム** — メモリエントリの閲覧 / 編集、ユーザープロファイルメモリ、容量トラッキング、検出可能なメモリプロバイダ (Honcho, Hindsight, Mem0, RetainDB, Supermemory, ByteRover) に対応 +- **ペルソナエディタ** — エージェントの SOUL.md パーソナリティを編集・リセット可能 +- **保存済みモデル** — プロバイダごとのモデル設定を CRUD で管理 +- **スケジュールタスク** — 分単位 / 時間単位 / 日単位 / 週単位 / カスタム cron に対応する cron ジョブビルダー(15 種類の配信先) +- **16 種類のメッセージングゲートウェイ** — Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Mattermost, Email (IMAP/SMTP), SMS (Twilio/Vonage), iMessage (BlueBubbles), DingTalk, Feishu/Lark, WeCom, WeChat (iLink Bot), Webhooks, Home Assistant +- **Hermes Office (Claw3d)** — ビジュアルな 3D インターフェース。開発サーバーとアダプタの管理機能を備える +- **バックアップ、インポート、デバッグダンプ** — 設定画面からデータの完全なバックアップ / リストアとシステム診断が可能 +- **ログビューア** — ゲートウェイとエージェントのログを設定画面から直接閲覧 +- **自動アップデーター** — electron-updater を使ったアップデートチェックとインストール +- **i18n 対応** — 全画面に対応する英語ロケールを含む国際化フレームワーク。コミュニティ翻訳の受け入れ準備済み +- **テストスイート** — SSE パーサ、IPC ハンドラ、preload API サーフェス、インストーラユーティリティ、定数バリデーションを Vitest で検証 + +## 動作の仕組み + +初回起動時、アプリは次の手順で動作します。 + +1. Hermes を**ローカル**で動かすか、**リモート**の Hermes API サーバーに接続するかを尋ねます。 +2. **ローカルモード:** `~/.hermes` に Hermes が既にインストールされているかを確認します。なければ、依存関係 (Git, uv, Python 3.11+) を解決しつつ公式インストーラを実行します。 +3. **リモートモード:** リモート API の URL と API キーを入力させ、接続を検証し、ローカルインストールをスキップします。 +4. API プロバイダまたはローカルモデルのエンドポイントを尋ねます。 +5. プロバイダ設定と API キーを Hermes の設定ファイルに保存します。 +6. セットアップが完了するとメインのワークスペースを起動します。 + +ローカルモードでは、チャットリクエストは `http://127.0.0.1:8642` 経由で SSE ストリーミングされます。リモートモードでは、設定したリモート URL に対して同じストリーミングプロトコルで通信します。デスクトップアプリはストリームをリアルタイムで解析し、ツール進捗、Markdown コンテンツ、トークン使用量を順次レンダリングします。 + +## 画面構成 + +| 画面 | 説明 | +| ------------- | --------------------------------------------------------------------------------------------- | +| **Chat** | ストリーミング会話 UI。スラッシュコマンド、ツール進捗、トークントラッキングに対応 | +| **Sessions** | 過去の会話の閲覧、検索、再開 | +| **Agents** | Hermes プロファイルの作成、削除、切り替え | +| **Skills** | バンドル済み / インストール済みスキルの閲覧、インストール、管理 | +| **Models** | プロバイダごとに保存されたモデル設定の管理 | +| **Memory** | メモリエントリとユーザープロファイルの閲覧 / 編集、メモリプロバイダの設定 | +| **Soul** | アクティブなプロファイルのペルソナ (SOUL.md) を編集 | +| **Tools** | 個別のツールセットを有効化 / 無効化 | +| **Schedules** | 配信先付きの cron ジョブを作成・管理 | +| **Gateway** | メッセージングプラットフォーム統合の設定と制御 | +| **Office** | Claw3d ビジュアルインターフェースのセットアップと管理 | +| **Settings** | プロバイダ設定、認証情報プール、バックアップ / インポート、ログビューア、ネットワーク、テーマ | + +## 対応プロバイダ + +### LLM プロバイダ + +| プロバイダ | 備考 | +| ------------------- | ---------------------------------------------- | +| **OpenRouter** | 単一 API で 200 以上のモデルを利用可能(推奨) | +| **Anthropic** | Claude に直接アクセス | +| **OpenAI** | GPT に直接アクセス | +| **Google (Gemini)** | Google AI Studio | +| **xAI (Grok)** | Grok モデル | +| **Nous Portal** | 無料枠あり | +| **Qwen** | QwenAI モデル | +| **MiniMax** | グローバル / 中国向けエンドポイント | +| **Hugging Face** | HF Inference 経由で 20 以上のオープンモデル | +| **Groq** | 高速推論 (Voice/STT) | +| **Local/Custom** | 任意の OpenAI 互換エンドポイント | + +LM Studio、Ollama、vLLM、llama.cpp 用のローカルプリセットが付属しています。 + +### メッセージングプラットフォーム + +Telegram、Discord、Slack、WhatsApp、Signal、Matrix/Element、Mattermost、Email (IMAP/SMTP)、SMS (Twilio & Vonage)、iMessage (BlueBubbles)、DingTalk、Feishu/Lark、WeCom、WeChat (iLink Bot)、Webhooks、Home Assistant。 + +### ツール統合 + +Exa Search、Parallel API、Tavily、Firecrawl、FAL.ai (画像生成)、Honcho、Browserbase、Weights & Biases、Tinker。 + +## 開発 + +### 前提条件 + +- Node.js と npm +- Hermes インストーラ用の Unix 系シェル環境 +- 初回起動時に Hermes をダウンロードするためのネットワークアクセス + +### 依存関係のインストール + +```bash +npm install +``` + +### 開発モードでアプリを起動 + +```bash +npm run dev +``` + +### チェック実行 + +```bash +npm run lint +npm run typecheck +``` + +### テスト実行 + +```bash +npm run test +npm run test:watch +``` + +### デスクトップアプリのビルド + +```bash +npm run build +``` + +プラットフォーム別パッケージング: + +```bash +npm run build:mac +npm run build:win +npm run build:linux +npm run build:rpm # Fedora/RHEL .rpm のみ +``` + +## 初回セットアップ + +アプリを初めて開くと、既存の Hermes インストールを検出するか、インストールを提案します。 + +UI でサポートされているセットアップパス: + +- `OpenRouter` +- `Anthropic` +- `OpenAI` +- OpenAI 互換 base URL を使った `Local LLM` + +以下のローカルプリセットが付属しています。 + +- LM Studio +- Ollama +- vLLM +- llama.cpp + +Hermes のファイルは以下の場所で管理されます。 + +- `~/.hermes` +- `~/.hermes/.env` +- `~/.hermes/config.yaml` +- `~/.hermes/hermes-agent` +- `~/.hermes/profiles/` — 名前付きプロファイルディレクトリ +- `~/.hermes/state.db` — セッション履歴データベース +- `~/.hermes/cron/jobs.json` — スケジュールタスク + +## 技術スタック + +- **Electron** 39 — クロスプラットフォームのデスクトップシェル +- **React** 19 — UI フレームワーク +- **TypeScript** 5.9 — main / renderer プロセス間で型安全性を確保 +- **Tailwind CSS** 4 — ユーティリティファーストのスタイリング +- **Vite** 7 + electron-vite — 高速な開発サーバーとビルドツール +- **better-sqlite3** — FTS5 全文検索付きのローカルセッションストレージ +- **i18next** — 国際化フレームワーク +- **Vitest** — テストランナー + +## 補足 + +- 本デスクトップアプリはエージェントの動作やツール実行を上流の Hermes Agent プロジェクトに依存しています。 +- 内蔵インストーラは公式の Hermes インストールスクリプトを `--skip-setup` 付きで実行し、その後 GUI でプロバイダ設定を完了します。 +- ローカルモデルプロバイダには API キーは不要ですが、互換サーバーが事前に起動している必要があります。 +- ネットワーク制限のある環境向けに、代替の npm レジストリ経路をサポートしています。 + +## コントリビューション + +コントリビューションを歓迎します!始め方は[コントリビューションガイド](CONTRIBUTING.md)をご覧ください。どこから手を付ければよいか分からない場合は、[Open Issues](https://github.com/fathah/hermes-desktop/issues) を確認してください。バグを見つけた、または機能要望がある場合は [Issue を作成してください](https://github.com/fathah/hermes-desktop/issues/new)。 + +## 関連プロジェクト + +コアエージェント、ドキュメント、CLI ワークフローについては、Hermes Agent 本体のリポジトリを参照してください。 + +- https://github.com/NousResearch/hermes-agent diff --git a/README.md b/README.md new file mode 100644 index 0000000..c82c47f --- /dev/null +++ b/README.md @@ -0,0 +1,350 @@ +HERMES DESKTOP + +
+

+ Twitter + Discord + License: MIT + Releases + + Stars + + + Downloads + + + Downloads + + +

+ +

+ English · + 简体中文 · + 日本語 · + Español (LATAM) +

+ +

+ + + + + + + + Star History Rank + + +

+ + +> **This project is in active development.** Features may change, and some things might break. If you run into a problem or have an idea, [open an issue](https://github.com/fathah/hermes-desktop/issues). Contributions are welcome! + +Hermes One is a community maintained native desktop app for installing, configuring, and chatting with [Hermes Agent](https://github.com/NousResearch/hermes-agent) — a self-improving AI assistant with tool use, multi-platform messaging, and a closed learning loop. + +Instead of managing the CLI by hand, the app walks through install, provider setup, and day-to-day usage in one place. It uses the official Hermes install script, stores Hermes in `~/.hermes`, and gives you a GUI for chat, sessions, profiles, memory, skills, tools, scheduling, messaging gateways, and more. + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/D1D41ZEKFO) + +## Sponsors + +> [Want to appear here?](mailto:fathah@hermesone.org) + +
+Click to collapse +
+ + + + + + + + + + + + +
Atlas Cloud Atlas Cloud is a full-modal, OpenAI-compatible AI inference platform. Use it in Hermes One by selecting Atlas Cloud as your provider. The base URL is pre-configured automatically.
Greptile Greptile is an AI code reviewer. It reviews and tests pull requests with full context of the codebase. It catches bugs, flags regressions, and leaves inline review comments on every PR automatically.
+ +
+ +## Install + +Download Now + +
+Windows +
+ +> **Windows users:** The installer is not code-signed. Windows SmartScreen will warn on first launch — click "More info" → "Run anyway". + +> **WSL users:** If the installer stalls at `Switching to root user to install dependencies...`, Playwright is waiting for a sudo password that has no TTY to read from. Grant passwordless sudo for the install, then revert when finished: +> +> ```bash +> echo "$USER ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/hermes-install +> # …re-run the installer; once it finishes: +> sudo rm /etc/sudoers.d/hermes-install +> ``` +> +> Tracked in [#109](https://github.com/fathah/hermes-desktop/issues/109). + +
+ +
+Fedora (RPM) +
+ +```bash +sudo dnf install ./hermes-desktop-.rpm +``` + +> **Fedora users:** The `.rpm` is not GPG-signed. If your system enforces signature checking, append `--nogpgcheck` to the install command. Auto-update is not supported for `.rpm` builds (limitation of `electron-updater`); reinstall the new `.rpm` to update. + +
+ +## Preview + + + + + + + + + + + + + + + + + + + + + + + + + + +
Chat
Chat
Profiles
Profiles
Models
Models
Providers
Providers
Tools
Tools
Discover
Skills
Schedules
Schedules
Gateway
Gateway
Persona
Persona
Kanban
Kanban
Office
Office
Settings
Settings
+ +## Features + +- **Guided first-run install** for Hermes Agent with progress tracking and dependency resolution +- **Local or remote backend** — run Hermes locally on `127.0.0.1:8642`, or connect the desktop app to a remote Hermes API server with URL + API key +- **Multi-provider support** — OpenRouter, Anthropic, OpenAI, Google (Gemini), xAI (Grok), Nous Portal, Qwen, MiniMax, Hugging Face, Groq, and local OpenAI-compatible endpoints (LM Studio, Atomic Chat, Ollama, vLLM, llama.cpp) +- **Streaming chat UI** with SSE streaming, tool progress indicators, markdown rendering, and syntax highlighting +- **Token usage tracking** — live prompt/completion token counts and cost display in the chat footer, plus a `/usage` slash command +- **22 slash commands** — `/new`, `/clear`, `/fast`, `/web`, `/image`, `/browse`, `/code`, `/shell`, `/usage`, `/help`, `/tools`, `/skills`, `/model`, `/memory`, `/persona`, `/version`, `/compact`, `/compress`, `/undo`, `/retry`, `/debug`, `/status`, and more +- **Session management** — full-text search (SQLite FTS5), date-grouped history, resume and search across conversations +- **Profile switching** — create, delete, and switch between separate Hermes environments with isolated config +- **14 toolsets** — web, browser, terminal, file, code execution, vision, image gen, TTS, skills, memory, session search, clarify, delegation, MoA, and task planning +- **Memory system** — view/edit memory entries, user profile memory, capacity tracking, and discoverable memory providers (Honcho, Hindsight, Mem0, RetainDB, Supermemory, ByteRover) +- **Persona editor** — edit and reset your agent's SOUL.md personality +- **Saved models** — CRUD management for model configurations across providers +- **Scheduled tasks** — cron job builder (minutes, hourly, daily, weekly, custom cron) with 15 delivery targets +- **16 messaging gateways** — Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Mattermost, Email (IMAP/SMTP), SMS (Twilio/Vonage), iMessage (BlueBubbles), DingTalk, Feishu/Lark, WeCom, WeChat (iLink Bot), Webhooks, Home Assistant +- **Hermes Office (Claw3d)** — visual 3D interface with dev server and adapter management +- **Backup, import & debug dump** — full data backup/restore and system diagnostics from Settings +- **Log viewer** — view gateway and agent logs directly from the Settings screen +- **Auto-updater** — check for and install updates via electron-updater +- **i18n ready** — internationalization framework with English locale covering all screens, ready for community translations +- **Test suite** — SSE parser, IPC handlers, preload API surface, installer utilities, and constants validation with Vitest + +## How It Works + +On first launch, the app: + +1. Asks whether you want to run Hermes **locally** or connect to a **remote** Hermes API server. +2. **Local mode:** checks whether Hermes is already installed in `~/.hermes`; if not, runs the official Hermes installer with dependency resolution (Git, uv, Python 3.11+). +3. **Remote mode:** prompts for the remote API URL and API key, validates the connection, and skips local install. +4. Prompts for an API provider or local model endpoint. +5. Saves provider config and API keys through Hermes config files. +6. Launches the main workspace once setup is complete. + +In local mode, chat requests go through `http://127.0.0.1:8642` with SSE streaming. In remote mode, the app talks to your configured remote URL with the same streaming protocol. The desktop app parses the stream in real time, rendering tool progress, markdown content, and token usage as it arrives. + +## Screens + +| Screen | Description | +| ------------- | ------------------------------------------------------------------------------------- | +| **Chat** | Streaming conversation UI with slash commands, tool progress, and token tracking | +| **Sessions** | Browse, search, and resume past conversations | +| **Agents** | Create, delete, and switch between Hermes profiles | +| **Skills** | Browse, install, and manage bundled and installed skills | +| **Models** | Manage saved model configurations per provider | +| **Memory** | View/edit memory entries, user profile, and configure memory providers | +| **Soul** | Edit the active profile's persona (SOUL.md) | +| **Tools** | Enable or disable individual toolsets | +| **Schedules** | Create and manage cron jobs with delivery targets | +| **Gateway** | Configure and control messaging platform integrations | +| **Office** | Claw3d visual interface setup and management | +| **Settings** | Provider config, credential pools, backup/import, log viewer, network settings, theme | + +## Supported Providers + +### Sponsors + +| Provider | Notes | +| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Atlas Cloud** | OpenAI-compatible gateway — DeepSeek, Qwen, GLM, Kimi, MiniMax and more ([atlascloud.ai](https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=hermes-desktop)) | + +### LLM Providers + +| Provider | Notes | +| ------------------- | ---------------------------------------- | +| **OpenRouter** | 200+ models via single API (recommended) | +| **Anthropic** | Direct Claude access | +| **OpenAI** | Direct GPT access | +| **Google (Gemini)** | Google AI Studio | +| **xAI (Grok)** | Grok models | +| **Nous Portal** | Free tier available | +| **Qwen** | QwenAI models | +| **MiniMax** | Global and China endpoints | +| **Hugging Face** | 20+ open models via HF Inference | +| **Groq** | Fast inference (voice/STT) | +| **Local/Custom** | Any OpenAI-compatible endpoint | + +Local presets are included for LM Studio, Atomic Chat, Ollama, vLLM, and llama.cpp. + +### Messaging Platforms + +Telegram, Discord, Slack, WhatsApp, Signal, Matrix/Element, Mattermost, Email (IMAP/SMTP), SMS (Twilio & Vonage), iMessage (BlueBubbles), DingTalk, Feishu/Lark, WeCom, WeChat (iLink Bot), Webhooks, and Home Assistant. + +### Tool Integrations + +Exa Search, Parallel API, Tavily, Firecrawl, FAL.ai (image generation), Honcho, Browserbase, Weights & Biases, and Tinker. + +## First-Time Setup + +When the app opens for the first time, it will either detect an existing Hermes installation or offer to install it for you. + +Supported setup paths in the UI: + +- `OpenRouter` +- `Anthropic` +- `OpenAI` +- `Local LLM` via an OpenAI-compatible base URL + +Local presets are included for: + +- LM Studio +- Atomic Chat +- Ollama +- vLLM +- llama.cpp + +Hermes files are managed in: + +- `~/.hermes` +- `~/.hermes/.env` +- `~/.hermes/config.yaml` +- `~/.hermes/hermes-agent` +- `~/.hermes/profiles/` — named profile directories +- `~/.hermes/state.db` — session history database +- `~/.hermes/cron/jobs.json` — scheduled tasks + +## Secrets provider + +By default, API keys live in `~/.hermes/.env` (the **env** provider). No +configuration is needed — this is byte-for-byte the historical behavior, and +nothing changes for you. + +If you'd rather not keep keys in a plaintext `.env`, the opt-in **command** +provider resolves them by running a helper command you configure. Resolution +order everywhere is: `process.env` → `.env` → provider → unset. + +Per-key helper (the requested key name arrives as `$HERMES_SECRET_KEY`): + +```yaml +# ~/.hermes/config.yaml +secrets: + provider: command + command: secret-tool lookup hermes "$HERMES_SECRET_KEY" +``` + +Or a helper that dumps a dotenv blob (e.g. a vault that unseals into tmpfs): + +```yaml +secrets: + provider: command + command: "cat /run/user/1000/hermes-secrets.env" +``` + +The helper's stdout may be either a single bare value (per-key helpers) or +`KEY=VALUE` lines (dotenv dumps); both shapes are auto-detected. + +### Vault / secret manager integration (no TPM required) + +The `command` provider is **vault-agnostic** — it runs whatever helper you +configure and reads its stdout. The helper is the only thing that needs to +talk to your secret store. If you don't have a TPM-sealed keyfile, any of +these work without code changes to Hermes: + +- **KeePassXC (password-only DB, no keyfile):** point `secrets.command` at a + small `kpxc-export.sh` script that does + `keepassxc-cli ls ~/secrets/hermes.kdbx <<<"$KPXC_PASSWORD"` and dumps + the relevant group as dotenv. Prompt the user for the master password + once per session. +- **GnuPG with a passphrase-only key:** `gpg --batch --passphrase-fd 0 +--decrypt ~/.keys/api-keys.gpg` works directly as the `command` value. + Pass the passphrase via a file descriptor or env var, never argv. +- **`pass` (the standard unix password manager):** + `command: "pass show hermes/$HERMES_SECRET_KEY"` for a per-key helper, + or a small wrapper script for a dotenv dump. +- **`secret-tool` (libsecret/Gnome Keyring):** + `command: "secret-tool lookup hermes $HERMES_SECRET_KEY"` (already + shown above as the canonical per-key example). +- **Bitwarden CLI:** `bw get item "$HERMES_SECRET_KEY" | jq -r .notes` + (after `bw unlock` in the session). +- **1Password CLI:** `op read "op://vault/$HERMES_SECRET_KEY/credential"`. +- **Plain env file with user-managed permissions:** + `command: "cat ~/.config/hermes/secrets.env"` with `chmod 600` and + the file owned by your user. Not as secure as a vault, but better than + a world-readable `.env`. + +The point: **any helper that prints a value (per-key) or a dotenv blob +(list-mode) on stdout will work**, and Hermes imposes a 3-second timeout +and 1 MiB output cap on the helper so a misbehaving one can't wedge the +app. The provider makes no assumptions about TPM, FIDO2, smart cards, +or platform keychains. + +Security model: + +- The command string is your own configuration — same trust level as `.env`. + It runs via `/bin/sh -c`, so the command provider is POSIX-only + (Linux/macOS); Windows stays on the env provider. +- The helper inherits the process environment plus `HERMES_SECRET_KEY`; the + key name is passed as data, never interpolated into the shell string. +- Hard 3-second timeout (resolution is synchronous on the main process — keep + helpers fast and non-interactive), 1 MiB output cap, and stderr is discarded. +- Resolved values are never logged or written to disk; failures degrade to + "key unset", logging only exit code/signal. +- The gateway-spawn broadcast uses a single `list()` call, never a per-key + helper loop. + +Source of truth: [`src/main/secrets/`](src/main/secrets/). + +## Notes + +- The desktop app depends on the upstream Hermes Agent project for agent behavior and tool execution. +- The built-in installer runs the official Hermes install script with `--skip-setup`, then completes provider configuration in the GUI. +- Local model providers do not require an API key, but the compatible server must already be running. +- Alternative npm registry routes are supported for environments with restricted network access. + +## Contributing + +Contributions are welcome! Check out the [Contributing Guide](CONTRIBUTING.md) to get started. If you're not sure where to begin, take a look at the [open issues](https://github.com/fathah/hermes-desktop/issues). Found a bug or have a feature request? [File an issue](https://github.com/fathah/hermes-desktop/issues/new). + +## Related Project + +This repo is not affiliated to **Nous Research**. This is a community maintained project. + +For the core agent, docs, and CLI workflows, see the main Hermes Agent repository: + +- https://github.com/NousResearch/hermes-agent diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..26c11b5 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`fathah/hermes-desktop` +- 原始仓库:https://github.com/fathah/hermes-desktop +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..9256f8f --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,268 @@ +HERMES DESKTOP + +
+

+ 文档 + Telegram + License: MIT + 下载 + + Stars + + + 下载量 + +

+ +> **本项目处于活跃开发阶段。** 功能可能会发生变化,某些功能也可能会失效。如果您遇到问题或有好的想法,请 [提交 Issue](https://github.com/fathah/hermes-desktop/issues)。欢迎贡献代码! + +## 语言 + +- English: `README.md` +- 简体中文: `README.zh-CN.md` +- 日本語: `README.ja-JP.md` + +Hermes Desktop 是一款原生的桌面应用程序,用于安装、配置并与 [Hermes Agent](https://github.com/NousResearch/hermes-agent) 进行聊天——这是一款具备工具调用、多平台消息传递和闭环学习能力的自我进化的 AI 助手。 + +无需手动管理命令行界面 (CLI),该应用可在一个统一界面中引导您完成安装、提供商设置以及日常使用。它使用官方的 Hermes 安装脚本,将 Hermes 存储在 `~/.hermes` 目录下,并为您提供涵盖聊天、会话、配置、记忆、技能、工具、计划任务、消息网关等功能的图形界面。 + +## 安装 + +Download Now + +### Windows + +> **Windows 用户注意:** 安装程序未进行代码签名。首次启动时 Windows SmartScreen 会弹出警告——请点击“更多信息” → “仍要运行”。 + +> **WSL 用户注意:** 如果安装程序停滞在 `Switching to root user to install dependencies...`,这说明 Playwright 正在等待输入 sudo 密码,但在没有 TTY 的情况下无法读取。请在安装期间授予无密码的 sudo 权限,完成后再恢复: +> +> ```bash +> echo "$USER ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/hermes-install +> # …重新运行安装程序;完成后执行: +> sudo rm /etc/sudoers.d/hermes-install +> ``` +> +> 详情请见 [#109](https://github.com/fathah/hermes-desktop/issues/109)。 + +### Fedora (RPM) + +```bash +sudo dnf install ./hermes-desktop-.rpm +``` + +> **Fedora 用户注意:** `.rpm` 包没有 GPG 签名。如果您的系统强制检查签名,请在安装命令后添加 `--nogpgcheck`。`.rpm` 构建不支持自动更新(这是 `electron-updater` 的限制);若要更新,请重新安装新的 `.rpm` 包。 + +## 预览 + + + + + + + + + + + + + + + + + + + + + + + + + + +
聊天 (Chat)
Chat
配置 (Profiles)
Profiles
模型 (Models)
Models
提供商 (Providers)
Providers
工具 (Tools)
Tools
技能 (Skills)
Skills
计划任务 (Schedules)
Schedules
网关 (Gateway)
Gateway
人格 (Persona)
Persona
看板 (Kanban)
Kanban
办公室 (Office)
Office
设置 (Settings)
Settings
+ +## 功能特性 + +- **向导式初次安装**:带有进度跟踪和依赖解析的 Hermes Agent 引导安装。 +- **本地或远程后端**:可在本地 `127.0.0.1:8642` 运行 Hermes,或通过 URL 和 API 密钥将桌面应用连接到远程的 Hermes API 服务器。 +- **多提供商支持**:OpenRouter, Anthropic, OpenAI, Google (Gemini), xAI (Grok), Nous Portal, Qwen, MiniMax, Hugging Face, Groq, 以及本地兼容 OpenAI 格式的端点 (LM Studio, Ollama, vLLM, llama.cpp)。 +- **流式聊天界面**:具有 SSE 流式传输、工具进度指示、Markdown 渲染和语法高亮。 +- **Token 使用情况追踪**:在聊天底部显示实时的 Prompt/补全 Token 计数及预估费用,并可通过 `/usage` 斜杠命令查看。 +- **22 个斜杠命令**:`/new`, `/clear`, `/fast`, `/web`, `/image`, `/browse`, `/code`, `/shell`, `/usage`, `/help`, `/tools`, `/skills`, `/model`, `/memory`, `/persona`, `/version`, `/compact`, `/compress`, `/undo`, `/retry`, `/debug`, `/status` 等等。 +- **会话管理**:全文检索 (SQLite FTS5)、按日期分组的历史记录、在会话之间继续聊天或搜索。 +- **配置切换 (Profile)**:创建、删除并切换不同的 Hermes 环境,配置完全隔离。 +- **14 种工具集**:网络、浏览器、终端、文件、代码执行、视觉识别、图像生成、语音合成 (TTS)、技能、记忆、会话搜索、澄清询问、委托调度、混合专家模型 (MoA) 和任务规划。 +- **记忆系统**:查看/编辑记忆条目和用户资料记忆,追踪容量,并发现不同的记忆提供商(如 Honcho, Hindsight, Mem0, RetainDB, Supermemory, ByteRover)。 +- **人格编辑器**:编辑并重置您的代理 (Agent) 的 `SOUL.md` 人格设定。 +- **已保存模型**:跨不同提供商对模型配置进行增删改查。 +- **计划任务**:支持 15 种推送目标的 Cron 任务构建器(分钟、小时、每日、每周、自定义 Cron)。 +- **16 个消息网关**:Telegram, Discord, Slack, WhatsApp, Signal, Matrix/Element, Mattermost, Email (IMAP/SMTP), SMS (Twilio & Vonage), iMessage (BlueBubbles), 钉钉 (DingTalk), 飞书 (Feishu/Lark), 企业微信 (WeCom), 微信 (WeChat iLink Bot), Webhooks 和 Home Assistant。 +- **Hermes 办公室 (Claw3d)**:具有开发服务器和适配器管理功能的可视化 3D 界面。 +- **备份、导入与诊断导出**:可在设置面板中完成完整的数据备份/恢复,并进行系统诊断。 +- **日志查看器**:直接在设置界面中查看网关和代理的运行日志。 +- **自动更新**:通过 `electron-updater` 检查并安装更新。 +- **国际化 (i18n)**:预置英文语言环境,涵盖所有界面,并已为社区翻译做好框架准备。 +- **测试套件**:涵盖 SSE 解析器、IPC 处理程序、预加载 API、安装程序工具以及常数验证的 Vitest 测试用例。 + +## 运行原理 + +在首次启动时,应用会: + +1. 询问您是希望在**本地**运行 Hermes,还是连接到**远程**的 Hermes API 服务器。 +2. **本地模式:** 检查 `~/.hermes` 目录下是否已安装 Hermes;如果未安装,则运行官方 Hermes 安装脚本并解决依赖关系 (Git, uv, Python 3.11+)。 +3. **远程模式:** 提示输入远程 API URL 和 API 密钥,验证连接,并跳过本地安装。 +4. 提示输入 API 提供商或本地模型端点。 +5. 将提供商配置和 API 密钥保存至 Hermes 配置文件。 +6. 设置完成后启动主工作区。 + +在本地模式下,聊天请求会通过带有 SSE 流的 `http://127.0.0.1:8642` 发送。在远程模式下,应用程序通过相同的流协议与您配置的远程 URL 进行通信。桌面应用会实时解析数据流,并在接收时渲染工具进度、Markdown 内容以及 token 消耗。 + +## 界面说明 + +| 界面 (Screen) | 描述 (Description) | +| -------------------- | --------------------------------------------------------- | +| **聊天 (Chat)** | 支持斜杠命令、工具进度展示和 token 跟踪的流式对话界面 | +| **会话 (Sessions)** | 浏览、搜索并恢复过去的对话 | +| **代理 (Agents)** | 创建、删除和在不同的 Hermes 配置 (Profile) 之间切换 | +| **技能 (Skills)** | 浏览、安装并管理内置及已安装的技能 | +| **模型 (Models)** | 管理并保存各个提供商的模型配置 | +| **记忆 (Memory)** | 查看/编辑记忆条目、用户配置,并配置记忆提供商 | +| **灵魂 (Soul)** | 编辑当前活动配置的代理人格设定 (`SOUL.md`) | +| **工具 (Tools)** | 启用或禁用特定的工具集 | +| **计划 (Schedules)** | 创建并管理定时任务及推送目标 | +| **网关 (Gateway)** | 配置和控制各类消息平台集成 | +| **办公室 (Office)** | Claw3d 可视化界面设置及管理 | +| **设置 (Settings)** | 提供商配置、凭证池、备份/导入、日志查看器、网络设置、主题 | + +## 支持的提供商 + +### 大语言模型 (LLM) 提供商 + +| 提供商 (Provider) | 备注说明 (Notes) | +| ------------------------------ | ---------------------------------------- | +| **OpenRouter** | 通过单一 API 访问 200+ 种模型 (推荐使用) | +| **Anthropic** | 直接访问 Claude 模型 | +| **OpenAI** | 直接访问 GPT 模型 | +| **Google (Gemini)** | Google AI Studio | +| **xAI (Grok)** | Grok 模型 | +| **Nous Portal** | 提供免费额度 | +| **Qwen (通义千问)** | QwenAI 模型 | +| **MiniMax** | 包含全球与中国区端点 | +| **Hugging Face** | 通过 HF Inference 访问 20+ 开源模型 | +| **Groq** | 快速推理 (支持语音/STT) | +| **本地/自定义 (Local/Custom)** | 任何兼容 OpenAI 格式的端点 | + +内置以下本地模型预设:LM Studio, Ollama, vLLM, llama.cpp。 + +### 消息平台 + +Telegram, Discord, Slack, WhatsApp, Signal, Matrix/Element, Mattermost, 电子邮件 (IMAP/SMTP), 短信 (Twilio & Vonage), iMessage (BlueBubbles), 钉钉 (DingTalk), 飞书 (Feishu/Lark), 企业微信 (WeCom), 微信 (WeChat iLink Bot), Webhooks 和 Home Assistant。 + +### 工具集成 + +Exa Search, Parallel API, Tavily, Firecrawl, FAL.ai (图像生成), Honcho, Browserbase, Weights & Biases 和 Tinker。 + +## 开发 + +### 前置要求 + +- Node.js 和 npm +- 能够运行 Hermes 安装程序的类 Unix Shell 环境 +- 首次运行安装 Hermes 时需要网络连接 + +### 安装依赖 + +```bash +npm install +``` + +### 在开发模式下启动应用 + +```bash +npm run dev +``` + +### 运行检查 + +```bash +npm run lint +npm run typecheck +``` + +### 运行测试 + +```bash +npm run test +npm run test:watch +``` + +### 构建桌面应用 + +```bash +npm run build +``` + +各平台打包命令: + +```bash +npm run build:mac +npm run build:win +npm run build:linux +npm run build:rpm # 仅适用于 Fedora/RHEL 的 .rpm 格式 +``` + +## 首次运行设置 + +当应用首次打开时,它会自动检测是否存在已安装的 Hermes 实例,或者提供帮您进行自动安装的选项。 + +UI 中支持的设置路径: + +- `OpenRouter` +- `Anthropic` +- `OpenAI` +- 通过兼容 OpenAI API 基础 URL 接入的 `本地大语言模型 (Local LLM)` + +内置预设包含: + +- LM Studio +- Ollama +- vLLM +- llama.cpp + +Hermes 的相关文件统一管理于以下目录: + +- `~/.hermes` +- `~/.hermes/.env` +- `~/.hermes/config.yaml` +- `~/.hermes/hermes-agent` +- `~/.hermes/profiles/` — 命名配置文件目录 +- `~/.hermes/state.db` — 会话历史数据库 +- `~/.hermes/cron/jobs.json` — 计划任务 + +## 技术栈 + +- **Electron 39** — 跨平台桌面外壳 +- **React 19** — UI 框架 +- **TypeScript 5.9** — 跨主进程和渲染进程的类型安全 +- **Tailwind CSS 4** — 实用优先的样式库 +- **Vite 7 + electron-vite** — 快速开发服务器及构建工具 +- **better-sqlite3** — 带有 FTS5 全文搜索功能的本地会话存储 +- **i18next** — 国际化框架 +- **Vitest** — 测试运行器 + +## 注意事项 + +- 此桌面应用依赖上游的 Hermes Agent 项目来处理代理行为和工具执行。 +- 内置安装程序会通过带有 `--skip-setup` 参数的方式运行官方 Hermes 安装脚本,然后在 GUI 界面中完成提供商相关的配置。 +- 本地模型提供商不需要 API 密钥,但您必须确保兼容的服务器已经在运行中。 +- 在网络受限的环境下,支持配置备用的 npm 镜像源路由。 + +## 参与贡献 + +欢迎大家参与贡献!查看 [参与贡献指南 (Contributing Guide)](CONTRIBUTING.md) 以开始。如果您不知从何入手,可以看一看 [开启的 Issues](https://github.com/fathah/hermes-desktop/issues)。发现了 Bug 或是对功能有新的需求? [提交一个 Issue](https://github.com/fathah/hermes-desktop/issues/new)。 + +## 相关项目 + +如果想了解核心代理功能、详细文档及命令行 (CLI) 的工作流程,请查阅主仓库 Hermes Agent: + +- https://github.com/NousResearch/hermes-agent diff --git a/assets/header.webp b/assets/header.webp new file mode 100644 index 0000000..e36946c Binary files /dev/null and b/assets/header.webp differ diff --git a/assets/partners/atlascloud.webp b/assets/partners/atlascloud.webp new file mode 100644 index 0000000..84edb7f Binary files /dev/null and b/assets/partners/atlascloud.webp differ diff --git a/assets/partners/greptile.webp b/assets/partners/greptile.webp new file mode 100644 index 0000000..8a8c93c Binary files /dev/null and b/assets/partners/greptile.webp differ diff --git a/build/afterPack.js b/build/afterPack.js new file mode 100644 index 0000000..b4a47dc --- /dev/null +++ b/build/afterPack.js @@ -0,0 +1,52 @@ +const { execSync } = require("child_process"); +const path = require("path"); + +// Sign a single path, ignoring "not an Mach-O" errors for non-binary files. +function sign(target) { + try { + execSync(`codesign --force --sign - "${target}"`, { stdio: "pipe" }); + } catch (e) { + // Ignore files that aren't signable (scripts, plists, etc.) + const msg = (e.stderr || e.stdout || "").toString(); + if ( + !msg.includes("is not an Mach-O file") && + !msg.includes("bundle format unrecognized") + ) { + throw e; + } + } +} + +exports.default = async function afterPack(context) { + if (context.electronPlatformName !== "darwin") return; + + const appPath = path.join( + context.appOutDir, + `${context.packager.appInfo.productFilename}.app`, + ); + + console.log(`Ad-hoc re-signing (inside-out): ${appPath}`); + + // Step 1: sign .dylib files (deepest leaves first) + execSync( + `find "${appPath}" -name "*.dylib" | while IFS= read -r f; do codesign --force --sign - "$f" 2>/dev/null || true; done`, + { stdio: "inherit", shell: "/bin/bash" }, + ); + + // Step 2: sign XPC services and nested .app bundles inside Frameworks + execSync( + `find "${appPath}/Contents/Frameworks" -mindepth 1 -maxdepth 4 \\( -name "*.xpc" -o -name "*.app" \\) -prune | while IFS= read -r f; do codesign --force --sign - "$f" 2>/dev/null || true; done`, + { stdio: "inherit", shell: "/bin/bash" }, + ); + + // Step 3: sign each .framework (the versioned bundle, not through symlinks) + execSync( + `find "${appPath}/Contents/Frameworks" -mindepth 1 -maxdepth 1 -name "*.framework" | while IFS= read -r f; do codesign --force --sign - "$f" 2>/dev/null || true; done`, + { stdio: "inherit", shell: "/bin/bash" }, + ); + + // Step 4: sign the outer .app + execSync(`codesign --force --sign - "${appPath}"`, { stdio: "inherit" }); + + console.log("Ad-hoc re-signing complete."); +}; diff --git a/build/entitlements.mac.inherit.plist b/build/entitlements.mac.inherit.plist new file mode 100644 index 0000000..6bc22e9 --- /dev/null +++ b/build/entitlements.mac.inherit.plist @@ -0,0 +1,14 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.cs.disable-library-validation + + + diff --git a/build/entitlements.mac.plist b/build/entitlements.mac.plist new file mode 100644 index 0000000..6bc22e9 --- /dev/null +++ b/build/entitlements.mac.plist @@ -0,0 +1,14 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.cs.disable-library-validation + + + diff --git a/build/icon.icns b/build/icon.icns new file mode 100644 index 0000000..4e1453b Binary files /dev/null and b/build/icon.icns differ diff --git a/build/icon.ico b/build/icon.ico new file mode 100644 index 0000000..c7ad72a Binary files /dev/null and b/build/icon.ico differ diff --git a/build/icon.png b/build/icon.png new file mode 100644 index 0000000..84f6c49 Binary files /dev/null and b/build/icon.png differ diff --git a/build/linux-after-install.sh b/build/linux-after-install.sh new file mode 100755 index 0000000..68ef08f --- /dev/null +++ b/build/linux-after-install.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# After-install hook for the .deb and .rpm packages. +# +# Sets the SUID root bit on chrome-sandbox so Electron's sandbox helper +# can elevate as required. Without this, Electron crashes on close (and +# on first sandboxed renderer launch on some configs) — issue #395. +# +# Why it's needed: Electron's setuid sandbox is the supported sandboxing +# path on Linux when unprivileged user namespaces aren't available. Newer +# Ubuntu (24.04+, definitely 26.04 LTS) ships AppArmor + kernel hardening +# that disable unprivileged user namespaces by default, so the SUID path +# becomes mandatory. +# +# This matches the postinst behaviour of other Electron apps (VS Code, +# Slack, Discord, Signal, etc.). electron-builder doesn't ship this by +# default — it has to be wired up via {deb,rpm}.afterInstall. + +set -e + +SANDBOX="/opt/Hermes One/chrome-sandbox" + +if [ -f "$SANDBOX" ]; then + # 4755 = SUID + rwxr-xr-x. Root-owned by package install; SUID is what + # lets the sandbox briefly elevate for the chroot/setresuid step before + # dropping back to the calling user. + chmod 4755 "$SANDBOX" +fi + +exit 0 diff --git a/build/winget/Installer.template.yaml b/build/winget/Installer.template.yaml new file mode 100644 index 0000000..393ebef --- /dev/null +++ b/build/winget/Installer.template.yaml @@ -0,0 +1,16 @@ +# Generated from this template by scripts/generate-winget-manifests.mjs. +# Placeholders ({{...}}) are replaced at build time. Do not edit the generated copy in dist/. +PackageIdentifier: NousResearch.HermesDesktop +PackageVersion: { { VERSION } } +InstallerLocale: en-US +InstallerType: nullsoft +Scope: user +MinimumOSVersion: 10.0.17763.0 +ReleaseDate: { { RELEASE_DATE } } +Installers: +- Architecture: x64 + InstallerUrl: { { INSTALLER_URL } } + InstallerSha256: { { INSTALLER_SHA256 } } + UpgradeBehavior: install +ManifestType: installer +ManifestVersion: 1.12.0 diff --git a/build/winget/Locale.en-US.template.yaml b/build/winget/Locale.en-US.template.yaml new file mode 100644 index 0000000..4fbb55d --- /dev/null +++ b/build/winget/Locale.en-US.template.yaml @@ -0,0 +1,25 @@ +# Generated from this template by scripts/generate-winget-manifests.mjs. +PackageIdentifier: NousResearch.HermesDesktop +PackageVersion: { { VERSION } } +PackageLocale: en-US +Publisher: Nous Research +PublisherUrl: https://github.com/fathah/hermes-desktop +PublisherSupportUrl: https://github.com/fathah/hermes-desktop/issues +PackageName: Hermes One +PackageUrl: https://github.com/fathah/hermes-desktop +License: MIT +LicenseUrl: https://github.com/fathah/hermes-desktop/blob/main/LICENSE +ShortDescription: Self-improving AI assistant desktop app +Description: |- + Hermes One is a native desktop app for installing, configuring, and chatting + with Hermes Agent — a self-improving AI assistant with tool use, multi-platform + messaging, and a closed learning loop. +Tags: +- ai +- agent +- desktop +- electron +- llm +ReleaseNotesUrl: { { RELEASE_NOTES_URL } } +ManifestType: defaultLocale +ManifestVersion: 1.12.0 diff --git a/build/winget/Version.template.yaml b/build/winget/Version.template.yaml new file mode 100644 index 0000000..1ec1514 --- /dev/null +++ b/build/winget/Version.template.yaml @@ -0,0 +1,6 @@ +# Generated from this template by scripts/generate-winget-manifests.mjs. +PackageIdentifier: NousResearch.HermesDesktop +PackageVersion: { { VERSION } } +DefaultLocale: en-US +ManifestType: version +ManifestVersion: 1.12.0 diff --git a/changelogs/0.4.5.md b/changelogs/0.4.5.md new file mode 100644 index 0000000..2bcb5cb --- /dev/null +++ b/changelogs/0.4.5.md @@ -0,0 +1,69 @@ +# Changelog — v0.4.3 → v0.4.5 + +## Features + +- **Chat attachments** — image and text-file attachments via click, drag-and-drop, and paste; arbitrary file attachments via path references _([@pmos69](https://github.com/pmos69))_ +- **Session deletion** — delete sessions from the Sessions screen _([@leedusty91-prog](https://github.com/leedusty91-prog), [@fathah](https://github.com/fathah))_ +- **OpenAI Codex provider** — setup card for CLI/OAuth-based Codex plan (no API key required) _([@leejamesss](https://github.com/leejamesss))_ +- **Models: provider auto-detect** — auto-detects provider from Base URL in Add/Edit dialog _([@andreab67](https://github.com/andreab67))_ +- **Kanban in SSH tunnel mode** — Kanban board now works when connected via SSH tunnel _([@andreab67](https://github.com/andreab67))_ +- **Office: remote Claw3D auto-detect** — detects and connects to remote Claw3D instance in SSH tunnel mode _([@andreab67](https://github.com/andreab67))_ +- **Providers expanded** — added NVIDIA NIM, Gemini CLI, MiniMax OAuth, and more OpenAI-compatible endpoints _([@pmos69](https://github.com/pmos69))_ +- **Settings: API key mask** — key mask sized to match the actual stored key length _([@pmos69](https://github.com/pmos69))_ +- **i18n: European Portuguese (pt-PT)** — new locale with native-speaker review _([@pmos69](https://github.com/pmos69))_ +- **i18n: Traditional Chinese (zh-TW)** — new locale _([@hansai-art](https://github.com/hansai-art))_ +- **Persist interface locale** — language selection now survives app restarts _([@ASTairov](https://github.com/ASTairov))_ + +## Security + +- **Remote API keys kept in main process** — keys no longer exposed to the renderer process _([@ASTairov](https://github.com/ASTairov))_ + +## Bug Fixes + +### Chat & Models + +- Remote mode URL doubling + local-spawn process leak _([@pmos69](https://github.com/pmos69))_ +- Remote API key wiped when only the URL is edited _([@pmos69](https://github.com/pmos69))_ +- URL normalization in health probe and API key lookup _([@pmos69](https://github.com/pmos69))_ +- Stale `base_url` not cleared when switching named providers _([@pmos69](https://github.com/pmos69))_ +- Stale `baseUrl` when selecting a named-provider model from the model picker _([@pmos69](https://github.com/pmos69))_ +- Models: entries not reloading when tab becomes visible _([@pmos69](https://github.com/pmos69))_ +- Models: intermediate/debounced writes to model library _([@pmos69](https://github.com/pmos69))_ +- Models: Add/Remove/Update not routed through SSH in tunnel mode _([@andreab67](https://github.com/andreab67))_ + +### Install Gate & Providers + +- OAuth-backed active profiles not recognized by install gate _([@pmos69](https://github.com/pmos69))_ +- Install gate not recognizing all known providers in `.env` _([@pmos69](https://github.com/pmos69))_ +- Providers: API keys not persisted on change (only on blur) _([@pmos69](https://github.com/pmos69))_ + +### Config + +- Nested YAML paths not supported in `getConfigValue` _([@andreab67](https://github.com/andreab67))_ +- Model fields reading/writing outside the `model:` block scope _([@pmos69](https://github.com/pmos69))_ +- Dotted YAML paths in `getConfig` / `setConfig` _([@pmos69](https://github.com/pmos69))_ +- Empty env var values rejected in `readEnv` _([@ytfh44](https://github.com/ytfh44))_ +- SSH config helpers not supporting dotted YAML paths _([@pmos69](https://github.com/pmos69))_ + +### SSH & Gateway + +- Tunnel state reset when ControlMaster takes over SSH process _([@arronLuo](https://github.com/arronLuo))_ +- `sudo-wrapper` bypass missing for non-mutating commands (doctor/update/dump/version) _([@andreab67](https://github.com/andreab67))_ +- Gateway: platform enabled state not detected from env vars on Windows _([@pmos69](https://github.com/pmos69))_ +- Gateway: `EPERM` not treated as alive + Python image verification on Windows _([@pmos69](https://github.com/pmos69))_ +- SSH tunnel multiplexing disabled for tunnel processes _([@andreab67](https://github.com/andreab67))_ + +### Installer & Windows + +- `%LOCALAPPDATA%` not matched correctly on Windows _([@pmos69](https://github.com/pmos69))_ +- Office stack start reliability on Windows _([@ASTairov](https://github.com/ASTairov))_ + +### Session & Database + +- Session cache: `messageCount` stale for old sessions on sync _([@pmos69](https://github.com/pmos69))_ +- `deleteSession` using a read-only DB connection (fixed to use writable) _([@ytfh44](https://github.com/ytfh44))_ + +### Other + +- API auth not sent correctly for desktop chat in server mode _([@pmos69](https://github.com/pmos69))_ +- Interface locale not persisted across restarts _([@ASTairov](https://github.com/ASTairov))_ diff --git a/changelogs/0.5.0.md b/changelogs/0.5.0.md new file mode 100644 index 0000000..53b00f2 --- /dev/null +++ b/changelogs/0.5.0.md @@ -0,0 +1,64 @@ +# Changelog — v0.4.5 → v0.5.0 + +## Features + +### Chat +- **Per-conversation context folder** — pin a local folder to a conversation; its files are available as context for that session _([@pmos69](https://github.com/pmos69))_ +- **Tool calls, tool output & reasoning in history** — collapsible sections in chat history show tool invocations, their results, and model reasoning steps _([@jason-edstrom](https://github.com/jason-edstrom))_ +- **Right-click context menu** — copy / paste / select-all and copy-entire-chat from a native context menu in the chat view _([@pmos69](https://github.com/pmos69))_ + +### Providers +- **In-app OAuth sign-in** — "Subscription / OAuth Plans" section on the Providers screen; sign in to ChatGPT (Codex), xAI Grok, Qwen, Gemini CLI, and MiniMax without leaving the app. The Codex device-code flow auto-opens the browser and copies the code to the clipboard _([@leejamesss](https://github.com/leejamesss))_ +- **Model autocomplete for OAuth / subscription providers** — live `/v1/models` discovery now works for OAuth-backed providers _([@pmos69](https://github.com/pmos69))_ +- **Live model-discovery autocomplete** — model name field autocompletes from the provider's live model list for all OpenAI-compatible endpoints _([@pmos69](https://github.com/pmos69))_ + +### Kanban +- **Claw3D HQ read-only board** — surfaces the Claw3D HQ board as a second read-only Kanban view alongside the local board _([@andreab67](https://github.com/andreab67))_ + +### Build & CI +- **Portable Windows build target** — new `win-portable` artifact in the release matrix _([@pmos69](https://github.com/pmos69))_ +- **GitHub Actions CI** — typecheck + full test suite now run on every push and PR _([@pmos69](https://github.com/pmos69))_ + +## Bug Fixes + +### Chat & Sessions +- Sessions screen reads the active profile's `state.db`, not the root one _([@pmos69](https://github.com/pmos69))_ +- Sessions tab now auto-refreshes while it stays open _([@pmos69](https://github.com/pmos69))_ +- Gateway session ID synced on session switch so context is not lost _([@pmos69](https://github.com/pmos69))_ +- Gateway sessions resumed via `X-Hermes-Session-Id` header _([@pmos69](https://github.com/pmos69))_ +- IPC chat-send callbacks guarded against destroyed renderer sender _([@Ricardo-M-L](https://github.com/Ricardo-M-L))_ + +### Providers & Config +- `model.api_key` auto-populated for known-host custom providers _([@pmos69](https://github.com/pmos69))_ +- OpenAI provider correctly routed through custom + explicit `base_url` in setup _([@pmos69](https://github.com/pmos69))_ + +### Tools +- `setToolsetEnabled` no longer drops the `cli` section when a trailing platform block is present _([@ytfh44](https://github.com/ytfh44))_ + +### Skills +- Install failures surfaced when the CLI exits 0 silently instead of disappearing _([@pmos69](https://github.com/pmos69))_ + +### Installer & Updater +- OpenClaw migration detector no longer false-positives on empty directory stubs _([@andreab67](https://github.com/andreab67))_ +- `getHermesVersion` wait capped so a stuck version flag can't leak polling intervals _([@Ricardo-M-L](https://github.com/Ricardo-M-L))_ +- Install confirmation prompt before writing + option to adopt an existing install _([@pmos69](https://github.com/pmos69))_ +- Auto-updater lifecycle logged to a file for post-mortem debugging _([@pmos69](https://github.com/pmos69))_ + +### SSH & Office +- SSH-mode gateway operations routed through `systemd` when `hermes.service` exists _([@pmos69](https://github.com/pmos69))_ +- Kanban SSH errors surface real messages instead of a misleading "unsupported mode" screen _([@pmos69](https://github.com/pmos69))_ +- Office (Claw3D) services authenticated to the gateway _([@pmos69](https://github.com/pmos69))_ + +### Tests +- Three failing tests on `main` repaired _([@pmos69](https://github.com/pmos69))_ + +## New Contributors + +- **[@leejamesss](https://github.com/leejamesss)** — in-app OAuth sign-in for subscription providers ([#102](https://github.com/fathah/hermes-desktop/pull/102)) +- **[@jason-edstrom](https://github.com/jason-edstrom)** — tool calls, tool output & reasoning in chat history ([#327](https://github.com/fathah/hermes-desktop/pull/327)) + +## Contributors + +[@pmos69](https://github.com/pmos69) · [@andreab67](https://github.com/andreab67) · [@Ricardo-M-L](https://github.com/Ricardo-M-L) · [@ytfh44](https://github.com/ytfh44) · [@leejamesss](https://github.com/leejamesss) · [@jason-edstrom](https://github.com/jason-edstrom) + +**Full diff:** [v0.4.5...v0.5.0](https://github.com/fathah/hermes-desktop/compare/v0.4.5...v0.5.0) diff --git a/changelogs/0.6.0.md b/changelogs/0.6.0.md new file mode 100644 index 0000000..cdbaf9e --- /dev/null +++ b/changelogs/0.6.0.md @@ -0,0 +1,133 @@ +# Changelog — v0.5.0 → v0.6.0 + +## Features + +### Hermes One (3D Office) +- **3D Office environment** — immersive CEO office with rigged characters, seating arrangements, and idle animations powered by Three.js / Troika +- **Per-agent chat modal** — open a floating chat window directly from an agent's desk in the Office with full session persistence +- **Bank feature** — Bank room added to the Office world with expandable road-line and city backdrop +- **Office pantry & row items** — Pantry room and increased row-item density in the office layout +- **Office revamp** — major CEO office redesign, control ease improvements, and efficient rendering with drag-building support +- **Performance improvements** — render pipeline optimised for the 3D scene; GPU crash fix applied + +### Gateway & Messaging Platforms +- **Multi-profile gateways** — each profile now runs its own independent gateway instance with dynamic port allocation +- **Gateway start-failure banner** — gateway boot errors surface in the UI instead of silently failing +- **Gateway mismatch error fix** — detects and reports gateway version mismatches +- **Collapsible sidebar** — sidebar can be collapsed to icon-only mode for more chat space +- **Improved messaging platform management** — gateway page redesigned; platform cards show richer state + +### Chat +- **Live structured tool-event stream** — tool calls, results, and reasoning render live as the agent works, with collapsible activity groups and multi-tool summaries +- **Reasoning effort picker** — choose the reasoning budget (low / medium / high) per message for supported models +- **Speech-to-text input** — microphone button in the chat bar for voice input +- **Clarify-request cards** — gateway `clarify.request` events render as inline cards in the conversation +- **Message queue** — messages typed while the agent is processing are queued and sent automatically when it's free +- **Bulk session deletion** — select and delete multiple sessions at once from the Sessions tab +- **Per-row session delete button** — individual delete button on each session row in the Sessions tab +- **Rename session** — rename any session directly from the Sessions tab +- **Chat context folder** — pin a local folder to a conversation for persistent file context (UI upgrade) +- **Pre-send validation** — Send button is disabled with an inline reason when the message cannot be sent +- **Chat layout upgrade** — refreshed chat header and profile layout + +### Providers & Models +- **Nous Portal** — model discovery with `(free)` flag in the model picker; API key + OAuth cards; auto-detected missing credentials +- **Ollama Cloud** — live model list in the picker; API key settings exposed in Providers +- **Atlas Cloud** — new OpenAI-compatible provider preset +- **AIML API** — new provider preset with full i18n labels +- **Xiaomi MiMo** — new provider preset +- **Auxiliary tasks model** — dedicated model selector for background / auxiliary tasks +- **Atomic Chat local preset** — one-click OpenAI-compatible local setup preset +- **Agnes context window** — correct context-length mapping for Agnes and DeepSeek models +- **Reasoning effort** — reasoning effort exposed per-message for models that support it + +### Discover & Skills +- **Discover page** — new Discover screen for browsing and installing skills and MCP servers +- **MCP server management UI** — add, remove, and configure MCP servers from a dedicated UI +- **Skill discovery** — browse the skill registry directly from the Discover page +- **Registry link** — direct link to the skill/MCP registry from the Discover page + +### Tools & Toolsets +- **Worktree terminal launcher** — launch a terminal scoped to any Git worktree +- **Worktree file panel** — browse worktree files with image preview, code preview, and "open in editor" action; toggle between worktrees +- **File icons** — VS Code–style material file icons in the worktree panel + +### Internationalisation +- **Turkish (tr)** locale — full translation across all screens +- **Polish (pl)** locale — full translation across all screens +- **Spanish LATAM (es-LATAM)** — README + 100 % UI translation +- **Japanese** README and CONTRIBUTING +- **Chinese (zh-TW / zh-CN)** Kanban translations +- **Portuguese (pt-PT)** config-health and chat-validation strings + +### Themes & Appearance +- **Theme support** — additional built-in colour themes selectable from Settings +- **Font modifications** — refined system font stack and heading weights + +## Bug Fixes + +### Chat & Sessions +- Fixed chat session split on cold gateway start +- Fixed duplicate split turns during stream reconciliation +- Fixed image prompt replay after DB refresh +- Fixed compressed image filename extension not syncing +- Fixed IME composition Enter sending truncated CJK text +- Fixed `Content-Length` header missing on image-containing chat POSTs +- Fixed clarify card ordering after reconcile + surfaced delivery failures +- Fixed partial stream failures after gateway resume +- Fixed chat errors from stale renderer state + +### Config & Credentials +- Fixed cross-profile API key migration bleeding keys between profiles +- Fixed `API_SERVER_KEY` resolution from `api_server.token` in `config.yaml` +- Fixed host-derived `_API_KEY` not being written for custom-provider chat +- Fixed network proxy settings not persisting across restarts +- Fixed local provider base URLs not persisting +- Fixed import backup file path resolution + +### SSH & Cron +- Fixed SSH API port fallback validation +- Fixed cron schedule prompts not passed positionally +- Fixed SSH API key not being cached before the first message (#212) + +### Gateway +- Fixed gateway restart after resume +- Fixed `API_SERVER_KEY` not injected into gateway spawn environment +- Fixed OAuth login output buffering + +### Skills & Tools +- Fixed skill uninstall edge cases +- Fixed `.trim()` guard on undefined stdout during skill install/uninstall (#145) +- Fixed staged attachments not cleaned up on session delete + +### Office +- Fixed Office gateway startup handoff +- Fixed `executeJavaScript` throwing before DOM is ready +- Fixed Office starting with wrong model (now uses desktop-configured model) +- Fixed Office gateway settings going stale on reconnect +- Quoted Office startup command script safely + +### Diagnose / Config Health +- Fixed config-health banner not refreshing after auto-fix +- Fixed "Show details" button not wiring to Settings nav +- Fixed Hermes profile create errors not surfacing in UI +- Hardened skill content and file writes against partial failures +- Recovered sessions from empty stale cache + +### i18n +- Removed extra Polish locale keys +- Added missing Turkish keys for agents, chat, and sessions screens +- Fixed Turkish locale consistency (`gözat` as single word, `Araçlar` for tools) +- Addressed Spanish i18n review feedback + +## New Contributors + +- **[@914e18c](https://github.com/fathah)** — Turkish locale +- **[@5abea69](https://github.com/fathah)** — Polish locale +- **[@2cba1e1](https://github.com/fathah)** — Spanish LATAM translation + +## Contributors + +[@pmos69](https://github.com/pmos69) · [@andreab67](https://github.com/andreab67) · [@Ricardo-M-L](https://github.com/Ricardo-M-L) · [@ytfh44](https://github.com/ytfh44) · [@leejamesss](https://github.com/leejamesss) + +**Full diff:** [v0.5.0...v0.6.0](https://github.com/fathah/hermes-desktop/compare/v0.5.0...v0.6.0) diff --git a/changelogs/0.7.0.md b/changelogs/0.7.0.md new file mode 100644 index 0000000..32a654b --- /dev/null +++ b/changelogs/0.7.0.md @@ -0,0 +1,106 @@ +# Changelog — v0.6.0 → v0.7.0 + +## Features + +### Wallets & Token Economy +- **Profile wallets** — each profile can create or import Ethereum wallets on Base mainnet; recovery phrases are encrypted with Electron `safeStorage` and never leave the main process (10 wallets per profile) +- **On-chain token balances** — live $H1 / $HD and native balance reads surfaced in the profile Wallet pane +- **Token economy groundwork** — wallet store with create / import / rename / delete / list, public-metadata stripping across the IPC boundary, and balance fetch + refresh + +### Secrets Provider +- **Pluggable secrets backend** — resolve provider keys and credentials through an env-var or external-command backend instead of plaintext config +- **Vault-aware key resolution** — secrets provider wired into every key-resolution path with a vault-first lookup and audit +- **Hardened sudo / secret request modal** — gateway `sudo.request` and `secret.request` events render in a guarded confirmation modal with a finished-guard and vault-first lookup + +### Chat +- **Slash commands** — type `/` in the chat bar to run commands; context-aware command palette +- **Collapsible code blocks** — long code blocks in chat collapse with copy support +- **Copy message** — copy button on each chat message bubble (via the hardened `hermesAPI.copyToClipboard` path) +- **Cancel queued messages** — remove individual messages from the send queue while the agent is busy +- **Notification sound** — optional sound when the agent finishes responding +- **Reconciliation rewrite** — chat history reconciliation aligned with the Hermes Agent dashboard using an interleave algorithm, preserving text streamed before tool calls and fixing duplicate / reordered turns +- **Visible text selection** — selection highlight is now visible inside chat bubbles + +### In-App Web Preview +- **Web preview panel** — open and browse web pages inside the app from the chat workspace +- **Navigation security gating** — preview navigation is gated and auto-navigation is restricted to vetted destinations + +### Sidebar & Navigation +- **Recent sessions list** — ChatGPT-style paged conversation list in the sidebar with infinite scroll, lazy cache reads, and project grouping by linked folder +- **Pinned chats & row context menu** — per-row options menu (Pin, Rename, Move to project, Delete) opened from a hover `…` button or right-click, with motion-driven open/close and page transitions +- **Collapsible, redesigned sidebar** — new layout, reordered navigation, brand-mark collapse toggle, nav logo, and footer action row +- **Full-list sessions modal** — Cmd/Ctrl+K opens the full session list reusing the Sessions screen + +### Profiles +- **Multi-profile sessions** — sessions are scoped per profile; the footer profile switcher keeps the active chat run aligned with the selected profile +- **Profile modal** — a single global modal to view and edit a profile's appearance, persona, agent memory, and wallet, opened from anywhere +- **Profile colours & appearance** — per-profile colour selection and avatar upload/remove +- **Profile enhancements** — basic profile management and mismatch handling improvements + +### Models +- **Chat model search** — searchable model picker in the chat bar +- **Session-based model override** — pick a model per session without changing the global default +- **Auxiliary tasks model** — dedicated model for background / auxiliary tasks + +### Desktop Updates +- **Desktop update controls** — in-app update checking and install controls with status surfaced in the UI +- **Auto-update** — desktop auto-update flow + +### Context Folder +- **Remote context folder** — bind a remote/SSH working folder to a conversation via an in-app picker +- **Persisted per session** — the linked folder is restored when re-opening a conversation and survives session deletion cleanup + +### Internationalisation +- **Hebrew (he)** — complete translation with full RTL support +- **Spanish LATAM (es-LATAM)** — synced with the latest README (Discord, star history, Secrets provider, sponsors) +- **Language updates** — additional locale syncs across screens + +### Branding +- **Hermes One** — rebranded default titles and onboarding copy +- **Updated links & badges** — download link moved to `hermesone.org`; Telegram badge replaced with Discord; refreshed README social and star-history badges + +### Performance +- **SQLite connection caching** — database connections are cached for faster repeated reads +- **GPU fallback** — automatic fallback to avoid GPU-related crashes during 3D / heavy rendering + +## Bug Fixes + +### Chat & Reconciliation +- Fixed previously-sent messages jumping to the bottom of the chat after an agent reply +- Fixed near-duplicate bubbles by pre-seeding seen result bubbles +- Fixed streamed text being clobbered across tool-call boundaries +- Fixed dashboard completion text replacement for re-streamed (e.g. CJK) deltas +- Suppressed duplicate remote dashboard assistant deltas +- Fixed synthetic tool ordering after the interleave reorder + +### Models & Providers +- Fixed the chat model picker overwriting the global default model +- Switched session model selection to a dedicated `sessionModelOverride` state +- Forwarded the model override through the runs-transport fallback +- Provider preset and endpoint updates + +### Secrets +- `getSecretSafe()` now honours the never-throws contract of `getSecret()` +- Closed two `list()` / `get()` consistency gaps and added a get-path spawn floor +- Fixed the S2 misroute guard to test the extracted line rather than full output + +### Profiles & Memory +- Fixed profile mismatch when switching the active agent +- Surfaced profile-appearance errors and de-duplicated resume handling +- Memory limits now read from the profile config + +### SSH & Security +- Reliable legacy fallback when the dashboard `/api/ws` endpoint is unavailable (#667) +- Fixed SSH profile tunnel routing to the selected profile's API port +- Restored the web-preview navigation gate dropped during the `app/` refactor +- Fixed SSH dashboard CSP loopback access + +### Platform +- Fixed a macOS click-blocking drag region in the custom window chrome +- Replaced `navigator.clipboard.writeText` with the hardened `hermesAPI.copyToClipboard` + +## Contributors + +[@hankkyy](https://github.com/hankkyy) · [@pmos69](https://github.com/pmos69) · [@emachado88](https://github.com/emachado88) · [@saxster](https://github.com/saxster) · Michael Valentin · Jesús Alberto Cornelio · Imre Eilertsen · Nazmus Samir · sint · Yosef Hai Yechezkel + +**Full diff:** [v0.6.0...v0.7.0](https://github.com/fathah/hermes-desktop/compare/v0.6.0...v0.7.0) diff --git a/dev-app-update.yml b/dev-app-update.yml new file mode 100644 index 0000000..70d7e4d --- /dev/null +++ b/dev-app-update.yml @@ -0,0 +1,4 @@ +provider: github +owner: fathah +repo: hermes-desktop +updaterCacheDirName: hermes-desktop-updater diff --git a/docs/SSH-TUNNEL-VPS.md b/docs/SSH-TUNNEL-VPS.md new file mode 100644 index 0000000..3c34915 --- /dev/null +++ b/docs/SSH-TUNNEL-VPS.md @@ -0,0 +1,306 @@ +# Connecting Hermes Desktop to a Remote Hermes VPS over SSH + +This guide walks through configuring Hermes Desktop to use a Hermes Agent +running on a remote server (a VPS, a HyperV/KVM VM, a Raspberry Pi on your +LAN, etc.) so that **every screen — Chat, Sessions, Skills, Memory, Soul, +Tools, Schedules, Gateway, Profiles, Models, Logs — works as if Hermes +were installed locally**. + +If you only need to chat against a remote Hermes and you don't care about +the management screens, the simpler **"Remote" mode** (HTTP URL + API key) +is enough. If you want full functionality parity, you need **"SSH Tunnel" +mode**, which is what this document covers. + +## Why SSH Tunnel mode (not plain Remote mode) + +The desktop app has two remote modes, and they cover very different +surface areas: + +| Screen / feature | Remote (HTTP + API key) | SSH Tunnel | +| ---------------------------------------------- | :------------------------: | :--------------: | +| Chat (`/v1/chat/completions`) | ✅ | ✅ | +| Sessions list & search | ❌ reads local `~/.hermes` | ✅ via SSH proxy | +| Skills (browse, install, uninstall) | ❌ reads local `~/.hermes` | ✅ via SSH proxy | +| Memory (view/edit entries, user profile) | ❌ reads local `~/.hermes` | ✅ via SSH proxy | +| Soul (persona editor) | ❌ reads local `~/.hermes` | ✅ via SSH proxy | +| Tools (toolset enable/disable) | ❌ reads local `~/.hermes` | ✅ via SSH proxy | +| Schedules (cron jobs) | ❌ reads local `~/.hermes` | ✅ via SSH proxy | +| Gateway (status, start/stop, platform toggles) | ❌ reads local | ✅ via SSH proxy | +| Profile switching | ❌ reads local | ✅ via SSH proxy | +| Models (saved per-provider configs) | ❌ reads local | ✅ via SSH proxy | +| Logs (gateway, agent) | ❌ reads local | ✅ via SSH proxy | + +Plain Remote mode only proxies the chat path. **All other screens read +the local `~/.hermes` directory**, so if you have no Hermes install on the +desktop's host, those screens look empty even though your remote Hermes +has data. SSH Tunnel mode proxies every screen via `sshExec` against the +remote host's `~/.hermes`, which is what you almost certainly want. + +## Prerequisites + +On the **desktop machine** (where Hermes Desktop runs): + +- An SSH key pair (e.g. `~/.ssh/id_ed25519` / `~/.ssh/id_ed25519.pub`). + Generate one with `ssh-keygen -t ed25519` if you don't have it. +- The OpenSSH client on `PATH`. macOS and Linux have it by default; + Windows 10/11 ship it as an optional feature ("OpenSSH Client"). + +On the **remote machine** (where Hermes Agent runs): + +- OpenSSH server reachable from the desktop host (port 22 by default). +- A user account whose `~/.hermes` directory contains your Hermes data + (more on this below). +- Your desktop's public key authorized for that user + (`~/.ssh/authorized_keys`). +- The Hermes API listening on `127.0.0.1:8642` (the default — it does + **not** need to be exposed publicly; the SSH tunnel forwards it). + +## Which user account should the desktop SSH in as? + +This is the most important decision and the most common source of "the +screens are empty" reports. + +The desktop app's SSH proxy uses paths like `~/.hermes/...` (which +resolves to `$HOME/.hermes/` of the SSH user). It must log in as the +**same user that runs Hermes Agent** so that `~` points at the directory +containing your real data. + +### Case A — You installed Hermes manually as your own user + +If you ran the Hermes installer interactively as e.g. `andrea` and your +data lives in `/home/andrea/.hermes`, SSH in as `andrea`. Nothing extra +to do. + +### Case B — Hermes runs as a dedicated service user (systemd) + +This is common on production VPSes. Hermes is installed under +`/opt/hermes` (or similar) and runs via a systemd unit like: + +```ini +[Service] +User=hermes +Group=hermes +Environment=HOME=/opt/hermes +ExecStart=/opt/hermes/hermes-agent/.venv/bin/hermes gateway +``` + +In this case the data lives at `/opt/hermes/.hermes/` and you need to +SSH in as the `hermes` user. Two things to set up: + +1. **Make sure the `hermes` user has a real login shell.** Hardened + installs sometimes set `/usr/sbin/nologin`. Switch it to bash: + + ```bash + sudo chsh -s /bin/bash hermes + ``` + +2. **Authorize your desktop's public key for the `hermes` user.** Run + this from an account with sudo (e.g. your normal login user): + + ```bash + PUBKEY="ssh-ed25519 AAAA... your-desktop-host" # paste yours + + sudo install -d -o hermes -g hermes -m 700 /opt/hermes/.ssh + sudo touch /opt/hermes/.ssh/authorized_keys + sudo chown hermes:hermes /opt/hermes/.ssh/authorized_keys + sudo chmod 600 /opt/hermes/.ssh/authorized_keys + echo "$PUBKEY" | sudo tee -a /opt/hermes/.ssh/authorized_keys + ``` + + **Note:** systemd's `ProtectHome=read-only` on the Hermes service unit + only restricts the Hermes process itself. Interactive SSH sessions + into the `hermes` user are unaffected, so the desktop can still + write skills, memory edits, soul updates, etc. + +### Case C — Hermes runs as root + +Don't. If it currently does, migrate it to a dedicated user before +exposing SSH to it. + +## Step-by-step setup + +### 1. Verify SSH works exactly as the desktop will call it + +The desktop spawns `ssh` with these flags (see `src/main/ssh-tunnel.ts`): +`-N -L :127.0.0.1:8642 -i -o BatchMode=yes +-o StrictHostKeyChecking=accept-new`. The critical one is +`BatchMode=yes` — **any password or passphrase prompt will fail closed +with no useful error message**. From your desktop, run: + +```bash +ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new \ + -i ~/.ssh/id_ed25519 -p 22 hermes@your.vps.example.com \ + 'curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8642/health' +``` + +You should see `200`. If you see `Permission denied (publickey)`, the +key isn't authorized for that user — double-check +`/opt/hermes/.ssh/authorized_keys` and its permissions (700 on the dir, +600 on the file, owned by the target user). If you see a passphrase +prompt, your key has a passphrase and SSH agent isn't loaded — either +remove the passphrase, or load it into the agent before launching the +desktop app. + +### 2. Configure the desktop app + +Open **Settings → Connection** and select **SSH Tunnel**. Fill in: + +| Field | Value | +| ------------------ | --------------------------------------------------------------------------------------------------- | +| SSH Host | hostname or IP of the remote (e.g. `your.vps.example.com`) | +| SSH Port | `22` (or your sshd port) | +| Username | the user whose `~/.hermes` is the real one (`hermes` in Case B) | +| Private Key Path | absolute path, e.g. `~/.ssh/id_ed25519` on macOS/Linux or `C:\Users\you\.ssh\id_ed25519` on Windows | +| Remote Hermes Port | `8642` (default) | + +Click **Test SSH Connection**. Expected result: "SSH tunnel connected!". +Then **Save** and restart the app. + +### 3. (Alternative) Edit `~/.hermes/desktop.json` directly + +If you prefer to skip the UI, the same config is stored at +`~/.hermes/desktop.json` (the desktop app's _local_ config, on the +desktop machine — not on the VPS): + +```json +{ + "connectionMode": "ssh", + "remoteUrl": "http://your.vps.example.com:8642", + "remoteApiKey": "", + "sshConfig": { + "host": "your.vps.example.com", + "port": 22, + "username": "hermes", + "keyPath": "/Users/you/.ssh/id_ed25519", + "remotePort": 8642, + "localPort": 18642 + } +} +``` + +`remoteUrl` / `remoteApiKey` are retained so you can switch back to +plain Remote mode by changing only `connectionMode`. + +## Verifying every screen works + +After restart, walk through these screens — each should reflect data +from the _remote_ `~/.hermes`, not your local one: + +- **Chat** — send a message. Tokens should stream. +- **Sessions** — should list past conversations from the VPS. +- **Skills** — should show installed skills from the VPS. +- **Memory** — should show memory entries from the VPS. +- **Soul** — should show your remote `SOUL.md`. +- **Tools** — should show toolset enable/disable state. +- **Profiles** — should list profiles defined on the VPS. +- **Schedules** — should show cron jobs from `~/.hermes/cron/jobs.json`. +- **Gateway** — should reflect the running gateway's state. + +If any screen still looks empty, see Troubleshooting below. + +## Troubleshooting + +### "SSH tunnel is not active" or chat hangs + +On **Linux/macOS** versions ≤ 0.4.3 there is a known +`ControlPersist` lifecycle bug — the SSH process exits immediately, +making the desktop think the tunnel died even though port-forwarding is +alive. See [#195][#195] and [#159][#159]. Upgrade to a build that +includes [PR #204][#204] or apply the fix from those issues. + +### "Permission denied (publickey)" from the desktop, but my key works in the terminal + +Most common causes: + +- You use a different key from your terminal (via `~/.ssh/config` host + alias or `ssh-agent`) than the path you configured in the desktop. The + desktop only uses the explicit key file you give it (`BatchMode=yes` + disables agent fallback negotiation in some configurations). +- The key has a passphrase and is unlocked only in the agent. Either + remove the passphrase or ensure the agent is loaded before launching + Hermes Desktop. + +### Screens are empty even after switching to SSH Tunnel mode + +You're almost certainly SSH'ing in as the wrong user — `~/.hermes` +resolves to that user's home, not where Hermes actually keeps its data. +Verify with: + +```bash +ssh -i @ 'ls -la ~/.hermes && pwd' +``` + +The directory should contain `SOUL.md`, `config.yaml`, `auth.json`, +`memories/`, `profiles/`, etc. If you see `No such file or directory`, +you're in the wrong account — re-read the **"Which user account"** +section above. + +### Settings → Hermes Agent shows blank Engine / Released / Python / OpenAI SDK + +Production installs commonly ship `/usr/local/bin/hermes` as a +`sudo -u hermes …` wrapper, and the sudoers policy refuses to run the +wrapper as the `hermes` user itself ("Sorry, user hermes is not allowed +to execute …"). The result: `sshGetHermesVersion` returns empty and the +Settings card renders four blanks while everything else works. + +Fixed in [PR #205][#205] by probing the venv binary directly. If your +build pre-dates that fix, you can verify locally with: + +```bash +ssh @ '/opt/hermes/hermes-agent/.venv/bin/hermes --version' +``` + +A working version string means the fix will populate the card once your +build includes #205. + +### Kanban shows "Kanban requires a local Hermes install" + +This screen is not yet wired for remote/SSH mode (the UI explicitly +says "Remote/SSH support is coming in a follow-up"). All other +management screens work in SSH tunnel mode; Kanban is the one +exception. Track upstream for the follow-up PR. + +### Office (Claw3D) offers to install Claw3D locally + +The Office screen detects Claw3D on the desktop host, not on the VPS. +If you're already running `hermes-office.service` on the VPS, that +service is independent of this screen — visit it directly at +`http://:3000`. Tighter integration is tracked in +[#196](https://github.com/fathah/hermes-desktop/issues/196). + +### `Test SSH Connection` succeeds but chat fails with 401 or auth errors + +Hermes API may require an API key locally even when bound to +`127.0.0.1`. Configure it in the desktop app's Settings → API key (or +leave blank if the gateway is configured for no-auth on localhost). The +key, if used, is the one stored in your remote Hermes `.env`/`auth.json`, +not a value you generate on the desktop. + +### Windows-specific: keys not persisting across restarts + +Tracked in [#182][#182]. If you hit this, store the desktop's API key +and SSH key path in a password manager and re-paste after a Windows +restart until the upstream fix lands. + +## Security notes + +- The SSH tunnel binds **only** to `127.0.0.1` on the desktop side. The + remote Hermes port is **not** exposed to the public internet at any + point in this flow. +- `BatchMode=yes` means a stolen desktop without an unlocked SSH key + cannot impersonate you to the remote Hermes — there's no password to + steal and no key-loading prompt to manipulate. +- `StrictHostKeyChecking=accept-new` trusts the host key on first + connection and pins it in `~/.ssh/known_hosts` after that. If the + remote host key ever changes (e.g. server reinstall), SSH will fail + closed and you'll need to manually re-trust it. This is the desired + behavior — don't change it. +- Authorize the desktop's pubkey only on the dedicated Hermes user, not + on root. The Hermes user is already what runs the agent; giving it + inbound SSH does not expand the blast radius. + +[#159]: https://github.com/fathah/hermes-desktop/issues/159 +[#182]: https://github.com/fathah/hermes-desktop/issues/182 +[#195]: https://github.com/fathah/hermes-desktop/issues/195 +[#204]: https://github.com/fathah/hermes-desktop/pull/204 +[#205]: https://github.com/fathah/hermes-desktop/pull/205 diff --git a/docs/chat-reconciliation-plan.md b/docs/chat-reconciliation-plan.md new file mode 100644 index 0000000..626212c --- /dev/null +++ b/docs/chat-reconciliation-plan.md @@ -0,0 +1,2370 @@ +# Chat Reconciliation Stabilization Plan + +## 2026-06-06 Direction Correction: Dashboard/WebSocket Timeline + +This document originally described a conservative stabilization path for the +existing Hermes One reconciliation layer. That path is useful as background, but +it is not sufficient as the long-term implementation direction. + +Manual testing and comparison with Hermes Agent desktop showed that PR540/PR545 +and the current Hermes One implementation still share the same risky shape: + +- live UI rows are assembled from one stream; +- restored/final rows are assembled from `state.db`; +- completion then tries to merge two independently-shaped transcripts; +- edge cases depend on content matching, row splitting, and heuristics. + +The corrected target is closer to Hermes Agent desktop: + +- use a live dashboard/WebSocket event stream as the active-turn source of + truth; +- build one ordered turn timeline from gateway events; +- project both live events and restored DB rows through the same normalizer; +- avoid whole-transcript text-key reconciliation for active turns; +- treat DB hydration as replacement/fill-in after a turn is settled, not as a + competing live transcript. + +The old reconciliation work should not be expanded into another pile of special +cases. It should be treated as a compatibility layer while the dashboard +transport is introduced. + +### What Hermes Agent Desktop Does Differently + +The latest Hermes Agent desktop app lives at: + +`apps/desktop` in `NousResearch/hermes-agent`. + +Relevant files: + +- `apps/desktop/electron/main.cjs` +- `apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts` +- `apps/desktop/src/app/session/hooks/use-message-stream.ts` +- `apps/desktop/src/app/session/hooks/use-prompt-actions.ts` +- `apps/desktop/src/app/session/hooks/use-session-actions.ts` +- `apps/desktop/src/lib/chat-messages.ts` +- `apps/desktop/src/lib/chat-runtime.ts` + +The desktop app starts a dashboard backend: + +```text +hermes dashboard --no-open --host 127.0.0.1 --port +``` + +It then connects the renderer to: + +```text +ws://127.0.0.1:/api/ws?token= +``` + +Submissions are JSON-RPC calls: + +```text +session.create +session.resume +prompt.submit +session.interrupt +``` + +Streaming arrives as ordered events: + +```text +message.start +reasoning.delta +reasoning.available +message.delta +tool.start +tool.progress +tool.generating +tool.complete +message.complete +error +clarify.request +approval.request +sudo.request +secret.request +subagent.* +``` + +The app does not render one independent "streamed transcript" and then reconcile +that against an unrelated DB transcript. It keeps a per-runtime-session state: + +```ts +interface ClientSessionState { + storedSessionId: string | null + messages: ChatMessage[] + busy: boolean + awaitingResponse: boolean + streamId: string | null + sawAssistantPayload: boolean + interrupted: boolean + needsInput: boolean +} +``` + +Live events mutate the in-flight assistant message's ordered `parts[]`: + +```ts +type ChatMessagePart = + | { type: "text"; text: string } + | { type: "reasoning"; text: string } + | { type: "tool-call"; toolCallId: string; toolName: string; args: unknown; result?: unknown } +``` + +The key invariant is boundary flushing: + +- before a tool starts, flush any queued assistant/reasoning text; +- before a tool completes, flush queued text; +- before final completion, flush queued text; +- when a final stored transcript is loaded, preserve local errors but otherwise + prefer the stable stored projection. + +This is why Hermes Agent desktop can show: + +```text +thought +tool call/result +assistant text +thought +tool call/result +assistant text +``` + +instead of: + +```text +all tool calls +all tool results +all intermediate text appended at the end +``` + +### Why DB Polling Did Not Solve The Bug + +We tried a mid-stream `state.db` polling bridge in the separate worktree. Manual +testing showed no visible streaming improvement. A DB dump after a tool-heavy +turn showed the rows appeared only near finalization, with effectively the same +timestamps. That means polling cannot recover live reasoning/tool-result order +when the backend does not persist those rows until the turn is done. + +Polling may remain useful as a fallback for older backends, but it is not the +primary design. + +### Why `/v1/runs` Is Not The Main Target + +PR540/PR545 already used the `/v1/runs`/event transport path and still had +reconciliation bugs. It gives more structured events than `/v1/chat/completions`, +but it does not by itself remove the fragile merge architecture if the renderer +still reconciles synthetic rows against DB rows by content/key heuristics. + +In this worktree, `/v1/runs` is now guarded behind: + +```text +HERMES_DESKTOP_ENABLE_RUNS_TRANSPORT=1 +``` + +Default development should not depend on it. + +## Corrected Phased Implementation Plan + +### Progress Note: 2026-06-07 + +Phase B is implemented in the separate worktree: + +- `src/main/dashboard.ts` can start/stop/probe a local dashboard backend with a + per-process `HERMES_DASHBOARD_SESSION_TOKEN`. +- Preload IPC exposes `dashboardStatus`, `startDashboard`, and `stopDashboard`. +- A sandbox smoke test confirmed `/api/status` accepts + `X-Hermes-Session-Token` and that the dashboard command is available in the + copied Hermes Agent install. + +Phase C is partially implemented behind an explicit renderer flag: + +- `src/renderer/src/screens/Chat/dashboardGatewayClient.ts` implements a small + JSON-RPC WebSocket client and now matches upstream's notification envelope: + `{"method":"event","params":{type,payload,session_id}}`. +- `src/renderer/src/screens/Chat/dashboardEventAdapter.ts` reduces ordered + dashboard events into Hermes One's existing `ChatMessage[]` shape. +- `src/renderer/src/screens/Chat/hooks/useDashboardChatTransport.ts` can create + or resume a dashboard runtime session and submit prompts through + `prompt.submit`. +- The route is disabled by default and only used when + `VITE_HERMES_DESKTOP_DASHBOARD_CHAT=1`. + +Current limitations before making this the normal sandbox path: + +- attachment submission still falls back to the existing HTTP transport; +- approval/clarify/sudo/secret events are not yet surfaced; +- runtime session ID and stored session ID are separated internally, but the + surrounding Hermes One navigation/history model still needs a fuller stored + session handoff; +- live manual testing with the flag enabled has not yet been completed. + +### Upstream Refresh: 2026-06-09 + +`NousResearch/hermes-agent` `origin/main` was inspected at commit +`57775e9e1` on 2026-06-09. Upstream has moved enough that several of our +compatibility assumptions should change. + +Useful upstream changes: + +- Dashboard chat is now always enabled in `hermes_cli/web_server.py` via + `_DASHBOARD_EMBEDDED_CHAT_ENABLED = True`. The old `embedded_chat` argument + is gone from current `start_server(...)`. +- The old `hermes dashboard --no-open --tui ...` invocation is accepted for + compatibility, but upstream Desktop now starts dashboard without `--tui`. +- `tui_gateway.server` exposes a `DESKTOP_BACKEND_CONTRACT` value. Current + contract `2` means remote non-image file upload via `file.attach` is present. +- Current gateway RPCs include: + - `session.create` + - `session.resume` + - `prompt.submit` + - `image.attach` + - `image.attach_bytes` + - `file.attach` + - `model.options` + - `model.save_key` +- Current dashboard REST endpoints include: + - `GET /api/model/options` + - `GET /api/model/recommended-default` + - `GET /api/model/auxiliary` + - `POST /api/model/set` +- Remote OAuth gateways are first-class upstream: + - `/api/status` reports `auth_required: true`; + - REST auth uses cookies; + - WebSocket auth uses a single-use ticket from `POST /api/auth/ws-ticket`; + - static `?token=` is still used for legacy token-mode gateways. +- Upstream Desktop probes the actual `/api/ws` WebSocket, not just + `/api/status`, before saying a remote connection is usable. +- Upstream Desktop syncs attachments before `prompt.submit`: + - local image path -> `image.attach`; + - remote image bytes -> `image.attach_bytes`; + - local file path -> `file.attach`; + - remote file bytes/data URL -> `file.attach`. +- Upstream Desktop retries `prompt.submit` once after a `session not found` + error by calling `session.resume` on the durable stored session id, then + submitting to the fresh live runtime id. +- Upstream event routing now intentionally accepts unscoped foreground turn + events (`message.*`, `reasoning.*`, `tool.*`) as belonging to the active chat + and drops only unscoped `subagent.*` events. +- Upstream has direct regression coverage for edge cases close to ours: + - duplicate user messages in `state.db` on failure/fallback paths; + - sleep/wake gateway restart recovery; + - remote gateway file attachments; + - background-window streaming stalls; + - focused-chat unscoped event routing. + +Immediate Hermes One adjustments from this refresh: + +- The compatibility addon must treat `_DASHBOARD_EMBEDDED_CHAT_ENABLED = True` + as already compatible. It should patch only older engines that still expose + `embedded_chat: bool = False`. +- Local/SSH dashboard startup can keep passing `--tui` for older engines, but it + should not depend on source patching after the bundled engine updates to the + current dashboard shape. +- Remote/SSH model lists and model writes should prefer upstream + `/api/model/options`, `model.options`, `model.save_key`, and + `/api/model/set` instead of direct remote `models.json` editing whenever the + backend supports them. +- Remote/SSH pasted attachments should move from Hermes One staging/fallback + behavior to the upstream attach RPCs, especially `image.attach_bytes` and + `file.attach`. +- Remote OAuth should be promoted from "unsupported warning" to the upstream + cookie + `/api/auth/ws-ticket` flow. +- Connection tests should probe `/api/ws` with the same credential mode the + renderer will use. +- We should add a Hermes One test matching upstream's submit recovery: + `prompt.submit` returns `session not found` -> `session.resume` -> retry + `prompt.submit` against the recovered live id. + +Validation performed after this refresh: + +- `npm test -- --run tests/hermes-agent-compat.test.ts tests/dashboard-remote.test.ts` + passed. +- `npm run typecheck` passed. + +### Phase A: Freeze The Existing Reconciliation Layer + +Goal: + +- stop expanding the PR540/545-style reconciliation path; +- keep current app behavior usable while we build the replacement path; +- protect manual testing from accidentally using `/v1/runs`. + +Actions: + +- keep the separate worktree and sandbox setup; +- use the sandboxed `Hermes One` app identity; +- keep config isolation and port isolation; +- make `/v1/runs` opt-in only; +- leave current DB refresh/reconciliation tests in place as regression coverage; +- document known limitations of the HTTP stream path: + - no reliable live tool results; + - no reliable live reasoning for some providers; + - DB rows can appear only at finalization; + - content-key reconciliation can misplace local errors and split text. + +Acceptance criteria: + +- the app builds and tests pass; +- normal chat still works through the existing HTTP path; +- `/v1/runs` is not selected unless explicitly enabled. + +### Phase B: Add Dashboard Capability Discovery + +Goal: + +- detect whether the installed Hermes Agent supports dashboard WebSocket mode; +- do this without replacing the chat path yet. + +Main-process additions: + +- add a `DashboardConnection` descriptor: + +```ts +interface DashboardConnection { + baseUrl: string + wsUrl: string + token: string + mode: "local" | "remote" + profile?: string + processPid?: number +} +``` + +- add IPC: + +```text +dashboard:get-connection +dashboard:start +dashboard:stop-dev-only +dashboard:status +``` + +- for local mode, start: + +```text +hermes dashboard --no-open --host 127.0.0.1 --port +``` + +- pass: + +```text +HERMES_DASHBOARD_SESSION_TOKEN= +HERMES_HOME= +``` + +- for remote/SSH mode, probe: + +```text +/api/status +/api/ws ticket/token support +``` + +Open questions to answer during this phase: + +- Does the Hermes One installed Hermes Agent version always ship + `hermes dashboard`? +- For existing user installs, do we need to update Hermes Agent before the + dashboard path is available? +- Can remote mode reuse the dashboard WebSocket, or only local mode initially? + +Acceptance criteria: + +- the app can start/probe dashboard without disturbing the existing gateway; +- dashboard uses sandbox ports in this worktree; +- dashboard logs are separate from the user's normal gateway logs; +- failure falls back to current HTTP chat. + +### Phase C: Add A Renderer Gateway Client + +Goal: + +- establish a live JSON-RPC WebSocket in the renderer, matching Hermes Agent + desktop's model. + +Renderer additions: + +- add a small `JsonRpcGatewayClient` or vendor/adapt the shared client if + dependency layout permits; +- expose a typed event stream: + +```ts +type GatewayEvent = + | { type: "message.start"; session_id?: string; payload?: unknown } + | { type: "message.delta"; session_id?: string; payload?: { text?: string; rendered?: string } } + | { type: "reasoning.delta"; session_id?: string; payload?: { text?: string } } + | { type: "reasoning.available"; session_id?: string; payload?: { text?: string } } + | { type: "tool.start"; session_id?: string; payload?: GatewayToolPayload } + | { type: "tool.progress"; session_id?: string; payload?: GatewayToolPayload } + | { type: "tool.complete"; session_id?: string; payload?: GatewayToolPayload } + | { type: "message.complete"; session_id?: string; payload?: { text?: string; rendered?: string; reasoning?: string; usage?: unknown } } + | { type: "error"; session_id?: string; payload?: { message?: string } } +``` + +- implement request/response calls: + +```text +session.create +session.resume +prompt.submit +session.interrupt +session.usage +model.options +``` + +Acceptance criteria: + +- WebSocket reconnects cleanly; +- request timeouts are surfaced as UI errors; +- events are filtered by active runtime session; +- no chat rendering has switched yet. + +### Phase D: Introduce An Ordered Turn Timeline + +Goal: + +- replace row-by-row live appends with a single active-turn state machine. + +Do not migrate all rendering to Hermes Agent desktop's `parts[]` in one jump +unless it proves simpler. Hermes One can use an intermediate timeline and then +project to existing `ChatMessage` rows. + +Suggested internal model: + +```ts +type TimelinePart = + | { type: "reasoning"; id: string; text: string; pending?: boolean } + | { type: "text"; id: string; text: string; pending?: boolean } + | { type: "tool"; id: string; callId: string; name: string; args: string; result?: string; status: "running" | "completed" | "failed" } + | { type: "error"; id: string; error: string } + +interface ActiveTimelineTurn { + turnId: string + userMessageId: string + assistantMessageId: string + parts: TimelinePart[] + queuedText: string + queuedReasoning: string + interrupted: boolean +} +``` + +Boundary rules copied from Hermes Agent desktop: + +- `message.delta`: append to `queuedText`, flush on animation/timer; +- `reasoning.delta`: append to `queuedReasoning`, flush on animation/timer; +- `tool.start`: flush queued text/reasoning, close reasoning segment, add tool; +- `tool.progress`: update the active tool by stable ID/name; +- `tool.complete`: flush queued text/reasoning, update matching tool with result; +- `message.complete`: flush everything, append final tail only, mark complete; +- `error`: flush partial content if useful, append local error, mark failed. + +Projection to existing UI rows: + +- reasoning part -> `ReasoningMessage`; +- tool part -> `ToolCallMessage` plus optional `ToolResultMessage`; +- text part -> agent `ChatBubbleMessage`; +- error part -> agent `ChatBubbleMessage` with `error`. + +Acceptance criteria: + +- pure unit tests prove event order is preserved; +- sequential tools render call/result pairs in event order; +- prose between tools stays between those tools; +- reasoning before/after tools stays in place; +- local errors stay attached to the failed user turn. + +### Phase E: Route New Chats Through Dashboard Transport + +Goal: + +- make dashboard/WebSocket the default for local sandbox chat while preserving + HTTP fallback. + +Submission flow: + +1. If no active runtime session: + - call `session.create`; + - store runtime session ID and stored session ID separately. +2. Optimistically insert the user message. +3. Call: + +```text +prompt.submit { session_id, text } +``` + +4. Drive the active timeline entirely from WebSocket events. +5. On `message.complete`, optionally hydrate stored session once to align + final artifacts. + +Fallback: + +- if dashboard connection fails before submit, use existing HTTP path; +- if dashboard fails mid-turn, render a local error anchored to that turn. + +Acceptance criteria: + +- "what time is it?" streams visibly; +- bad provider key displays an anchored local error; +- switching to a good provider in the same visible chat does not move/drop the + failed turn; +- no local error text is sent back to the model as assistant context. + +### Phase F: Restore Sessions Through The Same Projection + +Goal: + +- restored sessions and live sessions use the same visual representation. + +Implementation: + +- keep `getSessionMessages(sessionId)` as the stored transcript fetch; +- replace ad hoc DB row mapping with a normalizer equivalent to Hermes Agent + desktop's `toChatMessages()`; +- merge tool-call assistant rows and `role="tool"` rows into one ordered + assistant turn projection; +- preserve reasoning rows; +- preserve attachments/artifacts; +- preserve local-only errors only during the current app process unless an + overlay store is explicitly added later. + +Acceptance criteria: + +- restored session shows: + - user messages; + - assistant messages; + - reasoning/thoughts; + - tool calls; + - tool results; + - errors persisted by Hermes Agent; + - image/file artifacts and attachments; +- restored session no longer shows duplicated tool-call-only groups after a + final assistant response. + +### Phase G: Retire Whole-Transcript Text-Key Reconciliation + +Goal: + +- remove the bug-prone class of behavior. + +Implementation: + +- stop calling `reconcileStreamedWithDb()` for dashboard-driven active turns; +- use final DB hydration as: + - full replacement when the active turn has completed and no local error is + pending; + - stable-ID/turn-ID preservation only for local errors and in-flight UI state; +- keep old reconciliation only for HTTP fallback mode. + +Acceptance criteria: + +- no content-prefix matching is needed for normal dashboard turns; +- DB hydration cannot move a local failed turn after a later successful turn; +- split assistant text is handled by the stored-session normalizer, not by + post-hoc duplicate heuristics. + +### Phase H: Manual Test Matrix + +Run these in the sandboxed Hermes One instance, with the sandbox home and non-conflicting ports: + +- simple no-tool prompt with visible streaming; +- invalid API key/provider failure; +- invalid key followed by good provider in the same visible chat; +- DeepSeek reasoning-heavy prompt; +- GPT reasoning-heavy prompt; +- image generation skill with: + - reasoning before first tool; + - multiple sequential tool calls; + - tool results; + - intermediate assistant prose; + - final image artifact; +- restored session for each above case; +- app reload while a session is idle; +- app reload after failed provider turn; +- remote/SSH mode fallback. + +### Phase I: Optional Persistent Local Error Overlay + +Dashboard/Hermes Agent may not persist provider setup failures that happen +before a turn reaches the agent. If we want those local-only errors to survive +app restart, add a desktop-owned overlay store: + +```text +/desktop/session-overlays.json +``` + +Shape: + +```ts +interface SessionOverlayEvent { + sessionId: string + turnId: string + afterUserId?: string + afterUserText: string + error: string + createdAt: number +} +``` + +This should be a later phase because it is product behavior, not required for +live reconciliation correctness. + +## Current Status In This Worktree + +Already done: + +- separate worktree for development; +- sandbox scripts/config/ports; +- sandbox app identity `Hermes One`; +- local error metadata and rendering; +- preliminary local-error preservation tests; +- selected PR545 display pieces for grouped tools/reasoning; +- DB polling experiment, shown not to solve live ordering; +- `/v1/runs` experiment, now opt-in only. + +Next recommended phase: + +- package the Hermes Agent dashboard compatibility changes as a managed desktop + addon/overlay, then wire local, remote HTTP, and SSH deployment/checks through + that same capability probe. + +## 2026-06-09 Remaining Work After Dashboard Transport Stabilization + +The dashboard/WebSocket path, remote HTTP path, and SSH-over-tunnel path are now +implemented in the sandboxed Hermes One instance and covered by unit/integration tests. Manual live +testing has also exercised: + +- normal local and remote dashboard chat; +- bad provider/key failure followed by good-provider recovery; +- repeated failed/non-failed turns in one visible session; +- reasoning-heavy and tool-heavy turns; +- grouped tool calls and restored grouped tool calls; +- prompt-image attachments through dashboard transport; +- restored sessions with local desktop overlay rows; +- remote HTTP sessions and remote HTTP configured-model CRUD; +- SSH dashboard transport over a local tunnel. + +The remaining work is now mostly operational hardening, not a change in the +chat-reconciliation architecture. + +### 1. Final Regression Gate + +Status: implemented as a repeatable gate. + +Run the complete regression battery after every change in this final hardening +pass. The canonical manual and automated checklist now lives in: + +```text +docs/reconciliation-regression-playbook.md +``` + +Automated command: + +```powershell +npm run typecheck +npm test -- --run tests/remote-sessions.test.ts tests/remote-metadata.test.ts tests/remote-models.test.ts tests/dashboard-chat-transport.test.ts tests/dashboard-event-adapter.test.ts tests/live-tool-events.test.ts tests/live-reasoning-events.test.ts tests/tool-activity-group-title.test.ts tests/reconcile-streamed-with-db.test.ts tests/session-history-mapping.test.ts tests/sessions-history-items.test.ts tests/sessions-decode-content.test.ts src/renderer/src/screens/Chat/mediaUtils.test.ts src/renderer/src/screens/Chat/hooks/useChatIPC.test.tsx tests/chat-messages.test.ts tests/session-continuation-store.test.ts tests/dashboard-remote.test.ts tests/dashboard-launch.test.ts tests/dashboard-gateway-client.test.ts tests/hermes-agent-compat.test.ts tests/run-stream.test.ts src/renderer/src/screens/Sessions/Sessions.test.tsx +``` + +Current result on 2026-06-11: + +- 21 focused test files passed; +- 231 focused tests passed; +- full suite passed: 102 test files, 1158 tests passed, 3 skipped; +- TypeScript node and web typechecks passed. + +### 2. Hermes Agent Compatibility Addon/Overlay + +Status: first implementation slice complete. + +Hermes One currently depends on Hermes Agent dashboard capabilities that may not +be present, or may be present but not enabled correctly, in every installed or +remote Hermes Agent. We should not leave those fixes as manual source edits. + +Package them as a desktop-managed compatibility addon/overlay with this shape: + +- detect capabilities before patching: + - `/api/status` reachable; + - authenticated `/api/ws` accepts a WebSocket upgrade; + - dashboard HTML advertises embedded chat support; + - configured-model list/CRUD endpoints are available; + - session list and session message endpoints are available; +- deploy only the missing compatibility pieces; +- record the applied addon version on the target Hermes Agent home; +- support local Hermes Agent install, remote HTTP target when writable/deployable, + and SSH target; +- never modify provider credentials, user sessions, skills, memory, or model + configuration while applying the addon; +- expose a clear Settings/diagnostics result when the target cannot be patched + automatically. + +For the embedded-chat compatibility issue found during live testing, the addon +should avoid depending on a one-off source edit. The target behavior is: + +```text +hermes dashboard --no-open --host --port --tui +``` + +must produce a dashboard where: + +```text +GET /api/status -> authenticated success +GET /api/ws upgrade -> 101 Switching Protocols +``` + +If the installed engine does not do that, the addon should either patch/wrap the +dashboard launch behavior or install a small compatibility module that makes the +dashboard default compatible with Hermes One. + +Implemented first slice: + +- `src/main/hermes-agent-compat.ts` owns a versioned compatibility patch for the + reproduced embedded-dashboard-chat issue. +- Local Hermes Agent installs are checked before dashboard launch and after + `hermes update`. +- SSH Hermes Agent targets are checked before dashboard probing/startup and + after `hermes update` over SSH. +- The patch is intentionally narrow: it changes only the + `embedded_chat: bool = False` default in `hermes_cli/web_server.py` to + `True`. +- A diagnostic marker is written to: + - local: `/desktop-compat/dashboard-embedded-chat.json`; + - SSH: `~/.hermes/desktop-compat/dashboard-embedded-chat.json`. +- Plain remote HTTP is still probe-only. Hermes One cannot safely patch it + without either SSH access or a future Hermes Agent deploy endpoint. + +Current tests: + +- `tests/hermes-agent-compat.test.ts` covers compatible, patchable, and unknown + source shapes. +- The dashboard compatibility tests and full chat/session regression battery + pass after the slice. + +### 3. Active Transport Status Indicator + +Status: already implemented. + +The Settings UI has separate configuration choices for local/remote/SSH +dashboard behavior and reports the actual active path after probing. The probe +now checks the dashboard WebSocket, not only REST status, so a remote target with +healthy REST but disabled embedded chat is reported as unavailable instead of +silently pretending dashboard chat is usable. + +Keep this item as a regression check, but no new design work is currently +needed. + +### 4. Remote/SSH/Local Test Matrix + +Status: agreed, continue as a gate. + +Before merging, run the same user-visible cases across every transport that is +expected to work: + +- local dashboard auto; +- local dashboard forced; +- local legacy fallback; +- remote HTTP dashboard auto; +- remote HTTP dashboard forced; +- remote HTTP legacy fallback; +- SSH dashboard auto; +- SSH dashboard forced; +- SSH legacy fallback. + +For each transport, cover: + +- simple no-tool prompt; +- bad provider/key failure; +- bad provider followed by good provider in the same session; +- image prompt attachment; +- tool-heavy image-generation turn where the image path is mentioned in text; +- restored session; +- continue restored session; +- model switch and configured-model list refresh. + +Known upstream limitation from this pass: + +- Gemini failures in the current lab were traced to Hermes Agent upstream + behavior, not the Hermes One dashboard transport. + +### 5. Reapply Addon After Engine Updates + +Status: not yet implemented. + +Yes: any Hermes Agent compatibility patch/addon must be rechecked after every +engine update, for both local and remote targets. + +The update flow should become: + +1. run the normal Hermes Agent update; +2. restart/probe the updated engine; +3. run the compatibility capability probe; +4. if required, reapply the addon/overlay; +5. restart/probe again; +6. surface the final state in Settings and diagnostics. + +This applies to: + +- Hermes One's bundled/local Hermes Agent install; +- remote HTTP targets when Hermes One is allowed to deploy an addon; +- SSH targets through the tunnel/SSH deployment path. + +If the target cannot be modified, Hermes One should keep working in legacy mode +and clearly report why dashboard mode is unavailable. + +### 6. Final Review And Upstreaming + +Status: pending. + +Before this leaves the worktree: + +- split the work into reviewable commits/PRs; +- separate Hermes One changes from Hermes Agent compatibility changes; +- document the compatibility contract Hermes One expects from Hermes Agent; +- upstream the Hermes Agent embedded-chat/dashboard fix if possible; +- keep the addon/overlay path until the minimum supported Hermes Agent version + contains the fix natively. + +## Purpose + +Hermes One currently merges two views of chat state: + +- the renderer's streamed in-memory transcript +- the Hermes Agent `state.db` transcript loaded through `getSessionMessages()` + +That merge is valuable because the database contains artifacts that may not arrive +over the OpenAI-compatible stream, especially reasoning rows, tool calls, tool +results, and attachment metadata. But the current whole-session reconciliation is +fragile for local-only failures. A provider/key error can create a local renderer +error bubble that has no DB equivalent; after a later successful turn, the global +DB merge can move that error to the wrong position, drop neighboring lines, or +send the synthetic error text back to the model as prior assistant content. + +The implementation goal is to keep the useful DB artifact fill-in behavior while +making local failures explicitly modeled and anchored to their originating turn. + +## Design Principle + +Use different sources of truth for different phases: + +- Cold load / restored session: `state.db` is canonical. +- Active streaming turn: renderer order is canonical. +- Successful turn completion: DB may fill missing persisted artifacts into the + current visible transcript. +- Failed turn completion: local error state is canonical for that failed turn; + do not run a global DB reconciliation that can reorder it. + +In short: + +> DB is canonical for restored sessions. During active chat, streamed UI is +> canonical for ordering; DB only fills missing persisted artifacts for +> successful turns. + +## Current Hermes One Code Paths + +### Message Shape + +File: `src/renderer/src/screens/Chat/types.ts` + +Current `ChatBubbleMessage` has: + +- `id` +- optional `kind` +- `role: "user" | "agent"` +- `content` +- optional `attachments` + +It does not have: + +- `error` +- `pending` +- `localOnly` +- `turnId` +- an anchor to the user message that caused an assistant response/error + +This means provider errors are represented as normal assistant text, which makes +them indistinguishable from model output during reconciliation and future request +history construction. + +### Sending History + +File: `src/renderer/src/screens/Chat/hooks/useChatActions.ts` + +`sendToAgent()` currently sends: + +```ts +messagesRef.current.filter(hasContent).map((m) => ({ + role: m.role, + content: m.content, +})); +``` + +Because renderer-only errors are normal `role: "agent"` content bubbles, an error +like `Error: provider returned 401` can become part of the next provider's LLM +context. That is not only noisy; it can change model behavior in later turns. + +### Streaming + +File: `src/renderer/src/screens/Chat/hooks/useChatIPC.ts` + +Current streaming handlers: + +- `onChatChunk`: appends assistant text to the latest agent bubble or creates a + new agent bubble. +- `onChatReasoningChunk`: creates/appends a reasoning row in the active turn, + before assistant content when needed. +- `onChatToolEvent`: calls `upsertLiveToolEvent()`. +- `onChatDone`: fetches full DB transcript with `getSessionMessages()` and runs + `reconcileStreamedWithDb(prev, dbMessages)`. +- `onChatError`: appends a new content bubble: + +```ts +{ + id: `error-${Date.now()}`, + role: "agent", + content: `Error: ${error}`, +} +``` + +The error bubble has no relationship to the user turn that caused it. + +### DB Mapping + +File: `src/renderer/src/screens/Chat/sessionHistory.ts` + +`dbItemsToChatMessages()` maps DB rows to renderer messages: + +- `user` -> user bubble +- `assistant` -> agent bubble +- `reasoning` -> reasoning row +- `tool_call` -> tool call row +- `tool_result` -> tool result row + +This is still the right path for cold load and restored sessions. + +### Reconciliation + +File: `src/renderer/src/screens/Chat/sessionHistory.ts` + +`reconcileStreamedWithDb()` does a whole-session merge: + +1. Builds a map of streamed rows by reconciliation key. +2. Walks DB rows in canonical DB order. +3. Preserves streamed IDs when DB rows match streamed equivalents. +4. Inserts DB-only rows, especially reasoning/tool rows. +5. Preserves unmatched streamed prefix/suffix rows. +6. Drops some concatenated assistant split artifacts. + +This works for successful turns where all unmatched rows are either useful live +artifacts or benign duplicates. It is risky when unmatched rows are local errors, +because local errors may be appended as suffix rows after a later DB transcript. + +### Restored Session Flow + +File: `src/renderer/src/screens/Layout/Layout.tsx` + +`handleResumeSession()` loads DB rows and calls `setMessages(dbItemsToChatMessages(items))`. +This should remain DB-only for now. It restores persisted user/assistant rows, +reasoning, tool calls, tool results, and attachments. + +It will not restore purely local errors after app restart unless we add a +desktop-side overlay store or Hermes Agent itself persists failed turns. + +## Upstream Hermes Agent Desktop Lessons + +Source: `github.com/NousResearch/hermes-agent/apps/desktop` + +The upstream desktop app avoids this exact class of bugs mostly by avoiding raw +SQLite reconciliation in the renderer. It is an Electron shell that talks to a +`hermes dashboard` backend over gateway APIs and reuses the embedded TUI path. + +The relevant ideas to copy are local transcript invariants, not the full +dashboard architecture. + +### Upstream Message Model + +File: `apps/desktop/src/lib/chat-messages.ts` + +Upstream `ChatMessage` includes: + +- `id` +- `role` +- `parts` +- optional `timestamp` +- optional `pending` +- optional `error` +- optional `branchGroupId` +- optional `hidden` +- optional `attachmentRefs` + +The important part for Hermes One is `error` as metadata, not as normal assistant +text. + +### Upstream Error Handling + +File: `apps/desktop/src/app/session/hooks/use-message-stream.ts` + +Provider/gateway failures are converted into assistant error state by +`failAssistantMessage()`: + +- reuse current stream assistant message if it exists +- otherwise create a local assistant message +- set `error` +- clear `pending` +- clear turn busy state + +It does not append a normal assistant text bubble containing `Error: ...`. + +Upstream also detects completion-shaped errors with `completionErrorText()`. +Errors such as provider/gateway failure text returned through `message.complete` +are converted to assistant errors. + +### Upstream Hydration Guard + +File: `apps/desktop/src/app/session/hooks/use-message-stream.ts` + +At completion, upstream only hydrates from stored session data when: + +- there is no completion error +- there is no inline assistant error +- there is no unresolved user tail +- and the stream did not produce useful assistant payload or final text + +Hermes One should copy this invariant: failed turns should not trigger a broad DB +refresh/merge that can erase or reorder the local failure. + +### Upstream Error Preservation + +File: `apps/desktop/src/lib/chat-messages.ts` + +`preserveLocalAssistantErrors(nextMessages, currentMessages)` preserves local +assistant errors when hydration omits failed turns: + +- if a hydrated assistant reuses the same ID, carry over local `error` +- if a local assistant error has no hydrated equivalent, preserve it +- preserve the preceding local user when needed +- avoid duplicating the local user when the hydrated transcript already contains + equivalent tail user content + +Hermes One needs an adapted version that understands `content` bubbles rather +than upstream's `parts` model. + +## Scope Decision + +Do not migrate Hermes One to upstream's `parts[]` model in this fix. That would +touch rendering, transcript copying, history mapping, and live tool rendering all +at once. + +Do not migrate Hermes One to the upstream dashboard/gateway architecture in this +fix. That is a larger product direction decision. + +Instead: + +- keep Hermes One's current `ChatMessage` union +- add minimal metadata to support local errors and anchoring +- wrap or replace the risky whole-session merge at completion boundaries +- preserve the DB-based cold load behavior + +## Proposed Data Model Changes + +### Extend `ChatBubbleMessage` + +File: `src/renderer/src/screens/Chat/types.ts` + +Add optional fields: + +```ts +export interface ChatBubbleMessage { + id: string; + kind?: "user" | "assistant"; + role: "user" | "agent"; + content: string; + attachments?: Attachment[]; + + /** + * Local or streamed assistant failure metadata. This must not be sent back + * to the model as assistant content. + */ + error?: string; + + /** + * True while an optimistic assistant bubble is active. Useful for replacing + * the pending row with final text or error state. + */ + pending?: boolean; + + /** + * True for renderer-only UI state with no canonical DB row, such as provider + * setup/key failures. + */ + localOnly?: boolean; + + /** + * Renderer-local turn identity. Used only for preserving UI order and local + * failures during a live session. + */ + turnId?: string; +} +``` + +Keep these optional to preserve compatibility with existing tests and DB-loaded +messages. + +### Active Turn State + +Add a small renderer-local active turn structure: + +```ts +interface ActiveTurn { + turnId: string; + userId: string; + startIndex: number; + status: "running" | "failed" | "completed"; +} +``` + +This can live in `Chat.tsx` as a `useRef` and be passed to +`useChatActions()` and `useChatIPC()`. + +This does not need to be persisted. It is only for live-turn anchoring. + +## New Helper Module + +Create a helper module, for example: + +`src/renderer/src/screens/Chat/chatMessages.ts` + +Recommended helpers: + +```ts +export function isBubbleMessage(m: ChatMessage): m is ChatBubbleMessage; +export function isHistoryArtifact(m: ChatMessage): boolean; +export function visibleBubbleText(m: ChatBubbleMessage): string; +export function normalizeMessageText(text: string): string; +export function shouldSendToAgent(m: ChatMessage): m is ChatBubbleMessage; +export function shouldCopyToTranscript(m: ChatMessage): boolean; +export function displayTextForTranscript(m: ChatMessage): string; +export function isAssistantError(m: ChatMessage): m is ChatBubbleMessage; +``` + +`shouldSendToAgent()` should return false for: + +- non-bubble messages +- `localOnly` +- `error` +- hidden/future equivalent if added later +- empty content + +It should return true for persisted or streamed user/assistant content bubbles +that are legitimate conversation context. + +This helper should replace duplicated bubble checks in: + +- `useChatActions.ts` +- `MessageList.tsx` +- `MessageRow.tsx` +- `transcriptUtils.ts` +- `sessionHistory.ts` +- possibly `liveToolEvents.ts` + +Do this gradually; the first implementation can update only the paths needed for +the bug fix. + +## Error Rendering Plan + +### MessageList + +File: `src/renderer/src/screens/Chat/MessageList.tsx` + +Current filtering hides empty content bubbles: + +```ts +return ((m.content as string) || "").trim().length > 0; +``` + +Change visible filtering so assistant error bubbles render even when content is +empty: + +```ts +if (isBubble(m)) { + return Boolean(m.error) || ((m.content as string) || "").trim().length > 0; +} +``` + +### MessageRow + +File: `src/renderer/src/screens/Chat/MessageRow.tsx` + +Add rendering for `msg.error`. + +Suggested behavior: + +- show a visually distinct error bubble +- display `msg.error` +- do not run normal media token parsing for error text +- do not show approval bar for error rows + +Pseudo-flow: + +```tsx +if (msg.role === "agent" && msg.error) { + return ( +
+ {msg.error} +
+ ); +} +``` + +Add CSS only if existing error styles are not enough. + +## Sending History Plan + +File: `src/renderer/src/screens/Chat/hooks/useChatActions.ts` + +Replace: + +```ts +messagesRef.current.filter(hasContent).map(...) +``` + +with: + +```ts +messagesRef.current.filter(shouldSendToAgent).map((m) => ({ + role: m.role, + content: m.content, +})); +``` + +This ensures local errors do not pollute future prompts. + +Important: tool/reasoning rows are already excluded by `hasContent`, so this +should preserve current request-shaping behavior except for excluding local +assistant errors. + +## Sending Turn Creation Plan + +File: `src/renderer/src/screens/Chat/hooks/useChatActions.ts` + +Change `pushUser()` from a void helper to a helper that returns the inserted +message metadata: + +```ts +const pushUser = useCallback((content, idPrefix, attachments) => { + const turnId = `turn-${Date.now()}-${Math.random().toString(36).slice(2)}`; + const userId = `${idPrefix}-${Date.now()}`; + + setMessages((prev) => [ + ...prev, + { + id: userId, + role: "user", + content, + turnId, + ... + }, + ]); + + return { turnId, userId }; +}, ...); +``` + +Better: avoid two `Date.now()` calls for `turnId` and `userId`; compute once. + +In `handleSend()`: + +```ts +setIsLoading(true); +const turn = pushUser(text, "user", attachments); +activeTurnRef.current = { + ...turn, + startIndex: messagesRef.current.length, + status: "running", +}; +onSessionStarted?.(); +await sendToAgent(text, attachments); +``` + +The exact `startIndex` can be approximate because `setMessages` is async. It is +mostly useful for diagnostics and fallback; the stable `userId` and `turnId` are +more important. + +Apply similar turn metadata to: + +- `handleQuickAsk()` +- `handleApprove()` +- `handleDeny()` + +If `/approve` and `/deny` should not be treated as normal chat turns, document +that and avoid active turn changes there. Current code pushes them as user rows, +so they likely should get turn IDs for consistency. + +## Streaming Turn Update Plan + +File: `src/renderer/src/screens/Chat/hooks/useChatIPC.ts` + +Accept an optional `activeTurnRef` argument: + +```ts +activeTurnRef: React.MutableRefObject; +``` + +### `onChatChunk` + +Current behavior appends to the latest agent bubble, regardless of turn. That can +remain for the first pass to avoid changing streaming behavior. + +Optional improvement: + +- when creating a new agent bubble, include `turnId: activeTurnRef.current?.turnId` +- when appending to an existing agent bubble, preserve `turnId` +- if latest agent bubble is an error, do not append content to it; create a new + assistant bubble instead + +This prevents a late chunk from accidentally mutating an error row. + +### `onChatReasoningChunk` + +Preserve current insertion behavior. + +Add `turnId` to newly created reasoning rows only if we later add `turnId` to +history artifacts. Not required for this fix. + +### `onChatToolEvent` + +Preserve current `upsertLiveToolEvent()` behavior. + +Do not change tool rendering or ordering in the first implementation. + +### `onChatError` + +Replace append-only behavior with an anchored helper: + +```ts +setMessages((prev) => markActiveTurnFailed(prev, error, activeTurnRef.current)); +``` + +`markActiveTurnFailed()` should: + +1. Normalize message text: + - raw stored `error` should not include a duplicated `Error: ` prefix unless + the backend already supplied it. +2. If there is a pending/current assistant bubble for this turn: + - set `error` + - set `content: ""` unless there is useful partial content to preserve + - set `pending: false` + - set `localOnly: true` +3. Else insert an assistant error bubble immediately after the active user row. +4. If the active user row cannot be found: + - append the error at the tail as a fallback +5. Set `activeTurnRef.current.status = "failed"` +6. Clear loading/tool progress as today. + +Decision point: partial content plus error. + +Recommended first behavior: + +- if there is partial assistant content and then an error, keep `content` and set + `error` +- render both in `MessageRow`: content first, then error status +- mark `localOnly: true` only if the assistant message has no DB equivalent + +This avoids losing partial streamed output after a late provider failure. + +### `onChatDone` + +Before fetching DB: + +```ts +const activeTurn = activeTurnRef.current; +if (activeTurn?.status === "failed") return; +``` + +After success: + +- set loading false as today +- set active turn status to completed +- fetch DB as today +- call the safer reconciliation wrapper instead of raw `reconcileStreamedWithDb` +- clear `activeTurnRef.current` if it belongs to the completed session/turn + +Do not skip DB refresh for successful turns yet; DB refresh is still needed to +fill reasoning/tool rows that may only exist in `state.db`. + +## Reconciliation Plan + +Do not delete `reconcileStreamedWithDb()` immediately. It already handles useful +cases: + +- DB-only reasoning insertion +- future streamed reasoning ID preservation +- DB-only tool call/result insertion +- duplicate streamed content FIFO matching +- assistant split artifact removal +- DB attachments copied into streamed bubbles + +Instead add a wrapper around it, likely in `sessionHistory.ts`: + +```ts +export function reconcileAfterDbRefresh( + current: ReadonlyArray, + db: ReadonlyArray, + options?: { + activeTurn?: ActiveTurn | null; + }, +): ChatMessage[] { + if (options?.activeTurn?.status === "failed") return [...current]; + + const syncableCurrent = current.filter(isSyncableWithDb); + const localOnly = current.filter(isLocalOnlyPreserved); + + const reconciled = reconcileStreamedWithDb(syncableCurrent, db); + return preserveLocalAssistantErrors(reconciled, current); +} +``` + +### `isSyncableWithDb()` + +Should return false for: + +- assistant bubbles with `error` +- `localOnly` + +Should return true for: + +- user/assistant content bubbles without local error state +- reasoning/tool rows + +Reasoning/tool rows may be local live artifacts and should continue to be +matched/deduped against DB where possible. + +### `preserveLocalAssistantErrors()` + +Adapt upstream's helper to Hermes One. + +Input: + +- `nextMessages`: DB-reconciled messages +- `currentMessages`: local current transcript + +Algorithm: + +1. Build `existingIds` from `nextMessages`. +2. Build a normalized tail-user matcher from `nextMessages`. +3. Find all local assistant error bubbles: + - `role === "agent"` + - `error` present + - not hidden if hidden is later added +4. For each local error: + - if `nextMessages` already contains same ID, merge `error` into that row + - otherwise find nearest preceding visible user in `currentMessages` + - find matching user in `nextMessages` by: + - same `id`, if available + - same `turnId`, if available + - normalized content plus attachment refs/content + - insert the error immediately after the matched user and any local/DB rows + that belong before assistant response for that turn + - if no matched DB user exists, preserve the local user+error pair together +5. Preserve order among multiple local failed turns. +6. Do not append all failures blindly to the end. + +The hardest detail is where to insert error relative to DB rows that follow the +matched user. Recommended first pass: + +- insert immediately after the matched user if the DB does not contain an + assistant response before the next user +- if DB already has an assistant response for that same user, do not preserve the + error unless it has the same ID as that assistant row + +That avoids creating impossible turns like: + +```text +user A +assistant success A +assistant error A +``` + +For the bad-key-then-good-provider case, DB likely contains: + +```text +user good +assistant good +``` + +while current contains: + +```text +user bad +assistant error bad +user good +assistant good +``` + +The result should be: + +```text +user bad +assistant error bad +user good +assistant good +``` + +### Split Artifact Logic + +Keep `buildDbAssistantSplitSequences()` and `isCoveredByDbBubbleSplit()` for now. +These fix a separate class of duplicated assistant text around tool calls. + +But do not allow split-artifact logic to drop local error rows: + +- local errors should be filtered out before split artifact checks +- error rows should have `reconciliationKey() === null` + +### Reconciliation Key Changes + +Update `reconciliationKey()`: + +- if bubble has `error` or `localOnly`, return `null` +- otherwise keep current role/content matching + +This prevents local errors from matching real assistant text. + +## Completion Error Text Plan + +Upstream detects provider/gateway errors that arrive as final assistant text. +Hermes One currently can surface errors through `onError`, but failures may also +arrive as normal completion content depending on API behavior. + +Add a helper similar to upstream: + +```ts +const COMPLETION_ERROR_PATTERNS = [ + /^API call failed after \d+ retries:/i, + /^HTTP\s+\d{3}\b/i, + /^(Provider|Gateway)\s+error:/i, + /missing .*api.*key/i, + /api key.*missing/i, + /unauthorized|forbidden|invalid api key/i, +]; +``` + +Use cautiously. False positives could turn legitimate assistant text into error +UI. Recommended phase: + +- do not use this in the first patch unless there is a known repro where Hermes + One receives provider errors as `chat-done` content +- add tests before enabling + +## Restored Session Behavior + +The proposed changes preserve restored session artifacts that are persisted in +`state.db`. + +Cold restored sessions still flow through: + +```text +getSessionMessages(sessionId) + -> expandRowsToHistory() + -> dbItemsToChatMessages() + -> setMessages() +``` + +This restores: + +- user messages +- assistant messages +- reasoning/thought rows +- tool call rows +- tool result rows +- DB-backed attachments + +It does not restore local provider errors after app restart if Hermes Agent never +wrote them to `state.db`. That is already true today. + +If restart-persistent local errors are required, add a separate phase: + +### Optional Later Phase: Desktop Session Overlay + +Persist local-only events to a desktop-owned overlay file, e.g. + +```text +profileHome(activeProfile)/desktop/session-overlays.json +``` + +Shape: + +```ts +interface SessionOverlay { + sessionId: string; + localEvents: Array<{ + id: string; + turnId: string; + afterUserContent: string; + afterUserAttachments?: string[]; + role: "agent"; + error: string; + createdAt: number; + }>; +} +``` + +On restored session load: + +- load DB transcript +- overlay local error events by matching the nearest user +- prune overlays when a session is deleted + +Do not implement this in the first stabilization patch unless the product +requires failed provider rows to survive app restart. + +## Streaming Behavior Expectations + +Current streaming should remain mostly unchanged. + +Preserved: + +- assistant text still streams through `chat-chunk` +- reasoning still streams through `chat-reasoning-chunk` +- live tools still stream through `chat-tool-event` +- successful completion still fetches DB rows to fill missing artifacts +- session ID handling remains unchanged + +Changed: + +- failures become error metadata, not normal assistant content +- failed turns skip broad DB reconciliation +- local errors render even with empty content +- local errors are excluded from future LLM request history + +Potential behavior improvement: + +- partial output followed by error can be displayed as partial content plus + error status, instead of losing one or the other + +## Test Plan + +### Unit Tests: Message Helpers + +New or existing test file: + +`tests/chat-message-helpers.test.ts` + +Cases: + +- `shouldSendToAgent()` includes normal user/assistant content. +- `shouldSendToAgent()` excludes local assistant error. +- `shouldSendToAgent()` excludes empty pending assistant. +- `shouldSendToAgent()` excludes reasoning/tool rows. +- transcript copying includes a readable error line if desired. + +### Unit Tests: Error Preservation + +New or existing file: + +`tests/reconcile-streamed-with-db.test.ts` + +Add cases: + +1. Local failed turn before later successful DB turn: + +```text +current: user bad, assistant error bad, user good, assistant good streamed +db: user good, assistant good +expect: user bad, assistant error bad, user good, assistant good +``` + +2. Local failed turn where DB has the user but no assistant: + +```text +current: user bad, assistant error bad +db: user bad +expect: user bad, assistant error bad +``` + +3. Repeated prompt text: + +```text +current: user "hi", error, user "hi", assistant "ok" +db: user "hi", assistant "ok" +expect: do not attach error after successful "hi" +``` + +This may require `turnId` or positional heuristics to pass robustly. + +4. Existing DB artifacts still insert: + +```text +current: user, assistant content +db: user, reasoning, tool_call, tool_result, assistant content +expect: reasoning/tools present, streamed IDs preserved +``` + +5. Local error is not dropped by split artifact logic. + +6. Local error is not appended at the tail after unrelated later DB rows. + +### Unit Tests: IPC/Error Flow + +If the current test harness supports renderer hooks, add tests around +`useChatIPC()`: + +- `onChatError` marks current active turn failed and inserts error after user. +- `onChatDone` skips DB reconciliation if active turn failed. +- `onChatDone` still reconciles if active turn succeeded. + +If hook tests are expensive, keep this behavior in pure helper functions and +unit-test those helpers directly. + +### Existing Tests To Keep Passing + +Run: + +```bash +npm test -- reconcile-streamed-with-db +npm test -- transcriptUtils +npm test -- session-history +npm test -- sessions-history-items +``` + +Then broader: + +```bash +npm test +``` + +Adjust exact commands to the repo's configured Vitest filters. + +## Manual QA Plan + +### Scenario 1: Bad Provider Key Then Good Provider + +1. Configure a provider with an invalid/missing key. +2. Send `hello with bad provider`. +3. Confirm visible transcript: + - user prompt + - assistant error directly after that prompt +4. Switch to a working provider in the same session. +5. Send `hello with good provider`. +6. Confirm visible transcript order: + - bad user + - bad assistant error + - good user + - good assistant response +7. Confirm the bad error did not move after the good response. +8. Confirm request history for the good provider did not include the bad + provider error as assistant text. + +### Scenario 2: Reasoning/Tool Successful Turn + +1. Use a model/tool path that produces reasoning and tool calls. +2. Confirm live streaming still works. +3. Confirm DB refresh still inserts missing reasoning/tool results after done. +4. Confirm no duplicate assistant split text appears. + +### Scenario 3: Partial Output Then Error + +1. Force a stream that emits some text and then fails. +2. Confirm partial content remains visible if useful. +3. Confirm error status is visible and anchored. +4. Confirm next successful turn does not reorder the failed turn. + +### Scenario 4: Restored Session + +1. Open an existing persisted session. +2. Confirm user/assistant rows restore. +3. Confirm reasoning rows restore. +4. Confirm tool calls/results restore. +5. Confirm attachments restore. +6. Confirm local-only errors from before app restart are not expected unless the + optional overlay phase is implemented. + +## Phased Implementation Plan + +This should be implemented in phases. The failure touches message typing, +streaming, rendering, request-history construction, and DB reconciliation. Doing +everything in one patch would make regressions hard to isolate. + +Each phase should leave the app buildable and should preserve current streaming +behavior unless the phase explicitly changes it. + +### Phase 0: Baseline Audit And Repro Tests + +Goal: + +- Capture the current bug as failing tests before changing behavior. +- Confirm the implementation agent understands the current boundaries. + +Files to inspect: + +- `src/renderer/src/screens/Chat/types.ts` +- `src/renderer/src/screens/Chat/hooks/useChatActions.ts` +- `src/renderer/src/screens/Chat/hooks/useChatIPC.ts` +- `src/renderer/src/screens/Chat/sessionHistory.ts` +- `src/renderer/src/screens/Chat/MessageList.tsx` +- `src/renderer/src/screens/Chat/MessageRow.tsx` +- `tests/reconcile-streamed-with-db.test.ts` + +Implementation work: + +- Add tests for the known bad-key-then-good-provider ordering bug. +- Add tests proving local error text is currently preserved but lands in the + wrong place, if that is the existing behavior. +- Add tests showing existing successful DB artifact insertion still works: + reasoning, tool calls, tool results, and split assistant dedupe. + +Recommended test cases: + +```text +current: user bad, assistant local error, user good, assistant good streamed +db: user good, assistant good +expect: user bad, assistant local error, user good, assistant good +``` + +```text +current: user bad, assistant local error +db: user bad +expect: user bad, assistant local error +``` + +```text +current: user "hi", assistant local error, user "hi", assistant good +db: user "hi", assistant good +expect: error remains attached to first "hi", not the later successful "hi" +``` + +Acceptance criteria: + +- At least one new test fails on the current implementation and describes the + real reported bug. +- Existing reconciliation tests still pass before behavior changes, except for + any newly added bug repro tests. + +Stop/rollback guidance: + +- If the bug cannot be reproduced in a pure function, do not proceed directly to + UI changes. First extract the currently implicit merge behavior into a helper + that can be tested. + +### Phase 1: Add Message Metadata Without Behavior Changes + +Goal: + +- Make the type system capable of representing local errors as metadata. +- Avoid changing runtime behavior yet. + +Primary files: + +- `src/renderer/src/screens/Chat/types.ts` + +Implementation work: + +- Add optional fields to `ChatBubbleMessage`: + - `error?: string` + - `pending?: boolean` + - `localOnly?: boolean` + - `turnId?: string` +- Keep all fields optional. +- Do not change `onChatError` yet. +- Do not change reconciliation yet. + +Acceptance criteria: + +- TypeScript still compiles. +- No renderer behavior changes. +- Existing tests pass. + +Stop/rollback guidance: + +- If adding these fields creates type noise in many places, keep them limited to + `ChatBubbleMessage`; do not add metadata to reasoning/tool rows in this phase. + +### Phase 2: Centralize Chat Message Helpers + +Goal: + +- Stop spreading ad hoc checks like `"content" in m` and `!kind` across critical + paths before changing error semantics. + +Primary files: + +- new `src/renderer/src/screens/Chat/chatMessages.ts` +- `src/renderer/src/screens/Chat/hooks/useChatActions.ts` +- `src/renderer/src/screens/Chat/MessageList.tsx` +- `src/renderer/src/screens/Chat/transcriptUtils.ts` + +Implementation work: + +- Add helper functions: + - `isBubbleMessage()` + - `isAssistantError()` + - `normalizeMessageText()` + - `visibleBubbleText()` + - `shouldSendToAgent()` + - `shouldCopyToTranscript()` + - `displayTextForTranscript()` +- Initially wire only low-risk paths, or use the helpers in tests first. +- `shouldSendToAgent()` must exclude: + - assistant errors + - `localOnly` messages + - empty bubbles + - reasoning/tool rows + +Tests: + +- Add unit tests for helper behavior. +- Include a local assistant error and prove it is not sendable. + +Acceptance criteria: + +- No visible UI changes yet. +- Request-history filtering can be changed in a later phase by swapping to the + helper. + +Stop/rollback guidance: + +- If helper adoption becomes too broad, only adopt `shouldSendToAgent()` in + `useChatActions.ts` and leave display helpers for later. + +### Phase 3: Exclude Local Errors From Future LLM History + +Goal: + +- Fix the context pollution issue independently from UI ordering. + +Primary file: + +- `src/renderer/src/screens/Chat/hooks/useChatActions.ts` + +Implementation work: + +- Replace the current history construction: + +```ts +messagesRef.current.filter(hasContent).map(...) +``` + +with: + +```ts +messagesRef.current.filter(shouldSendToAgent).map(...) +``` + +- Keep the outgoing shape unchanged: + +```ts +{ + role: m.role, + content: m.content, +} +``` + +Tests: + +- Unit-test `shouldSendToAgent()`. +- If practical, add a hook-level test that verifies a local assistant error is + not included in `window.hermesAPI.sendMessage()` history. + +Acceptance criteria: + +- Normal user/assistant content is still sent. +- Reasoning/tool rows are still excluded. +- Local assistant errors are excluded. + +Stop/rollback guidance: + +- If hook-level testing is expensive, keep this phase covered by helper tests and + one focused manual check. + +### Phase 4: Track Active Turns + +Goal: + +- Give local errors an anchor so they can be inserted near the user prompt that + caused them. + +Primary files: + +- `src/renderer/src/screens/Chat/Chat.tsx` +- `src/renderer/src/screens/Chat/hooks/useChatActions.ts` +- `src/renderer/src/screens/Chat/hooks/useChatIPC.ts` + +Implementation work: + +- Add `activeTurnRef` in `Chat.tsx`: + +```ts +const activeTurnRef = useRef(null); +``` + +- Define `ActiveTurn` near chat types or in a local hook module: + +```ts +interface ActiveTurn { + turnId: string; + userId: string; + startIndex: number; + status: "running" | "failed" | "completed"; +} +``` + +- Pass `activeTurnRef` into `useChatActions()` and `useChatIPC()`. +- Change `pushUser()` to create and return stable `{ turnId, userId }`. +- Set `activeTurnRef.current` immediately before sending to the agent. +- Add `turnId` to the optimistic user bubble. + +Important constraints: + +- Do not change streaming chunk behavior in this phase. +- Do not change error rendering in this phase. + +Tests: + +- If helpers are extracted, test `createTurnIds()` or equivalent. +- Otherwise rely on Phase 5 behavior tests. + +Acceptance criteria: + +- Sending messages still works. +- Queued messages still drain. +- `/approve`, `/deny`, and `/btw` behavior is unchanged except for optional + `turnId` metadata. + +Stop/rollback guidance: + +- If passing `activeTurnRef` through hooks creates too much churn, put turn state + in a small `useActiveChatTurn()` hook and pass only the needed callbacks. + +### Phase 5: Render Error Metadata + +Goal: + +- Make the UI capable of showing assistant error metadata before switching + `onChatError` to produce it. + +Primary files: + +- `src/renderer/src/screens/Chat/MessageList.tsx` +- `src/renderer/src/screens/Chat/MessageRow.tsx` +- possibly chat CSS files + +Implementation work: + +- Update `MessageList` filtering so empty-content error bubbles remain visible. +- Update `MessageRow` so `msg.error` renders as an error state. +- Ensure approval detection does not run on error rows. +- Ensure media parsing does not treat error text as normal assistant markdown + unless intentionally desired. + +Tests: + +- Add render test if existing setup makes it easy. +- Otherwise unit-test visibility helper if `MessageList` filtering is extracted. + +Acceptance criteria: + +- A message with `{ role: "agent", content: "", error: "OpenRouter 403" }` + renders visibly. +- Empty non-error assistant placeholders remain hidden. + +Stop/rollback guidance: + +- If styling becomes contentious, render error text inside the existing agent + bubble first, with a class hook for later visual polish. + +### Phase 6: Replace Loose Error Append With Anchored Error Metadata + +Goal: + +- Fix the local error model. + +Primary file: + +- `src/renderer/src/screens/Chat/hooks/useChatIPC.ts` + +Supporting helper: + +- `markActiveTurnFailed(messages, error, activeTurn)` + +Implementation work: + +- Replace `onChatError` append-only behavior with `markActiveTurnFailed()`. +- The helper should: + - find the active user row by `activeTurn.userId` or `turnId` + - find an existing assistant bubble in the active turn if present + - preserve partial content if present + - set `error` + - set `pending: false` + - set `localOnly: true` when there is no DB equivalent + - insert the error immediately after the active user if no assistant row exists + - append as fallback only when no anchor can be found +- Set `activeTurnRef.current.status = "failed"`. +- Preserve existing `setToolProgress(null)` and `setIsLoading(false)`. + +Tests: + +- Pure helper tests for: + - no assistant yet -> insert after active user + - partial assistant exists -> mark partial assistant as errored + - active user missing -> append fallback + - repeated user text -> use ID/turnId rather than text + +Acceptance criteria: + +- Provider error appears directly after the prompt that caused it. +- Error is not represented as normal `content: "Error: ..."` assistant text. + +Stop/rollback guidance: + +- If preserving partial content complicates rendering, first support empty-content + error rows. Add partial-content preservation in a follow-up. + +### Phase 7: Add Safe DB Refresh Wrapper + +Goal: + +- Keep DB artifact fill-in for successful turns while protecting local-only + errors from global reordering. + +Primary file: + +- `src/renderer/src/screens/Chat/sessionHistory.ts` + +Implementation work: + +- Add `reconcileAfterDbRefresh(current, db, options)`. +- Keep `reconcileStreamedWithDb()` intact initially. +- The wrapper should: + - return current unchanged if `activeTurn.status === "failed"` + - remove local-only/error bubbles from the DB-syncable input + - run `reconcileStreamedWithDb(syncableCurrent, db)` + - call `preserveLocalAssistantErrors(reconciled, current)` +- Update `reconciliationKey()` so local error bubbles return `null`. + +Tests: + +- Existing reconciliation tests must still pass. +- New failed-turn tests must pass. +- DB-only reasoning/tool insertion tests must still pass. + +Acceptance criteria: + +- Successful turns still get DB-only reasoning/tool rows. +- Failed local errors do not move to the tail after later successful DB refresh. + +Stop/rollback guidance: + +- If the wrapper reveals too many edge cases, keep raw + `reconcileStreamedWithDb()` as an escape hatch and gate the wrapper only around + transcripts that contain local errors. + +### Phase 8: Wire Safe Reconciliation Into `onChatDone` + +Goal: + +- Change the runtime completion path after the pure reconciliation behavior is + tested. + +Primary file: + +- `src/renderer/src/screens/Chat/hooks/useChatIPC.ts` + +Implementation work: + +- In `onChatDone`: + - set session ID as today + - clear loading/tool progress as today + - if active turn is failed, skip DB fetch/reconciliation + - otherwise fetch DB messages + - call `reconcileAfterDbRefresh(prev, dbMessages, { activeTurn })` + - mark active turn completed/clear it + +Tests: + +- Hook-level tests if feasible. +- Otherwise rely on pure helper tests and manual QA. + +Acceptance criteria: + +- Successful turns still refresh artifacts. +- Failed turns do not trigger a DB merge that can reorder local errors. + +Stop/rollback guidance: + +- If skipping DB fetch on failed turn hides something important, change the guard + to fetch DB but run only `preserveLocalAssistantErrors()` without full + reconciliation. + +### Phase 9: Restored Session Verification + +Goal: + +- Confirm cold/restored sessions are not regressed. + +Primary files: + +- `src/renderer/src/screens/Layout/Layout.tsx` +- `src/renderer/src/screens/Chat/sessionHistory.ts` +- `src/main/sessions.ts` +- `src/main/ssh-remote.ts` + +Implementation work: + +- Prefer no code changes in this phase. +- Verify `handleResumeSession()` still uses pure DB: + +```ts +setMessages(dbItemsToChatMessages(items)); +``` + +Tests: + +- `session-history-mapping` +- `sessions-history-items` +- any SSH session history tests if present + +Acceptance criteria: + +- Persisted reasoning rows restore. +- Persisted tool calls restore. +- Persisted tool results restore. +- Persisted prompt image attachments restore. +- Local-only provider errors are not expected to survive restart unless the + optional overlay phase is later implemented. + +Stop/rollback guidance: + +- If users require restart-persistent error rows, do not overload + `state.db` reconciliation. Add the optional desktop overlay phase separately. + +### Phase 10: Manual QA Gate + +Goal: + +- Exercise real IPC/streaming behavior that unit tests may not cover. + +Manual scenarios: + +- Bad provider key, then good provider in same session. +- Missing key error, then same prompt after switching provider. +- Successful reasoning-heavy model response. +- Successful tool-heavy response. +- Partial stream followed by failure. +- Resume existing session with reasoning/tools/attachments. +- Queue messages while one turn is loading. +- Abort a turn. +- `/approve` and `/deny` if approval flow is active. + +Acceptance criteria: + +- No error rows jump to the bottom after a later success. +- No missing user/assistant lines. +- No duplicate split assistant artifacts. +- Reasoning/tool rows still appear. +- Local error text is not included in subsequent model context. +- Restored sessions still show persisted artifacts. + +Stop/rollback guidance: + +- If normal streaming regresses, first revert only Phase 8 wiring and keep the + type/helper/test work. That should restore current runtime behavior while + preserving most preparatory work. + +## Optional Later Phase: Persistent Local Error Overlay + +This is not required for the first stabilization fix. + +Goal: + +- Preserve local-only provider errors across app restart, even when Hermes Agent + never wrote a failed turn to `state.db`. + +Possible storage: + +```text +profileHome(activeProfile)/desktop/session-overlays.json +``` + +Possible event shape: + +```ts +interface LocalErrorOverlayEvent { + id: string; + sessionId: string; + turnId: string; + afterUserContent: string; + afterUserAttachmentIds?: string[]; + error: string; + createdAt: number; +} +``` + +Implementation notes: + +- Write overlay event when a local provider error is created. +- On restored session load, load DB transcript first. +- Overlay local errors by matching the nearest user row. +- Prune overlay events when sessions are deleted. +- Keep this separate from DB reconciliation so synthetic desktop UI state does + not pretend to be canonical Hermes Agent history. + +## Risks and Mitigations + +### Risk: Error Rows Disappear Because Empty Bubbles Are Filtered + +Mitigation: + +- Update `MessageList` visibility filter before changing `onChatError`. +- Add test for empty-content error rendering. + +### Risk: Local Error Is Sent Back To Model + +Mitigation: + +- Centralize request-history filtering in `shouldSendToAgent()`. +- Unit-test it. + +### Risk: Successful Reasoning/Tool Artifacts Stop Appearing + +Mitigation: + +- Keep `reconcileStreamedWithDb()` for successful turns. +- Add regression test with DB-only reasoning/tool rows. + +### Risk: Duplicate User Rows With Same Text + +Mitigation: + +- Prefer `id` and `turnId` over text matching when possible. +- Use text matching only as fallback. +- Add repeated-prompt tests. + +### Risk: Partial Assistant Output Followed By Error Gets Lost + +Mitigation: + +- Preserve partial `content` when marking a pending assistant as errored. +- Render content plus error status. + +### Risk: Too Many Files Change At Once + +Mitigation: + +- Implement in phases. +- Keep upstream `parts[]` model out of scope. +- Keep restored-session DB mapping unchanged. + +## Success Criteria + +The implementation is successful when: + +- A provider/key failure creates an anchored assistant error, not a loose text + bubble. +- A later successful turn in the same session does not move, drop, or duplicate + the failed turn. +- Local error text is not sent as assistant history to the next model/provider. +- Successful turns still receive DB-only reasoning, tool calls, tool results, + and attachment metadata. +- Restored sessions still load all persisted DB artifacts. +- Existing split-assistant/tool-call deduplication behavior remains intact. +- The fix is covered by unit tests that reproduce the bad-provider-then-good- + provider case. diff --git a/docs/reconciliation-regression-playbook.md b/docs/reconciliation-regression-playbook.md new file mode 100644 index 0000000..51ae45c --- /dev/null +++ b/docs/reconciliation-regression-playbook.md @@ -0,0 +1,270 @@ +# Chat Reconciliation Regression Playbook + +This playbook is the repeatable gate for the sandboxed Hermes One reconciliation work. +Run it after Hermes One changes, Hermes Agent engine updates, compatibility +addon changes, or remote/SSH lab changes. + +## Scope + +The goal is to verify that Hermes One uses the dashboard event stream as the +active-turn source of truth without losing behavior that existed in the legacy +desktop app: + +- ordered assistant output, reasoning, tool calls, tool results, errors, and + artifacts; +- restored sessions that match live sessions; +- successful continuation after restoring a session; +- recovery from failed provider turns; +- local, Remote HTTP, and SSH dashboard parity; +- legacy fallback still available where configured. + +## Automated Gate + +Run from the separate development worktree: + +```powershell +cd C:\Users\pmos6\Documents\Claude\Projects\Hermes-Desktop-reconcile + +npm run typecheck +npm test -- --run tests/remote-sessions.test.ts tests/remote-metadata.test.ts tests/remote-models.test.ts tests/dashboard-chat-transport.test.ts tests/dashboard-event-adapter.test.ts tests/live-tool-events.test.ts tests/live-reasoning-events.test.ts tests/tool-activity-group-title.test.ts tests/reconcile-streamed-with-db.test.ts tests/session-history-mapping.test.ts tests/sessions-history-items.test.ts tests/sessions-decode-content.test.ts src/renderer/src/screens/Chat/mediaUtils.test.ts src/renderer/src/screens/Chat/hooks/useChatIPC.test.tsx tests/chat-messages.test.ts tests/session-continuation-store.test.ts tests/dashboard-remote.test.ts tests/dashboard-launch.test.ts tests/dashboard-gateway-client.test.ts tests/hermes-agent-compat.test.ts tests/run-stream.test.ts src/renderer/src/screens/Sessions/Sessions.test.tsx +``` + +Expected result: + +- TypeScript node and web checks pass. +- All listed test files pass. + +When time permits, also run the entire suite: + +```powershell +npm test -- --run +``` + +## Lab Setup + +Start the disposable Remote HTTP target: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\remote-lab.ps1 up +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\remote-lab.ps1 status +``` + +Start the SSH tunnel lab: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\ssh-lab.ps1 up +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\ssh-lab.ps1 status +``` + +Start the sandboxed Hermes One instance: + +```powershell +npm run dev:sandbox +``` + +Pass criteria: + +- Remote status shows dashboard status, dashboard session auth, and legacy + OpenAI models auth as OK. +- SSH status reaches the remote dashboard through the tunnel. +- Hermes One title bar says `Hermes One`. +- Settings shows the active connection mode and active chat transport. + +## Connection Matrix + +Run the manual cases below in these modes: + +- Local, dashboard auto. +- Local, dashboard forced. +- Local, legacy fallback. +- Remote HTTP, dashboard auto. +- Remote HTTP, dashboard forced. +- Remote HTTP, legacy fallback. +- SSH, dashboard auto. +- SSH, dashboard forced. +- SSH, legacy fallback. + +For auto modes, Settings should show the resolved active path. Switching between +Local, Remote HTTP, and SSH should refresh: + +- model selector rows; +- Models page rows; +- Sessions page rows; +- Settings Hermes Agent metadata. + +Remote HTTP and SSH should show models configured on the remote Hermes Agent +side, not the local desktop's model library. + +## Manual Cases + +### 1. Clean Text Turn + +Prompt: + +```text +Reply with exactly TEXT_OK and no tools. +``` + +Pass criteria: + +- Streaming starts promptly. +- Final assistant message is present once. +- Restoring the session shows the user prompt and assistant answer once. + +### 2. Bad Provider Then Recovery + +Switch to a known-bad model/provider. + +Prompt: + +```text +Live regression bad provider turn. Reply with BAD_KEY_SHOULD_FAIL. +``` + +Then switch to a known-good model/provider in the same visible session. + +Prompt: + +```text +Live regression recovery after bad provider. Reply with exactly RECOVERY_AFTER_BAD_OK and do not mention BAD_KEY_SHOULD_FAIL. Do not use tools. +``` + +Pass criteria: + +- Failed turn appears as an error in sequence. +- Good-provider recovery does not repeat the bad-provider error. +- Restored session keeps both user prompts, the error, and the recovery answer + in order. +- Repeating failed/non-failed/failed/non-failed turns keeps every semi-session + boundary in order. + +### 3. Reasoning And Interleaved Output + +Use a reasoning-heavy model such as DeepSeek V4 Pro. + +Prompt: + +```text +Think briefly, then answer in two short sentences. Mention the words FIRST and SECOND in separate sentences. +``` + +Pass criteria: + +- Reasoning/thought blocks appear when the backend emits them. +- Intermediate assistant output stays in sequence with reasoning and tools. +- Restored session preserves reasoning blocks and assistant text order. + +### 4. Tool-Heavy Image Generation + +Use the AI Playground / ComfyUI skill. + +Prompt: + +```text +Generate an image of a toy duck in a bathtub using AI Playground / ComfyUI. Save it to the media folder if the workflow supports that, and show me the resulting file path. +``` + +Pass criteria: + +- Tool calls and tool results stream in order when emitted. +- Sequential tool calls are grouped as `N tools called`. +- Inside each group, each tool call is paired with its matching result. +- Final answer does not duplicate the same image because of markdown/path + parsing. +- Existing local Windows paths render as media when they exist. +- Remote/SSH paths under `/opt/data/images` render through the dashboard media + endpoint. +- Restored session shows the same grouped calls/results and the same media. + +### 5. Pasted Image Prompt + +Paste an image into the prompt box. + +Prompt: + +```text +What is this? +``` + +Pass criteria: + +- The user bubble appears once. +- The pasted image thumbnail appears in the user bubble. +- Hermes Agent fallback text such as `[The user attached an image but analysis failed.]` + is not shown as user-visible prompt content. +- The same session restored from Sessions still shows the image thumbnail, not + raw fallback text. +- Continuing the restored session does not duplicate the prior pasted-image + prompt. + +### 6. Remote Vision Tool + +In Remote HTTP and SSH dashboard modes, ask about a known image using a text-only +chat model plus configured auxiliary vision. + +Prompt: + +```text +Use vision_analyze to describe the attached image in one short sentence. +``` + +Pass criteria: + +- `vision_analyze` returns semantic visual content, not just pixel statistics. +- The remote lab resolves auxiliary vision to OpenRouter + `google/gemini-3-flash-preview` when an OpenRouter key is available. + +Optional container smoke test: + +```powershell +docker exec hermes-two-remote-lab-agent sh -lc 'cd /opt/hermes && HERMES_HOME=/opt/data /opt/hermes/.venv/bin/python3 - <<\"PY\" +import asyncio +from tools.vision_tools import vision_analyze_tool +async def main(): + out = await vision_analyze_tool("/opt/data/images/duck_bathtub.png", "Describe this image in one short sentence.") + print(out[:1200]) +asyncio.run(main()) +PY' +``` + +### 7. Session Search, Restore, Continue + +For each connection mode: + +1. Open Sessions. +2. Restore the newest session from that mode. +3. Verify the visible transcript matches the original live transcript. +4. Send: + +```text +Continue this session with exactly CONTINUE_OK and no tools. +``` + +Pass criteria: + +- Sessions list belongs to the active connection mode. +- Session restore does not mix local, Remote HTTP, and SSH sessions. +- Continuing a restored dashboard session does not produce `session not found`. +- Continuing does not write the remote/SSH session into local-only history. + +## Known Upstream Limitations + +- Gemini failures in the current lab have been traced to Hermes Agent upstream + behavior, not Hermes One dashboard reconciliation. +- Plain Remote HTTP cannot be patched by Hermes One unless the target exposes a + future deploy endpoint or is also reachable over SSH. +- The remote lab intentionally bridges to this Windows host's AI Playground + ComfyUI for testing. That is not normal remote deployment behavior. + +## Exit Criteria + +Before asking for review or preparing a PR: + +- Automated gate passes. +- Remote and SSH lab health checks pass. +- At least one dashboard-mode live pass succeeds for Local, Remote HTTP, and + SSH. +- Legacy fallback is checked at least once after any change that touches legacy + IPC or `/v1` paths. +- Any known failure is classified as Hermes One, Hermes Agent upstream, lab + setup, or provider/service behavior. diff --git a/docs/remote-access-lab.md b/docs/remote-access-lab.md new file mode 100644 index 0000000..bcbb71f --- /dev/null +++ b/docs/remote-access-lab.md @@ -0,0 +1,90 @@ +# Remote Access Lab + +This lab creates a disposable remote Hermes target for the sandboxed Hermes One instance without +touching the normal Hermes One worktree, config, or database. + +## Shape + +- Lab state lives in `.sandbox/remote-lab/hermes-home`. +- Containers are named `hermes-two-remote-lab-*`. +- The desktop connects to one URL: `http://127.0.0.1:19080`. +- A small nginx proxy routes: + - `/api/*` and `/api/ws` to the Hermes dashboard service in the agent container. + - `/v1/*` to the OpenAI-compatible API server in the same agent container. +- One disposable token is used for both: + - `X-Hermes-Session-Token` dashboard auth. + - `Authorization: Bearer ...` legacy API auth. +- The Hermes Agent image is built from the bundled checkout at + `.sandbox/hermes-home/hermes-agent` into `hermes-two-remote-lab-agent:local`, + because the public `nousresearch/hermes-agent:latest` image can lag the + bundled dashboard auth contract. +- The lab copies working model configuration, then strips messaging/webhook + platform credentials and opts out of bundled skill sync to avoid side effects. +- For regression testing only, the lab exports `COMFYUI_HOST` as + `http://host.docker.internal:49000` so Remote HTTP and SSH-dashboard tests can + use this Windows machine's AI Playground ComfyUI. Normal remote deployments + should not rely on access to connecting-host resources. +- Remote-generated images should be copied into `/opt/data/images` and surfaced + as `MEDIA:/opt/data/images/.png` when possible. That path is under the + Hermes home media roots exposed by the upstream dashboard `/api/media` + endpoint. +- If the copied config has an `OPENROUTER_API_KEY`, the lab pins + `auxiliary.vision` to OpenRouter with `google/gemini-3-flash-preview`. This + keeps `vision_analyze` on a vision-capable auxiliary model even when the + active chat model is a text-only custom provider such as DeepSeek. + +## Commands + +Run from the separate development worktree: + +```powershell +cd C:\Users\pmos6\Documents\Claude\Projects\Hermes-Desktop-reconcile + +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\remote-lab.ps1 init +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\remote-lab.ps1 up +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\remote-lab.ps1 status +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\remote-lab.ps1 configure-desktop +``` + +Stop the lab: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\remote-lab.ps1 down +``` + +Remove all lab state: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\remote-lab.ps1 clean +``` + +## Expected Probes + +`status` should show three OK checks: + +- `dashboard status` +- `dashboard sessions auth` +- `legacy OpenAI models auth` + +If Docker Desktop returns pipe/API 500 errors or hangs on `docker ps`, restart +Docker Desktop or run: + +```powershell +wsl --shutdown +``` + +Then reopen Docker Desktop and retry `scripts\remote-lab.ps1 up`. + +The first `up` may take several minutes because Docker builds the local Hermes +Agent image. Later starts reuse the image cache. + +## Hermes One Sandbox Settings + +After `configure-desktop`, the sandboxed Hermes One instance is set to: + +- Connection mode: Remote +- Remote URL: `http://127.0.0.1:19080` +- Chat transport: Auto + +That is the intended regression-test mode: dashboard first, legacy fallback +available through the same remote URL. diff --git a/docs/ssh-dashboard-transport.md b/docs/ssh-dashboard-transport.md new file mode 100644 index 0000000..2c2acd1 --- /dev/null +++ b/docs/ssh-dashboard-transport.md @@ -0,0 +1,97 @@ +# SSH dashboard transport — design for full support (issue #667) + +Status: **design / not yet implemented.** A reliable legacy-HTTP fallback shipped first (see +"Shipped now" below). This document captures the design for making the _dashboard_ chat transport +(profile switching, session history, slash commands, background prompts) work over an SSH tunnel. + +## Background: why SSH dashboard chat is broken + +The desktop's dashboard chat transport speaks WebSocket JSON-RPC at **`/api/ws`** +(`src/renderer/src/screens/Chat/dashboardGatewayClient.ts`, URL built in `src/main/dashboard.ts`). + +In `hermes-agent`, `/api/ws` is served **only** by `hermes dashboard` +(`hermes_cli/web_server.py` → `start_server`, gated by `_DASHBOARD_EMBEDDED_CHAT_ENABLED = True`). It is +**never** served by `hermes gateway` (`gateway/...`, the api_server, which serves `/v1/chat/completions`, +`/health`, etc.). + +But SSH mode today: + +1. Starts `hermes gateway start` on the remote — `buildGatewayStartCommand` in `src/main/ssh-remote.ts`. +2. Tunnels the **gateway** port (`config.remotePort`, default 8642) — `ensureSshTunnel` in + `src/main/ssh-tunnel.ts`. +3. Connects `ws://127.0.0.1:{tunnelPort}/api/ws` — which 404s on the gateway. + +A second, independent blocker: `/api/ws` authenticates with `HERMES_DASHBOARD_SESSION_TOKEN` +(`web_server.py` `_SESSION_TOKEN`; `?token=<…>` on loopback), but the SSH path passes the remote +`API_SERVER_KEY` (`sshReadRemoteApiKey` in `src/main/ssh-remote.ts`). Even a correctly-tunneled +dashboard would reject the WS upgrade. + +## Shipped now: reliable legacy fallback + +`src/renderer/src/screens/Chat/hooks/useDashboardChatTransport.ts` now latches a sticky +`dashboardUnavailableRef` on the first failed `ensureClient` for a remote/SSH connection, so subsequent +messages fall back to the working legacy HTTP transport (`/v1/chat/completions` through the tunnel) +_immediately_ instead of re-running the multi-second status+probe each time. It also fires +`onDashboardUnavailable` once, which `Chat.tsx` surfaces as a one-time toast. The flag resets on any +connection change. This makes SSH chat **work** (degraded: no profile switching / session history / +dashboard slash commands). + +## Full design: run a remote `hermes dashboard` and tunnel it + +Goal: restore the dashboard transport over SSH by talking to a real remote `hermes dashboard`. + +### 1. Start the remote dashboard (not the gateway) + +Add `sshStartDashboard(config, sessionToken, port)` in `src/main/ssh-remote.ts`, mirroring +`buildGatewayStartCommand`. It should run, detached: + +``` +HERMES_DASHBOARD_SESSION_TOKEN= \ + nohup hermes dashboard --no-open --host 127.0.0.1 --port \ + > $HOME/.hermes/dashboard.log 2>&1 & +``` + +This mirrors the **local** spawn in `src/main/hermes.ts:559` (`dashboard --no-open --host 127.0.0.1 +--port `, gated by `HERMES_DASHBOARD_SESSION_TOKEN`) — reuse that flag/arg shape. Add a matching +status/stop command pair (`buildDashboardStatusCommand` / `buildDashboardStopCommand`). + +### 2. Tunnel the dashboard port (in addition to / instead of the gateway port) + +The legacy fallback still needs the gateway tunnel for `/v1/chat/completions`, while the dashboard +transport needs the dashboard's `/api/ws` + `/api/sessions` + `/api/status`. Options: + +- **Second forward**: generalize `ensureSshTunnel` / `getSshTunnelUrl` in `src/main/ssh-tunnel.ts` to + manage a named set of forwards (gateway + dashboard), each `localPort → 127.0.0.1:remotePort`. +- Remote dashboard port: pick a free remote port over SSH (run a tiny Python one-liner like the + existing helpers in `ssh-remote.ts`) or document a fixed default; surface it in the SSH config UI. + +### 3. Authenticate `/api/ws` with the session token + +The desktop generates a `sessionToken` (e.g. `randomUUID()`), exports it to the remote dashboard's env +(step 1), and builds the WS URL as `ws://127.0.0.1:{dashTunnelPort}/api/ws?token=` — +replacing `API_SERVER_KEY`. Rewrite `sshDashboardConnectionFromConfig` in `src/main/dashboard.ts` to +this flow (it currently calls `sshStartGateway` + `sshReadRemoteApiKey`). Note `web_server.py`'s +`_ws_auth_ok` accepts the `?token=` query only on loopback / `--insecure`; over the SSH tunnel the +endpoint is loopback on the remote, so this should hold — **verify on a real host**. + +### 4. Compatibility + +`ensureSshDashboardCompatibility` (`src/main/hermes-agent-compat.ts`) already patches `web_server.py`'s +embedded-chat default and `/api/model/set`; keep it. Confirm the remote `hermes` build accepts +`dashboard --no-open --host --port` and the `HERMES_DASHBOARD_SESSION_TOKEN` env (v0.16.0 has +`_DASHBOARD_EMBEDDED_CHAT_ENABLED = True`, so embedded chat is available). + +### 5. Lifecycle + +- Stop the remote `hermes dashboard` on disconnect / app quit (best-effort, like `sshStopGateway`). +- Health-check the dashboard (`/api/status` through the tunnel) and restart on failure. +- Keep the gateway running too if other features depend on it. + +## Testing + +- **Unit**: command builders (`sshStartDashboard` / status / stop) and the multi-forward tunnel wiring, + following the existing `ssh-remote` / `ssh-tunnel` test patterns. +- **Manual E2E (required before merge)**: an SSH host running `hermes-agent` (see + `docs/SSH-TUNNEL-VPS.md`). Verify: tunnel up → dashboard starts remotely → `/api/ws` connects with the + session token → a sent message streams back → profile switching and session history work. This step + cannot be exercised without a real remote host. diff --git a/docs/superpowers/plans/2026-04-30-windows-winget-fedora-rpm-release.md b/docs/superpowers/plans/2026-04-30-windows-winget-fedora-rpm-release.md new file mode 100644 index 0000000..b2a2469 --- /dev/null +++ b/docs/superpowers/plans/2026-04-30-windows-winget-fedora-rpm-release.md @@ -0,0 +1,1238 @@ +# Windows (winget) and Fedora (RPM) Release Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Extend the existing GitHub Actions release pipeline to produce a Windows NSIS installer + winget manifests, and a Fedora `.rpm`, alongside the existing macOS/Linux artifacts. End state: open a PR from `Aiacos/hermes-desktop:feat/winget-rpm-release` to `fathah/hermes-desktop:main`. + +**Architecture:** Two new jobs added to `.github/workflows/release.yml` (Windows build + winget manifest generator), one existing job extended (Linux gets rpm), one job gated on a new `dry_run` input. Winget manifests are filled from YAML templates by a tested Node ESM script and uploaded as a CI artifact for manual submission to `microsoft/winget-pkgs`. + +**Tech Stack:** GitHub Actions, electron-builder 26, Node 22 ESM, vitest, fpm/rpmbuild on Ubuntu runner. + +**Spec:** `docs/superpowers/specs/2026-04-30-windows-winget-fedora-rpm-release-design.md` + +**Branch:** `feat/winget-rpm-release` (already created locally; see Task 1 to confirm). + +--- + +## Pre-flight: confirm starting state + +### Task 0: Confirm we're on the right branch with a clean tree + +- [ ] **Step 1: Check branch and status** + +Run: `git status && git branch --show-current && git log --oneline -3` + +Expected output: + +- Current branch: `feat/winget-rpm-release` +- Working tree clean +- Top commit: `docs: add design spec for Windows (winget) and Fedora (RPM) release` + +If on a different branch or tree is dirty, stop and resolve before proceeding. + +--- + +## Phase 1: electron-builder configuration + +### Task 1: Add RPM target and Linux packaging metadata to electron-builder.yml + +**Files:** + +- Modify: `electron-builder.yml` + +- [ ] **Step 1: Replace the `linux:` block and add `rpm:` block** + +Open `electron-builder.yml`. Replace the existing block: + +```yaml +linux: + target: + - AppImage + - snap + - deb + maintainer: electronjs.org + category: Utility +appImage: + artifactName: ${name}-${version}.${ext} +``` + +with: + +```yaml +linux: + target: + - AppImage + - snap + - deb + - rpm + maintainer: electronjs.org + vendor: Nous Research + category: Utility + synopsis: Self-improving AI assistant desktop app + description: | + Hermes Desktop is a native desktop app for installing, configuring, and chatting + with Hermes Agent — a self-improving AI assistant with tool use, multi-platform + messaging, and a closed learning loop. +appImage: + artifactName: ${name}-${version}.${ext} +rpm: + artifactName: ${name}-${version}.${ext} +``` + +- [ ] **Step 2: Verify YAML is still valid** + +Run: `node -e "console.log(require('js-yaml').load(require('fs').readFileSync('electron-builder.yml','utf-8')).linux.target)" 2>/dev/null || node -e "console.log(JSON.parse(JSON.stringify(require('yaml').parse(require('fs').readFileSync('electron-builder.yml','utf-8')))).linux.target)" 2>/dev/null || python3 -c "import yaml; print(yaml.safe_load(open('electron-builder.yml')).get('linux', {}).get('target'))"` + +Expected output: `['AppImage', 'snap', 'deb', 'rpm']` (Python form) or `[ 'AppImage', 'snap', 'deb', 'rpm' ]` (Node form). + +If neither `js-yaml`, `yaml`, nor Python yaml is available, just open the file and visually verify the four entries. + +### Task 2: Make NSIS settings explicit in electron-builder.yml + +**Files:** + +- Modify: `electron-builder.yml` + +- [ ] **Step 1: Replace the `nsis:` block** + +Replace: + +```yaml +nsis: + artifactName: ${name}-${version}-setup.${ext} + shortcutName: ${productName} + uninstallDisplayName: ${productName} + createDesktopShortcut: always +``` + +with: + +```yaml +nsis: + artifactName: ${name}-${version}-setup.${ext} + shortcutName: ${productName} + uninstallDisplayName: ${productName} + createDesktopShortcut: always + oneClick: true + perMachine: false +``` + +The two new fields make the existing electron-builder defaults explicit so the behavior cannot silently shift across electron-builder versions. + +### Task 3: Add `build:rpm` script to package.json + +**Files:** + +- Modify: `package.json` + +- [ ] **Step 1: Insert the script next to the existing `build:linux`** + +In `package.json` `"scripts"` block, after the line: + +```json +"build:linux": "electron-vite build && electron-builder --linux", +``` + +add: + +```json +"build:rpm": "npm run build && electron-builder --linux rpm", +``` + +Final scripts block excerpt should look like: + +```json +"build:mac": "electron-vite build && electron-builder --mac", +"build:linux": "electron-vite build && electron-builder --linux", +"build:rpm": "npm run build && electron-builder --linux rpm", +"test:watch": "vitest" +``` + +- [ ] **Step 2: Verify package.json still parses** + +Run: `node -e "console.log(require('./package.json').scripts['build:rpm'])"` + +Expected output: `npm run build && electron-builder --linux rpm` + +### Task 4: Local sanity-check the RPM build (Fedora host only) + +This step verifies that the new electron-builder `rpm` target produces a real RPM on the local Fedora machine before we trust CI to do it. This is a **manual, non-CI** check. Skip if not on a Fedora host. + +- [ ] **Step 1: Confirm rpmbuild is present** + +Run: `which rpmbuild && rpmbuild --version` + +Expected: a path under `/usr/bin/` and a version string. If missing, install with `sudo dnf install rpm-build`. + +- [ ] **Step 2: Run the RPM build** + +Run: `npm install && npm run build:rpm` + +Expected: completes without errors, ~2-5 minutes. Final lines should mention writing an `.rpm` file under `dist/`. + +- [ ] **Step 3: Confirm the RPM was produced** + +Run: `ls -la dist/*.rpm && rpm -qpi dist/*.rpm | head -20` + +Expected: + +- A file `dist/hermes-desktop-0.2.3.rpm` (or current version) of non-trivial size (~120-200 MB) +- `rpm -qpi` shows `Name: hermes-desktop`, `Version: 0.2.3`, `Vendor: Nous Research`, `License`, `Summary` matching our `synopsis`. + +If the RPM is missing or metadata is wrong, go back to Task 1 and fix. + +- [ ] **Step 4: Clean up dist before committing** + +Run: `rm -rf dist out node_modules/.cache` + +Reason: keeps the working tree clean so the next commit only contains our config changes. + +### Task 5: Commit electron-builder + package.json changes + +- [ ] **Step 1: Stage and commit** + +Run: + +```bash +git add electron-builder.yml package.json +git status +git commit -m "build: add rpm target and explicit nsis settings to electron-builder" +``` + +Expected: 1 commit, 2 files changed. `git status` should show clean working tree afterwards. + +--- + +## Phase 2: Winget manifest infrastructure + +### Task 6: Create the three winget manifest templates + +**Files:** + +- Create: `build/winget/Installer.template.yaml` +- Create: `build/winget/Locale.en-US.template.yaml` +- Create: `build/winget/Version.template.yaml` + +- [ ] **Step 1: Create the directory** + +Run: `mkdir -p build/winget` + +- [ ] **Step 2: Create `build/winget/Installer.template.yaml`** + +Contents: + +```yaml +# Generated from this template by scripts/generate-winget-manifests.mjs. +# Placeholders ({{...}}) are replaced at build time. Do not edit the generated copy in dist/. +PackageIdentifier: NousResearch.HermesDesktop +PackageVersion: { { VERSION } } +InstallerLocale: en-US +InstallerType: nullsoft +Scope: user +MinimumOSVersion: 10.0.17763.0 +ReleaseDate: { { RELEASE_DATE } } +Installers: + - Architecture: x64 + InstallerUrl: { { INSTALLER_URL } } + InstallerSha256: { { INSTALLER_SHA256 } } + UpgradeBehavior: install +ManifestType: installer +ManifestVersion: 1.6.0 +``` + +- [ ] **Step 3: Create `build/winget/Locale.en-US.template.yaml`** + +Contents: + +```yaml +# Generated from this template by scripts/generate-winget-manifests.mjs. +PackageIdentifier: NousResearch.HermesDesktop +PackageVersion: { { VERSION } } +PackageLocale: en-US +Publisher: Nous Research +PublisherUrl: https://github.com/fathah/hermes-desktop +PublisherSupportUrl: https://github.com/fathah/hermes-desktop/issues +PackageName: Hermes Agent +PackageUrl: https://github.com/fathah/hermes-desktop +License: MIT +LicenseUrl: https://github.com/fathah/hermes-desktop/blob/main/LICENSE +ShortDescription: Self-improving AI assistant desktop app +Description: |- + Hermes Desktop is a native desktop app for installing, configuring, and chatting + with Hermes Agent — a self-improving AI assistant with tool use, multi-platform + messaging, and a closed learning loop. +Tags: + - ai + - agent + - desktop + - electron + - llm +ReleaseNotesUrl: { { RELEASE_NOTES_URL } } +ManifestType: defaultLocale +ManifestVersion: 1.6.0 +``` + +- [ ] **Step 4: Create `build/winget/Version.template.yaml`** + +Contents: + +```yaml +# Generated from this template by scripts/generate-winget-manifests.mjs. +PackageIdentifier: NousResearch.HermesDesktop +PackageVersion: { { VERSION } } +DefaultLocale: en-US +ManifestType: version +ManifestVersion: 1.6.0 +``` + +- [ ] **Step 5: Verify the three files exist** + +Run: `ls -la build/winget/` + +Expected: three `.template.yaml` files listed. + +### Task 7: Write the failing test for the manifest generator + +**Files:** + +- Create: `tests/winget-generator.test.ts` + +- [ ] **Step 1: Create `tests/winget-generator.test.ts`** + +Contents: + +```typescript +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { join } from "path"; +import { + existsSync, + readFileSync, + mkdirSync, + writeFileSync, + rmSync, + mkdtempSync, +} from "fs"; +import { tmpdir } from "os"; +// @ts-expect-error - .mjs has no type declarations; we test it as JS. +import { generateWingetManifests } from "../scripts/generate-winget-manifests.mjs"; + +let TEST_DIR: string; + +beforeEach(() => { + TEST_DIR = mkdtempSync(join(tmpdir(), "winget-test-")); +}); + +afterEach(() => { + rmSync(TEST_DIR, { recursive: true, force: true }); +}); + +function setupTemplates(rootDir: string) { + const buildDir = join(rootDir, "build", "winget"); + mkdirSync(buildDir, { recursive: true }); + writeFileSync( + join(buildDir, "Installer.template.yaml"), + "Version: {{VERSION}}\nUrl: {{INSTALLER_URL}}\nSha: {{INSTALLER_SHA256}}\nDate: {{RELEASE_DATE}}\n", + ); + writeFileSync( + join(buildDir, "Locale.en-US.template.yaml"), + "Version: {{VERSION}}\nNotes: {{RELEASE_NOTES_URL}}\n", + ); + writeFileSync( + join(buildDir, "Version.template.yaml"), + "Version: {{VERSION}}\n", + ); +} + +describe("generateWingetManifests", () => { + it("produces three YAML files under the winget-pkgs directory layout", () => { + setupTemplates(TEST_DIR); + const distDir = join(TEST_DIR, "dist"); + mkdirSync(distDir, { recursive: true }); + writeFileSync( + join(distDir, "hermes-desktop-9.9.9-setup.exe"), + "fake-installer-bytes", + ); + + generateWingetManifests({ + rootDir: TEST_DIR, + version: "9.9.9", + name: "hermes-desktop", + publishOwner: "fathah", + }); + + const outDir = join( + distDir, + "winget", + "manifests", + "n", + "NousResearch", + "HermesDesktop", + "9.9.9", + ); + expect( + existsSync(join(outDir, "NousResearch.HermesDesktop.installer.yaml")), + ).toBe(true); + expect( + existsSync(join(outDir, "NousResearch.HermesDesktop.locale.en-US.yaml")), + ).toBe(true); + expect(existsSync(join(outDir, "NousResearch.HermesDesktop.yaml"))).toBe( + true, + ); + }); + + it("replaces all placeholders in the installer manifest", () => { + setupTemplates(TEST_DIR); + const distDir = join(TEST_DIR, "dist"); + mkdirSync(distDir, { recursive: true }); + writeFileSync( + join(distDir, "hermes-desktop-9.9.9-setup.exe"), + "fake-installer-bytes", + ); + + generateWingetManifests({ + rootDir: TEST_DIR, + version: "9.9.9", + name: "hermes-desktop", + publishOwner: "fathah", + }); + + const outFile = join( + distDir, + "winget", + "manifests", + "n", + "NousResearch", + "HermesDesktop", + "9.9.9", + "NousResearch.HermesDesktop.installer.yaml", + ); + const content = readFileSync(outFile, "utf-8"); + expect(content).toContain("Version: 9.9.9"); + expect(content).toContain( + "Url: https://github.com/fathah/hermes-desktop/releases/download/v9.9.9/hermes-desktop-9.9.9-setup.exe", + ); + expect(content).toMatch(/Sha: [A-F0-9]{64}/); + expect(content).toMatch(/Date: \d{4}-\d{2}-\d{2}/); + expect(content).not.toContain("{{"); + }); + + it("replaces ReleaseNotesUrl in the locale manifest", () => { + setupTemplates(TEST_DIR); + const distDir = join(TEST_DIR, "dist"); + mkdirSync(distDir, { recursive: true }); + writeFileSync( + join(distDir, "hermes-desktop-9.9.9-setup.exe"), + "fake-installer-bytes", + ); + + generateWingetManifests({ + rootDir: TEST_DIR, + version: "9.9.9", + name: "hermes-desktop", + publishOwner: "fathah", + }); + + const outFile = join( + distDir, + "winget", + "manifests", + "n", + "NousResearch", + "HermesDesktop", + "9.9.9", + "NousResearch.HermesDesktop.locale.en-US.yaml", + ); + const content = readFileSync(outFile, "utf-8"); + expect(content).toContain( + "Notes: https://github.com/fathah/hermes-desktop/releases/tag/v9.9.9", + ); + expect(content).not.toContain("{{"); + }); + + it("throws a clear error when the installer .exe is missing", () => { + setupTemplates(TEST_DIR); + mkdirSync(join(TEST_DIR, "dist"), { recursive: true }); + + expect(() => + generateWingetManifests({ + rootDir: TEST_DIR, + version: "9.9.9", + name: "hermes-desktop", + publishOwner: "fathah", + }), + ).toThrow(/installer not found/i); + }); +}); +``` + +### Task 8: Verify the test fails + +- [ ] **Step 1: Run vitest** + +Run: `npm run test` + +Expected: `tests/winget-generator.test.ts` fails with an import resolution error like `Failed to resolve import "../scripts/generate-winget-manifests.mjs"` or similar. **The other existing tests must still pass.** Total: 4 new tests failing, all existing tests passing. + +If existing tests fail, stop — that's an unrelated breakage we caused. Investigate before proceeding. + +### Task 9: Implement the manifest generator + +**Files:** + +- Create: `scripts/generate-winget-manifests.mjs` + +- [ ] **Step 1: Create the directory** + +Run: `mkdir -p scripts` + +- [ ] **Step 2: Create `scripts/generate-winget-manifests.mjs`** + +Contents: + +```javascript +// scripts/generate-winget-manifests.mjs +// +// Fills the YAML templates in build/winget/ with the current version, +// installer URL, and SHA256 of the NSIS installer in dist/, and writes +// the result under dist/winget/manifests/n/NousResearch/HermesDesktop//. +// +// Run from CLI: VERSION=0.2.3 PUBLISH_OWNER=fathah node scripts/generate-winget-manifests.mjs +// Or import as ESM and call generateWingetManifests({ rootDir, version, name, publishOwner }). + +import { createHash } from "node:crypto"; +import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +export function generateWingetManifests({ + rootDir, + version, + name, + publishOwner, +}) { + const exePath = join(rootDir, "dist", `${name}-${version}-setup.exe`); + if (!existsSync(exePath)) { + throw new Error( + `NSIS installer not found at ${exePath}. ` + + `Run electron-builder --win nsis first, or download the windows-artifacts CI artifact into dist/.`, + ); + } + + const sha256 = createHash("sha256") + .update(readFileSync(exePath)) + .digest("hex") + .toUpperCase(); + const releaseDate = new Date().toISOString().slice(0, 10); // YYYY-MM-DD + const installerUrl = `https://github.com/${publishOwner}/hermes-desktop/releases/download/v${version}/${name}-${version}-setup.exe`; + const releaseNotesUrl = `https://github.com/${publishOwner}/hermes-desktop/releases/tag/v${version}`; + + const replacements = { + VERSION: version, + INSTALLER_URL: installerUrl, + INSTALLER_SHA256: sha256, + RELEASE_DATE: releaseDate, + RELEASE_NOTES_URL: releaseNotesUrl, + }; + + const fillTemplate = (str) => + Object.entries(replacements).reduce( + (acc, [key, value]) => acc.replaceAll(`{{${key}}}`, value), + str, + ); + + const templateDir = join(rootDir, "build", "winget"); + const outDir = join( + rootDir, + "dist", + "winget", + "manifests", + "n", + "NousResearch", + "HermesDesktop", + version, + ); + mkdirSync(outDir, { recursive: true }); + + const files = [ + ["Installer.template.yaml", "NousResearch.HermesDesktop.installer.yaml"], + [ + "Locale.en-US.template.yaml", + "NousResearch.HermesDesktop.locale.en-US.yaml", + ], + ["Version.template.yaml", "NousResearch.HermesDesktop.yaml"], + ]; + + for (const [tmplName, outName] of files) { + const tmpl = readFileSync(join(templateDir, tmplName), "utf-8"); + writeFileSync(join(outDir, outName), fillTemplate(tmpl)); + } + + return { outDir, sha256, installerUrl }; +} + +// CLI entrypoint +const isCli = + process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]; +if (isCli) { + const rootDir = process.cwd(); + const pkg = JSON.parse(readFileSync(join(rootDir, "package.json"), "utf-8")); + const result = generateWingetManifests({ + rootDir, + version: process.env.VERSION || pkg.version, + name: pkg.name, + publishOwner: process.env.PUBLISH_OWNER || "fathah", + }); + console.log(`Winget manifests generated in ${result.outDir}`); + console.log(`InstallerSha256: ${result.sha256}`); + console.log(`InstallerUrl: ${result.installerUrl}`); +} +``` + +### Task 10: Verify the test passes + +- [ ] **Step 1: Run vitest** + +Run: `npm run test` + +Expected: all four `winget-generator` tests pass. All previously passing tests still pass. Output should end with `Tests` count incremented by 4. + +If failing: read the assertion error and fix the script. Do not modify the test to make it pass — modify the implementation. + +### Task 11: Commit winget infrastructure + +- [ ] **Step 1: Stage and commit** + +Run: + +```bash +git add build/winget/ scripts/generate-winget-manifests.mjs tests/winget-generator.test.ts +git status +git commit -m "feat: add winget manifest generator with templates and tests" +``` + +Expected: 5 files added, 1 commit. Working tree clean. + +--- + +## Phase 3: GitHub Actions workflow + +The following four tasks edit `.github/workflows/release.yml` in sequence. Each task ends with the workflow still being a syntactically valid YAML, but only after Task 16 is the workflow logically complete. Commit at the end of Phase 3 (Task 17), not after each individual edit. + +### Task 12: Add `dry_run` input and `is_dry_run` output + +**Files:** + +- Modify: `.github/workflows/release.yml` + +- [ ] **Step 1: Replace the `on:` block** + +Replace: + +```yaml +on: + push: + branches: + - release + workflow_dispatch: +``` + +with: + +```yaml +on: + push: + branches: + - release + workflow_dispatch: + inputs: + dry_run: + description: "Run all build jobs but skip publish (no tag, no GitHub Release)" + type: boolean + default: true +``` + +- [ ] **Step 2: Add `is_dry_run` to the `prepare` job outputs and add a step to compute it** + +In the `prepare` job, replace: + +```yaml +outputs: + version: ${{ steps.version.outputs.version }} + tag: ${{ steps.version.outputs.tag }} + tag_exists: ${{ steps.check.outputs.exists }} +``` + +with: + +```yaml +outputs: + version: ${{ steps.version.outputs.version }} + tag: ${{ steps.version.outputs.tag }} + tag_exists: ${{ steps.check.outputs.exists }} + is_dry_run: ${{ steps.mode.outputs.is_dry_run }} +``` + +Then, after the existing "Check if tag already exists" step inside `prepare.steps`, append: + +```yaml +- name: Compute dry-run flag + id: mode + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.dry_run }}" = "true" ]; then + echo "is_dry_run=true" >> "$GITHUB_OUTPUT" + echo "Dry run: builds will run, publish will be skipped." + else + echo "is_dry_run=false" >> "$GITHUB_OUTPUT" + echo "Real release: publish will run if tag does not exist." + fi +``` + +### Task 13: Extend `release_linux` with rpm + +**Files:** + +- Modify: `.github/workflows/release.yml` + +- [ ] **Step 1: Add `rpm` apt install before packaging, and rpm to the electron-builder targets** + +In the `release_linux` job, locate the steps after `Install dependencies` and `Build app`. Replace: + +```yaml +- name: Package Linux artifacts + run: npx electron-builder --linux AppImage deb --publish never + +- name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: linux-artifacts + path: | + dist/*.AppImage + dist/*.deb + dist/latest-linux.yml +``` + +with: + +```yaml +- name: Install rpmbuild + run: sudo apt-get update && sudo apt-get install -y rpm + +- name: Package Linux artifacts + run: npx electron-builder --linux AppImage deb rpm --publish never + +- name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: linux-artifacts + path: | + dist/*.AppImage + dist/*.deb + dist/*.rpm + dist/latest-linux.yml +``` + +### Task 14: Add `release_windows` job + +**Files:** + +- Modify: `.github/workflows/release.yml` + +- [ ] **Step 1: Insert a new job after `release_linux`, before `publish`** + +Add the following job block: + +```yaml +release_windows: + name: Build Windows + needs: prepare + if: needs.prepare.outputs.tag_exists == 'false' + runs-on: windows-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build app + run: npm run build + + - name: Package Windows artifacts + run: npx electron-builder --win nsis --x64 --publish never + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: windows-artifacts + path: | + dist/*.exe + dist/*.exe.blockmap + dist/latest.yml +``` + +### Task 15: Add `generate_winget` job + +**Files:** + +- Modify: `.github/workflows/release.yml` + +- [ ] **Step 1: Insert the new job after `release_windows`** + +Add the following job block: + +```yaml +generate_winget: + name: Generate winget manifests + needs: [prepare, release_windows] + if: needs.prepare.outputs.tag_exists == 'false' + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Download Windows installer artifact + uses: actions/download-artifact@v4 + with: + name: windows-artifacts + path: dist/ + + - name: Generate winget manifests + env: + VERSION: ${{ needs.prepare.outputs.version }} + PUBLISH_OWNER: fathah + run: node scripts/generate-winget-manifests.mjs + + - name: Upload winget manifests artifact + uses: actions/upload-artifact@v4 + with: + name: winget-manifests-${{ needs.prepare.outputs.version }} + path: dist/winget/ +``` + +### Task 16: Update the `publish` job (gate + explicit file list) + +**Files:** + +- Modify: `.github/workflows/release.yml` + +- [ ] **Step 1: Replace the `publish` job header and the gh-release files glob** + +Replace the existing `publish` job: + +```yaml +publish: + name: Publish Release + needs: [prepare, release_mac, release_linux] + if: needs.prepare.outputs.tag_exists == 'false' + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Create tag + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag ${{ needs.prepare.outputs.tag }} + git push origin ${{ needs.prepare.outputs.tag }} + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + + - name: Publish GitHub release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.prepare.outputs.tag }} + name: Hermes Desktop ${{ needs.prepare.outputs.tag }} + generate_release_notes: true + files: artifacts/* +``` + +with: + +```yaml +publish: + name: Publish Release + needs: [prepare, release_mac, release_linux, release_windows, generate_winget] + if: needs.prepare.outputs.is_dry_run == 'false' && needs.prepare.outputs.tag_exists == 'false' + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Create tag + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag ${{ needs.prepare.outputs.tag }} + git push origin ${{ needs.prepare.outputs.tag }} + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + + - name: Publish GitHub release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.prepare.outputs.tag }} + name: Hermes Desktop ${{ needs.prepare.outputs.tag }} + generate_release_notes: true + files: | + artifacts/*.dmg + artifacts/*.zip + artifacts/*.AppImage + artifacts/*.deb + artifacts/*.rpm + artifacts/*.exe + artifacts/*.blockmap + artifacts/latest.yml + artifacts/latest-linux.yml + artifacts/latest-mac.yml +``` + +### Task 17: Validate workflow syntax and commit + +- [ ] **Step 1: Verify YAML parses** + +Run: `python3 -c "import yaml; d = yaml.safe_load(open('.github/workflows/release.yml')); print('jobs:', list(d['jobs'].keys()))"` + +Expected output: `jobs: ['prepare', 'release_mac', 'release_linux', 'release_windows', 'generate_winget', 'publish']` + +If not, the YAML has a structural issue — open it and verify indentation. + +- [ ] **Step 2 (optional): Run actionlint if installed** + +Run: `command -v actionlint && actionlint .github/workflows/release.yml || echo "actionlint not installed, skipping"` + +Expected: either no output (lint clean) or "actionlint not installed, skipping". A real lint error must be fixed before commit. + +- [ ] **Step 3: Commit** + +Run: + +```bash +git add .github/workflows/release.yml +git status +git commit -m "ci: build Windows NSIS, Fedora RPM, and winget manifests in release workflow" +``` + +Expected: 1 commit, 1 file changed. Working tree clean. + +--- + +## Phase 4: Documentation + +### Task 18: Update README install section + +**Files:** + +- Modify: `README.md` + +- [ ] **Step 1: Replace the platform table and add notes** + +Locate the current Install section (lines ~22-37): + +````markdown +## Install + +Download the latest build from the [Releases](https://github.com/fathah/hermes-desktop/releases/) page. + +| Platform | File | +| -------- | --------------------- | +| macOS | `.dmg` | +| Linux | `.AppImage` or `.deb` | + +> **macOS users:** The app is not code-signed or notarized. macOS will block it on first launch. To fix this, run the following after installing: +> +> ```bash +> xattr -cr "/Applications/Hermes Agent.app" +> ``` +> +> Or right-click the app → **Open** → click **Open** in the confirmation dialog. +```` + +Replace the table and add a Linux/Windows notes block. The new section: + +````markdown +## Install + +Download the latest build from the [Releases](https://github.com/fathah/hermes-desktop/releases/) page. + +| Platform | File | +| -------------- | ----------------------- | +| macOS | `.dmg` | +| Linux (any) | `.AppImage` | +| Linux (Debian) | `.deb` | +| Linux (Fedora) | `.rpm` | +| Windows | `.exe` (NSIS installer) | + +### Windows (winget) + +Once the manifest has been accepted into [`microsoft/winget-pkgs`](https://github.com/microsoft/winget-pkgs), you can install with: + +```powershell +winget install NousResearch.HermesDesktop +``` +```` + +Until then, download the `.exe` from the Releases page. + +> **Windows users:** The installer is not code-signed. Windows SmartScreen will warn on first launch — click "More info" → "Run anyway". + +### Fedora (RPM) + +```bash +sudo dnf install ./hermes-desktop-.rpm +``` + +> **Fedora users:** The `.rpm` is not GPG-signed. If your system enforces signature checking, append `--nogpgcheck` to the install command. Auto-update is not supported for `.rpm` builds (limitation of `electron-updater`); reinstall the new `.rpm` to update. + +### macOS + +> **macOS users:** The app is not code-signed or notarized. macOS will block it on first launch. To fix this, run the following after installing: +> +> ```bash +> xattr -cr "/Applications/Hermes Agent.app" +> ``` +> +> Or right-click the app → **Open** → click **Open** in the confirmation dialog. + +```` + +- [ ] **Step 2: Verify the markdown renders sensibly** + +Open `README.md` and skim the new Install section. Confirm: table is well-formed, code fences close, no leftover duplicate headings. + +### Task 19: Commit README + +- [ ] **Step 1: Stage and commit** + +Run: +```bash +git add README.md +git status +git commit -m "docs: document Windows (winget) and Fedora (RPM) install paths" +```` + +Expected: 1 commit, 1 file changed. + +--- + +## Phase 5: Local verification gate + +### Task 20: Run full local verification + +This is the gate before pushing. **All must pass.** If any fails, fix the underlying issue and re-run; do not proceed. + +- [ ] **Step 1: Lint** + +Run: `npm run lint` + +Expected: exits 0 with no errors. + +- [ ] **Step 2: Typecheck** + +Run: `npm run typecheck` + +Expected: exits 0 with no errors. Both `tsconfig.node.json` and `tsconfig.web.json` checks succeed. + +- [ ] **Step 3: Tests** + +Run: `npm run test` + +Expected: all tests pass, including the four new `winget-generator` tests. Total count = previous total + 4. + +- [ ] **Step 4: Confirm working tree clean** + +Run: `git status` + +Expected: `nothing to commit, working tree clean`. + +--- + +## Phase 6: CI verification on the fork + +### Task 21: Push branch to Aiacos fork + +This step requires push access to `Aiacos/hermes-desktop` (the user's fork). If pushing requires interactive auth, the human operator runs the command. + +- [ ] **Step 1: Push** + +Run: `git push -u origin feat/winget-rpm-release` + +Expected: branch created on `origin` (which is `Aiacos/hermes-desktop` per `git remote -v`), tracking set up. + +If push is rejected, resolve auth (e.g., `gh auth login` or SSH key) before retrying. Do not force-push. + +### Task 22: Trigger workflow_dispatch with dry_run=true and observe CI + +- [ ] **Step 1: Trigger via gh CLI** + +Run: `gh workflow run release.yml --ref feat/winget-rpm-release -f dry_run=true` + +Expected: `gh` confirms the workflow was queued. (Alternative: trigger from the GitHub Actions UI on the `Release` workflow → Run workflow → branch `feat/winget-rpm-release` → leave dry_run checked.) + +- [ ] **Step 2: Watch the run** + +Run: `gh run watch` (or `gh run list --workflow=release.yml --branch=feat/winget-rpm-release --limit 1` then `gh run view --log-failed` if it fails) + +Expected progression: + +- `prepare` ✓ (~30s) +- `release_mac` x64 + arm64 ✓ (~10-15min in parallel) +- `release_linux` ✓ (~5-8min) +- `release_windows` ✓ (~8-12min) +- `generate_winget` ✓ (~30s, depends on `release_windows`) +- `publish` is **skipped** (status: `skipped`, not `failed`) + +Total wall-clock: ~15-20min (mac arm64 is usually the longest pole). + +- [ ] **Step 3: If any job fails** + +Read the failure with `gh run view --log-failed`. Most likely failure modes: + +- Windows job: `npm ci` fails on a native dep (better-sqlite3 needing windows-build-tools). Solution: add `npm config set msvs_version 2022` step or rely on electron-builder's own `install-app-deps`. +- Linux rpm job: `rpmbuild` missing a dep. Solution: ensure `rpm` and possibly `rpm-build` are both apt-installed. +- generate_winget: script error. Most likely a path mismatch — confirm artifact downloaded to `dist/`. + +Fix the issue, commit on the branch, push, and re-trigger. Do not proceed to PR until the dispatch succeeds end-to-end with `publish` skipped. + +### Task 23: Inspect the winget manifests artifact + +- [ ] **Step 1: Download the artifact** + +Run: `gh run download -n winget-manifests-0.2.3 -D /tmp/winget-check && ls -la /tmp/winget-check/manifests/n/NousResearch/HermesDesktop/0.2.3/` + +(Replace `0.2.3` with the actual `version` from `package.json` if it changed.) + +Expected: three `.yaml` files listed. + +- [ ] **Step 2: Visually inspect** + +Run: `cat /tmp/winget-check/manifests/n/NousResearch/HermesDesktop/0.2.3/*.yaml` + +Expected: + +- No `{{...}}` placeholders left. +- `InstallerSha256` is a 64-character uppercase hex string. +- `InstallerUrl` points to the `fathah/hermes-desktop` releases path with the correct version. +- `ReleaseDate` is today's date (UTC) in `YYYY-MM-DD`. +- `PackageVersion` matches `package.json`. + +If any field looks wrong, fix the generator or templates, commit, re-push, re-trigger. + +- [ ] **Step 3: Cleanup** + +Run: `rm -rf /tmp/winget-check` + +--- + +## Phase 7: Open PR upstream + +### Task 24: Open PR to `fathah/hermes-desktop:main` + +This is a "shared state" action visible to others. The human operator confirms before running. + +- [ ] **Step 1: Confirm PR target with the user** + +Ask the user: "Ready to open the PR from `Aiacos:feat/winget-rpm-release` to `fathah/hermes-desktop:main`? Or do you want to review the diff one more time first?" + +Wait for explicit confirmation. + +- [ ] **Step 2: Create the PR via gh** + +Run: + +```bash +gh pr create \ + --repo fathah/hermes-desktop \ + --base main \ + --head Aiacos:feat/winget-rpm-release \ + --title "ci: add Windows (winget) and Fedora (RPM) release artifacts" \ + --body "$(cat <<'EOF' +## Summary + +- Adds a `release_windows` job that builds an NSIS installer (`hermes-desktop--setup.exe`) on `windows-latest`. +- Adds a `generate_winget` job that fills YAML manifest templates (`build/winget/*.template.yaml`) with the installer SHA256 and uploads them as the `winget-manifests-` CI artifact, ready for manual submission to [`microsoft/winget-pkgs`](https://github.com/microsoft/winget-pkgs). +- Extends the existing `release_linux` job to also build an `.rpm` for Fedora alongside the existing `.AppImage` and `.deb`. +- Adds explicit `oneClick: true` / `perMachine: false` to NSIS (matches electron-builder defaults; pinning prevents future drift) and Linux packaging metadata (`vendor`, `synopsis`, `description`). +- Adds a `dry_run` boolean input to `workflow_dispatch` (default `true`) so the workflow can be tested on a branch without creating tags or releases. Real releases (push to `release` branch) are unaffected. + +The winget manifests are deliberately **not** included in the GitHub Release files (operator-facing, not user-facing). To publish to winget, download the `winget-manifests-` artifact from the release run and submit a PR to `microsoft/winget-pkgs`. + +Spec: `docs/superpowers/specs/2026-04-30-windows-winget-fedora-rpm-release-design.md` +Plan: `docs/superpowers/plans/2026-04-30-windows-winget-fedora-rpm-release.md` + +## Verification + +- [x] `npm run lint` +- [x] `npm run typecheck` +- [x] `npm run test` (4 new tests for the manifest generator) +- [x] `npm run build:rpm` produces a valid `.rpm` on Fedora +- [x] `workflow_dispatch` with `dry_run=true` on the fork: all build jobs succeed, `publish` skipped as expected +- [x] Generated winget manifests inspected: no leftover placeholders, valid SHA256, correct URLs + +## Notes for the maintainer + +- **No code-signing introduced.** Windows SmartScreen will warn on first install; Fedora `dnf` will warn on unsigned RPM. Adding signing is out of scope for this PR (would require certificates / GPG keys in repo secrets). +- **No auto-submit to winget-pkgs.** That would need a GitHub PAT in upstream secrets; the manual submit flow is one `cp -r` from the artifact directory. +- **Fedora `.rpm` does not auto-update** (electron-updater limitation). Documented in README. + +## Test plan + +- [ ] Maintainer pushes a real release (push to `release` branch); confirms all six jobs run and `publish` produces a GitHub Release with `.exe`, `.rpm`, `.deb`, `.AppImage`, `.dmg`, `.zip`. +- [ ] Maintainer downloads `winget-manifests-` artifact and (optionally) submits to `microsoft/winget-pkgs`. +EOF +)" +``` + +Expected: `gh` returns the PR URL. Print it back to the user. + +- [ ] **Step 3: Print the PR URL** + +Done. + +--- + +## Self-review checklist (executed before saving) + +**Spec coverage:** + +- ✅ Windows NSIS build → Tasks 14, 17 +- ✅ Winget manifest generation → Tasks 6, 7, 9, 11, 15 +- ✅ Fedora RPM in CI → Task 13 +- ✅ Local rpm sanity-check → Task 4 +- ✅ `dry_run` workflow_dispatch input → Task 12 +- ✅ `publish` gated on `is_dry_run` → Task 16 +- ✅ Explicit gh-release files list (replaces `artifacts/*` glob) → Task 16 +- ✅ Linux metadata (vendor/synopsis/description) → Task 1 +- ✅ Explicit NSIS oneClick/perMachine → Task 2 +- ✅ README updates for Windows + Fedora install → Task 18 +- ✅ Local verification (lint/typecheck/test) → Task 20 +- ✅ CI verification on fork → Tasks 21–23 +- ✅ Open PR upstream → Task 24 + +**Placeholder scan:** No "TBD" or "fill in details" left. The generated PR body in Task 24 has a "Test plan" with unchecked boxes — that is intentional, those are TODOs for the _maintainer_, not for the implementer. + +**Type/name consistency:** + +- `generateWingetManifests({ rootDir, version, name, publishOwner })` — same signature in test (Task 7), implementation (Task 9), and CLI entrypoint. +- `winget-manifests-${{ needs.prepare.outputs.version }}` — same artifact name in `generate_winget` job (Task 15) and in the CI inspection step (Task 23). +- `is_dry_run` output — defined in Task 12, consumed in Task 16. +- `publishOwner: "fathah"` — same in workflow env (Task 15) and CLI default (Task 9). diff --git a/docs/superpowers/specs/2026-04-30-windows-winget-fedora-rpm-release-design.md b/docs/superpowers/specs/2026-04-30-windows-winget-fedora-rpm-release-design.md new file mode 100644 index 0000000..fc5ca8c --- /dev/null +++ b/docs/superpowers/specs/2026-04-30-windows-winget-fedora-rpm-release-design.md @@ -0,0 +1,235 @@ +# Windows (winget) and Fedora (RPM) release automation + +**Status:** Approved (brainstorming) — pending implementation plan +**Date:** 2026-04-30 +**Branch target:** `Aiacos/hermes-desktop:feat/winget-rpm-release` → PR upstream `fathah/hermes-desktop:main` + +## Goal + +Extend the existing release pipeline so it produces: + +1. **Windows artifacts** — an NSIS installer (`.exe`) published in each GitHub Release, plus winget manifests (`Installer.yaml`, `Locale.en-US.yaml`, `Version.yaml`) generated as a CI artifact for manual submission to `microsoft/winget-pkgs`. +2. **Fedora artifacts** — an unsigned `.rpm` published in each GitHub Release, alongside the existing `.AppImage` and `.deb`. + +No changes to macOS. The existing `.AppImage` / `.deb` artifacts are rebuilt by the same job that now also produces `.rpm`; the only adjacent change touching them is shared `linux.*` metadata (`synopsis`, `description`, `vendor`) which improves their packaging metadata too. No code-signing introduced (none available). No COPR repository, no auto-submission to `microsoft/winget-pkgs`. + +## Non-goals + +- Code signing for Windows (no certificate available; SmartScreen warning persists). +- macOS notarization changes. +- GPG-signing the RPM (no signing key available; users install with `sudo dnf install ./file.rpm` or `--nogpgcheck`). +- A Fedora COPR repository. +- Automated PR submission to `microsoft/winget-pkgs` (would require a GitHub PAT in upstream secrets, which the PR author does not control). +- Auto-update support for `.rpm` (limitation of `electron-updater`). +- Windows ARM64 build (requires extra toolchain; nobody is testing it). + +## Architecture + +The existing `release.yml` workflow is extended in place. Two new jobs are added; one existing job is extended; a `dry_run` input is added to the manual-dispatch trigger. + +``` +release.yml (extended) +├─ prepare [ubuntu] unchanged + new is_dry_run output +├─ release_mac [macos] unchanged +├─ release_linux [ubuntu] ADD: rpm to electron-builder targets, install rpm apt package +├─ release_windows [windows] NEW: NSIS .exe + .blockmap + latest.yml +├─ generate_winget [ubuntu] NEW: SHA256 the .exe, fill manifest templates, upload as artifact +└─ publish [ubuntu] gated: skip if is_dry_run == 'true' +``` + +### Triggers + +- `push` to branch `release` — unchanged. Behaves as a real release: builds run, publish runs. +- `workflow_dispatch` — adds `inputs.dry_run` (boolean, default `true`). When `dry_run=true`, all build jobs run; `publish` is skipped. + +The default-true on dispatch is a safety net: a stray click in the Actions UI cannot trigger a real release. + +### Identifiers and naming + +- **Winget `PackageIdentifier`:** `NousResearch.HermesDesktop` (stable, never renamed) +- **Winget `Publisher`:** `Nous Research` (free-text; binaries hosted under `fathah/hermes-desktop`, which the moderation review will verify against the `InstallerUrl`) +- **Winget `PackageName`:** `Hermes Agent` (matches `productName` in `electron-builder.yml`) +- **NSIS scope:** `oneClick: true`, `perMachine: false` — installs into `%LOCALAPPDATA%`, no UAC prompt, aligns with `winget install` default user scope and with the app's existing `~/.hermes` user-state model. +- **RPM artifact name:** `hermes-desktop-.rpm` (no spaces, no arch suffix — consistent with the existing `.deb` and `.AppImage` naming. The default `${productName}` would produce `Hermes Agent-...rpm` which breaks `dnf install ./file.rpm`. We explicitly only build `x86_64`, so the missing arch suffix is unambiguous in practice.). + +## File changes + +### Modified + +#### `.github/workflows/release.yml` (~+120 / -10 lines) + +- Add `workflow_dispatch.inputs.dry_run` (boolean, default `true`). +- Add `prepare.outputs.is_dry_run` computed as `${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}` (string `'true'`/`'false'`). +- Add new job `release_windows`: + - `runs-on: windows-latest` + - `needs: prepare` + - `if: needs.prepare.outputs.tag_exists == 'false'` + - Steps: checkout, setup-node 22 with `cache: npm`, `npm ci`, `npm run build`, `npx electron-builder --win nsis --x64 --publish never`, upload `dist/*.exe`, `dist/*.exe.blockmap`, `dist/latest.yml` as artifact `windows-artifacts`. +- Modify existing `release_linux`: + - Add `sudo apt-get update && sudo apt-get install -y rpm` step before packaging. + - Change packaging command to `npx electron-builder --linux AppImage deb rpm --publish never`. + - Extend artifact upload paths to include `dist/*.rpm`. +- Add new job `generate_winget`: + - `runs-on: ubuntu-latest` + - `needs: [prepare, release_windows]` + - `if: needs.prepare.outputs.tag_exists == 'false'` + - Steps: checkout, download `windows-artifacts` to `dist/`, run `node scripts/generate-winget-manifests.mjs` with env `VERSION` and `PUBLISH_OWNER=fathah`, upload `dist/winget/` as artifact `winget-manifests-${{ needs.prepare.outputs.version }}`. +- Modify `publish`: + - `needs: [prepare, release_mac, release_linux, release_windows, generate_winget]` + - `if: needs.prepare.outputs.is_dry_run == 'false' && needs.prepare.outputs.tag_exists == 'false'` + - `download-artifact` step keeps `merge-multiple: true` and downloads into `artifacts/`. The winget manifests artifact ends up under `artifacts/winget/manifests/...` and is **deliberately excluded** from the GitHub Release files (manifests are operator-facing, not user-facing). + - The `softprops/action-gh-release` `files` input is changed from the existing `artifacts/*` glob to an **explicit list of patterns** that names each user-facing artifact: `artifacts/*.dmg`, `artifacts/*.zip`, `artifacts/*.AppImage`, `artifacts/*.deb`, `artifacts/*.rpm`, `artifacts/*.exe`, `artifacts/*.blockmap`, `artifacts/latest*.yml`. This is deterministic regardless of how `merge-multiple` lays out subdirectories, and prevents future artifacts from leaking into releases by accident. + +Concurrency block (`group: release`, `cancel-in-progress: true`) remains unchanged. **Caveat (pre-existing, not introduced by this change):** with `cancel-in-progress: true`, a _new_ run cancels the _currently running_ one in the same group. So a stray `workflow_dispatch` triggered during a real release would cancel the release. This risk is low (dispatches are manual; the dispatch defaults to `dry_run=true` which would not produce a tag anyway, but the cancellation of the in-flight real release is the actual hazard). Not addressed here to keep scope focused; flagged for a follow-up that scopes the concurrency group by `github.event_name`. + +#### `electron-builder.yml` (~+15 lines) + +- `linux.target`: add `rpm` (existing `AppImage`, `snap`, `deb` retained). +- `linux.synopsis`: short one-line description (required by fpm/rpmbuild for valid RPM metadata). +- `linux.description`: longer description. +- `linux.vendor`: `Nous Research` (or repo owner of upstream; finalized during implementation). +- New `rpm:` block with `artifactName: ${name}-${version}.${ext}`. +- `nsis:` block extended with explicit `oneClick: true` and `perMachine: false` (currently relies on electron-builder defaults; making them explicit prevents silent behavior change across electron-builder versions). + +### New + +#### `build/winget/Installer.template.yaml` + +YAML manifest with placeholders: `{{VERSION}}`, `{{INSTALLER_URL}}`, `{{INSTALLER_SHA256}}`, `{{RELEASE_DATE}}`. Format follows winget v1.6 schema, `InstallerType: nullsoft`, `Scope: user`, `MinimumOSVersion: 10.0.17763.0` (Windows 10 1809 — minimum supported by Electron 39). + +#### `build/winget/Locale.en-US.template.yaml` + +Locale manifest with placeholders: `{{VERSION}}`, `{{RELEASE_NOTES_URL}}`. Includes `Publisher: Nous Research`, `PublisherUrl: https://github.com/fathah/hermes-desktop`, `PackageName: Hermes Agent`, `License: MIT`, `LicenseUrl: https://github.com/fathah/hermes-desktop/blob/main/LICENSE`, `ShortDescription`, `Tags: [ai, agent, desktop, electron, llm]`. + +#### `build/winget/Version.template.yaml` + +Root version manifest with placeholders: `{{VERSION}}`. Trivial: `PackageIdentifier`, `PackageVersion`, `DefaultLocale: en-US`, `ManifestType: version`, `ManifestVersion: 1.6.0`. + +#### `scripts/generate-winget-manifests.mjs` + +Node ESM script (~50 lines, zero external deps). Reads `package.json` to get `version` and `name`. Locates `dist/--setup.exe`. Computes SHA256 with `node:crypto` `createHash('sha256')` over the file. Reads each `*.template.yaml` from `build/winget/`. Replaces all `{{KEY}}` placeholders by string `replaceAll`. Writes output to `dist/winget/manifests/n/NousResearch/HermesDesktop//`: + +- `NousResearch.HermesDesktop.installer.yaml` +- `NousResearch.HermesDesktop.locale.en-US.yaml` +- `NousResearch.HermesDesktop.yaml` + +The path mirrors the directory layout in `microsoft/winget-pkgs`, so the operator submitting the PR can `cp -r` directly. + +Exit code 1 with explicit error if the `.exe` is not found. + +#### `package.json` + +Add script `"build:rpm": "npm run build && electron-builder --linux rpm"` for local Fedora developers. CI does not use this script (it calls `npx electron-builder` directly). + +#### `README.md` + +Update the Install section's platform table to add Windows (`.exe` and, once accepted into winget-pkgs, `winget install NousResearch.HermesDesktop`) and Fedora (`.rpm`). Add a note that: + +- The Windows build is unsigned; Windows SmartScreen will warn on first launch. +- The `.rpm` is unsigned; install with `sudo dnf install ./hermes-desktop-.x86_64.rpm` (or use `--nogpgcheck` if a system policy enforces signature checking). +- Auto-update on Linux is supported only for `.AppImage` builds; `.rpm` and `.deb` users must download new releases manually. + +## Data flow + +### CI build (Windows) + +``` +checkout + → npm ci → node_modules + electron-builder install-app-deps + → npm run build → out/main + out/preload + out/renderer + → electron-builder --win nsis --x64 + → dist/hermes-desktop--setup.exe + → dist/hermes-desktop--setup.exe.blockmap + → dist/latest.yml + → upload-artifact "windows-artifacts" +``` + +### CI generate winget manifests + +``` +download-artifact "windows-artifacts" → dist/ +node scripts/generate-winget-manifests.mjs + → SHA256(dist/hermes-desktop--setup.exe) + → fill 3 templates with VERSION, INSTALLER_URL, INSTALLER_SHA256, RELEASE_DATE + → write dist/winget/manifests/n/NousResearch/HermesDesktop//*.yaml +upload-artifact "winget-manifests-" +``` + +### CI build (Linux) + +``` +checkout + → npm ci + → npm run build + → apt install rpm + → electron-builder --linux AppImage deb rpm + → dist/hermes-desktop-.AppImage + → dist/hermes-desktop-.deb + → dist/hermes-desktop-.x86_64.rpm + → dist/latest-linux.yml + → upload-artifact "linux-artifacts" +``` + +### Publish (only on real release) + +``` +download-artifact merge-multiple=true → artifacts/ + *.dmg, *.zip, *.AppImage, *.deb, *.rpm, *.exe, *.blockmap, latest-*.yml, latest.yml + + artifacts/winget/manifests/... (excluded from release files glob) +git tag v + push +softprops/action-gh-release files=artifacts/* (excludes winget/ subdir) +``` + +## Error handling and edge cases + +1. **Missing NSIS installer in `generate_winget` job:** The script exits with code 1 and a clear message. Failure is loud, not silent. +2. **`rpm` package missing on Linux runner:** electron-builder fails with a clear "Cannot find rpmbuild" error. We pre-install via apt, so this should not surface. +3. **Tag already exists:** All build jobs gate on `tag_exists == 'false'`; new jobs replicate this guard. +4. **Concurrency cancellation (pre-existing behavior):** With `cancel-in-progress: true`, a new run in the same group cancels the currently running one. A stray dispatch during a real release would cancel the release. Mitigation: dispatches are manual and rare; the safer fix (per-event-type concurrency group) is intentionally out of scope here. +5. **`workflow_dispatch` on a branch other than `release`:** Allowed and intended — that is how E2 verification works. The `prepare` step still reads version from `package.json`, so it builds whatever version is on the branch. Real releases only happen on `release` branch (no `is_dry_run` short-circuit there because `is_dry_run` is `false` for push events by definition). +6. **Operator submits stale manifests:** The CI artifact name includes the version (`winget-manifests-`), so it cannot be confused with a different release's manifests. The operator copies the directory wholesale into their `microsoft/winget-pkgs` clone. +7. **Manifest schema validation:** Validated by the `microsoft/winget-pkgs` moderation bot at PR-submission time. Common failure modes (missing `MinimumOSVersion`, invalid `License`, mismatched `InstallerType`) are addressed up front in the templates. + +## Verification plan + +### Local + +1. Create branch `feat/winget-rpm-release`. +2. Apply all file changes. +3. `npm run lint` +4. `npm run typecheck` +5. `npm run test` +6. `npm run build:rpm` — produces a real `.rpm` on this Fedora host (sanity-checks the new electron-builder rpm target). +7. `node scripts/generate-winget-manifests.mjs` against a placeholder `dist/--setup.exe` (created via `dd if=/dev/urandom of=dist/... bs=1M count=1` to provide arbitrary content) — verifies that the generator produces three valid YAML files with consistent placeholders replaced. +8. (Optional) `actionlint .github/workflows/release.yml` if installed. + +### CI on fork + +9. Push `feat/winget-rpm-release` to `Aiacos/hermes-desktop`. +10. Trigger `workflow_dispatch` from the Actions UI on `feat/winget-rpm-release` with `dry_run=true`. +11. Verify all build jobs succeed: + - `prepare` ✓ + - `release_mac` (x64 + arm64) ✓ + - `release_linux` (with `.rpm`) ✓ + - `release_windows` ✓ + - `generate_winget` ✓ + - `publish` SKIPPED (status: skipped, not failed) +12. Download the `winget-manifests-` artifact and inspect the three YAML files for correctness (URL, SHA256, version, no leftover placeholders). + +### PR + +13. Open PR `Aiacos:feat/winget-rpm-release` → `fathah:main`. PR description summarizes what was added, the verification done, and the manual steps the maintainer has to take post-merge to actually publish to winget (download manifest artifact from the first real release run, submit to `microsoft/winget-pkgs`). + +## Open questions deferred to implementation + +- Exact wording of `linux.synopsis` and `linux.description` (will follow `package.json.description` style). +- Final value of `linux.vendor` (`Nous Research` vs `fathah`). +- Whether to include `manifestVersion: 1.10.0` (current latest) or stick with `1.6.0` (more compatible with older `winget` clients). Default to `1.6.0` unless implementation reveals required fields. + +## Out-of-scope follow-ups (future PRs, if desired) + +- Auto-submit winget PR via `vedantmgoyal2009/winget-releaser` action (requires GitHub PAT in upstream secrets). +- Fedora COPR repository for `dnf install` integration. +- GPG-signing the RPM. +- Windows code signing. +- Windows ARM64 build. diff --git a/electron-builder.yml b/electron-builder.yml new file mode 100644 index 0000000..1a111df --- /dev/null +++ b/electron-builder.yml @@ -0,0 +1,73 @@ +appId: com.nousresearch.hermes +productName: Hermes One +directories: + buildResources: build +files: + - "!**/.vscode/*" + - "!src/*" + - "!electron.vite.config.{js,ts,mjs,cjs}" + - "!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}" + - "!{.env,.env.*,.npmrc,pnpm-lock.yaml}" + - "!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}" +asarUnpack: + - resources/** + - node_modules/better-sqlite3/build/Release/*.node +win: + executableName: hermes-agent + target: + - nsis + - portable +portable: + artifactName: ${name}-${version}-portable.${ext} +nsis: + artifactName: ${name}-${version}-setup.${ext} + shortcutName: ${productName} + uninstallDisplayName: ${productName} + createDesktopShortcut: always + oneClick: true + perMachine: false +mac: + artifactName: ${name}-${version}-${arch}-${os}.${ext} + icon: build/icon.icns + entitlements: build/entitlements.mac.plist + entitlementsInherit: build/entitlements.mac.inherit.plist + extendInfo: + - NSCameraUsageDescription: Application requests access to the device's camera. + - NSMicrophoneUsageDescription: Application requests access to the device's microphone. + - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. + - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. + hardenedRuntime: true + gatekeeperAssess: false + notarize: true +dmg: + artifactName: ${name}-${version}-${arch}.${ext} +linux: + target: + - AppImage + - snap + - deb + - rpm + maintainer: electronjs.org + vendor: Nous Research + category: Utility + synopsis: Self-improving AI assistant desktop app + description: >- + Hermes One is a native desktop app for installing, configuring, and chatting + with Hermes Agent — a self-improving AI assistant with tool use, multi-platform + messaging, and a closed learning loop. +appImage: + artifactName: ${name}-${version}.${ext} +deb: + # Run chmod 4755 on chrome-sandbox so Electron's setuid sandbox helper + # works on modern Linux distros that disable unprivileged user + # namespaces (Ubuntu 24.04+, etc.). Closes #395. + afterInstall: build/linux-after-install.sh +rpm: + artifactName: ${name}-${version}.${ext} + # Same SUID fix for .rpm consumers (Fedora 40+ also restricts userns). + afterInstall: build/linux-after-install.sh +npmRebuild: false +publish: + provider: github + owner: fathah + repo: hermes-desktop diff --git a/electron.vite.config.ts b/electron.vite.config.ts new file mode 100644 index 0000000..b3f595f --- /dev/null +++ b/electron.vite.config.ts @@ -0,0 +1,46 @@ +import { resolve } from "path"; +import { defineConfig } from "electron-vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; + +const rendererPort = Number(process.env.HERMES_DESKTOP_RENDERER_PORT || 0); + +export default defineConfig({ + main: { + build: { + rollupOptions: { + external: ["better-sqlite3"], + }, + }, + }, + preload: { + build: { + rollupOptions: { + input: { + index: resolve("src/preload/index.ts"), + askpass: resolve("src/preload/askpass.ts"), + }, + }, + }, + }, + renderer: { + ...(rendererPort > 0 + ? { + server: { + port: rendererPort, + strictPort: false, + }, + } + : {}), + resolve: { + alias: { + "@renderer": resolve("src/renderer/src"), + }, + // Ensure a single Three.js instance across our code, @react-three/fiber, + // drei and troika — multiple copies break `instanceof THREE.*` checks in + // the ported office agent renderer. + dedupe: ["three"], + }, + plugins: [tailwindcss(), react()], + }, +}); diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..56e594e --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,77 @@ +import { defineConfig } from "eslint/config"; +import tseslint from "@electron-toolkit/eslint-config-ts"; +import eslintConfigPrettier from "@electron-toolkit/eslint-config-prettier"; +import eslintPluginReact from "eslint-plugin-react"; +import eslintPluginReactHooks from "eslint-plugin-react-hooks"; +import eslintPluginReactRefresh from "eslint-plugin-react-refresh"; + +export default defineConfig( + { + ignores: [ + "**/node_modules", + "**/dist", + "**/out", + ".claude/**", + ".agents/**", + "build/**", + // CDP E2E harness — plain Node CommonJS scripts driving the + // dev electron via Chrome DevTools Protocol for live testing. + // They intentionally use require() because they run as one-off + // `node scripts/*.js` invocations outside the TS build, and + // they're not part of the shipped app. See scripts/README.md. + "scripts/e2e-attach.js", + "scripts/repro-*.js", + "scripts/probe-*.js", + "scripts/drive-*.js", + "scripts/verify-*.js", + ], + }, + tseslint.configs.recommended, + eslintPluginReact.configs.flat.recommended, + eslintPluginReact.configs.flat["jsx-runtime"], + { + settings: { + react: { + version: "detect", + }, + }, + }, + { + files: ["**/*.{ts,tsx}"], + plugins: { + "react-hooks": eslintPluginReactHooks, + "react-refresh": eslintPluginReactRefresh, + }, + rules: { + ...eslintPluginReactHooks.configs.recommended.rules, + ...eslintPluginReactRefresh.configs.vite.rules, + "react-hooks/set-state-in-effect": "off", + "react-hooks/refs": "off", + "react-refresh/only-export-components": "off", + // Honour the `_`-prefix convention used across the codebase for + // deliberately-unused parameters/variables (e.g. unused args kept for + // signature symmetry, or ignored caught errors). + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + }, + }, + { + // The 3D office (react-three-fiber) uses Three.js intrinsic elements whose + // props (position, args, rotation, intensity, ...) are flagged by the + // DOM-oriented `react/no-unknown-property` rule. Disable it here only. + files: ["src/renderer/src/screens/Office/office3d/**/*.{ts,tsx}"], + rules: { + "react/no-unknown-property": "off", + // Ported 3D art modules use many small internal helpers without explicit + // return annotations; the renderer doesn't require them here. + "@typescript-eslint/explicit-function-return-type": "off", + }, + }, + eslintConfigPrettier, +); diff --git a/lat.md/.cache/lat_init.json b/lat.md/.cache/lat_init.json new file mode 100644 index 0000000..b120729 --- /dev/null +++ b/lat.md/.cache/lat_init.json @@ -0,0 +1,7 @@ +{ + "init_version": 1, + "file_hashes": { + "CLAUDE.md": "5fd49383be7fd33deec07b4e7a6cbac441568611e392864c7474e5495c8ab0d3", + ".claude/skills/lat-md/SKILL.md": "39c77385d424367ce6f9290973e2abef5660d288e4efda3609e4c2c50e0b574a" + } +} diff --git a/lat.md/agent-sync.md b/lat.md/agent-sync.md new file mode 100644 index 0000000..96fca5a --- /dev/null +++ b/lat.md/agent-sync.md @@ -0,0 +1,91 @@ +# Cloud agent sync + +Syncs desktop profiles (the app's agents) with the signed-in [[hermes-account-login|Hermes One account]]'s cloud agents, bidirectionally, via the backend's `/api/agents` CRUD. + +Phase 1 covers the free parts from the backend's `docs/agent-sync.md`: color, persona (`SOUL.md` ↔ `systemPrompt`), memory (`memories/MEMORY.md` ↔ `memory`), and config basics (`model`/`provider` only — never the whole `config.yaml`, so no secrets leave the device). Skills, automations, and sessions are deferred. Deletions never propagate in either direction. + +## Sync engine + +[[src/main/agent-sync.ts#syncAgents]] runs one single-flight pass: link local profiles to cloud agents, reconcile each part, create missing counterparts on both sides, and unlink mappings whose cloud agent disappeared. + +The stored link (a profile's cloud `agentId`) is also read by [[wallet-token-balances#Wallet Sync]] via [[src/main/agent-sync.ts#getLinkedAgentId]], so backend-provisioned wallets can be fetched for the same agent. + +Requests are bearer-authenticated with the device-login token — the account is located app-wide by [[src/main/account-store.ts#findAccountProfile]] (the token is saved under whichever profile was active at sign-in). Linking keys on the cloud agent's stable `id`; names only match never-synced profiles to their cloud namesakes and are never used to rename. + +Links are **account-scoped**: every state write records the owning backend user id, and a pass skips (never unlinks, never pushes) profiles whose link belongs to a different account — signing out and back in as someone else must not re-upload the first account's agents to the second. A missing cloud agent only unlinks when the state provably belongs to the current account; legacy states without an owner are adopted when their agent exists in the account's list and skipped with a warning otherwise. Wallet flows apply the same rule through [[src/main/agent-sync.ts#getLinkedAgentAccountId]] — see [[wallet-token-balances#Wallet Sync]]. + +Per part, the pure [[src/main/agent-sync.ts#decidePartAction]] compares the last-sync base hash with both sides' current hashes: only one side moved → that side wins; both moved (or first sync) → last-writer-wins by timestamp (local file mtime vs the agent's `updatedAt`). Equal content is always a no-op. + +Pushes are built by [[src/main/agent-sync.ts#buildPushBody]], which enforces the backend's field limits by *skipping* oversize parts with a warning — truncating and later pulling back would destroy local content. An unset local model is also skipped so a PATCH can't clobber the cloud value with an empty string. Pulls write through the existing per-part helpers: [[src/main/soul.ts#writeSoul]], [[src/main/memory.ts#writeMemoryRaw]], [[src/main/profile-meta.ts#setProfileColor]], and [[src/main/config.ts#setModelConfig]] (preserving the local base URL). + +A profile's stable **id** (its directory slug), not its editable display **name**, keys every on-disk operation — `getModelConfig`/`readSoul`/`readMemoryRaw`, the `cloud-sync.json` state file, and all pull writes — so a renamed profile keeps syncing against the same directory. The display `name` is used only as the cloud agent's human label (create/name-match/warnings). + +Cloud-only agents are materialized locally by [[src/main/profiles.ts#createProfile]], which derives a valid, collision-free id from the cloud agent's display name and returns it; the pulled parts are then written under that id. + +## State file + +Each linked profile stores its mapping in `cloud-sync.json` under the profile home: the cloud `agentId`, the owning account's user id, the cloud-side name, and a per-part content hash from the last successful sync (the conflict-detection base). + +Parts that failed to push (or were skipped as oversize) keep their old base, so they stay pending rather than being silently marked clean. Deleting the file unlinks the profile — which is exactly what a pass does when the cloud agent was deleted in the console, leaving the local profile untouched. + +## IPC and UI + +The main process exposes `agent-sync-run`, `agent-sync-status`, and `agent-sync-linked-id` in [[src/main/ipc/register.ts#registerIpcHandlers]], next to the account handlers; a completed run also emits `agent-sync-updated` so the renderer can refresh. + +`agent-sync-linked-id` returns the cloud agent id a profile is linked to (or null) — for the per-profile Sync tab below. + +The preload bridge surfaces these as `syncAgents`, `getAgentSyncStatus`, `getLinkedAgentId`, and `onAgentSyncUpdated` on `window.hermesAPI`, typed by the shared shapes in [[src/shared/agent-sync.ts]]. + +[[src/renderer/src/screens/Agents/Agents.tsx]] (local mode only — Layout shows a remote notice otherwise) renders the sync affordance in the header: a signed-out hint pointing at the Providers account card, or a Sync button with the last pass's summary (warnings in the tooltip). It auto-runs one pass per visit when signed in, and reloads the profile list when a pass pull-created profiles. [[src/renderer/src/components/HermesAccountModal.tsx]] kicks off the first sync right after a successful sign-in. + +The profile modal's **Sync** tab, [[src/renderer/src/components/profile/ProfileSyncPane.tsx]], is a per-profile manual path for when the auto-sync hasn't run: it shows the signed-in account, whether this profile is linked to a cloud agent (`getLinkedAgentId`), and this profile's outcome from the last pass, with a **Sync now** button that runs the same app-wide `syncAgents()`. Sign-in/unauthorized/error states are surfaced inline. + +## Tests + +[[src/main/agent-sync.test.ts]] fakes the profile/config/fs surface and stubs `fetch`, so the tests exercise the reconciliation logic itself; one account-lookup test lives in [[src/main/account-store.test.ts]]. + +### Locates the account app-wide + +`findAccountProfile` finds a session saved under a named profile and prefers the default home when both have one, so sync works no matter which profile was active at sign-in. + +### Part decision matrix + +`decidePartAction` across the base/local/remote hash matrix: equal content is a no-op, a single moved side pushes or pulls, and both-moved / never-synced parts fall back to last-writer-wins by timestamp. + +### Push bodies stay within limits + +`buildPushBody` maps parts to exactly the backend's fields and nothing else, and skips oversize persona/memory and unset models instead of truncating or sending empty strings. + +### Keys on-disk work off the stable id + +A renamed profile (id `hello-agent`, display name `Hello Agent`) drives all on-disk sync — state file, part reads — off the id, while the cloud agent it creates carries the display name as its label. + +### Backs up new local profiles + +A never-synced local profile is POSTed to the backend with its four parts and the returned agent id is persisted to `cloud-sync.json`. + +### Links by name and pulls the newer side + +An unmapped profile links to its cloud namesake without creating a duplicate, and on first sync the newer side (the cloud, when local files are older) wins part by part. + +### Pull-creates cloud-only agents + +A cloud agent with no local counterpart becomes a local profile (via `createProfile`, which derives the id from the agent's display name), and its persona/color/memory/config are written locally under that id. + +### Unlinks deleted cloud agents + +When a mapped cloud agent disappears **and the link's recorded owner is the current account**, the pass removes `cloud-sync.json` and keeps the local profile — no DELETE is ever sent in either direction. + +### Skips foreign-linked profiles + +A profile whose state names a different `accountId` is left completely untouched — state file intact, nothing pushed or pulled — and wallet sync refuses it client-side with `status: "foreign"` before any backend call. + +### Leaves ambiguous legacy links alone + +A legacy state (no `accountId`) whose agent isn't in the current account's list is skipped with a warning, not unlinked. + +It could be a console deletion or another account's agent — and a wrong unlink would re-upload someone else's agent on the next pass, so skipping is the safe read. + +### Records the owning account + +Every successful pass stamps the current account's user id into the state file, adopting legacy links whose agent exists in this account's list. diff --git a/lat.md/analytics.md b/lat.md/analytics.md new file mode 100644 index 0000000..885119b --- /dev/null +++ b/lat.md/analytics.md @@ -0,0 +1,33 @@ +# Analytics + +Privacy-first, opt-out usage analytics that report anonymous events to the in-house Hermes analytics service. Replaces the former PostHog integration; no third-party analytics SDK is bundled. + +Events are sent directly over `fetch` from the renderer — there is no client library. Each event POSTs to `{VITE_ANALYTICS_BASE_URL}/v1/events` with an `x-api-key: {VITE_ANALYTICS_API_KEY}` header and a JSON body of `{ anonymous_id, event, source: "desktop", properties }`. The base URL and API key are injected at build time from GitHub Actions secrets, so analytics is silently disabled in local and unofficial builds where neither is configured. It is also disabled on the Vite dev server — [[src/renderer/src/utils/analytics.ts#isConfigured]] short-circuits when `import.meta.env.DEV` is true, since the `http://localhost:5173` dev origin isn't allowed by the service's CORS (every request would just fail preflight and spam the console). Packaged builds run `vite build`, so they are unaffected. + +## Per-install identity + +A random UUID created on first launch and persisted in `localStorage` under `hermes-anonymous-id` is the analytics user id — created if absent, reused if present. + +It contains no PII and never leaves the device except as the `anonymous_id` field on events. + +[[src/renderer/src/utils/analytics.ts]] owns this logic. `getOrCreateAnonymousId` reads or mints the id; `resetAnalytics` clears it so a fresh id is minted on the next event. + +## Consent + +Analytics is opt-out: enabled by default when the endpoint is configured, and the user can disable it from Settings. The choice is stored in `localStorage` under `hermes-analytics-enabled`. + +[[src/renderer/src/components/settings/PrivacyPane.tsx#PrivacyPane]] (the Privacy pane of the settings modal) renders the toggle and a short one-line privacy note, calling `setAnalyticsConsent`; the initial state comes from `getAnalyticsConsent` in [[src/renderer/src/components/settings/useSettingsData.ts#useSettingsData]]. When consent is off, `capture` short-circuits and no requests are made. + +## Capture surface + +`initAnalytics` runs once at renderer startup from [[src/renderer/src/main.tsx]] and emits an `app_opened` event. + +Its properties are `app_version` (the Hermes version from `package.json`, fetched over the `get-app-version` IPC — not the runtime version), `electron_version`, `node_version`, and `platform`. + +Screen navigation is tracked via `captureScreenView` from [[src/renderer/src/App.tsx#App]], and `captureFeatureUsage` records feature-level events. No chat content, prompts, model responses, file paths, or credentials are ever collected. + +## Build & CSP + +The `VITE_ANALYTICS_BASE_URL` and `VITE_ANALYTICS_API_KEY` secrets are injected into every `npm run build` step of the release workflow (`.github/workflows/release.yml`). + +The Content-Security-Policy in [[src/main/app/start.ts]] and `src/renderer/index.html` allows `connect-src` to reach the analytics host (`https://*.hermesone.org`); the former PostHog `script-src`/`connect-src` allowances were removed. diff --git a/lat.md/chat-commands.md b/lat.md/chat-commands.md new file mode 100644 index 0000000..13bb60f --- /dev/null +++ b/lat.md/chat-commands.md @@ -0,0 +1,122 @@ +# Slash command execution + +Typed slash commands (`/compact`, `/compress`, `/reset`, `/web`, …) are run through the gateway's command pipeline, not submitted to the model as plain prompt text. This is what makes them _do_ something instead of being echoed back as prose. + +The desktop talks to the hermes-agent gateway over JSON-RPC. A normal message goes via `prompt.submit`, which the gateway treats as a user turn — so a literal `/compact` reaches the model and comes back as text. Real commands must instead go through `slash.exec` (registry-backed worker) with a `command.dispatch` fallback for commands that resolve to an alias, plugin, skill, or an agent prompt. + +**Profile scoping over the unified SSH dashboard.** In SSH mode one machine dashboard serves every profile (see [[main-process#SSH dashboard transport]]), so chat calls must carry the active `profile` or the gateway runs them under its launch profile (`default`) — the agent would then answer as `default` even when a named profile is selected. [[src/main/remote-sessions.ts#RemoteSessionConfig]]`.profile` scopes the `/api/*` HTTP ops, and the `/api/ws` chat client passes `profile` on `session.create`/`session.resume` **and** `prompt.submit`/`prompt.background` ([[src/renderer/src/screens/Chat/hooks/useDashboardChatTransport.ts#submitDashboardPromptWithRecovery]]); `session.create` builds the agent and persists against that profile's `HERMES_HOME`/`state.db`, and each turn re-binds it. Omitted/`default` → the launch profile (unchanged for local and per-profile-remote setups). + +## Routing pipeline + +The pure routing logic lives in [[src/renderer/src/screens/Chat/slashExec.ts#executeSlash]]: try `slash.exec`, accept either rendered output or a structured dispatch result, and on rejection fall back to `command.dispatch`, returning `done`, `send`, or `error`. + +The name/argument split is done by [[src/renderer/src/screens/Chat/slashExec.ts#parseSlash]], which matches with the dotAll flag so a command's argument may span multiple lines (e.g. a multi-line `/remember` note) — an empty name is what `executeSlash` rejects as an empty command, so a multi-line body must not collapse the match. + +It mirrors hermes-agent's reference client (`web/src/lib/slashExec.ts`) so every front-end implements the same contract. Pending-input commands such as `/learn` can return `{type: "send"}` directly from `slash.exec`; that prompt still passes through the central model-submission path. + +## Local vs gateway commands + +Every typed slash command is resolved through the merged catalog before execution. Ownership is explicit (`target: "desktop" | "agent" | "model"`); display categories such as `info` do not determine routing. + +Desktop-only commands delegate to local renderer handlers, Agent commands use the gateway command pipeline, and model commands build a prompt through the shared model-submission formatter. The legacy transport reports Agent commands as unavailable instead of sending raw `/…` text to the model. + +## Commands never queue + +Slash commands run on the gateway's **persistent slash-worker subprocess**, concurrent with any in-flight turn — so they respond instantly and must NOT sit in the busy queue behind a running turn (only plain prompts queue). + +`handleSubmitOrQueue` in [[src/renderer/src/screens/Chat/Chat.tsx]] dispatches every `/…` input immediately to the central router. Desktop and slash-worker commands can complete concurrently; model commands and Agent `send`/skill directives are formatted once and queued when the main model turn is busy. + +Because no global loading state is set, the slash branch shows its own feedback: it inserts an in-place `⏳ Running …` agent bubble, buffers the pipeline output, and replaces that bubble with the result (or `error: …`) when the command resolves — otherwise a slow or unreachable gateway would leave the user staring at nothing. Handled UI actions without output silently remove the pending bubble without leaving conversation artifacts. + +## Transport connection lifecycle + + +Every dashboard turn first connects a JSON-RPC WebSocket to the gateway; that handshake must be time-bounded or a stalled socket wedges the whole transport with no error and no fallback (issue #718). + +[[src/renderer/src/screens/Chat/dashboardGatewayClient.ts#DashboardGatewayClient#connect]] resolves on `open`, rejects on `error` or an early `close`, **and** rejects on a connect-timeout (default 10s). A WebSocket stuck in `CONNECTING` — TCP accepted but the upgrade never completing, e.g. when a busy renderer starves the handshake — fires none of those events on its own, so without the timer the connect promise never settles. When it never settles, `ensureClient` in [[src/renderer/src/screens/Chat/hooks/useDashboardChatTransport.ts#useDashboardChatTransport]] never resolves, its cached `connectingRef` promise poisons every later send, `setIsLoading(false)` never runs, and the user sees a permanent loading spinner. The timeout makes the promise reject so auto mode falls back to the legacy HTTP transport (and explicit-dashboard mode surfaces a real error) instead of hanging. Per-request calls are separately bounded by their own 30s timeout. + +## Dashboard up ⇒ /api/ws only (never /v1 fallback) + +When a dashboard is available, chat goes through `/api/ws` **only** — never the `/v1` fallback, which 405s over the dashboard tunnel. + +This matches the reference `apps/desktop`, which has no `/v1` chat path at all (its `use-prompt-actions.ts` submits via `requestGateway('prompt.submit', …)` with a busy-retry). The fork's main-process `/v1` path (`sendMessageViaApi`/`sendMessageViaRuns`) exists solely for genuine gateway-only remotes; falling to it while a dashboard is up POSTs `/v1` to the dashboard tunnel — which has no `/v1` → **405**. + +So `ensureClient` distinguishes two failures: a **genuinely absent** dashboard (`startDashboard` → `running:false`) latches the negative flag and (auto mode) drops to legacy gateway `/v1`; a **transient** WS drop while the dashboard is up (a "socket hang up" from a tunnel blip) instead **retries the connect** (up to 3×, re-running `startDashboard` each time to re-establish the SSH tunnel). If it still can't connect, it throws a `dashboardWasReachable`-tagged error so `sendMessage` **fails the turn for the user to retry** rather than 405-ing on `/v1`. + +## Completion text reconciliation + +On `message.complete` the desktop reconciles the text streamed via `message.delta` with the turn's `final_response`, because a last-turn-only final would otherwise clobber text streamed before a tool call (#746). + +[[src/renderer/src/screens/Chat/dashboardEventAdapter.ts#completeAssistantWithFinalText]] rewrites the last assistant bubble through [[src/renderer/src/screens/Chat/dashboardEventAdapter.ts#mergeStreamedWithFinal]], which compares whitespace-insensitively and: uses the final text when it already contains the streamed text; keeps the streamed text when it contains the final (preserving pre-tool-call content); stitches a re-streamed boundary by dropping the duplicated word-aligned seam (rejecting coincidental mid-word overlaps); replaces a garbled re-stream with the final text when the two converge on a substantial common suffix (a corrupted-prefix delta — e.g. a mangled CJK stream — that ends the same sentence as the clean final, rather than the disjoint pre-tool-call + answer pair); and otherwise concatenates the two with a blank-line separator so segments never run together. On the remote/SSH path deltas are not rendered (`renderAssistantDeltas: false`), so the bubble starts empty and the final text is used verbatim. + +## Streaming source-of-truth ref + +`handleGatewayEvent` in [[src/renderer/src/screens/Chat/hooks/useDashboardChatTransport.ts#useDashboardChatTransport]] applies stream events against a synchronous `messagesRef`, not React state, because state lags a render behind and each successive delta must build on the previous one. + +The handler reads the ref, applies a delta, writes the ref back, then calls `setMessages`. An effect mirrors `messages` back into `messagesRef`, and its guard is a correctness invariant. Every `setMessages` in the hook stores the exact same array in the ref, so when React commits the hook's own push, `messages === messagesRef.current` and the effect must skip: re-adopting that snapshot let a second `message.delta` land on a pre-delta array and silently drop a chunk (#757). The effect therefore syncs only when the identity differs (`messages !== messagesRef.current`), which happens only when Chat state changes underneath the hook — a new user turn, `handleClear` emptying the list, or a clarify card resolving in place. A length comparison is wrong here: it misses the shrink and the same-length replacement. + +## Reasoning & tool activity rows + +Streamed reasoning and tool calls are folded into compact, collapsible transcript rows rather than stacked bubbles, so a turn with heavy thinking or many tool calls stays scannable. + +[[src/renderer/src/screens/Chat/HistoryRow.tsx#ReasoningRow]] renders the `Thought` / `Thinking…` row and [[src/renderer/src/screens/Chat/HistoryRow.tsx#ToolActivityGroup]] folds a contiguous run of tool calls/results into one row titled by [[src/renderer/src/screens/Chat/HistoryRow.tsx#toolActivityGroupTitle]]. Each row is collapsed by default and borderless (Codex-style): dim at rest, it brightens and reveals an expand chevron beside the title on hover/focus, and clicking toggles the body open. While the turn is still streaming the leading icon is a `Grid` loader (purple for reasoning, blue for tools); once finished it shows the brain/tool glyph. + +## Bubble hover timestamp + +Each user/assistant bubble reveals a relative "time ago" label on row hover, so the transcript stays uncluttered at rest but is still scrutable when a user wants to know _when_ something was said. + +The canonical time comes from state.db: [[src/renderer/src/screens/Chat/sessionHistory.ts#dbItemsToChatMessages]] copies each row's `timestamp` onto the `ChatBubbleMessage`, and [[src/renderer/src/screens/Chat/sessionHistory.ts#reconcileAfterDbRefresh|the end-of-stream reconcile]] adopts it onto the matching streamed bubble (via `mergeDbMetadataIntoStreamed`) so a live turn picks up its real time after refresh without remounting. state.db stores times in **seconds**, so `toEpochMs` in MessageRow scales any sub-`1e12` value up to milliseconds before use (otherwise it renders as ~Jan 1970). [[src/renderer/src/screens/Chat/MessageRow.tsx#formatBubbleTime]] builds the label with date-fns `formatDistanceToNowStrict` (e.g. "5 minutes ago", "just now" under 10s), with `formatBubbleTimeAbsolute` supplying the exact date/time as the `