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,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
|
+
}
|