praisonai-code 0.0.1__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 (309) hide show
  1. praisonai_code/__init__.py +17 -0
  2. praisonai_code/cli/__init__.py +12 -0
  3. praisonai_code/cli/_forward_shim.py +10 -0
  4. praisonai_code/cli/_paths.py +88 -0
  5. praisonai_code/cli/_warnings.py +58 -0
  6. praisonai_code/cli/app.py +757 -0
  7. praisonai_code/cli/approval_backend.py +272 -0
  8. praisonai_code/cli/branding.py +94 -0
  9. praisonai_code/cli/commands/__init__.py +114 -0
  10. praisonai_code/cli/commands/acp.py +80 -0
  11. praisonai_code/cli/commands/agent.py +116 -0
  12. praisonai_code/cli/commands/agents.py +80 -0
  13. praisonai_code/cli/commands/app.py +139 -0
  14. praisonai_code/cli/commands/attach.py +95 -0
  15. praisonai_code/cli/commands/audit.py +102 -0
  16. praisonai_code/cli/commands/auth.py +508 -0
  17. praisonai_code/cli/commands/batch.py +848 -0
  18. praisonai_code/cli/commands/benchmark.py +286 -0
  19. praisonai_code/cli/commands/browser.py +299 -0
  20. praisonai_code/cli/commands/call.py +45 -0
  21. praisonai_code/cli/commands/chat.py +332 -0
  22. praisonai_code/cli/commands/checkpoint.py +170 -0
  23. praisonai_code/cli/commands/code.py +276 -0
  24. praisonai_code/cli/commands/command.py +114 -0
  25. praisonai_code/cli/commands/commit.py +47 -0
  26. praisonai_code/cli/commands/completion.py +333 -0
  27. praisonai_code/cli/commands/config.py +681 -0
  28. praisonai_code/cli/commands/context.py +414 -0
  29. praisonai_code/cli/commands/daemon.py +203 -0
  30. praisonai_code/cli/commands/debug.py +142 -0
  31. praisonai_code/cli/commands/deploy.py +71 -0
  32. praisonai_code/cli/commands/diag.py +55 -0
  33. praisonai_code/cli/commands/docs.py +1575 -0
  34. praisonai_code/cli/commands/doctor.py +332 -0
  35. praisonai_code/cli/commands/endpoints.py +51 -0
  36. praisonai_code/cli/commands/environment.py +179 -0
  37. praisonai_code/cli/commands/eval.py +131 -0
  38. praisonai_code/cli/commands/examples.py +953 -0
  39. praisonai_code/cli/commands/flow.py +436 -0
  40. praisonai_code/cli/commands/github.py +752 -0
  41. praisonai_code/cli/commands/hooks.py +74 -0
  42. praisonai_code/cli/commands/init.py +174 -0
  43. praisonai_code/cli/commands/knowledge.py +440 -0
  44. praisonai_code/cli/commands/langextract.py +120 -0
  45. praisonai_code/cli/commands/langfuse.py +984 -0
  46. praisonai_code/cli/commands/loop.py +211 -0
  47. praisonai_code/cli/commands/lsp.py +112 -0
  48. praisonai_code/cli/commands/managed.py +659 -0
  49. praisonai_code/cli/commands/mcp.py +763 -0
  50. praisonai_code/cli/commands/memory.py +298 -0
  51. praisonai_code/cli/commands/models.py +264 -0
  52. praisonai_code/cli/commands/n8n.py +326 -0
  53. praisonai_code/cli/commands/obs.py +19 -0
  54. praisonai_code/cli/commands/package.py +76 -0
  55. praisonai_code/cli/commands/paths.py +106 -0
  56. praisonai_code/cli/commands/permissions.py +272 -0
  57. praisonai_code/cli/commands/plugins.py +609 -0
  58. praisonai_code/cli/commands/port.py +530 -0
  59. praisonai_code/cli/commands/profile.py +466 -0
  60. praisonai_code/cli/commands/publish.py +193 -0
  61. praisonai_code/cli/commands/rag.py +913 -0
  62. praisonai_code/cli/commands/realtime.py +52 -0
  63. praisonai_code/cli/commands/recipe.py +684 -0
  64. praisonai_code/cli/commands/registry.py +59 -0
  65. praisonai_code/cli/commands/replay.py +830 -0
  66. praisonai_code/cli/commands/research.py +49 -0
  67. praisonai_code/cli/commands/retrieval.py +377 -0
  68. praisonai_code/cli/commands/rules.py +71 -0
  69. praisonai_code/cli/commands/run.py +1573 -0
  70. praisonai_code/cli/commands/sandbox.py +371 -0
  71. praisonai_code/cli/commands/schedule.py +529 -0
  72. praisonai_code/cli/commands/serve.py +690 -0
  73. praisonai_code/cli/commands/session.py +450 -0
  74. praisonai_code/cli/commands/setup.py +174 -0
  75. praisonai_code/cli/commands/skills.py +545 -0
  76. praisonai_code/cli/commands/standardise.py +711 -0
  77. praisonai_code/cli/commands/templates.py +54 -0
  78. praisonai_code/cli/commands/test.py +558 -0
  79. praisonai_code/cli/commands/todo.py +74 -0
  80. praisonai_code/cli/commands/tools.py +205 -0
  81. praisonai_code/cli/commands/traces.py +145 -0
  82. praisonai_code/cli/commands/tracker.py +852 -0
  83. praisonai_code/cli/commands/train.py +613 -0
  84. praisonai_code/cli/commands/ui.py +172 -0
  85. praisonai_code/cli/commands/up.py +354 -0
  86. praisonai_code/cli/commands/validate.py +291 -0
  87. praisonai_code/cli/commands/version.py +101 -0
  88. praisonai_code/cli/commands/workflow.py +97 -0
  89. praisonai_code/cli/config_loader.py +437 -0
  90. praisonai_code/cli/configuration/__init__.py +27 -0
  91. praisonai_code/cli/configuration/config.schema.json +57 -0
  92. praisonai_code/cli/configuration/credentials.py +446 -0
  93. praisonai_code/cli/configuration/loader.py +364 -0
  94. praisonai_code/cli/configuration/model_resolver.py +161 -0
  95. praisonai_code/cli/configuration/oauth.py +389 -0
  96. praisonai_code/cli/configuration/paths.py +224 -0
  97. praisonai_code/cli/configuration/resolver.py +687 -0
  98. praisonai_code/cli/configuration/schema.py +317 -0
  99. praisonai_code/cli/execution/__init__.py +99 -0
  100. praisonai_code/cli/execution/core.py +208 -0
  101. praisonai_code/cli/execution/profiler.py +898 -0
  102. praisonai_code/cli/execution/request.py +85 -0
  103. praisonai_code/cli/execution/result.py +74 -0
  104. praisonai_code/cli/fallback_schema.py +416 -0
  105. praisonai_code/cli/features/__init__.py +278 -0
  106. praisonai_code/cli/features/_endpoint_registry.py +64 -0
  107. praisonai_code/cli/features/_search_registry.py +43 -0
  108. praisonai_code/cli/features/acp.py +236 -0
  109. praisonai_code/cli/features/action_orchestrator.py +576 -0
  110. praisonai_code/cli/features/agent_scheduler.py +773 -0
  111. praisonai_code/cli/features/agent_tools.py +603 -0
  112. praisonai_code/cli/features/agents.py +397 -0
  113. praisonai_code/cli/features/at_mentions.py +471 -0
  114. praisonai_code/cli/features/audit_cli.py +270 -0
  115. praisonai_code/cli/features/auto_memory.py +182 -0
  116. praisonai_code/cli/features/auto_mode.py +552 -0
  117. praisonai_code/cli/features/autonomy_mode.py +546 -0
  118. praisonai_code/cli/features/background.py +356 -0
  119. praisonai_code/cli/features/base.py +168 -0
  120. praisonai_code/cli/features/benchmark.py +1462 -0
  121. praisonai_code/cli/features/capabilities.py +1326 -0
  122. praisonai_code/cli/features/checkpoints.py +345 -0
  123. praisonai_code/cli/features/cli_profiler.py +335 -0
  124. praisonai_code/cli/features/code_intelligence.py +666 -0
  125. praisonai_code/cli/features/compaction.py +294 -0
  126. praisonai_code/cli/features/compare.py +534 -0
  127. praisonai_code/cli/features/config_hierarchy.py +366 -0
  128. praisonai_code/cli/features/context_manager.py +597 -0
  129. praisonai_code/cli/features/cost_tracker.py +514 -0
  130. praisonai_code/cli/features/csv_test_runner.py +736 -0
  131. praisonai_code/cli/features/custom_definitions.py +790 -0
  132. praisonai_code/cli/features/debug.py +810 -0
  133. praisonai_code/cli/features/deploy.py +605 -0
  134. praisonai_code/cli/features/diag.py +289 -0
  135. praisonai_code/cli/features/display_jsonl.py +173 -0
  136. praisonai_code/cli/features/doctor/__init__.py +63 -0
  137. praisonai_code/cli/features/doctor/checks/__init__.py +29 -0
  138. praisonai_code/cli/features/doctor/checks/acp_checks.py +220 -0
  139. praisonai_code/cli/features/doctor/checks/bot_checks.py +340 -0
  140. praisonai_code/cli/features/doctor/checks/config_checks.py +373 -0
  141. praisonai_code/cli/features/doctor/checks/db_checks.py +366 -0
  142. praisonai_code/cli/features/doctor/checks/env_checks.py +637 -0
  143. praisonai_code/cli/features/doctor/checks/gateway_checks.py +387 -0
  144. praisonai_code/cli/features/doctor/checks/lsp_checks.py +231 -0
  145. praisonai_code/cli/features/doctor/checks/mcp_checks.py +367 -0
  146. praisonai_code/cli/features/doctor/checks/memory_checks.py +268 -0
  147. praisonai_code/cli/features/doctor/checks/network_checks.py +251 -0
  148. praisonai_code/cli/features/doctor/checks/obs_checks.py +328 -0
  149. praisonai_code/cli/features/doctor/checks/packaging_checks.py +422 -0
  150. praisonai_code/cli/features/doctor/checks/performance_checks.py +235 -0
  151. praisonai_code/cli/features/doctor/checks/permissions_checks.py +259 -0
  152. praisonai_code/cli/features/doctor/checks/runtime_checks.py +650 -0
  153. praisonai_code/cli/features/doctor/checks/runtime_migration_checks.py +220 -0
  154. praisonai_code/cli/features/doctor/checks/selftest_checks.py +322 -0
  155. praisonai_code/cli/features/doctor/checks/serve_checks.py +426 -0
  156. praisonai_code/cli/features/doctor/checks/skills_checks.py +327 -0
  157. praisonai_code/cli/features/doctor/checks/tools_checks.py +371 -0
  158. praisonai_code/cli/features/doctor/engine.py +266 -0
  159. praisonai_code/cli/features/doctor/formatters.py +377 -0
  160. praisonai_code/cli/features/doctor/handler.py +564 -0
  161. praisonai_code/cli/features/doctor/models.py +276 -0
  162. praisonai_code/cli/features/doctor/registry.py +239 -0
  163. praisonai_code/cli/features/endpoints.py +1016 -0
  164. praisonai_code/cli/features/eval.py +559 -0
  165. praisonai_code/cli/features/examples.py +707 -0
  166. praisonai_code/cli/features/external_agents.py +231 -0
  167. praisonai_code/cli/features/fast_context.py +410 -0
  168. praisonai_code/cli/features/file_history.py +320 -0
  169. praisonai_code/cli/features/flow_display.py +566 -0
  170. praisonai_code/cli/features/git_attribution.py +159 -0
  171. praisonai_code/cli/features/git_integration.py +651 -0
  172. praisonai_code/cli/features/guardrail.py +171 -0
  173. praisonai_code/cli/features/handoff.py +252 -0
  174. praisonai_code/cli/features/hooks.py +583 -0
  175. praisonai_code/cli/features/hybrid_workflow.py +391 -0
  176. praisonai_code/cli/features/image.py +384 -0
  177. praisonai_code/cli/features/interactive_core_headless.py +450 -0
  178. praisonai_code/cli/features/interactive_runtime.py +600 -0
  179. praisonai_code/cli/features/interactive_test_harness.py +537 -0
  180. praisonai_code/cli/features/interactive_tools.py +428 -0
  181. praisonai_code/cli/features/interactive_tui.py +603 -0
  182. praisonai_code/cli/features/job_workflow.py +906 -0
  183. praisonai_code/cli/features/jobs.py +632 -0
  184. praisonai_code/cli/features/knowledge.py +531 -0
  185. praisonai_code/cli/features/knowledge_cli.py +438 -0
  186. praisonai_code/cli/features/lite.py +244 -0
  187. praisonai_code/cli/features/logs.py +200 -0
  188. praisonai_code/cli/features/lsp_cli.py +225 -0
  189. praisonai_code/cli/features/lsp_diagnostics.py +185 -0
  190. praisonai_code/cli/features/mcp.py +344 -0
  191. praisonai_code/cli/features/message_queue.py +587 -0
  192. praisonai_code/cli/features/metrics.py +210 -0
  193. praisonai_code/cli/features/migrate.py +1329 -0
  194. praisonai_code/cli/features/migration_flow.py +463 -0
  195. praisonai_code/cli/features/migration_spec.py +276 -0
  196. praisonai_code/cli/features/n8n.py +703 -0
  197. praisonai_code/cli/features/observability.py +293 -0
  198. praisonai_code/cli/features/ollama.py +361 -0
  199. praisonai_code/cli/features/output_modes.py +155 -0
  200. praisonai_code/cli/features/output_style.py +273 -0
  201. praisonai_code/cli/features/package.py +631 -0
  202. praisonai_code/cli/features/performance.py +308 -0
  203. praisonai_code/cli/features/persistence.py +636 -0
  204. praisonai_code/cli/features/profiler/__init__.py +81 -0
  205. praisonai_code/cli/features/profiler/core.py +558 -0
  206. praisonai_code/cli/features/profiler/optimizations.py +652 -0
  207. praisonai_code/cli/features/profiler/suite.py +386 -0
  208. praisonai_code/cli/features/queue/__init__.py +73 -0
  209. praisonai_code/cli/features/queue/manager.py +435 -0
  210. praisonai_code/cli/features/queue/models.py +289 -0
  211. praisonai_code/cli/features/queue/persistence.py +564 -0
  212. praisonai_code/cli/features/queue/scheduler.py +529 -0
  213. praisonai_code/cli/features/queue/worker.py +400 -0
  214. praisonai_code/cli/features/recipe.py +2187 -0
  215. praisonai_code/cli/features/recipe_creator.py +996 -0
  216. praisonai_code/cli/features/recipe_optimizer.py +1364 -0
  217. praisonai_code/cli/features/recipe_prompts.py +226 -0
  218. praisonai_code/cli/features/registry.py +229 -0
  219. praisonai_code/cli/features/repo_map.py +860 -0
  220. praisonai_code/cli/features/router.py +466 -0
  221. praisonai_code/cli/features/safe_shell.py +427 -0
  222. praisonai_code/cli/features/sandbox_cli.py +283 -0
  223. praisonai_code/cli/features/sandbox_executor.py +536 -0
  224. praisonai_code/cli/features/sdk_knowledge.py +500 -0
  225. praisonai_code/cli/features/session.py +222 -0
  226. praisonai_code/cli/features/session_checkpoints.py +208 -0
  227. praisonai_code/cli/features/setup/__init__.py +9 -0
  228. praisonai_code/cli/features/setup/handler.py +355 -0
  229. praisonai_code/cli/features/setup/templates.py +62 -0
  230. praisonai_code/cli/features/skills.py +940 -0
  231. praisonai_code/cli/features/slash_commands.py +692 -0
  232. praisonai_code/cli/features/telemetry.py +179 -0
  233. praisonai_code/cli/features/templates.py +1390 -0
  234. praisonai_code/cli/features/thinking.py +343 -0
  235. praisonai_code/cli/features/todo.py +334 -0
  236. praisonai_code/cli/features/tools.py +680 -0
  237. praisonai_code/cli/features/tui/__init__.py +83 -0
  238. praisonai_code/cli/features/tui/app.py +871 -0
  239. praisonai_code/cli/features/tui/cli.py +580 -0
  240. praisonai_code/cli/features/tui/config.py +150 -0
  241. praisonai_code/cli/features/tui/debug.py +526 -0
  242. praisonai_code/cli/features/tui/events.py +99 -0
  243. praisonai_code/cli/features/tui/mock_provider.py +328 -0
  244. praisonai_code/cli/features/tui/orchestrator.py +652 -0
  245. praisonai_code/cli/features/tui/screens/__init__.py +50 -0
  246. praisonai_code/cli/features/tui/screens/help.py +157 -0
  247. praisonai_code/cli/features/tui/screens/main.py +568 -0
  248. praisonai_code/cli/features/tui/screens/queue.py +174 -0
  249. praisonai_code/cli/features/tui/screens/session.py +124 -0
  250. praisonai_code/cli/features/tui/screens/settings.py +148 -0
  251. praisonai_code/cli/features/tui/session_store.py +198 -0
  252. praisonai_code/cli/features/tui/widgets/__init__.py +56 -0
  253. praisonai_code/cli/features/tui/widgets/chat.py +263 -0
  254. praisonai_code/cli/features/tui/widgets/command_popup.py +258 -0
  255. praisonai_code/cli/features/tui/widgets/composer.py +292 -0
  256. praisonai_code/cli/features/tui/widgets/file_popup.py +207 -0
  257. praisonai_code/cli/features/tui/widgets/queue_panel.py +223 -0
  258. praisonai_code/cli/features/tui/widgets/status.py +181 -0
  259. praisonai_code/cli/features/tui/widgets/tool_panel.py +307 -0
  260. praisonai_code/cli/features/wizard.py +289 -0
  261. praisonai_code/cli/features/workflow.py +802 -0
  262. praisonai_code/cli/features/yaml_utils.py +321 -0
  263. praisonai_code/cli/interactive/__init__.py +48 -0
  264. praisonai_code/cli/interactive/async_tui.py +1218 -0
  265. praisonai_code/cli/interactive/config.py +139 -0
  266. praisonai_code/cli/interactive/core.py +618 -0
  267. praisonai_code/cli/interactive/events.py +131 -0
  268. praisonai_code/cli/interactive/frontends/__init__.py +31 -0
  269. praisonai_code/cli/interactive/frontends/rich_frontend.py +462 -0
  270. praisonai_code/cli/interactive/frontends/textual_frontend.py +157 -0
  271. praisonai_code/cli/interactive/praison_io.py +502 -0
  272. praisonai_code/cli/interactive/repl.py +297 -0
  273. praisonai_code/cli/interactive/split_tui.py +456 -0
  274. praisonai_code/cli/interactive/tui_app.py +457 -0
  275. praisonai_code/cli/langfuse_client.py +360 -0
  276. praisonai_code/cli/main.py +7421 -0
  277. praisonai_code/cli/output/__init__.py +25 -0
  278. praisonai_code/cli/output/console.py +456 -0
  279. praisonai_code/cli/output/event_bridge.py +191 -0
  280. praisonai_code/cli/schedule_cli.py +54 -0
  281. praisonai_code/cli/schema_provider.py +23 -0
  282. praisonai_code/cli/session/__init__.py +16 -0
  283. praisonai_code/cli/session/resume.py +148 -0
  284. praisonai_code/cli/session/unified.py +548 -0
  285. praisonai_code/cli/state/__init__.py +31 -0
  286. praisonai_code/cli/state/identifiers.py +161 -0
  287. praisonai_code/cli/state/project_sessions.py +383 -0
  288. praisonai_code/cli/state/sessions.py +390 -0
  289. praisonai_code/cli/ui/__init__.py +160 -0
  290. praisonai_code/cli/ui/config.py +46 -0
  291. praisonai_code/cli/ui/events.py +61 -0
  292. praisonai_code/cli/ui/mg_backend.py +342 -0
  293. praisonai_code/cli/ui/plain.py +133 -0
  294. praisonai_code/cli/ui/rich_backend.py +162 -0
  295. praisonai_code/cli/unified_schema.py +655 -0
  296. praisonai_code/cli/utils/env_utils.py +126 -0
  297. praisonai_code/cli/utils/project.py +131 -0
  298. praisonai_code/cli_backends/__init__.py +73 -0
  299. praisonai_code/cli_backends/claude.py +373 -0
  300. praisonai_code/cli_backends/registry.py +113 -0
  301. praisonai_code/runtime/__init__.py +36 -0
  302. praisonai_code/runtime/__main__.py +81 -0
  303. praisonai_code/runtime/client.py +131 -0
  304. praisonai_code/runtime/descriptor.py +209 -0
  305. praisonai_code/runtime/server.py +356 -0
  306. praisonai_code-0.0.1.dist-info/METADATA +80 -0
  307. praisonai_code-0.0.1.dist-info/RECORD +309 -0
  308. praisonai_code-0.0.1.dist-info/WHEEL +5 -0
  309. praisonai_code-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1390 @@
