diff --git a/.aiox-core/infrastructure/scripts/ide-sync/index.js b/.aiox-core/infrastructure/scripts/ide-sync/index.js index 43719a4148..c742bb5e93 100644 --- a/.aiox-core/infrastructure/scripts/ide-sync/index.js +++ b/.aiox-core/infrastructure/scripts/ide-sync/index.js @@ -31,6 +31,7 @@ const claudeCodeTransformer = require('./transformers/claude-code'); const cursorTransformer = require('./transformers/cursor'); const antigravityTransformer = require('./transformers/antigravity'); const githubCopilotTransformer = require('./transformers/github-copilot'); +const kimiTransformer = require('./transformers/kimi'); // ANSI colors for output const colors = { @@ -87,6 +88,12 @@ function loadConfig(projectRoot) { path: '.antigravity/rules/agents', format: 'cursor-style', }, + kimi: { + enabled: true, + path: '.kimi/skills', + format: 'kimi-skill', + fallbackSources: ['.codex/agents'], + }, }, redirects: { 'aiox-developer': 'aiox-master', @@ -128,6 +135,7 @@ function getTransformer(format) { 'condensed-rules': cursorTransformer, 'cursor-style': antigravityTransformer, 'github-copilot': githubCopilotTransformer, + 'kimi-skill': kimiTransformer, }; return transformers[format] || claudeCodeTransformer; @@ -183,7 +191,18 @@ function syncIde(agents, ideConfig, ideName, projectRoot, options) { try { const content = transformer.transform(agent); const filename = transformer.getFilename(agent); - const targetPath = path.join(result.targetDir, filename); + + // Kimi format uses subdirectories per skill: /SKILL.md + let targetPath; + if (ideConfig.format === 'kimi-skill' && transformer.getDirname) { + const skillDir = path.join(result.targetDir, transformer.getDirname(agent)); + targetPath = path.join(skillDir, filename); + if (!options.dryRun) { + fs.ensureDirSync(skillDir); + } + } else { + targetPath = path.join(result.targetDir, filename); + } if (!options.dryRun) { fs.writeFileSync(targetPath, content, 'utf8'); @@ -375,7 +394,11 @@ async function commandValidate(options) { try { const content = transformer.transform(agent); const filename = transformer.getFilename(agent); - expectedFiles.push({ filename, content }); + // Kimi format stores each skill in /SKILL.md — record nested path + const relPath = ideConfig.format === 'kimi-skill' && transformer.getDirname + ? path.join(transformer.getDirname(agent), filename) + : filename; + expectedFiles.push({ filename: relPath, content }); } catch (error) { // Skip agents that fail to transform } diff --git a/.aiox-core/infrastructure/scripts/ide-sync/transformers/kimi.js b/.aiox-core/infrastructure/scripts/ide-sync/transformers/kimi.js new file mode 100644 index 0000000000..03a7cf7ba3 --- /dev/null +++ b/.aiox-core/infrastructure/scripts/ide-sync/transformers/kimi.js @@ -0,0 +1,328 @@ +/** + * Kimi Transformer - Skill-based agent activator + * @story Kimi IDE Integration + * + * Format: SKILL.md with YAML frontmatter + markdown instructions + * Target: .kimi/skills/aios-{agent-id}/SKILL.md + */ + +/** + * Transform agent data to Kimi skill format + * @param {object} agentData - Parsed agent data from agent-parser + * @returns {string} - Transformed SKILL.md content + */ +function transform(agentData) { + const rawAgent = agentData.agent || {}; + const fallbackAgent = agentData._fallback || {}; + const persona = agentData.persona_profile || {}; + const comm = persona.communication || {}; + const greetingLevels = comm.greeting_levels || {}; + const yaml = agentData.yaml || {}; + + const id = agentData.id; + const skillId = getSkillId(agentData); + const activationId = getPreferredActivationId(agentData); + const name = rawAgent.name || fallbackAgent.name || id; + const title = rawAgent.title || fallbackAgent.title || 'AIOS Agent'; + const icon = rawAgent.icon || fallbackAgent.icon || '🤖'; + const whenToUse = rawAgent.whenToUse || fallbackAgent.whenToUse || `Use for ${title.toLowerCase()} tasks`; + const archetype = persona.archetype || rawAgent.archetype || 'Specialist'; + const zodiac = persona.zodiac || rawAgent.zodiac || ''; + const tone = comm.tone || rawAgent.tone || 'professional'; + + const description = buildDescription(activationId, name, title, whenToUse); + const namedGreeting = greetingLevels.named || `${icon} ${name} ready`; + + // Extract rich sections from parsed YAML + const identitySection = buildIdentitySection(rawAgent, persona, yaml); + const protocolSection = buildProtocolSection(yaml); + const commandsTable = buildCommandsTable(agentData.commands); + const workflowSection = buildWorkflowSection(yaml); + const guardrailsSection = buildGuardrailsSection(yaml); + const handoffsSection = buildHandoffsSection(yaml); + const outputContractSection = buildOutputContractSection(yaml); + + // Full raw content + const rawContent = agentData.raw || ''; + const rawContentSection = buildRawContentSection(rawContent, id); + + const content = `--- +name: ${JSON.stringify(skillId)} +description: ${JSON.stringify(description)} +--- + +# ${icon} @${id} — ${name}${archetype !== 'Specialist' ? ` (${archetype})` : ''} | ${title} + +## Activation Protocol + +When this skill is invoked: + +1. Adopt the persona below immediately. Do NOT narrate the activation, do NOT comment on Kimi's mechanism, do NOT preface with internal reasoning. +2. Print the greeting verbatim from the next section. +3. List commands EXACTLY as they appear in the Star Commands table — do not summarize, do not invent shortcuts. +4. Wait for user input unless a star command was provided alongside the activation. + +## Activation Greeting + +\`\`\` +${namedGreeting} +\`\`\` + +${identitySection}${protocolSection}${commandsTable}${workflowSection}${guardrailsSection}${handoffsSection}${outputContractSection}--- + +## Full Agent Definition — ${id} + +> This section contains the COMPLETE operating guide for this agent. Read it ENTIRELY and adopt the persona, principles, protocols, and guardrails defined below. Do NOT invent tasks, processes, or workflows that are not documented here. + +${rawContentSection}`; + + return content; +} + +function buildIdentitySection(rawAgent, persona, yaml) { + const name = rawAgent.name || ''; + const role = persona.role || yaml.persona?.role || ''; + const style = persona.style || yaml.persona?.style || ''; + const focus = persona.focus || yaml.persona?.focus || ''; + const identity = persona.identity || yaml.persona?.identity || ''; + + if (!name && !role && !style && !focus && !identity) return ''; + + let section = '## Identity\n\n'; + if (name) section += `- **Name:** ${name}\n`; + if (role) section += `- **Role:** ${role}\n`; + if (style) section += `- **Style:** ${style}\n`; + if (focus) section += `- **Focus:** ${focus}\n`; + if (identity) section += `- **Identity:** ${identity}\n`; + section += '\n'; + return section; +} + +function buildProtocolSection(yaml) { + const items = []; + + if (yaml.cognitive_protocol && Array.isArray(yaml.cognitive_protocol)) { + items.push('### Cognitive Protocol\n\n' + yaml.cognitive_protocol.map(p => `- ${renderItem(p)}`).join('\n') + '\n'); + } + + if (yaml.evidence_policy) { + const ep = yaml.evidence_policy; + let text = '### Evidence Policy\n\n'; + if (ep.required_min_sources) text += `- Required minimum sources: ${ep.required_min_sources}\n`; + if (ep.accepted_types && Array.isArray(ep.accepted_types)) text += `- Accepted types: ${ep.accepted_types.join(', ')}\n`; + if (ep.reject_if && Array.isArray(ep.reject_if)) text += `- Reject if: ${ep.reject_if.map(r => `"${r}"`).join(', ')}\n`; + items.push(text + '\n'); + } + + if (yaml.confidence_model) { + const cm = yaml.confidence_model; + let text = '### Confidence Model\n\n'; + if (cm.formula) text += `- Formula: \`${cm.formula}\`\n`; + if (cm.thresholds) { + text += '- Thresholds:\n'; + for (const [k, v] of Object.entries(cm.thresholds)) text += ` - ${k}: ${v}\n`; + } + items.push(text + '\n'); + } + + if (yaml.core_principles && Array.isArray(yaml.core_principles)) { + items.push('### Core Principles\n\n' + yaml.core_principles.map(p => `- ${renderItem(p)}`).join('\n') + '\n'); + } + + return items.length > 0 ? items.join('\n') + '\n' : ''; +} + +function buildCommandsTable(commands) { + if (!commands || !Array.isArray(commands) || commands.length === 0) { + return ''; + } + + const normalized = commands.map(cmd => { + if (typeof cmd === 'string') { + const dashMatch = cmd.trim().match(/^\*?([a-zA-Z0-9:_-]+)\s*[-:]\s*(.+)$/); + if (dashMatch) { + return { name: dashMatch[1], description: dashMatch[2], visibility: ['full', 'quick'] }; + } + return { name: cmd.replace(/^\*/, '') || 'unknown', description: 'No description', visibility: ['full', 'quick'] }; + } + return cmd; + }); + + const allRows = normalized.map(cmd => { + let vis = 'full'; + if (cmd.visibility) { + if (Array.isArray(cmd.visibility)) { + vis = cmd.visibility.join(', '); + } else if (typeof cmd.visibility === 'string') { + vis = cmd.visibility; + } + } + return `| \`*${cmd.name}\` | ${cmd.description || 'No description'} | ${vis} |`; + }); + + // Always include guide/yolo/exit + const hasGuide = normalized.some(c => c.name === 'guide'); + const hasYolo = normalized.some(c => c.name === 'yolo'); + const hasExit = normalized.some(c => c.name === 'exit'); + + if (!hasGuide) allRows.push('| `*guide` | Show comprehensive usage guide | full |'); + if (!hasYolo) allRows.push('| `*yolo` | Toggle permission mode | full |'); + if (!hasExit) allRows.push('| `*exit` | Exit agent mode | full |'); + + return `## Star Commands\n\n| Command | Description | Visibility |\n|---------|-------------|------------|\n${allRows.join('\n')}\n\n`; +} + +function buildWorkflowSection(yaml) { + const items = []; + + if (yaml.mandatory_flow && Array.isArray(yaml.mandatory_flow)) { + items.push('### Mandatory Flow\n\nExecute **strictly in this order**. No skips allowed.\n\n' + + yaml.mandatory_flow.map((step, i) => `${i + 1}. ${renderItem(step)}`).join('\n') + '\n'); + } + + if (yaml.phase_release_rules && Array.isArray(yaml.phase_release_rules)) { + items.push('### Phase Release Rules\n\n' + yaml.phase_release_rules.map(r => `- ${renderItem(r)}`).join('\n') + '\n'); + } + + if (yaml.activation_instructions && Array.isArray(yaml.activation_instructions)) { + items.push('### Activation Instructions\n\n' + yaml.activation_instructions.map(i => `- ${renderItem(i)}`).join('\n') + '\n'); + } + + return items.length > 0 ? '## Workflow\n\n' + items.join('\n') + '\n' : ''; +} + +function buildGuardrailsSection(yaml) { + const items = []; + + if (yaml.veto_conditions && Array.isArray(yaml.veto_conditions)) { + items.push('### Veto Conditions\n\n' + yaml.veto_conditions.map(v => `- ❌ ${renderItem(v)}`).join('\n') + '\n'); + } + + if (yaml.agent_rules && Array.isArray(yaml.agent_rules)) { + items.push('### Agent Rules\n\n' + yaml.agent_rules.map(r => `- ${renderItem(r)}`).join('\n') + '\n'); + } + + if (yaml.design_rules) { + const dr = yaml.design_rules; + let text = '### Design Rules\n\n'; + for (const [key, value] of Object.entries(dr)) { + if (typeof value === 'object' && value.rule) { + text += `- **${key}:** ${value.rule}\n`; + } else if (typeof value === 'string') { + text += `- **${key}:** ${value}\n`; + } + } + items.push(text + '\n'); + } + + return items.length > 0 ? '## Guardrails\n\n' + items.join('\n') + '\n' : ''; +} + +function buildHandoffsSection(yaml) { + if (!yaml.handoffs || !Array.isArray(yaml.handoffs) || yaml.handoffs.length === 0) { + return ''; + } + + const rows = yaml.handoffs.map(h => { + const to = h.to || h.target || 'unknown'; + const when = h.when || h.condition || ''; + return `- **→ @${to}:** ${when}`; + }).join('\n'); + + return `## Handoffs\n\n${rows}\n\n`; +} + +function buildOutputContractSection(yaml) { + if (!yaml.output_contract && !yaml.output) return ''; + + const oc = yaml.output_contract || yaml.output; + let text = '## Output Contract\n\n'; + + if (oc.required && Array.isArray(oc.required)) { + text += '**Required deliverables:**\n\n' + oc.required.map(r => `- ${r}`).join('\n') + '\n\n'; + } + + if (oc.done_when && Array.isArray(oc.done_when)) { + text += '**Done when:**\n\n' + oc.done_when.map(d => `- ✅ ${d}`).join('\n') + '\n\n'; + } + + return text; +} + +function buildDescription(id, name, title, whenToUse) { + let desc = whenToUse + .replace(/\n/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + + if (desc.length > 300) { + desc = desc.substring(0, 297) + '...'; + } + + const triggerPhrases = [ + `activate ${id}`, + `switch to ${id}`, + `@${id}`, + ]; + + return `Activate the AIOS ${title} agent (${name}). ${desc} Trigger when user asks to ${id}, or says '${triggerPhrases.join("', '")}'.`; +} + +function buildRawContentSection(rawContent, id) { + if (!rawContent || rawContent.length === 0) { + return '> Agent definition not available.\n'; + } + + return rawContent; +} + +function capitalizeFirst(str) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +// Normalize an array item that may be a string OR a YAML-parsed object. +// `- CRITICAL: text` parses to { CRITICAL: 'text' } and must render as +// `**CRITICAL:** text`, not `[object Object]`. +function renderItem(value) { + if (value === null || value === undefined) return ''; + if (typeof value === 'string') return value; + if (typeof value !== 'object') return String(value); + + const entries = Object.entries(value); + if (entries.length === 0) return ''; + + return entries + .map(([k, v]) => { + const rendered = typeof v === 'string' ? v : JSON.stringify(v); + return `**${k}:** ${rendered}`; + }) + .join(' '); +} + +function getPreferredActivationId(agentData) { + const agent = agentData.agent || {}; + const preferred = agent.preferredActivationAlias || agent.preferred_activation_alias; + return String(preferred || agentData.id || '').trim(); +} + +function getSkillId(agentData) { + const id = getPreferredActivationId(agentData); + if (id.startsWith('aios-')) return id; + return `aios-${id}`; +} + +function getDirname(agentData) { + return getSkillId(agentData); +} + +function getFilename(_agentData) { + return 'SKILL.md'; +} + +module.exports = { + transform, + getSkillId, + getDirname, + getFilename, + format: 'kimi-skill', +}; diff --git a/.aiox-core/infrastructure/scripts/ide-sync/validator.js b/.aiox-core/infrastructure/scripts/ide-sync/validator.js index 66c04472bf..d225c639a7 100644 --- a/.aiox-core/infrastructure/scripts/ide-sync/validator.js +++ b/.aiox-core/infrastructure/scripts/ide-sync/validator.js @@ -28,6 +28,29 @@ function fileExists(filePath) { return fs.existsSync(filePath); } +/** + * Recursively collect *.md filenames under root, returned as paths relative to base. + * Used for IDE targets with nested layouts (e.g. Kimi: /SKILL.md). + */ +function walkMarkdown(root, base) { + const out = []; + let entries; + try { + entries = fs.readdirSync(root, { withFileTypes: true }); + } catch (e) { + return out; + } + for (const entry of entries) { + const full = path.join(root, entry.name); + if (entry.isDirectory()) { + out.push(...walkMarkdown(full, base)); + } else if (entry.isFile() && entry.name.endsWith('.md')) { + out.push(path.relative(base, full)); + } + } + return out; +} + /** * Read file content if it exists * @param {string} filePath - Path to read @@ -113,10 +136,11 @@ function validateIdeSync(expectedFiles, targetDir, redirectsConfig) { } } - // Check for orphaned files (files in target not in expected) + // Check for orphaned files (files in target not in expected). + // Recursive walk so nested layouts like Kimi's /SKILL.md are handled. if (fs.existsSync(targetDir)) { try { - const actualFiles = fs.readdirSync(targetDir).filter(f => f.endsWith('.md')); + const actualFiles = walkMarkdown(targetDir, targetDir); for (const file of actualFiles) { if (!expectedFilenames.has(file)) { diff --git a/tests/ide-sync/kimi-transformer.test.js b/tests/ide-sync/kimi-transformer.test.js new file mode 100644 index 0000000000..965182bcf0 --- /dev/null +++ b/tests/ide-sync/kimi-transformer.test.js @@ -0,0 +1,92 @@ +/** + * Tests for the Kimi transformer (kimi-skill format). + * Covers: + * - skillId normalization (no double aios- prefix) + * - preferredActivationAlias support + * - YAML object items in array fields render as **KEY:** value (not [object Object]) + * - Activation Protocol directive is present + * - getDirname / getFilename for nested layout + */ + +const path = require('path'); +const kimi = require(path.resolve( + __dirname, + '..', + '..', + '.aiox-core', + 'infrastructure', + 'scripts', + 'ide-sync', + 'transformers', + 'kimi' +)); + +function buildAgentData(overrides = {}) { + return { + id: overrides.id || 'dev', + agent: { + name: 'Dex', + title: 'Full Stack Developer', + icon: '💻', + whenToUse: 'Use for code implementation and refactoring', + ...overrides.agent, + }, + persona_profile: { + archetype: 'Builder', + communication: { + greeting_levels: { named: '💻 Dex (Builder) ready' }, + tone: 'pragmatic', + }, + ...overrides.persona_profile, + }, + yaml: overrides.yaml || {}, + commands: overrides.commands || [ + { name: 'help', description: 'Show all commands', visibility: ['full'] }, + ], + raw: overrides.raw || '', + }; +} + +describe('kimi transformer', () => { + test('normalizes skill id without double aios- prefix', () => { + expect(kimi.getSkillId({ id: 'dev', agent: {} })).toBe('aios-dev'); + expect(kimi.getSkillId({ id: 'aios-master', agent: {} })).toBe('aios-master'); + }); + + test('respects preferredActivationAlias', () => { + const skillId = kimi.getSkillId({ + id: 'davi-ribas-community-growth-strategist', + agent: { preferredActivationAlias: 'davi-ribas' }, + }); + expect(skillId).toBe('aios-davi-ribas'); + }); + + test('renders YAML object items in arrays without [object Object]', () => { + const data = buildAgentData({ + yaml: { + core_principles: [ + { CRITICAL: 'Story has ALL info you need.' }, + 'Numbered Options - Always use numbered lists', + ], + }, + }); + const out = kimi.transform(data); + expect(out).not.toMatch(/\[object Object\]/); + expect(out).toMatch(/\*\*CRITICAL:\*\* Story has ALL info you need\./); + expect(out).toMatch(/Numbered Options - Always use numbered lists/); + }); + + test('emits Activation Protocol directive', () => { + const out = kimi.transform(buildAgentData()); + expect(out).toMatch(/## Activation Protocol/); + expect(out).toMatch(/Adopt the persona below immediately/); + expect(out).toMatch(/EXACTLY as they appear in the Star Commands table/); + }); + + test('produces nested layout: /SKILL.md', () => { + const data = buildAgentData({ id: 'dev' }); + expect(kimi.getFilename(data)).toBe('SKILL.md'); + expect(kimi.getDirname(data)).toBe('aios-dev'); + expect(kimi.format).toBe('kimi-skill'); + }); +});