aicq-hermes-plugin 1.0.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ctz168
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,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: aicq-hermes-plugin
3
+ Version: 1.0.0
4
+ Summary: AICQ platform adapter plugin for Hermes agent — end-to-end encrypted chat with NaCl, friend management, file transfer, and tool calling
5
+ Author-email: ctz168 <ctz168@outlook.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://aicq.online
8
+ Project-URL: Repository, https://github.com/ctz168/pluginAICQ
9
+ Keywords: aicq,hermes,agent,encrypted-chat,e2ee,nacl,platform-adapter,plugin
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Communications :: Chat
19
+ Classifier: Topic :: Security :: Cryptography
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: aiohttp>=3.9
24
+ Requires-Dist: pynacl>=1.5
25
+ Requires-Dist: websockets>=12.0
26
+ Requires-Dist: aiosqlite>=0.19
27
+ Dynamic: license-file
28
+
29
+ # AICQ Hermes Plugin
30
+
31
+ Connect [Hermes Agent](https://github.com/nousresearch/hermes-agent) to the [AICQ](https://aicq.online) end-to-end encrypted chat network.
32
+
33
+ ## Features
34
+
35
+ - **Auto Registration & Login** — Ed25519 challenge-response authentication, registers on first run, reuses identity on subsequent starts
36
+ - **Master Binding** — Automatically adds the specified owner user as friend on startup
37
+ - **Text / File / Image Chat** — Full messaging support via WebSocket relay + REST fallback
38
+ - **Tool Calling** — 6 AICQ tools registered with Hermes (status, friends, chat send, history, file send)
39
+ - **Auto-Accept Friends** — Automatically accepts incoming friend requests
40
+ - **Unread Polling** — 30s periodic poll + WS reconnect fetch to never miss messages
41
+ - **E2EE** — NaCl (X25519 + XSalsa20-Poly1305) end-to-end encryption
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install aicq-hermes-plugin
47
+ ```
48
+
49
+ Or install from source:
50
+
51
+ ```bash
52
+ cd pluginAICQ/hermes-plugin
53
+ pip install -e .
54
+ ```
55
+
56
+ ## Configuration
57
+
58
+ Set environment variables or configure in `~/.hermes/.env`:
59
+
60
+ | Variable | Required | Default | Description |
61
+ |----------|----------|---------|-------------|
62
+ | `AICQ_SERVER_URL` | Yes | `https://aicq.online` | AICQ server URL |
63
+ | `AICQ_MASTER_NUMBER` | Yes | — | AICQ number of the master/owner to auto-bind |
64
+ | `AICQ_DATA_DIR` | No | `~/.aicq-hermes` | Directory for identity and data |
65
+ | `AICQ_AUTO_ACCEPT_FRIENDS` | No | `true` | Auto-accept friend requests |
66
+
67
+ ## Hermes Plugin Setup
68
+
69
+ 1. Install the plugin:
70
+ ```bash
71
+ pip install aicq-hermes-plugin
72
+ ```
73
+
74
+ 2. Copy to Hermes plugins directory:
75
+ ```bash
76
+ cp -r aicq_hermes/ ~/.hermes/plugins/aicq/
77
+ cp PLUGIN.yaml ~/.hermes/plugins/aicq/
78
+ ```
79
+
80
+ 3. Configure environment:
81
+ ```bash
82
+ # In ~/.hermes/.env
83
+ AICQ_SERVER_URL=https://aicq.online
84
+ AICQ_MASTER_NUMBER=1000000
85
+ ```
86
+
87
+ 4. Start Hermes with the AICQ platform:
88
+ ```bash
89
+ hermes gateway run
90
+ ```
91
+
92
+ ## Registered Tools
93
+
94
+ | Tool | Description |
95
+ |------|-------------|
96
+ | `aicq_status` | Get connection status, agent ID, master info |
97
+ | `aicq_friends_list` | List all AICQ friends |
98
+ | `aicq_friends_add` | Add a friend by AICQ number |
99
+ | `aicq_chat_send` | Send a message (text/image/file) |
100
+ | `aicq_chat_history` | Get conversation history |
101
+ | `aicq_chat_send_file` | Send a file from local path |
102
+
103
+ ## Architecture
104
+
105
+ ```
106
+ Hermes Agent
107
+
108
+ ├── AicqPlatformAdapter (BasePlatformAdapter)
109
+ │ ├── connect() → register/login + bind master + start WS
110
+ │ ├── disconnect() → close WS + stop polling
111
+ │ ├── send() → relay message to AICQ friend
112
+ │ └── set_message_handler() → forward inbound to Hermes
113
+
114
+ ├── IdentityManager → Ed25519 + X25519 key persistence
115
+ ├── AicqServerClient → REST API + WebSocket client
116
+ └── ChatManager → message dispatch, unread polling, file transfer
117
+ ```
118
+
119
+ ## License
120
+
121
+ MIT
122
+
@@ -0,0 +1,94 @@
1
+ # AICQ Hermes Plugin
2
+
3
+ Connect [Hermes Agent](https://github.com/nousresearch/hermes-agent) to the [AICQ](https://aicq.online) end-to-end encrypted chat network.
4
+
5
+ ## Features
6
+
7
+ - **Auto Registration & Login** — Ed25519 challenge-response authentication, registers on first run, reuses identity on subsequent starts
8
+ - **Master Binding** — Automatically adds the specified owner user as friend on startup
9
+ - **Text / File / Image Chat** — Full messaging support via WebSocket relay + REST fallback
10
+ - **Tool Calling** — 6 AICQ tools registered with Hermes (status, friends, chat send, history, file send)
11
+ - **Auto-Accept Friends** — Automatically accepts incoming friend requests
12
+ - **Unread Polling** — 30s periodic poll + WS reconnect fetch to never miss messages
13
+ - **E2EE** — NaCl (X25519 + XSalsa20-Poly1305) end-to-end encryption
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install aicq-hermes-plugin
19
+ ```
20
+
21
+ Or install from source:
22
+
23
+ ```bash
24
+ cd pluginAICQ/hermes-plugin
25
+ pip install -e .
26
+ ```
27
+
28
+ ## Configuration
29
+
30
+ Set environment variables or configure in `~/.hermes/.env`:
31
+
32
+ | Variable | Required | Default | Description |
33
+ |----------|----------|---------|-------------|
34
+ | `AICQ_SERVER_URL` | Yes | `https://aicq.online` | AICQ server URL |
35
+ | `AICQ_MASTER_NUMBER` | Yes | — | AICQ number of the master/owner to auto-bind |
36
+ | `AICQ_DATA_DIR` | No | `~/.aicq-hermes` | Directory for identity and data |
37
+ | `AICQ_AUTO_ACCEPT_FRIENDS` | No | `true` | Auto-accept friend requests |
38
+
39
+ ## Hermes Plugin Setup
40
+
41
+ 1. Install the plugin:
42
+ ```bash
43
+ pip install aicq-hermes-plugin
44
+ ```
45
+
46
+ 2. Copy to Hermes plugins directory:
47
+ ```bash
48
+ cp -r aicq_hermes/ ~/.hermes/plugins/aicq/
49
+ cp PLUGIN.yaml ~/.hermes/plugins/aicq/
50
+ ```
51
+
52
+ 3. Configure environment:
53
+ ```bash
54
+ # In ~/.hermes/.env
55
+ AICQ_SERVER_URL=https://aicq.online
56
+ AICQ_MASTER_NUMBER=1000000
57
+ ```
58
+
59
+ 4. Start Hermes with the AICQ platform:
60
+ ```bash
61
+ hermes gateway run
62
+ ```
63
+
64
+ ## Registered Tools
65
+
66
+ | Tool | Description |
67
+ |------|-------------|
68
+ | `aicq_status` | Get connection status, agent ID, master info |
69
+ | `aicq_friends_list` | List all AICQ friends |
70
+ | `aicq_friends_add` | Add a friend by AICQ number |
71
+ | `aicq_chat_send` | Send a message (text/image/file) |
72
+ | `aicq_chat_history` | Get conversation history |
73
+ | `aicq_chat_send_file` | Send a file from local path |
74
+
75
+ ## Architecture
76
+
77
+ ```
78
+ Hermes Agent
79
+
80
+ ├── AicqPlatformAdapter (BasePlatformAdapter)
81
+ │ ├── connect() → register/login + bind master + start WS
82
+ │ ├── disconnect() → close WS + stop polling
83
+ │ ├── send() → relay message to AICQ friend
84
+ │ └── set_message_handler() → forward inbound to Hermes
85
+
86
+ ├── IdentityManager → Ed25519 + X25519 key persistence
87
+ ├── AicqServerClient → REST API + WebSocket client
88
+ └── ChatManager → message dispatch, unread polling, file transfer
89
+ ```
90
+
91
+ ## License
92
+
93
+ MIT
94
+
@@ -0,0 +1,10 @@
1
+ """
2
+ AICQ Hermes Plugin — Platform adapter for Hermes agent.
3
+
4
+ Connects Hermes to the AICQ end-to-end encrypted chat network.
5
+ Supports: login, registration, master binding, text/file/image chat, tool calling.
6
+ """
7
+
8
+ from .adapter import AicqPlatformAdapter
9
+
10
+ __all__ = ["AicqPlatformAdapter"]
@@ -0,0 +1,307 @@
1
+ """
2
+ AICQ Platform Adapter for Hermes Agent.
3
+
4
+ Implements BasePlatformAdapter to connect Hermes to the AICQ
5
+ end-to-end encrypted chat network. Supports:
6
+ - Auto registration & login (Ed25519 challenge-response)
7
+ - Master binding (auto-add specified AICQ user as friend)
8
+ - Text, file, and image messaging
9
+ - Tool calling via AICQ gateway methods
10
+ - Friend request auto-accept
11
+ - Unread message polling on reconnect
12
+ """
13
+
14
+ import asyncio
15
+ import logging
16
+ import os
17
+ from typing import Optional
18
+
19
+ from .identity import IdentityManager
20
+ from .server_client import AicqServerClient
21
+ from .chat import ChatManager
22
+
23
+ logger = logging.getLogger("aicq-hermes")
24
+
25
+
26
+ class AicqPlatformAdapter:
27
+ """
28
+ AICQ platform adapter for Hermes agent.
29
+
30
+ Usage in Hermes plugin register():
31
+ ctx.register_platform(
32
+ name="aicq",
33
+ label="AICQ Encrypted Chat",
34
+ adapter_factory=lambda cfg: AicqPlatformAdapter(cfg),
35
+ ...
36
+ )
37
+ """
38
+
39
+ def __init__(self, config: dict):
40
+ self.config = config
41
+ self.server_url = config.get("AICQ_SERVER_URL", "https://aicq.online")
42
+ self.master_number = config.get("AICQ_MASTER_NUMBER", "")
43
+ self.data_dir = config.get("AICQ_DATA_DIR", os.path.expanduser("~/.aicq-hermes"))
44
+ self.auto_accept = config.get("AICQ_AUTO_ACCEPT_FRIENDS", "true").lower() == "true"
45
+ self.agent_id = config.get("agent_id", "default")
46
+
47
+ os.makedirs(self.data_dir, exist_ok=True)
48
+
49
+ # Core components
50
+ self.identity = IdentityManager(os.path.join(self.data_dir, "identities"))
51
+ self.server = AicqServerClient(self.server_url, self.identity)
52
+ self.chat = ChatManager(self.server, self.data_dir)
53
+
54
+ # State
55
+ self._connected = False
56
+ self._master_bound = False
57
+ self._running = False
58
+ self._message_handler = None # Hermes handle_message callback
59
+
60
+ # ── Hermes Platform Adapter Interface ───────────────────────────────
61
+
62
+ async def connect(self) -> bool:
63
+ """Connect to AICQ server: authenticate, bind master, start WS."""
64
+ try:
65
+ logger.info(f"Connecting to AICQ server: {self.server_url}")
66
+
67
+ # 1. Register or login
68
+ self.identity.get_or_create(self.agent_id, "Hermes AICQ Agent")
69
+ await self.server.ensure_auth(self.agent_id)
70
+ self.server._current_agent_id = self.agent_id
71
+
72
+ # 2. Bind master (add the owner as friend)
73
+ if self.master_number and not self._master_bound:
74
+ await self._bind_master()
75
+
76
+ # 3. Auto-accept pending friend requests
77
+ if self.auto_accept:
78
+ await self._auto_accept_friends()
79
+
80
+ # 4. Sync friends from server
81
+ await self._sync_friends()
82
+
83
+ # 5. Connect WebSocket
84
+ self.chat.set_on_new_message(self._on_inbound_message)
85
+ await self.server.connect_ws(self.agent_id)
86
+
87
+ # 6. Start unread polling
88
+ await self.chat.start_polling()
89
+
90
+ # 7. Fetch initial unread
91
+ await self._fetch_initial_unread()
92
+
93
+ self._connected = True
94
+ self._running = True
95
+ logger.info("AICQ connected successfully")
96
+ return True
97
+
98
+ except Exception as e:
99
+ logger.error(f"AICQ connection failed: {e}")
100
+ return False
101
+
102
+ async def disconnect(self) -> None:
103
+ """Disconnect from AICQ server."""
104
+ self._running = False
105
+ await self.chat.stop_polling()
106
+ await self.server.close()
107
+ self._connected = False
108
+ logger.info("AICQ disconnected")
109
+
110
+ async def send(self, chat_id: str, content: str, reply_to=None, metadata=None):
111
+ """Send a message to a chat (friend or group).
112
+
113
+ This is the primary send method called by Hermes gateway.
114
+ """
115
+ if not self._connected:
116
+ logger.warning("Not connected, cannot send message")
117
+ return None
118
+
119
+ is_group = metadata.get("is_group", False) if metadata else False
120
+ msg_type = metadata.get("msg_type", "text") if metadata else "text"
121
+
122
+ # Handle file/image sending
123
+ file_path = metadata.get("file_path") if metadata else None
124
+ if file_path and os.path.exists(file_path):
125
+ success = await self.chat.send_file(chat_id, file_path)
126
+ if success:
127
+ return {"status": "sent", "type": "file"}
128
+
129
+ # Text message
130
+ success = await self.chat.send_message(
131
+ target_id=chat_id,
132
+ content=content,
133
+ msg_type=msg_type,
134
+ is_group=is_group,
135
+ )
136
+
137
+ if success:
138
+ return {"status": "sent", "type": msg_type}
139
+ return None
140
+
141
+ async def send_typing(self, chat_id: str = None):
142
+ """Send typing indicator (optional, not natively supported by AICQ)."""
143
+ pass
144
+
145
+ def get_chat_info(self) -> dict:
146
+ """Return platform metadata for Hermes."""
147
+ return {
148
+ "platform": "aicq",
149
+ "server_url": self.server_url,
150
+ "connected": self._connected,
151
+ "agent_id": self.agent_id,
152
+ "master_bound": self._master_bound,
153
+ }
154
+
155
+ # ── Master Binding ──────────────────────────────────────────────────
156
+
157
+ async def _bind_master(self):
158
+ """Add the master/owner AICQ user as a friend automatically."""
159
+ try:
160
+ result = await self.server.add_friend_by_number(self.master_number)
161
+ status = result.get("status", "")
162
+ if status == "accepted" or result.get("to_id"):
163
+ self._master_bound = True
164
+ logger.info(f"Master bound: {self.master_number} (accepted)")
165
+ else:
166
+ logger.info(f"Master friend request sent: {self.master_number} (status: {status})")
167
+ self._master_bound = True # Request sent, will be accepted
168
+ except Exception as e:
169
+ logger.warning(f"Master bind failed: {e}")
170
+
171
+ # ── Friend Management ───────────────────────────────────────────────
172
+
173
+ async def _auto_accept_friends(self):
174
+ """Auto-accept pending friend requests."""
175
+ try:
176
+ requests = await self.server.list_friend_requests()
177
+ for req in requests:
178
+ req_id = req.get("id") or req.get("request_id") or req.get("session_id")
179
+ if req_id:
180
+ try:
181
+ await self.server.accept_friend_request(req_id)
182
+ logger.info(f"Auto-accepted friend request: {req_id}")
183
+ except Exception as e:
184
+ logger.warning(f"Auto-accept failed for {req_id}: {e}")
185
+ except Exception as e:
186
+ logger.warning(f"List friend requests failed: {e}")
187
+
188
+ async def _sync_friends(self):
189
+ """Sync friends from server into local state."""
190
+ try:
191
+ friends = await self.server.list_friends()
192
+ logger.info(f"Synced {len(friends)} friends from server")
193
+ except Exception as e:
194
+ logger.warning(f"Friends sync failed: {e}")
195
+
196
+ # ── Message Dispatch ────────────────────────────────────────────────
197
+
198
+ async def _on_inbound_message(self, msg: dict):
199
+ """Handle inbound AICQ message and forward to Hermes gateway."""
200
+ from_id = msg.get("from_id")
201
+ content = msg.get("content", "")
202
+ is_group = msg.get("is_group", False)
203
+
204
+ # Skip self messages
205
+ if from_id == self.server.server_account_id:
206
+ return
207
+
208
+ # Skip empty
209
+ if not content or not content.strip():
210
+ return
211
+
212
+ logger.info(f"Inbound message from {from_id}: {str(content)[:80]}")
213
+
214
+ # Forward to Hermes via the registered message handler
215
+ if self._message_handler:
216
+ try:
217
+ event = AicqMessageEvent(
218
+ chat_id=from_id if not is_group else msg.get("to_id"),
219
+ content=content,
220
+ sender_id=from_id,
221
+ is_group=is_group,
222
+ msg_type=msg.get("type", "text"),
223
+ raw=msg,
224
+ )
225
+ await self._message_handler(event)
226
+ except Exception as e:
227
+ logger.error(f"Message handler error: {e}")
228
+
229
+ def set_message_handler(self, handler):
230
+ """Set the Hermes message handler (called by gateway to receive messages)."""
231
+ self._message_handler = handler
232
+
233
+ async def _fetch_initial_unread(self):
234
+ """Fetch unread messages from all friends on startup."""
235
+ try:
236
+ friends = await self.server.list_friends()
237
+ for f in friends:
238
+ fid = f.get("id") or f.get("friend_id")
239
+ if fid:
240
+ await self.chat._fetch_unread(fid)
241
+ logger.info(f"Fetched initial unread from {len(friends)} friends")
242
+ except Exception as e:
243
+ logger.warning(f"Initial unread fetch failed: {e}")
244
+
245
+ # ── Tool Calling Support ────────────────────────────────────────────
246
+
247
+ async def aicq_status(self) -> dict:
248
+ """Get AICQ plugin status."""
249
+ return {
250
+ "connected": self._connected,
251
+ "server_url": self.server_url,
252
+ "agent_id": self.agent_id,
253
+ "server_account_id": self.server.server_account_id,
254
+ "master_bound": self._master_bound,
255
+ }
256
+
257
+ async def aicq_friends_list(self) -> list:
258
+ """List all friends."""
259
+ return await self.server.list_friends()
260
+
261
+ async def aicq_friends_add(self, aicq_number: str) -> dict:
262
+ """Add a friend by AICQ number."""
263
+ return await self.server.add_friend_by_number(aicq_number)
264
+
265
+ async def aicq_chat_send(self, target_id: str, content: str, msg_type: str = "text") -> dict:
266
+ """Send a chat message to a specific user."""
267
+ success = await self.chat.send_message(target_id, content, msg_type)
268
+ return {"success": success}
269
+
270
+ async def aicq_chat_history(self, friend_id: str, limit: int = 50) -> dict:
271
+ """Get chat history with a friend."""
272
+ return await self.server.get_conversation(friend_id, limit)
273
+
274
+ async def aicq_chat_send_file(self, target_id: str, file_path: str) -> dict:
275
+ """Send a file to a friend."""
276
+ success = await self.chat.send_file(target_id, file_path)
277
+ return {"success": success}
278
+
279
+ async def aicq_accept_friend_request(self, request_id: str) -> dict:
280
+ """Accept a friend request."""
281
+ return await self.server.accept_friend_request(request_id)
282
+
283
+
284
+ class AicqMessageEvent:
285
+ """Normalized message event for Hermes gateway."""
286
+
287
+ def __init__(self, chat_id: str, content: str, sender_id: str,
288
+ is_group: bool = False, msg_type: str = "text", raw: dict = None):
289
+ self.chat_id = chat_id
290
+ self.content = content
291
+ self.sender_id = sender_id
292
+ self.is_group = is_group
293
+ self.msg_type = msg_type
294
+ self.raw = raw or {}
295
+
296
+ @property
297
+ def text(self) -> str:
298
+ return self.content
299
+
300
+ def to_dict(self) -> dict:
301
+ return {
302
+ "chat_id": self.chat_id,
303
+ "content": self.content,
304
+ "sender_id": self.sender_id,
305
+ "is_group": self.is_group,
306
+ "msg_type": self.msg_type,
307
+ }