pythonclaw 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pythonclaw/__init__.py +17 -0
- pythonclaw/__main__.py +6 -0
- pythonclaw/channels/discord_bot.py +231 -0
- pythonclaw/channels/telegram_bot.py +236 -0
- pythonclaw/config.py +190 -0
- pythonclaw/core/__init__.py +25 -0
- pythonclaw/core/agent.py +773 -0
- pythonclaw/core/compaction.py +220 -0
- pythonclaw/core/knowledge/rag.py +93 -0
- pythonclaw/core/llm/anthropic_client.py +107 -0
- pythonclaw/core/llm/base.py +26 -0
- pythonclaw/core/llm/gemini_client.py +139 -0
- pythonclaw/core/llm/openai_compatible.py +39 -0
- pythonclaw/core/llm/response.py +57 -0
- pythonclaw/core/memory/manager.py +120 -0
- pythonclaw/core/memory/storage.py +164 -0
- pythonclaw/core/persistent_agent.py +103 -0
- pythonclaw/core/retrieval/__init__.py +6 -0
- pythonclaw/core/retrieval/chunker.py +78 -0
- pythonclaw/core/retrieval/dense.py +152 -0
- pythonclaw/core/retrieval/fusion.py +51 -0
- pythonclaw/core/retrieval/reranker.py +112 -0
- pythonclaw/core/retrieval/retriever.py +166 -0
- pythonclaw/core/retrieval/sparse.py +69 -0
- pythonclaw/core/session_store.py +269 -0
- pythonclaw/core/skill_loader.py +322 -0
- pythonclaw/core/skillhub.py +290 -0
- pythonclaw/core/tools.py +622 -0
- pythonclaw/core/utils.py +64 -0
- pythonclaw/daemon.py +221 -0
- pythonclaw/init.py +61 -0
- pythonclaw/main.py +489 -0
- pythonclaw/onboard.py +290 -0
- pythonclaw/scheduler/cron.py +310 -0
- pythonclaw/scheduler/heartbeat.py +178 -0
- pythonclaw/server.py +145 -0
- pythonclaw/session_manager.py +104 -0
- pythonclaw/templates/persona/demo_persona.md +2 -0
- pythonclaw/templates/skills/communication/CATEGORY.md +4 -0
- pythonclaw/templates/skills/communication/email/SKILL.md +54 -0
- pythonclaw/templates/skills/communication/email/__pycache__/send_email.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/communication/email/send_email.py +88 -0
- pythonclaw/templates/skills/data/CATEGORY.md +4 -0
- pythonclaw/templates/skills/data/csv_analyzer/SKILL.md +51 -0
- pythonclaw/templates/skills/data/csv_analyzer/__pycache__/analyze.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/csv_analyzer/analyze.py +138 -0
- pythonclaw/templates/skills/data/finance/SKILL.md +41 -0
- pythonclaw/templates/skills/data/finance/__pycache__/fetch_quote.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/finance/fetch_quote.py +118 -0
- pythonclaw/templates/skills/data/news/SKILL.md +39 -0
- pythonclaw/templates/skills/data/news/__pycache__/search_news.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/news/search_news.py +57 -0
- pythonclaw/templates/skills/data/pdf_reader/SKILL.md +40 -0
- pythonclaw/templates/skills/data/pdf_reader/__pycache__/read_pdf.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/pdf_reader/read_pdf.py +113 -0
- pythonclaw/templates/skills/data/scraper/SKILL.md +39 -0
- pythonclaw/templates/skills/data/scraper/__pycache__/scrape.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/scraper/scrape.py +92 -0
- pythonclaw/templates/skills/data/weather/SKILL.md +42 -0
- pythonclaw/templates/skills/data/weather/__pycache__/weather.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/weather/weather.py +142 -0
- pythonclaw/templates/skills/data/youtube/SKILL.md +43 -0
- pythonclaw/templates/skills/data/youtube/__pycache__/youtube_info.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/data/youtube/youtube_info.py +167 -0
- pythonclaw/templates/skills/dev/CATEGORY.md +4 -0
- pythonclaw/templates/skills/dev/code_runner/SKILL.md +46 -0
- pythonclaw/templates/skills/dev/code_runner/__pycache__/run_code.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/dev/code_runner/run_code.py +117 -0
- pythonclaw/templates/skills/dev/github/SKILL.md +52 -0
- pythonclaw/templates/skills/dev/github/__pycache__/gh.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/dev/github/gh.py +165 -0
- pythonclaw/templates/skills/dev/http_request/SKILL.md +40 -0
- pythonclaw/templates/skills/dev/http_request/__pycache__/request.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/dev/http_request/request.py +90 -0
- pythonclaw/templates/skills/google/CATEGORY.md +4 -0
- pythonclaw/templates/skills/google/workspace/SKILL.md +98 -0
- pythonclaw/templates/skills/google/workspace/check_setup.sh +52 -0
- pythonclaw/templates/skills/meta/CATEGORY.md +4 -0
- pythonclaw/templates/skills/meta/skill_creator/SKILL.md +151 -0
- pythonclaw/templates/skills/system/CATEGORY.md +4 -0
- pythonclaw/templates/skills/system/change_persona/SKILL.md +41 -0
- pythonclaw/templates/skills/system/change_setting/SKILL.md +65 -0
- pythonclaw/templates/skills/system/change_setting/__pycache__/update_config.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/system/change_setting/update_config.py +129 -0
- pythonclaw/templates/skills/system/change_soul/SKILL.md +41 -0
- pythonclaw/templates/skills/system/onboarding/SKILL.md +63 -0
- pythonclaw/templates/skills/system/onboarding/__pycache__/write_identity.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/system/onboarding/write_identity.py +218 -0
- pythonclaw/templates/skills/system/random/SKILL.md +33 -0
- pythonclaw/templates/skills/system/random/__pycache__/random_util.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/system/random/random_util.py +45 -0
- pythonclaw/templates/skills/system/time/SKILL.md +33 -0
- pythonclaw/templates/skills/system/time/__pycache__/time_util.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/system/time/time_util.py +81 -0
- pythonclaw/templates/skills/text/CATEGORY.md +4 -0
- pythonclaw/templates/skills/text/translator/SKILL.md +47 -0
- pythonclaw/templates/skills/text/translator/__pycache__/translate.cpython-311.pyc +0 -0
- pythonclaw/templates/skills/text/translator/translate.py +66 -0
- pythonclaw/templates/skills/web/CATEGORY.md +4 -0
- pythonclaw/templates/skills/web/tavily/SKILL.md +61 -0
- pythonclaw/templates/soul/SOUL.md +54 -0
- pythonclaw/web/__init__.py +1 -0
- pythonclaw/web/app.py +585 -0
- pythonclaw/web/static/favicon.png +0 -0
- pythonclaw/web/static/index.html +1318 -0
- pythonclaw/web/static/logo.png +0 -0
- pythonclaw-0.2.0.dist-info/METADATA +410 -0
- pythonclaw-0.2.0.dist-info/RECORD +112 -0
- pythonclaw-0.2.0.dist-info/WHEEL +5 -0
- pythonclaw-0.2.0.dist-info/entry_points.txt +2 -0
- pythonclaw-0.2.0.dist-info/licenses/LICENSE +21 -0
- pythonclaw-0.2.0.dist-info/top_level.txt +1 -0
pythonclaw/main.py
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PythonClaw CLI — entry point.
|
|
3
|
+
|
|
4
|
+
Subcommands
|
|
5
|
+
-----------
|
|
6
|
+
onboard Interactive first-time setup wizard
|
|
7
|
+
start Start the agent daemon (web dashboard + optional channels)
|
|
8
|
+
stop Stop the running daemon
|
|
9
|
+
status Show daemon status
|
|
10
|
+
chat Interactive CLI chat (foreground)
|
|
11
|
+
skill SkillHub marketplace (search / browse / install / info)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import asyncio
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
from . import config
|
|
19
|
+
from .core.persistent_agent import PersistentAgent
|
|
20
|
+
from .core.session_store import SessionStore
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ── Provider builder ─────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
def _build_provider():
|
|
26
|
+
"""Instantiate the LLM provider selected by config."""
|
|
27
|
+
provider_name = config.get_str(
|
|
28
|
+
"llm", "provider", env="LLM_PROVIDER", default="deepseek"
|
|
29
|
+
).lower()
|
|
30
|
+
|
|
31
|
+
if provider_name == "deepseek":
|
|
32
|
+
from .core.llm.openai_compatible import OpenAICompatibleProvider
|
|
33
|
+
api_key = config.get_str("llm", "deepseek", "apiKey", env="DEEPSEEK_API_KEY")
|
|
34
|
+
if not api_key:
|
|
35
|
+
raise ValueError("DEEPSEEK_API_KEY not set (env or pythonclaw.json)")
|
|
36
|
+
return OpenAICompatibleProvider(
|
|
37
|
+
api_key=api_key,
|
|
38
|
+
base_url=config.get_str(
|
|
39
|
+
"llm", "deepseek", "baseUrl", default="https://api.deepseek.com/v1",
|
|
40
|
+
),
|
|
41
|
+
model_name=config.get_str(
|
|
42
|
+
"llm", "deepseek", "model", default="deepseek-chat",
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if provider_name == "grok":
|
|
47
|
+
from .core.llm.openai_compatible import OpenAICompatibleProvider
|
|
48
|
+
api_key = config.get_str("llm", "grok", "apiKey", env="GROK_API_KEY")
|
|
49
|
+
if not api_key:
|
|
50
|
+
raise ValueError("GROK_API_KEY not set (env or pythonclaw.json)")
|
|
51
|
+
return OpenAICompatibleProvider(
|
|
52
|
+
api_key=api_key,
|
|
53
|
+
base_url=config.get_str(
|
|
54
|
+
"llm", "grok", "baseUrl", default="https://api.x.ai/v1",
|
|
55
|
+
),
|
|
56
|
+
model_name=config.get_str(
|
|
57
|
+
"llm", "grok", "model", default="grok-3",
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if provider_name in ("claude", "anthropic"):
|
|
62
|
+
from .core.llm.anthropic_client import AnthropicProvider
|
|
63
|
+
api_key = config.get_str("llm", "claude", "apiKey", env="ANTHROPIC_API_KEY")
|
|
64
|
+
if not api_key:
|
|
65
|
+
raise ValueError("ANTHROPIC_API_KEY not set (env or pythonclaw.json)")
|
|
66
|
+
return AnthropicProvider(api_key=api_key)
|
|
67
|
+
|
|
68
|
+
if provider_name == "gemini":
|
|
69
|
+
from .core.llm.gemini_client import GeminiProvider
|
|
70
|
+
api_key = config.get_str("llm", "gemini", "apiKey", env="GEMINI_API_KEY")
|
|
71
|
+
if not api_key:
|
|
72
|
+
raise ValueError("GEMINI_API_KEY not set (env or pythonclaw.json)")
|
|
73
|
+
return GeminiProvider(api_key=api_key)
|
|
74
|
+
|
|
75
|
+
if provider_name in ("kimi", "moonshot"):
|
|
76
|
+
from .core.llm.openai_compatible import OpenAICompatibleProvider
|
|
77
|
+
api_key = config.get_str("llm", "kimi", "apiKey", env="KIMI_API_KEY")
|
|
78
|
+
if not api_key:
|
|
79
|
+
raise ValueError("KIMI_API_KEY not set (env or pythonclaw.json)")
|
|
80
|
+
return OpenAICompatibleProvider(
|
|
81
|
+
api_key=api_key,
|
|
82
|
+
base_url=config.get_str(
|
|
83
|
+
"llm", "kimi", "baseUrl", default="https://api.moonshot.cn/v1",
|
|
84
|
+
),
|
|
85
|
+
model_name=config.get_str(
|
|
86
|
+
"llm", "kimi", "model", env="KIMI_MODEL", default="moonshot-v1-128k",
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if provider_name in ("glm", "zhipu", "chatglm"):
|
|
91
|
+
from .core.llm.openai_compatible import OpenAICompatibleProvider
|
|
92
|
+
api_key = config.get_str("llm", "glm", "apiKey", env="GLM_API_KEY")
|
|
93
|
+
if not api_key:
|
|
94
|
+
raise ValueError("GLM_API_KEY not set (env or pythonclaw.json)")
|
|
95
|
+
return OpenAICompatibleProvider(
|
|
96
|
+
api_key=api_key,
|
|
97
|
+
base_url=config.get_str(
|
|
98
|
+
"llm", "glm", "baseUrl",
|
|
99
|
+
default="https://open.bigmodel.cn/api/paas/v4/",
|
|
100
|
+
),
|
|
101
|
+
model_name=config.get_str(
|
|
102
|
+
"llm", "glm", "model", env="GLM_MODEL", default="glm-4-flash",
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
raise ValueError(f"Unknown LLM_PROVIDER: '{provider_name}'")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ── Ensure config is ready (auto-onboard if needed) ─────────────────────────
|
|
110
|
+
|
|
111
|
+
def _ensure_configured(config_path: str | None = None) -> None:
|
|
112
|
+
"""If no API key is configured, run the onboard wizard first."""
|
|
113
|
+
from .onboard import needs_onboard, run_onboard
|
|
114
|
+
|
|
115
|
+
if needs_onboard(config_path):
|
|
116
|
+
print("[PythonClaw] No LLM provider configured. Starting setup wizard...\n")
|
|
117
|
+
run_onboard(config_path)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ── Subcommand handlers ─────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
def _cmd_onboard(args) -> None:
|
|
123
|
+
from .onboard import run_onboard
|
|
124
|
+
run_onboard(args.config)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _cmd_start(args) -> None:
|
|
128
|
+
_ensure_configured(args.config)
|
|
129
|
+
|
|
130
|
+
if args.foreground:
|
|
131
|
+
_run_foreground(args)
|
|
132
|
+
else:
|
|
133
|
+
from .daemon import start_daemon
|
|
134
|
+
start_daemon(channels=args.channels, config_path=args.config)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _run_foreground(args) -> None:
|
|
138
|
+
"""Run the web server (+ optional channels) in the foreground."""
|
|
139
|
+
provider = None
|
|
140
|
+
try:
|
|
141
|
+
provider = _build_provider()
|
|
142
|
+
except Exception as exc:
|
|
143
|
+
print(f"[PythonClaw] Warning: LLM provider not configured ({exc})")
|
|
144
|
+
|
|
145
|
+
channels = args.channels or []
|
|
146
|
+
|
|
147
|
+
logging.basicConfig(
|
|
148
|
+
level=logging.INFO,
|
|
149
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
150
|
+
)
|
|
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
|
|
165
|
+
|
|
166
|
+
from .web.app import create_app
|
|
167
|
+
|
|
168
|
+
host = config.get_str("web", "host", default="0.0.0.0")
|
|
169
|
+
port = config.get_int("web", "port", default=7788)
|
|
170
|
+
|
|
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")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _cmd_stop(args) -> None:
|
|
177
|
+
from .daemon import stop_daemon
|
|
178
|
+
stop_daemon()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _cmd_status(args) -> None:
|
|
182
|
+
from .daemon import print_status
|
|
183
|
+
print_status()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _cmd_chat(args) -> None:
|
|
187
|
+
_ensure_configured(args.config)
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
provider = _build_provider()
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
print(f"Error: {exc}")
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
provider_name = config.get_str("llm", "provider", env="LLM_PROVIDER", default="deepseek")
|
|
196
|
+
verbose = config.get("agent", "verbose", default=True)
|
|
197
|
+
|
|
198
|
+
store = SessionStore()
|
|
199
|
+
session_id = "cli"
|
|
200
|
+
|
|
201
|
+
print(f"Initializing Agent with Provider: {provider_name.upper()}...")
|
|
202
|
+
agent = PersistentAgent(
|
|
203
|
+
provider=provider,
|
|
204
|
+
verbose=bool(verbose),
|
|
205
|
+
store=store,
|
|
206
|
+
session_id=session_id,
|
|
207
|
+
)
|
|
208
|
+
print(f"Loaded {len(agent.loaded_skill_names)} active skills.")
|
|
209
|
+
|
|
210
|
+
restored = len(agent.messages) - 1
|
|
211
|
+
if restored > 0:
|
|
212
|
+
print(f"Restored {restored} messages from previous session.")
|
|
213
|
+
|
|
214
|
+
cfg_path = config.config_path()
|
|
215
|
+
cfg_source = f" (config: {cfg_path})" if cfg_path else ""
|
|
216
|
+
print("\n--- PythonClaw Agent ---")
|
|
217
|
+
print(f"Provider: {provider_name}{cfg_source}")
|
|
218
|
+
print(f"Session: {store._path(session_id)}")
|
|
219
|
+
print("Commands: 'exit' to quit | '/compact [hint]' | '/status' | '/clear'")
|
|
220
|
+
|
|
221
|
+
while True:
|
|
222
|
+
try:
|
|
223
|
+
user_input = input("You: ").strip()
|
|
224
|
+
if not user_input:
|
|
225
|
+
continue
|
|
226
|
+
if user_input.lower() in ("exit", "quit"):
|
|
227
|
+
break
|
|
228
|
+
|
|
229
|
+
if user_input.startswith("/compact"):
|
|
230
|
+
hint = user_input[len("/compact"):].strip() or None
|
|
231
|
+
result = agent.compact(instruction=hint)
|
|
232
|
+
print(f"Bot: {result}")
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
if user_input == "/status":
|
|
236
|
+
memory_count = len(agent.memory.list_all())
|
|
237
|
+
print(
|
|
238
|
+
f"Bot: Session Status\n"
|
|
239
|
+
f" Provider : {type(agent.provider).__name__}\n"
|
|
240
|
+
f" Skills : {len(agent.loaded_skill_names)} loaded\n"
|
|
241
|
+
f" Memories : {memory_count} entries\n"
|
|
242
|
+
f" History : {len(agent.messages)} messages\n"
|
|
243
|
+
f" Compactions : {agent.compaction_count}\n"
|
|
244
|
+
f" Session File : {store._path(session_id)}"
|
|
245
|
+
)
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
if user_input == "/clear":
|
|
249
|
+
store.delete(session_id)
|
|
250
|
+
agent.clear_history()
|
|
251
|
+
print("Bot: Chat history cleared. Agent is still active with all skills and memory intact.")
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
response = agent.chat(user_input)
|
|
255
|
+
print(f"Bot: {response}")
|
|
256
|
+
except KeyboardInterrupt:
|
|
257
|
+
print("\nExiting...")
|
|
258
|
+
break
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _cmd_skill(args) -> None:
|
|
262
|
+
from .core import skillhub
|
|
263
|
+
|
|
264
|
+
action = args.skill_action
|
|
265
|
+
if not action:
|
|
266
|
+
print("Usage: pythonclaw skill {search,browse,install,info}")
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
if action == "search":
|
|
270
|
+
query = " ".join(args.query)
|
|
271
|
+
if not query:
|
|
272
|
+
print("Usage: pythonclaw skill search <query>")
|
|
273
|
+
return
|
|
274
|
+
print(f"Searching SkillHub for: {query} ...")
|
|
275
|
+
try:
|
|
276
|
+
results = skillhub.search(query, limit=args.limit or 10)
|
|
277
|
+
print(skillhub.format_search_results(results))
|
|
278
|
+
except RuntimeError as exc:
|
|
279
|
+
print(f"Error: {exc}")
|
|
280
|
+
|
|
281
|
+
elif action == "browse":
|
|
282
|
+
print("Browsing SkillHub catalog ...")
|
|
283
|
+
try:
|
|
284
|
+
results = skillhub.browse(limit=args.limit or 20, sort=args.sort or "score")
|
|
285
|
+
print(skillhub.format_search_results(results))
|
|
286
|
+
except RuntimeError as exc:
|
|
287
|
+
print(f"Error: {exc}")
|
|
288
|
+
|
|
289
|
+
elif action == "install":
|
|
290
|
+
skill_id = args.skill_id
|
|
291
|
+
if not skill_id:
|
|
292
|
+
print("Usage: pythonclaw skill install <skill-id>")
|
|
293
|
+
return
|
|
294
|
+
print(f"Installing skill: {skill_id} ...")
|
|
295
|
+
try:
|
|
296
|
+
path = skillhub.install_skill(skill_id)
|
|
297
|
+
print(f"Installed to: {path}")
|
|
298
|
+
print("The skill will be available next time the agent starts.")
|
|
299
|
+
except RuntimeError as exc:
|
|
300
|
+
print(f"Error: {exc}")
|
|
301
|
+
|
|
302
|
+
elif action == "info":
|
|
303
|
+
skill_id = args.skill_id
|
|
304
|
+
if not skill_id:
|
|
305
|
+
print("Usage: pythonclaw skill info <skill-id>")
|
|
306
|
+
return
|
|
307
|
+
print(f"Fetching skill detail: {skill_id} ...")
|
|
308
|
+
try:
|
|
309
|
+
detail = skillhub.get_skill_detail(skill_id)
|
|
310
|
+
if not detail:
|
|
311
|
+
print("Skill not found.")
|
|
312
|
+
return
|
|
313
|
+
print(f"\n Name: {detail.get('name', '?')}")
|
|
314
|
+
print(f" ID: {detail.get('id', skill_id)}")
|
|
315
|
+
if detail.get("description"):
|
|
316
|
+
print(f" Desc: {detail['description']}")
|
|
317
|
+
if detail.get("source_url"):
|
|
318
|
+
print(f" URL: {detail['source_url']}")
|
|
319
|
+
if detail.get("skill_md"):
|
|
320
|
+
print(f"\n--- SKILL.md Preview ---\n{detail['skill_md'][:500]}")
|
|
321
|
+
except RuntimeError as exc:
|
|
322
|
+
print(f"Error: {exc}")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ── Argument parser ──────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
328
|
+
parser = argparse.ArgumentParser(
|
|
329
|
+
prog="pythonclaw",
|
|
330
|
+
description="PythonClaw — Autonomous AI Agent Framework",
|
|
331
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
332
|
+
epilog=(
|
|
333
|
+
"Quick start:\n"
|
|
334
|
+
" pythonclaw onboard Set up your LLM provider\n"
|
|
335
|
+
" pythonclaw start Start the agent daemon\n"
|
|
336
|
+
" pythonclaw chat Interactive CLI chat\n"
|
|
337
|
+
"\n"
|
|
338
|
+
"Docs: https://github.com/ericwang915/PythonClaw"
|
|
339
|
+
),
|
|
340
|
+
)
|
|
341
|
+
parser.add_argument(
|
|
342
|
+
"--config",
|
|
343
|
+
metavar="PATH",
|
|
344
|
+
default=None,
|
|
345
|
+
help="Path to pythonclaw.json config file.",
|
|
346
|
+
)
|
|
347
|
+
# Hidden --mode for backward compat
|
|
348
|
+
parser.add_argument("--mode", default=None, help=argparse.SUPPRESS)
|
|
349
|
+
parser.add_argument("--channels", nargs="+", default=None, help=argparse.SUPPRESS)
|
|
350
|
+
|
|
351
|
+
sub = parser.add_subparsers(dest="command")
|
|
352
|
+
|
|
353
|
+
# onboard
|
|
354
|
+
sub.add_parser("onboard", help="Interactive first-time setup wizard")
|
|
355
|
+
|
|
356
|
+
# start
|
|
357
|
+
sp_start = sub.add_parser("start", help="Start the agent daemon")
|
|
358
|
+
sp_start.add_argument(
|
|
359
|
+
"--foreground", "-f", action="store_true",
|
|
360
|
+
help="Run in foreground (don't daemonize)",
|
|
361
|
+
)
|
|
362
|
+
sp_start.add_argument(
|
|
363
|
+
"--channels", nargs="+",
|
|
364
|
+
choices=["telegram", "discord"],
|
|
365
|
+
help="Also start messaging channels",
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# stop
|
|
369
|
+
sub.add_parser("stop", help="Stop the running daemon")
|
|
370
|
+
|
|
371
|
+
# status
|
|
372
|
+
sub.add_parser("status", help="Show daemon status")
|
|
373
|
+
|
|
374
|
+
# chat
|
|
375
|
+
sub.add_parser("chat", help="Interactive CLI chat (foreground)")
|
|
376
|
+
|
|
377
|
+
# skill
|
|
378
|
+
skill_parser = sub.add_parser("skill", help="SkillHub marketplace commands")
|
|
379
|
+
skill_sub = skill_parser.add_subparsers(dest="skill_action")
|
|
380
|
+
|
|
381
|
+
sp_search = skill_sub.add_parser("search", help="Search skills on SkillHub")
|
|
382
|
+
sp_search.add_argument("query", nargs="+", help="Search query")
|
|
383
|
+
sp_search.add_argument("--limit", type=int, default=10, help="Max results")
|
|
384
|
+
|
|
385
|
+
sp_browse = skill_sub.add_parser("browse", help="Browse SkillHub catalog")
|
|
386
|
+
sp_browse.add_argument("--limit", type=int, default=20, help="Max results")
|
|
387
|
+
sp_browse.add_argument("--sort", default="score",
|
|
388
|
+
choices=["score", "stars", "recent", "composite"])
|
|
389
|
+
|
|
390
|
+
sp_install = skill_sub.add_parser("install", help="Install a skill from SkillHub")
|
|
391
|
+
sp_install.add_argument("skill_id", help="Skill ID (from search results)")
|
|
392
|
+
|
|
393
|
+
sp_info = skill_sub.add_parser("info", help="Show details for a SkillHub skill")
|
|
394
|
+
sp_info.add_argument("skill_id", help="Skill ID")
|
|
395
|
+
|
|
396
|
+
return parser
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# ── Backward-compat --mode handler ───────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
def _handle_legacy_mode(args) -> None:
|
|
402
|
+
"""Support the old ``--mode cli|web|telegram|discord`` flags."""
|
|
403
|
+
mode = args.mode
|
|
404
|
+
channels_arg = getattr(args, "channels", None)
|
|
405
|
+
|
|
406
|
+
if channels_arg:
|
|
407
|
+
try:
|
|
408
|
+
provider = _build_provider()
|
|
409
|
+
except Exception as exc:
|
|
410
|
+
print(f"Error: {exc}")
|
|
411
|
+
return
|
|
412
|
+
from .server import run_server
|
|
413
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
|
414
|
+
asyncio.run(run_server(provider, channels=channels_arg))
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
if mode == "web":
|
|
418
|
+
provider = None
|
|
419
|
+
try:
|
|
420
|
+
provider = _build_provider()
|
|
421
|
+
except Exception as exc:
|
|
422
|
+
print(f"[PythonClaw] Warning: LLM provider not configured ({exc})")
|
|
423
|
+
try:
|
|
424
|
+
import uvicorn
|
|
425
|
+
except ImportError:
|
|
426
|
+
print("Error: pip install pythonclaw[web]")
|
|
427
|
+
return
|
|
428
|
+
from .web.app import create_app
|
|
429
|
+
host = config.get_str("web", "host", default="0.0.0.0")
|
|
430
|
+
port = config.get_int("web", "port", default=7788)
|
|
431
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
|
432
|
+
app = create_app(provider, build_provider_fn=_build_provider)
|
|
433
|
+
uvicorn.run(app, host=host, port=port, log_level="info")
|
|
434
|
+
return
|
|
435
|
+
|
|
436
|
+
if mode in ("telegram", "discord"):
|
|
437
|
+
try:
|
|
438
|
+
provider = _build_provider()
|
|
439
|
+
except Exception as exc:
|
|
440
|
+
print(f"Error: {exc}")
|
|
441
|
+
return
|
|
442
|
+
from .server import run_server
|
|
443
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
|
444
|
+
asyncio.run(run_server(provider, channels=[mode]))
|
|
445
|
+
return
|
|
446
|
+
|
|
447
|
+
# Default: cli
|
|
448
|
+
try:
|
|
449
|
+
provider = _build_provider()
|
|
450
|
+
except Exception as exc:
|
|
451
|
+
print(f"Error: {exc}")
|
|
452
|
+
return
|
|
453
|
+
_cmd_chat(args)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# ── Entry point ──────────────────────────────────────────────────────────────
|
|
457
|
+
|
|
458
|
+
def main():
|
|
459
|
+
config.load()
|
|
460
|
+
|
|
461
|
+
parser = _build_parser()
|
|
462
|
+
args = parser.parse_args()
|
|
463
|
+
|
|
464
|
+
if args.config:
|
|
465
|
+
config.load(args.config, force=True)
|
|
466
|
+
|
|
467
|
+
# Handle legacy --mode flag
|
|
468
|
+
if args.mode and not args.command:
|
|
469
|
+
_handle_legacy_mode(args)
|
|
470
|
+
return
|
|
471
|
+
|
|
472
|
+
dispatch = {
|
|
473
|
+
"onboard": _cmd_onboard,
|
|
474
|
+
"start": _cmd_start,
|
|
475
|
+
"stop": _cmd_stop,
|
|
476
|
+
"status": _cmd_status,
|
|
477
|
+
"chat": _cmd_chat,
|
|
478
|
+
"skill": _cmd_skill,
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
handler = dispatch.get(args.command)
|
|
482
|
+
if handler:
|
|
483
|
+
handler(args)
|
|
484
|
+
else:
|
|
485
|
+
parser.print_help()
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
if __name__ == "__main__":
|
|
489
|
+
main()
|