codegraphcontext 0.4.4__py3-none-any.whl → 0.4.5__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.
@@ -721,19 +721,31 @@ def watch_helper(path: str, context: Optional[str] = None):
721
721
  # Add the directory to watch
722
722
  if is_indexed:
723
723
  console.print("[green]✓[/green] Already indexed (no initial scan needed)")
724
- watcher.watch_directory(str(path_obj), perform_initial_scan=False)
724
+ watcher.watch_directory(
725
+ str(path_obj),
726
+ perform_initial_scan=False,
727
+ cgcignore_path=ctx.cgcignore_path,
728
+ )
725
729
  else:
726
730
  console.print("[yellow]⚠[/yellow] Not indexed yet. Performing initial scan...")
727
731
 
728
732
  # Index the repository first (like MCP does)
729
733
  async def do_index():
730
- await graph_builder.build_graph_from_path_async(path_obj, is_dependency=False)
734
+ await graph_builder.build_graph_from_path_async(
735
+ path_obj,
736
+ is_dependency=False,
737
+ cgcignore_path=ctx.cgcignore_path,
738
+ )
731
739
 
732
740
  asyncio.run(do_index())
733
741
  console.print("[green]✓[/green] Initial scan complete")
734
742
 
735
743
  # Now start watching (without another scan)
736
- watcher.watch_directory(str(path_obj), perform_initial_scan=False)
744
+ watcher.watch_directory(
745
+ str(path_obj),
746
+ perform_initial_scan=False,
747
+ cgcignore_path=ctx.cgcignore_path,
748
+ )
737
749
 
738
750
  console.print("[bold green]👀 Monitoring for file changes...[/bold green] (Press Ctrl+C to stop)")
739
751
  console.print("[dim]💡 Tip: Open a new terminal window to continue working[/dim]\n")
@@ -854,11 +854,11 @@ def doctor():
854
854
  elif default_db == "kuzudb":
855
855
  from importlib.util import find_spec
856
856
 
857
- if find_spec("kuzu") is not None:
857
+ if find_spec("real_ladybug") is not None:
858
858
  console.print(f" [green]✓[/green] KuzuDB is installed")
859
859
  else:
860
860
  console.print(f" [red]✗[/red] KuzuDB is not installed")
861
- console.print(f" Run: pip install kuzu")
861
+ console.print(f" Run: pip install real_ladybug")
862
862
  all_checks_passed = False
863
863
  else:
864
864
  # FalkorDB
@@ -22,7 +22,7 @@ import importlib.util
22
22
  def _is_kuzudb_available() -> bool:
23
23
  """Check if KùzuDB is installed."""
24
24
  try:
25
- return importlib.util.find_spec("kuzu") is not None
25
+ return importlib.util.find_spec("real_ladybug") is not None
26
26
  except ImportError:
27
27
  return False
28
28
 
