pythonclaw 0.6.3__py3-none-any.whl → 0.6.5__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.
@@ -244,8 +244,7 @@ class TelegramBot:
244
244
  except Exception:
245
245
  pass
246
246
 
247
- # Max wall-clock time for a single agent invocation (seconds).
248
- _AGENT_TIMEOUT = 180
247
+ _AGENT_TIMEOUT = 600
249
248
 
250
249
  async def _flush_stream(
251
250
  self,
@@ -253,98 +252,47 @@ class TelegramBot:
253
252
  token_queue: "_queue.Queue[str]",
254
253
  future: "asyncio.Future[str]",
255
254
  ) -> None:
256
- """Progressively stream tokens to Telegram via edit-in-place.
255
+ """Collect streamed tokens and deliver as 2-3 large messages.
257
256
 
258
- Uses send-then-edit (like OpenClaw): one live message that gets
259
- updated as tokens arrive (~1.5 s throttle). Tool-call markers
260
- produce a short status line and start a fresh message.
257
+ Strategy: accumulate all tokens silently. Tool-call markers are
258
+ stripped but do NOT trigger new messages. Content is edit-in-place
259
+ updated into a single live message; only when a message hits the
260
+ Telegram 4096 char limit is a new message started.
261
261
 
262
- Safeguards against hangs:
263
- - **Heartbeat**: if no tokens arrive for 15 s, sends a "still
264
- working" notification so the user knows the bot is alive.
265
- - **Overall timeout**: after ``_AGENT_TIMEOUT`` seconds the
266
- future is abandoned and a timeout message is sent.
262
+ No heartbeat / "still working" messages are sent.
267
263
  """
268
264
  buf: list[str] = []
269
265
  live_msg = None
270
266
  live_text = ""
271
267
  sent_any = False
272
- THROTTLE = 1.5
273
- HEARTBEAT_INTERVAL = 15.0
268
+ THROTTLE = 2.0
274
269
  last_edit = time.monotonic()
275
- last_token_time = time.monotonic()
276
270
  start_time = time.monotonic()
277
- heartbeat_sent = False
278
271
  _MARKER = re.compile(r'`\[calling:\s*([^\]]+)\]`')
279
272
 
280
273
  while not future.done():
281
- # ── Overall timeout guard ─────────────────────────────────
282
274
  if (time.monotonic() - start_time) > self._AGENT_TIMEOUT:
283
275
  logger.warning(
284
276
  "[Telegram] Agent timeout after %ds", self._AGENT_TIMEOUT,
285
277
  )
286
- try:
287
- await update.message.reply_text(
288
- "\u23f0 The operation timed out. "
289
- "Please try a simpler request."
290
- )
291
- except Exception:
292
- pass
293
- return
278
+ break
294
279
 
295
- # ── Drain token queue ─────────────────────────────────────
296
280
  drained = False
297
281
  while True:
298
282
  try:
299
283
  buf.append(token_queue.get_nowait())
300
284
  drained = True
301
- last_token_time = time.monotonic()
302
- heartbeat_sent = False
303
285
  except _queue.Empty:
304
286
  break
305
287
 
306
- # ── Heartbeat: notify user during long silences ───────────
307
- if (
308
- not drained
309
- and not heartbeat_sent
310
- and (time.monotonic() - last_token_time) > HEARTBEAT_INTERVAL
311
- ):
312
- try:
313
- await update.message.reply_text(
314
- "\u23f3 Still working\u2026"
315
- )
316
- except Exception:
317
- pass
318
- heartbeat_sent = True
319
-
320
288
  if not drained:
321
- await asyncio.sleep(0.3)
289
+ await asyncio.sleep(0.4)
322
290
  continue
323
291
 
324
- raw = "".join(buf)
292
+ raw = _MARKER.sub("", "".join(buf))
293
+ text = _clean_response(raw)
325
294
  now = time.monotonic()
326
295
 
327
- # ── Tool-call marker → status line + new message ──────────
328
- marker = _MARKER.search(raw)
329
- if marker:
330
- before = _clean_response(raw[:marker.start()])
331
- if before and before != live_text:
332
- try:
333
- if live_msg:
334
- await live_msg.edit_text(before[:4096])
335
- else:
336
- await update.message.reply_text(before[:4096])
337
- sent_any = True
338
- except Exception:
339
- pass
340
- live_msg = None
341
- live_text = ""
342
- buf = [raw[marker.end():].lstrip()]
343
- last_edit = now
344
- continue
345
-
346
- # ── Regular text → edit-in-place ──────────────────────────
347
- text = _clean_response(raw)
348
296
  if text and text != live_text and (now - last_edit) >= THROTTLE:
349
297
  try:
350
298
  if live_msg is None:
@@ -356,26 +304,27 @@ class TelegramBot:
356
304
  await live_msg.edit_text(text)
357
305
  live_text = text
358
306
  else:
359
- await live_msg.edit_text(text[:4096])
307
+ await live_msg.edit_text(live_text)
360
308
  live_msg = None
361
309
  live_text = ""
362
- buf = [text[4096:]]
310
+ buf = [text[len(live_text):] if live_text else text]
363
311
  sent_any = True
364
312
  except Exception:
365
313
  pass
366
314
  last_edit = now
367
315
 
368
- await asyncio.sleep(0.3)
316
+ await asyncio.sleep(0.4)
369
317
 
370
318
  # ── Final drain ───────────────────────────────────────────────
371
- response = future.result()
319
+ response = future.result() if future.done() else "(timed out)"
372
320
  while True:
373
321
  try:
374
322
  buf.append(token_queue.get_nowait())
375
323
  except _queue.Empty:
376
324
  break
377
325
 
378
- remaining = _clean_response("".join(buf).strip())
326
+ raw = _MARKER.sub("", "".join(buf))
327
+ remaining = _clean_response(raw.strip())
379
328
  if remaining and remaining != live_text:
380
329
  try:
381
330
  if live_msg and len(remaining) <= 4096:
pythonclaw/core/agent.py CHANGED
@@ -120,9 +120,9 @@ class Agent:
120
120
  cron_manager : CronScheduler instance (enables cron_add/remove/list tools)
121
121
  """
