ragtime-cli 0.2.7__tar.gz → 0.2.9__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.
- {ragtime_cli-0.2.7/ragtime_cli.egg-info → ragtime_cli-0.2.9}/PKG-INFO +1 -1
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/pyproject.toml +1 -1
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9/ragtime_cli.egg-info}/PKG-INFO +1 -1
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/src/cli.py +29 -9
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/src/db.py +22 -11
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/src/indexers/code.py +19 -3
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/src/mcp_server.py +14 -4
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/src/memory.py +30 -9
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/LICENSE +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/README.md +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/ragtime_cli.egg-info/SOURCES.txt +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/ragtime_cli.egg-info/dependency_links.txt +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/ragtime_cli.egg-info/entry_points.txt +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/ragtime_cli.egg-info/requires.txt +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/ragtime_cli.egg-info/top_level.txt +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/setup.cfg +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/src/__init__.py +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/src/commands/audit.md +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/src/commands/create-pr.md +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/src/commands/generate-docs.md +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/src/commands/handoff.md +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/src/commands/import-docs.md +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/src/commands/pr-graduate.md +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/src/commands/recall.md +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/src/commands/remember.md +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/src/commands/save.md +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/src/commands/start.md +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/src/config.py +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/src/indexers/__init__.py +0 -0
- {ragtime_cli-0.2.7 → ragtime_cli-0.2.9}/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.
|
|
3
|
+
Version: 0.2.9
|
|
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
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ragtime-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
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,
|
|
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,
|
|
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.
|
|
172
|
+
@click.version_option(version="0.2.9")
|
|
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
|
|
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
|
-
|
|
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.
|
|
2140
|
+
current = "0.2.9"
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
"
|
|
165
|
-
"
|
|
166
|
-
|
|
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
|
-
#
|
|
394
|
-
|
|
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+))?',
|
|
@@ -132,7 +132,7 @@ class RagtimeMCPServer:
|
|
|
132
132
|
},
|
|
133
133
|
{
|
|
134
134
|
"name": "search",
|
|
135
|
-
"description": "Semantic search over indexed
|
|
135
|
+
"description": "Semantic search over indexed code and docs. Returns function signatures, class definitions, and doc summaries with file paths and line numbers. IMPORTANT: Results are summaries only - use the Read tool on returned file paths to see full implementations before making code changes or decisions.",
|
|
136
136
|
"inputSchema": {
|
|
137
137
|
"type": "object",
|
|
138
138
|
"properties": {
|
|
@@ -487,7 +487,7 @@ class RagtimeMCPServer:
|
|
|
487
487
|
"protocolVersion": "2024-11-05",
|
|
488
488
|
"serverInfo": {
|
|
489
489
|
"name": "ragtime",
|
|
490
|
-
"version": "0.2.
|
|
490
|
+
"version": "0.2.9",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|