pythonclaw 0.6.0__py3-none-any.whl → 0.6.1__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.
@@ -32,6 +32,9 @@ from __future__ import annotations
32
32
  import asyncio
33
33
  import base64
34
34
  import logging
35
+ import queue as _queue
36
+ import re
37
+ import time
35
38
  from typing import TYPE_CHECKING
36
39
 
37
40
  from telegram import BotCommand, ReactionTypeEmoji, Update
@@ -212,23 +215,27 @@ class TelegramBot:
212
215
  except Exception:
213
216
  pass
214
217
 
215
- # Build multimodal input if photo is present
216
218
  chat_input = user_text or ""
217
219
  if has_photo:
218
220
  chat_input = await self._build_image_input(
219
221
  update, user_text or "What's in this image?"
220
222
  )
221
223
 
224
+ token_queue: _queue.Queue[str] = _queue.Queue()
225
+
222
226
  typing_task = asyncio.create_task(
223
227
  self._keep_typing(update.message.chat_id)
224
228
  )
225
229
  try:
226
230
  async with self._sm.acquire(sid):
227
231
  loop = asyncio.get_event_loop()
228
- response = await loop.run_in_executor(None, agent.chat, chat_input)
232
+ future = loop.run_in_executor(
233
+ None, agent.chat_stream, chat_input, token_queue.put,
234
+ )
235
+ await self._flush_stream(update, token_queue, future)
229
236
  except Exception as exc:
230
- logger.exception("[Telegram] Agent.chat() raised an exception")
231
- response = f"Sorry, something went wrong: {exc}"
237
+ logger.exception("[Telegram] Agent error")
238
+ await update.message.reply_text(f"Sorry, something went wrong: {exc}")
232
239
  finally:
233
240
  typing_task.cancel()
234
241
 
@@ -237,8 +244,164 @@ class TelegramBot:
237
244
  except Exception:
238
245
  pass
239
246
 
240
- for chunk in _split_message(response or "(no response)"):
241
- await update.message.reply_text(chunk)
247
+ # Max wall-clock time for a single agent invocation (seconds).
248
+ _AGENT_TIMEOUT = 180
249
+
250
+ async def _flush_stream(
251
+ self,
252
+ update: Update,
253
+ token_queue: "_queue.Queue[str]",
254
+ future: "asyncio.Future[str]",
255
+ ) -> None:
256
+ """Progressively stream tokens to Telegram via edit-in-place.
257
+
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.
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.
267
+ """
268
+ buf: list[str] = []
269
+ live_msg = None
270
+ live_text = ""
271
+ sent_any = False
272
+ THROTTLE = 1.5
273
+ HEARTBEAT_INTERVAL = 15.0
274
+ last_edit = time.monotonic()
275
+ last_token_time = time.monotonic()
276
+ start_time = time.monotonic()
277
+ heartbeat_sent = False
278
+ _MARKER = re.compile(r'`\[calling:\s*([^\]]+)\]`')
279
+
280
+ while not future.done():
281
+ # ── Overall timeout guard ─────────────────────────────────
282
+ if (time.monotonic() - start_time) > self._AGENT_TIMEOUT:
283
+ logger.warning(
284
+ "[Telegram] Agent timeout after %ds", self._AGENT_TIMEOUT,
285
+ )
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
294
+
295
+ # ── Drain token queue ─────────────────────────────────────
296
+ drained = False
297
+ while True:
298
+ try:
299
+ buf.append(token_queue.get_nowait())
300
+ drained = True
301
+ last_token_time = time.monotonic()
302
+ heartbeat_sent = False
303
+ except _queue.Empty:
304
+ break
305
+
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
+ if not drained:
321
+ await asyncio.sleep(0.3)
322
+ continue
323
+
324
+ raw = "".join(buf)
325
+ now = time.monotonic()
326
+
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
+ except Exception:
338
+ pass
339
+ live_msg = None
340
+ live_text = ""
341
+ tools = marker.group(1)
342
+ try:
343
+ await update.message.reply_text(
344
+ f"\U0001f527 {tools}\u2026"
345
+ )
346
+ except Exception:
347
+ pass
348
+ sent_any = True
349
+ buf = [raw[marker.end():].lstrip()]
350
+ last_edit = now
351
+ continue
352
+
353
+ # ── Regular text → edit-in-place ──────────────────────────
354
+ text = _clean_response(raw)
355
+ if text and text != live_text and (now - last_edit) >= THROTTLE:
356
+ try:
357
+ if live_msg is None:
358
+ live_msg = await update.message.reply_text(
359
+ text[:4096],
360
+ )
361
+ live_text = text[:4096]
362
+ elif len(text) <= 4096:
363
+ await live_msg.edit_text(text)
364
+ live_text = text
365
+ else:
366
+ await live_msg.edit_text(text[:4096])
367
+ live_msg = None
368
+ live_text = ""
369
+ buf = [text[4096:]]
370
+ sent_any = True
371
+ except Exception:
372
+ pass
373
+ last_edit = now
374
+
375
+ await asyncio.sleep(0.3)
376
+
377
+ # ── Final drain ───────────────────────────────────────────────
378
+ response = future.result()
379
+ while True:
380
+ try:
381
+ buf.append(token_queue.get_nowait())
382
+ except _queue.Empty:
383
+ break
384
+
385
+ remaining = _clean_response("".join(buf).strip())
386
+ if remaining and remaining != live_text:
387
+ try:
388
+ if live_msg and len(remaining) <= 4096:
389
+ await live_msg.edit_text(remaining)
390
+ elif live_msg:
391
+ await live_msg.edit_text(remaining[:4096])
392
+ for chunk in _split_message(remaining[4096:]):
393
+ await update.message.reply_text(chunk)
394
+ else:
395
+ for chunk in _split_message(remaining):
396
+ await update.message.reply_text(chunk)
397
+ sent_any = True
398
+ except Exception:
399
+ pass
400
+
401
+ if not sent_any:
402
+ text = _clean_response(response or "(no response)")
403
+ for chunk in _split_message(text):
404
+ await update.message.reply_text(chunk)
242
405
 
