code-context-control 2.41.0__tar.gz → 2.42.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.41.0 → code_context_control-2.42.0}/PKG-INFO +3 -3
- {code_context_control-2.41.0 → code_context_control-2.42.0}/README.md +2 -2
- code_context_control-2.42.0/cli/_hook_utils.py +304 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/c3.py +57 -139
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/hook_auto_snapshot.py +18 -10
- code_context_control-2.42.0/cli/hook_c3_signal.py +68 -0
- code_context_control-2.42.0/cli/hook_c3read.py +108 -0
- code_context_control-2.42.0/cli/hook_dispatch.py +265 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/hook_edit_ledger.py +91 -76
- code_context_control-2.42.0/cli/hook_edit_unlock.py +189 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/hook_filter.py +58 -43
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/hook_ghost_files.py +33 -23
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/hook_pretool_enforce.py +101 -106
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/hook_read.py +110 -98
- code_context_control-2.42.0/cli/hook_session_stats.py +69 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/hook_terse_advisor.py +61 -49
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/tools/_helpers.py +40 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/tools/delegate.py +90 -22
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/tools/read.py +22 -8
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/tools/status.py +44 -3
- {code_context_control-2.41.0 → code_context_control-2.42.0}/code_context_control.egg-info/PKG-INFO +3 -3
- {code_context_control-2.41.0 → code_context_control-2.42.0}/code_context_control.egg-info/SOURCES.txt +11 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/pyproject.toml +1 -1
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/agents.py +18 -1
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/compressor.py +166 -6
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/notifications.py +101 -8
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/output_filter.py +67 -8
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/session_benchmark.py +195 -108
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/session_manager.py +152 -10
- code_context_control-2.42.0/services/telemetry.py +236 -0
- code_context_control-2.42.0/tests/test_compressor_large_file.py +322 -0
- code_context_control-2.42.0/tests/test_delegate_cascade.py +246 -0
- code_context_control-2.42.0/tests/test_filter_backoff.py +156 -0
- code_context_control-2.42.0/tests/test_hook_dispatch.py +261 -0
- code_context_control-2.42.0/tests/test_hook_pretool_enforce.py +251 -0
- code_context_control-2.42.0/tests/test_hook_smoke.py +216 -0
- code_context_control-2.42.0/tests/test_hook_state.py +168 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_install_mcp_entrypoint.py +75 -1
- code_context_control-2.42.0/tests/test_notification_dedup.py +269 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_session_benchmark.py +30 -0
- code_context_control-2.42.0/tests/test_token_telemetry.py +332 -0
- code_context_control-2.41.0/cli/_hook_utils.py +0 -136
- code_context_control-2.41.0/cli/hook_c3_signal.py +0 -61
- code_context_control-2.41.0/cli/hook_c3read.py +0 -116
- code_context_control-2.41.0/cli/hook_edit_unlock.py +0 -178
- code_context_control-2.41.0/cli/hook_session_stats.py +0 -62
- {code_context_control-2.41.0 → code_context_control-2.42.0}/LICENSE +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/__init__.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/commands/__init__.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/commands/common.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/commands/parser.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/docs.html +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/edits.html +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/guide/bitbucket.html +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/guide/getting-started.html +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/guide/index.html +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/guide/oracle.html +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/guide/shared.css +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/guide/tools.html +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/guide/workflow.html +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/hub.html +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/hub_server.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/mcp_proxy.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/mcp_server.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/server.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/tools/__init__.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/tools/agent.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/tools/bitbucket.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/tools/compress.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/tools/edit.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/tools/edits.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/tools/filter.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/tools/impact.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/tools/memory.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/tools/project.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/tools/search.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/tools/session.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/tools/shell.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/tools/validate.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/ui/api.js +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/ui/app.js +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/ui/components/bitbucket.js +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/ui/components/chat.js +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/ui/components/dashboard.js +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/ui/components/edits.js +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/ui/components/instructions.js +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/ui/components/memory.js +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/ui/components/sessions.js +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/ui/components/settings.js +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/ui/components/sidebar.js +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/ui/icons.js +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/ui/shared.js +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/ui/theme.js +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/ui.html +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/ui_legacy.html +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/cli/ui_nano.html +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/code_context_control.egg-info/dependency_links.txt +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/code_context_control.egg-info/entry_points.txt +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/code_context_control.egg-info/requires.txt +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/code_context_control.egg-info/top_level.txt +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/core/__init__.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/core/config.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/core/ide.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/core/mcp_toml.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/core/web_security.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/__init__.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/config.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/mcp_oracle.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/oracle.html +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/oracle_server.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/services/__init__.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/services/activity_reporter.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/services/api_auth.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/services/c3_bridge.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/services/chat_engine.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/services/chat_store.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/services/cross_memory.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/services/federated_graph.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/services/health_checker.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/services/insight_engine.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/services/memory_reader.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/services/memory_writer.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/services/ollama_bridge.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/services/project_scanner.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/services/review_agent.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/services/tool_executor.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/oracle/services/tool_registry.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/__init__.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/activity_log.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/agent_base.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/auto_memory.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/bench/__init__.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/bench/external/__init__.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/bench/external/aider_polyglot.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/bench/external/swe_bench.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/benchmark_dashboard.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/bitbucket_client.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/bitbucket_credentials.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/circuit_breaker.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/claude_md.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/context_snapshot.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/conversation_store.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/doc_index.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/e2e_benchmark.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/e2e_evaluator.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/e2e_tasks.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/edit_ledger.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/embedding_index.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/error_reporting.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/file_memory.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/git_context.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/hub_service.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/indexer.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/memory.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/memory_consolidator.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/memory_graph.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/memory_grounder.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/memory_scorer.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/metrics.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/ollama_client.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/parser.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/project_manager.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/project_runtime.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/protocol.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/proxy_state.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/retrieval_broker.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/router.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/runtime.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/session_preloader.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/text_index.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/tool_classifier.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/transcript_index.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/validation_cache.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/vector_store.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/version_tracker.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/services/watcher.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/setup.cfg +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_activity_reporter.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_aider_polyglot.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_bitbucket_cli_smoke.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_bitbucket_client.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_bitbucket_config_fallback.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_bitbucket_credentials.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_bitbucket_tool.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_c3_shell.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_circuit_breaker.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_claude_md_merge.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_cli_smoke.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_e2e_benchmark.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_edit_ledger_hook.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_edit_normalization.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_enforcement_flip.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_federated_graph.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_ghost_files.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_git_branch_awareness.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_hub_server_smoke.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_lazy_store_init.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_mcp_host_guard.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_mcp_server_smoke.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_mcp_toml.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_memory_graph_api.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_memory_system.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_notification_discipline.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_oracle_api_auth.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_oracle_apikey_api.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_oracle_discovery_api.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_oracle_security_fixes.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_output_filter.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_permissions.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_project_manager.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_project_manager_merge.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_project_tool.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_read_coercion.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_service_durability.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_session_budget.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_shell_robustness.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_swe_bench.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_tool_registry.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_upgrade_and_version.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_validate.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_web_security.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tests/test_windows_reliability.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tui/__init__.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tui/backend.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tui/main.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tui/screens/__init__.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tui/screens/benchmark_view.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tui/screens/claudemd_view.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tui/screens/compress_view.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tui/screens/index_view.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tui/screens/init_view.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tui/screens/mcp_view.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tui/screens/optimize_view.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tui/screens/pipe_view.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tui/screens/projects_view.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tui/screens/search_view.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tui/screens/session_view.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tui/screens/stats.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tui/screens/ui_view.py +0 -0
- {code_context_control-2.41.0 → code_context_control-2.42.0}/tui/theme.tcss +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-context-control
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.42.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
|
|
@@ -92,7 +92,7 @@ A thin **local** layer that sits between your IDE and your repo. Every AI tool c
|
|
|
92
92
|
|
|
93
93
|
| Without C3 | With C3 |
|
|
94
94
|
|---|---|
|
|
95
|
-
| `Read` the whole 2,000-line file | `c3_compress` returns a
|
|
95
|
+
| `Read` the whole 2,000-line file | `c3_compress` returns a structural map at 40-70% of the original token count (30-60% smaller) → `c3_read(symbols=...)` for the exact function |
|
|
96
96
|
| `Grep` the whole repo blindly | `c3_search` returns ranked candidates with TF-IDF + symbol awareness |
|
|
97
97
|
| Dump full `pytest` output into the prompt | `c3_filter` distills 500 lines → 30 actionable ones |
|
|
98
98
|
| Edit, hope it compiled | `c3_edit` writes via a ledger + `c3_validate` runs `pyright`/`tsc` automatically |
|
|
@@ -197,7 +197,7 @@ c3 ui # opens http://127.0.0.1:3333
|
|
|
197
197
|
<img src="https://raw.githubusercontent.com/drknowhow/code-context-control/main/docs/screenshots/ui_dashboard.png" alt="C3 per-project dashboard" width="900">
|
|
198
198
|
</p>
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
Illustrative example from one project's dashboard (numbers vary by project): **448K tokens saved** (89.9% rate) — C3's estimate versus a full-file-read baseline — plus 208 files indexed, 20 sessions, codebase breakdown by language, current-session live counters (in/out tokens, cache reads, services online), and a stream of recent tool calls and file changes.
|
|
201
201
|
|
|
202
202
|
### 3. Edit Ledger — every AI-driven edit tracked
|
|
203
203
|
|
|
@@ -30,7 +30,7 @@ A thin **local** layer that sits between your IDE and your repo. Every AI tool c
|
|
|
30
30
|
|
|
31
31
|
| Without C3 | With C3 |
|
|
32
32
|
|---|---|
|
|
33
|
-
| `Read` the whole 2,000-line file | `c3_compress` returns a
|
|
33
|
+
| `Read` the whole 2,000-line file | `c3_compress` returns a structural map at 40-70% of the original token count (30-60% smaller) → `c3_read(symbols=...)` for the exact function |
|
|
34
34
|
| `Grep` the whole repo blindly | `c3_search` returns ranked candidates with TF-IDF + symbol awareness |
|
|
35
35
|
| Dump full `pytest` output into the prompt | `c3_filter` distills 500 lines → 30 actionable ones |
|
|
36
36
|
| Edit, hope it compiled | `c3_edit` writes via a ledger + `c3_validate` runs `pyright`/`tsc` automatically |
|
|
@@ -135,7 +135,7 @@ c3 ui # opens http://127.0.0.1:3333
|
|
|
135
135
|
<img src="https://raw.githubusercontent.com/drknowhow/code-context-control/main/docs/screenshots/ui_dashboard.png" alt="C3 per-project dashboard" width="900">
|
|
136
136
|
</p>
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
Illustrative example from one project's dashboard (numbers vary by project): **448K tokens saved** (89.9% rate) — C3's estimate versus a full-file-read baseline — plus 208 files indexed, 20 sessions, codebase breakdown by language, current-session live counters (in/out tokens, cache reads, services online), and a stream of recent tool calls and file changes.
|
|
139
139
|
|
|
140
140
|
### 3. Edit Ledger — every AI-driven edit tracked
|
|
141
141
|
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Shared utilities for C3 hook scripts — supports Claude Code and Gemini CLI.
|
|
2
|
+
|
|
3
|
+
Also owns the consolidated enforcement state (.c3/enforcement_state.json):
|
|
4
|
+
a single file replacing the previous trio of last_c3_call.json,
|
|
5
|
+
unlocked_files.json, and ad-hoc writers spread across four hook scripts.
|
|
6
|
+
All hook reads/writes of enforcement state MUST go through this module.
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import traceback
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
# Max size of hook_errors.log before it is rotated (50 KB)
|
|
16
|
+
_LOG_MAX_BYTES = 50 * 1024
|
|
17
|
+
|
|
18
|
+
# ── Consolidated enforcement state ───────────────────────────────────────────
|
|
19
|
+
# Canonical file (the ONLY file written from v2.42 on):
|
|
20
|
+
# {
|
|
21
|
+
# "session_id": "<claude session id or ''>",
|
|
22
|
+
# "last_c3_call": {"ts": "<ISO UTC>", "tool": "c3_search", "read_unlocked": true},
|
|
23
|
+
# "unlocked_files": {"<resolved path>": ["read", "edit"]}
|
|
24
|
+
# }
|
|
25
|
+
# Legacy files (READ as fallback for one release; never written anymore):
|
|
26
|
+
ENFORCEMENT_STATE_FILE = ".c3/enforcement_state.json"
|
|
27
|
+
LEGACY_SIGNAL_FILE = ".c3/last_c3_call.json"
|
|
28
|
+
LEGACY_UNLOCK_FILE = ".c3/unlocked_files.json"
|
|
29
|
+
|
|
30
|
+
# Critical state-layer warnings (e.g. corrupted state JSON) surfaced by the
|
|
31
|
+
# dispatcher as an additionalContext line so enforcement never silently stops
|
|
32
|
+
# enforcing. Drained via drain_state_warnings().
|
|
33
|
+
STATE_WARNINGS: list = []
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def log_hook_error(hook_name: str, exc: BaseException) -> None:
|
|
37
|
+
"""Append a timestamped error entry to .c3/hook_errors.log.
|
|
38
|
+
|
|
39
|
+
Never raises — hook scripts must not crash the IDE even in the error logger.
|
|
40
|
+
Rotates the log (renames to hook_errors.log.bak) when it exceeds 50 KB.
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
c3_dir = Path.cwd() / ".c3"
|
|
44
|
+
if not c3_dir.exists():
|
|
45
|
+
return
|
|
46
|
+
log_file = c3_dir / "hook_errors.log"
|
|
47
|
+
# Rotate if too large
|
|
48
|
+
try:
|
|
49
|
+
if log_file.exists() and log_file.stat().st_size > _LOG_MAX_BYTES:
|
|
50
|
+
log_file.replace(c3_dir / "hook_errors.log.bak")
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
54
|
+
tb = traceback.format_exc().strip()
|
|
55
|
+
line = f"[{ts}] [{hook_name}] {type(exc).__name__}: {exc}\n{tb}\n---\n"
|
|
56
|
+
with open(log_file, "a", encoding="utf-8") as f:
|
|
57
|
+
f.write(line)
|
|
58
|
+
except Exception:
|
|
59
|
+
pass # Absolutely must not propagate
|
|
60
|
+
|
|
61
|
+
def drain_state_warnings() -> list:
|
|
62
|
+
"""Return and clear accumulated critical state warnings.
|
|
63
|
+
|
|
64
|
+
Called by the dispatcher after each sub-hook so corruption events become
|
|
65
|
+
a visible "[c3:hook-error] ..." additionalContext line instead of a
|
|
66
|
+
silent enforcement gap.
|
|
67
|
+
"""
|
|
68
|
+
warnings = STATE_WARNINGS[:]
|
|
69
|
+
STATE_WARNINGS.clear()
|
|
70
|
+
return warnings
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _empty_state(session_id: str = "") -> dict:
|
|
74
|
+
return {"session_id": session_id or "", "last_c3_call": None, "unlocked_files": {}}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _atomic_write_json(path: Path, data: dict) -> None:
|
|
78
|
+
"""Write JSON atomically: temp file in the same directory + os.replace."""
|
|
79
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
tmp = path.with_name(f"{path.name}.tmp{os.getpid()}")
|
|
81
|
+
tmp.write_text(json.dumps(data), encoding="utf-8")
|
|
82
|
+
os.replace(tmp, path)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _read_legacy_state(base: Path) -> dict:
|
|
86
|
+
"""Build a state view from the pre-v2.42 files (read-only fallback)."""
|
|
87
|
+
state = _empty_state()
|
|
88
|
+
signal_path = base / LEGACY_SIGNAL_FILE
|
|
89
|
+
if signal_path.exists():
|
|
90
|
+
try:
|
|
91
|
+
data = json.loads(signal_path.read_text(encoding="utf-8"))
|
|
92
|
+
if isinstance(data, dict) and data.get("timestamp"):
|
|
93
|
+
state["last_c3_call"] = {
|
|
94
|
+
"ts": str(data.get("timestamp")),
|
|
95
|
+
"tool": str(data.get("tool", "")),
|
|
96
|
+
"read_unlocked": bool(data.get("read_unlocked", False)),
|
|
97
|
+
}
|
|
98
|
+
except Exception:
|
|
99
|
+
pass # Legacy file corruption is not critical — new file supersedes it
|
|
100
|
+
unlock_path = base / LEGACY_UNLOCK_FILE
|
|
101
|
+
if unlock_path.exists():
|
|
102
|
+
try:
|
|
103
|
+
data = json.loads(unlock_path.read_text(encoding="utf-8"))
|
|
104
|
+
if isinstance(data, dict):
|
|
105
|
+
state["unlocked_files"] = {
|
|
106
|
+
str(k): list(v) for k, v in data.items() if isinstance(v, list)
|
|
107
|
+
}
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
return state
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def load_enforcement_state(project_path: Path | None = None, session_id: str = "") -> dict:
|
|
114
|
+
"""Load consolidated enforcement state with legacy fallback + session scoping.
|
|
115
|
+
|
|
116
|
+
- Missing new file → read legacy last_c3_call.json / unlocked_files.json
|
|
117
|
+
(one-release migration path; writes only ever go to the new file).
|
|
118
|
+
- Corrupted new file → quarantine to *.corrupt, log, push a critical
|
|
119
|
+
warning to STATE_WARNINGS, and return empty state (fail-open to the
|
|
120
|
+
advisory path, never a hard-deny surprise).
|
|
121
|
+
- session_id mismatch → state written by another session is STALE:
|
|
122
|
+
return empty state for the current session.
|
|
123
|
+
"""
|
|
124
|
+
base = project_path if project_path is not None else Path.cwd()
|
|
125
|
+
state_path = base / ENFORCEMENT_STATE_FILE
|
|
126
|
+
state = None
|
|
127
|
+
if state_path.exists():
|
|
128
|
+
try:
|
|
129
|
+
data = json.loads(state_path.read_text(encoding="utf-8"))
|
|
130
|
+
if not isinstance(data, dict):
|
|
131
|
+
raise ValueError("enforcement_state.json root is not an object")
|
|
132
|
+
state = _empty_state()
|
|
133
|
+
state["session_id"] = str(data.get("session_id") or "")
|
|
134
|
+
last_call = data.get("last_c3_call")
|
|
135
|
+
state["last_c3_call"] = last_call if isinstance(last_call, dict) else None
|
|
136
|
+
unlocked = data.get("unlocked_files")
|
|
137
|
+
state["unlocked_files"] = unlocked if isinstance(unlocked, dict) else {}
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
log_hook_error("enforcement_state", exc)
|
|
140
|
+
try:
|
|
141
|
+
state_path.replace(state_path.with_name(state_path.name + ".corrupt"))
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
STATE_WARNINGS.append(
|
|
145
|
+
"[c3:hook-error] enforcement_state: corrupted "
|
|
146
|
+
f"{ENFORCEMENT_STATE_FILE} quarantined ({type(exc).__name__}); "
|
|
147
|
+
"see .c3/hook_errors.log"
|
|
148
|
+
)
|
|
149
|
+
return _empty_state(session_id)
|
|
150
|
+
if state is None:
|
|
151
|
+
state = _read_legacy_state(base)
|
|
152
|
+
# Session scoping: hook payloads carry session_id; state from a different
|
|
153
|
+
# session must not grant unlocks (signal files used to survive /clear).
|
|
154
|
+
if session_id and state.get("session_id") and state["session_id"] != session_id:
|
|
155
|
+
return _empty_state(session_id)
|
|
156
|
+
return state
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def save_enforcement_state(state: dict, project_path: Path | None = None) -> None:
|
|
160
|
+
"""Atomically persist the consolidated enforcement state."""
|
|
161
|
+
base = project_path if project_path is not None else Path.cwd()
|
|
162
|
+
try:
|
|
163
|
+
_atomic_write_json(base / ENFORCEMENT_STATE_FILE, state)
|
|
164
|
+
except Exception as exc:
|
|
165
|
+
log_hook_error("enforcement_state", exc)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def record_c3_signal(
|
|
169
|
+
tool: str,
|
|
170
|
+
read_unlocked: bool,
|
|
171
|
+
session_id: str = "",
|
|
172
|
+
project_path: Path | None = None,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Record 'a c3_* tool just completed' in the consolidated state."""
|
|
175
|
+
state = load_enforcement_state(project_path, session_id=session_id)
|
|
176
|
+
if session_id:
|
|
177
|
+
state["session_id"] = session_id
|
|
178
|
+
state["last_c3_call"] = {
|
|
179
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
180
|
+
"tool": tool,
|
|
181
|
+
"read_unlocked": bool(read_unlocked),
|
|
182
|
+
}
|
|
183
|
+
save_enforcement_state(state, project_path)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def record_unlocked_files(
|
|
187
|
+
paths,
|
|
188
|
+
categories,
|
|
189
|
+
session_id: str = "",
|
|
190
|
+
project_path: Path | None = None,
|
|
191
|
+
) -> None:
|
|
192
|
+
"""Merge sticky per-file unlock categories into the consolidated state."""
|
|
193
|
+
cats_to_add = {c for c in categories if c}
|
|
194
|
+
if not cats_to_add:
|
|
195
|
+
return
|
|
196
|
+
state = load_enforcement_state(project_path, session_id=session_id)
|
|
197
|
+
if session_id:
|
|
198
|
+
state["session_id"] = session_id
|
|
199
|
+
changed = False
|
|
200
|
+
for fp in paths:
|
|
201
|
+
if not fp:
|
|
202
|
+
continue
|
|
203
|
+
try:
|
|
204
|
+
normalized = str(Path(fp).resolve())
|
|
205
|
+
except OSError:
|
|
206
|
+
continue
|
|
207
|
+
cats = set(state["unlocked_files"].get(normalized, []))
|
|
208
|
+
merged = sorted(cats | cats_to_add)
|
|
209
|
+
if merged != state["unlocked_files"].get(normalized):
|
|
210
|
+
state["unlocked_files"][normalized] = merged
|
|
211
|
+
changed = True
|
|
212
|
+
if changed:
|
|
213
|
+
save_enforcement_state(state, project_path)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# Map Gemini CLI built-in tool names → canonical Claude Code equivalents
|
|
217
|
+
GEMINI_TOOL_MAP = {
|
|
218
|
+
"run_shell_command": "Bash",
|
|
219
|
+
"read_file": "Read",
|
|
220
|
+
"edit_file": "Edit",
|
|
221
|
+
"write_file": "Write",
|
|
222
|
+
"list_directory": "FindFiles",
|
|
223
|
+
"find_files": "FindFiles",
|
|
224
|
+
"grep": "SearchText",
|
|
225
|
+
"search_in_files_content": "SearchText",
|
|
226
|
+
"find_in_files": "SearchText",
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def normalize_tool_name(tool_name: str) -> str:
|
|
231
|
+
"""Normalize Gemini CLI tool names to their Claude Code equivalents."""
|
|
232
|
+
return GEMINI_TOOL_MAP.get(tool_name, tool_name)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def get_tool_output(data: dict) -> tuple:
|
|
236
|
+
"""Extract the output text and detect IDE format from hook stdin data.
|
|
237
|
+
|
|
238
|
+
Returns (output_text: str, is_gemini: bool).
|
|
239
|
+
Claude passes tool_response as a plain string.
|
|
240
|
+
Gemini wraps it in {llmContent, returnDisplay}.
|
|
241
|
+
"""
|
|
242
|
+
resp = data.get("tool_response", "")
|
|
243
|
+
if isinstance(resp, dict):
|
|
244
|
+
content = resp.get("llmContent", "") or resp.get("returnDisplay", "")
|
|
245
|
+
if isinstance(content, list):
|
|
246
|
+
# llmContent can be a list of content-part dicts like {text: "..."}
|
|
247
|
+
content = "\n".join(
|
|
248
|
+
p.get("text", str(p)) if isinstance(p, dict) else str(p)
|
|
249
|
+
for p in content
|
|
250
|
+
)
|
|
251
|
+
return str(content) if content is not None else "", True
|
|
252
|
+
return resp if isinstance(resp, str) else "", False
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def get_tool_input_path(data: dict) -> str:
|
|
256
|
+
"""Extract file path from tool_input, handling Claude (file_path),
|
|
257
|
+
Gemini (path), and NotebookEdit (notebook_path)."""
|
|
258
|
+
tool_input = data.get("tool_input", {})
|
|
259
|
+
return (
|
|
260
|
+
tool_input.get("file_path", "")
|
|
261
|
+
or tool_input.get("path", "")
|
|
262
|
+
or tool_input.get("notebook_path", "")
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def record_json_unlocks(
|
|
267
|
+
editable: list,
|
|
268
|
+
project_path: Path | None = None,
|
|
269
|
+
session_id: str = "",
|
|
270
|
+
) -> None:
|
|
271
|
+
"""Record file paths as read+edit unlocked in the enforcement state.
|
|
272
|
+
|
|
273
|
+
Compatibility wrapper kept for existing callers (hook_edit_unlock,
|
|
274
|
+
hook_c3read): unlocks now land in .c3/enforcement_state.json via the
|
|
275
|
+
consolidated state layer instead of the legacy unlocked_files.json.
|
|
276
|
+
Fails silently on I/O errors.
|
|
277
|
+
"""
|
|
278
|
+
try:
|
|
279
|
+
record_unlocked_files(
|
|
280
|
+
editable, {"read", "edit"},
|
|
281
|
+
session_id=session_id, project_path=project_path,
|
|
282
|
+
)
|
|
283
|
+
except Exception:
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def emit_additional_context(text: str, is_gemini: bool) -> None:
|
|
288
|
+
"""Write additionalContext JSON to stdout in the correct format for the IDE."""
|
|
289
|
+
if is_gemini:
|
|
290
|
+
sys.stdout.write(json.dumps({"hookSpecificOutput": {"additionalContext": text}}))
|
|
291
|
+
else:
|
|
292
|
+
sys.stdout.write(json.dumps({"additionalContext": text}))
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def emit_filtered_output(filtered: str, is_gemini: bool) -> None:
|
|
296
|
+
"""Write filtered tool output to stdout.
|
|
297
|
+
|
|
298
|
+
Claude Code: replaces the tool result entirely via tool_result.
|
|
299
|
+
Gemini CLI: no direct replacement — appends as additionalContext instead.
|
|
300
|
+
"""
|
|
301
|
+
if is_gemini:
|
|
302
|
+
sys.stdout.write(json.dumps({"hookSpecificOutput": {"additionalContext": filtered}}))
|
|
303
|
+
else:
|
|
304
|
+
sys.stdout.write(json.dumps({"tool_result": filtered}))
|
|
@@ -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.42.0"
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def _command_deps() -> CommandDeps:
|
|
@@ -4448,7 +4448,7 @@ def _uninstall_mcp_all(project_path: str):
|
|
|
4448
4448
|
# Remove hooks
|
|
4449
4449
|
hooks = settings.get("hooks", {}).get("PostToolUse", [])
|
|
4450
4450
|
new_hooks = []
|
|
4451
|
-
c3_hook_files = {"hook_filter.py", "hook_read.py", "hook_c3read.py"}
|
|
4451
|
+
c3_hook_files = {"hook_filter.py", "hook_read.py", "hook_c3read.py", "hook_dispatch.py"}
|
|
4452
4452
|
for h in hooks:
|
|
4453
4453
|
if h.get("matcher") in ("Bash", "Read", "mcp__c3__c3_read"):
|
|
4454
4454
|
h["hooks"] = [hook for hook in h.get("hooks", [])
|
|
@@ -5011,17 +5011,16 @@ def cmd_install_mcp(args):
|
|
|
5011
5011
|
# file; "cmd /c …" returns "cmd: command not found"). The single-quoted paths are
|
|
5012
5012
|
# correct — bash strips them and re-quotes for cmd.exe, preserving spaces/parens.
|
|
5013
5013
|
_hook_prefix = "cmd.exe /c " if sys.platform == "win32" else ""
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
hook_c3_signal_cmd = f"{_hook_prefix}{shlex.quote(sys.executable)} {shlex.quote(str(cli_dir / 'hook_c3_signal.py'))}"
|
|
5014
|
+
# v2.42: single dispatcher script per hook event instead of N separate
|
|
5015
|
+
# per-hook commands. One interpreter spawn per event; the dispatcher
|
|
5016
|
+
# (cli/hook_dispatch.py) runs all applicable sub-hooks in-process.
|
|
5017
|
+
_dispatch_base = (
|
|
5018
|
+
f"{_hook_prefix}{shlex.quote(sys.executable)} "
|
|
5019
|
+
f"{shlex.quote(str(cli_dir / 'hook_dispatch.py'))}"
|
|
5020
|
+
)
|
|
5021
|
+
hook_pretool_cmd = f"{_dispatch_base} pretool"
|
|
5022
|
+
hook_posttool_cmd = f"{_dispatch_base} posttool"
|
|
5023
|
+
hook_stop_cmd = f"{_dispatch_base} stop"
|
|
5025
5024
|
|
|
5026
5025
|
# Tool matcher names differ by IDE: Gemini uses snake_case built-in names.
|
|
5027
5026
|
if profile.name == "gemini":
|
|
@@ -5045,130 +5044,48 @@ def cmd_install_mcp(args):
|
|
|
5045
5044
|
extra_edit_matchers = ["MultiEdit", "NotebookEdit"]
|
|
5046
5045
|
|
|
5047
5046
|
# ── PostToolUse hooks ──
|
|
5047
|
+
# Matcher set is unchanged from pre-v2.42; every matcher now points at
|
|
5048
|
+
# the single posttool dispatcher (which sub-hooks run for which tool
|
|
5049
|
+
# moved into cli/hook_dispatch.py). One spawn per event instead of
|
|
5050
|
+
# up to three.
|
|
5051
|
+
_post_matcher_names = [
|
|
5052
|
+
shell_matcher,
|
|
5053
|
+
read_matcher,
|
|
5054
|
+
"mcp__c3__c3_read",
|
|
5055
|
+
"mcp__c3__c3_shell",
|
|
5056
|
+
"mcp__c3__c3_search",
|
|
5057
|
+
"mcp__c3__c3_compress",
|
|
5058
|
+
"mcp__c3__c3_filter",
|
|
5059
|
+
"mcp__c3__c3_memory",
|
|
5060
|
+
"mcp__c3__c3_validate",
|
|
5061
|
+
"mcp__c3__c3_edit",
|
|
5062
|
+
"mcp__c3__c3_edits",
|
|
5063
|
+
"mcp__c3__c3_impact",
|
|
5064
|
+
"mcp__c3__c3_status",
|
|
5065
|
+
"mcp__c3__c3_delegate",
|
|
5066
|
+
"mcp__c3__c3_session",
|
|
5067
|
+
"mcp__c3__c3_agent",
|
|
5068
|
+
edit_matcher,
|
|
5069
|
+
write_matcher,
|
|
5070
|
+
*extra_edit_matchers,
|
|
5071
|
+
]
|
|
5048
5072
|
desired_post_hooks = [
|
|
5049
|
-
{
|
|
5050
|
-
|
|
5051
|
-
"hooks": [
|
|
5052
|
-
{"type": "command", "command": hook_filter_cmd},
|
|
5053
|
-
{"type": "command", "command": hook_ghost_files_cmd},
|
|
5054
|
-
]
|
|
5055
|
-
},
|
|
5056
|
-
{
|
|
5057
|
-
"matcher": read_matcher,
|
|
5058
|
-
"hooks": [
|
|
5059
|
-
{"type": "command", "command": hook_read_cmd},
|
|
5060
|
-
{"type": "command", "command": hook_ghost_files_cmd},
|
|
5061
|
-
]
|
|
5062
|
-
},
|
|
5063
|
-
{
|
|
5064
|
-
"matcher": "mcp__c3__c3_read",
|
|
5065
|
-
"hooks": [
|
|
5066
|
-
{"type": "command", "command": hook_c3read_cmd},
|
|
5067
|
-
{"type": "command", "command": hook_c3_signal_cmd},
|
|
5068
|
-
{"type": "command", "command": hook_ghost_files_cmd},
|
|
5069
|
-
]
|
|
5070
|
-
},
|
|
5071
|
-
{
|
|
5072
|
-
"matcher": "mcp__c3__c3_shell",
|
|
5073
|
-
"hooks": [
|
|
5074
|
-
{"type": "command", "command": hook_c3_signal_cmd},
|
|
5075
|
-
{"type": "command", "command": hook_ghost_files_cmd},
|
|
5076
|
-
]
|
|
5077
|
-
},
|
|
5078
|
-
{
|
|
5079
|
-
"matcher": "mcp__c3__c3_search",
|
|
5080
|
-
"hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
|
|
5081
|
-
},
|
|
5082
|
-
{
|
|
5083
|
-
"matcher": "mcp__c3__c3_compress",
|
|
5084
|
-
"hooks": [
|
|
5085
|
-
{"type": "command", "command": hook_edit_unlock_cmd},
|
|
5086
|
-
{"type": "command", "command": hook_c3_signal_cmd},
|
|
5087
|
-
]
|
|
5088
|
-
},
|
|
5089
|
-
{
|
|
5090
|
-
"matcher": "mcp__c3__c3_filter",
|
|
5091
|
-
"hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
|
|
5092
|
-
},
|
|
5093
|
-
{
|
|
5094
|
-
"matcher": "mcp__c3__c3_memory",
|
|
5095
|
-
"hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
|
|
5096
|
-
},
|
|
5097
|
-
{
|
|
5098
|
-
"matcher": "mcp__c3__c3_validate",
|
|
5099
|
-
"hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
|
|
5100
|
-
},
|
|
5101
|
-
{
|
|
5102
|
-
"matcher": "mcp__c3__c3_edit",
|
|
5103
|
-
"hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
|
|
5104
|
-
},
|
|
5105
|
-
{
|
|
5106
|
-
"matcher": "mcp__c3__c3_edits",
|
|
5107
|
-
"hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
|
|
5108
|
-
},
|
|
5109
|
-
{
|
|
5110
|
-
"matcher": "mcp__c3__c3_impact",
|
|
5111
|
-
"hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
|
|
5112
|
-
},
|
|
5113
|
-
{
|
|
5114
|
-
"matcher": "mcp__c3__c3_status",
|
|
5115
|
-
"hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
|
|
5116
|
-
},
|
|
5117
|
-
{
|
|
5118
|
-
"matcher": "mcp__c3__c3_delegate",
|
|
5119
|
-
"hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
|
|
5120
|
-
},
|
|
5121
|
-
{
|
|
5122
|
-
"matcher": "mcp__c3__c3_session",
|
|
5123
|
-
"hooks": [{"type": "command", "command": hook_c3_signal_cmd}]
|
|
5124
|
-
},
|
|
5125
|
-
{
|
|
5126
|
-
"matcher": "mcp__c3__c3_agent",
|
|
5127
|
-
"hooks": [
|
|
5128
|
-
{"type": "command", "command": hook_edit_unlock_cmd},
|
|
5129
|
-
{"type": "command", "command": hook_c3_signal_cmd},
|
|
5130
|
-
]
|
|
5131
|
-
},
|
|
5132
|
-
{
|
|
5133
|
-
"matcher": edit_matcher,
|
|
5134
|
-
"hooks": [{"type": "command", "command": hook_edit_ledger_cmd}]
|
|
5135
|
-
},
|
|
5136
|
-
{
|
|
5137
|
-
"matcher": write_matcher,
|
|
5138
|
-
"hooks": [{"type": "command", "command": hook_edit_ledger_cmd}]
|
|
5139
|
-
},
|
|
5140
|
-
*[
|
|
5141
|
-
{"matcher": m, "hooks": [{"type": "command", "command": hook_edit_ledger_cmd}]}
|
|
5142
|
-
for m in extra_edit_matchers
|
|
5143
|
-
],
|
|
5073
|
+
{"matcher": m, "hooks": [{"type": "command", "command": hook_posttool_cmd}]}
|
|
5074
|
+
for m in _post_matcher_names
|
|
5144
5075
|
]
|
|
5145
5076
|
|
|
5146
5077
|
# ── PreToolUse hooks (enforcement — blocks native tools without prior c3_*) ──
|
|
5078
|
+
_pre_matcher_names = [
|
|
5079
|
+
read_matcher,
|
|
5080
|
+
grep_matcher,
|
|
5081
|
+
glob_matcher,
|
|
5082
|
+
edit_matcher,
|
|
5083
|
+
write_matcher,
|
|
5084
|
+
*extra_edit_matchers,
|
|
5085
|
+
]
|
|
5147
5086
|
desired_pre_hooks = [
|
|
5148
|
-
{
|
|
5149
|
-
|
|
5150
|
-
"hooks": [{"type": "command", "command": hook_enforce_cmd}]
|
|
5151
|
-
},
|
|
5152
|
-
{
|
|
5153
|
-
"matcher": grep_matcher,
|
|
5154
|
-
"hooks": [{"type": "command", "command": hook_enforce_cmd}]
|
|
5155
|
-
},
|
|
5156
|
-
{
|
|
5157
|
-
"matcher": glob_matcher,
|
|
5158
|
-
"hooks": [{"type": "command", "command": hook_enforce_cmd}]
|
|
5159
|
-
},
|
|
5160
|
-
{
|
|
5161
|
-
"matcher": edit_matcher,
|
|
5162
|
-
"hooks": [{"type": "command", "command": hook_enforce_cmd}]
|
|
5163
|
-
},
|
|
5164
|
-
{
|
|
5165
|
-
"matcher": write_matcher,
|
|
5166
|
-
"hooks": [{"type": "command", "command": hook_enforce_cmd}]
|
|
5167
|
-
},
|
|
5168
|
-
*[
|
|
5169
|
-
{"matcher": m, "hooks": [{"type": "command", "command": hook_enforce_cmd}]}
|
|
5170
|
-
for m in extra_edit_matchers
|
|
5171
|
-
],
|
|
5087
|
+
{"matcher": m, "hooks": [{"type": "command", "command": hook_pretool_cmd}]}
|
|
5088
|
+
for m in _pre_matcher_names
|
|
5172
5089
|
]
|
|
5173
5090
|
|
|
5174
5091
|
# Merge: replace existing C3 hooks (so re-running install-mcp updates commands),
|
|
@@ -5198,18 +5115,19 @@ def cmd_install_mcp(args):
|
|
|
5198
5115
|
{
|
|
5199
5116
|
"matcher": "",
|
|
5200
5117
|
"hooks": [
|
|
5201
|
-
{"type": "command", "command":
|
|
5202
|
-
{"type": "command", "command": hook_auto_snapshot_cmd},
|
|
5203
|
-
{"type": "command", "command": hook_terse_advisor_cmd},
|
|
5118
|
+
{"type": "command", "command": hook_stop_cmd},
|
|
5204
5119
|
]
|
|
5205
5120
|
},
|
|
5206
5121
|
]
|
|
5207
5122
|
stop_event = "Stop"
|
|
5208
5123
|
# Replace only C3's own stop hooks (identified by our hook scripts) and
|
|
5209
5124
|
# keep every user-added stop hook — including matcher-less ones, which
|
|
5210
|
-
# are the normal shape for Stop hooks.
|
|
5125
|
+
# are the normal shape for Stop hooks. The pre-v2.42 script names stay
|
|
5126
|
+
# in this tuple so re-running install-mcp migrates old per-hook
|
|
5127
|
+
# entries to the dispatcher.
|
|
5211
5128
|
_c3_stop_scripts = (
|
|
5212
5129
|
"hook_session_stats.py", "hook_auto_snapshot.py", "hook_terse_advisor.py",
|
|
5130
|
+
"hook_dispatch.py",
|
|
5213
5131
|
)
|
|
5214
5132
|
|
|
5215
5133
|
def _is_c3_stop_hook(entry: dict) -> bool:
|
|
@@ -5259,9 +5177,9 @@ def cmd_install_mcp(args):
|
|
|
5259
5177
|
json.dump(settings, f, indent=2)
|
|
5260
5178
|
|
|
5261
5179
|
print(f"Wrote {settings_path}")
|
|
5262
|
-
print(f" Hooks ({hook_event}):
|
|
5263
|
-
print(f" Hooks ({pre_event}): {read_matcher}/{grep_matcher}/{glob_matcher}/{edit_matcher}/{write_matcher} (c3 enforcement)")
|
|
5264
|
-
print(" Hooks (Stop): session_stats + auto_snapshot")
|
|
5180
|
+
print(f" Hooks ({hook_event}): dispatcher (1 spawn/event) — filter/ghost/read-guard/ledger/unlock/signal via cli/hook_dispatch.py posttool")
|
|
5181
|
+
print(f" Hooks ({pre_event}): dispatcher — {read_matcher}/{grep_matcher}/{glob_matcher}/{edit_matcher}/{write_matcher} (c3 enforcement)")
|
|
5182
|
+
print(" Hooks (Stop): dispatcher — session_stats + auto_snapshot + terse_advisor")
|
|
5265
5183
|
if profile.name == "claude-code":
|
|
5266
5184
|
print(" Claude MCP prompt settings enabled for this project")
|
|
5267
5185
|
if perm_tier and profile.name == "claude-code":
|
|
@@ -52,9 +52,11 @@ def _call_server(port: int, stop_hook_data: dict) -> bool:
|
|
|
52
52
|
return False
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
def _fallback_snapshot(stop_hook_data: dict) -> None:
|
|
55
|
+
def _fallback_snapshot(stop_hook_data: dict, base: Path | None = None) -> None:
|
|
56
56
|
"""Lightweight file-based snapshot when the UI server is not running."""
|
|
57
|
-
|
|
57
|
+
if base is None:
|
|
58
|
+
base = Path.cwd()
|
|
59
|
+
c3_dir = base / ".c3"
|
|
58
60
|
if not c3_dir.exists():
|
|
59
61
|
return
|
|
60
62
|
|
|
@@ -116,6 +118,19 @@ def _fallback_snapshot(stop_hook_data: dict) -> None:
|
|
|
116
118
|
json.dump(snapshot, f, indent=2)
|
|
117
119
|
|
|
118
120
|
|
|
121
|
+
def run(payload: dict, project_path: Path | None = None):
|
|
122
|
+
"""Core logic — importable by the dispatcher and tests. Returns None."""
|
|
123
|
+
base = project_path if project_path is not None else Path.cwd()
|
|
124
|
+
port = _find_server_port(str(base))
|
|
125
|
+
|
|
126
|
+
if port and _call_server(port, payload):
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
# Server not running or unreachable — fallback
|
|
130
|
+
_fallback_snapshot(payload, base)
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
119
134
|
def main() -> None:
|
|
120
135
|
try:
|
|
121
136
|
data = json.load(sys.stdin)
|
|
@@ -124,14 +139,7 @@ def main() -> None:
|
|
|
124
139
|
sys.exit(0)
|
|
125
140
|
|
|
126
141
|
try:
|
|
127
|
-
|
|
128
|
-
port = _find_server_port(project_path)
|
|
129
|
-
|
|
130
|
-
if port and _call_server(port, data):
|
|
131
|
-
sys.exit(0)
|
|
132
|
-
|
|
133
|
-
# Server not running or unreachable — fallback
|
|
134
|
-
_fallback_snapshot(data)
|
|
142
|
+
run(data)
|
|
135
143
|
except Exception as exc:
|
|
136
144
|
log_hook_error("hook_auto_snapshot", exc)
|
|
137
145
|
|