ragtime-cli 0.2.7__tar.gz → 0.2.8__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 (30) hide show
  1. {ragtime_cli-0.2.7/ragtime_cli.egg-info → ragtime_cli-0.2.8}/PKG-INFO +1 -1
  2. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/pyproject.toml +1 -1
  3. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8/ragtime_cli.egg-info}/PKG-INFO +1 -1
  4. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/cli.py +29 -9
  5. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/db.py +22 -11
  6. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/indexers/code.py +19 -3
  7. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/mcp_server.py +13 -3
  8. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/memory.py +30 -9
  9. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/LICENSE +0 -0
  10. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/README.md +0 -0
  11. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/ragtime_cli.egg-info/SOURCES.txt +0 -0
  12. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/ragtime_cli.egg-info/dependency_links.txt +0 -0
  13. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/ragtime_cli.egg-info/entry_points.txt +0 -0
  14. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/ragtime_cli.egg-info/requires.txt +0 -0
  15. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/ragtime_cli.egg-info/top_level.txt +0 -0
  16. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/setup.cfg +0 -0
  17. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/__init__.py +0 -0
  18. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/commands/audit.md +0 -0
  19. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/commands/create-pr.md +0 -0
  20. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/commands/generate-docs.md +0 -0
  21. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/commands/handoff.md +0 -0
  22. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/commands/import-docs.md +0 -0
  23. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/commands/pr-graduate.md +0 -0
  24. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/commands/recall.md +0 -0
  25. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/commands/remember.md +0 -0
  26. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/commands/save.md +0 -0
  27. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/commands/start.md +0 -0
  28. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/config.py +0 -0
  29. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/indexers/__init__.py +0 -0
  30. {ragtime_cli-0.2.7 → ragtime_cli-0.2.8}/src/indexers/docs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ragtime-cli
3
- Version: 0.2.7
3
+ Version: 0.2.8
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ragtime-cli"
3
- version = "0.2.7"
3
+ version = "0.2.8"
4
4
  description = "Local-first memory and RAG system for Claude Code - semantic search over code, docs, and team knowledge"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ragtime-cli
3
- Version: 0.2.7
3
+ Version: 0.2.8
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
@@ -75,6 +75,7 @@ def check_ghp_installed() -> bool:
75
75
 
76
76
  def get_issue_from_ghp(issue_num: int, path: Path) -> dict | None:
77
77
  """Get issue details using ghp issue open."""
78
+ import json
78
79
  try:
79
80
  result = subprocess.run(
80
81
  ["ghp", "issue", "open", str(issue_num), "--json"],
@@ -84,15 +85,15 @@ def get_issue_from_ghp(issue_num: int, path: Path) -> dict | None:
84
85
  timeout=30,
85
86
  )
86
87
  if result.returncode == 0:
87
- import json
88
88
  return json.loads(result.stdout)
89
- except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
89
+ except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError):
90
90
  pass
91
91
  return None
92
92
 
93
93
 
94
94
  def get_issue_from_gh(issue_num: int, path: Path) -> dict | None:
95
95
  """Get issue details using gh CLI."""
96
+ import json
96
97
  try:
97
98
  result = subprocess.run(
98
99
  ["gh", "issue", "view", str(issue_num), "--json", "title,body,labels,number"],
@@ -102,9 +103,8 @@ def get_issue_from_gh(issue_num: int, path: Path) -> dict | None:
102
103
  timeout=30,
103
104
  )
104
105
  if result.returncode == 0:
105
- import json
106
106
  return json.loads(result.stdout)
107
- except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
107
+ except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError):
108
108
  pass
109
109
  return None
110
110
 
@@ -169,7 +169,7 @@ def get_remote_branches_with_ragtime(path: Path) -> list[str]:
169
169
 
170
170
 
171
171
  @click.group()
172
- @click.version_option(version="0.2.7")
172
+ @click.version_option(version="0.2.8")
173
173
  def main():
174
174
  """Ragtime - semantic search over code and documentation."""
175
175
  pass
@@ -1254,7 +1254,6 @@ def daemon_start(path: Path, interval: str):
1254
1254
  pid_file.write_text(str(os.getpid()))
1255
1255
 
1256
1256
  # Redirect output to log file
1257
- # Note: log_fd is intentionally kept open for the lifetime of the daemon
1258
1257
  log_fd = open(log_file, "a")
1259
1258
  os.dup2(log_fd.fileno(), sys.stdout.fileno())
1260
1259
  os.dup2(log_fd.fileno(), sys.stderr.fileno())
@@ -1262,9 +1261,20 @@ def daemon_start(path: Path, interval: str):
1262
1261
  import time
1263
1262
  from datetime import datetime
1264
1263
 
1264
+ # Set up signal handler for clean shutdown
1265
+ running = True
1266
+
1267
+ def handle_shutdown(signum, frame):
1268
+ nonlocal running
1269
+ running = False
1270
+ print(f"\n[{datetime.now().isoformat()}] Received signal {signum}, shutting down...")
1271
+
1272
+ signal.signal(signal.SIGTERM, handle_shutdown)
1273
+ signal.signal(signal.SIGINT, handle_shutdown)
1274
+
1265
1275
  print(f"\n[{datetime.now().isoformat()}] Daemon started (interval: {interval})")
