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.
- hermes_mobile_plugin-0.0.15/LICENSE +21 -0
- hermes_mobile_plugin-0.0.15/PKG-INFO +135 -0
- hermes_mobile_plugin-0.0.15/README.md +117 -0
- hermes_mobile_plugin-0.0.15/pyproject.toml +45 -0
- hermes_mobile_plugin-0.0.15/setup.cfg +4 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/__init__.py +215 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/audio.py +253 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/audio_routes.py +502 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/cli.py +473 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/commands_routes.py +166 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/config.py +102 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/constants.py +122 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/ip_detector.py +127 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/plugin.yaml +18 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/providers_routes.py +182 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/qr_generator.py +276 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/supervisor.py +595 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/system_routes.py +448 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin/update_routes.py +268 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin.egg-info/PKG-INFO +135 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin.egg-info/SOURCES.txt +31 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin.egg-info/dependency_links.txt +1 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin.egg-info/entry_points.txt +3 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin.egg-info/requires.txt +8 -0
- hermes_mobile_plugin-0.0.15/src/hermes_mobile_plugin.egg-info/top_level.txt +1 -0
- hermes_mobile_plugin-0.0.15/tests/test_commands_routes.py +97 -0
- hermes_mobile_plugin-0.0.15/tests/test_config.py +166 -0
- hermes_mobile_plugin-0.0.15/tests/test_core.py +105 -0
- hermes_mobile_plugin-0.0.15/tests/test_cross_platform.py +109 -0
- hermes_mobile_plugin-0.0.15/tests/test_install.py +80 -0
- hermes_mobile_plugin-0.0.15/tests/test_ip_detector.py +125 -0
- hermes_mobile_plugin-0.0.15/tests/test_qr_generator.py +213 -0
- 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,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()
|