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,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
|