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