@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.
- package/.claude-plugin/marketplace.json +3 -3
- package/.github/workflows/publish.yml +20 -10
- package/.releaserc.json +31 -0
- package/AGENTS.md +6 -1
- package/CHANGELOG.md +76 -0
- package/CLAUDE.md +16 -3
- package/README.md +83 -16
- package/bun.lock +977 -0
- package/docs/plans/2026-02-14-feat-auto-detect-install-and-gemini-sync-plan.md +360 -0
- package/docs/plans/2026-02-25-feat-windsurf-global-scope-support-plan.md +627 -0
- package/docs/plans/2026-03-01-feat-ce-command-aliases-backwards-compatible-deprecation-plan.md +261 -0
- package/docs/plans/2026-03-01-fix-setup-skill-non-claude-llm-fallback-plan.md +140 -0
- package/docs/plans/2026-03-03-feat-sync-claude-mcp-all-supported-providers-plan.md +639 -0
- package/docs/plans/feature_opencode-commands-as-md-and-config-merge.md +574 -0
- package/docs/solutions/adding-converter-target-providers.md +693 -0
- package/docs/solutions/plugin-versioning-requirements.md +7 -3
- package/docs/specs/windsurf.md +477 -0
- package/package.json +10 -4
- package/plans/landing-page-launchkit-refresh.md +2 -2
- package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
- package/plugins/compound-engineering/CHANGELOG.md +82 -1
- package/plugins/compound-engineering/CLAUDE.md +14 -7
- package/plugins/compound-engineering/README.md +10 -7
- package/plugins/compound-engineering/agents/research/git-history-analyzer.md +1 -1
- package/plugins/compound-engineering/agents/research/learnings-researcher.md +1 -1
- package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +1 -1
- package/plugins/compound-engineering/commands/ce/brainstorm.md +145 -0
- package/plugins/compound-engineering/commands/ce/compound.md +240 -0
- package/plugins/compound-engineering/commands/ce/plan.md +636 -0
- package/plugins/compound-engineering/commands/ce/review.md +525 -0
- package/plugins/compound-engineering/commands/ce/work.md +470 -0
- package/plugins/compound-engineering/commands/create-agent-skill.md +1 -1
- package/plugins/compound-engineering/commands/deepen-plan.md +6 -6
- package/plugins/compound-engineering/commands/deploy-docs.md +1 -1
- package/plugins/compound-engineering/commands/feature-video.md +15 -6
- package/plugins/compound-engineering/commands/heal-skill.md +1 -1
- package/plugins/compound-engineering/commands/lfg.md +3 -3
- package/plugins/compound-engineering/commands/slfg.md +3 -3
- package/plugins/compound-engineering/commands/test-xcode.md +2 -2
- package/plugins/compound-engineering/commands/workflows/brainstorm.md +4 -123
- package/plugins/compound-engineering/commands/workflows/compound.md +4 -234
- package/plugins/compound-engineering/commands/workflows/plan.md +4 -562
- package/plugins/compound-engineering/commands/workflows/review.md +4 -522
- package/plugins/compound-engineering/commands/workflows/work.md +4 -448
- package/plugins/compound-engineering/skills/brainstorming/SKILL.md +3 -3
- package/plugins/compound-engineering/skills/create-agent-skills/workflows/add-workflow.md +6 -0
- package/plugins/compound-engineering/skills/create-agent-skills/workflows/create-new-skill.md +6 -0
- package/plugins/compound-engineering/skills/document-review/SKILL.md +1 -1
- package/plugins/compound-engineering/skills/file-todos/SKILL.md +1 -1
- package/plugins/compound-engineering/skills/git-worktree/SKILL.md +5 -5
- package/plugins/compound-engineering/skills/proof/SKILL.md +185 -0
- package/plugins/compound-engineering/skills/resolve-pr-parallel/SKILL.md +1 -1
- package/plugins/compound-engineering/skills/setup/SKILL.md +8 -2
- package/src/commands/convert.ts +101 -24
- package/src/commands/install.ts +102 -45
- package/src/commands/sync.ts +43 -62
- package/src/converters/claude-to-openclaw.ts +240 -0
- package/src/converters/claude-to-opencode.ts +12 -10
- package/src/converters/claude-to-qwen.ts +238 -0
- package/src/converters/claude-to-windsurf.ts +205 -0
- package/src/index.ts +2 -1
- package/src/parsers/claude-home.ts +55 -3
- package/src/sync/codex.ts +38 -62
- package/src/sync/commands.ts +198 -0
- package/src/sync/copilot.ts +14 -36
- package/src/sync/droid.ts +50 -9
- package/src/sync/gemini.ts +135 -0
- package/src/sync/json-config.ts +47 -0
- package/src/sync/kiro.ts +49 -0
- package/src/sync/mcp-transports.ts +19 -0
- package/src/sync/openclaw.ts +18 -0
- package/src/sync/opencode.ts +10 -30
- package/src/sync/pi.ts +12 -36
- package/src/sync/qwen.ts +66 -0
- package/src/sync/registry.ts +141 -0
- package/src/sync/skills.ts +21 -0
- package/src/sync/windsurf.ts +59 -0
- package/src/targets/index.ts +60 -1
- package/src/targets/openclaw.ts +96 -0
- package/src/targets/opencode.ts +76 -10
- package/src/targets/qwen.ts +64 -0
- package/src/targets/windsurf.ts +104 -0
- package/src/types/kiro.ts +3 -1
- package/src/types/openclaw.ts +52 -0
- package/src/types/opencode.ts +7 -8
- package/src/types/qwen.ts +51 -0
- package/src/types/windsurf.ts +35 -0
- package/src/utils/codex-agents.ts +1 -1
- package/src/utils/detect-tools.ts +37 -0
- package/src/utils/files.ts +14 -0
- package/src/utils/resolve-output.ts +50 -0
- package/src/utils/secrets.ts +24 -0
- package/src/utils/symlink.ts +4 -6
- package/tests/claude-home.test.ts +46 -0
- package/tests/cli.test.ts +180 -0
- package/tests/converter.test.ts +43 -10
- package/tests/detect-tools.test.ts +119 -0
- package/tests/openclaw-converter.test.ts +200 -0
- package/tests/opencode-writer.test.ts +142 -5
- package/tests/qwen-converter.test.ts +238 -0
- package/tests/resolve-output.test.ts +131 -0
- package/tests/sync-codex.test.ts +64 -0
- package/tests/sync-copilot.test.ts +60 -4
- package/tests/sync-droid.test.ts +44 -4
- package/tests/sync-gemini.test.ts +160 -0
- package/tests/sync-kiro.test.ts +83 -0
- package/tests/sync-openclaw.test.ts +51 -0
- package/tests/sync-qwen.test.ts +75 -0
- package/tests/sync-windsurf.test.ts +89 -0
- package/tests/windsurf-converter.test.ts +573 -0
- package/tests/windsurf-writer.test.ts +359 -0
- package/docs/css/docs.css +0 -675
- package/docs/css/style.css +0 -2886
- package/docs/index.html +0 -1046
- package/docs/js/main.js +0 -225
- package/docs/pages/agents.html +0 -649
- package/docs/pages/changelog.html +0 -534
- package/docs/pages/commands.html +0 -523
- package/docs/pages/getting-started.html +0 -582
- package/docs/pages/mcp-servers.html +0 -409
- package/docs/pages/skills.html +0 -611
package/src/sync/copilot.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
-
|
|
26
|
-
await
|
|
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, "
|
|
39
|
-
const existing = await readJsonSafe(mcpPath)
|
|
31
|
+
const mcpPath = path.join(outputRoot, "mcp-config.json")
|
|
40
32
|
const converted = convertMcpForCopilot(config.mcpServers)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 {
|
|
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
|
-
|
|
11
|
-
await
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
+
}
|
package/src/sync/kiro.ts
ADDED
|
@@ -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
|
+
}
|
package/src/sync/opencode.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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 {
|
|
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
|
|
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
|
|
39
|
-
|
|
40
|
-
const existing = await readJsonSafe(mcporterPath)
|
|
31
|
+
await ensureDir(path.dirname(mcporterPath))
|
|
41
32
|
const converted = convertMcpToMcporter(config.mcpServers)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
package/src/sync/qwen.ts
ADDED
|
@@ -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
|
+
}
|