@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
@@ -1,11 +1,13 @@
1
- import fs from "fs/promises"
2
1
  import path from "path"
3
2
  import type { ClaudeHomeConfig } from "../parsers/claude-home"
4
3
  import type { ClaudeMcpServer } from "../types/claude"
5
- import { forceSymlink, isValidSkillName } from "../utils/symlink"
4
+ import { syncCopilotCommands } from "./commands"
5
+ import { mergeJsonConfigAtKey } from "./json-config"
6
+ import { hasExplicitSseTransport } from "./mcp-transports"
7
+ import { syncSkills } from "./skills"
6
8
 
7
9
  type CopilotMcpServer = {
8
- type: string
10
+ type: "local" | "http" | "sse"
9
11
  command?: string
10
12
  args?: string[]
11
13
  url?: string
@@ -22,41 +24,17 @@ export async function syncToCopilot(
22
24
  config: ClaudeHomeConfig,
23
25
  outputRoot: string,
24
26
  ): Promise<void> {
25
- const skillsDir = path.join(outputRoot, "skills")
26
- await fs.mkdir(skillsDir, { recursive: true })
27
-
28
- for (const skill of config.skills) {
29
- if (!isValidSkillName(skill.name)) {
30
- console.warn(`Skipping skill with invalid name: ${skill.name}`)
31
- continue
32
- }
33
- const target = path.join(skillsDir, skill.name)
34
- await forceSymlink(skill.sourceDir, target)
35
- }
27
+ await syncSkills(config.skills, path.join(outputRoot, "skills"))
28
+ await syncCopilotCommands(config, outputRoot)
36
29
 
37
30
  if (Object.keys(config.mcpServers).length > 0) {
38
- const mcpPath = path.join(outputRoot, "copilot-mcp-config.json")
39
- const existing = await readJsonSafe(mcpPath)
31
+ const mcpPath = path.join(outputRoot, "mcp-config.json")
40
32
  const converted = convertMcpForCopilot(config.mcpServers)
41
- const merged: CopilotMcpConfig = {
42
- mcpServers: {
43
- ...(existing.mcpServers ?? {}),
44
- ...converted,
45
- },
46
- }
47
- await fs.writeFile(mcpPath, JSON.stringify(merged, null, 2), { mode: 0o600 })
48
- }
49
- }
50
-
51
- async function readJsonSafe(filePath: string): Promise<Partial<CopilotMcpConfig>> {
52
- try {
53
- const content = await fs.readFile(filePath, "utf-8")
54
- return JSON.parse(content) as Partial<CopilotMcpConfig>
55
- } catch (err) {
56
- if ((err as NodeJS.ErrnoException).code === "ENOENT") {
57
- return {}
58
- }
59
- throw err
33
+ await mergeJsonConfigAtKey({
34
+ configPath: mcpPath,
35
+ key: "mcpServers",
36
+ incoming: converted,
37
+ })
60
38
  }
61
39
  }
62
40
 
@@ -66,7 +44,7 @@ function convertMcpForCopilot(
66
44
  const result: Record<string, CopilotMcpServer> = {}
67
45
  for (const [name, server] of Object.entries(servers)) {
68
46
  const entry: CopilotMcpServer = {
69
- type: server.command ? "local" : "sse",
47
+ type: server.command ? "local" : hasExplicitSseTransport(server) ? "sse" : "http",
70
48
  tools: ["*"],
71
49
  }
72
50
 
package/src/sync/droid.ts CHANGED
@@ -1,21 +1,62 @@
1
- import fs from "fs/promises"
2
1
  import path from "path"
3
2
  import type { ClaudeHomeConfig } from "../parsers/claude-home"
4
- import { forceSymlink, isValidSkillName } from "../utils/symlink"
3
+ import type { ClaudeMcpServer } from "../types/claude"
4
+ import { syncDroidCommands } from "./commands"
5
+ import { mergeJsonConfigAtKey } from "./json-config"
6
+ import { syncSkills } from "./skills"
7
+
8
+ type DroidMcpServer = {
9
+ type: "stdio" | "http"
10
+ command?: string
11
+ args?: string[]
12
+ env?: Record<string, string>
13
+ url?: string
14
+ headers?: Record<string, string>
15
+ disabled: boolean
16
+ }
5
17
 
6
18
  export async function syncToDroid(
7
19
  config: ClaudeHomeConfig,
8
20
  outputRoot: string,
9
21
  ): Promise<void> {
10
- const skillsDir = path.join(outputRoot, "skills")
11
- await fs.mkdir(skillsDir, { recursive: true })
22
+ await syncSkills(config.skills, path.join(outputRoot, "skills"))
23
+ await syncDroidCommands(config, outputRoot)
24
+
25
+ if (Object.keys(config.mcpServers).length > 0) {
26
+ await mergeJsonConfigAtKey({
27
+ configPath: path.join(outputRoot, "mcp.json"),
28
+ key: "mcpServers",
29
+ incoming: convertMcpForDroid(config.mcpServers),
30
+ })
31
+ }
32
+ }
12
33
 
13
- for (const skill of config.skills) {
14
- if (!isValidSkillName(skill.name)) {
15
- console.warn(`Skipping skill with invalid name: ${skill.name}`)
34
+ function convertMcpForDroid(
35
+ servers: Record<string, ClaudeMcpServer>,
36
+ ): Record<string, DroidMcpServer> {
37
+ const result: Record<string, DroidMcpServer> = {}
38
+
39
+ for (const [name, server] of Object.entries(servers)) {
40
+ if (server.command) {
41
+ result[name] = {
42
+ type: "stdio",
43
+ command: server.command,
44
+ args: server.args,
45
+ env: server.env,
46
+ disabled: false,
47
+ }
16
48
  continue
17
49
  }
18
- const target = path.join(skillsDir, skill.name)
19
- await forceSymlink(skill.sourceDir, target)
50
+
51
+ if (server.url) {
52
+ result[name] = {
53
+ type: "http",
54
+ url: server.url,
55
+ headers: server.headers,
56
+ disabled: false,
57
+ }
58
+ }
20
59
  }
60
+
61
+ return result
21
62
  }
@@ -0,0 +1,135 @@
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 { syncGeminiCommands } from "./commands"
6
+ import { mergeJsonConfigAtKey } from "./json-config"
7
+ import { syncSkills } from "./skills"
8
+
9
+ type GeminiMcpServer = {
10
+ command?: string
11
+ args?: string[]
12
+ url?: string
13
+ env?: Record<string, string>
14
+ headers?: Record<string, string>
15
+ }
16
+
17
+ export async function syncToGemini(
18
+ config: ClaudeHomeConfig,
19
+ outputRoot: string,
20
+ ): Promise<void> {
21
+ await syncGeminiSkills(config.skills, outputRoot)
22
+ await syncGeminiCommands(config, outputRoot)
23
+
24
+ if (Object.keys(config.mcpServers).length > 0) {
25
+ const settingsPath = path.join(outputRoot, "settings.json")
26
+ const converted = convertMcpForGemini(config.mcpServers)
27
+ await mergeJsonConfigAtKey({
28
+ configPath: settingsPath,
29
+ key: "mcpServers",
30
+ incoming: converted,
31
+ })
32
+ }
33
+ }
34
+
35
+ async function syncGeminiSkills(
36
+ skills: ClaudeHomeConfig["skills"],
37
+ outputRoot: string,
38
+ ): Promise<void> {
39
+ const skillsDir = path.join(outputRoot, "skills")
40
+ const sharedSkillsDir = getGeminiSharedSkillsDir(outputRoot)
41
+
42
+ if (!sharedSkillsDir) {
43
+ await syncSkills(skills, skillsDir)
44
+ return
45
+ }
46
+
47
+ const canonicalSharedSkillsDir = await canonicalizePath(sharedSkillsDir)
48
+ const mirroredSkills: ClaudeHomeConfig["skills"] = []
49
+ const directSkills: ClaudeHomeConfig["skills"] = []
50
+
51
+ for (const skill of skills) {
52
+ if (await isWithinDir(skill.sourceDir, canonicalSharedSkillsDir)) {
53
+ mirroredSkills.push(skill)
54
+ } else {
55
+ directSkills.push(skill)
56
+ }
57
+ }
58
+
59
+ await removeGeminiMirrorConflicts(mirroredSkills, skillsDir, canonicalSharedSkillsDir)
60
+ await syncSkills(directSkills, skillsDir)
61
+ }
62
+
63
+ function getGeminiSharedSkillsDir(outputRoot: string): string | null {
64
+ if (path.basename(outputRoot) !== ".gemini") return null
65
+ return path.join(path.dirname(outputRoot), ".agents", "skills")
66
+ }
67
+
68
+ async function canonicalizePath(targetPath: string): Promise<string> {
69
+ try {
70
+ return await fs.realpath(targetPath)
71
+ } catch {
72
+ return path.resolve(targetPath)
73
+ }
74
+ }
75
+
76
+ async function isWithinDir(candidate: string, canonicalParentDir: string): Promise<boolean> {
77
+ const resolvedCandidate = await canonicalizePath(candidate)
78
+ return resolvedCandidate === canonicalParentDir
79
+ || resolvedCandidate.startsWith(`${canonicalParentDir}${path.sep}`)
80
+ }
81
+
82
+ async function removeGeminiMirrorConflicts(
83
+ skills: ClaudeHomeConfig["skills"],
84
+ skillsDir: string,
85
+ sharedSkillsDir: string,
86
+ ): Promise<void> {
87
+ for (const skill of skills) {
88
+ const duplicatePath = path.join(skillsDir, skill.name)
89
+
90
+ let stat
91
+ try {
92
+ stat = await fs.lstat(duplicatePath)
93
+ } catch (error) {
94
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
95
+ continue
96
+ }
97
+ throw error
98
+ }
99
+
100
+ if (!stat.isSymbolicLink()) {
101
+ continue
102
+ }
103
+
104
+ let resolvedTarget: string
105
+ try {
106
+ resolvedTarget = await canonicalizePath(duplicatePath)
107
+ } catch {
108
+ continue
109
+ }
110
+
111
+ if (resolvedTarget === await canonicalizePath(skill.sourceDir)
112
+ || await isWithinDir(resolvedTarget, sharedSkillsDir)) {
113
+ await fs.unlink(duplicatePath)
114
+ }
115
+ }
116
+ }
117
+
118
+ function convertMcpForGemini(
119
+ servers: Record<string, ClaudeMcpServer>,
120
+ ): Record<string, GeminiMcpServer> {
121
+ const result: Record<string, GeminiMcpServer> = {}
122
+ for (const [name, server] of Object.entries(servers)) {
123
+ const entry: GeminiMcpServer = {}
124
+ if (server.command) {
125
+ entry.command = server.command
126
+ if (server.args && server.args.length > 0) entry.args = server.args
127
+ if (server.env && Object.keys(server.env).length > 0) entry.env = server.env
128
+ } else if (server.url) {
129
+ entry.url = server.url
130
+ if (server.headers && Object.keys(server.headers).length > 0) entry.headers = server.headers
131
+ }
132
+ result[name] = entry
133
+ }
134
+ return result
135
+ }
@@ -0,0 +1,47 @@
1
+ import path from "path"
2
+ import { pathExists, readJson, writeJsonSecure } from "../utils/files"
3
+
4
+ type JsonObject = Record<string, unknown>
5
+
6
+ function isJsonObject(value: unknown): value is JsonObject {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value)
8
+ }
9
+
10
+ export async function mergeJsonConfigAtKey(options: {
11
+ configPath: string
12
+ key: string
13
+ incoming: Record<string, unknown>
14
+ }): Promise<void> {
15
+ const { configPath, key, incoming } = options
16
+ const existing = await readJsonObjectSafe(configPath)
17
+ const existingEntries = isJsonObject(existing[key]) ? existing[key] : {}
18
+ const merged = {
19
+ ...existing,
20
+ [key]: {
21
+ ...existingEntries,
22
+ ...incoming,
23
+ },
24
+ }
25
+
26
+ await writeJsonSecure(configPath, merged)
27
+ }
28
+
29
+ async function readJsonObjectSafe(configPath: string): Promise<JsonObject> {
30
+ if (!(await pathExists(configPath))) {
31
+ return {}
32
+ }
33
+
34
+ try {
35
+ const parsed = await readJson<unknown>(configPath)
36
+ if (isJsonObject(parsed)) {
37
+ return parsed
38
+ }
39
+ } catch {
40
+ // Fall through to warning and replacement.
41
+ }
42
+
43
+ console.warn(
44
+ `Warning: existing ${path.basename(configPath)} could not be parsed and will be replaced.`,
45
+ )
46
+ return {}
47
+ }
@@ -0,0 +1,49 @@
1
+ import path from "path"
2
+ import type { ClaudeHomeConfig } from "../parsers/claude-home"
3
+ import type { ClaudeMcpServer } from "../types/claude"
4
+ import type { KiroMcpServer } from "../types/kiro"
5
+ import { syncKiroCommands } from "./commands"
6
+ import { mergeJsonConfigAtKey } from "./json-config"
7
+ import { syncSkills } from "./skills"
8
+
9
+ export async function syncToKiro(
10
+ config: ClaudeHomeConfig,
11
+ outputRoot: string,
12
+ ): Promise<void> {
13
+ await syncSkills(config.skills, path.join(outputRoot, "skills"))
14
+ await syncKiroCommands(config, outputRoot)
15
+
16
+ if (Object.keys(config.mcpServers).length > 0) {
17
+ await mergeJsonConfigAtKey({
18
+ configPath: path.join(outputRoot, "settings", "mcp.json"),
19
+ key: "mcpServers",
20
+ incoming: convertMcpForKiro(config.mcpServers),
21
+ })
22
+ }
23
+ }
24
+
25
+ function convertMcpForKiro(
26
+ servers: Record<string, ClaudeMcpServer>,
27
+ ): Record<string, KiroMcpServer> {
28
+ const result: Record<string, KiroMcpServer> = {}
29
+
30
+ for (const [name, server] of Object.entries(servers)) {
31
+ if (server.command) {
32
+ result[name] = {
33
+ command: server.command,
34
+ args: server.args,
35
+ env: server.env,
36
+ }
37
+ continue
38
+ }
39
+
40
+ if (server.url) {
41
+ result[name] = {
42
+ url: server.url,
43
+ headers: server.headers,
44
+ }
45
+ }
46
+ }
47
+
48
+ return result
49
+ }
@@ -0,0 +1,19 @@
1
+ import type { ClaudeMcpServer } from "../types/claude"
2
+
3
+ function getTransportType(server: ClaudeMcpServer): string {
4
+ return server.type?.toLowerCase().trim() ?? ""
5
+ }
6
+
7
+ export function hasExplicitSseTransport(server: ClaudeMcpServer): boolean {
8
+ const type = getTransportType(server)
9
+ return type.includes("sse")
10
+ }
11
+
12
+ export function hasExplicitHttpTransport(server: ClaudeMcpServer): boolean {
13
+ const type = getTransportType(server)
14
+ return type.includes("http") || type.includes("streamable")
15
+ }
16
+
17
+ export function hasExplicitRemoteTransport(server: ClaudeMcpServer): boolean {
18
+ return hasExplicitSseTransport(server) || hasExplicitHttpTransport(server)
19
+ }
@@ -0,0 +1,18 @@
1
+ import path from "path"
2
+ import type { ClaudeHomeConfig } from "../parsers/claude-home"
3
+ import { warnUnsupportedOpenClawCommands } from "./commands"
4
+ import { syncSkills } from "./skills"
5
+
6
+ export async function syncToOpenClaw(
7
+ config: ClaudeHomeConfig,
8
+ outputRoot: string,
9
+ ): Promise<void> {
10
+ await syncSkills(config.skills, path.join(outputRoot, "skills"))
11
+ warnUnsupportedOpenClawCommands(config)
12
+
13
+ if (Object.keys(config.mcpServers).length > 0) {
14
+ console.warn(
15
+ "Warning: OpenClaw MCP sync is skipped because the current official OpenClaw docs do not clearly document an MCP server config contract.",
16
+ )
17
+ }
18
+ }
@@ -1,47 +1,27 @@
1
- import fs from "fs/promises"
2
1
  import path from "path"
3
2
  import type { ClaudeHomeConfig } from "../parsers/claude-home"
4
3
  import type { ClaudeMcpServer } from "../types/claude"
5
4
  import type { OpenCodeMcpServer } from "../types/opencode"
6
- import { forceSymlink, isValidSkillName } from "../utils/symlink"
5
+ import { syncOpenCodeCommands } from "./commands"
6
+ import { mergeJsonConfigAtKey } from "./json-config"
7
+ import { syncSkills } from "./skills"
7
8
 
8
9
  export async function syncToOpenCode(
9
10
  config: ClaudeHomeConfig,
10
11
  outputRoot: string,
11
12
  ): Promise<void> {
12
- // Ensure output directories exist
13
- const skillsDir = path.join(outputRoot, "skills")
14
- await fs.mkdir(skillsDir, { recursive: true })
15
-
16
- // Symlink skills (with validation)
17
- for (const skill of config.skills) {
18
- if (!isValidSkillName(skill.name)) {
19
- console.warn(`Skipping skill with invalid name: ${skill.name}`)
20
- continue
21
- }
22
- const target = path.join(skillsDir, skill.name)
23
- await forceSymlink(skill.sourceDir, target)
24
- }
13
+ await syncSkills(config.skills, path.join(outputRoot, "skills"))
14
+ await syncOpenCodeCommands(config, outputRoot)
25
15
 
26
16
  // Merge MCP servers into opencode.json
27
17
  if (Object.keys(config.mcpServers).length > 0) {
28
18
  const configPath = path.join(outputRoot, "opencode.json")
29
- const existing = await readJsonSafe(configPath)
30
19
  const mcpConfig = convertMcpForOpenCode(config.mcpServers)
31
- existing.mcp = { ...(existing.mcp ?? {}), ...mcpConfig }
32
- await fs.writeFile(configPath, JSON.stringify(existing, null, 2), { mode: 0o600 })
33
- }
34
- }
35
-
36
- async function readJsonSafe(filePath: string): Promise<Record<string, unknown>> {
37
- try {
38
- const content = await fs.readFile(filePath, "utf-8")
39
- return JSON.parse(content) as Record<string, unknown>
40
- } catch (err) {
41
- if ((err as NodeJS.ErrnoException).code === "ENOENT") {
42
- return {}
43
- }
44
- throw err
20
+ await mergeJsonConfigAtKey({
21
+ configPath,
22
+ key: "mcp",
23
+ incoming: mcpConfig,
24
+ })
45
25
  }
46
26
  }
47
27
 
package/src/sync/pi.ts CHANGED
@@ -1,8 +1,10 @@
1
- import fs from "fs/promises"
2
1
  import path from "path"
3
2
  import type { ClaudeHomeConfig } from "../parsers/claude-home"
4
3
  import type { ClaudeMcpServer } from "../types/claude"
5
- import { forceSymlink, isValidSkillName } from "../utils/symlink"
4
+ import { ensureDir } from "../utils/files"
5
+ import { syncPiCommands } from "./commands"
6
+ import { mergeJsonConfigAtKey } from "./json-config"
7
+ import { syncSkills } from "./skills"
6
8
 
7
9
  type McporterServer = {
8
10
  baseUrl?: string
@@ -20,45 +22,19 @@ export async function syncToPi(
20
22
  config: ClaudeHomeConfig,
21
23
  outputRoot: string,
22
24
  ): Promise<void> {
23
- const skillsDir = path.join(outputRoot, "skills")
24
25
  const mcporterPath = path.join(outputRoot, "compound-engineering", "mcporter.json")
25
26
 
26
- await fs.mkdir(skillsDir, { recursive: true })
27
-
28
- for (const skill of config.skills) {
29
- if (!isValidSkillName(skill.name)) {
30
- console.warn(`Skipping skill with invalid name: ${skill.name}`)
31
- continue
32
- }
33
- const target = path.join(skillsDir, skill.name)
34
- await forceSymlink(skill.sourceDir, target)
35
- }
27
+ await syncSkills(config.skills, path.join(outputRoot, "skills"))
28
+ await syncPiCommands(config, outputRoot)
36
29
 
37
30
  if (Object.keys(config.mcpServers).length > 0) {
38
- await fs.mkdir(path.dirname(mcporterPath), { recursive: true })
39
-
40
- const existing = await readJsonSafe(mcporterPath)
31
+ await ensureDir(path.dirname(mcporterPath))
41
32
  const converted = convertMcpToMcporter(config.mcpServers)
42
- const merged: McporterConfig = {
43
- mcpServers: {
44
- ...(existing.mcpServers ?? {}),
45
- ...converted.mcpServers,
46
- },
47
- }
48
-
49
- await fs.writeFile(mcporterPath, JSON.stringify(merged, null, 2), { mode: 0o600 })
50
- }
51
- }
52
-
53
- async function readJsonSafe(filePath: string): Promise<Partial<McporterConfig>> {
54
- try {
55
- const content = await fs.readFile(filePath, "utf-8")
56
- return JSON.parse(content) as Partial<McporterConfig>
57
- } catch (err) {
58
- if ((err as NodeJS.ErrnoException).code === "ENOENT") {
59
- return {}
60
- }
61
- throw err
33
+ await mergeJsonConfigAtKey({
34
+ configPath: mcporterPath,
35
+ key: "mcpServers",
36
+ incoming: converted.mcpServers,
37
+ })
62
38
  }
63
39
  }
64
40
 
@@ -0,0 +1,66 @@
1
+ import path from "path"
2
+ import type { ClaudeHomeConfig } from "../parsers/claude-home"
3
+ import type { ClaudeMcpServer } from "../types/claude"
4
+ import type { QwenMcpServer } from "../types/qwen"
5
+ import { syncQwenCommands } from "./commands"
6
+ import { mergeJsonConfigAtKey } from "./json-config"
7
+ import { hasExplicitRemoteTransport, hasExplicitSseTransport } from "./mcp-transports"
8
+ import { syncSkills } from "./skills"
9
+
10
+ export async function syncToQwen(
11
+ config: ClaudeHomeConfig,
12
+ outputRoot: string,
13
+ ): Promise<void> {
14
+ await syncSkills(config.skills, path.join(outputRoot, "skills"))
15
+ await syncQwenCommands(config, outputRoot)
16
+
17
+ if (Object.keys(config.mcpServers).length > 0) {
18
+ await mergeJsonConfigAtKey({
19
+ configPath: path.join(outputRoot, "settings.json"),
20
+ key: "mcpServers",
21
+ incoming: convertMcpForQwen(config.mcpServers),
22
+ })
23
+ }
24
+ }
25
+
26
+ function convertMcpForQwen(
27
+ servers: Record<string, ClaudeMcpServer>,
28
+ ): Record<string, QwenMcpServer> {
29
+ const result: Record<string, QwenMcpServer> = {}
30
+
31
+ for (const [name, server] of Object.entries(servers)) {
32
+ if (server.command) {
33
+ result[name] = {
34
+ command: server.command,
35
+ args: server.args,
36
+ env: server.env,
37
+ }
38
+ continue
39
+ }
40
+
41
+ if (!server.url) {
42
+ continue
43
+ }
44
+
45
+ if (hasExplicitSseTransport(server)) {
46
+ result[name] = {
47
+ url: server.url,
48
+ headers: server.headers,
49
+ }
50
+ continue
51
+ }
52
+
53
+ if (!hasExplicitRemoteTransport(server)) {
54
+ console.warn(
55
+ `Warning: Qwen MCP server "${name}" has an ambiguous remote transport; defaulting to Streamable HTTP.`,
56
+ )
57
+ }
58
+
59
+ result[name] = {
60
+ httpUrl: server.url,
61
+ headers: server.headers,
62
+ }
63
+ }
64
+
65
+ return result
66
+ }