From fc477143400fd11d953727bf1b4277af5ad308d1 Mon Sep 17 00:00:00 2001 From: Dominique Vidjanagni <41875532+DomVidja@users.noreply.github.com> Date: Tue, 12 Aug 2025 16:35:57 -0400 Subject: [PATCH] Feat/add-kilocode-rules (#1040) * feat: Add Kilo Code integration to TaskMaster * feat: Add Kilo profile configuration to rule transformer tests * refactor: Improve code formatting and consistency in Kilo profile and tests * fix: Correct formatting of workspaces in package.json * chore: add changeset for Kilo Code integration * feat: add Kilo Code rules and mode configurations - Add comprehensive rule sets for all modes (architect, ask, code, debug, orchestrator, test) - Update .kilocodemodes configuration with mode-specific settings - Configure MCP integration for Kilo Code profile - Establish consistent rule structure across all modes * refactor(kilo): simplify profile to reuse roo rules with replacements Remove duplicate Kilo-specific rule files and assets in favor of reusing roo rules with dynamic replacements, eliminating 900+ lines of duplicated code while maintaining full Kilo functionality. The profile now: - Reuses ROO_MODES constant instead of maintaining separate KILO_MODES - Applies text replacements to convert roo references to kilo - Maps roo rule files to kilo equivalents via fileMap - Removes all duplicate rule files from assets/kilocode directory * refactor(kilo): restructure object literals for consistency and remove duplicate customReplacements array based on CodeRabbit's suggestion * chore: remove disabled .mcp.json by mistake --------- Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> --- .changeset/curly-poets-move.md | 6 + src/constants/profiles.js | 4 +- src/profiles/index.js | 1 + src/profiles/kilo.js | 186 +++++++++++++++ tests/unit/profiles/kilo-integration.test.js | 192 ++++++++++++++++ .../profiles/rule-transformer-kilo.test.js | 216 ++++++++++++++++++ tests/unit/profiles/rule-transformer.test.js | 5 + 7 files changed, 609 insertions(+), 1 deletion(-) create mode 100644 .changeset/curly-poets-move.md create mode 100644 src/profiles/kilo.js create mode 100644 tests/unit/profiles/kilo-integration.test.js create mode 100644 tests/unit/profiles/rule-transformer-kilo.test.js diff --git a/.changeset/curly-poets-move.md b/.changeset/curly-poets-move.md new file mode 100644 index 00000000..87ebd8d6 --- /dev/null +++ b/.changeset/curly-poets-move.md @@ -0,0 +1,6 @@ +--- +"extension": minor +"task-master-ai": minor +--- + +"Add Kilo Code profile integration with custom modes and MCP configuration" diff --git a/src/constants/profiles.js b/src/constants/profiles.js index 9c24648e..56d1cd55 100644 --- a/src/constants/profiles.js +++ b/src/constants/profiles.js @@ -1,5 +1,5 @@ /** - * @typedef {'amp' | 'claude' | 'cline' | 'codex' | 'cursor' | 'gemini' | 'kiro' | 'opencode' | 'roo' | 'trae' | 'windsurf' | 'vscode' | 'zed'} RulesProfile + * @typedef {'amp' | 'claude' | 'cline' | 'codex' | 'cursor' | 'gemini' | 'kiro' | 'opencode' | 'kilo' | 'roo' | 'trae' | 'windsurf' | 'vscode' | 'zed'} RulesProfile */ /** @@ -18,6 +18,7 @@ * - gemini: Gemini integration * - kiro: Kiro IDE rules * - opencode: OpenCode integration + * - kilo: Kilo Code integration * - roo: Roo Code IDE rules * - trae: Trae IDE rules * - vscode: VS Code with GitHub Copilot integration @@ -38,6 +39,7 @@ export const RULE_PROFILES = [ 'gemini', 'kiro', 'opencode', + 'kilo', 'roo', 'trae', 'vscode', diff --git a/src/profiles/index.js b/src/profiles/index.js index d906e474..9bbbbcd0 100644 --- a/src/profiles/index.js +++ b/src/profiles/index.js @@ -5,6 +5,7 @@ export { clineProfile } from './cline.js'; export { codexProfile } from './codex.js'; export { cursorProfile } from './cursor.js'; export { geminiProfile } from './gemini.js'; +export { kiloProfile } from './kilo.js'; export { kiroProfile } from './kiro.js'; export { opencodeProfile } from './opencode.js'; export { rooProfile } from './roo.js'; diff --git a/src/profiles/kilo.js b/src/profiles/kilo.js new file mode 100644 index 00000000..b7efd5d8 --- /dev/null +++ b/src/profiles/kilo.js @@ -0,0 +1,186 @@ +// Kilo Code conversion profile for rule-transformer +import path from 'path'; +import fs from 'fs'; +import { isSilentMode, log } from '../../scripts/modules/utils.js'; +import { createProfile, COMMON_TOOL_MAPPINGS } from './base-profile.js'; +import { ROO_MODES } from '../constants/profiles.js'; + +// Utility function to apply kilo transformations to content +function applyKiloTransformations(content) { + const customReplacements = [ + // Replace roo-specific terms with kilo equivalents + { + from: /\broo\b/gi, + to: (match) => (match.charAt(0) === 'R' ? 'Kilo' : 'kilo') + }, + { from: /Roo/g, to: 'Kilo' }, + { from: /ROO/g, to: 'KILO' }, + { from: /roocode\.com/gi, to: 'kilocode.com' }, + { from: /docs\.roocode\.com/gi, to: 'docs.kilocode.com' }, + { from: /https?:\/\/roocode\.com/gi, to: 'https://kilocode.com' }, + { + from: /https?:\/\/docs\.roocode\.com/gi, + to: 'https://docs.kilocode.com' + }, + { from: /\.roo\//g, to: '.kilo/' }, + { from: /\.roomodes/g, to: '.kilocodemodes' }, + // Handle file extensions and directory references + { from: /roo-rules/g, to: 'kilo-rules' }, + { from: /rules-roo/g, to: 'rules-kilo' } + ]; + + let transformedContent = content; + for (const replacement of customReplacements) { + transformedContent = transformedContent.replace( + replacement.from, + replacement.to + ); + } + return transformedContent; +} + +// Utility function to copy files recursively +function copyRecursiveSync(src, dest) { + const exists = fs.existsSync(src); + const stats = exists && fs.statSync(src); + const isDirectory = exists && stats.isDirectory(); + if (isDirectory) { + if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); + fs.readdirSync(src).forEach((childItemName) => { + copyRecursiveSync( + path.join(src, childItemName), + path.join(dest, childItemName) + ); + }); + } else { + fs.copyFileSync(src, dest); + } +} + +// Lifecycle functions for Kilo profile +function onAddRulesProfile(targetDir, assetsDir) { + // Use the provided assets directory to find the roocode directory + const sourceDir = path.join(assetsDir, 'roocode'); + + if (!fs.existsSync(sourceDir)) { + log('error', `[Kilo] Source directory does not exist: ${sourceDir}`); + return; + } + + // Copy basic roocode structure first + copyRecursiveSync(sourceDir, targetDir); + log('debug', `[Kilo] Copied roocode directory to ${targetDir}`); + + // Transform .roomodes to .kilocodemodes + const roomodesSrc = path.join(sourceDir, '.roomodes'); + const kilocodemodesDest = path.join(targetDir, '.kilocodemodes'); + if (fs.existsSync(roomodesSrc)) { + try { + const roomodesContent = fs.readFileSync(roomodesSrc, 'utf8'); + const transformedContent = applyKiloTransformations(roomodesContent); + fs.writeFileSync(kilocodemodesDest, transformedContent); + log('debug', `[Kilo] Created .kilocodemodes at ${kilocodemodesDest}`); + + // Remove the original .roomodes file + fs.unlinkSync(path.join(targetDir, '.roomodes')); + } catch (err) { + log('error', `[Kilo] Failed to transform .roomodes: ${err.message}`); + } + } + + // Transform .roo directory to .kilo and apply kilo transformations to mode-specific rules + const rooModesDir = path.join(sourceDir, '.roo'); + const kiloModesDir = path.join(targetDir, '.kilo'); + + // Remove the copied .roo directory and create .kilo + if (fs.existsSync(path.join(targetDir, '.roo'))) { + fs.rmSync(path.join(targetDir, '.roo'), { recursive: true, force: true }); + } + + for (const mode of ROO_MODES) { + const src = path.join(rooModesDir, `rules-${mode}`, `${mode}-rules`); + const dest = path.join(kiloModesDir, `rules-${mode}`, `${mode}-rules`); + if (fs.existsSync(src)) { + try { + const destDir = path.dirname(dest); + if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true }); + + // Read, transform, and write the rule file + const ruleContent = fs.readFileSync(src, 'utf8'); + const transformedContent = applyKiloTransformations(ruleContent); + fs.writeFileSync(dest, transformedContent); + + log('debug', `[Kilo] Transformed and copied ${mode}-rules to ${dest}`); + } catch (err) { + log( + 'error', + `[Kilo] Failed to transform ${src} to ${dest}: ${err.message}` + ); + } + } + } +} + +function onRemoveRulesProfile(targetDir) { + const kilocodemodespath = path.join(targetDir, '.kilocodemodes'); + if (fs.existsSync(kilocodemodespath)) { + try { + fs.rmSync(kilocodemodespath, { force: true }); + log('debug', `[Kilo] Removed .kilocodemodes from ${kilocodemodespath}`); + } catch (err) { + log('error', `[Kilo] Failed to remove .kilocodemodes: ${err.message}`); + } + } + + const kiloDir = path.join(targetDir, '.kilo'); + if (fs.existsSync(kiloDir)) { + fs.readdirSync(kiloDir).forEach((entry) => { + if (entry.startsWith('rules-')) { + const modeDir = path.join(kiloDir, entry); + try { + fs.rmSync(modeDir, { recursive: true, force: true }); + log('debug', `[Kilo] Removed ${entry} directory from ${modeDir}`); + } catch (err) { + log('error', `[Kilo] Failed to remove ${modeDir}: ${err.message}`); + } + } + }); + if (fs.readdirSync(kiloDir).length === 0) { + try { + fs.rmSync(kiloDir, { recursive: true, force: true }); + log('debug', `[Kilo] Removed empty .kilo directory from ${kiloDir}`); + } catch (err) { + log('error', `[Kilo] Failed to remove .kilo directory: ${err.message}`); + } + } + } +} + +function onPostConvertRulesProfile(targetDir, assetsDir) { + onAddRulesProfile(targetDir, assetsDir); +} + +// Create and export kilo profile using the base factory with roo rule reuse +export const kiloProfile = createProfile({ + name: 'kilo', + displayName: 'Kilo Code', + url: 'kilocode.com', + docsUrl: 'docs.kilocode.com', + profileDir: '.kilo', + rulesDir: '.kilo/rules', + toolMappings: COMMON_TOOL_MAPPINGS.ROO_STYLE, + + fileMap: { + // Map roo rule files to kilo equivalents + 'rules/cursor_rules.mdc': 'kilo_rules.md', + 'rules/dev_workflow.mdc': 'dev_workflow.md', + 'rules/self_improve.mdc': 'self_improve.md', + 'rules/taskmaster.mdc': 'taskmaster.md' + }, + onAdd: onAddRulesProfile, + onRemove: onRemoveRulesProfile, + onPostConvert: onPostConvertRulesProfile +}); + +// Export lifecycle functions separately to avoid naming conflicts +export { onAddRulesProfile, onRemoveRulesProfile, onPostConvertRulesProfile }; diff --git a/tests/unit/profiles/kilo-integration.test.js b/tests/unit/profiles/kilo-integration.test.js new file mode 100644 index 00000000..119bf3e9 --- /dev/null +++ b/tests/unit/profiles/kilo-integration.test.js @@ -0,0 +1,192 @@ +import { jest } from '@jest/globals'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; + +// Mock external modules +jest.mock('child_process', () => ({ + execSync: jest.fn() +})); + +// Mock console methods +jest.mock('console', () => ({ + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + clear: jest.fn() +})); + +describe('Kilo Integration', () => { + let tempDir; + + beforeEach(() => { + jest.clearAllMocks(); + + // Create a temporary directory for testing + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-')); + + // Spy on fs methods + jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); + jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => { + if (filePath.toString().includes('.kilocodemodes')) { + return 'Existing kilocodemodes content'; + } + if (filePath.toString().includes('-rules')) { + return 'Existing mode rules content'; + } + return '{}'; + }); + jest.spyOn(fs, 'existsSync').mockImplementation(() => false); + jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {}); + }); + + afterEach(() => { + // Clean up the temporary directory + try { + fs.rmSync(tempDir, { recursive: true, force: true }); + } catch (err) { + console.error(`Error cleaning up: ${err.message}`); + } + }); + + // Test function that simulates the createProjectStructure behavior for Kilo files + function mockCreateKiloStructure() { + // Create main .kilo directory + fs.mkdirSync(path.join(tempDir, '.kilo'), { recursive: true }); + + // Create rules directory + fs.mkdirSync(path.join(tempDir, '.kilo', 'rules'), { recursive: true }); + + // Create mode-specific rule directories + const kiloModes = [ + 'architect', + 'ask', + 'orchestrator', + 'code', + 'debug', + 'test' + ]; + for (const mode of kiloModes) { + fs.mkdirSync(path.join(tempDir, '.kilo', `rules-${mode}`), { + recursive: true + }); + fs.writeFileSync( + path.join(tempDir, '.kilo', `rules-${mode}`, `${mode}-rules`), + `Content for ${mode} rules` + ); + } + + // Create additional directories + fs.mkdirSync(path.join(tempDir, '.kilo', 'config'), { recursive: true }); + fs.mkdirSync(path.join(tempDir, '.kilo', 'templates'), { recursive: true }); + fs.mkdirSync(path.join(tempDir, '.kilo', 'logs'), { recursive: true }); + + // Copy .kilocodemodes file + fs.writeFileSync( + path.join(tempDir, '.kilocodemodes'), + 'Kilocodemodes file content' + ); + } + + test('creates all required .kilo directories', () => { + // Act + mockCreateKiloStructure(); + + // Assert + expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.kilo'), { + recursive: true + }); + expect(fs.mkdirSync).toHaveBeenCalledWith( + path.join(tempDir, '.kilo', 'rules'), + { recursive: true } + ); + + // Verify all mode directories are created + expect(fs.mkdirSync).toHaveBeenCalledWith( + path.join(tempDir, '.kilo', 'rules-architect'), + { recursive: true } + ); + expect(fs.mkdirSync).toHaveBeenCalledWith( + path.join(tempDir, '.kilo', 'rules-ask'), + { recursive: true } + ); + expect(fs.mkdirSync).toHaveBeenCalledWith( + path.join(tempDir, '.kilo', 'rules-orchestrator'), + { recursive: true } + ); + expect(fs.mkdirSync).toHaveBeenCalledWith( + path.join(tempDir, '.kilo', 'rules-code'), + { recursive: true } + ); + expect(fs.mkdirSync).toHaveBeenCalledWith( + path.join(tempDir, '.kilo', 'rules-debug'), + { recursive: true } + ); + expect(fs.mkdirSync).toHaveBeenCalledWith( + path.join(tempDir, '.kilo', 'rules-test'), + { recursive: true } + ); + }); + + test('creates rule files for all modes', () => { + // Act + mockCreateKiloStructure(); + + // Assert - check all rule files are created + expect(fs.writeFileSync).toHaveBeenCalledWith( + path.join(tempDir, '.kilo', 'rules-architect', 'architect-rules'), + expect.any(String) + ); + expect(fs.writeFileSync).toHaveBeenCalledWith( + path.join(tempDir, '.kilo', 'rules-ask', 'ask-rules'), + expect.any(String) + ); + expect(fs.writeFileSync).toHaveBeenCalledWith( + path.join(tempDir, '.kilo', 'rules-orchestrator', 'orchestrator-rules'), + expect.any(String) + ); + expect(fs.writeFileSync).toHaveBeenCalledWith( + path.join(tempDir, '.kilo', 'rules-code', 'code-rules'), + expect.any(String) + ); + expect(fs.writeFileSync).toHaveBeenCalledWith( + path.join(tempDir, '.kilo', 'rules-debug', 'debug-rules'), + expect.any(String) + ); + expect(fs.writeFileSync).toHaveBeenCalledWith( + path.join(tempDir, '.kilo', 'rules-test', 'test-rules'), + expect.any(String) + ); + }); + + test('creates .kilocodemodes file in project root', () => { + // Act + mockCreateKiloStructure(); + + // Assert + expect(fs.writeFileSync).toHaveBeenCalledWith( + path.join(tempDir, '.kilocodemodes'), + expect.any(String) + ); + }); + + test('creates additional required Kilo directories', () => { + // Act + mockCreateKiloStructure(); + + // Assert + expect(fs.mkdirSync).toHaveBeenCalledWith( + path.join(tempDir, '.kilo', 'config'), + { recursive: true } + ); + expect(fs.mkdirSync).toHaveBeenCalledWith( + path.join(tempDir, '.kilo', 'templates'), + { recursive: true } + ); + expect(fs.mkdirSync).toHaveBeenCalledWith( + path.join(tempDir, '.kilo', 'logs'), + { recursive: true } + ); + }); +}); diff --git a/tests/unit/profiles/rule-transformer-kilo.test.js b/tests/unit/profiles/rule-transformer-kilo.test.js new file mode 100644 index 00000000..a32f9734 --- /dev/null +++ b/tests/unit/profiles/rule-transformer-kilo.test.js @@ -0,0 +1,216 @@ +import { jest } from '@jest/globals'; + +// Mock fs module before importing anything that uses it +jest.mock('fs', () => ({ + readFileSync: jest.fn(), + writeFileSync: jest.fn(), + existsSync: jest.fn(), + mkdirSync: jest.fn() +})); + +// Import modules after mocking +import fs from 'fs'; +import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js'; +import { kiloProfile } from '../../../src/profiles/kilo.js'; + +describe('Kilo Rule Transformer', () => { + // Set up spies on the mocked modules + const mockReadFileSync = jest.spyOn(fs, 'readFileSync'); + const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync'); + const mockExistsSync = jest.spyOn(fs, 'existsSync'); + const mockMkdirSync = jest.spyOn(fs, 'mkdirSync'); + const mockConsoleError = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + beforeEach(() => { + jest.clearAllMocks(); + // Setup default mocks + mockReadFileSync.mockReturnValue(''); + mockWriteFileSync.mockImplementation(() => {}); + mockExistsSync.mockReturnValue(true); + mockMkdirSync.mockImplementation(() => {}); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it('should correctly convert basic terms', () => { + const testContent = `--- +description: Test Cursor rule for basic terms +globs: **/* +alwaysApply: true +--- + +This is a Cursor rule that references cursor.so and uses the word Cursor multiple times. +Also has references to .mdc files.`; + + // Mock file read to return our test content + mockReadFileSync.mockReturnValue(testContent); + + // Call the actual function + const result = convertRuleToProfileRule( + 'source.mdc', + 'target.md', + kiloProfile + ); + + // Verify the function succeeded + expect(result).toBe(true); + + // Verify file operations were called correctly + expect(mockReadFileSync).toHaveBeenCalledWith('source.mdc', 'utf8'); + expect(mockWriteFileSync).toHaveBeenCalledTimes(1); + + // Get the transformed content that was written + const writeCall = mockWriteFileSync.mock.calls[0]; + const transformedContent = writeCall[1]; + + // Verify transformations + expect(transformedContent).toContain('Kilo'); + expect(transformedContent).toContain('kilocode.com'); + expect(transformedContent).toContain('.md'); + expect(transformedContent).not.toContain('cursor.so'); + expect(transformedContent).not.toContain('Cursor rule'); + }); + + it('should correctly convert tool references', () => { + const testContent = `--- +description: Test Cursor rule for tool references +globs: **/* +alwaysApply: true +--- + +- Use the search tool to find code +- The edit_file tool lets you modify files +- run_command executes terminal commands +- use_mcp connects to external services`; + + // Mock file read to return our test content + mockReadFileSync.mockReturnValue(testContent); + + // Call the actual function + const result = convertRuleToProfileRule( + 'source.mdc', + 'target.md', + kiloProfile + ); + + // Verify the function succeeded + expect(result).toBe(true); + + // Get the transformed content that was written + const writeCall = mockWriteFileSync.mock.calls[0]; + const transformedContent = writeCall[1]; + + // Verify transformations (Kilo uses different tool names) + expect(transformedContent).toContain('search_files tool'); + expect(transformedContent).toContain('apply_diff tool'); + expect(transformedContent).toContain('execute_command'); + expect(transformedContent).toContain('use_mcp_tool'); + }); + + it('should correctly update file references', () => { + const testContent = `--- +description: Test Cursor rule for file references +globs: **/* +alwaysApply: true +--- + +This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and +[taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).`; + + // Mock file read to return our test content + mockReadFileSync.mockReturnValue(testContent); + + // Call the actual function + const result = convertRuleToProfileRule( + 'source.mdc', + 'target.md', + kiloProfile + ); + + // Verify the function succeeded + expect(result).toBe(true); + + // Get the transformed content that was written + const writeCall = mockWriteFileSync.mock.calls[0]; + const transformedContent = writeCall[1]; + + // Verify transformations - no taskmaster subdirectory for Kilo + expect(transformedContent).toContain('(.kilo/rules/dev_workflow.md)'); // File path transformation for dev_workflow - no taskmaster subdirectory for Kilo + expect(transformedContent).toContain('(.kilo/rules/taskmaster.md)'); // File path transformation for taskmaster - no taskmaster subdirectory for Kilo + expect(transformedContent).not.toContain('(mdc:.cursor/rules/'); + }); + + it('should handle file read errors', () => { + // Mock file read to throw an error + mockReadFileSync.mockImplementation(() => { + throw new Error('File not found'); + }); + + // Call the actual function + const result = convertRuleToProfileRule( + 'nonexistent.mdc', + 'target.md', + kiloProfile + ); + + // Verify the function failed gracefully + expect(result).toBe(false); + + // Verify writeFileSync was not called + expect(mockWriteFileSync).not.toHaveBeenCalled(); + + // Verify error was logged + expect(mockConsoleError).toHaveBeenCalledWith( + 'Error converting rule file: File not found' + ); + }); + + it('should handle file write errors', () => { + const testContent = 'test content'; + mockReadFileSync.mockReturnValue(testContent); + + // Mock file write to throw an error + mockWriteFileSync.mockImplementation(() => { + throw new Error('Permission denied'); + }); + + // Call the actual function + const result = convertRuleToProfileRule( + 'source.mdc', + 'target.md', + kiloProfile + ); + + // Verify the function failed gracefully + expect(result).toBe(false); + + // Verify error was logged + expect(mockConsoleError).toHaveBeenCalledWith( + 'Error converting rule file: Permission denied' + ); + }); + + it('should create target directory if it does not exist', () => { + const testContent = 'test content'; + mockReadFileSync.mockReturnValue(testContent); + + // Mock directory doesn't exist initially + mockExistsSync.mockReturnValue(false); + + // Call the actual function + convertRuleToProfileRule( + 'source.mdc', + 'some/deep/path/target.md', + kiloProfile + ); + + // Verify directory creation was called + expect(mockMkdirSync).toHaveBeenCalledWith('some/deep/path', { + recursive: true + }); + }); +}); diff --git a/tests/unit/profiles/rule-transformer.test.js b/tests/unit/profiles/rule-transformer.test.js index 4e2fbcee..4cbaf6d6 100644 --- a/tests/unit/profiles/rule-transformer.test.js +++ b/tests/unit/profiles/rule-transformer.test.js @@ -228,6 +228,11 @@ describe('Rule Transformer - General', () => { mcpConfigName: 'mcp.json', expectedPath: '.roo/mcp.json' }, + kilo: { + mcpConfig: true, + mcpConfigName: 'mcp.json', + expectedPath: '.kilo/mcp.json' + }, trae: { mcpConfig: false, mcpConfigName: null,