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,230 @@
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.hasSkillMd = hasSkillMd;
37
+ exports.isGroupingDir = isGroupingDir;
38
+ exports.walkSkillsTree = walkSkillsTree;
39
+ exports.formatValidationWarnings = formatValidationWarnings;
40
+ exports.copySkillsDirectory = copySkillsDirectory;
41
+ exports.copySkillsDirectoryWithTransform = copySkillsDirectoryWithTransform;
42
+ const path = __importStar(require("path"));
43
+ const fs = __importStar(require("fs/promises"));
44
+ const constants_1 = require("../constants");
45
+ /**
46
+ * Checks if a directory contains a SKILL.md file.
47
+ */
48
+ async function hasSkillMd(dirPath) {
49
+ try {
50
+ const skillMdPath = path.join(dirPath, constants_1.SKILL_MD_FILENAME);
51
+ await fs.access(skillMdPath);
52
+ return true;
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
58
+ /**
59
+ * Checks if a directory is a grouping directory (contains subdirectories with SKILL.md).
60
+ */
61
+ async function isGroupingDir(dirPath) {
62
+ try {
63
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
64
+ const subdirs = entries.filter((e) => e.isDirectory());
65
+ for (const subdir of subdirs) {
66
+ const subdirPath = path.join(dirPath, subdir.name);
67
+ if (await hasSkillMd(subdirPath)) {
68
+ return true;
69
+ }
70
+ // Check recursively for nested grouping
71
+ if (await isGroupingDir(subdirPath)) {
72
+ return true;
73
+ }
74
+ }
75
+ return false;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
81
+ /**
82
+ * Walks the skills tree and discovers all skills.
83
+ * Returns skills and any validation warnings.
84
+ */
85
+ async function walkSkillsTree(root) {
86
+ const skills = [];
87
+ const warnings = [];
88
+ async function walk(currentPath, relativePath) {
89
+ try {
90
+ const entries = await fs.readdir(currentPath, { withFileTypes: true });
91
+ for (const entry of entries) {
92
+ if (!entry.isDirectory()) {
93
+ continue;
94
+ }
95
+ const entryPath = path.join(currentPath, entry.name);
96
+ const entryRelativePath = relativePath
97
+ ? path.join(relativePath, entry.name)
98
+ : entry.name;
99
+ const hasSkill = await hasSkillMd(entryPath);
100
+ const isGrouping = !hasSkill && (await isGroupingDir(entryPath));
101
+ if (hasSkill) {
102
+ // This is a valid skill directory
103
+ skills.push({
104
+ name: entry.name,
105
+ path: entryPath,
106
+ hasSkillMd: true,
107
+ valid: true,
108
+ });
109
+ }
110
+ else if (isGrouping) {
111
+ // This is a grouping directory, recurse into it
112
+ await walk(entryPath, entryRelativePath);
113
+ }
114
+ else {
115
+ // This is neither a skill nor a grouping directory - warn about it
116
+ warnings.push(`Directory '${entryRelativePath}' in .ruler/skills has no SKILL.md and contains no sub-skills. It may be malformed or stray.`);
117
+ }
118
+ }
119
+ }
120
+ catch (err) {
121
+ // If we can't read the directory, just return what we have
122
+ warnings.push(`Failed to read directory ${relativePath || 'root'}: ${err.message}`);
123
+ }
124
+ }
125
+ await walk(root, '');
126
+ return { skills, warnings };
127
+ }
128
+ /**
129
+ * Formats validation warnings for display.
130
+ */
131
+ function formatValidationWarnings(warnings) {
132
+ if (warnings.length === 0) {
133
+ return '';
134
+ }
135
+ return warnings.map((w) => ` - ${w}`).join('\n');
136
+ }
137
+ /**
138
+ * Recursively copies a directory and all its contents.
139
+ */
140
+ async function copyRecursive(src, dest) {
141
+ const stat = await fs.stat(src);
142
+ if (stat.isDirectory()) {
143
+ await fs.mkdir(dest, { recursive: true });
144
+ const entries = await fs.readdir(src, { withFileTypes: true });
145
+ for (const entry of entries) {
146
+ const srcPath = path.join(src, entry.name);
147
+ const destPath = path.join(dest, entry.name);
148
+ await copyRecursive(srcPath, destPath);
149
+ }
150
+ }
151
+ else {
152
+ await fs.copyFile(src, dest);
153
+ }
154
+ }
155
+ /**
156
+ * Copies the skills directory to the destination, preserving structure.
157
+ * Creates the destination directory if it doesn't exist.
158
+ */
159
+ async function copySkillsDirectory(srcDir, destDir) {
160
+ await fs.mkdir(destDir, { recursive: true });
161
+ await copyRecursive(srcDir, destDir);
162
+ }
163
+ /**
164
+ * Recursively copies and transforms skills by expanding @filename references.
165
+ * Transforms SKILL.md files to replace @filename with actual file content.
166
+ * This is needed for MCP agents that don't support Claude Code's @filename syntax.
167
+ */
168
+ async function copyAndTransformSkills(src, dest, projectRoot) {
169
+ const stat = await fs.stat(src);
170
+ if (stat.isDirectory()) {
171
+ await fs.mkdir(dest, { recursive: true });
172
+ const entries = await fs.readdir(src, { withFileTypes: true });
173
+ for (const entry of entries) {
174
+ const srcPath = path.join(src, entry.name);
175
+ const destPath = path.join(dest, entry.name);
176
+ await copyAndTransformSkills(srcPath, destPath, projectRoot);
177
+ }
178
+ }
179
+ else {
180
+ // Check if this is a SKILL.md file that needs transformation
181
+ if (path.basename(src) === constants_1.SKILL_MD_FILENAME) {
182
+ const content = await fs.readFile(src, 'utf8');
183
+ const transformed = await expandAtFilenameReferences(content, projectRoot);
184
+ await fs.writeFile(dest, transformed, 'utf8');
185
+ }
186
+ else {
187
+ // Copy other files as-is
188
+ await fs.copyFile(src, dest);
189
+ }
190
+ }
191
+ }
192
+ /**
193
+ * Expands @filename references in skill content by replacing them with actual file content.
194
+ * Strips frontmatter from referenced files to avoid duplication.
195
+ */
196
+ async function expandAtFilenameReferences(content, projectRoot) {
197
+ const { parseFrontmatter } = await Promise.resolve().then(() => __importStar(require('./FrontmatterParser')));
198
+ // Match @filename patterns (e.g., @.claude/rules/foo.mdc or @./relative/path)
199
+ const atFilenamePattern = /@([^\s]+)/g;
200
+ let transformed = content;
201
+ const matches = Array.from(content.matchAll(atFilenamePattern));
202
+ for (const match of matches) {
203
+ const fileReference = match[0]; // e.g., "@.claude/rules/foo.mdc"
204
+ const filePath = match[1]; // e.g., ".claude/rules/foo.mdc"
205
+ try {
206
+ // Resolve path relative to project root
207
+ const absolutePath = path.resolve(projectRoot, filePath);
208
+ const fileContent = await fs.readFile(absolutePath, 'utf8');
209
+ // Parse and strip frontmatter from the referenced file
210
+ // This prevents duplicate frontmatter in the final skill
211
+ const { body } = parseFrontmatter(fileContent);
212
+ // Replace the @filename reference with the content (without frontmatter)
213
+ transformed = transformed.replace(fileReference, body);
214
+ }
215
+ catch {
216
+ // If file can't be read, leave the reference as-is
217
+ // This allows graceful degradation
218
+ }
219
+ }
220
+ return transformed;
221
+ }
222
+ /**
223
+ * Copies skills directory with transformation for MCP agents.
224
+ * Expands @filename references to actual file content since MCP agents
225
+ * don't support Claude Code's @filename syntax.
226
+ */
227
+ async function copySkillsDirectoryWithTransform(srcDir, destDir, projectRoot) {
228
+ await fs.mkdir(destDir, { recursive: true });
229
+ await copyAndTransformSkills(srcDir, destDir, projectRoot);
230
+ }
@@ -0,0 +1,432 @@
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.loadUnifiedConfig = loadUnifiedConfig;
37
+ const fs_1 = require("fs");
38
+ const path = __importStar(require("path"));
39
+ const toml_1 = require("@iarna/toml");
40
+ const hash_1 = require("./hash");
41
+ const RuleProcessor_1 = require("./RuleProcessor");
42
+ const FileSystemUtils = __importStar(require("./FileSystemUtils"));
43
+ const FileSystemUtils_1 = require("./FileSystemUtils");
44
+ async function loadUnifiedConfig(options) {
45
+ // Resolve the effective .ruler directory (local or global), mirroring the main loader behavior
46
+ const resolvedRulerDir = (await FileSystemUtils.findRulerDir(options.projectRoot, true)) ||
47
+ path.join(options.projectRoot, '.ruler');
48
+ const meta = {
49
+ projectRoot: options.projectRoot,
50
+ rulerDir: resolvedRulerDir,
51
+ loadedAt: new Date(),
52
+ version: '0.0.0-dev',
53
+ };
54
+ const diagnostics = [];
55
+ // Read TOML if available
56
+ let tomlRaw = {};
57
+ const tomlFile = options.configPath
58
+ ? path.resolve(options.configPath)
59
+ : path.join(meta.rulerDir, 'ruler.toml');
60
+ try {
61
+ const text = await fs_1.promises.readFile(tomlFile, 'utf8');
62
+ tomlRaw = text.trim() ? (0, toml_1.parse)(text) : {};
63
+ meta.configFile = tomlFile;
64
+ }
65
+ catch (err) {
66
+ if (err.code !== 'ENOENT') {
67
+ diagnostics.push({
68
+ severity: 'warning',
69
+ code: 'TOML_READ_ERROR',
70
+ message: 'Failed to read ruler.toml',
71
+ file: tomlFile,
72
+ detail: err.message,
73
+ });
74
+ }
75
+ }
76
+ let defaultAgents;
77
+ if (tomlRaw &&
78
+ typeof tomlRaw === 'object' &&
79
+ tomlRaw.default_agents &&
80
+ Array.isArray(tomlRaw.default_agents)) {
81
+ defaultAgents = tomlRaw.default_agents.map((a) => String(a));
82
+ }
83
+ let nested = false;
84
+ if (tomlRaw &&
85
+ typeof tomlRaw === 'object' &&
86
+ typeof tomlRaw.nested === 'boolean') {
87
+ nested = tomlRaw.nested;
88
+ }
89
+ // Parse skills configuration
90
+ let skillsConfig;
91
+ if (tomlRaw && typeof tomlRaw === 'object') {
92
+ const skillsSection = tomlRaw.skills;
93
+ if (skillsSection && typeof skillsSection === 'object') {
94
+ const skillsObj = skillsSection;
95
+ skillsConfig = {};
96
+ if (typeof skillsObj.enabled === 'boolean') {
97
+ skillsConfig.enabled = skillsObj.enabled;
98
+ }
99
+ if (typeof skillsObj.generate_from_rules === 'boolean') {
100
+ skillsConfig.generate_from_rules = skillsObj.generate_from_rules;
101
+ }
102
+ }
103
+ }
104
+ // Parse rules configuration
105
+ let rulesInclude;
106
+ let rulesExclude;
107
+ if (tomlRaw && typeof tomlRaw === 'object') {
108
+ const rulesSection = tomlRaw.rules;
109
+ if (rulesSection && typeof rulesSection === 'object') {
110
+ const rulesObj = rulesSection;
111
+ if (Array.isArray(rulesObj.include)) {
112
+ rulesInclude = rulesObj.include.map((p) => String(p));
113
+ }
114
+ if (Array.isArray(rulesObj.exclude)) {
115
+ rulesExclude = rulesObj.exclude.map((p) => String(p));
116
+ }
117
+ // Note: merge_strategy is handled in ConfigLoader.ts for single configs
118
+ }
119
+ }
120
+ const toml = {
121
+ raw: tomlRaw,
122
+ schemaVersion: 1,
123
+ agents: {},
124
+ defaultAgents,
125
+ nested,
126
+ skills: skillsConfig,
127
+ };
128
+ // Collect rule markdown files
129
+ let ruleFiles = [];
130
+ try {
131
+ const dirEntries = await fs_1.promises.readdir(meta.rulerDir, { withFileTypes: true });
132
+ let mdFiles = dirEntries
133
+ .filter((e) => e.isFile() &&
134
+ (e.name.toLowerCase().endsWith('.md') ||
135
+ e.name.toLowerCase().endsWith('.mdc')))
136
+ .map((e) => path.join(meta.rulerDir, e.name));
137
+ // Apply include/exclude filters
138
+ if (rulesInclude || rulesExclude) {
139
+ // Normalize patterns (expand directory patterns to globs)
140
+ const normalizedInclude = rulesInclude?.map(FileSystemUtils_1.normalizePattern);
141
+ const normalizedExclude = rulesExclude?.map(FileSystemUtils_1.normalizePattern);
142
+ mdFiles = mdFiles.filter((file) => {
143
+ // Get relative path from rulerDir for pattern matching
144
+ const relativePath = path.relative(meta.rulerDir, file);
145
+ // Normalize to forward slashes for consistent pattern matching
146
+ const normalizedPath = relativePath.replace(/\\/g, '/');
147
+ // Check exclude patterns first (they take precedence)
148
+ if (normalizedExclude) {
149
+ for (const pattern of normalizedExclude) {
150
+ if ((0, FileSystemUtils_1.matchesPattern)(normalizedPath, pattern)) {
151
+ return false; // Exclude this file
152
+ }
153
+ }
154
+ }
155
+ // If include patterns are specified, file must match at least one
156
+ if (normalizedInclude && normalizedInclude.length > 0) {
157
+ for (const pattern of normalizedInclude) {
158
+ if ((0, FileSystemUtils_1.matchesPattern)(normalizedPath, pattern)) {
159
+ return true; // Include this file
160
+ }
161
+ }
162
+ return false; // No include pattern matched
163
+ }
164
+ // No include patterns specified, file passed exclude check
165
+ return true;
166
+ });
167
+ }
168
+ // Sort lexicographically then ensure AGENTS.md first
169
+ mdFiles.sort((a, b) => a.localeCompare(b));
170
+ mdFiles.sort((a, b) => {
171
+ const aIs = /agents\.md$/i.test(a);
172
+ const bIs = /agents\.md$/i.test(b);
173
+ if (aIs && !bIs)
174
+ return -1;
175
+ if (bIs && !aIs)
176
+ return 1;
177
+ return 0;
178
+ });
179
+ let order = 0;
180
+ ruleFiles = await Promise.all(mdFiles.map(async (file) => {
181
+ const content = await fs_1.promises.readFile(file, 'utf8');
182
+ const stat = await fs_1.promises.stat(file);
183
+ return {
184
+ path: file,
185
+ relativePath: path.basename(file),
186
+ content,
187
+ contentHash: (0, hash_1.sha256)(content),
188
+ mtimeMs: stat.mtimeMs,
189
+ size: stat.size,
190
+ order: order++,
191
+ primary: /agents\.md$/i.test(file),
192
+ };
193
+ }));
194
+ }
195
+ catch (err) {
196
+ diagnostics.push({
197
+ severity: 'warning',
198
+ code: 'RULES_READ_ERROR',
199
+ message: 'Failed reading rule files',
200
+ file: meta.rulerDir,
201
+ detail: err.message,
202
+ });
203
+ }
204
+ const concatenated = (0, RuleProcessor_1.concatenateRules)(ruleFiles.map((f) => ({ path: f.path, content: f.content })), path.dirname(meta.rulerDir));
205
+ const rules = {
206
+ files: ruleFiles,
207
+ concatenated,
208
+ concatenatedHash: (0, hash_1.sha256)(concatenated),
209
+ };
210
+ // Parse TOML MCP servers
211
+ const tomlMcpServers = {};
212
+ if (tomlRaw && typeof tomlRaw === 'object') {
213
+ const tomlObj = tomlRaw;
214
+ if (tomlObj.mcp_servers && typeof tomlObj.mcp_servers === 'object') {
215
+ const mcpServersRaw = tomlObj.mcp_servers;
216
+ for (const [name, def] of Object.entries(mcpServersRaw)) {
217
+ if (!def || typeof def !== 'object')
218
+ continue;
219
+ const serverDef = def;
220
+ const server = {};
221
+ // Parse command and args
222
+ if (typeof serverDef.command === 'string') {
223
+ server.command = serverDef.command;
224
+ }
225
+ if (Array.isArray(serverDef.args)) {
226
+ server.args = serverDef.args.map(String);
227
+ }
228
+ // Parse env
229
+ if (serverDef.env && typeof serverDef.env === 'object') {
230
+ server.env = Object.fromEntries(Object.entries(serverDef.env).filter(([, v]) => typeof v === 'string'));
231
+ }
232
+ // Parse URL and headers
233
+ if (typeof serverDef.url === 'string') {
234
+ server.url = serverDef.url;
235
+ }
236
+ if (serverDef.headers && typeof serverDef.headers === 'object') {
237
+ server.headers = Object.fromEntries(Object.entries(serverDef.headers).filter(([, v]) => typeof v === 'string'));
238
+ }
239
+ // Validate server configuration
240
+ const hasCommand = !!server.command;
241
+ const hasUrl = !!server.url;
242
+ if (!hasCommand && !hasUrl) {
243
+ diagnostics.push({
244
+ severity: 'warning',
245
+ code: 'MCP_TOML_INVALID_SERVER',
246
+ message: `MCP server '${name}' must have at least one of command or url`,
247
+ file: tomlFile,
248
+ });
249
+ continue;
250
+ }
251
+ if (hasCommand && hasUrl) {
252
+ diagnostics.push({
253
+ severity: 'warning',
254
+ code: 'MCP_TOML_FIELD_CONFLICT',
255
+ message: `MCP server '${name}' has both command and url - using url (remote)`,
256
+ file: tomlFile,
257
+ });
258
+ }
259
+ if (hasCommand && server.headers) {
260
+ diagnostics.push({
261
+ severity: 'warning',
262
+ code: 'MCP_TOML_FIELD_CONFLICT',
263
+ message: `MCP server '${name}' has headers with command (should be used with url only)`,
264
+ file: tomlFile,
265
+ });
266
+ }
267
+ if (hasUrl && server.env) {
268
+ diagnostics.push({
269
+ severity: 'warning',
270
+ code: 'MCP_TOML_FIELD_CONFLICT',
271
+ message: `MCP server '${name}' has env with url (should be used with command only)`,
272
+ file: tomlFile,
273
+ });
274
+ }
275
+ // Derive type - remote takes precedence if both are present
276
+ if (server.url) {
277
+ server.type = 'remote';
278
+ }
279
+ else if (server.command) {
280
+ server.type = 'stdio';
281
+ }
282
+ tomlMcpServers[name] = server;
283
+ }
284
+ }
285
+ }
286
+ // Store TOML MCP servers in toml config
287
+ toml.mcpServers = tomlMcpServers;
288
+ // MCP normalization - merge JSON and TOML
289
+ let mcp = null;
290
+ const mcpFile = path.join(meta.rulerDir, 'mcp.json');
291
+ const jsonMcpServers = {};
292
+ let mcpJsonExists = false;
293
+ // Pre-flight existence check so users see warning even if JSON invalid
294
+ try {
295
+ await fs_1.promises.access(mcpFile);
296
+ mcpJsonExists = true;
297
+ // Warning is handled by apply-engine to avoid duplication
298
+ }
299
+ catch {
300
+ // file not present
301
+ }
302
+ // Add deprecation warning if mcp.json exists (regardless of validity)
303
+ if (mcpJsonExists) {
304
+ meta.mcpFile = mcpFile;
305
+ diagnostics.push({
306
+ severity: 'warning',
307
+ code: 'MCP_JSON_DEPRECATED',
308
+ message: 'mcp.json detected: please migrate MCP servers to ruler.toml [mcp_servers.*] sections',
309
+ file: mcpFile,
310
+ });
311
+ }
312
+ try {
313
+ if (mcpJsonExists) {
314
+ const raw = await fs_1.promises.readFile(mcpFile, 'utf8');
315
+ let parsed;
316
+ try {
317
+ parsed = JSON.parse(raw);
318
+ }
319
+ catch (e) {
320
+ // Lenient fallback: strip comments and trailing commas then retry
321
+ const stripped = raw
322
+ // strip /* */ comments
323
+ .replace(/\/\*[\s\S]*?\*\//g, '')
324
+ // strip // comments
325
+ .replace(/(^|\s+)\/\/.*$/gm, '$1')
326
+ // remove trailing commas before } or ]
327
+ .replace(/,\s*([}\]])/g, '$1');
328
+ try {
329
+ parsed = JSON.parse(stripped);
330
+ }
331
+ catch {
332
+ throw e; // rethrow original error for diagnostics
333
+ }
334
+ }
335
+ const parsedObj = parsed;
336
+ const serversRaw = parsedObj.mcpServers ||
337
+ parsedObj.servers ||
338
+ {};
339
+ if (serversRaw && typeof serversRaw === 'object') {
340
+ for (const [name, def] of Object.entries(serversRaw)) {
341
+ if (!def || typeof def !== 'object')
342
+ continue;
343
+ const server = {};
344
+ if (typeof def.command === 'string')
345
+ server.command = def.command;
346
+ if (Array.isArray(def.command))
347
+ server.command = def.command[0];
348
+ if (Array.isArray(def.args))
349
+ server.args = def.args.map(String);
350
+ if (def.env && typeof def.env === 'object') {
351
+ server.env = Object.fromEntries(Object.entries(def.env).filter(([, v]) => typeof v === 'string'));
352
+ }
353
+ if (typeof def.url === 'string')
354
+ server.url = def.url;
355
+ if (def.headers && typeof def.headers === 'object') {
356
+ server.headers = Object.fromEntries(Object.entries(def.headers).filter(([, v]) => typeof v === 'string'));
357
+ }
358
+ // Derive type
359
+ if (server.url)
360
+ server.type = 'remote';
361
+ else if (server.command)
362
+ server.type = 'stdio';
363
+ jsonMcpServers[name] = server;
364
+ }
365
+ }
366
+ }
367
+ }
368
+ catch (err) {
369
+ if (mcpJsonExists) {
370
+ diagnostics.push({
371
+ severity: 'warning',
372
+ code: 'MCP_READ_ERROR',
373
+ message: 'Failed to read mcp.json',
374
+ file: mcpFile,
375
+ detail: err.message,
376
+ });
377
+ }
378
+ }
379
+ // Merge servers: start with JSON, overlay TOML (TOML wins per server name)
380
+ const mergedServers = { ...jsonMcpServers, ...tomlMcpServers };
381
+ // Create MCP bundle if we have any servers
382
+ if (Object.keys(mergedServers).length > 0 || mcpJsonExists) {
383
+ mcp = {
384
+ servers: mergedServers,
385
+ raw: mcpJsonExists ? { mcpServers: jsonMcpServers } : {},
386
+ hash: (0, hash_1.sha256)((0, hash_1.stableJson)(mergedServers)),
387
+ };
388
+ }
389
+ const config = {
390
+ meta,
391
+ toml,
392
+ rules,
393
+ mcp,
394
+ agents: {},
395
+ diagnostics,
396
+ hash: '', // placeholder, recompute after agents
397
+ };
398
+ // Agent resolution (basic): enabled set is CLI override or default_agents
399
+ const cliAgents = options.cliAgents && options.cliAgents.length > 0
400
+ ? options.cliAgents
401
+ : undefined;
402
+ const enabledList = cliAgents ?? toml.defaultAgents ?? [];
403
+ for (const name of enabledList) {
404
+ config.agents[name] = {
405
+ identifier: name,
406
+ enabled: true,
407
+ output: {},
408
+ mcp: { enabled: false, strategy: 'merge' },
409
+ };
410
+ }
411
+ // If CLI provided, mark defaults not included as disabled (optional design choice)
412
+ if (cliAgents) {
413
+ for (const name of toml.defaultAgents ?? []) {
414
+ if (!config.agents[name]) {
415
+ config.agents[name] = {
416
+ identifier: name,
417
+ enabled: false,
418
+ output: {},
419
+ mcp: { enabled: false, strategy: 'merge' },
420
+ };
421
+ }
422
+ }
423
+ }
424
+ // Recompute hash including agents list
425
+ config.hash = (0, hash_1.sha256)((0, hash_1.stableJson)({
426
+ toml: toml.defaultAgents,
427
+ rules: rules.concatenatedHash,
428
+ mcp: mcp ? mcp.hash : null,
429
+ agents: Object.entries(config.agents).map(([k, v]) => [k, v.enabled]),
430
+ }));
431
+ return config;
432
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });