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.
- package/LICENSE +21 -0
- package/README.md +989 -0
- package/dist/agents/AbstractAgent.js +92 -0
- package/dist/agents/AgentsMdAgent.js +85 -0
- package/dist/agents/AiderAgent.js +108 -0
- package/dist/agents/AmazonQCliAgent.js +103 -0
- package/dist/agents/AmpAgent.js +13 -0
- package/dist/agents/AugmentCodeAgent.js +70 -0
- package/dist/agents/ClaudeAgent.js +95 -0
- package/dist/agents/ClineAgent.js +53 -0
- package/dist/agents/CodexCliAgent.js +143 -0
- package/dist/agents/CopilotAgent.js +43 -0
- package/dist/agents/CrushAgent.js +128 -0
- package/dist/agents/CursorAgent.js +93 -0
- package/dist/agents/FirebaseAgent.js +61 -0
- package/dist/agents/FirebenderAgent.js +205 -0
- package/dist/agents/GeminiCliAgent.js +99 -0
- package/dist/agents/GooseAgent.js +58 -0
- package/dist/agents/IAgent.js +2 -0
- package/dist/agents/JulesAgent.js +14 -0
- package/dist/agents/JunieAgent.js +53 -0
- package/dist/agents/KiloCodeAgent.js +63 -0
- package/dist/agents/KiroAgent.js +50 -0
- package/dist/agents/OpenCodeAgent.js +99 -0
- package/dist/agents/OpenHandsAgent.js +56 -0
- package/dist/agents/QwenCodeAgent.js +82 -0
- package/dist/agents/RooCodeAgent.js +139 -0
- package/dist/agents/TraeAgent.js +54 -0
- package/dist/agents/WarpAgent.js +61 -0
- package/dist/agents/WindsurfAgent.js +27 -0
- package/dist/agents/ZedAgent.js +132 -0
- package/dist/agents/agent-utils.js +37 -0
- package/dist/agents/index.js +77 -0
- package/dist/cli/commands.js +136 -0
- package/dist/cli/handlers.js +221 -0
- package/dist/cli/index.js +5 -0
- package/dist/constants.js +58 -0
- package/dist/core/ConfigLoader.js +274 -0
- package/dist/core/FileSystemUtils.js +421 -0
- package/dist/core/FrontmatterParser.js +142 -0
- package/dist/core/GitignoreUtils.js +171 -0
- package/dist/core/RuleProcessor.js +60 -0
- package/dist/core/SkillsProcessor.js +528 -0
- package/dist/core/SkillsUtils.js +230 -0
- package/dist/core/UnifiedConfigLoader.js +432 -0
- package/dist/core/UnifiedConfigTypes.js +2 -0
- package/dist/core/agent-selection.js +52 -0
- package/dist/core/apply-engine.js +668 -0
- package/dist/core/config-utils.js +30 -0
- package/dist/core/hash.js +24 -0
- package/dist/core/revert-engine.js +413 -0
- package/dist/lib.js +196 -0
- package/dist/mcp/capabilities.js +65 -0
- package/dist/mcp/merge.js +39 -0
- package/dist/mcp/propagateOpenCodeMcp.js +116 -0
- package/dist/mcp/propagateOpenHandsMcp.js +169 -0
- package/dist/mcp/validate.js +17 -0
- package/dist/paths/mcp.js +120 -0
- package/dist/revert.js +186 -0
- package/dist/types.js +2 -0
- package/dist/vscode/settings.js +117 -0
- 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
|
+
}
|