ragtime-cli 0.2.3__py3-none-any.whl → 0.2.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.
Potentially problematic release.
This version of ragtime-cli might be problematic. Click here for more details.
- ragtime_cli-0.2.5.dist-info/METADATA +402 -0
- {ragtime_cli-0.2.3.dist-info → ragtime_cli-0.2.5.dist-info}/RECORD +13 -11
- src/cli.py +625 -4
- src/commands/generate-docs.md +325 -0
- src/config.py +1 -1
- src/indexers/__init__.py +6 -0
- src/indexers/code.py +473 -0
- src/mcp_server.py +1 -1
- src/memory.py +6 -1
- ragtime_cli-0.2.3.dist-info/METADATA +0 -220
- {ragtime_cli-0.2.3.dist-info → ragtime_cli-0.2.5.dist-info}/WHEEL +0 -0
- {ragtime_cli-0.2.3.dist-info → ragtime_cli-0.2.5.dist-info}/entry_points.txt +0 -0
- {ragtime_cli-0.2.3.dist-info → ragtime_cli-0.2.5.dist-info}/licenses/LICENSE +0 -0
- {ragtime_cli-0.2.3.dist-info → ragtime_cli-0.2.5.dist-info}/top_level.txt +0 -0
src/indexers/code.py
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Code indexer - extracts functions, classes, and types from source files.
|
|
3
|
+
|
|
4
|
+
Parses code to create searchable chunks for each meaningful unit (function, class, etc).
|
|
5
|
+
This allows searching for specific code constructs like "useAsyncState" or "JWTManager".
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
import re
|
|
10
|
+
from fnmatch import fnmatch
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Language file extensions
|
|
16
|
+
LANGUAGE_EXTENSIONS = {
|
|
17
|
+
"python": [".py"],
|
|
18
|
+
"typescript": [".ts", ".tsx"],
|
|
19
|
+
"javascript": [".js", ".jsx"],
|
|
20
|
+
"vue": [".vue"],
|
|
21
|
+
"dart": [".dart"],
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class CodeEntry:
|
|
27
|
+
"""A parsed code symbol ready for indexing."""
|
|
28
|
+
content: str # The actual code + context
|
|
29
|
+
file_path: str # Full path to file
|
|
30
|
+
language: str # python, typescript, etc.
|
|
31
|
+
symbol_name: str # Function/class/component name
|
|
32
|
+
symbol_type: str # function, class, interface, component, etc.
|
|
33
|
+
line_number: int # Line where symbol starts
|
|
34
|
+
docstring: str | None = None # Extracted docstring/JSDoc
|
|
35
|
+
|
|
36
|
+
def to_metadata(self) -> dict:
|
|
37
|
+
"""Convert to ChromaDB metadata dict."""
|
|
38
|
+
return {
|
|
39
|
+
"type": "code",
|
|
40
|
+
"file": self.file_path,
|
|
41
|
+
"language": self.language,
|
|
42
|
+
"symbol_name": self.symbol_name,
|
|
43
|
+
"symbol_type": self.symbol_type,
|
|
44
|
+
"line": self.line_number,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_extensions_for_languages(languages: list[str]) -> list[str]:
|
|
49
|
+
"""Get file extensions for the specified languages."""
|
|
50
|
+
extensions = []
|
|
51
|
+
for lang in languages:
|
|
52
|
+
extensions.extend(LANGUAGE_EXTENSIONS.get(lang, []))
|
|
53
|
+
return extensions
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def discover_code_files(
|
|
57
|
+
root: Path,
|
|
58
|
+
languages: list[str],
|
|
59
|
+
exclude: list[str] | None = None,
|
|
60
|
+
) -> list[Path]:
|
|
61
|
+
"""
|
|
62
|
+
Find all code files to index.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
root: Directory to search
|
|
66
|
+
languages: List of languages to include
|
|
67
|
+
exclude: Patterns to exclude
|
|
68
|
+
"""
|
|
69
|
+
exclude = exclude or [
|
|
70
|
+
"**/node_modules/**",
|
|
71
|
+
"**/.git/**",
|
|
72
|
+
"**/build/**",
|
|
73
|
+
"**/dist/**",
|
|
74
|
+
"**/__pycache__/**",
|
|
75
|
+
"**/.venv/**",
|
|
76
|
+
"**/venv/**",
|
|
77
|
+
"**/.dart_tool/**",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
extensions = get_extensions_for_languages(languages)
|
|
81
|
+
files = []
|
|
82
|
+
|
|
83
|
+
for ext in extensions:
|
|
84
|
+
for path in root.rglob(f"*{ext}"):
|
|
85
|
+
if path.is_file():
|
|
86
|
+
# Check exclusions using proper glob matching
|
|
87
|
+
skip = False
|
|
88
|
+
# Use relative path for matching to avoid absolute path issues
|
|
89
|
+
try:
|
|
90
|
+
rel_path = str(path.relative_to(root))
|
|
91
|
+
except ValueError:
|
|
92
|
+
rel_path = str(path)
|
|
93
|
+
|
|
94
|
+
for ex in exclude:
|
|
95
|
+
# Handle ** patterns by checking if pattern appears in path
|
|
96
|
+
if "**" in ex:
|
|
97
|
+
# Convert glob to a simpler check: **/node_modules/** means
|
|
98
|
+
# any path containing /node_modules/ segment
|
|
99
|
+
core_pattern = ex.replace("**", "").strip("/")
|
|
100
|
+
if core_pattern and f"/{core_pattern}/" in f"/{rel_path}/":
|
|
101
|
+
skip = True
|
|
102
|
+
break
|
|
103
|
+
elif fnmatch(rel_path, ex) or fnmatch(path.name, ex):
|
|
104
|
+
skip = True
|
|
105
|
+
break
|
|
106
|
+
if not skip:
|
|
107
|
+
files.append(path)
|
|
108
|
+
|
|
109
|
+
return files
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def index_python_file(file_path: Path, content: str) -> list[CodeEntry]:
|
|
113
|
+
"""Extract code entries from a Python file."""
|
|
114
|
+
entries = []
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
tree = ast.parse(content)
|
|
118
|
+
except SyntaxError:
|
|
119
|
+
return entries
|
|
120
|
+
|
|
121
|
+
for node in ast.walk(tree):
|
|
122
|
+
if isinstance(node, ast.ClassDef):
|
|
123
|
+
# Get class code (signature + docstring + method signatures)
|
|
124
|
+
start_line = node.lineno
|
|
125
|
+
docstring = ast.get_docstring(node) or ""
|
|
126
|
+
|
|
127
|
+
# Build a summary of the class
|
|
128
|
+
method_names = []
|
|
129
|
+
for item in node.body:
|
|
130
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
131
|
+
method_names.append(item.name)
|
|
132
|
+
|
|
133
|
+
class_summary = f"class {node.name}:\n"
|
|
134
|
+
if docstring:
|
|
135
|
+
class_summary += f' """{docstring}"""\n'
|
|
136
|
+
if method_names:
|
|
137
|
+
class_summary += f"\n # Methods: {', '.join(method_names)}\n"
|
|
138
|
+
|
|
139
|
+
entries.append(CodeEntry(
|
|
140
|
+
content=class_summary,
|
|
141
|
+
file_path=str(file_path),
|
|
142
|
+
language="python",
|
|
143
|
+
symbol_name=node.name,
|
|
144
|
+
symbol_type="class",
|
|
145
|
+
line_number=start_line,
|
|
146
|
+
docstring=docstring,
|
|
147
|
+
))
|
|
148
|
+
|
|
149
|
+
# Also index public methods
|
|
150
|
+
for item in node.body:
|
|
151
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
152
|
+
if item.name.startswith("_") and item.name != "__init__":
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
method_doc = ast.get_docstring(item) or ""
|
|
156
|
+
async_prefix = "async " if isinstance(item, ast.AsyncFunctionDef) else ""
|
|
157
|
+
|
|
158
|
+
# Get signature
|
|
159
|
+
args = []
|
|
160
|
+
for arg in item.args.args:
|
|
161
|
+
if arg.arg == "self":
|
|
162
|
+
continue
|
|
163
|
+
type_hint = ""
|
|
164
|
+
if arg.annotation:
|
|
165
|
+
try:
|
|
166
|
+
type_hint = f": {ast.unparse(arg.annotation)}"
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
args.append(f"{arg.arg}{type_hint}")
|
|
170
|
+
|
|
171
|
+
ret_type = ""
|
|
172
|
+
if item.returns:
|
|
173
|
+
try:
|
|
174
|
+
ret_type = f" -> {ast.unparse(item.returns)}"
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
method_sig = f"{async_prefix}def {item.name}({', '.join(args)}){ret_type}"
|
|
179
|
+
method_content = f"class {node.name}:\n {method_sig}:\n"
|
|
180
|
+
if method_doc:
|
|
181
|
+
method_content += f' """{method_doc}"""\n'
|
|
182
|
+
|
|
183
|
+
entries.append(CodeEntry(
|
|
184
|
+
content=method_content,
|
|
185
|
+
file_path=str(file_path),
|
|
186
|
+
language="python",
|
|
187
|
+
symbol_name=f"{node.name}.{item.name}",
|
|
188
|
+
symbol_type="method",
|
|
189
|
+
line_number=item.lineno,
|
|
190
|
+
docstring=method_doc,
|
|
191
|
+
))
|
|
192
|
+
|
|
193
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
194
|
+
# Top-level function
|
|
195
|
+
if hasattr(node, 'col_offset') and node.col_offset > 0:
|
|
196
|
+
continue # Skip nested functions
|
|
197
|
+
|
|
198
|
+
docstring = ast.get_docstring(node) or ""
|
|
199
|
+
async_prefix = "async " if isinstance(node, ast.AsyncFunctionDef) else ""
|
|
200
|
+
|
|
201
|
+
# Get signature
|
|
202
|
+
args = []
|
|
203
|
+
for arg in node.args.args:
|
|
204
|
+
type_hint = ""
|
|
205
|
+
if arg.annotation:
|
|
206
|
+
try:
|
|
207
|
+
type_hint = f": {ast.unparse(arg.annotation)}"
|
|
208
|
+
except Exception:
|
|
209
|
+
pass
|
|
210
|
+
args.append(f"{arg.arg}{type_hint}")
|
|
211
|
+
|
|
212
|
+
ret_type = ""
|
|
213
|
+
if node.returns:
|
|
214
|
+
try:
|
|
215
|
+
ret_type = f" -> {ast.unparse(node.returns)}"
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
func_sig = f"{async_prefix}def {node.name}({', '.join(args)}){ret_type}"
|
|
220
|
+
func_content = f"{func_sig}:\n"
|
|
221
|
+
if docstring:
|
|
222
|
+
func_content += f' """{docstring}"""\n'
|
|
223
|
+
|
|
224
|
+
entries.append(CodeEntry(
|
|
225
|
+
content=func_content,
|
|
226
|
+
file_path=str(file_path),
|
|
227
|
+
language="python",
|
|
228
|
+
symbol_name=node.name,
|
|
229
|
+
symbol_type="function",
|
|
230
|
+
line_number=node.lineno,
|
|
231
|
+
docstring=docstring,
|
|
232
|
+
))
|
|
233
|
+
|
|
234
|
+
return entries
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def index_typescript_file(file_path: Path, content: str) -> list[CodeEntry]:
|
|
238
|
+
"""Extract code entries from a TypeScript/JavaScript file."""
|
|
239
|
+
entries = []
|
|
240
|
+
lines = content.split("\n")
|
|
241
|
+
|
|
242
|
+
# Patterns for different constructs
|
|
243
|
+
patterns = [
|
|
244
|
+
# Exported functions
|
|
245
|
+
(r'export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)\s*(?:<[^>]+>)?\s*\(([^)]*)\)(?:\s*:\s*([^\{]+))?',
|
|
246
|
+
"function"),
|
|
247
|
+
# Arrow function exports
|
|
248
|
+
(r'export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*[^=]+)?\s*=>',
|
|
249
|
+
"function"),
|
|
250
|
+
# Class exports
|
|
251
|
+
(r'export\s+(?:default\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([^{]+))?',
|
|
252
|
+
"class"),
|
|
253
|
+
# Interface exports
|
|
254
|
+
(r'export\s+(?:default\s+)?interface\s+(\w+)(?:<[^>]+>)?(?:\s+extends\s+([^{]+))?',
|
|
255
|
+
"interface"),
|
|
256
|
+
# Type exports
|
|
257
|
+
(r'export\s+type\s+(\w+)(?:<[^>]+>)?\s*=',
|
|
258
|
+
"type"),
|
|
259
|
+
# Const exports (useful for config objects, composables, etc.)
|
|
260
|
+
(r'export\s+const\s+(\w+)\s*(?::\s*([^=]+))?\s*=\s*(?!.*=>)',
|
|
261
|
+
"constant"),
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
for i, line in enumerate(lines):
|
|
265
|
+
for pattern, symbol_type in patterns:
|
|
266
|
+
match = re.match(pattern, line.strip())
|
|
267
|
+
if match:
|
|
268
|
+
symbol_name = match.group(1)
|
|
269
|
+
|
|
270
|
+
# Get context (a few lines around the definition)
|
|
271
|
+
start = max(0, i - 1)
|
|
272
|
+
end = min(len(lines), i + 10)
|
|
273
|
+
context_lines = lines[start:end]
|
|
274
|
+
|
|
275
|
+
# Extract JSDoc if present
|
|
276
|
+
jsdoc = ""
|
|
277
|
+
if i > 0 and lines[i - 1].strip().endswith("*/"):
|
|
278
|
+
# Look backward for JSDoc start
|
|
279
|
+
for j in range(i - 1, max(0, i - 20), -1):
|
|
280
|
+
if "/**" in lines[j]:
|
|
281
|
+
jsdoc_lines = lines[j:i]
|
|
282
|
+
jsdoc = "\n".join(jsdoc_lines)
|
|
283
|
+
break
|
|
284
|
+
|
|
285
|
+
entries.append(CodeEntry(
|
|
286
|
+
content="\n".join(context_lines),
|
|
287
|
+
file_path=str(file_path),
|
|
288
|
+
language="typescript" if file_path.suffix in [".ts", ".tsx"] else "javascript",
|
|
289
|
+
symbol_name=symbol_name,
|
|
290
|
+
symbol_type=symbol_type,
|
|
291
|
+
line_number=i + 1,
|
|
292
|
+
docstring=jsdoc if jsdoc else None,
|
|
293
|
+
))
|
|
294
|
+
break
|
|
295
|
+
|
|
296
|
+
# Also look for Vue composables pattern (useXxx functions)
|
|
297
|
+
composable_pattern = r'(?:export\s+)?(?:const|function)\s+(use[A-Z]\w*)'
|
|
298
|
+
for i, line in enumerate(lines):
|
|
299
|
+
match = re.search(composable_pattern, line)
|
|
300
|
+
if match:
|
|
301
|
+
symbol_name = match.group(1)
|
|
302
|
+
# Check if we already indexed this
|
|
303
|
+
if not any(e.symbol_name == symbol_name for e in entries):
|
|
304
|
+
start = max(0, i - 1)
|
|
305
|
+
end = min(len(lines), i + 15)
|
|
306
|
+
|
|
307
|
+
entries.append(CodeEntry(
|
|
308
|
+
content="\n".join(lines[start:end]),
|
|
309
|
+
file_path=str(file_path),
|
|
310
|
+
language="typescript" if file_path.suffix in [".ts", ".tsx"] else "javascript",
|
|
311
|
+
symbol_name=symbol_name,
|
|
312
|
+
symbol_type="composable",
|
|
313
|
+
line_number=i + 1,
|
|
314
|
+
))
|
|
315
|
+
|
|
316
|
+
return entries
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def index_vue_file(file_path: Path, content: str) -> list[CodeEntry]:
|
|
320
|
+
"""Extract code entries from a Vue SFC file."""
|
|
321
|
+
entries = []
|
|
322
|
+
|
|
323
|
+
# Get component name from filename
|
|
324
|
+
component_name = file_path.stem
|
|
325
|
+
|
|
326
|
+
# Extract script section
|
|
327
|
+
script_match = re.search(
|
|
328
|
+
r'<script[^>]*(?:setup)?[^>]*>(.*?)</script>',
|
|
329
|
+
content,
|
|
330
|
+
re.DOTALL | re.IGNORECASE
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
script_content = script_match.group(1) if script_match else ""
|
|
334
|
+
|
|
335
|
+
# Add the component itself
|
|
336
|
+
entries.append(CodeEntry(
|
|
337
|
+
content=f"Vue Component: {component_name}\n\n{script_content[:500]}",
|
|
338
|
+
file_path=str(file_path),
|
|
339
|
+
language="vue",
|
|
340
|
+
symbol_name=component_name,
|
|
341
|
+
symbol_type="component",
|
|
342
|
+
line_number=1,
|
|
343
|
+
))
|
|
344
|
+
|
|
345
|
+
# If there's script content, parse it for composables and functions
|
|
346
|
+
if script_content:
|
|
347
|
+
# Look for composable usage (useXxx calls)
|
|
348
|
+
composable_usages = re.findall(r'(use[A-Z]\w*)\s*\(', script_content)
|
|
349
|
+
for composable in set(composable_usages):
|
|
350
|
+
# Find the line
|
|
351
|
+
for i, line in enumerate(content.split("\n")):
|
|
352
|
+
if composable in line:
|
|
353
|
+
entries.append(CodeEntry(
|
|
354
|
+
content=f"Uses composable: {composable}\n{line.strip()}",
|
|
355
|
+
file_path=str(file_path),
|
|
356
|
+
language="vue",
|
|
357
|
+
symbol_name=f"{component_name}:{composable}",
|
|
358
|
+
symbol_type="composable_usage",
|
|
359
|
+
line_number=i + 1,
|
|
360
|
+
))
|
|
361
|
+
break
|
|
362
|
+
|
|
363
|
+
# Parse script for functions
|
|
364
|
+
ts_entries = index_typescript_file(file_path, script_content)
|
|
365
|
+
for entry in ts_entries:
|
|
366
|
+
entry.language = "vue"
|
|
367
|
+
entry.symbol_name = f"{component_name}.{entry.symbol_name}"
|
|
368
|
+
entries.append(entry)
|
|
369
|
+
|
|
370
|
+
return entries
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def index_dart_file(file_path: Path, content: str) -> list[CodeEntry]:
|
|
374
|
+
"""Extract code entries from a Dart file."""
|
|
375
|
+
entries = []
|
|
376
|
+
lines = content.split("\n")
|
|
377
|
+
|
|
378
|
+
# Patterns for Dart constructs
|
|
379
|
+
patterns = [
|
|
380
|
+
# Class definitions
|
|
381
|
+
(r'(?:abstract\s+)?class\s+(\w+)(?:<[^>]+>)?(?:\s+extends\s+(\w+))?(?:\s+with\s+([^{]+))?(?:\s+implements\s+([^{]+))?',
|
|
382
|
+
"class"),
|
|
383
|
+
# Function definitions
|
|
384
|
+
(r'(?:Future<[^>]+>|void|int|String|bool|double|dynamic|\w+)\s+(\w+)\s*(?:<[^>]+>)?\s*\(',
|
|
385
|
+
"function"),
|
|
386
|
+
# Mixins
|
|
387
|
+
(r'mixin\s+(\w+)(?:\s+on\s+(\w+))?',
|
|
388
|
+
"mixin"),
|
|
389
|
+
# Extensions
|
|
390
|
+
(r'extension\s+(\w+)\s+on\s+(\w+)',
|
|
391
|
+
"extension"),
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
for i, line in enumerate(lines):
|
|
395
|
+
for pattern, symbol_type in patterns:
|
|
396
|
+
match = re.match(r'\s*' + pattern, line)
|
|
397
|
+
if match:
|
|
398
|
+
symbol_name = match.group(1)
|
|
399
|
+
|
|
400
|
+
# Get context
|
|
401
|
+
start = max(0, i - 1)
|
|
402
|
+
end = min(len(lines), i + 10)
|
|
403
|
+
|
|
404
|
+
# Extract doc comment if present
|
|
405
|
+
doc_comment = ""
|
|
406
|
+
if i > 0:
|
|
407
|
+
for j in range(i - 1, max(0, i - 20), -1):
|
|
408
|
+
if lines[j].strip().startswith("///"):
|
|
409
|
+
doc_comment = lines[j].strip() + "\n" + doc_comment
|
|
410
|
+
elif lines[j].strip():
|
|
411
|
+
break
|
|
412
|
+
|
|
413
|
+
entries.append(CodeEntry(
|
|
414
|
+
content="\n".join(lines[start:end]),
|
|
415
|
+
file_path=str(file_path),
|
|
416
|
+
language="dart",
|
|
417
|
+
symbol_name=symbol_name,
|
|
418
|
+
symbol_type=symbol_type,
|
|
419
|
+
line_number=i + 1,
|
|
420
|
+
docstring=doc_comment if doc_comment else None,
|
|
421
|
+
))
|
|
422
|
+
break
|
|
423
|
+
|
|
424
|
+
return entries
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def index_file(file_path: Path) -> list[CodeEntry]:
|
|
428
|
+
"""
|
|
429
|
+
Parse a single code file into CodeEntry objects.
|
|
430
|
+
|
|
431
|
+
Returns empty list if file can't be parsed.
|
|
432
|
+
"""
|
|
433
|
+
try:
|
|
434
|
+
content = file_path.read_text(encoding='utf-8')
|
|
435
|
+
except (IOError, UnicodeDecodeError):
|
|
436
|
+
return []
|
|
437
|
+
|
|
438
|
+
# Skip empty files
|
|
439
|
+
if not content.strip():
|
|
440
|
+
return []
|
|
441
|
+
|
|
442
|
+
suffix = file_path.suffix.lower()
|
|
443
|
+
|
|
444
|
+
if suffix == ".py":
|
|
445
|
+
return index_python_file(file_path, content)
|
|
446
|
+
elif suffix in [".ts", ".tsx", ".js", ".jsx"]:
|
|
447
|
+
return index_typescript_file(file_path, content)
|
|
448
|
+
elif suffix == ".vue":
|
|
449
|
+
return index_vue_file(file_path, content)
|
|
450
|
+
elif suffix == ".dart":
|
|
451
|
+
return index_dart_file(file_path, content)
|
|
452
|
+
|
|
453
|
+
return []
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def index_directory(
|
|
457
|
+
root: Path,
|
|
458
|
+
languages: list[str],
|
|
459
|
+
exclude: list[str] | None = None,
|
|
460
|
+
) -> list[CodeEntry]:
|
|
461
|
+
"""
|
|
462
|
+
Index all code files in a directory.
|
|
463
|
+
|
|
464
|
+
Returns list of CodeEntry objects ready for vector DB.
|
|
465
|
+
"""
|
|
466
|
+
files = discover_code_files(root, languages, exclude)
|
|
467
|
+
entries = []
|
|
468
|
+
|
|
469
|
+
for file_path in files:
|
|
470
|
+
file_entries = index_file(file_path)
|
|
471
|
+
entries.extend(file_entries)
|
|
472
|
+
|
|
473
|
+
return entries
|
src/mcp_server.py
CHANGED
src/memory.py
CHANGED
|
@@ -191,7 +191,12 @@ class MemoryStore:
|
|
|
191
191
|
return None
|
|
192
192
|
|
|
193
193
|
metadata = results["metadatas"][0]
|
|
194
|
-
|
|
194
|
+
file_rel_path = metadata.get("file", "")
|
|
195
|
+
|
|
196
|
+
if not file_rel_path:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
file_path = self.memory_dir / file_rel_path
|
|
195
200
|
|
|
196
201
|
if file_path.exists():
|
|
197
202
|
return Memory.from_file(file_path)
|