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
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,22 @@ Or add to your project's `.mcp.json`:
}
```

Then copy `.claude/rules/adloop.md` and `.claude/commands/` from this repo into your project for orchestration rules and slash commands.
Then install the orchestration rules + slash commands globally so every Claude Code session inherits them:

```bash
adloop install-rules
```

This writes a managed block to `~/.claude/CLAUDE.md` and copies the slash commands (prefixed `adloop-*`) into `~/.claude/commands/`. The block is delimited by sentinel comments so it's safe to run multiple times — re-running just refreshes the content. Two install modes:

- **inline** (default) — full rules embedded in `~/.claude/CLAUDE.md`. Reliable but adds ~10K tokens to every Claude Code session.
- **lazy** (`adloop install-rules --lazy`) — small directive in `CLAUDE.md` pointing at `~/.claude/rules/adloop.md`. Cheaper baseline cost; the LLM reads the rules file only when AdLoop tools are in scope.

To refresh after upgrading AdLoop: `adloop update-rules`. To remove cleanly: `adloop uninstall-rules` — only the managed block and `adloop-*` commands are touched, never your own content.

If you'd rather manage things by hand instead, copy `.claude/rules/adloop.md` and `.claude/commands/` from this repo into your project's `.claude/` directory.

**Claude Desktop / claude.ai** has no programmatic rules location. Run `adloop install-rules` and it will print the rules content for you to paste into Project settings → Custom instructions on claude.ai.

</details>

Expand Down
72 changes: 63 additions & 9 deletions scripts/sync-rules.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
#!/usr/bin/env python3
"""Sync orchestration rules from Cursor format to Claude Code format.
"""Sync orchestration rules + slash commands into all canonical locations.

Reads .cursor/rules/adloop.mdc (canonical source), strips Cursor-specific
frontmatter, prepends Claude Code frontmatter, and writes to
.claude/rules/adloop.md.
Reads ``.cursor/rules/adloop.mdc`` (the canonical, Cursor-flavoured source),
strips Cursor frontmatter, prepends Claude Code frontmatter, and writes the
result to:

Run this after editing adloop.mdc to keep both files in sync.
- ``.claude/rules/adloop.md`` — for Claude Code in *this* repo
- ``src/adloop/rules/adloop.md`` — bundled with the wheel for
``adloop install-rules`` to install
globally on user machines

Also copies ``.claude/commands/*.md`` into ``src/adloop/rules/commands/`` so
slash commands ship with the package.

Run this after editing ``adloop.mdc`` or any command file.
"""

from __future__ import annotations

import shutil
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent
CURSOR_RULES = REPO_ROOT / ".cursor" / "rules" / "adloop.mdc"
CLAUDE_RULES = REPO_ROOT / ".claude" / "rules" / "adloop.md"
CLAUDE_COMMANDS_DIR = REPO_ROOT / ".claude" / "commands"

PACKAGE_RULES_DIR = REPO_ROOT / "src" / "adloop" / "rules"
PACKAGE_RULES = PACKAGE_RULES_DIR / "adloop.md"
PACKAGE_COMMANDS_DIR = PACKAGE_RULES_DIR / "commands"

CLAUDE_FRONTMATTER = """\
---
Expand All @@ -29,17 +45,55 @@ def extract_body(content: str) -> str:
return content[end + 3:].lstrip("\n")


def main() -> None:
def sync_rules() -> str:
"""Sync the rules content. Returns the rendered Claude-format content."""
if not CURSOR_RULES.exists():
raise FileNotFoundError(f"Canonical rules not found: {CURSOR_RULES}")

body = extract_body(CURSOR_RULES.read_text())
rendered = CLAUDE_FRONTMATTER + "\n" + body

CLAUDE_RULES.parent.mkdir(parents=True, exist_ok=True)
CLAUDE_RULES.write_text(CLAUDE_FRONTMATTER + "\n" + body)
CLAUDE_RULES.write_text(rendered)

PACKAGE_RULES_DIR.mkdir(parents=True, exist_ok=True)
PACKAGE_RULES.write_text(rendered)

return rendered


def sync_commands() -> int:
"""Mirror .claude/commands/*.md into the package. Returns count copied."""
if not CLAUDE_COMMANDS_DIR.is_dir():
return 0

PACKAGE_COMMANDS_DIR.mkdir(parents=True, exist_ok=True)

# Remove stale commands that no longer exist in the source
source_names = {p.name for p in CLAUDE_COMMANDS_DIR.glob("*.md")}
for stale in PACKAGE_COMMANDS_DIR.glob("*.md"):
if stale.name not in source_names:
stale.unlink()

count = 0
for cmd in CLAUDE_COMMANDS_DIR.glob("*.md"):
shutil.copy2(cmd, PACKAGE_COMMANDS_DIR / cmd.name)
count += 1
return count


def main() -> None:
sync_rules()
cmd_count = sync_commands()

print(f"Synced: {CURSOR_RULES.relative_to(REPO_ROOT)}")
print(f" -> {CLAUDE_RULES.relative_to(REPO_ROOT)}")
print(f"Synced rules: {CURSOR_RULES.relative_to(REPO_ROOT)}")
print(f" -> {CLAUDE_RULES.relative_to(REPO_ROOT)}")
print(f" -> {PACKAGE_RULES.relative_to(REPO_ROOT)}")
print(
f"Synced {cmd_count} command(s):"
f" {CLAUDE_COMMANDS_DIR.relative_to(REPO_ROOT)}"
)
print(f" -> {PACKAGE_COMMANDS_DIR.relative_to(REPO_ROOT)}")


if __name__ == "__main__":
Expand Down
32 changes: 25 additions & 7 deletions src/adloop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,40 @@
def main() -> None:
"""Entry point for `adloop` console script.

Routes to the setup wizard when called as ``adloop init``,
otherwise starts the MCP server.
Subcommands:
adloop Start the MCP server (default).
adloop init Run the interactive setup wizard.
adloop install-rules Install Claude orchestration rules globally.
adloop update-rules Refresh the installed rules block.
adloop uninstall-rules Remove the installed rules block + commands.
adloop --version, -V Print version and exit.
"""
if len(sys.argv) > 1 and sys.argv[1] in ("--version", "-V"):
args = sys.argv[1:]

if args and args[0] in ("--version", "-V"):
print(f"adloop {__version__}")
return

if len(sys.argv) > 1 and sys.argv[1] == "init":
if args and args[0] == "init":
from adloop.cli import run_init_wizard

try:
run_init_wizard()
except KeyboardInterrupt:
print("\n\n Setup cancelled.\n")
sys.exit(130)
else:
from adloop.server import mcp
return

if args and args[0] in ("install-rules", "update-rules", "uninstall-rules"):
from adloop.cli import run_rules_command

try:
sys.exit(run_rules_command(args[0], args[1:]))
except KeyboardInterrupt:
print("\n\n Cancelled.\n")
sys.exit(130)
return

from adloop.server import mcp

mcp.run()
mcp.run()
115 changes: 114 additions & 1 deletion src/adloop/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,10 +580,123 @@ def _run_wizard_post_config(
for line in claude_snippet.splitlines():
_print(f" {line}")
_print()
_print(" Then copy .claude/rules/adloop.md and .claude/commands/ into your project.")
_print(" (Or run `adloop install-rules` after this wizard to install")
_print(" rules + slash commands automatically into ~/.claude/.)")

# Offer to install Claude rules globally now if a Claude installation
# is detected. Cursor is intentionally skipped — Cursor handles workspace
# rules natively via .cursor/rules/.
_maybe_offer_global_rules_install()

_print()
_print(" Restart your editor to pick up the MCP server.")
_print()
_print(" ✓ Setup complete!")
_print()


def _maybe_offer_global_rules_install() -> None:
"""If Claude is detected, offer to install rules globally during init."""
from adloop.rules_install import detect_clients, install_rules

clients = detect_clients()
if not clients:
return

_print()
_print(" ── Claude Orchestration Rules ──")
_print()
_print(" Detected Claude installations:")
for c in clients:
_print(f" • {c.display_name}")
_print()
_print(" AdLoop ships orchestration rules that teach Claude how to use")
_print(" these tools safely (43 tools, safety patterns, GAQL reference).")
_print(" Without them, you get raw tool access but no orchestration.")
_print()

if not _prompt_bool("Install rules globally now?", default=True):
_print(" Skipping. Run `adloop install-rules` later if you change your mind.")
return

_print()
_print(" Install mode:")
_print(" 1. inline (default) — full rules in ~/.claude/CLAUDE.md")
_print(" Reliable; loaded every Claude Code session (~10K tokens).")
_print(" 2. lazy — small directive in CLAUDE.md, full rules in")
_print(" ~/.claude/rules/adloop.md, loaded only when AdLoop is active.")
_print(" Cheaper baseline cost; slightly less reliable.")
_print()
raw = input(" Choose [1/2, default 1]: ").strip()
mode = "lazy" if raw == "2" else "inline"

_print()
results = install_rules(mode=mode)
_print_install_results(results)


def _print_install_results(results: list) -> None:
"""Pretty-print install/update/uninstall results."""
if not results:
_print(" No Claude installations detected — nothing to do.")
return
for r in results:
if r.action == "manual":
_print(f" ⚠ {r.client}: manual step required")
_print()
for line in r.instructions.splitlines():
_print(f" {line}")
_print()
continue
target = r.rules_target if r.rules_target else "(none)"
_print(f" ✓ {r.client}: {r.action} → {target}")
if r.commands_installed:
_print(
f" {len(r.commands_installed)} slash command(s) installed"
f" (prefixed adloop-*)"
)
if r.commands_removed:
_print(
f" {len(r.commands_removed)} slash command(s) removed"
)


def run_rules_command(subcommand: str, argv: list[str]) -> int:
"""Entry point for `adloop install-rules / update-rules / uninstall-rules`.

Returns the process exit code (0 = success).
"""
from adloop.rules_install import (
install_rules,
uninstall_rules,
update_rules,
)

mode = "inline"
install_commands = True
if "--lazy" in argv:
mode = "lazy"
if "--no-commands" in argv:
install_commands = False

_print()
if subcommand == "install-rules":
_print(" Installing AdLoop rules...")
results = install_rules(mode=mode, install_commands=install_commands)
elif subcommand == "update-rules":
_print(" Updating AdLoop rules...")
# update-rules preserves the existing mode when no flag is passed.
explicit_mode = mode if "--lazy" in argv or "--inline" in argv else None
results = update_rules(
mode=explicit_mode, install_commands=install_commands # type: ignore[arg-type]
)
elif subcommand == "uninstall-rules":
_print(" Uninstalling AdLoop rules...")
results = uninstall_rules(remove_commands=install_commands)
else:
_print(f" Unknown subcommand: {subcommand}")
return 1

_print_install_results(results)
_print()
return 0
6 changes: 6 additions & 0 deletions src/adloop/rules/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Bundled orchestration rules + slash commands.

Files in this package are written by ``scripts/sync-rules.py`` from the
canonical sources at ``.cursor/rules/adloop.mdc`` and ``.claude/commands/``.
Do not edit the files in this directory directly — they will be overwritten.
"""
Loading