aicm 0.20.0 → 0.20.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
@@ -17,6 +17,7 @@ A CLI tool for managing Agentic configurations across projects.
17
17
  - [Rules](#rules)
18
18
  - [Commands](#commands)
19
19
  - [Skills](#skills)
20
+ - [Agents](#agents)
20
21
  - [Hooks](#hooks)
21
22
  - [MCP Servers](#mcp-servers)
22
23
  - [Assets](#assets)
@@ -88,6 +89,7 @@ After installation, open Cursor and ask it to do something. Your AI assistant wi
88
89
  │ └── react.mdc
89
90
  ├── commands/ # Command files (.md) [optional]
90
91
  ├── skills/ # Agent Skills [optional]
92
+ ├── agents/ # Subagents (.md) [optional]
91
93
  ├── assets/ # Auxiliary files [optional]
92
94
  └── hooks.json # Hook configuration [optional]
93
95
  ```
@@ -142,7 +144,7 @@ The rules are now installed in `.cursor/rules/aicm/` and any MCP servers are con
142
144
  ### Notes
143
145
 
144
146
  - Generated files are always placed in subdirectories for deterministic cleanup and easy gitignore.
145
- - Users should add `.cursor/*/aicm/`, `.cursor/skills/`, `.claude/`, and `.codex/` to `.gitignore` to avoid tracking generated files.
147
+ - Users may add `.cursor/*/aicm/`, `.cursor/skills/`, `.cursor/agents/`, `.claude/`, and `.codex/` to `.gitignore` to avoid tracking generated files.
146
148
 
147
149
  ## Features
148
150
 
@@ -255,6 +257,77 @@ When installed, each skill directory is copied in its entirety (including `scrip
255
257
 
256
258
  In workspace mode, skills are installed both to each package and merged at the root level, similar to commands.
257
259
 
260
+ ### Agents
261
+
262
+ aicm supports [Cursor Subagents](https://cursor.com/docs/context/subagents) and [Claude Code Subagents](https://code.claude.com/docs/en/sub-agents) - specialized AI assistants that can be delegated specific tasks. Agents are markdown files with YAML frontmatter that define custom prompts, descriptions, and model configurations.
263
+
264
+ Create an `agents/` directory in your project (at the `rootDir` location):
265
+
266
+ ```
267
+ my-project/
268
+ ├── aicm.json
269
+ └── agents/
270
+ ├── code-reviewer.md
271
+ ├── debugger.md
272
+ └── specialized/
273
+ └── security-auditor.md
274
+ ```
275
+
276
+ Each agent file should have YAML frontmatter with at least a `name` and `description`:
277
+
278
+ ```markdown
279
+ ---
280
+ name: code-reviewer
281
+ description: Reviews code for quality and best practices. Use after code changes.
282
+ model: inherit
283
+ ---
284
+
285
+ You are a senior code reviewer ensuring high standards of code quality and security.
286
+
287
+ When invoked:
288
+
289
+ 1. Run git diff to see recent changes
290
+ 2. Focus on modified files
291
+ 3. Begin review immediately
292
+
293
+ Review checklist:
294
+
295
+ - Code is clear and readable
296
+ - Functions and variables are well-named
297
+ - No duplicated code
298
+ - Proper error handling
299
+ ```
300
+
301
+ Configure your `aicm.json`:
302
+
303
+ ```json
304
+ {
305
+ "rootDir": "./",
306
+ "targets": ["cursor", "claude"]
307
+ }
308
+ ```
309
+
310
+ Agents are installed to different locations based on the target:
311
+
312
+ | Target | Agents Location |
313
+ | ---------- | ----------------- |
314
+ | **Cursor** | `.cursor/agents/` |
315
+ | **Claude** | `.claude/agents/` |
316
+
317
+ A `.aicm.json` metadata file is created in the agents directory to track which agents are managed by aicm. This allows the clean command to remove only aicm-managed agents while preserving any manually created agents.
318
+
319
+ **Supported Configuration Fields:**
320
+
321
+ Only fields that work in both Cursor and Claude Code are documented:
322
+
323
+ - `name` - Unique identifier (defaults to filename without extension)
324
+ - `description` - When the agent should be used for task delegation
325
+ - `model` - Model to use (`inherit`, or platform-specific values like `sonnet`, `haiku`, `fast`)
326
+
327
+ > **Note:** Users may include additional platform-specific fields (e.g., `tools`, `hooks` for Claude Code, or `readonly`, `is_background` for Cursor) - aicm will preserve them, but they only work on the respective platform.
328
+
329
+ In workspace mode, agents are installed both to each package and merged at the root level, similar to commands and skills.
330
+
258
331
  ### Hooks
259
332
 
260
333
  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.
@@ -431,10 +504,11 @@ aicm automatically detects workspaces if your `package.json` contains a `workspa
431
504
  ### How It Works
432
505
 
433
506
  1. **Discover packages**: Automatically find all directories containing `aicm.json` files in your repository.
434
- 2. **Install per package**: Install rules, commands, and skills for each package individually in their respective directories.
507
+ 2. **Install per package**: Install rules, commands, skills, and agents for each package individually in their respective directories.
435
508
  3. **Merge MCP servers**: Write a merged `.cursor/mcp.json` at the repository root containing all MCP servers from every package.
436
509
  4. **Merge commands**: Write a merged `.cursor/commands/aicm/` at the repository root containing all commands from every package.
437
510
  5. **Merge skills**: Write merged skills to the repository root (e.g., `.cursor/skills/`) containing all skills from every package.
511
+ 6. **Merge agents**: Write merged agents to the repository root (e.g., `.cursor/agents/`) containing all agents from every package.
438
512
 
439
513
  For example, in a workspace structure like:
440
514
 
@@ -492,7 +566,7 @@ Create an `aicm.json` file in your project root, or an `aicm` key in your projec
492
566
 
493
567
  ### Configuration Options
494
568
 
495
- - **rootDir**: Directory containing your aicm structure. Must contain one or more of: `rules/`, `commands/`, `skills/`, `assets/`, `hooks/`, or `hooks.json`. If not specified, aicm will only install rules from presets and will not pick up any local directories.
569
+ - **rootDir**: Directory containing your aicm structure. Must contain one or more of: `rules/`, `commands/`, `skills/`, `agents/`, `assets/`, `hooks/`, or `hooks.json`. If not specified, aicm will only install rules from presets and will not pick up any local directories.
496
570
  - **targets**: IDEs/Agent targets where rules should be installed. Defaults to `["cursor"]`. Supported targets: `cursor`, `windsurf`, `codex`, `claude`.
497
571
  - **presets**: List of preset packages or paths to include.
498
572
  - **mcpServers**: MCP server configurations.
@@ -542,6 +616,8 @@ my-project/
542
616
  ├── skills/ # Agent Skills [optional]
543
617
  │ └── my-skill/
544
618
  │ └── SKILL.md
619
+ ├── agents/ # Subagents (.md) [optional]
620
+ │ └── code-reviewer.md
545
621
  ├── assets/ # Auxiliary files [optional]
546
622
  │ ├── schema.json
547
623
  │ └── examples/
@@ -200,6 +200,62 @@ function cleanSkills(cwd, verbose) {
200
200
  }
201
201
  return cleanedCount;
202
202
  }
203
+ /**
204
+ * Clean aicm-managed agents from agents directories
205
+ * Only removes agents that are tracked in .aicm.json metadata file
206
+ */
207
+ function cleanAgents(cwd, verbose) {
208
+ let cleanedCount = 0;
209
+ // Agents directories for each target
210
+ const agentsDirs = [
211
+ node_path_1.default.join(cwd, ".cursor", "agents"),
212
+ node_path_1.default.join(cwd, ".claude", "agents"),
213
+ ];
214
+ for (const agentsDir of agentsDirs) {
215
+ const metadataPath = node_path_1.default.join(agentsDir, ".aicm.json");
216
+ if (!fs_extra_1.default.existsSync(metadataPath)) {
217
+ continue;
218
+ }
219
+ try {
220
+ const metadata = fs_extra_1.default.readJsonSync(metadataPath);
221
+ // Remove all managed agents (names only)
222
+ for (const agentName of metadata.managedAgents || []) {
223
+ // Skip invalid names containing path separators (security check)
224
+ if (agentName.includes("/") || agentName.includes("\\")) {
225
+ console.warn(chalk_1.default.yellow(`Warning: Skipping invalid agent name "${agentName}" (contains path separator)`));
226
+ continue;
227
+ }
228
+ const fullPath = node_path_1.default.join(agentsDir, agentName + ".md");
229
+ if (fs_extra_1.default.existsSync(fullPath)) {
230
+ fs_extra_1.default.removeSync(fullPath);
231
+ if (verbose) {
232
+ console.log(chalk_1.default.gray(` Removed agent ${fullPath}`));
233
+ }
234
+ cleanedCount++;
235
+ }
236
+ }
237
+ // Remove the metadata file
238
+ fs_extra_1.default.removeSync(metadataPath);
239
+ if (verbose) {
240
+ console.log(chalk_1.default.gray(` Removed ${metadataPath}`));
241
+ }
242
+ // Remove the agents directory if it's now empty
243
+ if (fs_extra_1.default.existsSync(agentsDir)) {
244
+ const remainingEntries = fs_extra_1.default.readdirSync(agentsDir);
245
+ if (remainingEntries.length === 0) {
246
+ fs_extra_1.default.removeSync(agentsDir);
247
+ if (verbose) {
248
+ console.log(chalk_1.default.gray(` Removed empty directory ${agentsDir}`));
249
+ }
250
+ }
251
+ }
252
+ }
253
+ catch (_a) {
254
+ console.warn(chalk_1.default.yellow(`Warning: Failed to clean agents in ${agentsDir}`));
255
+ }
256
+ }
257
+ return cleanedCount;
258
+ }
203
259
  function cleanEmptyDirectories(cwd, verbose) {
204
260
  let cleanedCount = 0;
205
261
  const dirsToCheck = [
@@ -208,8 +264,10 @@ function cleanEmptyDirectories(cwd, verbose) {
208
264
  node_path_1.default.join(cwd, ".cursor", "assets"),
209
265
  node_path_1.default.join(cwd, ".cursor", "hooks"),
210
266
  node_path_1.default.join(cwd, ".cursor", "skills"),
267
+ node_path_1.default.join(cwd, ".cursor", "agents"),
211
268
  node_path_1.default.join(cwd, ".cursor"),
212
269
  node_path_1.default.join(cwd, ".claude", "skills"),
270
+ node_path_1.default.join(cwd, ".claude", "agents"),
213
271
  node_path_1.default.join(cwd, ".claude"),
214
272
  node_path_1.default.join(cwd, ".codex", "skills"),
215
273
  node_path_1.default.join(cwd, ".codex"),
@@ -266,6 +324,8 @@ async function cleanPackage(options = {}) {
266
324
  cleanedCount++;
267
325
  // Clean skills
268
326
  cleanedCount += cleanSkills(cwd, verbose);
327
+ // Clean agents
328
+ cleanedCount += cleanAgents(cwd, verbose);
269
329
  // Clean empty directories
270
330
  cleanedCount += cleanEmptyDirectories(cwd, verbose);
271
331
  return {
@@ -87,6 +87,51 @@ function collectWorkspaceSkillTargets(packages) {
87
87
  }
88
88
  return Array.from(targets);
89
89
  }
90
+ /**
91
+ * Merge agents from multiple workspace packages
92
+ * Agents are merged flat (not namespaced by preset)
93
+ * Dedupes preset agents that appear in multiple packages
94
+ */
95
+ function mergeWorkspaceAgents(packages) {
96
+ var _a;
97
+ const agents = [];
98
+ const seenPresetAgents = new Set();
99
+ for (const pkg of packages) {
100
+ // Agents are supported by cursor and claude targets
101
+ const hasAgentsTarget = pkg.config.config.targets.includes("cursor") ||
102
+ pkg.config.config.targets.includes("claude");
103
+ if (!hasAgentsTarget) {
104
+ continue;
105
+ }
106
+ for (const agent of (_a = pkg.config.agents) !== null && _a !== void 0 ? _a : []) {
107
+ if (agent.presetName) {
108
+ // Dedupe preset agents by preset+name combination
109
+ const presetKey = `${agent.presetName}::${agent.name}`;
110
+ if (seenPresetAgents.has(presetKey)) {
111
+ continue;
112
+ }
113
+ seenPresetAgents.add(presetKey);
114
+ }
115
+ agents.push(agent);
116
+ }
117
+ }
118
+ return agents;
119
+ }
120
+ /**
121
+ * Collect all targets that support agents from workspace packages
122
+ */
123
+ function collectWorkspaceAgentTargets(packages) {
124
+ const targets = new Set();
125
+ for (const pkg of packages) {
126
+ for (const target of pkg.config.config.targets) {
127
+ // Agents are supported by cursor and claude
128
+ if (target === "cursor" || target === "claude") {
129
+ targets.add(target);
130
+ }
131
+ }
132
+ }
133
+ return Array.from(targets);
134
+ }
90
135
  function mergeWorkspaceMcpServers(packages) {
91
136
  const merged = {};
92
137
  const info = {};
@@ -148,6 +193,7 @@ async function installWorkspacesPackages(packages, options = {}) {
148
193
  let totalAssetCount = 0;
149
194
  let totalHookCount = 0;
150
195
  let totalSkillCount = 0;
196
+ let totalAgentCount = 0;
151
197
  // Install packages sequentially for now (can be parallelized later)
152
198
  for (const pkg of packages) {
153
199
  const packagePath = pkg.absolutePath;
@@ -162,6 +208,7 @@ async function installWorkspacesPackages(packages, options = {}) {
162
208
  totalAssetCount += result.installedAssetCount;
163
209
  totalHookCount += result.installedHookCount;
164
210
  totalSkillCount += result.installedSkillCount;
211
+ totalAgentCount += result.installedAgentCount;
165
212
  results.push({
166
213
  path: pkg.relativePath,
167
214
  success: result.success,
@@ -171,6 +218,7 @@ async function installWorkspacesPackages(packages, options = {}) {
171
218
  installedAssetCount: result.installedAssetCount,
172
219
  installedHookCount: result.installedHookCount,
173
220
  installedSkillCount: result.installedSkillCount,
221
+ installedAgentCount: result.installedAgentCount,
174
222
  });
175
223
  }
176
224
  catch (error) {
@@ -183,6 +231,7 @@ async function installWorkspacesPackages(packages, options = {}) {
183
231
  installedAssetCount: 0,
184
232
  installedHookCount: 0,
185
233
  installedSkillCount: 0,
234
+ installedAgentCount: 0,
186
235
  });
187
236
  }
188
237
  }
@@ -195,6 +244,7 @@ async function installWorkspacesPackages(packages, options = {}) {
195
244
  totalAssetCount,
196
245
  totalHookCount,
197
246
  totalSkillCount,
247
+ totalAgentCount,
198
248
  };
199
249
  }
200
250
  /**
@@ -213,12 +263,13 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
213
263
  const isRoot = pkg.relativePath === ".";
214
264
  if (!isRoot)
215
265
  return true;
216
- // For root directories, only keep if it has rules, commands, skills, or presets
266
+ // For root directories, only keep if it has rules, commands, skills, agents, or presets
217
267
  const hasRules = pkg.config.rules && pkg.config.rules.length > 0;
218
268
  const hasCommands = pkg.config.commands && pkg.config.commands.length > 0;
219
269
  const hasSkills = pkg.config.skills && pkg.config.skills.length > 0;
270
+ const hasAgents = pkg.config.agents && pkg.config.agents.length > 0;
220
271
  const hasPresets = pkg.config.config.presets && pkg.config.config.presets.length > 0;
221
- return hasRules || hasCommands || hasSkills || hasPresets;
272
+ return hasRules || hasCommands || hasSkills || hasAgents || hasPresets;
222
273
  });
223
274
  if (packages.length === 0) {
224
275
  return {
@@ -229,6 +280,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
229
280
  installedAssetCount: 0,
230
281
  installedHookCount: 0,
231
282
  installedSkillCount: 0,
283
+ installedAgentCount: 0,
232
284
  packagesCount: 0,
233
285
  };
234
286
  }
@@ -271,6 +323,18 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
271
323
  const dedupedWorkspaceSkills = (0, install_1.dedupeSkillsForInstall)(workspaceSkills);
272
324
  (0, install_1.writeSkillsToTargets)(dedupedWorkspaceSkills, workspaceSkillTargets);
273
325
  }
326
+ // Merge and write agents for workspace
327
+ const workspaceAgents = mergeWorkspaceAgents(packages);
328
+ const workspaceAgentTargets = collectWorkspaceAgentTargets(packages);
329
+ if (workspaceAgents.length > 0) {
330
+ (0, install_1.warnPresetAgentCollisions)(workspaceAgents);
331
+ }
332
+ if (!dryRun &&
333
+ workspaceAgents.length > 0 &&
334
+ workspaceAgentTargets.length > 0) {
335
+ const dedupedWorkspaceAgents = (0, install_1.dedupeAgentsForInstall)(workspaceAgents);
336
+ (0, install_1.writeAgentsToTargets)(dedupedWorkspaceAgents, workspaceAgentTargets);
337
+ }
274
338
  const { merged: rootMcp, conflicts } = mergeWorkspaceMcpServers(packages);
275
339
  const hasCursorTarget = packages.some((p) => p.config.config.targets.includes("cursor"));
276
340
  if (!dryRun && hasCursorTarget && Object.keys(rootMcp).length > 0) {
@@ -299,6 +363,9 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
299
363
  if (pkg.installedSkillCount > 0) {
300
364
  summaryParts.push(`${pkg.installedSkillCount} skill${pkg.installedSkillCount === 1 ? "" : "s"}`);
301
365
  }
366
+ if (pkg.installedAgentCount > 0) {
367
+ summaryParts.push(`${pkg.installedAgentCount} agent${pkg.installedAgentCount === 1 ? "" : "s"}`);
368
+ }
302
369
  console.log(chalk_1.default.green(`✅ ${pkg.path} (${summaryParts.join(", ")})`));
303
370
  }
304
371
  else {
@@ -319,7 +386,10 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
319
386
  const skillSummary = result.totalSkillCount > 0
320
387
  ? `, ${result.totalSkillCount} skill${result.totalSkillCount === 1 ? "" : "s"} total`
321
388
  : "";
322
- console.log(chalk_1.default.green(`Successfully installed: ${result.packages.length - failedPackages.length}/${result.packages.length} packages (${result.totalRuleCount} rule${result.totalRuleCount === 1 ? "" : "s"} total${commandSummary}${hookSummary}${skillSummary})`));
389
+ const agentSummary = result.totalAgentCount > 0
390
+ ? `, ${result.totalAgentCount} agent${result.totalAgentCount === 1 ? "" : "s"} total`
391
+ : "";
392
+ console.log(chalk_1.default.green(`Successfully installed: ${result.packages.length - failedPackages.length}/${result.packages.length} packages (${result.totalRuleCount} rule${result.totalRuleCount === 1 ? "" : "s"} total${commandSummary}${hookSummary}${skillSummary}${agentSummary})`));
323
393
  console.log(chalk_1.default.red(`Failed packages: ${failedPackages.map((p) => p.path).join(", ")}`));
324
394
  }
325
395
  const errorDetails = failedPackages
@@ -333,6 +403,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
333
403
  installedAssetCount: result.totalAssetCount,
334
404
  installedHookCount: result.totalHookCount,
335
405
  installedSkillCount: result.totalSkillCount,
406
+ installedAgentCount: result.totalAgentCount,
336
407
  packagesCount: result.packages.length,
337
408
  };
338
409
  }
@@ -343,6 +414,7 @@ async function installWorkspaces(cwd, installOnCI, verbose = false, dryRun = fal
343
414
  installedAssetCount: result.totalAssetCount,
344
415
  installedHookCount: result.totalHookCount,
345
416
  installedSkillCount: result.totalSkillCount,
417
+ installedAgentCount: result.totalAgentCount,
346
418
  packagesCount: result.packages.length,
347
419
  };
348
420
  });
@@ -1,4 +1,4 @@
1
- import { ResolvedConfig, CommandFile, AssetFile, SkillFile, MCPServers, SupportedTarget } from "../utils/config";
1
+ import { ResolvedConfig, CommandFile, AssetFile, SkillFile, AgentFile, MCPServers, SupportedTarget } from "../utils/config";
2
2
  export interface InstallOptions {
3
3
  /**
4
4
  * Base directory to use instead of process.cwd()
@@ -53,6 +53,10 @@ export interface InstallResult {
53
53
  * Number of skills installed
54
54
  */
55
55
  installedSkillCount: number;
56
+ /**
57
+ * Number of agents installed
58
+ */
59
+ installedAgentCount: number;
56
60
  /**
57
61
  * Number of packages installed
58
62
  */
@@ -72,6 +76,20 @@ export declare function warnPresetSkillCollisions(skills: SkillFile[]): void;
72
76
  * Dedupe skills by name (last one wins)
73
77
  */
74
78
  export declare function dedupeSkillsForInstall(skills: SkillFile[]): SkillFile[];
79
+ /**
80
+ * Write agents to all supported target directories
81
+ * Similar to skills, agents are written directly to the agents directory
82
+ * with a .aicm.json metadata file tracking which agents are managed
83
+ */
84
+ export declare function writeAgentsToTargets(agents: AgentFile[], targets: SupportedTarget[]): void;
85
+ /**
86
+ * Warn about agent name collisions from different presets
87
+ */
88
+ export declare function warnPresetAgentCollisions(agents: AgentFile[]): void;
89
+ /**
90
+ * Dedupe agents by name (last one wins)
91
+ */
92
+ export declare function dedupeAgentsForInstall(agents: AgentFile[]): AgentFile[];
75
93
  export declare function warnPresetCommandCollisions(commands: CommandFile[]): void;
76
94
  export declare function dedupeCommandsForInstall(commands: CommandFile[]): CommandFile[];
77
95
  /**
@@ -8,6 +8,9 @@ exports.writeCommandsToTargets = writeCommandsToTargets;
8
8
  exports.writeSkillsToTargets = writeSkillsToTargets;
9
9
  exports.warnPresetSkillCollisions = warnPresetSkillCollisions;
10
10
  exports.dedupeSkillsForInstall = dedupeSkillsForInstall;
11
+ exports.writeAgentsToTargets = writeAgentsToTargets;
12
+ exports.warnPresetAgentCollisions = warnPresetAgentCollisions;
13
+ exports.dedupeAgentsForInstall = dedupeAgentsForInstall;
11
14
  exports.warnPresetCommandCollisions = warnPresetCommandCollisions;
12
15
  exports.dedupeCommandsForInstall = dedupeCommandsForInstall;
13
16
  exports.writeMcpServersToFile = writeMcpServersToFile;
@@ -330,6 +333,115 @@ function dedupeSkillsForInstall(skills) {
330
333
  }
331
334
  return Array.from(unique.values());
332
335
  }
336
+ /**
337
+ * Get the agents installation path for a target
338
+ * Returns null for targets that don't support agents
339
+ */
340
+ function getAgentsTargetPath(target) {
341
+ const projectDir = process.cwd();
342
+ switch (target) {
343
+ case "cursor":
344
+ return node_path_1.default.join(projectDir, ".cursor", "agents");
345
+ case "claude":
346
+ return node_path_1.default.join(projectDir, ".claude", "agents");
347
+ case "codex":
348
+ case "windsurf":
349
+ // Codex and Windsurf do not support agents
350
+ return null;
351
+ default:
352
+ return null;
353
+ }
354
+ }
355
+ /**
356
+ * Write agents to all supported target directories
357
+ * Similar to skills, agents are written directly to the agents directory
358
+ * with a .aicm.json metadata file tracking which agents are managed
359
+ */
360
+ function writeAgentsToTargets(agents, targets) {
361
+ if (agents.length === 0)
362
+ return;
363
+ for (const target of targets) {
364
+ const targetAgentsDir = getAgentsTargetPath(target);
365
+ if (!targetAgentsDir) {
366
+ // Target doesn't support agents
367
+ continue;
368
+ }
369
+ // Ensure the agents directory exists
370
+ fs_extra_1.default.ensureDirSync(targetAgentsDir);
371
+ // Read existing metadata to clean up old managed agents
372
+ const metadataPath = node_path_1.default.join(targetAgentsDir, ".aicm.json");
373
+ if (fs_extra_1.default.existsSync(metadataPath)) {
374
+ try {
375
+ const existingMetadata = fs_extra_1.default.readJsonSync(metadataPath);
376
+ // Remove previously managed agents
377
+ for (const agentName of existingMetadata.managedAgents || []) {
378
+ // Skip invalid names containing path separators
379
+ if (agentName.includes("/") || agentName.includes("\\")) {
380
+ console.warn(chalk_1.default.yellow(`Warning: Skipping invalid agent name "${agentName}" (contains path separator)`));
381
+ continue;
382
+ }
383
+ const fullPath = node_path_1.default.join(targetAgentsDir, agentName + ".md");
384
+ if (fs_extra_1.default.existsSync(fullPath)) {
385
+ fs_extra_1.default.removeSync(fullPath);
386
+ }
387
+ }
388
+ }
389
+ catch (_a) {
390
+ // Ignore errors reading metadata
391
+ }
392
+ }
393
+ const managedAgents = [];
394
+ for (const agent of agents) {
395
+ // Use base name only
396
+ const agentName = node_path_1.default.basename(agent.name, node_path_1.default.extname(agent.name));
397
+ const agentFile = node_path_1.default.join(targetAgentsDir, agentName + ".md");
398
+ fs_extra_1.default.writeFileSync(agentFile, agent.content);
399
+ managedAgents.push(agentName);
400
+ }
401
+ // Write metadata file to track managed agents
402
+ const metadata = {
403
+ managedAgents,
404
+ };
405
+ fs_extra_1.default.writeJsonSync(metadataPath, metadata, { spaces: 2 });
406
+ }
407
+ }
408
+ /**
409
+ * Warn about agent name collisions from different presets
410
+ */
411
+ function warnPresetAgentCollisions(agents) {
412
+ const collisions = new Map();
413
+ for (const agent of agents) {
414
+ if (!agent.presetName)
415
+ continue;
416
+ const entry = collisions.get(agent.name);
417
+ if (entry) {
418
+ entry.presets.add(agent.presetName);
419
+ entry.lastPreset = agent.presetName;
420
+ }
421
+ else {
422
+ collisions.set(agent.name, {
423
+ presets: new Set([agent.presetName]),
424
+ lastPreset: agent.presetName,
425
+ });
426
+ }
427
+ }
428
+ for (const [agentName, { presets, lastPreset }] of collisions) {
429
+ if (presets.size > 1) {
430
+ const presetList = Array.from(presets).sort().join(", ");
431
+ console.warn(chalk_1.default.yellow(`Warning: multiple presets provide the "${agentName}" agent (${presetList}). Using definition from ${lastPreset}.`));
432
+ }
433
+ }
434
+ }
435
+ /**
436
+ * Dedupe agents by name (last one wins)
437
+ */
438
+ function dedupeAgentsForInstall(agents) {
439
+ const unique = new Map();
440
+ for (const agent of agents) {
441
+ unique.set(agent.name, agent);
442
+ }
443
+ return Array.from(unique.values());
444
+ }
333
445
  function warnPresetCommandCollisions(commands) {
334
446
  const collisions = new Map();
335
447
  for (const command of commands) {
@@ -451,10 +563,11 @@ async function installPackage(options = {}) {
451
563
  installedAssetCount: 0,
452
564
  installedHookCount: 0,
453
565
  installedSkillCount: 0,
566
+ installedAgentCount: 0,
454
567
  packagesCount: 0,
455
568
  };
456
569
  }
457
- const { config, rules, commands, assets, skills, mcpServers, hooks, hookFiles, } = resolvedConfig;
570
+ const { config, rules, commands, assets, skills, agents, mcpServers, hooks, hookFiles, } = resolvedConfig;
458
571
  if (config.skipInstall === true) {
459
572
  return {
460
573
  success: true,
@@ -463,6 +576,7 @@ async function installPackage(options = {}) {
463
576
  installedAssetCount: 0,
464
577
  installedHookCount: 0,
465
578
  installedSkillCount: 0,
579
+ installedAgentCount: 0,
466
580
  packagesCount: 0,
467
581
  };
468
582
  }
@@ -470,11 +584,14 @@ async function installPackage(options = {}) {
470
584
  const commandsToInstall = dedupeCommandsForInstall(commands);
471
585
  warnPresetSkillCollisions(skills);
472
586
  const skillsToInstall = dedupeSkillsForInstall(skills);
587
+ warnPresetAgentCollisions(agents);
588
+ const agentsToInstall = dedupeAgentsForInstall(agents);
473
589
  try {
474
590
  if (!options.dryRun) {
475
591
  writeRulesToTargets(rules, assets, config.targets);
476
592
  writeCommandsToTargets(commandsToInstall, config.targets);
477
593
  writeSkillsToTargets(skillsToInstall, config.targets);
594
+ writeAgentsToTargets(agentsToInstall, config.targets);
478
595
  if (mcpServers && Object.keys(mcpServers).length > 0) {
479
596
  writeMcpServersToTargets(mcpServers, config.targets, cwd);
480
597
  }
@@ -486,6 +603,7 @@ async function installPackage(options = {}) {
486
603
  const uniqueCommandCount = new Set(commandsToInstall.map((command) => command.name)).size;
487
604
  const uniqueHookCount = (0, hooks_1.countHooks)(hooks);
488
605
  const uniqueSkillCount = skillsToInstall.length;
606
+ const uniqueAgentCount = agentsToInstall.length;
489
607
  return {
490
608
  success: true,
491
609
  installedRuleCount: uniqueRuleCount,
@@ -493,6 +611,7 @@ async function installPackage(options = {}) {
493
611
  installedAssetCount: assets.length,
494
612
  installedHookCount: uniqueHookCount,
495
613
  installedSkillCount: uniqueSkillCount,
614
+ installedAgentCount: uniqueAgentCount,
496
615
  packagesCount: 1,
497
616
  };
498
617
  }
@@ -505,6 +624,7 @@ async function installPackage(options = {}) {
505
624
  installedAssetCount: 0,
506
625
  installedHookCount: 0,
507
626
  installedSkillCount: 0,
627
+ installedAgentCount: 0,
508
628
  packagesCount: 0,
509
629
  };
510
630
  }
@@ -526,6 +646,7 @@ async function install(options = {}) {
526
646
  installedAssetCount: 0,
527
647
  installedHookCount: 0,
528
648
  installedSkillCount: 0,
649
+ installedAgentCount: 0,
529
650
  packagesCount: 0,
530
651
  };
531
652
  }
@@ -559,6 +680,7 @@ async function installCommand(installOnCI, verbose, dryRun) {
559
680
  const commandCount = result.installedCommandCount;
560
681
  const hookCount = result.installedHookCount;
561
682
  const skillCount = result.installedSkillCount;
683
+ const agentCount = result.installedAgentCount;
562
684
  const ruleMessage = ruleCount > 0 ? `${ruleCount} rule${ruleCount === 1 ? "" : "s"}` : null;
563
685
  const commandMessage = commandCount > 0
564
686
  ? `${commandCount} command${commandCount === 1 ? "" : "s"}`
@@ -567,6 +689,9 @@ async function installCommand(installOnCI, verbose, dryRun) {
567
689
  const skillMessage = skillCount > 0
568
690
  ? `${skillCount} skill${skillCount === 1 ? "" : "s"}`
569
691
  : null;
692
+ const agentMessage = agentCount > 0
693
+ ? `${agentCount} agent${agentCount === 1 ? "" : "s"}`
694
+ : null;
570
695
  const countsParts = [];
571
696
  if (ruleMessage) {
572
697
  countsParts.push(ruleMessage);
@@ -580,6 +705,9 @@ async function installCommand(installOnCI, verbose, dryRun) {
580
705
  if (skillMessage) {
581
706
  countsParts.push(skillMessage);
582
707
  }
708
+ if (agentMessage) {
709
+ countsParts.push(agentMessage);
710
+ }
583
711
  const countsMessage = countsParts.length > 0
584
712
  ? countsParts.join(", ").replace(/, ([^,]*)$/, " and $1")
585
713
  : "0 rules";
@@ -594,8 +722,9 @@ async function installCommand(installOnCI, verbose, dryRun) {
594
722
  else if (ruleCount === 0 &&
595
723
  commandCount === 0 &&
596
724
  hookCount === 0 &&
597
- skillCount === 0) {
598
- console.log("No rules, commands, hooks, or skills installed");
725
+ skillCount === 0 &&
726
+ agentCount === 0) {
727
+ console.log("No rules, commands, hooks, skills, or agents installed");
599
728
  }
600
729
  else if (result.packagesCount > 1) {
601
730
  console.log(`Successfully installed ${countsMessage} across ${result.packagesCount} packages`);
@@ -52,6 +52,7 @@ export interface SkillFile {
52
52
  source: "local" | "preset";
53
53
  presetName?: string;
54
54
  }
55
+ export type AgentFile = ManagedFile;
55
56
  export interface RuleCollection {
56
57
  [target: string]: RuleFile[];
57
58
  }
@@ -61,6 +62,7 @@ export interface ResolvedConfig {
61
62
  commands: CommandFile[];
62
63
  assets: AssetFile[];
63
64
  skills: SkillFile[];
65
+ agents: AgentFile[];
64
66
  mcpServers: MCPServers;
65
67
  hooks: HooksJson;
66
68
  hookFiles: HookFile[];
@@ -80,6 +82,11 @@ export declare function loadAssetsFromDirectory(directoryPath: string, source: "
80
82
  * Each direct subdirectory containing a SKILL.md file is considered a skill
81
83
  */
82
84
  export declare function loadSkillsFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<SkillFile[]>;
85
+ /**
86
+ * Load agents from an agents/ directory
87
+ * Agents are markdown files (.md) with YAML frontmatter
88
+ */
89
+ export declare function loadAgentsFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<AgentFile[]>;
83
90
  /**
84
91
  * Extract namespace from preset path for directory structure
85
92
  * Handles both npm packages and local paths consistently
@@ -95,6 +102,7 @@ export declare function loadAllRules(config: Config, cwd: string): Promise<{
95
102
  commands: CommandFile[];
96
103
  assets: AssetFile[];
97
104
  skills: SkillFile[];
105
+ agents: AgentFile[];
98
106
  mcpServers: MCPServers;
99
107
  hooks: HooksJson;
100
108
  hookFiles: HookFile[];
@@ -12,6 +12,7 @@ exports.loadRulesFromDirectory = loadRulesFromDirectory;
12
12
  exports.loadCommandsFromDirectory = loadCommandsFromDirectory;
13
13
  exports.loadAssetsFromDirectory = loadAssetsFromDirectory;
14
14
  exports.loadSkillsFromDirectory = loadSkillsFromDirectory;
15
+ exports.loadAgentsFromDirectory = loadAgentsFromDirectory;
15
16
  exports.extractNamespaceFromPresetPath = extractNamespaceFromPresetPath;
16
17
  exports.resolvePresetPath = resolvePresetPath;
17
18
  exports.loadPreset = loadPreset;
@@ -98,6 +99,7 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
98
99
  const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "commands"));
99
100
  const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "hooks.json"));
100
101
  const hasSkills = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "skills"));
102
+ const hasAgents = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "agents"));
101
103
  // In workspace mode, root config doesn't need these directories
102
104
  // since packages will have their own configurations
103
105
  if (!isWorkspaceMode &&
@@ -105,8 +107,9 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
105
107
  !hasCommands &&
106
108
  !hasHooks &&
107
109
  !hasSkills &&
110
+ !hasAgents &&
108
111
  !hasPresets) {
109
- throw new Error(`Root directory must contain at least one of: rules/, commands/, skills/, hooks.json, or have presets configured`);
112
+ throw new Error(`Root directory must contain at least one of: rules/, commands/, skills/, agents/, hooks.json, or have presets configured`);
110
113
  }
111
114
  }
112
115
  else if (!isWorkspaceMode && !hasPresets) {
@@ -235,6 +238,35 @@ async function loadSkillsFromDirectory(directoryPath, source, presetName) {
235
238
  }
236
239
  return skills;
237
240
  }
241
+ /**
242
+ * Load agents from an agents/ directory
243
+ * Agents are markdown files (.md) with YAML frontmatter
244
+ */
245
+ async function loadAgentsFromDirectory(directoryPath, source, presetName) {
246
+ const agents = [];
247
+ if (!fs_extra_1.default.existsSync(directoryPath)) {
248
+ return agents;
249
+ }
250
+ const pattern = node_path_1.default.join(directoryPath, "**/*.md").replace(/\\/g, "/");
251
+ const filePaths = await (0, fast_glob_1.default)(pattern, {
252
+ onlyFiles: true,
253
+ absolute: true,
254
+ });
255
+ filePaths.sort();
256
+ for (const filePath of filePaths) {
257
+ const content = await fs_extra_1.default.readFile(filePath, "utf8");
258
+ const relativePath = node_path_1.default.relative(directoryPath, filePath);
259
+ const agentName = relativePath.replace(/\.md$/, "").replace(/\\/g, "/");
260
+ agents.push({
261
+ name: agentName,
262
+ content,
263
+ sourcePath: filePath,
264
+ source,
265
+ presetName,
266
+ });
267
+ }
268
+ return agents;
269
+ }
238
270
  /**
239
271
  * Extract namespace from preset path for directory structure
240
272
  * Handles both npm packages and local paths consistently
@@ -292,8 +324,14 @@ async function loadPreset(presetPath, cwd) {
292
324
  const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "hooks.json"));
293
325
  const hasAssets = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "assets"));
294
326
  const hasSkills = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "skills"));
295
- if (!hasRules && !hasCommands && !hasHooks && !hasAssets && !hasSkills) {
296
- throw new Error(`Preset "${presetPath}" must have at least one of: rules/, commands/, skills/, hooks.json, or assets/`);
327
+ const hasAgents = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "agents"));
328
+ if (!hasRules &&
329
+ !hasCommands &&
330
+ !hasHooks &&
331
+ !hasAssets &&
332
+ !hasSkills &&
333
+ !hasAgents) {
334
+ throw new Error(`Preset "${presetPath}" must have at least one of: rules/, commands/, skills/, agents/, hooks.json, or assets/`);
297
335
  }
298
336
  return {
299
337
  config: presetConfig,
@@ -305,6 +343,7 @@ async function loadAllRules(config, cwd) {
305
343
  const allCommands = [];
306
344
  const allAssets = [];
307
345
  const allSkills = [];
346
+ const allAgents = [];
308
347
  const allHookFiles = [];
309
348
  const allHooksConfigs = [];
310
349
  let mergedMcpServers = { ...config.mcpServers };
@@ -342,6 +381,12 @@ async function loadAllRules(config, cwd) {
342
381
  const localSkills = await loadSkillsFromDirectory(skillsPath, "local");
343
382
  allSkills.push(...localSkills);
344
383
  }
384
+ // Load agents from agents/ subdirectory
385
+ const agentsPath = node_path_1.default.join(rootPath, "agents");
386
+ if (fs_extra_1.default.existsSync(agentsPath)) {
387
+ const localAgents = await loadAgentsFromDirectory(agentsPath, "local");
388
+ allAgents.push(...localAgents);
389
+ }
345
390
  }
346
391
  // Load presets
347
392
  if (config.presets) {
@@ -379,6 +424,12 @@ async function loadAllRules(config, cwd) {
379
424
  const presetSkills = await loadSkillsFromDirectory(presetSkillsPath, "preset", presetPath);
380
425
  allSkills.push(...presetSkills);
381
426
  }
427
+ // Load preset agents from agents/ subdirectory
428
+ const presetAgentsPath = node_path_1.default.join(presetRootDir, "agents");
429
+ if (fs_extra_1.default.existsSync(presetAgentsPath)) {
430
+ const presetAgents = await loadAgentsFromDirectory(presetAgentsPath, "preset", presetPath);
431
+ allAgents.push(...presetAgents);
432
+ }
382
433
  // Merge MCP servers from preset
383
434
  if (preset.config.mcpServers) {
384
435
  mergedMcpServers = mergePresetMcpServers(mergedMcpServers, preset.config.mcpServers);
@@ -392,6 +443,7 @@ async function loadAllRules(config, cwd) {
392
443
  commands: allCommands,
393
444
  assets: allAssets,
394
445
  skills: allSkills,
446
+ agents: allAgents,
395
447
  mcpServers: mergedMcpServers,
396
448
  hooks: mergedHooks,
397
449
  hookFiles: allHookFiles,
@@ -451,13 +503,14 @@ async function loadConfig(cwd) {
451
503
  const isWorkspaces = resolveWorkspaces(config, configResult.filepath, workingDir);
452
504
  validateConfig(config, configResult.filepath, workingDir, isWorkspaces);
453
505
  const configWithDefaults = applyDefaults(config, isWorkspaces);
454
- const { rules, commands, assets, skills, mcpServers, hooks, hookFiles } = await loadAllRules(configWithDefaults, workingDir);
506
+ const { rules, commands, assets, skills, agents, mcpServers, hooks, hookFiles, } = await loadAllRules(configWithDefaults, workingDir);
455
507
  return {
456
508
  config: configWithDefaults,
457
509
  rules,
458
510
  commands,
459
511
  assets,
460
512
  skills,
513
+ agents,
461
514
  mcpServers,
462
515
  hooks,
463
516
  hookFiles,
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Check if a resolved path is safely within the specified base directory.
3
+ * This prevents path traversal attacks where malicious paths like "../../../etc"
4
+ * or absolute paths could escape the intended directory.
5
+ *
6
+ * @param baseDir - The directory that should contain the path
7
+ * @param relativePath - The potentially untrusted relative path
8
+ * @returns The safely resolved full path, or null if the path would escape baseDir
9
+ */
10
+ export declare function resolveSafePath(baseDir: string, relativePath: string): string | null;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveSafePath = resolveSafePath;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ /**
9
+ * Check if a resolved path is safely within the specified base directory.
10
+ * This prevents path traversal attacks where malicious paths like "../../../etc"
11
+ * or absolute paths could escape the intended directory.
12
+ *
13
+ * @param baseDir - The directory that should contain the path
14
+ * @param relativePath - The potentially untrusted relative path
15
+ * @returns The safely resolved full path, or null if the path would escape baseDir
16
+ */
17
+ function resolveSafePath(baseDir, relativePath) {
18
+ // Resolve both to absolute paths
19
+ const resolvedBase = node_path_1.default.resolve(baseDir);
20
+ const resolvedTarget = node_path_1.default.resolve(baseDir, relativePath);
21
+ // The resolved path must start with the base directory + separator
22
+ // This ensures it's truly inside the directory, not a sibling with similar prefix
23
+ // e.g., /foo/bar should not match /foo/bar-other
24
+ if (!resolvedTarget.startsWith(resolvedBase + node_path_1.default.sep)) {
25
+ return null;
26
+ }
27
+ return resolvedTarget;
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicm",
3
- "version": "0.20.0",
3
+ "version": "0.20.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",