aicm 0.19.1 → 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 +190 -4
- package/dist/bin/aicm.js +0 -0
- package/dist/commands/clean.js +115 -0
- package/dist/commands/install-workspaces.js +148 -3
- package/dist/commands/install.d.ts +35 -1
- package/dist/commands/install.js +253 -3
- package/dist/utils/config.d.ts +21 -0
- package/dist/utils/config.js +106 -4
- package/dist/utils/safe-path.d.ts +10 -0
- package/dist/utils/safe-path.js +28 -0
- package/package.json +18 -16
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ResolvedConfig, CommandFile, AssetFile, 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()
|
|
@@ -49,6 +49,14 @@ export interface InstallResult {
|
|
|
49
49
|
* Number of hooks installed
|
|
50
50
|
*/
|
|
51
51
|
installedHookCount: number;
|
|
52
|
+
/**
|
|
53
|
+
* Number of skills installed
|
|
54
|
+
*/
|
|
55
|
+
installedSkillCount: number;
|
|
56
|
+
/**
|
|
57
|
+
* Number of agents installed
|
|
58
|
+
*/
|
|
59
|
+
installedAgentCount: number;
|
|
52
60
|
/**
|
|
53
61
|
* Number of packages installed
|
|
54
62
|
*/
|
|
@@ -56,6 +64,32 @@ export interface InstallResult {
|
|
|
56
64
|
}
|
|
57
65
|
export declare function writeAssetsToTargets(assets: AssetFile[], targets: SupportedTarget[]): void;
|
|
58
66
|
export declare function writeCommandsToTargets(commands: CommandFile[], targets: SupportedTarget[]): void;
|
|
67
|
+
/**
|
|
68
|
+
* Write skills to all supported target directories
|
|
69
|
+
*/
|
|
70
|
+
export declare function writeSkillsToTargets(skills: SkillFile[], targets: SupportedTarget[]): void;
|
|
71
|
+
/**
|
|
72
|
+
* Warn about skill name collisions from different presets
|
|
73
|
+
*/
|
|
74
|
+
export declare function warnPresetSkillCollisions(skills: SkillFile[]): void;
|
|
75
|
+
/**
|
|
76
|
+
* Dedupe skills by name (last one wins)
|
|
77
|
+
*/
|
|
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[];
|
|
59
93
|
export declare function warnPresetCommandCollisions(commands: CommandFile[]): void;
|
|
60
94
|
export declare function dedupeCommandsForInstall(commands: CommandFile[]): CommandFile[];
|
|
61
95
|
/**
|
package/dist/commands/install.js
CHANGED
|
@@ -5,6 +5,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.writeAssetsToTargets = writeAssetsToTargets;
|
|
7
7
|
exports.writeCommandsToTargets = writeCommandsToTargets;
|
|
8
|
+
exports.writeSkillsToTargets = writeSkillsToTargets;
|
|
9
|
+
exports.warnPresetSkillCollisions = warnPresetSkillCollisions;
|
|
10
|
+
exports.dedupeSkillsForInstall = dedupeSkillsForInstall;
|
|
11
|
+
exports.writeAgentsToTargets = writeAgentsToTargets;
|
|
12
|
+
exports.warnPresetAgentCollisions = warnPresetAgentCollisions;
|
|
13
|
+
exports.dedupeAgentsForInstall = dedupeAgentsForInstall;
|
|
8
14
|
exports.warnPresetCommandCollisions = warnPresetCommandCollisions;
|
|
9
15
|
exports.dedupeCommandsForInstall = dedupeCommandsForInstall;
|
|
10
16
|
exports.writeMcpServersToFile = writeMcpServersToFile;
|
|
@@ -228,6 +234,214 @@ function writeCommandsToTargets(commands, targets) {
|
|
|
228
234
|
// Other targets do not support commands yet
|
|
229
235
|
}
|
|
230
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Get the skills installation path for a target
|
|
239
|
+
* Returns null for targets that don't support skills
|
|
240
|
+
*/
|
|
241
|
+
function getSkillsTargetPath(target) {
|
|
242
|
+
const projectDir = process.cwd();
|
|
243
|
+
switch (target) {
|
|
244
|
+
case "cursor":
|
|
245
|
+
return node_path_1.default.join(projectDir, ".cursor", "skills");
|
|
246
|
+
case "claude":
|
|
247
|
+
return node_path_1.default.join(projectDir, ".claude", "skills");
|
|
248
|
+
case "codex":
|
|
249
|
+
return node_path_1.default.join(projectDir, ".codex", "skills");
|
|
250
|
+
case "windsurf":
|
|
251
|
+
// Windsurf does not support skills
|
|
252
|
+
return null;
|
|
253
|
+
default:
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Write a single skill to the target directory
|
|
259
|
+
* Copies the entire skill directory and writes .aicm.json metadata
|
|
260
|
+
*/
|
|
261
|
+
function writeSkillToTarget(skill, targetSkillsDir) {
|
|
262
|
+
const skillTargetPath = node_path_1.default.join(targetSkillsDir, skill.name);
|
|
263
|
+
// Remove existing skill directory if it exists (to ensure clean install)
|
|
264
|
+
if (fs_extra_1.default.existsSync(skillTargetPath)) {
|
|
265
|
+
fs_extra_1.default.removeSync(skillTargetPath);
|
|
266
|
+
}
|
|
267
|
+
// Copy the entire skill directory
|
|
268
|
+
fs_extra_1.default.copySync(skill.sourcePath, skillTargetPath);
|
|
269
|
+
// Write .aicm.json metadata file
|
|
270
|
+
// The presence of this file indicates the skill is managed by aicm
|
|
271
|
+
const metadata = {
|
|
272
|
+
source: skill.source,
|
|
273
|
+
};
|
|
274
|
+
if (skill.presetName) {
|
|
275
|
+
metadata.presetName = skill.presetName;
|
|
276
|
+
}
|
|
277
|
+
const metadataPath = node_path_1.default.join(skillTargetPath, ".aicm.json");
|
|
278
|
+
fs_extra_1.default.writeJsonSync(metadataPath, metadata, { spaces: 2 });
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Write skills to all supported target directories
|
|
282
|
+
*/
|
|
283
|
+
function writeSkillsToTargets(skills, targets) {
|
|
284
|
+
if (skills.length === 0)
|
|
285
|
+
return;
|
|
286
|
+
for (const target of targets) {
|
|
287
|
+
const targetSkillsDir = getSkillsTargetPath(target);
|
|
288
|
+
if (!targetSkillsDir) {
|
|
289
|
+
// Target doesn't support skills
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
// Ensure the skills directory exists
|
|
293
|
+
fs_extra_1.default.ensureDirSync(targetSkillsDir);
|
|
294
|
+
for (const skill of skills) {
|
|
295
|
+
writeSkillToTarget(skill, targetSkillsDir);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Warn about skill name collisions from different presets
|
|
301
|
+
*/
|
|
302
|
+
function warnPresetSkillCollisions(skills) {
|
|
303
|
+
const collisions = new Map();
|
|
304
|
+
for (const skill of skills) {
|
|
305
|
+
if (!skill.presetName)
|
|
306
|
+
continue;
|
|
307
|
+
const entry = collisions.get(skill.name);
|
|
308
|
+
if (entry) {
|
|
309
|
+
entry.presets.add(skill.presetName);
|
|
310
|
+
entry.lastPreset = skill.presetName;
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
collisions.set(skill.name, {
|
|
314
|
+
presets: new Set([skill.presetName]),
|
|
315
|
+
lastPreset: skill.presetName,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
for (const [skillName, { presets, lastPreset }] of collisions) {
|
|
320
|
+
if (presets.size > 1) {
|
|
321
|
+
const presetList = Array.from(presets).sort().join(", ");
|
|
322
|
+
console.warn(chalk_1.default.yellow(`Warning: multiple presets provide the "${skillName}" skill (${presetList}). Using definition from ${lastPreset}.`));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Dedupe skills by name (last one wins)
|
|
328
|
+
*/
|
|
329
|
+
function dedupeSkillsForInstall(skills) {
|
|
330
|
+
const unique = new Map();
|
|
331
|
+
for (const skill of skills) {
|
|
332
|
+
unique.set(skill.name, skill);
|
|
333
|
+
}
|
|
334
|
+
return Array.from(unique.values());
|
|
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
|
+
}
|
|
231
445
|
function warnPresetCommandCollisions(commands) {
|
|
232
446
|
const collisions = new Map();
|
|
233
447
|
for (const command of commands) {
|
|
@@ -348,10 +562,12 @@ async function installPackage(options = {}) {
|
|
|
348
562
|
installedCommandCount: 0,
|
|
349
563
|
installedAssetCount: 0,
|
|
350
564
|
installedHookCount: 0,
|
|
565
|
+
installedSkillCount: 0,
|
|
566
|
+
installedAgentCount: 0,
|
|
351
567
|
packagesCount: 0,
|
|
352
568
|
};
|
|
353
569
|
}
|
|
354
|
-
const { config, rules, commands, assets, mcpServers, hooks, hookFiles } = resolvedConfig;
|
|
570
|
+
const { config, rules, commands, assets, skills, agents, mcpServers, hooks, hookFiles, } = resolvedConfig;
|
|
355
571
|
if (config.skipInstall === true) {
|
|
356
572
|
return {
|
|
357
573
|
success: true,
|
|
@@ -359,15 +575,23 @@ async function installPackage(options = {}) {
|
|
|
359
575
|
installedCommandCount: 0,
|
|
360
576
|
installedAssetCount: 0,
|
|
361
577
|
installedHookCount: 0,
|
|
578
|
+
installedSkillCount: 0,
|
|
579
|
+
installedAgentCount: 0,
|
|
362
580
|
packagesCount: 0,
|
|
363
581
|
};
|
|
364
582
|
}
|
|
365
583
|
warnPresetCommandCollisions(commands);
|
|
366
584
|
const commandsToInstall = dedupeCommandsForInstall(commands);
|
|
585
|
+
warnPresetSkillCollisions(skills);
|
|
586
|
+
const skillsToInstall = dedupeSkillsForInstall(skills);
|
|
587
|
+
warnPresetAgentCollisions(agents);
|
|
588
|
+
const agentsToInstall = dedupeAgentsForInstall(agents);
|
|
367
589
|
try {
|
|
368
590
|
if (!options.dryRun) {
|
|
369
591
|
writeRulesToTargets(rules, assets, config.targets);
|
|
370
592
|
writeCommandsToTargets(commandsToInstall, config.targets);
|
|
593
|
+
writeSkillsToTargets(skillsToInstall, config.targets);
|
|
594
|
+
writeAgentsToTargets(agentsToInstall, config.targets);
|
|
371
595
|
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
372
596
|
writeMcpServersToTargets(mcpServers, config.targets, cwd);
|
|
373
597
|
}
|
|
@@ -378,12 +602,16 @@ async function installPackage(options = {}) {
|
|
|
378
602
|
const uniqueRuleCount = new Set(rules.map((rule) => rule.name)).size;
|
|
379
603
|
const uniqueCommandCount = new Set(commandsToInstall.map((command) => command.name)).size;
|
|
380
604
|
const uniqueHookCount = (0, hooks_1.countHooks)(hooks);
|
|
605
|
+
const uniqueSkillCount = skillsToInstall.length;
|
|
606
|
+
const uniqueAgentCount = agentsToInstall.length;
|
|
381
607
|
return {
|
|
382
608
|
success: true,
|
|
383
609
|
installedRuleCount: uniqueRuleCount,
|
|
384
610
|
installedCommandCount: uniqueCommandCount,
|
|
385
611
|
installedAssetCount: assets.length,
|
|
386
612
|
installedHookCount: uniqueHookCount,
|
|
613
|
+
installedSkillCount: uniqueSkillCount,
|
|
614
|
+
installedAgentCount: uniqueAgentCount,
|
|
387
615
|
packagesCount: 1,
|
|
388
616
|
};
|
|
389
617
|
}
|
|
@@ -395,6 +623,8 @@ async function installPackage(options = {}) {
|
|
|
395
623
|
installedCommandCount: 0,
|
|
396
624
|
installedAssetCount: 0,
|
|
397
625
|
installedHookCount: 0,
|
|
626
|
+
installedSkillCount: 0,
|
|
627
|
+
installedAgentCount: 0,
|
|
398
628
|
packagesCount: 0,
|
|
399
629
|
};
|
|
400
630
|
}
|
|
@@ -415,6 +645,8 @@ async function install(options = {}) {
|
|
|
415
645
|
installedCommandCount: 0,
|
|
416
646
|
installedAssetCount: 0,
|
|
417
647
|
installedHookCount: 0,
|
|
648
|
+
installedSkillCount: 0,
|
|
649
|
+
installedAgentCount: 0,
|
|
418
650
|
packagesCount: 0,
|
|
419
651
|
};
|
|
420
652
|
}
|
|
@@ -447,11 +679,19 @@ async function installCommand(installOnCI, verbose, dryRun) {
|
|
|
447
679
|
const ruleCount = result.installedRuleCount;
|
|
448
680
|
const commandCount = result.installedCommandCount;
|
|
449
681
|
const hookCount = result.installedHookCount;
|
|
682
|
+
const skillCount = result.installedSkillCount;
|
|
683
|
+
const agentCount = result.installedAgentCount;
|
|
450
684
|
const ruleMessage = ruleCount > 0 ? `${ruleCount} rule${ruleCount === 1 ? "" : "s"}` : null;
|
|
451
685
|
const commandMessage = commandCount > 0
|
|
452
686
|
? `${commandCount} command${commandCount === 1 ? "" : "s"}`
|
|
453
687
|
: null;
|
|
454
688
|
const hookMessage = hookCount > 0 ? `${hookCount} hook${hookCount === 1 ? "" : "s"}` : null;
|
|
689
|
+
const skillMessage = skillCount > 0
|
|
690
|
+
? `${skillCount} skill${skillCount === 1 ? "" : "s"}`
|
|
691
|
+
: null;
|
|
692
|
+
const agentMessage = agentCount > 0
|
|
693
|
+
? `${agentCount} agent${agentCount === 1 ? "" : "s"}`
|
|
694
|
+
: null;
|
|
455
695
|
const countsParts = [];
|
|
456
696
|
if (ruleMessage) {
|
|
457
697
|
countsParts.push(ruleMessage);
|
|
@@ -462,6 +702,12 @@ async function installCommand(installOnCI, verbose, dryRun) {
|
|
|
462
702
|
if (hookMessage) {
|
|
463
703
|
countsParts.push(hookMessage);
|
|
464
704
|
}
|
|
705
|
+
if (skillMessage) {
|
|
706
|
+
countsParts.push(skillMessage);
|
|
707
|
+
}
|
|
708
|
+
if (agentMessage) {
|
|
709
|
+
countsParts.push(agentMessage);
|
|
710
|
+
}
|
|
465
711
|
const countsMessage = countsParts.length > 0
|
|
466
712
|
? countsParts.join(", ").replace(/, ([^,]*)$/, " and $1")
|
|
467
713
|
: "0 rules";
|
|
@@ -473,8 +719,12 @@ async function installCommand(installOnCI, verbose, dryRun) {
|
|
|
473
719
|
console.log(`Dry run: validated ${countsMessage}`);
|
|
474
720
|
}
|
|
475
721
|
}
|
|
476
|
-
else if (ruleCount === 0 &&
|
|
477
|
-
|
|
722
|
+
else if (ruleCount === 0 &&
|
|
723
|
+
commandCount === 0 &&
|
|
724
|
+
hookCount === 0 &&
|
|
725
|
+
skillCount === 0 &&
|
|
726
|
+
agentCount === 0) {
|
|
727
|
+
console.log("No rules, commands, hooks, skills, or agents installed");
|
|
478
728
|
}
|
|
479
729
|
else if (result.packagesCount > 1) {
|
|
480
730
|
console.log(`Successfully installed ${countsMessage} across ${result.packagesCount} packages`);
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -46,6 +46,13 @@ export interface AssetFile {
|
|
|
46
46
|
}
|
|
47
47
|
export type RuleFile = ManagedFile;
|
|
48
48
|
export type CommandFile = ManagedFile;
|
|
49
|
+
export interface SkillFile {
|
|
50
|
+
name: string;
|
|
51
|
+
sourcePath: string;
|
|
52
|
+
source: "local" | "preset";
|
|
53
|
+
presetName?: string;
|
|
54
|
+
}
|
|
55
|
+
export type AgentFile = ManagedFile;
|
|
49
56
|
export interface RuleCollection {
|
|
50
57
|
[target: string]: RuleFile[];
|
|
51
58
|
}
|
|
@@ -54,6 +61,8 @@ export interface ResolvedConfig {
|
|
|
54
61
|
rules: RuleFile[];
|
|
55
62
|
commands: CommandFile[];
|
|
56
63
|
assets: AssetFile[];
|
|
64
|
+
skills: SkillFile[];
|
|
65
|
+
agents: AgentFile[];
|
|
57
66
|
mcpServers: MCPServers;
|
|
58
67
|
hooks: HooksJson;
|
|
59
68
|
hookFiles: HookFile[];
|
|
@@ -68,6 +77,16 @@ export declare function validateConfig(config: unknown, configFilePath: string,
|
|
|
68
77
|
export declare function loadRulesFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<RuleFile[]>;
|
|
69
78
|
export declare function loadCommandsFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<CommandFile[]>;
|
|
70
79
|
export declare function loadAssetsFromDirectory(directoryPath: string, source: "local" | "preset", presetName?: string): Promise<AssetFile[]>;
|
|
80
|
+
/**
|
|
81
|
+
* Load skills from a skills/ directory
|
|
82
|
+
* Each direct subdirectory containing a SKILL.md file is considered a skill
|
|
83
|
+
*/
|
|
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[]>;
|
|
71
90
|
/**
|
|
72
91
|
* Extract namespace from preset path for directory structure
|
|
73
92
|
* Handles both npm packages and local paths consistently
|
|
@@ -82,6 +101,8 @@ export declare function loadAllRules(config: Config, cwd: string): Promise<{
|
|
|
82
101
|
rules: RuleFile[];
|
|
83
102
|
commands: CommandFile[];
|
|
84
103
|
assets: AssetFile[];
|
|
104
|
+
skills: SkillFile[];
|
|
105
|
+
agents: AgentFile[];
|
|
85
106
|
mcpServers: MCPServers;
|
|
86
107
|
hooks: HooksJson;
|
|
87
108
|
hookFiles: HookFile[];
|
package/dist/utils/config.js
CHANGED
|
@@ -11,6 +11,8 @@ exports.validateConfig = validateConfig;
|
|
|
11
11
|
exports.loadRulesFromDirectory = loadRulesFromDirectory;
|
|
12
12
|
exports.loadCommandsFromDirectory = loadCommandsFromDirectory;
|
|
13
13
|
exports.loadAssetsFromDirectory = loadAssetsFromDirectory;
|
|
14
|
+
exports.loadSkillsFromDirectory = loadSkillsFromDirectory;
|
|
15
|
+
exports.loadAgentsFromDirectory = loadAgentsFromDirectory;
|
|
14
16
|
exports.extractNamespaceFromPresetPath = extractNamespaceFromPresetPath;
|
|
15
17
|
exports.resolvePresetPath = resolvePresetPath;
|
|
16
18
|
exports.loadPreset = loadPreset;
|
|
@@ -96,14 +98,18 @@ function validateConfig(config, configFilePath, cwd, isWorkspaceMode = false) {
|
|
|
96
98
|
const hasRules = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "rules"));
|
|
97
99
|
const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "commands"));
|
|
98
100
|
const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(rootPath, "hooks.json"));
|
|
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"));
|
|
99
103
|
// In workspace mode, root config doesn't need these directories
|
|
100
104
|
// since packages will have their own configurations
|
|
101
105
|
if (!isWorkspaceMode &&
|
|
102
106
|
!hasRules &&
|
|
103
107
|
!hasCommands &&
|
|
104
108
|
!hasHooks &&
|
|
109
|
+
!hasSkills &&
|
|
110
|
+
!hasAgents &&
|
|
105
111
|
!hasPresets) {
|
|
106
|
-
throw new Error(`Root directory must contain at least one of: rules/, commands/, 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`);
|
|
107
113
|
}
|
|
108
114
|
}
|
|
109
115
|
else if (!isWorkspaceMode && !hasPresets) {
|
|
@@ -202,6 +208,65 @@ async function loadAssetsFromDirectory(directoryPath, source, presetName) {
|
|
|
202
208
|
}
|
|
203
209
|
return assets;
|
|
204
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Load skills from a skills/ directory
|
|
213
|
+
* Each direct subdirectory containing a SKILL.md file is considered a skill
|
|
214
|
+
*/
|
|
215
|
+
async function loadSkillsFromDirectory(directoryPath, source, presetName) {
|
|
216
|
+
const skills = [];
|
|
217
|
+
if (!fs_extra_1.default.existsSync(directoryPath)) {
|
|
218
|
+
return skills;
|
|
219
|
+
}
|
|
220
|
+
// Get all direct subdirectories
|
|
221
|
+
const entries = await fs_extra_1.default.readdir(directoryPath, { withFileTypes: true });
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
if (!entry.isDirectory()) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const skillPath = node_path_1.default.join(directoryPath, entry.name);
|
|
227
|
+
const skillMdPath = node_path_1.default.join(skillPath, "SKILL.md");
|
|
228
|
+
// Only include directories that contain a SKILL.md file
|
|
229
|
+
if (!fs_extra_1.default.existsSync(skillMdPath)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
skills.push({
|
|
233
|
+
name: entry.name,
|
|
234
|
+
sourcePath: skillPath,
|
|
235
|
+
source,
|
|
236
|
+
presetName,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return skills;
|
|
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
|
+
}
|
|
205
270
|
/**
|
|
206
271
|
* Extract namespace from preset path for directory structure
|
|
207
272
|
* Handles both npm packages and local paths consistently
|
|
@@ -258,8 +323,15 @@ async function loadPreset(presetPath, cwd) {
|
|
|
258
323
|
const hasCommands = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "commands"));
|
|
259
324
|
const hasHooks = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "hooks.json"));
|
|
260
325
|
const hasAssets = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "assets"));
|
|
261
|
-
|
|
262
|
-
|
|
326
|
+
const hasSkills = fs_extra_1.default.existsSync(node_path_1.default.join(presetRootDir, "skills"));
|
|
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/`);
|
|
263
335
|
}
|
|
264
336
|
return {
|
|
265
337
|
config: presetConfig,
|
|
@@ -270,6 +342,8 @@ async function loadAllRules(config, cwd) {
|
|
|
270
342
|
const allRules = [];
|
|
271
343
|
const allCommands = [];
|
|
272
344
|
const allAssets = [];
|
|
345
|
+
const allSkills = [];
|
|
346
|
+
const allAgents = [];
|
|
273
347
|
const allHookFiles = [];
|
|
274
348
|
const allHooksConfigs = [];
|
|
275
349
|
let mergedMcpServers = { ...config.mcpServers };
|
|
@@ -301,6 +375,18 @@ async function loadAllRules(config, cwd) {
|
|
|
301
375
|
const localAssets = await loadAssetsFromDirectory(assetsPath, "local");
|
|
302
376
|
allAssets.push(...localAssets);
|
|
303
377
|
}
|
|
378
|
+
// Load skills from skills/ subdirectory
|
|
379
|
+
const skillsPath = node_path_1.default.join(rootPath, "skills");
|
|
380
|
+
if (fs_extra_1.default.existsSync(skillsPath)) {
|
|
381
|
+
const localSkills = await loadSkillsFromDirectory(skillsPath, "local");
|
|
382
|
+
allSkills.push(...localSkills);
|
|
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
|
+
}
|
|
304
390
|
}
|
|
305
391
|
// Load presets
|
|
306
392
|
if (config.presets) {
|
|
@@ -332,6 +418,18 @@ async function loadAllRules(config, cwd) {
|
|
|
332
418
|
const presetAssets = await loadAssetsFromDirectory(presetAssetsPath, "preset", presetPath);
|
|
333
419
|
allAssets.push(...presetAssets);
|
|
334
420
|
}
|
|
421
|
+
// Load preset skills from skills/ subdirectory
|
|
422
|
+
const presetSkillsPath = node_path_1.default.join(presetRootDir, "skills");
|
|
423
|
+
if (fs_extra_1.default.existsSync(presetSkillsPath)) {
|
|
424
|
+
const presetSkills = await loadSkillsFromDirectory(presetSkillsPath, "preset", presetPath);
|
|
425
|
+
allSkills.push(...presetSkills);
|
|
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
|
+
}
|
|
335
433
|
// Merge MCP servers from preset
|
|
336
434
|
if (preset.config.mcpServers) {
|
|
337
435
|
mergedMcpServers = mergePresetMcpServers(mergedMcpServers, preset.config.mcpServers);
|
|
@@ -344,6 +442,8 @@ async function loadAllRules(config, cwd) {
|
|
|
344
442
|
rules: allRules,
|
|
345
443
|
commands: allCommands,
|
|
346
444
|
assets: allAssets,
|
|
445
|
+
skills: allSkills,
|
|
446
|
+
agents: allAgents,
|
|
347
447
|
mcpServers: mergedMcpServers,
|
|
348
448
|
hooks: mergedHooks,
|
|
349
449
|
hookFiles: allHookFiles,
|
|
@@ -403,12 +503,14 @@ async function loadConfig(cwd) {
|
|
|
403
503
|
const isWorkspaces = resolveWorkspaces(config, configResult.filepath, workingDir);
|
|
404
504
|
validateConfig(config, configResult.filepath, workingDir, isWorkspaces);
|
|
405
505
|
const configWithDefaults = applyDefaults(config, isWorkspaces);
|
|
406
|
-
const { rules, commands, assets, mcpServers, hooks, hookFiles } = await loadAllRules(configWithDefaults, workingDir);
|
|
506
|
+
const { rules, commands, assets, skills, agents, mcpServers, hooks, hookFiles, } = await loadAllRules(configWithDefaults, workingDir);
|
|
407
507
|
return {
|
|
408
508
|
config: configWithDefaults,
|
|
409
509
|
rules,
|
|
410
510
|
commands,
|
|
411
511
|
assets,
|
|
512
|
+
skills,
|
|
513
|
+
agents,
|
|
412
514
|
mcpServers,
|
|
413
515
|
hooks,
|
|
414
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
|
+
}
|