code-context-control 2.39.0__tar.gz → 2.40.0__tar.gz
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.
- {code_context_control-2.39.0/code_context_control.egg-info → code_context_control-2.40.0}/PKG-INFO +16 -3
- {code_context_control-2.39.0 → code_context_control-2.40.0}/README.md +15 -2
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/c3.py +36 -11
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/commands/parser.py +1 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/bitbucket.py +25 -20
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/delegate.py +80 -2
- {code_context_control-2.39.0 → code_context_control-2.40.0/code_context_control.egg-info}/PKG-INFO +16 -3
- {code_context_control-2.39.0 → code_context_control-2.40.0}/code_context_control.egg-info/SOURCES.txt +3 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/core/config.py +34 -8
- {code_context_control-2.39.0 → code_context_control-2.40.0}/pyproject.toml +1 -1
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/bitbucket_client.py +46 -11
- code_context_control-2.40.0/services/circuit_breaker.py +86 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/claude_md.py +1 -1
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_bitbucket_client.py +63 -4
- code_context_control-2.40.0/tests/test_bitbucket_config_fallback.py +70 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_bitbucket_tool.py +10 -0
- code_context_control-2.40.0/tests/test_circuit_breaker.py +103 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/LICENSE +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/__init__.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/_hook_utils.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/commands/__init__.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/commands/common.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/docs.html +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/edits.html +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/guide/bitbucket.html +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/guide/getting-started.html +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/guide/index.html +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/guide/oracle.html +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/guide/shared.css +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/guide/tools.html +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/guide/workflow.html +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_auto_snapshot.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_c3_signal.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_c3read.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_edit_ledger.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_edit_unlock.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_filter.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_ghost_files.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_pretool_enforce.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_read.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_session_stats.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hook_terse_advisor.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hub.html +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/hub_server.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/mcp_proxy.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/mcp_server.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/server.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/__init__.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/_helpers.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/agent.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/compress.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/edit.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/edits.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/filter.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/impact.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/memory.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/project.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/read.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/search.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/session.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/shell.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/status.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/tools/validate.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/api.js +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/app.js +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/components/bitbucket.js +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/components/chat.js +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/components/dashboard.js +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/components/edits.js +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/components/instructions.js +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/components/memory.js +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/components/sessions.js +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/components/settings.js +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/components/sidebar.js +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/icons.js +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/shared.js +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui/theme.js +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui.html +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui_legacy.html +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/cli/ui_nano.html +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/code_context_control.egg-info/dependency_links.txt +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/code_context_control.egg-info/entry_points.txt +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/code_context_control.egg-info/requires.txt +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/code_context_control.egg-info/top_level.txt +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/core/__init__.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/core/ide.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/core/mcp_toml.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/core/web_security.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/__init__.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/config.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/mcp_oracle.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/oracle.html +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/oracle_server.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/__init__.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/activity_reporter.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/api_auth.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/c3_bridge.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/chat_engine.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/chat_store.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/cross_memory.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/federated_graph.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/health_checker.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/insight_engine.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/memory_reader.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/memory_writer.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/ollama_bridge.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/project_scanner.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/review_agent.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/tool_executor.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/oracle/services/tool_registry.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/__init__.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/activity_log.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/agent_base.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/agents.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/auto_memory.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/bench/__init__.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/bench/external/__init__.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/bench/external/aider_polyglot.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/bench/external/swe_bench.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/benchmark_dashboard.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/bitbucket_credentials.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/compressor.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/context_snapshot.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/conversation_store.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/doc_index.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/e2e_benchmark.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/e2e_evaluator.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/e2e_tasks.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/edit_ledger.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/embedding_index.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/error_reporting.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/file_memory.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/git_context.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/hub_service.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/indexer.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/memory.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/memory_consolidator.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/memory_graph.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/memory_grounder.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/memory_scorer.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/metrics.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/notifications.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/ollama_client.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/output_filter.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/parser.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/project_manager.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/project_runtime.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/protocol.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/proxy_state.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/retrieval_broker.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/router.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/runtime.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/session_benchmark.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/session_manager.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/session_preloader.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/text_index.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/tool_classifier.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/transcript_index.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/validation_cache.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/vector_store.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/version_tracker.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/services/watcher.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/setup.cfg +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_activity_reporter.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_aider_polyglot.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_bitbucket_cli_smoke.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_bitbucket_credentials.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_c3_shell.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_claude_md_merge.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_cli_smoke.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_e2e_benchmark.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_edit_ledger_hook.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_edit_normalization.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_enforcement_flip.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_federated_graph.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_ghost_files.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_git_branch_awareness.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_hub_server_smoke.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_install_mcp_entrypoint.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_lazy_store_init.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_mcp_host_guard.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_mcp_server_smoke.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_mcp_toml.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_memory_graph_api.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_memory_system.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_notification_discipline.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_oracle_api_auth.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_oracle_apikey_api.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_oracle_discovery_api.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_oracle_security_fixes.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_output_filter.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_permissions.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_project_manager.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_project_manager_merge.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_project_tool.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_read_coercion.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_service_durability.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_session_benchmark.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_session_budget.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_shell_robustness.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_swe_bench.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_tool_registry.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_upgrade_and_version.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_validate.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_web_security.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tests/test_windows_reliability.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/__init__.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/backend.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/main.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/__init__.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/benchmark_view.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/claudemd_view.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/compress_view.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/index_view.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/init_view.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/mcp_view.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/optimize_view.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/pipe_view.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/projects_view.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/search_view.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/session_view.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/stats.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/screens/ui_view.py +0 -0
- {code_context_control-2.39.0 → code_context_control-2.40.0}/tui/theme.tcss +0 -0
{code_context_control-2.39.0/code_context_control.egg-info → code_context_control-2.40.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-context-control
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.40.0
|
|
4
4
|
Summary: Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer.
|
|
5
5
|
Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -283,9 +283,12 @@ Access Token. Tokens live in the **OS keyring** (Windows Credential Manager,
|
|
|
283
283
|
macOS Keychain, Linux Secret Service) — never in `.c3/config.json`.
|
|
284
284
|
|
|
285
285
|
```bash
|
|
286
|
-
# One-time login per server
|
|
286
|
+
# One-time login per server (stored under this project's .c3/config.json)
|
|
287
287
|
c3 bitbucket login --url https://bitbucket.example.com
|
|
288
|
-
#
|
|
288
|
+
# -> prompts for username + PAT (masked)
|
|
289
|
+
|
|
290
|
+
# ...or store it globally so every C3 project can use it
|
|
291
|
+
c3 bitbucket login --global --url https://bitbucket.example.com
|
|
289
292
|
|
|
290
293
|
# Pin defaults so subsequent calls don't need project/repo
|
|
291
294
|
c3 bitbucket set-default --project PROJ --repo my-service
|
|
@@ -294,6 +297,16 @@ c3 bitbucket set-default --project PROJ --repo my-service
|
|
|
294
297
|
c3 bitbucket status
|
|
295
298
|
```
|
|
296
299
|
|
|
300
|
+
**Account resolution precedence:** the project's `.c3/config.json` wins, but when
|
|
301
|
+
it has no active account C3 falls back to the global `~/.c3/config.json`. So a
|
|
302
|
+
single `login --global` (or any login done from your home directory) is reusable
|
|
303
|
+
across every C3 project — the PAT always lives in the OS keyring, never on disk.
|
|
304
|
+
|
|
305
|
+
> **Upgrading:** stop the running `c3-mcp` server / CLI before `c3 upgrade`. A live
|
|
306
|
+
> process can hold package files open, leaving pip's `~`-prefixed backup dirs
|
|
307
|
+
> (`~ervices`, `~ools`, …) in `site-packages`; those are inert and safe to delete
|
|
308
|
+
> after the upgrade completes.
|
|
309
|
+
|
|
297
310
|
The MCP tool dispatches by `action`. Read-only actions: `status`, `whoami`,
|
|
298
311
|
`list_projects`, `list_repos`, `get_repo`, `list_prs`, `get_pr`, `get_pr_diff`,
|
|
299
312
|
`get_pr_activities`, `list_branches`, `list_commits`, `list_activity`,
|
|
@@ -221,9 +221,12 @@ Access Token. Tokens live in the **OS keyring** (Windows Credential Manager,
|
|
|
221
221
|
macOS Keychain, Linux Secret Service) — never in `.c3/config.json`.
|
|
222
222
|
|
|
223
223
|
```bash
|
|
224
|
-
# One-time login per server
|
|
224
|
+
# One-time login per server (stored under this project's .c3/config.json)
|
|
225
225
|
c3 bitbucket login --url https://bitbucket.example.com
|
|
226
|
-
#
|
|
226
|
+
# -> prompts for username + PAT (masked)
|
|
227
|
+
|
|
228
|
+
# ...or store it globally so every C3 project can use it
|
|
229
|
+
c3 bitbucket login --global --url https://bitbucket.example.com
|
|
227
230
|
|
|
228
231
|
# Pin defaults so subsequent calls don't need project/repo
|
|
229
232
|
c3 bitbucket set-default --project PROJ --repo my-service
|
|
@@ -232,6 +235,16 @@ c3 bitbucket set-default --project PROJ --repo my-service
|
|
|
232
235
|
c3 bitbucket status
|
|
233
236
|
```
|
|
234
237
|
|
|
238
|
+
**Account resolution precedence:** the project's `.c3/config.json` wins, but when
|
|
239
|
+
it has no active account C3 falls back to the global `~/.c3/config.json`. So a
|
|
240
|
+
single `login --global` (or any login done from your home directory) is reusable
|
|
241
|
+
across every C3 project — the PAT always lives in the OS keyring, never on disk.
|
|
242
|
+
|
|
243
|
+
> **Upgrading:** stop the running `c3-mcp` server / CLI before `c3 upgrade`. A live
|
|
244
|
+
> process can hold package files open, leaving pip's `~`-prefixed backup dirs
|
|
245
|
+
> (`~ervices`, `~ools`, …) in `site-packages`; those are inert and safe to delete
|
|
246
|
+
> after the upgrade completes.
|
|
247
|
+
|
|
235
248
|
The MCP tool dispatches by `action`. Read-only actions: `status`, `whoami`,
|
|
236
249
|
`list_projects`, `list_repos`, `get_repo`, `list_prs`, `get_pr`, `get_pr_diff`,
|
|
237
250
|
`get_pr_activities`, `list_branches`, `list_commits`, `list_activity`,
|
|
@@ -85,7 +85,7 @@ console = Console() if HAS_RICH else None
|
|
|
85
85
|
# Config
|
|
86
86
|
CONFIG_DIR = ".c3"
|
|
87
87
|
CONFIG_FILE = ".c3/config.json"
|
|
88
|
-
__version__ = "2.
|
|
88
|
+
__version__ = "2.40.0"
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def _command_deps() -> CommandDeps:
|
|
@@ -5577,14 +5577,19 @@ def _bb_cmd_login(args, project_path: str) -> None:
|
|
|
5577
5577
|
from services import bitbucket_credentials as bb_creds
|
|
5578
5578
|
from services.bitbucket_client import BitbucketDataCenterClient, BitbucketError
|
|
5579
5579
|
|
|
5580
|
+
# --global stores the account in ~/.c3/config.json so it is reusable in
|
|
5581
|
+
# every C3 project (load_bitbucket_config falls back to it automatically).
|
|
5582
|
+
if getattr(args, "use_global", False):
|
|
5583
|
+
project_path = str(Path.home())
|
|
5584
|
+
|
|
5580
5585
|
base_url = (args.url or "").rstrip("/")
|
|
5581
5586
|
username = args.username or input(f"Username for {base_url}: ").strip()
|
|
5582
5587
|
if not username:
|
|
5583
|
-
print("Login cancelled
|
|
5588
|
+
print("Login cancelled -- username required.")
|
|
5584
5589
|
return
|
|
5585
5590
|
token = args.token or getpass.getpass(f"Personal Access Token for {username}: ").strip()
|
|
5586
5591
|
if not token:
|
|
5587
|
-
print("Login cancelled
|
|
5592
|
+
print("Login cancelled -- token required.")
|
|
5588
5593
|
return
|
|
5589
5594
|
|
|
5590
5595
|
try:
|
|
@@ -5600,10 +5605,13 @@ def _bb_cmd_login(args, project_path: str) -> None:
|
|
|
5600
5605
|
if getattr(args, "insecure", False):
|
|
5601
5606
|
bb_creds.set_verify_tls(False, project_path=project_path)
|
|
5602
5607
|
|
|
5603
|
-
|
|
5608
|
+
scope = "global (~/.c3)" if getattr(args, "use_global", False) else "project"
|
|
5609
|
+
print(f"[OK] Stored credentials for {username}@{base_url} [{scope}]")
|
|
5604
5610
|
|
|
5605
|
-
# Connection probe
|
|
5606
|
-
# network blocked
|
|
5611
|
+
# Connection probe -- non-fatal if it fails (token might be valid but the
|
|
5612
|
+
# network is blocked right now). Gate success on application-properties
|
|
5613
|
+
# only; whoami enrichment is best-effort so a valid login never prints a
|
|
5614
|
+
# failure (Bitbucket DC has no /users/me).
|
|
5607
5615
|
try:
|
|
5608
5616
|
client = BitbucketDataCenterClient(
|
|
5609
5617
|
base_url=base_url, token=token,
|
|
@@ -5611,12 +5619,20 @@ def _bb_cmd_login(args, project_path: str) -> None:
|
|
|
5611
5619
|
)
|
|
5612
5620
|
props = client.application_properties()
|
|
5613
5621
|
version = props.get("version", "?")
|
|
5614
|
-
user = client.whoami()
|
|
5615
5622
|
print(f" Server: {version} ({base_url})")
|
|
5616
|
-
print(f" Auth as: {user.get('displayName', username)} <{user.get('emailAddress', '?')}>")
|
|
5617
5623
|
except BitbucketError as exc:
|
|
5618
5624
|
print(f"[warn] Connection probe failed: {exc}")
|
|
5619
|
-
print(" Token saved anyway
|
|
5625
|
+
print(" Token saved anyway -- re-test with `c3 bitbucket status`.")
|
|
5626
|
+
return
|
|
5627
|
+
try:
|
|
5628
|
+
user = client.whoami()
|
|
5629
|
+
if user:
|
|
5630
|
+
print(
|
|
5631
|
+
f" Auth as: {user.get('displayName', username)} "
|
|
5632
|
+
f"<{user.get('emailAddress', '?')}>"
|
|
5633
|
+
)
|
|
5634
|
+
except BitbucketError:
|
|
5635
|
+
pass
|
|
5620
5636
|
|
|
5621
5637
|
|
|
5622
5638
|
def _bb_cmd_logout(args, project_path: str) -> None:
|
|
@@ -5661,7 +5677,7 @@ def _bb_cmd_status(args, project_path: str) -> None:
|
|
|
5661
5677
|
return
|
|
5662
5678
|
token = bb_creds.load_token(active["base_url"], active["username"])
|
|
5663
5679
|
if not token:
|
|
5664
|
-
print(" Connection: FAIL
|
|
5680
|
+
print(" Connection: FAIL -- no token in keyring")
|
|
5665
5681
|
return
|
|
5666
5682
|
try:
|
|
5667
5683
|
client = BitbucketDataCenterClient(
|
|
@@ -5671,7 +5687,7 @@ def _bb_cmd_status(args, project_path: str) -> None:
|
|
|
5671
5687
|
props = client.application_properties()
|
|
5672
5688
|
print(f" Connection: OK (version {props.get('version','?')})")
|
|
5673
5689
|
except BitbucketError as exc:
|
|
5674
|
-
print(f" Connection: FAIL
|
|
5690
|
+
print(f" Connection: FAIL -- {exc}")
|
|
5675
5691
|
|
|
5676
5692
|
|
|
5677
5693
|
def _bb_cmd_use(args, project_path: str) -> None:
|
|
@@ -6571,6 +6587,15 @@ def _launch_tui() -> None:
|
|
|
6571
6587
|
|
|
6572
6588
|
|
|
6573
6589
|
def main():
|
|
6590
|
+
# Force UTF-8 on the CLI streams so server-supplied text (PR titles, branch
|
|
6591
|
+
# names, diffs) and our own glyphs render cleanly on Windows cp1252 consoles
|
|
6592
|
+
# instead of raising UnicodeEncodeError or mojibaking.
|
|
6593
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
6594
|
+
try:
|
|
6595
|
+
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
6596
|
+
except Exception:
|
|
6597
|
+
pass
|
|
6598
|
+
|
|
6574
6599
|
try:
|
|
6575
6600
|
from services import error_reporting
|
|
6576
6601
|
error_reporting.init(component="c3-cli", version=__version__)
|
|
@@ -303,6 +303,7 @@ def build_parser(version: str, parse_cli_ide_arg):
|
|
|
303
303
|
bb_login.add_argument("--token", help="Personal Access Token (prompted via getpass if omitted — preferred)")
|
|
304
304
|
bb_login.add_argument("--no-set-active", action="store_true", help="Do not switch the active account to this one")
|
|
305
305
|
bb_login.add_argument("--insecure", action="store_true", help="Disable TLS verification (self-signed certs)")
|
|
306
|
+
bb_login.add_argument("--global", dest="use_global", action="store_true", help="Store the account in the global ~/.c3/config.json so it is reusable in every C3 project")
|
|
306
307
|
bb_login.add_argument("project_path", nargs="?", default=".")
|
|
307
308
|
|
|
308
309
|
bb_logout = bb_subs.add_parser("logout", help="Remove a Bitbucket account from keyring + config")
|
|
@@ -50,7 +50,12 @@ def _cap(resp: str) -> str:
|
|
|
50
50
|
candidate = "\n".join(lines) + "\n[truncated]"
|
|
51
51
|
if count_tokens(candidate) <= _RESPONSE_TOKEN_CAP:
|
|
52
52
|
return candidate
|
|
53
|
-
|
|
53
|
+
# A single over-long line never splits above; hard-clamp by characters as a
|
|
54
|
+
# final guard (~4 chars/token) so one huge line can't blow past the cap.
|
|
55
|
+
head = "\n".join(lines[:20])
|
|
56
|
+
if count_tokens(head) > _RESPONSE_TOKEN_CAP:
|
|
57
|
+
head = head[:_RESPONSE_TOKEN_CAP * 4]
|
|
58
|
+
return head + "\n[truncated]"
|
|
54
59
|
|
|
55
60
|
|
|
56
61
|
def _build_client(svc) -> tuple[BitbucketDataCenterClient | None, str]:
|
|
@@ -111,20 +116,20 @@ def _format_pr(pr: dict) -> str:
|
|
|
111
116
|
author = (pr.get("author") or {}).get("user", {}).get("name", "?")
|
|
112
117
|
src = (pr.get("fromRef") or {}).get("displayId", "?")
|
|
113
118
|
dst = (pr.get("toRef") or {}).get("displayId", "?")
|
|
114
|
-
return f" #{pr_id} [{state:7}] {src}
|
|
119
|
+
return f" #{pr_id} [{state:7}] {src} -> {dst} by {author}\n {title}"
|
|
115
120
|
|
|
116
121
|
|
|
117
122
|
def _format_pr_full(pr: dict) -> str:
|
|
118
123
|
lines = [
|
|
119
|
-
f"PR #{pr.get('id')} [{pr.get('state')}]
|
|
120
|
-
f" {(pr.get('fromRef') or {}).get('displayId')}
|
|
124
|
+
f"PR #{pr.get('id')} [{pr.get('state')}] -- {pr.get('title','')}",
|
|
125
|
+
f" {(pr.get('fromRef') or {}).get('displayId')} -> {(pr.get('toRef') or {}).get('displayId')}",
|
|
121
126
|
f" Author: {(pr.get('author') or {}).get('user',{}).get('displayName','?')}",
|
|
122
127
|
f" Version: {pr.get('version')} | Open tasks: {pr.get('openTaskCount', 0)}",
|
|
123
128
|
]
|
|
124
129
|
reviewers = pr.get("reviewers") or []
|
|
125
130
|
if reviewers:
|
|
126
131
|
rs = ", ".join(
|
|
127
|
-
f"{r.get('user',{}).get('name','?')}({'
|
|
132
|
+
f"{r.get('user',{}).get('name','?')}({'[x]' if r.get('approved') else '[ ]'})"
|
|
128
133
|
for r in reviewers
|
|
129
134
|
)
|
|
130
135
|
lines.append(f" Reviewers: {rs}")
|
|
@@ -190,7 +195,7 @@ def _act_status(client: BitbucketDataCenterClient | None, err: str, svc) -> str:
|
|
|
190
195
|
version = props.get("version", "?")
|
|
191
196
|
out.append(f" Server : OK (version {version})")
|
|
192
197
|
except BitbucketError as exc:
|
|
193
|
-
out.append(f" Server : FAIL
|
|
198
|
+
out.append(f" Server : FAIL -- {exc}")
|
|
194
199
|
return "\n".join(out)
|
|
195
200
|
|
|
196
201
|
|
|
@@ -214,7 +219,7 @@ def _act_list_projects(client: BitbucketDataCenterClient, kwargs: dict) -> str:
|
|
|
214
219
|
for p in projects[:50]:
|
|
215
220
|
lines.append(f" {p.get('key','?'):16} {p.get('name','?')}")
|
|
216
221
|
if len(projects) > 50:
|
|
217
|
-
lines.append(f"
|
|
222
|
+
lines.append(f" ... +{len(projects) - 50} more")
|
|
218
223
|
return "\n".join(lines)
|
|
219
224
|
|
|
220
225
|
|
|
@@ -222,11 +227,11 @@ def _act_list_repos(client: BitbucketDataCenterClient, project: str) -> str:
|
|
|
222
227
|
repos = client.list_repos(project)
|
|
223
228
|
if not repos:
|
|
224
229
|
return f"[bitbucket:repos] {project}: (none)"
|
|
225
|
-
lines = [f"[bitbucket:repos] {project}
|
|
230
|
+
lines = [f"[bitbucket:repos] {project} - {len(repos)} repo(s)"]
|
|
226
231
|
for r in repos[:80]:
|
|
227
232
|
lines.append(f" {r.get('slug','?'):30} {r.get('name','?')}")
|
|
228
233
|
if len(repos) > 80:
|
|
229
|
-
lines.append(f"
|
|
234
|
+
lines.append(f" ... +{len(repos) - 80} more")
|
|
230
235
|
return "\n".join(lines)
|
|
231
236
|
|
|
232
237
|
|
|
@@ -252,7 +257,7 @@ def _act_list_prs(client, project: str, repo: str, kwargs: dict) -> str:
|
|
|
252
257
|
)
|
|
253
258
|
if not prs:
|
|
254
259
|
return f"[bitbucket:prs] {project}/{repo} state={state}: (none)"
|
|
255
|
-
out = [f"[bitbucket:prs] {project}/{repo} state={state}
|
|
260
|
+
out = [f"[bitbucket:prs] {project}/{repo} state={state} - {len(prs)} PR(s)"]
|
|
256
261
|
for pr in prs:
|
|
257
262
|
out.append(_format_pr(pr))
|
|
258
263
|
return "\n".join(out)
|
|
@@ -266,7 +271,7 @@ def _act_get_pr(client, project: str, repo: str, pr_id: int) -> str:
|
|
|
266
271
|
def _act_get_pr_diff(client, project: str, repo: str, pr_id: int, kwargs: dict) -> str:
|
|
267
272
|
diff = client.get_pr_diff(project, repo, pr_id, context_lines=int(kwargs.get("context_lines", 3)))
|
|
268
273
|
if len(diff) > _DIFF_PREVIEW_CHARS:
|
|
269
|
-
diff = diff[:_DIFF_PREVIEW_CHARS] + "\n
|
|
274
|
+
diff = diff[:_DIFF_PREVIEW_CHARS] + "\n... [diff truncated]"
|
|
270
275
|
return f"[bitbucket:diff] {project}/{repo}#{pr_id}\n{diff}"
|
|
271
276
|
|
|
272
277
|
|
|
@@ -274,7 +279,7 @@ def _act_get_pr_activities(client, project: str, repo: str, pr_id: int) -> str:
|
|
|
274
279
|
acts = client.get_pr_activities(project, repo, pr_id)
|
|
275
280
|
if not acts:
|
|
276
281
|
return f"[bitbucket:pr-activity] {project}/{repo}#{pr_id}: (none)"
|
|
277
|
-
out = [f"[bitbucket:pr-activity] {project}/{repo}#{pr_id}
|
|
282
|
+
out = [f"[bitbucket:pr-activity] {project}/{repo}#{pr_id} - {len(acts)} event(s)"]
|
|
278
283
|
for a in acts[:50]:
|
|
279
284
|
out.append(_format_activity(a))
|
|
280
285
|
return "\n".join(out)
|
|
@@ -341,11 +346,11 @@ def _act_list_branches(client, project: str, repo: str, kwargs: dict) -> str:
|
|
|
341
346
|
branches = client.list_branches(project, repo, filter_text=kwargs.get("filter", ""))
|
|
342
347
|
if not branches:
|
|
343
348
|
return f"[bitbucket:branches] {project}/{repo}: (none)"
|
|
344
|
-
out = [f"[bitbucket:branches] {project}/{repo}
|
|
349
|
+
out = [f"[bitbucket:branches] {project}/{repo} - {len(branches)} branch(es)"]
|
|
345
350
|
for b in branches[:80]:
|
|
346
351
|
out.append(_format_branch(b))
|
|
347
352
|
if len(branches) > 80:
|
|
348
|
-
out.append(f"
|
|
353
|
+
out.append(f" ... +{len(branches) - 80} more")
|
|
349
354
|
return "\n".join(out)
|
|
350
355
|
|
|
351
356
|
|
|
@@ -378,7 +383,7 @@ def _act_list_commits(client, project: str, repo: str, kwargs: dict) -> str:
|
|
|
378
383
|
)
|
|
379
384
|
if not commits:
|
|
380
385
|
return f"[bitbucket:commits] {project}/{repo}: (none)"
|
|
381
|
-
out = [f"[bitbucket:commits] {project}/{repo}
|
|
386
|
+
out = [f"[bitbucket:commits] {project}/{repo} - {len(commits)} commit(s)"]
|
|
382
387
|
for c in commits:
|
|
383
388
|
out.append(_format_commit(c))
|
|
384
389
|
return "\n".join(out)
|
|
@@ -388,7 +393,7 @@ def _act_list_activity(client, project: str, repo: str, kwargs: dict) -> str:
|
|
|
388
393
|
acts = client.list_repo_activities(project, repo, limit=int(kwargs.get("limit", 30)))
|
|
389
394
|
if not acts:
|
|
390
395
|
return f"[bitbucket:activity] {project}/{repo}: (none)"
|
|
391
|
-
out = [f"[bitbucket:activity] {project}/{repo}
|
|
396
|
+
out = [f"[bitbucket:activity] {project}/{repo} - {len(acts)} event(s)"]
|
|
392
397
|
for a in acts:
|
|
393
398
|
out.append(_format_commit(a))
|
|
394
399
|
return "\n".join(out)
|
|
@@ -401,7 +406,7 @@ def _act_build_status(client, kwargs: dict) -> str:
|
|
|
401
406
|
builds = client.get_build_status(commit)
|
|
402
407
|
if not builds:
|
|
403
408
|
return f"[bitbucket:build_status] {commit[:12]}: (none)"
|
|
404
|
-
out = [f"[bitbucket:build_status] {commit[:12]}
|
|
409
|
+
out = [f"[bitbucket:build_status] {commit[:12]} - {len(builds)} build(s)"]
|
|
405
410
|
for b in builds:
|
|
406
411
|
out.append(_format_build(b))
|
|
407
412
|
return "\n".join(out)
|
|
@@ -429,10 +434,10 @@ def _act_list_webhooks(client, project: str, repo: str) -> str:
|
|
|
429
434
|
hooks = client.list_webhooks(project, repo)
|
|
430
435
|
if not hooks:
|
|
431
436
|
return f"[bitbucket:webhooks] {project}/{repo}: (none)"
|
|
432
|
-
out = [f"[bitbucket:webhooks] {project}/{repo}
|
|
437
|
+
out = [f"[bitbucket:webhooks] {project}/{repo} - {len(hooks)} hook(s)"]
|
|
433
438
|
for h in hooks:
|
|
434
439
|
out.append(
|
|
435
|
-
f" #{h.get('id')} [{'on' if h.get('active') else 'off'}] {h.get('name','?')}
|
|
440
|
+
f" #{h.get('id')} [{'on' if h.get('active') else 'off'}] {h.get('name','?')} -> {h.get('url','?')}"
|
|
436
441
|
)
|
|
437
442
|
evs = h.get("events") or []
|
|
438
443
|
if evs:
|
|
@@ -454,7 +459,7 @@ def _act_create_webhook(client, project: str, repo: str, kwargs: dict) -> str:
|
|
|
454
459
|
active=bool(kwargs.get("active", True)),
|
|
455
460
|
secret=kwargs.get("secret", ""),
|
|
456
461
|
)
|
|
457
|
-
return f"[bitbucket:webhook-created] {project}/{repo} #{res.get('id','?')} {name}
|
|
462
|
+
return f"[bitbucket:webhook-created] {project}/{repo} #{res.get('id','?')} {name} -> {url}"
|
|
458
463
|
|
|
459
464
|
|
|
460
465
|
def _act_delete_webhook(client, project: str, repo: str, kwargs: dict) -> str:
|
|
@@ -11,10 +11,12 @@ import os
|
|
|
11
11
|
import shutil
|
|
12
12
|
import subprocess
|
|
13
13
|
import sys
|
|
14
|
+
import threading
|
|
14
15
|
import time
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
17
18
|
from core import count_tokens
|
|
19
|
+
from services.circuit_breaker import CircuitBreaker
|
|
18
20
|
|
|
19
21
|
log = logging.getLogger(__name__)
|
|
20
22
|
|
|
@@ -252,11 +254,20 @@ def _handle_claude_delegate(task: str, task_type: str, context: str,
|
|
|
252
254
|
file_path: str, svc, dcfg: dict, finalize) -> str:
|
|
253
255
|
"""Handle delegation via Claude Code CLI."""
|
|
254
256
|
timeout = int(dcfg.get("claude_timeout", 90))
|
|
257
|
+
breaker = _backend_breaker("claude", dcfg)
|
|
258
|
+
if not breaker.allow():
|
|
259
|
+
return finalize("c3_delegate", {"task_type": task_type, "backend": "claude"},
|
|
260
|
+
"[delegate:degraded] Claude skipped after repeated failures; retrying in "
|
|
261
|
+
f"~{breaker.cooldown_remaining()}s. Run 'claude --version' to diagnose.",
|
|
262
|
+
"degraded")
|
|
255
263
|
_log_progress(svc, f"[delegate] Routing {task_type} → Claude CLI...")
|
|
256
264
|
output, ok = _run_claude(task, context, cwd=str(svc.project_path), timeout=timeout)
|
|
257
265
|
if not ok:
|
|
266
|
+
if breaker.record_failure():
|
|
267
|
+
_notify_backend_degraded(svc, "claude", breaker)
|
|
258
268
|
return finalize("c3_delegate", {"task_type": task_type, "backend": "claude"},
|
|
259
269
|
output, "error")
|
|
270
|
+
breaker.record_success()
|
|
260
271
|
return finalize("c3_delegate", {"task_type": task_type, "backend": "claude"},
|
|
261
272
|
output, "ok")
|
|
262
273
|
|
|
@@ -681,6 +692,51 @@ DELEGATE_TASKS = {
|
|
|
681
692
|
_delegate_cache: dict[str, tuple[str, int]] = {}
|
|
682
693
|
_delegate_metrics = {"total_calls": 0, "tokens_saved": 0}
|
|
683
694
|
|
|
695
|
+
# Per-backend runtime circuit breakers. Distinct from the install-status flags
|
|
696
|
+
# (_gemini_available etc., which only answer "is the CLI on PATH"): these track
|
|
697
|
+
# *runtime* health so a broken-but-installed backend (expired auth, repeated
|
|
698
|
+
# timeouts) stops re-spawning a 90-120s subprocess on every call. Keyed by
|
|
699
|
+
# backend name and intentionally process-global — backend health (auth, CLI
|
|
700
|
+
# version) is a property of the host, not of any single project.
|
|
701
|
+
_backend_breakers: dict[str, CircuitBreaker] = {}
|
|
702
|
+
_backend_breakers_lock = threading.Lock()
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _backend_breaker(name: str, dcfg: dict | None = None) -> CircuitBreaker:
|
|
706
|
+
"""Return (creating on first use) the runtime circuit breaker for a backend."""
|
|
707
|
+
with _backend_breakers_lock:
|
|
708
|
+
breaker = _backend_breakers.get(name)
|
|
709
|
+
if breaker is None:
|
|
710
|
+
cfg = dcfg or {}
|
|
711
|
+
breaker = CircuitBreaker(
|
|
712
|
+
name,
|
|
713
|
+
failure_threshold=int(cfg.get("breaker_failure_threshold", 3) or 3),
|
|
714
|
+
cooldown_seconds=float(cfg.get("breaker_cooldown_seconds", 60) or 60),
|
|
715
|
+
)
|
|
716
|
+
_backend_breakers[name] = breaker
|
|
717
|
+
return breaker
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _notify_backend_degraded(svc, name: str, breaker: CircuitBreaker) -> None:
|
|
721
|
+
"""Surface a backend trip via the NotificationStore (best-effort, never raises)."""
|
|
722
|
+
notifications = getattr(svc, "notifications", None)
|
|
723
|
+
if notifications is None:
|
|
724
|
+
return
|
|
725
|
+
try:
|
|
726
|
+
notifications.add(
|
|
727
|
+
agent="c3",
|
|
728
|
+
severity="warning",
|
|
729
|
+
title=f"Delegate backend degraded: {name}",
|
|
730
|
+
message=(
|
|
731
|
+
f"{name} failed {breaker.failure_threshold}x consecutively; c3_delegate "
|
|
732
|
+
f"will skip it for ~{int(breaker.cooldown_seconds)}s instead of re-spawning "
|
|
733
|
+
f"the CLI. Run '{name} --version' to diagnose."
|
|
734
|
+
),
|
|
735
|
+
replace_if_unacked=True,
|
|
736
|
+
)
|
|
737
|
+
except Exception:
|
|
738
|
+
pass
|
|
739
|
+
|
|
684
740
|
|
|
685
741
|
def get_delegate_metrics() -> dict:
|
|
686
742
|
return dict(_delegate_metrics)
|
|
@@ -765,6 +821,13 @@ def _handle_codex_delegate(task: str, task_type: str, context: str,
|
|
|
765
821
|
"[delegate:error] Codex CLI not available. Run 'codex --version' to diagnose.",
|
|
766
822
|
"unavailable")
|
|
767
823
|
|
|
824
|
+
breaker = _backend_breaker("codex", dcfg)
|
|
825
|
+
if not breaker.allow():
|
|
826
|
+
return finalize("c3_delegate", {"task_type": task_type, "backend": "codex"},
|
|
827
|
+
"[delegate:degraded] Codex skipped after repeated failures; retrying in "
|
|
828
|
+
f"~{breaker.cooldown_remaining()}s. Run 'codex --version' to diagnose.",
|
|
829
|
+
"degraded")
|
|
830
|
+
|
|
768
831
|
# Resolve model/sandbox/reasoning from config or defaults
|
|
769
832
|
cdef = CODEX_MODELS.get(task_type, CODEX_MODELS.get("ask", {}))
|
|
770
833
|
model = dcfg.get("codex_default_model") or cdef.get("model", "gpt-5.3-codex-spark")
|
|
@@ -807,10 +870,13 @@ def _handle_codex_delegate(task: str, task_type: str, context: str,
|
|
|
807
870
|
elapsed = round(time.monotonic() - t0, 1)
|
|
808
871
|
|
|
809
872
|
if not ok:
|
|
873
|
+
if breaker.record_failure():
|
|
874
|
+
_notify_backend_degraded(svc, "codex", breaker)
|
|
810
875
|
return finalize("c3_delegate",
|
|
811
876
|
{"task_type": task_type, "backend": "codex", "model": model, "elapsed": f"{elapsed}s"},
|
|
812
877
|
output, "error")
|
|
813
878
|
|
|
879
|
+
breaker.record_success()
|
|
814
880
|
_delegate_metrics["total_calls"] += 1
|
|
815
881
|
_delegate_cache[ckey] = (output, count_tokens(output))
|
|
816
882
|
|
|
@@ -880,6 +946,13 @@ def _handle_gemini_delegate(task: str, task_type: str, context: str,
|
|
|
880
946
|
"[delegate:error] Gemini CLI not available. Run 'gemini --version' to diagnose.",
|
|
881
947
|
"unavailable")
|
|
882
948
|
|
|
949
|
+
breaker = _backend_breaker("gemini", dcfg)
|
|
950
|
+
if not breaker.allow():
|
|
951
|
+
return finalize("c3_delegate", {"task_type": task_type, "backend": "gemini"},
|
|
952
|
+
"[delegate:degraded] Gemini skipped after repeated failures; retrying in "
|
|
953
|
+
f"~{breaker.cooldown_remaining()}s. Run 'gemini --version' to diagnose.",
|
|
954
|
+
"degraded")
|
|
955
|
+
|
|
883
956
|
# Resolve model from config or defaults
|
|
884
957
|
gdef = GEMINI_MODELS.get(task_type, GEMINI_MODELS.get("ask", {}))
|
|
885
958
|
model = dcfg.get("gemini_default_model") or gdef.get("model", "gemini-2.5-flash")
|
|
@@ -919,10 +992,13 @@ def _handle_gemini_delegate(task: str, task_type: str, context: str,
|
|
|
919
992
|
elapsed = round(time.monotonic() - t0, 1)
|
|
920
993
|
|
|
921
994
|
if not ok:
|
|
995
|
+
if breaker.record_failure():
|
|
996
|
+
_notify_backend_degraded(svc, "gemini", breaker)
|
|
922
997
|
return finalize("c3_delegate",
|
|
923
998
|
{"task_type": task_type, "backend": "gemini", "model": model, "elapsed": f"{elapsed}s"},
|
|
924
999
|
output, "error")
|
|
925
1000
|
|
|
1001
|
+
breaker.record_success()
|
|
926
1002
|
_delegate_metrics["total_calls"] += 1
|
|
927
1003
|
_delegate_cache[ckey] = (output, count_tokens(output))
|
|
928
1004
|
|
|
@@ -1068,9 +1144,11 @@ def handle_delegate(task: str, task_type: str, context: str, file_path: str,
|
|
|
1068
1144
|
_gemini_avail = (_gemini_available is True) or (
|
|
1069
1145
|
_gemini_available is None and task_type not in _light_tasks and _is_gemini_on_path()
|
|
1070
1146
|
)
|
|
1071
|
-
if task_type in heavy_codex and _codex_avail and _codex_available is not False
|
|
1147
|
+
if (task_type in heavy_codex and _codex_avail and _codex_available is not False
|
|
1148
|
+
and _backend_breaker("codex", dcfg).allow()):
|
|
1072
1149
|
backend = "codex"
|
|
1073
|
-
elif task_type in heavy_gemini and _gemini_avail and _gemini_available is not False
|
|
1150
|
+
elif (task_type in heavy_gemini and _gemini_avail and _gemini_available is not False
|
|
1151
|
+
and _backend_breaker("gemini", dcfg).allow()):
|
|
1074
1152
|
backend = "gemini"
|
|
1075
1153
|
else:
|
|
1076
1154
|
backend = "ollama"
|
{code_context_control-2.39.0 → code_context_control-2.40.0/code_context_control.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-context-control
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.40.0
|
|
4
4
|
Summary: Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer.
|
|
5
5
|
Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -283,9 +283,12 @@ Access Token. Tokens live in the **OS keyring** (Windows Credential Manager,
|
|
|
283
283
|
macOS Keychain, Linux Secret Service) — never in `.c3/config.json`.
|
|
284
284
|
|
|
285
285
|
```bash
|
|
286
|
-
# One-time login per server
|
|
286
|
+
# One-time login per server (stored under this project's .c3/config.json)
|
|
287
287
|
c3 bitbucket login --url https://bitbucket.example.com
|
|
288
|
-
#
|
|
288
|
+
# -> prompts for username + PAT (masked)
|
|
289
|
+
|
|
290
|
+
# ...or store it globally so every C3 project can use it
|
|
291
|
+
c3 bitbucket login --global --url https://bitbucket.example.com
|
|
289
292
|
|
|
290
293
|
# Pin defaults so subsequent calls don't need project/repo
|
|
291
294
|
c3 bitbucket set-default --project PROJ --repo my-service
|
|
@@ -294,6 +297,16 @@ c3 bitbucket set-default --project PROJ --repo my-service
|
|
|
294
297
|
c3 bitbucket status
|
|
295
298
|
```
|
|
296
299
|
|
|
300
|
+
**Account resolution precedence:** the project's `.c3/config.json` wins, but when
|
|
301
|
+
it has no active account C3 falls back to the global `~/.c3/config.json`. So a
|
|
302
|
+
single `login --global` (or any login done from your home directory) is reusable
|
|
303
|
+
across every C3 project — the PAT always lives in the OS keyring, never on disk.
|
|
304
|
+
|
|
305
|
+
> **Upgrading:** stop the running `c3-mcp` server / CLI before `c3 upgrade`. A live
|
|
306
|
+
> process can hold package files open, leaving pip's `~`-prefixed backup dirs
|
|
307
|
+
> (`~ervices`, `~ools`, …) in `site-packages`; those are inert and safe to delete
|
|
308
|
+
> after the upgrade completes.
|
|
309
|
+
|
|
297
310
|
The MCP tool dispatches by `action`. Read-only actions: `status`, `whoami`,
|
|
298
311
|
`list_projects`, `list_repos`, `get_repo`, `list_prs`, `get_pr`, `get_pr_diff`,
|
|
299
312
|
`get_pr_activities`, `list_branches`, `list_commits`, `list_activity`,
|
|
@@ -108,6 +108,7 @@ services/auto_memory.py
|
|
|
108
108
|
services/benchmark_dashboard.py
|
|
109
109
|
services/bitbucket_client.py
|
|
110
110
|
services/bitbucket_credentials.py
|
|
111
|
+
services/circuit_breaker.py
|
|
111
112
|
services/claude_md.py
|
|
112
113
|
services/compressor.py
|
|
113
114
|
services/context_snapshot.py
|
|
@@ -158,9 +159,11 @@ tests/test_activity_reporter.py
|
|
|
158
159
|
tests/test_aider_polyglot.py
|
|
159
160
|
tests/test_bitbucket_cli_smoke.py
|
|
160
161
|
tests/test_bitbucket_client.py
|
|
162
|
+
tests/test_bitbucket_config_fallback.py
|
|
161
163
|
tests/test_bitbucket_credentials.py
|
|
162
164
|
tests/test_bitbucket_tool.py
|
|
163
165
|
tests/test_c3_shell.py
|
|
166
|
+
tests/test_circuit_breaker.py
|
|
164
167
|
tests/test_claude_md_merge.py
|
|
165
168
|
tests/test_cli_smoke.py
|
|
166
169
|
tests/test_e2e_benchmark.py
|
|
@@ -290,15 +290,41 @@ BITBUCKET_DEFAULTS = {
|
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
|
|
293
|
+
def _read_bitbucket_section(config_file: Path) -> dict:
|
|
294
|
+
"""Return the ``bitbucket`` section of a config file, or ``{}``."""
|
|
295
|
+
if not config_file.exists():
|
|
296
|
+
return {}
|
|
297
|
+
try:
|
|
298
|
+
with open(config_file, encoding="utf-8") as f:
|
|
299
|
+
data = json.load(f)
|
|
300
|
+
except Exception:
|
|
301
|
+
return {}
|
|
302
|
+
section = data.get("bitbucket", {})
|
|
303
|
+
return section if isinstance(section, dict) else {}
|
|
304
|
+
|
|
305
|
+
|
|
293
306
|
def load_bitbucket_config(project_path: str) -> dict:
|
|
294
|
-
"""Load Bitbucket config from .c3/config.json, merged with defaults.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
307
|
+
"""Load Bitbucket config from .c3/config.json, merged with defaults.
|
|
308
|
+
|
|
309
|
+
Resolution precedence: the project ``<project>/.c3/config.json`` wins, but
|
|
310
|
+
when it has no active account we fall back to the global
|
|
311
|
+
``~/.c3/config.json`` so a one-time ``c3 bitbucket login`` (or
|
|
312
|
+
``login --global``) is reusable across every C3 project. The PAT itself
|
|
313
|
+
always lives in the OS keyring, never in these files.
|
|
314
|
+
"""
|
|
315
|
+
project_file = Path(project_path) / ".c3" / "config.json"
|
|
316
|
+
overrides = _read_bitbucket_section(project_file)
|
|
317
|
+
if not (overrides.get("active") or {}).get("base_url"):
|
|
318
|
+
# Path.home() raises RuntimeError when no home dir is resolvable (e.g.
|
|
319
|
+
# a stripped subprocess env); treat that as "no global fallback".
|
|
298
320
|
try:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
overrides = data.get("bitbucket", {})
|
|
321
|
+
home_file = Path.home() / ".c3" / "config.json"
|
|
322
|
+
already_home = home_file.resolve() == project_file.resolve()
|
|
302
323
|
except Exception:
|
|
303
|
-
|
|
324
|
+
home_file = None
|
|
325
|
+
already_home = True
|
|
326
|
+
if home_file is not None and not already_home:
|
|
327
|
+
home_overrides = _read_bitbucket_section(home_file)
|
|
328
|
+
if (home_overrides.get("active") or {}).get("base_url"):
|
|
329
|
+
overrides = home_overrides
|
|
304
330
|
return {**BITBUCKET_DEFAULTS, **overrides}
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "code-context-control"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.40.0"
|
|
8
8
|
description = "Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|