aicm 0.19.1 → 0.20.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.
@@ -1,4 +1,4 @@
1
- import { ResolvedConfig, CommandFile, AssetFile, MCPServers, SupportedTarget } from "../utils/config";
1
+ import { ResolvedConfig, CommandFile, AssetFile, SkillFile, AgentFile, MCPServers, SupportedTarget } from "../utils/config";
2
2
  export interface InstallOptions {
3
3
  /**
4
4
  * Base directory to use instead of process.cwd()
@@ -49,6 +49,14 @@ export interface InstallResult {
49
49
  * Number of hooks installed
50
50
  */
51
51
  installedHookCount: number;
52
+ /**
53
+ * Number of skills installed
54
+ */
55
+ installedSkillCount: number;
56
+ /**
57
+ * Number of agents installed
58
+ */
59
+ installedAgentCount: number;
52
60
  /**
53
61
  * Number of packages installed
54
62
  */
@@ -56,6 +64,32 @@ export interface InstallResult {
56
64
  }
57
65
  export declare function writeAssetsToTargets(assets: AssetFile[], targets: SupportedTarget[]): void;
58
66
  export declare function writeCommandsToTargets(commands: CommandFile[], targets: SupportedTarget[]): void;
67
+ /**
68
+ * Write skills to all supported target directories
69
+ */
70
+ export declare function writeSkillsToTargets(skills: SkillFile[], targets: SupportedTarget[]): void;
71
+ /**
72
+ * Warn about skill name collisions from different presets
73
+ */
74
+ export declare function warnPresetSkillCollisions(skills: SkillFile[]): void;
75
+ /**
76
+ * Dedupe skills by name (last one wins)
77
+ */
78
+ export declare function dedupeSkillsForInstall(skills: SkillFile[]): SkillFile[];
79
+ /**
80
+ * Write agents to all supported target directories
81
+ * Similar to skills, agents are written directly to the agents directory
82
+ * with a .aicm.json metadata file tracking which agents are managed
83
+ */
84
+ export declare function writeAgentsToTargets(agents: AgentFile[], targets: SupportedTarget[]): void;
85
+ /**
86
+ * Warn about agent name collisions from different presets
87
+ */
88
+ export declare function warnPresetAgentCollisions(agents: AgentFile[]): void;
89
+ /**
90
+ * Dedupe agents by name (last one wins)
91
+ */
92
+ export declare function dedupeAgentsForInstall(agents: AgentFile[]): AgentFile[];
59
93
  export declare function warnPresetCommandCollisions(commands: CommandFile[]): void;
60
94
  export declare function dedupeCommandsForInstall(commands: CommandFile[]): CommandFile[];
61
95
  /**
@@ -5,6 +5,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.writeAssetsToTargets = writeAssetsToTargets;
7
7
  exports.writeCommandsToTargets = writeCommandsToTargets;
8
+ exports.writeSkillsToTargets = writeSkillsToTargets;
9
+ exports.warnPresetSkillCollisions = warnPresetSkillCollisions;
10
+ exports.dedupeSkillsForInstall = dedupeSkillsForInstall;
11
+ exports.writeAgentsToTargets = writeAgentsToTargets;
12
+ exports.warnPresetAgentCollisions = warnPresetAgentCollisions;
13
+ exports.dedupeAgentsForInstall = dedupeAgentsForInstall;
8
14
  exports.warnPresetCommandCollisions = warnPresetCommandCollisions;
9
15
  exports.dedupeCommandsForInstall = dedupeCommandsForInstall;
10
16
  exports.writeMcpServersToFile = writeMcpServersToFile;
@@ -228,6 +234,214 @@ function writeCommandsToTargets(commands, targets) {
228
234
  // Other targets do not support commands yet
229
235
  }
230
236
  }
237
+ /**
238
+ * Get the skills installation path for a target
239
+ * Returns null for targets that don't support skills
240
+ */
241
+ function getSkillsTargetPath(target) {
242
+ const projectDir = process.cwd();
243
+ switch (target) {
244
+ case "cursor":
245
+ return node_path_1.default.join(projectDir, ".cursor", "skills");
246
+ case "claude":
247
+ return node_path_1.default.join(projectDir, ".claude", "skills");
248
+ case "codex":
249
+ return node_path_1.default.join(projectDir, ".codex", "skills");
250
+ case "windsurf":
251
+ // Windsurf does not support skills
252
+ return null;
253
+ default:
254
+ return null;
255
+ }
256
+ }
257
+ /**
258
+ * Write a single skill to the target directory
259
+ * Copies the entire skill directory and writes .aicm.json metadata
260
+ */
261
+ function writeSkillToTarget(skill, targetSkillsDir) {
262
+ const skillTargetPath = node_path_1.default.join(targetSkillsDir, skill.name);
263
+ // Remove existing skill directory if it exists (to ensure clean install)
264
+ if (fs_extra_1.default.existsSync(skillTargetPath)) {
265
+ fs_extra_1.default.removeSync(skillTargetPath);
266
+ }
267
+ // Copy the entire skill directory
268
+ fs_extra_1.default.copySync(skill.sourcePath, skillTargetPath);
269
+ // Write .aicm.json metadata file
270
+ // The presence of this file indicates the skill is managed by aicm
271
+ const metadata = {
272
+ source: skill.source,
273
+ };
274
+ if (skill.presetName) {
275
+ metadata.presetName = skill.presetName;
276
+ }
277
+ const metadataPath = node_path_1.default.join(skillTargetPath, ".aicm.json");
278
+ fs_extra_1.default.writeJsonSync(metadataPath, metadata, { spaces: 2 });
279
+ }
280
+ /**
281
+ * Write skills to all supported target directories
282
+ */
283
+ function writeSkillsToTargets(skills, targets) {
284
+ if (skills.length === 0)
285
+ return;
286
+ for (const target of targets) {
287
+ const targetSkillsDir = getSkillsTargetPath(target);
288
+ if (!targetSkillsDir) {
289
+ // Target doesn't support skills
290
+ continue;
291
+ }
292
+ // Ensure the skills directory exists
293
+ fs_extra_1.default.ensureDirSync(targetSkillsDir);
294
+ for (const skill of skills) {
295
+ writeSkillToTarget(skill, targetSkillsDir);
296
+ }
297
+ }
298
+ }
299
+ /**
300
+ * Warn about skill name collisions from different presets
301
+ */
302
+ function warnPresetSkillCollisions(skills) {
303
+ const collisions = new Map();
304
+ for (const skill of skills) {
305
+ if (!skill.presetName)
306
+ continue;
307
+ const entry = collisions.get(skill.name);
308
+ if (entry) {
309
+ entry.presets.add(skill.presetName);
310
+ entry.lastPreset = skill.presetName;
311
+ }
312
+ else {
313
+ collisions.set(skill.name, {
314
+ presets: new Set([skill.presetName]),
315
+ lastPreset: skill.presetName,
316
+ });
317
+ }
318
+ }
319
+ for (const [skillName, { presets, lastPreset }] of collisions) {
320
+ if (presets.size > 1) {
321
+ const presetList = Array.from(presets).sort().join(", ");
322
+ console.warn(chalk_1.default.yellow(`Warning: multiple presets provide the "${skillName}" skill (${presetList}). Using definition from ${lastPreset}.`));
323
+ }
324
+ }
325
+ }
326
+ /**
327
+ * Dedupe skills by name (last one wins)
328
+ */
329
+ function dedupeSkillsForInstall(skills) {
330
+ const unique = new Map();
331
+ for (const skill of skills) {
332
+ unique.set(skill.name, skill);
333
+ }
334
+ return Array.from(unique.values());
335
+ }
336
+ /**
337
+ * Get the agents installation path for a target
338
+ * Returns null for targets that don't support agents
339
+ */
340
+ function getAgentsTargetPath(target) {
341
+ const projectDir = process.cwd();
342
+ switch (target) {
343
+ case "cursor":
344
+ return node_path_1.default.join(projectDir, ".cursor", "agents");
345
+ case "claude":
346
+ return node_path_1.default.join(projectDir, ".claude", "agents");
347
+ case "codex":
348
+ case "windsurf":
349
+ // Codex and Windsurf do not support agents
350
+ return null;
351
+ default:
352
+ return null;
353
+ }
354
+ }
355
+ /**
356
+ * Write agents to all supported target directories
357
+ * Similar to skills, agents are written directly to the agents directory
358
+ * with a .aicm.json metadata file tracking which agents are managed
359
+ */
360
+ function writeAgentsToTargets(agents, targets) {
361
+ if (agents.length === 0)
362
+ return;
363
+ for (const target of targets) {
364
+ const targetAgentsDir = getAgentsTargetPath(target);
365
+ if (!targetAgentsDir) {
366
+ // Target doesn't support agents
367
+ continue;
368
+ }
369
+ // Ensure the agents directory exists
370
+ fs_extra_1.default.ensureDirSync(targetAgentsDir);
371
+ // Read existing metadata to clean up old managed agents
372
+ const metadataPath = node_path_1.default.join(targetAgentsDir, ".aicm.json");
373
+ if (fs_extra_1.default.existsSync(metadataPath)) {
374
+ try {
375
+ const existingMetadata = fs_extra_1.default.readJsonSync(metadataPath);
376
+ // Remove previously managed agents
377
+ for (const agentName of existingMetadata.managedAgents || []) {
378
+ // Skip invalid names containing path separators
379
+ if (agentName.includes("/") || agentName.includes("\\")) {
380
+ console.warn(chalk_1.default.yellow(`Warning: Skipping invalid agent name "${agentName}" (contains path separator)`));
381
+ continue;
382
+ }
383
+ const fullPath = node_path_1.default.join(targetAgentsDir, agentName + ".md");
384
+ if (fs_extra_1.default.existsSync(fullPath)) {
385
+ fs_extra_1.default.removeSync(fullPath);
386
+ }
387
+ }
388
+ }
389
+ catch (_a) {
390
+ // Ignore errors reading metadata
391
+ }
392
+ }
393
+ const managedAgents = [];
394
+ for (const agent of agents) {
395
+ // Use base name only
396
+ const agentName = node_path_1.default.basename(agent.name, node_path_1.default.extname(agent.name));
397
+ const agentFile = node_path_1.default.join(targetAgentsDir, agentName + ".md");
398
+ fs_extra_1.default.writeFileSync(agentFile, agent.content);
399
+ managedAgents.push(agentName);
400
+ }
401
+ // Write metadata file to track managed agents
402
+ const metadata = {
403
+ managedAgents,
404
+ };
405
+ fs_extra_1.default.writeJsonSync(metadataPath, metadata, { spaces: 2 });
406
+ }
407
+ }
408
+ /**
409
+ * Warn about agent name collisions from different presets
410
+ */
411
+ function warnPresetAgentCollisions(agents) {
412
+ const collisions = new Map();
413
+ for (const agent of agents) {
414
+ if (!agent.presetName)
415
+ continue;
416
+ const entry = collisions.get(agent.name);
417
+ if (entry) {
418
+ entry.presets.add(agent.presetName);
419
+ entry.lastPreset = agent.presetName;
420
+ }
421
+ else {
422
+ collisions.set(agent.name, {
423
+ presets: new Set([agent.presetName]),
424
+ lastPreset: agent.presetName,
425
+ });
426
+ }
427
+ }
428
+ for (const [agentName, { presets, lastPreset }] of collisions) {
429
+ if (presets.size > 1) {
430
+ const presetList = Array.from(presets).sort().join(", ");
431
+ console.warn(chalk_1.default.yellow(`Warning: multiple presets provide the "${agentName}" agent (${presetList}). Using definition from ${lastPreset}.`));
432
+ }
433
+ }
434
+ }
435
+ /**
436
+ * Dedupe agents by name (last one wins)
437
+ */
438
+ function dedupeAgentsForInstall(agents) {
439
+ const unique = new Map();
440
+ for (const agent of agents) {
441
+ unique.set(agent.name, agent);
442
+ }
443
+ return Array.from(unique.values());
444
+ }
231
445
  function warnPresetCommandCollisions(commands) {
232
446
  const collisions = new Map();
233
447
  for (const command of commands) {
@@ -348,10 +562,12 @@ async function installPackage(options = {}) {
348
562
  installedCommandCount: 0,
349
563
  installedAssetCount: 0,
350
564
  installedHookCount: 0,
565
+ installedSkillCount: 0,
566
+ installedAgentCount: 0,
351
567
  packagesCount: 0,
352
568
  };
353
569
  }
354
- const { config, rules, commands, assets, mcpServers, hooks, hookFiles } = resolvedConfig;
570
+ const { config, rules, commands, assets, skills, agents, mcpServers, hooks, hookFiles, } = resolvedConfig;
355
571
  if (config.skipInstall === true) {
356
572
  return {
357
573
  success: true,
@@ -359,15 +575,23 @@ async function installPackage(options = {}) {
359
575
  installedCommandCount: 0,
360
576
  installedAssetCount: 0,
361
577
  installedHookCount: 0,
578
+ installedSkillCount: 0,
579
+ installedAgentCount: 0,
362
580
  packagesCount: 0,
363
581
  };
364
582
  }
365
583
  warnPresetCommandCollisions(commands);
366
584
  const commandsToInstall = dedupeCommandsForInstall(commands);
585
+ warnPresetSkillCollisions(skills);
586
+ const skillsToInstall = dedupeSkillsForInstall(skills);
587
+ warnPresetAgentCollisions(agents);
588
+ const agentsToInstall = dedupeAgentsForInstall(agents);
367
589
  try {
368
590
  if (!options.dryRun) {
369
591
  writeRulesToTargets(rules, assets, config.targets);
370
592
  writeCommandsToTargets(commandsToInstall, config.targets);
593
+ writeSkillsToTargets(skillsToInstall, config.targets);
594
+ writeAgentsToTargets(agentsToInstall, config.targets);
371
595
  if (mcpServers && Object.keys(mcpServers).length > 0) {
372
596
  writeMcpServersToTargets(mcpServers, config.targets, cwd);
373
597
  }
@@ -378,12 +602,16 @@ async function installPackage(options = {}) {
378
602
  const uniqueRuleCount = new Set(rules.map((rule) => rule.name)).size;
379
603
  const uniqueCommandCount = new Set(commandsToInstall.map((command) => command.name)).size;
380
604
  const uniqueHookCount = (0, hooks_1.countHooks)(hooks);
605
+ const uniqueSkillCount = skillsToInstall.length;
606
+ const uniqueAgentCount = agentsToInstall.length;
381
607
  return {
382
608
  success: true,
383
609
  installedRuleCount: uniqueRuleCount,
384
610
  installedCommandCount: uniqueCommandCount,
385
611
  installedAssetCount: assets.length,
386
612
  installedHookCount: uniqueHookCount,
613
+ installedSkillCount: uniqueSkillCount,
614
+ installedAgentCount: uniqueAgentCount,
387
615
  packagesCount: 1,
388
616
  };
389
617
  }
@@ -395,6 +623,8 @@ async function installPackage(options = {}) {
395
623
  installedCommandCount: 0,
396
624
  installedAssetCount: 0,
397
625
  installedHookCount: 0,
626
+ installedSkillCount: 0,
627
+ installedAgentCount: 0,
398
628
  packagesCount: 0,
399
629
  };
400
630
  }
@@ -415,6 +645,8 @@ async function install(options = {}) {
415
645
  installedCommandCount: 0,
416
646
  installedAssetCount: 0,
417
647
  installedHookCount: 0,
648
+ installedSkillCount: 0,
649
+ installedAgentCount: 0,
418
650
  packagesCount: 0,
419
651
  };
420
652
  }
@@ -447,11 +679,19 @@ async function installCommand(installOnCI, verbose, dryRun) {
447
679
  const ruleCount = result.installedRuleCount;
448
680
  const commandCount = result.installedCommandCount;
449
681
  const hookCount = result.installedHookCount;
682
+ const skillCount = result.installedSkillCount;
683
+ const agentCount = result.installedAgentCount;
450
684
  const ruleMessage = ruleCount > 0 ? `${ruleCount} rule${ruleCount === 1 ? "" : "s"}` : null;
451
685
  const commandMessage = commandCount > 0
452
686
  ? `${commandCount} command${commandCount === 1 ? "" : "s"}`
453
687
  : null;
454
688
  const hookMessage = hookCount > 0 ? `${hookCount} hook${hookCount === 1 ? "" : "s"}` : null;
689
+ const skillMessage = skillCount > 0
690
+ ? `${skillCount} skill${skillCount === 1 ? "" : "s"}`
691
+ : null;
692
+ const agentMessage = agentCount > 0
693
+ ? `${agentCount} agent${agentCount === 1 ? "" : "s"}`
694
+ : null;
455
695
  const countsParts = [];
456
696
  if (ruleMessage) {
457
697
  countsParts.push(ruleMessage);
@@ -462,6 +702,12 @@ async function installCommand(installOnCI, verbose, dryRun) {
462
702
  if (hookMessage) {
463
703
  countsParts.push(hookMessage);
464
704
  }
705
+ if (skillMessage) {
706
+ countsParts.push(skillMessage);
707
+ }
708
+ if (agentMessage) {
709
+ countsParts.push(agentMessage);
710
+ }
465
711
  const countsMessage = countsParts.length > 0
466
712
  ? countsParts.join(", ").replace(/, ([^,]*)$/, " and $1")
467
713
  : "0 rules";
@@ -473,8 +719,12 @@ async function installCommand(installOnCI, verbose, dryRun) {
473
719
  console.log(`Dry run: validated ${countsMessage}`);
474
720
  }
475
721
  }
476
- else if (ruleCount === 0 && commandCount === 0 && hookCount === 0) {
477
- console.log("No rules, commands, or hooks installed");
722
+ else if (ruleCount === 0 &&
723
+ commandCount === 0 &&
724
+ hookCount === 0 &&
725
+ skillCount === 0 &&
726
+ agentCount === 0) {
727
+ console.log("No rules, commands, hooks, skills, or agents installed");
478
728
  }
479
729
  else if (result.packagesCount > 1) {
480
730
  console.log(`Successfully installed ${countsMessage} across ${result.packagesCount} packages`);
@@ -46,6 +46,13 @@ export interface AssetFile {
46
46
  }
47
47
  export type RuleFile = ManagedFile;
48
48
  export type CommandFile = ManagedFile;
49
+ export interface SkillFile {
50
+ name: string;
51
+ sourcePath: string;
52
+ source: "local" | "preset";
53
+ presetName?: string;
54
+ }
55
+ export type AgentFile = ManagedFile;
49
56
  export interface RuleCollection {
50
57
  [target: string]: RuleFile[];
51
58
  }
@@ -54,6 +61,8 @@ export interface ResolvedConfig {
54
61
  rules: RuleFile[];
55
62
  commands: CommandFile[];
56
63
  assets: AssetFile[];
64
+ skills: SkillFile[];
65
+ agents: AgentFile[];
57
66
  mcpServers: MCPServers;
58
67
  hooks: HooksJson;
59
68
  hookFiles: HookFile[];
@@ -68,6 +77,16 @@ export declare function validateConfig(config: unknown, configFilePath: string,
68
77
  export declare function loadRulesFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<RuleFile[]>;
69
78
  export declare function loadCommandsFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<CommandFile[]>;
70
79
  export declare function loadAssetsFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<AssetFile[]>;
80
+ /**
81
+ * Load skills from a skills/ directory
82
+ * Each direct subdirectory containing a SKILL.md file is considered a skill
83
+ */
84
+ export declare function loadSkillsFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<SkillFile[]>;
85
+ /**
86
+ * Load agents from an agents/ directory
87
+ * Agents are markdown files (.md) with YAML frontmatter
88
+ */
89
+ export declare function loadAgentsFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<AgentFile[]>;
71
90
  /**
72
91
  * Extract namespace from preset path for directory structure
73
92
  * Handles both npm packages and local paths consistently
@@ -82,6 +101,8 @@ export declare function loadAllRules(config: Config, cwd: string): Promise<{
82
101
  rules: RuleFile[];
83
102
  commands: CommandFile[];
84
103
  assets: AssetFile[];
104
+ skills: SkillFile[];
105
+ agents: AgentFile[];
85
106
  mcpServers: MCPServers;
86
107
  hooks: HooksJson;
87
108
  hookFiles: HookFile[];
@@ -11,6 +11,8 @@ exports.validateConfig = validateConfig;
11
11
  exports.loadRulesFromDirectory = loadRulesFromDirectory;
12
12
  exports.loadCommandsFromDirectory = loadCommandsFromDirectory;
13
13
  exports.loadAssetsFromDirectory = loadAssetsFromDirectory;
14
+ exports.loadSkillsFromDirectory = loadSkillsFromDirectory;
15
+ exports.loadAgentsFromDirectory = loadAgentsFromDirectory;
14
16
  exports.extractNamespaceFromPresetPath = extractNamespaceFromPresetPath;
15
17
  exports.resolvePresetPath = resolvePresetPath;
16
18
  exports.loadPreset = loadPreset;
@@ -96,14 +98,18 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
96
98
  const hasRules = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "rules"));
97
99
  const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "commands"));
98
100
  const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "hooks.json"));
101
+ const hasSkills = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "skills"));
102
+ const hasAgents = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "agents"));
99
103
  // In workspace mode, root config doesn't need these directories
100
104
  // since packages will have their own configurations
101
105
  if (!isWorkspaceMode &&
102
106
  !hasRules &&
103
107
  !hasCommands &&
104
108
  !hasHooks &&
109
+ !hasSkills &&
110
+ !hasAgents &&
105
111
  !hasPresets) {
106
- throw new Error(`Root directory must contain at least one of: rules/, commands/, hooks.json, or have presets configured`);
112
+ throw new Error(`Root directory must contain at least one of: rules/, commands/, skills/, agents/, hooks.json, or have presets configured`);
107
113
  }
108
114
  }
109
115
  else if (!isWorkspaceMode && !hasPresets) {
@@ -202,6 +208,65 @@ async function loadAssetsFromDirectory(directoryPath, source, presetName) {
202
208
  }
203
209
  return assets;
204
210
  }
211
+ /**
212
+ * Load skills from a skills/ directory
213
+ * Each direct subdirectory containing a SKILL.md file is considered a skill
214
+ */
215
+ async function loadSkillsFromDirectory(directoryPath, source, presetName) {
216
+ const skills = [];
217
+ if (!fs_extra_1.default.existsSync(directoryPath)) {
218
+ return skills;
219
+ }
220
+ // Get all direct subdirectories
221
+ const entries = await fs_extra_1.default.readdir(directoryPath, { withFileTypes: true });
222
+ for (const entry of entries) {
223
+ if (!entry.isDirectory()) {
224
+ continue;
225
+ }
226
+ const skillPath = node_path_1.default.join(directoryPath, entry.name);
227
+ const skillMdPath = node_path_1.default.join(skillPath, "SKILL.md");
228
+ // Only include directories that contain a SKILL.md file
229
+ if (!fs_extra_1.default.existsSync(skillMdPath)) {
230
+ continue;
231
+ }
232
+ skills.push({
233
+ name: entry.name,
234
+ sourcePath: skillPath,
235
+ source,
236
+ presetName,
237
+ });
238
+ }
239
+ return skills;
240
+ }
241
+ /**
242
+ * Load agents from an agents/ directory
243
+ * Agents are markdown files (.md) with YAML frontmatter
244
+ */
245
+ async function loadAgentsFromDirectory(directoryPath, source, presetName) {
246
+ const agents = [];
247
+ if (!fs_extra_1.default.existsSync(directoryPath)) {
248
+ return agents;
249
+ }
250
+ const pattern = node_path_1.default.join(directoryPath, "**/*.md").replace(/\\/g, "/");
251
+ const filePaths = await (0, fast_glob_1.default)(pattern, {
252
+ onlyFiles: true,
253
+ absolute: true,
254
+ });
255
+ filePaths.sort();
256
+ for (const filePath of filePaths) {
257
+ const content = await fs_extra_1.default.readFile(filePath, "utf8");
258
+ const relativePath = node_path_1.default.relative(directoryPath, filePath);
259
+ const agentName = relativePath.replace(/\.md$/, "").replace(/\\/g, "/");
260
+ agents.push({
261
+ name: agentName,
262
+ content,
263
+ sourcePath: filePath,
264
+ source,
265
+ presetName,
266
+ });
267
+ }
268
+ return agents;
269
+ }
205
270
  /**
206
271
  * Extract namespace from preset path for directory structure
207
272
  * Handles both npm packages and local paths consistently
@@ -258,8 +323,15 @@ async function loadPreset(presetPath, cwd) {
258
323
  const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "commands"));
259
324
  const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "hooks.json"));
260
325
  const hasAssets = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "assets"));
261
- if (!hasRules && !hasCommands && !hasHooks && !hasAssets) {
262
- throw new Error(`Preset "${presetPath}" must have at least one of: rules/, commands/, hooks.json, or assets/`);
326
+ const hasSkills = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "skills"));
327
+ const hasAgents = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "agents"));
328
+ if (!hasRules &&
329
+ !hasCommands &&
330
+ !hasHooks &&
331
+ !hasAssets &&
332
+ !hasSkills &&
333
+ !hasAgents) {
334
+ throw new Error(`Preset "${presetPath}" must have at least one of: rules/, commands/, skills/, agents/, hooks.json, or assets/`);
263
335
  }
264
336
  return {
265
337
  config: presetConfig,
@@ -270,6 +342,8 @@ async function loadAllRules(config, cwd) {
270
342
  const allRules = [];
271
343
  const allCommands = [];
272
344
  const allAssets = [];
345
+ const allSkills = [];
346
+ const allAgents = [];
273
347
  const allHookFiles = [];
274
348
  const allHooksConfigs = [];
275
349
  let mergedMcpServers = { ...config.mcpServers };
@@ -301,6 +375,18 @@ async function loadAllRules(config, cwd) {
301
375
  const localAssets = await loadAssetsFromDirectory(assetsPath, "local");
302
376
  allAssets.push(...localAssets);
303
377
  }
378
+ // Load skills from skills/ subdirectory
379
+ const skillsPath = node_path_1.default.join(rootPath, "skills");
380
+ if (fs_extra_1.default.existsSync(skillsPath)) {
381
+ const localSkills = await loadSkillsFromDirectory(skillsPath, "local");
382
+ allSkills.push(...localSkills);
383
+ }
384
+ // Load agents from agents/ subdirectory
385
+ const agentsPath = node_path_1.default.join(rootPath, "agents");
386
+ if (fs_extra_1.default.existsSync(agentsPath)) {
387
+ const localAgents = await loadAgentsFromDirectory(agentsPath, "local");
388
+ allAgents.push(...localAgents);
389
+ }
304
390
  }
305
391
  // Load presets
306
392
  if (config.presets) {
@@ -332,6 +418,18 @@ async function loadAllRules(config, cwd) {
332
418
  const presetAssets = await loadAssetsFromDirectory(presetAssetsPath, "preset", presetPath);
333
419
  allAssets.push(...presetAssets);
334
420
  }
421
+ // Load preset skills from skills/ subdirectory
422
+ const presetSkillsPath = node_path_1.default.join(presetRootDir, "skills");
423
+ if (fs_extra_1.default.existsSync(presetSkillsPath)) {
424
+ const presetSkills = await loadSkillsFromDirectory(presetSkillsPath, "preset", presetPath);
425
+ allSkills.push(...presetSkills);
426
+ }
427
+ // Load preset agents from agents/ subdirectory
428
+ const presetAgentsPath = node_path_1.default.join(presetRootDir, "agents");
429
+ if (fs_extra_1.default.existsSync(presetAgentsPath)) {
430
+ const presetAgents = await loadAgentsFromDirectory(presetAgentsPath, "preset", presetPath);
431
+ allAgents.push(...presetAgents);
432
+ }
335
433
  // Merge MCP servers from preset
336
434
  if (preset.config.mcpServers) {
337
435
  mergedMcpServers = mergePresetMcpServers(mergedMcpServers, preset.config.mcpServers);
@@ -344,6 +442,8 @@ async function loadAllRules(config, cwd) {
344
442
  rules: allRules,
345
443
  commands: allCommands,
346
444
  assets: allAssets,
445
+ skills: allSkills,
446
+ agents: allAgents,
347
447
  mcpServers: mergedMcpServers,
348
448
  hooks: mergedHooks,
349
449
  hookFiles: allHookFiles,
@@ -403,12 +503,14 @@ async function loadConfig(cwd) {
403
503
  const isWorkspaces = resolveWorkspaces(config, configResult.filepath, workingDir);
404
504
  validateConfig(config, configResult.filepath, workingDir, isWorkspaces);
405
505
  const configWithDefaults = applyDefaults(config, isWorkspaces);
406
- const { rules, commands, assets, mcpServers, hooks, hookFiles } = await loadAllRules(configWithDefaults, workingDir);
506
+ const { rules, commands, assets, skills, agents, mcpServers, hooks, hookFiles, } = await loadAllRules(configWithDefaults, workingDir);
407
507
  return {
408
508
  config: configWithDefaults,
409
509
  rules,
410
510
  commands,
411
511
  assets,
512
+ skills,
513
+ agents,
412
514
  mcpServers,
413
515
  hooks,
414
516
  hookFiles,
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Check if a resolved path is safely within the specified base directory.
3
+ * This prevents path traversal attacks where malicious paths like "../../../etc"
4
+ * or absolute paths could escape the intended directory.
5
+ *
6
+ * @param baseDir - The directory that should contain the path
7
+ * @param relativePath - The potentially untrusted relative path
8
+ * @returns The safely resolved full path, or null if the path would escape baseDir
9
+ */
10
+ export declare function resolveSafePath(baseDir: string, relativePath: string): string | null;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveSafePath = resolveSafePath;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ /**
9
+ * Check if a resolved path is safely within the specified base directory.
10
+ * This prevents path traversal attacks where malicious paths like "../../../etc"
11
+ * or absolute paths could escape the intended directory.
12
+ *
13
+ * @param baseDir - The directory that should contain the path
14
+ * @param relativePath - The potentially untrusted relative path
15
+ * @returns The safely resolved full path, or null if the path would escape baseDir
16
+ */
17
+ function resolveSafePath(baseDir, relativePath) {
18
+ // Resolve both to absolute paths
19
+ const resolvedBase = node_path_1.default.resolve(baseDir);
20
+ const resolvedTarget = node_path_1.default.resolve(baseDir, relativePath);
21
+ // The resolved path must start with the base directory + separator
22
+ // This ensures it's truly inside the directory, not a sibling with similar prefix
23
+ // e.g., /foo/bar should not match /foo/bar-other
24
+ if (!resolvedTarget.startsWith(resolvedBase + node_path_1.default.sep)) {
25
+ return null;
26
+ }
27
+ return resolvedTarget;
28
+ }