Compare commits

...

22 Commits

Author SHA1 Message Date
alt-glitch
987962f453 chore(restructure): add .git-blame-ignore-revs for restructure commits
Tells git blame (and GitHub) to skip the git-mv and import-rewrite
commits so blame shows the original author.
2026-04-23 15:26:41 +05:30
alt-glitch
25072fe690 fix(restructure): fix stale references missed by import rewrite
- plugins_cmd.py: import rewriter changed `import hermes_cli` to
  `import hermes_agent.cli` but left variable usage as `hermes_cli.__file__`,
  causing a NameError at runtime
- scripts/hermes-gateway: stale `from gateway.run import` (no .py extension
  so it was missed by **/*.py globs)
- scripts/install.ps1: stale `tools\skills_sync.py` path, use
  hermes-skills-sync console_script instead
2026-04-23 14:08:24 +05:30
alt-glitch
ff99611e16 refactor(restructure): update Nix files for hermes_agent package
Update import paths in nix/checks.nix smoke tests from
hermes_cli.config to hermes_agent.cli.config.

Part of #14182, #14183
2026-04-23 12:14:24 +05:30
alt-glitch
76aebd73c3 refactor(restructure): update infrastructure for hermes_agent package
Update pyproject.toml entry points, packages.find, and package-data.
Delete py-modules (all top-level modules moved into hermes_agent/).
Add hermes-skills-sync console_script entry point.
Update Dockerfile HERMES_WEB_DIST path.
Update docker/entrypoint.sh, scripts/install.sh, setup-hermes.sh
to use hermes-skills-sync console_script.
Update web/vite.config.ts output directory.
Update MANIFEST.in to graft hermes_agent.
Update AGENTS.md project structure to reflect new layout.

Part of #14182, #14183
2026-04-23 12:10:25 +05:30
alt-glitch
a1e667b9f2 fix(restructure): fix test regressions from import rewrite
Fix variable name breakage (run_agent, hermes_constants, etc.) where
import rewriter changed 'import X' to 'import hermes_agent.Y' but
test code still referenced 'X' as a variable name.

Fix package-vs-module confusion (cli.auth, cli.models, cli.ui) where
single files became directories.

Fix hardcoded file paths in tests pointing to old locations.
Fix tool registry to discover tools in subpackage directories.
Fix stale import in hermes_agent/tools/__init__.py.

Part of #14182, #14183
2026-04-23 12:05:10 +05:30
alt-glitch
4b16341975 refactor(restructure): rewrite all imports for hermes_agent package
Rewrite all import statements, patch() targets, sys.modules keys,
importlib.import_module() strings, and subprocess -m references to use
hermes_agent.* paths.

Strip sys.path.insert hacks from production code (rely on editable install).
Update COMPONENT_PREFIXES for logger filtering.
Fix 3 hardcoded getLogger() calls to use __name__.
Update transport and tool registry discovery paths.
Update plugin module path strings.
Add legacy process-name patterns for gateway PID detection.
Add main() to skills_sync for console_script entry point.
Fix _get_bundled_dir() path traversal after move.

Part of #14182, #14183
2026-04-23 08:35:34 +05:30
alt-glitch
65ca3ba93b refactor(restructure): git mv all source files into hermes_agent/ package
Pure file moves, zero content changes. Every file in agent/, tools/,
hermes_cli/, gateway/, acp_adapter/, cron/, and plugins/ moves into
hermes_agent/. Top-level modules (run_agent.py, cli.py, etc.) move to
their new homes per the restructure manifest.

Git sees 100% similarity on all moves.

Part of #14182, #14183
2026-04-23 08:07:10 +05:30
alt-glitch
8bff8bf2c0 chore(restructure): add move map for hermes_agent package restructure
Complete mapping of all 268 source files from old locations to new
hermes_agent/ package structure. Used by the restructure mover and
later by the migration script.

Part of #14182, #14183
2026-04-23 08:04:08 +05:30
alt-glitch
acd1f17b88 Clean up TODO comment in auxiliary_client.py
Remove the unnecessary nudge about agent refactoring; the TODO describes
the actual work that needs to be done.
2026-04-23 04:49:22 +05:30
alt-glitch
850973295e Add helpful ImportError messages for optional dependencies
When optional dependencies are missing, raise ImportError with
installation
instructions pointing to the relevant extras group (e.g. `[messaging]`,
`[cli]`, `[mcp]`, etc.) instead of letting the import fail silently.
2026-04-23 04:46:01 +05:30
alt-glitch
847ffca715 Merge remote-tracking branch 'origin/main' into sid/types-and-lints
# Conflicts:
#	gateway/run.py
#	tools/delegate_tool.py
2026-04-22 14:27:37 +05:30
alt-glitch
67bc441099 refactor(types): simplify pass on P1 batch
Follow-up to 15ac253b per /simplify review:

- gateway/platforms/discord.py:3638 - move self.resolved = True *after*
  the `if interaction.data is None: return` guard. Previously the view
  was marked resolved before the None-guard, so a None data payload
  silently rejected the user's next click.
- agent/display.py:732 - replace `if self.start_time is None: continue`
  with `assert self.start_time is not None`. start() sets start_time
  before the animate thread starts, so the None branch was dead; the
  `continue` form would have busy-looped (skipping the 0.12s sleep).
- tests/hermes_cli/test_config_shapes.py - drop __total__ dunder
  restatement test (it just echoes the class declaration); trim commit
  narration from module docstring.
- tests/agent/test_credential_pool.py, tests/tools/test_rl_training_tool.py -
  drop "added in commit ..." banners (narrates the change per CLAUDE.md).
2026-04-21 20:40:12 +05:30
alt-glitch
a9ed7cb3b4 Merge remote-tracking branch 'origin/main' into sid/types-and-lints
# Conflicts:
#	gateway/platforms/base.py
#	gateway/platforms/qqbot/adapter.py
#	gateway/platforms/slack.py
#	hermes_cli/main.py
#	scripts/batch_runner.py
#	tools/skills_tool.py
#	uv.lock
2026-04-21 20:28:45 +05:30
alt-glitch
15ac253b11 fix(types): batch P1 ty hotfixes + run_agent.py annotation pass
15 P1 ship-stopper runtime bugs from the ty triage plus the cross-bucket
cleanup in run_agent.py. Net: -138 ty diagnostics (1953 -> 1815). Major
wins on not-subscriptable (-34), unresolved-attribute (-29),
invalid-argument-type (-26), invalid-type-form (-20),
unsupported-operator
(-18), invalid-key (-9).

Missing refs (structural):
- tools/rl_training_tool.py: RunState dataclass gains api_log_file,
  trainer_log_file, env_log_file fields; stop-run was closing undeclared
  handles.
- agent/credential_pool.py: remove_entry(entry_id) added, symmetric with
  add_entry; used by hermes_cli/web_server.py OAuth dashboard cleanup.
- hermes_cli/config.py: _CamofoxConfig TypedDict defined (was referenced
  by _BrowserConfig but never declared).
- hermes_cli/gateway.py: _setup_wecom_callback() added, mirroring
  _setup_wecom().
- tui_gateway/server.py: skills_hub imports corrected from
  hermes_cli.skills_hub -> tools.skills_hub.

Typo / deprecation:
- tools/transcription_tools.py: os.sys.modules -> sys.modules.
- gateway/platforms/bluebubbles.py: datetime.utcnow() ->
  datetime.now(timezone.utc).

None-guards:
- gateway/platforms/telegram.py:~2798 - msg.sticker None guard.
- gateway/platforms/discord.py:3602/3637 - interaction.data None +
  SelectMenu narrowing; :3009 - thread_id None before `in`; :1893 -
  guild.member_count None.
- gateway/platforms/matrix.py:2174/2185 - walrus-narrow
  re.search().group().
- agent/display.py:732 - start_time None before elapsed subtraction.
- gateway/run.py:10334 - assert _agent_timeout is not None before `//
  60`.

Platform override signature match:
- gateway/platforms/email.py: send_image accepts metadata kwarg;
  send_document accepts **kwargs (matches base class).

run_agent.py annotation pass:
- callable/any -> Callable/Any in annotation position (15 sites in
  run_agent.py + 5 in cli.py, toolset_distributions.py,
  tools/delegate_tool.py, hermes_cli/dingtalk_auth.py,
  tui_gateway/server.py).
- conversation_history param widened to list[dict[str, Any]] | None.
- OMIT_TEMPERATURE sentinel guarded from leaking into
  call_llm(temperature): kwargs-dict pattern at run_agent.py:7337 +
  scripts/trajectory_compressor.py:618/688.
- build_anthropic_client(timeout) widened to Optional[float].

Tests:
- tests/agent/test_credential_pool.py: remove_entry (id match,
  unknown-id, priority renumbering).
- tests/hermes_cli/test_config_shapes.py: _CamofoxConfig shape +
  nesting.
- tests/tools/test_rl_training_tool.py: RunState log_file fields.
2026-04-21 20:20:13 +05:30
alt-glitch
fb6d37495b fix: resolve not-subscriptable ty diagnostics across codebase
Add TypedDicts for DEFAULT_CONFIG, CLI state dicts (_ModelPickerState,
_ApprovalState, _ClarifyState), and OPTIONAL_ENV_VARS so ty can resolve
nested dict subscripts.  Guard Optional returns before subscripting
(toolsets, cron/scheduler, delegate_tool), coerce str|None to str before
slicing (gateway/run, run_agent), split ternary for isinstance narrowing
(wecom), and suppress discord interaction.data access with ty: ignore.
2026-04-21 16:59:13 +05:30
alt-glitch
72e7c0ce34 fix: declare undeclared soft deps in extras and remove silent import guards
Previously mutagen, aiohttp-socks, tiktoken, Pillow, psutil, datasets,
neutts, and soundfile were used behind try/except ImportError with silent
fallbacks, masking broken functionality at runtime.  Declare each in its
natural extra (messaging, cli, mcp, rl, new tts-local) so they get
installed, and remove the guards so missing deps crash loudly.
2026-04-21 16:20:45 +05:30
alt-glitch
f8d2365795 fix: resolve all call-non-callable ty diagnostics across codebase
Replace hasattr() duck-typing with isinstance() checks for DiscordAdapter
in gateway/run.py, add TypedDict for IMAGEGEN_BACKENDS in tools_config.py,
properly type fal_client getattr'd callables in image_generation_tool.py,
fix dict[str, object] → Callable annotation in approval.py, use
isinstance(BaseModel) in web_tools.py, capture _message_handler to local
in base.py, rename shadowed list_distributions parameter in batch_runner.py,
and remove dead queue_message branch.
2026-04-21 16:00:37 +05:30
alt-glitch
ca2b6a529e refactor: move standalone scripts to scripts/ directory
Move batch_runner, trajectory_compressor, mini_swe_runner, and rl_cli
from the project root into scripts/, update all imports, logger names,
pyproject.toml, and downstream test references.
2026-04-21 15:23:23 +05:30
alt-glitch
224e6d46d9 fix: resolve all invalid-return-type ty diagnostics across codebase
Widen return type annotations to match actual control flow, add
unreachable assertions after retry loops ty cannot prove terminate,
split ambiguous union returns (auth.py credential pool), and remove
the AIOHTTP_AVAILABLE conditional-import guard from api_server.py.
2026-04-21 14:55:09 +05:30
alt-glitch
d3dde0b459 Add TYPE_CHECKING imports to fix unresolved-reference type bugs 2026-04-21 13:16:25 +05:30
alt-glitch
3f4c5ac71e refactor: remove remaining redundant local imports (comprehensive sweep)
Full AST-based scan of all .py files to find every case where a module
or name is imported locally inside a function body but is already
available at module level.  This is the second pass — the first commit
handled the known cases from the lint report; this one catches
everything else.

Files changed (19):

  cli.py                — 16 removals: time as _time/_t/_tmod (×10),
                           re / re as _re (×2), os as _os, sys,
                           partial os from combo import,
                           from model_tools import get_tool_definitions
  gateway/run.py        —  8 removals: MessageEvent as _ME /
                           MessageType as _MT (×3), os as _os2,
                           MessageEvent+MessageType (×2), Platform,
                           BasePlatformAdapter as _BaseAdapter
  run_agent.py          —  6 removals: get_hermes_home as _ghh,
                           partial (contextlib, os as _os),
                           cleanup_vm, cleanup_browser,
                           set_interrupt as _sif (×2),
                           partial get_toolset_for_tool
  hermes_cli/main.py    —  4 removals: get_hermes_home, time as _time,
                           logging as _log, shutil
  hermes_cli/config.py  —  1 removal:  get_hermes_home as _ghome
  hermes_cli/runtime_provider.py
                        —  1 removal:  load_config as _load_bedrock_config
  hermes_cli/setup.py   —  2 removals: importlib.util (×2)
  hermes_cli/nous_subscription.py
                        —  1 removal:  from hermes_cli.config import load_config
  hermes_cli/tools_config.py
                        —  1 removal:  from hermes_cli.config import load_config, save_config
  cron/scheduler.py     —  3 removals: concurrent.futures, json as _json,
                           from hermes_cli.config import load_config
  batch_runner.py       —  1 removal:  list_distributions as get_all_dists
                           (kept print_distribution_info, not at top level)
  tools/send_message_tool.py
                        —  2 removals: import os (×2)
  tools/skills_tool.py  —  1 removal:  logging as _logging
  tools/browser_camofox.py
                        —  1 removal:  from hermes_cli.config import load_config
  tools/image_generation_tool.py
                        —  1 removal:  import fal_client
  environments/tool_context.py
                        —  1 removal:  concurrent.futures
  gateway/platforms/bluebubbles.py
                        —  1 removal:  httpx as _httpx
  gateway/platforms/whatsapp.py
                        —  1 removal:  import asyncio
  tui_gateway/server.py —  2 removals: from datetime import datetime,
                           import time

All alias references (_time, _t, _tmod, _re, _os, _os2, _json, _ghh,
_ghome, _sif, _ME, _MT, _BaseAdapter, _load_bedrock_config, _httpx,
_logging, _log, get_all_dists) updated to use the top-level names.
2026-04-21 12:46:31 +05:30
alt-glitch
08c378356d refactor: remove redundant local imports already available at module level
Sweep ~74 redundant local imports across 21 files where the same module
was already imported at the top level. Also includes type fixes and lint
cleanups on the same branch.
2026-04-21 12:35:10 +05:30
1007 changed files with 15438 additions and 12779 deletions

5
.git-blame-ignore-revs Normal file
View File

@@ -0,0 +1,5 @@
# hermes_agent package restructure (PR 1/3)
# Commit 2: pure git mv — all source files into hermes_agent/
65ca3ba93b3fa7fd2b15af5b62d54020061f3672
# Commit 3: rewrite all imports for hermes_agent package
4b16341975a1217588054f567d0f76dc5a3cc481

137
AGENTS.md
View File

@@ -12,68 +12,59 @@ source venv/bin/activate # ALWAYS activate before running Python
```
hermes-agent/
├── run_agent.py # AIAgent class — core conversation loop
├── model_tools.py # Tool orchestration, discover_builtin_tools(), handle_function_call()
├── toolsets.py # Toolset definitions, _HERMES_CORE_TOOLS list
├── cli.py # HermesCLI class — interactive CLI orchestrator
├── hermes_state.py # SessionDB — SQLite session store (FTS5 search)
├── 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 (provider-aware context)
│ ├── 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 — CLI visual customization
│ ├── skills_config.py # `hermes skills` — enable/disable skills per platform
│ ├── tools_config.py # `hermes tools` — enable/disable tools per platform
│ ├── skills_hub.py # `/skills` slash command (search, browse, install)
│ ├── models.py # Model catalog, provider model lists
│ ├── model_switch.py # Shared /model switch pipeline (CLI + gateway)
│ └── auth.py # Provider credential resolution
├── tools/ # Tool implementations (one file per tool)
│ ├── registry.py # Central tool registry (schemas, handlers, dispatch)
│ ├── approval.py # Dangerous command detection
│ ├── terminal_tool.py # Terminal orchestration
│ ├── process_registry.py # Background process management
│ ├── file_tools.py # File read/write/search/patch
│ ├── web_tools.py # Web search/extract (Parallel + Firecrawl)
│ ├── browser_tool.py # Browserbase browser automation
├── code_execution_tool.py # execute_code sandbox
│ ├── delegate_tool.py # Subagent delegation
│ ├── mcp_tool.py # MCP client (~1050 lines)
└── environments/ # Terminal backends (local, docker, ssh, modal, daytona, singularity)
├── gateway/ # Messaging platform gateway
│ ├── run.py # Main loop, slash commands, message dispatch
│ ├── session.py # SessionStore — conversation persistence
── platforms/ # Adapters: telegram, discord, slack, whatsapp, homeassistant, signal, qqbot
├── ui-tui/ # Ink (React) terminal UI — `hermes --tui`
│ ├── src/entry.tsx # TTY gate + render()
│ ├── src/app.tsx # Main state machine and UI
── src/gatewayClient.ts # Child process + JSON-RPC bridge
│ ├── src/app/ # Decomposed app logic (event handler, slash handler, stores, hooks)
│ ├── src/components/ # Ink components (branding, markdown, prompts, pickers, etc.)
│ ├── src/hooks/ # useCompletion, useInputHistory, useQueue, useVirtualHistory
│ └── src/lib/ # Pure helpers (history, osc52, text, rpc, messages)
├── hermes_agent/ # Single installable package
│ ├── agent/ # Core conversation loop and agent internals
│ │ ├── loop.py # AIAgent class — core conversation loop
│ │ ├── prompt_builder.py # System prompt assembly
│ │ ├── context/ # Context management (engine, compressor, references)
│ │ ├── memory/ # Memory management (manager, provider)
│ ├── image_gen/ # Image generation (provider, registry)
│ ├── display.py # KawaiiSpinner, tool preview formatting
│ ├── skill_commands.py # Skill slash commands (shared CLI/gateway)
│ └── trajectory.py # Trajectory saving helpers
│ ├── providers/ # LLM provider adapters and transports
│ ├── anthropic_adapter.py # Anthropic adapter
│ ├── anthropic_transport.py # Anthropic transport
│ ├── metadata.py # Model context lengths, token estimation
│ ├── auxiliary.py # Auxiliary LLM client (vision, summarization)
│ │ ├── caching.py # Anthropic prompt caching
│ └── credential_pool.py # Credential management
│ ├── tools/ # Tool implementations
│ ├── dispatch.py # Tool orchestration, discover_builtin_tools()
│ ├── toolsets.py # Toolset definitions
│ ├── registry.py # Central tool registry
│ ├── terminal.py # Terminal orchestration
│ ├── browser/ # Browser tools (tool, cdp, camofox, providers/)
│ ├── mcp/ # MCP client and server
│ ├── skills/ # Skill management (manager, tool, hub, guard, sync)
│ ├── media/ # Voice, TTS, transcription, image gen
│ ├── files/ # File operations (tools, operations, state)
│ └── security/ # Path security, URL safety, approval
│ ├── backends/ # Terminal backends (local, docker, ssh, modal, daytona, singularity)
│ ├── cli/ # CLI subcommands and setup
│ ├── main.py # Entry point — all `hermes` subcommands
│ ├── repl.py # HermesCLI class — interactive CLI orchestrator
│ ├── config.py # DEFAULT_CONFIG, OPTIONAL_ENV_VARS, migration
│ ├── commands.py # Slash command definitions
│ ├── auth/ # Provider credential resolution
│ ├── models/ # Model catalog, provider lists, switching
│ └── ui/ # Banner, colors, skin engine, callbacks, tips
│ ├── gateway/ # Messaging platform gateway
│ ├── run.py # Main loop, slash commands, message dispatch
│ ├── session.py # SessionStore — conversation persistence
│ │ └── platforms/ # Adapters: telegram, discord, slack, whatsapp, etc.
│ ├── acp/ # ACP server (VS Code / Zed / JetBrains integration)
│ ├── cron/ # Scheduler (jobs.py, scheduler.py)
── plugins/ # Plugin system (memory providers, context engines)
│ ├── constants.py # Shared constants
│ ├── state.py # SessionDB — SQLite session store
│ ├── logging.py # Logging configuration
── utils.py # Shared utilities
├── tui_gateway/ # Python JSON-RPC backend for the TUI
│ ├── entry.py # stdio entrypoint
│ ├── server.py # RPC handlers and session logic
│ ├── render.py # Optional rich/ANSI bridge
│ └── slash_worker.py # Persistent HermesCLI subprocess for slash commands
├── acp_adapter/ # ACP server (VS Code / Zed / JetBrains integration)
├── cron/ # Scheduler (jobs.py, scheduler.py)
├── ui-tui/ # Ink (React) terminal UI — `hermes --tui`
├── environments/ # RL training environments (Atropos)
├── tests/ # Pytest suite (~3000 tests)
└── batch_runner.py # Parallel batch processing
├── tests/ # Pytest suite
└── web/ # Vite + React web dashboard
```
**User config:** `~/.hermes/config.yaml` (settings), `~/.hermes/.env` (API keys)
@@ -81,18 +72,18 @@ hermes-agent/
## File Dependency Chain
```
tools/registry.py (no deps — imported by all tool files)
hermes_agent/tools/registry.py (no deps — imported by all tool files)
tools/*.py (each calls registry.register() at import time)
hermes_agent/tools/*.py (each calls registry.register() at import time)
model_tools.py (imports tools/registry + triggers tool discovery)
hermes_agent/tools/dispatch.py (imports registry + triggers tool discovery)
run_agent.py, cli.py, batch_runner.py, environments/
hermes_agent/agent/loop.py, hermes_agent/cli/repl.py, environments/
```
---
## AIAgent Class (run_agent.py)
## AIAgent Class (hermes_agent/agent/loop.py)
```python
class AIAgent:
@@ -138,14 +129,14 @@ Messages follow OpenAI format: `{"role": "system/user/assistant/tool", ...}`. Re
---
## CLI Architecture (cli.py)
## CLI Architecture (hermes_agent/cli/repl.py)
- **Rich** for banner/panels, **prompt_toolkit** for input with autocomplete
- **KawaiiSpinner** (`agent/display.py`) — animated faces during API calls, `┊` activity feed for tool results
- `load_cli_config()` in cli.py merges hardcoded defaults + user config YAML
- **Skin engine** (`hermes_cli/skin_engine.py`) — data-driven CLI theming; initialized from `display.skin` config key at startup; skins customize banner colors, spinner faces/verbs/wings, tool prefix, response box, branding text
- **KawaiiSpinner** (`hermes_agent/agent/display.py`) — animated faces during API calls, `┊` activity feed for tool results
- `load_cli_config()` in repl.py merges hardcoded defaults + user config YAML
- **Skin engine** (`hermes_agent/cli/ui/skin_engine.py`) — data-driven CLI theming; initialized from `display.skin` config key at startup; skins customize banner colors, spinner faces/verbs/wings, tool prefix, response box, branding text
- `process_command()` is a method on `HermesCLI` — dispatches on canonical command name resolved via `resolve_command()` from the central registry
- Skill slash commands: `agent/skill_commands.py` scans `~/.hermes/skills/`, injects as **user message** (not system prompt) to preserve prompt caching
- Skill slash commands: `hermes_agent/agent/skill_commands.py` scans `~/.hermes/skills/`, injects as **user message** (not system prompt) to preserve prompt caching
### Slash Command Registry (`hermes_cli/commands.py`)
@@ -272,7 +263,7 @@ registry.register(
**2. Add to `toolsets.py`** — either `_HERMES_CORE_TOOLS` (all platforms) or a new toolset.
Auto-discovery: any `tools/*.py` file with a top-level `registry.register()` call is imported automatically — no manual import list to maintain.
Auto-discovery: any `hermes_agent/tools/*.py` file with a top-level `registry.register()` call is imported automatically — no manual import list to maintain.
The registry handles schema collection, dispatch, availability checking, and error wrapping. All handlers MUST return a JSON string.
@@ -498,11 +489,11 @@ Rendering bugs in tmux/iTerm2 — ghosting on scroll. Use `curses` (stdlib) inst
### DO NOT use `\033[K` (ANSI erase-to-EOL) in spinner/display code
Leaks as literal `?[K` text under `prompt_toolkit`'s `patch_stdout`. Use space-padding: `f"\r{line}{' ' * pad}"`.
### `_last_resolved_tool_names` is a process-global in `model_tools.py`
### `_last_resolved_tool_names` is a process-global in `hermes_agent/tools/dispatch.py`
`_run_single_child()` in `delegate_tool.py` saves and restores this global around subagent execution. If you add new code that reads this global, be aware it may be temporarily stale during child agent runs.
### DO NOT hardcode cross-tool references in schema descriptions
Tool schema descriptions must not mention tools from other toolsets by name (e.g., `browser_navigate` saying "prefer web_search"). Those tools may be unavailable (missing API keys, disabled toolset), causing the model to hallucinate calls to non-existent tools. If a cross-reference is needed, add it dynamically in `get_tool_definitions()` in `model_tools.py` — see the `browser_navigate` / `execute_code` post-processing blocks for the pattern.
Tool schema descriptions must not mention tools from other toolsets by name (e.g., `browser_navigate` saying "prefer web_search"). Those tools may be unavailable (missing API keys, disabled toolset), causing the model to hallucinate calls to non-existent tools. If a cross-reference is needed, add it dynamically in `get_tool_definitions()` in `hermes_agent/tools/dispatch.py` — see the `browser_navigate` / `execute_code` post-processing blocks for the pattern.
### Tests must not write to `~/.hermes/`
The `_isolate_hermes_home` autouse fixture in `tests/conftest.py` redirects `HERMES_HOME` to a temp dir. Never hardcode `~/.hermes/` paths in tests.

View File

@@ -38,7 +38,7 @@ RUN npm install --prefer-offline --no-audit && \
# .dockerignore excludes node_modules, so the installs above survive.
COPY --chown=hermes:hermes . .
# Build web dashboard (Vite outputs to hermes_cli/web_dist/)
# Build web dashboard (Vite outputs to hermes_agent/cli/web_dist/)
RUN cd web && npm run build
# ---------- Python virtualenv ----------
@@ -48,7 +48,7 @@ RUN uv venv && \
uv pip install --no-cache-dir -e ".[all]"
# ---------- Runtime ----------
ENV HERMES_WEB_DIST=/opt/hermes/hermes_cli/web_dist
ENV HERMES_WEB_DIST=/opt/hermes/hermes_agent/cli/web_dist
ENV HERMES_HOME=/opt/data
VOLUME [ "/opt/data" ]
ENTRYPOINT [ "/opt/hermes/docker/entrypoint.sh" ]

View File

@@ -1,3 +1,4 @@
graft hermes_agent
graft skills
graft optional-skills
global-exclude __pycache__

View File

@@ -29,7 +29,7 @@ echo "📝 Logging to: $LOG_FILE"
# Point to the example dataset in this directory
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
python batch_runner.py \
python scripts/batch_runner.py \
--dataset_file="$SCRIPT_DIR/example_browser_tasks.jsonl" \
--batch_size=5 \
--run_name="browser_tasks_example" \

View File

@@ -4,7 +4,7 @@
# Generates tool-calling trajectories for multi-step web research tasks.
#
# Usage:
# python batch_runner.py \
# python scripts/batch_runner.py \
# --config datagen-config-examples/web_research.yaml \
# --run_name web_research_v1

View File

@@ -65,7 +65,7 @@ fi
# Sync bundled skills (manifest-based so user edits are preserved)
if [ -d "$INSTALL_DIR/skills" ]; then
python3 "$INSTALL_DIR/tools/skills_sync.py"
hermes-skills-sync
fi
exec hermes "$@"

View File

@@ -18,11 +18,14 @@ import logging
import os
import uuid
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Set
from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING
from model_tools import handle_function_call
from tools.terminal_tool import get_active_env
from tools.tool_result_storage import maybe_persist_tool_result, enforce_turn_budget
if TYPE_CHECKING:
from hermes_agent.tools.budget_config import BudgetConfig
from hermes_agent.tools.dispatch import handle_function_call
from hermes_agent.tools.terminal import get_active_env
from hermes_agent.tools.result_storage import maybe_persist_tool_result, enforce_turn_budget
# Thread pool for running sync tool calls that internally use asyncio.run()
# (e.g., the Modal/Docker/Daytona terminal backends). Running them in a separate
@@ -161,7 +164,7 @@ class HermesAgentLoop:
thresholds, per-turn aggregate budget, and preview size.
If None, uses DEFAULT_BUDGET (current hardcoded values).
"""
from tools.budget_config import DEFAULT_BUDGET
from hermes_agent.tools.budget_config import DEFAULT_BUDGET
self.server = server
self.tool_schemas = tool_schemas
self.valid_tool_names = valid_tool_names
@@ -187,7 +190,7 @@ class HermesAgentLoop:
tool_errors: List[ToolError] = []
# Per-loop TodoStore for the todo tool (ephemeral, dies with the loop)
from tools.todo_tool import TodoStore, todo_tool as _todo_tool
from hermes_agent.tools.todo import TodoStore, todo_tool as _todo_tool
_todo_store = TodoStore()
# Extract user task from first user message for browser_snapshot context

View File

@@ -60,7 +60,7 @@ from atroposlib.envs.server_handling.server_manager import APIServerConfig
from environments.agent_loop import AgentResult, HermesAgentLoop
from environments.hermes_base_env import HermesAgentBaseEnv, HermesAgentEnvConfig
from environments.tool_context import ToolContext
from tools.terminal_tool import (
from hermes_agent.tools.terminal import (
register_task_env_overrides,
clear_task_env_overrides,
cleanup_vm,
@@ -876,7 +876,7 @@ class TerminalBench2EvalEnv(HermesAgentBaseEnv):
# Let cancellations propagate (finally blocks run cleanup_vm)
await asyncio.gather(*eval_tasks, return_exceptions=True)
# Belt-and-suspenders: clean up any remaining sandboxes
from tools.terminal_tool import cleanup_all_environments
from hermes_agent.tools.terminal import cleanup_all_environments
cleanup_all_environments()
print("All sandboxes cleaned up.")
return
@@ -984,7 +984,7 @@ class TerminalBench2EvalEnv(HermesAgentBaseEnv):
# Kill all remaining sandboxes. Timed-out tasks leave orphaned thread
# pool workers still executing commands -- cleanup_all stops them.
from tools.terminal_tool import cleanup_all_environments
from hermes_agent.tools.terminal import cleanup_all_environments
print("\nCleaning up all sandboxes...")
cleanup_all_environments()

View File

@@ -709,7 +709,7 @@ class YCBenchEvalEnv(HermesAgentBaseEnv):
tqdm.write("\n[INTERRUPTED] Stopping evaluation...")
pbar.close()
try:
from tools.terminal_tool import cleanup_all_environments
from hermes_agent.tools.terminal import cleanup_all_environments
cleanup_all_environments()
except Exception:
pass
@@ -819,7 +819,7 @@ class YCBenchEvalEnv(HermesAgentBaseEnv):
print(f"Results saved to: {self._streaming_path}")
try:
from tools.terminal_tool import cleanup_all_environments
from hermes_agent.tools.terminal import cleanup_all_environments
cleanup_all_environments()
except Exception:
pass

View File

@@ -62,15 +62,15 @@ from atroposlib.type_definitions import Item
from environments.agent_loop import AgentResult, HermesAgentLoop
from environments.tool_context import ToolContext
from tools.budget_config import (
from hermes_agent.tools.budget_config import (
DEFAULT_RESULT_SIZE_CHARS,
DEFAULT_TURN_BUDGET_CHARS,
DEFAULT_PREVIEW_SIZE_CHARS,
)
# Import hermes-agent toolset infrastructure
from model_tools import get_tool_definitions
from toolset_distributions import sample_toolsets_from_distribution
from hermes_agent.tools.dispatch import get_tool_definitions
from hermes_agent.tools.distributions import sample_toolsets_from_distribution
logger = logging.getLogger(__name__)
@@ -209,7 +209,7 @@ class HermesAgentEnvConfig(BaseEnvConfig):
def build_budget_config(self):
"""Build a BudgetConfig from env config fields."""
from tools.budget_config import BudgetConfig
from hermes_agent.tools.budget_config import BudgetConfig
return BudgetConfig(
default_result_size=self.default_result_size_chars,
turn_budget=self.turn_budget_chars,

View File

@@ -31,9 +31,9 @@ from typing import Any, Dict, List, Optional
import asyncio
import concurrent.futures
from model_tools import handle_function_call
from tools.terminal_tool import cleanup_vm
from tools.browser_tool import cleanup_browser
from hermes_agent.tools.dispatch import handle_function_call
from hermes_agent.tools.terminal import cleanup_vm
from hermes_agent.tools.browser.tool import cleanup_browser
logger = logging.getLogger(__name__)
@@ -446,7 +446,7 @@ class ToolContext:
"""
# Kill any background processes from this rollout (safety net)
try:
from tools.process_registry import process_registry
from hermes_agent.tools.process_registry import process_registry
killed = process_registry.kill_all(task_id=self.task_id)
if killed:
logger.debug("Process cleanup for task %s: killed %d process(es)", self.task_id, killed)

2
hermes
View File

@@ -7,5 +7,5 @@ subcommands such as `gateway`, `cron`, and `doctor`.
"""
if __name__ == "__main__":
from hermes_cli.main import main
from hermes_agent.cli.main import main
main()

0
hermes_agent/__init__.py Normal file
View File

View File

@@ -8,7 +8,7 @@ from typing import Optional
def detect_provider() -> Optional[str]:
"""Resolve the active Hermes runtime provider, or None if unavailable."""
try:
from hermes_cli.runtime_provider import resolve_runtime_provider
from hermes_agent.cli.runtime_provider import resolve_runtime_provider
runtime = resolve_runtime_provider()
api_key = runtime.get("api_key")
provider = runtime.get("provider")

View File

@@ -17,7 +17,7 @@ import asyncio
import logging
import sys
from pathlib import Path
from hermes_constants import get_hermes_home
from hermes_agent.constants import get_hermes_home
# Methods clients send as periodic liveness probes. They are not part of the
@@ -83,7 +83,7 @@ def _setup_logging() -> None:
def _load_env() -> None:
"""Load .env from HERMES_HOME (default ``~/.hermes``)."""
from hermes_cli.env_loader import load_hermes_dotenv
from hermes_agent.cli.env_loader import load_hermes_dotenv
hermes_home = get_hermes_home()
loaded = load_hermes_dotenv(hermes_home=hermes_home)
@@ -104,11 +104,6 @@ def main() -> None:
logger = logging.getLogger(__name__)
logger.info("Starting hermes-agent ACP adapter")
# Ensure the project root is on sys.path so ``from run_agent import AIAgent`` works
project_root = str(Path(__file__).resolve().parent.parent)
if project_root not in sys.path:
sys.path.insert(0, project_root)
import acp
from .server import HermesACPAgent

View File

@@ -88,7 +88,7 @@ def make_tool_progress_cb(
snapshot = None
if name in {"write_file", "patch", "skill_manage"}:
try:
from agent.display import capture_local_edit_snapshot
from hermes_agent.agent.display import capture_local_edit_snapshot
snapshot = capture_local_edit_snapshot(name, args)
except Exception:

View File

@@ -52,20 +52,20 @@ try:
except ImportError:
from acp.schema import AuthMethod as AuthMethodAgent # type: ignore[attr-defined]
from acp_adapter.auth import detect_provider
from acp_adapter.events import (
from hermes_agent.acp.auth import detect_provider
from hermes_agent.acp.events import (
make_message_cb,
make_step_cb,
make_thinking_cb,
make_tool_progress_cb,
)
from acp_adapter.permissions import make_approval_callback
from acp_adapter.session import SessionManager, SessionState
from hermes_agent.acp.permissions import make_approval_callback
from hermes_agent.acp.session import SessionManager, SessionState
logger = logging.getLogger(__name__)
try:
from hermes_cli import __version__ as HERMES_VERSION
from hermes_agent.cli import __version__ as HERMES_VERSION
except Exception:
HERMES_VERSION = "0.0.0"
@@ -172,7 +172,7 @@ class HermesACPAgent(acp.Agent):
provider = getattr(state.agent, "provider", None) or detect_provider() or "openrouter"
try:
from hermes_cli.models import curated_models_for_provider, normalize_provider, provider_label
from hermes_agent.cli.models.models import curated_models_for_provider, normalize_provider, provider_label
normalized_provider = normalize_provider(provider)
provider_name = provider_label(normalized_provider)
@@ -235,7 +235,7 @@ class HermesACPAgent(acp.Agent):
new_model = raw_model.strip()
try:
from hermes_cli.models import detect_provider_for_model, parse_model_input
from hermes_agent.cli.models.models import detect_provider_for_model, parse_model_input
target_provider, new_model = parse_model_input(new_model, current_provider)
if target_provider == current_provider:
@@ -257,7 +257,7 @@ class HermesACPAgent(acp.Agent):
return
try:
from tools.mcp_tool import register_mcp_servers
from hermes_agent.tools.mcp.tool import register_mcp_servers
config_map: dict[str, dict] = {}
for server in mcp_servers:
@@ -285,7 +285,7 @@ class HermesACPAgent(acp.Agent):
return
try:
from model_tools import get_tool_definitions
from hermes_agent.tools.dispatch import get_tool_definitions
enabled_toolsets = getattr(state.agent, "enabled_toolsets", None) or ["hermes-acp"]
disabled_toolsets = getattr(state.agent, "disabled_toolsets", None)
@@ -572,7 +572,7 @@ class HermesACPAgent(acp.Agent):
nonlocal previous_approval_cb, previous_interactive
if approval_cb:
try:
from tools import terminal_tool as _terminal_tool
from hermes_agent.tools import terminal as _terminal_tool
previous_approval_cb = _terminal_tool._get_approval_callback()
_terminal_tool.set_approval_callback(approval_cb)
except Exception:
@@ -599,7 +599,7 @@ class HermesACPAgent(acp.Agent):
os.environ["HERMES_INTERACTIVE"] = previous_interactive
if approval_cb:
try:
from tools import terminal_tool as _terminal_tool
from hermes_agent.tools import terminal as _terminal_tool
_terminal_tool.set_approval_callback(previous_approval_cb)
except Exception:
logger.debug("Could not restore approval callback", exc_info=True)
@@ -618,7 +618,7 @@ class HermesACPAgent(acp.Agent):
final_response = result.get("final_response", "")
if final_response:
try:
from agent.title_generator import maybe_auto_title
from hermes_agent.agent.title_generator import maybe_auto_title
maybe_auto_title(
self.session_manager._get_db(),
@@ -753,7 +753,7 @@ class HermesACPAgent(acp.Agent):
def _cmd_tools(self, args: str, state: SessionState) -> str:
try:
from model_tools import get_tool_definitions
from hermes_agent.tools.dispatch import get_tool_definitions
toolsets = getattr(state.agent, "enabled_toolsets", None) or ["hermes-acp"]
tools = get_tool_definitions(enabled_toolsets=toolsets, quiet_mode=True)
if not tools:
@@ -804,7 +804,7 @@ class HermesACPAgent(acp.Agent):
if not hasattr(agent, "_compress_context"):
return "Context compression not available for this agent."
from agent.model_metadata import estimate_messages_tokens_rough
from hermes_agent.providers.metadata import estimate_messages_tokens_rough
original_count = len(state.history)
approx_tokens = estimate_messages_tokens_rough(state.history)

View File

@@ -8,7 +8,7 @@ history.
"""
from __future__ import annotations
from hermes_constants import get_hermes_home
from hermes_agent.constants import get_hermes_home
import copy
import json
@@ -100,7 +100,7 @@ def _register_task_cwd(task_id: str, cwd: str) -> None:
if not task_id:
return
try:
from tools.terminal_tool import register_task_env_overrides
from hermes_agent.tools.terminal import register_task_env_overrides
register_task_env_overrides(task_id, {"cwd": cwd})
except Exception:
logger.debug("Failed to register ACP task cwd override", exc_info=True)
@@ -111,7 +111,7 @@ def _clear_task_cwd(task_id: str) -> None:
if not task_id:
return
try:
from tools.terminal_tool import clear_task_env_overrides
from hermes_agent.tools.terminal import clear_task_env_overrides
clear_task_env_overrides(task_id)
except Exception:
logger.debug("Failed to clear ACP task cwd override", exc_info=True)
@@ -355,7 +355,7 @@ class SessionManager:
if self._db_instance is not None:
return self._db_instance
try:
from hermes_state import SessionDB
from hermes_agent.state import SessionDB
hermes_home = get_hermes_home()
self._db_instance = SessionDB(db_path=hermes_home / "state.db")
return self._db_instance
@@ -523,9 +523,9 @@ class SessionManager:
if self._agent_factory is not None:
return self._agent_factory()
from run_agent import AIAgent
from hermes_cli.config import load_config
from hermes_cli.runtime_provider import resolve_runtime_provider
from hermes_agent.agent.loop import AIAgent
from hermes_agent.cli.config import load_config
from hermes_agent.cli.runtime_provider import resolve_runtime_provider
config = load_config()
model_cfg = config.get("model")

View File

@@ -103,7 +103,7 @@ def _build_patch_mode_content(patch_text: str) -> List[Any]:
return [acp.tool_content(acp.text_block(""))]
try:
from tools.patch_parser import OperationType, parse_v4a_patch
from hermes_agent.tools.patch_parser import OperationType, parse_v4a_patch
operations, error = parse_v4a_patch(patch_text)
if error or not operations:
@@ -243,7 +243,7 @@ def _build_tool_complete_content(
if tool_name in {"write_file", "patch", "skill_manage"}:
try:
from agent.display import extract_edit_diff
from hermes_agent.agent.display import extract_edit_diff
diff_text = extract_edit_diff(
tool_name,

View File

View File

@@ -24,14 +24,14 @@ import re
import time
from typing import Any, Dict, List, Optional
from agent.auxiliary_client import call_llm
from agent.context_engine import ContextEngine
from agent.model_metadata import (
from hermes_agent.providers.auxiliary import call_llm
from hermes_agent.agent.context.engine import ContextEngine
from hermes_agent.providers.metadata import (
MINIMUM_CONTEXT_LENGTH,
get_model_context_length,
estimate_messages_tokens_rough,
)
from agent.redact import redact_sensitive_text
from hermes_agent.agent.redact import redact_sensitive_text
logger = logging.getLogger(__name__)

View File

@@ -11,7 +11,7 @@ from dataclasses import dataclass, field
from pathlib import Path
from typing import Awaitable, Callable
from agent.model_metadata import estimate_tokens_rough
from hermes_agent.providers.metadata import estimate_tokens_rough
_QUOTED_REFERENCE_VALUE = r'(?:`[^`\n]+`|"[^"\n]+"|\'[^\'\n]+\')'
REFERENCE_PATTERN = re.compile(
@@ -315,7 +315,7 @@ async def _fetch_url_content(
async def _default_url_fetcher(url: str) -> str:
from tools.web_tools import web_extract_tool
from hermes_agent.tools.web import web_extract_tool
raw = await web_extract_tool([url], format="markdown", use_llm_processing=True)
payload = json.loads(raw)
@@ -340,7 +340,7 @@ def _resolve_path(cwd: Path, target: str, *, allowed_root: Path | None = None) -
def _ensure_reference_path_allowed(path: Path) -> None:
from hermes_constants import get_hermes_home
from hermes_agent.constants import get_hermes_home
home = Path(os.path.expanduser("~")).resolve()
hermes_home = get_hermes_home().resolve()

View File

@@ -21,8 +21,8 @@ from pathlib import Path
from types import SimpleNamespace
from typing import Any
from agent.file_safety import get_read_block_error, is_write_denied
from agent.redact import redact_sensitive_text
from hermes_agent.agent.file_safety import get_read_block_error, is_write_denied
from hermes_agent.agent.redact import redact_sensitive_text
ACP_MARKER_BASE_URL = "acp://copilot"
_DEFAULT_TIMEOUT_SECONDS = 900.0

View File

@@ -13,7 +13,7 @@ from dataclasses import dataclass, field
from difflib import unified_diff
from pathlib import Path
from utils import safe_json_loads
from hermes_agent.utils import safe_json_loads
# ANSI escape codes for coloring tool failure indicators
_RED = "\033[31m"
@@ -43,7 +43,7 @@ def _diff_ansi() -> dict[str, str]:
plus = "\033[38;2;255;255;255;48;2;20;90;20m"
try:
from hermes_cli.skin_engine import get_active_skin
from hermes_agent.cli.ui.skin_engine import get_active_skin
skin = get_active_skin()
def _hex_fg(key: str, fallback_rgb: tuple[int, int, int]) -> str:
@@ -118,7 +118,7 @@ def get_tool_preview_max_len() -> int:
def _get_skin():
"""Get the active skin config, or None if not available."""
try:
from hermes_cli.skin_engine import get_active_skin
from hermes_agent.cli.ui.skin_engine import get_active_skin
return get_active_skin()
except Exception:
return None
@@ -148,7 +148,7 @@ def get_tool_emoji(tool_name: str, default: str = "⚡") -> str:
return override
# 2. Registry default
try:
from tools.registry import registry
from hermes_agent.tools.registry import registry
emoji = registry.get_emoji(tool_name, default="")
if emoji:
return emoji
@@ -311,7 +311,7 @@ def _resolve_skill_manage_paths(args: dict) -> list[Path]:
if not action or not name:
return []
from tools.skill_manager_tool import _find_skill, _resolve_skill_dir
from hermes_agent.tools.skills.manager import _find_skill, _resolve_skill_dir
if action == "create":
skill_dir = _resolve_skill_dir(name, args.get("category"))
@@ -729,6 +729,7 @@ class KawaiiSpinner:
time.sleep(0.1)
continue
frame = self.spinner_frames[self.frame_idx % len(self.spinner_frames)]
assert self.start_time is not None # start() sets it before thread starts
elapsed = time.time() - self.start_time
if wings:
left, right = wings[self.frame_idx % len(wings)]

View File

@@ -10,7 +10,7 @@ from typing import Optional
def _hermes_home_path() -> Path:
"""Resolve the active HERMES_HOME (profile-aware) without circular imports."""
try:
from hermes_constants import get_hermes_home # local import to avoid cycles
from hermes_agent.constants import get_hermes_home # local import to avoid cycles
return get_hermes_home()
except Exception:
return Path(os.path.expanduser("~/.hermes"))

View File

View File

@@ -164,7 +164,7 @@ def resolve_aspect_ratio(value: Optional[str]) -> str:
def _images_cache_dir() -> Path:
"""Return ``$HERMES_HOME/cache/images/``, creating parents as needed."""
from hermes_constants import get_hermes_home
from hermes_agent.constants import get_hermes_home
path = get_hermes_home() / "cache" / "images"
path.mkdir(parents=True, exist_ok=True)

View File

@@ -24,7 +24,7 @@ import logging
import threading
from typing import Dict, List, Optional
from agent.image_gen_provider import ImageGenProvider
from hermes_agent.agent.image_gen.provider import ImageGenProvider
logger = logging.getLogger(__name__)
@@ -80,7 +80,7 @@ def get_active_provider() -> Optional[ImageGenProvider]:
"""
configured: Optional[str] = None
try:
from hermes_cli.config import load_config
from hermes_agent.cli.config import load_config
cfg = load_config()
section = cfg.get("image_gen") if isinstance(cfg, dict) else None

View File

@@ -10,7 +10,7 @@ multi-platform architecture with additional cost estimation and platform
breakdown capabilities.
Usage:
from agent.insights import InsightsEngine
from hermes_agent.agent.insights import InsightsEngine
engine = InsightsEngine(db)
report = engine.generate(days=30)
print(engine.format_terminal(report))
@@ -22,7 +22,7 @@ from collections import Counter, defaultdict
from datetime import datetime
from typing import Any, Dict, List
from agent.usage_pricing import (
from hermes_agent.providers.pricing import (
CanonicalUsage,
DEFAULT_PRICING,
estimate_usage_cost,

View File

@@ -14,7 +14,7 @@ Features:
- Support for multiple model providers
Usage:
from run_agent import AIAgent
from hermes_agent.agent.loop import AIAgent
agent = AIAgent(base_url="http://localhost:30000/v1", model="claude-opus-4-20250514")
response = agent.run_conversation("Tell me about the latest Python updates")
@@ -37,18 +37,21 @@ import time
import threading
from types import SimpleNamespace
import uuid
from typing import List, Dict, Any, Optional
from typing import Callable, List, Dict, Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from hermes_agent.providers.rate_limiting import RateLimitState
from openai import OpenAI
import fire
from datetime import datetime
from pathlib import Path
from hermes_constants import get_hermes_home
from hermes_agent.constants import get_hermes_home
# Load .env from ~/.hermes/.env first, then project root as dev fallback.
# User-managed env files should override stale shell exports on restart.
from hermes_cli.env_loader import load_hermes_dotenv
from hermes_cli.timeouts import (
from hermes_agent.cli.env_loader import load_hermes_dotenv
from hermes_agent.cli.timeouts import (
get_provider_request_timeout,
get_provider_stale_timeout,
)
@@ -64,30 +67,30 @@ else:
# Import our tool system
from model_tools import (
from hermes_agent.tools.dispatch import (
get_tool_definitions,
get_toolset_for_tool,
handle_function_call,
check_toolset_requirements,
)
from tools.terminal_tool import cleanup_vm, get_active_env, is_persistent_env
from tools.tool_result_storage import maybe_persist_tool_result, enforce_turn_budget
from tools.interrupt import set_interrupt as _set_interrupt
from tools.browser_tool import cleanup_browser
from hermes_agent.tools.terminal import cleanup_vm, get_active_env, is_persistent_env
from hermes_agent.tools.result_storage import maybe_persist_tool_result, enforce_turn_budget
from hermes_agent.tools.interrupt import set_interrupt as _set_interrupt
from hermes_agent.tools.browser.tool import cleanup_browser
from hermes_constants import OPENROUTER_BASE_URL
from hermes_agent.constants import OPENROUTER_BASE_URL
# Agent internals extracted to agent/ package for modularity
from agent.memory_manager import build_memory_context_block, sanitize_context
from agent.retry_utils import jittered_backoff
from agent.error_classifier import classify_api_error, FailoverReason
from agent.prompt_builder import (
from hermes_agent.agent.memory.manager import build_memory_context_block, sanitize_context
from hermes_agent.providers.retry import jittered_backoff
from hermes_agent.providers.errors import classify_api_error, FailoverReason
from hermes_agent.agent.prompt_builder import (
DEFAULT_AGENT_IDENTITY, PLATFORM_HINTS,
MEMORY_GUIDANCE, SESSION_SEARCH_GUIDANCE, SKILLS_GUIDANCE,
build_nous_subscription_prompt,
)
from agent.model_metadata import (
from hermes_agent.providers.metadata import (
fetch_model_metadata,
estimate_tokens_rough, estimate_messages_tokens_rough, estimate_request_tokens_rough,
get_next_probe_tier, parse_context_limit_from_error,
@@ -95,12 +98,12 @@ from agent.model_metadata import (
save_context_length, is_local_endpoint,
query_ollama_num_ctx,
)
from agent.context_compressor import ContextCompressor
from agent.subdirectory_hints import SubdirectoryHintTracker
from agent.prompt_caching import apply_anthropic_cache_control
from agent.prompt_builder import build_skills_system_prompt, build_context_files_prompt, build_environment_hints, load_soul_md, TOOL_USE_ENFORCEMENT_GUIDANCE, TOOL_USE_ENFORCEMENT_MODELS, DEVELOPER_ROLE_MODELS, GOOGLE_MODEL_OPERATIONAL_GUIDANCE, OPENAI_MODEL_EXECUTION_GUIDANCE
from agent.usage_pricing import estimate_usage_cost, normalize_usage
from agent.codex_responses_adapter import (
from hermes_agent.agent.context.compressor import ContextCompressor
from hermes_agent.agent.subdirectory_hints import SubdirectoryHintTracker
from hermes_agent.providers.caching import apply_anthropic_cache_control
from hermes_agent.agent.prompt_builder import build_skills_system_prompt, build_context_files_prompt, build_environment_hints, load_soul_md, TOOL_USE_ENFORCEMENT_GUIDANCE, TOOL_USE_ENFORCEMENT_MODELS, DEVELOPER_ROLE_MODELS, GOOGLE_MODEL_OPERATIONAL_GUIDANCE, OPENAI_MODEL_EXECUTION_GUIDANCE
from hermes_agent.providers.pricing import estimate_usage_cost, normalize_usage
from hermes_agent.providers.codex_adapter import (
_chat_content_to_responses_parts,
_chat_messages_to_responses_input as _codex_chat_messages_to_responses_input,
_derive_responses_function_call_id as _codex_derive_responses_function_call_id,
@@ -114,17 +117,17 @@ from agent.codex_responses_adapter import (
_split_responses_tool_id as _codex_split_responses_tool_id,
_summarize_user_message_for_log,
)
from agent.display import (
from hermes_agent.agent.display import (
KawaiiSpinner, build_tool_preview as _build_tool_preview,
get_cute_tool_message as _get_cute_tool_message_impl,
_detect_tool_failure,
get_tool_emoji as _get_tool_emoji,
)
from agent.trajectory import (
from hermes_agent.agent.trajectory import (
convert_scratchpad_to_think, has_incomplete_scratchpad,
save_trajectory as _save_trajectory_to_file,
)
from utils import atomic_json_write, base_url_host_matches, base_url_hostname, env_var_enabled, normalize_proxy_url
from hermes_agent.utils import atomic_json_write, base_url_host_matches, base_url_hostname, env_var_enabled, normalize_proxy_url
@@ -733,17 +736,17 @@ class AIAgent:
provider_require_parameters: bool = False,
provider_data_collection: str = None,
session_id: str = None,
tool_progress_callback: callable = None,
tool_start_callback: callable = None,
tool_complete_callback: callable = None,
thinking_callback: callable = None,
reasoning_callback: callable = None,
clarify_callback: callable = None,
step_callback: callable = None,
stream_delta_callback: callable = None,
interim_assistant_callback: callable = None,
tool_gen_callback: callable = None,
status_callback: callable = None,
tool_progress_callback: Callable[..., Any] = None,
tool_start_callback: Callable[..., Any] = None,
tool_complete_callback: Callable[..., Any] = None,
thinking_callback: Callable[..., Any] = None,
reasoning_callback: Callable[..., Any] = None,
clarify_callback: Callable[..., Any] = None,
step_callback: Callable[..., Any] = None,
stream_delta_callback: Callable[..., Any] = None,
interim_assistant_callback: Callable[..., Any] = None,
tool_gen_callback: Callable[..., Any] = None,
status_callback: Callable[..., Any] = None,
max_tokens: int = None,
reasoning_config: Dict[str, Any] = None,
service_tier: str = None,
@@ -873,7 +876,7 @@ class AIAgent:
self.api_mode = "chat_completions"
try:
from hermes_cli.model_normalize import (
from hermes_agent.cli.models.normalize import (
_AGGREGATOR_PROVIDERS,
normalize_model_for_provider,
)
@@ -1023,7 +1026,7 @@ class AIAgent:
# Centralized logging — agent.log (INFO+) and errors.log (WARNING+)
# both live under ~/.hermes/logs/. Idempotent, so gateway mode
# (which creates a new AIAgent per message) won't duplicate handlers.
from hermes_logging import setup_logging, setup_verbose_logging
from hermes_agent.logging import setup_logging, setup_verbose_logging
setup_logging(hermes_home=_hermes_home)
if self.verbose_logging:
@@ -1037,10 +1040,10 @@ class AIAgent:
# File handlers (agent.log, errors.log) still capture everything.
for quiet_logger in [
'tools', # all tools.* (terminal, browser, web, file, etc.)
'run_agent', # agent runner internals
'trajectory_compressor',
'hermes_agent.agent.loop', # agent runner internals
'scripts.trajectory_compressor',
'cron', # scheduler (only relevant in daemon mode)
'hermes_cli', # CLI helpers
'hermes_agent.cli', # CLI helpers
]:
logging.getLogger(quiet_logger).setLevel(logging.ERROR)
@@ -1082,12 +1085,12 @@ class AIAgent:
_provider_timeout = get_provider_request_timeout(self.provider, self.model)
if self.api_mode == "anthropic_messages":
from agent.anthropic_adapter import build_anthropic_client, resolve_anthropic_token
from hermes_agent.providers.anthropic_adapter import build_anthropic_client, resolve_anthropic_token
# Bedrock + Claude → use AnthropicBedrock SDK for full feature parity
# (prompt caching, thinking budgets, adaptive thinking).
_is_bedrock_anthropic = self.provider == "bedrock"
if _is_bedrock_anthropic:
from agent.anthropic_adapter import build_anthropic_bedrock_client
from hermes_agent.providers.anthropic_adapter import build_anthropic_bedrock_client
_region_match = re.search(r"bedrock-runtime\.([a-z0-9-]+)\.", base_url or "")
_br_region = _region_match.group(1) if _region_match else "us-east-1"
self._bedrock_region = _br_region
@@ -1116,7 +1119,7 @@ class AIAgent:
# so injects Claude-Code identity headers and system prompts
# that cause 401/403 on their endpoints. Guards #1739 and
# the third-party identity-injection bug.
from agent.anthropic_adapter import _is_oauth_token as _is_oat
from hermes_agent.providers.anthropic_adapter import _is_oauth_token as _is_oat
self._is_anthropic_oauth = _is_oat(effective_key) if _is_native_anthropic else False
self._anthropic_client = build_anthropic_client(effective_key, base_url, timeout=_provider_timeout)
# No OpenAI client needed for Anthropic mode
@@ -1134,7 +1137,7 @@ class AIAgent:
# Guardrail config — read from config.yaml at init time.
self._bedrock_guardrail_config = None
try:
from hermes_cli.config import load_config as _load_br_cfg
from hermes_agent.cli.config import load_config as _load_br_cfg
_gr = _load_br_cfg().get("bedrock", {}).get("guardrail", {})
if _gr.get("guardrail_identifier") and _gr.get("guardrail_version"):
self._bedrock_guardrail_config = {
@@ -1170,7 +1173,7 @@ class AIAgent:
"X-OpenRouter-Categories": "productivity,cli-agent",
}
elif base_url_host_matches(effective_base, "api.githubcopilot.com"):
from hermes_cli.models import copilot_default_headers
from hermes_agent.cli.models.models import copilot_default_headers
client_kwargs["default_headers"] = copilot_default_headers()
elif base_url_host_matches(effective_base, "api.kimi.com"):
@@ -1180,11 +1183,11 @@ class AIAgent:
elif base_url_host_matches(effective_base, "portal.qwen.ai"):
client_kwargs["default_headers"] = _qwen_portal_headers()
elif base_url_host_matches(effective_base, "chatgpt.com"):
from agent.auxiliary_client import _codex_cloudflare_headers
from hermes_agent.providers.auxiliary import _codex_cloudflare_headers
client_kwargs["default_headers"] = _codex_cloudflare_headers(api_key)
else:
# No explicit creds — use the centralized provider router
from agent.auxiliary_client import resolve_provider_client
from hermes_agent.providers.auxiliary import resolve_provider_client
_routed_client, _ = resolve_provider_client(
self.provider or "auto", model=self.model, raw_codex=True)
if _routed_client is not None:
@@ -1208,7 +1211,7 @@ class AIAgent:
# (e.g. alibaba → DASHSCOPE_API_KEY, not ALIBABA_API_KEY).
_env_hint = f"{_explicit.upper()}_API_KEY"
try:
from hermes_cli.auth import PROVIDER_REGISTRY
from hermes_agent.cli.auth.auth import PROVIDER_REGISTRY
_pcfg = PROVIDER_REGISTRY.get(_explicit)
if _pcfg and _pcfg.api_key_env_vars:
_env_hint = _pcfg.api_key_env_vars[0]
@@ -1361,7 +1364,7 @@ class AIAgent:
self._cached_system_prompt: Optional[str] = None
# Filesystem checkpoint manager (transparent — not a tool)
from tools.checkpoint_manager import CheckpointManager
from hermes_agent.tools.checkpoint import CheckpointManager
self._checkpoint_mgr = CheckpointManager(
enabled=checkpoints_enabled,
max_snapshots=checkpoint_max_snapshots,
@@ -1397,12 +1400,12 @@ class AIAgent:
)
# In-memory todo list for task planning (one per agent/session)
from tools.todo_tool import TodoStore
from hermes_agent.tools.todo import TodoStore
self._todo_store = TodoStore()
# Load config once for memory, skills, and compression sections
try:
from hermes_cli.config import load_config as _load_agent_config
from hermes_agent.cli.config import load_config as _load_agent_config
_agent_cfg = _load_agent_config()
except Exception:
_agent_cfg = {}
@@ -1427,7 +1430,7 @@ class AIAgent:
self._memory_nudge_interval = int(mem_config.get("nudge_interval", 10))
self._memory_flush_min_turns = int(mem_config.get("flush_min_turns", 6))
if self._memory_enabled or self._user_profile_enabled:
from tools.memory_tool import MemoryStore
from hermes_agent.tools.memory import MemoryStore
self._memory_store = MemoryStore(
memory_char_limit=mem_config.get("memory_char_limit", 2200),
user_char_limit=mem_config.get("user_char_limit", 1375),
@@ -1446,8 +1449,8 @@ class AIAgent:
_mem_provider_name = mem_config.get("provider", "") if mem_config else ""
if _mem_provider_name:
from agent.memory_manager import MemoryManager as _MemoryManager
from plugins.memory import load_memory_provider as _load_mem
from hermes_agent.agent.memory.manager import MemoryManager as _MemoryManager
from hermes_agent.plugins.memory import load_memory_provider as _load_mem
self._memory_manager = _MemoryManager()
_mp = _load_mem(_mem_provider_name)
if _mp and _mp.is_available():
@@ -1476,7 +1479,7 @@ class AIAgent:
_init_kwargs["gateway_session_key"] = self._gateway_session_key
# Profile identity for per-profile provider scoping
try:
from hermes_cli.profiles import get_active_profile_name
from hermes_agent.cli.profiles import get_active_profile_name
_profile = get_active_profile_name()
_init_kwargs["agent_identity"] = _profile
_init_kwargs["agent_workspace"] = "hermes"
@@ -1587,7 +1590,7 @@ class AIAgent:
# Check custom_providers per-model context_length
if _config_context_length is None:
try:
from hermes_cli.config import get_compatible_custom_providers
from hermes_agent.cli.config import get_compatible_custom_providers
_custom_providers = get_compatible_custom_providers(_agent_cfg)
except Exception:
_custom_providers = _agent_cfg.get("custom_providers")
@@ -1638,7 +1641,7 @@ class AIAgent:
if _engine_name != "compressor":
# Try loading from plugins/context_engine/<name>/
try:
from plugins.context_engine import load_context_engine
from hermes_agent.plugins.context_engine import load_context_engine
_selected_engine = load_context_engine(_engine_name)
except Exception as _ce_load_err:
logger.debug("Context engine load from plugins/context_engine/: %s", _ce_load_err)
@@ -1646,7 +1649,7 @@ class AIAgent:
# Try general plugin system as fallback
if _selected_engine is None:
try:
from hermes_cli.plugins import get_plugin_context_engine
from hermes_agent.cli.plugins import get_plugin_context_engine
_candidate = get_plugin_context_engine()
if _candidate and _candidate.name == _engine_name:
_selected_engine = _candidate
@@ -1663,7 +1666,7 @@ class AIAgent:
if _selected_engine is not None:
self.context_compressor = _selected_engine
# Resolve context_length for plugin engines — mirrors switch_model() path
from agent.model_metadata import get_model_context_length
from hermes_agent.providers.metadata import get_model_context_length
_plugin_ctx_len = get_model_context_length(
self.model,
base_url=self.base_url,
@@ -1699,7 +1702,7 @@ class AIAgent:
# Reject models whose context window is below the minimum required
# for reliable tool-calling workflows (64K tokens).
from agent.model_metadata import MINIMUM_CONTEXT_LENGTH
from hermes_agent.providers.metadata import MINIMUM_CONTEXT_LENGTH
_ctx = getattr(self.context_compressor, "context_length", 0)
if _ctx and _ctx < MINIMUM_CONTEXT_LENGTH:
raise ValueError(
@@ -1876,7 +1879,7 @@ class AIAgent:
change persists across turns (unlike fallback which is
turn-scoped).
"""
from hermes_cli.providers import determine_api_mode
from hermes_agent.cli.providers import determine_api_mode
# ── Determine api_mode if not provided ──
if not api_mode:
@@ -1908,7 +1911,7 @@ class AIAgent:
# ── Build new client ──
if api_mode == "anthropic_messages":
from agent.anthropic_adapter import (
from hermes_agent.providers.anthropic_adapter import (
build_anthropic_client,
resolve_anthropic_token,
_is_oauth_token,
@@ -1956,7 +1959,7 @@ class AIAgent:
# ── Update context compressor ──
if hasattr(self, "context_compressor") and self.context_compressor:
from agent.model_metadata import get_model_context_length
from hermes_agent.providers.metadata import get_model_context_length
new_context_length = get_model_context_length(
self.model,
base_url=self.base_url,
@@ -2151,8 +2154,8 @@ class AIAgent:
if not self.compression_enabled:
return
try:
from agent.auxiliary_client import get_text_auxiliary_client
from agent.model_metadata import (
from hermes_agent.providers.auxiliary import get_text_auxiliary_client
from hermes_agent.providers.metadata import (
MINIMUM_CONTEXT_LENGTH,
get_model_context_length,
)
@@ -2445,7 +2448,7 @@ class AIAgent:
normalized_provider = (provider or "").strip().lower()
if normalized_provider == "copilot":
try:
from hermes_cli.models import _should_use_copilot_responses_api
from hermes_agent.cli.models.models import _should_use_copilot_responses_api
return _should_use_copilot_responses_api(model)
except Exception:
# Fall back to the generic GPT-5 rule if Copilot-specific
@@ -3753,7 +3756,7 @@ class AIAgent:
if not headers:
return
try:
from agent.rate_limit_tracker import parse_rate_limit_headers
from hermes_agent.providers.rate_limiting import parse_rate_limit_headers
state = parse_rate_limit_headers(headers, provider=self.provider)
if state is not None:
self._rate_limit_state = state
@@ -3885,7 +3888,7 @@ class AIAgent:
# 1. Kill background processes for this task
try:
from tools.process_registry import process_registry
from hermes_agent.tools.process_registry import process_registry
process_registry.kill_all(task_id=task_id)
except Exception:
pass
@@ -4102,7 +4105,7 @@ class AIAgent:
if context_files_prompt:
prompt_parts.append(context_files_prompt)
from hermes_time import now as _hermes_now
from hermes_agent.time import now as _hermes_now
now = _hermes_now()
timestamp_line = f"Conversation started: {now.strftime('%A, %B %d, %Y %I:%M %p')}"
if self.pass_session_id and self.session_id:
@@ -4230,7 +4233,7 @@ class AIAgent:
Returns the original list if no truncation was needed.
"""
from tools.delegate_tool import _get_max_concurrent_children
from hermes_agent.tools.delegate import _get_max_concurrent_children
max_children = _get_max_concurrent_children()
delegate_count = sum(1 for tc in tool_calls if tc.function.name == "delegate_task")
if delegate_count <= max_children:
@@ -4406,7 +4409,7 @@ class AIAgent:
return None
def _create_openai_client(self, client_kwargs: dict, *, reason: str, shared: bool) -> Any:
from agent.auxiliary_client import _validate_base_url, _validate_proxy_env_urls
from hermes_agent.providers.auxiliary import _validate_base_url, _validate_proxy_env_urls
# Treat client_kwargs as read-only. Callers pass self._client_kwargs (or shallow
# copies of it) in; any in-place mutation leaks back into the stored dict and is
# reused on subsequent requests. #10933 hit this by injecting an httpx.Client
@@ -4419,7 +4422,7 @@ class AIAgent:
_validate_proxy_env_urls()
_validate_base_url(client_kwargs.get("base_url"))
if self.provider == "copilot-acp" or str(client_kwargs.get("base_url", "")).startswith("acp://copilot"):
from agent.copilot_acp_client import CopilotACPClient
from hermes_agent.agent.copilot_acp_client import CopilotACPClient
client = CopilotACPClient(**client_kwargs)
logger.info(
@@ -4430,7 +4433,7 @@ class AIAgent:
)
return client
if self.provider == "google-gemini-cli" or str(client_kwargs.get("base_url", "")).startswith("cloudcode-pa://"):
from agent.gemini_cloudcode_adapter import GeminiCloudCodeClient
from hermes_agent.providers.gemini_cloudcode_adapter import GeminiCloudCodeClient
# Strip OpenAI-specific kwargs the Gemini client doesn't accept
safe_kwargs = {
@@ -4446,7 +4449,7 @@ class AIAgent:
)
return client
if self.provider == "gemini":
from agent.gemini_native_adapter import GeminiNativeClient, is_native_gemini_base_url
from hermes_agent.providers.gemini_adapter import GeminiNativeClient, is_native_gemini_base_url
base_url = str(client_kwargs.get("base_url", "") or "")
if is_native_gemini_base_url(base_url):
@@ -4695,7 +4698,7 @@ class AIAgent:
def _close_request_openai_client(self, client: Any, *, reason: str) -> None:
self._close_openai_client(client, reason=reason, shared=False)
def _run_codex_stream(self, api_kwargs: dict, client: Any = None, on_first_delta: callable = None):
def _run_codex_stream(self, api_kwargs: dict, client: Any = None, on_first_delta: Callable[..., Any] = None):
"""Execute one streaming Responses API request and return the final response."""
import httpx as _httpx
@@ -4901,7 +4904,7 @@ class AIAgent:
return False
try:
from hermes_cli.auth import resolve_codex_runtime_credentials
from hermes_agent.cli.auth.auth import resolve_codex_runtime_credentials
creds = resolve_codex_runtime_credentials(force_refresh=force)
except Exception as exc:
@@ -4930,7 +4933,7 @@ class AIAgent:
return False
try:
from hermes_cli.auth import resolve_nous_runtime_credentials
from hermes_agent.cli.auth.auth import resolve_nous_runtime_credentials
creds = resolve_nous_runtime_credentials(
min_key_ttl_seconds=max(60, int(os.getenv("HERMES_NOUS_MIN_KEY_TTL_SECONDS", "1800"))),
@@ -4969,7 +4972,7 @@ class AIAgent:
return False
try:
from agent.anthropic_adapter import resolve_anthropic_token, build_anthropic_client
from hermes_agent.providers.anthropic_adapter import resolve_anthropic_token, build_anthropic_client
new_token = resolve_anthropic_token()
except Exception as exc:
@@ -5002,19 +5005,19 @@ class AIAgent:
# Only treat as OAuth on native Anthropic; third-party endpoints using
# the Anthropic protocol must not trip OAuth paths (#1739 & third-party
# identity-injection guard).
from agent.anthropic_adapter import _is_oauth_token
from hermes_agent.providers.anthropic_adapter import _is_oauth_token
self._is_anthropic_oauth = _is_oauth_token(new_token) if self.provider == "anthropic" else False
return True
def _apply_client_headers_for_base_url(self, base_url: str) -> None:
from agent.auxiliary_client import _AI_GATEWAY_HEADERS, _OR_HEADERS
from hermes_agent.providers.auxiliary import _AI_GATEWAY_HEADERS, _OR_HEADERS
if base_url_host_matches(base_url, "openrouter.ai"):
self._client_kwargs["default_headers"] = dict(_OR_HEADERS)
elif base_url_host_matches(base_url, "ai-gateway.vercel.sh"):
self._client_kwargs["default_headers"] = dict(_AI_GATEWAY_HEADERS)
elif base_url_host_matches(base_url, "api.githubcopilot.com"):
from hermes_cli.models import copilot_default_headers
from hermes_agent.cli.models.models import copilot_default_headers
self._client_kwargs["default_headers"] = copilot_default_headers()
elif base_url_host_matches(base_url, "api.kimi.com"):
@@ -5022,7 +5025,7 @@ class AIAgent:
elif base_url_host_matches(base_url, "portal.qwen.ai"):
self._client_kwargs["default_headers"] = _qwen_portal_headers()
elif base_url_host_matches(base_url, "chatgpt.com"):
from agent.auxiliary_client import _codex_cloudflare_headers
from hermes_agent.providers.auxiliary import _codex_cloudflare_headers
self._client_kwargs["default_headers"] = _codex_cloudflare_headers(
self._client_kwargs.get("api_key", "")
)
@@ -5034,7 +5037,7 @@ class AIAgent:
runtime_base = getattr(entry, "runtime_base_url", None) or getattr(entry, "base_url", None) or self.base_url
if self.api_mode == "anthropic_messages":
from agent.anthropic_adapter import build_anthropic_client, _is_oauth_token
from hermes_agent.providers.anthropic_adapter import build_anthropic_client, _is_oauth_token
try:
self._anthropic_client.close()
@@ -5178,7 +5181,7 @@ class AIAgent:
result["response"] = self._anthropic_messages_create(api_kwargs)
elif self.api_mode == "bedrock_converse":
# Bedrock uses boto3 directly — no OpenAI client needed.
from agent.bedrock_adapter import (
from hermes_agent.providers.bedrock_adapter import (
_get_bedrock_runtime_client,
normalize_converse_response,
)
@@ -5243,7 +5246,7 @@ class AIAgent:
)
try:
if self.api_mode == "anthropic_messages":
from agent.anthropic_adapter import build_anthropic_client
from hermes_agent.providers.anthropic_adapter import build_anthropic_client
self._anthropic_client.close()
self._anthropic_client = build_anthropic_client(
@@ -5275,7 +5278,7 @@ class AIAgent:
# seed future retries.
try:
if self.api_mode == "anthropic_messages":
from agent.anthropic_adapter import build_anthropic_client
from hermes_agent.providers.anthropic_adapter import build_anthropic_client
self._anthropic_client.close()
self._anthropic_client = build_anthropic_client(
@@ -5391,7 +5394,7 @@ class AIAgent:
)
def _interruptible_streaming_api_call(
self, api_kwargs: dict, *, on_first_delta: callable = None
self, api_kwargs: dict, *, on_first_delta: Callable[..., Any] = None
):
"""Streaming variant of _interruptible_api_call for real-time token delivery.
@@ -5436,7 +5439,7 @@ class AIAgent:
def _bedrock_call():
try:
from agent.bedrock_adapter import (
from hermes_agent.providers.bedrock_adapter import (
_get_bedrock_runtime_client,
stream_converse_with_callbacks,
)
@@ -6016,7 +6019,7 @@ class AIAgent:
if self._interrupt_requested:
try:
if self.api_mode == "anthropic_messages":
from agent.anthropic_adapter import build_anthropic_client
from hermes_agent.providers.anthropic_adapter import build_anthropic_client
self._anthropic_client.close()
self._anthropic_client = build_anthropic_client(
@@ -6126,7 +6129,7 @@ class AIAgent:
# raw_codex=True because the main agent needs direct responses.stream()
# access for Codex providers.
try:
from agent.auxiliary_client import resolve_provider_client
from hermes_agent.providers.auxiliary import resolve_provider_client
# Pass base_url and api_key from fallback config so custom
# endpoints (e.g. Ollama Cloud) resolve correctly instead of
# falling through to OpenRouter defaults.
@@ -6147,7 +6150,7 @@ class AIAgent:
fb_provider)
return self._try_activate_fallback() # try next in chain
try:
from hermes_cli.model_normalize import normalize_model_for_provider
from hermes_agent.cli.models.normalize import normalize_model_for_provider
fb_model = normalize_model_for_provider(fb_model, fb_provider)
except Exception:
@@ -6190,7 +6193,7 @@ class AIAgent:
if fb_api_mode == "anthropic_messages":
# Build native Anthropic client instead of using OpenAI client
from agent.anthropic_adapter import build_anthropic_client, resolve_anthropic_token, _is_oauth_token
from hermes_agent.providers.anthropic_adapter import build_anthropic_client, resolve_anthropic_token, _is_oauth_token
effective_key = (fb_client.api_key or resolve_anthropic_token() or "") if fb_provider == "anthropic" else (fb_client.api_key or "")
self.api_key = effective_key
self._anthropic_api_key = effective_key
@@ -6243,7 +6246,7 @@ class AIAgent:
# context window (e.g. 200K) instead of the fallback's (e.g. 32K),
# causing oversized sessions to overflow the fallback.
if hasattr(self, 'context_compressor') and self.context_compressor:
from agent.model_metadata import get_model_context_length
from hermes_agent.providers.metadata import get_model_context_length
fb_context_length = get_model_context_length(
self.model, base_url=self.base_url,
api_key=self.api_key, provider=self.provider,
@@ -6304,7 +6307,7 @@ class AIAgent:
# ── Rebuild client for the primary provider ──
if self.api_mode == "anthropic_messages":
from agent.anthropic_adapter import build_anthropic_client
from hermes_agent.providers.anthropic_adapter import build_anthropic_client
self._anthropic_api_key = rt["anthropic_api_key"]
self._anthropic_base_url = rt["anthropic_base_url"]
self._anthropic_client = build_anthropic_client(
@@ -6401,7 +6404,7 @@ class AIAgent:
self.api_key = rt["api_key"]
if self.api_mode == "anthropic_messages":
from agent.anthropic_adapter import build_anthropic_client
from hermes_agent.providers.anthropic_adapter import build_anthropic_client
self._anthropic_api_key = rt["anthropic_api_key"]
self._anthropic_base_url = rt["anthropic_base_url"]
self._anthropic_client = build_anthropic_client(
@@ -6484,7 +6487,7 @@ class AIAgent:
description = ""
try:
from tools.vision_tools import vision_analyze_tool
from hermes_agent.tools.vision import vision_analyze_tool
result_json = asyncio.run(
vision_analyze_tool(image_url=vision_source, user_prompt=analysis_prompt)
@@ -6560,7 +6563,7 @@ class AIAgent:
"""Return the cached AnthropicTransport instance (lazy singleton)."""
t = getattr(self, "_anthropic_transport", None)
if t is None:
from agent.transports import get_transport
from hermes_agent.providers import get_transport
t = get_transport("anthropic_messages")
self._anthropic_transport = t
return t
@@ -6569,7 +6572,7 @@ class AIAgent:
"""Return the cached ResponsesApiTransport instance (lazy singleton)."""
t = getattr(self, "_codex_transport", None)
if t is None:
from agent.transports import get_transport
from hermes_agent.providers import get_transport
t = get_transport("codex_responses")
self._codex_transport = t
return t
@@ -6578,7 +6581,7 @@ class AIAgent:
"""Return the cached ChatCompletionsTransport instance (lazy singleton)."""
t = getattr(self, "_chat_completions_transport", None)
if t is None:
from agent.transports import get_transport
from hermes_agent.providers import get_transport
t = get_transport("chat_completions")
self._chat_completions_transport = t
return t
@@ -6587,7 +6590,7 @@ class AIAgent:
"""Return the cached BedrockTransport instance (lazy singleton)."""
t = getattr(self, "_bedrock_transport", None)
if t is None:
from agent.transports import get_transport
from hermes_agent.providers import get_transport
t = get_transport("bedrock_converse")
self._bedrock_transport = t
return t
@@ -6792,7 +6795,7 @@ class AIAgent:
# Temperature: _fixed_temperature_for_model may return OMIT_TEMPERATURE
# sentinel (temperature omitted entirely), a numeric override, or None.
try:
from agent.auxiliary_client import _fixed_temperature_for_model, OMIT_TEMPERATURE
from hermes_agent.providers.auxiliary import _fixed_temperature_for_model, OMIT_TEMPERATURE
_ft = _fixed_temperature_for_model(self.model, self.base_url)
_omit_temp = _ft is OMIT_TEMPERATURE
_fixed_temp = _ft if not _omit_temp else None
@@ -6819,7 +6822,7 @@ class AIAgent:
_ant_max = None
if (_is_or or _is_nous) and "claude" in (self.model or "").lower():
try:
from agent.anthropic_adapter import _get_anthropic_max_output
from hermes_agent.providers.anthropic_adapter import _get_anthropic_max_output
_ant_max = _get_anthropic_max_output(self.model)
except Exception:
pass # fail open — let the proxy pick its default
@@ -6885,7 +6888,7 @@ class AIAgent:
or base_url_host_matches(self._base_url_lower, "api.githubcopilot.com")
):
try:
from hermes_cli.models import github_model_reasoning_efforts
from hermes_agent.cli.models.models import github_model_reasoning_efforts
return bool(github_model_reasoning_efforts(self.model))
except Exception:
@@ -6909,7 +6912,7 @@ class AIAgent:
def _github_models_reasoning_extra_body(self) -> dict | None:
"""Format reasoning payload for GitHub Models/OpenAI-compatible routes."""
try:
from hermes_cli.models import github_model_reasoning_efforts
from hermes_agent.cli.models.models import github_model_reasoning_efforts
except Exception:
return None
@@ -7188,7 +7191,7 @@ class AIAgent:
# Use auxiliary client for the flush call when available --
# it's cheaper and avoids Codex Responses API incompatibility.
from agent.auxiliary_client import (
from hermes_agent.providers.auxiliary import (
call_llm as _call_llm,
_fixed_temperature_for_model,
OMIT_TEMPERATURE,
@@ -7205,12 +7208,15 @@ class AIAgent:
_flush_temperature = _fixed_temp
else:
_flush_temperature = 0.3
_flush_llm_kwargs: dict = {}
if _flush_temperature is not None:
_flush_llm_kwargs["temperature"] = _flush_temperature
try:
response = _call_llm(
task="flush_memories",
messages=api_messages,
tools=[memory_tool_def],
temperature=_flush_temperature,
**_flush_llm_kwargs,
max_tokens=5120,
# timeout resolved from auxiliary.flush_memories.timeout config
)
@@ -7248,7 +7254,7 @@ class AIAgent:
}
if _flush_temperature is not None:
api_kwargs["temperature"] = _flush_temperature
from agent.auxiliary_client import _get_task_timeout
from hermes_agent.providers.auxiliary import _get_task_timeout
response = self._ensure_primary_openai_client(reason="flush_memories").chat.completions.create(
**api_kwargs, timeout=_get_task_timeout("flush_memories")
)
@@ -7285,7 +7291,7 @@ class AIAgent:
try:
args = json.loads(tc.function.arguments)
flush_target = args.get("target", "memory")
from tools.memory_tool import memory_tool as _memory_tool
from hermes_agent.tools.memory import memory_tool as _memory_tool
_memory_tool(
action=args.get("action"),
target=flush_target,
@@ -7399,7 +7405,7 @@ class AIAgent:
# read content is summarised away — if the model re-reads the same
# file it needs the full content, not a "file unchanged" stub.
try:
from tools.file_tools import reset_file_dedup
from hermes_agent.tools.files.tools import reset_file_dedup
reset_file_dedup(task_id)
except Exception:
pass
@@ -7440,7 +7446,7 @@ class AIAgent:
New DELEGATE_TASK_SCHEMA fields only need to be added here to reach all
invocation paths (concurrent, sequential, inline).
"""
from tools.delegate_tool import delegate_task as _delegate_task
from hermes_agent.tools.delegate import delegate_task as _delegate_task
return _delegate_task(
goal=function_args.get("goal"),
context=function_args.get("context"),
@@ -7464,7 +7470,7 @@ class AIAgent:
# Check plugin hooks for a block directive before executing anything.
block_message: Optional[str] = None
try:
from hermes_cli.plugins import get_pre_tool_call_block_message
from hermes_agent.cli.plugins import get_pre_tool_call_block_message
block_message = get_pre_tool_call_block_message(
function_name, function_args, task_id=effective_task_id or "",
)
@@ -7474,7 +7480,7 @@ class AIAgent:
return json.dumps({"error": block_message}, ensure_ascii=False)
if function_name == "todo":
from tools.todo_tool import todo_tool as _todo_tool
from hermes_agent.tools.todo import todo_tool as _todo_tool
return _todo_tool(
todos=function_args.get("todos"),
merge=function_args.get("merge", False),
@@ -7483,7 +7489,7 @@ class AIAgent:
elif function_name == "session_search":
if not self._session_db:
return json.dumps({"success": False, "error": "Session database not available."})
from tools.session_search_tool import session_search as _session_search
from hermes_agent.tools.session_search import session_search as _session_search
return _session_search(
query=function_args.get("query", ""),
role_filter=function_args.get("role_filter"),
@@ -7493,7 +7499,7 @@ class AIAgent:
)
elif function_name == "memory":
target = function_args.get("target", "memory")
from tools.memory_tool import memory_tool as _memory_tool
from hermes_agent.tools.memory import memory_tool as _memory_tool
result = _memory_tool(
action=function_args.get("action"),
target=target,
@@ -7515,7 +7521,7 @@ class AIAgent:
elif self._memory_manager and self._memory_manager.has_tool(function_name):
return self._memory_manager.handle_tool_call(function_name, function_args)
elif function_name == "clarify":
from tools.clarify_tool import clarify_tool as _clarify_tool
from hermes_agent.tools.clarify import clarify_tool as _clarify_tool
return _clarify_tool(
question=function_args.get("question", ""),
choices=function_args.get("choices"),
@@ -7678,7 +7684,7 @@ class AIAgent:
# The callback is thread-local; the main thread's callback
# is invisible to worker threads.
try:
from tools.environments.base import set_activity_callback
from hermes_agent.backends.base import set_activity_callback
set_activity_callback(self._touch_activity)
except Exception:
pass
@@ -7893,7 +7899,7 @@ class AIAgent:
# Check plugin hooks for a block directive before executing.
_block_msg: Optional[str] = None
try:
from hermes_cli.plugins import get_pre_tool_call_block_message
from hermes_agent.cli.plugins import get_pre_tool_call_block_message
_block_msg = get_pre_tool_call_block_message(
function_name, function_args, task_id=effective_task_id or "",
)
@@ -7929,7 +7935,7 @@ class AIAgent:
# the agent while a command is running.
if _block_msg is None:
try:
from tools.environments.base import set_activity_callback
from hermes_agent.backends.base import set_activity_callback
set_activity_callback(self._touch_activity)
except Exception:
pass
@@ -7978,7 +7984,7 @@ class AIAgent:
function_result = json.dumps({"error": _block_msg}, ensure_ascii=False)
tool_duration = 0.0
elif function_name == "todo":
from tools.todo_tool import todo_tool as _todo_tool
from hermes_agent.tools.todo import todo_tool as _todo_tool
function_result = _todo_tool(
todos=function_args.get("todos"),
merge=function_args.get("merge", False),
@@ -7991,7 +7997,7 @@ class AIAgent:
if not self._session_db:
function_result = json.dumps({"success": False, "error": "Session database not available."})
else:
from tools.session_search_tool import session_search as _session_search
from hermes_agent.tools.session_search import session_search as _session_search
function_result = _session_search(
query=function_args.get("query", ""),
role_filter=function_args.get("role_filter"),
@@ -8004,7 +8010,7 @@ class AIAgent:
self._vprint(f" {_get_cute_tool_message_impl('session_search', function_args, tool_duration, result=function_result)}")
elif function_name == "memory":
target = function_args.get("target", "memory")
from tools.memory_tool import memory_tool as _memory_tool
from hermes_agent.tools.memory import memory_tool as _memory_tool
function_result = _memory_tool(
action=function_args.get("action"),
target=target,
@@ -8026,7 +8032,7 @@ class AIAgent:
if self._should_emit_quiet_tool_messages():
self._vprint(f" {_get_cute_tool_message_impl('memory', function_args, tool_duration, result=function_result)}")
elif function_name == "clarify":
from tools.clarify_tool import clarify_tool as _clarify_tool
from hermes_agent.tools.clarify import clarify_tool as _clarify_tool
function_result = _clarify_tool(
question=function_args.get("question", ""),
choices=function_args.get("choices"),
@@ -8281,7 +8287,7 @@ class AIAgent:
summary_extra_body = {}
try:
from agent.auxiliary_client import _fixed_temperature_for_model, OMIT_TEMPERATURE as _OMIT_TEMP
from hermes_agent.providers.auxiliary import _fixed_temperature_for_model, OMIT_TEMPERATURE as _OMIT_TEMP
except Exception:
_fixed_temperature_for_model = None
_OMIT_TEMP = None
@@ -8418,9 +8424,9 @@ class AIAgent:
self,
user_message: str,
system_message: str = None,
conversation_history: List[Dict[str, Any]] = None,
conversation_history: List[Dict[str, Any]] | None = None,
task_id: str = None,
stream_callback: Optional[callable] = None,
stream_callback: Optional[Callable[..., Any]] = None,
persist_user_message: Optional[str] = None,
) -> Dict[str, Any]:
"""
@@ -8448,7 +8454,7 @@ class AIAgent:
# Tag all log records on this thread with the session ID so
# ``hermes logs --session <id>`` can filter a single conversation.
from hermes_logging import set_session_context
from hermes_agent.logging import set_session_context
set_session_context(self.session_id)
# If the previous turn activated fallback, restore the primary
@@ -8610,7 +8616,7 @@ class AIAgent:
# continuation). Plugins can use this to initialise
# session-scoped state (e.g. warm a memory cache).
try:
from hermes_cli.plugins import invoke_hook as _invoke_hook
from hermes_agent.cli.plugins import invoke_hook as _invoke_hook
_invoke_hook(
"on_session_start",
session_id=self.session_id,
@@ -8711,7 +8717,7 @@ class AIAgent:
# All injected context is ephemeral (not persisted to session DB).
_plugin_user_context = ""
try:
from hermes_cli.plugins import invoke_hook as _invoke_hook
from hermes_agent.cli.plugins import invoke_hook as _invoke_hook
_pre_results = _invoke_hook(
"pre_llm_call",
session_id=self.session_id,
@@ -9085,7 +9091,7 @@ class AIAgent:
# deepens the rate limit hole.
if self.provider == "nous":
try:
from agent.nous_rate_guard import (
from hermes_agent.providers.nous_rate_guard import (
nous_rate_limit_remaining,
format_remaining as _fmt_nous_remaining,
)
@@ -9134,7 +9140,7 @@ class AIAgent:
api_kwargs = self._get_codex_transport().preflight_kwargs(api_kwargs, allow_stream=False)
try:
from hermes_cli.plugins import invoke_hook as _invoke_hook
from hermes_agent.cli.plugins import invoke_hook as _invoke_hook
_invoke_hook(
"pre_api_request",
task_id=effective_task_id,
@@ -9759,7 +9765,7 @@ class AIAgent:
# resume hitting Nous.
if self.provider == "nous":
try:
from agent.nous_rate_guard import clear_nous_rate_limit
from hermes_agent.providers.nous_rate_guard import clear_nous_rate_limit
clear_nous_rate_limit()
except Exception:
pass
@@ -10007,7 +10013,7 @@ class AIAgent:
and not anthropic_auth_retry_attempted
):
anthropic_auth_retry_attempted = True
from agent.anthropic_adapter import _is_oauth_token
from hermes_agent.providers.anthropic_adapter import _is_oauth_token
if self._try_refresh_anthropic_client_credentials():
print(f"{self.log_prefix}🔐 Anthropic credentials refreshed after 401. Retrying request...")
continue
@@ -10016,9 +10022,9 @@ class AIAgent:
auth_method = "Bearer (OAuth/setup-token)" if _is_oauth_token(key) else "x-api-key (API key)"
print(f"{self.log_prefix}🔐 Anthropic 401 — authentication failed.")
print(f"{self.log_prefix} Auth method: {auth_method}")
print(f"{self.log_prefix} Token prefix: {key[:12]}..." if key and len(key) > 12 else f"{self.log_prefix} Token: (empty or short)")
print(f"{self.log_prefix} Token prefix: {str(key)[:12]}..." if key and len(str(key)) > 12 else f"{self.log_prefix} Token: (empty or short)")
print(f"{self.log_prefix} Troubleshooting:")
from hermes_constants import display_hermes_home as _dhh_fn
from hermes_agent.constants import display_hermes_home as _dhh_fn
_dhh = _dhh_fn()
print(f"{self.log_prefix} • Check ANTHROPIC_TOKEN in {_dhh}/.env for Hermes-managed OAuth/setup tokens")
print(f"{self.log_prefix} • Check ANTHROPIC_API_KEY in {_dhh}/.env for API keys or legacy token values")
@@ -10227,7 +10233,7 @@ class AIAgent:
and not recovered_with_pool
):
try:
from agent.nous_rate_guard import record_nous_rate_limit
from hermes_agent.providers.nous_rate_guard import record_nous_rate_limit
_err_resp = getattr(api_error, "response", None)
_err_hdrs = (
getattr(_err_resp, "headers", None)
@@ -10770,7 +10776,7 @@ class AIAgent:
assistant_message.content = str(raw)
try:
from hermes_cli.plugins import invoke_hook as _invoke_hook
from hermes_agent.cli.plugins import invoke_hook as _invoke_hook
_assistant_tool_calls = getattr(assistant_message, "tool_calls", None) or []
_assistant_text = assistant_message.content or ""
_invoke_hook(
@@ -11416,7 +11422,7 @@ class AIAgent:
messages.append(assistant_msg)
if reasoning_text:
reasoning_preview = reasoning_text[:500] + "..." if len(reasoning_text) > 500 else reasoning_text
reasoning_preview = str(reasoning_text)[:500] + "..." if len(str(reasoning_text)) > 500 else reasoning_text
logger.warning(
"Reasoning-only response (no visible content) "
"after exhausting retries and fallback. "
@@ -11636,7 +11642,7 @@ class AIAgent:
# to an external memory system).
if final_response and not interrupted:
try:
from hermes_cli.plugins import invoke_hook as _invoke_hook
from hermes_agent.cli.plugins import invoke_hook as _invoke_hook
_invoke_hook(
"post_llm_call",
session_id=self.session_id,
@@ -11741,7 +11747,7 @@ class AIAgent:
# Fired at the very end of every run_conversation call.
# Plugins can use this for cleanup, flushing buffers, etc.
try:
from hermes_cli.plugins import invoke_hook as _invoke_hook
from hermes_agent.cli.plugins import invoke_hook as _invoke_hook
_invoke_hook(
"on_session_end",
session_id=self.session_id,
@@ -11755,7 +11761,7 @@ class AIAgent:
return result
def chat(self, message: str, stream_callback: Optional[callable] = None) -> str:
def chat(self, message: str, stream_callback: Optional[Callable[..., Any]] = None) -> str:
"""
Simple chat interface that returns just the final response.
@@ -11811,8 +11817,8 @@ def main(
# Handle tool listing
if list_tools:
from model_tools import get_all_tool_names, get_available_toolsets
from toolsets import get_all_toolsets, get_toolset_info
from hermes_agent.tools.dispatch import get_all_tool_names, get_available_toolsets
from hermes_agent.tools.toolsets import get_all_toolsets, get_toolset_info
print("📋 Available Tools & Toolsets:")
print("-" * 50)

View File

View File

@@ -33,8 +33,8 @@ import logging
import re
from typing import Any, Dict, List, Optional
from agent.memory_provider import MemoryProvider
from tools.registry import tool_error
from hermes_agent.agent.memory.provider import MemoryProvider
from hermes_agent.tools.registry import tool_error
logger = logging.getLogger(__name__)
@@ -361,7 +361,7 @@ class MemoryManager:
``get_hermes_home()`` themselves.
"""
if "hermes_home" not in kwargs:
from hermes_constants import get_hermes_home
from hermes_agent.constants import get_hermes_home
kwargs["hermes_home"] = str(get_hermes_home())
for provider in self._providers:
try:

View File

@@ -12,10 +12,10 @@ import threading
from collections import OrderedDict
from pathlib import Path
from hermes_constants import get_hermes_home, get_skills_dir, is_wsl
from hermes_agent.constants import get_hermes_home, get_skills_dir, is_wsl
from typing import Optional
from agent.skill_utils import (
from hermes_agent.agent.skill_utils import (
extract_skill_conditions,
extract_skill_description,
get_all_skills_dirs,
@@ -24,7 +24,7 @@ from agent.skill_utils import (
parse_frontmatter,
skill_matches_platform,
)
from utils import atomic_json_write
from hermes_agent.utils import atomic_json_write
logger = logging.getLogger(__name__)
@@ -619,7 +619,7 @@ def build_skills_system_prompt(
# ── Layer 1: in-process LRU cache ─────────────────────────────────
# Include the resolved platform so per-platform disabled-skill lists
# produce distinct cache entries (gateway serves multiple platforms).
from gateway.session_context import get_session_env
from hermes_agent.gateway.session_context import get_session_env
_platform_hint = (
os.environ.get("HERMES_PLATFORM")
or get_session_env("HERMES_SESSION_PLATFORM")
@@ -824,8 +824,8 @@ def build_skills_system_prompt(
def build_nous_subscription_prompt(valid_tool_names: "set[str] | None" = None) -> str:
"""Build a compact Nous subscription capability block for the system prompt."""
try:
from hermes_cli.nous_subscription import get_nous_subscription_features
from tools.tool_backend_helpers import managed_nous_tools_enabled
from hermes_agent.cli.nous_subscription import get_nous_subscription_features
from hermes_agent.tools.backend_helpers import managed_nous_tools_enabled
except Exception as exc:
logger.debug("Failed to import Nous subscription helper: %s", exc)
return ""
@@ -911,7 +911,7 @@ def load_soul_md() -> Optional[str]:
``skip_soul=True`` so SOUL.md isn't injected twice.
"""
try:
from hermes_cli.config import ensure_hermes_home
from hermes_agent.cli.config import ensure_hermes_home
ensure_hermes_home()
except Exception as e:
logger.debug("Could not ensure HERMES_HOME before loading SOUL.md: %s", e)

View File

@@ -75,7 +75,7 @@ try:
except ImportError: # pragma: no cover
fcntl = None # type: ignore[assignment]
from hermes_constants import get_hermes_home
from hermes_agent.constants import get_hermes_home
logger = logging.getLogger(__name__)
@@ -177,7 +177,7 @@ def register_from_config(
registered: List[ShellHookSpec] = []
# Import lazily — avoids circular imports at module-load time.
from hermes_cli.plugins import get_plugin_manager
from hermes_agent.cli.plugins import get_plugin_manager
manager = get_plugin_manager()
@@ -243,7 +243,7 @@ def _parse_hooks_block(hooks_cfg: Any) -> List[ShellHookSpec]:
Malformed entries warn-and-skip we never raise from config parsing
because a broken hook must not crash the agent.
"""
from hermes_cli.plugins import VALID_HOOKS
from hermes_agent.cli.plugins import VALID_HOOKS
if not isinstance(hooks_cfg, dict):
return []

View File

@@ -13,7 +13,7 @@ from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional
from hermes_constants import display_hermes_home
from hermes_agent.constants import display_hermes_home
logger = logging.getLogger(__name__)
@@ -39,7 +39,7 @@ _INLINE_SHELL_MAX_OUTPUT = 4000
def _load_skills_config() -> dict:
"""Load the ``skills`` section of config.yaml (best-effort)."""
try:
from hermes_cli.config import load_config
from hermes_agent.cli.config import load_config
cfg = load_config() or {}
skills_cfg = cfg.get("skills")
@@ -156,7 +156,7 @@ def _load_skill_payload(skill_identifier: str, task_id: str | None = None) -> tu
return None
try:
from tools.skills_tool import SKILLS_DIR, skill_view
from hermes_agent.tools.skills.tool import SKILLS_DIR, skill_view
identifier_path = Path(raw_identifier).expanduser()
if identifier_path.is_absolute():
@@ -202,7 +202,7 @@ def _inject_skill_config(loaded_skill: dict[str, Any], parts: list[str]) -> None
without needing to read config.yaml itself.
"""
try:
from agent.skill_utils import (
from hermes_agent.agent.skill_utils import (
extract_skill_config_vars,
parse_frontmatter,
resolve_skill_config_values,
@@ -241,7 +241,7 @@ def _build_skill_message(
session_id: str | None = None,
) -> str:
"""Format a loaded skill into a user/system message payload."""
from tools.skills_tool import SKILLS_DIR
from hermes_agent.tools.skills.tool import SKILLS_DIR
content = str(loaded_skill.get("content") or "")
@@ -344,8 +344,8 @@ def scan_skill_commands() -> Dict[str, Dict[str, Any]]:
global _skill_commands
_skill_commands = {}
try:
from tools.skills_tool import SKILLS_DIR, _parse_frontmatter, skill_matches_platform, _get_disabled_skill_names
from agent.skill_utils import get_external_skills_dirs
from hermes_agent.tools.skills.tool import SKILLS_DIR, _parse_frontmatter, skill_matches_platform, _get_disabled_skill_names
from hermes_agent.agent.skill_utils import get_external_skills_dirs
disabled = _get_disabled_skill_names()
seen_names: set = set()

View File

@@ -12,7 +12,7 @@ import sys
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple
from hermes_constants import get_config_path, get_skills_dir
from hermes_agent.constants import get_config_path, get_skills_dir
logger = logging.getLogger(__name__)
@@ -145,7 +145,7 @@ def get_disabled_skill_names(platform: str | None = None) -> Set[str]:
if not isinstance(skills_cfg, dict):
return set()
from gateway.session_context import get_session_env
from hermes_agent.gateway.session_context import get_session_env
resolved_platform = (
platform
or os.getenv("HERMES_PLATFORM")
@@ -455,7 +455,8 @@ def parse_qualified_name(name: str) -> Tuple[Optional[str], str]:
"""
if ":" not in name:
return None, name
return tuple(name.split(":", 1)) # type: ignore[return-value]
ns, bare = name.split(":", 1)
return ns, bare
def is_valid_namespace(candidate: Optional[str]) -> bool:

View File

@@ -19,7 +19,7 @@ import shlex
from pathlib import Path
from typing import Dict, Any, Optional, Set
from agent.prompt_builder import _scan_context_content
from hermes_agent.agent.prompt_builder import _scan_context_content
logger = logging.getLogger(__name__)

View File

@@ -8,7 +8,7 @@ import logging
import threading
from typing import Optional
from agent.auxiliary_client import call_llm
from hermes_agent.providers.auxiliary import call_llm
logger = logging.getLogger(__name__)

View File

@@ -8,6 +8,6 @@ The terminal_tool.py factory (_create_environment) selects the backend
based on the TERMINAL_ENV configuration.
"""
from tools.environments.base import BaseEnvironment
from hermes_agent.backends.base import BaseEnvironment
__all__ = ["BaseEnvironment"]

View File

@@ -20,8 +20,8 @@ from abc import ABC, abstractmethod
from pathlib import Path
from typing import IO, Callable, Protocol
from hermes_constants import get_hermes_home
from tools.interrupt import is_interrupted
from hermes_agent.constants import get_hermes_home
from hermes_agent.tools.interrupt import is_interrupted
logger = logging.getLogger(__name__)
@@ -245,7 +245,7 @@ class _ThreadedProcessHandle:
except Exception:
pass
def wait(self, timeout: float | None = None) -> int:
def wait(self, timeout: float | None = None) -> int | None:
self._done.wait(timeout=timeout)
return self._returncode
@@ -710,7 +710,7 @@ class BaseEnvironment(ABC):
# server, `yes > /dev/null`, etc.), leaking the subshell forever.
# Rewriting to `A && { B & }` runs B as a plain background in the
# current shell — no subshell wait.
from tools.terminal_tool import _rewrite_compound_background
from hermes_agent.tools.terminal import _rewrite_compound_background
exec_command = _rewrite_compound_background(exec_command)
effective_timeout = timeout or self.timeout
effective_cwd = cwd or self.cwd
@@ -755,9 +755,9 @@ class BaseEnvironment(ABC):
except Exception:
pass
def _prepare_command(self, command: str) -> tuple[str, str | None]:
def _prepare_command(self, command: str) -> tuple[str | None, str | None]:
"""Transform sudo commands if SUDO_PASSWORD is available."""
from tools.terminal_tool import _transform_sudo_command
from hermes_agent.tools.terminal import _transform_sudo_command
return _transform_sudo_command(command)

View File

@@ -12,11 +12,11 @@ import shlex
import threading
from pathlib import Path
from tools.environments.base import (
from hermes_agent.backends.base import (
BaseEnvironment,
_ThreadedProcessHandle,
)
from tools.environments.file_sync import (
from hermes_agent.backends.file_sync import (
FileSyncManager,
iter_sync_files,
quoted_mkdir_command,

View File

@@ -14,8 +14,8 @@ import sys
import uuid
from typing import Optional
from tools.environments.base import BaseEnvironment, _popen_bash
from tools.environments.local import _HERMES_PROVIDER_ENV_BLOCKLIST
from hermes_agent.backends.base import BaseEnvironment, _popen_bash
from hermes_agent.backends.local import _HERMES_PROVIDER_ENV_BLOCKLIST
logger = logging.getLogger(__name__)
@@ -91,7 +91,7 @@ def _normalize_env_dict(env: dict | None) -> dict[str, str]:
def _load_hermes_env_vars() -> dict[str, str]:
"""Load ~/.hermes/.env values without failing Docker command execution."""
try:
from hermes_cli.config import load_env
from hermes_agent.cli.config import load_env
return load_env() or {}
except Exception:
@@ -298,7 +298,7 @@ class DockerEnvironment(BaseEnvironment):
# Persistent workspace via bind mounts from a configurable host directory
# (TERMINAL_SANDBOX_DIR, default ~/.hermes/sandboxes/). Non-persistent
# mode uses tmpfs (ephemeral, fast, gone on cleanup).
from tools.environments.base import get_sandbox_dir
from hermes_agent.backends.base import get_sandbox_dir
# User-configured volume mounts (from config.yaml docker_volumes)
volume_args = []
@@ -362,7 +362,7 @@ class DockerEnvironment(BaseEnvironment):
# Mount credential files (OAuth tokens, etc.) declared by skills.
# Read-only so the container can authenticate but not modify host creds.
try:
from tools.credential_files import (
from hermes_agent.tools.credential_files import (
get_credential_file_mounts,
get_skills_directory_mount,
get_cache_directory_mounts,
@@ -464,7 +464,7 @@ class DockerEnvironment(BaseEnvironment):
explicit_forward_keys = set(self._forward_env)
passthrough_keys: set[str] = set()
try:
from tools.env_passthrough import get_all_passthrough
from hermes_agent.tools.env_passthrough import get_all_passthrough
passthrough_keys = set(get_all_passthrough())
except Exception:
pass

View File

@@ -24,8 +24,8 @@ except ImportError:
from pathlib import Path
from typing import Callable
from hermes_constants import get_hermes_home
from tools.environments.base import _file_mtime_key
from hermes_agent.constants import get_hermes_home
from hermes_agent.backends.base import _file_mtime_key
logger = logging.getLogger(__name__)
@@ -50,7 +50,7 @@ def iter_sync_files(container_base: str = "/root/.hermes") -> list[tuple[str, st
"""
# Late import: credential_files imports agent modules that create
# circular dependencies if loaded at file_sync module level.
from tools.credential_files import (
from hermes_agent.tools.credential_files import (
get_credential_file_mounts,
iter_cache_files,
iter_skills_files,

View File

@@ -7,7 +7,7 @@ import signal
import subprocess
import tempfile
from tools.environments.base import BaseEnvironment, _pipe_stdin
from hermes_agent.backends.base import BaseEnvironment, _pipe_stdin
_IS_WINDOWS = platform.system() == "Windows"
@@ -21,7 +21,7 @@ def _build_provider_env_blocklist() -> frozenset:
blocked: set[str] = set()
try:
from hermes_cli.auth import PROVIDER_REGISTRY
from hermes_agent.cli.auth.auth import PROVIDER_REGISTRY
for pconfig in PROVIDER_REGISTRY.values():
blocked.update(pconfig.api_key_env_vars)
if pconfig.base_url_env_var:
@@ -30,7 +30,7 @@ def _build_provider_env_blocklist() -> frozenset:
pass
try:
from hermes_cli.config import OPTIONAL_ENV_VARS
from hermes_agent.cli.config import OPTIONAL_ENV_VARS
for name, metadata in OPTIONAL_ENV_VARS.items():
category = metadata.get("category")
if category in {"tool", "messaging"}:
@@ -110,7 +110,7 @@ _HERMES_PROVIDER_ENV_BLOCKLIST = _build_provider_env_blocklist()
def _sanitize_subprocess_env(base_env: dict | None, extra_env: dict | None = None) -> dict:
"""Filter Hermes-managed secrets from a subprocess environment."""
try:
from tools.env_passthrough import is_env_passthrough as _is_passthrough
from hermes_agent.tools.env_passthrough import is_env_passthrough as _is_passthrough
except Exception:
_is_passthrough = lambda _: False # noqa: E731
@@ -130,7 +130,7 @@ def _sanitize_subprocess_env(base_env: dict | None, extra_env: dict | None = Non
sanitized[key] = value
# Per-profile HOME isolation for background processes (same as _make_run_env).
from hermes_constants import get_subprocess_home
from hermes_agent.constants import get_subprocess_home
_profile_home = get_subprocess_home()
if _profile_home:
sanitized["HOME"] = _profile_home
@@ -186,7 +186,7 @@ _SANE_PATH = (
def _make_run_env(env: dict) -> dict:
"""Build a run environment with a sane PATH and provider-var stripping."""
try:
from tools.env_passthrough import is_env_passthrough as _is_passthrough
from hermes_agent.tools.env_passthrough import is_env_passthrough as _is_passthrough
except Exception:
_is_passthrough = lambda _: False # noqa: E731
@@ -205,7 +205,7 @@ def _make_run_env(env: dict) -> dict:
# Per-profile HOME isolation: redirect system tool configs (git, ssh, gh,
# npm …) into {HERMES_HOME}/home/ when that directory exists. Only the
# subprocess sees the override — the Python process keeps the real HOME.
from hermes_constants import get_subprocess_home
from hermes_agent.constants import get_subprocess_home
_profile_home = get_subprocess_home()
if _profile_home:
run_env["HOME"] = _profile_home
@@ -220,7 +220,7 @@ def _read_terminal_shell_init_config() -> tuple[list[str], bool]:
execution never breaks because the config file is unreadable.
"""
try:
from hermes_cli.config import load_config
from hermes_agent.cli.config import load_config
cfg = load_config() or {}
terminal_cfg = cfg.get("terminal") or {}

View File

@@ -10,12 +10,12 @@ import uuid
from dataclasses import dataclass
from typing import Any, Dict, Optional
from tools.environments.modal_utils import (
from hermes_agent.backends.modal_utils import (
BaseModalExecutionEnvironment,
ModalExecStart,
PreparedModalExec,
)
from tools.managed_tool_gateway import resolve_managed_tool_gateway
from hermes_agent.tools.managed_gateway import resolve_managed_tool_gateway
logger = logging.getLogger(__name__)
@@ -214,7 +214,7 @@ class ManagedModalEnvironment(BaseModalExecutionEnvironment):
def _guard_unsupported_credential_passthrough(self) -> None:
"""Managed Modal does not sync or mount host credential files."""
try:
from tools.credential_files import get_credential_file_mounts
from hermes_agent.tools.credential_files import get_credential_file_mounts
except Exception:
return

View File

@@ -14,14 +14,14 @@ import threading
from pathlib import Path
from typing import Any, Optional
from hermes_constants import get_hermes_home
from tools.environments.base import (
from hermes_agent.constants import get_hermes_home
from hermes_agent.backends.base import (
BaseEnvironment,
_ThreadedProcessHandle,
_load_json_store,
_save_json_store,
)
from tools.environments.file_sync import (
from hermes_agent.backends.file_sync import (
FileSyncManager,
iter_sync_files,
quoted_mkdir_command,
@@ -187,7 +187,7 @@ class ModalEnvironment(BaseEnvironment):
cred_mounts = []
try:
from tools.credential_files import (
from hermes_agent.tools.credential_files import (
get_credential_file_mounts,
iter_skills_files,
iter_cache_files,

View File

@@ -20,8 +20,8 @@ from abc import abstractmethod
from dataclasses import dataclass
from typing import Any
from tools.environments.base import BaseEnvironment
from tools.interrupt import is_interrupted
from hermes_agent.backends.base import BaseEnvironment
from hermes_agent.tools.interrupt import is_interrupted
@dataclass(frozen=True)
@@ -136,7 +136,7 @@ class BaseModalExecutionEnvironment(BaseEnvironment):
# Periodic activity touch so the gateway knows we're alive
try:
from tools.environments.base import touch_activity_if_due
from hermes_agent.backends.base import touch_activity_if_due
touch_activity_if_due(_activity_state, "modal command running")
except Exception:
pass

View File

@@ -14,8 +14,8 @@ import uuid
from pathlib import Path
from typing import Optional
from hermes_constants import get_hermes_home
from tools.environments.base import (
from hermes_agent.constants import get_hermes_home
from hermes_agent.backends.base import (
BaseEnvironment,
_load_json_store,
_popen_bash,
@@ -75,7 +75,7 @@ def _get_scratch_dir() -> Path:
scratch_path.mkdir(parents=True, exist_ok=True)
return scratch_path
from tools.environments.base import get_sandbox_dir
from hermes_agent.backends.base import get_sandbox_dir
sandbox = get_sandbox_dir() / "singularity"
scratch = Path("/scratch")
@@ -202,7 +202,7 @@ class SingularityEnvironment(BaseEnvironment):
cmd.append("--writable-tmpfs")
try:
from tools.credential_files import get_credential_file_mounts, get_skills_directory_mount
from hermes_agent.tools.credential_files import get_credential_file_mounts, get_skills_directory_mount
for mount_entry in get_credential_file_mounts():
cmd.extend(["--bind", f"{mount_entry['host_path']}:{mount_entry['container_path']}:ro"])
for skills_mount in get_skills_directory_mount():

View File

@@ -9,8 +9,8 @@ import subprocess
import tempfile
from pathlib import Path
from tools.environments.base import BaseEnvironment, _popen_bash
from tools.environments.file_sync import (
from hermes_agent.backends.base import BaseEnvironment, _popen_bash
from hermes_agent.backends.file_sync import (
FileSyncManager,
iter_sync_files,
quoted_mkdir_command,

View File

View File

@@ -38,8 +38,8 @@ from typing import Any, Dict, List, Optional
import httpx
import yaml
from hermes_cli.config import get_hermes_home, get_config_path, read_raw_config
from hermes_constants import OPENROUTER_BASE_URL
from hermes_agent.cli.config import get_hermes_home, get_config_path, read_raw_config
from hermes_agent.constants import OPENROUTER_BASE_URL
logger = logging.getLogger(__name__)
@@ -329,7 +329,7 @@ def get_anthropic_key() -> str:
ANTHROPIC_API_KEY -> ANTHROPIC_TOKEN -> CLAUDE_CODE_OAUTH_TOKEN
"""
from hermes_cli.config import get_env_value
from hermes_agent.cli.config import get_env_value
for var in PROVIDER_REGISTRY["anthropic"].api_key_env_vars:
value = get_env_value(var) or os.getenv(var, "")
@@ -406,7 +406,7 @@ def _resolve_api_key_provider_secret(
if provider_id == "copilot":
# Use the dedicated copilot auth module for proper token validation
try:
from hermes_cli.copilot_auth import resolve_copilot_token
from hermes_agent.cli.auth.copilot import resolve_copilot_token
token, source = resolve_copilot_token()
if token:
return token, source
@@ -757,16 +757,20 @@ def _save_provider_state(auth_store: Dict[str, Any], provider_id: str, state: Di
auth_store["active_provider"] = provider_id
def read_credential_pool(provider_id: Optional[str] = None) -> Dict[str, Any]:
"""Return the persisted credential pool, or one provider slice."""
def read_credential_pool() -> Dict[str, Any]:
"""Return the entire persisted credential pool."""
auth_store = _load_auth_store()
pool = auth_store.get("credential_pool")
if not isinstance(pool, dict):
pool = {}
if provider_id is None:
return dict(pool)
provider_entries = pool.get(provider_id)
return list(provider_entries) if isinstance(provider_entries, list) else []
return dict(pool)
def read_provider_credentials(provider_id: str) -> List[Dict[str, Any]]:
"""Return credential entries for a single provider."""
pool = read_credential_pool()
entries = pool.get(provider_id)
return list(entries) if isinstance(entries, list) else []
def write_credential_pool(provider_id: str, entries: List[Dict[str, Any]]) -> Path:
@@ -862,7 +866,7 @@ def is_provider_explicitly_configured(provider_id: str) -> bool:
# 2. Check config.yaml model.provider
try:
from hermes_cli.config import load_config
from hermes_agent.cli.config import load_config
cfg = load_config()
model_cfg = cfg.get("model")
if isinstance(model_cfg, dict):
@@ -949,7 +953,7 @@ def _get_config_hint_for_unknown_provider(provider_name: str) -> str:
and returns a human-readable diagnostic, or empty string if nothing found.
"""
try:
from hermes_cli.config import validate_config_structure
from hermes_agent.cli.config import validate_config_structure
issues = validate_config_structure()
if not issues:
return ""
@@ -1064,7 +1068,7 @@ def resolve_provider(
# AWS Bedrock — detect via boto3 credential chain (IAM roles, SSO, env vars).
# This runs after API-key providers so explicit keys always win.
try:
from agent.bedrock_adapter import has_aws_credentials
from hermes_agent.providers.bedrock_adapter import has_aws_credentials
if has_aws_credentials():
return "bedrock"
except ImportError:
@@ -1327,7 +1331,7 @@ def resolve_gemini_oauth_runtime_credentials(
) -> Dict[str, Any]:
"""Resolve runtime OAuth creds for google-gemini-cli."""
try:
from agent.google_oauth import (
from hermes_agent.providers.google_oauth import (
GoogleOAuthError,
_credentials_path,
get_valid_access_token,
@@ -1366,7 +1370,7 @@ def resolve_gemini_oauth_runtime_credentials(
def get_gemini_oauth_auth_status() -> Dict[str, Any]:
"""Return a status dict for `hermes auth list` / `hermes status`."""
try:
from agent.google_oauth import _credentials_path, load_credentials
from hermes_agent.providers.google_oauth import _credentials_path, load_credentials
except ImportError:
return {"logged_in": False, "error": "agent.google_oauth unavailable"}
auth_path = _credentials_path()
@@ -2155,7 +2159,7 @@ def persist_nous_credentials(
Returns the upserted :class:`PooledCredential` entry (or ``None`` if
seeding somehow produced no match shouldn't happen).
"""
from agent.credential_pool import load_pool
from hermes_agent.providers.credential_pool import load_pool
state = dict(creds)
if label and str(label).strip():
@@ -2436,7 +2440,7 @@ def get_nous_auth_status() -> Dict[str, Any]:
# Check credential pool first — the dashboard device-code flow saves
# here but may not have written to the auth store yet.
try:
from agent.credential_pool import load_pool
from hermes_agent.providers.credential_pool import load_pool
pool = load_pool("nous")
if pool and pool.has_credentials():
entry = pool.select()
@@ -2490,7 +2494,7 @@ def get_codex_auth_status() -> Dict[str, Any]:
# Check credential pool first — this is where `hermes auth` and
# `hermes model` store device_code tokens.
try:
from agent.credential_pool import load_pool
from hermes_agent.providers.credential_pool import load_pool
pool = load_pool("openai-codex")
if pool and pool.has_credentials():
entry = pool.select()
@@ -2611,7 +2615,7 @@ def get_auth_status(provider_id: Optional[str] = None) -> Dict[str, Any]:
# AWS SDK providers (Bedrock) — check via boto3 credential chain
if pconfig and pconfig.auth_type == "aws_sdk":
try:
from agent.bedrock_adapter import has_aws_credentials
from hermes_agent.providers.bedrock_adapter import has_aws_credentials
return {"logged_in": has_aws_credentials(), "provider": target}
except ImportError:
return {"logged_in": False, "provider": target, "error": "boto3 not installed"}
@@ -2800,7 +2804,7 @@ def _prompt_model_selection(
If *unavailable_models* is provided, those models are shown grayed out
and unselectable, with an upgrade link to *portal_url*.
"""
from hermes_cli.models import _format_price_per_mtok
from hermes_agent.cli.models.models import _format_price_per_mtok
_unavailable = unavailable_models or []
@@ -2910,7 +2914,7 @@ def _prompt_model_selection(
title=effective_title,
)
idx = menu.show()
from hermes_cli.curses_ui import flush_stdin
from hermes_agent.cli.ui.curses import flush_stdin
flush_stdin()
if idx is None:
return None
@@ -2967,7 +2971,7 @@ def _save_model_choice(model_id: str) -> None:
The model is stored in config.yaml only NOT in .env. This avoids
conflicts in multi-agent setups where env vars would stomp each other.
"""
from hermes_cli.config import save_config, load_config
from hermes_agent.cli.config import save_config, load_config
config = load_config()
# Always use dict format so provider/base_url can be stored alongside
@@ -3046,7 +3050,7 @@ def _login_openai_codex(args, pconfig: ProviderConfig) -> None:
config_path = _update_config_for_provider("openai-codex", creds.get("base_url", DEFAULT_CODEX_BASE_URL))
print()
print("Login successful!")
from hermes_constants import display_hermes_home as _dhh
from hermes_agent.constants import display_hermes_home as _dhh
print(f" Auth state: {_dhh()}/auth.json")
print(f" Config updated: {config_path} (model.provider=openai-codex)")
@@ -3383,7 +3387,7 @@ def _login_nous(args, pconfig: ProviderConfig) -> None:
code="invalid_token",
)
from hermes_cli.models import (
from hermes_agent.cli.models.models import (
_PROVIDER_MODELS, get_pricing_for_provider,
check_nous_free_tier, partition_nous_models_by_tier,
)

View File

@@ -9,7 +9,7 @@ import time
from types import SimpleNamespace
import uuid
from agent.credential_pool import (
from hermes_agent.providers.credential_pool import (
AUTH_TYPE_API_KEY,
AUTH_TYPE_OAUTH,
CUSTOM_POOL_PREFIX,
@@ -27,9 +27,9 @@ from agent.credential_pool import (
list_custom_pool_providers,
load_pool,
)
import hermes_cli.auth as auth_mod
from hermes_cli.auth import PROVIDER_REGISTRY
from hermes_constants import OPENROUTER_BASE_URL
import hermes_agent.cli.auth.auth as auth_mod
from hermes_agent.cli.auth.auth import PROVIDER_REGISTRY
from hermes_agent.constants import OPENROUTER_BASE_URL
# Providers that support OAuth login in addition to API keys.
@@ -39,7 +39,7 @@ _OAUTH_CAPABLE_PROVIDERS = {"anthropic", "nous", "openai-codex", "qwen-oauth", "
def _get_custom_provider_names() -> list:
"""Return list of (display_name, pool_key, provider_key) tuples."""
try:
from hermes_cli.config import get_compatible_custom_providers, load_config
from hermes_agent.cli.config import get_compatible_custom_providers, load_config
config = load_config()
except Exception:
@@ -88,7 +88,7 @@ def _provider_base_url(provider: str) -> str:
if provider == "openrouter":
return OPENROUTER_BASE_URL
if provider.startswith(CUSTOM_POOL_PREFIX):
from agent.credential_pool import _get_custom_provider_config
from hermes_agent.providers.credential_pool import _get_custom_provider_config
cp_config = _get_custom_provider_config(provider)
if cp_config:
@@ -159,7 +159,7 @@ def auth_add_command(args) -> None:
# Matches the Codex device_code re-link pattern that predates this.
if not provider.startswith(CUSTOM_POOL_PREFIX):
try:
from hermes_cli.auth import (
from hermes_agent.cli.auth.auth import (
_load_auth_store,
unsuppress_credential_source,
)
@@ -197,7 +197,7 @@ def auth_add_command(args) -> None:
return
if provider == "anthropic":
from agent import anthropic_adapter as anthropic_mod
from hermes_agent.providers import anthropic_adapter as anthropic_mod
creds = anthropic_mod.run_hermes_oauth_login_pure()
if not creds:
@@ -271,7 +271,7 @@ def auth_add_command(args) -> None:
return
if provider == "google-gemini-cli":
from agent.google_oauth import run_gemini_oauth_login_pure
from hermes_agent.providers.google_oauth import run_gemini_oauth_login_pure
creds = run_gemini_oauth_login_pure()
label = (getattr(args, "label", None) or "").strip() or (
@@ -361,8 +361,8 @@ def auth_remove_command(args) -> None:
# handles its source-specific cleanup and we centralise suppression +
# user-facing output here so every source behaves identically from
# the user's perspective.
from agent.credential_sources import find_removal_step
from hermes_cli.auth import suppress_credential_source
from hermes_agent.providers.credential_sources import find_removal_step
from hermes_agent.cli.auth.auth import suppress_credential_source
step = find_removal_step(provider, removed.source)
if step is None:
@@ -396,7 +396,7 @@ def _interactive_auth() -> None:
# Show AWS Bedrock credential status (not in the pool — uses boto3 chain)
try:
from agent.bedrock_adapter import has_aws_credentials, resolve_aws_auth_env_var, resolve_bedrock_region
from hermes_agent.providers.bedrock_adapter import has_aws_credentials, resolve_aws_auth_env_var, resolve_bedrock_region
if has_aws_credentials():
auth_source = resolve_aws_auth_env_var() or "unknown"
region = resolve_bedrock_region()
@@ -558,7 +558,7 @@ def _interactive_strategy() -> None:
print("Invalid choice.")
return
from hermes_cli.config import load_config, save_config
from hermes_agent.cli.config import load_config, save_config
cfg = load_config()
pool_strategies = cfg.get("credential_pool_strategies") or {}
if not isinstance(pool_strategies, dict):

View File

@@ -18,7 +18,7 @@ import os
import sys
import time
import logging
from typing import Optional, Tuple
from typing import Any, Callable, Optional, Tuple
import requests
@@ -108,7 +108,7 @@ def wait_for_registration_success(
device_code: str,
interval: int = 3,
expires_in: int = 7200,
on_waiting: Optional[callable] = None,
on_waiting: Optional[Callable[..., Any]] = None,
) -> Tuple[str, str]:
"""Block until the registration succeeds or times out.
@@ -234,7 +234,7 @@ def dingtalk_qr_auth() -> Optional[Tuple[str, str]]:
Returns (client_id, client_secret) on success, or None if the user
cancelled or the flow failed.
"""
from hermes_cli.setup import print_info, print_success, print_warning, print_error
from hermes_agent.cli.setup_wizard import print_info, print_success, print_warning, print_error
print()
print_info(" Initializing DingTalk device authorization...")

View File

@@ -21,7 +21,7 @@ from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
from hermes_constants import get_default_hermes_root, get_hermes_home, display_hermes_home
from hermes_agent.constants import get_default_hermes_root, get_hermes_home, display_hermes_home
logger = logging.getLogger(__name__)
@@ -396,7 +396,7 @@ def run_import(args) -> None:
restored_profiles = []
if profiles_dir.is_dir():
try:
from hermes_cli.profiles import (
from hermes_agent.cli.profiles import (
create_wrapper_script, check_alias_collision,
_is_wrapper_dir_in_path, _get_wrapper_dir,
)

View File

@@ -16,9 +16,9 @@ import sys
from datetime import datetime
from pathlib import Path
from hermes_cli.config import get_hermes_home, get_config_path, load_config, save_config
from hermes_constants import get_optional_skills_dir
from hermes_cli.setup import (
from hermes_agent.cli.config import get_hermes_home, get_config_path, load_config, save_config
from hermes_agent.constants import get_optional_skills_dir
from hermes_agent.cli.setup_wizard import (
Colors,
color,
print_header,
@@ -30,7 +30,7 @@ from hermes_cli.setup import (
logger = logging.getLogger(__name__)
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
PROJECT_ROOT = Path(__file__).resolve().parents[2].resolve()
_OPENCLAW_SCRIPT = (
get_optional_skills_dir(PROJECT_ROOT / "optional-skills")
@@ -153,7 +153,7 @@ def _warn_if_gateway_running(auto_yes: bool) -> None:
(e.g. Telegram 409 "terminated by other getUpdates request"). Warn the
user and let them decide whether to continue.
"""
from gateway.status import get_running_pid, read_runtime_status
from hermes_agent.gateway.status import get_running_pid, read_runtime_status
if not get_running_pid():
return

View File

@@ -19,7 +19,7 @@ import subprocess
import sys
from pathlib import Path
from hermes_constants import is_wsl as _is_wsl
from hermes_agent.constants import is_wsl as _is_wsl
logger = logging.getLogger(__name__)
@@ -276,7 +276,7 @@ def _get_ps_exe() -> str | None:
global _ps_exe
if _ps_exe is False:
_ps_exe = _find_powershell()
return _ps_exe
return _ps_exe if isinstance(_ps_exe, str) else None
def _windows_has_image() -> bool:
@@ -395,14 +395,17 @@ def _wayland_save(dest: Path) -> bool:
def _convert_to_png(path: Path) -> bool:
"""Convert an image file to PNG in-place (requires Pillow or ImageMagick)."""
# Try Pillow first (likely installed in the venv)
try:
from PIL import Image
except ImportError:
raise ImportError(
"Pillow is required for clipboard image conversion. "
"Install with: pip install hermes-agent[cli]"
) from None
try:
img = Image.open(path)
img.save(path, "PNG")
return True
except ImportError:
pass
except Exception as e:
logger.debug("Pillow BMP→PNG conversion failed: %s", e)

View File

@@ -318,7 +318,7 @@ def _resolve_config_gates() -> set[str]:
if not gated:
return set()
try:
from hermes_cli.config import read_raw_config
from hermes_agent.cli.config import read_raw_config
cfg = read_raw_config()
except Exception:
return set()
@@ -497,7 +497,7 @@ def _collect_gateway_skill_entries(
# --- Tier 1: Plugin slash commands (never trimmed) ---------------------
plugin_pairs: list[tuple[str, str]] = []
try:
from hermes_cli.plugins import get_plugin_commands
from hermes_agent.cli.plugins import get_plugin_commands
plugin_cmds = get_plugin_commands()
for cmd_name in sorted(plugin_cmds):
name = sanitize_name(cmd_name) if sanitize_name else cmd_name
@@ -519,15 +519,15 @@ def _collect_gateway_skill_entries(
# --- Tier 2: Built-in skill commands (trimmed at cap) -----------------
_platform_disabled: set[str] = set()
try:
from agent.skill_utils import get_disabled_skill_names
from hermes_agent.agent.skill_utils import get_disabled_skill_names
_platform_disabled = get_disabled_skill_names(platform=platform)
except Exception:
pass
skill_triples: list[tuple[str, str, str]] = []
try:
from agent.skill_commands import get_skill_commands
from tools.skills_tool import SKILLS_DIR
from hermes_agent.agent.skill_commands import get_skill_commands
from hermes_agent.tools.skills.tool import SKILLS_DIR
_skills_dir = str(SKILLS_DIR.resolve())
_hub_dir = str((SKILLS_DIR / ".hub").resolve())
skill_cmds = get_skill_commands()
@@ -661,7 +661,7 @@ def discord_skill_commands_by_category(
_platform_disabled: set[str] = set()
try:
from agent.skill_utils import get_disabled_skill_names
from hermes_agent.agent.skill_utils import get_disabled_skill_names
_platform_disabled = get_disabled_skill_names(platform="discord")
except Exception:
pass
@@ -673,8 +673,8 @@ def discord_skill_commands_by_category(
hidden = 0
try:
from agent.skill_commands import get_skill_commands
from tools.skills_tool import SKILLS_DIR
from hermes_agent.agent.skill_commands import get_skill_commands
from hermes_agent.tools.skills.tool import SKILLS_DIR
_skills_dir = SKILLS_DIR.resolve()
_hub_dir = (SKILLS_DIR / ".hub").resolve()
skill_cmds = get_skill_commands()
@@ -1116,7 +1116,7 @@ class SlashCommandCompleter(Completer):
def _skin_completions(sub_text: str, sub_lower: str):
"""Yield completions for /skin from available skins."""
try:
from hermes_cli.skin_engine import list_skins
from hermes_agent.cli.ui.skin_engine import list_skins
for s in list_skins():
name = s["name"]
if name.startswith(sub_lower) and name != sub_lower:
@@ -1133,7 +1133,7 @@ class SlashCommandCompleter(Completer):
def _personality_completions(sub_text: str, sub_lower: str):
"""Yield completions for /personality from configured personalities."""
try:
from hermes_cli.config import load_config
from hermes_agent.cli.config import load_config
personalities = load_config().get("agent", {}).get("personalities", {})
if "none".startswith(sub_lower) and "none" != sub_lower:
yield Completion(
@@ -1162,7 +1162,7 @@ class SlashCommandCompleter(Completer):
seen = set()
# Config-based direct aliases (preferred — include provider info)
try:
from hermes_cli.model_switch import (
from hermes_agent.cli.models.switch import (
_ensure_direct_aliases, DIRECT_ALIASES, MODEL_ALIASES,
)
_ensure_direct_aliases()
@@ -1262,7 +1262,7 @@ class SlashCommandCompleter(Completer):
# Plugin-registered slash commands
try:
from hermes_cli.plugins import get_plugin_commands
from hermes_agent.cli.plugins import get_plugin_commands
for cmd_name, cmd_info in get_plugin_commands().items():
if cmd_name.startswith(word):
desc = str(cmd_info.get("description", "Plugin command"))

View File

@@ -23,7 +23,7 @@ import sys
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Any, Optional, List, Tuple
from typing import Dict, Any, Optional, List, Tuple, TypedDict, Union
logger = logging.getLogger(__name__)
@@ -61,8 +61,8 @@ _EXTRA_ENV_KEYS = frozenset({
})
import yaml
from hermes_cli.colors import Colors, color
from hermes_cli.default_soul import DEFAULT_SOUL_MD
from hermes_agent.cli.ui.colors import Colors, color
from hermes_agent.cli.default_soul import DEFAULT_SOUL_MD
# =============================================================================
@@ -169,7 +169,7 @@ def get_container_exec_info() -> Optional[dict]:
if os.environ.get("HERMES_DEV") == "1":
return None
from hermes_constants import is_container
from hermes_agent.constants import is_container
if is_container():
return None
@@ -205,7 +205,7 @@ def get_container_exec_info() -> Optional[dict]:
# =============================================================================
# Re-export from hermes_constants — canonical definition lives there.
from hermes_constants import get_hermes_home # noqa: F811,E402
from hermes_agent.constants import get_hermes_home # noqa: F811,E402
def get_config_path() -> Path:
"""Get the main config file path."""
@@ -217,7 +217,7 @@ def get_env_path() -> Path:
def get_project_root() -> Path:
"""Get the project installation directory."""
return Path(__file__).parent.parent.resolve()
return Path(__file__).resolve().parents[2].resolve()
def _secure_dir(path):
"""Set directory to owner-only access (0700 by default). No-op on Windows.
@@ -343,12 +343,363 @@ def _ensure_hermes_home_managed(home: Path):
# Config loading/saving
# =============================================================================
DEFAULT_CONFIG = {
class _AgentConfig(TypedDict):
max_turns: int
gateway_timeout: int
restart_drain_timeout: int
service_tier: str
tool_use_enforcement: str
gateway_timeout_warning: int
gateway_notify_interval: int
class _TerminalConfig(TypedDict):
backend: str
modal_mode: str
cwd: str
timeout: int
env_passthrough: List[str]
docker_image: str
docker_forward_env: List[str]
docker_env: Dict[str, str]
singularity_image: str
modal_image: str
daytona_image: str
container_cpu: int
container_memory: int
container_disk: int
container_persistent: bool
docker_volumes: List[str]
docker_mount_cwd_to_workspace: bool
persistent_shell: bool
class _CamofoxConfig(TypedDict, total=False):
managed_persistence: bool
class _BrowserConfig(TypedDict):
inactivity_timeout: int
command_timeout: int
record_sessions: bool
allow_private_urls: bool
cdp_url: str
camofox: _CamofoxConfig
class _CheckpointsConfig(TypedDict):
enabled: bool
max_snapshots: int
class _CompressionConfig(TypedDict):
enabled: bool
threshold: float
target_ratio: float
protect_last_n: int
class _BedrockDiscoveryConfig(TypedDict):
enabled: bool
provider_filter: List[str]
refresh_interval: int
class _BedrockGuardrailConfig(TypedDict):
guardrail_identifier: str
guardrail_version: str
stream_processing_mode: str
trace: str
class _BedrockConfig(TypedDict):
region: str
discovery: _BedrockDiscoveryConfig
guardrail: _BedrockGuardrailConfig
class _AuxiliaryTaskConfig(TypedDict, total=False):
provider: str
model: str
base_url: str
api_key: str
timeout: int
extra_body: Dict[str, Any]
max_concurrency: int
download_timeout: int
class _AuxiliaryConfig(TypedDict):
vision: _AuxiliaryTaskConfig
web_extract: _AuxiliaryTaskConfig
compression: _AuxiliaryTaskConfig
session_search: _AuxiliaryTaskConfig
skills_hub: _AuxiliaryTaskConfig
approval: _AuxiliaryTaskConfig
mcp: _AuxiliaryTaskConfig
flush_memories: _AuxiliaryTaskConfig
title_generation: _AuxiliaryTaskConfig
class _UserMessagePreviewConfig(TypedDict):
first_lines: int
last_lines: int
class _DisplayConfig(TypedDict):
compact: bool
personality: str
resume_display: str
busy_input_mode: str
bell_on_complete: bool
show_reasoning: bool
streaming: bool
final_response_markdown: str
inline_diffs: bool
show_cost: bool
skin: str
user_message_preview: _UserMessagePreviewConfig
interim_assistant_messages: bool
tool_progress_command: bool
tool_progress_overrides: Dict[str, Any]
tool_preview_length: int
platforms: Dict[str, Any]
class _DashboardConfig(TypedDict):
theme: str
class _PrivacyConfig(TypedDict):
redact_pii: bool
class _EdgeTtsConfig(TypedDict):
voice: str
class _ElevenlabsTtsConfig(TypedDict):
voice_id: str
model_id: str
class _OpenaiTtsConfig(TypedDict):
model: str
voice: str
class _XaiTtsConfig(TypedDict):
voice_id: str
language: str
sample_rate: int
bit_rate: int
class _MistralTtsConfig(TypedDict):
model: str
voice_id: str
class _NeuttsConfig(TypedDict):
ref_audio: str
ref_text: str
model: str
device: str
class _TtsConfig(TypedDict):
provider: str
edge: _EdgeTtsConfig
elevenlabs: _ElevenlabsTtsConfig
openai: _OpenaiTtsConfig
xai: _XaiTtsConfig
mistral: _MistralTtsConfig
neutts: _NeuttsConfig
class _LocalSttConfig(TypedDict):
model: str
language: str
class _OpenaiSttConfig(TypedDict):
model: str
class _MistralSttConfig(TypedDict):
model: str
class _SttConfig(TypedDict):
enabled: bool
provider: str
local: _LocalSttConfig
openai: _OpenaiSttConfig
mistral: _MistralSttConfig
class _VoiceConfig(TypedDict):
record_key: str
max_recording_seconds: int
auto_tts: bool
silence_threshold: int
silence_duration: float
class _HumanDelayConfig(TypedDict):
mode: str
min_ms: int
max_ms: int
class _ContextConfig(TypedDict):
engine: str
class _MemoryConfig(TypedDict):
memory_enabled: bool
user_profile_enabled: bool
memory_char_limit: int
user_char_limit: int
provider: str
class _DelegationConfig(TypedDict):
model: str
provider: str
base_url: str
api_key: str
max_iterations: int
reasoning_effort: str
class _SkillsConfig(TypedDict):
external_dirs: List[str]
class _ChannelPromptsConfig(TypedDict):
channel_prompts: Dict[str, str]
class _DiscordConfig(TypedDict):
require_mention: bool
free_response_channels: str
allowed_channels: str
auto_thread: bool
reactions: bool
channel_prompts: Dict[str, str]
server_actions: str
class _ApprovalsConfig(TypedDict):
mode: str
timeout: int
cron_mode: str
class _WebsiteBlocklistConfig(TypedDict):
enabled: bool
domains: List[str]
shared_files: List[str]
class _SecurityConfig(TypedDict):
redact_secrets: bool
tirith_enabled: bool
tirith_path: str
tirith_timeout: int
tirith_fail_open: bool
website_blocklist: _WebsiteBlocklistConfig
class _CronConfig(TypedDict):
wrap_response: bool
max_parallel_jobs: Optional[int]
class _CodeExecutionConfig(TypedDict):
mode: str
class _LoggingConfig(TypedDict):
level: str
max_size_mb: int
backup_count: int
class _NetworkConfig(TypedDict):
force_ipv4: bool
class _DefaultConfig(TypedDict):
model: str
providers: Dict[str, Any]
fallback_providers: List[Any]
credential_pool_strategies: Dict[str, Any]
toolsets: List[str]
agent: _AgentConfig
terminal: _TerminalConfig
browser: _BrowserConfig
checkpoints: _CheckpointsConfig
file_read_max_chars: int
compression: _CompressionConfig
bedrock: _BedrockConfig
auxiliary: _AuxiliaryConfig
display: _DisplayConfig
dashboard: _DashboardConfig
privacy: _PrivacyConfig
tts: _TtsConfig
stt: _SttConfig
voice: _VoiceConfig
human_delay: _HumanDelayConfig
context: _ContextConfig
memory: _MemoryConfig
delegation: _DelegationConfig
prefill_messages_file: str
skills: _SkillsConfig
honcho: Dict[str, Any]
timezone: str
discord: _DiscordConfig
whatsapp: Dict[str, Any]
telegram: _ChannelPromptsConfig
slack: _ChannelPromptsConfig
mattermost: _ChannelPromptsConfig
approvals: _ApprovalsConfig
command_allowlist: List[str]
quick_commands: Dict[str, Any]
hooks: Dict[str, Any]
hooks_auto_accept: bool
personalities: Dict[str, Any]
security: _SecurityConfig
cron: _CronConfig
code_execution: _CodeExecutionConfig
logging: _LoggingConfig
network: _NetworkConfig
_config_version: int
class _EnvVarRequired(TypedDict):
description: str
prompt: str
category: str
class _EnvVarOptional(TypedDict, total=False):
url: Optional[str]
password: bool
tools: List[str]
advanced: bool
class _EnvVarInfo(_EnvVarRequired, _EnvVarOptional):
pass
DEFAULT_CONFIG: _DefaultConfig = {
"model": "",
"providers": {},
"fallback_providers": [],
"credential_pool_strategies": {},
"toolsets": ["hermes-cli"],
"hermes_agent.tools.toolsets": ["hermes-cli"],
"agent": {
"max_turns": 90,
# Inactivity timeout for gateway agent execution (seconds).
@@ -919,7 +1270,7 @@ ENV_VARS_BY_VERSION: Dict[int, List[str]] = {
REQUIRED_ENV_VARS = {}
# Optional environment variables that enhance functionality
OPTIONAL_ENV_VARS = {
OPTIONAL_ENV_VARS: Dict[str, _EnvVarInfo] = {
# ── Provider (handled in provider selection, not shown in checklists) ──
"NOUS_BASE_URL": {
"description": "Nous Portal base URL override",
@@ -1853,7 +2204,7 @@ def get_missing_config_fields() -> List[Dict[str, Any]]:
config = load_config()
missing = []
def _check(defaults: dict, current: dict, prefix: str = ""):
def _check(defaults: Dict[str, Any], current: Dict[str, Any], prefix: str = ""):
for key, default_value in defaults.items():
if key.startswith('_'):
continue
@@ -1867,7 +2218,7 @@ def get_missing_config_fields() -> List[Dict[str, Any]]:
elif isinstance(default_value, dict) and isinstance(current.get(key), dict):
_check(default_value, current[key], full_key)
_check(DEFAULT_CONFIG, config)
_check(dict(DEFAULT_CONFIG), config)
return missing
@@ -1879,7 +2230,7 @@ def get_missing_skill_config_vars() -> List[Dict[str, Any]]:
config.yaml. Returns a list of dicts suitable for prompting.
"""
try:
from agent.skill_utils import discover_all_skill_config_vars, SKILL_CONFIG_PREFIX
from hermes_agent.agent.skill_utils import discover_all_skill_config_vars, SKILL_CONFIG_PREFIX
except Exception:
return []
@@ -2087,8 +2438,8 @@ def check_config_version() -> Tuple[int, int]:
Returns (current_version, latest_version).
"""
config = load_config()
current = config.get("_config_version", 0)
latest = DEFAULT_CONFIG.get("_config_version", 1)
current = int(config.get("_config_version", 0))
latest = int(DEFAULT_CONFIG.get("_config_version", 1))
return current, latest
@@ -2099,7 +2450,7 @@ def check_config_version() -> Tuple[int, int]:
# Fields that are valid at root level of config.yaml
_KNOWN_ROOT_KEYS = {
"_config_version", "model", "providers", "fallback_model",
"fallback_providers", "credential_pool_strategies", "toolsets",
"fallback_providers", "credential_pool_strategies", "hermes_agent.tools.toolsets",
"agent", "terminal", "display", "compression", "delegation",
"auxiliary", "custom_providers", "context", "memory", "gateway",
}
@@ -2781,7 +3132,7 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
print()
config = load_config()
try:
from agent.skill_utils import SKILL_CONFIG_PREFIX
from hermes_agent.agent.skill_utils import SKILL_CONFIG_PREFIX
except Exception:
SKILL_CONFIG_PREFIX = "skills.config"
for var in missing_skill_config:
@@ -2807,7 +3158,7 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
return results
def _deep_merge(base: dict, override: dict) -> dict:
def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
"""Recursively merge *override* into *base*, preserving nested defaults.
Keys in *override* take precedence. If both values are dicts the merge
@@ -2996,7 +3347,7 @@ def load_config() -> Dict[str, Any]:
ensure_hermes_home()
config_path = get_config_path()
config = copy.deepcopy(DEFAULT_CONFIG)
config: Dict[str, Any] = copy.deepcopy(DEFAULT_CONFIG)
if config_path.exists():
try:
@@ -3096,7 +3447,7 @@ def save_config(config: Dict[str, Any]):
if is_managed():
managed_error("save configuration")
return
from utils import atomic_yaml_write
from hermes_agent.utils import atomic_yaml_write
ensure_hermes_home()
config_path = get_config_path()
@@ -3532,7 +3883,7 @@ def show_config():
for env_key, name in keys:
value = get_env_value(env_key)
print(f" {name:<14} {redact_key(value)}")
from hermes_cli.auth import get_anthropic_key
from hermes_agent.cli.auth.auth import get_anthropic_key
anthropic_value = get_anthropic_key()
print(f" {'Anthropic':<14} {redact_key(anthropic_value)}")
@@ -3640,7 +3991,7 @@ def show_config():
# Skill config
try:
from agent.skill_utils import discover_all_skill_config_vars, resolve_skill_config_values
from hermes_agent.agent.skill_utils import discover_all_skill_config_vars, resolve_skill_config_values
skill_vars = discover_all_skill_config_vars()
if skill_vars:
resolved = resolve_skill_config_values(skill_vars)
@@ -3672,7 +4023,7 @@ def edit_config():
# Ensure config exists
if not config_path.exists():
save_config(DEFAULT_CONFIG)
save_config(dict(DEFAULT_CONFIG))
print(f"Created {config_path}")
# Find editor
@@ -3754,7 +4105,7 @@ def set_config_value(key: str, value: str):
# Write only user config back (not the full merged defaults)
ensure_hermes_home()
from utils import atomic_yaml_write
from hermes_agent.utils import atomic_yaml_write
atomic_yaml_write(config_path, user_config, sort_keys=False)
# Keep .env in sync for keys that terminal_tool reads directly from env vars.

View File

@@ -10,10 +10,9 @@ import sys
from pathlib import Path
from typing import Iterable, List, Optional
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
sys.path.insert(0, str(PROJECT_ROOT))
PROJECT_ROOT = Path(__file__).resolve().parents[2].resolve()
from hermes_cli.colors import Colors, color
from hermes_agent.cli.ui.colors import Colors, color
def _normalize_skills(single_skill=None, skills: Optional[Iterable[str]] = None) -> Optional[List[str]]:
@@ -33,14 +32,14 @@ def _normalize_skills(single_skill=None, skills: Optional[Iterable[str]] = None)
def _cron_api(**kwargs):
from tools.cronjob_tools import cronjob as cronjob_tool
from hermes_agent.tools.cronjob import cronjob as cronjob_tool
return json.loads(cronjob_tool(**kwargs))
def cron_list(show_all: bool = False):
"""List all scheduled jobs."""
from cron.jobs import list_jobs
from hermes_agent.cron.jobs import list_jobs
jobs = list_jobs(include_disabled=show_all)
@@ -110,7 +109,7 @@ def cron_list(show_all: bool = False):
print()
from hermes_cli.gateway import find_gateway_pids
from hermes_agent.cli.gateway import find_gateway_pids
if not find_gateway_pids():
print(color(" ⚠ Gateway is not running — jobs won't fire automatically.", Colors.YELLOW))
print(color(" Start it with: hermes gateway install", Colors.DIM))
@@ -120,14 +119,14 @@ def cron_list(show_all: bool = False):
def cron_tick():
"""Run due jobs once and exit."""
from cron.scheduler import tick
from hermes_agent.cron.scheduler import tick
tick(verbose=True)
def cron_status():
"""Show cron execution status."""
from cron.jobs import list_jobs
from hermes_cli.gateway import find_gateway_pids
from hermes_agent.cron.jobs import list_jobs
from hermes_agent.cli.gateway import find_gateway_pids
print()
@@ -185,7 +184,7 @@ def cron_create(args):
def cron_edit(args):
from cron.jobs import get_job
from hermes_agent.cron.jobs import get_job
job = get_job(args.job_id)
if not job:

View File

@@ -16,7 +16,7 @@ import urllib.request
from pathlib import Path
from typing import Optional
from hermes_constants import get_hermes_home
from hermes_agent.constants import get_hermes_home
# ---------------------------------------------------------------------------
@@ -319,7 +319,7 @@ def _resolve_log_path(log_name: str) -> Optional[Path]:
Returns the path if found, or None.
"""
from hermes_cli.logs import LOG_FILES
from hermes_agent.cli.logs import LOG_FILES
filename = LOG_FILES.get(log_name)
if not filename:
@@ -340,7 +340,7 @@ def _resolve_log_path(log_name: str) -> Optional[Path]:
def _read_log_tail(log_name: str, num_lines: int) -> str:
"""Read the last *num_lines* from a log file, or return a placeholder."""
from hermes_cli.logs import _read_last_n_lines
from hermes_agent.cli.logs import _read_last_n_lines
log_path = _resolve_log_path(log_name)
if log_path is None:
@@ -388,7 +388,7 @@ def _read_full_log(log_name: str, max_bytes: int = _MAX_LOG_BYTES) -> Optional[s
def _capture_dump() -> str:
"""Run ``hermes dump`` and return its stdout as a string."""
from hermes_cli.dump import run_dump
from hermes_agent.cli.dump import run_dump
class _FakeArgs:
show_keys = False

View File

@@ -10,8 +10,8 @@ import subprocess
import shutil
from pathlib import Path
from hermes_cli.config import get_project_root, get_hermes_home, get_env_path
from hermes_constants import display_hermes_home
from hermes_agent.cli.config import get_project_root, get_hermes_home, get_env_path
from hermes_agent.constants import display_hermes_home
PROJECT_ROOT = get_project_root()
HERMES_HOME = get_hermes_home()
@@ -28,9 +28,9 @@ if _env_path.exists():
# Also try project .env as dev fallback
load_dotenv(PROJECT_ROOT / ".env", override=False, encoding="utf-8")
from hermes_cli.colors import Colors, color
from hermes_constants import OPENROUTER_MODELS_URL
from utils import base_url_host_matches
from hermes_agent.cli.ui.colors import Colors, color
from hermes_agent.constants import OPENROUTER_MODELS_URL
from hermes_agent.utils import base_url_host_matches
_PROVIDER_ENV_HINTS = (
@@ -58,7 +58,7 @@ _PROVIDER_ENV_HINTS = (
)
from hermes_constants import is_termux as _is_termux
from hermes_agent.constants import is_termux as _is_termux
def _python_install_cmd() -> str:
@@ -92,7 +92,7 @@ def _has_provider_env_config(content: str) -> bool:
def _honcho_is_configured_for_doctor() -> bool:
"""Return True when Honcho is configured, even if this process has no active session."""
try:
from plugins.memory.honcho.client import HonchoClientConfig
from hermes_agent.plugins.memory.honcho.client import HonchoClientConfig
cfg = HonchoClientConfig.from_global_config()
return bool(cfg.enabled and (cfg.api_key or cfg.base_url))
@@ -132,7 +132,7 @@ def check_info(text: str):
def _check_gateway_service_linger(issues: list[str]) -> None:
"""Warn when a systemd user gateway service will stop after logout."""
try:
from hermes_cli.gateway import (
from hermes_agent.cli.gateway import (
get_systemd_linger_status,
get_systemd_unit_path,
is_linux,
@@ -290,12 +290,12 @@ def run_doctor(args):
known_providers: set = set()
try:
from hermes_cli.auth import PROVIDER_REGISTRY
from hermes_agent.cli.auth.auth import PROVIDER_REGISTRY
known_providers = set(PROVIDER_REGISTRY.keys()) | {"openrouter", "custom", "auto"}
except Exception:
pass
try:
from hermes_cli.auth import resolve_provider as _resolve_provider
from hermes_agent.cli.auth.auth import resolve_provider as _resolve_provider
except Exception:
_resolve_provider = None
@@ -338,7 +338,7 @@ def run_doctor(args):
# explicitly dispatch, which would produce false positives.
if canonical_provider and canonical_provider not in ("auto", "custom", "openrouter"):
try:
from hermes_cli.auth import PROVIDER_REGISTRY, get_auth_status
from hermes_agent.cli.auth.auth import PROVIDER_REGISTRY, get_auth_status
pconfig = PROVIDER_REGISTRY.get(canonical_provider)
if pconfig and getattr(pconfig, "auth_type", "") == "api_key":
status = get_auth_status(canonical_provider) or {}
@@ -379,7 +379,7 @@ def run_doctor(args):
config_path = HERMES_HOME / 'config.yaml'
if config_path.exists():
try:
from hermes_cli.config import check_config_version, migrate_config
from hermes_agent.cli.config import check_config_version, migrate_config
current_ver, latest_ver = check_config_version()
if current_ver < latest_ver:
check_warn(
@@ -419,7 +419,7 @@ def run_doctor(args):
model_section[k] = raw_config.pop(k)
else:
raw_config.pop(k)
from utils import atomic_yaml_write
from hermes_agent.utils import atomic_yaml_write
atomic_yaml_write(config_path, raw_config)
check_ok("Migrated stale root-level keys into model section")
fixed_count += 1
@@ -430,7 +430,7 @@ def run_doctor(args):
# Validate config structure (catches malformed custom_providers, etc.)
try:
from hermes_cli.config import validate_config_structure
from hermes_agent.cli.config import validate_config_structure
config_issues = validate_config_structure()
if config_issues:
print()
@@ -454,7 +454,7 @@ def run_doctor(args):
print(color("◆ Auth Providers", Colors.CYAN, Colors.BOLD))
try:
from hermes_cli.auth import (
from hermes_agent.cli.auth.auth import (
get_nous_auth_status,
get_codex_auth_status,
get_gemini_oauth_auth_status,
@@ -877,13 +877,13 @@ def run_doctor(args):
else:
check_warn("OpenRouter API", "(not configured)")
from hermes_cli.auth import get_anthropic_key
from hermes_agent.cli.auth.auth import get_anthropic_key
anthropic_key = get_anthropic_key()
if anthropic_key:
print(" Checking Anthropic API...", end="", flush=True)
try:
import httpx
from agent.anthropic_adapter import _is_oauth_token, _COMMON_BETAS, _OAUTH_ONLY_BETAS
from hermes_agent.providers.anthropic_adapter import _is_oauth_token, _COMMON_BETAS, _OAUTH_ONLY_BETAS
headers = {"anthropic-version": "2023-06-01"}
if _is_oauth_token(anthropic_key):
@@ -951,7 +951,7 @@ def run_doctor(args):
# with no /v1) don't support /models. Rewrite to the OpenAI-compat
# /v1 surface for health checks.
if _base and _base.rstrip("/").endswith("/anthropic"):
from agent.auxiliary_client import _to_openai_base_url
from hermes_agent.providers.auxiliary import _to_openai_base_url
_base = _to_openai_base_url(_base)
if base_url_host_matches(_base, "api.kimi.com") and _base.rstrip("/").endswith("/coding"):
_base = _base.rstrip("/") + "/v1"
@@ -977,7 +977,7 @@ def run_doctor(args):
# -- AWS Bedrock --
# Bedrock uses the AWS SDK credential chain, not API keys.
try:
from agent.bedrock_adapter import has_aws_credentials, resolve_aws_auth_env_var, resolve_bedrock_region
from hermes_agent.providers.bedrock_adapter import has_aws_credentials, resolve_aws_auth_env_var, resolve_bedrock_region
if has_aws_credentials():
_auth_var = resolve_aws_auth_env_var()
_region = resolve_bedrock_region()
@@ -1028,9 +1028,7 @@ def run_doctor(args):
print(color("◆ Tool Availability", Colors.CYAN, Colors.BOLD))
try:
# Add project root to path for imports
sys.path.insert(0, str(PROJECT_ROOT))
from model_tools import check_tool_availability, TOOLSET_REQUIREMENTS
from hermes_agent.tools.dispatch import check_tool_availability, TOOLSET_REQUIREMENTS
available, unavailable = check_tool_availability()
available, unavailable = _apply_doctor_tool_availability_overrides(available, unavailable)
@@ -1079,7 +1077,7 @@ def run_doctor(args):
else:
check_warn("Skills Hub directory not initialized", "(run: hermes skills list)")
from hermes_cli.config import get_env_value
from hermes_agent.cli.config import get_env_value
github_token = get_env_value("GITHUB_TOKEN") or get_env_value("GH_TOKEN")
if github_token:
check_ok("GitHub token configured (authenticated API access)")
@@ -1107,7 +1105,7 @@ def run_doctor(args):
check_ok("Built-in memory active", "(no external provider configured — this is fine)")
elif _active_memory_provider == "honcho":
try:
from plugins.memory.honcho.client import HonchoClientConfig, resolve_config_path
from hermes_agent.plugins.memory.honcho.client import HonchoClientConfig, resolve_config_path
hcfg = HonchoClientConfig.from_global_config()
_honcho_cfg_path = resolve_config_path()
@@ -1119,7 +1117,7 @@ def run_doctor(args):
check_fail("Honcho API key or base URL not set", "run: hermes memory setup")
issues.append("No Honcho API key — run 'hermes memory setup'")
else:
from plugins.memory.honcho.client import get_honcho_client, reset_honcho_client
from hermes_agent.plugins.memory.honcho.client import get_honcho_client, reset_honcho_client
reset_honcho_client()
try:
get_honcho_client(hcfg)
@@ -1137,7 +1135,7 @@ def run_doctor(args):
check_warn("Honcho check failed", str(_e))
elif _active_memory_provider == "mem0":
try:
from plugins.memory.mem0 import _load_config as _load_mem0_config
from hermes_agent.plugins.memory.mem0 import _load_config as _load_mem0_config
mem0_cfg = _load_mem0_config()
mem0_key = mem0_cfg.get("api_key", "")
if mem0_key:
@@ -1154,7 +1152,7 @@ def run_doctor(args):
else:
# Generic check for other memory providers (openviking, hindsight, etc.)
try:
from plugins.memory import load_memory_provider
from hermes_agent.plugins.memory import load_memory_provider
_provider = load_memory_provider(_active_memory_provider)
if _provider and _provider.is_available():
check_ok(f"{_active_memory_provider} provider active")
@@ -1169,7 +1167,7 @@ def run_doctor(args):
# Profiles
# =========================================================================
try:
from hermes_cli.profiles import list_profiles, _get_wrapper_dir, profile_exists
from hermes_agent.cli.profiles import list_profiles, _get_wrapper_dir, profile_exists
import re as _re
named_profiles = [p for p in list_profiles() if not p.is_default]

View File

@@ -13,8 +13,8 @@ import subprocess
import sys
from pathlib import Path
from hermes_cli.config import get_hermes_home, get_env_path, get_project_root, load_config
from hermes_constants import display_hermes_home
from hermes_agent.cli.config import get_hermes_home, get_env_path, get_project_root, load_config
from hermes_agent.constants import display_hermes_home
def _get_git_commit(project_root: Path) -> str:
@@ -44,7 +44,7 @@ def _redact(value: str) -> str:
def _gateway_status() -> str:
"""Return a short gateway status string."""
try:
from hermes_cli.gateway import get_gateway_runtime_snapshot
from hermes_agent.cli.gateway import get_gateway_runtime_snapshot
snapshot = get_gateway_runtime_snapshot()
if snapshot.running:
@@ -142,7 +142,7 @@ def _config_overrides(config: dict) -> dict[str, str]:
Returns a flat dict of dotpath -> value for interesting overrides.
"""
from hermes_cli.config import DEFAULT_CONFIG
from hermes_agent.cli.config import DEFAULT_CONFIG
overrides = {}
@@ -178,7 +178,7 @@ def _config_overrides(config: dict) -> dict[str, str]:
default_toolsets = DEFAULT_CONFIG.get("toolsets", [])
user_toolsets = config.get("toolsets", [])
if user_toolsets != default_toolsets:
overrides["toolsets"] = str(user_toolsets)
overrides["hermes_agent.tools.toolsets"] = str(user_toolsets)
# Fallback providers
fallbacks = config.get("fallback_providers", [])
@@ -207,7 +207,7 @@ def run_dump(args):
hermes_home = get_hermes_home()
try:
from hermes_cli import __version__, __release_date__
from hermes_agent.cli import __version__, __release_date__
except ImportError:
__version__ = "(unknown)"
__release_date__ = ""
@@ -223,7 +223,7 @@ def run_dump(args):
# Profile
try:
from hermes_cli.profiles import get_active_profile_name
from hermes_agent.cli.profiles import get_active_profile_name
profile = get_active_profile_name() or "(default)"
except Exception:
profile = "(default)"

View File

@@ -108,7 +108,7 @@ def _sanitize_env_file_if_needed(path: Path) -> None:
if not path.exists():
return
try:
from hermes_cli.config import _sanitize_env_lines
from hermes_agent.cli.config import _sanitize_env_lines
except ImportError:
return # early bootstrap — config module not available yet

View File

@@ -13,15 +13,15 @@ import sys
from dataclasses import dataclass
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
PROJECT_ROOT = Path(__file__).resolve().parents[2].resolve()
from gateway.status import terminate_pid
from gateway.restart import (
from hermes_agent.gateway.status import terminate_pid
from hermes_agent.gateway.restart import (
DEFAULT_GATEWAY_RESTART_DRAIN_TIMEOUT,
GATEWAY_SERVICE_RESTART_EXIT_CODE,
parse_restart_drain_timeout,
)
from hermes_cli.config import (
from hermes_agent.cli.config import (
get_env_value,
get_hermes_home,
is_managed,
@@ -31,11 +31,11 @@ from hermes_cli.config import (
)
# display_hermes_home is imported lazily at call sites to avoid ImportError
# when hermes_constants is cached from a pre-update version during `hermes update`.
from hermes_cli.setup import (
from hermes_agent.cli.setup_wizard import (
print_header, print_info, print_success, print_warning, print_error,
prompt, prompt_choice, prompt_yes_no,
)
from hermes_cli.colors import Colors, color
from hermes_agent.cli.ui.colors import Colors, color
# =============================================================================
@@ -192,6 +192,12 @@ def _scan_gateway_pids(exclude_pids: set[int], all_profiles: bool = False) -> li
"""
pids: list[int] = []
patterns = [
"hermes_agent.cli.main gateway",
"hermes_agent.cli.main --profile",
"hermes_agent.cli.main -p",
"hermes_agent/cli/main.py gateway",
"hermes_agent/cli/main.py --profile",
"hermes_agent/cli/main.py -p",
"hermes_cli.main gateway",
"hermes_cli.main --profile",
"hermes_cli.main -p",
@@ -303,7 +309,7 @@ def find_gateway_pids(exclude_pids: set | None = None, all_profiles: bool = Fals
pids: list[int] = []
if not all_profiles:
try:
from gateway.status import get_running_pid
from hermes_agent.gateway.status import get_running_pid
_append_unique_pid(pids, get_running_pid(), _exclude)
except Exception:
@@ -357,7 +363,7 @@ def get_gateway_runtime_snapshot(system: bool = False) -> GatewayRuntimeSnapshot
gateway_pids=gateway_pids,
)
from hermes_constants import is_container
from hermes_agent.constants import is_container
if is_linux() and is_container():
return GatewayRuntimeSnapshot(
@@ -445,7 +451,7 @@ def stop_profile_gateway() -> bool:
Returns True if a process was stopped, False if none was found.
"""
try:
from gateway.status import get_running_pid, remove_pid_file
from hermes_agent.gateway.status import get_running_pid, remove_pid_file
except ImportError:
return False
@@ -478,7 +484,7 @@ def is_linux() -> bool:
return sys.platform.startswith('linux')
from hermes_constants import is_container, is_termux, is_wsl
from hermes_agent.constants import is_container, is_termux, is_wsl
def _wsl_systemd_operational() -> bool:
@@ -552,7 +558,7 @@ def _profile_suffix() -> str:
"""
import hashlib
import re
from hermes_constants import get_default_hermes_root
from hermes_agent.constants import get_default_hermes_root
home = get_hermes_home().resolve()
default = get_default_hermes_root().resolve()
if home == default:
@@ -582,7 +588,7 @@ def _profile_arg(hermes_home: str | None = None) -> str:
service definition for a different user (e.g. system service).
"""
import re
from hermes_constants import get_default_hermes_root
from hermes_agent.constants import get_default_hermes_root
home = Path(hermes_home or str(get_hermes_home())).resolve()
default = get_default_hermes_root().resolve()
if home == default:
@@ -696,6 +702,8 @@ _LEGACY_SERVICE_NAMES: tuple[str, ...] = ("hermes.service",)
# ExecStart content markers that identify a unit as running our gateway.
# A legacy unit is only flagged when its file contains one of these.
_LEGACY_UNIT_EXECSTART_MARKERS: tuple[str, ...] = (
"hermes_agent.cli.main gateway",
"hermes_agent/cli/main.py gateway",
"hermes_cli.main gateway",
"hermes_cli/main.py gateway",
"gateway/run.py",
@@ -1221,7 +1229,7 @@ StartLimitBurst=5
Type=simple
User={username}
Group={group_name}
ExecStart={python_path} -m hermes_cli.main{f" {profile_arg}" if profile_arg else ""} gateway run --replace
ExecStart={python_path} -m hermes_agent.cli.main{f" {profile_arg}" if profile_arg else ""} gateway run --replace
WorkingDirectory={working_dir}
Environment="HOME={home_dir}"
Environment="USER={username}"
@@ -1256,7 +1264,7 @@ StartLimitBurst=5
[Service]
Type=simple
ExecStart={python_path} -m hermes_cli.main{f" {profile_arg}" if profile_arg else ""} gateway run --replace
ExecStart={python_path} -m hermes_agent.cli.main{f" {profile_arg}" if profile_arg else ""} gateway run --replace
WorkingDirectory={working_dir}
Environment="PATH={sane_path}"
Environment="VIRTUAL_ENV={venv_dir}"
@@ -1501,7 +1509,7 @@ def systemd_restart(system: bool = False):
if system:
_require_root_for_system_service("restart")
refresh_systemd_unit_if_needed(system=system)
from gateway.status import get_running_pid
from hermes_agent.gateway.status import get_running_pid
pid = get_running_pid()
if pid is not None and _request_gateway_self_restart(pid):
@@ -1689,7 +1697,7 @@ def generate_launchd_plist() -> str:
prog_args = [
f"<string>{python_path}</string>",
"<string>-m</string>",
"<string>hermes_cli.main</string>",
"<string>hermes_agent.cli.main</string>",
]
if profile_arg:
for part in profile_arg.split():
@@ -1799,7 +1807,7 @@ def launchd_install(force: bool = False):
print()
print("Next steps:")
print(" hermes gateway status # Check status")
from hermes_constants import display_hermes_home as _dhh
from hermes_agent.constants import display_hermes_home as _dhh
print(f" tail -f {_dhh()}/logs/gateway.log # View logs")
def launchd_uninstall():
@@ -1867,7 +1875,7 @@ def _wait_for_gateway_exit(timeout: float = 10.0, force_after: float | None = 5.
force_after: Seconds of graceful waiting before escalating to force-kill.
"""
import time
from gateway.status import get_running_pid
from hermes_agent.gateway.status import get_running_pid
deadline = time.monotonic() + timeout
force_deadline = (time.monotonic() + force_after) if force_after is not None else None
@@ -1901,7 +1909,7 @@ def launchd_restart():
label = get_launchd_label()
target = f"{_launchd_domain()}/{label}"
drain_timeout = _get_restart_drain_timeout()
from gateway.status import get_running_pid
from hermes_agent.gateway.status import get_running_pid
try:
pid = get_running_pid()
@@ -1982,9 +1990,7 @@ def run_gateway(verbose: int = 0, quiet: bool = False, replace: bool = False):
This prevents systemd restart loops when the old process
hasn't fully exited yet.
"""
sys.path.insert(0, str(PROJECT_ROOT))
from gateway.run import start_gateway
from hermes_agent.gateway.run import start_gateway
print("┌─────────────────────────────────────────────────────────┐")
print("│ ⚕ Hermes Gateway Starting... │")
@@ -2430,7 +2436,7 @@ def _platform_status(platform: dict) -> str:
def _runtime_health_lines() -> list[str]:
"""Summarize the latest persisted gateway runtime health state."""
try:
from gateway.status import read_runtime_status
from hermes_agent.gateway.status import read_runtime_status
except Exception:
return []
@@ -2562,7 +2568,7 @@ def _setup_standard_platform(platform: dict):
def _setup_whatsapp():
"""Delegate to the existing WhatsApp setup flow."""
from hermes_cli.main import cmd_whatsapp
from hermes_agent.cli.main import cmd_whatsapp
import argparse
cmd_whatsapp(argparse.Namespace())
@@ -2581,7 +2587,7 @@ def _setup_sms():
def _setup_dingtalk():
"""Configure DingTalk — QR scan (recommended) or manual credential entry."""
from hermes_cli.setup import (
from hermes_agent.cli.setup_wizard import (
prompt_choice, prompt_yes_no, print_info, print_success, print_warning,
)
@@ -2612,7 +2618,7 @@ def _setup_dingtalk():
if method == 0:
# ── QR-code device-flow authorization ──
try:
from hermes_cli.dingtalk_auth import dingtalk_qr_auth
from hermes_agent.cli.auth.dingtalk import dingtalk_qr_auth
except ImportError as exc:
print_warning(f" QR auth module failed to load ({exc}), falling back to manual input.")
_setup_standard_platform(dingtalk_platform)
@@ -2644,6 +2650,12 @@ def _setup_wecom():
_setup_standard_platform(wecom_platform)
def _setup_wecom_callback():
"""Configure WeCom Callback (self-built app) via the standard platform setup."""
wecom_platform = next(p for p in _PLATFORMS if p["key"] == "wecom_callback")
_setup_standard_platform(wecom_platform)
def _is_service_installed() -> bool:
"""Check if the gateway is installed as a system service."""
if supports_systemd_services():
@@ -2714,7 +2726,7 @@ def _setup_weixin():
return
try:
from gateway.platforms.weixin import check_weixin_requirements, qr_login
from hermes_agent.gateway.platforms.weixin import check_weixin_requirements, qr_login
except Exception as exc:
print_error(f" Weixin adapter import failed: {exc}")
print_info(" Install gateway dependencies first, then retry.")
@@ -2849,7 +2861,7 @@ def _setup_feishu():
if method_idx == 0:
# ── QR scan-to-create ──
try:
from gateway.platforms.feishu import qr_register
from hermes_agent.gateway.platforms.feishu import qr_register
except Exception as exc:
print_error(f" Feishu / Lark onboard import failed: {exc}")
qr_register = None
@@ -2890,7 +2902,7 @@ def _setup_feishu():
# Try to probe the bot with manual credentials
bot_name = None
try:
from gateway.platforms.feishu import probe_bot
from hermes_agent.gateway.platforms.feishu import probe_bot
bot_info = probe_bot(app_id, app_secret, domain)
if bot_info:
bot_name = bot_info.get("bot_name")
@@ -3123,11 +3135,11 @@ def _qqbot_qr_flow():
or None on failure/cancel.
"""
try:
from gateway.platforms.qqbot import (
from hermes_agent.gateway.platforms.qqbot import (
create_bind_task, poll_bind_result, build_connect_url,
decrypt_secret, BindStatus,
)
from gateway.platforms.qqbot.constants import ONBOARD_POLL_INTERVAL
from hermes_agent.gateway.platforms.qqbot.constants import ONBOARD_POLL_INTERVAL
except Exception as exc:
print_error(f" QQBot onboard import failed: {exc}")
return None
@@ -3465,7 +3477,7 @@ def gateway_setup():
print_info(" To enable systemd: add systemd=true to /etc/wsl.conf, then 'wsl --shutdown'")
else:
if is_termux():
from hermes_constants import display_hermes_home as _dhh
from hermes_agent.constants import display_hermes_home as _dhh
print_info(" Termux does not use systemd/launchd services.")
print_info(" Run in foreground: hermes gateway run")
print_info(f" Or start it manually in the background (best effort): nohup hermes gateway run >{_dhh()}/logs/gateway.log 2>&1 &")

View File

@@ -50,8 +50,8 @@ def hooks_command(args) -> None:
# ---------------------------------------------------------------------------
def _cmd_list(_args) -> None:
from hermes_cli.config import load_config
from agent import shell_hooks
from hermes_agent.cli.config import load_config
from hermes_agent.agent import shell_hooks
specs = shell_hooks.iter_configured_hooks(load_config())
@@ -186,9 +186,9 @@ _DEFAULT_PAYLOADS = {
def _cmd_test(args) -> None:
from hermes_cli.config import load_config
from hermes_cli.plugins import VALID_HOOKS
from agent import shell_hooks
from hermes_agent.cli.config import load_config
from hermes_agent.cli.plugins import VALID_HOOKS
from hermes_agent.agent import shell_hooks
event = args.event
if event not in VALID_HOOKS:
@@ -273,7 +273,7 @@ def _truncate(s: str, n: int) -> str:
# ---------------------------------------------------------------------------
def _cmd_revoke(args) -> None:
from agent import shell_hooks
from hermes_agent.agent import shell_hooks
removed = shell_hooks.revoke(args.command)
if removed == 0:
@@ -291,8 +291,8 @@ def _cmd_revoke(args) -> None:
# ---------------------------------------------------------------------------
def _cmd_doctor(_args) -> None:
from hermes_cli.config import load_config
from agent import shell_hooks
from hermes_agent.cli.config import load_config
from hermes_agent.agent import shell_hooks
specs = shell_hooks.iter_configured_hooks(load_config())

View File

@@ -24,7 +24,7 @@ from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional, Sequence
from hermes_constants import get_hermes_home, display_hermes_home
from hermes_agent.constants import get_hermes_home, display_hermes_home
# Known log files (name → filename)
LOG_FILES = {
@@ -191,7 +191,7 @@ def tail_log(
# Resolve component to logger name prefixes
component_prefixes = None
if component:
from hermes_logging import COMPONENT_PREFIXES
from hermes_agent.logging import COMPONENT_PREFIXES
component_lower = component.lower()
if component_lower not in COMPONENT_PREFIXES:
available = ", ".join(sorted(COMPONENT_PREFIXES))

File diff suppressed because it is too large Load Diff

View File

@@ -15,15 +15,15 @@ import re
import time
from typing import Any, Dict, List, Optional, Tuple
from hermes_cli.config import (
from hermes_agent.cli.config import (
load_config,
save_config,
get_env_value,
save_env_value,
get_hermes_home, # noqa: F401 — used by test mocks
)
from hermes_cli.colors import Colors, color
from hermes_constants import display_hermes_home
from hermes_agent.cli.ui.colors import Colors, color
from hermes_agent.constants import display_hermes_home
logger = logging.getLogger(__name__)
@@ -61,7 +61,7 @@ def _confirm(question: str, default: bool = True) -> bool:
def _prompt(question: str, *, password: bool = False, default: str = "") -> str:
from hermes_cli.cli_output import prompt as _shared_prompt
from hermes_agent.cli.ui.output import prompt as _shared_prompt
return _shared_prompt(question, default=default, password=password)
@@ -165,7 +165,7 @@ def _probe_single_server(
Returns list of ``(tool_name, description)`` tuples.
Raises on connection failure.
"""
from tools.mcp_tool import (
from hermes_agent.tools.mcp.tool import (
_ensure_mcp_loop,
_run_on_mcp_loop,
_connect_server,
@@ -279,7 +279,7 @@ def cmd_mcp_add(args):
_info(f"Starting OAuth flow for '{name}'...")
oauth_ok = False
try:
from tools.mcp_oauth_manager import get_manager
from hermes_agent.tools.mcp.oauth_manager import get_manager
oauth_auth = get_manager().get_or_build_provider(name, url, None)
if oauth_auth:
server_config["auth"] = "oauth"
@@ -372,7 +372,7 @@ def cmd_mcp_add(args):
if choice in ("s", "select"):
# Interactive tool selection
from hermes_cli.curses_ui import curses_checklist
from hermes_agent.cli.ui.curses import curses_checklist
labels = [f"{t[0]}{t[1]}" for t in tools]
pre_selected = set(range(len(tools)))
@@ -432,7 +432,7 @@ def cmd_mcp_remove(args):
# any provider instance cached in the current process (e.g. from an
# earlier `hermes mcp test` in the same session) is evicted too.
try:
from tools.mcp_oauth_manager import get_manager
from hermes_agent.tools.mcp.oauth_manager import get_manager
get_manager().remove(name)
_success("Cleaned up OAuth tokens")
except Exception:
@@ -616,7 +616,7 @@ def cmd_mcp_login(args):
# Wipe both disk and in-memory cache so the next probe forces a fresh
# OAuth flow.
try:
from tools.mcp_oauth_manager import get_manager
from hermes_agent.tools.mcp.oauth_manager import get_manager
mgr = get_manager()
mgr.remove(name)
except Exception as exc:
@@ -700,7 +700,7 @@ def cmd_mcp_configure(args):
print()
# Interactive checklist
from hermes_cli.curses_ui import curses_checklist
from hermes_agent.cli.ui.curses import curses_checklist
labels = [f"{t[0]}{t[1]}" for t in all_tools]
@@ -742,7 +742,7 @@ def mcp_command(args):
action = getattr(args, "mcp_action", None)
if action == "serve":
from mcp_serve import run_mcp_server
from hermes_agent.tools.mcp.serve import run_mcp_server
run_mcp_server(verbose=getattr(args, "verbose", False))
return

View File

@@ -12,7 +12,7 @@ import os
import sys
from pathlib import Path
from hermes_constants import get_hermes_home
from hermes_agent.constants import get_hermes_home
# ---------------------------------------------------------------------------
@@ -25,7 +25,7 @@ def _curses_select(title: str, items: list[tuple[str, str]], default: int = 0) -
items: list of (label, description) tuples.
Returns selected index, or default on escape/quit.
"""
from hermes_cli.curses_ui import curses_radiolist
from hermes_agent.cli.ui.curses import curses_radiolist
# Format (label, desc) tuples into display strings
display_items = [
f"{label} {desc}" if desc else label
@@ -58,7 +58,7 @@ def _prompt(label: str, default: str | None = None, secret: bool = False) -> str
def _install_dependencies(provider_name: str) -> None:
"""Install pip dependencies declared in plugin.yaml."""
import subprocess
from plugins.memory import find_provider_dir
from hermes_agent.plugins.memory import find_provider_dir
plugin_dir = find_provider_dir(provider_name)
if not plugin_dir:
@@ -148,7 +148,7 @@ def _get_available_providers() -> list:
Returns list of (name, description, provider_instance) tuples.
"""
try:
from plugins.memory import discover_memory_providers, load_memory_provider
from hermes_agent.plugins.memory import discover_memory_providers, load_memory_provider
raw = discover_memory_providers()
except Exception:
raw = []
@@ -184,7 +184,7 @@ def _get_available_providers() -> list:
def cmd_setup_provider(provider_name: str) -> None:
"""Run memory setup for a specific provider, skipping the picker."""
from hermes_cli.config import load_config, save_config
from hermes_agent.cli.config import load_config, save_config
providers = _get_available_providers()
match = None
@@ -220,7 +220,7 @@ def cmd_setup_provider(provider_name: str) -> None:
def cmd_setup(args) -> None:
"""Interactive memory provider setup wizard."""
from hermes_cli.config import load_config, save_config
from hermes_agent.cli.config import load_config, save_config
providers = _get_available_providers()
@@ -386,7 +386,7 @@ def _write_env_vars(env_path: Path, env_writes: dict) -> None:
def cmd_status(args) -> None:
"""Show current memory provider config."""
from hermes_cli.config import load_config
from hermes_agent.cli.config import load_config
config = load_config()
mem_config = config.get("memory", {})

View File

View File

@@ -16,7 +16,7 @@ from difflib import get_close_matches
from pathlib import Path
from typing import Any, NamedTuple, Optional
from hermes_cli import __version__ as _HERMES_VERSION
from hermes_agent.cli import __version__ as _HERMES_VERSION
# Identify ourselves so endpoints fronted by Cloudflare's Browser Integrity
# Check (error 1010) don't reject the default ``Python-urllib/*`` signature.
@@ -101,7 +101,7 @@ def _codex_curated_models() -> list[str]:
This keeps the gateway /model picker in sync with the CLI `hermes model`
flow without maintaining a separate static list.
"""
from hermes_cli.codex_models import DEFAULT_CODEX_MODELS, _add_forward_compat_models
from hermes_agent.cli.models.codex import DEFAULT_CODEX_MODELS, _add_forward_compat_models
return _add_forward_compat_models(list(DEFAULT_CODEX_MODELS))
@@ -488,7 +488,7 @@ def check_nous_free_tier() -> bool:
return cached_result
try:
from hermes_cli.auth import get_provider_auth_state, resolve_nous_runtime_credentials
from hermes_agent.cli.auth.auth import get_provider_auth_state, resolve_nous_runtime_credentials
# Ensure we have a fresh token (triggers refresh if needed)
resolve_nous_runtime_credentials(min_key_ttl_seconds=60)
@@ -583,7 +583,7 @@ def fetch_nous_recommended_models(
def _resolve_nous_portal_url() -> str:
"""Best-effort lookup of the Portal base URL the user is authed against."""
try:
from hermes_cli.auth import (
from hermes_agent.cli.auth.auth import (
DEFAULT_NOUS_PORTAL_URL,
get_provider_auth_state,
)
@@ -912,7 +912,7 @@ def fetch_ai_gateway_models(
if _ai_gateway_catalog_cache is not None and not force_refresh:
return list(_ai_gateway_catalog_cache)
from hermes_constants import AI_GATEWAY_BASE_URL
from hermes_agent.constants import AI_GATEWAY_BASE_URL
fallback = list(VERCEL_AI_GATEWAY_MODELS)
preferred_ids = [mid for mid, _ in fallback]
@@ -1133,7 +1133,7 @@ def fetch_ai_gateway_pricing(
``prompt`` / ``completion``. This translates. Cache read/write field names
already match.
"""
from hermes_constants import AI_GATEWAY_BASE_URL
from hermes_agent.constants import AI_GATEWAY_BASE_URL
cache_key = AI_GATEWAY_BASE_URL.rstrip("/")
if not force_refresh and cache_key in _pricing_cache:
@@ -1180,7 +1180,7 @@ def _resolve_openrouter_api_key() -> str:
def _resolve_nous_pricing_credentials() -> tuple[str, str]:
"""Return ``(api_key, base_url)`` for Nous Portal pricing, or empty strings."""
try:
from hermes_cli.auth import resolve_nous_runtime_credentials
from hermes_agent.cli.auth.auth import resolve_nous_runtime_credentials
creds = resolve_nous_runtime_credentials()
if creds:
return (creds.get("api_key", ""), creds.get("base_url", ""))
@@ -1248,7 +1248,7 @@ def list_available_providers() -> list[dict[str, str]]:
# Check if this provider has credentials available
has_creds = False
try:
from hermes_cli.auth import get_auth_status, has_usable_secret
from hermes_agent.cli.auth.auth import get_auth_status, has_usable_secret
if pid == "custom":
custom_base_url = _get_custom_base_url() or ""
has_creds = bool(custom_base_url.strip())
@@ -1307,7 +1307,7 @@ def parse_model_input(raw: str, current_provider: str) -> tuple[str, str]:
def _get_custom_base_url() -> str:
"""Get the custom endpoint base_url from config.yaml."""
try:
from hermes_cli.config import load_config
from hermes_agent.cli.config import load_config
config = load_config()
model_cfg = config.get("model", {})
if isinstance(model_cfg, dict):
@@ -1401,7 +1401,7 @@ def detect_provider_for_model(
# credential pool, or auth store entries.
has_creds = False
try:
from hermes_cli.auth import PROVIDER_REGISTRY
from hermes_agent.cli.auth.auth import PROVIDER_REGISTRY
pconfig = PROVIDER_REGISTRY.get(direct_match)
if pconfig:
for env_var in pconfig.api_key_env_vars:
@@ -1414,7 +1414,7 @@ def detect_provider_for_model(
# Claude Code tokens, and other non-env-var credentials (#10300).
if not has_creds:
try:
from agent.credential_pool import load_pool
from hermes_agent.providers.credential_pool import load_pool
pool = load_pool(direct_match)
if pool.has_credentials():
has_creds = True
@@ -1422,7 +1422,7 @@ def detect_provider_for_model(
pass
if not has_creds:
try:
from hermes_cli.auth import _load_auth_store
from hermes_agent.cli.auth.auth import _load_auth_store
store = _load_auth_store()
if direct_match in store.get("providers", {}) or direct_match in store.get("credential_pool", {}):
has_creds = True
@@ -1572,7 +1572,7 @@ def resolve_fast_mode_overrides(model_id: Optional[str]) -> dict[str, Any] | Non
def _resolve_copilot_catalog_api_key() -> str:
"""Best-effort GitHub token for fetching the Copilot model catalog."""
try:
from hermes_cli.auth import resolve_api_key_provider_credentials
from hermes_agent.cli.auth.auth import resolve_api_key_provider_credentials
creds = resolve_api_key_provider_credentials("copilot")
return str(creds.get("api_key") or "").strip()
@@ -1590,7 +1590,7 @@ def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False)
if normalized == "openrouter":
return model_ids(force_refresh=force_refresh)
if normalized == "openai-codex":
from hermes_cli.codex_models import get_codex_model_ids
from hermes_agent.cli.models.codex import get_codex_model_ids
return get_codex_model_ids()
if normalized in {"copilot", "copilot-acp"}:
@@ -1605,7 +1605,7 @@ def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False)
if normalized == "nous":
# Try live Nous Portal /models endpoint
try:
from hermes_cli.auth import fetch_nous_models, resolve_nous_runtime_credentials
from hermes_agent.cli.auth.auth import fetch_nous_models, resolve_nous_runtime_credentials
creds = resolve_nous_runtime_credentials()
if creds:
live = fetch_nous_models(api_key=creds.get("api_key", ""), inference_base_url=creds.get("base_url", ""))
@@ -1647,7 +1647,7 @@ def _fetch_anthropic_models(timeout: float = 5.0) -> Optional[list[str]]:
Claude Code auto-discovery). Returns sorted model IDs or None.
"""
try:
from agent.anthropic_adapter import resolve_anthropic_token, _is_oauth_token
from hermes_agent.providers.anthropic_adapter import resolve_anthropic_token, _is_oauth_token
except ImportError:
return None
@@ -1658,7 +1658,7 @@ def _fetch_anthropic_models(timeout: float = 5.0) -> Optional[list[str]]:
headers: dict[str, str] = {"anthropic-version": "2023-06-01"}
if _is_oauth_token(token):
headers["Authorization"] = f"Bearer {token}"
from agent.anthropic_adapter import _COMMON_BETAS, _OAUTH_ONLY_BETAS
from hermes_agent.providers.anthropic_adapter import _COMMON_BETAS, _OAUTH_ONLY_BETAS
headers["anthropic-beta"] = ",".join(_COMMON_BETAS + _OAUTH_ONLY_BETAS)
else:
headers["x-api-key"] = token
@@ -1701,7 +1701,7 @@ def copilot_default_headers() -> dict[str, str]:
Copilot CLI send on every request.
"""
try:
from hermes_cli.copilot_auth import copilot_request_headers
from hermes_agent.cli.auth.copilot import copilot_request_headers
return copilot_request_headers(is_agent_turn=True)
except ImportError:
return {
@@ -2117,7 +2117,7 @@ def _fetch_ai_gateway_models(timeout: float = 5.0) -> Optional[list[str]]:
return None
base_url = os.getenv("AI_GATEWAY_BASE_URL", "").strip()
if not base_url:
from hermes_constants import AI_GATEWAY_BASE_URL
from hermes_agent.constants import AI_GATEWAY_BASE_URL
base_url = AI_GATEWAY_BASE_URL
url = base_url.rstrip("/") + "/models"
@@ -2161,7 +2161,7 @@ _OLLAMA_CLOUD_CACHE_TTL = 3600 # 1 hour
def _ollama_cloud_cache_path() -> Path:
"""Return the path for the Ollama Cloud model cache."""
from hermes_constants import get_hermes_home
from hermes_agent.constants import get_hermes_home
return get_hermes_home() / "ollama_cloud_models_cache.json"
@@ -2195,7 +2195,7 @@ def _load_ollama_cloud_cache(*, ignore_ttl: bool = False) -> Optional[dict]:
def _save_ollama_cloud_cache(models: list[str]) -> None:
"""Persist the merged Ollama Cloud model list to disk."""
try:
from utils import atomic_json_write
from hermes_agent.utils import atomic_json_write
cache_path = _ollama_cloud_cache_path()
cache_path.parent.mkdir(parents=True, exist_ok=True)
atomic_json_write(cache_path, {"models": models, "cached_at": time.time()}, indent=None)
@@ -2240,7 +2240,7 @@ def fetch_ollama_cloud_models(
# 3. models.dev registry
mdev_models: list[str] = []
try:
from agent.models_dev import list_agentic_models
from hermes_agent.providers.metadata_dev import list_agentic_models
mdev_models = list_agentic_models("ollama-cloud")
except Exception:
pass
@@ -2510,7 +2510,7 @@ def validate_requested_model(
# AWS SDK control plane (ListFoundationModels + ListInferenceProfiles).
if normalized == "bedrock":
try:
from agent.bedrock_adapter import discover_bedrock_models, resolve_bedrock_region
from hermes_agent.providers.bedrock_adapter import discover_bedrock_models, resolve_bedrock_region
region = resolve_bedrock_region()
discovered = discover_bedrock_models(region)
discovered_ids = {m["id"] for m in discovered}

View File

@@ -184,7 +184,7 @@ def _normalize_provider_alias(provider_name: str) -> str:
if not raw:
return raw
try:
from hermes_cli.models import normalize_provider
from hermes_agent.cli.models.models import normalize_provider
return normalize_provider(raw)
except Exception:
@@ -382,7 +382,7 @@ def normalize_model_for_provider(model_input: str, target_provider: str) -> str:
# HTTP 400 "model_not_supported". See issue #6879.
if provider in {"copilot", "copilot-acp"}:
try:
from hermes_cli.models import normalize_copilot_model_id
from hermes_agent.cli.models.models import normalize_copilot_model_id
normalized = normalize_copilot_model_id(name)
if normalized:

View File

@@ -25,17 +25,17 @@ import re
from dataclasses import dataclass
from typing import List, NamedTuple, Optional
from hermes_cli.providers import (
from hermes_agent.cli.providers import (
custom_provider_slug,
determine_api_mode,
get_label,
is_aggregator,
resolve_provider_full,
)
from hermes_cli.model_normalize import (
from hermes_agent.cli.models.normalize import (
normalize_model_for_provider,
)
from agent.models_dev import (
from hermes_agent.providers.metadata_dev import (
ModelCapabilities,
ModelInfo,
get_model_capabilities,
@@ -193,7 +193,7 @@ def _load_direct_aliases() -> dict[str, DirectAlias]:
"""
merged = dict(_BUILTIN_DIRECT_ALIASES)
try:
from hermes_cli.config import load_config
from hermes_agent.cli.config import load_config
cfg = load_config()
user_aliases = cfg.get("model_aliases")
if isinstance(user_aliases, dict):
@@ -456,13 +456,13 @@ def switch_model(
Returns:
ModelSwitchResult with all information the caller needs.
"""
from hermes_cli.models import (
from hermes_agent.cli.models.models import (
copilot_model_api_mode,
detect_provider_for_model,
validate_requested_model,
opencode_model_api_mode,
)
from hermes_cli.runtime_provider import resolve_runtime_provider
from hermes_agent.cli.runtime_provider import resolve_runtime_provider
resolved_alias = ""
new_model = raw_input.strip()
@@ -486,7 +486,7 @@ def switch_model(
)
# Check for common config issues that cause provider resolution failures
try:
from hermes_cli.config import validate_config_structure
from hermes_agent.cli.config import validate_config_structure
_cfg_issues = validate_config_structure()
if _cfg_issues:
_switch_err += "\n\nRun 'hermes doctor' — config issues detected:"
@@ -505,7 +505,7 @@ def switch_model(
# If no model specified, try auto-detect from endpoint
if not new_model:
if pdef.base_url:
from hermes_cli.runtime_provider import _auto_detect_local_model
from hermes_agent.cli.runtime_provider import _auto_detect_local_model
detected = _auto_detect_local_model(pdef.base_url)
if detected:
new_model = detected
@@ -804,13 +804,13 @@ def list_authenticated_providers(
Only includes providers that have API keys set or are user-defined endpoints.
"""
import os
from agent.models_dev import (
from hermes_agent.providers.metadata_dev import (
PROVIDER_TO_MODELS_DEV,
fetch_models_dev,
get_provider_info as _mdev_pinfo,
)
from hermes_cli.auth import PROVIDER_REGISTRY
from hermes_cli.models import OPENROUTER_MODELS, _PROVIDER_MODELS
from hermes_agent.cli.auth.auth import PROVIDER_REGISTRY
from hermes_agent.cli.models.models import OPENROUTER_MODELS, _PROVIDER_MODELS
results: List[dict] = []
seen_slugs: set = set() # lowercase-normalized to catch case variants (#9545)
@@ -826,7 +826,7 @@ def list_authenticated_providers(
curated["nous"] = curated["openrouter"]
# Ollama Cloud uses dynamic discovery (no static curated list)
if "ollama-cloud" not in curated:
from hermes_cli.models import fetch_ollama_cloud_models
from hermes_agent.cli.models.models import fetch_ollama_cloud_models
curated["ollama-cloud"] = fetch_ollama_cloud_models()
# --- 1. Check Hermes-mapped providers ---
@@ -878,8 +878,8 @@ def list_authenticated_providers(
seen_mdev_ids.add(mdev_id)
# --- 2. Check Hermes-only providers (nous, openai-codex, copilot, opencode-go) ---
from hermes_cli.providers import HERMES_OVERLAYS
from hermes_cli.auth import PROVIDER_REGISTRY as _auth_registry
from hermes_agent.cli.providers import HERMES_OVERLAYS
from hermes_agent.cli.auth.auth import PROVIDER_REGISTRY as _auth_registry
# Build reverse mapping: models.dev ID → Hermes provider ID.
# HERMES_OVERLAYS keys may be models.dev IDs (e.g. "github-copilot")
@@ -913,7 +913,7 @@ def list_authenticated_providers(
# OAuth via external credential files).
if not has_creds:
try:
from hermes_cli.auth import _load_auth_store
from hermes_agent.cli.auth.auth import _load_auth_store
store = _load_auth_store()
providers_store = store.get("providers", {})
pool_store = store.get("credential_pool", {})
@@ -930,7 +930,7 @@ def list_authenticated_providers(
# imports on demand but aren't in the raw auth.json yet.
if not has_creds:
try:
from agent.credential_pool import load_pool
from hermes_agent.providers.credential_pool import load_pool
pool = load_pool(hermes_slug)
if pool.has_credentials():
has_creds = True
@@ -945,7 +945,7 @@ def list_authenticated_providers(
# configured.
if not has_creds and hermes_slug == "anthropic":
try:
from agent.anthropic_adapter import (
from hermes_agent.providers.anthropic_adapter import (
read_claude_code_credentials,
read_hermes_oauth_credentials,
)
@@ -981,7 +981,7 @@ def list_authenticated_providers(
# in PROVIDER_TO_MODELS_DEV or HERMES_OVERLAYS (keeps /model in sync
# with `hermes model`).
try:
from hermes_cli.models import CANONICAL_PROVIDERS as _canon_provs
from hermes_agent.cli.models.models import CANONICAL_PROVIDERS as _canon_provs
except ImportError:
_canon_provs = []
@@ -997,7 +997,7 @@ def list_authenticated_providers(
# Also check auth store and credential pool
if not _cp_has_creds:
try:
from hermes_cli.auth import _load_auth_store
from hermes_agent.cli.auth.auth import _load_auth_store
_cp_store = _load_auth_store()
_cp_providers_store = _cp_store.get("providers", {})
_cp_pool_store = _cp_store.get("credential_pool", {})
@@ -1010,7 +1010,7 @@ def list_authenticated_providers(
pass
if not _cp_has_creds:
try:
from agent.credential_pool import load_pool
from hermes_agent.providers.credential_pool import load_pool
_cp_pool = load_pool(_cp.slug)
if _cp_pool.has_credentials():
_cp_has_creds = True

View File

@@ -6,10 +6,10 @@ from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Iterable, Optional, Set
from hermes_cli.auth import get_nous_auth_status
from hermes_cli.config import get_env_value, load_config
from tools.managed_tool_gateway import is_managed_tool_gateway_ready
from tools.tool_backend_helpers import (
from hermes_agent.cli.auth.auth import get_nous_auth_status
from hermes_agent.cli.config import get_env_value, load_config
from hermes_agent.tools.managed_gateway import is_managed_tool_gateway_ready
from hermes_agent.tools.backend_helpers import (
fal_key_is_configured,
has_direct_modal_credentials,
managed_nous_tools_enabled,
@@ -82,7 +82,7 @@ def _model_config_dict(config: Dict[str, object]) -> Dict[str, object]:
def _toolset_enabled(config: Dict[str, object], toolset_key: str) -> bool:
from toolsets import resolve_toolset
from hermes_agent.tools.toolsets import resolve_toolset
platform_toolsets = config.get("platform_toolsets")
if not isinstance(platform_toolsets, dict) or not platform_toolsets:
@@ -123,7 +123,7 @@ def _has_agent_browser() -> bool:
agent_browser_bin = shutil.which("agent-browser")
local_bin = (
Path(__file__).parent.parent / "node_modules" / ".bin" / "agent-browser"
Path(__file__).resolve().parents[2] / "node_modules" / ".bin" / "agent-browser"
)
return bool(agent_browser_bin or local_bin.exists())
@@ -688,7 +688,7 @@ def prompt_enable_tool_gateway(config: Dict[str, object]) -> set[str]:
return set()
try:
from hermes_cli.setup import prompt_choice
from hermes_agent.cli.setup_wizard import prompt_choice
except Exception:
return set()
@@ -766,7 +766,7 @@ def prompt_enable_tool_gateway(config: Dict[str, object]) -> set[str]:
changed = apply_gateway_defaults(config, to_apply)
if changed:
from hermes_cli.config import save_config
from hermes_agent.cli.config import save_config
save_config(config)
# Only report the tools that actually switched (not already-managed ones)
newly_switched = changed - set(already_managed)

View File

@@ -10,7 +10,7 @@ Usage:
def pairing_command(args):
"""Handle hermes pairing subcommands."""
from gateway.pairing import PairingStore
from hermes_agent.gateway.pairing import PairingStore
store = PairingStore()
action = getattr(args, "pairing_action", None)

View File

@@ -43,8 +43,8 @@ from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Set, Union
from hermes_constants import get_hermes_home
from utils import env_var_enabled
from hermes_agent.constants import get_hermes_home
from hermes_agent.utils import env_var_enabled
try:
import yaml
@@ -73,7 +73,7 @@ VALID_HOOKS: Set[str] = {
"subagent_stop",
}
ENTRY_POINTS_GROUP = "hermes_agent.plugins"
ENTRY_POINTS_GROUP = "plugins"
_NS_PARENT = "hermes_plugins"
@@ -91,7 +91,7 @@ def _get_disabled_plugins() -> set:
``plugins.enabled``.
"""
try:
from hermes_cli.config import load_config
from hermes_agent.cli.config import load_config
config = load_config()
disabled = config.get("plugins", {}).get("disabled", [])
return set(disabled) if isinstance(disabled, list) else set()
@@ -114,7 +114,7 @@ def _get_enabled_plugins() -> Optional[set]:
* ``set(...)`` the concrete allow-list.
"""
try:
from hermes_cli.config import load_config
from hermes_agent.cli.config import load_config
config = load_config()
plugins_cfg = config.get("plugins")
if not isinstance(plugins_cfg, dict):
@@ -207,7 +207,7 @@ class PluginContext:
emoji: str = "",
) -> None:
"""Register a tool in the global registry **and** track it as plugin-provided."""
from tools.registry import registry
from hermes_agent.tools.registry import registry
registry.register(
name=name,
@@ -305,7 +305,7 @@ class PluginContext:
# Reject if it conflicts with a built-in command
try:
from hermes_cli.commands import resolve_command
from hermes_agent.cli.commands import resolve_command
if resolve_command(clean) is not None:
logger.warning(
"Plugin '%s' tried to register command '/%s' which conflicts "
@@ -341,7 +341,7 @@ class PluginContext:
Returns:
JSON string from the tool handler (same format as model tool calls).
"""
from tools.registry import registry
from hermes_agent.tools.registry import registry
# Wire up parent agent context when available (CLI mode).
# In gateway mode _cli_ref is None — tools degrade gracefully
@@ -372,7 +372,7 @@ class PluginContext:
)
return
# Defer the import to avoid circular deps at module level
from agent.context_engine import ContextEngine
from hermes_agent.agent.context.engine import ContextEngine
if not isinstance(engine, ContextEngine):
logger.warning(
"Plugin '%s' tried to register a context engine that does not "
@@ -397,8 +397,8 @@ class PluginContext:
``config.yaml`` matches against when routing ``image_generate``
tool calls.
"""
from agent.image_gen_provider import ImageGenProvider
from agent.image_gen_registry import register_provider
from hermes_agent.agent.image_gen.provider import ImageGenProvider
from hermes_agent.agent.image_gen.registry import register_provider
if not isinstance(provider, ImageGenProvider):
logger.warning(
@@ -452,7 +452,7 @@ class PluginContext:
ValueError: if *name* contains ``':'`` or invalid characters.
FileNotFoundError: if *path* does not exist.
"""
from agent.skill_utils import _NAMESPACE_RE
from hermes_agent.agent.skill_utils import _NAMESPACE_RE
if ":" in name:
raise ValueError(
@@ -1087,7 +1087,7 @@ def get_plugin_toolsets() -> List[tuple]:
return []
try:
from tools.registry import registry
from hermes_agent.tools.registry import registry
except Exception:
return []

View File

@@ -17,7 +17,7 @@ import sys
from pathlib import Path
from typing import Optional
from hermes_constants import get_hermes_home
from hermes_agent.constants import get_hermes_home
logger = logging.getLogger(__name__)
@@ -173,8 +173,8 @@ def _prompt_plugin_env_vars(manifest: dict, console) -> None:
if not requires_env:
return
from hermes_cli.config import get_env_value, save_env_value # noqa: F811
from hermes_constants import display_hermes_home
from hermes_agent.cli.config import get_env_value, save_env_value # noqa: F811
from hermes_agent.constants import display_hermes_home
# Normalise to list-of-dicts
env_specs: list[dict] = []
@@ -360,7 +360,7 @@ def cmd_install(
)
sys.exit(1)
if mv_int > _SUPPORTED_MANIFEST_VERSION:
from hermes_cli.config import recommended_update_command
from hermes_agent.cli.config import recommended_update_command
console.print(
f"[red]Error:[/red] Plugin '{plugin_name}' requires manifest_version "
f"{mv}, but this installer only supports up to {_SUPPORTED_MANIFEST_VERSION}.\n"
@@ -517,7 +517,7 @@ def _get_disabled_set() -> set:
listed in ``plugins.enabled``.
"""
try:
from hermes_cli.config import load_config
from hermes_agent.cli.config import load_config
config = load_config()
disabled = config.get("plugins", {}).get("disabled", [])
return set(disabled) if isinstance(disabled, list) else set()
@@ -527,7 +527,7 @@ def _get_disabled_set() -> set:
def _save_disabled_set(disabled: set) -> None:
"""Write the disabled plugins list to config.yaml."""
from hermes_cli.config import load_config, save_config
from hermes_agent.cli.config import load_config, save_config
config = load_config()
if "plugins" not in config:
config["plugins"] = {}
@@ -542,7 +542,7 @@ def _get_enabled_set() -> set:
the key is missing (same behaviour as "nothing enabled yet").
"""
try:
from hermes_cli.config import load_config
from hermes_agent.cli.config import load_config
config = load_config()
plugins_cfg = config.get("plugins", {})
if not isinstance(plugins_cfg, dict):
@@ -555,7 +555,7 @@ def _get_enabled_set() -> set:
def _save_enabled_set(enabled: set) -> None:
"""Write the enabled plugins list to config.yaml."""
from hermes_cli.config import load_config, save_config
from hermes_agent.cli.config import load_config, save_config
config = load_config()
if "plugins" not in config:
config["plugins"] = {}
@@ -631,8 +631,8 @@ def _plugin_exists(name: str) -> bool:
return True
# Bundled: <repo>/plugins/<name>/
from pathlib import Path as _P
import hermes_cli
repo_plugins = _P(hermes_cli.__file__).resolve().parent.parent / "plugins"
import hermes_agent.cli as _cli_pkg
repo_plugins = _P(_cli_pkg.__file__).resolve().parent.parent / "plugins"
if repo_plugins.is_dir():
candidate = repo_plugins / name
if candidate.is_dir() and (
@@ -659,8 +659,8 @@ def _discover_all_plugins() -> list:
seen: dict = {} # name -> (name, version, description, source, path)
# Bundled (<repo>/plugins/<name>/), excluding memory/ and context_engine/
import hermes_cli
repo_plugins = Path(hermes_cli.__file__).resolve().parent.parent / "plugins"
import hermes_agent.cli as _cli_pkg
repo_plugins = Path(_cli_pkg.__file__).resolve().parent.parent / "plugins"
for base, source in ((repo_plugins, "bundled"), (_plugins_dir(), "user")):
if not base.is_dir():
continue
@@ -743,7 +743,7 @@ def cmd_list() -> None:
def _discover_memory_providers() -> list[tuple[str, str]]:
"""Return [(name, description), ...] for available memory providers."""
try:
from plugins.memory import discover_memory_providers
from hermes_agent.plugins.memory import discover_memory_providers
return [(name, desc) for name, desc, _avail in discover_memory_providers()]
except Exception:
return []
@@ -752,7 +752,7 @@ def _discover_memory_providers() -> list[tuple[str, str]]:
def _discover_context_engines() -> list[tuple[str, str]]:
"""Return [(name, description), ...] for available context engines."""
try:
from plugins.context_engine import discover_context_engines
from hermes_agent.plugins.context_engine import discover_context_engines
return [(name, desc) for name, desc, _avail in discover_context_engines()]
except Exception:
return []
@@ -761,7 +761,7 @@ def _discover_context_engines() -> list[tuple[str, str]]:
def _get_current_memory_provider() -> str:
"""Return the current memory.provider from config (empty = built-in)."""
try:
from hermes_cli.config import load_config
from hermes_agent.cli.config import load_config
config = load_config()
return config.get("memory", {}).get("provider", "") or ""
except Exception:
@@ -771,7 +771,7 @@ def _get_current_memory_provider() -> str:
def _get_current_context_engine() -> str:
"""Return the current context.engine from config."""
try:
from hermes_cli.config import load_config
from hermes_agent.cli.config import load_config
config = load_config()
return config.get("context", {}).get("engine", "compressor") or "compressor"
except Exception:
@@ -780,7 +780,7 @@ def _get_current_context_engine() -> str:
def _save_memory_provider(name: str) -> None:
"""Persist memory.provider to config.yaml."""
from hermes_cli.config import load_config, save_config
from hermes_agent.cli.config import load_config, save_config
config = load_config()
if "memory" not in config:
config["memory"] = {}
@@ -790,7 +790,7 @@ def _save_memory_provider(name: str) -> None:
def _save_context_engine(name: str) -> None:
"""Persist context.engine to config.yaml."""
from hermes_cli.config import load_config, save_config
from hermes_agent.cli.config import load_config, save_config
config = load_config()
if "context" not in config:
config["context"] = {}
@@ -800,7 +800,7 @@ def _save_context_engine(name: str) -> None:
def _configure_memory_provider() -> bool:
"""Launch a radio picker for memory providers. Returns True if changed."""
from hermes_cli.curses_ui import curses_radiolist
from hermes_agent.cli.ui.curses import curses_radiolist
current = _get_current_memory_provider()
providers = _discover_memory_providers()
@@ -838,7 +838,7 @@ def _configure_memory_provider() -> bool:
def _configure_context_engine() -> bool:
"""Launch a radio picker for context engines. Returns True if changed."""
from hermes_cli.curses_ui import curses_radiolist
from hermes_agent.cli.ui.curses import curses_radiolist
current = _get_current_context_engine()
engines = _discover_context_engines()
@@ -938,7 +938,7 @@ def cmd_toggle() -> None:
def _run_composite_ui(curses, plugin_names, plugin_labels, plugin_selected,
disabled, categories, console):
"""Custom curses screen with checkboxes + category action rows."""
from hermes_cli.curses_ui import flush_stdin
from hermes_agent.cli.ui.curses import flush_stdin
chosen = set(plugin_selected)
n_plugins = len(plugin_names)
@@ -1188,7 +1188,7 @@ def _run_composite_ui(curses, plugin_names, plugin_labels, plugin_selected,
def _run_composite_fallback(plugin_names, plugin_labels, plugin_selected,
disabled, categories, console):
"""Text-based fallback for the composite plugins UI."""
from hermes_cli.colors import Colors, color
from hermes_agent.cli.ui.colors import Colors, color
print(color("\n Plugins", Colors.YELLOW))

View File

@@ -84,7 +84,7 @@ _DEFAULT_EXPORT_EXCLUDE_ROOT = frozenset({
"node_modules", # npm packages
# Databases & runtime state
"state.db", "state.db-shm", "state.db-wal",
"hermes_state.db",
"state.db",
"response_store.db", "response_store.db-shm", "response_store.db-wal",
"gateway.pid", "gateway_state.json", "processes.json",
"auth.json", # API keys, OAuth tokens, credential pools
@@ -138,7 +138,7 @@ def _get_default_hermes_home() -> Path:
In Docker/custom deployments where HERMES_HOME is outside ``~/.hermes``
(e.g. ``/opt/data``), returns HERMES_HOME directly.
"""
from hermes_constants import get_default_hermes_root
from hermes_agent.constants import get_default_hermes_root
return get_default_hermes_root()
@@ -301,7 +301,7 @@ def _read_config_model(profile_dir: Path) -> tuple:
def _check_gateway_running(profile_dir: Path) -> bool:
"""Check if a gateway is running for a given profile directory."""
try:
from gateway.status import get_running_pid
from hermes_agent.gateway.status import get_running_pid
return get_running_pid(profile_dir / "gateway.pid", cleanup_stale=False) is not None
except Exception:
return False
@@ -413,7 +413,7 @@ def create_profile(
if clone_from is not None or clone_all or clone_config:
if clone_from is None:
# Default: clone from active profile
from hermes_constants import get_hermes_home
from hermes_agent.constants import get_hermes_home
source_dir = get_hermes_home()
else:
validate_profile_name(clone_from)
@@ -455,7 +455,7 @@ def create_profile(
soul_path = profile_dir / "SOUL.md"
if not soul_path.exists():
try:
from hermes_cli.default_soul import DEFAULT_SOUL_MD
from hermes_agent.cli.default_soul import DEFAULT_SOUL_MD
soul_path.write_text(DEFAULT_SOUL_MD, encoding="utf-8")
except Exception:
pass # best-effort — don't fail profile creation over this
@@ -469,11 +469,11 @@ def seed_profile_skills(profile_dir: Path, quiet: bool = False) -> Optional[dict
Uses subprocess because sync_skills() caches HERMES_HOME at module level.
Returns the sync result dict, or None on failure.
"""
project_root = Path(__file__).parent.parent.resolve()
project_root = Path(__file__).resolve().parents[2].resolve()
try:
result = subprocess.run(
[sys.executable, "-c",
"import json; from tools.skills_sync import sync_skills; "
"import json; from hermes_agent.tools.skills.sync import sync_skills; "
"r = sync_skills(quiet=True); print(json.dumps(r))"],
env={**os.environ, "HERMES_HOME": str(profile_dir)},
cwd=str(project_root),
@@ -597,7 +597,7 @@ def _cleanup_gateway_service(name: str, profile_dir: Path) -> None:
old_home = os.environ.get("HERMES_HOME")
try:
os.environ["HERMES_HOME"] = str(profile_dir)
from hermes_cli.gateway import get_service_name, get_launchd_plist_path
from hermes_agent.cli.gateway import get_service_name, get_launchd_plist_path
if _platform.system() == "Linux":
svc_name = get_service_name()
@@ -720,7 +720,7 @@ def get_active_profile_name() -> str:
Returns the profile name if HERMES_HOME points into ``~/.hermes/profiles/<name>``.
Returns ``"custom"`` if HERMES_HOME is set to an unrecognized path.
"""
from hermes_constants import get_hermes_home
from hermes_agent.constants import get_hermes_home
hermes_home = get_hermes_home()
resolved = hermes_home.resolve()

View File

@@ -23,7 +23,7 @@ import logging
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple
from utils import base_url_host_matches, base_url_hostname
from hermes_agent.utils import base_url_host_matches, base_url_hostname
logger = logging.getLogger(__name__)
@@ -341,7 +341,7 @@ def get_provider(name: str) -> Optional[ProviderDef]:
# Try to get models.dev data
try:
from agent.models_dev import get_provider_info as _mdev_provider
from hermes_agent.providers.metadata_dev import get_provider_info as _mdev_provider
mdev_info = _mdev_provider(canonical)
except Exception:
mdev_info = None
@@ -596,7 +596,7 @@ def resolve_provider_full(
# 3. Try models.dev directly (for providers not in our ALIASES)
try:
from agent.models_dev import get_provider_info as _mdev_provider
from hermes_agent.providers.metadata_dev import get_provider_info as _mdev_provider
mdev_info = _mdev_provider(canonical)
if mdev_info is not None:
return ProviderDef(

File diff suppressed because it is too large Load Diff

View File

@@ -9,9 +9,9 @@ from typing import Any, Dict, Optional
logger = logging.getLogger(__name__)
from hermes_cli import auth as auth_mod
from agent.credential_pool import CredentialPool, PooledCredential, get_custom_provider_pool_key, load_pool
from hermes_cli.auth import (
from hermes_agent.cli.auth import auth as auth_mod
from hermes_agent.providers.credential_pool import CredentialPool, PooledCredential, get_custom_provider_pool_key, load_pool
from hermes_agent.cli.auth.auth import (
AuthError,
DEFAULT_CODEX_BASE_URL,
DEFAULT_QWEN_BASE_URL,
@@ -27,9 +27,9 @@ from hermes_cli.auth import (
resolve_external_process_provider_credentials,
has_usable_secret,
)
from hermes_cli.config import get_compatible_custom_providers, load_config
from hermes_constants import OPENROUTER_BASE_URL
from utils import base_url_host_matches, base_url_hostname
from hermes_agent.cli.config import get_compatible_custom_providers, load_config
from hermes_agent.constants import OPENROUTER_BASE_URL
from hermes_agent.utils import base_url_host_matches, base_url_hostname
def _normalize_custom_provider_name(value: str) -> str:
@@ -134,7 +134,7 @@ def _copilot_runtime_api_mode(model_cfg: Dict[str, Any], api_key: str) -> str:
return "chat_completions"
try:
from hermes_cli.models import copilot_model_api_mode
from hermes_agent.cli.models.models import copilot_model_api_mode
return copilot_model_api_mode(model_name, api_key=api_key)
except Exception:
@@ -206,7 +206,7 @@ def _resolve_runtime_from_pool_entry(
if configured_mode and _provider_supports_explicit_api_mode(provider, configured_provider):
api_mode = configured_mode
elif provider in ("opencode-zen", "opencode-go"):
from hermes_cli.models import opencode_model_api_mode
from hermes_agent.cli.models.models import opencode_model_api_mode
api_mode = opencode_model_api_mode(provider, model_cfg.get("default", ""))
else:
# Auto-detect Anthropic-compatible endpoints (/anthropic suffix,
@@ -567,7 +567,7 @@ def _resolve_explicit_runtime(
base_url = explicit_base_url or cfg_base_url or "https://api.anthropic.com"
api_key = explicit_api_key
if not api_key:
from agent.anthropic_adapter import resolve_anthropic_token
from hermes_agent.providers.anthropic_adapter import resolve_anthropic_token
api_key = resolve_anthropic_token()
if not api_key:
@@ -870,7 +870,7 @@ def resolve_runtime_provider(
# Anthropic (native Messages API)
if provider == "anthropic":
from agent.anthropic_adapter import resolve_anthropic_token
from hermes_agent.providers.anthropic_adapter import resolve_anthropic_token
token = resolve_anthropic_token()
if not token:
raise AuthError(
@@ -896,7 +896,7 @@ def resolve_runtime_provider(
# AWS Bedrock (native Converse API via boto3)
if provider == "bedrock":
from agent.bedrock_adapter import (
from hermes_agent.providers.bedrock_adapter import (
has_aws_credentials,
resolve_aws_auth_env_var,
resolve_bedrock_region,
@@ -989,7 +989,7 @@ def resolve_runtime_provider(
if configured_mode and _provider_supports_explicit_api_mode(provider, configured_provider):
api_mode = configured_mode
elif provider in ("opencode-zen", "opencode-go"):
from hermes_cli.models import opencode_model_api_mode
from hermes_agent.cli.models.models import opencode_model_api_mode
api_mode = opencode_model_api_mode(provider, model_cfg.get("default", ""))
else:
# Auto-detect Anthropic-compatible endpoints by URL convention

View File

@@ -20,14 +20,14 @@ import copy
from pathlib import Path
from typing import Optional, Dict, Any
from hermes_cli.nous_subscription import get_nous_subscription_features
from tools.tool_backend_helpers import managed_nous_tools_enabled
from utils import base_url_hostname
from hermes_constants import get_optional_skills_dir
from hermes_agent.cli.nous_subscription import get_nous_subscription_features
from hermes_agent.tools.backend_helpers import managed_nous_tools_enabled
from hermes_agent.utils import base_url_hostname
from hermes_agent.constants import get_optional_skills_dir
logger = logging.getLogger(__name__)
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
PROJECT_ROOT = Path(__file__).resolve().parents[2].resolve()
_DOCS_BASE = "https://hermes-agent.nousresearch.com/docs"
@@ -59,7 +59,7 @@ def _supports_same_provider_pool_setup(provider: str) -> bool:
return False
if provider == "openrouter":
return True
from hermes_cli.auth import PROVIDER_REGISTRY
from hermes_agent.cli.auth.auth import PROVIDER_REGISTRY
pconfig = PROVIDER_REGISTRY.get(provider)
if not pconfig:
@@ -129,7 +129,7 @@ def _set_reasoning_effort(config: Dict[str, Any], effort: str) -> None:
# Import config helpers
from hermes_cli.config import (
from hermes_agent.cli.config import (
DEFAULT_CONFIG,
get_hermes_home,
get_config_path,
@@ -142,7 +142,7 @@ from hermes_cli.config import (
)
# display_hermes_home imported lazily at call sites (stale-module safety during hermes update)
from hermes_cli.colors import Colors, color
from hermes_agent.cli.ui.colors import Colors, color
def print_header(title: str):
@@ -151,7 +151,7 @@ def print_header(title: str):
print(color(f"{title}", Colors.CYAN, Colors.BOLD))
from hermes_cli.cli_output import ( # noqa: E402
from hermes_agent.cli.ui.output import ( # noqa: E402
print_error,
print_info,
print_success,
@@ -212,7 +212,7 @@ def prompt(question: str, default: str = None, password: bool = False) -> str:
def _curses_prompt_choice(question: str, choices: list, default: int = 0, description: str | None = None) -> int:
"""Single-select menu using curses. Delegates to curses_radiolist."""
from hermes_cli.curses_ui import curses_radiolist
from hermes_agent.cli.ui.curses import curses_radiolist
return curses_radiolist(question, choices, selected=default, cancel_returns=-1, description=description)
@@ -302,7 +302,7 @@ def prompt_checklist(title: str, items: list, pre_selected: list = None) -> list
if pre_selected is None:
pre_selected = []
from hermes_cli.curses_ui import curses_checklist
from hermes_agent.cli.ui.curses import curses_checklist
chosen = curses_checklist(
title,
@@ -352,7 +352,7 @@ def _print_setup_summary(config: dict, hermes_home):
# Vision — use the same runtime resolver as the actual vision tools
try:
from agent.auxiliary_client import get_available_vision_backends
from hermes_agent.providers.auxiliary import get_available_vision_backends
_vision_backends = get_available_vision_backends()
except Exception:
@@ -419,8 +419,8 @@ def _print_setup_summary(config: dict, hermes_home):
# setups don't show as "missing FAL_KEY".
_img_backend = None
try:
from agent.image_gen_registry import list_providers
from hermes_cli.plugins import _ensure_plugins_discovered
from hermes_agent.agent.image_gen.registry import list_providers
from hermes_agent.cli.plugins import _ensure_plugins_discovered
_ensure_plugins_discovered()
for _p in list_providers():
@@ -536,7 +536,7 @@ def _print_setup_summary(config: dict, hermes_home):
print_warning(
"Some tools are disabled. Run 'hermes setup tools' to configure them,"
)
from hermes_constants import display_hermes_home as _dhh
from hermes_agent.constants import display_hermes_home as _dhh
print_warning(f"or edit {_dhh()}/.env directly to add the missing API keys.")
print()
@@ -560,7 +560,7 @@ def _print_setup_summary(config: dict, hermes_home):
print()
# Show file locations prominently
from hermes_constants import display_hermes_home as _dhh
from hermes_agent.constants import display_hermes_home as _dhh
print(color(f"📁 All your files are in {_dhh()}/:", Colors.CYAN, Colors.BOLD))
print()
print(f" {color('Settings:', Colors.YELLOW)} {get_config_path()}")
@@ -665,7 +665,7 @@ def setup_model_provider(config: dict, *, quick: bool = False):
When *quick* is True, skips credential rotation, vision, and TTS
configuration used by the streamlined first-time quick setup.
"""
from hermes_cli.config import load_config, save_config
from hermes_agent.cli.config import load_config, save_config
print_header("Inference Provider")
print_info("Choose how to connect to your main chat model.")
@@ -674,7 +674,7 @@ def setup_model_provider(config: dict, *, quick: bool = False):
# Delegate to the shared hermes model flow — handles provider picker,
# credential prompting, model selection, and config persistence.
from hermes_cli.main import select_provider_and_model
from hermes_agent.cli.main import select_provider_and_model
try:
select_provider_and_model()
except (SystemExit, KeyboardInterrupt):
@@ -708,8 +708,8 @@ def setup_model_provider(config: dict, *, quick: bool = False):
if not quick and _supports_same_provider_pool_setup(selected_provider):
try:
from types import SimpleNamespace
from agent.credential_pool import load_pool
from hermes_cli.auth_commands import auth_add_command
from hermes_agent.providers.credential_pool import load_pool
from hermes_agent.cli.auth.commands import auth_add_command
pool = load_pool(selected_provider)
entries = pool.entries()
@@ -786,7 +786,7 @@ def setup_model_provider(config: dict, *, quick: bool = False):
_vision_needs_setup = False
else:
try:
from agent.auxiliary_client import get_available_vision_backends
from hermes_agent.providers.auxiliary import get_available_vision_backends
_vision_backends = set(get_available_vision_backends())
except Exception:
_vision_backends = set()
@@ -1075,7 +1075,7 @@ def _setup_tts_provider(config: dict):
save_env_value("XAI_API_KEY", api_key)
print_success("xAI TTS API key saved")
else:
from hermes_constants import display_hermes_home as _dhh
from hermes_agent.constants import display_hermes_home as _dhh
print_warning(
"No xAI API key provided for TTS. Configure XAI_API_KEY via "
f"hermes setup model or {_dhh()}/.env to use xAI TTS. "
@@ -1284,8 +1284,8 @@ def setup_terminal_backend(config: dict):
elif selected_backend == "modal":
print_success("Terminal backend: Modal")
print_info("Serverless cloud sandboxes. Each session gets its own container.")
from tools.managed_tool_gateway import is_managed_tool_gateway_ready
from tools.tool_backend_helpers import normalize_modal_mode
from hermes_agent.tools.managed_gateway import is_managed_tool_gateway_ready
from hermes_agent.tools.backend_helpers import normalize_modal_mode
managed_modal_available = bool(
managed_nous_tools_enabled()
@@ -2040,49 +2040,49 @@ def _setup_whatsapp():
def _setup_weixin():
"""Configure Weixin (personal WeChat) via iLink Bot API QR login."""
from hermes_cli.gateway import _setup_weixin as _gateway_setup_weixin
from hermes_agent.cli.gateway import _setup_weixin as _gateway_setup_weixin
_gateway_setup_weixin()
def _setup_signal():
"""Configure Signal via gateway setup."""
from hermes_cli.gateway import _setup_signal as _gateway_setup_signal
from hermes_agent.cli.gateway import _setup_signal as _gateway_setup_signal
_gateway_setup_signal()
def _setup_email():
"""Configure Email via gateway setup."""
from hermes_cli.gateway import _setup_email as _gateway_setup_email
from hermes_agent.cli.gateway import _setup_email as _gateway_setup_email
_gateway_setup_email()
def _setup_sms():
"""Configure SMS (Twilio) via gateway setup."""
from hermes_cli.gateway import _setup_sms as _gateway_setup_sms
from hermes_agent.cli.gateway import _setup_sms as _gateway_setup_sms
_gateway_setup_sms()
def _setup_dingtalk():
"""Configure DingTalk via gateway setup."""
from hermes_cli.gateway import _setup_dingtalk as _gateway_setup_dingtalk
from hermes_agent.cli.gateway import _setup_dingtalk as _gateway_setup_dingtalk
_gateway_setup_dingtalk()
def _setup_feishu():
"""Configure Feishu / Lark via gateway setup."""
from hermes_cli.gateway import _setup_feishu as _gateway_setup_feishu
from hermes_agent.cli.gateway import _setup_feishu as _gateway_setup_feishu
_gateway_setup_feishu()
def _setup_wecom():
"""Configure WeCom (Enterprise WeChat) via gateway setup."""
from hermes_cli.gateway import _setup_wecom as _gateway_setup_wecom
from hermes_agent.cli.gateway import _setup_wecom as _gateway_setup_wecom
_gateway_setup_wecom()
def _setup_wecom_callback():
"""Configure WeCom Callback (self-built app) via gateway setup."""
from hermes_cli.gateway import _setup_wecom_callback as _gw_setup
from hermes_agent.cli.gateway import _setup_wecom_callback as _gw_setup
_gw_setup()
@@ -2155,7 +2155,7 @@ def _setup_bluebubbles():
def _setup_qqbot():
"""Configure QQ Bot (Official API v2) via gateway setup."""
from hermes_cli.gateway import _setup_qqbot as _gateway_setup_qqbot
from hermes_agent.cli.gateway import _setup_qqbot as _gateway_setup_qqbot
_gateway_setup_qqbot()
@@ -2194,7 +2194,7 @@ def _setup_webhooks():
save_env_value("WEBHOOK_ENABLED", "true")
print()
print_success("Webhooks enabled! Next steps:")
from hermes_constants import display_hermes_home as _dhh
from hermes_agent.constants import display_hermes_home as _dhh
print_info(f" 1. Define webhook routes in {_dhh()}/config.yaml")
print_info(" 2. Point your service (GitHub, GitLab, etc.) at:")
print_info(" http://your-server:8644/webhooks/<route-name>")
@@ -2318,7 +2318,7 @@ def setup_gateway(config: dict):
_is_linux = _platform.system() == "Linux"
_is_macos = _platform.system() == "Darwin"
from hermes_cli.gateway import (
from hermes_agent.cli.gateway import (
_is_service_installed,
_is_service_running,
supports_systemd_services,
@@ -2398,7 +2398,7 @@ def setup_gateway(config: dict):
print_info(" Or as a boot-time service: sudo hermes gateway install --system")
print_info(" Or run in foreground: hermes gateway")
else:
from hermes_constants import is_container
from hermes_agent.constants import is_container
if is_container():
print_info("Start the gateway to bring your bots online:")
print_info(" hermes gateway run # Run as container main process")
@@ -2428,7 +2428,7 @@ def setup_tools(config: dict, first_install: bool = False):
first_install: When True, uses the simplified first-install flow
(no platform menu, prompts for all unconfigured API keys).
"""
from hermes_cli.tools_config import tools_command
from hermes_agent.cli.tools_config import tools_command
tools_command(first_install=first_install, config=config)
@@ -2450,14 +2450,14 @@ def _model_section_has_credentials(config: dict) -> bool:
``OPENAI_API_KEY`` / ``OPENROUTER_API_KEY`` values through OpenRouter.
"""
try:
from hermes_cli.auth import get_active_provider
from hermes_agent.cli.auth.auth import get_active_provider
if get_active_provider():
return True
except Exception:
pass
try:
from hermes_cli.auth import PROVIDER_REGISTRY
from hermes_agent.cli.auth.auth import PROVIDER_REGISTRY
except Exception:
PROVIDER_REGISTRY = {} # type: ignore[assignment]
@@ -2863,7 +2863,7 @@ def run_setup_wizard(args):
hermes setup tools just tool configuration
hermes setup agent just agent settings
"""
from hermes_cli.config import is_managed, managed_error
from hermes_agent.cli.config import is_managed, managed_error
if is_managed():
managed_error("run setup wizard")
return
@@ -2918,7 +2918,7 @@ def run_setup_wizard(args):
return
# Check if this is an existing installation with a provider configured
from hermes_cli.auth import get_active_provider
from hermes_agent.cli.auth.auth import get_active_provider
active_provider = get_active_provider()
is_existing = (
@@ -3072,8 +3072,8 @@ def _resolve_hermes_chat_argv() -> Optional[list[str]]:
return [hermes_bin, "chat"]
try:
if importlib.util.find_spec("hermes_cli") is not None:
return [sys.executable, "-m", "hermes_cli.main", "chat"]
if importlib.util.find_spec("hermes_agent.cli") is not None:
return [sys.executable, "-m", "hermes_agent.cli.main", "chat"]
except Exception:
pass
@@ -3140,7 +3140,7 @@ def _run_first_time_quick_setup(config: dict, hermes_home, is_existing: bool):
def _run_quick_setup(config: dict, hermes_home):
"""Quick setup — only configure items that are missing."""
from hermes_cli.config import (
from hermes_agent.cli.config import (
get_missing_env_vars,
get_missing_config_fields,
check_config_version,

View File

@@ -13,9 +13,9 @@ Config stored in ~/.hermes/config.yaml under:
"""
from typing import List, Optional, Set
from hermes_cli.config import load_config, save_config
from hermes_cli.colors import Colors, color
from hermes_cli.platforms import PLATFORMS as _PLATFORMS
from hermes_agent.cli.config import load_config, save_config
from hermes_agent.cli.ui.colors import Colors, color
from hermes_agent.cli.platforms import PLATFORMS as _PLATFORMS
# Backward-compatible view: {key: label_string} so existing code that
# iterates ``PLATFORMS.items()`` or calls ``PLATFORMS.get(key)`` keeps
@@ -52,7 +52,7 @@ def save_disabled_skills(config: dict, disabled: Set[str], platform: Optional[st
def _list_all_skills() -> List[dict]:
"""Return all installed skills (ignoring disabled state)."""
try:
from tools.skills_tool import _find_all_skills
from hermes_agent.tools.skills.tool import _find_all_skills
return _find_all_skills(skip_disabled=True)
except Exception:
return []
@@ -93,7 +93,7 @@ def _select_platform() -> Optional[str]:
def _toggle_by_category(skills: List[dict], disabled: Set[str]) -> Set[str]:
"""Toggle all skills in a category at once."""
from hermes_cli.curses_ui import curses_checklist
from hermes_agent.cli.ui.curses import curses_checklist
categories = _get_categories(skills)
cat_labels = []
@@ -124,7 +124,7 @@ def _toggle_by_category(skills: List[dict], disabled: Set[str]) -> Set[str]:
def skills_command(args=None):
"""Entry point for `hermes skills`."""
from hermes_cli.curses_ui import curses_checklist
from hermes_agent.cli.ui.curses import curses_checklist
config = load_config()
skills = _list_all_skills()

View File

@@ -21,7 +21,7 @@ from rich.table import Table
# Lazy imports to avoid circular dependencies and slow startup.
# tools.skills_hub and tools.skills_guard are imported inside functions.
from hermes_constants import display_hermes_home
from hermes_agent.constants import display_hermes_home
_console = Console()
@@ -37,7 +37,7 @@ def _resolve_short_name(name: str, sources, console: Console) -> str:
matches exist, shows them and asks the user to use the full identifier.
Returns empty string if nothing found or ambiguous.
"""
from tools.skills_hub import unified_search
from hermes_agent.tools.skills.hub import unified_search
c = console or _console
c.print(f"[dim]Resolving '{name}'...[/]")
@@ -144,7 +144,7 @@ def _derive_category_from_install_path(install_path: str) -> str:
def do_search(query: str, source: str = "all", limit: int = 10,
console: Optional[Console] = None) -> None:
"""Search registries and display results as a Rich table."""
from tools.skills_hub import GitHubAuth, create_source_router, unified_search
from hermes_agent.tools.skills.hub import GitHubAuth, create_source_router, unified_search
c = console or _console
c.print(f"\n[bold]Searching for:[/] {query}")
@@ -187,7 +187,7 @@ def do_browse(page: int = 1, page_size: int = 20, source: str = "all",
Official skills are always shown first, regardless of source filter.
"""
from tools.skills_hub import (
from hermes_agent.tools.skills.hub import (
GitHubAuth, create_source_router, parallel_search_sources,
)
@@ -311,11 +311,11 @@ def do_install(identifier: str, category: str = "", force: bool = False,
console: Optional[Console] = None, skip_confirm: bool = False,
invalidate_cache: bool = True) -> None:
"""Fetch, quarantine, scan, confirm, and install a skill."""
from tools.skills_hub import (
from hermes_agent.tools.skills.hub import (
GitHubAuth, create_source_router, ensure_hub_dirs,
quarantine_bundle, install_from_quarantine, HubLockFile,
)
from tools.skills_guard import scan_skill, should_allow_install, format_scan_report
from hermes_agent.tools.skills.guard import scan_skill, should_allow_install, format_scan_report
c = console or _console
ensure_hub_dirs()
@@ -377,7 +377,7 @@ def do_install(identifier: str, category: str = "", force: bool = False,
q_path = quarantine_bundle(bundle)
except ValueError as exc:
c.print(f"[bold red]Installation blocked:[/] {exc}\n")
from tools.skills_hub import append_audit_log
from hermes_agent.tools.skills.hub import append_audit_log
append_audit_log("BLOCKED", bundle.name, bundle.source,
bundle.trust_level, "invalid_path", str(exc))
return
@@ -395,7 +395,7 @@ def do_install(identifier: str, category: str = "", force: bool = False,
c.print(f"\n[bold red]Installation blocked:[/] {reason}")
# Clean up quarantine
shutil.rmtree(q_path, ignore_errors=True)
from tools.skills_hub import append_audit_log
from hermes_agent.tools.skills.hub import append_audit_log
append_audit_log("BLOCKED", bundle.name, bundle.source,
bundle.trust_level, result.verdict,
f"{len(result.findings)}_findings")
@@ -445,18 +445,18 @@ def do_install(identifier: str, category: str = "", force: bool = False,
except ValueError as exc:
c.print(f"[bold red]Installation blocked:[/] {exc}\n")
shutil.rmtree(q_path, ignore_errors=True)
from tools.skills_hub import append_audit_log
from hermes_agent.tools.skills.hub import append_audit_log
append_audit_log("BLOCKED", bundle.name, bundle.source,
bundle.trust_level, "invalid_path", str(exc))
return
from tools.skills_hub import SKILLS_DIR
from hermes_agent.tools.skills.hub import SKILLS_DIR
c.print(f"[bold green]Installed:[/] {install_dir.relative_to(SKILLS_DIR)}")
c.print(f"[dim]Files: {', '.join(bundle.files.keys())}[/]\n")
if invalidate_cache:
# Invalidate the skills prompt cache so the new skill appears immediately
try:
from agent.prompt_builder import clear_skills_system_prompt_cache
from hermes_agent.agent.prompt_builder import clear_skills_system_prompt_cache
clear_skills_system_prompt_cache(clear_snapshot=True)
except Exception:
pass
@@ -467,7 +467,7 @@ def do_install(identifier: str, category: str = "", force: bool = False,
def do_inspect(identifier: str, console: Optional[Console] = None) -> None:
"""Preview a skill's SKILL.md content without installing."""
from tools.skills_hub import GitHubAuth, create_source_router
from hermes_agent.tools.skills.hub import GitHubAuth, create_source_router
c = console or _console
auth = GitHubAuth()
@@ -520,7 +520,7 @@ def browse_skills(page: int = 1, page_size: int = 20, source: str = "all") -> di
Returns ``{"items": [...], "page": int, "total_pages": int, "total": int}``.
"""
from tools.skills_hub import GitHubAuth, create_source_router
from hermes_agent.tools.skills.hub import GitHubAuth, create_source_router
page_size = max(1, min(page_size, 100))
_TRUST_RANK = {"builtin": 3, "trusted": 2, "community": 1}
@@ -563,7 +563,7 @@ def browse_skills(page: int = 1, page_size: int = 20, source: str = "all") -> di
def inspect_skill(identifier: str) -> Optional[dict]:
"""Skill metadata (+ SKILL.md preview) for programmatic callers."""
from tools.skills_hub import GitHubAuth, create_source_router
from hermes_agent.tools.skills.hub import GitHubAuth, create_source_router
class _Q:
def print(self, *a, **k):
@@ -601,9 +601,9 @@ def inspect_skill(identifier: str) -> Optional[dict]:
def do_list(source_filter: str = "all", console: Optional[Console] = None) -> None:
"""List installed skills, distinguishing hub, builtin, and local skills."""
from tools.skills_hub import HubLockFile, ensure_hub_dirs
from tools.skills_sync import _read_manifest
from tools.skills_tool import _find_all_skills
from hermes_agent.tools.skills.hub import HubLockFile, ensure_hub_dirs
from hermes_agent.tools.skills.sync import _read_manifest
from hermes_agent.tools.skills.tool import _find_all_skills
c = console or _console
ensure_hub_dirs()
@@ -659,7 +659,7 @@ def do_list(source_filter: str = "all", console: Optional[Console] = None) -> No
def do_check(name: Optional[str] = None, console: Optional[Console] = None) -> None:
"""Check hub-installed skills for upstream updates."""
from tools.skills_hub import check_for_skill_updates
from hermes_agent.tools.skills.hub import check_for_skill_updates
c = console or _console
results = check_for_skill_updates(name=name)
@@ -682,7 +682,7 @@ def do_check(name: Optional[str] = None, console: Optional[Console] = None) -> N
def do_update(name: Optional[str] = None, console: Optional[Console] = None) -> None:
"""Update hub-installed skills with upstream changes."""
from tools.skills_hub import HubLockFile, check_for_skill_updates
from hermes_agent.tools.skills.hub import HubLockFile, check_for_skill_updates
c = console or _console
lock = HubLockFile()
@@ -702,8 +702,8 @@ def do_update(name: Optional[str] = None, console: Optional[Console] = None) ->
def do_audit(name: Optional[str] = None, console: Optional[Console] = None) -> None:
"""Re-run security scan on installed hub skills."""
from tools.skills_hub import HubLockFile, SKILLS_DIR
from tools.skills_guard import scan_skill, format_scan_report
from hermes_agent.tools.skills.hub import HubLockFile, SKILLS_DIR
from hermes_agent.tools.skills.guard import scan_skill, format_scan_report
c = console or _console
lock = HubLockFile()
@@ -737,7 +737,7 @@ def do_uninstall(name: str, console: Optional[Console] = None,
skip_confirm: bool = False,
invalidate_cache: bool = True) -> None:
"""Remove a hub-installed skill with confirmation."""
from tools.skills_hub import uninstall_skill
from hermes_agent.tools.skills.hub import uninstall_skill
c = console or _console
@@ -757,7 +757,7 @@ def do_uninstall(name: str, console: Optional[Console] = None,
c.print(f"[bold green]{msg}[/]\n")
if invalidate_cache:
try:
from agent.prompt_builder import clear_skills_system_prompt_cache
from hermes_agent.agent.prompt_builder import clear_skills_system_prompt_cache
clear_skills_system_prompt_cache(clear_snapshot=True)
except Exception:
pass
@@ -773,7 +773,7 @@ def do_reset(name: str, restore: bool = False,
skip_confirm: bool = False,
invalidate_cache: bool = True) -> None:
"""Reset a bundled skill's manifest tracking (+ optionally restore from bundled)."""
from tools.skills_sync import reset_bundled_skill
from hermes_agent.tools.skills.sync import reset_bundled_skill
c = console or _console
@@ -804,7 +804,7 @@ def do_reset(name: str, restore: bool = False,
if invalidate_cache:
try:
from agent.prompt_builder import clear_skills_system_prompt_cache
from hermes_agent.agent.prompt_builder import clear_skills_system_prompt_cache
clear_skills_system_prompt_cache(clear_snapshot=True)
except Exception:
pass
@@ -815,7 +815,7 @@ def do_reset(name: str, restore: bool = False,
def do_tap(action: str, repo: str = "", console: Optional[Console] = None) -> None:
"""Manage taps (custom GitHub repo sources)."""
from tools.skills_hub import TapsManager
from hermes_agent.tools.skills.hub import TapsManager
c = console or _console
mgr = TapsManager()
@@ -859,8 +859,8 @@ def do_tap(action: str, repo: str = "", console: Optional[Console] = None) -> No
def do_publish(skill_path: str, target: str = "github", repo: str = "",
console: Optional[Console] = None) -> None:
"""Publish a local skill to a registry (GitHub PR or ClawHub submission)."""
from tools.skills_hub import GitHubAuth, SKILLS_DIR
from tools.skills_guard import scan_skill, format_scan_report
from hermes_agent.tools.skills.hub import GitHubAuth, SKILLS_DIR
from hermes_agent.tools.skills.guard import scan_skill, format_scan_report
c = console or _console
path = Path(skill_path)
@@ -1024,7 +1024,7 @@ def _github_publish(skill_path: Path, skill_name: str, target_repo: str,
def do_snapshot_export(output_path: str, console: Optional[Console] = None) -> None:
"""Export current hub skill configuration to a portable JSON file."""
from tools.skills_hub import HubLockFile, TapsManager
from hermes_agent.tools.skills.hub import HubLockFile, TapsManager
c = console or _console
lock = HubLockFile()
@@ -1065,7 +1065,7 @@ def do_snapshot_export(output_path: str, console: Optional[Console] = None) -> N
def do_snapshot_import(input_path: str, force: bool = False,
console: Optional[Console] = None) -> None:
"""Re-install skills from a snapshot file."""
from tools.skills_hub import TapsManager
from hermes_agent.tools.skills.hub import TapsManager
c = console or _console
inp = Path(input_path)

View File

@@ -19,7 +19,7 @@ def get_provider_request_timeout(
return None
try:
from hermes_cli.config import load_config
from hermes_agent.cli.config import load_config
except ImportError:
return None
@@ -48,7 +48,7 @@ def get_provider_stale_timeout(
return None
try:
from hermes_cli.config import load_config
from hermes_agent.cli.config import load_config
except ImportError:
return None

Some files were not shown because too many files have changed in this diff Show More