@every-env/compound-plugin 0.8.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +5 -1
- package/CHANGELOG.md +50 -0
- package/CLAUDE.md +3 -3
- package/README.md +52 -14
- package/docs/plans/2026-02-14-feat-auto-detect-install-and-gemini-sync-plan.md +360 -0
- package/docs/plans/2026-02-25-feat-windsurf-global-scope-support-plan.md +627 -0
- package/docs/plans/2026-03-01-feat-ce-command-aliases-backwards-compatible-deprecation-plan.md +261 -0
- package/docs/plans/feature_opencode-commands-as-md-and-config-merge.md +574 -0
- package/docs/solutions/adding-converter-target-providers.md +692 -0
- package/docs/solutions/plugin-versioning-requirements.md +3 -3
- package/docs/specs/kiro.md +171 -0
- package/docs/specs/windsurf.md +477 -0
- package/package.json +1 -1
- package/plans/landing-page-launchkit-refresh.md +2 -2
- package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
- package/plugins/compound-engineering/CHANGELOG.md +72 -1
- package/plugins/compound-engineering/CLAUDE.md +9 -7
- package/plugins/compound-engineering/README.md +10 -7
- package/plugins/compound-engineering/agents/research/git-history-analyzer.md +1 -1
- package/plugins/compound-engineering/agents/research/learnings-researcher.md +1 -1
- package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +1 -1
- package/plugins/compound-engineering/commands/ce/brainstorm.md +145 -0
- package/plugins/compound-engineering/commands/ce/compound.md +240 -0
- package/plugins/compound-engineering/commands/ce/plan.md +636 -0
- package/plugins/compound-engineering/commands/ce/review.md +525 -0
- package/plugins/compound-engineering/commands/ce/work.md +470 -0
- package/plugins/compound-engineering/commands/create-agent-skill.md +1 -1
- package/plugins/compound-engineering/commands/deepen-plan.md +6 -6
- package/plugins/compound-engineering/commands/deploy-docs.md +1 -1
- package/plugins/compound-engineering/commands/feature-video.md +15 -6
- package/plugins/compound-engineering/commands/heal-skill.md +1 -1
- package/plugins/compound-engineering/commands/lfg.md +3 -3
- package/plugins/compound-engineering/commands/slfg.md +3 -3
- package/plugins/compound-engineering/commands/test-xcode.md +2 -2
- package/plugins/compound-engineering/commands/workflows/brainstorm.md +4 -123
- package/plugins/compound-engineering/commands/workflows/compound.md +4 -234
- package/plugins/compound-engineering/commands/workflows/plan.md +4 -562
- package/plugins/compound-engineering/commands/workflows/review.md +4 -522
- package/plugins/compound-engineering/commands/workflows/work.md +4 -448
- package/plugins/compound-engineering/skills/brainstorming/SKILL.md +3 -3
- package/plugins/compound-engineering/skills/document-review/SKILL.md +1 -1
- package/plugins/compound-engineering/skills/file-todos/SKILL.md +1 -1
- package/plugins/compound-engineering/skills/git-worktree/SKILL.md +5 -5
- package/plugins/compound-engineering/skills/proof/SKILL.md +185 -0
- package/plugins/compound-engineering/skills/resolve-pr-parallel/SKILL.md +1 -1
- package/plugins/compound-engineering/skills/setup/SKILL.md +2 -2
- package/src/commands/convert.ts +101 -23
- package/src/commands/install.ts +102 -41
- package/src/commands/sync.ts +58 -38
- package/src/converters/claude-to-kiro.ts +262 -0
- package/src/converters/claude-to-openclaw.ts +240 -0
- package/src/converters/claude-to-opencode.ts +12 -10
- package/src/converters/claude-to-qwen.ts +238 -0
- package/src/converters/claude-to-windsurf.ts +205 -0
- package/src/sync/gemini.ts +76 -0
- package/src/targets/index.ts +69 -1
- package/src/targets/kiro.ts +122 -0
- package/src/targets/openclaw.ts +96 -0
- package/src/targets/opencode.ts +76 -10
- package/src/targets/qwen.ts +64 -0
- package/src/targets/windsurf.ts +104 -0
- package/src/types/kiro.ts +44 -0
- package/src/types/openclaw.ts +52 -0
- package/src/types/opencode.ts +7 -8
- package/src/types/qwen.ts +48 -0
- package/src/types/windsurf.ts +34 -0
- package/src/utils/detect-tools.ts +46 -0
- package/src/utils/files.ts +7 -0
- package/src/utils/resolve-output.ts +50 -0
- package/src/utils/secrets.ts +24 -0
- package/tests/cli.test.ts +78 -0
- package/tests/converter.test.ts +43 -10
- package/tests/detect-tools.test.ts +96 -0
- package/tests/kiro-converter.test.ts +381 -0
- package/tests/kiro-writer.test.ts +273 -0
- package/tests/openclaw-converter.test.ts +200 -0
- package/tests/opencode-writer.test.ts +142 -5
- package/tests/qwen-converter.test.ts +238 -0
- package/tests/resolve-output.test.ts +131 -0
- package/tests/sync-gemini.test.ts +106 -0
- package/tests/windsurf-converter.test.ts +573 -0
- package/tests/windsurf-writer.test.ts +359 -0
- package/docs/css/docs.css +0 -675
- package/docs/css/style.css +0 -2886
- package/docs/index.html +0 -1046
- package/docs/js/main.js +0 -225
- package/docs/pages/agents.html +0 -649
- package/docs/pages/changelog.html +0 -534
- package/docs/pages/commands.html +0 -523
- package/docs/pages/getting-started.html +0 -582
- package/docs/pages/mcp-servers.html +0 -409
- package/docs/pages/skills.html +0 -611
package/src/commands/sync.ts
CHANGED
|
@@ -7,30 +7,19 @@ import { syncToCodex } from "../sync/codex"
|
|
|
7
7
|
import { syncToPi } from "../sync/pi"
|
|
8
8
|
import { syncToDroid } from "../sync/droid"
|
|
9
9
|
import { syncToCopilot } from "../sync/copilot"
|
|
10
|
+
import { syncToGemini } from "../sync/gemini"
|
|
10
11
|
import { expandHome } from "../utils/resolve-home"
|
|
12
|
+
import { hasPotentialSecrets } from "../utils/secrets"
|
|
13
|
+
import { detectInstalledTools } from "../utils/detect-tools"
|
|
11
14
|
|
|
12
|
-
const validTargets = ["opencode", "codex", "pi", "droid", "copilot"] as const
|
|
15
|
+
const validTargets = ["opencode", "codex", "pi", "droid", "copilot", "gemini", "all"] as const
|
|
13
16
|
type SyncTarget = (typeof validTargets)[number]
|
|
14
17
|
|
|
15
18
|
function isValidTarget(value: string): value is SyncTarget {
|
|
16
19
|
return (validTargets as readonly string[]).includes(value)
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
function hasPotentialSecrets(mcpServers: Record<string, unknown>): boolean {
|
|
21
|
-
const sensitivePatterns = /key|token|secret|password|credential|api_key/i
|
|
22
|
-
for (const server of Object.values(mcpServers)) {
|
|
23
|
-
const env = (server as { env?: Record<string, string> }).env
|
|
24
|
-
if (env) {
|
|
25
|
-
for (const key of Object.keys(env)) {
|
|
26
|
-
if (sensitivePatterns.test(key)) return true
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return false
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function resolveOutputRoot(target: SyncTarget): string {
|
|
22
|
+
function resolveOutputRoot(target: string): string {
|
|
34
23
|
switch (target) {
|
|
35
24
|
case "opencode":
|
|
36
25
|
return path.join(os.homedir(), ".config", "opencode")
|
|
@@ -42,19 +31,46 @@ function resolveOutputRoot(target: SyncTarget): string {
|
|
|
42
31
|
return path.join(os.homedir(), ".factory")
|
|
43
32
|
case "copilot":
|
|
44
33
|
return path.join(process.cwd(), ".github")
|
|
34
|
+
case "gemini":
|
|
35
|
+
return path.join(process.cwd(), ".gemini")
|
|
36
|
+
default:
|
|
37
|
+
throw new Error(`No output root for target: ${target}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function syncTarget(target: string, config: Awaited<ReturnType<typeof loadClaudeHome>>, outputRoot: string): Promise<void> {
|
|
42
|
+
switch (target) {
|
|
43
|
+
case "opencode":
|
|
44
|
+
await syncToOpenCode(config, outputRoot)
|
|
45
|
+
break
|
|
46
|
+
case "codex":
|
|
47
|
+
await syncToCodex(config, outputRoot)
|
|
48
|
+
break
|
|
49
|
+
case "pi":
|
|
50
|
+
await syncToPi(config, outputRoot)
|
|
51
|
+
break
|
|
52
|
+
case "droid":
|
|
53
|
+
await syncToDroid(config, outputRoot)
|
|
54
|
+
break
|
|
55
|
+
case "copilot":
|
|
56
|
+
await syncToCopilot(config, outputRoot)
|
|
57
|
+
break
|
|
58
|
+
case "gemini":
|
|
59
|
+
await syncToGemini(config, outputRoot)
|
|
60
|
+
break
|
|
45
61
|
}
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
export default defineCommand({
|
|
49
65
|
meta: {
|
|
50
66
|
name: "sync",
|
|
51
|
-
description: "Sync Claude Code config (~/.claude/) to OpenCode, Codex, Pi, Droid, or
|
|
67
|
+
description: "Sync Claude Code config (~/.claude/) to OpenCode, Codex, Pi, Droid, Copilot, or Gemini",
|
|
52
68
|
},
|
|
53
69
|
args: {
|
|
54
70
|
target: {
|
|
55
71
|
type: "string",
|
|
56
|
-
|
|
57
|
-
description: "Target: opencode | codex | pi | droid | copilot",
|
|
72
|
+
default: "all",
|
|
73
|
+
description: "Target: opencode | codex | pi | droid | copilot | gemini | all (default: all)",
|
|
58
74
|
},
|
|
59
75
|
claudeHome: {
|
|
60
76
|
type: "string",
|
|
@@ -78,30 +94,34 @@ export default defineCommand({
|
|
|
78
94
|
)
|
|
79
95
|
}
|
|
80
96
|
|
|
97
|
+
if (args.target === "all") {
|
|
98
|
+
const detected = await detectInstalledTools()
|
|
99
|
+
const activeTargets = detected.filter((t) => t.detected).map((t) => t.name)
|
|
100
|
+
|
|
101
|
+
if (activeTargets.length === 0) {
|
|
102
|
+
console.log("No AI coding tools detected.")
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log(`Syncing to ${activeTargets.length} detected tool(s)...`)
|
|
107
|
+
for (const tool of detected) {
|
|
108
|
+
console.log(` ${tool.detected ? "✓" : "✗"} ${tool.name} — ${tool.reason}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const name of activeTargets) {
|
|
112
|
+
const outputRoot = resolveOutputRoot(name)
|
|
113
|
+
await syncTarget(name, config, outputRoot)
|
|
114
|
+
console.log(`✓ Synced to ${name}: ${outputRoot}`)
|
|
115
|
+
}
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
81
119
|
console.log(
|
|
82
120
|
`Syncing ${config.skills.length} skills, ${Object.keys(config.mcpServers).length} MCP servers...`,
|
|
83
121
|
)
|
|
84
122
|
|
|
85
123
|
const outputRoot = resolveOutputRoot(args.target)
|
|
86
|
-
|
|
87
|
-
switch (args.target) {
|
|
88
|
-
case "opencode":
|
|
89
|
-
await syncToOpenCode(config, outputRoot)
|
|
90
|
-
break
|
|
91
|
-
case "codex":
|
|
92
|
-
await syncToCodex(config, outputRoot)
|
|
93
|
-
break
|
|
94
|
-
case "pi":
|
|
95
|
-
await syncToPi(config, outputRoot)
|
|
96
|
-
break
|
|
97
|
-
case "droid":
|
|
98
|
-
await syncToDroid(config, outputRoot)
|
|
99
|
-
break
|
|
100
|
-
case "copilot":
|
|
101
|
-
await syncToCopilot(config, outputRoot)
|
|
102
|
-
break
|
|
103
|
-
}
|
|
104
|
-
|
|
124
|
+
await syncTarget(args.target, config, outputRoot)
|
|
105
125
|
console.log(`✓ Synced to ${args.target}: ${outputRoot}`)
|
|
106
126
|
},
|
|
107
127
|
})
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { formatFrontmatter } from "../utils/frontmatter"
|
|
4
|
+
import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude"
|
|
5
|
+
import type {
|
|
6
|
+
KiroAgent,
|
|
7
|
+
KiroAgentConfig,
|
|
8
|
+
KiroBundle,
|
|
9
|
+
KiroMcpServer,
|
|
10
|
+
KiroSkill,
|
|
11
|
+
KiroSteeringFile,
|
|
12
|
+
} from "../types/kiro"
|
|
13
|
+
import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
|
|
14
|
+
|
|
15
|
+
export type ClaudeToKiroOptions = ClaudeToOpenCodeOptions
|
|
16
|
+
|
|
17
|
+
const KIRO_SKILL_NAME_MAX_LENGTH = 64
|
|
18
|
+
const KIRO_SKILL_NAME_PATTERN = /^[a-z][a-z0-9-]*$/
|
|
19
|
+
const KIRO_DESCRIPTION_MAX_LENGTH = 1024
|
|
20
|
+
|
|
21
|
+
const CLAUDE_TO_KIRO_TOOLS: Record<string, string> = {
|
|
22
|
+
Bash: "shell",
|
|
23
|
+
Write: "write",
|
|
24
|
+
Read: "read",
|
|
25
|
+
Edit: "write", // NOTE: Kiro write is full-file, not surgical edit. Lossy mapping.
|
|
26
|
+
Glob: "glob",
|
|
27
|
+
Grep: "grep",
|
|
28
|
+
WebFetch: "web_fetch",
|
|
29
|
+
Task: "use_subagent",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function convertClaudeToKiro(
|
|
33
|
+
plugin: ClaudePlugin,
|
|
34
|
+
_options: ClaudeToKiroOptions,
|
|
35
|
+
): KiroBundle {
|
|
36
|
+
const usedSkillNames = new Set<string>()
|
|
37
|
+
|
|
38
|
+
// Pass-through skills are processed first — they're the source of truth
|
|
39
|
+
const skillDirs = plugin.skills.map((skill) => ({
|
|
40
|
+
name: skill.name,
|
|
41
|
+
sourceDir: skill.sourceDir,
|
|
42
|
+
}))
|
|
43
|
+
for (const skill of skillDirs) {
|
|
44
|
+
usedSkillNames.add(normalizeName(skill.name))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Convert agents to Kiro custom agents
|
|
48
|
+
const agentNames = plugin.agents.map((a) => normalizeName(a.name))
|
|
49
|
+
const agents = plugin.agents.map((agent) => convertAgentToKiroAgent(agent, agentNames))
|
|
50
|
+
|
|
51
|
+
// Convert commands to skills (generated)
|
|
52
|
+
const generatedSkills = plugin.commands.map((command) =>
|
|
53
|
+
convertCommandToSkill(command, usedSkillNames, agentNames),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// Convert MCP servers (stdio only)
|
|
57
|
+
const mcpServers = convertMcpServers(plugin.mcpServers)
|
|
58
|
+
|
|
59
|
+
// Build steering files from CLAUDE.md
|
|
60
|
+
const steeringFiles = buildSteeringFiles(plugin, agentNames)
|
|
61
|
+
|
|
62
|
+
// Warn about hooks
|
|
63
|
+
if (plugin.hooks && Object.keys(plugin.hooks.hooks).length > 0) {
|
|
64
|
+
console.warn(
|
|
65
|
+
"Warning: Kiro CLI hooks use a different format (preToolUse/postToolUse inside agent configs). Hooks were skipped during conversion.",
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { agents, generatedSkills, skillDirs, steeringFiles, mcpServers }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function convertAgentToKiroAgent(agent: ClaudeAgent, knownAgentNames: string[]): KiroAgent {
|
|
73
|
+
const name = normalizeName(agent.name)
|
|
74
|
+
const description = sanitizeDescription(
|
|
75
|
+
agent.description ?? `Use this agent for ${agent.name} tasks`,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const config: KiroAgentConfig = {
|
|
79
|
+
name,
|
|
80
|
+
description,
|
|
81
|
+
prompt: `file://./prompts/${name}.md`,
|
|
82
|
+
tools: ["*"],
|
|
83
|
+
resources: [
|
|
84
|
+
"file://.kiro/steering/**/*.md",
|
|
85
|
+
"skill://.kiro/skills/**/SKILL.md",
|
|
86
|
+
],
|
|
87
|
+
includeMcpJson: true,
|
|
88
|
+
welcomeMessage: `Switching to the ${name} agent. ${description}`,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let body = transformContentForKiro(agent.body.trim(), knownAgentNames)
|
|
92
|
+
if (agent.capabilities && agent.capabilities.length > 0) {
|
|
93
|
+
const capabilities = agent.capabilities.map((c) => `- ${c}`).join("\n")
|
|
94
|
+
body = `## Capabilities\n${capabilities}\n\n${body}`.trim()
|
|
95
|
+
}
|
|
96
|
+
if (body.length === 0) {
|
|
97
|
+
body = `Instructions converted from the ${agent.name} agent.`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { name, config, promptContent: body }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function convertCommandToSkill(
|
|
104
|
+
command: ClaudeCommand,
|
|
105
|
+
usedNames: Set<string>,
|
|
106
|
+
knownAgentNames: string[],
|
|
107
|
+
): KiroSkill {
|
|
108
|
+
const rawName = normalizeName(command.name)
|
|
109
|
+
const name = uniqueName(rawName, usedNames)
|
|
110
|
+
|
|
111
|
+
const description = sanitizeDescription(
|
|
112
|
+
command.description ?? `Converted from Claude command ${command.name}`,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
const frontmatter: Record<string, unknown> = { name, description }
|
|
116
|
+
|
|
117
|
+
let body = transformContentForKiro(command.body.trim(), knownAgentNames)
|
|
118
|
+
if (body.length === 0) {
|
|
119
|
+
body = `Instructions converted from the ${command.name} command.`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const content = formatFrontmatter(frontmatter, body)
|
|
123
|
+
return { name, content }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Transform Claude Code content to Kiro-compatible content.
|
|
128
|
+
*
|
|
129
|
+
* 1. Task agent calls: Task agent-name(args) -> Use the use_subagent tool ...
|
|
130
|
+
* 2. Path rewriting: .claude/ -> .kiro/, ~/.claude/ -> ~/.kiro/
|
|
131
|
+
* 3. Slash command refs: /workflows:plan -> use the workflows-plan skill
|
|
132
|
+
* 4. Claude tool names: Bash -> shell, Read -> read, etc.
|
|
133
|
+
* 5. Agent refs: @agent-name -> the agent-name agent (only for known agent names)
|
|
134
|
+
*/
|
|
135
|
+
export function transformContentForKiro(body: string, knownAgentNames: string[] = []): string {
|
|
136
|
+
let result = body
|
|
137
|
+
|
|
138
|
+
// 1. Transform Task agent calls
|
|
139
|
+
const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9-]*)\(([^)]+)\)/gm
|
|
140
|
+
result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => {
|
|
141
|
+
return `${prefix}Use the use_subagent tool to delegate to the ${normalizeName(agentName)} agent: ${args.trim()}`
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// 2. Rewrite .claude/ paths to .kiro/ (with word-boundary-like lookbehind)
|
|
145
|
+
result = result.replace(/(?<=^|\s|["'`])~\/\.claude\//gm, "~/.kiro/")
|
|
146
|
+
result = result.replace(/(?<=^|\s|["'`])\.claude\//gm, ".kiro/")
|
|
147
|
+
|
|
148
|
+
// 3. Slash command refs: /command-name -> skill activation language
|
|
149
|
+
result = result.replace(/(?<=^|\s)`?\/([a-zA-Z][a-zA-Z0-9_:-]*)`?/gm, (_match, cmdName: string) => {
|
|
150
|
+
const skillName = normalizeName(cmdName)
|
|
151
|
+
return `the ${skillName} skill`
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// 4. Claude tool names -> Kiro tool names
|
|
155
|
+
for (const [claudeTool, kiroTool] of Object.entries(CLAUDE_TO_KIRO_TOOLS)) {
|
|
156
|
+
// Match tool name references: "the X tool", "using X", "use X to"
|
|
157
|
+
const toolPattern = new RegExp(`\\b${claudeTool}\\b(?=\\s+tool|\\s+to\\s)`, "g")
|
|
158
|
+
result = result.replace(toolPattern, kiroTool)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 5. Transform @agent-name references (only for known agent names)
|
|
162
|
+
if (knownAgentNames.length > 0) {
|
|
163
|
+
const escapedNames = knownAgentNames.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
164
|
+
const agentRefPattern = new RegExp(`@(${escapedNames.join("|")})\\b`, "g")
|
|
165
|
+
result = result.replace(agentRefPattern, (_match, agentName: string) => {
|
|
166
|
+
return `the ${normalizeName(agentName)} agent`
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return result
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function convertMcpServers(
|
|
174
|
+
servers?: Record<string, ClaudeMcpServer>,
|
|
175
|
+
): Record<string, KiroMcpServer> {
|
|
176
|
+
if (!servers || Object.keys(servers).length === 0) return {}
|
|
177
|
+
|
|
178
|
+
const result: Record<string, KiroMcpServer> = {}
|
|
179
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
180
|
+
if (!server.command) {
|
|
181
|
+
console.warn(
|
|
182
|
+
`Warning: MCP server "${name}" has no command (HTTP/SSE transport). Kiro only supports stdio. Skipping.`,
|
|
183
|
+
)
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const entry: KiroMcpServer = { command: server.command }
|
|
188
|
+
if (server.args && server.args.length > 0) entry.args = server.args
|
|
189
|
+
if (server.env && Object.keys(server.env).length > 0) entry.env = server.env
|
|
190
|
+
|
|
191
|
+
console.log(`MCP server "${name}" will execute: ${server.command}${server.args ? " " + server.args.join(" ") : ""}`)
|
|
192
|
+
result[name] = entry
|
|
193
|
+
}
|
|
194
|
+
return result
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function buildSteeringFiles(plugin: ClaudePlugin, knownAgentNames: string[]): KiroSteeringFile[] {
|
|
198
|
+
const claudeMdPath = path.join(plugin.root, "CLAUDE.md")
|
|
199
|
+
if (!existsSync(claudeMdPath)) return []
|
|
200
|
+
|
|
201
|
+
let content: string
|
|
202
|
+
try {
|
|
203
|
+
content = readFileSync(claudeMdPath, "utf8")
|
|
204
|
+
} catch {
|
|
205
|
+
return []
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!content || content.trim().length === 0) return []
|
|
209
|
+
|
|
210
|
+
const transformed = transformContentForKiro(content, knownAgentNames)
|
|
211
|
+
return [{ name: "compound-engineering", content: transformed }]
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function normalizeName(value: string): string {
|
|
215
|
+
const trimmed = value.trim()
|
|
216
|
+
if (!trimmed) return "item"
|
|
217
|
+
let normalized = trimmed
|
|
218
|
+
.toLowerCase()
|
|
219
|
+
.replace(/[\\/]+/g, "-")
|
|
220
|
+
.replace(/[:\s]+/g, "-")
|
|
221
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
222
|
+
.replace(/-+/g, "-") // Collapse consecutive hyphens (Agent Skills standard)
|
|
223
|
+
.replace(/^-+|-+$/g, "")
|
|
224
|
+
|
|
225
|
+
// Enforce max length (truncate at last hyphen boundary)
|
|
226
|
+
if (normalized.length > KIRO_SKILL_NAME_MAX_LENGTH) {
|
|
227
|
+
normalized = normalized.slice(0, KIRO_SKILL_NAME_MAX_LENGTH)
|
|
228
|
+
const lastHyphen = normalized.lastIndexOf("-")
|
|
229
|
+
if (lastHyphen > 0) {
|
|
230
|
+
normalized = normalized.slice(0, lastHyphen)
|
|
231
|
+
}
|
|
232
|
+
normalized = normalized.replace(/-+$/g, "")
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Ensure name starts with a letter
|
|
236
|
+
if (normalized.length === 0 || !/^[a-z]/.test(normalized)) {
|
|
237
|
+
return "item"
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return normalized
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function sanitizeDescription(value: string, maxLength = KIRO_DESCRIPTION_MAX_LENGTH): string {
|
|
244
|
+
const normalized = value.replace(/\s+/g, " ").trim()
|
|
245
|
+
if (normalized.length <= maxLength) return normalized
|
|
246
|
+
const ellipsis = "..."
|
|
247
|
+
return normalized.slice(0, Math.max(0, maxLength - ellipsis.length)).trimEnd() + ellipsis
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function uniqueName(base: string, used: Set<string>): string {
|
|
251
|
+
if (!used.has(base)) {
|
|
252
|
+
used.add(base)
|
|
253
|
+
return base
|
|
254
|
+
}
|
|
255
|
+
let index = 2
|
|
256
|
+
while (used.has(`${base}-${index}`)) {
|
|
257
|
+
index += 1
|
|
258
|
+
}
|
|
259
|
+
const name = `${base}-${index}`
|
|
260
|
+
used.add(name)
|
|
261
|
+
return name
|
|
262
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { formatFrontmatter } from "../utils/frontmatter"
|
|
2
|
+
import type {
|
|
3
|
+
ClaudeAgent,
|
|
4
|
+
ClaudeCommand,
|
|
5
|
+
ClaudePlugin,
|
|
6
|
+
ClaudeMcpServer,
|
|
7
|
+
} from "../types/claude"
|
|
8
|
+
import type {
|
|
9
|
+
OpenClawBundle,
|
|
10
|
+
OpenClawCommandRegistration,
|
|
11
|
+
OpenClawPluginManifest,
|
|
12
|
+
OpenClawSkillFile,
|
|
13
|
+
} from "../types/openclaw"
|
|
14
|
+
import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
|
|
15
|
+
|
|
16
|
+
export type ClaudeToOpenClawOptions = ClaudeToOpenCodeOptions
|
|
17
|
+
|
|
18
|
+
export function convertClaudeToOpenClaw(
|
|
19
|
+
plugin: ClaudePlugin,
|
|
20
|
+
_options: ClaudeToOpenClawOptions,
|
|
21
|
+
): OpenClawBundle {
|
|
22
|
+
const enabledCommands = plugin.commands.filter((cmd) => !cmd.disableModelInvocation)
|
|
23
|
+
|
|
24
|
+
const agentSkills = plugin.agents.map(convertAgentToSkill)
|
|
25
|
+
const commandSkills = enabledCommands.map(convertCommandToSkill)
|
|
26
|
+
const commands = enabledCommands.map(convertCommand)
|
|
27
|
+
|
|
28
|
+
const skills: OpenClawSkillFile[] = [...agentSkills, ...commandSkills]
|
|
29
|
+
|
|
30
|
+
const skillDirCopies = plugin.skills.map((skill) => ({
|
|
31
|
+
sourceDir: skill.sourceDir,
|
|
32
|
+
name: skill.name,
|
|
33
|
+
}))
|
|
34
|
+
|
|
35
|
+
const allSkillDirs = [
|
|
36
|
+
...agentSkills.map((s) => s.dir),
|
|
37
|
+
...commandSkills.map((s) => s.dir),
|
|
38
|
+
...plugin.skills.map((s) => s.name),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const manifest = buildManifest(plugin, allSkillDirs)
|
|
42
|
+
|
|
43
|
+
const packageJson = buildPackageJson(plugin)
|
|
44
|
+
|
|
45
|
+
const openclawConfig = plugin.mcpServers
|
|
46
|
+
? buildOpenClawConfig(plugin.mcpServers)
|
|
47
|
+
: undefined
|
|
48
|
+
|
|
49
|
+
const entryPoint = generateEntryPoint(commands)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
manifest,
|
|
53
|
+
packageJson,
|
|
54
|
+
entryPoint,
|
|
55
|
+
skills,
|
|
56
|
+
skillDirCopies,
|
|
57
|
+
commands,
|
|
58
|
+
openclawConfig,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildManifest(plugin: ClaudePlugin, skillDirs: string[]): OpenClawPluginManifest {
|
|
63
|
+
return {
|
|
64
|
+
id: plugin.manifest.name,
|
|
65
|
+
name: formatDisplayName(plugin.manifest.name),
|
|
66
|
+
kind: "tool",
|
|
67
|
+
skills: skillDirs.map((dir) => `skills/${dir}`),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildPackageJson(plugin: ClaudePlugin): Record<string, unknown> {
|
|
72
|
+
return {
|
|
73
|
+
name: `openclaw-${plugin.manifest.name}`,
|
|
74
|
+
version: plugin.manifest.version,
|
|
75
|
+
type: "module",
|
|
76
|
+
private: true,
|
|
77
|
+
description: plugin.manifest.description,
|
|
78
|
+
main: "index.ts",
|
|
79
|
+
openclaw: {
|
|
80
|
+
extensions: [
|
|
81
|
+
{
|
|
82
|
+
id: plugin.manifest.name,
|
|
83
|
+
entry: "./index.ts",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
keywords: [
|
|
88
|
+
"openclaw",
|
|
89
|
+
"openclaw-plugin",
|
|
90
|
+
...(plugin.manifest.keywords ?? []),
|
|
91
|
+
],
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function convertAgentToSkill(agent: ClaudeAgent): OpenClawSkillFile {
|
|
96
|
+
const frontmatter: Record<string, unknown> = {
|
|
97
|
+
name: agent.name,
|
|
98
|
+
description: agent.description,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (agent.model && agent.model !== "inherit") {
|
|
102
|
+
frontmatter.model = agent.model
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const body = rewritePaths(agent.body)
|
|
106
|
+
const content = formatFrontmatter(frontmatter, body)
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
name: agent.name,
|
|
110
|
+
content,
|
|
111
|
+
dir: `agent-${agent.name}`,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function convertCommandToSkill(command: ClaudeCommand): OpenClawSkillFile {
|
|
116
|
+
const frontmatter: Record<string, unknown> = {
|
|
117
|
+
name: `cmd-${command.name}`,
|
|
118
|
+
description: command.description,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (command.model && command.model !== "inherit") {
|
|
122
|
+
frontmatter.model = command.model
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const body = rewritePaths(command.body)
|
|
126
|
+
const content = formatFrontmatter(frontmatter, body)
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
name: command.name,
|
|
130
|
+
content,
|
|
131
|
+
dir: `cmd-${command.name}`,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function convertCommand(command: ClaudeCommand): OpenClawCommandRegistration {
|
|
136
|
+
return {
|
|
137
|
+
name: command.name.replace(/:/g, "-"),
|
|
138
|
+
description: command.description ?? `Run ${command.name}`,
|
|
139
|
+
acceptsArgs: Boolean(command.argumentHint),
|
|
140
|
+
body: rewritePaths(command.body),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildOpenClawConfig(
|
|
145
|
+
servers: Record<string, ClaudeMcpServer>,
|
|
146
|
+
): Record<string, unknown> {
|
|
147
|
+
const mcpServers: Record<string, unknown> = {}
|
|
148
|
+
|
|
149
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
150
|
+
if (server.command) {
|
|
151
|
+
mcpServers[name] = {
|
|
152
|
+
type: "stdio",
|
|
153
|
+
command: server.command,
|
|
154
|
+
args: server.args ?? [],
|
|
155
|
+
env: server.env,
|
|
156
|
+
}
|
|
157
|
+
} else if (server.url) {
|
|
158
|
+
mcpServers[name] = {
|
|
159
|
+
type: "http",
|
|
160
|
+
url: server.url,
|
|
161
|
+
headers: server.headers,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { mcpServers }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function generateEntryPoint(commands: OpenClawCommandRegistration[]): string {
|
|
170
|
+
const commandRegistrations = commands
|
|
171
|
+
.map((cmd) => {
|
|
172
|
+
// JSON.stringify produces a fully-escaped string literal safe for JS/TS source embedding
|
|
173
|
+
const safeName = JSON.stringify(cmd.name)
|
|
174
|
+
const safeDesc = JSON.stringify(cmd.description ?? "")
|
|
175
|
+
const safeNotFound = JSON.stringify(`Command ${cmd.name} not found. Check skills directory.`)
|
|
176
|
+
return ` api.registerCommand({
|
|
177
|
+
name: ${safeName},
|
|
178
|
+
description: ${safeDesc},
|
|
179
|
+
acceptsArgs: ${cmd.acceptsArgs},
|
|
180
|
+
requireAuth: false,
|
|
181
|
+
handler: (ctx) => ({
|
|
182
|
+
text: skills[${safeName}] ?? ${safeNotFound},
|
|
183
|
+
}),
|
|
184
|
+
});`
|
|
185
|
+
})
|
|
186
|
+
.join("\n\n")
|
|
187
|
+
|
|
188
|
+
return `// Auto-generated OpenClaw plugin entry point
|
|
189
|
+
// Converted from Claude Code plugin format by compound-plugin CLI
|
|
190
|
+
import { promises as fs } from "fs";
|
|
191
|
+
import path from "path";
|
|
192
|
+
import { fileURLToPath } from "url";
|
|
193
|
+
|
|
194
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
195
|
+
|
|
196
|
+
// Pre-load skill bodies for command responses
|
|
197
|
+
const skills: Record<string, string> = {};
|
|
198
|
+
|
|
199
|
+
async function loadSkills() {
|
|
200
|
+
const skillsDir = path.join(__dirname, "skills");
|
|
201
|
+
try {
|
|
202
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
203
|
+
for (const entry of entries) {
|
|
204
|
+
if (!entry.isDirectory()) continue;
|
|
205
|
+
const skillPath = path.join(skillsDir, entry.name, "SKILL.md");
|
|
206
|
+
try {
|
|
207
|
+
const content = await fs.readFile(skillPath, "utf8");
|
|
208
|
+
// Strip frontmatter
|
|
209
|
+
const body = content.replace(/^---[\\s\\S]*?---\\n*/, "");
|
|
210
|
+
skills[entry.name.replace(/^cmd-/, "")] = body.trim();
|
|
211
|
+
} catch {
|
|
212
|
+
// Skill file not found, skip
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// Skills directory not found
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export default async function register(api) {
|
|
221
|
+
await loadSkills();
|
|
222
|
+
|
|
223
|
+
${commandRegistrations}
|
|
224
|
+
}
|
|
225
|
+
`
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function rewritePaths(body: string): string {
|
|
229
|
+
return body
|
|
230
|
+
.replace(/(?<=^|\s|["'`])~\/\.claude\//gm, "~/.openclaw/")
|
|
231
|
+
.replace(/(?<=^|\s|["'`])\.claude\//gm, ".openclaw/")
|
|
232
|
+
.replace(/\.claude-plugin\//g, "openclaw-plugin/")
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function formatDisplayName(name: string): string {
|
|
236
|
+
return name
|
|
237
|
+
.split("-")
|
|
238
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
239
|
+
.join(" ")
|
|
240
|
+
}
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
} from "../types/claude"
|
|
9
9
|
import type {
|
|
10
10
|
OpenCodeBundle,
|
|
11
|
-
|
|
11
|
+
OpenCodeCommandFile,
|
|
12
12
|
OpenCodeConfig,
|
|
13
13
|
OpenCodeMcpServer,
|
|
14
14
|
} from "../types/opencode"
|
|
@@ -66,13 +66,12 @@ export function convertClaudeToOpenCode(
|
|
|
66
66
|
options: ClaudeToOpenCodeOptions,
|
|
67
67
|
): OpenCodeBundle {
|
|
68
68
|
const agentFiles = plugin.agents.map((agent) => convertAgent(agent, options))
|
|
69
|
-
const
|
|
69
|
+
const cmdFiles = convertCommands(plugin.commands)
|
|
70
70
|
const mcp = plugin.mcpServers ? convertMcp(plugin.mcpServers) : undefined
|
|
71
71
|
const plugins = plugin.hooks ? [convertHooks(plugin.hooks)] : []
|
|
72
72
|
|
|
73
73
|
const config: OpenCodeConfig = {
|
|
74
74
|
$schema: "https://opencode.ai/config.json",
|
|
75
|
-
command: Object.keys(commandMap).length > 0 ? commandMap : undefined,
|
|
76
75
|
mcp: mcp && Object.keys(mcp).length > 0 ? mcp : undefined,
|
|
77
76
|
}
|
|
78
77
|
|
|
@@ -81,6 +80,7 @@ export function convertClaudeToOpenCode(
|
|
|
81
80
|
return {
|
|
82
81
|
config,
|
|
83
82
|
agents: agentFiles,
|
|
83
|
+
commandFiles: cmdFiles,
|
|
84
84
|
plugins,
|
|
85
85
|
skillDirs: plugin.skills.map((skill) => ({ sourceDir: skill.sourceDir, name: skill.name })),
|
|
86
86
|
}
|
|
@@ -111,20 +111,22 @@ function convertAgent(agent: ClaudeAgent, options: ClaudeToOpenCodeOptions) {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
// Commands are written as individual .md files rather than entries in opencode.json.
|
|
115
|
+
// Chosen over JSON map because opencode resolves commands by filename at runtime (ADR-001).
|
|
116
|
+
function convertCommands(commands: ClaudeCommand[]): OpenCodeCommandFile[] {
|
|
117
|
+
const files: OpenCodeCommandFile[] = []
|
|
116
118
|
for (const command of commands) {
|
|
117
119
|
if (command.disableModelInvocation) continue
|
|
118
|
-
const
|
|
120
|
+
const frontmatter: Record<string, unknown> = {
|
|
119
121
|
description: command.description,
|
|
120
|
-
template: rewriteClaudePaths(command.body),
|
|
121
122
|
}
|
|
122
123
|
if (command.model && command.model !== "inherit") {
|
|
123
|
-
|
|
124
|
+
frontmatter.model = normalizeModel(command.model)
|
|
124
125
|
}
|
|
125
|
-
|
|
126
|
+
const content = formatFrontmatter(frontmatter, rewriteClaudePaths(command.body))
|
|
127
|
+
files.push({ name: command.name, content })
|
|
126
128
|
}
|
|
127
|
-
return
|
|
129
|
+
return files
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
function convertMcp(servers: Record<string, ClaudeMcpServer>): Record<string, OpenCodeMcpServer> {
|