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,1573 @@
1
+ """
2
+ Run command group for PraisonAI CLI.
3
+
4
+ Provides agent execution commands.
5
+ """
6
+
7
+ from typing import Any, Dict, Optional, List
8
+
9
+ import typer
10
+
11
+ from ..output.console import get_output_controller
12
+ from ..state.identifiers import get_current_context
13
+ from ..configuration.resolver import resolve_config
14
+
15
+ app = typer.Typer(help="Run agents")
16
+
17
+
18
+ def _framework_help() -> str:
19
+ try:
20
+ from praisonai.framework_adapters.registry import framework_option_help
21
+ return framework_option_help()
22
+ except ImportError:
23
+ return "Framework: praisonai, crewai, autogen"
24
+
25
+
26
+ _FRAMEWORK_HELP = _framework_help()
27
+
28
+
29
+ def _parse_permissions(allow: Optional[List[str]], deny: Optional[List[str]], permissions_file: Optional[str], default: Optional[str]) -> Optional[dict]:
30
+ """Parse permission flags into a config dict.
31
+
32
+ Args:
33
+ allow: Pattern to allow
34
+ deny: Pattern to deny
35
+ permissions_file: Path to permissions file (YAML or JSON)
36
+ default: Default action (allow/deny/ask)
37
+
38
+ Returns:
39
+ Dict mapping patterns to actions, or None if no permissions specified
40
+ """
41
+ if not any([allow, deny, permissions_file, default]):
42
+ return None
43
+
44
+ import json
45
+ import yaml
46
+
47
+ config = {}
48
+
49
+ # Load from file if provided
50
+ if permissions_file:
51
+ try:
52
+ with open(permissions_file, 'r') as f:
53
+ if permissions_file.endswith('.json'):
54
+ file_config = json.load(f)
55
+ else:
56
+ file_config = yaml.safe_load(f)
57
+ if isinstance(file_config, dict):
58
+ config.update(file_config)
59
+ except (IOError, json.JSONDecodeError, yaml.YAMLError) as e:
60
+ from ..output.console import get_output_controller
61
+ get_output_controller().print_warning(f"Failed to load permissions file: {e}")
62
+
63
+ # Add CLI patterns (override file config)
64
+ if allow:
65
+ for pattern in allow:
66
+ config[pattern] = "allow"
67
+ if deny:
68
+ for pattern in deny:
69
+ config[pattern] = "deny"
70
+
71
+ # Add default pattern if specified
72
+ if default and default in ("allow", "deny", "ask"):
73
+ config["*"] = default
74
+
75
+ return config if config else None
76
+
77
+
78
+ def _mcp_server_to_command(server: dict) -> Optional[tuple]:
79
+ """Convert a resolved MCP server config entry to a (command, env) pair.
80
+
81
+ Supports the project ``config.yaml`` schema where a server is declared as
82
+ either ``command: ["npx", "-y", "@playwright/mcp"]`` (list) or
83
+ ``command: "npx -y @playwright/mcp"`` (string), with optional ``args`` and
84
+ ``env`` keys. Remote servers and disabled servers return None.
85
+
86
+ Returns:
87
+ Tuple of (command_string, env_string) suitable for ``args.mcp`` /
88
+ ``args.mcp_env``, or None if the server cannot be expressed that way.
89
+ """
90
+ if not isinstance(server, dict):
91
+ return None
92
+ if server.get("enabled") is False:
93
+ return None
94
+ # Only local (stdio) servers map to the command-string CLI path.
95
+ if server.get("type") == "remote" or server.get("url"):
96
+ return None
97
+
98
+ command = server.get("command")
99
+ args = server.get("args") or []
100
+
101
+ import shlex
102
+
103
+ if isinstance(command, list):
104
+ parts = [str(p) for p in command]
105
+ elif isinstance(command, str) and command:
106
+ parts = shlex.split(command)
107
+ else:
108
+ return None
109
+
110
+ if args:
111
+ parts = parts + [str(a) for a in args]
112
+ if not parts:
113
+ return None
114
+
115
+ # Quote each token so values with spaces/metacharacters survive the
116
+ # downstream ``shlex.split`` in the MCP handler.
117
+ command_str = " ".join(shlex.quote(part) for part in parts)
118
+
119
+ env = server.get("env") or {}
120
+ env_str = None
121
+ if isinstance(env, dict) and env:
122
+ # The downstream parser splits env on commas, so a comma inside a value
123
+ # would corrupt it. Skip such entries with a warning rather than
124
+ # silently producing wrong env vars.
125
+ safe_pairs = []
126
+ for k, v in env.items():
127
+ v_str = str(v)
128
+ if "," in v_str:
129
+ import warnings
130
+ warnings.warn(
131
+ f"MCP env var '{k}' contains a comma and cannot be passed "
132
+ "via the command-string CLI path; skipping it.",
133
+ stacklevel=2,
134
+ )
135
+ continue
136
+ safe_pairs.append(f"{k}={v_str}")
137
+ if safe_pairs:
138
+ env_str = ",".join(safe_pairs)
139
+
140
+ return command_str, env_str
141
+
142
+
143
+ def _collect_mcp_servers_from_config(config) -> List[dict]:
144
+ """Collect all enabled MCP servers from resolved config.
145
+
146
+ Returns a list of normalized server dicts (both local stdio and remote
147
+ URL servers). The structured list is handed to the core MCP client, which
148
+ already supports stdio, SSE, HTTP-stream and websocket transports — so a
149
+ project that declares multiple and/or remote servers gets every enabled
150
+ one wired, not just the first stdio one.
151
+
152
+ Returns:
153
+ List of server config dicts (possibly empty).
154
+ """
155
+ mcp = getattr(config, "mcp", None) or {}
156
+ if not isinstance(mcp, dict):
157
+ return []
158
+ servers = mcp.get("servers") or {}
159
+ if not isinstance(servers, dict):
160
+ return []
161
+
162
+ collected: List[dict] = []
163
+ for name, server in servers.items():
164
+ if not isinstance(server, dict):
165
+ continue
166
+ if server.get("enabled") is False:
167
+ continue
168
+ entry = dict(server)
169
+ entry.setdefault("name", name)
170
+ collected.append(entry)
171
+ return collected
172
+
173
+
174
+ def _build_mcp_tools(
175
+ mcp: Optional[str],
176
+ mcp_env: Optional[str],
177
+ mcp_servers: Optional[List[dict]],
178
+ verbose: bool = False,
179
+ ) -> list:
180
+ """Build aggregated MCP tools from a command string and/or structured list.
181
+
182
+ Both the single ``--mcp`` command string (ad-hoc) and every enabled server
183
+ from project config (local stdio *and* remote/URL) are wired, so the run
184
+ path exposes all configured MCP servers to the agent — not just the first.
185
+
186
+ Returns:
187
+ A flat list of MCP tool callables (possibly empty).
188
+ """
189
+ tools: list = []
190
+ try:
191
+ from praisonai.cli.features.mcp import MCPHandler
192
+ except ImportError:
193
+ return tools
194
+
195
+ handler = MCPHandler(verbose=verbose)
196
+
197
+ if mcp:
198
+ mcp_instance = handler.create_mcp_tools(mcp, mcp_env)
199
+ if mcp_instance:
200
+ tools.extend(list(mcp_instance))
201
+
202
+ for server in mcp_servers or []:
203
+ mcp_instance = handler.create_mcp_from_server(server)
204
+ if mcp_instance:
205
+ tools.extend(list(mcp_instance))
206
+
207
+ return tools
208
+
209
+
210
+ def _permissions_from_config(config) -> Optional[dict]:
211
+ """Convert a resolved ``permissions`` config section to a pattern->action dict.
212
+
213
+ Supports both the rule-list form::
214
+
215
+ permissions:
216
+ default: ask
217
+ rules:
218
+ - { pattern: "bash:git *", action: allow }
219
+
220
+ and a flat ``pattern: action`` mapping. The flat mapping mirrors the format
221
+ produced by ``_parse_permissions`` (used by ``--allow``/``--deny`` and
222
+ ``--permissions``), so both paths converge on the same structure.
223
+ """
224
+ permissions = getattr(config, "permissions", None) or {}
225
+ if not isinstance(permissions, dict) or not permissions:
226
+ return None
227
+
228
+ result: dict = {}
229
+
230
+ rules = permissions.get("rules")
231
+ if isinstance(rules, list):
232
+ for rule in rules:
233
+ if isinstance(rule, dict):
234
+ pattern = rule.get("pattern")
235
+ action = rule.get("action")
236
+ if pattern and action in ("allow", "deny", "ask"):
237
+ result[pattern] = action
238
+
239
+ default = permissions.get("default")
240
+ if default in ("allow", "deny", "ask"):
241
+ result["*"] = default
242
+
243
+ # Allow a flat pattern->action mapping alongside the structured form.
244
+ for key, value in permissions.items():
245
+ if key in ("rules", "default"):
246
+ continue
247
+ if isinstance(value, str) and value in ("allow", "deny", "ask"):
248
+ result[key] = value
249
+
250
+ return result or None
251
+
252
+
253
+ def _apply_config_defaults(
254
+ mcp: Optional[str],
255
+ mcp_env: Optional[str],
256
+ permissions_config: Optional[dict],
257
+ ) -> tuple:
258
+ """Layer resolved project config under explicit CLI flags.
259
+
260
+ MCP servers and permission policy declared in ``.praisonai/config.yaml`` are
261
+ surfaced here so any ``praisonai run`` in the directory inherits them. CLI
262
+ flags always take precedence (they are passed in already-resolved and only
263
+ config-derived values fill the gaps).
264
+
265
+ All enabled MCP servers from config — multiple local stdio servers *and*
266
+ remote/URL servers — are collected into a structured list so the run path
267
+ can wire every one of them, not just the first stdio server.
268
+
269
+ Returns:
270
+ Tuple of (mcp, mcp_env, permissions_config, mcp_servers) with config
271
+ defaults applied. ``mcp_servers`` is a (possibly empty) list of
272
+ structured server config dicts.
273
+ """
274
+ mcp_servers: List[dict] = []
275
+ try:
276
+ config = resolve_config()
277
+ except (ValueError, OSError):
278
+ return mcp, mcp_env, permissions_config, mcp_servers
279
+
280
+ # MCP: collect all enabled servers (local + remote) from config. The
281
+ # explicit single-string ``--mcp`` flag, when given, is wired separately
282
+ # via ``args.mcp`` and merged alongside these by the run handler.
283
+ mcp_servers = _collect_mcp_servers_from_config(config)
284
+
285
+ # Permissions: merge config rules underneath CLI-provided rules.
286
+ config_perms = _permissions_from_config(config)
287
+ if config_perms:
288
+ if permissions_config:
289
+ merged = dict(config_perms)
290
+ merged.update(permissions_config) # CLI flags override config
291
+ permissions_config = merged
292
+ else:
293
+ permissions_config = config_perms
294
+
295
+ return mcp, mcp_env, permissions_config, mcp_servers
296
+
297
+
298
+ def _checkpoints_auto_enabled() -> bool:
299
+ """Whether automatic run-checkpointing is enabled via project config.
300
+
301
+ Reads the ``checkpoints.auto`` key from the resolved project config
302
+ (stored under ``extra``). Defaults to ``True`` so interactive terminal
303
+ runs get an automatic safety net; opt out with ``--no-checkpoint`` or
304
+ ``checkpoints: {auto: false}``.
305
+ """
306
+ try:
307
+ config = resolve_config()
308
+ except (ValueError, OSError):
309
+ return True
310
+
311
+ checkpoints = (getattr(config, "extra", None) or {}).get("checkpoints")
312
+ if isinstance(checkpoints, dict) and "auto" in checkpoints:
313
+ return bool(checkpoints["auto"])
314
+ return True
315
+
316
+
317
+ def _checkpoints_storage_dir() -> Optional[str]:
318
+ """Return a configured ``checkpoints.storage_dir`` (or ``None``).
319
+
320
+ Keeps ``praisonai run`` auto-checkpoints and restores reading from the same
321
+ store the rest of the CLI (``code --checkpoints``, ``praisonai checkpoint``)
322
+ uses when ``checkpoints.storage_dir`` is configured.
323
+ """
324
+ try:
325
+ config = resolve_config()
326
+ except (ValueError, OSError):
327
+ return None
328
+ checkpoints = (getattr(config, "extra", None) or {}).get("checkpoints")
329
+ if isinstance(checkpoints, dict):
330
+ return checkpoints.get("storage_dir")
331
+ return None
332
+
333
+
334
+ def _auto_checkpoint(label: str, *, no_checkpoint: bool, workspace_dir: Optional[str] = None) -> None:
335
+ """Create an automatic checkpoint of the workspace before a run.
336
+
337
+ ``workspace_dir`` defaults to the current directory but should be the
338
+ directory of the project being run (e.g. the directory containing a target
339
+ YAML file) so the checkpoint protects the files the run will actually
340
+ touch.
341
+
342
+ Best-effort and quiet: any failure (e.g. protected path, no git, no
343
+ changes) is swallowed so a checkpoint problem never blocks the run and the
344
+ auto-checkpoint never leaks into machine-readable run output.
345
+ """
346
+ if no_checkpoint or not _checkpoints_auto_enabled():
347
+ return
348
+
349
+ import asyncio
350
+ import os
351
+
352
+ output = get_output_controller()
353
+ try:
354
+ from praisonai.cli.features.checkpoints import CheckpointsHandler
355
+
356
+ handler = CheckpointsHandler(
357
+ workspace_dir=workspace_dir or os.getcwd(),
358
+ storage_dir=_checkpoints_storage_dir(),
359
+ )
360
+ asyncio.run(handler.save(label, allow_empty=False, quiet=True))
361
+ except Exception as e: # pragma: no cover - defensive, never block the run
362
+ if getattr(output, "is_verbose", False):
363
+ output.print_info(f"Auto-checkpoint skipped: {e}")
364
+
365
+
366
+ def _restore_checkpoint(ref: str, workspace_dir: Optional[str] = None) -> None:
367
+ """Restore the workspace to a checkpoint reference ('last' or an id)."""
368
+ import asyncio
369
+ import os
370
+
371
+ from praisonai.cli.features.checkpoints import CheckpointsHandler
372
+
373
+ handler = CheckpointsHandler(
374
+ workspace_dir=workspace_dir or os.getcwd(),
375
+ storage_dir=_checkpoints_storage_dir(),
376
+ )
377
+
378
+ async def _run() -> bool:
379
+ service = await handler._get_service()
380
+ checkpoints = await service.list_checkpoints(limit=100)
381
+ if not checkpoints:
382
+ handler._print_error("No checkpoints found to restore")
383
+ return False
384
+ if ref in ("last", "latest"):
385
+ target = checkpoints[0].id
386
+ else:
387
+ # Exact id/short_id first, then a unique prefix; reject ambiguous
388
+ # prefixes so we never restore the wrong workspace.
389
+ exact = [cp.id for cp in checkpoints if cp.id == ref or cp.short_id == ref]
390
+ if exact:
391
+ target = exact[0]
392
+ else:
393
+ prefix_matches = [cp.id for cp in checkpoints if cp.id.startswith(ref)]
394
+ if len(prefix_matches) == 1:
395
+ target = prefix_matches[0]
396
+ elif len(prefix_matches) > 1:
397
+ handler._print_error(f"Ambiguous checkpoint reference: {ref}")
398
+ return False
399
+ else:
400
+ handler._print_error(f"No checkpoint found for: {ref}")
401
+ return False
402
+ return await handler.restore(target)
403
+
404
+ if not asyncio.run(_run()):
405
+ raise typer.Exit(1)
406
+
407
+
408
+ def _try_attach_runtime(
409
+ prompt: str,
410
+ *,
411
+ model: Optional[str],
412
+ output_mode: Optional[str],
413
+ session_id: Optional[str],
414
+ ) -> bool:
415
+ """Forward a plain prompt to a warm runtime when one is running.
416
+
417
+ Returns True when the request was handled by the warm runtime (so the caller
418
+ should skip in-process execution), or False to fall back to the normal
419
+ in-process path. Any runtime error is treated as a transparent fall-back.
420
+
421
+ Only the simple text path is attached: structured ``actions``/``json`` output
422
+ modes and session continuity stay in-process to preserve their behaviour.
423
+ """
424
+ # Structured output modes have richer in-process event bridging; don't attach.
425
+ if output_mode in ("actions", "json", "stream", "stream-json"):
426
+ return False
427
+
428
+ try:
429
+ from praisonai.runtime import get_runtime_descriptor, RuntimeClient, RuntimeUnavailable
430
+ except ImportError:
431
+ return False
432
+
433
+ # Require a version-compatible runtime: a stale (major-mismatched) server is
434
+ # not attached to, so the cold in-process path runs instead of silently
435
+ # talking to an incompatible runtime.
436
+ descriptor = get_runtime_descriptor(require_compatible=True)
437
+ if descriptor is None:
438
+ return False
439
+
440
+ output = get_output_controller()
441
+ try:
442
+ client = RuntimeClient(descriptor)
443
+ result = client.run(prompt, model=model, session_id=session_id)
444
+ except RuntimeUnavailable:
445
+ # Runtime went away mid-flight; fall back to in-process execution.
446
+ return False
447
+
448
+ output.emit_result(
449
+ message="Prompt completed",
450
+ data={"result": str(result) if result else None},
451
+ )
452
+ if result and not output.is_json_mode:
453
+ print(result)
454
+ return True
455
+
456
+
457
+ @app.callback(invoke_without_command=True)
458
+ def run_main(
459
+ ctx: typer.Context,
460
+ target: Optional[str] = typer.Argument(None, help="Agent file or prompt"),
461
+ model: Optional[str] = typer.Option(None, "--model", "-m", help="LLM model to use"),
462
+ framework: Optional[str] = typer.Option(None, "--framework", "-f", help=_FRAMEWORK_HELP),
463
+ interactive: bool = typer.Option(False, "--interactive", "-i", help="Interactive mode"),
464
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
465
+ stream: bool = typer.Option(False, "--stream/--no-stream", help="Stream output (default: off for production use)"),
466
+ trace: bool = typer.Option(False, "--trace", help="Enable tracing"),
467
+ memory: bool = typer.Option(False, "--memory", help="Enable memory"),
468
+ tools: Optional[str] = typer.Option(None, "--tools", "-t", help="Tools file path"),
469
+ toolset: Optional[str] = typer.Option(None, "--toolset", help="Named toolset groups (comma-separated, e.g., web,files)"),
470
+ max_tokens: int = typer.Option(16000, "--max-tokens", help="Maximum output tokens"),
471
+ profile: bool = typer.Option(False, "--profile", help="Enable CLI profiling (timing breakdown)"),
472
+ profile_deep: bool = typer.Option(False, "--profile-deep", help="Enable deep profiling (cProfile stats, higher overhead)"),
473
+ output_mode: Optional[str] = typer.Option(None, "--output", "-o", help="Output mode: silent (default), actions, verbose, json, stream"),
474
+ approval: Optional[str] = typer.Option(None, "--approval", help="Approval backend: console, slack, telegram, discord, webhook, http, agent, auto, none"),
475
+ approve_all_tools: bool = typer.Option(False, "--approve-all-tools", help="Require approval for ALL tool calls, not just dangerous tools"),
476
+ approval_timeout: Optional[str] = typer.Option(None, "--approval-timeout", help="Seconds to wait for approval. Use 'none' for indefinite wait"),
477
+ no_rules: bool = typer.Option(False, "--no-rules", help="Disable auto-injection of project instruction files"),
478
+ # Permission flags for CI-safe declarative policies
479
+ allow: Optional[List[str]] = typer.Option(None, "--allow", help="Permission pattern to allow (e.g., 'read:*', 'bash:git *'). Can be repeated."),
480
+ deny: Optional[List[str]] = typer.Option(None, "--deny", help="Permission pattern to deny (e.g., 'bash:rm *'). Can be repeated."),
481
+ permissions: Optional[str] = typer.Option(None, "--permissions", help="Permission file path (YAML or JSON) with allow/deny rules"),
482
+ permission_default: Optional[str] = typer.Option(None, "--permission-default", help="Default action for unmatched patterns: allow, deny, ask (default: ask)"),
483
+ # Session continuity options
484
+ continue_session: bool = typer.Option(False, "--continue", "-c", help="Continue the most recent session for this project"),
485
+ session: Optional[str] = typer.Option(None, "--session", "-s", help="Resume a specific session ID"),
486
+ fork: bool = typer.Option(False, "--fork", help="Fork from the specified session (requires --session)"),
487
+ no_save: bool = typer.Option(False, "--no-save", help="Don't auto-save session after execution"),
488
+ # Custom definitions
489
+ agent: Optional[str] = typer.Option(None, "--agent", "-a", help="Use a named custom agent"),
490
+ command: Optional[str] = typer.Option(None, "--command", help="Execute a named custom command"),
491
+ # Reasoning effort
492
+ thinking: Optional[str] = typer.Option(None, "--thinking", help="Reasoning effort (off, minimal, low, medium, high)"),
493
+ # Checkpoint / rewind
494
+ no_checkpoint: bool = typer.Option(False, "--no-checkpoint", help="Disable automatic file checkpoint before the run"),
495
+ restore: Optional[str] = typer.Option(None, "--restore", help="Restore the workspace to a checkpoint id (or 'last') and exit"),
496
+ # Warm-runtime live session: tag this run so other terminals can `attach`.
497
+ attach: Optional[str] = typer.Option(None, "--attach", help="Run on the warm runtime under this session id so other terminals can observe it via `praisonai attach <id>`"),
498
+ ):
499
+ """
500
+ Run agents from a file or prompt.
501
+
502
+ Examples:
503
+ praisonai run agents.yaml
504
+ praisonai run "What is the weather?"
505
+ praisonai run "What is the weather?" --continue
506
+ praisonai run "Add tests" --session abc123
507
+ praisonai run agents.yaml --interactive
508
+ praisonai run "What is 2+2?" --profile
509
+ """
510
+ output = get_output_controller()
511
+ _ = get_current_context() # Initialize context
512
+
513
+ # Rewind: restore the workspace to a prior checkpoint and exit. Handled
514
+ # before any execution so `praisonai run --restore last` is a pure undo.
515
+ if restore:
516
+ _restore_checkpoint(restore)
517
+ return
518
+
519
+ # Validate --thinking and resolve it to the core thinking_budget up front so
520
+ # an unknown value fails closed before any execution (consistent with the
521
+ # `code` command and MODE_RULES validation on custom agents).
522
+ from praisonai.cli.features.thinking import thinking_to_budget
523
+ try:
524
+ thinking_budget = thinking_to_budget(thinking)
525
+ except ValueError as exc:
526
+ output.print_error(str(exc))
527
+ raise typer.Exit(1)
528
+
529
+ # Early credential check before any processing
530
+ if target: # Only check if we actually have something to run
531
+ from praisonai.llm.credentials import is_configured
532
+ import sys
533
+
534
+ # Check if credentials are configured (use model if provided, else check general)
535
+ if not is_configured(model):
536
+ # In non-interactive mode, show clear error
537
+ if not sys.stdin.isatty() or output.is_json_mode:
538
+ output.print_error(
539
+ "No API key configured. Run: praisonai setup\n"
540
+ "or set environment variables like OPENAI_API_KEY"
541
+ )
542
+ raise typer.Exit(1)
543
+
544
+ # In interactive mode, offer to run setup
545
+ typer.echo(f"No API key configured{f' for model {model}' if model else ''}.")
546
+ run_setup = typer.confirm("Would you like to run the setup wizard now?")
547
+
548
+ if run_setup:
549
+ from praisonai.cli.commands.setup import _run_setup
550
+ exit_code = _run_setup(
551
+ non_interactive=False,
552
+ provider=None,
553
+ api_key=None,
554
+ model=None
555
+ )
556
+ if exit_code != 0:
557
+ output.print_error("Setup failed. Exiting.")
558
+ raise typer.Exit(exit_code)
559
+
560
+ output.print_success("Setup complete! Continuing with your run...")
561
+ # Re-check after setup
562
+ if not is_configured(model):
563
+ output.print_error("Setup completed but credentials still not detected.")
564
+ raise typer.Exit(1)
565
+ else:
566
+ output.print_info(
567
+ "To configure credentials:\n"
568
+ " - Run: praisonai setup\n"
569
+ " - Or set environment variables like OPENAI_API_KEY"
570
+ )
571
+ raise typer.Exit(0)
572
+
573
+ # Resolve configuration if model not explicitly provided
574
+ if model is None:
575
+ try:
576
+ config = resolve_config()
577
+ if config.agent.model:
578
+ model = config.agent.model
579
+ if verbose:
580
+ output.print_info(f"Using model from config: {model}")
581
+ except (ValueError, OSError) as e:
582
+ # Continue if config resolution fails, but log in verbose mode
583
+ if verbose:
584
+ output.print_info(f"Skipping config-based model fallback: {e}")
585
+
586
+ # Validate session options
587
+ if fork and not session:
588
+ output.print_error("--fork requires --session to specify which session to fork from")
589
+ raise typer.Exit(1)
590
+
591
+ if continue_session and session:
592
+ output.print_error("Cannot use both --continue and --session together")
593
+ raise typer.Exit(1)
594
+
595
+ # --attach tags a warm-runtime run so other terminals can observe it, but
596
+ # only the direct-prompt path forwards to the warm runtime. Reject it up
597
+ # front on profile/custom-agent/custom-command flows so users never
598
+ # pass --attach and silently get a session that produces no events.
599
+ if attach and (agent or command or profile or profile_deep):
600
+ output.print_error("--attach is only supported for direct prompt runs")
601
+ raise typer.Exit(1)
602
+
603
+ # Handle custom agent or command
604
+ if agent:
605
+ from praisonai.cli.features.custom_definitions import load_agent_from_name
606
+ agent_config = load_agent_from_name(agent)
607
+ if not agent_config:
608
+ output.print_error(f"Agent '{agent}' not found")
609
+ raise typer.Exit(1)
610
+
611
+ # Invocation-level permission flags override per-agent definition.
612
+ invocation_permissions = _parse_permissions(allow, deny, permissions, permission_default)
613
+
614
+ # Run with custom agent
615
+ _run_custom_agent(
616
+ agent_config,
617
+ target or "", # Use target as the prompt if provided
618
+ model=model,
619
+ verbose=verbose,
620
+ stream=stream,
621
+ trace=trace,
622
+ memory=memory,
623
+ tools=tools,
624
+ toolset=toolset,
625
+ max_tokens=max_tokens,
626
+ output_mode=output_mode,
627
+ approval=approval,
628
+ approve_all_tools=approve_all_tools,
629
+ approval_timeout=approval_timeout,
630
+ invocation_permissions=invocation_permissions,
631
+ continue_session=continue_session,
632
+ session=session,
633
+ fork=fork,
634
+ no_save=no_save,
635
+ thinking_budget=thinking_budget,
636
+ )
637
+ return
638
+
639
+ if command:
640
+ from praisonai.cli.features.custom_definitions import (
641
+ ShellSubstitutionError,
642
+ interpolate_command_template,
643
+ )
644
+ try:
645
+ prompt = interpolate_command_template(command, target or "")
646
+ except ShellSubstitutionError as exc:
647
+ output.print_error(str(exc))
648
+ raise typer.Exit(1)
649
+ if not prompt:
650
+ output.print_error(f"Command '{command}' not found")
651
+ raise typer.Exit(1)
652
+
653
+ # Run the interpolated command as a prompt
654
+ permissions_config = _parse_permissions(allow, deny, permissions, permission_default)
655
+ mcp_command, mcp_env, permissions_config, mcp_servers = _apply_config_defaults(
656
+ None, None, permissions_config
657
+ )
658
+ _run_prompt(
659
+ prompt,
660
+ model=model,
661
+ verbose=verbose,
662
+ stream=stream,
663
+ trace=trace,
664
+ memory=memory,
665
+ tools=tools,
666
+ toolset=toolset,
667
+ max_tokens=max_tokens,
668
+ output_mode=output_mode,
669
+ approval=approval,
670
+ approve_all_tools=approve_all_tools,
671
+ approval_timeout=approval_timeout,
672
+ no_rules=no_rules,
673
+ permissions_config=permissions_config,
674
+ mcp=mcp_command,
675
+ mcp_env=mcp_env,
676
+ mcp_servers=mcp_servers,
677
+ continue_session=continue_session,
678
+ session=session,
679
+ fork=fork,
680
+ no_save=no_save,
681
+ thinking_budget=thinking_budget,
682
+ )
683
+ return
684
+
685
+ if not target:
686
+ output.print_panel(
687
+ "Run agents from a file or prompt.\n\n"
688
+ "Usage:\n"
689
+ " praisonai run agents.yaml\n"
690
+ " praisonai run \"What is the weather?\"\n"
691
+ " praisonai run \"What is the weather?\" --continue\n"
692
+ " praisonai run \"Add tests\" --session abc123\n"
693
+ " praisonai run agents.yaml --interactive\n"
694
+ " praisonai run --agent researcher \"Find info on X\"\n"
695
+ " praisonai run --command summarize \"Long text here\"\n\n"
696
+ "Options:\n"
697
+ " --model, -m LLM model to use\n"
698
+ f" --framework, -f {_FRAMEWORK_HELP}\n"
699
+ " --interactive, -i Interactive mode\n"
700
+ " --verbose, -v Verbose output\n"
701
+ " --trace Enable tracing\n"
702
+ " --memory Enable memory\n"
703
+ " --continue, -c Continue the most recent session\n"
704
+ " --session, -s Resume a specific session ID\n"
705
+ " --fork Fork from specified session\n"
706
+ " --no-save Don't auto-save session\n"
707
+ " --agent, -a Use a named custom agent\n"
708
+ " --command Execute a named custom command",
709
+ title="Run Command"
710
+ )
711
+ return
712
+
713
+ # Emit start event
714
+ from ..output.event_bridge import SCHEMA_VERSION
715
+ output.emit_start(
716
+ message=f"Starting run: {target[:50]}..." if len(target) > 50 else f"Starting run: {target}",
717
+ data={
718
+ "schema_version": SCHEMA_VERSION,
719
+ "target": target,
720
+ "model": model,
721
+ "framework": framework,
722
+ }
723
+ )
724
+
725
+ # Check if target is a file or prompt
726
+ import os
727
+ is_file = os.path.exists(target) and (target.endswith('.yaml') or target.endswith('.yml'))
728
+
729
+ # Only the direct-prompt path forwards to the warm runtime, so reject
730
+ # --attach on file execution rather than letting it run with no observable
731
+ # session (the attach client would wait forever for events).
732
+ if attach and is_file:
733
+ output.print_error("--attach is only supported for direct prompt runs")
734
+ raise typer.Exit(1)
735
+
736
+ # Auto-checkpoint before file-based runs so a bad turn can be rewound with
737
+ # `praisonai run --restore last`. Scoped to YAML-file runs (which mutate
738
+ # project files) and snapshotted against the file's own directory so the
739
+ # checkpoint protects the right workspace. Plain-prompt runs don't touch
740
+ # project files, so they skip checkpointing (avoiding spurious "no changes"
741
+ # noise). Best-effort and gated by config (`checkpoints.auto`, default on)
742
+ # and `--no-checkpoint`.
743
+ if is_file:
744
+ from ..state.identifiers import get_current_context as _get_ctx
745
+ _run_id = getattr(_get_ctx(), "run_id", None)
746
+ _auto_checkpoint(
747
+ f"run:{_run_id}" if _run_id else "auto checkpoint before run",
748
+ no_checkpoint=no_checkpoint,
749
+ workspace_dir=os.path.dirname(os.path.abspath(target)) or None,
750
+ )
751
+
752
+ # Handle profiling
753
+ if profile or profile_deep:
754
+ if is_file:
755
+ # Profiling for YAML file execution
756
+ _run_from_file_profiled(
757
+ target,
758
+ model=model,
759
+ framework=framework,
760
+ verbose=verbose,
761
+ profile_deep=profile_deep,
762
+ continue_session=continue_session,
763
+ session=session,
764
+ fork=fork,
765
+ no_save=no_save,
766
+ )
767
+ else:
768
+ # Profiling for direct prompt
769
+ _run_prompt_profiled(
770
+ target,
771
+ model=model,
772
+ verbose=verbose,
773
+ profile_deep=profile_deep,
774
+ continue_session=continue_session,
775
+ session=session,
776
+ fork=fork,
777
+ no_save=no_save,
778
+ )
779
+ return
780
+
781
+ if is_file:
782
+ # Run from file
783
+ _run_from_file(
784
+ target,
785
+ model=model,
786
+ framework=framework,
787
+ interactive=interactive,
788
+ verbose=verbose,
789
+ stream=stream,
790
+ trace=trace,
791
+ memory=memory,
792
+ tools=tools,
793
+ max_tokens=max_tokens,
794
+ output_mode=output_mode,
795
+ continue_session=continue_session,
796
+ session=session,
797
+ fork=fork,
798
+ no_save=no_save,
799
+ )
800
+ else:
801
+ # Run as prompt
802
+ permissions_config = _parse_permissions(allow, deny, permissions, permission_default)
803
+ mcp_command, mcp_env, permissions_config, mcp_servers = _apply_config_defaults(
804
+ None, None, permissions_config
805
+ )
806
+ _run_prompt(
807
+ target,
808
+ model=model,
809
+ verbose=verbose,
810
+ stream=stream,
811
+ trace=trace,
812
+ memory=memory,
813
+ tools=tools,
814
+ toolset=toolset,
815
+ max_tokens=max_tokens,
816
+ output_mode=output_mode,
817
+ approval=approval,
818
+ approve_all_tools=approve_all_tools,
819
+ approval_timeout=approval_timeout,
820
+ no_rules=no_rules,
821
+ permissions_config=permissions_config,
822
+ mcp=mcp_command,
823
+ mcp_env=mcp_env,
824
+ mcp_servers=mcp_servers,
825
+ continue_session=continue_session,
826
+ session=session,
827
+ fork=fork,
828
+ no_save=no_save,
829
+ attach_session=attach,
830
+ thinking_budget=thinking_budget,
831
+ )
832
+
833
+
834
+ def _run_from_file(
835
+ file_path: str,
836
+ model: Optional[str] = None,
837
+ framework: Optional[str] = None,
838
+ interactive: bool = False,
839
+ verbose: bool = False,
840
+ stream: bool = True,
841
+ trace: bool = False,
842
+ memory: bool = False,
843
+ tools: Optional[str] = None,
844
+ max_tokens: int = 16000,
845
+ output_mode: Optional[str] = None,
846
+ continue_session: bool = False,
847
+ session: Optional[str] = None,
848
+ fork: bool = False,
849
+ no_save: bool = False,
850
+ ):
851
+ """Run agents from a YAML file."""
852
+ output = get_output_controller()
853
+
854
+ # Note: Credential check already done in run_main() entry point
855
+
856
+ try:
857
+ # Use existing PraisonAI class
858
+ from praisonai.cli.main import PraisonAI
859
+
860
+ praison = PraisonAI(
861
+ agent_file=file_path,
862
+ framework=framework or "praisonai",
863
+ )
864
+
865
+ # Set model if provided
866
+ if model:
867
+ praison.config_list[0]['model'] = model
868
+
869
+ # Handle session continuity for YAML files
870
+ session_id = None
871
+ auto_save_name = None
872
+
873
+ if continue_session or session or fork:
874
+ from ..state.project_sessions import find_last_session, session_exists_anywhere
875
+
876
+ if continue_session:
877
+ # Find last session for this project
878
+ session_id = find_last_session()
879
+ if session_id:
880
+ output.print_info(f"Continuing session: {session_id}")
881
+ else:
882
+ output.print_warning("No previous sessions found. Starting new session.")
883
+
884
+ elif session:
885
+ # Use specific session
886
+ if session_exists_anywhere(session):
887
+ session_id = session
888
+ output.print_info(f"Resuming session: {session_id}")
889
+ else:
890
+ output.print_error(f"Session not found: {session}")
891
+ raise typer.Exit(1)
892
+
893
+ # Handle forking
894
+ if fork:
895
+ from praisonaiagents.session.hierarchy import HierarchicalSessionStore
896
+ from ..utils.project import get_project_sessions_dir
897
+
898
+ # Create hierarchical store for forking
899
+ hierarchical_store = HierarchicalSessionStore(str(get_project_sessions_dir()))
900
+ forked_session_id = hierarchical_store.fork_session(session_id)
901
+ session_id = forked_session_id
902
+ output.print_info(f"Forked session {session} -> {forked_session_id}")
903
+
904
+ # Enable auto-save if not disabled
905
+ if not no_save:
906
+ import uuid
907
+ auto_save_name = session_id or "session-" + str(uuid.uuid4())[:8]
908
+
909
+ # Create args-like object for session configuration
910
+ if session_id or auto_save_name:
911
+ class Args:
912
+ pass
913
+
914
+ args = Args()
915
+ args.auto_save = auto_save_name
916
+ args.resume_session = session_id
917
+ args.cli_project_sessions = bool(session_id or auto_save_name)
918
+
919
+ praison.args = args
920
+
921
+ # Run
922
+ result = praison.run()
923
+
924
+ _record_session_usage(session_id or auto_save_name, model, output)
925
+ output.emit_result(
926
+ message="Run completed",
927
+ data={"result": str(result) if result else None}
928
+ )
929
+
930
+ if result:
931
+ if not output.is_json_mode:
932
+ output.print_success("Run completed")
933
+
934
+ except Exception as e:
935
+ output.emit_error(message=str(e))
936
+ output.print_error(str(e))
937
+ raise typer.Exit(1)
938
+
939
+
940
+ def _run_prompt(
941
+ prompt: str,
942
+ model: Optional[str] = None,
943
+ verbose: bool = False,
944
+ stream: bool = True,
945
+ trace: bool = False,
946
+ memory: bool = False,
947
+ tools: Optional[str] = None,
948
+ toolset: Optional[str] = None,
949
+ max_tokens: int = 16000,
950
+ output_mode: Optional[str] = None,
951
+ approval: Optional[str] = None,
952
+ approve_all_tools: bool = False,
953
+ approval_timeout: Optional[str] = None,
954
+ no_rules: bool = False,
955
+ permissions_config: Optional[dict] = None,
956
+ mcp: Optional[str] = None,
957
+ mcp_env: Optional[str] = None,
958
+ mcp_servers: Optional[List[dict]] = None,
959
+ continue_session: bool = False,
960
+ session: Optional[str] = None,
961
+ fork: bool = False,
962
+ no_save: bool = False,
963
+ attach_session: Optional[str] = None,
964
+ thinking_budget: Optional[int] = None,
965
+ ):
966
+ """Run a direct prompt."""
967
+ output = get_output_controller()
968
+
969
+ # Note: Credential check already done in run_main() entry point
970
+
971
+ try:
972
+ # Handle session continuity first (before any execution mode)
973
+ from praisonai.cli.main import PraisonAI
974
+
975
+ praison = PraisonAI()
976
+
977
+ if model:
978
+ praison.config_list[0]['model'] = model
979
+
980
+ # Handle session continuity
981
+ session_id = None
982
+ auto_save_name = None
983
+
984
+ if continue_session or session or fork:
985
+ from ..state.project_sessions import find_last_session, session_exists_anywhere
986
+
987
+ if continue_session:
988
+ # Find last session for this project
989
+ session_id = find_last_session()
990
+ if session_id:
991
+ output.print_info(f"Continuing session: {session_id}")
992
+ else:
993
+ output.print_warning("No previous sessions found. Starting new session.")
994
+
995
+ elif session:
996
+ # Use specific session
997
+ if session_exists_anywhere(session):
998
+ session_id = session
999
+ output.print_info(f"Resuming session: {session_id}")
1000
+ else:
1001
+ output.print_error(f"Session not found: {session}")
1002
+ raise typer.Exit(1)
1003
+
1004
+ # Handle forking
1005
+ if fork:
1006
+ from praisonaiagents.session.hierarchy import HierarchicalSessionStore
1007
+ from ..utils.project import get_project_sessions_dir
1008
+
1009
+ # Create hierarchical store for forking
1010
+ hierarchical_store = HierarchicalSessionStore(str(get_project_sessions_dir()))
1011
+ forked_session_id = hierarchical_store.fork_session(session_id)
1012
+ session_id = forked_session_id
1013
+ output.print_info(f"Forked session {session} -> {forked_session_id}")
1014
+
1015
+ # Enable auto-save if not disabled (for all runs, not just session continuity)
1016
+ if not no_save:
1017
+ import uuid
1018
+ auto_save_name = session_id or "session-" + str(uuid.uuid4())[:8]
1019
+
1020
+ # Detect-and-attach: if a warm runtime is running, forward the plain
1021
+ # prompt to it (skipping per-invocation cold-start) and fall back to
1022
+ # in-process execution otherwise. Only the simple text path attaches;
1023
+ # per-invocation tool/approval/memory overrides stay in-process so their
1024
+ # behaviour is preserved exactly.
1025
+ # Session continuity/forking is handled in-process; the warm runtime does
1026
+ # not carry session state, so any explicit session flag stays local.
1027
+ # Default auto-save also stays in-process until the warm path can persist
1028
+ # sessions the same way as the normal run path.
1029
+ # An explicit --thinking budget is a per-invocation override (like tools/
1030
+ # approval/memory), so it stays in-process: the warm runtime reuses a
1031
+ # cached agent and does not carry a per-call thinking budget, so attaching
1032
+ # would silently drop the requested setting.
1033
+ runtime_eligible = no_save and thinking_budget is None and not any([
1034
+ mcp, mcp_servers, tools, toolset, approval, approve_all_tools,
1035
+ memory, permissions_config, continue_session, session, fork,
1036
+ ])
1037
+ # When --attach <id> is given, tag the warm-runtime run with that id so
1038
+ # other terminals (`praisonai attach <id>`) observe its live events.
1039
+ runtime_session_id = attach_session or session_id
1040
+ if runtime_eligible and _try_attach_runtime(
1041
+ prompt,
1042
+ model=model,
1043
+ output_mode=output_mode,
1044
+ session_id=runtime_session_id,
1045
+ ):
1046
+ return
1047
+
1048
+ if output_mode == "actions":
1049
+ from praisonaiagents import Agent
1050
+ from ..state.project_sessions import build_cli_memory_config, apply_cli_session_continuity
1051
+
1052
+ agent_config = {
1053
+ "name": "RunAgent",
1054
+ "role": "Assistant",
1055
+ "goal": "Complete the task",
1056
+ "output": "actions", # Use actions preset
1057
+ }
1058
+ if model:
1059
+ agent_config["llm"] = model
1060
+
1061
+ # Resolve approval backend if specified
1062
+ if approval:
1063
+ from praisonai.cli.features.approval import resolve_approval_config
1064
+ agent_config["approval"] = resolve_approval_config(
1065
+ approval, all_tools=approve_all_tools, timeout=approval_timeout,
1066
+ permissions_config=permissions_config,
1067
+ )
1068
+
1069
+ # Add session support to Agent if needed
1070
+ from ..utils.project import build_cli_memory_config, apply_cli_session_continuity
1071
+ memory_cfg = build_cli_memory_config(session_id=session_id, auto_save=auto_save_name)
1072
+ if memory_cfg is not None:
1073
+ agent_config["memory"] = memory_cfg
1074
+
1075
+ # Wire all configured MCP servers (ad-hoc --mcp + config local/remote).
1076
+ if mcp or mcp_servers:
1077
+ mcp_tools = _build_mcp_tools(mcp, mcp_env, mcp_servers, verbose=verbose)
1078
+ if mcp_tools:
1079
+ agent_config["tools"] = list(agent_config.get("tools", [])) + mcp_tools
1080
+
1081
+ agent = Agent(**agent_config)
1082
+ # Reasoning effort applied via the property setter (not a
1083
+ # constructor kwarg) so defaults are unchanged when omitted.
1084
+ if thinking_budget is not None:
1085
+ agent.thinking_budget = thinking_budget
1086
+ if session_id or auto_save_name:
1087
+ apply_cli_session_continuity(agent, session_id or auto_save_name, auto_save=auto_save_name)
1088
+
1089
+ # Bridge per-step agent events into the structured output stream
1090
+ # so `--output stream-json` surfaces tool/text events, not just
1091
+ # start/result. No-op in non-JSON modes.
1092
+ from ..output.event_bridge import attach_bridge, detach_bridge
1093
+ bridge = attach_bridge(agent, output)
1094
+ if bridge is not None:
1095
+ bridge.emit_agent_message(agent_config.get("name"))
1096
+ try:
1097
+ result = agent.start(prompt)
1098
+ finally:
1099
+ detach_bridge(agent, bridge)
1100
+
1101
+ if bridge is not None:
1102
+ bridge.emit_run_result(result, ok=True)
1103
+ _record_session_usage(session_id or auto_save_name, model, output)
1104
+ output.emit_result(
1105
+ message="Prompt completed",
1106
+ data={"result": str(result) if result else None}
1107
+ )
1108
+
1109
+ # Don't print result again - actions mode already shows output
1110
+ return
1111
+
1112
+ # Use handle_direct_prompt for other modes
1113
+
1114
+ # Create args-like object for handle_direct_prompt
1115
+ class Args:
1116
+ pass
1117
+
1118
+ args = Args()
1119
+ args.llm = model
1120
+ args.verbose = verbose
1121
+ args.memory = memory
1122
+ args.tools = tools
1123
+ args.toolset = toolset
1124
+ args.max_tokens = max_tokens
1125
+ args.web_search = False
1126
+ args.web_fetch = False
1127
+ args.prompt_caching = False
1128
+ args.planning = False
1129
+ args.planning_tools = None
1130
+ args.planning_reasoning = False
1131
+ args.auto_approve_plan = False
1132
+ args.final_agent = None
1133
+ args.user_id = None
1134
+ # Enable session features based on flags
1135
+ args.auto_save = auto_save_name
1136
+ args.history = None
1137
+ args.resume_session = session_id
1138
+ args.cli_project_sessions = bool(session_id or auto_save_name)
1139
+ args.include_rules = None if no_rules else "auto"
1140
+ args.no_rules = no_rules
1141
+ args.workflow = None
1142
+ args.workflow_var = None
1143
+ args.claude_memory = False
1144
+ args.guardrail = None
1145
+ args.metrics = False
1146
+ args.image = None
1147
+ args.image_generate = False
1148
+ args.telemetry = False
1149
+ args.mcp = mcp
1150
+ args.mcp_env = mcp_env
1151
+ args.mcp_servers = mcp_servers or None
1152
+ args.fast_context = None
1153
+ args.handoff = None
1154
+ args.auto_memory = False
1155
+ args.todo = False
1156
+ args.router = False
1157
+ args.router_provider = None
1158
+ args.query_rewrite = False
1159
+ args.rewrite_tools = None
1160
+ args.expand_prompt = False
1161
+ args.expand_tools = None
1162
+ args.no_tools = False
1163
+ args.approval = approval
1164
+ args.thinking_budget = thinking_budget
1165
+
1166
+ praison.args = args
1167
+
1168
+ result = praison.handle_direct_prompt(prompt)
1169
+
1170
+ _record_session_usage(session_id or auto_save_name, model, output)
1171
+ output.emit_result(
1172
+ message="Prompt completed",
1173
+ data={"result": str(result) if result else None}
1174
+ )
1175
+
1176
+ if result and not output.is_json_mode:
1177
+ print(result)
1178
+
1179
+ except typer.Exit:
1180
+ raise
1181
+ except Exception as e:
1182
+ from ..output.event_bridge import StreamEventBridge
1183
+ StreamEventBridge(output).emit_run_error(str(e))
1184
+ output.emit_error(message=str(e))
1185
+ output.print_error(str(e))
1186
+ raise typer.Exit(1)
1187
+
1188
+
1189
+ def _record_session_usage(session_id, model, output) -> None:
1190
+ """Accumulate this run's token/cost usage into the active session and show
1191
+ a compact running total footer (Issue #2421).
1192
+
1193
+ Best-effort: never let usage accounting break a completed run. Stays quiet
1194
+ in JSON mode so machine-readable output is unaffected.
1195
+ """
1196
+ if not session_id:
1197
+ return
1198
+ try:
1199
+ from ..state.project_sessions import (
1200
+ accumulate_session_usage,
1201
+ format_usage_footer,
1202
+ )
1203
+
1204
+ usage = accumulate_session_usage(session_id, model=model)
1205
+ except Exception:
1206
+ return
1207
+
1208
+ if not usage or not usage.get("total_tokens"):
1209
+ return
1210
+ if output is not None and getattr(output, "is_json_mode", False):
1211
+ return
1212
+ try:
1213
+ footer = format_usage_footer(usage)
1214
+ if output is not None:
1215
+ output.print_info(footer)
1216
+ else:
1217
+ typer.echo(footer)
1218
+ except Exception:
1219
+ pass
1220
+
1221
+
1222
+ def _run_from_file_profiled(
1223
+ file_path: str,
1224
+ model: Optional[str] = None,
1225
+ framework: Optional[str] = None,
1226
+ verbose: bool = False,
1227
+ profile_deep: bool = False,
1228
+ continue_session: bool = False,
1229
+ session: Optional[str] = None,
1230
+ fork: bool = False,
1231
+ no_save: bool = False,
1232
+ ):
1233
+ """Run agents from a YAML file with profiling enabled."""
1234
+ from praisonai.cli.features.cli_profiler import (
1235
+ CLIProfileConfig,
1236
+ CLIProfiler,
1237
+ )
1238
+
1239
+ config = CLIProfileConfig(enabled=True, deep=profile_deep)
1240
+ profiler = CLIProfiler(config)
1241
+
1242
+ if profile_deep:
1243
+ typer.echo("⚠️ Deep profiling enabled - this adds significant overhead", err=True)
1244
+
1245
+ profiler.start()
1246
+
1247
+ # Import phase
1248
+ profiler.mark_import_start()
1249
+ try:
1250
+ from praisonai.cli.main import PraisonAI
1251
+ except ImportError as e:
1252
+ typer.echo(f"Error: {e}", err=True)
1253
+ raise typer.Exit(1)
1254
+ profiler.mark_import_end()
1255
+
1256
+ # Agent initialization phase
1257
+ profiler.mark_init_start()
1258
+ praison = PraisonAI(
1259
+ agent_file=file_path,
1260
+ framework=framework or "praisonai",
1261
+ )
1262
+ if model:
1263
+ praison.config_list[0]['model'] = model
1264
+
1265
+ # Apply session continuity if requested
1266
+ session_id = None
1267
+ auto_save_name = None
1268
+
1269
+ if continue_session or session or fork:
1270
+ from ..state.project_sessions import find_last_session, session_exists_anywhere
1271
+
1272
+ if continue_session:
1273
+ session_id = find_last_session()
1274
+ if not session_id:
1275
+ typer.echo("Warning: No previous sessions found. Starting new session.", err=True)
1276
+ elif session:
1277
+ if session_exists_anywhere(session):
1278
+ session_id = session
1279
+
1280
+ if fork:
1281
+ from praisonaiagents.session.hierarchy import HierarchicalSessionStore
1282
+ from ..utils.project import get_project_sessions_dir
1283
+
1284
+ hierarchical_store = HierarchicalSessionStore(str(get_project_sessions_dir()))
1285
+ forked_session_id = hierarchical_store.fork_session(session_id)
1286
+ session_id = forked_session_id
1287
+ else:
1288
+ typer.echo(f"Error: Session not found: {session}", err=True)
1289
+ raise typer.Exit(1)
1290
+
1291
+ if not no_save:
1292
+ import uuid
1293
+ auto_save_name = session_id or "session-" + str(uuid.uuid4())[:8]
1294
+ if session_id or auto_save_name:
1295
+ class Args:
1296
+ pass
1297
+
1298
+ args = Args()
1299
+ args.auto_save = auto_save_name
1300
+ args.resume_session = session_id
1301
+ args.cli_project_sessions = bool(session_id or auto_save_name)
1302
+
1303
+ praison.args = args
1304
+
1305
+ profiler.mark_init_end()
1306
+
1307
+ # Execution phase
1308
+ profiler.mark_exec_start()
1309
+ result = praison.run()
1310
+ profiler.mark_exec_end()
1311
+
1312
+ _record_session_usage(session_id or auto_save_name, model, None)
1313
+
1314
+ profiler.stop()
1315
+
1316
+ # Print result
1317
+ if result:
1318
+ print(result)
1319
+
1320
+ # Print profiling report
1321
+ profiler.print_report()
1322
+
1323
+
1324
+ def _run_custom_agent(
1325
+ agent_config: Dict[str, Any],
1326
+ prompt: str,
1327
+ model: Optional[str] = None,
1328
+ verbose: bool = False,
1329
+ stream: bool = True,
1330
+ trace: bool = False,
1331
+ memory: bool = False,
1332
+ tools: Optional[str] = None,
1333
+ toolset: Optional[str] = None,
1334
+ max_tokens: int = 16000,
1335
+ output_mode: Optional[str] = None,
1336
+ approval: Optional[str] = None,
1337
+ approve_all_tools: bool = False,
1338
+ approval_timeout: Optional[str] = None,
1339
+ invocation_permissions: Optional[dict] = None,
1340
+ continue_session: bool = False,
1341
+ session: Optional[str] = None,
1342
+ fork: bool = False,
1343
+ no_save: bool = False,
1344
+ thinking_budget: Optional[int] = None,
1345
+ ):
1346
+ """Run a custom agent definition."""
1347
+ output = get_output_controller()
1348
+
1349
+ try:
1350
+ from praisonaiagents import Agent
1351
+
1352
+ # Override model if specified
1353
+ if model:
1354
+ agent_config["llm"] = model
1355
+
1356
+ # Add verbose flag
1357
+ if verbose:
1358
+ agent_config["verbose"] = verbose
1359
+
1360
+ # Resolve per-agent permissions (from definition) layered with
1361
+ # invocation flags. Precedence: invocation flags > agent definition.
1362
+ agent_permissions = agent_config.pop("permissions", None) or {}
1363
+ merged_permissions = dict(agent_permissions)
1364
+ if invocation_permissions:
1365
+ merged_permissions.update(invocation_permissions)
1366
+
1367
+ if merged_permissions:
1368
+ from praisonai.cli.features.approval import resolve_approval_config
1369
+ # Preserve promptable `ask` rules: only fall back to non-interactive
1370
+ # when there is no interactive approval path. An explicit --approval
1371
+ # flag or any `ask` rule keeps the backend interactive so the user
1372
+ # can be prompted (e.g. the `review` preset's "ask before shell").
1373
+ has_ask_rules = any(
1374
+ str(action).strip().lower() == "ask"
1375
+ for action in merged_permissions.values()
1376
+ )
1377
+ # Default to a console backend so deny/ask rules are enforced even
1378
+ # when no explicit --approval flag is passed.
1379
+ agent_config["approval"] = resolve_approval_config(
1380
+ approval or "console",
1381
+ all_tools=approve_all_tools,
1382
+ timeout=approval_timeout,
1383
+ non_interactive=approval is None and not has_ask_rules,
1384
+ permissions_config=merged_permissions,
1385
+ )
1386
+ elif approval:
1387
+ from praisonai.cli.features.approval import resolve_approval_config
1388
+ agent_config["approval"] = resolve_approval_config(
1389
+ approval,
1390
+ all_tools=approve_all_tools,
1391
+ timeout=approval_timeout,
1392
+ )
1393
+
1394
+ # Handle session continuity
1395
+ session_id = None
1396
+ auto_save_name = None
1397
+
1398
+ if continue_session or session or fork:
1399
+ from ..state.project_sessions import find_last_session, session_exists_anywhere
1400
+
1401
+ if continue_session:
1402
+ session_id = find_last_session()
1403
+ if session_id:
1404
+ output.print_info(f"Continuing session: {session_id}")
1405
+ else:
1406
+ output.print_warning("No previous sessions found. Starting new session.")
1407
+
1408
+ elif session:
1409
+ if session_exists_anywhere(session):
1410
+ session_id = session
1411
+ output.print_info(f"Resuming session: {session_id}")
1412
+ else:
1413
+ output.print_error(f"Session not found: {session}")
1414
+ raise typer.Exit(1)
1415
+
1416
+ if fork:
1417
+ from praisonaiagents.session.hierarchy import HierarchicalSessionStore
1418
+ from ..utils.project import get_project_sessions_dir
1419
+
1420
+ hierarchical_store = HierarchicalSessionStore(str(get_project_sessions_dir()))
1421
+ forked_session_id = hierarchical_store.fork_session(session_id)
1422
+ session_id = forked_session_id
1423
+ output.print_info(f"Forked session {session} -> {forked_session_id}")
1424
+
1425
+ if not no_save:
1426
+ import uuid
1427
+ auto_save_name = session_id or "session-" + str(uuid.uuid4())[:8]
1428
+
1429
+ # Add session support to agent config
1430
+ if session_id:
1431
+ agent_config["resume_session"] = session_id
1432
+ if auto_save_name:
1433
+ agent_config["auto_save"] = auto_save_name
1434
+
1435
+ # Create and run agent
1436
+ agent = Agent(**agent_config)
1437
+ # Reasoning effort applied via the property setter (not a constructor
1438
+ # kwarg) so defaults are unchanged when --thinking is omitted.
1439
+ if thinking_budget is not None:
1440
+ agent.thinking_budget = thinking_budget
1441
+
1442
+ # Bridge per-step agent events into the structured output stream.
1443
+ from ..output.event_bridge import attach_bridge, detach_bridge
1444
+ bridge = attach_bridge(agent, output)
1445
+ if bridge is not None:
1446
+ bridge.emit_agent_message(agent_config.get("name"))
1447
+ try:
1448
+ result = agent.start(prompt)
1449
+ finally:
1450
+ detach_bridge(agent, bridge)
1451
+
1452
+ if bridge is not None:
1453
+ bridge.emit_run_result(result, ok=True)
1454
+ _record_session_usage(session_id or auto_save_name, model, output)
1455
+ output.emit_result(
1456
+ message="Agent completed",
1457
+ data={"result": str(result) if result else None}
1458
+ )
1459
+
1460
+ if result and not output.is_json_mode:
1461
+ print(result)
1462
+
1463
+ except typer.Exit:
1464
+ raise
1465
+ except Exception as e:
1466
+ from ..output.event_bridge import StreamEventBridge
1467
+ StreamEventBridge(output).emit_run_error(str(e))
1468
+ output.emit_error(message=str(e))
1469
+ output.print_error(str(e))
1470
+ raise typer.Exit(1)
1471
+
1472
+
1473
+ def _run_prompt_profiled(
1474
+ prompt: str,
1475
+ model: Optional[str] = None,
1476
+ verbose: bool = False,
1477
+ profile_deep: bool = False,
1478
+ continue_session: bool = False,
1479
+ session: Optional[str] = None,
1480
+ fork: bool = False,
1481
+ no_save: bool = False,
1482
+ ):
1483
+ """Run a direct prompt with profiling enabled."""
1484
+ from praisonai.cli.features.cli_profiler import (
1485
+ CLIProfileConfig,
1486
+ CLIProfiler,
1487
+ )
1488
+
1489
+ config = CLIProfileConfig(enabled=True, deep=profile_deep)
1490
+ profiler = CLIProfiler(config)
1491
+
1492
+ if profile_deep:
1493
+ typer.echo("⚠️ Deep profiling enabled - this adds significant overhead", err=True)
1494
+
1495
+ profiler.start()
1496
+
1497
+ # Import phase
1498
+ profiler.mark_import_start()
1499
+ try:
1500
+ from praisonaiagents import Agent
1501
+ except ImportError:
1502
+ typer.echo("Error: praisonaiagents not installed", err=True)
1503
+ raise typer.Exit(1)
1504
+ profiler.mark_import_end()
1505
+
1506
+ # Agent initialization phase
1507
+ profiler.mark_init_start()
1508
+ agent_config = {
1509
+ "name": "RunAgent",
1510
+ "role": "Assistant",
1511
+ "goal": "Complete the task",
1512
+ "verbose": verbose,
1513
+ }
1514
+ if model:
1515
+ agent_config["llm"] = model
1516
+
1517
+ # Apply session continuity if requested
1518
+ session_id = None
1519
+ auto_save_name = None
1520
+
1521
+ if continue_session or session or fork:
1522
+ from ..state.project_sessions import find_last_session, session_exists_anywhere
1523
+
1524
+ if continue_session:
1525
+ session_id = find_last_session()
1526
+ if not session_id:
1527
+ typer.echo("Warning: No previous sessions found. Starting new session.", err=True)
1528
+ elif session:
1529
+ if session_exists_anywhere(session):
1530
+ session_id = session
1531
+
1532
+ if fork:
1533
+ from praisonaiagents.session.hierarchy import HierarchicalSessionStore
1534
+ from ..utils.project import get_project_sessions_dir
1535
+
1536
+ hierarchical_store = HierarchicalSessionStore(str(get_project_sessions_dir()))
1537
+ forked_session_id = hierarchical_store.fork_session(session_id)
1538
+ session_id = forked_session_id
1539
+ else:
1540
+ typer.echo(f"Error: Session not found: {session}", err=True)
1541
+ raise typer.Exit(1)
1542
+
1543
+ if not no_save:
1544
+ import uuid
1545
+ auto_save_name = session_id or "session-" + str(uuid.uuid4())[:8]
1546
+ if session_id or auto_save_name:
1547
+ from ..state.project_sessions import build_cli_memory_config, apply_cli_session_continuity
1548
+
1549
+ memory_cfg = build_cli_memory_config(session_id, auto_save_name)
1550
+ if memory_cfg is not None:
1551
+ agent_config["memory"] = memory_cfg
1552
+
1553
+ agent = Agent(**agent_config)
1554
+ if session_id or auto_save_name:
1555
+ apply_cli_session_continuity(agent, session_id or auto_save_name, auto_save=auto_save_name)
1556
+
1557
+ profiler.mark_init_end()
1558
+
1559
+ # Execution phase
1560
+ profiler.mark_exec_start()
1561
+ response = agent.start(prompt)
1562
+ profiler.mark_exec_end()
1563
+
1564
+ _record_session_usage(session_id or auto_save_name, model, None)
1565
+
1566
+ profiler.stop()
1567
+
1568
+ # Print response
1569
+ if response:
1570
+ print(response)
1571
+
1572
+ # Print profiling report
1573
+ profiler.print_report()