aicm 0.17.3 → 0.19.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
@@ -14,11 +14,11 @@ A CLI tool for managing Agentic configurations across projects.
14
14
  - [Creating a Preset](#creating-a-preset)
15
15
  - [Using a Preset](#using-a-preset)
16
16
  - [Features](#features)
17
- - [Rules](#using-rules)
18
- - [Commands](#using-commands)
17
+ - [Rules](#rules)
18
+ - [Commands](#commands)
19
+ - [Hooks](#hooks)
19
20
  - [MCP Servers](#mcp-servers)
20
- - [Auxiliary Files](#referencing-auxiliary-files)
21
- - [Overrides](#overrides)
21
+ - [Assets](#assets)
22
22
  - [Workspaces Support](#workspaces-support)
23
23
  - [Configuration](#configuration)
24
24
  - [CLI Commands](#cli-commands)
@@ -78,19 +78,22 @@ After installation, open Cursor and ask it to do something. Your AI assistant wi
78
78
  1. **Create an npm package** with the following structure:
79
79
 
80
80
  ```
81
- @team/ai-preset
81
+ @team/ai-preset/
82
82
  ├── package.json
83
83
  ├── aicm.json
84
- └── rules/
85
- ├── typescript.mdc
86
- └── react.mdc
84
+ ├── rules/ # Rule files (.mdc)
85
+ ├── typescript.mdc
86
+ └── react.mdc
87
+ ├── commands/ # Command files (.md) [optional]
88
+ ├── assets/ # Auxiliary files [optional]
89
+ └── hooks.json # Hook configuration [optional]
87
90
  ```
88
91
 
89
92
  2. **Configure the preset's `aicm.json`**:
90
93
 
91
94
  ```json
92
95
  {
93
- "rulesDir": "rules",
96
+ "rootDir": "./",
94
97
  "mcpServers": {
95
98
  "my-mcp": { "url": "https://example.com/sse" }
96
99
  }
@@ -135,41 +138,149 @@ The rules are now installed in `.cursor/rules/aicm/` and any MCP servers are con
135
138
 
136
139
  ### Notes
137
140
 
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.
141
+ - Generated files are always placed in subdirectories for deterministic cleanup and easy gitignore.
142
+ - Users should add `.cursor/*/aicm/` to `.gitignore` to avoid tracking generated files. This single pattern covers all aicm-managed directories (rules, commands, assets, hooks).
140
143
 
141
144
  ## Features
142
145
 
143
- ### Using Rules
146
+ ### Rules
144
147
 
145
148
  aicm uses Cursor's `.mdc` files for rules. Read more about the format [here](https://cursor.com/docs/context/rules).
146
149
 
147
- Add a rules directory to your project configuration:
150
+ Create a `rules/` directory in your project (at the `rootDir` location):
151
+
152
+ ```
153
+ my-project/
154
+ ├── aicm.json
155
+ └── rules/
156
+ ├── typescript.mdc
157
+ └── react.mdc
158
+ ```
159
+
160
+ Configure your `aicm.json`:
148
161
 
149
162
  ```json
150
163
  {
151
- "rulesDir": "./rules",
164
+ "rootDir": "./",
152
165
  "targets": ["cursor"]
153
166
  }
154
167
  ```
155
168
 
156
169
  Rules are installed in `.cursor/rules/aicm/` and are loaded automatically by Cursor.
157
170
 
158
- ### Using Commands
171
+ ### Commands
159
172
 
160
173
  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.
161
174
 
162
- Add a commands directory to your project configuration:
175
+ Create a `commands/` directory in your project (at the `rootDir` location):
176
+
177
+ ```
178
+ my-project/
179
+ ├── aicm.json
180
+ └── commands/
181
+ ├── review.md
182
+ └── generate.md
183
+ ```
184
+
185
+ Configure your `aicm.json`:
163
186
 
164
187
  ```json
165
188
  {
166
- "commandsDir": "./commands",
189
+ "rootDir": "./",
167
190
  "targets": ["cursor"]
168
191
  }
169
192
  ```
170
193
 
171
194
  Command files ending in `.md` are installed to `.cursor/commands/aicm/` and appear in Cursor under the `/` command menu.
172
195
 
196
+ ### Hooks
197
+
198
+ aicm provides first-class support for [Cursor Agent Hooks](https://docs.cursor.com/advanced/hooks), allowing you to intercept and extend the agent's behavior. Hooks enable you to run custom scripts before/after shell execution, file edits, MCP calls, and more.
199
+
200
+ #### Basic Setup
201
+
202
+ Hooks follow a convention similar to Cursor's own structure:
203
+
204
+ ```
205
+ my-project/
206
+ ├── aicm.json
207
+ ├── hooks.json
208
+ └── hooks/
209
+ ├── audit.sh
210
+ └── format.js
211
+ ```
212
+
213
+ Your `hooks.json` file should reference scripts within the `hooks/` directory:
214
+
215
+ ```json
216
+ {
217
+ "version": 1,
218
+ "hooks": {
219
+ "beforeShellExecution": [{ "command": "./hooks/audit.sh" }],
220
+ "afterFileEdit": [{ "command": "./hooks/format.js" }]
221
+ }
222
+ }
223
+ ```
224
+
225
+ > **Important:** All hook scripts must be within the `hooks/` directory. References to files outside this directory will be warned about and skipped.
226
+
227
+ #### Installation Behavior
228
+
229
+ When you run `aicm install`, the following happens:
230
+
231
+ 1. **Directory Copy**: All files in the `hooks/` directory (except `hooks.json`) are copied
232
+ 2. **Path Rewriting**: Command paths in `hooks.json` are rewritten to point to `.cursor/hooks/aicm/`
233
+ 3. **File Installation**: Scripts are copied to `.cursor/hooks/aicm/` (for local hooks) or `.cursor/hooks/aicm/<preset-name>/` (for preset hooks) with their directory structure preserved
234
+ 4. **Config Merging**: Your hooks configuration is merged into `.cursor/hooks.json`
235
+
236
+ #### Preset Namespacing
237
+
238
+ aicm uses directory-based namespacing to prevent collisions:
239
+
240
+ ```
241
+ .cursor/hooks/aicm/
242
+ ├── preset-a/
243
+ │ └── validate.sh # From preset-a
244
+ └── preset-b/
245
+ └── validate.sh # From preset-b
246
+ ```
247
+
248
+ #### Workspace Support
249
+
250
+ In monorepo/workspace mode, hooks are:
251
+
252
+ - Installed individually for each package (in `package-x/.cursor/hooks.json`)
253
+ - Merged and installed at the root (in `.cursor/hooks.json`)
254
+ - Deduplicated by full path (including preset namespace)
255
+
256
+ **Example workspace structure:**
257
+
258
+ ```
259
+ my-monorepo/
260
+ ├── aicm.json (workspaces: true)
261
+ ├── .cursor/hooks.json (merged from all packages)
262
+ ├── package-a/
263
+ │ ├── aicm.json
264
+ │ ├── hooks.json
265
+ │ ├── hooks/
266
+ │ │ └── check.sh
267
+ │ └── .cursor/hooks.json (package-specific)
268
+ └── package-b/
269
+ ├── aicm.json
270
+ ├── hooks.json
271
+ ├── hooks/
272
+ │ └── validate.js
273
+ └── .cursor/hooks.json (package-specific)
274
+ ```
275
+
276
+ #### Content Collision Detection
277
+
278
+ If the same hook file (by path) has different content across workspace packages, aicm will:
279
+
280
+ 1. Warn you about the collision with full source information
281
+ 2. Use the last occurrence (last-writer-wins)
282
+ 3. Continue installation
283
+
173
284
  ### MCP Servers
174
285
 
175
286
  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.
@@ -187,47 +298,58 @@ You can configure MCP servers directly in your `aicm.json`, which is useful for
187
298
 
188
299
  When installed, these servers are automatically added to your `.cursor/mcp.json`.
189
300
 
190
- ### Referencing Auxiliary Files
301
+ ### Assets
191
302
 
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.
303
+ You can include assets (examples, schemas, scripts, etc.) that can be referenced by your rules, commands, and hooks by placing them in the `assets/` directory.
193
304
 
194
- Example `rules/my-rule.mdc`:
305
+ All files in `assets/` are copied to `.cursor/assets/aicm/` (for Cursor) or `.aicm/` (for Windsurf/Codex/Claude).
195
306
 
196
- ```markdown
197
- # My Rule
307
+ **Example structure:**
198
308
 
199
- See [Example](./example.ts) for details.
309
+ ```
310
+ my-project/
311
+ ├── aicm.json
312
+ ├── rules/
313
+ │ └── api-guide.mdc # References ../assets/schema.json
314
+ ├── commands/
315
+ │ └── generate.md # References ../assets/schema.json
316
+ ├── assets/
317
+ │ ├── schema.json
318
+ │ ├── examples/
319
+ │ │ └── config.ts
320
+ │ └── hooks/
321
+ │ └── validate.sh
322
+ └── hooks.json # References ./hooks/validate.sh
200
323
  ```
201
324
 
202
- #### Commands Referencing Files
203
-
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`:
325
+ **Referencing assets from rules and commands:**
207
326
 
208
327
  ```markdown
209
- # Generate Schema
328
+ <!-- rules/api.mdc -->
210
329
 
211
- Use the schema defined in [Schema Template](../rules/schema.json) to generate the response.
330
+ Use [this schema](../assets/schema.json) for validation.
331
+ Check the example at `../assets/examples/response.json`.
212
332
  ```
213
333
 
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).
334
+ **Note:** The `../assets/` path is automatically adjusted during installation to `../../assets/aicm/` to match the final directory structure. You don't need to worry about the installation paths - just use `../assets/`.
215
335
 
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`.
336
+ **After installation:**
217
337
 
218
- ### Overrides
219
-
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
- }
338
+ ```
339
+ .cursor/
340
+ ├── assets/aicm/ # All assets copied here
341
+ │ ├── schema.json
342
+ │ ├── examples/
343
+ │ │ └── config.ts
344
+ │ └── hooks/
345
+ │ └── validate.sh
346
+ ├── rules/aicm/
347
+ │ └── api-guide.mdc # References ../../assets/aicm/schema.json
348
+ ├── commands/aicm/
349
+ │ └── generate.md # References ../../assets/aicm/schema.json
350
+ └── hooks/
351
+ ├── aicm/
352
+ └── hooks.json
231
353
  ```
232
354
 
233
355
  ## Workspaces Support
@@ -271,6 +393,12 @@ Running `npx aicm install` will install rules for each package in their respecti
271
393
  - `packages/backend/.cursor/rules/aicm/`
272
394
  - `services/api/.cursor/rules/aicm/`
273
395
 
396
+ **Why install in both places?**
397
+ `aicm` installs configurations at both the package level AND the root level to support different workflows:
398
+
399
+ - **Package-level context:** When a developer opens a specific package folder (e.g., `packages/frontend`) in their IDE, they get the specific rules, commands, and MCP servers for that package.
400
+ - **Root-level context:** When a developer opens the monorepo root, `aicm` ensures they have access to all commands and MCP servers from all packages via the merged root configuration. While rules are typically read from nested directories by Cursor, commands and MCP servers must be configured at the root to be accessible.
401
+
274
402
  ### Preset Packages in Workspaces
275
403
 
276
404
  When you have a preset package within your workspace (a package that provides rules to be consumed by others), you can prevent aicm from installing rules into it by setting `skipInstall: true`:
@@ -278,7 +406,7 @@ When you have a preset package within your workspace (a package that provides ru
278
406
  ```json
279
407
  {
280
408
  "skipInstall": true,
281
- "rulesDir": "./rules",
409
+ "rootDir": "./",
282
410
  "targets": ["cursor"]
283
411
  }
284
412
  ```
@@ -291,25 +419,71 @@ Create an `aicm.json` file in your project root, or an `aicm` key in your projec
291
419
 
292
420
  ```json
293
421
  {
294
- "rulesDir": "./rules",
295
- "commandsDir": "./commands",
422
+ "rootDir": "./",
296
423
  "targets": ["cursor"],
297
424
  "presets": [],
298
- "overrides": {},
299
425
  "mcpServers": {},
300
426
  "skipInstall": false
301
427
  }
302
428
  ```
303
429
 
304
- - **rulesDir**: Directory containing all rule files.
305
- - **commandsDir**: Directory containing Cursor command files.
306
- - **targets**: IDEs/Agent targets where rules should be installed. Defaults to `["cursor"]`.
430
+ ### Configuration Options
431
+
432
+ - **rootDir**: Directory containing your aicm structure. Must contain one or more of: `rules/`, `commands/`, `assets/`, `hooks/`, or `hooks.json`. If not specified, aicm will only install rules from presets and will not pick up any local directories.
433
+ - **targets**: IDEs/Agent targets where rules should be installed. Defaults to `["cursor"]`. Supported targets: `cursor`, `windsurf`, `codex`, `claude`.
307
434
  - **presets**: List of preset packages or paths to include.
308
- - **overrides**: Map of rule names to `false` (disable) or a replacement file path.
309
435
  - **mcpServers**: MCP server configurations.
310
436
  - **workspaces**: Set to `true` to enable workspace mode. If not specified, aicm will automatically detect workspaces from your `package.json`.
311
437
  - **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.
312
438
 
439
+ ### Configuration Examples
440
+
441
+ #### Preset-Only Configuration
442
+
443
+ For projects that only consume presets and don't have their own rules, you can omit `rootDir`:
444
+
445
+ ```json
446
+ {
447
+ "presets": ["@company/ai-preset"]
448
+ }
449
+ ```
450
+
451
+ This ensures that only rules from the preset are installed, and any local directories like `commands/` or `rules/` in your project (used for your application) won't be accidentally picked up by aicm.
452
+
453
+ #### Mixed Local and Preset Configuration
454
+
455
+ To combine your own rules with preset rules:
456
+
457
+ ```json
458
+ {
459
+ "rootDir": "./ai-config",
460
+ "presets": ["@company/ai-preset"],
461
+ "targets": ["cursor", "windsurf"]
462
+ }
463
+ ```
464
+
465
+ This will load rules from both `./ai-config/rules/` and the preset, installing them to both Cursor and Windsurf.
466
+
467
+ ### Directory Structure
468
+
469
+ aicm uses a convention-based directory structure:
470
+
471
+ ```
472
+ my-project/
473
+ ├── aicm.json
474
+ ├── rules/ # Rule files (.mdc) [required for rules]
475
+ │ ├── api.mdc
476
+ │ └── testing.mdc
477
+ ├── commands/ # Command files (.md) [optional]
478
+ │ └── generate.md
479
+ ├── assets/ # Auxiliary files [optional]
480
+ │ ├── schema.json
481
+ │ └── examples/
482
+ ├── hooks/ # Hook scripts [optional]
483
+ │ └── validate.sh
484
+ └── hooks.json # Hook configuration [optional]
485
+ ```
486
+
313
487
  ## CLI Commands
314
488
 
315
489
  ### Global Options
@@ -369,7 +543,7 @@ install().then((result) => {
369
543
  // Install with custom options
370
544
  const customConfig = {
371
545
  targets: ["cursor"],
372
- rulesDir: "rules",
546
+ rootDir: "./",
373
547
  presets: ["@team/ai-preset"],
374
548
  };
375
549
 
package/dist/api.d.ts CHANGED
@@ -7,3 +7,4 @@ import { InstallOptions, InstallResult } from "./commands/install";
7
7
  export declare function install(options?: InstallOptions): Promise<InstallResult>;
8
8
  export type { InstallOptions, InstallResult } from "./commands/install";
9
9
  export type { ResolvedConfig, Config, RuleFile, CommandFile, MCPServers, } from "./utils/config";
10
+ export type { HookFile, HooksJson, HookType, HookCommand } from "./utils/hooks";
@@ -97,11 +97,68 @@ function cleanMcpServers(cwd, verbose) {
97
97
  return false;
98
98
  }
99
99
  }
100
+ function cleanHooks(cwd, verbose) {
101
+ const hooksJsonPath = node_path_1.default.join(cwd, ".cursor", "hooks.json");
102
+ const hooksDir = node_path_1.default.join(cwd, ".cursor", "hooks", "aicm");
103
+ let hasChanges = false;
104
+ // Clean hooks directory
105
+ if (fs_extra_1.default.existsSync(hooksDir)) {
106
+ fs_extra_1.default.removeSync(hooksDir);
107
+ if (verbose)
108
+ console.log(chalk_1.default.gray(` Removed ${hooksDir}`));
109
+ hasChanges = true;
110
+ }
111
+ // Clean hooks.json
112
+ if (fs_extra_1.default.existsSync(hooksJsonPath)) {
113
+ try {
114
+ const content = fs_extra_1.default.readJsonSync(hooksJsonPath);
115
+ // Filter out aicm-managed hooks (those pointing to hooks/aicm/)
116
+ const userConfig = {
117
+ version: content.version || 1,
118
+ hooks: {},
119
+ };
120
+ let removedAny = false;
121
+ if (content.hooks) {
122
+ for (const [hookType, hookCommands] of Object.entries(content.hooks)) {
123
+ if (Array.isArray(hookCommands)) {
124
+ const userCommands = hookCommands.filter((cmd) => !cmd.command || !cmd.command.includes("hooks/aicm/"));
125
+ if (userCommands.length < hookCommands.length) {
126
+ removedAny = true;
127
+ }
128
+ if (userCommands.length > 0) {
129
+ userConfig.hooks[hookType] = userCommands;
130
+ }
131
+ }
132
+ }
133
+ }
134
+ if (removedAny) {
135
+ const hasUserHooks = userConfig.hooks && Object.keys(userConfig.hooks).length > 0;
136
+ if (!hasUserHooks) {
137
+ fs_extra_1.default.removeSync(hooksJsonPath);
138
+ if (verbose)
139
+ console.log(chalk_1.default.gray(` Removed empty ${hooksJsonPath}`));
140
+ }
141
+ else {
142
+ fs_extra_1.default.writeJsonSync(hooksJsonPath, userConfig, { spaces: 2 });
143
+ if (verbose)
144
+ console.log(chalk_1.default.gray(` Cleaned aicm hooks from ${hooksJsonPath}`));
145
+ }
146
+ hasChanges = true;
147
+ }
148
+ }
149
+ catch (_a) {
150
+ console.warn(chalk_1.default.yellow(`Warning: Failed to clean hooks.json`));
151
+ }
152
+ }
153
+ return hasChanges;
154
+ }
100
155
  function cleanEmptyDirectories(cwd, verbose) {
101
156
  let cleanedCount = 0;
102
157
  const dirsToCheck = [
103
158
  node_path_1.default.join(cwd, ".cursor", "rules"),
104
159
  node_path_1.default.join(cwd, ".cursor", "commands"),
160
+ node_path_1.default.join(cwd, ".cursor", "assets"),
161
+ node_path_1.default.join(cwd, ".cursor", "hooks"),
105
162
  node_path_1.default.join(cwd, ".cursor"),
106
163
  ];
107
164
  for (const dir of dirsToCheck) {
@@ -130,6 +187,7 @@ async function cleanPackage(options = {}) {
130
187
  const filesToClean = [
131
188
  node_path_1.default.join(cwd, ".cursor", "rules", "aicm"),
132
189
  node_path_1.default.join(cwd, ".cursor", "commands", "aicm"),
190
+ node_path_1.default.join(cwd, ".cursor", "assets", "aicm"),
133
191
  node_path_1.default.join(cwd, ".aicm"),
134
192
  ];
135
193
  const rulesFilesToClean = [
@@ -150,6 +208,9 @@ async function cleanPackage(options = {}) {
150
208
  // Clean MCP servers
151
209
  if (cleanMcpServers(cwd, verbose))
152
210
  cleanedCount++;
211
+ // Clean hooks
212
+ if (cleanHooks(cwd, verbose))
213
+ cleanedCount++;
153
214
  // Clean empty directories
154
215
  cleanedCount += cleanEmptyDirectories(cwd, verbose);
155
216
  return {
@@ -8,7 +8,8 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const chalk_1 = __importDefault(require("chalk"));
10
10
  const defaultConfig = {
11
- rulesDir: "rules",
11
+ rootDir: "./",
12
+ targets: ["cursor"],
12
13
  };
13
14
  function initCommand() {
14
15
  const configPath = path_1.default.join(process.cwd(), "aicm.json");
@@ -17,11 +18,30 @@ function initCommand() {
17
18
  return;
18
19
  }
19
20
  try {
21
+ // Create standard directory structure
22
+ const dirs = ["rules", "commands", "assets", "hooks"];
23
+ for (const dir of dirs) {
24
+ const dirPath = path_1.default.join(process.cwd(), dir);
25
+ if (!fs_extra_1.default.existsSync(dirPath)) {
26
+ fs_extra_1.default.mkdirSync(dirPath, { recursive: true });
27
+ }
28
+ }
29
+ // Create placeholder file in rules directory
30
+ const rulesReadmePath = path_1.default.join(process.cwd(), "rules", ".gitkeep");
31
+ if (!fs_extra_1.default.existsSync(rulesReadmePath)) {
32
+ fs_extra_1.default.writeFileSync(rulesReadmePath, "# Place your .mdc rule files here\n");
33
+ }
20
34
  fs_extra_1.default.writeJsonSync(configPath, defaultConfig, { spaces: 2 });
21
35
  console.log(`Configuration file location: ${chalk_1.default.blue(configPath)}`);
36
+ console.log(`\nCreated directory structure:`);
37
+ console.log(` - ${chalk_1.default.blue("rules/")} for rule files (.mdc)`);
38
+ console.log(` - ${chalk_1.default.blue("commands/")} for command files (.md)`);
39
+ console.log(` - ${chalk_1.default.blue("assets/")} for auxiliary files`);
40
+ console.log(` - ${chalk_1.default.blue("hooks/")} for hook scripts`);
22
41
  console.log(`\nNext steps:`);
23
- console.log(` 1. Edit ${chalk_1.default.blue("aicm.json")} to configure your rules & presets`);
24
- console.log(` 2. Run ${chalk_1.default.blue("npx aicm install")} to install rules & mcps`);
42
+ console.log(` 1. Add your rule files to ${chalk_1.default.blue("rules/")} directory`);
43
+ console.log(` 2. Edit ${chalk_1.default.blue("aicm.json")} to configure presets if needed`);
44
+ console.log(` 3. Run ${chalk_1.default.blue("npx aicm install")} to install rules & mcps`);
25
45
  }
26
46
  catch (error) {
27
47
  console.error(chalk_1.default.red("Error creating configuration file:"), error);
@@ -0,0 +1,5 @@
1
+ import { InstallResult } from "./install";
2
+ /**
3
+ * Install rules across multiple packages in a workspace
4
+ */
5
+ export declare function installWorkspaces(cwd: string, installOnCI: boolean, verbose?: boolean, dryRun?: boolean): Promise<InstallResult>;