aicm 0.19.0 → 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/api.d.ts CHANGED
@@ -5,6 +5,12 @@ import { InstallOptions, InstallResult } from "./commands/install";
5
5
  * @returns Result of the install operation
6
6
  */
7
7
  export declare function install(options?: InstallOptions): Promise<InstallResult>;
8
+ /**
9
+ * Check if workspaces mode is enabled without loading all rules/presets
10
+ * @param cwd Current working directory (optional, defaults to process.cwd())
11
+ * @returns True if workspaces mode is enabled
12
+ */
13
+ export declare function checkWorkspacesEnabled(cwd?: string): Promise<boolean>;
8
14
  export type { InstallOptions, InstallResult } from "./commands/install";
9
15
  export type { ResolvedConfig, Config, RuleFile, CommandFile, MCPServers, } from "./utils/config";
10
16
  export type { HookFile, HooksJson, HookType, HookCommand } from "./utils/hooks";
package/dist/api.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.install = install;
4
+ exports.checkWorkspacesEnabled = checkWorkspacesEnabled;
4
5
  const install_1 = require("./commands/install");
6
+ const config_1 = require("./utils/config");
5
7
  /**
6
8
  * Install AICM rules based on configuration
7
9
  * @param options Installation options
@@ -10,3 +12,11 @@ const install_1 = require("./commands/install");
10
12
  async function install(options = {}) {
11
13
  return (0, install_1.install)(options);
12
14
  }
15
+ /**
16
+ * Check if workspaces mode is enabled without loading all rules/presets
17
+ * @param cwd Current working directory (optional, defaults to process.cwd())
18
+ * @returns True if workspaces mode is enabled
19
+ */
20
+ async function checkWorkspacesEnabled(cwd) {
21
+ return (0, config_1.checkWorkspacesEnabled)(cwd);
22
+ }
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 {
@@ -251,11 +306,9 @@ async function cleanWorkspaces(cwd, verbose = false) {
251
306
  };
252
307
  }
253
308
  async function clean(options = {}) {
254
- var _a;
255
309
  const cwd = options.cwd || process.cwd();
256
310
  const verbose = options.verbose || false;
257
- const shouldUseWorkspaces = ((_a = (await (0, config_1.loadConfig)(cwd))) === null || _a === void 0 ? void 0 : _a.config.workspaces) ||
258
- (0, config_1.detectWorkspacesFromPackageJson)(cwd);
311
+ const shouldUseWorkspaces = await (0, config_1.checkWorkspacesEnabled)(cwd);
259
312
  if (shouldUseWorkspaces) {
260
313
  return cleanWorkspaces(cwd, verbose);
261
314
  }
@@ -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;
@@ -23,11 +26,27 @@ const install_workspaces_1 = require("./install-workspaces");
23
26
  /**
24
27
  * Rewrite asset references from source paths to installation paths
25
28
  * Only rewrites the ../assets/ pattern - everything else is preserved
29
+ *
30
+ * @param content - The file content to rewrite
31
+ * @param presetName - The preset name if this file is from a preset
32
+ * @param fileInstallDepth - The depth of the file's installation directory relative to .cursor/
33
+ * For example: .cursor/commands/aicm/file.md has depth 2 (commands, aicm)
34
+ * .cursor/rules/aicm/preset/file.mdc has depth 3 (rules, aicm, preset)
26
35
  */
27
- function rewriteAssetReferences(content) {
28
- // Replace ../assets/ with ../../assets/aicm/
36
+ function rewriteAssetReferences(content, presetName, fileInstallDepth = 2) {
37
+ // Calculate the relative path from the file to .cursor/assets/aicm/
38
+ // We need to go up fileInstallDepth levels to reach .cursor/, then down to assets/aicm/
39
+ const upLevels = "../".repeat(fileInstallDepth);
40
+ // If this is from a preset, include the preset namespace in the asset path
41
+ let assetBasePath = "assets/aicm/";
42
+ if (presetName) {
43
+ const namespace = (0, config_1.extractNamespaceFromPresetPath)(presetName);
44
+ assetBasePath = node_path_1.default.posix.join("assets", "aicm", ...namespace) + "/";
45
+ }
46
+ const targetPath = upLevels + assetBasePath;
47
+ // Replace ../assets/ with the calculated target path
29
48
  // Handles both forward slashes and backslashes for cross-platform compatibility
30
- return content.replace(/\.\.[\\/]assets[\\/]/g, "../../assets/aicm/");
49
+ return content.replace(/\.\.[\\/]assets[\\/]/g, targetPath);
31
50
  }
32
51
  function getTargetPaths() {
33
52
  const projectDir = process.cwd();
@@ -54,8 +73,18 @@ function writeCursorRules(rules, cursorRulesDir) {
54
73
  }
55
74
  const ruleFile = rulePath + ".mdc";
56
75
  fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(ruleFile));
76
+ // Calculate the depth for asset path rewriting
77
+ // cursorRulesDir is .cursor/rules/aicm (depth 2 from .cursor)
78
+ // Add namespace depth if present
79
+ let fileInstallDepth = 2; // rules, aicm
80
+ if (rule.presetName) {
81
+ const namespace = (0, config_1.extractNamespaceFromPresetPath)(rule.presetName);
82
+ fileInstallDepth += namespace.length;
83
+ }
84
+ // Add any subdirectories in the rule name
85
+ fileInstallDepth += ruleNameParts.length - 1; // -1 because the last part is the filename
57
86
  // Rewrite asset references before writing
58
- const content = rewriteAssetReferences(rule.content);
87
+ const content = rewriteAssetReferences(rule.content, rule.presetName, fileInstallDepth);
59
88
  fs_extra_1.default.writeFileSync(ruleFile, content);
60
89
  }
61
90
  }
