Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
367 changes: 366 additions & 1 deletion lib/adapter-transforms.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,10 +297,375 @@ function transformForCodex(content, options) {
return content;
}

/**
* Transform content for Cursor (.mdc rule files).
*
* MDC format uses YAML frontmatter with `description`, `globs`, and
* `alwaysApply` fields followed by a markdown body.
*
* @param {string} content - Source markdown content (may have frontmatter)
* @param {Object} options
* @param {string} options.description - Rule description
* @param {string} options.pluginInstallPath - Absolute path to plugin install dir
* @param {string} [options.globs] - Optional glob pattern for file matching
* @param {boolean} [options.alwaysApply] - Whether rule always applies (default true)
* @returns {string} Transformed MDC content
*/
function transformRuleForCursor(content, options) {
const { description = '', pluginInstallPath, globs = '', alwaysApply = true } = options;

// Strip control characters and escape description for YAML
const cleanDescription = description.replace(/[\x00-\x1f\x7f]/g, ' ');
const escapedDescription = cleanDescription.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
const yamlDescription = `"${escapedDescription}"`;

// Build MDC frontmatter
let frontmatter = `---\ndescription: ${yamlDescription}\n`;
if (globs) {
frontmatter += `globs: ${JSON.stringify(globs)}\n`;
}
frontmatter += `alwaysApply: ${alwaysApply}\n---\n`;

// Strip existing frontmatter if present
if (content.startsWith('---')) {
content = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
}

content = frontmatter + content;

// Replace PLUGIN_ROOT paths with actual install path
// Use function replacement to avoid $ pattern interpretation in replacement string
content = content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => pluginInstallPath);
content = content.replace(/\$CLAUDE_PLUGIN_ROOT/g, () => pluginInstallPath);
content = content.replace(/\$\{PLUGIN_ROOT\}/g, () => pluginInstallPath);
content = content.replace(/\$PLUGIN_ROOT/g, () => pluginInstallPath);

// Strip Claude-specific syntax: Task tool calls (handles one level of nested braces)
content = content.replace(/await\s+Task\s*\(\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}\s*\);?/g, (match) => {
const agentMatch = match.match(/subagent_type:\s*["'](?:[^"':]+:)?([^"']+)["']/);
if (agentMatch) {
return `Invoke the ${agentMatch[1]} agent`;
}
return '';
});

// Strip require() statements
content = content.replace(/(?:const|let|var)\s+\{?[^}=\n]+\}?\s*=\s*require\s*\([^)]+\);?/g, '');
content = content.replace(/require\s*\(['"][^'"]+['"]\)/g, '');

// Strip plugin namespacing (e.g. next-task:agent-name -> agent-name)
content = content.replace(/(?:next-task|deslop|ship|sync-docs|audit-project|enhance|perf|repo-map|drift-detect|consult|debate|learn|web-ctl):([a-z][a-z0-9-]*)/g, '$1');

return content;
}

/**
* Transform skill content for Cursor.
*
* Minimal transform - Cursor reads SKILL.md frontmatter natively so we
* preserve it. Only replaces PLUGIN_ROOT paths and strips namespace prefixes.
*
* @param {string} content - Source SKILL.md content
* @param {Object} options
* @param {string} options.pluginInstallPath - Absolute path to plugin install dir
* @returns {string} Transformed skill content
*/
function transformSkillForCursor(content, options) {
const { pluginInstallPath } = options;

// Replace PLUGIN_ROOT paths with actual install path
content = content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => pluginInstallPath);
content = content.replace(/\$CLAUDE_PLUGIN_ROOT/g, () => pluginInstallPath);
content = content.replace(/\$\{PLUGIN_ROOT\}/g, () => pluginInstallPath);
content = content.replace(/\$PLUGIN_ROOT/g, () => pluginInstallPath);

// Strip plugin namespacing (e.g. next-task:agent-name -> agent-name)
content = content.replace(/(?:next-task|deslop|ship|sync-docs|audit-project|enhance|perf|repo-map|drift-detect|consult|debate|learn|web-ctl):([a-z][a-z0-9-]*)/g, '$1');

return content;
}

