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.
Files changed (30) hide show
  1. {ragtime_cli-0.2.12/ragtime_cli.egg-info → ragtime_cli-0.2.13}/PKG-INFO +1 -1
  2. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/pyproject.toml +1 -1
  3. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13/ragtime_cli.egg-info}/PKG-INFO +1 -1
  4. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/cli.py +40 -19
  5. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/mcp_server.py +1 -1
  6. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/memory.py +71 -14
  7. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/LICENSE +0 -0
  8. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/README.md +0 -0
  9. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/ragtime_cli.egg-info/SOURCES.txt +0 -0
  10. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/ragtime_cli.egg-info/dependency_links.txt +0 -0
  11. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/ragtime_cli.egg-info/entry_points.txt +0 -0
  12. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/ragtime_cli.egg-info/requires.txt +0 -0
  13. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/ragtime_cli.egg-info/top_level.txt +0 -0
  14. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/setup.cfg +0 -0
  15. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/__init__.py +0 -0
  16. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/audit.md +0 -0
  17. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/create-pr.md +0 -0
  18. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/generate-docs.md +0 -0
  19. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/handoff.md +0 -0
  20. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/import-docs.md +0 -0
  21. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/pr-graduate.md +0 -0
  22. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/recall.md +0 -0
  23. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/remember.md +0 -0
  24. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/save.md +0 -0
  25. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/commands/start.md +0 -0
  26. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/config.py +0 -0
  27. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/db.py +0 -0
  28. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/indexers/__init__.py +0 -0
  29. {ragtime_cli-0.2.12 → ragtime_cli-0.2.13}/src/indexers/code.py +0 -0
  30. {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.12
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
  [project]
2
2
  name = "ragtime-cli"
3
- version = "0.2.12"
3
+ version = "0.2.13"
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.12
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 duplicates without removing them")
739
+ @click.option("--dry-run", is_flag=True, help="Show what would be removed")
740
740
  def dedupe(path: Path, dry_run: bool):
741
- """Remove duplicate entries from the index.
741
+ """Clean up index: remove duplicates and orphaned entries.
742
742
 
743
- Keeps one entry per unique file path, removing duplicates created
744
- by older versions of reindex that generated random IDs.
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
- if file_path:
757
- if file_path not in by_file:
758
- by_file[file_path] = []
759
- by_file[file_path].append(mem_id)
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
- # Find duplicates
762
- duplicates_to_remove = []
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
- # Keep the first one, remove the rest
766
- duplicates_to_remove.extend(ids[1:])
785
+ duplicates.extend(ids[1:])
767
786
  if dry_run:
768
- click.echo(f" {file_path}: {len(ids)} copies (would remove {len(ids) - 1})")
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 duplicates_to_remove:
771
- click.echo("✓ No duplicates found")
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(duplicates_to_remove)} duplicate entries")
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(duplicates_to_remove)
779
- click.echo(f"✓ Removed {len(duplicates_to_remove)} duplicate entries")
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")
@@ -493,7 +493,7 @@ class RagtimeMCPServer:
493
493
  "protocolVersion": "2024-11-05",
494
494
  "serverInfo": {
495
495
  "name": "ragtime",
496
- "version": "0.2.12",
496
+ "version": "0.2.13",
497
497
  },
498
498
  "capabilities": {
499
499
  "tools": {},
@@ -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
- raise ValueError(f"No YAML frontmatter found in {path}")
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
- # Compute relative file path for indexing
136
- file_path = None
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
- namespace=frontmatter.get("namespace", "app"),
158
- type=frontmatter.get("type", "unknown"),
159
- component=frontmatter.get("component"),
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 any files not in ChromaDB.
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
- # Pass memory_dir so the actual file path is stored, not regenerated
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