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/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 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())