Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .codebuddy-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "compound-engineering-plugin",
"owner": {
"name": "studyzy (Devin Zeng)"
},
"description": "Compound Engineering plugin marketplace for CodeBuddy",
"plugins": [
{
"name": "compound-engineering",
"description": "Brainstorm, plan, debug, review, and compound learnings with AI agents",
"version": "3.14.3",
"author": {
"name": "studyzy (Devin Zeng)"
},
"homepage": "https://github.com/studyzy",
"repository": "https://github.com/EveryInc/compound-engineering-plugin",
"license": "MIT",
"keywords": [
"brainstorming",
"code-review",
"compound-engineering",
"debugging",
"ideation",
"workflows",
"workflow-automation"
],
"source": {
"source": "github",
"repo": "EveryInc/compound-engineering-plugin"
}
}
]
}
22 changes: 22 additions & 0 deletions .codebuddy-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "compound-engineering",
"version": "3.14.3",
Comment thread
studyzy marked this conversation as resolved.
"description": "Brainstorm, plan, debug, review, and compound learnings with AI agents",
"author": {
"name": "studyzy (Devin Zeng)",
"email": "studyzy@gmail.com"
},
"homepage": "https://github.com/studyzy",
"repository": "https://github.com/EveryInc/compound-engineering-plugin",
"license": "MIT",
"keywords": [
"brainstorming",
"code-review",
"compound-engineering",
"debugging",
"ideation",
"workflows",
"workflow-automation"
],
"skills": "./skills/"
}
8 changes: 7 additions & 1 deletion .github/release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
".agents/plugins/marketplace.json",
".kimi-plugin/marketplace.json",
".claude-plugin/marketplace.json",
".cursor-plugin/marketplace.json"
".cursor-plugin/marketplace.json",
".codebuddy-plugin/marketplace.json"
],
"extra-files": [
{
Expand Down Expand Up @@ -78,6 +79,11 @@
"type": "json",
"path": ".agy/plugin.json",
"jsonpath": "$.version"
},
{
"type": "json",
"path": ".codebuddy-plugin/plugin.json",
"jsonpath": "$.version"
}
]
},
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ todos/
.claude/worktrees/
__pycache__/
*.pyc

