feat(ce-update): add plugin version check skill and ce_platforms filtering (#532)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -136,7 +136,7 @@ Why: shell-heavy exploration causes avoidable permission prompts in sub-agent wo
|
|||||||
|
|
||||||
- [ ] Never instruct agents to use `find`, `ls`, `cat`, `head`, `tail`, `grep`, `rg`, `wc`, or `tree` through a shell for routine file discovery, content search, or file reading
|
- [ ] Never instruct agents to use `find`, `ls`, `cat`, `head`, `tail`, `grep`, `rg`, `wc`, or `tree` through a shell for routine file discovery, content search, or file reading
|
||||||
- [ ] Describe tools by capability class with platform hints — e.g., "Use the native file-search/glob tool (e.g., Glob in Claude Code)" — not by Claude Code-specific tool names alone
|
- [ ] Describe tools by capability class with platform hints — e.g., "Use the native file-search/glob tool (e.g., Glob in Claude Code)" — not by Claude Code-specific tool names alone
|
||||||
- [ ] When shell is the only option (e.g., `ast-grep`, `bundle show`, git commands), instruct one simple command at a time — no chaining (`&&`, `||`, `;`) and no error suppression (`2>/dev/null`, `|| true`). Simple pipes (e.g., `| jq .field`) and output redirection (e.g., `> file`) are acceptable when they don't obscure failures
|
- [ ] When shell is the only option (e.g., `ast-grep`, `bundle show`, git commands), instruct one simple command at a time — no chaining (`&&`, `||`, `;`) and no error suppression (`2>/dev/null`, `|| true`). Simple pipes (e.g., `| jq .field`) and output redirection (e.g., `> file`) are acceptable when they don't obscure failures. **Exception:** `!` backtick pre-resolution commands may use error suppression and `||` fallbacks to produce clean sentinel values (e.g., `|| echo '__FAILED__'`), since these run at load time and the model needs a parseable signal, not raw stderr
|
||||||
- [ ] Do not encode shell recipes for routine exploration when native tools can do the job; encode intent and preferred tool classes instead
|
- [ ] Do not encode shell recipes for routine exploration when native tools can do the job; encode intent and preferred tool classes instead
|
||||||
- [ ] For shell-only workflows (e.g., `gh`, `git`, `bundle show`, project CLIs), explicit command examples are acceptable when they are simple, task-scoped, and not chained together
|
- [ ] For shell-only workflows (e.g., `gh`, `git`, `bundle show`, project CLIs), explicit command examples are acceptable when they are simple, task-scoped, and not chained together
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ The primary entry points for engineering work, invoked as slash commands:
|
|||||||
| `/test-browser` | Run browser tests on PR-affected pages |
|
| `/test-browser` | Run browser tests on PR-affected pages |
|
||||||
| `/test-xcode` | Build and test iOS apps on simulator using XcodeBuildMCP |
|
| `/test-xcode` | Build and test iOS apps on simulator using XcodeBuildMCP |
|
||||||
| `/onboarding` | Generate `ONBOARDING.md` to help new contributors understand the codebase |
|
| `/onboarding` | Generate `ONBOARDING.md` to help new contributors understand the codebase |
|
||||||
|
| `/ce-update` | Check compound-engineering plugin version and fix stale cache (Claude Code only) |
|
||||||
| `/todo-resolve` | Resolve todos in parallel |
|
| `/todo-resolve` | Resolve todos in parallel |
|
||||||
| `/todo-triage` | Triage and prioritize pending todos |
|
| `/todo-triage` | Triage and prioritize pending todos |
|
||||||
|
|
||||||
|
|||||||
69
plugins/compound-engineering/skills/ce-update/SKILL.md
Normal file
69
plugins/compound-engineering/skills/ce-update/SKILL.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
name: ce-update
|
||||||
|
description: |
|
||||||
|
Check if the compound-engineering plugin is up to date and fix stale cache if not.
|
||||||
|
Use when the user says "update compound engineering", "check compound engineering version",
|
||||||
|
"ce update", "is compound engineering up to date", "update ce plugin", or reports issues
|
||||||
|
that might stem from a stale compound-engineering plugin version. This skill only works
|
||||||
|
in Claude Code — it relies on the plugin harness cache layout.
|
||||||
|
disable-model-invocation: true
|
||||||
|
ce_platforms: [claude]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Check & Fix Plugin Version
|
||||||
|
|
||||||
|
Verify the installed compound-engineering plugin version matches the latest released
|
||||||
|
version, and fix stale marketplace/cache state if it doesn't. Claude Code only.
|
||||||
|
|
||||||
|
## Pre-resolved context
|
||||||
|
|
||||||
|
The three sections below contain pre-resolved data. Only the **Plugin root
|
||||||
|
path** determines whether this session is Claude Code — if it contains an error
|
||||||
|
sentinel, an empty value, or a literal `${CLAUDE_PLUGIN_ROOT}` string, tell the
|
||||||
|
user this skill only works in Claude Code and stop. The other two sections may
|
||||||
|
contain error sentinels even in valid Claude Code sessions; the decision logic
|
||||||
|
below handles those cases.
|
||||||
|
|
||||||
|
**Plugin root path:**
|
||||||
|
!`echo "${CLAUDE_PLUGIN_ROOT}" 2>/dev/null || echo '__CE_UPDATE_ROOT_FAILED__'`
|
||||||
|
|
||||||
|
**Latest released version:**
|
||||||
|
!`gh release list --repo Everyinc/compound-engineering-plugin --limit 30 --json tagName --jq '[.[] | select(.tagName | startswith("compound-engineering-v"))][0].tagName | sub("compound-engineering-v";"")' 2>/dev/null || echo '__CE_UPDATE_VERSION_FAILED__'`
|
||||||
|
|
||||||
|
**Cached version folder(s):**
|
||||||
|
!`ls "${CLAUDE_PLUGIN_ROOT}/cache/every-marketplace/compound-engineering/" 2>/dev/null || echo '__CE_UPDATE_CACHE_FAILED__'`
|
||||||
|
|
||||||
|
## Decision logic
|
||||||
|
|
||||||
|
### 1. Platform gate
|
||||||
|
|
||||||
|
If **Plugin root path** contains `__CE_UPDATE_ROOT_FAILED__`, a literal
|
||||||
|
`${CLAUDE_PLUGIN_ROOT}` string, or is empty: tell the user this skill requires Claude Code
|
||||||
|
and stop. No further action.
|
||||||
|
|
||||||
|
### 2. Compare versions
|
||||||
|
|
||||||
|
If **Latest released version** contains `__CE_UPDATE_VERSION_FAILED__`: tell the user the
|
||||||
|
latest release could not be fetched (gh may be unavailable or rate-limited) and stop.
|
||||||
|
|
||||||
|
If **Cached version folder(s)** contains `__CE_UPDATE_CACHE_FAILED__`: no marketplace cache
|
||||||
|
exists. Tell the user: "No marketplace cache found — this appears to be a local dev checkout
|
||||||
|
or fresh install." and stop.
|
||||||
|
|
||||||
|
Take the **latest released version** and the **cached folder list**.
|
||||||
|
|
||||||
|
**Up to date** — exactly one cached folder exists AND its name matches the latest version:
|
||||||
|
- Tell the user: "compound-engineering **v{version}** is installed and up to date."
|
||||||
|
|
||||||
|
**Out of date or corrupted** — multiple cached folders exist, OR the single folder name
|
||||||
|
does not match the latest version. Use the **Plugin root path** value from above to
|
||||||
|
construct the delete path.
|
||||||
|
|
||||||
|
**Clear the stale cache:**
|
||||||
|
```bash
|
||||||
|
rm -rf "<plugin-root-path>/cache/every-marketplace/compound-engineering"
|
||||||
|
```
|
||||||
|
|
||||||
|
Tell the user:
|
||||||
|
- "compound-engineering was on **v{old}** but **v{latest}** is available."
|
||||||
|
- "Cleared the plugin cache. Now run `/plugin marketplace update` in this session, then restart Claude Code to pick up v{latest}."
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { formatFrontmatter } from "../utils/frontmatter"
|
import { formatFrontmatter } from "../utils/frontmatter"
|
||||||
import type { ClaudeAgent, ClaudeCommand, ClaudePlugin, ClaudeSkill } from "../types/claude"
|
import { type ClaudeAgent, type ClaudeCommand, type ClaudePlugin, type ClaudeSkill, filterSkillsByPlatform } from "../types/claude"
|
||||||
import type { CodexBundle, CodexGeneratedSkill } from "../types/codex"
|
import type { CodexBundle, CodexGeneratedSkill } from "../types/codex"
|
||||||
import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
|
import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
|
||||||
import {
|
import {
|
||||||
@@ -16,17 +16,18 @@ export function convertClaudeToCodex(
|
|||||||
plugin: ClaudePlugin,
|
plugin: ClaudePlugin,
|
||||||
_options: ClaudeToCodexOptions,
|
_options: ClaudeToCodexOptions,
|
||||||
): CodexBundle {
|
): CodexBundle {
|
||||||
|
const platformSkills = filterSkillsByPlatform(plugin.skills, "codex")
|
||||||
const invocableCommands = plugin.commands.filter((command) => !command.disableModelInvocation)
|
const invocableCommands = plugin.commands.filter((command) => !command.disableModelInvocation)
|
||||||
const applyCompoundWorkflowModel = shouldApplyCompoundWorkflowModel(plugin)
|
const applyCompoundWorkflowModel = shouldApplyCompoundWorkflowModel(plugin)
|
||||||
const canonicalWorkflowSkills = applyCompoundWorkflowModel
|
const canonicalWorkflowSkills = applyCompoundWorkflowModel
|
||||||
? plugin.skills.filter((skill) => isCanonicalCodexWorkflowSkill(skill.name))
|
? platformSkills.filter((skill) => isCanonicalCodexWorkflowSkill(skill.name))
|
||||||
: []
|
: []
|
||||||
const deprecatedWorkflowAliases = applyCompoundWorkflowModel
|
const deprecatedWorkflowAliases = applyCompoundWorkflowModel
|
||||||
? plugin.skills.filter((skill) => isDeprecatedCodexWorkflowAlias(skill.name))
|
? platformSkills.filter((skill) => isDeprecatedCodexWorkflowAlias(skill.name))
|
||||||
: []
|
: []
|
||||||
const copiedSkills = applyCompoundWorkflowModel
|
const copiedSkills = applyCompoundWorkflowModel
|
||||||
? plugin.skills.filter((skill) => !isDeprecatedCodexWorkflowAlias(skill.name))
|
? platformSkills.filter((skill) => !isDeprecatedCodexWorkflowAlias(skill.name))
|
||||||
: plugin.skills
|
: platformSkills
|
||||||
const skillDirs = copiedSkills.map((skill) => ({
|
const skillDirs = copiedSkills.map((skill) => ({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
sourceDir: skill.sourceDir,
|
sourceDir: skill.sourceDir,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { formatFrontmatter } from "../utils/frontmatter"
|
import { formatFrontmatter } from "../utils/frontmatter"
|
||||||
import { sanitizePathName } from "../utils/files"
|
import { sanitizePathName } from "../utils/files"
|
||||||
import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude"
|
import { type ClaudeAgent, type ClaudeCommand, type ClaudeMcpServer, type ClaudePlugin, filterSkillsByPlatform } from "../types/claude"
|
||||||
import type {
|
import type {
|
||||||
CopilotAgent,
|
CopilotAgent,
|
||||||
CopilotBundle,
|
CopilotBundle,
|
||||||
@@ -23,7 +23,7 @@ export function convertClaudeToCopilot(
|
|||||||
const agents = plugin.agents.map((agent) => convertAgent(agent, usedAgentNames))
|
const agents = plugin.agents.map((agent) => convertAgent(agent, usedAgentNames))
|
||||||
|
|
||||||
// Reserve sanitized skill names so generated skills (from commands) don't collide on disk
|
// Reserve sanitized skill names so generated skills (from commands) don't collide on disk
|
||||||
const skillDirs = plugin.skills.map((skill) => {
|
const skillDirs = filterSkillsByPlatform(plugin.skills, "copilot").map((skill) => {
|
||||||
usedSkillNames.add(sanitizePathName(skill.name))
|
usedSkillNames.add(sanitizePathName(skill.name))
|
||||||
return {
|
return {
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { formatFrontmatter } from "../utils/frontmatter"
|
import { formatFrontmatter } from "../utils/frontmatter"
|
||||||
import type { ClaudeAgent, ClaudeCommand, ClaudePlugin } from "../types/claude"
|
import { type ClaudeAgent, type ClaudeCommand, type ClaudePlugin, filterSkillsByPlatform } from "../types/claude"
|
||||||
import type { DroidBundle, DroidCommandFile, DroidAgentFile } from "../types/droid"
|
import type { DroidBundle, DroidCommandFile, DroidAgentFile } from "../types/droid"
|
||||||
import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
|
import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ export function convertClaudeToDroid(
|
|||||||
): DroidBundle {
|
): DroidBundle {
|
||||||
const commands = plugin.commands.map((command) => convertCommand(command))
|
const commands = plugin.commands.map((command) => convertCommand(command))
|
||||||
const droids = plugin.agents.map((agent) => convertAgent(agent))
|
const droids = plugin.agents.map((agent) => convertAgent(agent))
|
||||||
const skillDirs = plugin.skills.map((skill) => ({
|
const skillDirs = filterSkillsByPlatform(plugin.skills, "droid").map((skill) => ({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
sourceDir: skill.sourceDir,
|
sourceDir: skill.sourceDir,
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { formatFrontmatter } from "../utils/frontmatter"
|
import { formatFrontmatter } from "../utils/frontmatter"
|
||||||
import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude"
|
import { type ClaudeAgent, type ClaudeCommand, type ClaudeMcpServer, type ClaudePlugin, filterSkillsByPlatform } from "../types/claude"
|
||||||
import type { GeminiBundle, GeminiCommand, GeminiMcpServer, GeminiSkill } from "../types/gemini"
|
import type { GeminiBundle, GeminiCommand, GeminiMcpServer, GeminiSkill } from "../types/gemini"
|
||||||
import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
|
import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
|
||||||
|
|
||||||
@@ -14,7 +14,8 @@ export function convertClaudeToGemini(
|
|||||||
const usedSkillNames = new Set<string>()
|
const usedSkillNames = new Set<string>()
|
||||||
const usedCommandNames = new Set<string>()
|
const usedCommandNames = new Set<string>()
|
||||||
|
|
||||||
const skillDirs = plugin.skills.map((skill) => ({
|
const platformSkills = filterSkillsByPlatform(plugin.skills, "gemini")
|
||||||
|
const skillDirs = platformSkills.map((skill) => ({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
sourceDir: skill.sourceDir,
|
sourceDir: skill.sourceDir,
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { readFileSync, existsSync } from "fs"
|
import { readFileSync, existsSync } from "fs"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { formatFrontmatter } from "../utils/frontmatter"
|
import { formatFrontmatter } from "../utils/frontmatter"
|
||||||
import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude"
|
import { type ClaudeAgent, type ClaudeCommand, type ClaudeMcpServer, type ClaudePlugin, filterSkillsByPlatform } from "../types/claude"
|
||||||
import type {
|
import type {
|
||||||
KiroAgent,
|
KiroAgent,
|
||||||
KiroAgentConfig,
|
KiroAgentConfig,
|
||||||
@@ -36,7 +36,7 @@ export function convertClaudeToKiro(
|
|||||||
const usedSkillNames = new Set<string>()
|
const usedSkillNames = new Set<string>()
|
||||||
|
|
||||||
// Pass-through skills are processed first — they're the source of truth
|
// Pass-through skills are processed first — they're the source of truth
|
||||||
const skillDirs = plugin.skills.map((skill) => ({
|
const skillDirs = filterSkillsByPlatform(plugin.skills, "kiro").map((skill) => ({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
sourceDir: skill.sourceDir,
|
sourceDir: skill.sourceDir,
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { formatFrontmatter } from "../utils/frontmatter"
|
import { formatFrontmatter } from "../utils/frontmatter"
|
||||||
import { normalizeModelWithProvider } from "../utils/model"
|
import { normalizeModelWithProvider } from "../utils/model"
|
||||||
import { sanitizePathName } from "../utils/files"
|
import { sanitizePathName } from "../utils/files"
|
||||||
import type {
|
import {
|
||||||
ClaudeAgent,
|
type ClaudeAgent,
|
||||||
ClaudeCommand,
|
type ClaudeCommand,
|
||||||
ClaudePlugin,
|
type ClaudePlugin,
|
||||||
ClaudeMcpServer,
|
type ClaudeMcpServer,
|
||||||
|
filterSkillsByPlatform,
|
||||||
} from "../types/claude"
|
} from "../types/claude"
|
||||||
import type {
|
import type {
|
||||||
OpenClawBundle,
|
OpenClawBundle,
|
||||||
@@ -29,7 +30,8 @@ export function convertClaudeToOpenClaw(
|
|||||||
|
|
||||||
const skills: OpenClawSkillFile[] = [...agentSkills, ...commandSkills]
|
const skills: OpenClawSkillFile[] = [...agentSkills, ...commandSkills]
|
||||||
|
|
||||||
const skillDirCopies = plugin.skills.map((skill) => ({
|
const platformSkills = filterSkillsByPlatform(plugin.skills, "openclaw")
|
||||||
|
const skillDirCopies = platformSkills.map((skill) => ({
|
||||||
sourceDir: skill.sourceDir,
|
sourceDir: skill.sourceDir,
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
}))
|
}))
|
||||||
@@ -37,7 +39,7 @@ export function convertClaudeToOpenClaw(
|
|||||||
const allSkillDirs = [
|
const allSkillDirs = [
|
||||||
...agentSkills.map((s) => sanitizePathName(s.dir)),
|
...agentSkills.map((s) => sanitizePathName(s.dir)),
|
||||||
...commandSkills.map((s) => sanitizePathName(s.dir)),
|
...commandSkills.map((s) => sanitizePathName(s.dir)),
|
||||||
...plugin.skills.map((s) => sanitizePathName(s.name)),
|
...platformSkills.map((s) => sanitizePathName(s.name)),
|
||||||
]
|
]
|
||||||
|
|
||||||
const manifest = buildManifest(plugin, allSkillDirs)
|
const manifest = buildManifest(plugin, allSkillDirs)
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { formatFrontmatter } from "../utils/frontmatter"
|
import { formatFrontmatter } from "../utils/frontmatter"
|
||||||
import { normalizeModelWithProvider } from "../utils/model"
|
import { normalizeModelWithProvider } from "../utils/model"
|
||||||
import type {
|
import {
|
||||||
ClaudeAgent,
|
type ClaudeAgent,
|
||||||
ClaudeCommand,
|
type ClaudeCommand,
|
||||||
ClaudeHooks,
|
type ClaudeHooks,
|
||||||
ClaudePlugin,
|
type ClaudePlugin,
|
||||||
ClaudeMcpServer,
|
type ClaudeMcpServer,
|
||||||
|
filterSkillsByPlatform,
|
||||||
} from "../types/claude"
|
} from "../types/claude"
|
||||||
import type {
|
import type {
|
||||||
OpenCodeBundle,
|
OpenCodeBundle,
|
||||||
@@ -83,7 +84,7 @@ export function convertClaudeToOpenCode(
|
|||||||
agents: agentFiles,
|
agents: agentFiles,
|
||||||
commandFiles: cmdFiles,
|
commandFiles: cmdFiles,
|
||||||
plugins,
|
plugins,
|
||||||
skillDirs: plugin.skills.map((skill) => ({ sourceDir: skill.sourceDir, name: skill.name })),
|
skillDirs: filterSkillsByPlatform(plugin.skills, "opencode").map((skill) => ({ sourceDir: skill.sourceDir, name: skill.name })),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { formatFrontmatter } from "../utils/frontmatter"
|
import { formatFrontmatter } from "../utils/frontmatter"
|
||||||
import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude"
|
import { type ClaudeAgent, type ClaudeCommand, type ClaudeMcpServer, type ClaudePlugin, filterSkillsByPlatform } from "../types/claude"
|
||||||
import type {
|
import type {
|
||||||
PiBundle,
|
PiBundle,
|
||||||
PiGeneratedSkill,
|
PiGeneratedSkill,
|
||||||
@@ -17,8 +17,9 @@ export function convertClaudeToPi(
|
|||||||
plugin: ClaudePlugin,
|
plugin: ClaudePlugin,
|
||||||
_options: ClaudeToPiOptions,
|
_options: ClaudeToPiOptions,
|
||||||
): PiBundle {
|
): PiBundle {
|
||||||
|
const platformSkills = filterSkillsByPlatform(plugin.skills, "pi")
|
||||||
const promptNames = new Set<string>()
|
const promptNames = new Set<string>()
|
||||||
const usedSkillNames = new Set<string>(plugin.skills.map((skill) => normalizeName(skill.name)))
|
const usedSkillNames = new Set<string>(platformSkills.map((skill) => normalizeName(skill.name)))
|
||||||
|
|
||||||
const prompts = plugin.commands
|
const prompts = plugin.commands
|
||||||
.filter((command) => !command.disableModelInvocation)
|
.filter((command) => !command.disableModelInvocation)
|
||||||
@@ -35,7 +36,7 @@ export function convertClaudeToPi(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
prompts,
|
prompts,
|
||||||
skillDirs: plugin.skills.map((skill) => ({
|
skillDirs: platformSkills.map((skill) => ({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
sourceDir: skill.sourceDir,
|
sourceDir: skill.sourceDir,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { formatFrontmatter } from "../utils/frontmatter"
|
import { formatFrontmatter } from "../utils/frontmatter"
|
||||||
import { normalizeModelWithProvider } from "../utils/model"
|
import { normalizeModelWithProvider } from "../utils/model"
|
||||||
import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude"
|
import { type ClaudeAgent, type ClaudeCommand, type ClaudeMcpServer, type ClaudePlugin, filterSkillsByPlatform } from "../types/claude"
|
||||||
import type {
|
import type {
|
||||||
QwenAgentFile,
|
QwenAgentFile,
|
||||||
QwenBundle,
|
QwenBundle,
|
||||||
@@ -16,6 +16,7 @@ export type ClaudeToQwenOptions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function convertClaudeToQwen(plugin: ClaudePlugin, options: ClaudeToQwenOptions): QwenBundle {
|
export function convertClaudeToQwen(plugin: ClaudePlugin, options: ClaudeToQwenOptions): QwenBundle {
|
||||||
|
const platformSkills = filterSkillsByPlatform(plugin.skills, "qwen")
|
||||||
const agentFiles = plugin.agents.map((agent) => convertAgent(agent, options))
|
const agentFiles = plugin.agents.map((agent) => convertAgent(agent, options))
|
||||||
const cmdFiles = convertCommands(plugin.commands)
|
const cmdFiles = convertCommands(plugin.commands)
|
||||||
const mcp = plugin.mcpServers ? convertMcp(plugin.mcpServers) : undefined
|
const mcp = plugin.mcpServers ? convertMcp(plugin.mcpServers) : undefined
|
||||||
@@ -43,7 +44,7 @@ export function convertClaudeToQwen(plugin: ClaudePlugin, options: ClaudeToQwenO
|
|||||||
config,
|
config,
|
||||||
agents: agentFiles,
|
agents: agentFiles,
|
||||||
commandFiles: cmdFiles,
|
commandFiles: cmdFiles,
|
||||||
skillDirs: plugin.skills.map((skill) => ({ sourceDir: skill.sourceDir, name: skill.name })),
|
skillDirs: platformSkills.map((skill) => ({ sourceDir: skill.sourceDir, name: skill.name })),
|
||||||
contextFile,
|
contextFile,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,10 +182,11 @@ function generateContextFile(plugin: ClaudePlugin): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Skills section
|
// Skills section
|
||||||
if (plugin.skills.length > 0) {
|
const qwenSkills = filterSkillsByPlatform(plugin.skills, "qwen")
|
||||||
|
if (qwenSkills.length > 0) {
|
||||||
sections.push("## Skills")
|
sections.push("## Skills")
|
||||||
sections.push("")
|
sections.push("")
|
||||||
for (const skill of plugin.skills) {
|
for (const skill of qwenSkills) {
|
||||||
sections.push(`- ${skill.name}`)
|
sections.push(`- ${skill.name}`)
|
||||||
}
|
}
|
||||||
sections.push("")
|
sections.push("")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { formatFrontmatter } from "../utils/frontmatter"
|
import { formatFrontmatter } from "../utils/frontmatter"
|
||||||
import { sanitizePathName } from "../utils/files"
|
import { sanitizePathName } from "../utils/files"
|
||||||
import { findServersWithPotentialSecrets } from "../utils/secrets"
|
import { findServersWithPotentialSecrets } from "../utils/secrets"
|
||||||
import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude"
|
import { type ClaudeAgent, type ClaudeCommand, type ClaudeMcpServer, type ClaudePlugin, filterSkillsByPlatform } from "../types/claude"
|
||||||
import type { WindsurfBundle, WindsurfGeneratedSkill, WindsurfMcpConfig, WindsurfMcpServerEntry, WindsurfWorkflow } from "../types/windsurf"
|
import type { WindsurfBundle, WindsurfGeneratedSkill, WindsurfMcpConfig, WindsurfMcpServerEntry, WindsurfWorkflow } from "../types/windsurf"
|
||||||
import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
|
import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export function convertClaudeToWindsurf(
|
|||||||
const knownAgentNames = plugin.agents.map((a) => normalizeName(a.name))
|
const knownAgentNames = plugin.agents.map((a) => normalizeName(a.name))
|
||||||
|
|
||||||
// Pass-through skills (collected first so agent skill names can deduplicate against them)
|
// Pass-through skills (collected first so agent skill names can deduplicate against them)
|
||||||
const skillDirs = plugin.skills.map((skill) => ({
|
const skillDirs = filterSkillsByPlatform(plugin.skills, "windsurf").map((skill) => ({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
sourceDir: skill.sourceDir,
|
sourceDir: skill.sourceDir,
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -107,11 +107,13 @@ async function loadSkills(skillsDirs: string[]): Promise<ClaudeSkill[]> {
|
|||||||
const { data } = parseFrontmatter(raw, file)
|
const { data } = parseFrontmatter(raw, file)
|
||||||
const name = (data.name as string) ?? path.basename(path.dirname(file))
|
const name = (data.name as string) ?? path.basename(path.dirname(file))
|
||||||
const disableModelInvocation = data["disable-model-invocation"] === true ? true : undefined
|
const disableModelInvocation = data["disable-model-invocation"] === true ? true : undefined
|
||||||
|
const ce_platforms = Array.isArray(data.ce_platforms) ? (data.ce_platforms as string[]) : undefined
|
||||||
skills.push({
|
skills.push({
|
||||||
name,
|
name,
|
||||||
description: data.description as string | undefined,
|
description: data.description as string | undefined,
|
||||||
argumentHint: data["argument-hint"] as string | undefined,
|
argumentHint: data["argument-hint"] as string | undefined,
|
||||||
disableModelInvocation,
|
disableModelInvocation,
|
||||||
|
ce_platforms,
|
||||||
sourceDir: path.dirname(file),
|
sourceDir: path.dirname(file),
|
||||||
skillPath: file,
|
skillPath: file,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -49,10 +49,19 @@ export type ClaudeSkill = {
|
|||||||
description?: string
|
description?: string
|
||||||
argumentHint?: string
|
argumentHint?: string
|
||||||
disableModelInvocation?: boolean
|
disableModelInvocation?: boolean
|
||||||
|
ce_platforms?: string[]
|
||||||
sourceDir: string
|
sourceDir: string
|
||||||
skillPath: string
|
skillPath: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter skills to those available on a given platform.
|
||||||
|
* Skills without a `platforms` field are available everywhere.
|
||||||
|
*/
|
||||||
|
export function filterSkillsByPlatform(skills: ClaudeSkill[], platform: string): ClaudeSkill[] {
|
||||||
|
return skills.filter((skill) => !skill.ce_platforms || skill.ce_platforms.includes(platform))
|
||||||
|
}
|
||||||
|
|
||||||
export type ClaudePlugin = {
|
export type ClaudePlugin = {
|
||||||
root: string
|
root: string
|
||||||
manifest: ClaudeManifest
|
manifest: ClaudeManifest
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { loadClaudePlugin } from "../src/parsers/claude"
|
import { loadClaudePlugin } from "../src/parsers/claude"
|
||||||
|
import { filterSkillsByPlatform } from "../src/types/claude"
|
||||||
|
|
||||||
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
|
const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
|
||||||
const mcpFixtureRoot = path.join(import.meta.dir, "fixtures", "mcp-file")
|
const mcpFixtureRoot = path.join(import.meta.dir, "fixtures", "mcp-file")
|
||||||
@@ -16,7 +17,7 @@ describe("loadClaudePlugin", () => {
|
|||||||
expect(plugin.manifest.name).toBe("compound-engineering")
|
expect(plugin.manifest.name).toBe("compound-engineering")
|
||||||
expect(plugin.agents.length).toBe(2)
|
expect(plugin.agents.length).toBe(2)
|
||||||
expect(plugin.commands.length).toBe(7)
|
expect(plugin.commands.length).toBe(7)
|
||||||
expect(plugin.skills.length).toBe(2)
|
expect(plugin.skills.length).toBe(3)
|
||||||
expect(plugin.hooks).toBeDefined()
|
expect(plugin.hooks).toBeDefined()
|
||||||
expect(plugin.mcpServers).toBeDefined()
|
expect(plugin.mcpServers).toBeDefined()
|
||||||
|
|
||||||
@@ -66,6 +67,34 @@ describe("loadClaudePlugin", () => {
|
|||||||
expect(normalCommand?.disableModelInvocation).toBeUndefined()
|
expect(normalCommand?.disableModelInvocation).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("parses ce_platforms from skills", async () => {
|
||||||
|
const plugin = await loadClaudePlugin(fixtureRoot)
|
||||||
|
|
||||||
|
const claudeOnly = plugin.skills.find((skill) => skill.name === "claude-only-skill")
|
||||||
|
expect(claudeOnly).toBeDefined()
|
||||||
|
expect(claudeOnly?.ce_platforms).toEqual(["claude"])
|
||||||
|
|
||||||
|
const normalSkill = plugin.skills.find((skill) => skill.name === "skill-one")
|
||||||
|
expect(normalSkill?.ce_platforms).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("filterSkillsByPlatform includes skills without platforms field", async () => {
|
||||||
|
const plugin = await loadClaudePlugin(fixtureRoot)
|
||||||
|
const codexSkills = filterSkillsByPlatform(plugin.skills, "codex")
|
||||||
|
|
||||||
|
expect(codexSkills.find((s) => s.name === "skill-one")).toBeDefined()
|
||||||
|
expect(codexSkills.find((s) => s.name === "disabled-skill")).toBeDefined()
|
||||||
|
expect(codexSkills.find((s) => s.name === "claude-only-skill")).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("filterSkillsByPlatform includes skills matching the platform", async () => {
|
||||||
|
const plugin = await loadClaudePlugin(fixtureRoot)
|
||||||
|
const claudeSkills = filterSkillsByPlatform(plugin.skills, "claude")
|
||||||
|
|
||||||
|
expect(claudeSkills.find((s) => s.name === "skill-one")).toBeDefined()
|
||||||
|
expect(claudeSkills.find((s) => s.name === "claude-only-skill")).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
test("parses disable-model-invocation from skills", async () => {
|
test("parses disable-model-invocation from skills", async () => {
|
||||||
const plugin = await loadClaudePlugin(fixtureRoot)
|
const plugin = await loadClaudePlugin(fixtureRoot)
|
||||||
|
|
||||||
|
|||||||
7
tests/fixtures/sample-plugin/skills/claude-only-skill/SKILL.md
vendored
Normal file
7
tests/fixtures/sample-plugin/skills/claude-only-skill/SKILL.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
name: claude-only-skill
|
||||||
|
description: A skill restricted to Claude Code only
|
||||||
|
ce_platforms: [claude]
|
||||||
|
---
|
||||||
|
|
||||||
|
Claude-only skill body.
|
||||||
Reference in New Issue
Block a user