crumbs-cli 0.3.1__tar.gz → 0.3.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crumbs-cli
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Local, token-efficient cross-repo context for LLMs. CLI + MCP server.
5
5
  Author: crumbs
6
6
  License: MIT
@@ -6,4 +6,4 @@ crumbs to understand many repos at once without reading -- and paying tokens for
6
6
  -- the entire source tree.
7
7
  """
8
8
 
9
- __version__ = "0.3.1"
9
+ __version__ = "0.3.2"
@@ -54,6 +54,43 @@ def _git_info(root: Path) -> Dict[str, str]:
54
54
  return info
55
55
 
56
56
 
57
+ def newest_mtime(root: Path) -> float:
58
+ """Most recent mtime among indexable files (cheap, stat-only walk).
59
+
60
+ Uses the same directory/file filters as :func:`index_repo` so the set of
61
+ files considered matches what would actually be indexed.
62
+ """
63
+ newest = 0.0
64
+ for dirpath, dirnames, filenames in os.walk(root):
65
+ dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith(".") or d in (".github",)]
66
+ for fn in filenames:
67
+ if fn in SKIP_FILES:
68
+ continue
69
+ try:
70
+ m = (Path(dirpath) / fn).stat().st_mtime
71
+ except OSError:
72
+ continue
73
+ if m > newest:
74
+ newest = m
75
+ return newest
76
+
77
+
78
+ def is_stale(data: Dict[str, Any]) -> bool:
79
+ """True if the repo on disk has changed since it was last indexed.
80
+
81
+ Returns ``True`` for unreadable/missing metadata so callers re-index;
82
+ returns ``False`` if the path no longer exists (nothing to refresh from).
83
+ """
84
+ path = data.get("path")
85
+ indexed_at = data.get("indexed_at")
86
+ if not path or indexed_at is None:
87
+ return True
88
+ root = Path(path)
89
+ if not root.is_dir():
90
+ return False
91
+ return newest_mtime(root) > indexed_at
92
+
93
+
57
94
  def index_repo(path: str, name: Optional[str] = None) -> Dict[str, Any]:
58
95
  """Index a repository at ``path`` and persist its crumbs.
59
96
 
@@ -75,9 +75,20 @@ def _tool_list(args: Dict[str, Any]) -> str:
75
75
 
76
76
 
77
77
  def _resolve_or_index(selector: str) -> Optional[str]:
78
- """Resolve a repo selector; if it's an unindexed path, index it first."""
78
+ """Resolve a repo selector, indexing (or re-indexing when stale) as needed.
79
+
80
+ An already-indexed repo is rebuilt if its source has changed since the last
81
+ index, so map/search/context never serve an out-of-date crumb map.
82
+ """
79
83
  rid = store.resolve(selector)
80
84
  if rid:
85
+ data = store.load_repo(rid)
86
+ if data and not indexer.is_stale(data):
87
+ return rid
88
+ try:
89
+ indexer.index_repo(data["path"] if data else selector)
90
+ except (NotADirectoryError, FileNotFoundError, KeyError):
91
+ pass # keep the existing (possibly stale) index rather than failing
81
92
  return rid
82
93
  try:
83
94
  indexer.index_repo(selector)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crumbs-cli
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Local, token-efficient cross-repo context for LLMs. CLI + MCP server.
5
5
  Author: crumbs
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "crumbs-cli"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "Local, token-efficient cross-repo context for LLMs. CLI + MCP server."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -118,6 +118,35 @@ class TestIndexAndQuery(unittest.TestCase):
118
118
  self.assertIn("demo", out)
119
119
 
120
120
 
121
+ class TestStaleness(unittest.TestCase):
122
+ def setUp(self):
123
+ self.repo = Path(tempfile.mkdtemp(prefix="stale-repo-"))
124
+ make_repo(self.repo)
125
+ self.data = indexer.index_repo(str(self.repo), name="stale")
126
+
127
+ def test_fresh_index_is_not_stale(self):
128
+ self.assertFalse(indexer.is_stale(self.data))
129
+
130
+ def test_new_file_marks_index_stale(self):
131
+ # Write a file whose mtime is newer than the recorded index time.
132
+ new_file = self.repo / "src" / "added.py"
133
+ new_file.write_text("def freshly_added():\n return 1\n")
134
+ os.utime(new_file, (self.data["indexed_at"] + 10, self.data["indexed_at"] + 10))
135
+ self.assertTrue(indexer.is_stale(self.data))
136
+
137
+ def test_map_auto_reindexes_when_stale(self):
138
+ new_file = self.repo / "src" / "later.py"
139
+ new_file.write_text("def added_later():\n return 2\n")
140
+ os.utime(new_file, (self.data["indexed_at"] + 10, self.data["indexed_at"] + 10))
141
+ # crumbs_map should detect staleness and pick up the new symbol.
142
+ out = mcp._tool_map({"repo": str(self.repo)})
143
+ self.assertIn("added_later", out)
144
+
145
+ def test_missing_path_is_not_stale(self):
146
+ ghost = dict(self.data, path="/nonexistent/path/xyz")
147
+ self.assertFalse(indexer.is_stale(ghost))
148
+
149
+
121
150
  class TestMCP(unittest.TestCase):
122
151
  @classmethod
123
152
  def setUpClass(cls):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes