agentpacks 0.9.0 → 1.0.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 (67) hide show
  1. package/README.md +41 -14
  2. package/dist/api.js +403 -179
  3. package/dist/cli/export-cmd.js +58 -5
  4. package/dist/cli/generate.js +268 -59
  5. package/dist/cli/import-cmd.js +203 -95
  6. package/dist/cli/install.js +1 -0
  7. package/dist/cli/models-explain.js +56 -0
  8. package/dist/cli/pack/list.js +56 -0
  9. package/dist/cli/pack/validate.js +143 -18
  10. package/dist/cli/publish.js +1 -0
  11. package/dist/core/config.d.ts +1 -1
  12. package/dist/core/config.js +1 -0
  13. package/dist/core/index.js +56 -0
  14. package/dist/core/metarepo.js +1 -0
  15. package/dist/core/pack-loader.js +56 -0
  16. package/dist/exporters/cursor-plugin.js +109 -22
  17. package/dist/exporters/index.js +109 -22
  18. package/dist/features/index.d.ts +1 -1
  19. package/dist/features/index.js +59 -0
  20. package/dist/features/skills.d.ts +22 -0
  21. package/dist/features/skills.js +60 -1
  22. package/dist/importers/cursor.js +122 -26
  23. package/dist/importers/opencode.js +138 -24
  24. package/dist/importers/rulesync.js +147 -33
  25. package/dist/index.js +484 -244
  26. package/dist/node/api.js +403 -179
  27. package/dist/node/cli/export-cmd.js +58 -5
  28. package/dist/node/cli/generate.js +268 -59
  29. package/dist/node/cli/import-cmd.js +203 -95
  30. package/dist/node/cli/install.js +1 -0
  31. package/dist/node/cli/models-explain.js +56 -0
  32. package/dist/node/cli/pack/list.js +56 -0
  33. package/dist/node/cli/pack/validate.js +143 -18
  34. package/dist/node/cli/publish.js +1 -0
  35. package/dist/node/core/config.js +1 -0
  36. package/dist/node/core/index.js +56 -0
  37. package/dist/node/core/metarepo.js +1 -0
  38. package/dist/node/core/pack-loader.js +56 -0
  39. package/dist/node/exporters/cursor-plugin.js +109 -22
  40. package/dist/node/exporters/index.js +109 -22
  41. package/dist/node/features/index.js +59 -0
  42. package/dist/node/features/skills.js +60 -1
  43. package/dist/node/importers/cursor.js +122 -26
  44. package/dist/node/importers/opencode.js +138 -24
  45. package/dist/node/importers/rulesync.js +147 -33
  46. package/dist/node/index.js +484 -244
  47. package/dist/node/targets/claude-code.js +56 -1
  48. package/dist/node/targets/codex-cli.js +56 -1
  49. package/dist/node/targets/copilot.js +56 -1
  50. package/dist/node/targets/cursor.js +56 -5
  51. package/dist/node/targets/index.js +268 -59
  52. package/dist/node/targets/mistral-vibe.js +661 -0
  53. package/dist/node/targets/opencode.js +56 -1
  54. package/dist/node/targets/registry.js +267 -59
  55. package/dist/node/utils/model-allowlist.js +6 -2
  56. package/dist/targets/claude-code.js +56 -1
  57. package/dist/targets/codex-cli.js +56 -1
  58. package/dist/targets/copilot.js +56 -1
  59. package/dist/targets/cursor.js +56 -5
  60. package/dist/targets/index.d.ts +1 -0
  61. package/dist/targets/index.js +268 -59
  62. package/dist/targets/mistral-vibe.d.ts +13 -0
  63. package/dist/targets/mistral-vibe.js +661 -0
  64. package/dist/targets/opencode.js +56 -1
  65. package/dist/targets/registry.js +267 -59
  66. package/dist/utils/model-allowlist.js +6 -2
  67. package/package.json +15 -3
@@ -129,11 +129,101 @@ function serializeFrontmatter(data, content) {
129
129
  return matter.stringify(content, filtered);
130
130
  }
131
131
 
