@every-env/compound-plugin 0.9.0 → 2.34.2

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 (121) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.github/workflows/publish.yml +20 -10
  3. package/.releaserc.json +31 -0
  4. package/AGENTS.md +6 -1
  5. package/CHANGELOG.md +76 -0
  6. package/CLAUDE.md +16 -3
  7. package/README.md +83 -16
  8. package/bun.lock +977 -0
  9. package/docs/plans/2026-02-14-feat-auto-detect-install-and-gemini-sync-plan.md +360 -0
  10. package/docs/plans/2026-02-25-feat-windsurf-global-scope-support-plan.md +627 -0
  11. package/docs/plans/2026-03-01-feat-ce-command-aliases-backwards-compatible-deprecation-plan.md +261 -0
  12. package/docs/plans/2026-03-01-fix-setup-skill-non-claude-llm-fallback-plan.md +140 -0
  13. package/docs/plans/2026-03-03-feat-sync-claude-mcp-all-supported-providers-plan.md +639 -0
  14. package/docs/plans/feature_opencode-commands-as-md-and-config-merge.md +574 -0
  15. package/docs/solutions/adding-converter-target-providers.md +693 -0
  16. package/docs/solutions/plugin-versioning-requirements.md +7 -3
  17. package/docs/specs/windsurf.md +477 -0
  18. package/package.json +10 -4
  19. package/plans/landing-page-launchkit-refresh.md +2 -2
  20. package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
  21. package/plugins/compound-engineering/CHANGELOG.md +82 -1
  22. package/plugins/compound-engineering/CLAUDE.md +14 -7
  23. package/plugins/compound-engineering/README.md +10 -7
  24. package/plugins/compound-engineering/agents/research/git-history-analyzer.md +1 -1
  25. package/plugins/compound-engineering/agents/research/learnings-researcher.md +1 -1
  26. package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +1 -1
  27. package/plugins/compound-engineering/commands/ce/brainstorm.md +145 -0
  28. package/plugins/compound-engineering/commands/ce/compound.md +240 -0
  29. package/plugins/compound-engineering/commands/ce/plan.md +636 -0
  30. package/plugins/compound-engineering/commands/ce/review.md +525 -0
  31. package/plugins/compound-engineering/commands/ce/work.md +470 -0
  32. package/plugins/compound-engineering/commands/create-agent-skill.md +1 -1
  33. package/plugins/compound-engineering/commands/deepen-plan.md +6 -6
  34. package/plugins/compound-engineering/commands/deploy-docs.md +1 -1
  35. package/plugins/compound-engineering/commands/feature-video.md +15 -6
  36. package/plugins/compound-engineering/commands/heal-skill.md +1 -1
  37. package/plugins/compound-engineering/commands/lfg.md +3 -3
  38. package/plugins/compound-engineering/commands/slfg.md +3 -3
  39. package/plugins/compound-engineering/commands/test-xcode.md +2 -2
  40. package/plugins/compound-engineering/commands/workflows/brainstorm.md +4 -123
  41. package/plugins/compound-engineering/commands/workflows/compound.md +4 -234
  42. package/plugins/compound-engineering/commands/workflows/plan.md +4 -562
  43. package/plugins/compound-engineering/commands/workflows/review.md +4 -522
  44. package/plugins/compound-engineering/commands/workflows/work.md +4 -448
  45. package/plugins/compound-engineering/skills/brainstorming/SKILL.md +3 -3
  46. package/plugins/compound-engineering/skills/create-agent-skills/workflows/add-workflow.md +6 -0
  47. package/plugins/compound-engineering/skills/create-agent-skills/workflows/create-new-skill.md +6 -0
  48. package/plugins/compound-engineering/skills/document-review/SKILL.md +1 -1
  49. package/plugins/compound-engineering/skills/file-todos/SKILL.md +1 -1
  50. package/plugins/compound-engineering/skills/git-worktree/SKILL.md +5 -5
  51. package/plugins/compound-engineering/skills/proof/SKILL.md +185 -0
  52. package/plugins/compound-engineering/skills/resolve-pr-parallel/SKILL.md +1 -1
  53. package/plugins/compound-engineering/skills/setup/SKILL.md +8 -2
  54. package/src/commands/convert.ts +101 -24
  55. package/src/commands/install.ts +102 -45
  56. package/src/commands/sync.ts +43 -62
  57. package/src/converters/claude-to-openclaw.ts +240 -0
  58. package/src/converters/claude-to-opencode.ts +12 -10
  59. package/src/converters/claude-to-qwen.ts +238 -0
  60. package/src/converters/claude-to-windsurf.ts +205 -0
  61. package/src/index.ts +2 -1
  62. package/src/parsers/claude-home.ts +55 -3
  63. package/src/sync/codex.ts +38 -62
  64. package/src/sync/commands.ts +198 -0
  65. package/src/sync/copilot.ts +14 -36
  66. package/src/sync/droid.ts +50 -9
  67. package/src/sync/gemini.ts +135 -0
  68. package/src/sync/json-config.ts +47 -0
  69. package/src/sync/kiro.ts +49 -0
  70. package/src/sync/mcp-transports.ts +19 -0
  71. package/src/sync/openclaw.ts +18 -0
  72. package/src/sync/opencode.ts +10 -30
  73. package/src/sync/pi.ts +12 -36
  74. package/src/sync/qwen.ts +66 -0
  75. package/src/sync/registry.ts +141 -0
  76. package/src/sync/skills.ts +21 -0
  77. package/src/sync/windsurf.ts +59 -0
  78. package/src/targets/index.ts +60 -1
  79. package/src/targets/openclaw.ts +96 -0
  80. package/src/targets/opencode.ts +76 -10
  81. package/src/targets/qwen.ts +64 -0
  82. package/src/targets/windsurf.ts +104 -0
  83. package/src/types/kiro.ts +3 -1
  84. package/src/types/openclaw.ts +52 -0
  85. package/src/types/opencode.ts +7 -8
  86. package/src/types/qwen.ts +51 -0
  87. package/src/types/windsurf.ts +35 -0
  88. package/src/utils/codex-agents.ts +1 -1
  89. package/src/utils/detect-tools.ts +37 -0
  90. package/src/utils/files.ts +14 -0
  91. package/src/utils/resolve-output.ts +50 -0
  92. package/src/utils/secrets.ts +24 -0
  93. package/src/utils/symlink.ts +4 -6
  94. package/tests/claude-home.test.ts +46 -0
  95. package/tests/cli.test.ts +180 -0
  96. package/tests/converter.test.ts +43 -10
  97. package/tests/detect-tools.test.ts +119 -0
  98. package/tests/openclaw-converter.test.ts +200 -0
  99. package/tests/opencode-writer.test.ts +142 -5
  100. package/tests/qwen-converter.test.ts +238 -0
  101. package/tests/resolve-output.test.ts +131 -0
  102. package/tests/sync-codex.test.ts +64 -0
  103. package/tests/sync-copilot.test.ts +60 -4
  104. package/tests/sync-droid.test.ts +44 -4
  105. package/tests/sync-gemini.test.ts +160 -0
  106. package/tests/sync-kiro.test.ts +83 -0
  107. package/tests/sync-openclaw.test.ts +51 -0
  108. package/tests/sync-qwen.test.ts +75 -0
  109. package/tests/sync-windsurf.test.ts +89 -0
  110. package/tests/windsurf-converter.test.ts +573 -0
  111. package/tests/windsurf-writer.test.ts +359 -0
  112. package/docs/css/docs.css +0 -675
  113. package/docs/css/style.css +0 -2886
  114. package/docs/index.html +0 -1046
  115. package/docs/js/main.js +0 -225
  116. package/docs/pages/agents.html +0 -649
  117. package/docs/pages/changelog.html +0 -534
  118. package/docs/pages/commands.html +0 -523
  119. package/docs/pages/getting-started.html +0 -582
  120. package/docs/pages/mcp-servers.html +0 -409
  121. package/docs/pages/skills.html +0 -611