243
406
  async def _build_image_input(self, update: Update, caption: str) -> list:
244
407
  """Download photo and build a multimodal content array."""
@@ -330,14 +493,44 @@ class TelegramBot:
330
493
 
331
494
  # ── Utility ───────────────────────────────────────────────────────────────────
332
495
 
496
+ _LEAKED_TOOL_RE = re.compile(
497
+ r'<\s*\|?\s*(?:DSML|antml)\s*\|\s*function_calls[^>]*>'
498
+ r'[\s\S]*?'
499
+ r'<\s*/\s*\|?\s*(?:DSML|antml)\s*\|\s*function_calls\s*>',
500
+ re.IGNORECASE,
501
+ )
502
+
503
+
504
+ def _clean_response(text: str) -> str:
505
+ """Strip leaked tool-call XML/DSML markup from LLM output."""
506
+ text = _LEAKED_TOOL_RE.sub('', text)
507
+ text = re.sub(r'\n{3,}', '\n\n', text)
508
+ return text.strip()
509
+
510
+
333
511
  def _split_message(text: str, limit: int = 4096) -> list[str]:
334
- """Split a long string into chunks that fit within Telegram's message limit."""
512
+ """Split text into chunks respecting natural boundaries.
513
+
514
+ Tries paragraph breaks first, then newlines, then word boundaries,
515
+ and only falls back to a hard character cut as a last resort.
516
+ """
335
517
  if len(text) <= limit:
336
518
  return [text]
337
- chunks = []
519
+ chunks: list[str] = []
520
+ min_break = limit // 3
338
521
  while text:
339
- chunks.append(text[:limit])
340
- text = text[limit:]
522
+ if len(text) <= limit:
523
+ chunks.append(text)
524
+ break
525
+ split_at = text.rfind('\n\n', min_break, limit)
526
+ if split_at < min_break:
527
+ split_at = text.rfind('\n', min_break, limit)
528
+ if split_at < min_break:
529
+ split_at = text.rfind(' ', min_break, limit)
530
+ if split_at < min_break:
531
+ split_at = limit
532
+ chunks.append(text[:split_at].rstrip())
533
+ text = text[split_at:].lstrip()
341
534
  return chunks
342
535
 
343
536
 
pythonclaw/core/agent.py CHANGED
@@ -24,6 +24,7 @@ import logging
24
24
  import os
25
25
  import time
26
26
  from concurrent.futures import ThreadPoolExecutor, as_completed
27
+ from concurrent.futures import TimeoutError as FuturesTimeout
27
28
  from datetime import datetime
28
29
 
29
30
  from .. import config
@@ -121,6 +122,7 @@ class Agent:
121
122
 
