pythonclaw 0.2.1__tar.gz → 0.2.3__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 (124) hide show
  1. pythonclaw-0.2.3/MANIFEST.in +13 -0
  2. {pythonclaw-0.2.1/pythonclaw.egg-info → pythonclaw-0.2.3}/PKG-INFO +1 -1
  3. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pyproject.toml +7 -2
  4. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/__init__.py +1 -1
  5. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/main.py +37 -19
  6. pythonclaw-0.2.3/pythonclaw/server.py +125 -0
  7. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/web/app.py +86 -1
  8. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/web/static/index.html +43 -1
  9. {pythonclaw-0.2.1 → pythonclaw-0.2.3/pythonclaw.egg-info}/PKG-INFO +1 -1
  10. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw.egg-info/SOURCES.txt +1 -17
  11. pythonclaw-0.2.1/pythonclaw/server.py +0 -145
  12. pythonclaw-0.2.1/pythonclaw/templates/skills/communication/email/__pycache__/send_email.cpython-311.pyc +0 -0
  13. pythonclaw-0.2.1/pythonclaw/templates/skills/data/csv_analyzer/__pycache__/analyze.cpython-311.pyc +0 -0
  14. pythonclaw-0.2.1/pythonclaw/templates/skills/data/finance/__pycache__/fetch_quote.cpython-311.pyc +0 -0
  15. pythonclaw-0.2.1/pythonclaw/templates/skills/data/news/__pycache__/search_news.cpython-311.pyc +0 -0
  16. pythonclaw-0.2.1/pythonclaw/templates/skills/data/pdf_reader/__pycache__/read_pdf.cpython-311.pyc +0 -0
  17. pythonclaw-0.2.1/pythonclaw/templates/skills/data/scraper/__pycache__/scrape.cpython-311.pyc +0 -0
  18. pythonclaw-0.2.1/pythonclaw/templates/skills/data/weather/__pycache__/weather.cpython-311.pyc +0 -0
  19. pythonclaw-0.2.1/pythonclaw/templates/skills/data/youtube/__pycache__/youtube_info.cpython-311.pyc +0 -0
  20. pythonclaw-0.2.1/pythonclaw/templates/skills/dev/code_runner/__pycache__/run_code.cpython-311.pyc +0 -0
  21. pythonclaw-0.2.1/pythonclaw/templates/skills/dev/github/__pycache__/gh.cpython-311.pyc +0 -0
  22. pythonclaw-0.2.1/pythonclaw/templates/skills/dev/http_request/__pycache__/request.cpython-311.pyc +0 -0
  23. pythonclaw-0.2.1/pythonclaw/templates/skills/google/workspace/check_setup.sh +0 -52
  24. pythonclaw-0.2.1/pythonclaw/templates/skills/system/change_setting/__pycache__/update_config.cpython-311.pyc +0 -0
  25. pythonclaw-0.2.1/pythonclaw/templates/skills/system/onboarding/__pycache__/write_identity.cpython-311.pyc +0 -0
  26. pythonclaw-0.2.1/pythonclaw/templates/skills/system/random/__pycache__/random_util.cpython-311.pyc +0 -0
  27. pythonclaw-0.2.1/pythonclaw/templates/skills/system/time/__pycache__/time_util.cpython-311.pyc +0 -0
  28. pythonclaw-0.2.1/pythonclaw/templates/skills/text/translator/__pycache__/translate.cpython-311.pyc +0 -0
  29. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/LICENSE +0 -0
  30. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/README.md +0 -0
  31. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/__main__.py +0 -0
  32. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/channels/discord_bot.py +0 -0
  33. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/channels/telegram_bot.py +0 -0
  34. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/config.py +0 -0
  35. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/__init__.py +0 -0
  36. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/agent.py +0 -0
  37. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/compaction.py +0 -0
  38. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/knowledge/rag.py +0 -0
  39. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/llm/anthropic_client.py +0 -0
  40. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/llm/base.py +0 -0
  41. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/llm/gemini_client.py +0 -0
  42. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/llm/openai_compatible.py +0 -0
  43. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/llm/response.py +0 -0
  44. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/memory/manager.py +0 -0
  45. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/memory/storage.py +0 -0
  46. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/persistent_agent.py +0 -0
  47. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/retrieval/__init__.py +0 -0
  48. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/retrieval/chunker.py +0 -0
  49. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/retrieval/dense.py +0 -0
  50. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/retrieval/fusion.py +0 -0
  51. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/retrieval/reranker.py +0 -0
  52. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/retrieval/retriever.py +0 -0
  53. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/retrieval/sparse.py +0 -0
  54. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/session_store.py +0 -0
  55. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/skill_loader.py +0 -0
  56. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/skillhub.py +0 -0
  57. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/tools.py +0 -0
  58. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/core/utils.py +0 -0
  59. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/daemon.py +0 -0
  60. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/init.py +0 -0
  61. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/onboard.py +0 -0
  62. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/scheduler/cron.py +0 -0
  63. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/scheduler/heartbeat.py +0 -0
  64. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/session_manager.py +0 -0
  65. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/persona/demo_persona.md +0 -0
  66. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/communication/CATEGORY.md +0 -0
  67. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/communication/email/SKILL.md +0 -0
  68. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/communication/email/send_email.py +0 -0
  69. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/data/CATEGORY.md +0 -0
  70. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/data/csv_analyzer/SKILL.md +0 -0
  71. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/data/csv_analyzer/analyze.py +0 -0
  72. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/data/finance/SKILL.md +0 -0
  73. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/data/finance/fetch_quote.py +0 -0
  74. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/data/news/SKILL.md +0 -0
  75. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/data/news/search_news.py +0 -0
  76. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/data/pdf_reader/SKILL.md +0 -0
  77. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/data/pdf_reader/read_pdf.py +0 -0
  78. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/data/scraper/SKILL.md +0 -0
  79. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/data/scraper/scrape.py +0 -0
  80. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/data/weather/SKILL.md +0 -0
  81. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/data/weather/weather.py +0 -0
  82. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/data/youtube/SKILL.md +0 -0
  83. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/data/youtube/youtube_info.py +0 -0
  84. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/dev/CATEGORY.md +0 -0
  85. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/dev/code_runner/SKILL.md +0 -0
  86. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/dev/code_runner/run_code.py +0 -0
  87. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/dev/github/SKILL.md +0 -0
  88. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/dev/github/gh.py +0 -0
  89. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/dev/http_request/SKILL.md +0 -0
  90. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/dev/http_request/request.py +0 -0
  91. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/google/CATEGORY.md +0 -0
  92. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/google/workspace/SKILL.md +0 -0
  93. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/meta/CATEGORY.md +0 -0
  94. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/meta/skill_creator/SKILL.md +0 -0
  95. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/system/CATEGORY.md +0 -0
  96. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/system/change_persona/SKILL.md +0 -0
  97. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/system/change_setting/SKILL.md +0 -0
  98. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/system/change_setting/update_config.py +0 -0
  99. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/system/change_soul/SKILL.md +0 -0
  100. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/system/onboarding/SKILL.md +0 -0
  101. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/system/onboarding/write_identity.py +0 -0
  102. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/system/random/SKILL.md +0 -0
  103. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/system/random/random_util.py +0 -0
  104. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/system/time/SKILL.md +0 -0
  105. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/system/time/time_util.py +0 -0
  106. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/text/CATEGORY.md +0 -0
  107. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/text/translator/SKILL.md +0 -0
  108. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/text/translator/translate.py +0 -0
  109. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/web/CATEGORY.md +0 -0
  110. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/skills/web/tavily/SKILL.md +0 -0
  111. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/templates/soul/SOUL.md +0 -0
  112. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/web/__init__.py +0 -0
  113. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/web/static/favicon.png +0 -0
  114. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw/web/static/logo.png +0 -0
  115. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw.egg-info/dependency_links.txt +0 -0
  116. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw.egg-info/entry_points.txt +0 -0
  117. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw.egg-info/requires.txt +0 -0
  118. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/pythonclaw.egg-info/top_level.txt +0 -0
  119. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/setup.cfg +0 -0
  120. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/tests/test_compaction.py +0 -0
  121. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/tests/test_persistence.py +0 -0
  122. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/tests/test_rag_hybrid.py +0 -0
  123. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/tests/test_skills.py +0 -0
  124. {pythonclaw-0.2.1 → pythonclaw-0.2.3}/tests/test_soul.py +0 -0