.idea/
.serena/
.compound-engineering/*.local.yaml
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,24 @@ qwen extensions install EveryInc/compound-engineering-plugin:compound-engineerin

Qwen Code installs Claude Code-compatible plugins directly from GitHub and converts the plugin format during install, so no Bun install step is needed.

### CodeBuddy

Compound Engineering is available as a CodeBuddy plugin. Register the marketplace, then install the plugin.

1. **Register the marketplace with CodeBuddy:**

```bash
codebuddy plugin marketplace add EveryInc/compound-engineering-plugin
```

2. **Install the plugin:**

```bash
codebuddy plugin install compound-engineering@compound-engineering-plugin
```

The native CodeBuddy plugin install is self-contained for Compound Engineering. Specialist reviewer and research behavior lives inside the skills as local prompt assets; no separate custom-agent install step is required.

### OpenCode

Add Compound Engineering to the `plugin` array in your global or project `opencode.json`:
Expand Down Expand Up @@ -418,6 +436,15 @@ Restart OpenCode after changing `opencode.json`.
pi -e "$PWD"
```

**CodeBuddy**

```bash
codebuddy plugin marketplace add "$PWD"
codebuddy plugin install compound-engineering@compound-engineering-plugin
```

Use a separate `CODEBUDDY_HOME` when you want to keep local testing isolated from your normal CodeBuddy profile.

**Antigravity CLI (`agy`)**

```bash
Expand Down
13 changes: 11 additions & 2 deletions src/commands/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { loadClaudePlugin } from "../parsers/claude"
import { targets, validateScope } from "../targets"
import type { ClaudeToOpenCodeOptions, PermissionMode } from "../converters/claude-to-opencode"
import { ensureCodexAgentsFile } from "../utils/codex-agents"
import { expandHome, resolveCodexHome, resolveTargetHome } from "../utils/resolve-home"
import { expandHome, resolveCodeBuddyHome, resolveCodexHome, resolveTargetHome } from "../utils/resolve-home"
import { resolveOpenCodeWriteScope, resolveTargetOutputRoot } from "../utils/resolve-output"
import { detectInstalledTools } from "../utils/detect-tools"

Expand All @@ -25,7 +25,7 @@ export default defineCommand({
to: {
type: "string",
default: "opencode",
description: "Target format (opencode | codex | pi | antigravity | all)",
description: "Target format (opencode | codex | codebuddy | pi | antigravity | all)",
},
output: {
type: "string",
Expand All @@ -37,6 +37,11 @@ export default defineCommand({
alias: "codex-home",
description: "Write Codex output to this Codex root (default: $CODEX_HOME or ~/.codex)",
},
codebuddyHome: {
type: "string",
alias: "codebuddy-home",
description: "Write CodeBuddy output to this CodeBuddy root (default: ~/.codebuddy)",
},
piHome: {
type: "string",
alias: "pi-home",
Expand Down Expand Up @@ -84,6 +89,7 @@ export default defineCommand({
const outputRoot = resolveOutputRoot(args.output)
const hasExplicitOutput = Boolean(args.output && String(args.output).trim())
const codexHome = resolveCodexHome(args.codexHome)
const codebuddyHome = resolveCodeBuddyHome(args.codebuddyHome)
const piHome = resolveTargetHome(args.piHome, path.join(os.homedir(), ".pi", "agent"))

const options: ClaudeToOpenCodeOptions = {
Expand Down Expand Up @@ -126,6 +132,7 @@ export default defineCommand({
targetName: tool.name,
outputRoot,
codexHome,
codebuddyHome,
piHome,
pluginName: plugin.manifest.name,
hasExplicitOutput,
Expand Down Expand Up @@ -157,6 +164,7 @@ export default defineCommand({
targetName,
outputRoot,
codexHome,
codebuddyHome,
piHome,
pluginName: plugin.manifest.name,
hasExplicitOutput,
Expand Down Expand Up @@ -193,6 +201,7 @@ export default defineCommand({
targetName: extra,
outputRoot,
codexHome,
codebuddyHome,
piHome,
pluginName: plugin.manifest.name,
hasExplicitOutput,
Expand Down
185 changes: 185 additions & 0 deletions src/converters/claude-to-codebuddy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import fs, { type Dirent } from "fs"
import path from "path"
import { formatFrontmatter } from "../utils/frontmatter"
import { type ClaudeAgent, type ClaudeCommand, type ClaudePlugin, filterSkillsByPlatform } from "../types/claude"
import type { CodeBuddyAgent, CodeBuddyBundle, CodeBuddyGeneratedSkill, CodeBuddyGeneratedSkillSidecarDir } from "../types/codebuddy"
import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"

export type ClaudeToCodeBuddyOptions = ClaudeToOpenCodeOptions

const CODEBUDDY_DESCRIPTION_MAX_LENGTH = 1024

export function convertClaudeToCodeBuddy(
plugin: ClaudePlugin,
options: ClaudeToCodeBuddyOptions,
): CodeBuddyBundle {
const includeSkills = options.codexIncludeSkills ?? false

const platformSkills = filterSkillsByPlatform(plugin.skills, "codebuddy")
const invocableCommands = plugin.commands.filter((command) => !command.disableModelInvocation)
const copiedSkills = platformSkills
const skillDirs = copiedSkills.map((skill) => ({
name: skill.name,
sourceDir: skill.sourceDir,
}))
const promptNames = new Set<string>()
const usedSkillNames = new Set<string>(skillDirs.map((skill) => normalizeCodeBuddyName(skill.name)))

const commandPromptNames = new Map<string, string>()
for (const command of invocableCommands) {
commandPromptNames.set(
command.name,
uniqueName(normalizeCodeBuddyName(command.name), promptNames),
)
}

const agents = plugin.agents.map(convertAgent)

if (!includeSkills) {
const externallyManagedSkillNames = copiedSkills.map((skill) => skill.name)
return {
pluginName: plugin.manifest.name,
prompts: [],
skillDirs: [],
generatedSkills: [],
agents,
mcpServers: undefined,
hooks: plugin.hooks,
externallyManagedSkillNames,
}
}

const commandSkills: CodeBuddyGeneratedSkill[] = []
const prompts = invocableCommands.map((command) => {
const promptName = commandPromptNames.get(command.name)!
const commandSkill = convertCommandSkill(command, usedSkillNames)
commandSkills.push(commandSkill)
const content = renderPrompt(command, commandSkill.name)
return { name: promptName, content }
})

return {
pluginName: plugin.manifest.name,
prompts,
skillDirs,
generatedSkills: [...commandSkills],
agents,
mcpServers: plugin.mcpServers,
hooks: plugin.hooks,
}
}

function convertAgent(agent: ClaudeAgent): CodeBuddyAgent {
const name = buildCodeBuddyAgentName(agent)
const description = sanitizeDescription(
agent.description ?? `Converted from Claude agent ${agent.name}`,
)
let instructions = agent.body.trim()
if (agent.capabilities && agent.capabilities.length > 0) {
const capabilities = agent.capabilities.map((capability) => `- ${capability}`).join("\n")
instructions = `## Capabilities\n${capabilities}\n\n${instructions}`.trim()
}
if (instructions.length === 0) {
instructions = `Instructions converted from the ${agent.name} agent.`
}

return { name, description, instructions, sidecarDirs: collectReferencedSidecarDirs(agent) }
}

function convertCommandSkill(
command: ClaudeCommand,
usedNames: Set<string>,
): CodeBuddyGeneratedSkill {
const name = uniqueName(normalizeCodeBuddyName(command.name), usedNames)
const frontmatter: Record<string, unknown> = {
name,
description: sanitizeDescription(
command.description ?? `Converted from Claude command ${command.name}`,
),
}
const sections: string[] = []
if (command.argumentHint) {
sections.push(`## Arguments\n${command.argumentHint}`)
}
if (command.allowedTools && command.allowedTools.length > 0) {
sections.push(`## Allowed tools\n${command.allowedTools.map((tool) => `- ${tool}`).join("\n")}`)
}
const body = [...sections, command.body.trim()].filter(Boolean).join("\n\n").trim()
const content = formatFrontmatter(frontmatter, body.length > 0 ? body : command.body)
return { name, content }
}

function renderPrompt(
command: ClaudeCommand,
skillName: string,
): string {
const frontmatter: Record<string, unknown> = {
description: command.description,
"argument-hint": command.argumentHint,
}
const instructions = `Use the /${skillName} skill for this command and follow its instructions.`
const body = [instructions, "", command.body].join("\n").trim()
return formatFrontmatter(frontmatter, body)
}

function buildCodeBuddyAgentName(agent: ClaudeAgent): string {
const category = getAgentCategory(agent)
const agentName = normalizeCodeBuddyName(agent.name)
return category ? `${normalizeCodeBuddyName(category)}-${agentName}` : agentName
}

function getAgentCategory(agent: ClaudeAgent): string | null {
const parts = agent.sourcePath.split(path.sep)
const agentsIndex = parts.lastIndexOf("agents")
if (agentsIndex === -1) return null
const next = parts[agentsIndex + 1]
if (!next || next.endsWith(".md")) return null
return next
}

function sanitizeDescription(value: string, maxLength = CODEBUDDY_DESCRIPTION_MAX_LENGTH): string {
const normalized = value.replace(/\s+/g, " ").trim()
if (normalized.length <= maxLength) return normalized
const ellipsis = "..."
return normalized.slice(0, Math.max(0, maxLength - ellipsis.length)).trimEnd() + ellipsis
}

function normalizeCodeBuddyName(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9-]+/g, "-")
.replace(/^-+|-+$/g, "")
}

function uniqueName(base: string, used: Set<string>): string {
if (!used.has(base)) {
used.add(base)
return base
}
let index = 2
while (used.has(`${base}-${index}`)) {
index += 1
}
const name = `${base}-${index}`
used.add(name)
return name
}

function collectReferencedSidecarDirs(agent: ClaudeAgent): CodeBuddyGeneratedSkillSidecarDir[] {
const sourceDir = path.dirname(agent.sourcePath)
let entries: Dirent[]

try {
entries = fs.readdirSync(sourceDir, { withFileTypes: true })
} catch {
return []
}

return entries
.filter((entry) => entry.isDirectory())
.filter((entry) => agent.body.includes(`${entry.name}/`) || agent.body.includes(`\`${entry.name}\``))
.map((entry) => ({
sourceDir: path.join(sourceDir, entry.name),
targetName: entry.name,
}))
}
Loading