/* eslint-disable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument */ // Mechanical extraction from bin/install.js; keep behavior parity before typing. // @ts-nocheck 'use strict'; /** * Runtime Artifact Conversion Module. * * First slice: layout-reached command/skill artifact converters moved out of * bin/install.js so Runtime Artifact Layout no longer reaches through the * Installer Module for conversion behavior. */ import path from 'node:path'; import commandRoster = require('./command-roster.cjs'); const { readGsdCommandNames, transformContentToHyphen } = commandRoster; const pkg = require('../../../package.json'); const colorNameToHex = { cyan: '#00FFFF', red: '#FF0000', green: '#00FF00', blue: '#0000FF', yellow: '#FFFF00', magenta: '#FF00FF', orange: '#FFA500', purple: '#800080', pink: '#FFC0CB', white: '#FFFFFF', black: '#000000', gray: '#808080', grey: '#808080', }; // Tool name mapping from Claude Code to OpenCode // OpenCode uses lowercase tool names; special mappings for renamed tools const claudeToOpencodeTools = { AskUserQuestion: 'question', SlashCommand: 'skill', TodoWrite: 'todowrite', WebFetch: 'webfetch', WebSearch: 'websearch', // Plugin/MCP - keep for compatibility }; // Tool name mapping from Claude/GSD agents to Kimi CLI module paths. // Kimi custom agent YAML requires fully-qualified module paths. const claudeToKimiTools = { Read: 'kimi_cli.tools.file:ReadFile', ReadFile: 'kimi_cli.tools.file:ReadFile', Write: 'kimi_cli.tools.file:WriteFile', WriteFile: 'kimi_cli.tools.file:WriteFile', Edit: 'kimi_cli.tools.file:StrReplaceFile', MultiEdit: 'kimi_cli.tools.file:StrReplaceFile', StrReplaceFile: 'kimi_cli.tools.file:StrReplaceFile', Bash: 'kimi_cli.tools.shell:Shell', Shell: 'kimi_cli.tools.shell:Shell', Grep: 'kimi_cli.tools.file:Grep', Glob: 'kimi_cli.tools.file:Glob', Agent: 'kimi_cli.tools.agent:Agent', Task: 'kimi_cli.tools.agent:Agent', AskUserQuestion: 'kimi_cli.tools.ask_user:AskUserQuestion', TodoWrite: 'kimi_cli.tools.todo:SetTodoList', SetTodoList: 'kimi_cli.tools.todo:SetTodoList', WebSearch: 'kimi_cli.tools.web:SearchWeb', SearchWeb: 'kimi_cli.tools.web:SearchWeb', WebFetch: 'kimi_cli.tools.web:FetchURL', FetchURL: 'kimi_cli.tools.web:FetchURL', ReadMediaFile: 'kimi_cli.tools.file:ReadMediaFile', TaskList: 'kimi_cli.tools.background:TaskList', TaskOutput: 'kimi_cli.tools.background:TaskOutput', TaskStop: 'kimi_cli.tools.background:TaskStop', }; /** * Convert a Claude Code tool name to OpenCode format * - Applies special mappings (AskUserQuestion -> question, etc.) * - Converts to lowercase (except MCP tools which keep their format) */ function convertToolName(claudeTool) { // Check for special mapping first if (claudeToOpencodeTools[claudeTool]) { return claudeToOpencodeTools[claudeTool]; } // MCP tools (mcp__*) keep their format if (claudeTool.startsWith('mcp__')) { return claudeTool; } // Default: convert to lowercase return claudeTool.toLowerCase(); } function createKimiToolDiagnostic(reason, tool, source = null) { const isMcp = reason === 'mcp_managed'; return { level: 'warning', code: isMcp ? 'kimi_mcp_tool_excluded' : 'kimi_unsupported_tool', reason, message: isMcp ? `MCP-managed tool '${tool}' is configured outside Kimi agent YAML.` : `Tool '${tool}' is not supported by the Kimi tool mapper.`, value: tool, source, }; } /** * Convert a Claude/GSD tool name to a Kimi CLI module path. * @returns {string|null} Kimi module path, or null when excluded/unsupported. */ function convertKimiToolName(claudeTool) { const tool = String(claudeTool || '').trim(); if (!tool) return null; if (tool.startsWith('mcp__')) return null; return claudeToKimiTools[tool] || null; } function mapClaudeToolsToKimiTools(claudeTools, options = {}) { const diagnostics = []; const tools = []; const seen = new Set(); const source = options && Object.prototype.hasOwnProperty.call(options, 'source') ? options.source : null; for (const rawTool of Array.isArray(claudeTools) ? claudeTools : []) { const tool = String(rawTool || '').trim(); if (!tool) continue; if (tool.startsWith('mcp__')) { diagnostics.push(createKimiToolDiagnostic('mcp_managed', tool, source)); continue; } const kimiTool = convertKimiToolName(tool); if (!kimiTool) { diagnostics.push(createKimiToolDiagnostic('unsupported_tool', tool, source)); continue; } if (!seen.has(kimiTool)) { seen.add(kimiTool); tools.push(kimiTool); } } return { tools, diagnostics }; } const claudeToKiloAgentPermissions = { Read: 'read', Write: 'edit', Edit: 'edit', Bash: 'bash', Grep: 'grep', Glob: 'glob', Task: 'task', WebFetch: 'webfetch', WebSearch: 'websearch', TodoWrite: 'todowrite', AskUserQuestion: 'question', SlashCommand: 'skill', }; const kiloAgentPermissionOrder = [ 'read', 'edit', 'bash', 'grep', 'glob', 'task', 'webfetch', 'websearch', 'skill', 'question', 'todowrite', 'list', 'codesearch', 'lsp', ]; function convertClaudeToKiloPermissionTool(claudeTool) { return claudeToKiloAgentPermissions[claudeTool] || null; } function buildKiloAgentPermissionBlock(claudeTools) { const allowedPermissions = new Set(); for (const tool of claudeTools) { const mapped = convertClaudeToKiloPermissionTool(tool); if (mapped) { allowedPermissions.add(mapped); } } const lines = ['permission:']; for (const permission of kiloAgentPermissionOrder) { lines.push(` ${permission}: ${allowedPermissions.has(permission) ? 'allow' : 'deny'}`); } return lines; } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function replaceRelativePathReference(content, fromPath, toPath) { const escapedPath = escapeRegExp(fromPath); return content.replace( new RegExp(`(^|[^A-Za-z0-9_./-])${escapedPath}`, 'g'), (_, prefix) => `${prefix}${toPath}`, ); } /** * Apply Copilot-specific content conversion — CONV-06 (paths) + CONV-07 (command names). * Path mappings depend on install mode: * Global: ~/.claude/ → ~/.copilot/, ./.claude/ → ./.github/ * Local: ~/.claude/ → ./.github/, ./.claude/ → ./.github/ * Applied to ALL Copilot content (skills, agents, engine files). * @param {string} content - Source content to convert * @param {boolean} [isGlobal=false] - Whether this is a global install */ function convertClaudeToCopilotContent(content, isGlobal = false) { let c = content; // CONV-06: Path replacement — most specific first to avoid substring matches. // Handle both `~/.claude/foo` (trailing slash) and bare `~/.claude` forms in // one pass via a capture group, matching the approach used by Antigravity, // OpenCode, Kilo, and Codex converters (issue #2545). if (isGlobal) { c = c.replace(/\$HOME\/\.claude(\/|\b)/g, '$HOME/.copilot$1'); c = c.replace(/~\/\.claude(\/|\b)/g, '~/.copilot$1'); } else { c = c.replace(/\$HOME\/\.claude\//g, '.github/'); c = c.replace(/~\/\.claude\//g, '.github/'); c = c.replace(/\$HOME\/\.claude\b/g, '.github'); c = c.replace(/~\/\.claude\b/g, '.github'); } c = c.replace(/\.\/\.claude\//g, './.github/'); c = c.replace(/\.claude\//g, '.github/'); // CONV-07: Command name conversion (all gsd: references → gsd-) c = c.replace(/gsd:/g, 'gsd-'); // Runtime-neutral agent name replacement (#766) c = neutralizeAgentReferences(c, 'copilot-instructions.md'); return c; } /** * Convert a Claude command (.md) to a Copilot skill (SKILL.md). * Transforms frontmatter only — body passes through with CONV-06/07 applied. * Skills keep original tool names (no mapping) per CONTEXT.md decision. */ // isGlobal is the 5th positional arg (3rd/4th are runtime/cmdNames passed by the skills wrapper). See runtime-artifact-layout skillsKind. function convertClaudeCommandToCopilotSkill(content, skillName, _runtime = null, _cmdNames = null, isGlobal = false) { const converted = convertClaudeToCopilotContent(content, isGlobal); const { frontmatter, body } = extractFrontmatterAndBody(converted); if (!frontmatter) return converted; const description = extractFrontmatterField(frontmatter, 'description') || ''; const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint'); const agent = extractFrontmatterField(frontmatter, 'agent'); // CONV-02: Extract allowed-tools YAML multiline list → comma-separated string const toolsMatch = frontmatter.match(/^allowed-tools:\s*\n((?:\s+-\s+.+\n?)*)/m); let toolsLine = ''; if (toolsMatch) { const tools = toolsMatch[1].match(/^\s+-\s+(.+)/gm); if (tools) { toolsLine = tools.map(t => t.replace(/^\s+-\s+/, '').trim()).join(', '); } } // Reconstruct frontmatter in Copilot format // #2876: descriptions starting with a YAML flow indicator (`[BETA] …`, // `{ … }`, `*ref`, `&anchor`, etc.) parse as flow sequences/mappings and // crash gh-copilot's frontmatter loader. Always quote so any leading // character is parser-safe. let fm = `---\nname: ${skillName}\ndescription: ${yamlQuote(description)}\n`; if (argumentHint) fm += `argument-hint: ${yamlQuote(argumentHint)}\n`; if (agent) fm += `agent: ${agent}\n`; if (toolsLine) fm += `allowed-tools: ${toolsLine}\n`; fm += '---'; return `${fm}\n${body}`; } /** * Map a skill directory name (gsd-) to the frontmatter `name:` used * by Claude Code as the skill identity. Emits the hyphen form (gsd-) * so Claude Code autocomplete shows the canonical invocation form, not the * deprecated colon form. See #2808. * * Historical note: this previously returned `gsd:` (colon) because * workflows called Skill(skill="gsd:"). Those calls have been updated * to use hyphen form (#2808) so the colon rewrite is no longer needed. * * Codex must NOT use this helper: its adapter invokes skills as `$gsd-` * (shell-var syntax) — hyphen form is already correct there. */ function skillFrontmatterName(skillDirName) { if (typeof skillDirName !== 'string') return skillDirName; // Return the hyphen form as-is (gsd-) — canonical since #2808. return skillDirName; } /** * Qwen Code skills accept an optional numeric `priority` frontmatter field. * Per the Qwen skills spec (qwen-code/docs/users/features/skills.md, verified * #778): HIGHER values sort EARLIER in the `/skills` TUI listing (omitted ≈ 0; * negatives sort below unset). It affects ONLY the `/skills` list order — * slash-command completion and the `/help` view stay alphabetical. * * We assign descending priorities to GSD's main-loop commands so the most-used * workflow skills surface first; utility skills are deliberately left unset * (default 0) and sort below. * * NOTE: the #778 issue body proposed the INVERSE numbering (plan-phase: 10, * utilities: 90+). The verified spec shows that would BURY the core loop below * utilities, so we implement the spec-correct direction (core = high) instead. * Keyed by command stem (skill dir is `gsd-`). */ const QWEN_SKILL_PRIORITY = Object.freeze({ 'new-project': 100, 'discuss-phase': 95, 'plan-phase': 90, 'execute-phase': 85, progress: 80, 'verify-work': 75, phase: 70, review: 65, ship: 60, config: 55, surface: 50, 'resume-work': 45, 'pause-work': 40, help: 35, update: 30, }); /** * Convert a Claude command (.md) to a Claude skill (SKILL.md). * Claude Code is the native format, so minimal conversion needed — * preserve allowed-tools as YAML multiline list, preserve argument-hint. * Emits `name: gsd-` (hyphen) so Skill(skill="gsd-") calls and * tab autocomplete use the canonical command namespace. */ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, cmdNames = null) { const { frontmatter, body } = extractFrontmatterAndBody(content); if (!frontmatter) return content; // #3583: rewrite any /gsd: or gsd: in the body to the canonical // hyphen form (gsd-) so installed SKILL.md bodies match the hyphen // `name:` Claude Code (and Qwen/Hermes) register under (#2808). `cmdNames` // is optional and pre-computed by the caller for performance; direct test // calls fall back to reading the list. const names = cmdNames || readGsdCommandNames(); const normalizedBody = transformContentToHyphen(body, names); const description = extractFrontmatterField(frontmatter, 'description') || ''; const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint'); const agent = extractFrontmatterField(frontmatter, 'agent'); // #769: preserve context: and effort: from source command files so they // are emitted into the installed SKILL.md frontmatter unchanged. const context = extractFrontmatterField(frontmatter, 'context'); const effort = extractFrontmatterField(frontmatter, 'effort'); // Preserve allowed-tools as YAML multiline list (Claude native format) const toolsMatch = frontmatter.match(/^allowed-tools:\s*\n((?:\s+-\s+.+\n?)*)/m); let toolsBlock = ''; if (toolsMatch) { toolsBlock = 'allowed-tools:\n' + toolsMatch[1]; // Ensure trailing newline if (!toolsBlock.endsWith('\n')) toolsBlock += '\n'; } // Reconstruct frontmatter in Claude skill format const frontmatterName = skillFrontmatterName(skillName); let fm = `---\nname: ${frontmatterName}\ndescription: ${yamlQuote(description)}\n`; // Hermes' SKILL.md spec lists `version` as a required frontmatter field. // Track GSD's package version so Hermes' skill_view() reports a stable // identifier per install. if (runtime === 'hermes') fm += `version: ${yamlQuote(pkg.version)}\n`; // #778 (b) — Qwen-only numeric priority for /skills ordering. Scoped to qwen // so Claude/Hermes skill frontmatter is unchanged (they ignore the field, but // we keep their output byte-stable). skillName is the `gsd-` dir name. if (runtime === 'qwen') { const stem = typeof skillName === 'string' && skillName.startsWith('gsd-') ? skillName.slice(4) : skillName; const priority = Object.prototype.hasOwnProperty.call(QWEN_SKILL_PRIORITY, stem) ? QWEN_SKILL_PRIORITY[stem] : undefined; if (typeof priority === 'number') fm += `priority: ${priority}\n`; } if (argumentHint) fm += `argument-hint: ${yamlQuote(argumentHint)}\n`; if (agent) fm += `agent: ${agent}\n`; // #769: emit context: and effort: when present so the runtime can honour // them natively (context: fork = isolated subagent window; effort: = // token-budget tier). Fields are Claude-specific; unknown frontmatter // fields are silently ignored by other runtimes (backward-compatible). if (context) fm += `context: ${context}\n`; if (effort) fm += `effort: ${effort}\n`; if (toolsBlock) fm += toolsBlock; fm += '---'; return `${fm}\n${normalizedBody}`; } function normalizeKimiSkillName(skillName) { let text = String(skillName || '').trim().toLowerCase(); if (text.startsWith('/')) text = text.slice(1); if (text.startsWith('$')) text = text.slice(1); text = text.replace(/^gsd:/, 'gsd-'); if (!text.startsWith('gsd-')) text = `gsd-${text}`; text = text.replace(/[^a-z0-9-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); return text || 'gsd-command'; } function convertGsdCommandReferencesToKimiSkillInvocations(content, cmdNames) { if (!Array.isArray(cmdNames) || cmdNames.length === 0) return content; const commands = [...cmdNames].sort((a, b) => b.length - a.length).map(escapeRegExp); const commandGroup = commands.join('|'); const colonPattern = new RegExp(`(? `/skill:gsd-${cmd}`) .replace(hyphenPattern, (_, cmd) => `/skill:gsd-${cmd}`); } function convertClaudeCommandToKimiSkill(content, skillName, _runtime = null, cmdNames = null) { const { frontmatter, body } = extractFrontmatterAndBody(content); const kimiSkillName = normalizeKimiSkillName(skillName); const names = cmdNames || readGsdCommandNames(); const description = frontmatter ? extractFrontmatterField(frontmatter, 'description') || `Run GSD workflow ${kimiSkillName}.` : `Run GSD workflow ${kimiSkillName}.`; const normalizedBody = convertGsdCommandReferencesToKimiSkillInvocations( frontmatter ? body : content, names ); return `---\nname: ${kimiSkillName}\ndescription: ${yamlQuote(toSingleLine(description))}\n---\nInvoke this Kimi skill with \`/skill:${kimiSkillName}\`.\n\n${normalizedBody}`; } const KIMI_CANONICAL_GSD_AGENT_RE = /^gsd-[a-z0-9-]+$/; function parseKimiAgentSource(source) { if (typeof source === 'string') { return { path: null, content: source, }; } if (!source || typeof source !== 'object' || typeof source.content !== 'string') { return null; } return { path: typeof source.path === 'string' ? source.path : null, content: source.content, }; } function parseFrontmatterTools(frontmatter) { if (!frontmatter) return []; const lines = frontmatter.split(/\r?\n/); const tools = []; let collecting = false; for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; if (collecting) { if (trimmed.startsWith('- ')) { tools.push(trimmed.slice(2).trim()); continue; } collecting = false; } if (trimmed === 'tools:' || trimmed === 'allowed-tools:') { collecting = true; continue; } if (trimmed.startsWith('tools:') || trimmed.startsWith('allowed-tools:')) { const value = trimmed.slice(trimmed.indexOf(':') + 1).trim(); if (value) { for (const tool of value.split(',')) { const name = tool.trim(); if (name) tools.push(name); } } else { collecting = true; } } } return tools; } function addKimiAgentDiagnostic(diagnostics, code, message, value, source = null) { diagnostics.push({ level: 'warning', code, message, value, source, }); } function mapKimiAgentContractTools(toolNames, diagnostics, sourceName) { const result = mapClaudeToolsToKimiTools(toolNames, { source: sourceName }); diagnostics.push(...result.diagnostics); return result.tools; } function neutralizeKimiAgentPrompt(content) { const { frontmatter, body } = extractFrontmatterAndBody(content); let prompt = frontmatter ? body : content; prompt = neutralizeAgentReferences(prompt, 'AGENTS.md'); prompt = prompt.replace(/~\/\.claude\/gsd-core\b/g, 'GSD core'); prompt = prompt.replace(/\$HOME\/\.claude\/gsd-core\b/g, 'GSD core'); return prompt.replace(/^\s*\r?\n/, ''); } function pushKimiToolsYaml(lines, indent, tools) { const prefix = ' '.repeat(indent); if (!Array.isArray(tools) || tools.length === 0) { lines.push(`${prefix}tools: []`); return; } lines.push(`${prefix}tools:`); for (const tool of tools) { lines.push(`${prefix} - ${yamlQuote(tool)}`); } } function buildKimiRootAgentYaml({ description, tools, subagents }) { const lines = [ 'version: 1', 'agent:', ' name: gsd', ` description: ${yamlQuote(toSingleLine(description || 'Run GSD workflows in Kimi CLI.'))}`, ' extend: default', ' system_prompt_path: ./gsd.md', ]; pushKimiToolsYaml(lines, 2, tools); if (subagents.length > 0) { lines.push(' subagents:'); for (const subagent of subagents) { lines.push(` ${subagent.name}:`); lines.push(` path: ./subagents/${subagent.name}.yaml`); lines.push(` description: ${yamlQuote(toSingleLine(subagent.description))}`); } } return `${lines.join('\n')}\n`; } function buildKimiSubagentYaml({ name, description, tools }) { const lines = [ 'version: 1', 'agent:', ` name: ${name}`, ` description: ${yamlQuote(toSingleLine(description || `Run ${name}.`))}`, ` system_prompt_path: ./${name}.md`, ]; pushKimiToolsYaml(lines, 2, tools); return `${lines.join('\n')}\n`; } function buildKimiAgentArtifacts({ rootAgent = '', subagents = [], requestedSubagents = null, } = {}) { const diagnostics = []; const rootSource = parseKimiAgentSource(rootAgent) || { path: null, content: '' }; const { frontmatter: rootFrontmatter } = extractFrontmatterAndBody(rootSource.content); const rootDescription = rootFrontmatter ? extractFrontmatterField(rootFrontmatter, 'description') || 'Run GSD workflows in Kimi CLI.' : 'Run GSD workflows in Kimi CLI.'; const subagentSources = Array.isArray(subagents) ? subagents : []; if (!Array.isArray(subagents)) { addKimiAgentDiagnostic( diagnostics, 'kimi_unsupported_subagents_input', 'Subagents input must be an array of Markdown strings or source objects.', typeof subagents, null ); } const subagentMap = new Map(); for (const source of subagentSources) { const parsed = parseKimiAgentSource(source); if (!parsed) { addKimiAgentDiagnostic( diagnostics, 'kimi_unsupported_subagent_input', 'Subagent source must be a Markdown string or an object with content.', typeof source, null ); continue; } const { frontmatter } = extractFrontmatterAndBody(parsed.content); const fallbackName = parsed.path ? path.basename(parsed.path, path.extname(parsed.path)) : null; const name = frontmatter ? extractFrontmatterField(frontmatter, 'name') || fallbackName : fallbackName; if (!name || !KIMI_CANONICAL_GSD_AGENT_RE.test(name)) { addKimiAgentDiagnostic( diagnostics, 'kimi_invalid_subagent_name', 'Subagent source does not use a canonical gsd-* Kimi agent name.', name || '(missing)', parsed.path ); continue; } const description = frontmatter ? extractFrontmatterField(frontmatter, 'description') || `Run ${name}.` : `Run ${name}.`; const tools = mapKimiAgentContractTools(parseFrontmatterTools(frontmatter), diagnostics, name); subagentMap.set(name, { name, description, tools, prompt: neutralizeKimiAgentPrompt(parsed.content), }); } const requested = Array.isArray(requestedSubagents) && requestedSubagents.length > 0 ? requestedSubagents : [...subagentMap.keys()]; const selectedSubagents = []; for (const requestedName of requested) { if (subagentMap.has(requestedName)) { selectedSubagents.push(subagentMap.get(requestedName)); continue; } addKimiAgentDiagnostic( diagnostics, 'kimi_unknown_subagent', 'Requested subagent was not generated and will not be emitted in Kimi YAML.', requestedName, null ); } const rootTools = mapKimiAgentContractTools(parseFrontmatterTools(rootFrontmatter), diagnostics, 'gsd'); if (selectedSubagents.length > 0 && !rootTools.includes('kimi_cli.tools.agent:Agent')) { rootTools.push('kimi_cli.tools.agent:Agent'); } return { root: { name: 'gsd', yamlPath: 'agents/gsd.yaml', promptPath: 'agents/gsd.md', yaml: buildKimiRootAgentYaml({ description: rootDescription, tools: rootTools, subagents: selectedSubagents, }), prompt: neutralizeKimiAgentPrompt(rootSource.content), }, subagents: selectedSubagents.map((subagent) => ({ name: subagent.name, yamlPath: `agents/subagents/${subagent.name}.yaml`, promptPath: `agents/subagents/${subagent.name}.md`, yaml: buildKimiSubagentYaml(subagent), prompt: subagent.prompt, })), diagnostics, }; } /** * Apply Antigravity-specific content conversion — path replacement + command name conversion. * Path mappings depend on install mode: * Global: ~/.claude/ → ~/.gemini/antigravity/, ./.claude/ → ./.agents/ * Local: ~/.claude/ → .agents/, ./.claude/ → ./.agents/ * Applied to ALL Antigravity content (skills, agents, engine files). * @param {string} content - Source content to convert * @param {boolean} [isGlobal=false] - Whether this is a global install */ function convertClaudeToAntigravityContent(content, isGlobal = false) { let c = content; if (isGlobal) { c = c.replace(/\$HOME\/\.claude\//g, '$HOME/.gemini/antigravity/'); c = c.replace(/~\/\.claude\//g, '~/.gemini/antigravity/'); // Bare form (no trailing slash) — must come after slash form to avoid double-replace c = c.replace(/\$HOME\/\.claude\b/g, '$HOME/.gemini/antigravity'); c = c.replace(/~\/\.claude\b/g, '~/.gemini/antigravity'); } else { c = c.replace(/\$HOME\/\.claude\//g, '.agents/'); c = c.replace(/~\/\.claude\//g, '.agents/'); // Bare form (no trailing slash) — must come after slash form to avoid double-replace c = c.replace(/\$HOME\/\.claude\b/g, '.agents'); c = c.replace(/~\/\.claude\b/g, '.agents'); } c = c.replace(/\.\/\.claude\//g, './.agents/'); c = c.replace(/\.claude\//g, '.agents/'); // Command name conversion (all gsd: references → gsd-) c = c.replace(/gsd:/g, 'gsd-'); // Runtime-neutral agent name replacement (#766) c = neutralizeAgentReferences(c, 'GEMINI.md'); return c; } /** * Convert a Claude command (.md) to an Antigravity skill (SKILL.md). * Transforms frontmatter to minimal name + description only. * Body passes through with path/command conversions applied. */ // isGlobal is the 5th positional arg (3rd/4th are runtime/cmdNames passed by the skills wrapper). See runtime-artifact-layout skillsKind. function convertClaudeCommandToAntigravitySkill(content, skillName, _runtime = null, _cmdNames = null, isGlobal = false) { const converted = convertClaudeToAntigravityContent(content, isGlobal); const { frontmatter, body } = extractFrontmatterAndBody(converted); if (!frontmatter) return converted; const name = skillName || extractFrontmatterField(frontmatter, 'name') || 'unknown'; const description = extractFrontmatterField(frontmatter, 'description') || ''; // #2876: quote description so YAML flow indicators in the source // (e.g. `[BETA] …`) don't break downstream frontmatter parsers. const fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\n---`; return `${fm}\n${body}`; } function toSingleLine(value) { return value.replace(/\s+/g, ' ').trim(); } function yamlQuote(value) { return JSON.stringify(value); } function yamlIdentifier(value) { const text = String(value).trim(); if (/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(text)) { return text; } return yamlQuote(text); } function extractFrontmatterAndBody(content) { if (!content.startsWith('---')) { return { frontmatter: null, body: content }; } const endIndex = content.indexOf('---', 3); if (endIndex === -1) { return { frontmatter: null, body: content }; } return { frontmatter: content.substring(3, endIndex).trim(), body: content.substring(endIndex + 3), }; } function extractFrontmatterField(frontmatter, fieldName) { const regex = new RegExp(`^${fieldName}:\\s*(.+)$`, 'm'); const match = frontmatter.match(regex); if (!match) return null; return match[1].trim().replace(/^['"]|['"]$/g, ''); } function convertSlashCommandsToCursorSkillMentions(content) { // Keep leading "/" for slash commands; only normalize gsd: -> gsd-. // This preserves rendered "next step" commands like "/gsd-execute-phase 17". return content.replace(/gsd:/gi, 'gsd-'); } function convertClaudeToCursorMarkdown(content) { let converted = convertSlashCommandsToCursorSkillMentions(content); // Replace tool name references in body text converted = converted.replace(/\bBash\(/g, 'Shell('); converted = converted.replace(/\bEdit\(/g, 'StrReplace('); converted = converted.replace(/\bAskUserQuestion\b/g, 'conversational prompting'); // Replace subagent_type from Claude to Cursor format converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"'); converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}'); // Replace project-level Claude conventions with Cursor equivalents converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.cursor/rules/`'); converted = converted.replace(/\.\/CLAUDE\.md/g, '.cursor/rules/'); converted = converted.replace(/`CLAUDE\.md`/g, '`.cursor/rules/`'); converted = converted.replace(/\bCLAUDE\.md\b/g, '.cursor/rules/'); converted = converted.replace(/\.claude\/skills\//g, '.cursor/skills/'); // Remove Claude Code-specific bug workarounds before brand replacement converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, ''); converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, ''); // Replace "Claude Code" brand references with "Cursor" converted = converted.replace(/\bClaude Code\b/g, 'Cursor'); return converted; } function getCursorSkillAdapterHeader(skillName) { return ` ## A. Skill Invocation - This skill is invoked when the user mentions \`${skillName}\` or describes a task matching this skill. - Treat all user text after the skill mention as \`{{GSD_ARGS}}\`. - If no arguments are present, treat \`{{GSD_ARGS}}\` as empty. ## B. User Prompting When the workflow needs user input, prompt the user conversationally: - Present options as a numbered list in your response text - Ask the user to reply with their choice - For multi-select, ask for comma-separated numbers ## C. Tool Usage Use these Cursor tools when executing GSD workflows: - \`Shell\` for running commands (terminal operations) - \`StrReplace\` for editing existing files - \`Read\`, \`Write\`, \`Glob\`, \`Grep\`, \`Task\`, \`WebSearch\`, \`WebFetch\`, \`TodoWrite\` as needed ## D. Subagent Spawning When the workflow needs to spawn a subagent: - Use \`Task(subagent_type="generalPurpose", ...)\` - The \`model\` parameter maps to Cursor's model options (e.g., "fast") `; } function convertClaudeCommandToCursorSkill(content, skillName) { const converted = convertClaudeToCursorMarkdown(content); const { frontmatter, body } = extractFrontmatterAndBody(converted); let description = `Run GSD workflow ${skillName}.`; if (frontmatter) { const maybeDescription = extractFrontmatterField(frontmatter, 'description'); if (maybeDescription) { description = maybeDescription; } } description = toSingleLine(description); const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; const adapter = getCursorSkillAdapterHeader(skillName); return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`; } /** * Convert a Claude Code command to a Cursor 1.6 slash command (#785). * * Cursor slash commands live in `.cursor/commands/.md` and are * plain markdown — no YAML frontmatter, no adapter header. The filename * becomes the command name (e.g. `gsd-help.md` → `/gsd-help`). * * Applies the same `convertClaudeToCursorMarkdown` transforms as the skill * converter (tool renames, brand substitution, slash-command normalisation), * then strips the YAML frontmatter block so only the prose body remains. * * @param {string} content raw Claude Code command markdown (may have frontmatter) * @param {string} _commandName the target command name (unused; present for * API symmetry with other converters so the runtime-artifact-layout stage * function can call it uniformly) * @returns {string} plain markdown body, no frontmatter */ function convertClaudeCommandToCursorCommand(content, _commandName) { const converted = convertClaudeToCursorMarkdown(content); const { body } = extractFrontmatterAndBody(converted); return body.trimStart(); } // --- Windsurf converters --- // Windsurf uses a tool set similar to Cursor. // Config lives in .windsurf/ (local) and ~/.codeium/windsurf/ (global). function convertSlashCommandsToWindsurfSkillMentions(content) { // Keep leading "/" for slash commands; only normalize gsd: -> gsd-. return content.replace(/gsd:/gi, 'gsd-'); } function convertClaudeToWindsurfMarkdown(content) { let converted = convertSlashCommandsToWindsurfSkillMentions(content); // Replace tool name references in body text converted = converted.replace(/\bBash\(/g, 'Shell('); converted = converted.replace(/\bEdit\(/g, 'StrReplace('); converted = converted.replace(/\bAskUserQuestion\b/g, 'conversational prompting'); // Replace subagent_type from Claude to Windsurf format converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"'); converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}'); // Replace project-level Claude conventions with Windsurf/Devin equivalents // Workspace skills install to .devin/ (Devin Desktop preferred dir, #1085). // Legacy .windsurf/ is still recognized on read but new installs use .devin/. converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.devin/rules`'); converted = converted.replace(/\.\/CLAUDE\.md/g, '.devin/rules'); converted = converted.replace(/`CLAUDE\.md`/g, '`.devin/rules`'); converted = converted.replace(/\bCLAUDE\.md\b/g, '.devin/rules'); converted = converted.replace(/\.claude\/skills\//g, '.devin/skills/'); converted = converted.replace(/\.\/\.claude\//g, './.devin/'); converted = converted.replace(/\.claude\//g, '.devin/'); // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite. // Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore. converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.devin'); converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.devin'); // Environment variable name rewrite converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'WINDSURF_CONFIG_DIR'); // Remove Claude Code-specific bug workarounds before brand replacement converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, ''); converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, ''); // Replace "Claude Code" brand references with "Windsurf" converted = converted.replace(/\bClaude Code\b/g, 'Windsurf'); return converted; } function getWindsurfSkillAdapterHeader(skillName) { return ` ## A. Skill Invocation - This skill is invoked when the user mentions \`${skillName}\` or describes a task matching this skill. - Treat all user text after the skill mention as \`{{GSD_ARGS}}\`. - If no arguments are present, treat \`{{GSD_ARGS}}\` as empty. ## B. User Prompting When the workflow needs user input, prompt the user conversationally: - Present options as a numbered list in your response text - Ask the user to reply with their choice - For multi-select, ask for comma-separated numbers ## C. Tool Usage Use these Windsurf tools when executing GSD workflows: - \`Shell\` for running commands (terminal operations) - \`StrReplace\` for editing existing files - \`Read\`, \`Write\`, \`Glob\`, \`Grep\`, \`Task\`, \`WebSearch\`, \`WebFetch\`, \`TodoWrite\` as needed ## D. Subagent Spawning When the workflow needs to spawn a subagent: - Use \`Task(subagent_type="generalPurpose", ...)\` - The \`model\` parameter maps to Windsurf's model options (e.g., "fast") `; } function convertClaudeCommandToWindsurfSkill(content, skillName) { const converted = convertClaudeToWindsurfMarkdown(content); const { frontmatter, body } = extractFrontmatterAndBody(converted); let description = `Run GSD workflow ${skillName}.`; if (frontmatter) { const maybeDescription = extractFrontmatterField(frontmatter, 'description'); if (maybeDescription) { description = maybeDescription; } } description = toSingleLine(description); const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; const adapter = getWindsurfSkillAdapterHeader(skillName); return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`; } // --- Augment converters --- // Augment uses a tool set similar to Cursor/Windsurf. // Config lives in .augment/ (local) and ~/.augment/ (global). function convertSlashCommandsToAugmentSkillMentions(content) { return content.replace(/gsd:/gi, 'gsd-'); } function convertClaudeToAugmentMarkdown(content) { let converted = convertSlashCommandsToAugmentSkillMentions(content); converted = converted.replace(/\bBash\(/g, 'launch-process('); converted = converted.replace(/\bEdit\(/g, 'str-replace-editor('); converted = converted.replace(/\bRead\(/g, 'view('); converted = converted.replace(/\bWrite\(/g, 'save-file('); converted = converted.replace(/\bTodoWrite\(/g, 'add_tasks('); converted = converted.replace(/\bAskUserQuestion\b/g, 'conversational prompting'); // Replace subagent_type from Claude to Augment format converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"'); converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}'); // Replace project-level Claude conventions with Augment equivalents converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.augment/rules/`'); converted = converted.replace(/\.\/CLAUDE\.md/g, '.augment/rules/'); converted = converted.replace(/`CLAUDE\.md`/g, '`.augment/rules/`'); converted = converted.replace(/\bCLAUDE\.md\b/g, '.augment/rules/'); converted = converted.replace(/\.claude\/skills\//g, '.augment/skills/'); // Remove Claude Code-specific bug workarounds before brand replacement converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, ''); converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, ''); // Replace "Claude Code" brand references with "Augment" converted = converted.replace(/\bClaude Code\b/g, 'Augment'); return converted; } function getAugmentSkillAdapterHeader(skillName) { return ` ## A. Skill Invocation - This skill is invoked when the user mentions \`${skillName}\` or describes a task matching this skill. - Treat all user text after the skill mention as \`{{GSD_ARGS}}\`. - If no arguments are present, treat \`{{GSD_ARGS}}\` as empty. ## B. User Prompting When the workflow needs user input, prompt the user conversationally: - Present options as a numbered list in your response text - Ask the user to reply with their choice - For multi-select, ask for comma-separated numbers ## C. Tool Usage Use these Augment tools when executing GSD workflows: - \`launch-process\` for running commands (terminal operations) - \`str-replace-editor\` for editing existing files - \`view\` for reading files and listing directories - \`save-file\` for creating new files - \`grep\` for searching code (or use MCP servers for advanced search) - \`web-search\`, \`web-fetch\` for web queries - \`add_tasks\`, \`view_tasklist\`, \`update_tasks\` for task management ## D. Subagent Spawning When the workflow needs to spawn a subagent: - Use the built-in subagent spawning capability - Define agent prompts in \`.augment/agents/\` directory `; } function convertClaudeCommandToAugmentSkill(content, skillName) { const converted = convertClaudeToAugmentMarkdown(content); const { frontmatter, body } = extractFrontmatterAndBody(converted); let description = `Run GSD workflow ${skillName}.`; if (frontmatter) { const maybeDescription = extractFrontmatterField(frontmatter, 'description'); if (maybeDescription) { description = maybeDescription; } } description = toSingleLine(description); const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; const adapter = getAugmentSkillAdapterHeader(skillName); return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`; } function convertSlashCommandsToTraeSkillMentions(content) { return content.replace(/\/gsd:([a-z0-9-]+)/g, (_, commandName) => { return `/gsd-${commandName}`; }); } function convertClaudeToTraeMarkdown(content) { let converted = convertSlashCommandsToTraeSkillMentions(content); converted = converted.replace(/\bBash\(/g, 'Shell('); converted = converted.replace(/\bEdit\(/g, 'StrReplace('); // Replace general-purpose subagent type with Trae's equivalent "general_purpose_task" converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="general_purpose_task"'); converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}'); converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.trae/rules/`'); converted = converted.replace(/\.\/CLAUDE\.md/g, '.trae/rules/'); converted = converted.replace(/`CLAUDE\.md`/g, '`.trae/rules/`'); converted = converted.replace(/\bCLAUDE\.md\b/g, '.trae/rules/'); converted = converted.replace(/\.claude\/skills\//g, '.trae/skills/'); converted = converted.replace(/\.\/\.claude\//g, './.trae/'); converted = converted.replace(/\.claude\//g, '.trae/'); // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite. // Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore. converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.trae'); converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.trae'); // Environment variable name rewrite converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'TRAE_CONFIG_DIR'); converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, ''); converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, ''); converted = converted.replace(/\bClaude Code\b/g, 'Trae'); return converted; } function convertClaudeCommandToTraeSkill(content, skillName) { const converted = convertClaudeToTraeMarkdown(content); const { frontmatter, body } = extractFrontmatterAndBody(converted); let description = `Run GSD workflow ${skillName}.`; if (frontmatter) { const maybeDescription = extractFrontmatterField(frontmatter, 'description'); if (maybeDescription) { description = maybeDescription; } } description = toSingleLine(description); const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; // #2876: quote so YAML flow indicators (`[BETA] …`) don't break Trae's // frontmatter parser. return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n${body}`; } function convertSlashCommandsToCodebuddySkillMentions(content) { return content.replace(/\/gsd:([a-z0-9-]+)/g, (_, commandName) => { return `/gsd-${commandName}`; }); } function convertClaudeToCodebuddyMarkdown(content) { let converted = convertSlashCommandsToCodebuddySkillMentions(content); // CodeBuddy uses the same tool names as Claude Code (Bash, Edit, Read, Write, etc.) // No tool name conversion needed converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}'); converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`CODEBUDDY.md`'); converted = converted.replace(/\.\/CLAUDE\.md/g, 'CODEBUDDY.md'); converted = converted.replace(/`CLAUDE\.md`/g, '`CODEBUDDY.md`'); converted = converted.replace(/\bCLAUDE\.md\b/g, 'CODEBUDDY.md'); converted = converted.replace(/\.claude\/skills\//g, '.codebuddy/skills/'); converted = converted.replace(/\.\/\.claude\//g, './.codebuddy/'); converted = converted.replace(/\.claude\//g, '.codebuddy/'); converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, ''); converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, ''); converted = converted.replace(/\bClaude Code\b/g, 'CodeBuddy'); return converted; } function convertClaudeCommandToCodebuddySkill(content, skillName) { const converted = convertClaudeToCodebuddyMarkdown(content); const { frontmatter, body } = extractFrontmatterAndBody(converted); let description = `Run GSD workflow ${skillName}.`; if (frontmatter) { const maybeDescription = extractFrontmatterField(frontmatter, 'description'); if (maybeDescription) { description = maybeDescription; } } description = toSingleLine(description); const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; // #2876: quote so YAML flow indicators (`[BETA] …`) don't break // CodeBuddy's frontmatter parser. // // #789: mark user-invocable:false so the skill is NOT shown in CodeBuddy's // '/' menu (it defaults to true). The commands/ surface (#789) is the sole // '/' entry point; skills remain model-invocable background knowledge, // avoiding a duplicated /gsd-* entry per workflow. return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\nuser-invocable: false\n---\n${body}`; } /** * Convert a Claude Code slash-command (.md) to a CodeBuddy slash-command (.md). * * CodeBuddy reads user-level slash commands from ~/.codebuddy/commands/.md * (https://www.codebuddy.ai/docs/cli/slash-commands). The filename determines the * command name (gsd-help.md → /gsd-help), so the Claude-specific `name: gsd:` * frontmatter field is dropped. CodeBuddy command frontmatter supports * `description` and `argument-hint`; both are preserved when present. The body is * brand/path-converted via convertClaudeToCodebuddyMarkdown. * * @param {string} content raw Claude command markdown * @param {string} commandName installed command name (e.g. 'gsd-help') * @returns {string} */ function convertClaudeCommandToCodebuddyCommand(content, commandName) { const converted = convertClaudeToCodebuddyMarkdown(content); const { frontmatter, body } = extractFrontmatterAndBody(converted); let description = `Run GSD workflow ${commandName}.`; let argumentHint = ''; if (frontmatter) { const maybeDescription = extractFrontmatterField(frontmatter, 'description'); if (maybeDescription) description = maybeDescription; const maybeArgHint = extractFrontmatterField(frontmatter, 'argument-hint'); if (maybeArgHint) argumentHint = maybeArgHint; } description = toSingleLine(description); const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; // #2876: quote values so YAML flow indicators (`[BETA] …`, `[name]`) don't // break CodeBuddy's frontmatter parser. const lines = ['---', `description: ${yamlQuote(shortDescription)}`]; if (argumentHint) lines.push(`argument-hint: ${yamlQuote(toSingleLine(argumentHint))}`); lines.push('---', body.trimStart()); return lines.join('\n'); } // ── Cline converters ──────────────────────────────────────────────────────── function convertClaudeToCliineMarkdown(content) { let converted = content; // Cline uses the same tool names as Claude Code — no tool name conversion needed converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.clinerules`'); converted = converted.replace(/\.\/CLAUDE\.md/g, '.clinerules'); converted = converted.replace(/`CLAUDE\.md`/g, '`.clinerules`'); converted = converted.replace(/\bCLAUDE\.md\b/g, '.clinerules'); // Slash forms first (most specific — superset of bare forms) converted = converted.replace(/\.claude\/skills\//g, '.cline/skills/'); converted = converted.replace(/\.\/\.claude\//g, './.cline/'); converted = converted.replace(/\.claude\//g, '.cline/'); // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite converted = converted.replace(/~\/\.claude\b/g, '~/.cline'); converted = converted.replace(/\$HOME\/\.claude\b/g, '$HOME/.cline'); // Environment variable name rewrite converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'CLINE_CONFIG_DIR'); converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, ''); converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, ''); converted = converted.replace(/\bClaude Code\b/g, 'Cline'); return converted; } /** * Convert a Claude command (.md) to a Cline skill (SKILL.md). * Emits ONLY name + description frontmatter per the Cline skills spec * (https://docs.cline.bot/customization/skills) — no allowed-tools, * argument-hint, agent, or other Claude-specific fields. * Body is hyphen-normalised then converted via convertClaudeToCliineMarkdown * (.claude/→.cline/, "Claude Code"→"Cline", etc.). * Cline uses Claude-Code-compatible tool names, so no adapter header is needed. * Targets ~/.cline/skills//SKILL.md for Cline >= v3.48.0. */ function convertClaudeCommandToClineSkill(content, skillName, _runtime = null, cmdNames = null) { const { frontmatter, body } = extractFrontmatterAndBody(content); if (!frontmatter) return content; // Hyphen-normalise /gsd: → gsd- references in the body, then // apply Cline-specific markdown rewrites (.claude/→.cline/, etc.). const names = cmdNames || readGsdCommandNames(); const normalizedBody = transformContentToHyphen(body, names); const clineBody = convertClaudeToCliineMarkdown(normalizedBody); // Extract description; fall back to a generic string if absent. let description = extractFrontmatterField(frontmatter, 'description'); if (!description) description = `Run GSD workflow ${skillName}.`; description = toSingleLine(description); // Cline documented max is 1024 code points (not UTF-16 code units). // Use Array.from to iterate by code point so that multibyte characters // (e.g. emoji, astral-plane chars) are never split, which would produce // lone surrogates and corrupt the YAML output. const cp = Array.from(description); const shortDescription = cp.length > 1024 ? cp.slice(0, 1021).join('') + '...' : description; const fm = `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---`; return `${fm}\n${clineBody}`; } // ── End Cline converters ───────────────────────────────────────────────────── function convertSlashCommandsToCodexSkillMentions(content) { // Colon-style /gsd: never appears as a filesystem path segment, so no boundary guard is needed (unlike the hyphen-style below). let converted = content.replace(/\/gsd:([a-z0-9-]+)/gi, (_, commandName) => { return `$gsd-${String(commandName).toLowerCase()}`; }); // Convert hyphen-style command references (workflow output) to Codex $ prefix. // A real /gsd- MENTION is defined positively by two boundaries, so any // in-path occurrence is excluded by construction (no denylist of preceding // chars to maintain — see #712, supersedes the #637/#704 lookbehind treadmill): // 1. Left boundary: opens at start-of-string, whitespace, or an inline-prose // delimiter (backtick/quote/paren/bracket) — e.g. `/gsd-execute-phase`. // 2. Right boundary: the command token is NOT followed by a path separator // `/` (a path continues: `/gsd-core/bin/...`; a command does not). The // `(?![a-z0-9/-])` also blocks regex backtracking to a shorter command. // This converts backtick-wrapped MENTIONS (`/gsd-foo`) while leaving backtick- // wrapped PATHS (`/gsd-core/workflows/update.md`) untouched (#712). converted = converted.replace(/(?<=^|[\s`"'([])\/gsd-([a-z0-9-]+)(?![a-z0-9/-])/gi, (_, commandName) => { return `$gsd-${String(commandName).toLowerCase()}`; }); return converted; } const CODEX_GSD_TOOLS_INVOCATION = 'node "$HOME/.codex/gsd-core/bin/gsd-tools.cjs"'; function rewriteBareGsdToolsCommandsForCodex(content) { return content .replace(/(^[ \t]*)gsd-tools(?=\s)/gm, `$1${CODEX_GSD_TOOLS_INVOCATION}`) .replace(/(\$\(\s*)gsd-tools(?=\s)/g, `$1${CODEX_GSD_TOOLS_INVOCATION}`) .replace(/(`\s*)gsd-tools(?=\s)/g, `$1${CODEX_GSD_TOOLS_INVOCATION}`) .replace(/((?:&&|\|\||[;|])\s*)gsd-tools(?=\s)/g, `$1${CODEX_GSD_TOOLS_INVOCATION}`); } function convertClaudeToCodexMarkdown(content) { let converted = convertSlashCommandsToCodexSkillMentions(content); converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}'); // Remove /clear references — Codex has no equivalent command // Handle backtick-wrapped: `\/clear` then: → (removed) converted = converted.replace(/`\/clear`\s*,?\s*then:?\s*\n?/gi, ''); // Handle bare: /clear then: → (removed) converted = converted.replace(/\/clear\s*,?\s*then:?\s*\n?/gi, ''); // Handle standalone /clear on its own line converted = converted.replace(/^\s*`?\/clear`?\s*$/gm, ''); // Path replacement: .claude → .codex (#1430) converted = converted.replace(/\$HOME\/\.claude\//g, '$HOME/.codex/'); converted = converted.replace(/~\/\.claude\//g, '~/.codex/'); converted = converted.replace(/\.\/\.claude\//g, './.codex/'); // Bare ~/.claude without trailing slash (e.g. configDir = ~/.claude) converted = converted.replace(/\$HOME\/\.claude\b/g, '$HOME/.codex'); converted = converted.replace(/~\/\.claude\b/g, '~/.codex'); // Bare/project-relative .claude/... references (#2639). Covers strings like // "check `.claude/skills/`" where there is no ~/, $HOME/, or ./ anchor. // Negative lookbehind prevents double-replacing already-anchored forms and // avoids matching inside URLs or other slash-prefixed paths. converted = converted.replace(/(? ## A. Skill Invocation - This skill is invoked by mentioning \`${invocation}\`. - Treat all user text after \`${invocation}\` as \`{{GSD_ARGS}}\`. - If no arguments are present, treat \`{{GSD_ARGS}}\` as empty. ## B. AskUserQuestion → request_user_input Mapping GSD workflows use \`AskUserQuestion\` (Claude Code syntax). Translate to Codex \`request_user_input\`: Parameter mapping: - \`header\` → \`header\` - \`question\` → \`question\` - Options formatted as \`"Label" — description\` → \`{label: "Label", description: "description"}\` - Generate \`id\` from header: lowercase, replace spaces with underscores Batched calls: - \`AskUserQuestion([q1, q2])\` → single \`request_user_input\` with multiple entries in \`questions[]\` Multi-select workaround: - Codex has no \`multiSelect\`. Use sequential single-selects, or present a numbered freeform list asking the user to enter comma-separated numbers. Execute mode fallback: - When \`request_user_input\` is rejected or unavailable, activate TEXT_MODE: append \`--text\` to \`{{GSD_ARGS}}\` so the workflow's built-in text-mode branching takes over. Present every \`AskUserQuestion\` call as a plain-text numbered list, then stop and wait for the user's reply. Do NOT pick a default and continue (#3018 / #3808). - You may only proceed without a user answer when one of these is true: (a) the invocation included an explicit non-interactive flag (\`--auto\` or \`--all\`), (b) the user has explicitly approved a specific default for this question, or (c) the workflow's documented contract says defaults are safe (e.g. autonomous lifecycle paths). - Do NOT write workflow artifacts (CONTEXT.md, DISCUSSION-LOG.md, PLAN.md, checkpoint files) until the user has answered the plain-text questions or one of (a)-(c) above applies. Surfacing the questions and waiting is the correct response — silently defaulting and writing artifacts is the #3018 failure mode. ## C. Task() → spawn_agent Mapping GSD workflows use \`Task(...)\` (Claude Code syntax). Translate to Codex collaboration tools: **Schema detection (required first step):** Codex exposes two \`spawn_agent\` schemas: - **agent_type-capable schema** (e.g. \`multi_agent_v2\`): \`spawn_agent\` accepts \`agent_type\`, \`message\`, \`reasoning_effort\`, \`fork_context\`, etc. — typed GSD agent dispatch is available. - **Generic schema** (\`multi_agent_v1\`): \`spawn_agent\` accepts only \`message\`, \`items\`, \`fork_context\` — there is **no \`agent_type\` field**. Typed GSD agent dispatch is unavailable in this session. Before spawning, inspect the \`spawn_agent\` tool's visible parameter schema (via \`tool_search\` or the tool list) to determine which form is active. Typed mapping (agent_type-capable schema only): - \`Task(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\` - \`Agent(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\` - \`Task(model="...")\` → omit. \`spawn_agent\` has no inline \`model\` parameter; GSD embeds the resolved per-agent model directly into each agent's \`.toml\` at install time so \`model_overrides\` from \`.planning/config.json\` and \`~/.gsd/defaults.json\` are honored automatically by Codex's agent router. - Resolved \`reasoning_effort="low|medium|high|xhigh"\` (\`xhigh\` is a GSD/Codex tier, not a generic runtime enum) → pass \`reasoning_effort\` to \`spawn_agent\` when the runtime/tool supports it. Omit missing, empty, inherited, or unsupported values; do not invent one-off effort literals in workflow prose. - \`fork_context: false\` by default — GSD agents load their own context via \`\` blocks - \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct Codex mapping. Codex \`spawn_agent\` does not create or bind a git worktree automatically. Workflows that require this isolation must fail closed or use an explicit manual worktree protocol before spawning (#3360). Generic-agent workaround (multi_agent_v1 schema — NO agent_type field): When only the generic \`multi_agent_v1\` schema is available, typed GSD agent dispatch (\`gsd-planner\`, \`gsd-executor\`, etc.) is NOT possible. This is a known Codex limitation (openai/codex#15250). **This workaround is NOT equivalent to typed gsd-planner/gsd-executor execution** — GSD agents carry project-aware prompts, audit logging, and workflow context that a generic subagent lacks. Use the following fallback: 1. Resolve your active Codex config root — the directory that contains your \`config.toml\`. This directory is determined in priority order: \`$CODEX_HOME\` (if set), the path given by \`--config-dir\` (if passed on invocation), a local \`.codex\` directory in the current project (if \`--local\` was used), or the default global config directory. Read \`agents/.toml\` relative to that config root to extract the agent's system instructions. 2. Inject those instructions as a role-preamble into a generic \`spawn_agent(message=...)\` call. 3. Label results and logs clearly as "generic-agent workaround" so the orchestrator and user know full typed-agent guarantees are not in effect. 4. Where typed dispatch is mandatory for correctness (e.g. worktree isolation), fail closed and report the schema limitation rather than silently degrading. Spawn restriction: - Codex restricts \`spawn_agent\` to cases where the user has explicitly requested sub-agents. When automatic spawning is not permitted, do the work inline in the current agent rather than attempting to force a spawn. - In some Codex sessions, multi-agent tooling can be deferred. If \`spawn_agent\` is not currently visible, discover tools first via \`tool_search\` before defaulting to inline execution. Parallel fan-out: - Spawn multiple agents → collect agent IDs → \`wait(ids)\` for all to complete Result parsing: - Look for structured markers in agent output: \`CHECKPOINT\`, \`PLAN COMPLETE\`, \`SUMMARY\`, etc. - \`close_agent(id)\` after collecting results from each agent `; } function convertClaudeCommandToCodexSkill(content, skillName) { const converted = convertClaudeToCodexMarkdown(content); const { frontmatter, body } = extractFrontmatterAndBody(converted); let description = `Run GSD workflow ${skillName}.`; if (frontmatter) { const maybeDescription = extractFrontmatterField(frontmatter, 'description'); if (maybeDescription) { description = maybeDescription; } } description = toSingleLine(description); const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; const adapter = getCodexSkillAdapterHeader(skillName); return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(description)}\nmetadata:\n short-description: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`; } function neutralizeAgentReferences(content, instructionFile) { let c = content; // Replace standalone "Claude" (the agent) but preserve product/model names. // Negative lookahead avoids: Claude Code, Claude Opus/Sonnet/Haiku, Claude native, Claude-based c = c.replace(/\bClaude(?! Code| Opus| Sonnet| Haiku| native| based|-)\b(?!\.md)/g, 'the agent'); // Replace CLAUDE.md with runtime-appropriate instruction file if (instructionFile) { c = c.replace(/CLAUDE\.md/g, instructionFile); } // Remove instructions that conflict with AGENTS.md-based runtimes c = c.replace(/Do NOT load full `AGENTS\.md` files[^\n]*/g, ''); return c; } function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOverride = null } = {}) { // Replace tool name references in content (applies to all files) let convertedContent = content; convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question'); convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill'); convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite'); // Replace /gsd-command colon variant with /gsd-command for opencode (flat command structure) convertedContent = convertedContent.replace(/\/gsd:/g, '/gsd-'); // Replace ~/.claude and $HOME/.claude with OpenCode's config location convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/opencode'); convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/opencode'); // Replace general-purpose subagent type with OpenCode's equivalent "general" convertedContent = convertedContent.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"'); // Runtime-neutral agent name replacement (#766) convertedContent = neutralizeAgentReferences(convertedContent, 'AGENTS.md'); // Check if content has frontmatter if (!convertedContent.startsWith('---')) { return convertedContent; } // Find the end of frontmatter const endIndex = convertedContent.indexOf('---', 3); if (endIndex === -1) { return convertedContent; } const frontmatter = convertedContent.substring(3, endIndex).trim(); const body = convertedContent.substring(endIndex + 3); // Parse frontmatter line by line (simple YAML parsing) const lines = frontmatter.split('\n'); const newLines = []; let inAllowedTools = false; let inSkippedArray = false; const allowedTools = []; for (const line of lines) { const trimmed = line.trim(); // For agents: skip commented-out lines (e.g. hooks blocks) if (isAgent && trimmed.startsWith('#')) { continue; } // Detect start of allowed-tools array if (trimmed.startsWith('allowed-tools:')) { inAllowedTools = true; continue; } // Detect inline tools: field (comma-separated string) if (trimmed.startsWith('tools:')) { if (isAgent) { // Agents: strip tools entirely (not supported in OpenCode agent frontmatter) inSkippedArray = true; continue; } const toolsValue = trimmed.substring(6).trim(); if (toolsValue) { // Parse comma-separated tools const tools = toolsValue.split(',').map(t => t.trim()).filter(t => t); allowedTools.push(...tools); } continue; } // For agents: strip skills:, color:, memory:, maxTurns:, permissionMode:, disallowedTools: if (isAgent && /^(skills|color|memory|maxTurns|permissionMode|disallowedTools):/.test(trimmed)) { inSkippedArray = true; continue; } // Skip continuation lines of a stripped array/object field if (inSkippedArray) { if (trimmed.startsWith('- ') || trimmed.startsWith('#') || /^\s/.test(line)) { continue; } inSkippedArray = false; } // For commands: remove name: field (opencode uses filename for command name) // For agents: keep name: (required by OpenCode agents) if (!isAgent && trimmed.startsWith('name:')) { continue; } // Strip model: field — OpenCode doesn't support Claude Code model aliases // like 'haiku', 'sonnet', 'opus', or 'inherit'. Omitting lets OpenCode use // its configured default model. See #1156. if (trimmed.startsWith('model:')) { continue; } // Convert color names to hex for opencode (commands only; agents strip color above) if (trimmed.startsWith('color:')) { const colorValue = trimmed.substring(6).trim().toLowerCase(); const hexColor = colorNameToHex[colorValue]; if (hexColor) { newLines.push(`color: "${hexColor}"`); } else if (colorValue.startsWith('#')) { // Validate hex color format (#RGB or #RRGGBB) if (/^#[0-9a-f]{3}$|^#[0-9a-f]{6}$/i.test(colorValue)) { // Already hex and valid, keep as is newLines.push(line); } // Skip invalid hex colors } // Skip unknown color names continue; } // Collect allowed-tools items if (inAllowedTools) { if (trimmed.startsWith('- ')) { allowedTools.push(trimmed.substring(2).trim()); continue; } else if (trimmed && !trimmed.startsWith('-')) { // End of array, new field started inAllowedTools = false; } } // Keep other fields if (!inAllowedTools) { newLines.push(line); } } // For agents: add required OpenCode agent fields // Note: Do NOT add 'model: inherit' — OpenCode does not recognize the 'inherit' // keyword and throws ProviderModelNotFoundError. Omitting model: lets OpenCode // use its default model for subagents. See #1156. if (isAgent) { newLines.push('mode: subagent'); // Embed model override from ~/.gsd/defaults.json so model_overrides is // respected on OpenCode (which uses static agent frontmatter, not inline // Task() model parameters). See #2256. if (modelOverride) { newLines.push(['model:', modelOverride].join(' ')); } } // For commands: add tools object if we had allowed-tools or tools if (!isAgent && allowedTools.length > 0) { newLines.push('tools:'); for (const tool of allowedTools) { newLines.push(` ${convertToolName(tool)}: true`); } } // Rebuild frontmatter (body already has tool names converted) const newFrontmatter = newLines.join('\n').trim(); return `---\n${newFrontmatter}\n---${body}`; } // Kilo CLI — same conversion logic as OpenCode, different config paths. function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) { // Replace tool name references in content (applies to all files) let convertedContent = content; convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question'); convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill'); convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite'); // Replace /gsd-command colon variant with /gsd-command for Kilo (flat command structure) convertedContent = convertedContent.replace(/\/gsd:/g, '/gsd-'); // Replace ~/.claude and $HOME/.claude with Kilo's config location convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/kilo'); convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/kilo'); convertedContent = convertedContent.replace(/\.\/\.claude\//g, './.kilo/'); // Normalize both Claude skill directory variants to Kilo's canonical skills dir. convertedContent = replaceRelativePathReference(convertedContent, '.claude/skills/', '.kilo/skills/'); convertedContent = replaceRelativePathReference(convertedContent, '.agents/skills/', '.kilo/skills/'); convertedContent = replaceRelativePathReference(convertedContent, '.claude/agents/', '.kilo/agents/'); // Replace general-purpose subagent type with Kilo's equivalent "general" convertedContent = convertedContent.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"'); // Runtime-neutral agent name replacement (#766) convertedContent = neutralizeAgentReferences(convertedContent, 'AGENTS.md'); // Check if content has frontmatter if (!convertedContent.startsWith('---')) { return convertedContent; } // Find the end of frontmatter const endIndex = convertedContent.indexOf('---', 3); if (endIndex === -1) { return convertedContent; } const frontmatter = convertedContent.substring(3, endIndex).trim(); const body = convertedContent.substring(endIndex + 3); // Parse frontmatter line by line (simple YAML parsing) const lines = frontmatter.split('\n'); const newLines = []; let inAllowedTools = false; let inAgentTools = false; let inSkippedArray = false; const allowedTools = []; const agentTools = []; for (const line of lines) { const trimmed = line.trim(); // For agents: skip commented-out lines (e.g. hooks blocks) if (isAgent && trimmed.startsWith('#')) { continue; } // Detect start of allowed-tools array if (trimmed.startsWith('allowed-tools:')) { inAllowedTools = true; continue; } if (isAgent && inAgentTools) { if (trimmed.startsWith('- ')) { agentTools.push(trimmed.substring(2).trim()); continue; } if (trimmed && !trimmed.startsWith('-')) { inAgentTools = false; } } // Detect inline tools: field (comma-separated string) if (trimmed.startsWith('tools:')) { if (isAgent) { const toolsValue = trimmed.substring(6).trim(); if (toolsValue) { const tools = toolsValue.split(',').map(t => t.trim()).filter(t => t); agentTools.push(...tools); } else { inAgentTools = true; } continue; } const toolsValue = trimmed.substring(6).trim(); if (toolsValue) { // Parse comma-separated tools const tools = toolsValue.split(',').map(t => t.trim()).filter(t => t); allowedTools.push(...tools); } continue; } // For agents: strip skills:, color:, memory:, maxTurns:, permissionMode:, disallowedTools: if (isAgent && /^(skills|color|memory|maxTurns|permissionMode|disallowedTools):/.test(trimmed)) { inSkippedArray = true; continue; } // Skip continuation lines of a stripped array/object field if (inSkippedArray) { if (trimmed.startsWith('- ') || trimmed.startsWith('#') || /^\s/.test(line)) { continue; } inSkippedArray = false; } // For commands: remove name: field (Kilo uses filename for command name) // For agents: keep name: (required by Kilo agents) if (!isAgent && trimmed.startsWith('name:')) { continue; } // Strip model: field — Kilo doesn't support Claude Code model aliases // like 'haiku', 'sonnet', 'opus', or 'inherit'. Omitting lets Kilo use // its configured default model. if (trimmed.startsWith('model:')) { continue; } // Convert color names to hex for Kilo (commands only; agents strip color above) if (trimmed.startsWith('color:')) { const colorValue = trimmed.substring(6).trim().toLowerCase(); const hexColor = colorNameToHex[colorValue]; if (hexColor) { newLines.push(`color: "${hexColor}"`); } else if (colorValue.startsWith('#')) { // Validate hex color format (#RGB or #RRGGBB) if (/^#[0-9a-f]{3}$|^#[0-9a-f]{6}$/i.test(colorValue)) { // Already hex and valid, keep as is newLines.push(line); } // Skip invalid hex colors } // Skip unknown color names continue; } // Collect allowed-tools items if (inAllowedTools) { if (trimmed.startsWith('- ')) { const tool = trimmed.substring(2).trim(); if (isAgent) { agentTools.push(tool); } else { allowedTools.push(tool); } continue; } else if (trimmed && !trimmed.startsWith('-')) { // End of array, new field started inAllowedTools = false; } } // Keep other fields if (!inAllowedTools) { newLines.push(line); } } // For agents: add required Kilo agent fields if (isAgent) { newLines.push('mode: subagent'); newLines.push(...buildKiloAgentPermissionBlock(agentTools)); } // For commands: add tools object if we had allowed-tools or tools if (!isAgent && allowedTools.length > 0) { newLines.push('tools:'); for (const tool of allowedTools) { newLines.push(` ${convertToolName(tool)}: true`); } } // Rebuild frontmatter (body already has tool names converted) const newFrontmatter = newLines.join('\n').trim(); return `---\n${newFrontmatter}\n---${body}`; } // ── Agent converters — #1182 extraction ───────────────────────────────────── // These were previously only in bin/install.js. Extracted here so the module // is self-contained and #1173 descriptor-driven dispatch can call them without // reaching through the Installer Module. // NOTE: Do NOT remove the inline copies from bin/install.js in this PR — // that is #1175. This PR only adds them to the module's export surface. // Copilot tool name mapping — Claude Code tools to GitHub Copilot tools // Tool mapping applies ONLY to agents, NOT to skills (per CONTEXT.md decision) const claudeToCopilotTools = { Read: 'read', Write: 'edit', Edit: 'edit', Bash: 'execute', Grep: 'search', Glob: 'search', Task: 'agent', WebSearch: 'web', WebFetch: 'web', TodoWrite: 'todo', AskUserQuestion: 'ask_user', SlashCommand: 'skill', }; // Tool name mapping from Claude Code to Gemini CLI // Gemini CLI uses snake_case built-in tool names const claudeToGeminiTools = { Read: 'read_file', Write: 'write_file', Edit: 'replace', Bash: 'run_shell_command', Glob: 'glob', Grep: 'search_file_content', WebSearch: 'google_web_search', WebFetch: 'web_fetch', TodoWrite: 'write_todos', }; /** * Convert a Claude Code tool name to Gemini CLI format * - Applies Claude→Gemini mapping (Read→read_file, Bash→run_shell_command, etc.) * - Filters out MCP tools (mcp__*) — they are auto-discovered at runtime in Gemini * - Filters out Task/Agent — agents are auto-registered as tools in Gemini * @returns {string|null} Gemini tool name, or null if tool should be excluded */ function convertGeminiToolName(claudeTool) { // MCP tools: exclude — auto-discovered from mcpServers config at runtime if (claudeTool.startsWith('mcp__')) { return null; } // Task/Agent: exclude — agents are auto-registered as callable tools. // AskUserQuestion: exclude — Gemini CLI does not expose an ask_user tool; // emitting it causes frontmatter validation errors (#3362). if ( claudeTool === 'Task' || claudeTool === 'Agent' || claudeTool === 'AskUserQuestion' || claudeTool === 'ask_user' ) { return null; } // Check for explicit mapping if (claudeToGeminiTools[claudeTool]) { return claudeToGeminiTools[claudeTool]; } // Default: lowercase return claudeTool.toLowerCase(); } /** * Convert a Claude Code tool name to GitHub Copilot format. * - Applies explicit mapping from claudeToCopilotTools * - Handles mcp__context7__* prefix → io.github.upstash/context7/* * - Falls back to lowercase for unknown tools */ function convertCopilotToolName(claudeTool) { // mcp__context7__* wildcard → io.github.upstash/context7/* if (claudeTool.startsWith('mcp__context7__')) { return 'io.github.upstash/context7/' + claudeTool.slice('mcp__context7__'.length); } // Check explicit mapping if (claudeToCopilotTools[claudeTool]) { return claudeToCopilotTools[claudeTool]; } // mcp__{tavily,ref,jina,exa,firecrawl}__* use the generic MCP passthrough like exa/firecrawl; // add explicit Copilot registry mappings when the io.github ids are confirmed (#657 follow-up) // Default: lowercase return claudeTool.toLowerCase(); } /** * Convert a Claude agent (.md) to a GitHub Copilot agent. * CONV-04: JSON array format. CONV-05: Tool name mapping. */ function convertClaudeAgentToCopilotAgent(content, isGlobal = false) { const converted = convertClaudeToCopilotContent(content, isGlobal); const { frontmatter, body } = extractFrontmatterAndBody(converted); if (!frontmatter) return converted; const name = extractFrontmatterField(frontmatter, 'name') || 'unknown'; const description = extractFrontmatterField(frontmatter, 'description') || ''; const color = extractFrontmatterField(frontmatter, 'color'); const toolsRaw = extractFrontmatterField(frontmatter, 'tools') || ''; // CONV-04 + CONV-05: Map tools, deduplicate, format as JSON array const claudeTools = toolsRaw.split(',').map(t => t.trim()).filter(Boolean); const mappedTools = claudeTools.map(t => convertCopilotToolName(t)); const uniqueTools = [...new Set(mappedTools)]; const toolsArray = uniqueTools.length > 0 ? "['" + uniqueTools.join("', '") + "']" : '[]'; // Reconstruct frontmatter in Copilot format. Quote description (#2876) // so a leading YAML flow indicator (`[BETA] …`, `{ … }`, etc.) doesn't // crash the Copilot frontmatter loader. let fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\ntools: ${toolsArray}\n`; if (color) fm += `color: ${color}\n`; fm += '---'; return `${fm}\n${body}`; } /** * Convert a Claude agent (.md) to an Antigravity agent. * Uses Gemini tool names since Antigravity runs on Gemini 3 backend. */ function convertClaudeAgentToAntigravityAgent(content, isGlobal = false) { const converted = convertClaudeToAntigravityContent(content, isGlobal); const { frontmatter, body } = extractFrontmatterAndBody(converted); if (!frontmatter) return converted; const name = extractFrontmatterField(frontmatter, 'name') || 'unknown'; const description = extractFrontmatterField(frontmatter, 'description') || ''; const color = extractFrontmatterField(frontmatter, 'color'); const toolsRaw = extractFrontmatterField(frontmatter, 'tools') || ''; // Map tools to Gemini equivalents (reuse existing convertGeminiToolName) const claudeTools = toolsRaw.split(',').map(t => t.trim()).filter(Boolean); const mappedTools = claudeTools.map(t => convertGeminiToolName(t)).filter(Boolean); // #2876: quote description for the same reason as the skill variant. let fm = `---\nname: ${name}\ndescription: ${yamlQuote(description)}\ntools: ${mappedTools.join(', ')}\n`; if (color) fm += `color: ${color}\n`; fm += '---'; return `${fm}\n${body}`; } /** * Convert Claude Code agent markdown to Cursor agent format. * Strips frontmatter fields Cursor doesn't support (color, skills), * converts tool references, and adds a role context header. */ function convertClaudeAgentToCursorAgent(content) { const converted = convertClaudeToCursorMarkdown(content); const { frontmatter, body } = extractFrontmatterAndBody(converted); if (!frontmatter) return converted; const name = extractFrontmatterField(frontmatter, 'name') || 'unknown'; const description = extractFrontmatterField(frontmatter, 'description') || ''; const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`; return `${cleanFrontmatter}\n${body}`; } /** * Convert Claude Code agent markdown to Windsurf agent format. * Strips frontmatter fields Windsurf doesn't support (color, skills), * converts tool references, and adds a role context header. */ function convertClaudeAgentToWindsurfAgent(content) { const converted = convertClaudeToWindsurfMarkdown(content); const { frontmatter, body } = extractFrontmatterAndBody(converted); if (!frontmatter) return converted; const name = extractFrontmatterField(frontmatter, 'name') || 'unknown'; const description = extractFrontmatterField(frontmatter, 'description') || ''; const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`; return `${cleanFrontmatter}\n${body}`; } /** * Convert Claude Code agent markdown to Augment agent format. * Strips frontmatter fields Augment doesn't support (color, skills), * converts tool references, and cleans up for Augment agents. */ function convertClaudeAgentToAugmentAgent(content) { const converted = convertClaudeToAugmentMarkdown(content); const { frontmatter, body } = extractFrontmatterAndBody(converted); if (!frontmatter) return converted; const name = extractFrontmatterField(frontmatter, 'name') || 'unknown'; const description = extractFrontmatterField(frontmatter, 'description') || ''; const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`; return `${cleanFrontmatter}\n${body}`; } function convertClaudeAgentToTraeAgent(content) { const converted = convertClaudeToTraeMarkdown(content); const { frontmatter, body } = extractFrontmatterAndBody(converted); if (!frontmatter) return converted; const name = extractFrontmatterField(frontmatter, 'name') || 'unknown'; const description = extractFrontmatterField(frontmatter, 'description') || ''; const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`; return `${cleanFrontmatter}\n${body}`; } function convertClaudeAgentToCodebuddyAgent(content) { const converted = convertClaudeToCodebuddyMarkdown(content); const { frontmatter, body } = extractFrontmatterAndBody(converted); if (!frontmatter) return converted; const name = extractFrontmatterField(frontmatter, 'name') || 'unknown'; const description = extractFrontmatterField(frontmatter, 'description') || ''; const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`; return `${cleanFrontmatter}\n${body}`; } function convertClaudeAgentToClineAgent(content) { const converted = convertClaudeToCliineMarkdown(content); const { frontmatter, body } = extractFrontmatterAndBody(converted); if (!frontmatter) return converted; const name = extractFrontmatterField(frontmatter, 'name') || 'unknown'; const description = extractFrontmatterField(frontmatter, 'description') || ''; const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`; return `${cleanFrontmatter}\n${body}`; } /** * Convert Claude Code agent markdown to Codex agent format. * Applies base markdown conversions, then adds a header * and cleans up frontmatter (removes tools/color fields). */ function convertClaudeAgentToCodexAgent(content) { const converted = convertClaudeToCodexMarkdown(content); const { frontmatter, body } = extractFrontmatterAndBody(converted); if (!frontmatter) return converted; const name = extractFrontmatterField(frontmatter, 'name') || 'unknown'; const description = extractFrontmatterField(frontmatter, 'description') || ''; const tools = extractFrontmatterField(frontmatter, 'tools') || ''; const roleHeader = ` role: ${name} tools: ${tools} purpose: ${toSingleLine(description)} `; const cleanFrontmatter = `---\nname: ${yamlQuote(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`; return `${cleanFrontmatter}\n\n${roleHeader}\n${body}`; } // ── End agent converters #1182 ─────────────────────────────────────────────── /** * Shared SKILL.md writer for the OpenCode-family runtimes (OpenCode + Kilo), * which share a config schema (Kilo derives from OpenCode). OpenCode discovers * skills as `skills//SKILL.md` and Kilo follows the same layout * (https://opencode.ai/docs/skills, https://kilo.ai/docs/customize/skills). * * The skill body reuses the runtime's command-frontmatter converter for tool, * path, and `/gsd:`→`/gsd-` body rewrites, then rebuilds a minimal skill * frontmatter: only `name` (lowercase-hyphen, must match the containing * directory) and `description` (1–1024 chars) are emitted, per the OpenCode * skill spec. The command's `tools:`/`permission:` block is intentionally * dropped — OpenCode skills are loaded on-demand via the native skill tool and * inherit the calling agent's permissions. * * @param {string} content - Claude command markdown (with YAML frontmatter) * @param {string} skillName - Skill directory name (e.g. gsd-help) * @param {(content: string) => string} frontmatterConverter - runtime command converter * @returns {string} SKILL.md content */ function convertClaudeCommandToOpencodeFamilySkill(content, skillName, frontmatterConverter) { const converted = frontmatterConverter(content); const { frontmatter, body } = extractFrontmatterAndBody(converted); let description = `Run GSD workflow ${skillName}.`; if (frontmatter) { const maybeDescription = extractFrontmatterField(frontmatter, 'description'); if (maybeDescription) { description = maybeDescription; } } description = toSingleLine(description); // OpenCode skill descriptions must be 1–1024 characters. if (description.length > 1024) { description = `${description.slice(0, 1021)}...`; } // `name` must be lowercase alphanumeric with single-hyphen separators and // match the containing directory name (the staged dir is `${skillName}/`). const name = yamlIdentifier(skillName); return `---\nname: ${name}\ndescription: ${yamlQuote(description)}\n---\n\n${body.trimStart()}`; } /** * Convert a Claude command (.md) to an OpenCode skill (SKILL.md). * Thin wrapper over the shared OpenCode-family writer. */ function convertClaudeCommandToOpencodeSkill(content, skillName) { return convertClaudeCommandToOpencodeFamilySkill( content, skillName, (c) => convertClaudeToOpencodeFrontmatter(c), ); } /** * Convert a Claude command (.md) to a Kilo skill (SKILL.md). * Thin wrapper over the shared OpenCode-family writer (Kilo shares the schema). */ function convertClaudeCommandToKiloSkill(content, skillName) { return convertClaudeCommandToOpencodeFamilySkill( content, skillName, (c) => convertClaudeToKiloFrontmatter(c), ); } export = { yamlIdentifier, yamlQuote, toSingleLine, extractFrontmatterAndBody, extractFrontmatterField, skillFrontmatterName, convertClaudeToCopilotContent, convertClaudeCommandToCopilotSkill, convertClaudeToAntigravityContent, convertClaudeCommandToAntigravitySkill, convertClaudeCommandToClaudeSkill, convertClaudeCommandToKimiSkill, buildKimiAgentArtifacts, convertClaudeToCursorMarkdown, convertClaudeCommandToCursorSkill, convertClaudeCommandToCursorCommand, convertClaudeToWindsurfMarkdown, convertClaudeCommandToWindsurfSkill, convertClaudeToAugmentMarkdown, convertClaudeCommandToAugmentSkill, convertClaudeToTraeMarkdown, convertClaudeCommandToTraeSkill, convertClaudeToCodebuddyMarkdown, convertClaudeCommandToCodebuddySkill, convertClaudeCommandToCodebuddyCommand, convertClaudeToCliineMarkdown, convertClaudeCommandToClineSkill, convertSlashCommandsToCodexSkillMentions, getCodexSkillAdapterHeader, convertClaudeToCodexMarkdown, convertClaudeCommandToCodexSkill, neutralizeAgentReferences, convertClaudeCommandToOpencodeSkill, convertClaudeCommandToKiloSkill, readGsdCommandNames, transformContentToHyphen, // #1182: agent converters + tool-name table dependency closure claudeToCopilotTools, convertCopilotToolName, claudeToGeminiTools, convertGeminiToolName, convertClaudeAgentToCopilotAgent, convertClaudeAgentToAntigravityAgent, convertClaudeAgentToCursorAgent, convertClaudeAgentToWindsurfAgent, convertClaudeAgentToAugmentAgent, convertClaudeAgentToTraeAgent, convertClaudeAgentToCodebuddyAgent, convertClaudeAgentToClineAgent, convertClaudeAgentToCodexAgent, };