diff --git a/README.md b/README.md index 87901f15..6d1c6489 100644 --- a/README.md +++ b/README.md @@ -98,9 +98,12 @@ semble search "deployment guide" ./my-project --content docs # or: config, all # Find code similar to a known location semble find-related src/auth.py 42 ./my-project + +# Show only the first N lines of each result's snippet (0 = path/line range only) +semble search "authentication flow" ./my-project --max-snippet-lines 10 ``` -`--content` accepts `code` (default), `docs`, `config`, or `all`. `path` defaults to the current directory when omitted; git URLs are accepted. If `semble` is not on `$PATH`, use `uvx --from "semble[mcp]" semble` in its place. +`--content` accepts `code` (default), `docs`, `config`, or `all`. `path` defaults to the current directory when omitted; git URLs are accepted. If `semble` is not on `$PATH`, use `uvx --from "semble[mcp]" semble` in its place. `semble --version` (or `-V`) prints the installed version.
Controlling which files are indexed @@ -173,6 +176,8 @@ By default, your Semble savings statistics and any saved indexes are stored in t On first use, Semble also downloads the embedding model from Hugging Face and caches it in the standard Hugging Face cache (`~/.cache/huggingface/` by default, or `$HF_HOME` if set); this only happens once and requires network access. +Use `semble clear` to remove cached data: `semble clear index` (saved indexes), `semble clear savings` (usage stats), `semble clear orphans` (indexes for repos no longer present on disk), or `semble clear all` (everything). +
diff --git a/pyproject.toml b/pyproject.toml index 5da88b1d..665fd5ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,6 @@ dependencies = [ "vicinity>=0.4.4", "numpy>=1.24.0", "pathspec>=0.12", - "tree-sitter>=0.25,<0.26", "orjson", "questionary>=2.0,<3.0", "semble-grammars>=0.1.2", diff --git a/src/semble/cli.py b/src/semble/cli.py index dad4770d..eddd3df2 100644 --- a/src/semble/cli.py +++ b/src/semble/cli.py @@ -1,5 +1,6 @@ import argparse import asyncio +import io import json import re import sys @@ -66,6 +67,10 @@ def _add_content_args(p: argparse.ArgumentParser) -> None: def main() -> None: """Entry point for the semble command-line tool.""" + # Non-UTF-8 Windows consoles can't encode glyphs like "✓" and would otherwise crash. + for stream in (sys.stdout, sys.stderr): + if isinstance(stream, io.TextIOWrapper): + stream.reconfigure(errors="replace") if len(sys.argv) > 1 and sys.argv[1] in _CLI_DISPATCH_ARGS: _cli_main() else: diff --git a/src/semble/utils.py b/src/semble/utils.py index 19d786c9..640b0c42 100644 --- a/src/semble/utils.py +++ b/src/semble/utils.py @@ -22,9 +22,11 @@ def resolve_chunk(chunks: list[Chunk], file_path: str, line: int) -> Chunk | Non Reconstructs a Chunk from its JSON-primitive MCP tool arguments (file_path + line) before calling into the library. """ + # Normalize separators: file_path is stored with the platform's native separator. + file_path = file_path.replace("\\", "/") fallback = None for chunk in chunks: - if chunk.file_path == file_path and chunk.start_line <= line <= chunk.end_line: + if chunk.file_path.replace("\\", "/") == file_path and chunk.start_line <= line <= chunk.end_line: if line < chunk.end_line: return chunk if fallback is None: # line == end_line: boundary; keep as fallback for end-of-file chunks diff --git a/src/semble/version.py b/src/semble/version.py index ef409fb0..5073cce7 100644 --- a/src/semble/version.py +++ b/src/semble/version.py @@ -1,2 +1,2 @@ -__version_triple__ = (0, 5, 3) +__version_triple__ = (0, 5, 4) __version__ = ".".join(map(str, __version_triple__)) diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 77a46046..0f318f26 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -65,6 +65,10 @@ def test_resolve_chunk() -> None: # Line out of range returns None. assert resolve_chunk([interior], "src/a.py", 99) is None + # Separator mismatch (e.g. backslash-stored path, forward-slash query) still matches. + backslash_chunk = make_chunk("line1\nline2\nline3", "src\\a.py") + assert resolve_chunk([backslash_chunk], "src/a.py", 2) is backslash_chunk + @pytest.mark.parametrize( ("path", "expected"), diff --git a/uv.lock b/uv.lock index 74fc034b..7630799b 100644 --- a/uv.lock +++ b/uv.lock @@ -328,7 +328,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } wheels = [ @@ -400,7 +400,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ @@ -730,7 +730,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -2900,10 +2900,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "joblib", marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, + { name = "joblib" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } }, + { name = "threadpoolctl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } wheels = [ @@ -2949,10 +2949,10 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "joblib", marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, + { name = "joblib" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" } }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" } }, + { name = "threadpoolctl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } wheels = [ @@ -3002,7 +3002,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -3063,7 +3063,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -3140,7 +3140,6 @@ dependencies = [ { name = "pathspec" }, { name = "questionary" }, { name = "semble-grammars" }, - { name = "tree-sitter" }, { name = "vicinity" }, ] @@ -3187,7 +3186,6 @@ requires-dist = [ { name = "semble-grammars", specifier = ">=0.1.2" }, { name = "sentence-transformers", marker = "extra == 'benchmark'", specifier = ">=3.0" }, { name = "tiktoken", marker = "extra == 'benchmark'", specifier = ">=0.7" }, - { name = "tree-sitter", specifier = ">=0.25,<0.26" }, { name = "vicinity", specifier = ">=0.4.4" }, ] provides-extras = ["mcp", "benchmark", "dev"]