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,364 @@
1
+ """
2
+ Configuration loader for PraisonAI CLI.
3
+
4
+ Handles loading, merging, and saving configuration with precedence.
5
+ """
6
+
7
+ import os
8
+ import threading
9
+ from pathlib import Path
10
+ from typing import Any, Dict, Optional
11
+
12
+ from .paths import (
13
+ get_config_paths,
14
+ get_user_config_path,
15
+ get_project_config_path,
16
+ get_env_prefix,
17
+ ensure_config_dirs,
18
+ find_project_root,
19
+ )
20
+ from .schema import ConfigSchema, DEFAULT_CONFIG
21
+
22
+
23
+ # Thread-safe config cache
24
+ _config_lock = threading.Lock()
25
+ _config_cache: Optional[ConfigSchema] = None
26
+
27
+
28
+ def _load_toml(path: Path) -> Dict[str, Any]:
29
+ """Load a TOML file."""
30
+ try:
31
+ # Try tomllib (Python 3.11+) first
32
+ import tomllib
33
+ with open(path, "rb") as f:
34
+ return tomllib.load(f)
35
+ except ImportError:
36
+ pass
37
+
38
+ try:
39
+ # Fall back to tomli
40
+ import tomli
41
+ with open(path, "rb") as f:
42
+ return tomli.load(f)
43
+ except ImportError:
44
+ pass
45
+
46
+ # Last resort: basic TOML parsing
47
+ return _basic_toml_parse(path)
48
+
49
+
50
+ def _basic_toml_parse(path: Path) -> Dict[str, Any]:
51
+ """Basic TOML parser for simple configs."""
52
+ result: Dict[str, Any] = {}
53
+ current_section: Optional[str] = None
54
+
55
+ with open(path, "r", encoding="utf-8") as f:
56
+ for line in f:
57
+ line = line.strip()
58
+ if not line or line.startswith("#"):
59
+ continue
60
+
61
+ if line.startswith("[") and line.endswith("]"):
62
+ current_section = line[1:-1]
63
+ if current_section not in result:
64
+ result[current_section] = {}
65
+ elif "=" in line:
66
+ key, value = line.split("=", 1)
67
+ key = key.strip()
68
+ value = value.strip()
69
+
70
+ # Parse value
71
+ if value.lower() == "true":
72
+ parsed_value = True
73
+ elif value.lower() == "false":
74
+ parsed_value = False
75
+ elif value.startswith('"') and value.endswith('"'):
76
+ parsed_value = value[1:-1]
77
+ elif value.startswith("'") and value.endswith("'"):
78
+ parsed_value = value[1:-1]
79
+ else:
80
+ try:
81
+ parsed_value = int(value)
82
+ except ValueError:
83
+ try:
84
+ parsed_value = float(value)
85
+ except ValueError:
86
+ parsed_value = value
87
+
88
+ if current_section:
89
+ result[current_section][key] = parsed_value
90
+ else:
91
+ result[key] = parsed_value
92
+
93
+ return result
94
+
95
+
96
+ def _save_toml(path: Path, data: Dict[str, Any]) -> None:
97
+ """Save data to a TOML file."""
98
+ try:
99
+ import tomli_w
100
+ with open(path, "wb") as f:
101
+ tomli_w.dump(data, f)
102
+ return
103
+ except ImportError:
104
+ pass
105
+
106
+ # Fall back to basic TOML writing
107
+ _basic_toml_write(path, data)
108
+
109
+
110
+ def _basic_toml_write(path: Path, data: Dict[str, Any]) -> None:
111
+ """Basic TOML writer."""
112
+ lines = []
113
+
114
+ # Write top-level keys first
115
+ for key, value in data.items():
116
+ if not isinstance(value, dict):
117
+ lines.append(f"{key} = {_format_toml_value(value)}")
118
+
119
+ # Write sections
120
+ for key, value in data.items():
121
+ if isinstance(value, dict):
122
+ lines.append(f"\n[{key}]")
123
+ for sub_key, sub_value in value.items():
124
+ if not isinstance(sub_value, dict):
125
+ lines.append(f"{sub_key} = {_format_toml_value(sub_value)}")
126
+ else:
127
+ # Nested section
128
+ lines.append(f"\n[{key}.{sub_key}]")
129
+ for nested_key, nested_value in sub_value.items():
130
+ lines.append(f"{nested_key} = {_format_toml_value(nested_value)}")
131
+
132
+ path.parent.mkdir(parents=True, exist_ok=True)
133
+ with open(path, "w", encoding="utf-8") as f:
134
+ f.write("\n".join(lines) + "\n")
135
+
136
+
137
+ def _format_toml_value(value: Any) -> str:
138
+ """Format a value for TOML."""
139
+ if isinstance(value, bool):
140
+ return "true" if value else "false"
141
+ elif isinstance(value, str):
142
+ return f'"{value}"'
143
+ elif isinstance(value, (int, float)):
144
+ return str(value)
145
+ elif isinstance(value, list):
146
+ items = ", ".join(_format_toml_value(v) for v in value)
147
+ return f"[{items}]"
148
+ else:
149
+ return f'"{value}"'
150
+
151
+
152
+ def _merge_dicts(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
153
+ """Deep merge two dictionaries."""
154
+ result = base.copy()
155
+ for key, value in override.items():
156
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
157
+ result[key] = _merge_dicts(result[key], value)
158
+ else:
159
+ result[key] = value
160
+ return result
161
+
162
+
163
+ def _get_env_overrides() -> Dict[str, Any]:
164
+ """Get configuration overrides from environment variables."""
165
+ prefix = get_env_prefix()
166
+ overrides: Dict[str, Any] = {}
167
+
168
+ for key, value in os.environ.items():
169
+ if not key.startswith(prefix):
170
+ continue
171
+
172
+ # Convert PRAISONAI_OUTPUT_FORMAT to output.format
173
+ config_key = key[len(prefix):].lower()
174
+ parts = config_key.split("_")
175
+
176
+ # Parse value
177
+ if value.lower() == "true":
178
+ parsed_value = True
179
+ elif value.lower() == "false":
180
+ parsed_value = False
181
+ else:
182
+ try:
183
+ parsed_value = int(value)
184
+ except ValueError:
185
+ try:
186
+ parsed_value = float(value)
187
+ except ValueError:
188
+ parsed_value = value
189
+
190
+ # Build nested dict
191
+ if len(parts) >= 2:
192
+ section = parts[0]
193
+ sub_key = "_".join(parts[1:])
194
+ if section not in overrides:
195
+ overrides[section] = {}
196
+ overrides[section][sub_key] = parsed_value
197
+ else:
198
+ overrides[config_key] = parsed_value
199
+
200
+ return overrides
201
+
202
+
203
+ def _get_dotted_value(data: Dict[str, Any], key: str) -> Any:
204
+ """Get a value using dotted key notation."""
205
+ parts = key.split(".")
206
+ current = data
207
+ for part in parts:
208
+ if isinstance(current, dict) and part in current:
209
+ current = current[part]
210
+ else:
211
+ return None
212
+ return current
213
+
214
+
215
+ def _set_dotted_value(data: Dict[str, Any], key: str, value: Any) -> None:
216
+ """Set a value using dotted key notation."""
217
+ parts = key.split(".")
218
+ current = data
219
+ for part in parts[:-1]:
220
+ if part not in current:
221
+ current[part] = {}
222
+ current = current[part]
223
+ current[parts[-1]] = value
224
+
225
+
226
+ class ConfigLoader:
227
+ """
228
+ Configuration loader with precedence handling.
229
+
230
+ Precedence (highest to lowest):
231
+ 1. CLI flags (not handled here)
232
+ 2. Environment variables (PRAISONAI_*)
233
+ 3. Project config (.praison/config.toml)
234
+ 4. User config (~/.praison/config.toml)
235
+ 5. Defaults
236
+ """
237
+
238
+ def __init__(self, project_root: Optional[Path] = None):
239
+ # Remember whether the caller pinned an explicit root. When they did,
240
+ # config resolution stays anchored to that single root; otherwise we
241
+ # let get_config_paths() walk the cwd→project-root ancestor chain so
242
+ # nested .praison/config.toml files are merged (nearest wins).
243
+ self._explicit_root = project_root
244
+ self.project_root = project_root or find_project_root() or Path.cwd()
245
+ self._config: Optional[ConfigSchema] = None
246
+ self._raw_config: Dict[str, Any] = {}
247
+
248
+ def load(self, force_reload: bool = False) -> ConfigSchema:
249
+ """Load configuration with precedence."""
250
+ if self._config is not None and not force_reload:
251
+ return self._config
252
+
253
+ # Start with defaults
254
+ merged = DEFAULT_CONFIG.to_dict()
255
+
256
+ # Load config files in reverse precedence order
257
+ config_paths = get_config_paths(self._explicit_root)
258
+ for path in reversed(config_paths):
259
+ try:
260
+ file_config = _load_toml(path)
261
+ merged = _merge_dicts(merged, file_config)
262
+ except Exception:
263
+ pass # Skip invalid files
264
+
265
+ # Apply environment overrides
266
+ env_overrides = _get_env_overrides()
267
+ merged = _merge_dicts(merged, env_overrides)
268
+
269
+ self._raw_config = merged
270
+ self._config = ConfigSchema.from_dict(merged)
271
+ return self._config
272
+
273
+ def get(self, key: str, default: Any = None) -> Any:
274
+ """Get a configuration value using dotted key notation."""
275
+ self.load()
276
+ value = _get_dotted_value(self._raw_config, key)
277
+ return value if value is not None else default
278
+
279
+ def set(self, key: str, value: Any, scope: str = "user") -> None:
280
+ """
281
+ Set a configuration value.
282
+
283
+ Args:
284
+ key: Dotted key notation (e.g., "output.format")
285
+ value: Value to set
286
+ scope: "user" or "project"
287
+ """
288
+ if scope == "project":
289
+ config_path = get_project_config_path(self.project_root)
290
+ else:
291
+ config_path = get_user_config_path()
292
+
293
+ # Load existing config or create empty
294
+ if config_path.exists():
295
+ try:
296
+ existing = _load_toml(config_path)
297
+ except Exception:
298
+ existing = {}
299
+ else:
300
+ existing = {}
301
+
302
+ # Set the value
303
+ _set_dotted_value(existing, key, value)
304
+
305
+ # Ensure directory exists
306
+ ensure_config_dirs()
307
+ if scope == "project":
308
+ config_path.parent.mkdir(parents=True, exist_ok=True)
309
+
310
+ # Save
311
+ _save_toml(config_path, existing)
312
+
313
+ # Reload
314
+ self.load(force_reload=True)
315
+
316
+ def reset(self, scope: str = "user") -> None:
317
+ """Reset configuration to defaults."""
318
+ if scope == "project":
319
+ config_path = get_project_config_path(self.project_root)
320
+ else:
321
+ config_path = get_user_config_path()
322
+
323
+ if config_path.exists():
324
+ config_path.unlink()
325
+
326
+ self.load(force_reload=True)
327
+
328
+ def list_all(self) -> Dict[str, Any]:
329
+ """List all configuration values."""
330
+ self.load()
331
+ return self._raw_config.copy()
332
+
333
+
334
+ # Global config loader
335
+ _global_loader: Optional[ConfigLoader] = None
336
+
337
+
338
+ def get_config(project_root: Optional[Path] = None) -> ConfigSchema:
339
+ """Get the global configuration."""
340
+ global _global_loader, _config_cache
341
+
342
+ with _config_lock:
343
+ if _global_loader is None or project_root is not None:
344
+ _global_loader = ConfigLoader(project_root)
345
+
346
+ if _config_cache is None:
347
+ _config_cache = _global_loader.load()
348
+
349
+ return _config_cache
350
+
351
+
352
+ def load_config(project_root: Optional[Path] = None) -> ConfigSchema:
353
+ """Alias for get_config(); loads the global configuration."""
354
+ return get_config(project_root)
355
+
356
+
357
+ def get_config_loader(project_root: Optional[Path] = None) -> ConfigLoader:
358
+ """Get the global config loader."""
359
+ global _global_loader
360
+
361
+ with _config_lock:
362
+ if _global_loader is None or project_root is not None:
363
+ _global_loader = ConfigLoader(project_root)
364
+ return _global_loader
@@ -0,0 +1,161 @@
1
+ """
2
+ Provider-aware default model resolution with last-used recency.
3
+
4
+ Resolves a sensible default model for zero-config first runs in this
5
+ precedence:
6
+
7
+ 1. Explicit value (``--model`` / config / YAML) — handled by the caller.
8
+ 2. Most-recently-used model, persisted under ``~/.praison/state/model.json``.
9
+ 3. Best available model inferred from which provider credentials are present
10
+ (delegated to :func:`default_model_for_available_provider`).
11
+
12
+ This is a wrapper/first-run concern: recency state, credential scanning and the
13
+ one-line CLI notice live here, while core only owns a small provider-aware
14
+ helper that maps present credentials to a default.
15
+ """
16
+
17
+ import json
18
+ import os
19
+ from pathlib import Path
20
+ from typing import Optional
21
+
22
+ from .paths import get_user_config_dir
23
+
24
+
25
+ def _state_dir() -> Path:
26
+ """Return the user state directory (``~/.praison/state``)."""
27
+ return get_user_config_dir() / "state"
28
+
29
+
30
+ def _model_state_path() -> Path:
31
+ """Return the recency file path (``~/.praison/state/model.json``)."""
32
+ return _state_dir() / "model.json"
33
+
34
+
35
+ def get_recent_model() -> Optional[str]:
36
+ """Return the most-recently-used model, or ``None`` if not recorded."""
37
+ try:
38
+ path = _model_state_path()
39
+ if not path.exists():
40
+ return None
41
+ data = json.loads(path.read_text(encoding="utf-8"))
42
+ model = data.get("model")
43
+ return model if isinstance(model, str) and model else None
44
+ except Exception:
45
+ # Never let a corrupt/unreadable state file break model resolution.
46
+ return None
47
+
48
+
49
+ def set_recent_model(model: Optional[str]) -> bool:
50
+ """Persist ``model`` as the most-recently-used model.
51
+
52
+ Returns ``True`` on success, ``False`` if persistence failed (e.g. a
53
+ read-only home directory). Failure is non-fatal — the model still works
54
+ for the current run, it just won't be remembered.
55
+ """
56
+ if not model:
57
+ return False
58
+ try:
59
+ state_dir = _state_dir()
60
+ state_dir.mkdir(parents=True, exist_ok=True)
61
+ _model_state_path().write_text(
62
+ json.dumps({"model": model}), encoding="utf-8"
63
+ )
64
+ return True
65
+ except Exception:
66
+ return False
67
+
68
+
69
+ def _provider_for_model(model: str) -> Optional[str]:
70
+ """Best-effort map a model id to the credential env var that explains it."""
71
+ m = model.lower()
72
+ if m.startswith("anthropic/") or m.startswith("claude"):
73
+ return "ANTHROPIC_API_KEY"
74
+ if m.startswith("gemini/") or m.startswith("gemini"):
75
+ return "GEMINI_API_KEY"
76
+ if m.startswith("google/"):
77
+ return "GOOGLE_API_KEY"
78
+ if m.startswith("groq/"):
79
+ return "GROQ_API_KEY"
80
+ if m.startswith("cohere/"):
81
+ return "COHERE_API_KEY"
82
+ if m.startswith("ollama/"):
83
+ return "OLLAMA_HOST"
84
+ if (
85
+ m.startswith("gpt")
86
+ or m.startswith("o1")
87
+ or m.startswith("o3")
88
+ or m.startswith("o4")
89
+ or m.startswith("openai/")
90
+ ):
91
+ return "OPENAI_API_KEY"
92
+ return None
93
+
94
+
95
+ def resolve_default_model(
96
+ explicit: Optional[str] = None,
97
+ *,
98
+ persist: bool = True,
99
+ notify: bool = True,
100
+ ) -> str:
101
+ """Resolve the default model for a zero-config run.
102
+
103
+ Precedence: ``explicit`` > recent (persisted) > ``MODEL_NAME`` /
104
+ ``OPENAI_MODEL_NAME`` > provider-aware best > ``gpt-4o-mini``.
105
+
106
+ Only *user-chosen* models (``explicit`` and the env overrides) are
107
+ persisted as the recent model. Provider-*inferred* defaults and the
108
+ ``gpt-4o-mini`` fallback are intentionally **not** persisted: persisting
109
+ them would let a stale inferred default short-circuit credential-based
110
+ inference on a later run after the user's available providers have changed.
111
+
112
+ Args:
113
+ explicit: An explicitly configured model (``--model``/config/YAML).
114
+ When given it wins and is persisted as the recent model.
115
+ persist: Whether to remember user-chosen models for next time.
116
+ notify: Whether to emit a one-line transparency notice the first time a
117
+ provider-aware default is inferred (no explicit, no recent model).
118
+
119
+ Returns:
120
+ The resolved model id.
121
+ """
122
+ if explicit:
123
+ if persist:
124
+ set_recent_model(explicit)
125
+ return explicit
126
+
127
+ recent = get_recent_model()
128
+ if recent:
129
+ return recent
130
+
131
+ # Honour OPENAI_MODEL_NAME / MODEL_NAME for backward compatibility before
132
+ # falling back to credential-based inference.
133
+ env_model = os.environ.get("MODEL_NAME") or os.environ.get("OPENAI_MODEL_NAME")
134
+ if env_model:
135
+ if persist:
136
+ set_recent_model(env_model)
137
+ return env_model
138
+
139
+ try:
140
+ from ...llm.env import default_model_for_available_provider
141
+ model = default_model_for_available_provider()
142
+ except Exception:
143
+ model = "gpt-4o-mini"
144
+
145
+ if notify:
146
+ provider = _provider_for_model(model)
147
+ if provider and os.environ.get(provider):
148
+ try:
149
+ import typer
150
+ typer.echo(
151
+ f"No model set; using {model} because {provider} is present."
152
+ )
153
+ except Exception:
154
+ pass
155
+
156
+ # NOTE: provider-inferred defaults and the gpt-4o-mini fallback are
157
+ # deliberately NOT persisted. Persisting them would let a stale inferred
158
+ # default win over fresh credential-based inference on a later run when the
159
+ # user's available providers have changed, re-introducing the very
160
+ # zero-config failure this resolver exists to prevent.
161
+ return model