From e98bdfd5c10d1897f5dad142bd26d7a24bc85294 Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:58:26 +0200 Subject: [PATCH] add plugin skill refresh flag (#8913) Co-authored-by: Claude Opus 4.5 --- system_prompts/README.md | 12 ++++ system_prompts/generate.py | 115 +++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/system_prompts/README.md b/system_prompts/README.md index 05736dc55c..d3cd0b1e41 100644 --- a/system_prompts/README.md +++ b/system_prompts/README.md @@ -25,6 +25,17 @@ When SDK methods or the OpenFlow schema change, run: python system_prompts/generate.py ``` +To also refresh the standalone skills in a Claude plugin checkout: + +```bash +python system_prompts/generate.py --plugin-dir ~/windmill-claude-plugin +``` + +`--plugin-dir` accepts: +- the `windmill-claude-plugin` repo root +- a plugin root such as `plugins/windmill-code-plugin` +- a direct `skills/` directory + This will: 1. Parse TypeScript and Python SDK files to extract function signatures @@ -32,6 +43,7 @@ This will: 3. Parse the CLI commands 4. Assemble complete prompts from markdown files 5. Generate TypeScript exports in `auto-generated/` +6. Optionally refresh plugin-ready standalone `SKILL.md` files in the target directory ### Scope diff --git a/system_prompts/generate.py b/system_prompts/generate.py index b3e7588b97..b24fd6ca0b 100644 --- a/system_prompts/generate.py +++ b/system_prompts/generate.py @@ -11,11 +11,14 @@ This script: Usage: python generate.py + python generate.py --plugin-dir /path/to/windmill-claude-plugin """ +import argparse import ast import json import re +import shutil from pathlib import Path import yaml @@ -1104,13 +1107,121 @@ def generate_skills_ts_export(skills: list[str], schema_yaml_content: dict[str, return ts +def format_schema_for_markdown(schema_yaml: str, schema_name: str, file_pattern: str) -> str: + """Format a standalone schema block for plugin skill files.""" + return f"""## {schema_name} (`{file_pattern}`) + +Must be a YAML file that adheres to the following schema: + +```yaml +{schema_yaml.strip()} +```""" + + +def render_plugin_skill_content(skill_name: str, schema_yaml_content: dict[str, str]) -> str: + """Render plugin-ready skill content from generated base skill files.""" + skill_path = OUTPUT_SKILLS_DIR / skill_name / "SKILL.md" + if not skill_path.exists(): + raise FileNotFoundError(f"Missing generated skill content for {skill_name}: {skill_path}") + + skill_content = skill_path.read_text() + schema_mappings = SCHEMA_MAPPINGS.get(skill_name, []) + if not schema_mappings: + return skill_content + + schema_docs = [] + for schema_name, schema_key in schema_mappings: + schema_yaml = schema_yaml_content.get(schema_key) + if not schema_yaml: + continue + schema_docs.append( + format_schema_for_markdown( + schema_yaml=schema_yaml, + schema_name=schema_name, + file_pattern=f"*.{schema_key}.yaml", + ) + ) + + if not schema_docs: + return skill_content + + return f"{skill_content}\n\n" + "\n\n".join(schema_docs) + + +def resolve_plugin_skills_dir(plugin_dir: Path) -> Path: + """Resolve the plugin skills directory from a repo root, plugin root, or skills dir.""" + plugin_dir = plugin_dir.expanduser().resolve() + + repo_skills_dir = plugin_dir / "plugins" / "windmill-code-plugin" / "skills" + repo_plugin_json = plugin_dir / "plugins" / "windmill-code-plugin" / ".claude-plugin" / "plugin.json" + if repo_plugin_json.exists(): + return repo_skills_dir + + plugin_skills_dir = plugin_dir / "skills" + plugin_json = plugin_dir / ".claude-plugin" / "plugin.json" + if plugin_json.exists(): + return plugin_skills_dir + + if plugin_dir.name == "skills": + return plugin_dir + + return plugin_skills_dir + + +def generate_plugin_skills( + plugin_dir: Path, + skills: list[str], + schema_yaml_content: dict[str, str], +) -> Path: + """Generate standalone skills in a Claude plugin checkout.""" + skills_dir = resolve_plugin_skills_dir(plugin_dir) + skills_dir.mkdir(parents=True, exist_ok=True) + + expected_skills = set(skills) + for existing in skills_dir.iterdir(): + if existing.is_dir() and existing.name not in expected_skills: + shutil.rmtree(existing) + + for skill_name in skills: + skill_dir = skills_dir / skill_name + skill_dir.mkdir(parents=True, exist_ok=True) + (skill_dir / "SKILL.md").write_text( + render_plugin_skill_content(skill_name, schema_yaml_content) + ) + + print(f"\nGenerated for plugin:") + print(f" - {skills_dir} ({len(skills)} skills)") + return skills_dir + + # ============================================================================= # Main Entry Point # ============================================================================= +def parse_args() -> argparse.Namespace: + """Parse command line arguments.""" + parser = argparse.ArgumentParser( + description=( + "Generate Windmill system prompts, CLI guidance, and optionally " + "plugin-ready standalone skills." + ) + ) + parser.add_argument( + "--plugin-dir", + type=Path, + help=( + "Optional plugin target. Accepts a windmill-claude-plugin repo root, " + "a plugin root, or a skills directory, and refreshes standalone skills there." + ), + ) + return parser.parse_args() + + def main(): """Main generation function.""" + args = parse_args() + print("Generating system prompts documentation...") # Ensure output directories exist @@ -1344,6 +1455,10 @@ export function getDatatableSdkReference(): string { print(f" - auto-generated/schemas/ ({len(schema_yaml_content)} schema files)") print(f"\nGenerated for CLI:") print(f" - cli/src/guidance/skills.ts") + + if args.plugin_dir: + generate_plugin_skills(args.plugin_dir, skills, schema_yaml_content) + print("\nDone!")