hermes-mobile-plugin 0.0.15__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. hermes_mobile_plugin-0.0.15/LICENSE +21 -0
  2. hermes_mobile_plugin-0.0.15/PKG-INFO +135 -0
  3. hermes_mobile_plugin-0.0.15/README.md +117 -0
  4. hermes_mobile_plugin-0.0.15/pyproject.toml +45 -0
  5. hermes_mobile_plugin-0.0.15/setup.cfg +4 -0
  6. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/__init__.py +215 -0
  7. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/audio.py +253 -0
  8. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/audio_routes.py +502 -0
  9. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/cli.py +473 -0
  10. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/commands_routes.py +166 -0
  11. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/config.py +102 -0
  12. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/constants.py +122 -0
  13. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/ip_detector.py +127 -0
  14. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/plugin.yaml +18 -0
  15. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/providers_routes.py +182 -0
  16. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/qr_generator.py +276 -0
  17. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/supervisor.py +595 -0
  18. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/system_routes.py +448 -0
  19. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/update_routes.py +268 -0
  20. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin.egg-info/PKG-INFO +135 -0
  21. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin.egg-info/SOURCES.txt +31 -0
  22. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin.egg-info/dependency_links.txt +1 -0
  23. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin.egg-info/entry_points.txt +3 -0
  24. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin.egg-info/requires.txt +8 -0
  25. hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin.egg-info/top_level.txt +1 -0
  26. hermes_mobile_plugin-0.0.15/tests/test_commands_routes.py +97 -0
  27. hermes_mobile_plugin-0.0.15/tests/test_config.py +166 -0
  28. hermes_mobile_plugin-0.0.15/tests/test_core.py +105 -0
  29. hermes_mobile_plugin-0.0.15/tests/test_cross_platform.py +109 -0
  30. hermes_mobile_plugin-0.0.15/tests/test_install.py +80 -0
  31. hermes_mobile_plugin-0.0.15/tests/test_ip_detector.py +125 -0
  32. hermes_mobile_plugin-0.0.15/tests/test_qr_generator.py +213 -0
  33. hermes_mobile_plugin-0.0.15/tests/test_security.py +93 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sachin Taware
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: hermes-mobile-plugin
3
+ Version: 0.0.15
4
+ Summary: Hermes Mobile QR Plugin - Generates QR code for direct 8642 connection
5
+ Author: Sachin Taware
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: pyyaml>=6.0
11
+ Requires-Dist: qrcode>=7.4
12
+ Requires-Dist: aiohttp>=3.8
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=7.0; extra == "dev"
15
+ Requires-Dist: black; extra == "dev"
16
+ Requires-Dist: ruff; extra == "dev"
17
+ Dynamic: license-file
18
+
19
+ # Hermes Mobile Plugin
20
+
21
+ <p align="center">
22
+ <img src="https://img.shields.io/github/v/tag/tawaresachin/hermes-mobile-plugin?label=version&color=blue&style=for-the-badge" alt="version">
23
+ <img src="https://img.shields.io/github/actions/workflow/status/tawaresachin/hermes-mobile-plugin/ci.yml?branch=main&label=CI&style=for-the-badge" alt="CI">
24
+ <img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge" alt="MIT">
25
+ <img src="https://img.shields.io/badge/platforms-Windows%20%7C%20macOS%20%7C%20Linux%20%7C%20Android-blueviolet?style=for-the-badge" alt="platforms">
26
+ <img src="https://img.shields.io/badge/Python-3.10+-yellow?style=for-the-badge" alt="python">
27
+ </p>
28
+
29
+ The server-side half of **Hermes Mobile**: one plugin that turns your running
30
+ [Hermes Agent](https://hermes-agent.nousresearch.com) into a phone-ready hub — QR pairing, voice,
31
+ files, and server management. No extra daemon, no cloud relay: everything is mounted on the
32
+ gateway you already run (default port **8642**).
33
+
34
+ > You install **Hermes Agent** from the official site, then this **one plugin**. That's the whole
35
+ > server-side setup.
36
+
37
+ ## What you get
38
+
39
+ <img src="docs/screenshots/qr-pairing.jpg" width="360" alt="Pairing screen">
40
+
41
+ *Settings in the app after scanning the QR: server URL + key configured, connection live.*
42
+
43
+ | Capability | What it means for you |
44
+ |---|---|
45
+ | **QR pairing** | `install` prints/opens a QR with URL + API key; the app scans it (camera or gallery) and configures + tests the connection itself. |
46
+ | **Voice on-device** | Speech-to-text (Whisper) and text-to-speech routes on the gateway, so the phone mic/speaker work with zero extra services. |
47
+ | **File transfer** | Upload attachments from the phone, download anything the agent creates — the chat shows real file cards, previewable in-app. |
48
+ | **Context meter** | Live token usage / context window per session, straight from server truth. |
49
+ | **Slash commands** | The app's `/` menu is built from your actual server commands, not a hardcoded list. |
50
+ | **Update from your phone** | About screen checks and applies `hermes update` on the server — restart included. |
51
+ | **Keep-awake control** | Toggle the Termux/system wake lock remotely so long runs aren't killed. |
52
+ | **24/7 supervisor** | Optional watchdog auto-restarts the gateway if it ever dies, and survives reboots of the app. |
53
+ | **Crash reports** | The app pushes its crash dumps here (`~/.hermes/mobile-logs/diag/`) so issues are diagnosable without cables. |
54
+ | **Swarm-ready** | Turns started with the app's Swarm toggle carry their own orchestration directives — no extra server setup. |
55
+
56
+ Nothing is re-implemented: STT/TTS reuse Hermes Agent's own provider chain, sessions/models are
57
+ read from the agent's API. The plugin adds routes, not forks.
58
+
59
+ ## Install
60
+
61
+ **[Download the latest release →](https://github.com/tawaresachin/hermes-mobile-plugin/releases/latest)**
62
+ (every release ships a pure-Python `py3-none-any` wheel + sdist with `SHA256SUMS` checksums, built and version-verified by CI)
63
+
64
+ Into the same Python environment your Hermes Agent runs in (its venv):
65
+
66
+ ```bash
67
+ pip install git+https://github.com/tawaresachin/hermes-mobile-plugin@v0.0.12
68
+ # or from the release page: pip install hermes_mobile_plugin-<ver>-py3-none-any.whl
69
+ hermes-mobile-plugin install # registers plugin + generates the QR
70
+ ```
71
+
72
+ The plugin is pure Python — the one wheel runs unchanged on Windows, macOS, Linux and
73
+ Termux (native deps resolve per-OS from PyPI at install time). Not on PyPI; GitHub Releases
74
+ is the release channel, which matches how Hermes Agent itself updates from git.
75
+
76
+ `install` copies the plugin into the Hermes home your agent actually resolves (`~/.hermes` on Linux/Termux, XDG dirs on macOS, `%LOCALAPPDATA%\hermes` on Windows) and (re)starts the gateway.
77
+ Then grab the [Hermes Mobile APK](https://github.com/tawaresachin/hermes-mobile/releases), open
78
+ **Settings → Connect with QR**, and scan. LAN, Tailscale, or any tunnel — the QR carries the
79
+ address your phone can actually reach.
80
+
81
+ ## CLI
82
+
83
+ ```text
84
+ hermes-mobile-plugin install # set up + show QR
85
+ hermes-mobile-plugin qr # regenerate the QR (--no-browser)
86
+ hermes-mobile-plugin status # supervisor + gateway health
87
+ hermes-mobile-plugin supervisor --start|--stop|—daemon 24/7 watchdog
88
+ ```
89
+
90
+ ## HTTP routes (all Bearer-key protected)
91
+
92
+ Mounted on the api_server platform; the mobile app is the client.
93
+
94
+ | Route | Purpose |
95
+ |---|---|
96
+ | `POST /api/audio/transcribe` · `POST /api/audio/speak` | STT / TTS |
97
+ | `POST /api/audio/upload` · `GET /api/audio/download/{sid}/{name}` | attachments both ways |
98
+ | `GET /api/mobile/file` | fetch agent-created files for MEDIA cards |
99
+ | `GET /api/mobile/commands` · `POST /api/mobile/command/resolve` | slash-command menu |
100
+ | `GET /api/mobile/context-usage` · `GET /api/mobile/context-window` | context meter |
101
+ | `GET /api/mobile/update/check` · `POST /update/apply` · `GET /update/version` | server self-update |
102
+ | `GET /api/system/status` · `POST /api/system/awake` | host status, keep-awake |
103
+ | `POST /api/diag/log` · `GET /api/audio/health` | crash reports, liveness |
104
+
105
+ ## Cross-platform
106
+
107
+ Runs wherever Hermes Agent runs — **Windows, macOS, Linux, Termux/Android** — with per-platform
108
+ handling for process detachment, single-instance locks, launcher lookup (`hermes.exe` included),
109
+ and keep-awake (termux-wake-lock / caffeine / powershell), degrading honestly when a mechanism
110
+ doesn't exist. 53 tests cover the platform seams and the scripted-install paths.
111
+
112
+ **Termux note:** the shell installer skips pip there (it can trigger NDK toolchain builds);
113
+ Hermes Agent's own Termux environment must provide `pyyaml`, `qrcode` and `aiohttp` — the
114
+ standard agent install does.
115
+
116
+ ## Development
117
+
118
+ ```bash
119
+ git clone https://github.com/tawaresachin/hermes-mobile-plugin.git
120
+ cd hermes-mobile-plugin
121
+ pip install -e ".[dev]"
122
+ PYTHONPATH=src pytest # 53 tests
123
+ ```
124
+
125
+ CI (`.github/workflows/ci.yml`) runs the suite on ubuntu/macos/windows on every push, plus
126
+ sandboxed end-to-end dry-runs of `install.sh` (Linux + macOS, including the Termux skip-pip
127
+ path) and `install.bat` (Windows, fake venv + redirected home). Version tags additionally
128
+ build the wheel, verify the bundled manifest matches the tag, and attach it to the release.
129
+
130
+ Releases are tagged `v<version>`; `plugin.yaml` is the single source of the version string
131
+ (`PLUGIN_VERSION` derives from it — packaging metadata can't drift).
132
+
133
+ ## License
134
+
135
+ MIT © 2026 Sachin Taware
@@ -0,0 +1,117 @@
1
+ # Hermes Mobile Plugin
2
+
3
+ <p align="center">
4
+ <img src="https://img.shields.io/github/v/tag/tawaresachin/hermes-mobile-plugin?label=version&color=blue&style=for-the-badge" alt="version">
5
+ <img src="https://img.shields.io/github/actions/workflow/status/tawaresachin/hermes-mobile-plugin/ci.yml?branch=main&label=CI&style=for-the-badge" alt="CI">
6
+ <img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge" alt="MIT">
7
+ <img src="https://img.shields.io/badge/platforms-Windows%20%7C%20macOS%20%7C%20Linux%20%7C%20Android-blueviolet?style=for-the-badge" alt="platforms">
8
+ <img src="https://img.shields.io/badge/Python-3.10+-yellow?style=for-the-badge" alt="python">
9
+ </p>
10
+
11
+ The server-side half of **Hermes Mobile**: one plugin that turns your running
12
+ [Hermes Agent](https://hermes-agent.nousresearch.com) into a phone-ready hub — QR pairing, voice,
13
+ files, and server management. No extra daemon, no cloud relay: everything is mounted on the
14
+ gateway you already run (default port **8642**).
15
+
16
+ > You install **Hermes Agent** from the official site, then this **one plugin**. That's the whole
17
+ > server-side setup.
18
+
19
+ ## What you get
20
+
21
+ <img src="docs/screenshots/qr-pairing.jpg" width="360" alt="Pairing screen">
22
+
23
+ *Settings in the app after scanning the QR: server URL + key configured, connection live.*
24
+
25
+ | Capability | What it means for you |
26
+ |---|---|
27
+ | **QR pairing** | `install` prints/opens a QR with URL + API key; the app scans it (camera or gallery) and configures + tests the connection itself. |
28
+ | **Voice on-device** | Speech-to-text (Whisper) and text-to-speech routes on the gateway, so the phone mic/speaker work with zero extra services. |
29
+ | **File transfer** | Upload attachments from the phone, download anything the agent creates — the chat shows real file cards, previewable in-app. |
30
+ | **Context meter** | Live token usage / context window per session, straight from server truth. |
31
+ | **Slash commands** | The app's `/` menu is built from your actual server commands, not a hardcoded list. |
32
+ | **Update from your phone** | About screen checks and applies `hermes update` on the server — restart included. |
33
+ | **Keep-awake control** | Toggle the Termux/system wake lock remotely so long runs aren't killed. |
34
+ | **24/7 supervisor** | Optional watchdog auto-restarts the gateway if it ever dies, and survives reboots of the app. |
35
+ | **Crash reports** | The app pushes its crash dumps here (`~/.hermes/mobile-logs/diag/`) so issues are diagnosable without cables. |
36
+ | **Swarm-ready** | Turns started with the app's Swarm toggle carry their own orchestration directives — no extra server setup. |
37
+
38
+ Nothing is re-implemented: STT/TTS reuse Hermes Agent's own provider chain, sessions/models are
39
+ read from the agent's API. The plugin adds routes, not forks.
40
+
41
+ ## Install
42
+
43
+ **[Download the latest release →](https://github.com/tawaresachin/hermes-mobile-plugin/releases/latest)**
44
+ (every release ships a pure-Python `py3-none-any` wheel + sdist with `SHA256SUMS` checksums, built and version-verified by CI)
45
+
46
+ Into the same Python environment your Hermes Agent runs in (its venv):
47
+
48
+ ```bash
49
+ pip install git+https://github.com/tawaresachin/hermes-mobile-plugin@v0.0.12
50
+ # or from the release page: pip install hermes_mobile_plugin-<ver>-py3-none-any.whl
51
+ hermes-mobile-plugin install # registers plugin + generates the QR
52
+ ```
53
+
54
+ The plugin is pure Python — the one wheel runs unchanged on Windows, macOS, Linux and
55
+ Termux (native deps resolve per-OS from PyPI at install time). Not on PyPI; GitHub Releases
56
+ is the release channel, which matches how Hermes Agent itself updates from git.
57
+
58
+ `install` copies the plugin into the Hermes home your agent actually resolves (`~/.hermes` on Linux/Termux, XDG dirs on macOS, `%LOCALAPPDATA%\hermes` on Windows) and (re)starts the gateway.
59
+ Then grab the [Hermes Mobile APK](https://github.com/tawaresachin/hermes-mobile/releases), open
60
+ **Settings → Connect with QR**, and scan. LAN, Tailscale, or any tunnel — the QR carries the
61
+ address your phone can actually reach.
62
+
63
+ ## CLI
64
+
65
+ ```text
66
+ hermes-mobile-plugin install # set up + show QR
67
+ hermes-mobile-plugin qr # regenerate the QR (--no-browser)
68
+ hermes-mobile-plugin status # supervisor + gateway health
69
+ hermes-mobile-plugin supervisor --start|--stop|—daemon 24/7 watchdog
70
+ ```
71
+
72
+ ## HTTP routes (all Bearer-key protected)
73
+
74
+ Mounted on the api_server platform; the mobile app is the client.
75
+
76
+ | Route | Purpose |
77
+ |---|---|
78
+ | `POST /api/audio/transcribe` · `POST /api/audio/speak` | STT / TTS |
79
+ | `POST /api/audio/upload` · `GET /api/audio/download/{sid}/{name}` | attachments both ways |
80
+ | `GET /api/mobile/file` | fetch agent-created files for MEDIA cards |
81
+ | `GET /api/mobile/commands` · `POST /api/mobile/command/resolve` | slash-command menu |
82
+ | `GET /api/mobile/context-usage` · `GET /api/mobile/context-window` | context meter |
83
+ | `GET /api/mobile/update/check` · `POST /update/apply` · `GET /update/version` | server self-update |
84
+ | `GET /api/system/status` · `POST /api/system/awake` | host status, keep-awake |
85
+ | `POST /api/diag/log` · `GET /api/audio/health` | crash reports, liveness |
86
+
87
+ ## Cross-platform
88
+
89
+ Runs wherever Hermes Agent runs — **Windows, macOS, Linux, Termux/Android** — with per-platform
90
+ handling for process detachment, single-instance locks, launcher lookup (`hermes.exe` included),
91
+ and keep-awake (termux-wake-lock / caffeine / powershell), degrading honestly when a mechanism
92
+ doesn't exist. 53 tests cover the platform seams and the scripted-install paths.
93
+
94
+ **Termux note:** the shell installer skips pip there (it can trigger NDK toolchain builds);
95
+ Hermes Agent's own Termux environment must provide `pyyaml`, `qrcode` and `aiohttp` — the
96
+ standard agent install does.
97
+
98
+ ## Development
99
+
100
+ ```bash
101
+ git clone https://github.com/tawaresachin/hermes-mobile-plugin.git
102
+ cd hermes-mobile-plugin
103
+ pip install -e ".[dev]"
104
+ PYTHONPATH=src pytest # 53 tests
105
+ ```
106
+
107
+ CI (`.github/workflows/ci.yml`) runs the suite on ubuntu/macos/windows on every push, plus
108
+ sandboxed end-to-end dry-runs of `install.sh` (Linux + macOS, including the Termux skip-pip
109
+ path) and `install.bat` (Windows, fake venv + redirected home). Version tags additionally
110
+ build the wheel, verify the bundled manifest matches the tag, and attach it to the release.
111
+
112
+ Releases are tagged `v<version>`; `plugin.yaml` is the single source of the version string
113
+ (`PLUGIN_VERSION` derives from it — packaging metadata can't drift).
114
+
115
+ ## License
116
+
117
+ MIT © 2026 Sachin Taware
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hermes-mobile-plugin"
7
+ version = "0.0.15"
8
+ description = "Hermes Mobile QR Plugin - Generates QR code for direct 8642 connection"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "Sachin Taware"}]
13
+ dependencies = [
14
+ "pyyaml>=6.0",
15
+ "qrcode>=7.4",
16
+ "aiohttp>=3.8",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ dev = [
21
+ "pytest>=7.0",
22
+ "black",
23
+ "ruff",
24
+ ]
25
+
26
+ [project.scripts]
27
+ hermes-mobile-plugin = "hermes_mobile_plugin.cli:main"
28
+ hermes-mobile-install = "hermes_mobile_plugin.cli:install_plugin"
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
32
+
33
+ [tool.setuptools.package-data]
34
+ # Ship the loader manifest inside the wheel so `hermes-mobile-install`
35
+ # works from a pip install (no repo checkout) — the deploy code looks for
36
+ # plugin.yaml beside the package when the repo root is not there.
37
+ hermes_mobile_plugin = ["plugin.yaml"]
38
+
39
+ [tool.black]
40
+ line-length = 88
41
+ target-version = ["py310"]
42
+
43
+ [tool.ruff]
44
+ line-length = 88
45
+ target-version = "py310"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,215 @@
1
+ """Hermes Mobile QR Plugin - Clean architecture implementation.
2
+
3
+ Generates a QR code for the Hermes Mobile App to connect directly to the
4
+ Hermes Agent Desktop gateway (port 8642), and exposes voice STT/TTS routes
5
+ on the gateway so the mobile app can speak and listen without an extra
6
+ service.
7
+
8
+ Both behaviors are opt-in via ``plugins.hermes-mobile-qr`` in
9
+ ``~/.hermes/config.yaml``:
10
+
11
+ plugins:
12
+ hermes-mobile-qr:
13
+ auto_generate: true # QR code on startup
14
+ audio: true # voice routes on api_server
15
+
16
+ Audio routes are mounted via the standard plugin handler mechanism
17
+ (``ctx.register_platform_handler("api_server", _wire)``), which gives
18
+ the plugin a handle on the api_server's aiohttp ``web.Application``.
19
+ """
20
+
21
+ import asyncio
22
+ import logging
23
+ import threading
24
+ from pathlib import Path
25
+ from typing import Any, Optional
26
+
27
+ from .constants import PLUGIN_VERSION
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class HermesMobileQRPlugin:
33
+ """Plugin that generates QR codes for mobile app connection to 8642 gateway.
34
+
35
+ Used by the legacy entry-point style loader. The standard
36
+ ``register(ctx)`` function below is the path the modern plugin loader
37
+ uses; both entry points are kept for backward compatibility.
38
+ """
39
+
40
+ def __init__(self, config: dict, plugin_dir: Path):
41
+ self.config = config
42
+ self.plugin_dir = plugin_dir
43
+ self._qr_generated = False
44
+
45
+ async def startup(self) -> None:
46
+ """Called when Hermes Agent starts up (legacy entry-point style)."""
47
+ try:
48
+ from .config import get_plugin_config
49
+ plugin_config = get_plugin_config(self.config)
50
+ except ImportError:
51
+ plugin_config = self.config.get("plugins", {}).get("hermes-mobile-qr", {})
52
+
53
+ if plugin_config.get("auto_generate", True):
54
+ logger.info("[Hermes Mobile QR v%s] Plugin starting...", PLUGIN_VERSION)
55
+ await self._generate_qr_and_start_supervisor()
56
+ else:
57
+ logger.info("[Hermes Mobile QR v%s] Auto-generate disabled", PLUGIN_VERSION)
58
+
59
+ async def shutdown(self) -> None:
60
+ logger.info("[Hermes Mobile QR] Plugin shutting down...")
61
+
62
+ async def _generate_qr_and_start_supervisor(self) -> None:
63
+ if self._qr_generated:
64
+ return
65
+ try:
66
+ from .qr_generator import generate_qr
67
+ await generate_qr(self.config, self.plugin_dir)
68
+ self._qr_generated = True
69
+ logger.info("[Hermes Mobile QR] QR code generated successfully")
70
+
71
+ from .supervisor import ensure_running
72
+ ensure_running()
73
+ logger.info("[Hermes Mobile QR] Gateway watchdog verified/spawned")
74
+
75
+ except Exception as e:
76
+ logger.warning("[Hermes Mobile QR] Failed to initialize: %s", e)
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # Standard plugin entry point (register(ctx))
80
+ # ---------------------------------------------------------------------------
81
+
82
+ # Registration is keyed on the aiohttp app INSTANCE, not a global bool: a
83
+ # second api_server connect onto a DIFFERENT app must still get its routes,
84
+ # while a double-wire of the SAME app stays a no-op. (WeakSet so a dead app
85
+ # never pins memory and tests get a clean slate per instance.)
86
+ import weakref
87
+
88
+ _audio_wired_apps = weakref.WeakSet()
89
+ _audio_lock = threading.Lock()
90
+
91
+
92
+ def _wire_audio_routes(native: Any, adapter: Any) -> None:
93
+ """Factory passed to ``ctx.register_platform_handler("api_server", ...)``.
94
+
95
+ Receives the api_server's aiohttp ``web.Application`` as ``native``
96
+ and registers the plugin's voice routes on it. Guarded per-app so a
97
+ second connect of the SAME app doesn't double-register, while a fresh
98
+ app (hot-reload, tests) still gets mounted.
99
+ """
100
+ with _audio_lock:
101
+ if native is not None and native in _audio_wired_apps:
102
+ logger.debug("[hermes-mobile-qr] Audio routes already registered on this app; skipping")
103
+ return
104
+ try:
105
+ from .audio_routes import register as _register_routes
106
+ from .system_routes import register as _register_system_routes
107
+ from .commands_routes import register as _register_command_routes
108
+ from .update_routes import register as _register_update_routes
109
+ from .providers_routes import register as _register_providers_routes
110
+ except Exception as exc:
111
+ logger.warning(
112
+ "[hermes-mobile-qr] Could not import route modules: %s", exc
113
+ )
114
+ return
115
+ if native is None:
116
+ logger.debug(
117
+ "[hermes-mobile-qr] api_server native is None; "
118
+ "audio routes not mounted on this run"
119
+ )
120
+ return
121
+ try:
122
+ _register_routes(native)
123
+ _register_system_routes(native)
124
+ _register_command_routes(native)
125
+ _register_update_routes(native)
126
+ _register_providers_routes(native)
127
+ except Exception as exc:
128
+ logger.exception(
129
+ "[hermes-mobile-qr] Failed to register audio routes: %s", exc
130
+ )
131
+ return
132
+ if native is not None:
133
+ _audio_wired_apps.add(native)
134
+
135
+
136
+ async def _maybe_generate_qr(ctx: Any) -> None:
137
+ """Best-effort QR generation on startup, mirroring the legacy flow.
138
+
139
+ ``PluginContext`` exposes no ``.config``/``.plugin_dir`` attributes — the
140
+ QR payload needs the REAL platforms.api_server config, so load it from
141
+ config.yaml directly, and write the HTML next to the plugin package
142
+ (never the gateway's CWD).
143
+ """
144
+ try:
145
+ from .config import load_hermes_config, get_plugin_config
146
+ hermes_config = load_hermes_config()
147
+ plugin_config = get_plugin_config(hermes_config)
148
+ except ImportError:
149
+ hermes_config, plugin_config = {}, {}
150
+
151
+ if not plugin_config.get("auto_generate", True):
152
+ return
153
+
154
+ try:
155
+ from .constants import PLUGIN_DIR
156
+ from .qr_generator import generate_qr
157
+ out_dir = Path(ctx.plugin_dir) if getattr(ctx, "plugin_dir", None) else PLUGIN_DIR
158
+ await generate_qr(hermes_config, out_dir)
159
+ logger.info("[Hermes Mobile QR] QR code generated via register(ctx)")
160
+ except Exception as exc:
161
+ logger.warning("[Hermes Mobile QR] QR generation skipped: %s", exc)
162
+
163
+
164
+ def register(ctx: Any) -> None:
165
+ """Standard plugin entry point. Called by Hermes plugin loader.
166
+
167
+ Registers the plugin's voice STT/TTS routes on the api_server
168
+ platform, and (if enabled) generates a QR code on startup.
169
+ """
170
+ logger.info(
171
+ "[Hermes Mobile QR v%s] register(ctx) called", PLUGIN_VERSION
172
+ )
173
+
174
+ # Register the api_server handler — always safe (idempotent guard).
175
+ try:
176
+ ctx.register_platform_handler("api_server", _wire_audio_routes)
177
+ except Exception as exc:
178
+ logger.warning(
179
+ "[Hermes Mobile QR] Could not register api_server handler: %s", exc
180
+ )
181
+
182
+ # Keep-alive: spawn the detached gateway watchdog. It lives OUTSIDE the
183
+ # gateway process, so if the gateway dies (crash, OOM, reboot script) it
184
+ # restarts it and the app reconnects. Idempotent via PID file.
185
+ try:
186
+ from .supervisor import ensure_running
187
+ ensure_running()
188
+ except Exception as exc:
189
+ logger.warning("[Hermes Mobile QR] Watchdog spawn failed: %s", exc)
190
+
191
+ # Fire-and-forget QR generation so we don't block loader. Scheduling
192
+ # only ever happens on an ALREADY-running loop: a run_until_complete
193
+ # fallback here could block the plugin loader (or start a stray loop
194
+ # beside the gateway's). No loop at register-time -> QR skipped until
195
+ # the next startup; the CLI covers the manual path.
196
+ try:
197
+ asyncio.get_running_loop().create_task(_maybe_generate_qr(ctx))
198
+ except RuntimeError:
199
+ logger.debug("[hermes-mobile-qr] no running loop at register(); QR gen deferred")
200
+
201
+
202
+ # ---------------------------------------------------------------------------
203
+ # Legacy entry point (kept for backward compatibility)
204
+ # ---------------------------------------------------------------------------
205
+
206
+
207
+ def create_plugin(config: dict, plugin_dir: Path) -> HermesMobileQRPlugin:
208
+ """Factory function called by the legacy Hermes plugin loader."""
209
+ return HermesMobileQRPlugin(config, plugin_dir)
210
+
211
+
212
+ async def startup_hook(config: dict, plugin_dir: Path) -> None:
213
+ """Standalone startup hook if Hermes loader expects a function."""
214
+ plugin = create_plugin(config, plugin_dir)
215
+ await plugin.startup()