@@ -0,0 +1,13 @@
1
+ include LICENSE README.md
2
+ recursive-include pythonclaw *.py *.html *.css *.js *.png *.ico
3
+ recursive-include tests *.py
4
+
5
+ exclude pythonclaw.json
6
+ exclude pythonclaw.example.json
7
+ exclude .env .env.example
8
+ recursive-exclude * __pycache__
9
+ recursive-exclude * *.py[cod]
10
+ recursive-exclude * *.pyc
11
+ recursive-exclude context *
12
+ recursive-exclude .github *
13
+ recursive-exclude assets *
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonclaw
3
- Version: 0.2.1
3
+ Version: 0.2.3
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pythonclaw"
7
- version = "0.2.1"
7
+ version = "0.2.3"
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"}
@@ -83,6 +83,11 @@ include = ["pythonclaw*"]
83
83
 
84
84
  [tool.setuptools.package-data]
85
85
  pythonclaw = [
86
- "templates/**/*",
86
+ "templates/**/*.py",
87
+ "templates/**/*.yaml",
88
+ "templates/**/*.md",
87
89
  "web/static/**/*",
88
90
  ]
91
+
92
+ [tool.setuptools.exclude-package-data]
93
+ "*" = ["__pycache__", "*.pyc", "*.pyo"]
@@ -6,7 +6,7 @@ from .core.llm.base import LLMProvider
6
6
  from .core.llm.openai_compatible import OpenAICompatibleProvider
