From d3bda083c96d849c7100b1127ba0f054c62d7df7 Mon Sep 17 00:00:00 2001
From: Sebastian Sieber <>
Date: Mon, 22 Jun 2026 18:15:33 +0200
Subject: [PATCH 1/2] feat(plugin): add cowork-converter
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Converts a GitHub Copilot CLI / Claude Code plugin to a Microsoft 365
Copilot Cowork package (.zip).
- skills/cowork-converter/skill.md — skill definition with source
format detection, validation rules, and sideload instructions
- skills/cowork-converter/scripts/convert.py — Python 3 converter
(stdlib only, no third-party deps): discovers skills, validates
name/folder match and companion file limits, resolves shared
references per-skill, generates manifest.json v1.28, creates
placeholder icons, packages a compliant zip
- plugins/cowork-converter/.github/plugin/plugin.json — plugin metadata
- plugins/cowork-converter/README.md — usage documentation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.github/plugin/marketplace.json | 6 +
docs/README.plugins.md | 1 +
docs/README.skills.md | 1 +
.../.github/plugin/plugin.json | 22 ++
plugins/cowork-converter/README.md | 61 ++++
skills/cowork-converter/scripts/convert.py | 343 ++++++++++++++++++
skills/cowork-converter/skill.md | 165 +++++++++
7 files changed, 599 insertions(+)
create mode 100644 plugins/cowork-converter/.github/plugin/plugin.json
create mode 100644 plugins/cowork-converter/README.md
create mode 100644 skills/cowork-converter/scripts/convert.py
create mode 100644 skills/cowork-converter/skill.md
diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json
index f45cd7b24..a5a502076 100644
--- a/.github/plugin/marketplace.json
+++ b/.github/plugin/marketplace.json
@@ -190,6 +190,12 @@
"description": "Build applications with the GitHub Copilot SDK across multiple programming languages. Includes comprehensive instructions for C#, Go, Node.js/TypeScript, and Python to help you create AI-powered applications.",
"version": "1.0.0"
},
+ {
+ "name": "cowork-converter",
+ "source": "cowork-converter",
+ "description": "Convert a GitHub Copilot CLI / Claude Code plugin to a Microsoft 365 Copilot Cowork package (.zip). Handles skill discovery, shared-reference resolution, manifest generation, icon placeholders, validation, and zip packaging.",
+ "version": "1.0.0"
+ },
{
"name": "csharp-dotnet-development",
"source": "csharp-dotnet-development",
diff --git a/docs/README.plugins.md b/docs/README.plugins.md
index 78780526d..72d331898 100644
--- a/docs/README.plugins.md
+++ b/docs/README.plugins.md
@@ -40,6 +40,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-plugins) for guidelines on how t
| [context-engineering](../plugins/context-engineering/README.md) | Tools and techniques for maximizing GitHub Copilot effectiveness through better context management. Includes guidelines for structuring code, an agent for planning multi-file changes, and prompts for context-aware development. | 4 items | context, productivity, refactoring, best-practices, architecture |
| [context-matic](../plugins/context-matic/README.md) | Coding agents hallucinate APIs. ContextMatic gives them curated, versioned API and SDK docs. Ask your agent to "integrate the payments API" and it guesses — falling back on outdated training data and generic patterns that don't match your actual SDK. ContextMatic solves this by giving the agent deterministic, version-aware, SDK-native context at the exact moment it's needed. | 2 items | api-context, api-integration, mcp, sdk, apimatic, third-party-apis, sdks |
| [copilot-sdk](../plugins/copilot-sdk/README.md) | Build applications with the GitHub Copilot SDK across multiple programming languages. Includes comprehensive instructions for C#, Go, Node.js/TypeScript, and Python to help you create AI-powered applications. | 1 items | copilot-sdk, sdk, csharp, go, nodejs, typescript, python, ai, github-copilot |
+| [cowork-converter](../plugins/cowork-converter/README.md) | Convert a GitHub Copilot CLI / Claude Code plugin to a Microsoft 365 Copilot Cowork package (.zip). Handles skill discovery, shared-reference resolution, manifest generation, icon placeholders, validation, and zip packaging. | 1 items | cowork, microsoft-365, m365, plugin-converter, agent-skills, copilot |
| [csharp-dotnet-development](../plugins/csharp-dotnet-development/README.md) | Essential prompts, instructions, and chat modes for C# and .NET development including testing, documentation, and best practices. | 9 items | csharp, dotnet, aspnet, testing |
| [database-data-management](../plugins/database-data-management/README.md) | Database administration, SQL optimization, and data management tools for PostgreSQL, SQL Server, and general database development best practices. | 6 items | database, sql, postgresql, sql-server, dba, optimization, queries, data-management |
| [dataverse-sdk-for-python](../plugins/dataverse-sdk-for-python/README.md) | Comprehensive collection for building production-ready Python integrations with Microsoft Dataverse. Includes official documentation, best practices, advanced features, file operations, and code generation prompts. | 4 items | dataverse, python, integration, sdk |
diff --git a/docs/README.skills.md b/docs/README.skills.md
index 789e7eca7..39c2f71a2 100644
--- a/docs/README.skills.md
+++ b/docs/README.skills.md
@@ -107,6 +107,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to
| [copilot-spaces](../skills/copilot-spaces/SKILL.md)
`gh skills install github/awesome-copilot copilot-spaces` | Use Copilot Spaces to provide project-specific context to conversations. Use this skill when users mention a "Copilot space", want to load context from a shared knowledge base, discover available spaces, or ask questions grounded in curated project documentation, code, and instructions. | None |
| [copilot-usage-metrics](../skills/copilot-usage-metrics/SKILL.md)
`gh skills install github/awesome-copilot copilot-usage-metrics` | Retrieve and display GitHub Copilot usage metrics for organizations and enterprises using the GitHub CLI and REST API. | `get-enterprise-metrics.sh`
`get-enterprise-user-metrics.sh`
`get-org-metrics.sh`
`get-org-user-metrics.sh` |
| [cosmosdb-datamodeling](../skills/cosmosdb-datamodeling/SKILL.md)
`gh skills install github/awesome-copilot cosmosdb-datamodeling` | Step-by-step guide for capturing key application requirements for NoSQL use-case and produce Azure Cosmos DB Data NoSQL Model design using best practices and common patterns, artifacts_produced: "cosmosdb_requirements.md" file and "cosmosdb_data_model.md" file | None |
+| [cowork-converter](../skills/cowork-converter/SKILL.md)
`gh skills install github/awesome-copilot cowork-converter` | Convert a GitHub Copilot CLI plugin, Claude Code plugin, or any agent-skills directory into a Microsoft 365 Copilot Cowork package (.zip). Use when the user asks to convert, package, or publish a plugin to Cowork, mentions 'Cowork plugin', 'M365 Cowork', or asks to create a cowork.zip. Also triggers if the user asks to transform skills for Microsoft 365 Copilot distribution. | `scripts/convert.py`
`skill.md` |
| [create-agentsmd](../skills/create-agentsmd/SKILL.md)
`gh skills install github/awesome-copilot create-agentsmd` | Prompt for generating an AGENTS.md file for a repository | None |
| [create-architectural-decision-record](../skills/create-architectural-decision-record/SKILL.md)
`gh skills install github/awesome-copilot create-architectural-decision-record` | Create an Architectural Decision Record (ADR) document for AI-optimized decision documentation. | None |
| [create-github-action-workflow-specification](../skills/create-github-action-workflow-specification/SKILL.md)
`gh skills install github/awesome-copilot create-github-action-workflow-specification` | Create a formal specification for an existing GitHub Actions CI/CD workflow, optimized for AI consumption and workflow maintenance. | None |
diff --git a/plugins/cowork-converter/.github/plugin/plugin.json b/plugins/cowork-converter/.github/plugin/plugin.json
new file mode 100644
index 000000000..2f979aae0
--- /dev/null
+++ b/plugins/cowork-converter/.github/plugin/plugin.json
@@ -0,0 +1,22 @@
+{
+ "name": "cowork-converter",
+ "version": "1.0.0",
+ "description": "Convert a GitHub Copilot CLI / Claude Code plugin to a Microsoft 365 Copilot Cowork package (.zip). Handles skill discovery, shared-reference resolution, manifest generation, icon placeholders, validation, and zip packaging.",
+ "keywords": [
+ "cowork",
+ "microsoft-365",
+ "m365",
+ "plugin-converter",
+ "agent-skills",
+ "copilot"
+ ],
+ "author": {
+ "name": "sebastian-sieber",
+ "url": "https://github.com/sebastian-sieber"
+ },
+ "repository": "https://github.com/github/awesome-copilot",
+ "license": "MIT",
+ "skills": [
+ "./skills/cowork-converter/"
+ ]
+}
diff --git a/plugins/cowork-converter/README.md b/plugins/cowork-converter/README.md
new file mode 100644
index 000000000..f966ec3b2
--- /dev/null
+++ b/plugins/cowork-converter/README.md
@@ -0,0 +1,61 @@
+# cowork-converter
+
+Convert a GitHub Copilot CLI plugin, Claude Code plugin, or any agent-skills directory into a distributable **Microsoft 365 Copilot Cowork package** (`.zip`).
+
+## What it does
+
+Transforms an existing plugin into the M365 Unified App Manifest v1.28 format required by Copilot Cowork:
+
+- Discovers all skills (`SKILL.md` files) from Copilot CLI, Claude Code, or bare-skills layouts
+- Validates name/folder match and companion file limits per the Cowork spec
+- Resolves shared top-level references into per-skill `references/` folders
+- Generates `manifest.json` with `agentSkills[]` entries
+- Creates solid-color placeholder icons if none exist (pure stdlib PNG, no dependencies)
+- Packages a compliant `.zip` rooted at `manifest.json`
+- Prints a per-skill validation report
+
+## Usage
+
+After installing the plugin, ask Copilot:
+
+> *"Convert my plugin at ~/my-plugin to a Cowork package"*
+> *"Package these skills as a Cowork zip"*
+> *"Build a cowork.zip from this Claude plugin"*
+
+Or run the script directly:
+
+```bash
+python skills/cowork-converter/scripts/convert.py \
+ --source ./my-plugin \
+ --output ./dist \
+ --name-short "My Plugin" \
+ --name-full "My Plugin for Copilot Cowork" \
+ --description-short "One-line description" \
+ --description-full "Full description." \
+ --developer-name "Your Name" \
+ --website "https://example.com" \
+ --privacy-url "https://example.com/privacy" \
+ --terms-url "https://example.com/terms"
+```
+
+## Requirements
+
+- Python 3.8+ (standard library only, no third-party packages)
+
+## Output
+
+```
+-cowork/
+├── manifest.json
+├── color.png # 192×192
+├── outline.png # 32×32
+└── skills/
+ └── /
+ ├── SKILL.md
+ └── references/
+-cowork.zip
+```
+
+## Source
+
+Plugin maintained at [sebastian-sieber/cowork-converter](https://github.com/sebastian-sieber/cowork-converter).
diff --git a/skills/cowork-converter/scripts/convert.py b/skills/cowork-converter/scripts/convert.py
new file mode 100644
index 000000000..bf5c7ecc4
--- /dev/null
+++ b/skills/cowork-converter/scripts/convert.py
@@ -0,0 +1,343 @@
+#!/usr/bin/env python3
+"""
+Cowork Plugin Converter
+Converts a GitHub Copilot CLI / Claude Code plugin to a Microsoft 365 Cowork package.
+"""
+
+import argparse
+import json
+import os
+import re
+import shutil
+import struct
+import sys
+import uuid
+import zipfile
+import zlib
+from pathlib import Path
+
+# ── Helpers ──────────────────────────────────────────────────────────────────
+
+KEBAB_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
+FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
+FIELD_RE = re.compile(r"^(\w[\w-]*):\s*(.+)$", re.MULTILINE)
+
+UNSAFE_NAMES = {
+ "CON", "PRN", "AUX", "NUL",
+ *[f"COM{i}" for i in range(1, 10)],
+ *[f"LPT{i}" for i in range(1, 10)],
+}
+
+
+def parse_frontmatter(text: str) -> dict:
+ m = FRONTMATTER_RE.match(text)
+ if not m:
+ return {}
+ block = m.group(1)
+ fields: dict = {}
+ # Handle multi-line values (yaml block scalars): naive single-key extraction
+ for key, val in FIELD_RE.findall(block):
+ fields[key] = val.strip().strip('"').strip("'")
+ return fields
+
+
+def validate_name(name: str) -> list[str]:
+ errors = []
+ if not name:
+ errors.append("'name' field is missing")
+ return errors
+ if not KEBAB_RE.match(name):
+ errors.append(f"'name' field '{name}' is not valid kebab-case")
+ if len(name) > 64:
+ errors.append(f"'name' field '{name}' exceeds 64 characters")
+ return errors
+
+
+def validate_description(desc: str) -> list[str]:
+ if not desc:
+ return ["'description' field is missing"]
+ if len(desc) > 1024:
+ return [f"'description' field exceeds 1024 characters ({len(desc)} chars)"]
+ return []
+
+
+def companion_file_ok(path: Path) -> tuple[bool, str]:
+ name = path.name
+ if name.startswith("."):
+ return False, f"hidden file: {name}"
+ if name.upper() in UNSAFE_NAMES:
+ return False, f"Windows reserved name: {name}"
+ if ".." in path.parts:
+ return False, f"path traversal: {path}"
+ return True, ""
+
+
+def generate_placeholder_icon(size: int, color_hex: str = "#0078D4") -> bytes:
+ """Generate a minimal solid-color PNG."""
+ r = int(color_hex[1:3], 16)
+ g = int(color_hex[3:5], 16)
+ b = int(color_hex[5:7], 16)
+
+ def chunk(name: bytes, data: bytes) -> bytes:
+ c = struct.pack(">I", len(data)) + name + data
+ crc = zlib.crc32(name + data) & 0xFFFFFFFF
+ return c + struct.pack(">I", crc)
+
+ # IHDR
+ ihdr = struct.pack(">IIBBBBB", size, size, 8, 2, 0, 0, 0)
+ # IDAT: one row per line, filter byte 0 + RGB per pixel
+ raw = (bytes([0]) + bytes([r, g, b] * size)) * size
+ compressed = zlib.compress(raw, 9)
+ png = b"\x89PNG\r\n\x1a\n"
+ png += chunk(b"IHDR", ihdr)
+ png += chunk(b"IDAT", compressed)
+ png += chunk(b"IEND", b"")
+ return png
+
+
+def discover_skills(source: Path) -> list[Path]:
+ """
+ Return a list of skill folders (each containing SKILL.md).
+ Supports:
+ - source/skills//SKILL.md (Copilot CLI / Claude plugin)
+ - source//SKILL.md (bare skills dir)
+ """
+ candidates: list[Path] = []
+
+ skills_dir = source / "skills"
+ if skills_dir.is_dir():
+ for d in sorted(skills_dir.iterdir()):
+ if d.is_dir() and (d / "SKILL.md").exists():
+ candidates.append(d)
+ if candidates:
+ return candidates
+
+ # Bare structure: SKILL.md directly in sub-dirs
+ for d in sorted(source.iterdir()):
+ if d.is_dir() and (d / "SKILL.md").exists():
+ candidates.append(d)
+
+ return candidates
+
+
+def find_shared_references(source: Path) -> list[Path]:
+ """Collect reference files at source root or source/references/."""
+ refs: list[Path] = []
+ ref_dir = source / "references"
+ if ref_dir.is_dir():
+ refs.extend(sorted(ref_dir.glob("*.md")))
+ # Top-level .md files that aren't SKILL.md or README-like
+ for f in sorted(source.glob("*.md")):
+ if f.name.upper() not in {"README.MD", "SKILL.MD", "CHANGELOG.MD", "LICENSE.MD"}:
+ refs.append(f)
+ return refs
+
+
+def skill_links_to(skill_md_text: str, filename: str) -> bool:
+ """Return True if the SKILL.md body references the given filename."""
+ return filename.lower() in skill_md_text.lower()
+
+
+def deterministic_uuid(name: str) -> str:
+ return str(uuid.uuid5(uuid.NAMESPACE_DNS, name))
+
+
+# ── Main ─────────────────────────────────────────────────────────────────────
+
+def convert(args: argparse.Namespace) -> int:
+ source = Path(args.source).expanduser().resolve()
+ if not source.is_dir():
+ print(f"ERROR: source directory not found: {source}", file=sys.stderr)
+ return 1
+
+ plugin_slug = re.sub(r"[^a-z0-9-]", "-", args.name_short.lower()).strip("-")
+ output_root = Path(args.output).expanduser().resolve() if args.output else source.parent
+ out = output_root / f"{plugin_slug}-cowork"
+
+ if out.exists():
+ print(f"Output directory already exists: {out}")
+ print("Remove it first or choose a different output path.")
+ return 1
+
+ out.mkdir(parents=True)
+ skills_out = out / "skills"
+ skills_out.mkdir()
+
+ # ── Discover skills ───────────────────────────────────────────────────────
+ skill_folders = discover_skills(source)
+ if not skill_folders:
+ print("ERROR: No skills found (looking for SKILL.md in sub-directories).", file=sys.stderr)
+ return 1
+
+ shared_refs = find_shared_references(source)
+
+ # ── Process skills ────────────────────────────────────────────────────────
+ agent_skills: list[dict] = []
+ report_rows: list[tuple] = []
+ all_valid = True
+
+ for skill_folder in skill_folders:
+ skill_md_path = skill_folder / "SKILL.md"
+ skill_text = skill_md_path.read_text(encoding="utf-8")
+ fm = parse_frontmatter(skill_text)
+ name = fm.get("name", "").strip()
+ description = fm.get("description", "").strip()
+ folder_name = skill_folder.name
+
+ errors = []
+ errors.extend(validate_name(name))
+ errors.extend(validate_description(description))
+
+ if name and name != folder_name:
+ errors.append(f"folder '{folder_name}' does not match name '{name}'")
+
+ # Use folder name as canonical name if missing
+ canonical = name or folder_name
+
+ dest_skill = skills_out / canonical
+ dest_skill.mkdir()
+
+ # Copy SKILL.md
+ shutil.copy2(skill_md_path, dest_skill / "SKILL.md")
+
+ # Copy all companion files from skill folder (excluding SKILL.md)
+ companion_count = 0
+ companion_total_size = 0
+
+ for item in sorted(skill_folder.rglob("*")):
+ if item == skill_md_path or not item.is_file():
+ continue
+ rel = item.relative_to(skill_folder)
+ ok, reason = companion_file_ok(rel)
+ if not ok:
+ print(f" SKIP {rel}: {reason}")
+ continue
+ dest_file = dest_skill / rel
+ dest_file.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copy2(item, dest_file)
+ size = item.stat().st_size
+ companion_count += 1
+ companion_total_size += size
+ if size > 5 * 1024 * 1024:
+ errors.append(f"companion file {rel} exceeds 5 MB")
+
+ # Copy shared references if skill links to them
+ refs_dest = dest_skill / "references"
+ for ref in shared_refs:
+ if skill_links_to(skill_text, ref.name):
+ refs_dest.mkdir(exist_ok=True)
+ dest_ref = refs_dest / ref.name
+ if not dest_ref.exists():
+ shutil.copy2(ref, dest_ref)
+ companion_count += 1
+ companion_total_size += ref.stat().st_size
+
+ if companion_count > 20:
+ errors.append(f"exceeds 20 companion files ({companion_count})")
+ if companion_total_size > 10 * 1024 * 1024:
+ errors.append(f"total companion size {companion_total_size // 1024} KB exceeds 10 MB")
+
+ agent_skills.append({"folder": f"./skills/{canonical}"})
+
+ status = "✓" if not errors else "✗ " + "; ".join(errors)
+ if errors:
+ all_valid = False
+ report_rows.append((canonical, status, f"{companion_count}/20", f"{companion_total_size // 1024} KB"))
+
+ # ── Icons ─────────────────────────────────────────────────────────────────
+ icon_note = ""
+
+ for filename, size in [("color.png", 192), ("outline.png", 32)]:
+ src_icon = source / filename
+ dest_icon = out / filename
+ if src_icon.exists():
+ shutil.copy2(src_icon, dest_icon)
+ else:
+ placeholder = generate_placeholder_icon(size, args.accent_color)
+ dest_icon.write_bytes(placeholder)
+ icon_note += f" ⚠ {filename}: generated placeholder ({size}×{size}). Replace before store submission.\n"
+
+ # ── Manifest ──────────────────────────────────────────────────────────────
+ plugin_id = args.app_id or deterministic_uuid(f"cowork.{plugin_slug}")
+
+ manifest = {
+ "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.28/MicrosoftTeams.schema.json",
+ "manifestVersion": "1.28",
+ "version": "1.0.0",
+ "id": plugin_id,
+ "developer": {
+ "name": args.developer_name,
+ "websiteUrl": args.website,
+ "privacyUrl": args.privacy_url,
+ "termsOfUseUrl": args.terms_url,
+ },
+ "name": {
+ "short": args.name_short,
+ "full": args.name_full,
+ },
+ "description": {
+ "short": args.description_short,
+ "full": args.description_full,
+ },
+ "icons": {
+ "color": "color.png",
+ "outline": "outline.png",
+ },
+ "accentColor": args.accent_color,
+ "agentSkills": agent_skills,
+ }
+
+ (out / "manifest.json").write_text(
+ json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
+ )
+
+ # ── ZIP ───────────────────────────────────────────────────────────────────
+ zip_path = output_root / f"{plugin_slug}-cowork.zip"
+ with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
+ for file in sorted(out.rglob("*")):
+ if file.is_file() and not file.name.startswith("."):
+ zf.write(file, file.relative_to(out))
+
+ # ── Report ────────────────────────────────────────────────────────────────
+ col_w = max(len(r[0]) for r in report_rows) + 2
+ print(f"\n{'Skill':<{col_w}}{'Name/folder match':<22}{'Companions':<14}Size")
+ print("-" * (col_w + 46))
+ for skill, status, companions, size in report_rows:
+ print(f"{skill:<{col_w}}{status:<22}{companions:<14}{size}")
+
+ print(f"\nTotal: {len(agent_skills)} skill(s), {'all valid ✓' if all_valid else 'validation errors above ✗'}")
+ print(f"\nOutput: {out}")
+ print(f"Package: {zip_path}")
+
+ if icon_note:
+ print(f"\nIcon warnings:\n{icon_note.rstrip()}")
+
+ print(f"\nNext — sideload for testing:")
+ print(f" npm install -g @microsoft/m365agentstoolkit-cli")
+ print(f" atk auth login")
+ print(f" atk install --file-path \"{zip_path}\" --scope Personal")
+
+ return 0 if all_valid else 2
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(
+ description="Convert a Copilot CLI / Claude plugin to a Microsoft 365 Cowork package."
+ )
+ parser.add_argument("--source", required=True, help="Source plugin directory")
+ parser.add_argument("--output", default=None, help="Output directory (default: parent of source)")
+ parser.add_argument("--name-short", required=True, help="Short name (≤30 chars)")
+ parser.add_argument("--name-full", required=True, help="Full display name")
+ parser.add_argument("--description-short", required=True, help="Short description (≤80 chars)")
+ parser.add_argument("--description-full", required=True, help="Full description")
+ parser.add_argument("--developer-name", default="Unknown", help="Developer name")
+ parser.add_argument("--website", default="https://example.com")
+ parser.add_argument("--privacy-url", default="https://example.com/privacy")
+ parser.add_argument("--terms-url", default="https://example.com/terms")
+ parser.add_argument("--accent-color", default="#0078D4")
+ parser.add_argument("--app-id", default=None, help="Override auto-generated GUID")
+ return convert(parser.parse_args())
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/skills/cowork-converter/skill.md b/skills/cowork-converter/skill.md
new file mode 100644
index 000000000..2e2c72881
--- /dev/null
+++ b/skills/cowork-converter/skill.md
@@ -0,0 +1,165 @@
+---
+name: cowork-converter
+description: "Convert a GitHub Copilot CLI plugin, Claude Code plugin, or any agent-skills directory into a Microsoft 365 Copilot Cowork package (.zip). Use when the user asks to convert, package, or publish a plugin to Cowork, mentions 'Cowork plugin', 'M365 Cowork', or asks to create a cowork.zip. Also triggers if the user asks to transform skills for Microsoft 365 Copilot distribution."
+---
+
+# Cowork Plugin Converter
+
+Convert any Copilot CLI / Claude Code plugin into a distributable Microsoft 365 Copilot Cowork package.
+
+## What this skill produces
+
+```
+-cowork/
+├── manifest.json # M365 Unified App Manifest v1.28 with agentSkills
+├── color.png # 192×192 color icon
+├── outline.png # 32×32 outline icon
+└── skills/
+ └── /
+ ├── SKILL.md
+ └── references/ # Per-skill copy of shared reference files
+-cowork.zip # Ready-to-sideload package
+```
+
+## Inputs to gather
+
+Before running the conversion script, confirm:
+
+1. **Source plugin directory** — the folder containing the existing skills. If not provided, ask the user.
+2. **Output directory** — where to write the cowork package. Default: parent of source dir.
+3. **Plugin name** — short (≤30 chars) and full display name. Infer from source directory name if `plugin.json` / `manifest.json` is absent, then confirm with user.
+4. **Description** — short (≤80 chars) and full (≤4000 chars). Infer from existing metadata if available.
+5. **Developer info** — name, website, privacy URL, terms URL. Use reasonable defaults (e.g., from existing plugin.json), but confirm if absent.
+6. **Accent color** — hex code. Default `#0078D4`.
+7. **Icons** — check whether `color.png` and `outline.png` already exist at the source root. If absent, generate solid-color placeholders using the script.
+
+## Source formats recognised
+
+| Source format | Skill detection | Shared refs detection |
+|---|---|---|
+| Copilot CLI plugin (`skills/*/skill.md` or `SKILL.md`) | Walk `skills/` sub-dirs | `references/` at plugin root |
+| Claude Code plugin (`.claude-plugin/plugin.json` + `skills/`) | Walk `skills/` sub-dirs | Top-level `.md` files listed in `plugin.json` |
+| Bare skills directory (`*/SKILL.md` at root) | Walk root sub-dirs | Any `references/` or `*.md` at root |
+
+## Conversion workflow
+
+Run `scripts/convert.py` (shown below). If it fails, follow the manual steps.
+
+### Step 1 — Run the converter script
+
+```bash
+python ~/.copilot/skills/cowork-converter/scripts/convert.py \
+ --source \
+ --output \
+ --name-short "My Plugin" \
+ --name-full "My Plugin for Copilot Cowork" \
+ --description-short "One-line description" \
+ --description-full "Full description up to 4000 chars." \
+ --developer-name "Your Name" \
+ --website "https://example.com" \
+ --privacy-url "https://example.com/privacy" \
+ --terms-url "https://example.com/terms" \
+ --accent-color "#0078D4"
+```
+
+The script:
+- Discovers all skills in the source directory
+- Validates each skill's `name` frontmatter matches its folder name
+- Copies skills to `skills//SKILL.md`
+- Copies companion files (references, scripts, etc.) into each skill's folder
+- Resolves shared top-level references: copies them into every skill's `references/` folder that links to them
+- Generates `manifest.json` with all `agentSkills` entries
+- Creates placeholder icons if none exist (solid `#0078D4` squares)
+- Packages everything into `-cowork.zip`
+- Prints a validation report
+
+### Step 2 — Review the validation report
+
+The script outputs a table like:
+
+```
+Skill Folder/name match Companion files Size
+audit ✓ 5/20 42 KB
+design-qa ✓ 5/20 38 KB
+...
+TOTAL: 11 skills, all valid
+```
+
+Fix any `✗` entries before distributing.
+
+### Step 3 — Replace placeholder icons (if needed)
+
+If placeholder icons were generated, the script warns you. Replace them:
+- `color.png`: 192×192 px, full-color PNG
+- `outline.png`: 32×32 px, single-color outline PNG
+
+### Step 4 — Sideload for testing
+
+```bash
+npm install -g @microsoft/m365agentstoolkit-cli
+atk auth login
+atk install --file-path -cowork.zip --scope Personal
+```
+
+### Step 5 — Publish to tenant
+
+Upload via **M365 Admin Center → Manage Apps → Upload custom app**, then enable in **Cowork → Sources & Skills**.
+
+## Validation rules (enforce these)
+
+- Skill folder name must exactly match the `name` field in `SKILL.md` frontmatter (kebab-case)
+- `name` field: 1–64 chars, kebab-case only (lowercase, hyphens, no underscores, no consecutive hyphens)
+- `description` field: 1–1024 chars
+- Max 20 companion files per skill
+- Max 5 MB per companion file, max 10 MB total per skill
+- No path traversal (`..`) in companion file paths
+- No hidden files (`.` prefix) as companion files
+- Icons must be PNG; `color.png` must be 192×192, `outline.png` must be 32×32
+
+## What is NOT converted (inform the user)
+
+| Source feature | Status |
+|---|---|
+| `commands/` (slash commands) | Not supported in Cowork |
+| `agents/` sub-agents or `agents/openai.yaml` | Not supported in Cowork |
+| `hooks/` event handlers | Not supported in Cowork |
+| `settings.json` | Not applicable |
+| `bin/` executables | Not applicable |
+| `.mcp.json` MCP servers | Converted to `agentConnectors[]` if URL is present |
+
+## MCP connector conversion (optional)
+
+If the source has a `.mcp.json` with remote HTTPS server entries, add them to `manifest.json`:
+
+```json
+"agentConnectors": [
+ {
+ "id": "",
+ "displayName": "",
+ "description": "",
+ "toolSource": {
+ "remoteMcpServer": {
+ "mcpServerUrl": "",
+ "authorization": {
+ "type": "OAuthPluginVault",
+ "referenceId": ""
+ }
+ }
+ }
+ }
+]
+```
+
+Local (`stdio`) MCP servers cannot be converted — inform the user.
+
+## Final response
+
+After a successful conversion, report:
+
+- Output directory path
+- Zip file path
+- Number of skills converted
+- List of skills with companion file counts
+- Any features skipped (not supported in Cowork)
+- Icon status (original vs generated placeholder)
+- Next step: sideload command
From d84f3d9d43eb43bca154ff6ab55d19a6db6119a1 Mon Sep 17 00:00:00 2001
From: Sebastian Sieber <>
Date: Mon, 22 Jun 2026 18:18:24 +0200
Subject: [PATCH 2/2] fix: rename skill.md to SKILL.md for awesome-copilot
validator
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
skills/cowork-converter/{skill.md => SKILL.md} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename skills/cowork-converter/{skill.md => SKILL.md} (100%)
diff --git a/skills/cowork-converter/skill.md b/skills/cowork-converter/SKILL.md
similarity index 100%
rename from skills/cowork-converter/skill.md
rename to skills/cowork-converter/SKILL.md