crumbs-cli 0.3.0__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.0
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
@@ -46,10 +46,15 @@ can open *just that slice* of a file rather than the whole thing.
46
46
 
47
47
  ## Install
48
48
 
49
+ The distribution is named `crumbs-cli`; it provides the `crumbs` command.
50
+
49
51
  ```bash
50
- pip install -e . # provides the `crumbs` command
51
- # or run without installing:
52
- python3 -m crumbs --help
52
+ pipx install crumbs-cli # isolated, on your PATH (recommended)
53
+ # or, no install at all:
54
+ uvx --from crumbs-cli crumbs --help
55
+ # or, from a clone:
56
+ pip install -e . # dev install
57
+ python3 -m crumbs --help # run without installing
53
58
  ```
54
59
 
55
60
  ## Usage
@@ -66,6 +71,39 @@ crumbs remove my-web # drop a repo from the index
66
71
 
67
72
  A repo can be referenced by name, id, or path.
68
73
 
74
+ ## Use with Claude Code (MCP)
75
+
76
+ crumbs ships an MCP server (`crumbs mcp`) so an MCP host — Claude Code, Claude
77
+ Desktop, or any MCP client — can call it as native tools. It speaks the MCP wire
78
+ protocol over stdio with **zero dependencies** (no SDK).
79
+
80
+ **One-command install (Claude Code plugin):**
81
+
82
+ ```shell
83
+ /plugin marketplace add crumbs1505/crumbs
84
+ /plugin install crumbs@crumbs
85
+ ```
86
+
87
+ This bundles the MCP server and a skill; repo paths are auto-indexed on first
88
+ use. See [`plugin/`](plugin/) for details.
89
+
90
+ **Manual registration** (e.g. in a project `.mcp.json` or your Claude Code config):
91
+
92
+ ```jsonc
93
+ {
94
+ "mcpServers": {
95
+ "crumbs": { "command": "uvx", "args": ["--from", "crumbs-cli", "crumbs", "mcp"] }
96
+ }
97
+ }
98
+ ```
99
+
100
+ `uvx` fetches and runs crumbs on demand, so nothing needs to be installed first.
101
+ (If you installed via `pipx`, use `"command": "crumbs", "args": ["mcp"]` instead.)
102
+
103
+ The server exposes five model-controlled tools — `crumbs_map`, `crumbs_search`,
104
+ `crumbs_context`, `crumbs_index`, `crumbs_list` — and auto-indexes a repo path on
105
+ first use, so there is no manual setup step.
106
+
69
107
  ## Workflow with Claude
70
108
 
71
109
  1. `crumbs index` the repos you work across (once, or on a `crumbs refresh` cron).
@@ -105,6 +143,25 @@ safely.
105
143
  python3 -m unittest discover -s tests -v
106
144
  ```
107
145
 
146
+ ## Releasing
147
+
148
+ Releases are published to [PyPI](https://pypi.org/project/crumbs-cli/) automatically by
149
+ CI ([`.github/workflows/publish.yml`](.github/workflows/publish.yml)) whenever a version
150
+ tag is pushed. To cut a release:
151
+
152
+ 1. Bump the version in **all three** places: `pyproject.toml`, `crumbs/__init__.py`,
153
+ and `plugin/.claude-plugin/plugin.json` (keep them in sync).
154
+ 2. Commit the bump and push to `main`.
155
+ 3. Tag and push it:
156
+
157
+ ```bash
158
+ git tag v0.3.1 && git push origin v0.3.1
159
+ ```
160
+
161
+ CI then builds the sdist + wheel, runs `twine check`, and publishes to PyPI via
162
+ [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC — no token stored
163
+ in the repo). PyPI versions are immutable, so every release needs a new version number.
164
+
108
165
  ## License
109
166
 
110
167
  MIT
@@ -22,10 +22,15 @@ can open *just that slice* of a file rather than the whole thing.
22
22
 
23
23
  ## Install
24
24
 
25
+ The distribution is named `crumbs-cli`; it provides the `crumbs` command.
26
+
25
27
  ```bash
26
- pip install -e . # provides the `crumbs` command
27
- # or run without installing:
28
- python3 -m crumbs --help
28
+ pipx install crumbs-cli # isolated, on your PATH (recommended)
29
+ # or, no install at all:
30
+ uvx --from crumbs-cli crumbs --help
31
+ # or, from a clone:
32
+ pip install -e . # dev install
33
+ python3 -m crumbs --help # run without installing
29
34
  ```
30
35
 
31
36
  ## Usage
@@ -42,6 +47,39 @@ crumbs remove my-web # drop a repo from the index
42
47
 
43
48
  A repo can be referenced by name, id, or path.
44
49
 
50
+ ## Use with Claude Code (MCP)
51
+
52
+ crumbs ships an MCP server (`crumbs mcp`) so an MCP host — Claude Code, Claude
53
+ Desktop, or any MCP client — can call it as native tools. It speaks the MCP wire
54
+ protocol over stdio with **zero dependencies** (no SDK).
55
+
56
+ **One-command install (Claude Code plugin):**
57
+
58
+ ```shell
59
+ /plugin marketplace add crumbs1505/crumbs
60
+ /plugin install crumbs@crumbs
61
+ ```
62
+
63
+ This bundles the MCP server and a skill; repo paths are auto-indexed on first
64
+ use. See [`plugin/`](plugin/) for details.
65
+
66
+ **Manual registration** (e.g. in a project `.mcp.json` or your Claude Code config):
67
+
68
+ ```jsonc
69
+ {
70
+ "mcpServers": {
71
+ "crumbs": { "command": "uvx", "args": ["--from", "crumbs-cli", "crumbs", "mcp"] }
72
+ }
73
+ }
74
+ ```
75
+
76
+ `uvx` fetches and runs crumbs on demand, so nothing needs to be installed first.
77
+ (If you installed via `pipx`, use `"command": "crumbs", "args": ["mcp"]` instead.)
78
+
79
+ The server exposes five model-controlled tools — `crumbs_map`, `crumbs_search`,
80
+ `crumbs_context`, `crumbs_index`, `crumbs_list` — and auto-indexes a repo path on
81
+ first use, so there is no manual setup step.
82
+
45
83
  ## Workflow with Claude
46
84
 
47
85
  1. `crumbs index` the repos you work across (once, or on a `crumbs refresh` cron).
@@ -81,6 +119,25 @@ safely.
81
119
  python3 -m unittest discover -s tests -v
82
120
  ```
