feat: enhancement workflow preset system with multi-target CLI

- Add YAML-based enhancement workflow presets shipped inside the package
  (default, minimal, security-focus, architecture-comprehensive, api-documentation)
- Add `skill-seekers workflows` subcommand: list, show, copy, add, remove, validate
- copy/add/remove all accept multiple names/files in one invocation with partial-failure behaviour
- `add --name` override restricted to single-file operations
- Add 5 MCP tools: list_workflows, get_workflow, create_workflow, update_workflow, delete_workflow
- Fix: create command _add_common_args() now correctly forwards each --enhance-workflow
  as a separate flag instead of passing the whole list as a single argument
- Update README: reposition as "data layer for AI systems" with AI Skills front and centre
- Update CHANGELOG, QUICK_REFERENCE, CLAUDE.md with workflow preset details
- 1,880+ tests passing

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
yusyus
2026-02-18 21:22:16 +03:00
parent a9b51ab3fe
commit 265214ac27
25 changed files with 2381 additions and 201 deletions

View File

@@ -103,6 +103,12 @@ try:
submit_config_impl,
upload_skill_impl,
validate_config_impl,
# Workflow tools
list_workflows_impl,
get_workflow_impl,
create_workflow_impl,
update_workflow_impl,
delete_workflow_impl,
)
except ImportError:
# Fallback for direct script execution
@@ -137,6 +143,11 @@ except ImportError:
submit_config_impl,
upload_skill_impl,
validate_config_impl,
list_workflows_impl,
get_workflow_impl,
create_workflow_impl,
update_workflow_impl,
delete_workflow_impl,
)
# Initialize FastMCP server
@@ -1178,6 +1189,100 @@ async def export_to_qdrant(
return str(result)
# ============================================================================
# WORKFLOW TOOLS (5 tools)
# ============================================================================
@safe_tool_decorator(
description="List all available enhancement workflows (bundled defaults + user-created). Returns name, description, and source (bundled/user) for each."
)
async def list_workflows() -> str:
"""List all available enhancement workflow presets."""
result = list_workflows_impl({})
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="Get the full YAML content of a named enhancement workflow. Searches user dir first, then bundled defaults."
)
async def get_workflow(name: str) -> str:
"""
Get full YAML content of a workflow.
Args:
name: Workflow name (e.g. 'security-focus', 'default')
Returns:
YAML content of the workflow, or error message if not found.
"""
result = get_workflow_impl({"name": name})
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="Create a new user workflow from YAML content. The workflow is saved to ~/.config/skill-seekers/workflows/."
)
async def create_workflow(name: str, content: str) -> str:
"""
Create a new user workflow.
Args:
name: Workflow name (becomes the filename stem, e.g. 'my-custom')
content: Full YAML content of the workflow
Returns:
Success message with file path, or error message.
"""
result = create_workflow_impl({"name": name, "content": content})
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="Update (overwrite) an existing user workflow. Cannot update bundled workflows."
)
async def update_workflow(name: str, content: str) -> str:
"""
Update an existing user workflow.
Args:
name: Workflow name to update
content: New YAML content
Returns:
Success message, or error if workflow is bundled or invalid.
"""
result = update_workflow_impl({"name": name, "content": content})
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
@safe_tool_decorator(
description="Delete a user workflow by name. Bundled workflows cannot be deleted."
)
async def delete_workflow(name: str) -> str:
"""
Delete a user workflow.
Args:
name: Workflow name to delete
Returns:
Success message, or error if workflow is bundled or not found.
"""
result = delete_workflow_impl({"name": name})
if isinstance(result, list) and result:
return result[0].text if hasattr(result[0], "text") else str(result[0])
return str(result)
# ============================================================================
# MAIN ENTRY POINT
# ============================================================================

View File

