@every-env/compound-plugin 0.9.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 (87) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +5 -1
  3. package/CHANGELOG.md +42 -0
  4. package/CLAUDE.md +3 -3
  5. package/README.md +49 -15
  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/windsurf.md +477 -0
  13. package/package.json +1 -1
  14. package/plans/landing-page-launchkit-refresh.md +2 -2
  15. package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
  16. package/plugins/compound-engineering/CHANGELOG.md +72 -1
  17. package/plugins/compound-engineering/CLAUDE.md +9 -7
  18. package/plugins/compound-engineering/README.md +10 -7
  19. package/plugins/compound-engineering/agents/research/git-history-analyzer.md +1 -1
  20. package/plugins/compound-engineering/agents/research/learnings-researcher.md +1 -1
  21. package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +1 -1
  22. package/plugins/compound-engineering/commands/ce/brainstorm.md +145 -0
  23. package/plugins/compound-engineering/commands/ce/compound.md +240 -0
  24. package/plugins/compound-engineering/commands/ce/plan.md +636 -0
  25. package/plugins/compound-engineering/commands/ce/review.md +525 -0
  26. package/plugins/compound-engineering/commands/ce/work.md +470 -0
  27. package/plugins/compound-engineering/commands/create-agent-skill.md +1 -1
  28. package/plugins/compound-engineering/commands/deepen-plan.md +6 -6
  29. package/plugins/compound-engineering/commands/deploy-docs.md +1 -1
  30. package/plugins/compound-engineering/commands/feature-video.md +15 -6
  31. package/plugins/compound-engineering/commands/heal-skill.md +1 -1
  32. package/plugins/compound-engineering/commands/lfg.md +3 -3
  33. package/plugins/compound-engineering/commands/slfg.md +3 -3
  34. package/plugins/compound-engineering/commands/test-xcode.md +2 -2
  35. package/plugins/compound-engineering/commands/workflows/brainstorm.md +4 -123
  36. package/plugins/compound-engineering/commands/workflows/compound.md +4 -234
  37. package/plugins/compound-engineering/commands/workflows/plan.md +4 -562
  38. package/plugins/compound-engineering/commands/workflows/review.md +4 -522
  39. package/plugins/compound-engineering/commands/workflows/work.md +4 -448
  40. package/plugins/compound-engineering/skills/brainstorming/SKILL.md +3 -3
  41. package/plugins/compound-engineering/skills/document-review/SKILL.md +1 -1
  42. package/plugins/compound-engineering/skills/file-todos/SKILL.md +1 -1
  43. package/plugins/compound-engineering/skills/git-worktree/SKILL.md +5 -5
  44. package/plugins/compound-engineering/skills/proof/SKILL.md +185 -0
  45. package/plugins/compound-engineering/skills/resolve-pr-parallel/SKILL.md +1 -1
  46. package/plugins/compound-engineering/skills/setup/SKILL.md +2 -2
  47. package/src/commands/convert.ts +101 -24
  48. package/src/commands/install.ts +102 -45
  49. package/src/commands/sync.ts +58 -38
  50. package/src/converters/claude-to-openclaw.ts +240 -0
  51. package/src/converters/claude-to-opencode.ts +12 -10
  52. package/src/converters/claude-to-qwen.ts +238 -0
  53. package/src/converters/claude-to-windsurf.ts +205 -0
  54. package/src/sync/gemini.ts +76 -0
  55. package/src/targets/index.ts +60 -1
  56. package/src/targets/openclaw.ts +96 -0
  57. package/src/targets/opencode.ts +76 -10
  58. package/src/targets/qwen.ts +64 -0
  59. package/src/targets/windsurf.ts +104 -0
  60. package/src/types/openclaw.ts +52 -0
  61. package/src/types/opencode.ts +7 -8
  62. package/src/types/qwen.ts +48 -0
  63. package/src/types/windsurf.ts +34 -0
  64. package/src/utils/detect-tools.ts +46 -0
  65. package/src/utils/files.ts +7 -0
  66. package/src/utils/resolve-output.ts +50 -0
  67. package/src/utils/secrets.ts +24 -0
  68. package/tests/cli.test.ts +78 -0
  69. package/tests/converter.test.ts +43 -10
  70. package/tests/detect-tools.test.ts +96 -0
  71. package/tests/openclaw-converter.test.ts +200 -0
  72. package/tests/opencode-writer.test.ts +142 -5
  73. package/tests/qwen-converter.test.ts +238 -0
  74. package/tests/resolve-output.test.ts +131 -0
  75. package/tests/sync-gemini.test.ts +106 -0
  76. package/tests/windsurf-converter.test.ts +573 -0
  77. package/tests/windsurf-writer.test.ts +359 -0
  78. package/docs/css/docs.css +0 -675
  79. package/docs/css/style.css +0 -2886
  80. package/docs/index.html +0 -1046
  81. package/docs/js/main.js +0 -225
  82. package/docs/pages/agents.html +0 -649
  83. package/docs/pages/changelog.html +0 -534
  84. package/docs/pages/commands.html +0 -523
  85. package/docs/pages/getting-started.html +0 -582
  86. package/docs/pages/mcp-servers.html +0 -409
  87. package/docs/pages/skills.html +0 -611
@@ -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
- /** Check if any MCP servers have env vars that might contain secrets */
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 Copilot",
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
- required: true,
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,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
- OpenCodeCommandConfig,
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 commandMap = convertCommands(plugin.commands)
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
- function convertCommands(commands: ClaudeCommand[]): Record<string, OpenCodeCommandConfig> {
115
- const result: Record<string, OpenCodeCommandConfig> = {}
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 entry: OpenCodeCommandConfig = {
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
- entry.model = normalizeModel(command.model)
124
+ frontmatter.model = normalizeModel(command.model)
124
125
  }
125
- result[command.name] = entry
126
+ const content = formatFrontmatter(frontmatter, rewriteClaudePaths(command.body))
127
+ files.push({ name: command.name, content })
126
128
  }
127
- return result
129
+ return files
128
130
  }
129
131
 
130
132
  function convertMcp(servers: Record<string, ClaudeMcpServer>): Record<string, OpenCodeMcpServer> {
@@ -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
+ }