agent-skills-standard 2.4.8 → 2.5.1

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
@@ -5,7 +5,7 @@
5
5
 
6
6
  **Sync 242 AI coding standards to any project in one command.** Works with Cursor, Claude Code, GitHub Copilot, Gemini, Windsurf, Trae, Kiro, and Roo.
7
7
 
8
- **Current release:** `v2.4.7` — workflow-end telemetry guidance, MCP session-cost helpers, and clearer runtime integration for SDLC handoff reporting.
8
+ **Current release:** `v2.5.1` — automated SkillSpector security scanning, pull request evaluation gates, and verified release tags.
9
9
 
10
10
  ```bash
11
11
  npx agent-skills-standard@latest init # detect your stack
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ let child_process = require("child_process");
43
43
  let fs = require("fs");
44
44
  var package_default = {
45
45
  name: "agent-skills-standard",
46
- version: "2.4.8",
46
+ version: "2.5.1",
47
47
  description: "A CLI to manage and sync AI agent skills standard for Cursor, Claude, Copilot, Windsurf, and more.",
48
48
  main: "dist/index.js",
49
49
  bin: {
@@ -83,7 +83,7 @@ var package_default = {
83
83
  "dotenv": "^17.2.3",
84
84
  "fs-extra": "^11.2.0",
85
85
  "inquirer": "^8.2.5",
86
- "js-yaml": "^4.1.0",
86
+ "js-yaml": "^4.2.0",
87
87
  "picocolors": "^1.0.0",
88
88
  "zod": "^3.22.4"
89
89
  },
@@ -342,6 +342,7 @@ var Framework = /* @__PURE__ */ function(Framework) {
342
342
  Framework["Android"] = "android";
343
343
  Framework["iOS"] = "ios";
344
344
  Framework["Laravel"] = "laravel";
345
+ Framework["Python"] = "python";
345
346
  return Framework;
346
347
  }({});
347
348
  //#endregion
@@ -672,7 +673,8 @@ var BACKEND_FRAMEWORKS = [
672
673
  Framework.NestJS,
673
674
  Framework.Golang,
674
675
  Framework.SpringBoot,
675
- Framework.Laravel
676
+ Framework.Laravel,
677
+ Framework.Python
676
678
  ];
677
679
  var FRONTEND_FRAMEWORKS = [
678
680
  Framework.React,
@@ -1007,6 +1009,20 @@ var getFrameworkDefinition = (id) => {
1007
1009
  javascript: ["resources/js", "vite.config.js"]
1008
1010
  }
1009
1011
  };
1012
+ case Framework.Python: return {
1013
+ id,
1014
+ name: "Python",
1015
+ languages: ["python"],
1016
+ detectionFiles: [
1017
+ "pyproject.toml",
1018
+ "requirements.txt",
1019
+ "setup.py",
1020
+ "setup.cfg",
1021
+ "Pipfile",
1022
+ "poetry.lock",
1023
+ "uv.lock"
1024
+ ]
1025
+ };
1010
1026
  }
1011
1027
  };
1012
1028
  var SUPPORTED_AGENTS = Object.values(Agent).map(getAgentDefinition);
@@ -1042,11 +1058,181 @@ var SkillConfigSchema = zod.z.object({
1042
1058
  mcp: McpConfigSchema.optional()
1043
1059
  });
1044
1060
  var DEFAULT_SDLC_SUPPORT_CATEGORIES = ["quality-engineering"];
1061
+ var PRIMARY_CONFIG_FILE = ".skillsrc";
1062
+ var LEGACY_YAML_CONFIG_FILE = ".skillsrc.yaml";
1045
1063
  /**
1046
1064
  * Service for managing the `.skillsrc` configuration file.
1047
1065
  * Handles loading, saving, and initial construction of the configuration based on project metadata.
1048
1066
  */
1049
1067
  var ConfigService = class {
1068
+ getPrimaryConfigPath(cwd) {
1069
+ return path.default.join(cwd, PRIMARY_CONFIG_FILE);
1070
+ }
1071
+ getLegacyYamlConfigPath(cwd) {
1072
+ return path.default.join(cwd, LEGACY_YAML_CONFIG_FILE);
1073
+ }
1074
+ async resolveConfigPath(cwd) {
1075
+ const primaryPath = this.getPrimaryConfigPath(cwd);
1076
+ if (await fs_extra.default.pathExists(primaryPath)) return primaryPath;
1077
+ const legacyYamlPath = this.getLegacyYamlConfigPath(cwd);
1078
+ if (await fs_extra.default.pathExists(legacyYamlPath)) return legacyYamlPath;
1079
+ return null;
1080
+ }
1081
+ dumpConfig(config) {
1082
+ return js_yaml.default.dump(config, {
1083
+ noRefs: true,
1084
+ lineWidth: -1
1085
+ });
1086
+ }
1087
+ isRecord(value) {
1088
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1089
+ }
1090
+ stripAngleBrackets(value) {
1091
+ const trimmed = value.trim();
1092
+ if (trimmed.startsWith("<") && trimmed.endsWith(">")) return trimmed.slice(1, -1).trim();
1093
+ return trimmed;
1094
+ }
1095
+ parseLegacyList(value) {
1096
+ const trimmed = value.trim();
1097
+ if (!trimmed || trimmed === "[]") return [];
1098
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed.slice(1, -1).split(",").map((item) => item.trim()).filter(Boolean);
1099
+ if (trimmed.startsWith("- ")) return trimmed.replace(/^-\s+/, "").split(/\s+-\s+/).map((item) => item.trim()).filter(Boolean);
1100
+ return trimmed.split(",").map((item) => item.trim()).filter(Boolean);
1101
+ }
1102
+ parseLegacyBoolean(value) {
1103
+ const normalized = value.trim().toLowerCase();
1104
+ if (normalized === "true") return true;
1105
+ if (normalized === "false") return false;
1106
+ }
1107
+ migrateLegacyTextConfig(content) {
1108
+ const lines = content.split(/\r?\n/);
1109
+ const registryLine = lines.find((line) => line.trim().startsWith("registry:"));
1110
+ const agentsIndex = lines.findIndex((line) => line.trim() === "agents:");
1111
+ const skillsIndex = lines.findIndex((line) => line.trim() === "skills:");
1112
+ if (!registryLine || agentsIndex === -1 || skillsIndex === -1) return null;
1113
+ const registryMatch = registryLine.match(/^\s*registry:\s*(.+)$/);
1114
+ if (!registryMatch) return null;
1115
+ const config = {
1116
+ registry: this.stripAngleBrackets(registryMatch[1]),
1117
+ agents: [],
1118
+ skills: {}
1119
+ };
1120
+ const agents = [];
1121
+ for (let index = agentsIndex + 1; index < skillsIndex; index += 1) {
1122
+ const match = lines[index].match(/^\s*-\s+(.+)$/);
1123
+ if (!match) continue;
1124
+ agents.push(match[1].trim());
1125
+ }
1126
+ config.agents = agents;
1127
+ const sectionNames = new Set([
1128
+ "custom_overrides",
1129
+ "workflows",
1130
+ "mcp"
1131
+ ]);
1132
+ let currentCategory = null;
1133
+ for (let index = skillsIndex + 1; index < lines.length; index += 1) {
1134
+ const trimmed = lines[index].trim();
1135
+ if (!trimmed) continue;
1136
+ if (trimmed === "workflows:" || trimmed === "mcp:" || trimmed.startsWith("custom_overrides:")) break;
1137
+ const categoryMatch = trimmed.match(/^([A-Za-z0-9-]+):$/);
1138
+ if (categoryMatch && !sectionNames.has(categoryMatch[1])) {
1139
+ currentCategory = categoryMatch[1];
1140
+ config.skills[currentCategory] = {};
1141
+ continue;
1142
+ }
1143
+ if (!currentCategory) continue;
1144
+ const refMatch = trimmed.match(/^ref:\s*(.+)$/);
1145
+ if (refMatch) {
1146
+ config.skills[currentCategory].ref = refMatch[1].trim();
1147
+ continue;
1148
+ }
1149
+ const excludeMatch = trimmed.match(/^exclude:\s*(.+)$/);
1150
+ if (excludeMatch) {
1151
+ config.skills[currentCategory].exclude = this.parseLegacyList(excludeMatch[1]);
1152
+ continue;
1153
+ }
1154
+ const includeMatch = trimmed.match(/^include:\s*(.+)$/);
1155
+ if (includeMatch) config.skills[currentCategory].include = this.parseLegacyList(includeMatch[1]);
1156
+ }
1157
+ const overridesLine = lines.find((line) => line.trim().startsWith("custom_overrides:"));
1158
+ if (overridesLine) {
1159
+ const match = overridesLine.match(/^\s*custom_overrides:\s*(.*)$/);
1160
+ config.custom_overrides = this.parseLegacyList(match?.[1] ?? "");
1161
+ }
1162
+ const workflowsIndex = lines.findIndex((line) => line.trim() === "workflows:");
1163
+ if (workflowsIndex !== -1) {
1164
+ const workflows = [];
1165
+ for (let index = workflowsIndex + 1; index < lines.length; index += 1) {
1166
+ const trimmed = lines[index].trim();
1167
+ if (!trimmed) continue;
1168
+ if (trimmed === "mcp:") break;
1169
+ const match = trimmed.match(/^-\s+(.+)$/);
1170
+ if (match) workflows.push(match[1].trim());
1171
+ }
1172
+ config.workflows = workflows;
1173
+ }
1174
+ const mcpIndex = lines.findIndex((line) => line.trim() === "mcp:");
1175
+ if (mcpIndex !== -1) {
1176
+ const mcp = {};
1177
+ for (let index = mcpIndex + 1; index < lines.length; index += 1) {
1178
+ const trimmed = lines[index].trim();
1179
+ if (!trimmed) continue;
1180
+ const match = trimmed.match(/^([A-Za-z0-9_]+):\s*(.+)$/);
1181
+ if (!match) continue;
1182
+ const [, key, rawValue] = match;
1183
+ if (key === "enabled" || key === "prompted" || key === "snippets") mcp[key] = this.parseLegacyBoolean(rawValue);
1184
+ else mcp[key] = rawValue.trim();
1185
+ }
1186
+ config.mcp = mcp;
1187
+ }
1188
+ return config;
1189
+ }
1190
+ normalizeRawConfig(rawConfig) {
1191
+ if (!this.isRecord(rawConfig)) return {
1192
+ config: {},
1193
+ migrated: false
1194
+ };
1195
+ const normalized = { ...rawConfig };
1196
+ let migrated = false;
1197
+ if (typeof normalized.registry === "string") {
1198
+ const strippedRegistry = this.stripAngleBrackets(normalized.registry);
1199
+ if (strippedRegistry !== normalized.registry) {
1200
+ normalized.registry = strippedRegistry;
1201
+ migrated = true;
1202
+ }
1203
+ }
1204
+ if (Array.isArray(normalized.agents)) normalized.agents = normalized.agents.map((agent) => {
1205
+ if (typeof agent === "string" && agent.toLowerCase() === "openai") {
1206
+ migrated = true;
1207
+ return Agent.Codex;
1208
+ }
1209
+ return agent;
1210
+ });
1211
+ if (this.isRecord(normalized.skills)) {
1212
+ const normalizedSkills = {};
1213
+ for (const [category, value] of Object.entries(normalized.skills)) {
1214
+ if (!this.isRecord(value)) {
1215
+ normalizedSkills[category] = {};
1216
+ migrated = true;
1217
+ continue;
1218
+ }
1219
+ const entry = {};
1220
+ if (typeof value.ref === "string") entry.ref = value.ref;
1221
+ if (Array.isArray(value.include)) entry.include = value.include.filter((item) => typeof item === "string");
1222
+ if (Array.isArray(value.exclude)) entry.exclude = value.exclude.filter((item) => typeof item === "string");
1223
+ else if (typeof value.exclude === "string") {
1224
+ entry.exclude = this.parseLegacyList(value.exclude);
1225
+ migrated = true;
1226
+ }
1227
+ normalizedSkills[category] = entry;
1228
+ }
1229
+ normalized.skills = normalizedSkills;
1230
+ }
1231
+ return {
1232
+ config: normalized,
1233
+ migrated
1234
+ };
1235
+ }
1050
1236
  getCategoryRef(category, metadata) {
1051
1237
  const categoryMetadata = metadata.categories?.[category];
1052
1238
  if (!categoryMetadata?.version) return "main";
@@ -1059,27 +1245,26 @@ var ConfigService = class {
1059
1245
  * @throws Error if the configuration format is invalid
1060
1246
  */
1061
1247
  async loadConfig(cwd = process.cwd()) {
1062
- const configPath = path.default.join(cwd, ".skillsrc");
1063
- if (!await fs_extra.default.pathExists(configPath)) return null;
1248
+ const configPath = await this.resolveConfigPath(cwd);
1249
+ if (!configPath) return null;
1250
+ const primaryConfigPath = this.getPrimaryConfigPath(cwd);
1251
+ const shouldRenameLegacyYamlFile = configPath !== primaryConfigPath;
1064
1252
  try {
1065
1253
  const content = await fs_extra.default.readFile(configPath, "utf8");
1066
- const rawConfig = js_yaml.default.load(content);
1067
- if (rawConfig && Array.isArray(rawConfig.agents)) {
1068
- let needsMigration = false;
1069
- const updatedAgents = rawConfig.agents.map((agent) => {
1070
- if (typeof agent === "string" && agent.toLowerCase() === "openai") {
1071
- needsMigration = true;
1072
- return Agent.Codex;
1073
- }
1074
- return agent;
1075
- });
1076
- if (needsMigration) {
1077
- rawConfig.agents = updatedAgents;
1078
- await fs_extra.default.outputFile(configPath, js_yaml.default.dump(rawConfig));
1079
- }
1254
+ let rawConfig;
1255
+ let migratedFromText = false;
1256
+ try {
1257
+ rawConfig = js_yaml.default.load(content);
1258
+ } catch (parseError) {
1259
+ rawConfig = this.migrateLegacyTextConfig(content);
1260
+ migratedFromText = rawConfig !== null;
1261
+ if (!migratedFromText) throw parseError;
1080
1262
  }
1081
- const parsed = SkillConfigSchema.safeParse(rawConfig);
1263
+ const { config: normalizedConfig, migrated } = this.normalizeRawConfig(rawConfig);
1264
+ const parsed = SkillConfigSchema.safeParse(normalizedConfig);
1082
1265
  if (!parsed.success) throw new Error(`Invalid .skillsrc format: ${parsed.error.message}`);
1266
+ if (migratedFromText || migrated || shouldRenameLegacyYamlFile) await fs_extra.default.outputFile(primaryConfigPath, this.dumpConfig(parsed.data));
1267
+ if (shouldRenameLegacyYamlFile) await fs_extra.default.remove(configPath);
1083
1268
  return parsed.data;
1084
1269
  } catch (error) {
1085
1270
  throw new Error(`Failed to load config: ${error}`);
@@ -1091,8 +1276,10 @@ var ConfigService = class {
1091
1276
  * @param cwd Current working directory
1092
1277
  */
1093
1278
  async saveConfig(config, cwd = process.cwd()) {
1094
- const configPath = path.default.join(cwd, ".skillsrc");
1095
- await fs_extra.default.outputFile(configPath, js_yaml.default.dump(config));
1279
+ const primaryConfigPath = this.getPrimaryConfigPath(cwd);
1280
+ const legacyYamlPath = this.getLegacyYamlConfigPath(cwd);
1281
+ await fs_extra.default.outputFile(primaryConfigPath, this.dumpConfig(config));
1282
+ if (legacyYamlPath !== primaryConfigPath && await fs_extra.default.pathExists(legacyYamlPath)) await fs_extra.default.remove(legacyYamlPath);
1096
1283
  }
1097
1284
  /**
1098
1285
  * Constructs an initial configuration object based on project analysis.
@@ -1289,7 +1476,9 @@ process.stdin.on('end', () => {
1289
1476
  '[SKILL TRIGGER] Editing: ' + fileName + '\\n' +
1290
1477
  '-> Call load_skills_for_files(files=[ "' + filePath + '" ]) on the ' +
1291
1478
  'agent-skills-standard MCP. It returns applicable SKILL.md rules, ' +
1292
- 'or nothing if no skills match this file type.'
1479
+ 'or nothing if no skills match this file type.\\n' +
1480
+ '-> If this work spans a whole framework or migration, also call ' +
1481
+ 'get_category_guide(category="...") for the framework-level map.'
1293
1482
  );
1294
1483
  process.exit(0);
1295
1484
  } catch {
@@ -4188,25 +4377,35 @@ var MarkdownUtils = class {
4188
4377
  * @param rootDir Project root directory
4189
4378
  * @param targets Array of target file names
4190
4379
  * @param indexContent The markdown content to inject
4380
+ * @returns Array of target files that were successfully updated/created
4191
4381
  */
4192
4382
  static async injectIndex(rootDir, targets, indexContent) {
4383
+ const updated = [];
4193
4384
  for (const target of targets) {
4194
4385
  const targetPath = path.default.join(rootDir, target);
4195
4386
  let content = "";
4387
+ const markerStart = "<!-- SKILLS_INDEX_START -->";
4388
+ const markerEnd = "<!-- SKILLS_INDEX_END -->";
4196
4389
  if (await fs_extra.default.pathExists(targetPath)) {
4197
4390
  content = await fs_extra.default.readFile(targetPath, "utf8");
4198
- const markerStart = "<!-- SKILLS_INDEX_START -->";
4199
- const markerEnd = "<!-- SKILLS_INDEX_END -->";
4200
4391
  const startIndex = content.indexOf(markerStart);
4201
4392
  const endIndex = content.indexOf(markerEnd);
4202
4393
  if (startIndex !== -1 && endIndex !== -1 && startIndex < endIndex) content = `${content.substring(0, startIndex + 27)}\n${indexContent}\n${content.substring(endIndex)}`;
4203
- else if (startIndex !== -1 || endIndex !== -1) {
4204
- content = content.replace(markerStart, "").replace(markerEnd, "");
4205
- content = content.trimEnd() + `\n\n${markerStart}\n${indexContent}\n${markerEnd}\n`;
4206
- } else content = content.trimEnd() + `\n\n${markerStart}\n${indexContent}\n${markerEnd}\n`;
4207
- } else content = `<!-- SKILLS_INDEX_START -->\n${indexContent}\n<!-- SKILLS_INDEX_END -->\n`;
4394
+ else continue;
4395
+ } else content = [
4396
+ "# Project Context for AI Agents",
4397
+ "",
4398
+ "This file provides context and instructions for AI agents working in this repository.",
4399
+ "",
4400
+ markerStart,
4401
+ indexContent,
4402
+ markerEnd,
4403
+ ""
4404
+ ].join("\n");
4208
4405
  await fs_extra.default.outputFile(targetPath, content);
4406
+ updated.push(target);
4209
4407
  }
4408
+ return updated;
4210
4409
  }
4211
4410
  };
4212
4411
  //#endregion
@@ -4302,13 +4501,11 @@ var SyncService = class {
4302
4501
  if (Object.keys(categoryIndices).length > 0) console.log(picocolors.default.green(` ✅ Generated _INDEX.md for ${Object.keys(categoryIndices).length} categories.`));
4303
4502
  const mcpEnabled = config.mcp?.enabled === true;
4304
4503
  const routerIndex = await generator.assembleRouterIndex(baseDir, allowedCategories, mcpEnabled);
4305
- await MarkdownUtils.injectIndex(process.cwd(), ["AGENTS.md"], routerIndex);
4306
- console.log(picocolors.default.green(" ✅ AGENTS.md router index updated."));
4504
+ if ((await MarkdownUtils.injectIndex(process.cwd(), ["AGENTS.md"], routerIndex)).length > 0) console.log(picocolors.default.green(" ✅ AGENTS.md router index updated."));
4505
+ else console.log(picocolors.default.yellow(" ⚠️ Skipped AGENTS.md update: index markers <!-- SKILLS_INDEX_START --> … <!-- SKILLS_INDEX_END --> are missing or out of order. Opt-in by adding these markers to your file."));
4307
4506
  const serverDir = path.default.join(process.cwd(), "server");
4308
- if (await fs_extra.default.pathExists(serverDir)) {
4309
- await MarkdownUtils.injectIndex(serverDir, ["AGENTS.md"], routerIndex);
4310
- console.log(picocolors.default.green(" ✅ server/AGENTS.md router index updated."));
4311
- }
4507
+ if (await fs_extra.default.pathExists(serverDir)) if ((await MarkdownUtils.injectIndex(serverDir, ["AGENTS.md"], routerIndex)).length > 0) console.log(picocolors.default.green(" ✅ server/AGENTS.md router index updated."));
4508
+ else console.log(picocolors.default.yellow(" ⚠️ Skipped server/AGENTS.md update: index markers <!-- SKILLS_INDEX_START --> … <!-- SKILLS_INDEX_END --> are missing or out of order in server/AGENTS.md. Opt-in via markers."));
4312
4509
  await new AgentBridgeService().bridge(process.cwd(), agents);
4313
4510
  } catch (error) {
4314
4511
  console.log(picocolors.default.yellow(` ⚠️ Failed to update index: ${error}`));