rulesync 3.18.0 → 3.19.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/README.md +46 -0
- package/dist/index.cjs +224 -24
- package/dist/index.js +224 -24
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -557,6 +557,52 @@ Focus on the difference of MCP tools usage.
|
|
|
557
557
|
So, in this case, approximately 92% reduction in MCP tools consumption!
|
|
558
558
|
</details>
|
|
559
559
|
|
|
560
|
+
## Rulesync MCP Server (Experimental)
|
|
561
|
+
|
|
562
|
+
Rulesync provides an MCP (Model Context Protocol) server that enables AI agents to manage your Rulesync files. This allows AI agents to discover, read, create, update, and delete files dynamically.
|
|
563
|
+
|
|
564
|
+
### Usage
|
|
565
|
+
|
|
566
|
+
#### Starting the MCP Server
|
|
567
|
+
|
|
568
|
+
```bash
|
|
569
|
+
rulesync mcp
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
This starts an MCP server using stdio transport that AI agents can communicate with.
|
|
573
|
+
|
|
574
|
+
#### Configuration
|
|
575
|
+
|
|
576
|
+
Add the Rulesync MCP server to your `.rulesync/mcp.json`:
|
|
577
|
+
|
|
578
|
+
```json
|
|
579
|
+
{
|
|
580
|
+
"mcpServers": {
|
|
581
|
+
"rulesync-mcp": {
|
|
582
|
+
"type": "stdio",
|
|
583
|
+
"command": "npx",
|
|
584
|
+
"args": ["-y", "rulesync", "mcp"],
|
|
585
|
+
"env": {}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
For development, you can use:
|
|
592
|
+
|
|
593
|
+
```json
|
|
594
|
+
{
|
|
595
|
+
"mcpServers": {
|
|
596
|
+
"rulesync-mcp": {
|
|
597
|
+
"type": "stdio",
|
|
598
|
+
"command": "pnpm",
|
|
599
|
+
"args": ["dev", "mcp"],
|
|
600
|
+
"env": {}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
560
606
|
## Contributing
|
|
561
607
|
|
|
562
608
|
Issues and Pull Requests are welcome!
|
package/dist/index.cjs
CHANGED
|
@@ -37,6 +37,26 @@ var FeatureSchema = import_mini.z.enum(ALL_FEATURES);
|
|
|
37
37
|
var FeaturesSchema = import_mini.z.array(FeatureSchema);
|
|
38
38
|
var RulesyncFeaturesSchema = import_mini.z.array(import_mini.z.enum(ALL_FEATURES_WITH_WILDCARD));
|
|
39
39
|
|
|
40
|
+
// src/utils/error.ts
|
|
41
|
+
var import_zod = require("zod");
|
|
42
|
+
function isZodErrorLike(error) {
|
|
43
|
+
return error !== null && typeof error === "object" && "issues" in error && Array.isArray(error.issues) && error.issues.every(
|
|
44
|
+
(issue) => issue !== null && typeof issue === "object" && "path" in issue && Array.isArray(issue.path) && "message" in issue && typeof issue.message === "string"
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
function formatError(error) {
|
|
48
|
+
if (error instanceof import_zod.ZodError || isZodErrorLike(error)) {
|
|
49
|
+
return error.issues.map((issue) => {
|
|
50
|
+
const path2 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
51
|
+
return `${path2}${issue.message}`;
|
|
52
|
+
}).join("; ");
|
|
53
|
+
}
|
|
54
|
+
if (error instanceof Error) {
|
|
55
|
+
return `${error.name}: ${error.message}`;
|
|
56
|
+
}
|
|
57
|
+
return String(error);
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
// src/utils/logger.ts
|
|
41
61
|
var import_consola = require("consola");
|
|
42
62
|
|
|
@@ -222,26 +242,6 @@ var FeatureProcessor = class {
|
|
|
222
242
|
}
|
|
223
243
|
};
|
|
224
244
|
|
|
225
|
-
// src/utils/error.ts
|
|
226
|
-
var import_zod = require("zod");
|
|
227
|
-
function isZodErrorLike(error) {
|
|
228
|
-
return error !== null && typeof error === "object" && "issues" in error && Array.isArray(error.issues) && error.issues.every(
|
|
229
|
-
(issue) => issue !== null && typeof issue === "object" && "path" in issue && Array.isArray(issue.path) && "message" in issue && typeof issue.message === "string"
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
function formatError(error) {
|
|
233
|
-
if (error instanceof import_zod.ZodError || isZodErrorLike(error)) {
|
|
234
|
-
return error.issues.map((issue) => {
|
|
235
|
-
const path2 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
236
|
-
return `${path2}${issue.message}`;
|
|
237
|
-
}).join("; ");
|
|
238
|
-
}
|
|
239
|
-
if (error instanceof Error) {
|
|
240
|
-
return `${error.name}: ${error.message}`;
|
|
241
|
-
}
|
|
242
|
-
return String(error);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
245
|
// src/commands/agentsmd-command.ts
|
|
246
246
|
var import_node_path4 = require("path");
|
|
247
247
|
|
|
@@ -7706,8 +7706,200 @@ Attention, again, you are just the planner, so though you can read any files and
|
|
|
7706
7706
|
}
|
|
7707
7707
|
}
|
|
7708
7708
|
|
|
7709
|
+
// src/cli/commands/mcp.ts
|
|
7710
|
+
var import_node_path68 = require("path");
|
|
7711
|
+
var import_fastmcp = require("fastmcp");
|
|
7712
|
+
var import_mini23 = require("zod/mini");
|
|
7713
|
+
var MAX_RULE_SIZE_BYTES = 1024 * 1024;
|
|
7714
|
+
var MAX_RULES_COUNT = 1e3;
|
|
7715
|
+
var RULES_DIR_PREFIX = ".rulesync/rules";
|
|
7716
|
+
function validateRulePath(relativePathFromCwd) {
|
|
7717
|
+
const normalizedPath = relativePathFromCwd.replace(/\\/g, "/");
|
|
7718
|
+
if (!normalizedPath.startsWith(`${RULES_DIR_PREFIX}/`)) {
|
|
7719
|
+
throw new Error(`Invalid rule path: must be within ${RULES_DIR_PREFIX}/ directory`);
|
|
7720
|
+
}
|
|
7721
|
+
try {
|
|
7722
|
+
resolvePath(normalizedPath, process.cwd());
|
|
7723
|
+
} catch (error) {
|
|
7724
|
+
throw new Error(`Path validation failed: ${formatError(error)}`, { cause: error });
|
|
7725
|
+
}
|
|
7726
|
+
const filename = (0, import_node_path68.basename)(normalizedPath);
|
|
7727
|
+
if (!/^[a-zA-Z0-9_-]+\.md$/.test(filename)) {
|
|
7728
|
+
throw new Error(
|
|
7729
|
+
`Invalid filename: ${filename}. Must match pattern [a-zA-Z0-9_-]+.md (alphanumeric, hyphens, underscores only)`
|
|
7730
|
+
);
|
|
7731
|
+
}
|
|
7732
|
+
}
|
|
7733
|
+
async function listRules() {
|
|
7734
|
+
const rulesDir = (0, import_node_path68.join)(process.cwd(), ".rulesync", "rules");
|
|
7735
|
+
try {
|
|
7736
|
+
const files = await listDirectoryFiles(rulesDir);
|
|
7737
|
+
const mdFiles = files.filter((file) => file.endsWith(".md"));
|
|
7738
|
+
const rules = await Promise.all(
|
|
7739
|
+
mdFiles.map(async (file) => {
|
|
7740
|
+
try {
|
|
7741
|
+
const rule = await RulesyncRule.fromFile({
|
|
7742
|
+
relativeFilePath: file,
|
|
7743
|
+
validate: true
|
|
7744
|
+
});
|
|
7745
|
+
const frontmatter = rule.getFrontmatter();
|
|
7746
|
+
return {
|
|
7747
|
+
relativePathFromCwd: (0, import_node_path68.join)(".rulesync", "rules", file),
|
|
7748
|
+
frontmatter
|
|
7749
|
+
};
|
|
7750
|
+
} catch (error) {
|
|
7751
|
+
logger.error(`Failed to read rule file ${file}: ${formatError(error)}`);
|
|
7752
|
+
return null;
|
|
7753
|
+
}
|
|
7754
|
+
})
|
|
7755
|
+
);
|
|
7756
|
+
return rules.filter((rule) => rule !== null);
|
|
7757
|
+
} catch (error) {
|
|
7758
|
+
logger.error(`Failed to read rules directory: ${formatError(error)}`);
|
|
7759
|
+
return [];
|
|
7760
|
+
}
|
|
7761
|
+
}
|
|
7762
|
+
async function getRule({ relativePathFromCwd }) {
|
|
7763
|
+
validateRulePath(relativePathFromCwd);
|
|
7764
|
+
const filename = (0, import_node_path68.basename)(relativePathFromCwd);
|
|
7765
|
+
try {
|
|
7766
|
+
const rule = await RulesyncRule.fromFile({
|
|
7767
|
+
relativeFilePath: filename,
|
|
7768
|
+
validate: true
|
|
7769
|
+
});
|
|
7770
|
+
return {
|
|
7771
|
+
relativePathFromCwd: (0, import_node_path68.join)(".rulesync", "rules", filename),
|
|
7772
|
+
frontmatter: rule.getFrontmatter(),
|
|
7773
|
+
body: rule.getBody()
|
|
7774
|
+
};
|
|
7775
|
+
} catch (error) {
|
|
7776
|
+
throw new Error(`Failed to read rule file ${relativePathFromCwd}: ${formatError(error)}`, {
|
|
7777
|
+
cause: error
|
|
7778
|
+
});
|
|
7779
|
+
}
|
|
7780
|
+
}
|
|
7781
|
+
async function putRule({
|
|
7782
|
+
relativePathFromCwd,
|
|
7783
|
+
frontmatter,
|
|
7784
|
+
body
|
|
7785
|
+
}) {
|
|
7786
|
+
validateRulePath(relativePathFromCwd);
|
|
7787
|
+
const filename = (0, import_node_path68.basename)(relativePathFromCwd);
|
|
7788
|
+
const estimatedSize = JSON.stringify(frontmatter).length + body.length;
|
|
7789
|
+
if (estimatedSize > MAX_RULE_SIZE_BYTES) {
|
|
7790
|
+
throw new Error(
|
|
7791
|
+
`Rule size ${estimatedSize} bytes exceeds maximum ${MAX_RULE_SIZE_BYTES} bytes (1MB)`
|
|
7792
|
+
);
|
|
7793
|
+
}
|
|
7794
|
+
try {
|
|
7795
|
+
const existingRules = await listRules();
|
|
7796
|
+
const isUpdate = existingRules.some(
|
|
7797
|
+
(rule2) => rule2.relativePathFromCwd === (0, import_node_path68.join)(".rulesync", "rules", filename)
|
|
7798
|
+
);
|
|
7799
|
+
if (!isUpdate && existingRules.length >= MAX_RULES_COUNT) {
|
|
7800
|
+
throw new Error(`Maximum number of rules (${MAX_RULES_COUNT}) reached`);
|
|
7801
|
+
}
|
|
7802
|
+
const rule = new RulesyncRule({
|
|
7803
|
+
baseDir: process.cwd(),
|
|
7804
|
+
relativeDirPath: ".rulesync/rules",
|
|
7805
|
+
relativeFilePath: filename,
|
|
7806
|
+
frontmatter,
|
|
7807
|
+
body,
|
|
7808
|
+
validate: true
|
|
7809
|
+
});
|
|
7810
|
+
const rulesDir = (0, import_node_path68.join)(process.cwd(), ".rulesync", "rules");
|
|
7811
|
+
await ensureDir(rulesDir);
|
|
7812
|
+
await writeFileContent(rule.getFilePath(), rule.getFileContent());
|
|
7813
|
+
return {
|
|
7814
|
+
relativePathFromCwd: (0, import_node_path68.join)(".rulesync", "rules", filename),
|
|
7815
|
+
frontmatter: rule.getFrontmatter(),
|
|
7816
|
+
body: rule.getBody()
|
|
7817
|
+
};
|
|
7818
|
+
} catch (error) {
|
|
7819
|
+
throw new Error(`Failed to write rule file ${relativePathFromCwd}: ${formatError(error)}`, {
|
|
7820
|
+
cause: error
|
|
7821
|
+
});
|
|
7822
|
+
}
|
|
7823
|
+
}
|
|
7824
|
+
async function deleteRule({ relativePathFromCwd }) {
|
|
7825
|
+
validateRulePath(relativePathFromCwd);
|
|
7826
|
+
const filename = (0, import_node_path68.basename)(relativePathFromCwd);
|
|
7827
|
+
const fullPath = (0, import_node_path68.join)(process.cwd(), ".rulesync", "rules", filename);
|
|
7828
|
+
try {
|
|
7829
|
+
await removeFile(fullPath);
|
|
7830
|
+
return {
|
|
7831
|
+
relativePathFromCwd: (0, import_node_path68.join)(".rulesync", "rules", filename)
|
|
7832
|
+
};
|
|
7833
|
+
} catch (error) {
|
|
7834
|
+
throw new Error(`Failed to delete rule file ${relativePathFromCwd}: ${formatError(error)}`, {
|
|
7835
|
+
cause: error
|
|
7836
|
+
});
|
|
7837
|
+
}
|
|
7838
|
+
}
|
|
7839
|
+
async function mcpCommand({ version }) {
|
|
7840
|
+
const server = new import_fastmcp.FastMCP({
|
|
7841
|
+
name: "rulesync-mcp-server",
|
|
7842
|
+
// Type assertion is safe here because version comes from package.json which follows semver
|
|
7843
|
+
// eslint-disable-next-line no-type-assertion/no-type-assertion
|
|
7844
|
+
version
|
|
7845
|
+
});
|
|
7846
|
+
server.addTool({
|
|
7847
|
+
name: "listRules",
|
|
7848
|
+
description: "List all rules from .rulesync/rules/*.md with their frontmatter",
|
|
7849
|
+
parameters: import_mini23.z.object({}),
|
|
7850
|
+
execute: async () => {
|
|
7851
|
+
const rules = await listRules();
|
|
7852
|
+
const output = { rules };
|
|
7853
|
+
return JSON.stringify(output, null, 2);
|
|
7854
|
+
}
|
|
7855
|
+
});
|
|
7856
|
+
server.addTool({
|
|
7857
|
+
name: "getRule",
|
|
7858
|
+
description: "Get detailed information about a specific rule. relativePathFromCwd parameter is required.",
|
|
7859
|
+
parameters: import_mini23.z.object({
|
|
7860
|
+
relativePathFromCwd: import_mini23.z.string()
|
|
7861
|
+
}),
|
|
7862
|
+
execute: async (args) => {
|
|
7863
|
+
const result = await getRule({ relativePathFromCwd: args.relativePathFromCwd });
|
|
7864
|
+
return JSON.stringify(result, null, 2);
|
|
7865
|
+
}
|
|
7866
|
+
});
|
|
7867
|
+
server.addTool({
|
|
7868
|
+
name: "putRule",
|
|
7869
|
+
description: "Create or update a rule (upsert operation). relativePathFromCwd, frontmatter, and body parameters are required.",
|
|
7870
|
+
parameters: import_mini23.z.object({
|
|
7871
|
+
relativePathFromCwd: import_mini23.z.string(),
|
|
7872
|
+
frontmatter: RulesyncRuleFrontmatterSchema,
|
|
7873
|
+
body: import_mini23.z.string()
|
|
7874
|
+
}),
|
|
7875
|
+
execute: async (args) => {
|
|
7876
|
+
const result = await putRule({
|
|
7877
|
+
relativePathFromCwd: args.relativePathFromCwd,
|
|
7878
|
+
frontmatter: args.frontmatter,
|
|
7879
|
+
body: args.body
|
|
7880
|
+
});
|
|
7881
|
+
return JSON.stringify(result, null, 2);
|
|
7882
|
+
}
|
|
7883
|
+
});
|
|
7884
|
+
server.addTool({
|
|
7885
|
+
name: "deleteRule",
|
|
7886
|
+
description: "Delete a rule file. relativePathFromCwd parameter is required.",
|
|
7887
|
+
parameters: import_mini23.z.object({
|
|
7888
|
+
relativePathFromCwd: import_mini23.z.string()
|
|
7889
|
+
}),
|
|
7890
|
+
execute: async (args) => {
|
|
7891
|
+
const result = await deleteRule({ relativePathFromCwd: args.relativePathFromCwd });
|
|
7892
|
+
return JSON.stringify(result, null, 2);
|
|
7893
|
+
}
|
|
7894
|
+
});
|
|
7895
|
+
logger.info("Rulesync MCP server started via stdio");
|
|
7896
|
+
void server.start({
|
|
7897
|
+
transportType: "stdio"
|
|
7898
|
+
});
|
|
7899
|
+
}
|
|
7900
|
+
|
|
7709
7901
|
// src/cli/index.ts
|
|
7710
|
-
var getVersion = () => "3.
|
|
7902
|
+
var getVersion = () => "3.19.0";
|
|
7711
7903
|
var main = async () => {
|
|
7712
7904
|
const program = new import_commander.Command();
|
|
7713
7905
|
const version = getVersion();
|
|
@@ -7745,7 +7937,15 @@ var main = async () => {
|
|
|
7745
7937
|
experimentalGlobal: options.experimentalGlobal
|
|
7746
7938
|
});
|
|
7747
7939
|
} catch (error) {
|
|
7748
|
-
logger.error(
|
|
7940
|
+
logger.error(formatError(error));
|
|
7941
|
+
process.exit(1);
|
|
7942
|
+
}
|
|
7943
|
+
});
|
|
7944
|
+
program.command("mcp").description("Start MCP server for rulesync").action(async () => {
|
|
7945
|
+
try {
|
|
7946
|
+
await mcpCommand({ version });
|
|
7947
|
+
} catch (error) {
|
|
7948
|
+
logger.error(formatError(error));
|
|
7749
7949
|
process.exit(1);
|
|
7750
7950
|
}
|
|
7751
7951
|
});
|
|
@@ -7800,13 +8000,13 @@ var main = async () => {
|
|
|
7800
8000
|
experimentalSimulateSubagents: options.experimentalSimulateSubagents
|
|
7801
8001
|
});
|
|
7802
8002
|
} catch (error) {
|
|
7803
|
-
logger.error(
|
|
8003
|
+
logger.error(formatError(error));
|
|
7804
8004
|
process.exit(1);
|
|
7805
8005
|
}
|
|
7806
8006
|
});
|
|
7807
8007
|
program.parse();
|
|
7808
8008
|
};
|
|
7809
8009
|
main().catch((error) => {
|
|
7810
|
-
logger.error(
|
|
8010
|
+
logger.error(formatError(error));
|
|
7811
8011
|
process.exit(1);
|
|
7812
8012
|
});
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,26 @@ var FeatureSchema = z.enum(ALL_FEATURES);
|
|
|
14
14
|
var FeaturesSchema = z.array(FeatureSchema);
|
|
15
15
|
var RulesyncFeaturesSchema = z.array(z.enum(ALL_FEATURES_WITH_WILDCARD));
|
|
16
16
|
|
|
17
|
+
// src/utils/error.ts
|
|
18
|
+
import { ZodError } from "zod";
|
|
19
|
+
function isZodErrorLike(error) {
|
|
20
|
+
return error !== null && typeof error === "object" && "issues" in error && Array.isArray(error.issues) && error.issues.every(
|
|
21
|
+
(issue) => issue !== null && typeof issue === "object" && "path" in issue && Array.isArray(issue.path) && "message" in issue && typeof issue.message === "string"
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
function formatError(error) {
|
|
25
|
+
if (error instanceof ZodError || isZodErrorLike(error)) {
|
|
26
|
+
return error.issues.map((issue) => {
|
|
27
|
+
const path2 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
28
|
+
return `${path2}${issue.message}`;
|
|
29
|
+
}).join("; ");
|
|
30
|
+
}
|
|
31
|
+
if (error instanceof Error) {
|
|
32
|
+
return `${error.name}: ${error.message}`;
|
|
33
|
+
}
|
|
34
|
+
return String(error);
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
// src/utils/logger.ts
|
|
18
38
|
import { consola } from "consola";
|
|
19
39
|
|
|
@@ -199,26 +219,6 @@ var FeatureProcessor = class {
|
|
|
199
219
|
}
|
|
200
220
|
};
|
|
201
221
|
|
|
202
|
-
// src/utils/error.ts
|
|
203
|
-
import { ZodError } from "zod";
|
|
204
|
-
function isZodErrorLike(error) {
|
|
205
|
-
return error !== null && typeof error === "object" && "issues" in error && Array.isArray(error.issues) && error.issues.every(
|
|
206
|
-
(issue) => issue !== null && typeof issue === "object" && "path" in issue && Array.isArray(issue.path) && "message" in issue && typeof issue.message === "string"
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
function formatError(error) {
|
|
210
|
-
if (error instanceof ZodError || isZodErrorLike(error)) {
|
|
211
|
-
return error.issues.map((issue) => {
|
|
212
|
-
const path2 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
213
|
-
return `${path2}${issue.message}`;
|
|
214
|
-
}).join("; ");
|
|
215
|
-
}
|
|
216
|
-
if (error instanceof Error) {
|
|
217
|
-
return `${error.name}: ${error.message}`;
|
|
218
|
-
}
|
|
219
|
-
return String(error);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
222
|
// src/commands/agentsmd-command.ts
|
|
223
223
|
import { basename as basename3, join as join3 } from "path";
|
|
224
224
|
|
|
@@ -7683,8 +7683,200 @@ Attention, again, you are just the planner, so though you can read any files and
|
|
|
7683
7683
|
}
|
|
7684
7684
|
}
|
|
7685
7685
|
|
|
7686
|
+
// src/cli/commands/mcp.ts
|
|
7687
|
+
import { basename as basename18, join as join66 } from "path";
|
|
7688
|
+
import { FastMCP } from "fastmcp";
|
|
7689
|
+
import { z as z23 } from "zod/mini";
|
|
7690
|
+
var MAX_RULE_SIZE_BYTES = 1024 * 1024;
|
|
7691
|
+
var MAX_RULES_COUNT = 1e3;
|
|
7692
|
+
var RULES_DIR_PREFIX = ".rulesync/rules";
|
|
7693
|
+
function validateRulePath(relativePathFromCwd) {
|
|
7694
|
+
const normalizedPath = relativePathFromCwd.replace(/\\/g, "/");
|
|
7695
|
+
if (!normalizedPath.startsWith(`${RULES_DIR_PREFIX}/`)) {
|
|
7696
|
+
throw new Error(`Invalid rule path: must be within ${RULES_DIR_PREFIX}/ directory`);
|
|
7697
|
+
}
|
|
7698
|
+
try {
|
|
7699
|
+
resolvePath(normalizedPath, process.cwd());
|
|
7700
|
+
} catch (error) {
|
|
7701
|
+
throw new Error(`Path validation failed: ${formatError(error)}`, { cause: error });
|
|
7702
|
+
}
|
|
7703
|
+
const filename = basename18(normalizedPath);
|
|
7704
|
+
if (!/^[a-zA-Z0-9_-]+\.md$/.test(filename)) {
|
|
7705
|
+
throw new Error(
|
|
7706
|
+
`Invalid filename: ${filename}. Must match pattern [a-zA-Z0-9_-]+.md (alphanumeric, hyphens, underscores only)`
|
|
7707
|
+
);
|
|
7708
|
+
}
|
|
7709
|
+
}
|
|
7710
|
+
async function listRules() {
|
|
7711
|
+
const rulesDir = join66(process.cwd(), ".rulesync", "rules");
|
|
7712
|
+
try {
|
|
7713
|
+
const files = await listDirectoryFiles(rulesDir);
|
|
7714
|
+
const mdFiles = files.filter((file) => file.endsWith(".md"));
|
|
7715
|
+
const rules = await Promise.all(
|
|
7716
|
+
mdFiles.map(async (file) => {
|
|
7717
|
+
try {
|
|
7718
|
+
const rule = await RulesyncRule.fromFile({
|
|
7719
|
+
relativeFilePath: file,
|
|
7720
|
+
validate: true
|
|
7721
|
+
});
|
|
7722
|
+
const frontmatter = rule.getFrontmatter();
|
|
7723
|
+
return {
|
|
7724
|
+
relativePathFromCwd: join66(".rulesync", "rules", file),
|
|
7725
|
+
frontmatter
|
|
7726
|
+
};
|
|
7727
|
+
} catch (error) {
|
|
7728
|
+
logger.error(`Failed to read rule file ${file}: ${formatError(error)}`);
|
|
7729
|
+
return null;
|
|
7730
|
+
}
|
|
7731
|
+
})
|
|
7732
|
+
);
|
|
7733
|
+
return rules.filter((rule) => rule !== null);
|
|
7734
|
+
} catch (error) {
|
|
7735
|
+
logger.error(`Failed to read rules directory: ${formatError(error)}`);
|
|
7736
|
+
return [];
|
|
7737
|
+
}
|
|
7738
|
+
}
|
|
7739
|
+
async function getRule({ relativePathFromCwd }) {
|
|
7740
|
+
validateRulePath(relativePathFromCwd);
|
|
7741
|
+
const filename = basename18(relativePathFromCwd);
|
|
7742
|
+
try {
|
|
7743
|
+
const rule = await RulesyncRule.fromFile({
|
|
7744
|
+
relativeFilePath: filename,
|
|
7745
|
+
validate: true
|
|
7746
|
+
});
|
|
7747
|
+
return {
|
|
7748
|
+
relativePathFromCwd: join66(".rulesync", "rules", filename),
|
|
7749
|
+
frontmatter: rule.getFrontmatter(),
|
|
7750
|
+
body: rule.getBody()
|
|
7751
|
+
};
|
|
7752
|
+
} catch (error) {
|
|
7753
|
+
throw new Error(`Failed to read rule file ${relativePathFromCwd}: ${formatError(error)}`, {
|
|
7754
|
+
cause: error
|
|
7755
|
+
});
|
|
7756
|
+
}
|
|
7757
|
+
}
|
|
7758
|
+
async function putRule({
|
|
7759
|
+
relativePathFromCwd,
|
|
7760
|
+
frontmatter,
|
|
7761
|
+
body
|
|
7762
|
+
}) {
|
|
7763
|
+
validateRulePath(relativePathFromCwd);
|
|
7764
|
+
const filename = basename18(relativePathFromCwd);
|
|
7765
|
+
const estimatedSize = JSON.stringify(frontmatter).length + body.length;
|
|
7766
|
+
if (estimatedSize > MAX_RULE_SIZE_BYTES) {
|
|
7767
|
+
throw new Error(
|
|
7768
|
+
`Rule size ${estimatedSize} bytes exceeds maximum ${MAX_RULE_SIZE_BYTES} bytes (1MB)`
|
|
7769
|
+
);
|
|
7770
|
+
}
|
|
7771
|
+
try {
|
|
7772
|
+
const existingRules = await listRules();
|
|
7773
|
+
const isUpdate = existingRules.some(
|
|
7774
|
+
(rule2) => rule2.relativePathFromCwd === join66(".rulesync", "rules", filename)
|
|
7775
|
+
);
|
|
7776
|
+
if (!isUpdate && existingRules.length >= MAX_RULES_COUNT) {
|
|
7777
|
+
throw new Error(`Maximum number of rules (${MAX_RULES_COUNT}) reached`);
|
|
7778
|
+
}
|
|
7779
|
+
const rule = new RulesyncRule({
|
|
7780
|
+
baseDir: process.cwd(),
|
|
7781
|
+
relativeDirPath: ".rulesync/rules",
|
|
7782
|
+
relativeFilePath: filename,
|
|
7783
|
+
frontmatter,
|
|
7784
|
+
body,
|
|
7785
|
+
validate: true
|
|
7786
|
+
});
|
|
7787
|
+
const rulesDir = join66(process.cwd(), ".rulesync", "rules");
|
|
7788
|
+
await ensureDir(rulesDir);
|
|
7789
|
+
await writeFileContent(rule.getFilePath(), rule.getFileContent());
|
|
7790
|
+
return {
|
|
7791
|
+
relativePathFromCwd: join66(".rulesync", "rules", filename),
|
|
7792
|
+
frontmatter: rule.getFrontmatter(),
|
|
7793
|
+
body: rule.getBody()
|
|
7794
|
+
};
|
|
7795
|
+
} catch (error) {
|
|
7796
|
+
throw new Error(`Failed to write rule file ${relativePathFromCwd}: ${formatError(error)}`, {
|
|
7797
|
+
cause: error
|
|
7798
|
+
});
|
|
7799
|
+
}
|
|
7800
|
+
}
|
|
7801
|
+
async function deleteRule({ relativePathFromCwd }) {
|
|
7802
|
+
validateRulePath(relativePathFromCwd);
|
|
7803
|
+
const filename = basename18(relativePathFromCwd);
|
|
7804
|
+
const fullPath = join66(process.cwd(), ".rulesync", "rules", filename);
|
|
7805
|
+
try {
|
|
7806
|
+
await removeFile(fullPath);
|
|
7807
|
+
return {
|
|
7808
|
+
relativePathFromCwd: join66(".rulesync", "rules", filename)
|
|
7809
|
+
};
|
|
7810
|
+
} catch (error) {
|
|
7811
|
+
throw new Error(`Failed to delete rule file ${relativePathFromCwd}: ${formatError(error)}`, {
|
|
7812
|
+
cause: error
|
|
7813
|
+
});
|
|
7814
|
+
}
|
|
7815
|
+
}
|
|
7816
|
+
async function mcpCommand({ version }) {
|
|
7817
|
+
const server = new FastMCP({
|
|
7818
|
+
name: "rulesync-mcp-server",
|
|
7819
|
+
// Type assertion is safe here because version comes from package.json which follows semver
|
|
7820
|
+
// eslint-disable-next-line no-type-assertion/no-type-assertion
|
|
7821
|
+
version
|
|
7822
|
+
});
|
|
7823
|
+
server.addTool({
|
|
7824
|
+
name: "listRules",
|
|
7825
|
+
description: "List all rules from .rulesync/rules/*.md with their frontmatter",
|
|
7826
|
+
parameters: z23.object({}),
|
|
7827
|
+
execute: async () => {
|
|
7828
|
+
const rules = await listRules();
|
|
7829
|
+
const output = { rules };
|
|
7830
|
+
return JSON.stringify(output, null, 2);
|
|
7831
|
+
}
|
|
7832
|
+
});
|
|
7833
|
+
server.addTool({
|
|
7834
|
+
name: "getRule",
|
|
7835
|
+
description: "Get detailed information about a specific rule. relativePathFromCwd parameter is required.",
|
|
7836
|
+
parameters: z23.object({
|
|
7837
|
+
relativePathFromCwd: z23.string()
|
|
7838
|
+
}),
|
|
7839
|
+
execute: async (args) => {
|
|
7840
|
+
const result = await getRule({ relativePathFromCwd: args.relativePathFromCwd });
|
|
7841
|
+
return JSON.stringify(result, null, 2);
|
|
7842
|
+
}
|
|
7843
|
+
});
|
|
7844
|
+
server.addTool({
|
|
7845
|
+
name: "putRule",
|
|
7846
|
+
description: "Create or update a rule (upsert operation). relativePathFromCwd, frontmatter, and body parameters are required.",
|
|
7847
|
+
parameters: z23.object({
|
|
7848
|
+
relativePathFromCwd: z23.string(),
|
|
7849
|
+
frontmatter: RulesyncRuleFrontmatterSchema,
|
|
7850
|
+
body: z23.string()
|
|
7851
|
+
}),
|
|
7852
|
+
execute: async (args) => {
|
|
7853
|
+
const result = await putRule({
|
|
7854
|
+
relativePathFromCwd: args.relativePathFromCwd,
|
|
7855
|
+
frontmatter: args.frontmatter,
|
|
7856
|
+
body: args.body
|
|
7857
|
+
});
|
|
7858
|
+
return JSON.stringify(result, null, 2);
|
|
7859
|
+
}
|
|
7860
|
+
});
|
|
7861
|
+
server.addTool({
|
|
7862
|
+
name: "deleteRule",
|
|
7863
|
+
description: "Delete a rule file. relativePathFromCwd parameter is required.",
|
|
7864
|
+
parameters: z23.object({
|
|
7865
|
+
relativePathFromCwd: z23.string()
|
|
7866
|
+
}),
|
|
7867
|
+
execute: async (args) => {
|
|
7868
|
+
const result = await deleteRule({ relativePathFromCwd: args.relativePathFromCwd });
|
|
7869
|
+
return JSON.stringify(result, null, 2);
|
|
7870
|
+
}
|
|
7871
|
+
});
|
|
7872
|
+
logger.info("Rulesync MCP server started via stdio");
|
|
7873
|
+
void server.start({
|
|
7874
|
+
transportType: "stdio"
|
|
7875
|
+
});
|
|
7876
|
+
}
|
|
7877
|
+
|
|
7686
7878
|
// src/cli/index.ts
|
|
7687
|
-
var getVersion = () => "3.
|
|
7879
|
+
var getVersion = () => "3.19.0";
|
|
7688
7880
|
var main = async () => {
|
|
7689
7881
|
const program = new Command();
|
|
7690
7882
|
const version = getVersion();
|
|
@@ -7722,7 +7914,15 @@ var main = async () => {
|
|
|
7722
7914
|
experimentalGlobal: options.experimentalGlobal
|
|
7723
7915
|
});
|
|
7724
7916
|
} catch (error) {
|
|
7725
|
-
logger.error(
|
|
7917
|
+
logger.error(formatError(error));
|
|
7918
|
+
process.exit(1);
|
|
7919
|
+
}
|
|
7920
|
+
});
|
|
7921
|
+
program.command("mcp").description("Start MCP server for rulesync").action(async () => {
|
|
7922
|
+
try {
|
|
7923
|
+
await mcpCommand({ version });
|
|
7924
|
+
} catch (error) {
|
|
7925
|
+
logger.error(formatError(error));
|
|
7726
7926
|
process.exit(1);
|
|
7727
7927
|
}
|
|
7728
7928
|
});
|
|
@@ -7777,13 +7977,13 @@ var main = async () => {
|
|
|
7777
7977
|
experimentalSimulateSubagents: options.experimentalSimulateSubagents
|
|
7778
7978
|
});
|
|
7779
7979
|
} catch (error) {
|
|
7780
|
-
logger.error(
|
|
7980
|
+
logger.error(formatError(error));
|
|
7781
7981
|
process.exit(1);
|
|
7782
7982
|
}
|
|
7783
7983
|
});
|
|
7784
7984
|
program.parse();
|
|
7785
7985
|
};
|
|
7786
7986
|
main().catch((error) => {
|
|
7787
|
-
logger.error(
|
|
7987
|
+
logger.error(formatError(error));
|
|
7788
7988
|
process.exit(1);
|
|
7789
7989
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rulesync",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.19.0",
|
|
4
4
|
"description": "Unified AI rules management CLI tool that generates configuration files for various AI development tools",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@kimuson/modular-mcp": "0.0.5",
|
|
40
|
+
"fastmcp": "^3.22.0",
|
|
40
41
|
"chokidar": "4.0.3",
|
|
41
42
|
"commander": "14.0.2",
|
|
42
43
|
"consola": "3.4.2",
|