Merge pull request #7469 from omacom/hermes-agent

Add Hermes as a desktop app and a coding agent
This commit is contained in:
Spencer Bull authored and GitHub committed 2026-09-01 11:11:57 -05:00
commit b71dcad96e
24 files changed
+1216 -19

No files matched your search

+7
View File
@@ -86,6 +86,13 @@ codex)
command=(codex --approve-for-me)
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
;;
hermes)
if [[ -n ${prompt:-} ]]; then
command=(env -u HERMES_SESSION_SOURCE hermes chat --yolo --tui "--query=$prompt")
else
command=(hermes --yolo)
fi
;;
omp)
command=(omp --auto-approve)
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
+20 -4
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# omarchy:summary=Set and launch the default coding agent
# omarchy:args=[pi|omp|opencode|ori|claude|codex|grok|agy|copilot|crush]
# omarchy:args=[pi|omp|opencode|ori|claude|codex|grok|agy|hermes|copilot|crush]
# omarchy:examples=omarchy default agent | omarchy default agent codex | omarchy default agent claude
installing=false
@@ -33,20 +33,36 @@ codex) agent="codex"; name="Codex" ;;
crush) agent="crush"; name="Crush" ;;
grok) agent="grok"; name="Grok"; agent_package="npm:@xai-official/grok" ;;
agy | antigravity | antigravity-cli | gemini | gemini-cli) agent="agy"; name="Antigravity"; agent_package="antigravity-cli" ;;
hermes) agent="hermes"; name="Hermes"; agent_installer="omarchy-install-hermes-cli" ;;
copilot | github-copilot) agent="copilot"; name="GitHub Copilot" ;;
*)
echo "Usage: omarchy-default-agent <pi|omp|opencode|ori|claude|codex|grok|agy|copilot|crush>"
echo "Usage: omarchy-default-agent <pi|omp|opencode|ori|claude|codex|grok|agy|hermes|copilot|crush>"
exit 1
;;
esac
agent_package=${agent_package:-$agent}
if [[ $installing == "false" ]] && ! mise where "$agent_package" &>/dev/null; then
# Hermes reaches mise through its own installer rather than straight from
# here: it needs its interpreter pinned, and a bare `mise use` has nowhere to
# say so. See omarchy-install-hermes-cli.
if [[ -n ${agent_installer:-} ]]; then
# Not omarchy-cmd-present: the stub is on PATH from first boot and says
# nothing about whether Hermes is installed behind it. Treating a cold stub
# as installed skips the floating terminal and runs the minute-long install
# inside the menu action instead.
agent_present() { "$agent_installer" --check; }
agent_install() { "$agent_installer" --now; }
else
agent_present() { mise where "$agent_package" &>/dev/null; }
agent_install() { mise use -g "$agent_package"; }
fi
if [[ $installing == "false" ]] && ! agent_present; then
exec omarchy-launch-floating-terminal-with-presentation omarchy-default-agent --install "$agent"
fi
if ! mise use -g "$agent_package"; then
if ! agent_install; then
if [[ $installing == "true" ]]; then
echo "Could not install $name with mise" >&2
else
+26
View File
@@ -0,0 +1,26 @@
#!/bin/bash
# omarchy:summary=Install the Hermes desktop app
# omarchy:requires-sudo=true
set -e
# No CLI is installed here on purpose. Hermes Desktop only runs against a
# runtime built from its own commit, so it provisions one itself under
# ~/.hermes on first launch, which takes a few minutes and shows its own
# progress. Handing it the mise CLI instead fails: PyPI trails the tags, and
# the version gap fails the app's readiness probe with a 401.
echo "Installing Hermes Desktop..."
omarchy-pkg-add hermes-desktop
# If Hermes was already installed for the terminal, the app supersedes it: one
# machine, one Hermes. This drops that copy so the terminal, the default agent
# and the app all end up on the app's installation.
omarchy-install-hermes-cli || true
echo "Opening Hermes Desktop..."
setsid uwsm-app -- /usr/bin/hermes-desktop >/dev/null 2>&1 &
echo ""
echo "Hermes Desktop has been installed."
echo "Its first launch installs the Hermes runtime, which takes a few minutes."
+229
View File
@@ -0,0 +1,229 @@
#!/bin/bash
# omarchy:summary=Install the Hermes CLI as a mise-backed wrapper in ~/.local/bin
# omarchy:args=[--check|--now|--owns]
# omarchy:examples=omarchy install hermes cli | omarchy install hermes cli --now
# Hermes pins every one of its dependencies exactly and declares
# Requires-Python >=3.11,<3.14, so it can neither be built against Arch's
# Python nor share the python-* packages. mise builds it a private environment
# instead.
#
# It gets its own installer rather than a line in omarchy-mise-install because
# of the interpreter pin. Given no compatible interpreter to hand, uv builds
# the venv against the system Python in violation of Hermes' own bound,
# reports success, and leaves the breakage to surface later inside a
# dependency -- and omarchy-mise-install writes a fixed stub with nowhere to
# say otherwise.
#
# There is only ever one Hermes on a machine. hermes-desktop cannot run against
# this one -- it needs a runtime built from its own commit, and the version gap
# fails its readiness probe -- so it installs its own under ~/.hermes and puts
# that on PATH. When the package is present it therefore owns Hermes outright:
# this installer stands aside and removes its own copy, so the terminal, the
# default agent and the app are all the same installation.
set -euo pipefail
mode=${1:-}
tool='pipx:hermes-agent[extras=all]'
python='3.13'
# The line that identifies the stub as this installer's; matched whole, so a
# wrapper that merely mentions the command is not mistaken for ours.
marker='# Written by omarchy-install-hermes-cli.'
# The package, not the runtime directory: it is installed before the app has
# ever run, and that is exactly when we must not start building a second copy.
desktop_owns_hermes() {
omarchy-pkg-present hermes-desktop
}
# The venv appears at the python-deps stage, several stages before the one that
# installs the command, so its presence says nothing about being usable. The
# marker is written last, and the command is what the agent actually runs.
desktop_hermes_ready() {
[[ -f $HOME/.hermes/hermes-agent/.hermes-bootstrap-complete ]] || return 1
# An executable of that name proves nothing about whose it is; the app's own
# points into ~/.hermes, and anything else is not the install we are asking
# about. Matched as a plain string, because the path carries a dot and an
# unanchored pattern would also claim a wrapper pointing at ~/xhermes.
[[ -f $HOME/.local/bin/hermes ]] || return 1
grep -qF "$HOME/.hermes" "$HOME/.local/bin/hermes" || return 1
# And a marker left behind by an install whose venv has since gone answers
# for nothing, so the command has to run, exactly as a foreign one must.
hermes_prompt_ready
}
# Whether Hermes is really installed, not merely whether the stub exists. A
# stub on its own is cold: running it installs Hermes, which takes minutes.
installed() {
[[ -d "$(mise where "$tool" 2>/dev/null)/hermes-agent/lib/python$python" ]]
}
# The stub is the only thing this installer owns. Anything else at that path
# -- Hermes' official installer, a hand-rolled wrapper, even a dangling link
# -- was put there by the user and is never deleted or overwritten here.
# Symlinks count as foreign even when they resolve to a marked file: the stub
# is written as a regular file, so a link is someone else's arrangement.
ours() {
[[ -f $HOME/.local/bin/hermes && ! -L $HOME/.local/bin/hermes ]] &&
grep -qxF "$marker" "$HOME/.local/bin/hermes"
}
foreign_hermes() {
[[ -e $HOME/.local/bin/hermes || -L $HOME/.local/bin/hermes ]] && ! ours
}
# A hermes at that path is usable when it is a command that runs: a regular
# executable whose --version answers. The executable bit alone proves little -- a directory
# passes -x on search permission, and a wrapper whose interpreter or target is
# gone passes it too. The desktop app applies the same probe with the same 15
# second budget, so what passes here is what it will use.
hermes_runs() {
[[ -f $HOME/.local/bin/hermes && -x $HOME/.local/bin/hermes ]] &&
timeout 15 "$HOME/.local/bin/hermes" --version >/dev/null 2>&1
}
# The chat subcommand's --oneshot opt-out arrived with native interactive -q,
# so its presence is a stable capability check without relying on a version.
hermes_prompt_ready() {
local help
hermes_runs &&
help=$(timeout 15 "$HOME/.local/bin/hermes" chat --help 2>/dev/null) &&
grep -qF -- '--oneshot' <<<"$help"
}
# --owns answers whether the wrapper on PATH is the one this command wrote, so
# the migration and Remove Preinstalls do not each carry their own copy of the
# marker and drift from it.
if [[ $mode == "--owns" ]]; then
if ours; then exit 0; else exit 1; fi
fi
# --check lets callers tell a cold stub from a working one before they commit
# to a path that assumes Hermes is ready.
if [[ $mode == "--check" ]]; then
if desktop_owns_hermes; then
if desktop_hermes_ready; then exit 0; else exit 1; fi
fi
# A foreign command is ready only when it also supports prompted sessions;
# since it is not ours to replace, nothing this installer does will update it.
if foreign_hermes; then
if hermes_prompt_ready; then exit 0; else exit 1; fi
fi
if installed && hermes_prompt_ready; then exit 0; else exit 1; fi
fi
# Hand Hermes over to the app rather than keeping a second copy beside it.
if desktop_owns_hermes; then
# Not gated on that copy being healthy: `mise up` can rebuild it against the
# wrong interpreter and a half-finished install answers to neither test, and
# either way it is still a second Hermes. Removing nothing is harmless.
if mise where "$tool" >/dev/null 2>&1; then
echo "Hermes Desktop provides Hermes; removing the separate CLI install..." >&2
fi
mise rm -g "$tool" >/dev/null 2>&1 || true
mise uninstall --all "$tool" >/dev/null 2>&1 || true
# Our own stub has to go with it. Left in place it still answers `hermes`
# until the app's bootstrap overwrites it, and answering means building the
# second Hermes this whole arrangement exists to avoid.
if ours; then
rm -f "$HOME/.local/bin/hermes"
fi
if desktop_hermes_ready; then
exit 0
fi
echo "Hermes Desktop is installed but has not set Hermes up yet." >&2
echo "Launch Hermes Desktop once to finish installing it." >&2
exit 1
fi
# The user already has a hermes of their own. Leave it be: a working one is
# what the default agent will run, and a broken one is theirs to fix.
if foreign_hermes; then
if hermes_prompt_ready; then
exit 0
fi
if hermes_runs; then
echo "~/.local/bin/hermes does not support the interactive seeded sessions Omarchy needs." >&2
echo "Update it to a Hermes Agent release with interactive chat queries, then run omarchy-install-hermes-cli again." >&2
exit 1
fi
echo "~/.local/bin/hermes exists but is not runnable, and it was not installed by Omarchy." >&2
echo "Fix or remove it, then run omarchy-install-hermes-cli again." >&2
exit 1
fi
# Only the marked wrapper proves the matching mise environment is ours to replace.
if installed && ! hermes_prompt_ready; then
if ours; then
echo "Updating Hermes for prompted sessions..." >&2
mise rm -g "$tool" >/dev/null 2>&1 || true
mise uninstall --all "$tool" >/dev/null 2>&1 || true
else
echo "A Hermes mise environment exists without an Omarchy-owned wrapper." >&2
echo "Update or remove it explicitly, then run omarchy-install-hermes-cli again." >&2
exit 1
fi
fi
mkdir -p "$HOME/.local/bin"
rm -f "$HOME/.local/bin/hermes"
cat >"$HOME/.local/bin/hermes" <<EOF
#!/bin/bash
$marker
export UV_PYTHON="$python"
# Exported rather than set on the install line alone, so the version resolved
# to run agrees with the one just installed. Hermes ships several times a week
# and mise's cooldown would otherwise hold a new release back for days.
export MISE_MINIMUM_RELEASE_AGE=0
# mise up -- which omarchy update runs -- reinstalls without that pin, so this
# asks which interpreter is actually there rather than whether anything is.
if ! [[ -d "\$(mise where '$tool' 2>/dev/null)/hermes-agent/lib/python$python" ]]; then
echo "Installing Hermes on Python $python (this takes a minute)..." >&2
# mise's pipx backend shells out to uv, which a stock Omarchy does not have.
# It is fetched here rather than when this stub was written, so setting up a
# machine that never runs Hermes costs nothing.
if omarchy-cmd-missing uv && ! mise where uv >/dev/null 2>&1; then
mise use -g --quiet uv@latest || exit 1
fi
mise use -g --quiet --force '$tool' || exit 1
fi
# The pin belongs to building Hermes, not to everything Hermes then runs.
# Exported it would reach the agent and every command it shells out to, so a
# uv in the user's own project would resolve 3.13 there too -- uv only warns
# when that contradicts the project's requires-python, and builds it anyway.
exec env -u UV_PYTHON mise x '$tool' -- hermes "\$@"
EOF
chmod +x "$HOME/.local/bin/hermes"
# The desktop app resolves a hermes on PATH by running `hermes --version` with
# a 15 second budget, then falls back to cloning its own copy when that times
# out. A first-run mise install does not fit in 15 seconds, so anything that
# hands Hermes to the GUI has to install it here rather than leave it stubbed.
if [[ $mode == "--now" ]]; then
"$HOME/.local/bin/hermes" --version
if ! hermes_prompt_ready; then
echo "Hermes installed without the interactive seeded sessions Omarchy needs." >&2
exit 1
fi
fi
+9 -1
View File
@@ -84,7 +84,7 @@ fi
# Dev-aware skill symlinks. Cannot live in /etc/skel because OMARCHY_PATH may
# point at a dev checkout (omarchy dev link) where the target differs.
# Loops every skill directory, so shipping a new one needs no edit here.
mkdir -p ~/.agents/skills ~/.claude/skills ~/.codex/skills ~/.pi/agent/skills ~/.gemini/config/skills
mkdir -p ~/.agents/skills ~/.claude/skills ~/.codex/skills ~/.pi/agent/skills ~/.gemini/config/skills ~/.hermes/skills
for skill in "$OMARCHY_PATH"/default/agents/skills/*/; do
skill=${skill%/}
name=${skill##*/}
@@ -93,6 +93,14 @@ for skill in "$OMARCHY_PATH"/default/agents/skills/*/; do
ln -sfn "$skill" ~/.codex/skills/"$name"
ln -sfn "$skill" ~/.pi/agent/skills/"$name"
ln -sfn "$skill" ~/.gemini/config/skills/"$name"
ln -sfn "$skill" ~/.hermes/skills/"$name"
if [[ -d ~/.hermes/profiles ]]; then
for profile in ~/.hermes/profiles/*/; do
[[ -d $profile ]] || continue
mkdir -p "$profile/skills"
ln -sfn "$skill" "$profile/skills/$name"
done
fi
done
mkdir -p ~/Downloads ~/Pictures ~/Videos ~/.config/gtk-3.0
+59
View File
@@ -0,0 +1,59 @@
#!/bin/bash
# omarchy:summary=Remove the Hermes desktop app along with the Hermes runtime it installed.
# omarchy:requires-sudo=true
# -u so an unset HOME is an error rather than a set of rm -rf paths rooted at /.
set -euo pipefail
omarchy-pkg-drop hermes-desktop
# The app writes this when the runtime it provisions under ~/.hermes has landed,
# and it is the only thing that tells that runtime apart from one the user
# installed themselves -- the paths are the same either way. Without it the app
# never got that far: a machine where it was installed but never launched still
# has whatever was there before, and none of it is ours to delete.
if [[ -f $HOME/.hermes/hermes-agent/.hermes-bootstrap-complete ]]; then
# The checkout and venv, its own uv, its own node. None of it is any use once
# the app is gone. Not ~/.config/Hermes, which holds the gateway connections
# and their encrypted tokens, the active profile and the update settings. Not
# the rest of ~/.hermes either: the chats, memories and the skills Hermes
# wrote for itself are the user's, they are small, and finding them still
# there after a reinstall is the better surprise.
rm -rf \
"$HOME/.hermes/hermes-agent" \
"$HOME/.hermes/bootstrap-cache" \
"$HOME/.hermes/bin" \
"$HOME/.hermes/node"
# Only the wrappers pointing into ~/.hermes, matched as a plain string: the
# path carries a dot, so an unanchored pattern would also claim a wrapper
# pointing at a sibling like ~/xhermes.
for command in hermes hermes-agent hermes-acp; do
wrapper="$HOME/.local/bin/$command"
if [[ -f $wrapper && ! -L $wrapper ]] && grep -qF "$HOME/.hermes" "$wrapper"; then
rm -f "$wrapper"
fi
done
# When Hermes brought its own Node it symlinked these next to its own commands,
# and they point at what we just deleted. Only the links into ~/.hermes: a
# system Node, or someone else's, lives somewhere else entirely.
for command in node npm npx; do
link="$HOME/.local/bin/$command"
if [[ -L $link && $(readlink "$link") == "$HOME/.hermes"/* ]]; then
rm -f "$link"
fi
done
echo ""
echo "Hermes Desktop has been removed."
echo "Your chats, memories, and skills are still in ~/.hermes,"
echo "and your connections and settings in ~/.config/Hermes."
else
echo ""
echo "Hermes Desktop has been removed."
echo "It never finished installing its own Hermes, so nothing in ~/.hermes was touched."
fi
+8
View File
@@ -17,6 +17,14 @@ if gum confirm "Are you sure you want to remove all preinstalled web apps, TUI w
~/.local/bin/gh ~/.local/bin/opencode ~/.local/bin/playwright ~/.local/bin/playwright-cli ~/.local/bin/pi \
~/.local/bin/omp ~/.local/bin/ori ~/.local/bin/grok ~/.local/bin/crush ~/.local/bin/ghui ~/.local/bin/hunk
# Only the wrapper omarchy-install-hermes-cli wrote is a preinstall. Hermes
# Desktop's command, an official install, or anything else at that path is
# the user's, so it is the installer that decides whether the wrapper is its
# own, rather than a copy of its marker kept here.
if omarchy-install-hermes-cli --owns; then
rm -f ~/.local/bin/hermes
fi
omarchy-pkg-drop \
aether \
cliamp \