Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .envrc
Original file line number Diff line number Diff line change
@@ -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 flake.nix flake.lock nix/devShell.nix nix/tui.nix nix/package.nix nix/python.nix nix/hermes-agent.nix nix/desktop.nix

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
import { clearSessionSubagents, pruneDelegateFallbackSubagents, upsertSubagent } from '@/store/subagents'
import { clearActiveSessionTodos } from '@/store/todos'
import { recordToolDiff } from '@/store/tool-diffs'
import { reportInstallMethodWarning } from '@/store/updates'
import { notifyWorkspaceChanged, toolMayMutateFiles } from '@/store/workspace-events'
import type { RpcEvent } from '@/types/hermes'

Expand Down Expand Up @@ -216,6 +217,10 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
requestDesktopOnboarding(payload.credential_warning)
}

if (apply) {
reportInstallMethodWarning(payload?.install_warning)
}

void refreshHermesConfig()

if (modelChanged || providerChanged) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
setSessions,
setYoloActive
} from '@/store/session'
import { reportBackendContract } from '@/store/updates'
import { reportBackendContract, reportInstallMethodWarning } from '@/store/updates'
import type { SessionCreateResponse, SessionInfo, SessionRuntimeInfo } from '@/types/hermes'

import type { ClientSessionState } from '../../../types'
Expand Down Expand Up @@ -270,6 +270,8 @@ export function applyRuntimeInfo(info: SessionRuntimeInfo | undefined): SessionR
requestDesktopOnboarding(info.credential_warning)
}

reportInstallMethodWarning(info.install_warning)

