Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@

</div>

Semble is a code search library built for agents. It returns the exact code snippets they need instantly, using ~99% fewer tokens than grep+read. Indexing and searching a full codebase end-to-end takes under a second, matching the retrieval quality of a code-specialized transformer while indexing ~340x faster and querying ~17x faster (see [benchmarks](#benchmarks)). Everything runs on CPU with no API keys, GPU, or external services. Use it as an MCP server, a CLI tool via AGENTS.md, or a dedicated sub-agent, and any coding agent (Claude Code, Cursor, Codex, OpenCode, etc.) gets instant access to any repo.
Semble is a code search library built for agents. It returns the exact code snippets they need instantly, using ~99% fewer tokens than grep+read. Indexing and searching a full codebase end-to-end takes under a second for most repos, matching the retrieval quality of a code-specialized transformer while indexing ~340x faster and querying ~17x faster (see [benchmarks](#benchmarks)). Everything runs on CPU with no API keys, GPU, or external services. Use it as an MCP server, a CLI tool via AGENTS.md, or a dedicated sub-agent, and any coding agent (Claude Code, Cursor, Codex, OpenCode, etc.) gets instant access to any repo.

<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/MinishLab/semble/main/assets/images/demo-dark.gif">
<img src="https://raw.githubusercontent.com/MinishLab/semble/main/assets/images/demo-light.gif" width="1000" alt="semble install detecting Claude Code, Cursor, and Codex, then semble search returning ranked code snippets from pydantic" />
</picture>

## Quickstart

Expand Down
Binary file added assets/images/demo-dark.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/demo-light.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions scripts/demo/dark.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
Output "assets/images/demo-dark.gif"

Set Shell "bash"
Set FontSize 18
Set Width 1200
Set Height 672
Set Theme "Catppuccin Mocha"
Set Padding 20
Set Margin 30
Set MarginFill "#11111b"
Set BorderRadius 12
Set WindowBar Colorful
Set WindowBarSize 36
Set TypingSpeed 45ms

Hide
Type "exec env -i HOME=$DEMO_HOME PATH=$SEMBLE_BIN_DIR:/usr/bin:/bin TERM=xterm-256color /bin/bash --norc --noprofile"
Enter
Sleep 800ms
Type "cd $HOME; PS1='$ '; clear"
Enter
Sleep 300ms
Show

Type "semble install"
Enter
Sleep 1200ms
Enter
Sleep 500ms
Enter
Sleep 500ms
Enter
Sleep 2200ms
Comment thread
Pringled marked this conversation as resolved.

Hide
Type "clear"
Enter
Sleep 300ms
Show

Type "cd pydantic"
Enter
Sleep 600ms

Type@25ms `semble search "Where is snake_case converted to camelCase?" -k 1 --format text`
Enter
Wait@60s /\$\s*$/
Sleep 3500ms
48 changes: 48 additions & 0 deletions scripts/demo/light.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
Output "assets/images/demo-light.gif"

Set Shell "bash"
Set FontSize 18
Set Width 1200
Set Height 672
Set Theme "Catppuccin Latte"
Set Padding 20
Set Margin 30
Set MarginFill "#eff1f5"
Set BorderRadius 12
Set WindowBar Colorful
Set WindowBarSize 36
Set TypingSpeed 45ms

Hide
Type "exec env -i HOME=$DEMO_HOME PATH=$SEMBLE_BIN_DIR:/usr/bin:/bin TERM=xterm-256color /bin/bash --norc --noprofile"
Enter
Sleep 800ms
Type "cd $HOME; PS1='$ '; clear"
Enter
Sleep 300ms
Show

Type "semble install"
Enter
Sleep 1200ms
Enter
Sleep 500ms
Enter
Sleep 500ms
Enter
Sleep 2200ms

Hide
Type "clear"
Enter
Sleep 300ms
Show

Type "cd pydantic"
Enter
Sleep 600ms

Type@25ms `semble search "Where is snake_case converted to camelCase?" -k 1 --format text`
Enter
Wait@60s /\$\s*$/
Sleep 3500ms
34 changes: 34 additions & 0 deletions scripts/demo/record.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Records the README demo GIFs (assets/images/demo-{dark,light}.gif) from dark.tape and light.tape.
set -euo pipefail

cd "$(dirname "$0")/../.."
command -v vhs >/dev/null || { echo "vhs not found, install it with: brew install vhs" >&2; exit 1; }
[ -x .venv/bin/semble ] || { echo ".venv/bin/semble not found, run: make install" >&2; exit 1; }
export SEMBLE_BIN_DIR="$PWD/.venv/bin"

seed=$(mktemp -d)
dark=$(mktemp -d)
light=$(mktemp -d)
trap 'rm -rf "$seed" "$dark" "$light"' EXIT

# Create an isolated $HOME with agent config dirs so `semble install` detects some agents.
mkdir -p "$seed"/.claude "$seed"/.cursor "$seed"/.codex
git -c advice.detachedHead=false clone -q --depth 1 --branch v2.13.5 https://github.com/pydantic/pydantic "$seed/pydantic"
# Warm the model cache so the recording doesn't hit the network or print an HF Hub warning.
env -i HOME="$seed" PATH="$SEMBLE_BIN_DIR:/usr/bin:/bin" "$SEMBLE_BIN_DIR/semble" search x "$seed/pydantic" -k 1 >/dev/null

# Record the demo for both dark and light themes. Each home gets its own agent dirs and repo copy, but shares the
# seed's .cache: grammar dylibs load slowly the first time from a new path, which stalls the start of indexing.
for home in "$dark" "$light"; do
mkdir -p "$home"/.claude "$home"/.cursor "$home"/.codex
cp -R "$seed/pydantic" "$home/pydantic"
ln -s "$seed/.cache" "$home/.cache"
done
DEMO_HOME="$dark" vhs scripts/demo/dark.tape >/dev/null &
dark_pid=$!
DEMO_HOME="$light" vhs scripts/demo/light.tape >/dev/null &
light_pid=$!
wait "$dark_pid"
wait "$light_pid"
Comment thread
Pringled marked this conversation as resolved.
echo "Recorded assets/images/demo-dark.gif and assets/images/demo-light.gif"
9 changes: 8 additions & 1 deletion src/semble/index/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,14 @@ def create_index_from_path(

files = list(walk_files(path, resolved_extensions))
for file_path in tqdm(
files, desc="Indexing", unit="file", file=sys.stderr, leave=False, colour="green", disable=not show_progress_bar
files,
desc="Indexing",
unit="file",
file=sys.stderr,
leave=False,
colour="green",
miniters=1, # tqdm's adaptive miniters stalls the bar when fast files are followed by slow ones
disable=not show_progress_bar,
):
language = detect_language(file_path)
with contextlib.suppress(OSError):
Expand Down
12 changes: 9 additions & 3 deletions src/semble/installer/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,21 @@ def _checkbox(prompt: str, items: Sequence[tuple[str, _T, bool]]) -> list[_T] |
return questionary.checkbox(prompt, choices=choices, style=style, instruction=instruction).ask()


def _display_path(path: Path) -> str:
"""Return path with the home directory shortened to ~, keeping installer output readable."""
home = Path.home()
return str(Path("~") / path.relative_to(home)) if path.is_relative_to(home) else str(path)


def _print_plan(agents: list[AgentTarget], integrations: list[_Integration]) -> None:
"""Print what will be written or removed for each selected agent and integration."""
print(f"\n {_BOLD}Plan:{_RESET}\n")
for agent in agents:
print(f" {_BOLD}{agent.display_name}{_RESET}")
for integ in integrations:
path = integ.plan_path(agent)
ok = path is not None
print(f" {integ.label:<13} {_tick(ok)} {path if ok else '(not supported)'}")
shown = _display_path(path) if path is not None else "(not supported)"
print(f" {integ.label:<13} {_tick(path is not None)} {shown}")
print()


Expand All @@ -180,7 +186,7 @@ def _apply(mode: Mode, agents: list[AgentTarget], integrations: list[_Integratio
ok = result.action in ("created", "updated", "removed", "unchanged")
detail = _ACTION_DETAIL.get(result.action, "")
suffix = f" — {detail}" if detail else ""
print(f" {_tick(ok)} {integ.id.value} ({result.action}){suffix} → {result.path}")
print(f" {_tick(ok)} {integ.id.value} ({result.action}){suffix} → {_display_path(result.path)}")
print()


Expand Down
8 changes: 6 additions & 2 deletions tests/test_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,13 +520,17 @@ def ask(self):
assert _checkbox("Pick:", [("Option A", "a", False)]) == ["a"]


def test_print_plan(capsys, claude_agent):
"""_print_plan prints each agent, integration, and resolved path (or 'not supported')."""
@pytest.mark.parametrize("under_home", [True, False])
def test_print_plan(capsys, claude_agent, monkeypatch, tmp_path, under_home):
"""_print_plan prints each agent, integration, and path (shortened to ~ under home, or 'not supported')."""
monkeypatch.setattr(Path, "home", lambda: tmp_path if under_home else tmp_path / "elsewhere")
no_mcp = replace(claude_agent, display_name="No MCP Agent", mcp=None)
_print_plan([claude_agent, no_mcp], _INTEGRATIONS)
out = capsys.readouterr().out
assert "Claude Code" in out
assert "not supported" in out # no_mcp has no MCP
expected = Path("~") / ".claude" / "CLAUDE.md" if under_home else claude_agent.instructions_path
assert str(expected) in out


def test_run_completes(run_setup, monkeypatch, capsys):
Expand Down
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading