jupyter-hermes-proxy 0.1.0__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.
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ *.whl
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: jupyter-hermes-proxy
3
+ Version: 0.1.0
4
+ Summary: Jupyter server proxy for Hermes Agent dashboard
5
+ Project-URL: Homepage, https://github.com/dive4dec/jupyter-hermes-proxy
6
+ Project-URL: Bug Tracker, https://github.com/dive4dec/jupyter-hermes-proxy/issues
7
+ Author-email: Chung Chan <chung.chan@cityu.edu.hk>
8
+ License: MIT
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: jupyter-server-proxy>=4.0
11
+ Description-Content-Type: text/markdown
12
+
13
+ # jupyter-hermes-proxy
14
+
15
+ Launch the [Hermes Agent](https://github.com/nousresearch/hermes-agent) dashboard from JupyterLab as a native launcher entry.
16
+
17
+ ## Features
18
+
19
+ - One-click launch from the JupyterLab launcher panel with a custom Hermes icon
20
+ - Automatic port management via [jupyter-server-proxy](https://github.com/jupyter-server/jupyter-server-proxy)
21
+ - Works behind reverse proxies (no manual port forwarding needed)
22
+ - **Two modes**: spawn a new dashboard, or connect to an existing one
23
+ - Configurable Hermes executable path via `HERMES_BIN_PATH`
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install jupyter-hermes-proxy
29
+ ```
30
+
31
+ Then restart JupyterLab. The "Hermes Dashboard" launcher entry will appear automatically.
32
+
33
+ ## Configuration
34
+
35
+ ### Spawn mode (default)
36
+
37
+ By default the proxy searches for `hermes` in `PATH` and spawns a new dashboard process when you click the launcher.
38
+
39
+ Override the binary path:
40
+
41
+ ```bash
42
+ export HERMES_BIN_PATH=/path/to/hermes
43
+ ```
44
+
45
+ ### URL mode (connect to existing dashboard)
46
+
47
+ When Hermes dashboard is already running (e.g. supervised by s6 in Docker), point the proxy at it:
48
+
49
+ ```bash
50
+ export HERMES_DASHBOARD_URL=http://127.0.0.1:9119
51
+ ```
52
+
53
+ In this mode the proxy **does not spawn a new process** — it simply proxies traffic to the existing dashboard. No `hermes` binary on PATH is required.
54
+
55
+ ## Related Packages
56
+
57
+ - **[jupyter-ai-hermes](https://github.com/dive4dec/jupyter-ai-hermes)** — Hermes Agent as an ACP persona for Jupyter AI chat, with live notebook context injection and MCP tools for cell management.
58
+
59
+ Both packages share the same `HERMES_BIN_PATH` environment variable convention.
60
+
61
+ ## Development
62
+
63
+ ```bash
64
+ git clone git@github.com:dive4dec/jupyter-hermes-proxy.git
65
+ cd jupyter-hermes-proxy
66
+ pip install -e .
67
+ ```
68
+
69
+ ## How It Works
70
+
71
+ This package registers a server spec under the `jupyter_serverproxy_servers` entry point.
72
+
73
+ - **Spawn mode**: `jupyter-server-proxy` calls `setup_hermes()` which returns a command that spawns `hermes dashboard` on an ephemeral port and proxies it through the Jupyter server.
74
+ - **URL mode**: `setup_hermes()` returns the pre-existing dashboard URL directly, so jupyter-server-proxy connects to it without spawning anything.
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,66 @@
1
+ # jupyter-hermes-proxy
2
+
3
+ Launch the [Hermes Agent](https://github.com/nousresearch/hermes-agent) dashboard from JupyterLab as a native launcher entry.
4
+
5
+ ## Features
6
+
7
+ - One-click launch from the JupyterLab launcher panel with a custom Hermes icon
8
+ - Automatic port management via [jupyter-server-proxy](https://github.com/jupyter-server/jupyter-server-proxy)
9
+ - Works behind reverse proxies (no manual port forwarding needed)
10
+ - **Two modes**: spawn a new dashboard, or connect to an existing one
11
+ - Configurable Hermes executable path via `HERMES_BIN_PATH`
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install jupyter-hermes-proxy
17
+ ```
18
+
19
+ Then restart JupyterLab. The "Hermes Dashboard" launcher entry will appear automatically.
20
+
21
+ ## Configuration
22
+
23
+ ### Spawn mode (default)
24
+
25
+ By default the proxy searches for `hermes` in `PATH` and spawns a new dashboard process when you click the launcher.
26
+
27
+ Override the binary path:
28
+
29
+ ```bash
30
+ export HERMES_BIN_PATH=/path/to/hermes
31
+ ```
32
+
33
+ ### URL mode (connect to existing dashboard)
34
+
35
+ When Hermes dashboard is already running (e.g. supervised by s6 in Docker), point the proxy at it:
36
+
37
+ ```bash
38
+ export HERMES_DASHBOARD_URL=http://127.0.0.1:9119
39
+ ```
40
+
41
+ In this mode the proxy **does not spawn a new process** — it simply proxies traffic to the existing dashboard. No `hermes` binary on PATH is required.
42
+
43
+ ## Related Packages
44
+
45
+ - **[jupyter-ai-hermes](https://github.com/dive4dec/jupyter-ai-hermes)** — Hermes Agent as an ACP persona for Jupyter AI chat, with live notebook context injection and MCP tools for cell management.
46
+
47
+ Both packages share the same `HERMES_BIN_PATH` environment variable convention.
48
+
49
+ ## Development
50
+
51
+ ```bash
52
+ git clone git@github.com:dive4dec/jupyter-hermes-proxy.git
53
+ cd jupyter-hermes-proxy
54
+ pip install -e .
55
+ ```
56
+
57
+ ## How It Works
58
+
59
+ This package registers a server spec under the `jupyter_serverproxy_servers` entry point.
60
+
61
+ - **Spawn mode**: `jupyter-server-proxy` calls `setup_hermes()` which returns a command that spawns `hermes dashboard` on an ephemeral port and proxies it through the Jupyter server.
62
+ - **URL mode**: `setup_hermes()` returns the pre-existing dashboard URL directly, so jupyter-server-proxy connects to it without spawning anything.
63
+
64
+ ## License
65
+
66
+ MIT
@@ -0,0 +1,75 @@
1
+ """Jupyter server proxy for Hermes Agent dashboard."""
2
+
3
+ import os
4
+ import shutil
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ HERE = os.path.dirname(os.path.abspath(__file__))
8
+
9
+
10
+ def _find_hermes() -> str:
11
+ """Locate the ``hermes`` executable, respecting HERMES_BIN_PATH override."""
12
+ path = os.environ.get("HERMES_BIN_PATH") or shutil.which("hermes")
13
+ if not path:
14
+ raise FileNotFoundError(
15
+ "Cannot find 'hermes' in PATH. "
16
+ "Install Hermes Agent or set HERMES_BIN_PATH to its full path."
17
+ )
18
+ return path
19
+
20
+
21
+ def _hermes_cmd(port: int, unix_socket: str) -> List[str]:
22
+ """Build the command to launch the Hermes dashboard.
23
+
24
+ ``jupyter-server-proxy`` substitutes ``{port}`` with an ephemeral port
25
+ before calling this function, so we use that to tell the dashboard which
26
+ port to listen on.
27
+ """
28
+ hermes_bin = _find_hermes()
29
+ return [
30
+ hermes_bin,
31
+ "dashboard",
32
+ "--port={port}",
33
+ "--host=127.0.0.1",
34
+ "--no-open",
35
+ "--skip-build",
36
+ ]
37
+
38
+
39
+ def _hermes_url() -> Optional[str]:
40
+ """Return the pre-existing dashboard URL if HERMES_DASHBOARD_URL is set."""
41
+ return os.environ.get("HERMES_DASHBOARD_URL")
42
+
43
+
44
+ def setup_hermes() -> Dict[str, Any]:
45
+ """Return a jupyter-server-proxy server spec for the Hermes dashboard.
46
+
47
+ When ``HERMES_DASHBOARD_URL`` is set (e.g. ``http://127.0.0.1:9119``),
48
+ the proxy connects to an existing Hermes dashboard instead of spawning
49
+ a new process. This is the recommended mode in Docker/s6 deployments
50
+ where Hermes is already supervised.
51
+
52
+ See: https://jupyter-server-proxy.readthedocs.io/
53
+ """
54
+ existing_url = _hermes_url()
55
+
56
+ if existing_url:
57
+ return {
58
+ "url": existing_url,
59
+ "timeout": 90,
60
+ "new_browser_tab": True,
61
+ "launcher_entry": {
62
+ "title": "Hermes Dashboard",
63
+ "icon_path": os.path.join(HERE, "icons", "hermes.svg"),
64
+ },
65
+ }
66
+
67
+ return {
68
+ "command": _hermes_cmd,
69
+ "timeout": 90,
70
+ "new_browser_tab": True,
71
+ "launcher_entry": {
72
+ "title": "Hermes Dashboard",
73
+ "icon_path": os.path.join(HERE, "icons", "hermes.svg"),
74
+ },
75
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <circle cx="32" cy="32" r="30" fill="#6366f1"/>
3
+ <text x="32" y="42" text-anchor="middle" fill="white" font-size="28" font-family="Arial, sans-serif" font-weight="bold">H</text>
4
+ </svg>
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "jupyter-hermes-proxy"
7
+ version = "0.1.0"
8
+ description = "Jupyter server proxy for Hermes Agent dashboard"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ authors = [
12
+ {name = "Chung Chan", email = "chung.chan@cityu.edu.hk"},
13
+ ]
14
+ requires-python = ">=3.12"
15
+ dependencies = [
16
+ "jupyter-server-proxy>=4.0",
17
+ ]
18
+
19
+ [project.entry-points."jupyter_serverproxy_servers"]
20
+ hermes = "jupyter_hermes_proxy:setup_hermes"
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/dive4dec/jupyter-hermes-proxy"
24
+ "Bug Tracker" = "https://github.com/dive4dec/jupyter-hermes-proxy/issues"
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["jupyter_hermes_proxy"]
28
+ include = ["jupyter_hermes_proxy/icons"]
@@ -0,0 +1,141 @@
1
+ """Tests for jupyter-hermes-proxy hermes executable lookup and proxy modes."""
2
+
3
+ import os
4
+ import sys
5
+ from unittest.mock import patch
6
+
7
+ import pytest
8
+
9
+
10
+ class TestFindHermes:
11
+ """Test _find_hermes() resolves the hermes binary correctly."""
12
+
13
+ def test_env_var_overrides_path(self, monkeypatch):
14
+ """HERMES_BIN_PATH takes precedence over PATH lookup."""
15
+ import jupyter_hermes_proxy
16
+
17
+ path_bin = "/usr/local/bin/hermes"
18
+ env_bin = "/opt/data/mamba/envs/main/bin/hermes"
19
+
20
+ monkeypatch.setenv("HERMES_BIN_PATH", env_bin)
21
+ with patch("shutil.which", return_value=path_bin):
22
+ result = jupyter_hermes_proxy._find_hermes()
23
+
24
+ assert result == env_bin
25
+ assert result != path_bin
26
+
27
+ def test_falls_back_to_shutil_which(self, monkeypatch):
28
+ """shutil.which is used when HERMES_BIN_PATH is not set."""
29
+ import jupyter_hermes_proxy
30
+
31
+ path_bin = "/usr/local/bin/hermes"
32
+
33
+ monkeypatch.delenv("HERMES_BIN_PATH", raising=False)
34
+ with patch("shutil.which", return_value=path_bin):
35
+ result = jupyter_hermes_proxy._find_hermes()
36
+
37
+ assert result == path_bin
38
+
39
+ def test_raises_file_not_found_when_missing(self, monkeypatch):
40
+ """FileNotFoundError is raised when neither env var nor PATH works."""
41
+ import jupyter_hermes_proxy
42
+
43
+ monkeypatch.delenv("HERMES_BIN_PATH", raising=False)
44
+ with patch("shutil.which", return_value=None):
45
+ with pytest.raises(FileNotFoundError, match="hermes"):
46
+ jupyter_hermes_proxy._find_hermes()
47
+
48
+ def test_env_var_used_even_when_path_exists(self, monkeypatch):
49
+ """HERMES_BIN_PATH must override a binary found in PATH."""
50
+ import jupyter_hermes_proxy
51
+
52
+ path_bin = "/usr/local/bin/hermes"
53
+ env_bin = "/opt/custom/bin/hermes"
54
+
55
+ monkeypatch.setenv("HERMES_BIN_PATH", env_bin)
56
+ with patch("shutil.which", return_value=path_bin):
57
+ result = jupyter_hermes_proxy._find_hermes()
58
+
59
+ assert result == env_bin
60
+ assert result != path_bin
61
+
62
+
63
+ class TestHermesCmd:
64
+ """Test _hermes_cmd() builds the correct command list."""
65
+
66
+ def test_basic_command(self, monkeypatch):
67
+ """Command includes dashboard flags."""
68
+ import jupyter_hermes_proxy
69
+
70
+ monkeypatch.setattr(jupyter_hermes_proxy, "shutil", __import__("shutil"))
71
+ with patch("shutil.which", return_value="/usr/bin/hermes"):
72
+ cmd = jupyter_hermes_proxy._hermes_cmd(8765, "")
73
+
74
+ assert cmd[0] == "/usr/bin/hermes"
75
+ assert "dashboard" in cmd
76
+ assert "--port={port}" in cmd
77
+ assert "--host=127.0.0.1" in cmd
78
+ assert "--no-open" in cmd
79
+ assert "--skip-build" in cmd
80
+
81
+
82
+ class TestSetupHermes:
83
+ """Test setup_hermes() returns a valid server spec in both modes."""
84
+
85
+ def _get_module(self):
86
+ import jupyter_hermes_proxy
87
+ return jupyter_hermes_proxy
88
+
89
+ # -- Spawn mode (no HERMES_DASHBOARD_URL) --
90
+
91
+ def test_spawn_mode_has_command(self, monkeypatch):
92
+ """Spawn mode contains 'command', not 'url'."""
93
+ monkeypatch.delenv("HERMES_DASHBOARD_URL", raising=False)
94
+ with patch("shutil.which", return_value="/usr/bin/hermes"):
95
+ spec = self._get_module().setup_hermes()
96
+
97
+ assert "command" in spec
98
+ assert "url" not in spec
99
+
100
+ def test_spawn_mode_has_timeout_and_launcher(self, monkeypatch):
101
+ """Spawn spec contains all required keys."""
102
+ monkeypatch.delenv("HERMES_DASHBOARD_URL", raising=False)
103
+ with patch("shutil.which", return_value="/usr/bin/hermes"):
104
+ spec = self._get_module().setup_hermes()
105
+
106
+ assert spec["timeout"] == 90
107
+ assert spec["launcher_entry"]["title"] == "Hermes Dashboard"
108
+ assert "hermes.svg" in spec["launcher_entry"]["icon_path"]
109
+
110
+ # -- URL mode (HERMES_DASHBOARD_URL set) --
111
+
112
+ def test_url_mode_has_url_not_command(self, monkeypatch):
113
+ """URL mode contains 'url', not 'command'."""
114
+ monkeypatch.setenv("HERMES_DASHBOARD_URL", "http://127.0.0.1:9119")
115
+ spec = self._get_module().setup_hermes()
116
+
117
+ assert "url" in spec
118
+ assert "command" not in spec
119
+
120
+ def test_url_mode_preserves_url_value(self, monkeypatch):
121
+ """URL mode propagates the env var value directly."""
122
+ monkeypatch.setenv("HERMES_DASHBOARD_URL", "http://127.0.0.1:9119")
123
+ spec = self._get_module().setup_hermes()
124
+
125
+ assert spec["url"] == "http://127.0.0.1:9119"
126
+
127
+ def test_url_mode_has_timeout_and_launcher(self, monkeypatch):
128
+ """URL spec also contains timeout and launcher entry."""
129
+ monkeypatch.setenv("HERMES_DASHBOARD_URL", "http://127.0.0.1:9119")
130
+ spec = self._get_module().setup_hermes()
131
+
132
+ assert spec["timeout"] == 90
133
+ assert spec["launcher_entry"]["title"] == "Hermes Dashboard"
134
+ assert "hermes.svg" in spec["launcher_entry"]["icon_path"]
135
+
136
+ def test_url_mode_ignores_hermes_bin(self, monkeypatch):
137
+ """URL mode does not need hermes binary on PATH."""
138
+ monkeypatch.setenv("HERMES_DASHBOARD_URL", "http://127.0.0.1:9119")
139
+ # No shutil.which mock — should not be called
140
+ spec = self._get_module().setup_hermes()
141
+ assert spec["url"] == "http://127.0.0.1:9119"