Upload
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
"""Commands that drive the pet's *body* instead of the shell.
|
||||
|
||||
The server relays shell commands to this machine (see server_client.
|
||||
run_local_command). Rather than inventing a new payload type the desk API
|
||||
doesn't speak — this client can't change the server — a small `petctl`
|
||||
pseudo-command is intercepted before it ever reaches `subprocess`: if Bolt
|
||||
emits `petctl move top-left` or `petctl emote wave`, the pet does it and
|
||||
returns a normal-looking command output string, so from the server's side
|
||||
it's just another tool call that worked.
|
||||
|
||||
Pure parsing logic — no Qt, no subprocess — so it's cheap to unit test. The
|
||||
UI half lives in ui/pet_window.py (apply_action).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shlex
|
||||
from typing import Optional
|
||||
|
||||
# What Bolt is allowed to type. Anything else falls through to a real shell.
|
||||
_PREFIXES = ("petctl", "bolt-pet", "pet")
|
||||
|
||||
ANCHORS = (
|
||||
"top-left", "top", "top-right",
|
||||
"left", "center", "right",
|
||||
"bottom-left", "bottom", "bottom-right",
|
||||
"cursor", "random",
|
||||
)
|
||||
|
||||
EMOTES = ("wave", "hop", "spin", "nod", "shake", "bounce", "wiggle")
|
||||
|
||||
HELP = (
|
||||
"petctl move <x> <y> | <" + "|".join(ANCHORS) + ">\n"
|
||||
"petctl emote <" + "|".join(EMOTES) + ">\n"
|
||||
"petctl say <text>\n"
|
||||
"petctl wander on|off\n"
|
||||
"petctl nap on|off"
|
||||
)
|
||||
|
||||
|
||||
class ActionError(Exception):
|
||||
"""Bad petctl syntax — reported back to the server as command output."""
|
||||
|
||||
|
||||
def is_pet_command(command: str) -> bool:
|
||||
parts = (command or "").strip().split()
|
||||
return bool(parts) and parts[0].lower() in _PREFIXES
|
||||
|
||||
|
||||
def _bool_arg(value: str) -> bool:
|
||||
value = value.lower()
|
||||
if value in ("on", "true", "yes", "1", "start", "enable"):
|
||||
return True
|
||||
if value in ("off", "false", "no", "0", "stop", "disable"):
|
||||
return False
|
||||
raise ActionError(f"expected on/off, got {value!r}")
|
||||
|
||||
|
||||
def parse(command: str) -> Optional[dict]:
|
||||
"""Parse a `petctl ...` string into an action dict, or None if this isn't
|
||||
a pet command at all (caller should run it as a real shell command).
|
||||
Raises ActionError on a pet command that doesn't make sense."""
|
||||
if not is_pet_command(command):
|
||||
return None
|
||||
try:
|
||||
parts = shlex.split(command.strip())
|
||||
except ValueError as exc: # unbalanced quotes
|
||||
raise ActionError(f"couldn't parse arguments: {exc}") from exc
|
||||
verb = (parts[1].lower() if len(parts) > 1 else "help")
|
||||
args = parts[2:]
|
||||
|
||||
if verb in ("help", "-h", "--help"):
|
||||
return {"action": "help"}
|
||||
|
||||
if verb in ("move", "goto", "walk"):
|
||||
if not args:
|
||||
raise ActionError("move needs a target: " + ", ".join(ANCHORS) + ", or x y")
|
||||
if len(args) >= 2 and _looks_numeric(args[0]) and _looks_numeric(args[1]):
|
||||
return {"action": "move", "x": int(float(args[0])), "y": int(float(args[1]))}
|
||||
anchor = args[0].lower().replace("_", "-")
|
||||
if anchor not in ANCHORS:
|
||||
raise ActionError(f"unknown position {args[0]!r}; try one of: " + ", ".join(ANCHORS))
|
||||
return {"action": "move", "anchor": anchor}
|
||||
|
||||
if verb in ("emote", "do"):
|
||||
if not args:
|
||||
raise ActionError("emote needs a name: " + ", ".join(EMOTES))
|
||||
emote = args[0].lower()
|
||||
if emote not in EMOTES:
|
||||
raise ActionError(f"unknown emote {args[0]!r}; try one of: " + ", ".join(EMOTES))
|
||||
return {"action": "emote", "emote": emote}
|
||||
|
||||
if verb == "say":
|
||||
text = " ".join(args).strip()
|
||||
if not text:
|
||||
raise ActionError("say needs something to say")
|
||||
return {"action": "say", "text": text}
|
||||
|
||||
if verb == "wander":
|
||||
if not args:
|
||||
raise ActionError("wander needs on or off")
|
||||
return {"action": "wander", "enabled": _bool_arg(args[0])}
|
||||
|
||||
if verb in ("nap", "sleep", "dnd"):
|
||||
if not args:
|
||||
raise ActionError("nap needs on or off")
|
||||
return {"action": "nap", "enabled": _bool_arg(args[0])}
|
||||
|
||||
raise ActionError(f"unknown petctl verb {verb!r}\n{HELP}")
|
||||
|
||||
|
||||
def _looks_numeric(value: str) -> bool:
|
||||
try:
|
||||
float(value)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def describe(action: dict) -> str:
|
||||
"""The text handed back to the server as this "command"'s output. Phrased
|
||||
as a completed fact so the model doesn't narrate the mechanics of it."""
|
||||
kind = action.get("action")
|
||||
if kind == "move":
|
||||
where = action.get("anchor") or f"({action.get('x')}, {action.get('y')})"
|
||||
return f"[pet] walking to {where}"
|
||||
if kind == "emote":
|
||||
return f"[pet] {action['emote']}"
|
||||
if kind == "say":
|
||||
return "[pet] showing that in the speech bubble"
|
||||
if kind == "wander":
|
||||
return "[pet] wandering " + ("enabled" if action["enabled"] else "disabled")
|
||||
if kind == "nap":
|
||||
return "[pet] " + ("napping" if action["enabled"] else "awake")
|
||||
if kind == "help":
|
||||
return HELP
|
||||
return "[pet] ok"
|
||||
Reference in New Issue
Block a user