codecompass-mcp 2.2.0__tar.gz → 2.3.0__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 (38) hide show
  1. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/PKG-INFO +3 -3
  2. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/README.md +2 -2
  3. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/codecompass_mcp.egg-info/PKG-INFO +3 -3
  4. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/ingestion/chunker.py +0 -9
  5. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/ingestion/code_parser.py +90 -8
  6. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/ingestion/entity_resolver.py +13 -15
  7. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/ingestion/reader_agent.py +4 -3
  8. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/main.py +20 -0
  9. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/pyproject.toml +1 -1
  10. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/LICENSE +0 -0
  11. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/codecompass_mcp.egg-info/SOURCES.txt +0 -0
  12. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/codecompass_mcp.egg-info/dependency_links.txt +0 -0
  13. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/codecompass_mcp.egg-info/entry_points.txt +0 -0
  14. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/codecompass_mcp.egg-info/requires.txt +0 -0
  15. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/codecompass_mcp.egg-info/top_level.txt +0 -0
  16. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/config.py +0 -0
  17. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/graph/__init__.py +0 -0
  18. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/graph/cli.py +0 -0
  19. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/graph/code_graph_client.py +0 -0
  20. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/graph/code_query_cli.py +0 -0
  21. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/graph/setup.py +0 -0
  22. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/ingestion/__init__.py +0 -0
  23. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/ingestion/code_normalizer.py +0 -0
  24. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/ingestion/file_watcher.py +0 -0
  25. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/ingestion/graph_writer.py +0 -0
  26. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/ingestion/hierarchy_builder.py +0 -0
  27. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/ingestion/source_context.py +0 -0
  28. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/models/__init__.py +0 -0
  29. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/models/code_types.py +0 -0
  30. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/models/types.py +0 -0
  31. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/setup.cfg +0 -0
  32. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/tests/test_batch_impact.py +0 -0
  33. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/tests/test_blast_radius.py +0 -0
  34. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/tests/test_code_parser.py +0 -0
  35. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/tests/test_ingestion.py +0 -0
  36. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/tests/test_lit_css_parser.py +0 -0
  37. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/utils/__init__.py +0 -0
  38. {codecompass_mcp-2.2.0 → codecompass_mcp-2.3.0}/utils/formatting.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecompass-mcp
3
- Version: 2.2.0
3
+ Version: 2.3.0
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
@@ -129,10 +129,10 @@ All commands default to the current directory.
129
129
  ### Install
130
130
 
131
131
  ```bash
132
- # From the codecompass directory
133
- pip install -e .
132
+ pip install codecompass-mcp
134
133
  ```
135
134
 
135
+
136
136
  ### Index a project
137
137
 
138
138
  ```bash
@@ -92,10 +92,10 @@ All commands default to the current directory.
92
92
  ### Install
93
93
 
94
94
  ```bash
95
- # From the codecompass directory
96
- pip install -e .
95
+ pip install codecompass-mcp
97
96
  ```
98
97
 
98
+
99
99
  ### Index a project
100
100
 
101
101
  ```bash
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecompass-mcp
3
- Version: 2.2.0
3
+ Version: 2.3.0
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
@@ -129,10 +129,10 @@ All commands default to the current directory.
129
129
  ### Install
130
130
 
131
131
  ```bash
132
- # From the codecompass directory
133
- pip install -e .
132
+ pip install codecompass-mcp
134
133
  ```
135
134
 
135
+
136
136
  ### Index a project
137
137
 
