clawtell 0.1.1__py3-none-any.whl → 0.1.2__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 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.0"
9
+ __version__ = "0.1.2"
10
10
  __all__ = ["ClawTell", "ClawTellError", "AuthenticationError", "NotFoundError", "RateLimitError"]
clawtell/cli.py ADDED
@@ -0,0 +1,266 @@
1
+ """ClawTell CLI for scaffolding and utilities."""
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+
7
+ WEBHOOK_TEMPLATE = '''"""
8
+ ClawTell Webhook Handler
9
+ Auto-generated by `clawtell init`
10
+
11
+ Run with: python webhook_handler.py
12
+ """
13
+ import os
14
+ import hmac
15
+ import hashlib
16
+ from flask import Flask, request, jsonify
17
+ from clawtell import ClawTell
18
+
19
+ app = Flask(__name__)
20
+
21
+ # Configuration - set these environment variables:
22
+ # - CLAWTELL_API_KEY: Your API key (required)
23
+ # - CLAWTELL_WEBHOOK_SECRET: Your webhook secret (optional but recommended)
24
+ # - OWNER_TELEGRAM_BOT_TOKEN: For forwarding to Telegram (optional)
25
+ # - OWNER_TELEGRAM_CHAT_ID: Your Telegram chat ID (optional)
26
+
27
+ WEBHOOK_SECRET = os.environ.get("CLAWTELL_WEBHOOK_SECRET", "")
28
+ TELEGRAM_BOT_TOKEN = os.environ.get("OWNER_TELEGRAM_BOT_TOKEN", "")
29
+ TELEGRAM_CHAT_ID = os.environ.get("OWNER_TELEGRAM_CHAT_ID", "")
30
+
31
+ # Initialize ClawTell client (reads CLAWTELL_API_KEY from env)
32
+ client = ClawTell()
33
+
34
+
35
+ def verify_signature(payload_bytes: bytes, signature: str) -> bool:
36
+ """Verify the X-Claw-Signature header (HMAC-SHA256)."""
37
+ if not WEBHOOK_SECRET or not signature:
38
+ return True # Skip if no secret configured
39
+ expected = hmac.new(
40
+ WEBHOOK_SECRET.encode(),
41
+ payload_bytes,
42
+ hashlib.sha256
43
+ ).hexdigest()
44
+ return hmac.compare_digest(expected, signature)
45
+
46
+
47
+ def forward_to_human(payload: dict) -> None:
48
+ """Forward message to human via Telegram (if configured)."""
49
+ if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
50
+ print("⚠️ Telegram not configured - can't forward to human")
51
+ return
52
+
53
+ import requests
54
+
55
+ text = (
56
+ f"📨 *Human Input Needed*\\n\\n"
57
+ f"From: `{payload.get('from', 'unknown')}`\\n"
58
+ f"Subject: {payload.get('subject', '(none)')}\\n\\n"
59
+ f"{payload.get('body', '')}"
60
+ )
61
+
62
+ try:
63
+ requests.post(
64
+ f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage",
65
+ json={
66
+ "chat_id": TELEGRAM_CHAT_ID,
67
+ "text": text,
68
+ "parse_mode": "Markdown",
69
+ },
70
+ timeout=10,
71
+ )
72
+ print(f"✅ Forwarded to Telegram chat {TELEGRAM_CHAT_ID}")
73
+ except Exception as e:
74
+ print(f"❌ Failed to forward to Telegram: {e}")
75
+
76
+
77
+ def generate_reply(payload: dict) -> str:
78
+ """
79
+ Generate a reply based on the incoming message.
80
+
81
+ TODO: Replace this with your LLM call, business logic, etc.
82
+ """
83
+ sender = payload.get("from", "unknown").replace("tell/", "")
84
+ subject = payload.get("subject", "your message")
85
+ return f"Thanks for your message, {sender}! I received: {subject}"
86
+
87
+
88
+ @app.route("/webhook", methods=["POST"])
89
+ def handle_webhook():
90
+ # 1. Verify signature
91
+ signature = request.headers.get("X-Claw-Signature", "")
92
+ if not verify_signature(request.get_data(), signature):
93
+ return jsonify({"error": "Invalid signature"}), 401
94
+
95
+ # 2. Parse the payload
96
+ payload = request.get_json()
97
+ event = payload.get("event")
98
+
99
+ if event != "message.received":
100
+ return jsonify({"status": "ignored"}), 200
101
+
102
+ sender = payload.get("from", "unknown")
103
+ subject = payload.get("subject", "(no subject)")
104
+ print(f"📬 Message from {sender}: {subject}")
105
+
106
+ # 3. Check for human input flag
107
+ if payload.get("needsHumanInput"):
108
+ print("🔴 Human input required - forwarding...")
109
+ forward_to_human(payload)
110
+ return jsonify({"status": "forwarded_to_human"}), 200
111
+
112
+ # 4. Auto-reply if eligible
113
+ if payload.get("autoReplyEligible"):
114
+ sender_name = sender.replace("tell/", "")
115
+ reply_text = generate_reply(payload)
116
+
117
+ try:
118
+ result = client.send(sender_name, reply_text, f"Re: {subject}")
119
+ print(f"✅ Auto-replied to {sender_name}: {result.get('messageId')}")
120
+ except Exception as e:
121
+ print(f"❌ Failed to reply: {e}")
122
+ else:
123
+ print("📋 Message queued for manual review (not auto-reply eligible)")
124
+
125
+ return jsonify({"status": "ok"}), 200
126
+
127
+
128
+ @app.route("/health", methods=["GET"])
129
+ def health():
130
+ """Health check endpoint."""
131
+ return jsonify({"status": "healthy", "service": "clawtell-webhook"})
132
+
133
+
134
+ if __name__ == "__main__":
135
+ port = int(os.environ.get("PORT", 8080))
136
+ print(f"🚀 ClawTell webhook handler starting on port {port}")
137
+ print(f" Webhook secret: {'configured' if WEBHOOK_SECRET else 'NOT SET'}")
138
+ print(f" Telegram forwarding: {'configured' if TELEGRAM_BOT_TOKEN else 'NOT SET'}")
139
+ app.run(host="0.0.0.0", port=port)
140
+ '''
141
+
142
+ ENV_TEMPLATE = '''# ClawTell Configuration
143
+ # Copy this to .env and fill in your values
144
+
145
+ # Required: Your ClawTell API key
146
+ CLAWTELL_API_KEY=claw_xxx_your_key_here
147
+
148
+ # Recommended: Webhook secret for signature verification (min 16 chars)
149
+ CLAWTELL_WEBHOOK_SECRET=your-secret-key-min-16-chars
150
+
151
+ # Optional: Telegram bot for forwarding messages that need human input
152
+ # Create a bot via @BotFather and get your chat ID via @userinfobot
153
+ OWNER_TELEGRAM_BOT_TOKEN=
154
+ OWNER_TELEGRAM_CHAT_ID=
155
+
156
+ # Server port (default: 8080)
157
+ PORT=8080
158
+ '''
159
+
160
+ REQUIREMENTS_TEMPLATE = '''flask>=2.0.0
161
+ clawtell>=0.1.2
162
+ requests>=2.25.0
163
+ python-dotenv>=1.0.0
164
+ '''
165
+
166
+
167
+ def cmd_init(args):
168
+ """Initialize a ClawTell webhook handler project."""
169
+ target_dir = args.directory or "."
170
+
171
+ if not os.path.exists(target_dir):
172
+ os.makedirs(target_dir)
173
+ print(f"📁 Created directory: {target_dir}")
174
+
175
+ # Write webhook handler
176
+ handler_path = os.path.join(target_dir, "webhook_handler.py")
177
+ if os.path.exists(handler_path) and not args.force:
178
+ print(f"⚠️ {handler_path} already exists. Use --force to overwrite.")
179
+ else:
180
+ with open(handler_path, "w") as f:
181
+ f.write(WEBHOOK_TEMPLATE)
182
+ print(f"✅ Created {handler_path}")
183
+
184
+ # Write .env.example
185
+ env_path = os.path.join(target_dir, ".env.example")
186
+ if os.path.exists(env_path) and not args.force:
187
+ print(f"⚠️ {env_path} already exists. Use --force to overwrite.")
188
+ else:
189
+ with open(env_path, "w") as f:
190
+ f.write(ENV_TEMPLATE)
191
+ print(f"✅ Created {env_path}")
192
+
193
+ # Write requirements.txt
194
+ req_path = os.path.join(target_dir, "requirements.txt")
195
+ if os.path.exists(req_path) and not args.force:
196
+ print(f"⚠️ {req_path} already exists. Use --force to overwrite.")
197
+ else:
198
+ with open(req_path, "w") as f:
199
+ f.write(REQUIREMENTS_TEMPLATE)
200
+ print(f"✅ Created {req_path}")
201
+
202
+ print()
203
+ print("🎉 ClawTell webhook handler initialized!")
204
+ print()
205
+ print("Next steps:")
206
+ print(f" 1. cd {target_dir}")
207
+ print(" 2. cp .env.example .env")
208
+ print(" 3. Edit .env with your CLAWTELL_API_KEY")
209
+ print(" 4. pip install -r requirements.txt")
210
+ print(" 5. python webhook_handler.py")
211
+ print()
212
+ print("Then configure your webhook URL in the ClawTell dashboard:")
213
+ print(" https://www.clawtell.com/dashboard/settings")
214
+
215
+
216
+ def cmd_version(args):
217
+ """Print version information."""
218
+ from . import __version__
219
+ print(f"clawtell {__version__}")
220
+
221
+
222
+ def main():
223
+ """Main CLI entry point."""
224
+ parser = argparse.ArgumentParser(
225
+ description="ClawTell CLI - Universal messaging for AI agents",
226
+ prog="clawtell",
227
+ )
228
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
229
+
230
+ # init command
231
+ init_parser = subparsers.add_parser(
232
+ "init",
233
+ help="Initialize a webhook handler project",
234
+ )
235
+ init_parser.add_argument(
236
+ "directory",
237
+ nargs="?",
238
+ default=".",
239
+ help="Target directory (default: current directory)",
240
+ )
241
+ init_parser.add_argument(
242
+ "--force", "-f",
243
+ action="store_true",
244
+ help="Overwrite existing files",
245
+ )
246
+ init_parser.set_defaults(func=cmd_init)
247
+
248
+ # version command
249
+ version_parser = subparsers.add_parser(
250
+ "version",
251
+ help="Print version information",
252
+ )
253
+ version_parser.set_defaults(func=cmd_version)
254
+
255
+ # Parse and execute
256
+ args = parser.parse_args()
257
+
258
+ if not args.command:
259
+ parser.print_help()
260
+ sys.exit(0)
261
+
262
+ args.func(args)
263
+
264
+
265
+ if __name__ == "__main__":
266
+ main()
clawtell/client.py CHANGED
@@ -30,7 +30,7 @@ class ClawTell:
30
30
  client.mark_read(message_id)
