aicm 0.15.0 → 0.16.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
@@ -17,7 +17,9 @@ Modern AI-powered IDEs like Cursor and Agents like Codex enable developers to wr
17
17
  aicm accepts Cursor's `.mdc` format as it provides the most comprehensive feature set. For other AI tools and IDEs, aicm automatically generates compatible formats:
18
18
 
19
19
  - **Cursor**: Native `.mdc` files with full feature support
20
- - **Windsurf/Codex**: Generates `.windsurfrules`/`AGENTS.md` files with natural language adaptations
20
+ - **Windsurf**: Generates `.windsurfrules` file
21
+ - **Codex**: Generates `AGENTS.md` file
22
+ - **Claude**: Generates `CLAUDE.md` file
21
23
 
22
24
  This approach ensures you write your rules once in the richest format available, while maintaining compatibility across different AI development environments.
23
25
 
@@ -93,6 +95,53 @@ For project-specific rules, you can specify `rulesDir` in your `aicm.json` confi
93
95
  }
94
96
  ```
95
97
 
98
+ ### Using Commands
99
+
100
+ Cursor supports custom commands that can be invoked directly in the chat interface. aicm can manage these command files
101
+ alongside your rules and MCP configurations so they install automatically into Cursor.
102
+
103
+ #### Local Commands
104
+
105
+ Add a commands directory to your project configuration:
106
+
107
+ ```json
108
+ {
109
+ "commandsDir": "./commands",
110
+ "targets": ["cursor"]
111
+ }
112
+ ```
113
+
114
+ Command files ending in `.md` are installed to `.cursor/commands/aicm/` and appear in Cursor under the `/` command menu.
115
+
116
+ #### Commands in Presets
117
+
118
+ Presets can ship reusable command libraries in addition to rules:
119
+
120
+ ```json
121
+ {
122
+ "rulesDir": "rules",
123
+ "commandsDir": "commands"
124
+ }
125
+ ```
126
+
127
+ Preset command files install alongside local ones in `.cursor/commands/aicm/` so they appear with concise paths inside Cursor.
128
+ If multiple presets provide a command at the same relative path, aicm will warn during installation and use the version from the
129
+ last preset listed in your configuration. Use `overrides` to explicitly choose a different definition when needed.
130
+
131
+ #### Command Overrides
132
+
133
+ Use the existing `overrides` field to customize or disable commands provided by presets:
134
+
135
+ ```json
136
+ {
137
+ "presets": ["@team/dev-preset"],
138
+ "overrides": {
139
+ "legacy-command": false,
140
+ "custom-test": "./commands/test.md"
141
+ }
142
+ }
143
+ ```
144
+
96
145
  ### Notes
97
146
 
98
147
  - Generated rules are always placed in a subdirectory for deterministic cleanup and easy gitignore.
@@ -207,6 +256,7 @@ Create an `aicm.json` file in your project root, or an `aicm` key in your projec
207
256
  ```json
