codebrain 0.3.1__tar.gz → 0.3.3__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.
- {codebrain-0.3.1 → codebrain-0.3.3}/PKG-INFO +6 -4
- {codebrain-0.3.1 → codebrain-0.3.3}/README.md +5 -3
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/__init__.py +1 -1
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/cli.py +22 -20
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/mcp_lifecycle.py +96 -8
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain.egg-info/PKG-INFO +6 -4
- {codebrain-0.3.1 → codebrain-0.3.3}/pyproject.toml +1 -1
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_mcp_lifecycle.py +137 -1
- {codebrain-0.3.1 → codebrain-0.3.3}/LICENSE +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/__main__.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/actions/__init__.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/actions/base.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/actions/refactor.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/actions/reviewer.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/actions/test_gen.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/agent_bridge.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/analyzer.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/api.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/api_models.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/architecture.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/comprehension.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/config.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/context.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/cross_query.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/cross_registry.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/diff_impact.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/env_migration.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/equivalence.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/export.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/frontend.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/graph/__init__.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/graph/query.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/graph/schema.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/graph/store.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/hook_runner.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/hooks.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/indexer.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/kt.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/kt_video.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/llm.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/logging.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/mcp_server.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/memory/__init__.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/memory/store.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/migration.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/modernize.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/onboard.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/__init__.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/base.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/cobol_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/config_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/csharp_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/dart_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/fortran_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/frontend_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/go_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/java_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/kotlin_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/models.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/mumps_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/plsql_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/python_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/registry.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/rust_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/schema_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/typescript_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/typescript_treesitter.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/vue_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/py.typed +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/resolver.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/rewriter.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/schema_migration.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/settings.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/susa_auth.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/test_gaps.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/test_runner.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/tour.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/ui_migration.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/utils.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/validator.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/watcher/__init__.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/watcher/file_watcher.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain.egg-info/SOURCES.txt +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain.egg-info/dependency_links.txt +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain.egg-info/entry_points.txt +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain.egg-info/requires.txt +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/codebrain.egg-info/top_level.txt +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/setup.cfg +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_actions.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_agent_bridge.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_analyzer.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_api.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_architecture.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_ci.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_cli.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_comprehension.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_context.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_contracts_real.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_coverage_gaps.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_cross_repo.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_csharp_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_dart_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_dataflow.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_dead_code_confidence.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_diff_impact.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_env_migration.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_equivalence.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_error_recovery.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_export.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_fingerprints.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_frontend.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_gate_battle.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_go_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_hooks.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_indexer.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_infra_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_install.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_integration.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_java_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_jyotishyamitra.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_kotlin_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_kt.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_legacy_parsers.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_llm.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_mcp_server.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_memory.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_migration.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_modernize.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_multi_project_cli.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_narratives.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_onboard.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_orm_detection.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_output_quality.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_plugin_system.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_production_hardening.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_query.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_real_codebase.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_real_features.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_real_frontend.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_real_repos.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_real_world.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_resolver.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_rewriter.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_rust_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_scale.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_scale_optimizations.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_scale_real.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_schema.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_schema_migration.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_schema_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_settings.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_store.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_test_runner.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_tour.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_translate.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_ts_ast_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_ts_parser_enhanced.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_typescript_parser.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_ui_migration.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_utils.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_validation_narratives.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_validator.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_validator_scenarios.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_vscode_extension.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_watch_validate.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_watcher.py +0 -0
- {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_zoom.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codebrain
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Know what breaks before you break it. Structural knowledge graph for codebases — impact analysis, dead code detection, health scores. No LLM required.
|
|
5
5
|
Author: CodeBrain Contributors
|
|
6
6
|
License: MIT License
|
|
@@ -123,15 +123,17 @@ MCP config. Restart Claude Code and it has tools like
|
|
|
123
123
|
`mcp__codebrain__ask_codebase`, plus a few dozen more. It uses them on
|
|
124
124
|
its own.
|
|
125
125
|
|
|
126
|
-
Manual config (
|
|
126
|
+
Manual config — drop `.mcp.json` at **your project's root** (not in a
|
|
127
|
+
global config). CodeBrain's index lives in `<repo>/.codebrain/`, so a
|
|
128
|
+
global config would make every Claude session in every project fight
|
|
129
|
+
over the same database. `brain setup` writes the right thing for you.
|
|
127
130
|
|
|
128
131
|
```json
|
|
129
132
|
{
|
|
130
133
|
"mcpServers": {
|
|
131
134
|
"codebrain": {
|
|
132
135
|
"command": "python",
|
|
133
|
-
"args": ["-m", "codebrain.mcp_server"]
|
|
134
|
-
"env": { "CODEBRAIN_PROJECT": "/absolute/path/to/repo" }
|
|
136
|
+
"args": ["-m", "codebrain.mcp_server"]
|
|
135
137
|
}
|
|
136
138
|
}
|
|
137
139
|
}
|
|
@@ -52,15 +52,17 @@ MCP config. Restart Claude Code and it has tools like
|
|
|
52
52
|
`mcp__codebrain__ask_codebase`, plus a few dozen more. It uses them on
|
|
53
53
|
its own.
|
|
54
54
|
|
|
55
|
-
Manual config (
|
|
55
|
+
Manual config — drop `.mcp.json` at **your project's root** (not in a
|
|
56
|
+
global config). CodeBrain's index lives in `<repo>/.codebrain/`, so a
|
|
57
|
+
global config would make every Claude session in every project fight
|
|
58
|
+
over the same database. `brain setup` writes the right thing for you.
|
|
56
59
|
|
|
57
60
|
```json
|
|
58
61
|
{
|
|
59
62
|
"mcpServers": {
|
|
60
63
|
"codebrain": {
|
|
61
64
|
"command": "python",
|
|
62
|
-
"args": ["-m", "codebrain.mcp_server"]
|
|
63
|
-
"env": { "CODEBRAIN_PROJECT": "/absolute/path/to/repo" }
|
|
65
|
+
"args": ["-m", "codebrain.mcp_server"]
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
}
|
|
@@ -398,32 +398,34 @@ def setup(ctx: click.Context, force: bool) -> None:
|
|
|
398
398
|
claude_md.write_text(content)
|
|
399
399
|
click.echo(f"Created {claude_md}")
|
|
400
400
|
|
|
401
|
-
# 3. Configure MCP
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
401
|
+
# 3. Configure MCP scoped to *this* project via .mcp.json
|
|
402
|
+
#
|
|
403
|
+
# Project-local config is the right scope: CodeBrain's DB lives in
|
|
404
|
+
# <repo>/.codebrain/, the MCP only makes sense for the repo it was
|
|
405
|
+
# indexed against, and a global config makes every Claude session in
|
|
406
|
+
# every project fight over the same DB and PID file.
|
|
407
|
+
mcp_config_path = repo_root / ".mcp.json"
|
|
408
|
+
if mcp_config_path.exists():
|
|
409
|
+
try:
|
|
410
|
+
mcp_config = json.loads(mcp_config_path.read_text())
|
|
411
|
+
except json.JSONDecodeError:
|
|
412
|
+
mcp_config = {}
|
|
413
|
+
else:
|
|
414
|
+
mcp_config = {}
|
|
415
|
+
mcp_config.setdefault("mcpServers", {})
|
|
416
|
+
if "codebrain" in mcp_config["mcpServers"] and not force:
|
|
417
|
+
click.echo(f"MCP server already configured in {mcp_config_path}")
|
|
406
418
|
else:
|
|
407
|
-
|
|
408
|
-
if "mcpServers" not in settings:
|
|
409
|
-
settings["mcpServers"] = {}
|
|
410
|
-
if "codebrain" not in settings["mcpServers"]:
|
|
411
|
-
# Find the codebrain package location
|
|
412
|
-
import codebrain
|
|
413
|
-
cb_root = str(Path(codebrain.__file__).parent.parent)
|
|
414
|
-
settings["mcpServers"]["codebrain"] = {
|
|
419
|
+
mcp_config["mcpServers"]["codebrain"] = {
|
|
415
420
|
"command": "python",
|
|
416
421
|
"args": ["-m", "codebrain.mcp_server"],
|
|
417
|
-
"cwd": cb_root,
|
|
418
422
|
}
|
|
419
|
-
|
|
420
|
-
click.echo(f"
|
|
421
|
-
else:
|
|
422
|
-
click.echo("MCP server already configured")
|
|
423
|
+
mcp_config_path.write_text(json.dumps(mcp_config, indent=2) + "\n")
|
|
424
|
+
click.echo(f"Wrote MCP config to {mcp_config_path}")
|
|
423
425
|
|
|
424
426
|
click.echo()
|
|
425
|
-
click.echo(click.style("Done! Restart Claude Code to activate.", bold=True))
|
|
426
|
-
click.echo("Claude Code will
|
|
427
|
+
click.echo(click.style("Done! Restart Claude Code (in this repo) to activate.", bold=True))
|
|
428
|
+
click.echo("On first launch Claude Code will ask you to approve the project MCP server.")
|
|
427
429
|
|
|
428
430
|
|
|
429
431
|
def _generate_claude_md(project_name: str) -> str:
|
|
@@ -34,6 +34,12 @@ PARENT_POLL_SECONDS = 5
|
|
|
34
34
|
IDLE_TIMEOUT_SECONDS = int(os.environ.get("CODEBRAIN_MCP_IDLE_TIMEOUT", "1800"))
|
|
35
35
|
MAX_LIFETIME_SECONDS = int(os.environ.get("CODEBRAIN_MCP_MAX_LIFETIME", "0"))
|
|
36
36
|
STALE_AGE_SECONDS = 3600 # 1h with no parent-death signal still counts as stale
|
|
37
|
+
ANCESTOR_WALK_DEPTH = 6
|
|
38
|
+
# Names of host processes that own the MCP lifecycle. If any of these is found
|
|
39
|
+
# while walking up the ancestor chain, we watch *it* rather than the immediate
|
|
40
|
+
# parent — survives transient launchers (cmd.exe, Electron worker shells) that
|
|
41
|
+
# Claude Code uses on Windows. Match is case-insensitive substring.
|
|
42
|
+
HOST_PROCESS_NAME_HINTS = ("claude", "cursor", "vscode", "code")
|
|
37
43
|
|
|
38
44
|
_last_activity_lock = threading.Lock()
|
|
39
45
|
_last_activity: float = time.time()
|
|
@@ -73,6 +79,38 @@ def _read_pid_file(pid_file: Path) -> int | None:
|
|
|
73
79
|
return None
|
|
74
80
|
|
|
75
81
|
|
|
82
|
+
def _predecessor_has_live_host(pid: int) -> bool:
|
|
83
|
+
"""True if ``pid``'s ancestor chain contains a live host (claude/cursor/...).
|
|
84
|
+
|
|
85
|
+
Such a predecessor belongs to a *concurrent* sibling Claude session and
|
|
86
|
+
must not be terminated — its disappearance would silently break that
|
|
87
|
+
session's MCP. Without psutil we cannot tell, so be safe and assume yes.
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
import psutil
|
|
91
|
+
except ImportError:
|
|
92
|
+
return True
|
|
93
|
+
try:
|
|
94
|
+
proc = psutil.Process(pid)
|
|
95
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
96
|
+
return False
|
|
97
|
+
for _ in range(ANCESTOR_WALK_DEPTH):
|
|
98
|
+
try:
|
|
99
|
+
parent = proc.parent()
|
|
100
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
101
|
+
return False
|
|
102
|
+
if parent is None:
|
|
103
|
+
return False
|
|
104
|
+
try:
|
|
105
|
+
name = (parent.name() or "").lower()
|
|
106
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
107
|
+
return False
|
|
108
|
+
if any(hint in name for hint in HOST_PROCESS_NAME_HINTS):
|
|
109
|
+
return True
|
|
110
|
+
proc = parent
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
76
114
|
def _kill_stale_predecessor(pid_file: Path) -> None:
|
|
77
115
|
if not pid_file.exists():
|
|
78
116
|
return
|
|
@@ -90,6 +128,12 @@ def _kill_stale_predecessor(pid_file: Path) -> None:
|
|
|
90
128
|
# PID was reused by an unrelated process. Don't touch.
|
|
91
129
|
_log.debug("PID %d reused by unrelated process; leaving alone", old_pid)
|
|
92
130
|
return
|
|
131
|
+
if _predecessor_has_live_host(old_pid):
|
|
132
|
+
# Concurrent sibling Claude session is still using this MCP — leave it.
|
|
133
|
+
# Without this, two Claude windows on the same repo race each other and
|
|
134
|
+
# whichever started last kills the other's MCP.
|
|
135
|
+
_log.debug("PID %d has a live IDE host; sibling MCP, leaving alone", old_pid)
|
|
136
|
+
return
|
|
93
137
|
try:
|
|
94
138
|
proc = psutil.Process(old_pid)
|
|
95
139
|
_log.warning("Killing stale CodeBrain MCP predecessor PID %d", old_pid)
|
|
@@ -123,6 +167,54 @@ def _remove_pid_file(pid_file: Path) -> None:
|
|
|
123
167
|
pass
|
|
124
168
|
|
|
125
169
|
|
|
170
|
+
def _find_watch_target(start_pid: int) -> tuple[int, float | None]:
|
|
171
|
+
"""Pick the PID whose death should kill the MCP.
|
|
172
|
+
|
|
173
|
+
Walks up the ancestor chain (up to ANCESTOR_WALK_DEPTH levels) looking
|
|
174
|
+
for a process whose name matches HOST_PROCESS_NAME_HINTS — that's the
|
|
175
|
+
real IDE host. Falls back to ``start_pid`` if no hint matches, psutil
|
|
176
|
+
is unavailable, or ``CODEBRAIN_MCP_DISABLE_ANCESTOR_WALK=1`` is set.
|
|
177
|
+
|
|
178
|
+
Why: on Windows, Claude Code spawns the MCP via a transient launcher
|
|
179
|
+
(cmd.exe wrapper or Electron worker shell). The launcher exits soon
|
|
180
|
+
after the python child starts, so watching ``os.getppid()`` directly
|
|
181
|
+
causes the parent watchdog to mis-fire while Claude Code is still up.
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
import psutil
|
|
185
|
+
except ImportError:
|
|
186
|
+
return start_pid, None
|
|
187
|
+
|
|
188
|
+
fallback_create_time: float | None = None
|
|
189
|
+
try:
|
|
190
|
+
fallback_create_time = psutil.Process(start_pid).create_time()
|
|
191
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
192
|
+
return start_pid, None
|
|
193
|
+
|
|
194
|
+
if os.environ.get("CODEBRAIN_MCP_DISABLE_ANCESTOR_WALK") == "1":
|
|
195
|
+
return start_pid, fallback_create_time
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
proc = psutil.Process(start_pid)
|
|
199
|
+
for _ in range(ANCESTOR_WALK_DEPTH):
|
|
200
|
+
try:
|
|
201
|
+
name = (proc.name() or "").lower()
|
|
202
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
203
|
+
break
|
|
204
|
+
if any(hint in name for hint in HOST_PROCESS_NAME_HINTS):
|
|
205
|
+
return proc.pid, proc.create_time()
|
|
206
|
+
try:
|
|
207
|
+
parent = proc.parent()
|
|
208
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
209
|
+
break
|
|
210
|
+
if parent is None:
|
|
211
|
+
break
|
|
212
|
+
proc = parent
|
|
213
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
214
|
+
pass
|
|
215
|
+
return start_pid, fallback_create_time
|
|
216
|
+
|
|
217
|
+
|
|
126
218
|
def _parent_watchdog(initial_ppid: int, initial_create_time: float | None) -> None:
|
|
127
219
|
try:
|
|
128
220
|
import psutil
|
|
@@ -192,17 +284,13 @@ def install_watchdogs(repo_root: Path | None = None) -> None:
|
|
|
192
284
|
_write_pid_file(pid_file)
|
|
193
285
|
atexit.register(_remove_pid_file, pid_file)
|
|
194
286
|
|
|
195
|
-
|
|
196
|
-
initial_create_time
|
|
197
|
-
try:
|
|
198
|
-
import psutil
|
|
199
|
-
initial_create_time = psutil.Process(initial_ppid).create_time()
|
|
200
|
-
except Exception:
|
|
201
|
-
pass
|
|
287
|
+
immediate_ppid = os.getppid()
|
|
288
|
+
initial_ppid, initial_create_time = _find_watch_target(immediate_ppid)
|
|
202
289
|
|
|
203
290
|
_log.info(
|
|
204
|
-
"MCP watchdogs installed (ppid=%d, idle_timeout=%ds, max_lifetime=%ds)",
|
|
291
|
+
"MCP watchdogs installed (ppid=%d via=%d, idle_timeout=%ds, max_lifetime=%ds)",
|
|
205
292
|
initial_ppid,
|
|
293
|
+
immediate_ppid,
|
|
206
294
|
IDLE_TIMEOUT_SECONDS,
|
|
207
295
|
MAX_LIFETIME_SECONDS,
|
|
208
296
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codebrain
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Know what breaks before you break it. Structural knowledge graph for codebases — impact analysis, dead code detection, health scores. No LLM required.
|
|
5
5
|
Author: CodeBrain Contributors
|
|
6
6
|
License: MIT License
|
|
@@ -123,15 +123,17 @@ MCP config. Restart Claude Code and it has tools like
|
|
|
123
123
|
`mcp__codebrain__ask_codebase`, plus a few dozen more. It uses them on
|
|
124
124
|
its own.
|
|
125
125
|
|
|
126
|
-
Manual config (
|
|
126
|
+
Manual config — drop `.mcp.json` at **your project's root** (not in a
|
|
127
|
+
global config). CodeBrain's index lives in `<repo>/.codebrain/`, so a
|
|
128
|
+
global config would make every Claude session in every project fight
|
|
129
|
+
over the same database. `brain setup` writes the right thing for you.
|
|
127
130
|
|
|
128
131
|
```json
|
|
129
132
|
{
|
|
130
133
|
"mcpServers": {
|
|
131
134
|
"codebrain": {
|
|
132
135
|
"command": "python",
|
|
133
|
-
"args": ["-m", "codebrain.mcp_server"]
|
|
134
|
-
"env": { "CODEBRAIN_PROJECT": "/absolute/path/to/repo" }
|
|
136
|
+
"args": ["-m", "codebrain.mcp_server"]
|
|
135
137
|
}
|
|
136
138
|
}
|
|
137
139
|
}
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codebrain"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.3"
|
|
8
8
|
description = "Know what breaks before you break it. Structural knowledge graph for codebases — impact analysis, dead code detection, health scores. No LLM required."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {file = "LICENSE"}
|
|
@@ -109,6 +109,73 @@ def test_find_db_lock_holder_skips_unrelated_process(tmp_path):
|
|
|
109
109
|
assert ml.find_db_lock_holder(tmp_path) is None
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
def test_find_watch_target_falls_back_when_no_host_match(monkeypatch):
|
|
113
|
+
"""No claude/cursor/etc. ancestor → return the start pid unchanged."""
|
|
114
|
+
psutil = pytest.importorskip("psutil")
|
|
115
|
+
|
|
116
|
+
class _Proc:
|
|
117
|
+
def __init__(self, pid, name, parent=None, ctime=123.0):
|
|
118
|
+
self.pid = pid
|
|
119
|
+
self._name = name
|
|
120
|
+
self._parent = parent
|
|
121
|
+
self._ctime = ctime
|
|
122
|
+
def name(self): return self._name
|
|
123
|
+
def parent(self): return self._parent
|
|
124
|
+
def create_time(self): return self._ctime
|
|
125
|
+
|
|
126
|
+
great = _Proc(10, "init")
|
|
127
|
+
grand = _Proc(11, "bash", parent=great, ctime=200.0)
|
|
128
|
+
parent = _Proc(12, "python", parent=grand, ctime=300.0)
|
|
129
|
+
|
|
130
|
+
def fake_process(pid):
|
|
131
|
+
return {10: great, 11: grand, 12: parent}[pid]
|
|
132
|
+
|
|
133
|
+
monkeypatch.setattr(psutil, "Process", fake_process)
|
|
134
|
+
pid, ctime = ml._find_watch_target(12)
|
|
135
|
+
assert pid == 12
|
|
136
|
+
assert ctime == 300.0
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_find_watch_target_walks_to_claude(monkeypatch):
|
|
140
|
+
"""Ancestor named claude.exe → watch that, not the immediate parent."""
|
|
141
|
+
psutil = pytest.importorskip("psutil")
|
|
142
|
+
|
|
143
|
+
class _Proc:
|
|
144
|
+
def __init__(self, pid, name, parent=None, ctime=0.0):
|
|
145
|
+
self.pid = pid
|
|
146
|
+
self._name = name
|
|
147
|
+
self._parent = parent
|
|
148
|
+
self._ctime = ctime
|
|
149
|
+
def name(self): return self._name
|
|
150
|
+
def parent(self): return self._parent
|
|
151
|
+
def create_time(self): return self._ctime
|
|
152
|
+
|
|
153
|
+
claude = _Proc(100, "claude.exe", ctime=1000.0)
|
|
154
|
+
launcher = _Proc(200, "cmd.exe", parent=claude, ctime=2000.0)
|
|
155
|
+
py = _Proc(300, "python.exe", parent=launcher, ctime=3000.0)
|
|
156
|
+
|
|
157
|
+
monkeypatch.setattr(psutil, "Process", lambda pid: {100: claude, 200: launcher, 300: py}[pid])
|
|
158
|
+
pid, ctime = ml._find_watch_target(300)
|
|
159
|
+
assert pid == 100
|
|
160
|
+
assert ctime == 1000.0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_find_watch_target_handles_no_psutil(monkeypatch):
|
|
164
|
+
"""psutil missing → return start_pid with no create_time."""
|
|
165
|
+
import builtins
|
|
166
|
+
real_import = builtins.__import__
|
|
167
|
+
|
|
168
|
+
def blocked(name, *a, **k):
|
|
169
|
+
if name == "psutil":
|
|
170
|
+
raise ImportError("simulated")
|
|
171
|
+
return real_import(name, *a, **k)
|
|
172
|
+
|
|
173
|
+
monkeypatch.setattr(builtins, "__import__", blocked)
|
|
174
|
+
pid, ctime = ml._find_watch_target(42)
|
|
175
|
+
assert pid == 42
|
|
176
|
+
assert ctime is None
|
|
177
|
+
|
|
178
|
+
|
|
112
179
|
def test_install_watchdogs_is_idempotent(tmp_path, monkeypatch):
|
|
113
180
|
monkeypatch.setattr(ml, "_installed", False)
|
|
114
181
|
ml.install_watchdogs(tmp_path)
|
|
@@ -129,8 +196,11 @@ def test_install_watchdogs_kills_stale_predecessor(tmp_path, monkeypatch):
|
|
|
129
196
|
"""A previous PID file pointing to a real codebrain.mcp_server should be
|
|
130
197
|
killed. We simulate by spawning a real subprocess that imports
|
|
131
198
|
codebrain.mcp_server (so the cmdline check passes), then writing its
|
|
132
|
-
PID.
|
|
199
|
+
PID. The host-detection is stubbed because the pytest runner's ancestor
|
|
200
|
+
chain may legitimately contain an IDE host on dev machines (VS Code,
|
|
201
|
+
PyCharm) which would otherwise correctly spare the spawned process."""
|
|
133
202
|
monkeypatch.setattr(ml, "_installed", False)
|
|
203
|
+
monkeypatch.setattr(ml, "_predecessor_has_live_host", lambda _pid: False)
|
|
134
204
|
pid_file = tmp_path / CODEBRAIN_DIR / "mcp.pid"
|
|
135
205
|
pid_file.parent.mkdir(parents=True)
|
|
136
206
|
|
|
@@ -156,6 +226,68 @@ def test_install_watchdogs_kills_stale_predecessor(tmp_path, monkeypatch):
|
|
|
156
226
|
proc.kill()
|
|
157
227
|
|
|
158
228
|
|
|
229
|
+
def test_kill_stale_predecessor_spares_sibling_with_live_host(tmp_path, monkeypatch):
|
|
230
|
+
"""Predecessor whose ancestor chain contains a live IDE host represents a
|
|
231
|
+
concurrent sibling Claude session — must not be terminated."""
|
|
232
|
+
pytest.importorskip("psutil")
|
|
233
|
+
monkeypatch.setattr(ml, "_predecessor_has_live_host", lambda _pid: True)
|
|
234
|
+
|
|
235
|
+
proc = subprocess.Popen(
|
|
236
|
+
[sys.executable, "-c", "import time; time.sleep(10)", "codebrain.mcp_server"],
|
|
237
|
+
)
|
|
238
|
+
try:
|
|
239
|
+
time.sleep(0.3)
|
|
240
|
+
pid_file = tmp_path / "mcp.pid"
|
|
241
|
+
pid_file.write_text(f"{proc.pid}\n0\n")
|
|
242
|
+
|
|
243
|
+
ml._kill_stale_predecessor(pid_file)
|
|
244
|
+
|
|
245
|
+
time.sleep(0.5)
|
|
246
|
+
assert proc.poll() is None, "Sibling MCP killed despite live host"
|
|
247
|
+
finally:
|
|
248
|
+
proc.kill()
|
|
249
|
+
proc.wait(timeout=5)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_predecessor_has_live_host_returns_true_for_claude_ancestor(monkeypatch):
|
|
253
|
+
psutil = pytest.importorskip("psutil")
|
|
254
|
+
|
|
255
|
+
class _Proc:
|
|
256
|
+
def __init__(self, pid, name, parent=None):
|
|
257
|
+
self.pid = pid
|
|
258
|
+
self._name = name
|
|
259
|
+
self._parent = parent
|
|
260
|
+
def name(self): return self._name
|
|
261
|
+
def parent(self): return self._parent
|
|
262
|
+
|
|
263
|
+
claude = _Proc(100, "claude.exe")
|
|
264
|
+
cmd = _Proc(200, "cmd.exe", parent=claude)
|
|
265
|
+
py = _Proc(300, "python.exe", parent=cmd)
|
|
266
|
+
monkeypatch.setattr(psutil, "Process", lambda pid: {100: claude, 200: cmd, 300: py}[pid])
|
|
267
|
+
|
|
268
|
+
assert ml._predecessor_has_live_host(300) is True
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_predecessor_has_live_host_returns_false_for_orphan(monkeypatch):
|
|
272
|
+
psutil = pytest.importorskip("psutil")
|
|
273
|
+
|
|
274
|
+
class _Proc:
|
|
275
|
+
def __init__(self, pid, name, parent=None):
|
|
276
|
+
self.pid = pid
|
|
277
|
+
self._name = name
|
|
278
|
+
self._parent = parent
|
|
279
|
+
def name(self): return self._name
|
|
280
|
+
def parent(self): return self._parent
|
|
281
|
+
|
|
282
|
+
# python ← bash ← init — no IDE host anywhere.
|
|
283
|
+
init = _Proc(1, "init")
|
|
284
|
+
sh = _Proc(2, "bash", parent=init)
|
|
285
|
+
py = _Proc(3, "python.exe", parent=sh)
|
|
286
|
+
monkeypatch.setattr(psutil, "Process", lambda pid: {1: init, 2: sh, 3: py}[pid])
|
|
287
|
+
|
|
288
|
+
assert ml._predecessor_has_live_host(3) is False
|
|
289
|
+
|
|
290
|
+
|
|
159
291
|
# ---------------------------------------------------------------------------
|
|
160
292
|
# Stale-MCP discovery
|
|
161
293
|
# ---------------------------------------------------------------------------
|
|
@@ -284,6 +416,10 @@ def test_parent_watchdog_exits_when_parent_dies(tmp_path):
|
|
|
284
416
|
|
|
285
417
|
env = os.environ.copy()
|
|
286
418
|
env["CB_TEST_REPO"] = str(tmp_path)
|
|
419
|
+
# Without this, _find_watch_target may walk past the test parent to the
|
|
420
|
+
# IDE running pytest (claude/cursor/code) and watch *that* — which stays
|
|
421
|
+
# alive when we kill the immediate parent, defeating the test.
|
|
422
|
+
env["CODEBRAIN_MCP_DISABLE_ANCESTOR_WALK"] = "1"
|
|
287
423
|
parent = subprocess.Popen(
|
|
288
424
|
[sys.executable, "-c", parent_script],
|
|
289
425
|
stdout=subprocess.PIPE,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|