From 1fd770b263214b1a0ec8c1000bf36b8a0f18ada3 Mon Sep 17 00:00:00 2001 From: Soumyadeep Purkait Date: Fri, 27 Feb 2026 13:11:11 +0530 Subject: [PATCH 1/2] refactor: consolidate agent configurations and improve test coverage --- AGENTS.md | 8 +- src/specify_cli/__init__.py | 137 +--------------------- src/specify_cli/agent_config.py | 202 ++++++++++++++++++++++++++++++++ src/specify_cli/extensions.py | 117 +++--------------- tests/test_extensions.py | 44 ++++--- 5 files changed, 251 insertions(+), 257 deletions(-) create mode 100644 src/specify_cli/agent_config.py diff --git a/AGENTS.md b/AGENTS.md index d8dc0f08f7..08b9e8ffe6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,7 +58,7 @@ Follow these steps to add a new agent (using a hypothetical new agent as an exam **IMPORTANT**: Use the actual CLI tool name as the key, not a shortened version. -Add the new agent to the `AGENT_CONFIG` dictionary in `src/specify_cli/__init__.py`. This is the **single source of truth** for all agent metadata: +Add the new agent to the `AGENT_CONFIG` dictionary in `src/specify_cli/agent_config.py`. This is the **single source of truth** for all agent metadata: ```python AGENT_CONFIG = { @@ -69,6 +69,9 @@ AGENT_CONFIG = { "commands_subdir": "commands", # Subdirectory name for command files (default: "commands") "install_url": "https://example.com/install", # URL for installation docs (or None if IDE-based) "requires_cli": True, # True if CLI tool required, False for IDE-based agents + "command_format": "markdown", # File format for commands (markdown/toml) + "command_args": "$ARGUMENTS", # Placeholder for arguments + "command_extension": ".md", # File extension for commands }, } ``` @@ -90,6 +93,9 @@ This eliminates the need for special-case mappings throughout the codebase. - This field enables `--ai-skills` to locate command templates correctly for skill generation - `install_url`: Installation documentation URL (set to `None` for IDE-based agents) - `requires_cli`: Whether the agent requires a CLI tool check during initialization +- `command_format`: The format used for generating command files (`"markdown"` or `"toml"`) +- `command_args`: The placeholder string used for arguments (`"$ARGUMENTS"` or `"{{args}}"`) +- `command_extension`: The file extension for generated commands (`".md"` or `".toml"`) #### 2. Update CLI Help Text diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 5651ac7226..a212ae4e8e 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -123,142 +123,7 @@ def _format_rate_limit_error(status_code: int, headers: httpx.Headers, url: str) return "\n".join(lines) -# Agent configuration with name, folder, install URL, CLI tool requirement, and commands subdirectory -AGENT_CONFIG = { - "copilot": { - "name": "GitHub Copilot", - "folder": ".github/", - "commands_subdir": "agents", # Special: uses agents/ not commands/ - "install_url": None, # IDE-based, no CLI check needed - "requires_cli": False, - }, - "claude": { - "name": "Claude Code", - "folder": ".claude/", - "commands_subdir": "commands", - "install_url": "https://docs.anthropic.com/en/docs/claude-code/setup", - "requires_cli": True, - }, - "gemini": { - "name": "Gemini CLI", - "folder": ".gemini/", - "commands_subdir": "commands", - "install_url": "https://github.com/google-gemini/gemini-cli", - "requires_cli": True, - }, - "cursor-agent": { - "name": "Cursor", - "folder": ".cursor/", - "commands_subdir": "commands", - "install_url": None, # IDE-based - "requires_cli": False, - }, - "qwen": { - "name": "Qwen Code", - "folder": ".qwen/", - "commands_subdir": "commands", - "install_url": "https://github.com/QwenLM/qwen-code", - "requires_cli": True, - }, - "opencode": { - "name": "opencode", - "folder": ".opencode/", - "commands_subdir": "command", # Special: singular 'command' not 'commands' - "install_url": "https://opencode.ai", - "requires_cli": True, - }, - "codex": { - "name": "Codex CLI", - "folder": ".codex/", - "commands_subdir": "prompts", # Special: uses prompts/ not commands/ - "install_url": "https://github.com/openai/codex", - "requires_cli": True, - }, - "windsurf": { - "name": "Windsurf", - "folder": ".windsurf/", - "commands_subdir": "workflows", # Special: uses workflows/ not commands/ - "install_url": None, # IDE-based - "requires_cli": False, - }, - "kilocode": { - "name": "Kilo Code", - "folder": ".kilocode/", - "commands_subdir": "workflows", # Special: uses workflows/ not commands/ - "install_url": None, # IDE-based - "requires_cli": False, - }, - "auggie": { - "name": "Auggie CLI", - "folder": ".augment/", - "commands_subdir": "commands", - "install_url": "https://docs.augmentcode.com/cli/setup-auggie/install-auggie-cli", - "requires_cli": True, - }, - "codebuddy": { - "name": "CodeBuddy", - "folder": ".codebuddy/", - "commands_subdir": "commands", - "install_url": "https://www.codebuddy.ai/cli", - "requires_cli": True, - }, - "qodercli": { - "name": "Qoder CLI", - "folder": ".qoder/", - "commands_subdir": "commands", - "install_url": "https://qoder.com/cli", - "requires_cli": True, - }, - "roo": { - "name": "Roo Code", - "folder": ".roo/", - "commands_subdir": "commands", - "install_url": None, # IDE-based - "requires_cli": False, - }, - "q": { - "name": "Amazon Q Developer CLI", - "folder": ".amazonq/", - "commands_subdir": "prompts", # Special: uses prompts/ not commands/ - "install_url": "https://aws.amazon.com/developer/learning/q-developer-cli/", - "requires_cli": True, - }, - "amp": { - "name": "Amp", - "folder": ".agents/", - "commands_subdir": "commands", - "install_url": "https://ampcode.com/manual#install", - "requires_cli": True, - }, - "shai": { - "name": "SHAI", - "folder": ".shai/", - "commands_subdir": "commands", - "install_url": "https://github.com/ovh/shai", - "requires_cli": True, - }, - "agy": { - "name": "Antigravity", - "folder": ".agent/", - "commands_subdir": "workflows", # Special: uses workflows/ not commands/ - "install_url": None, # IDE-based - "requires_cli": False, - }, - "bob": { - "name": "IBM Bob", - "folder": ".bob/", - "commands_subdir": "commands", - "install_url": None, # IDE-based - "requires_cli": False, - }, - "generic": { - "name": "Generic (bring your own agent)", - "folder": None, # Set dynamically via --ai-commands-dir - "commands_subdir": "commands", - "install_url": None, - "requires_cli": False, - }, -} +from .agent_config import AGENT_CONFIG SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"} diff --git a/src/specify_cli/agent_config.py b/src/specify_cli/agent_config.py new file mode 100644 index 0000000000..33aabe2b1d --- /dev/null +++ b/src/specify_cli/agent_config.py @@ -0,0 +1,202 @@ +""" +Agent configurations for specify-cli. +Shared between CLI initialization and the extension system. +""" + +# Default values for agent metadata +DEFAULT_FORMAT = "markdown" +DEFAULT_ARGS = "$ARGUMENTS" +DEFAULT_EXTENSION = ".md" + +AGENT_CONFIG = { + "copilot": { + "name": "GitHub Copilot", + "folder": ".github/", + "commands_subdir": "agents", + "install_url": None, + "requires_cli": False, + "command_format": DEFAULT_FORMAT, + "command_args": DEFAULT_ARGS, + "command_extension": DEFAULT_EXTENSION, + }, + "claude": { + "name": "Claude Code", + "folder": ".claude/", + "commands_subdir": "commands", + "install_url": "https://docs.anthropic.com/en/docs/claude-code/setup", + "requires_cli": True, + "command_format": DEFAULT_FORMAT, + "command_args": DEFAULT_ARGS, + "command_extension": DEFAULT_EXTENSION, + }, + "gemini": { + "name": "Gemini CLI", + "folder": ".gemini/", + "commands_subdir": "commands", + "install_url": "https://github.com/google-gemini/gemini-cli", + "requires_cli": True, + "command_format": "toml", + "command_args": "{{args}}", + "command_extension": ".toml", + }, + "cursor-agent": { + "name": "Cursor", + "folder": ".cursor/", + "commands_subdir": "commands", + "install_url": None, + "requires_cli": False, + "command_format": DEFAULT_FORMAT, + "command_args": DEFAULT_ARGS, + "command_extension": DEFAULT_EXTENSION, + }, + "qwen": { + "name": "Qwen Code", + "folder": ".qwen/", + "commands_subdir": "commands", + "install_url": "https://github.com/QwenLM/qwen-code", + "requires_cli": True, + "command_format": "toml", + "command_args": "{{args}}", + "command_extension": ".toml", + }, + "opencode": { + "name": "opencode", + "folder": ".opencode/", + "commands_subdir": "command", + "install_url": "https://opencode.ai", + "requires_cli": True, + "command_format": DEFAULT_FORMAT, + "command_args": DEFAULT_ARGS, + "command_extension": DEFAULT_EXTENSION, + }, + "codex": { + "name": "Codex CLI", + "folder": ".codex/", + "commands_subdir": "prompts", + "install_url": "https://github.com/openai/codex", + "requires_cli": True, + "command_format": DEFAULT_FORMAT, + "command_args": DEFAULT_ARGS, + "command_extension": DEFAULT_EXTENSION, + }, + "windsurf": { + "name": "Windsurf", + "folder": ".windsurf/", + "commands_subdir": "workflows", + "install_url": None, + "requires_cli": False, + "command_format": DEFAULT_FORMAT, + "command_args": DEFAULT_ARGS, + "command_extension": DEFAULT_EXTENSION, + }, + "kilocode": { + "name": "Kilo Code", + "folder": ".kilocode/", + "commands_subdir": "rules", + "install_url": None, + "requires_cli": False, + "command_format": DEFAULT_FORMAT, + "command_args": DEFAULT_ARGS, + "command_extension": DEFAULT_EXTENSION, + }, + "auggie": { + "name": "Auggie CLI", + "folder": ".augment/", + "commands_subdir": "rules", + "install_url": "https://docs.augmentcode.com/cli/setup-auggie/install-auggie-cli", + "requires_cli": True, + "command_format": DEFAULT_FORMAT, + "command_args": DEFAULT_ARGS, + "command_extension": DEFAULT_EXTENSION, + }, + "codebuddy": { + "name": "CodeBuddy", + "folder": ".codebuddy/", + "commands_subdir": "commands", + "install_url": "https://www.codebuddy.ai/cli", + "requires_cli": True, + "command_format": DEFAULT_FORMAT, + "command_args": DEFAULT_ARGS, + "command_extension": DEFAULT_EXTENSION, + }, + "qodercli": { + "name": "Qoder CLI", + "folder": ".qoder/", + "commands_subdir": "commands", + "install_url": "https://qoder.com/cli", + "requires_cli": True, + "command_format": DEFAULT_FORMAT, + "command_args": DEFAULT_ARGS, + "command_extension": DEFAULT_EXTENSION, + }, + "roo": { + "name": "Roo Code", + "folder": ".roo/", + "commands_subdir": "rules", + "install_url": None, + "requires_cli": False, + "command_format": DEFAULT_FORMAT, + "command_args": DEFAULT_ARGS, + "command_extension": DEFAULT_EXTENSION, + }, + "q": { + "name": "Amazon Q Developer CLI", + "folder": ".amazonq/", + "commands_subdir": "prompts", + "install_url": "https://aws.amazon.com/developer/learning/q-developer-cli/", + "requires_cli": True, + "command_format": DEFAULT_FORMAT, + "command_args": DEFAULT_ARGS, + "command_extension": DEFAULT_EXTENSION, + }, + "amp": { + "name": "Amp", + "folder": ".agents/", + "commands_subdir": "commands", + "install_url": "https://ampcode.com/manual#install", + "requires_cli": True, + "command_format": DEFAULT_FORMAT, + "command_args": DEFAULT_ARGS, + "command_extension": DEFAULT_EXTENSION, + }, + "shai": { + "name": "SHAI", + "folder": ".shai/", + "commands_subdir": "commands", + "install_url": "https://github.com/ovh/shai", + "requires_cli": True, + "command_format": DEFAULT_FORMAT, + "command_args": DEFAULT_ARGS, + "command_extension": DEFAULT_EXTENSION, + }, + "agy": { + "name": "Antigravity", + "folder": ".agent/", + "commands_subdir": "workflows", + "install_url": None, + "requires_cli": False, + "command_format": DEFAULT_FORMAT, + "command_args": DEFAULT_ARGS, + "command_extension": DEFAULT_EXTENSION, + }, + "bob": { + "name": "IBM Bob", + "folder": ".bob/", + "commands_subdir": "commands", + "install_url": None, + "requires_cli": False, + "command_format": DEFAULT_FORMAT, + "command_args": DEFAULT_ARGS, + "command_extension": DEFAULT_EXTENSION, + }, + "generic": { + "name": "Generic (bring your own agent)", + "folder": None, + "commands_subdir": "commands", + "install_url": None, + "requires_cli": False, + "command_format": DEFAULT_FORMAT, + "command_args": DEFAULT_ARGS, + "command_extension": DEFAULT_EXTENSION, + }, +} diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index b8881e7c89..92fe7c7fdb 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -20,6 +20,7 @@ from packaging import version as pkg_version from packaging.specifiers import SpecifierSet, InvalidSpecifier +from .agent_config import AGENT_CONFIG class ExtensionError(Exception): """Base exception for extension-related errors.""" @@ -451,7 +452,7 @@ def remove(self, extension_id: str, keep_config: bool = False) -> bool: commands_dir = self.project_root / agent_config["dir"] for cmd_name in cmd_names: - cmd_file = commands_dir / f"{cmd_name}{agent_config['extension']}" + cmd_file = commands_dir / f"{cmd_name}{agent_config['command_extension']}" if cmd_file.exists(): cmd_file.unlink() @@ -581,102 +582,14 @@ class CommandRegistrar: # Agent configurations with directory, format, and argument placeholder AGENT_CONFIGS = { - "claude": { - "dir": ".claude/commands", - "format": "markdown", - "args": "$ARGUMENTS", - "extension": ".md" - }, - "gemini": { - "dir": ".gemini/commands", - "format": "toml", - "args": "{{args}}", - "extension": ".toml" - }, - "copilot": { - "dir": ".github/agents", - "format": "markdown", - "args": "$ARGUMENTS", - "extension": ".md" - }, - "cursor": { - "dir": ".cursor/commands", - "format": "markdown", - "args": "$ARGUMENTS", - "extension": ".md" - }, - "qwen": { - "dir": ".qwen/commands", - "format": "toml", - "args": "{{args}}", - "extension": ".toml" - }, - "opencode": { - "dir": ".opencode/command", - "format": "markdown", - "args": "$ARGUMENTS", - "extension": ".md" - }, - "windsurf": { - "dir": ".windsurf/workflows", - "format": "markdown", - "args": "$ARGUMENTS", - "extension": ".md" - }, - "kilocode": { - "dir": ".kilocode/rules", - "format": "markdown", - "args": "$ARGUMENTS", - "extension": ".md" - }, - "auggie": { - "dir": ".augment/rules", - "format": "markdown", - "args": "$ARGUMENTS", - "extension": ".md" - }, - "roo": { - "dir": ".roo/rules", - "format": "markdown", - "args": "$ARGUMENTS", - "extension": ".md" - }, - "codebuddy": { - "dir": ".codebuddy/commands", - "format": "markdown", - "args": "$ARGUMENTS", - "extension": ".md" - }, - "qodercli": { - "dir": ".qoder/commands", - "format": "markdown", - "args": "$ARGUMENTS", - "extension": ".md" - }, - "q": { - "dir": ".amazonq/prompts", - "format": "markdown", - "args": "$ARGUMENTS", - "extension": ".md" - }, - "amp": { - "dir": ".agents/commands", - "format": "markdown", - "args": "$ARGUMENTS", - "extension": ".md" - }, - "shai": { - "dir": ".shai/commands", - "format": "markdown", - "args": "$ARGUMENTS", - "extension": ".md" - }, - "bob": { - "dir": ".bob/commands", - "format": "markdown", - "args": "$ARGUMENTS", - "extension": ".md" + agent_id: { + "dir": f"{cfg['folder']}{cfg['commands_subdir']}", + "command_format": cfg["command_format"], + "command_args": cfg["command_args"], + "command_extension": cfg["command_extension"] } + for agent_id, cfg in AGENT_CONFIG.items() + if cfg.get("folder") } @staticmethod @@ -856,26 +769,26 @@ def register_commands_for_agent( # Convert argument placeholders body = self._convert_argument_placeholder( - body, "$ARGUMENTS", agent_config["args"] + body, "$ARGUMENTS", agent_config["command_args"] ) # Render in agent-specific format - if agent_config["format"] == "markdown": + if agent_config["command_format"] == "markdown": output = self._render_markdown_command(frontmatter, body, manifest.id) - elif agent_config["format"] == "toml": + elif agent_config["command_format"] == "toml": output = self._render_toml_command(frontmatter, body, manifest.id) else: - raise ExtensionError(f"Unsupported format: {agent_config['format']}") + raise ExtensionError(f"Unsupported format: {agent_config['command_format']}") # Write command file - dest_file = commands_dir / f"{cmd_name}{agent_config['extension']}" + dest_file = commands_dir / f"{cmd_name}{agent_config['command_extension']}" dest_file.write_text(output) registered.append(cmd_name) # Register aliases for alias in cmd_info.get("aliases", []): - alias_file = commands_dir / f"{alias}{agent_config['extension']}" + alias_file = commands_dir / f"{alias}{agent_config['command_extension']}" alias_file.write_text(output) registered.append(alias) diff --git a/tests/test_extensions.py b/tests/test_extensions.py index a2c4121ed4..f9e499c4d3 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -443,17 +443,19 @@ def test_render_frontmatter(self): assert output.endswith("---\n") assert "description: Test command" in output - def test_register_commands_for_claude(self, extension_dir, project_dir): - """Test registering commands for Claude agent.""" - # Create .claude directory - claude_dir = project_dir / ".claude" / "commands" - claude_dir.mkdir(parents=True) + @pytest.mark.parametrize("agent_id, config", CommandRegistrar.AGENT_CONFIGS.items()) + def test_register_commands_for_each_agent(self, agent_id, config, extension_dir, project_dir): + """Test registering commands for all agents.""" + # Create agent directory + agent_dir = project_dir / config["dir"] + agent_dir.mkdir(parents=True) ExtensionManager(project_dir) # Initialize manager (side effects only) manifest = ExtensionManifest(extension_dir / "extension.yml") registrar = CommandRegistrar() - registered = registrar.register_commands_for_claude( + registered = registrar.register_commands_for_agent( + agent_id, manifest, extension_dir, project_dir @@ -463,16 +465,22 @@ def test_register_commands_for_claude(self, extension_dir, project_dir): assert "speckit.test.hello" in registered # Check command file was created - cmd_file = claude_dir / "speckit.test.hello.md" + cmd_file = agent_dir / f"speckit.test.hello{config['command_extension']}" assert cmd_file.exists() content = cmd_file.read_text() - assert "description: Test hello command" in content - assert "" in content - assert "" in content - - def test_command_with_aliases(self, project_dir, temp_dir): - """Test registering a command with aliases.""" + if config["command_format"] == "toml": + assert 'description = "Test hello command"' in content + assert "# Extension: test-ext" in content + assert "# Config: .specify/extensions/test-ext/" in content + else: + assert "description: Test hello command" in content + assert "" in content + assert "" in content + + @pytest.mark.parametrize("agent_id, config", CommandRegistrar.AGENT_CONFIGS.items()) + def test_command_with_aliases(self, agent_id, config, project_dir, temp_dir): + """Test registering a command with aliases for all agents.""" import yaml # Create extension with command alias @@ -507,18 +515,18 @@ def test_command_with_aliases(self, project_dir, temp_dir): (ext_dir / "commands").mkdir() (ext_dir / "commands" / "cmd.md").write_text("---\ndescription: Test\n---\n\nTest") - claude_dir = project_dir / ".claude" / "commands" - claude_dir.mkdir(parents=True) + agent_dir = project_dir / config["dir"] + agent_dir.mkdir(parents=True) manifest = ExtensionManifest(ext_dir / "extension.yml") registrar = CommandRegistrar() - registered = registrar.register_commands_for_claude(manifest, ext_dir, project_dir) + registered = registrar.register_commands_for_agent(agent_id, manifest, ext_dir, project_dir) assert len(registered) == 2 assert "speckit.alias.cmd" in registered assert "speckit.shortcut" in registered - assert (claude_dir / "speckit.alias.cmd.md").exists() - assert (claude_dir / "speckit.shortcut.md").exists() + assert (agent_dir / f"speckit.alias.cmd{config['command_extension']}").exists() + assert (agent_dir / f"speckit.shortcut{config['command_extension']}").exists() # ===== Utility Function Tests ===== From d26c59207f4028408d7571f9de5ecfbdbfa68755 Mon Sep 17 00:00:00 2001 From: Soumyadeep Purkait Date: Sat, 28 Feb 2026 20:48:31 +0530 Subject: [PATCH 2/2] refactor: reorder AGENT_CONFIG import statement to the top of the file --- src/specify_cli/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index a212ae4e8e..d19deb4a57 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -54,6 +54,8 @@ import truststore from datetime import datetime, timezone +from .agent_config import AGENT_CONFIG + ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) client = httpx.Client(verify=ssl_context) @@ -123,8 +125,6 @@ def _format_rate_limit_error(status_code: int, headers: httpx.Headers, url: str) return "\n".join(lines) -from .agent_config import AGENT_CONFIG - SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"} CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude"