clawtell 0.1.2__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.2 → clawtell-0.1.3}/PKG-INFO +1 -1
- {clawtell-0.1.2 → clawtell-0.1.3}/clawtell/__init__.py +1 -1
- clawtell-0.1.3/clawtell/cli.py +395 -0
- {clawtell-0.1.2 → clawtell-0.1.3}/clawtell.egg-info/PKG-INFO +1 -1
- {clawtell-0.1.2 → clawtell-0.1.3}/setup.py +1 -1
- clawtell-0.1.2/clawtell/cli.py +0 -266
- {clawtell-0.1.2 → clawtell-0.1.3}/README.md +0 -0
- {clawtell-0.1.2 → clawtell-0.1.3}/clawtell/client.py +0 -0
- {clawtell-0.1.2 → clawtell-0.1.3}/clawtell/exceptions.py +0 -0
- {clawtell-0.1.2 → clawtell-0.1.3}/clawtell.egg-info/SOURCES.txt +0 -0
- {clawtell-0.1.2 → clawtell-0.1.3}/clawtell.egg-info/dependency_links.txt +0 -0
- {clawtell-0.1.2 → clawtell-0.1.3}/clawtell.egg-info/entry_points.txt +0 -0
- {clawtell-0.1.2 → clawtell-0.1.3}/clawtell.egg-info/requires.txt +0 -0
- {clawtell-0.1.2 → clawtell-0.1.3}/clawtell.egg-info/top_level.txt +0 -0
- {clawtell-0.1.2 → clawtell-0.1.3}/setup.cfg +0 -0
|
@@ -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()
|
clawtell-0.1.2/clawtell/cli.py
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|