7
7
  from .init import init
8
8
 
9
- __version__ = "0.2.1"
9
+ __version__ = "0.2.3"
10
10
  __all__ = [
11
11
  "Agent",
12
12
  "LLMProvider",
@@ -149,28 +149,46 @@ def _run_foreground(args) -> None:
149
149
  format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
150
150
  )
151
151
 
152
- if channels:
153
- # Run web + channels together
154
- from .server import run_server
155
- print(f"[PythonClaw] Starting (web + {', '.join(channels)})...")
156
- asyncio.run(run_server(provider, channels=channels))
157
- else:
158
- # Web-only
159
- try:
160
- import uvicorn
161
- except ImportError:
162
- print("Error: Web mode requires 'fastapi' and 'uvicorn'.")
163
- print("Install with: pip install pythonclaw[web]")
164
- return
152
+ try:
153
+ import uvicorn
154
+ except ImportError:
155
+ print("Error: Web mode requires 'fastapi' and 'uvicorn'.")
156
+ print("Install with: pip install pythonclaw[web]")
157
+ return
165
158
 
166
- from .web.app import create_app
159
+ from .web.app import create_app
167
160
 
168
- host = config.get_str("web", "host", default="0.0.0.0")
169
- port = config.get_int("web", "port", default=7788)
161
+ host = config.get_str("web", "host", default="0.0.0.0")
162
+ port = config.get_int("web", "port", default=7788)
170
163
 
171
- app = create_app(provider, build_provider_fn=_build_provider)
172
- print(f"[PythonClaw] Web dashboard: http://localhost:{port}")
173
- uvicorn.run(app, host=host, port=port, log_level="info")
164
+ app = create_app(provider, build_provider_fn=_build_provider)
165
+
166
+ ch_to_start = channels or _detect_configured_channels()
167
+ if ch_to_start:
168
+ from .server import start_channels
169
+ from .web import app as web_app_module
170
+ label = "explicit" if channels else "auto-detected"
171
+ print(f"[PythonClaw] Channels ({label}): {', '.join(ch_to_start)}")
172
+
173
+ @app.on_event("startup")
174
+ async def _start_channels():
175
+ bots = await start_channels(provider, ch_to_start)
176
+ web_app_module._active_bots.extend(bots)
177
+
178
+ print(f"[PythonClaw] Web dashboard: http://localhost:{port}")
179
+ uvicorn.run(app, host=host, port=port, log_level="info")
180
+
181
+
182
+ def _detect_configured_channels() -> list[str]:
183
+ """Return channel names that have a token configured."""
184
+ found = []
185
+ tg_token = config.get_str("channels", "telegram", "token", default="")
186
+ if tg_token:
187
+ found.append("telegram")
188
+ dc_token = config.get_str("channels", "discord", "token", default="")
189
+ if dc_token:
190
+ found.append("discord")
191
+ return found
174
192
 
175
193
 
176
194
  def _cmd_stop(args) -> None:
