ragtime-cli 0.2.10__py3-none-any.whl → 0.2.11__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.
- {ragtime_cli-0.2.10.dist-info → ragtime_cli-0.2.11.dist-info}/METADATA +21 -3
- {ragtime_cli-0.2.10.dist-info → ragtime_cli-0.2.11.dist-info}/RECORD +11 -11
- src/cli.py +10 -2
- src/config.py +4 -3
- src/db.py +23 -2
- src/mcp_server.py +9 -3
- src/memory.py +33 -14
- {ragtime_cli-0.2.10.dist-info → ragtime_cli-0.2.11.dist-info}/WHEEL +0 -0
- {ragtime_cli-0.2.10.dist-info → ragtime_cli-0.2.11.dist-info}/entry_points.txt +0 -0
- {ragtime_cli-0.2.10.dist-info → ragtime_cli-0.2.11.dist-info}/licenses/LICENSE +0 -0
- {ragtime_cli-0.2.10.dist-info → ragtime_cli-0.2.11.dist-info}/top_level.txt +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.11
|
|
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
|
|
@@ -121,6 +121,10 @@ ragtime search "useAsyncState" --type code
|
|
|
121
121
|
# Search only docs
|
|
122
122
|
ragtime search "authentication" --type docs --namespace app
|
|
123
123
|
|
|
124
|
+
# Hybrid search: semantic + keyword filtering
|
|
125
|
+
# Use -r/--require to ensure terms appear in results
|
|
126
|
+
ragtime search "error handling" -r mobile -r dart
|
|
127
|
+
|
|
124
128
|
# Reindex memory files
|
|
125
129
|
ragtime reindex
|
|
126
130
|
|
|
@@ -233,9 +237,9 @@ ragtime setup-ghp
|
|
|
233
237
|
|
|
234
238
|
```yaml
|
|
235
239
|
docs:
|
|
236
|
-
paths: ["docs"
|
|
240
|
+
paths: ["docs"]
|
|
237
241
|
patterns: ["**/*.md"]
|
|
238
|
-
exclude: ["**/node_modules/**"]
|
|
242
|
+
exclude: ["**/node_modules/**", "**/.ragtime/**"]
|
|
239
243
|
|
|
240
244
|
code:
|
|
241
245
|
paths: ["."]
|
|
@@ -259,6 +263,20 @@ This is intentional - embeddings work better on focused summaries than large cod
|
|
|
259
263
|
|
|
260
264
|
For Claude/MCP usage: The search tool description instructs Claude to read returned file paths for full implementations before making code changes.
|
|
261
265
|
|
|
266
|
+
### Hybrid Search
|
|
267
|
+
|
|
268
|
+
Semantic search can lose qualifiers - "error handling in mobile app" might return web app results because "error handling" dominates the embedding. Use `require_terms` to ensure specific words appear:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
# CLI
|
|
272
|
+
ragtime search "error handling" -r mobile -r dart
|
|
273
|
+
|
|
274
|
+
# MCP
|
|
275
|
+
search(query="error handling", require_terms=["mobile", "dart"])
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
This combines semantic similarity (finds conceptually related content) with keyword filtering (ensures qualifiers aren't ignored).
|
|
279
|
+
|
|
262
280
|
## Code Indexing
|
|
263
281
|
|
|
264
282
|
The code indexer extracts meaningful symbols from your codebase:
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
ragtime_cli-0.2.
|
|
1
|
+
ragtime_cli-0.2.11.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=
|
|
4
|
-
src/config.py,sha256=
|
|
5
|
-
src/db.py,sha256=
|
|
6
|
-
src/mcp_server.py,sha256=
|
|
7
|
-
src/memory.py,sha256=
|
|
3
|
+
src/cli.py,sha256=HDZNFg5shUU1s6JLi9Wn_TydEXx-92JCginJYgH3BlE,74375
|
|
4
|
+
src/config.py,sha256=tQ6gPLr4ksn2bJPIUjtELFr-k01Eg4g-LDo3GNE6P0Q,4600
|
|
5
|
+
src/db.py,sha256=ueSThFXkhI5MFwXICkNW3zqCawGDi3kqFQnbm4st_Ew,8186
|
|
6
|
+
src/mcp_server.py,sha256=SvkoGkBqoaZkW3KXiE5qHbbkTIjby94mcxMXgJKv8ik,21559
|
|
7
|
+
src/memory.py,sha256=lmDgC9AH24dog6dvbGgHt95TANUgdBZSnLcqM0isx10,15639
|
|
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
|
|
@@ -18,8 +18,8 @@ src/commands/start.md,sha256=qoqhkMgET74DBx8YPIT1-wqCiVBUDxlmevigsCinHSY,6506
|
|
|
18
18
|
src/indexers/__init__.py,sha256=MYoCPZUpHakMX1s2vWnc9shjWfx_X1_0JzUhpKhnKUQ,454
|
|
19
19
|
src/indexers/code.py,sha256=G2TbiKbWj0e7DV5KsU8-Ggw6ziDb4zTuZ4Bu3ryV4g8,18059
|
|
20
20
|
src/indexers/docs.py,sha256=nyewQ4Ug4SCuhne4TuLDlUDzz9GH2STInddj81ocz50,3555
|
|
21
|
-
ragtime_cli-0.2.
|
|
22
|
-
ragtime_cli-0.2.
|
|
23
|
-
ragtime_cli-0.2.
|
|
24
|
-
ragtime_cli-0.2.
|
|
25
|
-
ragtime_cli-0.2.
|
|
21
|
+
ragtime_cli-0.2.11.dist-info/METADATA,sha256=M0M_WyDQE5zDvpoMeo3RG18NJNRUw6IMOVqKh4mexVY,11269
|
|
22
|
+
ragtime_cli-0.2.11.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
23
|
+
ragtime_cli-0.2.11.dist-info/entry_points.txt,sha256=cWLbeyMxZNbew-THS3bHXTpCRXt1EaUy5QUOXGXLjl4,75
|
|
24
|
+
ragtime_cli-0.2.11.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
|
|
25
|
+
ragtime_cli-0.2.11.dist-info/RECORD,,
|
src/cli.py
CHANGED
|
@@ -469,12 +469,19 @@ def index(path: Path, index_type: str, clear: bool):
|
|
|
469
469
|
@click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
470
470
|
@click.option("--type", "type_filter", type=click.Choice(["all", "docs", "code"]), default="all")
|
|
471
471
|
@click.option("--namespace", "-n", help="Filter by namespace")
|
|
472
|
+
@click.option("--require", "-r", "require_terms", multiple=True,
|
|
473
|
+
help="Terms that MUST appear in results (repeatable)")
|
|
472
474
|
@click.option("--include-archive", is_flag=True, help="Also search archived branches")
|
|
473
475
|
@click.option("--limit", "-l", default=5, help="Max results")
|
|
474
476
|
@click.option("--verbose", "-v", is_flag=True, help="Show full content")
|
|
475
477
|
def search(query: str, path: Path, type_filter: str, namespace: str,
|
|
476
|
-
include_archive: bool, limit: int, verbose: bool):
|
|
477
|
-
"""
|
|
478
|
+
require_terms: tuple, include_archive: bool, limit: int, verbose: bool):
|
|
479
|
+
"""
|
|
480
|
+
Hybrid search: semantic similarity + keyword filtering.
|
|
481
|
+
|
|
482
|
+
Use --require/-r to ensure specific terms appear in results.
|
|
483
|
+
Example: ragtime search "error handling" -r mobile -r dart
|
|
484
|
+
"""
|
|
478
485
|
path = Path(path).resolve()
|
|
479
486
|
db = get_db(path)
|
|
480
487
|
|
|
@@ -485,6 +492,7 @@ def search(query: str, path: Path, type_filter: str, namespace: str,
|
|
|
485
492
|
limit=limit,
|
|
486
493
|
type_filter=type_arg,
|
|
487
494
|
namespace=namespace,
|
|
495
|
+
require_terms=list(require_terms) if require_terms else None,
|
|
488
496
|
)
|
|
489
497
|
|
|
490
498
|
if not results:
|
src/config.py
CHANGED
|
@@ -12,13 +12,14 @@ import yaml
|
|
|
12
12
|
@dataclass
|
|
13
13
|
class DocsConfig:
|
|
14
14
|
"""Configuration for docs indexing."""
|
|
15
|
-
|
|
15
|
+
# Note: .ragtime/ is NOT included here - memories are indexed separately via 'reindex'
|
|
16
|
+
# to avoid duplicate entries (same file indexed as both doc and memory)
|
|
17
|
+
paths: list[str] = field(default_factory=lambda: ["docs"])
|
|
16
18
|
patterns: list[str] = field(default_factory=lambda: ["**/*.md"])
|
|
17
19
|
exclude: list[str] = field(default_factory=lambda: [
|
|
18
20
|
"**/node_modules/**",
|
|
19
21
|
"**/.git/**",
|
|
20
|
-
"**/.ragtime
|
|
21
|
-
"**/.ragtime/branches/.*", # Exclude synced (dot-prefixed) branches
|
|
22
|
+
"**/.ragtime/**", # Memories indexed separately
|
|
22
23
|
])
|
|
23
24
|
|
|
24
25
|
|
src/db.py
CHANGED
|
@@ -84,16 +84,20 @@ class RagtimeDB:
|
|
|
84
84
|
limit: int = 10,
|
|
85
85
|
type_filter: str | None = None,
|
|
86
86
|
namespace: str | None = None,
|
|
87
|
+
require_terms: list[str] | None = None,
|
|
87
88
|
**filters,
|
|
88
89
|
) -> list[dict]:
|
|
89
90
|
"""
|
|
90
|
-
|
|
91
|
+
Hybrid search: semantic similarity + keyword filtering.
|
|
91
92
|
|
|
92
93
|
Args:
|
|
93
94
|
query: Natural language search query
|
|
94
95
|
limit: Max results to return
|
|
95
96
|
type_filter: "code" or "docs" (None = both)
|
|
96
97
|
namespace: Filter by namespace (for docs)
|
|
98
|
+
require_terms: List of terms that MUST appear in results (case-insensitive).
|
|
99
|
+
Use for scoped queries like "error handling in mobile" with
|
|
100
|
+
require_terms=["mobile"] to ensure "mobile" isn't ignored.
|
|
97
101
|
**filters: Additional metadata filters (None values are ignored)
|
|
98
102
|
|
|
99
103
|
Returns:
|
|
@@ -121,9 +125,12 @@ class RagtimeDB:
|
|
|
121
125
|
else:
|
|
122
126
|
where = {"$and": conditions}
|
|
123
127
|
|
|
128
|
+
# When using require_terms, fetch more results since we'll filter some out
|
|
129
|
+
fetch_limit = limit * 5 if require_terms else limit
|
|
130
|
+
|
|
124
131
|
results = self.collection.query(
|
|
125
132
|
query_texts=[query],
|
|
126
|
-
n_results=
|
|
133
|
+
n_results=fetch_limit,
|
|
127
134
|
where=where,
|
|
128
135
|
)
|
|
129
136
|
|
|
@@ -131,12 +138,26 @@ class RagtimeDB:
|
|
|
131
138
|
output = []
|
|
132
139
|
if results["documents"] and results["documents"][0]:
|
|
133
140
|
for i, doc in enumerate(results["documents"][0]):
|
|
141
|
+
# Hybrid filtering: ensure required terms appear
|
|
142
|
+
if require_terms:
|
|
143
|
+
doc_lower = doc.lower()
|
|
144
|
+
# Also check file path in metadata for code/file matches
|
|
145
|
+
file_path = (results["metadatas"][0][i].get("file", "") or "").lower()
|
|
146
|
+
combined_text = f"{doc_lower} {file_path}"
|
|
147
|
+
|
|
148
|
+
if not all(term.lower() in combined_text for term in require_terms):
|
|
149
|
+
continue
|
|
150
|
+
|
|
134
151
|
output.append({
|
|
135
152
|
"content": doc,
|
|
136
153
|
"metadata": results["metadatas"][0][i] if results["metadatas"] else {},
|
|
137
154
|
"distance": results["distances"][0][i] if results["distances"] else None,
|
|
138
155
|
})
|
|
139
156
|
|
|
157
|
+
# Stop once we have enough
|
|
158
|
+
if len(output) >= limit:
|
|
159
|
+
break
|
|
160
|
+
|
|
140
161
|
return output
|
|
141
162
|
|
|
142
163
|
def delete(self, ids: list[str]) -> None:
|
src/mcp_server.py
CHANGED
|
@@ -132,7 +132,7 @@ class RagtimeMCPServer:
|
|
|
132
132
|
},
|
|
133
133
|
{
|
|
134
134
|
"name": "search",
|
|
135
|
-
"description": "
|
|
135
|
+
"description": "Hybrid search over indexed code and docs (semantic + keyword). 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": {
|
|
@@ -152,6 +152,11 @@ class RagtimeMCPServer:
|
|
|
152
152
|
"type": "string",
|
|
153
153
|
"description": "Filter by component"
|
|
154
154
|
},
|
|
155
|
+
"require_terms": {
|
|
156
|
+
"type": "array",
|
|
157
|
+
"items": {"type": "string"},
|
|
158
|
+
"description": "Terms that MUST appear in results (case-insensitive). Use for scoped queries like 'error handling in mobile' with require_terms=['mobile'] to ensure the qualifier isn't lost in semantic search."
|
|
159
|
+
},
|
|
155
160
|
"limit": {
|
|
156
161
|
"type": "integer",
|
|
157
162
|
"default": 10,
|
|
@@ -333,13 +338,14 @@ class RagtimeMCPServer:
|
|
|
333
338
|
}
|
|
334
339
|
|
|
335
340
|
def _search(self, args: dict) -> dict:
|
|
336
|
-
"""Search indexed content."""
|
|
341
|
+
"""Search indexed content with hybrid semantic + keyword matching."""
|
|
337
342
|
results = self.db.search(
|
|
338
343
|
query=args["query"],
|
|
339
344
|
limit=args.get("limit", 10),
|
|
340
345
|
namespace=args.get("namespace"),
|
|
341
346
|
type_filter=args.get("type"),
|
|
342
347
|
component=args.get("component"),
|
|
348
|
+
require_terms=args.get("require_terms"),
|
|
343
349
|
)
|
|
344
350
|
|
|
345
351
|
return {
|
|
@@ -487,7 +493,7 @@ class RagtimeMCPServer:
|
|
|
487
493
|
"protocolVersion": "2024-11-05",
|
|
488
494
|
"serverInfo": {
|
|
489
495
|
"name": "ragtime",
|
|
490
|
-
"version": "0.2.
|
|
496
|
+
"version": "0.2.11",
|
|
491
497
|
},
|
|
492
498
|
"capabilities": {
|
|
493
499
|
"tools": {},
|
src/memory.py
CHANGED
|
@@ -207,25 +207,41 @@ class MemoryStore:
|
|
|
207
207
|
|
|
208
208
|
def get(self, memory_id: str) -> Optional[Memory]:
|
|
209
209
|
"""Get a memory by ID."""
|
|
210
|
-
# Search in ChromaDB to find the
|
|
211
|
-
results = self.db.collection.get(ids=[memory_id])
|
|
210
|
+
# Search in ChromaDB to find the memory
|
|
211
|
+
results = self.db.collection.get(ids=[memory_id], include=["documents", "metadatas"])
|
|
212
212
|
|
|
213
213
|
if not results["ids"]:
|
|
214
214
|
return None
|
|
215
215
|
|
|
216
216
|
metadata = results["metadatas"][0]
|
|
217
|
+
content = results["documents"][0] if results["documents"] else ""
|
|
217
218
|
file_rel_path = metadata.get("file", "")
|
|
218
219
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if file_path.exists():
|
|
225
|
-
# Pass relative_to so the memory preserves its actual file path
|
|
226
|
-
return Memory.from_file(file_path, relative_to=self.memory_dir)
|
|
220
|
+
# Try to read from file first (has full frontmatter data)
|
|
221
|
+
if file_rel_path:
|
|
222
|
+
file_path = self.memory_dir / file_rel_path
|
|
223
|
+
if file_path.exists():
|
|
224
|
+
return Memory.from_file(file_path, relative_to=self.memory_dir)
|
|
227
225
|
|
|
228
|
-
|
|
226
|
+
# Fall back to constructing from ChromaDB data
|
|
227
|
+
# This handles cases where file path is wrong or file was deleted
|
|
228
|
+
return Memory(
|
|
229
|
+
id=memory_id,
|
|
230
|
+
content=content,
|
|
231
|
+
namespace=metadata.get("namespace", "unknown"),
|
|
232
|
+
type=metadata.get("type", "unknown"),
|
|
233
|
+
component=metadata.get("component"),
|
|
234
|
+
confidence=metadata.get("confidence", "medium"),
|
|
235
|
+
confidence_reason=metadata.get("confidence_reason"),
|
|
236
|
+
source=metadata.get("source", "unknown"),
|
|
237
|
+
status=metadata.get("status", "active"),
|
|
238
|
+
added=metadata.get("added", ""),
|
|
239
|
+
author=metadata.get("author"),
|
|
240
|
+
issue=metadata.get("issue"),
|
|
241
|
+
epic=metadata.get("epic"),
|
|
242
|
+
branch=metadata.get("branch"),
|
|
243
|
+
_file_path=file_rel_path,
|
|
244
|
+
)
|
|
229
245
|
|
|
230
246
|
def delete(self, memory_id: str) -> bool:
|
|
231
247
|
"""Delete a memory by ID."""
|
|
@@ -322,10 +338,13 @@ class MemoryStore:
|
|
|
322
338
|
if component:
|
|
323
339
|
conditions.append({"component": component})
|
|
324
340
|
|
|
341
|
+
# Exclude docs/code entries - they use type="docs" or type="code"
|
|
342
|
+
# while memories use types like "architecture", "feature", etc.
|
|
343
|
+
# This is especially important for wildcard queries
|
|
344
|
+
conditions.append({"type": {"$nin": ["docs", "code"]}})
|
|
345
|
+
|
|
325
346
|
# Build where clause with $and if multiple conditions
|
|
326
|
-
if len(conditions) ==
|
|
327
|
-
where = None
|
|
328
|
-
elif len(conditions) == 1:
|
|
347
|
+
if len(conditions) == 1:
|
|
329
348
|
where = conditions[0]
|
|
330
349
|
else:
|
|
331
350
|
where = {"$and": conditions}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|