@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.
Files changed (93) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +5 -1
  3. package/CHANGELOG.md +50 -0
  4. package/CLAUDE.md +3 -3
  5. package/README.md +52 -14
  6. package/docs/plans/2026-02-14-feat-auto-detect-install-and-gemini-sync-plan.md +360 -0
  7. package/docs/plans/2026-02-25-feat-windsurf-global-scope-support-plan.md +627 -0
  8. package/docs/plans/2026-03-01-feat-ce-command-aliases-backwards-compatible-deprecation-plan.md +261 -0
  9. package/docs/plans/feature_opencode-commands-as-md-and-config-merge.md +574 -0
  10. package/docs/solutions/adding-converter-target-providers.md +692 -0
  11. package/docs/solutions/plugin-versioning-requirements.md +3 -3
  12. package/docs/specs/kiro.md +171 -0
  13. package/docs/specs/windsurf.md +477 -0
  14. package/package.json +1 -1
  15. package/plans/landing-page-launchkit-refresh.md +2 -2
  16. package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
  17. package/plugins/compound-engineering/CHANGELOG.md +72 -1
  18. package/plugins/compound-engineering/CLAUDE.md +9 -7
  19. package/plugins/compound-engineering/README.md +10 -7
  20. package/plugins/compound-engineering/agents/research/git-history-analyzer.md +1 -1
  21. package/plugins/compound-engineering/agents/research/learnings-researcher.md +1 -1
  22. package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +1 -1
  23. package/plugins/compound-engineering/commands/ce/brainstorm.md +145 -0
  24. package/plugins/compound-engineering/commands/ce/compound.md +240 -0
  25. package/plugins/compound-engineering/commands/ce/plan.md +636 -0
  26. package/plugins/compound-engineering/commands/ce/review.md +525 -0
  27. package/plugins/compound-engineering/commands/ce/work.md +470 -0
  28. package/plugins/compound-engineering/commands/create-agent-skill.md +1 -1
  29. package/plugins/compound-engineering/commands/deepen-plan.md +6 -6
  30. package/plugins/compound-engineering/commands/deploy-docs.md +1 -1
  31. package/plugins/compound-engineering/commands/feature-video.md +15 -6
  32. package/plugins/compound-engineering/commands/heal-skill.md +1 -1
  33. package/plugins/compound-engineering/commands/lfg.md +3 -3
  34. package/plugins/compound-engineering/commands/slfg.md +3 -3
  35. package/plugins/compound-engineering/commands/test-xcode.md +2 -2
  36. package/plugins/compound-engineering/commands/workflows/brainstorm.md +4 -123
  37. package/plugins/compound-engineering/commands/workflows/compound.md +4 -234
  38. package/plugins/compound-engineering/commands/workflows/plan.md +4 -562
  39. package/plugins/compound-engineering/commands/workflows/review.md +4 -522
  40. package/plugins/compound-engineering/commands/workflows/work.md +4 -448
  41. package/plugins/compound-engineering/skills/brainstorming/SKILL.md +3 -3
  42. package/plugins/compound-engineering/skills/document-review/SKILL.md +1 -1
  43. package/plugins/compound-engineering/skills/file-todos/SKILL.md +1 -1
  44. package/plugins/compound-engineering/skills/git-worktree/SKILL.md +5 -5
  45. package/plugins/compound-engineering/skills/proof/SKILL.md +185 -0
  46. package/plugins/compound-engineering/skills/resolve-pr-parallel/SKILL.md +1 -1
  47. package/plugins/compound-engineering/skills/setup/SKILL.md +2 -2
  48. package/src/commands/convert.ts +101 -23
  49. package/src/commands/install.ts +102 -41
  50. package/src/commands/sync.ts +58 -38
  51. package/src/converters/claude-to-kiro.ts +262 -0
  52. package/src/converters/claude-to-openclaw.ts +240 -0
  53. package/src/converters/claude-to-opencode.ts +12 -10
  54. package/src/converters/claude-to-qwen.ts +238 -0
  55. package/src/converters/claude-to-windsurf.ts +205 -0
  56. package/src/sync/gemini.ts +76 -0
  57. package/src/targets/index.ts +69 -1
  58. package/src/targets/kiro.ts +122 -0
  59. package/src/targets/openclaw.ts +96 -0
  60. package/src/targets/opencode.ts +76 -10
  61. package/src/targets/qwen.ts +64 -0
  62. package/src/targets/windsurf.ts +104 -0
  63. package/src/types/kiro.ts +44 -0
  64. package/src/types/openclaw.ts +52 -0
  65. package/src/types/opencode.ts +7 -8
  66. package/src/types/qwen.ts +48 -0
  67. package/src/types/windsurf.ts +34 -0
  68. package/src/utils/detect-tools.ts +46 -0
  69. package/src/utils/files.ts +7 -0
  70. package/src/utils/resolve-output.ts +50 -0
  71. package/src/utils/secrets.ts +24 -0
  72. package/tests/cli.test.ts +78 -0
  73. package/tests/converter.test.ts +43 -10
  74. package/tests/detect-tools.test.ts +96 -0
  75. package/tests/kiro-converter.test.ts +381 -0
  76. package/tests/kiro-writer.test.ts +273 -0
  77. package/tests/openclaw-converter.test.ts +200 -0
  78. package/tests/opencode-writer.test.ts +142 -5
  79. package/tests/qwen-converter.test.ts +238 -0
  80. package/tests/resolve-output.test.ts +131 -0
  81. package/tests/sync-gemini.test.ts +106 -0
  82. package/tests/windsurf-converter.test.ts +573 -0
  83. package/tests/windsurf-writer.test.ts +359 -0
  84. package/docs/css/docs.css +0 -675
  85. package/docs/css/style.css +0 -2886
  86. package/docs/index.html +0 -1046
  87. package/docs/js/main.js +0 -225
  88. package/docs/pages/agents.html +0 -649
  89. package/docs/pages/changelog.html +0 -534
  90. package/docs/pages/commands.html +0 -523
  91. package/docs/pages/getting-started.html +0 -582
  92. package/docs/pages/mcp-servers.html +0 -409
  93. package/docs/pages/skills.html +0 -611
