|
| 1 | +"""Alquimia AI integration.""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +from typing import Any |
| 6 | + |
| 7 | +from ..._utils import dump_frontmatter |
| 8 | +from ..base import SkillsIntegration |
| 9 | + |
| 10 | +# Mapping of command template stem → argument-hint text shown inline |
| 11 | +# when a user invokes the slash command in Alquimia AI. |
| 12 | +ARGUMENT_HINTS: dict[str, str] = { |
| 13 | + "specify": "Describe the feature you want to specify", |
| 14 | + "plan": "Optional guidance for the planning phase", |
| 15 | + "tasks": "Optional task generation constraints", |
| 16 | + "implement": "Optional implementation guidance or task filter", |
| 17 | + "analyze": "Optional focus areas for analysis", |
| 18 | + "clarify": "Optional areas to clarify in the spec", |
| 19 | + "constitution": "Principles or values for the project constitution", |
| 20 | + "checklist": "Domain or focus area for the checklist", |
| 21 | + "taskstoissues": "Optional filter or label for GitHub issues", |
| 22 | +} |
| 23 | + |
| 24 | + |
| 25 | +class AlquimiaAIIntegration(SkillsIntegration): |
| 26 | + """Integration for Alquimia AI skills.""" |
| 27 | + |
| 28 | + key = "alquimia" |
| 29 | + config = { |
| 30 | + "name": "Alquimia AI", |
| 31 | + "folder": ".alquimia/", |
| 32 | + "commands_subdir": "skills", |
| 33 | + "install_url": "https://docs.alquimia.ai", |
| 34 | + "requires_cli": True, |
| 35 | + } |
| 36 | + registrar_config = { |
| 37 | + "dir": ".alquimia/skills", |
| 38 | + "format": "markdown", |
| 39 | + "args": "$ARGUMENTS", |
| 40 | + "extension": "/SKILL.md", |
| 41 | + } |
| 42 | + multi_install_safe = True |
| 43 | + |
| 44 | + def _render_skill( |
| 45 | + self, template_name: str, frontmatter: dict[str, Any], body: str |
| 46 | + ) -> str: |
| 47 | + """Render a processed command template as an Alquimia skill.""" |
| 48 | + skill_name = f"speckit-{template_name.replace('.', '-')}" |
| 49 | + description = frontmatter.get( |
| 50 | + "description", |
| 51 | + f"Spec-kit workflow command: {template_name}", |
| 52 | + ) |
| 53 | + skill_frontmatter = self._build_skill_fm( |
| 54 | + skill_name, description, f"templates/commands/{template_name}.md" |
| 55 | + ) |
| 56 | + frontmatter_text = dump_frontmatter(skill_frontmatter) |
| 57 | + return f"---\n{frontmatter_text}\n---\n\n{body.strip()}\n" |
| 58 | + |
| 59 | + def _build_skill_fm(self, name: str, description: str, source: str) -> dict: |
| 60 | + from specify_cli.agents import CommandRegistrar |
| 61 | + |
| 62 | + return CommandRegistrar.build_skill_frontmatter( |
| 63 | + self.key, name, description, source |
| 64 | + ) |
| 65 | + |
| 66 | + @staticmethod |
| 67 | + def inject_argument_hint(content: str, hint: str) -> str: |
| 68 | + """Insert ``argument-hint`` after the first ``description:`` in YAML frontmatter. |
| 69 | +
|
| 70 | + Skips injection if ``argument-hint:`` already exists in the |
| 71 | + frontmatter to avoid duplicate keys. |
| 72 | + """ |
| 73 | + lines = content.splitlines(keepends=True) |
| 74 | + |
| 75 | + # Pre-scan: bail out if argument-hint already present in frontmatter |
| 76 | + dash_count = 0 |
| 77 | + for line in lines: |
| 78 | + stripped = line.rstrip("\n\r") |
| 79 | + if stripped == "---": |
| 80 | + dash_count += 1 |
| 81 | + if dash_count == 2: |
| 82 | + break |
| 83 | + continue |
| 84 | + if dash_count == 1 and stripped.startswith("argument-hint:"): |
| 85 | + return content # already present |
| 86 | + |
| 87 | + out: list[str] = [] |
| 88 | + in_fm = False |
| 89 | + dash_count = 0 |
| 90 | + injected = False |
| 91 | + for line in lines: |
| 92 | + stripped = line.rstrip("\n\r") |
| 93 | + if stripped == "---": |
| 94 | + dash_count += 1 |
| 95 | + in_fm = dash_count == 1 |
| 96 | + out.append(line) |
| 97 | + continue |
| 98 | + if in_fm and not injected and stripped.startswith("description:"): |
| 99 | + out.append(line) |
| 100 | + # Preserve the exact line-ending style (\r\n vs \n) |
| 101 | + if line.endswith("\r\n"): |
| 102 | + eol = "\r\n" |
| 103 | + elif line.endswith("\n"): |
| 104 | + eol = "\n" |
| 105 | + else: |
| 106 | + eol = "" |
| 107 | + escaped = hint.replace("\\", "\\\\").replace('"', '\\"') |
| 108 | + out.append(f'argument-hint: "{escaped}"{eol}') |
| 109 | + injected = True |
| 110 | + continue |
| 111 | + out.append(line) |
| 112 | + return "".join(out) |
| 113 | + |
| 114 | + @staticmethod |
| 115 | + def _inject_frontmatter_flag(content: str, key: str, value: str = "true") -> str: |
| 116 | + """Insert ``key: value`` before the closing ``---`` if not already present.""" |
| 117 | + lines = content.splitlines(keepends=True) |
| 118 | + |
| 119 | + # Pre-scan: bail out if already present in frontmatter |
| 120 | + dash_count = 0 |
| 121 | + for line in lines: |
| 122 | + stripped = line.rstrip("\n\r") |
| 123 | + if stripped == "---": |
| 124 | + dash_count += 1 |
| 125 | + if dash_count == 2: |
| 126 | + break |
| 127 | + continue |
| 128 | + if dash_count == 1 and stripped.startswith(f"{key}:"): |
| 129 | + return content |
| 130 | + |
| 131 | + # Inject before the closing --- of frontmatter |
| 132 | + out: list[str] = [] |
| 133 | + dash_count = 0 |
| 134 | + injected = False |
| 135 | + for line in lines: |
| 136 | + stripped = line.rstrip("\n\r") |
| 137 | + if stripped == "---": |
| 138 | + dash_count += 1 |
| 139 | + if dash_count == 2 and not injected: |
| 140 | + if line.endswith("\r\n"): |
| 141 | + eol = "\r\n" |
| 142 | + elif line.endswith("\n"): |
| 143 | + eol = "\n" |
| 144 | + else: |
| 145 | + eol = "" |
| 146 | + out.append(f"{key}: {value}{eol}") |
| 147 | + injected = True |
| 148 | + out.append(line) |
| 149 | + return "".join(out) |
| 150 | + |
| 151 | + def post_process_skill_content(self, content: str) -> str: |
| 152 | + """Inject Alquimia-specific frontmatter flags, hints and hook notes.""" |
| 153 | + updated = super().post_process_skill_content(content) |
| 154 | + updated = self._inject_frontmatter_flag(updated, "user-invocable") |
| 155 | + updated = self._inject_frontmatter_flag( |
| 156 | + updated, "disable-model-invocation", "false" |
| 157 | + ) |
| 158 | + for line in updated.splitlines(): |
| 159 | + if line.startswith("name:"): |
| 160 | + name = line.removeprefix("name:").strip().strip("\"'") |
| 161 | + hint = ARGUMENT_HINTS.get(name.removeprefix("speckit-")) |
| 162 | + if hint: |
| 163 | + updated = self.inject_argument_hint(updated, hint) |
| 164 | + break |
| 165 | + return updated |
0 commit comments