@openhands/extensions
Advanced tools
| { | ||
| ".": "0.14.0" | ||
| ".": "0.15.0" | ||
| } |
@@ -17,3 +17,3 @@ { | ||
| "description": "Add comprehensive JavaDoc documentation to Java classes and methods. Use when documenting Java code, adding API documentation, or improving code documentation.", | ||
| "category": "documentation", | ||
| "category": "code-quality", | ||
| "keywords": [ | ||
@@ -56,3 +56,3 @@ "javadoc", | ||
| "description": "Upgrade Apache Spark applications between major versions (2.x→3.x, 3.x→4.x). Covers build files, deprecated APIs, configuration changes, SQL/DataFrame updates, and test validation.", | ||
| "category": "development", | ||
| "category": "environment", | ||
| "keywords": [ | ||
@@ -59,0 +59,0 @@ "spark", |
@@ -17,3 +17,3 @@ { | ||
| "description": "Create file-based sub-agents as Markdown files \u2014 no Python code required. Guides the user through a structured interview and generates a ready-to-deploy .md agent file following the OpenHands SDK specification.", | ||
| "category": "development", | ||
| "category": "agent-authoring", | ||
| "keywords": [ | ||
@@ -32,3 +32,3 @@ "agent", | ||
| "description": "Add (import) an OpenHands skill from a GitHub repository into the current workspace.", | ||
| "category": "productivity", | ||
| "category": "agent-authoring", | ||
| "keywords": [ | ||
@@ -45,3 +45,3 @@ "skill", | ||
| "description": "Persist and retrieve repository-specific knowledge using AGENTS.md files. Use when you want to save important information about a codebase (build commands, code style, workflows) for future sessions.", | ||
| "category": "productivity", | ||
| "category": "agent-authoring", | ||
| "keywords": [ | ||
@@ -58,3 +58,3 @@ "memory", | ||
| "description": "Guided workflow for building custom AI agents using the OpenHands Software Agent SDK. Use when you want to create a new agent through an interactive interview process that gathers requirements and generates implementation plans.", | ||
| "category": "development", | ||
| "category": "agent-authoring", | ||
| "keywords": [ | ||
@@ -71,3 +71,3 @@ "agent", | ||
| "description": "Simplifies and refines code across three dimensions - code reuse, code quality, and efficiency - while preserving all functionality. Analyzes recently modified code and provides actionable improvement suggestions.", | ||
| "category": "development", | ||
| "category": "code-quality", | ||
| "keywords": [ | ||
@@ -87,3 +87,3 @@ "simplify", | ||
| "description": "Create and manage OpenHands automations - scheduled tasks that run in sandboxes. Use the prompt preset to create automations from natural language, or manage existing automations.", | ||
| "category": "integration", | ||
| "category": "automations", | ||
| "keywords": [ | ||
@@ -101,3 +101,3 @@ "automation", | ||
| "description": "Work effectively inside a local Agent Canvas environment, including local agent-server auth, safe workspace hygiene, and local conversation delegation.", | ||
| "category": "development", | ||
| "category": "agent-authoring", | ||
| "keywords": [ | ||
@@ -115,3 +115,3 @@ "agent-canvas", | ||
| "description": "Reference skill for the OpenHands Software Agent SDK - build AI agents with custom tools, LLM configuration, conversations, sub-agent delegation, MCP integration, security, and persistence.", | ||
| "category": "development", | ||
| "category": "agent-authoring", | ||
| "keywords": [ | ||
@@ -130,3 +130,3 @@ "sdk", | ||
| "description": "Interact with Azure DevOps repositories, pull requests, and APIs using the AZURE_DEVOPS_TOKEN environment variable. Use when working with code hosted on Azure DevOps or managing Azure DevOps resources.", | ||
| "category": "integration", | ||
| "category": "code-hosting", | ||
| "keywords": [ | ||
@@ -143,3 +143,3 @@ "azure", | ||
| "description": "Bitbucket integration hub. Detects whether the repository is on Bitbucket Cloud or Bitbucket Data Center and directs you to the matching detailed skill (bitbucket-cloud or bitbucket-data-center). Use for any Bitbucket repository or pull request task.", | ||
| "category": "integration", | ||
| "category": "code-hosting", | ||
| "keywords": [ | ||
@@ -155,3 +155,3 @@ "bitbucket", | ||
| "description": "Bitbucket Cloud (bitbucket.org) specifics — authenticate with BITBUCKET_TOKEN, use the REST API v2, workspace/repo_slug repositories, and the create_bitbucket_pr tool. Loaded on demand by the bitbucket skill once a Cloud environment is detected.", | ||
| "category": "integration", | ||
| "category": "code-hosting", | ||
| "keywords": [ | ||
@@ -168,3 +168,3 @@ "bitbucket", | ||
| "description": "Bitbucket Data Center (self-hosted Bitbucket Server) specifics — authenticate with BITBUCKET_DATA_CENTER_TOKEN, use the REST API 1.0, PROJECT/repo_slug repositories, scm/ git remotes, and the create_bitbucket_data_center_pr tool. Loaded on demand by the bitbucket skill once a Data Center environment is detected.", | ||
| "category": "integration", | ||
| "category": "code-hosting", | ||
| "keywords": [ | ||
@@ -209,3 +209,3 @@ "bitbucket", | ||
| "description": "Query and analyze Datadog logs, metrics, APM traces, and monitors using the Datadog API. Use when debugging production issues, monitoring application performance, or investigating alerts.", | ||
| "category": "monitoring", | ||
| "category": "integrations", | ||
| "keywords": [ | ||
@@ -223,3 +223,3 @@ "datadog", | ||
| "description": "Common project operations using Deno (tasks, run/test/lint/fmt, and dependency management).", | ||
| "category": "development", | ||
| "category": "environment", | ||
| "keywords": [ | ||
@@ -236,3 +236,3 @@ "deno", | ||
| "description": "Build and automate Discord integrations (bots, webhooks, slash commands, and REST API workflows). Use when the user mentions Discord, a Discord server/guild, channels, webhooks, bot tokens, slash commands/application commands, discord.js, or discord.py.", | ||
| "category": "integration", | ||
| "category": "integrations", | ||
| "keywords": [ | ||
@@ -249,3 +249,3 @@ "discord", | ||
| "description": "Run Docker commands within a container environment, including starting the Docker daemon and managing containers. Use when building, running, or managing Docker containers and images.", | ||
| "category": "infrastructure", | ||
| "category": "environment", | ||
| "keywords": [ | ||
@@ -261,3 +261,3 @@ "docker", | ||
| "description": "A test skill that responds to the magic word 'flarglebargle' with a compliment. Use for testing skill activation and trigger functionality.", | ||
| "category": "testing", | ||
| "category": "other", | ||
| "keywords": [ | ||
@@ -287,3 +287,3 @@ "test", | ||
| "description": "Interact with GitHub repositories, pull requests, issues, and workflows using the GITHUB_TOKEN environment variable and GitHub CLI. Use when working with code hosted on GitHub or managing GitHub resources.", | ||
| "category": "integration", | ||
| "category": "code-hosting", | ||
| "keywords": [ | ||
@@ -301,3 +301,3 @@ "github", | ||
| "description": "Create, debug, and test GitHub Actions workflows and custom actions. Use when building CI/CD pipelines, automating workflows, or troubleshooting GitHub Actions.", | ||
| "category": "integration", | ||
| "category": "code-hosting", | ||
| "keywords": [ | ||
@@ -315,3 +315,3 @@ "github-actions", | ||
| "description": "Post structured PR reviews to GitHub with inline comments/suggestions in a single API call.", | ||
| "category": "code-quality", | ||
| "category": "code-hosting", | ||
| "keywords": [ | ||
@@ -328,3 +328,3 @@ "github", | ||
| "description": "Create a cron automation that polls a GitHub repository for issue and PR comments containing a configurable trigger phrase (@OpenHands by default). Starts an OpenHands conversation with full issue/PR context, posts acknowledgement comments with a conversation link, and summarises results back to the issue/PR when the agent finishes.", | ||
| "category": "productivity", | ||
| "category": "automations", | ||
| "keywords": [ | ||
@@ -345,3 +345,3 @@ "github", | ||
| "description": "Interact with GitLab repositories, merge requests, and APIs using the GITLAB_TOKEN environment variable. Use when working with code hosted on GitLab or managing GitLab resources.", | ||
| "category": "integration", | ||
| "category": "code-hosting", | ||
| "keywords": [ | ||
@@ -357,3 +357,3 @@ "gitlab", | ||
| "description": "Read, modify, execute, and convert Jupyter notebooks programmatically. Use when working with .ipynb files for data science workflows, including editing cells, clearing outputs, or converting to other formats.", | ||
| "category": "data-science", | ||
| "category": "environment", | ||
| "keywords": [ | ||
@@ -370,3 +370,3 @@ "jupyter", | ||
| "description": "Set up and manage local Kubernetes clusters using KIND (Kubernetes IN Docker). Use when testing Kubernetes applications locally or developing cloud-native workloads.", | ||
| "category": "infrastructure", | ||
| "category": "environment", | ||
| "keywords": [ | ||
@@ -396,3 +396,3 @@ "kubernetes", | ||
| "description": "Interact with Linear project management - query issues, update status, create tickets using the Linear GraphQL API.", | ||
| "category": "integration", | ||
| "category": "integrations", | ||
| "keywords": [ | ||
@@ -423,3 +423,3 @@ "linear", | ||
| "description": "Create, search, and update Notion pages/databases using the Notion API. Use for documenting work, generating runbooks, and automating knowledge base updates.", | ||
| "category": "productivity", | ||
| "category": "integrations", | ||
| "keywords": [ | ||
@@ -435,3 +435,3 @@ "notion", | ||
| "description": "Handle npm package installation in non-interactive environments by piping confirmations. Use when installing Node.js packages that require user confirmation prompts.", | ||
| "category": "development", | ||
| "category": "environment", | ||
| "keywords": [ | ||
@@ -473,3 +473,3 @@ "npm", | ||
| "description": "Use the OpenHands Cloud REST API (V1) and agent-server APIs to create and manage Cloud or local backend conversations, including multi-conversation delegation workflows. Includes minimal Python and TypeScript clients under scripts/.", | ||
| "category": "development", | ||
| "category": "agent-authoring", | ||
| "keywords": [ | ||
@@ -490,3 +490,3 @@ "openhands", | ||
| "description": "Install and use pdflatex to compile LaTeX documents into PDFs on Linux. Use when generating academic papers, research publications, or any documents written in LaTeX.", | ||
| "category": "documentation", | ||
| "category": "environment", | ||
| "keywords": [ | ||
@@ -528,3 +528,3 @@ "latex", | ||
| "description": "Generate a Product Requirements Document (PRD) for a new feature through an interactive clarifying-question workflow. Use when planning a feature, starting a new project, or when asked to create a PRD.", | ||
| "category": "productivity", | ||
| "category": "writing", | ||
| "keywords": [ | ||
@@ -554,3 +554,3 @@ "prd", | ||
| "description": "Security best practices for secure coding, authentication, authorization, and data protection. Use when developing features that handle sensitive data, user authentication, or require security review.", | ||
| "category": "security", | ||
| "category": "code-quality", | ||
| "keywords": [ | ||
@@ -567,3 +567,3 @@ "security", | ||
| "description": "Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.", | ||
| "category": "development", | ||
| "category": "agent-authoring", | ||
| "keywords": [ | ||
@@ -579,3 +579,3 @@ "skill", | ||
| "description": "Create a cron automation that polls up to 10 Slack channels every minute and starts an OpenHands conversation when a configurable trigger phrase is detected. Forwards thread replies to running conversations and posts summaries back to Slack when the agent finishes.", | ||
| "category": "productivity", | ||
| "category": "automations", | ||
| "keywords": [ | ||
@@ -595,3 +595,3 @@ "slack", | ||
| "description": "Establish and manage SSH connections to remote machines, including key generation, configuration, and file transfers. Use when connecting to remote servers, executing remote commands, or transferring files via SCP.", | ||
| "category": "infrastructure", | ||
| "category": "environment", | ||
| "keywords": [ | ||
@@ -608,3 +608,3 @@ "ssh", | ||
| "description": "Install and configure Swift programming language on Debian Linux for server-side development. Use when building Swift applications on Linux or setting up a Swift development environment.", | ||
| "category": "development", | ||
| "category": "environment", | ||
| "keywords": [ | ||
@@ -633,3 +633,3 @@ "swift", | ||
| "description": "Common project, dependency, and environment operations using uv.", | ||
| "category": "development", | ||
| "category": "environment", | ||
| "keywords": [ | ||
@@ -646,3 +646,3 @@ "uv", | ||
| "description": "Keep a long-lived fork in sync with its upstream. Creates a cron automation that fetches upstream changes, rebases local customizations on top, verifies the software works, and replaces the running version when the rebase is clean.", | ||
| "category": "integration", | ||
| "category": "integrations", | ||
| "keywords": [ | ||
@@ -661,3 +661,3 @@ "fork", | ||
| "description": "Deploy and manage applications on Vercel, including preview deployments and deployment protection.", | ||
| "category": "integration", | ||
| "category": "integrations", | ||
| "keywords": [ | ||
@@ -687,3 +687,3 @@ "vercel", | ||
| "description": "Iterate on a GitHub pull request \u2014 drive it through CI, code review, and QA until merge-ready. Monitors state, fixes failures, addresses review feedback, retries flaky checks, and pushes fixes in one continuous loop.", | ||
| "category": "productivity", | ||
| "category": "code-quality", | ||
| "keywords": [ | ||
@@ -702,3 +702,3 @@ "github", | ||
| "description": "Back factual claims and field values with official, verifiable sources. Use when the user asks to fill fields, answer questions, or make claims that must be supported by an exact quote and an official link.", | ||
| "category": "productivity", | ||
| "category": "writing", | ||
| "keywords": [ | ||
@@ -717,3 +717,3 @@ "evidence", | ||
| "description": "Create an automation that reviews GitHub pull requests when they are opened or updated. Inspects the diff, changed files, tests, and existing discussion via GitHub MCP, then posts a concise review highlighting risks, security issues, missing tests, and next steps.", | ||
| "category": "productivity", | ||
| "category": "automations", | ||
| "keywords": [ | ||
@@ -732,3 +732,3 @@ "github", | ||
| "description": "Create an automation that drafts incident retrospectives by gathering incident-channel messages from Slack, collecting linked tickets from Linear, and publishing a retrospective draft to Notion with timeline, impact summary, root-cause hypotheses, and action items.", | ||
| "category": "productivity", | ||
| "category": "automations", | ||
| "keywords": [ | ||
@@ -748,3 +748,3 @@ "incident", | ||
| "description": "Create an automation that triages new Linear issues by inspecting title, description, team, and recent related issues. Suggests labels, priority, likely owner, and potential duplicates, then posts a clarifying comment.", | ||
| "category": "productivity", | ||
| "category": "automations", | ||
| "keywords": [ | ||
@@ -764,3 +764,3 @@ "linear", | ||
| "description": "Create a recurring automation that researches a topic using Tavily web search and publishes a structured brief to Notion with executive summary, key findings, implications, and source citations.", | ||
| "category": "productivity", | ||
| "category": "automations", | ||
| "keywords": [ | ||
@@ -779,3 +779,3 @@ "research", | ||
| "description": "Create an automation that generates an async standup digest from Slack. Searches selected channels for messages since the previous workday, groups updates by project, highlights blockers and decisions, and posts a summary to a target channel.", | ||
| "category": "productivity", | ||
| "category": "automations", | ||
| "keywords": [ | ||
@@ -795,3 +795,3 @@ "slack", | ||
| "description": "Write and revise technical explanations in flowing, direct, conversational prose that stays concise without becoming fragmented or vague.", | ||
| "category": "productivity", | ||
| "category": "writing", | ||
| "keywords": [ | ||
@@ -805,3 +805,3 @@ "technical-writing" | ||
| "description": "Write and edit clear, accessible prose in a plain English content style: active voice, front-loaded content, sentence case, and accessible formatting.", | ||
| "category": "productivity", | ||
| "category": "writing", | ||
| "keywords": [ | ||
@@ -815,3 +815,3 @@ "plain-english" | ||
| "description": "Deploy a cron-based OpenHands automation that watches a Jira Cloud project for issues labeled with a configurable label (default: create-pr) and spawns an agent conversation to create a GitHub pull request for each new issue found. The target GitHub repo is read from the ticket body - no repo parameter required at deploy time.", | ||
| "category": "productivity", | ||
| "category": "automations", | ||
| "keywords": [ | ||
@@ -818,0 +818,0 @@ "jira", |
+1
-1
| { | ||
| "name": "@openhands/extensions", | ||
| "version": "0.14.0", | ||
| "version": "0.15.0", | ||
| "description": "Public OpenHands extension catalogs for skills, plugins, integrations, and automation templates.", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
+1
-1
| [project] | ||
| name = "openhands-extensions" | ||
| version = "0.14.0" | ||
| version = "0.15.0" | ||
| description = "OpenHands extensions, plugins, and skills (Python bindings for the integration catalog)" | ||
@@ -5,0 +5,0 @@ requires-python = ">=3.12" |
@@ -21,3 +21,3 @@ """Package version, derived from installed package metadata. | ||
| #: the annotation must stay on the assignment; on its own line it is a no-op. | ||
| _FALLBACK_VERSION = "0.14.0" # x-release-please-version | ||
| _FALLBACK_VERSION = "0.15.0" # x-release-please-version | ||
@@ -24,0 +24,0 @@ try: |
+4
-1
@@ -185,4 +185,7 @@ # OpenHands Extensions | ||
| 4. (Optional) Add `README.md`, `references/`, `scripts/`, etc. | ||
| 5. Submit a pull request | ||
| 5. Add an entry to `marketplaces/openhands-extensions.json` with a `category` — this is what the OpenHands Skills page uses to group the skill. Skills with no entry are grouped as "Uncategorized". Marketplace entries also require `.plugin/plugin.json` and vendor symlinks. | ||
| 6. Submit a pull request | ||
| Valid skill categories: `automations`, `environment`, `code-hosting`, `agent-authoring`, `code-quality`, `integrations`, `writing`, `design`, `other`. | ||
| ### Adding a Plugin | ||
@@ -189,0 +192,0 @@ |
@@ -17,2 +17,58 @@ #!/usr/bin/env node | ||
| const MARKETPLACES_DIR = join(__dirname, "..", "marketplaces"); | ||
| const SKILL_SOURCE_PREFIX = "./skills/"; | ||
| /** | ||
| * Categories for skill entries, consumed by the agent-canvas /skills facet rail. | ||
| * | ||
| * Distinct from the `category` on marketplace *plugin* entries, which serves Claude Code marketplace browsing and keeps its own values. | ||
| */ | ||
| export const SKILL_CATEGORY_IDS = [ | ||
| "automations", | ||
| "environment", | ||
| "code-hosting", | ||
| "agent-authoring", | ||
| "code-quality", | ||
| "integrations", | ||
| "writing", | ||
| "design", | ||
| "other", | ||
| ]; | ||
| const FALLBACK_CATEGORY = "other"; | ||
| /** Build a `skill directory name -> {category, file}` map from every manifest. */ | ||
| export function buildCategoryMap(marketplacesDir) { | ||
| const map = new Map(); | ||
| for (const filename of readdirSync(marketplacesDir).filter((f) => f.endsWith(".json")).sort()) { | ||
| const manifest = JSON.parse(readFileSync(join(marketplacesDir, filename), "utf-8")); | ||
| for (const entry of manifest.plugins ?? []) { | ||
| const source = entry.source ?? ""; | ||
| if (!source.startsWith(SKILL_SOURCE_PREFIX)) continue; | ||
| const name = source.slice(SKILL_SOURCE_PREFIX.length); | ||
| const { category } = entry; | ||
| if (!SKILL_CATEGORY_IDS.includes(category)) { | ||
| throw new Error( | ||
| `${filename}: skill "${name}" has category "${category}", expected one of: ${SKILL_CATEGORY_IDS.join(", ")}`, | ||
| ); | ||
| } | ||
| const existing = map.get(name); | ||
| if (existing && existing.category !== category) { | ||
| throw new Error( | ||
| `Conflicting categories for skill "${name}": ${existing.file} says "${existing.category}", ${filename} says "${category}"`, | ||
| ); | ||
| } | ||
| map.set(name, { category, file: filename }); | ||
| } | ||
| } | ||
| return map; | ||
| } | ||
| /** Minimal YAML frontmatter parser for the flat format used by SKILL.md. */ | ||
@@ -55,5 +111,11 @@ export function parseFrontmatter(raw) { | ||
| /** Build the catalog from SKILL.md files in the given directory. */ | ||
| export function buildCatalog(skillsDir) { | ||
| /** | ||
| * Build the catalog from SKILL.md files in the given directory. | ||
| * | ||
| * Pass an isolated `marketplacesDir` when building from fixtures; the default reads this repo's real manifests. | ||
| */ | ||
| export function buildCatalog(skillsDir, marketplacesDir = MARKETPLACES_DIR) { | ||
| const entries = []; | ||
| const categories = buildCategoryMap(marketplacesDir); | ||
| const uncategorized = []; | ||
@@ -76,2 +138,5 @@ for (const dir of readdirSync(skillsDir).sort()) { | ||
| const mapped = categories.get(dir); | ||
| if (!mapped) uncategorized.push(dir); | ||
| entries.push({ | ||
@@ -82,2 +147,3 @@ name: fm.name?.trim() || dir, | ||
| content: body, | ||
| category: mapped?.category ?? FALLBACK_CATEGORY, | ||
| ...(fm.license ? { license: fm.license } : {}), | ||
@@ -88,2 +154,8 @@ ...(fm.compatibility ? { compatibility: fm.compatibility } : {}), | ||
| if (uncategorized.length > 0) { | ||
| console.warn( | ||
| `Warning: no marketplace entry, category defaults to "${FALLBACK_CATEGORY}": ${uncategorized.join(", ")}`, | ||
| ); | ||
| } | ||
| return entries; | ||
@@ -98,3 +170,4 @@ } | ||
| const source = `// Auto-generated by scripts/build-skills-catalog.mjs — do not edit. | ||
| // Source of truth: skills/*/SKILL.md | ||
| // Source of truth: skills/*/SKILL.md and marketplaces/*.json (category) | ||
| export const SKILL_CATEGORY_IDS = ${JSON.stringify(SKILL_CATEGORY_IDS)}; | ||
| export const SKILLS_CATALOG = ${JSON.stringify(entries, null, 2)}; | ||
@@ -101,0 +174,0 @@ export default SKILLS_CATALOG; |
+21
-0
@@ -0,1 +1,20 @@ | ||
| /** | ||
| * Categories for skill entries, consumed by the agent-canvas /skills facet rail. | ||
| * | ||
| * Sourced from the `category` field on marketplace entries whose `source` starts with `./skills/`. | ||
| * Distinct from the `category` on marketplace *plugin* entries, which serves Claude Code marketplace browsing. | ||
| */ | ||
| export type SkillCategoryId = | ||
| | "automations" | ||
| | "environment" | ||
| | "code-hosting" | ||
| | "agent-authoring" | ||
| | "code-quality" | ||
| | "integrations" | ||
| | "writing" | ||
| | "design" | ||
| | "other"; | ||
| export const SKILL_CATEGORY_IDS: readonly SkillCategoryId[]; | ||
| export interface SkillCatalogEntry { | ||
@@ -6,2 +25,4 @@ name: string; | ||
| content: string; | ||
| /** `"other"` when the skill has no marketplace entry. */ | ||
| category: SkillCategoryId; | ||
| license?: string; | ||
@@ -8,0 +29,0 @@ compatibility?: string; |
@@ -30,2 +30,9 @@ """Tests for the skills catalog codegen (scripts/build-skills-catalog.mjs). | ||
| def empty_marketplaces(tmp_path: Path) -> Path: | ||
| """Isolates fixtures from the real manifests, so a fixture named after a real skill can't inherit its category.""" | ||
| path = tmp_path / "marketplaces" | ||
| path.mkdir() | ||
| return path | ||
| # --------------------------------------------------------------------------- | ||
@@ -138,5 +145,7 @@ # parseFrontmatter unit tests (via Node subprocess) | ||
| markets_dir = empty_marketplaces(tmp_path) | ||
| script = textwrap.dedent(f"""\ | ||
| import {{ buildCatalog }} from './scripts/build-skills-catalog.mjs'; | ||
| const entries = buildCatalog({json.dumps(str(skills_dir))}); | ||
| const entries = buildCatalog({json.dumps(str(skills_dir))}, {json.dumps(str(markets_dir))}); | ||
| process.stdout.write(JSON.stringify(entries)); | ||
@@ -178,5 +187,7 @@ """) | ||
| markets_dir = empty_marketplaces(tmp_path) | ||
| script = textwrap.dedent(f"""\ | ||
| import {{ buildCatalog }} from './scripts/build-skills-catalog.mjs'; | ||
| buildCatalog({json.dumps(str(skills_dir))}); | ||
| buildCatalog({json.dumps(str(skills_dir))}, {json.dumps(str(markets_dir))}); | ||
| """) | ||
@@ -204,2 +215,4 @@ result = run_node(script) | ||
| markets_dir = empty_marketplaces(tmp_path) | ||
| output = tmp_path / "output.js" | ||
@@ -209,3 +222,3 @@ script = textwrap.dedent(f"""\ | ||
| import {{ buildCatalog }} from './scripts/build-skills-catalog.mjs'; | ||
| const entries = buildCatalog({json.dumps(str(skills_dir))}); | ||
| const entries = buildCatalog({json.dumps(str(skills_dir))}, {json.dumps(str(markets_dir))}); | ||
| const src = "export const SKILLS_CATALOG = " + JSON.stringify(entries) + ";\\nexport default SKILLS_CATALOG;\\n"; | ||
@@ -254,5 +267,7 @@ writeFileSync({json.dumps(str(output))}, src); | ||
| markets_dir = empty_marketplaces(tmp_path) | ||
| script = textwrap.dedent(f"""\ | ||
| import {{ buildCatalog }} from './scripts/build-skills-catalog.mjs'; | ||
| const entries = buildCatalog({json.dumps(str(skills_dir))}); | ||
| const entries = buildCatalog({json.dumps(str(skills_dir))}, {json.dumps(str(markets_dir))}); | ||
| process.stdout.write(JSON.stringify(entries)); | ||
@@ -343,1 +358,196 @@ """) | ||
| assert before == after, "skills/index.js is out of date — run: node scripts/build-skills-catalog.mjs" | ||
| # --------------------------------------------------------------------------- | ||
| # Marketplace skill categories (consumed by the agent-canvas /skills rail) | ||
| # --------------------------------------------------------------------------- | ||
| MARKETPLACES_DIR = ROOT / "marketplaces" | ||
| SKILL_CATEGORY_IDS = { | ||
| "automations", | ||
| "environment", | ||
| "code-hosting", | ||
| "agent-authoring", | ||
| "code-quality", | ||
| "integrations", | ||
| "writing", | ||
| "design", | ||
| "other", | ||
| } | ||
| # Skills with no marketplace entry, so they fall back to "other". | ||
| # Adding entries would mean creating .plugin/plugin.json and vendor symlinks (see test_skill_plugin_loading.py), which publishes them as Codex/Claude Code plugins. | ||
| SKILLS_WITHOUT_MARKETPLACE_ENTRY = {"qa-changes", "release-notes"} | ||
| EXPECTED_CATEGORY_COUNTS = { | ||
| "environment": 10, | ||
| "automations": 9, | ||
| "code-hosting": 8, | ||
| "agent-authoring": 8, | ||
| "code-quality": 6, | ||
| "integrations": 6, | ||
| "writing": 4, | ||
| "design": 2, | ||
| "other": 1, | ||
| } | ||
| def _marketplace_skill_categories() -> dict[str, str]: | ||
| """Map skill directory name -> category, across every marketplace manifest.""" | ||
| result: dict[str, str] = {} | ||
| for path in sorted(MARKETPLACES_DIR.glob("*.json")): | ||
| manifest = json.loads(path.read_text()) | ||
| for entry in manifest.get("plugins", []): | ||
| source = entry.get("source", "") | ||
| if not source.startswith("./skills/"): | ||
| continue | ||
| result[source.split("/")[-1]] = entry.get("category") | ||
| return result | ||
| class TestMarketplaceSkillCategories: | ||
| def test_every_skill_entry_uses_a_known_category(self): | ||
| bad = { | ||
| name: category | ||
| for name, category in _marketplace_skill_categories().items() | ||
| if category not in SKILL_CATEGORY_IDS | ||
| } | ||
| assert bad == {}, f"Unknown categories: {bad}" | ||
| def test_uncovered_skills_are_exactly_the_known_exceptions(self): | ||
| dirs = { | ||
| d.name | ||
| for d in (ROOT / "skills").iterdir() | ||
| if d.is_dir() and not d.name.startswith(".") | ||
| } | ||
| uncovered = dirs - set(_marketplace_skill_categories()) | ||
| assert uncovered == SKILLS_WITHOUT_MARKETPLACE_ENTRY | ||
| def test_category_distribution_is_balanced(self): | ||
| from collections import Counter | ||
| counts = dict(Counter(_marketplace_skill_categories().values())) | ||
| assert counts == EXPECTED_CATEGORY_COUNTS | ||
| def test_plugin_entries_keep_their_own_taxonomy(self): | ||
| """Plugin entries are for Claude Code browsing and must not be rewritten.""" | ||
| categories = set() | ||
| for path in sorted(MARKETPLACES_DIR.glob("*.json")): | ||
| manifest = json.loads(path.read_text()) | ||
| for entry in manifest.get("plugins", []): | ||
| if entry.get("source", "").startswith("./skills/"): | ||
| continue | ||
| categories.add(entry.get("category")) | ||
| assert categories - SKILL_CATEGORY_IDS, ( | ||
| "Plugin entries appear to have been rewritten to the skill taxonomy" | ||
| ) | ||
| class TestCategoryJoin: | ||
| def _build(self, tmp_path, skills: dict[str, str], manifests: dict[str, dict], check: bool = True): | ||
| skills_dir = tmp_path / "skills" | ||
| skills_dir.mkdir() | ||
| for name, content in skills.items(): | ||
| (skills_dir / name).mkdir() | ||
| (skills_dir / name / "SKILL.md").write_text(content) | ||
| markets_dir = tmp_path / "marketplaces" | ||
| markets_dir.mkdir() | ||
| for filename, manifest in manifests.items(): | ||
| (markets_dir / filename).write_text(json.dumps(manifest)) | ||
| script = textwrap.dedent(f"""\ | ||
| import {{ buildCatalog }} from './scripts/build-skills-catalog.mjs'; | ||
| const entries = buildCatalog({json.dumps(str(skills_dir))}, {json.dumps(str(markets_dir))}); | ||
| process.stdout.write(JSON.stringify(entries)); | ||
| """) | ||
| return run_node(script, check=check) | ||
| def test_category_is_joined_from_the_manifest(self, tmp_path): | ||
| result = self._build( | ||
| tmp_path, | ||
| {"docker": "---\nname: docker\ndescription: d\n---\nBody"}, | ||
| {"m.json": {"plugins": [{"name": "docker", "source": "./skills/docker", "category": "environment"}]}}, | ||
| ) | ||
| entries = json.loads(result.stdout) | ||
| assert entries[0]["category"] == "environment" | ||
| def test_plugin_entries_are_ignored_when_building_the_map(self, tmp_path): | ||
| result = self._build( | ||
| tmp_path, | ||
| {"docker": "---\nname: docker\ndescription: d\n---\nBody"}, | ||
| {"m.json": {"plugins": [ | ||
| {"name": "some-plugin", "source": "./plugins/some-plugin", "category": "utilities"}, | ||
| {"name": "docker", "source": "./skills/docker", "category": "environment"}, | ||
| ]}}, | ||
| ) | ||
| entries = json.loads(result.stdout) | ||
| assert entries[0]["category"] == "environment" | ||
| def test_unknown_category_throws_naming_the_skill(self, tmp_path): | ||
| result = self._build( | ||
| tmp_path, | ||
| {"docker": "---\nname: docker\ndescription: d\n---\nBody"}, | ||
| {"m.json": {"plugins": [{"name": "docker", "source": "./skills/docker", "category": "code-hostig"}]}}, | ||
| check=False, | ||
| ) | ||
| assert result.returncode != 0 | ||
| assert "docker" in result.stderr | ||
| assert "code-hostig" in result.stderr | ||
| assert "environment" in result.stderr # the legal set is printed | ||
| def test_conflicting_categories_across_manifests_throws(self, tmp_path): | ||
| result = self._build( | ||
| tmp_path, | ||
| {"docker": "---\nname: docker\ndescription: d\n---\nBody"}, | ||
| { | ||
| "a.json": {"plugins": [{"name": "docker", "source": "./skills/docker", "category": "environment"}]}, | ||
| "b.json": {"plugins": [{"name": "docker", "source": "./skills/docker", "category": "design"}]}, | ||
| }, | ||
| check=False, | ||
| ) | ||
| assert result.returncode != 0 | ||
| assert "docker" in result.stderr | ||
| assert "Conflicting categories" in result.stderr | ||
| assert "environment" in result.stderr # the first manifest's value | ||
| assert "design" in result.stderr # the second manifest's conflicting value | ||
| def test_skill_without_an_entry_gets_other_and_warns(self, tmp_path): | ||
| result = self._build( | ||
| tmp_path, | ||
| {"lonely": "---\nname: lonely\ndescription: d\n---\nBody"}, | ||
| {"m.json": {"plugins": []}}, | ||
| ) | ||
| entries = json.loads(result.stdout) | ||
| assert entries[0]["category"] == "other" | ||
| assert "lonely" in result.stderr | ||
| class TestGeneratedCategories: | ||
| def test_every_entry_has_a_known_category(self): | ||
| script = textwrap.dedent("""\ | ||
| import { SKILLS_CATALOG, SKILL_CATEGORY_IDS } from './skills/index.js'; | ||
| const legal = new Set(SKILL_CATEGORY_IDS); | ||
| for (const entry of SKILLS_CATALOG) { | ||
| if (!legal.has(entry.category)) { | ||
| console.error('Bad category for ' + entry.name + ': ' + entry.category); | ||
| process.exit(1); | ||
| } | ||
| } | ||
| """) | ||
| run_node(script) | ||
| def test_uncovered_skills_land_in_other(self): | ||
| # flarglebargle is the one skill whose marketplace entry sets "other" on purpose: it is a trigger-testing skill, not a real category member. | ||
| script = textwrap.dedent(f"""\ | ||
| import {{ SKILLS_CATALOG }} from './skills/index.js'; | ||
| const expected = {json.dumps(sorted(SKILLS_WITHOUT_MARKETPLACE_ENTRY))}; | ||
| const actual = SKILLS_CATALOG.filter(e => e.category === 'other').map(e => e.name).sort(); | ||
| const extra = actual.filter(n => !expected.includes(n) && n !== 'flarglebargle'); | ||
| if (extra.length) {{ | ||
| console.error('Unexpected uncategorized skills: ' + extra.join(', ')); | ||
| process.exit(1); | ||
| }} | ||
| """) | ||
| run_node(script) |
Sorry, the diff of this file is too big to display
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 7 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 5 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
2535977
0.55%27702
1.15%211
1.44%69
2.99%