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.
Files changed (168) hide show
  1. {codebrain-0.3.1 → codebrain-0.3.3}/PKG-INFO +6 -4
  2. {codebrain-0.3.1 → codebrain-0.3.3}/README.md +5 -3
  3. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/__init__.py +1 -1
  4. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/cli.py +22 -20
  5. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/mcp_lifecycle.py +96 -8
  6. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain.egg-info/PKG-INFO +6 -4
  7. {codebrain-0.3.1 → codebrain-0.3.3}/pyproject.toml +1 -1
  8. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_mcp_lifecycle.py +137 -1
  9. {codebrain-0.3.1 → codebrain-0.3.3}/LICENSE +0 -0
  10. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/__main__.py +0 -0
  11. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/actions/__init__.py +0 -0
  12. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/actions/base.py +0 -0
  13. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/actions/refactor.py +0 -0
  14. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/actions/reviewer.py +0 -0
  15. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/actions/test_gen.py +0 -0
  16. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/agent_bridge.py +0 -0
  17. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/analyzer.py +0 -0
  18. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/api.py +0 -0
  19. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/api_models.py +0 -0
  20. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/architecture.py +0 -0
  21. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/comprehension.py +0 -0
  22. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/config.py +0 -0
  23. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/context.py +0 -0
  24. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/cross_query.py +0 -0
  25. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/cross_registry.py +0 -0
  26. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/diff_impact.py +0 -0
  27. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/env_migration.py +0 -0
  28. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/equivalence.py +0 -0
  29. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/export.py +0 -0
  30. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/frontend.py +0 -0
  31. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/graph/__init__.py +0 -0
  32. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/graph/query.py +0 -0
  33. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/graph/schema.py +0 -0
  34. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/graph/store.py +0 -0
  35. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/hook_runner.py +0 -0
  36. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/hooks.py +0 -0
  37. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/indexer.py +0 -0
  38. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/kt.py +0 -0
  39. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/kt_video.py +0 -0
  40. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/llm.py +0 -0
  41. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/logging.py +0 -0
  42. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/mcp_server.py +0 -0
  43. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/memory/__init__.py +0 -0
  44. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/memory/store.py +0 -0
  45. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/migration.py +0 -0
  46. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/modernize.py +0 -0
  47. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/onboard.py +0 -0
  48. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/__init__.py +0 -0
  49. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/base.py +0 -0
  50. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/cobol_parser.py +0 -0
  51. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/config_parser.py +0 -0
  52. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/csharp_parser.py +0 -0
  53. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/dart_parser.py +0 -0
  54. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/fortran_parser.py +0 -0
  55. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/frontend_parser.py +0 -0
  56. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/go_parser.py +0 -0
  57. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/java_parser.py +0 -0
  58. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/kotlin_parser.py +0 -0
  59. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/models.py +0 -0
  60. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/mumps_parser.py +0 -0
  61. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/plsql_parser.py +0 -0
  62. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/python_parser.py +0 -0
  63. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/registry.py +0 -0
  64. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/rust_parser.py +0 -0
  65. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/schema_parser.py +0 -0
  66. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/typescript_parser.py +0 -0
  67. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/typescript_treesitter.py +0 -0
  68. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/parser/vue_parser.py +0 -0
  69. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/py.typed +0 -0
  70. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/resolver.py +0 -0
  71. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/rewriter.py +0 -0
  72. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/schema_migration.py +0 -0
  73. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/settings.py +0 -0
  74. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/susa_auth.py +0 -0
  75. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/test_gaps.py +0 -0
  76. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/test_runner.py +0 -0
  77. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/tour.py +0 -0
  78. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/ui_migration.py +0 -0
  79. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/utils.py +0 -0
  80. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/validator.py +0 -0
  81. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/watcher/__init__.py +0 -0
  82. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain/watcher/file_watcher.py +0 -0
  83. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain.egg-info/SOURCES.txt +0 -0
  84. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain.egg-info/dependency_links.txt +0 -0
  85. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain.egg-info/entry_points.txt +0 -0
  86. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain.egg-info/requires.txt +0 -0
  87. {codebrain-0.3.1 → codebrain-0.3.3}/codebrain.egg-info/top_level.txt +0 -0
  88. {codebrain-0.3.1 → codebrain-0.3.3}/setup.cfg +0 -0
  89. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_actions.py +0 -0
  90. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_agent_bridge.py +0 -0
  91. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_analyzer.py +0 -0
  92. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_api.py +0 -0
  93. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_architecture.py +0 -0
  94. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_ci.py +0 -0
  95. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_cli.py +0 -0
  96. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_comprehension.py +0 -0
  97. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_context.py +0 -0
  98. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_contracts_real.py +0 -0
  99. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_coverage_gaps.py +0 -0
  100. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_cross_repo.py +0 -0
  101. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_csharp_parser.py +0 -0
  102. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_dart_parser.py +0 -0
  103. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_dataflow.py +0 -0
  104. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_dead_code_confidence.py +0 -0
  105. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_diff_impact.py +0 -0
  106. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_env_migration.py +0 -0
  107. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_equivalence.py +0 -0
  108. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_error_recovery.py +0 -0
  109. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_export.py +0 -0
  110. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_fingerprints.py +0 -0
  111. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_frontend.py +0 -0
  112. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_gate_battle.py +0 -0
  113. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_go_parser.py +0 -0
  114. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_hooks.py +0 -0
  115. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_indexer.py +0 -0
  116. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_infra_parser.py +0 -0
  117. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_install.py +0 -0
  118. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_integration.py +0 -0
  119. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_java_parser.py +0 -0
  120. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_jyotishyamitra.py +0 -0
  121. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_kotlin_parser.py +0 -0
  122. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_kt.py +0 -0
  123. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_legacy_parsers.py +0 -0
  124. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_llm.py +0 -0
  125. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_mcp_server.py +0 -0
  126. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_memory.py +0 -0
  127. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_migration.py +0 -0
  128. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_modernize.py +0 -0
  129. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_multi_project_cli.py +0 -0
  130. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_narratives.py +0 -0
  131. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_onboard.py +0 -0
  132. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_orm_detection.py +0 -0
  133. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_output_quality.py +0 -0
  134. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_parser.py +0 -0
  135. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_plugin_system.py +0 -0
  136. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_production_hardening.py +0 -0
  137. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_query.py +0 -0
  138. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_real_codebase.py +0 -0
  139. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_real_features.py +0 -0
  140. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_real_frontend.py +0 -0
  141. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_real_repos.py +0 -0
  142. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_real_world.py +0 -0
  143. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_resolver.py +0 -0
  144. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_rewriter.py +0 -0
  145. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_rust_parser.py +0 -0
  146. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_scale.py +0 -0
  147. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_scale_optimizations.py +0 -0
  148. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_scale_real.py +0 -0
  149. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_schema.py +0 -0
  150. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_schema_migration.py +0 -0
  151. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_schema_parser.py +0 -0
  152. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_settings.py +0 -0
  153. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_store.py +0 -0
  154. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_test_runner.py +0 -0
  155. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_tour.py +0 -0
  156. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_translate.py +0 -0
  157. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_ts_ast_parser.py +0 -0
  158. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_ts_parser_enhanced.py +0 -0
  159. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_typescript_parser.py +0 -0
  160. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_ui_migration.py +0 -0
  161. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_utils.py +0 -0
  162. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_validation_narratives.py +0 -0
  163. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_validator.py +0 -0
  164. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_validator_scenarios.py +0 -0
  165. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_vscode_extension.py +0 -0
  166. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_watch_validate.py +0 -0
  167. {codebrain-0.3.1 → codebrain-0.3.3}/tests/test_watcher.py +0 -0
  168. {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.1
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 (any MCP-compatible client):
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 (any MCP-compatible client):
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
  }
@@ -7,4 +7,4 @@ try:
7
7
  except PackageNotFoundError:
8
8
  # Source checkout without installation (e.g. running from a worktree).
9
9
  # Keep in sync with [project.version] in pyproject.toml.
10
- __version__ = "0.3.1"
10
+ __version__ = "0.3.3"
@@ -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 globally
402
- settings_path = Path.home() / ".claude" / "settings.json"
403
- settings_path.parent.mkdir(parents=True, exist_ok=True)
404
- if settings_path.exists():
405
- settings = json.loads(settings_path.read_text())
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
- settings = {}
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
- settings_path.write_text(json.dumps(settings, indent=2))
420
- click.echo(f"Added CodeBrain MCP server to {settings_path}")
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 now use CodeBrain tools to understand your codebase.")
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
- initial_ppid = os.getppid()
196
- initial_create_time: float | None = None
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.1
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 (any MCP-compatible client):
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.1"
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