feat(install): warn pip/Homebrew installs are unsupported (CLI, TUI, desktop) (#57225)

* feat(install): warn pip/Homebrew installs are unsupported (CLI, TUI, desktop)

pip and Homebrew are now Unsupported install methods per
website/docs/getting-started/platform-support.md. Surface a
warn-don't-block deprecation notice everywhere the install method is
already shown, pointing at the platform-support docs and noting these
installs will not receive further updates. NixOS (Tier 2) is untouched.

- hermes_cli/config.py: shared is_unsupported_install_method() /
  format_unsupported_install_warning() helpers so the wording and docs
  link stay consistent across every surface.
- hermes_cli/banner.py: generalize the existing pip-only banner
  warning to also cover Homebrew.
- hermes_cli/main.py: hermes update and hermes update --check print
  the warning before proceeding (still update; warn, don't block).
- tui_gateway/server.py: session.info gains install_warning.
- ui-tui: SessionPanel renders install_warning alongside the existing
  'N commits behind' notice.
- apps/desktop: SessionRuntimeInfo/GatewayEventPayload gain
  install_warning; applyRuntimeInfo + the live session.info event fire
  a snoozable warning toast via a new reportInstallMethodWarning(),
  mirroring the existing backend-contract-skew toast pattern. i18n
  strings added for en/zh/zh-hant/ja.
- Tests: updated pip banner assertions for the new wording, added a
  Homebrew banner test, and two tui_gateway session_info tests
  (install_warning present for pip, absent for git).

* fix(nix): make `hermes` in developement environment actually work

install modules as editable overlay with uv

* feat: print install method when running --version

* fix: correct detect install method when running from a subtree
fix/verification-admin-route-recovery
ethernet 2026-07-07 21:13:19 -07:00 committed by GitHub
parent 9de9c25f62
commit 4d7f8ade3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 340 additions and 95 deletions

2
.envrc
View File

@ -1,4 +1,4 @@
watch_file pyproject.toml uv.lock watch_file pyproject.toml uv.lock hermes
watch_file package-lock.json package.json web/package.json ui-tui/package.json website/package.json apps/shared/package.json apps/desktop/package.json ui-tui/packages/hermes-ink/package.json watch_file package-lock.json package.json web/package.json ui-tui/package.json website/package.json apps/shared/package.json apps/desktop/package.json ui-tui/packages/hermes-ink/package.json
watch_file flake.nix flake.lock nix/devShell.nix nix/tui.nix nix/package.nix nix/python.nix nix/hermes-agent.nix nix/desktop.nix watch_file flake.nix flake.lock nix/devShell.nix nix/tui.nix nix/package.nix nix/python.nix nix/hermes-agent.nix nix/desktop.nix

View File

@ -39,6 +39,7 @@ import {
import { clearSessionSubagents, pruneDelegateFallbackSubagents, upsertSubagent } from '@/store/subagents' import { clearSessionSubagents, pruneDelegateFallbackSubagents, upsertSubagent } from '@/store/subagents'
import { clearActiveSessionTodos } from '@/store/todos' import { clearActiveSessionTodos } from '@/store/todos'
import { recordToolDiff } from '@/store/tool-diffs' import { recordToolDiff } from '@/store/tool-diffs'
import { reportInstallMethodWarning } from '@/store/updates'
import { notifyWorkspaceChanged, toolMayMutateFiles } from '@/store/workspace-events' import { notifyWorkspaceChanged, toolMayMutateFiles } from '@/store/workspace-events'
import type { RpcEvent } from '@/types/hermes' import type { RpcEvent } from '@/types/hermes'
@ -216,6 +217,10 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
requestDesktopOnboarding(payload.credential_warning) requestDesktopOnboarding(payload.credential_warning)
} }
if (apply) {
reportInstallMethodWarning(payload?.install_warning)
}
void refreshHermesConfig() void refreshHermesConfig()
if (modelChanged || providerChanged) { if (modelChanged || providerChanged) {

View File

@ -19,7 +19,7 @@ import {
setSessions, setSessions,
setYoloActive setYoloActive
} from '@/store/session' } from '@/store/session'
import { reportBackendContract } from '@/store/updates' import { reportBackendContract, reportInstallMethodWarning } from '@/store/updates'
import type { SessionCreateResponse, SessionInfo, SessionRuntimeInfo } from '@/types/hermes' import type { SessionCreateResponse, SessionInfo, SessionRuntimeInfo } from '@/types/hermes'
import type { ClientSessionState } from '../../../types' import type { ClientSessionState } from '../../../types'
@ -270,6 +270,8 @@ export function applyRuntimeInfo(info: SessionRuntimeInfo | undefined): SessionR
requestDesktopOnboarding(info.credential_warning) requestDesktopOnboarding(info.credential_warning)
} }
reportInstallMethodWarning(info.install_warning)
if (typeof info.model === 'string') { if (typeof info.model === 'string') {
setCurrentModel(info.model) setCurrentModel(info.model)
sessionState.model = info.model sessionState.model = info.model

View File

@ -119,6 +119,7 @@ export const en: Translations = {
backendOutOfDateTitle: 'Backend out of date', backendOutOfDateTitle: 'Backend out of date',
backendOutOfDateMessage: backendOutOfDateMessage:
'Your Hermes backend is older than this desktop build and may not work correctly. Update to align them.', 'Your Hermes backend is older than this desktop build and may not work correctly. Update to align them.',
installMethodUnsupportedTitle: 'Unsupported install method',
updateHermes: 'Update Hermes', updateHermes: 'Update Hermes',
updateReadyTitle: 'Update ready', updateReadyTitle: 'Update ready',
updateReadyMessage: count => `${count} new change${count === 1 ? '' : 's'} available.`, updateReadyMessage: count => `${count} new change${count === 1 ? '' : 's'} available.`,

View File

@ -120,6 +120,7 @@ export const ja = defineLocale({
backendOutOfDateTitle: 'バックエンドが古いです', backendOutOfDateTitle: 'バックエンドが古いです',
backendOutOfDateMessage: backendOutOfDateMessage:
'Hermes バックエンドがこのデスクトップビルドより古く、正常に動作しない場合があります。更新して揃えてください。', 'Hermes バックエンドがこのデスクトップビルドより古く、正常に動作しない場合があります。更新して揃えてください。',
installMethodUnsupportedTitle: 'サポート対象外のインストール方法',
updateHermes: 'Hermes を更新', updateHermes: 'Hermes を更新',
updateReadyTitle: '更新の準備ができました', updateReadyTitle: '更新の準備ができました',
updateReadyMessage: count => `${count} 件の新しい変更が利用可能です。`, updateReadyMessage: count => `${count} 件の新しい変更が利用可能です。`,

View File

@ -161,6 +161,7 @@ export interface Translations {
copyDetailFailed: string copyDetailFailed: string
backendOutOfDateTitle: string backendOutOfDateTitle: string
backendOutOfDateMessage: string backendOutOfDateMessage: string
installMethodUnsupportedTitle: string
updateHermes: string updateHermes: string
updateReadyTitle: string updateReadyTitle: string
updateReadyMessage: (count: number) => string updateReadyMessage: (count: number) => string

View File

@ -116,6 +116,7 @@ export const zhHant = defineLocale({
copyDetailFailed: '無法複製通知詳情', copyDetailFailed: '無法複製通知詳情',
backendOutOfDateTitle: '後端版本過舊', backendOutOfDateTitle: '後端版本過舊',
backendOutOfDateMessage: '您的 Hermes 後端早於目前的桌面版本,可能無法正常運作。請更新以保持一致。', backendOutOfDateMessage: '您的 Hermes 後端早於目前的桌面版本,可能無法正常運作。請更新以保持一致。',
installMethodUnsupportedTitle: '不受支援的安裝方式',
updateHermes: '更新 Hermes', updateHermes: '更新 Hermes',
updateReadyTitle: '有可用更新', updateReadyTitle: '有可用更新',
updateReadyMessage: count => `${count} 項新變更可用。`, updateReadyMessage: count => `${count} 項新變更可用。`,

View File

@ -116,6 +116,7 @@ export const zh: Translations = {
copyDetailFailed: '无法复制通知详情', copyDetailFailed: '无法复制通知详情',
backendOutOfDateTitle: '后端版本过旧', backendOutOfDateTitle: '后端版本过旧',
backendOutOfDateMessage: '你的 Hermes 后端早于当前桌面构建,可能无法正常工作。请更新以保持一致。', backendOutOfDateMessage: '你的 Hermes 后端早于当前桌面构建,可能无法正常工作。请更新以保持一致。',
installMethodUnsupportedTitle: '不受支持的安装方式',
updateHermes: '更新 Hermes', updateHermes: '更新 Hermes',
updateReadyTitle: '有可用更新', updateReadyTitle: '有可用更新',
updateReadyMessage: count => `${count} 项新更改可用。`, updateReadyMessage: count => `${count} 项新更改可用。`,

View File

@ -51,6 +51,7 @@ export type GatewayEventPayload = {
cwd?: string cwd?: string
branch?: string branch?: string
credential_warning?: string credential_warning?: string
install_warning?: string
personality?: string personality?: string
usage?: Partial<UsageStats> usage?: Partial<UsageStats>
// agent.terminal.output — live chunk for a read-only agent terminal tab // agent.terminal.output — live chunk for a read-only agent terminal tab

View File

@ -111,6 +111,24 @@ function isSkewToastSnoozed(): boolean {
return Number.isFinite(until) && Date.now() < until return Number.isFinite(until) && Date.now() < until
} }
const INSTALL_METHOD_TOAST_ID = 'install-method-not-supported'
// Same time-based snooze pattern as the update/skew toasts: the warning is
// re-derived from every session.info (session.create/resume/activate all
// route through applyRuntimeInfo), so without a snooze it would re-pop on
// every session switch even right after the user dismissed it.
const INSTALL_METHOD_TOAST_SNOOZE_KEY = 'hermes:install-method-toast-snooze-until'
const INSTALL_METHOD_TOAST_COOLDOWN_MS = 24 * 60 * 60 * 1000
function snoozeInstallMethodToast(): void {
persistString(INSTALL_METHOD_TOAST_SNOOZE_KEY, String(Date.now() + INSTALL_METHOD_TOAST_COOLDOWN_MS))
}
function isInstallMethodToastSnoozed(): boolean {
const until = Number(storedString(INSTALL_METHOD_TOAST_SNOOZE_KEY) || 0)
return Number.isFinite(until) && Date.now() < until
}
/** /**
* Guard against a desktop GUI talking to a backend that predates its contract * Guard against a desktop GUI talking to a backend that predates its contract
* (e.g. a bb/gui-built app pointed at a `main` checkout). Rather than failing * (e.g. a bb/gui-built app pointed at a `main` checkout). Rather than failing
@ -151,6 +169,27 @@ export function reportBackendContract(contract: number | undefined): void {
}) })
} }
export function reportInstallMethodWarning(message: string | undefined): void {
if (!message) {
dismissNotification(INSTALL_METHOD_TOAST_ID)
return
}
if (isInstallMethodToastSnoozed()) {
return
}
notify({
durationMs: 0,
id: INSTALL_METHOD_TOAST_ID,
kind: 'warning',
message,
onDismiss: () => snoozeInstallMethodToast(),
title: translateNow('notifications.installMethodUnsupportedTitle')
})
}
/** /**
* Fire a toast when an update is available, at most once per cooldown window. * Fire a toast when an update is available, at most once per cooldown window.
* Closing the toast dismissing it or opening the updates window from it * Closing the toast dismissing it or opening the updates window from it

View File

@ -399,6 +399,7 @@ export interface SessionRuntimeInfo {
cwd?: string cwd?: string
desktop_contract?: number desktop_contract?: number
fast?: boolean fast?: boolean
install_warning?: string
model?: string model?: string
personality?: string personality?: string
provider?: string provider?: string

View File

@ -2,7 +2,6 @@
Pure display functions with no HermesCLI state dependency. Pure display functions with no HermesCLI state dependency.
""" """
import json import json
import logging import logging
import os import os
@ -322,8 +321,8 @@ def check_for_updates() -> Optional[int]:
# both the Rich banner (build_welcome_banner) and the Ink badge # both the Rich banner (build_welcome_banner) and the Ink badge
# (branding.tsx, guarded on `typeof === 'number' && > 0`) show nothing. # (branding.tsx, guarded on `typeof === 'number' && > 0`) show nothing.
try: try:
from hermes_cli.config import detect_install_method from hermes_cli.config import detect_install_method, get_project_root
if detect_install_method() == "docker": if detect_install_method(get_project_root()) == "docker":
return None return None
except Exception: except Exception:
pass pass
@ -889,17 +888,23 @@ def build_welcome_banner(console: "Console", model: str, cwd: str,
except Exception: except Exception:
pass # Never break the banner over an update check pass # Never break the banner over an update check
# Pip-install warning — `pip install hermes-agent` is not the supported # Unsupported install-method warning — pip/PyPI and Homebrew are no
# install path (it exists on PyPI for internal/CI reasons, not end users). # longer an officially supported distribution method (see
# Such installs miss the git checkout + installer-managed deps, so updates, # website/docs/getting-started/platform-support.md). Such installs miss
# self-update, and issue triage don't behave correctly. Warn, don't block. # the git checkout + installer-managed deps, so updates, self-update, and
# issue triage don't behave correctly. Warn, don't block. NixOS is fully
# supported and never hits this.
try: try:
from hermes_cli.config import detect_install_method from hermes_cli.config import (
if detect_install_method() == "pip": detect_install_method,
format_unsupported_install_warning,
is_unsupported_install_method,
get_project_root
)
_install_method = detect_install_method(get_project_root())
if is_unsupported_install_method(_install_method):
right_lines.append( right_lines.append(
"[bold yellow]⚠ pip install not officially supported[/]" f"[bold yellow]⚠ {format_unsupported_install_warning(_install_method)}[/]"
"[dim yellow] — exists for reasons other than user install; "
"expect instability and an inability to support issues[/]"
) )
except Exception: except Exception:
pass # Never break the banner over the install-method check pass # Never break the banner over the install-method check

View File

@ -441,8 +441,20 @@ def detect_install_method(project_root: Optional[Path] = None) -> str:
managed = get_managed_system() managed = get_managed_system()
if managed: if managed:
return managed.lower().replace(" ", "-") return managed.lower().replace(" ", "-")
if (root / ".git").is_dir():
# detect git repo installs (normal installer, development env)
git_path = root / ".git"
if git_path.is_dir():
return "git" return "git"
# detect git repo installs from worktrees
if git_path.is_file():
try:
content = git_path.read_text(encoding="utf-8").strip()
if content.startswith("gitdir:"):
return "git"
except OSError:
pass
return "pip" return "pip"
@ -528,10 +540,54 @@ def recommended_update_command() -> str:
managed_cmd = get_managed_update_command() managed_cmd = get_managed_update_command()
if managed_cmd: if managed_cmd:
return managed_cmd return managed_cmd
method = detect_install_method() method = detect_install_method(get_project_root())
return recommended_update_command_for_method(method) return recommended_update_command_for_method(method)
# =============================================================================
# Unsupported install methods (pip, Homebrew) — deprecation notice
# =============================================================================
#
# pip/PyPI and Homebrew are NOT an officially supported distribution method
# (see website/docs/getting-started/platform-support.md, "Unsupported"
# section). pip exists on PyPI for internal/CI reasons, not end-user installs;
# Homebrew is a legacy packaging path. Unlike NixOS/Homebrew "managed mode"
# (which hard-blocks config writes), this is a warn-don't-block deprecation
# notice surfaced everywhere the user might see install-method state: the CLI
# banner, the TUI/desktop session info panel, and ``hermes update``. NixOS
# stays fully supported (Tier 2) and must never hit this path.
PLATFORM_SUPPORT_DOCS_URL = "https://hermes-agent.nousresearch.com/docs/getting-started/platform-support"
_UNSUPPORTED_INSTALL_METHODS = frozenset({"pip", "homebrew"})
def is_unsupported_install_method(method: str) -> bool:
"""Whether ``method`` (from ``detect_install_method()``) is deprecated."""
return method in _UNSUPPORTED_INSTALL_METHODS
def unsupported_install_method_label(method: str) -> str:
"""Human-readable name for an unsupported install method."""
return "pip" if method == "pip" else "Homebrew"
def format_unsupported_install_warning(method: str) -> str:
"""Plain-text (no markup) deprecation notice for pip/Homebrew installs.
Shared verbatim across the CLI banner, TUI/desktop ``session.info``, and
``hermes update`` / ``hermes update --check`` so the wording and the
docs link stays consistent across every surface instead of drifting
into three slightly different warnings.
"""
label = unsupported_install_method_label(method)
return (
f"{label} installs are no longer an officially supported platform and "
f"will not receive further updates. See {PLATFORM_SUPPORT_DOCS_URL} "
"for supported install methods."
)
# Long-form text for ``hermes update`` / ``--check`` when running inside the # Long-form text for ``hermes update`` / ``--check`` when running inside the
# Docker image. Surfaced by ``cmd_update`` and ``_cmd_update_check`` in # Docker image. Surfaced by ``cmd_update`` and ``_cmd_update_check`` in
# hermes_cli/main.py; lives here so the wording stays consistent and we # hermes_cli/main.py; lives here so the wording stays consistent and we

View File

@ -229,9 +229,9 @@ def _read_openai_version_fast() -> str | None:
def _print_fast_version_info() -> None: def _print_fast_version_info() -> None:
from hermes_cli import __release_date__, __version__ from hermes_cli import __release_date__, __version__
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
print(f"Hermes Agent v{__version__} ({__release_date__})") print(f"Hermes Agent v{__version__} ({__release_date__})")
print(f"Project: {project_root}") print(f"Install directory: {PROJECT_ROOT}")
print(f"Python: {sys.version.split()[0]}") print(f"Python: {sys.version.split()[0]}")
openai_version = _read_openai_version_fast() openai_version = _read_openai_version_fast()
@ -4329,10 +4329,12 @@ def cmd_import(args):
def _print_version_info(*, check_updates: bool = True) -> None: def _print_version_info(*, check_updates: bool = True) -> None:
from hermes_cli.config import detect_install_method
from hermes_cli.banner import format_banner_version_label from hermes_cli.banner import format_banner_version_label
print(format_banner_version_label()) print(format_banner_version_label())
print(f"Project: {PROJECT_ROOT}") print(f"Install directory: {PROJECT_ROOT}")
print(f"Install method: {detect_install_method(PROJECT_ROOT)}")
# Show Python version # Show Python version
print(f"Python: {sys.version.split()[0]}") print(f"Python: {sys.version.split()[0]}")
@ -8371,8 +8373,14 @@ def _cmd_update_check(branch: str = "main", *, branch_explicit: bool = False):
on a PyPI install we surface a one-line notice instead of silently on a PyPI install we surface a one-line notice instead of silently
dropping the flag. dropping the flag.
""" """
from hermes_cli.config import detect_install_method from hermes_cli.config import (
detect_install_method,
format_unsupported_install_warning,
is_unsupported_install_method,
)
method = detect_install_method(PROJECT_ROOT) method = detect_install_method(PROJECT_ROOT)
if is_unsupported_install_method(method):
print(f"{format_unsupported_install_warning(method)}")
if method == "docker": if method == "docker":
# Docker can't ``git fetch`` from within the container. Surface the # Docker can't ``git fetch`` from within the container. Surface the
# same long-form ``docker pull`` guidance ``hermes update`` (apply # same long-form ``docker pull`` guidance ``hermes update`` (apply
@ -9255,10 +9263,21 @@ def cmd_update(args):
from hermes_cli.config import ( from hermes_cli.config import (
detect_install_method, detect_install_method,
format_docker_update_message, format_docker_update_message,
format_unsupported_install_warning,
is_managed, is_managed,
is_unsupported_install_method,
managed_error, managed_error,
) )
# Deprecation notice for pip/Homebrew installs — printed before the
# managed-mode early-return below so Homebrew users (who are blocked from
# applying the update here) still see it. Warn, don't block: the update
# itself still proceeds (except Homebrew, which is managed-mode blocked
# for an unrelated reason — brew owns its own upgrade path).
_install_method_for_warning = detect_install_method(PROJECT_ROOT)
if is_unsupported_install_method(_install_method_for_warning):
print(f"{format_unsupported_install_warning(_install_method_for_warning)}")
if is_managed(): if is_managed():
managed_error("update Hermes Agent") managed_error("update Hermes Agent")
return return

View File

@ -28,13 +28,20 @@
packages = packages =
with pkgs; with pkgs;
[ [
(pkgs.runCommand "hermes" { } ''
mkdir -p $out/bin
install -Dm755 ${../hermes} $out/bin/hermes
'')
uv uv
] ]
++ self'.packages.default.passthru.devDeps; ++ self'.packages.default.passthru.devDeps;
shellHook = '' shellHook = ''
echo "Hermes Agent dev shell"
${combinedNonNpm} ${combinedNonNpm}
${hermesNpmLib.mkNpmDevShellHook npmPackageJsonPaths} ${hermesNpmLib.mkNpmDevShellHook npmPackageJsonPaths}
# for the devshell to pick up the src
export HERMES_PYTHON_SRC_ROOT=$(git rev-parse --show-toplevel)
echo "Hermes Agent dev shell in $HERMES_PYTHON_SRC_ROOT"
echo "Ready. Run 'hermes' to start." echo "Ready. Run 'hermes' to start."
''; '';
}; };

View File

@ -44,7 +44,7 @@ let
dependency-groups = [ "all" ] ++ extraDependencyGroups; dependency-groups = [ "all" ] ++ extraDependencyGroups;
}; };
hermesVenv = mkHermesVenv extraDependencyGroups; hermesVenv = (mkHermesVenv extraDependencyGroups).venv;
hermesNpmLib = callPackage ./lib.nix { hermesNpmLib = callPackage ./lib.nix {
inherit npm-lockfile-fix nodejs; inherit npm-lockfile-fix nodejs;
@ -200,33 +200,37 @@ stdenv.mkDerivation (finalAttrs: {
runHook postInstall runHook postInstall
''; '';
passthru = { passthru =
inherit let
hermesTui devPython = (mkHermesVenv (extraDependencyGroups ++ [ "dev" ])).editableVenv;
hermesWeb in
hermesNpmLib {
hermesVenv inherit
; hermesTui
hermesWeb
hermesNpmLib
hermesVenv
;
# `hermesDesktop` references `finalAttrs.finalPackage` (this whole # `hermesDesktop` references `finalAttrs.finalPackage` (this whole
# derivation, after all overrides are applied) so the desktop wrapper # derivation, after all overrides are applied) so the desktop wrapper
# can prepend its `/bin` to PATH. The desktop's resolver step 4 # can prepend its `/bin` to PATH. The desktop's resolver step 4
# ("existing hermes on PATH") then picks up the fully wrapped # ("existing hermes on PATH") then picks up the fully wrapped
# `hermes` binary — venv with all deps, bundled skills/plugins, # `hermes` binary — venv with all deps, bundled skills/plugins,
# runtime PATH (ripgrep/git/ffmpeg/etc). No re-implementation # runtime PATH (ripgrep/git/ffmpeg/etc). No re-implementation
# of the agent resolution in the desktop wrapper. # of the agent resolution in the desktop wrapper.
hermesDesktop = callPackage ./desktop.nix { hermesDesktop = callPackage ./desktop.nix {
inherit hermesNpmLib electron; inherit hermesNpmLib electron;
hermesAgent = finalAttrs.finalPackage; hermesAgent = finalAttrs.finalPackage;
};
devShellHook = ''
export HERMES_PYTHON=${devPython}/bin/python3
'';
devDeps = runtimeDeps ++ [ devPython ];
}; };
devShellHook = ''
export HERMES_PYTHON=${hermesVenv}/bin/python3
'';
devDeps = runtimeDeps ++ [ (mkHermesVenv (extraDependencyGroups ++ [ "dev" ])) ];
};
meta = with lib; { meta = with lib; {
description = "AI agent with advanced tool-calling capabilities"; description = "AI agent with advanced tool-calling capabilities";
homepage = "https://github.com/NousResearch/hermes-agent"; homepage = "https://github.com/NousResearch/hermes-agent";

View File

@ -27,7 +27,8 @@ let
dependency-groups = { }; dependency-groups = { };
}; };
mkPrebuiltOverride = final: from: dependencies: mkPrebuiltOverride =
final: from: dependencies:
hacks.nixpkgsPrebuilt { hacks.nixpkgsPrebuilt {
inherit from; inherit from;
prev = { prev = {
@ -38,64 +39,100 @@ let
# Legacy alibabacloud packages ship only sdists with setup.py/setup.cfg # Legacy alibabacloud packages ship only sdists with setup.py/setup.cfg
# and no pyproject.toml, so setuptools isn't declared as a build dep. # and no pyproject.toml, so setuptools isn't declared as a build dep.
buildSystemOverrides = final: prev: builtins.mapAttrs buildSystemOverrides =
(name: _: prev.${name}.overrideAttrs (old: { final: prev:
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ final.setuptools ]; builtins.mapAttrs
})) (
(lib.genAttrs [ name: _:
"alibabacloud-credentials-api" prev.${name}.overrideAttrs (old: {
"alibabacloud-endpoint-util" nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ final.setuptools ];
"alibabacloud-gateway-dingtalk" })
"alibabacloud-gateway-spi" )
"alibabacloud-tea" (
] (_: null)); lib.genAttrs [
"alibabacloud-credentials-api"
"alibabacloud-endpoint-util"
"alibabacloud-gateway-dingtalk"
"alibabacloud-gateway-spi"
"alibabacloud-tea"
] (_: null)
);
pythonPackageOverrides = final: _prev: pythonPackageOverrides =
if isAarch64Darwin then { final: _prev:
numpy = mkPrebuiltOverride final python312.pkgs.numpy { }; if isAarch64Darwin then
{
numpy = mkPrebuiltOverride final python312.pkgs.numpy { };
pyarrow = mkPrebuiltOverride final python312.pkgs.pyarrow { }; pyarrow = mkPrebuiltOverride final python312.pkgs.pyarrow { };
av = mkPrebuiltOverride final python312.pkgs.av { }; av = mkPrebuiltOverride final python312.pkgs.av { };
humanfriendly = mkPrebuiltOverride final python312.pkgs.humanfriendly { }; humanfriendly = mkPrebuiltOverride final python312.pkgs.humanfriendly { };
coloredlogs = mkPrebuiltOverride final python312.pkgs.coloredlogs { coloredlogs = mkPrebuiltOverride final python312.pkgs.coloredlogs {
humanfriendly = [ ]; humanfriendly = [ ];
}; };
onnxruntime = mkPrebuiltOverride final python312.pkgs.onnxruntime { onnxruntime = mkPrebuiltOverride final python312.pkgs.onnxruntime {
coloredlogs = [ ]; coloredlogs = [ ];
numpy = [ ]; numpy = [ ];
packaging = [ ]; packaging = [ ];
}; };
ctranslate2 = mkPrebuiltOverride final python312.pkgs.ctranslate2 { ctranslate2 = mkPrebuiltOverride final python312.pkgs.ctranslate2 {
numpy = [ ]; numpy = [ ];
pyyaml = [ ]; pyyaml = [ ];
}; };
faster-whisper = mkPrebuiltOverride final python312.pkgs.faster-whisper { faster-whisper = mkPrebuiltOverride final python312.pkgs.faster-whisper {
av = [ ]; av = [ ];
ctranslate2 = [ ]; ctranslate2 = [ ];
huggingface-hub = [ ]; huggingface-hub = [ ];
onnxruntime = [ ]; onnxruntime = [ ];
tokenizers = [ ]; tokenizers = [ ];
tqdm = [ ]; tqdm = [ ];
}; };
} else {}; }
else
{ };
pythonSet = pythonSet =
(callPackage pyproject-nix.build.packages { (callPackage pyproject-nix.build.packages {
python = python312; python = python312;
}).overrideScope }).overrideScope
(lib.composeManyExtensions [ (
pyproject-build-systems.overlays.default lib.composeManyExtensions [
overlay pyproject-build-systems.overlays.default
buildSystemOverrides overlay
pythonPackageOverrides buildSystemOverrides
]); pythonPackageOverrides
]
);
editableOverlay = workspace.mkEditablePyprojectOverlay {
root = "$HERMES_PYTHON_SRC_ROOT"; # resolved at shellHook time
};
workspaceRoot = ./..;
editableSet = pythonSet.overrideScope (
lib.composeManyExtensions [
editableOverlay
(final: prev: {
hermes-agent = prev.hermes-agent.overrideAttrs (old: {
# point straight at the real source instead of the filtered nix store copy
src = workspaceRoot;
nativeBuildInputs = old.nativeBuildInputs ++ final.resolveBuildSystem { editables = [ ]; };
});
})
]
);
in in
pythonSet.mkVirtualEnv "hermes-agent-env" { {
hermes-agent = dependency-groups; venv = pythonSet.mkVirtualEnv "hermes-agent-env" {
hermes-agent = dependency-groups;
};
editableVenv = editableSet.mkVirtualEnv "hermes-agent-editable-env" {
hermes-agent = dependency-groups;
};
} }

View File

@ -195,7 +195,33 @@ def test_banner_warns_on_pip_install(tmp_path):
out = buf.getvalue() out = buf.getvalue()
assert "officially" in out assert "officially" in out
assert "instability" in out assert "platform-support" in out
def test_banner_warns_on_homebrew_install(tmp_path):
"""The welcome banner surfaces a warning when the install method is homebrew."""
import io
from rich.console import Console
from hermes_cli import banner
hh = tmp_path / ".hermes"
hh.mkdir()
(hh / ".install_method").write_text("homebrew\n")
with patch("hermes_cli.config.get_hermes_home", return_value=hh), \
patch("hermes_constants.get_hermes_home", return_value=hh):
buf = io.StringIO()
console = Console(file=buf, width=400, force_terminal=False, color_system=None)
banner.build_welcome_banner(
console, model="m", cwd="/tmp",
tools=[{"function": {"name": "terminal"}}],
enabled_toolsets=["terminal"],
)
out = buf.getvalue()
assert "officially" in out
assert "Homebrew" in out
assert "platform-support" in out
def test_banner_no_pip_warning_on_git_install(tmp_path): def test_banner_no_pip_warning_on_git_install(tmp_path):

View File

@ -408,7 +408,7 @@ def test_termux_ultrafast_version_runs_before_heavy_startup(
out = capsys.readouterr().out out = capsys.readouterr().out
assert "Hermes Agent v" in out assert "Hermes Agent v" in out
assert "Project:" in out assert "Install directory:" in out
assert "Python:" in out assert "Python:" in out
assert "OpenAI SDK:" in out assert "OpenAI SDK:" in out

View File

@ -4727,6 +4727,25 @@ def test_session_info_includes_session_title(monkeypatch):
assert info["title"] == "Dashboard title" assert info["title"] == "Dashboard title"
def test_session_info_includes_install_warning_for_pip(monkeypatch):
"""pip installs surface install_warning; git installs don't (issue: pip/brew deprecation)."""
monkeypatch.setattr("hermes_cli.config.detect_install_method", lambda: "pip")
info = server._session_info(types.SimpleNamespace(tools=[], model="", provider=""))
assert "install_warning" in info
assert "pip" in info["install_warning"]
assert "platform-support" in info["install_warning"]
def test_session_info_omits_install_warning_for_git(monkeypatch):
monkeypatch.setattr("hermes_cli.config.detect_install_method", lambda: "git")
info = server._session_info(types.SimpleNamespace(tools=[], model="", provider=""))
assert "install_warning" not in info
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# History-mutating commands must reject while session.running is True. # History-mutating commands must reject while session.running is True.
# Without these guards, prompt.submit's post-run history write either # Without these guards, prompt.submit's post-run history write either

View File

@ -3238,6 +3238,18 @@ def _session_info(agent, session: dict | None = None) -> dict:
"usage": _get_usage(agent), "usage": _get_usage(agent),
"profile_name": _current_profile_name(), "profile_name": _current_profile_name(),
} }
try:
from hermes_cli.config import (
detect_install_method,
format_unsupported_install_warning,
is_unsupported_install_method,
)
_install_method = detect_install_method()
if is_unsupported_install_method(_install_method):
info["install_warning"] = format_unsupported_install_warning(_install_method)
except Exception:
pass
try: try:
from hermes_cli import __version__, __release_date__ from hermes_cli import __version__, __release_date__

View File

@ -412,6 +412,12 @@ export function SessionPanel({ info, maxWidth, sid, t }: SessionPanelProps) {
</Text> </Text>
</Text> </Text>
)} )}
{info.install_warning && (
<Text bold color={t.color.warn} wrap="wrap">
! {info.install_warning}
</Text>
)}
</Box> </Box>
</Box> </Box>
) )

View File

@ -150,6 +150,7 @@ export interface McpServerStatus {
export interface SessionInfo { export interface SessionInfo {
cwd?: string cwd?: string
fast?: boolean fast?: boolean
install_warning?: string
lazy?: boolean lazy?: boolean
mcp_servers?: McpServerStatus[] mcp_servers?: McpServerStatus[]
model: string model: string