132
+ // src/features/skills.ts
133
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
134
+ import { basename, join as join2 } from "path";
135
+ var SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
136
+ var SKILL_NAME_MAX_LENGTH = 64;
137
+ function parseSkills(skillsDir, packName) {
138
+ const dirs = listDirs(skillsDir);
139
+ const skills = [];
140
+ for (const dir of dirs) {
141
+ const skillMd = join2(dir, "SKILL.md");
142
+ if (existsSync2(skillMd)) {
143
+ skills.push(parseSkillFile(skillMd, dir, packName));
144
+ }
145
+ }
146
+ return skills;
147
+ }
148
+ function parseSkillFile(filepath, skillDir, packName) {
149
+ const raw = readFileSync2(filepath, "utf-8");
150
+ const { data, content } = parseFrontmatter(raw);
151
+ return {
152
+ name: data.name ?? basename(skillDir),
153
+ sourcePath: filepath,
154
+ sourceDir: skillDir,
155
+ packName,
156
+ meta: data,
157
+ content
158
+ };
159
+ }
160
+ function buildSkillFrontmatter(skill) {
161
+ return {
162
+ ...skill.meta,
163
+ name: skill.name
164
+ };
165
+ }
166
+ function serializeSkill(skill) {
167
+ return serializeFrontmatter(buildSkillFrontmatter(skill), skill.content);
168
+ }
169
+ function normalizeImportedSkillMarkdown(source, skillName) {
170
+ const { data, content } = parseFrontmatter(source);
171
+ const normalized = {
172
+ ...data,
173
+ name: skillName
174
+ };
175
+ let addedDescription = false;
176
+ const description = normalized.description;
177
+ if (typeof description !== "string" || description.trim().length === 0) {
178
+ normalized.description = `Imported skill: ${skillName}`;
179
+ addedDescription = true;
180
+ }
181
+ return {
182
+ content: serializeFrontmatter(normalized, content),
183
+ addedDescription
184
+ };
185
+ }
186
+ function validateAgentSkillsFrontmatter(skill) {
187
+ const errors = [];
188
+ const dirName = basename(skill.sourceDir);
189
+ const declaredName = skill.meta.name;
190
+ if (typeof declaredName !== "string" || declaredName.trim().length === 0) {
191
+ errors.push('Missing required frontmatter field "name".');
192
+ } else {
193
+ if (declaredName.length > SKILL_NAME_MAX_LENGTH) {
194
+ errors.push(`Invalid "name": must be at most ${SKILL_NAME_MAX_LENGTH} characters.`);
195
+ }
196
+ if (!SKILL_NAME_PATTERN.test(declaredName)) {
197
+ errors.push('Invalid "name": use lowercase letters, numbers, and single hyphens only.');
198
+ }
199
+ if (declaredName !== dirName) {
200
+ errors.push(`Invalid "name": must match containing directory "${dirName}".`);
201
+ }
202
+ }
203
+ const description = skill.meta.description;
204
+ if (typeof description !== "string" || description.trim().length === 0) {
205
+ errors.push('Missing required frontmatter field "description".');
206
+ }
207
+ const allowedTools = skill.meta["allowed-tools"];
208
+ if (allowedTools !== undefined && (!Array.isArray(allowedTools) || allowedTools.some((tool) => typeof tool !== "string" || tool.length === 0))) {
209
+ errors.push('Invalid "allowed-tools": expected an array of non-empty strings.');
210
+ }
211
+ return errors;
212
+ }
213
+ function skillMatchesTarget(skill, targetId) {
214
+ const { targets } = skill.meta;
215
+ if (!targets || targets === "*")
216
+ return true;
217
+ if (Array.isArray(targets) && targets.includes("*"))
218
+ return true;
219
+ return Array.isArray(targets) && targets.includes(targetId);
220
+ }
221
+
132
222
  // src/features/hooks.ts
133
- import { join as join2 } from "path";
223
+ import { join as join3 } from "path";
134
224
  var TARGET_OVERRIDE_KEYS = ["cursor", "claudecode", "opencode"];
