pythonclaw 0.5.0__tar.gz → 0.6.0__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.
Files changed (135) hide show
  1. {pythonclaw-0.5.0/pythonclaw.egg-info → pythonclaw-0.6.0}/PKG-INFO +9 -9
  2. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/README.md +8 -8
  3. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pyproject.toml +1 -1
  4. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/channels/discord_bot.py +65 -11
  5. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/channels/telegram_bot.py +97 -22
  6. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/channels/whatsapp_bot.py +60 -8
  7. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/agent.py +299 -80
  8. pythonclaw-0.6.0/pythonclaw/core/llm/anthropic_client.py +294 -0
  9. pythonclaw-0.6.0/pythonclaw/core/llm/base.py +55 -0
  10. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/llm/gemini_client.py +52 -1
  11. pythonclaw-0.6.0/pythonclaw/core/llm/openai_compatible.py +105 -0
  12. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/memory/manager.py +75 -0
  13. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/memory/storage.py +83 -0
  14. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/persistent_agent.py +27 -1
  15. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/session_store.py +1 -1
  16. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/skill_loader.py +13 -13
  17. pythonclaw-0.6.0/pythonclaw/core/skillhub.py +309 -0
  18. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/tools.py +37 -0
  19. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/main.py +15 -10
  20. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/onboard.py +14 -14
  21. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/communication/CATEGORY.md +1 -1
  22. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/communication/slack/SKILL.md +1 -1
  23. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/data/CATEGORY.md +1 -1
  24. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/dev/CATEGORY.md +1 -1
  25. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/google/CATEGORY.md +1 -1
  26. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/media/CATEGORY.md +1 -1
  27. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/media/image_gen/SKILL.md +1 -1
  28. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/media/spotify/SKILL.md +1 -1
  29. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/media/tts/SKILL.md +1 -1
  30. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/meta/CATEGORY.md +1 -1
  31. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/productivity/CATEGORY.md +1 -1
  32. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/productivity/notion/SKILL.md +1 -1
  33. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/productivity/obsidian/SKILL.md +1 -1
  34. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/productivity/trello/SKILL.md +1 -1
  35. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/system/CATEGORY.md +1 -1
  36. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/system/model_usage/SKILL.md +1 -1
  37. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/system/session_logs/SKILL.md +1 -1
  38. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/text/CATEGORY.md +1 -1
  39. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/web/CATEGORY.md +1 -1
  40. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/web/summarize/SKILL.md +1 -1
  41. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/web/app.py +134 -29
  42. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/web/static/index.html +276 -107
  43. {pythonclaw-0.5.0 → pythonclaw-0.6.0/pythonclaw.egg-info}/PKG-INFO +9 -9
  44. pythonclaw-0.5.0/pythonclaw/core/llm/anthropic_client.py +0 -107
  45. pythonclaw-0.5.0/pythonclaw/core/llm/base.py +0 -26
  46. pythonclaw-0.5.0/pythonclaw/core/llm/openai_compatible.py +0 -39
  47. pythonclaw-0.5.0/pythonclaw/core/skillhub.py +0 -319
  48. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/LICENSE +0 -0
  49. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/MANIFEST.in +0 -0
  50. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/__init__.py +0 -0
  51. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/__main__.py +0 -0
  52. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/config.py +0 -0
  53. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/__init__.py +0 -0
  54. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/compaction.py +0 -0
  55. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/knowledge/rag.py +0 -0
  56. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/llm/response.py +0 -0
  57. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/retrieval/__init__.py +0 -0
  58. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/retrieval/chunker.py +0 -0
  59. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/retrieval/dense.py +0 -0
  60. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/retrieval/fusion.py +0 -0
  61. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/retrieval/reranker.py +0 -0
  62. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/retrieval/retriever.py +0 -0
  63. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/retrieval/sparse.py +0 -0
  64. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/core/utils.py +0 -0
  65. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/daemon.py +0 -0
  66. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/init.py +0 -0
  67. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/scheduler/cron.py +0 -0
  68. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/scheduler/heartbeat.py +0 -0
  69. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/server.py +0 -0
  70. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/session_manager.py +0 -0
  71. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/persona/demo_persona.md +0 -0
  72. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/communication/email/SKILL.md +0 -0
  73. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/communication/email/send_email.py +0 -0
  74. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/communication/slack/slack_api.py +0 -0
  75. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/data/csv_analyzer/SKILL.md +0 -0
  76. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/data/csv_analyzer/analyze.py +0 -0
  77. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/data/finance/SKILL.md +0 -0
  78. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/data/finance/fetch_quote.py +0 -0
  79. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/data/news/SKILL.md +0 -0
  80. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/data/news/search_news.py +0 -0
  81. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/data/pdf_reader/SKILL.md +0 -0
  82. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/data/pdf_reader/read_pdf.py +0 -0
  83. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/data/scraper/SKILL.md +0 -0
  84. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/data/scraper/scrape.py +0 -0
  85. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/data/weather/SKILL.md +0 -0
  86. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/data/weather/weather.py +0 -0
  87. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/data/youtube/SKILL.md +0 -0
  88. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/data/youtube/youtube_info.py +0 -0
  89. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/dev/code_runner/SKILL.md +0 -0
  90. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/dev/code_runner/run_code.py +0 -0
  91. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/dev/github/SKILL.md +0 -0
  92. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/dev/github/gh.py +0 -0
  93. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/dev/http_request/SKILL.md +0 -0
  94. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/dev/http_request/request.py +0 -0
  95. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/google/workspace/SKILL.md +0 -0
  96. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/google/workspace/check_setup.sh +0 -0
  97. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/media/image_gen/generate.py +0 -0
  98. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/media/spotify/spotify_ctl.py +0 -0
  99. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/media/tts/speak.py +0 -0
  100. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/meta/skill_creator/SKILL.md +0 -0
  101. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/productivity/notion/notion_api.py +0 -0
  102. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/productivity/obsidian/obsidian_vault.py +0 -0
  103. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/productivity/trello/trello_api.py +0 -0
  104. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/system/change_persona/SKILL.md +0 -0
  105. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/system/change_setting/SKILL.md +0 -0
  106. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/system/change_setting/update_config.py +0 -0
  107. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/system/change_soul/SKILL.md +0 -0
  108. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/system/model_usage/usage_stats.py +0 -0
  109. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/system/onboarding/SKILL.md +0 -0
  110. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/system/onboarding/write_identity.py +0 -0
  111. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/system/random/SKILL.md +0 -0
  112. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/system/random/random_util.py +0 -0
  113. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/system/session_logs/search_sessions.py +0 -0
  114. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/system/time/SKILL.md +0 -0
  115. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/system/time/time_util.py +0 -0
  116. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/text/translator/SKILL.md +0 -0
  117. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/text/translator/translate.py +0 -0
  118. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/web/summarize/summarize_url.py +0 -0
  119. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/skills/web/tavily/SKILL.md +0 -0
  120. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/soul/SOUL.md +0 -0
  121. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/templates/tools/TOOLS.md +0 -0
  122. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/web/__init__.py +0 -0
  123. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/web/static/favicon.png +0 -0
  124. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw/web/static/logo.png +0 -0
  125. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw.egg-info/SOURCES.txt +0 -0
  126. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw.egg-info/dependency_links.txt +0 -0
  127. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw.egg-info/entry_points.txt +0 -0
  128. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw.egg-info/requires.txt +0 -0
  129. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/pythonclaw.egg-info/top_level.txt +0 -0
  130. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/setup.cfg +0 -0
  131. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/tests/test_compaction.py +0 -0
  132. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/tests/test_persistence.py +0 -0
  133. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/tests/test_rag_hybrid.py +0 -0
  134. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/tests/test_skills.py +0 -0
  135. {pythonclaw-0.5.0 → pythonclaw-0.6.0}/tests/test_soul.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonclaw
3
- Version: 0.5.0
3
+ Version: 0.6.0
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
@@ -78,7 +78,7 @@ Dynamic: license-file
78
78
  | | Feature | Details |
79
79
  |---|---------|---------|
80
80
  | 🧠 | **Provider-agnostic** | DeepSeek, Grok, Claude, Gemini, Kimi, GLM — or any OpenAI-compatible API |
81
- | 🛠️ | **Three-tier skills** | Progressive loading: metadata → instructions → resources. Community marketplace via [SkillHub](https://www.skillhub.club) |
81
+ | 🛠️ | **Three-tier skills** | Progressive loading: metadata → instructions → resources. Community marketplace via [ClawHub](https://clawhub.com) (13K+ free skills) |
82
82
  | 💾 | **Persistent memory** | Markdown-based long-term memory with daily logs and semantic recall |
83
83
  | 🔍 | **Hybrid RAG** | BM25 + dense embeddings + RRF fusion + LLM re-ranking |
84
84
  | 🌐 | **Web dashboard** | Browser UI for chat, config, skill catalog, identity editing, and marketplace |
@@ -133,7 +133,7 @@ pythonclaw onboard
133
133
  | `pythonclaw stop` | Stop the running daemon |
134
134
  | `pythonclaw status` | Show daemon status (PID, uptime, port) |
135
135
  | `pythonclaw chat` | Interactive CLI chat (foreground REPL) |
136
- | `pythonclaw skill search <query>` | Search skills on [SkillHub](https://www.skillhub.club) |
136
+ | `pythonclaw skill search <query>` | Search skills on [ClawHub](https://clawhub.com) |
137
137
  | `pythonclaw skill browse` | Browse top-rated skills |
138
138
  | `pythonclaw skill install <id>` | Install a community skill |
139
139
  | `pythonclaw skill info <id>` | View skill details |
@@ -191,7 +191,7 @@ $ pythonclaw start
191
191
  │ LLM Provider Abstraction Layer │
192
192
  │ DeepSeek │ Grok │ Claude │ Gemini │ Kimi │ GLM │
193
193
  ├──────────────────────────────────────────────────────────────┤
194
- SkillHub Marketplace (skillhub.club)
194
+ ClawHub Marketplace (clawhub.com)
195
195
  └──────────────────────────────────────────────────────────────┘
196
196
  ```
197
197
 
@@ -204,7 +204,7 @@ Start with `pythonclaw start` and open **http://localhost:7788**.
204
204
  - **Dashboard** — agent status, soul/persona preview, tool list
205
205
  - **Chat** — real-time chat with voice input (Deepgram)
206
206
  - **Skill Catalog** — browse installed skills by category
207
- - **Marketplace** — search and install skills from [SkillHub](https://www.skillhub.club)
207
+ - **Marketplace** — search and install skills from [ClawHub](https://clawhub.com)
208
208
  - **Configuration** — edit LLM provider, API keys, and settings in-browser
209
209
 
210
210
  ---
@@ -272,9 +272,9 @@ description: Execute Python code safely in an isolated subprocess.
272
272
  Run `python {skill_path}/run_code.py "expression"`
273
273
  ```
274
274
 
275
- ### SkillHub Marketplace
275
+ ### ClawHub Marketplace
276
276
 
277
- Browse and install 22,000+ community skills from [skillhub.club](https://www.skillhub.club):
277
+ Browse and install 13,000+ community skills from [ClawHub](https://clawhub.com) — free, no API key required:
278
278
 
279
279
  ```bash
280
280
  pythonclaw skill search "database backup"
@@ -351,7 +351,7 @@ PythonClaw/
351
351
  │ │ ├── agent.py # Core reasoning loop
352
352
  │ │ ├── tools.py # Tool schemas and execution
353
353
  │ │ ├── skill_loader.py # Three-tier skill system
354
- │ │ ├── skillhub.py # SkillHub marketplace client
354
+ │ │ ├── skillhub.py # ClawHub marketplace client
355
355
  │ │ ├── persistent_agent.py
356
356
  │ │ ├── compaction.py # Context compaction
357
357
  │ │ ├── llm/ # Provider adapters
@@ -397,7 +397,7 @@ We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
397
397
  | CLI | `openclaw start/stop` | `pythonclaw start/stop/status` |
398
398
  | Dashboard | Web UI | Web UI (localhost:7788) |
399
399
  | Memory | Markdown | Markdown (long-term + daily) |
400
- | Skills | Plugin system | Three-tier + SkillHub marketplace |
400
+ | Skills | Plugin system | Three-tier + ClawHub marketplace |
401
401
  | Channels | Discord, Telegram, WhatsApp | CLI, Web, Telegram, Discord, WhatsApp |
402
402
  | Voice | — | Deepgram STT |
403
403
  | LLM Providers | OpenAI, Anthropic, Gemini | DeepSeek, Grok, Claude, Gemini, Kimi, GLM |
@@ -36,7 +36,7 @@
36
36
  | | Feature | Details |
37
37
  |---|---------|---------|
38
38
  | 🧠 | **Provider-agnostic** | DeepSeek, Grok, Claude, Gemini, Kimi, GLM — or any OpenAI-compatible API |
39
- | 🛠️ | **Three-tier skills** | Progressive loading: metadata → instructions → resources. Community marketplace via [SkillHub](https://www.skillhub.club) |
39
+ | 🛠️ | **Three-tier skills** | Progressive loading: metadata → instructions → resources. Community marketplace via [ClawHub](https://clawhub.com) (13K+ free skills) |
40
40
  | 💾 | **Persistent memory** | Markdown-based long-term memory with daily logs and semantic recall |
41
41
  | 🔍 | **Hybrid RAG** | BM25 + dense embeddings + RRF fusion + LLM re-ranking |
42
42
  | 🌐 | **Web dashboard** | Browser UI for chat, config, skill catalog, identity editing, and marketplace |
@@ -91,7 +91,7 @@ pythonclaw onboard
91
91
  | `pythonclaw stop` | Stop the running daemon |
92
92
  | `pythonclaw status` | Show daemon status (PID, uptime, port) |
93
93
  | `pythonclaw chat` | Interactive CLI chat (foreground REPL) |
94
- | `pythonclaw skill search <query>` | Search skills on [SkillHub](https://www.skillhub.club) |
94
+ | `pythonclaw skill search <query>` | Search skills on [ClawHub](https://clawhub.com) |
95
95
  | `pythonclaw skill browse` | Browse top-rated skills |
96
96
  | `pythonclaw skill install <id>` | Install a community skill |
97
97
  | `pythonclaw skill info <id>` | View skill details |
@@ -149,7 +149,7 @@ $ pythonclaw start
149
149
  │ LLM Provider Abstraction Layer │
150
150
  │ DeepSeek │ Grok │ Claude │ Gemini │ Kimi │ GLM │
151
151
  ├──────────────────────────────────────────────────────────────┤
152
- SkillHub Marketplace (skillhub.club)
152
+ ClawHub Marketplace (clawhub.com)
153
153
  └──────────────────────────────────────────────────────────────┘
154
154
  ```
155
155
 
@@ -162,7 +162,7 @@ Start with `pythonclaw start` and open **http://localhost:7788**.
162
162
  - **Dashboard** — agent status, soul/persona preview, tool list
163
163
  - **Chat** — real-time chat with voice input (Deepgram)
164
164
  - **Skill Catalog** — browse installed skills by category
165
- - **Marketplace** — search and install skills from [SkillHub](https://www.skillhub.club)
165
+ - **Marketplace** — search and install skills from [ClawHub](https://clawhub.com)
166
166
  - **Configuration** — edit LLM provider, API keys, and settings in-browser
167
167
 
168
168
  ---
@@ -230,9 +230,9 @@ description: Execute Python code safely in an isolated subprocess.
230
230
  Run `python {skill_path}/run_code.py "expression"`
231
231
  ```
232
232
 
233
- ### SkillHub Marketplace
233
+ ### ClawHub Marketplace
234
234
 
235
- Browse and install 22,000+ community skills from [skillhub.club](https://www.skillhub.club):
235
+ Browse and install 13,000+ community skills from [ClawHub](https://clawhub.com) — free, no API key required:
236
236
 
237
237
  ```bash
238
238
  pythonclaw skill search "database backup"
@@ -309,7 +309,7 @@ PythonClaw/
309
309
  │ │ ├── agent.py # Core reasoning loop
310
310
  │ │ ├── tools.py # Tool schemas and execution
311
311
  │ │ ├── skill_loader.py # Three-tier skill system
312
- │ │ ├── skillhub.py # SkillHub marketplace client
312
+ │ │ ├── skillhub.py # ClawHub marketplace client
313
313
  │ │ ├── persistent_agent.py
314
314
  │ │ ├── compaction.py # Context compaction
315
315
  │ │ ├── llm/ # Provider adapters
@@ -355,7 +355,7 @@ We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
355
355
  | CLI | `openclaw start/stop` | `pythonclaw start/stop/status` |
356
356
  | Dashboard | Web UI | Web UI (localhost:7788) |
357
357
  | Memory | Markdown | Markdown (long-term + daily) |
358
- | Skills | Plugin system | Three-tier + SkillHub marketplace |
358
+ | Skills | Plugin system | Three-tier + ClawHub marketplace |
359
359
  | Channels | Discord, Telegram, WhatsApp | CLI, Web, Telegram, Discord, WhatsApp |
360
360
  | Voice | — | Deepgram STT |
361
361
  | LLM Providers | OpenAI, Anthropic, Gemini | DeepSeek, Grok, Claude, Gemini, Kimi, GLM |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pythonclaw"
7
- version = "0.5.0"
7
+ version = "0.6.0"
8
8
  description = "OpenClaw reimagined in pure Python — autonomous AI agent with memory, RAG, skills, web dashboard, and multi-channel support."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Discord channel for PythonClaw.
3
3
 
4
- Session IDs: "discord:{user_id}" (DMs) or "discord:{channel_id}" (guilds)
4
+ Session IDs: "discord:dm:{user_id}" (DMs) or "discord:{channel_id}" (guilds)
5
5
 
6
6
  Commands
7
7
  --------
@@ -9,21 +9,29 @@ Commands
9
9
  !status — show session info
10
10
  !compact [hint] — compact conversation history
11
11
  <text> — forwarded to Agent.chat(), reply sent back
12
+ <image> — image attachments sent to LLM for analysis
12
13
 
13
14
  The bot responds to:
14
15
  - Direct messages (always)
15
- - Channel mentions (@bot message) in guilds
16
- - Optionally all messages in whitelisted channels
16
+ - Channel mentions (@bot message) in guilds (when requireMention=true)
17
+ - All messages in whitelisted channels (when requireMention=false)
17
18
 
18
19
  Access control
19
20
  --------------
20
21
  Set DISCORD_ALLOWED_USERS to a comma-separated list of Discord user IDs.
21
22
  Set DISCORD_ALLOWED_CHANNELS to restrict which guild channels the bot listens in.
22
23
  Leave empty to allow all.
24
+
25
+ Group behaviour
26
+ ---------------
27
+ Set ``channels.discord.requireMention`` to ``true`` to require @bot mention
28
+ in guild channels. Default is ``false`` (respond when mentioned OR in
29
+ whitelisted channels).
23
30
  """
24
31
 
25
32
  from __future__ import annotations
26
33
 
34
+ import base64
27
35
  import logging
28
36
  from typing import TYPE_CHECKING
29
37
 
@@ -52,11 +60,13 @@ class DiscordBot:
52
60
  token: str,
53
61
  allowed_users: list[int] | None = None,
54
62
  allowed_channels: list[int] | None = None,
63
+ require_mention: bool = False,
55
64
  ) -> None:
56
65
  self._sm = session_manager
57
66
  self._token = token
58
67
  self._allowed_users: set[int] = set(allowed_users) if allowed_users else set()
59
68
  self._allowed_channels: set[int] = set(allowed_channels) if allowed_channels else set()
69
+ self._require_mention = require_mention
60
70
 
61
71
  intents = discord.Intents.default()
62
72
  intents.message_content = True
@@ -113,20 +123,27 @@ class DiscordBot:
113
123
  is_dm = isinstance(message.channel, discord.DMChannel)
114
124
  is_mentioned = client.user in message.mentions if not is_dm else False
115
125
 
116
- # In guilds, only respond to mentions or whitelisted channels
117
- if not is_dm and not is_mentioned and not self._is_allowed_channel(message.channel.id):
118
- return
126
+ if not is_dm:
127
+ if self._require_mention and not is_mentioned:
128
+ return
129
+ if not self._require_mention and not is_mentioned:
130
+ if not self._is_allowed_channel(message.channel.id):
131
+ return
119
132
 
120
133
  if not self._is_allowed_user(message.author.id):
121
134
  await message.reply("Sorry, you are not authorised to use this bot.")
122
135
  return
123
136
 
124
137
  content = message.content.strip()
125
- # Remove bot mention from the beginning
126
138
  if is_mentioned and client.user:
127
139
  content = content.replace(f"<@{client.user.id}>", "").strip()
128
140
 
129
- if not content:
141
+ has_image = any(
142
+ a.content_type and a.content_type.startswith("image/")
143
+ for a in message.attachments
144
+ )
145
+
146
+ if not content and not has_image:
130
147
  return
131
148
 
132
149
  # Command dispatch
@@ -141,7 +158,35 @@ class DiscordBot:
141
158
  await self._cmd_compact(message, is_dm, hint)
142
159
  return
143
160
 
144
- await self._handle_chat(message, content, is_dm)
161
+ chat_input = content or ""
162
+ if has_image:
163
+ chat_input = await self._build_image_input(
164
+ message, content or "What's in this image?"
165
+ )
166
+
167
+ await self._handle_chat(message, chat_input, is_dm)
168
+
169
+ # ── Image handling ────────────────────────────────────────────────────────
170
+
171
+ @staticmethod
172
+ async def _build_image_input(message: discord.Message, caption: str) -> list:
173
+ """Download image attachments and build multimodal content array."""
174
+ parts: list[dict] = [{"type": "text", "text": caption}]
175
+ for att in message.attachments:
176
+ if att.content_type and att.content_type.startswith("image/"):
177
+ try:
178
+ data = await att.read()
179
+ b64 = base64.b64encode(data).decode()
180
+ media_type = att.content_type.split(";")[0]
181
+ parts.append({
182
+ "type": "image_url",
183
+ "image_url": {
184
+ "url": f"data:{media_type};base64,{b64}",
185
+ },
186
+ })
187
+ except Exception:
188
+ logger.warning("[Discord] Failed to download attachment %s", att.filename)
189
+ return parts
145
190
 
146
191
  # ── Command implementations ───────────────────────────────────────────────
147
192
 
@@ -180,12 +225,17 @@ class DiscordBot:
180
225
  for chunk in self._split_message(result or "(no result)"):
181
226
  await message.reply(chunk)
182
227
 
183
- async def _handle_chat(self, message: discord.Message, content: str, is_dm: bool) -> None:
228
+ async def _handle_chat(
229
+ self,
230
+ message: discord.Message,
231
+ content: str | list,
232
+ is_dm: bool,
233
+ ) -> None:
184
234
  sid = self._session_id(message.author.id if is_dm else message.channel.id, is_dm)
185
235
  agent = self._sm.get_or_create(sid)
186
236
 
187
237
  if self._sm.is_locked(sid):
188
- await message.reply("Processing previous message")
238
+ await message.reply("Processing previous message\u2026")
189
239
 
190
240
  async with message.channel.typing():
191
241
  try:
@@ -226,11 +276,15 @@ def create_bot(session_manager: "SessionManager") -> "DiscordBot":
226
276
  allowed_channels = config.get_int_list(
227
277
  "channels", "discord", "allowedChannels", env="DISCORD_ALLOWED_CHANNELS",
228
278
  )
279
+ require_mention = config.get_bool(
280
+ "channels", "discord", "requireMention", default=False,
281
+ )
229
282
  return DiscordBot(
230
283
  session_manager=session_manager,
231
284
  token=token,
232
285
  allowed_users=allowed_users or None,
233
286
  allowed_channels=allowed_channels or None,
287
+ require_mention=require_mention,
234
288
  )
235
289
 
236
290
 
@@ -14,16 +14,23 @@ Commands
14
14
  /status — show session info (provider, skills, memory, tokens, compactions)
15
15
  /compact [hint] — compact conversation history
16
16
  <text> — forwarded to Agent.chat(), reply sent back
17
+ <photo> — image sent to LLM with optional caption
17
18
 
18
19
  Access control
19
20
  --------------
20
21
  Set TELEGRAM_ALLOWED_USERS to a comma-separated list of integer Telegram user
21
22
  IDs to restrict access. Leave empty (or unset) to allow all users.
23
+
24
+ Group behaviour
25
+ ---------------
26
+ Set ``channels.telegram.requireMention`` to ``true`` in pythonclaw.json to
27
+ require @bot mention in group chats. DMs always respond.
22
28
  """
23
29
 
24
30
  from __future__ import annotations
25
31
 
26
32
  import asyncio
33
+ import base64
27
34
  import logging
28
35
  from typing import TYPE_CHECKING
29
36
 
@@ -57,11 +64,14 @@ class TelegramBot:
57
64
  session_manager: "SessionManager",
58
65
  token: str,
59
66
  allowed_users: list[int] | None = None,
67
+ require_mention: bool = False,
60
68
  ) -> None:
61
69
  self._sm = session_manager
62
70
  self._token = token
63
71
  self._allowed_users: set[int] = set(allowed_users) if allowed_users else set()
72
+ self._require_mention = require_mention
64
73
  self._app: Application | None = None
74
+ self._bot_username: str | None = None
65
75
 
66
76
  # ── Session ID convention ─────────────────────────────────────────────────
67
77
 
@@ -93,6 +103,29 @@ class TelegramBot:
93
103
  return False
94
104
  return True
95
105
 
106
+ def _is_group(self, update: Update) -> bool:
107
+ """Return True if the message is from a group/supergroup."""
108
+ return update.effective_chat.type in ("group", "supergroup")
109
+
110
+ def _is_mentioned(self, update: Update) -> bool:
111
+ """Check if the bot is @mentioned in the message text."""
112
+ text = update.message.text or update.message.caption or ""
113
+ if self._bot_username and f"@{self._bot_username}" in text:
114
+ return True
115
+ entities = update.message.entities or update.message.caption_entities or []
116
+ for ent in entities:
117
+ if ent.type == "mention" and self._bot_username:
118
+ mention = text[ent.offset:ent.offset + ent.length]
119
+ if mention.lower() == f"@{self._bot_username.lower()}":
120
+ return True
121
+ return False
122
+
123
+ def _strip_mention(self, text: str) -> str:
124
+ """Remove the @bot mention from message text."""
125
+ if self._bot_username:
126
+ text = text.replace(f"@{self._bot_username}", "").strip()
127
+ return text
128
+
96
129
  # ── Command handlers ──────────────────────────────────────────────────────
97
130
 
98
131
  async def _cmd_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
@@ -101,13 +134,14 @@ class TelegramBot:
101
134
  sid = self._session_id(update.effective_chat.id)
102
135
  self._sm.get_or_create(sid)
103
136
  await update.message.reply_text(
104
- "👋 Hi! I'm your PythonClaw agent.\n\n"
105
- "Just send me a message and I'll do my best to help.\n\n"
137
+ "\U0001f44b Hi! I'm your PythonClaw agent.\n\n"
138
+ "Just send me a message and I'll do my best to help.\n"
139
+ "You can also send photos and I'll analyze them.\n\n"
106
140
  "Commands:\n"
107
- " /start show this message\n"
108
- " /reset start a fresh session\n"
109
- " /status show session info\n"
110
- " /compact [hint] compact conversation history"
141
+ " /start \u2014 show this message\n"
142
+ " /reset \u2014 start a fresh session\n"
143
+ " /status \u2014 show session info\n"
144
+ " /compact [hint] \u2014 compact conversation history"
111
145
  )
112
146
 
113
147
  async def _cmd_reset(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
@@ -124,7 +158,7 @@ class TelegramBot:
124
158
  agent = self._sm.get_or_create(sid)
125
159
  from ..core.compaction import estimate_tokens
126
160
  await update.message.reply_text(
127
- f"📊 Session Status\n"
161
+ f"\U0001f4ca Session Status\n"
128
162
  f" Session ID : {sid}\n"
129
163
  f" Provider : {type(agent.provider).__name__}\n"
130
164
  f" Skills : {len(agent.loaded_skill_names)} loaded\n"
@@ -141,7 +175,7 @@ class TelegramBot:
141
175
  sid = self._session_id(update.effective_chat.id)
142
176
  agent = self._sm.get_or_create(sid)
143
177
  hint: str | None = " ".join(context.args).strip() or None if context.args else None
144
- await update.message.reply_text(" Compacting conversation history...")
178
+ await update.message.reply_text("\u23f3 Compacting conversation history...")
145
179
  try:
146
180
  result = agent.compact(instruction=hint)
147
181
  except Exception as exc:
@@ -149,43 +183,55 @@ class TelegramBot:
149
183
  for chunk in _split_message(result):
150
184
  await update.message.reply_text(chunk)
151
185
 
152
- # ── Message handler ───────────────────────────────────────────────────────
186
+ # ── Message handler (text + photos) ───────────────────────────────────────
153
187
 
154
188
  async def _handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
155
189
  if not await self._check_access(update, context):
156
190
  return
157
- user_text = (update.message.text or "").strip()
158
- if not user_text:
191
+
192
+ if self._is_group(update) and self._require_mention:
193
+ if not self._is_mentioned(update):
194
+ return
195
+
196
+ user_text = (update.message.text or update.message.caption or "").strip()
197
+ user_text = self._strip_mention(user_text)
198
+
199
+ has_photo = bool(update.message.photo)
200
+
201
+ if not user_text and not has_photo:
159
202
  return
203
+
160
204
  sid = self._session_id(update.effective_chat.id)
161
205
  agent = self._sm.get_or_create(sid)
162
206
 
163
207
  if self._sm.is_locked(sid):
164
- await update.message.reply_text(" Processing previous message")
208
+ await update.message.reply_text("\u23f3 Processing previous message\u2026")
165
209
 
166
- # React to the message so the user knows the bot saw it
167
210
  try:
168
- await update.message.set_reaction([ReactionTypeEmoji("👀")])
211
+ await update.message.set_reaction([ReactionTypeEmoji("\U0001f440")])
169
212
  except Exception:
170
- pass # reaction API may fail on older bot API or in groups
213
+ pass
214
+
215
+ # Build multimodal input if photo is present
216
+ chat_input = user_text or ""
217
+ if has_photo:
218
+ chat_input = await self._build_image_input(
219
+ update, user_text or "What's in this image?"
220
+ )
171
221
 
172
- # Keep "typing…" visible for the entire duration of processing.
173
- # Telegram's typing indicator expires after ~5 s, so we resend it
174
- # on a loop until the agent finishes.
175
222
  typing_task = asyncio.create_task(
176
223
  self._keep_typing(update.message.chat_id)
177
224
  )
178
225
  try:
179
226
  async with self._sm.acquire(sid):
180
227
  loop = asyncio.get_event_loop()
181
- response = await loop.run_in_executor(None, agent.chat, user_text)
228
+ response = await loop.run_in_executor(None, agent.chat, chat_input)
182
229
  except Exception as exc:
183
230
  logger.exception("[Telegram] Agent.chat() raised an exception")
184
231
  response = f"Sorry, something went wrong: {exc}"
185
232
  finally:
186
233
  typing_task.cancel()
187
234
 
188
- # Clear the "seen" reaction once we reply
189
235
  try:
190
236
  await update.message.set_reaction([])
191
237
  except Exception:
@@ -194,6 +240,23 @@ class TelegramBot:
194
240
  for chunk in _split_message(response or "(no response)"):
195
241
  await update.message.reply_text(chunk)
196
242
 
243
+ async def _build_image_input(self, update: Update, caption: str) -> list:
244
+ """Download photo and build a multimodal content array."""
245
+ photo = update.message.photo[-1] # highest resolution
246
+ file = await photo.get_file()
247
+ data = await file.download_as_bytearray()
248
+ b64 = base64.b64encode(bytes(data)).decode()
249
+
250
+ return [
251
+ {"type": "text", "text": caption},
252
+ {
253
+ "type": "image_url",
254
+ "image_url": {
255
+ "url": f"data:image/jpeg;base64,{b64}",
256
+ },
257
+ },
258
+ ]
259
+
197
260
  async def _keep_typing(self, chat_id: int) -> None:
198
261
  """Re-send the 'typing' chat action every 4 s until cancelled."""
199
262
  try:
@@ -220,7 +283,10 @@ class TelegramBot:
220
283
  app.add_handler(CommandHandler("reset", self._cmd_reset))
221
284
  app.add_handler(CommandHandler("status", self._cmd_status))
222
285
  app.add_handler(CommandHandler("compact", self._cmd_compact))
223
- app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self._handle_message))
286
+ app.add_handler(MessageHandler(
287
+ (filters.TEXT | filters.PHOTO) & ~filters.COMMAND,
288
+ self._handle_message,
289
+ ))
224
290
  self._app = app
225
291
  return app
226
292
 
@@ -228,7 +294,12 @@ class TelegramBot:
228
294
  """Register slash-commands with Telegram so they appear in the menu."""
229
295
  try:
230
296
  await self._app.bot.set_my_commands(self._BOT_COMMANDS)
231
- logger.info("[Telegram] Registered %d bot commands", len(self._BOT_COMMANDS))
297
+ me = await self._app.bot.get_me()
298
+ self._bot_username = me.username
299
+ logger.info(
300
+ "[Telegram] Registered %d bot commands, username=@%s",
301
+ len(self._BOT_COMMANDS), self._bot_username,
302
+ )
232
303
  except Exception:
233
304
  logger.warning("[Telegram] Failed to register bot commands", exc_info=True)
234
305
 
@@ -280,10 +351,14 @@ def create_bot(session_manager: "SessionManager") -> TelegramBot:
280
351
  allowed_users = config.get_int_list(
281
352
  "channels", "telegram", "allowedUsers", env="TELEGRAM_ALLOWED_USERS",
282
353
  )
354
+ require_mention = config.get_bool(
355
+ "channels", "telegram", "requireMention", default=False,
356
+ )
283
357
  return TelegramBot(
284
358
  session_manager=session_manager,
285
359
  token=token,
286
360
  allowed_users=allowed_users or None,
361
+ require_mention=require_mention,
287
362
  )
288
363
 
289
364