ragtime-cli 0.2.4__py3-none-any.whl → 0.2.6__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ragtime-cli
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Local-first memory and RAG system for Claude Code - semantic search over code, docs, and team knowledge
5
5
  Author-email: Bret Martineau <bretwardjames@gmail.com>
6
6
  License-Expression: MIT
@@ -35,7 +35,8 @@ Local-first memory and RAG system for Claude Code. Semantic search over code, do
35
35
  ## Features
36
36
 
37
37
  - **Memory Storage**: Store structured knowledge with namespaces, types, and metadata
38
- - **Semantic Search**: Query memories and docs with natural language
38
+ - **Semantic Search**: Query memories, docs, and code with natural language
39
+ - **Code Indexing**: Index functions, classes, and composables from Python, TypeScript, Vue, and Dart
39
40
  - **Cross-Branch Sync**: Share context with teammates before PRs merge
40
41
  - **Convention Checking**: Verify code follows team standards before PRs
41
42
  - **Doc Generation**: Generate documentation from code (stubs or AI-powered)
@@ -96,11 +97,26 @@ ragtime forget <memory-id>
96
97
  ### Search & Indexing
97
98
 
98
99
  ```bash
99
- # Index docs
100
+ # Index everything (docs + code)
101
+ ragtime index
102
+
103
+ # Index only docs
100
104
  ragtime index --type docs
101
105
 
102
- # Semantic search
103
- ragtime search "how does auth work" --namespace app --limit 10
106
+ # Index only code (functions, classes, composables)
107
+ ragtime index --type code
108
+
109
+ # Re-index with clear (removes old entries)
110
+ ragtime index --clear
111
+
112
+ # Semantic search across all content
113
+ ragtime search "how does auth work" --limit 10
114
+
115
+ # Search only code
116
+ ragtime search "useAsyncState" --type code
117
+
118
+ # Search only docs
119
+ ragtime search "authentication" --type docs --namespace app
104
120
 
105
121
  # Reindex memory files
106
122
  ragtime reindex
@@ -220,13 +236,42 @@ docs:
220
236
 
221
237
  code:
222
238
  paths: ["."]
223
- languages: ["python", "typescript", "dart"]
239
+ languages: ["python", "typescript", "javascript", "vue", "dart"]
240
+ exclude: ["**/node_modules/**", "**/build/**", "**/dist/**"]
224
241
 
225
242
  conventions:
226
243
  files: [".ragtime/CONVENTIONS.md"]
227
244
  also_search_memories: true
228
245
  ```
229
246
 
247
+ ## Code Indexing
248
+
249
+ The code indexer extracts meaningful symbols from your codebase:
250
+
251
+ | Language | What Gets Indexed |
252
+ |----------|-------------------|
253
+ | Python | Classes, methods, functions (with docstrings) |
254
+ | TypeScript/JS | Exported functions, classes, interfaces, types, constants |
255
+ | Vue | Components, composable usage (useXxx calls) |
256
+ | Dart | Classes, functions, mixins, extensions |
257
+
258
+ Each symbol is indexed with:
259
+ - **content**: The code snippet with signature and docstring
260
+ - **file**: Full path to the source file
261
+ - **line**: Line number for quick navigation
262
+ - **symbol_name**: Searchable name (e.g., `useAsyncState`, `JWTManager.validate`)
263
+ - **symbol_type**: `function`, `class`, `method`, `interface`, `composable`, etc.
264
+
265
+ Example search results:
266
+ ```
267
+ ragtime search "useAsyncState" --type code
268
+
269
+ [1] /apps/web/components/agency/payers.vue
270
+ Type: code | Symbol: payers:useAsyncState
271
+ Score: 0.892
272
+ Uses composable: useAsyncState...
273
+ ```
274
+
230
275
  ## Memory Format
231
276
 
232
277
  Memories are markdown files with YAML frontmatter:
@@ -1,10 +1,10 @@
1
- ragtime_cli-0.2.4.dist-info/licenses/LICENSE,sha256=9A0wJs2PRDciGRH4F8JUJ-aMKYQyq_gVu2ixrXs-l5A,1070
1
+ ragtime_cli-0.2.6.dist-info/licenses/LICENSE,sha256=9A0wJs2PRDciGRH4F8JUJ-aMKYQyq_gVu2ixrXs-l5A,1070
2
2
  src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- src/cli.py,sha256=iNCYKD6w1gi64eNeb8BFj-xPzVnQLqHMzmPzoRGE3tI,67135
