aicm 0.16.1 → 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();
@@ -357,15 +408,17 @@ async function installPackage(options = {}) {
357
408
  error: new Error("Configuration file not found"),
358
409
  installedRuleCount: 0,
359
410
  installedCommandCount: 0,
411
+ installedAssetCount: 0,
360
412
  packagesCount: 0,
361
413
  };
362
414
  }
363
- const { config, rules, commands, mcpServers } = resolvedConfig;
415
+ const { config, rules, commands, assets, mcpServers } = resolvedConfig;
364
416
  if (config.skipInstall === true) {
365
417
  return {
366
418
  success: true,
367
419
  installedRuleCount: 0,
368
420
  installedCommandCount: 0,
421
+ installedAssetCount: 0,
369
422
  packagesCount: 0,
370
423
  };
371
424
  }
@@ -373,7 +426,7 @@ async function installPackage(options = {}) {
373
426
  const commandsToInstall = dedupeCommandsForInstall(commands);
374
427
  try {
375
428
  if (!options.dryRun) {
376
- writeRulesToTargets(rules, config.targets);
429
+ writeRulesToTargets(rules, assets, config.targets);
377
430
  writeCommandsToTargets(commandsToInstall, config.targets);
378
431
  if (mcpServers && Object.keys(mcpServers).length > 0) {
379
432
  writeMcpServersToTargets(mcpServers, config.targets, cwd);
@@ -385,6 +438,7 @@ async function installPackage(options = {}) {
385
438
  success: true,
386
439
  installedRuleCount: uniqueRuleCount,
387
440
  installedCommandCount: uniqueCommandCount,
441
+ installedAssetCount: assets.length,
388
442
  packagesCount: 1,
389
443
  };
390
444
  }
@@ -394,6 +448,7 @@ async function installPackage(options = {}) {
394
448
  error: error instanceof Error ? error : new Error(String(error)),
395
449
  installedRuleCount: 0,
396
450
  installedCommandCount: 0,
451
+ installedAssetCount: 0,
397
452
  packagesCount: 0,
398
453
  };
399
454
  }
@@ -406,6 +461,7 @@ async function installWorkspacesPackages(packages, options = {}) {
406
461
  const results = [];
407
462
  let totalRuleCount = 0;
408
463
  let totalCommandCount = 0;
464
+ let totalAssetCount = 0;
409
465
  // Install packages sequentially for now (can be parallelized later)
410
466
  for (const pkg of packages) {
411
467
  const packagePath = pkg.absolutePath;
@@ -417,12 +473,14 @@ async function installWorkspacesPackages(packages, options = {}) {
417
473
  });
418
474
  totalRuleCount += result.installedRuleCount;
419
475
  totalCommandCount += result.installedCommandCount;
476
+ totalAssetCount += result.installedAssetCount;
420
477
  results.push({
421
478
  path: pkg.relativePath,
422
479
  success: result.success,
423
480
  error: result.error,
424
481
  installedRuleCount: result.installedRuleCount,
425
482
  installedCommandCount: result.installedCommandCount,
483
+ installedAssetCount: result.installedAssetCount,
426
484
  });
427
485
  }
428
486
  catch (error) {
@@ -432,6 +490,7 @@ async function installWorkspacesPackages(packages, options = {}) {
432
490
  error: error instanceof Error ? error : new Error(String(error)),
433
491
  installedRuleCount: 0,
434
492
  installedCommandCount: 0,
493
+ installedAssetCount: 0,
435
494
  });
436
495
  }
437
496
  }
@@ -441,6 +500,7 @@ async function installWorkspacesPackages(packages, options = {}) {
441
500
  packages: results,
442
501
  totalRuleCount,
443
502
  totalCommandCount,
503
+ totalAssetCount,
444
504
  };
445
505
  }
446
506
  /**
@@ -471,6 +531,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
471
531
  error: new Error("No packages with aicm configurations found"),
472
532
  installedRuleCount: 0,
473
533
  installedCommandCount: 0,
534
+ installedAssetCount: 0,
474
535
  packagesCount: 0,
475
536
  };
476
537
  }
@@ -538,6 +599,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
538
599
  error: new Error(`Package installation failed for ${failedPackages.length} package(s): ${errorDetails}`),
539
600
  installedRuleCount: result.totalRuleCount,
540
601
  installedCommandCount: result.totalCommandCount,
602
+ installedAssetCount: result.totalAssetCount,
541
603
  packagesCount: result.packages.length,
542
604
  };
543
605
  }
@@ -545,6 +607,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
545
607
  success: true,
546
608
  installedRuleCount: result.totalRuleCount,
547
609
  installedCommandCount: result.totalCommandCount,
610
+ installedAssetCount: result.totalAssetCount,
548
611
  packagesCount: result.packages.length,
549
612
  };
550
613
  });
@@ -562,6 +625,7 @@ async function install(options = {}) {
562
625
  success: true,
563
626
  installedRuleCount: 0,
564
627
  installedCommandCount: 0,
628
+ installedAssetCount: 0,
565
629
  packagesCount: 0,
566
630
  };
567
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.1",
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",