@every-env/compound-plugin 0.12.0 → 2.34.3

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 (55) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.github/workflows/publish.yml +20 -10
  3. package/.releaserc.json +36 -0
  4. package/AGENTS.md +1 -0
  5. package/CHANGELOG.md +39 -0
  6. package/CLAUDE.md +14 -0
  7. package/README.md +35 -2
  8. package/bun.lock +977 -0
  9. package/docs/plans/2026-03-01-fix-setup-skill-non-claude-llm-fallback-plan.md +140 -0
  10. package/docs/plans/2026-03-03-feat-sync-claude-mcp-all-supported-providers-plan.md +639 -0
  11. package/docs/solutions/adding-converter-target-providers.md +2 -1
  12. package/docs/solutions/plugin-versioning-requirements.md +4 -0
  13. package/package.json +10 -4
  14. package/plugins/compound-engineering/.claude-plugin/plugin.json +1 -1
  15. package/plugins/compound-engineering/CHANGELOG.md +10 -0
  16. package/plugins/compound-engineering/CLAUDE.md +5 -0
  17. package/plugins/compound-engineering/skills/create-agent-skills/workflows/add-workflow.md +6 -0
  18. package/plugins/compound-engineering/skills/create-agent-skills/workflows/create-new-skill.md +6 -0
  19. package/plugins/compound-engineering/skills/setup/SKILL.md +6 -0
  20. package/src/commands/sync.ts +21 -60
  21. package/src/index.ts +2 -1
  22. package/src/parsers/claude-home.ts +55 -3
  23. package/src/sync/codex.ts +38 -62
  24. package/src/sync/commands.ts +198 -0
  25. package/src/sync/copilot.ts +14 -36
  26. package/src/sync/droid.ts +50 -9
  27. package/src/sync/gemini.ts +87 -28
  28. package/src/sync/json-config.ts +47 -0
  29. package/src/sync/kiro.ts +49 -0
  30. package/src/sync/mcp-transports.ts +19 -0
  31. package/src/sync/openclaw.ts +18 -0
  32. package/src/sync/opencode.ts +10 -30
  33. package/src/sync/pi.ts +12 -36
  34. package/src/sync/qwen.ts +66 -0
  35. package/src/sync/registry.ts +141 -0
  36. package/src/sync/skills.ts +21 -0
  37. package/src/sync/windsurf.ts +59 -0
  38. package/src/types/kiro.ts +3 -1
  39. package/src/types/qwen.ts +3 -0
  40. package/src/types/windsurf.ts +1 -0
  41. package/src/utils/codex-agents.ts +1 -1
  42. package/src/utils/detect-tools.ts +4 -13
  43. package/src/utils/files.ts +7 -0
  44. package/src/utils/symlink.ts +4 -6
  45. package/tests/claude-home.test.ts +46 -0
  46. package/tests/cli.test.ts +102 -0
  47. package/tests/detect-tools.test.ts +30 -7
  48. package/tests/sync-codex.test.ts +64 -0
  49. package/tests/sync-copilot.test.ts +60 -4
  50. package/tests/sync-droid.test.ts +44 -4
  51. package/tests/sync-gemini.test.ts +54 -0
  52. package/tests/sync-kiro.test.ts +83 -0
  53. package/tests/sync-openclaw.test.ts +51 -0
  54. package/tests/sync-qwen.test.ts +75 -0
  55. package/tests/sync-windsurf.test.ts +89 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compound-engineering",
3
- "version": "2.38.0",
3
+ "version": "2.38.1",
4
4
  "description": "AI-powered development tools. 29 agents, 22 commands, 20 skills, 1 MCP server for code review, research, design, and workflow automation.",
