diff --git a/src/converters/claude-to-gemini.ts b/src/converters/claude-to-gemini.ts index 3f136a0..7dc4389 100644 --- a/src/converters/claude-to-gemini.ts +++ b/src/converters/claude-to-gemini.ts @@ -1,6 +1,6 @@ import { formatFrontmatter } from "../utils/frontmatter" import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude" -import type { GeminiBundle, GeminiCommand, GeminiSkill } from "../types/gemini" +import type { GeminiBundle, GeminiCommand, GeminiMcpServer, GeminiSkill } from "../types/gemini" import type { ClaudeToOpenCodeOptions } from "./claude-to-opencode" export type ClaudeToGeminiOptions = ClaudeToOpenCodeOptions @@ -109,12 +109,12 @@ export function transformContentForGemini(body: string): string { function convertMcpServers( servers?: Record, -): GeminiBundle["mcpServers"] | undefined { +): Record | undefined { if (!servers || Object.keys(servers).length === 0) return undefined - const result: NonNullable = {} + const result: Record = {} for (const [name, server] of Object.entries(servers)) { - const entry: NonNullable[string] = {} + const entry: GeminiMcpServer = {} if (server.command) { entry.command = server.command if (server.args && server.args.length > 0) entry.args = server.args diff --git a/src/targets/gemini.ts b/src/targets/gemini.ts index 0ed9ae9..0bc8c66 100644 --- a/src/targets/gemini.ts +++ b/src/targets/gemini.ts @@ -37,11 +37,14 @@ export async function writeGeminiBundle(outputRoot: string, bundle: GeminiBundle try { existingSettings = await readJson>(settingsPath) } catch { - // If existing file is invalid JSON, start fresh + console.warn("Warning: existing settings.json could not be parsed and will be replaced.") } } - const merged = { ...existingSettings, mcpServers: bundle.mcpServers } + const existingMcp = (existingSettings.mcpServers && typeof existingSettings.mcpServers === "object") + ? existingSettings.mcpServers as Record + : {} + const merged = { ...existingSettings, mcpServers: { ...existingMcp, ...bundle.mcpServers } } await writeJson(settingsPath, merged) } } diff --git a/src/types/gemini.ts b/src/types/gemini.ts index 25172d3..7e37e69 100644 --- a/src/types/gemini.ts +++ b/src/types/gemini.ts @@ -13,15 +13,17 @@ export type GeminiCommand = { content: string // Full TOML content } +export type GeminiMcpServer = { + command?: string + args?: string[] + env?: Record + url?: string + headers?: Record +} + export type GeminiBundle = { generatedSkills: GeminiSkill[] // From agents skillDirs: GeminiSkillDir[] // From skills (pass-through) commands: GeminiCommand[] - mcpServers?: Record - url?: string - headers?: Record - }> + mcpServers?: Record } diff --git a/tests/gemini-converter.test.ts b/tests/gemini-converter.test.ts index 9531faf..bd9675a 100644 --- a/tests/gemini-converter.test.ts +++ b/tests/gemini-converter.test.ts @@ -267,6 +267,25 @@ describe("convertClaudeToGemini", () => { expect(bundle.commands).toHaveLength(0) }) + test("agent name colliding with skill name gets deduplicated", () => { + const plugin: ClaudePlugin = { + ...fixturePlugin, + skills: [{ name: "security-reviewer", description: "Existing skill", sourceDir: "/tmp/skill", skillPath: "/tmp/skill/SKILL.md" }], + agents: [{ name: "Security Reviewer", description: "Agent version", body: "Body.", sourcePath: "/tmp/agents/sr.md" }], + commands: [], + } + + const bundle = convertClaudeToGemini(plugin, { + agentMode: "subagent", + inferTemperature: false, + permissions: "none", + }) + + // Agent should be deduplicated since skill already has "security-reviewer" + expect(bundle.generatedSkills[0].name).toBe("security-reviewer-2") + expect(bundle.skillDirs[0].name).toBe("security-reviewer") + }) + test("hooks present emits console.warn", () => { const warnings: string[] = [] const originalWarn = console.warn @@ -339,4 +358,16 @@ describe("toToml", () => { const result = toToml('Say "hello"', "Prompt") expect(result).toContain('description = "Say \\"hello\\""') }) + + test("escapes triple quotes in prompt", () => { + const result = toToml("A command", 'Content with """ inside it') + // Should not contain an unescaped """ that would close the TOML multi-line string prematurely + // The prompt section should have the escaped version + expect(result).toContain('description = "A command"') + expect(result).toContain('prompt = """') + // The inner """ should be escaped + expect(result).not.toMatch(/""".*""".*"""/s) // Should not have 3 separate triple-quote sequences (open, content, close would make 3) + // Verify it contains the escaped form + expect(result).toContain('\\"\\"\\"') + }) }) diff --git a/tests/gemini-writer.test.ts b/tests/gemini-writer.test.ts index 8b02ab3..a6a9df3 100644 --- a/tests/gemini-writer.test.ts +++ b/tests/gemini-writer.test.ts @@ -173,7 +173,9 @@ describe("writeGeminiBundle", () => { const content = JSON.parse(await fs.readFile(settingsPath, "utf8")) // Should preserve existing model key expect(content.model).toBe("gemini-2.5-pro") - // mcpServers should be replaced (not merged) with new content + // Should preserve existing MCP server + expect(content.mcpServers.old.command).toBe("old-cmd") + // Should add new MCP server expect(content.mcpServers.newServer.command).toBe("new-cmd") }) })