Skip to content
Open
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
11 changes: 10 additions & 1 deletion integrations/catalog.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
{
"schema_version": "1.0",
"updated_at": "2026-06-02T00:00:00Z",
"updated_at": "2026-06-22T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/integrations/catalog.json",
"integrations": {
"alquimia": {
"id": "alquimia_ai",
"name": "Alquimia AI",
"version": "1.0.0",
"description": "Alquimia AI CLI integration",
"author": "Alquimia AI team",
"repository": "https://github.com/github/spec-kit",
"tags": ["cli", "alquimia"]
},
"claude": {
"id": "claude",
"name": "Claude Code",
Expand Down
2 changes: 2 additions & 0 deletions src/specify_cli/integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def _register_builtins() -> None:
"""
# -- Imports (alphabetical) -------------------------------------------
from .agy import AgyIntegration
from .alquimia import AlquimiaAIIntegration
from .amp import AmpIntegration
Comment on lines 49 to 52
from .auggie import AuggieIntegration
from .bob import BobIntegration
Expand Down Expand Up @@ -83,6 +84,7 @@ def _register_builtins() -> None:

# -- Registration (alphabetical) --------------------------------------
_register(AgyIntegration())
_register(AlquimiaAIIntegration())
_register(AmpIntegration())
_register(AuggieIntegration())
_register(BobIntegration())
Expand Down
202 changes: 202 additions & 0 deletions src/specify_cli/integrations/alquimia/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
"""Alquimia AI integration."""

from __future__ import annotations

from pathlib import Path
from typing import Any

import yaml

from ..base import SkillsIntegration
from ..manifest import IntegrationManifest

# Mapping of command template stem → argument-hint text shown inline
# when a user invokes the slash command in Alquimia AI.
ARGUMENT_HINTS: dict[str, str] = {
"specify": "Describe the feature you want to specify",
"plan": "Optional guidance for the planning phase",
"tasks": "Optional task generation constraints",
"implement": "Optional implementation guidance or task filter",
"analyze": "Optional focus areas for analysis",
"clarify": "Optional areas to clarify in the spec",
"constitution": "Principles or values for the project constitution",
"checklist": "Domain or focus area for the checklist",
"taskstoissues": "Optional filter or label for GitHub issues",
}


class AlquimiaAIIntegration(SkillsIntegration):
"""Integration for Alquimia AI skills."""

key = "alquimia"
config = {
"name": "Alquimia AI",
"folder": ".alquimia/",
"commands_subdir": "skills",
"install_url": "https://docs.alquimia.ai",
"requires_cli": True,
}
registrar_config = {
"dir": ".alquimia/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": "/SKILL.md",
}
context_file = "ALQUIMIA.md"
multi_install_safe = True

@staticmethod
def inject_argument_hint(content: str, hint: str) -> str:
"""Insert ``argument-hint`` after the first ``description:`` in YAML frontmatter.

Skips injection if ``argument-hint:`` already exists in the
frontmatter to avoid duplicate keys.
"""
lines = content.splitlines(keepends=True)

# Pre-scan: bail out if argument-hint already present in frontmatter
dash_count = 0
for line in lines:
stripped = line.rstrip("\n\r")
if stripped == "---":
dash_count += 1
if dash_count == 2:
break
continue
if dash_count == 1 and stripped.startswith("argument-hint:"):
return content # already present

out: list[str] = []
in_fm = False
dash_count = 0
injected = False
for line in lines:
stripped = line.rstrip("\n\r")
if stripped == "---":
dash_count += 1
in_fm = dash_count == 1
out.append(line)
continue
if in_fm and not injected and stripped.startswith("description:"):
out.append(line)
# Preserve the exact line-ending style (\r\n vs \n)
if line.endswith("\r\n"):
eol = "\r\n"
elif line.endswith("\n"):
eol = "\n"
else:
eol = ""
escaped = hint.replace("\\", "\\\\").replace('"', '\\"')
out.append(f'argument-hint: "{escaped}"{eol}')
injected = True
continue
out.append(line)
return "".join(out)

def _render_skill(
self, template_name: str, frontmatter: dict[str, Any], body: str
) -> str:
"""Render a processed command template as an Alquimia skill."""
skill_name = f"speckit-{template_name.replace('.', '-')}"
description = frontmatter.get(
"description",
f"Spec-kit workflow command: {template_name}",
)
skill_frontmatter = self._build_skill_fm(
skill_name, description, f"templates/commands/{template_name}.md"
)
frontmatter_text = yaml.safe_dump(skill_frontmatter, sort_keys=False).strip()
return f"---\n{frontmatter_text}\n---\n\n{body.strip()}\n"

def _build_skill_fm(self, name: str, description: str, source: str) -> dict:
from specify_cli.agents import CommandRegistrar

return CommandRegistrar.build_skill_frontmatter(
self.key, name, description, source
)

@staticmethod
def _inject_frontmatter_flag(content: str, key: str, value: str = "true") -> str:
"""Insert ``key: value`` before the closing ``---`` if not already present."""
lines = content.splitlines(keepends=True)

# Pre-scan: bail out if already present in frontmatter
dash_count = 0
for line in lines:
stripped = line.rstrip("\n\r")
if stripped == "---":
dash_count += 1
if dash_count == 2:
break
continue
if dash_count == 1 and stripped.startswith(f"{key}:"):
return content

# Inject before the closing --- of frontmatter
out: list[str] = []
dash_count = 0
injected = False
for line in lines:
stripped = line.rstrip("\n\r")
if stripped == "---":
dash_count += 1
if dash_count == 2 and not injected:
if line.endswith("\r\n"):
eol = "\r\n"
elif line.endswith("\n"):
eol = "\n"
else:
eol = ""
out.append(f"{key}: {value}{eol}")
injected = True
out.append(line)
return "".join(out)

def post_process_skill_content(self, content: str) -> str:
"""Inject Alquimia-specific frontmatter flags and hook notes."""
updated = super().post_process_skill_content(content)
updated = self._inject_frontmatter_flag(updated, "user-invocable")
updated = self._inject_frontmatter_flag(updated, "disable-model-invocation", "false")
return updated

def setup(
self,
project_root: Path,
manifest: IntegrationManifest,
parsed_options: dict[str, Any] | None = None,
**opts: Any,
) -> list[Path]:
"""Install Alquimia skills, then inject Alquimia-specific flags and argument-hints."""
created = super().setup(project_root, manifest, parsed_options, **opts)

# Post-process generated skill files
skills_dir = self.skills_dest(project_root).resolve()

for path in created:
# Only touch SKILL.md files under the skills directory
try:
path.resolve().relative_to(skills_dir)
except ValueError:
continue
if path.name != "SKILL.md":
continue

content_bytes = path.read_bytes()
content = content_bytes.decode("utf-8")

updated = content

# Inject argument-hint if available for this skill
skill_dir_name = path.parent.name # e.g. "speckit-plan"
stem = skill_dir_name
if stem.startswith("speckit-"):
stem = stem[len("speckit-") :]
hint = ARGUMENT_HINTS.get(stem, "")
if hint:
updated = self.inject_argument_hint(updated, hint)

if updated != content:
path.write_bytes(updated.encode("utf-8"))
self.record_file_in_manifest(path, project_root, manifest)

return created
Loading