skiller 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +989 -0
  3. package/dist/agents/AbstractAgent.js +92 -0
  4. package/dist/agents/AgentsMdAgent.js +85 -0
  5. package/dist/agents/AiderAgent.js +108 -0
  6. package/dist/agents/AmazonQCliAgent.js +103 -0
  7. package/dist/agents/AmpAgent.js +13 -0
  8. package/dist/agents/AugmentCodeAgent.js +70 -0
  9. package/dist/agents/ClaudeAgent.js +95 -0
  10. package/dist/agents/ClineAgent.js +53 -0
  11. package/dist/agents/CodexCliAgent.js +143 -0
  12. package/dist/agents/CopilotAgent.js +43 -0
  13. package/dist/agents/CrushAgent.js +128 -0
  14. package/dist/agents/CursorAgent.js +93 -0
  15. package/dist/agents/FirebaseAgent.js +61 -0
  16. package/dist/agents/FirebenderAgent.js +205 -0
  17. package/dist/agents/GeminiCliAgent.js +99 -0
  18. package/dist/agents/GooseAgent.js +58 -0
  19. package/dist/agents/IAgent.js +2 -0
  20. package/dist/agents/JulesAgent.js +14 -0
  21. package/dist/agents/JunieAgent.js +53 -0
  22. package/dist/agents/KiloCodeAgent.js +63 -0
  23. package/dist/agents/KiroAgent.js +50 -0
  24. package/dist/agents/OpenCodeAgent.js +99 -0
  25. package/dist/agents/OpenHandsAgent.js +56 -0
  26. package/dist/agents/QwenCodeAgent.js +82 -0
  27. package/dist/agents/RooCodeAgent.js +139 -0
  28. package/dist/agents/TraeAgent.js +54 -0
  29. package/dist/agents/WarpAgent.js +61 -0
  30. package/dist/agents/WindsurfAgent.js +27 -0
  31. package/dist/agents/ZedAgent.js +132 -0
  32. package/dist/agents/agent-utils.js +37 -0
  33. package/dist/agents/index.js +77 -0
  34. package/dist/cli/commands.js +136 -0
  35. package/dist/cli/handlers.js +221 -0
  36. package/dist/cli/index.js +5 -0
  37. package/dist/constants.js +58 -0
  38. package/dist/core/ConfigLoader.js +274 -0
  39. package/dist/core/FileSystemUtils.js +421 -0
  40. package/dist/core/FrontmatterParser.js +142 -0
  41. package/dist/core/GitignoreUtils.js +171 -0
  42. package/dist/core/RuleProcessor.js +60 -0
  43. package/dist/core/SkillsProcessor.js +528 -0
  44. package/dist/core/SkillsUtils.js +230 -0
  45. package/dist/core/UnifiedConfigLoader.js +432 -0
  46. package/dist/core/UnifiedConfigTypes.js +2 -0
  47. package/dist/core/agent-selection.js +52 -0
  48. package/dist/core/apply-engine.js +668 -0
  49. package/dist/core/config-utils.js +30 -0
  50. package/dist/core/hash.js +24 -0
  51. package/dist/core/revert-engine.js +413 -0
  52. package/dist/lib.js +196 -0
  53. package/dist/mcp/capabilities.js +65 -0
  54. package/dist/mcp/merge.js +39 -0
  55. package/dist/mcp/propagateOpenCodeMcp.js +116 -0
  56. package/dist/mcp/propagateOpenHandsMcp.js +169 -0
  57. package/dist/mcp/validate.js +17 -0
  58. package/dist/paths/mcp.js +120 -0
  59. package/dist/revert.js +186 -0
  60. package/dist/types.js +2 -0
  61. package/dist/vscode/settings.js +117 -0
  62. package/package.json +77 -0
