pythonclaw 0.2.0__tar.gz → 0.2.2__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.
- {pythonclaw-0.2.0/pythonclaw.egg-info → pythonclaw-0.2.2}/PKG-INFO +5 -2
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/README.md +4 -1
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pyproject.toml +1 -1
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/__init__.py +1 -1
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/main.py +37 -19
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/onboard.py +48 -0
- pythonclaw-0.2.2/pythonclaw/server.py +125 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/web/app.py +86 -1
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/web/static/index.html +126 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2/pythonclaw.egg-info}/PKG-INFO +5 -2
- pythonclaw-0.2.0/pythonclaw/server.py +0 -145
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/LICENSE +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/__main__.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/channels/discord_bot.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/channels/telegram_bot.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/config.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/__init__.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/agent.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/compaction.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/knowledge/rag.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/llm/anthropic_client.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/llm/base.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/llm/gemini_client.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/llm/openai_compatible.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/llm/response.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/memory/manager.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/memory/storage.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/persistent_agent.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/retrieval/__init__.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/retrieval/chunker.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/retrieval/dense.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/retrieval/fusion.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/retrieval/reranker.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/retrieval/retriever.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/retrieval/sparse.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/session_store.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/skill_loader.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/skillhub.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/tools.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/core/utils.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/daemon.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/init.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/scheduler/cron.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/scheduler/heartbeat.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/session_manager.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/persona/demo_persona.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/communication/CATEGORY.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/communication/email/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/communication/email/__pycache__/send_email.cpython-311.pyc +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/communication/email/send_email.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/CATEGORY.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/csv_analyzer/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/csv_analyzer/__pycache__/analyze.cpython-311.pyc +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/csv_analyzer/analyze.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/finance/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/finance/__pycache__/fetch_quote.cpython-311.pyc +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/finance/fetch_quote.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/news/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/news/__pycache__/search_news.cpython-311.pyc +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/news/search_news.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/pdf_reader/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/pdf_reader/__pycache__/read_pdf.cpython-311.pyc +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/pdf_reader/read_pdf.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/scraper/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/scraper/__pycache__/scrape.cpython-311.pyc +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/scraper/scrape.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/weather/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/weather/__pycache__/weather.cpython-311.pyc +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/weather/weather.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/youtube/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/youtube/__pycache__/youtube_info.cpython-311.pyc +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/youtube/youtube_info.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/dev/CATEGORY.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/dev/code_runner/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/dev/code_runner/__pycache__/run_code.cpython-311.pyc +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/dev/code_runner/run_code.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/dev/github/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/dev/github/__pycache__/gh.cpython-311.pyc +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/dev/github/gh.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/dev/http_request/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/dev/http_request/__pycache__/request.cpython-311.pyc +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/dev/http_request/request.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/google/CATEGORY.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/google/workspace/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/google/workspace/check_setup.sh +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/meta/CATEGORY.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/meta/skill_creator/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/CATEGORY.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/change_persona/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/change_setting/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/change_setting/__pycache__/update_config.cpython-311.pyc +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/change_setting/update_config.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/change_soul/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/onboarding/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/onboarding/__pycache__/write_identity.cpython-311.pyc +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/onboarding/write_identity.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/random/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/random/__pycache__/random_util.cpython-311.pyc +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/random/random_util.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/time/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/time/__pycache__/time_util.cpython-311.pyc +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/time/time_util.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/text/CATEGORY.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/text/translator/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/text/translator/__pycache__/translate.cpython-311.pyc +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/text/translator/translate.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/web/CATEGORY.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/web/tavily/SKILL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/soul/SOUL.md +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/web/__init__.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/web/static/favicon.png +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/web/static/logo.png +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw.egg-info/SOURCES.txt +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw.egg-info/dependency_links.txt +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw.egg-info/entry_points.txt +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw.egg-info/requires.txt +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw.egg-info/top_level.txt +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/setup.cfg +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/tests/test_compaction.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/tests/test_persistence.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/tests/test_rag_hybrid.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/tests/test_skills.py +0 -0
- {pythonclaw-0.2.0 → pythonclaw-0.2.2}/tests/test_soul.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonclaw
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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
|
|
@@ -76,7 +76,10 @@ Dynamic: license-file
|
|
|
76
76
|
<a href="https://github.com/ericwang915/PythonClaw/actions/workflows/ci.yml">
|
|
77
77
|
<img src="https://github.com/ericwang915/PythonClaw/actions/workflows/ci.yml/badge.svg" alt="CI">
|
|
78
78
|
</a>
|
|
79
|
-
<
|
|
79
|
+
<a href="https://pypi.org/project/pythonclaw/">
|
|
80
|
+
<img src="https://img.shields.io/pypi/v/pythonclaw?color=blue" alt="PyPI">
|
|
81
|
+
</a>
|
|
82
|
+
<img src="https://img.shields.io/pypi/pyversions/pythonclaw" alt="Python">
|
|
80
83
|
<a href="LICENSE">
|
|
81
84
|
<img src="https://img.shields.io/github/license/ericwang915/PythonClaw" alt="MIT License">
|
|
82
85
|
</a>
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
<a href="https://github.com/ericwang915/PythonClaw/actions/workflows/ci.yml">
|
|
14
14
|
<img src="https://github.com/ericwang915/PythonClaw/actions/workflows/ci.yml/badge.svg" alt="CI">
|
|
15
15
|
</a>
|
|
16
|
-
<
|
|
16
|
+
<a href="https://pypi.org/project/pythonclaw/">
|
|
17
|
+
<img src="https://img.shields.io/pypi/v/pythonclaw?color=blue" alt="PyPI">
|
|
18
|
+
</a>
|
|
19
|
+
<img src="https://img.shields.io/pypi/pyversions/pythonclaw" alt="Python">
|
|
17
20
|
<a href="LICENSE">
|
|
18
21
|
<img src="https://img.shields.io/github/license/ericwang915/PythonClaw" alt="MIT License">
|
|
19
22
|
</a>
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pythonclaw"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
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"}
|
|
@@ -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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
print(
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
159
|
+
from .web.app import create_app
|
|
167
160
|
|
|
168
|
-
|
|
169
|
-
|
|
161
|
+
host = config.get_str("web", "host", default="0.0.0.0")
|
|
162
|
+
port = config.get_int("web", "port", default=7788)
|
|
170
163
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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:
|
|
@@ -214,6 +214,54 @@ def _optional_keys(cfg: dict) -> None:
|
|
|
214
214
|
print(" → SkillHub key set")
|
|
215
215
|
|
|
216
216
|
print()
|
|
217
|
+
_channel_keys(cfg)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _channel_keys(cfg: dict) -> None:
|
|
221
|
+
print(_c(" Channels (press Enter to skip):", _DIM))
|
|
222
|
+
print()
|
|
223
|
+
|
|
224
|
+
channels = cfg.setdefault("channels", {})
|
|
225
|
+
|
|
226
|
+
# Telegram
|
|
227
|
+
tg = channels.setdefault("telegram", {"token": "", "allowedUsers": []})
|
|
228
|
+
tg_existing = tg.get("token", "")
|
|
229
|
+
if tg_existing:
|
|
230
|
+
masked = tg_existing[:6] + "****" + tg_existing[-4:] if len(tg_existing) > 10 else "****"
|
|
231
|
+
print(f" Telegram Bot Token (current: {masked}, press Enter to keep)")
|
|
232
|
+
token = input(" Telegram Bot Token: ").strip()
|
|
233
|
+
if token:
|
|
234
|
+
tg["token"] = token
|
|
235
|
+
print(" → Telegram token set")
|
|
236
|
+
elif tg_existing:
|
|
237
|
+
print(" → Keeping existing Telegram token")
|
|
238
|
+
|
|
239
|
+
allowed = input(" Telegram Allowed User IDs (comma-separated, or Enter to allow all): ").strip()
|
|
240
|
+
if allowed:
|
|
241
|
+
tg["allowedUsers"] = [uid.strip() for uid in allowed.split(",") if uid.strip()]
|
|
242
|
+
print(f" → {len(tg['allowedUsers'])} user(s) whitelisted")
|
|
243
|
+
|
|
244
|
+
print()
|
|
245
|
+
|
|
246
|
+
# Discord
|
|
247
|
+
dc = channels.setdefault("discord", {"token": "", "allowedUsers": [], "allowedChannels": []})
|
|
248
|
+
dc_existing = dc.get("token", "")
|
|
249
|
+
if dc_existing:
|
|
250
|
+
masked = dc_existing[:6] + "****" + dc_existing[-4:] if len(dc_existing) > 10 else "****"
|
|
251
|
+
print(f" Discord Bot Token (current: {masked}, press Enter to keep)")
|
|
252
|
+
dc_token = input(" Discord Bot Token: ").strip()
|
|
253
|
+
if dc_token:
|
|
254
|
+
dc["token"] = dc_token
|
|
255
|
+
print(" → Discord token set")
|
|
256
|
+
elif dc_existing:
|
|
257
|
+
print(" → Keeping existing Discord token")
|
|
258
|
+
|
|
259
|
+
dc_channels = input(" Discord Allowed Channel IDs (comma-separated, or Enter to allow all): ").strip()
|
|
260
|
+
if dc_channels:
|
|
261
|
+
dc["allowedChannels"] = [ch.strip() for ch in dc_channels.split(",") if ch.strip()]
|
|
262
|
+
print(f" → {len(dc['allowedChannels'])} channel(s) whitelisted")
|
|
263
|
+
|
|
264
|
+
print()
|
|
217
265
|
|
|
218
266
|
|
|
219
267
|
def _validate_key(cfg: dict, provider: dict) -> 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
|
-
|
|
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 -->
|
|
@@ -266,6 +276,18 @@
|
|
|
266
276
|
</div>
|
|
267
277
|
</div>
|
|
268
278
|
|
|
279
|
+
<!-- Memory -->
|
|
280
|
+
<div class="section-card mb-6">
|
|
281
|
+
<h3 class="text-[.8125rem] font-semibold text-white mb-3 flex items-center gap-2">
|
|
282
|
+
<svg class="w-4 h-4 text-cyan-400" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24"><path d="M12 2a10 10 0 100 20 10 10 0 000-20z"/><path d="M12 6v6l4 2"/></svg>
|
|
283
|
+
Memory
|
|
284
|
+
<span id="memory-badge" class="text-[.625rem] text-gray-500 font-normal ml-1">0 entries</span>
|
|
285
|
+
</h3>
|
|
286
|
+
<div id="memory-content" class="max-h-[260px] overflow-y-auto chat-scroll">
|
|
287
|
+
<span class="text-[.75rem] text-gray-600 italic">No memories stored yet.</span>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
269
291
|
<!-- Tools & Quick Actions Row -->
|
|
270
292
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
|
271
293
|
<!-- Tools List -->
|
|
@@ -479,6 +501,33 @@
|
|
|
479
501
|
</div>
|
|
480
502
|
</div>
|
|
481
503
|
|
|
504
|
+
<!-- Channels -->
|
|
505
|
+
<div class="cfg-section">
|
|
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>
|
|
510
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4">
|
|
511
|
+
<div class="sm:col-span-2">
|
|
512
|
+
<label class="block text-xs text-gray-500 mb-1.5">Telegram Bot Token</label>
|
|
513
|
+
<input id="cfg-tg-token" type="password" class="input-field">
|
|
514
|
+
</div>
|
|
515
|
+
<div>
|
|
516
|
+
<label class="block text-xs text-gray-500 mb-1.5">Telegram Allowed Users</label>
|
|
517
|
+
<input id="cfg-tg-users" type="text" placeholder="Comma-separated user IDs (blank = allow all)" class="input-field">
|
|
518
|
+
</div>
|
|
519
|
+
<div class="sm:col-span-2 mt-2">
|
|
520
|
+
<label class="block text-xs text-gray-500 mb-1.5">Discord Bot Token</label>
|
|
521
|
+
<input id="cfg-dc-token" type="password" class="input-field">
|
|
522
|
+
</div>
|
|
523
|
+
<div>
|
|
524
|
+
<label class="block text-xs text-gray-500 mb-1.5">Discord Allowed Channels</label>
|
|
525
|
+
<input id="cfg-dc-channels" type="text" placeholder="Comma-separated channel IDs (blank = allow all)" class="input-field">
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
<p class="text-[.6875rem] text-gray-600 mt-3">Channels auto-start after saving. Tokens are stored securely.</p>
|
|
529
|
+
</div>
|
|
530
|
+
|
|
482
531
|
<!-- Web Dashboard -->
|
|
483
532
|
<div class="cfg-section">
|
|
484
533
|
<h3 class="cfg-title">Web Dashboard</h3>
|
|
@@ -601,6 +650,21 @@ async function refreshDashboard() {
|
|
|
601
650
|
banner.classList.toggle('hidden', d.providerReady !== false);
|
|
602
651
|
} catch (e) { console.error('Status fetch:', e); }
|
|
603
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
|
+
|
|
604
668
|
try {
|
|
605
669
|
const res2 = await fetch('/api/identity');
|
|
606
670
|
const id = await res2.json();
|
|
@@ -653,6 +717,29 @@ async function refreshDashboard() {
|
|
|
653
717
|
`).join('');
|
|
654
718
|
}
|
|
655
719
|
} catch (e) { console.error('Identity fetch:', e); }
|
|
720
|
+
|
|
721
|
+
try {
|
|
722
|
+
const res3 = await fetch('/api/memories');
|
|
723
|
+
const mem = await res3.json();
|
|
724
|
+
const container = document.getElementById('memory-content');
|
|
725
|
+
const badge = document.getElementById('memory-badge');
|
|
726
|
+
const entries = mem.memories || {};
|
|
727
|
+
const keys = Object.keys(entries);
|
|
728
|
+
badge.textContent = keys.length + ' entries';
|
|
729
|
+
|
|
730
|
+
if (keys.length === 0) {
|
|
731
|
+
container.innerHTML = '<span class="text-[.75rem] text-gray-600 italic">No memories stored yet. Chat with the agent to build memory.</span>';
|
|
732
|
+
} else {
|
|
733
|
+
container.innerHTML = '<div class="space-y-2">' + keys.map(k => {
|
|
734
|
+
const val = escapeHtml(String(entries[k]));
|
|
735
|
+
const preview = val.length > 200 ? val.substring(0, 200) + '...' : val;
|
|
736
|
+
return `<div class="px-3 py-2 rounded-lg bg-surface-800/50 border border-surface-700/30">
|
|
737
|
+
<div class="text-[.6875rem] text-cyan-400 font-medium mb-1">${escapeHtml(k)}</div>
|
|
738
|
+
<div class="text-[.75rem] text-gray-400 leading-relaxed whitespace-pre-wrap">${preview}</div>
|
|
739
|
+
</div>`;
|
|
740
|
+
}).join('') + '</div>';
|
|
741
|
+
}
|
|
742
|
+
} catch (e) { console.error('Memories fetch:', e); }
|
|
656
743
|
}
|
|
657
744
|
|
|
658
745
|
// ── Identity Editor ──────────────────────────────────────────────────────────
|
|
@@ -1008,12 +1095,38 @@ async function loadConfig() {
|
|
|
1008
1095
|
ghTokenField.placeholder = secretsSet['skills.github.token']
|
|
1009
1096
|
? '(token is set — leave blank to keep)' : 'ghp_xxxxxxxxxx';
|
|
1010
1097
|
|
|
1098
|
+
const channels = _rawConfig.channels || {};
|
|
1099
|
+
const tgCfg = channels.telegram || {};
|
|
1100
|
+
const tgTokenField = document.getElementById('cfg-tg-token');
|
|
1101
|
+
tgTokenField.value = '';
|
|
1102
|
+
tgTokenField.placeholder = secretsSet['channels.telegram.token'] ? '(token is set — leave blank to keep)' : 'Enter Telegram bot token';
|
|
1103
|
+
document.getElementById('cfg-tg-users').value = (tgCfg.allowedUsers || []).join(', ');
|
|
1104
|
+
|
|
1105
|
+
const dcCfg = channels.discord || {};
|
|
1106
|
+
const dcTokenField = document.getElementById('cfg-dc-token');
|
|
1107
|
+
dcTokenField.value = '';
|
|
1108
|
+
dcTokenField.placeholder = secretsSet['channels.discord.token'] ? '(token is set — leave blank to keep)' : 'Enter Discord bot token';
|
|
1109
|
+
document.getElementById('cfg-dc-channels').value = (dcCfg.allowedChannels || []).join(', ');
|
|
1110
|
+
|
|
1011
1111
|
const web = _rawConfig.web || {};
|
|
1012
1112
|
document.getElementById('cfg-web-host').value = web.host || '0.0.0.0';
|
|
1013
1113
|
document.getElementById('cfg-web-port').value = web.port || 7788;
|
|
1014
1114
|
|
|
1015
1115
|
document.getElementById('cfg-json-raw').value = JSON.stringify(_rawConfig, null, 2);
|
|
1016
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) {}
|
|
1017
1130
|
} catch (e) { console.error('Config fetch:', e); }
|
|
1018
1131
|
}
|
|
1019
1132
|
|
|
@@ -1090,6 +1203,19 @@ async function saveConfig() {
|
|
|
1090
1203
|
const ghToken = document.getElementById('cfg-github-token').value.trim();
|
|
1091
1204
|
if (ghToken) cfg.skills.github.token = ghToken;
|
|
1092
1205
|
|
|
1206
|
+
if (!cfg.channels) cfg.channels = {};
|
|
1207
|
+
if (!cfg.channels.telegram) cfg.channels.telegram = {};
|
|
1208
|
+
const tgToken = document.getElementById('cfg-tg-token').value.trim();
|
|
1209
|
+
if (tgToken) cfg.channels.telegram.token = tgToken;
|
|
1210
|
+
const tgUsers = document.getElementById('cfg-tg-users').value.trim();
|
|
1211
|
+
cfg.channels.telegram.allowedUsers = tgUsers ? tgUsers.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
1212
|
+
|
|
1213
|
+
if (!cfg.channels.discord) cfg.channels.discord = {};
|
|
1214
|
+
const dcToken = document.getElementById('cfg-dc-token').value.trim();
|
|
1215
|
+
if (dcToken) cfg.channels.discord.token = dcToken;
|
|
1216
|
+
const dcChannels = document.getElementById('cfg-dc-channels').value.trim();
|
|
1217
|
+
cfg.channels.discord.allowedChannels = dcChannels ? dcChannels.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
1218
|
+
|
|
1093
1219
|
if (!cfg.web) cfg.web = {};
|
|
1094
1220
|
cfg.web.host = document.getElementById('cfg-web-host').value.trim() || '0.0.0.0';
|
|
1095
1221
|
cfg.web.port = parseInt(document.getElementById('cfg-web-port').value) || 7788;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonclaw
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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
|
|
@@ -76,7 +76,10 @@ Dynamic: license-file
|
|
|
76
76
|
<a href="https://github.com/ericwang915/PythonClaw/actions/workflows/ci.yml">
|
|
77
77
|
<img src="https://github.com/ericwang915/PythonClaw/actions/workflows/ci.yml/badge.svg" alt="CI">
|
|
78
78
|
</a>
|
|
79
|
-
<
|
|
79
|
+
<a href="https://pypi.org/project/pythonclaw/">
|
|
80
|
+
<img src="https://img.shields.io/pypi/v/pythonclaw?color=blue" alt="PyPI">
|
|
81
|
+
</a>
|
|
82
|
+
<img src="https://img.shields.io/pypi/pyversions/pythonclaw" alt="Python">
|
|
80
83
|
<a href="LICENSE">
|
|
81
84
|
<img src="https://img.shields.io/github/license/ericwang915/PythonClaw" alt="MIT License">
|
|
82
85
|
</a>
|
|
@@ -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.")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/communication/email/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
{pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/communication/email/send_email.py
RENAMED
|
File without changes
|
|
File without changes
|
{pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/csv_analyzer/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
{pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/csv_analyzer/analyze.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/finance/fetch_quote.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/pdf_reader/read_pdf.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/data/youtube/youtube_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/dev/code_runner/run_code.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/dev/http_request/request.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/google/workspace/check_setup.sh
RENAMED
|
File without changes
|
|
File without changes
|
{pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/meta/skill_creator/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
{pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/change_persona/SKILL.md
RENAMED
|
File without changes
|
{pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/change_setting/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/change_soul/SKILL.md
RENAMED
|
File without changes
|
{pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/onboarding/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/system/random/random_util.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pythonclaw-0.2.0 → pythonclaw-0.2.2}/pythonclaw/templates/skills/text/translator/translate.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|