@every-env/compound-plugin 0.5.1 → 0.7.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 +1 -1
- package/CHANGELOG.md +34 -0
- package/README.md +20 -3
- package/docs/plans/2026-02-14-feat-add-gemini-cli-target-provider-plan.md +370 -0
- package/docs/specs/gemini.md +122 -0
- package/package.json +1 -1
- package/plugins/compound-engineering/.claude-plugin/plugin.json +1 -1
- package/plugins/compound-engineering/CHANGELOG.md +17 -0
- package/plugins/compound-engineering/commands/workflows/plan.md +3 -0
- package/plugins/compound-engineering/commands/workflows/work.md +8 -1
- package/src/commands/convert.ts +14 -25
- package/src/commands/install.ts +28 -26
- package/src/commands/sync.ts +44 -21
- package/src/converters/claude-to-gemini.ts +193 -0
- package/src/converters/claude-to-opencode.ts +16 -0
- package/src/converters/claude-to-pi.ts +205 -0
- package/src/sync/cursor.ts +78 -0
- package/src/sync/droid.ts +21 -0
- package/src/sync/pi.ts +88 -0
- package/src/targets/gemini.ts +68 -0
- package/src/targets/index.ts +18 -0
- package/src/targets/pi.ts +131 -0
- package/src/templates/pi/compat-extension.ts +452 -0
- package/src/types/gemini.ts +29 -0
- package/src/types/pi.ts +40 -0
- package/src/utils/resolve-home.ts +17 -0
- package/tests/cli.test.ts +76 -0
- package/tests/converter.test.ts +29 -0
- package/tests/gemini-converter.test.ts +373 -0
- package/tests/gemini-writer.test.ts +181 -0
- package/tests/pi-converter.test.ts +116 -0
- package/tests/pi-writer.test.ts +99 -0
- package/tests/sync-cursor.test.ts +92 -0
- package/tests/sync-droid.test.ts +57 -0
- package/tests/sync-pi.test.ts +68 -0
package/src/commands/convert.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { loadClaudePlugin } from "../parsers/claude"
|
|
|
5
5
|
import { targets } from "../targets"
|
|
6
6
|
import type { PermissionMode } from "../converters/claude-to-opencode"
|
|
7
7
|
import { ensureCodexAgentsFile } from "../utils/codex-agents"
|
|
8
|
+
import { expandHome, resolveTargetHome } from "../utils/resolve-home"
|
|
8
9
|
|
|
9
10
|
const permissionModes: PermissionMode[] = ["none", "broad", "from-commands"]
|
|
10
11
|
|
|
@@ -22,7 +23,7 @@ export default defineCommand({
|
|
|
22
23
|
to: {
|
|
23
24
|
type: "string",
|
|
24
25
|
default: "opencode",
|
|
25
|
-
description: "Target format (opencode | codex | droid | cursor)",
|
|
26
|
+
description: "Target format (opencode | codex | droid | cursor | pi | gemini)",
|
|
26
27
|
},
|
|
27
28
|
output: {
|
|
28
29
|
type: "string",
|
|
@@ -34,6 +35,11 @@ export default defineCommand({
|
|
|
34
35
|
alias: "codex-home",
|
|
35
36
|
description: "Write Codex output to this .codex root (ex: ~/.codex)",
|
|
36
37
|
},
|
|
38
|
+
piHome: {
|
|
39
|
+
type: "string",
|
|
40
|
+
alias: "pi-home",
|
|
41
|
+
description: "Write Pi output to this Pi root (ex: ~/.pi/agent or ./.pi)",
|
|
42
|
+
},
|
|
37
43
|
also: {
|
|
38
44
|
type: "string",
|
|
39
45
|
description: "Comma-separated extra targets to generate (ex: codex)",
|
|
@@ -72,7 +78,8 @@ export default defineCommand({
|
|
|
72
78
|
|
|
73
79
|
const plugin = await loadClaudePlugin(String(args.source))
|
|
74
80
|
const outputRoot = resolveOutputRoot(args.output)
|
|
75
|
-
const codexHome =
|
|
81
|
+
const codexHome = resolveTargetHome(args.codexHome, path.join(os.homedir(), ".codex"))
|
|
82
|
+
const piHome = resolveTargetHome(args.piHome, path.join(os.homedir(), ".pi", "agent"))
|
|
76
83
|
|
|
77
84
|
const options = {
|
|
78
85
|
agentMode: String(args.agentMode) === "primary" ? "primary" : "subagent",
|
|
@@ -80,7 +87,7 @@ export default defineCommand({
|
|
|
80
87
|
permissions: permissions as PermissionMode,
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome)
|
|
90
|
+
const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome, piHome)
|
|
84
91
|
const bundle = target.convert(plugin, options)
|
|
85
92
|
if (!bundle) {
|
|
86
93
|
throw new Error(`Target ${targetName} did not return a bundle.`)
|
|
@@ -106,7 +113,7 @@ export default defineCommand({
|
|
|
106
113
|
console.warn(`Skipping ${extra}: no output returned.`)
|
|
107
114
|
continue
|
|
108
115
|
}
|
|
109
|
-
const extraRoot = resolveTargetOutputRoot(extra, path.join(outputRoot, extra), codexHome)
|
|
116
|
+
const extraRoot = resolveTargetOutputRoot(extra, path.join(outputRoot, extra), codexHome, piHome)
|
|
110
117
|
await handler.write(extraRoot, extraBundle)
|
|
111
118
|
console.log(`Converted ${plugin.manifest.name} to ${extra} at ${extraRoot}`)
|
|
112
119
|
}
|
|
@@ -125,26 +132,6 @@ function parseExtraTargets(value: unknown): string[] {
|
|
|
125
132
|
.filter(Boolean)
|
|
126
133
|
}
|
|
127
134
|
|
|
128
|
-
function resolveCodexHome(value: unknown): string | null {
|
|
129
|
-
if (!value) return null
|
|
130
|
-
const raw = String(value).trim()
|
|
131
|
-
if (!raw) return null
|
|
132
|
-
const expanded = expandHome(raw)
|
|
133
|
-
return path.resolve(expanded)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function resolveCodexRoot(value: unknown): string {
|
|
137
|
-
return resolveCodexHome(value) ?? path.join(os.homedir(), ".codex")
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function expandHome(value: string): string {
|
|
141
|
-
if (value === "~") return os.homedir()
|
|
142
|
-
if (value.startsWith(`~${path.sep}`)) {
|
|
143
|
-
return path.join(os.homedir(), value.slice(2))
|
|
144
|
-
}
|
|
145
|
-
return value
|
|
146
|
-
}
|
|
147
|
-
|
|
148
135
|
function resolveOutputRoot(value: unknown): string {
|
|
149
136
|
if (value && String(value).trim()) {
|
|
150
137
|
const expanded = expandHome(String(value).trim())
|
|
@@ -153,9 +140,11 @@ function resolveOutputRoot(value: unknown): string {
|
|
|
153
140
|
return process.cwd()
|
|
154
141
|
}
|
|
155
142
|
|
|
156
|
-
function resolveTargetOutputRoot(targetName: string, outputRoot: string, codexHome: string): string {
|
|
143
|
+
function resolveTargetOutputRoot(targetName: string, outputRoot: string, codexHome: string, piHome: string): string {
|
|
157
144
|
if (targetName === "codex") return codexHome
|
|
145
|
+
if (targetName === "pi") return piHome
|
|
158
146
|
if (targetName === "droid") return path.join(os.homedir(), ".factory")
|
|
159
147
|
if (targetName === "cursor") return path.join(outputRoot, ".cursor")
|
|
148
|
+
if (targetName === "gemini") return path.join(outputRoot, ".gemini")
|
|
160
149
|
return outputRoot
|
|
161
150
|
}
|
package/src/commands/install.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { targets } from "../targets"
|
|
|
7
7
|
import { pathExists } from "../utils/files"
|
|
8
8
|
import type { PermissionMode } from "../converters/claude-to-opencode"
|
|
9
9
|
import { ensureCodexAgentsFile } from "../utils/codex-agents"
|
|
10
|
+
import { expandHome, resolveTargetHome } from "../utils/resolve-home"
|
|
10
11
|
|
|
11
12
|
const permissionModes: PermissionMode[] = ["none", "broad", "from-commands"]
|
|
12
13
|
|
|
@@ -24,7 +25,7 @@ export default defineCommand({
|
|
|
24
25
|
to: {
|
|
25
26
|
type: "string",
|
|
26
27
|
default: "opencode",
|
|
27
|
-
description: "Target format (opencode | codex | droid | cursor)",
|
|
28
|
+
description: "Target format (opencode | codex | droid | cursor | pi | gemini)",
|
|
28
29
|
},
|
|
29
30
|
output: {
|
|
30
31
|
type: "string",
|
|
@@ -36,6 +37,11 @@ export default defineCommand({
|
|
|
36
37
|
alias: "codex-home",
|
|
37
38
|
description: "Write Codex output to this .codex root (ex: ~/.codex)",
|
|
38
39
|
},
|
|
40
|
+
piHome: {
|
|
41
|
+
type: "string",
|
|
42
|
+
alias: "pi-home",
|
|
43
|
+
description: "Write Pi output to this Pi root (ex: ~/.pi/agent or ./.pi)",
|
|
44
|
+
},
|
|
39
45
|
also: {
|
|
40
46
|
type: "string",
|
|
41
47
|
description: "Comma-separated extra targets to generate (ex: codex)",
|
|
@@ -76,7 +82,8 @@ export default defineCommand({
|
|
|
76
82
|
try {
|
|
77
83
|
const plugin = await loadClaudePlugin(resolvedPlugin.path)
|
|
78
84
|
const outputRoot = resolveOutputRoot(args.output)
|
|
79
|
-
const codexHome =
|
|
85
|
+
const codexHome = resolveTargetHome(args.codexHome, path.join(os.homedir(), ".codex"))
|
|
86
|
+
const piHome = resolveTargetHome(args.piHome, path.join(os.homedir(), ".pi", "agent"))
|
|
80
87
|
|
|
81
88
|
const options = {
|
|
82
89
|
agentMode: String(args.agentMode) === "primary" ? "primary" : "subagent",
|
|
@@ -88,7 +95,8 @@ export default defineCommand({
|
|
|
88
95
|
if (!bundle) {
|
|
89
96
|
throw new Error(`Target ${targetName} did not return a bundle.`)
|
|
90
97
|
}
|
|
91
|
-
const
|
|
98
|
+
const hasExplicitOutput = Boolean(args.output && String(args.output).trim())
|
|
99
|
+
const primaryOutputRoot = resolveTargetOutputRoot(targetName, outputRoot, codexHome, piHome, hasExplicitOutput)
|
|
92
100
|
await target.write(primaryOutputRoot, bundle)
|
|
93
101
|
console.log(`Installed ${plugin.manifest.name} to ${primaryOutputRoot}`)
|
|
94
102
|
|
|
@@ -109,7 +117,7 @@ export default defineCommand({
|
|
|
109
117
|
console.warn(`Skipping ${extra}: no output returned.`)
|
|
110
118
|
continue
|
|
111
119
|
}
|
|
112
|
-
const extraRoot = resolveTargetOutputRoot(extra, path.join(outputRoot, extra), codexHome)
|
|
120
|
+
const extraRoot = resolveTargetOutputRoot(extra, path.join(outputRoot, extra), codexHome, piHome, hasExplicitOutput)
|
|
113
121
|
await handler.write(extraRoot, extraBundle)
|
|
114
122
|
console.log(`Installed ${plugin.manifest.name} to ${extraRoot}`)
|
|
115
123
|
}
|
|
@@ -151,26 +159,6 @@ function parseExtraTargets(value: unknown): string[] {
|
|
|
151
159
|
.filter(Boolean)
|
|
152
160
|
}
|
|
153
161
|
|
|
154
|
-
function resolveCodexHome(value: unknown): string | null {
|
|
155
|
-
if (!value) return null
|
|
156
|
-
const raw = String(value).trim()
|
|
157
|
-
if (!raw) return null
|
|
158
|
-
const expanded = expandHome(raw)
|
|
159
|
-
return path.resolve(expanded)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function resolveCodexRoot(value: unknown): string {
|
|
163
|
-
return resolveCodexHome(value) ?? path.join(os.homedir(), ".codex")
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function expandHome(value: string): string {
|
|
167
|
-
if (value === "~") return os.homedir()
|
|
168
|
-
if (value.startsWith(`~${path.sep}`)) {
|
|
169
|
-
return path.join(os.homedir(), value.slice(2))
|
|
170
|
-
}
|
|
171
|
-
return value
|
|
172
|
-
}
|
|
173
|
-
|
|
174
162
|
function resolveOutputRoot(value: unknown): string {
|
|
175
163
|
if (value && String(value).trim()) {
|
|
176
164
|
const expanded = expandHome(String(value).trim())
|
|
@@ -181,10 +169,24 @@ function resolveOutputRoot(value: unknown): string {
|
|
|
181
169
|
return path.join(os.homedir(), ".config", "opencode")
|
|
182
170
|
}
|
|
183
171
|
|
|
184
|
-
function resolveTargetOutputRoot(
|
|
172
|
+
function resolveTargetOutputRoot(
|
|
173
|
+
targetName: string,
|
|
174
|
+
outputRoot: string,
|
|
175
|
+
codexHome: string,
|
|
176
|
+
piHome: string,
|
|
177
|
+
hasExplicitOutput: boolean,
|
|
178
|
+
): string {
|
|
185
179
|
if (targetName === "codex") return codexHome
|
|
180
|
+
if (targetName === "pi") return piHome
|
|
186
181
|
if (targetName === "droid") return path.join(os.homedir(), ".factory")
|
|
187
|
-
if (targetName === "cursor")
|
|
182
|
+
if (targetName === "cursor") {
|
|
183
|
+
const base = hasExplicitOutput ? outputRoot : process.cwd()
|
|
184
|
+
return path.join(base, ".cursor")
|
|
185
|
+
}
|
|
186
|
+
if (targetName === "gemini") {
|
|
187
|
+
const base = hasExplicitOutput ? outputRoot : process.cwd()
|
|
188
|
+
return path.join(base, ".gemini")
|
|
189
|
+
}
|
|
188
190
|
return outputRoot
|
|
189
191
|
}
|
|
190
192
|
|
package/src/commands/sync.ts
CHANGED
|
@@ -4,9 +4,16 @@ import path from "path"
|
|
|
4
4
|
import { loadClaudeHome } from "../parsers/claude-home"
|
|
5
5
|
import { syncToOpenCode } from "../sync/opencode"
|
|
6
6
|
import { syncToCodex } from "../sync/codex"
|
|
7
|
+
import { syncToPi } from "../sync/pi"
|
|
8
|
+
import { syncToDroid } from "../sync/droid"
|
|
9
|
+
import { syncToCursor } from "../sync/cursor"
|
|
10
|
+
import { expandHome } from "../utils/resolve-home"
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
|
|
12
|
+
const validTargets = ["opencode", "codex", "pi", "droid", "cursor"] as const
|
|
13
|
+
type SyncTarget = (typeof validTargets)[number]
|
|
14
|
+
|
|
15
|
+
function isValidTarget(value: string): value is SyncTarget {
|
|
16
|
+
return (validTargets as readonly string[]).includes(value)
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
/** Check if any MCP servers have env vars that might contain secrets */
|
|
@@ -23,16 +30,31 @@ function hasPotentialSecrets(mcpServers: Record<string, unknown>): boolean {
|
|
|
23
30
|
return false
|
|
24
31
|
}
|
|
25
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 "cursor":
|
|
44
|
+
return path.join(process.cwd(), ".cursor")
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
26
48
|
export default defineCommand({
|
|
27
49
|
meta: {
|
|
28
50
|
name: "sync",
|
|
29
|
-
description: "Sync Claude Code config (~/.claude/) to OpenCode or
|
|
51
|
+
description: "Sync Claude Code config (~/.claude/) to OpenCode, Codex, Pi, Droid, or Cursor",
|
|
30
52
|
},
|
|
31
53
|
args: {
|
|
32
54
|
target: {
|
|
33
55
|
type: "string",
|
|
34
56
|
required: true,
|
|
35
|
-
description: "Target: opencode | codex",
|
|
57
|
+
description: "Target: opencode | codex | pi | droid | cursor",
|
|
36
58
|
},
|
|
37
59
|
claudeHome: {
|
|
38
60
|
type: "string",
|
|
@@ -42,7 +64,7 @@ export default defineCommand({
|
|
|
42
64
|
},
|
|
43
65
|
async run({ args }) {
|
|
44
66
|
if (!isValidTarget(args.target)) {
|
|
45
|
-
throw new Error(`Unknown target: ${args.target}. Use
|
|
67
|
+
throw new Error(`Unknown target: ${args.target}. Use one of: ${validTargets.join(", ")}`)
|
|
46
68
|
}
|
|
47
69
|
|
|
48
70
|
const claudeHome = expandHome(args.claudeHome ?? path.join(os.homedir(), ".claude"))
|
|
@@ -60,25 +82,26 @@ export default defineCommand({
|
|
|
60
82
|
`Syncing ${config.skills.length} skills, ${Object.keys(config.mcpServers).length} MCP servers...`,
|
|
61
83
|
)
|
|
62
84
|
|
|
63
|
-
const outputRoot =
|
|
64
|
-
args.target === "opencode"
|
|
65
|
-
? path.join(os.homedir(), ".config", "opencode")
|
|
66
|
-
: path.join(os.homedir(), ".codex")
|
|
85
|
+
const outputRoot = resolveOutputRoot(args.target)
|
|
67
86
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
switch (args.target) {
|
|
88
|
+
case "opencode":
|
|
89
|
+
await syncToOpenCode(config, outputRoot)
|
|
90
|
+
break
|
|
91
|
+
case "codex":
|
|
92
|
+
await syncToCodex(config, outputRoot)
|
|
93
|
+
break
|
|
94
|
+
case "pi":
|
|
95
|
+
await syncToPi(config, outputRoot)
|
|
96
|
+
break
|
|
97
|
+
case "droid":
|
|
98
|
+
await syncToDroid(config, outputRoot)
|
|
99
|
+
break
|
|
100
|
+
case "cursor":
|
|
101
|
+
await syncToCursor(config, outputRoot)
|
|
102
|
+
break
|
|
72
103
|
}
|
|
73
104
|
|
|
74
105
|
console.log(`✓ Synced to ${args.target}: ${outputRoot}`)
|
|
75
106
|
},
|
|
76
107
|
})
|
|
77
|
-
|
|
78
|
-
function expandHome(value: string): string {
|
|
79
|
-
if (value === "~") return os.homedir()
|
|
80
|
-
if (value.startsWith(`~${path.sep}`)) {
|
|
81
|
-
return path.join(os.homedir(), value.slice(2))
|
|
82
|
-
}
|
|
83
|
-
return value
|
|
84
|
-
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { formatFrontmatter } from "../utils/frontmatter"
|
|
2
|
+
import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude"
|
|
3
|
+
import type { GeminiBundle, GeminiCommand, GeminiMcpServer, GeminiSkill } from "../types/gemini"
|
|
4
|
+
import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode"
|
|
5
|
+
|
|
6
|
+
export type ClaudeToGeminiOptions = ClaudeToOpenCodeOptions
|
|
7
|
+
|
|
8
|
+
const GEMINI_DESCRIPTION_MAX_LENGTH = 1024
|
|
9
|
+
|
|
10
|
+
export function convertClaudeToGemini(
|
|
11
|
+
plugin: ClaudePlugin,
|
|
12
|
+
_options: ClaudeToGeminiOptions,
|
|
13
|
+
): GeminiBundle {
|
|
14
|
+
const usedSkillNames = new Set<string>()
|
|
15
|
+
const usedCommandNames = new Set<string>()
|
|
16
|
+
|
|
17
|
+
const skillDirs = plugin.skills.map((skill) => ({
|
|
18
|
+
name: skill.name,
|
|
19
|
+
sourceDir: skill.sourceDir,
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
// Reserve skill names from pass-through skills
|
|
23
|
+
for (const skill of skillDirs) {
|
|
24
|
+
usedSkillNames.add(normalizeName(skill.name))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const generatedSkills = plugin.agents.map((agent) => convertAgentToSkill(agent, usedSkillNames))
|
|
28
|
+
|
|
29
|
+
const commands = plugin.commands.map((command) => convertCommand(command, usedCommandNames))
|
|
30
|
+
|
|
31
|
+
const mcpServers = convertMcpServers(plugin.mcpServers)
|
|
32
|
+
|
|
33
|
+
if (plugin.hooks && Object.keys(plugin.hooks.hooks).length > 0) {
|
|
34
|
+
console.warn("Warning: Gemini CLI hooks use a different format (BeforeTool/AfterTool with matchers). Hooks were skipped during conversion.")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { generatedSkills, skillDirs, commands, mcpServers }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function convertAgentToSkill(agent: ClaudeAgent, usedNames: Set<string>): GeminiSkill {
|
|
41
|
+
const name = uniqueName(normalizeName(agent.name), usedNames)
|
|
42
|
+
const description = sanitizeDescription(
|
|
43
|
+
agent.description ?? `Use this skill for ${agent.name} tasks`,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const frontmatter: Record<string, unknown> = { name, description }
|
|
47
|
+
|
|
48
|
+
let body = transformContentForGemini(agent.body.trim())
|
|
49
|
+
if (agent.capabilities && agent.capabilities.length > 0) {
|
|
50
|
+
const capabilities = agent.capabilities.map((c) => `- ${c}`).join("\n")
|
|
51
|
+
body = `## Capabilities\n${capabilities}\n\n${body}`.trim()
|
|
52
|
+
}
|
|
53
|
+
if (body.length === 0) {
|
|
54
|
+
body = `Instructions converted from the ${agent.name} agent.`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const content = formatFrontmatter(frontmatter, body)
|
|
58
|
+
return { name, content }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function convertCommand(command: ClaudeCommand, usedNames: Set<string>): GeminiCommand {
|
|
62
|
+
// Preserve namespace structure: workflows:plan -> workflows/plan
|
|
63
|
+
const commandPath = resolveCommandPath(command.name)
|
|
64
|
+
const pathKey = commandPath.join("/")
|
|
65
|
+
uniqueName(pathKey, usedNames) // Track for dedup
|
|
66
|
+
|
|
67
|
+
const description = command.description ?? `Converted from Claude command ${command.name}`
|
|
68
|
+
const transformedBody = transformContentForGemini(command.body.trim())
|
|
69
|
+
|
|
70
|
+
let prompt = transformedBody
|
|
71
|
+
if (command.argumentHint) {
|
|
72
|
+
prompt += `\n\nUser request: {{args}}`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const content = toToml(description, prompt)
|
|
76
|
+
return { name: pathKey, content }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Transform Claude Code content to Gemini-compatible content.
|
|
81
|
+
*
|
|
82
|
+
* 1. Task agent calls: Task agent-name(args) -> Use the agent-name skill to: args
|
|
83
|
+
* 2. Path rewriting: .claude/ -> .gemini/, ~/.claude/ -> ~/.gemini/
|
|
84
|
+
* 3. Agent references: @agent-name -> the agent-name skill
|
|
85
|
+
*/
|
|
86
|
+
export function transformContentForGemini(body: string): string {
|
|
87
|
+
let result = body
|
|
88
|
+
|
|
89
|
+
// 1. Transform Task agent calls
|
|
90
|
+
const taskPattern = /^(\s*-?\s*)Task\s+([a-z][a-z0-9-]*)\(([^)]+)\)/gm
|
|
91
|
+
result = result.replace(taskPattern, (_match, prefix: string, agentName: string, args: string) => {
|
|
92
|
+
const skillName = normalizeName(agentName)
|
|
93
|
+
return `${prefix}Use the ${skillName} skill to: ${args.trim()}`
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// 2. Rewrite .claude/ paths to .gemini/
|
|
97
|
+
result = result
|
|
98
|
+
.replace(/~\/\.claude\//g, "~/.gemini/")
|
|
99
|
+
.replace(/\.claude\//g, ".gemini/")
|
|
100
|
+
|
|
101
|
+
// 3. Transform @agent-name references
|
|
102
|
+
const agentRefPattern = /@([a-z][a-z0-9-]*-(?:agent|reviewer|researcher|analyst|specialist|oracle|sentinel|guardian|strategist))/gi
|
|
103
|
+
result = result.replace(agentRefPattern, (_match, agentName: string) => {
|
|
104
|
+
return `the ${normalizeName(agentName)} skill`
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return result
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function convertMcpServers(
|
|
111
|
+
servers?: Record<string, ClaudeMcpServer>,
|
|
112
|
+
): Record<string, GeminiMcpServer> | undefined {
|
|
113
|
+
if (!servers || Object.keys(servers).length === 0) return undefined
|
|
114
|
+
|
|
115
|
+
const result: Record<string, GeminiMcpServer> = {}
|
|
116
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
117
|
+
const entry: GeminiMcpServer = {}
|
|
118
|
+
if (server.command) {
|
|
119
|
+
entry.command = server.command
|
|
120
|
+
if (server.args && server.args.length > 0) entry.args = server.args
|
|
121
|
+
if (server.env && Object.keys(server.env).length > 0) entry.env = server.env
|
|
122
|
+
} else if (server.url) {
|
|
123
|
+
entry.url = server.url
|
|
124
|
+
if (server.headers && Object.keys(server.headers).length > 0) entry.headers = server.headers
|
|
125
|
+
}
|
|
126
|
+
result[name] = entry
|
|
127
|
+
}
|
|
128
|
+
return result
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Resolve command name to path segments.
|
|
133
|
+
* workflows:plan -> ["workflows", "plan"]
|
|
134
|
+
* plan -> ["plan"]
|
|
135
|
+
*/
|
|
136
|
+
function resolveCommandPath(name: string): string[] {
|
|
137
|
+
return name.split(":").map((segment) => normalizeName(segment))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Serialize to TOML command format.
|
|
142
|
+
* Uses multi-line strings (""") for prompt field.
|
|
143
|
+
*/
|
|
144
|
+
export function toToml(description: string, prompt: string): string {
|
|
145
|
+
const lines: string[] = []
|
|
146
|
+
lines.push(`description = ${formatTomlString(description)}`)
|
|
147
|
+
|
|
148
|
+
// Use multi-line string for prompt
|
|
149
|
+
const escapedPrompt = prompt.replace(/\\/g, "\\\\").replace(/"""/g, '\\"\\"\\"')
|
|
150
|
+
lines.push(`prompt = """`)
|
|
151
|
+
lines.push(escapedPrompt)
|
|
152
|
+
lines.push(`"""`)
|
|
153
|
+
|
|
154
|
+
return lines.join("\n")
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatTomlString(value: string): string {
|
|
158
|
+
return JSON.stringify(value)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function normalizeName(value: string): string {
|
|
162
|
+
const trimmed = value.trim()
|
|
163
|
+
if (!trimmed) return "item"
|
|
164
|
+
const normalized = trimmed
|
|
165
|
+
.toLowerCase()
|
|
166
|
+
.replace(/[\\/]+/g, "-")
|
|
167
|
+
.replace(/[:\s]+/g, "-")
|
|
168
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
169
|
+
.replace(/-+/g, "-")
|
|
170
|
+
.replace(/^-+|-+$/g, "")
|
|
171
|
+
return normalized || "item"
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function sanitizeDescription(value: string, maxLength = GEMINI_DESCRIPTION_MAX_LENGTH): string {
|
|
175
|
+
const normalized = value.replace(/\s+/g, " ").trim()
|
|
176
|
+
if (normalized.length <= maxLength) return normalized
|
|
177
|
+
const ellipsis = "..."
|
|
178
|
+
return normalized.slice(0, Math.max(0, maxLength - ellipsis.length)).trimEnd() + ellipsis
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function uniqueName(base: string, used: Set<string>): string {
|
|
182
|
+
if (!used.has(base)) {
|
|
183
|
+
used.add(base)
|
|
184
|
+
return base
|
|
185
|
+
}
|
|
186
|
+
let index = 2
|
|
187
|
+
while (used.has(`${base}-${index}`)) {
|
|
188
|
+
index += 1
|
|
189
|
+
}
|
|
190
|
+
const name = `${base}-${index}`
|
|
191
|
+
used.add(name)
|
|
192
|
+
return name
|
|
193
|
+
}
|
|
@@ -250,8 +250,24 @@ function rewriteClaudePaths(body: string): string {
|
|
|
250
250
|
.replace(/\.claude\//g, ".opencode/")
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
// Bare Claude family aliases used in Claude Code (e.g. `model: haiku`).
|
|
254
|
+
// Update these when new model generations are released.
|
|
255
|
+
const CLAUDE_FAMILY_ALIASES: Record<string, string> = {
|
|
256
|
+
haiku: "claude-haiku-4-5",
|
|
257
|
+
sonnet: "claude-sonnet-4-5",
|
|
258
|
+
opus: "claude-opus-4-6",
|
|
259
|
+
}
|
|
260
|
+
|
|
253
261
|
function normalizeModel(model: string): string {
|
|
254
262
|
if (model.includes("/")) return model
|
|
263
|
+
if (CLAUDE_FAMILY_ALIASES[model]) {
|
|
264
|
+
const resolved = `anthropic/${CLAUDE_FAMILY_ALIASES[model]}`
|
|
265
|
+
console.warn(
|
|
266
|
+
`Warning: bare model alias "${model}" mapped to "${resolved}". ` +
|
|
267
|
+
`Update CLAUDE_FAMILY_ALIASES if a newer version is available.`,
|
|
268
|
+
)
|
|
269
|
+
return resolved
|
|
270
|
+
}
|
|
255
271
|
if (/^claude-/.test(model)) return `anthropic/${model}`
|
|
256
272
|
if (/^(gpt-|o1-|o3-)/.test(model)) return `openai/${model}`
|
|
257
273
|
if (/^gemini-/.test(model)) return `google/${model}`
|