@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.github/workflows/publish.yml +20 -10
- package/.releaserc.json +36 -0
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +39 -0
- package/CLAUDE.md +14 -0
- package/README.md +35 -2
- package/bun.lock +977 -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/solutions/adding-converter-target-providers.md +2 -1
- package/docs/solutions/plugin-versioning-requirements.md +4 -0
- package/package.json +10 -4
- package/plugins/compound-engineering/.claude-plugin/plugin.json +1 -1
- package/plugins/compound-engineering/CHANGELOG.md +10 -0
- package/plugins/compound-engineering/CLAUDE.md +5 -0
- 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/setup/SKILL.md +6 -0
- package/src/commands/sync.ts +21 -60
- 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 +87 -28
- 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/types/kiro.ts +3 -1
- package/src/types/qwen.ts +3 -0
- package/src/types/windsurf.ts +1 -0
- package/src/utils/codex-agents.ts +1 -1
- package/src/utils/detect-tools.ts +4 -13
- package/src/utils/files.ts +7 -0
- package/src/utils/symlink.ts +4 -6
- package/tests/claude-home.test.ts +46 -0
- package/tests/cli.test.ts +102 -0
- package/tests/detect-tools.test.ts +30 -7
- 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 +54 -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "compound-engineering",
|
|
3
|
-
"version": "2.38.
|
|
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
|
package/plugins/compound-engineering/skills/create-agent-skills/workflows/create-new-skill.md
CHANGED
|
@@ -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
|
package/src/commands/sync.ts
CHANGED
|
@@ -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 {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 = [
|
|
16
|
-
type SyncTarget =
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
113
|
-
|
|
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
|
|
124
|
-
|
|
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:
|
|
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
|
+
}
|