aicm 0.16.0 → 0.17.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
@@ -95,6 +95,32 @@ For project-specific rules, you can specify `rulesDir` in your `aicm.json` confi
95
95
  }
96
96
  ```
97
97
 
98
+ **Referencing Auxiliary Files**
99
+
100
+ You can place any file (e.g., `example.ts`, `schema.json`, `guide.md`) in your `rulesDir` alongside your `.mdc` files. These assets are automatically copied to the target location. You can reference them in your rules using relative paths, and aicm will automatically rewrite the links to point to the correct location for each target IDE.
101
+
102
+ Example `rules/my-rule.mdc`:
103
+
104
+ ```markdown
105
+ # My Rule
106
+
107
+ See [Example](./example.ts) for details.
108
+ ```
109
+
110
+ **Commands referencing files**
111
+
112
+ You can also use this feature to create commands that reference auxiliary files in your `rulesDir`. Since assets in `rulesDir` are copied to the target directory, your commands can link to them.
113
+
114
+ For example, if you have a schema file at `rules/schema.json` and a command at `commands/generate-schema.md`:
115
+
116
+ ```markdown
117
+ # Generate Schema
118
+
119
+ Use the schema defined in [Schema Template](../rules/schema.json) to generate the response.
120
+ ```
121
+
122
+ When installed, `aicm` will automatically rewrite the link to point to the correct location of `schema.json` in the target environment (e.g., `../../rules/aicm/schema.json` for Cursor).
123
+
98
124
  ### Using Commands
99
125
 
100
126
  Cursor supports custom commands that can be invoked directly in the chat interface. aicm can manage these command files
@@ -41,6 +41,10 @@ export interface InstallResult {
41
41
  * Number of commands installed
42
42
  */
43
43
  installedCommandCount: number;
44
+ /**
45
+ * Number of assets installed
46
+ */
47
+ installedAssetCount: number;
44
48
  /**
45
49
  * Number of packages installed
46
50
  */
@@ -18,9 +18,7 @@ function getTargetPaths() {
18
18
  const projectDir = process.cwd();
19
19
  return {
20
20
  cursor: node_path_1.default.join(projectDir, ".cursor", "rules", "aicm"),
21
- windsurf: node_path_1.default.join(projectDir, ".aicm"),
22
- codex: node_path_1.default.join(projectDir, ".aicm"),
23
- claude: node_path_1.default.join(projectDir, ".aicm"),
21
+ aicm: node_path_1.default.join(projectDir, ".aicm"),
24
22
  };
25
23
  }
26
24
  function writeCursorRules(rules, cursorRulesDir) {
@@ -53,9 +51,29 @@ function writeCursorCommands(commands, cursorCommandsDir) {
53
51
  const commandPath = node_path_1.default.join(cursorCommandsDir, ...commandNameParts);
54
52
  const commandFile = commandPath + ".md";
55
53
  fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(commandFile));
56
- fs_extra_1.default.writeFileSync(commandFile, command.content);
54
+ // If the command file references assets in the rules directory, we need to rewrite the links.
55
+ // Commands are installed in .cursor/commands/aicm/
56
+ // Rules/assets are installed in .cursor/rules/aicm/
57
+ // So a link like "../rules/asset.json" in source (from commands/ to rules/)
58
+ // needs to become "../../rules/aicm/asset.json" in target (from .cursor/commands/aicm/ to .cursor/rules/aicm/)
59
+ const content = rewriteCommandRelativeLinks(command.content);
60
+ fs_extra_1.default.writeFileSync(commandFile, content);
57
61
  }
58
62
  }
63
+ function rewriteCommandRelativeLinks(content) {
64
+ return content.replace(/\[([^\]]*)\]\(([^)]+)\)/g, (match, text, url) => {
65
+ if (url.startsWith("http") || url.startsWith("#") || url.startsWith("/")) {
66
+ return match;
67
+ }
68
+ // Check if it's a link to the rules directory
69
+ if (url.includes("rules/")) {
70
+ const parts = url.split("/");
71
+ const filename = parts[parts.length - 1];
72
+ return `[${text}](../../rules/aicm/${filename})`;
73
+ }
74
+ return match;
75
+ });
76
+ }
59
77
  function extractNamespaceFromPresetPath(presetPath) {
60
78
  // Special case: npm package names always use forward slashes, regardless of platform
61
79
  if (presetPath.startsWith("@")) {
@@ -68,7 +86,7 @@ function extractNamespaceFromPresetPath(presetPath) {
68
86
  /**
69
87
  * Write rules to a shared directory and update the given rules file
70
88
  */
71
- function writeRulesForFile(rules, ruleDir, rulesFile) {
89
+ function writeRulesForFile(rules, assets, ruleDir, rulesFile) {
72
90
  fs_extra_1.default.emptyDirSync(ruleDir);
73
91
  const ruleFiles = rules.map((rule) => {
74
92
  let rulePath;
@@ -83,9 +101,10 @@ function writeRulesForFile(rules, ruleDir, rulesFile) {
83
101
  // For local rules, maintain the original flat structure
84
102
  rulePath = node_path_1.default.join(ruleDir, ...ruleNameParts);
85
103
  }
104
+ const content = rule.content;
86
105
  const physicalRulePath = rulePath + ".md";
87
106
  fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(physicalRulePath));
88
- fs_extra_1.default.writeFileSync(physicalRulePath, rule.content);
107
+ fs_extra_1.default.writeFileSync(physicalRulePath, content);
89
108
  const relativeRuleDir = node_path_1.default.basename(ruleDir);
90
109
  // For the rules file, maintain the same structure
91
110
  let windsurfPath;
@@ -102,16 +121,46 @@ function writeRulesForFile(rules, ruleDir, rulesFile) {
102
121
  return {
103
122
  name: rule.name,
104
123
  path: windsurfPathPosix,
105
- metadata: (0, rules_file_writer_1.parseRuleFrontmatter)(rule.content),
124
+ metadata: (0, rules_file_writer_1.parseRuleFrontmatter)(content),
106
125
  };
107
126
  });
108
127
  const rulesContent = (0, rules_file_writer_1.generateRulesFileContent)(ruleFiles);
109
128
  (0, rules_file_writer_1.writeRulesFile)(rulesContent, node_path_1.default.join(process.cwd(), rulesFile));
110
129
  }
130
+ function writeAssetsToTargets(assets, targets) {
131
+ const targetPaths = getTargetPaths();
132
+ for (const target of targets) {
133
+ let targetDir;
134
+ switch (target) {
135
+ case "cursor":
136
+ targetDir = targetPaths.cursor;
137
+ break;
138
+ case "windsurf":
139
+ case "codex":
140
+ case "claude":
141
+ targetDir = targetPaths.aicm;
142
+ break;
143
+ default:
144
+ continue;
145
+ }
146
+ for (const asset of assets) {
147
+ let assetPath;
148
+ if (asset.presetName) {
149
+ const namespace = extractNamespaceFromPresetPath(asset.presetName);
150
+ assetPath = node_path_1.default.join(targetDir, ...namespace, asset.name);
151
+ }
152
+ else {
153
+ assetPath = node_path_1.default.join(targetDir, asset.name);
154
+ }
155
+ fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(assetPath));
156
+ fs_extra_1.default.writeFileSync(assetPath, asset.content);
157
+ }
158
+ }
159
+ }
111
160
  /**
112
161
  * Write all collected rules to their respective IDE targets
113
162
  */
114
- function writeRulesToTargets(rules, targets) {
163
+ function writeRulesToTargets(rules, assets, targets) {
115
164
  const targetPaths = getTargetPaths();
116
165
  for (const target of targets) {
117
166
  switch (target) {
@@ -122,21 +171,23 @@ function writeRulesToTargets(rules, targets) {
122
171
  break;
123
172
  case "windsurf":
124
173
  if (rules.length > 0) {
125
- writeRulesForFile(rules, targetPaths.windsurf, ".windsurfrules");
174
+ writeRulesForFile(rules, assets, targetPaths.aicm, ".windsurfrules");
126
175
  }
127
176
  break;
128
177
  case "codex":
129
178
  if (rules.length > 0) {
130
- writeRulesForFile(rules, targetPaths.codex, "AGENTS.md");
179
+ writeRulesForFile(rules, assets, targetPaths.aicm, "AGENTS.md");
131
180
  }
132
181
  break;
133
182
  case "claude":
134
183
  if (rules.length > 0) {
135
- writeRulesForFile(rules, targetPaths.claude, "CLAUDE.md");
184
+ writeRulesForFile(rules, assets, targetPaths.aicm, "CLAUDE.md");
136
185
  }
137
186
  break;
138
187
  }
139
188
  }
189
+ // Write assets after rules so they don't get wiped by emptyDirSync
190
+ writeAssetsToTargets(assets, targets);
140
191
  }
141
192
  function writeCommandsToTargets(commands, targets) {
142
193
  const projectDir = process.cwd();
@@ -180,6 +231,37 @@ function dedupeCommandsForInstall(commands) {
180
231
  }
181
232
  return Array.from(unique.values());
182
233
  }
234
+ function mergeWorkspaceCommands(packages) {
235
+ var _a;
236
+ const commands = [];
237
+ const seenPresetCommands = new Set();
238
+ for (const pkg of packages) {
239
+ const hasCursorTarget = pkg.config.config.targets.includes("cursor");
240
+ if (!hasCursorTarget) {
241
+ continue;
242
+ }
243
+ for (const command of (_a = pkg.config.commands) !== null && _a !== void 0 ? _a : []) {
244
+ if (command.presetName) {
245
+ const presetKey = `${command.presetName}::${command.name}`;
246
+ if (seenPresetCommands.has(presetKey)) {
247
+ continue;
248
+ }
249
+ seenPresetCommands.add(presetKey);
250
+ }
251
+ commands.push(command);
252
+ }
253
+ }
254
+ return commands;
255
+ }
256
+ function collectWorkspaceCommandTargets(packages) {
257
+ const targets = new Set();
258
+ for (const pkg of packages) {
259
+ if (pkg.config.config.targets.includes("cursor")) {
260
+ targets.add("cursor");
261
+ }
262
+ }
263
+ return Array.from(targets);
264
+ }
183
265
  /**
184
266
  * Write MCP servers configuration to IDE targets
185
267
  */
@@ -326,15 +408,17 @@ async function installPackage(options = {}) {
326
408
  error: new Error("Configuration file not found"),
327
409
  installedRuleCount: 0,
328
410
  installedCommandCount: 0,
411
+ installedAssetCount: 0,
329
412
  packagesCount: 0,
330
413
  };
331
414
  }
332
- const { config, rules, commands, mcpServers } = resolvedConfig;
415
+ const { config, rules, commands, assets, mcpServers } = resolvedConfig;
333
416
  if (config.skipInstall === true) {
334
417
  return {
335
418
  success: true,
336
419
  installedRuleCount: 0,
337
420
  installedCommandCount: 0,
421
+ installedAssetCount: 0,
338
422
  packagesCount: 0,
339
423
  };
340
424
  }
@@ -342,7 +426,7 @@ async function installPackage(options = {}) {
342
426
  const commandsToInstall = dedupeCommandsForInstall(commands);
343
427
  try {
344
428
  if (!options.dryRun) {
345
- writeRulesToTargets(rules, config.targets);
429
+ writeRulesToTargets(rules, assets, config.targets);
346
430
  writeCommandsToTargets(commandsToInstall, config.targets);
347
431
  if (mcpServers && Object.keys(mcpServers).length > 0) {
348
432
  writeMcpServersToTargets(mcpServers, config.targets, cwd);
@@ -354,6 +438,7 @@ async function installPackage(options = {}) {
354
438
  success: true,
355
439
  installedRuleCount: uniqueRuleCount,
356
440
  installedCommandCount: uniqueCommandCount,
441
+ installedAssetCount: assets.length,
357
442
  packagesCount: 1,
358
443
  };
359
444
  }
@@ -363,6 +448,7 @@ async function installPackage(options = {}) {
363
448
  error: error instanceof Error ? error : new Error(String(error)),
364
449
  installedRuleCount: 0,
365
450
  installedCommandCount: 0,
451
+ installedAssetCount: 0,
366
452
  packagesCount: 0,
367
453
  };
368
454
  }
@@ -375,6 +461,7 @@ async function installWorkspacesPackages(packages, options = {}) {
375
461
  const results = [];
376
462
  let totalRuleCount = 0;
377
463
  let totalCommandCount = 0;
464
+ let totalAssetCount = 0;
378
465
  // Install packages sequentially for now (can be parallelized later)
379
466
  for (const pkg of packages) {
380
467
  const packagePath = pkg.absolutePath;
@@ -386,12 +473,14 @@ async function installWorkspacesPackages(packages, options = {}) {
386
473
  });
387
474
  totalRuleCount += result.installedRuleCount;
388
475
  totalCommandCount += result.installedCommandCount;
476
+ totalAssetCount += result.installedAssetCount;
389
477
  results.push({
390
478
  path: pkg.relativePath,
391
479
  success: result.success,
392
480
  error: result.error,
393
481
  installedRuleCount: result.installedRuleCount,
394
482
  installedCommandCount: result.installedCommandCount,
483
+ installedAssetCount: result.installedAssetCount,
395
484
  });
396
485
  }
397
486
  catch (error) {
@@ -401,6 +490,7 @@ async function installWorkspacesPackages(packages, options = {}) {
401
490
  error: error instanceof Error ? error : new Error(String(error)),
402
491
  installedRuleCount: 0,
403
492
  installedCommandCount: 0,
493
+ installedAssetCount: 0,
404
494
  });
405
495
  }
406
496
  }
@@ -410,6 +500,7 @@ async function installWorkspacesPackages(packages, options = {}) {
410
500
  packages: results,
411
501
  totalRuleCount,
412
502
  totalCommandCount,
503
+ totalAssetCount,
413
504
  };
414
505
  }
415
506
  /**
@@ -440,6 +531,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
440
531
  error: new Error("No packages with aicm configurations found"),
441
532
  installedRuleCount: 0,
442
533
  installedCommandCount: 0,
534
+ installedAssetCount: 0,
443
535
  packagesCount: 0,
444
536
  };
445
537
  }
@@ -455,6 +547,17 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
455
547
  verbose,
456
548
  dryRun,
457
549
  });
550
+ const workspaceCommands = mergeWorkspaceCommands(packages);
551
+ const workspaceCommandTargets = collectWorkspaceCommandTargets(packages);
552
+ if (workspaceCommands.length > 0) {
553
+ warnPresetCommandCollisions(workspaceCommands);
554
+ }
555
+ if (!dryRun &&
556
+ workspaceCommands.length > 0 &&
557
+ workspaceCommandTargets.length > 0) {
558
+ const dedupedWorkspaceCommands = dedupeCommandsForInstall(workspaceCommands);
559
+ writeCommandsToTargets(dedupedWorkspaceCommands, workspaceCommandTargets);
560
+ }
458
561
  const { merged: rootMcp, conflicts } = mergeWorkspaceMcpServers(packages);
459
562
  const hasCursorTarget = packages.some((p) => p.config.config.targets.includes("cursor"));
460
563
  if (!dryRun && hasCursorTarget && Object.keys(rootMcp).length > 0) {
@@ -496,6 +599,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
496
599
  error: new Error(`Package installation failed for ${failedPackages.length} package(s): ${errorDetails}`),
497
600
  installedRuleCount: result.totalRuleCount,
498
601
  installedCommandCount: result.totalCommandCount,
602
+ installedAssetCount: result.totalAssetCount,
499
603
  packagesCount: result.packages.length,
500
604
  };
501
605
  }
@@ -503,6 +607,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
503
607
  success: true,
504
608
  installedRuleCount: result.totalRuleCount,
505
609
  installedCommandCount: result.totalCommandCount,
610
+ installedAssetCount: result.totalAssetCount,
506
611
  packagesCount: result.packages.length,
507
612
  };
508
613
  });
@@ -520,6 +625,7 @@ async function install(options = {}) {
520
625
  success: true,
521
626
  installedRuleCount: 0,
522
627
  installedCommandCount: 0,
628
+ installedAssetCount: 0,
523
629
  packagesCount: 0,
524
630
  };
525
631
  }
@@ -40,6 +40,13 @@ export interface ManagedFile {
40
40
  source: "local" | "preset";
41
41
  presetName?: string;
42
42
  }
43
+ export interface AssetFile {
44
+ name: string;
45
+ content: Buffer;
46
+ sourcePath: string;
47
+ source: "local" | "preset";
48
+ presetName?: string;
49
+ }
43
50
  export type RuleFile = ManagedFile;
44
51
  export type CommandFile = ManagedFile;
45
52
  export interface RuleCollection {
@@ -49,6 +56,7 @@ export interface ResolvedConfig {
49
56
  config: Config;
50
57
  rules: RuleFile[];
51
58
  commands: CommandFile[];
59
+ assets: AssetFile[];
52
60
  mcpServers: MCPServers;
53
61
  }
54
62
  export declare const ALLOWED_CONFIG_KEYS: readonly ["rulesDir", "commandsDir", "targets", "presets", "overrides", "mcpServers", "workspaces", "skipInstall"];
@@ -60,6 +68,7 @@ export declare function applyDefaults(config: RawConfig, workspaces: boolean): C
60
68
  export declare function validateConfig(config: unknown, configFilePath: string, cwd: string, isWorkspaceMode?: boolean): asserts config is Config;
61
69
  export declare function loadRulesFromDirectory(rulesDir: string, source: "local" | "preset", presetName?: string): Promise<RuleFile[]>;
62
70
  export declare function loadCommandsFromDirectory(commandsDir: string, source: "local" | "preset", presetName?: string): Promise<CommandFile[]>;
71
+ export declare function loadAssetsFromDirectory(rulesDir: string, source: "local" | "preset", presetName?: string): Promise<AssetFile[]>;
63
72
  export declare function resolvePresetPath(presetPath: string, cwd: string): string | null;
64
73
  export declare function loadPreset(presetPath: string, cwd: string): Promise<{
65
74
  config: Config;
@@ -69,6 +78,7 @@ export declare function loadPreset(presetPath: string, cwd: string): Promise<{
69
78
  export declare function loadAllRules(config: Config, cwd: string): Promise<{
70
79
  rules: RuleFile[];
71
80
  commands: CommandFile[];
81
+ assets: AssetFile[];
72
82
  mcpServers: MCPServers;
73
83
  }>;
74
84
  export declare function applyOverrides<T extends ManagedFile>(files: T[], overrides: Record<string, string | false>, cwd: string): T[];
@@ -10,6 +10,7 @@ exports.applyDefaults = applyDefaults;
10
10
  exports.validateConfig = validateConfig;
11
11
  exports.loadRulesFromDirectory = loadRulesFromDirectory;
12
12
  exports.loadCommandsFromDirectory = loadCommandsFromDirectory;
13
+ exports.loadAssetsFromDirectory = loadAssetsFromDirectory;
13
14
  exports.resolvePresetPath = resolvePresetPath;
14
15
  exports.loadPreset = loadPreset;
15
16
  exports.loadAllRules = loadAllRules;
@@ -175,6 +176,34 @@ async function loadCommandsFromDirectory(commandsDir, source, presetName) {
175
176
  }
176
177
  return commands;
177
178
  }
179
+ async function loadAssetsFromDirectory(rulesDir, source, presetName) {
180
+ const assets = [];
181
+ if (!fs_extra_1.default.existsSync(rulesDir)) {
182
+ return assets;
183
+ }
184
+ // Find all files except .mdc files and hidden files
185
+ const pattern = node_path_1.default.join(rulesDir, "**/*").replace(/\\/g, "/");
186
+ const filePaths = await (0, fast_glob_1.default)(pattern, {
187
+ onlyFiles: true,
188
+ absolute: true,
189
+ ignore: ["**/*.mdc", "**/.*"],
190
+ });
191
+ for (const filePath of filePaths) {
192
+ const content = await fs_extra_1.default.readFile(filePath);
193
+ // Preserve directory structure by using relative path from rulesDir
194
+ const relativePath = node_path_1.default.relative(rulesDir, filePath);
195
+ // Keep extension for assets
196
+ const assetName = relativePath.replace(/\\/g, "/");
197
+ assets.push({
198
+ name: assetName,
199
+ content,
200
+ sourcePath: filePath,
201
+ source,
202
+ presetName,
203
+ });
204
+ }
205
+ return assets;
206
+ }
178
207
  function resolvePresetPath(presetPath, cwd) {
179
208
  // Support specifying aicm.json directory and load the config from it
180
209
  if (!presetPath.endsWith(".json")) {
@@ -230,12 +259,15 @@ async function loadPreset(presetPath, cwd) {
230
259
  async function loadAllRules(config, cwd) {
231
260
  const allRules = [];
232
261
  const allCommands = [];
262
+ const allAssets = [];
233
263
  let mergedMcpServers = { ...config.mcpServers };
234
264
  // Load local rules only if rulesDir is provided
235
265
  if (config.rulesDir) {
236
266
  const localRulesPath = node_path_1.default.resolve(cwd, config.rulesDir);
237
267
  const localRules = await loadRulesFromDirectory(localRulesPath, "local");
268
+ const localAssets = await loadAssetsFromDirectory(localRulesPath, "local");
238
269
  allRules.push(...localRules);
270
+ allAssets.push(...localAssets);
239
271
  }
240
272
  if (config.commandsDir) {
241
273
  const localCommandsPath = node_path_1.default.resolve(cwd, config.commandsDir);
@@ -247,7 +279,9 @@ async function loadAllRules(config, cwd) {
247
279
  const preset = await loadPreset(presetPath, cwd);
248
280
  if (preset.rulesDir) {
249
281
  const presetRules = await loadRulesFromDirectory(preset.rulesDir, "preset", presetPath);
282
+ const presetAssets = await loadAssetsFromDirectory(preset.rulesDir, "preset", presetPath);
250
283
  allRules.push(...presetRules);
284
+ allAssets.push(...presetAssets);
251
285
  }
252
286
  if (preset.commandsDir) {
253
287
  const presetCommands = await loadCommandsFromDirectory(preset.commandsDir, "preset", presetPath);
@@ -262,6 +296,7 @@ async function loadAllRules(config, cwd) {
262
296
  return {
263
297
  rules: allRules,
264
298
  commands: allCommands,
299
+ assets: allAssets,
265
300
  mcpServers: mergedMcpServers,
266
301
  };
267
302
  }
@@ -341,9 +376,10 @@ async function loadConfig(cwd) {
341
376
  const isWorkspaces = resolveWorkspaces(config, configResult.filepath, workingDir);
342
377
  validateConfig(config, configResult.filepath, workingDir, isWorkspaces);
343
378
  const configWithDefaults = applyDefaults(config, isWorkspaces);
344
- const { rules, commands, mcpServers } = await loadAllRules(configWithDefaults, workingDir);
379
+ const { rules, commands, assets, mcpServers } = await loadAllRules(configWithDefaults, workingDir);
345
380
  let rulesWithOverrides = rules;
346
381
  let commandsWithOverrides = commands;
382
+ // Note: Assets are not currently supported in overrides as they are binary/varied files
347
383
  if (configWithDefaults.overrides) {
348
384
  const overrides = configWithDefaults.overrides;
349
385
  const ruleNames = new Set(rules.map((rule) => rule.name));
@@ -366,6 +402,7 @@ async function loadConfig(cwd) {
366
402
  config: configWithDefaults,
367
403
  rules: rulesWithOverrides,
368
404
  commands: commandsWithOverrides,
405
+ assets,
369
406
  mcpServers,
370
407
  };
371
408
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicm",
3
- "version": "0.16.0",
3
+ "version": "0.17.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",