138
138
  ```bash
@@ -1,5 +1,4 @@
1
1
  import re
2
- import PyPDF2
3
2
 
4
3
  # Rough approximation: 1 token ≈ 4 characters (standard for English prose/code).
5
4
  # Used when a tokeniser library is unavailable.
@@ -10,14 +9,6 @@ def _estimate_tokens(text: str) -> int:
10
9
  return max(1, len(text) // _CHARS_PER_TOKEN)
11
10
 
12
11
 
13
- def chunk_pdf(filepath: str, tokens_per_chunk: int = 500, overlap_tokens: int = 50) -> list[str]:
14
- """Split PDF into overlapping token-sized chunks."""
15
- with open(filepath, "rb") as f:
16
- reader = PyPDF2.PdfReader(f)
17
- full_text = " ".join(page.extract_text() or "" for page in reader.pages)
18
- return chunk_text(full_text, tokens_per_chunk=tokens_per_chunk, overlap_tokens=overlap_tokens)
19
-
20
-
21
12
  def chunk_text(text: str, tokens_per_chunk: int = 500, overlap_tokens: int = 50) -> list[str]:
22
13
  """
23
14
  Split text into chunks of approximately `tokens_per_chunk` tokens with
@@ -21,7 +21,7 @@ from models.code_types import CodeTriple
21
21
  # Constants
22
22
  # ---------------------------------------------------------------------------
23
23
 
24
- SUPPORTED_EXTENSIONS = {".py", ".js", ".ts", ".tsx", ".html", ".css", ".scss"}
24
+ SUPPORTED_EXTENSIONS = {".py", ".js", ".ts", ".tsx", ".html", ".css", ".scss", ".php"}
25
25
 
26
26
  # Relation types
27
27
  DEFINED_IN = "DEFINED_IN"
@@ -101,6 +101,11 @@ def _load_html_parser() -> tuple[Parser, Language]:
101
101
  return _make_parser(tshtml.language)
102
102
 
103
103
 
104
+ def _load_php_parser() -> tuple[Parser, Language]:
105
+ import tree_sitter_php as tsphp
106
+ return _make_parser(tsphp.language)
107
+
108
+
104
109
  def _load_css_parser() -> tuple[Parser, Language]:
105
110
  import tree_sitter_css as tscss
106
111
  return _make_parser(tscss.language)
@@ -114,6 +119,7 @@ _PARSER_LOADERS: dict[str, Callable[[], tuple[Parser, Language]]] = {
114
119
  ".html": _load_html_parser,
115
120
  ".css": _load_css_parser,
116
121
  ".scss": _load_css_parser,
122
+ ".php": _load_php_parser,
117
123
  }
118
124
 
119
125
 
@@ -147,6 +153,15 @@ def _child_of_type(node: Node, type_name: str) -> Node | None:
147
153
  return None
148
154
 
149
155
 
156
+ def _get_node_name(node: Node, types: list[str]) -> str | None:
157
+ """Try multiple type names to find an identifier/name node."""
158
+ for t in types:
159
+ found = _child_of_type(node, t)
160
+ if found:
161
+ return _text(found)
162
+ return None
163
+
164
+
150
165
  def _children_of_type(node: Node, type_name: str) -> list[Node]:
151
166
  return [c for c in node.children if c.type == type_name]
152
167
 
@@ -174,23 +189,23 @@ def _extract_python(root: Node, source: bytes, file_path: str) -> list[CodeTripl
174
189
  for node in _walk(root):
175
190
  match node.type:
176
191
  case "function_definition":
177
- name_node = _child_of_type(node, "identifier")
178
- if name_node:
192
+ name = _get_node_name(node, ["identifier"])
193
+ if name:
179
194
  triples.append(CodeTriple(
180
- from_entity=_text(name_node),
195
+ from_entity=name,
181
196
  from_type=TYPE_FUNCTION,
182
197
  relation_type=DEFINED_IN,
183
198
  to_entity=module_name,
184
199
  to_type=TYPE_MODULE,
185
200
  source_file=file_path,
186
- line_number=_line(name_node),
201
+ line_number=_line(node),
187
202
  ))
188
203
 
189
204
  case "class_definition":
190
- name_node = _child_of_type(node, "identifier")
191
- if not name_node:
205
+ name = _get_node_name(node, ["identifier"])
206
+ if not name:
192
207
  continue
193
- class_name = _text(name_node)
208
+ class_name = name
194
209
 
195
210
  # Extract base classes from argument_list
196
211
  arg_list = _child_of_type(node, "argument_list")
@@ -281,6 +296,72 @@ def _extract_python_callee(call_node: Node) -> str | None:
281
296
  return None
282
297
 
283
298
 
299
+ # ---------------------------------------------------------------------------
300
+ # PHP extraction
301
+ # ---------------------------------------------------------------------------
302
+
303
+ def _extract_php(root: Node, source: bytes, file_path: str) -> list[CodeTriple]:
304
+ module_name = _module_name_from_path(file_path)
305
+ triples: list[CodeTriple] = []
306
+
307
+ for node in _walk(root):
308
+ match node.type:
309
+ case "function_definition":
310
+ name = _get_node_name(node, ["function_name", "identifier"])
311
+ if name:
312
+ triples.append(CodeTriple(
313
+ from_entity=name,
314
+ from_type=TYPE_FUNCTION,
315
+ relation_type=DEFINED_IN,
316
+ to_entity=module_name,
317
+ to_type=TYPE_MODULE,
318
+ source_file=file_path,
319
+ line_number=_line(node),
320
+ ))
321
+
322
+ case "class_definition":
323
+ name = _get_node_name(node, ["class_name", "identifier"])
324
+ if not name:
325
+ continue
326
+ class_name = name
327
+
328
+ # Extract base classes (extends)
329
+ extends_node = _child_of_type(node, "extends_clause")
330
+ if extends_node:
331
+ base_node = _child_of_type(extends_node, "type_name") or _child_of_type(extends_node, "identifier")
332
+ if base_node:
333
+ triples.append(CodeTriple(
334
+ from_entity=class_name,
335
+ from_type=TYPE_CLASS,
336
+ relation_type=INHERITS,
337
+ to_entity=_text(base_node),
338
+ to_type=TYPE_CLASS,
339
+ source_file=file_path,
340
+ line_number=_line(base_node),
341
+ ))
342
+
343
+ case "function_call":
344
+ callee = _extract_php_callee(node)
345
+ if callee and _is_meaningful_callee(callee):
346
+ scope = _enclosing_scope(node) # Reuse python-style scope walker for now
347
+ caller = scope or module_name
348
+ caller_type = TYPE_FUNCTION if scope else TYPE_MODULE
349
+ triples.append(CodeTriple(
350
+ from_entity=caller,
351
+ from_type=caller_type,
352
+ relation_type=CALLS,
353
+ to_entity=callee,
354
+ to_type=TYPE_FUNCTION,
355
+ source_file=file_path,
356
+ line_number=_line(node),
357
+ ))
358
+
359
+ return triples
360
+
361
+
362
+ def _extract_php_callee(call_node: Node) -> str | None:
363
+ return _get_node_name(call_node, ["function_name", "identifier"])
364
+
284
365
  # ---------------------------------------------------------------------------
285
366
  # JavaScript / TypeScript extraction
286
367
  # ---------------------------------------------------------------------------
@@ -647,6 +728,7 @@ _EXTRACTORS: dict[str, Callable] = {
647
728
  ".html": _extract_html,
648
729
  ".css": _extract_css,
649
730
  ".scss": _extract_css,
731
+ ".php": _extract_php,
650
732
  }
651
733
 
652
734
 
@@ -23,7 +23,7 @@ Return ONLY valid JSON, no other text:
23
23
  If no clear duplicates exist, return {"groups": []}"""
24
24
 
25
25
 
26
- def resolve_entities(client, dry_run: bool = False) -> int:
26
+ def resolve_entities(client) -> int:
27
27
  """Identify and merge duplicate entity nodes in the local graph."""
28
28
  all_nodes = [
29
29
  {"id": n, "name": attr.get("name", n), "type": attr.get("entity_type", "")}
@@ -79,20 +79,18 @@ def resolve_entities(client, dry_run: bool = False) -> int:
79
79
  dup_node = name_to_node.get(dup_name)
80
80
  if not dup_node or dup_node["id"] == canonical_node["id"]:
81
81
  continue
82
- tag = "[dry-run] " if dry_run else ""
83
- print(f" {tag}'{dup_name}' → '{canonical_name}'")
84
- if not dry_run:
85
- # Remap all edges from duplicate to canonical
86
- dup_id = dup_node["id"]
87
- canon_id = canonical_node["id"]
88
- for pred in list(client.graph.predecessors(dup_id)):
89
- for key, data in list(client.graph.get_edge_data(pred, dup_id).items()):
90
- client.graph.add_edge(pred, canon_id, **data)
91
- for succ in list(client.graph.successors(dup_id)):
92
- for key, data in list(client.graph.get_edge_data(dup_id, succ).items()):
93
- client.graph.add_edge(canon_id, succ, **data)
94
- client.graph.remove_node(dup_id)
95
- merged_count += 1
82
+ print(f" '{dup_name}' → '{canonical_name}'")
83
+ # Remap all edges from duplicate to canonical
84
+ dup_id = dup_node["id"]
85
+ canon_id = canonical_node["id"]
86
+ for pred in list(client.graph.predecessors(dup_id)):
87
+ for key, data in list(client.graph.get_edge_data(pred, dup_id).items()):
88
+ client.graph.add_edge(pred, canon_id, **data)
89
+ for succ in list(client.graph.successors(dup_id)):
90
+ for key, data in list(client.graph.get_edge_data(dup_id, succ).items()):
91
+ client.graph.add_edge(canon_id, succ, **data)
92
+ client.graph.remove_node(dup_id)
93
+ merged_count += 1
96
94
 
97
95
  if merged_count:
98
96
  client.save()
@@ -58,9 +58,10 @@ def _extract_triples_sync(chunk: str) -> list[Triple]:
58
58
  )
59
59
 
60
60
  try:
61
- raw = response.content[0].text
62
- raw = re.sub(r"^```[a-z]*\n?", "", raw.strip(), flags=re.MULTILINE)
63
- raw = re.sub(r"```$", "", raw.strip(), flags=re.MULTILINE)
61
+ raw = response.content[0].text.strip()
62
+ if raw.startswith("```"):
63
+ raw = re.sub(r"^```[a-z]*\n?", "", raw)
64
+ raw = re.sub(r"```$", "", raw)
64
65
  data = json.loads(raw.strip())
