From 6d211fd0d6fb8fc7081174bdf7a4eed78642f80c Mon Sep 17 00:00:00 2001 From: Pringled Date: Wed, 5 Aug 2026 11:03:52 +0200 Subject: [PATCH 1/5] fix: Fix Windows console issues, remove tree-sitter dep --- pyproject.toml | 1 - src/semble/cli.py | 4 ++++ src/semble/utils.py | 3 ++- tests/test_cli.py | 4 +++- tests/test_mcp.py | 4 ++++ uv.lock | 28 +++++++++++++--------------- 6 files changed, 26 insertions(+), 18 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5da88b1d8..665fd5ceb 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 dad4770d5..c30c1d85d 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,9 @@ def _add_content_args(p: argparse.ArgumentParser) -> None: def main() -> None: """Entry point for the semble command-line tool.""" + 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 19d786c95..f6ee52a5f 100644 --- a/src/semble/utils.py +++ b/src/semble/utils.py @@ -22,9 +22,10 @@ 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. """ + 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/tests/test_cli.py b/tests/test_cli.py index e4245f600..85fe080f4 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -21,8 +21,10 @@ ], ) def test_main_calls_asyncio_run(argv: list[str], monkeypatch: pytest.MonkeyPatch) -> None: - """main() delegates to asyncio.run(serve(...)) when no CLI subcommand is given.""" + """main() delegates to asyncio.run(serve(...)), tolerating streams without reconfigure().""" monkeypatch.setattr(sys, "argv", argv) + monkeypatch.setattr(sys, "stdout", object()) + monkeypatch.setattr(sys, "stderr", object()) with patch("asyncio.run") as mock_run: mock_run.side_effect = lambda coro: coro.close() main() diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 77a46046e..0f318f268 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 74fc034bb..7630799b4 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"] From c4b725a70e0cc0c6032306cbcb421425178fa722 Mon Sep 17 00:00:00 2001 From: Pringled Date: Wed, 5 Aug 2026 11:06:51 +0200 Subject: [PATCH 2/5] docs: Update docstrings --- src/semble/cli.py | 1 + src/semble/utils.py | 1 + tests/test_cli.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/semble/cli.py b/src/semble/cli.py index c30c1d85d..eddd3df2d 100644 --- a/src/semble/cli.py +++ b/src/semble/cli.py @@ -67,6 +67,7 @@ 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") diff --git a/src/semble/utils.py b/src/semble/utils.py index f6ee52a5f..640b0c42f 100644 --- a/src/semble/utils.py +++ b/src/semble/utils.py @@ -22,6 +22,7 @@ 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: diff --git a/tests/test_cli.py b/tests/test_cli.py index 85fe080f4..3e6c2b46e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -21,7 +21,7 @@ ], ) def test_main_calls_asyncio_run(argv: list[str], monkeypatch: pytest.MonkeyPatch) -> None: - """main() delegates to asyncio.run(serve(...)), tolerating streams without reconfigure().""" + """main() delegates to asyncio.run(serve(...)) when no CLI subcommand is given.""" monkeypatch.setattr(sys, "argv", argv) monkeypatch.setattr(sys, "stdout", object()) monkeypatch.setattr(sys, "stderr", object()) From fdd4f6673c50f2b429247e8a0b7de3b316478732 Mon Sep 17 00:00:00 2001 From: Pringled Date: Wed, 5 Aug 2026 11:08:33 +0200 Subject: [PATCH 3/5] tests: Update test --- tests/test_cli.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 3e6c2b46e..e4245f600 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -23,8 +23,6 @@ def test_main_calls_asyncio_run(argv: list[str], monkeypatch: pytest.MonkeyPatch) -> None: """main() delegates to asyncio.run(serve(...)) when no CLI subcommand is given.""" monkeypatch.setattr(sys, "argv", argv) - monkeypatch.setattr(sys, "stdout", object()) - monkeypatch.setattr(sys, "stderr", object()) with patch("asyncio.run") as mock_run: mock_run.side_effect = lambda coro: coro.close() main() From 19a67600b7910b8fe71f0443f65db9224410e550 Mon Sep 17 00:00:00 2001 From: Pringled Date: Wed, 5 Aug 2026 11:09:13 +0200 Subject: [PATCH 4/5] chore: Bump version --- src/semble/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/semble/version.py b/src/semble/version.py index ef409fb07..5073cce70 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__)) From b098a7b878854fe23697f254f28bdb89f6ea02e2 Mon Sep 17 00:00:00 2001 From: Pringled Date: Thu, 6 Aug 2026 07:25:21 +0200 Subject: [PATCH 5/5] update docs --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 87901f15d..6d1c64893 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). +