208
257
  {
209
258
  "rulesDir": "./rules",
259
+ "commandsDir": "./commands",
210
260
  "targets": ["cursor"],
211
261
  "presets": [],
212
262
  "overrides": {},
@@ -216,6 +266,7 @@ Create an `aicm.json` file in your project root, or an `aicm` key in your projec
216
266
  ```
217
267
 
218
268
  - **rulesDir**: Directory containing all rule files.
269
+ - **commandsDir**: Directory containing Cursor command files.
219
270
  - **targets**: IDEs/Agent targets where rules should be installed. Defaults to `["cursor"]`.
220
271
  - **presets**: List of preset packages or paths to include.
221
272
  - **overrides**: Map of rule names to `false` (disable) or a replacement file path.
package/dist/api.d.ts CHANGED
@@ -6,4 +6,4 @@ import { InstallOptions, InstallResult } from "./commands/install";
6
6
  */
7
7
  export declare function install(options?: InstallOptions): Promise<InstallResult>;
8
8
  export type { InstallOptions, InstallResult } from "./commands/install";
9
- export type { ResolvedConfig, Config, RuleFile, MCPServers, } from "./utils/config";
9
+ export type { ResolvedConfig, Config, RuleFile, CommandFile, MCPServers, } from "./utils/config";
@@ -37,6 +37,10 @@ export interface InstallResult {
37
37
  * Number of rules installed
38
38
  */
39
39
  installedRuleCount: number;
40
+ /**
41
+ * Number of commands installed
42
+ */
43
+ installedCommandCount: number;
40
44
  /**
41
45
  * Number of packages installed
42
46
  */
@@ -20,6 +20,7 @@ function getTargetPaths() {
20
20
  cursor: node_path_1.default.join(projectDir, ".cursor", "rules", "aicm"),
21
21
  windsurf: node_path_1.default.join(projectDir, ".aicm"),
22
22
  codex: node_path_1.default.join(projectDir, ".aicm"),
23
+ claude: node_path_1.default.join(projectDir, ".aicm"),
23
24
  };
24
25
  }
25
26
  function writeCursorRules(rules, cursorRulesDir) {
@@ -42,6 +43,19 @@ function writeCursorRules(rules, cursorRulesDir) {
42
43
  fs_extra_1.default.writeFileSync(ruleFile, rule.content);
43
44
  }
44
45
  }
46
+ function writeCursorCommands(commands, cursorCommandsDir) {
47
+ fs_extra_1.default.removeSync(cursorCommandsDir);
48
+ for (const command of commands) {
49
+ const commandNameParts = command.name
50
+ .replace(/\\/g, "/")
51
+ .split("/")
52
+ .filter(Boolean);
53
+ const commandPath = node_path_1.default.join(cursorCommandsDir, ...commandNameParts);
54
+ const commandFile = commandPath + ".md";
55
+ fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(commandFile));
56
+ fs_extra_1.default.writeFileSync(commandFile, command.content);
57
+ }
58
+ }
45
59
  function extractNamespaceFromPresetPath(presetPath) {
46
60
  // Special case: npm package names always use forward slashes, regardless of platform
47
61
  if (presetPath.startsWith("@")) {
@@ -49,7 +63,7 @@ function extractNamespaceFromPresetPath(presetPath) {
49
63
  return presetPath.split("/");
50
64
  }
51
65
  const parts = presetPath.split(node_path_1.default.sep);
52
- return parts.filter((part) => part.length > 0); // Filter out empty segments
66
+ return parts.filter((part) => part.length > 0 && part !== "." && part !== "..");
53
67
  }
54
68
  /**
55
69
  * Write rules to a shared directory and update the given rules file
@@ -116,8 +130,55 @@ function writeRulesToTargets(rules, targets) {
116
130
  writeRulesForFile(rules, targetPaths.codex, "AGENTS.md");
117
131
  }
118
132
  break;
133
+ case "claude":
134
+ if (rules.length > 0) {
135
+ writeRulesForFile(rules, targetPaths.claude, "CLAUDE.md");
136
+ }
137
+ break;
138
+ }
139
+ }
140
+ }
141
+ function writeCommandsToTargets(commands, targets) {
142
+ const projectDir = process.cwd();
143
+ const cursorRoot = node_path_1.default.join(projectDir, ".cursor");
144
+ for (const target of targets) {
145
+ if (target === "cursor") {
146
+ const commandsDir = node_path_1.default.join(cursorRoot, "commands", "aicm");
147
+ writeCursorCommands(commands, commandsDir);
119
148
  }
149
+ // Other targets do not support commands yet
150
+ }
151
+ }
152
+ function warnPresetCommandCollisions(commands) {
153
+ const collisions = new Map();
154
+ for (const command of commands) {
155
+ if (!command.presetName)
156
+ continue;
157
+ const entry = collisions.get(command.name);
158
+ if (entry) {
159
+ entry.presets.add(command.presetName);
160
+ entry.lastPreset = command.presetName;
161
+ }
162
+ else {
163
+ collisions.set(command.name, {
164
+ presets: new Set([command.presetName]),
165
+ lastPreset: command.presetName,
166
+ });
167
+ }
168
+ }
169
+ for (const [commandName, { presets, lastPreset }] of collisions) {
170
+ if (presets.size > 1) {
171
+ const presetList = Array.from(presets).sort().join(", ");
172
+ console.warn(chalk_1.default.yellow(`Warning: multiple presets provide the "${commandName}" command (${presetList}). Using definition from ${lastPreset}.`));
173
+ }
174
+ }
175
+ }
176
+ function dedupeCommandsForInstall(commands) {
177
+ const unique = new Map();
178
+ for (const command of commands) {
179
+ unique.set(command.name, command);
120
180
  }
181
+ return Array.from(unique.values());
121
182
  }
122
183
  /**
123
184
  * Write MCP servers configuration to IDE targets
@@ -264,29 +325,35 @@ async function installPackage(options = {}) {
264
325
  success: false,
265
326
  error: new Error("Configuration file not found"),
266
327
  installedRuleCount: 0,
328
+ installedCommandCount: 0,
267
329
  packagesCount: 0,
268
330
  };
269
331
  }
270
- const { config, rules, mcpServers } = resolvedConfig;
332
+ const { config, rules, commands, mcpServers } = resolvedConfig;
271
333
  if (config.skipInstall === true) {
272
334
  return {
273
335
  success: true,
274
336
  installedRuleCount: 0,
337
+ installedCommandCount: 0,
275
338
  packagesCount: 0,
276
339
  };
277
340
  }
341
+ warnPresetCommandCollisions(commands);
342
+ const commandsToInstall = dedupeCommandsForInstall(commands);
278
343
  try {
279
344
  if (!options.dryRun) {
280
- // Write rules to targets
281
345
  writeRulesToTargets(rules, config.targets);
282
- // Write MCP servers
346
+ writeCommandsToTargets(commandsToInstall, config.targets);
283
347
  if (mcpServers && Object.keys(mcpServers).length > 0) {
284
348
  writeMcpServersToTargets(mcpServers, config.targets, cwd);
285
349
  }
286
350
  }
351
+ const uniqueRuleCount = new Set(rules.map((rule) => rule.name)).size;
352
+ const uniqueCommandCount = new Set(commandsToInstall.map((command) => command.name)).size;
287
353
  return {
288
354
  success: true,
289
- installedRuleCount: rules.length,
355
+ installedRuleCount: uniqueRuleCount,
356
+ installedCommandCount: uniqueCommandCount,
290
357
  packagesCount: 1,
291
358
  };
292
359
  }
@@ -295,6 +362,7 @@ async function installPackage(options = {}) {
295
362
  success: false,
296
363
  error: error instanceof Error ? error : new Error(String(error)),
297
364
  installedRuleCount: 0,
365
+ installedCommandCount: 0,
298
366
  packagesCount: 0,
299
367
  };
300
368
  }
@@ -306,6 +374,7 @@ async function installPackage(options = {}) {
306
374
  async function installWorkspacesPackages(packages, options = {}) {
307
375
  const results = [];
308
376
  let totalRuleCount = 0;
377
+ let totalCommandCount = 0;
309
378
  // Install packages sequentially for now (can be parallelized later)
310
379
  for (const pkg of packages) {
311
380
  const packagePath = pkg.absolutePath;
@@ -316,11 +385,13 @@ async function installWorkspacesPackages(packages, options = {}) {
316
385
  config: pkg.config,
317
386
  });
318
387
  totalRuleCount += result.installedRuleCount;
388
+ totalCommandCount += result.installedCommandCount;
319
389
  results.push({
320
390
  path: pkg.relativePath,
321
391
  success: result.success,
322
392
  error: result.error,
323
393
  installedRuleCount: result.installedRuleCount,
394
+ installedCommandCount: result.installedCommandCount,
324
395
  });
325
396
  }
326
397
  catch (error) {
@@ -329,6 +400,7 @@ async function installWorkspacesPackages(packages, options = {}) {
329
400
  success: false,
330
401
  error: error instanceof Error ? error : new Error(String(error)),
331
402
  installedRuleCount: 0,
403
+ installedCommandCount: 0,
332
404
  });
333
405
  }
334
406
  }
@@ -337,6 +409,7 @@ async function installWorkspacesPackages(packages, options = {}) {
337
409
  success: failedPackages.length === 0,
338
410
  packages: results,
339
411
  totalRuleCount,
412
+ totalCommandCount,
340
413
  };
341
414
  }
342
415
  /**
@@ -355,16 +428,18 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
355
428
  const isRoot = pkg.relativePath === ".";
356
429
  if (!isRoot)
357
430
  return true;
358
- // For root directories, only keep if it has rules or presets
431
+ // For root directories, only keep if it has rules, commands, or presets
359
432
  const hasRules = pkg.config.rules && pkg.config.rules.length > 0;
433
+ const hasCommands = pkg.config.commands && pkg.config.commands.length > 0;
360
434
  const hasPresets = pkg.config.config.presets && pkg.config.config.presets.length > 0;
361
- return hasRules || hasPresets;
435
+ return hasRules || hasCommands || hasPresets;
362
436
  });
363
437
  if (packages.length === 0) {
364
438
  return {
365
439
  success: false,
366
440
  error: new Error("No packages with aicm configurations found"),
367
441
  installedRuleCount: 0,
442
+ installedCommandCount: 0,
368
443
  packagesCount: 0,
369
444
  };
370
445
  }
@@ -392,7 +467,11 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
392
467
  if (verbose) {
393
468
  result.packages.forEach((pkg) => {
394
469
  if (pkg.success) {
395
- console.log(chalk_1.default.green(`✅ ${pkg.path} (${pkg.installedRuleCount} rules)`));
470
+ const summaryParts = [`${pkg.installedRuleCount} rules`];
471
+ if (pkg.installedCommandCount > 0) {
472
+ summaryParts.push(`${pkg.installedCommandCount} command${pkg.installedCommandCount === 1 ? "" : "s"}`);
473
+ }
474
+ console.log(chalk_1.default.green(`✅ ${pkg.path} (${summaryParts.join(", ")})`));
396
475
  }
397
476
  else {
398
477
  console.log(chalk_1.default.red(`❌ ${pkg.path}: ${pkg.error}`));
@@ -403,7 +482,10 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
403
482
  if (failedPackages.length > 0) {
404
483
  console.log(chalk_1.default.yellow(`Installation completed with errors`));
405
484
  if (verbose) {
406
- console.log(chalk_1.default.green(`Successfully installed: ${result.packages.length - failedPackages.length}/${result.packages.length} packages (${result.totalRuleCount} rules total)`));
485
+ const commandSummary = result.totalCommandCount > 0
486
+ ? `, ${result.totalCommandCount} command${result.totalCommandCount === 1 ? "" : "s"} total`
487
+ : "";
488
+ 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})`));
407
489
  console.log(chalk_1.default.red(`Failed packages: ${failedPackages.map((p) => p.path).join(", ")}`));
