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,389 @@
1
+ """
2
+ Browser-based (OAuth / device-code) provider sign-in for the PraisonAI CLI.
3
+
4
+ This module implements the operator-onboarding OAuth flow for the ``auth login``
5
+ command. It intentionally lives in the wrapper layer (``praisonai``) because it
6
+ is heavy, provider-specific onboarding UX; it *reuses* the protocol-first OAuth
7
+ primitives shipped in the core SDK (``praisonaiagents.mcp``) rather than
8
+ reimplementing PKCE / a local-callback server.
9
+
10
+ Two flow styles are supported per provider:
11
+ - ``device``: RFC 8628 device-authorization grant (print a code + URL, poll).
12
+ - ``authcode``: RFC 7636 authorization-code + PKCE via a local callback.
13
+
14
+ Provider OAuth endpoints are not hardcoded for unknown providers; a provider is
15
+ "OAuth-capable" only if it appears in :data:`OAUTH_PROVIDERS` or the caller
16
+ supplies explicit endpoints. Providers without a config fall back to API-key
17
+ login automatically (handled by the CLI command).
18
+ """
19
+
20
+ import time
21
+ import webbrowser
22
+ from dataclasses import dataclass, field
23
+ from typing import Any, Dict, Optional, Tuple
24
+
25
+
26
+ @dataclass
27
+ class OAuthProviderConfig:
28
+ """OAuth endpoint configuration for a single provider."""
29
+ flow: str # "device" or "authcode"
30
+ client_id: str
31
+ token_url: str
32
+ device_authorization_url: Optional[str] = None
33
+ authorization_url: Optional[str] = None
34
+ scope: Optional[str] = None
35
+ audience: Optional[str] = None
36
+ extra_auth_params: Dict[str, str] = field(default_factory=dict)
37
+
38
+
39
+ # Built-in OAuth-capable providers. This registry is intentionally small and
40
+ # additive: providers absent here transparently fall back to API-key login.
41
+ # Endpoints can also be supplied at call-time (e.g. for self-hosted gateways).
42
+ OAUTH_PROVIDERS: Dict[str, OAuthProviderConfig] = {}
43
+
44
+
45
+ def get_provider_config(
46
+ provider: str,
47
+ overrides: Optional[Dict[str, Any]] = None,
48
+ ) -> Optional[OAuthProviderConfig]:
49
+ """
50
+ Resolve an :class:`OAuthProviderConfig` for ``provider``.
51
+
52
+ Args:
53
+ provider: Provider name (case-insensitive).
54
+ overrides: Optional dict of endpoint overrides. If it contains the
55
+ minimum required fields it is used to construct an ad-hoc config,
56
+ allowing OAuth for providers not in the built-in registry.
57
+
58
+ Returns:
59
+ Config if the provider supports OAuth, else None.
60
+ """
61
+ overrides = overrides or {}
62
+ base = OAUTH_PROVIDERS.get(provider.lower())
63
+
64
+ merged: Dict[str, Any] = {}
65
+ if base is not None:
66
+ merged.update(base.__dict__)
67
+ merged.update({k: v for k, v in overrides.items() if v is not None})
68
+
69
+ if not merged.get("client_id") or not merged.get("token_url"):
70
+ return None
71
+ if not merged.get("flow"):
72
+ merged["flow"] = "device" if merged.get("device_authorization_url") else "authcode"
73
+
74
+ return OAuthProviderConfig(
75
+ flow=merged["flow"],
76
+ client_id=merged["client_id"],
77
+ token_url=merged["token_url"],
78
+ device_authorization_url=merged.get("device_authorization_url"),
79
+ authorization_url=merged.get("authorization_url"),
80
+ scope=merged.get("scope"),
81
+ audience=merged.get("audience"),
82
+ extra_auth_params=merged.get("extra_auth_params") or {},
83
+ )
84
+
85
+
86
+ def provider_supports_oauth(
87
+ provider: str,
88
+ overrides: Optional[Dict[str, Any]] = None,
89
+ ) -> bool:
90
+ """Return True if ``provider`` has a usable OAuth configuration."""
91
+ return get_provider_config(provider, overrides) is not None
92
+
93
+
94
+ def _tokens_to_credential_kwargs(payload: Dict[str, Any]) -> Dict[str, Any]:
95
+ """Normalise a token endpoint response into store kwargs."""
96
+ access_token = payload.get("access_token")
97
+ expires_in = payload.get("expires_in")
98
+ expires_at = (time.time() + float(expires_in)) if expires_in else None
99
+ return {
100
+ "access_token": access_token,
101
+ "refresh_token": payload.get("refresh_token"),
102
+ "expires_at": expires_at,
103
+ "scope": payload.get("scope"),
104
+ }
105
+
106
+
107
+ def run_device_code_flow(
108
+ config: OAuthProviderConfig,
109
+ *,
110
+ open_browser: bool = True,
111
+ on_prompt=None,
112
+ poll_timeout: float = 300.0,
113
+ ) -> Dict[str, Any]:
114
+ """
115
+ Run the RFC 8628 device-authorization flow.
116
+
117
+ Args:
118
+ config: Provider OAuth config (must have ``device_authorization_url``).
119
+ open_browser: Whether to attempt opening the verification URL.
120
+ on_prompt: Optional callback ``(verification_uri, user_code)`` used to
121
+ display instructions to the user.
122
+ poll_timeout: Max seconds to poll for authorization.
123
+
124
+ Returns:
125
+ Dict of credential kwargs (access_token, refresh_token, expires_at,
126
+ scope) plus ``token_url`` and ``client_id`` for later refresh.
127
+
128
+ Raises:
129
+ RuntimeError: On request failure or timeout.
130
+ """
131
+ try:
132
+ import requests
133
+ except ImportError as exc:
134
+ raise RuntimeError(
135
+ "OAuth login requires the optional 'requests' package. "
136
+ "Install it with: pip install requests"
137
+ ) from exc
138
+
139
+ if not config.device_authorization_url:
140
+ raise RuntimeError("Provider does not define a device authorization endpoint")
141
+
142
+ data = {"client_id": config.client_id}
143
+ if config.scope:
144
+ data["scope"] = config.scope
145
+ if config.audience:
146
+ data["audience"] = config.audience
147
+
148
+ resp = requests.post(config.device_authorization_url, data=data, timeout=30)
149
+ resp.raise_for_status()
150
+ dev = resp.json()
151
+
152
+ device_code = dev.get("device_code")
153
+ user_code = dev.get("user_code")
154
+ verification_uri = dev.get("verification_uri") or dev.get("verification_url")
155
+ verification_uri_complete = dev.get("verification_uri_complete")
156
+ interval = float(dev.get("interval", 5))
157
+
158
+ if on_prompt:
159
+ on_prompt(verification_uri, user_code)
160
+
161
+ if open_browser and (verification_uri_complete or verification_uri):
162
+ try:
163
+ webbrowser.open(verification_uri_complete or verification_uri)
164
+ except Exception:
165
+ pass
166
+
167
+ deadline = time.time() + poll_timeout
168
+ token_data = {
169
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
170
+ "device_code": device_code,
171
+ "client_id": config.client_id,
172
+ }
173
+
174
+ while time.time() < deadline:
175
+ time.sleep(interval)
176
+ tok = requests.post(config.token_url, data=token_data, timeout=30)
177
+ payload = tok.json()
178
+ if tok.status_code == 200 and payload.get("access_token"):
179
+ return _tokens_to_credential_kwargs(payload)
180
+
181
+ error = payload.get("error")
182
+ if error == "authorization_pending":
183
+ continue
184
+ if error == "slow_down":
185
+ interval += 5
186
+ continue
187
+ raise RuntimeError(f"Device authorization failed: {error or tok.status_code}")
188
+
189
+ raise RuntimeError("Timed out waiting for device authorization")
190
+
191
+
192
+ def run_authcode_flow(
193
+ config: OAuthProviderConfig,
194
+ *,
195
+ open_browser: bool = True,
196
+ on_prompt=None,
197
+ callback_timeout: float = 300.0,
198
+ ) -> Dict[str, Any]:
199
+ """
200
+ Run the RFC 7636 authorization-code + PKCE flow via a local callback.
201
+
202
+ Reuses the core SDK's :class:`OAuthCallbackHandler` and PKCE helpers so the
203
+ wrapper does not reimplement OAuth client primitives.
204
+
205
+ Returns:
206
+ Dict of credential kwargs (access_token, refresh_token, expires_at,
207
+ scope).
208
+
209
+ Raises:
210
+ RuntimeError: On request failure or timeout.
211
+ """
212
+ try:
213
+ import requests
214
+ except ImportError as exc:
215
+ raise RuntimeError(
216
+ "OAuth login requires the optional 'requests' package. "
217
+ "Install it with: pip install requests"
218
+ ) from exc
219
+
220
+ from praisonaiagents.mcp.mcp_oauth_callback import (
221
+ OAuthCallbackHandler,
222
+ OAUTH_CALLBACK_PORT,
223
+ OAUTH_CALLBACK_PATH,
224
+ generate_state,
225
+ generate_code_verifier,
226
+ generate_code_challenge,
227
+ get_redirect_url,
228
+ )
229
+
230
+ if not config.authorization_url:
231
+ raise RuntimeError("Provider does not define an authorization endpoint")
232
+
233
+ handler = OAuthCallbackHandler()
234
+ state = generate_state()
235
+ verifier = generate_code_verifier()
236
+ challenge = generate_code_challenge(verifier)
237
+ redirect_uri = get_redirect_url()
238
+
239
+ # ``OAuthCallbackHandler`` only holds in-memory state; it does not listen on
240
+ # the redirect URI. Start a short-lived local HTTP server that receives the
241
+ # provider redirect and forwards (state, code) into the handler so that
242
+ # ``wait_for_callback`` can unblock.
243
+ server = _start_callback_server(handler, OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH)
244
+
245
+ params = {
246
+ "response_type": "code",
247
+ "client_id": config.client_id,
248
+ "redirect_uri": redirect_uri,
249
+ "state": state,
250
+ "code_challenge": challenge,
251
+ "code_challenge_method": "S256",
252
+ }
253
+ if config.scope:
254
+ params["scope"] = config.scope
255
+ if config.audience:
256
+ params["audience"] = config.audience
257
+ params.update(config.extra_auth_params)
258
+
259
+ from urllib.parse import urlencode
260
+
261
+ auth_url = f"{config.authorization_url}?{urlencode(params)}"
262
+
263
+ if on_prompt:
264
+ on_prompt(auth_url, None)
265
+ if open_browser:
266
+ try:
267
+ webbrowser.open(auth_url)
268
+ except Exception:
269
+ pass
270
+
271
+ try:
272
+ code = handler.wait_for_callback(state, timeout=callback_timeout)
273
+ finally:
274
+ handler.clear_state(state)
275
+ server.shutdown()
276
+ server.server_close()
277
+
278
+ token_data = {
279
+ "grant_type": "authorization_code",
280
+ "code": code,
281
+ "redirect_uri": redirect_uri,
282
+ "client_id": config.client_id,
283
+ "code_verifier": verifier,
284
+ }
285
+ tok = requests.post(config.token_url, data=token_data, timeout=30)
286
+ tok.raise_for_status()
287
+ payload = tok.json()
288
+ if not payload.get("access_token"):
289
+ raise RuntimeError("Token endpoint did not return an access token")
290
+ return _tokens_to_credential_kwargs(payload)
291
+
292
+
293
+ def _start_callback_server(handler, port: int, path: str):
294
+ """
295
+ Start a short-lived local HTTP server to receive the OAuth redirect.
296
+
297
+ The provider redirects the browser to ``http://127.0.0.1:<port><path>?...``;
298
+ this server parses the ``state``/``code`` (or ``error``) query parameters,
299
+ forwards them into ``handler.receive_callback`` so a blocked
300
+ ``wait_for_callback`` can unblock, and shows a minimal browser confirmation.
301
+
302
+ Args:
303
+ handler: An ``OAuthCallbackHandler`` to receive (state, code) pairs.
304
+ port: Localhost port to listen on (matches the registered redirect URI).
305
+ path: Expected callback path.
306
+
307
+ Returns:
308
+ A running ``http.server.HTTPServer`` whose ``serve_forever`` loop runs on
309
+ a daemon thread. Callers must ``shutdown()`` + ``server_close()`` it.
310
+ """
311
+ import threading
312
+ from http.server import BaseHTTPRequestHandler, HTTPServer
313
+ from urllib.parse import urlparse, parse_qs
314
+
315
+ class _CallbackRequestHandler(BaseHTTPRequestHandler):
316
+ def do_GET(self): # noqa: N802 (stdlib-mandated name)
317
+ parsed = urlparse(self.path)
318
+ if parsed.path != path:
319
+ self.send_response(404)
320
+ self.end_headers()
321
+ return
322
+
323
+ params = parse_qs(parsed.query)
324
+ state = (params.get("state") or [None])[0]
325
+ code = (params.get("code") or [None])[0]
326
+ error = (params.get("error") or [None])[0]
327
+
328
+ if state and code:
329
+ handler.receive_callback(state, code)
330
+ body = b"Sign-in complete. You can close this tab."
331
+ else:
332
+ body = (
333
+ f"Sign-in failed: {error or 'missing code'}. "
334
+ "You can close this tab."
335
+ ).encode()
336
+
337
+ self.send_response(200)
338
+ self.send_header("Content-Type", "text/plain; charset=utf-8")
339
+ self.end_headers()
340
+ self.wfile.write(body)
341
+
342
+ def log_message(self, *args): # silence default stderr logging
343
+ return
344
+
345
+ server = HTTPServer(("127.0.0.1", port), _CallbackRequestHandler)
346
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
347
+ thread.start()
348
+ return server
349
+
350
+
351
+ def run_oauth_login(
352
+ provider: str,
353
+ *,
354
+ overrides: Optional[Dict[str, Any]] = None,
355
+ open_browser: bool = True,
356
+ on_prompt=None,
357
+ timeout: float = 300.0,
358
+ ) -> Tuple[OAuthProviderConfig, Dict[str, Any]]:
359
+ """
360
+ Run the appropriate OAuth flow for ``provider`` and return tokens.
361
+
362
+ Args:
363
+ provider: Provider name.
364
+ overrides: Optional endpoint overrides (enables ad-hoc/self-hosted).
365
+ open_browser: Whether to auto-open the browser.
366
+ on_prompt: Optional callback for displaying instructions to the user.
367
+ timeout: Max seconds to wait for authorization.
368
+
369
+ Returns:
370
+ Tuple of (resolved config, credential kwargs ready for the store).
371
+
372
+ Raises:
373
+ ValueError: If the provider does not support OAuth.
374
+ RuntimeError: On flow failure/timeout.
375
+ """
376
+ config = get_provider_config(provider, overrides)
377
+ if config is None:
378
+ raise ValueError(f"Provider '{provider}' does not support OAuth login")
379
+
380
+ if config.flow == "device":
381
+ tokens = run_device_code_flow(
382
+ config, open_browser=open_browser, on_prompt=on_prompt, poll_timeout=timeout
383
+ )
384
+ else:
385
+ tokens = run_authcode_flow(
386
+ config, open_browser=open_browser, on_prompt=on_prompt, callback_timeout=timeout
387
+ )
388
+
389
+ return config, tokens
@@ -0,0 +1,224 @@
1
+ """
2
+ Configuration paths for PraisonAI CLI.
3
+
4
+ Defines standard locations for configuration files.
5
+ """
6
+
7
+ import os
8
+ from pathlib import Path
9
+ from typing import List, Optional
10
+
11
+
12
+ # Directory markers that identify a PraisonAI project root.
13
+ # Order matters: the first existing marker is used as the config directory
14
+ # so that detection and config read/write stay aligned.
15
+ _PROJECT_MARKERS = (".praison", ".praisonai")
16
+ # Default config directory name used when no marker exists yet.
17
+ _DEFAULT_CONFIG_DIRNAME = ".praison"
18
+ # VCS markers used as a fallback when no project config dir is present.
19
+ _VCS_MARKERS = (".git",)
20
+
21
+
22
+ def _config_dirname_for(root: Path) -> str:
23
+ """Return the config directory name to use under ``root``.
24
+
25
+ Prefers an existing project marker directory so that a repo created with
26
+ ``.praisonai`` reads and writes config from the same directory it was
27
+ detected by, rather than silently switching to ``.praison``.
28
+ """
29
+ for marker in _PROJECT_MARKERS:
30
+ if (root / marker).is_dir():
31
+ return marker
32
+ return _DEFAULT_CONFIG_DIRNAME
33
+
34
+
35
+ def find_project_root(start: Optional[Path] = None) -> Optional[Path]:
36
+ """Walk upward from ``start`` (or cwd) to locate the project root.
37
+
38
+ The project root is the nearest ancestor (including ``start`` itself)
39
+ that contains a project marker directory (``.praison``/``.praisonai``)
40
+ or a VCS root (``.git``). This lets the CLI behave identically from any
41
+ sub-directory of a project tree.
42
+
43
+ An explicit override may be supplied via the ``PRAISONAI_PROJECT``
44
+ environment variable, in which case that path is returned directly.
45
+
46
+ Returns:
47
+ The resolved project root, or ``None`` if no marker is found.
48
+ """
49
+ override = os.environ.get("PRAISONAI_PROJECT")
50
+ if override:
51
+ try:
52
+ override_path = Path(override).expanduser().resolve()
53
+ except (OSError, ValueError):
54
+ override_path = None
55
+ # Only honour an override that points to an existing directory;
56
+ # an invalid value falls through to normal discovery rather than
57
+ # aborting or anchoring config writes to a bogus path.
58
+ if override_path is not None and override_path.is_dir():
59
+ return override_path
60
+
61
+ try:
62
+ cur = (start or Path.cwd()).resolve()
63
+ except (OSError, ValueError):
64
+ return None
65
+
66
+ for d in (cur, *cur.parents):
67
+ if any((d / m).is_dir() for m in _PROJECT_MARKERS):
68
+ return d
69
+ if any((d / m).exists() for m in _VCS_MARKERS):
70
+ return d
71
+ return None
72
+
73
+
74
+ def get_user_config_dir() -> Path:
75
+ """Get user configuration directory (~/.praison/)."""
76
+ return Path.home() / ".praison"
77
+
78
+
79
+ def get_user_config_path() -> Path:
80
+ """Get user configuration file path (~/.praison/config.toml)."""
81
+ return get_user_config_dir() / "config.toml"
82
+
83
+
84
+ def get_project_config_dir(project_root: Optional[Path] = None) -> Path:
85
+ """Get project configuration directory (.praison/).
86
+
87
+ When no ``project_root`` is given, the root is discovered by walking up
88
+ from the current working directory via :func:`find_project_root`, so the
89
+ project config is found from any sub-directory. Falls back to cwd when no
90
+ project marker is present.
91
+ """
92
+ root = project_root or find_project_root() or Path.cwd()
93
+ return root / _config_dirname_for(root)
94
+
95
+
96
+ def get_project_config_path(project_root: Optional[Path] = None) -> Path:
97
+ """Get project configuration file path (.praison/config.toml)."""
98
+ return get_project_config_dir(project_root) / "config.toml"
99
+
100
+
101
+ def get_sessions_dir() -> Path:
102
+ """Get sessions directory (~/.praison/sessions/)."""
103
+ return get_user_config_dir() / "sessions"
104
+
105
+
106
+ def get_traces_dir() -> Path:
107
+ """Get traces directory (~/.praison/traces/)."""
108
+ return get_user_config_dir() / "traces"
109
+
110
+
111
+ def get_logs_dir() -> Path:
112
+ """Get logs directory (~/.praison/logs/)."""
113
+ return get_user_config_dir() / "logs"
114
+
115
+
116
+ def get_cache_dir() -> Path:
117
+ """Get cache directory (~/.praison/cache/)."""
118
+ return get_user_config_dir() / "cache"
119
+
120
+
121
+ def get_config_paths(project_root: Optional[Path] = None) -> List[Path]:
122
+ """
123
+ Get all configuration file paths in precedence order (highest first).
124
+
125
+ Precedence (highest first):
126
+ 1. Project configs along the ancestor chain (nearest cwd wins, then
127
+ farther ancestors up to the project root): .praison/config.toml
128
+ 2. User config: ~/.praison/config.toml
129
+
130
+ When no ``project_root`` is supplied, the chain is collected by walking
131
+ up from cwd to the detected project root so the CLI behaves identically
132
+ from any sub-directory.
133
+
134
+ Returns:
135
+ List of paths in precedence order
136
+ """
137
+ paths: List[Path] = []
138
+ seen: set = set()
139
+
140
+ if project_root is not None:
141
+ roots = [project_root]
142
+ else:
143
+ roots = _project_config_search_roots()
144
+
145
+ # Project configs (highest precedence), nearest-to-cwd first.
146
+ for root in roots:
147
+ project_config = (root / _config_dirname_for(root) / "config.toml")
148
+ if not project_config.exists():
149
+ continue
150
+ resolved = project_config.resolve()
151
+ if resolved not in seen:
152
+ seen.add(resolved)
153
+ paths.append(project_config)
154
+
155
+ # User config
156
+ user_config = get_user_config_path()
157
+ if user_config.exists() and user_config.resolve() not in seen:
158
+ paths.append(user_config)
159
+
160
+ return paths
161
+
162
+
163
+ def _project_config_search_roots() -> List[Path]:
164
+ """Return candidate roots from cwd up to the project root (nearest first)."""
165
+ try:
166
+ cur = Path.cwd().resolve()
167
+ except (OSError, ValueError):
168
+ return []
169
+
170
+ project_root = find_project_root(cur)
171
+ # No project marker anywhere above cwd: don't walk to the filesystem
172
+ # root, otherwise an unrelated ancestor's .praison/config.toml would be
173
+ # picked up with higher precedence than the user config.
174
+ if project_root is None:
175
+ return [cur]
176
+
177
+ roots: List[Path] = []
178
+ for d in (cur, *cur.parents):
179
+ roots.append(d)
180
+ if d == project_root:
181
+ break
182
+ else:
183
+ # project_root is outside the cwd ancestry (e.g. PRAISONAI_PROJECT
184
+ # override pointing elsewhere): honour it explicitly.
185
+ roots.append(project_root)
186
+ return roots
187
+
188
+
189
+ def ensure_config_dirs() -> None:
190
+ """Ensure all configuration directories exist."""
191
+ dirs = [
192
+ get_user_config_dir(),
193
+ get_sessions_dir(),
194
+ get_traces_dir(),
195
+ get_logs_dir(),
196
+ get_cache_dir(),
197
+ ]
198
+ for d in dirs:
199
+ d.mkdir(parents=True, exist_ok=True)
200
+
201
+
202
+ def get_env_prefix() -> str:
203
+ """Get environment variable prefix."""
204
+ return "PRAISONAI_"
205
+
206
+
207
+ def env_to_config_key(env_var: str) -> Optional[str]:
208
+ """
209
+ Convert environment variable name to config key.
210
+
211
+ Example: PRAISONAI_OUTPUT_FORMAT -> output.format
212
+ """
213
+ prefix = get_env_prefix()
214
+ if not env_var.startswith(prefix):
215
+ return None
216
+
217
+ key = env_var[len(prefix):].lower()
218
+ # Convert underscores to dots for nested keys
219
+ # Single underscore = dot, double underscore = single underscore
220
+ parts = key.split("__")
221
+ result_parts = []
222
+ for part in parts:
223
+ result_parts.append(part.replace("_", "."))
224
+ return "_".join(result_parts)