122
123
  MAX_TOOL_ROUNDS = 8
123
124
  MAX_PARALLEL_SKILLS = 5
125
+ TOOL_TIMEOUT = 90
124
126
 
125
127
  def __init__(
126
128
  self,
@@ -333,8 +335,11 @@ Always verify command output.
333
335
 
334
336
  ### Response Guidelines
335
337
  - Answer the user's question directly and concisely.
338
+ - For complex multi-step tasks: share a **brief plan** first (2-4 bullet points), then work step by step. Report progress after each major step — don't wait until the end.
339
+ - Keep responses focused and concise — under 300 words when possible. Break long answers into short paragraphs.
336
340
  - Do NOT mention what skills or tools you have available, unless explicitly asked.
337
341
  - Do NOT list other things you can do at the end of your response.
342
+ - NEVER output tool calls as XML or text. Always use the function calling API.
338
343
  """
339
344
  # ── Auto-inject memory context ────────────────────────────────────
340
345
  boot_mem = self.memory.boot_context(max_chars=3000)
@@ -939,53 +944,53 @@ Don't repeat this if `bot_name` already exists in memory.
939
944
  ],
940
945
  })
941
946
 
942
- if len(tool_calls) == 1:
943
- t0 = time.monotonic()
944
- result = self._execute_tool_call(tool_calls[0])
945
- _log_detail({
946
- "event": "tool_result",
947
- "round": tool_rounds,
948
- "name": tool_calls[0].function.name,
949
- "elapsed_ms": int((time.monotonic() - t0) * 1000),
950
- "result_len": len(result),
951
- })
947
+ t0 = time.monotonic()
948
+ results: dict[str, str] = {}
949
+ with ThreadPoolExecutor(max_workers=min(len(tool_calls), 8)) as pool:
950
+ futures = {
951
+ pool.submit(self._execute_tool_call, tc): tc
952
+ for tc in tool_calls
953
+ }
954
+ for future in as_completed(futures, timeout=self.TOOL_TIMEOUT):
955
+ tc = futures[future]
956
+ try:
957
+ results[tc.id] = future.result()
958
+ except Exception as exc:
959
+ results[tc.id] = f"Error: {exc}"
960
+ for tc in tool_calls:
961
+ if tc.id not in results:
962
+ results[tc.id] = (
963
+ f"Error: tool '{tc.function.name}' timed out "
964
+ f"after {self.TOOL_TIMEOUT}s"
965
+ )
966
+ _log_detail({
967
+ "event": "tool_results",
968
+ "round": tool_rounds,
969
+ "count": len(tool_calls),
970
+ "elapsed_ms": int((time.monotonic() - t0) * 1000),
971
+ "tools": [tc.function.name for tc in tool_calls],
972
+ })
973
+ for tc in tool_calls:
952
974
  self.messages.append({
953
975
  "role": "tool",
954
- "tool_call_id": tool_calls[0].id,
955
- "content": result,
976
+ "tool_call_id": tc.id,
977
+ "content": results[tc.id],
956
978
  })
957
- else:
958
- t0 = time.monotonic()
959
- results: dict[str, str] = {}
960
- with ThreadPoolExecutor(max_workers=min(len(tool_calls), 8)) as pool:
961
- futures = {
962
- pool.submit(self._execute_tool_call, tc): tc
963
- for tc in tool_calls
964
- }
965
- for future in as_completed(futures):
966
- tc = futures[future]
967
- try:
968
- results[tc.id] = future.result()
969
- except Exception as exc:
970
- results[tc.id] = f"Error: {exc}"
971
- _log_detail({
972
- "event": "tool_results_parallel",
973
- "round": tool_rounds,
974
- "count": len(tool_calls),
975
- "elapsed_ms": int((time.monotonic() - t0) * 1000),
976
- "tools": [tc.function.name for tc in tool_calls],
977
- })
978
- for tc in tool_calls:
979
- self.messages.append({
980
- "role": "tool",
981
- "tool_call_id": tc.id,
982
- "content": results[tc.id],
983
- })
984
979
 
985
980
  for injection in self.pending_injections:
986
981
  self.messages.append({"role": "system", "content": injection})
987
982
  self.pending_injections = []
988
983
 
984
+ except FuturesTimeout:
985
+ logger.warning("Tool execution timed out at round %d", tool_rounds)
986
+ for tc in tool_calls:
987
+ if tc.id not in results:
988
+ self.messages.append({
989
+ "role": "tool",
990
+ "tool_call_id": tc.id,
991
+ "content": f"Error: timed out after {self.TOOL_TIMEOUT}s",
992
+ })
993
+ continue
989
994
  except Exception as exc:
990
995
  logger.exception("Critical error in Agent.chat()")
991
996
  return f"Error: {exc}"
@@ -1087,34 +1092,33 @@ Don't repeat this if `bot_name` already exists in memory.
1087
1092
  names = ", ".join(tc.function.name for tc in tool_calls)
1088
1093
  on_token(f"\n\n`[calling: {names}]`\n\n")
1089
1094
 
1090
- if len(tool_calls) == 1:
1091
- result = self._execute_tool_call(tool_calls[0])
1095
+ results: dict[str, str] = {}
1096
+ with ThreadPoolExecutor(
1097
+ max_workers=min(len(tool_calls), 8)
1098
+ ) as pool:
1099
+ futures = {
1100
+ pool.submit(self._execute_tool_call, tc): tc
1101
+ for tc in tool_calls
1102
+ }
1103
+ for future in as_completed(
1104
+ futures, timeout=self.TOOL_TIMEOUT
1105
+ ):
1106
+ tc = futures[future]
1107
+ try:
1108
+ results[tc.id] = future.result()
1109
+ except Exception as exc:
1110
+ results[tc.id] = f"Error: {exc}"
1111
+ for tc in tool_calls:
1112
+ if tc.id not in results:
1113
+ results[tc.id] = (
1114
+ f"Error: tool '{tc.function.name}' timed out "
1115
+ f"after {self.TOOL_TIMEOUT}s"
1116
+ )
1092
1117
  self.messages.append({
1093
1118
  "role": "tool",
1094
- "tool_call_id": tool_calls[0].id,
1095
- "content": result,
1119
+ "tool_call_id": tc.id,
1120
+ "content": results[tc.id],
1096
1121
  })
1097
- else:
1098
- results: dict[str, str] = {}
1099
- with ThreadPoolExecutor(
1100
- max_workers=min(len(tool_calls), 8)
1101
- ) as pool:
1102
- futures = {
1103
- pool.submit(self._execute_tool_call, tc): tc
1104
- for tc in tool_calls
1105
- }
1106
- for future in as_completed(futures):
1107
- tc = futures[future]
1108
- try:
1109
- results[tc.id] = future.result()
1110
- except Exception as exc:
1111
- results[tc.id] = f"Error: {exc}"
1112
- for tc in tool_calls:
1113
- self.messages.append({
1114
- "role": "tool",
1115
- "tool_call_id": tc.id,
1116
- "content": results[tc.id],
1117
- })
1118
1122
 
1119
1123
  for injection in self.pending_injections:
1120
1124
  self.messages.append(
@@ -1122,6 +1126,16 @@ Don't repeat this if `bot_name` already exists in memory.
1122
1126
  )
1123
1127
  self.pending_injections = []
1124
1128
 
1129
+ except FuturesTimeout:
1130
+ logger.warning("Tool execution timed out in stream round %d", tool_rounds)
1131
+ for tc in tool_calls:
1132
+ if tc.id not in results:
1133
+ self.messages.append({
1134
+ "role": "tool",
1135
+ "tool_call_id": tc.id,
1136
+ "content": f"Error: timed out after {self.TOOL_TIMEOUT}s",
1137
+ })
1138
+ continue
1125
1139
  except Exception as exc:
1126
1140
  logger.exception("Critical error in Agent.chat_stream()")
1127
1141
  return f"Error: {exc}"
@@ -27,7 +27,10 @@ class AnthropicProvider(LLMProvider):
27
27
  supports_images = True
28
28
 
29
29
  def __init__(self, api_key: str, model_name: str = "claude-sonnet-4-20250514"):
30
- self.client = anthropic.Anthropic(api_key=api_key)
30
+ self.client = anthropic.Anthropic(
31
+ api_key=api_key,
32
+ timeout=120.0,
33
+ )
31
34
  self.model_name = model_name
32
35
  self._auth_type = (
33
36
  "setup-token" if not api_key.startswith("sk-ant-") else "api-key"
@@ -104,6 +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
108
  )
108
109
 
109
110
  # Convert to OpenAI-compatible format
@@ -19,7 +19,11 @@ class OpenAICompatibleProvider(LLMProvider):
19
19
  """Thin wrapper around the OpenAI SDK for chat completions."""
20
20
 
21
21
  def __init__(self, api_key: str, base_url: str, model_name: str) -> None:
22
- self.client = OpenAI(api_key=api_key, base_url=base_url)
22
+ self.client = OpenAI(
23
+ api_key=api_key,
24
+ base_url=base_url,
25
+ timeout=120.0,
26
+ )
23
27
  self.model_name = model_name
24
28
 
25
29
  def chat(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonclaw
3
- Version: 0.6.0
3
+ Version: 0.6.1
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,10 +8,10 @@ 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=w-kcb7u-_k9PGySiY8L4vBryLVI6HBTpoRM_j0FxJsY,14835
11
+ pythonclaw/channels/telegram_bot.py,sha256=Yb2kKQy9V1VAJWJzHNvCtGAxircxi4h7fImh7Qg1A3U,22183
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=Iow1S7qL7uJAe3IcdCCFpZoZy5TSJpxF6ccYJSqVtBw,49271
14
+ pythonclaw/core/agent.py,sha256=4YfUQ5VzKdzf6CACoXIYS3XNb39eGeuZtR59j5HtEF8,50078
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
@@ -20,10 +20,10 @@ pythonclaw/core/skillhub.py,sha256=3MGJ81DFcgcVDCD_Wvf5vHJhcDLZf6IKOhzGzaHkPPQ,1
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=9rCok7eW9SbHTUzXT_WJr8sj3bnbrIqT9wo1_KNMAJo,11747
23
+ pythonclaw/core/llm/anthropic_client.py,sha256=w6mXlpxvdmulZMD8_ocC8cufDON-RjLIvslWiPb-GnM,11797
24
24
  pythonclaw/core/llm/base.py,sha256=y1muHBuK14rvzWlXmoSf6ahz6Xi0BojpnDUTRhaD3pI,1683
25
- pythonclaw/core/llm/gemini_client.py,sha256=qAcavB4O7ahpmpv-rSpk4VS0gkvsZQuL6aKMEDVUFZ4,7172
26
- pythonclaw/core/llm/openai_compatible.py,sha256=eZcYEn7UlUn0oPxyGKK4Z37-uZ12sh432ZjwzPtAbDE,3505
25
+ pythonclaw/core/llm/gemini_client.py,sha256=Oarzq12YnuMdivAYLLsVmMUTvNCmXXxHfitbUAebYu8,7218
26
+ pythonclaw/core/llm/openai_compatible.py,sha256=4FK1OB93tB_ARs_0vdvBADtP3cuNReixNWf6PUkfCys,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.0.dist-info/licenses/LICENSE,sha256=wbYsm5Ofe8cnxHgWSnSG1vUJDNiY1DIeTyxHSbo1HqM,1066
116
- pythonclaw-0.6.0.dist-info/METADATA,sha256=bWG1XYP7WsHs8SsCHCOwBXVESI_oEQ9fYRhklSqatZE,14919
117
- pythonclaw-0.6.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
118
- pythonclaw-0.6.0.dist-info/entry_points.txt,sha256=4uGCuBw-id_22IRdkoxPVOOcwgiPX5lNEpD1XKQWE4I,52
119
- pythonclaw-0.6.0.dist-info/top_level.txt,sha256=S_lM2VH3gP3UeZbSWHXIrBOCNtoqn5pk491IAzgsV7M,11
120
- pythonclaw-0.6.0.dist-info/RECORD,,
115
+ pythonclaw-0.6.1.dist-info/licenses/LICENSE,sha256=wbYsm5Ofe8cnxHgWSnSG1vUJDNiY1DIeTyxHSbo1HqM,1066
116
+ pythonclaw-0.6.1.dist-info/METADATA,sha256=n3lgHkJkHYMEk70VecYW9rgsP1gdwJukIdQeRtkooPE,14919
117
+ pythonclaw-0.6.1.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
118
+ pythonclaw-0.6.1.dist-info/entry_points.txt,sha256=4uGCuBw-id_22IRdkoxPVOOcwgiPX5lNEpD1XKQWE4I,52
119
+ pythonclaw-0.6.1.dist-info/top_level.txt,sha256=S_lM2VH3gP3UeZbSWHXIrBOCNtoqn5pk491IAzgsV7M,11
120
+ pythonclaw-0.6.1.dist-info/RECORD,,