aicm 0.16.1 → 0.17.1

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
@@ -2,56 +2,76 @@
2
2
 
3
3
  > AI Configuration Manager
4
4
 
5
- A CLI tool for managing Agentic configurations across projects
5
+ A CLI tool for managing Agentic configurations across projects.
6
6
 
7
7
  ![aicm](https://github.com/user-attachments/assets/ca38f2d6-ece6-43ad-a127-6f4fce8b2a5a)
8
8
 
9
+ ## Table of Contents
10
+
11
+ - [Why](#why)
12
+ - [Supported Environments](#supported-environments)
13
+ - [Getting Started](#getting-started)
14
+ - [Creating a Preset](#creating-a-preset)
15
+ - [Using a Preset](#using-a-preset)
16
+ - [Features](#features)
17
+ - [Rules](#using-rules)
18
+ - [Commands](#using-commands)
19
+ - [MCP Servers](#mcp-servers)
20
+ - [Auxiliary Files](#referencing-auxiliary-files)
21
+ - [Overrides](#overrides)
22
+ - [Workspaces Support](#workspaces-support)
23
+ - [Configuration](#configuration)
24
+ - [CLI Commands](#cli-commands)
25
+ - [Node.js API](#nodejs-api)
26
+
9
27
  ## Why
10
28
 
11
- Modern AI-powered IDEs like Cursor and Agents like Codex enable developers to write custom instructions to maintain context across coding sessions. They also support MCPs for enhanced functionality. However, sharing these configurations across multiple projects is a challenge.
29
+ Modern AI-powered IDEs like Cursor and Agents like Codex allow developers to add custom instructions, commands, and MCP servers. However, keeping these configurations consistent across a team or multiple projects is a challenge.
12
30
 
13
- **aicm** solves this by enabling you to create reusable presets that bundle rules and MCP configurations together. With multi-target support, you can write your rules once and deploy them consistently across different AI tools and IDEs.
31
+ **aicm** enables **"Write Once, Use Everywhere"** for your AI configurations.
14
32
 
15
- ## How it works
33
+ - **Team Consistency:** Ensure every developer on your team uses the same rules and best practices.
34
+ - **Reusable Presets:** Bundle your rules, commands & MCP configurations into npm packages (e.g., `@company/ai-preset`) to share them across your organization.
35
+ - **Multi-Target Support:** Write rules once in the comprehensive `.mdc` format, and automatically deploy them to Cursor, Windsurf, Codex, and Claude.
16
36
 
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:
37
+ ## Supported Environments
18
38
 
19
- - **Cursor**: Native `.mdc` files with full feature support
20
- - **Windsurf**: Generates `.windsurfrules` file
21
- - **Codex**: Generates `AGENTS.md` file
22
- - **Claude**: Generates `CLAUDE.md` file
39
+ aicm acts as a bridge between your configuration and your AI tools. It accepts Cursor's `.mdc` format and can transform it for other environments:
23
40
 
24
- This approach ensures you write your rules once in the richest format available, while maintaining compatibility across different AI development environments.
41
+ | Target | Installation |
42
+ | ------------ | ------------------------------------------------------------------------------ |
43
+ | **Cursor** | Copies `.mdc` files to `.cursor/rules/aicm/` and configures `.cursor/mcp.json` |
44
+ | **Windsurf** | Generates a `.windsurfrules` file that links to rules in `.aicm/` |
45
+ | **Codex** | Generates an `AGENTS.md` file that references rules in `.aicm/` |
46
+ | **Claude** | Generates a `CLAUDE.md` file that references rules in `.aicm/` |
25
47
 
26
48
  ## Getting Started
27
49
 
28
50
  The easiest way to get started with aicm is by using **presets** - npm packages containing rules and MCP configurations that you can install in any project.
29
51
 
30
- ### Using a preset
52
+ ### Demo
31
53
 
32
- 1. **Install a preset npm package**:
54
+ We'll install [an npm package](https://github.com/ranyitz/pirate-coding) containing a simple "Pirate Coding" preset to demonstrate how aicm works.
55
+
56
+ 1. **Install the demo preset package**:
33
57
 
34
58
  ```bash
35
- npm install --save-dev @team/ai-preset
59
+ npm install --save-dev pirate-coding
36
60
  ```
37
61
 
38
- 2. **Create an `aicm.json` file** in your project root:
62
+ 2. **Create an `aicm.json` file** in your project:
39
63
 
40
- ```json
41
- { "presets": ["@team/ai-preset"] }
64
+ ```bash
65
+ echo '{ "presets": ["pirate-coding"] }' > aicm.json
42
66
  ```
43
67
 
44
- 3. **Add a prepare script** to your `package.json` to install all preset rules and MCPs:
68
+ 3. **Install all rules & MCPs from your configuration**:
45
69
 
46
- ```json
47
- {
48
- "scripts": {
49
- "prepare": "npx aicm -y install"
50
- }
51
- }
70
+ ```bash
71
+ npx aicm install
52
72
  ```
53
73
 
54
- The rules are now installed in `.cursor/rules/aicm/` and any MCP servers are configured in `.cursor/mcp.json`.
74
+ After installation, open Cursor and ask it to do something. Your AI assistant will respond with pirate-themed coding advice.
55
75
 
56
76
  ### Creating a Preset
57
77
 
@@ -85,111 +105,130 @@ The rules are now installed in `.cursor/rules/aicm/` and any MCP servers are con
85
105
 
86
106
  > **Note:** This is syntactic sugar for `@team/ai-preset/aicm.json`.
87
107
 
88
- ### Using Local Rules
108
+ ### Using a Preset
89
109
 
90
- For project-specific rules, you can specify `rulesDir` in your `aicm.json` config. This approach allows you to write rules once and automatically generate them for all configured targets.
110
+ To use a real preset in your production project:
91
111
 
92
- ```json
93
- {
94
- "rulesDir": "path/to/rules/dir"
95
- }
96
- ```
112
+ 1. **Install a preset npm package**:
97
113
 
98
- ### Using Commands
114
+ ```bash
115
+ npm install --save-dev @team/ai-preset
116
+ ```
99
117
 
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.
118
+ 2. **Create an `aicm.json` file** in your project root:
102
119
 
103
- #### Local Commands
120
+ ```json
121
+ { "presets": ["@team/ai-preset"] }
122
+ ```
104
123
 
105
- Add a commands directory to your project configuration:
124
+ 3. **Add a prepare script** to your `package.json` to ensure rules are always up to date:
106
125
 
107
126
  ```json
108
127
  {
109
- "commandsDir": "./commands",
110
- "targets": ["cursor"]
128
+ "scripts": {
129
+ "prepare": "npx aicm -y install"
130
+ }
111
131
  }
112
132
  ```
113
133
 
114
- Command files ending in `.md` are installed to `.cursor/commands/aicm/` and appear in Cursor under the `/` command menu.
134
+ The rules are now installed in `.cursor/rules/aicm/` and any MCP servers are configured in `.cursor/mcp.json`.
135
+
136
+ ### Notes
137
+
138
+ - Generated rules are always placed in a subdirectory for deterministic cleanup and easy gitignore.
139
+ - Users should add `.cursor/rules/aicm/` and `.aicm/` (for Windsurf/Codex) to `.gitignore` to avoid tracking generated rules.
140
+
141
+ ## Features
142
+
143
+ ### Using Rules
115
144
 
116
- #### Commands in Presets
145
+ aicm uses Cursor's `.mdc` files for rules. Read more about the format [here](https://cursor.com/docs/context/rules).
117
146
 
118
- Presets can ship reusable command libraries in addition to rules:
147
+ Add a rules directory to your project configuration:
119
148
 
120
149
  ```json
121
150
  {
122
- "rulesDir": "rules",
123
- "commandsDir": "commands"
151
+ "rulesDir": "./rules",
152
+ "targets": ["cursor"]
124
153
  }
125
154
  ```
126
155
 
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.
156
+ Rules are installed in `.cursor/rules/aicm/` and are loaded automatically by Cursor.
130
157
 
131
- #### Command Overrides
158
+ ### Using Commands
159
+
160
+ Cursor supports custom commands that can be invoked directly in the chat interface. aicm can manage these command files alongside your rules and MCP configurations.
132
161
 
133
- Use the existing `overrides` field to customize or disable commands provided by presets:
162
+ Add a commands directory to your project configuration:
134
163
 
135
164
  ```json
136
165
  {
137
- "presets": ["@team/dev-preset"],
138
- "overrides": {
139
- "legacy-command": false,
140
- "custom-test": "./commands/test.md"
141
- }
166
+ "commandsDir": "./commands",
167
+ "targets": ["cursor"]
142
168
  }
143
169
  ```
144
170
 
145
- ### Notes
146
-
147
- - Generated rules are always placed in a subdirectory for deterministic cleanup and easy gitignore.
148
- - Users may add `.cursor/rules/aicm/` and `.aicm/` (for Windsurf/Codex) to `.gitignore` if they do not want to track generated rules.
171
+ Command files ending in `.md` are installed to `.cursor/commands/aicm/` and appear in Cursor under the `/` command menu.
149
172
 
150
- ### Overrides
173
+ ### MCP Servers
151
174
 
152
- You can disable or replace specific rules provided by presets using the `overrides` field:
175
+ You can configure MCP servers directly in your `aicm.json`, which is useful for sharing mcp configurations across your team or bundling them into presets.
153
176
 
154
177
  ```json
155
178
  {
156
- "presets": ["@company/ai-rules"],
157
- "overrides": {
158
- "rule-from-preset-a": "./rules/override-rule.mdc",
159
- "rule-from-preset-b": false
179
+ "mcpServers": {
180
+ "Playwright": {
181
+ "command": "npx",
182
+ "args": ["@playwright/mcp"]
183
+ }
160
184
  }
161
185
  }
162
186
  ```
163
187
 
164
- ### Demo
188
+ When installed, these servers are automatically added to your `.cursor/mcp.json`.
165
189
 
166
- We'll install [an npm package](https://github.com/ranyitz/pirate-coding) containing a simple preset to demonstrate how aicm works.
190
+ ### Referencing Auxiliary Files
167
191
 
168
- 1. **Install the demo preset package**:
192
+ 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.
169
193
 
170
- ```bash
171
- npm install --save-dev pirate-coding
172
- ```
194
+ Example `rules/my-rule.mdc`:
173
195
 
174
- 2. **Create an `aicm.json` file** in your project:
196
+ ```markdown
197
+ # My Rule
175
198
 
176
- ```bash
177
- echo '{ "presets": ["pirate-coding"] }' > aicm.json
199
+ See [Example](./example.ts) for details.
178
200
  ```
179
201
 
180
- 3. **Install all rules & MCPs from your configuration**:
202
+ #### Commands Referencing Files
181
203
 
182
- ```bash
183
- npx aicm install
204
+ 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.
205
+
206
+ For example, if you have a schema file at `rules/schema.json` and a command at `commands/generate-schema.md`:
207
+
208
+ ```markdown
209
+ # Generate Schema
210
+
211
+ Use the schema defined in [Schema Template](../rules/schema.json) to generate the response.
184
212
  ```
185
213
 
186
- This command installs all configured rules and MCPs to their IDE-specific locations.
214
+ 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).
187
215
 
188
- After installation, open Cursor and ask it to do something. Your AI assistant will respond with pirate-themed coding advice. You can also ask it about the aicm library which uses https://gitmcp.io/ to give you advice based on the latest documentation.
216
+ > **Note:** Path rewriting works for any relative path format in your commands - markdown links, inline code references, or bare paths - as long as they point to actual files in your `rulesDir`.
189
217
 
190
- ## Security Note
218
+ ### Overrides
191
219
 
192
- To prevent [prompt-injection](https://en.wikipedia.org/wiki/Prompt_injection), use only packages from trusted sources.
220
+ You can disable or replace specific rules or commands provided by presets using the `overrides` field:
221
+
222
+ ```json
223
+ {
224
+ "presets": ["@company/ai-rules"],
225
+ "overrides": {
226
+ "rule-from-preset-a": "./rules/override-rule.mdc",
227
+ "rule-from-preset-b": false,
228
+ "legacy-command": false
229
+ }
230
+ }
231
+ ```
193
232
 
194
233
  ## Workspaces Support
195
234
 
@@ -203,17 +242,14 @@ You can enable workspaces mode by setting the `workspaces` property to `true` in
203
242
  }
204
243
  ```
205
244
 
206
- aicm automatically detects workspaces if your `package.json` contains a `workspaces` configuration:
245
+ aicm automatically detects workspaces if your `package.json` contains a `workspaces` configuration.
207
246
 
208
247
  ### How It Works
209
248
 
210
- 1. **Discover packages**: Automatically find all directories containing `aicm.json` files in your repository
211
- 2. **Install per package**: Install rules and MCPs for each package individually in their respective directories
212
- 3. **Merge MCP servers**: Write a merged `.cursor/mcp.json` at the repository root containing all MCP servers from every package
213
-
214
- ### How It Works
215
-
216
- Each directory containing an `aicm.json` file is treated as a separate package with its own configuration.
249
+ 1. **Discover packages**: Automatically find all directories containing `aicm.json` files in your repository.
250
+ 2. **Install per package**: Install rules and MCPs for each package individually in their respective directories.
251
+ 3. **Merge MCP servers**: Write a merged `.cursor/mcp.json` at the repository root containing all MCP servers from every package.
252
+ 4. **Merge commands**: Write a merged `.cursor/commands/aicm/` at the repository root containing all commands from every package.
217
253
 
218
254
  For example, in a workspace structure like:
219
255
 
@@ -274,17 +310,7 @@ Create an `aicm.json` file in your project root, or an `aicm` key in your projec
274
310
  - **workspaces**: Set to `true` to enable workspace mode. If not specified, aicm will automatically detect workspaces from your `package.json`.
275
311
  - **skipInstall**: Set to `true` to skip rule installation for this package. Useful for preset packages that provide rules but shouldn't have rules installed into them.
276
312
 
277
- ### MCP Server Installation
278
-
279
- - **Cursor**: MCP server configs are written to `.cursor/mcp.json`.
280
-
281
- ## Supported Targets
282
-
283
- - **Cursor**: Rules are installed as individual `.mdc` files in the Cursor rules directory (`.cursor/rules/aicm/`), mcp servers are installed to `.cursor/mcp.json`
284
- - **Windsurf**: Rules are installed in the `.aicm` directory which should be added to your `.gitignore` file. Our approach for Windsurf is to create links from the `.windsurfrules` file to the respective rules in the `.aicm` directory. There is no support for local mcp servers at the moment.
285
- - **Codex**: Rules are installed in the `.aicm` directory and referenced from `AGENTS.md`.
286
-
287
- ## Commands
313
+ ## CLI Commands
288
314
 
289
315
  ### Global Options
290
316
 
@@ -347,27 +373,9 @@ install({
347
373
  });
348
374
  ```
349
375
 
350
- ### API Reference
351
-
352
- #### `install(options?: InstallOptions): Promise<InstallResult>`
353
-
354
- Installs rules and MCP servers based on configuration.
355
-
356
- **Options:**
357
-
358
- - `cwd`: Base directory to use instead of `process.cwd()`
359
- - `config`: Custom config object to use instead of loading from file
360
- - `installOnCI`: Run installation on CI environments (default: `false`)
361
- - `verbose`: Show verbose output and stack traces for debugging (default: `false`)
362
- - `dryRun`: Simulate installation without writing files, useful for preset validation in CI (default: `false`)
363
-
364
- **Returns:**
365
-
366
- A Promise that resolves to an object with:
376
+ ## Security Note
367
377
 
368
- - `success`: Whether the operation was successful
369
- - `error`: Error object if the operation failed
370
- - `installedRuleCount`: Number of rules installed
378
+ To prevent [prompt-injection](https://en.wikipedia.org/wiki/Prompt_injection), use only packages from trusted sources.
371
379
 
372
380
  ## Contributing
373
381
 
@@ -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) {
@@ -43,7 +41,7 @@ function writeCursorRules(rules, cursorRulesDir) {
43
41
  fs_extra_1.default.writeFileSync(ruleFile, rule.content);
44
42
  }
45
43
  }
46
- function writeCursorCommands(commands, cursorCommandsDir) {
44
+ function writeCursorCommands(commands, cursorCommandsDir, assets) {
47
45
  fs_extra_1.default.removeSync(cursorCommandsDir);
48
46
  for (const command of commands) {
49
47
  const commandNameParts = command.name
@@ -53,9 +51,25 @@ 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, command.sourcePath, assets);
60
+ fs_extra_1.default.writeFileSync(commandFile, content);
57
61
  }
58
62
  }
63
+ function rewriteCommandRelativeLinks(content, commandSourcePath, assets) {
64
+ const commandDir = node_path_1.default.dirname(commandSourcePath);
65
+ const assetMap = new Map(assets.map((a) => [node_path_1.default.normalize(a.sourcePath), a.name]));
66
+ return content.replace(/\.\.[/\\][\w\-/\\.]+/g, (match) => {
67
+ const resolved = node_path_1.default.normalize(node_path_1.default.resolve(commandDir, match));
68
+ return assetMap.has(resolved)
69
+ ? `../../rules/aicm/${assetMap.get(resolved)}`
70
+ : match;
71
+ });
72
+ }
59
73
  function extractNamespaceFromPresetPath(presetPath) {
60
74
  // Special case: npm package names always use forward slashes, regardless of platform
61
75
  if (presetPath.startsWith("@")) {
@@ -68,7 +82,7 @@ function extractNamespaceFromPresetPath(presetPath) {
68
82
  /**
69
83
  * Write rules to a shared directory and update the given rules file
70
84
  */
71
- function writeRulesForFile(rules, ruleDir, rulesFile) {
85
+ function writeRulesForFile(rules, assets, ruleDir, rulesFile) {
72
86
  fs_extra_1.default.emptyDirSync(ruleDir);
73
87
  const ruleFiles = rules.map((rule) => {
74
88
  let rulePath;
@@ -83,9 +97,10 @@ function writeRulesForFile(rules, ruleDir, rulesFile) {
83
97
  // For local rules, maintain the original flat structure
84
98
  rulePath = node_path_1.default.join(ruleDir, ...ruleNameParts);
85
99
  }
100
+ const content = rule.content;
86
101
  const physicalRulePath = rulePath + ".md";
87
102
  fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(physicalRulePath));
88
- fs_extra_1.default.writeFileSync(physicalRulePath, rule.content);
103
+ fs_extra_1.default.writeFileSync(physicalRulePath, content);
89
104
  const relativeRuleDir = node_path_1.default.basename(ruleDir);
90
105
  // For the rules file, maintain the same structure
91
106
  let windsurfPath;
@@ -102,16 +117,46 @@ function writeRulesForFile(rules, ruleDir, rulesFile) {
102
117
  return {
103
118
  name: rule.name,
104
119
  path: windsurfPathPosix,
105
- metadata: (0, rules_file_writer_1.parseRuleFrontmatter)(rule.content),
120
+ metadata: (0, rules_file_writer_1.parseRuleFrontmatter)(content),
106
121
  };
107
122
  });
108
123
  const rulesContent = (0, rules_file_writer_1.generateRulesFileContent)(ruleFiles);
109
124
  (0, rules_file_writer_1.writeRulesFile)(rulesContent, node_path_1.default.join(process.cwd(), rulesFile));
110
125
  }
126
+ function writeAssetsToTargets(assets, targets) {
127
+ const targetPaths = getTargetPaths();
128
+ for (const target of targets) {
129
+ let targetDir;
130
+ switch (target) {
131
+ case "cursor":
132
+ targetDir = targetPaths.cursor;
133
+ break;
134
+ case "windsurf":
135
+ case "codex":
136
+ case "claude":
137
+ targetDir = targetPaths.aicm;
138
+ break;
139
+ default:
140
+ continue;
141
+ }
142
+ for (const asset of assets) {
143
+ let assetPath;
144
+ if (asset.presetName) {
145
+ const namespace = extractNamespaceFromPresetPath(asset.presetName);
146
+ assetPath = node_path_1.default.join(targetDir, ...namespace, asset.name);
147
+ }
148
+ else {
149
+ assetPath = node_path_1.default.join(targetDir, asset.name);
150
+ }
151
+ fs_extra_1.default.ensureDirSync(node_path_1.default.dirname(assetPath));
152
+ fs_extra_1.default.writeFileSync(assetPath, asset.content);
153
+ }
154
+ }
155
+ }
111
156
  /**
112
157
  * Write all collected rules to their respective IDE targets
113
158
  */
114
- function writeRulesToTargets(rules, targets) {
159
+ function writeRulesToTargets(rules, assets, targets) {
115
160
  const targetPaths = getTargetPaths();
116
161
  for (const target of targets) {
117
162
  switch (target) {
@@ -122,29 +167,31 @@ function writeRulesToTargets(rules, targets) {
122
167
  break;
123
168
  case "windsurf":
124
169
  if (rules.length > 0) {
125
- writeRulesForFile(rules, targetPaths.windsurf, ".windsurfrules");
170
+ writeRulesForFile(rules, assets, targetPaths.aicm, ".windsurfrules");
126
171
  }
127
172
  break;
128
173
  case "codex":
129
174
  if (rules.length > 0) {
130
- writeRulesForFile(rules, targetPaths.codex, "AGENTS.md");
175
+ writeRulesForFile(rules, assets, targetPaths.aicm, "AGENTS.md");
131
176
  }
132
177
  break;
133
178
  case "claude":
134
179
  if (rules.length > 0) {
135
- writeRulesForFile(rules, targetPaths.claude, "CLAUDE.md");
180
+ writeRulesForFile(rules, assets, targetPaths.aicm, "CLAUDE.md");
136
181
  }
137
182
  break;
138
183
  }
139
184
  }
185
+ // Write assets after rules so they don't get wiped by emptyDirSync
186
+ writeAssetsToTargets(assets, targets);
140
187
  }
141
- function writeCommandsToTargets(commands, targets) {
188
+ function writeCommandsToTargets(commands, assets, targets) {
142
189
  const projectDir = process.cwd();
143
190
  const cursorRoot = node_path_1.default.join(projectDir, ".cursor");
144
191
  for (const target of targets) {
145
192
  if (target === "cursor") {
146
193
  const commandsDir = node_path_1.default.join(cursorRoot, "commands", "aicm");
147
- writeCursorCommands(commands, commandsDir);
194
+ writeCursorCommands(commands, commandsDir, assets);
148
195
  }
149
196
  // Other targets do not support commands yet
150
197
  }
@@ -357,15 +404,17 @@ async function installPackage(options = {}) {
357
404
  error: new Error("Configuration file not found"),
358
405
  installedRuleCount: 0,
359
406
  installedCommandCount: 0,
407
+ installedAssetCount: 0,
360
408
  packagesCount: 0,
361
409
  };
362
410
  }
363
- const { config, rules, commands, mcpServers } = resolvedConfig;
411
+ const { config, rules, commands, assets, mcpServers } = resolvedConfig;
364
412
  if (config.skipInstall === true) {
365
413
  return {
366
414
  success: true,
367
415
  installedRuleCount: 0,
368
416
  installedCommandCount: 0,
417
+ installedAssetCount: 0,
369
418
  packagesCount: 0,
370
419
  };
371
420
  }
@@ -373,8 +422,8 @@ async function installPackage(options = {}) {
373
422
  const commandsToInstall = dedupeCommandsForInstall(commands);
374
423
  try {
375
424
  if (!options.dryRun) {
376
- writeRulesToTargets(rules, config.targets);
377
- writeCommandsToTargets(commandsToInstall, config.targets);
425
+ writeRulesToTargets(rules, assets, config.targets);
426
+ writeCommandsToTargets(commandsToInstall, assets, config.targets);
378
427
  if (mcpServers && Object.keys(mcpServers).length > 0) {
379
428
  writeMcpServersToTargets(mcpServers, config.targets, cwd);
380
429
  }
@@ -385,6 +434,7 @@ async function installPackage(options = {}) {
385
434
  success: true,
386
435
  installedRuleCount: uniqueRuleCount,
387
436
  installedCommandCount: uniqueCommandCount,
437
+ installedAssetCount: assets.length,
388
438
  packagesCount: 1,
389
439
  };
390
440
  }
@@ -394,6 +444,7 @@ async function installPackage(options = {}) {
394
444
  error: error instanceof Error ? error : new Error(String(error)),
395
445
  installedRuleCount: 0,
396
446
  installedCommandCount: 0,
447
+ installedAssetCount: 0,
397
448
  packagesCount: 0,
398
449
  };
399
450
  }
@@ -406,6 +457,7 @@ async function installWorkspacesPackages(packages, options = {}) {
406
457
  const results = [];
407
458
  let totalRuleCount = 0;
408
459
  let totalCommandCount = 0;
460
+ let totalAssetCount = 0;
409
461
  // Install packages sequentially for now (can be parallelized later)
410
462
  for (const pkg of packages) {
411
463
  const packagePath = pkg.absolutePath;
@@ -417,12 +469,14 @@ async function installWorkspacesPackages(packages, options = {}) {
417
469
  });
418
470
  totalRuleCount += result.installedRuleCount;
419
471
  totalCommandCount += result.installedCommandCount;
472
+ totalAssetCount += result.installedAssetCount;
420
473
  results.push({
421
474
  path: pkg.relativePath,
422
475
  success: result.success,
423
476
  error: result.error,
424
477
  installedRuleCount: result.installedRuleCount,
425
478
  installedCommandCount: result.installedCommandCount,
479
+ installedAssetCount: result.installedAssetCount,
426
480
  });
427
481
  }
428
482
  catch (error) {
@@ -432,6 +486,7 @@ async function installWorkspacesPackages(packages, options = {}) {
432
486
  error: error instanceof Error ? error : new Error(String(error)),
433
487
  installedRuleCount: 0,
434
488
  installedCommandCount: 0,
489
+ installedAssetCount: 0,
435
490
  });
436
491
  }
437
492
  }
@@ -441,6 +496,7 @@ async function installWorkspacesPackages(packages, options = {}) {
441
496
  packages: results,
442
497
  totalRuleCount,
443
498
  totalCommandCount,
499
+ totalAssetCount,
444
500
  };
445
501
  }
446
502
  /**
@@ -471,6 +527,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
471
527
  error: new Error("No packages with aicm configurations found"),
472
528
  installedRuleCount: 0,
473
529
  installedCommandCount: 0,
530
+ installedAssetCount: 0,
474
531
  packagesCount: 0,
475
532
  };
476
533
  }
@@ -495,7 +552,9 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
495
552
  workspaceCommands.length > 0 &&
496
553
  workspaceCommandTargets.length > 0) {
497
554
  const dedupedWorkspaceCommands = dedupeCommandsForInstall(workspaceCommands);
498
- writeCommandsToTargets(dedupedWorkspaceCommands, workspaceCommandTargets);
555
+ // Collect all assets from packages for command path rewriting
556
+ const allAssets = packages.flatMap((pkg) => { var _a; return (_a = pkg.config.assets) !== null && _a !== void 0 ? _a : []; });
557
+ writeCommandsToTargets(dedupedWorkspaceCommands, allAssets, workspaceCommandTargets);
499
558
  }
500
559
  const { merged: rootMcp, conflicts } = mergeWorkspaceMcpServers(packages);
501
560
  const hasCursorTarget = packages.some((p) => p.config.config.targets.includes("cursor"));
@@ -538,6 +597,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
538
597
  error: new Error(`Package installation failed for ${failedPackages.length} package(s): ${errorDetails}`),
539
598
  installedRuleCount: result.totalRuleCount,
540
599
  installedCommandCount: result.totalCommandCount,
600
+ installedAssetCount: result.totalAssetCount,
541
601
  packagesCount: result.packages.length,
542
602
  };
543
603
  }
@@ -545,6 +605,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
545
605
  success: true,
546
606
  installedRuleCount: result.totalRuleCount,
547
607
  installedCommandCount: result.totalCommandCount,
608
+ installedAssetCount: result.totalAssetCount,
548
609
  packagesCount: result.packages.length,
549
610
  };
550
611
  });
@@ -562,6 +623,7 @@ async function install(options = {}) {
562
623
  success: true,
563
624
  installedRuleCount: 0,
564
625
  installedCommandCount: 0,
626
+ installedAssetCount: 0,
565
627
  packagesCount: 0,
566
628
  };
567
629
  }
@@ -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.1",
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",