ragtime-cli 0.2.12__tar.gz → 0.2.13__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.12/ragtime_cli.egg-info → ragtime_cli-0.2.13}/PKG-INFO +1 -1
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/pyproject.toml +1 -1
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13/ragtime_cli.egg-info}/PKG-INFO +1 -1
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/cli.py +40 -19
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/mcp_server.py +1 -1
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/memory.py +71 -14
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/LICENSE +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/README.md +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/ragtime_cli.egg-info/SOURCES.txt +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/ragtime_cli.egg-info/dependency_links.txt +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/ragtime_cli.egg-info/entry_points.txt +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/ragtime_cli.egg-info/requires.txt +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/ragtime_cli.egg-info/top_level.txt +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/setup.cfg +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/__init__.py +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/audit.md +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/create-pr.md +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/generate-docs.md +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/handoff.md +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/import-docs.md +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/pr-graduate.md +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/recall.md +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/remember.md +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/save.md +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/start.md +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/config.py +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/db.py +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/indexers/__init__.py +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/indexers/code.py +0 -0
- {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/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.13
|
|
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.13
|
|
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
|
|
@@ -736,47 +736,68 @@ def reindex(path: Path):
|
|
|
736
736
|
|
|
737
737
|
@main.command()
|
|
738
738
|
@click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
739
|
-
@click.option("--dry-run", is_flag=True, help="Show
|
|
739
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be removed")
|
|
740
740
|
def dedupe(path: Path, dry_run: bool):
|
|
741
|
-
"""
|
|
741
|
+
"""Clean up index: remove duplicates and orphaned entries.
|
|
742
742
|
|
|
743
|
-
|
|
744
|
-
|
|
743
|
+
- Removes duplicate entries (keeps one per file path)
|
|
744
|
+
- Removes orphaned entries (files that no longer exist on disk)
|
|
745
745
|
"""
|
|
746
746
|
path = Path(path).resolve()
|
|
747
747
|
db = get_db(path)
|
|
748
|
+
memory_dir = path / ".ragtime"
|
|
748
749
|
|
|
749
750
|
# Get all entries with their file paths
|
|
750
751
|
results = db.collection.get(include=["metadatas"])
|
|
751
752
|
|
|
752
|
-
# Group by file path
|
|
753
|
+
# Group by file path and track orphans
|
|
753
754
|
by_file: dict[str, list[str]] = {}
|
|
755
|
+
orphans: list[str] = []
|
|
756
|
+
|
|
754
757
|
for i, mem_id in enumerate(results["ids"]):
|
|
755
758
|
file_path = results["metadatas"][i].get("file", "")
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
759
|
+
entry_type = results["metadatas"][i].get("type", "")
|
|
760
|
+
|
|
761
|
+
# Skip docs/code entries - only clean up memory entries
|
|
762
|
+
if entry_type in ("docs", "code"):
|
|
763
|
+
continue
|
|
764
|
+
|
|
765
|
+
if not file_path:
|
|
766
|
+
orphans.append(mem_id)
|
|
767
|
+
continue
|
|
760
768
|
|
|
761
|
-
|
|
762
|
-
|
|
769
|
+
# Check if file exists on disk
|
|
770
|
+
full_path = memory_dir / file_path
|
|
771
|
+
if not full_path.exists():
|
|
772
|
+
orphans.append(mem_id)
|
|
773
|
+
if dry_run:
|
|
774
|
+
click.echo(f" Orphan: {file_path} (file missing)")
|
|
775
|
+
continue
|
|
776
|
+
|
|
777
|
+
if file_path not in by_file:
|
|
778
|
+
by_file[file_path] = []
|
|
779
|
+
by_file[file_path].append(mem_id)
|
|
780
|
+
|
|
781
|
+
# Find duplicates (keep first, remove rest)
|
|
782
|
+
duplicates: list[str] = []
|
|
763
783
|
for file_path, ids in by_file.items():
|
|
764
784
|
if len(ids) > 1:
|
|
765
|
-
|
|
766
|
-
duplicates_to_remove.extend(ids[1:])
|
|
785
|
+
duplicates.extend(ids[1:])
|
|
767
786
|
if dry_run:
|
|
768
|
-
click.echo(f" {file_path}
|
|
787
|
+
click.echo(f" Duplicate: {file_path} ({len(ids)} copies, removing {len(ids) - 1})")
|
|
788
|
+
|
|
789
|
+
to_remove = orphans + duplicates
|
|
769
790
|
|
|
770
|
-
if not
|
|
771
|
-
click.echo("✓
|
|
791
|
+
if not to_remove:
|
|
792
|
+
click.echo("✓ Index is clean (no duplicates or orphans)")
|
|
772
793
|
return
|
|
773
794
|
|
|
774
795
|
if dry_run:
|
|
775
|
-
click.echo(f"\nWould remove {len(
|
|
796
|
+
click.echo(f"\nWould remove {len(orphans)} orphans + {len(duplicates)} duplicates = {len(to_remove)} entries")
|
|
776
797
|
click.echo("Run without --dry-run to remove them")
|
|
777
798
|
else:
|
|
778
|
-
db.delete(
|
|
779
|
-
click.echo(f"✓ Removed {len(
|
|
799
|
+
db.delete(to_remove)
|
|
800
|
+
click.echo(f"✓ Removed {len(orphans)} orphans + {len(duplicates)} duplicates = {len(to_remove)} entries")
|
|
780
801
|
|
|
781
802
|
|
|
782
803
|
@main.command("new-branch")
|
|
@@ -110,35 +110,83 @@ class Memory:
|
|
|
110
110
|
slug = re.sub(r'[-\s]+', '-', slug).strip('-')
|
|
111
111
|
return slug[:40] # Limit length
|
|
112
112
|
|
|
113
|
+
@classmethod
|
|
114
|
+
def _infer_metadata_from_path(cls, relative_path: str) -> dict:
|
|
115
|
+
"""
|
|
116
|
+
Infer namespace, component, and type from folder structure.
|
|
117
|
+
|
|
118
|
+
Supports:
|
|
119
|
+
app/{component}/*.md → namespace=app, component={component}
|
|
120
|
+
app/*.md → namespace=app
|
|
121
|
+
team/*.md → namespace=team
|
|
122
|
+
users/{username}/*.md → namespace=user-{username}
|
|
123
|
+
branches/{branch}/*.md → namespace=branch-{branch}
|
|
124
|
+
"""
|
|
125
|
+
parts = relative_path.replace("\\", "/").split("/")
|
|
126
|
+
metadata = {}
|
|
127
|
+
|
|
128
|
+
if len(parts) >= 1:
|
|
129
|
+
first = parts[0]
|
|
130
|
+
if first == "app":
|
|
131
|
+
metadata["namespace"] = "app"
|
|
132
|
+
if len(parts) >= 3: # app/{component}/file.md
|
|
133
|
+
metadata["component"] = parts[1]
|
|
134
|
+
elif first == "team":
|
|
135
|
+
metadata["namespace"] = "team"
|
|
136
|
+
elif first == "users" and len(parts) >= 2:
|
|
137
|
+
metadata["namespace"] = f"user-{parts[1]}"
|
|
138
|
+
elif first == "branches" and len(parts) >= 2:
|
|
139
|
+
metadata["namespace"] = f"branch-{parts[1]}"
|
|
140
|
+
|
|
141
|
+
return metadata
|
|
142
|
+
|
|
113
143
|
@classmethod
|
|
114
144
|
def from_file(cls, path: Path, relative_to: Optional[Path] = None) -> "Memory":
|
|
115
145
|
"""
|
|
116
146
|
Parse a memory from a markdown file with YAML frontmatter.
|
|
117
147
|
|
|
148
|
+
If no frontmatter exists, infers metadata from folder structure.
|
|
149
|
+
|
|
118
150
|
Args:
|
|
119
151
|
path: Full path to the markdown file
|
|
120
152
|
relative_to: Base directory to compute relative path from (for indexing)
|
|
121
153
|
"""
|
|
122
154
|
text = path.read_text()
|
|
123
155
|
|
|
156
|
+
# Compute relative path for inference and indexing
|
|
157
|
+
file_path = None
|
|
158
|
+
if relative_to:
|
|
159
|
+
try:
|
|
160
|
+
file_path = str(path.relative_to(relative_to))
|
|
161
|
+
except ValueError:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
# Handle files without frontmatter - infer from path
|
|
124
165
|
if not text.startswith("---"):
|
|
125
|
-
|
|
166
|
+
inferred = cls._infer_metadata_from_path(file_path or str(path))
|
|
167
|
+
# Generate stable ID from path
|
|
168
|
+
memory_id = hashlib.sha256((file_path or str(path)).encode()).hexdigest()[:8]
|
|
169
|
+
|
|
170
|
+
return cls(
|
|
171
|
+
id=memory_id,
|
|
172
|
+
content=text.strip(),
|
|
173
|
+
namespace=inferred.get("namespace", "app"),
|
|
174
|
+
type=inferred.get("type", "note"),
|
|
175
|
+
component=inferred.get("component"),
|
|
176
|
+
source="file",
|
|
177
|
+
_file_path=file_path,
|
|
178
|
+
)
|
|
126
179
|
|
|
127
180
|
# Split frontmatter and content
|
|
128
181
|
parts = text.split("---", 2)
|
|
129
182
|
if len(parts) < 3:
|
|
130
183
|
raise ValueError(f"Invalid frontmatter format in {path}")
|
|
131
184
|
|
|
132
|
-
frontmatter = yaml.safe_load(parts[1])
|
|
185
|
+
frontmatter = yaml.safe_load(parts[1]) or {}
|
|
133
186
|
content = parts[2].strip()
|
|
134
187
|
|
|
135
|
-
#
|
|
136
|
-
|
|
137
|
-
if relative_to:
|
|
138
|
-
try:
|
|
139
|
-
file_path = str(path.relative_to(relative_to))
|
|
140
|
-
except ValueError:
|
|
141
|
-
pass # path not relative to base, will regenerate
|
|
188
|
+
# Infer missing metadata from folder structure
|
|
189
|
+
inferred = cls._infer_metadata_from_path(file_path or str(path))
|
|
142
190
|
|
|
143
191
|
# Use frontmatter ID if present, otherwise derive stable ID from file path
|
|
144
192
|
# This ensures reindex is idempotent - same file always gets same ID
|
|
@@ -154,9 +202,10 @@ class Memory:
|
|
|
154
202
|
return cls(
|
|
155
203
|
id=memory_id,
|
|
156
204
|
content=content,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
205
|
+
# Use frontmatter if present, fall back to inferred, then defaults
|
|
206
|
+
namespace=frontmatter.get("namespace") or inferred.get("namespace", "app"),
|
|
207
|
+
type=frontmatter.get("type") or inferred.get("type", "note"),
|
|
208
|
+
component=frontmatter.get("component") or inferred.get("component"),
|
|
160
209
|
confidence=frontmatter.get("confidence", "medium"),
|
|
161
210
|
confidence_reason=frontmatter.get("confidence_reason"),
|
|
162
211
|
source=frontmatter.get("source", "file"),
|
|
@@ -423,7 +472,9 @@ class MemoryStore:
|
|
|
423
472
|
"""
|
|
424
473
|
Reindex all memory files.
|
|
425
474
|
|
|
426
|
-
Scans .ragtime/ and indexes
|
|
475
|
+
Scans .ragtime/ and indexes files. Removes old entries for each file
|
|
476
|
+
before upserting to prevent duplicates from ID changes.
|
|
477
|
+
|
|
427
478
|
Returns count of files indexed.
|
|
428
479
|
"""
|
|
429
480
|
if not self.memory_dir.exists():
|
|
@@ -432,7 +483,13 @@ class MemoryStore:
|
|
|
432
483
|
count = 0
|
|
433
484
|
for md_file in self.memory_dir.rglob("*.md"):
|
|
434
485
|
try:
|
|
435
|
-
#
|
|
486
|
+
# Compute relative path for this file
|
|
487
|
+
rel_path = str(md_file.relative_to(self.memory_dir))
|
|
488
|
+
|
|
489
|
+
# Delete any existing entries for this file path (handles ID changes)
|
|
490
|
+
self.db.delete_by_file([rel_path])
|
|
491
|
+
|
|
492
|
+
# Parse and index with stable ID
|
|
436
493
|
memory = Memory.from_file(md_file, relative_to=self.memory_dir)
|
|
437
494
|
self.db.upsert(
|
|
438
495
|
ids=[memory.id],
|
|
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
|
|
File without changes
|
|
File without changes
|