aicm 0.18.0 → 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.
@@ -11,10 +11,10 @@ exports.validateConfig = validateConfig;
11
11
  exports.loadRulesFromDirectory = loadRulesFromDirectory;
12
12
  exports.loadCommandsFromDirectory = loadCommandsFromDirectory;
13
13
  exports.loadAssetsFromDirectory = loadAssetsFromDirectory;
14
+ exports.extractNamespaceFromPresetPath = extractNamespaceFromPresetPath;
14
15
  exports.resolvePresetPath = resolvePresetPath;
15
16
  exports.loadPreset = loadPreset;
16
17
  exports.loadAllRules = loadAllRules;
17
- exports.applyOverrides = applyOverrides;
18
18
  exports.loadConfigFile = loadConfigFile;
19
19
  exports.loadConfig = loadConfig;
20
20
  exports.saveConfig = saveConfig;
@@ -22,12 +22,11 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
22
22
  const node_path_1 = __importDefault(require("node:path"));
23
23
  const cosmiconfig_1 = require("cosmiconfig");
24
24
  const fast_glob_1 = __importDefault(require("fast-glob"));
25
+ const hooks_1 = require("./hooks");
25
26
  exports.ALLOWED_CONFIG_KEYS = [
26
- "rulesDir",
27
- "commandsDir",
27
+ "rootDir",
28
28
  "targets",
29
29
  "presets",
30
- "overrides",
31
30
  "mcpServers",
32
31
  "workspaces",
33
32
  "skipInstall",
@@ -63,11 +62,9 @@ function resolveWorkspaces(config, configFilePath, cwd) {
63
62
  }
64
63
  function applyDefaults(config, workspaces) {
65
64
  return {
66
- rulesDir: config.rulesDir,
67
- commandsDir: config.commandsDir,
65
+ rootDir: config.rootDir,
68
66
  targets: config.targets || ["cursor"],
69
67
  presets: config.presets || [],
70
- overrides: config.overrides || {},
71
68
  mcpServers: config.mcpServers || {},
72
69
  workspaces,
73
70
  skipInstall: config.skipInstall || false,
@@ -81,36 +78,37 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
81
78
  if (unknownKeys.length > 0) {
82
79
  throw new Error(`Invalid configuration at ${configFilePath}: unknown keys: ${unknownKeys.join(", ")}`);
83
80
  }
84
- // Validate that either rulesDir or presets is provided
85
- const hasRulesDir = "rulesDir" in config && typeof config.rulesDir === "string";
86
- const hasCommandsDir = "commandsDir" in config && typeof config.commandsDir === "string";
81
+ // Validate rootDir
82
+ const hasRootDir = "rootDir" in config && typeof config.rootDir === "string";
87
83
  const hasPresets = "presets" in config &&
88
84
  Array.isArray(config.presets) &&
89
85
  config.presets.length > 0;
90
- // In workspace mode, root config doesn't need rulesDir or presets
91
- // since packages will have their own configurations
92
- if (!isWorkspaceMode && !hasRulesDir && !hasPresets && !hasCommandsDir) {
93
- throw new Error(`At least one of rulesDir, commandsDir, or presets must be specified in config at ${configFilePath}`);
94
- }
95
- // Validate rulesDir if provided
96
- if (hasRulesDir) {
97
- const rulesPath = node_path_1.default.resolve(cwd, config.rulesDir);
98
- if (!fs_extra_1.default.existsSync(rulesPath)) {
99
- throw new Error(`Rules directory does not exist: ${rulesPath}`);
100
- }
101
- if (!fs_extra_1.default.statSync(rulesPath).isDirectory()) {
102
- throw new Error(`Rules path is not a directory: ${rulesPath}`);
86
+ if (hasRootDir) {
87
+ const rootPath = node_path_1.default.resolve(cwd, config.rootDir);
88
+ if (!fs_extra_1.default.existsSync(rootPath)) {
89
+ throw new Error(`Root directory does not exist: ${rootPath}`);
103
90
  }
104
- }
105
- if (hasCommandsDir) {
106
- const commandsPath = node_path_1.default.resolve(cwd, config.commandsDir);
107
- if (!fs_extra_1.default.existsSync(commandsPath)) {
108
- throw new Error(`Commands directory does not exist: ${commandsPath}`);
91
+ if (!fs_extra_1.default.statSync(rootPath).isDirectory()) {
92
+ throw new Error(`Root path is not a directory: ${rootPath}`);
109
93
  }
110
- if (!fs_extra_1.default.statSync(commandsPath).isDirectory()) {
111
- throw new Error(`Commands path is not a directory: ${commandsPath}`);
94
+ // Check for at least one valid subdirectory or file
95
+ const hasRules = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "rules"));
96
+ const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "commands"));
97
+ const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "hooks.json"));
98
+ // In workspace mode, root config doesn't need these directories
99
+ // since packages will have their own configurations
100
+ if (!isWorkspaceMode &&
101
+ !hasRules &&
102
+ !hasCommands &&
103
+ !hasHooks &&
104
+ !hasPresets) {
105
+ throw new Error(`Root directory must contain at least one of: rules/, commands/, hooks.json, or have presets configured`);
112
106
  }
113
107
  }
108
+ else if (!isWorkspaceMode && !hasPresets) {
109
+ // If no rootDir specified and not in workspace mode, must have presets
110
+ throw new Error(`At least one of rootDir or presets must be specified in config at ${configFilePath}`);
111
+ }
114
112
  if ("targets" in config) {
115
113
  if (!Array.isArray(config.targets)) {
116
114
  throw new Error(`targets must be an array in config at ${configFilePath}`);
@@ -124,22 +122,21 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
124
122
  }
125
123
  }
126
124
  }