@@ -0,0 +1,238 @@
1
+ import { formatFrontmatter } from "../utils/frontmatter"
2
+ import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude"
3
+ import type {
4
+ QwenAgentFile,
5
+ QwenBundle,
6
+ QwenCommandFile,
7
+ QwenExtensionConfig,
8
+ QwenMcpServer,
9
+ QwenSetting,
10
+ } from "../types/qwen"
11
+
12
+ export type ClaudeToQwenOptions = {
13
+ agentMode: "primary" | "subagent"
14
+ inferTemperature: boolean
15
+ }
16
+
17
+ export function convertClaudeToQwen(plugin: ClaudePlugin, options: ClaudeToQwenOptions): QwenBundle {
18
+ const agentFiles = plugin.agents.map((agent) => convertAgent(agent, options))
19
+ const cmdFiles = convertCommands(plugin.commands)
20
+ const mcp = plugin.mcpServers ? convertMcp(plugin.mcpServers) : undefined
21
+ const settings = extractSettings(plugin.mcpServers)
22
+
23
+ const config: QwenExtensionConfig = {
24
+ name: plugin.manifest.name,
25
+ version: plugin.manifest.version || "1.0.0",
26
+ commands: "commands",
27
+ skills: "skills",
28
+ agents: "agents",
29
+ }
30
+
31
+ if (mcp && Object.keys(mcp).length > 0) {
32
+ config.mcpServers = mcp
33
+ }
34
+
35
+ if (settings && settings.length > 0) {
36
+ config.settings = settings
37
+ }
38
+
39
+ const contextFile = generateContextFile(plugin)
40
+
41
+ return {
42
+ config,
43
+ agents: agentFiles,
44
+ commandFiles: cmdFiles,
45
+ skillDirs: plugin.skills.map((skill) => ({ sourceDir: skill.sourceDir, name: skill.name })),
46
+ contextFile,
47
+ }
48
+ }
49
+
50
+ function convertAgent(agent: ClaudeAgent, options: ClaudeToQwenOptions): QwenAgentFile {
51
+ const frontmatter: Record<string, unknown> = {
52
+ name: agent.name,
53
+ description: agent.description,
54
+ }
55
+
56
+ if (agent.model && agent.model !== "inherit") {
57
+ frontmatter.model = normalizeModel(agent.model)
58
+ }
59
+
60
+ if (options.inferTemperature) {
61
+ const temperature = inferTemperature(agent)
62
+ if (temperature !== undefined) {
63
+ frontmatter.temperature = temperature
64
+ }
65
+ }
66
+
67
+ // Qwen supports both YAML and Markdown for agents
68
+ // Using YAML format for structured config
69
+ const content = formatFrontmatter(frontmatter, rewriteQwenPaths(agent.body))
70
+
71
+ return {
72
+ name: agent.name,
73
+ content,
74
+ format: "yaml",
75
+ }
76
+ }
77
+
78
+ function convertCommands(commands: ClaudeCommand[]): QwenCommandFile[] {
79
+ const files: QwenCommandFile[] = []
80
+ for (const command of commands) {
81
+ if (command.disableModelInvocation) continue
82
+ const frontmatter: Record<string, unknown> = {
83
+ description: command.description,
84
+ }
85
+ if (command.model && command.model !== "inherit") {
86
+ frontmatter.model = normalizeModel(command.model)
87
+ }
88
+ if (command.allowedTools && command.allowedTools.length > 0) {
89
+ frontmatter.allowedTools = command.allowedTools
90
+ }
91
+ const content = formatFrontmatter(frontmatter, rewriteQwenPaths(command.body))
92
+ files.push({ name: command.name, content })
93
+ }
94
+ return files
95
+ }
96
+
97
+ function convertMcp(servers: Record<string, ClaudeMcpServer>): Record<string, QwenMcpServer> {
98
+ const result: Record<string, QwenMcpServer> = {}
99
+ for (const [name, server] of Object.entries(servers)) {
100
+ if (server.command) {
101
+ result[name] = {
102
+ command: server.command,
103
+ args: server.args,
104
+ env: server.env,
105
+ }
106
+ continue
107
+ }
108
+
109
+ if (server.url) {
110
+ // Qwen only supports stdio (command-based) MCP servers — skip remote servers
111
+ console.warn(
112
+ `Warning: Remote MCP server '${name}' (URL: ${server.url}) is not supported in Qwen format. Qwen only supports stdio MCP servers. Skipping.`,
113
+ )
114
+ }
115
+ }
116
+ return result
117
+ }
118
+
119
+ function extractSettings(mcpServers?: Record<string, ClaudeMcpServer>): QwenSetting[] {
120
+ const settings: QwenSetting[] = []
121
+ if (!mcpServers) return settings
122
+
123
+ for (const [name, server] of Object.entries(mcpServers)) {
124
+ if (server.env) {
125
+ for (const [envVar, value] of Object.entries(server.env)) {
126
+ // Only add settings for environment variables that look like placeholders
127
+ if (value.startsWith("${") || value.includes("YOUR_") || value.includes("XXX")) {
128
+ settings.push({
129
+ name: formatSettingName(envVar),
130
+ description: `Environment variable for ${name} MCP server`,
131
+ envVar,
132
+ sensitive: envVar.toLowerCase().includes("key") || envVar.toLowerCase().includes("token") || envVar.toLowerCase().includes("secret"),
133
+ })
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ return settings
140
+ }
141
+
142
+ function formatSettingName(envVar: string): string {
143
+ return envVar
144
+ .replace(/_/g, " ")
145
+ .toLowerCase()
146
+ .replace(/\b\w/g, (c) => c.toUpperCase())
147
+ }
148
+
149
+ function generateContextFile(plugin: ClaudePlugin): string {
150
+ const sections: string[] = []
151
+
152
+ // Plugin description
153
+ sections.push(`# ${plugin.manifest.name}`)
154
+ sections.push("")
155
+ if (plugin.manifest.description) {
156
+ sections.push(plugin.manifest.description)
157
+ sections.push("")
158
+ }
159
+
160
+ // Agents section
161
+ if (plugin.agents.length > 0) {
162
+ sections.push("## Agents")
163
+ sections.push("")
164
+ for (const agent of plugin.agents) {
165
+ sections.push(`- **${agent.name}**: ${agent.description || "No description"}`)
166
+ }
167
+ sections.push("")
168
+ }
169
+
170
+ // Commands section
171
+ if (plugin.commands.length > 0) {
172
+ sections.push("## Commands")
173
+ sections.push("")
174
+ for (const command of plugin.commands) {
175
+ if (!command.disableModelInvocation) {
176
+ sections.push(`- **/${command.name}**: ${command.description || "No description"}`)
177
+ }
178
+ }
179
+ sections.push("")
180
+ }
181
+
182
+ // Skills section
183
+ if (plugin.skills.length > 0) {
184
+ sections.push("## Skills")
185
+ sections.push("")
186
+ for (const skill of plugin.skills) {
187
+ sections.push(`- ${skill.name}`)
188
+ }
189
+ sections.push("")
190
+ }
191
+
192
+ return sections.join("\n")
193
+ }
194
+
195
+ function rewriteQwenPaths(body: string): string {
196
+ return body
197
+ .replace(/(?<=^|\s|["'`])~\/\.claude\//gm, "~/.qwen/")
198
+ .replace(/(?<=^|\s|["'`])\.claude\//gm, ".qwen/")
199
+ }
200
+
201
+ const CLAUDE_FAMILY_ALIASES: Record<string, string> = {
202
+ haiku: "claude-haiku",
203
+ sonnet: "claude-sonnet",
204
+ opus: "claude-opus",
205
+ }
206
+
207
+ function normalizeModel(model: string): string {
208
+ if (model.includes("/")) return model
209
+ if (CLAUDE_FAMILY_ALIASES[model]) {
210
+ const resolved = `anthropic/${CLAUDE_FAMILY_ALIASES[model]}`
211
+ console.warn(
212
+ `Warning: bare model alias "${model}" mapped to "${resolved}".`,
213
+ )
214
+ return resolved
215
+ }
216
+ if (/^claude-/.test(model)) return `anthropic/${model}`
217
+ if (/^(gpt-|o1-|o3-)/.test(model)) return `openai/${model}`
218
+ if (/^gemini-/.test(model)) return `google/${model}`
219
+ if (/^qwen-/.test(model)) return `qwen/${model}`
220
+ return `anthropic/${model}`
221
+ }
222
+
223
+ function inferTemperature(agent: ClaudeAgent): number | undefined {
224
+ const sample = `${agent.name} ${agent.description ?? ""}`.toLowerCase()
225
+ if (/(review|audit|security|sentinel|oracle|lint|verification|guardian)/.test(sample)) {
226
+ return 0.1
227
+ }
228
+ if (/(plan|planning|architecture|strategist|analysis|research)/.test(sample)) {
229
+ return 0.2
230
+ }
231
+ if (/(doc|readme|changelog|editor|writer)/.test(sample)) {
232
+ return 0.3
233
+ }
234
+ if (/(brainstorm|creative|ideate|design|concept)/.test(sample)) {
235
+ return 0.6
236
+ }
237
+ return undefined
238
+ }
@@ -0,0 +1,205 @@
1
+ import { formatFrontmatter } from "../utils/frontmatter"
2
+ import { findServersWithPotentialSecrets } from "../utils/secrets"
3
+ import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude"
4
+ import type { WindsurfBundle, WindsurfGeneratedSkill, WindsurfMcpConfig, WindsurfMcpServerEntry, WindsurfWorkflow } from "../types/windsurf"
5
+ import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
6
+
7
+ export type ClaudeToWindsurfOptions = ClaudeToOpenCodeOptions
8
+
9
+ const WINDSURF_WORKFLOW_CHAR_LIMIT = 12_000
10
+
11
+ export function convertClaudeToWindsurf(
12
+ plugin: ClaudePlugin,
13
+ _options: ClaudeToWindsurfOptions,
14
+ ): WindsurfBundle {
15
+ const knownAgentNames = plugin.agents.map((a) => normalizeName(a.name))
16
+
17
+ // Pass-through skills (collected first so agent skill names can deduplicate against them)
18
+ const skillDirs = plugin.skills.map((skill) => ({
19
+ name: skill.name,
20
+ sourceDir: skill.sourceDir,
21
+ }))
22
+
23
+ // Convert agents to skills (seed usedNames with pass-through skill names)
24
+ const usedSkillNames = new Set<string>(skillDirs.map((s) => s.name))
25
+ const agentSkills = plugin.agents.map((agent) =>
26
+ convertAgentToSkill(agent, knownAgentNames, usedSkillNames),
27
+ )
28
+
29
+ // Convert commands to workflows
30
+ const usedCommandNames = new Set<string>()
31
+ const commandWorkflows = plugin.commands.map((command) =>
32
+ convertCommandToWorkflow(command, knownAgentNames, usedCommandNames),
33
+ )
34
+
35
+ // Build MCP config
36
+ const mcpConfig = buildMcpConfig(plugin.mcpServers)
37
+
38
+ // Warn about hooks
39
+ if (plugin.hooks && Object.keys(plugin.hooks.hooks).length > 0) {
40
+ console.warn(
41
+ "Warning: Windsurf has no hooks equivalent. Hooks were skipped during conversion.",
42
+ )
43
+ }
44
+
45
+ return { agentSkills, commandWorkflows, skillDirs, mcpConfig }
46
+ }
47
+
48
+ function convertAgentToSkill(
49
+ agent: ClaudeAgent,
50
+ knownAgentNames: string[],
51
+ usedNames: Set<string>,
52
+ ): WindsurfGeneratedSkill {
53
+ const name = uniqueName(normalizeName(agent.name), usedNames)
54
+ const description = sanitizeDescription(
55
+ agent.description ?? `Converted from Claude agent ${agent.name}`,
56
+ )
57
+
58
+ let body = transformContentForWindsurf(agent.body.trim(), knownAgentNames)
59
+ if (agent.capabilities && agent.capabilities.length > 0) {
60
+ const capabilities = agent.capabilities.map((c) => `- ${c}`).join("\n")
61
+ body = `## Capabilities\n${capabilities}\n\n${body}`.trim()
62
+ }
63
+ if (body.length === 0) {
64
+ body = `Instructions converted from the ${agent.name} agent.`
65
+ }
66
+
67
+ const content = formatFrontmatter({ name, description }, `# ${name}\n\n${body}`) + "\n"
68
+ return { name, content }
69
+ }
70
+
71
+ function convertCommandToWorkflow(
72
+ command: ClaudeCommand,
73
+ knownAgentNames: string[],
74
+ usedNames: Set<string>,
75
+ ): WindsurfWorkflow {
76
+ const name = uniqueName(normalizeName(command.name), usedNames)
77
+ const description = sanitizeDescription(
78
+ command.description ?? `Converted from Claude command ${command.name}`,
79
+ )
80
+
81
+ let body = transformContentForWindsurf(command.body.trim(), knownAgentNames)
82
+ if (command.argumentHint) {
83
+ body = `> Arguments: ${command.argumentHint}\n\n${body}`
84
+ }
85
+ if (body.length === 0) {
86
+ body = `Instructions converted from the ${command.name} command.`
87
+ }
88
+
89
+ const frontmatter: Record<string, unknown> = { description }
90
+ const fullContent = formatFrontmatter(frontmatter, `# ${name}\n\n${body}`)
91
+ if (fullContent.length > WINDSURF_WORKFLOW_CHAR_LIMIT) {
92
+ console.warn(
93
+ `Warning: Workflow "${name}" is ${fullContent.length} characters (limit: ${WINDSURF_WORKFLOW_CHAR_LIMIT}). It may be truncated by Windsurf.`,
94
+ )
95
+ }
96
+
97
+ return { name, description, body }
98
+ }
99
+
100
+ /**
101
+ * Transform Claude Code content to Windsurf-compatible content.
102
+ *
103
+ * 1. Path rewriting: .claude/ -> .windsurf/, ~/.claude/ -> ~/.codeium/windsurf/
104
+ * 2. Slash command refs: /workflows:plan -> /workflows-plan (Windsurf invokes workflows as /{name})
105
+ * 3. @agent-name refs: kept as @agent-name (already Windsurf skill invocation syntax)
106
+ * 4. Task agent calls: Task agent-name(args) -> Use the @agent-name skill: args
107
+ */
108
+ export function transformContentForWindsurf(body: string, knownAgentNames: string[] = []): string {
109
+ let result = body
110
+
111
+ // 1. Rewrite paths
112
+ result = result.replace(/(?<=^|\s|["'`])~\/\.claude\//gm, "~/.codeium/windsurf/")
113
+ result = result.replace(/(?<=^|\s|["'`])\.claude\//gm, ".windsurf/")
114
+
115
+ // 2. Slash command refs: /workflows:plan -> /workflows-plan (Windsurf invokes as /{name})
116
+ result = result.replace(/(?<=^|\s)`?\/([a-zA-Z][a-zA-Z0-9_:-]*)`?/gm, (_match, cmdName: string) => {
117
+ const workflowName = normalizeName(cmdName)
118
+ return `/${workflowName}`
119
+ })
120
+
121
+ // 3. @agent-name references: no transformation needed.
122
+ // In Windsurf, @skill-name is the native invocation syntax for skills.
123
+ // Since agents are now mapped to skills, @agent-name already works correctly.
124
+
125
+ // 4. Transform Task agent calls to skill references
126
+ const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9-]*)\(([^)]+)\)/gm
127
+ result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => {
128
+ return `${prefix}Use the @${normalizeName(agentName)} skill: ${args.trim()}`
129
+ })
130
+
131
+ return result
132
+ }
133
+
134
+ function buildMcpConfig(servers?: Record<string, ClaudeMcpServer>): WindsurfMcpConfig | null {
135
+ if (!servers || Object.keys(servers).length === 0) return null
136
+
137
+ const result: Record<string, WindsurfMcpServerEntry> = {}
138
+ for (const [name, server] of Object.entries(servers)) {
139
+ if (server.command) {
140
+ // stdio transport
141
+ const entry: WindsurfMcpServerEntry = { command: server.command }
142
+ if (server.args?.length) entry.args = server.args
143
+ if (server.env && Object.keys(server.env).length > 0) entry.env = server.env
144
+ result[name] = entry
145
+ } else if (server.url) {
146
+ // HTTP/SSE transport
147
+ const entry: WindsurfMcpServerEntry = { serverUrl: server.url }
148
+ if (server.headers && Object.keys(server.headers).length > 0) entry.headers = server.headers
149
+ if (server.env && Object.keys(server.env).length > 0) entry.env = server.env
150
+ result[name] = entry
151
+ } else {
152
+ console.warn(`Warning: MCP server "${name}" has no command or URL. Skipping.`)
153
+ continue
154
+ }
155
+ }
156
+
157
+ if (Object.keys(result).length === 0) return null
158
+
159
+ // Warn about secrets (don't redact — they're needed for the config to work)
160
+ const flagged = findServersWithPotentialSecrets(result)
161
+ if (flagged.length > 0) {
162
+ console.warn(
163
+ `Warning: MCP servers contain env vars that may include secrets: ${flagged.join(", ")}.\n` +
164
+ " These will be written to mcp_config.json. Review before sharing the config file.",
165
+ )
166
+ }
167
+
168
+ return { mcpServers: result }
169
+ }
170
+
171
+ export function normalizeName(value: string): string {
172
+ const trimmed = value.trim()
173
+ if (!trimmed) return "item"
174
+ let normalized = trimmed
175
+ .toLowerCase()
176
+ .replace(/[\\/]+/g, "-")
177
+ .replace(/[:\s]+/g, "-")
178
+ .replace(/[^a-z0-9_-]+/g, "-")
179
+ .replace(/-+/g, "-")
180
+ .replace(/^-+|-+$/g, "")
181
+
182
+ if (normalized.length === 0 || !/^[a-z]/.test(normalized)) {
183
+ return "item"
184
+ }
185
+
186
+ return normalized
187
+ }
188
+
189
+ function sanitizeDescription(value: string): string {
190
+ return value.replace(/\s+/g, " ").trim()
191
+ }
192
+
193
+ function uniqueName(base: string, used: Set<string>): string {
194
+ if (!used.has(base)) {
195
+ used.add(base)
196
+ return base
197
+ }
198
+ let index = 2
199
+ while (used.has(`${base}-${index}`)) {
200
+ index += 1
201
+ }
202
+ const name = `${base}-${index}`
203
+ used.add(name)
204
+ return name
205
+ }
@@ -0,0 +1,76 @@
1
+ import fs from "fs/promises"
2
+ import path from "path"
3
+ import type { ClaudeHomeConfig } from "../parsers/claude-home"
4
+ import type { ClaudeMcpServer } from "../types/claude"
5
+ import { forceSymlink, isValidSkillName } from "../utils/symlink"
6
+
7
+ type GeminiMcpServer = {
8
+ command?: string
9
+ args?: string[]
10
+ url?: string
11
+ env?: Record<string, string>
12
+ headers?: Record<string, string>
13
+ }
14
+
15
+ export async function syncToGemini(
16
+ config: ClaudeHomeConfig,
17
+ outputRoot: string,
18
+ ): Promise<void> {
19
+ const skillsDir = path.join(outputRoot, "skills")
20
+ await fs.mkdir(skillsDir, { recursive: true })
21
+
22
+ for (const skill of config.skills) {
23
+ if (!isValidSkillName(skill.name)) {
24
+ console.warn(`Skipping skill with invalid name: ${skill.name}`)
25
+ continue
26
+ }
27
+ const target = path.join(skillsDir, skill.name)
28
+ await forceSymlink(skill.sourceDir, target)
29
+ }
30
+
31
+ if (Object.keys(config.mcpServers).length > 0) {
32
+ const settingsPath = path.join(outputRoot, "settings.json")
33
+ const existing = await readJsonSafe(settingsPath)
34
+ const converted = convertMcpForGemini(config.mcpServers)
35
+ const existingMcp =
36
+ existing.mcpServers && typeof existing.mcpServers === "object"
37
+ ? (existing.mcpServers as Record<string, unknown>)
38
+ : {}
39
+ const merged = {
40
+ ...existing,
41
+ mcpServers: { ...existingMcp, ...converted },
42
+ }
43
+ await fs.writeFile(settingsPath, JSON.stringify(merged, null, 2), { mode: 0o600 })
44
+ }
45
+ }
46
+
47
+ async function readJsonSafe(filePath: string): Promise<Record<string, unknown>> {
48
+ try {
49
+ const content = await fs.readFile(filePath, "utf-8")
50
+ return JSON.parse(content) as Record<string, unknown>
51
+ } catch (err) {
52
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
53
+ return {}
54
+ }
55
+ throw err
56
+ }
57
+ }
58
+
59
+ function convertMcpForGemini(
60
+ servers: Record<string, ClaudeMcpServer>,
61
+ ): Record<string, GeminiMcpServer> {
62
+ const result: Record<string, GeminiMcpServer> = {}
63
+ for (const [name, server] of Object.entries(servers)) {
64
+ const entry: GeminiMcpServer = {}
65
+ if (server.command) {
66
+ entry.command = server.command
67
+ if (server.args && server.args.length > 0) entry.args = server.args
68
+ if (server.env && Object.keys(server.env).length > 0) entry.env = server.env
69
+ } else if (server.url) {
70
+ entry.url = server.url
71
+ if (server.headers && Object.keys(server.headers).length > 0) entry.headers = server.headers
72
+ }
73
+ result[name] = entry
74
+ }
75
+ return result
76
+ }
@@ -5,24 +5,66 @@ import type { DroidBundle } from "../types/droid"
5
5
  import type { PiBundle } from "../types/pi"
6
6
  import type { CopilotBundle } from "../types/copilot"
7
7
  import type { GeminiBundle } from "../types/gemini"
8
+ import type { KiroBundle } from "../types/kiro"
9
+ import type { WindsurfBundle } from "../types/windsurf"
10
+ import type { OpenClawBundle } from "../types/openclaw"
11
+ import type { QwenBundle } from "../types/qwen"
8
12
  import { convertClaudeToOpenCode, type ClaudeToOpenCodeOptions } from "../converters/claude-to-opencode"
9
13
  import { convertClaudeToCodex } from "../converters/claude-to-codex"
10
14
  import { convertClaudeToDroid } from "../converters/claude-to-droid"
11
15
  import { convertClaudeToPi } from "../converters/claude-to-pi"
12
16
  import { convertClaudeToCopilot } from "../converters/claude-to-copilot"
13
17
  import { convertClaudeToGemini } from "../converters/claude-to-gemini"
18
+ import { convertClaudeToKiro } from "../converters/claude-to-kiro"
19
+ import { convertClaudeToWindsurf } from "../converters/claude-to-windsurf"
20
+ import { convertClaudeToOpenClaw } from "../converters/claude-to-openclaw"
21
+ import { convertClaudeToQwen } from "../converters/claude-to-qwen"
14
22
  import { writeOpenCodeBundle } from "./opencode"
15
23
  import { writeCodexBundle } from "./codex"
16
24
  import { writeDroidBundle } from "./droid"
17
25
  import { writePiBundle } from "./pi"
18
26
  import { writeCopilotBundle } from "./copilot"
19
27
  import { writeGeminiBundle } from "./gemini"
28
+ import { writeKiroBundle } from "./kiro"
29
+ import { writeWindsurfBundle } from "./windsurf"
30
+ import { writeOpenClawBundle } from "./openclaw"
31
+ import { writeQwenBundle } from "./qwen"
32
+
33
+ export type TargetScope = "global" | "workspace"
34
+
35
+ export function isTargetScope(value: string): value is TargetScope {
36
+ return value === "global" || value === "workspace"
37
+ }
38
+
39
+ /**
40
+ * Validate a --scope flag against a target's supported scopes.
41
+ * Returns the resolved scope (explicit or default) or throws on invalid input.
42
+ */
43
+ export function validateScope(
44
+ targetName: string,
45
+ target: TargetHandler,
46
+ scopeArg: string | undefined,
47
+ ): TargetScope | undefined {
48
+ if (scopeArg === undefined) return target.defaultScope
49
+
50
+ if (!target.supportedScopes) {
51
+ throw new Error(`Target "${targetName}" does not support the --scope flag.`)
52
+ }
53
+ if (!isTargetScope(scopeArg) || !target.supportedScopes.includes(scopeArg)) {
54
+ throw new Error(`Target "${targetName}" does not support --scope ${scopeArg}. Supported: ${target.supportedScopes.join(", ")}`)
55
+ }
56
+ return scopeArg
57
+ }
20
58
 
21
59
  export type TargetHandler<TBundle = unknown> = {
22
60
  name: string
23
61
  implemented: boolean
62
+ /** Default scope when --scope is not provided. Only meaningful when supportedScopes is defined. */
63
+ defaultScope?: TargetScope
64
+ /** Valid scope values. If absent, the --scope flag is rejected for this target. */
65
+ supportedScopes?: TargetScope[]
24
66
  convert: (plugin: ClaudePlugin, options: ClaudeToOpenCodeOptions) => TBundle | null
25
- write: (outputRoot: string, bundle: TBundle) => Promise<void>
67
+ write: (outputRoot: string, bundle: TBundle, scope?: TargetScope) => Promise<void>
26
68
  }
27
69
 
28
70
  export const targets: Record<string, TargetHandler> = {
@@ -62,4 +104,30 @@ export const targets: Record<string, TargetHandler> = {
62
104
  convert: convertClaudeToGemini as TargetHandler<GeminiBundle>["convert"],
63
105
  write: writeGeminiBundle as TargetHandler<GeminiBundle>["write"],
64
106
  },
107
+ kiro: {
108
+ name: "kiro",
109
+ implemented: true,
110
+ convert: convertClaudeToKiro as TargetHandler<KiroBundle>["convert"],
111
+ write: writeKiroBundle as TargetHandler<KiroBundle>["write"],
112
+ },
113
+ windsurf: {
114
+ name: "windsurf",
115
+ implemented: true,
116
+ defaultScope: "global",
117
+ supportedScopes: ["global", "workspace"],
118
+ convert: convertClaudeToWindsurf as TargetHandler<WindsurfBundle>["convert"],
119
+ write: writeWindsurfBundle as TargetHandler<WindsurfBundle>["write"],
120
+ },
121
+ openclaw: {
122
+ name: "openclaw",
123
+ implemented: true,
124
+ convert: convertClaudeToOpenClaw as TargetHandler<OpenClawBundle>["convert"],
125
+ write: writeOpenClawBundle as TargetHandler<OpenClawBundle>["write"],
126
+ },
127
+ qwen: {
128
+ name: "qwen",
129
+ implemented: true,
130
+ convert: convertClaudeToQwen as TargetHandler<QwenBundle>["convert"],
131
+ write: writeQwenBundle as TargetHandler<QwenBundle>["write"],
132
+ },
65
133
  }