clawtell 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl
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/__init__.py +1 -1
- clawtell/cli.py +173 -44
- {clawtell-0.1.2.dist-info → clawtell-0.1.3.dist-info}/METADATA +1 -1
- clawtell-0.1.3.dist-info/RECORD +9 -0
- clawtell-0.1.2.dist-info/RECORD +0 -9
- {clawtell-0.1.2.dist-info → clawtell-0.1.3.dist-info}/WHEEL +0 -0
- {clawtell-0.1.2.dist-info → clawtell-0.1.3.dist-info}/entry_points.txt +0 -0
- {clawtell-0.1.2.dist-info → clawtell-0.1.3.dist-info}/top_level.txt +0 -0
clawtell/__init__.py
CHANGED
|
@@ -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"]
|
clawtell/cli.py
CHANGED
|
@@ -13,20 +13,40 @@ Run with: python webhook_handler.py
|
|
|
13
13
|
import os
|
|
14
14
|
import hmac
|
|
15
15
|
import hashlib
|
|
16
|
+
import requests
|
|
16
17
|
from flask import Flask, request, jsonify
|
|
17
18
|
from clawtell import ClawTell
|
|
18
19
|
|
|
19
20
|
app = Flask(__name__)
|
|
20
21
|
|
|
21
|
-
#
|
|
22
|
-
# -
|
|
23
|
-
#
|
|
24
|
-
# - OWNER_TELEGRAM_BOT_TOKEN: For forwarding to Telegram (optional)
|
|
25
|
-
# - OWNER_TELEGRAM_CHAT_ID: Your Telegram chat ID (optional)
|
|
22
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
23
|
+
# CONFIGURATION - Set these environment variables
|
|
24
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
26
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)
|
|
27
30
|
WEBHOOK_SECRET = os.environ.get("CLAWTELL_WEBHOOK_SECRET", "")
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
30
50
|
|
|
31
51
|
# Initialize ClawTell client (reads CLAWTELL_API_KEY from env)
|
|
32
52
|
client = ClawTell()
|
|
@@ -44,34 +64,115 @@ def verify_signature(payload_bytes: bytes, signature: str) -> bool:
|
|
|
44
64
|
return hmac.compare_digest(expected, signature)
|
|
45
65
|
|
|
46
66
|
|
|
47
|
-
def
|
|
48
|
-
"""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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]
|
|
52
76
|
|
|
53
|
-
|
|
77
|
+
success = False
|
|
54
78
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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}")
|
|
61
101
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
75
176
|
|
|
76
177
|
|
|
77
178
|
def generate_reply(payload: dict) -> str:
|
|
@@ -105,8 +206,8 @@ def handle_webhook():
|
|
|
105
206
|
|
|
106
207
|
# 3. Check for human input flag
|
|
107
208
|
if payload.get("needsHumanInput"):
|
|
108
|
-
print("🔴 Human input required - forwarding...")
|
|
109
|
-
|
|
209
|
+
print("🔴 Human input required - forwarding to owner...")
|
|
210
|
+
notify_human(payload)
|
|
110
211
|
return jsonify({"status": "forwarded_to_human"}), 200
|
|
111
212
|
|
|
112
213
|
# 4. Auto-reply if eligible
|
|
@@ -133,25 +234,52 @@ def health():
|
|
|
133
234
|
|
|
134
235
|
if __name__ == "__main__":
|
|
135
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
|
+
|
|
136
249
|
print(f"🚀 ClawTell webhook handler starting on port {port}")
|
|
137
|
-
print(f" Webhook secret: {'configured' if WEBHOOK_SECRET else 'NOT SET'}")
|
|
138
|
-
print(f"
|
|
250
|
+
print(f" Webhook secret: {'configured ✓' if WEBHOOK_SECRET else 'NOT SET'}")
|
|
251
|
+
print(f" Human notifications: {', '.join(channels) if channels else 'NONE CONFIGURED'}")
|
|
139
252
|
app.run(host="0.0.0.0", port=port)
|
|
140
253
|
'''
|
|
141
254
|
|
|
142
|
-
ENV_TEMPLATE = '''#
|
|
255
|
+
ENV_TEMPLATE = '''# ══════════════════════════════════════════════════════════════════════
|
|
256
|
+
# ClawTell Configuration
|
|
143
257
|
# Copy this to .env and fill in your values
|
|
258
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
144
259
|
|
|
145
|
-
# Required: Your ClawTell API key
|
|
260
|
+
# Required: Your ClawTell API key (get from dashboard after registration)
|
|
146
261
|
CLAWTELL_API_KEY=claw_xxx_your_key_here
|
|
147
262
|
|
|
148
263
|
# Recommended: Webhook secret for signature verification (min 16 chars)
|
|
149
264
|
CLAWTELL_WEBHOOK_SECRET=your-secret-key-min-16-chars
|
|
150
265
|
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
|
|
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=
|
|
155
283
|
|
|
156
284
|
# Server port (default: 8080)
|
|
157
285
|
PORT=8080
|
|
@@ -206,8 +334,9 @@ def cmd_init(args):
|
|
|
206
334
|
print(f" 1. cd {target_dir}")
|
|
207
335
|
print(" 2. cp .env.example .env")
|
|
208
336
|
print(" 3. Edit .env with your CLAWTELL_API_KEY")
|
|
209
|
-
print(" 4.
|
|
210
|
-
print(" 5.
|
|
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")
|
|
211
340
|
print()
|
|
212
341
|
print("Then configure your webhook URL in the ClawTell dashboard:")
|
|
213
342
|
print(" https://www.clawtell.com/dashboard/settings")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
clawtell/__init__.py,sha256=BhjKMjTEmS3ACzWcyVBhP_sIGa-3fyKKw55zg9vQfzQ,304
|
|
2
|
+
clawtell/cli.py,sha256=BruTSnAa5Z4kDcvgaAgQhCwjEuiYi9uI_T4uyEGf90o,14516
|
|
3
|
+
clawtell/client.py,sha256=dkAx9Df9YD6LYo3oD_J09KMnLwBsJX_46DxSYMFu2Ww,14143
|
|
4
|
+
clawtell/exceptions.py,sha256=HQxHk68Z1BkV3RKsIqt5pTmCcH5Abe6dnWIs-OFqe9s,722
|
|
5
|
+
clawtell-0.1.3.dist-info/METADATA,sha256=HJjKd2y8jcUhIpmbGRy1hZgn52lmoTh7agw4IGVd5JY,5858
|
|
6
|
+
clawtell-0.1.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
7
|
+
clawtell-0.1.3.dist-info/entry_points.txt,sha256=QSphdy-oEADOkCz63ROVmNZ2FXCYfkJvuSM6ym4Nhho,47
|
|
8
|
+
clawtell-0.1.3.dist-info/top_level.txt,sha256=V6KZMDnZ41xr_BEe0DpG-qlvRjwOtL1cDHAFamomSpM,9
|
|
9
|
+
clawtell-0.1.3.dist-info/RECORD,,
|
clawtell-0.1.2.dist-info/RECORD
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
clawtell/__init__.py,sha256=-uneQZO-iDvfR2KRHW9SLPB-FWfzFvoGX9uj9hVcoho,304
|
|
2
|
-
clawtell/cli.py,sha256=ZHxd_zEFLqdhTsULGJf0vMmdA4V0JA7ioemr0eBrlMc,8240
|
|
3
|
-
clawtell/client.py,sha256=dkAx9Df9YD6LYo3oD_J09KMnLwBsJX_46DxSYMFu2Ww,14143
|
|
4
|
-
clawtell/exceptions.py,sha256=HQxHk68Z1BkV3RKsIqt5pTmCcH5Abe6dnWIs-OFqe9s,722
|
|
5
|
-
clawtell-0.1.2.dist-info/METADATA,sha256=G9b36zRTlJLa79pi9HPUB4U1hucUYfcfsHYheuTNoDk,5858
|
|
6
|
-
clawtell-0.1.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
7
|
-
clawtell-0.1.2.dist-info/entry_points.txt,sha256=QSphdy-oEADOkCz63ROVmNZ2FXCYfkJvuSM6ym4Nhho,47
|
|
8
|
-
clawtell-0.1.2.dist-info/top_level.txt,sha256=V6KZMDnZ41xr_BEe0DpG-qlvRjwOtL1cDHAFamomSpM,9
|
|
9
|
-
clawtell-0.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|