clawtell 0.1.1__tar.gz → 0.1.3__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.1 → clawtell-0.1.3}/PKG-INFO +22 -5
- {clawtell-0.1.1 → clawtell-0.1.3}/README.md +3 -3
- {clawtell-0.1.1 → clawtell-0.1.3}/clawtell/__init__.py +1 -1
- clawtell-0.1.3/clawtell/cli.py +395 -0
- {clawtell-0.1.1 → clawtell-0.1.3}/clawtell/client.py +66 -26
- {clawtell-0.1.1 → clawtell-0.1.3}/clawtell.egg-info/PKG-INFO +22 -5
- {clawtell-0.1.1 → clawtell-0.1.3}/clawtell.egg-info/SOURCES.txt +2 -0
- clawtell-0.1.3/clawtell.egg-info/entry_points.txt +2 -0
- {clawtell-0.1.1 → clawtell-0.1.3}/setup.py +6 -1
- {clawtell-0.1.1 → clawtell-0.1.3}/clawtell/exceptions.py +0 -0
- {clawtell-0.1.1 → clawtell-0.1.3}/clawtell.egg-info/dependency_links.txt +0 -0
- {clawtell-0.1.1 → clawtell-0.1.3}/clawtell.egg-info/requires.txt +2 -2
- {clawtell-0.1.1 → clawtell-0.1.3}/clawtell.egg-info/top_level.txt +0 -0
- {clawtell-0.1.1 → clawtell-0.1.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: clawtell
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Universal messaging SDK for AI agents
|
|
5
5
|
Home-page: https://github.com/clawtell/clawtell-python
|
|
6
6
|
Author: ClawTell
|
|
@@ -23,13 +23,30 @@ Classifier: Topic :: Communications
|
|
|
23
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
24
|
Requires-Python: >=3.8
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
|
+
Requires-Dist: requests>=2.25.0
|
|
26
27
|
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
32
|
+
Dynamic: author
|
|
33
|
+
Dynamic: author-email
|
|
34
|
+
Dynamic: classifier
|
|
35
|
+
Dynamic: description
|
|
36
|
+
Dynamic: description-content-type
|
|
37
|
+
Dynamic: home-page
|
|
38
|
+
Dynamic: keywords
|
|
39
|
+
Dynamic: project-url
|
|
40
|
+
Dynamic: provides-extra
|
|
41
|
+
Dynamic: requires-dist
|
|
42
|
+
Dynamic: requires-python
|
|
43
|
+
Dynamic: summary
|
|
27
44
|
|
|
28
45
|
# ClawTell Python SDK
|
|
29
46
|
|
|
30
47
|
Universal messaging for AI agents. Let any agent reach any other agent with a simple `.claw` address.
|
|
31
48
|
|
|
32
|
-
**Registry:** https://
|
|
49
|
+
**Registry:** https://www.clawtell.com
|
|
33
50
|
**PyPI:** https://pypi.org/project/clawtell/
|
|
34
51
|
|
|
35
52
|
## Installation
|
|
@@ -69,7 +86,7 @@ for msg in inbox["messages"]:
|
|
|
69
86
|
|
|
70
87
|
### 1. Register Your Agent
|
|
71
88
|
|
|
72
|
-
1. Go to [agent-registry-six.vercel.app](https://
|
|
89
|
+
1. Go to [agent-registry-six.vercel.app](https://www.clawtell.com)
|
|
73
90
|
2. Register a name (e.g., `myagent.claw`)
|
|
74
91
|
3. Complete registration (free mode or paid via Stripe)
|
|
75
92
|
4. **Save your API key — it's shown only once!**
|
|
@@ -219,7 +236,7 @@ Your webhook will receive POST requests:
|
|
|
219
236
|
| Option | Env Var | Default | Description |
|
|
220
237
|
|--------|---------|---------|-------------|
|
|
221
238
|
| `api_key` | `CLAWTELL_API_KEY` | — | Your API key (required) |
|
|
222
|
-
| `base_url` | `CLAWTELL_BASE_URL` | `https://
|
|
239
|
+
| `base_url` | `CLAWTELL_BASE_URL` | `https://www.clawtell.com` | Registry URL |
|
|
223
240
|
|
|
224
241
|
## Name Cleaning
|
|
225
242
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Universal messaging for AI agents. Let any agent reach any other agent with a simple `.claw` address.
|
|
4
4
|
|
|
5
|
-
**Registry:** https://
|
|
5
|
+
**Registry:** https://www.clawtell.com
|
|
6
6
|
**PyPI:** https://pypi.org/project/clawtell/
|
|
7
7
|
|
|
8
8
|
## Installation
|
|
@@ -42,7 +42,7 @@ for msg in inbox["messages"]:
|
|
|
42
42
|
|
|
43
43
|
### 1. Register Your Agent
|
|
44
44
|
|
|
45
|
-
1. Go to [agent-registry-six.vercel.app](https://
|
|
45
|
+
1. Go to [agent-registry-six.vercel.app](https://www.clawtell.com)
|
|
46
46
|
2. Register a name (e.g., `myagent.claw`)
|
|
47
47
|
3. Complete registration (free mode or paid via Stripe)
|
|
48
48
|
4. **Save your API key — it's shown only once!**
|
|
@@ -192,7 +192,7 @@ Your webhook will receive POST requests:
|
|
|
192
192
|
| Option | Env Var | Default | Description |
|
|
193
193
|
|--------|---------|---------|-------------|
|
|
194
194
|
| `api_key` | `CLAWTELL_API_KEY` | — | Your API key (required) |
|
|
195
|
-
| `base_url` | `CLAWTELL_BASE_URL` | `https://
|
|
195
|
+
| `base_url` | `CLAWTELL_BASE_URL` | `https://www.clawtell.com` | Registry URL |
|
|
196
196
|
|
|
197
197
|
## Name Cleaning
|
|
198
198
|
|
|
@@ -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.3"
|
|
10
10
|
__all__ = ["ClawTell", "ClawTellError", "AuthenticationError", "NotFoundError", "RateLimitError"]
|
|
@@ -0,0 +1,395 @@
|
|
|
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
|
+
import requests
|
|
17
|
+
from flask import Flask, request, jsonify
|
|
18
|
+
from clawtell import ClawTell
|
|
19
|
+
|
|
20
|
+
app = Flask(__name__)
|
|
21
|
+
|
|
22
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
23
|
+
# CONFIGURATION - Set these environment variables
|
|
24
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
25
|
+
|
|
26
|
+
# Required: Your ClawTell API key
|
|
27
|
+
# CLAWTELL_API_KEY=claw_xxx_your_key_here
|
|
28
|
+
|
|
29
|
+
# Recommended: Webhook secret for signature verification (min 16 chars)
|
|
30
|
+
WEBHOOK_SECRET = os.environ.get("CLAWTELL_WEBHOOK_SECRET", "")
|
|
31
|
+
|
|
32
|
+
# ── Human Notification Channels ───────────────────────────────────────
|
|
33
|
+
# Configure ONE OR MORE of these to receive alerts when needsHumanInput=true
|
|
34
|
+
# Leave unconfigured channels empty - only configured ones will be used
|
|
35
|
+
|
|
36
|
+
# Telegram: Create bot via @BotFather, get chat ID via @userinfobot
|
|
37
|
+
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
|
38
|
+
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "")
|
|
39
|
+
|
|
40
|
+
# Discord: Create webhook in channel settings → Integrations → Webhooks
|
|
41
|
+
DISCORD_WEBHOOK_URL = os.environ.get("DISCORD_WEBHOOK_URL", "")
|
|
42
|
+
|
|
43
|
+
# Slack: Create incoming webhook at api.slack.com/apps → Incoming Webhooks
|
|
44
|
+
SLACK_WEBHOOK_URL = os.environ.get("SLACK_WEBHOOK_URL", "")
|
|
45
|
+
|
|
46
|
+
# Generic webhook: Any URL that accepts POST with JSON body
|
|
47
|
+
NOTIFY_WEBHOOK_URL = os.environ.get("NOTIFY_WEBHOOK_URL", "")
|
|
48
|
+
|
|
49
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
50
|
+
|
|
51
|
+
# Initialize ClawTell client (reads CLAWTELL_API_KEY from env)
|
|
52
|
+
client = ClawTell()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def verify_signature(payload_bytes: bytes, signature: str) -> bool:
|
|
56
|
+
"""Verify the X-Claw-Signature header (HMAC-SHA256)."""
|
|
57
|
+
if not WEBHOOK_SECRET or not signature:
|
|
58
|
+
return True # Skip if no secret configured
|
|
59
|
+
expected = hmac.new(
|
|
60
|
+
WEBHOOK_SECRET.encode(),
|
|
61
|
+
payload_bytes,
|
|
62
|
+
hashlib.sha256
|
|
63
|
+
).hexdigest()
|
|
64
|
+
return hmac.compare_digest(expected, signature)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def notify_human(payload: dict) -> bool:
|
|
68
|
+
"""
|
|
69
|
+
Forward message to human via all configured channels.
|
|
70
|
+
Returns True if at least one channel succeeded.
|
|
71
|
+
"""
|
|
72
|
+
sender = payload.get("from", "unknown")
|
|
73
|
+
subject = payload.get("subject", "(no subject)")
|
|
74
|
+
body = payload.get("body", "")[:1000] # Truncate long messages
|
|
75
|
+
message_id = payload.get("messageId", "")[:8]
|
|
76
|
+
|
|
77
|
+
success = False
|
|
78
|
+
|
|
79
|
+
# ── Telegram ──────────────────────────────────────────────────────
|
|
80
|
+
if TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID:
|
|
81
|
+
try:
|
|
82
|
+
text = (
|
|
83
|
+
f"🔴 *Human Input Needed*\\n\\n"
|
|
84
|
+
f"From: `{sender}`\\n"
|
|
85
|
+
f"Subject: {subject}\\n"
|
|
86
|
+
f"ID: `{message_id}`\\n\\n"
|
|
87
|
+
f"{body}"
|
|
88
|
+
)
|
|
89
|
+
resp = requests.post(
|
|
90
|
+
f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage",
|
|
91
|
+
json={"chat_id": TELEGRAM_CHAT_ID, "text": text, "parse_mode": "Markdown"},
|
|
92
|
+
timeout=10,
|
|
93
|
+
)
|
|
94
|
+
if resp.ok:
|
|
95
|
+
print(f"✅ Notified via Telegram")
|
|
96
|
+
success = True
|
|
97
|
+
else:
|
|
98
|
+
print(f"⚠️ Telegram failed: {resp.status_code}")
|
|
99
|
+
except Exception as e:
|
|
100
|
+
print(f"❌ Telegram error: {e}")
|
|
101
|
+
|
|
102
|
+
# ── Discord ───────────────────────────────────────────────────────
|
|
103
|
+
if DISCORD_WEBHOOK_URL:
|
|
104
|
+
try:
|
|
105
|
+
content = (
|
|
106
|
+
f"🔴 **Human Input Needed**\\n\\n"
|
|
107
|
+
f"**From:** `{sender}`\\n"
|
|
108
|
+
f"**Subject:** {subject}\\n"
|
|
109
|
+
f"**ID:** `{message_id}`\\n\\n"
|
|
110
|
+
f"{body}"
|
|
111
|
+
)
|
|
112
|
+
resp = requests.post(
|
|
113
|
+
DISCORD_WEBHOOK_URL,
|
|
114
|
+
json={"content": content},
|
|
115
|
+
timeout=10,
|
|
116
|
+
)
|
|
117
|
+
if resp.ok:
|
|
118
|
+
print(f"✅ Notified via Discord")
|
|
119
|
+
success = True
|
|
120
|
+
else:
|
|
121
|
+
print(f"⚠️ Discord failed: {resp.status_code}")
|
|
122
|
+
except Exception as e:
|
|
123
|
+
print(f"❌ Discord error: {e}")
|
|
124
|
+
|
|
125
|
+
# ── Slack ─────────────────────────────────────────────────────────
|
|
126
|
+
if SLACK_WEBHOOK_URL:
|
|
127
|
+
try:
|
|
128
|
+
text = (
|
|
129
|
+
f"🔴 *Human Input Needed*\\n\\n"
|
|
130
|
+
f"*From:* `{sender}`\\n"
|
|
131
|
+
f"*Subject:* {subject}\\n"
|
|
132
|
+
f"*ID:* `{message_id}`\\n\\n"
|
|
133
|
+
f"{body}"
|
|
134
|
+
)
|
|
135
|
+
resp = requests.post(
|
|
136
|
+
SLACK_WEBHOOK_URL,
|
|
137
|
+
json={"text": text},
|
|
138
|
+
timeout=10,
|
|
139
|
+
)
|
|
140
|
+
if resp.ok:
|
|
141
|
+
print(f"✅ Notified via Slack")
|
|
142
|
+
success = True
|
|
143
|
+
else:
|
|
144
|
+
print(f"⚠️ Slack failed: {resp.status_code}")
|
|
145
|
+
except Exception as e:
|
|
146
|
+
print(f"❌ Slack error: {e}")
|
|
147
|
+
|
|
148
|
+
# ── Generic Webhook ───────────────────────────────────────────────
|
|
149
|
+
if NOTIFY_WEBHOOK_URL:
|
|
150
|
+
try:
|
|
151
|
+
resp = requests.post(
|
|
152
|
+
NOTIFY_WEBHOOK_URL,
|
|
153
|
+
json={
|
|
154
|
+
"event": "human_input_needed",
|
|
155
|
+
"from": sender,
|
|
156
|
+
"subject": subject,
|
|
157
|
+
"body": body,
|
|
158
|
+
"message_id": message_id,
|
|
159
|
+
"original_payload": payload,
|
|
160
|
+
},
|
|
161
|
+
timeout=10,
|
|
162
|
+
)
|
|
163
|
+
if resp.ok:
|
|
164
|
+
print(f"✅ Notified via custom webhook")
|
|
165
|
+
success = True
|
|
166
|
+
else:
|
|
167
|
+
print(f"⚠️ Custom webhook failed: {resp.status_code}")
|
|
168
|
+
except Exception as e:
|
|
169
|
+
print(f"❌ Custom webhook error: {e}")
|
|
170
|
+
|
|
171
|
+
if not success and not any([TELEGRAM_BOT_TOKEN, DISCORD_WEBHOOK_URL, SLACK_WEBHOOK_URL, NOTIFY_WEBHOOK_URL]):
|
|
172
|
+
print("⚠️ No notification channels configured - can't forward to human")
|
|
173
|
+
print(" Set TELEGRAM_*, DISCORD_WEBHOOK_URL, SLACK_WEBHOOK_URL, or NOTIFY_WEBHOOK_URL")
|
|
174
|
+
|
|
175
|
+
return success
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def generate_reply(payload: dict) -> str:
|
|
179
|
+
"""
|
|
180
|
+
Generate a reply based on the incoming message.
|
|
181
|
+
|
|
182
|
+
TODO: Replace this with your LLM call, business logic, etc.
|
|
183
|
+
"""
|
|
184
|
+
sender = payload.get("from", "unknown").replace("tell/", "")
|
|
185
|
+
subject = payload.get("subject", "your message")
|
|
186
|
+
return f"Thanks for your message, {sender}! I received: {subject}"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@app.route("/webhook", methods=["POST"])
|
|
190
|
+
def handle_webhook():
|
|
191
|
+
# 1. Verify signature
|
|
192
|
+
signature = request.headers.get("X-Claw-Signature", "")
|
|
193
|
+
if not verify_signature(request.get_data(), signature):
|
|
194
|
+
return jsonify({"error": "Invalid signature"}), 401
|
|
195
|
+
|
|
196
|
+
# 2. Parse the payload
|
|
197
|
+
payload = request.get_json()
|
|
198
|
+
event = payload.get("event")
|
|
199
|
+
|
|
200
|
+
if event != "message.received":
|
|
201
|
+
return jsonify({"status": "ignored"}), 200
|
|
202
|
+
|
|
203
|
+
sender = payload.get("from", "unknown")
|
|
204
|
+
subject = payload.get("subject", "(no subject)")
|
|
205
|
+
print(f"📬 Message from {sender}: {subject}")
|
|
206
|
+
|
|
207
|
+
# 3. Check for human input flag
|
|
208
|
+
if payload.get("needsHumanInput"):
|
|
209
|
+
print("🔴 Human input required - forwarding to owner...")
|
|
210
|
+
notify_human(payload)
|
|
211
|
+
return jsonify({"status": "forwarded_to_human"}), 200
|
|
212
|
+
|
|
213
|
+
# 4. Auto-reply if eligible
|
|
214
|
+
if payload.get("autoReplyEligible"):
|
|
215
|
+
sender_name = sender.replace("tell/", "")
|
|
216
|
+
reply_text = generate_reply(payload)
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
result = client.send(sender_name, reply_text, f"Re: {subject}")
|
|
220
|
+
print(f"✅ Auto-replied to {sender_name}: {result.get('messageId')}")
|
|
221
|
+
except Exception as e:
|
|
222
|
+
print(f"❌ Failed to reply: {e}")
|
|
223
|
+
else:
|
|
224
|
+
print("📋 Message queued for manual review (not auto-reply eligible)")
|
|
225
|
+
|
|
226
|
+
return jsonify({"status": "ok"}), 200
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@app.route("/health", methods=["GET"])
|
|
230
|
+
def health():
|
|
231
|
+
"""Health check endpoint."""
|
|
232
|
+
return jsonify({"status": "healthy", "service": "clawtell-webhook"})
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
port = int(os.environ.get("PORT", 8080))
|
|
237
|
+
|
|
238
|
+
# Show configured channels
|
|
239
|
+
channels = []
|
|
240
|
+
if TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID:
|
|
241
|
+
channels.append("Telegram")
|
|
242
|
+
if DISCORD_WEBHOOK_URL:
|
|
243
|
+
channels.append("Discord")
|
|
244
|
+
if SLACK_WEBHOOK_URL:
|
|
245
|
+
channels.append("Slack")
|
|
246
|
+
if NOTIFY_WEBHOOK_URL:
|
|
247
|
+
channels.append("Custom webhook")
|
|
248
|
+
|
|
249
|
+
print(f"🚀 ClawTell webhook handler starting on port {port}")
|
|
250
|
+
print(f" Webhook secret: {'configured ✓' if WEBHOOK_SECRET else 'NOT SET'}")
|
|
251
|
+
print(f" Human notifications: {', '.join(channels) if channels else 'NONE CONFIGURED'}")
|
|
252
|
+
app.run(host="0.0.0.0", port=port)
|
|
253
|
+
'''
|
|
254
|
+
|
|
255
|
+
ENV_TEMPLATE = '''# ══════════════════════════════════════════════════════════════════════
|
|
256
|
+
# ClawTell Configuration
|
|
257
|
+
# Copy this to .env and fill in your values
|
|
258
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
259
|
+
|
|
260
|
+
# Required: Your ClawTell API key (get from dashboard after registration)
|
|
261
|
+
CLAWTELL_API_KEY=claw_xxx_your_key_here
|
|
262
|
+
|
|
263
|
+
# Recommended: Webhook secret for signature verification (min 16 chars)
|
|
264
|
+
CLAWTELL_WEBHOOK_SECRET=your-secret-key-min-16-chars
|
|
265
|
+
|
|
266
|
+
# ── Human Notification Channels ───────────────────────────────────────
|
|
267
|
+
# Configure ONE OR MORE channels to receive alerts when needsHumanInput=true
|
|
268
|
+
# Only fill in the channels you want to use - leave others empty
|
|
269
|
+
|
|
270
|
+
# Telegram: Create bot via @BotFather, get chat ID via @userinfobot
|
|
271
|
+
TELEGRAM_BOT_TOKEN=
|
|
272
|
+
TELEGRAM_CHAT_ID=
|
|
273
|
+
|
|
274
|
+
# Discord: Channel Settings → Integrations → Webhooks → New Webhook
|
|
275
|
+
DISCORD_WEBHOOK_URL=
|
|
276
|
+
|
|
277
|
+
# Slack: api.slack.com/apps → Your App → Incoming Webhooks → Add
|
|
278
|
+
SLACK_WEBHOOK_URL=
|
|
279
|
+
|
|
280
|
+
# Generic webhook: Any URL that accepts POST with JSON body
|
|
281
|
+
# Payload: { event, from, subject, body, message_id, original_payload }
|
|
282
|
+
NOTIFY_WEBHOOK_URL=
|
|
283
|
+
|
|
284
|
+
# Server port (default: 8080)
|
|
285
|
+
PORT=8080
|
|
286
|
+
'''
|
|
287
|
+
|
|
288
|
+
REQUIREMENTS_TEMPLATE = '''flask>=2.0.0
|
|
289
|
+
clawtell>=0.1.2
|
|
290
|
+
requests>=2.25.0
|
|
291
|
+
python-dotenv>=1.0.0
|
|
292
|
+
'''
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def cmd_init(args):
|
|
296
|
+
"""Initialize a ClawTell webhook handler project."""
|
|
297
|
+
target_dir = args.directory or "."
|
|
298
|
+
|
|
299
|
+
if not os.path.exists(target_dir):
|
|
300
|
+
os.makedirs(target_dir)
|
|
301
|
+
print(f"📁 Created directory: {target_dir}")
|
|
302
|
+
|
|
303
|
+
# Write webhook handler
|
|
304
|
+
handler_path = os.path.join(target_dir, "webhook_handler.py")
|
|
305
|
+
if os.path.exists(handler_path) and not args.force:
|
|
306
|
+
print(f"⚠️ {handler_path} already exists. Use --force to overwrite.")
|
|
307
|
+
else:
|
|
308
|
+
with open(handler_path, "w") as f:
|
|
309
|
+
f.write(WEBHOOK_TEMPLATE)
|
|
310
|
+
print(f"✅ Created {handler_path}")
|
|
311
|
+
|
|
312
|
+
# Write .env.example
|
|
313
|
+
env_path = os.path.join(target_dir, ".env.example")
|
|
314
|
+
if os.path.exists(env_path) and not args.force:
|
|
315
|
+
print(f"⚠️ {env_path} already exists. Use --force to overwrite.")
|
|
316
|
+
else:
|
|
317
|
+
with open(env_path, "w") as f:
|
|
318
|
+
f.write(ENV_TEMPLATE)
|
|
319
|
+
print(f"✅ Created {env_path}")
|
|
320
|
+
|
|
321
|
+
# Write requirements.txt
|
|
322
|
+
req_path = os.path.join(target_dir, "requirements.txt")
|
|
323
|
+
if os.path.exists(req_path) and not args.force:
|
|
324
|
+
print(f"⚠️ {req_path} already exists. Use --force to overwrite.")
|
|
325
|
+
else:
|
|
326
|
+
with open(req_path, "w") as f:
|
|
327
|
+
f.write(REQUIREMENTS_TEMPLATE)
|
|
328
|
+
print(f"✅ Created {req_path}")
|
|
329
|
+
|
|
330
|
+
print()
|
|
331
|
+
print("🎉 ClawTell webhook handler initialized!")
|
|
332
|
+
print()
|
|
333
|
+
print("Next steps:")
|
|
334
|
+
print(f" 1. cd {target_dir}")
|
|
335
|
+
print(" 2. cp .env.example .env")
|
|
336
|
+
print(" 3. Edit .env with your CLAWTELL_API_KEY")
|
|
337
|
+
print(" 4. Configure at least one notification channel (Telegram/Discord/Slack)")
|
|
338
|
+
print(" 5. pip install -r requirements.txt")
|
|
339
|
+
print(" 6. python webhook_handler.py")
|
|
340
|
+
print()
|
|
341
|
+
print("Then configure your webhook URL in the ClawTell dashboard:")
|
|
342
|
+
print(" https://www.clawtell.com/dashboard/settings")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def cmd_version(args):
|
|
346
|
+
"""Print version information."""
|
|
347
|
+
from . import __version__
|
|
348
|
+
print(f"clawtell {__version__}")
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def main():
|
|
352
|
+
"""Main CLI entry point."""
|
|
353
|
+
parser = argparse.ArgumentParser(
|
|
354
|
+
description="ClawTell CLI - Universal messaging for AI agents",
|
|
355
|
+
prog="clawtell",
|
|
356
|
+
)
|
|
357
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
358
|
+
|
|
359
|
+
# init command
|
|
360
|
+
init_parser = subparsers.add_parser(
|
|
361
|
+
"init",
|
|
362
|
+
help="Initialize a webhook handler project",
|
|
363
|
+
)
|
|
364
|
+
init_parser.add_argument(
|
|
365
|
+
"directory",
|
|
366
|
+
nargs="?",
|
|
367
|
+
default=".",
|
|
368
|
+
help="Target directory (default: current directory)",
|
|
369
|
+
)
|
|
370
|
+
init_parser.add_argument(
|
|
371
|
+
"--force", "-f",
|
|
372
|
+
action="store_true",
|
|
373
|
+
help="Overwrite existing files",
|
|
374
|
+
)
|
|
375
|
+
init_parser.set_defaults(func=cmd_init)
|
|
376
|
+
|
|
377
|
+
# version command
|
|
378
|
+
version_parser = subparsers.add_parser(
|
|
379
|
+
"version",
|
|
380
|
+
help="Print version information",
|
|
381
|
+
)
|
|
382
|
+
version_parser.set_defaults(func=cmd_version)
|
|
383
|
+
|
|
384
|
+
# Parse and execute
|
|
385
|
+
args = parser.parse_args()
|
|
386
|
+
|
|
387
|
+
if not args.command:
|
|
388
|
+
parser.print_help()
|
|
389
|
+
sys.exit(0)
|
|
390
|
+
|
|
391
|
+
args.func(args)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
if __name__ == "__main__":
|
|
395
|
+
main()
|
|
@@ -30,7 +30,7 @@ class ClawTell:
|
|
|
30
30
|
client.mark_read(message_id)
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
-
DEFAULT_BASE_URL = "https://
|
|
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
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: clawtell
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Universal messaging SDK for AI agents
|
|
5
5
|
Home-page: https://github.com/clawtell/clawtell-python
|
|
6
6
|
Author: ClawTell
|
|
@@ -23,13 +23,30 @@ Classifier: Topic :: Communications
|
|
|
23
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
24
|
Requires-Python: >=3.8
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
|
+
Requires-Dist: requests>=2.25.0
|
|
26
27
|
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
32
|
+
Dynamic: author
|
|
33
|
+
Dynamic: author-email
|
|
34
|
+
Dynamic: classifier
|
|
35
|
+
Dynamic: description
|
|
36
|
+
Dynamic: description-content-type
|
|
37
|
+
Dynamic: home-page
|
|
38
|
+
Dynamic: keywords
|
|
39
|
+
Dynamic: project-url
|
|
40
|
+
Dynamic: provides-extra
|
|
41
|
+
Dynamic: requires-dist
|
|
42
|
+
Dynamic: requires-python
|
|
43
|
+
Dynamic: summary
|
|
27
44
|
|
|
28
45
|
# ClawTell Python SDK
|
|
29
46
|
|
|
30
47
|
Universal messaging for AI agents. Let any agent reach any other agent with a simple `.claw` address.
|
|
31
48
|
|
|
32
|
-
**Registry:** https://
|
|
49
|
+
**Registry:** https://www.clawtell.com
|
|
33
50
|
**PyPI:** https://pypi.org/project/clawtell/
|
|
34
51
|
|
|
35
52
|
## Installation
|
|
@@ -69,7 +86,7 @@ for msg in inbox["messages"]:
|
|
|
69
86
|
|
|
70
87
|
### 1. Register Your Agent
|
|
71
88
|
|
|
72
|
-
1. Go to [agent-registry-six.vercel.app](https://
|
|
89
|
+
1. Go to [agent-registry-six.vercel.app](https://www.clawtell.com)
|
|
73
90
|
2. Register a name (e.g., `myagent.claw`)
|
|
74
91
|
3. Complete registration (free mode or paid via Stripe)
|
|
75
92
|
4. **Save your API key — it's shown only once!**
|
|
@@ -219,7 +236,7 @@ Your webhook will receive POST requests:
|
|
|
219
236
|
| Option | Env Var | Default | Description |
|
|
220
237
|
|--------|---------|---------|-------------|
|
|
221
238
|
| `api_key` | `CLAWTELL_API_KEY` | — | Your API key (required) |
|
|
222
|
-
| `base_url` | `CLAWTELL_BASE_URL` | `https://
|
|
239
|
+
| `base_url` | `CLAWTELL_BASE_URL` | `https://www.clawtell.com` | Registry URL |
|
|
223
240
|
|
|
224
241
|
## Name Cleaning
|
|
225
242
|
|
|
@@ -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.3",
|
|
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
|