65
66
  except (json.JSONDecodeError, IndexError) as e:
66
67
  print(f"[reader_agent] JSON parse failed: {e} | raw: {raw[:200]!r}")
@@ -23,6 +23,10 @@ console = Console()
23
23
 
24
24
  _CODECOMPASS_START = "<!-- codecompass-code-graph-start -->"
25
25
  _CODECOMPASS_END = "<!-- codecompass-code-graph-end -->"
26
+ _CODECOMPASS_READ_INSTRUCTION = (
27
+ "Read `.codecompass` before making any changes or before reading any file. "
28
+ "If you think codecompass will help in any way, use it."
29
+ )
26
30
 
27
31
 
28
32
  def init_project(repo_path: str) -> None:
@@ -69,6 +73,20 @@ def init_project(repo_path: str) -> None:
69
73
  with open(file_path, "w") as f:
70
74
  f.write(content)
71
75
 
76
+ # ponytail: minimal way to create claude.md in repo root
77
+ claude_md_path = os.path.join(repo_path, "claude.md")
78
+ if not os.path.exists(claude_md_path):
79
+ with open(claude_md_path, "w") as f:
80
+ f.write(f"AGENTS.md\n\n{_CODECOMPASS_READ_INSTRUCTION}\n")
81
+ else:
82
+ with open(claude_md_path) as f:
83
+ claude_md_content = f.read()
84
+ if _CODECOMPASS_READ_INSTRUCTION not in claude_md_content:
85
+ with open(claude_md_path, "a") as f:
86
+ if claude_md_content and not claude_md_content.endswith("\n"):
87
+ f.write("\n")
88
+ f.write(f"\n{_CODECOMPASS_READ_INSTRUCTION}\n")
89
+
72
90
  _ensure_gitignore(repo_path)
73
91
  console.print(f"[bold green]Initialized CodeCompass in:[/] {compass_dir}")
74
92
  _register_project_agents_md(repo_path)
@@ -173,6 +191,8 @@ def _register_project_agents_md(repo_path: str) -> None:
173
191
  block = f"""{_CODECOMPASS_START}
174
192
  ## Code graph
175
193
 
194
+ **{_CODECOMPASS_READ_INSTRUCTION}**
195
+
176
196
  This project has a CodeCompass code knowledge graph at `.codecompass/graph.json`.
177
197
  Every node carries `kind` (e.g. `function:python`, `class:typescript`) and a
178
198
  human-readable `description`. Use it as your primary navigation tool.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codecompass-mcp"
3
- version = "2.2.0"
3
+ version = "2.3.0"
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"}
File without changes