@@ -0,0 +1,125 @@
1
+ """
2
+ Daemon server for PythonClaw — multi-channel mode.
3
+
4
+ Supports Telegram and Discord channels, individually or combined.
5
+ The web dashboard always runs; channels are started alongside it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import logging
12
+ import os
13
+ import signal
14
+
15
+ from .core.llm.base import LLMProvider
16
+ from .core.persistent_agent import PersistentAgent
17
+ from .core.session_store import SessionStore
18
+ from .scheduler.cron import CronScheduler
19
+ from .session_manager import SessionManager
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ async def start_channels(
25
+ provider: LLMProvider,
26
+ channels: list[str],
27
+ ) -> list:
28
+ """Start messaging channels (Telegram, Discord) as background tasks.
29
+
30
+ Returns the list of successfully started bot objects.
31
+ Safe to call during FastAPI startup — failures are logged, not raised.
32
+ """
33
+ store = SessionStore()
34
+ session_manager = SessionManager(agent_factory=lambda sid: None, store=store)
35
+
36
+ jobs_path = os.path.join("context", "cron", "jobs.yaml")
37
+ scheduler = CronScheduler(
38
+ session_manager=session_manager,
39
+ jobs_path=jobs_path,
40
+ )
41
+
42
+ def agent_factory(session_id: str) -> PersistentAgent:
43
+ return PersistentAgent(
44
+ provider=provider,
45
+ store=store,
46
+ session_id=session_id,
47
+ cron_manager=scheduler,
48
+ verbose=False,
49
+ )
50
+
51
+ session_manager.set_factory(agent_factory)
52
+
53
+ active_bots: list = []
54
+
55
+ if "telegram" in channels:
56
+ try:
57
+ from .channels.telegram_bot import create_bot_from_env
58
+ bot = create_bot_from_env(session_manager)
59
+ scheduler._telegram_bot = bot
60
+ await bot.start_async()
61
+ active_bots.append(bot)
62
+ logger.info("[Server] Telegram bot started.")
63
+ except Exception as exc:
64
+ logger.warning("[Server] Telegram channel failed to start: %s", exc)
65
+
66
+ if "discord" in channels:
67
+ try:
68
+ from .channels.discord_bot import create_bot_from_env as create_discord
69
+ discord_bot = create_discord(session_manager)
70
+ asyncio.create_task(discord_bot.start_async())
71
+ active_bots.append(discord_bot)
72
+ logger.info("[Server] Discord bot started.")
73
+ except Exception as exc:
74
+ logger.warning("[Server] Discord channel failed to start: %s", exc)
75
+
76
+ if active_bots:
77
+ scheduler.start()
78
+ logger.info("[Server] Channels running: %s", ", ".join(channels))
79
+ else:
80
+ logger.warning("[Server] No channels started — check tokens in pythonclaw.json.")
81
+
82
+ return active_bots
83
+
84
+
85
+ async def run_server(
86
+ provider: LLMProvider,
87
+ channels: list[str] | None = None,
88
+ ) -> None:
89
+ """Standalone server entry point (channels only, no web).
90
+
91
+ Kept for backward compatibility. Prefer using ``start_channels``
92
+ together with the web dashboard in ``_run_foreground``.
93
+ """
94
+ if channels is None:
95
+ channels = ["telegram"]
96
+
97
+ active_bots = await start_channels(provider, channels)
98
+
99
+ if not active_bots:
100
+ logger.error("[Server] No channels started. Exiting.")
101
+ return
102
+
103
+ stop_event = asyncio.Event()
104
+
105
+ def _signal_handler() -> None:
106
+ logger.info("[Server] Shutdown signal received.")
107
+ stop_event.set()
108
+
109
+ loop = asyncio.get_running_loop()
110
+ for sig in (signal.SIGINT, signal.SIGTERM):
111
+ try:
112
+ loop.add_signal_handler(sig, _signal_handler)
113
+ except (NotImplementedError, OSError):
114
+ pass
115
+
116
+ try:
117
+ await stop_event.wait()
118
+ except (KeyboardInterrupt, asyncio.CancelledError):
119
+ pass
120
+ finally:
121
+ logger.info("[Server] Shutting down...")
122
+ for bot in active_bots:
123
+ if hasattr(bot, 'stop_async'):
124
+ await bot.stop_async()
125
+ logger.info("[Server] Shutdown complete.")
@@ -37,6 +37,7 @@ _provider: LLMProvider | None = None
37
37
  _store: SessionStore | None = None
38
38
  _start_time: float = 0.0
39
39
  _build_provider_fn = None
40
+ _active_bots: list = []
40
41
 
41
42
  WEB_SESSION_ID = "web:dashboard"
42
43
 
@@ -73,6 +74,8 @@ def create_app(provider: LLMProvider | None, *, build_provider_fn=None) -> FastA
73
74
  app.add_api_route("/api/skillhub/search", _api_skillhub_search, methods=["POST"])
74
75
  app.add_api_route("/api/skillhub/browse", _api_skillhub_browse, methods=["GET"])
75
76
  app.add_api_route("/api/skillhub/install", _api_skillhub_install, methods=["POST"])
77
+ app.add_api_route("/api/channels", _api_channels_status, methods=["GET"])
78
+ app.add_api_route("/api/channels/restart", _api_channels_restart, methods=["POST"])
76
79
  app.add_websocket_route("/ws/chat", _ws_chat)
77
80
 
78
81
  return app
@@ -226,7 +229,14 @@ async def _api_config_save(request: Request):
226
229
  logger.warning("[Web] Provider rebuild failed: %s", exc)
227
230
  _provider = None
228
231
 
229
- return {"ok": True, "configPath": str(cfg_path), "providerReady": _provider is not None}
232
+ channels_started = await _maybe_start_channels()
233
+
234
+ return {
235
+ "ok": True,
236
+ "configPath": str(cfg_path),
237
+ "providerReady": _provider is not None,
238
+ "channelsStarted": channels_started,
239
+ }
230
240
 
231
241
 
232
242
  async def _api_skills():
@@ -507,6 +517,81 @@ async def _api_skillhub_install(request: Request):
507
517
  return JSONResponse({"ok": False, "error": str(exc)}, status_code=500)
508
518
 
509
519
 
520
+ async def _maybe_start_channels() -> list[str]:
521
+ """Start channels whose tokens are now configured but not yet running."""
522
+ global _active_bots
523
+ if _provider is None:
524
+ return []
525
+
526
+ wanted = []
527
+ tg_token = config.get_str("channels", "telegram", "token", default="")
528
+ if tg_token:
529
+ wanted.append("telegram")
530
+ dc_token = config.get_str("channels", "discord", "token", default="")
531
+ if dc_token:
532
+ wanted.append("discord")
533
+
534
+ if not wanted:
535
+ return []
536
+
537
+ running_types = set()
538
+ for bot in _active_bots:
539
+ cls_name = type(bot).__name__.lower()
540
+ if "telegram" in cls_name:
541
+ running_types.add("telegram")
542
+ elif "discord" in cls_name:
543
+ running_types.add("discord")
544
+
545
+ to_start = [ch for ch in wanted if ch not in running_types]
546
+ if not to_start:
547
+ return list(running_types)
548
+
549
+ try:
550
+ from ..server import start_channels
551
+ new_bots = await start_channels(_provider, to_start)
552
+ _active_bots.extend(new_bots)
553
+ return [ch for ch in wanted if ch in running_types or ch in to_start]
554
+ except Exception as exc:
555
+ logger.warning("[Web] Channel start failed: %s", exc)
556
+ return list(running_types)
557
+
558
+
559
+ async def _api_channels_status():
560
+ """Return status of messaging channels."""
561
+ channels = []
562
+ for bot in _active_bots:
563
+ cls_name = type(bot).__name__
564
+ ch_type = "telegram" if "Telegram" in cls_name else "discord" if "Discord" in cls_name else cls_name
565
+ channels.append({"type": ch_type, "running": True})
566
+
567
+ tg_token = config.get_str("channels", "telegram", "token", default="")
568
+ dc_token = config.get_str("channels", "discord", "token", default="")
569
+ running_types = {c["type"] for c in channels}
570
+
571
+ if tg_token and "telegram" not in running_types:
572
+ channels.append({"type": "telegram", "running": False, "tokenSet": True})
573
+ if dc_token and "discord" not in running_types:
574
+ channels.append({"type": "discord", "running": False, "tokenSet": True})
575
+
576
+ return {"channels": channels}
577
+
578
+
579
+ async def _api_channels_restart(request: Request):
580
+ """Stop and restart all configured channels."""
581
+ global _active_bots
582
+
583
+ for bot in _active_bots:
584
+ if hasattr(bot, "stop_async"):
585
+ try:
586
+ await bot.stop_async()
587
+ except Exception:
588
+ pass
589
+ _active_bots = []
590
+
591
+ started = await _maybe_start_channels()
592
+ return {"ok": True, "channels": started}
593
+
594
+
510
595
  def _reload_agent_identity() -> None:
511
596
  """Reload the agent's soul/persona from disk without full reset."""
