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: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ ai-config-kit/
│ ├── winsurf.py # Windsurf (.windsurf/rules/*.md)
│ ├── codex.py # OpenAI Codex CLI (AGENTS.md sections)
│ ├── copilot.py # GitHub Copilot (.github/instructions/*.md)
│ ├── anteroom.py # Anteroom (ANTEROOM.md sections)
│ └── detector.py # Tool detection logic
├── cli/ # Typer CLI commands
│ ├── main.py # CLI app definition
Expand Down Expand Up @@ -397,6 +398,7 @@ Different IDEs support different component types:
- **Claude Code**: All components (instructions, MCP, hooks, commands, resources)
- **Cline**: Instructions and resources only
- **Codex CLI**: Instructions and resources only (via AGENTS.md sections)
- **Anteroom**: Instructions and resources only (via ANTEROOM.md sections)
- **Cursor**: Instructions and resources only
- **Kiro**: Instructions and resources only
- **Roo Code**: Instructions, MCP, commands, and resources
Expand All @@ -410,6 +412,7 @@ Components are translated to IDE-specific formats:
- **Claude Code**: `.md` files in `.claude/rules/`, `.claude/hooks/`, `.claude/commands/`
- **Cline**: `.md` files in `.clinerules/`
- **Codex CLI**: Sections in `AGENTS.md` at project root (using HTML comment markers)
- **Anteroom**: Sections in `ANTEROOM.md` at project root (using HTML comment markers)
- **Cursor**: `.mdc` files in `.cursor/rules/`
- **Roo Code**: `.md` files in `.roo/rules/`, `.roo/commands/`
- **Kiro**: `.md` files in `.kiro/steering/`
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ devsync install
- **Instructions** -- share coding standards, style guides, and AI prompts from Git repos
- **MCP Servers** -- distribute Model Context Protocol configs with secure credential management
- **Packages** -- bundle instructions, MCP servers, hooks, commands, and resources together
- **22 IDE integrations** -- Claude Code, Cursor, Windsurf, GitHub Copilot, and 18 more
- **23 IDE integrations** -- Claude Code, Cursor, Windsurf, GitHub Copilot, and 19 more
- **Templates** -- IDE-targeted content with slash commands, hooks, and backups
- **Conflict resolution** -- skip, overwrite, or rename when files already exist

Expand Down
216 changes: 216 additions & 0 deletions devsync/ai_tools/anteroom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
"""Anteroom AI tool integration."""

import re
import shutil
from pathlib import Path
from typing import Optional

from devsync.ai_tools.base import AITool
from devsync.core.models import AIToolType, InstallationScope, Instruction

START_MARKER = "<!-- devsync:start:{name} -->"
END_MARKER = "<!-- devsync:end:{name} -->"
SECTION_PATTERN = r"<!-- devsync:start:{name} -->\n.*?\n<!-- devsync:end:{name} -->"


class AnteroomTool(AITool):
"""Integration for Anteroom.

Anteroom reads a single ANTEROOM.md file at the project root.
DevSync manages individual instruction sections using HTML comment markers:

<!-- devsync:start:instruction-name -->
... instruction content ...
<!-- devsync:end:instruction-name -->
"""

@property
def tool_type(self) -> AIToolType:
"""Return the AI tool type identifier."""
return AIToolType.ANTEROOM

@property
def tool_name(self) -> str:
"""Return human-readable tool name."""
return "Anteroom"

def is_installed(self) -> bool:
"""Check if Anteroom is installed on the system.

Returns:
True if aroom binary is found on PATH or ~/.anteroom/ exists
"""
if shutil.which("aroom") is not None:
return True
config_dir = Path.home() / ".anteroom"
return config_dir.exists()

def get_instructions_directory(self) -> Path:
"""Get the directory where instructions should be installed.

Raises:
NotImplementedError: Anteroom only supports project-level installation
"""
raise NotImplementedError(
f"{self.tool_name} global installation is not supported. "
"Anteroom uses project-level ANTEROOM.md only. "
"Please use project-level installation instead (--scope project)."
)

def get_instruction_file_extension(self) -> str:
"""Get the file extension for Anteroom instructions.

Returns:
File extension including the dot
"""
return ".md"

def get_project_instructions_directory(self, project_root: Path) -> Path:
"""Get the directory for project-specific Anteroom instructions.

ANTEROOM.md lives at the project root.

Args:
project_root: Path to the project root directory

Returns:
Path to project root
"""
return project_root