5
5
  "author": {
6
6
  "name": "Kieran Klaassen",
@@ -5,6 +5,16 @@ All notable changes to the compound-engineering plugin will be documented in thi
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.38.1] - 2026-03-01
9
+
10
+ ### Fixed
11
+
12
+ - **Cross-platform `AskUserQuestion` fallback** — `setup` skill and `create-new-skill`/`add-workflow` workflows now include an "Interaction Method" preamble that instructs non-Claude LLMs (Codex, Gemini, Copilot, Kiro) to use numbered lists instead of `AskUserQuestion`, preventing silent auto-configuration. ([#204](https://github.com/EveryInc/compound-engineering-plugin/issues/204))
13
+ - **Codex AGENTS.md `AskUserQuestion` mapping** — Strengthened from "ask the user in chat" to structured numbered-list guidance with multi-select support and a "never skip or auto-configure" rule.
14
+ - **Skill compliance checklist** — Added `AskUserQuestion` lint rule to `CLAUDE.md` to prevent recurrence.
15
+
16
+ ---
17
+
8
18
  ## [2.38.0] - 2026-03-01
9
19
 
10
20
  ### Changed
@@ -75,6 +75,11 @@ When adding or modifying skills, verify compliance with skill-creator spec:
75
75
  - [ ] Use imperative/infinitive form (verb-first instructions)
76
76
  - [ ] Avoid second person ("you should") - use objective language ("To accomplish X, do Y")
77
77
 
78
+ ### AskUserQuestion Usage
79
+
80
+ - [ ] If the skill uses `AskUserQuestion`, it must include an "Interaction Method" preamble explaining the numbered-list fallback for non-Claude environments
81
+ - [ ] Prefer avoiding `AskUserQuestion` entirely (see `brainstorming/SKILL.md` pattern) for skills intended to run cross-platform
82
+
78
83
  ### Quick Validation Command
79
84
 
80
85
  ```bash
@@ -1,5 +1,11 @@
1
1
  # Workflow: Add a Workflow to Existing Skill
2
2
 
3
+ ## Interaction Method
4
+
5
+ If `AskUserQuestion` is available, use it for all prompts below.
6
+
7
+ If not, present each question as a numbered list and wait for a reply before proceeding to the next step. Never skip or auto-configure.
8
+
3
9
  <required_reading>
4
10
  **Read these reference files NOW:**
5
11
  1. references/recommended-structure.md
@@ -1,5 +1,11 @@
1
1
  # Workflow: Create a New Skill
2
2
 
3
+ ## Interaction Method
4
+
5
+ If `AskUserQuestion` is available, use it for all prompts below.
6
+
7
+ If not, present each question as a numbered list and wait for a reply before proceeding to the next step. For multiSelect questions, accept comma-separated numbers (e.g. `1, 3`). Never skip or auto-configure.
8
+
3
9
  <required_reading>
4
10
  **Read these reference files NOW:**
5
11
  1. references/recommended-structure.md
@@ -6,6 +6,12 @@ disable-model-invocation: true
6
6
 
7
7
  # Compound Engineering Setup
8
8
 
9
+ ## Interaction Method
10
+
11
+ If `AskUserQuestion` is available, use it for all prompts below.
12
+
13
+ If not, present each question as a numbered list and wait for a reply before proceeding to the next step. For multiSelect questions, accept comma-separated numbers (e.g. `1, 3`). Never skip or auto-configure.
14
+
9
15
  Interactive setup for `compound-engineering.local.md` — configures which agents run during `/ce:review` and `/ce:work`.
10
16
 
11
17
  ## Step 1: Check Existing Config
@@ -1,76 +1,34 @@
1
1
  import { defineCommand } from "citty"
2
- import os from "os"
3
2
  import path from "path"
4
3
  import { loadClaudeHome } from "../parsers/claude-home"
5
- import { syncToOpenCode } from "../sync/opencode"
6
- import { syncToCodex } from "../sync/codex"
7
- import { syncToPi } from "../sync/pi"
8
- import { syncToDroid } from "../sync/droid"
9
- import { syncToCopilot } from "../sync/copilot"
10
- import { syncToGemini } from "../sync/gemini"
4
+ import {
5
+ getDefaultSyncRegistryContext,
6
+ getSyncTarget,
7
+ isSyncTargetName,
8
+ syncTargetNames,
9
+ type SyncTargetName,
10
+ } from "../sync/registry"
11
11
  import { expandHome } from "../utils/resolve-home"
12
12
  import { hasPotentialSecrets } from "../utils/secrets"
13
13
  import { detectInstalledTools } from "../utils/detect-tools"
14
14
 
15
- const validTargets = ["opencode", "codex", "pi", "droid", "copilot", "gemini", "all"] as const
16
- type SyncTarget = (typeof validTargets)[number]
15
+ const validTargets = [...syncTargetNames, "all"] as const
16
+ type SyncTarget = SyncTargetName | "all"
17
17
 
18
18
  function isValidTarget(value: string): value is SyncTarget {
19
- return (validTargets as readonly string[]).includes(value)
20
- }
21
-
22
- function resolveOutputRoot(target: string): string {
23
- switch (target) {
24
- case "opencode":
25
- return path.join(os.homedir(), ".config", "opencode")
26
- case "codex":
27
- return path.join(os.homedir(), ".codex")
28
- case "pi":
29
- return path.join(os.homedir(), ".pi", "agent")
30
- case "droid":
31
- return path.join(os.homedir(), ".factory")
32
- case "copilot":
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
61
- }
19
+ return value === "all" || isSyncTargetName(value)
62
20
  }
63
21
 
64
22
  export default defineCommand({
65
23
  meta: {
66
24
  name: "sync",
67
- description: "Sync Claude Code config (~/.claude/) to OpenCode, Codex, Pi, Droid, Copilot, or Gemini",
25
+ description: "Sync Claude Code config (~/.claude/) to supported provider configs and skills",
68
26
  },
69
27
  args: {
70
28
  target: {
71
29
  type: "string",
72
30
  default: "all",
73
- description: "Target: opencode | codex | pi | droid | copilot | gemini | all (default: all)",
31
+ description: `Target: ${syncTargetNames.join(" | ")} | all (default: all)`,
74
32
  },
75
33
  claudeHome: {
76
34
  type: "string",
@@ -83,7 +41,8 @@ export default defineCommand({
83
41
  throw new Error(`Unknown target: ${args.target}. Use one of: ${validTargets.join(", ")}`)
84
42
  }
85
43
 
86
- const claudeHome = expandHome(args.claudeHome ?? path.join(os.homedir(), ".claude"))
44
+ const { home, cwd } = getDefaultSyncRegistryContext()
45
+ const claudeHome = expandHome(args.claudeHome ?? path.join(home, ".claude"))
87
46
  const config = await loadClaudeHome(claudeHome)
88
47
 
89
48
  // Warn about potential secrets in MCP env vars
@@ -109,19 +68,21 @@ export default defineCommand({
109
68
  }
110
69
 
111
70
  for (const name of activeTargets) {
112
- const outputRoot = resolveOutputRoot(name)
113
- await syncTarget(name, config, outputRoot)
71
+ const target = getSyncTarget(name as SyncTargetName)
72
+ const outputRoot = target.resolveOutputRoot(home, cwd)
73
+ await target.sync(config, outputRoot)
114
74
  console.log(`✓ Synced to ${name}: ${outputRoot}`)
115
75
  }
116
76
  return
117
77
  }
118
78
 
119
79
  console.log(
120
- `Syncing ${config.skills.length} skills, ${Object.keys(config.mcpServers).length} MCP servers...`,
80
+ `Syncing ${config.skills.length} skills, ${config.commands?.length ?? 0} commands, ${Object.keys(config.mcpServers).length} MCP servers...`,
121
81
  )
122
82
 
123
- const outputRoot = resolveOutputRoot(args.target)
124
- await syncTarget(args.target, config, outputRoot)
83
+ const target = getSyncTarget(args.target as SyncTargetName)
84
+ const outputRoot = target.resolveOutputRoot(home, cwd)
85
+ await target.sync(config, outputRoot)
125
86
  console.log(`✓ Synced to ${args.target}: ${outputRoot}`)
126
87
  },
127
88
  })
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
+ }