512
597
  global _agent
@@ -209,6 +209,16 @@
209
209
  <div id="stat-history" class="text-[.6875rem] text-gray-500">0 msgs in history</div>
210
210
  </div>
211
211
  </div>
212
+ <div class="stat-card section-card flex items-start gap-3.5">
213
+ <div class="w-9 h-9 rounded-lg bg-cyan-500/15 flex items-center justify-center shrink-0 mt-0.5">
214
+ <svg class="w-4.5 h-4.5 text-cyan-400" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>
215
+ </div>
216
+ <div class="min-w-0">
217
+ <div class="text-[.6875rem] text-gray-500 font-medium uppercase tracking-wider mb-1">Channels</div>
218
+ <div id="stat-channels" class="text-base font-semibold text-white">Web only</div>
219
+ <div id="stat-channels-detail" class="text-[.6875rem] text-gray-500">web dashboard</div>
220
+ </div>
221
+ </div>
212
222
  </div>
213
223
 
214
224
  <!-- Identity & Status Row -->
@@ -493,7 +503,10 @@
493
503
 
494
504
  <!-- Channels -->
495
505
  <div class="cfg-section">
496
- <h3 class="cfg-title">Channels</h3>
506
+ <h3 class="cfg-title flex items-center gap-2">
507
+ Channels
508
+ <span id="cfg-channel-status" class="text-[.625rem] font-normal text-gray-500"></span>
509
+ </h3>
497
510
  <div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4">
498
511
  <div class="sm:col-span-2">
499
512
  <label class="block text-xs text-gray-500 mb-1.5">Telegram Bot Token</label>
@@ -512,6 +525,7 @@
512
525
  <input id="cfg-dc-channels" type="text" placeholder="Comma-separated channel IDs (blank = allow all)" class="input-field">
513
526
  </div>
514
527
  </div>
528
+ <p class="text-[.6875rem] text-gray-600 mt-3">Channels auto-start after saving. Tokens are stored securely.</p>
515
529
  </div>
516
530
 
517
531
  <!-- Web Dashboard -->
@@ -636,6 +650,21 @@ async function refreshDashboard() {
636
650
  banner.classList.toggle('hidden', d.providerReady !== false);
637
651
  } catch (e) { console.error('Status fetch:', e); }
638
652
 
