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.
- package/README.md +64 -8
- package/dist/index.js +421 -6
- 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
|
|
35
|
-
|
|
|
36
|
-
| Claude Code
|
|
37
|
-
| Cursor
|
|
38
|
-
| Windsurf
|
|
39
|
-
| OpenHands
|
|
40
|
-
|
|
|
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 `.
|
|
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
|
|
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,
|
|
40
|
-
name: "Agents (Copilot, Cline, VS Code,
|
|
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
|
|
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 },
|
|
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(
|
|
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.
|
|
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",
|