1266
1276
 
1267
- while True:
1277
+ while running:
1268
1278
  try:
1269
1279
  print(f"[{datetime.now().isoformat()}] Running sync...")
1270
1280
 
@@ -1287,7 +1297,17 @@ def daemon_start(path: Path, interval: str):
1287
1297
  except Exception as e:
1288
1298
  print(f"[{datetime.now().isoformat()}] Error: {e}")
1289
1299
 
1290
- time.sleep(interval_seconds)
1300
+ # Sleep in small increments to respond to signals faster
1301
+ for _ in range(interval_seconds):
1302
+ if not running:
1303
+ break
1304
+ time.sleep(1)
1305
+
1306
+ # Clean up
1307
+ print(f"[{datetime.now().isoformat()}] Daemon stopped")
1308
+ log_fd.close()
1309
+ if pid_file.exists():
1310
+ pid_file.unlink()
1291
1311
 
1292
1312
 
1293
1313
  @daemon.command("stop")
@@ -2117,7 +2137,7 @@ def update(check: bool):
2117
2137
  from urllib.request import urlopen
2118
2138
  from urllib.error import URLError
2119
2139
 
2120
- current = "0.2.7"
2140
+ current = "0.2.8"
2121
2141
 
2122
2142
  click.echo(f"Current version: {current}")
2123
2143
  click.echo("Checking PyPI for updates...")
@@ -154,17 +154,28 @@ class RagtimeDB:
154
154
 
155
155
  def stats(self) -> dict:
156
156
  """Get index statistics."""
157
- count = self.collection.count()
158
-
159
- # Count by type
160
- docs_count = len(self.collection.get(where={"type": "docs"})["ids"])
161
- code_count = len(self.collection.get(where={"type": "code"})["ids"])
162
-
163
- return {
164
- "total": count,
165
- "docs": docs_count,
166
- "code": code_count,
167
- }
157
+ try:
158
+ count = self.collection.count()
159
+
160
+ # Count by type - only retrieve IDs, not full documents
161
+ docs_result = self.collection.get(where={"type": "docs"}, include=[])
162
+ code_result = self.collection.get(where={"type": "code"}, include=[])
163
+
164
+ docs_count = len(docs_result["ids"])
165
+ code_count = len(code_result["ids"])
166
+
167
+ return {
168
+ "total": count,
169
+ "docs": docs_count,
170
+ "code": code_count,
171
+ }
172
+ except Exception:
173
+ # Return zeros if collection is corrupted or unavailable
174
+ return {
175
+ "total": 0,
176
+ "docs": 0,
177
+ "code": 0,
178
+ }
168
179
 
169
180
  def get_indexed_files(self, type_filter: str | None = None) -> dict[str, float]:
