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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ semble install
- **MCP server**: lets the agent call Semble directly as a tool.
- **Instructions**: adds CLI usage guidance to AGENTS.md / CLAUDE.md.
- **Sub-agent**: installs a dedicated `semble-search` sub-agent.
- **Skill**: installs a `semble` skill plus a one-line pointer to it in AGENTS.md / CLAUDE.md, instead of the full instructions.

To undo the setup, run `semble uninstall`.

Expand All @@ -71,7 +72,7 @@ For sandboxed or scripted environments, skip the prompts with `--agent` and, opt
semble install --agent claude --type mcp subagent --yes
```

`--agent` accepts one or more agent ids (e.g. `claude`, `codex`, `pi`); `--type` accepts `mcp`, `instructions`, `subagent`, or `all` (default: all); `--yes` skips the confirmation prompt (requires `--agent` for a fully non-interactive run).
`--agent` accepts one or more agent ids (e.g. `claude`, `codex`, `pi`); `--type` accepts `mcp`, `instructions`, `subagent`, `skill`, or `all` (default: all but `skill`); `--yes` skips the confirmation prompt (requires `--agent` for a fully non-interactive run).

</details>

Expand Down
23 changes: 21 additions & 2 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

## Recommended: `semble install`

The interactive installer detects your installed agents and configures any combination of three integrations globally:
The interactive installer detects your installed agents and configures any combination of four integrations globally:

- **[MCP server](#mcp-server)**: exposes Semble as a native tool your agent can call directly.
- **[AGENTS.md](#instructions-agentsmd--claudemd)**: adds a Semble usage guide to the agent's config file (`CLAUDE.md`, `AGENTS.md`, etc.).
- **[Sub-agent](#sub-agent)**: installs a dedicated `semble-search` sub-agent for harnesses that support it.
- **[Skill](#skill)**: installs a `semble` skill plus a one-line pointer to it in AGENTS.md / CLAUDE.md, as an alternative to the full block. Off by default.

Install the CLI with [uv](https://docs.astral.sh/uv/getting-started/installation/), then run:

Expand Down Expand Up @@ -34,7 +35,7 @@ semble install --agent claude pi --type mcp subagent --yes
```

- `--agent` — one or more agent ids (see the list above; use the lowercase form, e.g. `claude`, `codex`, `pi`).
- `--type` — one or more of `mcp`, `instructions`, `subagent`, or `all` (default: all). Requires `--agent`.
- `--type` — one or more of `mcp`, `instructions`, `subagent`, `skill`, or `all` (default: all but `skill`). Requires `--agent`.
- `-y`/`--yes` — skip the confirmation prompt. Requires `--agent` for a fully non-interactive run.

