PraisonAI 3.0.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 (393) hide show
  1. praisonai/__init__.py +54 -0
  2. praisonai/__main__.py +15 -0
  3. praisonai/acp/__init__.py +54 -0
  4. praisonai/acp/config.py +159 -0
  5. praisonai/acp/server.py +587 -0
  6. praisonai/acp/session.py +219 -0
  7. praisonai/adapters/__init__.py +50 -0
  8. praisonai/adapters/readers.py +395 -0
  9. praisonai/adapters/rerankers.py +315 -0
  10. praisonai/adapters/retrievers.py +394 -0
  11. praisonai/adapters/vector_stores.py +409 -0
  12. praisonai/agent_scheduler.py +337 -0
  13. praisonai/agents_generator.py +903 -0
  14. praisonai/api/call.py +292 -0
  15. praisonai/auto.py +1197 -0
  16. praisonai/capabilities/__init__.py +275 -0
  17. praisonai/capabilities/a2a.py +140 -0
  18. praisonai/capabilities/assistants.py +283 -0
  19. praisonai/capabilities/audio.py +320 -0
  20. praisonai/capabilities/batches.py +469 -0
  21. praisonai/capabilities/completions.py +336 -0
  22. praisonai/capabilities/container_files.py +155 -0
  23. praisonai/capabilities/containers.py +93 -0
  24. praisonai/capabilities/embeddings.py +158 -0
  25. praisonai/capabilities/files.py +467 -0
  26. praisonai/capabilities/fine_tuning.py +293 -0
  27. praisonai/capabilities/guardrails.py +182 -0
  28. praisonai/capabilities/images.py +330 -0
  29. praisonai/capabilities/mcp.py +190 -0
  30. praisonai/capabilities/messages.py +270 -0
  31. praisonai/capabilities/moderations.py +154 -0
  32. praisonai/capabilities/ocr.py +217 -0
  33. praisonai/capabilities/passthrough.py +204 -0
  34. praisonai/capabilities/rag.py +207 -0
  35. praisonai/capabilities/realtime.py +160 -0
  36. praisonai/capabilities/rerank.py +165 -0
  37. praisonai/capabilities/responses.py +266 -0
  38. praisonai/capabilities/search.py +109 -0
  39. praisonai/capabilities/skills.py +133 -0
  40. praisonai/capabilities/vector_store_files.py +334 -0
  41. praisonai/capabilities/vector_stores.py +304 -0
  42. praisonai/capabilities/videos.py +141 -0
  43. praisonai/chainlit_ui.py +304 -0
  44. praisonai/chat/__init__.py +106 -0
  45. praisonai/chat/app.py +125 -0
  46. praisonai/cli/__init__.py +26 -0
  47. praisonai/cli/app.py +213 -0
  48. praisonai/cli/commands/__init__.py +75 -0
  49. praisonai/cli/commands/acp.py +70 -0
  50. praisonai/cli/commands/completion.py +333 -0
  51. praisonai/cli/commands/config.py +166 -0
  52. praisonai/cli/commands/debug.py +142 -0
  53. praisonai/cli/commands/diag.py +55 -0
  54. praisonai/cli/commands/doctor.py +166 -0
  55. praisonai/cli/commands/environment.py +179 -0
  56. praisonai/cli/commands/lsp.py +112 -0
  57. praisonai/cli/commands/mcp.py +210 -0
  58. praisonai/cli/commands/profile.py +457 -0
  59. praisonai/cli/commands/run.py +228 -0
  60. praisonai/cli/commands/schedule.py +150 -0
  61. praisonai/cli/commands/serve.py +97 -0
  62. praisonai/cli/commands/session.py +212 -0
  63. praisonai/cli/commands/traces.py +145 -0
  64. praisonai/cli/commands/version.py +101 -0
  65. praisonai/cli/configuration/__init__.py +18 -0
  66. praisonai/cli/configuration/loader.py +353 -0
  67. praisonai/cli/configuration/paths.py +114 -0
  68. praisonai/cli/configuration/schema.py +164 -0
  69. praisonai/cli/features/__init__.py +268 -0
  70. praisonai/cli/features/acp.py +236 -0
  71. praisonai/cli/features/action_orchestrator.py +546 -0
  72. praisonai/cli/features/agent_scheduler.py +773 -0
  73. praisonai/cli/features/agent_tools.py +474 -0
  74. praisonai/cli/features/agents.py +375 -0
  75. praisonai/cli/features/at_mentions.py +471 -0
  76. praisonai/cli/features/auto_memory.py +182 -0
  77. praisonai/cli/features/autonomy_mode.py +490 -0
  78. praisonai/cli/features/background.py +356 -0
  79. praisonai/cli/features/base.py +168 -0
  80. praisonai/cli/features/capabilities.py +1326 -0
  81. praisonai/cli/features/checkpoints.py +338 -0
  82. praisonai/cli/features/code_intelligence.py +652 -0
  83. praisonai/cli/features/compaction.py +294 -0
  84. praisonai/cli/features/compare.py +534 -0
  85. praisonai/cli/features/cost_tracker.py +514 -0
  86. praisonai/cli/features/debug.py +810 -0
  87. praisonai/cli/features/deploy.py +517 -0
  88. praisonai/cli/features/diag.py +289 -0
  89. praisonai/cli/features/doctor/__init__.py +63 -0
  90. praisonai/cli/features/doctor/checks/__init__.py +24 -0
  91. praisonai/cli/features/doctor/checks/acp_checks.py +240 -0
  92. praisonai/cli/features/doctor/checks/config_checks.py +366 -0
  93. praisonai/cli/features/doctor/checks/db_checks.py +366 -0
  94. praisonai/cli/features/doctor/checks/env_checks.py +543 -0
  95. praisonai/cli/features/doctor/checks/lsp_checks.py +199 -0
  96. praisonai/cli/features/doctor/checks/mcp_checks.py +349 -0
  97. praisonai/cli/features/doctor/checks/memory_checks.py +268 -0
  98. praisonai/cli/features/doctor/checks/network_checks.py +251 -0
  99. praisonai/cli/features/doctor/checks/obs_checks.py +328 -0
  100. praisonai/cli/features/doctor/checks/performance_checks.py +235 -0
  101. praisonai/cli/features/doctor/checks/permissions_checks.py +259 -0
  102. praisonai/cli/features/doctor/checks/selftest_checks.py +322 -0
  103. praisonai/cli/features/doctor/checks/serve_checks.py +426 -0
  104. praisonai/cli/features/doctor/checks/skills_checks.py +231 -0
  105. praisonai/cli/features/doctor/checks/tools_checks.py +371 -0
  106. praisonai/cli/features/doctor/engine.py +266 -0
  107. praisonai/cli/features/doctor/formatters.py +310 -0
  108. praisonai/cli/features/doctor/handler.py +397 -0
  109. praisonai/cli/features/doctor/models.py +264 -0
  110. praisonai/cli/features/doctor/registry.py +239 -0
  111. praisonai/cli/features/endpoints.py +1019 -0
  112. praisonai/cli/features/eval.py +560 -0
  113. praisonai/cli/features/external_agents.py +231 -0
  114. praisonai/cli/features/fast_context.py +410 -0
  115. praisonai/cli/features/flow_display.py +566 -0
  116. praisonai/cli/features/git_integration.py +651 -0
  117. praisonai/cli/features/guardrail.py +171 -0
  118. praisonai/cli/features/handoff.py +185 -0
  119. praisonai/cli/features/hooks.py +583 -0
  120. praisonai/cli/features/image.py +384 -0
  121. praisonai/cli/features/interactive_runtime.py +585 -0
  122. praisonai/cli/features/interactive_tools.py +380 -0
  123. praisonai/cli/features/interactive_tui.py +603 -0
  124. praisonai/cli/features/jobs.py +632 -0
  125. praisonai/cli/features/knowledge.py +531 -0
  126. praisonai/cli/features/lite.py +244 -0
  127. praisonai/cli/features/lsp_cli.py +225 -0
  128. praisonai/cli/features/mcp.py +169 -0
  129. praisonai/cli/features/message_queue.py +587 -0
  130. praisonai/cli/features/metrics.py +211 -0
  131. praisonai/cli/features/n8n.py +673 -0
  132. praisonai/cli/features/observability.py +293 -0
  133. praisonai/cli/features/ollama.py +361 -0
  134. praisonai/cli/features/output_style.py +273 -0
  135. praisonai/cli/features/package.py +631 -0
  136. praisonai/cli/features/performance.py +308 -0
  137. praisonai/cli/features/persistence.py +636 -0
  138. praisonai/cli/features/profile.py +226 -0
  139. praisonai/cli/features/profiler/__init__.py +81 -0
  140. praisonai/cli/features/profiler/core.py +558 -0
  141. praisonai/cli/features/profiler/optimizations.py +652 -0
  142. praisonai/cli/features/profiler/suite.py +386 -0
  143. praisonai/cli/features/profiling.py +350 -0
  144. praisonai/cli/features/queue/__init__.py +73 -0
  145. praisonai/cli/features/queue/manager.py +395 -0
  146. praisonai/cli/features/queue/models.py +286 -0
  147. praisonai/cli/features/queue/persistence.py +564 -0
  148. praisonai/cli/features/queue/scheduler.py +484 -0
  149. praisonai/cli/features/queue/worker.py +372 -0
  150. praisonai/cli/features/recipe.py +1723 -0
  151. praisonai/cli/features/recipes.py +449 -0
  152. praisonai/cli/features/registry.py +229 -0
  153. praisonai/cli/features/repo_map.py +860 -0
  154. praisonai/cli/features/router.py +466 -0
  155. praisonai/cli/features/sandbox_executor.py +515 -0
  156. praisonai/cli/features/serve.py +829 -0
  157. praisonai/cli/features/session.py +222 -0
  158. praisonai/cli/features/skills.py +856 -0
  159. praisonai/cli/features/slash_commands.py +650 -0
  160. praisonai/cli/features/telemetry.py +179 -0
  161. praisonai/cli/features/templates.py +1384 -0
  162. praisonai/cli/features/thinking.py +305 -0
  163. praisonai/cli/features/todo.py +334 -0
  164. praisonai/cli/features/tools.py +680 -0
  165. praisonai/cli/features/tui/__init__.py +83 -0
  166. praisonai/cli/features/tui/app.py +580 -0
  167. praisonai/cli/features/tui/cli.py +566 -0
  168. praisonai/cli/features/tui/debug.py +511 -0
  169. praisonai/cli/features/tui/events.py +99 -0
  170. praisonai/cli/features/tui/mock_provider.py +328 -0
  171. praisonai/cli/features/tui/orchestrator.py +652 -0
  172. praisonai/cli/features/tui/screens/__init__.py +50 -0
  173. praisonai/cli/features/tui/screens/main.py +245 -0
  174. praisonai/cli/features/tui/screens/queue.py +174 -0
  175. praisonai/cli/features/tui/screens/session.py +124 -0
  176. praisonai/cli/features/tui/screens/settings.py +148 -0
  177. praisonai/cli/features/tui/widgets/__init__.py +56 -0
  178. praisonai/cli/features/tui/widgets/chat.py +261 -0
  179. praisonai/cli/features/tui/widgets/composer.py +224 -0
  180. praisonai/cli/features/tui/widgets/queue_panel.py +200 -0
  181. praisonai/cli/features/tui/widgets/status.py +167 -0
  182. praisonai/cli/features/tui/widgets/tool_panel.py +248 -0
  183. praisonai/cli/features/workflow.py +720 -0
  184. praisonai/cli/legacy.py +236 -0
  185. praisonai/cli/main.py +5559 -0
  186. praisonai/cli/schedule_cli.py +54 -0
  187. praisonai/cli/state/__init__.py +31 -0
  188. praisonai/cli/state/identifiers.py +161 -0
  189. praisonai/cli/state/sessions.py +313 -0
  190. praisonai/code/__init__.py +93 -0
  191. praisonai/code/agent_tools.py +344 -0
  192. praisonai/code/diff/__init__.py +21 -0
  193. praisonai/code/diff/diff_strategy.py +432 -0
  194. praisonai/code/tools/__init__.py +27 -0
  195. praisonai/code/tools/apply_diff.py +221 -0
  196. praisonai/code/tools/execute_command.py +275 -0
  197. praisonai/code/tools/list_files.py +274 -0
  198. praisonai/code/tools/read_file.py +206 -0
  199. praisonai/code/tools/search_replace.py +248 -0
  200. praisonai/code/tools/write_file.py +217 -0
  201. praisonai/code/utils/__init__.py +46 -0
  202. praisonai/code/utils/file_utils.py +307 -0
  203. praisonai/code/utils/ignore_utils.py +308 -0
  204. praisonai/code/utils/text_utils.py +276 -0
  205. praisonai/db/__init__.py +64 -0
  206. praisonai/db/adapter.py +531 -0
  207. praisonai/deploy/__init__.py +62 -0
  208. praisonai/deploy/api.py +231 -0
  209. praisonai/deploy/docker.py +454 -0
  210. praisonai/deploy/doctor.py +367 -0
  211. praisonai/deploy/main.py +327 -0
  212. praisonai/deploy/models.py +179 -0
  213. praisonai/deploy/providers/__init__.py +33 -0
  214. praisonai/deploy/providers/aws.py +331 -0
  215. praisonai/deploy/providers/azure.py +358 -0
  216. praisonai/deploy/providers/base.py +101 -0
  217. praisonai/deploy/providers/gcp.py +314 -0
  218. praisonai/deploy/schema.py +208 -0
  219. praisonai/deploy.py +185 -0
  220. praisonai/endpoints/__init__.py +53 -0
  221. praisonai/endpoints/a2u_server.py +410 -0
  222. praisonai/endpoints/discovery.py +165 -0
  223. praisonai/endpoints/providers/__init__.py +28 -0
  224. praisonai/endpoints/providers/a2a.py +253 -0
  225. praisonai/endpoints/providers/a2u.py +208 -0
  226. praisonai/endpoints/providers/agents_api.py +171 -0
  227. praisonai/endpoints/providers/base.py +231 -0
  228. praisonai/endpoints/providers/mcp.py +263 -0
  229. praisonai/endpoints/providers/recipe.py +206 -0
  230. praisonai/endpoints/providers/tools_mcp.py +150 -0
  231. praisonai/endpoints/registry.py +131 -0
  232. praisonai/endpoints/server.py +161 -0
  233. praisonai/inbuilt_tools/__init__.py +24 -0
  234. praisonai/inbuilt_tools/autogen_tools.py +117 -0
  235. praisonai/inc/__init__.py +2 -0
  236. praisonai/inc/config.py +96 -0
  237. praisonai/inc/models.py +155 -0
  238. praisonai/integrations/__init__.py +56 -0
  239. praisonai/integrations/base.py +303 -0
  240. praisonai/integrations/claude_code.py +270 -0
  241. praisonai/integrations/codex_cli.py +255 -0
  242. praisonai/integrations/cursor_cli.py +195 -0
  243. praisonai/integrations/gemini_cli.py +222 -0
  244. praisonai/jobs/__init__.py +67 -0
  245. praisonai/jobs/executor.py +425 -0
  246. praisonai/jobs/models.py +230 -0
  247. praisonai/jobs/router.py +314 -0
  248. praisonai/jobs/server.py +186 -0
  249. praisonai/jobs/store.py +203 -0
  250. praisonai/llm/__init__.py +66 -0
  251. praisonai/llm/registry.py +382 -0
  252. praisonai/mcp_server/__init__.py +152 -0
  253. praisonai/mcp_server/adapters/__init__.py +74 -0
  254. praisonai/mcp_server/adapters/agents.py +128 -0
  255. praisonai/mcp_server/adapters/capabilities.py +168 -0
  256. praisonai/mcp_server/adapters/cli_tools.py +568 -0
  257. praisonai/mcp_server/adapters/extended_capabilities.py +462 -0
  258. praisonai/mcp_server/adapters/knowledge.py +93 -0
  259. praisonai/mcp_server/adapters/memory.py +104 -0
  260. praisonai/mcp_server/adapters/prompts.py +306 -0
  261. praisonai/mcp_server/adapters/resources.py +124 -0
  262. praisonai/mcp_server/adapters/tools_bridge.py +280 -0
  263. praisonai/mcp_server/auth/__init__.py +48 -0
  264. praisonai/mcp_server/auth/api_key.py +291 -0
  265. praisonai/mcp_server/auth/oauth.py +460 -0
  266. praisonai/mcp_server/auth/oidc.py +289 -0
  267. praisonai/mcp_server/auth/scopes.py +260 -0
  268. praisonai/mcp_server/cli.py +852 -0
  269. praisonai/mcp_server/elicitation.py +445 -0
  270. praisonai/mcp_server/icons.py +302 -0
  271. praisonai/mcp_server/recipe_adapter.py +573 -0
  272. praisonai/mcp_server/recipe_cli.py +824 -0
  273. praisonai/mcp_server/registry.py +703 -0
  274. praisonai/mcp_server/sampling.py +422 -0
  275. praisonai/mcp_server/server.py +490 -0
  276. praisonai/mcp_server/tasks.py +443 -0
  277. praisonai/mcp_server/transports/__init__.py +18 -0
  278. praisonai/mcp_server/transports/http_stream.py +376 -0
  279. praisonai/mcp_server/transports/stdio.py +132 -0
  280. praisonai/persistence/__init__.py +84 -0
  281. praisonai/persistence/config.py +238 -0
  282. praisonai/persistence/conversation/__init__.py +25 -0
  283. praisonai/persistence/conversation/async_mysql.py +427 -0
  284. praisonai/persistence/conversation/async_postgres.py +410 -0
  285. praisonai/persistence/conversation/async_sqlite.py +371 -0
  286. praisonai/persistence/conversation/base.py +151 -0
  287. praisonai/persistence/conversation/json_store.py +250 -0
  288. praisonai/persistence/conversation/mysql.py +387 -0
  289. praisonai/persistence/conversation/postgres.py +401 -0
  290. praisonai/persistence/conversation/singlestore.py +240 -0
  291. praisonai/persistence/conversation/sqlite.py +341 -0
  292. praisonai/persistence/conversation/supabase.py +203 -0
  293. praisonai/persistence/conversation/surrealdb.py +287 -0
  294. praisonai/persistence/factory.py +301 -0
  295. praisonai/persistence/hooks/__init__.py +18 -0
  296. praisonai/persistence/hooks/agent_hooks.py +297 -0
  297. praisonai/persistence/knowledge/__init__.py +26 -0
  298. praisonai/persistence/knowledge/base.py +144 -0
  299. praisonai/persistence/knowledge/cassandra.py +232 -0
  300. praisonai/persistence/knowledge/chroma.py +295 -0
  301. praisonai/persistence/knowledge/clickhouse.py +242 -0
  302. praisonai/persistence/knowledge/cosmosdb_vector.py +438 -0
  303. praisonai/persistence/knowledge/couchbase.py +286 -0
  304. praisonai/persistence/knowledge/lancedb.py +216 -0
  305. praisonai/persistence/knowledge/langchain_adapter.py +291 -0
  306. praisonai/persistence/knowledge/lightrag_adapter.py +212 -0
  307. praisonai/persistence/knowledge/llamaindex_adapter.py +256 -0
  308. praisonai/persistence/knowledge/milvus.py +277 -0
  309. praisonai/persistence/knowledge/mongodb_vector.py +306 -0
  310. praisonai/persistence/knowledge/pgvector.py +335 -0
  311. praisonai/persistence/knowledge/pinecone.py +253 -0
  312. praisonai/persistence/knowledge/qdrant.py +301 -0
  313. praisonai/persistence/knowledge/redis_vector.py +291 -0
  314. praisonai/persistence/knowledge/singlestore_vector.py +299 -0
  315. praisonai/persistence/knowledge/surrealdb_vector.py +309 -0
  316. praisonai/persistence/knowledge/upstash_vector.py +266 -0
  317. praisonai/persistence/knowledge/weaviate.py +223 -0
  318. praisonai/persistence/migrations/__init__.py +10 -0
  319. praisonai/persistence/migrations/manager.py +251 -0
  320. praisonai/persistence/orchestrator.py +406 -0
  321. praisonai/persistence/state/__init__.py +21 -0
  322. praisonai/persistence/state/async_mongodb.py +200 -0
  323. praisonai/persistence/state/base.py +107 -0
  324. praisonai/persistence/state/dynamodb.py +226 -0
  325. praisonai/persistence/state/firestore.py +175 -0
  326. praisonai/persistence/state/gcs.py +155 -0
  327. praisonai/persistence/state/memory.py +245 -0
  328. praisonai/persistence/state/mongodb.py +158 -0
  329. praisonai/persistence/state/redis.py +190 -0
  330. praisonai/persistence/state/upstash.py +144 -0
  331. praisonai/persistence/tests/__init__.py +3 -0
  332. praisonai/persistence/tests/test_all_backends.py +633 -0
  333. praisonai/profiler.py +1214 -0
  334. praisonai/recipe/__init__.py +134 -0
  335. praisonai/recipe/bridge.py +278 -0
  336. praisonai/recipe/core.py +893 -0
  337. praisonai/recipe/exceptions.py +54 -0
  338. praisonai/recipe/history.py +402 -0
  339. praisonai/recipe/models.py +266 -0
  340. praisonai/recipe/operations.py +440 -0
  341. praisonai/recipe/policy.py +422 -0
  342. praisonai/recipe/registry.py +849 -0
  343. praisonai/recipe/runtime.py +214 -0
  344. praisonai/recipe/security.py +711 -0
  345. praisonai/recipe/serve.py +859 -0
  346. praisonai/recipe/server.py +613 -0
  347. praisonai/scheduler/__init__.py +45 -0
  348. praisonai/scheduler/agent_scheduler.py +552 -0
  349. praisonai/scheduler/base.py +124 -0
  350. praisonai/scheduler/daemon_manager.py +225 -0
  351. praisonai/scheduler/state_manager.py +155 -0
  352. praisonai/scheduler/yaml_loader.py +193 -0
  353. praisonai/scheduler.py +194 -0
  354. praisonai/setup/__init__.py +1 -0
  355. praisonai/setup/build.py +21 -0
  356. praisonai/setup/post_install.py +23 -0
  357. praisonai/setup/setup_conda_env.py +25 -0
  358. praisonai/setup.py +16 -0
  359. praisonai/templates/__init__.py +116 -0
  360. praisonai/templates/cache.py +364 -0
  361. praisonai/templates/dependency_checker.py +358 -0
  362. praisonai/templates/discovery.py +391 -0
  363. praisonai/templates/loader.py +564 -0
  364. praisonai/templates/registry.py +511 -0
  365. praisonai/templates/resolver.py +206 -0
  366. praisonai/templates/security.py +327 -0
  367. praisonai/templates/tool_override.py +498 -0
  368. praisonai/templates/tools_doctor.py +256 -0
  369. praisonai/test.py +105 -0
  370. praisonai/train.py +562 -0
  371. praisonai/train_vision.py +306 -0
  372. praisonai/ui/agents.py +824 -0
  373. praisonai/ui/callbacks.py +57 -0
  374. praisonai/ui/chainlit_compat.py +246 -0
  375. praisonai/ui/chat.py +532 -0
  376. praisonai/ui/code.py +717 -0
  377. praisonai/ui/colab.py +474 -0
  378. praisonai/ui/colab_chainlit.py +81 -0
  379. praisonai/ui/components/aicoder.py +284 -0
  380. praisonai/ui/context.py +283 -0
  381. praisonai/ui/database_config.py +56 -0
  382. praisonai/ui/db.py +294 -0
  383. praisonai/ui/realtime.py +488 -0
  384. praisonai/ui/realtimeclient/__init__.py +756 -0
  385. praisonai/ui/realtimeclient/tools.py +242 -0
  386. praisonai/ui/sql_alchemy.py +710 -0
  387. praisonai/upload_vision.py +140 -0
  388. praisonai/version.py +1 -0
  389. praisonai-3.0.0.dist-info/METADATA +3493 -0
  390. praisonai-3.0.0.dist-info/RECORD +393 -0
  391. praisonai-3.0.0.dist-info/WHEEL +5 -0
  392. praisonai-3.0.0.dist-info/entry_points.txt +4 -0
  393. praisonai-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1384 @@
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
+ # PraisonAIAgents format (agents + tasks)
595
+ from praisonaiagents import Agent, Task, PraisonAIAgents
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 = PraisonAIAgents(
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
851
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
852
+ zip_ref.extractall(tmpdir)
853
+
854
+ # Find the template
855
+ extracted_dir = Path(tmpdir) / f"{repo}-main"
856
+ template_src = extracted_dir / template_path
857
+
858
+ if not template_src.exists():
859
+ # Try common paths
860
+ for alt_path in [
861
+ extracted_dir / "agent_recipes" / "templates" / template_path,
862
+ extracted_dir / "templates" / template_path,
863
+ ]:
864
+ if alt_path.exists():
865
+ template_src = alt_path
866
+ break
867
+
868
+ if not template_src.exists():
869
+ print(f"[red]Template not found: {template_path}[/red]")
870
+ return 1
871
+
872
+ # Copy to ~/.praison/templates/
873
+ templates_dir = Path.home() / ".praison" / "templates"
874
+ templates_dir.mkdir(parents=True, exist_ok=True)
875
+ dest = templates_dir / template_src.name
876
+
877
+ if dest.exists():
878
+ shutil.rmtree(dest)
879
+ shutil.copytree(template_src, dest)
880
+
881
+ print(f"\n✅ Added template from GitHub: {user}/{repo}/{template_path}")
882
+ print(f" Saved to: {dest}")
883
+ return 0
884
+
885
+ except Exception as e:
886
+ print(f"[red]Failed to download from GitHub: {e}[/red]")
887
+ return 1
888
+
889
+ else:
890
+ print(f"[red]Unknown source format: {source}[/red]")
891
+ print("Use: github:user/repo/template or ./local-path")
892
+ return 1
893
+
894
+ def cmd_add_sources(self, args: List[str]) -> int:
895
+ """
896
+ Add a template source to persistent config.
897
+
898
+ Args:
899
+ args: [source] - github:user/repo or URL
900
+ """
901
+ if not args:
902
+ print("[red]Usage: praisonai templates add-sources <source>[/red]")
903
+ return 1
904
+
905
+ source = args[0]
906
+ config = self._load_templates_config()
907
+
908
+ if "sources" not in config:
909
+ config["sources"] = []
910
+
911
+ if source in config["sources"]:
912
+ print(f"[yellow]Source '{source}' already in config[/yellow]")
913
+ return 0
914
+
915
+ config["sources"].append(source)
916
+ self._save_templates_config(config)
917
+
918
+ print(f"\n✅ Added template source: {source}")
919
+ print(f" Config saved to: {self._get_templates_config_path()}")
920
+ return 0
921
+
922
+ def cmd_remove_sources(self, args: List[str]) -> int:
923
+ """
924
+ Remove a template source from persistent config.
925
+
926
+ Args:
927
+ args: [source] - source to remove
928
+ """
929
+ if not args:
930
+ print("[red]Usage: praisonai templates remove-sources <source>[/red]")
931
+ return 1
932
+
933
+ source = args[0]
934
+ config = self._load_templates_config()
935
+
936
+ if "sources" not in config or source not in config["sources"]:
937
+ print(f"[yellow]Source '{source}' not found in config[/yellow]")
938
+ return 1
939
+
940
+ config["sources"].remove(source)
941
+ self._save_templates_config(config)
942
+
943
+ print(f"\n✅ Removed template source: {source}")
944
+ return 0
945
+
946
+ def cmd_browse(self, args: List[str]) -> int:
947
+ """
948
+ Open template catalog in browser.
949
+
950
+ Args:
951
+ args: [--local] [--url <url>] [--print]
952
+ """
953
+ import webbrowser
954
+
955
+ # Default catalog URL
956
+ catalog_url = "https://mervinpraison.github.io/praisonai-template-catalog"
957
+
958
+ # Parse arguments
959
+ print_only = "--print" in args
960
+ local_mode = "--local" in args
961
+
962
+ # Custom URL
963
+ if "--url" in args:
964
+ idx = args.index("--url")
965
+ if idx + 1 < len(args):
966
+ catalog_url = args[idx + 1]
967
+
968
+ if local_mode:
969
+ print("Local catalog server not implemented yet.")
970
+ print(f"Visit the online catalog at: {catalog_url}")
971
+ return 0
972
+
973
+ if print_only:
974
+ print(catalog_url)
975
+ return 0
976
+
977
+ print(f"Opening template catalog: {catalog_url}")
978
+ try:
979
+ webbrowser.open(catalog_url)
980
+ return 0
981
+ except Exception as e:
982
+ print(f"Failed to open browser: {e}")
983
+ print(f"Visit: {catalog_url}")
984
+ return 1
985
+
986
+ def cmd_catalog(self, args: List[str]) -> int:
987
+ """
988
+ Catalog management commands.
989
+
990
+ Subcommands:
991
+ build Build catalog locally
992
+ sync Sync catalog sources
993
+ """
994
+ if not args:
995
+ print("Usage: praisonai templates catalog <build|sync> [options]")
996
+ print("\nSubcommands:")
997
+ print(" build Build catalog locally")
998
+ print(" sync Sync catalog sources from GitHub")
999
+ print("\nExamples:")
1000
+ print(" praisonai templates catalog build --out ./dist")
1001
+ print(" praisonai templates catalog sync --source agent-recipes")
1002
+ return 0
1003
+
1004
+ subcmd = args[0]
1005
+ remaining = args[1:]
1006
+
1007
+ if subcmd == "build":
1008
+ return self._catalog_build(remaining)
1009
+ elif subcmd == "sync":
1010
+ return self._catalog_sync(remaining)
1011
+ else:
1012
+ print(f"Unknown catalog command: {subcmd}")
1013
+ return 1
1014
+
1015
+ def _catalog_build(self, args: List[str]) -> int:
1016
+ """Build catalog locally."""
1017
+ import subprocess
1018
+ import os
1019
+
1020
+ # Parse arguments
1021
+ out_dir = None
1022
+ source_dir = None
1023
+ minify = "--minify" in args
1024
+
1025
+ if "--out" in args:
1026
+ idx = args.index("--out")
1027
+ if idx + 1 < len(args):
1028
+ out_dir = args[idx + 1]
1029
+
1030
+ if "--source" in args:
1031
+ idx = args.index("--source")
1032
+ if idx + 1 < len(args):
1033
+ source_dir = args[idx + 1]
1034
+
1035
+ # Check if catalog repo is available locally
1036
+ catalog_repo = Path.home() / "praisonai-template-catalog"
1037
+ if not catalog_repo.exists():
1038
+ # Try to find it relative to this package
1039
+ possible_paths = [
1040
+ Path(__file__).parent.parent.parent.parent.parent.parent / "praisonai-template-catalog",
1041
+ Path.cwd() / "praisonai-template-catalog",
1042
+ ]
1043
+ for p in possible_paths:
1044
+ if p.exists():
1045
+ catalog_repo = p
1046
+ break
1047
+
1048
+ if catalog_repo.exists() and (catalog_repo / "scripts" / "build-catalog.js").exists():
1049
+ print(f"Building catalog using: {catalog_repo}")
1050
+ cmd = ["node", "scripts/build-catalog.js"]
1051
+ if out_dir:
1052
+ cmd.extend(["--out", out_dir])
1053
+ if source_dir:
1054
+ cmd.extend(["--source", source_dir])
1055
+ if minify:
1056
+ cmd.append("--minify")
1057
+
1058
+ try:
1059
+ result = subprocess.run(cmd, cwd=str(catalog_repo), capture_output=True, text=True)
1060
+ print(result.stdout)
1061
+ if result.stderr:
1062
+ print(result.stderr)
1063
+ return result.returncode
1064
+ except FileNotFoundError:
1065
+ print("Node.js not found. Please install Node.js to build the catalog.")
1066
+ return 1
1067
+ else:
1068
+ # Fallback: Generate minimal catalog using Python
1069
+ print("Catalog repo not found locally. Generating minimal catalog...")
1070
+ return self._generate_minimal_catalog(out_dir, source_dir)
1071
+
1072
+ def _generate_minimal_catalog(self, out_dir: str = None, source_dir: str = None) -> int:
1073
+ """Generate minimal catalog JSON using Python."""
1074
+ import json
1075
+ from datetime import datetime
1076
+
1077
+ try:
1078
+ import yaml
1079
+ except ImportError:
1080
+ print("PyYAML not installed. Install with: pip install pyyaml")
1081
+ return 1
1082
+
1083
+ # Find templates
1084
+ if source_dir:
1085
+ templates_dir = Path(source_dir)
1086
+ else:
1087
+ # Try Agent-Recipes
1088
+ possible_paths = [
1089
+ Path.home() / "Agent-Recipes" / "agent_recipes" / "templates",
1090
+ Path.cwd() / "Agent-Recipes" / "agent_recipes" / "templates",
1091
+ ]
1092
+ templates_dir = None
1093
+ for p in possible_paths:
1094
+ if p.exists():
1095
+ templates_dir = p
1096
+ break
1097
+
1098
+ if not templates_dir:
1099
+ # Try package templates
1100
+ try:
1101
+ import agent_recipes
1102
+ templates_dir = Path(agent_recipes.__file__).parent / "templates"
1103
+ except ImportError:
1104
+ pass
1105
+
1106
+ if not templates_dir or not templates_dir.exists():
1107
+ print("No templates directory found.")
1108
+ return 1
1109
+
1110
+ print(f"Scanning templates in: {templates_dir}")
1111
+
1112
+ templates = []
1113
+ for entry in templates_dir.iterdir():
1114
+ if not entry.is_dir():
1115
+ continue
1116
+ template_yaml = entry / "TEMPLATE.yaml"
1117
+ if template_yaml.exists():
1118
+ try:
1119
+ with open(template_yaml) as f:
1120
+ data = yaml.safe_load(f)
1121
+ if data:
1122
+ templates.append({
1123
+ "name": data.get("name", entry.name),
1124
+ "version": data.get("version", "1.0.0"),
1125
+ "description": data.get("description", ""),
1126
+ "author": data.get("author", "Unknown"),
1127
+ "license": data.get("license", "Apache-2.0"),
1128
+ "tags": data.get("tags", []),
1129
+ "requires": data.get("requires", {}),
1130
+ })
1131
+ except Exception as e:
1132
+ print(f" Warning: Failed to parse {template_yaml}: {e}")
1133
+
1134
+ # Output
1135
+ output = {
1136
+ "version": "1.0.0",
1137
+ "generated_at": datetime.now().isoformat(),
1138
+ "count": len(templates),
1139
+ "templates": templates
1140
+ }
1141
+
1142
+ out_path = Path(out_dir) if out_dir else Path.cwd() / "templates.json"
1143
+ if out_path.is_dir():
1144
+ out_path = out_path / "templates.json"
1145
+
1146
+ out_path.parent.mkdir(parents=True, exist_ok=True)
1147
+ with open(out_path, "w") as f:
1148
+ json.dump(output, f, indent=2)
1149
+
1150
+ print(f"Generated: {out_path} ({len(templates)} templates)")
1151
+ return 0
1152
+
1153
+ def _catalog_sync(self, args: List[str]) -> int:
1154
+ """Sync catalog sources."""
1155
+ import subprocess
1156
+
1157
+ # Parse arguments
1158
+ config_path = None
1159
+ source_name = None
1160
+ cache_dir = None
1161
+
1162
+ if "--config" in args:
1163
+ idx = args.index("--config")
1164
+ if idx + 1 < len(args):
1165
+ config_path = args[idx + 1]
1166
+
1167
+ if "--source" in args:
1168
+ idx = args.index("--source")
1169
+ if idx + 1 < len(args):
1170
+ source_name = args[idx + 1]
1171
+
1172
+ if "--cache-dir" in args:
1173
+ idx = args.index("--cache-dir")
1174
+ if idx + 1 < len(args):
1175
+ cache_dir = args[idx + 1]
1176
+
1177
+ # Check if catalog repo is available
1178
+ catalog_repo = Path.home() / "praisonai-template-catalog"
1179
+ if catalog_repo.exists() and (catalog_repo / "scripts" / "sync-sources.js").exists():
1180
+ print(f"Syncing using: {catalog_repo}")
1181
+ cmd = ["node", "scripts/sync-sources.js"]
1182
+ if config_path:
1183
+ cmd.extend(["--config", config_path])
1184
+ if source_name:
1185
+ cmd.extend(["--source", source_name])
1186
+ if cache_dir:
1187
+ cmd.extend(["--cache-dir", cache_dir])
1188
+
1189
+ try:
1190
+ result = subprocess.run(cmd, cwd=str(catalog_repo), capture_output=True, text=True)
1191
+ print(result.stdout)
1192
+ if result.stderr:
1193
+ print(result.stderr)
1194
+ return result.returncode
1195
+ except FileNotFoundError:
1196
+ print("Node.js not found. Please install Node.js.")
1197
+ return 1
1198
+ else:
1199
+ # Fallback: Clone Agent-Recipes
1200
+ print("Catalog repo not found. Cloning Agent-Recipes directly...")
1201
+ cache_path = Path(cache_dir) if cache_dir else Path.home() / ".praison" / "cache"
1202
+ cache_path.mkdir(parents=True, exist_ok=True)
1203
+
1204
+ target = cache_path / "Agent-Recipes"
1205
+ if target.exists():
1206
+ print(f"Updating: {target}")
1207
+ try:
1208
+ subprocess.run(["git", "-C", str(target), "pull", "--depth=1"], check=True)
1209
+ print("✓ Updated successfully")
1210
+ return 0
1211
+ except subprocess.CalledProcessError as e:
1212
+ print(f"Failed to update: {e}")
1213
+ return 1
1214
+ else:
1215
+ print(f"Cloning to: {target}")
1216
+ try:
1217
+ subprocess.run([
1218
+ "git", "clone", "--depth=1",
1219
+ "https://github.com/MervinPraison/Agent-Recipes.git",
1220
+ str(target)
1221
+ ], check=True)
1222
+ print("✓ Cloned successfully")
1223
+ return 0
1224
+ except subprocess.CalledProcessError as e:
1225
+ print(f"Failed to clone: {e}")
1226
+ return 1
1227
+
1228
+ def cmd_validate(self, args: List[str]) -> int:
1229
+ """
1230
+ Validate template YAML files.
1231
+
1232
+ Args:
1233
+ args: [--source <dir>] [--strict] [--json]
1234
+ """
1235
+ import subprocess
1236
+
1237
+ # Parse arguments
1238
+ source_dir = None
1239
+ strict = "--strict" in args
1240
+ json_output = "--json" in args
1241
+
1242
+ if "--source" in args:
1243
+ idx = args.index("--source")
1244
+ if idx + 1 < len(args):
1245
+ source_dir = args[idx + 1]
1246
+
1247
+ # Check if catalog repo is available
1248
+ catalog_repo = Path.home() / "praisonai-template-catalog"
1249
+ if catalog_repo.exists() and (catalog_repo / "scripts" / "validate-templates.js").exists():
1250
+ cmd = ["node", "scripts/validate-templates.js"]
1251
+ if source_dir:
1252
+ cmd.extend(["--source", source_dir])
1253
+ if strict:
1254
+ cmd.append("--strict")
1255
+ if json_output:
1256
+ cmd.append("--json")
1257
+
1258
+ try:
1259
+ result = subprocess.run(cmd, cwd=str(catalog_repo), capture_output=True, text=True)
1260
+ print(result.stdout)
1261
+ if result.stderr:
1262
+ print(result.stderr)
1263
+ return result.returncode
1264
+ except FileNotFoundError:
1265
+ print("Node.js not found.")
1266
+ return 1
1267
+ else:
1268
+ # Fallback: Basic Python validation
1269
+ return self._validate_templates_python(source_dir, strict, json_output)
1270
+
1271
+ def _validate_templates_python(self, source_dir: str = None, strict: bool = False, json_output: bool = False) -> int:
1272
+ """Validate templates using Python."""
1273
+ import json as json_module
1274
+
1275
+ try:
1276
+ import yaml
1277
+ except ImportError:
1278
+ print("PyYAML not installed. Install with: pip install pyyaml")
1279
+ return 1
1280
+
1281
+ # Find templates directory
1282
+ if source_dir:
1283
+ templates_dir = Path(source_dir)
1284
+ else:
1285
+ possible_paths = [
1286
+ Path.home() / "Agent-Recipes" / "agent_recipes" / "templates",
1287
+ Path.cwd() / "Agent-Recipes" / "agent_recipes" / "templates",
1288
+ ]
1289
+ templates_dir = None
1290
+ for p in possible_paths:
1291
+ if p.exists():
1292
+ templates_dir = p
1293
+ break
1294
+
1295
+ if not templates_dir or not templates_dir.exists():
1296
+ print(f"Templates directory not found: {source_dir or 'default locations'}")
1297
+ return 1
1298
+
1299
+ print(f"Validating templates in: {templates_dir}\n")
1300
+
1301
+ results = []
1302
+ errors_count = 0
1303
+ warnings_count = 0
1304
+
1305
+ for entry in sorted(templates_dir.iterdir()):
1306
+ if not entry.is_dir() or entry.name.startswith("."):
1307
+ continue
1308
+
1309
+ template_yaml = entry / "TEMPLATE.yaml"
1310
+ if not template_yaml.exists():
1311
+ continue
1312
+
1313
+ result = {"name": entry.name, "valid": True, "errors": [], "warnings": []}
1314
+
1315
+ try:
1316
+ with open(template_yaml) as f:
1317
+ data = yaml.safe_load(f)
1318
+
1319
+ # Check required fields
1320
+ required = ["name", "version", "description"]
1321
+ for field in required:
1322
+ if not data.get(field):
1323
+ result["errors"].append(f"Missing required field: {field}")
1324
+ result["valid"] = False
1325
+
1326
+ # Check version format
1327
+ version = data.get("version", "")
1328
+ if version and not self._is_valid_version(version):
1329
+ result["warnings"].append(f"Invalid version format: {version}")
1330
+
1331
+ # Check workflow file
1332
+ workflow = data.get("workflow", "workflow.yaml")
1333
+ if isinstance(workflow, str) and not (entry / workflow).exists():
1334
+ if strict:
1335
+ result["errors"].append(f"Workflow file not found: {workflow}")
1336
+ result["valid"] = False
1337
+ else:
1338
+ result["warnings"].append(f"Workflow file not found: {workflow}")
1339
+
1340
+ except yaml.YAMLError as e:
1341
+ result["errors"].append(f"Invalid YAML: {e}")
1342
+ result["valid"] = False
1343
+ except Exception as e:
1344
+ result["errors"].append(f"Error: {e}")
1345
+ result["valid"] = False
1346
+
1347
+ results.append(result)
1348
+ errors_count += len(result["errors"])
1349
+ warnings_count += len(result["warnings"])
1350
+
1351
+ # Output
1352
+ if json_output:
1353
+ print(json_module.dumps(results, indent=2))
1354
+ else:
1355
+ for r in results:
1356
+ status = "✓" if r["valid"] else "✗"
1357
+ color = "\033[32m" if r["valid"] else "\033[31m"
1358
+ print(f"{color}{status}\033[0m {r['name']}")
1359
+ for err in r["errors"]:
1360
+ print(f" \033[31m✗ ERROR:\033[0m {err}")
1361
+ for warn in r["warnings"]:
1362
+ print(f" \033[33m⚠ WARNING:\033[0m {warn}")
1363
+
1364
+ print(f"\n{len(results)} templates checked, {errors_count} errors, {warnings_count} warnings")
1365
+
1366
+ return 1 if errors_count > 0 else 0
1367
+
1368
+ def _is_valid_version(self, version: str) -> bool:
1369
+ """Check if version string is valid semver."""
1370
+ import re
1371
+ return bool(re.match(r'^\d+\.\d+\.\d+', version))
1372
+
1373
+
1374
+ def import_time():
1375
+ """Get current time (helper for cache expiry display)."""
1376
+ import time
1377
+ return time.time()
1378
+
1379
+
1380
+ # Convenience function for CLI integration
1381
+ def handle_templates_command(args: List[str]) -> int:
1382
+ """Handle templates command from main CLI."""
1383
+ handler = TemplatesHandler()
1384
+ return handler.handle(args)