653
+ try {
654
+ const chRes = await fetch('/api/channels');
655
+ const chData = await chRes.json();
656
+ const running = (chData.channels || []).filter(c => c.running).map(c => c.type);
657
+ const chEl = document.getElementById('stat-channels');
658
+ const chDetailEl = document.getElementById('stat-channels-detail');
659
+ if (running.length > 0) {
660
+ chEl.textContent = (running.length + 1) + ' active';
661
+ chDetailEl.textContent = 'web + ' + running.join(', ');
662
+ } else {
663
+ chEl.textContent = 'Web only';
664
+ chDetailEl.textContent = 'web dashboard';
665
+ }
666
+ } catch (e) {}
667
+
639
668
  try {
640
669
  const res2 = await fetch('/api/identity');
641
670
  const id = await res2.json();
@@ -1085,6 +1114,19 @@ async function loadConfig() {
1085
1114
 
1086
1115
  document.getElementById('cfg-json-raw').value = JSON.stringify(_rawConfig, null, 2);
1087
1116
  document.getElementById('config-save-status').textContent = '';
1117
+
1118
+ // Channel status
1119
+ try {
1120
+ const chRes = await fetch('/api/channels');
1121
+ const chData = await chRes.json();
1122
+ const statusEl = document.getElementById('cfg-channel-status');
1123
+ const running = (chData.channels || []).filter(c => c.running).map(c => c.type);
1124
+ if (running.length > 0) {
1125
+ statusEl.innerHTML = '<span class="text-accent-green">● ' + running.join(', ') + ' running</span>';
1126
+ } else {
1127
+ statusEl.textContent = '';
1128
+ }
1129
+ } catch (e) {}
1088
1130
  } catch (e) { console.error('Config fetch:', e); }
1089
1131
  }
1090
1132
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonclaw
3
- Version: 0.2.1
3
+ Version: 0.2.3
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
@@ -1,4 +1,5 @@
1
1
  LICENSE
2
+ MANIFEST.in
2
3
  README.md
3
4
  pyproject.toml
4
5
  pythonclaw/__init__.py
@@ -48,63 +49,46 @@ pythonclaw/templates/persona/demo_persona.md
48
49
  pythonclaw/templates/skills/communication/CATEGORY.md
49
50
  pythonclaw/templates/skills/communication/email/SKILL.md
50
51
  pythonclaw/templates/skills/communication/email/send_email.py
51
- pythonclaw/templates/skills/communication/email/__pycache__/send_email.cpython-311.pyc
52
52
  pythonclaw/templates/skills/data/CATEGORY.md
53
53
  pythonclaw/templates/skills/data/csv_analyzer/SKILL.md
54
54
  pythonclaw/templates/skills/data/csv_analyzer/analyze.py
55
- pythonclaw/templates/skills/data/csv_analyzer/__pycache__/analyze.cpython-311.pyc
56
55
  pythonclaw/templates/skills/data/finance/SKILL.md
57
56
  pythonclaw/templates/skills/data/finance/fetch_quote.py
58
- pythonclaw/templates/skills/data/finance/__pycache__/fetch_quote.cpython-311.pyc
59
57
  pythonclaw/templates/skills/data/news/SKILL.md
60
58
  pythonclaw/templates/skills/data/news/search_news.py
61
- pythonclaw/templates/skills/data/news/__pycache__/search_news.cpython-311.pyc
62
59
  pythonclaw/templates/skills/data/pdf_reader/SKILL.md
63
60
  pythonclaw/templates/skills/data/pdf_reader/read_pdf.py
64
- pythonclaw/templates/skills/data/pdf_reader/__pycache__/read_pdf.cpython-311.pyc
65
61
  pythonclaw/templates/skills/data/scraper/SKILL.md
66
62
  pythonclaw/templates/skills/data/scraper/scrape.py
67
- pythonclaw/templates/skills/data/scraper/__pycache__/scrape.cpython-311.pyc
68
63
  pythonclaw/templates/skills/data/weather/SKILL.md
69
64
  pythonclaw/templates/skills/data/weather/weather.py
70
- pythonclaw/templates/skills/data/weather/__pycache__/weather.cpython-311.pyc
71
65
  pythonclaw/templates/skills/data/youtube/SKILL.md
72
66
  pythonclaw/templates/skills/data/youtube/youtube_info.py
73
- pythonclaw/templates/skills/data/youtube/__pycache__/youtube_info.cpython-311.pyc
74
67
  pythonclaw/templates/skills/dev/CATEGORY.md
75
68
  pythonclaw/templates/skills/dev/code_runner/SKILL.md
76
69
  pythonclaw/templates/skills/dev/code_runner/run_code.py
77
- pythonclaw/templates/skills/dev/code_runner/__pycache__/run_code.cpython-311.pyc
78
70
  pythonclaw/templates/skills/dev/github/SKILL.md
79
71
  pythonclaw/templates/skills/dev/github/gh.py
80
- pythonclaw/templates/skills/dev/github/__pycache__/gh.cpython-311.pyc
81
72
  pythonclaw/templates/skills/dev/http_request/SKILL.md
82
73
  pythonclaw/templates/skills/dev/http_request/request.py