170
181
  """
@@ -249,23 +249,38 @@ def index_typescript_file(file_path: Path, content: str) -> list[CodeEntry]:
249
249
  entries = []
250
250
  lines = content.split("\n")
251
251
 
252
- # Patterns for different constructs
252
+ # Patterns for different constructs (exported and non-exported)
253
253
  patterns = [
254
254
  # Exported functions
255
255
  (r'export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)\s*(?:<[^>]+>)?\s*\(([^)]*)\)(?:\s*:\s*([^\{]+))?',
256
256
  "function"),
257
+ # Non-exported functions (top-level, not inside class/object)
258
+ (r'^(?:async\s+)?function\s+(\w+)\s*(?:<[^>]+>)?\s*\(',
259
+ "function"),
257
260
  # Arrow function exports
258
261
  (r'export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*[^=]+)?\s*=>',
259
262
  "function"),
263
+ # Non-exported arrow functions (top-level const)
264
+ (r'^const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*[^=]+)?\s*=>',
265
+ "function"),
260
266
  # Class exports
261
267
  (r'export\s+(?:default\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([^{]+))?',
262
268
  "class"),
269
+ # Non-exported classes
270
+ (r'^class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([^{]+))?',
271
+ "class"),
263
272
  # Interface exports
264
273
  (r'export\s+(?:default\s+)?interface\s+(\w+)(?:<[^>]+>)?(?:\s+extends\s+([^{]+))?',
265
274
  "interface"),
275
+ # Non-exported interfaces
276
+ (r'^interface\s+(\w+)(?:<[^>]+>)?(?:\s+extends\s+([^{]+))?',
277
+ "interface"),
266
278
  # Type exports
267
279
  (r'export\s+type\s+(\w+)(?:<[^>]+>)?\s*=',
268
280
  "type"),
281
+ # Non-exported types
282
+ (r'^type\s+(\w+)(?:<[^>]+>)?\s*=',
283
+ "type"),
269
284
  # Const exports (useful for config objects, composables, etc.)
270
285
  (r'export\s+const\s+(\w+)\s*(?::\s*([^=]+))?\s*=\s*(?!.*=>)',
271
286
  "constant"),
@@ -390,8 +405,9 @@ def index_dart_file(file_path: Path, content: str) -> list[CodeEntry]:
390
405
  # Class definitions
391
406
  (r'(?:abstract\s+)?class\s+(\w+)(?:<[^>]+>)?(?:\s+extends\s+(\w+))?(?:\s+with\s+([^{]+))?(?:\s+implements\s+([^{]+))?',
392
407
  "class"),
393
- # Function definitions
394
- (r'(?:Future<[^>]+>|void|int|String|bool|double|dynamic|\w+)\s+(\w+)\s*(?:<[^>]+>)?\s*\(',
408
+ # Top-level function definitions - explicit return types to avoid matching variable declarations
409
+ # The re.match already anchors at start, \s* is prepended in loop
410
+ (r'(?:Future<[^>]+>|Stream<[^>]+>|void|int|String|bool|double|dynamic|List<[^>]+>|Map<[^>]+>|Set<[^>]+>)\s+(\w+)\s*(?:<[^>]+>)?\s*\(',
395
411
  "function"),
396
412
  # Mixins
397
413
  (r'mixin\s+(\w+)(?:\s+on\s+(\w+))?',
@@ -487,7 +487,7 @@ class RagtimeMCPServer:
487
487
  "protocolVersion": "2024-11-05",
488
488
  "serverInfo": {
489
489
  "name": "ragtime",
490
- "version": "0.2.6",
490
+ "version": "0.2.8",
491
491
  },
492
492
  "capabilities": {
493
493
  "tools": {},
@@ -563,8 +563,18 @@ class RagtimeMCPServer:
563
563
  sys.stdout.write(json.dumps(response) + "\n")
564
564
  sys.stdout.flush()
565
565
 
566
- except json.JSONDecodeError:
567
- continue
566
+ except json.JSONDecodeError as e:
567
+ # Log error and send JSON-RPC error response
568
+ error_response = {
569
+ "jsonrpc": "2.0",
570
+ "id": None,
571
+ "error": {
572
+ "code": -32700,
573
+ "message": f"Parse error: {e}",
574
+ },
575
+ }
576
+ sys.stdout.write(json.dumps(error_response) + "\n")
577
+ sys.stdout.flush()
568
578
  except KeyboardInterrupt:
569
579
  break
570
580
 
@@ -80,18 +80,23 @@ class Memory:
80
80
 
81
81
  if self.namespace == "app":
82
82
  if self.component:
83
- return f"app/{self.component}/{self.id}-{slug}.md"
83
+ # Sanitize component to prevent path traversal
84
+ safe_component = self._slugify(self.component)
85
+ return f"app/{safe_component}/{self.id}-{slug}.md"
84
86
  return f"app/{self.id}-{slug}.md"
85
87
  elif self.namespace == "team":
86
88
  return f"team/{self.id}-{slug}.md"
87
89
  elif self.namespace.startswith("user-"):
88
- username = self.namespace.replace("user-", "")
90
+ # Sanitize username to prevent path traversal
91
+ username = self._slugify(self.namespace.replace("user-", ""))
89
92
  return f"users/{username}/{self.id}-{slug}.md"
90
93
  elif self.namespace.startswith("branch-"):
91
94
  branch_slug = self._slugify(self.namespace.replace("branch-", ""))
92
95
  return f"branches/{branch_slug}/{self.id}-{slug}.md"
93
96
  else:
94
- return f"other/{self.namespace}/{self.id}-{slug}.md"
97
+ # Sanitize namespace to prevent path traversal
98
+ safe_namespace = self._slugify(self.namespace)
99
+ return f"other/{safe_namespace}/{self.id}-{slug}.md"
95
100
 
96
101
  @staticmethod
97
102
  def _slugify(text: str) -> str:
@@ -376,9 +381,25 @@ class MemoryStore:
376
381
 
377
382
  def _cleanup_empty_dirs(self, dir_path: Path) -> None:
378
383
  """Remove empty directories up to memory_dir."""
379
- while dir_path != self.memory_dir and dir_path.exists():
380
- if not any(dir_path.iterdir()):
381
- dir_path.rmdir()
382
- dir_path = dir_path.parent
383
- else:
384
- break
384
+ # Resolve paths to handle symlinks and ensure we stay within bounds
385
+ try:
386
+ dir_path = dir_path.resolve()
387
+ memory_dir_resolved = self.memory_dir.resolve()
388
+ except OSError:
389
+ return # Can't resolve paths, bail out safely
390
+
391
+ # Ensure dir_path is actually under memory_dir
392
+ try:
393
+ dir_path.relative_to(memory_dir_resolved)
394
+ except ValueError:
395
+ return # dir_path is not under memory_dir, bail out
396
+
397
+ while dir_path != memory_dir_resolved and dir_path.exists():
398
+ try:
399
+ if not any(dir_path.iterdir()):
400
+ dir_path.rmdir()
401
+ dir_path = dir_path.parent.resolve()
402
+ else:
403
+ break
404
+ except OSError:
405
+ break # Permission error or other issue, stop cleanup
File without changes
File without changes
File without changes
File without changes
File without changes