135
225
  function parseHooks(packDir, packName) {
136
- const hooksPath = join2(packDir, "hooks", "hooks.json");
226
+ const hooksPath = join3(packDir, "hooks", "hooks.json");
137
227
  const raw = readJsonOrNull(hooksPath);
138
228
  if (!raw)
139
229
  return null;
@@ -168,7 +258,7 @@ function resolveHooksForTarget(hooks, targetId) {
168
258
  }
169
259
 
170
260
  // src/exporters/cursor-plugin.ts
171
- import { resolve, join as join3 } from "path";
261
+ import { resolve, join as join4 } from "path";
172
262
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
173
263
  function exportCursorPlugin(pack, outputDir) {
174
264
  const filesWritten = [];
@@ -204,7 +294,7 @@ function exportCursorPlugin(pack, outputDir) {
204
294
  manifest.keywords = pack.manifest.tags;
205
295
  }
206
296
  if (pack.rules.length > 0) {
207
- const rulesDir = join3(pluginDir, "rules");
297
+ const rulesDir = join4(pluginDir, "rules");
208
298
  ensureDir(rulesDir);
209
299
  manifest.rules = "rules";
210
300
  for (const rule of pack.rules) {
@@ -217,13 +307,13 @@ function exportCursorPlugin(pack, outputDir) {
217
307
  if (globs)
218
308
  fm.globs = globs;
219
309
  const filename = `${rule.name}.mdc`;
220
- const filepath = join3(rulesDir, filename);
310
+ const filepath = join4(rulesDir, filename);
221
311
  writeFileSync2(filepath, serializeFrontmatter(fm, rule.content));
222
312
  filesWritten.push(filepath);
223
313
  }
224
314
  }
225
315
  if (pack.agents.length > 0) {
226
- const agentsDir = join3(pluginDir, "agents");
316
+ const agentsDir = join4(pluginDir, "agents");
227
317
  ensureDir(agentsDir);
228
318
  manifest.agents = "agents";
229
319
  for (const agent of pack.agents) {
@@ -232,29 +322,26 @@ function exportCursorPlugin(pack, outputDir) {
232
322
  description: agent.meta.description ?? ""
233
323
  };
234
324
  const filename = `${agent.name}.md`;
235
- const filepath = join3(agentsDir, filename);
325
+ const filepath = join4(agentsDir, filename);
236
326
  writeFileSync2(filepath, serializeFrontmatter(fm, agent.content));
237
327
  filesWritten.push(filepath);
238
328
  }
239
329
  }
240
330
  if (pack.skills.length > 0) {
241
- const skillsDir = join3(pluginDir, "skills");
331
+ const skillsDir = join4(pluginDir, "skills");
242
332
  ensureDir(skillsDir);
243
333
  manifest.skills = "skills";
244
334
  for (const skill of pack.skills) {
245
- const skillSubDir = join3(skillsDir, skill.name);
335
+ const skillSubDir = join4(skillsDir, skill.name);
246
336
  ensureDir(skillSubDir);
247
- const fm = {
248
- name: skill.name,
249
- description: skill.meta.description ?? ""
250
- };
251
- const filepath = join3(skillSubDir, "SKILL.md");
252
- writeFileSync2(filepath, serializeFrontmatter(fm, skill.content));
337
+ const filepath = join4(skillSubDir, "SKILL.md");
338
+ const content = serializeFrontmatter(buildSkillFrontmatter(skill), skill.content);
339
+ writeFileSync2(filepath, content);
253
340
  filesWritten.push(filepath);
254
341
  }
255
342
  }
256
343
  if (pack.commands.length > 0) {
257
- const commandsDir = join3(pluginDir, "commands");
344
+ const commandsDir = join4(pluginDir, "commands");
258
345
  ensureDir(commandsDir);
259
346
  manifest.commands = "commands";
260
347
  for (const cmd of pack.commands) {
@@ -265,7 +352,7 @@ function exportCursorPlugin(pack, outputDir) {
265
352
  fm.description = cmd.meta.description;
266
353
  }
267
354
  const filename = `${cmd.name}.md`;
268
- const filepath = join3(commandsDir, filename);
355
+ const filepath = join4(commandsDir, filename);
269
356
  writeFileSync2(filepath, serializeFrontmatter(fm, cmd.content));
270
357
  filesWritten.push(filepath);
271
358
  }
@@ -273,9 +360,9 @@ function exportCursorPlugin(pack, outputDir) {
273
360
  if (pack.hooks) {
274
361
  const events = resolveHooksForTarget(pack.hooks, "cursor");
275
362
  if (Object.keys(events).length > 0) {
276
- const hooksDir = join3(pluginDir, "hooks");
363
+ const hooksDir = join4(pluginDir, "hooks");
277
364
  ensureDir(hooksDir);
278
- const filepath = join3(hooksDir, "hooks.json");
365
+ const filepath = join4(hooksDir, "hooks.json");
279
366
  writeFileSync2(filepath, JSON.stringify({ version: pack.hooks.version ?? 1, hooks: events }, null, 2) + `
280
367
  `);
281
368
  filesWritten.push(filepath);
@@ -284,14 +371,14 @@ function exportCursorPlugin(pack, outputDir) {
284
371
  }
285
372
  if (pack.mcp && Object.keys(pack.mcp.servers).length > 0) {
286
373
  manifest.mcpServers = ".mcp.json";
287
- const filepath = join3(pluginDir, ".mcp.json");
374
+ const filepath = join4(pluginDir, ".mcp.json");
288
375
  writeFileSync2(filepath, JSON.stringify({ mcpServers: pack.mcp.servers }, null, 2) + `
289
376
  `);
290
377
  filesWritten.push(filepath);
291
378
  }
292
- const manifestDir = join3(pluginDir, ".cursor-plugin");
379
+ const manifestDir = join4(pluginDir, ".cursor-plugin");
293
380
  ensureDir(manifestDir);
294
- const manifestPath = join3(manifestDir, "plugin.json");
381
+ const manifestPath = join4(manifestDir, "plugin.json");
295
382
  writeFileSync2(manifestPath, JSON.stringify(manifest, null, 2) + `
296
383
  `);
297
384
  filesWritten.push(manifestPath);
@@ -129,11 +129,101 @@ function serializeFrontmatter(data, content) {
129
129
  return matter.stringify(content, filtered);
130
130
  }
131
131
 
132
+ // src/features/skills.ts
133
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
134
+ import { basename, join as join2 } from "path";
135
+ var SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
136
+ var SKILL_NAME_MAX_LENGTH = 64;
137
+ function parseSkills(skillsDir, packName) {
138
+ const dirs = listDirs(skillsDir);
139
+ const skills = [];
140
+ for (const dir of dirs) {
141
+ const skillMd = join2(dir, "SKILL.md");
142
+ if (existsSync2(skillMd)) {
143
+ skills.push(parseSkillFile(skillMd, dir, packName));
144
+ }
145
+ }
146
+ return skills;
147
+ }
148
+ function parseSkillFile(filepath, skillDir, packName) {
149
+ const raw = readFileSync2(filepath, "utf-8");
150
+ const { data, content } = parseFrontmatter(raw);
151
+ return {
152
+ name: data.name ?? basename(skillDir),
153
+ sourcePath: filepath,
154
+ sourceDir: skillDir,
155
+ packName,
156
+ meta: data,
157
+ content
158
+ };
159
+ }
160
+ function buildSkillFrontmatter(skill) {
161
+ return {
162
+ ...skill.meta,
163
+ name: skill.name
164
+ };
165
+ }
166
+ function serializeSkill(skill) {
167
+ return serializeFrontmatter(buildSkillFrontmatter(skill), skill.content);
168
+ }
169
+ function normalizeImportedSkillMarkdown(source, skillName) {
170
+ const { data, content } = parseFrontmatter(source);
171
+ const normalized = {
172
+ ...data,
173
+ name: skillName
174
+ };
175
+ let addedDescription = false;
176
+ const description = normalized.description;
177
+ if (typeof description !== "string" || description.trim().length === 0) {
178
+ normalized.description = `Imported skill: ${skillName}`;
179
+ addedDescription = true;
180
+ }
181
+ return {
182
+ content: serializeFrontmatter(normalized, content),
183
+ addedDescription
184
+ };
185
+ }
186
+ function validateAgentSkillsFrontmatter(skill) {
187
+ const errors = [];
188
+ const dirName = basename(skill.sourceDir);
189
+ const declaredName = skill.meta.name;
190
+ if (typeof declaredName !== "string" || declaredName.trim().length === 0) {
191
+ errors.push('Missing required frontmatter field "name".');
192
+ } else {
193
+ if (declaredName.length > SKILL_NAME_MAX_LENGTH) {
194
+ errors.push(`Invalid "name": must be at most ${SKILL_NAME_MAX_LENGTH} characters.`);
195
+ }
196
+ if (!SKILL_NAME_PATTERN.test(declaredName)) {
197
+ errors.push('Invalid "name": use lowercase letters, numbers, and single hyphens only.');
198
+ }
199
+ if (declaredName !== dirName) {
200
+ errors.push(`Invalid "name": must match containing directory "${dirName}".`);
201
+ }
202
+ }
203
+ const description = skill.meta.description;
204
+ if (typeof description !== "string" || description.trim().length === 0) {
205
+ errors.push('Missing required frontmatter field "description".');
206
+ }
207
+ const allowedTools = skill.meta["allowed-tools"];
208
+ if (allowedTools !== undefined && (!Array.isArray(allowedTools) || allowedTools.some((tool) => typeof tool !== "string" || tool.length === 0))) {
209
+ errors.push('Invalid "allowed-tools": expected an array of non-empty strings.');
210
+ }
211
+ return errors;
212
+ }
213
+ function skillMatchesTarget(skill, targetId) {
214
+ const { targets } = skill.meta;
215
+ if (!targets || targets === "*")
216
+ return true;
217
+ if (Array.isArray(targets) && targets.includes("*"))
218
+ return true;
219
+ return Array.isArray(targets) && targets.includes(targetId);
220
+ }
221
+
132
222
  // src/features/hooks.ts
133
- import { join as join2 } from "path";
223
+ import { join as join3 } from "path";
134
224
  var TARGET_OVERRIDE_KEYS = ["cursor", "claudecode", "opencode"];
135
225
  function parseHooks(packDir, packName) {
136
- const hooksPath = join2(packDir, "hooks", "hooks.json");
226
+ const hooksPath = join3(packDir, "hooks", "hooks.json");
137
227
  const raw = readJsonOrNull(hooksPath);
138
228
  if (!raw)
139
229
  return null;
@@ -168,7 +258,7 @@ function resolveHooksForTarget(hooks, targetId) {
168
258
  }
169
259
 
170
260
  // src/exporters/cursor-plugin.ts
171
- import { resolve, join as join3 } from "path";
261
+ import { resolve, join as join4 } from "path";
172
262
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
173
263
  function exportCursorPlugin(pack, outputDir) {
174
264
  const filesWritten = [];
@@ -204,7 +294,7 @@ function exportCursorPlugin(pack, outputDir) {
204
294
  manifest.keywords = pack.manifest.tags;
205
295
  }
206
296
  if (pack.rules.length > 0) {
207
- const rulesDir = join3(pluginDir, "rules");
297
+ const rulesDir = join4(pluginDir, "rules");
208
298
  ensureDir(rulesDir);
209
299
  manifest.rules = "rules";
210
300
  for (const rule of pack.rules) {
@@ -217,13 +307,13 @@ function exportCursorPlugin(pack, outputDir) {
217
307
  if (globs)
218
308
  fm.globs = globs;
219
309
  const filename = `${rule.name}.mdc`;
220
- const filepath = join3(rulesDir, filename);
310
+ const filepath = join4(rulesDir, filename);
221
311
  writeFileSync2(filepath, serializeFrontmatter(fm, rule.content));
222
312
  filesWritten.push(filepath);
223
313
  }
224
314
  }
225
315
  if (pack.agents.length > 0) {
226
- const agentsDir = join3(pluginDir, "agents");
316
+ const agentsDir = join4(pluginDir, "agents");
227
317
  ensureDir(agentsDir);
228
318
  manifest.agents = "agents";
229
319
  for (const agent of pack.agents) {
@@ -232,29 +322,26 @@ function exportCursorPlugin(pack, outputDir) {
232
322
  description: agent.meta.description ?? ""
233
323
  };
234
324
  const filename = `${agent.name}.md`;
235
- const filepath = join3(agentsDir, filename);
325
+ const filepath = join4(agentsDir, filename);
236
326
  writeFileSync2(filepath, serializeFrontmatter(fm, agent.content));
237
327
  filesWritten.push(filepath);
238
328
  }
239
329
  }
240
330
  if (pack.skills.length > 0) {
241
- const skillsDir = join3(pluginDir, "skills");
331
+ const skillsDir = join4(pluginDir, "skills");
242
332
  ensureDir(skillsDir);
243
333
  manifest.skills = "skills";
244
334
  for (const skill of pack.skills) {
245
- const skillSubDir = join3(skillsDir, skill.name);
335
+ const skillSubDir = join4(skillsDir, skill.name);
246
336
  ensureDir(skillSubDir);
247
- const fm = {
248
- name: skill.name,
249
- description: skill.meta.description ?? ""
250
- };
251
- const filepath = join3(skillSubDir, "SKILL.md");
252
- writeFileSync2(filepath, serializeFrontmatter(fm, skill.content));
337
+ const filepath = join4(skillSubDir, "SKILL.md");
338
+ const content = serializeFrontmatter(buildSkillFrontmatter(skill), skill.content);
339
+ writeFileSync2(filepath, content);
253
340
  filesWritten.push(filepath);
254
341
  }
255
342
  }
256
343
  if (pack.commands.length > 0) {
257
- const commandsDir = join3(pluginDir, "commands");
344
+ const commandsDir = join4(pluginDir, "commands");
258
345
  ensureDir(commandsDir);
259
346
  manifest.commands = "commands";
260
347
  for (const cmd of pack.commands) {
@@ -265,7 +352,7 @@ function exportCursorPlugin(pack, outputDir) {
265
352
  fm.description = cmd.meta.description;
266
353
  }
267
354
  const filename = `${cmd.name}.md`;
268
- const filepath = join3(commandsDir, filename);
355
+ const filepath = join4(commandsDir, filename);
269
356
  writeFileSync2(filepath, serializeFrontmatter(fm, cmd.content));
270
357
  filesWritten.push(filepath);
271
358
  }
@@ -273,9 +360,9 @@ function exportCursorPlugin(pack, outputDir) {
273
360
  if (pack.hooks) {
274
361
  const events = resolveHooksForTarget(pack.hooks, "cursor");
275
362
  if (Object.keys(events).length > 0) {
276
- const hooksDir = join3(pluginDir, "hooks");
363
+ const hooksDir = join4(pluginDir, "hooks");
277
364
  ensureDir(hooksDir);
278
- const filepath = join3(hooksDir, "hooks.json");
365
+ const filepath = join4(hooksDir, "hooks.json");
279
366
  writeFileSync2(filepath, JSON.stringify({ version: pack.hooks.version ?? 1, hooks: events }, null, 2) + `
280
367
  `);
281
368
  filesWritten.push(filepath);
@@ -284,14 +371,14 @@ function exportCursorPlugin(pack, outputDir) {
284
371
  }
285
372
  if (pack.mcp && Object.keys(pack.mcp.servers).length > 0) {
286
373
  manifest.mcpServers = ".mcp.json";
287
- const filepath = join3(pluginDir, ".mcp.json");
374
+ const filepath = join4(pluginDir, ".mcp.json");
288
375
  writeFileSync2(filepath, JSON.stringify({ mcpServers: pack.mcp.servers }, null, 2) + `
289
376
  `);
290
377
  filesWritten.push(filepath);
291
378
  }
292
- const manifestDir = join3(pluginDir, ".cursor-plugin");
379
+ const manifestDir = join4(pluginDir, ".cursor-plugin");
293
380
  ensureDir(manifestDir);
294
- const manifestPath = join3(manifestDir, "plugin.json");
381
+ const manifestPath = join4(manifestDir, "plugin.json");
295
382
  writeFileSync2(manifestPath, JSON.stringify(manifest, null, 2) + `
296
383
  `);
297
384
  filesWritten.push(manifestPath);
@@ -1,7 +1,7 @@
1
1
  export { type ParsedRule, type RuleFrontmatter, parseRules, ruleMatchesTarget, getRootRules, getDetailRules, } from './rules.js';
2
2
  export { type ParsedCommand, type CommandFrontmatter, parseCommands, commandMatchesTarget, } from './commands.js';
3
3
  export { type ParsedAgent, type AgentFrontmatter, parseAgents, agentMatchesTarget, } from './agents.js';
4
- export { type ParsedSkill, type SkillFrontmatter, parseSkills, skillMatchesTarget, } from './skills.js';
4
+ export { type ParsedSkill, type SkillFrontmatter, parseSkills, buildSkillFrontmatter, serializeSkill, normalizeImportedSkillMarkdown, validateAgentSkillsFrontmatter, skillMatchesTarget, } from './skills.js';
5
5
  export { type ParsedHooks, type HookEvents, type HookEntry, parseHooks, resolveHooksForTarget, } from './hooks.js';
6
6
  export { type ParsedPlugin, parsePlugins } from './plugins.js';
7
7
  export { type ParsedMcp, type McpServerEntry, parseMcp, mergeMcpConfigs, } from './mcp.js';
@@ -219,6 +219,8 @@ function agentMatchesTarget(agent, targetId) {
219
219
  // src/features/skills.ts
220
220
  import { readFileSync as readFileSync5, existsSync as existsSync2 } from "fs";
221
221
  import { basename as basename4, join as join2 } from "path";
222
+ var SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
223
+ var SKILL_NAME_MAX_LENGTH = 64;
222
224
  function parseSkills(skillsDir, packName) {
223
225
  const dirs = listDirs(skillsDir);
224
226
  const skills = [];
@@ -242,6 +244,59 @@ function parseSkillFile(filepath, skillDir, packName) {
242
244
  content
243
245
  };
244
246
  }
247
+ function buildSkillFrontmatter(skill) {
248
+ return {
249
+ ...skill.meta,
250
+ name: skill.name
251
+ };
252
+ }
253
+ function serializeSkill(skill) {
254
+ return serializeFrontmatter(buildSkillFrontmatter(skill), skill.content);
255
+ }
256
+ function normalizeImportedSkillMarkdown(source, skillName) {
257
+ const { data, content } = parseFrontmatter(source);
258
+ const normalized = {
259
+ ...data,
260
+ name: skillName
261
+ };
262
+ let addedDescription = false;
263
+ const description = normalized.description;
264
+ if (typeof description !== "string" || description.trim().length === 0) {
265
+ normalized.description = `Imported skill: ${skillName}`;
266
+ addedDescription = true;
267
+ }
268
+ return {
269
+ content: serializeFrontmatter(normalized, content),
270
+ addedDescription
271
+ };
272
+ }
273
+ function validateAgentSkillsFrontmatter(skill) {
274
+ const errors = [];
275
+ const dirName = basename4(skill.sourceDir);
276
+ const declaredName = skill.meta.name;
277
+ if (typeof declaredName !== "string" || declaredName.trim().length === 0) {
278
+ errors.push('Missing required frontmatter field "name".');
279
+ } else {
280
+ if (declaredName.length > SKILL_NAME_MAX_LENGTH) {
281
+ errors.push(`Invalid "name": must be at most ${SKILL_NAME_MAX_LENGTH} characters.`);
282
+ }
283
+ if (!SKILL_NAME_PATTERN.test(declaredName)) {
284
+ errors.push('Invalid "name": use lowercase letters, numbers, and single hyphens only.');
285
+ }
286
+ if (declaredName !== dirName) {
287
+ errors.push(`Invalid "name": must match containing directory "${dirName}".`);
288
+ }
289
+ }
290
+ const description = skill.meta.description;
291
+ if (typeof description !== "string" || description.trim().length === 0) {
292
+ errors.push('Missing required frontmatter field "description".');
293
+ }
294
+ const allowedTools = skill.meta["allowed-tools"];
295
+ if (allowedTools !== undefined && (!Array.isArray(allowedTools) || allowedTools.some((tool) => typeof tool !== "string" || tool.length === 0))) {
296
+ errors.push('Invalid "allowed-tools": expected an array of non-empty strings.');
297
+ }
298
+ return errors;
299
+ }
245
300
  function skillMatchesTarget(skill, targetId) {
246
301
  const { targets } = skill.meta;
247
302
  if (!targets || targets === "*")
@@ -547,7 +602,9 @@ function scanModelsForSecrets(config) {
547
602
  return warnings;
548
603
  }
549
604
  export {
605
+ validateAgentSkillsFrontmatter,
550
606
  skillMatchesTarget,
607
+ serializeSkill,
551
608
  scanModelsForSecrets,
552
609
  ruleMatchesTarget,
553
610
  resolveHooksForTarget,
@@ -560,12 +617,14 @@ export {
560
617
  parseHooks,
561
618
  parseCommands,
562
619
  parseAgents,
620
+ normalizeImportedSkillMarkdown,
563
621
  mergeModelsConfigs,
564
622
  mergeMcpConfigs,
565
623
  mergeIgnorePatterns,
566
624
  getRootRules,
567
625
  getDetailRules,
568
626
  commandMatchesTarget,
627
+ buildSkillFrontmatter,
569
628
  agentMatchesTarget,
570
629
  ModelsSchema
571
630
  };
@@ -4,6 +4,8 @@
4
4
  export interface SkillFrontmatter {
5
5
  name?: string;
6
6
  description?: string;
7
+ 'allowed-tools'?: string[];
8
+ compatibility?: unknown;
7
9
  targets?: string[] | '*';
8
10
  /** Per-target overrides */
9
11
  claudecode?: {
@@ -36,6 +38,26 @@ export declare function parseSkills(skillsDir: string, packName: string): Parsed
36
38
  * Parse a single SKILL.md file.
37
39
  */
38
40
  export declare function parseSkillFile(filepath: string, skillDir: string, packName: string): ParsedSkill;
41
+ /**
42
+ * Build output frontmatter for a SKILL.md file.
43
+ * Preserves all metadata while ensuring `name` exists.
44
+ */
45
+ export declare function buildSkillFrontmatter(skill: ParsedSkill): Record<string, unknown>;
46
+ /**
47
+ * Serialize a parsed skill back to SKILL.md format.
48
+ */
49
+ export declare function serializeSkill(skill: ParsedSkill): string;
50
+ /**
51
+ * Normalize imported SKILL.md content to AgentSkills-compatible minimum metadata.
52
+ */
53
+ export declare function normalizeImportedSkillMarkdown(source: string, skillName: string): {
54
+ content: string;
55
+ addedDescription: boolean;
56
+ };
57
+ /**
58
+ * Validate AgentSkills frontmatter requirements for a parsed skill.
59
+ */
60
+ export declare function validateAgentSkillsFrontmatter(skill: ParsedSkill): string[];
39
61
  /**
40
62
  * Check if a skill targets a specific tool.
41
63
  */
@@ -132,6 +132,8 @@ function serializeFrontmatter(data, content) {
132
132
  // src/features/skills.ts
133
133
  import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
134
134
  import { basename, join as join2 } from "path";
135
+ var SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
136
+ var SKILL_NAME_MAX_LENGTH = 64;
135
137
  function parseSkills(skillsDir, packName) {
136
138
  const dirs = listDirs(skillsDir);
137
139
  const skills = [];
@@ -155,6 +157,59 @@ function parseSkillFile(filepath, skillDir, packName) {
155
157
  content
156
158
  };
157
159
  }
160
+ function buildSkillFrontmatter(skill) {
161
+ return {
162
+ ...skill.meta,
163
+ name: skill.name
164
+ };
165
+ }
166
+ function serializeSkill(skill) {
167
+ return serializeFrontmatter(buildSkillFrontmatter(skill), skill.content);
168
+ }
169
+ function normalizeImportedSkillMarkdown(source, skillName) {
170
+ const { data, content } = parseFrontmatter(source);
171
+ const normalized = {
172
+ ...data,
173
+ name: skillName
174
+ };
175
+ let addedDescription = false;
176
+ const description = normalized.description;
177
+ if (typeof description !== "string" || description.trim().length === 0) {
178
+ normalized.description = `Imported skill: ${skillName}`;
179
+ addedDescription = true;
180
+ }
181
+ return {
182
+ content: serializeFrontmatter(normalized, content),
183
+ addedDescription
184
+ };
185
+ }
186
+ function validateAgentSkillsFrontmatter(skill) {
187
+ const errors = [];
188
+ const dirName = basename(skill.sourceDir);
189
+ const declaredName = skill.meta.name;
190
+ if (typeof declaredName !== "string" || declaredName.trim().length === 0) {
191
+ errors.push('Missing required frontmatter field "name".');
192
+ } else {
193
+ if (declaredName.length > SKILL_NAME_MAX_LENGTH) {
194
+ errors.push(`Invalid "name": must be at most ${SKILL_NAME_MAX_LENGTH} characters.`);
195
+ }
196
+ if (!SKILL_NAME_PATTERN.test(declaredName)) {
197
+ errors.push('Invalid "name": use lowercase letters, numbers, and single hyphens only.');
198
+ }
199
+ if (declaredName !== dirName) {
200
+ errors.push(`Invalid "name": must match containing directory "${dirName}".`);
201
+ }
202
+ }
203
+ const description = skill.meta.description;
204
+ if (typeof description !== "string" || description.trim().length === 0) {
205
+ errors.push('Missing required frontmatter field "description".');
206
+ }
207
+ const allowedTools = skill.meta["allowed-tools"];
208
+ if (allowedTools !== undefined && (!Array.isArray(allowedTools) || allowedTools.some((tool) => typeof tool !== "string" || tool.length === 0))) {
209
+ errors.push('Invalid "allowed-tools": expected an array of non-empty strings.');
210
+ }
211
+ return errors;
212
+ }
158
213
  function skillMatchesTarget(skill, targetId) {
159
214
  const { targets } = skill.meta;
160
215
  if (!targets || targets === "*")
@@ -164,7 +219,11 @@ function skillMatchesTarget(skill, targetId) {
164
219
  return Array.isArray(targets) && targets.includes(targetId);
165
220
  }
166
221
  export {
222
+ validateAgentSkillsFrontmatter,
167
223
  skillMatchesTarget,
224
+ serializeSkill,
168
225
  parseSkills,
169
- parseSkillFile
226
+ parseSkillFile,
227
+ normalizeImportedSkillMarkdown,
228
+ buildSkillFrontmatter
170
229
  };