408
490
  }
409
491
  const errorDetails = failedPackages
@@ -413,12 +495,14 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
413
495
  success: false,
414
496
  error: new Error(`Package installation failed for ${failedPackages.length} package(s): ${errorDetails}`),
415
497
  installedRuleCount: result.totalRuleCount,
498
+ installedCommandCount: result.totalCommandCount,
416
499
  packagesCount: result.packages.length,
417
500
  };
418
501
  }
419
502
  return {
420
503
  success: true,
421
504
  installedRuleCount: result.totalRuleCount,
505
+ installedCommandCount: result.totalCommandCount,
422
506
  packagesCount: result.packages.length,
423
507
  };
424
508
  });
@@ -435,6 +519,7 @@ async function install(options = {}) {
435
519
  return {
436
520
  success: true,
437
521
  installedRuleCount: 0,
522
+ installedCommandCount: 0,
438
523
  packagesCount: 0,
439
524
  };
440
525
  }
@@ -464,23 +549,36 @@ async function installCommand(installOnCI, verbose, dryRun) {
464
549
  throw (_a = result.error) !== null && _a !== void 0 ? _a : new Error("Installation failed with unknown error");
465
550
  }
466
551
  else {
467
- const rulesInstalledMessage = `${result.installedRuleCount} rule${result.installedRuleCount === 1 ? "" : "s"}`;
552
+ const ruleCount = result.installedRuleCount;
553
+ const commandCount = result.installedCommandCount;
554
+ const ruleMessage = ruleCount > 0 ? `${ruleCount} rule${ruleCount === 1 ? "" : "s"}` : null;
555
+ const commandMessage = commandCount > 0
556
+ ? `${commandCount} command${commandCount === 1 ? "" : "s"}`
557
+ : null;
558
+ const countsParts = [];
559
+ if (ruleMessage) {
560
+ countsParts.push(ruleMessage);
561
+ }
562
+ if (commandMessage) {
563
+ countsParts.push(commandMessage);
564
+ }
565
+ const countsMessage = countsParts.length > 0 ? countsParts.join(" and ") : "0 rules";
468
566
  if (dryRun) {
469
567
  if (result.packagesCount > 1) {
470
- console.log(`Dry run: validated ${rulesInstalledMessage} across ${result.packagesCount} packages`);
568
+ console.log(`Dry run: validated ${countsMessage} across ${result.packagesCount} packages`);
471
569
  }
472
570
  else {
473
- console.log(`Dry run: validated ${rulesInstalledMessage}`);
571
+ console.log(`Dry run: validated ${countsMessage}`);
474
572
  }
475
573
  }
476
- else if (result.installedRuleCount === 0) {
477
- console.log("No rules installed");
574
+ else if (ruleCount === 0 && commandCount === 0) {
575
+ console.log("No rules or commands installed");
478
576
  }
479
577
  else if (result.packagesCount > 1) {
480
- console.log(`Successfully installed ${rulesInstalledMessage} across ${result.packagesCount} packages`);
578
+ console.log(`Successfully installed ${countsMessage} across ${result.packagesCount} packages`);
481
579
  }
482
580
  else {
483
- console.log(`Successfully installed ${rulesInstalledMessage}`);
581
+ console.log(`Successfully installed ${countsMessage}`);
484
582
  }
485
583
  }
486
584
  }
