ragtime-cli 0.2.11__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.11/ragtime_cli.egg-info → ragtime_cli-0.2.13}/PKG-INFO +1 -1
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/pyproject.toml +1 -1
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13/ragtime_cli.egg-info}/PKG-INFO +1 -1
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/src/cli.py +66 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/src/mcp_server.py +1 -1
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/src/memory.py +84 -15
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/LICENSE +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/README.md +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/ragtime_cli.egg-info/SOURCES.txt +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/ragtime_cli.egg-info/dependency_links.txt +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/ragtime_cli.egg-info/entry_points.txt +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/ragtime_cli.egg-info/requires.txt +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/ragtime_cli.egg-info/top_level.txt +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/setup.cfg +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/src/__init__.py +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/src/commands/audit.md +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/src/commands/create-pr.md +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/src/commands/generate-docs.md +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/src/commands/handoff.md +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/src/commands/import-docs.md +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/src/commands/pr-graduate.md +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/src/commands/recall.md +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/src/commands/remember.md +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/src/commands/save.md +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/src/commands/start.md +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/src/config.py +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/src/db.py +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/src/indexers/__init__.py +0 -0
- {ragtime_cli-0.2.11 → ragtime_cli-0.2.13}/src/indexers/code.py +0 -0
- {ragtime_cli-0.2.11 → 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
|
|
@@ -734,6 +734,72 @@ def reindex(path: Path):
|
|
|
734
734
|
click.echo(f"✓ Reindexed {count} memory files")
|
|
735
735
|
|
|
736
736
|
|
|
737
|
+
@main.command()
|
|
738
|
+
@click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
739
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be removed")
|
|
740
|
+
def dedupe(path: Path, dry_run: bool):
|
|
741
|
+
"""Clean up index: remove duplicates and orphaned entries.
|
|
742
|
+
|
|
743
|
+
- Removes duplicate entries (keeps one per file path)
|
|
744
|
+
- Removes orphaned entries (files that no longer exist on disk)
|
|
745
|
+
"""
|
|
746
|
+
path = Path(path).resolve()
|
|
747
|
+
db = get_db(path)
|
|
748
|
+
memory_dir = path / ".ragtime"
|
|
749
|
+
|
|
750
|
+
# Get all entries with their file paths
|
|
751
|
+
results = db.collection.get(include=["metadatas"])
|
|
752
|
+
|
|
753
|
+
# Group by file path and track orphans
|
|
754
|
+
by_file: dict[str, list[str]] = {}
|
|
755
|
+
orphans: list[str] = []
|
|
756
|
+
|
|
757
|
+
for i, mem_id in enumerate(results["ids"]):
|
|
758
|
+
file_path = results["metadatas"][i].get("file", "")
|
|
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
|
|
768
|
+
|
|
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] = []
|
|
783
|
+
for file_path, ids in by_file.items():
|
|
784
|
+
if len(ids) > 1:
|
|
785
|
+
duplicates.extend(ids[1:])
|
|
786
|
+
if dry_run:
|
|
787
|
+
click.echo(f" Duplicate: {file_path} ({len(ids)} copies, removing {len(ids) - 1})")
|
|
788
|
+
|
|
789
|
+
to_remove = orphans + duplicates
|
|
790
|
+
|
|
791
|
+
if not to_remove:
|
|
792
|
+
click.echo("✓ Index is clean (no duplicates or orphans)")
|
|
793
|
+
return
|
|
794
|
+
|
|
795
|
+
if dry_run:
|
|
796
|
+
click.echo(f"\nWould remove {len(orphans)} orphans + {len(duplicates)} duplicates = {len(to_remove)} entries")
|
|
797
|
+
click.echo("Run without --dry-run to remove them")
|
|
798
|
+
else:
|
|
799
|
+
db.delete(to_remove)
|
|
800
|
+
click.echo(f"✓ Removed {len(orphans)} orphans + {len(duplicates)} duplicates = {len(to_remove)} entries")
|
|
801
|
+
|
|
802
|
+
|
|
737
803
|
@main.command("new-branch")
|
|
738
804
|
@click.argument("issue", type=int)
|
|
739
805
|
@click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
@@ -10,6 +10,7 @@ from dataclasses import dataclass, field
|
|
|
10
10
|
from datetime import date
|
|
11
11
|
from typing import Optional
|
|
12
12
|
import uuid
|
|
13
|
+
import hashlib
|
|
13
14
|
import re
|
|
14
15
|
import yaml
|
|
15
16
|
|
|
@@ -109,42 +110,102 @@ class Memory:
|
|
|
109
110
|
slug = re.sub(r'[-\s]+', '-', slug).strip('-')
|
|
110
111
|
return slug[:40] # Limit length
|
|
111
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
|
+
|
|
112
143
|
@classmethod
|
|
113
144
|
def from_file(cls, path: Path, relative_to: Optional[Path] = None) -> "Memory":
|
|
114
145
|
"""
|
|
115
146
|
Parse a memory from a markdown file with YAML frontmatter.
|
|
116
147
|
|
|
148
|
+
If no frontmatter exists, infers metadata from folder structure.
|
|
149
|
+
|
|
117
150
|
Args:
|
|
118
151
|
path: Full path to the markdown file
|
|
119
152
|
relative_to: Base directory to compute relative path from (for indexing)
|
|
120
153
|
"""
|
|
121
154
|
text = path.read_text()
|
|
122
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
|
|
123
165
|
if not text.startswith("---"):
|
|
124
|
-
|
|
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
|
+
)
|
|
125
179
|
|
|
126
180
|
# Split frontmatter and content
|
|
127
181
|
parts = text.split("---", 2)
|
|
128
182
|
if len(parts) < 3:
|
|
129
183
|
raise ValueError(f"Invalid frontmatter format in {path}")
|
|
130
184
|
|
|
131
|
-
frontmatter = yaml.safe_load(parts[1])
|
|
185
|
+
frontmatter = yaml.safe_load(parts[1]) or {}
|
|
132
186
|
content = parts[2].strip()
|
|
133
187
|
|
|
134
|
-
#
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
188
|
+
# Infer missing metadata from folder structure
|
|
189
|
+
inferred = cls._infer_metadata_from_path(file_path or str(path))
|
|
190
|
+
|
|
191
|
+
# Use frontmatter ID if present, otherwise derive stable ID from file path
|
|
192
|
+
# This ensures reindex is idempotent - same file always gets same ID
|
|
193
|
+
if "id" in frontmatter:
|
|
194
|
+
memory_id = frontmatter["id"]
|
|
195
|
+
elif file_path:
|
|
196
|
+
# Stable hash of relative path
|
|
197
|
+
memory_id = hashlib.sha256(file_path.encode()).hexdigest()[:8]
|
|
198
|
+
else:
|
|
199
|
+
# Fallback: hash of absolute path
|
|
200
|
+
memory_id = hashlib.sha256(str(path).encode()).hexdigest()[:8]
|
|
141
201
|
|
|
142
202
|
return cls(
|
|
143
|
-
id=
|
|
203
|
+
id=memory_id,
|
|
144
204
|
content=content,
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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"),
|
|
148
209
|
confidence=frontmatter.get("confidence", "medium"),
|
|
149
210
|
confidence_reason=frontmatter.get("confidence_reason"),
|
|
150
211
|
source=frontmatter.get("source", "file"),
|
|
@@ -411,7 +472,9 @@ class MemoryStore:
|
|
|
411
472
|
"""
|
|
412
473
|
Reindex all memory files.
|
|
413
474
|
|
|
414
|
-
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
|
+
|
|
415
478
|
Returns count of files indexed.
|
|
416
479
|
"""
|
|
417
480
|
if not self.memory_dir.exists():
|
|
@@ -420,7 +483,13 @@ class MemoryStore:
|
|
|
420
483
|
count = 0
|
|
421
484
|
for md_file in self.memory_dir.rglob("*.md"):
|
|
422
485
|
try:
|
|
423
|
-
#
|
|
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
|
|
424
493
|
memory = Memory.from_file(md_file, relative_to=self.memory_dir)
|
|
425
494
|
self.db.upsert(
|
|
426
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
|