@intellectronica/ruler 0.3.42 → 0.3.43
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 +97 -10
- package/dist/agents/AbstractAgent.js +3 -2
- package/dist/agents/AgentsMdAgent.js +3 -2
- package/dist/agents/AiderAgent.js +4 -3
- package/dist/agents/AmazonQCliAgent.js +6 -4
- package/dist/agents/AugmentCodeAgent.js +3 -2
- package/dist/agents/CodexCliAgent.js +1 -1
- package/dist/agents/CrushAgent.d.ts +1 -1
- package/dist/agents/CrushAgent.js +15 -6
- package/dist/agents/FirebenderAgent.js +5 -4
- package/dist/agents/GeminiCliAgent.d.ts +1 -0
- package/dist/agents/GeminiCliAgent.js +11 -5
- package/dist/agents/IAgent.d.ts +2 -0
- package/dist/agents/MistralVibeAgent.js +14 -3
- package/dist/agents/OpenCodeAgent.d.ts +1 -1
- package/dist/agents/OpenCodeAgent.js +10 -3
- package/dist/agents/QwenCodeAgent.d.ts +1 -0
- package/dist/agents/QwenCodeAgent.js +9 -3
- package/dist/agents/RooCodeAgent.js +3 -2
- package/dist/agents/ZedAgent.js +3 -3
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/core/ConfigLoader.d.ts +2 -0
- package/dist/core/ConfigLoader.js +73 -6
- package/dist/core/FileSystemUtils.d.ts +4 -2
- package/dist/core/FileSystemUtils.js +120 -3
- package/dist/core/GitignoreUtils.d.ts +10 -0
- package/dist/core/GitignoreUtils.js +62 -31
- package/dist/core/SkillsProcessor.d.ts +2 -2
- package/dist/core/SkillsProcessor.js +46 -37
- package/dist/core/SubagentsProcessor.js +8 -5
- package/dist/core/UnifiedConfigLoader.js +54 -2
- package/dist/core/UnifiedConfigTypes.d.ts +3 -1
- package/dist/core/agent-selection.js +6 -4
- package/dist/core/apply-engine.d.ts +1 -0
- package/dist/core/apply-engine.js +38 -15
- package/dist/core/revert-engine.d.ts +2 -1
- package/dist/core/revert-engine.js +73 -26
- package/dist/lib.js +9 -6
- package/dist/mcp/merge.js +28 -26
- package/dist/mcp/propagateOpenCodeMcp.d.ts +1 -1
- package/dist/mcp/propagateOpenCodeMcp.js +10 -3
- package/dist/mcp/propagateOpenHandsMcp.d.ts +1 -1
- package/dist/mcp/propagateOpenHandsMcp.js +18 -7
- package/dist/paths/mcp.d.ts +1 -1
- package/dist/paths/mcp.js +11 -4
- package/dist/revert.js +27 -27
- package/dist/vscode/settings.d.ts +1 -1
- package/dist/vscode/settings.js +3 -3
- package/package.json +4 -3
|
@@ -54,6 +54,7 @@ const path = __importStar(require("path"));
|
|
|
54
54
|
const fs = __importStar(require("fs/promises"));
|
|
55
55
|
const constants_1 = require("../constants");
|
|
56
56
|
const SkillsUtils_1 = require("./SkillsUtils");
|
|
57
|
+
const FileSystemUtils_1 = require("./FileSystemUtils");
|
|
57
58
|
/**
|
|
58
59
|
* Discovers skills in the project's .ruler/skills directory.
|
|
59
60
|
* Returns discovered skills and any validation warnings.
|
|
@@ -102,7 +103,8 @@ async function getSkillsGitignorePaths(projectRoot, agents) {
|
|
|
102
103
|
factory: FACTORY_SKILLS_PATH,
|
|
103
104
|
antigravity: ANTIGRAVITY_SKILLS_PATH,
|
|
104
105
|
};
|
|
105
|
-
|
|
106
|
+
const pathSet = new Set(Array.from(selectedTargets).map((target) => path.join(projectRoot, targetPaths[target])));
|
|
107
|
+
return Array.from(pathSet);
|
|
106
108
|
}
|
|
107
109
|
/**
|
|
108
110
|
* Module-level state to track if experimental warning has been shown.
|
|
@@ -137,7 +139,7 @@ const SKILL_TARGET_TO_IDENTIFIERS = new Map([
|
|
|
137
139
|
['factory', ['factory']],
|
|
138
140
|
['antigravity', ['antigravity']],
|
|
139
141
|
]);
|
|
140
|
-
const SKILL_TARGET_PATHS = [
|
|
142
|
+
const SKILL_TARGET_PATHS = Array.from(new Set([
|
|
141
143
|
constants_1.CLAUDE_SKILLS_PATH,
|
|
142
144
|
constants_1.CODEX_SKILLS_PATH,
|
|
143
145
|
constants_1.OPENCODE_SKILLS_PATH,
|
|
@@ -151,7 +153,7 @@ const SKILL_TARGET_PATHS = [
|
|
|
151
153
|
constants_1.WINDSURF_SKILLS_PATH,
|
|
152
154
|
constants_1.FACTORY_SKILLS_PATH,
|
|
153
155
|
constants_1.ANTIGRAVITY_SKILLS_PATH,
|
|
154
|
-
];
|
|
156
|
+
]));
|
|
155
157
|
function getSelectedSkillTargets(agents) {
|
|
156
158
|
const selectedIdentifiers = new Set(agents
|
|
157
159
|
.filter((agent) => agent.supportsNativeSkills?.())
|
|
@@ -185,10 +187,14 @@ async function cleanupSkillsDirectory(projectRoot, relativePath, dryRun, verbose
|
|
|
185
187
|
(0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${relativePath}`, verbose, dryRun);
|
|
186
188
|
return;
|
|
187
189
|
}
|
|
190
|
+
await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(targetPath, projectRoot, 'Refusing to remove skills directory through symlinked path');
|
|
188
191
|
await fs.rm(targetPath, { recursive: true, force: true });
|
|
189
192
|
(0, constants_1.logVerboseInfo)(`Removed ${relativePath} (skills disabled)`, verbose, dryRun);
|
|
190
193
|
}
|
|
191
|
-
async function createTempSkillsDir(parentDir) {
|
|
194
|
+
async function createTempSkillsDir(parentDir, containmentRoot) {
|
|
195
|
+
if (containmentRoot) {
|
|
196
|
+
await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(parentDir, containmentRoot, 'Refusing to create temporary skills directory through symlinked path');
|
|
197
|
+
}
|
|
192
198
|
return fs.mkdtemp(path.join(parentDir, 'skills.tmp-'));
|
|
193
199
|
}
|
|
194
200
|
const TRANSIENT_RENAME_ERROR_CODES = new Set(['EPERM', 'EBUSY', 'ENOTEMPTY']);
|
|
@@ -204,7 +210,10 @@ function isTransientRenameError(error) {
|
|
|
204
210
|
function wait(ms) {
|
|
205
211
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
206
212
|
}
|
|
207
|
-
async function replaceSkillsDirectory(tempDir, targetDir, fsOps = fs) {
|
|
213
|
+
async function replaceSkillsDirectory(tempDir, targetDir, fsOps = fs, containmentRoot) {
|
|
214
|
+
if (containmentRoot) {
|
|
215
|
+
await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(targetDir, containmentRoot, 'Refusing to replace skills directory through symlinked path');
|
|
216
|
+
}
|
|
208
217
|
let lastError;
|
|
209
218
|
for (let attempt = 1; attempt <= RENAME_RETRY_ATTEMPTS; attempt += 1) {
|
|
210
219
|
try {
|
|
@@ -362,7 +371,7 @@ async function propagateSkillsForClaude(projectRoot, options) {
|
|
|
362
371
|
// Ensure .claude directory exists
|
|
363
372
|
await fs.mkdir(claudeDir, { recursive: true });
|
|
364
373
|
// Use atomic replace: copy to temp, then rename
|
|
365
|
-
const tempDir = await createTempSkillsDir(claudeDir);
|
|
374
|
+
const tempDir = await createTempSkillsDir(claudeDir, projectRoot);
|
|
366
375
|
try {
|
|
367
376
|
// Copy to temp directory
|
|
368
377
|
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
@@ -375,7 +384,7 @@ async function propagateSkillsForClaude(projectRoot, options) {
|
|
|
375
384
|
// Target didn't exist, that's fine
|
|
376
385
|
}
|
|
377
386
|
// Rename temp to target
|
|
378
|
-
await replaceSkillsDirectory(tempDir, claudeSkillsPath);
|
|
387
|
+
await replaceSkillsDirectory(tempDir, claudeSkillsPath, fs, projectRoot);
|
|
379
388
|
}
|
|
380
389
|
catch (error) {
|
|
381
390
|
// Clean up temp directory on error
|
|
@@ -390,14 +399,14 @@ async function propagateSkillsForClaude(projectRoot, options) {
|
|
|
390
399
|
return [];
|
|
391
400
|
}
|
|
392
401
|
/**
|
|
393
|
-
* Propagates skills for OpenAI Codex CLI by copying .ruler/skills to .
|
|
402
|
+
* Propagates skills for OpenAI Codex CLI by copying .ruler/skills to .agents/skills.
|
|
394
403
|
* Uses atomic replace to ensure safe overwriting of existing skills.
|
|
395
404
|
* Returns dry-run steps if dryRun is true, otherwise returns empty array.
|
|
396
405
|
*/
|
|
397
406
|
async function propagateSkillsForCodex(projectRoot, options) {
|
|
398
407
|
const skillsDir = path.join(projectRoot, constants_1.RULER_SKILLS_PATH);
|
|
399
|
-
const
|
|
400
|
-
const
|
|
408
|
+
const agentsSkillsPath = path.join(projectRoot, constants_1.CODEX_SKILLS_PATH);
|
|
409
|
+
const agentsDir = path.dirname(agentsSkillsPath);
|
|
401
410
|
// Check if source skills directory exists
|
|
402
411
|
try {
|
|
403
412
|
await fs.access(skillsDir);
|
|
@@ -409,23 +418,23 @@ async function propagateSkillsForCodex(projectRoot, options) {
|
|
|
409
418
|
if (options.dryRun) {
|
|
410
419
|
return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.CODEX_SKILLS_PATH}`];
|
|
411
420
|
}
|
|
412
|
-
// Ensure .
|
|
413
|
-
await fs.mkdir(
|
|
421
|
+
// Ensure .agents directory exists
|
|
422
|
+
await fs.mkdir(agentsDir, { recursive: true });
|
|
414
423
|
// Use atomic replace: copy to temp, then rename
|
|
415
|
-
const tempDir = await createTempSkillsDir(
|
|
424
|
+
const tempDir = await createTempSkillsDir(agentsDir, projectRoot);
|
|
416
425
|
try {
|
|
417
426
|
// Copy to temp directory
|
|
418
427
|
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
419
428
|
// Atomically replace the target
|
|
420
429
|
// First, remove existing target if it exists
|
|
421
430
|
try {
|
|
422
|
-
await fs.rm(
|
|
431
|
+
await fs.rm(agentsSkillsPath, { recursive: true, force: true });
|
|
423
432
|
}
|
|
424
433
|
catch {
|
|
425
434
|
// Target didn't exist, that's fine
|
|
426
435
|
}
|
|
427
436
|
// Rename temp to target
|
|
428
|
-
await replaceSkillsDirectory(tempDir,
|
|
437
|
+
await replaceSkillsDirectory(tempDir, agentsSkillsPath, fs, projectRoot);
|
|
429
438
|
}
|
|
430
439
|
catch (error) {
|
|
431
440
|
// Clean up temp directory on error
|
|
@@ -462,7 +471,7 @@ async function propagateSkillsForOpenCode(projectRoot, options) {
|
|
|
462
471
|
// Ensure .opencode directory exists
|
|
463
472
|
await fs.mkdir(opencodeDir, { recursive: true });
|
|
464
473
|
// Use atomic replace: copy to temp, then rename
|
|
465
|
-
const tempDir = await createTempSkillsDir(opencodeDir);
|
|
474
|
+
const tempDir = await createTempSkillsDir(opencodeDir, projectRoot);
|
|
466
475
|
try {
|
|
467
476
|
// Copy to temp directory
|
|
468
477
|
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
@@ -475,7 +484,7 @@ async function propagateSkillsForOpenCode(projectRoot, options) {
|
|
|
475
484
|
// Target didn't exist, that's fine
|
|
476
485
|
}
|
|
477
486
|
// Rename temp to target
|
|
478
|
-
await replaceSkillsDirectory(tempDir, opencodeSkillsPath);
|
|
487
|
+
await replaceSkillsDirectory(tempDir, opencodeSkillsPath, fs, projectRoot);
|
|
479
488
|
}
|
|
480
489
|
catch (error) {
|
|
481
490
|
// Clean up temp directory on error
|
|
@@ -512,7 +521,7 @@ async function propagateSkillsForPi(projectRoot, options) {
|
|
|
512
521
|
// Ensure .pi directory exists
|
|
513
522
|
await fs.mkdir(piDir, { recursive: true });
|
|
514
523
|
// Use atomic replace: copy to temp, then rename
|
|
515
|
-
const tempDir = await createTempSkillsDir(piDir);
|
|
524
|
+
const tempDir = await createTempSkillsDir(piDir, projectRoot);
|
|
516
525
|
try {
|
|
517
526
|
// Copy to temp directory
|
|
518
527
|
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
@@ -525,7 +534,7 @@ async function propagateSkillsForPi(projectRoot, options) {
|
|
|
525
534
|
// Target didn't exist, that's fine
|
|
526
535
|
}
|
|
527
536
|
// Rename temp to target
|
|
528
|
-
await replaceSkillsDirectory(tempDir, piSkillsPath);
|
|
537
|
+
await replaceSkillsDirectory(tempDir, piSkillsPath, fs, projectRoot);
|
|
529
538
|
}
|
|
530
539
|
catch (error) {
|
|
531
540
|
// Clean up temp directory on error
|
|
@@ -562,7 +571,7 @@ async function propagateSkillsForGoose(projectRoot, options) {
|
|
|
562
571
|
// Ensure .agents directory exists
|
|
563
572
|
await fs.mkdir(gooseDir, { recursive: true });
|
|
564
573
|
// Use atomic replace: copy to temp, then rename
|
|
565
|
-
const tempDir = await createTempSkillsDir(gooseDir);
|
|
574
|
+
const tempDir = await createTempSkillsDir(gooseDir, projectRoot);
|
|
566
575
|
try {
|
|
567
576
|
// Copy to temp directory
|
|
568
577
|
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
@@ -575,7 +584,7 @@ async function propagateSkillsForGoose(projectRoot, options) {
|
|
|
575
584
|
// Target didn't exist, that's fine
|
|
576
585
|
}
|
|
577
586
|
// Rename temp to target
|
|
578
|
-
await replaceSkillsDirectory(tempDir, gooseSkillsPath);
|
|
587
|
+
await replaceSkillsDirectory(tempDir, gooseSkillsPath, fs, projectRoot);
|
|
579
588
|
}
|
|
580
589
|
catch (error) {
|
|
581
590
|
// Clean up temp directory on error
|
|
@@ -612,7 +621,7 @@ async function propagateSkillsForVibe(projectRoot, options) {
|
|
|
612
621
|
// Ensure .vibe directory exists
|
|
613
622
|
await fs.mkdir(vibeDir, { recursive: true });
|
|
614
623
|
// Use atomic replace: copy to temp, then rename
|
|
615
|
-
const tempDir = await createTempSkillsDir(vibeDir);
|
|
624
|
+
const tempDir = await createTempSkillsDir(vibeDir, projectRoot);
|
|
616
625
|
try {
|
|
617
626
|
// Copy to temp directory
|
|
618
627
|
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
@@ -625,7 +634,7 @@ async function propagateSkillsForVibe(projectRoot, options) {
|
|
|
625
634
|
// Target didn't exist, that's fine
|
|
626
635
|
}
|
|
627
636
|
// Rename temp to target
|
|
628
|
-
await replaceSkillsDirectory(tempDir, vibeSkillsPath);
|
|
637
|
+
await replaceSkillsDirectory(tempDir, vibeSkillsPath, fs, projectRoot);
|
|
629
638
|
}
|
|
630
639
|
catch (error) {
|
|
631
640
|
// Clean up temp directory on error
|
|
@@ -662,7 +671,7 @@ async function propagateSkillsForRoo(projectRoot, options) {
|
|
|
662
671
|
// Ensure .roo directory exists
|
|
663
672
|
await fs.mkdir(rooDir, { recursive: true });
|
|
664
673
|
// Use atomic replace: copy to temp, then rename
|
|
665
|
-
const tempDir = await createTempSkillsDir(rooDir);
|
|
674
|
+
const tempDir = await createTempSkillsDir(rooDir, projectRoot);
|
|
666
675
|
try {
|
|
667
676
|
// Copy to temp directory
|
|
668
677
|
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
@@ -675,7 +684,7 @@ async function propagateSkillsForRoo(projectRoot, options) {
|
|
|
675
684
|
// Target didn't exist, that's fine
|
|
676
685
|
}
|
|
677
686
|
// Rename temp to target
|
|
678
|
-
await replaceSkillsDirectory(tempDir, rooSkillsPath);
|
|
687
|
+
await replaceSkillsDirectory(tempDir, rooSkillsPath, fs, projectRoot);
|
|
679
688
|
}
|
|
680
689
|
catch (error) {
|
|
681
690
|
// Clean up temp directory on error
|
|
@@ -712,7 +721,7 @@ async function propagateSkillsForGemini(projectRoot, options) {
|
|
|
712
721
|
// Ensure .gemini directory exists
|
|
713
722
|
await fs.mkdir(geminiDir, { recursive: true });
|
|
714
723
|
// Use atomic replace: copy to temp, then rename
|
|
715
|
-
const tempDir = await createTempSkillsDir(geminiDir);
|
|
724
|
+
const tempDir = await createTempSkillsDir(geminiDir, projectRoot);
|
|
716
725
|
try {
|
|
717
726
|
// Copy to temp directory
|
|
718
727
|
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
@@ -725,7 +734,7 @@ async function propagateSkillsForGemini(projectRoot, options) {
|
|
|
725
734
|
// Target didn't exist, that's fine
|
|
726
735
|
}
|
|
727
736
|
// Rename temp to target
|
|
728
|
-
await replaceSkillsDirectory(tempDir, geminiSkillsPath);
|
|
737
|
+
await replaceSkillsDirectory(tempDir, geminiSkillsPath, fs, projectRoot);
|
|
729
738
|
}
|
|
730
739
|
catch (error) {
|
|
731
740
|
// Clean up temp directory on error
|
|
@@ -758,7 +767,7 @@ async function propagateSkillsForJunie(projectRoot, options) {
|
|
|
758
767
|
return [`Copy skills from ${constants_1.RULER_SKILLS_PATH} to ${constants_1.JUNIE_SKILLS_PATH}`];
|
|
759
768
|
}
|
|
760
769
|
await fs.mkdir(junieDir, { recursive: true });
|
|
761
|
-
const tempDir = await createTempSkillsDir(junieDir);
|
|
770
|
+
const tempDir = await createTempSkillsDir(junieDir, projectRoot);
|
|
762
771
|
try {
|
|
763
772
|
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
764
773
|
try {
|
|
@@ -767,7 +776,7 @@ async function propagateSkillsForJunie(projectRoot, options) {
|
|
|
767
776
|
catch {
|
|
768
777
|
// Target didn't exist, that's fine
|
|
769
778
|
}
|
|
770
|
-
await replaceSkillsDirectory(tempDir, junieSkillsPath);
|
|
779
|
+
await replaceSkillsDirectory(tempDir, junieSkillsPath, fs, projectRoot);
|
|
771
780
|
}
|
|
772
781
|
catch (error) {
|
|
773
782
|
try {
|
|
@@ -803,7 +812,7 @@ async function propagateSkillsForCursor(projectRoot, options) {
|
|
|
803
812
|
// Ensure .cursor directory exists
|
|
804
813
|
await fs.mkdir(cursorDir, { recursive: true });
|
|
805
814
|
// Use atomic replace: copy to temp, then rename
|
|
806
|
-
const tempDir = await createTempSkillsDir(cursorDir);
|
|
815
|
+
const tempDir = await createTempSkillsDir(cursorDir, projectRoot);
|
|
807
816
|
try {
|
|
808
817
|
// Copy to temp directory
|
|
809
818
|
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
@@ -816,7 +825,7 @@ async function propagateSkillsForCursor(projectRoot, options) {
|
|
|
816
825
|
// Target didn't exist, that's fine
|
|
817
826
|
}
|
|
818
827
|
// Rename temp to target
|
|
819
|
-
await replaceSkillsDirectory(tempDir, cursorSkillsPath);
|
|
828
|
+
await replaceSkillsDirectory(tempDir, cursorSkillsPath, fs, projectRoot);
|
|
820
829
|
}
|
|
821
830
|
catch (error) {
|
|
822
831
|
// Clean up temp directory on error
|
|
@@ -853,7 +862,7 @@ async function propagateSkillsForWindsurf(projectRoot, options) {
|
|
|
853
862
|
// Ensure .windsurf directory exists
|
|
854
863
|
await fs.mkdir(windsurfDir, { recursive: true });
|
|
855
864
|
// Use atomic replace: copy to temp, then rename
|
|
856
|
-
const tempDir = await createTempSkillsDir(windsurfDir);
|
|
865
|
+
const tempDir = await createTempSkillsDir(windsurfDir, projectRoot);
|
|
857
866
|
try {
|
|
858
867
|
// Copy to temp directory
|
|
859
868
|
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
@@ -866,7 +875,7 @@ async function propagateSkillsForWindsurf(projectRoot, options) {
|
|
|
866
875
|
// Target didn't exist, that's fine
|
|
867
876
|
}
|
|
868
877
|
// Rename temp to target
|
|
869
|
-
await replaceSkillsDirectory(tempDir, windsurfSkillsPath);
|
|
878
|
+
await replaceSkillsDirectory(tempDir, windsurfSkillsPath, fs, projectRoot);
|
|
870
879
|
}
|
|
871
880
|
catch (error) {
|
|
872
881
|
// Clean up temp directory on error
|
|
@@ -903,7 +912,7 @@ async function propagateSkillsForFactory(projectRoot, options) {
|
|
|
903
912
|
// Ensure .factory directory exists
|
|
904
913
|
await fs.mkdir(factoryDir, { recursive: true });
|
|
905
914
|
// Use atomic replace: copy to temp, then rename
|
|
906
|
-
const tempDir = await createTempSkillsDir(factoryDir);
|
|
915
|
+
const tempDir = await createTempSkillsDir(factoryDir, projectRoot);
|
|
907
916
|
try {
|
|
908
917
|
// Copy to temp directory
|
|
909
918
|
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
@@ -916,7 +925,7 @@ async function propagateSkillsForFactory(projectRoot, options) {
|
|
|
916
925
|
// Target didn't exist, that's fine
|
|
917
926
|
}
|
|
918
927
|
// Rename temp to target
|
|
919
|
-
await replaceSkillsDirectory(tempDir, factorySkillsPath);
|
|
928
|
+
await replaceSkillsDirectory(tempDir, factorySkillsPath, fs, projectRoot);
|
|
920
929
|
}
|
|
921
930
|
catch (error) {
|
|
922
931
|
// Clean up temp directory on error
|
|
@@ -955,7 +964,7 @@ async function propagateSkillsForAntigravity(projectRoot, options) {
|
|
|
955
964
|
// Ensure .agent directory exists
|
|
956
965
|
await fs.mkdir(antigravityDir, { recursive: true });
|
|
957
966
|
// Use atomic replace: copy to temp, then rename
|
|
958
|
-
const tempDir = await createTempSkillsDir(antigravityDir);
|
|
967
|
+
const tempDir = await createTempSkillsDir(antigravityDir, projectRoot);
|
|
959
968
|
try {
|
|
960
969
|
// Copy to temp directory
|
|
961
970
|
await (0, SkillsUtils_1.copySkillsDirectory)(skillsDir, tempDir);
|
|
@@ -968,7 +977,7 @@ async function propagateSkillsForAntigravity(projectRoot, options) {
|
|
|
968
977
|
// Target didn't exist, that's fine
|
|
969
978
|
}
|
|
970
979
|
// Rename temp to target
|
|
971
|
-
await replaceSkillsDirectory(tempDir, antigravitySkillsPath);
|
|
980
|
+
await replaceSkillsDirectory(tempDir, antigravitySkillsPath, fs, projectRoot);
|
|
972
981
|
}
|
|
973
982
|
catch (error) {
|
|
974
983
|
// Clean up temp directory on error
|
|
@@ -48,6 +48,7 @@ const yaml = __importStar(require("js-yaml"));
|
|
|
48
48
|
const toml_1 = require("@iarna/toml");
|
|
49
49
|
const constants_1 = require("../constants");
|
|
50
50
|
const SubagentsUtils_1 = require("./SubagentsUtils");
|
|
51
|
+
const FileSystemUtils_1 = require("./FileSystemUtils");
|
|
51
52
|
/**
|
|
52
53
|
* Discovers subagent definitions in `.ruler/agents/`.
|
|
53
54
|
* Each `.md` file is parsed for YAML frontmatter (name, description, …).
|
|
@@ -173,8 +174,9 @@ function ensureBodyFormatting(body) {
|
|
|
173
174
|
* Stages files into a temp directory and atomically swaps it into place.
|
|
174
175
|
* Mirrors the pattern used by SkillsProcessor for safe overwriting.
|
|
175
176
|
*/
|
|
176
|
-
async function writeAgentsDirectoryAtomic(targetDir, files) {
|
|
177
|
+
async function writeAgentsDirectoryAtomic(targetDir, files, projectRoot) {
|
|
177
178
|
const parent = path.dirname(targetDir);
|
|
179
|
+
await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(targetDir, projectRoot, 'Refusing to replace subagents directory through symlinked path');
|
|
178
180
|
await fs.mkdir(parent, { recursive: true });
|
|
179
181
|
const tempDir = path.join(parent, `agents.tmp-${Date.now()}`);
|
|
180
182
|
await fs.mkdir(tempDir, { recursive: true });
|
|
@@ -310,7 +312,7 @@ async function propagateSubagentsForClaude(projectRoot, subagents, options) {
|
|
|
310
312
|
name: getSourceRelativeMdPath(s),
|
|
311
313
|
content: buildClaudeFile(s),
|
|
312
314
|
}));
|
|
313
|
-
await writeAgentsDirectoryAtomic(targetDir, files);
|
|
315
|
+
await writeAgentsDirectoryAtomic(targetDir, files, projectRoot);
|
|
314
316
|
return [];
|
|
315
317
|
}
|
|
316
318
|
async function propagateSubagentsForCursor(projectRoot, subagents, options) {
|
|
@@ -324,7 +326,7 @@ async function propagateSubagentsForCursor(projectRoot, subagents, options) {
|
|
|
324
326
|
name: getSourceRelativeMdPath(s),
|
|
325
327
|
content: buildCursorFile(s),
|
|
326
328
|
}));
|
|
327
|
-
await writeAgentsDirectoryAtomic(targetDir, files);
|
|
329
|
+
await writeAgentsDirectoryAtomic(targetDir, files, projectRoot);
|
|
328
330
|
return [];
|
|
329
331
|
}
|
|
330
332
|
async function propagateSubagentsForCodex(projectRoot, subagents, options) {
|
|
@@ -338,7 +340,7 @@ async function propagateSubagentsForCodex(projectRoot, subagents, options) {
|
|
|
338
340
|
name: withExtension(getSourceRelativeMdPath(s), '.toml'),
|
|
339
341
|
content: buildCodexFile(s),
|
|
340
342
|
}));
|
|
341
|
-
await writeAgentsDirectoryAtomic(targetDir, files);
|
|
343
|
+
await writeAgentsDirectoryAtomic(targetDir, files, projectRoot);
|
|
342
344
|
return [];
|
|
343
345
|
}
|
|
344
346
|
async function propagateSubagentsForCopilot(projectRoot, subagents, options) {
|
|
@@ -361,7 +363,7 @@ async function propagateSubagentsForCopilot(projectRoot, subagents, options) {
|
|
|
361
363
|
name: getSourceRelativeMdPath(s),
|
|
362
364
|
content: buildCopilotFile(s, false, verbose).content,
|
|
363
365
|
}));
|
|
364
|
-
await writeAgentsDirectoryAtomic(targetDir, files);
|
|
366
|
+
await writeAgentsDirectoryAtomic(targetDir, files, projectRoot);
|
|
365
367
|
return [];
|
|
366
368
|
}
|
|
367
369
|
/* ------------------------------------------------------------------ */
|
|
@@ -379,6 +381,7 @@ async function cleanupSubagentsDir(projectRoot, relPath, dryRun, verbose) {
|
|
|
379
381
|
(0, constants_1.logVerboseInfo)(`DRY RUN: Would remove ${relPath}`, verbose, dryRun);
|
|
380
382
|
return;
|
|
381
383
|
}
|
|
384
|
+
await (0, FileSystemUtils_1.assertManagedPathInsideRoot)(target, projectRoot, 'Refusing to remove subagents directory through symlinked path');
|
|
382
385
|
await fs.rm(target, { recursive: true, force: true });
|
|
383
386
|
(0, constants_1.logVerboseInfo)(`Removed ${relPath} (subagents disabled)`, verbose, dryRun);
|
|
384
387
|
}
|
|
@@ -40,6 +40,18 @@ const toml_1 = require("@iarna/toml");
|
|
|
40
40
|
const hash_1 = require("./hash");
|
|
41
41
|
const RuleProcessor_1 = require("./RuleProcessor");
|
|
42
42
|
const FileSystemUtils = __importStar(require("./FileSystemUtils"));
|
|
43
|
+
const KNOWN_MCP_SERVER_FIELDS = new Set([
|
|
44
|
+
'type',
|
|
45
|
+
'command',
|
|
46
|
+
'args',
|
|
47
|
+
'env',
|
|
48
|
+
'url',
|
|
49
|
+
'headers',
|
|
50
|
+
'timeout',
|
|
51
|
+
]);
|
|
52
|
+
function copyAdditionalMcpServerFields(def) {
|
|
53
|
+
return Object.fromEntries(Object.entries(def).filter(([key]) => !KNOWN_MCP_SERVER_FIELDS.has(key)));
|
|
54
|
+
}
|
|
43
55
|
async function loadUnifiedConfig(options) {
|
|
44
56
|
// Resolve the effective .ruler directory (local or global), mirroring the main loader behavior
|
|
45
57
|
const resolvedRulerDir = (await FileSystemUtils.findRulerDir(options.projectRoot, options.checkGlobal ?? true)) || path.join(options.projectRoot, '.ruler');
|
|
@@ -169,7 +181,7 @@ async function loadUnifiedConfig(options) {
|
|
|
169
181
|
if (!def || typeof def !== 'object')
|
|
170
182
|
continue;
|
|
171
183
|
const serverDef = def;
|
|
172
|
-
const server =
|
|
184
|
+
const server = copyAdditionalMcpServerFields(serverDef);
|
|
173
185
|
// Parse command and args
|
|
174
186
|
if (typeof serverDef.command === 'string') {
|
|
175
187
|
server.command = serverDef.command;
|
|
@@ -319,7 +331,7 @@ async function loadUnifiedConfig(options) {
|
|
|
319
331
|
});
|
|
320
332
|
continue;
|
|
321
333
|
}
|
|
322
|
-
const server =
|
|
334
|
+
const server = copyAdditionalMcpServerFields(def);
|
|
323
335
|
if (typeof def.command === 'string')
|
|
324
336
|
server.command = def.command;
|
|
325
337
|
if (Array.isArray(def.command))
|
|
@@ -337,6 +349,46 @@ async function loadUnifiedConfig(options) {
|
|
|
337
349
|
if (typeof def.timeout === 'number') {
|
|
338
350
|
server.timeout = def.timeout;
|
|
339
351
|
}
|
|
352
|
+
const hasCommand = !!server.command;
|
|
353
|
+
const hasUrl = !!server.url;
|
|
354
|
+
if (!hasCommand && !hasUrl) {
|
|
355
|
+
diagnostics.push({
|
|
356
|
+
severity: 'warning',
|
|
357
|
+
code: 'MCP_JSON_INVALID_SERVER',
|
|
358
|
+
message: `MCP server '${name}' must have at least one of command or url`,
|
|
359
|
+
file: mcpFile,
|
|
360
|
+
});
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (hasCommand && hasUrl) {
|
|
364
|
+
diagnostics.push({
|
|
365
|
+
severity: 'warning',
|
|
366
|
+
code: 'MCP_JSON_FIELD_CONFLICT',
|
|
367
|
+
message: `MCP server '${name}' has both command and url - using url (remote)`,
|
|
368
|
+
file: mcpFile,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
if (hasCommand && server.headers) {
|
|
372
|
+
diagnostics.push({
|
|
373
|
+
severity: 'warning',
|
|
374
|
+
code: 'MCP_JSON_FIELD_CONFLICT',
|
|
375
|
+
message: `MCP server '${name}' has headers with command (should be used with url only)`,
|
|
376
|
+
file: mcpFile,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
if (hasUrl && server.env) {
|
|
380
|
+
diagnostics.push({
|
|
381
|
+
severity: 'warning',
|
|
382
|
+
code: 'MCP_JSON_FIELD_CONFLICT',
|
|
383
|
+
message: `MCP server '${name}' has env with url (should be used with command only)`,
|
|
384
|
+
file: mcpFile,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (hasCommand && hasUrl) {
|
|
388
|
+
delete server.command;
|
|
389
|
+
delete server.args;
|
|
390
|
+
delete server.env;
|
|
391
|
+
}
|
|
340
392
|
// Derive type
|
|
341
393
|
if (server.url)
|
|
342
394
|
server.type = 'remote';
|
|
@@ -35,6 +35,7 @@ export interface AgentTomlConfig {
|
|
|
35
35
|
outputPathInstructions?: string;
|
|
36
36
|
outputPathConfig?: string;
|
|
37
37
|
mcp?: McpConfig;
|
|
38
|
+
mcpServers?: Record<string, McpServerDef>;
|
|
38
39
|
source: AgentConfigSourceMeta;
|
|
39
40
|
}
|
|
40
41
|
export interface AgentConfigSourceMeta {
|
|
@@ -61,7 +62,8 @@ export interface McpBundle {
|
|
|
61
62
|
hash: string;
|
|
62
63
|
}
|
|
63
64
|
export interface McpServerDef {
|
|
64
|
-
|
|
65
|
+
[key: string]: unknown;
|
|
66
|
+
type?: string;
|
|
65
67
|
command?: string;
|
|
66
68
|
args?: string[];
|
|
67
69
|
env?: Record<string, string>;
|
|
@@ -22,11 +22,15 @@ function agentMatchesFilter(agent, filter, validAgentIdentifiers) {
|
|
|
22
22
|
function resolveSelectedAgents(config, allAgents) {
|
|
23
23
|
// CLI --agents > config.default_agents > per-agent.enabled flags > default all
|
|
24
24
|
let selected = allAgents;
|
|
25
|
+
const validAgentIdentifiers = new Set(allAgents.map((agent) => agent.getIdentifier()));
|
|
26
|
+
const validAgentNames = new Set(allAgents.map((agent) => agent.getName().toLowerCase()));
|
|
27
|
+
const invalidConfiguredAgents = Object.keys(config.agentConfigs).filter((identifier) => !validAgentIdentifiers.has(identifier));
|
|
28
|
+
if (invalidConfiguredAgents.length > 0) {
|
|
29
|
+
throw (0, constants_1.createRulerError)(`Invalid agent configured: ${invalidConfiguredAgents.join(', ')}`, `Valid agents are: ${[...validAgentIdentifiers].join(', ')}`);
|
|
30
|
+
}
|
|
25
31
|
if (config.cliAgents && config.cliAgents.length > 0) {
|
|
26
32
|
const filters = config.cliAgents.map((n) => n.toLowerCase());
|
|
27
33
|
// Check if any of the specified agents don't exist
|
|
28
|
-
const validAgentIdentifiers = new Set(allAgents.map((agent) => agent.getIdentifier()));
|
|
29
|
-
const validAgentNames = new Set(allAgents.map((agent) => agent.getName().toLowerCase()));
|
|
30
34
|
const invalidAgents = filters.filter((filter) => !validAgentIdentifiers.has(filter) &&
|
|
31
35
|
![...validAgentNames].some((name) => name.includes(filter)));
|
|
32
36
|
if (invalidAgents.length > 0) {
|
|
@@ -37,8 +41,6 @@ function resolveSelectedAgents(config, allAgents) {
|
|
|
37
41
|
else if (config.defaultAgents && config.defaultAgents.length > 0) {
|
|
38
42
|
const defaults = config.defaultAgents.map((n) => n.toLowerCase());
|
|
39
43
|
// Check if any of the default agents don't exist
|
|
40
|
-
const validAgentIdentifiers = new Set(allAgents.map((agent) => agent.getIdentifier()));
|
|
41
|
-
const validAgentNames = new Set(allAgents.map((agent) => agent.getName().toLowerCase()));
|
|
42
44
|
const invalidAgents = defaults.filter((filter) => !validAgentIdentifiers.has(filter) &&
|
|
43
45
|
![...validAgentNames].some((name) => name.includes(filter)));
|
|
44
46
|
if (invalidAgents.length > 0) {
|