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.
- praisonai_code/__init__.py +17 -0
- praisonai_code/cli/__init__.py +12 -0
- praisonai_code/cli/_forward_shim.py +10 -0
- praisonai_code/cli/_paths.py +88 -0
- praisonai_code/cli/_warnings.py +58 -0
- praisonai_code/cli/app.py +757 -0
- praisonai_code/cli/approval_backend.py +272 -0
- praisonai_code/cli/branding.py +94 -0
- praisonai_code/cli/commands/__init__.py +114 -0
- praisonai_code/cli/commands/acp.py +80 -0
- praisonai_code/cli/commands/agent.py +116 -0
- praisonai_code/cli/commands/agents.py +80 -0
- praisonai_code/cli/commands/app.py +139 -0
- praisonai_code/cli/commands/attach.py +95 -0
- praisonai_code/cli/commands/audit.py +102 -0
- praisonai_code/cli/commands/auth.py +508 -0
- praisonai_code/cli/commands/batch.py +848 -0
- praisonai_code/cli/commands/benchmark.py +286 -0
- praisonai_code/cli/commands/browser.py +299 -0
- praisonai_code/cli/commands/call.py +45 -0
- praisonai_code/cli/commands/chat.py +332 -0
- praisonai_code/cli/commands/checkpoint.py +170 -0
- praisonai_code/cli/commands/code.py +276 -0
- praisonai_code/cli/commands/command.py +114 -0
- praisonai_code/cli/commands/commit.py +47 -0
- praisonai_code/cli/commands/completion.py +333 -0
- praisonai_code/cli/commands/config.py +681 -0
- praisonai_code/cli/commands/context.py +414 -0
- praisonai_code/cli/commands/daemon.py +203 -0
- praisonai_code/cli/commands/debug.py +142 -0
- praisonai_code/cli/commands/deploy.py +71 -0
- praisonai_code/cli/commands/diag.py +55 -0
- praisonai_code/cli/commands/docs.py +1575 -0
- praisonai_code/cli/commands/doctor.py +332 -0
- praisonai_code/cli/commands/endpoints.py +51 -0
- praisonai_code/cli/commands/environment.py +179 -0
- praisonai_code/cli/commands/eval.py +131 -0
- praisonai_code/cli/commands/examples.py +953 -0
- praisonai_code/cli/commands/flow.py +436 -0
- praisonai_code/cli/commands/github.py +752 -0
- praisonai_code/cli/commands/hooks.py +74 -0
- praisonai_code/cli/commands/init.py +174 -0
- praisonai_code/cli/commands/knowledge.py +440 -0
- praisonai_code/cli/commands/langextract.py +120 -0
- praisonai_code/cli/commands/langfuse.py +984 -0
- praisonai_code/cli/commands/loop.py +211 -0
- praisonai_code/cli/commands/lsp.py +112 -0
- praisonai_code/cli/commands/managed.py +659 -0
- praisonai_code/cli/commands/mcp.py +763 -0
- praisonai_code/cli/commands/memory.py +298 -0
- praisonai_code/cli/commands/models.py +264 -0
- praisonai_code/cli/commands/n8n.py +326 -0
- praisonai_code/cli/commands/obs.py +19 -0
- praisonai_code/cli/commands/package.py +76 -0
- praisonai_code/cli/commands/paths.py +106 -0
- praisonai_code/cli/commands/permissions.py +272 -0
- praisonai_code/cli/commands/plugins.py +609 -0
- praisonai_code/cli/commands/port.py +530 -0
- praisonai_code/cli/commands/profile.py +466 -0
- praisonai_code/cli/commands/publish.py +193 -0
- praisonai_code/cli/commands/rag.py +913 -0
- praisonai_code/cli/commands/realtime.py +52 -0
- praisonai_code/cli/commands/recipe.py +684 -0
- praisonai_code/cli/commands/registry.py +59 -0
- praisonai_code/cli/commands/replay.py +830 -0
- praisonai_code/cli/commands/research.py +49 -0
- praisonai_code/cli/commands/retrieval.py +377 -0
- praisonai_code/cli/commands/rules.py +71 -0
- praisonai_code/cli/commands/run.py +1573 -0
- praisonai_code/cli/commands/sandbox.py +371 -0
- praisonai_code/cli/commands/schedule.py +529 -0
- praisonai_code/cli/commands/serve.py +690 -0
- praisonai_code/cli/commands/session.py +450 -0
- praisonai_code/cli/commands/setup.py +174 -0
- praisonai_code/cli/commands/skills.py +545 -0
- praisonai_code/cli/commands/standardise.py +711 -0
- praisonai_code/cli/commands/templates.py +54 -0
- praisonai_code/cli/commands/test.py +558 -0
- praisonai_code/cli/commands/todo.py +74 -0
- praisonai_code/cli/commands/tools.py +205 -0
- praisonai_code/cli/commands/traces.py +145 -0
- praisonai_code/cli/commands/tracker.py +852 -0
- praisonai_code/cli/commands/train.py +613 -0
- praisonai_code/cli/commands/ui.py +172 -0
- praisonai_code/cli/commands/up.py +354 -0
- praisonai_code/cli/commands/validate.py +291 -0
- praisonai_code/cli/commands/version.py +101 -0
- praisonai_code/cli/commands/workflow.py +97 -0
- praisonai_code/cli/config_loader.py +437 -0
- praisonai_code/cli/configuration/__init__.py +27 -0
- praisonai_code/cli/configuration/config.schema.json +57 -0
- praisonai_code/cli/configuration/credentials.py +446 -0
- praisonai_code/cli/configuration/loader.py +364 -0
- praisonai_code/cli/configuration/model_resolver.py +161 -0
- praisonai_code/cli/configuration/oauth.py +389 -0
- praisonai_code/cli/configuration/paths.py +224 -0
- praisonai_code/cli/configuration/resolver.py +687 -0
- praisonai_code/cli/configuration/schema.py +317 -0
- praisonai_code/cli/execution/__init__.py +99 -0
- praisonai_code/cli/execution/core.py +208 -0
- praisonai_code/cli/execution/profiler.py +898 -0
- praisonai_code/cli/execution/request.py +85 -0
- praisonai_code/cli/execution/result.py +74 -0
- praisonai_code/cli/fallback_schema.py +416 -0
- praisonai_code/cli/features/__init__.py +278 -0
- praisonai_code/cli/features/_endpoint_registry.py +64 -0
- praisonai_code/cli/features/_search_registry.py +43 -0
- praisonai_code/cli/features/acp.py +236 -0
- praisonai_code/cli/features/action_orchestrator.py +576 -0
- praisonai_code/cli/features/agent_scheduler.py +773 -0
- praisonai_code/cli/features/agent_tools.py +603 -0
- praisonai_code/cli/features/agents.py +397 -0
- praisonai_code/cli/features/at_mentions.py +471 -0
- praisonai_code/cli/features/audit_cli.py +270 -0
- praisonai_code/cli/features/auto_memory.py +182 -0
- praisonai_code/cli/features/auto_mode.py +552 -0
- praisonai_code/cli/features/autonomy_mode.py +546 -0
- praisonai_code/cli/features/background.py +356 -0
- praisonai_code/cli/features/base.py +168 -0
- praisonai_code/cli/features/benchmark.py +1462 -0
- praisonai_code/cli/features/capabilities.py +1326 -0
- praisonai_code/cli/features/checkpoints.py +345 -0
- praisonai_code/cli/features/cli_profiler.py +335 -0
- praisonai_code/cli/features/code_intelligence.py +666 -0
- praisonai_code/cli/features/compaction.py +294 -0
- praisonai_code/cli/features/compare.py +534 -0
- praisonai_code/cli/features/config_hierarchy.py +366 -0
- praisonai_code/cli/features/context_manager.py +597 -0
- praisonai_code/cli/features/cost_tracker.py +514 -0
- praisonai_code/cli/features/csv_test_runner.py +736 -0
- praisonai_code/cli/features/custom_definitions.py +790 -0
- praisonai_code/cli/features/debug.py +810 -0
- praisonai_code/cli/features/deploy.py +605 -0
- praisonai_code/cli/features/diag.py +289 -0
- praisonai_code/cli/features/display_jsonl.py +173 -0
- praisonai_code/cli/features/doctor/__init__.py +63 -0
- praisonai_code/cli/features/doctor/checks/__init__.py +29 -0
- praisonai_code/cli/features/doctor/checks/acp_checks.py +220 -0
- praisonai_code/cli/features/doctor/checks/bot_checks.py +340 -0
- praisonai_code/cli/features/doctor/checks/config_checks.py +373 -0
- praisonai_code/cli/features/doctor/checks/db_checks.py +366 -0
- praisonai_code/cli/features/doctor/checks/env_checks.py +637 -0
- praisonai_code/cli/features/doctor/checks/gateway_checks.py +387 -0
- praisonai_code/cli/features/doctor/checks/lsp_checks.py +231 -0
- praisonai_code/cli/features/doctor/checks/mcp_checks.py +367 -0
- praisonai_code/cli/features/doctor/checks/memory_checks.py +268 -0
- praisonai_code/cli/features/doctor/checks/network_checks.py +251 -0
- praisonai_code/cli/features/doctor/checks/obs_checks.py +328 -0
- praisonai_code/cli/features/doctor/checks/packaging_checks.py +422 -0
- praisonai_code/cli/features/doctor/checks/performance_checks.py +235 -0
- praisonai_code/cli/features/doctor/checks/permissions_checks.py +259 -0
- praisonai_code/cli/features/doctor/checks/runtime_checks.py +650 -0
- praisonai_code/cli/features/doctor/checks/runtime_migration_checks.py +220 -0
- praisonai_code/cli/features/doctor/checks/selftest_checks.py +322 -0
- praisonai_code/cli/features/doctor/checks/serve_checks.py +426 -0
- praisonai_code/cli/features/doctor/checks/skills_checks.py +327 -0
- praisonai_code/cli/features/doctor/checks/tools_checks.py +371 -0
- praisonai_code/cli/features/doctor/engine.py +266 -0
- praisonai_code/cli/features/doctor/formatters.py +377 -0
- praisonai_code/cli/features/doctor/handler.py +564 -0
- praisonai_code/cli/features/doctor/models.py +276 -0
- praisonai_code/cli/features/doctor/registry.py +239 -0
- praisonai_code/cli/features/endpoints.py +1016 -0
- praisonai_code/cli/features/eval.py +559 -0
- praisonai_code/cli/features/examples.py +707 -0
- praisonai_code/cli/features/external_agents.py +231 -0
- praisonai_code/cli/features/fast_context.py +410 -0
- praisonai_code/cli/features/file_history.py +320 -0
- praisonai_code/cli/features/flow_display.py +566 -0
- praisonai_code/cli/features/git_attribution.py +159 -0
- praisonai_code/cli/features/git_integration.py +651 -0
- praisonai_code/cli/features/guardrail.py +171 -0
- praisonai_code/cli/features/handoff.py +252 -0
- praisonai_code/cli/features/hooks.py +583 -0
- praisonai_code/cli/features/hybrid_workflow.py +391 -0
- praisonai_code/cli/features/image.py +384 -0
- praisonai_code/cli/features/interactive_core_headless.py +450 -0
- praisonai_code/cli/features/interactive_runtime.py +600 -0
- praisonai_code/cli/features/interactive_test_harness.py +537 -0
- praisonai_code/cli/features/interactive_tools.py +428 -0
- praisonai_code/cli/features/interactive_tui.py +603 -0
- praisonai_code/cli/features/job_workflow.py +906 -0
- praisonai_code/cli/features/jobs.py +632 -0
- praisonai_code/cli/features/knowledge.py +531 -0
- praisonai_code/cli/features/knowledge_cli.py +438 -0
- praisonai_code/cli/features/lite.py +244 -0
- praisonai_code/cli/features/logs.py +200 -0
- praisonai_code/cli/features/lsp_cli.py +225 -0
- praisonai_code/cli/features/lsp_diagnostics.py +185 -0
- praisonai_code/cli/features/mcp.py +344 -0
- praisonai_code/cli/features/message_queue.py +587 -0
- praisonai_code/cli/features/metrics.py +210 -0
- praisonai_code/cli/features/migrate.py +1329 -0
- praisonai_code/cli/features/migration_flow.py +463 -0
- praisonai_code/cli/features/migration_spec.py +276 -0
- praisonai_code/cli/features/n8n.py +703 -0
- praisonai_code/cli/features/observability.py +293 -0
- praisonai_code/cli/features/ollama.py +361 -0
- praisonai_code/cli/features/output_modes.py +155 -0
- praisonai_code/cli/features/output_style.py +273 -0
- praisonai_code/cli/features/package.py +631 -0
- praisonai_code/cli/features/performance.py +308 -0
- praisonai_code/cli/features/persistence.py +636 -0
- praisonai_code/cli/features/profiler/__init__.py +81 -0
- praisonai_code/cli/features/profiler/core.py +558 -0
- praisonai_code/cli/features/profiler/optimizations.py +652 -0
- praisonai_code/cli/features/profiler/suite.py +386 -0
- praisonai_code/cli/features/queue/__init__.py +73 -0
- praisonai_code/cli/features/queue/manager.py +435 -0
- praisonai_code/cli/features/queue/models.py +289 -0
- praisonai_code/cli/features/queue/persistence.py +564 -0
- praisonai_code/cli/features/queue/scheduler.py +529 -0
- praisonai_code/cli/features/queue/worker.py +400 -0
- praisonai_code/cli/features/recipe.py +2187 -0
- praisonai_code/cli/features/recipe_creator.py +996 -0
- praisonai_code/cli/features/recipe_optimizer.py +1364 -0
- praisonai_code/cli/features/recipe_prompts.py +226 -0
- praisonai_code/cli/features/registry.py +229 -0
- praisonai_code/cli/features/repo_map.py +860 -0
- praisonai_code/cli/features/router.py +466 -0
- praisonai_code/cli/features/safe_shell.py +427 -0
- praisonai_code/cli/features/sandbox_cli.py +283 -0
- praisonai_code/cli/features/sandbox_executor.py +536 -0
- praisonai_code/cli/features/sdk_knowledge.py +500 -0
- praisonai_code/cli/features/session.py +222 -0
- praisonai_code/cli/features/session_checkpoints.py +208 -0
- praisonai_code/cli/features/setup/__init__.py +9 -0
- praisonai_code/cli/features/setup/handler.py +355 -0
- praisonai_code/cli/features/setup/templates.py +62 -0
- praisonai_code/cli/features/skills.py +940 -0
- praisonai_code/cli/features/slash_commands.py +692 -0
- praisonai_code/cli/features/telemetry.py +179 -0
- praisonai_code/cli/features/templates.py +1390 -0
- praisonai_code/cli/features/thinking.py +343 -0
- praisonai_code/cli/features/todo.py +334 -0
- praisonai_code/cli/features/tools.py +680 -0
- praisonai_code/cli/features/tui/__init__.py +83 -0
- praisonai_code/cli/features/tui/app.py +871 -0
- praisonai_code/cli/features/tui/cli.py +580 -0
- praisonai_code/cli/features/tui/config.py +150 -0
- praisonai_code/cli/features/tui/debug.py +526 -0
- praisonai_code/cli/features/tui/events.py +99 -0
- praisonai_code/cli/features/tui/mock_provider.py +328 -0
- praisonai_code/cli/features/tui/orchestrator.py +652 -0
- praisonai_code/cli/features/tui/screens/__init__.py +50 -0
- praisonai_code/cli/features/tui/screens/help.py +157 -0
- praisonai_code/cli/features/tui/screens/main.py +568 -0
- praisonai_code/cli/features/tui/screens/queue.py +174 -0
- praisonai_code/cli/features/tui/screens/session.py +124 -0
- praisonai_code/cli/features/tui/screens/settings.py +148 -0
- praisonai_code/cli/features/tui/session_store.py +198 -0
- praisonai_code/cli/features/tui/widgets/__init__.py +56 -0
- praisonai_code/cli/features/tui/widgets/chat.py +263 -0
- praisonai_code/cli/features/tui/widgets/command_popup.py +258 -0
- praisonai_code/cli/features/tui/widgets/composer.py +292 -0
- praisonai_code/cli/features/tui/widgets/file_popup.py +207 -0
- praisonai_code/cli/features/tui/widgets/queue_panel.py +223 -0
- praisonai_code/cli/features/tui/widgets/status.py +181 -0
- praisonai_code/cli/features/tui/widgets/tool_panel.py +307 -0
- praisonai_code/cli/features/wizard.py +289 -0
- praisonai_code/cli/features/workflow.py +802 -0
- praisonai_code/cli/features/yaml_utils.py +321 -0
- praisonai_code/cli/interactive/__init__.py +48 -0
- praisonai_code/cli/interactive/async_tui.py +1218 -0
- praisonai_code/cli/interactive/config.py +139 -0
- praisonai_code/cli/interactive/core.py +618 -0
- praisonai_code/cli/interactive/events.py +131 -0
- praisonai_code/cli/interactive/frontends/__init__.py +31 -0
- praisonai_code/cli/interactive/frontends/rich_frontend.py +462 -0
- praisonai_code/cli/interactive/frontends/textual_frontend.py +157 -0
- praisonai_code/cli/interactive/praison_io.py +502 -0
- praisonai_code/cli/interactive/repl.py +297 -0
- praisonai_code/cli/interactive/split_tui.py +456 -0
- praisonai_code/cli/interactive/tui_app.py +457 -0
- praisonai_code/cli/langfuse_client.py +360 -0
- praisonai_code/cli/main.py +7421 -0
- praisonai_code/cli/output/__init__.py +25 -0
- praisonai_code/cli/output/console.py +456 -0
- praisonai_code/cli/output/event_bridge.py +191 -0
- praisonai_code/cli/schedule_cli.py +54 -0
- praisonai_code/cli/schema_provider.py +23 -0
- praisonai_code/cli/session/__init__.py +16 -0
- praisonai_code/cli/session/resume.py +148 -0
- praisonai_code/cli/session/unified.py +548 -0
- praisonai_code/cli/state/__init__.py +31 -0
- praisonai_code/cli/state/identifiers.py +161 -0
- praisonai_code/cli/state/project_sessions.py +383 -0
- praisonai_code/cli/state/sessions.py +390 -0
- praisonai_code/cli/ui/__init__.py +160 -0
- praisonai_code/cli/ui/config.py +46 -0
- praisonai_code/cli/ui/events.py +61 -0
- praisonai_code/cli/ui/mg_backend.py +342 -0
- praisonai_code/cli/ui/plain.py +133 -0
- praisonai_code/cli/ui/rich_backend.py +162 -0
- praisonai_code/cli/unified_schema.py +655 -0
- praisonai_code/cli/utils/env_utils.py +126 -0
- praisonai_code/cli/utils/project.py +131 -0
- praisonai_code/cli_backends/__init__.py +73 -0
- praisonai_code/cli_backends/claude.py +373 -0
- praisonai_code/cli_backends/registry.py +113 -0
- praisonai_code/runtime/__init__.py +36 -0
- praisonai_code/runtime/__main__.py +81 -0
- praisonai_code/runtime/client.py +131 -0
- praisonai_code/runtime/descriptor.py +209 -0
- praisonai_code/runtime/server.py +356 -0
- praisonai_code-0.0.1.dist-info/METADATA +80 -0
- praisonai_code-0.0.1.dist-info/RECORD +309 -0
- praisonai_code-0.0.1.dist-info/WHEEL +5 -0
- 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()
|