diff --git a/README.md b/README.md index cb9aff1..3d23a11 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,12 @@ The generator supports the following AI coding assistants: - Windows: `%APPDATA%\Code - Insiders\User\prompts` - **OpenCode CLI**: Commands installed to `~/.config/opencode/command` - **Amazon Q**: Commands installed to `~/.aws/amazonq/prompts` (Windows & macOS/Linux) +- **Kiro CLI**: Prompts installed to `~/.kiro/prompts` + - Invoke with `@prompt-name` (e.g., `@generate-spec`) + - **Note**: Kiro CLI prompts do not support tool permissions. Run `/tools trust-all` at the start of your session to auto-approve file operations (write, shell, etc.). +- **Kiro IDE**: Steering files installed to `~/.kiro/steering` + - Invoke with `/prompt-name` slash command (e.g., `/generate-spec`) + - Files have `inclusion: manual` frontmatter for manual inclusion ## Documentation diff --git a/docs/slash-command-generator.md b/docs/slash-command-generator.md index e11eea1..cc4222e 100644 --- a/docs/slash-command-generator.md +++ b/docs/slash-command-generator.md @@ -6,7 +6,7 @@ The Slash Command Generator automates the creation of slash command files for AI The generator reads markdown prompts from the `prompts/` directory and produces command files in the appropriate format for each configured AI assistant. It supports: -- **Multiple agents**: 7 supported AI assistants with different command formats +- **Multiple agents**: 11 supported AI assistants with different command formats - **Auto-detection**: Automatically detects configured agents in your workspace - **Dry run mode**: Preview changes without writing files - **Safe overwrite handling**: Prompts before overwriting existing files with backup support @@ -189,10 +189,13 @@ The following agents are supported: | Agent | Display Name | Format | Extension | Target Directory | Reference | |-------|--------------|--------|-----------|------------------|-----------| +| `amazon-q` | Amazon Q | Markdown | `.md` | `.aws/amazonq/prompts` | [Home](https://aws.amazon.com/q/) · [Docs](https://docs.aws.amazon.com/amazonq/) | | `claude-code` | Claude Code | Markdown | `.md` | `.claude/commands` | [Home](https://docs.claude.com/) · [Docs](https://docs.claude.com/en/docs/claude-code/overview) | | `codex-cli` | Codex CLI | Markdown | `.md` | `.codex/prompts` | [Home](https://developers.openai.com/codex) · [Docs](https://developers.openai.com/codex/cli/) | | `cursor` | Cursor | Markdown | `.md` | `.cursor/commands` | [Home](https://cursor.com/) · [Docs](https://cursor.com/docs) | | `gemini-cli` | Gemini CLI | TOML | `.toml` | `.gemini/commands` | [Home](https://github.com/google-gemini/gemini-cli) · [Docs](https://geminicli.com/docs/) | +| `kiro-cli` | Kiro CLI | Kiro | `.md` | `.kiro/prompts` | [Home](https://kiro.dev/cli/) · [Docs](https://kiro.dev/docs/cli/) | +| `kiro-ide` | Kiro IDE | Kiro IDE | `.md` | `.kiro/steering` | [Home](https://kiro.dev/) · [Docs](https://kiro.dev/docs/) | | `opencode` | OpenCode CLI | Markdown | `.md` | `.config/opencode/command` | [Home](https://opencode.ai) · [Docs](https://opencode.ai/docs/commands) | | `vs-code` | VS Code | Markdown | `.prompt.md` | Platform-specific (see note below) | [Home](https://code.visualstudio.com/) · [Docs](https://code.visualstudio.com/docs) | | `vs-code-insiders` | VS Code Insiders | Markdown | `.prompt.md` | Platform-specific (see note below) | [Home](https://code.visualstudio.com/insiders/) · [Docs](https://code.visualstudio.com/docs) | @@ -214,6 +217,53 @@ The following agents are supported: The generator automatically detects your platform and installs commands to the correct location. VS Code and VS Code Insiders maintain separate prompt directories and do not share configurations. +### Kiro (CLI and IDE) + +Kiro has two separate products that use different command formats. You can install for one or both: + +| Product | What it does | Install path | Invocation | +|---------|-------------|--------------|------------| +| **Kiro CLI** | Terminal-based AI assistant | `~/.kiro/prompts/*.md` | `@prompt-name` | +| **Kiro IDE** | VS Code-based IDE with steering files | `~/.kiro/steering/*.md` | `/prompt-name` | + +#### Quick Start + +```bash +# Install for Kiro CLI only +uv run slash-man generate --agent kiro-cli --yes + +# Install for Kiro IDE only +uv run slash-man generate --agent kiro-ide --yes + +# Install for both +uv run slash-man generate --agent kiro-cli --agent kiro-ide --yes + +# Install SDD workflow prompts from GitHub +uv run slash-man generate --agent kiro-cli --agent kiro-ide \ + --github-repo liatrio-labs/spec-driven-workflow \ + --github-branch main --github-path prompts --yes +``` + +#### How It Works + +The generator automatically adapts prompts for Kiro's conventions: + +- **Kiro IDE steering files get YAML frontmatter** with `inclusion: manual`, `name`, `description`, and `tools: ["*"]` (wildcard tool access) +- **Kiro CLI prompts are plain markdown** with no frontmatter — just the prompt body +- **Prompt names are preserved**: A source prompt named `SDD-1-generate-spec` becomes `SDD-1-generate-spec.md` and is invoked as `@SDD-1-generate-spec` (CLI) or `/SDD-1-generate-spec` (IDE) + +#### Kiro CLI Tool Permissions + +Kiro CLI prompts do not support declaring tool permissions. By default, Kiro CLI will prompt you to confirm every `write` and `shell` operation, which interrupts workflows like SDD that need to create files automatically. + +**Workaround**: Run `/tools trust-all` at the start of your Kiro CLI session to auto-approve all tool operations for that session. This only needs to be done once per session. + +```text +> /tools trust-all +``` + +See the [Kiro CLI permissions documentation](https://kiro.dev/docs/cli/chat/permissions/) for more details. + ## Command File Formats ### Markdown Format diff --git a/slash_commands/config.py b/slash_commands/config.py index acfa89d..10eaba8 100644 --- a/slash_commands/config.py +++ b/slash_commands/config.py @@ -13,6 +13,8 @@ class CommandFormat(str, Enum): MARKDOWN = "markdown" TOML = "toml" + KIRO = "kiro" + KIRO_IDE = "kiro-ide" @dataclass(frozen=True) @@ -140,6 +142,24 @@ def get_command_dir(self) -> str: (".aws/amazonq",), None, ), + ( + "kiro-cli", + "Kiro CLI", + ".kiro/prompts", + CommandFormat.KIRO, + ".md", + (".kiro",), + None, + ), + ( + "kiro-ide", + "Kiro IDE", + ".kiro/steering", + CommandFormat.KIRO_IDE, + ".md", + (".kiro",), + None, + ), ) _SORTED_AGENT_DATA = tuple(sorted(_SUPPORTED_AGENT_DATA, key=lambda item: item[0])) diff --git a/slash_commands/generators.py b/slash_commands/generators.py index 35c4b82..53e3d25 100644 --- a/slash_commands/generators.py +++ b/slash_commands/generators.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re from datetime import UTC, datetime from typing import Any, Protocol @@ -291,6 +292,117 @@ def _dict_to_toml(self, data: dict) -> str: return tomli_w.dumps(data) +def _strip_ordering_prefix(name: str) -> str: + """Strip ordering prefixes like 'SDD-1-' from a prompt name.""" + return re.sub(r"^[A-Z]+-\d+-", "", name) + + +class KiroCommandGenerator: + """Generator for Kiro CLI prompts. + + Kiro CLI expects simple markdown files with no frontmatter. + The prompt content is injected directly when the user invokes @prompt-name. + Tracking metadata is appended as a trailing HTML comment so it does not + interfere with the prompt instructions the model sees first. + """ + + def generate( + self, + prompt: MarkdownPrompt, + agent: AgentConfig, + source_metadata: dict[str, Any] | None = None, + ) -> str: + """Generate a Kiro CLI prompt file. + + Args: + prompt: The source prompt to generate from + agent: The agent configuration + source_metadata: Optional source metadata (local or GitHub) + + Returns: + Simple markdown content for Kiro CLI + """ + _description, arguments, _enabled = _apply_agent_overrides(prompt, agent) + + # Replace placeholders in body + body = _replace_placeholders(prompt.body, arguments, replace_double_braces=True) + + # Output the prompt body directly — no extra headers or preamble. + # The body already contains its own structure (headings, sections, etc.) + output = body + "\n" + + # Append tracking metadata as a trailing HTML comment. + # Placed at the end so it doesn't pollute the instructions the model sees first. + meta_lines = [ + f"source: {prompt.name}", + f"version: {__version__}", + f"updated: {datetime.now(UTC).strftime('%Y-%m-%d')}", + ] + if source_metadata: + if "source_repo" in source_metadata: + meta_lines.append(f"repo: {source_metadata['source_repo']}") + + output += "\n\n" + + return _normalize_output(output) + + +class KiroIdeCommandGenerator: + """Generator for Kiro IDE steering files. + + Kiro IDE expects markdown files with YAML frontmatter containing + inclusion mode. Files are stored at ~/.kiro/steering/*.md and + are manually included via / command markers in chat. + """ + + def generate( + self, + prompt: MarkdownPrompt, + agent: AgentConfig, + source_metadata: dict[str, Any] | None = None, + ) -> str: + """Generate a Kiro IDE steering file. + + Args: + prompt: The source prompt to generate from + agent: The agent configuration + source_metadata: Optional source metadata (local or GitHub) + + Returns: + Markdown with Kiro IDE steering frontmatter + """ + description, arguments, _enabled = _apply_agent_overrides(prompt, agent) + + # Build Kiro IDE steering frontmatter with inclusion first, then name, description, tools + frontmatter: dict[str, Any] = { + "inclusion": "manual", + "name": prompt.name, + "description": description, + "tools": ["*"], + } + + # Replace placeholders and rewrite command references (/ prefix for steering files) + body = _replace_placeholders(prompt.body, arguments, replace_double_braces=True) + + # Format as YAML frontmatter + body + yaml_content = yaml.safe_dump(frontmatter, allow_unicode=True, sort_keys=False) + output = f"---\n{yaml_content}---\n\n{body}\n" + + # Append tracking metadata as a trailing HTML comment + meta_lines = [ + f"source: {prompt.name}", + f"version: {__version__}", + f"updated: {datetime.now(UTC).strftime('%Y-%m-%d')}", + ] + if source_metadata: + if "source_repo" in source_metadata: + meta_lines.append(f"repo: {source_metadata['source_repo']}") + + output += "\n\n" + + return _normalize_output(output) + + class CommandGenerator: """Base class for command generators.""" @@ -301,5 +413,9 @@ def create(format: CommandFormat) -> CommandGeneratorProtocol: return MarkdownCommandGenerator() elif format == CommandFormat.TOML: return TomlCommandGenerator() + elif format == CommandFormat.KIRO: + return KiroCommandGenerator() + elif format == CommandFormat.KIRO_IDE: + return KiroIdeCommandGenerator() else: raise ValueError(f"Unsupported command format: {format}") diff --git a/slash_commands/writer.py b/slash_commands/writer.py index 07eb9a0..d21e520 100644 --- a/slash_commands/writer.py +++ b/slash_commands/writer.py @@ -510,6 +510,8 @@ def _is_generated_file(self, file_path: Path, agent: AgentConfig) -> bool: return self._is_generated_markdown(content) elif agent.command_format.value == "toml": return self._is_generated_toml(content) + elif agent.command_format.value in ("kiro", "kiro-ide"): + return self._is_generated_kiro(content) return False def _is_generated_markdown(self, content: str) -> bool: @@ -561,6 +563,18 @@ def _is_generated_toml(self, content: str) -> bool: except tomllib.TOMLDecodeError: return False + def _is_generated_kiro(self, content: str) -> bool: + """Check if Kiro CLI content was generated by this tool. + + Args: + content: File content + + Returns: + True if generated by this tool + """ + # Kiro files have an HTML comment marker (at the end of the file) + return "") + + # Must NOT have frontmatter + assert not generated.startswith("---") + + +# -- Kiro IDE agent generator tests -------------------------------------------- + + +def test_kiro_ide_generator_produces_frontmatter_with_tools(sample_prompt): + """Test that KiroIdeCommandGenerator produces markdown with Kiro IDE steering frontmatter.""" + agent = get_agent_config("kiro-ide") + generator = KiroIdeCommandGenerator() + + generated = generator.generate(sample_prompt, agent) + frontmatter, body = _extract_frontmatter_and_body(generated) + + # Check that inclusion is first, then name, description, tools + assert frontmatter["inclusion"] == "manual" + assert frontmatter["name"] == "sample-prompt" + assert "description" in frontmatter + assert frontmatter["tools"] == ["*"] + # Should NOT have markdown-generator fields + assert "tags" not in frontmatter + assert "arguments" not in frontmatter + assert "meta" not in frontmatter + assert "enabled" not in frontmatter + + assert "# Sample Prompt" in body + + +def test_kiro_ide_generator_includes_tracking_comment(sample_prompt): + """Test that tracking metadata is appended as a trailing HTML comment.""" + agent = get_agent_config("kiro-ide") + generator = KiroIdeCommandGenerator() + + generated = generator.generate(sample_prompt, agent) + + assert "") diff --git a/tests/test_writer.py b/tests/test_writer.py index 23c3370..2690371 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -885,3 +885,122 @@ def test_writer_writes_vs_code_to_platform_specific_path( assert len(result["files"]) == 1 assert result["files"][0]["path"] == str(expected_path) assert result["files"][0]["agent"] == "vs-code" + + +def test_writer_finds_generated_kiro_prompt_files(tmp_path): + """Test that writer can find generated Kiro prompt files.""" + command_dir = tmp_path / ".kiro" / "prompts" + command_dir.mkdir(parents=True, exist_ok=True) + + generated_file = command_dir / "test-command.md" + generated_file.write_text( + "# Test Command\n\nDo things.\n\n" + "\n" + ) + + # Create a non-generated file (no tracking comment) + non_generated_file = command_dir / "manual-prompt.md" + non_generated_file.write_text("# Manual Prompt\n\nJust a plain file.\n") + + writer = SlashCommandWriter( + prompts_dir=tmp_path / "prompts", + agents=[], + dry_run=False, + base_path=tmp_path, + ) + + found_files = writer.find_generated_files(agents=["kiro-cli"], include_backups=False) + + assert len(found_files) == 1 + assert isinstance(found_files[0]["path"], str) + assert found_files[0]["path"] == str(generated_file) + assert found_files[0]["agent"] == "kiro-cli" + assert found_files[0]["type"] == "command" + + +def test_writer_generates_kiro_prompt_files(mock_prompt_load: Path, tmp_path): + """Test that writer generates Kiro prompt markdown files.""" + prompts_dir = mock_prompt_load + + writer = SlashCommandWriter( + prompts_dir=prompts_dir, + agents=["kiro-cli"], + dry_run=False, + base_path=tmp_path, + ) + + writer.generate() + + # Verify markdown files were created in the prompts directory + prompts_output_dir = tmp_path / ".kiro" / "prompts" + assert prompts_output_dir.exists() + + # Check that at least one markdown file was created + md_files = list(prompts_output_dir.glob("*.md")) + assert len(md_files) > 0 + + # Verify the generated file is plain markdown with tracking comment + for md_file in md_files: + content = md_file.read_text() + assert not content.startswith("---") # No frontmatter + assert "\n" + ) + + # Create a non-generated file (no tracking comment) + non_generated_file = command_dir / "manual-agent.md" + non_generated_file.write_text( + "---\ninclusion: manual\nname: manual\ntools: ['*']\n---\n\n# Manual\n" + ) + + writer = SlashCommandWriter( + prompts_dir=tmp_path / "prompts", + agents=[], + dry_run=False, + base_path=tmp_path, + ) + + found_files = writer.find_generated_files(agents=["kiro-ide"], include_backups=False) + + assert len(found_files) == 1 + assert found_files[0]["path"] == str(generated_file) + assert found_files[0]["agent"] == "kiro-ide" + assert found_files[0]["type"] == "command" + + +def test_writer_generates_kiro_ide_agent_files(mock_prompt_load: Path, tmp_path): + """Test that writer generates Kiro IDE steering markdown files.""" + prompts_dir = mock_prompt_load + + writer = SlashCommandWriter( + prompts_dir=prompts_dir, + agents=["kiro-ide"], + dry_run=False, + base_path=tmp_path, + ) + + writer.generate() + + steering_output_dir = tmp_path / ".kiro" / "steering" + assert steering_output_dir.exists() + + md_files = list(steering_output_dir.glob("*.md")) + assert len(md_files) > 0 + + for md_file in md_files: + content = md_file.read_text() + assert content.startswith("---") # Has frontmatter + assert "inclusion: manual" in content # Has inclusion field + assert "tools:" in content # Has tools field + assert "