cortex-local 2.6.0__tar.gz → 2.7.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 (28) hide show
  1. {cortex_local-2.6.0 → cortex_local-2.7.0}/PKG-INFO +2 -1
  2. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/chunker.py +56 -1
  3. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/code_graph.py +18 -0
  4. cortex_local-2.7.0/cortex/core/godot.py +296 -0
  5. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/mcp_server.py +2 -1
  6. {cortex_local-2.6.0 → cortex_local-2.7.0}/pyproject.toml +1 -0
  7. {cortex_local-2.6.0 → cortex_local-2.7.0}/.gitignore +0 -0
  8. {cortex_local-2.6.0 → cortex_local-2.7.0}/README.md +0 -0
  9. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/__init__.py +0 -0
  10. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/cli.py +0 -0
  11. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/config.py +0 -0
  12. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/__init__.py +0 -0
  13. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/cache.py +0 -0
  14. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/formatters.py +0 -0
  15. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/global_graph.py +0 -0
  16. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/graph.py +0 -0
  17. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/notes_graph.py +0 -0
  18. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/roslyn_analyzer.py +0 -0
  19. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/roslyn_tool/CortexAnalyzer.csproj +0 -0
  20. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/core/roslyn_tool/Program.cs +0 -0
  21. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/db.py +0 -0
  22. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/embedder.py +0 -0
  23. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/indexer.py +0 -0
  24. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/state.py +0 -0
  25. {cortex_local-2.6.0 → cortex_local-2.7.0}/cortex/watcher.py +0 -0
  26. {cortex_local-2.6.0 → cortex_local-2.7.0}/hatch_build.py +0 -0
  27. {cortex_local-2.6.0 → cortex_local-2.7.0}/tests/test_imports.py +0 -0
  28. {cortex_local-2.6.0 → cortex_local-2.7.0}/tests/test_stdio_init.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cortex-local
3
- Version: 2.6.0
3
+ Version: 2.7.0
4
4
  Summary: Local semantic search MCP server — notes + code, embedded Qdrant, fastembed
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: click>=8.0.0
@@ -10,6 +10,7 @@ Requires-Dist: networkx>=3.0
10
10
  Requires-Dist: qdrant-client>=1.17.0
11
11
  Requires-Dist: tqdm>=4.0
12
12
  Requires-Dist: tree-sitter-c-sharp
13
+ Requires-Dist: tree-sitter-gdscript
13
14
  Requires-Dist: tree-sitter-javascript
14
15
  Requires-Dist: tree-sitter-kotlin
15
16
  Requires-Dist: tree-sitter-python
@@ -6,6 +6,11 @@ import os
6
6
  import re as _re
7
7
  from pathlib import Path
8
8
 
9
+ try:
10
+ from . import godot as _godot
11
+ except ImportError: # server: core/ is on sys.path as top-level modules
12
+ import godot as _godot
13
+
9
14
  _RAZOR_BLOCK_RE = _re.compile(r'@(?:code|functions)\s*\{', _re.IGNORECASE)
10
15
 
11
16
 
@@ -32,7 +37,8 @@ def _extract_razor_cs_blocks(source: str) -> list:
32
37
 
33
38
  REPOS_DIR = Path(os.environ.get("REPOS_DIR", "/tmp/repos"))
34
39
 
35
- CODE_EXTS = {".js", ".ts", ".tsx", ".jsx", ".svelte", ".py", ".java", ".go", ".rs", ".css", ".html", ".kt", ".kts", ".gd", ".yml", ".yaml", ".cs", ".razor", ".cshtml"}
40
+ CODE_EXTS = {".js", ".ts", ".tsx", ".jsx", ".svelte", ".py", ".java", ".go", ".rs", ".css", ".html", ".kt", ".kts", ".gd", ".yml", ".yaml", ".cs", ".razor", ".cshtml",
41
+ ".tscn", ".tres", ".godot", ".gdshader", ".dialogue"}
36
42
  SKIP_DIRS = {"node_modules", ".git", "dist", "build", ".next", ".svelte-kit", "__pycache__", ".gradle", "target", "bin", "obj", ".godot"}