def get_instruction_path(
self,
instruction_name: str,
scope: InstallationScope = InstallationScope.GLOBAL,
project_root: Optional[Path] = None,
) -> Path:
"""Get the path to ANTEROOM.md.

Args:
instruction_name: Name of the instruction (unused for path)
scope: Installation scope (must be PROJECT)
project_root: Project root path

Returns:
Path to ANTEROOM.md

Raises:
ValueError: If scope is PROJECT but project_root is None
NotImplementedError: If scope is GLOBAL
"""
if scope == InstallationScope.GLOBAL:
raise NotImplementedError(
f"{self.tool_name} global installation is not supported. "
"Please use project-level installation instead (--scope project)."
)
if project_root is None:
raise ValueError("project_root is required for PROJECT scope")
return project_root / "ANTEROOM.md"

def instruction_exists(
self,
instruction_name: str,
scope: InstallationScope = InstallationScope.GLOBAL,
project_root: Optional[Path] = None,
) -> bool:
"""Check if an instruction section exists in ANTEROOM.md.

Args:
instruction_name: Name of the instruction
scope: Installation scope
project_root: Project root path

Returns:
True if the instruction's section markers exist in ANTEROOM.md
"""
try:
path = self.get_instruction_path(instruction_name, scope, project_root)
if not path.exists():
return False
content = path.read_text(encoding="utf-8")
start = START_MARKER.format(name=instruction_name)
return start in content
except (FileNotFoundError, ValueError, NotImplementedError):
return False

def install_instruction(
self,
instruction: Instruction,
overwrite: bool = False,
scope: InstallationScope = InstallationScope.GLOBAL,
project_root: Optional[Path] = None,
) -> Path:
"""Install an instruction as a section in ANTEROOM.md.

Args:
instruction: Instruction to install
overwrite: Whether to overwrite existing section
scope: Installation scope
project_root: Project root path

Returns:
Path to ANTEROOM.md

Raises:
FileExistsError: If instruction section exists and overwrite=False
"""
path = self.get_instruction_path(instruction.name, scope, project_root)

start = START_MARKER.format(name=instruction.name)
end = END_MARKER.format(name=instruction.name)
section = f"{start}\n{instruction.content}\n{end}"

if path.exists():
content = path.read_text(encoding="utf-8")
if start in content:
if not overwrite:
raise FileExistsError(f"Instruction already exists in ANTEROOM.md: {instruction.name}")
pattern = SECTION_PATTERN.format(name=re.escape(instruction.name))
content = re.sub(pattern, section, content, flags=re.DOTALL)
path.write_text(content, encoding="utf-8")
return path
if content and not content.endswith("\n"):
content += "\n"
content += "\n" + section + "\n"
path.write_text(content, encoding="utf-8")
else:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(section + "\n", encoding="utf-8")

return path

def uninstall_instruction(
self,
instruction_name: str,
scope: InstallationScope = InstallationScope.GLOBAL,
project_root: Optional[Path] = None,
) -> bool:
"""Remove an instruction section from ANTEROOM.md.

Args:
instruction_name: Name of instruction to remove
scope: Installation scope
project_root: Project root path

Returns:
True if section was removed, False if it didn't exist
"""
try:
path = self.get_instruction_path(instruction_name, scope, project_root)
if not path.exists():
return False

content = path.read_text(encoding="utf-8")
start = START_MARKER.format(name=instruction_name)
if start not in content:
return False

pattern = SECTION_PATTERN.format(name=re.escape(instruction_name))
new_content = re.sub(pattern, "", content, flags=re.DOTALL)
new_content = re.sub(r"\n{3,}", "\n\n", new_content).strip()
if new_content:
new_content += "\n"
path.write_text(new_content, encoding="utf-8")
return True
except (FileNotFoundError, ValueError, NotImplementedError):
return False
18 changes: 18 additions & 0 deletions devsync/ai_tools/capability_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,24 @@ def supports_component(self, component_type: ComponentType) -> bool:
"DevSync manages sections within this file using HTML comment markers."
),
),
AIToolType.ANTEROOM: IDECapability(
tool_type=AIToolType.ANTEROOM,
tool_name="Anteroom",
supported_components={
ComponentType.INSTRUCTION,
ComponentType.RESOURCE,
},
instructions_directory="",
instruction_file_extension=".md",
supports_project_scope=True,
supports_global_scope=False,
mcp_config_path=None,
mcp_project_config_path=None,
notes=(
"Anteroom uses a single ANTEROOM.md file at the project root. "
"DevSync manages sections within this file using HTML comment markers."
),
),
AIToolType.COPILOT: IDECapability(
tool_type=AIToolType.COPILOT,
tool_name="GitHub Copilot",
Expand Down
3 changes: 3 additions & 0 deletions devsync/ai_tools/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from devsync.ai_tools.aider import AiderTool
from devsync.ai_tools.amazonq import AmazonQTool
from devsync.ai_tools.amp import AmpTool
from devsync.ai_tools.anteroom import AnteroomTool
from devsync.ai_tools.antigravity import AntigravityTool
from devsync.ai_tools.augment import AugmentTool
from devsync.ai_tools.base import AITool
Expand Down Expand Up @@ -56,6 +57,7 @@ def __init__(self) -> None:
AIToolType.OPENHANDS: OpenHandsTool(),
AIToolType.AMP: AmpTool(),
AIToolType.OPENCODE: OpenCodeTool(),
AIToolType.ANTEROOM: AnteroomTool(),
}

