@every-env/compound-plugin 0.2.0 → 0.5.0
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 +2 -2
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/deploy-docs.yml +3 -3
- package/.github/workflows/publish.yml +37 -0
- package/README.md +12 -3
- package/docs/index.html +13 -13
- package/docs/pages/changelog.html +39 -0
- package/docs/plans/2026-02-08-feat-convert-local-md-settings-for-opencode-codex-plan.md +143 -0
- package/docs/plans/2026-02-08-feat-simplify-plugin-settings-plan.md +195 -0
- package/docs/plans/2026-02-08-refactor-reduce-plugin-context-token-usage-plan.md +212 -0
- package/docs/plans/2026-02-09-refactor-dspy-ruby-skill-update-plan.md +104 -0
- package/docs/plans/2026-02-12-feat-add-cursor-cli-target-provider-plan.md +306 -0
- package/docs/specs/cursor.md +85 -0
- package/package.json +1 -1
- package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
- package/plugins/compound-engineering/CHANGELOG.md +64 -0
- package/plugins/compound-engineering/README.md +5 -3
- package/plugins/compound-engineering/agents/design/design-implementation-reviewer.md +16 -1
- package/plugins/compound-engineering/agents/design/design-iterator.md +28 -1
- package/plugins/compound-engineering/agents/design/figma-design-sync.md +19 -1
- package/plugins/compound-engineering/agents/docs/ankane-readme-writer.md +16 -1
- package/plugins/compound-engineering/agents/research/best-practices-researcher.md +16 -1
- package/plugins/compound-engineering/agents/research/framework-docs-researcher.md +16 -1
- package/plugins/compound-engineering/agents/research/git-history-analyzer.md +16 -1
- package/plugins/compound-engineering/agents/research/learnings-researcher.md +22 -1
- package/plugins/compound-engineering/agents/research/repo-research-analyst.md +22 -1
- package/plugins/compound-engineering/agents/review/agent-native-reviewer.md +16 -1
- package/plugins/compound-engineering/agents/review/architecture-strategist.md +16 -1
- package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +16 -1
- package/plugins/compound-engineering/agents/review/data-integrity-guardian.md +16 -1
- package/plugins/compound-engineering/agents/review/data-migration-expert.md +16 -1
- package/plugins/compound-engineering/agents/review/deployment-verification-agent.md +16 -1
- package/plugins/compound-engineering/agents/review/dhh-rails-reviewer.md +22 -1
- package/plugins/compound-engineering/agents/review/julik-frontend-races-reviewer.md +20 -21
- package/plugins/compound-engineering/agents/review/kieran-python-reviewer.md +30 -1
- package/plugins/compound-engineering/agents/review/kieran-rails-reviewer.md +30 -1
- package/plugins/compound-engineering/agents/review/kieran-typescript-reviewer.md +30 -1
- package/plugins/compound-engineering/agents/review/pattern-recognition-specialist.md +16 -1
- package/plugins/compound-engineering/agents/review/performance-oracle.md +28 -1
- package/plugins/compound-engineering/agents/review/schema-drift-detector.md +16 -1
- package/plugins/compound-engineering/agents/review/security-sentinel.md +22 -1
- package/plugins/compound-engineering/agents/workflow/bug-reproduction-validator.md +16 -1
- package/plugins/compound-engineering/agents/workflow/every-style-editor.md +1 -1
- package/plugins/compound-engineering/agents/workflow/pr-comment-resolver.md +16 -1
- package/plugins/compound-engineering/agents/workflow/spec-flow-analyzer.md +22 -1
- package/plugins/compound-engineering/commands/agent-native-audit.md +1 -0
- package/plugins/compound-engineering/commands/changelog.md +1 -0
- package/plugins/compound-engineering/commands/create-agent-skill.md +1 -0
- package/plugins/compound-engineering/commands/deploy-docs.md +1 -0
- package/plugins/compound-engineering/commands/generate_command.md +1 -0
- package/plugins/compound-engineering/commands/heal-skill.md +1 -0
- package/plugins/compound-engineering/commands/lfg.md +1 -0
- package/plugins/compound-engineering/commands/report-bug.md +1 -0
- package/plugins/compound-engineering/commands/reproduce-bug.md +1 -0
- package/plugins/compound-engineering/commands/resolve_parallel.md +1 -0
- package/plugins/compound-engineering/commands/slfg.md +1 -0
- package/plugins/compound-engineering/commands/{xcode-test.md → test-xcode.md} +2 -1
- package/plugins/compound-engineering/commands/triage.md +1 -0
- package/plugins/compound-engineering/commands/workflows/brainstorm.md +6 -1
- package/plugins/compound-engineering/commands/workflows/compound.md +1 -0
- package/plugins/compound-engineering/commands/workflows/review.md +23 -21
- package/plugins/compound-engineering/commands/workflows/work.md +29 -15
- package/plugins/compound-engineering/skills/compound-docs/SKILL.md +1 -0
- package/plugins/compound-engineering/skills/dspy-ruby/SKILL.md +539 -396
- package/plugins/compound-engineering/skills/dspy-ruby/assets/config-template.rb +159 -331
- package/plugins/compound-engineering/skills/dspy-ruby/assets/module-template.rb +210 -236
- package/plugins/compound-engineering/skills/dspy-ruby/assets/signature-template.rb +173 -95
- package/plugins/compound-engineering/skills/dspy-ruby/references/core-concepts.md +552 -143
- package/plugins/compound-engineering/skills/dspy-ruby/references/observability.md +366 -0
- package/plugins/compound-engineering/skills/dspy-ruby/references/optimization.md +440 -460
- package/plugins/compound-engineering/skills/dspy-ruby/references/providers.md +305 -225
- package/plugins/compound-engineering/skills/dspy-ruby/references/toolsets.md +502 -0
- package/plugins/compound-engineering/skills/file-todos/SKILL.md +1 -0
- package/plugins/compound-engineering/skills/orchestrating-swarms/SKILL.md +1 -0
- package/plugins/compound-engineering/skills/setup/SKILL.md +168 -0
- package/plugins/compound-engineering/skills/skill-creator/SKILL.md +1 -0
- package/src/commands/convert.ts +10 -5
- package/src/commands/install.ts +10 -5
- package/src/converters/claude-to-codex.ts +9 -3
- package/src/converters/claude-to-cursor.ts +166 -0
- package/src/converters/claude-to-droid.ts +174 -0
- package/src/converters/claude-to-opencode.ts +9 -2
- package/src/parsers/claude.ts +4 -0
- package/src/targets/cursor.ts +48 -0
- package/src/targets/droid.ts +50 -0
- package/src/targets/index.ts +18 -0
- package/src/types/claude.ts +2 -0
- package/src/types/cursor.ts +29 -0
- package/src/types/droid.ts +20 -0
- package/tests/claude-parser.test.ts +24 -2
- package/tests/codex-converter.test.ts +100 -0
- package/tests/converter.test.ts +76 -0
- package/tests/cursor-converter.test.ts +347 -0
- package/tests/cursor-writer.test.ts +137 -0
- package/tests/droid-converter.test.ts +277 -0
- package/tests/droid-writer.test.ts +100 -0
- package/tests/fixtures/sample-plugin/commands/disabled-command.md +7 -0
- package/tests/fixtures/sample-plugin/skills/disabled-skill/SKILL.md +7 -0
- package/plugins/compound-engineering/commands/technical_review.md +0 -7
- /package/{plugins/compound-engineering → .claude}/commands/release-docs.md +0 -0
package/src/commands/convert.ts
CHANGED
|
@@ -22,7 +22,7 @@ export default defineCommand({
|
|
|
22
22
|
to: {
|
|
23
23
|
type: "string",
|
|
24
24
|
default: "opencode",
|
|
25
|
-
description: "Target format (opencode | codex)",
|
|
25
|
+
description: "Target format (opencode | codex | droid | cursor)",
|
|
26
26
|
},
|
|
27
27
|
output: {
|
|
28
28
|
type: "string",
|
|
@@ -80,7 +80,7 @@ export default defineCommand({
|
|
|
80
80
|
permissions: permissions as PermissionMode,
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
const primaryOutputRoot = targetName
|
|
83
|
+
const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome)
|
|
84
84
|
const bundle = target.convert(plugin, options)
|
|
85
85
|
if (!bundle) {
|
|
86
86
|
throw new Error(`Target ${targetName} did not return a bundle.`)
|
|
@@ -106,9 +106,7 @@ export default defineCommand({
|
|
|
106
106
|
console.warn(`Skipping ${extra}: no output returned.`)
|
|
107
107
|
continue
|
|
108
108
|
}
|
|
109
|
-
const extraRoot = extra
|
|
110
|
-
? codexHome
|
|
111
|
-
: path.join(outputRoot, extra)
|
|
109
|
+
const extraRoot = resolveTargetOutputRoot(extra, path.join(outputRoot, extra), codexHome)
|
|
112
110
|
await handler.write(extraRoot, extraBundle)
|
|
113
111
|
console.log(`Converted ${plugin.manifest.name} to ${extra} at ${extraRoot}`)
|
|
114
112
|
}
|
|
@@ -154,3 +152,10 @@ function resolveOutputRoot(value: unknown): string {
|
|
|
154
152
|
}
|
|
155
153
|
return process.cwd()
|
|
156
154
|
}
|
|
155
|
+
|
|
156
|
+
function resolveTargetOutputRoot(targetName: string, outputRoot: string, codexHome: string): string {
|
|
157
|
+
if (targetName === "codex") return codexHome
|
|
158
|
+
if (targetName === "droid") return path.join(os.homedir(), ".factory")
|
|
159
|
+
if (targetName === "cursor") return path.join(outputRoot, ".cursor")
|
|
160
|
+
return outputRoot
|
|
161
|
+
}
|
package/src/commands/install.ts
CHANGED
|
@@ -24,7 +24,7 @@ export default defineCommand({
|
|
|
24
24
|
to: {
|
|
25
25
|
type: "string",
|
|
26
26
|
default: "opencode",
|
|
27
|
-
description: "Target format (opencode | codex)",
|
|
27
|
+
description: "Target format (opencode | codex | droid | cursor)",
|
|
28
28
|
},
|
|
29
29
|
output: {
|
|
30
30
|
type: "string",
|
|
@@ -88,7 +88,7 @@ export default defineCommand({
|
|
|
88
88
|
if (!bundle) {
|
|
89
89
|
throw new Error(`Target ${targetName} did not return a bundle.`)
|
|
90
90
|
}
|
|
91
|
-
const primaryOutputRoot = targetName
|
|
91
|
+
const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome)
|
|
92
92
|
await target.write(primaryOutputRoot, bundle)
|
|
93
93
|
console.log(`Installed ${plugin.manifest.name} to ${primaryOutputRoot}`)
|
|
94
94
|
|
|
@@ -109,9 +109,7 @@ export default defineCommand({
|
|
|
109
109
|
console.warn(`Skipping ${extra}: no output returned.`)
|
|
110
110
|
continue
|
|
111
111
|
}
|
|
112
|
-
const extraRoot = extra
|
|
113
|
-
? codexHome
|
|
114
|
-
: path.join(outputRoot, extra)
|
|
112
|
+
const extraRoot = resolveTargetOutputRoot(extra, path.join(outputRoot, extra), codexHome)
|
|
115
113
|
await handler.write(extraRoot, extraBundle)
|
|
116
114
|
console.log(`Installed ${plugin.manifest.name} to ${extraRoot}`)
|
|
117
115
|
}
|
|
@@ -180,6 +178,13 @@ function resolveOutputRoot(value: unknown): string {
|
|
|
180
178
|
return path.join(os.homedir(), ".config", "opencode")
|
|
181
179
|
}
|
|
182
180
|
|
|
181
|
+
function resolveTargetOutputRoot(targetName: string, outputRoot: string, codexHome: string): string {
|
|
182
|
+
if (targetName === "codex") return codexHome
|
|
183
|
+
if (targetName === "droid") return path.join(os.homedir(), ".factory")
|
|
184
|
+
if (targetName === "cursor") return path.join(outputRoot, ".cursor")
|
|
185
|
+
return outputRoot
|
|
186
|
+
}
|
|
187
|
+
|
|
183
188
|
async function resolveGitHubPluginPath(pluginName: string): Promise<ResolvedPluginPath> {
|
|
184
189
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "compound-plugin-"))
|
|
185
190
|
const source = resolveGitHubSource()
|
|
@@ -19,7 +19,8 @@ export function convertClaudeToCodex(
|
|
|
19
19
|
|
|
20
20
|
const usedSkillNames = new Set<string>(skillDirs.map((skill) => normalizeName(skill.name)))
|
|
21
21
|
const commandSkills: CodexGeneratedSkill[] = []
|
|
22
|
-
const
|
|
22
|
+
const invocableCommands = plugin.commands.filter((command) => !command.disableModelInvocation)
|
|
23
|
+
const prompts = invocableCommands.map((command) => {
|
|
23
24
|
const promptName = uniqueName(normalizeName(command.name), promptNames)
|
|
24
25
|
const commandSkill = convertCommandSkill(command, usedSkillNames)
|
|
25
26
|
commandSkills.push(commandSkill)
|
|
@@ -45,7 +46,7 @@ function convertAgent(agent: ClaudeAgent, usedNames: Set<string>): CodexGenerate
|
|
|
45
46
|
)
|
|
46
47
|
const frontmatter: Record<string, unknown> = { name, description }
|
|
47
48
|
|
|
48
|
-
let body = agent.body.trim()
|
|
49
|
+
let body = transformContentForCodex(agent.body.trim())
|
|
49
50
|
if (agent.capabilities && agent.capabilities.length > 0) {
|
|
50
51
|
const capabilities = agent.capabilities.map((capability) => `- ${capability}`).join("\n")
|
|
51
52
|
body = `## Capabilities\n${capabilities}\n\n${body}`.trim()
|
|
@@ -120,7 +121,12 @@ function transformContentForCodex(body: string): string {
|
|
|
120
121
|
return `/prompts:${normalizedName}`
|
|
121
122
|
})
|
|
122
123
|
|
|
123
|
-
// 3.
|
|
124
|
+
// 3. Rewrite .claude/ paths to .codex/
|
|
125
|
+
result = result
|
|
126
|
+
.replace(/~\/\.claude\//g, "~/.codex/")
|
|
127
|
+
.replace(/\.claude\//g, ".codex/")
|
|
128
|
+
|
|
129
|
+
// 4. Transform @agent-name references
|
|
124
130
|
// Match: @agent-name in text (not emails)
|
|
125
131
|
const agentRefPattern = /@([a-z][a-z0-9-]*-(?:agent|reviewer|researcher|analyst|specialist|oracle|sentinel|guardian|strategist))/gi
|
|
126
132
|
result = result.replace(agentRefPattern, (_match, agentName: string) => {
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { formatFrontmatter } from "../utils/frontmatter"
|
|
2
|
+
import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude"
|
|
3
|
+
import type { CursorBundle, CursorCommand, CursorMcpServer, CursorRule } from "../types/cursor"
|
|
4
|
+
import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
|
|
5
|
+
|
|
6
|
+
export type ClaudeToCursorOptions = ClaudeToOpenCodeOptions
|
|
7
|
+
|
|
8
|
+
export function convertClaudeToCursor(
|
|
9
|
+
plugin: ClaudePlugin,
|
|
10
|
+
_options: ClaudeToCursorOptions,
|
|
11
|
+
): CursorBundle {
|
|
12
|
+
const usedRuleNames = new Set<string>()
|
|
13
|
+
const usedCommandNames = new Set<string>()
|
|
14
|
+
|
|
15
|
+
const rules = plugin.agents.map((agent) => convertAgentToRule(agent, usedRuleNames))
|
|
16
|
+
const commands = plugin.commands.map((command) => convertCommand(command, usedCommandNames))
|
|
17
|
+
const skillDirs = plugin.skills.map((skill) => ({
|
|
18
|
+
name: skill.name,
|
|
19
|
+
sourceDir: skill.sourceDir,
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
const mcpServers = convertMcpServers(plugin.mcpServers)
|
|
23
|
+
|
|
24
|
+
if (plugin.hooks && Object.keys(plugin.hooks.hooks).length > 0) {
|
|
25
|
+
console.warn("Warning: Cursor does not support hooks. Hooks were skipped during conversion.")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { rules, commands, skillDirs, mcpServers }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function convertAgentToRule(agent: ClaudeAgent, usedNames: Set<string>): CursorRule {
|
|
32
|
+
const name = uniqueName(normalizeName(agent.name), usedNames)
|
|
33
|
+
const description = agent.description ?? `Converted from Claude agent ${agent.name}`
|
|
34
|
+
|
|
35
|
+
const frontmatter: Record<string, unknown> = {
|
|
36
|
+
description,
|
|
37
|
+
alwaysApply: false,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let body = transformContentForCursor(agent.body.trim())
|
|
41
|
+
if (agent.capabilities && agent.capabilities.length > 0) {
|
|
42
|
+
const capabilities = agent.capabilities.map((c) => `- ${c}`).join("\n")
|
|
43
|
+
body = `## Capabilities\n${capabilities}\n\n${body}`.trim()
|
|
44
|
+
}
|
|
45
|
+
if (body.length === 0) {
|
|
46
|
+
body = `Instructions converted from the ${agent.name} agent.`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const content = formatFrontmatter(frontmatter, body)
|
|
50
|
+
return { name, content }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function convertCommand(command: ClaudeCommand, usedNames: Set<string>): CursorCommand {
|
|
54
|
+
const name = uniqueName(flattenCommandName(command.name), usedNames)
|
|
55
|
+
|
|
56
|
+
const sections: string[] = []
|
|
57
|
+
|
|
58
|
+
if (command.description) {
|
|
59
|
+
sections.push(`<!-- ${command.description} -->`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (command.argumentHint) {
|
|
63
|
+
sections.push(`## Arguments\n${command.argumentHint}`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const transformedBody = transformContentForCursor(command.body.trim())
|
|
67
|
+
sections.push(transformedBody)
|
|
68
|
+
|
|
69
|
+
const content = sections.filter(Boolean).join("\n\n").trim()
|
|
70
|
+
return { name, content }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Transform Claude Code content to Cursor-compatible content.
|
|
75
|
+
*
|
|
76
|
+
* 1. Task agent calls: Task agent-name(args) -> Use the agent-name skill to: args
|
|
77
|
+
* 2. Slash commands: /workflows:plan -> /plan (flatten namespace)
|
|
78
|
+
* 3. Path rewriting: .claude/ -> .cursor/
|
|
79
|
+
* 4. Agent references: @agent-name -> the agent-name rule
|
|
80
|
+
*/
|
|
81
|
+
export function transformContentForCursor(body: string): string {
|
|
82
|
+
let result = body
|
|
83
|
+
|
|
84
|
+
// 1. Transform Task agent calls
|
|
85
|
+
const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9-]*)\(([^)]+)\)/gm
|
|
86
|
+
result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => {
|
|
87
|
+
const skillName = normalizeName(agentName)
|
|
88
|
+
return `${prefix}Use the ${skillName} skill to: ${args.trim()}`
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// 2. Transform slash command references (flatten namespaces)
|
|
92
|
+
const slashCommandPattern = /(?<![:\w])\/([a-z][a-z0-9_:-]*?)(?=[\s,."')\]}`]|$)/gi
|
|
93
|
+
result = result.replace(slashCommandPattern, (match, commandName: string) => {
|
|
94
|
+
if (commandName.includes("/")) return match
|
|
95
|
+
if (["dev", "tmp", "etc", "usr", "var", "bin", "home"].includes(commandName)) return match
|
|
96
|
+
const flattened = flattenCommandName(commandName)
|
|
97
|
+
return `/${flattened}`
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// 3. Rewrite .claude/ paths to .cursor/
|
|
101
|
+
result = result
|
|
102
|
+
.replace(/~\/\.claude\//g, "~/.cursor/")
|
|
103
|
+
.replace(/\.claude\//g, ".cursor/")
|
|
104
|
+
|
|
105
|
+
// 4. Transform @agent-name references
|
|
106
|
+
const agentRefPattern = /@([a-z][a-z0-9-]*-(?:agent|reviewer|researcher|analyst|specialist|oracle|sentinel|guardian|strategist))/gi
|
|
107
|
+
result = result.replace(agentRefPattern, (_match, agentName: string) => {
|
|
108
|
+
return `the ${normalizeName(agentName)} rule`
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return result
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function convertMcpServers(
|
|
115
|
+
servers?: Record<string, ClaudeMcpServer>,
|
|
116
|
+
): Record<string, CursorMcpServer> | undefined {
|
|
117
|
+
if (!servers || Object.keys(servers).length === 0) return undefined
|
|
118
|
+
|
|
119
|
+
const result: Record<string, CursorMcpServer> = {}
|
|
120
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
121
|
+
const entry: CursorMcpServer = {}
|
|
122
|
+
if (server.command) {
|
|
123
|
+
entry.command = server.command
|
|
124
|
+
if (server.args && server.args.length > 0) entry.args = server.args
|
|
125
|
+
if (server.env && Object.keys(server.env).length > 0) entry.env = server.env
|
|
126
|
+
} else if (server.url) {
|
|
127
|
+
entry.url = server.url
|
|
128
|
+
if (server.headers && Object.keys(server.headers).length > 0) entry.headers = server.headers
|
|
129
|
+
}
|
|
130
|
+
result[name] = entry
|
|
131
|
+
}
|
|
132
|
+
return result
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function flattenCommandName(name: string): string {
|
|
136
|
+
const colonIndex = name.lastIndexOf(":")
|
|
137
|
+
const base = colonIndex >= 0 ? name.slice(colonIndex + 1) : name
|
|
138
|
+
return normalizeName(base)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeName(value: string): string {
|
|
142
|
+
const trimmed = value.trim()
|
|
143
|
+
if (!trimmed) return "item"
|
|
144
|
+
const normalized = trimmed
|
|
145
|
+
.toLowerCase()
|
|
146
|
+
.replace(/[\\/]+/g, "-")
|
|
147
|
+
.replace(/[:\s]+/g, "-")
|
|
148
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
149
|
+
.replace(/-+/g, "-")
|
|
150
|
+
.replace(/^-+|-+$/g, "")
|
|
151
|
+
return normalized || "item"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function uniqueName(base: string, used: Set<string>): string {
|
|
155
|
+
if (!used.has(base)) {
|
|
156
|
+
used.add(base)
|
|
157
|
+
return base
|
|
158
|
+
}
|
|
159
|
+
let index = 2
|
|
160
|
+
while (used.has(`${base}-${index}`)) {
|
|
161
|
+
index += 1
|
|
162
|
+
}
|
|
163
|
+
const name = `${base}-${index}`
|
|
164
|
+
used.add(name)
|
|
165
|
+
return name
|
|
166
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { formatFrontmatter } from "../utils/frontmatter"
|
|
2
|
+
import type { ClaudeAgent, ClaudeCommand, ClaudePlugin } from "../types/claude"
|
|
3
|
+
import type { DroidBundle, DroidCommandFile, DroidAgentFile } from "../types/droid"
|
|
4
|
+
import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
|
|
5
|
+
|
|
6
|
+
export type ClaudeToDroidOptions = ClaudeToOpenCodeOptions
|
|
7
|
+
|
|
8
|
+
const CLAUDE_TO_DROID_TOOLS: Record<string, string> = {
|
|
9
|
+
read: "Read",
|
|
10
|
+
write: "Create",
|
|
11
|
+
edit: "Edit",
|
|
12
|
+
multiedit: "Edit",
|
|
13
|
+
bash: "Execute",
|
|
14
|
+
grep: "Grep",
|
|
15
|
+
glob: "Glob",
|
|
16
|
+
list: "LS",
|
|
17
|
+
ls: "LS",
|
|
18
|
+
webfetch: "FetchUrl",
|
|
19
|
+
websearch: "WebSearch",
|
|
20
|
+
task: "Task",
|
|
21
|
+
todowrite: "TodoWrite",
|
|
22
|
+
todoread: "TodoWrite",
|
|
23
|
+
question: "AskUser",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const VALID_DROID_TOOLS = new Set([
|
|
27
|
+
"Read",
|
|
28
|
+
"LS",
|
|
29
|
+
"Grep",
|
|
30
|
+
"Glob",
|
|
31
|
+
"Create",
|
|
32
|
+
"Edit",
|
|
33
|
+
"ApplyPatch",
|
|
34
|
+
"Execute",
|
|
35
|
+
"WebSearch",
|
|
36
|
+
"FetchUrl",
|
|
37
|
+
"TodoWrite",
|
|
38
|
+
"Task",
|
|
39
|
+
"AskUser",
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
export function convertClaudeToDroid(
|
|
43
|
+
plugin: ClaudePlugin,
|
|
44
|
+
_options: ClaudeToDroidOptions,
|
|
45
|
+
): DroidBundle {
|
|
46
|
+
const commands = plugin.commands.map((command) => convertCommand(command))
|
|
47
|
+
const droids = plugin.agents.map((agent) => convertAgent(agent))
|
|
48
|
+
const skillDirs = plugin.skills.map((skill) => ({
|
|
49
|
+
name: skill.name,
|
|
50
|
+
sourceDir: skill.sourceDir,
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
return { commands, droids, skillDirs }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function convertCommand(command: ClaudeCommand): DroidCommandFile {
|
|
57
|
+
const name = flattenCommandName(command.name)
|
|
58
|
+
const frontmatter: Record<string, unknown> = {
|
|
59
|
+
description: command.description,
|
|
60
|
+
}
|
|
61
|
+
if (command.argumentHint) {
|
|
62
|
+
frontmatter["argument-hint"] = command.argumentHint
|
|
63
|
+
}
|
|
64
|
+
if (command.disableModelInvocation) {
|
|
65
|
+
frontmatter["disable-model-invocation"] = true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const body = transformContentForDroid(command.body.trim())
|
|
69
|
+
const content = formatFrontmatter(frontmatter, body)
|
|
70
|
+
return { name, content }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function convertAgent(agent: ClaudeAgent): DroidAgentFile {
|
|
74
|
+
const name = normalizeName(agent.name)
|
|
75
|
+
const frontmatter: Record<string, unknown> = {
|
|
76
|
+
name,
|
|
77
|
+
description: agent.description,
|
|
78
|
+
model: agent.model && agent.model !== "inherit" ? agent.model : "inherit",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const tools = mapAgentTools(agent)
|
|
82
|
+
if (tools) {
|
|
83
|
+
frontmatter.tools = tools
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let body = agent.body.trim()
|
|
87
|
+
if (agent.capabilities && agent.capabilities.length > 0) {
|
|
88
|
+
const capabilities = agent.capabilities.map((c) => `- ${c}`).join("\n")
|
|
89
|
+
body = `## Capabilities\n${capabilities}\n\n${body}`.trim()
|
|
90
|
+
}
|
|
91
|
+
if (body.length === 0) {
|
|
92
|
+
body = `Instructions converted from the ${agent.name} agent.`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
body = transformContentForDroid(body)
|
|
96
|
+
|
|
97
|
+
const content = formatFrontmatter(frontmatter, body)
|
|
98
|
+
return { name, content }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function mapAgentTools(agent: ClaudeAgent): string[] | undefined {
|
|
102
|
+
const bodyLower = `${agent.name} ${agent.description ?? ""} ${agent.body}`.toLowerCase()
|
|
103
|
+
|
|
104
|
+
const mentionedTools = new Set<string>()
|
|
105
|
+
for (const [claudeTool, droidTool] of Object.entries(CLAUDE_TO_DROID_TOOLS)) {
|
|
106
|
+
if (bodyLower.includes(claudeTool)) {
|
|
107
|
+
mentionedTools.add(droidTool)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (mentionedTools.size === 0) return undefined
|
|
112
|
+
return [...mentionedTools].filter((t) => VALID_DROID_TOOLS.has(t)).sort()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Transform Claude Code content to Factory Droid-compatible content.
|
|
117
|
+
*
|
|
118
|
+
* 1. Slash commands: /workflows:plan → /plan, /command-name stays as-is
|
|
119
|
+
* 2. Task agent calls: Task agent-name(args) → Task agent-name: args
|
|
120
|
+
* 3. Agent references: @agent-name → the agent-name droid
|
|
121
|
+
*/
|
|
122
|
+
function transformContentForDroid(body: string): string {
|
|
123
|
+
let result = body
|
|
124
|
+
|
|
125
|
+
// 1. Transform Task agent calls
|
|
126
|
+
// Match: Task repo-research-analyst(feature_description)
|
|
127
|
+
const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9-]*)\(([^)]+)\)/gm
|
|
128
|
+
result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => {
|
|
129
|
+
const name = normalizeName(agentName)
|
|
130
|
+
return `${prefix}Task ${name}: ${args.trim()}`
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// 2. Transform slash command references
|
|
134
|
+
// /workflows:plan → /plan, /command-name stays as-is
|
|
135
|
+
const slashCommandPattern = /(?<![:\w])\/([a-z][a-z0-9_:-]*?)(?=[\s,."')\]}`]|$)/gi
|
|
136
|
+
result = result.replace(slashCommandPattern, (match, commandName: string) => {
|
|
137
|
+
if (commandName.includes('/')) return match
|
|
138
|
+
if (['dev', 'tmp', 'etc', 'usr', 'var', 'bin', 'home'].includes(commandName)) return match
|
|
139
|
+
const flattened = flattenCommandName(commandName)
|
|
140
|
+
return `/${flattened}`
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// 3. Transform @agent-name references to droid references
|
|
144
|
+
const agentRefPattern = /@agent-([a-z][a-z0-9-]*)/gi
|
|
145
|
+
result = result.replace(agentRefPattern, (_match, agentName: string) => {
|
|
146
|
+
return `the ${normalizeName(agentName)} droid`
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
return result
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Flatten a command name by stripping the namespace prefix.
|
|
154
|
+
* "workflows:plan" → "plan"
|
|
155
|
+
* "plan_review" → "plan_review"
|
|
156
|
+
*/
|
|
157
|
+
function flattenCommandName(name: string): string {
|
|
158
|
+
const colonIndex = name.lastIndexOf(":")
|
|
159
|
+
const base = colonIndex >= 0 ? name.slice(colonIndex + 1) : name
|
|
160
|
+
return normalizeName(base)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizeName(value: string): string {
|
|
164
|
+
const trimmed = value.trim()
|
|
165
|
+
if (!trimmed) return "item"
|
|
166
|
+
const normalized = trimmed
|
|
167
|
+
.toLowerCase()
|
|
168
|
+
.replace(/[\\/]+/g, "-")
|
|
169
|
+
.replace(/[:\s]+/g, "-")
|
|
170
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
171
|
+
.replace(/-+/g, "-")
|
|
172
|
+
.replace(/^-+|-+$/g, "")
|
|
173
|
+
return normalized || "item"
|
|
174
|
+
}
|
|
@@ -103,7 +103,7 @@ function convertAgent(agent: ClaudeAgent, options: ClaudeToOpenCodeOptions) {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
const content = formatFrontmatter(frontmatter, agent.body)
|
|
106
|
+
const content = formatFrontmatter(frontmatter, rewriteClaudePaths(agent.body))
|
|
107
107
|
|
|
108
108
|
return {
|
|
109
109
|
name: agent.name,
|
|
@@ -114,9 +114,10 @@ function convertAgent(agent: ClaudeAgent, options: ClaudeToOpenCodeOptions) {
|
|
|
114
114
|
function convertCommands(commands: ClaudeCommand[]): Record<string, OpenCodeCommandConfig> {
|
|
115
115
|
const result: Record<string, OpenCodeCommandConfig> = {}
|
|
116
116
|
for (const command of commands) {
|
|
117
|
+
if (command.disableModelInvocation) continue
|
|
117
118
|
const entry: OpenCodeCommandConfig = {
|
|
118
119
|
description: command.description,
|
|
119
|
-
template: command.body,
|
|
120
|
+
template: rewriteClaudePaths(command.body),
|
|
120
121
|
}
|
|
121
122
|
if (command.model && command.model !== "inherit") {
|
|
122
123
|
entry.model = normalizeModel(command.model)
|
|
@@ -243,6 +244,12 @@ function renderHookStatements(
|
|
|
243
244
|
return statements
|
|
244
245
|
}
|
|
245
246
|
|
|
247
|
+
function rewriteClaudePaths(body: string): string {
|
|
248
|
+
return body
|
|
249
|
+
.replace(/~\/\.claude\//g, "~/.config/opencode/")
|
|
250
|
+
.replace(/\.claude\//g, ".opencode/")
|
|
251
|
+
}
|
|
252
|
+
|
|
246
253
|
function normalizeModel(model: string): string {
|
|
247
254
|
if (model.includes("/")) return model
|
|
248
255
|
if (/^claude-/.test(model)) return `anthropic/${model}`
|
package/src/parsers/claude.ts
CHANGED
|
@@ -83,12 +83,14 @@ async function loadCommands(commandsDirs: string[]): Promise<ClaudeCommand[]> {
|
|
|
83
83
|
const { data, body } = parseFrontmatter(raw)
|
|
84
84
|
const name = (data.name as string) ?? path.basename(file, ".md")
|
|
85
85
|
const allowedTools = parseAllowedTools(data["allowed-tools"])
|
|
86
|
+
const disableModelInvocation = data["disable-model-invocation"] === true ? true : undefined
|
|
86
87
|
commands.push({
|
|
87
88
|
name,
|
|
88
89
|
description: data.description as string | undefined,
|
|
89
90
|
argumentHint: data["argument-hint"] as string | undefined,
|
|
90
91
|
model: data.model as string | undefined,
|
|
91
92
|
allowedTools,
|
|
93
|
+
disableModelInvocation,
|
|
92
94
|
body: body.trim(),
|
|
93
95
|
sourcePath: file,
|
|
94
96
|
})
|
|
@@ -104,9 +106,11 @@ async function loadSkills(skillsDirs: string[]): Promise<ClaudeSkill[]> {
|
|
|
104
106
|
const raw = await readText(file)
|
|
105
107
|
const { data } = parseFrontmatter(raw)
|
|
106
108
|
const name = (data.name as string) ?? path.basename(path.dirname(file))
|
|
109
|
+
const disableModelInvocation = data["disable-model-invocation"] === true ? true : undefined
|
|
107
110
|
skills.push({
|
|
108
111
|
name,
|
|
109
112
|
description: data.description as string | undefined,
|
|
113
|
+
disableModelInvocation,
|
|
110
114
|
sourceDir: path.dirname(file),
|
|
111
115
|
skillPath: file,
|
|
112
116
|
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { backupFile, copyDir, ensureDir, writeJson, writeText } from "../utils/files"
|
|
3
|
+
import type { CursorBundle } from "../types/cursor"
|
|
4
|
+
|
|
5
|
+
export async function writeCursorBundle(outputRoot: string, bundle: CursorBundle): Promise<void> {
|
|
6
|
+
const paths = resolveCursorPaths(outputRoot)
|
|
7
|
+
await ensureDir(paths.cursorDir)
|
|
8
|
+
|
|
9
|
+
if (bundle.rules.length > 0) {
|
|
10
|
+
const rulesDir = path.join(paths.cursorDir, "rules")
|
|
11
|
+
for (const rule of bundle.rules) {
|
|
12
|
+
await writeText(path.join(rulesDir, `${rule.name}.mdc`), rule.content + "\n")
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (bundle.commands.length > 0) {
|
|
17
|
+
const commandsDir = path.join(paths.cursorDir, "commands")
|
|
18
|
+
for (const command of bundle.commands) {
|
|
19
|
+
await writeText(path.join(commandsDir, `${command.name}.md`), command.content + "\n")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (bundle.skillDirs.length > 0) {
|
|
24
|
+
const skillsDir = path.join(paths.cursorDir, "skills")
|
|
25
|
+
for (const skill of bundle.skillDirs) {
|
|
26
|
+
await copyDir(skill.sourceDir, path.join(skillsDir, skill.name))
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (bundle.mcpServers && Object.keys(bundle.mcpServers).length > 0) {
|
|
31
|
+
const mcpPath = path.join(paths.cursorDir, "mcp.json")
|
|
32
|
+
const backupPath = await backupFile(mcpPath)
|
|
33
|
+
if (backupPath) {
|
|
34
|
+
console.log(`Backed up existing mcp.json to ${backupPath}`)
|
|
35
|
+
}
|
|
36
|
+
await writeJson(mcpPath, { mcpServers: bundle.mcpServers })
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveCursorPaths(outputRoot: string) {
|
|
41
|
+
const base = path.basename(outputRoot)
|
|
42
|
+
// If already pointing at .cursor, write directly into it
|
|
43
|
+
if (base === ".cursor") {
|
|
44
|
+
return { cursorDir: outputRoot }
|
|
45
|
+
}
|
|
46
|
+
// Otherwise nest under .cursor
|
|
47
|
+
return { cursorDir: path.join(outputRoot, ".cursor") }
|
|
48
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { copyDir, ensureDir, writeText } from "../utils/files"
|
|
3
|
+
import type { DroidBundle } from "../types/droid"
|
|
4
|
+
|
|
5
|
+
export async function writeDroidBundle(outputRoot: string, bundle: DroidBundle): Promise<void> {
|
|
6
|
+
const paths = resolveDroidPaths(outputRoot)
|
|
7
|
+
await ensureDir(paths.root)
|
|
8
|
+
|
|
9
|
+
if (bundle.commands.length > 0) {
|
|
10
|
+
await ensureDir(paths.commandsDir)
|
|
11
|
+
for (const command of bundle.commands) {
|
|
12
|
+
await writeText(path.join(paths.commandsDir, `${command.name}.md`), command.content + "\n")
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (bundle.droids.length > 0) {
|
|
17
|
+
await ensureDir(paths.droidsDir)
|
|
18
|
+
for (const droid of bundle.droids) {
|
|
19
|
+
await writeText(path.join(paths.droidsDir, `${droid.name}.md`), droid.content + "\n")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (bundle.skillDirs.length > 0) {
|
|
24
|
+
await ensureDir(paths.skillsDir)
|
|
25
|
+
for (const skill of bundle.skillDirs) {
|
|
26
|
+
await copyDir(skill.sourceDir, path.join(paths.skillsDir, skill.name))
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveDroidPaths(outputRoot: string) {
|
|
32
|
+
const base = path.basename(outputRoot)
|
|
33
|
+
// If pointing directly at ~/.factory or .factory, write into it
|
|
34
|
+
if (base === ".factory") {
|
|
35
|
+
return {
|
|
36
|
+
root: outputRoot,
|
|
37
|
+
commandsDir: path.join(outputRoot, "commands"),
|
|
38
|
+
droidsDir: path.join(outputRoot, "droids"),
|
|
39
|
+
skillsDir: path.join(outputRoot, "skills"),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Otherwise nest under .factory
|
|
44
|
+
return {
|
|
45
|
+
root: outputRoot,
|
|
46
|
+
commandsDir: path.join(outputRoot, ".factory", "commands"),
|
|
47
|
+
droidsDir: path.join(outputRoot, ".factory", "droids"),
|
|
48
|
+
skillsDir: path.join(outputRoot, ".factory", "skills"),
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/targets/index.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { ClaudePlugin } from "../types/claude"
|
|
2
2
|
import type { OpenCodeBundle } from "../types/opencode"
|
|
3
3
|
import type { CodexBundle } from "../types/codex"
|
|
4
|
+
import type { DroidBundle } from "../types/droid"
|
|
5
|
+
import type { CursorBundle } from "../types/cursor"
|
|
4
6
|
import { convertClaudeToOpenCode, type ClaudeToOpenCodeOptions } from "../converters/claude-to-opencode"
|
|
5
7
|
import { convertClaudeToCodex } from "../converters/claude-to-codex"
|
|
8
|
+
import { convertClaudeToDroid } from "../converters/claude-to-droid"
|
|
9
|
+
import { convertClaudeToCursor } from "../converters/claude-to-cursor"
|
|
6
10
|
import { writeOpenCodeBundle } from "./opencode"
|
|
7
11
|
import { writeCodexBundle } from "./codex"
|
|
12
|
+
import { writeDroidBundle } from "./droid"
|
|
13
|
+
import { writeCursorBundle } from "./cursor"
|
|
8
14
|
|
|
9
15
|
export type TargetHandler<TBundle = unknown> = {
|
|
10
16
|
name: string
|
|
@@ -26,4 +32,16 @@ export const targets: Record<string, TargetHandler> = {
|
|
|
26
32
|
convert: convertClaudeToCodex as TargetHandler<CodexBundle>["convert"],
|
|
27
33
|
write: writeCodexBundle as TargetHandler<CodexBundle>["write"],
|
|
28
34
|
},
|
|
35
|
+
droid: {
|
|
36
|
+
name: "droid",
|
|
37
|
+
implemented: true,
|
|
38
|
+
convert: convertClaudeToDroid as TargetHandler<DroidBundle>["convert"],
|
|
39
|
+
write: writeDroidBundle as TargetHandler<DroidBundle>["write"],
|
|
40
|
+
},
|
|
41
|
+
cursor: {
|
|
42
|
+
name: "cursor",
|
|
43
|
+
implemented: true,
|
|
44
|
+
convert: convertClaudeToCursor as TargetHandler<CursorBundle>["convert"],
|
|
45
|
+
write: writeCursorBundle as TargetHandler<CursorBundle>["write"],
|
|
46
|
+
},
|
|
29
47
|
}
|
package/src/types/claude.ts
CHANGED
|
@@ -39,6 +39,7 @@ export type ClaudeCommand = {
|
|
|
39
39
|
argumentHint?: string
|
|
40
40
|
model?: string
|
|
41
41
|
allowedTools?: string[]
|
|
42
|
+
disableModelInvocation?: boolean
|
|
42
43
|
body: string
|
|
43
44
|
sourcePath: string
|
|
44
45
|
}
|
|
@@ -46,6 +47,7 @@ export type ClaudeCommand = {
|
|
|
46
47
|
export type ClaudeSkill = {
|
|
47
48
|
name: string
|
|
48
49
|
description?: string
|
|
50
|
+
disableModelInvocation?: boolean
|
|
49
51
|
sourceDir: string
|
|
50
52
|
skillPath: string
|
|
51
53
|
}
|