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.
- hermes_plugin_pushary-0.1.0/PKG-INFO +67 -0
- hermes_plugin_pushary-0.1.0/README.md +53 -0
- hermes_plugin_pushary-0.1.0/hermes_plugin_pushary.egg-info/PKG-INFO +67 -0
- hermes_plugin_pushary-0.1.0/hermes_plugin_pushary.egg-info/SOURCES.txt +11 -0
- hermes_plugin_pushary-0.1.0/hermes_plugin_pushary.egg-info/dependency_links.txt +1 -0
- hermes_plugin_pushary-0.1.0/hermes_plugin_pushary.egg-info/entry_points.txt +2 -0
- hermes_plugin_pushary-0.1.0/hermes_plugin_pushary.egg-info/top_level.txt +1 -0
- hermes_plugin_pushary-0.1.0/pushary_plugin/__init__.py +115 -0
- hermes_plugin_pushary-0.1.0/pushary_plugin/api.py +87 -0
- hermes_plugin_pushary-0.1.0/pushary_plugin/schemas.py +141 -0
- hermes_plugin_pushary-0.1.0/pushary_plugin/tools.py +73 -0
- hermes_plugin_pushary-0.1.0/pyproject.toml +31 -0
- hermes_plugin_pushary-0.1.0/setup.cfg +4 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
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"]
|