if (typeof info.model === 'string') {
setCurrentModel(info.model)
sessionState.model = info.model
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export const en: Translations = {
backendOutOfDateTitle: 'Backend out of date',
backendOutOfDateMessage:
'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',
updateReadyTitle: 'Update ready',
updateReadyMessage: count => `${count} new change${count === 1 ? '' : 's'} available.`,
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export const ja = defineLocale({
backendOutOfDateTitle: 'バックエンドが古いです',
backendOutOfDateMessage:
'Hermes バックエンドがこのデスクトップビルドより古く、正常に動作しない場合があります。更新して揃えてください。',
installMethodUnsupportedTitle: 'サポート対象外のインストール方法',
updateHermes: 'Hermes を更新',
updateReadyTitle: '更新の準備ができました',
updateReadyMessage: count => `${count} 件の新しい変更が利用可能です。`,
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export interface Translations {
copyDetailFailed: string
backendOutOfDateTitle: string
backendOutOfDateMessage: string
installMethodUnsupportedTitle: string
updateHermes: string
updateReadyTitle: string
updateReadyMessage: (count: number) => string
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/i18n/zh-hant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export const zhHant = defineLocale({
copyDetailFailed: '無法複製通知詳情',
backendOutOfDateTitle: '後端版本過舊',
backendOutOfDateMessage: '您的 Hermes 後端早於目前的桌面版本,可能無法正常運作。請更新以保持一致。',
installMethodUnsupportedTitle: '不受支援的安裝方式',
updateHermes: '更新 Hermes',
updateReadyTitle: '有可用更新',
updateReadyMessage: count => `有 ${count} 項新變更可用。`,
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export const zh: Translations = {
copyDetailFailed: '无法复制通知详情',
backendOutOfDateTitle: '后端版本过旧',
backendOutOfDateMessage: '你的 Hermes 后端早于当前桌面构建,可能无法正常工作。请更新以保持一致。',
installMethodUnsupportedTitle: '不受支持的安装方式',
updateHermes: '更新 Hermes',
updateReadyTitle: '有可用更新',
updateReadyMessage: count => `有 ${count} 项新更改可用。`,
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/lib/chat-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type GatewayEventPayload = {
cwd?: string
branch?: string
credential_warning?: string
install_warning?: string
personality?: string
usage?: Partial<UsageStats>
// agent.terminal.output — live chunk for a read-only agent terminal tab
Expand Down
39 changes: 39 additions & 0 deletions apps/desktop/src/store/updates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,24 @@ function isSkewToastSnoozed(): boolean {
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
* (e.g. a bb/gui-built app pointed at a `main` checkout). Rather than failing
Expand Down Expand Up @@ -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.
* Closing the toast — dismissing it or opening the updates window from it —
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/types/hermes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ export interface SessionRuntimeInfo {
cwd?: string
desktop_contract?: number
fast?: boolean
install_warning?: string
model?: string
personality?: string
provider?: string
Expand Down
29 changes: 17 additions & 12 deletions hermes_cli/banner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

Pure display functions with no HermesCLI state dependency.
"""

import json
import logging
import os
Expand Down Expand Up @@ -322,8 +321,8 @@ def check_for_updates() -> Optional[int]:
# both the Rich banner (build_welcome_banner) and the Ink badge
# (branding.tsx, guarded on `typeof === 'number' && > 0`) show nothing.
try:
from hermes_cli.config import detect_install_method
if detect_install_method() == "docker":
from hermes_cli.config import detect_install_method, get_project_root
if detect_install_method(get_project_root()) == "docker":
return None
except Exception:
pass
Expand Down Expand Up @@ -873,17 +872,23 @@ def build_welcome_banner(console: "Console", model: str, cwd: str,
except Exception:
pass # Never break the banner over an update check

# Pip-install warning — `pip install hermes-agent` is not the supported
# install path (it exists on PyPI for internal/CI reasons, not end users).
# Such installs miss the git checkout + installer-managed deps, so updates,
# self-update, and issue triage don't behave correctly. Warn, don't block.
# Unsupported install-method warning — pip/PyPI and Homebrew are no
# longer an officially supported distribution method (see
# website/docs/getting-started/platform-support.md). Such installs miss
# 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:
from hermes_cli.config import detect_install_method
if detect_install_method() == "pip":
from hermes_cli.config import (
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(
"[bold yellow]⚠ pip install not officially supported[/]"
"[dim yellow] — exists for reasons other than user install; "
"expect instability and an inability to support issues[/]"
f"[bold yellow]⚠ {format_unsupported_install_warning(_install_method)}[/]"
)
except Exception:
pass # Never break the banner over the install-method check
Expand Down
60 changes: 58 additions & 2 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,8 +439,20 @@ def detect_install_method(project_root: Optional[Path] = None) -> str:
managed = get_managed_system()
if managed:
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"

# 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"


Expand Down Expand Up @@ -526,10 +538,54 @@ def recommended_update_command() -> str:
managed_cmd = get_managed_update_command()
if managed_cmd:
return managed_cmd
method = detect_install_method()
method = detect_install_method(get_project_root())
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
# Docker image. Surfaced by ``cmd_update`` and ``_cmd_update_check`` in
# hermes_cli/main.py; lives here so the wording stays consistent and we
Expand Down
27 changes: 23 additions & 4 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,9 @@ def _read_openai_version_fast() -> str | None:
def _print_fast_version_info() -> None:
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"Project: {project_root}")
print(f"Install directory: {PROJECT_ROOT}")

print(f"Python: {sys.version.split()[0]}")

openai_version = _read_openai_version_fast()
Expand Down Expand Up @@ -4338,10 +4338,12 @@ def cmd_import(args):


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

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
print(f"Python: {sys.version.split()[0]}")
Expand Down Expand Up @@ -8375,8 +8377,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
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)
if is_unsupported_install_method(method):
print(f"⚠ {format_unsupported_install_warning(method)}")
if method == "docker":
# Docker can't ``git fetch`` from within the container. Surface the
# same long-form ``docker pull`` guidance ``hermes update`` (apply
Expand Down Expand Up @@ -9259,10 +9267,21 @@ def cmd_update(args):
from hermes_cli.config import (
detect_install_method,
format_docker_update_message,
format_unsupported_install_warning,
is_managed,
is_unsupported_install_method,
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():
managed_error("update Hermes Agent")
return
Expand Down
9 changes: 8 additions & 1 deletion nix/devShell.nix
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,20 @@
packages =
with pkgs;
[
(pkgs.runCommand "hermes" { } ''
mkdir -p $out/bin
install -Dm755 ${../hermes} $out/bin/hermes
'')
uv
]
++ self'.packages.default.passthru.devDeps;
shellHook = ''
echo "Hermes Agent dev shell"
${combinedNonNpm}
${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."
'';
};
Expand Down
Loading
Loading