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.
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/PKG-INFO +61 -4
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/README.md +60 -3
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/crumbs/__init__.py +1 -1
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/crumbs/indexer.py +37 -0
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/crumbs/mcp.py +12 -1
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/crumbs_cli.egg-info/PKG-INFO +61 -4
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/pyproject.toml +1 -1
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/tests/test_crumbs.py +29 -0
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/LICENSE +0 -0
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/crumbs/__main__.py +0 -0
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/crumbs/cli.py +0 -0
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/crumbs/digest.py +0 -0
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/crumbs/extractors.py +0 -0
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/crumbs/query.py +0 -0
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/crumbs/store.py +0 -0
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/crumbs_cli.egg-info/SOURCES.txt +0 -0
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/crumbs_cli.egg-info/dependency_links.txt +0 -0
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/crumbs_cli.egg-info/entry_points.txt +0 -0
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/crumbs_cli.egg-info/top_level.txt +0 -0
- {crumbs_cli-0.3.0 → crumbs_cli-0.3.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crumbs-cli
|
|
3
|
-
Version: 0.3.
|
|
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
|
-
|
|
51
|
-
# or
|
|
52
|
-
|
|
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
|
-
|
|
27
|
-
# or
|
|
28
|
-
|
|
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
|
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
51
|
-
# or
|
|
52
|
-
|
|
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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|