aicm 0.19.1 → 0.20.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 CHANGED
@@ -16,6 +16,7 @@ A CLI tool for managing Agentic configurations across projects.
16
16
  - [Features](#features)
17
17
  - [Rules](#rules)
18
18
  - [Commands](#commands)
19
+ - [Skills](#skills)
19
20
  - [Hooks](#hooks)
20
21
  - [MCP Servers](#mcp-servers)
21
22
  - [Assets](#assets)
@@ -23,6 +24,7 @@ A CLI tool for managing Agentic configurations across projects.
23
24
  - [Configuration](#configuration)
24
25
  - [CLI Commands](#cli-commands)
25
26
  - [Node.js API](#nodejs-api)
27
+ - [FAQ](#faq)
26
28
 
27
29
  ## Why
28
30
 
@@ -85,6 +87,7 @@ After installation, open Cursor and ask it to do something. Your AI assistant wi
85
87
  │ ├── typescript.mdc
86
88
  │ └── react.mdc
87
89
  ├── commands/ # Command files (.md) [optional]
90
+ ├── skills/ # Agent Skills [optional]
88
91
  ├── assets/ # Auxiliary files [optional]
89
92
  └── hooks.json # Hook configuration [optional]
90
93
  ```
@@ -139,7 +142,7 @@ The rules are now installed in `.cursor/rules/aicm/` and any MCP servers are con
139
142
  ### Notes
140
143
 
141
144
  - Generated files are always placed in subdirectories for deterministic cleanup and easy gitignore.
142
- - Users should add `.cursor/*/aicm/` to `.gitignore` to avoid tracking generated files. This single pattern covers all aicm-managed directories (rules, commands, assets, hooks).
145
+ - Users should add `.cursor/*/aicm/`, `.cursor/skills/`, `.claude/`, and `.codex/` to `.gitignore` to avoid tracking generated files.
143
146
 
144
147
  ## Features
145
148
 
@@ -193,6 +196,65 @@ Configure your `aicm.json`:
193
196
 
194
197
  Command files ending in `.md` are installed to `.cursor/commands/aicm/` and appear in Cursor under the `/` command menu.
195
198
 
199
+ ### Skills
200
+
201
+ aicm supports [Agent Skills](https://agentskills.io) - a standard format for giving AI agents new capabilities and expertise. Skills are folders containing instructions, scripts, and resources that agents can discover and use.
202
+
203
+ Create a `skills/` directory where each subdirectory is a skill (containing a `SKILL.md` file):
204
+
205
+ ```
206
+ my-project/
207
+ ├── aicm.json
208
+ └── skills/
209
+ ├── pdf-processing/
210
+ │ ├── SKILL.md
211
+ │ ├── scripts/
212
+ │ │ └── extract.py
213
+ │ └── references/
214
+ │ └── REFERENCE.md
215
+ └── code-review/
216
+ └── SKILL.md
217
+ ```
218
+
219
+ Each skill must have a `SKILL.md` file with YAML frontmatter:
220
+
221
+ ```markdown
222
+ ---
223
+ name: pdf-processing
224
+ description: Extract text and tables from PDF files, fill forms, merge documents.
225
+ ---
226
+
227
+ # PDF Processing Skill
228
+
229
+ This skill enables working with PDF documents.
230
+
231
+ ## Usage
232
+
233
+ Run the extraction script:
234
+ scripts/extract.py
235
+ ```
236
+
237
+ Configure your `aicm.json`:
238
+
239
+ ```json
240
+ {
241
+ "rootDir": "./",
242
+ "targets": ["cursor"]
243
+ }
244
+ ```
245
+
246
+ Skills are installed to different locations based on the target:
247
+
248
+ | Target | Skills Location |
249
+ | ---------- | ----------------- |
250
+ | **Cursor** | `.cursor/skills/` |
251
+ | **Claude** | `.claude/skills/` |
252
+ | **Codex** | `.codex/skills/` |
253
+
254
+ When installed, each skill directory is copied in its entirety (including `scripts/`, `references/`, `assets/` subdirectories). A `.aicm.json` file is added inside each installed skill to track that it's managed by aicm.
255
+
256
+ In workspace mode, skills are installed both to each package and merged at the root level, similar to commands.
257
+
196
258
  ### Hooks
197
259
 
198
260
  aicm provides first-class support for [Cursor Agent Hooks](https://docs.cursor.com/advanced/hooks), allowing you to intercept and extend the agent's behavior. Hooks enable you to run custom scripts before/after shell execution, file edits, MCP calls, and more.
@@ -369,9 +431,10 @@ aicm automatically detects workspaces if your `package.json` contains a `workspa
369
431
  ### How It Works
370
432
 
371
433
  1. **Discover packages**: Automatically find all directories containing `aicm.json` files in your repository.
372
- 2. **Install per package**: Install rules and MCPs for each package individually in their respective directories.
434
+ 2. **Install per package**: Install rules, commands, and skills for each package individually in their respective directories.
373
435
  3. **Merge MCP servers**: Write a merged `.cursor/mcp.json` at the repository root containing all MCP servers from every package.
374
436
  4. **Merge commands**: Write a merged `.cursor/commands/aicm/` at the repository root containing all commands from every package.
437
+ 5. **Merge skills**: Write merged skills to the repository root (e.g., `.cursor/skills/`) containing all skills from every package.
375
438
 
376
439
  For example, in a workspace structure like:
377
440
 
@@ -429,7 +492,7 @@ Create an `aicm.json` file in your project root, or an `aicm` key in your projec
429
492
 
430
493
  ### Configuration Options
431
494
 
432
- - **rootDir**: Directory containing your aicm structure. Must contain one or more of: `rules/`, `commands/`, `assets/`, `hooks/`, or `hooks.json`. If not specified, aicm will only install rules from presets and will not pick up any local directories.
495
+ - **rootDir**: Directory containing your aicm structure. Must contain one or more of: `rules/`, `commands/`, `skills/`, `assets/`, `hooks/`, or `hooks.json`. If not specified, aicm will only install rules from presets and will not pick up any local directories.
433
496
  - **targets**: IDEs/Agent targets where rules should be installed. Defaults to `["cursor"]`. Supported targets: `cursor`, `windsurf`, `codex`, `claude`.
434
497
  - **presets**: List of preset packages or paths to include.
435
498
  - **mcpServers**: MCP server configurations.
@@ -471,11 +534,14 @@ aicm uses a convention-based directory structure:
471
534
  ```
472
535
  my-project/
473
536
  ├── aicm.json
474
- ├── rules/ # Rule files (.mdc) [required for rules]
537
+ ├── rules/ # Rule files (.mdc) [optional]
475
538
  │ ├── api.mdc
476
539
  │ └── testing.mdc
477
540
  ├── commands/ # Command files (.md) [optional]
478
541
  │ └── generate.md
542
+ ├── skills/ # Agent Skills [optional]
543
+ │ └── my-skill/
544
+ │ └── SKILL.md
479
545
  ├── assets/ # Auxiliary files [optional]
480
546
  │ ├── schema.json
481
547
  │ └── examples/
@@ -559,6 +625,50 @@ install({
559
625
 
560
626
  To prevent [prompt-injection](https://en.wikipedia.org/wiki/Prompt_injection), use only packages from trusted sources.
561
627
 
628
+ ## FAQ
629
+
630
+ ### Can I reference rules from commands or vice versa?
631
+
632
+ **No, direct references between rules and commands are not supported.** This is because:
633
+
634
+ - **Commands are hoisted** to the root level in workspace mode (`.cursor/commands/aicm/`)
635
+ - **Rules remain nested** at the package level (`package-a/.cursor/rules/aicm/`)
636
+ - This creates broken relative paths when commands try to reference rules
637
+
638
+ **❌ Don't do this:**
639
+
640
+ ```markdown
641
+ <!-- commands/validate.md -->
642
+
643
+ Follow the rules in [api-rule.mdc](../rules/api-rule.mdc) <!-- BROKEN! -->
644
+ ```
645
+
646
+ **✅ Do this instead:**
647
+
648
+ ```markdown
649
+ <!-- Put shared content in assets/coding-standards.md -->
650
+
651
+ # Coding Standards
652
+
653
+ - Use TypeScript for all new code
654
+ - Follow ESLint rules
655
+ - Write unit tests for all functions
656
+ ```
657
+
658
+ ```markdown
659
+ <!-- rules/api-rule.mdc -->
660
+
661
+ Follow the coding standards in [coding-standards.md](../assets/coding-standards.md).
662
+ ```
663
+
664
+ ```markdown
665
+ <!-- commands/validate.md -->
666
+
667
+ Validate against our [coding standards](../assets/coding-standards.md).
668
+ ```
669
+
670
+ Use shared assets for content that needs to be referenced by both rules and commands. Assets are properly rewritten and work in all modes.
671
+
562
672
  ## Contributing
563
673
 
564
674
  Contributions are welcome! Please feel free to open an issue or submit a Pull Request.
package/dist/bin/aicm.js CHANGED
File without changes
@@ -152,6 +152,54 @@ function cleanHooks(cwd, verbose) {
152
152
  }
153
153
  return hasChanges;
154
154
  }
155
+ /**
156
+ * Clean aicm-managed skills from a skills directory
157
+ * Only removes skills that have .aicm.json (presence indicates aicm management)
158
+ */
159
+ function cleanSkills(cwd, verbose) {
160
+ let cleanedCount = 0;
161
+ // Skills directories for each target
162
+ const skillsDirs = [
163
+ node_path_1.default.join(cwd, ".cursor", "skills"),
164
+ node_path_1.default.join(cwd, ".claude", "skills"),
165
+ node_path_1.default.join(cwd, ".codex", "skills"),
166
+ ];
167
+ for (const skillsDir of skillsDirs) {
168
+ if (!fs_extra_1.default.existsSync(skillsDir)) {
169
+ continue;
170
+ }
171
+ try {
172
+ const entries = fs_extra_1.default.readdirSync(skillsDir, { withFileTypes: true });
173
+ for (const entry of entries) {
174
+ if (!entry.isDirectory()) {
175
+ continue;
176
+ }
177
+ const skillPath = node_path_1.default.join(skillsDir, entry.name);
178
+ const metadataPath = node_path_1.default.join(skillPath, ".aicm.json");
179
+ // Only clean skills that have .aicm.json (presence indicates aicm management)
180
+ if (fs_extra_1.default.existsSync(metadataPath)) {
181
+ fs_extra_1.default.removeSync(skillPath);
182
+ if (verbose) {
183
+ console.log(chalk_1.default.gray(` Removed skill ${skillPath}`));
184
+ }
185
+ cleanedCount++;
186
+ }
187
+ }
188
+ // Remove the skills directory if it's now empty
189
+ const remainingEntries = fs_extra_1.default.readdirSync(skillsDir);
190
+ if (remainingEntries.length === 0) {
191
+ fs_extra_1.default.removeSync(skillsDir);
192
+ if (verbose) {
193
+ console.log(chalk_1.default.gray(` Removed empty directory ${skillsDir}`));
194
+ }
195
+ }
196
+ }
197
+ catch (_a) {
198
+ console.warn(chalk_1.default.yellow(`Warning: Failed to clean skills in ${skillsDir}`));
199
+ }
200
+ }
201
+ return cleanedCount;
202
+ }
155
203
  function cleanEmptyDirectories(cwd, verbose) {
156
204
  let cleanedCount = 0;
157
205
  const dirsToCheck = [
@@ -159,7 +207,12 @@ function cleanEmptyDirectories(cwd, verbose) {
159
207
  node_path_1.default.join(cwd, ".cursor", "commands"),
160
208
  node_path_1.default.join(cwd, ".cursor", "assets"),
161
209
  node_path_1.default.join(cwd, ".cursor", "hooks"),
210
+ node_path_1.default.join(cwd, ".cursor", "skills"),
162
211
  node_path_1.default.join(cwd, ".cursor"),
212
+ node_path_1.default.join(cwd, ".claude", "skills"),
213
+ node_path_1.default.join(cwd, ".claude"),
214
+ node_path_1.default.join(cwd, ".codex", "skills"),
215
+ node_path_1.default.join(cwd, ".codex"),
163
216
  ];
164
217
  for (const dir of dirsToCheck) {
165
218
  if (fs_extra_1.default.existsSync(dir)) {
@@ -211,6 +264,8 @@ async function cleanPackage(options = {}) {
211
264
  // Clean hooks
212
265
  if (cleanHooks(cwd, verbose))
213
266
  cleanedCount++;
267
+ // Clean skills
268
+ cleanedCount += cleanSkills(cwd, verbose);
214
269
  // Clean empty directories
215
270
  cleanedCount += cleanEmptyDirectories(cwd, verbose);
216
271
  return {
@@ -41,6 +41,52 @@ function collectWorkspaceCommandTargets(packages) {
41
41
  }
42
42
  return Array.from(targets);
43
43
  }
44
+ /**
45
+ * Merge skills from multiple workspace packages
46
+ * Skills are merged flat (not namespaced by preset)
47
+ * Dedupes preset skills that appear in multiple packages
48
+ */
49
+ function mergeWorkspaceSkills(packages) {
50
+ var _a;
51
+ const skills = [];
52
+ const seenPresetSkills = new Set();
53
+ for (const pkg of packages) {
54
+ // Skills are supported by cursor, claude, and codex targets
55
+ const hasSkillsTarget = pkg.config.config.targets.includes("cursor") ||
56
+ pkg.config.config.targets.includes("claude") ||
57
+ pkg.config.config.targets.includes("codex");
58
+ if (!hasSkillsTarget) {
59
+ continue;
60
+ }
61
+ for (const skill of (_a = pkg.config.skills) !== null && _a !== void 0 ? _a : []) {
62
+ if (skill.presetName) {
63
+ // Dedupe preset skills by preset+name combination
64
+ const presetKey = `${skill.presetName}::${skill.name}`;
65
+ if (seenPresetSkills.has(presetKey)) {
66
+ continue;
67
+ }
68
+ seenPresetSkills.add(presetKey);
69
+ }
70
+ skills.push(skill);
71
+ }
72
+ }
73
+ return skills;
74
+ }
75
+ /**
76
+ * Collect all targets that support skills from workspace packages
77
+ */
78
+ function collectWorkspaceSkillTargets(packages) {
79
+ const targets = new Set();
80
+ for (const pkg of packages) {
81
+ for (const target of pkg.config.config.targets) {
82
+ // Skills are supported by cursor, claude, and codex
83
+ if (target === "cursor" || target === "claude" || target === "codex") {
84
+ targets.add(target);
85
+ }
86
+ }
87
+ }
88
+ return Array.from(targets);
89
+ }
44
90
  function mergeWorkspaceMcpServers(packages) {
45
91
  const merged = {};
46
92
  const info = {};
@@ -101,6 +147,7 @@ async function installWorkspacesPackages(packages, options = {}) {
101
147
  let totalCommandCount = 0;
102
148
  let totalAssetCount = 0;
103
149
  let totalHookCount = 0;
150
+ let totalSkillCount = 0;
104
151
  // Install packages sequentially for now (can be parallelized later)
105
152
  for (const pkg of packages) {
106
153
  const packagePath = pkg.absolutePath;
@@ -114,6 +161,7 @@ async function installWorkspacesPackages(packages, options = {}) {
114
161
  totalCommandCount += result.installedCommandCount;
115
162
  totalAssetCount += result.installedAssetCount;
116
163
  totalHookCount += result.installedHookCount;
164
+ totalSkillCount += result.installedSkillCount;
117
165
  results.push({
118
166
  path: pkg.relativePath,
119
167
  success: result.success,
@@ -122,6 +170,7 @@ async function installWorkspacesPackages(packages, options = {}) {
122
170
  installedCommandCount: result.installedCommandCount,
123
171
  installedAssetCount: result.installedAssetCount,
124
172
  installedHookCount: result.installedHookCount,
173
+ installedSkillCount: result.installedSkillCount,
125
174
  });
126
175
  }
127
176
  catch (error) {
@@ -133,6 +182,7 @@ async function installWorkspacesPackages(packages, options = {}) {
133
182
  installedCommandCount: 0,
134
183
  installedAssetCount: 0,
135
184
  installedHookCount: 0,
185
+ installedSkillCount: 0,
136
186
  });
137
187
  }
138
188
  }
@@ -144,6 +194,7 @@ async function installWorkspacesPackages(packages, options = {}) {
144
194
  totalCommandCount,
145
195
  totalAssetCount,
146
196
  totalHookCount,
197
+ totalSkillCount,
147
198
  };
148
199
  }
149
200
  /**
@@ -162,11 +213,12 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
162
213
  const isRoot = pkg.relativePath === ".";
163
214
  if (!isRoot)
164
215
  return true;
165
- // For root directories, only keep if it has rules, commands, or presets
216
+ // For root directories, only keep if it has rules, commands, skills, or presets
166
217
  const hasRules = pkg.config.rules && pkg.config.rules.length > 0;
167
218
  const hasCommands = pkg.config.commands && pkg.config.commands.length > 0;
219
+ const hasSkills = pkg.config.skills && pkg.config.skills.length > 0;
168
220
  const hasPresets = pkg.config.config.presets && pkg.config.config.presets.length > 0;
169
- return hasRules || hasCommands || hasPresets;
221
+ return hasRules || hasCommands || hasSkills || hasPresets;
170
222
  });
171
223
  if (packages.length === 0) {
172
224
  return {
@@ -176,6 +228,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
176
228
  installedCommandCount: 0,
177
229
  installedAssetCount: 0,
178
230
  installedHookCount: 0,
231
+ installedSkillCount: 0,
179
232
  packagesCount: 0,
180
233
  };
181
234
  }
@@ -206,6 +259,18 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
206
259
  (0, install_1.writeAssetsToTargets)(allAssets, workspaceCommandTargets);
207
260
  (0, install_1.writeCommandsToTargets)(dedupedWorkspaceCommands, workspaceCommandTargets);
208
261
  }
262
+ // Merge and write skills for workspace
263
+ const workspaceSkills = mergeWorkspaceSkills(packages);
264
+ const workspaceSkillTargets = collectWorkspaceSkillTargets(packages);
265
+ if (workspaceSkills.length > 0) {
266
+ (0, install_1.warnPresetSkillCollisions)(workspaceSkills);
267
+ }
268
+ if (!dryRun &&
269
+ workspaceSkills.length > 0 &&
270
+ workspaceSkillTargets.length > 0) {
271
+ const dedupedWorkspaceSkills = (0, install_1.dedupeSkillsForInstall)(workspaceSkills);
272
+ (0, install_1.writeSkillsToTargets)(dedupedWorkspaceSkills, workspaceSkillTargets);
273
+ }
209
274
  const { merged: rootMcp, conflicts } = mergeWorkspaceMcpServers(packages);
210
275
  const hasCursorTarget = packages.some((p) => p.config.config.targets.includes("cursor"));
211
276
  if (!dryRun && hasCursorTarget && Object.keys(rootMcp).length > 0) {
@@ -231,6 +296,9 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
231
296
  if (pkg.installedHookCount > 0) {
232
297
  summaryParts.push(`${pkg.installedHookCount} hook${pkg.installedHookCount === 1 ? "" : "s"}`);
233
298
  }
299
+ if (pkg.installedSkillCount > 0) {
300
+ summaryParts.push(`${pkg.installedSkillCount} skill${pkg.installedSkillCount === 1 ? "" : "s"}`);
301
+ }
234
302
  console.log(chalk_1.default.green(`✅ ${pkg.path} (${summaryParts.join(", ")})`));
235
303
  }
236
304
  else {
@@ -248,7 +316,10 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
248
316
  const hookSummary = result.totalHookCount > 0
249
317
  ? `, ${result.totalHookCount} hook${result.totalHookCount === 1 ? "" : "s"} total`
250
318
  : "";
251
- console.log(chalk_1.default.green(`Successfully installed: ${result.packages.length - failedPackages.length}/${result.packages.length} packages (${result.totalRuleCount} rule${result.totalRuleCount === 1 ? "" : "s"} total${commandSummary}${hookSummary})`));
319
+ const skillSummary = result.totalSkillCount > 0
320
+ ? `, ${result.totalSkillCount} skill${result.totalSkillCount === 1 ? "" : "s"} total`
321
+ : "";
322
+ console.log(chalk_1.default.green(`Successfully installed: ${result.packages.length - failedPackages.length}/${result.packages.length} packages (${result.totalRuleCount} rule${result.totalRuleCount === 1 ? "" : "s"} total${commandSummary}${hookSummary}${skillSummary})`));
252
323
  console.log(chalk_1.default.red(`Failed packages: ${failedPackages.map((p) => p.path).join(", ")}`));
253
324
  }
254
325
  const errorDetails = failedPackages
@@ -261,6 +332,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
261
332
  installedCommandCount: result.totalCommandCount,
262
333
  installedAssetCount: result.totalAssetCount,
263
334
  installedHookCount: result.totalHookCount,
335
+ installedSkillCount: result.totalSkillCount,
264
336
  packagesCount: result.packages.length,
265
337
  };
266
338
  }
@@ -270,6 +342,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
270
342
  installedCommandCount: result.totalCommandCount,
271
343
  installedAssetCount: result.totalAssetCount,
272
344
  installedHookCount: result.totalHookCount,
345
+ installedSkillCount: result.totalSkillCount,
273
346
  packagesCount: result.packages.length,
274
347
  };
275
348
  });
@@ -1,4 +1,4 @@
1
- import { ResolvedConfig, CommandFile, AssetFile, MCPServers, SupportedTarget } from "../utils/config";
1
+ import { ResolvedConfig, CommandFile, AssetFile, SkillFile, 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,10 @@ 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;
52
56
  /**
53
57
  * Number of packages installed
54
58
  */
@@ -56,6 +60,18 @@ export interface InstallResult {
56
60
  }
57
61
  export declare function writeAssetsToTargets(assets: AssetFile[], targets: SupportedTarget[]): void;
58
62
  export declare function writeCommandsToTargets(commands: CommandFile[], targets: SupportedTarget[]): void;
63
+ /**
64
+ * Write skills to all supported target directories
65
+ */
66
+ export declare function writeSkillsToTargets(skills: SkillFile[], targets: SupportedTarget[]): void;
67
+ /**
68
+ * Warn about skill name collisions from different presets
69
+ */
70
+ export declare function warnPresetSkillCollisions(skills: SkillFile[]): void;
71
+ /**
72
+ * Dedupe skills by name (last one wins)
73
+ */
74
+ export declare function dedupeSkillsForInstall(skills: SkillFile[]): SkillFile[];
59
75
  export declare function warnPresetCommandCollisions(commands: CommandFile[]): void;
60
76
  export declare function dedupeCommandsForInstall(commands: CommandFile[]): CommandFile[];
61
77
  /**
@@ -5,6 +5,9 @@ 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;
8
11
  exports.warnPresetCommandCollisions = warnPresetCommandCollisions;
9
12
  exports.dedupeCommandsForInstall = dedupeCommandsForInstall;
10
13
  exports.writeMcpServersToFile = writeMcpServersToFile;
@@ -228,6 +231,105 @@ function writeCommandsToTargets(commands, targets) {
228
231
  // Other targets do not support commands yet
229
232
  }
230
233
  }
234
+ /**
235
+ * Get the skills installation path for a target
236
+ * Returns null for targets that don't support skills
237
+ */
238
+ function getSkillsTargetPath(target) {
239
+ const projectDir = process.cwd();
240
+ switch (target) {
241
+ case "cursor":
242
+ return node_path_1.default.join(projectDir, ".cursor", "skills");
243
+ case "claude":
244
+ return node_path_1.default.join(projectDir, ".claude", "skills");
245
+ case "codex":
246
+ return node_path_1.default.join(projectDir, ".codex", "skills");
247
+ case "windsurf":
248
+ // Windsurf does not support skills
249
+ return null;
250
+ default:
251
+ return null;
252
+ }
253
+ }
254
+ /**
255
+ * Write a single skill to the target directory
256
+ * Copies the entire skill directory and writes .aicm.json metadata
257
+ */
258
+ function writeSkillToTarget(skill, targetSkillsDir) {
259
+ const skillTargetPath = node_path_1.default.join(targetSkillsDir, skill.name);
260
+ // Remove existing skill directory if it exists (to ensure clean install)
261
+ if (fs_extra_1.default.existsSync(skillTargetPath)) {
262
+ fs_extra_1.default.removeSync(skillTargetPath);
263
+ }
264
+ // Copy the entire skill directory
265
+ fs_extra_1.default.copySync(skill.sourcePath, skillTargetPath);
266
+ // Write .aicm.json metadata file
267
+ // The presence of this file indicates the skill is managed by aicm
268
+ const metadata = {
269
+ source: skill.source,
270
+ };
271
+ if (skill.presetName) {
272
+ metadata.presetName = skill.presetName;
273
+ }
274
+ const metadataPath = node_path_1.default.join(skillTargetPath, ".aicm.json");
275
+ fs_extra_1.default.writeJsonSync(metadataPath, metadata, { spaces: 2 });
276
+ }
277
+ /**
278
+ * Write skills to all supported target directories
279
+ */
280
+ function writeSkillsToTargets(skills, targets) {
281
+ if (skills.length === 0)
282
+ return;
283
+ for (const target of targets) {
284
+ const targetSkillsDir = getSkillsTargetPath(target);
285
+ if (!targetSkillsDir) {
286
+ // Target doesn't support skills
287
+ continue;
288
+ }
289
+ // Ensure the skills directory exists
290
+ fs_extra_1.default.ensureDirSync(targetSkillsDir);
291
+ for (const skill of skills) {
292
+ writeSkillToTarget(skill, targetSkillsDir);
293
+ }
294
+ }
295
+ }
296
+ /**
297
+ * Warn about skill name collisions from different presets
298
+ */
299
+ function warnPresetSkillCollisions(skills) {
300
+ const collisions = new Map();
301
+ for (const skill of skills) {
302
+ if (!skill.presetName)
303
+ continue;
304
+ const entry = collisions.get(skill.name);
305
+ if (entry) {
306
+ entry.presets.add(skill.presetName);
307
+ entry.lastPreset = skill.presetName;
308
+ }
309
+ else {
310
+ collisions.set(skill.name, {
311
+ presets: new Set([skill.presetName]),
312
+ lastPreset: skill.presetName,
313
+ });
314
+ }
315
+ }
316
+ for (const [skillName, { presets, lastPreset }] of collisions) {
317
+ if (presets.size > 1) {
318
+ const presetList = Array.from(presets).sort().join(", ");
319
+ console.warn(chalk_1.default.yellow(`Warning: multiple presets provide the "${skillName}" skill (${presetList}). Using definition from ${lastPreset}.`));
320
+ }
321
+ }
322
+ }
323
+ /**
324
+ * Dedupe skills by name (last one wins)
325
+ */
326
+ function dedupeSkillsForInstall(skills) {
327
+ const unique = new Map();
328
+ for (const skill of skills) {
329
+ unique.set(skill.name, skill);
330
+ }
331
+ return Array.from(unique.values());
332
+ }
231
333
  function warnPresetCommandCollisions(commands) {
232
334
  const collisions = new Map();
233
335
  for (const command of commands) {
@@ -348,10 +450,11 @@ async function installPackage(options = {}) {
348
450
  installedCommandCount: 0,
349
451
  installedAssetCount: 0,
350
452
  installedHookCount: 0,
453
+ installedSkillCount: 0,
351
454
  packagesCount: 0,
352
455
  };
353
456
  }
354
- const { config, rules, commands, assets, mcpServers, hooks, hookFiles } = resolvedConfig;
457
+ const { config, rules, commands, assets, skills, mcpServers, hooks, hookFiles, } = resolvedConfig;
355
458
  if (config.skipInstall === true) {
356
459
  return {
357
460
  success: true,
@@ -359,15 +462,19 @@ async function installPackage(options = {}) {
359
462
  installedCommandCount: 0,
360
463
  installedAssetCount: 0,
361
464
  installedHookCount: 0,
465
+ installedSkillCount: 0,
362
466
  packagesCount: 0,
363
467
  };
364
468
  }
365
469
  warnPresetCommandCollisions(commands);
366
470
  const commandsToInstall = dedupeCommandsForInstall(commands);
471
+ warnPresetSkillCollisions(skills);
472
+ const skillsToInstall = dedupeSkillsForInstall(skills);
367
473
  try {
368
474
  if (!options.dryRun) {
369
475
  writeRulesToTargets(rules, assets, config.targets);
370
476
  writeCommandsToTargets(commandsToInstall, config.targets);
477
+ writeSkillsToTargets(skillsToInstall, config.targets);
371
478
  if (mcpServers && Object.keys(mcpServers).length > 0) {
372
479
  writeMcpServersToTargets(mcpServers, config.targets, cwd);
373
480
  }
@@ -378,12 +485,14 @@ async function installPackage(options = {}) {
378
485
  const uniqueRuleCount = new Set(rules.map((rule) => rule.name)).size;
379
486
  const uniqueCommandCount = new Set(commandsToInstall.map((command) => command.name)).size;
380
487
  const uniqueHookCount = (0, hooks_1.countHooks)(hooks);
488
+ const uniqueSkillCount = skillsToInstall.length;
381
489
  return {
382
490
  success: true,
383
491
  installedRuleCount: uniqueRuleCount,
384
492
  installedCommandCount: uniqueCommandCount,
385
493
  installedAssetCount: assets.length,
386
494
  installedHookCount: uniqueHookCount,
495
+ installedSkillCount: uniqueSkillCount,
387
496
  packagesCount: 1,
388
497
  };
389
498
  }
@@ -395,6 +504,7 @@ async function installPackage(options = {}) {
395
504
  installedCommandCount: 0,
396
505
  installedAssetCount: 0,
397
506
  installedHookCount: 0,
507
+ installedSkillCount: 0,
398
508
  packagesCount: 0,
399
509
  };
400
510
  }
@@ -415,6 +525,7 @@ async function install(options = {}) {
415
525
  installedCommandCount: 0,
416
526
  installedAssetCount: 0,
417
527
  installedHookCount: 0,
528
+ installedSkillCount: 0,
418
529
  packagesCount: 0,
419
530
  };
420
531
  }
@@ -447,11 +558,15 @@ async function installCommand(installOnCI, verbose, dryRun) {
447
558
  const ruleCount = result.installedRuleCount;
448
559
  const commandCount = result.installedCommandCount;
449
560
  const hookCount = result.installedHookCount;
561
+ const skillCount = result.installedSkillCount;
450
562
  const ruleMessage = ruleCount > 0 ? `${ruleCount} rule${ruleCount === 1 ? "" : "s"}` : null;
451
563
  const commandMessage = commandCount > 0
452
564
  ? `${commandCount} command${commandCount === 1 ? "" : "s"}`
453
565
  : null;
454
566
  const hookMessage = hookCount > 0 ? `${hookCount} hook${hookCount === 1 ? "" : "s"}` : null;
567
+ const skillMessage = skillCount > 0
568
+ ? `${skillCount} skill${skillCount === 1 ? "" : "s"}`
569
+ : null;
455
570
  const countsParts = [];
456
571
  if (ruleMessage) {
457
572
  countsParts.push(ruleMessage);
@@ -462,6 +577,9 @@ async function installCommand(installOnCI, verbose, dryRun) {
462
577
  if (hookMessage) {
463
578
  countsParts.push(hookMessage);
464
579
  }
580
+ if (skillMessage) {
581
+ countsParts.push(skillMessage);
582
+ }
465
583
  const countsMessage = countsParts.length > 0
466
584
  ? countsParts.join(", ").replace(/, ([^,]*)$/, " and $1")
467
585
  : "0 rules";
@@ -473,8 +591,11 @@ async function installCommand(installOnCI, verbose, dryRun) {
473
591
  console.log(`Dry run: validated ${countsMessage}`);
474
592
  }
475
593
  }
476
- else if (ruleCount === 0 && commandCount === 0 && hookCount === 0) {
477
- console.log("No rules, commands, or hooks installed");
594
+ else if (ruleCount === 0 &&
595
+ commandCount === 0 &&
596
+ hookCount === 0 &&
597
+ skillCount === 0) {
598
+ console.log("No rules, commands, hooks, or skills installed");
478
599
  }
479
600
  else if (result.packagesCount > 1) {
480
601
  console.log(`Successfully installed ${countsMessage} across ${result.packagesCount} packages`);
@@ -46,6 +46,12 @@ 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
+ }
49
55
  export interface RuleCollection {
50
56
  [target: string]: RuleFile[];
51
57
  }
@@ -54,6 +60,7 @@ export interface ResolvedConfig {
54
60
  rules: RuleFile[];
55
61
  commands: CommandFile[];
56
62
  assets: AssetFile[];
63
+ skills: SkillFile[];
57
64
  mcpServers: MCPServers;
58
65
  hooks: HooksJson;
59
66
  hookFiles: HookFile[];
@@ -68,6 +75,11 @@ export declare function validateConfig(config: unknown, configFilePath: string,
68
75
  export declare function loadRulesFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<RuleFile[]>;
69
76
  export declare function loadCommandsFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<CommandFile[]>;
70
77
  export declare function loadAssetsFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<AssetFile[]>;
78
+ /**
79
+ * Load skills from a skills/ directory
80
+ * Each direct subdirectory containing a SKILL.md file is considered a skill
81
+ */
82
+ export declare function loadSkillsFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<SkillFile[]>;
71
83
  /**
72
84
  * Extract namespace from preset path for directory structure
73
85
  * Handles both npm packages and local paths consistently
@@ -82,6 +94,7 @@ export declare function loadAllRules(config: Config, cwd: string): Promise<{
82
94
  rules: RuleFile[];
83
95
  commands: CommandFile[];
84
96
  assets: AssetFile[];
97
+ skills: SkillFile[];
85
98
  mcpServers: MCPServers;
86
99
  hooks: HooksJson;
87
100
  hookFiles: HookFile[];
@@ -11,6 +11,7 @@ exports.validateConfig = validateConfig;
11
11
  exports.loadRulesFromDirectory = loadRulesFromDirectory;
12
12
  exports.loadCommandsFromDirectory = loadCommandsFromDirectory;
13
13
  exports.loadAssetsFromDirectory = loadAssetsFromDirectory;
14
+ exports.loadSkillsFromDirectory = loadSkillsFromDirectory;
14
15
  exports.extractNamespaceFromPresetPath = extractNamespaceFromPresetPath;
15
16
  exports.resolvePresetPath = resolvePresetPath;
16
17
  exports.loadPreset = loadPreset;
@@ -96,14 +97,16 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
96
97
  const hasRules = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "rules"));
97
98
  const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "commands"));
98
99
  const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "hooks.json"));
100
+ const hasSkills = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "skills"));
99
101
  // In workspace mode, root config doesn't need these directories
100
102
  // since packages will have their own configurations
101
103
  if (!isWorkspaceMode &&
102
104
  !hasRules &&
103
105
  !hasCommands &&
104
106
  !hasHooks &&
107
+ !hasSkills &&
105
108
  !hasPresets) {
106
- throw new Error(`Root directory must contain at least one of: rules/, commands/, hooks.json, or have presets configured`);
109
+ throw new Error(`Root directory must contain at least one of: rules/, commands/, skills/, hooks.json, or have presets configured`);
107
110
  }
108
111
  }
109
112
  else if (!isWorkspaceMode && !hasPresets) {
@@ -202,6 +205,36 @@ async function loadAssetsFromDirectory(directoryPath, source, presetName) {
202
205
  }
203
206
  return assets;
204
207
  }
208
+ /**
209
+ * Load skills from a skills/ directory
210
+ * Each direct subdirectory containing a SKILL.md file is considered a skill
211
+ */
212
+ async function loadSkillsFromDirectory(directoryPath, source, presetName) {
213
+ const skills = [];
214
+ if (!fs_extra_1.default.existsSync(directoryPath)) {
215
+ return skills;
216
+ }
217
+ // Get all direct subdirectories
218
+ const entries = await fs_extra_1.default.readdir(directoryPath, { withFileTypes: true });
219
+ for (const entry of entries) {
220
+ if (!entry.isDirectory()) {
221
+ continue;
222
+ }
223
+ const skillPath = node_path_1.default.join(directoryPath, entry.name);
224
+ const skillMdPath = node_path_1.default.join(skillPath, "SKILL.md");
225
+ // Only include directories that contain a SKILL.md file
226
+ if (!fs_extra_1.default.existsSync(skillMdPath)) {
227
+ continue;
228
+ }
229
+ skills.push({
230
+ name: entry.name,
231
+ sourcePath: skillPath,
232
+ source,
233
+ presetName,
234
+ });
235
+ }
236
+ return skills;
237
+ }
205
238
  /**
206
239
  * Extract namespace from preset path for directory structure
207
240
  * Handles both npm packages and local paths consistently
@@ -258,8 +291,9 @@ async function loadPreset(presetPath, cwd) {
258
291
  const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "commands"));
259
292
  const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "hooks.json"));
260
293
  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/`);
294
+ const hasSkills = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "skills"));
295
+ if (!hasRules && !hasCommands && !hasHooks && !hasAssets && !hasSkills) {
296
+ throw new Error(`Preset "${presetPath}" must have at least one of: rules/, commands/, skills/, hooks.json, or assets/`);
263
297
  }
264
298
  return {
265
299
  config: presetConfig,
@@ -270,6 +304,7 @@ async function loadAllRules(config, cwd) {
270
304
  const allRules = [];
271
305
  const allCommands = [];
272
306
  const allAssets = [];
307
+ const allSkills = [];
273
308
  const allHookFiles = [];
274
309
  const allHooksConfigs = [];
275
310
  let mergedMcpServers = { ...config.mcpServers };
@@ -301,6 +336,12 @@ async function loadAllRules(config, cwd) {
301
336
  const localAssets = await loadAssetsFromDirectory(assetsPath, "local");
302
337
  allAssets.push(...localAssets);
303
338
  }
339
+ // Load skills from skills/ subdirectory
340
+ const skillsPath = node_path_1.default.join(rootPath, "skills");
341
+ if (fs_extra_1.default.existsSync(skillsPath)) {
342
+ const localSkills = await loadSkillsFromDirectory(skillsPath, "local");
343
+ allSkills.push(...localSkills);
344
+ }
304
345
  }
305
346
  // Load presets
306
347
  if (config.presets) {
@@ -332,6 +373,12 @@ async function loadAllRules(config, cwd) {
332
373
  const presetAssets = await loadAssetsFromDirectory(presetAssetsPath, "preset", presetPath);
333
374
  allAssets.push(...presetAssets);
334
375
  }
376
+ // Load preset skills from skills/ subdirectory
377
+ const presetSkillsPath = node_path_1.default.join(presetRootDir, "skills");
378
+ if (fs_extra_1.default.existsSync(presetSkillsPath)) {
379
+ const presetSkills = await loadSkillsFromDirectory(presetSkillsPath, "preset", presetPath);
380
+ allSkills.push(...presetSkills);
381
+ }
335
382
  // Merge MCP servers from preset
336
383
  if (preset.config.mcpServers) {
337
384
  mergedMcpServers = mergePresetMcpServers(mergedMcpServers, preset.config.mcpServers);
@@ -344,6 +391,7 @@ async function loadAllRules(config, cwd) {
344
391
  rules: allRules,
345
392
  commands: allCommands,
346
393
  assets: allAssets,
394
+ skills: allSkills,
347
395
  mcpServers: mergedMcpServers,
348
396
  hooks: mergedHooks,
349
397
  hookFiles: allHookFiles,
@@ -403,12 +451,13 @@ async function loadConfig(cwd) {
403
451
  const isWorkspaces = resolveWorkspaces(config, configResult.filepath, workingDir);
404
452
  validateConfig(config, configResult.filepath, workingDir, isWorkspaces);
405
453
  const configWithDefaults = applyDefaults(config, isWorkspaces);
406
- const { rules, commands, assets, mcpServers, hooks, hookFiles } = await loadAllRules(configWithDefaults, workingDir);
454
+ const { rules, commands, assets, skills, mcpServers, hooks, hookFiles } = await loadAllRules(configWithDefaults, workingDir);
407
455
  return {
408
456
  config: configWithDefaults,
409
457
  rules,
410
458
  commands,
411
459
  assets,
460
+ skills,
412
461
  mcpServers,
413
462
  hooks,
414
463
  hookFiles,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicm",
3
- "version": "0.19.1",
3
+ "version": "0.20.0",
4
4
  "description": "A TypeScript CLI tool for managing AI IDE rules across different projects and teams",
5
5
  "main": "dist/api.js",
6
6
  "types": "dist/api.d.ts",
@@ -12,6 +12,21 @@
12
12
  "README.md",
13
13
  "LICENSE"
14
14
  ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "watch": "tsc --watch",
18
+ "start": "node dist/bin/aicm.js",
19
+ "dev": "ts-node src/bin/aicm.ts",
20
+ "test": "pnpm build && jest",
21
+ "test:watch": "jest --watch",
22
+ "test:all": "npm run build && npm run test",
23
+ "format": "prettier --write .",
24
+ "format:check": "prettier --check .",
25
+ "lint": "eslint",
26
+ "prepare": "husky install && npx ts-node src/bin/aicm.ts install",
27
+ "version": "auto-changelog -p && git add CHANGELOG.md",
28
+ "release": "np --no-tests"
29
+ },
15
30
  "keywords": [
16
31
  "ai",
17
32
  "ide",
@@ -51,18 +66,5 @@
51
66
  "*.{js,ts,json,md,mjs}": "prettier --write",
52
67
  "*.ts": "eslint"
53
68
  },
54
- "scripts": {
55
- "build": "tsc",
56
- "watch": "tsc --watch",
57
- "start": "node dist/bin/aicm.js",
58
- "dev": "ts-node src/bin/aicm.ts",
59
- "test": "pnpm build && jest",
60
- "test:watch": "jest --watch",
61
- "test:all": "npm run build && npm run test",
62
- "format": "prettier --write .",
63
- "format:check": "prettier --check .",
64
- "lint": "eslint",
65
- "version": "auto-changelog -p && git add CHANGELOG.md",
66
- "release": "np --no-tests"
67
- }
68
- }
69
+ "packageManager": "pnpm@10.18.3"
70
+ }