feat(pets): split /pets (list) from /pet (select)

/pets is the collection — installed pets with the active one marked, plus
/pets gallery to browse the petdex catalog. /pet is selection only: /pet
<slug> to bring one out, /pet off to put it away, bare /pet for status.

Both are cli_only — a sprite can't render in a Telegram/Slack message (which
is why /pet never had a gateway handler), so they stay on the CLI/TUI/desktop
surfaces and out of the messaging menus, keeping Slack under its 50-slash cap.
This commit is contained in:
Brooklyn Nicholson 2026-06-15 12:50:26 -05:00
parent c176054c33
commit 0586c8073e
4 changed files with 54 additions and 26 deletions

View File

@ -121,7 +121,8 @@ const DESKTOP_COMMAND_SPECS: readonly DesktopCommandSpec[] = [
{ name: '/debug', description: 'Create a debug report', surface: exec() },
{ name: '/goal', description: 'Manage the standing goal for this session', surface: exec() },
{ name: '/personality', description: 'Switch personality for this session', surface: exec(), args: true },
{ name: '/pet', description: 'Adopt an animated petdex mascot (/pet boba, /pet off)', surface: exec(), args: true },
{ name: '/pet', description: 'Bring out a petdex mascot (/pet boba, /pet off)', surface: exec(), args: true },
{ name: '/pets', description: 'List your pets, or browse the petdex gallery', surface: exec(), args: true },
{ name: '/queue', description: 'Queue a prompt for the next turn', aliases: ['/q'], surface: exec() },
{ name: '/retry', description: 'Retry the last user message', surface: exec() },
{ name: '/rollback', description: 'List or restore filesystem checkpoints', surface: exec() },

2
cli.py
View File

@ -7607,6 +7607,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
self._handle_personality_command(cmd_original)
elif canonical == "pet":
self._handle_pet_command(cmd_original)
elif canonical == "pets":
self._handle_pets_command(cmd_original)
elif canonical == "retry":
retry_msg = self.retry_last()
if retry_msg and hasattr(self, '_pending_input'):

View File

@ -1010,12 +1010,11 @@ class CLICommandsMixin:
print()
def _handle_pet_command(self, cmd: str):
"""Install / select / disable an animated petdex mascot.
"""Bring out a petdex mascot (selection only — see ``/pets`` for the list).
``/pet`` or ``/pet status`` show current pet + installed list
``/pet <slug>`` install (if needed) + make active
``/pet off`` disable the pet display
``/pet list`` browse the petdex gallery (first 20)
``/pet <slug>`` adopt (install if needed) + make active
``/pet off`` put the pet away
``/pet`` show the active pet + a pointer to ``/pets``
Writes ``display.pet.*`` to config; the CLI/TUI/desktop pet surfaces
pick the change up on their next poll, so the pet appears shortly.
@ -1028,23 +1027,42 @@ class CLICommandsMixin:
arg = parts[1].strip() if len(parts) > 1 else ""
low = arg.lower()
if not arg or low == "status":
if not arg:
cfg = _pet_config()
installed = [p.slug for p in store.installed_pets()]
active = store.resolve_active_pet(str(cfg.get("slug", "") or ""))
state = "on" if cfg.get("enabled") else "off"
print(f"(^_^) Pet: {state} · active: {active.slug if active else 'none'}")
print(f" installed: {', '.join(installed) or 'none (try /pet boba)'}")
print(" Usage: /pet <slug> · /pet off · /pet list")
print(f"(^_^) Pet: {state} · active: {active.display_name if active else 'none'}")
print(" /pet <slug> to bring one out · /pet off · /pets for your collection")
return
if low == "off":
_set_enabled(False)
print("(-_-)zzZ Pet disabled.")
print("(-_-)zzZ Pet put away.")
return
if low == "list":
from agent.pet.manifest import fetch_manifest
print(f"(o_o) Fetching '{arg}' from petdex…")
try:
pet = store.install_pet(arg)
except (store.PetStoreError, ManifestError) as exc:
print(f"(x_x) Couldn't adopt '{arg}': {exc}")
return
_set_active(arg)
print(f"(^_^)b {pet.display_name} is out — it'll pop in shortly.")
def _handle_pets_command(self, cmd: str):
"""List your pets, or browse the petdex gallery.
``/pets`` your installed pets (active one marked)
``/pets gallery`` browse the petdex catalog (first 20)
"""
from agent.pet import store
from agent.pet.manifest import ManifestError, fetch_manifest
from hermes_cli.pets import _pet_config
parts = cmd.split(maxsplit=1)
arg = parts[1].strip().lower() if len(parts) > 1 else ""
if arg in ("gallery", "all", "browse"):
try:
entries = fetch_manifest()
except ManifestError as exc:
@ -1053,20 +1071,25 @@ class CLICommandsMixin:
installed = {p.slug for p in store.installed_pets()}
print(f"(^o^)/ petdex gallery — first 20 of {len(entries)}:")
for entry in entries[:20]:
mark = "" if entry.slug in installed else " "
mark = "" if entry.slug in installed else ""
print(f" {mark} {entry.slug:<24} {entry.display_name}")
print(" Adopt one with: /pet <slug>")
print(" /pet <slug> to bring one out")
return
# Treat the argument as a slug: install (if needed) + activate.
print(f"(o_o) Fetching '{arg}' from petdex…")
try:
pet = store.install_pet(arg)
except (store.PetStoreError, ManifestError) as exc:
print(f"(x_x) Couldn't adopt '{arg}': {exc}")
cfg = _pet_config()
active = store.resolve_active_pet(str(cfg.get("slug", "") or ""))
active_slug = active.slug if active else ""
installed = store.installed_pets()
state = "on" if cfg.get("enabled") else "off"
print(f"(^_^) Your pets ({state}):")
if not installed:
print(" none yet — /pet boba to adopt one · /pets gallery to browse")
return
_set_active(arg)
print(f"(^_^)b {pet.display_name} adopted and set as your active pet — it'll pop in shortly.")
for p in installed:
mark = "" if p.slug == active_slug else " "
print(f" {mark} {p.slug:<24} {p.display_name}")
print(" /pet <slug> to switch · /pet off · /pets gallery to browse")
def _handle_cron_command(self, cmd: str):
"""Handle the /cron command to manage scheduled tasks."""

View File

@ -176,8 +176,10 @@ COMMAND_REGISTRY: list[CommandDef] = [
subcommands=("pending", "approve", "reject", "approval")),
CommandDef("bundles", "List skill bundles (aliases /<name> for multiple skills)",
"Tools & Skills"),
CommandDef("pet", "Install/select an animated petdex mascot", "Tools & Skills",
args_hint="[slug|off|list]", subcommands=("off", "list", "status")),
CommandDef("pet", "Bring out a petdex mascot (/pet <slug>, /pet off)", "Tools & Skills",
cli_only=True, args_hint="<slug|off>", subcommands=("off",)),
CommandDef("pets", "List your pets, or browse the petdex gallery", "Tools & Skills",
cli_only=True, args_hint="[gallery]", subcommands=("gallery",)),
CommandDef("cron", "Manage scheduled tasks", "Tools & Skills",
cli_only=True, args_hint="[subcommand]",
subcommands=("list", "add", "create", "edit", "pause", "resume", "run", "remove")),