@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/commands/sync.ts
CHANGED
|
@@ -1,60 +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
|
-
|
|
4
|
+
import {
|
|
5
|
+
getDefaultSyncRegistryContext,
|
|
6
|
+
getSyncTarget,
|
|
7
|
+
isSyncTargetName,
|
|
8
|
+
syncTargetNames,
|
|
9
|
+
type SyncTargetName,
|
|
10
|
+
} from "../sync/registry"
|
|
10
11
|
import { expandHome } from "../utils/resolve-home"
|
|
12
|
+
import { hasPotentialSecrets } from "../utils/secrets"
|
|
13
|
+
import { detectInstalledTools } from "../utils/detect-tools"
|
|
11
14
|
|
|
12
|
-
const validTargets = [
|
|
13
|
-
type SyncTarget =
|
|
15
|
+
const validTargets = [...syncTargetNames, "all"] as const
|
|
16
|
+
type SyncTarget = SyncTargetName | "all"
|
|
14
17
|
|
|
15
18
|
function isValidTarget(value: string): value is SyncTarget {
|
|
16
|
-
return
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Check if any MCP servers have env vars that might contain secrets */
|
|
20
|
-
function hasPotentialSecrets(mcpServers: Record<string, unknown>): boolean {
|
|
21
|
-
const sensitivePatterns = /key|token|secret|password|credential|api_key/i
|
|
22
|
-
for (const server of Object.values(mcpServers)) {
|
|
23
|
-
const env = (server as { env?: Record<string, string> }).env
|
|
24
|
-
if (env) {
|
|
25
|
-
for (const key of Object.keys(env)) {
|
|
26
|
-
if (sensitivePatterns.test(key)) return true
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return false
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function resolveOutputRoot(target: SyncTarget): string {
|
|
34
|
-
switch (target) {
|
|
35
|
-
case "opencode":
|
|
36
|
-
return path.join(os.homedir(), ".config", "opencode")
|
|
37
|
-
case "codex":
|
|
38
|
-
return path.join(os.homedir(), ".codex")
|
|
39
|
-
case "pi":
|
|
40
|
-
return path.join(os.homedir(), ".pi", "agent")
|
|
41
|
-
case "droid":
|
|
42
|
-
return path.join(os.homedir(), ".factory")
|
|
43
|
-
case "copilot":
|
|
44
|
-
return path.join(process.cwd(), ".github")
|
|
45
|
-
}
|
|
19
|
+
return value === "all" || isSyncTargetName(value)
|
|
46
20
|
}
|
|
47
21
|
|
|
48
22
|
export default defineCommand({
|
|
49
23
|
meta: {
|
|
50
24
|
name: "sync",
|
|
51
|
-
description: "Sync Claude Code config (~/.claude/) to
|
|
25
|
+
description: "Sync Claude Code config (~/.claude/) to supported provider configs and skills",
|
|
52
26
|
},
|
|
53
27
|
args: {
|
|
54
28
|
target: {
|
|
55
29
|
type: "string",
|
|
56
|
-
|
|
57
|
-
description:
|
|
30
|
+
default: "all",
|
|
31
|
+
description: `Target: ${syncTargetNames.join(" | ")} | all (default: all)`,
|
|
58
32
|
},
|
|
59
33
|
claudeHome: {
|
|
60
34
|
type: "string",
|
|
@@ -67,7 +41,8 @@ export default defineCommand({
|
|
|
67
41
|
throw new Error(`Unknown target: ${args.target}. Use one of: ${validTargets.join(", ")}`)
|
|
68
42
|
}
|
|
69
43
|
|
|
70
|
-
const
|
|
44
|
+
const { home, cwd } = getDefaultSyncRegistryContext()
|
|
45
|
+
const claudeHome = expandHome(args.claudeHome ?? path.join(home, ".claude"))
|
|
71
46
|
const config = await loadClaudeHome(claudeHome)
|
|
72
47
|
|
|
73
48
|
// Warn about potential secrets in MCP env vars
|
|
@@ -78,30 +53,36 @@ export default defineCommand({
|
|
|
78
53
|
)
|
|
79
54
|
}
|
|
80
55
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
56
|
+
if (args.target === "all") {
|
|
57
|
+
const detected = await detectInstalledTools()
|
|
58
|
+
const activeTargets = detected.filter((t) => t.detected).map((t) => t.name)
|
|
84
59
|
|
|
85
|
-
|
|
60
|
+
if (activeTargets.length === 0) {
|
|
61
|
+
console.log("No AI coding tools detected.")
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`Syncing to ${activeTargets.length} detected tool(s)...`)
|
|
66
|
+
for (const tool of detected) {
|
|
67
|
+
console.log(` ${tool.detected ? "✓" : "✗"} ${tool.name} — ${tool.reason}`)
|
|
68
|
+
}
|
|
86
69
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
case "pi":
|
|
95
|
-
await syncToPi(config, outputRoot)
|
|
96
|
-
break
|
|
97
|
-
case "droid":
|
|
98
|
-
await syncToDroid(config, outputRoot)
|
|
99
|
-
break
|
|
100
|
-
case "copilot":
|
|
101
|
-
await syncToCopilot(config, outputRoot)
|
|
102
|
-
break
|
|
70
|
+
for (const name of activeTargets) {
|
|
71
|
+
const target = getSyncTarget(name as SyncTargetName)
|
|
72
|
+
const outputRoot = target.resolveOutputRoot(home, cwd)
|
|
73
|
+
await target.sync(config, outputRoot)
|
|
74
|
+
console.log(`✓ Synced to ${name}: ${outputRoot}`)
|
|
75
|
+
}
|
|
76
|
+
return
|
|
103
77
|
}
|
|
104
78
|
|
|
79
|
+
console.log(
|
|
80
|
+
`Syncing ${config.skills.length} skills, ${config.commands?.length ?? 0} commands, ${Object.keys(config.mcpServers).length} MCP servers...`,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const target = getSyncTarget(args.target as SyncTargetName)
|
|
84
|
+
const outputRoot = target.resolveOutputRoot(home, cwd)
|
|
85
|
+
await target.sync(config, outputRoot)
|
|
105
86
|
console.log(`✓ Synced to ${args.target}: ${outputRoot}`)
|
|
106
87
|
},
|
|
107
88
|
})
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { formatFrontmatter } from "../utils/frontmatter"
|
|
2
|
+
import type {
|
|
3
|
+
ClaudeAgent,
|
|
4
|
+
ClaudeCommand,
|
|
5
|
+
ClaudePlugin,
|
|
6
|
+
ClaudeMcpServer,
|
|
7
|
+
} from "../types/claude"
|
|
8
|
+
import type {
|
|
9
|
+
OpenClawBundle,
|
|
10
|
+
OpenClawCommandRegistration,
|
|
11
|
+
OpenClawPluginManifest,
|
|
12
|
+
OpenClawSkillFile,
|
|
13
|
+
} from "../types/openclaw"
|
|
14
|
+
import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
|
|
15
|
+
|
|
16
|
+
export type ClaudeToOpenClawOptions = ClaudeToOpenCodeOptions
|
|
17
|
+
|
|
18
|
+
export function convertClaudeToOpenClaw(
|
|
19
|
+
plugin: ClaudePlugin,
|
|
20
|
+
_options: ClaudeToOpenClawOptions,
|
|
21
|
+
): OpenClawBundle {
|
|
22
|
+
const enabledCommands = plugin.commands.filter((cmd) => !cmd.disableModelInvocation)
|
|
23
|
+
|
|
24
|
+
const agentSkills = plugin.agents.map(convertAgentToSkill)
|
|
25
|
+
const commandSkills = enabledCommands.map(convertCommandToSkill)
|
|
26
|
+
const commands = enabledCommands.map(convertCommand)
|
|
27
|
+
|
|
28
|
+
const skills: OpenClawSkillFile[] = [...agentSkills, ...commandSkills]
|
|
29
|
+
|
|
30
|
+
const skillDirCopies = plugin.skills.map((skill) => ({
|
|
31
|
+
sourceDir: skill.sourceDir,
|
|
32
|
+
name: skill.name,
|
|
33
|
+
}))
|
|
34
|
+
|
|
35
|
+
const allSkillDirs = [
|
|
36
|
+
...agentSkills.map((s) => s.dir),
|
|
37
|
+
...commandSkills.map((s) => s.dir),
|
|
38
|
+
...plugin.skills.map((s) => s.name),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const manifest = buildManifest(plugin, allSkillDirs)
|
|
42
|
+
|
|
43
|
+
const packageJson = buildPackageJson(plugin)
|
|
44
|
+
|
|
45
|
+
const openclawConfig = plugin.mcpServers
|
|
46
|
+
? buildOpenClawConfig(plugin.mcpServers)
|
|
47
|
+
: undefined
|
|
48
|
+
|
|
49
|
+
const entryPoint = generateEntryPoint(commands)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
manifest,
|
|
53
|
+
packageJson,
|
|
54
|
+
entryPoint,
|
|
55
|
+
skills,
|
|
56
|
+
skillDirCopies,
|
|
57
|
+
commands,
|
|
58
|
+
openclawConfig,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildManifest(plugin: ClaudePlugin, skillDirs: string[]): OpenClawPluginManifest {
|
|
63
|
+
return {
|
|
64
|
+
id: plugin.manifest.name,
|
|
65
|
+
name: formatDisplayName(plugin.manifest.name),
|
|
66
|
+
kind: "tool",
|
|
67
|
+
skills: skillDirs.map((dir) => `skills/${dir}`),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildPackageJson(plugin: ClaudePlugin): Record<string, unknown> {
|
|
72
|
+
return {
|
|
73
|
+
name: `openclaw-${plugin.manifest.name}`,
|
|
74
|
+
version: plugin.manifest.version,
|
|
75
|
+
type: "module",
|
|
76
|
+
private: true,
|
|
77
|
+
description: plugin.manifest.description,
|
|
78
|
+
main: "index.ts",
|
|
79
|
+
openclaw: {
|
|
80
|
+
extensions: [
|
|
81
|
+
{
|
|
82
|
+
id: plugin.manifest.name,
|
|
83
|
+
entry: "./index.ts",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
keywords: [
|
|
88
|
+
"openclaw",
|
|
89
|
+
"openclaw-plugin",
|
|
90
|
+
...(plugin.manifest.keywords ?? []),
|
|
91
|
+
],
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function convertAgentToSkill(agent: ClaudeAgent): OpenClawSkillFile {
|
|
96
|
+
const frontmatter: Record<string, unknown> = {
|
|
97
|
+
name: agent.name,
|
|
98
|
+
description: agent.description,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (agent.model && agent.model !== "inherit") {
|
|
102
|
+
frontmatter.model = agent.model
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const body = rewritePaths(agent.body)
|
|
106
|
+
const content = formatFrontmatter(frontmatter, body)
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
name: agent.name,
|
|
110
|
+
content,
|
|
111
|
+
dir: `agent-${agent.name}`,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function convertCommandToSkill(command: ClaudeCommand): OpenClawSkillFile {
|
|
116
|
+
const frontmatter: Record<string, unknown> = {
|
|
117
|
+
name: `cmd-${command.name}`,
|
|
118
|
+
description: command.description,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (command.model && command.model !== "inherit") {
|
|
122
|
+
frontmatter.model = command.model
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const body = rewritePaths(command.body)
|
|
126
|
+
const content = formatFrontmatter(frontmatter, body)
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
name: command.name,
|
|
130
|
+
content,
|
|
131
|
+
dir: `cmd-${command.name}`,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function convertCommand(command: ClaudeCommand): OpenClawCommandRegistration {
|
|
136
|
+
return {
|
|
137
|
+
name: command.name.replace(/:/g, "-"),
|
|
138
|
+
description: command.description ?? `Run ${command.name}`,
|
|
139
|
+
acceptsArgs: Boolean(command.argumentHint),
|
|
140
|
+
body: rewritePaths(command.body),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildOpenClawConfig(
|
|
145
|
+
servers: Record<string, ClaudeMcpServer>,
|
|
146
|
+
): Record<string, unknown> {
|
|
147
|
+
const mcpServers: Record<string, unknown> = {}
|
|
148
|
+
|
|
149
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
150
|
+
if (server.command) {
|
|
151
|
+
mcpServers[name] = {
|
|
152
|
+
type: "stdio",
|
|
153
|
+
command: server.command,
|
|
154
|
+
args: server.args ?? [],
|
|
155
|
+
env: server.env,
|
|
156
|
+
}
|
|
157
|
+
} else if (server.url) {
|
|
158
|
+
mcpServers[name] = {
|
|
159
|
+
type: "http",
|
|
160
|
+
url: server.url,
|
|
161
|
+
headers: server.headers,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { mcpServers }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function generateEntryPoint(commands: OpenClawCommandRegistration[]): string {
|
|
170
|
+
const commandRegistrations = commands
|
|
171
|
+
.map((cmd) => {
|
|
172
|
+
// JSON.stringify produces a fully-escaped string literal safe for JS/TS source embedding
|
|
173
|
+
const safeName = JSON.stringify(cmd.name)
|
|
174
|
+
const safeDesc = JSON.stringify(cmd.description ?? "")
|
|
175
|
+
const safeNotFound = JSON.stringify(`Command ${cmd.name} not found. Check skills directory.`)
|
|
176
|
+
return ` api.registerCommand({
|
|
177
|
+
name: ${safeName},
|
|
178
|
+
description: ${safeDesc},
|
|
179
|
+
acceptsArgs: ${cmd.acceptsArgs},
|
|
180
|
+
requireAuth: false,
|
|
181
|
+
handler: (ctx) => ({
|
|
182
|
+
text: skills[${safeName}] ?? ${safeNotFound},
|
|
183
|
+
}),
|
|
184
|
+
});`
|
|
185
|
+
})
|
|
186
|
+
.join("\n\n")
|
|
187
|
+
|
|
188
|
+
return `// Auto-generated OpenClaw plugin entry point
|
|
189
|
+
// Converted from Claude Code plugin format by compound-plugin CLI
|
|
190
|
+
import { promises as fs } from "fs";
|
|
191
|
+
import path from "path";
|
|
192
|
+
import { fileURLToPath } from "url";
|
|
193
|
+
|
|
194
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
195
|
+
|
|
196
|
+
// Pre-load skill bodies for command responses
|
|
197
|
+
const skills: Record<string, string> = {};
|
|
198
|
+
|
|
199
|
+
async function loadSkills() {
|
|
200
|
+
const skillsDir = path.join(__dirname, "skills");
|
|
201
|
+
try {
|
|
202
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
203
|
+
for (const entry of entries) {
|
|
204
|
+
if (!entry.isDirectory()) continue;
|
|
205
|
+
const skillPath = path.join(skillsDir, entry.name, "SKILL.md");
|
|
206
|
+
try {
|
|
207
|
+
const content = await fs.readFile(skillPath, "utf8");
|
|
208
|
+
// Strip frontmatter
|
|
209
|
+
const body = content.replace(/^---[\\s\\S]*?---\\n*/, "");
|
|
210
|
+
skills[entry.name.replace(/^cmd-/, "")] = body.trim();
|
|
211
|
+
} catch {
|
|
212
|
+
// Skill file not found, skip
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// Skills directory not found
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export default async function register(api) {
|
|
221
|
+
await loadSkills();
|
|
222
|
+
|
|
223
|
+
${commandRegistrations}
|
|
224
|
+
}
|
|
225
|
+
`
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function rewritePaths(body: string): string {
|
|
229
|
+
return body
|
|
230
|
+
.replace(/(?<=^|\s|["'`])~\/\.claude\//gm, "~/.openclaw/")
|
|
231
|
+
.replace(/(?<=^|\s|["'`])\.claude\//gm, ".openclaw/")
|
|
232
|
+
.replace(/\.claude-plugin\//g, "openclaw-plugin/")
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function formatDisplayName(name: string): string {
|
|
236
|
+
return name
|
|
237
|
+
.split("-")
|
|
238
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
239
|
+
.join(" ")
|
|
240
|
+
}
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
} from "../types/claude"
|
|
9
9
|
import type {
|
|
10
10
|
OpenCodeBundle,
|
|
11
|
-
|
|
11
|
+
OpenCodeCommandFile,
|
|
12
12
|
OpenCodeConfig,
|
|
13
13
|
OpenCodeMcpServer,
|
|
14
14
|
} from "../types/opencode"
|
|
@@ -66,13 +66,12 @@ export function convertClaudeToOpenCode(
|
|
|
66
66
|
options: ClaudeToOpenCodeOptions,
|
|
67
67
|
): OpenCodeBundle {
|
|
68
68
|
const agentFiles = plugin.agents.map((agent) => convertAgent(agent, options))
|
|
69
|
-
const
|
|
69
|
+
const cmdFiles = convertCommands(plugin.commands)
|
|
70
70
|
const mcp = plugin.mcpServers ? convertMcp(plugin.mcpServers) : undefined
|
|
71
71
|
const plugins = plugin.hooks ? [convertHooks(plugin.hooks)] : []
|
|
72
72
|
|
|
73
73
|
const config: OpenCodeConfig = {
|
|
74
74
|
$schema: "https://opencode.ai/config.json",
|
|
75
|
-
command: Object.keys(commandMap).length > 0 ? commandMap : undefined,
|
|
76
75
|
mcp: mcp && Object.keys(mcp).length > 0 ? mcp : undefined,
|
|
77
76
|
}
|
|
78
77
|
|
|
@@ -81,6 +80,7 @@ export function convertClaudeToOpenCode(
|
|
|
81
80
|
return {
|
|
82
81
|
config,
|
|
83
82
|
agents: agentFiles,
|
|
83
|
+
commandFiles: cmdFiles,
|
|
84
84
|
plugins,
|
|
85
85
|
skillDirs: plugin.skills.map((skill) => ({ sourceDir: skill.sourceDir, name: skill.name })),
|
|
86
86
|
}
|
|
@@ -111,20 +111,22 @@ function convertAgent(agent: ClaudeAgent, options: ClaudeToOpenCodeOptions) {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
// Commands are written as individual .md files rather than entries in opencode.json.
|
|
115
|
+
// Chosen over JSON map because opencode resolves commands by filename at runtime (ADR-001).
|
|
116
|
+
function convertCommands(commands: ClaudeCommand[]): OpenCodeCommandFile[] {
|
|
117
|
+
const files: OpenCodeCommandFile[] = []
|
|
116
118
|
for (const command of commands) {
|
|
117
119
|
if (command.disableModelInvocation) continue
|
|
118
|
-
const
|
|
120
|
+
const frontmatter: Record<string, unknown> = {
|
|
119
121
|
description: command.description,
|
|
120
|
-
template: rewriteClaudePaths(command.body),
|
|
121
122
|
}
|
|
122
123
|
if (command.model && command.model !== "inherit") {
|
|
123
|
-
|
|
124
|
+
frontmatter.model = normalizeModel(command.model)
|
|
124
125
|
}
|
|
125
|
-
|
|
126
|
+
const content = formatFrontmatter(frontmatter, rewriteClaudePaths(command.body))
|
|
127
|
+
files.push({ name: command.name, content })
|
|
126
128
|
}
|
|
127
|
-
return
|
|
129
|
+
return files
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
function convertMcp(servers: Record<string, ClaudeMcpServer>): Record<string, OpenCodeMcpServer> {
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { formatFrontmatter } from "../utils/frontmatter"
|
|
2
|
+
import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude"
|
|
3
|
+
import type {
|
|
4
|
+
QwenAgentFile,
|
|
5
|
+
QwenBundle,
|
|
6
|
+
QwenCommandFile,
|
|
7
|
+
QwenExtensionConfig,
|
|
8
|
+
QwenMcpServer,
|
|
9
|
+
QwenSetting,
|
|
10
|
+
} from "../types/qwen"
|
|
11
|
+
|
|
12
|
+
export type ClaudeToQwenOptions = {
|
|
13
|
+
agentMode: "primary" | "subagent"
|
|
14
|
+
inferTemperature: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function convertClaudeToQwen(plugin: ClaudePlugin, options: ClaudeToQwenOptions): QwenBundle {
|
|
18
|
+
const agentFiles = plugin.agents.map((agent) => convertAgent(agent, options))
|
|
19
|
+
const cmdFiles = convertCommands(plugin.commands)
|
|
20
|
+
const mcp = plugin.mcpServers ? convertMcp(plugin.mcpServers) : undefined
|
|
21
|
+
const settings = extractSettings(plugin.mcpServers)
|
|
22
|
+
|
|
23
|
+
const config: QwenExtensionConfig = {
|
|
24
|
+
name: plugin.manifest.name,
|
|
25
|
+
version: plugin.manifest.version || "1.0.0",
|
|
26
|
+
commands: "commands",
|
|
27
|
+
skills: "skills",
|
|
28
|
+
agents: "agents",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (mcp && Object.keys(mcp).length > 0) {
|
|
32
|
+
config.mcpServers = mcp
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (settings && settings.length > 0) {
|
|
36
|
+
config.settings = settings
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const contextFile = generateContextFile(plugin)
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
config,
|
|
43
|
+
agents: agentFiles,
|
|
44
|
+
commandFiles: cmdFiles,
|
|
45
|
+
skillDirs: plugin.skills.map((skill) => ({ sourceDir: skill.sourceDir, name: skill.name })),
|
|
46
|
+
contextFile,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function convertAgent(agent: ClaudeAgent, options: ClaudeToQwenOptions): QwenAgentFile {
|
|
51
|
+
const frontmatter: Record<string, unknown> = {
|
|
52
|
+
name: agent.name,
|
|
53
|
+
description: agent.description,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (agent.model && agent.model !== "inherit") {
|
|
57
|
+
frontmatter.model = normalizeModel(agent.model)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (options.inferTemperature) {
|
|
61
|
+
const temperature = inferTemperature(agent)
|
|
62
|
+
if (temperature !== undefined) {
|
|
63
|
+
frontmatter.temperature = temperature
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Qwen supports both YAML and Markdown for agents
|
|
68
|
+
// Using YAML format for structured config
|
|
69
|
+
const content = formatFrontmatter(frontmatter, rewriteQwenPaths(agent.body))
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
name: agent.name,
|
|
73
|
+
content,
|
|
74
|
+
format: "yaml",
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function convertCommands(commands: ClaudeCommand[]): QwenCommandFile[] {
|
|
79
|
+
const files: QwenCommandFile[] = []
|
|
80
|
+
for (const command of commands) {
|
|
81
|
+
if (command.disableModelInvocation) continue
|
|
82
|
+
const frontmatter: Record<string, unknown> = {
|
|
83
|
+
description: command.description,
|
|
84
|
+
}
|
|
85
|
+
if (command.model && command.model !== "inherit") {
|
|
86
|
+
frontmatter.model = normalizeModel(command.model)
|
|
87
|
+
}
|
|
88
|
+
if (command.allowedTools && command.allowedTools.length > 0) {
|
|
89
|
+
frontmatter.allowedTools = command.allowedTools
|
|
90
|
+
}
|
|
91
|
+
const content = formatFrontmatter(frontmatter, rewriteQwenPaths(command.body))
|
|
92
|
+
files.push({ name: command.name, content })
|
|
93
|
+
}
|
|
94
|
+
return files
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function convertMcp(servers: Record<string, ClaudeMcpServer>): Record<string, QwenMcpServer> {
|
|
98
|
+
const result: Record<string, QwenMcpServer> = {}
|
|
99
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
100
|
+
if (server.command) {
|
|
101
|
+
result[name] = {
|
|
102
|
+
command: server.command,
|
|
103
|
+
args: server.args,
|
|
104
|
+
env: server.env,
|
|
105
|
+
}
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (server.url) {
|
|
110
|
+
// Qwen only supports stdio (command-based) MCP servers — skip remote servers
|
|
111
|
+
console.warn(
|
|
112
|
+
`Warning: Remote MCP server '${name}' (URL: ${server.url}) is not supported in Qwen format. Qwen only supports stdio MCP servers. Skipping.`,
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return result
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function extractSettings(mcpServers?: Record<string, ClaudeMcpServer>): QwenSetting[] {
|
|
120
|
+
const settings: QwenSetting[] = []
|
|
121
|
+
if (!mcpServers) return settings
|
|
122
|
+
|
|
123
|
+
for (const [name, server] of Object.entries(mcpServers)) {
|
|
124
|
+
if (server.env) {
|
|
125
|
+
for (const [envVar, value] of Object.entries(server.env)) {
|
|
126
|
+
// Only add settings for environment variables that look like placeholders
|
|
127
|
+
if (value.startsWith("${") || value.includes("YOUR_") || value.includes("XXX")) {
|
|
128
|
+
settings.push({
|
|
129
|
+
name: formatSettingName(envVar),
|
|
130
|
+
description: `Environment variable for ${name} MCP server`,
|
|
131
|
+
envVar,
|
|
132
|
+
sensitive: envVar.toLowerCase().includes("key") || envVar.toLowerCase().includes("token") || envVar.toLowerCase().includes("secret"),
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return settings
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function formatSettingName(envVar: string): string {
|
|
143
|
+
return envVar
|
|
144
|
+
.replace(/_/g, " ")
|
|
145
|
+
.toLowerCase()
|
|
146
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function generateContextFile(plugin: ClaudePlugin): string {
|
|
150
|
+
const sections: string[] = []
|
|
151
|
+
|
|
152
|
+
// Plugin description
|
|
153
|
+
sections.push(`# ${plugin.manifest.name}`)
|
|
154
|
+
sections.push("")
|
|
155
|
+
if (plugin.manifest.description) {
|
|
156
|
+
sections.push(plugin.manifest.description)
|
|
157
|
+
sections.push("")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Agents section
|
|
161
|
+
if (plugin.agents.length > 0) {
|
|
162
|
+
sections.push("## Agents")
|
|
163
|
+
sections.push("")
|
|
164
|
+
for (const agent of plugin.agents) {
|
|
165
|
+
sections.push(`- **${agent.name}**: ${agent.description || "No description"}`)
|
|
166
|
+
}
|
|
167
|
+
sections.push("")
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Commands section
|
|
171
|
+
if (plugin.commands.length > 0) {
|
|
172
|
+
sections.push("## Commands")
|
|
173
|
+
sections.push("")
|
|
174
|
+
for (const command of plugin.commands) {
|
|
175
|
+
if (!command.disableModelInvocation) {
|
|
176
|
+
sections.push(`- **/${command.name}**: ${command.description || "No description"}`)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
sections.push("")
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Skills section
|
|
183
|
+
if (plugin.skills.length > 0) {
|
|
184
|
+
sections.push("## Skills")
|
|
185
|
+
sections.push("")
|
|
186
|
+
for (const skill of plugin.skills) {
|
|
187
|
+
sections.push(`- ${skill.name}`)
|
|
188
|
+
}
|
|
189
|
+
sections.push("")
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return sections.join("\n")
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function rewriteQwenPaths(body: string): string {
|
|
196
|
+
return body
|
|
197
|
+
.replace(/(?<=^|\s|["'`])~\/\.claude\//gm, "~/.qwen/")
|
|
198
|
+
.replace(/(?<=^|\s|["'`])\.claude\//gm, ".qwen/")
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const CLAUDE_FAMILY_ALIASES: Record<string, string> = {
|
|
202
|
+
haiku: "claude-haiku",
|
|
203
|
+
sonnet: "claude-sonnet",
|
|
204
|
+
opus: "claude-opus",
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeModel(model: string): string {
|
|
208
|
+
if (model.includes("/")) return model
|
|
209
|
+
if (CLAUDE_FAMILY_ALIASES[model]) {
|
|
210
|
+
const resolved = `anthropic/${CLAUDE_FAMILY_ALIASES[model]}`
|
|
211
|
+
console.warn(
|
|
212
|
+
`Warning: bare model alias "${model}" mapped to "${resolved}".`,
|
|
213
|
+
)
|
|
214
|
+
return resolved
|
|
215
|
+
}
|
|
216
|
+
if (/^claude-/.test(model)) return `anthropic/${model}`
|
|
217
|
+
if (/^(gpt-|o1-|o3-)/.test(model)) return `openai/${model}`
|
|
218
|
+
if (/^gemini-/.test(model)) return `google/${model}`
|
|
219
|
+
if (/^qwen-/.test(model)) return `qwen/${model}`
|
|
220
|
+
return `anthropic/${model}`
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function inferTemperature(agent: ClaudeAgent): number | undefined {
|
|
224
|
+
const sample = `${agent.name} ${agent.description ?? ""}`.toLowerCase()
|
|
225
|
+
if (/(review|audit|security|sentinel|oracle|lint|verification|guardian)/.test(sample)) {
|
|
226
|
+
return 0.1
|
|
227
|
+
}
|
|
228
|
+
if (/(plan|planning|architecture|strategist|analysis|research)/.test(sample)) {
|
|
229
|
+
return 0.2
|
|
230
|
+
}
|
|
231
|
+
if (/(doc|readme|changelog|editor|writer)/.test(sample)) {
|
|
232
|
+
return 0.3
|
|
233
|
+
}
|
|
234
|
+
if (/(brainstorm|creative|ideate|design|concept)/.test(sample)) {
|
|
235
|
+
return 0.6
|
|
236
|
+
}
|
|
237
|
+
return undefined
|
|
238
|
+
}
|