rulesync 38.0.0 → 40.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/cli/index.cjs +626 -385
- package/dist/cli/index.js +626 -385
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-B9NOtObR.cjs → import-BRZzK3w5.cjs} +3099 -204
- package/dist/{import-CoFTCNq7.js → import-Chn7Nqnm.js} +3072 -189
- package/dist/{import-CoFTCNq7.js.map → import-Chn7Nqnm.js.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +26 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +7 -7
|
@@ -522,6 +522,7 @@ const rulesProcessorToolTargetTuple = [
|
|
|
522
522
|
"cursor",
|
|
523
523
|
"deepagents",
|
|
524
524
|
"factorydroid",
|
|
525
|
+
"firebender",
|
|
525
526
|
"goose",
|
|
526
527
|
"grokcli",
|
|
527
528
|
"gitlabduo",
|
|
@@ -551,6 +552,7 @@ const rulesProcessorToolTargetTuple = [
|
|
|
551
552
|
"trae",
|
|
552
553
|
"vibe",
|
|
553
554
|
"warp",
|
|
555
|
+
"xum",
|
|
554
556
|
"devin",
|
|
555
557
|
"devin-plugin",
|
|
556
558
|
"zcode",
|
|
@@ -560,6 +562,7 @@ const rulesProcessorToolTargetTuple = [
|
|
|
560
562
|
];
|
|
561
563
|
const ignoreProcessorToolTargetTuple = [
|
|
562
564
|
"aiassistant",
|
|
565
|
+
"androidstudio",
|
|
563
566
|
"antigravity-cli",
|
|
564
567
|
"augmentcode",
|
|
565
568
|
"bob",
|
|
@@ -570,6 +573,7 @@ const ignoreProcessorToolTargetTuple = [
|
|
|
570
573
|
"continue",
|
|
571
574
|
"crush",
|
|
572
575
|
"cursor",
|
|
576
|
+
"firebender",
|
|
573
577
|
"grokcli",
|
|
574
578
|
"hermesagent",
|
|
575
579
|
"junie",
|
|
@@ -617,6 +621,7 @@ const mcpProcessorToolTargetTuple = [
|
|
|
617
621
|
"deepagents",
|
|
618
622
|
"dsh",
|
|
619
623
|
"factorydroid",
|
|
624
|
+
"firebender",
|
|
620
625
|
"goose",
|
|
621
626
|
"grokcli",
|
|
622
627
|
"gitlabduo",
|
|
@@ -646,6 +651,7 @@ const mcpProcessorToolTargetTuple = [
|
|
|
646
651
|
"vibe-plugin",
|
|
647
652
|
"warp",
|
|
648
653
|
"warpcli",
|
|
654
|
+
"xum",
|
|
649
655
|
"devin",
|
|
650
656
|
"devin-plugin",
|
|
651
657
|
"zcode",
|
|
@@ -672,6 +678,7 @@ const commandsProcessorToolTargetTuple = [
|
|
|
672
678
|
"copilot",
|
|
673
679
|
"cursor",
|
|
674
680
|
"factorydroid",
|
|
681
|
+
"firebender",
|
|
675
682
|
"goose",
|
|
676
683
|
"grokcli",
|
|
677
684
|
"gitlabduo",
|
|
@@ -727,6 +734,7 @@ const subagentsProcessorToolTargetTuple = [
|
|
|
727
734
|
"devin",
|
|
728
735
|
"devin-plugin",
|
|
729
736
|
"factorydroid",
|
|
737
|
+
"firebender",
|
|
730
738
|
"goose",
|
|
731
739
|
"grokcli",
|
|
732
740
|
"hermesagent",
|
|
@@ -751,6 +759,7 @@ const subagentsProcessorToolTargetTuple = [
|
|
|
751
759
|
"takt",
|
|
752
760
|
"vibe",
|
|
753
761
|
"vibe-plugin",
|
|
762
|
+
"xum",
|
|
754
763
|
"zcode",
|
|
755
764
|
"zcode-plugin"
|
|
756
765
|
];
|
|
@@ -758,6 +767,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
758
767
|
"agentsmd",
|
|
759
768
|
"agentsskills",
|
|
760
769
|
"aiassistant",
|
|
770
|
+
"androidstudio",
|
|
761
771
|
"amp",
|
|
762
772
|
"antigravity-cli",
|
|
763
773
|
"antigravity-ide",
|
|
@@ -781,6 +791,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
781
791
|
"cursor",
|
|
782
792
|
"deepagents",
|
|
783
793
|
"factorydroid",
|
|
794
|
+
"firebender",
|
|
784
795
|
"goose",
|
|
785
796
|
"grokcli",
|
|
786
797
|
"gitlabduo",
|
|
@@ -810,6 +821,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
810
821
|
"vibe",
|
|
811
822
|
"vibe-plugin",
|
|
812
823
|
"warp",
|
|
824
|
+
"xum",
|
|
813
825
|
"devin",
|
|
814
826
|
"devin-plugin",
|
|
815
827
|
"zcode",
|
|
@@ -844,6 +856,7 @@ const hooksProcessorToolTargetTuple = [
|
|
|
844
856
|
"pi",
|
|
845
857
|
"pool",
|
|
846
858
|
"factorydroid",
|
|
859
|
+
"firebender",
|
|
847
860
|
"goose",
|
|
848
861
|
"hermesagent",
|
|
849
862
|
"kimi-code",
|
|
@@ -1035,6 +1048,57 @@ async function assertWritablePathInsideRoot(params) {
|
|
|
1035
1048
|
existingPath = parentPath;
|
|
1036
1049
|
}
|
|
1037
1050
|
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Thrown by {@link assertPathResolvesInsideRoot} when a symbolic link leads
|
|
1053
|
+
* the target outside the root, or the target is reached through a dangling
|
|
1054
|
+
* link. Any other error it throws comes from the filesystem itself.
|
|
1055
|
+
*/
|
|
1056
|
+
var PathOutsideRootError = class extends Error {
|
|
1057
|
+
constructor(message, options) {
|
|
1058
|
+
super(message, options);
|
|
1059
|
+
this.name = "PathOutsideRootError";
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
/**
|
|
1063
|
+
* Refuse a write or removal target whose real location is outside `rootPath`.
|
|
1064
|
+
*
|
|
1065
|
+
* Unlike {@link assertWritablePathInsideRoot}, a symbolic link on the way is
|
|
1066
|
+
* allowed as long as it resolves inside the root: projects deliberately link a
|
|
1067
|
+
* skill directory to a shared one next to it. What this stops is a link that
|
|
1068
|
+
* leads elsewhere — a cloned repository committing `.rulesync/skills/x ->
|
|
1069
|
+
* ~/.ssh`, say — which a textual check such as {@link checkPathTraversal}
|
|
1070
|
+
* cannot see. The deepest existing ancestor of `targetPath` is resolved, and
|
|
1071
|
+
* an existing target that is itself a link is resolved too.
|
|
1072
|
+
*/
|
|
1073
|
+
async function assertPathResolvesInsideRoot(params) {
|
|
1074
|
+
const { rootPath, targetPath } = params;
|
|
1075
|
+
let realRoot;
|
|
1076
|
+
try {
|
|
1077
|
+
realRoot = await realpath(rootPath);
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
if (isFileNotFoundError(error)) {
|
|
1080
|
+
if (await isSymlink(rootPath)) throw new PathOutsideRootError(`Refusing to write through a dangling symbolic link: ${rootPath}.`, { cause: error });
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
throw error;
|
|
1084
|
+
}
|
|
1085
|
+
let existingPath = resolve(targetPath);
|
|
1086
|
+
while (true) {
|
|
1087
|
+
let realExisting;
|
|
1088
|
+
try {
|
|
1089
|
+
realExisting = await realpath(existingPath);
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
1092
|
+
if (await isSymlink(existingPath)) throw new PathOutsideRootError(`Refusing to write through a dangling symbolic link: ${targetPath}.`, { cause: error });
|
|
1093
|
+
const parentPath = dirname(existingPath);
|
|
1094
|
+
if (parentPath === existingPath) throw error;
|
|
1095
|
+
existingPath = parentPath;
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
if (pathEscapesRoot(relative(realRoot, realExisting))) throw new PathOutsideRootError(`Path resolves outside ${rootPath} through a symbolic link: ${targetPath}.`);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1038
1102
|
async function assertTreeContainsNoSymlinks(dirPath) {
|
|
1039
1103
|
for (const entry of await readdir(dirPath, { withFileTypes: true })) {
|
|
1040
1104
|
const entryPath = join(dirPath, entry.name);
|
|
@@ -3758,6 +3822,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
|
|
|
3758
3822
|
".devin/config.json",
|
|
3759
3823
|
".dsh/cordis.patch.yml",
|
|
3760
3824
|
".factory/settings.json",
|
|
3825
|
+
"firebender.json",
|
|
3826
|
+
".firebender/firebender.json",
|
|
3761
3827
|
".grok/config.toml",
|
|
3762
3828
|
".grok/sandbox.toml",
|
|
3763
3829
|
".letta/settings.json",
|
|
@@ -4219,14 +4285,103 @@ function stringifyFrontmatter(body, frontmatter, options) {
|
|
|
4219
4285
|
} } });
|
|
4220
4286
|
return matter.stringify(file, cleanFrontmatter);
|
|
4221
4287
|
}
|
|
4288
|
+
/**
|
|
4289
|
+
* Thrown in place of gray-matter's built-in JavaScript engine.
|
|
4290
|
+
*
|
|
4291
|
+
* gray-matter picks the parser from the text after the opening fence, so a
|
|
4292
|
+
* file that opens with `---js` or `---javascript` has its frontmatter handed
|
|
4293
|
+
* to `eval()`. Frontmatter here comes from third-party skills, fetched sources
|
|
4294
|
+
* and untrusted repositories, so that would run arbitrary code on the machine
|
|
4295
|
+
* of whoever generates or imports it. Every engine name that resolves to code
|
|
4296
|
+
* execution is mapped to this refusal; {@link assertYamlFrontmatterLanguage}
|
|
4297
|
+
* refuses those fences earlier still, so this is the second line of defense.
|
|
4298
|
+
*/
|
|
4299
|
+
function refuseExecutableFrontmatter() {
|
|
4300
|
+
throw new Error("Frontmatter written in an executable language (JavaScript / CoffeeScript) is not supported; use YAML");
|
|
4301
|
+
}
|
|
4302
|
+
const REFUSED_FRONTMATTER_ENGINE = {
|
|
4303
|
+
parse: refuseExecutableFrontmatter,
|
|
4304
|
+
stringify: refuseExecutableFrontmatter
|
|
4305
|
+
};
|
|
4306
|
+
/**
|
|
4307
|
+
* Engines passed to every gray-matter parse. Supplying them merges over
|
|
4308
|
+
* gray-matter's defaults, so the JavaScript engine (and the CoffeeScript names
|
|
4309
|
+
* a plugin could register) can never be reached through an alias.
|
|
4310
|
+
*
|
|
4311
|
+
* `patches/gray-matter@4.0.3.patch` also deletes the JavaScript engine, but a
|
|
4312
|
+
* pnpm patch only applies inside this repository: gray-matter is an external
|
|
4313
|
+
* dependency of the published package, so an installed rulesync runs the
|
|
4314
|
+
* unpatched gray-matter, and this override plus the language check are what
|
|
4315
|
+
* protect it. Exported for the test that pins that.
|
|
4316
|
+
*/
|
|
4317
|
+
const FRONTMATTER_PARSE_OPTIONS = { engines: {
|
|
4318
|
+
javascript: REFUSED_FRONTMATTER_ENGINE,
|
|
4319
|
+
js: REFUSED_FRONTMATTER_ENGINE,
|
|
4320
|
+
coffee: REFUSED_FRONTMATTER_ENGINE,
|
|
4321
|
+
coffeescript: REFUSED_FRONTMATTER_ENGINE,
|
|
4322
|
+
cson: REFUSED_FRONTMATTER_ENGINE
|
|
4323
|
+
} };
|
|
4324
|
+
/** Frontmatter languages accepted after the opening `---` fence. */
|
|
4325
|
+
const ALLOWED_FRONTMATTER_LANGUAGE_PATTERN = /^(?:ya?ml)?$/i;
|
|
4326
|
+
/**
|
|
4327
|
+
* Read the opening frontmatter fence the way gray-matter does, or return
|
|
4328
|
+
* `undefined` when gray-matter would see no frontmatter at all.
|
|
4329
|
+
*
|
|
4330
|
+
* gray-matter strips one byte order mark, treats `---` as a fence only when a
|
|
4331
|
+
* fourth `-` does not follow, and takes the language from the text between the
|
|
4332
|
+
* fence and the first `\r?\n`, trimmed — dropping the last character when
|
|
4333
|
+
* there is no line break at all. Every check below that has to agree with
|
|
4334
|
+
* gray-matter on where the block is (the language check, the size guard, the
|
|
4335
|
+
* repair pass) goes through this one reading, so none of them can be bypassed
|
|
4336
|
+
* by a fence line gray-matter accepts and a stricter pattern would not (a
|
|
4337
|
+
* stray `\r` before the line break, say).
|
|
4338
|
+
*/
|
|
4339
|
+
function readFrontmatterFence(content) {
|
|
4340
|
+
const offset = content.startsWith("") ? 1 : 0;
|
|
4341
|
+
const text = content.slice(offset);
|
|
4342
|
+
if (!text.startsWith("---") || text.charAt(3) === "-") return;
|
|
4343
|
+
const afterFence = text.slice(3);
|
|
4344
|
+
const lineBreak = /\r?\n/.exec(afterFence);
|
|
4345
|
+
const language = afterFence.slice(0, lineBreak ? lineBreak.index : -1).trim();
|
|
4346
|
+
const lineBreakStart = lineBreak ? offset + 3 + lineBreak.index : void 0;
|
|
4347
|
+
return {
|
|
4348
|
+
language,
|
|
4349
|
+
lineBreakStart,
|
|
4350
|
+
lineEnd: lineBreak && lineBreakStart !== void 0 ? lineBreakStart + lineBreak[0].length : void 0
|
|
4351
|
+
};
|
|
4352
|
+
}
|
|
4353
|
+
/**
|
|
4354
|
+
* Refuse a frontmatter fence that names any language other than YAML.
|
|
4355
|
+
*
|
|
4356
|
+
* Only YAML is ever written by rulesync or by the tools it targets, so anything
|
|
4357
|
+
* else — `---js` above all, whose block gray-matter would `eval()` — is
|
|
4358
|
+
* rejected before gray-matter sees the content.
|
|
4359
|
+
*/
|
|
4360
|
+
function assertYamlFrontmatterLanguage(content) {
|
|
4361
|
+
const fence = readFrontmatterFence(content);
|
|
4362
|
+
if (fence && !ALLOWED_FRONTMATTER_LANGUAGE_PATTERN.test(fence.language)) throw new Error(`Unsupported frontmatter language ${JSON.stringify(fence.language.slice(0, 40))}; only YAML frontmatter is supported`);
|
|
4363
|
+
}
|
|
4364
|
+
/**
|
|
4365
|
+
* The number of characters gray-matter would hand to the YAML parser as the
|
|
4366
|
+
* frontmatter block. Unlike {@link findFrontmatterBlockBounds}, a missing
|
|
4367
|
+
* closing fence does not make this zero: gray-matter then parses everything
|
|
4368
|
+
* after the fence line, so that is what the size guard has to measure.
|
|
4369
|
+
*/
|
|
4370
|
+
function measureFrontmatterBlockChars(content) {
|
|
4371
|
+
const fence = readFrontmatterFence(content);
|
|
4372
|
+
if (fence?.lineBreakStart === void 0) return 0;
|
|
4373
|
+
const rest = content.slice(fence.lineBreakStart);
|
|
4374
|
+
const closeIndex = rest.indexOf("\n---");
|
|
4375
|
+
return closeIndex === -1 ? rest.length : closeIndex;
|
|
4376
|
+
}
|
|
4222
4377
|
function parseFrontmatter(content, filePath) {
|
|
4223
4378
|
let frontmatter;
|
|
4224
4379
|
let body;
|
|
4225
4380
|
let hasFrontmatter;
|
|
4226
4381
|
try {
|
|
4227
|
-
|
|
4228
|
-
if (
|
|
4229
|
-
const result = matter(content,
|
|
4382
|
+
assertYamlFrontmatterLanguage(content);
|
|
4383
|
+
if (measureFrontmatterBlockChars(content) > 65536) throw new Error(`Frontmatter block is larger than ${MAX_FRONTMATTER_RAW_CHARS} characters; refusing to parse it (a complex YAML key can cost memory while parsing, before any post-parse budget applies)`);
|
|
4384
|
+
const result = matter(content, FRONTMATTER_PARSE_OPTIONS);
|
|
4230
4385
|
frontmatter = deepRemoveNullishObject(result.data);
|
|
4231
4386
|
body = result.content;
|
|
4232
4387
|
hasFrontmatter = result.matter !== "" || content.trimStart().startsWith("---");
|
|
@@ -4285,22 +4440,28 @@ function repairFrontmatterLine(line) {
|
|
|
4285
4440
|
}
|
|
4286
4441
|
/**
|
|
4287
4442
|
* Locate a raw `---`-delimited frontmatter block's bounds within `content`,
|
|
4288
|
-
* without parsing it
|
|
4289
|
-
*
|
|
4290
|
-
*
|
|
4291
|
-
*
|
|
4292
|
-
*
|
|
4293
|
-
*
|
|
4443
|
+
* without parsing it, for the YAML repair pass below and the rule loaders that
|
|
4444
|
+
* pre-process `globs:`. The opening fence is read exactly as gray-matter reads
|
|
4445
|
+
* it ({@link readFrontmatterFence}); an explicit `yaml` / `yml` language is
|
|
4446
|
+
* still YAML, any other language yields no bounds. gray-matter ends the block
|
|
4447
|
+
* at the first `\n---`, with no requirement that the delimiter be alone on its
|
|
4448
|
+
* line, so a stricter pattern here would run past gray-matter's delimiter and
|
|
4449
|
+
* act on text that is really the body. A block with no closing fence has no
|
|
4450
|
+
* bounds; the size guard measures that case separately.
|
|
4294
4451
|
*/
|
|
4295
4452
|
function findFrontmatterBlockBounds(content) {
|
|
4296
|
-
const
|
|
4297
|
-
if (!
|
|
4298
|
-
const blockStart =
|
|
4299
|
-
const closing = /\r?\n---/.exec(content.slice(
|
|
4453
|
+
const fence = readFrontmatterFence(content);
|
|
4454
|
+
if (fence?.lineBreakStart === void 0 || fence.lineEnd === void 0 || !ALLOWED_FRONTMATTER_LANGUAGE_PATTERN.test(fence.language)) return;
|
|
4455
|
+
const blockStart = fence.lineEnd;
|
|
4456
|
+
const closing = /\r?\n---/.exec(content.slice(fence.lineBreakStart));
|
|
4300
4457
|
if (!closing) return;
|
|
4458
|
+
if (closing.index === 0) return {
|
|
4459
|
+
blockStart,
|
|
4460
|
+
blockEnd: blockStart
|
|
4461
|
+
};
|
|
4301
4462
|
return {
|
|
4302
4463
|
blockStart,
|
|
4303
|
-
blockEnd:
|
|
4464
|
+
blockEnd: fence.lineBreakStart + closing.index
|
|
4304
4465
|
};
|
|
4305
4466
|
}
|
|
4306
4467
|
/**
|
|
@@ -4332,8 +4493,9 @@ function repairMalformedFrontmatterYaml(content) {
|
|
|
4332
4493
|
* narrow — one pass, top-level entries only, and the original error is what
|
|
4333
4494
|
* surfaces if it does not help, so a genuinely broken file still fails with the
|
|
4334
4495
|
* message that describes what is actually wrong with it. A file with no closing
|
|
4335
|
-
*
|
|
4336
|
-
*
|
|
4496
|
+
* `---` is not repaired at all, and one whose opening fence names a language
|
|
4497
|
+
* other than YAML is refused outright (an explicit `---yaml` / `---yml` fence
|
|
4498
|
+
* is repaired like a plain one).
|
|
4337
4499
|
*
|
|
4338
4500
|
* @see https://agentskills.io/client-implementation/adding-skills-support
|
|
4339
4501
|
*/
|
|
@@ -4698,6 +4860,29 @@ const CURSOR_HOOK_EVENTS = [
|
|
|
4698
4860
|
"workspaceOpen"
|
|
4699
4861
|
];
|
|
4700
4862
|
/**
|
|
4863
|
+
* Hook events supported by Firebender.
|
|
4864
|
+
*
|
|
4865
|
+
* Firebender's `hooks.json` follows Cursor's layout, and every event it fires
|
|
4866
|
+
* is spelled like its canonical counterpart, so no name mapping is needed.
|
|
4867
|
+
* @see https://docs.firebender.com/multi-agent/hooks
|
|
4868
|
+
*/
|
|
4869
|
+
const FIREBENDER_HOOK_EVENTS = [
|
|
4870
|
+
"sessionStart",
|
|
4871
|
+
"sessionEnd",
|
|
4872
|
+
"subagentStart",
|
|
4873
|
+
"subagentStop",
|
|
4874
|
+
"preToolUse",
|
|
4875
|
+
"postToolUse",
|
|
4876
|
+
"beforeShellExecution",
|
|
4877
|
+
"afterShellExecution",
|
|
4878
|
+
"beforeMCPExecution",
|
|
4879
|
+
"afterMCPExecution",
|
|
4880
|
+
"beforeReadFile",
|
|
4881
|
+
"afterFileEdit",
|
|
4882
|
+
"preCompact",
|
|
4883
|
+
"stop"
|
|
4884
|
+
];
|
|
4885
|
+
/**
|
|
4701
4886
|
* Hook events supported by Claude Code.
|
|
4702
4887
|
*
|
|
4703
4888
|
* Covers the full documented event surface.
|
|
@@ -4978,6 +5163,40 @@ const COPILOT_HOOK_EVENTS = [
|
|
|
4978
5163
|
"userPromptExpansion"
|
|
4979
5164
|
];
|
|
4980
5165
|
/**
|
|
5166
|
+
* The {@link COPILOT_HOOK_EVENTS} whose hook entries honor an optional
|
|
5167
|
+
* `matcher` regex. The hooks reference documents matcher filtering on
|
|
5168
|
+
* `preToolUse` / `postToolUse` / `permissionRequest` (tool name), `preCompact`
|
|
5169
|
+
* (trigger) and `subagentStart` (agent name), plus `notification`; the last
|
|
5170
|
+
* two are not authorable for `copilot`, so they are not listed here. VS Code's
|
|
5171
|
+
* migration guide confirms the Copilot harness "applies documented matcher
|
|
5172
|
+
* rules" (only the legacy Local harness ignores them).
|
|
5173
|
+
*
|
|
5174
|
+
* @see https://docs.github.com/en/copilot/reference/hooks-reference
|
|
5175
|
+
* @see https://code.visualstudio.com/docs/agent-customization/migrate-customizations
|
|
5176
|
+
*/
|
|
5177
|
+
const COPILOT_MATCHER_HOOK_EVENTS = [
|
|
5178
|
+
"preToolUse",
|
|
5179
|
+
"postToolUse",
|
|
5180
|
+
"preCompact",
|
|
5181
|
+
"subagentStart"
|
|
5182
|
+
];
|
|
5183
|
+
/**
|
|
5184
|
+
* The {@link COPILOTCLI_HOOK_EVENTS} whose hook entries honor an optional
|
|
5185
|
+
* `matcher` regex: the shared {@link COPILOT_MATCHER_HOOK_EVENTS} plus the two
|
|
5186
|
+
* CLI-only events, `permissionRequest` (tool name) and `notification`
|
|
5187
|
+
* (notification type).
|
|
5188
|
+
*
|
|
5189
|
+
* Confirmed by the upstream changelog: v1.0.36 (2026-04-24, `preToolUse.matcher`
|
|
5190
|
+
* honored) and v1.0.63 (2026-06-15, "PostToolUse hook matchers (e.g.
|
|
5191
|
+
* `Edit|Write`) are now honored instead of silently dropped").
|
|
5192
|
+
* @see https://docs.github.com/en/copilot/reference/hooks-reference
|
|
5193
|
+
*/
|
|
5194
|
+
const COPILOTCLI_MATCHER_HOOK_EVENTS = [
|
|
5195
|
+
...COPILOT_MATCHER_HOOK_EVENTS,
|
|
5196
|
+
"permissionRequest",
|
|
5197
|
+
"notification"
|
|
5198
|
+
];
|
|
5199
|
+
/**
|
|
4981
5200
|
* Hook events supported by the GitHub Copilot CLI (`copilotcli-hooks.ts`).
|
|
4982
5201
|
*
|
|
4983
5202
|
* The CLI documents a wider event surface than the shared cloud-agent set, so
|
|
@@ -5131,12 +5350,15 @@ const KIRO_IDE_HOOK_EVENTS = [
|
|
|
5131
5350
|
/**
|
|
5132
5351
|
* Hook events supported by Google Antigravity (both the IDE and the CLI).
|
|
5133
5352
|
*
|
|
5134
|
-
* Antigravity exposes a Claude-style hooks surface covering the
|
|
5135
|
-
* tool/model/turn lifecycle events it documents: `
|
|
5136
|
-
* `PreInvocation`, `PostInvocation`, and `Stop`.
|
|
5137
|
-
* (`PreInvocation`/`PostInvocation`)
|
|
5353
|
+
* Antigravity exposes a Claude-style hooks surface covering the six
|
|
5354
|
+
* session/tool/model/turn lifecycle events it documents: `SessionStart`,
|
|
5355
|
+
* `PreToolUse`, `PostToolUse`, `PreInvocation`, `PostInvocation`, and `Stop`.
|
|
5356
|
+
* `SessionStart`, the model-invocation events (`PreInvocation`/`PostInvocation`)
|
|
5357
|
+
* and `Stop` are matcher-less handler lists.
|
|
5358
|
+
* https://antigravity.google/docs/hooks
|
|
5138
5359
|
*/
|
|
5139
5360
|
const ANTIGRAVITY_HOOK_EVENTS = [
|
|
5361
|
+
"sessionStart",
|
|
5140
5362
|
"preToolUse",
|
|
5141
5363
|
"postToolUse",
|
|
5142
5364
|
"preModelInvocation",
|
|
@@ -5909,8 +6131,8 @@ const CONTINUE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANO
|
|
|
5909
6131
|
/**
|
|
5910
6132
|
* Hook events supported by Hermes Agent's native Shell Hooks system.
|
|
5911
6133
|
*
|
|
5912
|
-
* Hermes validates hook events against a fixed `VALID_HOOKS` set —
|
|
5913
|
-
* of v0.
|
|
6134
|
+
* Hermes validates hook events against a fixed `VALID_HOOKS` set — 46 entries as
|
|
6135
|
+
* of v0.22.3 (tag `v0.22.3`); see {@link HERMESAGENT_NATIVE_HOOK_EVENTS} for the
|
|
5914
6136
|
* full list. Only the events with a clean 1:1 canonical equivalent are mapped
|
|
5915
6137
|
* here. All other native events round-trip through `hermesagent.hooks`.
|
|
5916
6138
|
* @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
|
|
@@ -5934,15 +6156,16 @@ const HERMESAGENT_HOOK_EVENTS = [
|
|
|
5934
6156
|
* observers, `gateway_platform_event` and `transform_api_error_classification`.
|
|
5935
6157
|
* Shell hooks are gated on the same set.
|
|
5936
6158
|
*
|
|
5937
|
-
* Verified against the tag `v0.
|
|
6159
|
+
* Verified against the tag `v0.22.3`: `VALID_HOOKS` holds 46 entries, grown
|
|
5938
6160
|
* from 23 at `v2026.8.3` (v0.20.0) and 37 at `v2026.8.16` (v0.20.2) — v0.21
|
|
5939
6161
|
* added `agent_loop_stopped` and `on_room_member_activity`, v0.21.5 added
|
|
5940
|
-
* `pre_auxiliary_call` and `post_auxiliary_call`,
|
|
6162
|
+
* `pre_auxiliary_call` and `post_auxiliary_call`, v0.21.6 added
|
|
5941
6163
|
* `post_gateway_admission`, `on_human_input_request` and
|
|
5942
|
-
* `on_human_input_resolved
|
|
6164
|
+
* `on_human_input_resolved`, and v0.22.3 added `on_session_delete` and
|
|
6165
|
+
* `post_compaction`. This list holds 45 of them — see
|
|
5943
6166
|
* {@link HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS} for the exclusion.
|
|
5944
6167
|
*
|
|
5945
|
-
* @see https://github.com/NousResearch/hermes-agent/blob/v0.
|
|
6168
|
+
* @see https://github.com/NousResearch/hermes-agent/blob/v0.22.3/hermes_cli/plugins.py
|
|
5946
6169
|
*/
|
|
5947
6170
|
const HERMESAGENT_NATIVE_HOOK_EVENTS = [
|
|
5948
6171
|
"pre_tool_call",
|
|
@@ -5966,6 +6189,7 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
|
|
|
5966
6189
|
"on_session_end",
|
|
5967
6190
|
"on_session_finalize",
|
|
5968
6191
|
"on_session_reset",
|
|
6192
|
+
"on_session_delete",
|
|
5969
6193
|
"on_skill_lifecycle",
|
|
5970
6194
|
"subagent_start",
|
|
5971
6195
|
"subagent_stop",
|
|
@@ -5987,7 +6211,8 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
|
|
|
5987
6211
|
"on_kanban_task_updated",
|
|
5988
6212
|
"on_kanban_dispatch_tick",
|
|
5989
6213
|
"gateway_platform_event",
|
|
5990
|
-
"pre_command"
|
|
6214
|
+
"pre_command",
|
|
6215
|
+
"post_compaction"
|
|
5991
6216
|
];
|
|
5992
6217
|
/**
|
|
5993
6218
|
* `VALID_HOOKS` entries a SHELL hook may not register for. `VALID_HOOKS` doubles
|
|
@@ -5999,8 +6224,8 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
|
|
|
5999
6224
|
* rulesync only ever writes shell hooks, so these are excluded from
|
|
6000
6225
|
* {@link HERMESAGENT_NATIVE_HOOK_EVENTS} and warned about with their own message.
|
|
6001
6226
|
*
|
|
6002
|
-
* @see https://github.com/NousResearch/hermes-agent/blob/v0.
|
|
6003
|
-
* @see https://github.com/NousResearch/hermes-agent/blob/v0.
|
|
6227
|
+
* @see https://github.com/NousResearch/hermes-agent/blob/v0.22.3/hermes_cli/plugins.py — `SHELL_UNSUPPORTED_HOOKS`
|
|
6228
|
+
* @see https://github.com/NousResearch/hermes-agent/blob/v0.22.3/agent/shell_hooks.py — `_parse_hooks_block`
|
|
6004
6229
|
*/
|
|
6005
6230
|
const HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS = ["transform_api_error_classification"];
|
|
6006
6231
|
/**
|
|
@@ -6045,6 +6270,7 @@ const HooksConfigSchema = z.looseObject({
|
|
|
6045
6270
|
version: z.optional(z.number()),
|
|
6046
6271
|
hooks: canonicalHooksRecordSchema,
|
|
6047
6272
|
cursor: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
6273
|
+
firebender: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
6048
6274
|
claudecode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
6049
6275
|
copilot: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
6050
6276
|
copilotcli: z.optional(z.looseObject({
|
|
@@ -6181,10 +6407,11 @@ const CANONICAL_TO_AUGMENTCODE_EVENT_NAMES = {
|
|
|
6181
6407
|
const AUGMENTCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_AUGMENTCODE_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
6182
6408
|
/**
|
|
6183
6409
|
* Map canonical camelCase event names to Antigravity PascalCase.
|
|
6184
|
-
* Antigravity uses the same PascalCase names as Claude for its tool/turn events,
|
|
6410
|
+
* Antigravity uses the same PascalCase names as Claude for its session/tool/turn events,
|
|
6185
6411
|
* plus `PreInvocation`/`PostInvocation` for the model-invocation lifecycle.
|
|
6186
6412
|
*/
|
|
6187
6413
|
const CANONICAL_TO_ANTIGRAVITY_EVENT_NAMES = {
|
|
6414
|
+
sessionStart: "SessionStart",
|
|
6188
6415
|
preToolUse: "PreToolUse",
|
|
6189
6416
|
postToolUse: "PostToolUse",
|
|
6190
6417
|
preModelInvocation: "PreInvocation",
|
|
@@ -7148,7 +7375,7 @@ const McpServerSchema = z.looseObject({
|
|
|
7148
7375
|
crushSessionless: z.optional(z.boolean()),
|
|
7149
7376
|
crushChannelEnabled: z.optional(z.boolean()),
|
|
7150
7377
|
crushChannelReply: z.optional(CrushChannelReplySchema),
|
|
7151
|
-
zcodeOauth: z.optional(z.looseObject({
|
|
7378
|
+
zcodeOauth: z.optional(z.looseObject({})),
|
|
7152
7379
|
poolAllow: z.optional(z.array(z.string())),
|
|
7153
7380
|
headers: z.optional(z.record(z.string(), z.string())),
|
|
7154
7381
|
/**
|
|
@@ -7203,14 +7430,19 @@ const toolScopedMcpSchema = z.looseObject({ mcpServers: z.optional(z.record(z.st
|
|
|
7203
7430
|
/**
|
|
7204
7431
|
* Kimi Code's tool-scoped block also carries the `[mcp]` defaults from its
|
|
7205
7432
|
* shared user config: connect and per-tool-call timeouts that apply to *every*
|
|
7206
|
-
* MCP server, including ones rulesync did not write.
|
|
7207
|
-
* `
|
|
7433
|
+
* MCP server, including ones rulesync did not write, and (Kimi Code 2.3.0)
|
|
7434
|
+
* `defaultDeferred`, which loads every server's tools on demand unless the
|
|
7435
|
+
* server opts out. Per-server `startupTimeoutMs` / `toolTimeoutMs` / `deferred`
|
|
7436
|
+
* in `mcpServers` still win. `defaultDeferred` only takes effect when Kimi's
|
|
7437
|
+
* experimental `tool-select` flag is on, and is captured when a server connects
|
|
7438
|
+
* (MoonshotAI/kimi-code#4210).
|
|
7208
7439
|
*
|
|
7209
7440
|
* @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#mcp
|
|
7210
7441
|
*/
|
|
7211
7442
|
const kimiCodeScopedMcpSchema = z.extend(toolScopedMcpSchema, {
|
|
7212
7443
|
startupTimeoutMs: z.optional(z.number()),
|
|
7213
|
-
toolTimeoutMs: z.optional(z.number())
|
|
7444
|
+
toolTimeoutMs: z.optional(z.number()),
|
|
7445
|
+
defaultDeferred: z.optional(z.boolean())
|
|
7214
7446
|
});
|
|
7215
7447
|
/**
|
|
7216
7448
|
* Qwen Code's tool-scoped block also carries the server-level allow/deny lists
|
|
@@ -7250,6 +7482,7 @@ const RulesyncMcpFileSchema = z.looseObject({
|
|
|
7250
7482
|
devin: z.optional(toolScopedMcpSchema),
|
|
7251
7483
|
dsh: z.optional(toolScopedMcpSchema),
|
|
7252
7484
|
factorydroid: z.optional(toolScopedMcpSchema),
|
|
7485
|
+
firebender: z.optional(toolScopedMcpSchema),
|
|
7253
7486
|
gitlabduo: z.optional(toolScopedMcpSchema),
|
|
7254
7487
|
goose: z.optional(toolScopedMcpSchema),
|
|
7255
7488
|
grokcli: z.optional(toolScopedMcpSchema),
|
|
@@ -7274,6 +7507,7 @@ const RulesyncMcpFileSchema = z.looseObject({
|
|
|
7274
7507
|
vibe: z.optional(toolScopedMcpSchema),
|
|
7275
7508
|
warp: z.optional(toolScopedMcpSchema),
|
|
7276
7509
|
warpcli: z.optional(toolScopedMcpSchema),
|
|
7510
|
+
xum: z.optional(toolScopedMcpSchema),
|
|
7277
7511
|
zed: z.optional(toolScopedMcpSchema)
|
|
7278
7512
|
});
|
|
7279
7513
|
/**
|
|
@@ -7977,13 +8211,16 @@ const CopilotPermissionsOverrideSchema = z.looseObject({
|
|
|
7977
8211
|
});
|
|
7978
8212
|
/**
|
|
7979
8213
|
* oh-my-pi's `tools.approvalMode` values: the tool tiers it auto-approves
|
|
7980
|
-
* before prompting. `always-ask` auto-approves only read-only tools,
|
|
7981
|
-
*
|
|
8214
|
+
* before prompting. `always-ask` auto-approves only read-only tools,
|
|
8215
|
+
* `always-ask-cached` (Ask and remember) also prompts before file reads and
|
|
8216
|
+
* lets a prompt remember the granted file or folder, `write` also
|
|
8217
|
+
* auto-approves workspace writes, and `yolo` (oh-my-pi's default) every tier.
|
|
7982
8218
|
*
|
|
7983
8219
|
* @see https://github.com/can1357/oh-my-pi/blob/main/docs/approval-mode.md
|
|
7984
8220
|
*/
|
|
7985
8221
|
const OMP_APPROVAL_MODES = [
|
|
7986
8222
|
"always-ask",
|
|
8223
|
+
"always-ask-cached",
|
|
7987
8224
|
"write",
|
|
7988
8225
|
"yolo"
|
|
7989
8226
|
];
|
|
@@ -8004,10 +8241,13 @@ const KimiCodePermissionsOverrideSchema = z.looseObject({
|
|
|
8004
8241
|
])),
|
|
8005
8242
|
/**
|
|
8006
8243
|
* Kimi's `[permission] dangerous_command_guard` switch (Kimi Code 0.40.0,
|
|
8007
|
-
* default `true`). `false` turns off
|
|
8008
|
-
*
|
|
8244
|
+
* default `true`). Since Kimi Code 2.3.0, `false` turns off every built-in
|
|
8245
|
+
* guard confirmation policy: dangerous commands (`rm -rf`, `shutdown` and the
|
|
8246
|
+
* like), sensitive-file access (`.env`, `id_rsa`, ...) and git control-path
|
|
8247
|
+
* access. Before 2.3.0 it only covered dangerous commands.
|
|
8009
8248
|
*
|
|
8010
8249
|
* @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#permission
|
|
8250
|
+
* @see https://github.com/MoonshotAI/kimi-code/pull/4250
|
|
8011
8251
|
*/
|
|
8012
8252
|
dangerousCommandGuard: z.optional(z.boolean()),
|
|
8013
8253
|
rules: z.optional(z.array(z.looseObject({
|
|
@@ -8367,6 +8607,8 @@ const TabninePermissionsOverrideSchema = z.looseObject({
|
|
|
8367
8607
|
* { "allowDynamicBash": true, "rawAllow": ["Bash=pnpm test"] }
|
|
8368
8608
|
* @example
|
|
8369
8609
|
* { "mode": "deny" }
|
|
8610
|
+
* @example
|
|
8611
|
+
* { "tools": { "disabled": ["web_fetch"] }, "askOnExternalOrigin": true }
|
|
8370
8612
|
*/
|
|
8371
8613
|
const ReasonixPermissionsOverrideSchema = z.looseObject({
|
|
8372
8614
|
permission: z.optional(ToolScopedPermissionSchema),
|
|
@@ -8380,7 +8622,9 @@ const ReasonixPermissionsOverrideSchema = z.looseObject({
|
|
|
8380
8622
|
"ask",
|
|
8381
8623
|
"allow",
|
|
8382
8624
|
"deny"
|
|
8383
|
-
]))
|
|
8625
|
+
])),
|
|
8626
|
+
askOnExternalOrigin: z.optional(z.boolean()),
|
|
8627
|
+
tools: z.optional(z.strictObject({ disabled: z.optional(z.array(z.string())) }))
|
|
8384
8628
|
});
|
|
8385
8629
|
/**
|
|
8386
8630
|
* Tool-scoped override block for Qoder. `outputStyle` is written to the
|
|
@@ -11194,6 +11438,15 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
|
|
|
11194
11438
|
compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
|
|
11195
11439
|
metadata: z.optional(z.looseObject({}))
|
|
11196
11440
|
})),
|
|
11441
|
+
xum: z.optional(z.looseObject({
|
|
11442
|
+
"disable-model-invocation": z.optional(z.boolean()),
|
|
11443
|
+
advertise: z.optional(z.boolean()),
|
|
11444
|
+
"user-invocable": z.optional(z.boolean()),
|
|
11445
|
+
"argument-hint": z.optional(z.string()),
|
|
11446
|
+
when_to_use: z.optional(z.string()),
|
|
11447
|
+
"when-to-use": z.optional(z.string()),
|
|
11448
|
+
paths: z.optional(z.union([z.string(), z.array(z.string())]))
|
|
11449
|
+
})),
|
|
11197
11450
|
zcode: z.optional(z.looseObject({
|
|
11198
11451
|
when_to_use: z.optional(z.string()),
|
|
11199
11452
|
license: z.optional(z.string()),
|
|
@@ -11472,7 +11725,7 @@ async function getLocalSkillDirNames(sourceTree) {
|
|
|
11472
11725
|
* The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
|
|
11473
11726
|
* default that applies to every tool supporting the flag (claudecode, codebuddy, copilot,
|
|
11474
11727
|
* copilotcli, crush, cursor, zed, pi, omp, qwencode, vibe, grokcli, factorydroid, dsh, reasonix,
|
|
11475
|
-
* commandcode, lettacode, opencode, mimocode). Each tool's own section may override that
|
|
11728
|
+
* commandcode, lettacode, opencode, mimocode, xum). Each tool's own section may override that
|
|
11476
11729
|
* default with a per-target value. A defined section value (including `false`)
|
|
11477
11730
|
* always wins over the root default.
|
|
11478
11731
|
*
|
|
@@ -11492,7 +11745,7 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
|
|
|
11492
11745
|
* The rulesync skill frontmatter exposes a root-level `user-invocable` default
|
|
11493
11746
|
* that applies to every tool supporting the flag (claudecode, codebuddy, copilot,
|
|
11494
11747
|
* copilotcli, crush, cursor, qwencode, vibe, grokcli, factorydroid, dsh, reasonix, commandcode,
|
|
11495
|
-
* lettacode). Each tool's own section may override that default with a
|
|
11748
|
+
* lettacode, xum). Each tool's own section may override that default with a
|
|
11496
11749
|
* per-target value. A defined section value (including `false`) always wins
|
|
11497
11750
|
* over the root default.
|
|
11498
11751
|
*
|
|
@@ -13069,6 +13322,35 @@ const CRUSH_CONFIG_DECLARATION = {
|
|
|
13069
13322
|
}
|
|
13070
13323
|
};
|
|
13071
13324
|
/**
|
|
13325
|
+
* Firebender's `firebender.json`, shared by the project (`<project>/firebender.json`)
|
|
13326
|
+
* and personal (`~/.firebender/firebender.json`) spellings so a policy edit
|
|
13327
|
+
* lands on both. The file also carries `rules`, `commands`, `mcpEnvFile`,
|
|
13328
|
+
* `worktrees` and `useCursorRules` keys rulesync does not own, so every writer
|
|
13329
|
+
* refuses to read-modify-write a file it could not parse. Each feature owns
|
|
13330
|
+
* one whole top-level key: `ignore` (recomputed from `.rulesync/.aiignore`),
|
|
13331
|
+
* `mcpServers` (the complete server map) and `agents` (recomputed from the
|
|
13332
|
+
* existing list, with the entries for rulesync's own agent directory replaced).
|
|
13333
|
+
* https://docs.firebender.com/api-reference/syntax
|
|
13334
|
+
*/
|
|
13335
|
+
const FIREBENDER_CONFIG_DECLARATION = {
|
|
13336
|
+
format: "json",
|
|
13337
|
+
invalidRootPolicy: "error",
|
|
13338
|
+
features: {
|
|
13339
|
+
ignore: {
|
|
13340
|
+
kind: "replace-owned-keys",
|
|
13341
|
+
ownedKeys: ["ignore"]
|
|
13342
|
+
},
|
|
13343
|
+
subagents: {
|
|
13344
|
+
kind: "replace-owned-keys",
|
|
13345
|
+
ownedKeys: ["agents"]
|
|
13346
|
+
},
|
|
13347
|
+
mcp: {
|
|
13348
|
+
kind: "replace-owned-keys",
|
|
13349
|
+
ownedKeys: ["mcpServers"]
|
|
13350
|
+
}
|
|
13351
|
+
}
|
|
13352
|
+
};
|
|
13353
|
+
/**
|
|
13072
13354
|
* What the two Claude Code settings files have in common: both are plain JSON,
|
|
13073
13355
|
* and both validate against the one published schema, which the gateway
|
|
13074
13356
|
* points every file it writes at. `.claude/settings.local.json` is the same
|
|
@@ -13308,6 +13590,15 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13308
13590
|
ownedKeys: ["servers"]
|
|
13309
13591
|
} }
|
|
13310
13592
|
},
|
|
13593
|
+
".xum/mcp.jsonc": {
|
|
13594
|
+
format: "jsonc",
|
|
13595
|
+
invalidRootPolicy: "error",
|
|
13596
|
+
jsoncParseErrors: "error",
|
|
13597
|
+
features: { mcp: {
|
|
13598
|
+
kind: "replace-owned-keys",
|
|
13599
|
+
ownedKeys: ["servers"]
|
|
13600
|
+
} }
|
|
13601
|
+
},
|
|
13311
13602
|
".qwen/settings.json": {
|
|
13312
13603
|
format: "json",
|
|
13313
13604
|
features: {
|
|
@@ -13395,6 +13686,8 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13395
13686
|
".zcode/cli/config.json": ZCODE_CONFIG_DECLARATION,
|
|
13396
13687
|
"crush.json": CRUSH_CONFIG_DECLARATION,
|
|
13397
13688
|
".config/crush/crush.json": CRUSH_CONFIG_DECLARATION,
|
|
13689
|
+
"firebender.json": FIREBENDER_CONFIG_DECLARATION,
|
|
13690
|
+
".firebender/firebender.json": FIREBENDER_CONFIG_DECLARATION,
|
|
13398
13691
|
".bob/settings.json": {
|
|
13399
13692
|
format: "json",
|
|
13400
13693
|
invalidRootPolicy: "error",
|
|
@@ -13767,7 +14060,8 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13767
14060
|
ownedKeys: [
|
|
13768
14061
|
"permissions",
|
|
13769
14062
|
"sandbox",
|
|
13770
|
-
"agent"
|
|
14063
|
+
"agent",
|
|
14064
|
+
"tools"
|
|
13771
14065
|
]
|
|
13772
14066
|
}
|
|
13773
14067
|
}
|
|
@@ -13788,7 +14082,8 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13788
14082
|
ownedKeys: [
|
|
13789
14083
|
"permissions",
|
|
13790
14084
|
"sandbox",
|
|
13791
|
-
"agent"
|
|
14085
|
+
"agent",
|
|
14086
|
+
"tools"
|
|
13792
14087
|
]
|
|
13793
14088
|
}
|
|
13794
14089
|
}
|
|
@@ -20229,6 +20524,178 @@ var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
|
|
|
20229
20524
|
}
|
|
20230
20525
|
};
|
|
20231
20526
|
//#endregion
|
|
20527
|
+
//#region src/constants/firebender-paths.ts
|
|
20528
|
+
/**
|
|
20529
|
+
* Firebender (the coding agent plugin for Android Studio and other JetBrains
|
|
20530
|
+
* IDEs) keeps project configuration under `.firebender/` plus a project-root
|
|
20531
|
+
* `firebender.json`, and user configuration under `~/.firebender/` (including
|
|
20532
|
+
* `~/.firebender/firebender.json`).
|
|
20533
|
+
*
|
|
20534
|
+
* @see https://docs.firebender.com/api-reference/syntax
|
|
20535
|
+
* @see https://docs.firebender.com/api-reference/rules
|
|
20536
|
+
* @see https://docs.firebender.com/api-reference/commands
|
|
20537
|
+
* @see https://docs.firebender.com/api-reference/agents
|
|
20538
|
+
* @see https://docs.firebender.com/api-reference/skills
|
|
20539
|
+
* @see https://docs.firebender.com/api-reference/agents-md
|
|
20540
|
+
* @see https://docs.firebender.com/multi-agent/hooks
|
|
20541
|
+
*/
|
|
20542
|
+
const FIREBENDER_DIR = ".firebender";
|
|
20543
|
+
/**
|
|
20544
|
+
* `firebender.json`: at the project root for project scope, and under
|
|
20545
|
+
* `~/.firebender/` for the personal config. It is shared by the ignore
|
|
20546
|
+
* (`ignore`), MCP (`mcpServers`) and subagents (`agents`) features.
|
|
20547
|
+
*/
|
|
20548
|
+
const FIREBENDER_CONFIG_FILE_NAME = "firebender.json";
|
|
20549
|
+
/** `.firebender/AGENTS.md`, the first file Firebender looks for in both scopes. */
|
|
20550
|
+
const FIREBENDER_AGENTS_MD_FILE_NAME = "AGENTS.md";
|
|
20551
|
+
join(FIREBENDER_DIR, "rules");
|
|
20552
|
+
/** Commands: `.firebender/commands/*.mdc` (project) and `~/.firebender/commands/*.mdc` (global). */
|
|
20553
|
+
const FIREBENDER_COMMANDS_DIR_PATH = join(FIREBENDER_DIR, "commands");
|
|
20554
|
+
/** Agents: Markdown files registered through the `agents` array of `firebender.json`. */
|
|
20555
|
+
const FIREBENDER_AGENTS_DIR_NAME = "agents";
|
|
20556
|
+
const FIREBENDER_AGENTS_DIR_PATH = join(FIREBENDER_DIR, FIREBENDER_AGENTS_DIR_NAME);
|
|
20557
|
+
/** Skills: `.firebender/skills/<name>/SKILL.md` (project) and `~/.firebender/skills/` (global). */
|
|
20558
|
+
const FIREBENDER_SKILLS_DIR_PATH = join(FIREBENDER_DIR, "skills");
|
|
20559
|
+
/** Hooks: `.firebender/hooks.json` (project) and `~/.firebender/hooks.json` (global). */
|
|
20560
|
+
const FIREBENDER_HOOKS_FILE_NAME = "hooks.json";
|
|
20561
|
+
/**
|
|
20562
|
+
* The directory holding `firebender.json` for a scope: the project root, or
|
|
20563
|
+
* `~/.firebender/` for the personal config.
|
|
20564
|
+
*/
|
|
20565
|
+
function getFirebenderConfigDir({ global }) {
|
|
20566
|
+
return global ? FIREBENDER_DIR : ".";
|
|
20567
|
+
}
|
|
20568
|
+
//#endregion
|
|
20569
|
+
//#region src/features/commands/firebender-command.ts
|
|
20570
|
+
/**
|
|
20571
|
+
* Frontmatter of a Firebender command. Every field is optional: `name`
|
|
20572
|
+
* defaults to the file name, `model` is `default`, `quick` or a model ID, and
|
|
20573
|
+
* `mode` is one of `auto`, `read`, `write` or `composer`. Unknown keys are
|
|
20574
|
+
* preserved.
|
|
20575
|
+
*
|
|
20576
|
+
* @see https://docs.firebender.com/api-reference/commands
|
|
20577
|
+
*/
|
|
20578
|
+
const FirebenderCommandFrontmatterSchema = z.looseObject({
|
|
20579
|
+
name: z.optional(z.string()),
|
|
20580
|
+
description: z.optional(z.string()),
|
|
20581
|
+
model: z.optional(z.string()),
|
|
20582
|
+
mode: z.optional(z.string())
|
|
20583
|
+
});
|
|
20584
|
+
const FIREBENDER_COMMAND_EXTENSION = ".mdc";
|
|
20585
|
+
/**
|
|
20586
|
+
* Command generator for Firebender: one `.mdc` file per command under
|
|
20587
|
+
* `.firebender/commands/` (project) and `~/.firebender/commands/` (global),
|
|
20588
|
+
* with optional `name` / `description` / `model` / `mode` frontmatter. The
|
|
20589
|
+
* canonical `description` is written as is; `name`, `model` and `mode` come
|
|
20590
|
+
* from the `firebender:` section of the rulesync command.
|
|
20591
|
+
*
|
|
20592
|
+
* @see https://docs.firebender.com/api-reference/commands
|
|
20593
|
+
*/
|
|
20594
|
+
var FirebenderCommand = class FirebenderCommand extends ToolCommand {
|
|
20595
|
+
frontmatter;
|
|
20596
|
+
body;
|
|
20597
|
+
constructor({ frontmatter, body, ...rest }) {
|
|
20598
|
+
if (rest.validate) {
|
|
20599
|
+
const result = FirebenderCommandFrontmatterSchema.safeParse(frontmatter);
|
|
20600
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
20601
|
+
}
|
|
20602
|
+
super({
|
|
20603
|
+
...rest,
|
|
20604
|
+
fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true })
|
|
20605
|
+
});
|
|
20606
|
+
this.frontmatter = frontmatter;
|
|
20607
|
+
this.body = body;
|
|
20608
|
+
}
|
|
20609
|
+
static getSettablePaths(_options = {}) {
|
|
20610
|
+
return { relativeDirPath: FIREBENDER_COMMANDS_DIR_PATH };
|
|
20611
|
+
}
|
|
20612
|
+
getBody() {
|
|
20613
|
+
return this.body;
|
|
20614
|
+
}
|
|
20615
|
+
getFrontmatter() {
|
|
20616
|
+
return this.frontmatter;
|
|
20617
|
+
}
|
|
20618
|
+
toRulesyncCommand() {
|
|
20619
|
+
const { description, ...restFields } = this.frontmatter;
|
|
20620
|
+
const rulesyncFrontmatter = {
|
|
20621
|
+
targets: ["*"],
|
|
20622
|
+
description,
|
|
20623
|
+
...Object.keys(restFields).length > 0 && { firebender: restFields }
|
|
20624
|
+
};
|
|
20625
|
+
return new RulesyncCommand({
|
|
20626
|
+
outputRoot: ".",
|
|
20627
|
+
frontmatter: rulesyncFrontmatter,
|
|
20628
|
+
body: this.body,
|
|
20629
|
+
relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
|
|
20630
|
+
relativeFilePath: this.relativeFilePath.replace(/\.mdc$/, ".md"),
|
|
20631
|
+
fileContent: stringifyFrontmatter(this.body, rulesyncFrontmatter),
|
|
20632
|
+
validate: true
|
|
20633
|
+
});
|
|
20634
|
+
}
|
|
20635
|
+
static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
|
|
20636
|
+
const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
|
|
20637
|
+
const firebenderFields = rulesyncFrontmatter.firebender ?? {};
|
|
20638
|
+
const frontmatter = {
|
|
20639
|
+
...rulesyncFrontmatter.description && { description: rulesyncFrontmatter.description },
|
|
20640
|
+
...firebenderFields
|
|
20641
|
+
};
|
|
20642
|
+
return new FirebenderCommand({
|
|
20643
|
+
outputRoot,
|
|
20644
|
+
frontmatter,
|
|
20645
|
+
body: rulesyncCommand.getBody(),
|
|
20646
|
+
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
20647
|
+
relativeFilePath: rulesyncCommand.getRelativeFilePath().replace(/\.md$/, FIREBENDER_COMMAND_EXTENSION),
|
|
20648
|
+
validate
|
|
20649
|
+
});
|
|
20650
|
+
}
|
|
20651
|
+
validate() {
|
|
20652
|
+
if (!this.frontmatter) return {
|
|
20653
|
+
success: true,
|
|
20654
|
+
error: null
|
|
20655
|
+
};
|
|
20656
|
+
const result = FirebenderCommandFrontmatterSchema.safeParse(this.frontmatter);
|
|
20657
|
+
if (result.success) return {
|
|
20658
|
+
success: true,
|
|
20659
|
+
error: null
|
|
20660
|
+
};
|
|
20661
|
+
return {
|
|
20662
|
+
success: false,
|
|
20663
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
20664
|
+
};
|
|
20665
|
+
}
|
|
20666
|
+
static isTargetedByRulesyncCommand(rulesyncCommand) {
|
|
20667
|
+
return this.isTargetedByRulesyncCommandDefault({
|
|
20668
|
+
rulesyncCommand,
|
|
20669
|
+
toolTarget: "firebender"
|
|
20670
|
+
});
|
|
20671
|
+
}
|
|
20672
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
20673
|
+
const paths = this.getSettablePaths({ global });
|
|
20674
|
+
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
20675
|
+
const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
20676
|
+
const result = FirebenderCommandFrontmatterSchema.safeParse(frontmatter);
|
|
20677
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
20678
|
+
return new FirebenderCommand({
|
|
20679
|
+
outputRoot,
|
|
20680
|
+
relativeDirPath: paths.relativeDirPath,
|
|
20681
|
+
relativeFilePath,
|
|
20682
|
+
frontmatter: result.data,
|
|
20683
|
+
body: body.trim(),
|
|
20684
|
+
validate
|
|
20685
|
+
});
|
|
20686
|
+
}
|
|
20687
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
20688
|
+
return new FirebenderCommand({
|
|
20689
|
+
outputRoot,
|
|
20690
|
+
relativeDirPath,
|
|
20691
|
+
relativeFilePath,
|
|
20692
|
+
frontmatter: {},
|
|
20693
|
+
body: "",
|
|
20694
|
+
validate: false
|
|
20695
|
+
});
|
|
20696
|
+
}
|
|
20697
|
+
};
|
|
20698
|
+
//#endregion
|
|
20232
20699
|
//#region src/features/commands/gitlabduo-command.ts
|
|
20233
20700
|
/**
|
|
20234
20701
|
* GitLab Duo CLI custom slash commands are Markdown files named after the
|
|
@@ -20821,6 +21288,60 @@ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
|
|
|
20821
21288
|
}
|
|
20822
21289
|
};
|
|
20823
21290
|
//#endregion
|
|
21291
|
+
//#region src/features/shared/hermes-plugin-commands.ts
|
|
21292
|
+
/**
|
|
21293
|
+
* Normalize a name into a Hermes Agent plugin slash command name.
|
|
21294
|
+
*
|
|
21295
|
+
* `ctx.register_command()` lowercases the name, and Hermes' messaging gateway
|
|
21296
|
+
* looks plugin commands up with every `_` turned into `-`
|
|
21297
|
+
* (`gateway/run_inbound_hooks.py`), so only a lowercase, hyphenated name is
|
|
21298
|
+
* reachable from every surface.
|
|
21299
|
+
*/
|
|
21300
|
+
function hermesSlashName(value) {
|
|
21301
|
+
return value.toLowerCase().replace(/[ _]/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
|
|
21302
|
+
}
|
|
21303
|
+
/**
|
|
21304
|
+
* Python helpers shared by the generated Hermes command plugins.
|
|
21305
|
+
*
|
|
21306
|
+
* A plugin slash command runs outside any agent turn, so it cannot call
|
|
21307
|
+
* `delegate_task` (which requires a parent agent). Instead the handler queues
|
|
21308
|
+
* its prompt as a user message with `ctx.inject_message()`, passing the calling
|
|
21309
|
+
* session's key: a `session_key` handler keyword on newer Hermes, or
|
|
21310
|
+
* `HERMES_SESSION_KEY` from the session context the gateway binds around the
|
|
21311
|
+
* handler on older releases.
|
|
21312
|
+
*/
|
|
21313
|
+
function getHermesQueueUserMessagePython({ pluginName }) {
|
|
21314
|
+
return `def _session_key(kwargs):
|
|
21315
|
+
session_key = kwargs.get("session_key")
|
|
21316
|
+
if session_key:
|
|
21317
|
+
return session_key
|
|
21318
|
+
try:
|
|
21319
|
+
from gateway.session_context import get_session_env
|
|
21320
|
+
except Exception:
|
|
21321
|
+
return None
|
|
21322
|
+
return get_session_env("HERMES_SESSION_KEY") or None
|
|
21323
|
+
|
|
21324
|
+
|
|
21325
|
+
def _queue_user_message(ctx, message, kwargs, label):
|
|
21326
|
+
"""Queue *message* as the session's next user turn; return an error reply, or None."""
|
|
21327
|
+
session_key = _session_key(kwargs)
|
|
21328
|
+
try:
|
|
21329
|
+
if session_key:
|
|
21330
|
+
queued = ctx.inject_message(message, role="user", session_key=session_key)
|
|
21331
|
+
else:
|
|
21332
|
+
queued = ctx.inject_message(message, role="user")
|
|
21333
|
+
except Exception as error:
|
|
21334
|
+
return f"Could not queue {label}: {error}"
|
|
21335
|
+
if not queued:
|
|
21336
|
+
return (
|
|
21337
|
+
f"Hermes refused to queue {label}: there is no active session, or this plugin "
|
|
21338
|
+
"may not inject messages. In gateway sessions, set "
|
|
21339
|
+
"plugins.entries.${pluginName}.allow_gateway_injection: true in config.yaml."
|
|
21340
|
+
)
|
|
21341
|
+
return None
|
|
21342
|
+
`;
|
|
21343
|
+
}
|
|
21344
|
+
//#endregion
|
|
20824
21345
|
//#region src/features/skills/agentsskills-skill.ts
|
|
20825
21346
|
const AgentsSkillsSkillFrontmatterSchema = z.looseObject({
|
|
20826
21347
|
name: z.string(),
|
|
@@ -21303,9 +21824,6 @@ const HermesCommandSpecSchema = z.object({
|
|
|
21303
21824
|
description: z.string(),
|
|
21304
21825
|
prompt: z.string()
|
|
21305
21826
|
});
|
|
21306
|
-
function hermesSlashName(value) {
|
|
21307
|
-
return value.toLowerCase().replace(/[ _]/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
|
|
21308
|
-
}
|
|
21309
21827
|
function getPluginManifestContent$2() {
|
|
21310
21828
|
return [
|
|
21311
21829
|
"name: rulesync-commands",
|
|
@@ -21339,6 +21857,8 @@ def _load_commands():
|
|
|
21339
21857
|
return commands
|
|
21340
21858
|
|
|
21341
21859
|
|
|
21860
|
+
${getHermesQueueUserMessagePython({ pluginName: "rulesync-commands" })}
|
|
21861
|
+
|
|
21342
21862
|
def _register_command(ctx, command):
|
|
21343
21863
|
slug = command.get("slug")
|
|
21344
21864
|
if not slug:
|
|
@@ -21348,31 +21868,22 @@ def _register_command(ctx, command):
|
|
|
21348
21868
|
prompt = command.get("prompt") or ""
|
|
21349
21869
|
|
|
21350
21870
|
def handler(args=None, **kwargs):
|
|
21351
|
-
del kwargs
|
|
21352
21871
|
raw_args = ""
|
|
21353
21872
|
if isinstance(args, dict):
|
|
21354
21873
|
raw_args = args.get("args") or args.get("context") or args.get("prompt") or ""
|
|
21355
21874
|
elif args is not None:
|
|
21356
21875
|
raw_args = str(args)
|
|
21357
21876
|
|
|
21358
|
-
|
|
21877
|
+
parts = [prompt] if prompt else []
|
|
21359
21878
|
if raw_args:
|
|
21360
|
-
|
|
21879
|
+
parts.append(f"User arguments:\\n{raw_args}")
|
|
21361
21880
|
|
|
21362
|
-
#
|
|
21363
|
-
#
|
|
21364
|
-
#
|
|
21365
|
-
|
|
21366
|
-
|
|
21367
|
-
|
|
21368
|
-
"tasks": [
|
|
21369
|
-
{
|
|
21370
|
-
"goal": description,
|
|
21371
|
-
"context": "\\n\\n".join(context_parts),
|
|
21372
|
-
},
|
|
21373
|
-
],
|
|
21374
|
-
},
|
|
21375
|
-
)
|
|
21881
|
+
# A slash command runs outside any agent turn, so calling delegate_task
|
|
21882
|
+
# here fails with "delegate_task requires a parent agent context".
|
|
21883
|
+
# Queue the command prompt as the session's next user message instead.
|
|
21884
|
+
label = f"the /{slug} command"
|
|
21885
|
+
error = _queue_user_message(ctx, "\\n\\n".join(parts) or description, kwargs, label)
|
|
21886
|
+
return error or f"Queued {label}."
|
|
21376
21887
|
|
|
21377
21888
|
ctx.register_command(slug, handler, description)
|
|
21378
21889
|
|
|
@@ -23697,6 +24208,7 @@ var QwencodeCommand = class QwencodeCommand extends ToolCommand {
|
|
|
23697
24208
|
const REASONIX_PROJECT_MCP_FILE_NAME = "reasonix.toml";
|
|
23698
24209
|
const REASONIX_GLOBAL_DIR = ".reasonix";
|
|
23699
24210
|
const REASONIX_GLOBAL_MCP_FILE_NAME = "config.toml";
|
|
24211
|
+
const REASONIX_NESTED_PROJECT_CONFIG_RELATIVE_FILE_PATH = join(".reasonix", REASONIX_PROJECT_MCP_FILE_NAME);
|
|
23700
24212
|
const REASONIX_PROJECT_PERMISSIONS_FILE_NAME = REASONIX_PROJECT_MCP_FILE_NAME;
|
|
23701
24213
|
const REASONIX_GLOBAL_PERMISSIONS_FILE_NAME = REASONIX_GLOBAL_MCP_FILE_NAME;
|
|
23702
24214
|
const REASONIX_DIR = REASONIX_GLOBAL_DIR;
|
|
@@ -25244,6 +25756,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
|
|
|
25244
25756
|
supportsSubdirectory: false
|
|
25245
25757
|
}
|
|
25246
25758
|
}],
|
|
25759
|
+
["firebender", {
|
|
25760
|
+
class: FirebenderCommand,
|
|
25761
|
+
meta: {
|
|
25762
|
+
extension: "mdc",
|
|
25763
|
+
supportsProject: true,
|
|
25764
|
+
supportsGlobal: true,
|
|
25765
|
+
isSimulated: false,
|
|
25766
|
+
supportsSubdirectory: false
|
|
25767
|
+
}
|
|
25768
|
+
}],
|
|
25247
25769
|
["goose", {
|
|
25248
25770
|
class: GooseCommand,
|
|
25249
25771
|
meta: {
|
|
@@ -28034,25 +28556,65 @@ function toolHooksToCanonical({ hooks, converterConfig, logger }) {
|
|
|
28034
28556
|
//#endregion
|
|
28035
28557
|
//#region src/features/hooks/antigravity-hooks.ts
|
|
28036
28558
|
/**
|
|
28037
|
-
*
|
|
28038
|
-
* `
|
|
28039
|
-
|
|
28040
|
-
|
|
28041
|
-
|
|
28559
|
+
* Canonical events whose Antigravity counterparts (`SessionStart`,
|
|
28560
|
+
* `PreInvocation`, `PostInvocation`, `Stop`) take no matcher.
|
|
28561
|
+
*/
|
|
28562
|
+
const ANTIGRAVITY_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set([
|
|
28563
|
+
"sessionStart",
|
|
28564
|
+
"preModelInvocation",
|
|
28565
|
+
"postModelInvocation",
|
|
28566
|
+
"stop"
|
|
28567
|
+
]);
|
|
28568
|
+
/**
|
|
28569
|
+
* Antigravity event names whose value is a flat list of handlers
|
|
28570
|
+
* (`"Stop": [ { "type": "command", "command": "..." } ]`) rather than a list
|
|
28571
|
+
* of `{ "matcher", "hooks": [...] }` groups. Antigravity's loader rejects a
|
|
28572
|
+
* wrapped group under these events ("command hook must specify 'command'")
|
|
28573
|
+
* and then skips the whole `hooks.json`.
|
|
28574
|
+
* https://antigravity.google/docs/hooks
|
|
28575
|
+
*/
|
|
28576
|
+
const ANTIGRAVITY_FLAT_HANDLER_EVENTS = new Set([...ANTIGRAVITY_NO_MATCHER_EVENTS].map((event) => lookupOwn({
|
|
28577
|
+
record: CANONICAL_TO_ANTIGRAVITY_EVENT_NAMES,
|
|
28578
|
+
key: event
|
|
28579
|
+
}) ?? event));
|
|
28580
|
+
/**
|
|
28581
|
+
* Antigravity uses a Claude-Code-like `hooks.json`. The `PreToolUse` /
|
|
28582
|
+
* `PostToolUse` events use the matcher-based group shape, while `SessionStart`,
|
|
28583
|
+
* `PreInvocation`, `PostInvocation` and `Stop` are flat handler lists (see
|
|
28584
|
+
* {@link toAntigravityEventMap}). `projectDirVar` is empty because Antigravity
|
|
28585
|
+
* does not document a project-directory variable for commands.
|
|
28042
28586
|
*/
|
|
28043
28587
|
const ANTIGRAVITY_CONVERTER_CONFIG = {
|
|
28044
28588
|
supportedEvents: ANTIGRAVITY_HOOK_EVENTS,
|
|
28045
28589
|
canonicalToToolEventNames: CANONICAL_TO_ANTIGRAVITY_EVENT_NAMES,
|
|
28046
28590
|
toolToCanonicalEventNames: ANTIGRAVITY_TO_CANONICAL_EVENT_NAMES,
|
|
28047
28591
|
projectDirVar: "",
|
|
28048
|
-
noMatcherEvents:
|
|
28049
|
-
"preModelInvocation",
|
|
28050
|
-
"postModelInvocation",
|
|
28051
|
-
"stop"
|
|
28052
|
-
]),
|
|
28592
|
+
noMatcherEvents: ANTIGRAVITY_NO_MATCHER_EVENTS,
|
|
28053
28593
|
supportedHookTypes: /* @__PURE__ */ new Set(["command"])
|
|
28054
28594
|
};
|
|
28055
28595
|
/**
|
|
28596
|
+
* Turn the shared converter's matcher-group output into Antigravity's shape:
|
|
28597
|
+
* the matcher-less events get their handlers directly under the event key.
|
|
28598
|
+
*/
|
|
28599
|
+
function toAntigravityEventMap(eventMap) {
|
|
28600
|
+
return Object.fromEntries(Object.entries(eventMap).map(([event, entries]) => {
|
|
28601
|
+
if (!ANTIGRAVITY_FLAT_HANDLER_EVENTS.has(event)) return [event, entries];
|
|
28602
|
+
return [event, entries.flatMap((entry) => isPlainObject$1(entry) && Array.isArray(entry.hooks) ? entry.hooks : [])];
|
|
28603
|
+
}));
|
|
28604
|
+
}
|
|
28605
|
+
/**
|
|
28606
|
+
* Reverse of {@link toAntigravityEventMap} for import: wrap each flat handler
|
|
28607
|
+
* of a matcher-less event in a `{ hooks: [...] }` group so the shared converter
|
|
28608
|
+
* can read it. An entry that is already a group (`{ hooks: [...] }`, as older
|
|
28609
|
+
* rulesync versions wrote) is kept as is.
|
|
28610
|
+
*/
|
|
28611
|
+
function toMatcherGroupEventMap(eventMap) {
|
|
28612
|
+
return Object.fromEntries(Object.entries(eventMap).map(([event, entries]) => {
|
|
28613
|
+
if (!ANTIGRAVITY_FLAT_HANDLER_EVENTS.has(event) || !Array.isArray(entries)) return [event, entries];
|
|
28614
|
+
return [event, entries.map((entry) => isPlainObject$1(entry) && !("hooks" in entry) ? { hooks: [entry] } : entry)];
|
|
28615
|
+
}));
|
|
28616
|
+
}
|
|
28617
|
+
/**
|
|
28056
28618
|
* Antigravity's `hooks.json` is keyed by a named hook whose value holds the
|
|
28057
28619
|
* per-event map (plus an optional `enabled` flag). rulesync writes a single
|
|
28058
28620
|
* generated hook under this stable name.
|
|
@@ -28089,8 +28651,8 @@ function flattenAntigravityHooks(parsed) {
|
|
|
28089
28651
|
* Hooks generator for Google Antigravity (both the IDE and the CLI).
|
|
28090
28652
|
*
|
|
28091
28653
|
* Antigravity writes a dedicated `hooks.json` keyed by a named hook whose
|
|
28092
|
-
* value holds the
|
|
28093
|
-
*
|
|
28654
|
+
* value holds the event map (`{ "<name>": { "<Event>": [...] } }`): matcher
|
|
28655
|
+
* groups for the tool events, flat handler lists for the others. Project and global modes share the
|
|
28094
28656
|
* same shape; only the location differs (`.agents/hooks.json` vs
|
|
28095
28657
|
* `~/.gemini/config/hooks.json`).
|
|
28096
28658
|
*
|
|
@@ -28134,12 +28696,12 @@ var AntigravityHooks = class extends ToolHooks {
|
|
|
28134
28696
|
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
28135
28697
|
const paths = this.getSettablePaths({ global });
|
|
28136
28698
|
const config = rulesyncHooks.getJson();
|
|
28137
|
-
const eventMap = canonicalToToolHooks({
|
|
28699
|
+
const eventMap = toAntigravityEventMap(canonicalToToolHooks({
|
|
28138
28700
|
config,
|
|
28139
28701
|
toolOverrideHooks: config[this.getOverrideKey()]?.hooks,
|
|
28140
28702
|
converterConfig: ANTIGRAVITY_CONVERTER_CONFIG,
|
|
28141
28703
|
logger
|
|
28142
|
-
});
|
|
28704
|
+
}));
|
|
28143
28705
|
const antigravityHooks = Object.keys(eventMap).length > 0 ? { [ANTIGRAVITY_HOOK_NAME]: eventMap } : {};
|
|
28144
28706
|
const fileContent = JSON.stringify(antigravityHooks, null, 2);
|
|
28145
28707
|
return new this({
|
|
@@ -28158,7 +28720,7 @@ var AntigravityHooks = class extends ToolHooks {
|
|
|
28158
28720
|
throw new Error(`Failed to parse Antigravity hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
28159
28721
|
}
|
|
28160
28722
|
const hooks = toolHooksToCanonical({
|
|
28161
|
-
hooks: flattenAntigravityHooks(parsed),
|
|
28723
|
+
hooks: toMatcherGroupEventMap(flattenAntigravityHooks(parsed)),
|
|
28162
28724
|
converterConfig: ANTIGRAVITY_CONVERTER_CONFIG,
|
|
28163
28725
|
logger
|
|
28164
28726
|
});
|
|
@@ -30622,11 +31184,17 @@ var ContinueHooks = class extends SettingsJsonHooks {
|
|
|
30622
31184
|
* entry that uses them instead of a shell field is not mistaken for one
|
|
30623
31185
|
* without a command.
|
|
30624
31186
|
*
|
|
31187
|
+
* An optional `matcher` regex is honored on the events in
|
|
31188
|
+
* {@link COPILOT_MATCHER_HOOK_EVENTS}. The VS Code per-OS override fields come
|
|
31189
|
+
* from the legacy Local hooks reference and are kept for existing files.
|
|
31190
|
+
*
|
|
30625
31191
|
* @see https://docs.github.com/en/copilot/reference/hooks-reference
|
|
31192
|
+
* @see https://code.visualstudio.com/docs/agent-customization/hooks
|
|
30626
31193
|
* @see https://code.visualstudio.com/docs/agents/reference/hooks-reference
|
|
30627
31194
|
*/
|
|
30628
31195
|
const CopilotHookEntrySchema = z.looseObject({
|
|
30629
31196
|
type: z.string(),
|
|
31197
|
+
matcher: z.optional(z.string()),
|
|
30630
31198
|
bash: z.optional(z.string()),
|
|
30631
31199
|
powershell: z.optional(z.string()),
|
|
30632
31200
|
command: z.optional(z.string()),
|
|
@@ -30660,8 +31228,10 @@ function canonicalToCopilotHooks(config) {
|
|
|
30660
31228
|
...sharedConfigHooks,
|
|
30661
31229
|
...config.copilot?.hooks
|
|
30662
31230
|
};
|
|
31231
|
+
const matcherEvents = new Set(COPILOT_MATCHER_HOOK_EVENTS);
|
|
30663
31232
|
const copilot = {};
|
|
30664
31233
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
31234
|
+
const matcherSupported = matcherEvents.has(eventName);
|
|
30665
31235
|
const copilotEventName = lookupOwn({
|
|
30666
31236
|
record: CANONICAL_TO_COPILOT_EVENT_NAMES,
|
|
30667
31237
|
key: eventName
|
|
@@ -30669,7 +31239,7 @@ function canonicalToCopilotHooks(config) {
|
|
|
30669
31239
|
const entries = [];
|
|
30670
31240
|
for (const def of definitions) {
|
|
30671
31241
|
const hookType = def.type ?? "command";
|
|
30672
|
-
if (def.matcher) continue;
|
|
31242
|
+
if (def.matcher && !matcherSupported) continue;
|
|
30673
31243
|
if (hookType !== "command") continue;
|
|
30674
31244
|
const command = def.command;
|
|
30675
31245
|
const timeout = def.timeout;
|
|
@@ -30677,6 +31247,7 @@ function canonicalToCopilotHooks(config) {
|
|
|
30677
31247
|
const rest = Object.fromEntries(Object.entries(def).filter(([k]) => !canonicalSchemaKeys.includes(k)));
|
|
30678
31248
|
entries.push({
|
|
30679
31249
|
type: hookType,
|
|
31250
|
+
...def.matcher && { matcher: def.matcher },
|
|
30680
31251
|
...command !== void 0 && command !== null && { [commandField]: command },
|
|
30681
31252
|
...def.env !== void 0 && { env: def.env },
|
|
30682
31253
|
...timeout !== void 0 && timeout !== null && { timeoutSec: timeout },
|
|
@@ -30771,6 +31342,7 @@ function copilotHooksToCanonical(copilotHooks, logger) {
|
|
|
30771
31342
|
if (command === void 0 && hasOsOverride(passthrough)) logger?.warn(`Copilot hook on '${copilotEventName}' has only VS Code per-OS overrides (windows/linux/osx) and no portable command; it is preserved for copilot, but other targets will generate it without a command until a \`command\` is added.`);
|
|
30772
31343
|
defs.push({
|
|
30773
31344
|
type: "command",
|
|
31345
|
+
...entry.matcher !== void 0 && entry.matcher !== "" && { matcher: entry.matcher },
|
|
30774
31346
|
...command !== void 0 && { command },
|
|
30775
31347
|
...shell !== void 0 && { shell },
|
|
30776
31348
|
...timeout !== void 0 && { timeout },
|
|
@@ -30914,21 +31486,13 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
|
|
|
30914
31486
|
*/
|
|
30915
31487
|
/**
|
|
30916
31488
|
* Canonical events whose Copilot CLI hook entries honor an optional `matcher`
|
|
30917
|
-
* field (a regex compiled as `^(?:PATTERN)
|
|
30918
|
-
*
|
|
30919
|
-
*
|
|
30920
|
-
*
|
|
30921
|
-
*
|
|
30922
|
-
* @see https://docs.github.com/en/copilot/reference/hooks-reference
|
|
31489
|
+
* field (a regex compiled as `^(?:PATTERN)$`). The hooks reference filters each
|
|
31490
|
+
* on a different field: the tool name for `preToolUse` / `postToolUse` /
|
|
31491
|
+
* `permissionRequest`, `notification_type`, the `preCompact` trigger
|
|
31492
|
+
* (`manual` | `auto`) and the `subagentStart` agent name.
|
|
31493
|
+
* See {@link COPILOTCLI_MATCHER_HOOK_EVENTS}.
|
|
30923
31494
|
*/
|
|
30924
|
-
const COPILOTCLI_MATCHER_EVENTS =
|
|
30925
|
-
"preToolUse",
|
|
30926
|
-
"postToolUse",
|
|
30927
|
-
"notification",
|
|
30928
|
-
"permissionRequest",
|
|
30929
|
-
"preCompact",
|
|
30930
|
-
"subagentStart"
|
|
30931
|
-
]);
|
|
31495
|
+
const COPILOTCLI_MATCHER_EVENTS = new Set(COPILOTCLI_MATCHER_HOOK_EVENTS);
|
|
30932
31496
|
/** Human-readable list of the matcher-aware events, for the drop warning. */
|
|
30933
31497
|
const COPILOTCLI_MATCHER_EVENTS_LABEL = [...COPILOTCLI_MATCHER_EVENTS].join("/");
|
|
30934
31498
|
const CopilotCliHookEntrySchema = z.looseObject({
|
|
@@ -32527,6 +33091,116 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
|
|
|
32527
33091
|
}
|
|
32528
33092
|
};
|
|
32529
33093
|
//#endregion
|
|
33094
|
+
//#region src/features/hooks/firebender-hooks.ts
|
|
33095
|
+
/**
|
|
33096
|
+
* Hooks generator for Firebender.
|
|
33097
|
+
*
|
|
33098
|
+
* Firebender reads `.firebender/hooks.json` (project) and
|
|
33099
|
+
* `~/.firebender/hooks.json` (global). The file follows Cursor's layout — a
|
|
33100
|
+
* `version` plus a `hooks` map from camelCase event names to lists of
|
|
33101
|
+
* `{ command, timeout?, loop_limit?, matcher? }` entries — and every event is
|
|
33102
|
+
* spelled like its canonical counterpart. The per-hook `type` accepts
|
|
33103
|
+
* `"command" | "prompt"`, but the docs never say what a prompt hook does, so
|
|
33104
|
+
* only command hooks are generated (the HooksProcessor warns about the rest).
|
|
33105
|
+
*
|
|
33106
|
+
* @see https://docs.firebender.com/multi-agent/hooks
|
|
33107
|
+
*/
|
|
33108
|
+
var FirebenderHooks = class FirebenderHooks extends ToolHooks {
|
|
33109
|
+
constructor(params) {
|
|
33110
|
+
super({
|
|
33111
|
+
...params,
|
|
33112
|
+
fileContent: params.fileContent ?? "{}"
|
|
33113
|
+
});
|
|
33114
|
+
}
|
|
33115
|
+
static getSettablePaths(_options = {}) {
|
|
33116
|
+
return {
|
|
33117
|
+
relativeDirPath: FIREBENDER_DIR,
|
|
33118
|
+
relativeFilePath: FIREBENDER_HOOKS_FILE_NAME
|
|
33119
|
+
};
|
|
33120
|
+
}
|
|
33121
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
33122
|
+
const paths = FirebenderHooks.getSettablePaths({ global });
|
|
33123
|
+
const fileContent = await readFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
|
|
33124
|
+
return new FirebenderHooks({
|
|
33125
|
+
outputRoot,
|
|
33126
|
+
relativeDirPath: paths.relativeDirPath,
|
|
33127
|
+
relativeFilePath: paths.relativeFilePath,
|
|
33128
|
+
fileContent,
|
|
33129
|
+
validate
|
|
33130
|
+
});
|
|
33131
|
+
}
|
|
33132
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
|
|
33133
|
+
const config = rulesyncHooks.getJson();
|
|
33134
|
+
const supportedEvents = new Set(FIREBENDER_HOOK_EVENTS);
|
|
33135
|
+
const sharedHooks = {};
|
|
33136
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (supportedEvents.has(event)) sharedHooks[event] = defs;
|
|
33137
|
+
const mergedHooks = {
|
|
33138
|
+
...sharedHooks,
|
|
33139
|
+
...config.firebender?.hooks
|
|
33140
|
+
};
|
|
33141
|
+
const firebenderHooks = {};
|
|
33142
|
+
for (const [eventName, defs] of Object.entries(mergedHooks)) {
|
|
33143
|
+
const mappedDefs = defs.filter((def) => (def.type ?? "command") === "command").filter((def) => typeof def.command === "string" && def.command.length > 0).map((def) => ({
|
|
33144
|
+
command: def.command,
|
|
33145
|
+
...def.timeout !== void 0 && def.timeout !== null && { timeout: def.timeout },
|
|
33146
|
+
...def.loop_limit !== void 0 && { loop_limit: def.loop_limit },
|
|
33147
|
+
...def.matcher !== void 0 && def.matcher !== null && { matcher: def.matcher }
|
|
33148
|
+
}));
|
|
33149
|
+
if (mappedDefs.length > 0) firebenderHooks[eventName] = mappedDefs;
|
|
33150
|
+
}
|
|
33151
|
+
const paths = FirebenderHooks.getSettablePaths({ global });
|
|
33152
|
+
const merged = mergeGeneratedHookLists({
|
|
33153
|
+
existingContent: await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "",
|
|
33154
|
+
generatedHooks: firebenderHooks,
|
|
33155
|
+
shape: "flat",
|
|
33156
|
+
preserveUnowned,
|
|
33157
|
+
previouslyOwned: preserveUnowned ? parseHooksOwnershipLock(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0,
|
|
33158
|
+
logger
|
|
33159
|
+
});
|
|
33160
|
+
const fileContent = JSON.stringify({
|
|
33161
|
+
version: config.version ?? 1,
|
|
33162
|
+
hooks: merged.hooks
|
|
33163
|
+
}, null, 2);
|
|
33164
|
+
return new FirebenderHooks({
|
|
33165
|
+
outputRoot,
|
|
33166
|
+
relativeDirPath: paths.relativeDirPath,
|
|
33167
|
+
relativeFilePath: paths.relativeFilePath,
|
|
33168
|
+
fileContent,
|
|
33169
|
+
ownedHookRefs: preserveUnowned ? merged.owned : void 0,
|
|
33170
|
+
validate
|
|
33171
|
+
});
|
|
33172
|
+
}
|
|
33173
|
+
static supportsPreserveUnowned() {
|
|
33174
|
+
return true;
|
|
33175
|
+
}
|
|
33176
|
+
static async getAuxiliaryFiles({ toolHooks } = {}) {
|
|
33177
|
+
return toolHooks instanceof FirebenderHooks ? toolHooks.getOwnershipLockFiles() : [];
|
|
33178
|
+
}
|
|
33179
|
+
toRulesyncHooks() {
|
|
33180
|
+
const parsed = JSON.parse(this.getFileContent());
|
|
33181
|
+
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
33182
|
+
hooks: parsed.hooks ?? {},
|
|
33183
|
+
overrideKey: "firebender",
|
|
33184
|
+
version: parsed.version ?? 1
|
|
33185
|
+
}), null, 2) });
|
|
33186
|
+
}
|
|
33187
|
+
validate() {
|
|
33188
|
+
return {
|
|
33189
|
+
success: true,
|
|
33190
|
+
error: null
|
|
33191
|
+
};
|
|
33192
|
+
}
|
|
33193
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
33194
|
+
return new FirebenderHooks({
|
|
33195
|
+
outputRoot,
|
|
33196
|
+
relativeDirPath,
|
|
33197
|
+
relativeFilePath,
|
|
33198
|
+
fileContent: "{}",
|
|
33199
|
+
validate: false
|
|
33200
|
+
});
|
|
33201
|
+
}
|
|
33202
|
+
};
|
|
33203
|
+
//#endregion
|
|
32530
33204
|
//#region src/features/hooks/gitlabduo-hooks.ts
|
|
32531
33205
|
const GITLABDUO_CONVERTER_CONFIG = {
|
|
32532
33206
|
supportedEvents: GITLABDUO_HOOK_EVENTS,
|
|
@@ -32954,7 +33628,7 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
|
|
|
32954
33628
|
for (const [nativeEvent, definitions] of Object.entries(toolOverrideHooks ?? {})) {
|
|
32955
33629
|
if (HERMESAGENT_CANONICAL_EVENTS.has(nativeEvent)) continue;
|
|
32956
33630
|
if (HERMESAGENT_SHELL_UNSUPPORTED_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is Python-plugin-only; Hermes Agent refuses it for shell hooks, so the generated entry will never run.`);
|
|
32957
|
-
else if (!HERMESAGENT_NATIVE_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is not documented by Hermes Agent v0.
|
|
33631
|
+
else if (!HERMESAGENT_NATIVE_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is not documented by Hermes Agent v0.22.3; preserving it for forward compatibility.`);
|
|
32958
33632
|
setHermesHookEntries({
|
|
32959
33633
|
result,
|
|
32960
33634
|
event: nativeEvent,
|
|
@@ -36937,6 +37611,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
36937
37611
|
supportedHookTypes: ["command", "prompt"],
|
|
36938
37612
|
supportsMatcher: true
|
|
36939
37613
|
}],
|
|
37614
|
+
["firebender", {
|
|
37615
|
+
class: FirebenderHooks,
|
|
37616
|
+
meta: {
|
|
37617
|
+
supportsProject: true,
|
|
37618
|
+
supportsGlobal: true,
|
|
37619
|
+
supportsImport: true
|
|
37620
|
+
},
|
|
37621
|
+
supportedEvents: FIREBENDER_HOOK_EVENTS,
|
|
37622
|
+
supportedHookTypes: ["command"],
|
|
37623
|
+
supportsMatcher: true
|
|
37624
|
+
}],
|
|
36940
37625
|
["claudecode", {
|
|
36941
37626
|
class: ClaudecodeHooks,
|
|
36942
37627
|
meta: {
|
|
@@ -37033,7 +37718,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
37033
37718
|
},
|
|
37034
37719
|
supportedEvents: COPILOT_HOOK_EVENTS,
|
|
37035
37720
|
supportedHookTypes: ["command"],
|
|
37036
|
-
supportsMatcher:
|
|
37721
|
+
supportsMatcher: true,
|
|
37722
|
+
matcherEvents: COPILOT_MATCHER_HOOK_EVENTS
|
|
37037
37723
|
}],
|
|
37038
37724
|
["copilotcli", {
|
|
37039
37725
|
class: CopilotcliHooks,
|
|
@@ -37048,7 +37734,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
37048
37734
|
"prompt",
|
|
37049
37735
|
"http"
|
|
37050
37736
|
],
|
|
37051
|
-
supportsMatcher:
|
|
37737
|
+
supportsMatcher: true
|
|
37052
37738
|
}],
|
|
37053
37739
|
["kilo", {
|
|
37054
37740
|
class: KiloHooks,
|
|
@@ -37808,6 +38494,67 @@ var AiassistantIgnore = class AiassistantIgnore extends ToolIgnore {
|
|
|
37808
38494
|
}
|
|
37809
38495
|
};
|
|
37810
38496
|
//#endregion
|
|
38497
|
+
//#region src/constants/androidstudio-paths.ts
|
|
38498
|
+
/**
|
|
38499
|
+
* Gemini in Android Studio configuration-layout conventions.
|
|
38500
|
+
*
|
|
38501
|
+
* Only the Android Studio-specific files are owned by the `androidstudio`
|
|
38502
|
+
* target. The shared `AGENTS.md` and `.agents/skills/` surfaces Gemini also
|
|
38503
|
+
* reads belong to the `agentsmd` and `agentsskills` targets.
|
|
38504
|
+
*/
|
|
38505
|
+
const ANDROIDSTUDIO_IGNORE_FILE_NAME = ".aiexclude";
|
|
38506
|
+
const ANDROIDSTUDIO_SKILLS_DIR_PATH = join(".android-studio", "skills");
|
|
38507
|
+
//#endregion
|
|
38508
|
+
//#region src/features/ignore/androidstudio-ignore.ts
|
|
38509
|
+
/**
|
|
38510
|
+
* Ignore generator for Gemini in Android Studio.
|
|
38511
|
+
*
|
|
38512
|
+
* Gemini reads `.aiexclude` files (gitignore syntax) anywhere in the project;
|
|
38513
|
+
* each one applies to the files at or below its directory and blocks both the
|
|
38514
|
+
* chat and the editor AI features such as code completion. Rulesync writes the
|
|
38515
|
+
* project-root file only. There is no user-level variant.
|
|
38516
|
+
*
|
|
38517
|
+
* @see https://developer.android.com/studio/gemini/aiexclude
|
|
38518
|
+
*/
|
|
38519
|
+
var AndroidstudioIgnore = class AndroidstudioIgnore extends ToolIgnore {
|
|
38520
|
+
static getSettablePaths() {
|
|
38521
|
+
return {
|
|
38522
|
+
relativeDirPath: ".",
|
|
38523
|
+
relativeFilePath: ANDROIDSTUDIO_IGNORE_FILE_NAME
|
|
38524
|
+
};
|
|
38525
|
+
}
|
|
38526
|
+
toRulesyncIgnore() {
|
|
38527
|
+
return this.toRulesyncIgnoreDefault();
|
|
38528
|
+
}
|
|
38529
|
+
static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
|
|
38530
|
+
return new AndroidstudioIgnore({
|
|
38531
|
+
outputRoot,
|
|
38532
|
+
relativeDirPath: this.getSettablePaths().relativeDirPath,
|
|
38533
|
+
relativeFilePath: this.getSettablePaths().relativeFilePath,
|
|
38534
|
+
fileContent: rulesyncIgnore.getFileContent()
|
|
38535
|
+
});
|
|
38536
|
+
}
|
|
38537
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true }) {
|
|
38538
|
+
const fileContent = await readFileContent(join(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath));
|
|
38539
|
+
return new AndroidstudioIgnore({
|
|
38540
|
+
outputRoot,
|
|
38541
|
+
relativeDirPath: this.getSettablePaths().relativeDirPath,
|
|
38542
|
+
relativeFilePath: this.getSettablePaths().relativeFilePath,
|
|
38543
|
+
fileContent,
|
|
38544
|
+
validate
|
|
38545
|
+
});
|
|
38546
|
+
}
|
|
38547
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
38548
|
+
return new AndroidstudioIgnore({
|
|
38549
|
+
outputRoot,
|
|
38550
|
+
relativeDirPath,
|
|
38551
|
+
relativeFilePath,
|
|
38552
|
+
fileContent: "",
|
|
38553
|
+
validate: false
|
|
38554
|
+
});
|
|
38555
|
+
}
|
|
38556
|
+
};
|
|
38557
|
+
//#endregion
|
|
37811
38558
|
//#region src/features/ignore/antigravity-cli-ignore.ts
|
|
37812
38559
|
/**
|
|
37813
38560
|
* Antigravity CLI is the successor to Gemini CLI and is built on the same
|
|
@@ -38469,6 +39216,95 @@ var DevinIgnore = class DevinIgnore extends ToolIgnore {
|
|
|
38469
39216
|
}
|
|
38470
39217
|
};
|
|
38471
39218
|
//#endregion
|
|
39219
|
+
//#region src/features/ignore/firebender-ignore.ts
|
|
39220
|
+
function readIgnorePatterns(fileContent) {
|
|
39221
|
+
const ignore = parseSharedConfig({
|
|
39222
|
+
format: "json",
|
|
39223
|
+
fileContent
|
|
39224
|
+
}).ignore;
|
|
39225
|
+
if (!Array.isArray(ignore)) return [];
|
|
39226
|
+
return ignore.filter((pattern) => typeof pattern === "string");
|
|
39227
|
+
}
|
|
39228
|
+
/**
|
|
39229
|
+
* Ignore generator for Firebender.
|
|
39230
|
+
*
|
|
39231
|
+
* Firebender reads gitignore-style patterns from the `ignore` array of
|
|
39232
|
+
* `firebender.json` (project root) and `~/.firebender/firebender.json`
|
|
39233
|
+
* (personal); the two lists are appended together. The file is shared with the
|
|
39234
|
+
* MCP (`mcpServers`) and subagents (`agents`) features and carries settings
|
|
39235
|
+
* rulesync does not own, so only the `ignore` key is written and the file is
|
|
39236
|
+
* never deleted.
|
|
39237
|
+
*
|
|
39238
|
+
* @see https://docs.firebender.com/api-reference/syntax
|
|
39239
|
+
*/
|
|
39240
|
+
var FirebenderIgnore = class FirebenderIgnore extends ToolIgnore {
|
|
39241
|
+
constructor(params) {
|
|
39242
|
+
super(params);
|
|
39243
|
+
this.patterns = readIgnorePatterns(this.fileContent || "{}");
|
|
39244
|
+
}
|
|
39245
|
+
static getSettablePaths({ global = false } = {}) {
|
|
39246
|
+
return {
|
|
39247
|
+
relativeDirPath: getFirebenderConfigDir({ global }),
|
|
39248
|
+
relativeFilePath: FIREBENDER_CONFIG_FILE_NAME
|
|
39249
|
+
};
|
|
39250
|
+
}
|
|
39251
|
+
/** `firebender.json` is a user-managed config file, so rulesync never deletes it. */
|
|
39252
|
+
isDeletable() {
|
|
39253
|
+
return false;
|
|
39254
|
+
}
|
|
39255
|
+
toRulesyncIgnore() {
|
|
39256
|
+
return new RulesyncIgnore({
|
|
39257
|
+
outputRoot: ".",
|
|
39258
|
+
relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
|
|
39259
|
+
relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
|
|
39260
|
+
fileContent: this.patterns.filter((pattern) => pattern.length > 0).join("\n")
|
|
39261
|
+
});
|
|
39262
|
+
}
|
|
39263
|
+
static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
|
|
39264
|
+
const patterns = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
39265
|
+
const paths = this.getSettablePaths({ global });
|
|
39266
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
39267
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
39268
|
+
const managedPatterns = [...new Set(patterns)];
|
|
39269
|
+
return new FirebenderIgnore({
|
|
39270
|
+
outputRoot,
|
|
39271
|
+
relativeDirPath: paths.relativeDirPath,
|
|
39272
|
+
relativeFilePath: paths.relativeFilePath,
|
|
39273
|
+
fileContent: applySharedConfigPatch({
|
|
39274
|
+
fileKey: sharedConfigFileKey(paths),
|
|
39275
|
+
feature: "ignore",
|
|
39276
|
+
existingContent,
|
|
39277
|
+
patch: { ignore: managedPatterns.length > 0 ? managedPatterns : void 0 },
|
|
39278
|
+
filePath
|
|
39279
|
+
}),
|
|
39280
|
+
validate: true,
|
|
39281
|
+
global
|
|
39282
|
+
});
|
|
39283
|
+
}
|
|
39284
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
39285
|
+
const paths = this.getSettablePaths({ global });
|
|
39286
|
+
const fileContent = await readFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
|
|
39287
|
+
return new FirebenderIgnore({
|
|
39288
|
+
outputRoot,
|
|
39289
|
+
relativeDirPath: paths.relativeDirPath,
|
|
39290
|
+
relativeFilePath: paths.relativeFilePath,
|
|
39291
|
+
fileContent,
|
|
39292
|
+
validate,
|
|
39293
|
+
global
|
|
39294
|
+
});
|
|
39295
|
+
}
|
|
39296
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
39297
|
+
return new FirebenderIgnore({
|
|
39298
|
+
outputRoot,
|
|
39299
|
+
relativeDirPath,
|
|
39300
|
+
relativeFilePath,
|
|
39301
|
+
fileContent: "{}",
|
|
39302
|
+
validate: false,
|
|
39303
|
+
global
|
|
39304
|
+
});
|
|
39305
|
+
}
|
|
39306
|
+
};
|
|
39307
|
+
//#endregion
|
|
38472
39308
|
//#region src/features/ignore/grokcli-ignore.ts
|
|
38473
39309
|
/** The base built-in profile the rulesync profile extends (Grok's own default). */
|
|
38474
39310
|
const GROKCLI_SANDBOX_BASE_PROFILE = "workspace";
|
|
@@ -39288,6 +40124,31 @@ const REASONIX_READ_TOOL_NAME = "read_file";
|
|
|
39288
40124
|
* treated as rulesync-owned on generate so the dormant copy is replaced.
|
|
39289
40125
|
*/
|
|
39290
40126
|
const REASONIX_LEGACY_READ_TOOL_NAME = "Read";
|
|
40127
|
+
async function isPathPresent(path) {
|
|
40128
|
+
return await fileExists(path) || await isPresentButUnresolvable(path);
|
|
40129
|
+
}
|
|
40130
|
+
/**
|
|
40131
|
+
* Warns when a project-scope generate is about to create `./reasonix.toml`
|
|
40132
|
+
* next to an existing `.reasonix/reasonix.toml`.
|
|
40133
|
+
*
|
|
40134
|
+
* Since v2.46.0 Reasonix reads `.reasonix/reasonix.toml` as the project config
|
|
40135
|
+
* when `./reasonix.toml` is absent, and never merges the two: once the root
|
|
40136
|
+
* file exists it wins, so every setting in the nested file (permissions,
|
|
40137
|
+
* sandbox, disabled tools, MCP servers, ...) silently stops applying. rulesync
|
|
40138
|
+
* writes only the root file, so it cannot keep the nested one in effect; this
|
|
40139
|
+
* at least names the problem, once per run however many of the features that
|
|
40140
|
+
* write the file (mcp, permissions, ignore) are generated. Like Reasonix, anything at the root path
|
|
40141
|
+
* (including a broken symlink) counts as the root file being there.
|
|
40142
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.47.1/internal/contract/config/paths.go
|
|
40143
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.47.1/docs/CONFIG_PATHS.md
|
|
40144
|
+
*/
|
|
40145
|
+
async function warnIfReasonixNestedProjectConfigShadowed({ outputRoot, global, logger }) {
|
|
40146
|
+
if (global) return;
|
|
40147
|
+
if (await isPathPresent(join(outputRoot, "reasonix.toml"))) return;
|
|
40148
|
+
if (!await isPathPresent(join(outputRoot, REASONIX_NESTED_PROJECT_CONFIG_RELATIVE_FILE_PATH))) return;
|
|
40149
|
+
const nested = toPosixPath(REASONIX_NESTED_PROJECT_CONFIG_RELATIVE_FILE_PATH);
|
|
40150
|
+
warnOnceWithFallback(logger, `Reasonix: ${nested} exists but ${REASONIX_PROJECT_MCP_FILE_NAME} does not. Generating creates ${REASONIX_PROJECT_MCP_FILE_NAME}, and Reasonix then reads it instead of ${nested} (the two are never merged), so every setting in ${nested} stops applying. Move those settings into ${REASONIX_PROJECT_MCP_FILE_NAME} or the rulesync sources.`);
|
|
40151
|
+
}
|
|
39291
40152
|
//#endregion
|
|
39292
40153
|
//#region src/features/ignore/reasonix-ignore.ts
|
|
39293
40154
|
const permissionsTableOf = (document) => toReasonixTable(document.permissions);
|
|
@@ -39371,7 +40232,12 @@ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
|
|
|
39371
40232
|
fileContent: rulesyncPatterns.join("\n")
|
|
39372
40233
|
});
|
|
39373
40234
|
}
|
|
39374
|
-
static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
|
|
40235
|
+
static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false, logger }) {
|
|
40236
|
+
await warnIfReasonixNestedProjectConfigShadowed({
|
|
40237
|
+
outputRoot,
|
|
40238
|
+
global,
|
|
40239
|
+
logger
|
|
40240
|
+
});
|
|
39375
40241
|
const readDenies = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((pattern) => buildReadDenyEntry(pattern, REASONIX_READ_TOOL_NAME));
|
|
39376
40242
|
const paths = this.getSettablePaths({ global });
|
|
39377
40243
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
@@ -39786,6 +40652,7 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
|
|
|
39786
40652
|
const IgnoreProcessorToolTargetSchema = z.enum(ignoreProcessorToolTargetTuple);
|
|
39787
40653
|
const toolIgnoreFactories = /* @__PURE__ */ new Map([
|
|
39788
40654
|
["aiassistant", { class: AiassistantIgnore }],
|
|
40655
|
+
["androidstudio", { class: AndroidstudioIgnore }],
|
|
39789
40656
|
["antigravity-cli", { class: AntigravityCliIgnore }],
|
|
39790
40657
|
["augmentcode", { class: AugmentcodeIgnore }],
|
|
39791
40658
|
["bob", { class: BobIgnore }],
|
|
@@ -39796,6 +40663,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
|
|
|
39796
40663
|
["continue", { class: ContinueIgnore }],
|
|
39797
40664
|
["crush", { class: CrushIgnore }],
|
|
39798
40665
|
["cursor", { class: CursorIgnore }],
|
|
40666
|
+
["firebender", { class: FirebenderIgnore }],
|
|
39799
40667
|
["grokcli", { class: GrokcliIgnore }],
|
|
39800
40668
|
["hermesagent", { class: HermesagentIgnore }],
|
|
39801
40669
|
["junie", { class: JunieIgnore }],
|
|
@@ -39820,6 +40688,7 @@ const ignoreProcessorToolTargets = [...toolIgnoreFactories.keys()];
|
|
|
39820
40688
|
const ignoreProcessorGlobalToolTargets = [
|
|
39821
40689
|
"continue",
|
|
39822
40690
|
"devin",
|
|
40691
|
+
"firebender",
|
|
39823
40692
|
"grokcli",
|
|
39824
40693
|
"kilo",
|
|
39825
40694
|
"kiro",
|
|
@@ -39946,7 +40815,8 @@ var IgnoreProcessor = class extends FeatureProcessor {
|
|
|
39946
40815
|
outputRoot: this.outputRoot,
|
|
39947
40816
|
rulesyncIgnore,
|
|
39948
40817
|
options: this.featureOptions,
|
|
39949
|
-
global: this.global
|
|
40818
|
+
global: this.global,
|
|
40819
|
+
logger: this.logger
|
|
39950
40820
|
});
|
|
39951
40821
|
const auxiliaryFiles = await factory.class.getAuxiliaryFiles?.({
|
|
39952
40822
|
toolIgnore,
|
|
@@ -40296,13 +41166,31 @@ function renameServerField(servers, from, to) {
|
|
|
40296
41166
|
return [name, Object.fromEntries(Object.entries(config).map(([key, value]) => key === from ? [to, value] : [key, value]))];
|
|
40297
41167
|
}));
|
|
40298
41168
|
}
|
|
41169
|
+
function isEmptyValue(value) {
|
|
41170
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
41171
|
+
return isPlainObject$1(value) && Object.keys(value).length === 0;
|
|
41172
|
+
}
|
|
40299
41173
|
/**
|
|
40300
41174
|
* Antigravity uses `serverUrl` (not the canonical `url`) for HTTP/SSE MCP
|
|
40301
41175
|
* servers. Convert the canonical shape into Antigravity's on the way out, and
|
|
40302
|
-
* back to canonical on the way in
|
|
41176
|
+
* back to canonical on the way in.
|
|
41177
|
+
*
|
|
41178
|
+
* Antigravity documents no `type` field: the transport follows from `command`
|
|
41179
|
+
* (stdio) or `serverUrl` (Streamable HTTP / SSE), and `agy plugin validate`
|
|
41180
|
+
* reports the canonical `type` as an unknown field, so it is not written. A
|
|
41181
|
+
* remote server that carries `command`, `args` or `env` was also observed to be
|
|
41182
|
+
* refused at startup (runtime behavior reported by `agy plugin validate` 1.4.4,
|
|
41183
|
+
* not documented), so an empty `args` / `env` left on a `serverUrl` server is
|
|
41184
|
+
* dropped.
|
|
41185
|
+
* https://antigravity.google/docs/mcp
|
|
40303
41186
|
*/
|
|
40304
41187
|
function toAntigravityMcpServers(servers) {
|
|
40305
|
-
|
|
41188
|
+
const renamed = renameServerField(servers, "url", "serverUrl");
|
|
41189
|
+
return Object.fromEntries(Object.entries(renamed).map(([name, config]) => {
|
|
41190
|
+
if (!isPlainObject$1(config)) return [name, config];
|
|
41191
|
+
const isRemote = typeof config.serverUrl === "string";
|
|
41192
|
+
return [name, Object.fromEntries(Object.entries(config).filter(([key, value]) => key !== "type" && !(isRemote && (key === "args" || key === "env") && isEmptyValue(value))))];
|
|
41193
|
+
}));
|
|
40306
41194
|
}
|
|
40307
41195
|
function toCanonicalMcpServers(servers) {
|
|
40308
41196
|
return renameServerField(servers, "serverUrl", "url");
|
|
@@ -44492,6 +45380,60 @@ function applyDeepagentsCommand({ name, server, converted, logger }) {
|
|
|
44492
45380
|
return true;
|
|
44493
45381
|
}
|
|
44494
45382
|
/**
|
|
45383
|
+
* Translate a canonical `oauth` block into dcode's custom Slack OAuth app
|
|
45384
|
+
* settings, or `undefined` (after warning) when it has to be left out.
|
|
45385
|
+
*
|
|
45386
|
+
* Since deepagents-code 0.1.87, `_validate_slack_oauth_config` accepts `oauth`
|
|
45387
|
+
* only as `{ client_id, client_secret? }` of non-empty strings, and only on a
|
|
45388
|
+
* `slack.com` / `*.slack.com` server with `auth: "oauth"`; anything else makes
|
|
45389
|
+
* dcode skip the whole server. The canonical block usually carries Claude
|
|
45390
|
+
* Code's `{ clientId, callbackPort, ... }` shape, so `clientId` becomes
|
|
45391
|
+
* `client_id` (a native `client_id` wins), `client_secret` passes through, and
|
|
45392
|
+
* every other key is dropped.
|
|
45393
|
+
*
|
|
45394
|
+
* @see https://github.com/langchain-ai/deepagents/blob/deepagents-code%3D%3D0.1.87/libs/code/deepagents_code/mcp_config.py
|
|
45395
|
+
*/
|
|
45396
|
+
function toDeepagentsOauth({ name, oauth, converted, logger }) {
|
|
45397
|
+
if (!isSlackOauthServer(converted)) {
|
|
45398
|
+
logger?.warn(`${TOOL_NAME} MCP: dropping oauth on "${name}"; deepagents reads it only on a Slack (slack.com) server with auth: "oauth" and skips any other server that sets it.`);
|
|
45399
|
+
return;
|
|
45400
|
+
}
|
|
45401
|
+
const source = isRecord$1(oauth) ? oauth : {};
|
|
45402
|
+
const result = {};
|
|
45403
|
+
for (const [key, value] of Object.entries(source)) {
|
|
45404
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
45405
|
+
const nativeKey = key === "clientId" ? "client_id" : key;
|
|
45406
|
+
if (nativeKey !== "client_id" && nativeKey !== "client_secret") {
|
|
45407
|
+
logger?.warn(`${TOOL_NAME} MCP: dropping oauth.${key} on "${name}"; deepagents accepts only client_id and client_secret.`);
|
|
45408
|
+
continue;
|
|
45409
|
+
}
|
|
45410
|
+
if (nativeKey !== key && isDeepagentsOauthValue(source[nativeKey])) continue;
|
|
45411
|
+
if (!isDeepagentsOauthValue(value)) {
|
|
45412
|
+
logger?.warn(`${TOOL_NAME} MCP: dropping oauth.${key} on "${name}"; deepagents requires a non-empty string without control characters.`);
|
|
45413
|
+
continue;
|
|
45414
|
+
}
|
|
45415
|
+
result[nativeKey] = value;
|
|
45416
|
+
}
|
|
45417
|
+
if (result.client_id === void 0) {
|
|
45418
|
+
logger?.warn(`${TOOL_NAME} MCP: dropping oauth on "${name}"; deepagents requires oauth.client_id (canonical clientId) and skips a server without it.`);
|
|
45419
|
+
return;
|
|
45420
|
+
}
|
|
45421
|
+
return result;
|
|
45422
|
+
}
|
|
45423
|
+
function isDeepagentsOauthValue(value) {
|
|
45424
|
+
return typeof value === "string" && value.trim() !== "" && !/[\x00-\x1f\x7f]/.test(value);
|
|
45425
|
+
}
|
|
45426
|
+
function isSlackOauthServer(server) {
|
|
45427
|
+
if (server.auth !== "oauth" || typeof server.url !== "string") return false;
|
|
45428
|
+
let host;
|
|
45429
|
+
try {
|
|
45430
|
+
host = new URL(server.url).hostname.toLowerCase();
|
|
45431
|
+
} catch {
|
|
45432
|
+
return false;
|
|
45433
|
+
}
|
|
45434
|
+
return host === "slack.com" || host.endsWith(".slack.com");
|
|
45435
|
+
}
|
|
45436
|
+
/**
|
|
44495
45437
|
* Translate one canonical server into dcode's `.mcp.json` shape, or `null` to
|
|
44496
45438
|
* skip it.
|
|
44497
45439
|
*
|
|
@@ -44514,7 +45456,7 @@ function toDeepagentsServer({ name, server, logger }) {
|
|
|
44514
45456
|
reason: "the WebSocket transport, which deepagents does not support",
|
|
44515
45457
|
logger
|
|
44516
45458
|
});
|
|
44517
|
-
const { enabledTools, disabledTools, type: _type, transport, disabled: _disabled, httpUrl, ...rest } = server;
|
|
45459
|
+
const { enabledTools, disabledTools, type: _type, transport, disabled: _disabled, httpUrl, oauth, ...rest } = server;
|
|
44518
45460
|
const converted = { ...rest };
|
|
44519
45461
|
if (!applyDeepagentsCommand({
|
|
44520
45462
|
name,
|
|
@@ -44529,6 +45471,15 @@ function toDeepagentsServer({ name, server, logger }) {
|
|
|
44529
45471
|
if (transport !== void 0) converted.transport = normalized;
|
|
44530
45472
|
else converted.type = normalized;
|
|
44531
45473
|
}
|
|
45474
|
+
if (oauth !== void 0) {
|
|
45475
|
+
const deepagentsOauth = toDeepagentsOauth({
|
|
45476
|
+
name,
|
|
45477
|
+
oauth,
|
|
45478
|
+
converted,
|
|
45479
|
+
logger
|
|
45480
|
+
});
|
|
45481
|
+
if (deepagentsOauth !== void 0) converted.oauth = deepagentsOauth;
|
|
45482
|
+
}
|
|
44532
45483
|
if (enabledTools !== void 0 && disabledTools !== void 0) return warnAndSkipMcpServer({
|
|
44533
45484
|
toolName: TOOL_NAME,
|
|
44534
45485
|
serverName: name,
|
|
@@ -44555,6 +45506,13 @@ function toRulesyncServer(server) {
|
|
|
44555
45506
|
const converted = { ...rest };
|
|
44556
45507
|
for (const key of ["type", "transport"]) if (converted[key] === "streamable_http" || converted[key] === "streamable-http") converted[key] = "http";
|
|
44557
45508
|
if (Array.isArray(allowedTools)) converted.enabledTools = allowedTools;
|
|
45509
|
+
if (isRecord$1(converted.oauth) && Object.hasOwn(converted.oauth, "client_id")) {
|
|
45510
|
+
const { client_id: clientId, ...oauthRest } = converted.oauth;
|
|
45511
|
+
converted.oauth = Object.hasOwn(oauthRest, "clientId") ? oauthRest : {
|
|
45512
|
+
clientId,
|
|
45513
|
+
...oauthRest
|
|
45514
|
+
};
|
|
45515
|
+
}
|
|
44558
45516
|
return converted;
|
|
44559
45517
|
}
|
|
44560
45518
|
var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
|
|
@@ -45317,6 +46275,171 @@ var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
|
|
|
45317
46275
|
}
|
|
45318
46276
|
};
|
|
45319
46277
|
//#endregion
|
|
46278
|
+
//#region src/features/mcp/firebender-mcp.ts
|
|
46279
|
+
/**
|
|
46280
|
+
* The keys of the canonical server schema. Firebender reads only `command`,
|
|
46281
|
+
* `args`, `env`, `url` and `headers` of them, so the rest are dropped; a key
|
|
46282
|
+
* NOT on this list is unknown to rulesync and passed through untouched, so a
|
|
46283
|
+
* Firebender-native field such as `deferLoading` written in the tool-scoped
|
|
46284
|
+
* `firebender.mcpServers` block (or on a shared server) survives.
|
|
46285
|
+
*/
|
|
46286
|
+
const CANONICAL_MCP_SERVER_KEYS$1 = new Set(Object.keys(McpServerSchema.def.shape));
|
|
46287
|
+
function passthroughNonCanonicalFields$1(serverConfig) {
|
|
46288
|
+
return Object.fromEntries(Object.entries(serverConfig).filter(([key]) => !CANONICAL_MCP_SERVER_KEYS$1.has(key)));
|
|
46289
|
+
}
|
|
46290
|
+
/**
|
|
46291
|
+
* Translate one canonical server into Firebender's `mcpServers` entry, or
|
|
46292
|
+
* return `null` (with a warning) for a server Firebender cannot express. A
|
|
46293
|
+
* stdio server is `{ command, args?, env? }`, a remote one `{ url, headers? }`;
|
|
46294
|
+
* Firebender has no `type` field and no per-server disable switch.
|
|
46295
|
+
*
|
|
46296
|
+
* @see https://docs.firebender.com/api-reference/syntax
|
|
46297
|
+
*/
|
|
46298
|
+
function convertServerToFirebenderFormat({ serverName, serverConfig, logger }) {
|
|
46299
|
+
if (serverConfig.disabled === true) return warnAndSkipMcpServer({
|
|
46300
|
+
toolName: "Firebender",
|
|
46301
|
+
serverName,
|
|
46302
|
+
reason: "`disabled: true`, and Firebender has no per-server disable switch",
|
|
46303
|
+
logger
|
|
46304
|
+
});
|
|
46305
|
+
if (declaresNoTransport(serverConfig)) return warnAndSkipMcpServer({
|
|
46306
|
+
toolName: "Firebender",
|
|
46307
|
+
serverName,
|
|
46308
|
+
reason: "no command or url",
|
|
46309
|
+
logger
|
|
46310
|
+
});
|
|
46311
|
+
const passthrough = passthroughNonCanonicalFields$1(serverConfig);
|
|
46312
|
+
if (isRemoteMcpServer(serverConfig)) {
|
|
46313
|
+
const url = resolveRemoteMcpUrl(serverConfig);
|
|
46314
|
+
if (url === void 0) return warnAndSkipMcpServer({
|
|
46315
|
+
toolName: "Firebender",
|
|
46316
|
+
serverName,
|
|
46317
|
+
reason: "a remote transport but no url",
|
|
46318
|
+
logger
|
|
46319
|
+
});
|
|
46320
|
+
return {
|
|
46321
|
+
...passthrough,
|
|
46322
|
+
url,
|
|
46323
|
+
...serverConfig.headers !== void 0 && { headers: serverConfig.headers }
|
|
46324
|
+
};
|
|
46325
|
+
}
|
|
46326
|
+
const [command, ...args] = resolveLocalMcpCommand(serverConfig);
|
|
46327
|
+
if (command === void 0) return warnAndSkipMcpServer({
|
|
46328
|
+
toolName: "Firebender",
|
|
46329
|
+
serverName,
|
|
46330
|
+
reason: "a local transport but no command",
|
|
46331
|
+
logger
|
|
46332
|
+
});
|
|
46333
|
+
return {
|
|
46334
|
+
...passthrough,
|
|
46335
|
+
command,
|
|
46336
|
+
...args.length > 0 && { args },
|
|
46337
|
+
...serverConfig.env !== void 0 && { env: serverConfig.env }
|
|
46338
|
+
};
|
|
46339
|
+
}
|
|
46340
|
+
/**
|
|
46341
|
+
* MCP generator for Firebender.
|
|
46342
|
+
*
|
|
46343
|
+
* Firebender reads MCP servers from the `mcpServers` key of `firebender.json`
|
|
46344
|
+
* (project root) and `~/.firebender/firebender.json` (personal; it wins on
|
|
46345
|
+
* name clashes). The file is shared with the ignore (`ignore`) and subagents
|
|
46346
|
+
* (`agents`) features and carries settings rulesync does not own, so only the
|
|
46347
|
+
* `mcpServers` key is written and the file is never deleted.
|
|
46348
|
+
*
|
|
46349
|
+
* @see https://docs.firebender.com/api-reference/syntax
|
|
46350
|
+
*/
|
|
46351
|
+
var FirebenderMcp = class FirebenderMcp extends ToolMcp {
|
|
46352
|
+
json;
|
|
46353
|
+
constructor(params) {
|
|
46354
|
+
super(params);
|
|
46355
|
+
this.json = parseSharedConfig({
|
|
46356
|
+
format: "json",
|
|
46357
|
+
fileContent: this.fileContent || "{}"
|
|
46358
|
+
});
|
|
46359
|
+
}
|
|
46360
|
+
getJson() {
|
|
46361
|
+
return this.json;
|
|
46362
|
+
}
|
|
46363
|
+
static getSettablePaths({ global = false } = {}) {
|
|
46364
|
+
return {
|
|
46365
|
+
relativeDirPath: getFirebenderConfigDir({ global }),
|
|
46366
|
+
relativeFilePath: FIREBENDER_CONFIG_FILE_NAME
|
|
46367
|
+
};
|
|
46368
|
+
}
|
|
46369
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
46370
|
+
const paths = this.getSettablePaths({ global });
|
|
46371
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
|
|
46372
|
+
return new FirebenderMcp({
|
|
46373
|
+
outputRoot,
|
|
46374
|
+
relativeDirPath: paths.relativeDirPath,
|
|
46375
|
+
relativeFilePath: paths.relativeFilePath,
|
|
46376
|
+
fileContent,
|
|
46377
|
+
validate,
|
|
46378
|
+
global
|
|
46379
|
+
});
|
|
46380
|
+
}
|
|
46381
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
46382
|
+
const paths = this.getSettablePaths({ global });
|
|
46383
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
46384
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
46385
|
+
const mcpServers = {};
|
|
46386
|
+
for (const [serverName, serverConfig] of Object.entries(rulesyncMcp.getMcpServers())) {
|
|
46387
|
+
const converted = convertServerToFirebenderFormat({
|
|
46388
|
+
serverName,
|
|
46389
|
+
serverConfig,
|
|
46390
|
+
logger
|
|
46391
|
+
});
|
|
46392
|
+
if (converted !== null) mcpServers[serverName] = converted;
|
|
46393
|
+
}
|
|
46394
|
+
return new FirebenderMcp({
|
|
46395
|
+
outputRoot,
|
|
46396
|
+
relativeDirPath: paths.relativeDirPath,
|
|
46397
|
+
relativeFilePath: paths.relativeFilePath,
|
|
46398
|
+
fileContent: applySharedConfigPatch({
|
|
46399
|
+
fileKey: sharedConfigFileKey(paths),
|
|
46400
|
+
feature: "mcp",
|
|
46401
|
+
existingContent,
|
|
46402
|
+
patch: { mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : void 0 },
|
|
46403
|
+
filePath
|
|
46404
|
+
}),
|
|
46405
|
+
validate,
|
|
46406
|
+
global
|
|
46407
|
+
});
|
|
46408
|
+
}
|
|
46409
|
+
toRulesyncMcp() {
|
|
46410
|
+
const servers = this.json.mcpServers;
|
|
46411
|
+
const mcpServers = {};
|
|
46412
|
+
if (isMcpServers(servers)) {
|
|
46413
|
+
for (const [serverName, server] of Object.entries(servers)) if (server !== null && typeof server === "object" && !Array.isArray(server)) mcpServers[serverName] = server;
|
|
46414
|
+
}
|
|
46415
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
46416
|
+
}
|
|
46417
|
+
validate() {
|
|
46418
|
+
return {
|
|
46419
|
+
success: true,
|
|
46420
|
+
error: null
|
|
46421
|
+
};
|
|
46422
|
+
}
|
|
46423
|
+
/**
|
|
46424
|
+
* `firebender.json` is a user-managed file shared with other features (the
|
|
46425
|
+
* ignore feature's `ignore`, the subagents feature's `agents`), so it must
|
|
46426
|
+
* not be deleted.
|
|
46427
|
+
*/
|
|
46428
|
+
isDeletable() {
|
|
46429
|
+
return false;
|
|
46430
|
+
}
|
|
46431
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
46432
|
+
return new FirebenderMcp({
|
|
46433
|
+
outputRoot,
|
|
46434
|
+
relativeDirPath,
|
|
46435
|
+
relativeFilePath,
|
|
46436
|
+
fileContent: "{}",
|
|
46437
|
+
validate: false,
|
|
46438
|
+
global
|
|
46439
|
+
});
|
|
46440
|
+
}
|
|
46441
|
+
};
|
|
46442
|
+
//#endregion
|
|
45320
46443
|
//#region src/features/mcp/gitlabduo-mcp.ts
|
|
45321
46444
|
const CANONICAL_TO_GITLABDUO_TRANSPORT = {
|
|
45322
46445
|
local: "stdio",
|
|
@@ -46122,6 +47245,50 @@ function copyHermesOauth(source) {
|
|
|
46122
47245
|
if (typeof source.cimd === "boolean") oauth.cimd = source.cimd;
|
|
46123
47246
|
return Object.keys(oauth).length > 0 ? oauth : void 0;
|
|
46124
47247
|
}
|
|
47248
|
+
const HERMES_COST_UNIT_MAX_LENGTH = 32;
|
|
47249
|
+
function describeHermesValue(value) {
|
|
47250
|
+
return typeof value === "number" ? String(value) : JSON.stringify(value) ?? String(value);
|
|
47251
|
+
}
|
|
47252
|
+
/**
|
|
47253
|
+
* Copies the per-server MCP cost keys Hermes v0.22.3 reads in
|
|
47254
|
+
* `tools/mcp_tool_costs.py`: `tool_costs` (declared per-tool costs keyed by
|
|
47255
|
+
* MCP tool name; each a number, a `[min, max]` range or `{credits, note}`),
|
|
47256
|
+
* `spend_limit` (per-chat budget for the server's costed tools; `0`/absent =
|
|
47257
|
+
* no limit) and `cost_unit` (label shown with costs; default `credits`).
|
|
47258
|
+
*
|
|
47259
|
+
* A costed call that would exceed `spend_limit` asks first even on a
|
|
47260
|
+
* `trust: full` server, so dropping the key on regenerate would silently
|
|
47261
|
+
* remove the guard. Values Hermes would reject when loading the server are
|
|
47262
|
+
* dropped with a warning instead of being written into a config Hermes then
|
|
47263
|
+
* refuses: `spend_limit` must be a finite non-negative number, `cost_unit` a
|
|
47264
|
+
* non-blank string of at most 32 characters, and `tool_costs` a mapping. The
|
|
47265
|
+
* mapping is rebuilt as an opaque object (so no reference is shared with the
|
|
47266
|
+
* source), with prototype-pollution keys removed at every depth since its
|
|
47267
|
+
* values may themselves be objects.
|
|
47268
|
+
*/
|
|
47269
|
+
function copyHermesCostFields({ source, target, logger }) {
|
|
47270
|
+
let copied = false;
|
|
47271
|
+
const { spend_limit: spendLimit, cost_unit: costUnit, tool_costs: toolCosts } = source;
|
|
47272
|
+
if (spendLimit !== void 0) {
|
|
47273
|
+
if (typeof spendLimit === "number" && Number.isFinite(spendLimit) && spendLimit >= 0) {
|
|
47274
|
+
target.spend_limit = spendLimit;
|
|
47275
|
+
copied = true;
|
|
47276
|
+
} else logger?.warn(`Hermes MCP: dropping spend_limit ${describeHermesValue(spendLimit)}; Hermes requires a non-negative number.`);
|
|
47277
|
+
}
|
|
47278
|
+
if (costUnit !== void 0) {
|
|
47279
|
+
if (typeof costUnit === "string" && costUnit.trim() !== "" && costUnit.length <= HERMES_COST_UNIT_MAX_LENGTH) {
|
|
47280
|
+
target.cost_unit = costUnit;
|
|
47281
|
+
copied = true;
|
|
47282
|
+
} else logger?.warn(`Hermes MCP: dropping cost_unit ${describeHermesValue(costUnit)}; Hermes requires a non-empty string of at most ${HERMES_COST_UNIT_MAX_LENGTH} characters.`);
|
|
47283
|
+
}
|
|
47284
|
+
if (toolCosts !== void 0) {
|
|
47285
|
+
if (isPlainObject$1(toolCosts)) {
|
|
47286
|
+
target.tool_costs = omitPrototypePollutionKeysDeep(toolCosts);
|
|
47287
|
+
copied = true;
|
|
47288
|
+
} else logger?.warn("Hermes MCP: dropping tool_costs; Hermes requires a mapping of MCP tool names to costs.");
|
|
47289
|
+
}
|
|
47290
|
+
return copied;
|
|
47291
|
+
}
|
|
46125
47292
|
const HERMES_OPAQUE_MAPPING_KEYS = [
|
|
46126
47293
|
"sampling",
|
|
46127
47294
|
"elicitation",
|
|
@@ -46133,13 +47300,18 @@ const HERMES_OPAQUE_MAPPING_KEYS = [
|
|
|
46133
47300
|
* alias — `auth` (`oauth` for OAuth 2.1/PKCE), mTLS `client_cert` (string PEM
|
|
46134
47301
|
* path, or `[cert, key]`/`[cert, key, password]` list) and `client_key`,
|
|
46135
47302
|
* `connect_timeout` (seconds), `supports_parallel_tool_calls`, `protocol`,
|
|
46136
|
-
* `lazy`, `keepalive_interval`, `elicitation`, `trust`,
|
|
47303
|
+
* `lazy`, `keepalive_interval`, `elicitation`, `trust`, `identity_header`, and
|
|
47304
|
+
* the cost keys (see {@link copyHermesCostFields}) —
|
|
46137
47305
|
* verbatim from `source` to `target`. Field names are identical on both sides
|
|
46138
47306
|
* (the canonical `McpServerSchema` is a `looseObject`), so this serves export
|
|
46139
47307
|
* and import alike. See the Hermes mcp-config-reference.
|
|
46140
47308
|
*/
|
|
46141
|
-
function copyHermesAdvancedFields(source, target) {
|
|
46142
|
-
let copied =
|
|
47309
|
+
function copyHermesAdvancedFields(source, target, logger) {
|
|
47310
|
+
let copied = copyHermesCostFields({
|
|
47311
|
+
source,
|
|
47312
|
+
target,
|
|
47313
|
+
logger
|
|
47314
|
+
});
|
|
46143
47315
|
if (typeof source.auth === "string") {
|
|
46144
47316
|
target.auth = source.auth;
|
|
46145
47317
|
copied = true;
|
|
@@ -46248,7 +47420,7 @@ function applyHermesToolsBlock(hermesTools, server) {
|
|
|
46248
47420
|
* as Hermes's own `transport: sse`, without which Hermes would connect to it
|
|
46249
47421
|
* over Streamable HTTP.
|
|
46250
47422
|
*/
|
|
46251
|
-
function convertServerToHermes(config) {
|
|
47423
|
+
function convertServerToHermes(config, logger) {
|
|
46252
47424
|
const out = {};
|
|
46253
47425
|
const command = config.command;
|
|
46254
47426
|
const url = resolveHermesUrl(config);
|
|
@@ -46271,7 +47443,7 @@ function convertServerToHermes(config) {
|
|
|
46271
47443
|
if (config.disabled === true) out.enabled = false;
|
|
46272
47444
|
const timeout = resolveHermesTimeout(config);
|
|
46273
47445
|
if (timeout !== void 0) out.timeout = timeout;
|
|
46274
|
-
copyHermesAdvancedFields(config, out);
|
|
47446
|
+
copyHermesAdvancedFields(config, out, logger);
|
|
46275
47447
|
const tools = buildHermesToolsBlock(config);
|
|
46276
47448
|
if (Object.keys(tools).length > 0) out.tools = tools;
|
|
46277
47449
|
return out;
|
|
@@ -46279,11 +47451,11 @@ function convertServerToHermes(config) {
|
|
|
46279
47451
|
/**
|
|
46280
47452
|
* Converts rulesync canonical MCP servers into Hermes `mcp_servers:` entries.
|
|
46281
47453
|
*/
|
|
46282
|
-
function convertToHermesFormat(mcpServers) {
|
|
47454
|
+
function convertToHermesFormat(mcpServers, logger) {
|
|
46283
47455
|
const result = {};
|
|
46284
47456
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
46285
47457
|
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
46286
|
-
result[name] = convertServerToHermes(config);
|
|
47458
|
+
result[name] = convertServerToHermes(config, logger);
|
|
46287
47459
|
}
|
|
46288
47460
|
return result;
|
|
46289
47461
|
}
|
|
@@ -46303,7 +47475,7 @@ function mergeHermesMcpServers(config, mcpServers) {
|
|
|
46303
47475
|
* Mirrors {@link convertToHermesFormat}: `enabled: false` maps back to the
|
|
46304
47476
|
* canonical `disabled: true`, and only recognized fields are carried over.
|
|
46305
47477
|
*/
|
|
46306
|
-
function convertFromHermesFormat(mcpServers) {
|
|
47478
|
+
function convertFromHermesFormat(mcpServers, logger) {
|
|
46307
47479
|
const result = {};
|
|
46308
47480
|
const hermesOverrides = {};
|
|
46309
47481
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
@@ -46320,7 +47492,7 @@ function convertFromHermesFormat(mcpServers) {
|
|
|
46320
47492
|
if (isRecord$1(config.tools)) applyHermesToolsBlock(config.tools, server);
|
|
46321
47493
|
result[name] = server;
|
|
46322
47494
|
const hermesServer = { ...server };
|
|
46323
|
-
if (copyHermesAdvancedFields(config, hermesServer)) hermesOverrides[name] = hermesServer;
|
|
47495
|
+
if (copyHermesAdvancedFields(config, hermesServer, logger)) hermesOverrides[name] = hermesServer;
|
|
46324
47496
|
}
|
|
46325
47497
|
return {
|
|
46326
47498
|
mcpServers: result,
|
|
@@ -46339,8 +47511,10 @@ function convertFromHermesFormat(mcpServers) {
|
|
|
46339
47511
|
*/
|
|
46340
47512
|
var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
46341
47513
|
config;
|
|
46342
|
-
|
|
47514
|
+
logger;
|
|
47515
|
+
constructor({ logger, ...params }) {
|
|
46343
47516
|
super(params);
|
|
47517
|
+
this.logger = logger;
|
|
46344
47518
|
this.config = this.fileContent !== void 0 ? parseSharedConfig({
|
|
46345
47519
|
format: "yaml",
|
|
46346
47520
|
fileContent: this.fileContent
|
|
@@ -46384,7 +47558,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
46384
47558
|
static getExtraSharedWritePaths() {
|
|
46385
47559
|
return getHermesagentSharedConfigWritePaths();
|
|
46386
47560
|
}
|
|
46387
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
47561
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
|
|
46388
47562
|
if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
|
|
46389
47563
|
const paths = this.getSettablePaths({ global });
|
|
46390
47564
|
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
@@ -46394,17 +47568,18 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
46394
47568
|
relativeFilePath: paths.relativeFilePath,
|
|
46395
47569
|
fileContent,
|
|
46396
47570
|
validate,
|
|
46397
|
-
global
|
|
47571
|
+
global,
|
|
47572
|
+
logger
|
|
46398
47573
|
});
|
|
46399
47574
|
}
|
|
46400
|
-
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
47575
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
46401
47576
|
if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
|
|
46402
47577
|
const paths = this.getSettablePaths({ global });
|
|
46403
47578
|
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
46404
47579
|
const merged = mergeHermesMcpServers(parseSharedConfig({
|
|
46405
47580
|
format: "yaml",
|
|
46406
47581
|
fileContent
|
|
46407
|
-
}), convertToHermesFormat(rulesyncMcp.getMcpServers()));
|
|
47582
|
+
}), convertToHermesFormat(rulesyncMcp.getMcpServers(), logger));
|
|
46408
47583
|
return new HermesagentMcp({
|
|
46409
47584
|
outputRoot,
|
|
46410
47585
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -46416,11 +47591,12 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
46416
47591
|
patch: { mcp_servers: merged.mcp_servers }
|
|
46417
47592
|
}),
|
|
46418
47593
|
validate,
|
|
46419
|
-
global
|
|
47594
|
+
global,
|
|
47595
|
+
logger
|
|
46420
47596
|
});
|
|
46421
47597
|
}
|
|
46422
47598
|
toRulesyncMcp() {
|
|
46423
|
-
const { mcpServers: servers, hermesOverrides } = convertFromHermesFormat(isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
|
|
47599
|
+
const { mcpServers: servers, hermesOverrides } = convertFromHermesFormat(isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {}, this.logger);
|
|
46424
47600
|
return this.toRulesyncMcpDefault({
|
|
46425
47601
|
outputRoot: getHermesagentRulesyncOutputRoot({
|
|
46426
47602
|
nativeOutputRoot: this.outputRoot,
|
|
@@ -47304,14 +48480,15 @@ async function readKimiCodeMcpDefaults({ outputRoot }) {
|
|
|
47304
48480
|
if (!config.parsed) return {};
|
|
47305
48481
|
return {
|
|
47306
48482
|
...typeof config.mcp.startup_timeout_ms === "number" && { startupTimeoutMs: config.mcp.startup_timeout_ms },
|
|
47307
|
-
...typeof config.mcp.tool_timeout_ms === "number" && { toolTimeoutMs: config.mcp.tool_timeout_ms }
|
|
48483
|
+
...typeof config.mcp.tool_timeout_ms === "number" && { toolTimeoutMs: config.mcp.tool_timeout_ms },
|
|
48484
|
+
...typeof config.mcp.default_deferred === "boolean" && { defaultDeferred: config.mcp.default_deferred }
|
|
47308
48485
|
};
|
|
47309
48486
|
}
|
|
47310
48487
|
/**
|
|
47311
48488
|
* Kimi's `[mcp]` section in the shared user config sets the default connect and
|
|
47312
|
-
* per-tool-call timeouts
|
|
47313
|
-
* write. Per-server values in
|
|
47314
|
-
* the
|
|
48489
|
+
* per-tool-call timeouts, and whether tools load on demand, for *every* MCP
|
|
48490
|
+
* server, including ones rulesync did not write. Per-server values in
|
|
48491
|
+
* `mcp.json` still take precedence; this only moves the default.
|
|
47315
48492
|
*
|
|
47316
48493
|
* Written as an auxiliary file so it goes through the normal write phase and
|
|
47317
48494
|
* respects `--dry-run`, and merged through the shared-config gateway so the
|
|
@@ -47319,7 +48496,8 @@ async function readKimiCodeMcpDefaults({ outputRoot }) {
|
|
|
47319
48496
|
*
|
|
47320
48497
|
* The gateway replaces an owned key wholesale, and `mcp` is a table, so the
|
|
47321
48498
|
* section is recomputed from the existing file: authoring only
|
|
47322
|
-
* `startupTimeoutMs` must not delete a hand-written `tool_timeout_ms
|
|
48499
|
+
* `startupTimeoutMs` must not delete a hand-written `tool_timeout_ms` or
|
|
48500
|
+
* `default_deferred`. Same
|
|
47323
48501
|
* approach the `.vibe/config.toml` `tools` writer takes.
|
|
47324
48502
|
*
|
|
47325
48503
|
* @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#mcp
|
|
@@ -47346,7 +48524,8 @@ var KimiCodeMcpConfigToml = class KimiCodeMcpConfigToml extends ToolFile {
|
|
|
47346
48524
|
patch: { mcp: {
|
|
47347
48525
|
...existingSection,
|
|
47348
48526
|
...defaults.startupTimeoutMs !== void 0 && { startup_timeout_ms: defaults.startupTimeoutMs },
|
|
47349
|
-
...defaults.toolTimeoutMs !== void 0 && { tool_timeout_ms: defaults.toolTimeoutMs }
|
|
48527
|
+
...defaults.toolTimeoutMs !== void 0 && { tool_timeout_ms: defaults.toolTimeoutMs },
|
|
48528
|
+
...defaults.defaultDeferred !== void 0 && { default_deferred: defaults.defaultDeferred }
|
|
47350
48529
|
} },
|
|
47351
48530
|
filePath: relativeConfigPath
|
|
47352
48531
|
});
|
|
@@ -47444,18 +48623,20 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
|
|
|
47444
48623
|
if (!isRecord$1(block)) return [];
|
|
47445
48624
|
const startupTimeoutMs = typeof block.startupTimeoutMs === "number" ? block.startupTimeoutMs : void 0;
|
|
47446
48625
|
const toolTimeoutMs = typeof block.toolTimeoutMs === "number" ? block.toolTimeoutMs : void 0;
|
|
47447
|
-
|
|
48626
|
+
const defaultDeferred = typeof block.defaultDeferred === "boolean" ? block.defaultDeferred : void 0;
|
|
48627
|
+
if (startupTimeoutMs === void 0 && toolTimeoutMs === void 0 && defaultDeferred === void 0) return [];
|
|
47448
48628
|
const existing = await readKimiCodeConfig({ outputRoot });
|
|
47449
48629
|
if (!existing.parsed) {
|
|
47450
48630
|
const paths = kimiCodeConfigRelativePath();
|
|
47451
|
-
warnWithFallback(logger, `Skipping the Kimi Code MCP
|
|
48631
|
+
warnWithFallback(logger, `Skipping the Kimi Code MCP [mcp] defaults: ${join(paths.relativeDirPath, paths.relativeFilePath)} is not valid TOML.`);
|
|
47452
48632
|
return [];
|
|
47453
48633
|
}
|
|
47454
48634
|
return [KimiCodeMcpConfigToml.fromDefaults({
|
|
47455
48635
|
outputRoot,
|
|
47456
48636
|
defaults: {
|
|
47457
48637
|
startupTimeoutMs,
|
|
47458
|
-
toolTimeoutMs
|
|
48638
|
+
toolTimeoutMs,
|
|
48639
|
+
defaultDeferred
|
|
47459
48640
|
},
|
|
47460
48641
|
existing
|
|
47461
48642
|
})];
|
|
@@ -50031,6 +51212,11 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
|
|
|
50031
51212
|
});
|
|
50032
51213
|
}
|
|
50033
51214
|
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
51215
|
+
await warnIfReasonixNestedProjectConfigShadowed({
|
|
51216
|
+
outputRoot,
|
|
51217
|
+
global,
|
|
51218
|
+
logger
|
|
51219
|
+
});
|
|
50034
51220
|
const paths = this.getSettablePaths({ global });
|
|
50035
51221
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
50036
51222
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
@@ -52008,6 +53194,397 @@ var WarpcliMcp = class extends WarpMcp {
|
|
|
52008
53194
|
}
|
|
52009
53195
|
};
|
|
52010
53196
|
//#endregion
|
|
53197
|
+
//#region src/constants/xum-paths.ts
|
|
53198
|
+
/**
|
|
53199
|
+
* Xum configuration-layout conventions.
|
|
53200
|
+
*
|
|
53201
|
+
* Xum (Coder's desktop app and `xum run` CLI for running coding agents in
|
|
53202
|
+
* parallel workspaces, formerly Mux) keeps its project assets under a `.xum/`
|
|
53203
|
+
* directory at the project root and the matching user assets under `~/.xum/`.
|
|
53204
|
+
* Legacy `.mux/` files are still read when the matching `.xum/` file is
|
|
53205
|
+
* missing; rulesync writes only the `.xum/` paths.
|
|
53206
|
+
*
|
|
53207
|
+
* @see https://xum.cdr.dev/reference/mux-compatibility
|
|
53208
|
+
*/
|
|
53209
|
+
const XUM_DIR = ".xum";
|
|
53210
|
+
const XUM_RULE_FILE_NAME = "AGENTS.md";
|
|
53211
|
+
const XUM_LOCAL_RULE_FILE_NAME = "AGENTS.local.md";
|
|
53212
|
+
const XUM_MCP_FILE_NAME = "mcp.jsonc";
|
|
53213
|
+
const XUM_AGENTS_DIR_PATH = join(XUM_DIR, "agents");
|
|
53214
|
+
const XUM_SKILLS_DIR_PATH = join(XUM_DIR, "skills");
|
|
53215
|
+
//#endregion
|
|
53216
|
+
//#region src/features/mcp/xum-mcp.ts
|
|
53217
|
+
const EMPTY_XUM_MCP_CONFIG = JSON.stringify({ servers: {} }, null, 2);
|
|
53218
|
+
/** Characters that never need quoting in a POSIX shell word. */
|
|
53219
|
+
const SHELL_SAFE_WORD = /^[A-Za-z0-9_\-.,:/@%+=]+$/;
|
|
53220
|
+
/**
|
|
53221
|
+
* A command string free of quotes, escapes, expansions and operators, which
|
|
53222
|
+
* can be split on whitespace back into argv without changing its meaning.
|
|
53223
|
+
*/
|
|
53224
|
+
const SPLIT_SAFE_COMMAND = /^[^'"\\$`|&;<>(){}*?[\]~#!\n]+$/;
|
|
53225
|
+
/** Single-quote one argv element for the POSIX shell Xum runs commands in. */
|
|
53226
|
+
function quoteShellWord(value) {
|
|
53227
|
+
if (SHELL_SAFE_WORD.test(value)) return value;
|
|
53228
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
53229
|
+
}
|
|
53230
|
+
/**
|
|
53231
|
+
* Build the shell command string a Xum stdio server is launched with. A
|
|
53232
|
+
* `command` string is kept verbatim (Xum itself treats it as a raw shell
|
|
53233
|
+
* string, so `npx -y pkg` authored as one string keeps working); an explicit
|
|
53234
|
+
* argv (`command` as an array) and every `args` element are quoted where
|
|
53235
|
+
* needed, so `/opt/my tools/srv` stays one word.
|
|
53236
|
+
*/
|
|
53237
|
+
function buildCommandString(server) {
|
|
53238
|
+
const args = server.args ?? [];
|
|
53239
|
+
if (Array.isArray(server.command)) {
|
|
53240
|
+
const [executable] = server.command;
|
|
53241
|
+
if (executable === void 0 || executable.trim() === "") return void 0;
|
|
53242
|
+
return [...server.command, ...args].map(quoteShellWord).join(" ");
|
|
53243
|
+
}
|
|
53244
|
+
if (typeof server.command !== "string" || server.command.trim() === "") return void 0;
|
|
53245
|
+
return [server.command, ...args.map(quoteShellWord)].join(" ");
|
|
53246
|
+
}
|
|
53247
|
+
/** A name bash accepts on the left of a `NAME=value` assignment. */
|
|
53248
|
+
const SHELL_ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
53249
|
+
/** A canonical `${VAR}` reference whose name bash can expand. */
|
|
53250
|
+
const ENV_VAR_REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
53251
|
+
/**
|
|
53252
|
+
* Quote an env value for a `NAME=value` assignment. Literal text is
|
|
53253
|
+
* single-quoted, so nothing in it is expanded or run, while every canonical
|
|
53254
|
+
* `${VAR}` reference is emitted as `"${VAR}"` so bash still expands it from
|
|
53255
|
+
* the environment Xum starts the server with. The pieces are concatenated into
|
|
53256
|
+
* one shell word: `Bearer ${TOKEN}` becomes `'Bearer '"${TOKEN}"`.
|
|
53257
|
+
*/
|
|
53258
|
+
function quoteEnvValue(value) {
|
|
53259
|
+
if (value === "") return "''";
|
|
53260
|
+
const parts = [];
|
|
53261
|
+
let last = 0;
|
|
53262
|
+
for (const match of value.matchAll(ENV_VAR_REFERENCE)) {
|
|
53263
|
+
if (match.index > last) parts.push(quoteShellWord(value.slice(last, match.index)));
|
|
53264
|
+
parts.push(`"\${${match[1]}}"`);
|
|
53265
|
+
last = match.index + match[0].length;
|
|
53266
|
+
}
|
|
53267
|
+
if (last < value.length) parts.push(quoteShellWord(value.slice(last)));
|
|
53268
|
+
return parts.join("");
|
|
53269
|
+
}
|
|
53270
|
+
/**
|
|
53271
|
+
* Build the `NAME=value ` prefix that sets a stdio server's `env` for its
|
|
53272
|
+
* command only (Xum runs the command string with bash). Returns the prefix
|
|
53273
|
+
* plus the entries left out: names that cannot be bash assignment targets and
|
|
53274
|
+
* entries whose value is not a string.
|
|
53275
|
+
*/
|
|
53276
|
+
function buildEnvPrefix(env) {
|
|
53277
|
+
const assignments = [];
|
|
53278
|
+
const invalidNames = [];
|
|
53279
|
+
const nonStringNames = [];
|
|
53280
|
+
for (const [name, value] of Object.entries(env)) {
|
|
53281
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
53282
|
+
if (!SHELL_ENV_NAME.test(name)) {
|
|
53283
|
+
invalidNames.push(name);
|
|
53284
|
+
continue;
|
|
53285
|
+
}
|
|
53286
|
+
if (typeof value !== "string") {
|
|
53287
|
+
nonStringNames.push(name);
|
|
53288
|
+
continue;
|
|
53289
|
+
}
|
|
53290
|
+
assignments.push(`${name}=${quoteEnvValue(value)}`);
|
|
53291
|
+
}
|
|
53292
|
+
return {
|
|
53293
|
+
prefix: assignments.map((assignment) => `${assignment} `).join(""),
|
|
53294
|
+
invalidNames,
|
|
53295
|
+
nonStringNames
|
|
53296
|
+
};
|
|
53297
|
+
}
|
|
53298
|
+
/** Bash reserved words; a command starting with one cannot take an assignment prefix. */
|
|
53299
|
+
const BASH_RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
53300
|
+
"!",
|
|
53301
|
+
"[[",
|
|
53302
|
+
"]]",
|
|
53303
|
+
"{",
|
|
53304
|
+
"}",
|
|
53305
|
+
"case",
|
|
53306
|
+
"coproc",
|
|
53307
|
+
"do",
|
|
53308
|
+
"done",
|
|
53309
|
+
"elif",
|
|
53310
|
+
"else",
|
|
53311
|
+
"esac",
|
|
53312
|
+
"fi",
|
|
53313
|
+
"for",
|
|
53314
|
+
"function",
|
|
53315
|
+
"if",
|
|
53316
|
+
"in",
|
|
53317
|
+
"select",
|
|
53318
|
+
"then",
|
|
53319
|
+
"time",
|
|
53320
|
+
"until",
|
|
53321
|
+
"while"
|
|
53322
|
+
]);
|
|
53323
|
+
/**
|
|
53324
|
+
* Explain why a `NAME=value` prefix would not reach the server, or return
|
|
53325
|
+
* `undefined` when it would. Bash applies such assignments to the first simple
|
|
53326
|
+
* command only: with a list or pipeline (`cd x && srv`) the server would not
|
|
53327
|
+
* see them, and in front of a group, parenthesized list or reserved word (`( srv )`,
|
|
53328
|
+
* `{ srv; }`, `if ...`) they are a syntax error that keeps the server from
|
|
53329
|
+
* starting. Only the verbatim `command` string is checked for operators,
|
|
53330
|
+
* outside complete single-quoted words; argv elements are quoted by
|
|
53331
|
+
* {@link quoteShellWord}.
|
|
53332
|
+
*/
|
|
53333
|
+
function envPrefixBlocker(server, baseCommand) {
|
|
53334
|
+
if (typeof server.command === "string") {
|
|
53335
|
+
const unquoted = server.command.replace(/'[^']*'/g, "");
|
|
53336
|
+
if (unquoted.includes("'") || /[;&|(){}\n]/.test(unquoted)) return "its command string contains shell operators, grouping or a newline, so bash would not pass NAME=value assignments in front of it to the server";
|
|
53337
|
+
}
|
|
53338
|
+
const [firstWord] = baseCommand.trimStart().split(/\s+/);
|
|
53339
|
+
if (firstWord !== void 0 && BASH_RESERVED_WORDS.has(firstWord)) return `its command starts with the bash reserved word ${quoteValueForWarning(firstWord)}, which cannot follow NAME=value assignments`;
|
|
53340
|
+
}
|
|
53341
|
+
/**
|
|
53342
|
+
* One leading `NAME=value` assignment in exactly the shape {@link quoteEnvValue}
|
|
53343
|
+
* writes: bare shell-safe characters, single-quoted text (with the `'\''`
|
|
53344
|
+
* escape) and `"${VAR}"` references, followed by spaces or tabs (a newline
|
|
53345
|
+
* would end the command, so the assignment would not apply to what follows). Bare characters
|
|
53346
|
+
* are matched one at a time, so every alternative starts with a distinct
|
|
53347
|
+
* character and the pattern cannot backtrack exponentially.
|
|
53348
|
+
*/
|
|
53349
|
+
const LEADING_ENV_ASSIGNMENT = /^([A-Za-z_][A-Za-z0-9_]*)=((?:[A-Za-z0-9_\-.,:/@%+=]|'[^']*'|\\'|"\$\{[A-Za-z_][A-Za-z0-9_]*\}")*)[ \t]+/;
|
|
53350
|
+
/** Decode a value written by {@link quoteEnvValue} back into its canonical form. */
|
|
53351
|
+
function unquoteEnvValue(quoted) {
|
|
53352
|
+
return quoted.replace(/'([^']*)'|\\'|"(\$\{[A-Za-z_][A-Za-z0-9_]*\})"/g, (_m, single, ref) => typeof single === "string" ? single : typeof ref === "string" ? ref : "'");
|
|
53353
|
+
}
|
|
53354
|
+
/**
|
|
53355
|
+
* Split leading `NAME=value` assignments off a command string, but only when
|
|
53356
|
+
* every one of them is in the shape rulesync writes and a plain command
|
|
53357
|
+
* follows (see {@link envPrefixBlocker}); anything else (for example a `$(...)` substitution) keeps the whole string as
|
|
53358
|
+
* the command.
|
|
53359
|
+
*/
|
|
53360
|
+
function splitEnvPrefix(command) {
|
|
53361
|
+
const env = {};
|
|
53362
|
+
let rest = command.trimStart();
|
|
53363
|
+
for (;;) {
|
|
53364
|
+
const match = LEADING_ENV_ASSIGNMENT.exec(rest);
|
|
53365
|
+
if (match === null) break;
|
|
53366
|
+
const [whole, name, quoted] = match;
|
|
53367
|
+
if (name === void 0 || quoted === void 0 || PROTOTYPE_POLLUTION_KEYS.has(name)) break;
|
|
53368
|
+
env[name] = unquoteEnvValue(quoted);
|
|
53369
|
+
rest = rest.slice(whole.length);
|
|
53370
|
+
}
|
|
53371
|
+
if (Object.keys(env).length === 0 || rest.trim() === "" || /^[A-Za-z_][A-Za-z0-9_]*=/.test(rest) || envPrefixBlocker({ command: rest }, rest) !== void 0) return { rest: command };
|
|
53372
|
+
return {
|
|
53373
|
+
env,
|
|
53374
|
+
rest
|
|
53375
|
+
};
|
|
53376
|
+
}
|
|
53377
|
+
/** Read a command string back into `env` + `command` + `args`. */
|
|
53378
|
+
function parseCommandString(command) {
|
|
53379
|
+
const { env, rest } = splitEnvPrefix(command);
|
|
53380
|
+
return {
|
|
53381
|
+
...env !== void 0 && { env },
|
|
53382
|
+
...splitCommandString(rest)
|
|
53383
|
+
};
|
|
53384
|
+
}
|
|
53385
|
+
/** Split a plain command string back into `command` + `args` when it is safe to. */
|
|
53386
|
+
function splitCommandString(command) {
|
|
53387
|
+
const trimmed = command.trim();
|
|
53388
|
+
if (!SPLIT_SAFE_COMMAND.test(trimmed)) return { command };
|
|
53389
|
+
const [head, ...rest] = trimmed.split(/\s+/);
|
|
53390
|
+
if (head === void 0) return { command };
|
|
53391
|
+
return rest.length > 0 ? {
|
|
53392
|
+
command: head,
|
|
53393
|
+
args: rest
|
|
53394
|
+
} : { command: head };
|
|
53395
|
+
}
|
|
53396
|
+
function resolveXumServerKind(server) {
|
|
53397
|
+
const candidate = server.type ?? server.transport;
|
|
53398
|
+
if (candidate === "stdio" || candidate === "local") return "stdio";
|
|
53399
|
+
if (candidate === "http" || candidate === "streamable-http") return "http";
|
|
53400
|
+
if (candidate === "sse") return "sse";
|
|
53401
|
+
if (candidate !== void 0) return { unsupported: candidate };
|
|
53402
|
+
if (server.command !== void 0) return "stdio";
|
|
53403
|
+
const url = server.url ?? server.httpUrl;
|
|
53404
|
+
if (typeof url === "string") return /^wss?:\/\//i.test(url) ? { unsupported: "ws" } : "auto";
|
|
53405
|
+
}
|
|
53406
|
+
/** Convert one canonical server into a Xum `servers` entry, or `null` to skip it. */
|
|
53407
|
+
function convertServerToXum({ name, server, logger }) {
|
|
53408
|
+
const skip = (reason) => {
|
|
53409
|
+
warnWithFallback(logger, `Xum MCP: skipping ${quoteValueForWarning(name)} because ${reason}.`);
|
|
53410
|
+
return null;
|
|
53411
|
+
};
|
|
53412
|
+
const kind = resolveXumServerKind(server);
|
|
53413
|
+
if (kind === void 0) return skip("it has neither a command nor a URL");
|
|
53414
|
+
if (typeof kind === "object") return skip(`it uses the "${kind.unsupported}" transport, which Xum does not support (only stdio, http and sse)`);
|
|
53415
|
+
const settings = {};
|
|
53416
|
+
if (server.disabled === true) settings.disabled = true;
|
|
53417
|
+
if (isStringArray$1(server.enabledTools)) settings.toolAllowlist = server.enabledTools;
|
|
53418
|
+
if (kind !== "stdio") {
|
|
53419
|
+
const url = server.url ?? server.httpUrl;
|
|
53420
|
+
if (typeof url !== "string" || url === "") return skip("it has no URL");
|
|
53421
|
+
return {
|
|
53422
|
+
...kind !== "auto" && { transport: kind },
|
|
53423
|
+
url,
|
|
53424
|
+
...isPlainObject$1(server.headers) && Object.keys(server.headers).length > 0 && { headers: server.headers },
|
|
53425
|
+
...settings
|
|
53426
|
+
};
|
|
53427
|
+
}
|
|
53428
|
+
const baseCommand = buildCommandString(server);
|
|
53429
|
+
if (baseCommand === void 0) return skip("it has no command");
|
|
53430
|
+
let command = baseCommand;
|
|
53431
|
+
if (isPlainObject$1(server.env) && Object.keys(server.env).length > 0) {
|
|
53432
|
+
const blocker = envPrefixBlocker(server, baseCommand);
|
|
53433
|
+
if (blocker !== void 0) warnWithFallback(logger, `Xum MCP: ${quoteValueForWarning(name)} sets env, but ${blocker}; the server is written without env.`);
|
|
53434
|
+
else {
|
|
53435
|
+
const { prefix, invalidNames, nonStringNames } = buildEnvPrefix(server.env);
|
|
53436
|
+
command = `${prefix}${baseCommand}`;
|
|
53437
|
+
if (invalidNames.length > 0) warnWithFallback(logger, `Xum MCP: ${quoteValueForWarning(name)} sets env ${invalidNames.map(quoteValueForWarning).join(", ")}, which cannot be a bash variable name; the server is written without it.`);
|
|
53438
|
+
if (nonStringNames.length > 0) warnWithFallback(logger, `Xum MCP: ${quoteValueForWarning(name)} sets env ${nonStringNames.map(quoteValueForWarning).join(", ")} to a value that is not a string; the server is written without it.`);
|
|
53439
|
+
}
|
|
53440
|
+
}
|
|
53441
|
+
if (typeof server.cwd === "string") warnWithFallback(logger, `Xum MCP: ${quoteValueForWarning(name)} sets cwd, which Xum's mcp.jsonc cannot express; the server is written without it (Xum starts stdio servers in the workspace directory).`);
|
|
53442
|
+
return Object.keys(settings).length > 0 ? {
|
|
53443
|
+
command,
|
|
53444
|
+
...settings
|
|
53445
|
+
} : command;
|
|
53446
|
+
}
|
|
53447
|
+
/** Copy only the string values of a header map (secret references have no canonical form). */
|
|
53448
|
+
function copyStringHeaders(value) {
|
|
53449
|
+
if (!isPlainObject$1(value)) return void 0;
|
|
53450
|
+
const result = {};
|
|
53451
|
+
for (const [key, entry] of Object.entries(value)) if (!PROTOTYPE_POLLUTION_KEYS.has(key) && typeof entry === "string") result[key] = entry;
|
|
53452
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
53453
|
+
}
|
|
53454
|
+
/** Read a Xum `servers` map back into the canonical shape. */
|
|
53455
|
+
function convertFromXumFormat(servers) {
|
|
53456
|
+
if (!isPlainObject$1(servers)) return {};
|
|
53457
|
+
const result = {};
|
|
53458
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
53459
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
53460
|
+
if (typeof entry === "string") {
|
|
53461
|
+
result[name] = parseCommandString(entry);
|
|
53462
|
+
continue;
|
|
53463
|
+
}
|
|
53464
|
+
if (!isPlainObject$1(entry)) continue;
|
|
53465
|
+
const server = {};
|
|
53466
|
+
if (typeof entry.url === "string") {
|
|
53467
|
+
if (entry.transport === "http" || entry.transport === "sse") server.type = entry.transport;
|
|
53468
|
+
server.url = entry.url;
|
|
53469
|
+
const headers = copyStringHeaders(entry.headers);
|
|
53470
|
+
if (headers !== void 0) server.headers = headers;
|
|
53471
|
+
} else if (typeof entry.command === "string") Object.assign(server, parseCommandString(entry.command));
|
|
53472
|
+
else continue;
|
|
53473
|
+
if (entry.disabled === true) server.disabled = true;
|
|
53474
|
+
if (isStringArray$1(entry.toolAllowlist)) server.enabledTools = entry.toolAllowlist;
|
|
53475
|
+
result[name] = server;
|
|
53476
|
+
}
|
|
53477
|
+
return result;
|
|
53478
|
+
}
|
|
53479
|
+
/**
|
|
53480
|
+
* Xum MCP configuration.
|
|
53481
|
+
*
|
|
53482
|
+
* Xum reads `~/.xum/mcp.jsonc` (user) and the repo override `.xum/mcp.jsonc`
|
|
53483
|
+
* (project; a server there wins over the user one of the same name). Both are
|
|
53484
|
+
* a JSONC document with a `servers` map:
|
|
53485
|
+
*
|
|
53486
|
+
* - a stdio server is a shell command string run with bash (the object form
|
|
53487
|
+
* `{ command }` carries `disabled` / `toolAllowlist`); `env` is written as
|
|
53488
|
+
* `NAME=value` assignments in front of the command, and `cwd`, which has no
|
|
53489
|
+
* equivalent, is dropped with a warning;
|
|
53490
|
+
* - a remote server is `{ transport: "http" | "sse" | "auto", url, headers }`
|
|
53491
|
+
* (the transport defaults to auto-detection).
|
|
53492
|
+
*
|
|
53493
|
+
* rulesync owns only `servers`; other top-level keys (Xum writes
|
|
53494
|
+
* `enabledPluginServers` into the user file) are preserved. The project file
|
|
53495
|
+
* may be removed by `--delete`; the user file never is.
|
|
53496
|
+
*
|
|
53497
|
+
* @see https://xum.cdr.dev/config/mcp-servers
|
|
53498
|
+
* @see https://github.com/coder/xum/blob/main/src/node/services/mcpConfigService.ts
|
|
53499
|
+
*/
|
|
53500
|
+
var XumMcp = class XumMcp extends ToolMcp {
|
|
53501
|
+
json;
|
|
53502
|
+
constructor(params) {
|
|
53503
|
+
super(params);
|
|
53504
|
+
this.json = parseSharedConfig({
|
|
53505
|
+
format: "jsonc",
|
|
53506
|
+
fileContent: this.fileContent,
|
|
53507
|
+
filePath: join(this.relativeDirPath, this.relativeFilePath),
|
|
53508
|
+
invalidRootPolicy: "error",
|
|
53509
|
+
jsoncParseErrors: "error"
|
|
53510
|
+
});
|
|
53511
|
+
}
|
|
53512
|
+
getJson() {
|
|
53513
|
+
return this.json;
|
|
53514
|
+
}
|
|
53515
|
+
isDeletable() {
|
|
53516
|
+
return !this.global;
|
|
53517
|
+
}
|
|
53518
|
+
static getSettablePaths(_options = {}) {
|
|
53519
|
+
return {
|
|
53520
|
+
relativeDirPath: XUM_DIR,
|
|
53521
|
+
relativeFilePath: XUM_MCP_FILE_NAME
|
|
53522
|
+
};
|
|
53523
|
+
}
|
|
53524
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
53525
|
+
const paths = this.getSettablePaths({ global });
|
|
53526
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? EMPTY_XUM_MCP_CONFIG;
|
|
53527
|
+
return new XumMcp({
|
|
53528
|
+
outputRoot,
|
|
53529
|
+
relativeDirPath: paths.relativeDirPath,
|
|
53530
|
+
relativeFilePath: paths.relativeFilePath,
|
|
53531
|
+
fileContent,
|
|
53532
|
+
validate,
|
|
53533
|
+
global
|
|
53534
|
+
});
|
|
53535
|
+
}
|
|
53536
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
53537
|
+
const paths = this.getSettablePaths({ global });
|
|
53538
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
53539
|
+
const existingContent = await readFileContentOrNull(filePath) ?? EMPTY_XUM_MCP_CONFIG;
|
|
53540
|
+
const servers = {};
|
|
53541
|
+
for (const [name, server] of Object.entries(rulesyncMcp.getMcpServers())) {
|
|
53542
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
53543
|
+
const entry = convertServerToXum({
|
|
53544
|
+
name,
|
|
53545
|
+
server,
|
|
53546
|
+
logger
|
|
53547
|
+
});
|
|
53548
|
+
if (entry !== null) servers[name] = entry;
|
|
53549
|
+
}
|
|
53550
|
+
return new XumMcp({
|
|
53551
|
+
outputRoot,
|
|
53552
|
+
relativeDirPath: paths.relativeDirPath,
|
|
53553
|
+
relativeFilePath: paths.relativeFilePath,
|
|
53554
|
+
fileContent: applySharedConfigPatch({
|
|
53555
|
+
fileKey: sharedConfigFileKey(paths),
|
|
53556
|
+
feature: "mcp",
|
|
53557
|
+
existingContent,
|
|
53558
|
+
patch: { servers },
|
|
53559
|
+
filePath,
|
|
53560
|
+
logger
|
|
53561
|
+
}),
|
|
53562
|
+
validate,
|
|
53563
|
+
global
|
|
53564
|
+
});
|
|
53565
|
+
}
|
|
53566
|
+
toRulesyncMcp() {
|
|
53567
|
+
const mcpServers = convertFromXumFormat(this.json.servers);
|
|
53568
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
53569
|
+
}
|
|
53570
|
+
validate() {
|
|
53571
|
+
return {
|
|
53572
|
+
success: true,
|
|
53573
|
+
error: null
|
|
53574
|
+
};
|
|
53575
|
+
}
|
|
53576
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
53577
|
+
return new XumMcp({
|
|
53578
|
+
outputRoot,
|
|
53579
|
+
relativeDirPath,
|
|
53580
|
+
relativeFilePath,
|
|
53581
|
+
fileContent: "{}",
|
|
53582
|
+
validate: false,
|
|
53583
|
+
global
|
|
53584
|
+
});
|
|
53585
|
+
}
|
|
53586
|
+
};
|
|
53587
|
+
//#endregion
|
|
52011
53588
|
//#region src/features/mcp/zcode-mcp.ts
|
|
52012
53589
|
/**
|
|
52013
53590
|
* Single spelling of the config.json codec/policy, matching the
|
|
@@ -52041,7 +53618,7 @@ function asZcodeRemoteType(stated, url) {
|
|
|
52041
53618
|
}
|
|
52042
53619
|
/**
|
|
52043
53620
|
* The keys each OAuth flow of ZCode's per-server `oauth` union accepts on a
|
|
52044
|
-
* remote server, and which of them are required. Read off the 3.14.3
|
|
53621
|
+
* remote server, and which of them are required. Read off the 3.14.3–3.16.2
|
|
52045
53622
|
* runtime config schema, since ZCode's MCP page only says to "enable OAuth for
|
|
52046
53623
|
* the service in its configuration". Both variants are strict, so any other
|
|
52047
53624
|
* key would make ZCode skip the server.
|
|
@@ -52145,7 +53722,11 @@ function resolveZcodeTimeoutMs({ serverName, config, logger }) {
|
|
|
52145
53722
|
}
|
|
52146
53723
|
/**
|
|
52147
53724
|
* The per-server keys ZCode reads on every transport: `timeoutMs`,
|
|
52148
|
-
* `protocolVersion` and the enable toggle (`enableKey`).
|
|
53725
|
+
* `protocolVersion`, `alwaysLoad` and the enable toggle (`enableKey`).
|
|
53726
|
+
*
|
|
53727
|
+
* `alwaysLoad` (added in 3.16.2 alongside tool search) keeps a server's tools
|
|
53728
|
+
* loaded up front instead of deferring them behind tool search. ZCode's schema
|
|
53729
|
+
* types it as a boolean, so any other value is dropped with a warning.
|
|
52149
53730
|
*/
|
|
52150
53731
|
function toZcodeCommonKeys({ serverName, config, enableKey, logger }) {
|
|
52151
53732
|
const common = {};
|
|
@@ -52158,6 +53739,9 @@ function toZcodeCommonKeys({ serverName, config, enableKey, logger }) {
|
|
|
52158
53739
|
const { protocolVersion } = config;
|
|
52159
53740
|
if (ZCODE_PROTOCOL_VERSIONS.has(protocolVersion)) common.protocolVersion = protocolVersion;
|
|
52160
53741
|
else if (protocolVersion !== void 0) logger?.warn(`ZCode MCP server "${serverName}": dropping protocolVersion ${JSON.stringify(protocolVersion)}, as ZCode accepts only ${[...ZCODE_PROTOCOL_VERSIONS].map((v) => JSON.stringify(v)).join(", ")}.`);
|
|
53742
|
+
const { alwaysLoad } = config;
|
|
53743
|
+
if (typeof alwaysLoad === "boolean") common.alwaysLoad = alwaysLoad;
|
|
53744
|
+
else if (alwaysLoad !== void 0) logger?.warn(`ZCode MCP server "${serverName}": dropping alwaysLoad ${JSON.stringify(alwaysLoad)}, as it must be a boolean.`);
|
|
52161
53745
|
if (config.disabled === true) common[enableKey] = false;
|
|
52162
53746
|
return common;
|
|
52163
53747
|
}
|
|
@@ -52170,11 +53754,11 @@ function toZcodeCommonKeys({ serverName, config, enableKey, logger }) {
|
|
|
52170
53754
|
* absent.
|
|
52171
53755
|
*
|
|
52172
53756
|
* Every server also carries `timeoutMs` (from the canonical `timeout`, or an
|
|
52173
|
-
* explicit `timeoutMs`)
|
|
52174
|
-
* carry `cwd`, and remote servers an `oauth` built
|
|
52175
|
-
* `zcodeOauth`. That key is stripped by `RulesyncMcp.getMcpServers()`, so it
|
|
53757
|
+
* explicit `timeoutMs`), an allow-listed `protocolVersion` and a boolean
|
|
53758
|
+
* `alwaysLoad`; stdio servers carry `cwd`, and remote servers an `oauth` built
|
|
53759
|
+
* from the namespaced `zcodeOauth`. That key is stripped by `RulesyncMcp.getMcpServers()`, so it
|
|
52176
53760
|
* is read from the authored server through `rawServerOf`. ZCode's docs do not
|
|
52177
|
-
* name these keys, so they follow the 3.14.3
|
|
53761
|
+
* name these keys, so they follow the 3.14.3–3.16.2 runtime's per-server
|
|
52178
53762
|
* schema, which is strict: an unknown key makes ZCode skip the server, which
|
|
52179
53763
|
* is why the output stays a whitelist.
|
|
52180
53764
|
*
|
|
@@ -52269,8 +53853,8 @@ const ZCODE_ENABLE_KEYS = /* @__PURE__ */ new Set(["enable", "enabled"]);
|
|
|
52269
53853
|
* and unknown keys pass through untouched so an import keeps whatever a
|
|
52270
53854
|
* hand-authored entry declared. `timeoutMs` is lifted back into the canonical
|
|
52271
53855
|
* `timeout` (milliseconds); when the server also states a `timeout`, ZCode
|
|
52272
|
-
* has already dropped that one, so `timeoutMs` replaces it.
|
|
52273
|
-
* `oauth` is imported as the namespaced `zcodeOauth`, so
|
|
53856
|
+
* has already dropped that one, so `timeoutMs` replaces it. Any record
|
|
53857
|
+
* `oauth` is imported as the namespaced `zcodeOauth`, so a client secret is
|
|
52274
53858
|
* never emitted to another tool. Generation back out is a whitelist, so only
|
|
52275
53859
|
* the keys `convertToZcodeFormat` writes are re-emitted.
|
|
52276
53860
|
*
|
|
@@ -52296,7 +53880,7 @@ function convertFromZcodeFormat({ zcodeServers }) {
|
|
|
52296
53880
|
continue;
|
|
52297
53881
|
}
|
|
52298
53882
|
if (key === "timeout" && typeof config.timeoutMs === "number") continue;
|
|
52299
|
-
if (key === "oauth" &&
|
|
53883
|
+
if (key === "oauth" && isRecord$1(value)) {
|
|
52300
53884
|
converted[ZCODE_OAUTH_CANONICAL_KEY] = value;
|
|
52301
53885
|
continue;
|
|
52302
53886
|
}
|
|
@@ -52942,6 +54526,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
52942
54526
|
supportsDisabledTools: true
|
|
52943
54527
|
}
|
|
52944
54528
|
}],
|
|
54529
|
+
["firebender", {
|
|
54530
|
+
class: FirebenderMcp,
|
|
54531
|
+
meta: {
|
|
54532
|
+
supportsProject: true,
|
|
54533
|
+
supportsGlobal: true,
|
|
54534
|
+
supportsEnabledTools: false,
|
|
54535
|
+
supportsDisabledTools: false
|
|
54536
|
+
}
|
|
54537
|
+
}],
|
|
52945
54538
|
["goose", {
|
|
52946
54539
|
class: GooseMcp,
|
|
52947
54540
|
meta: {
|
|
@@ -53212,6 +54805,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
53212
54805
|
supportsDisabledTools: false
|
|
53213
54806
|
}
|
|
53214
54807
|
}],
|
|
54808
|
+
["xum", {
|
|
54809
|
+
class: XumMcp,
|
|
54810
|
+
meta: {
|
|
54811
|
+
supportsProject: true,
|
|
54812
|
+
supportsGlobal: true,
|
|
54813
|
+
supportsEnabledTools: true,
|
|
54814
|
+
supportsDisabledTools: false
|
|
54815
|
+
}
|
|
54816
|
+
}],
|
|
53215
54817
|
["devin", {
|
|
53216
54818
|
class: DevinMcp,
|
|
53217
54819
|
meta: {
|
|
@@ -57293,6 +58895,11 @@ const CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS = [
|
|
|
57293
58895
|
reason: "lets sandboxed commands send Apple Events, which removes code-execution isolation",
|
|
57294
58896
|
widens: isNotFalse
|
|
57295
58897
|
},
|
|
58898
|
+
{
|
|
58899
|
+
path: ["allowNestedUserNamespaces"],
|
|
58900
|
+
reason: "lets sandboxed Linux commands create user namespaces, which gets around the sandbox's file protections",
|
|
58901
|
+
widens: isNotFalse
|
|
58902
|
+
},
|
|
57296
58903
|
{
|
|
57297
58904
|
path: ["allowUnsandboxedCommands"],
|
|
57298
58905
|
reason: "controls whether Claude may retry a blocked command outside the sandbox",
|
|
@@ -67742,7 +69349,7 @@ const MANAGED_OMP_TOOLS = /* @__PURE__ */ new Set([
|
|
|
67742
69349
|
* catch-all one at its own strictness, a pattern-specific one as `prompt`).
|
|
67743
69350
|
*
|
|
67744
69351
|
* The `omp.approvalMode` override writes `tools.approvalMode` (`always-ask`,
|
|
67745
|
-
* `write` or `yolo`); without it the key is left as it is in the file, and on
|
|
69352
|
+
* `always-ask-cached`, `write` or `yolo`); without it the key is left as it is in the file, and on
|
|
67746
69353
|
* import a recognized value is read back into the override.
|
|
67747
69354
|
*
|
|
67748
69355
|
* `config.yml` carries every other oh-my-pi setting, so only `tools.approval`
|
|
@@ -70014,6 +71621,8 @@ function buildReasonixPermissionEntry(toolName, pattern) {
|
|
|
70014
71621
|
const REASONIX_TOOL_LABEL = "Reasonix";
|
|
70015
71622
|
/** The `[permissions]` key the override's `allowDynamicBash` writes and reads. */
|
|
70016
71623
|
const REASONIX_ALLOW_DYNAMIC_BASH_KEY = "allow_dynamic_bash";
|
|
71624
|
+
/** The `[permissions]` key the override's `askOnExternalOrigin` writes and reads. */
|
|
71625
|
+
const REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY = "ask_on_external_origin";
|
|
70017
71626
|
/**
|
|
70018
71627
|
* `[sandbox]` keys whose authored value loosens the enforcement layer beneath
|
|
70019
71628
|
* the permission policy: they take Bash out of its OS jail, open that jail to
|
|
@@ -70216,6 +71825,95 @@ function applyPermissionsMode({ permissions, authored, global, relativeFilePath,
|
|
|
70216
71825
|
reason: "lets every writer call that no rule matches run without a prompt"
|
|
70217
71826
|
}];
|
|
70218
71827
|
}
|
|
71828
|
+
/**
|
|
71829
|
+
* Writes the override's `askOnExternalOrigin` into `[permissions]`. Reasonix
|
|
71830
|
+
* honors the key only in the user config (`narrowPermissions` in
|
|
71831
|
+
* `project_scope.go`), so a project-scope generate skips it with a warning and
|
|
71832
|
+
* leaves whatever the file had. Turning it on only adds prompts, so that is
|
|
71833
|
+
* quiet; turning off a value the file had on is reported, since Auto and YOLO
|
|
71834
|
+
* then run calls that repeat text from a web page or tool result without asking.
|
|
71835
|
+
* As with `applyAllowDynamicBash`, only a literal `true` counts as quiet, so a
|
|
71836
|
+
* value a `--no-validate` run let through is never written in silence.
|
|
71837
|
+
*
|
|
71838
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.47.1/docs/GUIDE.md
|
|
71839
|
+
*/
|
|
71840
|
+
function applyAskOnExternalOrigin({ permissions, override, global, relativeFilePath, logger }) {
|
|
71841
|
+
const authored = override?.askOnExternalOrigin;
|
|
71842
|
+
if (authored === void 0) return [];
|
|
71843
|
+
if (!global) {
|
|
71844
|
+
logger?.warn(`Reasonix permissions: skipping reasonix.askOnExternalOrigin for the project-scoped ${relativeFilePath}; Reasonix honors [permissions] ask_on_external_origin only in the user config and ignores a project value, so generate it with --global instead.`);
|
|
71845
|
+
return [];
|
|
71846
|
+
}
|
|
71847
|
+
const previous = permissions[REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY];
|
|
71848
|
+
permissions[REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY] = authored;
|
|
71849
|
+
if (authored === true || previous !== true && authored === false) return [];
|
|
71850
|
+
return [{
|
|
71851
|
+
label: `permissions.${REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY}`,
|
|
71852
|
+
reason: "lets Auto and YOLO run a call that repeats text from a web page or tool result without asking first"
|
|
71853
|
+
}];
|
|
71854
|
+
}
|
|
71855
|
+
/**
|
|
71856
|
+
* The `tools` patch entry — the `[tools]` table with the override's `disabled`
|
|
71857
|
+
* list laid over it, or nothing when the override does not author one — and the
|
|
71858
|
+
* trust rows it carries. Adding a name only removes a tool, and a project file
|
|
71859
|
+
* can only add to the user's list, so that stays quiet. The list is replaced
|
|
71860
|
+
* whole, though, so a name the file had turned off and the override leaves out
|
|
71861
|
+
* is turned back on, and that is reported, as are a `[tools]` or a `disabled`
|
|
71862
|
+
* the file holds in a shape that cannot be read. Only `disabled` is written:
|
|
71863
|
+
* the rest of `[tools]` (timeouts, search backends, ...) stays as it is.
|
|
71864
|
+
*/
|
|
71865
|
+
function applyToolsDisabled({ existing, override }) {
|
|
71866
|
+
const disabled = override?.tools?.disabled;
|
|
71867
|
+
if (disabled === void 0) return {
|
|
71868
|
+
patch: {},
|
|
71869
|
+
entries: []
|
|
71870
|
+
};
|
|
71871
|
+
const patch = { tools: {
|
|
71872
|
+
...asReasonixRecord(existing),
|
|
71873
|
+
disabled
|
|
71874
|
+
} };
|
|
71875
|
+
if (existing === void 0) return {
|
|
71876
|
+
patch,
|
|
71877
|
+
entries: []
|
|
71878
|
+
};
|
|
71879
|
+
if (!isRecord$1(existing)) return {
|
|
71880
|
+
patch,
|
|
71881
|
+
entries: [{
|
|
71882
|
+
label: "tools",
|
|
71883
|
+
reason: replacedUnreadableReason({
|
|
71884
|
+
shape: "object",
|
|
71885
|
+
toolLabel: REASONIX_TOOL_LABEL
|
|
71886
|
+
})
|
|
71887
|
+
}]
|
|
71888
|
+
};
|
|
71889
|
+
const before = existing.disabled;
|
|
71890
|
+
if (before === void 0) return {
|
|
71891
|
+
patch,
|
|
71892
|
+
entries: []
|
|
71893
|
+
};
|
|
71894
|
+
if (!Array.isArray(before)) return {
|
|
71895
|
+
patch,
|
|
71896
|
+
entries: [{
|
|
71897
|
+
label: "tools.disabled",
|
|
71898
|
+
reason: replacedUnreadableReason({
|
|
71899
|
+
shape: "list",
|
|
71900
|
+
toolLabel: REASONIX_TOOL_LABEL
|
|
71901
|
+
})
|
|
71902
|
+
}]
|
|
71903
|
+
};
|
|
71904
|
+
const reEnabled = uniq(before.filter((name) => !disabled.includes(name)));
|
|
71905
|
+
if (reEnabled.length === 0) return {
|
|
71906
|
+
patch,
|
|
71907
|
+
entries: []
|
|
71908
|
+
};
|
|
71909
|
+
return {
|
|
71910
|
+
patch,
|
|
71911
|
+
entries: [{
|
|
71912
|
+
label: "tools.disabled",
|
|
71913
|
+
reason: `turns back on ${reEnabled.map((name) => quoteValueForWarning(name)).join(", ")}, which the file had turned off`
|
|
71914
|
+
}]
|
|
71915
|
+
};
|
|
71916
|
+
}
|
|
70219
71917
|
function parseReasonixConfig(fileContent) {
|
|
70220
71918
|
const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
|
|
70221
71919
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
@@ -70271,6 +71969,11 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
70271
71969
|
});
|
|
70272
71970
|
}
|
|
70273
71971
|
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
|
|
71972
|
+
await warnIfReasonixNestedProjectConfigShadowed({
|
|
71973
|
+
outputRoot,
|
|
71974
|
+
global,
|
|
71975
|
+
logger
|
|
71976
|
+
});
|
|
70274
71977
|
const paths = this.getSettablePaths({ global });
|
|
70275
71978
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
70276
71979
|
const relativeFilePathForLog = toPosixPath(join(paths.relativeDirPath, paths.relativeFilePath));
|
|
@@ -70330,7 +72033,20 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
70330
72033
|
relativeFilePath: relativeFilePathForLog,
|
|
70331
72034
|
logger
|
|
70332
72035
|
}));
|
|
72036
|
+
trustAffecting.push(...applyAskOnExternalOrigin({
|
|
72037
|
+
permissions: mergedPermissions,
|
|
72038
|
+
override,
|
|
72039
|
+
global,
|
|
72040
|
+
relativeFilePath: relativeFilePathForLog,
|
|
72041
|
+
logger
|
|
72042
|
+
}));
|
|
70333
72043
|
const patch = { permissions: mergedPermissions };
|
|
72044
|
+
const toolsDisabled = applyToolsDisabled({
|
|
72045
|
+
existing: parsed.tools,
|
|
72046
|
+
override
|
|
72047
|
+
});
|
|
72048
|
+
Object.assign(patch, toolsDisabled.patch);
|
|
72049
|
+
trustAffecting.push(...toolsDisabled.entries);
|
|
70334
72050
|
if (override?.sandbox !== void 0) {
|
|
70335
72051
|
const authoredSandbox = dropUserOnlySandboxKeys({
|
|
70336
72052
|
sandbox: override.sandbox,
|
|
@@ -70404,6 +72120,14 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
70404
72120
|
const allowDynamicBash = permissions[REASONIX_ALLOW_DYNAMIC_BASH_KEY];
|
|
70405
72121
|
if (typeof allowDynamicBash === "boolean") reasonixOverride.allowDynamicBash = allowDynamicBash;
|
|
70406
72122
|
if (this.global && typeof permissions.mode === "string" && REASONIX_PERMISSION_MODES.has(permissions.mode)) reasonixOverride.mode = permissions.mode;
|
|
72123
|
+
const askOnExternalOrigin = permissions[REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY];
|
|
72124
|
+
if (this.global && typeof askOnExternalOrigin === "boolean") reasonixOverride.askOnExternalOrigin = askOnExternalOrigin;
|
|
72125
|
+
const toolsDisabled = asReasonixRecord(this.toml.tools).disabled;
|
|
72126
|
+
if (Array.isArray(toolsDisabled)) {
|
|
72127
|
+
const names = toReasonixStringArray(toolsDisabled);
|
|
72128
|
+
if (names.length < toolsDisabled.length) warnWithFallback(void 0, `Reasonix permissions: ${this.getRelativePathFromCwd()} [tools] disabled holds ${toolsDisabled.length - names.length} non-string entries; they are not imported into reasonix.tools.disabled, so the next generate drops them.`);
|
|
72129
|
+
reasonixOverride.tools = { disabled: names };
|
|
72130
|
+
}
|
|
70407
72131
|
if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
|
|
70408
72132
|
if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
|
|
70409
72133
|
if (allowSplit.exact.length > 0) reasonixOverride.rawAllow = allowSplit.exact;
|
|
@@ -71985,7 +73709,12 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
|
71985
73709
|
const authoredProviderOptions = isPlainObject$1(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
|
|
71986
73710
|
const runtimeModeRefusesProviderOptions = authoredProviderOptions !== void 0 && runtime.active;
|
|
71987
73711
|
if (runtimeModeRefusesProviderOptions) logger?.warn(`Takt permissions: not writing "${TAKT_PROVIDER_OPTIONS_KEY}" to ${filePath} because ${runtime.filePaths.join(" + ")} puts Takt in runtime provider mode (Takt 0.56.0+), where any legacy provider setting in config.yaml makes Takt fail with "Mixed provider configuration detected". Move those options to \`provider.profiles.<profile>.options\` in runtime.yaml, and remove them from the \`takt\` block of the rulesync source. A "${TAKT_PROVIDER_OPTIONS_KEY}" already in ${filePath} is left untouched — rulesync does not own that key.`);
|
|
71988
|
-
const overrideProviderOptions = runtimeModeRefusesProviderOptions ? void 0 :
|
|
73712
|
+
const overrideProviderOptions = runtimeModeRefusesProviderOptions ? void 0 : dropUntrustedProviderOptions({
|
|
73713
|
+
providerOptions: authoredProviderOptions,
|
|
73714
|
+
global,
|
|
73715
|
+
filePath,
|
|
73716
|
+
logger
|
|
73717
|
+
});
|
|
71989
73718
|
const authoredPolicies = pickSecurityPolicies(override, {
|
|
71990
73719
|
filePath,
|
|
71991
73720
|
logger,
|
|
@@ -72489,6 +74218,68 @@ function taktModeToRulesyncConfig(mode) {
|
|
|
72489
74218
|
default: return { permission: { bash: { [CATCH_ALL_PATTERN$1]: "deny" } } };
|
|
72490
74219
|
}
|
|
72491
74220
|
}
|
|
74221
|
+
const TAKT_BASE_URL_PROVIDERS = [
|
|
74222
|
+
"codex",
|
|
74223
|
+
"claude",
|
|
74224
|
+
"deepseek_harness"
|
|
74225
|
+
];
|
|
74226
|
+
const TAKT_DEEPSEEK_HARNESS_KEY = "deepseek_harness";
|
|
74227
|
+
/** Mirrors Takt's `isLoopbackBaseUrl`: localhost, *.localhost, ::1 or 127.x.x.x. */
|
|
74228
|
+
function isLoopbackBaseUrl(value) {
|
|
74229
|
+
let hostname;
|
|
74230
|
+
try {
|
|
74231
|
+
hostname = new URL(value).hostname.toLowerCase();
|
|
74232
|
+
} catch {
|
|
74233
|
+
return false;
|
|
74234
|
+
}
|
|
74235
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || hostname === "[::1]") return true;
|
|
74236
|
+
const octets = hostname.split(".");
|
|
74237
|
+
return octets.length === 4 && octets[0] === "127" && octets.every((octet) => /^\d+$/.test(octet) && Number(octet) <= 255);
|
|
74238
|
+
}
|
|
74239
|
+
/** The keys of one `provider_options.<provider>` entry Takt rejects in config.yaml. */
|
|
74240
|
+
function untrustedProviderOptionKeys({ provider, options, global }) {
|
|
74241
|
+
const keys = [];
|
|
74242
|
+
if (provider === TAKT_DEEPSEEK_HARNESS_KEY) {
|
|
74243
|
+
if (options.reasoning_effort !== void 0) keys.push("reasoning_effort");
|
|
74244
|
+
const { plugins } = options;
|
|
74245
|
+
if (!global && plugins !== void 0 && (!Array.isArray(plugins) || plugins.length > 0)) keys.push("plugins");
|
|
74246
|
+
}
|
|
74247
|
+
const baseUrl = options.base_url;
|
|
74248
|
+
if (!global && TAKT_BASE_URL_PROVIDERS.includes(provider) && baseUrl !== void 0 && (typeof baseUrl !== "string" || !isLoopbackBaseUrl(baseUrl))) keys.push("base_url");
|
|
74249
|
+
return keys;
|
|
74250
|
+
}
|
|
74251
|
+
/**
|
|
74252
|
+
* Drop the `provider_options` entries Takt refuses to load from `config.yaml`
|
|
74253
|
+
* (it fails at startup with a `Configuration error`), warning for each:
|
|
74254
|
+
* - `deepseek_harness.reasoning_effort` in either scope — Takt accepts it only
|
|
74255
|
+
* in runtime profile options or `TAKT_PROVIDER_OPTIONS_DEEPSEEK_HARNESS_REASONING_EFFORT`.
|
|
74256
|
+
* - at project scope, a non-empty `deepseek_harness.plugins` (Takt 0.71.0+,
|
|
74257
|
+
* global config only) and a non-loopback `{codex,claude,deepseek_harness}.base_url`.
|
|
74258
|
+
* https://github.com/nrslib/takt/blob/main/src/infra/config/providerOptions.ts
|
|
74259
|
+
* https://github.com/nrslib/takt/blob/main/docs/configuration.md
|
|
74260
|
+
*/
|
|
74261
|
+
function dropUntrustedProviderOptions({ providerOptions, global, filePath, logger }) {
|
|
74262
|
+
if (providerOptions === void 0) return;
|
|
74263
|
+
const dropped = [];
|
|
74264
|
+
const result = {};
|
|
74265
|
+
for (const [provider, options] of Object.entries(providerOptions)) {
|
|
74266
|
+
if (!isPlainObject$1(options)) {
|
|
74267
|
+
result[provider] = options;
|
|
74268
|
+
continue;
|
|
74269
|
+
}
|
|
74270
|
+
const untrusted = untrustedProviderOptionKeys({
|
|
74271
|
+
provider,
|
|
74272
|
+
options,
|
|
74273
|
+
global
|
|
74274
|
+
});
|
|
74275
|
+
const kept = Object.fromEntries(Object.entries(options).filter(([key]) => !untrusted.includes(key)));
|
|
74276
|
+
dropped.push(...untrusted.map((key) => `${TAKT_PROVIDER_OPTIONS_KEY}.${provider}.${key}`));
|
|
74277
|
+
if (Object.keys(kept).length > 0 || Object.keys(options).length === 0) result[provider] = kept;
|
|
74278
|
+
}
|
|
74279
|
+
if (dropped.length === 0) return providerOptions;
|
|
74280
|
+
logger?.warn(`Takt permissions: not writing ${dropped.map((key) => `"${key}"`).join(", ")} to ${filePath} because Takt refuses to load ${dropped.length === 1 ? "it" : "them"} from ${global ? "config.yaml" : "a project config.yaml"} and fails at startup with a "Configuration error". Set non-loopback base URLs and DeepSeek Harness plugins in the global ~/.takt/config.yaml (or the TAKT_PROVIDER_OPTIONS_* environment variables), and DeepSeek Harness reasoning_effort in a runtime.yaml profile's options or TAKT_PROVIDER_OPTIONS_DEEPSEEK_HARNESS_REASONING_EFFORT, then remove them from the \`takt\` block of the rulesync source.`);
|
|
74281
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
74282
|
+
}
|
|
72492
74283
|
/**
|
|
72493
74284
|
* Lift Takt's security-policy keys out of a source object, keeping only the
|
|
72494
74285
|
* shapes Takt itself accepts (a boolean, or a table of booleans). Used in both
|
|
@@ -76423,6 +78214,11 @@ const TOOL_DISPLAY = [
|
|
|
76423
78214
|
label: "Factory Droid",
|
|
76424
78215
|
group: "ai"
|
|
76425
78216
|
},
|
|
78217
|
+
{
|
|
78218
|
+
key: "firebender",
|
|
78219
|
+
label: "Firebender",
|
|
78220
|
+
group: "ai"
|
|
78221
|
+
},
|
|
76426
78222
|
{
|
|
76427
78223
|
key: "opencode",
|
|
76428
78224
|
label: "OpenCode",
|
|
@@ -76578,6 +78374,11 @@ const TOOL_DISPLAY = [
|
|
|
76578
78374
|
label: "JetBrains AI Assistant",
|
|
76579
78375
|
group: "ai"
|
|
76580
78376
|
},
|
|
78377
|
+
{
|
|
78378
|
+
key: "androidstudio",
|
|
78379
|
+
label: "Gemini in Android Studio",
|
|
78380
|
+
group: "ai"
|
|
78381
|
+
},
|
|
76581
78382
|
{
|
|
76582
78383
|
key: "junie",
|
|
76583
78384
|
label: "JetBrains Junie",
|
|
@@ -76613,6 +78414,11 @@ const TOOL_DISPLAY = [
|
|
|
76613
78414
|
label: "Warp Agent CLI",
|
|
76614
78415
|
group: "ai"
|
|
76615
78416
|
},
|
|
78417
|
+
{
|
|
78418
|
+
key: "xum",
|
|
78419
|
+
label: "Xum",
|
|
78420
|
+
group: "ai"
|
|
78421
|
+
},
|
|
76616
78422
|
{
|
|
76617
78423
|
key: "replit",
|
|
76618
78424
|
label: "Replit",
|
|
@@ -77757,6 +79563,27 @@ var AmpSkill = class AmpSkill extends ToolSkill {
|
|
|
77757
79563
|
}
|
|
77758
79564
|
};
|
|
77759
79565
|
//#endregion
|
|
79566
|
+
//#region src/features/skills/androidstudio-skill.ts
|
|
79567
|
+
/**
|
|
79568
|
+
* Gemini in Android Studio skills follow the Agent Skills open standard
|
|
79569
|
+
* (`<name>/SKILL.md` with `name` and `description` frontmatter), so this target
|
|
79570
|
+
* reuses {@link AgentsSkillsSkill} with Android Studio's own location:
|
|
79571
|
+
* `.android-studio/skills/` at the project root (project) or in the home
|
|
79572
|
+
* directory (global). The shared `.agents/skills/` location Android Studio also
|
|
79573
|
+
* reads belongs to the `agentsskills` target.
|
|
79574
|
+
*
|
|
79575
|
+
* @see https://developer.android.com/studio/gemini/skills
|
|
79576
|
+
*/
|
|
79577
|
+
var AndroidstudioSkill = class extends AgentsSkillsSkill {
|
|
79578
|
+
static getSettablePaths(_options = {}) {
|
|
79579
|
+
return { relativeDirPath: ANDROIDSTUDIO_SKILLS_DIR_PATH };
|
|
79580
|
+
}
|
|
79581
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
79582
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
79583
|
+
return targets.includes("*") || targets.includes("androidstudio");
|
|
79584
|
+
}
|
|
79585
|
+
};
|
|
79586
|
+
//#endregion
|
|
77760
79587
|
//#region src/features/skills/antigravity-plugin-skill.ts
|
|
77761
79588
|
var AntigravityPluginSkill = class extends AntigravityIdeSkill {
|
|
77762
79589
|
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
@@ -81225,6 +83052,26 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
81225
83052
|
}
|
|
81226
83053
|
};
|
|
81227
83054
|
//#endregion
|
|
83055
|
+
//#region src/features/skills/firebender-skill.ts
|
|
83056
|
+
/**
|
|
83057
|
+
* Firebender skills follow the Agent Skills layout (`<name>/SKILL.md` with the
|
|
83058
|
+
* required `name` and `description` frontmatter, plus optional supporting
|
|
83059
|
+
* files), so this target reuses {@link AgentsSkillsSkill} with Firebender's
|
|
83060
|
+
* recommended locations: `.firebender/skills/` (project) and
|
|
83061
|
+
* `~/.firebender/skills/` (user).
|
|
83062
|
+
*
|
|
83063
|
+
* @see https://docs.firebender.com/api-reference/skills
|
|
83064
|
+
*/
|
|
83065
|
+
var FirebenderSkill = class extends AgentsSkillsSkill {
|
|
83066
|
+
static getSettablePaths(_options = {}) {
|
|
83067
|
+
return { relativeDirPath: FIREBENDER_SKILLS_DIR_PATH };
|
|
83068
|
+
}
|
|
83069
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
83070
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
83071
|
+
return targets.includes("*") || targets.includes("firebender");
|
|
83072
|
+
}
|
|
83073
|
+
};
|
|
83074
|
+
//#endregion
|
|
81228
83075
|
//#region src/features/skills/gitlabduo-skill.ts
|
|
81229
83076
|
/**
|
|
81230
83077
|
* GitLab Duo skills follow the Agent Skills standard (`name` and `description`
|
|
@@ -84760,6 +86607,110 @@ var WarpSkill = class WarpSkill extends ToolSkill {
|
|
|
84760
86607
|
}
|
|
84761
86608
|
};
|
|
84762
86609
|
//#endregion
|
|
86610
|
+
//#region src/features/skills/xum-skill.ts
|
|
86611
|
+
/**
|
|
86612
|
+
* The Agent Skills standard keys the `agentsskills` section carries. On import
|
|
86613
|
+
* they go back there, exactly as for the `agentsskills` target, and every other
|
|
86614
|
+
* key besides `name` / `description` goes into the `xum` section.
|
|
86615
|
+
*/
|
|
86616
|
+
const AGENT_SKILLS_STANDARD_KEYS = /* @__PURE__ */ new Set([
|
|
86617
|
+
"name",
|
|
86618
|
+
"description",
|
|
86619
|
+
"license",
|
|
86620
|
+
"compatibility",
|
|
86621
|
+
"metadata",
|
|
86622
|
+
"allowed-tools"
|
|
86623
|
+
]);
|
|
86624
|
+
/**
|
|
86625
|
+
* Xum skills follow the Agent Skills layout (`<name>/SKILL.md` with the
|
|
86626
|
+
* required `name` and `description` frontmatter; the directory name is
|
|
86627
|
+
* lowercase letters, digits and single hyphens up to 64 characters, and
|
|
86628
|
+
* `name` must match it), so this target reuses {@link AgentsSkillsSkill} with
|
|
86629
|
+
* Xum's locations: `.xum/skills/` (project) and `~/.xum/skills/` (user).
|
|
86630
|
+
* Within one directory `.xum/skills` wins over `.agents/skills`.
|
|
86631
|
+
*
|
|
86632
|
+
* On top of the standard fields, Xum reads `disable-model-invocation` (and its
|
|
86633
|
+
* own `advertise: false` alias), `user-invocable`, `argument-hint`,
|
|
86634
|
+
* `when_to_use` / `when-to-use` and `paths` (globs that make the skill
|
|
86635
|
+
* path-scoped). Those come from the `xum` section of the rulesync skill and are
|
|
86636
|
+
* written verbatim; the two invocation gates fall back to the root-level
|
|
86637
|
+
* rulesync values.
|
|
86638
|
+
*
|
|
86639
|
+
* @see https://xum.cdr.dev/agents/agent-skills
|
|
86640
|
+
*/
|
|
86641
|
+
var XumSkill = class XumSkill extends AgentsSkillsSkill {
|
|
86642
|
+
static getSettablePaths(_options = {}) {
|
|
86643
|
+
return { relativeDirPath: XUM_SKILLS_DIR_PATH };
|
|
86644
|
+
}
|
|
86645
|
+
toRulesyncSkill() {
|
|
86646
|
+
const base = super.toRulesyncSkill();
|
|
86647
|
+
const xumSection = Object.fromEntries(Object.entries(this.getFrontmatter()).filter(([key]) => !AGENT_SKILLS_STANDARD_KEYS.has(key)));
|
|
86648
|
+
if (Object.keys(xumSection).length === 0) return base;
|
|
86649
|
+
const rulesyncFrontmatter = {
|
|
86650
|
+
...base.getFrontmatter(),
|
|
86651
|
+
xum: xumSection
|
|
86652
|
+
};
|
|
86653
|
+
return new RulesyncSkill({
|
|
86654
|
+
outputRoot: base.getOutputRoot(),
|
|
86655
|
+
relativeDirPath: base.getRelativeDirPath(),
|
|
86656
|
+
dirName: base.getDirName(),
|
|
86657
|
+
frontmatter: rulesyncFrontmatter,
|
|
86658
|
+
body: base.getBody(),
|
|
86659
|
+
otherFiles: base.getOtherFiles(),
|
|
86660
|
+
validate: true,
|
|
86661
|
+
global: this.global
|
|
86662
|
+
});
|
|
86663
|
+
}
|
|
86664
|
+
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false, logger }) {
|
|
86665
|
+
const settablePaths = XumSkill.getSettablePaths({ global });
|
|
86666
|
+
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
86667
|
+
const dirName = rulesyncSkill.getDirName();
|
|
86668
|
+
const xumSection = {};
|
|
86669
|
+
const ignoredKeys = [];
|
|
86670
|
+
for (const [key, value] of Object.entries(rulesyncFrontmatter.xum ?? {})) if (AGENT_SKILLS_STANDARD_KEYS.has(key)) ignoredKeys.push(key);
|
|
86671
|
+
else xumSection[key] = value;
|
|
86672
|
+
if (ignoredKeys.length > 0) warnWithFallback(logger, `${join(outputRoot, settablePaths.relativeDirPath, dirName, SKILL_FILE_NAME)}: the xum section sets ${ignoredKeys.map(quoteValueForWarning).join(", ")}, which ${ignoredKeys.length === 1 ? "is" : "are"} ignored there; set name/description at the root and the Agent Skills standard fields at the root or in the agentsskills section.`);
|
|
86673
|
+
const userInvocable = resolveUserInvocable({
|
|
86674
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
86675
|
+
section: xumSection
|
|
86676
|
+
});
|
|
86677
|
+
const disableModelInvocation = resolveDisableModelInvocation({
|
|
86678
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
86679
|
+
section: xumSection
|
|
86680
|
+
});
|
|
86681
|
+
const frontmatter = {
|
|
86682
|
+
name: rulesyncFrontmatter.name,
|
|
86683
|
+
description: rulesyncFrontmatter.description,
|
|
86684
|
+
...toSpecConformantAgentSkillFields(resolveAgentsSkillsSection(rulesyncFrontmatter)),
|
|
86685
|
+
...xumSection,
|
|
86686
|
+
...userInvocable !== void 0 && { "user-invocable": userInvocable },
|
|
86687
|
+
...disableModelInvocation !== void 0 && { "disable-model-invocation": disableModelInvocation }
|
|
86688
|
+
};
|
|
86689
|
+
AgentsSkillsSkill.reportSpecViolations({
|
|
86690
|
+
outputRoot,
|
|
86691
|
+
relativeDirPath: settablePaths.relativeDirPath,
|
|
86692
|
+
dirName,
|
|
86693
|
+
frontmatter,
|
|
86694
|
+
sourceAllowedTools: rulesyncFrontmatter.agentsskills?.["allowed-tools"],
|
|
86695
|
+
logger
|
|
86696
|
+
});
|
|
86697
|
+
return new XumSkill({
|
|
86698
|
+
outputRoot,
|
|
86699
|
+
relativeDirPath: settablePaths.relativeDirPath,
|
|
86700
|
+
dirName,
|
|
86701
|
+
frontmatter,
|
|
86702
|
+
body: rulesyncSkill.getBody(),
|
|
86703
|
+
otherFiles: rulesyncSkill.getOtherFiles(),
|
|
86704
|
+
validate,
|
|
86705
|
+
global
|
|
86706
|
+
});
|
|
86707
|
+
}
|
|
86708
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
86709
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
86710
|
+
return targets.includes("*") || targets.includes("xum");
|
|
86711
|
+
}
|
|
86712
|
+
};
|
|
86713
|
+
//#endregion
|
|
84763
86714
|
//#region src/features/skills/zcode-skill.ts
|
|
84764
86715
|
const ZcodeSkillFrontmatterSchema = z.looseObject({
|
|
84765
86716
|
name: z.string(),
|
|
@@ -85175,6 +87126,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
85175
87126
|
supportsGlobal: false
|
|
85176
87127
|
}
|
|
85177
87128
|
}],
|
|
87129
|
+
["androidstudio", {
|
|
87130
|
+
class: AndroidstudioSkill,
|
|
87131
|
+
meta: {
|
|
87132
|
+
supportsProject: true,
|
|
87133
|
+
supportsSimulated: false,
|
|
87134
|
+
supportsGlobal: true
|
|
87135
|
+
}
|
|
87136
|
+
}],
|
|
85178
87137
|
["amp", {
|
|
85179
87138
|
class: AmpSkill,
|
|
85180
87139
|
meta: {
|
|
@@ -85360,6 +87319,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
85360
87319
|
supportsGlobal: true
|
|
85361
87320
|
}
|
|
85362
87321
|
}],
|
|
87322
|
+
["firebender", {
|
|
87323
|
+
class: FirebenderSkill,
|
|
87324
|
+
meta: {
|
|
87325
|
+
supportsProject: true,
|
|
87326
|
+
supportsSimulated: false,
|
|
87327
|
+
supportsGlobal: true
|
|
87328
|
+
}
|
|
87329
|
+
}],
|
|
85363
87330
|
["goose", {
|
|
85364
87331
|
class: GooseSkill,
|
|
85365
87332
|
meta: {
|
|
@@ -85610,6 +87577,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
85610
87577
|
supportsGlobal: true
|
|
85611
87578
|
}
|
|
85612
87579
|
}],
|
|
87580
|
+
["xum", {
|
|
87581
|
+
class: XumSkill,
|
|
87582
|
+
meta: {
|
|
87583
|
+
supportsProject: true,
|
|
87584
|
+
supportsSimulated: false,
|
|
87585
|
+
supportsGlobal: true
|
|
87586
|
+
}
|
|
87587
|
+
}],
|
|
85613
87588
|
["devin", {
|
|
85614
87589
|
class: DevinSkill,
|
|
85615
87590
|
meta: {
|
|
@@ -89456,6 +91431,338 @@ var FactorydroidSubagent = class FactorydroidSubagent extends ToolSubagent {
|
|
|
89456
91431
|
}
|
|
89457
91432
|
};
|
|
89458
91433
|
//#endregion
|
|
91434
|
+
//#region src/features/subagents/firebender-subagent.ts
|
|
91435
|
+
/**
|
|
91436
|
+
* Frontmatter of a Firebender agent file. `tools` is a comma-separated string
|
|
91437
|
+
* of tool groups (`read`, `edit`, `execution`, `other`) and MCP tools
|
|
91438
|
+
* (`mcp_<server>_<tool>`); `model` is `small`, `medium`, `large`, `inherit` or a
|
|
91439
|
+
* model ID; `callable` lets the main agent delegate to it. Unknown keys are
|
|
91440
|
+
* preserved.
|
|
91441
|
+
*
|
|
91442
|
+
* @see https://docs.firebender.com/api-reference/agents
|
|
91443
|
+
*/
|
|
91444
|
+
const FirebenderSubagentFrontmatterSchema = z.looseObject({
|
|
91445
|
+
name: z.optional(z.string()),
|
|
91446
|
+
description: z.optional(z.string()),
|
|
91447
|
+
color: z.optional(z.string()),
|
|
91448
|
+
icon: z.optional(z.string()),
|
|
91449
|
+
tools: z.optional(z.string()),
|
|
91450
|
+
model: z.optional(z.string()),
|
|
91451
|
+
callable: z.optional(z.boolean())
|
|
91452
|
+
});
|
|
91453
|
+
/**
|
|
91454
|
+
* The `agents` entry Firebender resolves to an agent file rulesync writes:
|
|
91455
|
+
* relative to the project root for the project config, and to `~/.firebender`
|
|
91456
|
+
* for the personal one.
|
|
91457
|
+
*/
|
|
91458
|
+
function toAgentsEntry({ relativeFilePath, global }) {
|
|
91459
|
+
const fileName = toPosixPath(relativeFilePath);
|
|
91460
|
+
return global ? posix.join(FIREBENDER_AGENTS_DIR_NAME, fileName) : posix.join(toPosixPath(FIREBENDER_AGENTS_DIR_PATH), fileName);
|
|
91461
|
+
}
|
|
91462
|
+
/**
|
|
91463
|
+
* The agent file name an existing `agents` entry points at when it lies
|
|
91464
|
+
* directly inside the directory rulesync writes agents to
|
|
91465
|
+
* (`.firebender/agents/` in the project; `agents/` or `~/.firebender/agents/`
|
|
91466
|
+
* in the personal config), or `undefined` for any other entry
|
|
91467
|
+
* (`~/firebender-agents/...`, an absolute path, a nested path).
|
|
91468
|
+
*/
|
|
91469
|
+
function managedAgentFileName({ entry, global }) {
|
|
91470
|
+
const normalized = toPosixPath(entry).replace(/^\.\//, "");
|
|
91471
|
+
const prefix = global ? `${FIREBENDER_AGENTS_DIR_NAME}/` : `${toPosixPath(FIREBENDER_AGENTS_DIR_PATH)}/`;
|
|
91472
|
+
const homePrefix = `~/${toPosixPath(FIREBENDER_AGENTS_DIR_PATH)}/`;
|
|
91473
|
+
const rest = normalized.startsWith(prefix) ? normalized.slice(prefix.length) : global && normalized.startsWith(homePrefix) ? normalized.slice(homePrefix.length) : void 0;
|
|
91474
|
+
return rest !== void 0 && rest.endsWith(".md") && !rest.includes("/") ? rest : void 0;
|
|
91475
|
+
}
|
|
91476
|
+
/**
|
|
91477
|
+
* Subagent generator for Firebender.
|
|
91478
|
+
*
|
|
91479
|
+
* Each agent is a Markdown file with YAML frontmatter, written to
|
|
91480
|
+
* `.firebender/agents/<name>.md` (project) or `~/.firebender/agents/<name>.md`
|
|
91481
|
+
* (global). Firebender only loads agents listed in the `agents` array of
|
|
91482
|
+
* `firebender.json`, so generation also registers every file there — see
|
|
91483
|
+
* {@link FirebenderAgentsRegistry}. Rulesync subagents are meant to be
|
|
91484
|
+
* delegated to, so `callable: true` is written unless the `firebender:`
|
|
91485
|
+
* section says otherwise.
|
|
91486
|
+
*
|
|
91487
|
+
* @see https://docs.firebender.com/api-reference/agents
|
|
91488
|
+
*/
|
|
91489
|
+
var FirebenderSubagent = class FirebenderSubagent extends ToolSubagent {
|
|
91490
|
+
frontmatter;
|
|
91491
|
+
body;
|
|
91492
|
+
constructor({ frontmatter, body, ...rest }) {
|
|
91493
|
+
if (rest.validate !== false) {
|
|
91494
|
+
const result = FirebenderSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
91495
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
91496
|
+
}
|
|
91497
|
+
super({ ...rest });
|
|
91498
|
+
this.frontmatter = frontmatter;
|
|
91499
|
+
this.body = body;
|
|
91500
|
+
}
|
|
91501
|
+
static getSettablePaths(_options = {}) {
|
|
91502
|
+
return { relativeDirPath: FIREBENDER_AGENTS_DIR_PATH };
|
|
91503
|
+
}
|
|
91504
|
+
/**
|
|
91505
|
+
* Generation also read-modify-writes the `agents` key of `firebender.json`,
|
|
91506
|
+
* so the write is declared for the shared-file order derivation.
|
|
91507
|
+
*/
|
|
91508
|
+
static getExtraSharedWritePaths({ global = false } = {}) {
|
|
91509
|
+
return [{
|
|
91510
|
+
relativeDirPath: getFirebenderConfigDir({ global }),
|
|
91511
|
+
relativeFilePath: FIREBENDER_CONFIG_FILE_NAME
|
|
91512
|
+
}];
|
|
91513
|
+
}
|
|
91514
|
+
getFrontmatter() {
|
|
91515
|
+
return this.frontmatter;
|
|
91516
|
+
}
|
|
91517
|
+
getBody() {
|
|
91518
|
+
return this.body;
|
|
91519
|
+
}
|
|
91520
|
+
toRulesyncSubagent() {
|
|
91521
|
+
const { name, description, callable, ...rest } = this.frontmatter;
|
|
91522
|
+
const firebenderSection = {
|
|
91523
|
+
...rest,
|
|
91524
|
+
...callable !== true && { callable: callable ?? false }
|
|
91525
|
+
};
|
|
91526
|
+
return new RulesyncSubagent({
|
|
91527
|
+
outputRoot: ".",
|
|
91528
|
+
frontmatter: {
|
|
91529
|
+
targets: ["*"],
|
|
91530
|
+
name: name ?? basename(this.getRelativeFilePath(), ".md"),
|
|
91531
|
+
description,
|
|
91532
|
+
...Object.keys(firebenderSection).length > 0 && { firebender: firebenderSection }
|
|
91533
|
+
},
|
|
91534
|
+
body: this.body,
|
|
91535
|
+
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
|
|
91536
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
91537
|
+
validate: true
|
|
91538
|
+
});
|
|
91539
|
+
}
|
|
91540
|
+
/**
|
|
91541
|
+
* Every targeted subagent becomes its own agent file, plus one
|
|
91542
|
+
* {@link FirebenderAgentsRegistry} that lists them in `firebender.json`.
|
|
91543
|
+
* The registry is emitted even when no subagent is targeted, so agents a
|
|
91544
|
+
* previous run registered are retracted once their files are gone. The
|
|
91545
|
+
* agent directory is listed up front so the registry can tell a registered
|
|
91546
|
+
* file that still exists (kept, whoever wrote it) from a stale one.
|
|
91547
|
+
*/
|
|
91548
|
+
static async fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, global = false, logger, deleteOrphans = false }) {
|
|
91549
|
+
const agentsDirPath = this.getSettablePaths({ global }).relativeDirPath;
|
|
91550
|
+
const agentsDirEscapesRoot = await writablePathEscapesRoot({
|
|
91551
|
+
rootPath: outputRoot,
|
|
91552
|
+
targetPath: join(outputRoot, agentsDirPath, "agent.md")
|
|
91553
|
+
});
|
|
91554
|
+
const existingAgentFileNames = new Set(agentsDirEscapesRoot ? [] : await listDirectoryEntryNames(join(outputRoot, agentsDirPath)));
|
|
91555
|
+
const subagents = rulesyncSubagents.map((rulesyncSubagent) => this.fromRulesyncSubagent({
|
|
91556
|
+
outputRoot,
|
|
91557
|
+
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
91558
|
+
rulesyncSubagent,
|
|
91559
|
+
global
|
|
91560
|
+
}));
|
|
91561
|
+
const registry = new FirebenderAgentsRegistry({
|
|
91562
|
+
outputRoot,
|
|
91563
|
+
relativeDirPath: getFirebenderConfigDir({ global }),
|
|
91564
|
+
relativeFilePath: FIREBENDER_CONFIG_FILE_NAME,
|
|
91565
|
+
entries: subagents.map((subagent) => toAgentsEntry({
|
|
91566
|
+
relativeFilePath: subagent.getRelativeFilePath(),
|
|
91567
|
+
global
|
|
91568
|
+
})),
|
|
91569
|
+
existingAgentFileNames,
|
|
91570
|
+
deleteOrphans,
|
|
91571
|
+
frozen: agentsDirEscapesRoot,
|
|
91572
|
+
global,
|
|
91573
|
+
logger
|
|
91574
|
+
});
|
|
91575
|
+
return [...subagents, registry];
|
|
91576
|
+
}
|
|
91577
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
|
|
91578
|
+
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
91579
|
+
const firebenderSection = rulesyncFrontmatter.firebender ?? {};
|
|
91580
|
+
const frontmatter = {
|
|
91581
|
+
name: rulesyncFrontmatter.name,
|
|
91582
|
+
...rulesyncFrontmatter.description !== void 0 && { description: rulesyncFrontmatter.description },
|
|
91583
|
+
callable: true,
|
|
91584
|
+
...firebenderSection
|
|
91585
|
+
};
|
|
91586
|
+
const body = rulesyncSubagent.getBody();
|
|
91587
|
+
return new FirebenderSubagent({
|
|
91588
|
+
outputRoot,
|
|
91589
|
+
frontmatter,
|
|
91590
|
+
body,
|
|
91591
|
+
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
91592
|
+
relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
|
|
91593
|
+
fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true }),
|
|
91594
|
+
validate,
|
|
91595
|
+
global
|
|
91596
|
+
});
|
|
91597
|
+
}
|
|
91598
|
+
validate() {
|
|
91599
|
+
if (!this.frontmatter) return {
|
|
91600
|
+
success: true,
|
|
91601
|
+
error: null
|
|
91602
|
+
};
|
|
91603
|
+
const result = FirebenderSubagentFrontmatterSchema.safeParse(this.frontmatter);
|
|
91604
|
+
if (result.success) return {
|
|
91605
|
+
success: true,
|
|
91606
|
+
error: null
|
|
91607
|
+
};
|
|
91608
|
+
return {
|
|
91609
|
+
success: false,
|
|
91610
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
91611
|
+
};
|
|
91612
|
+
}
|
|
91613
|
+
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
91614
|
+
return this.isTargetedByRulesyncSubagentDefault({
|
|
91615
|
+
rulesyncSubagent,
|
|
91616
|
+
toolTarget: "firebender"
|
|
91617
|
+
});
|
|
91618
|
+
}
|
|
91619
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
91620
|
+
const paths = this.getSettablePaths({ global });
|
|
91621
|
+
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
91622
|
+
const fileContent = await readFileContent(filePath);
|
|
91623
|
+
const { frontmatter, body } = parseFrontmatter(fileContent, filePath);
|
|
91624
|
+
const result = FirebenderSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
91625
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
91626
|
+
return new FirebenderSubagent({
|
|
91627
|
+
outputRoot,
|
|
91628
|
+
relativeDirPath: paths.relativeDirPath,
|
|
91629
|
+
relativeFilePath,
|
|
91630
|
+
frontmatter: result.data,
|
|
91631
|
+
body: body.trim(),
|
|
91632
|
+
fileContent,
|
|
91633
|
+
validate,
|
|
91634
|
+
global
|
|
91635
|
+
});
|
|
91636
|
+
}
|
|
91637
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
91638
|
+
return new FirebenderSubagent({
|
|
91639
|
+
outputRoot,
|
|
91640
|
+
relativeDirPath,
|
|
91641
|
+
relativeFilePath,
|
|
91642
|
+
frontmatter: {},
|
|
91643
|
+
body: "",
|
|
91644
|
+
fileContent: "",
|
|
91645
|
+
validate: false
|
|
91646
|
+
});
|
|
91647
|
+
}
|
|
91648
|
+
};
|
|
91649
|
+
/**
|
|
91650
|
+
* The `agents` array of `firebender.json`, which Firebender requires to load
|
|
91651
|
+
* an agent file. Every generated agent is registered. An existing entry that
|
|
91652
|
+
* points into rulesync's agent directory is kept while its file exists (a
|
|
91653
|
+
* hand-written agent there stays registered) and dropped, with a warning, once
|
|
91654
|
+
* the file is gone. Under `--delete` the orphan sweep removes every agent file
|
|
91655
|
+
* there that this run does not generate, but only after this file is written,
|
|
91656
|
+
* so in that mode such entries are dropped here too and a single run
|
|
91657
|
+
* converges. Every other entry and every other key of the file is kept.
|
|
91658
|
+
* The array is merged into the file on disk at write time and the file itself
|
|
91659
|
+
* is never deleted.
|
|
91660
|
+
*/
|
|
91661
|
+
var FirebenderAgentsRegistry = class extends ToolSubagent {
|
|
91662
|
+
entries;
|
|
91663
|
+
existingAgentFileNames;
|
|
91664
|
+
deleteOrphans;
|
|
91665
|
+
frozen;
|
|
91666
|
+
logger;
|
|
91667
|
+
mergedContent = null;
|
|
91668
|
+
constructor({ entries, existingAgentFileNames = /* @__PURE__ */ new Set(), deleteOrphans = false, frozen = false, logger, ...rest }) {
|
|
91669
|
+
super({
|
|
91670
|
+
...rest,
|
|
91671
|
+
fileContent: ""
|
|
91672
|
+
});
|
|
91673
|
+
this.entries = entries;
|
|
91674
|
+
this.existingAgentFileNames = existingAgentFileNames;
|
|
91675
|
+
this.deleteOrphans = deleteOrphans;
|
|
91676
|
+
this.frozen = frozen;
|
|
91677
|
+
this.logger = logger;
|
|
91678
|
+
}
|
|
91679
|
+
getEntries() {
|
|
91680
|
+
return this.entries;
|
|
91681
|
+
}
|
|
91682
|
+
isDeletable() {
|
|
91683
|
+
return false;
|
|
91684
|
+
}
|
|
91685
|
+
shouldMergeExistingFileContent() {
|
|
91686
|
+
return true;
|
|
91687
|
+
}
|
|
91688
|
+
setFileContent(newFileContent) {
|
|
91689
|
+
this.mergedContent = this.mergeInto(newFileContent);
|
|
91690
|
+
}
|
|
91691
|
+
getFileContent() {
|
|
91692
|
+
if (this.mergedContent === null) this.mergedContent = this.mergeInto("");
|
|
91693
|
+
return this.mergedContent;
|
|
91694
|
+
}
|
|
91695
|
+
mergeInto(existingContent) {
|
|
91696
|
+
const paths = {
|
|
91697
|
+
relativeDirPath: this.getRelativeDirPath(),
|
|
91698
|
+
relativeFilePath: this.getRelativeFilePath()
|
|
91699
|
+
};
|
|
91700
|
+
const filePath = join(this.getOutputRoot(), paths.relativeDirPath, paths.relativeFilePath);
|
|
91701
|
+
const existing = parseSharedConfig({
|
|
91702
|
+
format: "json",
|
|
91703
|
+
fileContent: existingContent,
|
|
91704
|
+
filePath,
|
|
91705
|
+
invalidRootPolicy: "error"
|
|
91706
|
+
});
|
|
91707
|
+
if (this.frozen) return applySharedConfigPatch({
|
|
91708
|
+
fileKey: sharedConfigFileKey(paths),
|
|
91709
|
+
feature: "subagents",
|
|
91710
|
+
existingContent,
|
|
91711
|
+
patch: { agents: existing.agents },
|
|
91712
|
+
filePath,
|
|
91713
|
+
logger: this.logger
|
|
91714
|
+
});
|
|
91715
|
+
const existingEntries = Array.isArray(existing.agents) ? existing.agents : [];
|
|
91716
|
+
const generatedByFileName = new Map(this.entries.map((entry) => [managedAgentFileName({
|
|
91717
|
+
entry,
|
|
91718
|
+
global: this.global
|
|
91719
|
+
}) ?? entry, entry]));
|
|
91720
|
+
const agents = [];
|
|
91721
|
+
const placed = /* @__PURE__ */ new Set();
|
|
91722
|
+
const dropped = [];
|
|
91723
|
+
for (const entry of existingEntries) {
|
|
91724
|
+
const fileName = typeof entry === "string" ? managedAgentFileName({
|
|
91725
|
+
entry,
|
|
91726
|
+
global: this.global
|
|
91727
|
+
}) : void 0;
|
|
91728
|
+
if (fileName === void 0) {
|
|
91729
|
+
agents.push(entry);
|
|
91730
|
+
continue;
|
|
91731
|
+
}
|
|
91732
|
+
const generated = generatedByFileName.get(fileName);
|
|
91733
|
+
if (generated !== void 0) {
|
|
91734
|
+
if (!placed.has(fileName)) {
|
|
91735
|
+
agents.push(generated);
|
|
91736
|
+
placed.add(fileName);
|
|
91737
|
+
}
|
|
91738
|
+
} else if (!this.deleteOrphans && this.existingAgentFileNames.has(fileName)) agents.push(entry);
|
|
91739
|
+
else dropped.push(String(entry));
|
|
91740
|
+
}
|
|
91741
|
+
for (const [fileName, entry] of generatedByFileName) if (!placed.has(fileName)) {
|
|
91742
|
+
agents.push(entry);
|
|
91743
|
+
placed.add(fileName);
|
|
91744
|
+
}
|
|
91745
|
+
if (dropped.length > 0) this.logger?.warn(`Unregistered Firebender agent(s) ${dropped.map((entry) => quoteValueForWarning(entry)).join(", ")} from ${filePath}: ` + (this.deleteOrphans ? "--delete removes agent files that .rulesync/subagents/ no longer generates." : "the agent file no longer exists."));
|
|
91746
|
+
return applySharedConfigPatch({
|
|
91747
|
+
fileKey: sharedConfigFileKey(paths),
|
|
91748
|
+
feature: "subagents",
|
|
91749
|
+
existingContent,
|
|
91750
|
+
patch: { agents: agents.length > 0 ? agents : void 0 },
|
|
91751
|
+
filePath,
|
|
91752
|
+
logger: this.logger
|
|
91753
|
+
});
|
|
91754
|
+
}
|
|
91755
|
+
toRulesyncSubagent() {
|
|
91756
|
+
throw new Error(`${this.getRelativePathFromCwd()} registers Firebender agents and is not a subagent itself.`);
|
|
91757
|
+
}
|
|
91758
|
+
validate() {
|
|
91759
|
+
return {
|
|
91760
|
+
success: true,
|
|
91761
|
+
error: null
|
|
91762
|
+
};
|
|
91763
|
+
}
|
|
91764
|
+
};
|
|
91765
|
+
//#endregion
|
|
89459
91766
|
//#region src/features/subagents/goose-subagent.ts
|
|
89460
91767
|
/**
|
|
89461
91768
|
* Goose custom agents (v1.34.0+) are Markdown files with YAML frontmatter —
|
|
@@ -89743,13 +92050,13 @@ function subagentSlug(relativeFilePath) {
|
|
|
89743
92050
|
return basename(relativeFilePath, ".md").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
89744
92051
|
}
|
|
89745
92052
|
function hermesCommandName(slug) {
|
|
89746
|
-
return `
|
|
92053
|
+
return hermesSlashName(`rulesync-subagent-${slug}`);
|
|
89747
92054
|
}
|
|
89748
92055
|
function getPluginManifestContent() {
|
|
89749
92056
|
return [
|
|
89750
92057
|
"name: rulesync-subagents",
|
|
89751
92058
|
"version: \"1.0.0\"",
|
|
89752
|
-
"description: Exposes RuleSync subagents as Hermes
|
|
92059
|
+
"description: Exposes RuleSync subagents as Hermes slash commands that queue a delegation prompt.",
|
|
89753
92060
|
""
|
|
89754
92061
|
].join("\n");
|
|
89755
92062
|
}
|
|
@@ -89757,9 +92064,12 @@ function getPluginInitContent() {
|
|
|
89757
92064
|
return `"""RuleSync-generated Hermes subagent commands."""
|
|
89758
92065
|
|
|
89759
92066
|
import json
|
|
92067
|
+
import logging
|
|
92068
|
+
import re
|
|
89760
92069
|
from pathlib import Path
|
|
89761
92070
|
|
|
89762
92071
|
|
|
92072
|
+
logger = logging.getLogger(__name__)
|
|
89763
92073
|
SUBAGENTS_DIR = Path(__file__).resolve().parents[2] / "rulesync" / "subagents"
|
|
89764
92074
|
|
|
89765
92075
|
|
|
@@ -89779,52 +92089,71 @@ def _load_subagents():
|
|
|
89779
92089
|
return subagents
|
|
89780
92090
|
|
|
89781
92091
|
|
|
89782
|
-
def
|
|
92092
|
+
def _command_name(subagent, slug):
|
|
92093
|
+
# The spec carries the name Rulesync normalized; Hermes' messaging gateway
|
|
92094
|
+
# resolves plugin commands with "_" turned into "-", so keep it hyphenated.
|
|
92095
|
+
hermes = subagent.get("hermes")
|
|
92096
|
+
name = hermes.get("command") if isinstance(hermes, dict) else None
|
|
92097
|
+
name = name or f"rulesync-subagent-{slug}"
|
|
92098
|
+
# Mirrors hermesSlashName() in src/features/shared/hermes-plugin-commands.ts.
|
|
92099
|
+
name = re.sub(r"[ _]", "-", str(name).lower())
|
|
92100
|
+
name = re.sub(r"[^a-z0-9-]", "", name)
|
|
92101
|
+
return re.sub(r"-{2,}", "-", name).strip("-")
|
|
92102
|
+
|
|
92103
|
+
|
|
92104
|
+
${getHermesQueueUserMessagePython({ pluginName: "rulesync-subagents" })}
|
|
92105
|
+
|
|
92106
|
+
def _register_subagent(ctx, subagent, registered):
|
|
89783
92107
|
slug = subagent.get("slug")
|
|
89784
92108
|
if not slug:
|
|
89785
92109
|
return
|
|
89786
92110
|
|
|
89787
|
-
command_name =
|
|
92111
|
+
command_name = _command_name(subagent, slug)
|
|
92112
|
+
if command_name in registered:
|
|
92113
|
+
logger.warning(
|
|
92114
|
+
"rulesync-subagents: %s and %s both map to /%s; keeping the first.",
|
|
92115
|
+
registered[command_name],
|
|
92116
|
+
subagent.get("_path"),
|
|
92117
|
+
command_name,
|
|
92118
|
+
)
|
|
92119
|
+
return
|
|
92120
|
+
registered[command_name] = subagent.get("_path")
|
|
92121
|
+
|
|
89788
92122
|
name = subagent.get("name") or slug
|
|
89789
92123
|
description = subagent.get("description") or f"Delegate work to the {name} RuleSync subagent."
|
|
89790
92124
|
system_prompt = subagent.get("prompt") or ""
|
|
89791
92125
|
|
|
89792
92126
|
def handler(args=None, **kwargs):
|
|
89793
|
-
del kwargs
|
|
89794
92127
|
user_context = ""
|
|
89795
92128
|
if isinstance(args, dict):
|
|
89796
92129
|
user_context = args.get("context") or args.get("task") or args.get("prompt") or ""
|
|
89797
92130
|
elif args is not None:
|
|
89798
92131
|
user_context = str(args)
|
|
89799
92132
|
|
|
89800
|
-
|
|
92133
|
+
# A slash command runs outside any agent turn, so calling delegate_task
|
|
92134
|
+
# here fails with "delegate_task requires a parent agent context".
|
|
92135
|
+
# Queue the brief as a user message instead; the agent then delegates.
|
|
92136
|
+
parts = [
|
|
92137
|
+
f"Use the delegate_task tool to hand the following task to a subagent "
|
|
92138
|
+
f"acting as the {name} RuleSync subagent.",
|
|
92139
|
+
f"Goal: {description}",
|
|
92140
|
+
]
|
|
89801
92141
|
if system_prompt:
|
|
89802
|
-
|
|
92142
|
+
parts.append(f"Subagent instructions:\\n{system_prompt}")
|
|
89803
92143
|
if user_context:
|
|
89804
|
-
|
|
92144
|
+
parts.append(f"Task details:\\n{user_context}")
|
|
89805
92145
|
|
|
89806
|
-
|
|
89807
|
-
|
|
89808
|
-
|
|
89809
|
-
# advertises; a top-level "goal" is an unadvertised legacy path.
|
|
89810
|
-
return ctx.dispatch_tool(
|
|
89811
|
-
"delegate_task",
|
|
89812
|
-
{
|
|
89813
|
-
"tasks": [
|
|
89814
|
-
{
|
|
89815
|
-
"goal": description,
|
|
89816
|
-
"context": "\\n\\n".join(context_parts),
|
|
89817
|
-
},
|
|
89818
|
-
],
|
|
89819
|
-
},
|
|
89820
|
-
)
|
|
92146
|
+
label = f"the {name} subagent task"
|
|
92147
|
+
error = _queue_user_message(ctx, "\\n\\n".join(parts), kwargs, label)
|
|
92148
|
+
return error or f"Queued {label}; the agent will delegate it."
|
|
89821
92149
|
|
|
89822
92150
|
ctx.register_command(command_name, handler, description)
|
|
89823
92151
|
|
|
89824
92152
|
|
|
89825
92153
|
def register(ctx):
|
|
92154
|
+
registered = {}
|
|
89826
92155
|
for subagent in _load_subagents():
|
|
89827
|
-
_register_subagent(ctx, subagent)
|
|
92156
|
+
_register_subagent(ctx, subagent, registered)
|
|
89828
92157
|
`;
|
|
89829
92158
|
}
|
|
89830
92159
|
function getEnabledPluginConfigContent({ currentContent, global }) {
|
|
@@ -89855,7 +92184,7 @@ function getSubagentSpec(rulesyncSubagent) {
|
|
|
89855
92184
|
prompt: rulesyncSubagent.getBody(),
|
|
89856
92185
|
hermes: {
|
|
89857
92186
|
command: hermesCommandName(slug),
|
|
89858
|
-
dispatch: "
|
|
92187
|
+
dispatch: "inject_message"
|
|
89859
92188
|
}
|
|
89860
92189
|
};
|
|
89861
92190
|
}
|
|
@@ -90531,6 +92860,15 @@ const KiroCliSubagentJsonSchema = z.looseObject({
|
|
|
90531
92860
|
allowedTools: z.optional(z.nullable(z.array(z.string()))),
|
|
90532
92861
|
includeMcpJson: z.optional(z.nullable(z.boolean()))
|
|
90533
92862
|
});
|
|
92863
|
+
/**
|
|
92864
|
+
* The name Kiro gives an agent file: its path relative to the agents directory
|
|
92865
|
+
* without the extension, so a nested `team/planner.md` is `team/planner`.
|
|
92866
|
+
* https://kiro.dev/docs/custom-agents/
|
|
92867
|
+
*/
|
|
92868
|
+
function kiroAgentNameFromPath(relativeFilePath) {
|
|
92869
|
+
const posixPath = toPosixPath(relativeFilePath);
|
|
92870
|
+
return posixPath.slice(0, posixPath.length - extname(posixPath).length);
|
|
92871
|
+
}
|
|
90534
92872
|
var KiroSubagent = class extends ToolSubagent {
|
|
90535
92873
|
body;
|
|
90536
92874
|
constructor({ body, ...rest }) {
|
|
@@ -90566,7 +92904,7 @@ var KiroSubagent = class extends ToolSubagent {
|
|
|
90566
92904
|
outputRoot: ".",
|
|
90567
92905
|
frontmatter: {
|
|
90568
92906
|
targets: [toolTarget],
|
|
90569
|
-
name: name ??
|
|
92907
|
+
name: name ?? kiroAgentNameFromPath(this.getRelativeFilePath()),
|
|
90570
92908
|
description: description ?? void 0,
|
|
90571
92909
|
...Object.keys(kiroSection).length > 0 && { kiro: kiroSection }
|
|
90572
92910
|
},
|
|
@@ -90716,7 +93054,7 @@ var KiroIdeSubagent = class KiroIdeSubagent extends ToolSubagent {
|
|
|
90716
93054
|
outputRoot: ".",
|
|
90717
93055
|
frontmatter: {
|
|
90718
93056
|
targets: ["*"],
|
|
90719
|
-
name: name ?? this.getRelativeFilePath()
|
|
93057
|
+
name: name ?? kiroAgentNameFromPath(this.getRelativeFilePath()),
|
|
90720
93058
|
description: description ?? "",
|
|
90721
93059
|
...Object.keys(kiroIdeSection).length > 0 && { "kiro-ide": kiroIdeSection }
|
|
90722
93060
|
},
|
|
@@ -92647,6 +94985,166 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
|
|
|
92647
94985
|
}
|
|
92648
94986
|
};
|
|
92649
94987
|
//#endregion
|
|
94988
|
+
//#region src/features/subagents/xum-subagent.ts
|
|
94989
|
+
/**
|
|
94990
|
+
* The built-in agent a rulesync subagent inherits from when its `xum` block
|
|
94991
|
+
* names neither a `base` nor its own `tools`. Xum gives an agent no tools at
|
|
94992
|
+
* all unless some agent in its `base` chain declares them, so a definition
|
|
94993
|
+
* without either would be a prompt that cannot read or edit anything.
|
|
94994
|
+
*/
|
|
94995
|
+
const XUM_DEFAULT_BASE_AGENT = "exec";
|
|
94996
|
+
/**
|
|
94997
|
+
* Frontmatter of a Xum agent definition. `name` (the display name) is
|
|
94998
|
+
* required; the other documented fields are typed loosely and unknown keys are
|
|
94999
|
+
* kept (looseObject) so a round trip never drops them.
|
|
95000
|
+
*
|
|
95001
|
+
* @see https://xum.cdr.dev/agents
|
|
95002
|
+
*/
|
|
95003
|
+
const XumSubagentFrontmatterSchema = z.looseObject({
|
|
95004
|
+
name: z.string(),
|
|
95005
|
+
description: z.optional(z.string()),
|
|
95006
|
+
base: z.optional(z.string()),
|
|
95007
|
+
disabled: z.optional(z.boolean()),
|
|
95008
|
+
ui: z.optional(z.looseObject({})),
|
|
95009
|
+
prompt: z.optional(z.looseObject({})),
|
|
95010
|
+
subagent: z.optional(z.looseObject({})),
|
|
95011
|
+
ai: z.optional(z.looseObject({})),
|
|
95012
|
+
tools: z.optional(z.looseObject({}))
|
|
95013
|
+
});
|
|
95014
|
+
/**
|
|
95015
|
+
* Subagent generator for Xum (formerly Mux).
|
|
95016
|
+
*
|
|
95017
|
+
* Xum reads custom agent definitions — Markdown with YAML frontmatter — from
|
|
95018
|
+
* the top level of `.xum/agents/` (project) and `~/.xum/agents/` (user); the
|
|
95019
|
+
* body is the agent's instruction prompt. The file name (without `.md`) is the
|
|
95020
|
+
* agent id, so it must be lowercase letters and digits with single `-` / `_`
|
|
95021
|
+
* separators; Xum skips other files with a warning.
|
|
95022
|
+
*
|
|
95023
|
+
* rulesync writes `name` and `description`, spreads the `xum` section (`base`,
|
|
95024
|
+
* `ui`, `prompt`, `subagent`, `ai`, `tools`, ...) on top, and fills two
|
|
95025
|
+
* defaults so the definition works as a delegated subagent out of the box:
|
|
95026
|
+
* `subagent.runnable: true` (required for `task({ agentId })` to spawn it) and,
|
|
95027
|
+
* when the section sets neither `base` nor `tools`, `base: exec`.
|
|
95028
|
+
*
|
|
95029
|
+
* @see https://xum.cdr.dev/agents
|
|
95030
|
+
*/
|
|
95031
|
+
var XumSubagent = class XumSubagent extends ToolSubagent {
|
|
95032
|
+
frontmatter;
|
|
95033
|
+
body;
|
|
95034
|
+
constructor({ frontmatter, body, ...rest }) {
|
|
95035
|
+
if (rest.validate !== false) {
|
|
95036
|
+
const result = XumSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
95037
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
95038
|
+
}
|
|
95039
|
+
super({ ...rest });
|
|
95040
|
+
this.frontmatter = frontmatter;
|
|
95041
|
+
this.body = body;
|
|
95042
|
+
}
|
|
95043
|
+
static getSettablePaths(_options = {}) {
|
|
95044
|
+
return { relativeDirPath: XUM_AGENTS_DIR_PATH };
|
|
95045
|
+
}
|
|
95046
|
+
getFrontmatter() {
|
|
95047
|
+
return this.frontmatter;
|
|
95048
|
+
}
|
|
95049
|
+
getBody() {
|
|
95050
|
+
return this.body;
|
|
95051
|
+
}
|
|
95052
|
+
toRulesyncSubagent() {
|
|
95053
|
+
const { name, description, ...restFields } = this.frontmatter;
|
|
95054
|
+
return new RulesyncSubagent({
|
|
95055
|
+
outputRoot: ".",
|
|
95056
|
+
frontmatter: {
|
|
95057
|
+
targets: ["*"],
|
|
95058
|
+
name,
|
|
95059
|
+
description,
|
|
95060
|
+
...Object.keys(restFields).length > 0 && { xum: restFields }
|
|
95061
|
+
},
|
|
95062
|
+
body: this.body,
|
|
95063
|
+
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
|
|
95064
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
95065
|
+
validate: true
|
|
95066
|
+
});
|
|
95067
|
+
}
|
|
95068
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
|
|
95069
|
+
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
95070
|
+
const xumSection = this.filterToolSpecificSection(rulesyncFrontmatter.xum ?? {}, ["name", "description"]);
|
|
95071
|
+
const subagentSection = xumSection.subagent;
|
|
95072
|
+
const result = XumSubagentFrontmatterSchema.safeParse({
|
|
95073
|
+
name: rulesyncFrontmatter.name,
|
|
95074
|
+
...rulesyncFrontmatter.description && { description: rulesyncFrontmatter.description },
|
|
95075
|
+
...xumSection.base === void 0 && xumSection.tools === void 0 && { base: XUM_DEFAULT_BASE_AGENT },
|
|
95076
|
+
...xumSection,
|
|
95077
|
+
subagent: {
|
|
95078
|
+
runnable: true,
|
|
95079
|
+
...isPlainObject$1(subagentSection) ? subagentSection : {}
|
|
95080
|
+
}
|
|
95081
|
+
});
|
|
95082
|
+
if (!result.success) throw new Error(`Invalid xum subagent frontmatter in ${rulesyncSubagent.getRelativeFilePath()}: ${formatError(result.error)}`);
|
|
95083
|
+
const body = rulesyncSubagent.getBody();
|
|
95084
|
+
return new XumSubagent({
|
|
95085
|
+
outputRoot,
|
|
95086
|
+
frontmatter: result.data,
|
|
95087
|
+
body,
|
|
95088
|
+
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
95089
|
+
relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
|
|
95090
|
+
fileContent: stringifyFrontmatter(body, result.data),
|
|
95091
|
+
validate
|
|
95092
|
+
});
|
|
95093
|
+
}
|
|
95094
|
+
validate() {
|
|
95095
|
+
if (!this.frontmatter) return {
|
|
95096
|
+
success: true,
|
|
95097
|
+
error: null
|
|
95098
|
+
};
|
|
95099
|
+
const result = XumSubagentFrontmatterSchema.safeParse(this.frontmatter);
|
|
95100
|
+
if (result.success) return {
|
|
95101
|
+
success: true,
|
|
95102
|
+
error: null
|
|
95103
|
+
};
|
|
95104
|
+
return {
|
|
95105
|
+
success: false,
|
|
95106
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
95107
|
+
};
|
|
95108
|
+
}
|
|
95109
|
+
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
95110
|
+
return this.isTargetedByRulesyncSubagentDefault({
|
|
95111
|
+
rulesyncSubagent,
|
|
95112
|
+
toolTarget: "xum"
|
|
95113
|
+
});
|
|
95114
|
+
}
|
|
95115
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
95116
|
+
const paths = this.getSettablePaths({ global });
|
|
95117
|
+
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
95118
|
+
const fileContent = await readFileContent(filePath);
|
|
95119
|
+
const { frontmatter, body } = parseFrontmatter(fileContent, filePath);
|
|
95120
|
+
const result = XumSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
95121
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
95122
|
+
return new XumSubagent({
|
|
95123
|
+
outputRoot,
|
|
95124
|
+
relativeDirPath: paths.relativeDirPath,
|
|
95125
|
+
relativeFilePath,
|
|
95126
|
+
frontmatter: result.data,
|
|
95127
|
+
body: body.trim(),
|
|
95128
|
+
fileContent,
|
|
95129
|
+
validate
|
|
95130
|
+
});
|
|
95131
|
+
}
|
|
95132
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
95133
|
+
return new XumSubagent({
|
|
95134
|
+
outputRoot,
|
|
95135
|
+
relativeDirPath,
|
|
95136
|
+
relativeFilePath,
|
|
95137
|
+
frontmatter: {
|
|
95138
|
+
name: "",
|
|
95139
|
+
description: ""
|
|
95140
|
+
},
|
|
95141
|
+
body: "",
|
|
95142
|
+
fileContent: "",
|
|
95143
|
+
validate: false
|
|
95144
|
+
});
|
|
95145
|
+
}
|
|
95146
|
+
};
|
|
95147
|
+
//#endregion
|
|
92650
95148
|
//#region src/features/subagents/zcode-subagent.ts
|
|
92651
95149
|
const ZcodeSubagentFrontmatterSchema = z.looseObject({
|
|
92652
95150
|
name: z.string(),
|
|
@@ -93112,6 +95610,17 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
93112
95610
|
filePattern: "*.md"
|
|
93113
95611
|
}
|
|
93114
95612
|
}],
|
|
95613
|
+
["firebender", {
|
|
95614
|
+
class: FirebenderSubagent,
|
|
95615
|
+
meta: {
|
|
95616
|
+
supportsProject: true,
|
|
95617
|
+
supportsSimulated: false,
|
|
95618
|
+
supportsGlobal: true,
|
|
95619
|
+
emitsEmptyAggregate: true,
|
|
95620
|
+
emitsEmptyAggregateOnDelete: true,
|
|
95621
|
+
filePattern: "*.md"
|
|
95622
|
+
}
|
|
95623
|
+
}],
|
|
93115
95624
|
["goose", {
|
|
93116
95625
|
class: GooseSubagent,
|
|
93117
95626
|
meta: {
|
|
@@ -93163,7 +95672,8 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
93163
95672
|
supportsProject: true,
|
|
93164
95673
|
supportsSimulated: false,
|
|
93165
95674
|
supportsGlobal: true,
|
|
93166
|
-
filePattern: "
|
|
95675
|
+
filePattern: "**/*.json",
|
|
95676
|
+
supportsNestedPaths: true
|
|
93167
95677
|
}
|
|
93168
95678
|
}],
|
|
93169
95679
|
["kiro-ide", {
|
|
@@ -93172,7 +95682,8 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
93172
95682
|
supportsProject: true,
|
|
93173
95683
|
supportsSimulated: false,
|
|
93174
95684
|
supportsGlobal: true,
|
|
93175
|
-
filePattern: "
|
|
95685
|
+
filePattern: "**/*.md",
|
|
95686
|
+
supportsNestedPaths: true
|
|
93176
95687
|
}
|
|
93177
95688
|
}],
|
|
93178
95689
|
["lettacode", {
|
|
@@ -93339,6 +95850,15 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
93339
95850
|
filePattern: "*.toml"
|
|
93340
95851
|
}
|
|
93341
95852
|
}],
|
|
95853
|
+
["xum", {
|
|
95854
|
+
class: XumSubagent,
|
|
95855
|
+
meta: {
|
|
95856
|
+
supportsProject: true,
|
|
95857
|
+
supportsSimulated: false,
|
|
95858
|
+
supportsGlobal: true,
|
|
95859
|
+
filePattern: "*.md"
|
|
95860
|
+
}
|
|
95861
|
+
}],
|
|
93342
95862
|
["zcode", {
|
|
93343
95863
|
class: ZcodeSubagent,
|
|
93344
95864
|
meta: {
|
|
@@ -93393,6 +95913,7 @@ function compareRelativePathsShallowestFirst(left, right) {
|
|
|
93393
95913
|
var SubagentsProcessor = class extends FeatureProcessor {
|
|
93394
95914
|
toolTarget;
|
|
93395
95915
|
global;
|
|
95916
|
+
deleteOrphans;
|
|
93396
95917
|
getFactory;
|
|
93397
95918
|
/**
|
|
93398
95919
|
* Whether the last `loadRulesyncFiles` found a `subagents/` directory under
|
|
@@ -93400,13 +95921,14 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
93400
95921
|
* agents in its Pool settings are its own and must not be retracted.
|
|
93401
95922
|
*/
|
|
93402
95923
|
rulesyncSourceDirFound = false;
|
|
93403
|
-
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, logger }) {
|
|
95924
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, deleteOrphans = false, logger }) {
|
|
93404
95925
|
super({
|
|
93405
95926
|
outputRoot,
|
|
93406
95927
|
inputRoots,
|
|
93407
95928
|
dryRun,
|
|
93408
95929
|
logger
|
|
93409
95930
|
});
|
|
95931
|
+
this.deleteOrphans = deleteOrphans;
|
|
93410
95932
|
const result = SubagentsProcessorToolTargetSchema.safeParse(toolTarget);
|
|
93411
95933
|
if (!result.success) throw new Error(`Invalid tool target for SubagentsProcessor: ${toolTarget}. ${formatError(result.error)}`);
|
|
93412
95934
|
this.toolTarget = result.data;
|
|
@@ -93414,7 +95936,9 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
93414
95936
|
this.getFactory = getFactory;
|
|
93415
95937
|
}
|
|
93416
95938
|
emitsToolFilesForEmptySource() {
|
|
93417
|
-
|
|
95939
|
+
const { meta } = this.getFactory(this.toolTarget);
|
|
95940
|
+
if (meta.emitsEmptyAggregate !== true) return false;
|
|
95941
|
+
return this.rulesyncSourceDirFound || this.deleteOrphans && meta.emitsEmptyAggregateOnDelete === true;
|
|
93418
95942
|
}
|
|
93419
95943
|
async convertRulesyncFilesToToolFiles(rulesyncFiles) {
|
|
93420
95944
|
const rulesyncSubagents = rulesyncFiles.filter((file) => file instanceof RulesyncSubagent);
|
|
@@ -93430,11 +95954,12 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
93430
95954
|
});
|
|
93431
95955
|
if (factory.class.fromRulesyncSubagents) {
|
|
93432
95956
|
if (targeted.length === 0 && !this.emitsToolFilesForEmptySource()) return [];
|
|
93433
|
-
const toolSubagents = factory.class.fromRulesyncSubagents({
|
|
95957
|
+
const toolSubagents = await factory.class.fromRulesyncSubagents({
|
|
93434
95958
|
outputRoot: this.outputRoot,
|
|
93435
95959
|
rulesyncSubagents: targeted,
|
|
93436
95960
|
global: this.global,
|
|
93437
|
-
logger: this.logger
|
|
95961
|
+
logger: this.logger,
|
|
95962
|
+
deleteOrphans: this.deleteOrphans && !this.hasRulesyncSourceLoadFailure()
|
|
93438
95963
|
});
|
|
93439
95964
|
return Array.isArray(toolSubagents) ? toolSubagents : [toolSubagents];
|
|
93440
95965
|
}
|
|
@@ -94180,7 +96705,7 @@ const isAiassistantApply = (value) => AIASSISTANT_APPLY_VALUES.includes(value);
|
|
|
94180
96705
|
/**
|
|
94181
96706
|
* Globs that match the whole project and so say nothing beyond "always".
|
|
94182
96707
|
*/
|
|
94183
|
-
const UNIVERSAL_GLOBS$
|
|
96708
|
+
const UNIVERSAL_GLOBS$6 = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
94184
96709
|
const METADATA_DELIMITER = "---";
|
|
94185
96710
|
/**
|
|
94186
96711
|
* Splits text into lines that keep their own line terminator, so the body can
|
|
@@ -94305,7 +96830,7 @@ var AiassistantRule = class AiassistantRule extends ToolRule {
|
|
|
94305
96830
|
*/
|
|
94306
96831
|
static buildMetadata(frontmatter) {
|
|
94307
96832
|
const globs = (frontmatter.globs ?? []).map((glob) => flattenValue(glob)).filter((glob) => glob.length > 0).flatMap((glob) => expandBraceAlternations(glob));
|
|
94308
|
-
const hasUniversalGlob = globs.some((glob) => UNIVERSAL_GLOBS$
|
|
96833
|
+
const hasUniversalGlob = globs.some((glob) => UNIVERSAL_GLOBS$6.has(glob));
|
|
94309
96834
|
const instructions = frontmatter.description && flattenValue(frontmatter.description);
|
|
94310
96835
|
const explicitApply = frontmatter.aiassistant?.apply && flattenValue(frontmatter.aiassistant.apply);
|
|
94311
96836
|
const apply = explicitApply ? explicitApply : frontmatter.root !== true && globs.length > 0 && !hasUniversalGlob ? "by file patterns" : frontmatter.root !== true && globs.length === 0 && instructions ? "by model decision" : "always";
|
|
@@ -96959,7 +99484,7 @@ function normalizePatternList(value) {
|
|
|
96959
99484
|
* an always-on rule into a "when any file is in context" rule. Dropped on
|
|
96960
99485
|
* generate and mapped back on import.
|
|
96961
99486
|
*/
|
|
96962
|
-
const UNIVERSAL_GLOBS$
|
|
99487
|
+
const UNIVERSAL_GLOBS$5 = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
96963
99488
|
/**
|
|
96964
99489
|
* Rule generator for Continue (continuedev/continue), the open-source IDE
|
|
96965
99490
|
* extension and CLI (`cn`).
|
|
@@ -97073,7 +99598,7 @@ var ContinueRule = class ContinueRule extends ToolRule {
|
|
|
97073
99598
|
if (alwaysApply === true) return;
|
|
97074
99599
|
const targetGlobs = continueGlobs ?? parentGlobs;
|
|
97075
99600
|
if (!targetGlobs || targetGlobs.length === 0) return;
|
|
97076
|
-
if (targetGlobs.every((glob) => UNIVERSAL_GLOBS$
|
|
99601
|
+
if (targetGlobs.every((glob) => UNIVERSAL_GLOBS$5.has(glob.trim()))) return;
|
|
97077
99602
|
return targetGlobs;
|
|
97078
99603
|
}
|
|
97079
99604
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
@@ -97565,7 +100090,7 @@ const CursorRuleFrontmatterSchema = z.object({
|
|
|
97565
100090
|
* does not already say. Same pair the Cline and Qwen Code adapters treat as
|
|
97566
100091
|
* universal.
|
|
97567
100092
|
*/
|
|
97568
|
-
const UNIVERSAL_GLOBS$
|
|
100093
|
+
const UNIVERSAL_GLOBS$4 = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
97569
100094
|
var CursorRule = class CursorRule extends ToolRule {
|
|
97570
100095
|
frontmatter;
|
|
97571
100096
|
body;
|
|
@@ -97606,7 +100131,8 @@ var CursorRule = class CursorRule extends ToolRule {
|
|
|
97606
100131
|
}
|
|
97607
100132
|
/**
|
|
97608
100133
|
* Custom parse function for Cursor MDC files
|
|
97609
|
-
* MDC files don't support quotes in YAML, so we need to handle patterns like *.ts specially
|
|
100134
|
+
* MDC files don't support quotes in YAML, so we need to handle patterns like *.ts specially.
|
|
100135
|
+
* Also used by Firebender, whose `.firebender/rules/*.mdc` files carry the same frontmatter.
|
|
97610
100136
|
*/
|
|
97611
100137
|
static parseCursorFrontmatter(fileContent, filePath) {
|
|
97612
100138
|
const bounds = findFrontmatterBlockBounds(fileContent);
|
|
@@ -97669,7 +100195,7 @@ var CursorRule = class CursorRule extends ToolRule {
|
|
|
97669
100195
|
static resolveCursorGlobs({ cursorSpecificGlobs, parentGlobs, alwaysApply }) {
|
|
97670
100196
|
const targetGlobs = cursorSpecificGlobs !== void 0 ? cursorSpecificGlobs : parentGlobs;
|
|
97671
100197
|
if (!targetGlobs || targetGlobs.length === 0) return;
|
|
97672
|
-
if (alwaysApply && targetGlobs.every((glob) => UNIVERSAL_GLOBS$
|
|
100198
|
+
if (alwaysApply && targetGlobs.every((glob) => UNIVERSAL_GLOBS$4.has(glob.trim()))) return;
|
|
97673
100199
|
return targetGlobs.join(",");
|
|
97674
100200
|
}
|
|
97675
100201
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
|
|
@@ -98583,6 +101109,211 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
98583
101109
|
}
|
|
98584
101110
|
};
|
|
98585
101111
|
//#endregion
|
|
101112
|
+
//#region src/features/rules/firebender-rule.ts
|
|
101113
|
+
/**
|
|
101114
|
+
* Frontmatter of a Firebender `.mdc` rule. Every field is optional; `globs`
|
|
101115
|
+
* is one comma-separated string, and a YAML list is tolerated on import.
|
|
101116
|
+
*/
|
|
101117
|
+
const FirebenderRuleFrontmatterSchema = z.looseObject({
|
|
101118
|
+
description: z.optional(z.nullable(z.string())),
|
|
101119
|
+
globs: z.optional(z.nullable(z.union([z.string(), z.array(z.string())]))),
|
|
101120
|
+
alwaysApply: z.optional(z.nullable(z.boolean()))
|
|
101121
|
+
});
|
|
101122
|
+
/** Globs that match every file, and so add nothing to an always-applied rule. */
|
|
101123
|
+
const UNIVERSAL_GLOBS$3 = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
101124
|
+
function normalizeGlobs$2(globs) {
|
|
101125
|
+
if (globs === null || globs === void 0) return [];
|
|
101126
|
+
return (typeof globs === "string" ? globs.split(",") : globs).map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
101127
|
+
}
|
|
101128
|
+
/**
|
|
101129
|
+
* Rule generator for Firebender.
|
|
101130
|
+
*
|
|
101131
|
+
* Root rule: Firebender looks for `.firebender/AGENTS.md` first (then the
|
|
101132
|
+
* project-root `AGENTS.md`, then the same pair in parent directories, then
|
|
101133
|
+
* `~/.firebender/AGENTS.md`) and uses the first file it finds, so the root rule
|
|
101134
|
+
* is written to `.firebender/AGENTS.md` in the project and to
|
|
101135
|
+
* `~/.firebender/AGENTS.md` globally, where it does not collide with the
|
|
101136
|
+
* project-root `AGENTS.md` the `agentsmd` target owns.
|
|
101137
|
+
*
|
|
101138
|
+
* Non-root rules: `.firebender/rules/*.mdc` (project) and
|
|
101139
|
+
* `~/.firebender/rules/*.mdc` (personal), each with optional Cursor-style
|
|
101140
|
+
* frontmatter: `description`, `globs` (one comma-separated string) and
|
|
101141
|
+
* `alwaysApply`. A rule with specific globs is written with
|
|
101142
|
+
* `alwaysApply: false` and those globs; every other rule is always applied.
|
|
101143
|
+
*
|
|
101144
|
+
* @see https://docs.firebender.com/api-reference/rules
|
|
101145
|
+
* @see https://docs.firebender.com/api-reference/agents-md
|
|
101146
|
+
*/
|
|
101147
|
+
var FirebenderRule = class FirebenderRule extends ToolRule {
|
|
101148
|
+
frontmatter;
|
|
101149
|
+
body;
|
|
101150
|
+
constructor({ frontmatter = {}, body, ...rest }) {
|
|
101151
|
+
if (rest.validate && !rest.root) {
|
|
101152
|
+
const result = FirebenderRuleFrontmatterSchema.safeParse(frontmatter);
|
|
101153
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
101154
|
+
}
|
|
101155
|
+
const resolvedBody = body ?? rest.fileContent;
|
|
101156
|
+
super({
|
|
101157
|
+
...rest,
|
|
101158
|
+
fileContent: rest.root ? resolvedBody : stringifyFrontmatter(resolvedBody, FirebenderRule.toOutputFrontmatter(frontmatter), { avoidBlockScalars: true })
|
|
101159
|
+
});
|
|
101160
|
+
this.frontmatter = frontmatter;
|
|
101161
|
+
this.body = resolvedBody;
|
|
101162
|
+
}
|
|
101163
|
+
static getSettablePaths({ excludeToolDir } = {}) {
|
|
101164
|
+
return {
|
|
101165
|
+
root: {
|
|
101166
|
+
relativeDirPath: buildToolPath(FIREBENDER_DIR, ".", excludeToolDir),
|
|
101167
|
+
relativeFilePath: FIREBENDER_AGENTS_MD_FILE_NAME
|
|
101168
|
+
},
|
|
101169
|
+
nonRoot: { relativeDirPath: buildToolPath(FIREBENDER_DIR, "rules", excludeToolDir) }
|
|
101170
|
+
};
|
|
101171
|
+
}
|
|
101172
|
+
/**
|
|
101173
|
+
* The frontmatter written to a `.mdc` file: `alwaysApply` first, then the
|
|
101174
|
+
* flattened `description`, then the globs joined the way Firebender's docs
|
|
101175
|
+
* spell them (`"*.kt, *.kts"`).
|
|
101176
|
+
*/
|
|
101177
|
+
static toOutputFrontmatter(frontmatter) {
|
|
101178
|
+
const description = typeof frontmatter.description === "string" ? frontmatter.description.replace(/[\r\n\u0085\u2028\u2029]+/g, " ").trim() : "";
|
|
101179
|
+
const globs = normalizeGlobs$2(frontmatter.globs);
|
|
101180
|
+
return {
|
|
101181
|
+
...typeof frontmatter.alwaysApply === "boolean" && { alwaysApply: frontmatter.alwaysApply },
|
|
101182
|
+
...description !== "" && { description },
|
|
101183
|
+
...globs.length > 0 && { globs: globs.join(", ") }
|
|
101184
|
+
};
|
|
101185
|
+
}
|
|
101186
|
+
/**
|
|
101187
|
+
* Derives the Firebender frontmatter from the canonical one: specific globs
|
|
101188
|
+
* make a file-scoped rule, everything else (no globs, universal globs) is
|
|
101189
|
+
* always applied. Firebender splits `globs` on every comma, so a brace
|
|
101190
|
+
* alternation such as `*.{ts,tsx}` is expanded into one glob per branch.
|
|
101191
|
+
*/
|
|
101192
|
+
static buildFrontmatter(frontmatter) {
|
|
101193
|
+
const globs = normalizeGlobs$2(frontmatter.globs ?? []).flatMap((glob) => expandBraceAlternations(glob));
|
|
101194
|
+
const specificGlobs = globs.filter((glob) => !UNIVERSAL_GLOBS$3.has(glob));
|
|
101195
|
+
const isFileScoped = specificGlobs.length > 0 && specificGlobs.length === globs.length;
|
|
101196
|
+
const description = frontmatter.description?.trim() || void 0;
|
|
101197
|
+
return {
|
|
101198
|
+
alwaysApply: !isFileScoped,
|
|
101199
|
+
...description !== void 0 && { description },
|
|
101200
|
+
...isFileScoped && { globs: specificGlobs.join(", ") }
|
|
101201
|
+
};
|
|
101202
|
+
}
|
|
101203
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
|
|
101204
|
+
const paths = this.getSettablePaths();
|
|
101205
|
+
const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
|
|
101206
|
+
if (rulesyncFrontmatter.root ?? false) return new FirebenderRule({
|
|
101207
|
+
outputRoot,
|
|
101208
|
+
relativeDirPath: paths.root.relativeDirPath,
|
|
101209
|
+
relativeFilePath: paths.root.relativeFilePath,
|
|
101210
|
+
fileContent: rulesyncRule.getBody(),
|
|
101211
|
+
validate,
|
|
101212
|
+
root: true,
|
|
101213
|
+
description: rulesyncFrontmatter.description,
|
|
101214
|
+
globs: rulesyncFrontmatter.globs
|
|
101215
|
+
});
|
|
101216
|
+
return new FirebenderRule({
|
|
101217
|
+
outputRoot,
|
|
101218
|
+
relativeDirPath: paths.nonRoot.relativeDirPath,
|
|
101219
|
+
relativeFilePath: rulesyncRule.getRelativeFilePath().replace(/\.md$/, ".mdc"),
|
|
101220
|
+
frontmatter: this.buildFrontmatter(rulesyncFrontmatter),
|
|
101221
|
+
body: rulesyncRule.getBody(),
|
|
101222
|
+
fileContent: "",
|
|
101223
|
+
validate,
|
|
101224
|
+
root: false
|
|
101225
|
+
});
|
|
101226
|
+
}
|
|
101227
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true }) {
|
|
101228
|
+
const paths = this.getSettablePaths();
|
|
101229
|
+
if (relativeFilePath === paths.root.relativeFilePath && (relativeDirPath === void 0 || relativeDirPath === paths.root.relativeDirPath)) {
|
|
101230
|
+
const fileContent = await readFileContent(join(outputRoot, paths.root.relativeDirPath, paths.root.relativeFilePath));
|
|
101231
|
+
return new FirebenderRule({
|
|
101232
|
+
outputRoot,
|
|
101233
|
+
relativeDirPath: paths.root.relativeDirPath,
|
|
101234
|
+
relativeFilePath: paths.root.relativeFilePath,
|
|
101235
|
+
fileContent,
|
|
101236
|
+
validate,
|
|
101237
|
+
root: true
|
|
101238
|
+
});
|
|
101239
|
+
}
|
|
101240
|
+
const filePath = join(outputRoot, paths.nonRoot.relativeDirPath, relativeFilePath);
|
|
101241
|
+
const fileContent = await readFileContent(filePath);
|
|
101242
|
+
const { frontmatter, body } = CursorRule.parseCursorFrontmatter(fileContent, filePath);
|
|
101243
|
+
const result = FirebenderRuleFrontmatterSchema.safeParse(frontmatter);
|
|
101244
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
101245
|
+
return new FirebenderRule({
|
|
101246
|
+
outputRoot,
|
|
101247
|
+
relativeDirPath: paths.nonRoot.relativeDirPath,
|
|
101248
|
+
relativeFilePath,
|
|
101249
|
+
frontmatter: result.data,
|
|
101250
|
+
body: body.trim(),
|
|
101251
|
+
fileContent: "",
|
|
101252
|
+
validate,
|
|
101253
|
+
root: false
|
|
101254
|
+
});
|
|
101255
|
+
}
|
|
101256
|
+
toRulesyncRule() {
|
|
101257
|
+
if (this.isRoot()) return this.toRulesyncRuleDefault();
|
|
101258
|
+
const sourceGlobs = normalizeGlobs$2(this.frontmatter.globs);
|
|
101259
|
+
const alwaysApply = this.frontmatter.alwaysApply ?? void 0;
|
|
101260
|
+
const description = this.frontmatter.description?.trim() || void 0;
|
|
101261
|
+
const globs = alwaysApply === true || sourceGlobs.length === 0 ? ["**/*"] : sourceGlobs;
|
|
101262
|
+
return new RulesyncRule({
|
|
101263
|
+
outputRoot: process.cwd(),
|
|
101264
|
+
frontmatter: {
|
|
101265
|
+
targets: ["*"],
|
|
101266
|
+
root: false,
|
|
101267
|
+
...description !== void 0 && { description },
|
|
101268
|
+
globs
|
|
101269
|
+
},
|
|
101270
|
+
body: this.body,
|
|
101271
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
101272
|
+
relativeFilePath: this.getRelativeFilePath().replace(/\.mdc$/, ".md"),
|
|
101273
|
+
validate: true
|
|
101274
|
+
});
|
|
101275
|
+
}
|
|
101276
|
+
validate() {
|
|
101277
|
+
if (this.isRoot() || !this.frontmatter) return {
|
|
101278
|
+
success: true,
|
|
101279
|
+
error: null
|
|
101280
|
+
};
|
|
101281
|
+
const result = FirebenderRuleFrontmatterSchema.safeParse(this.frontmatter);
|
|
101282
|
+
if (result.success) return {
|
|
101283
|
+
success: true,
|
|
101284
|
+
error: null
|
|
101285
|
+
};
|
|
101286
|
+
return {
|
|
101287
|
+
success: false,
|
|
101288
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
101289
|
+
};
|
|
101290
|
+
}
|
|
101291
|
+
getFrontmatter() {
|
|
101292
|
+
return this.frontmatter;
|
|
101293
|
+
}
|
|
101294
|
+
getBody() {
|
|
101295
|
+
return this.body;
|
|
101296
|
+
}
|
|
101297
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
101298
|
+
const paths = this.getSettablePaths();
|
|
101299
|
+
const isRoot = relativeFilePath === paths.root.relativeFilePath && relativeDirPath === paths.root.relativeDirPath;
|
|
101300
|
+
return new FirebenderRule({
|
|
101301
|
+
outputRoot,
|
|
101302
|
+
relativeDirPath,
|
|
101303
|
+
relativeFilePath,
|
|
101304
|
+
fileContent: "",
|
|
101305
|
+
validate: false,
|
|
101306
|
+
root: isRoot
|
|
101307
|
+
});
|
|
101308
|
+
}
|
|
101309
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
101310
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
101311
|
+
rulesyncRule,
|
|
101312
|
+
toolTarget: "firebender"
|
|
101313
|
+
});
|
|
101314
|
+
}
|
|
101315
|
+
};
|
|
101316
|
+
//#endregion
|
|
98586
101317
|
//#region src/features/rules/gitlabduo-rule.ts
|
|
98587
101318
|
/**
|
|
98588
101319
|
* GitLab Duo CLI reads custom rules from a single plain-Markdown file:
|
|
@@ -101864,6 +104595,117 @@ var WarpRule = class extends NestedAgentsmdRule {
|
|
|
101864
104595
|
}
|
|
101865
104596
|
};
|
|
101866
104597
|
//#endregion
|
|
104598
|
+
//#region src/features/rules/xum-rule.ts
|
|
104599
|
+
/** An ATX level-1 heading line (`# Title`), which Xum's parser treats as an H1. */
|
|
104600
|
+
const ATX_H1_LINE = /^ {0,3}#(?:[ \t]|$)/;
|
|
104601
|
+
/**
|
|
104602
|
+
* Make `body` start at a level-1 heading, adding `# <heading>` when it does not
|
|
104603
|
+
* already open with one.
|
|
104604
|
+
*
|
|
104605
|
+
* Xum reads `.xum/AGENTS.md` (and appends `AGENTS.local.md`) as one Markdown
|
|
104606
|
+
* document and treats a `Mode:` / `Model:` / `Tool:` heading as a scoped
|
|
104607
|
+
* section that runs until the next heading of the same or a higher level.
|
|
104608
|
+
* Fragments rulesync concatenates into that document — folded topic rules and
|
|
104609
|
+
* the local overlay — would otherwise fall inside a scoped section the
|
|
104610
|
+
* preceding fragment ends with, and Xum would inject them only in that mode.
|
|
104611
|
+
* Only an H1 closes a section of any level, so each such fragment opens with
|
|
104612
|
+
* one.
|
|
104613
|
+
*
|
|
104614
|
+
* @see https://github.com/coder/xum/blob/main/src/node/utils/main/markdown.ts
|
|
104615
|
+
*/
|
|
104616
|
+
function ensureXumSectionBoundary({ body, heading }) {
|
|
104617
|
+
const firstLine = body.split(/\r?\n/).find((line) => line.trim() !== "");
|
|
104618
|
+
if (firstLine !== void 0 && ATX_H1_LINE.test(firstLine)) return body;
|
|
104619
|
+
const safeHeading = heading.replaceAll(/[\r\n:]/g, " ").trim() || "Instructions";
|
|
104620
|
+
return body.trim() === "" ? `# ${safeHeading}\n` : `# ${safeHeading}\n\n${body}`;
|
|
104621
|
+
}
|
|
104622
|
+
/** The heading a folded rule opens with: its file name without the extension. */
|
|
104623
|
+
function xumRuleHeading(rulesyncRule) {
|
|
104624
|
+
const fileName = basename(rulesyncRule.getRelativeFilePath());
|
|
104625
|
+
return fileName.slice(0, fileName.length - extname(fileName).length);
|
|
104626
|
+
}
|
|
104627
|
+
/**
|
|
104628
|
+
* Rule generator for Xum (Coder's parallel coding-agent app, formerly Mux).
|
|
104629
|
+
*
|
|
104630
|
+
* Xum layers its own `.xum/AGENTS.md` (project) and `~/.xum/AGENTS.md` (user)
|
|
104631
|
+
* on top of the shared root `AGENTS.md` / `AGENT.md` / `CLAUDE.md`, which the
|
|
104632
|
+
* `agentsmd` target already writes, so this target writes only the
|
|
104633
|
+
* Xum-dedicated file. Only these files honor the `## Model: <regex>` and
|
|
104634
|
+
* `## Mode: <mode>` scoped sections; rulesync passes the body through as-is,
|
|
104635
|
+
* so such sections authored in a rule keep working. The personal
|
|
104636
|
+
* `.xum/AGENTS.local.md` overlay is written for a `localRoot: true` rule.
|
|
104637
|
+
*
|
|
104638
|
+
* @see https://xum.cdr.dev/agents/instruction-files
|
|
104639
|
+
*/
|
|
104640
|
+
var XumRule = class XumRule extends ToolRule {
|
|
104641
|
+
constructor({ fileContent, root, ...rest }) {
|
|
104642
|
+
super({
|
|
104643
|
+
...rest,
|
|
104644
|
+
fileContent,
|
|
104645
|
+
root: root ?? false
|
|
104646
|
+
});
|
|
104647
|
+
}
|
|
104648
|
+
static getSettablePaths(_options = {}) {
|
|
104649
|
+
return { root: {
|
|
104650
|
+
relativeDirPath: XUM_DIR,
|
|
104651
|
+
relativeFilePath: XUM_RULE_FILE_NAME
|
|
104652
|
+
} };
|
|
104653
|
+
}
|
|
104654
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
|
|
104655
|
+
const { root } = this.getSettablePaths({ global });
|
|
104656
|
+
const relativePath = join(root.relativeDirPath, root.relativeFilePath);
|
|
104657
|
+
const fileContent = await readFileContent(join(outputRoot, relativePath));
|
|
104658
|
+
return new XumRule({
|
|
104659
|
+
outputRoot,
|
|
104660
|
+
relativeDirPath: root.relativeDirPath,
|
|
104661
|
+
relativeFilePath: root.relativeFilePath,
|
|
104662
|
+
fileContent,
|
|
104663
|
+
validate,
|
|
104664
|
+
root: true
|
|
104665
|
+
});
|
|
104666
|
+
}
|
|
104667
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
104668
|
+
const { root } = this.getSettablePaths({ global });
|
|
104669
|
+
const isRoot = rulesyncRule.getFrontmatter().root ?? false;
|
|
104670
|
+
return new XumRule({
|
|
104671
|
+
outputRoot,
|
|
104672
|
+
relativeDirPath: root.relativeDirPath,
|
|
104673
|
+
relativeFilePath: root.relativeFilePath,
|
|
104674
|
+
fileContent: isRoot ? rulesyncRule.getBody() : ensureXumSectionBoundary({
|
|
104675
|
+
body: rulesyncRule.getBody(),
|
|
104676
|
+
heading: xumRuleHeading(rulesyncRule)
|
|
104677
|
+
}),
|
|
104678
|
+
validate,
|
|
104679
|
+
root: isRoot
|
|
104680
|
+
});
|
|
104681
|
+
}
|
|
104682
|
+
toRulesyncRule() {
|
|
104683
|
+
return this.toRulesyncRuleDefault();
|
|
104684
|
+
}
|
|
104685
|
+
validate() {
|
|
104686
|
+
return {
|
|
104687
|
+
success: true,
|
|
104688
|
+
error: null
|
|
104689
|
+
};
|
|
104690
|
+
}
|
|
104691
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
104692
|
+
return new XumRule({
|
|
104693
|
+
outputRoot,
|
|
104694
|
+
relativeDirPath,
|
|
104695
|
+
relativeFilePath,
|
|
104696
|
+
fileContent: "",
|
|
104697
|
+
validate: false,
|
|
104698
|
+
root: relativeFilePath === "AGENTS.md" && relativeDirPath === ".xum"
|
|
104699
|
+
});
|
|
104700
|
+
}
|
|
104701
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
104702
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
104703
|
+
rulesyncRule,
|
|
104704
|
+
toolTarget: "xum"
|
|
104705
|
+
});
|
|
104706
|
+
}
|
|
104707
|
+
};
|
|
104708
|
+
//#endregion
|
|
101867
104709
|
//#region src/features/rules/zcode-rule.ts
|
|
101868
104710
|
var ZcodeRule = class ZcodeRule extends ToolRule {
|
|
101869
104711
|
constructor({ fileContent, root, ...rest }) {
|
|
@@ -102352,6 +105194,14 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
102352
105194
|
collisionPolicy: "compose"
|
|
102353
105195
|
}
|
|
102354
105196
|
}],
|
|
105197
|
+
["firebender", {
|
|
105198
|
+
class: FirebenderRule,
|
|
105199
|
+
meta: {
|
|
105200
|
+
extension: "mdc",
|
|
105201
|
+
supportsGlobal: true,
|
|
105202
|
+
ruleDiscoveryMode: "auto"
|
|
105203
|
+
}
|
|
105204
|
+
}],
|
|
102355
105205
|
["goose", {
|
|
102356
105206
|
class: GooseRule,
|
|
102357
105207
|
meta: {
|
|
@@ -102635,6 +105485,17 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
102635
105485
|
collisionPolicy: "fold"
|
|
102636
105486
|
}
|
|
102637
105487
|
}],
|
|
105488
|
+
["xum", {
|
|
105489
|
+
class: XumRule,
|
|
105490
|
+
meta: {
|
|
105491
|
+
extension: "md",
|
|
105492
|
+
supportsGlobal: true,
|
|
105493
|
+
ruleDiscoveryMode: "auto",
|
|
105494
|
+
collisionPolicy: "fold",
|
|
105495
|
+
localRootMode: "separate-local-file",
|
|
105496
|
+
localRootFileName: XUM_LOCAL_RULE_FILE_NAME
|
|
105497
|
+
}
|
|
105498
|
+
}],
|
|
102638
105499
|
["devin", {
|
|
102639
105500
|
class: DevinRule,
|
|
102640
105501
|
meta: {
|
|
@@ -103405,18 +106266,39 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
103405
106266
|
root: true,
|
|
103406
106267
|
localRoot
|
|
103407
106268
|
});
|
|
103408
|
-
|
|
103409
|
-
|
|
103410
|
-
|
|
103411
|
-
|
|
103412
|
-
|
|
103413
|
-
relativeDirPath: relativeDirPath ?? ".qwen",
|
|
103414
|
-
relativeFilePath: fileName,
|
|
103415
|
-
fileContent: body,
|
|
103416
|
-
validate: true,
|
|
103417
|
-
root: true,
|
|
106269
|
+
return this.buildToolDirLocalRootFile({
|
|
106270
|
+
factory,
|
|
106271
|
+
fileName,
|
|
106272
|
+
body,
|
|
106273
|
+
relativeDirPath,
|
|
103418
106274
|
localRoot
|
|
103419
106275
|
});
|
|
106276
|
+
}
|
|
106277
|
+
/**
|
|
106278
|
+
* Qwen Code and Xum read the personal local file from their tool directory
|
|
106279
|
+
* (`.qwen/QWEN.local.md`, `.xum/AGENTS.local.md`), not the project root
|
|
106280
|
+
* (project scope only; global handling never reaches here).
|
|
106281
|
+
*/
|
|
106282
|
+
buildToolDirLocalRootFile({ factory, fileName, body, relativeDirPath, localRoot }) {
|
|
106283
|
+
const toolDirEntry = [[QwencodeRule, QWENCODE_DIR], [XumRule, XUM_DIR]].find(([base]) => isClassOrSubclassOf({
|
|
106284
|
+
candidate: factory.class,
|
|
106285
|
+
base
|
|
106286
|
+
}));
|
|
106287
|
+
if (toolDirEntry) {
|
|
106288
|
+
const [toolDirClass, toolDir] = toolDirEntry;
|
|
106289
|
+
return new toolDirClass({
|
|
106290
|
+
outputRoot: this.outputRoot,
|
|
106291
|
+
relativeDirPath: relativeDirPath ?? toolDir,
|
|
106292
|
+
relativeFilePath: fileName,
|
|
106293
|
+
fileContent: toolDirClass === XumRule && !localRoot ? ensureXumSectionBoundary({
|
|
106294
|
+
body,
|
|
106295
|
+
heading: "Local instructions"
|
|
106296
|
+
}) : body,
|
|
106297
|
+
validate: true,
|
|
106298
|
+
root: true,
|
|
106299
|
+
localRoot
|
|
106300
|
+
});
|
|
106301
|
+
}
|
|
103420
106302
|
return null;
|
|
103421
106303
|
}
|
|
103422
106304
|
/**
|
|
@@ -106168,6 +109050,7 @@ async function generateSubagentsCore(params) {
|
|
|
106168
109050
|
toolTarget,
|
|
106169
109051
|
global: config.getGlobal(),
|
|
106170
109052
|
dryRun: config.isPreviewMode(),
|
|
109053
|
+
deleteOrphans: config.getDelete(),
|
|
106171
109054
|
logger
|
|
106172
109055
|
});
|
|
106173
109056
|
const result = await processFeatureWithRulesyncFiles({
|
|
@@ -106790,6 +109673,6 @@ async function importChecksCore(params) {
|
|
|
106790
109673
|
return writtenCount;
|
|
106791
109674
|
}
|
|
106792
109675
|
//#endregion
|
|
106793
|
-
export { RulesyncHooks as $,
|
|
109676
|
+
export { RulesyncHooks as $, stripHiddenCharacters as $n, readFileContentOrNull as $t, groupSpellingsByCaseFoldedIdentity as A, RULESYNC_MCP_RELATIVE_FILE_PATH as An, PathOutsideRootError as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, RULESYNC_USER_CONFIG_DIR_NAME as Bn, fileExists as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Cn, fallbackLogger as Ct, FACTORYDROID_DIR as D, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as En, resetRunWarningState as Et, CLAUDECODE_LOCAL_RULE_FILE_NAME as F, RULESYNC_RELATIVE_DIR_PATH as Fn, assertWritablePathInsideRoot as Ft, RulesyncSubagentFrontmatterSchema as G, DEPRECATED_FEATURE_REPLACEMENTS as Gn, isFileSystemError as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, parseCommaSeparatedList as Hn, getFileSizeAndExecutableBit as Ht, CLAUDECODE_MEMORIES_DIR_NAME as I, RULESYNC_RULES_RELATIVE_DIR_PATH as In, checkPathTraversal as It, RulesyncRule as J, hasDeceptiveHiddenCharacters as Jn, listFilePathsRecursively as Jt, RulesyncSkill as K, formatError as Kn, isSymlink as Kt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as L, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Ln, createTempDirectory as Lt, CODEXCLI_DIR as M, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Mn, assertDirectoryIfExists as Mt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as N, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Nn, assertPathResolvesInsideRoot as Nt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_IGNORE_RELATIVE_FILE_PATH as On, CLIError as Ot, CLAUDECODE_DIR as P, RULESYNC_PERMISSIONS_SCHEMA_URL as Pn, assertTreeContainsNoSymlinks as Pt, RulesyncIgnore as Q, stripControlCharactersKeepingLineFeeds as Qn, readFileContent as Qt, CLAUDECODE_SKILLS_DIR_PATH as R, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Rn, directoryExists as Rt, COMMANDCODE_DIR as S, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, ALL_FEATURES as Un, getHomeDirectory as Ut, AUGMENTCODE_DIR as V, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as Vn, findFilesByGlobs as Vt, RulesyncSubagent as W, ALL_FEATURES_WITH_WILDCARD as Wn, isFileNotFoundError as Wt, RulesyncPermissions as X, quoteForLog as Xn, pathEscapesRoot as Xt, RulesyncRuleFrontmatterSchema as Y, hasEnclosingMarkOutsideKeycap as Yn, listSubdirectoryNames as Yt, RulesyncMcp as Z, stripControlCharacters as Zn, posixRelativePathEscapesRoot as Zt, CRUSH_LOCAL_RULE_FILE_NAME as _, CURATED_RULES_FEATURE_SUBDIR as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, resolvePath as an, RulesyncCheck as at, LETTACODE_DIR as b, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, splitPathSegments as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, writeFileContent as dn, ConfigResolver as dt, removeDirectory as en, getRulesyncSourceCandidates as et, displayWidthOf as f, mapWithConcurrency as fn, mergeInputRootConfigs as ft, HooksProcessor as g, ToolTargetSchema as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, PACKAGING_TOOL_TARGETS as hn, ConfigFileSchema as ht, inspectInputRoots as i, removeTempDirectory as in, RulesyncCommandFrontmatterSchema as it, CODEXCLI_BASH_RULES_FILE_NAME as j, RULESYNC_MCP_SCHEMA_URL as jn, applyFileMode as jt, caseFoldIdentity as k, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as kn, ErrorCodes as kt, SubagentsProcessor as l, toPosixPath as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, ALL_TOOL_TARGETS_WITH_WILDCARD as mn, CONFLICTING_TARGET_PAIRS as mt, formatSourceLoadFailure as n, removeFile as nn, parseJsonc as nt, convertFromTool as o, resolvedRelativePath as on, RulesyncCheckFrontmatterSchema as ot, shortenToWidth as p, ALL_TOOL_TARGETS as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, truncateText as qn, listDirectoryEntryNames as qt, generate as r, removeFileStrict as rn, RulesyncCommand as rt, isPackagingToolTarget as s, runWithDirectoryRollback as sn, stringifyFrontmatter as st, importFromTool as t, removeDirectoryStrict as tn, resolveRulesyncSourceWritePath as tt, SkillsProcessor as u, writeFileBuffer as un, SKILL_FILE_NAME as ut, CommandsProcessor as v, FEATURE_SOURCE_TREE_ENTRIES as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_CONFIG_SCHEMA_URL as wn, warnOnConflictingFlags as wt, LETTACODE_SETTINGS_LOCAL_FILE_NAME as x, RULESYNC_CHECKS_RELATIVE_DIR_PATH as xn, JsonLogger as xt, REASONIX_LOCAL_RULE_FILE_NAME as y, MAX_FILE_SIZE as yn, findControlCharacter as yt, QWENCODE_DIR as z, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as zn, ensureDir as zt };
|
|
106794
109677
|
|
|
106795
|
-
//# sourceMappingURL=import-
|
|
109678
|
+
//# sourceMappingURL=import-Chn7Nqnm.js.map
|