claw-msg 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.
- claw_msg-0.1.0/PKG-INFO +106 -0
- claw_msg-0.1.0/README.md +74 -0
- claw_msg-0.1.0/pyproject.toml +54 -0
- claw_msg-0.1.0/setup.cfg +4 -0
- claw_msg-0.1.0/src/claw_msg/__init__.py +6 -0
- claw_msg-0.1.0/src/claw_msg/cli/__init__.py +0 -0
- claw_msg-0.1.0/src/claw_msg/cli/main.py +211 -0
- claw_msg-0.1.0/src/claw_msg/client/__init__.py +0 -0
- claw_msg-0.1.0/src/claw_msg/client/agent.py +148 -0
- claw_msg-0.1.0/src/claw_msg/client/connection.py +132 -0
- claw_msg-0.1.0/src/claw_msg/client/credentials.py +43 -0
- claw_msg-0.1.0/src/claw_msg/client/http.py +95 -0
- claw_msg-0.1.0/src/claw_msg/common/__init__.py +0 -0
- claw_msg-0.1.0/src/claw_msg/common/errors.py +21 -0
- claw_msg-0.1.0/src/claw_msg/common/models.py +96 -0
- claw_msg-0.1.0/src/claw_msg/common/protocol.py +21 -0
- claw_msg-0.1.0/src/claw_msg/daemon/__init__.py +0 -0
- claw_msg-0.1.0/src/claw_msg/daemon/runner.py +25 -0
- claw_msg-0.1.0/src/claw_msg/daemon/service.py +76 -0
- claw_msg-0.1.0/src/claw_msg/daemon/webhook.py +22 -0
- claw_msg-0.1.0/src/claw_msg/server/__init__.py +0 -0
- claw_msg-0.1.0/src/claw_msg/server/app.py +49 -0
- claw_msg-0.1.0/src/claw_msg/server/auth.py +69 -0
- claw_msg-0.1.0/src/claw_msg/server/broker.py +64 -0
- claw_msg-0.1.0/src/claw_msg/server/config.py +14 -0
- claw_msg-0.1.0/src/claw_msg/server/database.py +95 -0
- claw_msg-0.1.0/src/claw_msg/server/offline_queue.py +75 -0
- claw_msg-0.1.0/src/claw_msg/server/presence.py +43 -0
- claw_msg-0.1.0/src/claw_msg/server/rate_limit.py +39 -0
- claw_msg-0.1.0/src/claw_msg/server/routes_agents.py +130 -0
- claw_msg-0.1.0/src/claw_msg/server/routes_messages.py +114 -0
- claw_msg-0.1.0/src/claw_msg/server/routes_rooms.py +129 -0
- claw_msg-0.1.0/src/claw_msg/server/routes_ws.py +190 -0
- claw_msg-0.1.0/src/claw_msg.egg-info/PKG-INFO +106 -0
- claw_msg-0.1.0/src/claw_msg.egg-info/SOURCES.txt +43 -0
- claw_msg-0.1.0/src/claw_msg.egg-info/dependency_links.txt +1 -0
- claw_msg-0.1.0/src/claw_msg.egg-info/entry_points.txt +2 -0
- claw_msg-0.1.0/src/claw_msg.egg-info/requires.txt +14 -0
- claw_msg-0.1.0/src/claw_msg.egg-info/top_level.txt +1 -0
- claw_msg-0.1.0/tests/test_direct_messaging.py +53 -0
- claw_msg-0.1.0/tests/test_offline_queue.py +49 -0
- claw_msg-0.1.0/tests/test_registration.py +50 -0
- claw_msg-0.1.0/tests/test_rooms.py +93 -0
- claw_msg-0.1.0/tests/test_sdk_agent.py +62 -0
- claw_msg-0.1.0/tests/test_websocket.py +92 -0
claw_msg-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: claw-msg
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent-to-agent messaging layer — pip install → 5 lines → agents talk
|
|
5
|
+
Author-email: Sangbum <snuhsb@snu.ac.kr>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: agent,messaging,websocket,ai,multi-agent
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Framework :: FastAPI
|
|
15
|
+
Classifier: Topic :: Communications
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: fastapi>=0.110.0
|
|
20
|
+
Requires-Dist: uvicorn[standard]>=0.27.0
|
|
21
|
+
Requires-Dist: aiosqlite>=0.20.0
|
|
22
|
+
Requires-Dist: bcrypt>=4.1.0
|
|
23
|
+
Requires-Dist: websockets>=12.0
|
|
24
|
+
Requires-Dist: httpx>=0.27.0
|
|
25
|
+
Requires-Dist: pydantic>=2.0.0
|
|
26
|
+
Requires-Dist: click>=8.1.0
|
|
27
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
31
|
+
Requires-Dist: httpx>=0.27.0; extra == "dev"
|
|
32
|
+
|
|
33
|
+
# claw-msg
|
|
34
|
+
|
|
35
|
+
Agent-to-agent messaging layer. `pip install → 5 lines → agents talk`.
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install -e .
|
|
41
|
+
|
|
42
|
+
# Start the broker
|
|
43
|
+
claw-msg serve --port 8000
|
|
44
|
+
|
|
45
|
+
# Register agents (in separate terminals)
|
|
46
|
+
claw-msg register --name agent-a --broker http://localhost:8000
|
|
47
|
+
claw-msg register --name agent-b --broker http://localhost:8000
|
|
48
|
+
|
|
49
|
+
# Send a message
|
|
50
|
+
claw-msg send --to <agent-b-id> --broker http://localhost:8000 --token <token-a> "hello!"
|
|
51
|
+
|
|
52
|
+
# Listen for messages
|
|
53
|
+
claw-msg listen --broker http://localhost:8000 --token <token-b>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## SDK Usage
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from claw_msg import Agent
|
|
60
|
+
import asyncio
|
|
61
|
+
|
|
62
|
+
async def main():
|
|
63
|
+
agent = Agent("http://localhost:8000", name="my-agent")
|
|
64
|
+
await agent.register()
|
|
65
|
+
print(f"registered: {agent.agent_id}")
|
|
66
|
+
|
|
67
|
+
# Send a direct message
|
|
68
|
+
await agent.send("<other-agent-id>", "hello!")
|
|
69
|
+
|
|
70
|
+
# Create and use rooms
|
|
71
|
+
room = await agent.create_room("general")
|
|
72
|
+
await agent.send_to_room(room["id"], "hello room!")
|
|
73
|
+
|
|
74
|
+
asyncio.run(main())
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Features
|
|
78
|
+
|
|
79
|
+
- **WebSocket real-time messaging** with auto-reconnect
|
|
80
|
+
- **HTTP polling fallback** for stateless agents
|
|
81
|
+
- **Rooms** for group messaging
|
|
82
|
+
- **Offline queue** with 7-day TTL and at-least-once delivery
|
|
83
|
+
- **Agent discovery** by name/capabilities
|
|
84
|
+
- **Rate limiting** (60 msg/min token bucket)
|
|
85
|
+
- **Presence tracking** (online/offline/last_seen)
|
|
86
|
+
- **CLI** for all operations
|
|
87
|
+
- **Daemon mode** with webhook forwarding + systemd/launchd service generation
|
|
88
|
+
|
|
89
|
+
## Architecture
|
|
90
|
+
|
|
91
|
+
Same package provides both **broker** (server) and **SDK** (client):
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
┌──────────┐ WebSocket ┌──────────┐ WebSocket ┌──────────┐
|
|
95
|
+
│ Agent A │◄────────────►│ Broker │◄────────────►│ Agent B │
|
|
96
|
+
└──────────┘ └──────────┘ └──────────┘
|
|
97
|
+
│
|
|
98
|
+
SQLite + WAL
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Development
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install -e ".[dev]"
|
|
105
|
+
pytest tests/ -v
|
|
106
|
+
```
|
claw_msg-0.1.0/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# claw-msg
|
|
2
|
+
|
|
3
|
+
Agent-to-agent messaging layer. `pip install → 5 lines → agents talk`.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -e .
|
|
9
|
+
|
|
10
|
+
# Start the broker
|
|
11
|
+
claw-msg serve --port 8000
|
|
12
|
+
|
|
13
|
+
# Register agents (in separate terminals)
|
|
14
|
+
claw-msg register --name agent-a --broker http://localhost:8000
|
|
15
|
+
claw-msg register --name agent-b --broker http://localhost:8000
|
|
16
|
+
|
|
17
|
+
# Send a message
|
|
18
|
+
claw-msg send --to <agent-b-id> --broker http://localhost:8000 --token <token-a> "hello!"
|
|
19
|
+
|
|
20
|
+
# Listen for messages
|
|
21
|
+
claw-msg listen --broker http://localhost:8000 --token <token-b>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## SDK Usage
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from claw_msg import Agent
|
|
28
|
+
import asyncio
|
|
29
|
+
|
|
30
|
+
async def main():
|
|
31
|
+
agent = Agent("http://localhost:8000", name="my-agent")
|
|
32
|
+
await agent.register()
|
|
33
|
+
print(f"registered: {agent.agent_id}")
|
|
34
|
+
|
|
35
|
+
# Send a direct message
|
|
36
|
+
await agent.send("<other-agent-id>", "hello!")
|
|
37
|
+
|
|
38
|
+
# Create and use rooms
|
|
39
|
+
room = await agent.create_room("general")
|
|
40
|
+
await agent.send_to_room(room["id"], "hello room!")
|
|
41
|
+
|
|
42
|
+
asyncio.run(main())
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- **WebSocket real-time messaging** with auto-reconnect
|
|
48
|
+
- **HTTP polling fallback** for stateless agents
|
|
49
|
+
- **Rooms** for group messaging
|
|
50
|
+
- **Offline queue** with 7-day TTL and at-least-once delivery
|
|
51
|
+
- **Agent discovery** by name/capabilities
|
|
52
|
+
- **Rate limiting** (60 msg/min token bucket)
|
|
53
|
+
- **Presence tracking** (online/offline/last_seen)
|
|
54
|
+
- **CLI** for all operations
|
|
55
|
+
- **Daemon mode** with webhook forwarding + systemd/launchd service generation
|
|
56
|
+
|
|
57
|
+
## Architecture
|
|
58
|
+
|
|
59
|
+
Same package provides both **broker** (server) and **SDK** (client):
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
┌──────────┐ WebSocket ┌──────────┐ WebSocket ┌──────────┐
|
|
63
|
+
│ Agent A │◄────────────►│ Broker │◄────────────►│ Agent B │
|
|
64
|
+
└──────────┘ └──────────┘ └──────────┘
|
|
65
|
+
│
|
|
66
|
+
SQLite + WAL
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Development
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install -e ".[dev]"
|
|
73
|
+
pytest tests/ -v
|
|
74
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "claw-msg"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Agent-to-agent messaging layer — pip install → 5 lines → agents talk"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = {text = "MIT"}
|
|
7
|
+
authors = [
|
|
8
|
+
{name = "Sangbum", email = "snuhsb@snu.ac.kr"}
|
|
9
|
+
]
|
|
10
|
+
keywords = ["agent", "messaging", "websocket", "ai", "multi-agent"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Framework :: FastAPI",
|
|
19
|
+
"Topic :: Communications",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
]
|
|
22
|
+
requires-python = ">=3.11"
|
|
23
|
+
dependencies = [
|
|
24
|
+
"fastapi>=0.110.0",
|
|
25
|
+
"uvicorn[standard]>=0.27.0",
|
|
26
|
+
"aiosqlite>=0.20.0",
|
|
27
|
+
"bcrypt>=4.1.0",
|
|
28
|
+
"websockets>=12.0",
|
|
29
|
+
"httpx>=0.27.0",
|
|
30
|
+
"pydantic>=2.0.0",
|
|
31
|
+
"click>=8.1.0",
|
|
32
|
+
"python-dotenv>=1.0.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=8.0.0",
|
|
38
|
+
"pytest-asyncio>=0.23.0",
|
|
39
|
+
"httpx>=0.27.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
claw-msg = "claw_msg.cli.main:cli"
|
|
44
|
+
|
|
45
|
+
[build-system]
|
|
46
|
+
requires = ["setuptools>=68.0"]
|
|
47
|
+
build-backend = "setuptools.build_meta"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["src"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
asyncio_mode = "auto"
|
|
54
|
+
testpaths = ["tests"]
|
claw_msg-0.1.0/setup.cfg
ADDED
|
File without changes
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""claw-msg CLI — register, send, listen, serve, rooms, daemon."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group()
|
|
13
|
+
def cli():
|
|
14
|
+
"""claw-msg — Agent-to-agent messaging layer."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@cli.command()
|
|
19
|
+
@click.option("--host", default="0.0.0.0", help="Host to bind to")
|
|
20
|
+
@click.option("--port", default=8000, type=int, help="Port to bind to")
|
|
21
|
+
def serve(host: str, port: int):
|
|
22
|
+
"""Start the claw-msg broker server."""
|
|
23
|
+
import uvicorn
|
|
24
|
+
from claw_msg.server.app import app
|
|
25
|
+
|
|
26
|
+
uvicorn.run(app, host=host, port=port)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@cli.command()
|
|
30
|
+
@click.option("--name", required=True, help="Agent name")
|
|
31
|
+
@click.option("--broker", required=True, help="Broker URL")
|
|
32
|
+
@click.option("--capabilities", default="", help="Comma-separated capabilities")
|
|
33
|
+
@click.option("--application", is_flag=True, help="Register as application agent")
|
|
34
|
+
def register(name: str, broker: str, capabilities: str, application: bool):
|
|
35
|
+
"""Register a new agent with the broker."""
|
|
36
|
+
|
|
37
|
+
async def _register():
|
|
38
|
+
from claw_msg.client.agent import Agent
|
|
39
|
+
|
|
40
|
+
caps = [c.strip() for c in capabilities.split(",") if c.strip()] if capabilities else []
|
|
41
|
+
agent = Agent(broker, name=name, capabilities=caps)
|
|
42
|
+
agent_id = await agent.register()
|
|
43
|
+
click.echo(f"Registered: {agent_id}")
|
|
44
|
+
click.echo(f"Token: {agent.token}")
|
|
45
|
+
click.echo(f"Credentials saved to ~/.claw-msg/credentials.json")
|
|
46
|
+
|
|
47
|
+
asyncio.run(_register())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@cli.command()
|
|
51
|
+
@click.option("--to", "to_agent", required=True, help="Recipient agent ID")
|
|
52
|
+
@click.option("--broker", required=True, help="Broker URL")
|
|
53
|
+
@click.option("--token", required=True, help="Your agent token")
|
|
54
|
+
@click.argument("message")
|
|
55
|
+
def send(to_agent: str, broker: str, token: str, message: str):
|
|
56
|
+
"""Send a message to another agent."""
|
|
57
|
+
|
|
58
|
+
async def _send():
|
|
59
|
+
from claw_msg.client.agent import Agent
|
|
60
|
+
|
|
61
|
+
agent = Agent(broker, token=token)
|
|
62
|
+
result = await agent.send(to_agent, message)
|
|
63
|
+
click.echo(f"Sent: {result.get('id', 'ok') if result else 'ok'}")
|
|
64
|
+
|
|
65
|
+
asyncio.run(_send())
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@cli.command()
|
|
69
|
+
@click.option("--broker", required=True, help="Broker URL")
|
|
70
|
+
@click.option("--token", required=True, help="Your agent token")
|
|
71
|
+
def listen(broker: str, token: str):
|
|
72
|
+
"""Listen for incoming messages via WebSocket."""
|
|
73
|
+
|
|
74
|
+
async def _listen():
|
|
75
|
+
from claw_msg.client.agent import Agent
|
|
76
|
+
|
|
77
|
+
agent = Agent(broker, token=token)
|
|
78
|
+
|
|
79
|
+
@agent.on_message
|
|
80
|
+
async def handle(msg):
|
|
81
|
+
click.echo(f"[{msg.get('from_agent', '?')}] {msg.get('content', '')}")
|
|
82
|
+
|
|
83
|
+
click.echo("Listening for messages (Ctrl+C to stop)...")
|
|
84
|
+
await agent.listen()
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
asyncio.run(_listen())
|
|
88
|
+
except KeyboardInterrupt:
|
|
89
|
+
click.echo("\nStopped.")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@cli.group()
|
|
93
|
+
def rooms():
|
|
94
|
+
"""Room management commands."""
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@rooms.command("create")
|
|
99
|
+
@click.option("--broker", required=True, help="Broker URL")
|
|
100
|
+
@click.option("--token", required=True, help="Your agent token")
|
|
101
|
+
@click.option("--name", required=True, help="Room name")
|
|
102
|
+
@click.option("--description", default="", help="Room description")
|
|
103
|
+
def room_create(broker: str, token: str, name: str, description: str):
|
|
104
|
+
"""Create a new room."""
|
|
105
|
+
|
|
106
|
+
async def _create():
|
|
107
|
+
from claw_msg.client.agent import Agent
|
|
108
|
+
|
|
109
|
+
agent = Agent(broker, token=token)
|
|
110
|
+
result = await agent.create_room(name=name, description=description)
|
|
111
|
+
click.echo(f"Room created: {result.get('id', '')}")
|
|
112
|
+
|
|
113
|
+
asyncio.run(_create())
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@rooms.command("join")
|
|
117
|
+
@click.option("--broker", required=True, help="Broker URL")
|
|
118
|
+
@click.option("--token", required=True, help="Your agent token")
|
|
119
|
+
@click.option("--room-id", required=True, help="Room ID to join")
|
|
120
|
+
def room_join(broker: str, token: str, room_id: str):
|
|
121
|
+
"""Join an existing room."""
|
|
122
|
+
|
|
123
|
+
async def _join():
|
|
124
|
+
from claw_msg.client.agent import Agent
|
|
125
|
+
|
|
126
|
+
agent = Agent(broker, token=token)
|
|
127
|
+
await agent.join_room(room_id)
|
|
128
|
+
click.echo(f"Joined room: {room_id}")
|
|
129
|
+
|
|
130
|
+
asyncio.run(_join())
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@rooms.command("leave")
|
|
134
|
+
@click.option("--broker", required=True, help="Broker URL")
|
|
135
|
+
@click.option("--token", required=True, help="Your agent token")
|
|
136
|
+
@click.option("--room-id", required=True, help="Room ID to leave")
|
|
137
|
+
def room_leave(broker: str, token: str, room_id: str):
|
|
138
|
+
"""Leave a room."""
|
|
139
|
+
|
|
140
|
+
async def _leave():
|
|
141
|
+
from claw_msg.client.agent import Agent
|
|
142
|
+
|
|
143
|
+
agent = Agent(broker, token=token)
|
|
144
|
+
await agent.leave_room(room_id)
|
|
145
|
+
click.echo(f"Left room: {room_id}")
|
|
146
|
+
|
|
147
|
+
asyncio.run(_leave())
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@cli.command()
|
|
151
|
+
@click.option("--broker", required=True, help="Broker URL")
|
|
152
|
+
@click.option("--token", required=True, help="Your agent token")
|
|
153
|
+
@click.option("--webhook", required=True, help="Webhook URL to forward messages to")
|
|
154
|
+
def daemon(broker: str, token: str, webhook: str):
|
|
155
|
+
"""Run as a daemon — forward messages to a webhook URL."""
|
|
156
|
+
|
|
157
|
+
async def _run():
|
|
158
|
+
from claw_msg.daemon.runner import run_daemon
|
|
159
|
+
|
|
160
|
+
await run_daemon(broker, token, webhook)
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
asyncio.run(_run())
|
|
164
|
+
except KeyboardInterrupt:
|
|
165
|
+
click.echo("\nDaemon stopped.")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@cli.command("install-service")
|
|
169
|
+
@click.option("--broker", required=True, help="Broker URL")
|
|
170
|
+
@click.option("--token", required=True, help="Your agent token")
|
|
171
|
+
@click.option("--webhook", required=True, help="Webhook URL")
|
|
172
|
+
@click.option("--type", "svc_type", type=click.Choice(["systemd", "launchd"]), default="systemd")
|
|
173
|
+
def install_service(broker: str, token: str, webhook: str, svc_type: str):
|
|
174
|
+
"""Install a system service for the daemon."""
|
|
175
|
+
from claw_msg.daemon.service import install_launchd_service, install_systemd_service
|
|
176
|
+
|
|
177
|
+
if svc_type == "systemd":
|
|
178
|
+
path = install_systemd_service(broker, token, webhook)
|
|
179
|
+
click.echo(f"Systemd service installed: {path}")
|
|
180
|
+
click.echo("Run: systemctl --user enable --now claw-msg-daemon")
|
|
181
|
+
else:
|
|
182
|
+
path = install_launchd_service(broker, token, webhook)
|
|
183
|
+
click.echo(f"Launchd plist installed: {path}")
|
|
184
|
+
click.echo("Run: launchctl load ~/Library/LaunchAgents/com.claw-msg.daemon.plist")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@cli.command("agents")
|
|
188
|
+
@click.option("--broker", required=True, help="Broker URL")
|
|
189
|
+
@click.option("--name", default=None, help="Search by name")
|
|
190
|
+
def list_agents(broker: str, name: str | None):
|
|
191
|
+
"""Search for agents on the broker."""
|
|
192
|
+
|
|
193
|
+
async def _search():
|
|
194
|
+
from claw_msg.client.http import HttpClient
|
|
195
|
+
|
|
196
|
+
http = HttpClient(broker, "")
|
|
197
|
+
async with __import__("httpx").AsyncClient() as c:
|
|
198
|
+
params = {}
|
|
199
|
+
if name:
|
|
200
|
+
params["name"] = name
|
|
201
|
+
resp = await c.get(f"{broker.rstrip('/')}/agents/", params=params)
|
|
202
|
+
resp.raise_for_status()
|
|
203
|
+
for agent in resp.json():
|
|
204
|
+
status = agent.get("status", "?")
|
|
205
|
+
click.echo(f" {agent['id']} {agent['name']} [{status}]")
|
|
206
|
+
|
|
207
|
+
asyncio.run(_search())
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
if __name__ == "__main__":
|
|
211
|
+
cli()
|
|
File without changes
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""High-level Agent SDK — the main public interface for claw-msg."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Any, Callable, Coroutine
|
|
7
|
+
|
|
8
|
+
from claw_msg.client.connection import Connection
|
|
9
|
+
from claw_msg.client.credentials import store_credentials
|
|
10
|
+
from claw_msg.client.http import HttpClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Agent:
|
|
14
|
+
"""
|
|
15
|
+
Agent SDK — register, send/receive messages, join rooms.
|
|
16
|
+
|
|
17
|
+
Usage::
|
|
18
|
+
|
|
19
|
+
agent = Agent("http://localhost:8000", name="my-agent")
|
|
20
|
+
await agent.register()
|
|
21
|
+
await agent.send("agent-id-here", "hello!")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
broker_url: str,
|
|
27
|
+
name: str = "unnamed-agent",
|
|
28
|
+
capabilities: list[str] | None = None,
|
|
29
|
+
metadata: dict[str, Any] | None = None,
|
|
30
|
+
token: str | None = None,
|
|
31
|
+
agent_id: str | None = None,
|
|
32
|
+
):
|
|
33
|
+
self._broker_url = broker_url.rstrip("/")
|
|
34
|
+
self._name = name
|
|
35
|
+
self._capabilities = capabilities or []
|
|
36
|
+
self._metadata = metadata or {}
|
|
37
|
+
self._token = token
|
|
38
|
+
self._agent_id = agent_id
|
|
39
|
+
self._connection: Connection | None = None
|
|
40
|
+
self._http: HttpClient | None = None
|
|
41
|
+
self._message_handlers: list[Callable[[dict], Coroutine]] = []
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def agent_id(self) -> str | None:
|
|
45
|
+
return self._agent_id
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def token(self) -> str | None:
|
|
49
|
+
return self._token
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def connected(self) -> bool:
|
|
53
|
+
return self._connection is not None and self._connection.connected
|
|
54
|
+
|
|
55
|
+
async def register(self) -> str:
|
|
56
|
+
"""Register this agent with the broker. Returns agent_id."""
|
|
57
|
+
http = HttpClient(self._broker_url, "")
|
|
58
|
+
result = await http.register(
|
|
59
|
+
name=self._name,
|
|
60
|
+
capabilities=self._capabilities,
|
|
61
|
+
metadata=self._metadata,
|
|
62
|
+
)
|
|
63
|
+
self._agent_id = result["agent_id"]
|
|
64
|
+
self._token = result["token"]
|
|
65
|
+
self._http = HttpClient(self._broker_url, self._token)
|
|
66
|
+
|
|
67
|
+
store_credentials(self._broker_url, self._agent_id, self._token, self._name)
|
|
68
|
+
return self._agent_id
|
|
69
|
+
|
|
70
|
+
def on_message(self, handler: Callable[[dict], Coroutine]):
|
|
71
|
+
"""Register a message handler (decorator-style)."""
|
|
72
|
+
self._message_handlers.append(handler)
|
|
73
|
+
return handler
|
|
74
|
+
|
|
75
|
+
async def _dispatch_message(self, msg: dict):
|
|
76
|
+
for handler in self._message_handlers:
|
|
77
|
+
await handler(msg)
|
|
78
|
+
|
|
79
|
+
async def connect(self):
|
|
80
|
+
"""Establish WebSocket connection to the broker."""
|
|
81
|
+
if not self._token:
|
|
82
|
+
raise RuntimeError("Must register or provide a token before connecting")
|
|
83
|
+
self._connection = Connection(
|
|
84
|
+
self._broker_url,
|
|
85
|
+
self._token,
|
|
86
|
+
on_message=self._dispatch_message,
|
|
87
|
+
)
|
|
88
|
+
self._agent_id = await self._connection.connect()
|
|
89
|
+
self._http = HttpClient(self._broker_url, self._token)
|
|
90
|
+
|
|
91
|
+
async def listen(self):
|
|
92
|
+
"""Start listening for messages (blocking). Auto-reconnects."""
|
|
93
|
+
if not self._token:
|
|
94
|
+
raise RuntimeError("Must register or provide a token before listening")
|
|
95
|
+
self._connection = Connection(
|
|
96
|
+
self._broker_url,
|
|
97
|
+
self._token,
|
|
98
|
+
on_message=self._dispatch_message,
|
|
99
|
+
)
|
|
100
|
+
await self._connection.listen()
|
|
101
|
+
|
|
102
|
+
async def send(self, to: str, content: str, content_type: str = "text/plain", reply_to: str | None = None) -> dict | None:
|
|
103
|
+
"""Send a direct message. Uses WebSocket if connected, HTTP otherwise."""
|
|
104
|
+
if self._connection and self._connection.connected:
|
|
105
|
+
await self._connection.send_message(to=to, content=content, content_type=content_type, reply_to=reply_to)
|
|
106
|
+
return None
|
|
107
|
+
else:
|
|
108
|
+
return await self._get_http().send_message(to=to, content=content, content_type=content_type, reply_to=reply_to)
|
|
109
|
+
|
|
110
|
+
async def send_to_room(self, room_id: str, content: str, content_type: str = "text/plain") -> dict | None:
|
|
111
|
+
"""Send a message to a room."""
|
|
112
|
+
if self._connection and self._connection.connected:
|
|
113
|
+
await self._connection.send_message(room_id=room_id, content=content, content_type=content_type)
|
|
114
|
+
return None
|
|
115
|
+
else:
|
|
116
|
+
return await self._get_http().send_message(room_id=room_id, content=content, content_type=content_type)
|
|
117
|
+
|
|
118
|
+
async def get_messages(self, since: str | None = None, limit: int = 50) -> list[dict]:
|
|
119
|
+
"""Fetch message history via HTTP."""
|
|
120
|
+
return await self._get_http().get_messages(since=since, limit=limit)
|
|
121
|
+
|
|
122
|
+
async def search_agents(self, name: str | None = None, capability: str | None = None) -> list[dict]:
|
|
123
|
+
"""Search for agents."""
|
|
124
|
+
return await self._get_http().search_agents(name=name, capability=capability)
|
|
125
|
+
|
|
126
|
+
async def create_room(self, name: str, description: str = "", max_members: int = 50) -> dict:
|
|
127
|
+
"""Create a room."""
|
|
128
|
+
return await self._get_http().create_room(name=name, description=description, max_members=max_members)
|
|
129
|
+
|
|
130
|
+
async def join_room(self, room_id: str) -> dict:
|
|
131
|
+
"""Join a room."""
|
|
132
|
+
return await self._get_http().join_room(room_id)
|
|
133
|
+
|
|
134
|
+
async def leave_room(self, room_id: str) -> dict:
|
|
135
|
+
"""Leave a room."""
|
|
136
|
+
return await self._get_http().leave_room(room_id)
|
|
137
|
+
|
|
138
|
+
async def close(self):
|
|
139
|
+
"""Close connection."""
|
|
140
|
+
if self._connection:
|
|
141
|
+
await self._connection.close()
|
|
142
|
+
|
|
143
|
+
def _get_http(self) -> HttpClient:
|
|
144
|
+
if not self._http:
|
|
145
|
+
if not self._token:
|
|
146
|
+
raise RuntimeError("Must register or provide a token")
|
|
147
|
+
self._http = HttpClient(self._broker_url, self._token)
|
|
148
|
+
return self._http
|