`semble uninstall` accepts the same flags.
Expand Down Expand Up @@ -479,3 +480,21 @@ For harnesses that support sub-agents (Claude Code, Cursor, Gemini CLI, Kiro, Op
| Grok Build | `grok.md` | `~/.grok/agents/semble-search.md` |
| Qwen Code | `qwen.md` | `~/.qwen/agents/semble-search.md` |
| Kilo Code | `kilo.md` | `~/.config/kilo/agents/semble-search.md` |

### Skill

For harnesses that support [Agent Skills](https://agentskills.io), you can install a `semble` skill instead of the AGENTS.md block. Copy [`src/semble/agents/skill.md`](../src/semble/agents/skill.md) to the destination below as `SKILL.md`:

| Agent | Destination |
|---|---|
| Claude Code | `~/.claude/skills/semble/SKILL.md` |
| Qwen Code | `~/.qwen/skills/semble/SKILL.md` |
| Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode, Pi, Kilo Code | `~/.agents/skills/semble/SKILL.md` |

`~/.agents/skills` is shared between agents, so uninstalling the skill for one of them removes it for all of them.

Agents rarely load a skill on their own, so also add this line to your AGENTS.md / CLAUDE.md (`semble install` does this for you):

```markdown
Use the `semble` skill for code search instead of Grep or Glob.
```
2 changes: 2 additions & 0 deletions src/semble/agents/kilo.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ mode: subagent
permission:
bash: allow
read: allow
glob: deny
grep: deny
---

Use `semble search` to find code by describing what it does or naming a symbol/identifier, instead of grep:
Expand Down
2 changes: 2 additions & 0 deletions src/semble/agents/opencode.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ mode: subagent
permission:
bash: allow
read: allow
glob: deny
grep: deny
---

Use `semble search` to find code by describing what it does or naming a symbol/identifier, instead of grep:
Expand Down
45 changes: 45 additions & 0 deletions src/semble/agents/skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
name: semble
description: Code search for the current repository. Use this before Grep, Glob or find whenever you need to locate code: where something is implemented, which file defines a function, class or symbol, how a feature works, or where docs and config live.
---

Use `semble search` to find code by describing what it does or naming a symbol/identifier, instead of grep:

```bash
semble search "authentication flow" ./my-project --max-snippet-lines 10 # first 10 lines only, concise
semble search "save_pretrained" ./my-project # full chunk content
semble search "save model to disk" ./my-project --top-k 10 # more results
```

Results are cached automatically on first run and invalidated when files change.

Use `--content docs` to search documentation and prose, `--content config` for config files (yaml, toml, etc.), or `--content all` to search code, docs, and config:

```bash
semble search "deployment guide" ./my-project --content docs
semble search "database host port" ./my-project --content config
semble search "authentication" ./my-project --content all
```

Use `semble find-related` to discover code similar to a known location (pass `file_path` and `line` from a prior search result):

```bash
semble find-related src/auth.py 42 ./my-project
```

`path` defaults to the current directory when omitted; git URLs are accepted. Pass several paths or URLs to search related repos together, e.g. when this project calls a service defined in a sibling repo. Result paths are then prefixed with the repo name and the output includes a `repos` map from prefix to location:

```bash
semble search "invoice endpoint" ./service-a ../service-b
```

If `semble` is not on `$PATH`, use `uvx --from "semble[mcp]" semble` in its place.

### Workflow

1. Start with `semble search` to find relevant chunks. The index is built and cached automatically.
2. Use `--content docs` for documentation, `--content config` for config files, or `--content all` for everything.
3. Navigate directly to the returned file and line. Do not re-search or grep for the same content.
4. Optionally use `semble find-related` with a promising result's `file_path` and `line` to discover related implementations.
5. If the answer may live in a dependent or sibling repo, pass all relevant repo paths to one `semble search` call.
6. Use grep only when you need every occurrence of a literal string across the whole repo (e.g., all callers of a renamed function).
10 changes: 8 additions & 2 deletions src/semble/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ def _cli_main() -> None:
nargs="+",
choices=[*(t.value for t in IntegrationType), "all"],
metavar="TYPE",
help="Integrations to include (mcp, instructions, subagent, or all). Default: all. Requires --agent.",
help="Integrations to include (mcp, instructions, subagent, skill, or all). Default: all but skill. Requires --agent.",
)
p.add_argument(
"-y",
Expand All @@ -351,7 +351,13 @@ def _cli_main() -> None:

from semble.installer import run

integration_ids = None if not args.type or "all" in args.type else [IntegrationType(t) for t in args.type]
integration_ids = (
None
if not args.type
else list(IntegrationType)
if "all" in args.type
else [IntegrationType(t) for t in args.type]
)
run(args.command, agent_ids=args.agent, integration_ids=integration_ids, yes=args.yes)
elif args.command == "clear":
_run_clear(args.type)
Expand Down
17 changes: 17 additions & 0 deletions src/semble/installer/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class IntegrationType(str, Enum):
MCP = "mcp"
INSTRUCTIONS = "instructions"
SUBAGENT = "subagent"
SKILL = "skill"


SEMBLE_START = "<!-- SEMBLE_START -->"
Expand Down Expand Up @@ -125,6 +126,8 @@ def semble_pin() -> str:
{SEMBLE_END}
"""

SKILL_POINTER = f"{SEMBLE_START}\nUse the `semble` skill for code search instead of Grep or Glob.\n{SEMBLE_END}\n"


@dataclass(frozen=True)
class McpConfig:
Expand Down Expand Up @@ -155,6 +158,11 @@ class AgentTarget:
mcp: McpConfig | None
instructions_path: Path | None # None = not supported for this agent
subagent_path: Path | None = None # global (user-level) sub-agent file; None = unsupported
skill_path: Path | None = None # global (user-level) SKILL.md; None = unsupported


# Cross-agent skills directory, read by Codex, Cursor, Gemini CLI, Copilot, OpenCode, Pi and Kilo.
_SHARED_SKILL_PATH = _HOME / ".agents" / "skills" / "semble" / "SKILL.md"


def _xdg_config_dir(app: str) -> Path:
Expand Down Expand Up @@ -191,6 +199,7 @@ def _vscode_mcp_path() -> Path:
mcp=McpConfig(_HOME / ".claude.json", "mcpServers", _STDIO_SERVER_CONFIG),
instructions_path=_HOME / ".claude" / "CLAUDE.md",
subagent_path=_HOME / ".claude" / "agents" / "semble-search.md",
skill_path=_HOME / ".claude" / "skills" / "semble" / "SKILL.md",
),
AgentTarget(
id="cursor",
Expand All @@ -200,6 +209,7 @@ def _vscode_mcp_path() -> Path:
mcp=McpConfig(_HOME / ".cursor" / "mcp.json", "mcpServers", _STDIO_SERVER_CONFIG),
instructions_path=None, # Cursor instructions are project-local .mdc files
subagent_path=_HOME / ".cursor" / "agents" / "semble-search.md",
skill_path=_SHARED_SKILL_PATH,
),
AgentTarget(
id="gemini",
Expand All @@ -209,6 +219,7 @@ def _vscode_mcp_path() -> Path:
mcp=McpConfig(_HOME / ".gemini" / "settings.json", "mcpServers", _STDIO_SERVER_CONFIG),
instructions_path=_HOME / ".gemini" / "GEMINI.md",
subagent_path=_HOME / ".gemini" / "agents" / "semble-search.md",
skill_path=_SHARED_SKILL_PATH,
),
AgentTarget(
id="kiro",
Expand All @@ -227,6 +238,7 @@ def _vscode_mcp_path() -> Path:
mcp=McpConfig(_xdg_jsonc_path("opencode"), "mcp", _OPENCODE_SERVER_CONFIG),
instructions_path=_xdg_config_dir("opencode") / "AGENTS.md",
subagent_path=_xdg_config_dir("opencode") / "agents" / "semble-search.md",
skill_path=_SHARED_SKILL_PATH,
),
AgentTarget(
id="copilot",
Expand All @@ -236,6 +248,7 @@ def _vscode_mcp_path() -> Path:
mcp=McpConfig(_HOME / ".copilot" / "mcp-config.json", "mcpServers", _BARE_STDIO_SERVER_CONFIG),
instructions_path=None,
subagent_path=_HOME / ".copilot" / "agents" / "semble-search.agent.md",
skill_path=_SHARED_SKILL_PATH,
),
AgentTarget(
id="codex",
Expand All @@ -245,6 +258,7 @@ def _vscode_mcp_path() -> Path:
mcp=McpConfig(_HOME / ".codex" / "config.toml", "mcp_servers", _STDIO_SERVER_CONFIG, format="toml"),
instructions_path=_HOME / ".codex" / "AGENTS.md",
subagent_path=_HOME / ".codex" / "agents" / "semble-search.toml",
skill_path=_SHARED_SKILL_PATH,
),
AgentTarget(
id="zcode",
Expand Down Expand Up @@ -299,6 +313,7 @@ def _vscode_mcp_path() -> Path:
mcp=McpConfig(_HOME / ".pi" / "agent" / "mcp.json", "mcpServers", _BARE_STDIO_SERVER_CONFIG),
instructions_path=None,
subagent_path=_HOME / ".pi" / "agents" / "semble-search.md",
skill_path=_SHARED_SKILL_PATH,
),
AgentTarget(
id="commandcode",
Expand Down Expand Up @@ -335,6 +350,7 @@ def _vscode_mcp_path() -> Path:
mcp=McpConfig(_HOME / ".qwen" / "settings.json", "mcpServers", _STDIO_SERVER_CONFIG),
instructions_path=_HOME / ".qwen" / "QWEN.md",
subagent_path=_HOME / ".qwen" / "agents" / "semble-search.md",
skill_path=_HOME / ".qwen" / "skills" / "semble" / "SKILL.md",
),
AgentTarget(
id="cline",
Expand All @@ -356,6 +372,7 @@ def _vscode_mcp_path() -> Path:
mcp=McpConfig(_xdg_jsonc_path("kilo"), "mcp", _OPENCODE_SERVER_CONFIG),
instructions_path=_xdg_config_dir("kilo") / "AGENTS.md",
subagent_path=_xdg_config_dir("kilo") / "agents" / "semble-search.md",
skill_path=_SHARED_SKILL_PATH,
),
]

Expand Down
61 changes: 50 additions & 11 deletions src/semble/installer/installer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import sys
from contextlib import suppress
from dataclasses import dataclass
from importlib.resources import files
from pathlib import Path
Expand All @@ -12,6 +13,7 @@
AGENTS,
INSTRUCTIONS,
SEMBLE_PIN,
SKILL_POINTER,
AgentTarget,
IntegrationType,
Mode,
Expand Down Expand Up @@ -41,13 +43,14 @@

@dataclass(frozen=True)
class _Integration:
"""Descriptor for one installer integration (MCP server, instructions, sub-agent)."""
"""Descriptor for one installer integration (MCP server, instructions, sub-agent, skill)."""

id: IntegrationType
label: str
desc: str
apply: Callable[[AgentTarget, Mode], WriteResult | None]
plan_path: Callable[[AgentTarget], Path | None]
default: bool = True # selected when the user doesn't choose integrations


def merge_mcp(agent: AgentTarget) -> WriteResult:
Expand Down Expand Up @@ -83,11 +86,8 @@ def _apply_instructions(agent: AgentTarget, mode: Mode) -> WriteResult | None:
return WriteResult(path, action)


def _apply_subagent(agent: AgentTarget, mode: Mode) -> WriteResult | None:
"""Apply or remove the global sub-agent file for one agent."""
dest = agent.subagent_path
if dest is None:
return None
def _sync_file(dest: Path, mode: Mode, render: Callable[[], str]) -> WriteResult:
"""Write the rendered file to dest on install, or delete it on uninstall."""
if mode == "uninstall":
if not dest.exists():
return WriteResult(dest, "not-found")
Expand All @@ -96,9 +96,8 @@ def _apply_subagent(agent: AgentTarget, mode: Mode) -> WriteResult | None:
existed = dest.exists()
dest.parent.mkdir(parents=True, exist_ok=True)
try:
src = files("semble").joinpath(f"agents/{agent.id}{dest.suffix}").read_text(encoding="utf-8")
# Bytes on both sides: no newline translation on Windows, and an undecodable file is just stale.
content = src.replace('"semble[mcp]"', f'"{SEMBLE_PIN}"').encode("utf-8")
content = render().encode("utf-8")
if existed and dest.read_bytes() == content:
return WriteResult(dest, "unchanged")
dest.write_bytes(content)
Expand All @@ -107,6 +106,38 @@ def _apply_subagent(agent: AgentTarget, mode: Mode) -> WriteResult | None:
return WriteResult(dest, "updated" if existed else "created")


def _bundled(name: str) -> str:
"""Read a bundled agent file, pinning its uvx fallback to the installed semble version."""
src = files("semble").joinpath(f"agents/{name}").read_text(encoding="utf-8")
return src.replace('"semble[mcp]"', f'"{SEMBLE_PIN}"')


def _apply_subagent(agent: AgentTarget, mode: Mode) -> WriteResult | None:
"""Apply or remove the global sub-agent file for one agent."""
dest = agent.subagent_path
if dest is None:
return None
return _sync_file(dest, mode, lambda: _bundled(f"{agent.id}{dest.suffix}"))


def _apply_skill(agent: AgentTarget, mode: Mode) -> WriteResult | None:
"""Apply or remove the global semble skill for one agent."""
dest = agent.skill_path
if dest is None:
return None
result = _sync_file(dest, mode, lambda: _bundled("skill.md"))
if result.action == "removed":
with suppress(OSError): # leave the skill directory if anything else is in it
dest.parent.rmdir()
# Agents rarely load a skill unprompted, so point to it from the instructions file.
pointer = agent.instructions_path
if pointer is not None and mode == "install":
replace_or_append_marked(pointer, SKILL_POINTER)
elif pointer is not None and pointer.exists() and SKILL_POINTER in pointer.read_text(encoding="utf-8"):
remove_marked(pointer) # leave a full instructions block alone
return result


_INTEGRATIONS: list[_Integration] = [
_Integration(
IntegrationType.MCP,
Expand All @@ -115,6 +146,14 @@ def _apply_subagent(agent: AgentTarget, mode: Mode) -> WriteResult | None:
_apply_mcp,
lambda a: a.mcp.path if a.mcp else None,
),
_Integration(
IntegrationType.SKILL,
"Skill",
"installs a semble skill, with a one-line pointer in AGENTS.md / CLAUDE.md",
_apply_skill,
lambda a: a.skill_path,
default=False, # alternative to the instructions block; listed first so a chosen full block wins
),
_Integration(
IntegrationType.INSTRUCTIONS,
"Instructions",
Expand Down Expand Up @@ -203,7 +242,7 @@ def run(
"""Install or uninstall semble across coding agents.

Prompts interactively unless `agent_ids` is given, in which case it runs unattended
against those agents (and `integration_ids`, or all integrations if omitted).
against those agents (and `integration_ids`, or the default integrations if omitted).
"""
install = mode == "install"
print(f"\n {_BOLD}{'Semble Installer' if install else 'Semble Uninstaller'}{_RESET}\n")
Expand All @@ -213,7 +252,7 @@ def run(
chosen_integrations = (
[i for i in _INTEGRATIONS if i.id in integration_ids]
if integration_ids is not None
else list(_INTEGRATIONS)
else [i for i in _INTEGRATIONS if i.default]
) or _exit("No matching integrations. Exiting.", code=1)
else:
agent_items = [
Expand All @@ -225,7 +264,7 @@ def run(
) or _exit("Nothing selected. Exiting.")

max_label = max(len(i.label) for i in _INTEGRATIONS)
integ_items = [(f"{i.label:<{max_label}} — {i.desc}", i, True) for i in _INTEGRATIONS]
integ_items = [(f"{i.label:<{max_label}} — {i.desc}", i, i.default) for i in _INTEGRATIONS]
chosen_integrations = _checkbox(
f"Select integrations to {'enable' if install else 'remove'}:", integ_items
) or _exit("Nothing selected. Exiting.")
Expand Down
Loading
Loading