codecompass-mcp 6.0.0__tar.gz → 6.0.2__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 (41) hide show
  1. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/PKG-INFO +1 -1
  2. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/codecompass_mcp.egg-info/PKG-INFO +1 -1
  3. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/pi_setup.py +50 -9
  4. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/pyproject.toml +1 -1
  5. codecompass_mcp-6.0.2/tests/test_pi_setup.py +93 -0
  6. codecompass_mcp-6.0.0/tests/test_pi_setup.py +0 -42
  7. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/LICENSE +0 -0
  8. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/README.md +0 -0
  9. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/codecompass_mcp.egg-info/SOURCES.txt +0 -0
  10. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/codecompass_mcp.egg-info/dependency_links.txt +0 -0
  11. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/codecompass_mcp.egg-info/entry_points.txt +0 -0
  12. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/codecompass_mcp.egg-info/requires.txt +0 -0
  13. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/codecompass_mcp.egg-info/top_level.txt +0 -0
  14. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/config.py +0 -0
  15. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/graph/__init__.py +0 -0
  16. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/graph/code_graph_client.py +0 -0
  17. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/graph/code_queries.py +0 -0
  18. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/graph/vector_store.py +0 -0
  19. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/ingestion/__init__.py +0 -0
  20. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/ingestion/agent_writes.py +0 -0
  21. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/ingestion/code_normalizer.py +0 -0
  22. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/ingestion/code_parser.py +0 -0
  23. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/ingestion/file_watcher.py +0 -0
  24. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/ingestion/hierarchy_builder.py +0 -0
  25. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/ingestion/source_context.py +0 -0
  26. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/main.py +0 -0
  27. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/mcp_server.py +0 -0
  28. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/models/__init__.py +0 -0
  29. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/models/code_types.py +0 -0
  30. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/setup.cfg +0 -0
  31. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/tests/test_accuracy_fixes.py +0 -0
  32. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/tests/test_agent_writes.py +0 -0
  33. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/tests/test_block_hook.py +0 -0
  34. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/tests/test_code_parser.py +0 -0
  35. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/tests/test_descriptions.py +0 -0
  36. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/tests/test_ingest_progress.py +0 -0
  37. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/tests/test_init.py +0 -0
  38. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/tests/test_lit_css_parser.py +0 -0
  39. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/tests/test_mcp_server.py +0 -0
  40. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/tests/test_php_parser.py +0 -0
  41. {codecompass_mcp-6.0.0 → codecompass_mcp-6.0.2}/tests/test_vector_store.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecompass-mcp
3
- Version: 6.0.0
3
+ Version: 6.0.2
4
4
  Summary: Structural code context for AI coding agents — a local code knowledge graph for blast radius, impact, deps, dead code, and flow tracing
5
5
  Author: CodeCompass contributors
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecompass-mcp
3
- Version: 6.0.0
3
+ Version: 6.0.2
4
4
  Summary: Structural code context for AI coding agents — a local code knowledge graph for blast radius, impact, deps, dead code, and flow tracing
5
5
  Author: CodeCompass contributors
6
6
  License: MIT
@@ -20,6 +20,7 @@ from __future__ import annotations
20
20
  import json
21
21
  import shutil
22
22
  import subprocess
23
+ import sys
23
24
  from pathlib import Path
24
25
 
25
26
  # Where pi loads user-global skills and pi-mcp-adapter reads user-global servers.
@@ -30,21 +31,47 @@ _MCP_CONFIG = Path.home() / ".config" / "mcp" / "mcp.json"
30
31
  _ADAPTER_PKG = "npm:pi-mcp-adapter"
31
32
  _ADAPTER_NAME = "pi-mcp-adapter"
32
33
 
34
+ def _server_command() -> str:
35
+ """Absolute path to the codecompass-mcp console script, when we can find it.
36
+
37
+ A bare "codecompass-mcp" only resolves if pi inherits a PATH that reaches
38
+ this interpreter's bin dir — which it often doesn't. Under pyenv it is
39
+ worse than missing: the shim resolves a *different* Python depending on the
40
+ launch directory's .python-version, so pi silently gets "command not found"
41
+ while the same name works in a shell inside the project. The script sitting
42
+ next to sys.executable is the one belonging to the environment codecompass
43
+ is actually installed in.
44
+ """
45
+ script = Path(sys.executable).with_name("codecompass-mcp")
46
+ return str(script) if script.exists() else "codecompass-mcp"
47
+
48
+
33
49
  # The codecompass-mcp server entry merged into the user-global mcp.json.
34
- _SERVER_CONFIG = {"command": "codecompass-mcp"}
50
+ def _server_config() -> dict:
51
+ return {"command": _server_command()}
35
52
 
36
53
  # Shipped as the pi skill. pi has the MCP tools natively via pi-mcp-adapter;
37
54
  # this teaches the orient-first discipline and lists the capabilities.
38
55
  #
39
- # The marker below is what makes the skill self-updating: init-style, we only
56
+ # The marker is what makes the skill self-updating: init-style, we only
40
57
  # overwrite files we wrote. Strip the line to take ownership of your copy.
58
+ # It goes in the BODY, never above the frontmatter — pi requires the opening
59
+ # `---` on line 1 and reports "description is required" for anything else.
41
60
  _SKILL_MARKER = "<!-- Installed by `codecompass setup-pi` — rewritten on upgrade. -->"
42
61
 
43
- _SKILL_MD = _SKILL_MARKER + """
62
+ # Copies installed before the marker existed. A file carrying one of these is
63
+ # ours to replace, even without the marker — otherwise every pre-6.0.0 install
64
+ # would be pinned to its original skill text forever.
65
+ _LEGACY_SIGNATURES = (
66
+ "CodeCompass maps a repo into a queryable graph",
67
+ )
68
+
69
+ _SKILL_MD = """\
44
70
  ---
45
71
  name: codecompass
46
72
  description: Orient in any indexed repo through the CodeCompass code graph before reading files. Use for discovery, impact/dependency traces, dead-code checks, and flow analysis in any repository with a .codecompass/graph.json index.
47
73
  ---
74
+ """ + _SKILL_MARKER + """
48
75
 
49
76
  # CodeCompass
50
77
 
@@ -114,8 +141,10 @@ def _install_adapter() -> None:
114
141
  subprocess.run(["pi", "install", _ADAPTER_PKG], check=False, timeout=300)
115
142
 
116
143
 
117
- def _write_mcp_config() -> None:
118
- """Merge the codecompass server into the user-global mcp.json, preserving others."""
144
+ def _write_mcp_config() -> bool:
145
+ """Merge the codecompass server into the user-global mcp.json, preserving
146
+ others. Returns True if the file changed — a no-op when already correct, so
147
+ this is cheap enough to run on every invocation."""
119
148
  config: dict = {}
120
149
  if _MCP_CONFIG.exists():
121
150
  try:
@@ -123,19 +152,26 @@ def _write_mcp_config() -> None:
123
152
  except (json.JSONDecodeError, OSError):
124
153
  config = {}
125
154
  servers = config.setdefault("mcpServers", {})
126
- servers["codecompass"] = _SERVER_CONFIG
155
+ wanted = _server_config()
156
+ if servers.get("codecompass") == wanted:
157
+ return False
158
+ servers["codecompass"] = wanted
127
159
  _MCP_CONFIG.parent.mkdir(parents=True, exist_ok=True)
128
160
  _MCP_CONFIG.write_text(json.dumps(config, indent=2) + "\n")
161
+ return True
129
162
 
130
163
 
131
164
  def _skill_is_current() -> bool:
132
165
  """True when the installed skill needs no write — either it is already our
133
- latest text, or the user has taken it over by stripping the marker."""
166
+ latest text, or it is a file the user wrote and we must not touch."""
134
167
  try:
135
168
  existing = _SKILL_FILE.read_text()
136
169
  except OSError:
137
170
  return False
138
- return existing == _SKILL_MD or _SKILL_MARKER not in existing
171
+ if existing == _SKILL_MD:
172
+ return True
173
+ ours = _SKILL_MARKER in existing or any(s in existing for s in _LEGACY_SIGNATURES)
174
+ return not ours
139
175
 
140
176
 
141
177
  def setup_pi(force: bool = False, quiet: bool = False) -> bool:
@@ -155,6 +191,12 @@ def setup_pi(force: bool = False, quiet: bool = False) -> bool:
155
191
  say("pi not installed; skipping CodeCompass pi setup.")
156
192
  return False
157
193
 
194
+ # Always reconcile the server entry: the path it should point at changes
195
+ # when codecompass is reinstalled into a different environment, and a stale
196
+ # one leaves pi with a server it cannot spawn.
197
+ if _write_mcp_config():
198
+ say(f"Pointed pi at {_server_command()} in {_MCP_CONFIG}")
199
+
158
200
  if _skill_is_current() and not force:
159
201
  return True
160
202
 
@@ -164,7 +206,6 @@ def setup_pi(force: bool = False, quiet: bool = False) -> bool:
164
206
 
165
207
  _SKILL_DIR.mkdir(parents=True, exist_ok=True)
166
208
  _SKILL_FILE.write_text(_SKILL_MD)
167
- _write_mcp_config()
168
209
  say(f"CodeCompass wired into pi: {_SKILL_FILE}, {_MCP_CONFIG}")
169
210
  return True
170
211
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codecompass-mcp"
3
- version = "6.0.0"
3
+ version = "6.0.2"
4
4
  description = "Structural code context for AI coding agents — a local code knowledge graph for blast radius, impact, deps, dead code, and flow tracing"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -0,0 +1,93 @@
1
+ """The pi skill file self-updates on upgrade, but never clobbers a user's copy;
2
+ the mcp.json entry always points at a command pi can actually spawn."""
3
+
4
+ import json
5
+
6
+ import pi_setup
7
+
8
+
9
+ def test_skill_frontmatter_is_first():
10
+ """pi parses YAML frontmatter only when `---` is line 1 — anything above it
11
+ (like our generated-by marker) makes it report "description is required"."""
12
+ lines = pi_setup._SKILL_MD.splitlines()
13
+ assert lines[0] == "---"
14
+ assert any(line.startswith("description:") for line in lines[:5])
15
+ closing = lines.index("---", 1)
16
+ assert pi_setup._SKILL_MARKER in lines[closing + 1] # marker sits in the body
17
+
18
+
19
+ def _install(tmp_path, monkeypatch):
20
+ """Point pi_setup at a temp home and pretend pi + adapter are installed."""
21
+ skill_file = tmp_path / "SKILL.md"
22
+ monkeypatch.setattr(pi_setup, "_SKILL_DIR", tmp_path)
23
+ monkeypatch.setattr(pi_setup, "_SKILL_FILE", skill_file)
24
+ monkeypatch.setattr(pi_setup, "_MCP_CONFIG", tmp_path / "mcp.json")
25
+ monkeypatch.setattr(pi_setup, "_pi_available", lambda: True)
26
+ monkeypatch.setattr(pi_setup, "_adapter_installed", lambda: True)
27
+ return skill_file
28
+
29
+
30
+ def test_stale_skill_is_rewritten_but_user_edits_are_kept(tmp_path, monkeypatch):
31
+ skill_file = _install(tmp_path, monkeypatch)
32
+
33
+ assert pi_setup.setup_pi(quiet=True) is True
34
+ assert skill_file.read_text() == pi_setup._SKILL_MD
35
+
36
+ # An older release's text — marker present, content stale -> rewritten.
37
+ skill_file.write_text(pi_setup._SKILL_MARKER + "\nold instructions\n")
38
+ pi_setup.setup_pi(quiet=True)
39
+ assert skill_file.read_text() == pi_setup._SKILL_MD
40
+
41
+ # The user took ownership by stripping the marker -> left alone.
42
+ skill_file.write_text("my own skill\n")
43
+ pi_setup.setup_pi(quiet=True)
44
+ assert skill_file.read_text() == "my own skill\n"
45
+
46
+ # ...unless explicitly forced.
47
+ pi_setup.setup_pi(force=True, quiet=True)
48
+ assert skill_file.read_text() == pi_setup._SKILL_MD
49
+
50
+
51
+ def test_pre_marker_skill_is_adopted_and_updated(tmp_path, monkeypatch):
52
+ """A copy installed before the marker existed is still ours to replace."""
53
+ skill_file = _install(tmp_path, monkeypatch)
54
+ skill_file.write_text(
55
+ "---\nname: codecompass\n---\n\n"
56
+ "CodeCompass maps a repo into a queryable graph so you orient...\n"
57
+ )
58
+
59
+ pi_setup.setup_pi(quiet=True)
60
+ assert skill_file.read_text() == pi_setup._SKILL_MD
61
+
62
+
63
+ def test_mcp_config_points_at_an_absolute_command(tmp_path, monkeypatch):
64
+ """A bare name resolves through PATH/pyenv and can land on a Python that
65
+ doesn't have codecompass installed — pin the interpreter's own script."""
66
+ _install(tmp_path, monkeypatch)
67
+ config = tmp_path / "mcp.json"
68
+
69
+ script = tmp_path / "codecompass-mcp"
70
+ script.write_text("#!/bin/sh\n")
71
+ monkeypatch.setattr(pi_setup.sys, "executable", str(tmp_path / "python"))
72
+
73
+ pi_setup.setup_pi(quiet=True)
74
+ entry = json.loads(config.read_text())["mcpServers"]["codecompass"]
75
+ assert entry == {"command": str(script)}
76
+
77
+ # Other servers in the file are preserved, and a stale codecompass entry is
78
+ # corrected even when the skill file is already up to date.
79
+ config.write_text(json.dumps({"mcpServers": {
80
+ "other": {"command": "keep-me"},
81
+ "codecompass": {"command": "codecompass-mcp"},
82
+ }}))
83
+ pi_setup.setup_pi(quiet=True)
84
+ servers = json.loads(config.read_text())["mcpServers"]
85
+ assert servers["other"] == {"command": "keep-me"}
86
+ assert servers["codecompass"] == {"command": str(script)}
87
+
88
+
89
+ def test_no_pi_no_write(tmp_path, monkeypatch):
90
+ skill_file = _install(tmp_path, monkeypatch)
91
+ monkeypatch.setattr(pi_setup, "_pi_available", lambda: False)
92
+ assert pi_setup.setup_pi(quiet=True) is False
93
+ assert not skill_file.exists()
@@ -1,42 +0,0 @@
1
- """The pi skill file self-updates on upgrade, but never clobbers a user's copy."""
2
-
3
- import pi_setup
4
-
5
-
6
- def _install(tmp_path, monkeypatch):
7
- """Point pi_setup at a temp home and pretend pi + adapter are installed."""
8
- skill_file = tmp_path / "SKILL.md"
9
- monkeypatch.setattr(pi_setup, "_SKILL_DIR", tmp_path)
10
- monkeypatch.setattr(pi_setup, "_SKILL_FILE", skill_file)
11
- monkeypatch.setattr(pi_setup, "_MCP_CONFIG", tmp_path / "mcp.json")
12
- monkeypatch.setattr(pi_setup, "_pi_available", lambda: True)
13
- monkeypatch.setattr(pi_setup, "_adapter_installed", lambda: True)
14
- return skill_file
15
-
16
-
17
- def test_stale_skill_is_rewritten_but_user_edits_are_kept(tmp_path, monkeypatch):
18
- skill_file = _install(tmp_path, monkeypatch)
19
-
20
- assert pi_setup.setup_pi(quiet=True) is True
21
- assert skill_file.read_text() == pi_setup._SKILL_MD
22
-
23
- # An older release's text — marker present, content stale -> rewritten.
24
- skill_file.write_text(pi_setup._SKILL_MARKER + "\nold instructions\n")
25
- pi_setup.setup_pi(quiet=True)
26
- assert skill_file.read_text() == pi_setup._SKILL_MD
27
-
28
- # The user took ownership by stripping the marker -> left alone.
29
- skill_file.write_text("my own skill\n")
30
- pi_setup.setup_pi(quiet=True)
31
- assert skill_file.read_text() == "my own skill\n"
32
-
33
- # ...unless explicitly forced.
34
- pi_setup.setup_pi(force=True, quiet=True)
35
- assert skill_file.read_text() == pi_setup._SKILL_MD
36
-
37
-
38
- def test_no_pi_no_write(tmp_path, monkeypatch):
39
- skill_file = _install(tmp_path, monkeypatch)
40
- monkeypatch.setattr(pi_setup, "_pi_available", lambda: False)
41
- assert pi_setup.setup_pi(quiet=True) is False
42
- assert not skill_file.exists()
File without changes
File without changes