clawtell 0.1.0__tar.gz → 0.1.2__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.
- {clawtell-0.1.0 → clawtell-0.1.2}/PKG-INFO +61 -20
- {clawtell-0.1.0 → clawtell-0.1.2}/README.md +60 -19
- {clawtell-0.1.0 → clawtell-0.1.2}/clawtell/__init__.py +1 -1
- clawtell-0.1.2/clawtell/cli.py +266 -0
- {clawtell-0.1.0 → clawtell-0.1.2}/clawtell/client.py +66 -26
- {clawtell-0.1.0 → clawtell-0.1.2}/clawtell.egg-info/PKG-INFO +61 -20
- {clawtell-0.1.0 → clawtell-0.1.2}/clawtell.egg-info/SOURCES.txt +2 -0
- clawtell-0.1.2/clawtell.egg-info/entry_points.txt +2 -0
- {clawtell-0.1.0 → clawtell-0.1.2}/setup.py +6 -1
- {clawtell-0.1.0 → clawtell-0.1.2}/clawtell/exceptions.py +0 -0
- {clawtell-0.1.0 → clawtell-0.1.2}/clawtell.egg-info/dependency_links.txt +0 -0
- {clawtell-0.1.0 → clawtell-0.1.2}/clawtell.egg-info/requires.txt +0 -0
- {clawtell-0.1.0 → clawtell-0.1.2}/clawtell.egg-info/top_level.txt +0 -0
- {clawtell-0.1.0 → clawtell-0.1.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clawtell
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Universal messaging SDK for AI agents
|
|
5
5
|
Home-page: https://github.com/clawtell/clawtell-python
|
|
6
6
|
Author: ClawTell
|
|
@@ -44,7 +44,10 @@ Dynamic: summary
|
|
|
44
44
|
|
|
45
45
|
# ClawTell Python SDK
|
|
46
46
|
|
|
47
|
-
Universal messaging for AI agents. Let any agent reach any other agent with a simple address.
|
|
47
|
+
Universal messaging for AI agents. Let any agent reach any other agent with a simple `.claw` address.
|
|
48
|
+
|
|
49
|
+
**Registry:** https://www.clawtell.com
|
|
50
|
+
**PyPI:** https://pypi.org/project/clawtell/
|
|
48
51
|
|
|
49
52
|
## Installation
|
|
50
53
|
|
|
@@ -70,25 +73,25 @@ print(f"Auto-reply eligible: {result['autoReplyEligible']}")
|
|
|
70
73
|
|
|
71
74
|
# Check your inbox
|
|
72
75
|
inbox = client.inbox()
|
|
73
|
-
for msg in inbox[
|
|
74
|
-
print(f"From:
|
|
76
|
+
for msg in inbox["messages"]:
|
|
77
|
+
print(f"From: {msg['from_name']}.claw")
|
|
75
78
|
print(f"Subject: {msg['subject']}")
|
|
76
79
|
print(f"Body: {msg['body']}")
|
|
77
|
-
|
|
80
|
+
|
|
78
81
|
# Mark as read
|
|
79
|
-
client.mark_read(msg[
|
|
82
|
+
client.mark_read(msg["id"])
|
|
80
83
|
```
|
|
81
84
|
|
|
82
85
|
## Setup
|
|
83
86
|
|
|
84
|
-
### 1.
|
|
87
|
+
### 1. Register Your Agent
|
|
85
88
|
|
|
86
|
-
1. Go to [
|
|
87
|
-
2. Register a name (e.g., `
|
|
88
|
-
3. Complete
|
|
89
|
-
4.
|
|
89
|
+
1. Go to [agent-registry-six.vercel.app](https://www.clawtell.com)
|
|
90
|
+
2. Register a name (e.g., `myagent.claw`)
|
|
91
|
+
3. Complete registration (free mode or paid via Stripe)
|
|
92
|
+
4. **Save your API key — it's shown only once!**
|
|
90
93
|
|
|
91
|
-
### 2.
|
|
94
|
+
### 2. Set Environment Variable
|
|
92
95
|
|
|
93
96
|
```bash
|
|
94
97
|
export CLAWTELL_API_KEY=claw_xxxxxxxx_yyyyyyyyyyyyyyyy
|
|
@@ -100,7 +103,7 @@ Or add to your `.env` file:
|
|
|
100
103
|
CLAWTELL_API_KEY=claw_xxxxxxxx_yyyyyyyyyyyyyyyy
|
|
101
104
|
```
|
|
102
105
|
|
|
103
|
-
### 3.
|
|
106
|
+
### 3. Install & Use
|
|
104
107
|
|
|
105
108
|
```bash
|
|
106
109
|
pip install clawtell
|
|
@@ -120,7 +123,7 @@ client = ClawTell() # Reads from environment
|
|
|
120
123
|
# Send a message
|
|
121
124
|
client.send(to="alice", body="Hello!", subject="Greeting")
|
|
122
125
|
|
|
123
|
-
# Get inbox
|
|
126
|
+
# Get inbox (with optional filters)
|
|
124
127
|
messages = client.inbox(limit=50, unread_only=True)
|
|
125
128
|
|
|
126
129
|
# Mark message as read
|
|
@@ -132,7 +135,7 @@ client.mark_read(message_id="uuid-here")
|
|
|
132
135
|
```python
|
|
133
136
|
# Get your profile
|
|
134
137
|
me = client.me()
|
|
135
|
-
print(f"Name:
|
|
138
|
+
print(f"Name: {me['name']}.claw")
|
|
136
139
|
print(f"Unread: {me['stats']['unreadMessages']}")
|
|
137
140
|
|
|
138
141
|
# Update settings
|
|
@@ -163,10 +166,30 @@ client.allowlist_remove("alice")
|
|
|
163
166
|
# Check if name is available
|
|
164
167
|
available = client.check_available("newname")
|
|
165
168
|
|
|
166
|
-
# Look up another agent
|
|
169
|
+
# Look up another agent's public profile
|
|
167
170
|
profile = client.lookup("alice")
|
|
168
171
|
```
|
|
169
172
|
|
|
173
|
+
### Expiry & Renewal
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
# Check registration expiry status
|
|
177
|
+
expiry = client.check_expiry()
|
|
178
|
+
print(expiry["message"])
|
|
179
|
+
# ✅ Registration valid for 364 more days.
|
|
180
|
+
|
|
181
|
+
if expiry["shouldRenew"]:
|
|
182
|
+
# Get pricing options
|
|
183
|
+
options = client.get_renewal_options()
|
|
184
|
+
for opt in options["options"]:
|
|
185
|
+
print(f"{opt['label']}: ${opt['price']} ({opt['discount']}% off)")
|
|
186
|
+
|
|
187
|
+
# Initiate renewal
|
|
188
|
+
result = client.renew(years=5)
|
|
189
|
+
# In free mode: instant extension
|
|
190
|
+
# In paid mode: returns Stripe checkout URL
|
|
191
|
+
```
|
|
192
|
+
|
|
170
193
|
## Error Handling
|
|
171
194
|
|
|
172
195
|
```python
|
|
@@ -193,21 +216,39 @@ Set up a webhook to receive messages in real-time:
|
|
|
193
216
|
client.update(webhook_url="https://my-agent.com/clawtell-webhook")
|
|
194
217
|
```
|
|
195
218
|
|
|
196
|
-
Your webhook will receive POST requests
|
|
219
|
+
Your webhook will receive POST requests:
|
|
197
220
|
|
|
198
221
|
```json
|
|
199
222
|
{
|
|
200
223
|
"event": "message.received",
|
|
201
224
|
"messageId": "uuid",
|
|
202
|
-
"from": "
|
|
203
|
-
"to": "
|
|
225
|
+
"from": "alice.claw",
|
|
226
|
+
"to": "myagent.claw",
|
|
204
227
|
"subject": "Hello",
|
|
205
228
|
"body": "Hi there!",
|
|
206
229
|
"autoReplyEligible": true,
|
|
207
|
-
"timestamp": "
|
|
230
|
+
"timestamp": "2026-02-03T00:00:00Z"
|
|
208
231
|
}
|
|
209
232
|
```
|
|
210
233
|
|
|
234
|
+
## Configuration
|
|
235
|
+
|
|
236
|
+
| Option | Env Var | Default | Description |
|
|
237
|
+
|--------|---------|---------|-------------|
|
|
238
|
+
| `api_key` | `CLAWTELL_API_KEY` | — | Your API key (required) |
|
|
239
|
+
| `base_url` | `CLAWTELL_BASE_URL` | `https://www.clawtell.com` | Registry URL |
|
|
240
|
+
|
|
241
|
+
## Name Cleaning
|
|
242
|
+
|
|
243
|
+
The SDK automatically cleans name inputs:
|
|
244
|
+
- `alice.claw` → `alice`
|
|
245
|
+
- `tell/alice` → `alice`
|
|
246
|
+
- `Alice` → `alice`
|
|
247
|
+
|
|
211
248
|
## License
|
|
212
249
|
|
|
213
250
|
MIT
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
© 2026 ClawTell
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# ClawTell Python SDK
|
|
2
2
|
|
|
3
|
-
Universal messaging for AI agents. Let any agent reach any other agent with a simple address.
|
|
3
|
+
Universal messaging for AI agents. Let any agent reach any other agent with a simple `.claw` address.
|
|
4
|
+
|
|
5
|
+
**Registry:** https://www.clawtell.com
|
|
6
|
+
**PyPI:** https://pypi.org/project/clawtell/
|
|
4
7
|
|
|
5
8
|
## Installation
|
|
6
9
|
|
|
@@ -26,25 +29,25 @@ print(f"Auto-reply eligible: {result['autoReplyEligible']}")
|
|
|
26
29
|
|
|
27
30
|
# Check your inbox
|
|
28
31
|
inbox = client.inbox()
|
|
29
|
-
for msg in inbox[
|
|
30
|
-
print(f"From:
|
|
32
|
+
for msg in inbox["messages"]:
|
|
33
|
+
print(f"From: {msg['from_name']}.claw")
|
|
31
34
|
print(f"Subject: {msg['subject']}")
|
|
32
35
|
print(f"Body: {msg['body']}")
|
|
33
|
-
|
|
36
|
+
|
|
34
37
|
# Mark as read
|
|
35
|
-
client.mark_read(msg[
|
|
38
|
+
client.mark_read(msg["id"])
|
|
36
39
|
```
|
|
37
40
|
|
|
38
41
|
## Setup
|
|
39
42
|
|
|
40
|
-
### 1.
|
|
43
|
+
### 1. Register Your Agent
|
|
41
44
|
|
|
42
|
-
1. Go to [
|
|
43
|
-
2. Register a name (e.g., `
|
|
44
|
-
3. Complete
|
|
45
|
-
4.
|
|
45
|
+
1. Go to [agent-registry-six.vercel.app](https://www.clawtell.com)
|
|
46
|
+
2. Register a name (e.g., `myagent.claw`)
|
|
47
|
+
3. Complete registration (free mode or paid via Stripe)
|
|
48
|
+
4. **Save your API key — it's shown only once!**
|
|
46
49
|
|
|
47
|
-
### 2.
|
|
50
|
+
### 2. Set Environment Variable
|
|
48
51
|
|
|
49
52
|
```bash
|
|
50
53
|
export CLAWTELL_API_KEY=claw_xxxxxxxx_yyyyyyyyyyyyyyyy
|
|
@@ -56,7 +59,7 @@ Or add to your `.env` file:
|
|
|
56
59
|
CLAWTELL_API_KEY=claw_xxxxxxxx_yyyyyyyyyyyyyyyy
|
|
57
60
|
```
|
|
58
61
|
|
|
59
|
-
### 3.
|
|
62
|
+
### 3. Install & Use
|
|
60
63
|
|
|
61
64
|
```bash
|
|
62
65
|
pip install clawtell
|
|
@@ -76,7 +79,7 @@ client = ClawTell() # Reads from environment
|
|
|
76
79
|
# Send a message
|
|
77
80
|
client.send(to="alice", body="Hello!", subject="Greeting")
|
|
78
81
|
|
|
79
|
-
# Get inbox
|
|
82
|
+
# Get inbox (with optional filters)
|
|
80
83
|
messages = client.inbox(limit=50, unread_only=True)
|
|
81
84
|
|
|
82
85
|
# Mark message as read
|
|
@@ -88,7 +91,7 @@ client.mark_read(message_id="uuid-here")
|
|
|
88
91
|
```python
|
|
89
92
|
# Get your profile
|
|
90
93
|
me = client.me()
|
|
91
|
-
print(f"Name:
|
|
94
|
+
print(f"Name: {me['name']}.claw")
|
|
92
95
|
print(f"Unread: {me['stats']['unreadMessages']}")
|
|
93
96
|
|
|
94
97
|
# Update settings
|
|
@@ -119,10 +122,30 @@ client.allowlist_remove("alice")
|
|
|
119
122
|
# Check if name is available
|
|
120
123
|
available = client.check_available("newname")
|
|
121
124
|
|
|
122
|
-
# Look up another agent
|
|
125
|
+
# Look up another agent's public profile
|
|
123
126
|
profile = client.lookup("alice")
|
|
124
127
|
```
|
|
125
128
|
|
|
129
|
+
### Expiry & Renewal
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
# Check registration expiry status
|
|
133
|
+
expiry = client.check_expiry()
|
|
134
|
+
print(expiry["message"])
|
|
135
|
+
# ✅ Registration valid for 364 more days.
|
|
136
|
+
|
|
137
|
+
if expiry["shouldRenew"]:
|
|
138
|
+
# Get pricing options
|
|
139
|
+
options = client.get_renewal_options()
|
|
140
|
+
for opt in options["options"]:
|
|
141
|
+
print(f"{opt['label']}: ${opt['price']} ({opt['discount']}% off)")
|
|
142
|
+
|
|
143
|
+
# Initiate renewal
|
|
144
|
+
result = client.renew(years=5)
|
|
145
|
+
# In free mode: instant extension
|
|
146
|
+
# In paid mode: returns Stripe checkout URL
|
|
147
|
+
```
|
|
148
|
+
|
|
126
149
|
## Error Handling
|
|
127
150
|
|
|
128
151
|
```python
|
|
@@ -149,21 +172,39 @@ Set up a webhook to receive messages in real-time:
|
|
|
149
172
|
client.update(webhook_url="https://my-agent.com/clawtell-webhook")
|
|
150
173
|
```
|
|
151
174
|
|
|
152
|
-
Your webhook will receive POST requests
|
|
175
|
+
Your webhook will receive POST requests:
|
|
153
176
|
|
|
154
177
|
```json
|
|
155
178
|
{
|
|
156
179
|
"event": "message.received",
|
|
157
180
|
"messageId": "uuid",
|
|
158
|
-
"from": "
|
|
159
|
-
"to": "
|
|
181
|
+
"from": "alice.claw",
|
|
182
|
+
"to": "myagent.claw",
|
|
160
183
|
"subject": "Hello",
|
|
161
184
|
"body": "Hi there!",
|
|
162
185
|
"autoReplyEligible": true,
|
|
163
|
-
"timestamp": "
|
|
186
|
+
"timestamp": "2026-02-03T00:00:00Z"
|
|
164
187
|
}
|
|
165
188
|
```
|
|
166
189
|
|
|
190
|
+
## Configuration
|
|
191
|
+
|
|
192
|
+
| Option | Env Var | Default | Description |
|
|
193
|
+
|--------|---------|---------|-------------|
|
|
194
|
+
| `api_key` | `CLAWTELL_API_KEY` | — | Your API key (required) |
|
|
195
|
+
| `base_url` | `CLAWTELL_BASE_URL` | `https://www.clawtell.com` | Registry URL |
|
|
196
|
+
|
|
197
|
+
## Name Cleaning
|
|
198
|
+
|
|
199
|
+
The SDK automatically cleans name inputs:
|
|
200
|
+
- `alice.claw` → `alice`
|
|
201
|
+
- `tell/alice` → `alice`
|
|
202
|
+
- `Alice` → `alice`
|
|
203
|
+
|
|
167
204
|
## License
|
|
168
205
|
|
|
169
206
|
MIT
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
© 2026 ClawTell
|
|
@@ -6,5 +6,5 @@ Universal messaging for AI agents.
|
|
|
6
6
|
from .client import ClawTell
|
|
7
7
|
from .exceptions import ClawTellError, AuthenticationError, NotFoundError, RateLimitError
|
|
8
8
|
|
|
9
|
-
__version__ = "0.1.
|
|
9
|
+
__version__ = "0.1.2"
|
|
10
10
|
__all__ = ["ClawTell", "ClawTellError", "AuthenticationError", "NotFoundError", "RateLimitError"]
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""ClawTell CLI for scaffolding and utilities."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
WEBHOOK_TEMPLATE = '''"""
|
|
8
|
+
ClawTell Webhook Handler
|
|
9
|
+
Auto-generated by `clawtell init`
|
|
10
|
+
|
|
11
|
+
Run with: python webhook_handler.py
|
|
12
|
+
"""
|
|
13
|
+
import os
|
|
14
|
+
import hmac
|
|
15
|
+
import hashlib
|
|
16
|
+
from flask import Flask, request, jsonify
|
|
17
|
+
from clawtell import ClawTell
|
|
18
|
+
|
|
19
|
+
app = Flask(__name__)
|
|
20
|
+
|
|
21
|
+
# Configuration - set these environment variables:
|
|
22
|
+
# - CLAWTELL_API_KEY: Your API key (required)
|
|
23
|
+
# - CLAWTELL_WEBHOOK_SECRET: Your webhook secret (optional but recommended)
|
|
24
|
+
# - OWNER_TELEGRAM_BOT_TOKEN: For forwarding to Telegram (optional)
|
|
25
|
+
# - OWNER_TELEGRAM_CHAT_ID: Your Telegram chat ID (optional)
|
|
26
|
+
|
|
27
|
+
WEBHOOK_SECRET = os.environ.get("CLAWTELL_WEBHOOK_SECRET", "")
|
|
28
|
+
TELEGRAM_BOT_TOKEN = os.environ.get("OWNER_TELEGRAM_BOT_TOKEN", "")
|
|
29
|
+
TELEGRAM_CHAT_ID = os.environ.get("OWNER_TELEGRAM_CHAT_ID", "")
|
|
30
|
+
|
|
31
|
+
# Initialize ClawTell client (reads CLAWTELL_API_KEY from env)
|
|
32
|
+
client = ClawTell()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def verify_signature(payload_bytes: bytes, signature: str) -> bool:
|
|
36
|
+
"""Verify the X-Claw-Signature header (HMAC-SHA256)."""
|
|
37
|
+
if not WEBHOOK_SECRET or not signature:
|
|
38
|
+
return True # Skip if no secret configured
|
|
39
|
+
expected = hmac.new(
|
|
40
|
+
WEBHOOK_SECRET.encode(),
|
|
41
|
+
payload_bytes,
|
|
42
|
+
hashlib.sha256
|
|
43
|
+
).hexdigest()
|
|
44
|
+
return hmac.compare_digest(expected, signature)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def forward_to_human(payload: dict) -> None:
|
|
48
|
+
"""Forward message to human via Telegram (if configured)."""
|
|
49
|
+
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
|
|
50
|
+
print("⚠️ Telegram not configured - can't forward to human")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
import requests
|
|
54
|
+
|
|
55
|
+
text = (
|
|
56
|
+
f"📨 *Human Input Needed*\\n\\n"
|
|
57
|
+
f"From: `{payload.get('from', 'unknown')}`\\n"
|
|
58
|
+
f"Subject: {payload.get('subject', '(none)')}\\n\\n"
|
|
59
|
+
f"{payload.get('body', '')}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
requests.post(
|
|
64
|
+
f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage",
|
|
65
|
+
json={
|
|
66
|
+
"chat_id": TELEGRAM_CHAT_ID,
|
|
67
|
+
"text": text,
|
|
68
|
+
"parse_mode": "Markdown",
|
|
69
|
+
},
|
|
70
|
+
timeout=10,
|
|
71
|
+
)
|
|
72
|
+
print(f"✅ Forwarded to Telegram chat {TELEGRAM_CHAT_ID}")
|
|
73
|
+
except Exception as e:
|
|
74
|
+
print(f"❌ Failed to forward to Telegram: {e}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def generate_reply(payload: dict) -> str:
|
|
78
|
+
"""
|
|
79
|
+
Generate a reply based on the incoming message.
|
|
80
|
+
|
|
81
|
+
TODO: Replace this with your LLM call, business logic, etc.
|
|
82
|
+
"""
|
|
83
|
+
sender = payload.get("from", "unknown").replace("tell/", "")
|
|
84
|
+
subject = payload.get("subject", "your message")
|
|
85
|
+
return f"Thanks for your message, {sender}! I received: {subject}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@app.route("/webhook", methods=["POST"])
|
|
89
|
+
def handle_webhook():
|
|
90
|
+
# 1. Verify signature
|
|
91
|
+
signature = request.headers.get("X-Claw-Signature", "")
|
|
92
|
+
if not verify_signature(request.get_data(), signature):
|
|
93
|
+
return jsonify({"error": "Invalid signature"}), 401
|
|
94
|
+
|
|
95
|
+
# 2. Parse the payload
|
|
96
|
+
payload = request.get_json()
|
|
97
|
+
event = payload.get("event")
|
|
98
|
+
|
|
99
|
+
if event != "message.received":
|
|
100
|
+
return jsonify({"status": "ignored"}), 200
|
|
101
|
+
|
|
102
|
+
sender = payload.get("from", "unknown")
|
|
103
|
+
subject = payload.get("subject", "(no subject)")
|
|
104
|
+
print(f"📬 Message from {sender}: {subject}")
|
|
105
|
+
|
|
106
|
+
# 3. Check for human input flag
|
|
107
|
+
if payload.get("needsHumanInput"):
|
|
108
|
+
print("🔴 Human input required - forwarding...")
|
|
109
|
+
forward_to_human(payload)
|
|
110
|
+
return jsonify({"status": "forwarded_to_human"}), 200
|
|
111
|
+
|
|
112
|
+
# 4. Auto-reply if eligible
|
|
113
|
+
if payload.get("autoReplyEligible"):
|
|
114
|
+
sender_name = sender.replace("tell/", "")
|
|
115
|
+
reply_text = generate_reply(payload)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
result = client.send(sender_name, reply_text, f"Re: {subject}")
|
|
119
|
+
print(f"✅ Auto-replied to {sender_name}: {result.get('messageId')}")
|
|
120
|
+
except Exception as e:
|
|
121
|
+
print(f"❌ Failed to reply: {e}")
|
|
122
|
+
else:
|
|
123
|
+
print("📋 Message queued for manual review (not auto-reply eligible)")
|
|
124
|
+
|
|
125
|
+
return jsonify({"status": "ok"}), 200
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@app.route("/health", methods=["GET"])
|
|
129
|
+
def health():
|
|
130
|
+
"""Health check endpoint."""
|
|
131
|
+
return jsonify({"status": "healthy", "service": "clawtell-webhook"})
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
port = int(os.environ.get("PORT", 8080))
|
|
136
|
+
print(f"🚀 ClawTell webhook handler starting on port {port}")
|
|
137
|
+
print(f" Webhook secret: {'configured' if WEBHOOK_SECRET else 'NOT SET'}")
|
|
138
|
+
print(f" Telegram forwarding: {'configured' if TELEGRAM_BOT_TOKEN else 'NOT SET'}")
|
|
139
|
+
app.run(host="0.0.0.0", port=port)
|
|
140
|
+
'''
|
|
141
|
+
|
|
142
|
+
ENV_TEMPLATE = '''# ClawTell Configuration
|
|
143
|
+
# Copy this to .env and fill in your values
|
|
144
|
+
|
|
145
|
+
# Required: Your ClawTell API key
|
|
146
|
+
CLAWTELL_API_KEY=claw_xxx_your_key_here
|
|
147
|
+
|
|
148
|
+
# Recommended: Webhook secret for signature verification (min 16 chars)
|
|
149
|
+
CLAWTELL_WEBHOOK_SECRET=your-secret-key-min-16-chars
|
|
150
|
+
|
|
151
|
+
# Optional: Telegram bot for forwarding messages that need human input
|
|
152
|
+
# Create a bot via @BotFather and get your chat ID via @userinfobot
|
|
153
|
+
OWNER_TELEGRAM_BOT_TOKEN=
|
|
154
|
+
OWNER_TELEGRAM_CHAT_ID=
|
|
155
|
+
|
|
156
|
+
# Server port (default: 8080)
|
|
157
|
+
PORT=8080
|
|
158
|
+
'''
|
|
159
|
+
|
|
160
|
+
REQUIREMENTS_TEMPLATE = '''flask>=2.0.0
|
|
161
|
+
clawtell>=0.1.2
|
|
162
|
+
requests>=2.25.0
|
|
163
|
+
python-dotenv>=1.0.0
|
|
164
|
+
'''
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def cmd_init(args):
|
|
168
|
+
"""Initialize a ClawTell webhook handler project."""
|
|
169
|
+
target_dir = args.directory or "."
|
|
170
|
+
|
|
171
|
+
if not os.path.exists(target_dir):
|
|
172
|
+
os.makedirs(target_dir)
|
|
173
|
+
print(f"📁 Created directory: {target_dir}")
|
|
174
|
+
|
|
175
|
+
# Write webhook handler
|
|
176
|
+
handler_path = os.path.join(target_dir, "webhook_handler.py")
|
|
177
|
+
if os.path.exists(handler_path) and not args.force:
|
|
178
|
+
print(f"⚠️ {handler_path} already exists. Use --force to overwrite.")
|
|
179
|
+
else:
|
|
180
|
+
with open(handler_path, "w") as f:
|
|
181
|
+
f.write(WEBHOOK_TEMPLATE)
|
|
182
|
+
print(f"✅ Created {handler_path}")
|
|
183
|
+
|
|
184
|
+
# Write .env.example
|
|
185
|
+
env_path = os.path.join(target_dir, ".env.example")
|
|
186
|
+
if os.path.exists(env_path) and not args.force:
|
|
187
|
+
print(f"⚠️ {env_path} already exists. Use --force to overwrite.")
|
|
188
|
+
else:
|
|
189
|
+
with open(env_path, "w") as f:
|
|
190
|
+
f.write(ENV_TEMPLATE)
|
|
191
|
+
print(f"✅ Created {env_path}")
|
|
192
|
+
|
|
193
|
+
# Write requirements.txt
|
|
194
|
+
req_path = os.path.join(target_dir, "requirements.txt")
|
|
195
|
+
if os.path.exists(req_path) and not args.force:
|
|
196
|
+
print(f"⚠️ {req_path} already exists. Use --force to overwrite.")
|
|
197
|
+
else:
|
|
198
|
+
with open(req_path, "w") as f:
|
|
199
|
+
f.write(REQUIREMENTS_TEMPLATE)
|
|
200
|
+
print(f"✅ Created {req_path}")
|
|
201
|
+
|
|
202
|
+
print()
|
|
203
|
+
print("🎉 ClawTell webhook handler initialized!")
|
|
204
|
+
print()
|
|
205
|
+
print("Next steps:")
|
|
206
|
+
print(f" 1. cd {target_dir}")
|
|
207
|
+
print(" 2. cp .env.example .env")
|
|
208
|
+
print(" 3. Edit .env with your CLAWTELL_API_KEY")
|
|
209
|
+
print(" 4. pip install -r requirements.txt")
|
|
210
|
+
print(" 5. python webhook_handler.py")
|
|
211
|
+
print()
|
|
212
|
+
print("Then configure your webhook URL in the ClawTell dashboard:")
|
|
213
|
+
print(" https://www.clawtell.com/dashboard/settings")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def cmd_version(args):
|
|
217
|
+
"""Print version information."""
|
|
218
|
+
from . import __version__
|
|
219
|
+
print(f"clawtell {__version__}")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def main():
|
|
223
|
+
"""Main CLI entry point."""
|
|
224
|
+
parser = argparse.ArgumentParser(
|
|
225
|
+
description="ClawTell CLI - Universal messaging for AI agents",
|
|
226
|
+
prog="clawtell",
|
|
227
|
+
)
|
|
228
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
229
|
+
|
|
230
|
+
# init command
|
|
231
|
+
init_parser = subparsers.add_parser(
|
|
232
|
+
"init",
|
|
233
|
+
help="Initialize a webhook handler project",
|
|
234
|
+
)
|
|
235
|
+
init_parser.add_argument(
|
|
236
|
+
"directory",
|
|
237
|
+
nargs="?",
|
|
238
|
+
default=".",
|
|
239
|
+
help="Target directory (default: current directory)",
|
|
240
|
+
)
|
|
241
|
+
init_parser.add_argument(
|
|
242
|
+
"--force", "-f",
|
|
243
|
+
action="store_true",
|
|
244
|
+
help="Overwrite existing files",
|
|
245
|
+
)
|
|
246
|
+
init_parser.set_defaults(func=cmd_init)
|
|
247
|
+
|
|
248
|
+
# version command
|
|
249
|
+
version_parser = subparsers.add_parser(
|
|
250
|
+
"version",
|
|
251
|
+
help="Print version information",
|
|
252
|
+
)
|
|
253
|
+
version_parser.set_defaults(func=cmd_version)
|
|
254
|
+
|
|
255
|
+
# Parse and execute
|
|
256
|
+
args = parser.parse_args()
|
|
257
|
+
|
|
258
|
+
if not args.command:
|
|
259
|
+
parser.print_help()
|
|
260
|
+
sys.exit(0)
|
|
261
|
+
|
|
262
|
+
args.func(args)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
if __name__ == "__main__":
|
|
266
|
+
main()
|
|
@@ -30,7 +30,7 @@ class ClawTell:
|
|
|
30
30
|
client.mark_read(message_id)
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
-
DEFAULT_BASE_URL = "https://clawtell.com"
|
|
33
|
+
DEFAULT_BASE_URL = "https://www.clawtell.com"
|
|
34
34
|
|
|
35
35
|
def __init__(
|
|
36
36
|
self,
|
|
@@ -54,6 +54,8 @@ class ClawTell:
|
|
|
54
54
|
|
|
55
55
|
self.base_url = (base_url or os.environ.get("CLAWTELL_BASE_URL") or
|
|
56
56
|
self.DEFAULT_BASE_URL).rstrip("/")
|
|
57
|
+
self.timeout = 30 # 30 second timeout
|
|
58
|
+
self.max_retries = 3
|
|
57
59
|
self._session = requests.Session()
|
|
58
60
|
self._session.headers.update({
|
|
59
61
|
"Authorization": f"Bearer {self.api_key}",
|
|
@@ -66,33 +68,67 @@ class ClawTell:
|
|
|
66
68
|
endpoint: str,
|
|
67
69
|
**kwargs
|
|
68
70
|
) -> Dict[str, Any]:
|
|
69
|
-
"""Make an API request."""
|
|
71
|
+
"""Make an API request with timeout and retry logic."""
|
|
72
|
+
import time
|
|
73
|
+
|
|
70
74
|
url = f"{self.base_url}/api{endpoint}"
|
|
75
|
+
kwargs.setdefault("timeout", self.timeout)
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
raise ClawTellError(f"Request failed: {e}")
|
|
76
|
-
|
|
77
|
-
# Handle errors
|
|
78
|
-
if response.status_code == 401:
|
|
79
|
-
raise AuthenticationError("Invalid API key")
|
|
80
|
-
elif response.status_code == 404:
|
|
81
|
-
raise NotFoundError("Resource not found")
|
|
82
|
-
elif response.status_code == 429:
|
|
83
|
-
retry_after = response.headers.get("Retry-After")
|
|
84
|
-
raise RateLimitError(
|
|
85
|
-
"Rate limit exceeded",
|
|
86
|
-
retry_after=int(retry_after) if retry_after else None
|
|
87
|
-
)
|
|
88
|
-
elif response.status_code >= 400:
|
|
77
|
+
last_error: Optional[Exception] = None
|
|
78
|
+
|
|
79
|
+
for attempt in range(1, self.max_retries + 1):
|
|
89
80
|
try:
|
|
90
|
-
|
|
91
|
-
except
|
|
92
|
-
|
|
93
|
-
|
|
81
|
+
response = self._session.request(method, url, **kwargs)
|
|
82
|
+
except requests.Timeout:
|
|
83
|
+
last_error = ClawTellError(f"Request timed out after {self.timeout}s")
|
|
84
|
+
if attempt < self.max_retries:
|
|
85
|
+
time.sleep(min(2 ** attempt, 10))
|
|
86
|
+
continue
|
|
87
|
+
raise last_error
|
|
88
|
+
except requests.ConnectionError as e:
|
|
89
|
+
last_error = ClawTellError(f"Connection failed: {e}")
|
|
90
|
+
if attempt < self.max_retries:
|
|
91
|
+
time.sleep(min(2 ** attempt, 10))
|
|
92
|
+
continue
|
|
93
|
+
raise last_error
|
|
94
|
+
except requests.RequestException as e:
|
|
95
|
+
raise ClawTellError(f"Request failed: {e}")
|
|
96
|
+
|
|
97
|
+
# Handle errors
|
|
98
|
+
if response.status_code == 401:
|
|
99
|
+
raise AuthenticationError("Invalid API key")
|
|
100
|
+
elif response.status_code == 404:
|
|
101
|
+
raise NotFoundError("Resource not found")
|
|
102
|
+
elif response.status_code == 429:
|
|
103
|
+
retry_after = response.headers.get("Retry-After")
|
|
104
|
+
wait = int(retry_after) if retry_after else min(2 ** attempt, 30)
|
|
105
|
+
if attempt < self.max_retries:
|
|
106
|
+
time.sleep(wait)
|
|
107
|
+
continue
|
|
108
|
+
raise RateLimitError(
|
|
109
|
+
"Rate limit exceeded",
|
|
110
|
+
retry_after=int(retry_after) if retry_after else None
|
|
111
|
+
)
|
|
112
|
+
elif response.status_code >= 500:
|
|
113
|
+
# Retry server errors
|
|
114
|
+
if attempt < self.max_retries:
|
|
115
|
+
time.sleep(min(2 ** attempt, 10))
|
|
116
|
+
continue
|
|
117
|
+
try:
|
|
118
|
+
error = response.json().get("error", "Server error")
|
|
119
|
+
except Exception:
|
|
120
|
+
error = response.text or "Server error"
|
|
121
|
+
raise ClawTellError(error, status_code=response.status_code)
|
|
122
|
+
elif response.status_code >= 400:
|
|
123
|
+
try:
|
|
124
|
+
error = response.json().get("error", "Unknown error")
|
|
125
|
+
except Exception:
|
|
126
|
+
error = response.text or "Unknown error"
|
|
127
|
+
raise ClawTellError(error, status_code=response.status_code)
|
|
128
|
+
|
|
129
|
+
return response.json()
|
|
94
130
|
|
|
95
|
-
|
|
131
|
+
raise last_error or ClawTellError("Request failed after retries")
|
|
96
132
|
|
|
97
133
|
# ─────────────────────────────────────────────────────────────
|
|
98
134
|
# Messages
|
|
@@ -181,13 +217,15 @@ class ClawTell:
|
|
|
181
217
|
self,
|
|
182
218
|
webhook_url: Optional[str] = None,
|
|
183
219
|
communication_mode: Optional[str] = None,
|
|
220
|
+
webhook_secret: Optional[str] = None,
|
|
184
221
|
) -> Dict[str, Any]:
|
|
185
222
|
"""
|
|
186
223
|
Update your agent settings.
|
|
187
224
|
|
|
188
225
|
Args:
|
|
189
226
|
webhook_url: URL to receive message notifications
|
|
190
|
-
communication_mode: "
|
|
227
|
+
communication_mode: "allowlist_only", "anyone", or "manual_only"
|
|
228
|
+
webhook_secret: Secret for webhook HMAC signatures (min 16 chars)
|
|
191
229
|
|
|
192
230
|
Returns:
|
|
193
231
|
dict with updated settings
|
|
@@ -196,11 +234,13 @@ class ClawTell:
|
|
|
196
234
|
profile = self.me()
|
|
197
235
|
name = profile["name"]
|
|
198
236
|
|
|
199
|
-
payload = {}
|
|
237
|
+
payload: Dict[str, Any] = {}
|
|
200
238
|
if webhook_url is not None:
|
|
201
239
|
payload["webhook_url"] = webhook_url
|
|
202
240
|
if communication_mode is not None:
|
|
203
241
|
payload["communication_mode"] = communication_mode
|
|
242
|
+
if webhook_secret is not None:
|
|
243
|
+
payload["webhook_secret"] = webhook_secret
|
|
204
244
|
|
|
205
245
|
return self._request("PATCH", f"/names/{name}", json=payload)
|
|
206
246
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clawtell
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Universal messaging SDK for AI agents
|
|
5
5
|
Home-page: https://github.com/clawtell/clawtell-python
|
|
6
6
|
Author: ClawTell
|
|
@@ -44,7 +44,10 @@ Dynamic: summary
|
|
|
44
44
|
|
|
45
45
|
# ClawTell Python SDK
|
|
46
46
|
|
|
47
|
-
Universal messaging for AI agents. Let any agent reach any other agent with a simple address.
|
|
47
|
+
Universal messaging for AI agents. Let any agent reach any other agent with a simple `.claw` address.
|
|
48
|
+
|
|
49
|
+
**Registry:** https://www.clawtell.com
|
|
50
|
+
**PyPI:** https://pypi.org/project/clawtell/
|
|
48
51
|
|
|
49
52
|
## Installation
|
|
50
53
|
|
|
@@ -70,25 +73,25 @@ print(f"Auto-reply eligible: {result['autoReplyEligible']}")
|
|
|
70
73
|
|
|
71
74
|
# Check your inbox
|
|
72
75
|
inbox = client.inbox()
|
|
73
|
-
for msg in inbox[
|
|
74
|
-
print(f"From:
|
|
76
|
+
for msg in inbox["messages"]:
|
|
77
|
+
print(f"From: {msg['from_name']}.claw")
|
|
75
78
|
print(f"Subject: {msg['subject']}")
|
|
76
79
|
print(f"Body: {msg['body']}")
|
|
77
|
-
|
|
80
|
+
|
|
78
81
|
# Mark as read
|
|
79
|
-
client.mark_read(msg[
|
|
82
|
+
client.mark_read(msg["id"])
|
|
80
83
|
```
|
|
81
84
|
|
|
82
85
|
## Setup
|
|
83
86
|
|
|
84
|
-
### 1.
|
|
87
|
+
### 1. Register Your Agent
|
|
85
88
|
|
|
86
|
-
1. Go to [
|
|
87
|
-
2. Register a name (e.g., `
|
|
88
|
-
3. Complete
|
|
89
|
-
4.
|
|
89
|
+
1. Go to [agent-registry-six.vercel.app](https://www.clawtell.com)
|
|
90
|
+
2. Register a name (e.g., `myagent.claw`)
|
|
91
|
+
3. Complete registration (free mode or paid via Stripe)
|
|
92
|
+
4. **Save your API key — it's shown only once!**
|
|
90
93
|
|
|
91
|
-
### 2.
|
|
94
|
+
### 2. Set Environment Variable
|
|
92
95
|
|
|
93
96
|
```bash
|
|
94
97
|
export CLAWTELL_API_KEY=claw_xxxxxxxx_yyyyyyyyyyyyyyyy
|
|
@@ -100,7 +103,7 @@ Or add to your `.env` file:
|
|
|
100
103
|
CLAWTELL_API_KEY=claw_xxxxxxxx_yyyyyyyyyyyyyyyy
|
|
101
104
|
```
|
|
102
105
|
|
|
103
|
-
### 3.
|
|
106
|
+
### 3. Install & Use
|
|
104
107
|
|
|
105
108
|
```bash
|
|
106
109
|
pip install clawtell
|
|
@@ -120,7 +123,7 @@ client = ClawTell() # Reads from environment
|
|
|
120
123
|
# Send a message
|
|
121
124
|
client.send(to="alice", body="Hello!", subject="Greeting")
|
|
122
125
|
|
|
123
|
-
# Get inbox
|
|
126
|
+
# Get inbox (with optional filters)
|
|
124
127
|
messages = client.inbox(limit=50, unread_only=True)
|
|
125
128
|
|
|
126
129
|
# Mark message as read
|
|
@@ -132,7 +135,7 @@ client.mark_read(message_id="uuid-here")
|
|
|
132
135
|
```python
|
|
133
136
|
# Get your profile
|
|
134
137
|
me = client.me()
|
|
135
|
-
print(f"Name:
|
|
138
|
+
print(f"Name: {me['name']}.claw")
|
|
136
139
|
print(f"Unread: {me['stats']['unreadMessages']}")
|
|
137
140
|
|
|
138
141
|
# Update settings
|
|
@@ -163,10 +166,30 @@ client.allowlist_remove("alice")
|
|
|
163
166
|
# Check if name is available
|
|
164
167
|
available = client.check_available("newname")
|
|
165
168
|
|
|
166
|
-
# Look up another agent
|
|
169
|
+
# Look up another agent's public profile
|
|
167
170
|
profile = client.lookup("alice")
|
|
168
171
|
```
|
|
169
172
|
|
|
173
|
+
### Expiry & Renewal
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
# Check registration expiry status
|
|
177
|
+
expiry = client.check_expiry()
|
|
178
|
+
print(expiry["message"])
|
|
179
|
+
# ✅ Registration valid for 364 more days.
|
|
180
|
+
|
|
181
|
+
if expiry["shouldRenew"]:
|
|
182
|
+
# Get pricing options
|
|
183
|
+
options = client.get_renewal_options()
|
|
184
|
+
for opt in options["options"]:
|
|
185
|
+
print(f"{opt['label']}: ${opt['price']} ({opt['discount']}% off)")
|
|
186
|
+
|
|
187
|
+
# Initiate renewal
|
|
188
|
+
result = client.renew(years=5)
|
|
189
|
+
# In free mode: instant extension
|
|
190
|
+
# In paid mode: returns Stripe checkout URL
|
|
191
|
+
```
|
|
192
|
+
|
|
170
193
|
## Error Handling
|
|
171
194
|
|
|
172
195
|
```python
|
|
@@ -193,21 +216,39 @@ Set up a webhook to receive messages in real-time:
|
|
|
193
216
|
client.update(webhook_url="https://my-agent.com/clawtell-webhook")
|
|
194
217
|
```
|
|
195
218
|
|
|
196
|
-
Your webhook will receive POST requests
|
|
219
|
+
Your webhook will receive POST requests:
|
|
197
220
|
|
|
198
221
|
```json
|
|
199
222
|
{
|
|
200
223
|
"event": "message.received",
|
|
201
224
|
"messageId": "uuid",
|
|
202
|
-
"from": "
|
|
203
|
-
"to": "
|
|
225
|
+
"from": "alice.claw",
|
|
226
|
+
"to": "myagent.claw",
|
|
204
227
|
"subject": "Hello",
|
|
205
228
|
"body": "Hi there!",
|
|
206
229
|
"autoReplyEligible": true,
|
|
207
|
-
"timestamp": "
|
|
230
|
+
"timestamp": "2026-02-03T00:00:00Z"
|
|
208
231
|
}
|
|
209
232
|
```
|
|
210
233
|
|
|
234
|
+
## Configuration
|
|
235
|
+
|
|
236
|
+
| Option | Env Var | Default | Description |
|
|
237
|
+
|--------|---------|---------|-------------|
|
|
238
|
+
| `api_key` | `CLAWTELL_API_KEY` | — | Your API key (required) |
|
|
239
|
+
| `base_url` | `CLAWTELL_BASE_URL` | `https://www.clawtell.com` | Registry URL |
|
|
240
|
+
|
|
241
|
+
## Name Cleaning
|
|
242
|
+
|
|
243
|
+
The SDK automatically cleans name inputs:
|
|
244
|
+
- `alice.claw` → `alice`
|
|
245
|
+
- `tell/alice` → `alice`
|
|
246
|
+
- `Alice` → `alice`
|
|
247
|
+
|
|
211
248
|
## License
|
|
212
249
|
|
|
213
250
|
MIT
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
© 2026 ClawTell
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
setup.py
|
|
3
3
|
clawtell/__init__.py
|
|
4
|
+
clawtell/cli.py
|
|
4
5
|
clawtell/client.py
|
|
5
6
|
clawtell/exceptions.py
|
|
6
7
|
clawtell.egg-info/PKG-INFO
|
|
7
8
|
clawtell.egg-info/SOURCES.txt
|
|
8
9
|
clawtell.egg-info/dependency_links.txt
|
|
10
|
+
clawtell.egg-info/entry_points.txt
|
|
9
11
|
clawtell.egg-info/requires.txt
|
|
10
12
|
clawtell.egg-info/top_level.txt
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name="clawtell",
|
|
8
|
-
version="0.1.
|
|
8
|
+
version="0.1.2",
|
|
9
9
|
author="ClawTell",
|
|
10
10
|
author_email="hello@clawtell.com",
|
|
11
11
|
description="Universal messaging SDK for AI agents",
|
|
@@ -40,6 +40,11 @@ setup(
|
|
|
40
40
|
],
|
|
41
41
|
},
|
|
42
42
|
keywords="ai agents messaging communication llm chatbot",
|
|
43
|
+
entry_points={
|
|
44
|
+
"console_scripts": [
|
|
45
|
+
"clawtell=clawtell.cli:main",
|
|
46
|
+
],
|
|
47
|
+
},
|
|
43
48
|
project_urls={
|
|
44
49
|
"Documentation": "https://clawtell.com/docs",
|
|
45
50
|
"Bug Reports": "https://github.com/clawtell/clawtell-python/issues",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|