diff --git a/CLAUDE.md b/CLAUDE.md index 7aec3dd..46e267e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 @@ -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 @@ -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/` diff --git a/README.md b/README.md index bb505d3..7fbc4ee 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/devsync/ai_tools/anteroom.py b/devsync/ai_tools/anteroom.py new file mode 100644 index 0000000..cc2540b --- /dev/null +++ b/devsync/ai_tools/anteroom.py @@ -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 = "" +END_MARKER = "" +SECTION_PATTERN = r"\n.*?\n" + + +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: + + + ... instruction content ... + + """ + + @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 diff --git a/devsync/ai_tools/capability_registry.py b/devsync/ai_tools/capability_registry.py index c079672..9505569 100644 --- a/devsync/ai_tools/capability_registry.py +++ b/devsync/ai_tools/capability_registry.py @@ -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", diff --git a/devsync/ai_tools/detector.py b/devsync/ai_tools/detector.py index d296dc3..eab2369 100644 --- a/devsync/ai_tools/detector.py +++ b/devsync/ai_tools/detector.py @@ -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 @@ -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]: @@ -132,6 +134,7 @@ def get_primary_tool(self) -> Optional[AITool]: AIToolType.OPENHANDS, AIToolType.AMP, AIToolType.OPENCODE, + AIToolType.ANTEROOM, ] for tool_type in priority: diff --git a/devsync/ai_tools/translator.py b/devsync/ai_tools/translator.py index 2816ea2..8e27454 100644 --- a/devsync/ai_tools/translator.py +++ b/devsync/ai_tools/translator.py @@ -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"\n{content}\n" + + return TranslatedComponent( + component_type=ComponentType.INSTRUCTION, + component_name=component.name, + target_path="ANTEROOM.md", + content=wrapped, + metadata={"section_based": True}, + ) + + 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/).""" @@ -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) diff --git a/devsync/core/component_detector.py b/devsync/core/component_detector.py index 8c2d151..a1a3dc7 100644 --- a/devsync/core/component_detector.py +++ b/devsync/core/component_detector.py @@ -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"} diff --git a/devsync/core/models.py b/devsync/core/models.py index 326e0cc..b65b947 100644 --- a/devsync/core/models.py +++ b/devsync/core/models.py @@ -31,6 +31,7 @@ class AIToolType(Enum): OPENHANDS = "openhands" AMP = "amp" OPENCODE = "opencode" + ANTEROOM = "anteroom" class ConflictResolution(Enum): @@ -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}") diff --git a/docs/index.md b/docs/index.md index 0986e91..c4723c7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -40,9 +40,9 @@ Bundle instructions, MCP servers, hooks, commands, and resources into installabl