From efae525dc5c70523527bcd3ed2c15630f0744daa Mon Sep 17 00:00:00 2001 From: Wing Lian Date: Mon, 30 Mar 2026 05:48:06 -0400 Subject: [PATCH] feat(plugins): add inject_message interface for remote message injection (#3778) --- cli.py | 5 +++++ hermes_cli/plugins.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/cli.py b/cli.py index 3371b9821..4c76ff142 100644 --- a/cli.py +++ b/cli.py @@ -6210,6 +6210,11 @@ class HermesCLI: self._interrupt_queue = queue.Queue() # For messages typed while agent is running self._should_exit = False self._last_ctrl_c_time = 0 # Track double Ctrl+C for force exit + + # Give plugin manager a CLI reference so plugins can inject messages + from hermes_cli.plugins import get_plugin_manager + get_plugin_manager()._cli_ref = self + # Config file watcher — detect mcp_servers changes and auto-reload from hermes_cli.config import get_config_path as _get_config_path _cfg_path = _get_config_path() diff --git a/hermes_cli/plugins.py b/hermes_cli/plugins.py index c72bc59e7..0146014f3 100644 --- a/hermes_cli/plugins.py +++ b/hermes_cli/plugins.py @@ -152,6 +152,34 @@ class PluginContext: self._manager._plugin_tool_names.add(name) logger.debug("Plugin %s registered tool: %s", self.manifest.name, name) + # -- message injection -------------------------------------------------- + + def inject_message(self, content: str, role: str = "user") -> bool: + """Inject a message into the active conversation. + + If the agent is idle (waiting for user input), this starts a new turn. + If the agent is running, this interrupts and injects the message. + + This enables plugins (e.g. remote control viewers, messaging bridges) + to send messages into the conversation from external sources. + + Returns True if the message was queued successfully. + """ + cli = self._manager._cli_ref + if cli is None: + logger.warning("inject_message: no CLI reference (not available in gateway mode)") + return False + + msg = content if role == "user" else f"[{role}] {content}" + + if getattr(cli, "_agent_running", False): + # Agent is mid-turn — interrupt with the message + cli._interrupt_queue.put(msg) + else: + # Agent is idle — queue as next input + cli._pending_input.put(msg) + return True + # -- hook registration -------------------------------------------------- def register_hook(self, hook_name: str, callback: Callable) -> None: @@ -184,6 +212,7 @@ class PluginManager: self._hooks: Dict[str, List[Callable]] = {} self._plugin_tool_names: Set[str] = set() self._discovered: bool = False + self._cli_ref = None # Set by CLI after plugin discovery # ----------------------------------------------------------------------- # Public