37
43
  LANG_MAP = {
38
44
  ".js": "javascript", ".ts": "typescript", ".tsx": "typescript",
@@ -41,6 +47,8 @@ LANG_MAP = {
41
47
  ".kt": "kotlin", ".kts": "kotlin", ".gd": "gdscript",
42
48
  ".yml": "yaml", ".yaml": "yaml", ".cs": "csharp",
43
49
  ".razor": "razor", ".cshtml": "cshtml",
50
+ ".tscn": "godot-scene", ".tres": "godot-resource", ".godot": "godot-project",
51
+ ".gdshader": "gdshader", ".dialogue": "dialogue",
44
52
  }
45
53
 
46
54
  def _is_excluded(rel: str, patterns: list) -> bool:
@@ -297,6 +305,8 @@ def chunk_file(path: Path, repo_name: str, base_dir: Path | None = None, github_
297
305
  rel = path.name
298
306
  language = LANG_MAP.get(ext, ext.lstrip("."))
299
307
  extra = _extra_tokens(rel, lines)
308
+ if ext == ".gd":
309
+ extra = (extra + " " + _godot.gd_context(source_text)).strip()
300
310
 
301
311
  def _header(rel: str, s: int, e: int, tokens: str) -> str:
302
312
  h = f"# {repo_name}/{rel} (lines {s}-{e})\n# {tokens}"
@@ -331,6 +341,29 @@ def chunk_file(path: Path, repo_name: str, base_dir: Path | None = None, github_
331
341
  })
332
342
  return chunks
333
343
 
344
+ # Godot scenes/resources/project: index a readable summary, not the raw serialization
345
+ if ext in _godot.GODOT_SUMMARY_EXTS:
346
+ summary = _godot.summarize(path, source_text).splitlines()
347
+ if not summary:
348
+ return []
349
+ tokens = _path_tokens(repo_name, rel)
350
+ n_file = len(lines)
351
+ windows = [summary[i:i + MAX_CHUNK_LINES] for i in range(0, len(summary), MAX_CHUNK_LINES)]
352
+ k = len(windows)
353
+ out = []
354
+ for i, w in enumerate(windows):
355
+ # distinct pseudo line ranges per window (chunk ids are keyed on start_line)
356
+ s_line = 1 + (i * n_file) // k
357
+ e_line = max(s_line, ((i + 1) * n_file) // k)
358
+ url = f"{github_url_base}/{rel}#L{s_line}-L{e_line}" if github_url_base else ""
359
+ out.append({
360
+ "repo": repo_name, "file": rel, "language": language,
361
+ "start_line": s_line, "end_line": e_line,
362
+ "text": f"{_header(rel, s_line, e_line, tokens)}\n\n" + "\n".join(w),
363
+ "github_url": url,
364
+ })
365
+ return out
366
+
334
367
  # Small files: keep as single chunk to avoid diluting path/filename signal
335
368
  if len(lines) <= MAX_CHUNK_LINES:
336
369
  body = "\n".join(lines).strip()
@@ -371,6 +404,28 @@ def chunk_file(path: Path, repo_name: str, base_dir: Path | None = None, github_
371
404
  if chunks:
372
405
  return chunks
373
406
 
407
+ # GDScript: functions / inner classes / member blocks, annotations kept with their decl
408
+ if ext == ".gd":
409
+ spans = _godot.gd_spans(raw)
410
+ if spans:
411
+ chunks = []
412
+ tokens = _path_tokens(repo_name, rel)
413
+ for s0, e, _kind in spans:
414
+ if e - s0 > MAX_CHUNK_LINES:
415
+ chunks.extend(_sliding_window(lines, s0, e, repo_name, rel, language, github_url_base, extra))
416
+ continue
417
+ body = "\n".join(lines[s0:e]).strip()
418
+ if body:
419
+ url = f"{github_url_base}/{rel}#L{s0+1}-L{e}" if github_url_base else ""
420
+ chunks.append({
421
+ "repo": repo_name, "file": rel, "language": language,
422
+ "start_line": s0 + 1, "end_line": e,
423
+ "text": f"{_header(rel, s0+1, e, tokens)}\n\n{body}",
424
+ "github_url": url,
425
+ })
426
+ if chunks:
427
+ return chunks
428
+
374
429
  if _TS_AVAILABLE and ext in _TS_LANGUAGES:
375
430
  try:
376
431
  parser = _TSParser(_TS_LANGUAGES[ext])
@@ -8,6 +8,13 @@ from pathlib import Path
8
8
 
9
9
  log = logging.getLogger("cortex.graph")
10
10
 
11
+ try:
12
+ from . import godot as _godot
13
+ except ImportError: # server: core/ is on sys.path as top-level modules
14
+ import godot as _godot
15
+
16
+ _GODOT_EXTS = (".gd", ".tscn", ".tres", ".godot")
17
+
11
18
  try:
12
19
  import networkx as nx
13
20
  _NX_AVAILABLE = True
@@ -336,6 +343,17 @@ def build_code_graph(repo_path: Path, all_files: set):
336
343
  _add_edge(G, rel, target, "call")
337
344
  for target in _extract_inheritance_targets(path, ext, all_files, rel, symbol_to_file):
338
345
  _add_edge(G, rel, target, "inherits")
346
+ godot_files = [f for f in all_files if f.endswith(_GODOT_EXTS)]
347
+ if godot_files:
348
+ proj = _godot.GodotProject(repo_path, all_files)
349
+ for rel in godot_files:
350
+ try:
351
+ text = (repo_path / rel).read_text(encoding="utf-8", errors="replace")
352
+ except Exception:
353
+ continue
354
+ for target, kind in _godot.gd_edges(rel, text, proj):
355
+ if target in all_files:
356
+ _add_edge(G, rel, target, kind)
339
357
  return G
340
358
 
341
359
 
@@ -0,0 +1,296 @@
1
+ """Godot support: GDScript semantic chunks, scene/resource/project summaries, graph edges.
2
+
3
+ Used by chunker.chunk_file (chunks) and code_graph.build_code_graph (edges). Paths handed in
4
+ and out are repo-relative POSIX; `res://` paths are resolved against the directory that holds
5
+ project.godot (the Godot project root, not necessarily the repo root).
6
+ """
7
+
8
+ import re
9
+ from pathlib import Path
10
+
11
+ try:
12
+ from tree_sitter import Language, Parser
13
+ import tree_sitter_gdscript as _tsgd
14
+ _GD_LANG = Language(_tsgd.language())
15
+ except Exception: # grammar missing -> chunker falls back to the sliding window
16
+ _GD_LANG = None
17
+
18
+ GODOT_SUMMARY_EXTS = {".tscn", ".tres", ".godot"}
19
+
20
+ # ── res:// / uid:// resolution ───────────────────────────────────────────────
21
+
22
+ _SECTION_RE = re.compile(r'^\[(\w+)([^\]]*)\]\s*$', re.MULTILINE)
23
+ _ATTR_RE = re.compile(r'(\w+)=("([^"]*)"|[^\s\]]+)')
24
+
25
+
26
+ def _attrs(s: str) -> dict:
27
+ return {m.group(1): (m.group(3) if m.group(3) is not None else m.group(2)) for m in _ATTR_RE.finditer(s)}
28
+
29
+
30
+ class GodotProject:
31
+ """Repo-level Godot context: project roots, autoloads, uid map, class_name map."""
32
+
33
+ def __init__(self, repo_path: Path, all_files: set):
34
+ self.roots = sorted({str(Path(f).parent.as_posix()) for f in all_files if Path(f).name == "project.godot"},
35
+ key=len, reverse=True)
36
+ self.autoloads: dict = {} # Name -> repo-relative file
37
+ self.uids: dict = {} # uid://x -> repo-relative file
38
+ self.classes: dict = {} # class_name -> repo-relative file
39
+ for f in all_files:
40
+ p = repo_path / f
41
+ if Path(f).name == "project.godot":
42
+ for name, target in _parse_autoloads(_read(p)).items():
43
+ resolved = self.res(target, f)
44
+ if resolved:
45
+ self.autoloads[name] = resolved
46
+ elif f.endswith(".gd"):
47
+ m = re.search(r'^class_name\s+(\w+)', _read(p), re.MULTILINE)
48
+ if m:
49
+ self.classes.setdefault(m.group(1), f)
50
+ if f.endswith((".tscn", ".tres")):
51
+ head = _read(p, 400)
52
+ m = re.search(r'uid="(uid://[^"]+)"', head.split("\n", 1)[0])
53
+ if m:
54
+ self.uids[m.group(1)] = f
55
+ # `.uid` twin files (Godot 4.4+): script.gd.uid contains the script's uid
56
+ for uidfile in repo_path.rglob("*.uid"):
57
+ try:
58
+ rel = uidfile.relative_to(repo_path).as_posix()[:-4]
59
+ if rel in all_files:
60
+ self.uids[uidfile.read_text(errors="replace").strip()] = rel
61
+ except Exception:
62
+ continue
63
+
64
+ def project_root(self, rel: str) -> str:
65
+ for r in self.roots:
66
+ if r == "." or rel.startswith(r + "/"):
67
+ return r
68
+ return "."
69
+
70
+ def res(self, path: str, from_file: str) -> str | None:
71
+ """res://a/b.gd (or uid://...) -> repo-relative path, None if unknown."""
72
+ path = path.strip().lstrip("*")
73
+ if path.startswith("uid://"):
74
+ return self.uids.get(path)
75
+ if not path.startswith("res://"):
76
+ return None
77
+ root = self.project_root(from_file)
78
+ tail = path[len("res://"):]
79
+ return tail if root == "." else f"{root}/{tail}"
80
+
81
+
82
+ def _read(p: Path, limit: int | None = None) -> str:
83
+ try:
84
+ text = p.read_text(encoding="utf-8", errors="replace")
85
+ return text[:limit] if limit else text
86
+ except Exception:
87
+ return ""
88
+
89
+
90
+ def _parse_autoloads(text: str) -> dict:
91
+ out, in_section = {}, False
92
+ for line in text.splitlines():
93
+ s = line.strip()
94
+ if s.startswith("["):
95
+ in_section = s == "[autoload]"
96
+ continue
97
+ if in_section and "=" in s:
98
+ k, v = s.split("=", 1)
99
+ out[k.strip()] = v.strip().strip('"')
100
+ return out
101
+
102
+
103
+ # ── GDScript chunking ────────────────────────────────────────────────────────
104
+
105
+ _DECL_TYPES = {"signal_statement", "variable_statement", "const_statement", "enum_definition",
106
+ "export_variable_statement", "onready_variable_statement"}
107
+
108
+
109
+ def gd_context(source: str) -> str:
110
+ """'class_name X extends Y' for embedding headers ('' when the file declares neither)."""
111
+ cn = re.search(r'^class_name\s+(\w+)', source, re.MULTILINE)
112
+ ex = re.search(r'^extends\s+(\S+)', source, re.MULTILINE)
113
+ parts = []
114
+ if cn:
115
+ parts.append(f"class_name {cn.group(1)}")
116
+ if ex:
117
+ parts.append(f"extends {ex.group(1)}")
118
+ return " ".join(parts)
119
+
120
+
121
+ def gd_spans(raw: bytes) -> list | None:
122
+ """Top-level semantic spans [(start_line0, end_line_excl, kind)], or None without the grammar.
123
+
124
+ Functions and inner classes are their own spans, with preceding annotations (@rpc,
125
+ @warning_ignore ...) attached. Runs of signals/vars/consts/enums form one 'members' span.
126
+ """
127
+ if _GD_LANG is None:
128
+ return None
129
+ try:
130
+ tree = Parser(_GD_LANG).parse(raw)
131
+ except Exception:
132
+ return None
133
+ spans: list = []
134
+ pending_ann = None # start line of annotations waiting for their declaration
135
+ decl = None # [start, end] of the current members run
136
+ for node in tree.root_node.children:
137
+ t = node.type
138
+ s, e = node.start_point[0], node.end_point[0] + 1
139
+ if t == "annotation":
140
+ pending_ann = s if pending_ann is None else pending_ann
141
+ continue
142
+ start = pending_ann if pending_ann is not None else s
143
+ pending_ann = None
144
+ if t in ("function_definition", "class_definition", "constructor_definition"):
145
+ if decl:
146
+ spans.append((decl[0], decl[1], "members")); decl = None
147
+ spans.append((start, e, "class" if t == "class_definition" else "function"))
148
+ elif t in _DECL_TYPES:
149
+ decl = [decl[0], e] if decl else [start, e]
150
+ # class_name / extends / comments: covered by the header context
151
+ if decl:
152
+ spans.append((decl[0], decl[1], "members"))
153
+ return spans
154
+
155
+
156
+ # ── Scene / resource / project summaries (indexed instead of raw text) ──────
157
+
158
+ def _ext_resources(text: str) -> dict:
159
+ out = {}
160
+ for m in _SECTION_RE.finditer(text):
161
+ if m.group(1) == "ext_resource":
162
+ a = _attrs(m.group(2))
163
+ if "id" in a:
164
+ out[a["id"]] = a.get("path") or a.get("uid", "")
165
+ return out
166
+
167
+
168
+ def _sections(text: str):
169
+ """Yield (kind, attrs, body_lines) for each [section] in a .tscn/.tres file."""
170
+ matches = list(_SECTION_RE.finditer(text))
171
+ for i, m in enumerate(matches):
172
+ end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
173
+ body = text[m.end():end].strip("\n").splitlines()
174
+ yield m.group(1), _attrs(m.group(2)), body
175
+
176
+
177
+ _EXTRES_REF = re.compile(r'ExtResource\(\s*"?([^")\s]+)"?\s*\)')
178
+ _SKIP_VALUE = re.compile(r'^(Packed\w*Array|PoolByteArray|Image)\(')
179
+
180
+
181
+ def summarize_tscn(text: str) -> str:
182
+ ext = _ext_resources(text)
183
+ lines = ["Godot scene", "Nodes:"]
184
+ conns = []
185
+ for kind, a, body in _sections(text):
186
+ if kind == "node":
187
+ name, parent = a.get("name", "?"), a.get("parent")
188
+ path = name if parent is None else (name if parent == "." else f"{parent}/{name}")
189
+ desc = f" {path}"
190
+ if a.get("type"):
191
+ desc += f" ({a['type']})"
192
+ inst = _EXTRES_REF.search(a.get("instance", ""))
193
+ if inst:
194
+ desc += f" instance={ext.get(inst.group(1), inst.group(1))}"
195
+ for line in body:
196
+ if line.startswith("script ="):
197
+ ref = _EXTRES_REF.search(line)
198
+ if ref:
199
+ desc += f" script={ext.get(ref.group(1), ref.group(1))}"
200
+ lines.append(desc)
201
+ elif kind == "connection":
202
+ conns.append(f" {a.get('from', '?')}.{a.get('signal', '?')} -> {a.get('method', '?')} (on {a.get('to', '?')})")
203
+ if conns:
204
+ lines.append("Signal connections:")
205
+ lines += conns
206
+ if ext:
207
+ lines.append("Uses: " + ", ".join(sorted(set(v for v in ext.values() if v))))
208
+ return "\n".join(lines)
209
+
210
+
211
+ def summarize_tres(text: str) -> str:
212
+ ext = _ext_resources(text)
213
+ lines = []
214
+ for kind, a, body in _sections(text):
215
+ if kind == "gd_resource":
216
+ head = f"Godot resource type={a.get('type', '?')}"
217
+ if a.get("script_class"):
218
+ head += f" script_class={a['script_class']}"
219
+ lines.append(head)
220
+ elif kind in ("resource", "sub_resource"):
221
+ if kind == "sub_resource":
222
+ lines.append(f"Sub-resource {a.get('type', '')} {a.get('id', '')}".rstrip())
223
+ for line in body:
224
+ if "=" not in line:
225
+ continue
226
+ k, v = (x.strip() for x in line.split("=", 1))
227
+ if _SKIP_VALUE.match(v):
228
+ continue
229
+ v = _EXTRES_REF.sub(lambda m: ext.get(m.group(1), m.group(1)), v)
230
+ lines.append(f" {k} = {v[:200]}")
231
+ if ext:
232
+ lines.append("Uses: " + ", ".join(sorted(set(v for v in ext.values() if v))))
233
+ return "\n".join(lines)
234
+
235
+
236
+ def summarize_project(text: str) -> str:
237
+ lines, section, actions = ["Godot project.godot"], "", []
238
+ for raw in text.splitlines():
239
+ s = raw.strip()
240
+ if s.startswith("["):
241
+ section = s.strip("[]")
242
+ continue
243
+ if "=" not in s or s.startswith(";"):
244
+ continue
245
+ k, v = (x.strip() for x in s.split("=", 1))
246
+ if section == "application" and k in ("config/name", "run/main_scene", "config/features"):
247
+ lines.append(f"{k} = {v}")
248
+ elif section == "autoload":
249
+ lines.append(f"autoload {k} = {v.strip(chr(34)).lstrip('*')}")
250
+ elif section == "input" and not k.startswith(" "):
251
+ actions.append(k)
252
+ if actions:
253
+ lines.append("Input actions: " + ", ".join(actions))
254
+ return "\n".join(lines)
255
+
256
+
257
+ def summarize(path: Path, text: str) -> str:
258
+ if path.suffix == ".tscn":
259
+ return summarize_tscn(text)
260
+ if path.suffix == ".tres":
261
+ return summarize_tres(text)
262
+ return summarize_project(text)
263
+
264
+
265
+ # ── Graph edges ──────────────────────────────────────────────────────────────
266
+
267
+ _RES_STR = re.compile(r'"\*?((?:res|uid)://[^"]+)"')
268
+ _LOAD_RE = re.compile(r'\b(?:preload|load|ResourceLoader\.load)\s*\(\s*"((?:res|uid)://[^"]+)"')
269
+ _EXTENDS_RE = re.compile(r'^extends\s+("([^"]+)"|(\w+))', re.MULTILINE)
270
+ _USE_RE = re.compile(r'\b([A-Z]\w*)\s*(?:\.|\(|$)|(?::|\bas|\bis|->)\s*([A-Z]\w*)', re.MULTILINE)
271
+
272
+
273
+ def gd_edges(rel: str, text: str, proj: GodotProject) -> list:
274
+ """[(target, edge_type)] for a .gd / .tscn / .tres / project.godot file."""
275
+ edges: list = []
276
+ if rel.endswith(".gd"):
277
+ m = _EXTENDS_RE.search(text)
278
+ if m:
279
+ tgt = proj.res(m.group(2), rel) if m.group(2) else proj.classes.get(m.group(3))
280
+ if tgt:
281
+ edges.append((tgt, "inherits"))
282
+ for m in _LOAD_RE.finditer(text):
283
+ tgt = proj.res(m.group(1), rel)
284
+ if tgt:
285
+ edges.append((tgt, "import"))
286
+ used = {a or b for a, b in _USE_RE.findall(text)}
287
+ for name in used:
288
+ tgt = proj.classes.get(name) or proj.autoloads.get(name)
289
+ if tgt:
290
+ edges.append((tgt, "call"))
291
+ else: # scenes/resources/project: every res:// reference is a dependency
292
+ for m in _RES_STR.finditer(text):
293
+ tgt = proj.res(m.group(1), rel)
294
+ if tgt:
295
+ edges.append((tgt, "import"))
296
+ return [(t, k) for t, k in edges if t != rel]
@@ -369,9 +369,10 @@ def get_neighbors(file: str, repo: str) -> str:
369
369
  meta = get_graph_meta(repo)
370
370
  if not meta:
371
371
  return f"No graph data for '{repo}'. Run reindex() first."
372
+ file = file.removeprefix("res://") # Godot paths: res:// is relative to the project root
372
373
  file_meta = meta.get(file)
373
374
  if file_meta is None:
374
- matches = [f for f in meta if file in f]
375
+ matches = sorted((f for f in meta if f.endswith("/" + file) or file in f), key=len)
375
376
  if not matches:
376
377
  return f"File '{file}' not found in graph for '{repo}'."
377
378
  file = matches[0]
@@ -22,6 +22,7 @@ dependencies = [
22
22
  "tree-sitter-typescript",
23
23
  "tree-sitter-kotlin",
24
24
  "tree-sitter-c-sharp",
25
+ "tree-sitter-gdscript",
25
26
  ]
26
27
 
27
28
  [project.scripts]
File without changes
File without changes
File without changes
File without changes