4
- src/config.py,sha256=kaJV-7yI-LMW16e1w6JvM1ZCjgPf8EiBBOM4OXio6I8,4045
3
+ src/cli.py,sha256=Hnf2cRcI-czCBV7CGyGMOUTI_ZqxuA8ZDK9R_msw540,70299
4
+ src/config.py,sha256=mlqRkq1gi_bINqqz6TqT4GAR0M0G_hyeo7oY2zr-xsE,4066
5
5
  src/db.py,sha256=BKrlhilXYHNaj-ZcffinSXVhdUqowmwpFPBx7aLxamU,4642
6
- src/mcp_server.py,sha256=Tx0i73GXO0YmcVrdO7UjRMS0auN8fBG2LOpHuf_LUC0,20374
7
- src/memory.py,sha256=POT2lYeBcEx4_MPbsIdet6ScwcjmuETz8Dxmz-Z_7IY,11939
6
+ src/mcp_server.py,sha256=uOixURyPK0sodt3OqZJJBlUHncas9PPC49N-OGGuSAg,20374
7
+ src/memory.py,sha256=8kuHBLDTsZdSBumgA9FRJhHp_VNeoV78QFmiDcJs7YI,12033
8
8
  src/commands/audit.md,sha256=Xkucm-gfBIMalK9wf7NBbyejpsqBTUAGGlb7GxMtMPY,5137
9
9
  src/commands/create-pr.md,sha256=u6-jVkDP_6bJQp6ImK039eY9F6B9E2KlAVlvLY-WV6Q,9483
10
10
  src/commands/generate-docs.md,sha256=9W2Yy-PDyC3p5k39uEb31z5YAHkSKsQLg6gV3tLgSnQ,7015
@@ -15,10 +15,11 @@ src/commands/recall.md,sha256=unQPWsmocKRoQR7jRtjrj8aVcMHverjGR6u5mYL8TLw,6008
15
15
  src/commands/remember.md,sha256=nNewsUhIqF4wtD1jhVDZvmLZjdcmPN6NmUM43SdWepc,5368
16
16
  src/commands/save.md,sha256=7gTpW46AU9Y4l8XVZ8f4h1sEdBfVqIRA7hlidUxMAC4,251
17
17
  src/commands/start.md,sha256=qoqhkMgET74DBx8YPIT1-wqCiVBUDxlmevigsCinHSY,6506
18
- src/indexers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ src/indexers/__init__.py,sha256=MYoCPZUpHakMX1s2vWnc9shjWfx_X1_0JzUhpKhnKUQ,454
19
+ src/indexers/code.py,sha256=f-D6NLgayPbD-8goVKI-vlsiHgyrzS8Mkmo1oRFrBII,16473
19
20
  src/indexers/docs.py,sha256=7FENHaKSvC1T557bRzvmrjyaG_vK94GuQG9XMZdr89w,3349
20
- ragtime_cli-0.2.4.dist-info/METADATA,sha256=VtuTNejyKDZqNrVuC5tJGNHWDHVrIY-2Pk1-k0NlwWY,8383
21
- ragtime_cli-0.2.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
22
- ragtime_cli-0.2.4.dist-info/entry_points.txt,sha256=cWLbeyMxZNbew-THS3bHXTpCRXt1EaUy5QUOXGXLjl4,75
23
- ragtime_cli-0.2.4.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
24
- ragtime_cli-0.2.4.dist-info/RECORD,,
21
+ ragtime_cli-0.2.6.dist-info/METADATA,sha256=fBoyzYYbWaK5RgBU_VGUXAxxhWIsQXozRIMTsFKjbBQ,9875
22
+ ragtime_cli-0.2.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
23
+ ragtime_cli-0.2.6.dist-info/entry_points.txt,sha256=cWLbeyMxZNbew-THS3bHXTpCRXt1EaUy5QUOXGXLjl4,75
24
+ ragtime_cli-0.2.6.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
25
+ ragtime_cli-0.2.6.dist-info/RECORD,,
src/cli.py CHANGED
@@ -11,7 +11,10 @@ import sys
11
11
 
12
12
  from .db import RagtimeDB
13
13
  from .config import RagtimeConfig, init_config
14
- from .indexers.docs import index_directory as index_docs
14
+ from .indexers import (
15
+ discover_docs, index_doc_file, DocEntry,
16
+ discover_code_files, index_code_file, CodeEntry,
17
+ )
15
18
  from .memory import Memory, MemoryStore
16
19
 
17
20
 
@@ -166,7 +169,7 @@ def get_remote_branches_with_ragtime(path: Path) -> list[str]:
166
169
 
167
170
 
168
171
  @click.group()
169
- @click.version_option(version="0.2.4")
172
+ @click.version_option(version="0.2.6")
170
173
  def main():
