From c797c4e584edd517d8a63034a3964ed5604b1651 Mon Sep 17 00:00:00 2001 From: Yang Date: Sat, 28 Feb 2026 14:47:30 +0800 Subject: [PATCH 1/2] feat: add Kimi Code CLI support --- tools/cli/installers/lib/ide/kimi-cli.js | 376 ++++++++++++++++++ tools/cli/installers/lib/ide/manager.js | 2 +- .../installers/lib/ide/platform-codes.yaml | 7 + 3 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 tools/cli/installers/lib/ide/kimi-cli.js diff --git a/tools/cli/installers/lib/ide/kimi-cli.js b/tools/cli/installers/lib/ide/kimi-cli.js new file mode 100644 index 000000000..c9b36b883 --- /dev/null +++ b/tools/cli/installers/lib/ide/kimi-cli.js @@ -0,0 +1,376 @@ +const path = require('node:path'); +const fs = require('fs-extra'); +const yaml = require('yaml'); +const { BaseIdeSetup } = require('./_base-ide'); +const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); +const { AgentCommandGenerator } = require('./shared/agent-command-generator'); +const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); +const { getTasksFromBmad } = require('./shared/bmad-artifacts'); +const { toDashPath, customAgentDashName } = require('./shared/path-utils'); +const prompts = require('../../../lib/prompts'); + +/** + * Kimi Code CLI setup handler + * Creates Agent Skills in .kimi/skills/ directory + */ +class KimiCliSetup extends BaseIdeSetup { + constructor() { + super('kimi-cli', 'Kimi Code CLI', true); + } + + /** + * Setup Kimi CLI configuration + * @param {string} projectDir - Project directory + * @param {string} bmadDir - BMAD installation directory + * @param {Object} options - Setup options + */ + async setup(projectDir, bmadDir, options = {}) { + if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); + + const { artifacts, counts } = await this.collectClaudeArtifacts(projectDir, bmadDir, options); + + // Install to .kimi/skills + const destDir = this.getKimiSkillsDir(projectDir); + await fs.ensureDir(destDir); + await this.clearOldBmadSkills(destDir, options); + + // Collect and write agent skills + const agentGen = new AgentCommandGenerator(this.bmadFolderName); + const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); + const agentCount = await this.writeSkillArtifacts(destDir, agentArtifacts, 'agent-launcher'); + + // Collect and write task skills + const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []); + const taskArtifacts = []; + for (const task of tasks) { + const content = await this.readAndProcessWithProject( + task.path, + { + module: task.module, + name: task.name, + }, + projectDir, + ); + taskArtifacts.push({ + type: 'task', + name: task.name, + displayName: task.name, + module: task.module, + path: task.path, + sourcePath: task.path, + relativePath: path.join(task.module, 'tasks', `${task.name}.md`), + content, + }); + } + + const ttGen = new TaskToolCommandGenerator(this.bmadFolderName); + const taskSkillArtifacts = taskArtifacts.map((artifact) => ({ + ...artifact, + content: ttGen.generateCommandContent(artifact, artifact.type), + })); + const tasksWritten = await this.writeSkillArtifacts(destDir, taskSkillArtifacts, 'task'); + + // Collect and write workflow skills + const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); + const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); + const workflowCount = await this.writeSkillArtifacts(destDir, workflowArtifacts, 'workflow-command'); + + const written = agentCount + workflowCount + tasksWritten; + + if (!options.silent) { + await prompts.log.success( + `${this.name} configured: ${counts.agents} agents, ${counts.workflows} workflows, ${counts.tasks} tasks, ${written} skills → ${destDir}`, + ); + } + + return { + success: true, + artifacts, + counts, + destination: destDir, + written, + }; + } + + /** + * Detect Kimi CLI installation by checking for BMAD skills + */ + async detect(projectDir) { + const dir = this.getKimiSkillsDir(projectDir || process.cwd()); + + if (await fs.pathExists(dir)) { + try { + const entries = await fs.readdir(dir); + if (entries && entries.some((entry) => entry && typeof entry === 'string' && entry.startsWith('bmad'))) { + return true; + } + } catch { + // Ignore errors + } + } + + return false; + } + + /** + * Collect Claude-style artifacts for Kimi CLI export. + * Returns the normalized artifact list for further processing. + */ + async collectClaudeArtifacts(projectDir, bmadDir, options = {}) { + const selectedModules = options.selectedModules || []; + const artifacts = []; + + // Generate agent launchers + const agentGen = new AgentCommandGenerator(this.bmadFolderName); + const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules); + + for (const artifact of agentArtifacts) { + artifacts.push({ + type: 'agent', + module: artifact.module, + sourcePath: artifact.sourcePath, + relativePath: artifact.relativePath, + content: artifact.content, + }); + } + + const tasks = await getTasksFromBmad(bmadDir, selectedModules); + for (const task of tasks) { + const content = await this.readAndProcessWithProject( + task.path, + { + module: task.module, + name: task.name, + }, + projectDir, + ); + + artifacts.push({ + type: 'task', + name: task.name, + displayName: task.name, + module: task.module, + path: task.path, + sourcePath: task.path, + relativePath: path.join(task.module, 'tasks', `${task.name}.md`), + content, + }); + } + + const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); + const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); + artifacts.push(...workflowArtifacts); + + return { + artifacts, + counts: { + agents: agentArtifacts.length, + tasks: tasks.length, + workflows: workflowCounts.commands, + workflowLaunchers: workflowCounts.launchers, + }, + }; + } + + getKimiSkillsDir(projectDir) { + if (!projectDir) { + throw new Error('projectDir is required for project-scoped skill installation'); + } + return path.join(projectDir, '.kimi', 'skills'); + } + + /** + * Write artifacts as Agent Skills (agentskills.io format). + * Each artifact becomes a directory containing SKILL.md. + * @param {string} destDir - Base skills directory + * @param {Array} artifacts - Artifacts to write + * @param {string} artifactType - Type filter (e.g., 'agent-launcher', 'workflow-command', 'task') + * @returns {number} Number of skills written + */ + async writeSkillArtifacts(destDir, artifacts, artifactType) { + let writtenCount = 0; + + for (const artifact of artifacts) { + // Filter by type if the artifact has a type field + if (artifact.type && artifact.type !== artifactType) { + continue; + } + + // Get the dash-format name (e.g., bmad-bmm-create-prd.md) and remove .md + const flatName = toDashPath(artifact.relativePath); + const skillName = flatName.replace(/\.md$/, ''); + + // Create skill directory + const skillDir = path.join(destDir, skillName); + await fs.ensureDir(skillDir); + + // Transform content: rewrite frontmatter for skills format + const skillContent = this.transformToSkillFormat(artifact.content, skillName); + + // Write SKILL.md with platform-native line endings + const os = require('node:os'); + const platformContent = skillContent.replaceAll('\n', os.EOL); + await fs.writeFile(path.join(skillDir, 'SKILL.md'), platformContent, 'utf8'); + writtenCount++; + } + + return writtenCount; + } + + /** + * Transform artifact content to Agent Skills format. + * Ensures name matches directory. + * @param {string} content - Original content with YAML frontmatter + * @param {string} skillName - Skill name (must match directory name) + * @returns {string} Transformed content + */ + transformToSkillFormat(content, skillName) { + // Normalize line endings so body matches rebuilt frontmatter (both LF) + content = content.replaceAll('\r\n', '\n').replaceAll('\r', '\n'); + + // Parse frontmatter + const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); + if (!fmMatch) { + // No frontmatter -- wrap with minimal frontmatter + const fm = yaml.stringify({ name: skillName, description: skillName }).trimEnd(); + return `---\n${fm}\n---\n\n${content}`; + } + + const frontmatter = fmMatch[1]; + const body = fmMatch[2]; + + // Parse frontmatter with yaml library to handle all quoting variants + let description; + try { + const parsed = yaml.parse(frontmatter); + description = parsed?.description || `${skillName} skill`; + } catch { + description = `${skillName} skill`; + } + + // Build new frontmatter with only skills-spec fields, let yaml handle quoting + const newFrontmatter = yaml.stringify({ name: skillName, description }, { lineWidth: 0 }).trimEnd(); + return `---\n${newFrontmatter}\n---\n${body}`; + } + + /** + * Remove existing BMAD skill directories from the skills directory. + */ + async clearOldBmadSkills(destDir, options = {}) { + if (!(await fs.pathExists(destDir))) { + return; + } + + let entries; + try { + entries = await fs.readdir(destDir); + } catch (error) { + if (!options.silent) await prompts.log.warn(`Warning: Could not read directory ${destDir}: ${error.message}`); + return; + } + + if (!entries || !Array.isArray(entries)) { + return; + } + + for (const entry of entries) { + if (!entry || typeof entry !== 'string') { + continue; + } + if (!entry.startsWith('bmad')) { + continue; + } + + const entryPath = path.join(destDir, entry); + try { + await fs.remove(entryPath); + } catch (error) { + if (!options.silent) { + await prompts.log.message(` Skipping ${entry}: ${error.message}`); + } + } + } + } + + async readAndProcessWithProject(filePath, metadata, projectDir) { + const rawContent = await fs.readFile(filePath, 'utf8'); + const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n'); + return super.processContent(content, metadata, projectDir); + } + + /** + * Get instructions for project-specific installation + * @param {string} projectDir - Optional project directory + * @param {string} destDir - Optional destination directory + * @returns {string} Instructions text + */ + getProjectSpecificInstructions(projectDir = null, destDir = null) { + const lines = [ + 'Project-Specific Kimi CLI Configuration', + '', + `Skills installed to: ${destDir || '/.kimi/skills'}`, + '', + 'Kimi CLI automatically discovers skills in .kimi/skills/ at and above the current directory and in your home directory.', + 'Use "/skill:" to load a skill.', + '', + 'Example:', + ' /skill:bmad-agent-bmm-dev', + ]; + + return lines.join('\n'); + } + + /** + * Cleanup Kimi CLI configuration + */ + async cleanup(projectDir = null) { + if (projectDir) { + const destDir = this.getKimiSkillsDir(projectDir); + await this.clearOldBmadSkills(destDir); + } + } + + /** + * Install a custom agent launcher for Kimi CLI as an Agent Skill + * @param {string} projectDir - Project directory + * @param {string} agentName - Agent name (e.g., "fred-commit-poet") + * @param {string} agentPath - Path to compiled agent (relative to project root) + * @param {Object} metadata - Agent metadata + * @returns {Object|null} Info about created skill + */ + async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { + const destDir = this.getKimiSkillsDir(projectDir); + + // Skill name from the dash name (without .md) + const skillName = customAgentDashName(agentName).replace(/\.md$/, ''); + const skillDir = path.join(destDir, skillName); + await fs.ensureDir(skillDir); + + const description = metadata?.description || `${agentName} agent`; + const fm = yaml.stringify({ name: skillName, description }).trimEnd(); + const skillContent = + `---\n${fm}\n---\n` + + "\nYou must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.\n" + + '\n\n' + + `1. LOAD the FULL agent file from @${agentPath}\n` + + '2. READ its entire contents - this contains the complete agent persona, menu, and instructions\n' + + '3. FOLLOW every step in the section precisely\n' + + '4. DISPLAY the welcome/greeting as instructed\n' + + '5. PRESENT the numbered menu\n' + + '6. WAIT for user input before proceeding\n' + + '\n'; + + // Write with platform-native line endings + const os = require('node:os'); + const platformContent = skillContent.replaceAll('\n', os.EOL); + const skillPath = path.join(skillDir, 'SKILL.md'); + await fs.writeFile(skillPath, platformContent, 'utf8'); + + return { + path: path.relative(projectDir, skillPath), + command: `/skill:${skillName}`, + }; + } +} + +module.exports = { KimiCliSetup }; diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index 9e286fdd3..f30f2de38 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -61,7 +61,7 @@ class IdeManager { */ async loadCustomInstallerFiles() { const ideDir = __dirname; - const customFiles = ['codex.js', 'github-copilot.js', 'kilo.js', 'rovodev.js']; + const customFiles = ['codex.js', 'github-copilot.js', 'kilo.js', 'rovodev.js', 'kimi-cli.js']; for (const file of customFiles) { const filePath = path.join(ideDir, file); diff --git a/tools/cli/installers/lib/ide/platform-codes.yaml b/tools/cli/installers/lib/ide/platform-codes.yaml index 4e6ca8070..a61bbdafe 100644 --- a/tools/cli/installers/lib/ide/platform-codes.yaml +++ b/tools/cli/installers/lib/ide/platform-codes.yaml @@ -117,6 +117,13 @@ platforms: description: "AI coding platform" # No installer config - uses custom kilo.js (creates .kilocodemodes file) + kimi-cli: + name: "Kimi Code CLI" + preferred: false + category: cli + description: "Moonshot AI's official CLI for Kimi" + # No installer config - uses custom kimi-cli.js (creates .kimi/skills directory structure) + kiro: name: "Kiro" preferred: false From 304b68130d8ed48463c761ee052ed0c83a41fae7 Mon Sep 17 00:00:00 2001 From: Yang Date: Wed, 4 Mar 2026 15:41:51 +0800 Subject: [PATCH 2/2] fix: address CodeRabbit review comments for Kimi CLI support --- tools/cli/installers/lib/ide/kimi-cli.js | 24 +++++++++++++----------- tools/cli/installers/lib/ide/manager.js | 4 ++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/tools/cli/installers/lib/ide/kimi-cli.js b/tools/cli/installers/lib/ide/kimi-cli.js index c9b36b883..3263a326c 100644 --- a/tools/cli/installers/lib/ide/kimi-cli.js +++ b/tools/cli/installers/lib/ide/kimi-cli.js @@ -1,4 +1,5 @@ const path = require('node:path'); +const os = require('node:os'); const fs = require('fs-extra'); const yaml = require('yaml'); const { BaseIdeSetup } = require('./_base-ide'); @@ -15,7 +16,7 @@ const prompts = require('../../../lib/prompts'); */ class KimiCliSetup extends BaseIdeSetup { constructor() { - super('kimi-cli', 'Kimi Code CLI', true); + super('kimi-cli', 'Kimi Code CLI', false); } /** @@ -25,6 +26,9 @@ class KimiCliSetup extends BaseIdeSetup { * @param {Object} options - Setup options */ async setup(projectDir, bmadDir, options = {}) { + if (!bmadDir) { + throw new Error('bmadDir is required for Kimi CLI setup'); + } if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); const { artifacts, counts } = await this.collectClaudeArtifacts(projectDir, bmadDir, options); @@ -40,7 +44,7 @@ class KimiCliSetup extends BaseIdeSetup { const agentCount = await this.writeSkillArtifacts(destDir, agentArtifacts, 'agent-launcher'); // Collect and write task skills - const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []); + const tasks = await getTasksFromBmad(bmadDir); const taskArtifacts = []; for (const task of tasks) { const content = await this.readAndProcessWithProject( @@ -104,8 +108,8 @@ class KimiCliSetup extends BaseIdeSetup { if (entries && entries.some((entry) => entry && typeof entry === 'string' && entry.startsWith('bmad'))) { return true; } - } catch { - // Ignore errors + } catch (error) { + if (!options.silent) await prompts.log.debug(`Debug: Could not read directory ${dir}: ${error.message}`); } } @@ -208,7 +212,6 @@ class KimiCliSetup extends BaseIdeSetup { const skillContent = this.transformToSkillFormat(artifact.content, skillName); // Write SKILL.md with platform-native line endings - const os = require('node:os'); const platformContent = skillContent.replaceAll('\n', os.EOL); await fs.writeFile(path.join(skillDir, 'SKILL.md'), platformContent, 'utf8'); writtenCount++; @@ -229,7 +232,7 @@ class KimiCliSetup extends BaseIdeSetup { content = content.replaceAll('\r\n', '\n').replaceAll('\r', '\n'); // Parse frontmatter - const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); + const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/); if (!fmMatch) { // No frontmatter -- wrap with minimal frontmatter const fm = yaml.stringify({ name: skillName, description: skillName }).trimEnd(); @@ -304,7 +307,7 @@ class KimiCliSetup extends BaseIdeSetup { * @param {string} destDir - Optional destination directory * @returns {string} Instructions text */ - getProjectSpecificInstructions(projectDir = null, destDir = null) { + getProjectSpecificInstructions(destDir = null) { const lines = [ 'Project-Specific Kimi CLI Configuration', '', @@ -323,10 +326,10 @@ class KimiCliSetup extends BaseIdeSetup { /** * Cleanup Kimi CLI configuration */ - async cleanup(projectDir = null) { + async cleanup(projectDir = null, options = {}) { if (projectDir) { const destDir = this.getKimiSkillsDir(projectDir); - await this.clearOldBmadSkills(destDir); + await this.clearOldBmadSkills(destDir, options); } } @@ -352,7 +355,7 @@ class KimiCliSetup extends BaseIdeSetup { `---\n${fm}\n---\n` + "\nYou must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.\n" + '\n\n' + - `1. LOAD the FULL agent file from @${agentPath}\n` + + `1. LOAD the FULL agent file from ${agentPath}\n` + '2. READ its entire contents - this contains the complete agent persona, menu, and instructions\n' + '3. FOLLOW every step in the section precisely\n' + '4. DISPLAY the welcome/greeting as instructed\n' + @@ -361,7 +364,6 @@ class KimiCliSetup extends BaseIdeSetup { '\n'; // Write with platform-native line endings - const os = require('node:os'); const platformContent = skillContent.replaceAll('\n', os.EOL); const skillPath = path.join(skillDir, 'SKILL.md'); await fs.writeFile(skillPath, platformContent, 'utf8'); diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index f30f2de38..82cfe4746 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -8,7 +8,7 @@ const prompts = require('../../../lib/prompts'); * Dynamically discovers and loads IDE handlers * * Loading strategy: - * 1. Custom installer files (codex.js, github-copilot.js, kilo.js, rovodev.js) - for platforms with unique installation logic + * 1. Custom installer files (codex.js, github-copilot.js, kilo.js, rovodev.js, kimi-cli.js) - for platforms with unique installation logic * 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns */ class IdeManager { @@ -44,7 +44,7 @@ class IdeManager { /** * Dynamically load all IDE handlers - * 1. Load custom installer files first (codex.js, github-copilot.js, kilo.js, rovodev.js) + * 1. Load custom installer files first (codex.js, github-copilot.js, kilo.js, rovodev.js, kimi-cli.js) * 2. Load config-driven handlers from platform-codes.yaml */ async loadHandlers() {