/**
* Transform command content for Cursor.
*
* Light transform - strips frontmatter, replaces PLUGIN_ROOT paths,
* removes require() statements and Task() calls, strips namespace prefixes.
*
* @param {string} content - Source command markdown content
* @param {Object} options
* @param {string} options.pluginInstallPath - Absolute path to plugin install dir
* @returns {string} Transformed command content
*/
function transformCommandForCursor(content, options) {
const { pluginInstallPath } = options;

// Strip existing frontmatter if present
if (content.startsWith('---')) {
content = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
}

// Replace PLUGIN_ROOT paths with actual install path
content = content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => pluginInstallPath);
content = content.replace(/\$CLAUDE_PLUGIN_ROOT/g, () => pluginInstallPath);
content = content.replace(/\$\{PLUGIN_ROOT\}/g, () => pluginInstallPath);
content = content.replace(/\$PLUGIN_ROOT/g, () => pluginInstallPath);

// Strip require() statements
content = content.replace(/(?:const|let|var)\s+\{?[^}=\n]+\}?\s*=\s*require\s*\([^)]+\);?/g, '');
content = content.replace(/require\s*\(['"][^'"]+['"]\)/g, '');

// Strip Claude-specific syntax: Task tool calls (handles one level of nested braces)
content = content.replace(/await\s+Task\s*\(\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}\s*\);?/g, (match) => {
const agentMatch = match.match(/subagent_type:\s*["'](?:[^"':]+:)?([^"']+)["']/);
if (agentMatch) {
return `Invoke the ${agentMatch[1]} agent`;
}
return '';
});

// Strip plugin namespacing (e.g. next-task:agent-name -> agent-name)
content = content.replace(/(?:next-task|deslop|ship|sync-docs|audit-project|enhance|perf|repo-map|drift-detect|consult|debate|learn|web-ctl):([a-z][a-z0-9-]*)/g, '$1');

return content;
}

/**
* Transform skill content for Kiro.
*
* Minimal transform - Kiro reads standard SKILL.md format natively so we
* preserve it. Only replaces PLUGIN_ROOT paths and strips namespace prefixes.
*
* @param {string} content - Source SKILL.md content
* @param {Object} options
* @param {string} options.pluginInstallPath - Absolute path to plugin install dir
* @returns {string} Transformed skill content
*/
function transformSkillForKiro(content, options) {
const { pluginInstallPath } = options;

content = content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => pluginInstallPath);
content = content.replace(/\$CLAUDE_PLUGIN_ROOT/g, () => pluginInstallPath);
content = content.replace(/\$\{PLUGIN_ROOT\}/g, () => pluginInstallPath);
content = content.replace(/\$PLUGIN_ROOT/g, () => pluginInstallPath);

content = content.replace(/(?:next-task|deslop|ship|sync-docs|audit-project|enhance|perf|repo-map|drift-detect|consult|debate|learn|web-ctl):([a-z][a-z0-9-]*)/g, '$1');

return content;
}

/**
* Transform command content for Kiro prompt files.
*
* Strips existing frontmatter, prepends inclusion: manual frontmatter,
* replaces PLUGIN_ROOT paths, removes require()/Task() calls, strips namespaces.
*/
function transformCommandForKiro(content, options) {
const { pluginInstallPath, name = '', description = '' } = options;

if (content.startsWith('---')) {
content = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
}

const cleanDescription = description.replace(/[\x00-\x1f\x7f]/g, ' ');
const escapedDescription = cleanDescription.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
let frontmatter = '---\n';
frontmatter += 'inclusion: manual\n';
if (name) frontmatter += `name: "${name}"\n`;
if (description) frontmatter += `description: "${escapedDescription}"\n`;
frontmatter += '---\n';

content = frontmatter + content;

content = content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => pluginInstallPath);
content = content.replace(/\$CLAUDE_PLUGIN_ROOT/g, () => pluginInstallPath);
content = content.replace(/\$\{PLUGIN_ROOT\}/g, () => pluginInstallPath);
content = content.replace(/\$PLUGIN_ROOT/g, () => pluginInstallPath);

content = content.replace(/(?:const|let|var)\s+\{?[^}=\n]+\}?\s*=\s*require\s*\([^)]+\);?/g, '');
content = content.replace(/require\s*\(['"][^'"]+['"]\)/g, '');

