clawtell 0.1.2__tar.gz → 0.1.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clawtell
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Universal messaging SDK for AI agents
5
5
  Home-page: https://github.com/clawtell/clawtell-python
6
6
  Author: 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.2"
10
- __all__ = ["ClawTell", "ClawTellError", "AuthenticationError", "NotFoundError", "RateLimitError"]
9
+ __version__ = "0.1.4"
10
+ __all__ = ["ClawTell", "ClawTellError", "AuthenticationError", "NotFoundError", "RateLimitError", "__version__"]
@@ -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()
@@ -386,3 +386,42 @@ class ClawTell:
386
386
  dict with checkout URL (paid mode) or new expiry (free mode)
387
387
  """
388
388
  return self._request("POST", "/renew", json={"years": years})
389
+
390
+ # ─────────────────────────────────────────────────────────────
391
+ # Updates
392
+ # ─────────────────────────────────────────────────────────────
393
+
394
+ def check_updates(self) -> Dict[str, Any]:
395
+ """
396
+ Check for SDK and skill updates.
397
+
398
+ Returns:
399
+ dict with hasUpdates, updates list, latestVersions
400
+
401
+ Example:
402
+ updates = client.check_updates()
403
+ if updates['hasUpdates']:
404
+ for update in updates['updates']:
405
+ print(f"Update available: {update['sdk']} {update['latest']}")
406
+ print(f" Upgrade: {update['upgradeCommand']}")
407
+ """
408
+ return self._request("GET", "/updates")
409
+
410
+ def register_version(self, notify_on_updates: bool = True) -> Dict[str, Any]:
411
+ """
412
+ Register your SDK version with ClawTell for update notifications.
413
+ Call this on agent startup to get notified of important updates.
414
+
415
+ Args:
416
+ notify_on_updates: Whether to receive webhook notifications for updates
417
+
418
+ Returns:
419
+ dict with hasUpdates and any available updates
420
+ """
421
+ from . import __version__
422
+
423
+ return self._request("POST", "/updates", json={
424
+ "sdk": "python",
425
+ "sdkVersion": __version__,
426
+ "notifyOnUpdates": notify_on_updates,
427
+ })
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clawtell
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Universal messaging SDK for AI agents
5
5
  Home-page: https://github.com/clawtell/clawtell-python
6
6
  Author: ClawTell
@@ -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.2",
8
+ version="0.1.4",
9
9
  author="ClawTell",
10
10
  author_email="hello@clawtell.com",
11
11
  description="Universal messaging SDK for AI agents",
@@ -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