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.

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
@@ -487,7 +487,7 @@ class RagtimeMCPServer:
487
487
  "protocolVersion": "2024-11-05",
488
488
  "serverInfo": {
489
489
  "name": "ragtime",
490
- "version": "0.1.0",
490
+ "version": "0.2.5",
491
491
  },
492
492
  "capabilities": {
493
493
  "tools": {},
src/memory.py CHANGED
@@ -191,7 +191,12 @@ class MemoryStore:
191
191
  return None
192
192
 
193
193
  metadata = results["metadatas"][0]
194
- file_path = self.memory_dir / metadata.get("file", "")
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)