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,266 @@
1
+ """
2
+ Doctor engine for executing health checks.
3
+
4
+ Provides the core execution logic for running doctor checks.
5
+ """
6
+
7
+ import os
8
+ import platform
9
+ import sys
10
+ import traceback
11
+ from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeoutError
12
+ from datetime import datetime, timezone
13
+ from typing import Callable, List, Optional
14
+ import time
15
+
16
+ from .models import (
17
+ CheckCategory,
18
+ CheckResult,
19
+ CheckStatus,
20
+ CheckSeverity,
21
+ DoctorConfig,
22
+ DoctorReport,
23
+ EnvironmentSummary,
24
+ )
25
+ from .registry import CheckRegistry, get_registry
26
+
27
+
28
+ class DoctorEngine:
29
+ """
30
+ Engine for executing doctor checks.
31
+
32
+ Handles check execution, timeout management, and report generation.
33
+ """
34
+
35
+ def __init__(self, config: Optional[DoctorConfig] = None):
36
+ """
37
+ Initialize the doctor engine.
38
+
39
+ Args:
40
+ config: Doctor configuration
41
+ """
42
+ self.config = config or DoctorConfig()
43
+ self.registry = get_registry()
44
+ self._results: List[CheckResult] = []
45
+
46
+ def get_environment_summary(self) -> EnvironmentSummary:
47
+ """Gather environment information."""
48
+ # Get package versions lazily
49
+ praisonai_version = "unknown"
50
+ praisonaiagents_version = "unknown"
51
+
52
+ try:
53
+ from praisonai.version import __version__ as pai_version
54
+ praisonai_version = pai_version
55
+ except ImportError:
56
+ pass
57
+
58
+ try:
59
+ import praisonaiagents
60
+ praisonaiagents_version = getattr(praisonaiagents, "__version__", "unknown")
61
+ except ImportError:
62
+ pass
63
+
64
+ return EnvironmentSummary(
65
+ python_version=f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
66
+ python_executable=sys.executable,
67
+ os_name=platform.system(),
68
+ os_version=platform.release(),
69
+ architecture=platform.machine(),
70
+ praisonai_version=praisonai_version,
71
+ praisonaiagents_version=praisonaiagents_version,
72
+ working_directory=os.getcwd(),
73
+ virtual_env=os.environ.get("VIRTUAL_ENV"),
74
+ )
75
+
76
+ def run_check(
77
+ self,
78
+ check_id: str,
79
+ implementation: Callable,
80
+ timeout: Optional[float] = None,
81
+ ) -> CheckResult:
82
+ """
83
+ Run a single check with timeout handling.
84
+
85
+ Args:
86
+ check_id: Check identifier
87
+ implementation: Check function
88
+ timeout: Timeout in seconds
89
+
90
+ Returns:
91
+ Check result
92
+ """
93
+ timeout = timeout or self.config.timeout
94
+ definition = self.registry.get_check(check_id)
95
+
96
+ start_time = time.time()
97
+
98
+ try:
99
+ # Run with timeout using ThreadPoolExecutor
100
+ with ThreadPoolExecutor(max_workers=1) as executor:
101
+ future = executor.submit(implementation, self.config)
102
+ try:
103
+ result = future.result(timeout=timeout)
104
+ result.duration_ms = (time.time() - start_time) * 1000
105
+ return result
106
+ except FuturesTimeoutError:
107
+ return CheckResult(
108
+ id=check_id,
109
+ title=definition.title if definition else check_id,
110
+ category=definition.category if definition else CheckCategory.ENVIRONMENT,
111
+ status=CheckStatus.ERROR,
112
+ message=f"Check timed out after {timeout}s",
113
+ remediation="Increase timeout with --timeout or check for hanging operations",
114
+ duration_ms=(time.time() - start_time) * 1000,
115
+ severity=CheckSeverity.HIGH,
116
+ )
117
+ except Exception as e:
118
+ # Capture exception details but redact potential secrets
119
+ tb = traceback.format_exc()
120
+ return CheckResult(
121
+ id=check_id,
122
+ title=definition.title if definition else check_id,
123
+ category=definition.category if definition else CheckCategory.ENVIRONMENT,
124
+ status=CheckStatus.ERROR,
125
+ message=f"Check failed with error: {type(e).__name__}: {str(e)[:100]}",
126
+ details=tb[:500] if not self.config.quiet else None,
127
+ remediation="Check the error details and fix the underlying issue",
128
+ duration_ms=(time.time() - start_time) * 1000,
129
+ severity=CheckSeverity.HIGH,
130
+ )
131
+
132
+ def run_checks(
133
+ self,
134
+ check_ids: Optional[List[str]] = None,
135
+ categories: Optional[List[CheckCategory]] = None,
136
+ ) -> List[CheckResult]:
137
+ """
138
+ Run multiple checks.
139
+
140
+ Args:
141
+ check_ids: Specific check IDs to run (None = all)
142
+ categories: Filter by categories
143
+
144
+ Returns:
145
+ List of check results
146
+ """
147
+ # Get filtered checks
148
+ checks = self.registry.filter_checks(
149
+ only=check_ids or self.config.only or None,
150
+ skip=self.config.skip or None,
151
+ categories=categories,
152
+ deep_mode=self.config.deep,
153
+ )
154
+
155
+ # Resolve dependencies and get ordered list
156
+ ordered_ids = self.registry.resolve_dependencies([c.id for c in checks])
157
+
158
+ results = []
159
+ failed_deps = set()
160
+
161
+ for check_id in ordered_ids:
162
+ definition = self.registry.get_check(check_id)
163
+ implementation = self.registry.get_implementation(check_id)
164
+
165
+ if not definition or not implementation:
166
+ continue
167
+
168
+ # Check if dependencies failed
169
+ if any(dep in failed_deps for dep in definition.dependencies):
170
+ results.append(CheckResult(
171
+ id=check_id,
172
+ title=definition.title,
173
+ category=definition.category,
174
+ status=CheckStatus.SKIP,
175
+ message="Skipped due to failed dependency",
176
+ severity=definition.severity,
177
+ ))
178
+ continue
179
+
180
+ # Run the check
181
+ result = self.run_check(check_id, implementation)
182
+ results.append(result)
183
+
184
+ # Track failures for dependency resolution
185
+ if result.status in (CheckStatus.FAIL, CheckStatus.ERROR):
186
+ failed_deps.add(check_id)
187
+
188
+ # Fail fast if configured
189
+ if self.config.fail_fast:
190
+ break
191
+
192
+ self._results = results
193
+ return results
194
+
195
+ def generate_report(self, results: Optional[List[CheckResult]] = None) -> DoctorReport:
196
+ """
197
+ Generate a complete doctor report.
198
+
199
+ Args:
200
+ results: Check results (uses stored results if None)
201
+
202
+ Returns:
203
+ Complete doctor report
204
+ """
205
+ results = results or self._results
206
+
207
+ report = DoctorReport(
208
+ version="1.0.0",
209
+ timestamp=datetime.now(timezone.utc).isoformat(),
210
+ environment=self.get_environment_summary(),
211
+ results=results,
212
+ mode="deep" if self.config.deep else "fast",
213
+ filters={
214
+ "only": self.config.only,
215
+ "skip": self.config.skip,
216
+ },
217
+ )
218
+
219
+ report.calculate_summary()
220
+ report.exit_code = report.calculate_exit_code(strict=self.config.strict)
221
+
222
+ return report
223
+
224
+ def run(
225
+ self,
226
+ check_ids: Optional[List[str]] = None,
227
+ categories: Optional[List[CheckCategory]] = None,
228
+ ) -> DoctorReport:
229
+ """
230
+ Run checks and generate report.
231
+
232
+ Args:
233
+ check_ids: Specific check IDs to run
234
+ categories: Filter by categories
235
+
236
+ Returns:
237
+ Complete doctor report
238
+ """
239
+ start_time = time.time()
240
+
241
+ results = self.run_checks(check_ids, categories)
242
+ report = self.generate_report(results)
243
+
244
+ report.duration_ms = (time.time() - start_time) * 1000
245
+
246
+ return report
247
+
248
+
249
+ def run_doctor(
250
+ config: Optional[DoctorConfig] = None,
251
+ check_ids: Optional[List[str]] = None,
252
+ categories: Optional[List[CheckCategory]] = None,
253
+ ) -> DoctorReport:
254
+ """
255
+ Convenience function to run doctor checks.
256
+
257
+ Args:
258
+ config: Doctor configuration
259
+ check_ids: Specific check IDs to run
260
+ categories: Filter by categories
261
+
262
+ Returns:
263
+ Complete doctor report
264
+ """
265
+ engine = DoctorEngine(config)
266
+ return engine.run(check_ids, categories)
@@ -0,0 +1,377 @@
1
+ """
2
+ Output formatters for the Doctor CLI module.
3
+
4
+ Provides text and JSON formatting with secret redaction.
5
+ """
6
+
7
+ import json
8
+ import re
9
+ import sys
10
+ from typing import Any, Dict, List, Optional, TextIO
11
+ from .models import CheckResult, CheckStatus, DoctorReport, ReportSummary
12
+
13
+
14
+ # Patterns for detecting secrets
15
+ SECRET_PATTERNS = [
16
+ re.compile(r'(sk-[a-zA-Z0-9]{20,})', re.IGNORECASE), # OpenAI
17
+ re.compile(r'(sk-ant-[a-zA-Z0-9-]{20,})', re.IGNORECASE), # Anthropic
18
+ re.compile(r'(AIza[a-zA-Z0-9_-]{35})', re.IGNORECASE), # Google
19
+ re.compile(r'(tvly-[a-zA-Z0-9]{20,})', re.IGNORECASE), # Tavily
20
+ re.compile(r'(xai-[a-zA-Z0-9]{20,})', re.IGNORECASE), # xAI
21
+ re.compile(r'([a-zA-Z0-9_-]*api[_-]?key[a-zA-Z0-9_-]*=\s*["\']?[a-zA-Z0-9_-]{16,})', re.IGNORECASE),
22
+ re.compile(r'([a-zA-Z0-9_-]*token[a-zA-Z0-9_-]*=\s*["\']?[a-zA-Z0-9_-]{16,})', re.IGNORECASE),
23
+ re.compile(r'([a-zA-Z0-9_-]*secret[a-zA-Z0-9_-]*=\s*["\']?[a-zA-Z0-9_-]{16,})', re.IGNORECASE),
24
+ re.compile(r'(password\s*=\s*["\']?[^\s"\']{8,})', re.IGNORECASE),
25
+ ]
26
+
27
+ # Known API key environment variable names
28
+ API_KEY_ENV_VARS = [
29
+ "OPENAI_API_KEY",
30
+ "ANTHROPIC_API_KEY",
31
+ "GOOGLE_API_KEY",
32
+ "GEMINI_API_KEY",
33
+ "TAVILY_API_KEY",
34
+ "EXA_API_KEY",
35
+ "COHERE_API_KEY",
36
+ "HUGGINGFACE_API_KEY",
37
+ "HF_TOKEN",
38
+ "LANGFUSE_PUBLIC_KEY",
39
+ "LANGFUSE_SECRET_KEY",
40
+ "LANGSMITH_API_KEY",
41
+ "AGENTOPS_API_KEY",
42
+ "WANDB_API_KEY",
43
+ "DATADOG_API_KEY",
44
+ "BRAINTRUST_API_KEY",
45
+ "YDC_API_KEY",
46
+ ]
47
+
48
+
49
+ def redact_secrets(text: str, show_prefix_suffix: bool = False) -> str:
50
+ """
51
+ Redact secrets from text.
52
+
53
+ Args:
54
+ text: Text to redact
55
+ show_prefix_suffix: If True, show first 4 and last 4 chars
56
+
57
+ Returns:
58
+ Text with secrets redacted
59
+ """
60
+ if not text:
61
+ return text
62
+
63
+ result = text
64
+
65
+ for pattern in SECRET_PATTERNS:
66
+ def replace_match(match):
67
+ secret = match.group(1)
68
+ if show_prefix_suffix and len(secret) > 12:
69
+ return f"{secret[:4]}...{secret[-4:]}"
70
+ return "***REDACTED***"
71
+ result = pattern.sub(replace_match, result)
72
+
73
+ return result
74
+
75
+
76
+ def redact_dict(data: Dict[str, Any], show_prefix_suffix: bool = False) -> Dict[str, Any]:
77
+ """
78
+ Recursively redact secrets from a dictionary.
79
+
80
+ Args:
81
+ data: Dictionary to redact
82
+ show_prefix_suffix: If True, show first 4 and last 4 chars
83
+
84
+ Returns:
85
+ Dictionary with secrets redacted
86
+ """
87
+ result = {}
88
+ for key, value in data.items():
89
+ if isinstance(value, str):
90
+ # Check if key suggests it's a secret
91
+ key_lower = key.lower()
92
+ if any(s in key_lower for s in ["key", "token", "secret", "password", "credential"]):
93
+ if show_prefix_suffix and len(value) > 12:
94
+ result[key] = f"{value[:4]}...{value[-4:]}"
95
+ else:
96
+ result[key] = "***REDACTED***"
97
+ else:
98
+ result[key] = redact_secrets(value, show_prefix_suffix)
99
+ elif isinstance(value, dict):
100
+ result[key] = redact_dict(value, show_prefix_suffix)
101
+ elif isinstance(value, list):
102
+ result[key] = [
103
+ redact_dict(v, show_prefix_suffix) if isinstance(v, dict)
104
+ else redact_secrets(v, show_prefix_suffix) if isinstance(v, str)
105
+ else v
106
+ for v in value
107
+ ]
108
+ else:
109
+ result[key] = value
110
+ return result
111
+
112
+
113
+ class BaseFormatter:
114
+ """Base class for output formatters."""
115
+
116
+ def __init__(
117
+ self,
118
+ no_color: bool = False,
119
+ quiet: bool = False,
120
+ redact: bool = True,
121
+ show_prefix_suffix: bool = False,
122
+ ):
123
+ self.no_color = no_color or not sys.stdout.isatty()
124
+ self.quiet = quiet
125
+ self.redact = redact
126
+ self.show_prefix_suffix = show_prefix_suffix
127
+
128
+ def format_report(self, report: DoctorReport) -> str:
129
+ """Format a complete report."""
130
+ raise NotImplementedError
131
+
132
+ def format_result(self, result: CheckResult) -> str:
133
+ """Format a single check result."""
134
+ raise NotImplementedError
135
+
136
+ def _safe_write(self, output: TextIO, text: str) -> None:
137
+ """Write text, falling back to ASCII on legacy-console encoding errors.
138
+
139
+ On Windows consoles using a legacy code page (e.g. cp1252), writing
140
+ Unicode characters raises UnicodeEncodeError. When that happens we
141
+ retry with an ASCII-safe representation so the command never crashes.
142
+ """
143
+ try:
144
+ output.write(text)
145
+ except UnicodeEncodeError:
146
+ encoding = getattr(output, "encoding", None) or "ascii"
147
+ safe_text = text.encode(encoding, errors="replace").decode(encoding, errors="replace")
148
+ output.write(safe_text)
149
+
150
+ def write(self, report: DoctorReport, output: Optional[TextIO] = None) -> None:
151
+ """Write formatted report to output."""
152
+ output = output or sys.stdout
153
+ rendered = self.format_report(report)
154
+ if not rendered.endswith("\n"):
155
+ rendered += "\n"
156
+ self._safe_write(output, rendered)
157
+
158
+
159
+ class TextFormatter(BaseFormatter):
160
+ """Text formatter with optional color support."""
161
+
162
+ def __init__(
163
+ self,
164
+ no_color: bool = False,
165
+ quiet: bool = False,
166
+ redact: bool = True,
167
+ show_prefix_suffix: bool = False,
168
+ ):
169
+ super().__init__(no_color, quiet, redact, show_prefix_suffix)
170
+ # Cache encoding detection result at initialization
171
+ self._unicode_supported = self._can_encode_unicode()
172
+
173
+ # ANSI color codes
174
+ COLORS = {
175
+ "reset": "\033[0m",
176
+ "bold": "\033[1m",
177
+ "dim": "\033[2m",
178
+ "red": "\033[31m",
179
+ "green": "\033[32m",
180
+ "yellow": "\033[33m",
181
+ "blue": "\033[34m",
182
+ "cyan": "\033[36m",
183
+ "white": "\033[37m",
184
+ }
185
+
186
+ STATUS_COLORS = {
187
+ CheckStatus.PASS: "green",
188
+ CheckStatus.WARN: "yellow",
189
+ CheckStatus.FAIL: "red",
190
+ CheckStatus.SKIP: "dim",
191
+ CheckStatus.ERROR: "red",
192
+ }
193
+
194
+ STATUS_SYMBOLS = {
195
+ CheckStatus.PASS: "✓",
196
+ CheckStatus.WARN: "⚠",
197
+ CheckStatus.FAIL: "✗",
198
+ CheckStatus.SKIP: "○",
199
+ CheckStatus.ERROR: "✗",
200
+ }
201
+
202
+ # ASCII fallback symbols for systems that don't support UTF-8
203
+ STATUS_SYMBOLS_ASCII = {
204
+ CheckStatus.PASS: "[OK]",
205
+ CheckStatus.WARN: "[!]",
206
+ CheckStatus.FAIL: "[X]",
207
+ CheckStatus.SKIP: "[-]",
208
+ CheckStatus.ERROR: "[X]",
209
+ }
210
+
211
+ # Divider character with ASCII fallback for legacy (cp1252) consoles
212
+ DIVIDER_UNICODE = "━"
213
+ DIVIDER_ASCII = "-"
214
+
215
+ def _color(self, text: str, color: str) -> str:
216
+ """Apply color to text if colors are enabled."""
217
+ if self.no_color:
218
+ return text
219
+ return f"{self.COLORS.get(color, '')}{text}{self.COLORS['reset']}"
220
+
221
+ def _divider(self, width: int = 70) -> str:
222
+ """Return a horizontal divider, using ASCII on non-Unicode consoles."""
223
+ char = self.DIVIDER_UNICODE if self._unicode_supported else self.DIVIDER_ASCII
224
+ return char * width
225
+
226
+ def _can_encode_unicode(self) -> bool:
227
+ """Check if stdout can encode Unicode symbols."""
228
+ try:
229
+ # Get stdout encoding
230
+ encoding = getattr(sys.stdout, 'encoding', 'ascii') or 'ascii'
231
+
232
+ # UTF-8 always supports Unicode
233
+ if encoding.lower() in ('utf-8', 'utf8'):
234
+ return True
235
+
236
+ # Test if we can encode our specific Unicode symbols
237
+ test_symbols = ["✓", "⚠", "✗", "○", self.DIVIDER_UNICODE]
238
+ for symbol in test_symbols:
239
+ try:
240
+ symbol.encode(encoding, errors='strict')
241
+ except (UnicodeEncodeError, LookupError):
242
+ return False
243
+ return True
244
+ except (AttributeError, TypeError):
245
+ # If we can't determine encoding, assume ASCII-only is safer
246
+ return False
247
+
248
+ def _status_symbol(self, status: CheckStatus) -> str:
249
+ """Get colored status symbol with encoding safety."""
250
+ if self._unicode_supported:
251
+ symbol = self.STATUS_SYMBOLS.get(status, "?")
252
+ else:
253
+ symbol = self.STATUS_SYMBOLS_ASCII.get(status, "[?]")
254
+ color = self.STATUS_COLORS.get(status, "white")
255
+ return self._color(symbol, color)
256
+
257
+ def format_result(self, result: CheckResult) -> str:
258
+ """Format a single check result."""
259
+ symbol = self._status_symbol(result.status)
260
+ message = result.message
261
+ if self.redact:
262
+ message = redact_secrets(message, self.show_prefix_suffix)
263
+
264
+ line = f"{symbol} {result.title}: {message}"
265
+
266
+ if result.details and not self.quiet:
267
+ details = result.details
268
+ if self.redact:
269
+ details = redact_secrets(details, self.show_prefix_suffix)
270
+ line += f"\n {self._color(details, 'dim')}"
271
+
272
+ if result.remediation and result.status in (CheckStatus.FAIL, CheckStatus.ERROR):
273
+ line += f"\n {self._color('Fix:', 'yellow')} {result.remediation}"
274
+
275
+ return line
276
+
277
+ def format_summary(self, summary: ReportSummary, strict: bool = False) -> str:
278
+ """Format the summary section."""
279
+ parts = []
280
+
281
+ if summary.passed > 0:
282
+ parts.append(self._color(f"{summary.passed} passed", "green"))
283
+ if summary.warnings > 0:
284
+ color = "red" if strict else "yellow"
285
+ parts.append(self._color(f"{summary.warnings} warnings", color))
286
+ if summary.failed > 0:
287
+ parts.append(self._color(f"{summary.failed} failed", "red"))
288
+ if summary.skipped > 0:
289
+ parts.append(self._color(f"{summary.skipped} skipped", "dim"))
290
+ if summary.errors > 0:
291
+ parts.append(self._color(f"{summary.errors} errors", "red"))
292
+
293
+ return f"{summary.total} checks: " + ", ".join(parts)
294
+
295
+ def format_report(self, report: DoctorReport) -> str:
296
+ """Format a complete report."""
297
+ lines = []
298
+
299
+ # Header
300
+ if not self.quiet:
301
+ header = f"PraisonAI Doctor v{report.version}"
302
+ lines.append(self._color(header, "bold"))
303
+ lines.append(self._divider())
304
+
305
+ # Results
306
+ for result in report.results:
307
+ lines.append(self.format_result(result))
308
+
309
+ # Summary
310
+ if not self.quiet:
311
+ lines.append(self._divider())
312
+ lines.append(self.format_summary(report.summary))
313
+
314
+ # Duration
315
+ if not self.quiet and report.duration_ms > 0:
316
+ lines.append(self._color(f"Completed in {report.duration_ms:.0f}ms", "dim"))
317
+
318
+ return "\n".join(lines)
319
+
320
+
321
+ class JsonFormatter(BaseFormatter):
322
+ """JSON formatter with deterministic output."""
323
+
324
+ def format_result(self, result: CheckResult) -> str:
325
+ """Format a single check result as JSON."""
326
+ data = result.to_dict()
327
+ if self.redact:
328
+ data = redact_dict(data, self.show_prefix_suffix)
329
+ return json.dumps(data, indent=2, sort_keys=True)
330
+
331
+ def format_report(self, report: DoctorReport) -> str:
332
+ """Format a complete report as JSON."""
333
+ data = report.to_dict()
334
+ if self.redact:
335
+ data = redact_dict(data, self.show_prefix_suffix)
336
+
337
+ # Ensure deterministic ordering
338
+ return json.dumps(data, indent=2, sort_keys=True)
339
+
340
+ # write() is inherited from BaseFormatter: it appends a trailing newline
341
+ # when absent (json.dumps never emits one) and routes through _safe_write,
342
+ # so the behaviour is identical and no override is needed.
343
+
344
+
345
+ def get_formatter(
346
+ format_type: str = "text",
347
+ no_color: bool = False,
348
+ quiet: bool = False,
349
+ redact: bool = True,
350
+ show_prefix_suffix: bool = False,
351
+ ) -> BaseFormatter:
352
+ """
353
+ Get a formatter instance.
354
+
355
+ Args:
356
+ format_type: "text" or "json"
357
+ no_color: Disable ANSI colors
358
+ quiet: Minimal output
359
+ redact: Redact secrets
360
+ show_prefix_suffix: Show partial secrets
361
+
362
+ Returns:
363
+ Formatter instance
364
+ """
365
+ if format_type == "json":
366
+ return JsonFormatter(
367
+ no_color=True, # JSON never has colors
368
+ quiet=quiet,
369
+ redact=redact,
370
+ show_prefix_suffix=show_prefix_suffix,
371
+ )
372
+ return TextFormatter(
373
+ no_color=no_color,
374
+ quiet=quiet,
375
+ redact=redact,
376
+ show_prefix_suffix=show_prefix_suffix,
377
+ )