codebrain 0.3.1__py3-none-any.whl → 0.3.3__py3-none-any.whl

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/__init__.py CHANGED
@@ -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"
codebrain/cli.py CHANGED
@@ -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
  }
@@ -1,11 +1,11 @@
1
- codebrain/__init__.py,sha256=nsPZeGCmOLCMzB2G177d_qr41TxyAMLOr8l4xDXcW-E,392
1
+ codebrain/__init__.py,sha256=hgsuOwRgYpLaAkDEScZOHphtJuwPjL4EogAfpqqV-ss,392
2
2
  codebrain/__main__.py,sha256=dgd9lRZovV1k1tScEW_wvPPjPEACXc-EcLtJ7AN3M48,115
3
3
  codebrain/agent_bridge.py,sha256=JDny3232R6rfFsQTWOoYygGgykx6rb8ektLRENPUcDQ,6262
4
4
  codebrain/analyzer.py,sha256=RxyajZp0E8XuB_1SkK1UEXD00gOo3e2vtBzS-US6ms8,40701
5
5
  codebrain/api.py,sha256=LmeFK3KyUKR1xz4tAoPteaRA06c09KA965Hk33VhV6I,31653
6
6
  codebrain/api_models.py,sha256=mE5sIsf8WLEWjGm2w6-baeyVoJH_65FOglHwFe3aJhs,1989
7
7
  codebrain/architecture.py,sha256=RCN_LUNTjd0fXYFIYDacwH17qEeFCxNCyF6irbs5yfE,29485
8
- codebrain/cli.py,sha256=BJIYqWmMaJXWkKS3QMaLr-z0s9n4aCcpNI0te1kuJUM,168822
8
+ codebrain/cli.py,sha256=UKGKCU6SRsck2mD8vyl_aA5muO3KlG8bnts4Xu0w52k,169014
9
9
  codebrain/comprehension.py,sha256=eAVf1abecxVo72_2p8aEgRmSeXR7VNMyJnt_DTst47E,83023
10
10
  codebrain/config.py,sha256=UvStpfDNvulMHkN7xnGjEF3EKvdf9tF3Cv4A01FdEQw,1745
11
11
  codebrain/context.py,sha256=BFZ_WWRf242cJli-TXoz5JFqcvsKkwbmuUMBX8WvM9o,10980
@@ -23,7 +23,7 @@ codebrain/kt.py,sha256=iXeHArPbSrrr2ObH4nuyXPobdBXSOR3b7JbIqXB0kj4,17199
23
23
  codebrain/kt_video.py,sha256=quaUyPUJh8xXsj52aBCPp_L8fekNC4vHmNYrUTfCqcE,29211
24
24
  codebrain/llm.py,sha256=0D5c5BJMVkLz2OoybfMxlUdJEbz16cZoNgVmm-LzFH0,25606
25
25
  codebrain/logging.py,sha256=ORR5L8REVlh4aJz9vqErmi76aqjLN2xVKoA2gSv_jas,1110
26
- codebrain/mcp_lifecycle.py,sha256=VA67vULtTKzhShiA3cA1KACBjKr9wVQbQjCzqjz895c,10575
26
+ codebrain/mcp_lifecycle.py,sha256=FzZBKk_MLUFC97TgT_M_EG0oNZgUAPZXzukm1U3LUKc,14172
27
27
  codebrain/mcp_server.py,sha256=Lb5ob3NI-S0Ln7Yp8G8HxWsKvAuzVzl123N08ZkxPyw,110648
28
28
  codebrain/migration.py,sha256=ZMcug6OsvvK-DVdfmqhWCUx-oVj0KQD0EGlBVx6Jxvk,29489
29
29
  codebrain/modernize.py,sha256=4usRjIFONEThpYhemLIjoYXb9ohEbO7j2wzVO15Dz5A,38525
@@ -74,9 +74,9 @@ codebrain/parser/typescript_treesitter.py,sha256=7RTdcA-HTgrlRhEMwSGa-63saNcjqtP
74
74
  codebrain/parser/vue_parser.py,sha256=ZWtjUOyItn_SNfvD6T8j7alV4aIfcHA94WDCS5P59Ps,13054
75
75
  codebrain/watcher/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
76
76
  codebrain/watcher/file_watcher.py,sha256=EUXQ5L8gutOCvGgtNZ9wMt6RL74wA6snH7vnDnxm13A,6562
77
- codebrain-0.3.1.dist-info/licenses/LICENSE,sha256=Dxb0L3H90lGFFik90WQXfMkM_8utGA1BDqizqdCT3UE,1079
78
- codebrain-0.3.1.dist-info/METADATA,sha256=QcpdsZ2pSV2KPKYs2LmJcmg_uagdCDMm9kFIlxqIHfA,9953
79
- codebrain-0.3.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
80
- codebrain-0.3.1.dist-info/entry_points.txt,sha256=FHKSyI7le7GK78HxAKQK54-yzt-yHNxX0df3VpGC5wg,44
81
- codebrain-0.3.1.dist-info/top_level.txt,sha256=mUxCZc80EyNOMzd2vAm22uIhnOb1Aw7ZOAUT_u-ksx4,10
82
- codebrain-0.3.1.dist-info/RECORD,,
77
+ codebrain-0.3.3.dist-info/licenses/LICENSE,sha256=Dxb0L3H90lGFFik90WQXfMkM_8utGA1BDqizqdCT3UE,1079
78
+ codebrain-0.3.3.dist-info/METADATA,sha256=h3KSuM87MlEXw-mhfoTpZDlKqyToQVPt_E594UEJPWM,10129
79
+ codebrain-0.3.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
80
+ codebrain-0.3.3.dist-info/entry_points.txt,sha256=FHKSyI7le7GK78HxAKQK54-yzt-yHNxX0df3VpGC5wg,44
81
+ codebrain-0.3.3.dist-info/top_level.txt,sha256=mUxCZc80EyNOMzd2vAm22uIhnOb1Aw7ZOAUT_u-ksx4,10
82
+ codebrain-0.3.3.dist-info/RECORD,,