// Transform code blocks containing Promise.all + Task() (parallel reviewer spawns).
// The bare Task() regex below can't reach inside fenced code blocks.
// Fence boundaries must be at line start (^```) to avoid matching backtick
// template literals inside the code as false fence endings.
content = content.replace(/^```(?:javascript|js)?\n([\s\S]*?)^```$/gm, (fullBlock, codeContent) => {
if (!codeContent.includes('Promise.all') || !codeContent.includes('Task(')) return fullBlock;
// Use lazy [\s\S]*? for the template literal body: it stops at the first backtick and
// naturally matches any character including standalone $ (e.g. $100, $BUDGET).
const taskMatches = [...codeContent.matchAll(/Task\s*\(\s*\{[\s\S]*?subagent_type:\s*['"](?:[^"':]+:)?([^'"]+)['"][\s\S]*?prompt:\s*`([\s\S]*?)`/gs)];
if (taskMatches.length < 2) return fullBlock;

const delegations = taskMatches.map(m => {
const agent = m[1];
const promptFirstLine = m[2].split('\n').find(l => l.trim()) || '';
return `Delegate to the \`${agent}\` subagent:\n> ${promptFirstLine.trim()}`;
});

let result = delegations.join('\n\n');

const hasReviewKeyword = delegations.some(d =>
/review|quality|security|performance|test|coverage/i.test(d)
);
if (delegations.length >= 4 && hasReviewKeyword) {
result = `**Review phase (Kiro - max 4 agents, fallback to 2 sequential):**\n\nTry delegating to these subagents (experimental parallel spawning):\n\n${result}\n\nIf parallel spawning is unavailable, run 2 combined reviewers sequentially:\n1. Delegate to the \`reviewer-quality-security\` subagent (code quality + security)\n2. Then delegate to the \`reviewer-perf-test\` subagent (performance + test coverage)\n\nAggregate all findings from whichever execution path succeeded.`;
}

return result;
});

// Transform bare Task() calls outside code blocks
content = content.replace(/await\s+Task\s*\(\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}\s*\);?/g, (match) => {
const agentMatch = match.match(/subagent_type:\s*["'](?:[^"':]+:)?([^"']+)["']/);
const promptMatch = match.match(/prompt:\s*[`"']([\s\S]*?)[`"']/);
if (agentMatch) {
const agentName = agentMatch[1];
const prompt = promptMatch ? promptMatch[1].replace(/\\n/g, '\n').trim() : '';
if (prompt) {
return `Delegate to the \`${agentName}\` subagent:\n> ${prompt.split('\n')[0]}`;
}
return `Delegate to the \`${agentName}\` subagent.`;
}
return '';
});

// Transform AskUserQuestion to markdown prompt for Kiro chat
content = content.replace(/(?:await\s+)?AskUserQuestion\s*\(\s*\{[\s\S]*?\}\s*\);?/g, (match) => {
const questionMatch = match.match(/question:\s*["'`]([\s\S]*?)["'`]/);
const question = questionMatch ? questionMatch[1] : 'Please choose:';
const optionMatches = [...match.matchAll(/label:\s*["'`]([^"'`]+)["'`][\s\S]*?description:\s*["'`]([^"'`]+)["'`]/g)];
if (optionMatches.length > 0) {
const options = optionMatches.map((m, i) => `${i + 1}. **${m[1]}** - ${m[2]}`).join('\n');
return `**${question}**\n\n${options}\n\nReply with the number or name of your choice.`;
}
return `**${question}**\n\nReply in chat with your choice.`;
});

content = content.replace(/(?:next-task|deslop|ship|sync-docs|audit-project|enhance|perf|repo-map|drift-detect|consult|debate|learn|web-ctl):([a-z][a-z0-9-]*)/g, '$1');

// Batch parallel reviewer delegations for Kiro's agent limit.
// Detects 4+ consecutive "Delegate to" lines with review-related names
// and rewrites as try-4-then-fallback-to-2 pattern.
const reviewerBatchPattern = /((?:Delegate to the `[^`]*` subagent[^\n]*\n){4,})/g;
content = content.replace(reviewerBatchPattern, (block) => {
const delegations = block.match(/Delegate to the `([^`]+)` subagent/g) || [];
if (delegations.length < 4) return block;

const hasReviewKeyword = delegations.some(d =>
/review|quality|security|performance|test|coverage/i.test(d)
);
if (!hasReviewKeyword) return block;

return `**Review phase (Kiro - max 4 agents, fallback to 2 sequential):**

Try delegating to these subagents (experimental parallel spawning):
${block}
If parallel spawning is unavailable, run 2 combined reviewers sequentially:
1. Delegate to the \`reviewer-quality-security\` subagent (code quality + security)
2. Then delegate to the \`reviewer-perf-test\` subagent (performance + test coverage)

Aggregate all findings from whichever execution path succeeded.\n`;
});

return content;
}

/**
* Transform agent markdown+frontmatter to Kiro JSON format.
*
* Parses frontmatter for name/description/model/tools, uses body as prompt.
* Returns a JSON string matching Kiro's agent schema.
*/
function transformAgentForKiro(content, options) {
const { pluginInstallPath } = options || {};

const frontmatter = discovery.parseFrontmatter(content);
let body = content;
if (content.startsWith('---')) {
const endIdx = content.indexOf('\n---', 3);
if (endIdx !== -1) {
body = content.substring(endIdx + 4).replace(/^\n/, '');
}
}

if (pluginInstallPath) {
body = body.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, () => pluginInstallPath);
body = body.replace(/\$CLAUDE_PLUGIN_ROOT/g, () => pluginInstallPath);
body = body.replace(/\$\{PLUGIN_ROOT\}/g, () => pluginInstallPath);
body = body.replace(/\$PLUGIN_ROOT/g, () => pluginInstallPath);
}

body = body.replace(/(?:next-task|deslop|ship|sync-docs|audit-project|enhance|perf|repo-map|drift-detect|consult|debate|learn|web-ctl):([a-z][a-z0-9-]*)/g, '$1');

const agent = {
name: frontmatter.name || '',
description: frontmatter.description || '',
prompt: body.trim()
};

if (frontmatter.tools) {
// parseFrontmatter returns arrays for YAML list syntax, string for inline
const toolItems = Array.isArray(frontmatter.tools)
? frontmatter.tools.map(t => t.toLowerCase())
: [frontmatter.tools.toLowerCase()];
const toolStr = toolItems.join(' ');
const tools = [];
if (toolStr.includes('read')) tools.push('read');
if (toolStr.includes('edit') || toolStr.includes('write')) tools.push('write');
if (toolStr.includes('bash') || toolStr.includes('shell')) tools.push('shell');
if (toolStr.includes('glob')) tools.push('read');
if (toolStr.includes('grep')) tools.push('read');
if (toolStr.includes('task') || toolStr.includes('agent')) tools.push('shell');
if (toolStr.includes('web') || toolStr.includes('fetch')) tools.push('shell');
if (toolStr.includes('notebook')) tools.push('write');
if (toolStr.includes('lsp')) tools.push('read');
const deduped = [...new Set(tools)];
agent.tools = deduped.length > 0 ? deduped : ['read'];
} else {
agent.tools = ['read'];
}

agent.resources = ['file://.kiro/prompts/**/*.md'];

return JSON.stringify(agent, null, 2);
}

/**
* Generate a combined reviewer agent JSON for Kiro's 4-agent limit.
* Merges multiple review responsibilities into a single agent.
*
* @param {Array<{name: string, focus: string}>} roles - Review passes to combine
* @param {string} name - Agent name
* @param {string} description - Agent description
* @returns {string} JSON string for .kiro/agents/*.json
*/
function generateCombinedReviewerAgent(roles, name, description) {
const sections = roles.map(r =>
`## ${r.name} Review\n\nFocus: ${r.focus}`
).join('\n\n---\n\n');

const agent = {
name,
description,
prompt: `You are a combined code reviewer covering multiple review passes in a single session.\n\n${sections}\n\nFor each file you review, check ALL of the above review dimensions. Return findings as a JSON array with objects containing: pass (which review), file, line, severity (critical/high/medium/low), description, suggestion.`,
tools: ['read'],
resources: ['file://.kiro/prompts/**/*.md'],
};

return JSON.stringify(agent, null, 2);
}

module.exports = {
transformBodyForOpenCode,
transformCommandFrontmatterForOpenCode,
transformAgentFrontmatterForOpenCode,
transformSkillBodyForOpenCode,
transformForCodex
transformForCodex,
transformRuleForCursor,
transformSkillForCursor,
transformCommandForCursor,
transformForCursor: transformRuleForCursor,
transformSkillForKiro,
transformCommandForKiro,
transformAgentForKiro,
generateCombinedReviewerAgent
};
Loading
Loading