1
+ """
2
+ Templates CLI Feature Handler
3
+
4
+ Provides CLI commands for template management:
5
+ - list, search, info, install, cache clear
6
+ - run templates directly
7
+ - init projects from templates
8
+ - Custom templates directory support with precedence
9
+ """
10
+
11
+ import shutil
12
+ from pathlib import Path
13
+ from typing import Any, Dict, List
14
+
15
+
16
+ class TemplatesHandler:
17
+ """
18
+ CLI handler for template operations.
19
+
20
+ Commands:
21
+ - list: List available templates
22
+ - search: Search templates by query
23
+ - info: Show template details
24
+ - install: Install a template to cache
25
+ - cache: Cache management (clear, list)
26
+ - run: Run a template directly
27
+ - init: Initialize a project from template
28
+ """
29
+
30
+ def __init__(self):
31
+ """Initialize the handler with lazy-loaded dependencies."""
32
+ self._loader = None
33
+ self._registry = None
34
+ self._cache = None
35
+
36
+ @property
37
+ def loader(self):
38
+ """Lazy load template loader."""
39
+ if self._loader is None:
40
+ from praisonai.templates import TemplateLoader
41
+ self._loader = TemplateLoader()
42
+ return self._loader
43
+
44
+ @property
45
+ def registry(self):
46
+ """Lazy load template registry."""
47
+ if self._registry is None:
48
+ from praisonai.templates import TemplateRegistry
49
+ self._registry = TemplateRegistry()
50
+ return self._registry
51
+
52
+ @property
53
+ def cache(self):
54
+ """Lazy load template cache."""
55
+ if self._cache is None:
56
+ from praisonai.templates import TemplateCache
57
+ self._cache = TemplateCache()
58
+ return self._cache
59
+
60
+ def handle(self, args: List[str]) -> int:
61
+ """
62
+ Handle templates subcommand.
63
+
64
+ Args:
65
+ args: Command arguments
66
+
67
+ Returns:
68
+ Exit code (0 for success)
69
+ """
70
+ if not args:
71
+ self._print_help()
72
+ return 0
73
+
74
+ command = args[0]
75
+ remaining = args[1:]
76
+
77
+ commands = {
78
+ "list": self.cmd_list,
79
+ "search": self.cmd_search,
80
+ "info": self.cmd_info,
81
+ "install": self.cmd_install,
82
+ "cache": self.cmd_cache,
83
+ "run": self.cmd_run,
84
+ "init": self.cmd_init,
85
+ "add": self.cmd_add,
86
+ "add-sources": self.cmd_add_sources,
87
+ "remove-sources": self.cmd_remove_sources,
88
+ "browse": self.cmd_browse,
89
+ "catalog": self.cmd_catalog,
90
+ "validate": self.cmd_validate,
91
+ "help": lambda _: self._print_help() or 0,
92
+ }
93
+
94
+ if command in commands:
95
+ return commands[command](remaining)
96
+ else:
97
+ print(f"[red]Unknown command: {command}[/red]")
98
+ self._print_help()
99
+ return 1
100
+
101
+ def _print_help(self):
102
+ """Print help message."""
103
+ help_text = """
104
+ [bold cyan]PraisonAI Templates[/bold cyan]
105
+
106
+ [bold]Usage:[/bold]
107
+ praisonai templates <command> [options]
108
+
109
+ [bold]Commands:[/bold]
110
+ list List available templates
111
+ search <query> Search templates by name or tags
112
+ info <template> Show template details
113
+ install <uri> Install a template to cache
114
+ cache clear Clear the template cache
115
+ cache list List cached templates
116
+ run <template> Run a template directly
117
+ init <name> Initialize a project from template
118
+ add <source> Add template from GitHub or local path
119
+ add-sources <src> Add a template source to persistent config
120
+ remove-sources Remove a template source from config
121
+ browse Open template catalog in browser
122
+ catalog build Build catalog locally
123
+ catalog sync Sync catalog sources
124
+ validate Validate template YAML files
125
+
126
+ [bold]Options:[/bold]
127
+ --offline Use only cached templates (no network)
128
+ --source <src> Filter by source (custom, package, all)
129
+ --custom-dir <path> Add custom templates directory
130
+ --paths Show template search paths
131
+
132
+ [bold]Examples:[/bold]
133
+ praisonai templates list
134
+ praisonai templates search video
135
+ praisonai templates info transcript-generator
136
+ praisonai templates install github:MervinPraison/agent-recipes/transcript-generator
137
+ praisonai templates run transcript-generator ./audio.mp3
138
+ praisonai templates add github:user/repo/my-template
139
+ praisonai templates add-sources github:MervinPraison/Agent-Recipes
140
+ praisonai init my-project --template transcript-generator
141
+ """
142
+ try:
143
+ from rich import print as rprint
144
+ rprint(help_text)
145
+ except ImportError:
146
+ print(help_text.replace("[bold cyan]", "").replace("[/bold cyan]", "")
147
+ .replace("[bold]", "").replace("[/bold]", "")
148
+ .replace("[red]", "").replace("[/red]", ""))
149
+
150
+ def cmd_list(self, args: List[str]) -> int:
151
+ """List available templates from all sources including custom directories."""
152
+ source_filter = None
153
+ custom_dirs = []
154
+ show_paths = "--paths" in args
155
+
156
+ # Parse --source filter
157
+ if "--source" in args:
158
+ idx = args.index("--source")
159
+ if idx + 1 < len(args):
160
+ source_filter = args[idx + 1]
161
+
162
+ # Parse --custom-dir (can be specified multiple times)
163
+ i = 0
164
+ while i < len(args):
165
+ if args[i] == "--custom-dir" and i + 1 < len(args):
166
+ custom_dirs.append(args[i + 1])
167
+ i += 2
168
+ else:
169
+ i += 1
170
+
171
+ try:
172
+ from praisonai.templates.discovery import TemplateDiscovery
173
+
174
+ discovery = TemplateDiscovery(
175
+ custom_dirs=custom_dirs if custom_dirs else None,
176
+ include_package=True,
177
+ include_defaults=True
178
+ )
179
+
180
+ # Show search paths if requested
181
+ if show_paths:
182
+ print("\nTemplate Search Paths (in priority order):")
183
+ for path, source, exists in discovery.get_search_paths():
184
+ status = "✓" if exists else "✗"
185
+ print(f" {status} [{source}] {path}")
186
+ print()
187
+
188
+ # Discover templates
189
+ templates = discovery.list_templates(source_filter=source_filter)
190
+
191
+ if not templates:
192
+ print("No templates found.")
193
+ if not show_paths:
194
+ print("Use --paths to see search locations.")
195
+ return 0
196
+
197
+ try:
198
+ from rich.console import Console
199
+ from rich.table import Table
200
+
201
+ console = Console()
202
+ table = Table(title="Available Templates")
203
+ table.add_column("Name", style="cyan")
204
+ table.add_column("Version", style="green")
205
+ table.add_column("Description")
206
+ table.add_column("Source", style="dim")
207
+
208
+ for t in sorted(templates, key=lambda x: (x.priority, x.name)):
209
+ desc = t.description or ""
210
+ table.add_row(
211
+ t.name,
212
+ t.version or "1.0.0",
213
+ desc[:50] + "..." if len(desc) > 50 else desc,
214
+ t.source
215
+ )
216
+
217
+ console.print(table)
218
+ except ImportError:
219
+ print(f"{'Name':<25} {'Version':<10} {'Source':<10} {'Description':<35}")
220
+ print("-" * 80)
221
+ for t in sorted(templates, key=lambda x: (x.priority, x.name)):
222
+ desc = (t.description or "")[:35]
223
+ if len(t.description or "") > 35:
224
+ desc += "..."
225
+ print(f"{t.name:<25} {t.version or '1.0.0':<10} {t.source:<10} {desc:<35}")
226
+
227
+ return 0
228
+
229
+ except Exception as e:
230
+ print(f"Error listing templates: {e}")
231
+ import traceback
232
+ traceback.print_exc()
233
+ return 1
234
+
235
+ def cmd_search(self, args: List[str]) -> int:
236
+ """Search templates by query."""
237
+ if not args or args[0].startswith("--"):
238
+ print("Usage: praisonai templates search <query>")
239
+ return 1
240
+
241
+ query = args[0]
242
+ offline = "--offline" in args
243
+
244
+ try:
245
+ from praisonai.templates import search_templates
246
+ templates = search_templates(query, offline=offline)
247
+
248
+ if not templates:
249
+ print(f"No templates found matching '{query}'")
250
+ return 0
251
+
252
+ print(f"Found {len(templates)} template(s) matching '{query}':\n")
253
+ for t in templates:
254
+ print(f" • {t.name} (v{t.version})")
255
+ if t.description:
256
+ print(f" {t.description}")
257
+ if t.tags:
258
+ print(f" Tags: {', '.join(t.tags)}")
259
+ print()
260
+
261
+ return 0
262
+
263
+ except Exception as e:
264
+ print(f"Error searching templates: {e}")
265
+ return 1
266
+
267
+ def cmd_info(self, args: List[str]) -> int:
268
+ """Show template details with custom directory support."""
269
+ if not args or args[0].startswith("--"):
270
+ print("Usage: praisonai templates info <template>")
271
+ return 1
272
+
273
+ name_or_uri = args[0]
274
+ offline = "--offline" in args
275
+ custom_dirs = []
276
+
277
+ # Parse --custom-dir
278
+ i = 0
279
+ while i < len(args):
280
+ if args[i] == "--custom-dir" and i + 1 < len(args):
281
+ custom_dirs.append(args[i + 1])
282
+ i += 2
283
+ else:
284
+ i += 1
285
+
286
+ try:
287
+ # First try to find in custom/local directories
288
+ from praisonai.templates.discovery import TemplateDiscovery
289
+
290
+ discovery = TemplateDiscovery(
291
+ custom_dirs=custom_dirs if custom_dirs else None,
292
+ include_package=True,
293
+ include_defaults=True
294
+ )
295
+
296
+ discovered = discovery.find_template(name_or_uri)
297
+
298
+ if discovered:
299
+ # Load from discovered path
300
+ from praisonai.templates import load_template
301
+ template = load_template(str(discovered.path), offline=offline)
302
+ else:
303
+ # Fall back to URI resolution
304
+ from praisonai.templates import load_template
305
+ template = load_template(name_or_uri, offline=offline)
306
+
307
+ # Check dependency availability
308
+ from praisonai.templates.dependency_checker import DependencyChecker
309
+ checker = DependencyChecker()
310
+ deps = checker.check_template_dependencies(template)
311
+
312
+ # Build availability strings
313
+ def format_dep_list(items, key="available"):
314
+ result = []
315
+ for item in items:
316
+ status = "✓" if item.get(key, False) else "✗"
317
+ hint = ""
318
+ if not item.get(key, False):
319
+ if item.get("install_hint"):
320
+ hint = f" ({item['install_hint']})"
321
+ result.append(f"{status} {item['name']}{hint}")
322
+ return result
323
+
324
+ tools_status = format_dep_list(deps["tools"])
325
+ pkgs_status = format_dep_list(deps["packages"])
326
+ env_status = format_dep_list(deps["env"])
327
+
328
+ try:
329
+ from rich.console import Console
330
+ from rich.panel import Panel
331
+ from rich.markdown import Markdown
332
+
333
+ console = Console()
334
+
335
+ info = f"""
336
+ **Name:** {template.name}
337
+ **Version:** {template.version}
338
+ **Author:** {template.author or 'Unknown'}
339
+ **License:** {template.license or 'Not specified'}
340
+
341
+ **Description:**
342
+ {template.description}
343
+
344
+ **Tags:** {', '.join(template.tags) if template.tags else 'None'}
345
+
346
+ **Skills:** {', '.join(template.skills) if template.skills else 'None'}
347
+
348
+ **Path:** {template.path}
349
+ """
350
+ console.print(Panel(Markdown(info), title=f"Template: {template.name}"))
351
+
352
+ # Print dependency status
353
+ all_ok = "✓" if deps["all_satisfied"] else "✗"
354
+ console.print(f"\n[bold]Dependencies Status:[/bold] {all_ok}")
355
+
356
+ if tools_status:
357
+ console.print("\n[bold]Required Tools:[/bold]")
358
+ for t in tools_status:
359
+ color = "green" if t.startswith("✓") else "red"
360
+ console.print(f" [{color}]{t}[/{color}]")
361
+
362
+ if pkgs_status:
363
+ console.print("\n[bold]Required Packages:[/bold]")
364
+ for p in pkgs_status:
365
+ color = "green" if p.startswith("✓") else "red"
366
+ console.print(f" [{color}]{p}[/{color}]")
367
+
368
+ if env_status:
369
+ console.print("\n[bold]Required Environment:[/bold]")
370
+ for e in env_status:
371
+ color = "green" if e.startswith("✓") else "red"
372
+ console.print(f" [{color}]{e}[/{color}]")
373
+
374
+ # Print install hints if any missing
375
+ if not deps["all_satisfied"]:
376
+ hints = checker.get_install_hints(template)
377
+ if hints:
378
+ console.print("\n[bold yellow]To fix missing dependencies:[/bold yellow]")
379
+ for hint in hints:
380
+ console.print(f" • {hint}")
381
+
382
+ except ImportError:
383
+ print(f"Template: {template.name}")
384
+ print(f"Version: {template.version}")
385
+ print(f"Author: {template.author or 'Unknown'}")
386
+ print(f"Description: {template.description}")
387
+ print(f"Path: {template.path}")
388
+ print(f"\nDependencies: {'All satisfied' if deps['all_satisfied'] else 'Some missing'}")
389
+ if tools_status:
390
+ print("\nRequired Tools:")
391
+ for t in tools_status:
392
+ print(f" {t}")
393
+ if pkgs_status:
394
+ print("\nRequired Packages:")
395
+ for p in pkgs_status:
396
+ print(f" {p}")
397
+ if env_status:
398
+ print("\nRequired Environment:")
399
+ for e in env_status:
400
+ print(f" {e}")
401
+
402
+ return 0
403
+
404
+ except Exception as e:
405
+ print(f"Error loading template info: {e}")
406
+ return 1
407
+
408
+ def cmd_install(self, args: List[str]) -> int:
409
+ """Install a template to cache."""
410
+ if not args or args[0].startswith("--"):
411
+ print("Usage: praisonai templates install <uri>")
412
+ return 1
413
+
414
+ uri = args[0]
415
+
416
+ try:
417
+ from praisonai.templates import install_template
418
+
419
+ print(f"Installing template: {uri}")
420
+ cached = install_template(uri)
421
+ print(f"✓ Template installed to: {cached.path}")
422
+
423
+ return 0
424
+
425
+ except Exception as e:
426
+ print(f"Error installing template: {e}")
427
+ return 1
428
+
429
+ def cmd_cache(self, args: List[str]) -> int:
430
+ """Cache management commands."""
431
+ if not args:
432
+ print("Usage: praisonai templates cache <clear|list|size>")
433
+ return 1
434
+
435
+ subcmd = args[0]
436
+
437
+ if subcmd == "clear":
438
+ source = None
439
+ if len(args) > 1:
440
+ source = args[1]
441
+
442
+ try:
443
+ from praisonai.templates import clear_cache
444
+ count = clear_cache(source)
445
+ print(f"✓ Cleared {count} cached template(s)")
446
+ return 0
447
+ except Exception as e:
448
+ print(f"Error clearing cache: {e}")
449
+ return 1
450
+
451
+ elif subcmd == "list":
452
+ try:
453
+ cached = self.cache.list_cached()
454
+ if not cached:
455
+ print("No cached templates.")
456
+ return 0
457
+
458
+ print(f"Cached templates ({len(cached)}):\n")
459
+ for path, meta in cached:
460
+ status = "pinned" if meta.is_pinned else "expires in " + str(int(meta.ttl_seconds - (import_time() - meta.fetched_at))) + "s"
461
+ print(f" • {path.name}")
462
+ print(f" Path: {path}")
463
+ print(f" Status: {status}")
464
+ print()
465
+
466
+ return 0
467
+ except Exception as e:
468
+ print(f"Error listing cache: {e}")
469
+ return 1
470
+
471
+ elif subcmd == "size":
472
+ size = self.cache.get_cache_size()
473
+ print(f"Cache size: {size / 1024 / 1024:.2f} MB")
474
+ return 0
475
+
476
+ else:
477
+ print(f"Unknown cache command: {subcmd}")
478
+ return 1
479
+
480
+ def cmd_run(self, args: List[str]) -> int:
481
+ """Run a template directly."""
482
+ if not args or args[0].startswith("--"):
483
+ print("Usage: praisonai templates run <template> [args...] [--strict-tools] [--offline]")
484
+ return 1
485
+
486
+ uri = args[0]
487
+ template_args = args[1:]
488
+ offline = "--offline" in args
489
+ strict_tools = "--strict-tools" in args
490
+ no_template_tools_py = "--no-template-tools-py" in args
491
+
492
+ # Parse --tools, --tools-dir, --tools-source overrides
493
+ tools_files = []
494
+ tools_dirs = []
495
+ tools_sources_override = []
496
+ i = 0
497
+ while i < len(args):
498
+ if args[i] == "--tools" and i + 1 < len(args):
499
+ tools_files.append(args[i + 1])
500
+ i += 2
501
+ elif args[i] == "--tools-dir" and i + 1 < len(args):
502
+ tools_dirs.append(args[i + 1])
503
+ i += 2
504
+ elif args[i] == "--tools-source" and i + 1 < len(args):
505
+ tools_sources_override.append(args[i + 1])
506
+ i += 2
507
+ else:
508
+ i += 1
509
+
510
+ try:
511
+ from praisonai.templates.loader import TemplateLoader
512
+ from praisonai.templates.discovery import TemplateDiscovery
513
+
514
+ # First try to find in custom/local directories
515
+ discovery = TemplateDiscovery(
516
+ custom_dirs=tools_dirs if tools_dirs else None,
517
+ include_package=True,
518
+ include_defaults=True
519
+ )
520
+
521
+ discovered = discovery.find_template(uri)
522
+
523
+ loader = TemplateLoader(offline=offline)
524
+
525
+ if discovered:
526
+ # Load from discovered path
527
+ template = loader.load(str(discovered.path), offline=offline)
528
+ else:
529
+ # Fall back to URI resolution
530
+ template = loader.load(uri, offline=offline)
531
+
532
+ # Strict mode: fail-fast on missing dependencies
533
+ if strict_tools:
534
+ from praisonai.templates.dependency_checker import DependencyChecker, StrictModeError
535
+ checker = DependencyChecker()
536
+ try:
537
+ checker.enforce_strict_mode(template)
538
+ print("✓ All dependencies satisfied (strict mode)")
539
+ except StrictModeError as e:
540
+ print(f"✗ Strict mode check failed:\n{e}")
541
+ return 1
542
+ else:
543
+ # Non-strict: warn but continue
544
+ missing = loader.check_requirements(template)
545
+ if missing["missing_packages"]:
546
+ print(f"Warning: Missing packages: {', '.join(missing['missing_packages'])}")
547
+ print("Install with: pip install " + " ".join(missing["missing_packages"]))
548
+ if missing["missing_env"]:
549
+ print(f"Warning: Missing environment variables: {', '.join(missing['missing_env'])}")
550
+
551
+ # Load tool overrides if specified
552
+ tool_registry = None
553
+ from praisonai.templates.tool_override import create_tool_registry_with_overrides, resolve_tools
554
+
555
+ # Get template directory for local tools.py autoload (unless disabled)
556
+ template_dir = None
557
+ if not no_template_tools_py:
558
+ template_dir = str(template.path) if template.path else None
559
+
560
+ # Get tools_sources from template requires + CLI overrides
561
+ tools_sources = []
562
+ if template.requires and isinstance(template.requires, dict):
563
+ ts = template.requires.get("tools_sources", [])
564
+ if ts:
565
+ tools_sources.extend(ts)
566
+ # Add CLI --tools-source overrides
567
+ if tools_sources_override:
568
+ tools_sources.extend(tools_sources_override)
569
+
570
+ # Always build registry (includes defaults + template sources)
571
+ tool_registry = create_tool_registry_with_overrides(
572
+ override_files=tools_files if tools_files else None,
573
+ override_dirs=tools_dirs if tools_dirs else None,
574
+ include_defaults=True,
575
+ tools_sources=tools_sources if tools_sources else None,
576
+ template_dir=template_dir,
577
+ )
578
+
579
+ if tools_files or tools_dirs:
580
+ print(f"✓ Loaded {len(tool_registry)} tools from overrides")
581
+
582
+ # Load and run workflow
583
+ workflow_config = loader.load_workflow_config(template)
584
+
585
+ # Parse template args into config
586
+ config = self._parse_template_args(template_args, template)
587
+
588
+ # Merge config
589
+ if config:
590
+ workflow_config = {**workflow_config, **config}
591
+
592
+ # Run workflow - determine which class to use based on config structure
593
+ if "agents" in workflow_config and "tasks" in workflow_config:
594
+ # Agents format (agents + tasks)
595
+ from praisonaiagents import Agent, Task, AgentTeam
596
+
597
+ # Build agents
598
+ agents_config = workflow_config.get("agents", [])
599
+ agents = []
600
+ agent_map = {}
601
+
602
+ for agent_cfg in agents_config:
603
+ # Resolve tool names to callable tools
604
+ agent_tool_names = agent_cfg.get("tools", [])
605
+ resolved_agent_tools = resolve_tools(
606
+ agent_tool_names,
607
+ registry=tool_registry,
608
+ template_dir=template_dir
609
+ )
610
+
611
+ agent = Agent(
612
+ name=agent_cfg.get("name", "Agent"),
613
+ role=agent_cfg.get("role", ""),
614
+ goal=agent_cfg.get("goal", ""),
615
+ backstory=agent_cfg.get("backstory", ""),
616
+ tools=resolved_agent_tools,
617
+ llm=agent_cfg.get("llm"),
618
+ verbose=agent_cfg.get("verbose", True)
619
+ )
620
+ agents.append(agent)
621
+ agent_map[agent_cfg.get("name", "Agent")] = agent
622
+
623
+ # Build tasks
624
+ tasks_config = workflow_config.get("tasks", [])
625
+ tasks = []
626
+
627
+ for task_cfg in tasks_config:
628
+ agent_name = task_cfg.get("agent", "")
629
+ agent = agent_map.get(agent_name, agents[0] if agents else None)
630
+
631
+ task = Task(
632
+ name=task_cfg.get("name", "Task"),
633
+ description=task_cfg.get("description", ""),
634
+ expected_output=task_cfg.get("expected_output", ""),
635
+ agent=agent
636
+ )
637
+ tasks.append(task)
638
+
639
+ # Run
640
+ praison_agents = AgentTeam(
641
+ agents=agents,
642
+ tasks=tasks,
643
+ process=workflow_config.get("process", "sequential"),
644
+ verbose=workflow_config.get("verbose", 1)
645
+ )
646
+ praison_agents.start()
647
+ elif "steps" in workflow_config:
648
+ # Workflow format (steps) - resolve tools in each step's agent
649
+ from praisonaiagents import Workflow
650
+
651
+ # Resolve tools for each step's agent if specified
652
+ steps = workflow_config.get("steps", [])
653
+ for step in steps:
654
+ if "agent" in step and isinstance(step["agent"], dict):
655
+ agent_cfg = step["agent"]
656
+ if "tools" in agent_cfg:
657
+ agent_cfg["tools"] = resolve_tools(
658
+ agent_cfg["tools"],
659
+ registry=tool_registry,
660
+ template_dir=template_dir
661
+ )
662
+
663
+ workflow = Workflow(**workflow_config)
664
+ workflow.run()
665
+ else:
666
+ raise ValueError("Invalid workflow config: must have 'agents'+'tasks' or 'steps'")
667
+
668
+ print(f"\n✓ Template '{template.name}' completed successfully")
669
+ return 0
670
+
671
+ except Exception as e:
672
+ print(f"Error running template: {e}")
673
+ import traceback
674
+ traceback.print_exc()
675
+ return 1
676
+
677
+ def cmd_init(self, args: List[str]) -> int:
678
+ """Initialize a project from template."""
679
+ if not args or args[0].startswith("--"):
680
+ print("Usage: praisonai templates init <project-name> --template <template>")
681
+ return 1
682
+
683
+ project_name = args[0]
684
+ template_uri = None
685
+ offline = "--offline" in args
686
+
687
+ if "--template" in args:
688
+ idx = args.index("--template")
689
+ if idx + 1 < len(args):
690
+ template_uri = args[idx + 1]
691
+
692
+ if not template_uri:
693
+ print("Error: --template is required")
694
+ return 1
695
+
696
+ try:
697
+ from praisonai.templates import load_template
698
+
699
+ template = load_template(template_uri, offline=offline)
700
+
701
+ # Create project directory
702
+ project_dir = Path(project_name)
703
+ if project_dir.exists():
704
+ print(f"Error: Directory '{project_name}' already exists")
705
+ return 1
706
+
707
+ project_dir.mkdir(parents=True)
708
+
709
+ # Copy template files
710
+ for item in template.path.iterdir():
711
+ if item.name.startswith("."):
712
+ continue
713
+ if item.is_file():
714
+ shutil.copy2(item, project_dir / item.name)
715
+ elif item.is_dir():
716
+ shutil.copytree(item, project_dir / item.name)
717
+
718
+ print(f"✓ Created project '{project_name}' from template '{template.name}'")
719
+ print("Next steps:")
720
+ print(f" cd {project_name}")
721
+ print(" praisonai run workflow.yaml")
722
+
723
+ return 0
724
+
725
+ except Exception as e:
726
+ print(f"Error initializing project: {e}")
727
+ return 1
728
+
729
+ def _parse_template_args(
730
+ self,
731
+ args: List[str],
732
+ template
733
+ ) -> Dict[str, Any]:
734
+ """Parse template-specific arguments."""
735
+ config = {}
736
+
737
+ # Handle positional args based on CLI config
738
+ cli_config = template.cli
739
+ if cli_config and "args" in cli_config:
740
+ positional_idx = 0
741
+ for arg_def in cli_config["args"]:
742
+ if arg_def.get("positional"):
743
+ if positional_idx < len(args) and not args[positional_idx].startswith("--"):
744
+ config[arg_def["name"]] = args[positional_idx]
745
+ positional_idx += 1
746
+
747
+ # Handle named args
748
+ i = 0
749
+ while i < len(args):
750
+ if args[i].startswith("--"):
751
+ key = args[i][2:].replace("-", "_")
752
+ if i + 1 < len(args) and not args[i + 1].startswith("--"):
753
+ config[key] = args[i + 1]
754
+ i += 2
755
+ else:
756
+ config[key] = True
757
+ i += 1
758
+ else:
759
+ i += 1
760
+
761
+ return config
762
+
763
+ def _get_templates_config_path(self):
764
+ """Get the path to the templates config file."""
765
+ config_dir = Path.home() / ".praison"
766
+ config_dir.mkdir(parents=True, exist_ok=True)
767
+ return config_dir / "templates_sources.yaml"
768
+
769
+ def _load_templates_config(self) -> Dict[str, Any]:
770
+ """Load templates config from file."""
771
+ import yaml
772
+ config_path = self._get_templates_config_path()
773
+ if config_path.exists():
774
+ with open(config_path, 'r') as f:
775
+ return yaml.safe_load(f) or {}
776
+ return {"sources": []}
777
+
778
+ def _save_templates_config(self, config: Dict[str, Any]):
779
+ """Save templates config to file."""
780
+ import yaml
781
+ config_path = self._get_templates_config_path()
782
+ with open(config_path, 'w') as f:
783
+ yaml.dump(config, f, default_flow_style=False)
784
+
785
+ def cmd_add(self, args: List[str]) -> int:
786
+ """
787
+ Add template from GitHub or local path.
788
+
789
+ Args:
790
+ args: [source] - github:user/repo/template or local path
791
+ """
792
+ if not args:
793
+ print("[red]Usage: praisonai templates add <source>[/red]")
794
+ print(" source: github:user/repo/template or local path")
795
+ return 1
796
+
797
+ source = args[0]
798
+
799
+ # Check if it's a local directory
800
+ if source.startswith("./") or source.startswith("/"):
801
+ path = Path(source).resolve()
802
+ if path.exists() and path.is_dir():
803
+ # Check for TEMPLATE.yaml
804
+ template_yaml = path / "TEMPLATE.yaml"
805
+ if not template_yaml.exists():
806
+ print(f"[red]Not a valid template: {path} (missing TEMPLATE.yaml)[/red]")
807
+ return 1
808
+
809
+ # Copy to ~/.praison/templates/
810
+ templates_dir = Path.home() / ".praison" / "templates"
811
+ templates_dir.mkdir(parents=True, exist_ok=True)
812
+ dest = templates_dir / path.name
813
+
814
+ if dest.exists():
815
+ shutil.rmtree(dest)
816
+ shutil.copytree(path, dest)
817
+
818
+ print(f"\n✅ Added template: {path.name}")
819
+ print(f" Copied to: {dest}")
820
+ return 0
821
+ else:
822
+ print(f"[red]Directory not found: {source}[/red]")
823
+ return 1
824
+
825
+ # Check if it's a GitHub reference
826
+ elif source.startswith("github:"):
827
+ github_path = source[7:] # Remove "github:"
828
+ parts = github_path.split("/")
829
+ if len(parts) < 3:
830
+ print("[red]Invalid GitHub format. Use: github:user/repo/template-name[/red]")
831
+ return 1
832
+
833
+ user, repo = parts[0], parts[1]
834
+ template_path = "/".join(parts[2:])
835
+
836
+ # Download from GitHub
837
+ import urllib.request
838
+ import tempfile
839
+ import zipfile
840
+
841
+ try:
842
+ # Download repo as zip
843
+ zip_url = f"https://github.com/{user}/{repo}/archive/refs/heads/main.zip"
844
+ print(f"Downloading from: {zip_url}")
845
+
846
+ with tempfile.TemporaryDirectory() as tmpdir:
847
+ zip_path = Path(tmpdir) / "repo.zip"
848
+ urllib.request.urlretrieve(zip_url, zip_path)
849
+
850
+ # Extract safely, preventing Zip Slip
851
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
852
+ import os
853
+ tmpdir_real = os.path.realpath(tmpdir)
854
+ for zip_info in zip_ref.infolist():
855
+ extracted_path = os.path.realpath(os.path.join(tmpdir_real, zip_info.filename))
856
+ if not extracted_path.startswith(tmpdir_real):
857
+ raise Exception(f"Attempted Zip Slip vulnerabilities detected: {zip_info.filename}")
858
+ zip_ref.extractall(tmpdir)
859
+
860
+ # Find the template
861
+ extracted_dir = Path(tmpdir) / f"{repo}-main"
862
+ template_src = extracted_dir / template_path
863
+
864
+ if not template_src.exists():
865
+ # Try common paths
866
+ for alt_path in [
867
+ extracted_dir / "agent_recipes" / "templates" / template_path,
868
+ extracted_dir / "templates" / template_path,
869
+ ]:
870
+ if alt_path.exists():
871
+ template_src = alt_path
872
+ break
873
+
874
+ if not template_src.exists():
875
+ print(f"[red]Template not found: {template_path}[/red]")
876
+ return 1
877
+
878
+ # Copy to ~/.praison/templates/
879
+ templates_dir = Path.home() / ".praison" / "templates"
880
+ templates_dir.mkdir(parents=True, exist_ok=True)
881
+ dest = templates_dir / template_src.name
882
+
883
+ if dest.exists():
884
+ shutil.rmtree(dest)
885
+ shutil.copytree(template_src, dest)
886
+
887
+ print(f"\n✅ Added template from GitHub: {user}/{repo}/{template_path}")
888
+ print(f" Saved to: {dest}")
889
+ return 0
890
+
891
+ except Exception as e:
892
+ print(f"[red]Failed to download from GitHub: {e}[/red]")
893
+ return 1
894
+
895
+ else:
896
+ print(f"[red]Unknown source format: {source}[/red]")
897
+ print("Use: github:user/repo/template or ./local-path")
898
+ return 1
899
+
900
+ def cmd_add_sources(self, args: List[str]) -> int:
901
+ """
902
+ Add a template source to persistent config.
903
+
904
+ Args:
905
+ args: [source] - github:user/repo or URL
906
+ """
907
+ if not args:
908
+ print("[red]Usage: praisonai templates add-sources <source>[/red]")
909
+ return 1
910
+
911
+ source = args[0]
912
+ config = self._load_templates_config()
913
+
914
+ if "sources" not in config:
915
+ config["sources"] = []
916
+
917
+ if source in config["sources"]:
918
+ print(f"[yellow]Source '{source}' already in config[/yellow]")
919
+ return 0
920
+
921
+ config["sources"].append(source)
922
+ self._save_templates_config(config)
923
+
924
+ print(f"\n✅ Added template source: {source}")
925
+ print(f" Config saved to: {self._get_templates_config_path()}")
926
+ return 0
927
+
928
+ def cmd_remove_sources(self, args: List[str]) -> int:
929
+ """
930
+ Remove a template source from persistent config.
931
+
932
+ Args:
933
+ args: [source] - source to remove
934
+ """
935
+ if not args:
936
+ print("[red]Usage: praisonai templates remove-sources <source>[/red]")
937
+ return 1
938
+
939
+ source = args[0]
940
+ config = self._load_templates_config()
941
+
942
+ if "sources" not in config or source not in config["sources"]:
943
+ print(f"[yellow]Source '{source}' not found in config[/yellow]")
944
+ return 1
945
+
946
+ config["sources"].remove(source)
947
+ self._save_templates_config(config)
948
+
949
+ print(f"\n✅ Removed template source: {source}")
950
+ return 0
951
+
952
+ def cmd_browse(self, args: List[str]) -> int:
953
+ """
954
+ Open template catalog in browser.
955
+
956
+ Args:
957
+ args: [--local] [--url <url>] [--print]
958
+ """
959
+ import webbrowser
960
+
961
+ # Default catalog URL
962
+ catalog_url = "https://mervinpraison.github.io/praisonai-template-catalog"
963
+
964
+ # Parse arguments
965
+ print_only = "--print" in args
966
+ local_mode = "--local" in args
967
+
968
+ # Custom URL
969
+ if "--url" in args:
970
+ idx = args.index("--url")
971
+ if idx + 1 < len(args):
972
+ catalog_url = args[idx + 1]
973
+
974
+ if local_mode:
975
+ print("Local catalog server not implemented yet.")
976
+ print(f"Visit the online catalog at: {catalog_url}")
977
+ return 0
978
+
979
+ if print_only:
980
+ print(catalog_url)
981
+ return 0
982
+
983
+ print(f"Opening template catalog: {catalog_url}")
984
+ try:
985
+ webbrowser.open(catalog_url)
986
+ return 0
987
+ except Exception as e:
988
+ print(f"Failed to open browser: {e}")
989
+ print(f"Visit: {catalog_url}")
990
+ return 1
991
+
992
+ def cmd_catalog(self, args: List[str]) -> int:
993
+ """
994
+ Catalog management commands.
995
+
996
+ Subcommands:
997
+ build Build catalog locally
998
+ sync Sync catalog sources
999
+ """
1000
+ if not args:
1001
+ print("Usage: praisonai templates catalog <build|sync> [options]")
1002
+ print("\nSubcommands:")
1003
+ print(" build Build catalog locally")
1004
+ print(" sync Sync catalog sources from GitHub")
1005
+ print("\nExamples:")
1006
+ print(" praisonai templates catalog build --out ./dist")
1007
+ print(" praisonai templates catalog sync --source agent-recipes")
1008
+ return 0
1009
+
1010
+ subcmd = args[0]
1011
+ remaining = args[1:]
1012
+
1013
+ if subcmd == "build":
1014
+ return self._catalog_build(remaining)
1015
+ elif subcmd == "sync":
1016
+ return self._catalog_sync(remaining)
1017
+ else:
1018
+ print(f"Unknown catalog command: {subcmd}")
1019
+ return 1
1020
+
1021
+ def _catalog_build(self, args: List[str]) -> int:
1022
+ """Build catalog locally."""
1023
+ import subprocess
1024
+ import os
1025
+
1026
+ # Parse arguments
1027
+ out_dir = None
1028
+ source_dir = None
1029
+ minify = "--minify" in args
1030
+
1031
+ if "--out" in args:
1032
+ idx = args.index("--out")
1033
+ if idx + 1 < len(args):
1034
+ out_dir = args[idx + 1]
1035
+
1036
+ if "--source" in args:
1037
+ idx = args.index("--source")
1038
+ if idx + 1 < len(args):
1039
+ source_dir = args[idx + 1]
1040
+
1041
+ # Check if catalog repo is available locally
1042
+ catalog_repo = Path.home() / "praisonai-template-catalog"
1043
+ if not catalog_repo.exists():
1044
+ # Try to find it relative to this package
1045
+ possible_paths = [
1046
+ Path(__file__).parent.parent.parent.parent.parent.parent / "praisonai-template-catalog",
1047
+ Path.cwd() / "praisonai-template-catalog",
1048
+ ]
1049
+ for p in possible_paths:
1050
+ if p.exists():
1051
+ catalog_repo = p
1052
+ break
1053
+
1054
+ if catalog_repo.exists() and (catalog_repo / "scripts" / "build-catalog.js").exists():
1055
+ print(f"Building catalog using: {catalog_repo}")
1056
+ cmd = ["node", "scripts/build-catalog.js"]
1057
+ if out_dir:
1058
+ cmd.extend(["--out", out_dir])
1059
+ if source_dir:
1060
+ cmd.extend(["--source", source_dir])
1061
+ if minify:
1062
+ cmd.append("--minify")
1063
+
1064
+ try:
1065
+ result = subprocess.run(cmd, cwd=str(catalog_repo), capture_output=True, text=True)
1066
+ print(result.stdout)
1067
+ if result.stderr:
1068
+ print(result.stderr)
1069
+ return result.returncode
1070
+ except FileNotFoundError:
1071
+ print("Node.js not found. Please install Node.js to build the catalog.")
1072
+ return 1
1073
+ else:
1074
+ # Fallback: Generate minimal catalog using Python
1075
+ print("Catalog repo not found locally. Generating minimal catalog...")
1076
+ return self._generate_minimal_catalog(out_dir, source_dir)
1077
+
1078
+ def _generate_minimal_catalog(self, out_dir: str = None, source_dir: str = None) -> int:
1079
+ """Generate minimal catalog JSON using Python."""
1080
+ import json
1081
+ from datetime import datetime
1082
+
1083
+ try:
1084
+ import yaml
1085
+ except ImportError:
1086
+ print("PyYAML not installed. Install with: pip install pyyaml")
1087
+ return 1
1088
+
1089
+ # Find templates
1090
+ if source_dir:
1091
+ templates_dir = Path(source_dir)
1092
+ else:
1093
+ # Try Agent-Recipes
1094
+ possible_paths = [
1095
+ Path.home() / "Agent-Recipes" / "agent_recipes" / "templates",
1096
+ Path.cwd() / "Agent-Recipes" / "agent_recipes" / "templates",
1097
+ ]
1098
+ templates_dir = None
1099
+ for p in possible_paths:
1100
+ if p.exists():
1101
+ templates_dir = p
1102
+ break
1103
+
1104
+ if not templates_dir:
1105
+ # Try package templates
1106
+ try:
1107
+ import agent_recipes
1108
+ templates_dir = Path(agent_recipes.__file__).parent / "templates"
1109
+ except ImportError:
1110
+ pass
1111
+
1112
+ if not templates_dir or not templates_dir.exists():
1113
+ print("No templates directory found.")
1114
+ return 1
1115
+
1116
+ print(f"Scanning templates in: {templates_dir}")
1117
+
1118
+ templates = []
1119
+ for entry in templates_dir.iterdir():
1120
+ if not entry.is_dir():
1121
+ continue
1122
+ template_yaml = entry / "TEMPLATE.yaml"
1123
+ if template_yaml.exists():
1124
+ try:
1125
+ with open(template_yaml) as f:
1126
+ data = yaml.safe_load(f)
1127
+ if data:
1128
+ templates.append({
1129
+ "name": data.get("name", entry.name),
1130
+ "version": data.get("version", "1.0.0"),
1131
+ "description": data.get("description", ""),
1132
+ "author": data.get("author", "Unknown"),
1133
+ "license": data.get("license", "Apache-2.0"),
1134
+ "tags": data.get("tags", []),
1135
+ "requires": data.get("requires", {}),
1136
+ })
1137
+ except Exception as e:
1138
+ print(f" Warning: Failed to parse {template_yaml}: {e}")
1139
+
1140
+ # Output
1141
+ output = {
1142
+ "version": "1.0.0",
1143
+ "generated_at": datetime.now().isoformat(),
1144
+ "count": len(templates),
1145
+ "templates": templates
1146
+ }
1147
+
1148
+ out_path = Path(out_dir) if out_dir else Path.cwd() / "templates.json"
1149
+ if out_path.is_dir():
1150
+ out_path = out_path / "templates.json"
1151
+
1152
+ out_path.parent.mkdir(parents=True, exist_ok=True)
1153
+ with open(out_path, "w") as f:
1154
+ json.dump(output, f, indent=2)
1155
+
1156
+ print(f"Generated: {out_path} ({len(templates)} templates)")
1157
+ return 0
1158
+
1159
+ def _catalog_sync(self, args: List[str]) -> int:
1160
+ """Sync catalog sources."""
1161
+ import subprocess
1162
+
1163
+ # Parse arguments
1164
+ config_path = None
1165
+ source_name = None
1166
+ cache_dir = None
1167
+
1168
+ if "--config" in args:
1169
+ idx = args.index("--config")
1170
+ if idx + 1 < len(args):
1171
+ config_path = args[idx + 1]
1172
+
1173
+ if "--source" in args:
1174
+ idx = args.index("--source")
1175
+ if idx + 1 < len(args):
1176
+ source_name = args[idx + 1]
1177
+
1178
+ if "--cache-dir" in args:
1179
+ idx = args.index("--cache-dir")
1180
+ if idx + 1 < len(args):
1181
+ cache_dir = args[idx + 1]
1182
+
1183
+ # Check if catalog repo is available
1184
+ catalog_repo = Path.home() / "praisonai-template-catalog"
1185
+ if catalog_repo.exists() and (catalog_repo / "scripts" / "sync-sources.js").exists():
1186
+ print(f"Syncing using: {catalog_repo}")
1187
+ cmd = ["node", "scripts/sync-sources.js"]
1188
+ if config_path:
1189
+ cmd.extend(["--config", config_path])
1190
+ if source_name:
1191
+ cmd.extend(["--source", source_name])
1192
+ if cache_dir:
1193
+ cmd.extend(["--cache-dir", cache_dir])
1194
+
1195
+ try:
1196
+ result = subprocess.run(cmd, cwd=str(catalog_repo), capture_output=True, text=True)
1197
+ print(result.stdout)
1198
+ if result.stderr:
1199
+ print(result.stderr)
1200
+ return result.returncode
1201
+ except FileNotFoundError:
1202
+ print("Node.js not found. Please install Node.js.")
1203
+ return 1
1204
+ else:
1205
+ # Fallback: Clone Agent-Recipes
1206
+ print("Catalog repo not found. Cloning Agent-Recipes directly...")
1207
+ cache_path = Path(cache_dir) if cache_dir else Path.home() / ".praison" / "cache"
1208
+ cache_path.mkdir(parents=True, exist_ok=True)
1209
+
1210
+ target = cache_path / "Agent-Recipes"
1211
+ if target.exists():
1212
+ print(f"Updating: {target}")
1213
+ try:
1214
+ subprocess.run(["git", "-C", str(target), "pull", "--depth=1"], check=True)
1215
+ print("✓ Updated successfully")
1216
+ return 0
1217
+ except subprocess.CalledProcessError as e:
1218
+ print(f"Failed to update: {e}")
1219
+ return 1
1220
+ else:
1221
+ print(f"Cloning to: {target}")
1222
+ try:
1223
+ subprocess.run([
1224
+ "git", "clone", "--depth=1",
1225
+ "https://github.com/MervinPraison/Agent-Recipes.git",
1226
+ str(target)
1227
+ ], check=True)
1228
+ print("✓ Cloned successfully")
1229
+ return 0
1230
+ except subprocess.CalledProcessError as e:
1231
+ print(f"Failed to clone: {e}")
1232
+ return 1
1233
+
1234
+ def cmd_validate(self, args: List[str]) -> int:
1235
+ """
1236
+ Validate template YAML files.
1237
+
1238
+ Args:
1239
+ args: [--source <dir>] [--strict] [--json]
1240
+ """
1241
+ import subprocess
1242
+
1243
+ # Parse arguments
1244
+ source_dir = None
1245
+ strict = "--strict" in args
1246
+ json_output = "--json" in args
1247
+
1248
+ if "--source" in args:
1249
+ idx = args.index("--source")
1250
+ if idx + 1 < len(args):
1251
+ source_dir = args[idx + 1]
1252
+
1253
+ # Check if catalog repo is available
1254
+ catalog_repo = Path.home() / "praisonai-template-catalog"
1255
+ if catalog_repo.exists() and (catalog_repo / "scripts" / "validate-templates.js").exists():
1256
+ cmd = ["node", "scripts/validate-templates.js"]
1257
+ if source_dir:
1258
+ cmd.extend(["--source", source_dir])
1259
+ if strict:
1260
+ cmd.append("--strict")
1261
+ if json_output:
1262
+ cmd.append("--json")
1263
+
1264
+ try:
1265
+ result = subprocess.run(cmd, cwd=str(catalog_repo), capture_output=True, text=True)
1266
+ print(result.stdout)
1267
+ if result.stderr:
1268
+ print(result.stderr)
1269
+ return result.returncode
1270
+ except FileNotFoundError:
1271
+ print("Node.js not found.")
1272
+ return 1
1273
+ else:
1274
+ # Fallback: Basic Python validation
1275
+ return self._validate_templates_python(source_dir, strict, json_output)
1276
+
1277
+ def _validate_templates_python(self, source_dir: str = None, strict: bool = False, json_output: bool = False) -> int:
1278
+ """Validate templates using Python."""
1279
+ import json as json_module
1280
+
1281
+ try:
1282
+ import yaml
1283
+ except ImportError:
1284
+ print("PyYAML not installed. Install with: pip install pyyaml")
1285
+ return 1
1286
+
1287
+ # Find templates directory
1288
+ if source_dir:
1289
+ templates_dir = Path(source_dir)
1290
+ else:
1291
+ possible_paths = [
1292
+ Path.home() / "Agent-Recipes" / "agent_recipes" / "templates",
1293
+ Path.cwd() / "Agent-Recipes" / "agent_recipes" / "templates",
1294
+ ]
1295
+ templates_dir = None
1296
+ for p in possible_paths:
1297
+ if p.exists():
1298
+ templates_dir = p
1299
+ break
1300
+
1301
+ if not templates_dir or not templates_dir.exists():
1302
+ print(f"Templates directory not found: {source_dir or 'default locations'}")
1303
+ return 1
1304
+
1305
+ print(f"Validating templates in: {templates_dir}\n")
1306
+
1307
+ results = []
1308
+ errors_count = 0
1309
+ warnings_count = 0
1310
+
1311
+ for entry in sorted(templates_dir.iterdir()):
1312
+ if not entry.is_dir() or entry.name.startswith("."):
1313
+ continue
1314
+
1315
+ template_yaml = entry / "TEMPLATE.yaml"
1316
+ if not template_yaml.exists():
1317
+ continue
1318
+
1319
+ result = {"name": entry.name, "valid": True, "errors": [], "warnings": []}
1320
+
1321
+ try:
1322
+ with open(template_yaml) as f:
1323
+ data = yaml.safe_load(f)
1324
+
1325
+ # Check required fields
1326
+ required = ["name", "version", "description"]
1327
+ for field in required:
1328
+ if not data.get(field):
1329
+ result["errors"].append(f"Missing required field: {field}")
1330
+ result["valid"] = False
1331
+
1332
+ # Check version format
1333
+ version = data.get("version", "")
1334
+ if version and not self._is_valid_version(version):
1335
+ result["warnings"].append(f"Invalid version format: {version}")
1336
+
1337
+ # Check workflow file
1338
+ workflow = data.get("workflow", "workflow.yaml")
1339
+ if isinstance(workflow, str) and not (entry / workflow).exists():
1340
+ if strict:
1341
+ result["errors"].append(f"Workflow file not found: {workflow}")
1342
+ result["valid"] = False
1343
+ else:
1344
+ result["warnings"].append(f"Workflow file not found: {workflow}")
1345
+
1346
+ except yaml.YAMLError as e:
1347
+ result["errors"].append(f"Invalid YAML: {e}")
1348
+ result["valid"] = False
1349
+ except Exception as e:
1350
+ result["errors"].append(f"Error: {e}")
1351
+ result["valid"] = False
1352
+
1353
+ results.append(result)
1354
+ errors_count += len(result["errors"])
1355
+ warnings_count += len(result["warnings"])
1356
+
1357
+ # Output
1358
+ if json_output:
1359
+ print(json_module.dumps(results, indent=2))
1360
+ else:
1361
+ for r in results:
1362
+ status = "✓" if r["valid"] else "✗"
1363
+ color = "\033[32m" if r["valid"] else "\033[31m"
1364
+ print(f"{color}{status}\033[0m {r['name']}")
1365
+ for err in r["errors"]:
1366
+ print(f" \033[31m✗ ERROR:\033[0m {err}")
1367
+ for warn in r["warnings"]:
1368
+ print(f" \033[33m⚠ WARNING:\033[0m {warn}")
1369
+
1370
+ print(f"\n{len(results)} templates checked, {errors_count} errors, {warnings_count} warnings")
1371
+
1372
+ return 1 if errors_count > 0 else 0
1373
+
1374
+ def _is_valid_version(self, version: str) -> bool:
1375
+ """Check if version string is valid semver."""
1376
+ import re
1377
+ return bool(re.match(r'^\d+\.\d+\.\d+', version))
1378
+
1379
+
1380
+ def import_time():
1381
+ """Get current time (helper for cache expiry display)."""
1382
+ import time
1383
+ return time.time()
1384
+
1385
+
1386
+ # Convenience function for CLI integration
1387
+ def handle_templates_command(args: List[str]) -> int:
1388
+ """Handle templates command from main CLI."""
1389
+ handler = TemplatesHandler()
1390
+ return handler.handle(args)