skill-rules 0.1.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +64 -8
  2. package/dist/index.js +421 -6
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -31,13 +31,14 @@ sr use --off # restore all skills, clear active stage
31
31
 
32
32
  ## Supported IDEs
33
33
 
34
- | IDE | Detected by | Skills directory |
35
- | ----------------------------------------------------- | ------------- | -------------------- |
36
- | Claude Code | `.claude/` | `.claude/skills/` |
37
- | Cursor | `.cursor/` | `.cursor/skills/` |
38
- | Windsurf | `.windsurf/` | `.windsurf/skills/` |
39
- | OpenHands | `.openhands/` | `.openhands/skills/` |
40
- | GitHub Copilot, Cline, VS Code, OpenCode, Codex, Kiro | `.agents/` | `.agents/skills/` |
34
+ | IDE | Detected by | Skills directory |
35
+ | ------------------------------------------- | ------------- | -------------------- |
36
+ | Claude Code | `.claude/` | `.claude/skills/` |
37
+ | Cursor | `.cursor/` | `.cursor/skills/` |
38
+ | Windsurf | `.windsurf/` | `.windsurf/skills/` |
39
+ | OpenHands | `.openhands/` | `.openhands/skills/` |
40
+ | OpenCode | `.opencode/` | `.opencode/skills/` |
41
+ | GitHub Copilot, Cline, VS Code, Codex, Kiro | `.agents/` | `.agents/skills/` |
41
42
 
42
43
  ---
43
44
 
@@ -117,6 +118,7 @@ flowchart TD
117
118
  SR --> WINDSURF[.windsurf/skills/]
118
119
  SR --> AGENTS[.agents/skills/]
119
120
  SR --> OH[.openhands/skills/]
