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.
Files changed (112) hide show
  1. pythonclaw/__init__.py +17 -0
  2. pythonclaw/__main__.py +6 -0
  3. pythonclaw/channels/discord_bot.py +231 -0
  4. pythonclaw/channels/telegram_bot.py +236 -0
  5. pythonclaw/config.py +190 -0
  6. pythonclaw/core/__init__.py +25 -0
  7. pythonclaw/core/agent.py +773 -0
  8. pythonclaw/core/compaction.py +220 -0
  9. pythonclaw/core/knowledge/rag.py +93 -0
  10. pythonclaw/core/llm/anthropic_client.py +107 -0
  11. pythonclaw/core/llm/base.py +26 -0
  12. pythonclaw/core/llm/gemini_client.py +139 -0
  13. pythonclaw/core/llm/openai_compatible.py +39 -0
  14. pythonclaw/core/llm/response.py +57 -0
  15. pythonclaw/core/memory/manager.py +120 -0
  16. pythonclaw/core/memory/storage.py +164 -0
  17. pythonclaw/core/persistent_agent.py +103 -0
  18. pythonclaw/core/retrieval/__init__.py +6 -0
  19. pythonclaw/core/retrieval/chunker.py +78 -0
  20. pythonclaw/core/retrieval/dense.py +152 -0
  21. pythonclaw/core/retrieval/fusion.py +51 -0
  22. pythonclaw/core/retrieval/reranker.py +112 -0
  23. pythonclaw/core/retrieval/retriever.py +166 -0
  24. pythonclaw/core/retrieval/sparse.py +69 -0
  25. pythonclaw/core/session_store.py +269 -0
  26. pythonclaw/core/skill_loader.py +322 -0
  27. pythonclaw/core/skillhub.py +290 -0
  28. pythonclaw/core/tools.py +622 -0
  29. pythonclaw/core/utils.py +64 -0
  30. pythonclaw/daemon.py +221 -0
  31. pythonclaw/init.py +61 -0
  32. pythonclaw/main.py +489 -0
  33. pythonclaw/onboard.py +290 -0
  34. pythonclaw/scheduler/cron.py +310 -0
  35. pythonclaw/scheduler/heartbeat.py +178 -0
  36. pythonclaw/server.py +145 -0
  37. pythonclaw/session_manager.py +104 -0
  38. pythonclaw/templates/persona/demo_persona.md +2 -0
  39. pythonclaw/templates/skills/communication/CATEGORY.md +4 -0
  40. pythonclaw/templates/skills/communication/email/SKILL.md +54 -0
  41. pythonclaw/templates/skills/communication/email/__pycache__/send_email.cpython-311.pyc +0 -0
  42. pythonclaw/templates/skills/communication/email/send_email.py +88 -0
  43. pythonclaw/templates/skills/data/CATEGORY.md +4 -0
  44. pythonclaw/templates/skills/data/csv_analyzer/SKILL.md +51 -0
  45. pythonclaw/templates/skills/data/csv_analyzer/__pycache__/analyze.cpython-311.pyc +0 -0
  46. pythonclaw/templates/skills/data/csv_analyzer/analyze.py +138 -0
  47. pythonclaw/templates/skills/data/finance/SKILL.md +41 -0
  48. pythonclaw/templates/skills/data/finance/__pycache__/fetch_quote.cpython-311.pyc +0 -0
  49. pythonclaw/templates/skills/data/finance/fetch_quote.py +118 -0
  50. pythonclaw/templates/skills/data/news/SKILL.md +39 -0
  51. pythonclaw/templates/skills/data/news/__pycache__/search_news.cpython-311.pyc +0 -0
  52. pythonclaw/templates/skills/data/news/search_news.py +57 -0
  53. pythonclaw/templates/skills/data/pdf_reader/SKILL.md +40 -0
  54. pythonclaw/templates/skills/data/pdf_reader/__pycache__/read_pdf.cpython-311.pyc +0 -0
  55. pythonclaw/templates/skills/data/pdf_reader/read_pdf.py +113 -0
  56. pythonclaw/templates/skills/data/scraper/SKILL.md +39 -0
  57. pythonclaw/templates/skills/data/scraper/__pycache__/scrape.cpython-311.pyc +0 -0
  58. pythonclaw/templates/skills/data/scraper/scrape.py +92 -0
  59. pythonclaw/templates/skills/data/weather/SKILL.md +42 -0
  60. pythonclaw/templates/skills/data/weather/__pycache__/weather.cpython-311.pyc +0 -0
  61. pythonclaw/templates/skills/data/weather/weather.py +142 -0
  62. pythonclaw/templates/skills/data/youtube/SKILL.md +43 -0
  63. pythonclaw/templates/skills/data/youtube/__pycache__/youtube_info.cpython-311.pyc +0 -0
  64. pythonclaw/templates/skills/data/youtube/youtube_info.py +167 -0
  65. pythonclaw/templates/skills/dev/CATEGORY.md +4 -0
  66. pythonclaw/templates/skills/dev/code_runner/SKILL.md +46 -0
  67. pythonclaw/templates/skills/dev/code_runner/__pycache__/run_code.cpython-311.pyc +0 -0
  68. pythonclaw/templates/skills/dev/code_runner/run_code.py +117 -0
  69. pythonclaw/templates/skills/dev/github/SKILL.md +52 -0
  70. pythonclaw/templates/skills/dev/github/__pycache__/gh.cpython-311.pyc +0 -0
  71. pythonclaw/templates/skills/dev/github/gh.py +165 -0
  72. pythonclaw/templates/skills/dev/http_request/SKILL.md +40 -0
  73. pythonclaw/templates/skills/dev/http_request/__pycache__/request.cpython-311.pyc +0 -0
  74. pythonclaw/templates/skills/dev/http_request/request.py +90 -0
  75. pythonclaw/templates/skills/google/CATEGORY.md +4 -0
  76. pythonclaw/templates/skills/google/workspace/SKILL.md +98 -0
  77. pythonclaw/templates/skills/google/workspace/check_setup.sh +52 -0
  78. pythonclaw/templates/skills/meta/CATEGORY.md +4 -0
  79. pythonclaw/templates/skills/meta/skill_creator/SKILL.md +151 -0
  80. pythonclaw/templates/skills/system/CATEGORY.md +4 -0
  81. pythonclaw/templates/skills/system/change_persona/SKILL.md +41 -0
  82. pythonclaw/templates/skills/system/change_setting/SKILL.md +65 -0
  83. pythonclaw/templates/skills/system/change_setting/__pycache__/update_config.cpython-311.pyc +0 -0
  84. pythonclaw/templates/skills/system/change_setting/update_config.py +129 -0
  85. pythonclaw/templates/skills/system/change_soul/SKILL.md +41 -0
  86. pythonclaw/templates/skills/system/onboarding/SKILL.md +63 -0
  87. pythonclaw/templates/skills/system/onboarding/__pycache__/write_identity.cpython-311.pyc +0 -0
  88. pythonclaw/templates/skills/system/onboarding/write_identity.py +218 -0
  89. pythonclaw/templates/skills/system/random/SKILL.md +33 -0
  90. pythonclaw/templates/skills/system/random/__pycache__/random_util.cpython-311.pyc +0 -0
  91. pythonclaw/templates/skills/system/random/random_util.py +45 -0
  92. pythonclaw/templates/skills/system/time/SKILL.md +33 -0
  93. pythonclaw/templates/skills/system/time/__pycache__/time_util.cpython-311.pyc +0 -0
  94. pythonclaw/templates/skills/system/time/time_util.py +81 -0
  95. pythonclaw/templates/skills/text/CATEGORY.md +4 -0
  96. pythonclaw/templates/skills/text/translator/SKILL.md +47 -0
  97. pythonclaw/templates/skills/text/translator/__pycache__/translate.cpython-311.pyc +0 -0
  98. pythonclaw/templates/skills/text/translator/translate.py +66 -0
  99. pythonclaw/templates/skills/web/CATEGORY.md +4 -0
  100. pythonclaw/templates/skills/web/tavily/SKILL.md +61 -0
  101. pythonclaw/templates/soul/SOUL.md +54 -0
  102. pythonclaw/web/__init__.py +1 -0
  103. pythonclaw/web/app.py +585 -0
  104. pythonclaw/web/static/favicon.png +0 -0
  105. pythonclaw/web/static/index.html +1318 -0
  106. pythonclaw/web/static/logo.png +0 -0
  107. pythonclaw-0.2.0.dist-info/METADATA +410 -0
  108. pythonclaw-0.2.0.dist-info/RECORD +112 -0
  109. pythonclaw-0.2.0.dist-info/WHEEL +5 -0
  110. pythonclaw-0.2.0.dist-info/entry_points.txt +2 -0
  111. pythonclaw-0.2.0.dist-info/licenses/LICENSE +21 -0
  112. 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()