ccproxy-api 0.1.6__py3-none-any.whl → 0.2.0__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.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +439 -212
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +145 -176
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +402 -530
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +558 -0
- ccproxy/data/codex_headers_fallback.json +121 -0
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +63 -107
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +346 -314
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +95 -342
- ccproxy/utils/version_checker.py +279 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1231
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -269
- ccproxy/services/codex_detection_service.py +0 -263
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.6.dist-info/METADATA +0 -615
- ccproxy_api-0.1.6.dist-info/RECORD +0 -189
- ccproxy_api-0.1.6.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
"""CLI commands for interacting with plugins."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated, Any, cast, get_args, get_origin
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from pydantic import BaseModel, ValidationError
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
from ccproxy.config.settings import Settings
|
|
16
|
+
from ccproxy.core.plugins.discovery import (
|
|
17
|
+
PluginDiscovery,
|
|
18
|
+
PluginFilter,
|
|
19
|
+
build_combined_plugin_denylist,
|
|
20
|
+
)
|
|
21
|
+
from ccproxy.core.plugins.interfaces import PluginFactory
|
|
22
|
+
from ccproxy.templates import PluginTemplateType, build_plugin_scaffold
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(
|
|
26
|
+
name="plugins", help="Manage and inspect plugins.", no_args_is_help=True
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
PLUGIN_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class PluginConfigField:
|
|
35
|
+
"""Renderable representation of a plugin configuration field."""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
type_label: str
|
|
39
|
+
default_label: str
|
|
40
|
+
value_label: str
|
|
41
|
+
description: str
|
|
42
|
+
required: bool
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class PluginMetadata:
|
|
47
|
+
"""Aggregated metadata and configuration for a plugin."""
|
|
48
|
+
|
|
49
|
+
name: str
|
|
50
|
+
version: str | None
|
|
51
|
+
description: str | None
|
|
52
|
+
enabled: bool
|
|
53
|
+
status_reason: str | None
|
|
54
|
+
config_fields: tuple[PluginConfigField, ...]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _format_annotation(annotation: Any) -> str:
|
|
58
|
+
"""Return a human-readable label for a type annotation."""
|
|
59
|
+
|
|
60
|
+
if annotation is None:
|
|
61
|
+
return "Any"
|
|
62
|
+
module = getattr(annotation, "__module__", "")
|
|
63
|
+
if module == "typing":
|
|
64
|
+
return str(annotation).removeprefix("typing.")
|
|
65
|
+
if hasattr(annotation, "__name__"):
|
|
66
|
+
return str(annotation.__name__)
|
|
67
|
+
return str(annotation)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _format_default(field: Any) -> str:
|
|
71
|
+
"""Render default value or factory for display."""
|
|
72
|
+
|
|
73
|
+
default_factory = getattr(field, "default_factory", None)
|
|
74
|
+
if default_factory is not None:
|
|
75
|
+
factory_name = getattr(default_factory, "__name__", repr(default_factory))
|
|
76
|
+
return f"<factory:{factory_name}>"
|
|
77
|
+
|
|
78
|
+
if field.is_required():
|
|
79
|
+
return "required"
|
|
80
|
+
|
|
81
|
+
default_value = getattr(field, "default", None)
|
|
82
|
+
if isinstance(default_value, str):
|
|
83
|
+
return f'"{default_value}"'
|
|
84
|
+
return repr(default_value)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _format_value(value: Any, indent: int = 0, max_depth: int = 3) -> str:
|
|
88
|
+
"""Render an actual configuration value for display with recursive formatting.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
value: The value to format
|
|
92
|
+
indent: Current indentation level
|
|
93
|
+
max_depth: Maximum recursion depth to prevent excessive nesting
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Formatted string representation
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
if value is None:
|
|
100
|
+
return "—"
|
|
101
|
+
|
|
102
|
+
# Prevent excessive recursion
|
|
103
|
+
if indent >= max_depth:
|
|
104
|
+
return repr(value)
|
|
105
|
+
|
|
106
|
+
# Handle strings
|
|
107
|
+
if isinstance(value, str):
|
|
108
|
+
return f'"{value}"'
|
|
109
|
+
|
|
110
|
+
# Handle Path objects
|
|
111
|
+
if hasattr(value, "__fspath__"):
|
|
112
|
+
return str(value)
|
|
113
|
+
|
|
114
|
+
# Handle booleans and numbers
|
|
115
|
+
if isinstance(value, bool | int | float):
|
|
116
|
+
return str(value)
|
|
117
|
+
|
|
118
|
+
# Handle enums
|
|
119
|
+
if hasattr(value, "__class__") and hasattr(value.__class__, "__members__"):
|
|
120
|
+
return f"{value.__class__.__name__}.{value.name}"
|
|
121
|
+
|
|
122
|
+
# Handle Pydantic models
|
|
123
|
+
if isinstance(value, BaseModel):
|
|
124
|
+
return _format_pydantic_model(value, indent, max_depth)
|
|
125
|
+
|
|
126
|
+
# Handle lists
|
|
127
|
+
if isinstance(value, list | tuple):
|
|
128
|
+
return _format_list(value, indent, max_depth)
|
|
129
|
+
|
|
130
|
+
# Handle dicts
|
|
131
|
+
if isinstance(value, dict):
|
|
132
|
+
return _format_dict(value, indent, max_depth)
|
|
133
|
+
|
|
134
|
+
# Fallback to repr for other types
|
|
135
|
+
return repr(value)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _format_pydantic_model(model: BaseModel, indent: int, max_depth: int) -> str:
|
|
139
|
+
"""Format a Pydantic model recursively."""
|
|
140
|
+
if indent >= max_depth:
|
|
141
|
+
return repr(model)
|
|
142
|
+
|
|
143
|
+
indent_str = " " * (indent + 1)
|
|
144
|
+
lines = [f"{model.__class__.__name__}("]
|
|
145
|
+
|
|
146
|
+
for field_name in model.model_fields:
|
|
147
|
+
field_value = getattr(model, field_name)
|
|
148
|
+
formatted_value = _format_value(field_value, indent + 1, max_depth)
|
|
149
|
+
|
|
150
|
+
# Handle multiline values with proper indentation
|
|
151
|
+
if "\n" in formatted_value:
|
|
152
|
+
# First line goes on same line as field name
|
|
153
|
+
value_lines = formatted_value.split("\n")
|
|
154
|
+
lines.append(f"{indent_str}{field_name}={value_lines[0]}")
|
|
155
|
+
# Subsequent lines maintain their indentation
|
|
156
|
+
for value_line in value_lines[1:]:
|
|
157
|
+
lines.append(value_line)
|
|
158
|
+
# Remove trailing comma from last value line and add it properly
|
|
159
|
+
if lines[-1].endswith(","):
|
|
160
|
+
lines[-1] = lines[-1]
|
|
161
|
+
else:
|
|
162
|
+
lines[-1] = lines[-1] + ","
|
|
163
|
+
else:
|
|
164
|
+
lines.append(f"{indent_str}{field_name}={formatted_value},")
|
|
165
|
+
|
|
166
|
+
lines.append(" " * indent + ")")
|
|
167
|
+
return "\n".join(lines)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _format_list(
|
|
171
|
+
items: list[Any] | tuple[Any, ...], indent: int, max_depth: int
|
|
172
|
+
) -> str:
|
|
173
|
+
"""Format a list or tuple recursively."""
|
|
174
|
+
if not items:
|
|
175
|
+
return "[]"
|
|
176
|
+
|
|
177
|
+
if indent >= max_depth:
|
|
178
|
+
return repr(items)
|
|
179
|
+
|
|
180
|
+
# For simple types, keep on one line
|
|
181
|
+
if all(isinstance(item, str | int | float | bool | type(None)) for item in items):
|
|
182
|
+
formatted_items = [_format_value(item, indent, max_depth) for item in items]
|
|
183
|
+
return f"[{', '.join(formatted_items)}]"
|
|
184
|
+
|
|
185
|
+
# For complex types, use multi-line format
|
|
186
|
+
indent_str = " " * (indent + 1)
|
|
187
|
+
lines = ["["]
|
|
188
|
+
for item in items:
|
|
189
|
+
formatted_item = _format_value(item, indent + 1, max_depth)
|
|
190
|
+
# If the formatted item is multiline, indent each line
|
|
191
|
+
if "\n" in formatted_item:
|
|
192
|
+
indented_lines = [
|
|
193
|
+
indent_str + line if i == 0 else " " * (indent + 1) + line
|
|
194
|
+
for i, line in enumerate(formatted_item.split("\n"))
|
|
195
|
+
]
|
|
196
|
+
lines.append("\n".join(indented_lines) + ",")
|
|
197
|
+
else:
|
|
198
|
+
lines.append(f"{indent_str}{formatted_item},")
|
|
199
|
+
|
|
200
|
+
lines.append(" " * indent + "]")
|
|
201
|
+
return "\n".join(lines)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _format_dict(d: dict[Any, Any], indent: int, max_depth: int) -> str:
|
|
205
|
+
"""Format a dictionary recursively."""
|
|
206
|
+
if not d:
|
|
207
|
+
return "{}"
|
|
208
|
+
|
|
209
|
+
if indent >= max_depth:
|
|
210
|
+
return repr(d)
|
|
211
|
+
|
|
212
|
+
indent_str = " " * (indent + 1)
|
|
213
|
+
lines = ["{"]
|
|
214
|
+
|
|
215
|
+
for key, value in d.items():
|
|
216
|
+
formatted_key = _format_value(key, indent, max_depth)
|
|
217
|
+
formatted_value = _format_value(value, indent + 1, max_depth)
|
|
218
|
+
|
|
219
|
+
# If the formatted value is multiline, handle indentation
|
|
220
|
+
if "\n" in formatted_value:
|
|
221
|
+
lines.append(f"{indent_str}{formatted_key}: {formatted_value},")
|
|
222
|
+
else:
|
|
223
|
+
lines.append(f"{indent_str}{formatted_key}: {formatted_value},")
|
|
224
|
+
|
|
225
|
+
lines.append(" " * indent + "}")
|
|
226
|
+
return "\n".join(lines)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _extract_nested_model_types(
|
|
230
|
+
config_class: type[BaseModel] | None,
|
|
231
|
+
) -> dict[str, type[BaseModel]]:
|
|
232
|
+
"""Extract all nested Pydantic model types from a config class.
|
|
233
|
+
|
|
234
|
+
Returns a dict mapping model class names to their types, in dependency order.
|
|
235
|
+
"""
|
|
236
|
+
if config_class is None:
|
|
237
|
+
return {}
|
|
238
|
+
|
|
239
|
+
nested_types: dict[str, type[BaseModel]] = {}
|
|
240
|
+
seen: set[type[BaseModel]] = set()
|
|
241
|
+
|
|
242
|
+
def _extract_from_annotation(annotation: Any) -> None:
|
|
243
|
+
"""Recursively extract BaseModel subclasses from type annotations."""
|
|
244
|
+
if annotation is None:
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
# Check if it's a BaseModel subclass
|
|
248
|
+
try:
|
|
249
|
+
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
|
250
|
+
if annotation not in seen:
|
|
251
|
+
seen.add(annotation)
|
|
252
|
+
nested_types[annotation.__name__] = annotation
|
|
253
|
+
# Recursively extract nested types from this model's fields
|
|
254
|
+
for field in annotation.model_fields.values():
|
|
255
|
+
_extract_from_annotation(field.annotation)
|
|
256
|
+
return
|
|
257
|
+
except TypeError:
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
# Handle generic types (list, dict, Union, etc.)
|
|
261
|
+
origin = get_origin(annotation)
|
|
262
|
+
if origin is not None:
|
|
263
|
+
# Get type arguments
|
|
264
|
+
args = get_args(annotation)
|
|
265
|
+
for arg in args:
|
|
266
|
+
_extract_from_annotation(arg)
|
|
267
|
+
|
|
268
|
+
# Scan all fields
|
|
269
|
+
for field in config_class.model_fields.values():
|
|
270
|
+
_extract_from_annotation(field.annotation)
|
|
271
|
+
|
|
272
|
+
return nested_types
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def describe_config_model(
|
|
276
|
+
config_class: type[BaseModel] | None,
|
|
277
|
+
config_instance: BaseModel | None = None,
|
|
278
|
+
) -> tuple[PluginConfigField, ...]:
|
|
279
|
+
"""Convert a plugin config model into display-ready field metadata."""
|
|
280
|
+
|
|
281
|
+
if config_class is None:
|
|
282
|
+
return ()
|
|
283
|
+
|
|
284
|
+
fields_info: list[PluginConfigField] = []
|
|
285
|
+
for field_name, field in config_class.model_fields.items():
|
|
286
|
+
type_label = _format_annotation(field.annotation)
|
|
287
|
+
default_label = _format_default(field)
|
|
288
|
+
description = field.description or ""
|
|
289
|
+
required = field.is_required()
|
|
290
|
+
value_label = "—"
|
|
291
|
+
|
|
292
|
+
if config_instance is not None:
|
|
293
|
+
value = getattr(config_instance, field_name, None)
|
|
294
|
+
value_label = _format_value(value)
|
|
295
|
+
|
|
296
|
+
fields_info.append(
|
|
297
|
+
PluginConfigField(
|
|
298
|
+
name=field_name,
|
|
299
|
+
type_label=type_label,
|
|
300
|
+
default_label=default_label,
|
|
301
|
+
value_label=value_label,
|
|
302
|
+
description=description,
|
|
303
|
+
required=required,
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return tuple(fields_info)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _load_all_plugin_factories(
|
|
311
|
+
settings: Settings,
|
|
312
|
+
) -> tuple[dict[str, PluginFactory], PluginFilter, set[str]]:
|
|
313
|
+
"""Load plugin factories without applying filters for inspection."""
|
|
314
|
+
|
|
315
|
+
plugin_dirs = [Path(path) for path in settings.plugin_discovery.directories]
|
|
316
|
+
discovery = PluginDiscovery(plugin_dirs)
|
|
317
|
+
|
|
318
|
+
combined_denylist = build_combined_plugin_denylist(
|
|
319
|
+
getattr(settings, "disabled_plugins", None),
|
|
320
|
+
getattr(settings, "plugins", None),
|
|
321
|
+
)
|
|
322
|
+
filter_config = PluginFilter(
|
|
323
|
+
enabled_plugins=getattr(settings, "enabled_plugins", None),
|
|
324
|
+
disabled_plugins=combined_denylist,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
factories = discovery.load_entry_point_factories(plugin_filter=None)
|
|
328
|
+
|
|
329
|
+
if not settings.plugins_disable_local_discovery:
|
|
330
|
+
discovery.discover_plugins()
|
|
331
|
+
filesystem_factories = discovery.load_all_factories(plugin_filter=None)
|
|
332
|
+
for name, factory in filesystem_factories.items():
|
|
333
|
+
factories[name] = factory
|
|
334
|
+
|
|
335
|
+
return factories, filter_config, combined_denylist
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _build_config_instance(
|
|
339
|
+
manifest: Any,
|
|
340
|
+
settings: Settings,
|
|
341
|
+
) -> BaseModel | None:
|
|
342
|
+
"""Instantiate the plugin config using current settings."""
|
|
343
|
+
|
|
344
|
+
config_class = getattr(manifest, "config_class", None)
|
|
345
|
+
if config_class is None:
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
config_data = settings.plugins.get(manifest.name)
|
|
349
|
+
try:
|
|
350
|
+
if config_data is None:
|
|
351
|
+
return config_class() # type: ignore[no-any-return]
|
|
352
|
+
return config_class.model_validate(config_data) # type: ignore[no-any-return]
|
|
353
|
+
except ValidationError:
|
|
354
|
+
# Fall back to defaults to avoid breaking the CLI view
|
|
355
|
+
try:
|
|
356
|
+
return cast(BaseModel, config_class())
|
|
357
|
+
except ValidationError:
|
|
358
|
+
return None
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _derive_status_reason(
|
|
362
|
+
name: str,
|
|
363
|
+
settings: Settings,
|
|
364
|
+
combined_denylist: set[str],
|
|
365
|
+
) -> str | None:
|
|
366
|
+
"""Determine why a plugin is disabled, if applicable."""
|
|
367
|
+
|
|
368
|
+
if name in combined_denylist:
|
|
369
|
+
return "disabled via config"
|
|
370
|
+
if settings.enabled_plugins is not None and name not in set(
|
|
371
|
+
settings.enabled_plugins
|
|
372
|
+
):
|
|
373
|
+
return "disabled via not allow-listed"
|
|
374
|
+
if not settings.enable_plugins:
|
|
375
|
+
return "disabled via plugin system disabled"
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _select_scaffold_root(settings: Settings) -> Path:
|
|
380
|
+
"""Choose a sensible default root for new plugin scaffolds."""
|
|
381
|
+
|
|
382
|
+
directories = settings.plugin_discovery.directories
|
|
383
|
+
if not directories:
|
|
384
|
+
return Path.cwd()
|
|
385
|
+
for candidate in reversed(directories):
|
|
386
|
+
candidate_path = Path(candidate)
|
|
387
|
+
parts = candidate_path.parts
|
|
388
|
+
if len(parts) >= 2 and parts[-2:] == ("ccproxy", "plugins"):
|
|
389
|
+
continue
|
|
390
|
+
return candidate_path
|
|
391
|
+
|
|
392
|
+
return Path(directories[-1])
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def gather_plugin_metadata(settings: Settings) -> tuple[PluginMetadata, ...]:
|
|
396
|
+
"""Collect plugin metadata and configuration for CLI display."""
|
|
397
|
+
|
|
398
|
+
factories, filter_config, combined_denylist = _load_all_plugin_factories(settings)
|
|
399
|
+
|
|
400
|
+
metadata_list: list[PluginMetadata] = []
|
|
401
|
+
for name in sorted(factories):
|
|
402
|
+
factory = factories[name]
|
|
403
|
+
manifest = factory.get_manifest()
|
|
404
|
+
config_instance = _build_config_instance(manifest, settings)
|
|
405
|
+
config_fields = describe_config_model(manifest.config_class, config_instance)
|
|
406
|
+
enabled = settings.enable_plugins and filter_config.is_enabled(name)
|
|
407
|
+
status_reason = (
|
|
408
|
+
None
|
|
409
|
+
if enabled
|
|
410
|
+
else _derive_status_reason(name, settings, combined_denylist)
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
metadata_list.append(
|
|
414
|
+
PluginMetadata(
|
|
415
|
+
name=name,
|
|
416
|
+
version=getattr(manifest, "version", None),
|
|
417
|
+
description=getattr(manifest, "description", None),
|
|
418
|
+
enabled=enabled,
|
|
419
|
+
status_reason=status_reason,
|
|
420
|
+
config_fields=config_fields,
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
return tuple(metadata_list)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@app.command(name="list")
|
|
428
|
+
def list_plugins() -> None:
|
|
429
|
+
"""List all discovered plugins and high-level details."""
|
|
430
|
+
|
|
431
|
+
console = Console()
|
|
432
|
+
settings_obj = Settings.from_config()
|
|
433
|
+
|
|
434
|
+
plugins = gather_plugin_metadata(settings_obj)
|
|
435
|
+
if not plugins:
|
|
436
|
+
console.print("No plugins found.")
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
table = Table(
|
|
440
|
+
title="Discovered Plugins",
|
|
441
|
+
show_header=True,
|
|
442
|
+
header_style="bold magenta",
|
|
443
|
+
)
|
|
444
|
+
table.add_column("Plugin", style="bold")
|
|
445
|
+
table.add_column("Version", style="cyan")
|
|
446
|
+
table.add_column("Status", style="green")
|
|
447
|
+
table.add_column("Config Fields", style="yellow")
|
|
448
|
+
table.add_column("Description", style="dim")
|
|
449
|
+
|
|
450
|
+
for plugin in plugins:
|
|
451
|
+
status = "Enabled" if plugin.enabled else "Disabled"
|
|
452
|
+
if plugin.status_reason:
|
|
453
|
+
status = f"{status} ({plugin.status_reason})"
|
|
454
|
+
config_count = str(len(plugin.config_fields)) if plugin.config_fields else "0"
|
|
455
|
+
table.add_row(
|
|
456
|
+
plugin.name,
|
|
457
|
+
plugin.version or "unknown",
|
|
458
|
+
status,
|
|
459
|
+
config_count,
|
|
460
|
+
plugin.description or "",
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
console.print(table)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
@app.command()
|
|
467
|
+
def settings(
|
|
468
|
+
plugin: str | None = typer.Argument(None, help="Plugin to inspect"),
|
|
469
|
+
) -> None:
|
|
470
|
+
"""Show configuration fields for plugins."""
|
|
471
|
+
from ccproxy.cli._settings_help import print_settings_help
|
|
472
|
+
|
|
473
|
+
console = Console()
|
|
474
|
+
settings_obj = Settings.from_config()
|
|
475
|
+
|
|
476
|
+
plugins = gather_plugin_metadata(settings_obj)
|
|
477
|
+
if not plugins:
|
|
478
|
+
console.print("No plugins found.")
|
|
479
|
+
return
|
|
480
|
+
|
|
481
|
+
if plugin is not None:
|
|
482
|
+
plugins = tuple(p for p in plugins if p.name == plugin)
|
|
483
|
+
if not plugins:
|
|
484
|
+
console.print(f"Plugin '{plugin}' not found.")
|
|
485
|
+
return
|
|
486
|
+
|
|
487
|
+
# Load plugin factories to get config classes
|
|
488
|
+
factories, _filter_config, _combined_denylist = _load_all_plugin_factories(
|
|
489
|
+
settings_obj
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
for plugin_meta in plugins:
|
|
493
|
+
# Get the plugin factory and config
|
|
494
|
+
factory = factories.get(plugin_meta.name)
|
|
495
|
+
if not factory:
|
|
496
|
+
console.print(
|
|
497
|
+
f"[yellow]Warning: Could not load factory for {plugin_meta.name}[/yellow]"
|
|
498
|
+
)
|
|
499
|
+
continue
|
|
500
|
+
|
|
501
|
+
manifest = factory.get_manifest()
|
|
502
|
+
config_class = getattr(manifest, "config_class", None)
|
|
503
|
+
|
|
504
|
+
if not config_class:
|
|
505
|
+
console.print(f" {plugin_meta.name}: No configuration fields declared.")
|
|
506
|
+
continue
|
|
507
|
+
|
|
508
|
+
# Get the config instance
|
|
509
|
+
config_instance = _build_config_instance(manifest, settings_obj)
|
|
510
|
+
|
|
511
|
+
# Use generic settings display
|
|
512
|
+
print_settings_help(
|
|
513
|
+
config_class,
|
|
514
|
+
config_instance,
|
|
515
|
+
version=plugin_meta.version,
|
|
516
|
+
enabled=plugin_meta.enabled,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@app.command()
|
|
521
|
+
def dependencies() -> None:
|
|
522
|
+
"""Display how plugin dependencies are managed."""
|
|
523
|
+
|
|
524
|
+
console = Console()
|
|
525
|
+
console.print(
|
|
526
|
+
"Plugin dependencies are managed at the package level (pyproject.toml/extras)."
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
@app.command()
|
|
531
|
+
def scaffold(
|
|
532
|
+
plugin_name: Annotated[
|
|
533
|
+
str,
|
|
534
|
+
typer.Argument(
|
|
535
|
+
help="New plugin package name (snake_case).",
|
|
536
|
+
),
|
|
537
|
+
],
|
|
538
|
+
plugin_type: Annotated[
|
|
539
|
+
PluginTemplateType,
|
|
540
|
+
typer.Option(
|
|
541
|
+
"--type",
|
|
542
|
+
"-t",
|
|
543
|
+
help="Scaffold type to generate (system, provider, auth).",
|
|
544
|
+
case_sensitive=False,
|
|
545
|
+
),
|
|
546
|
+
] = PluginTemplateType.SYSTEM,
|
|
547
|
+
description: Annotated[
|
|
548
|
+
str,
|
|
549
|
+
typer.Option(
|
|
550
|
+
"--description",
|
|
551
|
+
"-d",
|
|
552
|
+
help="Plugin description stored in the manifest.",
|
|
553
|
+
),
|
|
554
|
+
] = "Custom CCProxy plugin.",
|
|
555
|
+
version: Annotated[
|
|
556
|
+
str,
|
|
557
|
+
typer.Option(
|
|
558
|
+
"--version",
|
|
559
|
+
"-v",
|
|
560
|
+
help="Semver version recorded in the manifest.",
|
|
561
|
+
),
|
|
562
|
+
] = "0.1.0",
|
|
563
|
+
output_path: Annotated[
|
|
564
|
+
Path | None,
|
|
565
|
+
typer.Option(
|
|
566
|
+
"--path",
|
|
567
|
+
"-p",
|
|
568
|
+
help="Directory to create the plugin in (defaults to user plugin dir).",
|
|
569
|
+
file_okay=False,
|
|
570
|
+
dir_okay=True,
|
|
571
|
+
writable=True,
|
|
572
|
+
resolve_path=True,
|
|
573
|
+
),
|
|
574
|
+
] = None,
|
|
575
|
+
include_tests: Annotated[
|
|
576
|
+
bool,
|
|
577
|
+
typer.Option(
|
|
578
|
+
"--with-tests/--no-tests",
|
|
579
|
+
help="Include placeholder pytest files in the scaffold.",
|
|
580
|
+
),
|
|
581
|
+
] = False,
|
|
582
|
+
force: Annotated[
|
|
583
|
+
bool,
|
|
584
|
+
typer.Option(
|
|
585
|
+
"--force/--no-force",
|
|
586
|
+
help="Overwrite existing files when the directory already exists.",
|
|
587
|
+
),
|
|
588
|
+
] = False,
|
|
589
|
+
) -> None:
|
|
590
|
+
"""Generate a plugin scaffold to jump-start development."""
|
|
591
|
+
|
|
592
|
+
console = Console()
|
|
593
|
+
settings_obj = Settings.from_config()
|
|
594
|
+
raw_name = plugin_name.strip()
|
|
595
|
+
normalised = raw_name.lower()
|
|
596
|
+
|
|
597
|
+
if not PLUGIN_NAME_PATTERN.match(normalised):
|
|
598
|
+
raise typer.BadParameter(
|
|
599
|
+
"Plugin name must start with a letter and use lowercase, digits, or underscores.",
|
|
600
|
+
param_hint="plugin_name",
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
plugin_name = normalised
|
|
604
|
+
|
|
605
|
+
if output_path is None:
|
|
606
|
+
target_root = _select_scaffold_root(settings_obj)
|
|
607
|
+
else:
|
|
608
|
+
target_root = output_path
|
|
609
|
+
|
|
610
|
+
target_root = target_root.expanduser()
|
|
611
|
+
target_root.mkdir(parents=True, exist_ok=True)
|
|
612
|
+
|
|
613
|
+
target_dir = target_root / plugin_name
|
|
614
|
+
if target_dir.exists():
|
|
615
|
+
has_content = any(target_dir.iterdir())
|
|
616
|
+
if has_content and not force:
|
|
617
|
+
console.print(
|
|
618
|
+
f"[red]Directory {target_dir} already exists. Use --force to overwrite.[/red]"
|
|
619
|
+
)
|
|
620
|
+
raise typer.Exit(code=1)
|
|
621
|
+
else:
|
|
622
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
623
|
+
|
|
624
|
+
try:
|
|
625
|
+
files = build_plugin_scaffold(
|
|
626
|
+
plugin_name=plugin_name,
|
|
627
|
+
description=description,
|
|
628
|
+
version=version,
|
|
629
|
+
template_type=plugin_type,
|
|
630
|
+
include_tests=include_tests,
|
|
631
|
+
)
|
|
632
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
633
|
+
console.print(f"[red]Failed to build scaffold: {exc}[/red]")
|
|
634
|
+
raise typer.Exit(code=1) from exc
|
|
635
|
+
|
|
636
|
+
created: list[tuple[str, str]] = []
|
|
637
|
+
for relative_path, content in files.items():
|
|
638
|
+
destination = target_dir / relative_path
|
|
639
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
640
|
+
action = "overwrote" if destination.exists() else "created"
|
|
641
|
+
destination.write_text(content, encoding="utf-8")
|
|
642
|
+
created.append((action, relative_path))
|
|
643
|
+
|
|
644
|
+
console.print(
|
|
645
|
+
f"[bold green]Plugin scaffold ready[/bold green] in [cyan]{target_dir}[/cyan]"
|
|
646
|
+
)
|
|
647
|
+
if raw_name != plugin_name:
|
|
648
|
+
console.print(
|
|
649
|
+
f" • Normalised plugin name to [bold]{plugin_name}[/bold] from '{raw_name}'."
|
|
650
|
+
)
|
|
651
|
+
for action, relative_path in created:
|
|
652
|
+
console.print(f" • {action}: {relative_path}")
|
|
653
|
+
console.print(
|
|
654
|
+
" • Update config and runtime files before enabling the plugin.",
|
|
655
|
+
style="dim",
|
|
656
|
+
)
|
|
657
|
+
if settings_obj.plugins_disable_local_discovery:
|
|
658
|
+
console.print(
|
|
659
|
+
" • Local plugin discovery is disabled. Set `plugins_disable_local_discovery = false`"
|
|
660
|
+
" in your config or export `PLUGINS_DISABLE_LOCAL_DISCOVERY=false` to load filesystem"
|
|
661
|
+
" plugins.",
|
|
662
|
+
style="yellow",
|
|
663
|
+
)
|
|
664
|
+
if not settings_obj.enable_plugins:
|
|
665
|
+
console.print(
|
|
666
|
+
" • Plugin system is disabled (`enable_plugins = false`). Update configuration to"
|
|
667
|
+
" load plugins.",
|
|
668
|
+
style="yellow",
|
|
669
|
+
)
|