83
- pythonclaw/templates/skills/dev/http_request/__pycache__/request.cpython-311.pyc
84
74
  pythonclaw/templates/skills/google/CATEGORY.md
85
75
  pythonclaw/templates/skills/google/workspace/SKILL.md
86
- pythonclaw/templates/skills/google/workspace/check_setup.sh
87
76
  pythonclaw/templates/skills/meta/CATEGORY.md
88
77
  pythonclaw/templates/skills/meta/skill_creator/SKILL.md
89
78
  pythonclaw/templates/skills/system/CATEGORY.md
90
79
  pythonclaw/templates/skills/system/change_persona/SKILL.md
91
80
  pythonclaw/templates/skills/system/change_setting/SKILL.md
92
81
  pythonclaw/templates/skills/system/change_setting/update_config.py
93
- pythonclaw/templates/skills/system/change_setting/__pycache__/update_config.cpython-311.pyc
94
82
  pythonclaw/templates/skills/system/change_soul/SKILL.md
95
83
  pythonclaw/templates/skills/system/onboarding/SKILL.md
96
84
  pythonclaw/templates/skills/system/onboarding/write_identity.py
97
- pythonclaw/templates/skills/system/onboarding/__pycache__/write_identity.cpython-311.pyc
98
85
  pythonclaw/templates/skills/system/random/SKILL.md
99
86
  pythonclaw/templates/skills/system/random/random_util.py
100
- pythonclaw/templates/skills/system/random/__pycache__/random_util.cpython-311.pyc
101
87
  pythonclaw/templates/skills/system/time/SKILL.md
102
88
  pythonclaw/templates/skills/system/time/time_util.py
103
- pythonclaw/templates/skills/system/time/__pycache__/time_util.cpython-311.pyc
104
89
  pythonclaw/templates/skills/text/CATEGORY.md
105
90
  pythonclaw/templates/skills/text/translator/SKILL.md
106
91
  pythonclaw/templates/skills/text/translator/translate.py
107
- pythonclaw/templates/skills/text/translator/__pycache__/translate.cpython-311.pyc
108
92
  pythonclaw/templates/skills/web/CATEGORY.md
109
93
  pythonclaw/templates/skills/web/tavily/SKILL.md
110
94
  pythonclaw/templates/soul/SOUL.md