@@ -70,7 +70,7 @@ def get_database_manager(db_path: Optional[str] = None) -> Union['DatabaseManage
70
70
  db_type = db_type.lower()
71
71
  if db_type == 'kuzudb':
72
72
  if not _is_kuzudb_available():
73
- raise ValueError("Database set to 'kuzudb' but Kùzu is not installed.\nRun 'pip install kuzu'")
73
+ raise ValueError("Database set to 'kuzudb' but Kùzu is not installed.\nRun 'pip install real_ladybug'")
74
74
  from .database_kuzu import KuzuDBManager
75
75
  info_logger(f"Using KùzuDB (explicit) at {db_path or 'default path'}")
76
76
  return KuzuDBManager(db_path=db_path)
@@ -147,7 +147,7 @@ def get_database_manager(db_path: Optional[str] = None) -> Union['DatabaseManage
147
147
  return DatabaseManager()
148
148
 
149
149
  error_msg = "No database backend available.\n"
150
- error_msg += "Recommended: Install KùzuDB for zero-config ('pip install kuzu')\n"
150
+ error_msg += "Recommended: Install KùzuDB for zero-config ('pip install real_ladybug')\n"
151
151
 
152
152
  if platform.system() != "Windows":
153
153
  error_msg += "Alternative: Install FalkorDB Lite ('pip install falkordblite')\n"
@@ -66,7 +66,7 @@ class KuzuDBManager:
66
66
  if self._conn is None:
67
67
  with self._lock:
68
68
  if self._conn is None:
69
- import kuzu
69
+ import real_ladybug as kuzu
70
70
  max_retries = 5
71
71
  for attempt in range(max_retries):
72
72
  try:
@@ -77,7 +77,7 @@ class KuzuDBManager:
77
77
  info_logger("KùzuDB connection established and schema verified")
78
78
  break
79
79
  except ImportError:
80
- error_logger("KùzuDB is not installed. Run 'pip install kuzu'")
80
+ error_logger("KùzuDB is not installed. Run 'pip install real_ladybug'")
81
81
  raise ValueError("KùzuDB missing.")
82
82
  except Exception as e:
83
83
  if "lock" in str(e).lower() and attempt < max_retries - 1:
@@ -156,6 +156,26 @@ class KuzuDBManager:
156
156
  warning_logger(f"Kuzu Schema Rel Error ({table_name}): {e}")
157
157
  debug_log(f"Kuzu Schema Rel Error ({table_name}): {e}")
158
158
 
159
+ self._run_schema_migrations()
160
+
161
+ def _run_schema_migrations(self):
162
+ """Add columns introduced after older local Kùzu databases were created."""
163
+ migrations = [
164
+ ("Module", "full_import_name", "STRING"),
165
+ ("IMPORTS", "full_import_name", "STRING"),
166
+ ("IMPORTS", "imported_name", "STRING"),
167
+ ]
168
+
169
+ for table_name, column_name, column_type in migrations:
170
+ try:
171
+ self._conn.execute(f"ALTER TABLE `{table_name}` ADD {column_name} {column_type}")
172
+ except Exception as e:
173
+ err = str(e).lower()
174
+ if "already exists" in err or "duplicate" in err:
175
+ continue
176
+ warning_logger(f"Kuzu Schema Migration Error ({table_name}.{column_name}): {e}")
177
+ debug_log(f"Kuzu Schema Migration Error ({table_name}.{column_name}): {e}")
178
+
159
179
  def close_driver(self):
160
180
  """Closes the connection."""
161
181
  if self._conn is not None:
@@ -188,10 +208,10 @@ class KuzuDBManager:
188
208
  @staticmethod
189
209
  def test_connection(db_path: str = None) -> Tuple[bool, Optional[str]]:
190
210
  try:
191
- import kuzu
211
+ import real_ladybug as kuzu
192
212
  return True, None
193
213
  except ImportError:
194
- return False, "KùzuDB is not installed. Run 'pip install kuzu'"
214
+ return False, "KùzuDB is not installed. Run 'pip install real_ladybug'"
195
215
 
196
216
  class KuzuDriverWrapper:
197
217
  def __init__(self, conn):
@@ -10,9 +10,13 @@ from watchdog.observers import Observer
10
10
  from watchdog.events import FileSystemEventHandler
11
11
 
12
12
  if typing.TYPE_CHECKING:
13
+ from pathspec import PathSpec
13
14
  from codegraphcontext.tools.graph_builder import GraphBuilder
14
15
  from codegraphcontext.core.jobs import JobManager
15
16
 
17
+ from codegraphcontext.core.cgcignore import build_ignore_spec
18
+ from codegraphcontext.tools.indexing.constants import DEFAULT_IGNORE_PATTERNS
19
+ from codegraphcontext.cli.config_manager import get_config_value
16
20
  from codegraphcontext.utils.debug_log import debug_log, info_logger, error_logger, warning_logger
17
21
 
18
22
  class RepositoryEventHandler(FileSystemEventHandler):
@@ -23,7 +27,15 @@ class RepositoryEventHandler(FileSystemEventHandler):
23
27
  to build a baseline and then uses this cached state to perform efficient
24
28
  updates when files are changed, created, or deleted.
25
29
  """
26
- def __init__(self, graph_builder: "GraphBuilder", repo_path: Path, debounce_interval=2.0, perform_initial_scan: bool = True):
30
+ def __init__(
31
+ self,
32
+ graph_builder: "GraphBuilder",
33
+ repo_path: Path,
34
+ debounce_interval=2.0,
35
+ perform_initial_scan: bool = True,
36
+ cgcignore_path: str = None,
37
+ ignore_spec: "PathSpec" = None,
38
+ ):
27
39
  """
28
40
  Initializes the event handler.
29
41
 
@@ -32,12 +44,17 @@ class RepositoryEventHandler(FileSystemEventHandler):
32
44
  repo_path: The absolute path to the repository directory to watch.
33
45
  debounce_interval: The time in seconds to wait for more changes before processing an event.
34
46
  perform_initial_scan: Whether to perform an initial scan of the repository.
47
+ cgcignore_path: Optional explicit .cgcignore path from the active context.
48
+ ignore_spec: Optional precompiled ignore spec, useful for tests.
35
49
  """
36
50
  super().__init__()
37
51
  self.graph_builder = graph_builder
38
- self.repo_path = repo_path
52
+ self.repo_path = repo_path.resolve()
39
53
  self.debounce_interval = debounce_interval
40
54
  self.timers = {} # A dictionary to manage debounce timers for file paths.
55
+ self.ignore_root = self.repo_path
56
+ self.ignore_spec = ignore_spec
57
+ self._load_ignore_spec(cgcignore_path)
41
58
 
42
59
  # Caches for the repository's state.
43
60
  self.all_file_data = []
@@ -47,11 +64,65 @@ class RepositoryEventHandler(FileSystemEventHandler):
47
64
  if perform_initial_scan:
48
65
  self._initial_scan()
49
66
 
67
+ def _load_ignore_spec(self, cgcignore_path: str = None) -> None:
68
+ """Load .cgcignore rules using the same defaults as repository indexing."""
69
+ if self.ignore_spec is not None:
70
+ return
71
+ try:
72
+ self.ignore_spec, resolved_cgcignore = build_ignore_spec(
73
+ ignore_root=self.ignore_root,
74
+ default_patterns=DEFAULT_IGNORE_PATTERNS,
75
+ explicit_path=cgcignore_path,
76
+ )
77
+ if resolved_cgcignore:
78
+ debug_log(
79
+ f"Watcher using .cgcignore at {resolved_cgcignore} "
80
+ f"(filtering relative to {self.ignore_root})"
81
+ )
82
+ except OSError as e:
83
+ self.ignore_spec = None
84
+ warning_logger(f"Could not load/create watcher .cgcignore: {e}")
85
+
86
+ def _should_ignore(self, path: str | Path) -> bool:
87
+ """Return True when a path is excluded by .cgcignore or IGNORE_DIRS."""
88
+ path_obj = Path(path).resolve()
89
+ ignore_root = getattr(self, "ignore_root", getattr(self, "repo_path", None))
90
+
91
+ ignore_dirs_str = get_config_value("IGNORE_DIRS") or ""
92
+ if ignore_dirs_str and ignore_root:
93
+ ignore_dirs = {d.strip().lower() for d in ignore_dirs_str.split(",") if d.strip()}
94
+ try:
95
+ parts = {p.lower() for p in path_obj.relative_to(ignore_root).parent.parts}
96
+ if parts.intersection(ignore_dirs):
97
+ return True
98
+ except ValueError:
99
+ pass
100
+
101
+ ignore_spec = getattr(self, "ignore_spec", None)
102
+ if not ignore_spec or not ignore_root:
103
+ return False
104
+
105
+ try:
106
+ rel_path = path_obj.relative_to(ignore_root).as_posix()
107
+ except ValueError:
108
+ return False
109
+ return ignore_spec.match_file(rel_path)
110
+
111
+ def _is_supported_code_file(self, path: str | Path) -> bool:
112
+ path_obj = Path(path)
113
+ return path_obj.is_file() and path_obj.suffix in self.graph_builder.parsers and not self._should_ignore(path_obj)
114
+
115
+ def _iter_supported_files(self) -> list[Path]:
116
+ supported_extensions = self.graph_builder.parsers.keys()
117
+ return [
118
+ f for f in self.repo_path.rglob("*")
119
+ if f.is_file() and f.suffix in supported_extensions and not self._should_ignore(f)
120
+ ]
121
+
50
122
  def _initial_scan(self):
51
123
  """Scans the entire repository, parses all files, and builds the initial graph."""
52
124
  info_logger(f"Performing initial scan for watcher: {self.repo_path}")
53
- supported_extensions = self.graph_builder.parsers.keys()
54
- all_files = [f for f in self.repo_path.rglob("*") if f.is_file() and f.suffix in supported_extensions]
125
+ all_files = self._iter_supported_files()
55
126
 
56
127
  # 1. Pre-scan all files to get a global map of where every symbol is defined.
57
128
  self.imports_map = self.graph_builder.pre_scan_imports(all_files)
@@ -126,6 +197,10 @@ class RepositoryEventHandler(FileSystemEventHandler):
126
197
  """
127
198
  info_logger(f"File change detected (incremental update): {event_path_str}")
128
199
  changed_path = Path(event_path_str)
200
+ if self._should_ignore(changed_path):
201
+ debug_log(f"Ignored watcher update based on .cgcignore: {changed_path}")
202
+ return
203
+
129
204
  changed_path_str = str(changed_path.resolve())
130
205
  supported_extensions = self.graph_builder.parsers.keys()
131
206
 
@@ -160,7 +235,7 @@ class RepositoryEventHandler(FileSystemEventHandler):
160
235
  subset_file_data = []
161
236
  for path_str in affected_paths:
162
237
  p = Path(path_str)
163
- if p.exists() and p.suffix in supported_extensions:
238
+ if p.exists() and p.suffix in supported_extensions and not self._should_ignore(p):
164
239
  parsed = self.graph_builder.parse_file(self.repo_path, p)
165
240
  if "error" not in parsed:
166
241
  subset_file_data.append(parsed)
@@ -177,22 +252,22 @@ class RepositoryEventHandler(FileSystemEventHandler):
177
252
 
178
253
  # The following methods are called by the watchdog observer when a file event occurs.
179
254
  def on_created(self, event):
180
- if not event.is_directory and Path(event.src_path).suffix in self.graph_builder.parsers:
255
+ if not event.is_directory and self._is_supported_code_file(event.src_path):
181
256
  self._debounce(event.src_path, lambda: self._handle_modification(event.src_path))
182
257
 
183
258
  def on_modified(self, event):
184
- if not event.is_directory and Path(event.src_path).suffix in self.graph_builder.parsers:
259
+ if not event.is_directory and self._is_supported_code_file(event.src_path):
185
260
  self._debounce(event.src_path, lambda: self._handle_modification(event.src_path))
186
261
 
187
262
  def on_deleted(self, event):
188
- if not event.is_directory and Path(event.src_path).suffix in self.graph_builder.parsers:
263
+ if not event.is_directory and Path(event.src_path).suffix in self.graph_builder.parsers and not self._should_ignore(event.src_path):
189
264
  self._debounce(event.src_path, lambda: self._handle_modification(event.src_path))
190
265
 
191
266
  def on_moved(self, event):
192
267
  if not event.is_directory:
193
- if Path(event.src_path).suffix in self.graph_builder.parsers:
268
+ if Path(event.src_path).suffix in self.graph_builder.parsers and not self._should_ignore(event.src_path):
194
269
  self._debounce(event.src_path, lambda: self._handle_modification(event.src_path))
195
- if Path(event.dest_path).suffix in self.graph_builder.parsers:
270
+ if Path(event.dest_path).suffix in self.graph_builder.parsers and not self._should_ignore(event.dest_path):
196
271
  self._debounce(event.dest_path, lambda: self._handle_modification(event.dest_path))
197
272
 
198
273
 
@@ -207,7 +282,7 @@ class CodeWatcher:
207
282
  self.watched_paths = set() # Keep track of paths already being watched.
208
283
  self.watches = {} # Store watch objects to allow unscheduling
209
284
 
210
- def watch_directory(self, path: str, perform_initial_scan: bool = True):
285
+ def watch_directory(self, path: str, perform_initial_scan: bool = True, cgcignore_path: str = None):
211
286
  """Schedules a directory to be watched for changes."""
212
287
  path_obj = Path(path).resolve()
213
288
  path_str = str(path_obj)
@@ -217,7 +292,12 @@ class CodeWatcher:
217
292
  return {"message": f"Path already being watched: {path_str}"}
218
293
 
219
294
  # Create a new, dedicated event handler for this specific repository path.
220
- event_handler = RepositoryEventHandler(self.graph_builder, path_obj, perform_initial_scan=perform_initial_scan)
295
+ event_handler = RepositoryEventHandler(
296
+ self.graph_builder,
297
+ path_obj,
298
+ perform_initial_scan=perform_initial_scan,
299
+ cgcignore_path=cgcignore_path,
300
+ )
221
301
 
222
302
  watch = self.observer.schedule(event_handler, path_str, recursive=True)
223
303
  self.watches[path_str] = watch
@@ -864,6 +864,42 @@ class CodeFinder:
864
864
  """Find all dependencies and dependents of a module"""
865
865
  with self.driver.session() as session:
866
866
  repo_filter = "AND file.path STARTS WITH $repo_path" if repo_path else ""
867
+ backend = getattr(self.db_manager, "get_backend_type", lambda: "")()
868
+
869
+ # KuzuDB is stricter about OPTIONAL MATCH variable scoping, and nested
870
+ # repository ownership is already represented in the file path.
871
+ if backend == "kuzudb":
872
+ importers_result = session.run(f"""
873
+ MATCH (file:File)-[imp:IMPORTS]->(module:Module)
874
+ WHERE (module.name = $module_name OR module.full_import_name CONTAINS $module_name) {repo_filter}
875
+ RETURN DISTINCT
876
+ file.path as importer_file_path,
877
+ imp.line_number as import_line_number,
878
+ file.is_dependency as file_is_dependency,
879
+ '' as repository_name
880
+ ORDER BY file_is_dependency ASC, importer_file_path
881
+ LIMIT 50
882
+ """, module_name=module_name, repo_path=repo_path)
883
+
884
+ imports_result = session.run(f"""
885
+ MATCH (file:File)-[:IMPORTS]->(target_module:Module)
886
+ WHERE (target_module.name = $module_name OR target_module.full_import_name CONTAINS $module_name) {repo_filter}
887
+ WITH file, target_module
888
+ MATCH (file)-[imp:IMPORTS]->(other_module:Module)
889
+ WHERE other_module.name <> target_module.name
890
+ RETURN DISTINCT
891
+ other_module.name as imported_module,
892
+ imp.alias as import_alias
893
+ ORDER BY imported_module
894
+ LIMIT 50
895
+ """, module_name=module_name, repo_path=repo_path)
896
+
897
+ return {
898
+ "module_name": module_name,
899
+ "importers": importers_result.data(),
900
+ "imports": imports_result.data()
901
+ }
902
+
867
903
  # Find files that import this module (who imports this module)
868
904
  importers_result = session.run(f"""
869
905
  MATCH (file:File)-[imp:IMPORTS]->(module:Module {{name: $module_name}})
@@ -278,19 +278,28 @@ class GraphWriter:
278
278
  js_imports = []
279
279
  other_imports = []
280
280
  for imp in file_data.get("imports", []):
281
- if lang == "javascript":
281
+ if lang in {"javascript", "typescript", "tsx"}:
282
282
  module_name = imp.get("source")
283
283
  if module_name:
284
284
  js_imports.append(
285
285
  {
286
286
  "module_name": module_name,
287
287
  "imported_name": imp.get("name", "*"),
288
- "alias": imp.get("alias"),
289
- "line_number": imp.get("line_number"),
288
+ "alias": imp.get("alias") or "",
289
+ "line_number": imp.get("line_number") or 0,
290
290
  }
291
291
  )
292
292
  else:
293
- other_imports.append(imp)
293
+ module_name = imp.get("name") or imp.get("source") or imp.get("full_import_name")
294
+ if module_name:
295
+ other_imports.append(
296
+ {
297
+ "name": module_name,
298
+ "alias": imp.get("alias") or "",
299
+ "full_import_name": imp.get("full_import_name") or module_name,
300
+ "line_number": imp.get("line_number") or 0,
301
+ }
302
+ )
294
303
 
295
304
  if js_imports:
296
305
  session.run(
@@ -20,6 +20,7 @@ def resolve_function_call(
20
20
  if called_name in __builtins__:
21
21
  return None
22
22
 
23
+ resolved_called_name = called_name
23
24
  resolved_path = None
24
25
  full_call = call.get("full_name", called_name)
25
26
  base_obj = full_call.split(".")[0] if "." in full_call else None
@@ -43,6 +44,14 @@ def resolve_function_call(
43
44
 
44
45
  if not resolved_path:
45
46
  possible_paths = imports_map.get(lookup_name, [])
47
+ if not possible_paths and lookup_name in local_imports:
48
+ imported_name = local_imports[lookup_name]
49
+ alias_paths = imports_map.get(imported_name, [])
50
+ if alias_paths:
51
+ possible_paths = alias_paths
52
+ lookup_name = imported_name
53
+ if called_name == base_obj or called_name == call["name"]:
54
+ resolved_called_name = imported_name
46
55
  if len(possible_paths) == 1:
47
56
  resolved_path = possible_paths[0]
48
57
  elif len(possible_paths) > 1:
@@ -74,8 +83,8 @@ def resolve_function_call(
74
83
  if called_name in local_names:
75
84
  resolved_path = caller_file_path
76
85
  is_unresolved_external = False
77
- elif called_name in imports_map and imports_map[called_name]:
78
- candidates = imports_map[called_name]
86
+ elif resolved_called_name in imports_map and imports_map[resolved_called_name]:
87
+ candidates = imports_map[resolved_called_name]
79
88
  for path in candidates:
80
89
  for imp_name in local_imports.values():
81
90
  if imp_name.replace(".", "/") in path:
@@ -100,7 +109,7 @@ def resolve_function_call(
100
109
  "caller_name": caller_name,
101
110
  "caller_file_path": caller_file_path,
102
111
  "caller_line_number": caller_line_number,
103
- "called_name": called_name,
112
+ "called_name": resolved_called_name,
104
113
  "called_file_path": resolved_path,
105
114
  "line_number": call["line_number"],
106
115
  "args": call.get("args", []),
@@ -109,7 +118,7 @@ def resolve_function_call(
109
118
  return {
110
119
  "type": "file",
111
120
  "caller_file_path": caller_file_path,
112
- "called_name": called_name,
121
+ "called_name": resolved_called_name,
113
122
  "called_file_path": resolved_path,
114
123
  "line_number": call["line_number"],
115
124
  "args": call.get("args", []),
@@ -51,6 +51,23 @@ DART_QUERIES = {
51
51
  """,
52
52
  }
53
53
 
54
+ SIGNATURE_TYPES = (
55
+ "function_signature",
56
+ "method_signature",
57
+ "getter_signature",
58
+ "setter_signature",
59
+ "constructor_signature",
60
+ "factory_constructor_signature",
61
+ "operator_signature",
62
+ )
63
+ CONTAINER_TYPES = (
64
+ "class_definition",
65
+ "mixin_declaration",
66
+ "extension_declaration",
67
+ )
68
+ _CALL_PUNCTUATION = {".", "?.", "..", "?..", ";", ",", "(", ")", "[", "]"}
69
+
70
+
54
71
  class DartTreeSitterParser:
55
72
  """A Dart-specific parser using tree-sitter, encapsulating language-specific logic."""
56
73
 
@@ -65,15 +82,101 @@ class DartTreeSitterParser:
65
82
  if not node: return ""
66
83
  return node.text.decode('utf-8')
67
84
 
68
- def _get_parent_context(self, node, types=('function_signature', 'class_definition', 'mixin_declaration', 'extension_declaration')):
85
+ def _get_parent_context(self, node, types=SIGNATURE_TYPES + CONTAINER_TYPES):
69
86
  curr = node.parent
70
87
  while curr:
71
88
  if curr.type in types:
72
89
  name_node = curr.child_by_field_name('name')
90
+ if name_node is None:
91
+ for child in curr.children:
92
+ if child.type == "identifier":
93
+ name_node = child
94
+ break
73
95
  return self._get_node_text(name_node) if name_node else None, curr.type, curr.start_point[0] + 1
96
+
97
+ # Dart places function_body next to its signature under class_body/program,
98
+ # so calls inside a body cannot find the signature through parent links.
99
+ if curr.type == "function_body":
100
+ parent = curr.parent
101
+ if parent is not None:
102
+ siblings = list(parent.children)
103
+ try:
104
+ idx = siblings.index(curr)
105
+ except ValueError:
106
+ idx = -1
107
+
108
+ for i in range(idx - 1, -1, -1):
109
+ sibling = siblings[i]
110
+ if sibling.type not in types:
111
+ continue
112
+
113
+ target = sibling
114
+ for child in sibling.children:
115
+ if child.type in SIGNATURE_TYPES:
116
+ target = child
117
+ break
118
+
119
+ name_node = target.child_by_field_name("name")
120
+ if name_node is None:
121
+ for child in target.children:
122
+ if child.type == "identifier":
123
+ name_node = child
124
+ break
125
+ if name_node is not None:
126
+ return self._get_node_text(name_node), target.type, target.start_point[0] + 1
74
127
  curr = curr.parent
75
128
  return None, None, None
76
129
 
130
+ def _last_identifier_in(self, node):
131
+ """Return the deepest-last identifier under node."""
132
+ last = None
133
+ for child in node.children:
134
+ if child.type == "identifier":
135
+ last = child
136
+ else:
137
+ deeper = self._last_identifier_in(child)
138
+ if deeper is not None:
139
+ last = deeper
140
+ return last
141
+
142
+ def _name_node_for_call_selector(self, call_node):
143
+ """Find the receiver/member name paired with a captured argument selector."""
144
+ parent = call_node.parent
145
+ if parent is None:
146
+ return None
147
+
148
+ siblings = list(parent.children)
149
+ try:
150
+ idx = siblings.index(call_node)
151
+ except ValueError:
152
+ return None
153
+
154
+ for i in range(idx - 1, -1, -1):
155
+ sibling = siblings[i]
156
+ if sibling.type in _CALL_PUNCTUATION:
157
+ continue
158
+ if sibling.type == "identifier":
159
+ return sibling
160
+ name_node = self._last_identifier_in(sibling)
161
+ if name_node is not None:
162
+ return name_node
163
+ break
164
+ return None
165
+
166
+ def _has_call_selector_sibling(self, name_node):
167
+ """Return True if an identifier is a receiver beside a call selector."""
168
+ parent = name_node.parent
169
+ if parent is None:
170
+ return False
171
+ for sibling in parent.children:
172
+ if sibling is name_node:
173
+ continue
174
+ if sibling.type == "selector":
175
+ for child in sibling.children:
176
+ if child.type == "argument_part":
177
+ return True
178
+ return False
179
+
77
180
  def _calculate_complexity(self, node):
78
181
  complexity_nodes = {
79
182
  "if_statement", "for_statement", "while_statement", "do_statement",
@@ -293,23 +396,14 @@ class DartTreeSitterParser:
293
396
 
294
397
  for node, capture_name in execute_query(self.language, query_str, root_node):
295
398
  if capture_name in ("name", "call"):
296
- # Ensure we are at the right node level
297
- target_node = node
298
399
  if capture_name == "call":
299
- name_node = None
300
- for child in node.children:
301
- if child.type == 'identifier':
302
- name_node = child
303
- break
304
- if child.type == 'selector':
305
- for sub in child.children:
306
- if sub.type == 'identifier':
307
- name_node = sub
308
- break
309
- if name_node:
310
- target_node = name_node
311
- else:
400
+ target_node = self._name_node_for_call_selector(node)
401
+ if target_node is None:
402
+ continue
403
+ else:
404
+ if self._has_call_selector_sibling(node):
312
405
  continue
406
+ target_node = node
313
407
 
314
408
  # Deduplicate by start byte
315
409
  node_id = target_node.start_byte
@@ -145,6 +145,7 @@ class PythonTreeSitterParser:
145
145
  classes = self._find_classes(root_node)
146
146
  imports = self._find_imports(root_node)
147
147
  function_calls = self._find_calls(root_node)
148
+ self._attach_module_context(functions, function_calls, root_node, index_source)
148
149
  variables = self._find_variables(root_node)
149
150
 
150
151
  return {
@@ -165,6 +166,41 @@ class PythonTreeSitterParser:
165
166
  os.remove(temp_py_file)
166
167
  info_logger(f"Removed temporary file: {temp_py_file}")
167
168
 
169
+ def _attach_module_context(self, functions, function_calls, root_node, index_source: bool = False):
170
+ """Represent module-level executable code as Python's <module> frame."""
171
+ module_level_calls = [
172
+ call for call in function_calls
173
+ if not call.get("context") or call["context"][0] is None
174
+ ]
175
+ if not module_level_calls:
176
+ return
177
+
178
+ has_module_frame = any(
179
+ func.get("name") == "<module>" and func.get("line_number") == 1
180
+ for func in functions
181
+ )
182
+ if not has_module_frame:
183
+ module_func = {
184
+ "name": "<module>",
185
+ "line_number": 1,
186
+ "end_line": root_node.end_point[0] + 1,
187
+ "args": [],
188
+ "cyclomatic_complexity": 1,
189
+ "context": None,
190
+ "context_type": "module",
191
+ "class_context": None,
192
+ "decorators": [],
193
+ "lang": self.language_name,
194
+ "is_dependency": False,
195
+ }
196
+ if index_source:
197
+ module_func["source"] = self._get_node_text(root_node)
198
+ module_func["docstring"] = self._get_docstring(root_node)
199
+ functions.append(module_func)
200
+
201
+ for call in module_level_calls:
202
+ call["context"] = ("<module>", "module", 1)
203
+
168
204
  def _find_lambda_assignments(self, root_node, index_source: bool = False):
169
205
  functions = []
170
206
  query_str = PY_QUERIES.get('lambda_assignments')
@@ -573,4 +609,4 @@ def pre_scan_python(files: list[Path], parser_wrapper) -> dict:
573
609
  finally:
574
610
  if temp_py_file and temp_py_file.exists():
575
611
  os.remove(temp_py_file)
576
- return imports_map
612
+ return imports_map
@@ -226,9 +226,11 @@ class ScipIndexParser:
226
226
  """
227
227
  try:
228
228
  from . import scip_pb2 # type: ignore
229
- except ImportError:
229
+ except Exception as e:
230
230
  error_logger(
231
- "scip_pb2.py not found in tools directory."
231
+ "Failed to import codegraphcontext.tools.scip_pb2. "
232
+ "Ensure protobuf>=3.20,<3.21 is installed in the CodeGraphContext environment. "
233
+ f"Original error: {e}"
232
234
  )
233
235
  return {}
234
236
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codegraphcontext
3
- Version: 0.4.4
3
+ Version: 0.4.5
4
4
  Summary: An MCP server that indexes local code into a graph database to provide context to AI assistants.
5
5
  Author-email: Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
6
6
  License: MIT License
@@ -52,8 +52,9 @@ Requires-Dist: nbconvert>=7.16.6
52
52
  Requires-Dist: pathspec>=0.12.1
53
53
  Requires-Dist: falkordb>=0.1.0
54
54
  Requires-Dist: requests>=2.28.0
55
+ Requires-Dist: protobuf<3.21,>=3.20
55
56
  Requires-Dist: falkordblite>=0.1.0; sys_platform != "win32" and python_version >= "3.12"
56
- Requires-Dist: kuzu>=0.4.0; sys_platform == "win32" or (sys_platform != "win32" and python_version >= "3.10" and python_version < "3.12")
57
+ Requires-Dist: real_ladybug; sys_platform == "win32" or (sys_platform != "win32" and python_version >= "3.10")
57
58
  Requires-Dist: fastapi>=0.100.0
58
59
  Requires-Dist: uvicorn>=0.22.0
59
60
  Provides-Extra: parsing
@@ -165,7 +166,7 @@ A powerful **MCP server** and **CLI toolkit** that indexes local code into a gra
165
166
  ---
166
167
 
167
168
  ## Project Details
168
- - **Version:** 0.4.4
169
+ - **Version:** 0.4.5
169
170
  - **Authors:** Shashank Shekhar Singh <shashankshekharsingh1205@gmail.com>
170
171
  - **License:** MIT License (See [LICENSE](LICENSE) for details)
171
172
  - **Website:** [CodeGraphContext](http://codegraphcontext.vercel.app/)
@@ -228,7 +229,7 @@ CodeGraphContext supports multiple graph database backends to suit your environm
228
229
  | **Setup** | Zero-config / Embedded | Zero-config / In-process | Docker / External |
229
230
  | **Platform** | **All (Windows Native, macOS, Linux)** | Unix-only (Linux/macOS/WSL) | All Platforms |
230
231
  | **Use Case** | Desktop, IDE, Local development | Specialized Unix development | Enterprise, Massive graphs |
231
- | **Requirement**| `pip install kuzu` | `pip install falkordblite` | Neo4j Server / Docker |
232
+ | **Requirement**| `pip install real_ladybug` | `pip install falkordblite` | Neo4j Server / Docker |
232
233
  | **Speed** | ⚡ Extremely Fast | ⚡ Fast | 🚀 Scalable |
233
234
  | **Persistence**| Yes (to disk) | Yes (to disk) | Yes (to disk) |
234
235
 
@@ -339,7 +340,7 @@ Use CodeGraphContext as an **MCP server** for AI assistants:
339
340
 
340
341
  2. **Database Setup (Automatic)**
341
342
 
342
- - **KùzuDB (default on Windows):** Runs natively on Windows, macOS, and Linux. On Windows it is the usual embedded choice; `pip install kuzu` if needed.
343
+ - **KùzuDB (default on Windows):** Runs natively on Windows, macOS, and Linux. On Windows it is the usual embedded choice; `pip install real_ladybug` if needed.
343
344
  - **FalkorDB Lite (typical default on Unix):** When Python 3.12+ and `falkordblite` are available on Unix/macOS/WSL, the embedded backend prefers FalkorDB Lite; otherwise KùzuDB is used.
344
345
  - **Neo4j (Alternative):** To use Neo4j instead, or if you prefer a server-based approach, run: `cgc neo4j setup`
345
346
 
@@ -4,30 +4,30 @@ codegraphcontext/prompts.py,sha256=E5P55paM0oHfBcNVfxkxpXRGZnya1kr_mKDg9i49FwM,6
4
4
  codegraphcontext/server.py,sha256=AMe3BVvon0cNE1uyPB-E2a0XedBTWqSiIZv19KEPZh4,19120
5
5
  codegraphcontext/tool_definitions.py,sha256=kDRI9WrfkV9TienmYvetGlimNdb1AdiOWuuYid_ZtAk,13177
6
6
  codegraphcontext/cli/__init__.py,sha256=v6CMDVKM5d_sXn3S5nZNf0phXn0IdrnhLazUoen9k9w,38
7
- codegraphcontext/cli/cli_helpers.py,sha256=toXzoiiig7UmOZ7-Yr7euKTb-71FEyxYt4qhCj3PjqQ,32683
7
+ codegraphcontext/cli/cli_helpers.py,sha256=-_7N4rCZuunyDiUsD1G1ik9MS40JWLnP5zItmYxYtuk,32993
8
8
  codegraphcontext/cli/config_manager.py,sha256=s13JltwsmHF0jWK65XFD7gM3OIhDNPsBWpm2qNPZrkU,38522
9
- codegraphcontext/cli/main.py,sha256=g6ITjieGo54w3LKJcaxigefDlZOT3jLmCWOw2QudALQ,94571
9
+ codegraphcontext/cli/main.py,sha256=_dXQI_PQkqTD_P-iaDfRc4Iz9kuLWkPYg_ZxX3c6YHM,94587
10
10
  codegraphcontext/cli/registry_commands.py,sha256=mAJDYw1XUbzhj_SMM9PgO4MzECQj0OR3LU54x-OpPeU,15947
11
11
  codegraphcontext/cli/setup_macos.py,sha256=Xjlv_9jk9qv8Gh7stpH1pvlalzC0Fg176y7jc5G1zh0,3575
12
12
  codegraphcontext/cli/setup_wizard.py,sha256=jrh8504YwKAp-7b-gOW4fwRTD_yxlBYaVPfRk0TYlvQ,42573
13
13
  codegraphcontext/cli/visualizer.py,sha256=YgqXA-o1l3qRABat_s-wT0gTM_rmHmOhpUtxS7Rhcfk,1861
14
- codegraphcontext/core/__init__.py,sha256=t0VsF48zSsw0yAGsg3__7RH_R4IraF0iqnGLOtA_9G8,7245
14
+ codegraphcontext/core/__init__.py,sha256=fOkVEnsA7ikCn3xAhGGSjAokzVn4WgKWJMwBYWDZdEY,7269
15
15
  codegraphcontext/core/bundle_registry.py,sha256=WR0apx71czZLFrTP7a8k3l0vXYsDGyZKn-qzRTzuu7o,7866
16
16
  codegraphcontext/core/cgc_bundle.py,sha256=lqhOQQegm6fCmdndcag7eoS0DV5KNrF8Sm-2VNZ5HR4,35070
17
17
  codegraphcontext/core/cgcignore.py,sha256=uoydGpSR8LVdWYS40C3o0YFkV0s_v9IouxPLF_GsTxY,4172
18
18
  codegraphcontext/core/database.py,sha256=2UL8AyE6vX5mVHg1zcD74TYEREmddhKzN7Bjy2THQwM,11435
19
19
  codegraphcontext/core/database_falkordb.py,sha256=7jBLK_Y4u4ozeVV59vZr9E90Z3XZ_Cq6p4DUyq3WNnc,19741
20
20
  codegraphcontext/core/database_falkordb_remote.py,sha256=aMiuJTB_-2rBUpbAmX8ElZiZrYZbOXK-On3ncgMCyhY,7118
21
- codegraphcontext/core/database_kuzu.py,sha256=eznuzOZrd_QnXiZKF_nWLmJ30juOwtqhgrgiyTv5kv4,31551
21
+ codegraphcontext/core/database_kuzu.py,sha256=g_ceVfg0GZUU8UNzvqwI8yL8ETJ9VmHBCUmHbpgzaJQ,32492
22
22
  codegraphcontext/core/falkor_worker.py,sha256=bMcTOac6e8IzSPjCd-YulGVh-S7QsNtXs2c8hx6M6gk,4958
23
23
  codegraphcontext/core/jobs.py,sha256=d6v_IERdEcDlIsz3CW3p0QMp3N-6dgQICs3FZRPgPgU,4809
24
- codegraphcontext/core/watcher.py,sha256=np5dBVUnNVZ2Wslvk-q9QWZotj4VirqhJMv8WqTUQLQ,13213
24
+ codegraphcontext/core/watcher.py,sha256=XcBDALfUx3ypMmVPmMcjq4q9khLLVQkNmO-IiNAAbj8,16508
25
25
  codegraphcontext/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  codegraphcontext/tools/advanced_language_query_tool.py,sha256=APtC-KSkXXSrVLwiw3kgi6Q-xaqXl6skLWHPyuNh358,3670
27
- codegraphcontext/tools/code_finder.py,sha256=TrdNmN6VHUguGdX4izY2MsM82vlx4E2B9SpZsVpyz30,60463
27
+ codegraphcontext/tools/code_finder.py,sha256=hS3_rVxmIplDJyazMNrwQVFTvDvFXg36UekDq3CDYaA,62345
28
28
  codegraphcontext/tools/graph_builder.py,sha256=jiHHI1XIvmDJ8-BlWPe_WBNEVtXBUCfwhHTkUka-sAg,13219
29
29
  codegraphcontext/tools/package_resolver.py,sha256=KtbdMReTezszjdsqYniL-Xb-QUsrAJWtf1NSiyIPkLI,18704
30
- codegraphcontext/tools/scip_indexer.py,sha256=MdxesJ4GQBOwrE2q3SZjImkzh6BSPQeA1tYsR5Zerok,20021
30
+ codegraphcontext/tools/scip_indexer.py,sha256=bVMkSfvemeXciS6XiXrC5ZgFz33YiW80x6OASfU9zwQ,20169
31
31
  codegraphcontext/tools/scip_pb2.py,sha256=dwOMNKlu6VyLq5h8kTPZRDqxrwfVL8yw7I7ziokFG48,98229
32
32
  codegraphcontext/tools/system.py,sha256=goQOs1A9gvd7SvDPVZB6Jd0DRJKQmjEkn-uiTE40VNM,6046
33
33
  codegraphcontext/tools/tree_sitter_parser.py,sha256=x8gDBCQze65r97DwlIovflg02wrY5SKm4nCVxxIja2E,4500
@@ -46,14 +46,14 @@ codegraphcontext/tools/indexing/schema.py,sha256=Lyzc_06fJx7eMb7a1OiSvEJGzkyXuUX
46
46
  codegraphcontext/tools/indexing/schema_contract.py,sha256=0705pzAw3h9tO9EIfRbdXl5_WudY2l6drE6zX843120,1054
47
47
  codegraphcontext/tools/indexing/scip_pipeline.py,sha256=Y9pvJM4HfbrDQC1nFj7Dgil42nVOwjoNarfIvk7O-vA,5362
48
48
  codegraphcontext/tools/indexing/persistence/__init__.py,sha256=-fSSq1MMR2TB0iuzT80cxGAoKs55mzdYkduwyIzXCh0,59
49
- codegraphcontext/tools/indexing/persistence/writer.py,sha256=ByoUZRBnxZWQXm7D6vZpGtXJs7SUCoB1Uv5EVXHiNd8,32990
49
+ codegraphcontext/tools/indexing/persistence/writer.py,sha256=41bY9inZKEEtchEP8tYaxK0ciH-oGlKm6k1XDCgb79Q,33538
50
50
  codegraphcontext/tools/indexing/resolution/__init__.py,sha256=gsnYNNawrafAyBCKhG7SOF2Um5GblZ-rhgz6S5J2v1s,249
51
- codegraphcontext/tools/indexing/resolution/calls.py,sha256=BROGHHtRdqzLdsNNi56ZSzbrTGnaOoWZdyCQbQ2r-sM,7743
51
+ codegraphcontext/tools/indexing/resolution/calls.py,sha256=0nxb7Em2A6LwPl3DjA7cXADvAC0NRSliD-nT2ssbCgI,8256
52
52
  codegraphcontext/tools/indexing/resolution/inheritance.py,sha256=NZsYDwJpWoOdRSMNl8bPrBgP07iaMD5z1wgm2ELel0I,3292
53
53
  codegraphcontext/tools/languages/c.py,sha256=mrXQWEvMtzLVElATYNZ7VThNk1ZNZdw2UV8i6r2y2y4,22291
54
54
  codegraphcontext/tools/languages/cpp.py,sha256=aXBF_MX43PpRTr5U7UBcn5mZvP6fjP4Q8g21JuCho38,25761
55
55
  codegraphcontext/tools/languages/csharp.py,sha256=0ZIpV531i8yhz4vktaKkzbHDg-TCxgSZPC69SBkmfYU,22594
56
- codegraphcontext/tools/languages/dart.py,sha256=Qql2NhCBpAgh2EmWYJWbkqDAt6grvfrSLyKBs7HdRNU,15442
56
+ codegraphcontext/tools/languages/dart.py,sha256=qt6NkN1IZZWQCcT6lpUqDdpBScXI498dJNjS1ZQPf3k,18705
57
57
  codegraphcontext/tools/languages/elixir.py,sha256=mFsMvi_5ZlCsrB5QQ7Kklj94tLo3D6aFU7ukuhGmGSI,18815
58
58
  codegraphcontext/tools/languages/go.py,sha256=cBfeM3Qt9W73KKb39VPhS6LlcWLdVJpvrAnnUeSiH4E,20456
59
59
  codegraphcontext/tools/languages/haskell.py,sha256=LboJms-iUZVBY32Q5GvWlyHCGXT4ov05JVO-zbdOFlQ,17285
@@ -62,7 +62,7 @@ codegraphcontext/tools/languages/javascript.py,sha256=XB-icOtzIERAtackWWmaayGAWR
62
62
  codegraphcontext/tools/languages/kotlin.py,sha256=6HvrClCgCNiGJnTjDhyHtuAy2PyGZwJN2Mx1zOA6T2c,28051
63
63
  codegraphcontext/tools/languages/perl.py,sha256=EHFpcGO2NLPPxyx4s0kmtqQ5_ODFLx7QHFDIvwlRb84,10004
64
64
  codegraphcontext/tools/languages/php.py,sha256=mqCoahKTrKzc0lsE-1K3S_UBT-pyIMi8_CrCJ2kHCUg,23350
65
- codegraphcontext/tools/languages/python.py,sha256=qB1rMjHKMtx4MHxg0zAOkFU34Zvete4JsdwW33b7_xk,25731
65
+ codegraphcontext/tools/languages/python.py,sha256=abyN-POHLoA2PLkk8NikDgAi3368_II4Lk0-wNjky4Y,27189
66
66
  codegraphcontext/tools/languages/ruby.py,sha256=sd803ZpfQbsWuqXlUvX4UShM2NkkgUf9xOkl160X8oM,21993
67
67
  codegraphcontext/tools/languages/rust.py,sha256=TD7H4SsSsPWJnIUoThkA8gxIKnQhqL6_cVGnAaOBeEE,11991
68
68
  codegraphcontext/tools/languages/scala.py,sha256=-seQcdX0UJfDPiyhgf4kg6kDXJsSNcfVH472aSv1dH8,23003
@@ -124,9 +124,9 @@ codegraphcontext/viz/dist/wasm/tree-sitter-typescript.wasm,sha256=hRVATc7tOOHthq
124
124
  codegraphcontext/viz/dist/wasm/tree-sitter.wasm,sha256=CCeVuI_hXktkBD-DW01xYPv5XoAYVRIqzJUJI5te-RY,196763
125
125
  codegraphcontext/viz/dist/wasm/web-tree-sitter.js,sha256=DIaCNqRylrT_PBVw8g4ImeSnhP9uXNe_ycOlUiVGPko,153666
126
126
  codegraphcontext/viz/dist/wasm/web-tree-sitter.wasm,sha256=CCeVuI_hXktkBD-DW01xYPv5XoAYVRIqzJUJI5te-RY,196763
127
- codegraphcontext-0.4.4.dist-info/licenses/LICENSE,sha256=rh8M-bJpQYJnw2vtRVgt0t7piMZXh5QzaKeNEI0vqqA,1061
128
- codegraphcontext-0.4.4.dist-info/METADATA,sha256=FDddbhcyfIBGfUlyBl9UHaQgdtotbA4sed_Jo_M59rM,22330
129
- codegraphcontext-0.4.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
130
- codegraphcontext-0.4.4.dist-info/entry_points.txt,sha256=LCxWCWMshdvYGoHBPuQZ8C-e4CiNSHCLXofrNSGHkoE,103
131
- codegraphcontext-0.4.4.dist-info/top_level.txt,sha256=CBgc6LAPZIO5FS0nSYYkylDifHsZTIqw3Gf5UwDxeGI,17
132
- codegraphcontext-0.4.4.dist-info/RECORD,,
127
+ codegraphcontext-0.4.5.dist-info/licenses/LICENSE,sha256=rh8M-bJpQYJnw2vtRVgt0t7piMZXh5QzaKeNEI0vqqA,1061
128
+ codegraphcontext-0.4.5.dist-info/METADATA,sha256=IJH89toSZECngKn505IurSIunzimNBsqsT2Dy_n0MWU,22355
129
+ codegraphcontext-0.4.5.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
130
+ codegraphcontext-0.4.5.dist-info/entry_points.txt,sha256=LCxWCWMshdvYGoHBPuQZ8C-e4CiNSHCLXofrNSGHkoE,103
131
+ codegraphcontext-0.4.5.dist-info/top_level.txt,sha256=CBgc6LAPZIO5FS0nSYYkylDifHsZTIqw3Gf5UwDxeGI,17
132
+ codegraphcontext-0.4.5.dist-info/RECORD,,