122
122
 
123
- MAX_TOOL_ROUNDS = 8
123
+ MAX_TOOL_ROUNDS = 12
124
124
  MAX_PARALLEL_SKILLS = 5
125
- TOOL_TIMEOUT = 90
125
+ TOOL_TIMEOUT = 300
126
126
 
127
127
  def __init__(
128
128
  self,
@@ -29,7 +29,7 @@ class AnthropicProvider(LLMProvider):
29
29
  def __init__(self, api_key: str, model_name: str = "claude-sonnet-4-20250514"):
30
30
  self.client = anthropic.Anthropic(
31
31
  api_key=api_key,
32
- timeout=120.0,
32
+ timeout=300.0,
33
33
  )
34
34
  self.model_name = model_name
35
35
  self._auth_type = (
@@ -104,7 +104,7 @@ class GeminiProvider(LLMProvider):
104
104
  response = self.model.generate_content(
105
105
  contents=gemini_history,
106
106
  tools=gemini_tools,
107
- request_options={"timeout": 120},
107
+ request_options={"timeout": 300},
108
108
  )
109
109
 
110
110
  # Convert to OpenAI-compatible format
@@ -22,7 +22,7 @@ class OpenAICompatibleProvider(LLMProvider):
22
22
  self.client = OpenAI(
23
23
  api_key=api_key,
24
24
  base_url=base_url,
25
- timeout=120.0,
25
+ timeout=300.0,
26
26
  )
27
27
  self.model_name = model_name
28
28
 
@@ -15,10 +15,14 @@ Available ClawHub endpoints
15
15
  GET /api/certified — security-verified skills
16
16
  GET /api/stats — platform statistics
17
17
  GET /api/health — API status
18
+
19
+ Skill download (full ZIP with SKILL.md + assets):
20
+ GET https://wry-manatee-359.convex.site/api/v1/download?slug=SLUG
18
21
  """
19
22
 
20
23
  from __future__ import annotations
21
24
 
25
+ import io
22
26
  import json
23
27
  import logging
24
28
  import os
@@ -26,12 +30,14 @@ import re
26
30
  import ssl
27
31
  import urllib.error
28
32
  import urllib.request
33
+ import zipfile
29
34
  from typing import Any
30
35
 
31
36
  logger = logging.getLogger(__name__)
32
37
 
33
38
  CLAWHUB_API = "https://topclawhubskills.com/api"
34
39
  CLAWHUB_WEB = "https://clawhub.com"
40
+ CLAWHUB_DOWNLOAD = "https://wry-manatee-359.convex.site/api/v1/download"
35
41
 
36
42
 
37
43
  def _get_ssl_ctx() -> ssl.SSLContext:
@@ -128,26 +134,16 @@ def browse(
128
134
 
129
135
 
130
136
  def get_skill_detail(skill_id: str) -> dict | None:
131
- """Fetch detail for a skill.
132
-
133
- ClawHub search results already contain summary info. For full
134
- instructions, the skill must be installed (``clawhub install``).
135
- We return whatever metadata we have from the listing.
136
- """
137
+ """Fetch metadata for a skill from ClawHub search API."""
137
138
  try:
138
139
  result = _api_get("/search", params={"q": skill_id})
139
140
  data = result.get("data", [])
140
141
  for s in data:
141
142
  if s.get("slug") == skill_id:
142
- normalized = _normalize([s])[0]
143
- normalized["skill_md"] = _build_skill_md(s)
144
- return normalized
143
+ return _normalize([s])[0]
145
144
 
146
145
  if data:
147
- s = data[0]
148
- normalized = _normalize([s])[0]
149
- normalized["skill_md"] = _build_skill_md(s)
150
- return normalized
146
+ return _normalize([data[0]])[0]
151
147
  except Exception as exc:
152
148
  logger.warning("ClawHub detail fetch failed for '%s': %s", skill_id, exc)
153
149
 
@@ -180,41 +176,19 @@ def verify_api() -> dict:
180
176
 
181
177
  # ── Install ───────────────────────────────────────────────────────────────────
182
178
 
183
- def _build_skill_md(skill: dict) -> str:
184
- """Build a SKILL.md from ClawHub metadata."""
185
- name = skill.get("display_name", skill.get("slug", "unknown"))
186
- slug = skill.get("slug", "")
187
- summary = skill.get("summary", "No description.")
188
- author = skill.get("owner_handle", "")
189
- safe_name = re.sub(r"[^a-zA-Z0-9_]", "_", name.lower()).strip("_")
190
-
191
- lines = [
192
- "---",
193
- f"name: {safe_name}",
194
- "description: >",
195
- f" {summary}",
196
- "---",
197
- "",
198
- f"# {name}",
199
- "",
200
- ]
201
- if author:
202
- lines.append(f"*By @{author} on ClawHub*")
203
- lines.append("")
204
- lines.append(f"Source: {CLAWHUB_WEB}/skills/{slug}")
205
- lines.append("")
206
- lines.append("## Instructions")
207
- lines.append("")
208
- lines.append(f"This skill was imported from ClawHub (`{slug}`).")
209
- lines.append("Refer to the source page for full documentation and usage instructions.")
210
- lines.append("")
211
- if summary:
212
- lines.append("## Description")
213
- lines.append("")
214
- lines.append(summary)
215
- lines.append("")
216
-
217
- return "\n".join(lines)
179
+ def _download_skill_zip(slug: str) -> bytes:
180
+ """Download the full skill ZIP from ClawHub's Convex CDN."""
181
+ url = f"{CLAWHUB_DOWNLOAD}?slug={urllib.request.quote(slug)}"
182
+ req = urllib.request.Request(
183
+ url, headers={"User-Agent": "PythonClaw/1.0"},
184
+ )
185
+ try:
186
+ with urllib.request.urlopen(req, timeout=30, context=_get_ssl_ctx()) as resp:
187
+ return resp.read()
188
+ except Exception as exc:
189
+ raise RuntimeError(
190
+ f"Failed to download skill '{slug}' from ClawHub: {exc}"
191
+ ) from exc
218
192
 
219
193
 
220
194
  def install_skill(
@@ -225,24 +199,16 @@ def install_skill(
225
199
  ) -> str:
226
200
  """Download and install a skill from ClawHub into the local skills directory.
227
201
 
202
+ Downloads the full ZIP archive from ClawHub (contains SKILL.md plus
203
+ any assets, scripts, references, etc.) and extracts it.
204
+
228
205
  Returns the path to the installed skill directory.
229
206
  """
230
207
  if target_dir is None:
231
208
  from .. import config as _cfg
232
209
  target_dir = os.path.join(str(_cfg.PYTHONCLAW_HOME), "context", "skills")
233
210
 
234
- detail = None
235
- if not skill_md_override:
236
- detail = get_skill_detail(skill_id)
237
- if not detail:
238
- raise RuntimeError(f"Could not fetch skill '{skill_id}' from ClawHub.")
239
-
240
- skill_md = skill_md_override or detail.get("skill_md", "")
241
- if not skill_md:
242
- raise RuntimeError(f"No SKILL.md content found for '{skill_id}'.")
243
-
244
- skill_name = _derive_skill_name(skill_id, skill_md, detail)
245
- safe_name = re.sub(r"[^a-zA-Z0-9_-]", "_", skill_name).strip("_")
211
+ safe_name = re.sub(r"[^a-zA-Z0-9_-]", "_", skill_id).strip("_")
246
212
  if not safe_name:
247
213
  safe_name = "imported_skill"
248
214
 
@@ -250,33 +216,47 @@ def install_skill(
250
216
  skill_dir = os.path.join(target_dir, category, safe_name)
251
217
  os.makedirs(skill_dir, exist_ok=True)
252
218
 
253
- md_path = os.path.join(skill_dir, "SKILL.md")
254
- if not skill_md.startswith("---"):
255
- skill_md = f"---\nname: {safe_name}\ndescription: Imported from ClawHub ({skill_id})\n---\n\n{skill_md}"
256
-
257
- with open(md_path, "w", encoding="utf-8") as f:
258
- f.write(skill_md + "\n")
259
-
260
- fallback_url = f"{CLAWHUB_WEB}/skills/{skill_id}"
261
- source_url = detail.get("source_url", fallback_url) if detail else fallback_url
219
+ if skill_md_override:
220
+ md_path = os.path.join(skill_dir, "SKILL.md")
221
+ md = skill_md_override
222
+ if not md.startswith("---"):
223
+ md = f"---\nname: {safe_name}\ndescription: Imported from ClawHub ({skill_id})\n---\n\n{md}"
224
+ with open(md_path, "w", encoding="utf-8") as f:
225
+ f.write(md + "\n")
226
+ else:
227
+ raw_zip = _download_skill_zip(skill_id)
228
+ try:
229
+ zf = zipfile.ZipFile(io.BytesIO(raw_zip))
230
+ except zipfile.BadZipFile as exc:
231
+ raise RuntimeError(
232
+ f"ClawHub returned invalid ZIP for '{skill_id}'."
233
+ ) from exc
234
+
235
+ for member in zf.namelist():
236
+ if member.startswith("__MACOSX") or member.startswith("."):
237
+ continue
238
+ dest = os.path.join(skill_dir, member)
239
+ if member.endswith("/"):
240
+ os.makedirs(dest, exist_ok=True)
241
+ else:
242
+ os.makedirs(os.path.dirname(dest), exist_ok=True)
243
+ with open(dest, "wb") as f:
244
+ f.write(zf.read(member))
245
+
246
+ if not os.path.exists(os.path.join(skill_dir, "SKILL.md")):
247
+ logger.warning("No SKILL.md found in ZIP for '%s'", skill_id)
248
+
249
+ source_url = f"{CLAWHUB_WEB}/skills/{skill_id}"
262
250
  meta_path = os.path.join(skill_dir, ".clawhub.json")
263
251
  with open(meta_path, "w", encoding="utf-8") as f:
264
- json.dump({"id": skill_id, "source": source_url, "installed_by": "pythonclaw"}, f, indent=2)
252
+ json.dump(
253
+ {"id": skill_id, "source": source_url, "installed_by": "pythonclaw"},
254
+ f, indent=2,
255
+ )
265
256
 
266
257
  return skill_dir
267
258
 
268
259
 
269
- def _derive_skill_name(skill_id: str, skill_md: str, detail: dict | None) -> str:
270
- """Extract a reasonable skill name from available data."""
271
- name_match = re.search(r"^name:\s*(.+)$", skill_md, re.MULTILINE)
272
- if name_match:
273
- return name_match.group(1).strip()
274
- if detail and detail.get("name"):
275
- return detail["name"]
276
- parts = skill_id.rsplit("-", 1)
277
- return parts[-1] if parts else skill_id
278
-
279
-
280
260
  def format_search_results(results: list[dict]) -> str:
281
261
  """Format search results for CLI display."""
282
262
  if not results:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonclaw
3
- Version: 0.6.3
3
+ Version: 0.6.5
4
4
  Summary: OpenClaw reimagined in pure Python — autonomous AI agent with memory, RAG, skills, web dashboard, and multi-channel support.
5
5
  Author-email: Eric Wang <wangchen2007915@gmail.com>
6
6
  License: MIT
@@ -8,22 +8,22 @@ pythonclaw/onboard.py,sha256=X6ViAduToi9P9J_WdWm9mKQtTb48IwGj5DiqS4jjt0Y,13921
8
8
  pythonclaw/server.py,sha256=zUV09uNTmzK597swGwt45gdmVxuHPsF7ogVV0DHBIhA,4521
9
9
  pythonclaw/session_manager.py,sha256=LKRolNa2i3evb6Ps1zRbajlk4AfvujbR1iPlhfAMBj8,5981
10
10
  pythonclaw/channels/discord_bot.py,sha256=95IJcBJlcnOSbsg0LILq6uBYfhdpb-iLLlRw_41Xz7U,11744
11
- pythonclaw/channels/telegram_bot.py,sha256=WGZFgDcHIOLKNSphMT79ZrpC_6XbsZ-WU8t9Yto8LsQ,21944
11
+ pythonclaw/channels/telegram_bot.py,sha256=QCwGZlTm6bZIr-E557EEwQktFXvmwvJ4DJJviXgfG00,19610
12
12
  pythonclaw/channels/whatsapp_bot.py,sha256=60n6W3ONEIKAdpmI6gCS9RWf5KkLtMUU4J5NJH8vQEY,10650
13
13
  pythonclaw/core/__init__.py,sha256=G5LCqUcCIcYYbMv6SreqS-kj8T9n-IvBAhHEG7wDF5w,661
14
- pythonclaw/core/agent.py,sha256=75cLwIF2HH9-IH9YpHoujyPGg9Sapg6vCTMMfHRY1vk,50158
14
+ pythonclaw/core/agent.py,sha256=kNXHAZRy0NcL3lktI4E3P7dz_d2ubTrp3bE7HaUYFGk,50160
15
15
  pythonclaw/core/compaction.py,sha256=b3zrqwBhPmsmQdfRjrvKK6j0hcfNLpiRrZ8qFxY1h1U,8966
16
16
  pythonclaw/core/persistent_agent.py,sha256=nnY1vaZFsn0Wd_MQ27wbG7sRjO1v2ZxbwXjnJGKsBZc,4932
17
17
  pythonclaw/core/session_store.py,sha256=V44wMBbMpbDCh1aiCa5lb0_iXrKA_7VrR1rOHGGZYhs,9458
18
18
  pythonclaw/core/skill_loader.py,sha256=CE4ffrgkTxSMILESQTtFa3vo59U9oi3olJqW8FtCCUA,14582
19
- pythonclaw/core/skillhub.py,sha256=3MGJ81DFcgcVDCD_Wvf5vHJhcDLZf6IKOhzGzaHkPPQ,10341
19
+ pythonclaw/core/skillhub.py,sha256=WdEpDJCIjYMYIqCPPvwy_J9IEZlDmHES-ix_i63UgoA,9765
20
20
  pythonclaw/core/tools.py,sha256=e3ZZnZ5uZt1bj30IBMJP9ZAwhXUEs-3F_q1tmE2Uk90,23205
21
21
  pythonclaw/core/utils.py,sha256=Ih_ZYnulGlxctdyVy4oKknjvkwFS6ZHcdrznIFIAwxo,1919
22
22
  pythonclaw/core/knowledge/rag.py,sha256=_6GKs8ZFirMQhOeT-CAJBkwLcPkEz7Og-gWKMfUezDw,2895
23
- pythonclaw/core/llm/anthropic_client.py,sha256=w6mXlpxvdmulZMD8_ocC8cufDON-RjLIvslWiPb-GnM,11797
23
+ pythonclaw/core/llm/anthropic_client.py,sha256=MrzXK79V5brWnfUdl8HPD3sEZ__8iRgILATb4tSRWes,11797
24
24
  pythonclaw/core/llm/base.py,sha256=y1muHBuK14rvzWlXmoSf6ahz6Xi0BojpnDUTRhaD3pI,1683
25
- pythonclaw/core/llm/gemini_client.py,sha256=Oarzq12YnuMdivAYLLsVmMUTvNCmXXxHfitbUAebYu8,7218
26
- pythonclaw/core/llm/openai_compatible.py,sha256=4FK1OB93tB_ARs_0vdvBADtP3cuNReixNWf6PUkfCys,3567
25
+ pythonclaw/core/llm/gemini_client.py,sha256=pCFcxBov7Kp9uVZ_TEgo7MRFsqxyd71lX2A9NqAwGSs,7218
26
+ pythonclaw/core/llm/openai_compatible.py,sha256=PeqIaZNyCKEyqTnHpmzmIn7djoGN2HJUU4E4uRP47Ts,3567
27
27
  pythonclaw/core/llm/response.py,sha256=hNCsi0aV1ffXsFuDNnBpRp96cFtVDfX_XEC34QZoykc,1223
28
28
  pythonclaw/core/memory/manager.py,sha256=JzNT6CGVRmmIqbOflRzF7HxSfPfI5jLu8tmF6-91ZVA,8945
29
29
  pythonclaw/core/memory/storage.py,sha256=mHDN8yCVUZ5srOwYWDNjUhbELXka-X8zSexFWEBUB1M,9119
@@ -112,9 +112,9 @@ pythonclaw/web/app.py,sha256=uudrxieo5oGwhQUBLzkmn6GU6SnR4VKlRYOg1bFAYQg,32208
112
112
  pythonclaw/web/static/favicon.png,sha256=zJA13uE8mSe6lOlR5NyAhiOmnZkfv7ZlBbSBNCH7iTM,2557
113
113
  pythonclaw/web/static/index.html,sha256=wU4Lw0NcenS0i0HsJS6a6ceFefDxpRsUQnCXVUXYWVU,93734
114
114
  pythonclaw/web/static/logo.png,sha256=h7v0HHllD23FtmCL2UvjjTDt0UgqKjGy5jOhI3rb7lM,28359
115
- pythonclaw-0.6.3.dist-info/licenses/LICENSE,sha256=wbYsm5Ofe8cnxHgWSnSG1vUJDNiY1DIeTyxHSbo1HqM,1066
116
- pythonclaw-0.6.3.dist-info/METADATA,sha256=7uJE3m-kzB0X-DKqlHM5D2GLGfSY7yFrmx-6Xb3NiJc,14919
117
- pythonclaw-0.6.3.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
118
- pythonclaw-0.6.3.dist-info/entry_points.txt,sha256=4uGCuBw-id_22IRdkoxPVOOcwgiPX5lNEpD1XKQWE4I,52
119
- pythonclaw-0.6.3.dist-info/top_level.txt,sha256=S_lM2VH3gP3UeZbSWHXIrBOCNtoqn5pk491IAzgsV7M,11
120
- pythonclaw-0.6.3.dist-info/RECORD,,
115
+ pythonclaw-0.6.5.dist-info/licenses/LICENSE,sha256=wbYsm5Ofe8cnxHgWSnSG1vUJDNiY1DIeTyxHSbo1HqM,1066
116
+ pythonclaw-0.6.5.dist-info/METADATA,sha256=yPGiMt8r11nq1IuBY-MPNIbF2Vbx6lFc2A1rrFWBVF4,14919
117
+ pythonclaw-0.6.5.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
118
+ pythonclaw-0.6.5.dist-info/entry_points.txt,sha256=4uGCuBw-id_22IRdkoxPVOOcwgiPX5lNEpD1XKQWE4I,52
119
+ pythonclaw-0.6.5.dist-info/top_level.txt,sha256=S_lM2VH3gP3UeZbSWHXIrBOCNtoqn5pk491IAzgsV7M,11
120
+ pythonclaw-0.6.5.dist-info/RECORD,,