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 +1 -1
- codebrain/cli.py +22 -20
- codebrain/mcp_lifecycle.py +96 -8
- {codebrain-0.3.1.dist-info → codebrain-0.3.3.dist-info}/METADATA +6 -4
- {codebrain-0.3.1.dist-info → codebrain-0.3.3.dist-info}/RECORD +9 -9
- {codebrain-0.3.1.dist-info → codebrain-0.3.3.dist-info}/WHEEL +0 -0
- {codebrain-0.3.1.dist-info → codebrain-0.3.3.dist-info}/entry_points.txt +0 -0
- {codebrain-0.3.1.dist-info → codebrain-0.3.3.dist-info}/licenses/LICENSE +0 -0
- {codebrain-0.3.1.dist-info → codebrain-0.3.3.dist-info}/top_level.txt +0 -0
codebrain/__init__.py
CHANGED
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
|
|
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:
|
codebrain/mcp_lifecycle.py
CHANGED
|
@@ -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
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
codebrain/__init__.py,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
78
|
-
codebrain-0.3.
|
|
79
|
-
codebrain-0.3.
|
|
80
|
-
codebrain-0.3.
|
|
81
|
-
codebrain-0.3.
|
|
82
|
-
codebrain-0.3.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|