@@ -13,14 +13,28 @@ async function listCommand() {
13
13
  console.log(`Run ${chalk_1.default.blue("npx aicm init")} to create one.`);
14
14
  return;
15
15
  }
16
- if (!config.rules || config.rules.length === 0) {
17
- console.log(chalk_1.default.yellow("No rules defined in configuration."));
18
- console.log(`Edit your ${chalk_1.default.blue("aicm.json")} file to add rules.`);
16
+ const hasRules = config.rules && config.rules.length > 0;
17
+ const hasCommands = config.commands && config.commands.length > 0;
18
+ if (!hasRules && !hasCommands) {
19
+ console.log(chalk_1.default.yellow("No rules or commands defined in configuration."));
20
+ console.log(`Edit your ${chalk_1.default.blue("aicm.json")} file to add rules or commands.`);
19
21
  return;
20
22
  }
21
- console.log(chalk_1.default.blue("Configured Rules:"));
22
- console.log(chalk_1.default.dim("".repeat(50)));
23
- for (const rule of config.rules) {
24
- console.log(`${chalk_1.default.bold(rule.name)} - ${rule.sourcePath} ${rule.presetName ? `[${rule.presetName}]` : ""}`);
23
+ if (hasRules) {
24
+ console.log(chalk_1.default.blue("Configured Rules:"));
25
+ console.log(chalk_1.default.dim("─".repeat(50)));
26
+ for (const rule of config.rules) {
27
+ console.log(`${chalk_1.default.bold(rule.name)} - ${rule.sourcePath} ${rule.presetName ? `[${rule.presetName}]` : ""}`);
28
+ }
29
+ }
30
+ if (hasCommands) {
31
+ if (hasRules) {
32
+ console.log();
33
+ }
34
+ console.log(chalk_1.default.blue("Configured Commands:"));
35
+ console.log(chalk_1.default.dim("─".repeat(50)));
36
+ for (const command of config.commands) {
37
+ console.log(`${chalk_1.default.bold(command.name)} - ${command.sourcePath} ${command.presetName ? `[${command.presetName}]` : ""}`);
38
+ }
25
39
  }
26
40
  }
@@ -1,6 +1,7 @@
1
1
  import { CosmiconfigResult } from "cosmiconfig";
2
2
  export interface RawConfig {
3
3
  rulesDir?: string;
4
+ commandsDir?: string;
4
5
  targets?: string[];
5
6
  presets?: string[];
6
7
  overrides?: Record<string, string | false>;
@@ -10,6 +11,7 @@ export interface RawConfig {
10
11
  }
11
12
  export interface Config {
12
13
  rulesDir?: string;
14
+ commandsDir?: string;
13
15
  targets: string[];
14
16
  presets?: string[];
15
17
  overrides?: Record<string, string | false>;
@@ -31,39 +33,45 @@ export type MCPServer = {
31
33
  export interface MCPServers {
32
34
  [serverName: string]: MCPServer;
33
35
  }
34
- export interface RuleFile {
36
+ export interface ManagedFile {
35
37
  name: string;
36
38
  content: string;
37
39
  sourcePath: string;
38
40
  source: "local" | "preset";
39
41
  presetName?: string;
40
42
  }
43
+ export type RuleFile = ManagedFile;
44
+ export type CommandFile = ManagedFile;
41
45
  export interface RuleCollection {
42
46
  [target: string]: RuleFile[];
43
47
  }
44
48
  export interface ResolvedConfig {
45
49
  config: Config;
46
50
  rules: RuleFile[];
51
+ commands: CommandFile[];
47
52
  mcpServers: MCPServers;
48
53
  }
49
- export declare const ALLOWED_CONFIG_KEYS: readonly ["rulesDir", "targets", "presets", "overrides", "mcpServers", "workspaces", "skipInstall"];
50
- export declare const SUPPORTED_TARGETS: readonly ["cursor", "windsurf", "codex"];
54
+ export declare const ALLOWED_CONFIG_KEYS: readonly ["rulesDir", "commandsDir", "targets", "presets", "overrides", "mcpServers", "workspaces", "skipInstall"];
55
+ export declare const SUPPORTED_TARGETS: readonly ["cursor", "windsurf", "codex", "claude"];
51
56
  export type SupportedTarget = (typeof SUPPORTED_TARGETS)[number];
52
57
  export declare function detectWorkspacesFromPackageJson(cwd: string): boolean;
53
58
  export declare function resolveWorkspaces(config: unknown, configFilePath: string, cwd: string): boolean;
54
59
  export declare function applyDefaults(config: RawConfig, workspaces: boolean): Config;
55
60
  export declare function validateConfig(config: unknown, configFilePath: string, cwd: string, isWorkspaceMode?: boolean): asserts config is Config;
56
61
  export declare function loadRulesFromDirectory(rulesDir: string, source: "local" | "preset", presetName?: string): Promise<RuleFile[]>;
62
+ export declare function loadCommandsFromDirectory(commandsDir: string, source: "local" | "preset", presetName?: string): Promise<CommandFile[]>;
57
63
  export declare function resolvePresetPath(presetPath: string, cwd: string): string | null;
58
64
  export declare function loadPreset(presetPath: string, cwd: string): Promise<{
59
65
  config: Config;
60
- rulesDir: string;
66
+ rulesDir?: string;
67
+ commandsDir?: string;
61
68
  }>;
62
69
  export declare function loadAllRules(config: Config, cwd: string): Promise<{
63
70
  rules: RuleFile[];
71
+ commands: CommandFile[];
64
72
  mcpServers: MCPServers;
65
73
  }>;
66
- export declare function applyOverrides(rules: RuleFile[], overrides: Record<string, string | false>, cwd: string): RuleFile[];
74
+ export declare function applyOverrides<T extends ManagedFile>(files: T[], overrides: Record<string, string | false>, cwd: string): T[];
67
75
  export declare function loadConfigFile(searchFrom?: string): Promise<CosmiconfigResult>;
68
76
  export declare function loadConfig(cwd?: string): Promise<ResolvedConfig | null>;
69
77
  export declare function saveConfig(config: Config, cwd?: string): boolean;
@@ -9,6 +9,7 @@ exports.resolveWorkspaces = resolveWorkspaces;
9
9
  exports.applyDefaults = applyDefaults;
10
10
  exports.validateConfig = validateConfig;
11
11
  exports.loadRulesFromDirectory = loadRulesFromDirectory;
12
+ exports.loadCommandsFromDirectory = loadCommandsFromDirectory;
12
13
  exports.resolvePresetPath = resolvePresetPath;
13
14
  exports.loadPreset = loadPreset;
14
15
  exports.loadAllRules = loadAllRules;
@@ -22,6 +23,7 @@ const cosmiconfig_1 = require("cosmiconfig");
22
23
  const fast_glob_1 = __importDefault(require("fast-glob"));
23
24
  exports.ALLOWED_CONFIG_KEYS = [
24
25
  "rulesDir",
26
+ "commandsDir",
25
27
  "targets",
26
28
  "presets",
27
29
  "overrides",
@@ -29,7 +31,12 @@ exports.ALLOWED_CONFIG_KEYS = [
29
31
  "workspaces",
30
32
  "skipInstall",
31
33
  ];
32
- exports.SUPPORTED_TARGETS = ["cursor", "windsurf", "codex"];
34
+ exports.SUPPORTED_TARGETS = [
35
+ "cursor",
36
+ "windsurf",
37
+ "codex",
38
+ "claude",
39
+ ];
33
40
  function detectWorkspacesFromPackageJson(cwd) {
34
41
  try {
35
42
  const packageJsonPath = node_path_1.default.join(cwd, "package.json");
@@ -56,6 +63,7 @@ function resolveWorkspaces(config, configFilePath, cwd) {
56
63
  function applyDefaults(config, workspaces) {
57
64
  return {
58
65
  rulesDir: config.rulesDir,
66
+ commandsDir: config.commandsDir,
59
67
  targets: config.targets || ["cursor"],
60
68
  presets: config.presets || [],
61
69
  overrides: config.overrides || {},
@@ -74,13 +82,14 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
74
82
  }
75
83
  // Validate that either rulesDir or presets is provided
76
84
  const hasRulesDir = "rulesDir" in config && typeof config.rulesDir === "string";
85
+ const hasCommandsDir = "commandsDir" in config && typeof config.commandsDir === "string";
77
86
  const hasPresets = "presets" in config &&
78
87
  Array.isArray(config.presets) &&
79
88
  config.presets.length > 0;
80
89
  // In workspace mode, root config doesn't need rulesDir or presets
81
90
  // since packages will have their own configurations
82
- if (!isWorkspaceMode && !hasRulesDir && !hasPresets) {
83
- throw new Error(`Either rulesDir or presets must be specified in config at ${configFilePath}`);
91
+ if (!isWorkspaceMode && !hasRulesDir && !hasPresets && !hasCommandsDir) {
92
+ throw new Error(`At least one of rulesDir, commandsDir, or presets must be specified in config at ${configFilePath}`);
84
93
  }
85
94
  // Validate rulesDir if provided
86
95
  if (hasRulesDir) {
@@ -92,6 +101,15 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
92
101
  throw new Error(`Rules path is not a directory: ${rulesPath}`);
93
102
  }
94
103
  }
104
+ if (hasCommandsDir) {
105
+ const commandsPath = node_path_1.default.resolve(cwd, config.commandsDir);
106
+ if (!fs_extra_1.default.existsSync(commandsPath)) {
107
+ throw new Error(`Commands directory does not exist: ${commandsPath}`);
108
+ }
109
+ if (!fs_extra_1.default.statSync(commandsPath).isDirectory()) {
110
+ throw new Error(`Commands path is not a directory: ${commandsPath}`);
111
+ }
112
+ }
95
113
  if ("targets" in config) {
96
114
  if (!Array.isArray(config.targets)) {
97
115
  throw new Error(`targets must be an array in config at ${configFilePath}`);
@@ -132,6 +150,31 @@ async function loadRulesFromDirectory(rulesDir, source, presetName) {
132
150
  }
133
151
  return rules;
134
152
  }
153
+ async function loadCommandsFromDirectory(commandsDir, source, presetName) {
154
+ const commands = [];
155
+ if (!fs_extra_1.default.existsSync(commandsDir)) {
156
+ return commands;
157
+ }
158
+ const pattern = node_path_1.default.join(commandsDir, "**/*.md").replace(/\\/g, "/");
159
+ const filePaths = await (0, fast_glob_1.default)(pattern, {
160
+ onlyFiles: true,
161
+ absolute: true,
162
+ });
163
+ filePaths.sort();
164
+ for (const filePath of filePaths) {
165
+ const content = await fs_extra_1.default.readFile(filePath, "utf8");
166
+ const relativePath = node_path_1.default.relative(commandsDir, filePath);
167
+ const commandName = relativePath.replace(/\.md$/, "").replace(/\\/g, "/");
168
+ commands.push({
169
+ name: commandName,
170
+ content,
171
+ sourcePath: filePath,
172
+ source,
173
+ presetName,
174
+ });
175
+ }
176
+ return commands;
177
+ }
135
178
  function resolvePresetPath(presetPath, cwd) {
136
179
  // Support specifying aicm.json directory and load the config from it
137
180
  if (!presetPath.endsWith(".json")) {
@@ -168,20 +211,25 @@ async function loadPreset(presetPath, cwd) {
168
211
  catch (error) {
169
212
  throw new Error(`Failed to load preset "${presetPath}": ${error instanceof Error ? error.message : "Unknown error"}`);
170
213
  }
171
- // Validate that preset has rulesDir
172
- if (!presetConfig.rulesDir) {
173
- throw new Error(`Preset "${presetPath}" must have a rulesDir specified`);
174
- }
175
- // Resolve preset's rules directory relative to the preset file
176
214
  const presetDir = node_path_1.default.dirname(resolvedPresetPath);
177
- const presetRulesDir = node_path_1.default.resolve(presetDir, presetConfig.rulesDir);
215
+ const presetRulesDir = presetConfig.rulesDir
216
+ ? node_path_1.default.resolve(presetDir, presetConfig.rulesDir)
217
+ : undefined;
218
+ const presetCommandsDir = presetConfig.commandsDir
219
+ ? node_path_1.default.resolve(presetDir, presetConfig.commandsDir)
220
+ : undefined;
221
+ if (!presetRulesDir && !presetCommandsDir) {
222
+ throw new Error(`Preset "${presetPath}" must have a rulesDir or commandsDir specified`);
223
+ }
178
224
  return {
179
225
  config: presetConfig,
180
226
  rulesDir: presetRulesDir,
227
+ commandsDir: presetCommandsDir,
181
228
  };
182
229
  }
183
230
  async function loadAllRules(config, cwd) {
184
231
  const allRules = [];
232
+ const allCommands = [];
185
233
  let mergedMcpServers = { ...config.mcpServers };
186
234
  // Load local rules only if rulesDir is provided
187
235
  if (config.rulesDir) {
@@ -189,11 +237,22 @@ async function loadAllRules(config, cwd) {
189
237
  const localRules = await loadRulesFromDirectory(localRulesPath, "local");
190
238
  allRules.push(...localRules);
191
239
  }
240
+ if (config.commandsDir) {
241
+ const localCommandsPath = node_path_1.default.resolve(cwd, config.commandsDir);
242
+ const localCommands = await loadCommandsFromDirectory(localCommandsPath, "local");
243
+ allCommands.push(...localCommands);
244
+ }
192
245
  if (config.presets) {
193
246
  for (const presetPath of config.presets) {
194
247
  const preset = await loadPreset(presetPath, cwd);
195
- const presetRules = await loadRulesFromDirectory(preset.rulesDir, "preset", presetPath);
196
- allRules.push(...presetRules);
248
+ if (preset.rulesDir) {
249
+ const presetRules = await loadRulesFromDirectory(preset.rulesDir, "preset", presetPath);
250
+ allRules.push(...presetRules);
251
+ }
252
+ if (preset.commandsDir) {
253
+ const presetCommands = await loadCommandsFromDirectory(preset.commandsDir, "preset", presetPath);
254
+ allCommands.push(...presetCommands);
255
+ }
197
256
  // Merge MCP servers from preset
198
257
  if (preset.config.mcpServers) {
199
258
  mergedMcpServers = mergePresetMcpServers(mergedMcpServers, preset.config.mcpServers);
@@ -202,41 +261,43 @@ async function loadAllRules(config, cwd) {
202
261
  }
203
262
  return {
204
263
  rules: allRules,
264
+ commands: allCommands,
205
265
  mcpServers: mergedMcpServers,
206
266
  };
207
267
  }
208
- function applyOverrides(rules, overrides, cwd) {
209
- // Validate that all override rule names exist in the resolved rules
210
- for (const ruleName of Object.keys(overrides)) {
211
- // TODO: support better error messages with edit distance, helping the user in case of a typo
212
- // TODO: or shows a list of potential rules to override
213
- if (!rules.some((rule) => rule.name === ruleName)) {
214
- throw new Error(`Override rule "${ruleName}" does not exist in resolved rules`);
268
+ function applyOverrides(files, overrides, cwd) {
269
+ // Validate that all override names exist in the resolved files
270
+ for (const name of Object.keys(overrides)) {
271
+ if (!files.some((file) => file.name === name)) {
272
+ throw new Error(`Override entry "${name}" does not exist in resolved files`);
215
273
  }
216
274
  }
217
- const ruleMap = new Map();
218
- for (const rule of rules) {
219
- ruleMap.set(rule.name, rule);
275
+ const fileMap = new Map();
276
+ for (const file of files) {
277
+ fileMap.set(file.name, file);
220
278
  }
221
- for (const [ruleName, override] of Object.entries(overrides)) {
279
+ for (const [name, override] of Object.entries(overrides)) {
222
280
  if (override === false) {
223
- ruleMap.delete(ruleName);
281
+ fileMap.delete(name);
224
282
  }
225
283
  else if (typeof override === "string") {
226
284
  const overridePath = node_path_1.default.resolve(cwd, override);
227
285
  if (!fs_extra_1.default.existsSync(overridePath)) {
228
- throw new Error(`Override rule file not found: ${override} in ${cwd}`);
286
+ throw new Error(`Override file not found: ${override} in ${cwd}`);
229
287
  }
230
288
  const content = fs_extra_1.default.readFileSync(overridePath, "utf8");
231
- ruleMap.set(ruleName, {
232
- name: ruleName,
289
+ const existing = fileMap.get(name);
290
+ fileMap.set(name, {
291
+ ...(existing !== null && existing !== void 0 ? existing : {}),
292
+ name,
233
293
  content,
234
294
  sourcePath: overridePath,
235
295
  source: "local",
296
+ presetName: undefined,
236
297
  });
237
298
  }
238
299
  }
239
- return Array.from(ruleMap.values());
300
+ return Array.from(fileMap.values());
240
301
  }
241
302
  /**
242
303
  * Merge preset MCP servers with local config MCP servers
@@ -280,14 +341,31 @@ async function loadConfig(cwd) {
280
341
  const isWorkspaces = resolveWorkspaces(config, configResult.filepath, workingDir);
281
342
  validateConfig(config, configResult.filepath, workingDir, isWorkspaces);
282
343
  const configWithDefaults = applyDefaults(config, isWorkspaces);
283
- const { rules, mcpServers } = await loadAllRules(configWithDefaults, workingDir);
344
+ const { rules, commands, mcpServers } = await loadAllRules(configWithDefaults, workingDir);
284
345
  let rulesWithOverrides = rules;
346
+ let commandsWithOverrides = commands;
285
347
  if (configWithDefaults.overrides) {
286
- rulesWithOverrides = applyOverrides(rules, configWithDefaults.overrides, workingDir);
348
+ const overrides = configWithDefaults.overrides;
349
+ const ruleNames = new Set(rules.map((rule) => rule.name));
350
+ const commandNames = new Set(commands.map((command) => command.name));
351
+ for (const overrideName of Object.keys(overrides)) {
352
+ if (!ruleNames.has(overrideName) && !commandNames.has(overrideName)) {
353
+ throw new Error(`Override entry "${overrideName}" does not exist in resolved rules or commands`);
354
+ }
355
+ }
356
+ const ruleOverrides = Object.fromEntries(Object.entries(overrides).filter(([name]) => ruleNames.has(name)));
357
+ const commandOverrides = Object.fromEntries(Object.entries(overrides).filter(([name]) => commandNames.has(name)));
358
+ if (Object.keys(ruleOverrides).length > 0) {
359
+ rulesWithOverrides = applyOverrides(rules, ruleOverrides, workingDir);
360
+ }
361
+ if (Object.keys(commandOverrides).length > 0) {
362
+ commandsWithOverrides = applyOverrides(commands, commandOverrides, workingDir);
363
+ }
287
364
  }
288
365
  return {
289
366
  config: configWithDefaults,
290
367
  rules: rulesWithOverrides,
368
+ commands: commandsWithOverrides,
291
369
  mcpServers,
292
370
  };
293
371
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicm",
3
- "version": "0.15.0",
3
+ "version": "0.16.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",