mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
Remove unused imports (F401) and duplicate/shadowed import redefinitions (F811) across the codebase using ruff's safe autofixes. No behavioral changes -- imports only. - ~1400 safe autofixes applied across 644 files (net -1072 lines) - __init__.py re-exports preserved (excluded from F401 removal so public re-export surfaces stay intact) - Re-exports that are imported or monkeypatched by tests but look unused in their defining module are kept with explicit # noqa: F401 (gateway/run.py load_dotenv; run_agent re-exports from agent.message_sanitization, agent.context_compressor, agent.retry_utils, agent.prompt_builder, agent.process_bootstrap, agent.codex_responses_adapter) - Unsafe F841 (unused-variable) fixes deliberately skipped -- those can change behavior when the RHS has side effects - ruff lints remain disabled in pyproject.toml (only PLW1514 is selected); this is a one-time cleanup, not a config change Verification: - python -m compileall: clean - pytest --collect-only: all 27161 tests collect (zero import errors) - core entry points import clean (run_agent, model_tools, cli, toolsets, hermes_state, batch_runner, gateway) - static scan: every name any test imports directly from an edited module still resolves
306 lines
11 KiB
Python
306 lines
11 KiB
Python
"""``hermes lsp`` CLI subcommand.
|
|
|
|
Subcommands:
|
|
|
|
- ``status`` — show service state, configured servers, install status.
|
|
- ``install <server_id>`` — eagerly install one server's binary.
|
|
- ``install-all`` — try to install every server with a known recipe.
|
|
- ``restart`` — tear down running clients so the next edit re-spawns.
|
|
- ``which <server_id>`` — print the resolved binary path for one server.
|
|
- ``list`` — print the registry of supported servers.
|
|
|
|
The handlers are kept here (rather than in
|
|
``hermes_cli/main.py``) so the LSP module ships self-contained.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
|
|
|
|
def register_subparser(subparsers: argparse._SubParsersAction) -> None:
|
|
"""Wire the ``hermes lsp`` subcommand tree into the main argparse."""
|
|
parser = subparsers.add_parser(
|
|
"lsp",
|
|
help="Language Server Protocol management",
|
|
description=(
|
|
"Manage the LSP layer that powers post-write semantic "
|
|
"diagnostics in write_file/patch."
|
|
),
|
|
)
|
|
sub = parser.add_subparsers(dest="lsp_command")
|
|
|
|
sub_status = sub.add_parser("status", help="Show LSP service status")
|
|
sub_status.add_argument(
|
|
"--json", action="store_true", help="Emit machine-readable JSON"
|
|
)
|
|
|
|
sub_list = sub.add_parser("list", help="List supported language servers")
|
|
sub_list.add_argument(
|
|
"--installed-only",
|
|
action="store_true",
|
|
help="Only show servers whose binary is currently available",
|
|
)
|
|
|
|
sub_install = sub.add_parser("install", help="Install a server binary")
|
|
sub_install.add_argument("server", help="Server id (e.g. pyright, gopls)")
|
|
|
|
sub_install_all = sub.add_parser(
|
|
"install-all",
|
|
help="Install every server with a known auto-install recipe",
|
|
)
|
|
sub_install_all.add_argument(
|
|
"--include-manual",
|
|
action="store_true",
|
|
help="Even attempt servers marked manual-install (best effort)",
|
|
)
|
|
|
|
sub_restart = sub.add_parser(
|
|
"restart",
|
|
help="Tear down running LSP clients (next edit re-spawns)",
|
|
)
|
|
|
|
sub_which = sub.add_parser("which", help="Print binary path for a server")
|
|
sub_which.add_argument("server", help="Server id")
|
|
|
|
parser.set_defaults(func=run_lsp_command)
|
|
|
|
|
|
def run_lsp_command(args: argparse.Namespace) -> int:
|
|
"""Top-level dispatcher for ``hermes lsp <subcommand>``."""
|
|
sub = getattr(args, "lsp_command", None) or "status"
|
|
try:
|
|
if sub == "status":
|
|
return _cmd_status(getattr(args, "json", False))
|
|
if sub == "list":
|
|
return _cmd_list(getattr(args, "installed_only", False))
|
|
if sub == "install":
|
|
return _cmd_install(args.server)
|
|
if sub == "install-all":
|
|
return _cmd_install_all(getattr(args, "include_manual", False))
|
|
if sub == "restart":
|
|
return _cmd_restart()
|
|
if sub == "which":
|
|
return _cmd_which(args.server)
|
|
sys.stderr.write(f"unknown lsp subcommand: {sub}\n")
|
|
return 2
|
|
except KeyboardInterrupt:
|
|
return 130
|
|
|
|
|
|
def _cmd_status(emit_json: bool) -> int:
|
|
from agent.lsp import get_service
|
|
from agent.lsp.servers import SERVERS
|
|
from agent.lsp.install import detect_status
|
|
|
|
svc = get_service()
|
|
service_active = svc is not None
|
|
info = svc.get_status() if svc is not None else {"enabled": False}
|
|
|
|
if emit_json:
|
|
import json
|
|
payload = {
|
|
"service": info,
|
|
"registry": [
|
|
{
|
|
"server_id": s.server_id,
|
|
"extensions": list(s.extensions),
|
|
"description": s.description,
|
|
"binary_status": detect_status(_recipe_pkg_for(s.server_id)),
|
|
}
|
|
for s in SERVERS
|
|
],
|
|
}
|
|
sys.stdout.write(json.dumps(payload, indent=2) + "\n")
|
|
return 0
|
|
|
|
out = []
|
|
out.append("LSP Service")
|
|
out.append("===========")
|
|
out.append(f" enabled: {info.get('enabled', False)}")
|
|
if service_active:
|
|
out.append(f" wait_mode: {info.get('wait_mode')}")
|
|
out.append(f" wait_timeout: {info.get('wait_timeout')}s")
|
|
out.append(f" install_strategy:{info.get('install_strategy')}")
|
|
clients = info.get("clients") or []
|
|
if clients:
|
|
out.append(f" active clients: {len(clients)}")
|
|
for c in clients:
|
|
out.append(
|
|
f" - {c['server_id']:20s} state={c['state']:10s} root={c['workspace_root']}"
|
|
)
|
|
else:
|
|
out.append(" active clients: none")
|
|
broken = info.get("broken") or []
|
|
if broken:
|
|
out.append(f" broken pairs: {len(broken)}")
|
|
for b in broken:
|
|
out.append(f" - {b}")
|
|
disabled = info.get("disabled_servers") or []
|
|
if disabled:
|
|
out.append(f" disabled in cfg: {', '.join(disabled)}")
|
|
|
|
# Surface backend-tool gaps that aren't visible in the registry table:
|
|
# some servers spawn fine but emit no diagnostics without a sidecar
|
|
# binary (bash-language-server -> shellcheck).
|
|
backend_warnings = _backend_warnings()
|
|
if backend_warnings:
|
|
out.append("")
|
|
out.append("Backend warnings")
|
|
out.append("================")
|
|
for line in backend_warnings:
|
|
out.append(f" ! {line}")
|
|
out.append("")
|
|
out.append("Registered Servers")
|
|
out.append("==================")
|
|
for s in SERVERS:
|
|
pkg = _recipe_pkg_for(s.server_id)
|
|
status = detect_status(pkg)
|
|
marker = {
|
|
"installed": "✓",
|
|
"missing": "·",
|
|
"manual-only": "?",
|
|
}.get(status, " ")
|
|
ext_summary = ", ".join(list(s.extensions)[:5])
|
|
if len(s.extensions) > 5:
|
|
ext_summary += f", … (+{len(s.extensions) - 5})"
|
|
out.append(
|
|
f" {marker} {s.server_id:24s} [{status:11s}] {ext_summary}"
|
|
)
|
|
if s.description:
|
|
out.append(f" {s.description}")
|
|
sys.stdout.write("\n".join(out) + "\n")
|
|
return 0
|
|
|
|
|
|
def _cmd_list(installed_only: bool) -> int:
|
|
from agent.lsp.servers import SERVERS
|
|
from agent.lsp.install import detect_status
|
|
|
|
for s in SERVERS:
|
|
pkg = _recipe_pkg_for(s.server_id)
|
|
status = detect_status(pkg)
|
|
if installed_only and status != "installed":
|
|
continue
|
|
sys.stdout.write(
|
|
f"{s.server_id:24s} [{status:11s}] {','.join(s.extensions)}\n"
|
|
)
|
|
return 0
|
|
|
|
|
|
def _cmd_install(server_id: str) -> int:
|
|
from agent.lsp.install import try_install, INSTALL_RECIPES, detect_status
|
|
pkg = _recipe_pkg_for(server_id)
|
|
pre_status = detect_status(pkg)
|
|
if pre_status == "installed":
|
|
sys.stdout.write(f"{server_id} already installed\n")
|
|
return 0
|
|
sys.stdout.write(f"installing {server_id} (pkg={pkg}) ...\n")
|
|
sys.stdout.flush()
|
|
bin_path = try_install(pkg, "auto")
|
|
if bin_path is None:
|
|
recipe = INSTALL_RECIPES.get(pkg)
|
|
if recipe and recipe.get("strategy") == "manual":
|
|
sys.stderr.write(
|
|
f"{server_id}: this server requires a manual install. "
|
|
f"See documentation.\n"
|
|
)
|
|
else:
|
|
sys.stderr.write(f"{server_id}: install failed (see logs).\n")
|
|
return 1
|
|
sys.stdout.write(f"installed: {bin_path}\n")
|
|
return 0
|
|
|
|
|
|
def _cmd_install_all(include_manual: bool) -> int:
|
|
from agent.lsp.servers import SERVERS
|
|
from agent.lsp.install import try_install, INSTALL_RECIPES, detect_status
|
|
|
|
rc = 0
|
|
for s in SERVERS:
|
|
pkg = _recipe_pkg_for(s.server_id)
|
|
recipe = INSTALL_RECIPES.get(pkg)
|
|
if recipe is None:
|
|
continue
|
|
if recipe.get("strategy") == "manual" and not include_manual:
|
|
continue
|
|
if detect_status(pkg) == "installed":
|
|
sys.stdout.write(f" {s.server_id:24s} already installed\n")
|
|
continue
|
|
sys.stdout.write(f" installing {s.server_id} (pkg={pkg}) ... ")
|
|
sys.stdout.flush()
|
|
path = try_install(pkg, "auto")
|
|
if path:
|
|
sys.stdout.write(f"ok ({path})\n")
|
|
else:
|
|
sys.stdout.write("FAILED\n")
|
|
rc = 1
|
|
return rc
|
|
|
|
|
|
def _cmd_restart() -> int:
|
|
from agent.lsp import shutdown_service
|
|
|
|
shutdown_service()
|
|
sys.stdout.write("LSP service shut down. Next edit will respawn clients.\n")
|
|
return 0
|
|
|
|
|
|
def _cmd_which(server_id: str) -> int:
|
|
from agent.lsp.install import INSTALL_RECIPES, hermes_lsp_bin_dir
|
|
import shutil as _shutil
|
|
|
|
recipe = INSTALL_RECIPES.get(server_id)
|
|
bin_name = (recipe or {}).get("bin", server_id)
|
|
staged = hermes_lsp_bin_dir() / bin_name
|
|
if staged.exists():
|
|
sys.stdout.write(str(staged) + "\n")
|
|
return 0
|
|
on_path = _shutil.which(bin_name)
|
|
if on_path:
|
|
sys.stdout.write(on_path + "\n")
|
|
return 0
|
|
sys.stderr.write(f"{server_id}: not installed\n")
|
|
return 1
|
|
|
|
|
|
def _recipe_pkg_for(server_id: str) -> str:
|
|
"""Map a registry ``server_id`` to its install-recipe package key."""
|
|
# The mapping lives here (not in install.py) because it's a CLI
|
|
# convenience layer. Most server_ids are also their own recipe
|
|
# key, but a few differ (e.g. ``vue-language-server`` →
|
|
# ``@vue/language-server``).
|
|
aliases = {
|
|
"vue-language-server": "@vue/language-server",
|
|
"astro-language-server": "@astrojs/language-server",
|
|
"dockerfile-ls": "dockerfile-language-server-nodejs",
|
|
"typescript": "typescript-language-server",
|
|
}
|
|
return aliases.get(server_id, server_id)
|
|
|
|
|
|
def _backend_warnings() -> list:
|
|
"""Return human-readable notes about LSP backend tools that are missing
|
|
in a way that won't surface elsewhere.
|
|
|
|
Some language servers ship as thin wrappers around an external CLI for
|
|
actual diagnostics — they spawn cleanly but never emit any errors when
|
|
the sidecar binary isn't on PATH. bash-language-server / shellcheck
|
|
is the load-bearing example.
|
|
|
|
Returned strings are short, actionable, and include the install
|
|
suggestion across common platforms.
|
|
"""
|
|
import shutil as _shutil
|
|
from agent.lsp.install import hermes_lsp_bin_dir
|
|
notes: list = []
|
|
bash_installed = _shutil.which("bash-language-server") is not None or (
|
|
(hermes_lsp_bin_dir() / "bash-language-server").exists()
|
|
)
|
|
if bash_installed and _shutil.which("shellcheck") is None:
|
|
notes.append(
|
|
"bash-language-server is installed but shellcheck is missing — "
|
|
"diagnostics will be empty (apt: shellcheck, brew: shellcheck, "
|
|
"scoop: shellcheck)."
|
|
)
|
|
return notes
|