def detect_installed_tools(self) -> list[AITool]:
Expand Down Expand Up @@ -132,6 +134,7 @@ def get_primary_tool(self) -> Optional[AITool]:
AIToolType.OPENHANDS,
AIToolType.AMP,
AIToolType.OPENCODE,
AIToolType.ANTEROOM,
]

for tool_type in priority:
Expand Down
29 changes: 29 additions & 0 deletions devsync/ai_tools/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,34 @@ def translate_mcp_server(self, component: MCPServerComponent, package_root: Path
raise NotImplementedError("OpenCode does not support MCP servers")


class AnteroomTranslator(ComponentTranslator):
"""Translator for Anteroom (ANTEROOM.md at project root)."""

@property
def tool_type(self) -> AIToolType:
return AIToolType.ANTEROOM

def translate_instruction(self, component: InstructionComponent, package_root: Path) -> TranslatedComponent:
"""Translate instruction to Anteroom format with section markers."""
instruction_path = package_root / component.file
with open(instruction_path, "r") as f:
content = f.read()

wrapped = f"<!-- devsync:start:{component.name} -->\n{content}\n<!-- devsync:end:{component.name} -->"

return TranslatedComponent(
component_type=ComponentType.INSTRUCTION,
component_name=component.name,
target_path="ANTEROOM.md",
content=wrapped,
metadata={"section_based": True},
Comment on lines +844 to +846

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Merge section-based Anteroom installs instead of file-skipping

By translating every instruction to the same ANTEROOM.md target, package installs with more than one instruction will currently drop later instructions under default ConflictResolution.SKIP: _install_instruction_component in devsync/cli/package_install.py checks target_file.exists() and skips instead of appending/replacing sections, so only the first translated instruction is installed. This makes multi-instruction package installs for Anteroom incomplete in normal usage.

Useful? React with 👍 / 👎.

)

def translate_mcp_server(self, component: MCPServerComponent, package_root: Path) -> TranslatedComponent:
"""Anteroom does not support MCP servers."""
raise NotImplementedError("Anteroom does not support MCP servers")


class CopilotTranslator(ComponentTranslator):
"""Translator for GitHub Copilot (.github/instructions/)."""

Expand Down Expand Up @@ -899,6 +927,7 @@ def get_translator(tool_type: AIToolType) -> ComponentTranslator:
AIToolType.OPENHANDS: OpenHandsTranslator,
AIToolType.AMP: AmpTranslator,
AIToolType.OPENCODE: OpenCodeTranslator,
AIToolType.ANTEROOM: AnteroomTranslator,
}

translator_class = translators.get(tool_type)
Expand Down
1 change: 1 addition & 0 deletions devsync/core/component_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ class ComponentDetector:
SINGLE_INSTRUCTION_FILES = {
".github/copilot-instructions.md": "copilot",
"AGENTS.md": "codex",
"ANTEROOM.md": "anteroom",
}

INSTRUCTION_EXTENSIONS = {".md", ".mdc", ".instructions.md"}
Expand Down
2 changes: 2 additions & 0 deletions devsync/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class AIToolType(Enum):
OPENHANDS = "openhands"
AMP = "amp"
OPENCODE = "opencode"
ANTEROOM = "anteroom"


class ConflictResolution(Enum):
Expand Down Expand Up @@ -455,6 +456,7 @@ def __post_init__(self) -> None:
"openhands",
"amp",
"opencode",
"anteroom",
]
if self.ide not in valid_ides:
raise ValueError(f"Invalid IDE type: {self.ide}. Must be one of {valid_ides}")
Expand Down
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ Bundle instructions, MCP servers, hooks, commands, and resources into installabl

<div class="feature-card" markdown>

### 22 IDE Integrations
### 23 IDE Integrations

Claude Code, Cursor, Windsurf, GitHub Copilot, Codex CLI, Cline, Kiro, Roo Code, and 14 more.
Claude Code, Cursor, Windsurf, GitHub Copilot, Codex CLI, Cline, Kiro, Roo Code, Anteroom, and 14 more.

[See all IDEs](ide-integrations/index.md){ .md-button }

Expand Down
Loading
Loading