171
174
  """Ragtime - semantic search over code and documentation."""
172
175
  pass
@@ -273,34 +276,98 @@ def index(path: Path, index_type: str, clear: bool):
273
276
  db.clear(type_filter=index_type)
274
277
 
275
278
  if index_type in ("all", "docs"):
276
- total_entries = []
279
+ # Discover all doc files first
280
+ all_doc_files = []
277
281
  for docs_path in config.docs.paths:
278
282
  docs_root = path / docs_path
279
283
  if not docs_root.exists():
280
284
  click.echo(f" Docs path {docs_root} not found, skipping...")
281
285
  continue
282
- click.echo(f"Indexing docs in {docs_root}...")
283
- entries = index_docs(
286
+ files = discover_docs(
284
287
  docs_root,
285
288
  patterns=config.docs.patterns,
286
289
  exclude=config.docs.exclude,
287
290
  )
288
- total_entries.extend(entries)
289
-
290
- if total_entries:
291
- ids = [e.file_path for e in total_entries]
292
- documents = [e.content for e in total_entries]
293
- metadatas = [e.to_metadata() for e in total_entries]
294
- db.upsert(ids=ids, documents=documents, metadatas=metadatas)
295
- click.echo(f" Indexed {len(total_entries)} documents")
291
+ all_doc_files.extend(files)
292
+
293
+ if all_doc_files:
294
+ click.echo(f"Found {len(all_doc_files)} doc files")
295
+ total_entries = []
296
+ with click.progressbar(
297
+ all_doc_files,
298
+ label=" Processing",
299
+ show_percent=True,
300
+ show_pos=True,
301
+ item_show_func=lambda f: f.name[:30] if f else "",
302
+ ) as files:
303
+ for file_path in files:
304
+ entry = index_doc_file(file_path)
305
+ if entry:
306
+ total_entries.append(entry)
307
+
308
+ if total_entries:
309
+ ids = [e.file_path for e in total_entries]
310
+ documents = [e.content for e in total_entries]
311
+ metadatas = [e.to_metadata() for e in total_entries]
312
+ db.upsert(ids=ids, documents=documents, metadatas=metadatas)
313
+ click.echo(f" Indexed {len(total_entries)} documents")
314
+ else:
315
+ click.echo(" No valid documents found")
296
316
  else:
297
317
  click.echo(" No documents found")
298
318
 
299
319
  if index_type in ("all", "code"):
320
+ # Build exclusion list for code
300
321
  code_exclude = list(config.code.exclude)
301
322
  for docs_path in config.docs.paths:
302
323
  code_exclude.append(f"**/{docs_path}/**")
303
- click.echo("Code indexing not yet implemented")
324
+
325
+ # Discover all code files first
326
+ all_code_files = []
327
+ for code_path_str in config.code.paths:
328
+ code_root = path / code_path_str
329
+ if not code_root.exists():
330
+ click.echo(f" Code path {code_root} not found, skipping...")
331
+ continue
332
+ files = discover_code_files(
333
+ code_root,
334
+ languages=config.code.languages,
335
+ exclude=code_exclude,
336
+ )
337
+ all_code_files.extend(files)
338
+
339
+ if all_code_files:
340
+ click.echo(f"Found {len(all_code_files)} code files")
341
+ total_entries = []
342
+ with click.progressbar(
343
+ all_code_files,
344
+ label=" Processing",
345
+ show_percent=True,
346
+ show_pos=True,
347
+ item_show_func=lambda f: f.name[:30] if f else "",
348
+ ) as files:
349
+ for file_path in files:
350
+ file_entries = index_code_file(file_path)
351
+ total_entries.extend(file_entries)
352
+
353
+ if total_entries:
354
+ # Create unique IDs: file:line:symbol
355
+ ids = [f"{e.file_path}:{e.line_number}:{e.symbol_name}" for e in total_entries]
356
+ documents = [e.content for e in total_entries]
357
+ metadatas = [e.to_metadata() for e in total_entries]
358
+ db.upsert(ids=ids, documents=documents, metadatas=metadatas)
359
+ click.echo(f" Indexed {len(total_entries)} code symbols")
360
+
361
+ # Show breakdown by type
362
+ by_type = {}
363
+ for e in total_entries:
364
+ by_type[e.symbol_type] = by_type.get(e.symbol_type, 0) + 1
365
+ breakdown = ", ".join(f"{count} {typ}s" for typ, count in sorted(by_type.items()))
366
+ click.echo(f" ({breakdown})")
367
+ else:
368
+ click.echo(" No code symbols found")
369
+ else:
370
+ click.echo(" No code files found")
304
371
 
305
372
  stats = db.stats()
306
373
  click.echo(f"\nIndex stats: {stats['total']} total ({stats['docs']} docs, {stats['code']} code)")
@@ -1041,7 +1108,15 @@ def daemon_start(path: Path, interval: str):
1041
1108
 
1042
1109
  Runs git fetch && ragtime sync on an interval to keep
1043
1110
  remote branches synced automatically.
1111
+
1112
+ Note: This command requires Unix (Linux/macOS). On Windows, use Task Scheduler instead.
1044
1113
  """
1114
+ # Check for Windows - os.fork() is Unix-only
1115
+ if sys.platform == "win32":
1116
+ click.echo("✗ Daemon mode is not supported on Windows.", err=True)
1117
+ click.echo(" Use Windows Task Scheduler to run 'ragtime sync' periodically instead.")
1118
+ return
1119
+
1045
1120
  path = Path(path).resolve()
1046
1121
  pid_file = get_pid_file(path)
1047
1122
  log_file = get_log_file(path)
@@ -1088,6 +1163,7 @@ def daemon_start(path: Path, interval: str):
1088
1163
  pid_file.write_text(str(os.getpid()))
1089
1164
 
1090
1165
  # Redirect output to log file
1166
+ # Note: log_fd is intentionally kept open for the lifetime of the daemon
1091
1167
  log_fd = open(log_file, "a")
1092
1168
  os.dup2(log_fd.fileno(), sys.stdout.fileno())
1093
1169
  os.dup2(log_fd.fileno(), sys.stderr.fileno())
@@ -1652,7 +1728,7 @@ def get_function_params(node) -> list:
1652
1728
  if arg.annotation:
1653
1729
  try:
1654
1730
  type_hint = ast.unparse(arg.annotation)
1655
- except:
1731
+ except Exception:
1656
1732
  type_hint = "Any"
1657
1733
 
1658
1734
  default = "-"
@@ -1660,7 +1736,7 @@ def get_function_params(node) -> list:
1660
1736
  if default_idx >= 0 and default_idx < len(defaults):
1661
1737
  try:
1662
1738
  default = f"`{ast.unparse(defaults[default_idx])}`"
1663
- except:
1739
+ except Exception:
1664
1740
  default = "..."
1665
1741
 
1666
1742
  params.append({
@@ -1679,7 +1755,7 @@ def get_return_annotation(node) -> str:
1679
1755
  if node.returns:
1680
1756
  try:
1681
1757
  return ast.unparse(node.returns)
1682
- except:
1758
+ except Exception:
1683
1759
  return "Any"
1684
1760
  return "None"
1685
1761
 
@@ -1803,7 +1879,7 @@ def audit(docs_path: Path, path: Path, fix: bool, as_json: bool):
1803
1879
  parts = content.split("---", 2)
1804
1880
  if len(parts) >= 3:
1805
1881
  existing_meta = yaml.safe_load(parts[1]) or {}
1806
- except:
1882
+ except Exception:
1807
1883
  pass
1808
1884
 
1809
1885
  # Analyze file for suggestions
@@ -1950,7 +2026,7 @@ def update(check: bool):
1950
2026
  from urllib.request import urlopen
1951
2027
  from urllib.error import URLError
1952
2028
 
1953
- current = "0.2.4"
2029
+ current = "0.2.6"
1954
2030
 
1955
2031
  click.echo(f"Current version: {current}")
1956
2032
  click.echo("Checking PyPI for updates...")
src/config.py CHANGED
@@ -26,7 +26,7 @@ class DocsConfig:
26
26
  class CodeConfig:
27
27
  """Configuration for code indexing."""
28
28
  paths: list[str] = field(default_factory=lambda: ["."])
29
- languages: list[str] = field(default_factory=lambda: ["dart", "typescript", "python"])
29
+ languages: list[str] = field(default_factory=lambda: ["python", "typescript", "javascript", "vue", "dart"])
30
30
  exclude: list[str] = field(default_factory=lambda: [
31
31
  "**/node_modules/**",
32
32
  "**/.git/**",
src/indexers/__init__.py CHANGED
@@ -0,0 +1,11 @@
1
+ """Indexers for ragtime - parse different content types for vector search."""
2
+
3
+ from .docs import index_directory as index_docs, DocEntry, discover_docs, index_file as index_doc_file
4
+ from .code import index_directory as index_code, CodeEntry, discover_code_files, index_file as index_code_file
5
+
6
+ __all__ = [
7
+ "index_docs", "index_code",
8
+ "DocEntry", "CodeEntry",
9
+ "discover_docs", "discover_code_files",
10
+ "index_doc_file", "index_code_file",
11
+ ]
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.6",
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)