127
- // Validate override rule names will be checked after rule resolution
128
125
  }
129
- async function loadRulesFromDirectory(rulesDir, source, presetName) {
126
+ async function loadRulesFromDirectory(directoryPath, source, presetName) {
130
127
  const rules = [];
131
- if (!fs_extra_1.default.existsSync(rulesDir)) {
128
+ if (!fs_extra_1.default.existsSync(directoryPath)) {
132
129
  return rules;
133
130
  }
134
- const pattern = node_path_1.default.join(rulesDir, "**/*.mdc").replace(/\\/g, "/");
131
+ const pattern = node_path_1.default.join(directoryPath, "**/*.mdc").replace(/\\/g, "/");
135
132
  const filePaths = await (0, fast_glob_1.default)(pattern, {
136
133
  onlyFiles: true,
137
134
  absolute: true,
138
135
  });
139
136
  for (const filePath of filePaths) {
140
137
  const content = await fs_extra_1.default.readFile(filePath, "utf8");
141
- // Preserve directory structure by using relative path from rulesDir
142
- const relativePath = node_path_1.default.relative(rulesDir, filePath);
138
+ // Preserve directory structure by using relative path from source directory
139
+ const relativePath = node_path_1.default.relative(directoryPath, filePath);
143
140
  const ruleName = relativePath.replace(/\.mdc$/, "").replace(/\\/g, "/");
144
141
  rules.push({
145
142
  name: ruleName,
@@ -151,12 +148,12 @@ async function loadRulesFromDirectory(rulesDir, source, presetName) {
151
148
  }
152
149
  return rules;
153
150
  }
154
- async function loadCommandsFromDirectory(commandsDir, source, presetName) {
151
+ async function loadCommandsFromDirectory(directoryPath, source, presetName) {
155
152
  const commands = [];
156
- if (!fs_extra_1.default.existsSync(commandsDir)) {
153
+ if (!fs_extra_1.default.existsSync(directoryPath)) {
157
154
  return commands;
158
155
  }
159
- const pattern = node_path_1.default.join(commandsDir, "**/*.md").replace(/\\/g, "/");
156
+ const pattern = node_path_1.default.join(directoryPath, "**/*.md").replace(/\\/g, "/");
160
157
  const filePaths = await (0, fast_glob_1.default)(pattern, {
161
158
  onlyFiles: true,
162
159
  absolute: true,
@@ -164,7 +161,7 @@ async function loadCommandsFromDirectory(commandsDir, source, presetName) {
164
161
  filePaths.sort();
165
162
  for (const filePath of filePaths) {
166
163
  const content = await fs_extra_1.default.readFile(filePath, "utf8");
167
- const relativePath = node_path_1.default.relative(commandsDir, filePath);
164
+ const relativePath = node_path_1.default.relative(directoryPath, filePath);
168
165
  const commandName = relativePath.replace(/\.md$/, "").replace(/\\/g, "/");
169
166
  commands.push({
170
167
  name: commandName,
@@ -176,13 +173,13 @@ async function loadCommandsFromDirectory(commandsDir, source, presetName) {
176
173
  }
177
174
  return commands;
178
175
  }
179
- async function loadAssetsFromDirectory(rulesDir, source, presetName) {
176
+ async function loadAssetsFromDirectory(directoryPath, source, presetName) {
180
177
  const assets = [];
181
- if (!fs_extra_1.default.existsSync(rulesDir)) {
178
+ if (!fs_extra_1.default.existsSync(directoryPath)) {
182
179
  return assets;
183
180
  }
184
181
  // Find all files except .mdc files and hidden files
185
- const pattern = node_path_1.default.join(rulesDir, "**/*").replace(/\\/g, "/");
182
+ const pattern = node_path_1.default.join(directoryPath, "**/*").replace(/\\/g, "/");
186
183
  const filePaths = await (0, fast_glob_1.default)(pattern, {
187
184
  onlyFiles: true,
188
185
  absolute: true,
@@ -190,8 +187,8 @@ async function loadAssetsFromDirectory(rulesDir, source, presetName) {
190
187
  });
191
188
  for (const filePath of filePaths) {
192
189
  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);
190
+ // Preserve directory structure by using relative path from source directory
191
+ const relativePath = node_path_1.default.relative(directoryPath, filePath);
195
192
  // Keep extension for assets
196
193
  const assetName = relativePath.replace(/\\/g, "/");
197
194
  assets.push({
@@ -204,6 +201,19 @@ async function loadAssetsFromDirectory(rulesDir, source, presetName) {
204
201
  }
205
202
  return assets;
206
203
  }
204
+ /**
205
+ * Extract namespace from preset path for directory structure
206
+ * Handles both npm packages and local paths consistently
207
+ */
208
+ function extractNamespaceFromPresetPath(presetPath) {
209
+ // Special case: npm package names always use forward slashes, regardless of platform
210
+ if (presetPath.startsWith("@")) {
211
+ // For scoped packages like @scope/package/subdir, create nested directories
212
+ return presetPath.split("/");
213
+ }
214
+ const parts = presetPath.split(node_path_1.default.sep);
215
+ return parts.filter((part) => part.length > 0 && part !== "." && part !== "..");
216
+ }
207
217
  function resolvePresetPath(presetPath, cwd) {
208
218
  // Support specifying aicm.json directory and load the config from it
209
219
  if (!presetPath.endsWith(".json")) {
@@ -241,99 +251,103 @@ async function loadPreset(presetPath, cwd) {
241
251
  throw new Error(`Failed to load preset "${presetPath}": ${error instanceof Error ? error.message : "Unknown error"}`);
242
252
  }
243
253
  const presetDir = node_path_1.default.dirname(resolvedPresetPath);
244
- const presetRulesDir = presetConfig.rulesDir
245
- ? node_path_1.default.resolve(presetDir, presetConfig.rulesDir)
246
- : undefined;
247
- const presetCommandsDir = presetConfig.commandsDir
248
- ? node_path_1.default.resolve(presetDir, presetConfig.commandsDir)
249
- : undefined;
250
- if (!presetRulesDir && !presetCommandsDir) {
251
- throw new Error(`Preset "${presetPath}" must have a rulesDir or commandsDir specified`);
254
+ const presetRootDir = node_path_1.default.resolve(presetDir, presetConfig.rootDir || "./");
255
+ // Check for at least one valid subdirectory
256
+ const hasRules = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "rules"));
257
+ const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "commands"));
258
+ const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "hooks.json"));
259
+ const hasAssets = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "assets"));
260
+ if (!hasRules && !hasCommands && !hasHooks && !hasAssets) {
261
+ throw new Error(`Preset "${presetPath}" must have at least one of: rules/, commands/, hooks.json, or assets/`);
252
262
  }
253
263
  return {
254
264
  config: presetConfig,
255
- rulesDir: presetRulesDir,
256
- commandsDir: presetCommandsDir,
265
+ rootDir: presetRootDir,
257
266
  };
258
267
  }
259
268
  async function loadAllRules(config, cwd) {
260
269
  const allRules = [];
261
270
  const allCommands = [];
262
271
  const allAssets = [];
272
+ const allHookFiles = [];
273
+ const allHooksConfigs = [];
263
274
  let mergedMcpServers = { ...config.mcpServers };
264
- // Load local rules only if rulesDir is provided
265
- if (config.rulesDir) {
266
- const localRulesPath = node_path_1.default.resolve(cwd, config.rulesDir);
267
- const localRules = await loadRulesFromDirectory(localRulesPath, "local");
268
- const localAssets = await loadAssetsFromDirectory(localRulesPath, "local");
269
- allRules.push(...localRules);
270
- allAssets.push(...localAssets);
271
- }
272
- if (config.commandsDir) {
273
- const localCommandsPath = node_path_1.default.resolve(cwd, config.commandsDir);
274
- const localCommands = await loadCommandsFromDirectory(localCommandsPath, "local");
275
- allCommands.push(...localCommands);
275
+ // Load local files from rootDir only if specified
276
+ if (config.rootDir) {
277
+ const rootPath = node_path_1.default.resolve(cwd, config.rootDir);
278
+ // Load rules from rules/ subdirectory
279
+ const rulesPath = node_path_1.default.join(rootPath, "rules");
280
+ if (fs_extra_1.default.existsSync(rulesPath)) {
281
+ const localRules = await loadRulesFromDirectory(rulesPath, "local");
282
+ allRules.push(...localRules);
283
+ }
284
+ // Load commands from commands/ subdirectory
285
+ const commandsPath = node_path_1.default.join(rootPath, "commands");
286
+ if (fs_extra_1.default.existsSync(commandsPath)) {
287
+ const localCommands = await loadCommandsFromDirectory(commandsPath, "local");
288
+ allCommands.push(...localCommands);
289
+ }
290
+ // Load hooks from hooks.json (sibling to hooks/ directory)
291
+ const hooksFilePath = node_path_1.default.join(rootPath, "hooks.json");
292
+ if (fs_extra_1.default.existsSync(hooksFilePath)) {
293
+ const { config: localHooksConfig, files: localHookFiles } = await (0, hooks_1.loadHooksFromDirectory)(rootPath, "local");
294
+ allHooksConfigs.push(localHooksConfig);
295
+ allHookFiles.push(...localHookFiles);
296
+ }
297
+ // Load assets from assets/ subdirectory
298
+ const assetsPath = node_path_1.default.join(rootPath, "assets");
299
+ if (fs_extra_1.default.existsSync(assetsPath)) {
300
+ const localAssets = await loadAssetsFromDirectory(assetsPath, "local");
301
+ allAssets.push(...localAssets);
302
+ }
276
303
  }
304
+ // Load presets
277
305
  if (config.presets) {
278
306
  for (const presetPath of config.presets) {
279
307
  const preset = await loadPreset(presetPath, cwd);
280
- if (preset.rulesDir) {
281
- const presetRules = await loadRulesFromDirectory(preset.rulesDir, "preset", presetPath);
282
- const presetAssets = await loadAssetsFromDirectory(preset.rulesDir, "preset", presetPath);
308
+ const presetRootDir = preset.rootDir;
309
+ // Load preset rules from rules/ subdirectory
310
+ const presetRulesPath = node_path_1.default.join(presetRootDir, "rules");
311
+ if (fs_extra_1.default.existsSync(presetRulesPath)) {
312
+ const presetRules = await loadRulesFromDirectory(presetRulesPath, "preset", presetPath);
283
313
  allRules.push(...presetRules);
284
- allAssets.push(...presetAssets);
285
314
  }
286
- if (preset.commandsDir) {
287
- const presetCommands = await loadCommandsFromDirectory(preset.commandsDir, "preset", presetPath);
315
+ // Load preset commands from commands/ subdirectory
316
+ const presetCommandsPath = node_path_1.default.join(presetRootDir, "commands");
317
+ if (fs_extra_1.default.existsSync(presetCommandsPath)) {
318
+ const presetCommands = await loadCommandsFromDirectory(presetCommandsPath, "preset", presetPath);
288
319
  allCommands.push(...presetCommands);
289
320
  }
321
+ // Load preset hooks from hooks.json (sibling to hooks/ directory)
322
+ const presetHooksFile = node_path_1.default.join(presetRootDir, "hooks.json");
323
+ if (fs_extra_1.default.existsSync(presetHooksFile)) {
324
+ const { config: presetHooksConfig, files: presetHookFiles } = await (0, hooks_1.loadHooksFromDirectory)(presetRootDir, "preset", presetPath);
325
+ allHooksConfigs.push(presetHooksConfig);
326
+ allHookFiles.push(...presetHookFiles);
327
+ }
328
+ // Load preset assets from assets/ subdirectory
329
+ const presetAssetsPath = node_path_1.default.join(presetRootDir, "assets");
330
+ if (fs_extra_1.default.existsSync(presetAssetsPath)) {
331
+ const presetAssets = await loadAssetsFromDirectory(presetAssetsPath, "preset", presetPath);
332
+ allAssets.push(...presetAssets);
333
+ }
290
334
  // Merge MCP servers from preset
291
335
  if (preset.config.mcpServers) {
292
336
  mergedMcpServers = mergePresetMcpServers(mergedMcpServers, preset.config.mcpServers);
293
337
  }
294
338
  }
295
339
  }
340
+ // Merge all hooks configurations
341
+ const mergedHooks = (0, hooks_1.mergeHooksConfigs)(allHooksConfigs);
296
342
  return {
297
343
  rules: allRules,
298
344
  commands: allCommands,
299
345
  assets: allAssets,
300
346
  mcpServers: mergedMcpServers,
347
+ hooks: mergedHooks,
348
+ hookFiles: allHookFiles,
301
349
  };
302
350
  }
303
- function applyOverrides(files, overrides, cwd) {
304
- // Validate that all override names exist in the resolved files
305
- for (const name of Object.keys(overrides)) {
306
- if (!files.some((file) => file.name === name)) {
307
- throw new Error(`Override entry "${name}" does not exist in resolved files`);
308
- }
309
- }
310
- const fileMap = new Map();
311
- for (const file of files) {
312
- fileMap.set(file.name, file);
313
- }
314
- for (const [name, override] of Object.entries(overrides)) {
315
- if (override === false) {
316
- fileMap.delete(name);
317
- }
318
- else if (typeof override === "string") {
319
- const overridePath = node_path_1.default.resolve(cwd, override);
320
- if (!fs_extra_1.default.existsSync(overridePath)) {
321
- throw new Error(`Override file not found: ${override} in ${cwd}`);
322
- }
323
- const content = fs_extra_1.default.readFileSync(overridePath, "utf8");
324
- const existing = fileMap.get(name);
325
- fileMap.set(name, {
326
- ...(existing !== null && existing !== void 0 ? existing : {}),
327
- name,
328
- content,
329
- sourcePath: overridePath,
330
- source: "local",
331
- presetName: undefined,
332
- });
333
- }
334
- }
335
- return Array.from(fileMap.values());
336
- }
337
351
  /**
338
352
  * Merge preset MCP servers with local config MCP servers
339
353
  * Local config takes precedence over preset config
@@ -347,7 +361,7 @@ function mergePresetMcpServers(configMcpServers, presetMcpServers) {
347
361
  delete newMcpServers[serverName];
348
362
  continue;
349
363
  }
350
- // Only add if not already defined in config (override handled by config)
364
+ // Only add if not already defined in config (local config takes precedence)
351
365
  if (!Object.prototype.hasOwnProperty.call(newMcpServers, serverName)) {
352
366
  newMcpServers[serverName] = serverConfig;
353
367
  }
@@ -376,34 +390,15 @@ async function loadConfig(cwd) {
376
390
  const isWorkspaces = resolveWorkspaces(config, configResult.filepath, workingDir);
377
391
  validateConfig(config, configResult.filepath, workingDir, isWorkspaces);
378
392
  const configWithDefaults = applyDefaults(config, isWorkspaces);
379
- const { rules, commands, assets, mcpServers } = await loadAllRules(configWithDefaults, workingDir);
380
- let rulesWithOverrides = rules;
381
- let commandsWithOverrides = commands;
382
- // Note: Assets are not currently supported in overrides as they are binary/varied files
383
- if (configWithDefaults.overrides) {
384
- const overrides = configWithDefaults.overrides;
385
- const ruleNames = new Set(rules.map((rule) => rule.name));
386
- const commandNames = new Set(commands.map((command) => command.name));
387
- for (const overrideName of Object.keys(overrides)) {
388
- if (!ruleNames.has(overrideName) && !commandNames.has(overrideName)) {
389
- throw new Error(`Override entry "${overrideName}" does not exist in resolved rules or commands`);
390
- }
391
- }
392
- const ruleOverrides = Object.fromEntries(Object.entries(overrides).filter(([name]) => ruleNames.has(name)));
393
- const commandOverrides = Object.fromEntries(Object.entries(overrides).filter(([name]) => commandNames.has(name)));
394
- if (Object.keys(ruleOverrides).length > 0) {
395
- rulesWithOverrides = applyOverrides(rules, ruleOverrides, workingDir);
396
- }
397
- if (Object.keys(commandOverrides).length > 0) {
398
- commandsWithOverrides = applyOverrides(commands, commandOverrides, workingDir);
399
- }
400
- }
393
+ const { rules, commands, assets, mcpServers, hooks, hookFiles } = await loadAllRules(configWithDefaults, workingDir);
401
394
  return {
402
395
  config: configWithDefaults,
403
- rules: rulesWithOverrides,
404
- commands: commandsWithOverrides,
396
+ rules,
397
+ commands,
405
398
  assets,
406
399
  mcpServers,
400
+ hooks,
401
+ hookFiles,
407
402
  };
408
403
  }
409
404
  function saveConfig(config, cwd) {
@@ -0,0 +1,50 @@
1
+ export type HookType = "beforeShellExecution" | "beforeMCPExecution" | "afterShellExecution" | "afterMCPExecution" | "beforeReadFile" | "afterFileEdit" | "beforeSubmitPrompt" | "stop";
2
+ export interface HookCommand {
3
+ command: string;
4
+ }
5
+ export interface HooksJson {
6
+ version: number;
7
+ hooks: {
8
+ [K in HookType]?: HookCommand[];
9
+ };
10
+ }
11
+ export interface HookFile {
12
+ name: string;
13
+ basename: string;
14
+ content: Buffer;
15
+ sourcePath: string;
16
+ source: "local" | "preset";
17
+ presetName?: string;
18
+ }
19
+ /**
20
+ * Load hooks configuration from a root directory
21
+ * The directory should contain hooks.json (sibling to hooks/ directory)
22
+ * All files in the hooks/ subdirectory are copied during installation
23
+ */
24
+ export declare function loadHooksFromDirectory(rootDir: string, source: "local" | "preset", presetName?: string): Promise<{
25
+ config: HooksJson;
26
+ files: HookFile[];
27
+ }>;
28
+ /**
29
+ * Merge multiple hooks configurations into one
30
+ * Later configurations override earlier ones for the same hook type
31
+ */
32
+ export declare function mergeHooksConfigs(configs: HooksJson[]): HooksJson;
33
+ /**
34
+ * Rewrite command paths to point to the managed hooks directory (hooks/aicm/)
35
+ * At this point, paths are already namespaced filenames from loadHooksFromDirectory
36
+ */
37
+ export declare function rewriteHooksConfigToManagedDir(hooksConfig: HooksJson): HooksJson;
38
+ /**
39
+ * Count the number of hook entries in a hooks configuration
40
+ */
41
+ export declare function countHooks(hooksConfig: HooksJson): number;
42
+ /**
43
+ * Dedupe hook files by namespaced path, warn on content conflicts
44
+ * Presets are namespaced with directories, so same basename from different presets won't collide
45
+ */
46
+ export declare function dedupeHookFiles(hookFiles: HookFile[]): HookFile[];
47
+ /**
48
+ * Write hooks configuration and files to Cursor target
49
+ */
50
+ export declare function writeHooksToCursor(hooksConfig: HooksJson, hookFiles: HookFile[], cwd: string): void;