rulix 0.1.0
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 +237 -0
- package/dist/chunk-I6MHHT6P.cjs +1235 -0
- package/dist/chunk-I6MHHT6P.cjs.map +1 -0
- package/dist/chunk-IX4ZOAKV.js +1235 -0
- package/dist/chunk-IX4ZOAKV.js.map +1 -0
- package/dist/cli/index.cjs +417 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +417 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.cjs +91 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +298 -0
- package/dist/index.d.ts +298 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,1235 @@
|
|
|
1
|
+
// src/adapters/agents-md.ts
|
|
2
|
+
import { access, writeFile } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
var AGENTS_MD = "AGENTS.md";
|
|
5
|
+
var GENERATED_HEADER = "<!-- Generated by Rulix. Do not edit directly. -->\n\n";
|
|
6
|
+
var CATEGORY_LABELS = {
|
|
7
|
+
style: "Style",
|
|
8
|
+
security: "Security",
|
|
9
|
+
testing: "Testing",
|
|
10
|
+
architecture: "Architecture",
|
|
11
|
+
workflow: "Workflow",
|
|
12
|
+
general: "General"
|
|
13
|
+
};
|
|
14
|
+
var CATEGORY_ORDER = [
|
|
15
|
+
"architecture",
|
|
16
|
+
"style",
|
|
17
|
+
"security",
|
|
18
|
+
"testing",
|
|
19
|
+
"workflow",
|
|
20
|
+
"general"
|
|
21
|
+
];
|
|
22
|
+
async function pathExists(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
await access(filePath);
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function groupByCategory(rules) {
|
|
31
|
+
const groups = /* @__PURE__ */ new Map();
|
|
32
|
+
for (const rule of rules) {
|
|
33
|
+
const existing = groups.get(rule.category);
|
|
34
|
+
if (existing) {
|
|
35
|
+
existing.push(rule);
|
|
36
|
+
} else {
|
|
37
|
+
groups.set(rule.category, [rule]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return groups;
|
|
41
|
+
}
|
|
42
|
+
function buildCategorySection(category, rules) {
|
|
43
|
+
const sorted = [...rules].sort((a, b) => a.priority - b.priority);
|
|
44
|
+
const label = CATEGORY_LABELS[category];
|
|
45
|
+
const sections = sorted.map((r) => `### ${r.description}
|
|
46
|
+
|
|
47
|
+
${r.content}`);
|
|
48
|
+
return `## ${label}
|
|
49
|
+
|
|
50
|
+
${sections.join("\n\n")}`;
|
|
51
|
+
}
|
|
52
|
+
function buildAgentsMdContent(rules, includeHeader) {
|
|
53
|
+
const exportable = rules.filter(
|
|
54
|
+
(r) => r.scope === "always" || r.scope === "file-scoped"
|
|
55
|
+
);
|
|
56
|
+
if (exportable.length === 0) return "";
|
|
57
|
+
const groups = groupByCategory(exportable);
|
|
58
|
+
const sections = [];
|
|
59
|
+
for (const category of CATEGORY_ORDER) {
|
|
60
|
+
const categoryRules = groups.get(category);
|
|
61
|
+
if (categoryRules && categoryRules.length > 0) {
|
|
62
|
+
sections.push(buildCategorySection(category, categoryRules));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const header = includeHeader ? GENERATED_HEADER : "";
|
|
66
|
+
return `${header}# AGENTS.md
|
|
67
|
+
|
|
68
|
+
${sections.join("\n\n")}
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
var agentsMdAdapter = {
|
|
72
|
+
name: "agents-md",
|
|
73
|
+
displayName: "AGENTS.md",
|
|
74
|
+
async detect(projectRoot) {
|
|
75
|
+
return pathExists(join(projectRoot, AGENTS_MD));
|
|
76
|
+
},
|
|
77
|
+
async import(_projectRoot) {
|
|
78
|
+
return { rules: [], warnings: [], source: AGENTS_MD };
|
|
79
|
+
},
|
|
80
|
+
async export(rules, projectRoot, options) {
|
|
81
|
+
const dryRun = options?.dryRun === true;
|
|
82
|
+
const content = buildAgentsMdContent(rules, true);
|
|
83
|
+
if (content === "") {
|
|
84
|
+
return { filesWritten: [], filesDeleted: [], warnings: [] };
|
|
85
|
+
}
|
|
86
|
+
if (!dryRun) {
|
|
87
|
+
await writeFile(join(projectRoot, AGENTS_MD), content, "utf-8");
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
filesWritten: [AGENTS_MD],
|
|
91
|
+
filesDeleted: [],
|
|
92
|
+
warnings: []
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
getTokenBudget() {
|
|
96
|
+
return {
|
|
97
|
+
maxTokens: 32768,
|
|
98
|
+
maxInstructions: 0,
|
|
99
|
+
warningThreshold: 0.8,
|
|
100
|
+
source: "AGENTS.md convention"
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/core/tokenizer.ts
|
|
106
|
+
var CHARS_PER_TOKEN = 4;
|
|
107
|
+
function estimateTokens(text) {
|
|
108
|
+
if (text.length === 0) return 0;
|
|
109
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
110
|
+
}
|
|
111
|
+
function estimateRuleTokens(content, description) {
|
|
112
|
+
return estimateTokens(`${description}
|
|
113
|
+
${content}`);
|
|
114
|
+
}
|
|
115
|
+
function sumTokens(tokenCounts) {
|
|
116
|
+
let total = 0;
|
|
117
|
+
for (const count of tokenCounts) {
|
|
118
|
+
total += count;
|
|
119
|
+
}
|
|
120
|
+
return total;
|
|
121
|
+
}
|
|
122
|
+
function computeBudgetUsage(used, max) {
|
|
123
|
+
const percentage = max === 0 ? 100 : used / max * 100;
|
|
124
|
+
return {
|
|
125
|
+
used,
|
|
126
|
+
max,
|
|
127
|
+
percentage,
|
|
128
|
+
exceeded: used > max
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/adapters/claude-code.ts
|
|
133
|
+
import {
|
|
134
|
+
access as access2,
|
|
135
|
+
mkdir,
|
|
136
|
+
readdir,
|
|
137
|
+
readFile,
|
|
138
|
+
rm,
|
|
139
|
+
writeFile as writeFile2
|
|
140
|
+
} from "fs/promises";
|
|
141
|
+
import { basename, join as join2 } from "path";
|
|
142
|
+
var CLAUDE_MD = "CLAUDE.md";
|
|
143
|
+
var RULES_DIR = ".claude/rules";
|
|
144
|
+
var CONTEXT_PREFIX = "Context: ";
|
|
145
|
+
async function pathExists2(filePath) {
|
|
146
|
+
try {
|
|
147
|
+
await access2(filePath);
|
|
148
|
+
return true;
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function listMdFiles(dir) {
|
|
154
|
+
try {
|
|
155
|
+
const entries = await readdir(dir);
|
|
156
|
+
return entries.filter((f) => f.endsWith(".md")).sort();
|
|
157
|
+
} catch {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function toKebabCase(text) {
|
|
162
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
163
|
+
}
|
|
164
|
+
function stripQuotes(s) {
|
|
165
|
+
const t = s.trim();
|
|
166
|
+
if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) {
|
|
167
|
+
return t.slice(1, -1);
|
|
168
|
+
}
|
|
169
|
+
return t;
|
|
170
|
+
}
|
|
171
|
+
function splitFrontmatter(raw) {
|
|
172
|
+
const lines = raw.replace(/\r\n/g, "\n").split("\n");
|
|
173
|
+
if (lines[0]?.trim() !== "---") return null;
|
|
174
|
+
for (let i = 1; i < lines.length; i++) {
|
|
175
|
+
if (lines[i]?.trim() === "---") {
|
|
176
|
+
return {
|
|
177
|
+
yaml: lines.slice(1, i).join("\n"),
|
|
178
|
+
content: lines.slice(i + 1).join("\n").trim()
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
function parsePaths(yaml) {
|
|
185
|
+
for (const line of yaml.split("\n")) {
|
|
186
|
+
const trimmed = line.trim();
|
|
187
|
+
const colonIdx = trimmed.indexOf(":");
|
|
188
|
+
if (colonIdx === -1) continue;
|
|
189
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
190
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
191
|
+
if (key === "paths" && value !== "") {
|
|
192
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
193
|
+
const inner = value.slice(1, -1).trim();
|
|
194
|
+
return inner === "" ? [] : inner.split(",").map(stripQuotes);
|
|
195
|
+
}
|
|
196
|
+
return [stripQuotes(value)];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return void 0;
|
|
200
|
+
}
|
|
201
|
+
function splitByH2(raw) {
|
|
202
|
+
const parts = raw.split(/^(?=## )/m);
|
|
203
|
+
let preamble = "";
|
|
204
|
+
const sections = [];
|
|
205
|
+
for (const part of parts) {
|
|
206
|
+
if (!part.startsWith("## ")) {
|
|
207
|
+
preamble = part.trim();
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const newlineIdx = part.indexOf("\n");
|
|
211
|
+
if (newlineIdx === -1) {
|
|
212
|
+
sections.push({ heading: part.slice(3).trim(), content: "" });
|
|
213
|
+
} else {
|
|
214
|
+
sections.push({
|
|
215
|
+
heading: part.slice(3, newlineIdx).trim(),
|
|
216
|
+
content: part.slice(newlineIdx + 1).trim()
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return { preamble, sections };
|
|
221
|
+
}
|
|
222
|
+
function createImportedRule(id, scope, description, content, filePath, globs) {
|
|
223
|
+
return {
|
|
224
|
+
id,
|
|
225
|
+
scope,
|
|
226
|
+
description,
|
|
227
|
+
content,
|
|
228
|
+
category: "general",
|
|
229
|
+
priority: 3,
|
|
230
|
+
estimatedTokens: estimateRuleTokens(content, description),
|
|
231
|
+
...globs ? { globs } : {},
|
|
232
|
+
source: {
|
|
233
|
+
adapter: "claude-code",
|
|
234
|
+
filePath,
|
|
235
|
+
importedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function importClaudeMdContent(raw) {
|
|
240
|
+
const content = raw.replace(/\r\n/g, "\n").trim();
|
|
241
|
+
if (content === "") return [];
|
|
242
|
+
const { preamble, sections } = splitByH2(content);
|
|
243
|
+
if (sections.length === 0) {
|
|
244
|
+
if (!preamble) return [];
|
|
245
|
+
return [
|
|
246
|
+
createImportedRule(
|
|
247
|
+
"claude-md",
|
|
248
|
+
"always",
|
|
249
|
+
"CLAUDE.md contents",
|
|
250
|
+
preamble,
|
|
251
|
+
CLAUDE_MD
|
|
252
|
+
)
|
|
253
|
+
];
|
|
254
|
+
}
|
|
255
|
+
const rules = [];
|
|
256
|
+
if (preamble) {
|
|
257
|
+
rules.push(
|
|
258
|
+
createImportedRule(
|
|
259
|
+
"claude-md-preamble",
|
|
260
|
+
"always",
|
|
261
|
+
"CLAUDE.md preamble",
|
|
262
|
+
preamble,
|
|
263
|
+
CLAUDE_MD
|
|
264
|
+
)
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
for (const section of sections) {
|
|
268
|
+
const isContext = section.heading.startsWith(CONTEXT_PREFIX);
|
|
269
|
+
const description = isContext ? section.heading.slice(CONTEXT_PREFIX.length) : section.heading;
|
|
270
|
+
const scope = isContext ? "agent-selected" : "always";
|
|
271
|
+
const id = toKebabCase(description);
|
|
272
|
+
if (id && section.content) {
|
|
273
|
+
rules.push(
|
|
274
|
+
createImportedRule(id, scope, description, section.content, CLAUDE_MD)
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return rules;
|
|
279
|
+
}
|
|
280
|
+
async function importClaudeMd(projectRoot) {
|
|
281
|
+
const filePath = join2(projectRoot, CLAUDE_MD);
|
|
282
|
+
if (!await pathExists2(filePath)) return [];
|
|
283
|
+
const raw = await readFile(filePath, "utf-8");
|
|
284
|
+
return importClaudeMdContent(raw);
|
|
285
|
+
}
|
|
286
|
+
function importClaudeRuleFile(raw, filePath, id) {
|
|
287
|
+
const description = id.replace(/-/g, " ");
|
|
288
|
+
const parts = splitFrontmatter(raw);
|
|
289
|
+
if (!parts) {
|
|
290
|
+
const content = raw.replace(/\r\n/g, "\n").trim();
|
|
291
|
+
return createImportedRule(id, "always", description, content, filePath);
|
|
292
|
+
}
|
|
293
|
+
const paths = parsePaths(parts.yaml);
|
|
294
|
+
const scope = paths && paths.length > 0 ? "file-scoped" : "always";
|
|
295
|
+
const globs = scope === "file-scoped" ? paths : void 0;
|
|
296
|
+
return createImportedRule(
|
|
297
|
+
id,
|
|
298
|
+
scope,
|
|
299
|
+
description,
|
|
300
|
+
parts.content,
|
|
301
|
+
filePath,
|
|
302
|
+
globs
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
async function importClaudeRuleFiles(projectRoot) {
|
|
306
|
+
const dir = join2(projectRoot, RULES_DIR);
|
|
307
|
+
const files = await listMdFiles(dir);
|
|
308
|
+
const rules = [];
|
|
309
|
+
for (const file of files) {
|
|
310
|
+
const filePath = join2(RULES_DIR, file);
|
|
311
|
+
const raw = await readFile(join2(projectRoot, filePath), "utf-8");
|
|
312
|
+
rules.push(importClaudeRuleFile(raw, filePath, basename(file, ".md")));
|
|
313
|
+
}
|
|
314
|
+
return rules;
|
|
315
|
+
}
|
|
316
|
+
function buildClaudeMdContent(rules) {
|
|
317
|
+
const sorted = [...rules].sort((a, b) => a.priority - b.priority);
|
|
318
|
+
const sections = [];
|
|
319
|
+
for (const rule of sorted) {
|
|
320
|
+
const prefix = rule.scope === "agent-selected" ? CONTEXT_PREFIX : "";
|
|
321
|
+
sections.push(`## ${prefix}${rule.description}
|
|
322
|
+
|
|
323
|
+
${rule.content}`);
|
|
324
|
+
}
|
|
325
|
+
return `${sections.join("\n\n")}
|
|
326
|
+
`;
|
|
327
|
+
}
|
|
328
|
+
function buildClaudeRuleFile(rule) {
|
|
329
|
+
if (!rule.globs || rule.globs.length === 0) {
|
|
330
|
+
return `${rule.content}
|
|
331
|
+
`;
|
|
332
|
+
}
|
|
333
|
+
const lines = ["---"];
|
|
334
|
+
if (rule.globs.length === 1) {
|
|
335
|
+
const first = rule.globs[0];
|
|
336
|
+
if (first !== void 0) lines.push(`paths: "${first}"`);
|
|
337
|
+
} else {
|
|
338
|
+
const items = rule.globs.map((g) => `"${g}"`).join(", ");
|
|
339
|
+
lines.push(`paths: [${items}]`);
|
|
340
|
+
}
|
|
341
|
+
lines.push("---");
|
|
342
|
+
lines.push("");
|
|
343
|
+
lines.push(rule.content);
|
|
344
|
+
lines.push("");
|
|
345
|
+
return lines.join("\n");
|
|
346
|
+
}
|
|
347
|
+
async function exportClaudeMd(rules, projectRoot, dryRun) {
|
|
348
|
+
const mdRules = rules.filter(
|
|
349
|
+
(r) => r.scope === "always" || r.scope === "agent-selected"
|
|
350
|
+
);
|
|
351
|
+
if (mdRules.length === 0) return [];
|
|
352
|
+
if (!dryRun) {
|
|
353
|
+
const content = buildClaudeMdContent(mdRules);
|
|
354
|
+
await writeFile2(join2(projectRoot, CLAUDE_MD), content, "utf-8");
|
|
355
|
+
}
|
|
356
|
+
return [CLAUDE_MD];
|
|
357
|
+
}
|
|
358
|
+
async function exportClaudeRules(rules, projectRoot, dryRun) {
|
|
359
|
+
const fileScoped = rules.filter((r) => r.scope === "file-scoped");
|
|
360
|
+
if (fileScoped.length === 0) return [];
|
|
361
|
+
const dir = join2(projectRoot, RULES_DIR);
|
|
362
|
+
if (!dryRun) await mkdir(dir, { recursive: true });
|
|
363
|
+
const written = [];
|
|
364
|
+
for (const rule of fileScoped) {
|
|
365
|
+
const filePath = join2(RULES_DIR, `${rule.id}.md`);
|
|
366
|
+
if (!dryRun) {
|
|
367
|
+
await writeFile2(
|
|
368
|
+
join2(projectRoot, filePath),
|
|
369
|
+
buildClaudeRuleFile(rule),
|
|
370
|
+
"utf-8"
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
written.push(filePath);
|
|
374
|
+
}
|
|
375
|
+
return written;
|
|
376
|
+
}
|
|
377
|
+
async function deleteStaleRuleFiles(rules, projectRoot, dryRun) {
|
|
378
|
+
const dir = join2(projectRoot, RULES_DIR);
|
|
379
|
+
const existing = await listMdFiles(dir);
|
|
380
|
+
const exportedIds = new Set(
|
|
381
|
+
rules.filter((r) => r.scope === "file-scoped").map((r) => `${r.id}.md`)
|
|
382
|
+
);
|
|
383
|
+
const deleted = [];
|
|
384
|
+
for (const file of existing) {
|
|
385
|
+
if (!exportedIds.has(file)) {
|
|
386
|
+
const filePath = join2(RULES_DIR, file);
|
|
387
|
+
if (!dryRun) await rm(join2(projectRoot, filePath));
|
|
388
|
+
deleted.push(filePath);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return deleted;
|
|
392
|
+
}
|
|
393
|
+
var claudeCodeAdapter = {
|
|
394
|
+
name: "claude-code",
|
|
395
|
+
displayName: "Claude Code",
|
|
396
|
+
async detect(projectRoot) {
|
|
397
|
+
const claudeMd = join2(projectRoot, CLAUDE_MD);
|
|
398
|
+
const claudeDir = join2(projectRoot, ".claude");
|
|
399
|
+
return await pathExists2(claudeMd) || await pathExists2(claudeDir);
|
|
400
|
+
},
|
|
401
|
+
async import(projectRoot) {
|
|
402
|
+
const mdRules = await importClaudeMd(projectRoot);
|
|
403
|
+
const ruleFiles = await importClaudeRuleFiles(projectRoot);
|
|
404
|
+
return {
|
|
405
|
+
rules: [...mdRules, ...ruleFiles],
|
|
406
|
+
warnings: [],
|
|
407
|
+
source: CLAUDE_MD
|
|
408
|
+
};
|
|
409
|
+
},
|
|
410
|
+
async export(rules, projectRoot, options) {
|
|
411
|
+
const dryRun = options?.dryRun === true;
|
|
412
|
+
const strategy = options?.strategy ?? "overwrite";
|
|
413
|
+
const mdWritten = await exportClaudeMd(rules, projectRoot, dryRun);
|
|
414
|
+
const rulesWritten = await exportClaudeRules(rules, projectRoot, dryRun);
|
|
415
|
+
const filesDeleted = strategy === "overwrite" ? await deleteStaleRuleFiles(rules, projectRoot, dryRun) : [];
|
|
416
|
+
return {
|
|
417
|
+
filesWritten: [...mdWritten, ...rulesWritten],
|
|
418
|
+
filesDeleted,
|
|
419
|
+
warnings: []
|
|
420
|
+
};
|
|
421
|
+
},
|
|
422
|
+
getTokenBudget() {
|
|
423
|
+
return {
|
|
424
|
+
maxTokens: 2e3,
|
|
425
|
+
maxInstructions: 150,
|
|
426
|
+
warningThreshold: 0.8,
|
|
427
|
+
source: "Claude Code documentation"
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// src/adapters/cursor.ts
|
|
433
|
+
import {
|
|
434
|
+
access as access3,
|
|
435
|
+
mkdir as mkdir2,
|
|
436
|
+
readdir as readdir2,
|
|
437
|
+
readFile as readFile2,
|
|
438
|
+
rm as rm2,
|
|
439
|
+
writeFile as writeFile3
|
|
440
|
+
} from "fs/promises";
|
|
441
|
+
import { basename as basename2, join as join3 } from "path";
|
|
442
|
+
var RULES_DIR2 = ".cursor/rules";
|
|
443
|
+
var LEGACY_FILE = ".cursorrules";
|
|
444
|
+
var MDC_EXT = ".mdc";
|
|
445
|
+
async function pathExists3(filePath) {
|
|
446
|
+
try {
|
|
447
|
+
await access3(filePath);
|
|
448
|
+
return true;
|
|
449
|
+
} catch {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
async function listMdcFiles(dir) {
|
|
454
|
+
try {
|
|
455
|
+
const entries = await readdir2(dir);
|
|
456
|
+
return entries.filter((f) => f.endsWith(MDC_EXT)).sort();
|
|
457
|
+
} catch {
|
|
458
|
+
return [];
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function stripQuotes2(s) {
|
|
462
|
+
const t = s.trim();
|
|
463
|
+
if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) {
|
|
464
|
+
return t.slice(1, -1);
|
|
465
|
+
}
|
|
466
|
+
return t;
|
|
467
|
+
}
|
|
468
|
+
function parseInlineArray(raw) {
|
|
469
|
+
const inner = raw.slice(1, -1).trim();
|
|
470
|
+
if (inner === "") return [];
|
|
471
|
+
return inner.split(",").map(stripQuotes2);
|
|
472
|
+
}
|
|
473
|
+
function splitMdcFrontmatter(raw) {
|
|
474
|
+
const lines = raw.replace(/\r\n/g, "\n").split("\n");
|
|
475
|
+
if (lines[0]?.trim() !== "---") return null;
|
|
476
|
+
for (let i = 1; i < lines.length; i++) {
|
|
477
|
+
if (lines[i]?.trim() === "---") {
|
|
478
|
+
return {
|
|
479
|
+
yaml: lines.slice(1, i).join("\n"),
|
|
480
|
+
content: lines.slice(i + 1).join("\n").trim()
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
function parseMdcFields(yaml) {
|
|
487
|
+
let description;
|
|
488
|
+
let globs;
|
|
489
|
+
let alwaysApply = false;
|
|
490
|
+
for (const line of yaml.split("\n")) {
|
|
491
|
+
const trimmed = line.trim();
|
|
492
|
+
if (trimmed === "") continue;
|
|
493
|
+
const colonIdx = trimmed.indexOf(":");
|
|
494
|
+
if (colonIdx === -1) continue;
|
|
495
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
496
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
497
|
+
if (key === "description" && value !== "") {
|
|
498
|
+
description = stripQuotes2(value);
|
|
499
|
+
} else if (key === "globs" && value !== "") {
|
|
500
|
+
globs = value.startsWith("[") && value.endsWith("]") ? parseInlineArray(value) : [stripQuotes2(value)];
|
|
501
|
+
} else if (key === "alwaysApply") {
|
|
502
|
+
alwaysApply = value === "true";
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return { description, globs, alwaysApply };
|
|
506
|
+
}
|
|
507
|
+
function determineScope(fields) {
|
|
508
|
+
if (fields.alwaysApply) return "always";
|
|
509
|
+
if (fields.globs && fields.globs.length > 0) return "file-scoped";
|
|
510
|
+
if (fields.description) return "agent-selected";
|
|
511
|
+
return "always";
|
|
512
|
+
}
|
|
513
|
+
function importRuleWithoutFrontmatter(raw, filePath, id) {
|
|
514
|
+
const content = raw.replace(/\r\n/g, "\n").trim();
|
|
515
|
+
const description = id.replace(/-/g, " ");
|
|
516
|
+
return {
|
|
517
|
+
rule: {
|
|
518
|
+
id,
|
|
519
|
+
scope: "always",
|
|
520
|
+
description,
|
|
521
|
+
content,
|
|
522
|
+
category: "general",
|
|
523
|
+
priority: 3,
|
|
524
|
+
estimatedTokens: estimateRuleTokens(content, description),
|
|
525
|
+
source: {
|
|
526
|
+
adapter: "cursor",
|
|
527
|
+
filePath,
|
|
528
|
+
importedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
warning: {
|
|
532
|
+
filePath,
|
|
533
|
+
message: "No frontmatter found, treating as always-on rule"
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function importRuleWithFrontmatter(parts, filePath, id) {
|
|
538
|
+
const fields = parseMdcFields(parts.yaml);
|
|
539
|
+
const scope = determineScope(fields);
|
|
540
|
+
const description = fields.description ?? id.replace(/-/g, " ");
|
|
541
|
+
return {
|
|
542
|
+
id,
|
|
543
|
+
scope,
|
|
544
|
+
description,
|
|
545
|
+
content: parts.content,
|
|
546
|
+
category: "general",
|
|
547
|
+
priority: 3,
|
|
548
|
+
estimatedTokens: estimateRuleTokens(parts.content, description),
|
|
549
|
+
...scope === "file-scoped" && fields.globs ? { globs: fields.globs } : {},
|
|
550
|
+
source: {
|
|
551
|
+
adapter: "cursor",
|
|
552
|
+
filePath,
|
|
553
|
+
importedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
async function importMdcFiles(projectRoot) {
|
|
558
|
+
const rules = [];
|
|
559
|
+
const warnings = [];
|
|
560
|
+
const dir = join3(projectRoot, RULES_DIR2);
|
|
561
|
+
const files = await listMdcFiles(dir);
|
|
562
|
+
for (const file of files) {
|
|
563
|
+
const filePath = join3(RULES_DIR2, file);
|
|
564
|
+
const raw = await readFile2(join3(projectRoot, filePath), "utf-8");
|
|
565
|
+
const id = basename2(file, MDC_EXT);
|
|
566
|
+
const parts = splitMdcFrontmatter(raw);
|
|
567
|
+
if (!parts) {
|
|
568
|
+
const result = importRuleWithoutFrontmatter(raw, filePath, id);
|
|
569
|
+
rules.push(result.rule);
|
|
570
|
+
warnings.push(result.warning);
|
|
571
|
+
} else {
|
|
572
|
+
rules.push(importRuleWithFrontmatter(parts, filePath, id));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return { rules, warnings };
|
|
576
|
+
}
|
|
577
|
+
async function importLegacyFile(projectRoot) {
|
|
578
|
+
const legacyPath = join3(projectRoot, LEGACY_FILE);
|
|
579
|
+
if (!await pathExists3(legacyPath)) return { rules: [], warnings: [] };
|
|
580
|
+
const raw = await readFile2(legacyPath, "utf-8");
|
|
581
|
+
const content = raw.trim();
|
|
582
|
+
if (content === "") return { rules: [], warnings: [] };
|
|
583
|
+
return {
|
|
584
|
+
rules: [
|
|
585
|
+
{
|
|
586
|
+
id: "cursorrules-legacy",
|
|
587
|
+
scope: "always",
|
|
588
|
+
description: "Legacy .cursorrules file",
|
|
589
|
+
content,
|
|
590
|
+
category: "general",
|
|
591
|
+
priority: 3,
|
|
592
|
+
estimatedTokens: estimateRuleTokens(content, "Legacy .cursorrules"),
|
|
593
|
+
source: {
|
|
594
|
+
adapter: "cursor",
|
|
595
|
+
filePath: LEGACY_FILE,
|
|
596
|
+
importedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
],
|
|
600
|
+
warnings: [
|
|
601
|
+
{
|
|
602
|
+
filePath: LEGACY_FILE,
|
|
603
|
+
message: ".cursorrules is deprecated. Migrate to .cursor/rules/*.mdc"
|
|
604
|
+
}
|
|
605
|
+
]
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function serializeMdcGlobs(globs) {
|
|
609
|
+
if (globs.length === 1) {
|
|
610
|
+
const first = globs[0];
|
|
611
|
+
if (first !== void 0) return `globs: "${first}"`;
|
|
612
|
+
}
|
|
613
|
+
const items = globs.map((g) => `"${g}"`).join(", ");
|
|
614
|
+
return `globs: [${items}]`;
|
|
615
|
+
}
|
|
616
|
+
function ruleToMdc(rule) {
|
|
617
|
+
const lines = ["---"];
|
|
618
|
+
lines.push(`description: "${rule.description}"`);
|
|
619
|
+
if (rule.scope === "file-scoped" && rule.globs && rule.globs.length > 0) {
|
|
620
|
+
lines.push(serializeMdcGlobs(rule.globs));
|
|
621
|
+
}
|
|
622
|
+
lines.push(`alwaysApply: ${rule.scope === "always"}`);
|
|
623
|
+
lines.push("---");
|
|
624
|
+
lines.push("");
|
|
625
|
+
lines.push(rule.content);
|
|
626
|
+
lines.push("");
|
|
627
|
+
return lines.join("\n");
|
|
628
|
+
}
|
|
629
|
+
async function deleteStaleFiles(projectRoot, exportedIds, dryRun) {
|
|
630
|
+
const dir = join3(projectRoot, RULES_DIR2);
|
|
631
|
+
const existing = await listMdcFiles(dir);
|
|
632
|
+
const deleted = [];
|
|
633
|
+
for (const file of existing) {
|
|
634
|
+
if (!exportedIds.has(file)) {
|
|
635
|
+
const filePath = join3(RULES_DIR2, file);
|
|
636
|
+
if (!dryRun) await rm2(join3(projectRoot, filePath));
|
|
637
|
+
deleted.push(filePath);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return deleted;
|
|
641
|
+
}
|
|
642
|
+
var cursorAdapter = {
|
|
643
|
+
name: "cursor",
|
|
644
|
+
displayName: "Cursor",
|
|
645
|
+
async detect(projectRoot) {
|
|
646
|
+
const rulesDir = join3(projectRoot, RULES_DIR2);
|
|
647
|
+
const legacyFile = join3(projectRoot, LEGACY_FILE);
|
|
648
|
+
return await pathExists3(rulesDir) || await pathExists3(legacyFile);
|
|
649
|
+
},
|
|
650
|
+
async import(projectRoot) {
|
|
651
|
+
const mdc = await importMdcFiles(projectRoot);
|
|
652
|
+
const legacy = await importLegacyFile(projectRoot);
|
|
653
|
+
return {
|
|
654
|
+
rules: [...mdc.rules, ...legacy.rules],
|
|
655
|
+
warnings: [...mdc.warnings, ...legacy.warnings],
|
|
656
|
+
source: RULES_DIR2
|
|
657
|
+
};
|
|
658
|
+
},
|
|
659
|
+
async export(rules, projectRoot, options) {
|
|
660
|
+
const dir = join3(projectRoot, RULES_DIR2);
|
|
661
|
+
const dryRun = options?.dryRun === true;
|
|
662
|
+
const strategy = options?.strategy ?? "overwrite";
|
|
663
|
+
const filesWritten = [];
|
|
664
|
+
const warnings = [];
|
|
665
|
+
if (!dryRun) await mkdir2(dir, { recursive: true });
|
|
666
|
+
for (const rule of rules) {
|
|
667
|
+
const filePath = join3(RULES_DIR2, `${rule.id}${MDC_EXT}`);
|
|
668
|
+
if (!dryRun) {
|
|
669
|
+
await writeFile3(join3(projectRoot, filePath), ruleToMdc(rule), "utf-8");
|
|
670
|
+
}
|
|
671
|
+
filesWritten.push(filePath);
|
|
672
|
+
}
|
|
673
|
+
const exportedIds = new Set(rules.map((r) => `${r.id}${MDC_EXT}`));
|
|
674
|
+
const filesDeleted = strategy === "overwrite" ? await deleteStaleFiles(projectRoot, exportedIds, dryRun) : [];
|
|
675
|
+
return { filesWritten, filesDeleted, warnings };
|
|
676
|
+
},
|
|
677
|
+
getTokenBudget() {
|
|
678
|
+
return {
|
|
679
|
+
maxTokens: 1e4,
|
|
680
|
+
maxInstructions: 500,
|
|
681
|
+
warningThreshold: 0.8,
|
|
682
|
+
source: "Cursor documentation"
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
// src/adapters/registry.ts
|
|
688
|
+
var BUILTIN_ADAPTERS = [
|
|
689
|
+
cursorAdapter,
|
|
690
|
+
claudeCodeAdapter,
|
|
691
|
+
agentsMdAdapter
|
|
692
|
+
];
|
|
693
|
+
var adapterMap = new Map(
|
|
694
|
+
BUILTIN_ADAPTERS.map((a) => [a.name, a])
|
|
695
|
+
);
|
|
696
|
+
function getAdapters() {
|
|
697
|
+
return [...adapterMap.values()];
|
|
698
|
+
}
|
|
699
|
+
function getAdapter(name) {
|
|
700
|
+
return adapterMap.get(name);
|
|
701
|
+
}
|
|
702
|
+
function getAdapterNames() {
|
|
703
|
+
return [...adapterMap.keys()];
|
|
704
|
+
}
|
|
705
|
+
async function detectAdapters(projectRoot) {
|
|
706
|
+
const results = await Promise.all(
|
|
707
|
+
BUILTIN_ADAPTERS.map(async (adapter) => ({
|
|
708
|
+
adapter,
|
|
709
|
+
detected: await adapter.detect(projectRoot)
|
|
710
|
+
}))
|
|
711
|
+
);
|
|
712
|
+
return results.filter((r) => r.detected).map((r) => r.adapter);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/core/ir.ts
|
|
716
|
+
var RulixError = class extends Error {
|
|
717
|
+
constructor(code, message, cause) {
|
|
718
|
+
super(message);
|
|
719
|
+
this.code = code;
|
|
720
|
+
this.cause = cause;
|
|
721
|
+
}
|
|
722
|
+
name = "RulixError";
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// src/core/config.ts
|
|
726
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
727
|
+
import { join as join4 } from "path";
|
|
728
|
+
var RULIX_DIR = ".rulix";
|
|
729
|
+
var CONFIG_FILENAME = "config.json";
|
|
730
|
+
var RULES_DIR3 = "rules";
|
|
731
|
+
var DEFAULT_OPTIONS = {
|
|
732
|
+
tokenEstimation: "heuristic",
|
|
733
|
+
agentsMdHeader: true,
|
|
734
|
+
claudeMdStrategy: "concatenate",
|
|
735
|
+
syncOnSave: false
|
|
736
|
+
};
|
|
737
|
+
var DEFAULT_CONFIG = {
|
|
738
|
+
targets: [],
|
|
739
|
+
presets: [],
|
|
740
|
+
overrides: {},
|
|
741
|
+
options: DEFAULT_OPTIONS
|
|
742
|
+
};
|
|
743
|
+
function configPath(projectRoot) {
|
|
744
|
+
return join4(projectRoot, RULIX_DIR, CONFIG_FILENAME);
|
|
745
|
+
}
|
|
746
|
+
function rulesPath(projectRoot) {
|
|
747
|
+
return join4(projectRoot, RULIX_DIR, RULES_DIR3);
|
|
748
|
+
}
|
|
749
|
+
function createDefaultConfig() {
|
|
750
|
+
return DEFAULT_CONFIG;
|
|
751
|
+
}
|
|
752
|
+
function isRecord(value) {
|
|
753
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
754
|
+
}
|
|
755
|
+
function isStringArray(value) {
|
|
756
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
757
|
+
}
|
|
758
|
+
function configError(message) {
|
|
759
|
+
return { ok: false, error: new RulixError("CONFIG_INVALID", message) };
|
|
760
|
+
}
|
|
761
|
+
function resolveOptions(raw) {
|
|
762
|
+
if (raw === void 0) return { ok: true, value: DEFAULT_OPTIONS };
|
|
763
|
+
if (!isRecord(raw)) return configError('"options" must be an object');
|
|
764
|
+
if (raw.tokenEstimation !== void 0 && raw.tokenEstimation !== "heuristic" && raw.tokenEstimation !== "tiktoken") {
|
|
765
|
+
return configError(
|
|
766
|
+
'"options.tokenEstimation" must be "heuristic" or "tiktoken"'
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
if (raw.claudeMdStrategy !== void 0 && raw.claudeMdStrategy !== "concatenate" && raw.claudeMdStrategy !== "reference") {
|
|
770
|
+
return configError(
|
|
771
|
+
'"options.claudeMdStrategy" must be "concatenate" or "reference"'
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
if (raw.agentsMdHeader !== void 0 && typeof raw.agentsMdHeader !== "boolean") {
|
|
775
|
+
return configError('"options.agentsMdHeader" must be a boolean');
|
|
776
|
+
}
|
|
777
|
+
if (raw.syncOnSave !== void 0 && typeof raw.syncOnSave !== "boolean") {
|
|
778
|
+
return configError('"options.syncOnSave" must be a boolean');
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
ok: true,
|
|
782
|
+
value: {
|
|
783
|
+
tokenEstimation: raw.tokenEstimation ?? DEFAULT_OPTIONS.tokenEstimation,
|
|
784
|
+
agentsMdHeader: raw.agentsMdHeader ?? DEFAULT_OPTIONS.agentsMdHeader,
|
|
785
|
+
claudeMdStrategy: raw.claudeMdStrategy ?? DEFAULT_OPTIONS.claudeMdStrategy,
|
|
786
|
+
syncOnSave: raw.syncOnSave ?? DEFAULT_OPTIONS.syncOnSave
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
function resolveConfig(raw) {
|
|
791
|
+
if (!isRecord(raw)) return configError("Config must be a JSON object");
|
|
792
|
+
if (raw.targets !== void 0 && !isStringArray(raw.targets)) {
|
|
793
|
+
return configError('"targets" must be an array of strings');
|
|
794
|
+
}
|
|
795
|
+
if (raw.presets !== void 0 && !isStringArray(raw.presets)) {
|
|
796
|
+
return configError('"presets" must be an array of strings');
|
|
797
|
+
}
|
|
798
|
+
if (raw.overrides !== void 0 && !isRecord(raw.overrides)) {
|
|
799
|
+
return configError('"overrides" must be an object');
|
|
800
|
+
}
|
|
801
|
+
const options = resolveOptions(raw.options);
|
|
802
|
+
if (!options.ok) return options;
|
|
803
|
+
return {
|
|
804
|
+
ok: true,
|
|
805
|
+
value: {
|
|
806
|
+
targets: raw.targets ?? DEFAULT_CONFIG.targets,
|
|
807
|
+
presets: raw.presets ?? DEFAULT_CONFIG.presets,
|
|
808
|
+
overrides: raw.overrides ?? DEFAULT_CONFIG.overrides,
|
|
809
|
+
options: options.value
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
async function loadConfig(projectRoot) {
|
|
814
|
+
const filePath = configPath(projectRoot);
|
|
815
|
+
let content;
|
|
816
|
+
try {
|
|
817
|
+
content = await readFile3(filePath, "utf-8");
|
|
818
|
+
} catch (error) {
|
|
819
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
820
|
+
return { ok: true, value: createDefaultConfig() };
|
|
821
|
+
}
|
|
822
|
+
throw error;
|
|
823
|
+
}
|
|
824
|
+
let raw;
|
|
825
|
+
try {
|
|
826
|
+
raw = JSON.parse(content);
|
|
827
|
+
} catch {
|
|
828
|
+
return configError(`Invalid JSON in ${filePath}`);
|
|
829
|
+
}
|
|
830
|
+
return resolveConfig(raw);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/core/parser.ts
|
|
834
|
+
import { mkdir as mkdir3, readdir as readdir3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
835
|
+
import { join as join5 } from "path";
|
|
836
|
+
var DEFAULT_CATEGORY = "general";
|
|
837
|
+
var DEFAULT_PRIORITY = 3;
|
|
838
|
+
function parseError(message, filePath) {
|
|
839
|
+
const suffix = filePath ? ` in ${filePath}` : "";
|
|
840
|
+
return {
|
|
841
|
+
ok: false,
|
|
842
|
+
error: new RulixError("PARSE_ERROR", `${message}${suffix}`)
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
function stripQuotes3(value) {
|
|
846
|
+
const t = value.trim();
|
|
847
|
+
if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) {
|
|
848
|
+
return t.slice(1, -1);
|
|
849
|
+
}
|
|
850
|
+
return t;
|
|
851
|
+
}
|
|
852
|
+
function parseInlineArray2(raw) {
|
|
853
|
+
const inner = raw.slice(1, -1).trim();
|
|
854
|
+
if (inner === "") return [];
|
|
855
|
+
return inner.split(",").map(stripQuotes3);
|
|
856
|
+
}
|
|
857
|
+
function parseYamlValue(raw) {
|
|
858
|
+
const trimmed = raw.trim();
|
|
859
|
+
if (trimmed === "") return trimmed;
|
|
860
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
861
|
+
return parseInlineArray2(trimmed);
|
|
862
|
+
}
|
|
863
|
+
if (/^-?\d+$/.test(trimmed)) return Number(trimmed);
|
|
864
|
+
if (trimmed === "true") return true;
|
|
865
|
+
if (trimmed === "false") return false;
|
|
866
|
+
return stripQuotes3(trimmed);
|
|
867
|
+
}
|
|
868
|
+
function splitFrontmatter2(raw) {
|
|
869
|
+
const lines = raw.replace(/\r\n/g, "\n").split("\n");
|
|
870
|
+
if (lines[0]?.trim() !== "---") {
|
|
871
|
+
return parseError("File must start with frontmatter (---)");
|
|
872
|
+
}
|
|
873
|
+
let closingIndex = -1;
|
|
874
|
+
for (let i = 1; i < lines.length; i++) {
|
|
875
|
+
if (lines[i]?.trim() === "---") {
|
|
876
|
+
closingIndex = i;
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (closingIndex === -1) {
|
|
881
|
+
return parseError("Unterminated frontmatter (missing closing ---)");
|
|
882
|
+
}
|
|
883
|
+
return {
|
|
884
|
+
ok: true,
|
|
885
|
+
value: {
|
|
886
|
+
yaml: lines.slice(1, closingIndex).join("\n"),
|
|
887
|
+
content: lines.slice(closingIndex + 1).join("\n")
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
function parseFrontmatterFields(yaml) {
|
|
892
|
+
const fields = {};
|
|
893
|
+
const lines = yaml.split("\n");
|
|
894
|
+
let arrayKey;
|
|
895
|
+
let arrayItems = [];
|
|
896
|
+
for (const line of lines) {
|
|
897
|
+
const trimmed = line.trim();
|
|
898
|
+
if (trimmed === "") continue;
|
|
899
|
+
if (trimmed.startsWith("- ")) {
|
|
900
|
+
if (arrayKey !== void 0) {
|
|
901
|
+
arrayItems.push(stripQuotes3(trimmed.slice(2)));
|
|
902
|
+
}
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
if (arrayKey !== void 0) {
|
|
906
|
+
fields[arrayKey] = arrayItems;
|
|
907
|
+
arrayKey = void 0;
|
|
908
|
+
arrayItems = [];
|
|
909
|
+
}
|
|
910
|
+
const colonIndex = trimmed.indexOf(":");
|
|
911
|
+
if (colonIndex === -1) continue;
|
|
912
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
913
|
+
const rawValue = trimmed.slice(colonIndex + 1).trim();
|
|
914
|
+
if (rawValue === "") {
|
|
915
|
+
arrayKey = key;
|
|
916
|
+
arrayItems = [];
|
|
917
|
+
} else {
|
|
918
|
+
fields[key] = parseYamlValue(rawValue);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (arrayKey !== void 0) fields[arrayKey] = arrayItems;
|
|
922
|
+
return fields;
|
|
923
|
+
}
|
|
924
|
+
function isValidScope(value) {
|
|
925
|
+
return value === "always" || value === "file-scoped" || value === "agent-selected";
|
|
926
|
+
}
|
|
927
|
+
function isValidCategory(value) {
|
|
928
|
+
return value === "style" || value === "security" || value === "testing" || value === "architecture" || value === "workflow" || value === "general";
|
|
929
|
+
}
|
|
930
|
+
function normalizeGlobs(value) {
|
|
931
|
+
if (value === void 0) return void 0;
|
|
932
|
+
if (typeof value === "string") return [value];
|
|
933
|
+
if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
|
|
934
|
+
return value;
|
|
935
|
+
}
|
|
936
|
+
return void 0;
|
|
937
|
+
}
|
|
938
|
+
function parseRule(raw, filePath) {
|
|
939
|
+
const split = splitFrontmatter2(raw);
|
|
940
|
+
if (!split.ok) return parseError(split.error.message, filePath);
|
|
941
|
+
const fields = parseFrontmatterFields(split.value.yaml);
|
|
942
|
+
const content = split.value.content.trim();
|
|
943
|
+
const { id, scope, description } = fields;
|
|
944
|
+
if (typeof id !== "string" || id === "") {
|
|
945
|
+
return parseError('Missing required field "id"', filePath);
|
|
946
|
+
}
|
|
947
|
+
if (!isValidScope(scope)) {
|
|
948
|
+
return parseError('Invalid or missing "scope"', filePath);
|
|
949
|
+
}
|
|
950
|
+
if (typeof description !== "string" || description === "") {
|
|
951
|
+
return parseError('Missing required field "description"', filePath);
|
|
952
|
+
}
|
|
953
|
+
const globs = normalizeGlobs(fields.globs);
|
|
954
|
+
const extendsVal = typeof fields.extends === "string" ? fields.extends : void 0;
|
|
955
|
+
return {
|
|
956
|
+
ok: true,
|
|
957
|
+
value: {
|
|
958
|
+
id,
|
|
959
|
+
scope,
|
|
960
|
+
description,
|
|
961
|
+
content,
|
|
962
|
+
category: isValidCategory(fields.category) ? fields.category : DEFAULT_CATEGORY,
|
|
963
|
+
priority: typeof fields.priority === "number" ? fields.priority : DEFAULT_PRIORITY,
|
|
964
|
+
estimatedTokens: estimateRuleTokens(content, description),
|
|
965
|
+
...globs !== void 0 ? { globs } : {},
|
|
966
|
+
...extendsVal !== void 0 ? { extends: extendsVal } : {}
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
function serializeGlobs(globs, lines) {
|
|
971
|
+
if (globs.length === 1) {
|
|
972
|
+
const first = globs[0];
|
|
973
|
+
if (first !== void 0) lines.push(`globs: "${first}"`);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
lines.push("globs:");
|
|
977
|
+
for (const glob of globs) {
|
|
978
|
+
lines.push(` - "${glob}"`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function serializeRule(rule) {
|
|
982
|
+
const lines = ["---"];
|
|
983
|
+
lines.push(`id: ${rule.id}`);
|
|
984
|
+
lines.push(`scope: ${rule.scope}`);
|
|
985
|
+
lines.push(`description: "${rule.description}"`);
|
|
986
|
+
if (rule.globs && rule.globs.length > 0) serializeGlobs(rule.globs, lines);
|
|
987
|
+
lines.push(`category: ${rule.category}`);
|
|
988
|
+
lines.push(`priority: ${rule.priority}`);
|
|
989
|
+
if (rule.extends) lines.push(`extends: ${rule.extends}`);
|
|
990
|
+
lines.push("---");
|
|
991
|
+
lines.push("");
|
|
992
|
+
lines.push(rule.content);
|
|
993
|
+
lines.push("");
|
|
994
|
+
return lines.join("\n");
|
|
995
|
+
}
|
|
996
|
+
async function loadRules(projectRoot) {
|
|
997
|
+
const dir = rulesPath(projectRoot);
|
|
998
|
+
let entries;
|
|
999
|
+
try {
|
|
1000
|
+
entries = await readdir3(dir);
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1003
|
+
return { ok: true, value: [] };
|
|
1004
|
+
}
|
|
1005
|
+
throw error;
|
|
1006
|
+
}
|
|
1007
|
+
const mdFiles = entries.filter((f) => f.endsWith(".md")).sort();
|
|
1008
|
+
const rules = [];
|
|
1009
|
+
for (const file of mdFiles) {
|
|
1010
|
+
const filePath = join5(dir, file);
|
|
1011
|
+
const raw = await readFile4(filePath, "utf-8");
|
|
1012
|
+
const result = parseRule(raw, filePath);
|
|
1013
|
+
if (!result.ok) return result;
|
|
1014
|
+
rules.push(result.value);
|
|
1015
|
+
}
|
|
1016
|
+
return { ok: true, value: rules };
|
|
1017
|
+
}
|
|
1018
|
+
async function writeRule(projectRoot, rule) {
|
|
1019
|
+
const dir = rulesPath(projectRoot);
|
|
1020
|
+
await mkdir3(dir, { recursive: true });
|
|
1021
|
+
await writeFile4(join5(dir, `${rule.id}.md`), serializeRule(rule), "utf-8");
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// src/core/validator.ts
|
|
1025
|
+
var MIN_CONTENT_LENGTH = 20;
|
|
1026
|
+
var MAX_CONTENT_LINES = 50;
|
|
1027
|
+
var VAGUE_PATTERNS = [
|
|
1028
|
+
/handle .+ properly/i,
|
|
1029
|
+
/do .+ correctly/i,
|
|
1030
|
+
/implement .+ properly/i,
|
|
1031
|
+
/make sure .+ works/i,
|
|
1032
|
+
/ensure .+ is correct/i,
|
|
1033
|
+
/follow best practices/i
|
|
1034
|
+
];
|
|
1035
|
+
function createIssue(code, severity, message, ruleId, suggestion) {
|
|
1036
|
+
return {
|
|
1037
|
+
code,
|
|
1038
|
+
severity,
|
|
1039
|
+
message,
|
|
1040
|
+
...ruleId !== void 0 ? { ruleId } : {},
|
|
1041
|
+
...suggestion !== void 0 ? { suggestion } : {}
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
function checkDuplicateIds(rules) {
|
|
1045
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1046
|
+
const issues = [];
|
|
1047
|
+
for (const rule of rules) {
|
|
1048
|
+
const count = (seen.get(rule.id) ?? 0) + 1;
|
|
1049
|
+
seen.set(rule.id, count);
|
|
1050
|
+
if (count === 2) {
|
|
1051
|
+
issues.push(
|
|
1052
|
+
createIssue(
|
|
1053
|
+
"V001",
|
|
1054
|
+
"error",
|
|
1055
|
+
`Duplicate rule ID "${rule.id}"`,
|
|
1056
|
+
rule.id,
|
|
1057
|
+
"Rename one of the duplicate rules"
|
|
1058
|
+
)
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return issues;
|
|
1063
|
+
}
|
|
1064
|
+
function checkRequiredFields(rule) {
|
|
1065
|
+
const issues = [];
|
|
1066
|
+
if (rule.id === "") {
|
|
1067
|
+
issues.push(createIssue("V002", "error", "Rule has empty ID", rule.id));
|
|
1068
|
+
}
|
|
1069
|
+
if (rule.description === "") {
|
|
1070
|
+
issues.push(
|
|
1071
|
+
createIssue(
|
|
1072
|
+
"V002",
|
|
1073
|
+
"error",
|
|
1074
|
+
`Rule "${rule.id}" has empty description`,
|
|
1075
|
+
rule.id
|
|
1076
|
+
)
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
return issues;
|
|
1080
|
+
}
|
|
1081
|
+
function checkFileScopedGlobs(rule) {
|
|
1082
|
+
if (rule.scope !== "file-scoped") return [];
|
|
1083
|
+
if (!rule.globs || rule.globs.length === 0) {
|
|
1084
|
+
return [
|
|
1085
|
+
createIssue(
|
|
1086
|
+
"V003",
|
|
1087
|
+
"error",
|
|
1088
|
+
`Rule "${rule.id}" is file-scoped but has no globs`,
|
|
1089
|
+
rule.id,
|
|
1090
|
+
'Add globs or change scope to "always"'
|
|
1091
|
+
)
|
|
1092
|
+
];
|
|
1093
|
+
}
|
|
1094
|
+
return [];
|
|
1095
|
+
}
|
|
1096
|
+
function checkShortContent(rule) {
|
|
1097
|
+
if (rule.content.length < MIN_CONTENT_LENGTH) {
|
|
1098
|
+
return [
|
|
1099
|
+
createIssue(
|
|
1100
|
+
"V004",
|
|
1101
|
+
"warning",
|
|
1102
|
+
`Rule "${rule.id}" has very short content (${rule.content.length} chars)`,
|
|
1103
|
+
rule.id,
|
|
1104
|
+
"Consider adding more detail"
|
|
1105
|
+
)
|
|
1106
|
+
];
|
|
1107
|
+
}
|
|
1108
|
+
return [];
|
|
1109
|
+
}
|
|
1110
|
+
function checkVagueDescription(rule) {
|
|
1111
|
+
for (const pattern of VAGUE_PATTERNS) {
|
|
1112
|
+
if (pattern.test(rule.description)) {
|
|
1113
|
+
return [
|
|
1114
|
+
createIssue(
|
|
1115
|
+
"V005",
|
|
1116
|
+
"warning",
|
|
1117
|
+
`Rule "${rule.id}" has a vague description`,
|
|
1118
|
+
rule.id,
|
|
1119
|
+
"Be more specific about what conventions to follow"
|
|
1120
|
+
)
|
|
1121
|
+
];
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return [];
|
|
1125
|
+
}
|
|
1126
|
+
function checkDefaultCategory(rule) {
|
|
1127
|
+
if (rule.category === "general") {
|
|
1128
|
+
return [
|
|
1129
|
+
createIssue(
|
|
1130
|
+
"V007",
|
|
1131
|
+
"info",
|
|
1132
|
+
`Rule "${rule.id}" has no specific category`,
|
|
1133
|
+
rule.id,
|
|
1134
|
+
"Consider assigning a category (style, security, testing, architecture, workflow)"
|
|
1135
|
+
)
|
|
1136
|
+
];
|
|
1137
|
+
}
|
|
1138
|
+
return [];
|
|
1139
|
+
}
|
|
1140
|
+
function isValidGlobSyntax(pattern) {
|
|
1141
|
+
if (pattern.trim() === "") return false;
|
|
1142
|
+
let brackets = 0;
|
|
1143
|
+
let braces = 0;
|
|
1144
|
+
for (const ch of pattern) {
|
|
1145
|
+
if (ch === "[") brackets++;
|
|
1146
|
+
else if (ch === "]") brackets--;
|
|
1147
|
+
else if (ch === "{") braces++;
|
|
1148
|
+
else if (ch === "}") braces--;
|
|
1149
|
+
if (brackets < 0 || braces < 0) return false;
|
|
1150
|
+
}
|
|
1151
|
+
return brackets === 0 && braces === 0;
|
|
1152
|
+
}
|
|
1153
|
+
function checkGlobSyntax(rule) {
|
|
1154
|
+
if (!rule.globs) return [];
|
|
1155
|
+
const issues = [];
|
|
1156
|
+
for (const glob of rule.globs) {
|
|
1157
|
+
if (!isValidGlobSyntax(glob)) {
|
|
1158
|
+
issues.push(
|
|
1159
|
+
createIssue(
|
|
1160
|
+
"V009",
|
|
1161
|
+
"error",
|
|
1162
|
+
`Rule "${rule.id}" has invalid glob pattern: "${glob}"`,
|
|
1163
|
+
rule.id
|
|
1164
|
+
)
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return issues;
|
|
1169
|
+
}
|
|
1170
|
+
function checkLongContent(rule) {
|
|
1171
|
+
const lineCount = rule.content.split("\n").length;
|
|
1172
|
+
if (lineCount > MAX_CONTENT_LINES) {
|
|
1173
|
+
return [
|
|
1174
|
+
createIssue(
|
|
1175
|
+
"V010",
|
|
1176
|
+
"warning",
|
|
1177
|
+
`Rule "${rule.id}" has ${lineCount} lines (>${MAX_CONTENT_LINES})`,
|
|
1178
|
+
rule.id,
|
|
1179
|
+
"Consider splitting into smaller rules"
|
|
1180
|
+
)
|
|
1181
|
+
];
|
|
1182
|
+
}
|
|
1183
|
+
return [];
|
|
1184
|
+
}
|
|
1185
|
+
function buildResult(issues) {
|
|
1186
|
+
return {
|
|
1187
|
+
passed: issues.every((i) => i.severity !== "error"),
|
|
1188
|
+
errors: issues.filter((i) => i.severity === "error"),
|
|
1189
|
+
warnings: issues.filter((i) => i.severity === "warning"),
|
|
1190
|
+
info: issues.filter((i) => i.severity === "info")
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
function validateRules(rules) {
|
|
1194
|
+
const issues = [];
|
|
1195
|
+
issues.push(...checkDuplicateIds(rules));
|
|
1196
|
+
for (const rule of rules) {
|
|
1197
|
+
issues.push(...checkRequiredFields(rule));
|
|
1198
|
+
issues.push(...checkFileScopedGlobs(rule));
|
|
1199
|
+
issues.push(...checkShortContent(rule));
|
|
1200
|
+
issues.push(...checkVagueDescription(rule));
|
|
1201
|
+
issues.push(...checkDefaultCategory(rule));
|
|
1202
|
+
issues.push(...checkGlobSyntax(rule));
|
|
1203
|
+
issues.push(...checkLongContent(rule));
|
|
1204
|
+
}
|
|
1205
|
+
return buildResult(issues);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
export {
|
|
1209
|
+
agentsMdAdapter,
|
|
1210
|
+
estimateTokens,
|
|
1211
|
+
estimateRuleTokens,
|
|
1212
|
+
sumTokens,
|
|
1213
|
+
computeBudgetUsage,
|
|
1214
|
+
claudeCodeAdapter,
|
|
1215
|
+
cursorAdapter,
|
|
1216
|
+
getAdapters,
|
|
1217
|
+
getAdapter,
|
|
1218
|
+
getAdapterNames,
|
|
1219
|
+
detectAdapters,
|
|
1220
|
+
RulixError,
|
|
1221
|
+
RULIX_DIR,
|
|
1222
|
+
CONFIG_FILENAME,
|
|
1223
|
+
RULES_DIR3 as RULES_DIR,
|
|
1224
|
+
configPath,
|
|
1225
|
+
rulesPath,
|
|
1226
|
+
createDefaultConfig,
|
|
1227
|
+
resolveConfig,
|
|
1228
|
+
loadConfig,
|
|
1229
|
+
parseRule,
|
|
1230
|
+
serializeRule,
|
|
1231
|
+
loadRules,
|
|
1232
|
+
writeRule,
|
|
1233
|
+
validateRules
|
|
1234
|
+
};
|
|
1235
|
+
//# sourceMappingURL=chunk-IX4ZOAKV.js.map
|