121
+ SR --> OC[.opencode/skills/]
120
122
  ```
121
123
 
122
124
  ### Two files, two owners
@@ -302,6 +304,60 @@ sr use --help
302
304
 
303
305
  ---
304
306
 
307
+ ## MCP Server
308
+
309
+ `skill-rules` ships a built-in [Model Context Protocol](https://modelcontextprotocol.io) server so Claude (or any MCP-compatible agent) can manage your skills directly — no terminal required.
310
+
311
+ ```bash
312
+ sr mcp # start with global install
313
+ npx skill-rules mcp # start without installing
314
+ ```
315
+
316
+ The server uses **stdio transport** and runs in the project directory where it is started. It exposes the following tools:
317
+
318
+ | Tool | Description |
319
+ | -------- | ---------------------------------------------------------------------- |
320
+ | `sync` | Sync active skills across all detected IDEs |
321
+ | `status` | Show active stage, detected IDEs, installed skills, and stash contents |
322
+ | `init` | Initialize `skills-lock.json`, `skills.rules`, and update `.gitignore` |
323
+ | `add` | Assign a skill to a stage (`skill`, `stage`, optional `track`) |
324
+ | `remove` | Remove a skill from a stage or all stages |
325
+ | `use` | Activate a stage or restore everything with `off: true` |
326
+ | `list` | List all skills with stages, install status, and git tracking |
327
+ | `ignore` | Regenerate the `.gitignore` block |
328
+
329
+ ### Configure in Claude Code
330
+
331
+ Add to your project's `.mcp.json`:
332
+
333
+ ```json
334
+ {
335
+ "mcpServers": {
336
+ "skill-rules": {
337
+ "command": "npx",
338
+ "args": ["-y", "skill-rules", "mcp"]
339
+ }
340
+ }
341
+ }
342
+ ```
343
+
344
+ Or add globally in `~/.claude/settings.json` (requires global install):
345
+
346
+ ```json
347
+ {
348
+ "mcpServers": {
349
+ "skill-rules": {
350
+ "command": "sr",
351
+ "args": ["mcp"]
352
+ }
353
+ }
354
+ }
355
+ ```
356
+
357
+ Once connected, Claude can call `sync`, `status`, `use`, and any other tool directly from the conversation — no CLI needed.
358
+
359
+ ---
360
+
305
361
  ## Configuration files
306
362
 
307
363
  ### `skills-lock.json`
@@ -399,7 +455,7 @@ your-project/
399
455
  ## Requirements
400
456
 
401
457
  - Node.js >= 20 (LTS)
402
- - At least one IDE directory (`.claude`, `.cursor`, `.windsurf`, `.agents`, or `.openhands`) at the project root
458
+ - At least one IDE directory (`.claude`, `.cursor`, `.windsurf`, `.agents`, `.openhands`, or `.opencode`) at the project root
403
459
 
404
460
  ---
405
461
 
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { Command } from "commander";
5
5
  import { readFileSync as readFileSync5 } from "fs";
6
6
  import { fileURLToPath } from "url";
7
- import { join as join11, dirname } from "path";
7
+ import { join as join12, dirname } from "path";
8
8
 
9
9
  // src/commands/run.jsx
10
10
  import React2, { useEffect, useState as useState2 } from "react";
@@ -35,9 +35,14 @@ var IDES = {
35
35
  detectDir: ".openhands",
36
36
  skillsDir: ".openhands/skills"
37
37
  },
38
+ opencode: {
39
+ name: "OpenCode",
40
+ detectDir: ".opencode",
41
+ skillsDir: ".opencode/skills"
42
+ },
38
43
  agents: {
39
- // Shared by: GitHub Copilot, Cline, VS Code, OpenCode, Codex, Kiro, and others
40
- name: "Agents (Copilot, Cline, VS Code, OpenCode, Codex, Kiro)",
44
+ // Shared by: GitHub Copilot, Cline, VS Code, Codex, Kiro, and others
45
+ name: "Agents (Copilot, Cline, VS Code, Codex, Kiro)",
41
46
  detectDir: ".agents",
42
47
  skillsDir: ".agents/skills"
43
48
  }
@@ -380,10 +385,10 @@ Available: ${available.join(", ")}` : "\nNo stages defined yet."}${hint}`
380
385
  const msg = activeStage ? `Stage [${activeStage}] has no skills. Run: skill-rules add --stage ${activeStage}` : "No skills yet. Run: skill-rules add";
381
386
  return /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, msg);
382
387
  }
383
- const ok = results.filter((r) => r.status === "ok");
388
+ const ok2 = results.filter((r) => r.status === "ok");
384
389
  const synced = results.filter((r) => r.status === "synced");
385
390
  const missing = results.filter((r) => r.status === "missing");
386
- return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React2.createElement(Header, null, "skill-rules", activeStage ? ` [${activeStage}]` : ""), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "IDEs (", ides.length, ")"), ides.map((ide) => /* @__PURE__ */ React2.createElement(IDEItem, { key: ide.id, ide }))), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "Skills (", results.length, ")"), results.map((r) => /* @__PURE__ */ React2.createElement(SkillStatus, { key: r.name, name: r.name, status: r.status, detail: r.detail }))), /* @__PURE__ */ React2.createElement(Box2, { gap: 3 }, ok.length > 0 && /* @__PURE__ */ React2.createElement(Text2, { color: "green" }, ok.length, " up to date"), synced.length > 0 && /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, synced.length, " synced"), missing.length > 0 && /* @__PURE__ */ React2.createElement(Text2, { color: "red" }, missing.length, " missing \u2014 install via ", /* @__PURE__ */ React2.createElement(Text2, { bold: true }, "skills.sh"), " or", " ", /* @__PURE__ */ React2.createElement(Text2, { bold: true }, "autoskill"))));
391
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React2.createElement(Header, null, "skill-rules", activeStage ? ` [${activeStage}]` : ""), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "IDEs (", ides.length, ")"), ides.map((ide) => /* @__PURE__ */ React2.createElement(IDEItem, { key: ide.id, ide }))), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "Skills (", results.length, ")"), results.map((r) => /* @__PURE__ */ React2.createElement(SkillStatus, { key: r.name, name: r.name, status: r.status, detail: r.detail }))), /* @__PURE__ */ React2.createElement(Box2, { gap: 3 }, ok2.length > 0 && /* @__PURE__ */ React2.createElement(Text2, { color: "green" }, ok2.length, " up to date"), synced.length > 0 && /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, synced.length, " synced"), missing.length > 0 && /* @__PURE__ */ React2.createElement(Text2, { color: "red" }, missing.length, " missing \u2014 install via ", /* @__PURE__ */ React2.createElement(Text2, { bold: true }, "skills.sh"), " or", " ", /* @__PURE__ */ React2.createElement(Text2, { bold: true }, "autoskill"))));
387
392
  }
388
393
 
389
394
  // src/commands/init.jsx
@@ -1101,9 +1106,418 @@ function DoneView({ data }) {
1101
1106
  return null;
1102
1107
  }
1103
1108
 
1109
+ // src/mcp/server.js
1110
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1111
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1112
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
1113
+ import { existsSync as existsSync10, writeFileSync as writeFileSync5 } from "fs";
1114
+ import { join as join11 } from "path";
1115
+ var TOOLS = [
1116
+ {
1117
+ name: "sync",
1118
+ description: "Sync active skills across all detected IDEs. Copies skills from IDEs that have them to IDEs that are missing them.",
1119
+ inputSchema: {
1120
+ type: "object",
1121
+ properties: {
1122
+ stage: {
1123
+ type: "string",
1124
+ description: "Stage to filter skills by. Defaults to the currently active stage."
1125
+ }
1126
+ }
1127
+ }
1128
+ },
1129
+ {
1130
+ name: "status",
1131
+ description: "Show current state: active stage, detected IDEs, installed skills with their stage assignments, and stash contents.",
1132
+ inputSchema: { type: "object", properties: {} }
1133
+ },
1134
+ {
1135
+ name: "init",
1136
+ description: "Initialize skill-rules in the project. Creates skills-lock.json and skills.rules if absent, and updates .gitignore.",
1137
+ inputSchema: { type: "object", properties: {} }
1138
+ },
1139
+ {
1140
+ name: "add",
1141
+ description: "Assign a skill to a stage.",
1142
+ inputSchema: {
1143
+ type: "object",
1144
+ properties: {
1145
+ skill: { type: "string", description: "Skill name to assign" },
1146
+ stage: { type: "string", description: "Stage name to assign the skill to" },
1147
+ track: {
1148
+ type: "boolean",
1149
+ description: "Commit this skill to git instead of gitignoring it (default: false)"
1150
+ }
1151
+ },
1152
+ required: ["skill", "stage"]
1153
+ }
1154
+ },
1155
+ {
1156
+ name: "remove",
1157
+ description: "Remove a skill from a stage, or from all stages when no stage is given.",
1158
+ inputSchema: {
1159
+ type: "object",
1160
+ properties: {
1161
+ skill: { type: "string", description: "Skill name to remove" },
1162
+ stage: {
1163
+ type: "string",
1164
+ description: "Stage to remove from. Omit to remove from all stages."
1165
+ }
1166
+ },
1167
+ required: ["skill"]
1168
+ }
1169
+ },
1170
+ {
1171
+ name: "use",
1172
+ description: "Activate a stage: stash skills not assigned to it, restore skills that are. Pass off: true to restore everything and clear the active stage.",
1173
+ inputSchema: {
1174
+ type: "object",
1175
+ properties: {
1176
+ stage: { type: "string", description: "Stage name to activate" },
1177
+ off: {
1178
+ type: "boolean",
1179
+ description: "Restore all stashed skills and clear the active stage"
1180
+ }
1181
+ }
1182
+ }
1183
+ },
1184
+ {
1185
+ name: "list",
1186
+ description: "List all installed skills with their stage assignments, installation status, and git tracking. Use track/untrack to change tracking.",
1187
+ inputSchema: {
1188
+ type: "object",
1189
+ properties: {
1190
+ track: { type: "string", description: "Skill name to start committing to git" },
1191
+ untrack: { type: "string", description: "Skill name to exclude from git" }
1192
+ }
1193
+ }
1194
+ },
1195
+ {
1196
+ name: "ignore",
1197
+ description: "Regenerate the skill-rules block in .gitignore for all detected IDE directories.",
1198
+ inputSchema: { type: "object", properties: {} }
1199
+ }
1200
+ ];
1201
+ function ok(text) {
1202
+ return { content: [{ type: "text", text }] };
1203
+ }
1204
+ function fail(text) {
1205
+ return { content: [{ type: "text", text: `Error: ${text}` }], isError: true };
1206
+ }
1207
+ function toolSync(cwd, args) {
1208
+ const stage = args.stage ?? getActiveStage(cwd);
1209
+ const ides = detectIDEs(cwd);
1210
+ if (ides.length === 0)
1211
+ return fail(
1212
+ "No IDE directories detected. Expected: .claude/ .cursor/ .windsurf/ .agents/ .openhands/ .opencode/"
1213
+ );
1214
+ const lock = readLock(cwd);
1215
+ const rules = readRules(cwd);
1216
+ if (stage && rules && !(stage in (rules.stages ?? {}))) {
1217
+ const available = listStages(rules);
1218
+ return fail(
1219
+ `Stage "${stage}" not found.${available.length ? ` Available: ${available.join(", ")}` : " No stages defined yet."}`
1220
+ );
1221
+ }
1222
+ syncGitignore(cwd, ides, lock);
1223
+ const skillsToCheck = stage ? getActiveSkills(rules, stage) : Object.keys(lock.skills);
1224
+ if (skillsToCheck.length === 0)
1225
+ return ok(
1226
+ `No skills${stage ? ` in stage [${stage}]` : ""} to sync.
1227
+ IDEs (${ides.length}): ${ides.map((i) => i.name).join(", ")}`
1228
+ );
1229
+ const lines = [
1230
+ `Sync${stage ? ` [${stage}]` : ""} \u2014 ${ides.length} IDE${ides.length !== 1 ? "s" : ""}`,
1231
+ ""
1232
+ ];
1233
+ for (const skillName of skillsToCheck) {
1234
+ const sources = findSkillSources(cwd, ides, skillName);
1235
+ if (sources.length === 0) {
1236
+ lines.push(`missing ${skillName}`);
1237
+ } else if (sources.length < ides.length) {
1238
+ const copied = syncSkillToMissingIDEs(cwd, ides, skillName, sources[0]);
1239
+ lines.push(
1240
+ `synced ${skillName} \u2192 ${copied} IDE${copied !== 1 ? "s" : ""} from ${sources[0].name}`
1241
+ );
1242
+ } else {
1243
+ lines.push(`ok ${skillName}`);
1244
+ }
1245
+ }
1246
+ return ok(lines.join("\n"));
1247
+ }
1248
+ function toolStatus(cwd) {
1249
+ const ides = detectIDEs(cwd);
1250
+ const lock = readLock(cwd);
1251
+ const rules = readRules(cwd);
1252
+ const activeStage = getActiveStage(cwd);
1253
+ const stashed = listStashed(cwd);
1254
+ const stages = listStages(rules ?? { stages: {} });
1255
+ const skills = Object.entries(lock.skills);
1256
+ const lines = [
1257
+ `Active stage : ${activeStage ? `[${activeStage}]` : "none"}`,
1258
+ `IDEs : ${ides.length > 0 ? ides.map((i) => i.name).join(", ") : "none detected"}`,
1259
+ `Stages : ${stages.length > 0 ? stages.join(", ") : "none"}`,
1260
+ `Stashed : ${stashed.length > 0 ? stashed.join(", ") : "none"}`,
1261
+ ""
1262
+ ];
1263
+ if (skills.length === 0) {
1264
+ lines.push("No skills installed yet.");
1265
+ } else {
1266
+ lines.push("Skills:");
1267
+ for (const [name, info] of skills) {
1268
+ const skillStages = stages.filter((s) => (rules?.stages[s] ?? []).includes(name));
1269
+ const stageStr = skillStages.length ? skillStages.join(", ") : "\u2014";
1270
+ const tracking = info.track ? " [git tracked]" : "";
1271
+ lines.push(` ${name.padEnd(22)} ${stageStr}${tracking}`);
1272
+ }
1273
+ }
1274
+ return ok(lines.join("\n"));
1275
+ }
1276
+ function toolInit(cwd) {
1277
+ const ides = detectIDEs(cwd);
1278
+ const lines = [];
1279
+ const lockPath = join11(cwd, "skills-lock.json");
1280
+ if (!existsSync10(lockPath)) {
1281
+ writeFileSync5(lockPath, JSON.stringify({ version: 1, skills: {} }, null, 2) + "\n");
1282
+ lines.push("created skills-lock.json");
1283
+ } else {
1284
+ lines.push("exists skills-lock.json");
1285
+ }
1286
+ const rulesPath = join11(cwd, "skills.rules");
1287
+ if (!existsSync10(rulesPath)) {
1288
+ writeFileSync5(rulesPath, JSON.stringify(createEmptyRules(), null, 2) + "\n");
1289
+ lines.push("created skills.rules");
1290
+ } else {
1291
+ lines.push("exists skills.rules");
1292
+ }
1293
+ if (ides.length > 0) {
1294
+ const lock = readLock(cwd);
1295
+ syncGitignore(cwd, ides, lock);
1296
+ lines.push("updated .gitignore");
1297
+ lines.push(`IDEs ${ides.map((i) => i.name).join(", ")}`);
1298
+ } else {
1299
+ lines.push(
1300
+ "warning No IDE directories detected \u2014 .gitignore not updated\n Create one of: .claude/ .cursor/ .windsurf/ .agents/ .openhands/ .opencode/"
1301
+ );
1302
+ }
1303
+ return ok(lines.join("\n"));
1304
+ }
1305
+ function toolAdd(cwd, args) {
1306
+ const { skill, stage, track = false } = args;
1307
+ const lock = readLock(cwd);
1308
+ const ides = detectIDEs(cwd);
1309
+ const rules = readRules(cwd) ?? createEmptyRules();
1310
+ const inLock = !!lock.skills[skill];
1311
+ const alreadyInStage = (rules.stages[stage] ?? []).includes(skill);
1312
+ const isNewStage = !listStages(rules).includes(stage);
1313
+ if (!alreadyInStage) {
1314
+ addSkillToStage(rules, skill, stage);
1315
+ writeRules(rules, cwd);
1316
+ }
1317
+ if (track && inLock) {
1318
+ setSkillTracked(lock, skill, true);
1319
+ writeLock(lock, cwd);
1320
+ }
1321
+ if (ides.length > 0) syncGitignore(cwd, ides, lock);
1322
+ const lines = [];
1323
+ if (!inLock)
1324
+ lines.push(
1325
+ `warning "${skill}" not in skills-lock.json \u2014 install via skills.sh or autoskill first`
1326
+ );
1327
+ if (isNewStage && !alreadyInStage) lines.push(`created stage [${stage}]`);
1328
+ lines.push(
1329
+ alreadyInStage ? `exists "${skill}" already in [${stage}]` : `added "${skill}" \u2192 [${stage}]`
1330
+ );
1331
+ if (track)
1332
+ lines.push(
1333
+ inLock ? "tracked git tracking enabled" : "warning git tracking skipped (not in lock)"
1334
+ );
1335
+ return ok(lines.join("\n"));
1336
+ }
1337
+ function toolRemove(cwd, args) {
1338
+ const { skill, stage } = args;
1339
+ const rules = readRules(cwd);
1340
+ if (!rules) return fail("No skills.rules found. Run init first.");
1341
+ const stages = listStages(rules);
1342
+ const inAnyStage = stages.some((s) => (rules.stages[s] ?? []).includes(skill));
1343
+ if (!inAnyStage) return fail(`"${skill}" is not assigned to any stage.`);
1344
+ if (stage && !(rules.stages[stage] ?? []).includes(skill)) {
1345
+ const assignedTo = stages.filter((s) => (rules.stages[s] ?? []).includes(skill));
1346
+ return fail(`"${skill}" is not in [${stage}]. Assigned to: ${assignedTo.join(", ")}`);
1347
+ }
1348
+ removeSkillFromStage(rules, skill, stage ?? null);
1349
+ writeRules(rules, cwd);
1350
+ const lock = readLock(cwd);
1351
+ const ides = detectIDEs(cwd);
1352
+ if (ides.length > 0) syncGitignore(cwd, ides, lock);
1353
+ return ok(`removed "${skill}" from ${stage ? `[${stage}]` : "all stages"}`);
1354
+ }
1355
+ function toolUse(cwd, args) {
1356
+ const { stage, off } = args;
1357
+ const ides = detectIDEs(cwd);
1358
+ const lock = readLock(cwd);
1359
+ if (!stage && !off) {
1360
+ const activeStage = getActiveStage(cwd);
1361
+ const stashed = listStashed(cwd);
1362
+ if (!activeStage)
1363
+ return ok(
1364
+ `No active stage \u2014 all skills available.
1365
+ Stashed: ${stashed.length > 0 ? stashed.join(", ") : "none"}`
1366
+ );
1367
+ return ok(
1368
+ `Active stage: [${activeStage}]
1369
+ Stashed (${stashed.length}): ${stashed.join(", ") || "none"}`
1370
+ );
1371
+ }
1372
+ if (off) {
1373
+ const stashed = listStashed(cwd);
1374
+ if (stashed.length === 0) {
1375
+ clearActiveStage(cwd);
1376
+ return ok("Nothing stashed \u2014 no active stage to clear.");
1377
+ }
1378
+ if (ides.length === 0)
1379
+ return fail("No IDE directories detected \u2014 cannot restore stashed skills.");
1380
+ for (const s of stashed) restoreSkill(cwd, ides, s);
1381
+ clearActiveStage(cwd);
1382
+ syncGitignore(cwd, ides, lock);
1383
+ return ok(
1384
+ `Restored all stashed skills:
1385
+ ${stashed.map((s) => ` restored ${s}`).join("\n")}
1386
+
1387
+ No active stage \u2014 all skills available.`
1388
+ );
1389
+ }
1390
+ if (ides.length === 0) return fail("No IDE directories detected.");
1391
+ const rules = readRules(cwd);
1392
+ if (!rules) return fail("No skills.rules found. Run init first.");
1393
+ const available = listStages(rules);
1394
+ if (!available.includes(stage))
1395
+ return fail(
1396
+ `Stage "${stage}" not found.${available.length ? ` Available: ${available.join(", ")}` : " No stages defined yet."}`
1397
+ );
1398
+ const currentStage = getActiveStage(cwd);
1399
+ if (currentStage === stage) return ok(`Stage [${stage}] is already active.`);
1400
+ const targetSkills = new Set(rules.stages[stage] ?? []);
1401
+ const allStagedSkills = new Set(getActiveSkills(rules, null));
1402
+ const trackedSkills = new Set(
1403
+ Object.entries(lock.skills ?? {}).filter(([, info]) => info.track).map(([name]) => name)
1404
+ );
1405
+ const toActivate = [], toRestore = [], toMissing = [], toStash = [], toSkip = [];
1406
+ for (const skill of allStagedSkills) {
1407
+ if (trackedSkills.has(skill)) {
1408
+ toSkip.push(skill);
1409
+ continue;
1410
+ }
1411
+ if (targetSkills.has(skill)) {
1412
+ if (isStashed(cwd, skill)) toRestore.push(skill);
1413
+ else {
1414
+ const inIDEs = ides.some((ide) => existsSync10(join11(cwd, ide.skillsDir, skill)));
1415
+ if (inIDEs) toActivate.push(skill);
1416
+ else toMissing.push(skill);
1417
+ }
1418
+ } else if (!isStashed(cwd, skill)) {
1419
+ const inIDEs = ides.some((ide) => existsSync10(join11(cwd, ide.skillsDir, skill)));
1420
+ if (inIDEs) toStash.push(skill);
1421
+ }
1422
+ }
1423
+ for (const skill of toStash) stashSkill(cwd, ides, skill);
1424
+ for (const skill of toRestore) restoreSkill(cwd, ides, skill);
1425
+ setActiveStage(stage, cwd);
1426
+ syncGitignore(cwd, ides, lock);
1427
+ const lines = [`Active stage: [${stage}]`, ""];
1428
+ if (toActivate.length) lines.push(...toActivate.map((s) => `keep ${s}`));
1429
+ if (toRestore.length) lines.push(...toRestore.map((s) => `restored ${s}`));
1430
+ if (toStash.length) lines.push(...toStash.map((s) => `stashed ${s}`));
1431
+ if (toMissing.length) lines.push(...toMissing.map((s) => `missing ${s} (not installed)`));
1432
+ if (toSkip.length) lines.push(...toSkip.map((s) => `skip ${s} (tracked in git)`));
1433
+ return ok(lines.join("\n"));
1434
+ }
1435
+ function toolList(cwd, args) {
1436
+ const { track, untrack } = args;
1437
+ const lock = readLock(cwd);
1438
+ const ides = detectIDEs(cwd);
1439
+ if (track || untrack) {
1440
+ const skillName = track ?? untrack;
1441
+ if (!lock.skills[skillName]) return fail(`"${skillName}" not found in skills-lock.json`);
1442
+ setSkillTracked(lock, skillName, !!track);
1443
+ writeLock(lock, cwd);
1444
+ syncGitignore(cwd, ides, lock);
1445
+ return ok(
1446
+ `${track ? "tracked" : "untracked"} "${skillName}" \u2014 ${track ? "will be committed to git" : "excluded from git"}`
1447
+ );
1448
+ }
1449
+ const rules = readRules(cwd);
1450
+ const stages = listStages(rules ?? { stages: {} });
1451
+ const skills = Object.entries(lock.skills);
1452
+ if (skills.length === 0) return ok("No skills installed yet. Run: sr add");
1453
+ const header = `${"SKILL".padEnd(22)} ${"STAGES".padEnd(18)} INSTALLED TRACKING`;
1454
+ const lines = [header, ""];
1455
+ for (const [name, info] of skills) {
1456
+ const sources = findSkillSources(cwd, ides, name);
1457
+ const skillStages = stages.filter((s) => (rules?.stages[s] ?? []).includes(name));
1458
+ const installed = sources.length === 0 ? "missing" : sources.length === ides.length ? "all IDEs" : "partial";
1459
+ const tracking = info.track ? "git" : "ignored";
1460
+ lines.push(
1461
+ `${name.padEnd(22)} ${(skillStages.join(", ") || "\u2014").padEnd(18)} ${installed.padEnd(13)}${tracking}`
1462
+ );
1463
+ }
1464
+ return ok(lines.join("\n"));
1465
+ }
1466
+ function toolIgnore(cwd) {
1467
+ const ides = detectIDEs(cwd);
1468
+ if (ides.length === 0) return fail("No IDE directories detected. Nothing to ignore.");
1469
+ const lock = readLock(cwd);
1470
+ syncGitignore(cwd, ides, lock);
1471
+ return ok(
1472
+ `.gitignore updated for ${ides.length} IDE${ides.length !== 1 ? "s" : ""}: ${ides.map((i) => i.name).join(", ")}`
1473
+ );
1474
+ }
1475
+ async function startMcpServer() {
1476
+ const cwd = process.cwd();
1477
+ const server = new Server(
1478
+ { name: "skill-rules", version: "0.2.0" },
1479
+ { capabilities: { tools: {} } }
1480
+ );
1481
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
1482
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1483
+ const { name, arguments: args = {} } = request.params;
1484
+ try {
1485
+ switch (name) {
1486
+ case "sync":
1487
+ return toolSync(cwd, args);
1488
+ case "status":
1489
+ return toolStatus(cwd);
1490
+ case "init":
1491
+ return toolInit(cwd);
1492
+ case "add":
1493
+ return toolAdd(cwd, args);
1494
+ case "remove":
1495
+ return toolRemove(cwd, args);
1496
+ case "use":
1497
+ return toolUse(cwd, args);
1498
+ case "list":
1499
+ return toolList(cwd, args);
1500
+ case "ignore":
1501
+ return toolIgnore(cwd);
1502
+ default:
1503
+ return fail(`Unknown tool: ${name}`);
1504
+ }
1505
+ } catch (e) {
1506
+ return fail(e.message);
1507
+ }
1508
+ });
1509
+ const transport = new StdioServerTransport();
1510
+ await server.connect(transport);
1511
+ }
1512
+
1513
+ // src/commands/mcp.js
1514
+ async function mcp() {
1515
+ await startMcpServer();
1516
+ }
1517
+
1104
1518
  // src/cli.jsx
1105
1519
  var __dirname = dirname(fileURLToPath(import.meta.url));
1106
- var { version } = JSON.parse(readFileSync5(join11(__dirname, "../package.json"), "utf8"));
1520
+ var { version } = JSON.parse(readFileSync5(join12(__dirname, "../package.json"), "utf8"));
1107
1521
  var stageOption = ["-s, --stage <stage>", "limit to a specific stage (e.g. dev, qa)"];
1108
1522
  function createCli() {
1109
1523
  const program = new Command();
@@ -1115,6 +1529,7 @@ function createCli() {
1115
1529
  program.command("ignore").description("Regenerate .gitignore for detected IDE skill directories").action(ignore);
1116
1530
  program.command("use [stage]").description("Activate a stage \u2014 stashes skills not in it, restores those that are").option("--off", "restore all stashed skills and clear the active stage").action(use);
1117
1531
  program.command("help").description("Show help for all commands").action(help);
1532
+ program.command("mcp").description("Start the skill-rules MCP server (stdio transport)").action(mcp);
1118
1533
  return program;
1119
1534
  }
1120
1535
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-rules",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Sync AI agent skills across Claude Code, Cursor, Windsurf and more — activate per-environment rule sets with one command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,7 @@
15
15
  ],
16
16
  "dependencies": {
17
17
  "@inkjs/ui": "~2.0.0",
18
+ "@modelcontextprotocol/sdk": "~1.29.0",
18
19
  "commander": "~12.1.0",
19
20
  "figures": "~6.1.0",
20
21
  "ink": "~5.2.1",