hermes-plugin-pushary 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,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: hermes-plugin-pushary
3
+ Version: 0.1.0
4
+ Summary: Pushary push notifications plugin for Hermes Agent
5
+ Author-email: RalphNex OU <business@pushary.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://pushary.com
8
+ Project-URL: Repository, https://github.com/pushary/pushary
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Pushary Plugin for Hermes Agent
16
+
17
+ Push notifications and human-in-the-loop for [Hermes Agent](https://hermes-agent.nousresearch.com/) via [Pushary](https://pushary.com).
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install hermes-plugin-pushary
23
+ ```
24
+
25
+ Or manually:
26
+
27
+ ```bash
28
+ cp -r pushary_plugin ~/.hermes/plugins/pushary
29
+ cp plugin.yaml ~/.hermes/plugins/pushary/
30
+ ```
31
+
32
+ Then enable:
33
+
34
+ ```bash
35
+ hermes plugins enable pushary
36
+ ```
37
+
38
+ ## Setup
39
+
40
+ Set your API key:
41
+
42
+ ```bash
43
+ export PUSHARY_API_KEY="pk_xxx.sk_xxx"
44
+ ```
45
+
46
+ Get a key at [pushary.com/sign-up](https://pushary.com/sign-up?from=hermes).
47
+
48
+ ## Tools
49
+
50
+ | Tool | Description |
51
+ |------|-------------|
52
+ | `pushary_notify` | Send a push notification with optional rich context |
53
+ | `pushary_ask` | Ask a question via push (yes/no, multiple choice, or free text) |
54
+ | `pushary_wait` | Wait for the user's answer |
55
+ | `pushary_cancel` | Cancel a pending question |
56
+
57
+ ## Auto-Notifications
58
+
59
+ The plugin automatically sends push notifications when tools return errors (up to 3 per session).
60
+
61
+ Set `PUSHARY_AUTO_NOTIFY_SESSION_END=1` to also get notified when a session ends.
62
+
63
+ Set `PUSHARY_AGENT_NAME` to identify this Hermes instance in notifications (e.g., `"Hermes - daily-briefing"`).
64
+
65
+ ## License
66
+
67
+ MIT
@@ -0,0 +1,53 @@
1
+ # Pushary Plugin for Hermes Agent
2
+
3
+ Push notifications and human-in-the-loop for [Hermes Agent](https://hermes-agent.nousresearch.com/) via [Pushary](https://pushary.com).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install hermes-plugin-pushary
9
+ ```
10
+
11
+ Or manually:
12
+
13
+ ```bash
14
+ cp -r pushary_plugin ~/.hermes/plugins/pushary
15
+ cp plugin.yaml ~/.hermes/plugins/pushary/
16
+ ```
17
+
18
+ Then enable:
19
+
20
+ ```bash
21
+ hermes plugins enable pushary
22
+ ```
23
+
24
+ ## Setup
25
+
26
+ Set your API key:
27
+
28
+ ```bash
29
+ export PUSHARY_API_KEY="pk_xxx.sk_xxx"
30
+ ```
31
+
32
+ Get a key at [pushary.com/sign-up](https://pushary.com/sign-up?from=hermes).
33
+
34
+ ## Tools
35
+
36
+ | Tool | Description |
37
+ |------|-------------|
38
+ | `pushary_notify` | Send a push notification with optional rich context |
39
+ | `pushary_ask` | Ask a question via push (yes/no, multiple choice, or free text) |
40
+ | `pushary_wait` | Wait for the user's answer |
41
+ | `pushary_cancel` | Cancel a pending question |
42
+
43
+ ## Auto-Notifications
44
+
45
+ The plugin automatically sends push notifications when tools return errors (up to 3 per session).
46
+
47
+ Set `PUSHARY_AUTO_NOTIFY_SESSION_END=1` to also get notified when a session ends.
48
+
49
+ Set `PUSHARY_AGENT_NAME` to identify this Hermes instance in notifications (e.g., `"Hermes - daily-briefing"`).
50
+
51
+ ## License
52
+
53
+ MIT
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: hermes-plugin-pushary
3
+ Version: 0.1.0
4
+ Summary: Pushary push notifications plugin for Hermes Agent
5
+ Author-email: RalphNex OU <business@pushary.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://pushary.com
8
+ Project-URL: Repository, https://github.com/pushary/pushary
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Pushary Plugin for Hermes Agent
16
+
17
+ Push notifications and human-in-the-loop for [Hermes Agent](https://hermes-agent.nousresearch.com/) via [Pushary](https://pushary.com).
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install hermes-plugin-pushary
23
+ ```
24
+
25
+ Or manually:
26
+
27
+ ```bash
28
+ cp -r pushary_plugin ~/.hermes/plugins/pushary
29
+ cp plugin.yaml ~/.hermes/plugins/pushary/
30
+ ```
31
+
32
+ Then enable:
33
+
34
+ ```bash
35
+ hermes plugins enable pushary
36
+ ```
37
+
38
+ ## Setup
39
+
40
+ Set your API key:
41
+
42
+ ```bash
43
+ export PUSHARY_API_KEY="pk_xxx.sk_xxx"
44
+ ```
45
+
46
+ Get a key at [pushary.com/sign-up](https://pushary.com/sign-up?from=hermes).
47
+
48
+ ## Tools
49
+
50
+ | Tool | Description |
51
+ |------|-------------|
52
+ | `pushary_notify` | Send a push notification with optional rich context |
53
+ | `pushary_ask` | Ask a question via push (yes/no, multiple choice, or free text) |
54
+ | `pushary_wait` | Wait for the user's answer |
55
+ | `pushary_cancel` | Cancel a pending question |
56
+
57
+ ## Auto-Notifications
58
+
59
+ The plugin automatically sends push notifications when tools return errors (up to 3 per session).
60
+
61
+ Set `PUSHARY_AUTO_NOTIFY_SESSION_END=1` to also get notified when a session ends.
62
+
63
+ Set `PUSHARY_AGENT_NAME` to identify this Hermes instance in notifications (e.g., `"Hermes - daily-briefing"`).
64
+
65
+ ## License
66
+
67
+ MIT
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ hermes_plugin_pushary.egg-info/PKG-INFO
4
+ hermes_plugin_pushary.egg-info/SOURCES.txt
5
+ hermes_plugin_pushary.egg-info/dependency_links.txt
6
+ hermes_plugin_pushary.egg-info/entry_points.txt
7
+ hermes_plugin_pushary.egg-info/top_level.txt
8
+ pushary_plugin/__init__.py
9
+ pushary_plugin/api.py
10
+ pushary_plugin/schemas.py
11
+ pushary_plugin/tools.py
@@ -0,0 +1,2 @@
1
+ [hermes_agent.plugins]
2
+ pushary = pushary_plugin
@@ -0,0 +1,115 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ from pathlib import Path
5
+ from . import schemas, tools, api
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ _error_count = 0
10
+ _tool_count = 0
11
+
12
+
13
+ def register(ctx):
14
+ ctx.register_tool(
15
+ name="pushary_notify",
16
+ toolset="pushary",
17
+ schema=schemas.PUSHARY_NOTIFY,
18
+ handler=tools.pushary_notify,
19
+ description="Send push notification to user's phone",
20
+ )
21
+
22
+ ctx.register_tool(
23
+ name="pushary_ask",
24
+ toolset="pushary",
25
+ schema=schemas.PUSHARY_ASK,
26
+ handler=tools.pushary_ask,
27
+ description="Ask user a question via push notification",
28
+ )
29
+
30
+ ctx.register_tool(
31
+ name="pushary_wait",
32
+ toolset="pushary",
33
+ schema=schemas.PUSHARY_WAIT,
34
+ handler=tools.pushary_wait,
35
+ description="Wait for user's answer to a push question",
36
+ )
37
+
38
+ ctx.register_tool(
39
+ name="pushary_cancel",
40
+ toolset="pushary",
41
+ schema=schemas.PUSHARY_CANCEL,
42
+ handler=tools.pushary_cancel,
43
+ description="Cancel a pending push question",
44
+ )
45
+
46
+ ctx.register_hook("post_tool_call", _on_post_tool_call)
47
+ ctx.register_hook("on_session_end", _on_session_end)
48
+
49
+ skills_dir = Path(__file__).parent.parent / "skills"
50
+ if skills_dir.is_dir():
51
+ for child in sorted(skills_dir.iterdir()):
52
+ skill_md = child / "SKILL.md"
53
+ if child.is_dir() and skill_md.exists():
54
+ ctx.register_skill(child.name, skill_md)
55
+
56
+ logger.info("[pushary] Plugin registered: 4 tools, 2 hooks")
57
+
58
+
59
+ def _on_post_tool_call(tool_name, args, result, duration_ms=0, **kwargs):
60
+ global _error_count, _tool_count
61
+ _tool_count += 1
62
+
63
+ if not os.environ.get("PUSHARY_API_KEY"):
64
+ return
65
+
66
+ try:
67
+ parsed = json.loads(result) if isinstance(result, str) else result
68
+ except (json.JSONDecodeError, TypeError):
69
+ parsed = {}
70
+
71
+ if isinstance(parsed, dict) and parsed.get("error"):
72
+ _error_count += 1
73
+ if _error_count <= 3:
74
+ try:
75
+ api.send_notification(
76
+ title="Tool error in Hermes",
77
+ body=f"{tool_name} failed: {str(parsed['error'])[:150]}",
78
+ agent_name=os.environ.get("PUSHARY_AGENT_NAME", "Hermes"),
79
+ context={
80
+ "type": "error",
81
+ "errorMessage": str(parsed["error"])[:500],
82
+ "summary": f"Tool '{tool_name}' returned an error after {duration_ms}ms",
83
+ },
84
+ )
85
+ except Exception:
86
+ pass
87
+
88
+
89
+ def _on_session_end(session_id, completed=False, interrupted=False, **kwargs):
90
+ if not os.environ.get("PUSHARY_API_KEY"):
91
+ return
92
+ if not os.environ.get("PUSHARY_AUTO_NOTIFY_SESSION_END"):
93
+ return
94
+
95
+ global _error_count, _tool_count
96
+ try:
97
+ status = "completed" if completed else ("interrupted" if interrupted else "ended")
98
+ api.send_notification(
99
+ title=f"Hermes session {status}",
100
+ body=f"Session used {_tool_count} tool calls with {_error_count} errors",
101
+ agent_name=os.environ.get("PUSHARY_AGENT_NAME", "Hermes"),
102
+ context={
103
+ "type": "task_complete" if completed else "info",
104
+ "summary": f"Session {session_id[:8]} {status}",
105
+ "details": [
106
+ f"{_tool_count} tool calls executed",
107
+ f"{_error_count} errors encountered",
108
+ ],
109
+ },
110
+ )
111
+ except Exception:
112
+ pass
113
+ finally:
114
+ _error_count = 0
115
+ _tool_count = 0
@@ -0,0 +1,87 @@
1
+ import json
2
+ import os
3
+ import urllib.request
4
+ import urllib.error
5
+
6
+ BASE_URL = os.environ.get("PUSHARY_BASE_URL", "https://pushary.com")
7
+
8
+
9
+ def _get_api_key():
10
+ key = os.environ.get("PUSHARY_API_KEY")
11
+ if not key:
12
+ raise ValueError(
13
+ "PUSHARY_API_KEY not set. Get your key at https://pushary.com/sign-up?from=hermes"
14
+ )
15
+ return key
16
+
17
+
18
+ def _mcp_call(tool_name, params):
19
+ api_key = _get_api_key()
20
+ url = f"{BASE_URL}/api/mcp/mcp"
21
+ payload = json.dumps({
22
+ "jsonrpc": "2.0",
23
+ "id": 1,
24
+ "method": "tools/call",
25
+ "params": {"name": tool_name, "arguments": params},
26
+ }).encode("utf-8")
27
+
28
+ req = urllib.request.Request(
29
+ url,
30
+ data=payload,
31
+ headers={
32
+ "Content-Type": "application/json",
33
+ "Authorization": f"Bearer {api_key}",
34
+ },
35
+ method="POST",
36
+ )
37
+
38
+ try:
39
+ with urllib.request.urlopen(req, timeout=60) as resp:
40
+ body = json.loads(resp.read().decode("utf-8"))
41
+ except urllib.error.HTTPError as e:
42
+ return {"error": f"HTTP {e.code}: {e.reason}"}
43
+ except Exception as e:
44
+ return {"error": str(e)}
45
+
46
+ if "error" in body:
47
+ return {"error": body["error"].get("message", str(body["error"]))}
48
+
49
+ text = body.get("result", {}).get("content", [{}])[0].get("text", "{}")
50
+ try:
51
+ return json.loads(text)
52
+ except json.JSONDecodeError:
53
+ return {"raw": text}
54
+
55
+
56
+ def send_notification(title, body, agent_name=None, context=None):
57
+ params = {"title": title, "body": body}
58
+ if agent_name:
59
+ params["agentName"] = agent_name
60
+ if context:
61
+ params["context"] = context
62
+ return _mcp_call("send_notification", params)
63
+
64
+
65
+ def ask_user(question, question_type="confirm", options=None, placeholder=None,
66
+ context=None, agent_name=None):
67
+ params = {"question": question, "type": question_type}
68
+ if options:
69
+ params["options"] = options
70
+ if placeholder:
71
+ params["placeholder"] = placeholder
72
+ if context:
73
+ params["context"] = context
74
+ if agent_name:
75
+ params["agentName"] = agent_name
76
+ return _mcp_call("ask_user", params)
77
+
78
+
79
+ def wait_for_answer(correlation_id, timeout_ms=30000):
80
+ return _mcp_call("wait_for_answer", {
81
+ "correlationId": correlation_id,
82
+ "timeoutMs": min(timeout_ms, 55000),
83
+ })
84
+
85
+
86
+ def cancel_question(correlation_id):
87
+ return _mcp_call("cancel_question", {"correlationId": correlation_id})
@@ -0,0 +1,141 @@
1
+ PUSHARY_NOTIFY = {
2
+ "name": "pushary_notify",
3
+ "description": (
4
+ "Send a push notification to the user's phone or desktop. Use when a task "
5
+ "completes, an error occurs, or a long-running process finishes. The user "
6
+ "sees this on their lock screen even if they're away from the computer. "
7
+ "Optionally include structured context with file changes, error details, "
8
+ "and next steps."
9
+ ),
10
+ "parameters": {
11
+ "type": "object",
12
+ "properties": {
13
+ "title": {
14
+ "type": "string",
15
+ "description": "Notification title (max 100 chars, aim for under 60)",
16
+ },
17
+ "body": {
18
+ "type": "string",
19
+ "description": "Notification body (max 500 chars, aim for under 200)",
20
+ },
21
+ "agent_name": {
22
+ "type": "string",
23
+ "description": 'Identifies this Hermes instance (e.g., "Hermes - daily-briefing")',
24
+ },
25
+ "context_type": {
26
+ "type": "string",
27
+ "enum": ["task_complete", "error", "info"],
28
+ "description": "Type of notification context for the detail page",
29
+ },
30
+ "summary": {
31
+ "type": "string",
32
+ "description": "Short summary shown on the detail page",
33
+ },
34
+ "details": {
35
+ "type": "array",
36
+ "items": {"type": "string"},
37
+ "description": "Bullet-point details",
38
+ },
39
+ "files_changed": {
40
+ "type": "array",
41
+ "items": {"type": "string"},
42
+ "description": "List of files that were changed",
43
+ },
44
+ "error_message": {
45
+ "type": "string",
46
+ "description": "Error message if context_type is error",
47
+ },
48
+ "next_steps": {
49
+ "type": "string",
50
+ "description": "Suggested next steps for the user",
51
+ },
52
+ },
53
+ "required": ["title", "body"],
54
+ },
55
+ }
56
+
57
+ PUSHARY_ASK = {
58
+ "name": "pushary_ask",
59
+ "description": (
60
+ "Ask the user a question via push notification and get their answer from "
61
+ "their phone's lock screen. Supports three question types: confirm (yes/no), "
62
+ "select (multiple choice with 2-6 options), and input (free text). Returns a "
63
+ "correlationId to use with pushary_wait. Use for decisions that need the "
64
+ "user's input when they may not be watching the terminal."
65
+ ),
66
+ "parameters": {
67
+ "type": "object",
68
+ "properties": {
69
+ "question": {
70
+ "type": "string",
71
+ "description": "The question to ask (max 500 chars)",
72
+ },
73
+ "type": {
74
+ "type": "string",
75
+ "enum": ["confirm", "select", "input"],
76
+ "description": "Question type: confirm (yes/no), select (pick from options), input (free text)",
77
+ "default": "confirm",
78
+ },
79
+ "options": {
80
+ "type": "array",
81
+ "items": {"type": "string"},
82
+ "description": "Choices for select type (2-6 options)",
83
+ },
84
+ "placeholder": {
85
+ "type": "string",
86
+ "description": "Placeholder text for input type",
87
+ },
88
+ "context": {
89
+ "type": "string",
90
+ "description": "What you're working on, shown above the question",
91
+ },
92
+ "agent_name": {
93
+ "type": "string",
94
+ "description": 'Identifies this Hermes instance (e.g., "Hermes - server-maintenance")',
95
+ },
96
+ },
97
+ "required": ["question"],
98
+ },
99
+ }
100
+
101
+ PUSHARY_WAIT = {
102
+ "name": "pushary_wait",
103
+ "description": (
104
+ "Wait for the user's answer to a question sent via pushary_ask. "
105
+ "Polls until the user responds or the timeout is reached. Call this "
106
+ "immediately after pushary_ask with the returned correlationId."
107
+ ),
108
+ "parameters": {
109
+ "type": "object",
110
+ "properties": {
111
+ "correlation_id": {
112
+ "type": "string",
113
+ "description": "The correlationId returned by pushary_ask",
114
+ },
115
+ "timeout_ms": {
116
+ "type": "integer",
117
+ "description": "How long to wait in milliseconds (default 30000, max 55000)",
118
+ "default": 30000,
119
+ },
120
+ },
121
+ "required": ["correlation_id"],
122
+ },
123
+ }
124
+
125
+ PUSHARY_CANCEL = {
126
+ "name": "pushary_cancel",
127
+ "description": (
128
+ "Cancel a pending question so it can no longer be answered. Use when "
129
+ "the question is no longer relevant."
130
+ ),
131
+ "parameters": {
132
+ "type": "object",
133
+ "properties": {
134
+ "correlation_id": {
135
+ "type": "string",
136
+ "description": "The correlationId of the question to cancel",
137
+ },
138
+ },
139
+ "required": ["correlation_id"],
140
+ },
141
+ }
@@ -0,0 +1,73 @@
1
+ import json
2
+ from . import api
3
+
4
+
5
+ def pushary_notify(params, **kwargs):
6
+ title = params.get("title", "")
7
+ body = params.get("body", "")
8
+ agent_name = params.get("agent_name")
9
+
10
+ context = None
11
+ context_type = params.get("context_type")
12
+ if context_type:
13
+ context = {"type": context_type}
14
+ for field in ("summary", "details", "files_changed", "error_message", "next_steps"):
15
+ val = params.get(field)
16
+ if val is not None:
17
+ camel = field.replace("_c", "C").replace("_m", "M").replace("_s", "S")
18
+ if field == "files_changed":
19
+ camel = "filesChanged"
20
+ elif field == "error_message":
21
+ camel = "errorMessage"
22
+ elif field == "next_steps":
23
+ camel = "nextSteps"
24
+ context[camel] = val
25
+
26
+ try:
27
+ result = api.send_notification(title, body, agent_name=agent_name, context=context)
28
+ return json.dumps(result)
29
+ except Exception as e:
30
+ return json.dumps({"error": str(e)})
31
+
32
+
33
+ def pushary_ask(params, **kwargs):
34
+ question = params.get("question", "")
35
+ question_type = params.get("type", "confirm")
36
+ options = params.get("options")
37
+ placeholder = params.get("placeholder")
38
+ context = params.get("context")
39
+ agent_name = params.get("agent_name")
40
+
41
+ try:
42
+ result = api.ask_user(
43
+ question,
44
+ question_type=question_type,
45
+ options=options,
46
+ placeholder=placeholder,
47
+ context=context,
48
+ agent_name=agent_name,
49
+ )
50
+ return json.dumps(result)
51
+ except Exception as e:
52
+ return json.dumps({"error": str(e)})
53
+
54
+
55
+ def pushary_wait(params, **kwargs):
56
+ correlation_id = params.get("correlation_id", "")
57
+ timeout_ms = params.get("timeout_ms", 30000)
58
+
59
+ try:
60
+ result = api.wait_for_answer(correlation_id, timeout_ms)
61
+ return json.dumps(result)
62
+ except Exception as e:
63
+ return json.dumps({"error": str(e)})
64
+
65
+
66
+ def pushary_cancel(params, **kwargs):
67
+ correlation_id = params.get("correlation_id", "")
68
+
69
+ try:
70
+ result = api.cancel_question(correlation_id)
71
+ return json.dumps(result)
72
+ except Exception as e:
73
+ return json.dumps({"error": str(e)})
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hermes-plugin-pushary"
7
+ version = "0.1.0"
8
+ description = "Pushary push notifications plugin for Hermes Agent"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ authors = [{name = "RalphNex OU", email = "business@pushary.com"}]
12
+ requires-python = ">=3.10"
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ ]
18
+
19
+ [project.urls]
20
+ Homepage = "https://pushary.com"
21
+ Repository = "https://github.com/pushary/pushary"
22
+
23
+ [project.entry-points."hermes_agent.plugins"]
24
+ pushary = "pushary_plugin"
25
+
26
+ [tool.setuptools.packages.find]
27
+ where = ["."]
28
+ include = ["pushary_plugin*"]
29
+
30
+ [tool.setuptools.package-data]
31
+ "*" = ["plugin.yaml", "skills/**/*.md"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+