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,366 @@
1
+ """
2
+ Hierarchical Configuration System for PraisonAI CLI.
3
+
4
+ Provides project → user → global config precedence with JSON schema validation.
5
+ """
6
+
7
+ import os
8
+ import json
9
+ import logging
10
+ from typing import Dict, Any, Optional, List
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ # Config file names in order of precedence (highest first)
18
+ CONFIG_FILES = [
19
+ ".praison.json", # Project-local (hidden)
20
+ "praison.json", # Project-local (visible)
21
+ ]
22
+
23
+ USER_CONFIG_PATH = "~/.config/praison/praison.json"
24
+ GLOBAL_CONFIG_PATH = "/etc/praison/praison.json"
25
+
26
+ # When no git root is found, cap the upward walk to avoid scanning arbitrary
27
+ # parent directories up to the filesystem root (e.g. picking up ~/.praison.json).
28
+ # Mirrors the cap used by integration/context_files.py for consistency.
29
+ _MAX_WALK_UP_DEPTH = 10
30
+
31
+
32
+ class ConfigValidationError(Exception):
33
+ """Raised when config validation fails."""
34
+ pass
35
+
36
+
37
+ @dataclass
38
+ class ConfigSource:
39
+ """Represents a configuration source."""
40
+ path: str
41
+ level: str # "project", "user", "global"
42
+ data: Dict[str, Any]
43
+
44
+
45
+ # JSON Schema for configuration validation
46
+ CONFIG_SCHEMA = {
47
+ "type": "object",
48
+ "properties": {
49
+ "model": {"type": "string"},
50
+ "temperature": {"type": "number", "minimum": 0, "maximum": 2},
51
+ "max_tokens": {"type": "integer", "minimum": 1},
52
+ "providers": {
53
+ "type": "object",
54
+ "additionalProperties": {
55
+ "type": "object",
56
+ "properties": {
57
+ "api_key": {"type": "string"},
58
+ "base_url": {"type": "string"},
59
+ "model": {"type": "string"},
60
+ }
61
+ }
62
+ },
63
+ "mcp": {
64
+ "type": "object",
65
+ "additionalProperties": {
66
+ "type": "object",
67
+ "properties": {
68
+ "command": {"type": "string"},
69
+ "args": {"type": "array", "items": {"type": "string"}},
70
+ "env": {"type": "object"},
71
+ "disabled_tools": {"type": "array", "items": {"type": "string"}},
72
+ }
73
+ }
74
+ },
75
+ "permissions": {
76
+ "type": "object",
77
+ "properties": {
78
+ "allowed_tools": {"type": "array", "items": {"type": "string"}},
79
+ "allowed_paths": {"type": "array", "items": {"type": "string"}},
80
+ "auto_approve": {"type": "boolean"},
81
+ }
82
+ },
83
+ "lsp": {
84
+ "type": "object",
85
+ "additionalProperties": {
86
+ "type": "object",
87
+ "properties": {
88
+ "command": {"type": "string"},
89
+ "args": {"type": "array", "items": {"type": "string"}},
90
+ }
91
+ }
92
+ },
93
+ "output": {
94
+ "type": "object",
95
+ "properties": {
96
+ "mode": {"type": "string", "enum": ["compact", "verbose", "quiet"]},
97
+ "color": {"type": "boolean"},
98
+ }
99
+ },
100
+ "attribution": {
101
+ "type": "object",
102
+ "properties": {
103
+ "style": {"type": "string", "enum": ["assisted-by", "co-authored-by", "none"]},
104
+ "include_model": {"type": "boolean"},
105
+ }
106
+ },
107
+ }
108
+ }
109
+
110
+
111
+ def validate_config(config: Dict[str, Any], schema: Dict = CONFIG_SCHEMA) -> List[str]:
112
+ """
113
+ Validate config against schema. Returns list of validation errors.
114
+
115
+ Simple validation without external dependencies.
116
+ """
117
+ errors = []
118
+
119
+ def validate_value(value: Any, schema_part: Dict, path: str) -> None:
120
+ expected_type = schema_part.get("type")
121
+
122
+ if expected_type == "string" and not isinstance(value, str):
123
+ errors.append(f"{path}: expected string, got {type(value).__name__}")
124
+ elif expected_type == "number" and not isinstance(value, (int, float)):
125
+ errors.append(f"{path}: expected number, got {type(value).__name__}")
126
+ elif expected_type == "integer" and not isinstance(value, int):
127
+ errors.append(f"{path}: expected integer, got {type(value).__name__}")
128
+ elif expected_type == "boolean" and not isinstance(value, bool):
129
+ errors.append(f"{path}: expected boolean, got {type(value).__name__}")
130
+ elif expected_type == "array" and not isinstance(value, list):
131
+ errors.append(f"{path}: expected array, got {type(value).__name__}")
132
+ elif expected_type == "object" and not isinstance(value, dict):
133
+ errors.append(f"{path}: expected object, got {type(value).__name__}")
134
+
135
+ # Check enum
136
+ if "enum" in schema_part and value not in schema_part["enum"]:
137
+ errors.append(f"{path}: value must be one of {schema_part['enum']}")
138
+
139
+ # Check minimum/maximum for numbers
140
+ if isinstance(value, (int, float)):
141
+ if "minimum" in schema_part and value < schema_part["minimum"]:
142
+ errors.append(f"{path}: value must be >= {schema_part['minimum']}")
143
+ if "maximum" in schema_part and value > schema_part["maximum"]:
144
+ errors.append(f"{path}: value must be <= {schema_part['maximum']}")
145
+
146
+ # Validate top-level properties
147
+ properties = schema.get("properties", {})
148
+ for key, value in config.items():
149
+ if key in properties:
150
+ validate_value(value, properties[key], key)
151
+
152
+ return errors
153
+
154
+
155
+ class HierarchicalConfig:
156
+ """
157
+ Hierarchical configuration manager.
158
+
159
+ Loads configuration from multiple sources with precedence:
160
+ 1. Project-level (.praison.json or praison.json in current dir)
161
+ 2. User-level (~/.config/praison/praison.json)
162
+ 3. Global-level (/etc/praison/praison.json)
163
+
164
+ Higher precedence configs override lower ones.
165
+
166
+ Usage:
167
+ config = HierarchicalConfig()
168
+ settings = config.load()
169
+
170
+ # Access settings
171
+ model = settings.get("model", "gpt-4o-mini")
172
+ """
173
+
174
+ def __init__(
175
+ self,
176
+ project_dir: Optional[str] = None,
177
+ user_config: Optional[str] = None,
178
+ global_config: Optional[str] = None,
179
+ walk_up: bool = True,
180
+ ):
181
+ self.project_dir = project_dir or os.getcwd()
182
+ self.user_config = os.path.expanduser(user_config or USER_CONFIG_PATH)
183
+ self.global_config = global_config or GLOBAL_CONFIG_PATH
184
+ self.walk_up = walk_up
185
+ self._sources: List[ConfigSource] = []
186
+ self._merged: Dict[str, Any] = {}
187
+
188
+ @property
189
+ def precedence(self) -> List[str]:
190
+ """Return config precedence order."""
191
+ return ["project", "user", "global"]
192
+
193
+ def _find_project_config(self) -> Optional[str]:
194
+ """
195
+ Find the project-level config file.
196
+
197
+ When ``walk_up`` is enabled (default), the search starts in
198
+ ``project_dir`` and walks upward through parent directories until a
199
+ config file is found or the filesystem root is reached. This mirrors
200
+ the git-root project scoping used by session storage, so running the
201
+ CLI from a subdirectory still discovers the project's config at its
202
+ root. The walk stops at a directory containing a ``.git`` marker
203
+ (inclusive) so configuration never leaks across project boundaries.
204
+ When no ``.git`` boundary exists, the walk is capped at
205
+ ``_MAX_WALK_UP_DEPTH`` levels to avoid picking up unrelated ancestor
206
+ configs (e.g. ``~/.praison.json``).
207
+ """
208
+ start = Path(self.project_dir).resolve()
209
+
210
+ if not self.walk_up:
211
+ for filename in CONFIG_FILES:
212
+ path = start / filename
213
+ if path.exists():
214
+ return str(path)
215
+ return None
216
+
217
+ current = start
218
+ depth = 0
219
+ while True:
220
+ for filename in CONFIG_FILES:
221
+ path = current / filename
222
+ if path.exists():
223
+ return str(path)
224
+
225
+ # Stop at the project boundary (git root) to avoid leaking config
226
+ # from unrelated parent projects.
227
+ if (current / ".git").exists():
228
+ return None
229
+
230
+ parent = current.parent
231
+ if parent == current: # Reached filesystem root.
232
+ return None
233
+
234
+ # No git boundary found yet: cap the walk to avoid scanning
235
+ # arbitrary parent directories up to the filesystem root.
236
+ if depth >= _MAX_WALK_UP_DEPTH:
237
+ return None
238
+
239
+ current = parent
240
+ depth += 1
241
+
242
+ def _load_json(self, path: str) -> Optional[Dict[str, Any]]:
243
+ """Load JSON file, return None if not found or invalid."""
244
+ if not os.path.exists(path):
245
+ return None
246
+
247
+ try:
248
+ with open(path, "r") as f:
249
+ return json.load(f)
250
+ except json.JSONDecodeError as e:
251
+ logger.warning(f"Invalid JSON in {path}: {e}")
252
+ return None
253
+ except Exception as e:
254
+ logger.warning(f"Failed to load {path}: {e}")
255
+ return None
256
+
257
+ def _deep_merge(self, base: Dict, override: Dict) -> Dict:
258
+ """Deep merge two dicts, with override taking precedence."""
259
+ result = base.copy()
260
+
261
+ for key, value in override.items():
262
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
263
+ result[key] = self._deep_merge(result[key], value)
264
+ else:
265
+ result[key] = value
266
+
267
+ return result
268
+
269
+ def load(self, validate: bool = False) -> Dict[str, Any]:
270
+ """
271
+ Load and merge configuration from all sources.
272
+
273
+ Args:
274
+ validate: If True, validate config against schema
275
+
276
+ Returns:
277
+ Merged configuration dict
278
+
279
+ Raises:
280
+ ConfigValidationError: If validate=True and validation fails
281
+ """
282
+ self._sources = []
283
+ self._merged = {}
284
+
285
+ # Load in reverse precedence order (global first, project last)
286
+ # Global config
287
+ global_data = self._load_json(self.global_config)
288
+ if global_data:
289
+ self._sources.append(ConfigSource(
290
+ path=self.global_config,
291
+ level="global",
292
+ data=global_data
293
+ ))
294
+ self._merged = self._deep_merge(self._merged, global_data)
295
+
296
+ # User config
297
+ user_data = self._load_json(self.user_config)
298
+ if user_data:
299
+ self._sources.append(ConfigSource(
300
+ path=self.user_config,
301
+ level="user",
302
+ data=user_data
303
+ ))
304
+ self._merged = self._deep_merge(self._merged, user_data)
305
+
306
+ # Project config
307
+ project_path = self._find_project_config()
308
+ if project_path:
309
+ project_data = self._load_json(project_path)
310
+ if project_data:
311
+ self._sources.append(ConfigSource(
312
+ path=project_path,
313
+ level="project",
314
+ data=project_data
315
+ ))
316
+ self._merged = self._deep_merge(self._merged, project_data)
317
+
318
+ # Validate if requested
319
+ if validate:
320
+ errors = validate_config(self._merged)
321
+ if errors:
322
+ raise ConfigValidationError(
323
+ f"Configuration validation failed:\n" + "\n".join(f" - {e}" for e in errors)
324
+ )
325
+
326
+ return self._merged
327
+
328
+ def get(self, key: str, default: Any = None) -> Any:
329
+ """Get a config value."""
330
+ if not self._merged:
331
+ self.load()
332
+ return self._merged.get(key, default)
333
+
334
+ def get_sources(self) -> List[ConfigSource]:
335
+ """Get list of loaded config sources."""
336
+ return self._sources
337
+
338
+ def save_user_config(self, config: Dict[str, Any]) -> None:
339
+ """Save configuration to user config file."""
340
+ os.makedirs(os.path.dirname(self.user_config), exist_ok=True)
341
+ with open(self.user_config, "w") as f:
342
+ json.dump(config, f, indent=2)
343
+
344
+ def save_project_config(self, config: Dict[str, Any], hidden: bool = True) -> None:
345
+ """Save configuration to project config file."""
346
+ filename = ".praison.json" if hidden else "praison.json"
347
+ path = os.path.join(self.project_dir, filename)
348
+ with open(path, "w") as f:
349
+ json.dump(config, f, indent=2)
350
+
351
+
352
+ # Global config instance
353
+ _config: Optional[HierarchicalConfig] = None
354
+
355
+
356
+ def get_config() -> HierarchicalConfig:
357
+ """Get the global config instance."""
358
+ global _config
359
+ if _config is None:
360
+ _config = HierarchicalConfig()
361
+ return _config
362
+
363
+
364
+ def load_config() -> Dict[str, Any]:
365
+ """Load and return the merged configuration."""
366
+ return get_config().load()