@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
|
@@ -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:
|
|
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
|
|
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
|
|
5
|
-
import {
|
|
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
|
-
|
|
12
|
-
|
|
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 =
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
53
|
+
const legacyMarkerIndex = withoutCurrentBlock.indexOf(LEGACY_MARKER)
|
|
54
|
+
const cleaned = legacyMarkerIndex === -1
|
|
55
|
+
? withoutCurrentBlock
|
|
56
|
+
: withoutCurrentBlock.slice(0, legacyMarkerIndex).trimEnd()
|
|
70
57
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|