@@ -69,8 +98,14 @@ function writeCursorCommands(commands, cursorCommandsDir) {
69
98
  const commandPath = node_path_1.default.join(cursorCommandsDir, ...commandNameParts);
70
99
  const commandFile = commandPath + ".md";
71
100
  fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(commandFile));
101
+ // Calculate the depth for asset path rewriting
102
+ // cursorCommandsDir is .cursor/commands/aicm (depth 2 from .cursor)
103
+ // Commands are NOT namespaced by preset, but we still need to account for subdirectories
104
+ let fileInstallDepth = 2; // commands, aicm
105
+ // Add any subdirectories in the command name
106
+ fileInstallDepth += commandNameParts.length - 1; // -1 because the last part is the filename
72
107
  // Rewrite asset references before writing
73
- const content = rewriteAssetReferences(command.content);
108
+ const content = rewriteAssetReferences(command.content, command.presetName, fileInstallDepth);
74
109
  fs_extra_1.default.writeFileSync(commandFile, content);
75
110
  }
76
111
  }
@@ -92,8 +127,12 @@ function writeRulesForFile(rules, assets, ruleDir, rulesFile) {
92
127
  // For local rules, maintain the original flat structure
93
128
  rulePath = node_path_1.default.join(ruleDir, ...ruleNameParts);
94
129
  }
95
- // Rewrite asset references before writing
96
- const content = rewriteAssetReferences(rule.content);
130
+ // For windsurf/codex/claude, assets are installed at the same namespace level as rules
131
+ // Example: .aicm/my-preset/rule.md and .aicm/my-preset/asset.json
132
+ // So we need to remove the 'assets/' part from the path
133
+ // ../assets/file.json -> ../file.json
134
+ // ../../assets/file.json -> ../../file.json
135
+ const content = rule.content.replace(/(\.\.[/\\])assets[/\\]/g, "$1");
97
136
  const physicalRulePath = rulePath + ".md";
98
137
  fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(physicalRulePath));
99
138
  fs_extra_1.default.writeFileSync(physicalRulePath, content);
@@ -192,6 +231,105 @@ function writeCommandsToTargets(commands, targets) {
192
231
  // Other targets do not support commands yet
193
232
  }
194
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
+ }
195
333
  function warnPresetCommandCollisions(commands) {
196
334
  const collisions = new Map();
197
335
  for (const command of commands) {
@@ -312,10 +450,11 @@ async function installPackage(options = {}) {
312
450
  installedCommandCount: 0,
313
451
  installedAssetCount: 0,
314
452
  installedHookCount: 0,
453
+ installedSkillCount: 0,
315
454
  packagesCount: 0,
316
455
  };
317
456
  }
318
- const { config, rules, commands, assets, mcpServers, hooks, hookFiles } = resolvedConfig;
457
+ const { config, rules, commands, assets, skills, mcpServers, hooks, hookFiles, } = resolvedConfig;
319
458
  if (config.skipInstall === true) {
320
459
  return {
321
460
  success: true,
@@ -323,15 +462,19 @@ async function installPackage(options = {}) {
323
462
  installedCommandCount: 0,
324
463
  installedAssetCount: 0,
325
464
  installedHookCount: 0,
465
+ installedSkillCount: 0,
326
466
  packagesCount: 0,
327
467
  };
328
468
  }
329
469
  warnPresetCommandCollisions(commands);
330
470
  const commandsToInstall = dedupeCommandsForInstall(commands);
471
+ warnPresetSkillCollisions(skills);
472
+ const skillsToInstall = dedupeSkillsForInstall(skills);
331
473
  try {
332
474
  if (!options.dryRun) {
333
475
  writeRulesToTargets(rules, assets, config.targets);
334
476
  writeCommandsToTargets(commandsToInstall, config.targets);
477
+ writeSkillsToTargets(skillsToInstall, config.targets);
335
478
  if (mcpServers && Object.keys(mcpServers).length > 0) {
336
479
  writeMcpServersToTargets(mcpServers, config.targets, cwd);
337
480
  }
@@ -342,12 +485,14 @@ async function installPackage(options = {}) {
342
485
  const uniqueRuleCount = new Set(rules.map((rule) => rule.name)).size;
343
486
  const uniqueCommandCount = new Set(commandsToInstall.map((command) => command.name)).size;
344
487
  const uniqueHookCount = (0, hooks_1.countHooks)(hooks);
488
+ const uniqueSkillCount = skillsToInstall.length;
345
489
  return {
346
490
  success: true,
347
491
  installedRuleCount: uniqueRuleCount,
348
492
  installedCommandCount: uniqueCommandCount,
349
493
  installedAssetCount: assets.length,
350
494
  installedHookCount: uniqueHookCount,
495
+ installedSkillCount: uniqueSkillCount,
351
496
  packagesCount: 1,
352
497
  };
353
498
  }
@@ -359,6 +504,7 @@ async function installPackage(options = {}) {
359
504
  installedCommandCount: 0,
360
505
  installedAssetCount: 0,
361
506
  installedHookCount: 0,
507
+ installedSkillCount: 0,
362
508
  packagesCount: 0,
363
509
  };
364
510
  }
@@ -379,6 +525,7 @@ async function install(options = {}) {
379
525
  installedCommandCount: 0,
380
526
  installedAssetCount: 0,
381
527
  installedHookCount: 0,
528
+ installedSkillCount: 0,
382
529
  packagesCount: 0,
383
530
  };
384
531
  }
@@ -411,11 +558,15 @@ async function installCommand(installOnCI, verbose, dryRun) {
411
558
  const ruleCount = result.installedRuleCount;
412
559
  const commandCount = result.installedCommandCount;
413
560
  const hookCount = result.installedHookCount;
561
+ const skillCount = result.installedSkillCount;
414
562
  const ruleMessage = ruleCount > 0 ? `${ruleCount} rule${ruleCount === 1 ? "" : "s"}` : null;
415
563
  const commandMessage = commandCount > 0
416
564
  ? `${commandCount} command${commandCount === 1 ? "" : "s"}`
417
565
  : null;
418
566
  const hookMessage = hookCount > 0 ? `${hookCount} hook${hookCount === 1 ? "" : "s"}` : null;
567
+ const skillMessage = skillCount > 0
568
+ ? `${skillCount} skill${skillCount === 1 ? "" : "s"}`
569
+ : null;
419
570
  const countsParts = [];
420
571
  if (ruleMessage) {
421
572
  countsParts.push(ruleMessage);
@@ -426,6 +577,9 @@ async function installCommand(installOnCI, verbose, dryRun) {
426
577
  if (hookMessage) {
427
578
  countsParts.push(hookMessage);
428
579
  }
580
+ if (skillMessage) {
581
+ countsParts.push(skillMessage);
582
+ }
429
583
  const countsMessage = countsParts.length > 0
430
584
  ? countsParts.join(", ").replace(/, ([^,]*)$/, " and $1")
431
585
  : "0 rules";
@@ -437,8 +591,11 @@ async function installCommand(installOnCI, verbose, dryRun) {
437
591
  console.log(`Dry run: validated ${countsMessage}`);
438
592
  }
439
593
  }
440
- else if (ruleCount === 0 && commandCount === 0 && hookCount === 0) {
441
- 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");
442
599
  }
443
600
  else if (result.packagesCount > 1) {
444
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,10 +94,16 @@ 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[];
88
101
  }>;
89
102
  export declare function loadConfigFile(searchFrom?: string): Promise<CosmiconfigResult>;
103
+ /**
104
+ * Check if workspaces mode is enabled without loading all rules/presets
105
+ * This is useful for commands that only need to know the workspace setting
106
+ */
107
+ export declare function checkWorkspacesEnabled(cwd?: string): Promise<boolean>;
90
108
  export declare function loadConfig(cwd?: string): Promise<ResolvedConfig | null>;
91
109
  export declare function saveConfig(config: Config, cwd?: string): boolean;
@@ -11,11 +11,13 @@ 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;
17
18
  exports.loadAllRules = loadAllRules;
18
19
  exports.loadConfigFile = loadConfigFile;
20
+ exports.checkWorkspacesEnabled = checkWorkspacesEnabled;
19
21
  exports.loadConfig = loadConfig;
20
22
  exports.saveConfig = saveConfig;
21
23
  const fs_extra_1 = __importDefault(require("fs-extra"));
@@ -95,14 +97,16 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
95
97
  const hasRules = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "rules"));
96
98
  const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "commands"));
97
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"));
98
101
  // In workspace mode, root config doesn't need these directories
99
102
  // since packages will have their own configurations
100
103
  if (!isWorkspaceMode &&
101
104
  !hasRules &&
102
105
  !hasCommands &&
103
106
  !hasHooks &&
107
+ !hasSkills &&
104
108
  !hasPresets) {
105
- 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`);
106
110
  }
107
111
  }
108
112
  else if (!isWorkspaceMode && !hasPresets) {
@@ -201,6 +205,36 @@ async function loadAssetsFromDirectory(directoryPath, source, presetName) {
201
205
  }
202
206
  return assets;
203
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
+ }
204
238
  /**
205
239
  * Extract namespace from preset path for directory structure
206
240
  * Handles both npm packages and local paths consistently
@@ -257,8 +291,9 @@ async function loadPreset(presetPath, cwd) {
257
291
  const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "commands"));
258
292
  const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "hooks.json"));
259
293
  const hasAssets = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "assets"));
260
- if (!hasRules && !hasCommands && !hasHooks && !hasAssets) {
261
- 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/`);
262
297
  }
263
298
  return {
264
299
  config: presetConfig,
@@ -269,6 +304,7 @@ async function loadAllRules(config, cwd) {
269
304
  const allRules = [];
270
305
  const allCommands = [];
271
306
  const allAssets = [];
307
+ const allSkills = [];
272
308
  const allHookFiles = [];
273
309
  const allHooksConfigs = [];
274
310
  let mergedMcpServers = { ...config.mcpServers };
@@ -300,6 +336,12 @@ async function loadAllRules(config, cwd) {
300
336
  const localAssets = await loadAssetsFromDirectory(assetsPath, "local");
301
337
  allAssets.push(...localAssets);
302
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
+ }
303
345
  }
304
346
  // Load presets
305
347
  if (config.presets) {
@@ -331,6 +373,12 @@ async function loadAllRules(config, cwd) {
331
373
  const presetAssets = await loadAssetsFromDirectory(presetAssetsPath, "preset", presetPath);
332
374
  allAssets.push(...presetAssets);
333
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
+ }
334
382
  // Merge MCP servers from preset
335
383
  if (preset.config.mcpServers) {
336
384
  mergedMcpServers = mergePresetMcpServers(mergedMcpServers, preset.config.mcpServers);
@@ -343,6 +391,7 @@ async function loadAllRules(config, cwd) {
343
391
  rules: allRules,
344
392
  commands: allCommands,
345
393
  assets: allAssets,
394
+ skills: allSkills,
346
395
  mcpServers: mergedMcpServers,
347
396
  hooks: mergedHooks,
348
397
  hookFiles: allHookFiles,
@@ -380,6 +429,18 @@ async function loadConfigFile(searchFrom) {
380
429
  throw new Error(`Failed to load configuration: ${error instanceof Error ? error.message : "Unknown error"}`);
381
430
  }
382
431
  }
432
+ /**
433
+ * Check if workspaces mode is enabled without loading all rules/presets
434
+ * This is useful for commands that only need to know the workspace setting
435
+ */
436
+ async function checkWorkspacesEnabled(cwd) {
437
+ const workingDir = cwd || process.cwd();
438
+ const configResult = await loadConfigFile(workingDir);
439
+ if (!(configResult === null || configResult === void 0 ? void 0 : configResult.config)) {
440
+ return detectWorkspacesFromPackageJson(workingDir);
441
+ }
442
+ return resolveWorkspaces(configResult.config, configResult.filepath, workingDir);
443
+ }
383
444
  async function loadConfig(cwd) {
384
445
  const workingDir = cwd || process.cwd();
385
446
  const configResult = await loadConfigFile(workingDir);
@@ -390,12 +451,13 @@ async function loadConfig(cwd) {
390
451
  const isWorkspaces = resolveWorkspaces(config, configResult.filepath, workingDir);
391
452
  validateConfig(config, configResult.filepath, workingDir, isWorkspaces);
392
453
  const configWithDefaults = applyDefaults(config, isWorkspaces);
393
- const { rules, commands, assets, mcpServers, hooks, hookFiles } = await loadAllRules(configWithDefaults, workingDir);
454
+ const { rules, commands, assets, skills, mcpServers, hooks, hookFiles } = await loadAllRules(configWithDefaults, workingDir);
394
455
  return {
395
456
  config: configWithDefaults,
396
457
  rules,
397
458
  commands,
398
459
  assets,
460
+ skills,
399
461
  mcpServers,
400
462
  hooks,
401
463
  hookFiles,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicm",
3
- "version": "0.19.0",
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
+ }