rulesync 3.28.1 → 3.29.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 CHANGED
@@ -213,11 +213,28 @@ npx rulesync gitignore
213
213
 
214
214
  You can configure Rulesync by creating a `rulesync.jsonc` file in the root of your project.
215
215
 
216
+ ### JSON Schema Support
217
+
218
+ Rulesync provides a JSON Schema for editor validation and autocompletion. Add the `$schema` property to your `rulesync.jsonc`:
219
+
220
+ ```jsonc
221
+ // rulesync.jsonc
222
+ {
223
+ "$schema": "https://raw.githubusercontent.com/dyoshikawa/rulesync/refs/heads/main/config-schema.json",
224
+ "targets": ["claudecode"],
225
+ "features": ["rules"]
226
+ }
227
+ ```
228
+
229
+ ### Configuration Options
230
+
216
231
  Example:
217
232
 
218
233
  ```jsonc
219
234
  // rulesync.jsonc
220
235
  {
236
+ "$schema": "https://raw.githubusercontent.com/dyoshikawa/rulesync/refs/heads/main/config-schema.json",
237
+
221
238
  // List of tools to generate configurations for. You can specify "*" to generate all tools.
222
239
  "targets": ["cursor", "claudecode", "geminicli", "opencode", "codexcli"],
223
240
 
@@ -670,6 +687,16 @@ The Rulesync MCP server provides the following tools:
670
687
 
671
688
  </details>
672
689
 
690
+ <details>
691
+ <summary>Skills Management</summary>
692
+
693
+ - `listSkills` - List all skill directories
694
+ - `getSkill` - Get a specific skill (SKILL.md and other files)
695
+ - `putSkill` - Create or update a skill directory
696
+ - `deleteSkill` - Delete a skill directory
697
+
698
+ </details>
699
+
673
700
  <details>
674
701
  <summary>Ignore Files Management</summary>
675
702
 
package/dist/index.cjs CHANGED
@@ -290,6 +290,10 @@ var ConfigParamsSchema = import_mini3.z.object({
290
290
  experimentalSimulateSubagents: (0, import_mini3.optional)(import_mini3.z.boolean())
291
291
  });
292
292
  var PartialConfigParamsSchema = import_mini3.z.partial(ConfigParamsSchema);
293
+ var ConfigFileSchema = import_mini3.z.object({
294
+ $schema: (0, import_mini3.optional)(import_mini3.z.string()),
295
+ ...import_mini3.z.partial(ConfigParamsSchema).shape
296
+ });
293
297
  var RequiredConfigParamsSchema = import_mini3.z.required(ConfigParamsSchema);
294
298
  var Config = class {
295
299
  baseDirs;
@@ -419,7 +423,9 @@ var ConfigResolver = class {
419
423
  try {
420
424
  const fileContent = await readFileContent(validatedConfigPath);
421
425
  const jsonData = (0, import_jsonc_parser.parse)(fileContent);
422
- configByFile = PartialConfigParamsSchema.parse(jsonData);
426
+ const parsed = ConfigFileSchema.parse(jsonData);
427
+ const { $schema: _schema, ...configParams2 } = parsed;
428
+ configByFile = configParams2;
423
429
  } catch (error) {
424
430
  logger.error(`Failed to load config file: ${formatError(error)}`);
425
431
  throw error;
@@ -665,6 +671,13 @@ var AiFile = class {
665
671
  setFileContent(newFileContent) {
666
672
  this.fileContent = newFileContent;
667
673
  }
674
+ /**
675
+ * Returns whether this file can be deleted by rulesync.
676
+ * Override in subclasses that should not be deleted (e.g., user-managed config files).
677
+ */
678
+ isDeletable() {
679
+ return true;
680
+ }
668
681
  };
669
682
 
670
683
  // src/features/commands/tool-command.ts
@@ -2324,6 +2337,13 @@ var ClaudecodeIgnore = class _ClaudecodeIgnore extends ToolIgnore {
2324
2337
  relativeFilePath: "settings.local.json"
2325
2338
  };
2326
2339
  }
2340
+ /**
2341
+ * ClaudecodeIgnore uses settings.local.json which is a user-managed config file.
2342
+ * It should not be deleted by rulesync.
2343
+ */
2344
+ isDeletable() {
2345
+ return false;
2346
+ }
2327
2347
  toRulesyncIgnore() {
2328
2348
  const rulesyncPatterns = this.patterns.map((pattern) => {
2329
2349
  if (pattern.startsWith("Read(") && pattern.endsWith(")")) {
@@ -2814,7 +2834,7 @@ var IgnoreProcessor = class extends FeatureProcessor {
2814
2834
  try {
2815
2835
  const toolIgnores = await this.loadToolIgnores();
2816
2836
  if (forDeletion) {
2817
- return toolIgnores.filter((toolFile) => !(toolFile instanceof ClaudecodeIgnore));
2837
+ return toolIgnores.filter((toolFile) => toolFile.isDeletable());
2818
2838
  }
2819
2839
  return toolIgnores;
2820
2840
  } catch (error) {
@@ -3316,6 +3336,14 @@ var ClaudecodeMcp = class _ClaudecodeMcp extends ToolMcp {
3316
3336
  getJson() {
3317
3337
  return this.json;
3318
3338
  }
3339
+ /**
3340
+ * In global mode, ~/.claude/.claude.json should not be deleted
3341
+ * as it may contain other user settings.
3342
+ * In local mode, .mcp.json can be safely deleted.
3343
+ */
3344
+ isDeletable() {
3345
+ return !this.global;
3346
+ }
3319
3347
  static getSettablePaths({ global } = {}) {
3320
3348
  if (global) {
3321
3349
  return {
@@ -3898,6 +3926,12 @@ var OpencodeMcp = class _OpencodeMcp extends ToolMcp {
3898
3926
  getJson() {
3899
3927
  return this.json;
3900
3928
  }
3929
+ /**
3930
+ * opencode.json may contain other settings, so it should not be deleted.
3931
+ */
3932
+ isDeletable() {
3933
+ return false;
3934
+ }
3901
3935
  static getSettablePaths({ global } = {}) {
3902
3936
  if (global) {
3903
3937
  return {
@@ -4182,11 +4216,7 @@ var McpProcessor = class extends FeatureProcessor {
4182
4216
  })();
4183
4217
  logger.info(`Successfully loaded ${toolMcps.length} ${this.toolTarget} MCP files`);
4184
4218
  if (forDeletion) {
4185
- let filteredMcps = toolMcps.filter((toolFile) => !(toolFile instanceof OpencodeMcp));
4186
- if (this.global) {
4187
- filteredMcps = filteredMcps.filter((toolFile) => !(toolFile instanceof ClaudecodeMcp));
4188
- }
4189
- return filteredMcps;
4219
+ return toolMcps.filter((toolFile) => toolFile.isDeletable());
4190
4220
  }
4191
4221
  return toolMcps;
4192
4222
  } catch (error) {
@@ -4567,20 +4597,13 @@ var SimulatedSkill = class extends ToolSkill {
4567
4597
  name: rulesyncFrontmatter.name,
4568
4598
  description: rulesyncFrontmatter.description
4569
4599
  };
4570
- const otherFiles = rulesyncSkill.getOtherFiles();
4571
- if (otherFiles.length > 0) {
4572
- logger.warn(
4573
- `Skill "${rulesyncFrontmatter.name}" has ${otherFiles.length} additional file(s) that will be ignored for simulated skill generation.`
4574
- );
4575
- }
4576
4600
  return {
4577
4601
  baseDir: rulesyncSkill.getBaseDir(),
4578
4602
  relativeDirPath: this.getSettablePaths().relativeDirPath,
4579
4603
  dirName: rulesyncSkill.getDirName(),
4580
4604
  frontmatter: simulatedFrontmatter,
4581
4605
  body: rulesyncSkill.getBody(),
4582
- otherFiles: [],
4583
- // Simulated skills ignore otherFiles
4606
+ otherFiles: rulesyncSkill.getOtherFiles(),
4584
4607
  validate
4585
4608
  };
4586
4609
  }
@@ -4602,14 +4625,19 @@ var SimulatedSkill = class extends ToolSkill {
4602
4625
  if (!result.success) {
4603
4626
  throw new Error(`Invalid frontmatter in ${skillFilePath}: ${formatError(result.error)}`);
4604
4627
  }
4628
+ const otherFiles = await this.collectOtherFiles(
4629
+ baseDir,
4630
+ actualRelativeDirPath,
4631
+ dirName,
4632
+ SKILL_FILE_NAME
4633
+ );
4605
4634
  return {
4606
4635
  baseDir,
4607
4636
  relativeDirPath: actualRelativeDirPath,
4608
4637
  dirName,
4609
4638
  frontmatter: result.data,
4610
4639
  body: content.trim(),
4611
- otherFiles: [],
4612
- // Simulated skills ignore otherFiles
4640
+ otherFiles,
4613
4641
  validate: true
4614
4642
  };
4615
4643
  }
@@ -10024,13 +10052,247 @@ var ruleTools = {
10024
10052
  }
10025
10053
  };
10026
10054
 
10027
- // src/mcp/subagents.ts
10055
+ // src/mcp/skills.ts
10028
10056
  var import_node_path88 = require("path");
10029
10057
  var import_mini35 = require("zod/mini");
10058
+ var maxSkillSizeBytes = 1024 * 1024;
10059
+ var maxSkillsCount = 1e3;
10060
+ function aiDirFileToMcpSkillFile(file) {
10061
+ return {
10062
+ name: file.relativeFilePathToDirPath,
10063
+ body: file.fileBuffer.toString("utf-8")
10064
+ };
10065
+ }
10066
+ function mcpSkillFileToAiDirFile(file) {
10067
+ return {
10068
+ relativeFilePathToDirPath: file.name,
10069
+ fileBuffer: Buffer.from(file.body, "utf-8")
10070
+ };
10071
+ }
10072
+ function extractDirName(relativeDirPathFromCwd) {
10073
+ const dirName = (0, import_node_path88.basename)(relativeDirPathFromCwd);
10074
+ if (!dirName) {
10075
+ throw new Error(`Invalid path: ${relativeDirPathFromCwd}`);
10076
+ }
10077
+ return dirName;
10078
+ }
10079
+ async function listSkills() {
10080
+ const skillsDir = (0, import_node_path88.join)(process.cwd(), RULESYNC_SKILLS_RELATIVE_DIR_PATH);
10081
+ try {
10082
+ const skillDirPaths = await findFilesByGlobs((0, import_node_path88.join)(skillsDir, "*"), { type: "dir" });
10083
+ const skills = await Promise.all(
10084
+ skillDirPaths.map(async (dirPath) => {
10085
+ const dirName = (0, import_node_path88.basename)(dirPath);
10086
+ if (!dirName) return null;
10087
+ try {
10088
+ const skill = await RulesyncSkill.fromDir({
10089
+ dirName
10090
+ });
10091
+ const frontmatter = skill.getFrontmatter();
10092
+ return {
10093
+ relativeDirPathFromCwd: (0, import_node_path88.join)(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName),
10094
+ frontmatter
10095
+ };
10096
+ } catch (error) {
10097
+ logger.error(`Failed to read skill directory ${dirName}: ${formatError(error)}`);
10098
+ return null;
10099
+ }
10100
+ })
10101
+ );
10102
+ return skills.filter((skill) => skill !== null);
10103
+ } catch (error) {
10104
+ logger.error(`Failed to read skills directory: ${formatError(error)}`);
10105
+ return [];
10106
+ }
10107
+ }
10108
+ async function getSkill({ relativeDirPathFromCwd }) {
10109
+ checkPathTraversal({
10110
+ relativePath: relativeDirPathFromCwd,
10111
+ intendedRootDir: process.cwd()
10112
+ });
10113
+ const dirName = extractDirName(relativeDirPathFromCwd);
10114
+ try {
10115
+ const skill = await RulesyncSkill.fromDir({
10116
+ dirName
10117
+ });
10118
+ return {
10119
+ relativeDirPathFromCwd: (0, import_node_path88.join)(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName),
10120
+ frontmatter: skill.getFrontmatter(),
10121
+ body: skill.getBody(),
10122
+ otherFiles: skill.getOtherFiles().map(aiDirFileToMcpSkillFile)
10123
+ };
10124
+ } catch (error) {
10125
+ throw new Error(
10126
+ `Failed to read skill directory ${relativeDirPathFromCwd}: ${formatError(error)}`,
10127
+ {
10128
+ cause: error
10129
+ }
10130
+ );
10131
+ }
10132
+ }
10133
+ async function putSkill({
10134
+ relativeDirPathFromCwd,
10135
+ frontmatter,
10136
+ body,
10137
+ otherFiles = []
10138
+ }) {
10139
+ checkPathTraversal({
10140
+ relativePath: relativeDirPathFromCwd,
10141
+ intendedRootDir: process.cwd()
10142
+ });
10143
+ const dirName = extractDirName(relativeDirPathFromCwd);
10144
+ const estimatedSize = JSON.stringify(frontmatter).length + body.length + otherFiles.reduce((acc, file) => acc + file.name.length + file.body.length, 0);
10145
+ if (estimatedSize > maxSkillSizeBytes) {
10146
+ throw new Error(
10147
+ `Skill size ${estimatedSize} bytes exceeds maximum ${maxSkillSizeBytes} bytes (1MB)`
10148
+ );
10149
+ }
10150
+ try {
10151
+ const existingSkills = await listSkills();
10152
+ const isUpdate = existingSkills.some(
10153
+ (skill2) => skill2.relativeDirPathFromCwd === (0, import_node_path88.join)(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName)
10154
+ );
10155
+ if (!isUpdate && existingSkills.length >= maxSkillsCount) {
10156
+ throw new Error(`Maximum number of skills (${maxSkillsCount}) reached`);
10157
+ }
10158
+ const aiDirFiles = otherFiles.map(mcpSkillFileToAiDirFile);
10159
+ const skill = new RulesyncSkill({
10160
+ baseDir: process.cwd(),
10161
+ relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
10162
+ dirName,
10163
+ frontmatter,
10164
+ body,
10165
+ otherFiles: aiDirFiles,
10166
+ validate: true
10167
+ });
10168
+ const skillDirPath = (0, import_node_path88.join)(process.cwd(), RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName);
10169
+ await ensureDir(skillDirPath);
10170
+ const skillFilePath = (0, import_node_path88.join)(skillDirPath, SKILL_FILE_NAME);
10171
+ const skillFileContent = stringifyFrontmatter(body, frontmatter);
10172
+ await writeFileContent(skillFilePath, skillFileContent);
10173
+ for (const file of otherFiles) {
10174
+ checkPathTraversal({
10175
+ relativePath: file.name,
10176
+ intendedRootDir: skillDirPath
10177
+ });
10178
+ const filePath = (0, import_node_path88.join)(skillDirPath, file.name);
10179
+ const fileDir = (0, import_node_path88.join)(skillDirPath, (0, import_node_path88.dirname)(file.name));
10180
+ if (fileDir !== skillDirPath) {
10181
+ await ensureDir(fileDir);
10182
+ }
10183
+ await writeFileContent(filePath, file.body);
10184
+ }
10185
+ return {
10186
+ relativeDirPathFromCwd: (0, import_node_path88.join)(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName),
10187
+ frontmatter: skill.getFrontmatter(),
10188
+ body: skill.getBody(),
10189
+ otherFiles: skill.getOtherFiles().map(aiDirFileToMcpSkillFile)
10190
+ };
10191
+ } catch (error) {
10192
+ throw new Error(
10193
+ `Failed to write skill directory ${relativeDirPathFromCwd}: ${formatError(error)}`,
10194
+ {
10195
+ cause: error
10196
+ }
10197
+ );
10198
+ }
10199
+ }
10200
+ async function deleteSkill({
10201
+ relativeDirPathFromCwd
10202
+ }) {
10203
+ checkPathTraversal({
10204
+ relativePath: relativeDirPathFromCwd,
10205
+ intendedRootDir: process.cwd()
10206
+ });
10207
+ const dirName = extractDirName(relativeDirPathFromCwd);
10208
+ const skillDirPath = (0, import_node_path88.join)(process.cwd(), RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName);
10209
+ try {
10210
+ if (await directoryExists(skillDirPath)) {
10211
+ await removeDirectory(skillDirPath);
10212
+ }
10213
+ return {
10214
+ relativeDirPathFromCwd: (0, import_node_path88.join)(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName)
10215
+ };
10216
+ } catch (error) {
10217
+ throw new Error(
10218
+ `Failed to delete skill directory ${relativeDirPathFromCwd}: ${formatError(error)}`,
10219
+ {
10220
+ cause: error
10221
+ }
10222
+ );
10223
+ }
10224
+ }
10225
+ var McpSkillFileSchema = import_mini35.z.object({
10226
+ name: import_mini35.z.string(),
10227
+ body: import_mini35.z.string()
10228
+ });
10229
+ var skillToolSchemas = {
10230
+ listSkills: import_mini35.z.object({}),
10231
+ getSkill: import_mini35.z.object({
10232
+ relativeDirPathFromCwd: import_mini35.z.string()
10233
+ }),
10234
+ putSkill: import_mini35.z.object({
10235
+ relativeDirPathFromCwd: import_mini35.z.string(),
10236
+ frontmatter: RulesyncSkillFrontmatterSchema,
10237
+ body: import_mini35.z.string(),
10238
+ otherFiles: import_mini35.z.optional(import_mini35.z.array(McpSkillFileSchema))
10239
+ }),
10240
+ deleteSkill: import_mini35.z.object({
10241
+ relativeDirPathFromCwd: import_mini35.z.string()
10242
+ })
10243
+ };
10244
+ var skillTools = {
10245
+ listSkills: {
10246
+ name: "listSkills",
10247
+ description: `List all skills from ${(0, import_node_path88.join)(RULESYNC_SKILLS_RELATIVE_DIR_PATH, "*", SKILL_FILE_NAME)} with their frontmatter.`,
10248
+ parameters: skillToolSchemas.listSkills,
10249
+ execute: async () => {
10250
+ const skills = await listSkills();
10251
+ const output = { skills };
10252
+ return JSON.stringify(output, null, 2);
10253
+ }
10254
+ },
10255
+ getSkill: {
10256
+ name: "getSkill",
10257
+ description: "Get detailed information about a specific skill including SKILL.md content and other files. relativeDirPathFromCwd parameter is required.",
10258
+ parameters: skillToolSchemas.getSkill,
10259
+ execute: async (args) => {
10260
+ const result = await getSkill({ relativeDirPathFromCwd: args.relativeDirPathFromCwd });
10261
+ return JSON.stringify(result, null, 2);
10262
+ }
10263
+ },
10264
+ putSkill: {
10265
+ name: "putSkill",
10266
+ description: "Create or update a skill (upsert operation). relativeDirPathFromCwd, frontmatter, and body parameters are required. otherFiles is optional.",
10267
+ parameters: skillToolSchemas.putSkill,
10268
+ execute: async (args) => {
10269
+ const result = await putSkill({
10270
+ relativeDirPathFromCwd: args.relativeDirPathFromCwd,
10271
+ frontmatter: args.frontmatter,
10272
+ body: args.body,
10273
+ otherFiles: args.otherFiles
10274
+ });
10275
+ return JSON.stringify(result, null, 2);
10276
+ }
10277
+ },
10278
+ deleteSkill: {
10279
+ name: "deleteSkill",
10280
+ description: "Delete a skill directory and all its contents. relativeDirPathFromCwd parameter is required.",
10281
+ parameters: skillToolSchemas.deleteSkill,
10282
+ execute: async (args) => {
10283
+ const result = await deleteSkill({ relativeDirPathFromCwd: args.relativeDirPathFromCwd });
10284
+ return JSON.stringify(result, null, 2);
10285
+ }
10286
+ }
10287
+ };
10288
+
10289
+ // src/mcp/subagents.ts
10290
+ var import_node_path89 = require("path");
10291
+ var import_mini36 = require("zod/mini");
10030
10292
  var maxSubagentSizeBytes = 1024 * 1024;
10031
10293
  var maxSubagentsCount = 1e3;
10032
10294
  async function listSubagents() {
10033
- const subagentsDir = (0, import_node_path88.join)(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH);
10295
+ const subagentsDir = (0, import_node_path89.join)(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH);
10034
10296
  try {
10035
10297
  const files = await listDirectoryFiles(subagentsDir);
10036
10298
  const mdFiles = files.filter((file) => file.endsWith(".md"));
@@ -10043,7 +10305,7 @@ async function listSubagents() {
10043
10305
  });
10044
10306
  const frontmatter = subagent.getFrontmatter();
10045
10307
  return {
10046
- relativePathFromCwd: (0, import_node_path88.join)(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, file),
10308
+ relativePathFromCwd: (0, import_node_path89.join)(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, file),
10047
10309
  frontmatter
10048
10310
  };
10049
10311
  } catch (error) {
@@ -10065,14 +10327,14 @@ async function getSubagent({ relativePathFromCwd }) {
10065
10327
  relativePath: relativePathFromCwd,
10066
10328
  intendedRootDir: process.cwd()
10067
10329
  });
10068
- const filename = (0, import_node_path88.basename)(relativePathFromCwd);
10330
+ const filename = (0, import_node_path89.basename)(relativePathFromCwd);
10069
10331
  try {
10070
10332
  const subagent = await RulesyncSubagent.fromFile({
10071
10333
  relativeFilePath: filename,
10072
10334
  validate: true
10073
10335
  });
10074
10336
  return {
10075
- relativePathFromCwd: (0, import_node_path88.join)(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename),
10337
+ relativePathFromCwd: (0, import_node_path89.join)(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename),
10076
10338
  frontmatter: subagent.getFrontmatter(),
10077
10339
  body: subagent.getBody()
10078
10340
  };
@@ -10091,7 +10353,7 @@ async function putSubagent({
10091
10353
  relativePath: relativePathFromCwd,
10092
10354
  intendedRootDir: process.cwd()
10093
10355
  });
10094
- const filename = (0, import_node_path88.basename)(relativePathFromCwd);
10356
+ const filename = (0, import_node_path89.basename)(relativePathFromCwd);
10095
10357
  const estimatedSize = JSON.stringify(frontmatter).length + body.length;
10096
10358
  if (estimatedSize > maxSubagentSizeBytes) {
10097
10359
  throw new Error(
@@ -10101,7 +10363,7 @@ async function putSubagent({
10101
10363
  try {
10102
10364
  const existingSubagents = await listSubagents();
10103
10365
  const isUpdate = existingSubagents.some(
10104
- (subagent2) => subagent2.relativePathFromCwd === (0, import_node_path88.join)(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename)
10366
+ (subagent2) => subagent2.relativePathFromCwd === (0, import_node_path89.join)(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename)
10105
10367
  );
10106
10368
  if (!isUpdate && existingSubagents.length >= maxSubagentsCount) {
10107
10369
  throw new Error(`Maximum number of subagents (${maxSubagentsCount}) reached`);
@@ -10114,11 +10376,11 @@ async function putSubagent({
10114
10376
  body,
10115
10377
  validate: true
10116
10378
  });
10117
- const subagentsDir = (0, import_node_path88.join)(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH);
10379
+ const subagentsDir = (0, import_node_path89.join)(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH);
10118
10380
  await ensureDir(subagentsDir);
10119
10381
  await writeFileContent(subagent.getFilePath(), subagent.getFileContent());
10120
10382
  return {
10121
- relativePathFromCwd: (0, import_node_path88.join)(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename),
10383
+ relativePathFromCwd: (0, import_node_path89.join)(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename),
10122
10384
  frontmatter: subagent.getFrontmatter(),
10123
10385
  body: subagent.getBody()
10124
10386
  };
@@ -10133,12 +10395,12 @@ async function deleteSubagent({ relativePathFromCwd }) {
10133
10395
  relativePath: relativePathFromCwd,
10134
10396
  intendedRootDir: process.cwd()
10135
10397
  });
10136
- const filename = (0, import_node_path88.basename)(relativePathFromCwd);
10137
- const fullPath = (0, import_node_path88.join)(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename);
10398
+ const filename = (0, import_node_path89.basename)(relativePathFromCwd);
10399
+ const fullPath = (0, import_node_path89.join)(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename);
10138
10400
  try {
10139
10401
  await removeFile(fullPath);
10140
10402
  return {
10141
- relativePathFromCwd: (0, import_node_path88.join)(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename)
10403
+ relativePathFromCwd: (0, import_node_path89.join)(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename)
10142
10404
  };
10143
10405
  } catch (error) {
10144
10406
  throw new Error(
@@ -10150,23 +10412,23 @@ async function deleteSubagent({ relativePathFromCwd }) {
10150
10412
  }
10151
10413
  }
10152
10414
  var subagentToolSchemas = {
10153
- listSubagents: import_mini35.z.object({}),
10154
- getSubagent: import_mini35.z.object({
10155
- relativePathFromCwd: import_mini35.z.string()
10415
+ listSubagents: import_mini36.z.object({}),
10416
+ getSubagent: import_mini36.z.object({
10417
+ relativePathFromCwd: import_mini36.z.string()
10156
10418
  }),
10157
- putSubagent: import_mini35.z.object({
10158
- relativePathFromCwd: import_mini35.z.string(),
10419
+ putSubagent: import_mini36.z.object({
10420
+ relativePathFromCwd: import_mini36.z.string(),
10159
10421
  frontmatter: RulesyncSubagentFrontmatterSchema,
10160
- body: import_mini35.z.string()
10422
+ body: import_mini36.z.string()
10161
10423
  }),
10162
- deleteSubagent: import_mini35.z.object({
10163
- relativePathFromCwd: import_mini35.z.string()
10424
+ deleteSubagent: import_mini36.z.object({
10425
+ relativePathFromCwd: import_mini36.z.string()
10164
10426
  })
10165
10427
  };
10166
10428
  var subagentTools = {
10167
10429
  listSubagents: {
10168
10430
  name: "listSubagents",
10169
- description: `List all subagents from ${(0, import_node_path88.join)(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, "*.md")} with their frontmatter.`,
10431
+ description: `List all subagents from ${(0, import_node_path89.join)(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, "*.md")} with their frontmatter.`,
10170
10432
  parameters: subagentToolSchemas.listSubagents,
10171
10433
  execute: async () => {
10172
10434
  const subagents = await listSubagents();
@@ -10213,7 +10475,7 @@ async function mcpCommand({ version }) {
10213
10475
  name: "Rulesync MCP Server",
10214
10476
  // eslint-disable-next-line no-type-assertion/no-type-assertion
10215
10477
  version,
10216
- instructions: "This server handles Rulesync files including rules, commands, MCP, ignore files, and subagents for any AI agents. It should be used when you need those files."
10478
+ instructions: "This server handles Rulesync files including rules, commands, MCP, ignore files, subagents and skills for any AI agents. It should be used when you need those files."
10217
10479
  });
10218
10480
  server.addTool(ruleTools.listRules);
10219
10481
  server.addTool(ruleTools.getRule);
@@ -10227,6 +10489,10 @@ async function mcpCommand({ version }) {
10227
10489
  server.addTool(subagentTools.getSubagent);
10228
10490
  server.addTool(subagentTools.putSubagent);
10229
10491
  server.addTool(subagentTools.deleteSubagent);
10492
+ server.addTool(skillTools.listSkills);
10493
+ server.addTool(skillTools.getSkill);
10494
+ server.addTool(skillTools.putSkill);
10495
+ server.addTool(skillTools.deleteSkill);
10230
10496
  server.addTool(ignoreTools.getIgnoreFile);
10231
10497
  server.addTool(ignoreTools.putIgnoreFile);
10232
10498
  server.addTool(ignoreTools.deleteIgnoreFile);
@@ -10240,7 +10506,7 @@ async function mcpCommand({ version }) {
10240
10506
  }
10241
10507
 
10242
10508
  // src/cli/index.ts
10243
- var getVersion = () => "3.28.1";
10509
+ var getVersion = () => "3.29.0";
10244
10510
  var main = async () => {
10245
10511
  const program = new import_commander.Command();
10246
10512
  const version = getVersion();
package/dist/index.js CHANGED
@@ -267,6 +267,10 @@ var ConfigParamsSchema = z3.object({
267
267
  experimentalSimulateSubagents: optional(z3.boolean())
268
268
  });
269
269
  var PartialConfigParamsSchema = z3.partial(ConfigParamsSchema);
270
+ var ConfigFileSchema = z3.object({
271
+ $schema: optional(z3.string()),
272
+ ...z3.partial(ConfigParamsSchema).shape
273
+ });
270
274
  var RequiredConfigParamsSchema = z3.required(ConfigParamsSchema);
271
275
  var Config = class {
272
276
  baseDirs;
@@ -396,7 +400,9 @@ var ConfigResolver = class {
396
400
  try {
397
401
  const fileContent = await readFileContent(validatedConfigPath);
398
402
  const jsonData = parseJsonc(fileContent);
399
- configByFile = PartialConfigParamsSchema.parse(jsonData);
403
+ const parsed = ConfigFileSchema.parse(jsonData);
404
+ const { $schema: _schema, ...configParams2 } = parsed;
405
+ configByFile = configParams2;
400
406
  } catch (error) {
401
407
  logger.error(`Failed to load config file: ${formatError(error)}`);
402
408
  throw error;
@@ -642,6 +648,13 @@ var AiFile = class {
642
648
  setFileContent(newFileContent) {
643
649
  this.fileContent = newFileContent;
644
650
  }
651
+ /**
652
+ * Returns whether this file can be deleted by rulesync.
653
+ * Override in subclasses that should not be deleted (e.g., user-managed config files).
654
+ */
655
+ isDeletable() {
656
+ return true;
657
+ }
645
658
  };
646
659
 
647
660
  // src/features/commands/tool-command.ts
@@ -2301,6 +2314,13 @@ var ClaudecodeIgnore = class _ClaudecodeIgnore extends ToolIgnore {
2301
2314
  relativeFilePath: "settings.local.json"
2302
2315
  };
2303
2316
  }
2317
+ /**
2318
+ * ClaudecodeIgnore uses settings.local.json which is a user-managed config file.
2319
+ * It should not be deleted by rulesync.
2320
+ */
2321
+ isDeletable() {
2322
+ return false;
2323
+ }
2304
2324
  toRulesyncIgnore() {
2305
2325
  const rulesyncPatterns = this.patterns.map((pattern) => {
2306
2326
  if (pattern.startsWith("Read(") && pattern.endsWith(")")) {
@@ -2791,7 +2811,7 @@ var IgnoreProcessor = class extends FeatureProcessor {
2791
2811
  try {
2792
2812
  const toolIgnores = await this.loadToolIgnores();
2793
2813
  if (forDeletion) {
2794
- return toolIgnores.filter((toolFile) => !(toolFile instanceof ClaudecodeIgnore));
2814
+ return toolIgnores.filter((toolFile) => toolFile.isDeletable());
2795
2815
  }
2796
2816
  return toolIgnores;
2797
2817
  } catch (error) {
@@ -3293,6 +3313,14 @@ var ClaudecodeMcp = class _ClaudecodeMcp extends ToolMcp {
3293
3313
  getJson() {
3294
3314
  return this.json;
3295
3315
  }
3316
+ /**
3317
+ * In global mode, ~/.claude/.claude.json should not be deleted
3318
+ * as it may contain other user settings.
3319
+ * In local mode, .mcp.json can be safely deleted.
3320
+ */
3321
+ isDeletable() {
3322
+ return !this.global;
3323
+ }
3296
3324
  static getSettablePaths({ global } = {}) {
3297
3325
  if (global) {
3298
3326
  return {
@@ -3875,6 +3903,12 @@ var OpencodeMcp = class _OpencodeMcp extends ToolMcp {
3875
3903
  getJson() {
3876
3904
  return this.json;
3877
3905
  }
3906
+ /**
3907
+ * opencode.json may contain other settings, so it should not be deleted.
3908
+ */
3909
+ isDeletable() {
3910
+ return false;
3911
+ }
3878
3912
  static getSettablePaths({ global } = {}) {
3879
3913
  if (global) {
3880
3914
  return {
@@ -4159,11 +4193,7 @@ var McpProcessor = class extends FeatureProcessor {
4159
4193
  })();
4160
4194
  logger.info(`Successfully loaded ${toolMcps.length} ${this.toolTarget} MCP files`);
4161
4195
  if (forDeletion) {
4162
- let filteredMcps = toolMcps.filter((toolFile) => !(toolFile instanceof OpencodeMcp));
4163
- if (this.global) {
4164
- filteredMcps = filteredMcps.filter((toolFile) => !(toolFile instanceof ClaudecodeMcp));
4165
- }
4166
- return filteredMcps;
4196
+ return toolMcps.filter((toolFile) => toolFile.isDeletable());
4167
4197
  }
4168
4198
  return toolMcps;
4169
4199
  } catch (error) {
@@ -4544,20 +4574,13 @@ var SimulatedSkill = class extends ToolSkill {
4544
4574
  name: rulesyncFrontmatter.name,
4545
4575
  description: rulesyncFrontmatter.description
4546
4576
  };
4547
- const otherFiles = rulesyncSkill.getOtherFiles();
4548
- if (otherFiles.length > 0) {
4549
- logger.warn(
4550
- `Skill "${rulesyncFrontmatter.name}" has ${otherFiles.length} additional file(s) that will be ignored for simulated skill generation.`
4551
- );
4552
- }
4553
4577
  return {
4554
4578
  baseDir: rulesyncSkill.getBaseDir(),
4555
4579
  relativeDirPath: this.getSettablePaths().relativeDirPath,
4556
4580
  dirName: rulesyncSkill.getDirName(),
4557
4581
  frontmatter: simulatedFrontmatter,
4558
4582
  body: rulesyncSkill.getBody(),
4559
- otherFiles: [],
4560
- // Simulated skills ignore otherFiles
4583
+ otherFiles: rulesyncSkill.getOtherFiles(),
4561
4584
  validate
4562
4585
  };
4563
4586
  }
@@ -4579,14 +4602,19 @@ var SimulatedSkill = class extends ToolSkill {
4579
4602
  if (!result.success) {
4580
4603
  throw new Error(`Invalid frontmatter in ${skillFilePath}: ${formatError(result.error)}`);
4581
4604
  }
4605
+ const otherFiles = await this.collectOtherFiles(
4606
+ baseDir,
4607
+ actualRelativeDirPath,
4608
+ dirName,
4609
+ SKILL_FILE_NAME
4610
+ );
4582
4611
  return {
4583
4612
  baseDir,
4584
4613
  relativeDirPath: actualRelativeDirPath,
4585
4614
  dirName,
4586
4615
  frontmatter: result.data,
4587
4616
  body: content.trim(),
4588
- otherFiles: [],
4589
- // Simulated skills ignore otherFiles
4617
+ otherFiles,
4590
4618
  validate: true
4591
4619
  };
4592
4620
  }
@@ -10001,13 +10029,247 @@ var ruleTools = {
10001
10029
  }
10002
10030
  };
10003
10031
 
10004
- // src/mcp/subagents.ts
10005
- import { basename as basename23, join as join86 } from "path";
10032
+ // src/mcp/skills.ts
10033
+ import { basename as basename23, dirname as dirname2, join as join86 } from "path";
10006
10034
  import { z as z35 } from "zod/mini";
10035
+ var maxSkillSizeBytes = 1024 * 1024;
10036
+ var maxSkillsCount = 1e3;
10037
+ function aiDirFileToMcpSkillFile(file) {
10038
+ return {
10039
+ name: file.relativeFilePathToDirPath,
10040
+ body: file.fileBuffer.toString("utf-8")
10041
+ };
10042
+ }
10043
+ function mcpSkillFileToAiDirFile(file) {
10044
+ return {
10045
+ relativeFilePathToDirPath: file.name,
10046
+ fileBuffer: Buffer.from(file.body, "utf-8")
10047
+ };
10048
+ }
10049
+ function extractDirName(relativeDirPathFromCwd) {
10050
+ const dirName = basename23(relativeDirPathFromCwd);
10051
+ if (!dirName) {
10052
+ throw new Error(`Invalid path: ${relativeDirPathFromCwd}`);
10053
+ }
10054
+ return dirName;
10055
+ }
10056
+ async function listSkills() {
10057
+ const skillsDir = join86(process.cwd(), RULESYNC_SKILLS_RELATIVE_DIR_PATH);
10058
+ try {
10059
+ const skillDirPaths = await findFilesByGlobs(join86(skillsDir, "*"), { type: "dir" });
10060
+ const skills = await Promise.all(
10061
+ skillDirPaths.map(async (dirPath) => {
10062
+ const dirName = basename23(dirPath);
10063
+ if (!dirName) return null;
10064
+ try {
10065
+ const skill = await RulesyncSkill.fromDir({
10066
+ dirName
10067
+ });
10068
+ const frontmatter = skill.getFrontmatter();
10069
+ return {
10070
+ relativeDirPathFromCwd: join86(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName),
10071
+ frontmatter
10072
+ };
10073
+ } catch (error) {
10074
+ logger.error(`Failed to read skill directory ${dirName}: ${formatError(error)}`);
10075
+ return null;
10076
+ }
10077
+ })
10078
+ );
10079
+ return skills.filter((skill) => skill !== null);
10080
+ } catch (error) {
10081
+ logger.error(`Failed to read skills directory: ${formatError(error)}`);
10082
+ return [];
10083
+ }
10084
+ }
10085
+ async function getSkill({ relativeDirPathFromCwd }) {
10086
+ checkPathTraversal({
10087
+ relativePath: relativeDirPathFromCwd,
10088
+ intendedRootDir: process.cwd()
10089
+ });
10090
+ const dirName = extractDirName(relativeDirPathFromCwd);
10091
+ try {
10092
+ const skill = await RulesyncSkill.fromDir({
10093
+ dirName
10094
+ });
10095
+ return {
10096
+ relativeDirPathFromCwd: join86(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName),
10097
+ frontmatter: skill.getFrontmatter(),
10098
+ body: skill.getBody(),
10099
+ otherFiles: skill.getOtherFiles().map(aiDirFileToMcpSkillFile)
10100
+ };
10101
+ } catch (error) {
10102
+ throw new Error(
10103
+ `Failed to read skill directory ${relativeDirPathFromCwd}: ${formatError(error)}`,
10104
+ {
10105
+ cause: error
10106
+ }
10107
+ );
10108
+ }
10109
+ }
10110
+ async function putSkill({
10111
+ relativeDirPathFromCwd,
10112
+ frontmatter,
10113
+ body,
10114
+ otherFiles = []
10115
+ }) {
10116
+ checkPathTraversal({
10117
+ relativePath: relativeDirPathFromCwd,
10118
+ intendedRootDir: process.cwd()
10119
+ });
10120
+ const dirName = extractDirName(relativeDirPathFromCwd);
10121
+ const estimatedSize = JSON.stringify(frontmatter).length + body.length + otherFiles.reduce((acc, file) => acc + file.name.length + file.body.length, 0);
10122
+ if (estimatedSize > maxSkillSizeBytes) {
10123
+ throw new Error(
10124
+ `Skill size ${estimatedSize} bytes exceeds maximum ${maxSkillSizeBytes} bytes (1MB)`
10125
+ );
10126
+ }
10127
+ try {
10128
+ const existingSkills = await listSkills();
10129
+ const isUpdate = existingSkills.some(
10130
+ (skill2) => skill2.relativeDirPathFromCwd === join86(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName)
10131
+ );
10132
+ if (!isUpdate && existingSkills.length >= maxSkillsCount) {
10133
+ throw new Error(`Maximum number of skills (${maxSkillsCount}) reached`);
10134
+ }
10135
+ const aiDirFiles = otherFiles.map(mcpSkillFileToAiDirFile);
10136
+ const skill = new RulesyncSkill({
10137
+ baseDir: process.cwd(),
10138
+ relativeDirPath: RULESYNC_SKILLS_RELATIVE_DIR_PATH,
10139
+ dirName,
10140
+ frontmatter,
10141
+ body,
10142
+ otherFiles: aiDirFiles,
10143
+ validate: true
10144
+ });
10145
+ const skillDirPath = join86(process.cwd(), RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName);
10146
+ await ensureDir(skillDirPath);
10147
+ const skillFilePath = join86(skillDirPath, SKILL_FILE_NAME);
10148
+ const skillFileContent = stringifyFrontmatter(body, frontmatter);
10149
+ await writeFileContent(skillFilePath, skillFileContent);
10150
+ for (const file of otherFiles) {
10151
+ checkPathTraversal({
10152
+ relativePath: file.name,
10153
+ intendedRootDir: skillDirPath
10154
+ });
10155
+ const filePath = join86(skillDirPath, file.name);
10156
+ const fileDir = join86(skillDirPath, dirname2(file.name));
10157
+ if (fileDir !== skillDirPath) {
10158
+ await ensureDir(fileDir);
10159
+ }
10160
+ await writeFileContent(filePath, file.body);
10161
+ }
10162
+ return {
10163
+ relativeDirPathFromCwd: join86(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName),
10164
+ frontmatter: skill.getFrontmatter(),
10165
+ body: skill.getBody(),
10166
+ otherFiles: skill.getOtherFiles().map(aiDirFileToMcpSkillFile)
10167
+ };
10168
+ } catch (error) {
10169
+ throw new Error(
10170
+ `Failed to write skill directory ${relativeDirPathFromCwd}: ${formatError(error)}`,
10171
+ {
10172
+ cause: error
10173
+ }
10174
+ );
10175
+ }
10176
+ }
10177
+ async function deleteSkill({
10178
+ relativeDirPathFromCwd
10179
+ }) {
10180
+ checkPathTraversal({
10181
+ relativePath: relativeDirPathFromCwd,
10182
+ intendedRootDir: process.cwd()
10183
+ });
10184
+ const dirName = extractDirName(relativeDirPathFromCwd);
10185
+ const skillDirPath = join86(process.cwd(), RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName);
10186
+ try {
10187
+ if (await directoryExists(skillDirPath)) {
10188
+ await removeDirectory(skillDirPath);
10189
+ }
10190
+ return {
10191
+ relativeDirPathFromCwd: join86(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName)
10192
+ };
10193
+ } catch (error) {
10194
+ throw new Error(
10195
+ `Failed to delete skill directory ${relativeDirPathFromCwd}: ${formatError(error)}`,
10196
+ {
10197
+ cause: error
10198
+ }
10199
+ );
10200
+ }
10201
+ }
10202
+ var McpSkillFileSchema = z35.object({
10203
+ name: z35.string(),
10204
+ body: z35.string()
10205
+ });
10206
+ var skillToolSchemas = {
10207
+ listSkills: z35.object({}),
10208
+ getSkill: z35.object({
10209
+ relativeDirPathFromCwd: z35.string()
10210
+ }),
10211
+ putSkill: z35.object({
10212
+ relativeDirPathFromCwd: z35.string(),
10213
+ frontmatter: RulesyncSkillFrontmatterSchema,
10214
+ body: z35.string(),
10215
+ otherFiles: z35.optional(z35.array(McpSkillFileSchema))
10216
+ }),
10217
+ deleteSkill: z35.object({
10218
+ relativeDirPathFromCwd: z35.string()
10219
+ })
10220
+ };
10221
+ var skillTools = {
10222
+ listSkills: {
10223
+ name: "listSkills",
10224
+ description: `List all skills from ${join86(RULESYNC_SKILLS_RELATIVE_DIR_PATH, "*", SKILL_FILE_NAME)} with their frontmatter.`,
10225
+ parameters: skillToolSchemas.listSkills,
10226
+ execute: async () => {
10227
+ const skills = await listSkills();
10228
+ const output = { skills };
10229
+ return JSON.stringify(output, null, 2);
10230
+ }
10231
+ },
10232
+ getSkill: {
10233
+ name: "getSkill",
10234
+ description: "Get detailed information about a specific skill including SKILL.md content and other files. relativeDirPathFromCwd parameter is required.",
10235
+ parameters: skillToolSchemas.getSkill,
10236
+ execute: async (args) => {
10237
+ const result = await getSkill({ relativeDirPathFromCwd: args.relativeDirPathFromCwd });
10238
+ return JSON.stringify(result, null, 2);
10239
+ }
10240
+ },
10241
+ putSkill: {
10242
+ name: "putSkill",
10243
+ description: "Create or update a skill (upsert operation). relativeDirPathFromCwd, frontmatter, and body parameters are required. otherFiles is optional.",
10244
+ parameters: skillToolSchemas.putSkill,
10245
+ execute: async (args) => {
10246
+ const result = await putSkill({
10247
+ relativeDirPathFromCwd: args.relativeDirPathFromCwd,
10248
+ frontmatter: args.frontmatter,
10249
+ body: args.body,
10250
+ otherFiles: args.otherFiles
10251
+ });
10252
+ return JSON.stringify(result, null, 2);
10253
+ }
10254
+ },
10255
+ deleteSkill: {
10256
+ name: "deleteSkill",
10257
+ description: "Delete a skill directory and all its contents. relativeDirPathFromCwd parameter is required.",
10258
+ parameters: skillToolSchemas.deleteSkill,
10259
+ execute: async (args) => {
10260
+ const result = await deleteSkill({ relativeDirPathFromCwd: args.relativeDirPathFromCwd });
10261
+ return JSON.stringify(result, null, 2);
10262
+ }
10263
+ }
10264
+ };
10265
+
10266
+ // src/mcp/subagents.ts
10267
+ import { basename as basename24, join as join87 } from "path";
10268
+ import { z as z36 } from "zod/mini";
10007
10269
  var maxSubagentSizeBytes = 1024 * 1024;
10008
10270
  var maxSubagentsCount = 1e3;
10009
10271
  async function listSubagents() {
10010
- const subagentsDir = join86(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH);
10272
+ const subagentsDir = join87(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH);
10011
10273
  try {
10012
10274
  const files = await listDirectoryFiles(subagentsDir);
10013
10275
  const mdFiles = files.filter((file) => file.endsWith(".md"));
@@ -10020,7 +10282,7 @@ async function listSubagents() {
10020
10282
  });
10021
10283
  const frontmatter = subagent.getFrontmatter();
10022
10284
  return {
10023
- relativePathFromCwd: join86(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, file),
10285
+ relativePathFromCwd: join87(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, file),
10024
10286
  frontmatter
10025
10287
  };
10026
10288
  } catch (error) {
@@ -10042,14 +10304,14 @@ async function getSubagent({ relativePathFromCwd }) {
10042
10304
  relativePath: relativePathFromCwd,
10043
10305
  intendedRootDir: process.cwd()
10044
10306
  });
10045
- const filename = basename23(relativePathFromCwd);
10307
+ const filename = basename24(relativePathFromCwd);
10046
10308
  try {
10047
10309
  const subagent = await RulesyncSubagent.fromFile({
10048
10310
  relativeFilePath: filename,
10049
10311
  validate: true
10050
10312
  });
10051
10313
  return {
10052
- relativePathFromCwd: join86(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename),
10314
+ relativePathFromCwd: join87(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename),
10053
10315
  frontmatter: subagent.getFrontmatter(),
10054
10316
  body: subagent.getBody()
10055
10317
  };
@@ -10068,7 +10330,7 @@ async function putSubagent({
10068
10330
  relativePath: relativePathFromCwd,
10069
10331
  intendedRootDir: process.cwd()
10070
10332
  });
10071
- const filename = basename23(relativePathFromCwd);
10333
+ const filename = basename24(relativePathFromCwd);
10072
10334
  const estimatedSize = JSON.stringify(frontmatter).length + body.length;
10073
10335
  if (estimatedSize > maxSubagentSizeBytes) {
10074
10336
  throw new Error(
@@ -10078,7 +10340,7 @@ async function putSubagent({
10078
10340
  try {
10079
10341
  const existingSubagents = await listSubagents();
10080
10342
  const isUpdate = existingSubagents.some(
10081
- (subagent2) => subagent2.relativePathFromCwd === join86(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename)
10343
+ (subagent2) => subagent2.relativePathFromCwd === join87(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename)
10082
10344
  );
10083
10345
  if (!isUpdate && existingSubagents.length >= maxSubagentsCount) {
10084
10346
  throw new Error(`Maximum number of subagents (${maxSubagentsCount}) reached`);
@@ -10091,11 +10353,11 @@ async function putSubagent({
10091
10353
  body,
10092
10354
  validate: true
10093
10355
  });
10094
- const subagentsDir = join86(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH);
10356
+ const subagentsDir = join87(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH);
10095
10357
  await ensureDir(subagentsDir);
10096
10358
  await writeFileContent(subagent.getFilePath(), subagent.getFileContent());
10097
10359
  return {
10098
- relativePathFromCwd: join86(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename),
10360
+ relativePathFromCwd: join87(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename),
10099
10361
  frontmatter: subagent.getFrontmatter(),
10100
10362
  body: subagent.getBody()
10101
10363
  };
@@ -10110,12 +10372,12 @@ async function deleteSubagent({ relativePathFromCwd }) {
10110
10372
  relativePath: relativePathFromCwd,
10111
10373
  intendedRootDir: process.cwd()
10112
10374
  });
10113
- const filename = basename23(relativePathFromCwd);
10114
- const fullPath = join86(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename);
10375
+ const filename = basename24(relativePathFromCwd);
10376
+ const fullPath = join87(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename);
10115
10377
  try {
10116
10378
  await removeFile(fullPath);
10117
10379
  return {
10118
- relativePathFromCwd: join86(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename)
10380
+ relativePathFromCwd: join87(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename)
10119
10381
  };
10120
10382
  } catch (error) {
10121
10383
  throw new Error(
@@ -10127,23 +10389,23 @@ async function deleteSubagent({ relativePathFromCwd }) {
10127
10389
  }
10128
10390
  }
10129
10391
  var subagentToolSchemas = {
10130
- listSubagents: z35.object({}),
10131
- getSubagent: z35.object({
10132
- relativePathFromCwd: z35.string()
10392
+ listSubagents: z36.object({}),
10393
+ getSubagent: z36.object({
10394
+ relativePathFromCwd: z36.string()
10133
10395
  }),
10134
- putSubagent: z35.object({
10135
- relativePathFromCwd: z35.string(),
10396
+ putSubagent: z36.object({
10397
+ relativePathFromCwd: z36.string(),
10136
10398
  frontmatter: RulesyncSubagentFrontmatterSchema,
10137
- body: z35.string()
10399
+ body: z36.string()
10138
10400
  }),
10139
- deleteSubagent: z35.object({
10140
- relativePathFromCwd: z35.string()
10401
+ deleteSubagent: z36.object({
10402
+ relativePathFromCwd: z36.string()
10141
10403
  })
10142
10404
  };
10143
10405
  var subagentTools = {
10144
10406
  listSubagents: {
10145
10407
  name: "listSubagents",
10146
- description: `List all subagents from ${join86(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, "*.md")} with their frontmatter.`,
10408
+ description: `List all subagents from ${join87(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, "*.md")} with their frontmatter.`,
10147
10409
  parameters: subagentToolSchemas.listSubagents,
10148
10410
  execute: async () => {
10149
10411
  const subagents = await listSubagents();
@@ -10190,7 +10452,7 @@ async function mcpCommand({ version }) {
10190
10452
  name: "Rulesync MCP Server",
10191
10453
  // eslint-disable-next-line no-type-assertion/no-type-assertion
10192
10454
  version,
10193
- instructions: "This server handles Rulesync files including rules, commands, MCP, ignore files, and subagents for any AI agents. It should be used when you need those files."
10455
+ instructions: "This server handles Rulesync files including rules, commands, MCP, ignore files, subagents and skills for any AI agents. It should be used when you need those files."
10194
10456
  });
10195
10457
  server.addTool(ruleTools.listRules);
10196
10458
  server.addTool(ruleTools.getRule);
@@ -10204,6 +10466,10 @@ async function mcpCommand({ version }) {
10204
10466
  server.addTool(subagentTools.getSubagent);
10205
10467
  server.addTool(subagentTools.putSubagent);
10206
10468
  server.addTool(subagentTools.deleteSubagent);
10469
+ server.addTool(skillTools.listSkills);
10470
+ server.addTool(skillTools.getSkill);
10471
+ server.addTool(skillTools.putSkill);
10472
+ server.addTool(skillTools.deleteSkill);
10207
10473
  server.addTool(ignoreTools.getIgnoreFile);
10208
10474
  server.addTool(ignoreTools.putIgnoreFile);
10209
10475
  server.addTool(ignoreTools.deleteIgnoreFile);
@@ -10217,7 +10483,7 @@ async function mcpCommand({ version }) {
10217
10483
  }
10218
10484
 
10219
10485
  // src/cli/index.ts
10220
- var getVersion = () => "3.28.1";
10486
+ var getVersion = () => "3.29.0";
10221
10487
  var main = async () => {
10222
10488
  const program = new Command();
10223
10489
  const version = getVersion();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulesync",
3
- "version": "3.28.1",
3
+ "version": "3.29.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",
@@ -49,15 +49,15 @@
49
49
  "zod": "4.1.13"
50
50
  },
51
51
  "devDependencies": {
52
- "@anthropic-ai/claude-agent-sdk": "0.1.53",
53
- "@biomejs/biome": "2.3.7",
52
+ "@anthropic-ai/claude-agent-sdk": "0.1.55",
53
+ "@biomejs/biome": "2.3.8",
54
54
  "@eslint/js": "9.39.1",
55
55
  "@secretlint/secretlint-rule-preset-recommend": "11.2.5",
56
56
  "@tsconfig/node24": "24.0.3",
57
57
  "@types/js-yaml": "4.0.9",
58
58
  "@types/node": "24.10.1",
59
- "@typescript/native-preview": "7.0.0-dev.20251125.1",
60
- "@vitest/coverage-v8": "4.0.13",
59
+ "@typescript/native-preview": "7.0.0-dev.20251130.1",
60
+ "@vitest/coverage-v8": "4.0.14",
61
61
  "cspell": "9.3.2",
62
62
  "eslint": "9.39.1",
63
63
  "eslint-plugin-import": "2.32.0",
@@ -70,12 +70,12 @@
70
70
  "oxlint": "1.30.0",
71
71
  "secretlint": "11.2.5",
72
72
  "simple-git-hooks": "2.13.1",
73
- "sort-package-json": "3.4.0",
73
+ "sort-package-json": "3.5.0",
74
74
  "tsup": "8.5.1",
75
- "tsx": "4.20.6",
75
+ "tsx": "4.21.0",
76
76
  "typescript": "5.9.3",
77
77
  "typescript-eslint": "8.48.0",
78
- "vitest": "4.0.13"
78
+ "vitest": "4.0.14"
79
79
  },
80
80
  "engines": {
81
81
  "node": ">=22.0.0"
@@ -98,6 +98,7 @@
98
98
  "eslint:fix": "eslint . --fix --max-warnings 0 --cache",
99
99
  "fix": "pnpm run bcheck:fix && pnpm run oxlint:fix && pnpm run eslint:fix",
100
100
  "generate": "pnpm run dev generate",
101
+ "generate:schema": "tsx scripts/generate-json-schema.ts",
101
102
  "knip": "knip",
102
103
  "oxlint": "oxlint . --max-warnings 0",
103
104
  "oxlint:fix": "oxlint . --fix --max-warnings 0",