83
121
 
122
+ ## Releasing
123
+
124
+ Releases are published to [PyPI](https://pypi.org/project/crumbs-cli/) automatically by
125
+ CI ([`.github/workflows/publish.yml`](.github/workflows/publish.yml)) whenever a version
126
+ tag is pushed. To cut a release:
127
+
128
+ 1. Bump the version in **all three** places: `pyproject.toml`, `crumbs/__init__.py`,
129
+ and `plugin/.claude-plugin/plugin.json` (keep them in sync).
130
+ 2. Commit the bump and push to `main`.
131
+ 3. Tag and push it:
132
+
133
+ ```bash
134
+ git tag v0.3.1 && git push origin v0.3.1
135
+ ```
136
+
137
+ CI then builds the sdist + wheel, runs `twine check`, and publishes to PyPI via
138
+ [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC — no token stored
139
+ in the repo). PyPI versions are immutable, so every release needs a new version number.
140
+
84
141
  ## License
85
142
 
86
143
  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.0"
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.0
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
@@ -46,10 +46,15 @@ can open *just that slice* of a file rather than the whole thing.
46
46
 
47
47
  ## Install
48
48
 
49
+ The distribution is named `crumbs-cli`; it provides the `crumbs` command.
50
+
49
51
  ```bash
50
- pip install -e . # provides the `crumbs` command
51
- # or run without installing:
52
- python3 -m crumbs --help
52
+ pipx install crumbs-cli # isolated, on your PATH (recommended)
53
+ # or, no install at all:
54
+ uvx --from crumbs-cli crumbs --help
55
+ # or, from a clone:
56
+ pip install -e . # dev install
57
+ python3 -m crumbs --help # run without installing
53
58
  ```
54
59
 
55
60
  ## Usage
@@ -66,6 +71,39 @@ crumbs remove my-web # drop a repo from the index
66
71
 
67
72
  A repo can be referenced by name, id, or path.
68
73
 
74
+ ## Use with Claude Code (MCP)
75
+
76
+ crumbs ships an MCP server (`crumbs mcp`) so an MCP host — Claude Code, Claude
77
+ Desktop, or any MCP client — can call it as native tools. It speaks the MCP wire
78
+ protocol over stdio with **zero dependencies** (no SDK).
79
+
80
+ **One-command install (Claude Code plugin):**
81
+
82
+ ```shell
83
+ /plugin marketplace add crumbs1505/crumbs
84
+ /plugin install crumbs@crumbs
85
+ ```
86
+
87
+ This bundles the MCP server and a skill; repo paths are auto-indexed on first
88
+ use. See [`plugin/`](plugin/) for details.
89
+
90
+ **Manual registration** (e.g. in a project `.mcp.json` or your Claude Code config):
91
+
92
+ ```jsonc
93
+ {
94
+ "mcpServers": {
95
+ "crumbs": { "command": "uvx", "args": ["--from", "crumbs-cli", "crumbs", "mcp"] }
96
+ }
97
+ }
98
+ ```
99
+
100
+ `uvx` fetches and runs crumbs on demand, so nothing needs to be installed first.
101
+ (If you installed via `pipx`, use `"command": "crumbs", "args": ["mcp"]` instead.)
102
+
103
+ The server exposes five model-controlled tools — `crumbs_map`, `crumbs_search`,
104
+ `crumbs_context`, `crumbs_index`, `crumbs_list` — and auto-indexes a repo path on
105
+ first use, so there is no manual setup step.
106
+
69
107
  ## Workflow with Claude
70
108
 
71
109
  1. `crumbs index` the repos you work across (once, or on a `crumbs refresh` cron).
@@ -105,6 +143,25 @@ safely.
105
143
  python3 -m unittest discover -s tests -v
106
144
  ```
107
145
 
146
+ ## Releasing
147
+
148
+ Releases are published to [PyPI](https://pypi.org/project/crumbs-cli/) automatically by
149
+ CI ([`.github/workflows/publish.yml`](.github/workflows/publish.yml)) whenever a version
150
+ tag is pushed. To cut a release:
151
+
152
+ 1. Bump the version in **all three** places: `pyproject.toml`, `crumbs/__init__.py`,
153
+ and `plugin/.claude-plugin/plugin.json` (keep them in sync).
154
+ 2. Commit the bump and push to `main`.
155
+ 3. Tag and push it:
156
+
157
+ ```bash
158
+ git tag v0.3.1 && git push origin v0.3.1
159
+ ```
160
+
161
+ CI then builds the sdist + wheel, runs `twine check`, and publishes to PyPI via
162
+ [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC — no token stored
163
+ in the repo). PyPI versions are immutable, so every release needs a new version number.
164
+
108
165
  ## License
109
166
 
110
167
  MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "crumbs-cli"
7
- version = "0.3.0"
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