diff --git a/bin/omarchy-install-ai-ghost b/bin/omarchy-install-ai-ghost new file mode 100755 index 00000000000..50170e7cca7 --- /dev/null +++ b/bin/omarchy-install-ai-ghost @@ -0,0 +1,25 @@ +#!/bin/bash + +# omarchy:summary=Install Ghost and start it for this login +# omarchy:requires-sudo=true + +set -euo pipefail + +echo "Installing Ghost..." +omarchy-pkg-add ghost + +# The package carries the daemon, the HUD plugin, the CLI, the browser relay, +# and the computer-use helper. Two things need this login's hand: the daemon is +# a user unit, and the HUD is an omarchy-shell plugin, so it is linked into this +# user's plugins directory and enabled in the shell already running. +echo "Starting Ghost for this login..." +systemctl --user daemon-reload +systemctl --user enable --now ghostd.service + +mkdir -p ~/.config/omarchy/plugins +ln -sfn /usr/share/ghost/plugin ~/.config/omarchy/plugins/ferdousbhai.ghost +omarchy-shell shell rescanPlugins +omarchy plugin enable ferdousbhai.ghost + +echo "" +echo "Ghost has been installed. Press Super + Ctrl + G to summon it." diff --git a/bin/omarchy-remove-ai-ghost b/bin/omarchy-remove-ai-ghost new file mode 100755 index 00000000000..8be725f5fc9 --- /dev/null +++ b/bin/omarchy-remove-ai-ghost @@ -0,0 +1,19 @@ +#!/bin/bash + +# omarchy:summary=Remove Ghost, keeping every ghost home and credential in place. +# omarchy:requires-sudo=true + +set -euo pipefail + +# The HUD plugin and the daemon hold the package's files open, so both go first. +# `plugin remove` disables it and unlinks the symlink; the rm covers a shell +# that is not running to answer. +omarchy plugin remove ferdousbhai.ghost --yes 2>/dev/null || true +rm -f "$HOME/.config/omarchy/plugins/ferdousbhai.ghost" +systemctl --user disable --now ghostd.service 2>/dev/null || true +systemctl --user daemon-reload + +omarchy-pkg-drop ghost + +echo "" +echo "Ghost has been removed. Your ghosts (~/ghosts), settings, and state are still there." diff --git a/default/omarchy/omarchy-menu.jsonc b/default/omarchy/omarchy-menu.jsonc index 7583d8d8ce9..c8243cbea57 100644 --- a/default/omarchy/omarchy-menu.jsonc +++ b/default/omarchy/omarchy-menu.jsonc @@ -242,6 +242,7 @@ "install.terminal.kitty": {"icon":"","label":"Kitty","disabled":"omarchy-pkg-present kitty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal kitty'"}, "install.ai.chatgpt": {"icon":"","iconFont":"omarchy","label":"ChatGPT Desktop","disabled":"omarchy-pkg-present openai-codex-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-chatgpt"}, "install.ai.claude": {"icon":"","iconFont":"omarchy","label":"Claude Desktop","disabled":"omarchy-pkg-present claude-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-claude"}, + "install.ai.ghost": {"icon":"󰊠","label":"Ghost","disabled":"omarchy-pkg-present ghost","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-ghost"}, "install.ai.dictation": {"icon":"","label":"Dictation","disabled":"omarchy-pkg-present voxtype-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install"}, "install.ai.grok-bot": {"icon":"","iconFont":"omarchy","label":"Grok Bot","disabled":"omarchy-pkg-present grok-bot","action":"omarchy-install-and-launch 'Grok Bot' grok-bot grok-bot"}, "install.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","disabled":"omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-hermes"}, @@ -311,6 +312,7 @@ "remove.service.tailscale": {"icon":"","label":"Tailscale","when":"omarchy-pkg-present tailscale","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-service-tailscale"}, "remove.ai.chatgpt": {"icon":"","iconFont":"omarchy","label":"ChatGPT Desktop","when":"omarchy-pkg-present openai-codex-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-chatgpt"}, "remove.ai.claude": {"icon":"","iconFont":"omarchy","label":"Claude Desktop","when":"omarchy-pkg-present claude-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-claude"}, + "remove.ai.ghost": {"icon":"󰊠","label":"Ghost","when":"omarchy-pkg-present ghost","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-ghost"}, "remove.ai.dictation": {"icon":"","label":"Dictation","when":"omarchy-pkg-present voxtype-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-remove"}, "remove.ai.grok-bot": {"icon":"","iconFont":"omarchy","label":"Grok Bot","when":"omarchy-pkg-present grok-bot","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-grok-bot"}, "remove.ai.lm-studio": {"icon":"","iconFont":"omarchy","label":"LM Studio","when":"omarchy-pkg-present lmstudio-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-lm-studio"}, diff --git a/manual/17-ai.md b/manual/17-ai.md index 8c3d48b024c..3162d9fd89f 100644 --- a/manual/17-ai.md +++ b/manual/17-ai.md @@ -48,7 +48,7 @@ Crashes can also be silenced one program at a time, which is what the diagnosis ### Desktop apps -The _Install > AI_ menu also carries a few graphical AI apps: the ChatGPT desktop app, the Claude desktop app (Anthropic's Linux beta, with Chat, Cowork, and Claude Code tabs), Grok Bot for chatting with xAI's models, Hermes Desktop, OpenClaw, and the Perplexity desktop app. +The _Install > AI_ menu also carries a few graphical AI apps: the ChatGPT desktop app, the Claude desktop app (Anthropic's Linux beta, with Chat, Cowork, and Claude Code tabs), Ghost (a local AI persona with its own HUD, summoned with `Super + Ctrl + G`), Grok Bot for chatting with xAI's models, Hermes Desktop, OpenClaw, and the Perplexity desktop app. Hermes Desktop is the one to know about, because there is only ever one Hermes on a machine. The app only runs against a runtime built from its own commit, so it installs one of its own under `~/.hermes` on first launch, which takes a few minutes and shows its own progress. From then on that is the Hermes the terminal `hermes` command and the default agent use too, whichever order you installed them in. Installing it also hands Hermes the Omarchy theme as a skin named `omarchy`, which every Hermes surface follows as you switch themes; pick another under Hermes' Appearance settings or with `/skin` if you'd rather it didn't, and Omarchy leaves that choice alone. Removing the app under _Remove > AI_ takes that runtime with it, and keeps your chats, memories, and the skills Hermes wrote for itself unless you tell it otherwise: it asks, defaulting to no, whether that data and your connection settings should go too. diff --git a/test/shell.d/ghost-install-test.sh b/test/shell.d/ghost-install-test.sh new file mode 100755 index 00000000000..720bd796623 --- /dev/null +++ b/test/shell.d/ghost-install-test.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +mkdir -p "$tmp_dir/bin" "$tmp_dir/home" +export TEST_LOG="$tmp_dir/log" +export HOME="$tmp_dir/home" + +# Package installation and the user manager are stubbed; the script's job is +# the order between them. +cat >"$tmp_dir/bin/omarchy-pkg-add" <<'SCRIPT' +#!/bin/bash +printf 'add:%s\n' "$*" >>"$TEST_LOG" +SCRIPT +cat >"$tmp_dir/bin/systemctl" <<'SCRIPT' +#!/bin/bash +printf 'systemctl:%s\n' "$*" >>"$TEST_LOG" +SCRIPT +# The HUD is an omarchy-shell plugin, so the install also talks to the running +# shell. Both are stubbed; the script's job is the order between them. +cat >"$tmp_dir/bin/omarchy-shell" <<'SCRIPT' +#!/bin/bash +printf 'omarchy-shell:%s\n' "$*" >>"$TEST_LOG" +SCRIPT +cat >"$tmp_dir/bin/omarchy" <<'SCRIPT' +#!/bin/bash +printf 'omarchy:%s\n' "$*" >>"$TEST_LOG" +SCRIPT +chmod +x "$tmp_dir/bin"/* +export PATH="$tmp_dir/bin:$PATH" + +: >"$TEST_LOG" +"$ROOT/bin/omarchy-install-ai-ghost" >"$tmp_dir/output" 2>&1 || fail "Ghost install succeeds" "$(cat "$tmp_dir/output")" + +grep -q '^add:ghost$' "$TEST_LOG" || fail "Ghost install adds the ghost package" +pass "Ghost install adds the ghost package" + +grep -q '^systemctl:--user enable --now ghostd.service$' "$TEST_LOG" || + fail "Ghost install enables and starts the daemon unit" +pass "Ghost install enables and starts the daemon unit" + +grep -q '^omarchy:plugin enable ferdousbhai.ghost$' "$TEST_LOG" || + fail "Ghost install enables the HUD plugin in the running shell" +pass "Ghost install enables the HUD plugin in the running shell" + +[[ -L $HOME/.config/omarchy/plugins/ferdousbhai.ghost ]] || + fail "Ghost install links the packaged plugin into the user's plugins directory" +pass "Ghost install links the packaged plugin into the user's plugins directory" + +[[ $(grep -n '^add:ghost$' "$TEST_LOG" | cut -d: -f1) -lt $(grep -n 'enable --now' "$TEST_LOG" | cut -d: -f1) ]] || + fail "Ghost install starts the daemon only after the package is present" +pass "Ghost install starts the daemon only after the package is present" + +grep -q 'Super + Ctrl + G' "$tmp_dir/output" || fail "Ghost install names the summon key" +pass "Ghost install names the summon key" + +# A failed package install must not try to start units that do not exist. +cat >"$tmp_dir/bin/omarchy-pkg-add" <<'SCRIPT' +#!/bin/bash +printf 'add:%s\n' "$*" >>"$TEST_LOG" +exit 1 +SCRIPT +: >"$TEST_LOG" +rc=0 +"$ROOT/bin/omarchy-install-ai-ghost" >/dev/null 2>&1 || rc=$? +[[ $rc != 0 ]] || fail "Ghost install aborts when the package cannot be added" +! grep -q 'enable --now' "$TEST_LOG" || fail "Ghost install aborts when the package cannot be added" "units enabled anyway" +pass "Ghost install aborts when the package cannot be added" diff --git a/test/shell.d/remove-ai-test.sh b/test/shell.d/remove-ai-test.sh index d81e6ca6811..72d706bb1df 100644 --- a/test/shell.d/remove-ai-test.sh +++ b/test/shell.d/remove-ai-test.sh @@ -378,3 +378,47 @@ rc=0 ! grep -q '^drop:openclaw$' "$TEST_LOG" || fail "OpenClaw removal aborts when systemd cannot be reached" "package dropped anyway" pass "OpenClaw removal aborts when systemd cannot be reached" + +# Ghost's removal takes the package, its daemon unit, and its HUD plugin, and +# nothing of the owner's: ghost homes, credentials, and daemon state stay where +# they are. +cat >"$tmp_dir/bin/systemctl" <<'SCRIPT' +#!/bin/bash +printf 'systemctl:%s\n' "$*" >>"$TEST_LOG" +SCRIPT +cat >"$tmp_dir/bin/omarchy" <<'SCRIPT' +#!/bin/bash +printf 'omarchy:%s\n' "$*" >>"$TEST_LOG" +SCRIPT +chmod +x "$tmp_dir/bin/systemctl" "$tmp_dir/bin/omarchy" +cat >"$tmp_dir/bin/omarchy-pkg-present" <<'SCRIPT' +#!/bin/bash +[[ $1 == ghost ]] +SCRIPT +chmod +x "$tmp_dir/bin/omarchy-pkg-present" + +: >"$TEST_LOG" +fresh_home +mkdir -p "$HOME/ghosts/casper" "$HOME/.config/ghost" "$HOME/.local/state/ghost" \ + "$HOME/.config/omarchy/plugins" +ln -sfn /usr/share/ghost/plugin "$HOME/.config/omarchy/plugins/ferdousbhai.ghost" +"$ROOT/bin/omarchy-remove-ai-ghost" >/dev/null + +grep -q '^drop:ghost$' "$TEST_LOG" || fail "Ghost removal drops the package" +pass "Ghost removal drops the package" + +grep -q '^systemctl:--user disable --now ghostd.service$' "$TEST_LOG" || + fail "Ghost removal stops the daemon before the package goes" +pass "Ghost removal stops the daemon before the package goes" + +grep -q '^omarchy:plugin remove ferdousbhai.ghost --yes$' "$TEST_LOG" || + fail "Ghost removal takes the HUD plugin out of the running shell" +pass "Ghost removal takes the HUD plugin out of the running shell" + +[[ ! -e $HOME/.config/omarchy/plugins/ferdousbhai.ghost ]] || + fail "Ghost removal unlinks the plugin from the user's plugins directory" +pass "Ghost removal unlinks the plugin from the user's plugins directory" + +[[ -d $HOME/ghosts/casper && -d $HOME/.config/ghost && -d $HOME/.local/state/ghost ]] || + fail "Ghost removal keeps ghost homes, settings, and state" +pass "Ghost removal keeps ghost homes, settings, and state"