@@ -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
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { defineCommand, runMain } from "citty"
3
+ import packageJson from "../package.json"
3
4
  import convert from "./commands/convert"
4
5
  import install from "./commands/install"
5
6
  import listCommand from "./commands/list"
@@ -8,7 +9,7 @@ import sync from "./commands/sync"
8
9
  const main = defineCommand({
9
10
  meta: {
10
11
  name: "compound-plugin",
11
- version: "0.1.0",
12
+ version: packageJson.version,
12
13
  description: "Convert Claude Code plugins into other agent formats",
13
14
  },
14
15
  subCommands: {
@@ -1,22 +1,26 @@
1
1
  import path from "path"
2
2
  import os from "os"
3
3
  import fs from "fs/promises"
4
- import type { ClaudeSkill, ClaudeMcpServer } from "../types/claude"
4
+ import { parseFrontmatter } from "../utils/frontmatter"
5
+ import { walkFiles } from "../utils/files"
6
+ import type { ClaudeCommand, ClaudeSkill, ClaudeMcpServer } from "../types/claude"
5
7
 
6
8
  export interface ClaudeHomeConfig {
7
9
  skills: ClaudeSkill[]
10
+ commands?: ClaudeCommand[]
8
11
  mcpServers: Record<string, ClaudeMcpServer>
9
12
  }
10
13
 
11
14
  export async function loadClaudeHome(claudeHome?: string): Promise<ClaudeHomeConfig> {
12
15
  const home = claudeHome ?? path.join(os.homedir(), ".claude")
13
16
 
14
- const [skills, mcpServers] = await Promise.all([
17
+ const [skills, commands, mcpServers] = await Promise.all([
15
18
  loadPersonalSkills(path.join(home, "skills")),
19
+ loadPersonalCommands(path.join(home, "commands")),
16
20
  loadSettingsMcp(path.join(home, "settings.json")),
17
21
  ])
18
22
 
19
- return { skills, mcpServers }
23
+ return { skills, commands, mcpServers }
20
24
  }
21
25
 
22
26
  async function loadPersonalSkills(skillsDir: string): Promise<ClaudeSkill[]> {
@@ -63,3 +67,51 @@ async function loadSettingsMcp(
63
67
  return {} // File doesn't exist or invalid JSON
64
68
  }
65
69
  }
70
+
71
+ async function loadPersonalCommands(commandsDir: string): Promise<ClaudeCommand[]> {
72
+ try {
73
+ const files = (await walkFiles(commandsDir))
74
+ .filter((file) => file.endsWith(".md"))
75
+ .sort()
76
+
77
+ const commands: ClaudeCommand[] = []
78
+ for (const file of files) {
79
+ const raw = await fs.readFile(file, "utf8")
80
+ const { data, body } = parseFrontmatter(raw)
81
+ commands.push({
82
+ name: typeof data.name === "string" ? data.name : deriveCommandName(commandsDir, file),
83
+ description: data.description as string | undefined,
84
+ argumentHint: data["argument-hint"] as string | undefined,
85
+ model: data.model as string | undefined,
86
+ allowedTools: parseAllowedTools(data["allowed-tools"]),
87
+ disableModelInvocation: data["disable-model-invocation"] === true ? true : undefined,
88
+ body: body.trim(),
89
+ sourcePath: file,
90
+ })
91
+ }
92
+
93
+ return commands
94
+ } catch {
95
+ return []
96
+ }
97
+ }
98
+
99
+ function deriveCommandName(commandsDir: string, filePath: string): string {
100
+ const relative = path.relative(commandsDir, filePath)
101
+ const withoutExt = relative.replace(/\.md$/i, "")
102
+ return withoutExt.split(path.sep).join(":")
103
+ }
104
+
105
+ function parseAllowedTools(value: unknown): string[] | undefined {
106
+ if (!value) return undefined
107
+ if (Array.isArray(value)) {
108
+ return value.map((item) => String(item))
109
+ }
110
+ if (typeof value === "string") {
111
+ return value
112
+ .split(/,/)
113
+ .map((item) => item.trim())
114
+ .filter(Boolean)
115
+ }
116
+ return undefined
117
+ }
package/src/sync/codex.ts CHANGED
@@ -1,31 +1,29 @@
1
1
  import fs from "fs/promises"
2
2
  import path from "path"
3
3
  import type { ClaudeHomeConfig } from "../parsers/claude-home"
4
- import type { ClaudeMcpServer } from "../types/claude"
5
- import { forceSymlink, isValidSkillName } from "../utils/symlink"
4
+ import { renderCodexConfig } from "../targets/codex"
5
+ import { writeTextSecure } from "../utils/files"
6
+ import { syncCodexCommands } from "./commands"
7
+ import { syncSkills } from "./skills"
8
+
9
+ const CURRENT_START_MARKER = "# BEGIN compound-plugin Claude Code MCP"
10
+ const CURRENT_END_MARKER = "# END compound-plugin Claude Code MCP"
11
+ const LEGACY_MARKER = "# MCP servers synced from Claude Code"
6
12
 
7
13
  export async function syncToCodex(
8
14
  config: ClaudeHomeConfig,
9
15
  outputRoot: string,
10
16
  ): Promise<void> {
11
- // Ensure output directories exist
12
- const skillsDir = path.join(outputRoot, "skills")
13
- await fs.mkdir(skillsDir, { recursive: true })
14
-
15
- // Symlink skills (with validation)
16
- for (const skill of config.skills) {
17
- if (!isValidSkillName(skill.name)) {
18
- console.warn(`Skipping skill with invalid name: ${skill.name}`)
19
- continue
20
- }
21
- const target = path.join(skillsDir, skill.name)
22
- await forceSymlink(skill.sourceDir, target)
23
- }
17
+ await syncSkills(config.skills, path.join(outputRoot, "skills"))
18
+ await syncCodexCommands(config, outputRoot)
24
19
 
25
20
  // Write MCP servers to config.toml (TOML format)
26
21
  if (Object.keys(config.mcpServers).length > 0) {
27
22
  const configPath = path.join(outputRoot, "config.toml")
28
- const mcpToml = convertMcpForCodex(config.mcpServers)
23
+ const mcpToml = renderCodexConfig(config.mcpServers)
24
+ if (!mcpToml) {
25
+ return
26
+ }
29
27
 
30
28
  // Read existing config and merge idempotently
31
29
  let existingContent = ""
@@ -37,56 +35,34 @@ export async function syncToCodex(
37
35
  }
38
36
  }
39
37
 
40
- // Remove any existing Claude Code MCP section to make idempotent
41
- const marker = "# MCP servers synced from Claude Code"
42
- const markerIndex = existingContent.indexOf(marker)
43
- if (markerIndex !== -1) {
44
- existingContent = existingContent.slice(0, markerIndex).trimEnd()
45
- }
46
-
47
- const newContent = existingContent
48
- ? existingContent + "\n\n" + marker + "\n" + mcpToml
49
- : "# Codex config - synced from Claude Code\n\n" + mcpToml
50
-
51
- await fs.writeFile(configPath, newContent, { mode: 0o600 })
52
- }
53
- }
54
-
55
- /** Escape a string for TOML double-quoted strings */
56
- function escapeTomlString(str: string): string {
57
- return str
58
- .replace(/\\/g, "\\\\")
59
- .replace(/"/g, '\\"')
60
- .replace(/\n/g, "\\n")
61
- .replace(/\r/g, "\\r")
62
- .replace(/\t/g, "\\t")
63
- }
38
+ const managedBlock = [
39
+ CURRENT_START_MARKER,
40
+ mcpToml.trim(),
41
+ CURRENT_END_MARKER,
42
+ "",
43
+ ].join("\n")
64
44
 
65
- function convertMcpForCodex(servers: Record<string, ClaudeMcpServer>): string {
66
- const sections: string[] = []
45
+ const withoutCurrentBlock = existingContent.replace(
46
+ new RegExp(
47
+ `${escapeForRegex(CURRENT_START_MARKER)}[\\s\\S]*?${escapeForRegex(CURRENT_END_MARKER)}\\n?`,
48
+ "g",
49
+ ),
50
+ "",
51
+ ).trimEnd()
67
52
 
68
- for (const [name, server] of Object.entries(servers)) {
69
- if (!server.command) continue
53
+ const legacyMarkerIndex = withoutCurrentBlock.indexOf(LEGACY_MARKER)
54
+ const cleaned = legacyMarkerIndex === -1
55
+ ? withoutCurrentBlock
56
+ : withoutCurrentBlock.slice(0, legacyMarkerIndex).trimEnd()
70
57
 
71
- const lines: string[] = []
72
- lines.push(`[mcp_servers.${name}]`)
73
- lines.push(`command = "${escapeTomlString(server.command)}"`)
74
-
75
- if (server.args && server.args.length > 0) {
76
- const argsStr = server.args.map((arg) => `"${escapeTomlString(arg)}"`).join(", ")
77
- lines.push(`args = [${argsStr}]`)
78
- }
58
+ const newContent = cleaned
59
+ ? `${cleaned}\n\n${managedBlock}`
60
+ : `${managedBlock}`
79
61
 
80
- if (server.env && Object.keys(server.env).length > 0) {
81
- lines.push("")
82
- lines.push(`[mcp_servers.${name}.env]`)
83
- for (const [key, value] of Object.entries(server.env)) {
84
- lines.push(`${key} = "${escapeTomlString(value)}"`)
85
- }
86
- }
87
-
88
- sections.push(lines.join("\n"))
62
+ await writeTextSecure(configPath, newContent)
89
63
  }
64
+ }
90
65
 
91
- return sections.join("\n\n") + "\n"
66
+ function escapeForRegex(value: string): string {
67
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
92
68
  }
@@ -0,0 +1,198 @@
1
+ import path from "path"
2
+ import type { ClaudeHomeConfig } from "../parsers/claude-home"
3
+ import type { ClaudePlugin } from "../types/claude"
4
+ import { backupFile, writeText } from "../utils/files"
5
+ import { convertClaudeToCodex } from "../converters/claude-to-codex"
6
+ import { convertClaudeToCopilot } from "../converters/claude-to-copilot"
7
+ import { convertClaudeToDroid } from "../converters/claude-to-droid"
8
+ import { convertClaudeToGemini } from "../converters/claude-to-gemini"
9
+ import { convertClaudeToKiro } from "../converters/claude-to-kiro"
10
+ import { convertClaudeToOpenCode, type ClaudeToOpenCodeOptions } from "../converters/claude-to-opencode"
11
+ import { convertClaudeToPi } from "../converters/claude-to-pi"
12
+ import { convertClaudeToQwen, type ClaudeToQwenOptions } from "../converters/claude-to-qwen"
13
+ import { convertClaudeToWindsurf } from "../converters/claude-to-windsurf"
14
+ import { writeWindsurfBundle } from "../targets/windsurf"
15
+
16
+ type WindsurfSyncScope = "global" | "workspace"
17
+
18
+ const HOME_SYNC_PLUGIN_ROOT = path.join(process.cwd(), ".compound-sync-home")
19
+
20
+ const DEFAULT_SYNC_OPTIONS: ClaudeToOpenCodeOptions = {
21
+ agentMode: "subagent",
22
+ inferTemperature: false,
23
+ permissions: "none",
24
+ }
25
+
26
+ const DEFAULT_QWEN_SYNC_OPTIONS: ClaudeToQwenOptions = {
27
+ agentMode: "subagent",
28
+ inferTemperature: false,
29
+ }
30
+
31
+ function hasCommands(config: ClaudeHomeConfig): boolean {
32
+ return (config.commands?.length ?? 0) > 0
33
+ }
34
+
35
+ function buildClaudeHomePlugin(config: ClaudeHomeConfig): ClaudePlugin {
36
+ return {
37
+ root: HOME_SYNC_PLUGIN_ROOT,
38
+ manifest: {
39
+ name: "claude-home",
40
+ version: "1.0.0",
41
+ description: "Personal Claude Code home config",
42
+ },
43
+ agents: [],
44
+ commands: config.commands ?? [],
45
+ skills: config.skills,
46
+ mcpServers: undefined,
47
+ }
48
+ }
49
+
50
+ export async function syncOpenCodeCommands(
51
+ config: ClaudeHomeConfig,
52
+ outputRoot: string,
53
+ ): Promise<void> {
54
+ if (!hasCommands(config)) return
55
+
56
+ const plugin = buildClaudeHomePlugin(config)
57
+ const bundle = convertClaudeToOpenCode(plugin, DEFAULT_SYNC_OPTIONS)
58
+
59
+ for (const commandFile of bundle.commandFiles) {
60
+ const commandPath = path.join(outputRoot, "commands", `${commandFile.name}.md`)
61
+ const backupPath = await backupFile(commandPath)
62
+ if (backupPath) {
63
+ console.log(`Backed up existing command file to ${backupPath}`)
64
+ }
65
+ await writeText(commandPath, commandFile.content + "\n")
66
+ }
67
+ }
68
+
69
+ export async function syncCodexCommands(
70
+ config: ClaudeHomeConfig,
71
+ outputRoot: string,
72
+ ): Promise<void> {
73
+ if (!hasCommands(config)) return
74
+
75
+ const plugin = buildClaudeHomePlugin(config)
76
+ const bundle = convertClaudeToCodex(plugin, DEFAULT_SYNC_OPTIONS)
77
+ for (const prompt of bundle.prompts) {
78
+ await writeText(path.join(outputRoot, "prompts", `${prompt.name}.md`), prompt.content + "\n")
79
+ }
80
+ for (const skill of bundle.generatedSkills) {
81
+ await writeText(path.join(outputRoot, "skills", skill.name, "SKILL.md"), skill.content + "\n")
82
+ }
83
+ }
84
+
85
+ export async function syncPiCommands(
86
+ config: ClaudeHomeConfig,
87
+ outputRoot: string,
88
+ ): Promise<void> {
89
+ if (!hasCommands(config)) return
90
+
91
+ const plugin = buildClaudeHomePlugin(config)
92
+ const bundle = convertClaudeToPi(plugin, DEFAULT_SYNC_OPTIONS)
93
+ for (const prompt of bundle.prompts) {
94
+ await writeText(path.join(outputRoot, "prompts", `${prompt.name}.md`), prompt.content + "\n")
95
+ }
96
+ for (const extension of bundle.extensions) {
97
+ await writeText(path.join(outputRoot, "extensions", extension.name), extension.content + "\n")
98
+ }
99
+ }
100
+
101
+ export async function syncDroidCommands(
102
+ config: ClaudeHomeConfig,
103
+ outputRoot: string,
104
+ ): Promise<void> {
105
+ if (!hasCommands(config)) return
106
+
107
+ const plugin = buildClaudeHomePlugin(config)
108
+ const bundle = convertClaudeToDroid(plugin, DEFAULT_SYNC_OPTIONS)
109
+ for (const command of bundle.commands) {
110
+ await writeText(path.join(outputRoot, "commands", `${command.name}.md`), command.content + "\n")
111
+ }
112
+ }
113
+
114
+ export async function syncCopilotCommands(
115
+ config: ClaudeHomeConfig,
116
+ outputRoot: string,
117
+ ): Promise<void> {
118
+ if (!hasCommands(config)) return
119
+
120
+ const plugin = buildClaudeHomePlugin(config)
121
+ const bundle = convertClaudeToCopilot(plugin, DEFAULT_SYNC_OPTIONS)
122
+
123
+ for (const skill of bundle.generatedSkills) {
124
+ await writeText(path.join(outputRoot, "skills", skill.name, "SKILL.md"), skill.content + "\n")
125
+ }
126
+ }
127
+
128
+ export async function syncGeminiCommands(
129
+ config: ClaudeHomeConfig,
130
+ outputRoot: string,
131
+ ): Promise<void> {
132
+ if (!hasCommands(config)) return
133
+
134
+ const plugin = buildClaudeHomePlugin(config)
135
+ const bundle = convertClaudeToGemini(plugin, DEFAULT_SYNC_OPTIONS)
136
+ for (const command of bundle.commands) {
137
+ await writeText(path.join(outputRoot, "commands", `${command.name}.toml`), command.content + "\n")
138
+ }
139
+ }
140
+
141
+ export async function syncKiroCommands(
142
+ config: ClaudeHomeConfig,
143
+ outputRoot: string,
144
+ ): Promise<void> {
145
+ if (!hasCommands(config)) return
146
+
147
+ const plugin = buildClaudeHomePlugin(config)
148
+ const bundle = convertClaudeToKiro(plugin, DEFAULT_SYNC_OPTIONS)
149
+ for (const skill of bundle.generatedSkills) {
150
+ await writeText(path.join(outputRoot, "skills", skill.name, "SKILL.md"), skill.content + "\n")
151
+ }
152
+ }
153
+
154
+ export async function syncWindsurfCommands(
155
+ config: ClaudeHomeConfig,
156
+ outputRoot: string,
157
+ scope: WindsurfSyncScope = "global",
158
+ ): Promise<void> {
159
+ if (!hasCommands(config)) return
160
+
161
+ const plugin = buildClaudeHomePlugin(config)
162
+ const bundle = convertClaudeToWindsurf(plugin, DEFAULT_SYNC_OPTIONS)
163
+ await writeWindsurfBundle(outputRoot, {
164
+ agentSkills: [],
165
+ commandWorkflows: bundle.commandWorkflows,
166
+ skillDirs: [],
167
+ mcpConfig: null,
168
+ }, scope)
169
+ }
170
+
171
+ export async function syncQwenCommands(
172
+ config: ClaudeHomeConfig,
173
+ outputRoot: string,
174
+ ): Promise<void> {
175
+ if (!hasCommands(config)) return
176
+
177
+ const plugin = buildClaudeHomePlugin(config)
178
+ const bundle = convertClaudeToQwen(plugin, DEFAULT_QWEN_SYNC_OPTIONS)
179
+
180
+ for (const commandFile of bundle.commandFiles) {
181
+ const parts = commandFile.name.split(":")
182
+ if (parts.length > 1) {
183
+ const nestedDir = path.join(outputRoot, "commands", ...parts.slice(0, -1))
184
+ await writeText(path.join(nestedDir, `${parts[parts.length - 1]}.md`), commandFile.content + "\n")
185
+ continue
186
+ }
187
+
188
+ await writeText(path.join(outputRoot, "commands", `${commandFile.name}.md`), commandFile.content + "\n")
189
+ }
190
+ }
191
+
192
+ export function warnUnsupportedOpenClawCommands(config: ClaudeHomeConfig): void {
193
+ if (!hasCommands(config)) return
194
+
195
+ console.warn(
196
+ "Warning: OpenClaw personal command sync is skipped because this sync target currently has no documented user-level command surface.",
197
+ )
198
+ }