code-context-control 2.39.1__tar.gz → 2.41.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.1/code_context_control.egg-info → code_context_control-2.41.0}/PKG-INFO +16 -3
- {code_context_control-2.39.1 → code_context_control-2.41.0}/README.md +15 -2
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/c3.py +36 -11
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/commands/parser.py +1 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/tools/bitbucket.py +25 -20
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/tools/shell.py +71 -1
- {code_context_control-2.39.1 → code_context_control-2.41.0/code_context_control.egg-info}/PKG-INFO +16 -3
- {code_context_control-2.39.1 → code_context_control-2.41.0}/code_context_control.egg-info/SOURCES.txt +1 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/core/config.py +34 -8
- {code_context_control-2.39.1 → code_context_control-2.41.0}/pyproject.toml +1 -1
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/bitbucket_client.py +46 -11
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/claude_md.py +1 -1
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_bitbucket_client.py +63 -4
- code_context_control-2.41.0/tests/test_bitbucket_config_fallback.py +70 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_bitbucket_tool.py +10 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_c3_shell.py +75 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/LICENSE +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/__init__.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/_hook_utils.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/commands/__init__.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/commands/common.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/docs.html +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/edits.html +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/guide/bitbucket.html +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/guide/getting-started.html +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/guide/index.html +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/guide/oracle.html +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/guide/shared.css +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/guide/tools.html +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/guide/workflow.html +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/hook_auto_snapshot.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/hook_c3_signal.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/hook_c3read.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/hook_edit_ledger.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/hook_edit_unlock.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/hook_filter.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/hook_ghost_files.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/hook_pretool_enforce.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/hook_read.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/hook_session_stats.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/hook_terse_advisor.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/hub.html +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/hub_server.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/mcp_proxy.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/mcp_server.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/server.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/tools/__init__.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/tools/_helpers.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/tools/agent.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/tools/compress.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/tools/delegate.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/tools/edit.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/tools/edits.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/tools/filter.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/tools/impact.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/tools/memory.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/tools/project.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/tools/read.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/tools/search.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/tools/session.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/tools/status.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/tools/validate.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/ui/api.js +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/ui/app.js +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/ui/components/bitbucket.js +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/ui/components/chat.js +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/ui/components/dashboard.js +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/ui/components/edits.js +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/ui/components/instructions.js +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/ui/components/memory.js +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/ui/components/sessions.js +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/ui/components/settings.js +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/ui/components/sidebar.js +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/ui/icons.js +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/ui/shared.js +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/ui/theme.js +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/ui.html +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/ui_legacy.html +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/cli/ui_nano.html +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/code_context_control.egg-info/dependency_links.txt +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/code_context_control.egg-info/entry_points.txt +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/code_context_control.egg-info/requires.txt +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/code_context_control.egg-info/top_level.txt +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/core/__init__.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/core/ide.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/core/mcp_toml.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/core/web_security.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/__init__.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/config.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/mcp_oracle.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/oracle.html +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/oracle_server.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/services/__init__.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/services/activity_reporter.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/services/api_auth.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/services/c3_bridge.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/services/chat_engine.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/services/chat_store.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/services/cross_memory.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/services/federated_graph.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/services/health_checker.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/services/insight_engine.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/services/memory_reader.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/services/memory_writer.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/services/ollama_bridge.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/services/project_scanner.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/services/review_agent.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/services/tool_executor.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/oracle/services/tool_registry.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/__init__.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/activity_log.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/agent_base.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/agents.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/auto_memory.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/bench/__init__.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/bench/external/__init__.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/bench/external/aider_polyglot.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/bench/external/swe_bench.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/benchmark_dashboard.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/bitbucket_credentials.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/circuit_breaker.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/compressor.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/context_snapshot.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/conversation_store.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/doc_index.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/e2e_benchmark.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/e2e_evaluator.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/e2e_tasks.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/edit_ledger.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/embedding_index.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/error_reporting.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/file_memory.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/git_context.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/hub_service.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/indexer.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/memory.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/memory_consolidator.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/memory_graph.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/memory_grounder.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/memory_scorer.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/metrics.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/notifications.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/ollama_client.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/output_filter.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/parser.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/project_manager.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/project_runtime.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/protocol.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/proxy_state.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/retrieval_broker.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/router.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/runtime.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/session_benchmark.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/session_manager.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/session_preloader.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/text_index.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/tool_classifier.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/transcript_index.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/validation_cache.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/vector_store.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/version_tracker.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/services/watcher.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/setup.cfg +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_activity_reporter.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_aider_polyglot.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_bitbucket_cli_smoke.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_bitbucket_credentials.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_circuit_breaker.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_claude_md_merge.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_cli_smoke.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_e2e_benchmark.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_edit_ledger_hook.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_edit_normalization.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_enforcement_flip.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_federated_graph.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_ghost_files.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_git_branch_awareness.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_hub_server_smoke.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_install_mcp_entrypoint.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_lazy_store_init.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_mcp_host_guard.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_mcp_server_smoke.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_mcp_toml.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_memory_graph_api.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_memory_system.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_notification_discipline.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_oracle_api_auth.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_oracle_apikey_api.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_oracle_discovery_api.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_oracle_security_fixes.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_output_filter.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_permissions.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_project_manager.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_project_manager_merge.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_project_tool.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_read_coercion.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_service_durability.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_session_benchmark.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_session_budget.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_shell_robustness.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_swe_bench.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_tool_registry.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_upgrade_and_version.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_validate.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_web_security.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tests/test_windows_reliability.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tui/__init__.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tui/backend.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tui/main.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tui/screens/__init__.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tui/screens/benchmark_view.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tui/screens/claudemd_view.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tui/screens/compress_view.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tui/screens/index_view.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tui/screens/init_view.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tui/screens/mcp_view.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tui/screens/optimize_view.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tui/screens/pipe_view.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tui/screens/projects_view.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tui/screens/search_view.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tui/screens/session_view.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tui/screens/stats.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tui/screens/ui_view.py +0 -0
- {code_context_control-2.39.1 → code_context_control-2.41.0}/tui/theme.tcss +0 -0
{code_context_control-2.39.1/code_context_control.egg-info → code_context_control-2.41.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.41.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.41.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:
|
|
@@ -4,12 +4,21 @@ Wraps subprocess.Popen with the Windows-safe pattern from
|
|
|
4
4
|
services/edit_ledger.py::_git_combined (Popen + taskkill /F /T + stdin=DEVNULL).
|
|
5
5
|
Auto-filters long stdout via handle_filter, auto-logs git mutations
|
|
6
6
|
to the edit ledger, and accounts stdout tokens against session budget.
|
|
7
|
+
|
|
8
|
+
Shell selection: on Windows, commands are run through Git Bash (bash.exe) when
|
|
9
|
+
it is available, so c3_shell speaks the same POSIX dialect as the native Bash
|
|
10
|
+
tool (forward-slash paths, single quotes, `$VAR`, ls/grep/cat, heredocs). This
|
|
11
|
+
avoids the cmd.exe/POSIX mismatch that forced callers to fall back to native
|
|
12
|
+
Bash for bash-flavored commands. Set C3_SHELL_BASH=0 to force cmd.exe, or point
|
|
13
|
+
C3_SHELL_BASH at a specific bash.exe to override discovery. POSIX platforms are
|
|
14
|
+
unchanged (shell=True → /bin/sh).
|
|
7
15
|
"""
|
|
8
16
|
from __future__ import annotations
|
|
9
17
|
|
|
10
18
|
import asyncio
|
|
11
19
|
import os
|
|
12
20
|
import re
|
|
21
|
+
import shutil
|
|
13
22
|
import subprocess
|
|
14
23
|
import sys
|
|
15
24
|
import time
|
|
@@ -59,6 +68,58 @@ _MAX_TIMEOUT = 600
|
|
|
59
68
|
_FILTER_THRESHOLD_LINES = 30
|
|
60
69
|
|
|
61
70
|
|
|
71
|
+
# Cache for discovered Git Bash path: [] = uncomputed, [None]/[path] = computed.
|
|
72
|
+
_bash_cache: list = []
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _discover_git_bash() -> str | None:
|
|
76
|
+
"""Locate a Git-for-Windows bash.exe, never WSL/System32 bash.
|
|
77
|
+
|
|
78
|
+
WSL's bash runs in a Linux subsystem with /mnt/c paths, which would break
|
|
79
|
+
the Windows `cwd` semantics every caller relies on — so it is rejected.
|
|
80
|
+
"""
|
|
81
|
+
candidates: list[str] = []
|
|
82
|
+
for base_env in ("ProgramW6432", "ProgramFiles", "ProgramFiles(x86)"):
|
|
83
|
+
base = os.environ.get(base_env)
|
|
84
|
+
if base:
|
|
85
|
+
candidates.append(os.path.join(base, "Git", "bin", "bash.exe"))
|
|
86
|
+
candidates.append(os.path.join(base, "Git", "usr", "bin", "bash.exe"))
|
|
87
|
+
candidates.append(r"C:\Program Files\Git\bin\bash.exe")
|
|
88
|
+
candidates.append(r"C:\Program Files\Git\usr\bin\bash.exe")
|
|
89
|
+
for path in candidates:
|
|
90
|
+
if os.path.isfile(path):
|
|
91
|
+
return path
|
|
92
|
+
# Last resort: PATH lookup, but reject WSL/Store bash (System32/WindowsApps).
|
|
93
|
+
found = shutil.which("bash")
|
|
94
|
+
if found:
|
|
95
|
+
low = found.lower()
|
|
96
|
+
if "system32" not in low and "windowsapps" not in low:
|
|
97
|
+
return found
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _select_bash() -> str | None:
|
|
102
|
+
"""Return the bash.exe to run commands through on Windows, else None.
|
|
103
|
+
|
|
104
|
+
None means "use the platform default shell" (cmd.exe on Windows via
|
|
105
|
+
shell=True, /bin/sh on POSIX). Honors the C3_SHELL_BASH override:
|
|
106
|
+
'0'/'cmd' forces the platform default; an existing file path forces that
|
|
107
|
+
bash. Discovery is cached after the first call.
|
|
108
|
+
"""
|
|
109
|
+
if sys.platform != "win32":
|
|
110
|
+
return None
|
|
111
|
+
override = os.environ.get("C3_SHELL_BASH")
|
|
112
|
+
if override is not None:
|
|
113
|
+
if override.strip().lower() in ("0", "", "cmd", "false", "off"):
|
|
114
|
+
return None
|
|
115
|
+
if os.path.isfile(override):
|
|
116
|
+
return override
|
|
117
|
+
# Unrecognized override → fall through to discovery.
|
|
118
|
+
if not _bash_cache:
|
|
119
|
+
_bash_cache.append(_discover_git_bash())
|
|
120
|
+
return _bash_cache[0]
|
|
121
|
+
|
|
122
|
+
|
|
62
123
|
def _popen_kwargs() -> dict:
|
|
63
124
|
# Force UTF-8 in child processes so Unicode output (→, box-drawing, emoji)
|
|
64
125
|
# doesn't crash on Windows' legacy cp1252 console encoding. setdefault so an
|
|
@@ -89,8 +150,17 @@ def _kill_tree(proc: subprocess.Popen) -> None:
|
|
|
89
150
|
def _run_sync(cmd: str, cwd: str, timeout: int) -> dict:
|
|
90
151
|
"""Blocking subprocess run with hard kill on timeout. Returns structured dict."""
|
|
91
152
|
start = time.time()
|
|
153
|
+
bash = _select_bash()
|
|
154
|
+
if bash:
|
|
155
|
+
# POSIX dialect via Git Bash — matches the native Bash tool.
|
|
156
|
+
popen_target: object = [bash, "-c", cmd]
|
|
157
|
+
use_shell = False
|
|
158
|
+
else:
|
|
159
|
+
# Platform default: cmd.exe on Windows, /bin/sh on POSIX.
|
|
160
|
+
popen_target = cmd
|
|
161
|
+
use_shell = True
|
|
92
162
|
proc = subprocess.Popen(
|
|
93
|
-
|
|
163
|
+
popen_target, shell=use_shell, cwd=cwd,
|
|
94
164
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
95
165
|
text=True, encoding="utf-8", errors="replace",
|
|
96
166
|
**_popen_kwargs(),
|
{code_context_control-2.39.1 → code_context_control-2.41.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.41.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`,
|
|
@@ -159,6 +159,7 @@ tests/test_activity_reporter.py
|
|
|
159
159
|
tests/test_aider_polyglot.py
|
|
160
160
|
tests/test_bitbucket_cli_smoke.py
|
|
161
161
|
tests/test_bitbucket_client.py
|
|
162
|
+
tests/test_bitbucket_config_fallback.py
|
|
162
163
|
tests/test_bitbucket_credentials.py
|
|
163
164
|
tests/test_bitbucket_tool.py
|
|
164
165
|
tests/test_c3_shell.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.41.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"
|
|
@@ -28,7 +28,15 @@ _TIMEOUT = 30 # seconds
|
|
|
28
28
|
_API = "/rest/api/1.0"
|
|
29
29
|
_BUILD_API = "/rest/build-status/1.0"
|
|
30
30
|
_BRANCH_UTILS = "/rest/branch-utils/1.0"
|
|
31
|
-
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from importlib.metadata import version as _pkg_version
|
|
34
|
+
|
|
35
|
+
_C3_VERSION = _pkg_version("code-context-control")
|
|
36
|
+
except Exception: # pragma: no cover - package metadata may be unavailable
|
|
37
|
+
_C3_VERSION = "0.0.0"
|
|
38
|
+
|
|
39
|
+
_USER_AGENT = f"c3-bitbucket/{_C3_VERSION}"
|
|
32
40
|
|
|
33
41
|
|
|
34
42
|
class BitbucketError(RuntimeError):
|
|
@@ -80,6 +88,8 @@ class BitbucketDataCenterClient:
|
|
|
80
88
|
params: dict | None = None,
|
|
81
89
|
api_root: str = _API,
|
|
82
90
|
raw: bool = False,
|
|
91
|
+
accept: str = "application/json",
|
|
92
|
+
return_headers: bool = False,
|
|
83
93
|
) -> Any:
|
|
84
94
|
full_path = f"{api_root}{path}"
|
|
85
95
|
if params:
|
|
@@ -92,7 +102,7 @@ class BitbucketDataCenterClient:
|
|
|
92
102
|
|
|
93
103
|
headers = {
|
|
94
104
|
"Authorization": f"Bearer {self._token}",
|
|
95
|
-
"Accept":
|
|
105
|
+
"Accept": accept,
|
|
96
106
|
"User-Agent": _USER_AGENT,
|
|
97
107
|
}
|
|
98
108
|
data: bytes | None = None
|
|
@@ -109,14 +119,19 @@ class BitbucketDataCenterClient:
|
|
|
109
119
|
try:
|
|
110
120
|
with urllib.request.urlopen(req, timeout=self._timeout, context=self._ssl_context()) as resp:
|
|
111
121
|
payload = resp.read()
|
|
122
|
+
resp_headers = {k: v for k, v in resp.headers.items()}
|
|
112
123
|
if raw:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
body_out: Any = payload
|
|
125
|
+
elif not payload:
|
|
126
|
+
body_out = {}
|
|
127
|
+
else:
|
|
128
|
+
try:
|
|
129
|
+
body_out = json.loads(payload.decode("utf-8"))
|
|
130
|
+
except json.JSONDecodeError:
|
|
131
|
+
body_out = {"raw": payload.decode("utf-8", errors="replace")}
|
|
132
|
+
if return_headers:
|
|
133
|
+
return body_out, resp_headers
|
|
134
|
+
return body_out
|
|
120
135
|
except urllib.error.HTTPError as exc:
|
|
121
136
|
try:
|
|
122
137
|
err_payload = json.loads(exc.read().decode("utf-8"))
|
|
@@ -168,8 +183,27 @@ class BitbucketDataCenterClient:
|
|
|
168
183
|
return self._request("GET", "/application-properties")
|
|
169
184
|
|
|
170
185
|
def whoami(self) -> dict:
|
|
171
|
-
"""The authenticated user (PAT owner).
|
|
172
|
-
|
|
186
|
+
"""The authenticated user (PAT owner).
|
|
187
|
+
|
|
188
|
+
Bitbucket Data Center / Server has no ``/users/me`` endpoint (that is a
|
|
189
|
+
Cloud convention; DC treats ``me`` as a literal username and 404s).
|
|
190
|
+
Resolve the account from the ``X-AUSERNAME`` header that rides on every
|
|
191
|
+
authenticated response, then enrich with the user record when possible.
|
|
192
|
+
"""
|
|
193
|
+
_body, headers = self._request(
|
|
194
|
+
"GET", "/application-properties", return_headers=True
|
|
195
|
+
)
|
|
196
|
+
slug = ""
|
|
197
|
+
for key, value in (headers or {}).items():
|
|
198
|
+
if key.lower() == "x-ausername":
|
|
199
|
+
slug = urllib.parse.unquote(value or "")
|
|
200
|
+
break
|
|
201
|
+
if not slug:
|
|
202
|
+
return {}
|
|
203
|
+
try:
|
|
204
|
+
return self._request("GET", f"/users/{urllib.parse.quote(slug)}")
|
|
205
|
+
except BitbucketError:
|
|
206
|
+
return {"name": slug, "slug": slug, "displayName": slug}
|
|
173
207
|
|
|
174
208
|
# ── Projects & repos (read) ───────────────────────────
|
|
175
209
|
|
|
@@ -282,6 +316,7 @@ class BitbucketDataCenterClient:
|
|
|
282
316
|
f"/projects/{project_key}/repos/{repo_slug}/pull-requests/{pr_id}/diff",
|
|
283
317
|
params={"contextLines": context_lines},
|
|
284
318
|
raw=True,
|
|
319
|
+
accept="text/plain",
|
|
285
320
|
)
|
|
286
321
|
if isinstance(payload, bytes):
|
|
287
322
|
return payload.decode("utf-8", errors="replace")
|