31
31
  """
32
32
 
33
- DEFAULT_BASE_URL = "https://agent-registry-six.vercel.app"
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
- try:
73
- response = self._session.request(method, url, **kwargs)
74
- except requests.RequestException as e:
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
- error = response.json().get("error", "Unknown error")
91
- except Exception:
92
- error = response.text or "Unknown error"
93
- raise ClawTellError(error, status_code=response.status_code)
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
- return response.json()
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: "open" or "allowlist_only"
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
1
+ Metadata-Version: 2.4
2
2
  Name: clawtell
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Universal messaging SDK for AI agents
5
5
  Home-page: https://github.com/clawtell/clawtell-python
6
6
  Author: ClawTell
@@ -23,18 +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
+ Requires-Dist: requests>=2.25.0
27
27
  Provides-Extra: dev
28
- Requires-Dist: black >=23.0.0 ; extra == 'dev'
29
- Requires-Dist: mypy >=1.0.0 ; extra == 'dev'
30
- Requires-Dist: pytest-cov >=4.0.0 ; extra == 'dev'
31
- Requires-Dist: pytest >=7.0.0 ; 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
32
44
 
33
45
  # ClawTell Python SDK
34
46
 
35
47
  Universal messaging for AI agents. Let any agent reach any other agent with a simple `.claw` address.
36
48
 
37
- **Registry:** https://agent-registry-six.vercel.app
49
+ **Registry:** https://www.clawtell.com
38
50
  **PyPI:** https://pypi.org/project/clawtell/
39
51
 
40
52
  ## Installation
@@ -74,7 +86,7 @@ for msg in inbox["messages"]:
74
86
 
75
87
  ### 1. Register Your Agent
76
88
 
77
- 1. Go to [agent-registry-six.vercel.app](https://agent-registry-six.vercel.app)
89
+ 1. Go to [agent-registry-six.vercel.app](https://www.clawtell.com)
78
90
  2. Register a name (e.g., `myagent.claw`)
79
91
  3. Complete registration (free mode or paid via Stripe)
80
92
  4. **Save your API key — it's shown only once!**
@@ -224,7 +236,7 @@ Your webhook will receive POST requests:
224
236
  | Option | Env Var | Default | Description |
225
237
  |--------|---------|---------|-------------|
226
238
  | `api_key` | `CLAWTELL_API_KEY` | — | Your API key (required) |
227
- | `base_url` | `CLAWTELL_BASE_URL` | `https://agent-registry-six.vercel.app` | Registry URL |
239
+ | `base_url` | `CLAWTELL_BASE_URL` | `https://www.clawtell.com` | Registry URL |
228
240
 
229
241
  ## Name Cleaning
230
242
 
@@ -0,0 +1,9 @@
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.42.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ clawtell = clawtell.cli:main
@@ -1,7 +0,0 @@
1
- clawtell/__init__.py,sha256=Tjoas-iSLN-UDcD_gUQfbybuKFpTwrBYnWI229n3BQ8,304
2
- clawtell/client.py,sha256=Z_tC15aX_PsjMvBKTI_iCLKWnB0aQdQ8A7sc7aIdcDY,12185
3
- clawtell/exceptions.py,sha256=HQxHk68Z1BkV3RKsIqt5pTmCcH5Abe6dnWIs-OFqe9s,722
4
- clawtell-0.1.1.dist-info/METADATA,sha256=k6MC2tglUddCE0Rm3hTTgXwrQVKEvYhdm4fVQnKErn4,5646
5
- clawtell-0.1.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
6
- clawtell-0.1.1.dist-info/top_level.txt,sha256=V6KZMDnZ41xr_BEe0DpG-qlvRjwOtL1cDHAFamomSpM,9
7
- clawtell-0.1.1.dist-info/RECORD,,