@@ -96,6 +96,21 @@ from .vector_db_tools import (
from .vector_db_tools import (
export_to_weaviate_impl,
)
from .workflow_tools import (
create_workflow_tool as create_workflow_impl,
)
from .workflow_tools import (
delete_workflow_tool as delete_workflow_impl,
)
from .workflow_tools import (
get_workflow_tool as get_workflow_impl,
)
from .workflow_tools import (
list_workflows_tool as list_workflows_impl,
)
from .workflow_tools import (
update_workflow_tool as update_workflow_impl,
)
__all__ = [
"__version__",
@@ -132,4 +147,10 @@ __all__ = [
"export_to_chroma_impl",
"export_to_faiss_impl",
"export_to_qdrant_impl",
# Workflow tools
"list_workflows_impl",
"get_workflow_impl",
"create_workflow_impl",
"update_workflow_impl",
"delete_workflow_impl",
]

View File

@@ -0,0 +1,226 @@
"""
MCP Tool Implementations for Workflow Management
5 tools:
list_workflows list all workflows (bundled + user) with source info
get_workflow return full YAML of a named workflow
create_workflow write a new YAML to user dir
update_workflow overwrite an existing user workflow
delete_workflow remove a user workflow by name
"""
from __future__ import annotations
from pathlib import Path
import yaml
try:
from mcp.types import TextContent
except ImportError:
# Graceful degradation for testing without mcp installed
class TextContent: # type: ignore[no-redef]
def __init__(self, type: str, text: str):
self.type = type
self.text = text
USER_WORKFLOWS_DIR = Path.home() / ".config" / "skill-seekers" / "workflows"
def _ensure_user_dir() -> Path:
USER_WORKFLOWS_DIR.mkdir(parents=True, exist_ok=True)
return USER_WORKFLOWS_DIR
def _bundled_names() -> list[str]:
from importlib.resources import files as importlib_files
try:
pkg = importlib_files("skill_seekers.workflows")
names = []
for item in pkg.iterdir():
name = str(item.name)
if name.endswith((".yaml", ".yml")):
names.append(name.removesuffix(".yaml").removesuffix(".yml"))
return sorted(names)
except Exception:
return []
def _user_names() -> list[str]:
if not USER_WORKFLOWS_DIR.exists():
return []
return sorted(
p.stem for p in USER_WORKFLOWS_DIR.iterdir() if p.suffix in (".yaml", ".yml")
)
def _read_bundled(name: str) -> str | None:
from importlib.resources import files as importlib_files
for suffix in (".yaml", ".yml"):
try:
pkg_ref = importlib_files("skill_seekers.workflows").joinpath(name + suffix)
return pkg_ref.read_text(encoding="utf-8")
except (FileNotFoundError, TypeError, ModuleNotFoundError):
continue
return None
def _read_workflow(name: str) -> str | None:
"""Read YAML text: user dir first, then bundled."""
for suffix in (".yaml", ".yml"):
p = USER_WORKFLOWS_DIR / (name + suffix)
if p.exists():
return p.read_text(encoding="utf-8")
return _read_bundled(name)
def _validate_yaml(text: str) -> dict:
"""Parse and basic-validate workflow YAML; returns parsed dict."""
data = yaml.safe_load(text)
if not isinstance(data, dict):
raise ValueError("Workflow YAML root must be a mapping")
if "stages" not in data:
raise ValueError("Workflow must contain a 'stages' key")
return data
# ──────────────────────────────────────────────────────────────────────────────
# Tool implementations
# ──────────────────────────────────────────────────────────────────────────────
def list_workflows_tool(args: dict) -> list:
"""Return all workflows with name, description, and source."""
result: list[dict[str, str]] = []
for name in _bundled_names():
desc = ""
text = _read_bundled(name)
if text:
try:
data = yaml.safe_load(text)
desc = data.get("description", "")
except Exception:
pass
result.append({"name": name, "description": desc, "source": "bundled"})
for name in _user_names():
desc = ""
text = _read_workflow(name)
if text:
try:
data = yaml.safe_load(text)
desc = data.get("description", "")
except Exception:
pass
result.append({"name": name, "description": desc, "source": "user"})
output = yaml.dump(result, default_flow_style=False, sort_keys=False)
return [TextContent(type="text", text=output)]
def get_workflow_tool(args: dict) -> list:
"""Return full YAML content of a named workflow."""
name = args.get("name", "").strip()
if not name:
return [TextContent(type="text", text="Error: 'name' parameter is required.")]
text = _read_workflow(name)
if text is None:
bundled = _bundled_names()
user = _user_names()
available = bundled + [f"{n} (user)" for n in user]
msg = (
f"Error: Workflow '{name}' not found.\n"
f"Available workflows: {', '.join(available) if available else 'none'}"
)
return [TextContent(type="text", text=msg)]
return [TextContent(type="text", text=text)]
def create_workflow_tool(args: dict) -> list:
"""Write a new workflow YAML to the user directory."""
name = args.get("name", "").strip()
content = args.get("content", "")
if not name:
return [TextContent(type="text", text="Error: 'name' parameter is required.")]
if not content:
return [TextContent(type="text", text="Error: 'content' parameter is required.")]
# Validate
try:
_validate_yaml(content)
except Exception as exc:
return [TextContent(type="text", text=f"Error: Invalid workflow YAML {exc}")]
dest = _ensure_user_dir() / (name + ".yaml")
if dest.exists():
return [
TextContent(
type="text",
text=f"Error: Workflow '{name}' already exists in user dir. Use update_workflow to overwrite.",
)
]
dest.write_text(content, encoding="utf-8")
return [TextContent(type="text", text=f"Created workflow '{name}' at: {dest}")]
def update_workflow_tool(args: dict) -> list:
"""Overwrite an existing user workflow. Cannot update bundled workflows."""
name = args.get("name", "").strip()
content = args.get("content", "")
if not name:
return [TextContent(type="text", text="Error: 'name' parameter is required.")]
if not content:
return [TextContent(type="text", text="Error: 'content' parameter is required.")]
if name in _bundled_names() and name not in _user_names():
return [
TextContent(
type="text",
text=(
f"Error: '{name}' is a bundled workflow and cannot be updated. "
"Use create_workflow with a different name, or copy it first with "
"'skill-seekers workflows copy'."
),
)
]
# Validate
try:
_validate_yaml(content)
except Exception as exc:
return [TextContent(type="text", text=f"Error: Invalid workflow YAML {exc}")]
dest = _ensure_user_dir() / (name + ".yaml")
dest.write_text(content, encoding="utf-8")
return [TextContent(type="text", text=f"Updated workflow '{name}' at: {dest}")]
def delete_workflow_tool(args: dict) -> list:
"""Remove a user workflow by name. Bundled workflows cannot be deleted."""
name = args.get("name", "").strip()
if not name:
return [TextContent(type="text", text="Error: 'name' parameter is required.")]
if name in _bundled_names():
return [
TextContent(
type="text",
text=f"Error: '{name}' is a bundled workflow and cannot be deleted.",
)
]
for suffix in (".yaml", ".yml"):
candidate = USER_WORKFLOWS_DIR / (name + suffix)
if candidate.exists():
candidate.unlink()
return [TextContent(type="text", text=f"Deleted user workflow: {candidate}")]
return [TextContent(type="text", text=f"Error: User workflow '{name}' not found.")]