@@ -1,145 +0,0 @@
1
- """
2
- Daemon server for PythonClaw — multi-channel mode.
3
-
4
- Supports Telegram and Discord channels, individually or combined.
5
-
6
- Architecture
7
- ------------
8
- +----------------------------------------+
9
- | SessionManager |
10
- | "{channel}:{id}" → Agent |
11
- | "cron:{job_id}" → Agent |
12
- | (Markdown-backed via SessionStore) |
13
- +----------------------------------------+
14
- |
15
- +--------------------+--------------------+
16
- | | |
17
- TelegramBot CronScheduler HeartbeatMonitor
18
- DiscordBot static + dynamic
19
- jobs
20
- """
21
-
22
- from __future__ import annotations
23
-
24
- import asyncio
25
- import logging
26
- import os
27
- import signal
28
-
29
- from .core.llm.base import LLMProvider
30
- from .core.persistent_agent import PersistentAgent
31
- from .core.session_store import SessionStore
32
- from .scheduler.cron import CronScheduler
33
- from .scheduler.heartbeat import create_heartbeat
34
- from .session_manager import SessionManager
35
-
36
- logger = logging.getLogger(__name__)
37
-
38
-
39
- async def run_server(
40
- provider: LLMProvider,
41
- channels: list[str] | None = None,
42
- ) -> None:
43
- """
44
- Main entry point for daemon mode.
45
-
46
- Parameters
47
- ----------
48
- provider : the LLM provider to use
49
- channels : list of channels to start, e.g. ["telegram", "discord"].
50
- Defaults to ["telegram"] for backward compatibility.
51
- """
52
- if channels is None:
53
- channels = ["telegram"]
54
-
55
- # ── 1. Session store (Markdown persistence) ───────────────────────────────
56
- store = SessionStore()
57
- logger.info("[Server] SessionStore initialised at '%s'", store.base_dir)
58
-
59
- # ── 2. SessionManager (placeholder factory, updated below) ────────────────
60
- session_manager = SessionManager(agent_factory=lambda sid: None, store=store)
61
-
62
- # ── 3. CronScheduler ─────────────────────────────────────────────────────
63
- jobs_path = os.path.join("context", "cron", "jobs.yaml")
64
- scheduler = CronScheduler(
65
- session_manager=session_manager,
66
- jobs_path=jobs_path,
67
- )
68
-
69
- # ── 4. Real agent factory ─────────────────────────────────────────────────
70
- def agent_factory(session_id: str) -> PersistentAgent:
71
- return PersistentAgent(
72
- provider=provider,
73
- store=store,
74
- session_id=session_id,
75
- cron_manager=scheduler,
76
- verbose=False,
77
- )
78
-
79
- session_manager.set_factory(agent_factory)
80
-
81
- # ── 5. Start channels ─────────────────────────────────────────────────────
82
- active_bots: list = []
83
-
84
- if "telegram" in channels:
85
- try:
86
- from .channels.telegram_bot import create_bot_from_env
87
- bot = create_bot_from_env(session_manager)
88
- scheduler._telegram_bot = bot
89
- await bot.start_async()
90
- active_bots.append(bot)
91
- logger.info("[Server] Telegram bot started.")
92
- except (ValueError, ImportError) as exc:
93
- logger.warning("[Server] Telegram skipped: %s", exc)
94
-
95
- if "discord" in channels:
96
- try:
97
- from .channels.discord_bot import create_bot_from_env as create_discord
98
- discord_bot = create_discord(session_manager)
99
- asyncio.create_task(discord_bot.start_async())
100
- active_bots.append(discord_bot)
101
- logger.info("[Server] Discord bot started.")
102
- except (ValueError, ImportError) as exc:
103
- logger.warning("[Server] Discord skipped: %s", exc)
104
-
105
- if not active_bots:
106
- logger.error("[Server] No channels started. Check your pythonclaw.json configuration.")
107
- return
108
-
109
- # ── 6. Start scheduler ────────────────────────────────────────────────────
110
- scheduler.start()
111
-
112
- # ── 7. Heartbeat monitor ──────────────────────────────────────────────────
113
- telegram_bot = next((b for b in active_bots if hasattr(b, '_app')), None)
114
- heartbeat = create_heartbeat(provider=provider, telegram_bot=telegram_bot)
115
- await heartbeat.start()
116
-
117
- logger.info("[Server] All subsystems running (%s). Press Ctrl-C to stop.",
118
- ", ".join(channels))
119
-
120
- # ── Graceful shutdown ─────────────────────────────────────────────────────
121
- stop_event = asyncio.Event()
122
-
123
- def _signal_handler() -> None:
124
- logger.info("[Server] Shutdown signal received.")
125
- stop_event.set()
126
-
127
- loop = asyncio.get_running_loop()
128
- for sig in (signal.SIGINT, signal.SIGTERM):
129
- try:
130
- loop.add_signal_handler(sig, _signal_handler)
131
- except (NotImplementedError, OSError):
132
- pass
133
-
134
- try:
135
- await stop_event.wait()
136
- except (KeyboardInterrupt, asyncio.CancelledError):
137
- pass
138
- finally:
139
- logger.info("[Server] Shutting down subsystems...")
140
- await heartbeat.stop()
141
- scheduler.stop()
142
- for bot in active_bots:
143
- if hasattr(bot, 'stop_async'):
144
- await bot.stop_async()
145
- logger.info("[Server] Shutdown complete.")
@@ -1,52 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Pre-activation check for the Google Workspace (gog) skill.
3
- # Exit 0 = ready, exit 1 = not ready (output tells the user what to fix).
4
-
5
- set -euo pipefail
6
-
7
- # ?? 1. Is gog installed? ??????????????????????????????????????????????????????
8
- if ! command -v gog &>/dev/null; then
9
- cat <<'EOF'
10
- ERROR: The 'gog' CLI is not installed.
11
-
12
- To install on macOS:
13
- brew install steipete/tap/gogcli
14
-
15
- To install on Linux:
16
- curl -fsSL https://api.github.com/repos/steipete/gogcli/releases/latest \
17
- | grep browser_download_url | grep linux_amd64
18
- # Download the tarball, extract, then: sudo install -m 0755 gog /usr/local/bin/gog
19
-
20
- More info: https://github.com/steipete/gogcli
21
- EOF
22
- exit 1
23
- fi
24
-
25
- echo "gog version: $(gog --version 2>/dev/null || echo 'unknown')"
26
-
27
- # ?? 2. Is at least one account configured? ????????????????????????????????????
28
- AUTH_OUTPUT=$(gog auth list 2>&1)
29
- if echo "$AUTH_OUTPUT" | grep -qi "no tokens"; then
30
- cat <<'EOF'
31
-
32
- ERROR: No Google account is configured in gog.
33
-
34
- Setup steps:
35
- 1. Go to https://console.cloud.google.com/apis/credentials
36
- 2. Create an OAuth 2.0 Client ID (Desktop App type)
37
- 3. Download the client_secret.json file
38
- 4. Run:
39
- gog auth credentials /path/to/client_secret.json
40
- gog auth add you@gmail.com --services gmail,calendar,drive,contacts,sheets,docs
41
- 5. A browser window will open ? authorize the app.
42
-
43
- After setup, try again.
44
- EOF
45
- exit 1
46
- fi
47
-
48
- echo ""
49
- echo "Configured accounts:"
50
- echo "$AUTH_OUTPUT"
51
- echo ""
52
- echo "Ready to use Google Workspace commands."
File without changes
File without changes
File without changes