@@ -0,0 +1,668 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadNestedConfigurations = loadNestedConfigurations;
37
+ exports.loadSingleConfiguration = loadSingleConfiguration;
38
+ exports.processHierarchicalConfigurations = processHierarchicalConfigurations;
39
+ exports.processSingleConfiguration = processSingleConfiguration;
40
+ exports.applyConfigurationsToAgents = applyConfigurationsToAgents;
41
+ exports.updateGitignore = updateGitignore;
42
+ const path = __importStar(require("path"));
43
+ const fs_1 = require("fs");
44
+ const FileSystemUtils = __importStar(require("./FileSystemUtils"));
45
+ const RuleProcessor_1 = require("./RuleProcessor");
46
+ const ConfigLoader_1 = require("./ConfigLoader");
47
+ const GitignoreUtils_1 = require("./GitignoreUtils");
48
+ const merge_1 = require("../mcp/merge");
49
+ const mcp_1 = require("../paths/mcp");
50
+ const propagateOpenHandsMcp_1 = require("../mcp/propagateOpenHandsMcp");
51
+ const propagateOpenCodeMcp_1 = require("../mcp/propagateOpenCodeMcp");
52
+ const agent_utils_1 = require("../agents/agent-utils");
53
+ const capabilities_1 = require("../mcp/capabilities");
54
+ const constants_1 = require("../constants");
55
+ async function loadNestedConfigurations(projectRoot, configPath, localOnly, resolvedNested) {
56
+ const { dirs: rulerDirs } = await findRulerDirectories(projectRoot, localOnly, true);
57
+ const results = [];
58
+ const rulerDirConfigs = await processIndependentRulerDirs(rulerDirs, configPath, resolvedNested);
59
+ for (const { rulerDir, files, config } of rulerDirConfigs) {
60
+ results.push(await createHierarchicalConfiguration(rulerDir, files, config, configPath));
61
+ }
62
+ return results;
63
+ }
64
+ /**
65
+ * Processes each .ruler directory independently, returning configuration for each.
66
+ * Each .ruler directory gets its own rules (not merged with others).
67
+ */
68
+ async function processIndependentRulerDirs(rulerDirs, configPath, resolvedNested) {
69
+ const results = [];
70
+ // Process each .ruler directory independently
71
+ for (const rulerDir of rulerDirs) {
72
+ // Load config first to get rules filtering options
73
+ const config = await loadConfigForRulerDir(rulerDir, configPath, resolvedNested);
74
+ // Apply rules filtering if configured
75
+ const files = await FileSystemUtils.readMarkdownFiles(rulerDir, {
76
+ include: config.rules?.include,
77
+ exclude: config.rules?.exclude,
78
+ merge_strategy: config.rules?.merge_strategy,
79
+ });
80
+ results.push({ rulerDir, files, config });
81
+ }
82
+ return results;
83
+ }
84
+ async function createHierarchicalConfiguration(rulerDir, files, config, cliConfigPath) {
85
+ await warnAboutLegacyMcpJson(rulerDir);
86
+ const concatenatedRules = (0, RuleProcessor_1.concatenateRules)(files, path.dirname(rulerDir));
87
+ const directoryRoot = path.dirname(rulerDir);
88
+ const localConfigPath = path.join(rulerDir, 'ruler.toml');
89
+ let configPathToUse = cliConfigPath;
90
+ try {
91
+ await fs_1.promises.access(localConfigPath);
92
+ configPathToUse = localConfigPath;
93
+ }
94
+ catch {
95
+ // fall back to CLI config or default resolution
96
+ }
97
+ const { loadUnifiedConfig } = await Promise.resolve().then(() => __importStar(require('./UnifiedConfigLoader')));
98
+ const unifiedConfig = await loadUnifiedConfig({
99
+ projectRoot: directoryRoot,
100
+ configPath: configPathToUse,
101
+ });
102
+ let rulerMcpJson = null;
103
+ if (unifiedConfig.mcp && Object.keys(unifiedConfig.mcp.servers).length > 0) {
104
+ rulerMcpJson = {
105
+ mcpServers: unifiedConfig.mcp.servers,
106
+ };
107
+ }
108
+ return {
109
+ rulerDir,
110
+ config,
111
+ concatenatedRules,
112
+ ruleFiles: files,
113
+ rulerMcpJson,
114
+ };
115
+ }
116
+ async function loadConfigForRulerDir(rulerDir, cliConfigPath, resolvedNested) {
117
+ const directoryRoot = path.dirname(rulerDir);
118
+ const localConfigPath = path.join(rulerDir, 'ruler.toml');
119
+ let hasLocalConfig = false;
120
+ try {
121
+ await fs_1.promises.access(localConfigPath);
122
+ hasLocalConfig = true;
123
+ }
124
+ catch {
125
+ hasLocalConfig = false;
126
+ }
127
+ const loaded = await (0, ConfigLoader_1.loadConfig)({
128
+ projectRoot: directoryRoot,
129
+ configPath: hasLocalConfig ? localConfigPath : cliConfigPath,
130
+ });
131
+ const cloned = cloneLoadedConfig(loaded);
132
+ if (resolvedNested) {
133
+ if (hasLocalConfig && loaded.nestedDefined && loaded.nested === false) {
134
+ (0, constants_1.logWarn)(`Nested mode is enabled but ${localConfigPath} sets nested = false. Continuing with nested processing.`);
135
+ }
136
+ cloned.nested = true;
137
+ cloned.nestedDefined = true;
138
+ }
139
+ return cloned;
140
+ }
141
+ function cloneLoadedConfig(config) {
142
+ const clonedAgentConfigs = {};
143
+ for (const [agent, agentConfig] of Object.entries(config.agentConfigs)) {
144
+ clonedAgentConfigs[agent] = {
145
+ ...agentConfig,
146
+ mcp: agentConfig.mcp ? { ...agentConfig.mcp } : undefined,
147
+ };
148
+ }
149
+ return {
150
+ defaultAgents: config.defaultAgents ? [...config.defaultAgents] : undefined,
151
+ agentConfigs: clonedAgentConfigs,
152
+ cliAgents: config.cliAgents ? [...config.cliAgents] : undefined,
153
+ mcp: config.mcp ? { ...config.mcp } : undefined,
154
+ gitignore: config.gitignore ? { ...config.gitignore } : undefined,
155
+ nested: config.nested,
156
+ nestedDefined: config.nestedDefined,
157
+ };
158
+ }
159
+ /**
160
+ * Finds ruler directories based on the specified mode.
161
+ */
162
+ async function findRulerDirectories(projectRoot, localOnly, hierarchical) {
163
+ if (hierarchical) {
164
+ const dirs = await FileSystemUtils.findAllRulerDirs(projectRoot);
165
+ const allDirs = [...dirs];
166
+ // Add global config if not local-only
167
+ if (!localOnly) {
168
+ const globalDir = await FileSystemUtils.findGlobalRulerDir();
169
+ if (globalDir) {
170
+ allDirs.push(globalDir);
171
+ }
172
+ }
173
+ if (allDirs.length === 0) {
174
+ throw (0, constants_1.createRulerError)(`.ruler directory not found`, `Searched from: ${projectRoot}`);
175
+ }
176
+ return { dirs: allDirs, primaryDir: allDirs[0] };
177
+ }
178
+ else {
179
+ const dir = await FileSystemUtils.findRulerDir(projectRoot, !localOnly);
180
+ if (!dir) {
181
+ throw (0, constants_1.createRulerError)(`.ruler directory not found`, `Searched from: ${projectRoot}`);
182
+ }
183
+ return { dirs: [dir], primaryDir: dir };
184
+ }
185
+ }
186
+ /**
187
+ * Warns about legacy mcp.json files if they exist.
188
+ */
189
+ async function warnAboutLegacyMcpJson(rulerDir) {
190
+ try {
191
+ const legacyMcpPath = path.join(rulerDir, 'mcp.json');
192
+ await fs_1.promises.access(legacyMcpPath);
193
+ (0, constants_1.logWarn)('Warning: Using legacy .ruler/mcp.json. Please migrate to ruler.toml. This fallback will be removed in a future release.');
194
+ }
195
+ catch {
196
+ // ignore
197
+ }
198
+ }
199
+ /**
200
+ * Loads configuration for single-directory mode (existing behavior).
201
+ */
202
+ async function loadSingleConfiguration(projectRoot, configPath, localOnly) {
203
+ // Find the single ruler directory
204
+ const { dirs: rulerDirs, primaryDir } = await findRulerDirectories(projectRoot, localOnly, false);
205
+ // Warn about legacy mcp.json
206
+ await warnAboutLegacyMcpJson(primaryDir);
207
+ // Load the ruler.toml configuration
208
+ const config = await (0, ConfigLoader_1.loadConfig)({
209
+ projectRoot,
210
+ configPath,
211
+ });
212
+ // Read rule files with filtering options from config
213
+ const files = await FileSystemUtils.readMarkdownFiles(rulerDirs[0], {
214
+ include: config.rules?.include,
215
+ exclude: config.rules?.exclude,
216
+ merge_strategy: config.rules?.merge_strategy,
217
+ });
218
+ // Concatenate rules
219
+ const concatenatedRules = (0, RuleProcessor_1.concatenateRules)(files, path.dirname(primaryDir));
220
+ // Load unified config to get merged MCP configuration
221
+ const { loadUnifiedConfig } = await Promise.resolve().then(() => __importStar(require('./UnifiedConfigLoader')));
222
+ const unifiedConfig = await loadUnifiedConfig({ projectRoot, configPath });
223
+ // Synthesize rulerMcpJson from unified MCP bundle for backward compatibility
224
+ let rulerMcpJson = null;
225
+ if (unifiedConfig.mcp && Object.keys(unifiedConfig.mcp.servers).length > 0) {
226
+ rulerMcpJson = {
227
+ mcpServers: unifiedConfig.mcp.servers,
228
+ };
229
+ }
230
+ return {
231
+ config,
232
+ concatenatedRules,
233
+ ruleFiles: files,
234
+ rulerMcpJson,
235
+ rulerDir: primaryDir,
236
+ };
237
+ }
238
+ /**
239
+ * Processes hierarchical configurations by applying rules to each .ruler directory independently.
240
+ * Each directory gets its own set of rules and generates its own agent files.
241
+ * @param agents Array of agents to process
242
+ * @param configurations Array of hierarchical configurations for each .ruler directory
243
+ * @param verbose Whether to enable verbose logging
244
+ * @param dryRun Whether to perform a dry run
245
+ * @param cliMcpEnabled Whether MCP is enabled via CLI
246
+ * @param cliMcpStrategy MCP strategy from CLI
247
+ * @returns Promise resolving to array of generated file paths
248
+ */
249
+ async function processHierarchicalConfigurations(agents, configurations, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup = true, skillsEnabled = true) {
250
+ const allGeneratedPaths = [];
251
+ for (const config of configurations) {
252
+ (0, constants_1.logVerboseInfo)(`Processing .ruler directory: ${config.rulerDir}`, verbose, dryRun);
253
+ const rulerRoot = path.dirname(config.rulerDir);
254
+ const paths = await applyConfigurationsToAgents(agents, config.concatenatedRules, config.rulerMcpJson, config.config, rulerRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup, skillsEnabled, config.ruleFiles);
255
+ const normalizedPaths = paths.map((p) => path.isAbsolute(p) ? p : path.join(rulerRoot, p));
256
+ allGeneratedPaths.push(...normalizedPaths);
257
+ }
258
+ return allGeneratedPaths;
259
+ }
260
+ /**
261
+ * Processes a single configuration by applying rules to all selected agents.
262
+ * All rules are concatenated and applied to generate agent files in the project root.
263
+ * @param agents Array of agents to process
264
+ * @param configuration Single ruler configuration with concatenated rules
265
+ * @param projectRoot Root directory of the project
266
+ * @param verbose Whether to enable verbose logging
267
+ * @param dryRun Whether to perform a dry run
268
+ * @param cliMcpEnabled Whether MCP is enabled via CLI
269
+ * @param cliMcpStrategy MCP strategy from CLI
270
+ * @returns Promise resolving to array of generated file paths
271
+ */
272
+ async function processSingleConfiguration(agents, configuration, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup = true, skillsEnabled = true) {
273
+ return await applyConfigurationsToAgents(agents, configuration.concatenatedRules, configuration.rulerMcpJson, configuration.config, projectRoot, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup, skillsEnabled, configuration.ruleFiles, configuration.rulerDir);
274
+ }
275
+ /**
276
+ * Adds Skillz MCP server to rulerMcpJson if skills exist and any agent needs it.
277
+ * Returns augmented MCP config or original if no changes needed.
278
+ */
279
+ async function addSkillzMcpServerIfNeeded(rulerMcpJson, projectRoot, agents, verbose) {
280
+ // Check if any agent supports MCP stdio but not native skills
281
+ // Agents with native skills support (Claude Code, Cursor) are automatically excluded
282
+ const hasAgentNeedingSkillz = agents.some((agent) => agent.supportsMcpStdio?.() && !agent.supportsNativeSkills?.());
283
+ if (!hasAgentNeedingSkillz) {
284
+ return rulerMcpJson;
285
+ }
286
+ // Check if .skillz directory exists
287
+ try {
288
+ const { SKILLZ_DIR } = await Promise.resolve().then(() => __importStar(require('../constants')));
289
+ const skillzPath = path.join(projectRoot, SKILLZ_DIR);
290
+ await fs_1.promises.access(skillzPath);
291
+ // Skills exist, add Skillz MCP server
292
+ const { buildSkillzMcpConfig } = await Promise.resolve().then(() => __importStar(require('./SkillsProcessor')));
293
+ const skillzMcp = buildSkillzMcpConfig(projectRoot);
294
+ // Initialize empty config if null
295
+ const baseConfig = rulerMcpJson || { mcpServers: {} };
296
+ const mcpServers = baseConfig.mcpServers || {};
297
+ (0, constants_1.logVerbose)('Adding Skillz MCP server to configuration for agents that need it', verbose);
298
+ return {
299
+ ...baseConfig,
300
+ mcpServers: {
301
+ ...mcpServers,
302
+ ...skillzMcp,
303
+ },
304
+ };
305
+ }
306
+ catch {
307
+ // No .skillz directory, return original config
308
+ return rulerMcpJson;
309
+ }
310
+ }
311
+ /**
312
+ * Applies configurations to the selected agents (internal function).
313
+ * @param agents Array of agents to process
314
+ * @param concatenatedRules Concatenated rule content
315
+ * @param rulerMcpJson MCP configuration JSON
316
+ * @param config Loaded configuration
317
+ * @param projectRoot Root directory of the project
318
+ * @param verbose Whether to enable verbose logging
319
+ * @param dryRun Whether to perform a dry run
320
+ * @returns Promise resolving to array of generated file paths
321
+ */
322
+ async function applyConfigurationsToAgents(agents, concatenatedRules, rulerMcpJson, config, projectRoot, verbose, dryRun, cliMcpEnabled = true, cliMcpStrategy, backup = true, skillsEnabled = true, ruleFiles, rulerDir) {
323
+ const generatedPaths = [];
324
+ let agentsMdWritten = false;
325
+ // Add Skillz MCP server to rulerMcpJson if skills are enabled
326
+ // This must happen before calling agent.applyRulerConfig() so that agents
327
+ // that handle MCP internally (e.g. Codex, Gemini) receive the Skillz server
328
+ let augmentedRulerMcpJson = rulerMcpJson;
329
+ if (skillsEnabled && !dryRun) {
330
+ augmentedRulerMcpJson = await addSkillzMcpServerIfNeeded(rulerMcpJson, projectRoot, agents, verbose);
331
+ }
332
+ for (const agent of agents) {
333
+ (0, constants_1.logInfo)(`Applying rules for ${agent.getName()}...`, dryRun);
334
+ (0, constants_1.logVerbose)(`Processing agent: ${agent.getName()}`, verbose);
335
+ const agentConfig = config.agentConfigs[agent.getIdentifier()];
336
+ // Collect output paths for .gitignore
337
+ const outputPaths = (0, agent_utils_1.getAgentOutputPaths)(agent, projectRoot, agentConfig);
338
+ (0, constants_1.logVerbose)(`Agent ${agent.getName()} output paths: ${outputPaths.join(', ')}`, verbose);
339
+ generatedPaths.push(...outputPaths);
340
+ // Only add the backup file paths to the gitignore list if backups are enabled
341
+ if (backup) {
342
+ const backupPaths = outputPaths.map((p) => `${p}.bak`);
343
+ generatedPaths.push(...backupPaths);
344
+ }
345
+ if (dryRun) {
346
+ (0, constants_1.logVerbose)(`DRY RUN: Would write rules to: ${outputPaths.join(', ')}`, verbose);
347
+ }
348
+ else {
349
+ let skipApplyForThisAgent = false;
350
+ if (agent.getIdentifier() === 'jules' ||
351
+ agent.getIdentifier() === 'agentsmd') {
352
+ if (agentsMdWritten) {
353
+ // Skip rewriting AGENTS.md, but still allow MCP handling below
354
+ skipApplyForThisAgent = true;
355
+ }
356
+ else {
357
+ agentsMdWritten = true;
358
+ }
359
+ }
360
+ let finalAgentConfig = agentConfig;
361
+ if (agent.getIdentifier() === 'augmentcode' && augmentedRulerMcpJson) {
362
+ const resolvedStrategy = cliMcpStrategy ??
363
+ agentConfig?.mcp?.strategy ??
364
+ config.mcp?.strategy ??
365
+ 'merge';
366
+ finalAgentConfig = {
367
+ ...agentConfig,
368
+ mcp: {
369
+ ...agentConfig?.mcp,
370
+ strategy: resolvedStrategy,
371
+ },
372
+ };
373
+ }
374
+ if (!skipApplyForThisAgent) {
375
+ await agent.applyRulerConfig(concatenatedRules, projectRoot, augmentedRulerMcpJson, finalAgentConfig, backup, ruleFiles, rulerDir, config.rules?.merge_strategy);
376
+ // Add .cursor/rules to gitignore when copying from .claude
377
+ if (agent.getIdentifier() === 'cursor' &&
378
+ config.rules?.merge_strategy === 'cursor' &&
379
+ rulerDir &&
380
+ path.basename(rulerDir) === '.claude') {
381
+ const cursorRulesPath = path.join(projectRoot, '.cursor', 'rules');
382
+ generatedPaths.push(cursorRulesPath);
383
+ }
384
+ }
385
+ }
386
+ // Handle MCP configuration
387
+ await handleMcpConfiguration(agent, agentConfig, config, augmentedRulerMcpJson, projectRoot, generatedPaths, verbose, dryRun, cliMcpEnabled, cliMcpStrategy, backup, skillsEnabled);
388
+ }
389
+ return generatedPaths;
390
+ }
391
+ async function handleMcpConfiguration(agent, agentConfig, config, rulerMcpJson, projectRoot, generatedPaths, verbose, dryRun, cliMcpEnabled = true, cliMcpStrategy, backup = true, skillsEnabled = true) {
392
+ if (!(0, capabilities_1.agentSupportsMcp)(agent)) {
393
+ (0, constants_1.logVerbose)(`Agent ${agent.getName()} does not support MCP - skipping MCP configuration`, verbose);
394
+ return;
395
+ }
396
+ const dest = await (0, mcp_1.getNativeMcpPath)(agent.getName(), projectRoot);
397
+ const mcpEnabledForAgent = cliMcpEnabled && (agentConfig?.mcp?.enabled ?? config.mcp?.enabled ?? true);
398
+ if (!dest || !mcpEnabledForAgent) {
399
+ return;
400
+ }
401
+ let filteredMcpJson = rulerMcpJson
402
+ ? (0, capabilities_1.filterMcpConfigForAgent)(rulerMcpJson, agent)
403
+ : null;
404
+ // Remove Skillz MCP server for agents with native skills support
405
+ if (filteredMcpJson && agent.supportsNativeSkills?.()) {
406
+ const { SKILLZ_MCP_SERVER_NAME } = await Promise.resolve().then(() => __importStar(require('../constants')));
407
+ if (filteredMcpJson.mcpServers &&
408
+ typeof filteredMcpJson.mcpServers === 'object') {
409
+ const mcpServers = { ...filteredMcpJson.mcpServers };
410
+ delete mcpServers[SKILLZ_MCP_SERVER_NAME];
411
+ filteredMcpJson = {
412
+ ...filteredMcpJson,
413
+ mcpServers,
414
+ };
415
+ (0, constants_1.logVerboseInfo)(`Removed Skillz MCP server for ${agent.getName()} (has native skills support)`, verbose, dryRun);
416
+ }
417
+ }
418
+ // Add Skillz MCP server for agents that support stdio but not native skills
419
+ // Only add if skills are enabled
420
+ // Agents with native skills support (Claude Code, Cursor) are automatically excluded
421
+ if (skillsEnabled &&
422
+ agent.supportsMcpStdio?.() &&
423
+ !agent.supportsNativeSkills?.()) {
424
+ // Check if .skillz directory exists
425
+ try {
426
+ const { SKILLZ_DIR } = await Promise.resolve().then(() => __importStar(require('../constants')));
427
+ const skillzPath = path.join(projectRoot, SKILLZ_DIR);
428
+ await fs_1.promises.access(skillzPath);
429
+ // Skills exist, add Skillz MCP server
430
+ const { buildSkillzMcpConfig } = await Promise.resolve().then(() => __importStar(require('./SkillsProcessor')));
431
+ const skillzMcp = buildSkillzMcpConfig(projectRoot);
432
+ // Merge Skillz server into MCP config
433
+ // Initialize empty config if null
434
+ if (!filteredMcpJson) {
435
+ filteredMcpJson = { mcpServers: {} };
436
+ }
437
+ const mcpServers = filteredMcpJson.mcpServers || {};
438
+ filteredMcpJson = {
439
+ ...filteredMcpJson,
440
+ mcpServers: {
441
+ ...mcpServers,
442
+ ...skillzMcp,
443
+ },
444
+ };
445
+ (0, constants_1.logVerboseInfo)(`Added Skillz MCP server for ${agent.getName()}`, verbose, dryRun);
446
+ }
447
+ catch {
448
+ // No .skillz directory, skip adding Skillz server
449
+ }
450
+ }
451
+ if (!filteredMcpJson) {
452
+ (0, constants_1.logVerbose)(`No compatible MCP servers found for ${agent.getName()} - skipping MCP configuration`, verbose);
453
+ return;
454
+ }
455
+ await updateGitignoreForMcpFile(dest, projectRoot, generatedPaths, backup);
456
+ await applyMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, projectRoot, cliMcpStrategy, dryRun, verbose, backup);
457
+ }
458
+ async function updateGitignoreForMcpFile(dest, projectRoot, generatedPaths, backup = true) {
459
+ if (dest.startsWith(projectRoot)) {
460
+ const relativeDest = path.relative(projectRoot, dest);
461
+ generatedPaths.push(relativeDest);
462
+ if (backup) {
463
+ generatedPaths.push(`${relativeDest}.bak`);
464
+ }
465
+ }
466
+ }
467
+ async function applyMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, projectRoot, cliMcpStrategy, dryRun, verbose, backup = true) {
468
+ // Prevent writing MCP configs outside the project root (e.g., legacy home-directory targets)
469
+ if (!dest.startsWith(projectRoot)) {
470
+ (0, constants_1.logVerbose)(`Skipping MCP config for ${agent.getName()} because target path is outside project: ${dest}`, verbose);
471
+ return;
472
+ }
473
+ if (agent.getIdentifier() === 'openhands') {
474
+ return await applyOpenHandsMcpConfiguration(filteredMcpJson, dest, dryRun, verbose, backup);
475
+ }
476
+ if (agent.getIdentifier() === 'opencode') {
477
+ return await applyOpenCodeMcpConfiguration(filteredMcpJson, dest, dryRun, verbose, backup);
478
+ }
479
+ // Agents that handle MCP configuration internally should not have external MCP handling
480
+ if (agent.getIdentifier() === 'codex' ||
481
+ agent.getIdentifier() === 'zed' ||
482
+ agent.getIdentifier() === 'gemini-cli' ||
483
+ agent.getIdentifier() === 'amazon-q-cli' ||
484
+ agent.getIdentifier() === 'crush') {
485
+ (0, constants_1.logVerbose)(`Skipping external MCP config for ${agent.getName()} - handled internally by agent`, verbose);
486
+ return;
487
+ }
488
+ return await applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, cliMcpStrategy, dryRun, verbose, backup);
489
+ }
490
+ async function applyOpenHandsMcpConfiguration(filteredMcpJson, dest, dryRun, verbose, backup = true) {
491
+ if (dryRun) {
492
+ (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config by updating TOML file: ${dest}`, verbose);
493
+ }
494
+ else {
495
+ await (0, propagateOpenHandsMcp_1.propagateMcpToOpenHands)(filteredMcpJson, dest, backup);
496
+ }
497
+ }
498
+ async function applyOpenCodeMcpConfiguration(filteredMcpJson, dest, dryRun, verbose, backup = true) {
499
+ if (dryRun) {
500
+ (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config by updating OpenCode config file: ${dest}`, verbose);
501
+ }
502
+ else {
503
+ await (0, propagateOpenCodeMcp_1.propagateMcpToOpenCode)(filteredMcpJson, dest, backup);
504
+ }
505
+ }
506
+ /**
507
+ * Transform MCP server types for Claude Code compatibility.
508
+ * Claude expects "http" for HTTP servers and "sse" for SSE servers, not "remote".
509
+ */
510
+ function transformMcpForClaude(mcpJson) {
511
+ if (!mcpJson.mcpServers || typeof mcpJson.mcpServers !== 'object') {
512
+ return mcpJson;
513
+ }
514
+ const transformedMcp = { ...mcpJson };
515
+ const transformedServers = {};
516
+ for (const [name, serverDef] of Object.entries(mcpJson.mcpServers)) {
517
+ if (serverDef && typeof serverDef === 'object') {
518
+ const server = serverDef;
519
+ const transformedServer = { ...server };
520
+ // Transform type: "remote" to appropriate Claude types
521
+ if (server.type === 'remote' &&
522
+ server.url &&
523
+ typeof server.url === 'string') {
524
+ const url = server.url;
525
+ // Check if URL suggests SSE (contains /sse path segment)
526
+ if (/\/sse(\/|$)/i.test(url)) {
527
+ transformedServer.type = 'sse';
528
+ }
529
+ else {
530
+ transformedServer.type = 'http';
531
+ }
532
+ }
533
+ transformedServers[name] = transformedServer;
534
+ }
535
+ else {
536
+ transformedServers[name] = serverDef;
537
+ }
538
+ }
539
+ transformedMcp.mcpServers = transformedServers;
540
+ return transformedMcp;
541
+ }
542
+ /**
543
+ * Transform MCP server types for Kilo Code compatibility.
544
+ * Kilo Code expects "streamable-http" for remote HTTP servers, not "remote".
545
+ */
546
+ function transformMcpForKiloCode(mcpJson) {
547
+ if (!mcpJson.mcpServers || typeof mcpJson.mcpServers !== 'object') {
548
+ return mcpJson;
549
+ }
550
+ const transformedMcp = { ...mcpJson };
551
+ const transformedServers = {};
552
+ for (const [name, serverDef] of Object.entries(mcpJson.mcpServers)) {
553
+ if (serverDef && typeof serverDef === 'object') {
554
+ const server = serverDef;
555
+ const transformedServer = { ...server };
556
+ // Transform type: "remote" to "streamable-http" for HTTP-based servers
557
+ if (server.type === 'remote' &&
558
+ server.url &&
559
+ typeof server.url === 'string') {
560
+ transformedServer.type = 'streamable-http';
561
+ }
562
+ transformedServers[name] = transformedServer;
563
+ }
564
+ else {
565
+ transformedServers[name] = serverDef;
566
+ }
567
+ }
568
+ transformedMcp.mcpServers = transformedServers;
569
+ return transformedMcp;
570
+ }
571
+ async function applyStandardMcpConfiguration(agent, filteredMcpJson, dest, agentConfig, config, cliMcpStrategy, dryRun, verbose, backup = true) {
572
+ const strategy = cliMcpStrategy ??
573
+ agentConfig?.mcp?.strategy ??
574
+ config.mcp?.strategy ??
575
+ 'merge';
576
+ const serverKey = agent.getMcpServerKey?.() ?? 'mcpServers';
577
+ // Skip agents with empty server keys (e.g., AgentsMdAgent, GooseAgent)
578
+ if (serverKey === '') {
579
+ (0, constants_1.logVerbose)(`Skipping MCP config for ${agent.getName()} - agent has empty server key`, verbose);
580
+ return;
581
+ }
582
+ (0, constants_1.logVerbose)(`Applying filtered MCP config for ${agent.getName()} with strategy: ${strategy} and key: ${serverKey}`, verbose);
583
+ if (dryRun) {
584
+ (0, constants_1.logVerbose)(`DRY RUN: Would apply MCP config to: ${dest}`, verbose);
585
+ }
586
+ else {
587
+ // Transform MCP config for agent-specific compatibility
588
+ let mcpToMerge = filteredMcpJson;
589
+ if (agent.getIdentifier() === 'claude') {
590
+ mcpToMerge = transformMcpForClaude(filteredMcpJson);
591
+ }
592
+ else if (agent.getIdentifier() === 'kilocode') {
593
+ mcpToMerge = transformMcpForKiloCode(filteredMcpJson);
594
+ }
595
+ const existing = await (0, mcp_1.readNativeMcp)(dest);
596
+ const merged = (0, merge_1.mergeMcp)(existing, mcpToMerge, strategy, serverKey);
597
+ // Firebase Studio (IDX) expects no "type" fields in .idx/mcp.json server entries.
598
+ // Sanitize merged config by stripping 'type' from each server when targeting Firebase.
599
+ const sanitizeForFirebase = (obj) => {
600
+ if (agent.getIdentifier() !== 'firebase')
601
+ return obj;
602
+ const out = { ...obj };
603
+ const servers = out[serverKey] || {};
604
+ const cleanedServers = {};
605
+ for (const [name, def] of Object.entries(servers)) {
606
+ if (def && typeof def === 'object') {
607
+ const copy = { ...def };
608
+ delete copy.type;
609
+ cleanedServers[name] = copy;
610
+ }
611
+ else {
612
+ cleanedServers[name] = def;
613
+ }
614
+ }
615
+ out[serverKey] = cleanedServers;
616
+ return out;
617
+ };
618
+ const toWrite = sanitizeForFirebase(merged);
619
+ // Only backup and write if content would actually change (idempotent)
620
+ const currentContent = JSON.stringify(existing, null, 2);
621
+ const newContent = JSON.stringify(toWrite, null, 2);
622
+ if (currentContent !== newContent) {
623
+ if (backup) {
624
+ const { backupFile } = await Promise.resolve().then(() => __importStar(require('../core/FileSystemUtils')));
625
+ await backupFile(dest);
626
+ }
627
+ await (0, mcp_1.writeNativeMcp)(dest, toWrite);
628
+ }
629
+ else {
630
+ (0, constants_1.logVerbose)(`MCP config for ${agent.getName()} is already up to date - skipping backup and write`, verbose);
631
+ }
632
+ }
633
+ }
634
+ /**
635
+ * Updates the .gitignore file with generated paths.
636
+ * @param projectRoot Root directory of the project
637
+ * @param generatedPaths Array of generated file paths
638
+ * @param config Loaded configuration
639
+ * @param cliGitignoreEnabled CLI gitignore setting
640
+ * @param dryRun Whether to perform a dry run
641
+ */
642
+ async function updateGitignore(projectRoot, generatedPaths, config, cliGitignoreEnabled, dryRun) {
643
+ // Configuration precedence: CLI > TOML > Default (enabled)
644
+ let gitignoreEnabled;
645
+ if (cliGitignoreEnabled !== undefined) {
646
+ gitignoreEnabled = cliGitignoreEnabled;
647
+ }
648
+ else if (config.gitignore?.enabled !== undefined) {
649
+ gitignoreEnabled = config.gitignore.enabled;
650
+ }
651
+ else {
652
+ gitignoreEnabled = true; // Default enabled
653
+ }
654
+ if (gitignoreEnabled && generatedPaths.length > 0) {
655
+ const uniquePaths = [...new Set(generatedPaths)];
656
+ // Note: Individual backup patterns are added per-file in the collection phase
657
+ // No need to add a broad *.bak pattern here
658
+ if (uniquePaths.length > 0) {
659
+ if (dryRun) {
660
+ (0, constants_1.logInfo)(`Would update .gitignore with ${uniquePaths.length} unique path(s): ${uniquePaths.join(', ')}`, dryRun);
661
+ }
662
+ else {
663
+ await (0, GitignoreUtils_1.updateGitignore)(projectRoot, uniquePaths);
664
+ (0, constants_1.logInfo)(`Updated .gitignore with ${uniquePaths.length} unique path(s) in the Ruler block.`, dryRun);
665
+ }
666
+ }
667
+ }
668
+ }