@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.
Files changed (100) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.github/workflows/ci.yml +1 -1
  3. package/.github/workflows/deploy-docs.yml +3 -3
  4. package/.github/workflows/publish.yml +37 -0
  5. package/README.md +12 -3
  6. package/docs/index.html +13 -13
  7. package/docs/pages/changelog.html +39 -0
  8. package/docs/plans/2026-02-08-feat-convert-local-md-settings-for-opencode-codex-plan.md +143 -0
  9. package/docs/plans/2026-02-08-feat-simplify-plugin-settings-plan.md +195 -0
  10. package/docs/plans/2026-02-08-refactor-reduce-plugin-context-token-usage-plan.md +212 -0
  11. package/docs/plans/2026-02-09-refactor-dspy-ruby-skill-update-plan.md +104 -0
  12. package/docs/plans/2026-02-12-feat-add-cursor-cli-target-provider-plan.md +306 -0
  13. package/docs/specs/cursor.md +85 -0
  14. package/package.json +1 -1
  15. package/plugins/compound-engineering/.claude-plugin/plugin.json +2 -2
  16. package/plugins/compound-engineering/CHANGELOG.md +64 -0
  17. package/plugins/compound-engineering/README.md +5 -3
  18. package/plugins/compound-engineering/agents/design/design-implementation-reviewer.md +16 -1
  19. package/plugins/compound-engineering/agents/design/design-iterator.md +28 -1
  20. package/plugins/compound-engineering/agents/design/figma-design-sync.md +19 -1
  21. package/plugins/compound-engineering/agents/docs/ankane-readme-writer.md +16 -1
  22. package/plugins/compound-engineering/agents/research/best-practices-researcher.md +16 -1
  23. package/plugins/compound-engineering/agents/research/framework-docs-researcher.md +16 -1
  24. package/plugins/compound-engineering/agents/research/git-history-analyzer.md +16 -1
  25. package/plugins/compound-engineering/agents/research/learnings-researcher.md +22 -1
  26. package/plugins/compound-engineering/agents/research/repo-research-analyst.md +22 -1
  27. package/plugins/compound-engineering/agents/review/agent-native-reviewer.md +16 -1
  28. package/plugins/compound-engineering/agents/review/architecture-strategist.md +16 -1
  29. package/plugins/compound-engineering/agents/review/code-simplicity-reviewer.md +16 -1
  30. package/plugins/compound-engineering/agents/review/data-integrity-guardian.md +16 -1
  31. package/plugins/compound-engineering/agents/review/data-migration-expert.md +16 -1
  32. package/plugins/compound-engineering/agents/review/deployment-verification-agent.md +16 -1
  33. package/plugins/compound-engineering/agents/review/dhh-rails-reviewer.md +22 -1
  34. package/plugins/compound-engineering/agents/review/julik-frontend-races-reviewer.md +20 -21
  35. package/plugins/compound-engineering/agents/review/kieran-python-reviewer.md +30 -1
  36. package/plugins/compound-engineering/agents/review/kieran-rails-reviewer.md +30 -1
  37. package/plugins/compound-engineering/agents/review/kieran-typescript-reviewer.md +30 -1
  38. package/plugins/compound-engineering/agents/review/pattern-recognition-specialist.md +16 -1
  39. package/plugins/compound-engineering/agents/review/performance-oracle.md +28 -1
  40. package/plugins/compound-engineering/agents/review/schema-drift-detector.md +16 -1
  41. package/plugins/compound-engineering/agents/review/security-sentinel.md +22 -1
  42. package/plugins/compound-engineering/agents/workflow/bug-reproduction-validator.md +16 -1
  43. package/plugins/compound-engineering/agents/workflow/every-style-editor.md +1 -1
  44. package/plugins/compound-engineering/agents/workflow/pr-comment-resolver.md +16 -1
  45. package/plugins/compound-engineering/agents/workflow/spec-flow-analyzer.md +22 -1
  46. package/plugins/compound-engineering/commands/agent-native-audit.md +1 -0
  47. package/plugins/compound-engineering/commands/changelog.md +1 -0
  48. package/plugins/compound-engineering/commands/create-agent-skill.md +1 -0
  49. package/plugins/compound-engineering/commands/deploy-docs.md +1 -0
  50. package/plugins/compound-engineering/commands/generate_command.md +1 -0
  51. package/plugins/compound-engineering/commands/heal-skill.md +1 -0
  52. package/plugins/compound-engineering/commands/lfg.md +1 -0
  53. package/plugins/compound-engineering/commands/report-bug.md +1 -0
  54. package/plugins/compound-engineering/commands/reproduce-bug.md +1 -0
  55. package/plugins/compound-engineering/commands/resolve_parallel.md +1 -0
  56. package/plugins/compound-engineering/commands/slfg.md +1 -0
  57. package/plugins/compound-engineering/commands/{xcode-test.md → test-xcode.md} +2 -1
  58. package/plugins/compound-engineering/commands/triage.md +1 -0
  59. package/plugins/compound-engineering/commands/workflows/brainstorm.md +6 -1
  60. package/plugins/compound-engineering/commands/workflows/compound.md +1 -0
  61. package/plugins/compound-engineering/commands/workflows/review.md +23 -21
  62. package/plugins/compound-engineering/commands/workflows/work.md +29 -15
  63. package/plugins/compound-engineering/skills/compound-docs/SKILL.md +1 -0
  64. package/plugins/compound-engineering/skills/dspy-ruby/SKILL.md +539 -396
  65. package/plugins/compound-engineering/skills/dspy-ruby/assets/config-template.rb +159 -331
  66. package/plugins/compound-engineering/skills/dspy-ruby/assets/module-template.rb +210 -236
  67. package/plugins/compound-engineering/skills/dspy-ruby/assets/signature-template.rb +173 -95
  68. package/plugins/compound-engineering/skills/dspy-ruby/references/core-concepts.md +552 -143
  69. package/plugins/compound-engineering/skills/dspy-ruby/references/observability.md +366 -0
  70. package/plugins/compound-engineering/skills/dspy-ruby/references/optimization.md +440 -460
  71. package/plugins/compound-engineering/skills/dspy-ruby/references/providers.md +305 -225
  72. package/plugins/compound-engineering/skills/dspy-ruby/references/toolsets.md +502 -0
  73. package/plugins/compound-engineering/skills/file-todos/SKILL.md +1 -0
  74. package/plugins/compound-engineering/skills/orchestrating-swarms/SKILL.md +1 -0
  75. package/plugins/compound-engineering/skills/setup/SKILL.md +168 -0
  76. package/plugins/compound-engineering/skills/skill-creator/SKILL.md +1 -0
  77. package/src/commands/convert.ts +10 -5
  78. package/src/commands/install.ts +10 -5
  79. package/src/converters/claude-to-codex.ts +9 -3
  80. package/src/converters/claude-to-cursor.ts +166 -0
  81. package/src/converters/claude-to-droid.ts +174 -0
  82. package/src/converters/claude-to-opencode.ts +9 -2
  83. package/src/parsers/claude.ts +4 -0
  84. package/src/targets/cursor.ts +48 -0
  85. package/src/targets/droid.ts +50 -0
  86. package/src/targets/index.ts +18 -0
  87. package/src/types/claude.ts +2 -0
  88. package/src/types/cursor.ts +29 -0
  89. package/src/types/droid.ts +20 -0
  90. package/tests/claude-parser.test.ts +24 -2
  91. package/tests/codex-converter.test.ts +100 -0
  92. package/tests/converter.test.ts +76 -0
  93. package/tests/cursor-converter.test.ts +347 -0
  94. package/tests/cursor-writer.test.ts +137 -0
  95. package/tests/droid-converter.test.ts +277 -0
  96. package/tests/droid-writer.test.ts +100 -0
  97. package/tests/fixtures/sample-plugin/commands/disabled-command.md +7 -0
  98. package/tests/fixtures/sample-plugin/skills/disabled-skill/SKILL.md +7 -0
  99. package/plugins/compound-engineering/commands/technical_review.md +0 -7
  100. /package/{plugins/compound-engineering → .claude}/commands/release-docs.md +0 -0
@@ -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 === "codex" && codexHome ? codexHome : outputRoot
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 === "codex" && codexHome
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
+ }
@@ -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 === "codex" && codexHome ? codexHome : outputRoot
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 === "codex" && codexHome
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 prompts = plugin.commands.map((command) => {
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. Transform @agent-name references
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}`
@@ -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
+ }
@@ -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
  }
@@ -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
  }