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
|
@@ -457,36 +457,36 @@ const isFeatureValueEnabled = (value) => {
|
|
|
457
457
|
const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
458
458
|
//#endregion
|
|
459
459
|
//#region src/constants/rulesync-paths.ts
|
|
460
|
-
const { join: join$
|
|
460
|
+
const { join: join$441 } = node_path.posix;
|
|
461
461
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
462
462
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
463
463
|
const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
|
|
464
464
|
const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
|
|
465
465
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
466
466
|
const RULES_FEATURE_SUBDIR = "rules";
|
|
467
|
-
const CURATED_RULES_FEATURE_SUBDIR = join$
|
|
467
|
+
const CURATED_RULES_FEATURE_SUBDIR = join$441(RULES_FEATURE_SUBDIR, ".curated");
|
|
468
468
|
const COMMANDS_FEATURE_SUBDIR = "commands";
|
|
469
469
|
const SUBAGENTS_FEATURE_SUBDIR = "subagents";
|
|
470
470
|
const CHECKS_FEATURE_SUBDIR = "checks";
|
|
471
471
|
const SKILLS_FEATURE_SUBDIR = "skills";
|
|
472
|
-
const CURATED_SKILLS_FEATURE_SUBDIR = join$
|
|
473
|
-
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$
|
|
474
|
-
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$
|
|
475
|
-
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$
|
|
476
|
-
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$
|
|
477
|
-
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$
|
|
478
|
-
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$
|
|
479
|
-
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$
|
|
480
|
-
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$
|
|
481
|
-
join$
|
|
482
|
-
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
483
|
-
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
472
|
+
const CURATED_SKILLS_FEATURE_SUBDIR = join$441(SKILLS_FEATURE_SUBDIR, ".curated");
|
|
473
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
|
|
474
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
|
|
475
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
|
|
476
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
|
|
477
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
|
|
478
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
479
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
480
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
481
|
+
join$441(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
482
|
+
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
483
|
+
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
484
484
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
485
|
-
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$
|
|
485
|
+
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
486
486
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
487
487
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
488
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$
|
|
489
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
488
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
|
|
489
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
490
490
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
491
491
|
const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
|
|
492
492
|
const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
|
|
@@ -544,6 +544,7 @@ const rulesProcessorToolTargetTuple = [
|
|
|
544
544
|
"cursor",
|
|
545
545
|
"deepagents",
|
|
546
546
|
"factorydroid",
|
|
547
|
+
"firebender",
|
|
547
548
|
"goose",
|
|
548
549
|
"grokcli",
|
|
549
550
|
"gitlabduo",
|
|
@@ -573,6 +574,7 @@ const rulesProcessorToolTargetTuple = [
|
|
|
573
574
|
"trae",
|
|
574
575
|
"vibe",
|
|
575
576
|
"warp",
|
|
577
|
+
"xum",
|
|
576
578
|
"devin",
|
|
577
579
|
"devin-plugin",
|
|
578
580
|
"zcode",
|
|
@@ -582,6 +584,7 @@ const rulesProcessorToolTargetTuple = [
|
|
|
582
584
|
];
|
|
583
585
|
const ignoreProcessorToolTargetTuple = [
|
|
584
586
|
"aiassistant",
|
|
587
|
+
"androidstudio",
|
|
585
588
|
"antigravity-cli",
|
|
586
589
|
"augmentcode",
|
|
587
590
|
"bob",
|
|
@@ -592,6 +595,7 @@ const ignoreProcessorToolTargetTuple = [
|
|
|
592
595
|
"continue",
|
|
593
596
|
"crush",
|
|
594
597
|
"cursor",
|
|
598
|
+
"firebender",
|
|
595
599
|
"grokcli",
|
|
596
600
|
"hermesagent",
|
|
597
601
|
"junie",
|
|
@@ -639,6 +643,7 @@ const mcpProcessorToolTargetTuple = [
|
|
|
639
643
|
"deepagents",
|
|
640
644
|
"dsh",
|
|
641
645
|
"factorydroid",
|
|
646
|
+
"firebender",
|
|
642
647
|
"goose",
|
|
643
648
|
"grokcli",
|
|
644
649
|
"gitlabduo",
|
|
@@ -668,6 +673,7 @@ const mcpProcessorToolTargetTuple = [
|
|
|
668
673
|
"vibe-plugin",
|
|
669
674
|
"warp",
|
|
670
675
|
"warpcli",
|
|
676
|
+
"xum",
|
|
671
677
|
"devin",
|
|
672
678
|
"devin-plugin",
|
|
673
679
|
"zcode",
|
|
@@ -694,6 +700,7 @@ const commandsProcessorToolTargetTuple = [
|
|
|
694
700
|
"copilot",
|
|
695
701
|
"cursor",
|
|
696
702
|
"factorydroid",
|
|
703
|
+
"firebender",
|
|
697
704
|
"goose",
|
|
698
705
|
"grokcli",
|
|
699
706
|
"gitlabduo",
|
|
@@ -749,6 +756,7 @@ const subagentsProcessorToolTargetTuple = [
|
|
|
749
756
|
"devin",
|
|
750
757
|
"devin-plugin",
|
|
751
758
|
"factorydroid",
|
|
759
|
+
"firebender",
|
|
752
760
|
"goose",
|
|
753
761
|
"grokcli",
|
|
754
762
|
"hermesagent",
|
|
@@ -773,6 +781,7 @@ const subagentsProcessorToolTargetTuple = [
|
|
|
773
781
|
"takt",
|
|
774
782
|
"vibe",
|
|
775
783
|
"vibe-plugin",
|
|
784
|
+
"xum",
|
|
776
785
|
"zcode",
|
|
777
786
|
"zcode-plugin"
|
|
778
787
|
];
|
|
@@ -780,6 +789,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
780
789
|
"agentsmd",
|
|
781
790
|
"agentsskills",
|
|
782
791
|
"aiassistant",
|
|
792
|
+
"androidstudio",
|
|
783
793
|
"amp",
|
|
784
794
|
"antigravity-cli",
|
|
785
795
|
"antigravity-ide",
|
|
@@ -803,6 +813,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
803
813
|
"cursor",
|
|
804
814
|
"deepagents",
|
|
805
815
|
"factorydroid",
|
|
816
|
+
"firebender",
|
|
806
817
|
"goose",
|
|
807
818
|
"grokcli",
|
|
808
819
|
"gitlabduo",
|
|
@@ -832,6 +843,7 @@ const skillsProcessorToolTargetTuple = [
|
|
|
832
843
|
"vibe",
|
|
833
844
|
"vibe-plugin",
|
|
834
845
|
"warp",
|
|
846
|
+
"xum",
|
|
835
847
|
"devin",
|
|
836
848
|
"devin-plugin",
|
|
837
849
|
"zcode",
|
|
@@ -866,6 +878,7 @@ const hooksProcessorToolTargetTuple = [
|
|
|
866
878
|
"pi",
|
|
867
879
|
"pool",
|
|
868
880
|
"factorydroid",
|
|
881
|
+
"firebender",
|
|
869
882
|
"goose",
|
|
870
883
|
"hermesagent",
|
|
871
884
|
"kimi-code",
|
|
@@ -1057,6 +1070,57 @@ async function assertWritablePathInsideRoot(params) {
|
|
|
1057
1070
|
existingPath = parentPath;
|
|
1058
1071
|
}
|
|
1059
1072
|
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Thrown by {@link assertPathResolvesInsideRoot} when a symbolic link leads
|
|
1075
|
+
* the target outside the root, or the target is reached through a dangling
|
|
1076
|
+
* link. Any other error it throws comes from the filesystem itself.
|
|
1077
|
+
*/
|
|
1078
|
+
var PathOutsideRootError = class extends Error {
|
|
1079
|
+
constructor(message, options) {
|
|
1080
|
+
super(message, options);
|
|
1081
|
+
this.name = "PathOutsideRootError";
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
/**
|
|
1085
|
+
* Refuse a write or removal target whose real location is outside `rootPath`.
|
|
1086
|
+
*
|
|
1087
|
+
* Unlike {@link assertWritablePathInsideRoot}, a symbolic link on the way is
|
|
1088
|
+
* allowed as long as it resolves inside the root: projects deliberately link a
|
|
1089
|
+
* skill directory to a shared one next to it. What this stops is a link that
|
|
1090
|
+
* leads elsewhere — a cloned repository committing `.rulesync/skills/x ->
|
|
1091
|
+
* ~/.ssh`, say — which a textual check such as {@link checkPathTraversal}
|
|
1092
|
+
* cannot see. The deepest existing ancestor of `targetPath` is resolved, and
|
|
1093
|
+
* an existing target that is itself a link is resolved too.
|
|
1094
|
+
*/
|
|
1095
|
+
async function assertPathResolvesInsideRoot(params) {
|
|
1096
|
+
const { rootPath, targetPath } = params;
|
|
1097
|
+
let realRoot;
|
|
1098
|
+
try {
|
|
1099
|
+
realRoot = await (0, node_fs_promises.realpath)(rootPath);
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
if (isFileNotFoundError(error)) {
|
|
1102
|
+
if (await isSymlink(rootPath)) throw new PathOutsideRootError(`Refusing to write through a dangling symbolic link: ${rootPath}.`, { cause: error });
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
throw error;
|
|
1106
|
+
}
|
|
1107
|
+
let existingPath = (0, node_path.resolve)(targetPath);
|
|
1108
|
+
while (true) {
|
|
1109
|
+
let realExisting;
|
|
1110
|
+
try {
|
|
1111
|
+
realExisting = await (0, node_fs_promises.realpath)(existingPath);
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
1114
|
+
if (await isSymlink(existingPath)) throw new PathOutsideRootError(`Refusing to write through a dangling symbolic link: ${targetPath}.`, { cause: error });
|
|
1115
|
+
const parentPath = (0, node_path.dirname)(existingPath);
|
|
1116
|
+
if (parentPath === existingPath) throw error;
|
|
1117
|
+
existingPath = parentPath;
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
if (pathEscapesRoot((0, node_path.relative)(realRoot, realExisting))) throw new PathOutsideRootError(`Path resolves outside ${rootPath} through a symbolic link: ${targetPath}.`);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1060
1124
|
async function assertTreeContainsNoSymlinks(dirPath) {
|
|
1061
1125
|
for (const entry of await (0, node_fs_promises.readdir)(dirPath, { withFileTypes: true })) {
|
|
1062
1126
|
const entryPath = (0, node_path.join)(dirPath, entry.name);
|
|
@@ -3780,6 +3844,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
|
|
|
3780
3844
|
".devin/config.json",
|
|
3781
3845
|
".dsh/cordis.patch.yml",
|
|
3782
3846
|
".factory/settings.json",
|
|
3847
|
+
"firebender.json",
|
|
3848
|
+
".firebender/firebender.json",
|
|
3783
3849
|
".grok/config.toml",
|
|
3784
3850
|
".grok/sandbox.toml",
|
|
3785
3851
|
".letta/settings.json",
|
|
@@ -4241,14 +4307,103 @@ function stringifyFrontmatter(body, frontmatter, options) {
|
|
|
4241
4307
|
} } });
|
|
4242
4308
|
return gray_matter.default.stringify(file, cleanFrontmatter);
|
|
4243
4309
|
}
|
|
4310
|
+
/**
|
|
4311
|
+
* Thrown in place of gray-matter's built-in JavaScript engine.
|
|
4312
|
+
*
|
|
4313
|
+
* gray-matter picks the parser from the text after the opening fence, so a
|
|
4314
|
+
* file that opens with `---js` or `---javascript` has its frontmatter handed
|
|
4315
|
+
* to `eval()`. Frontmatter here comes from third-party skills, fetched sources
|
|
4316
|
+
* and untrusted repositories, so that would run arbitrary code on the machine
|
|
4317
|
+
* of whoever generates or imports it. Every engine name that resolves to code
|
|
4318
|
+
* execution is mapped to this refusal; {@link assertYamlFrontmatterLanguage}
|
|
4319
|
+
* refuses those fences earlier still, so this is the second line of defense.
|
|
4320
|
+
*/
|
|
4321
|
+
function refuseExecutableFrontmatter() {
|
|
4322
|
+
throw new Error("Frontmatter written in an executable language (JavaScript / CoffeeScript) is not supported; use YAML");
|
|
4323
|
+
}
|
|
4324
|
+
const REFUSED_FRONTMATTER_ENGINE = {
|
|
4325
|
+
parse: refuseExecutableFrontmatter,
|
|
4326
|
+
stringify: refuseExecutableFrontmatter
|
|
4327
|
+
};
|
|
4328
|
+
/**
|
|
4329
|
+
* Engines passed to every gray-matter parse. Supplying them merges over
|
|
4330
|
+
* gray-matter's defaults, so the JavaScript engine (and the CoffeeScript names
|
|
4331
|
+
* a plugin could register) can never be reached through an alias.
|
|
4332
|
+
*
|
|
4333
|
+
* `patches/gray-matter@4.0.3.patch` also deletes the JavaScript engine, but a
|
|
4334
|
+
* pnpm patch only applies inside this repository: gray-matter is an external
|
|
4335
|
+
* dependency of the published package, so an installed rulesync runs the
|
|
4336
|
+
* unpatched gray-matter, and this override plus the language check are what
|
|
4337
|
+
* protect it. Exported for the test that pins that.
|
|
4338
|
+
*/
|
|
4339
|
+
const FRONTMATTER_PARSE_OPTIONS = { engines: {
|
|
4340
|
+
javascript: REFUSED_FRONTMATTER_ENGINE,
|
|
4341
|
+
js: REFUSED_FRONTMATTER_ENGINE,
|
|
4342
|
+
coffee: REFUSED_FRONTMATTER_ENGINE,
|
|
4343
|
+
coffeescript: REFUSED_FRONTMATTER_ENGINE,
|
|
4344
|
+
cson: REFUSED_FRONTMATTER_ENGINE
|
|
4345
|
+
} };
|
|
4346
|
+
/** Frontmatter languages accepted after the opening `---` fence. */
|
|
4347
|
+
const ALLOWED_FRONTMATTER_LANGUAGE_PATTERN = /^(?:ya?ml)?$/i;
|
|
4348
|
+
/**
|
|
4349
|
+
* Read the opening frontmatter fence the way gray-matter does, or return
|
|
4350
|
+
* `undefined` when gray-matter would see no frontmatter at all.
|
|
4351
|
+
*
|
|
4352
|
+
* gray-matter strips one byte order mark, treats `---` as a fence only when a
|
|
4353
|
+
* fourth `-` does not follow, and takes the language from the text between the
|
|
4354
|
+
* fence and the first `\r?\n`, trimmed — dropping the last character when
|
|
4355
|
+
* there is no line break at all. Every check below that has to agree with
|
|
4356
|
+
* gray-matter on where the block is (the language check, the size guard, the
|
|
4357
|
+
* repair pass) goes through this one reading, so none of them can be bypassed
|
|
4358
|
+
* by a fence line gray-matter accepts and a stricter pattern would not (a
|
|
4359
|
+
* stray `\r` before the line break, say).
|
|
4360
|
+
*/
|
|
4361
|
+
function readFrontmatterFence(content) {
|
|
4362
|
+
const offset = content.startsWith("") ? 1 : 0;
|
|
4363
|
+
const text = content.slice(offset);
|
|
4364
|
+
if (!text.startsWith("---") || text.charAt(3) === "-") return;
|
|
4365
|
+
const afterFence = text.slice(3);
|
|
4366
|
+
const lineBreak = /\r?\n/.exec(afterFence);
|
|
4367
|
+
const language = afterFence.slice(0, lineBreak ? lineBreak.index : -1).trim();
|
|
4368
|
+
const lineBreakStart = lineBreak ? offset + 3 + lineBreak.index : void 0;
|
|
4369
|
+
return {
|
|
4370
|
+
language,
|
|
4371
|
+
lineBreakStart,
|
|
4372
|
+
lineEnd: lineBreak && lineBreakStart !== void 0 ? lineBreakStart + lineBreak[0].length : void 0
|
|
4373
|
+
};
|
|
4374
|
+
}
|
|
4375
|
+
/**
|
|
4376
|
+
* Refuse a frontmatter fence that names any language other than YAML.
|
|
4377
|
+
*
|
|
4378
|
+
* Only YAML is ever written by rulesync or by the tools it targets, so anything
|
|
4379
|
+
* else — `---js` above all, whose block gray-matter would `eval()` — is
|
|
4380
|
+
* rejected before gray-matter sees the content.
|
|
4381
|
+
*/
|
|
4382
|
+
function assertYamlFrontmatterLanguage(content) {
|
|
4383
|
+
const fence = readFrontmatterFence(content);
|
|
4384
|
+
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`);
|
|
4385
|
+
}
|
|
4386
|
+
/**
|
|
4387
|
+
* The number of characters gray-matter would hand to the YAML parser as the
|
|
4388
|
+
* frontmatter block. Unlike {@link findFrontmatterBlockBounds}, a missing
|
|
4389
|
+
* closing fence does not make this zero: gray-matter then parses everything
|
|
4390
|
+
* after the fence line, so that is what the size guard has to measure.
|
|
4391
|
+
*/
|
|
4392
|
+
function measureFrontmatterBlockChars(content) {
|
|
4393
|
+
const fence = readFrontmatterFence(content);
|
|
4394
|
+
if (fence?.lineBreakStart === void 0) return 0;
|
|
4395
|
+
const rest = content.slice(fence.lineBreakStart);
|
|
4396
|
+
const closeIndex = rest.indexOf("\n---");
|
|
4397
|
+
return closeIndex === -1 ? rest.length : closeIndex;
|
|
4398
|
+
}
|
|
4244
4399
|
function parseFrontmatter(content, filePath) {
|
|
4245
4400
|
let frontmatter;
|
|
4246
4401
|
let body;
|
|
4247
4402
|
let hasFrontmatter;
|
|
4248
4403
|
try {
|
|
4249
|
-
|
|
4250
|
-
if (
|
|
4251
|
-
const result = (0, gray_matter.default)(content,
|
|
4404
|
+
assertYamlFrontmatterLanguage(content);
|
|
4405
|
+
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)`);
|
|
4406
|
+
const result = (0, gray_matter.default)(content, FRONTMATTER_PARSE_OPTIONS);
|
|
4252
4407
|
frontmatter = deepRemoveNullishObject(result.data);
|
|
4253
4408
|
body = result.content;
|
|
4254
4409
|
hasFrontmatter = result.matter !== "" || content.trimStart().startsWith("---");
|
|
@@ -4307,22 +4462,28 @@ function repairFrontmatterLine(line) {
|
|
|
4307
4462
|
}
|
|
4308
4463
|
/**
|
|
4309
4464
|
* Locate a raw `---`-delimited frontmatter block's bounds within `content`,
|
|
4310
|
-
* without parsing it
|
|
4311
|
-
*
|
|
4312
|
-
*
|
|
4313
|
-
*
|
|
4314
|
-
*
|
|
4315
|
-
*
|
|
4465
|
+
* without parsing it, for the YAML repair pass below and the rule loaders that
|
|
4466
|
+
* pre-process `globs:`. The opening fence is read exactly as gray-matter reads
|
|
4467
|
+
* it ({@link readFrontmatterFence}); an explicit `yaml` / `yml` language is
|
|
4468
|
+
* still YAML, any other language yields no bounds. gray-matter ends the block
|
|
4469
|
+
* at the first `\n---`, with no requirement that the delimiter be alone on its
|
|
4470
|
+
* line, so a stricter pattern here would run past gray-matter's delimiter and
|
|
4471
|
+
* act on text that is really the body. A block with no closing fence has no
|
|
4472
|
+
* bounds; the size guard measures that case separately.
|
|
4316
4473
|
*/
|
|
4317
4474
|
function findFrontmatterBlockBounds(content) {
|
|
4318
|
-
const
|
|
4319
|
-
if (!
|
|
4320
|
-
const blockStart =
|
|
4321
|
-
const closing = /\r?\n---/.exec(content.slice(
|
|
4475
|
+
const fence = readFrontmatterFence(content);
|
|
4476
|
+
if (fence?.lineBreakStart === void 0 || fence.lineEnd === void 0 || !ALLOWED_FRONTMATTER_LANGUAGE_PATTERN.test(fence.language)) return;
|
|
4477
|
+
const blockStart = fence.lineEnd;
|
|
4478
|
+
const closing = /\r?\n---/.exec(content.slice(fence.lineBreakStart));
|
|
4322
4479
|
if (!closing) return;
|
|
4480
|
+
if (closing.index === 0) return {
|
|
4481
|
+
blockStart,
|
|
4482
|
+
blockEnd: blockStart
|
|
4483
|
+
};
|
|
4323
4484
|
return {
|
|
4324
4485
|
blockStart,
|
|
4325
|
-
blockEnd:
|
|
4486
|
+
blockEnd: fence.lineBreakStart + closing.index
|
|
4326
4487
|
};
|
|
4327
4488
|
}
|
|
4328
4489
|
/**
|
|
@@ -4354,8 +4515,9 @@ function repairMalformedFrontmatterYaml(content) {
|
|
|
4354
4515
|
* narrow — one pass, top-level entries only, and the original error is what
|
|
4355
4516
|
* surfaces if it does not help, so a genuinely broken file still fails with the
|
|
4356
4517
|
* message that describes what is actually wrong with it. A file with no closing
|
|
4357
|
-
*
|
|
4358
|
-
*
|
|
4518
|
+
* `---` is not repaired at all, and one whose opening fence names a language
|
|
4519
|
+
* other than YAML is refused outright (an explicit `---yaml` / `---yml` fence
|
|
4520
|
+
* is repaired like a plain one).
|
|
4359
4521
|
*
|
|
4360
4522
|
* @see https://agentskills.io/client-implementation/adding-skills-support
|
|
4361
4523
|
*/
|
|
@@ -4720,6 +4882,29 @@ const CURSOR_HOOK_EVENTS = [
|
|
|
4720
4882
|
"workspaceOpen"
|
|
4721
4883
|
];
|
|
4722
4884
|
/**
|
|
4885
|
+
* Hook events supported by Firebender.
|
|
4886
|
+
*
|
|
4887
|
+
* Firebender's `hooks.json` follows Cursor's layout, and every event it fires
|
|
4888
|
+
* is spelled like its canonical counterpart, so no name mapping is needed.
|
|
4889
|
+
* @see https://docs.firebender.com/multi-agent/hooks
|
|
4890
|
+
*/
|
|
4891
|
+
const FIREBENDER_HOOK_EVENTS = [
|
|
4892
|
+
"sessionStart",
|
|
4893
|
+
"sessionEnd",
|
|
4894
|
+
"subagentStart",
|
|
4895
|
+
"subagentStop",
|
|
4896
|
+
"preToolUse",
|
|
4897
|
+
"postToolUse",
|
|
4898
|
+
"beforeShellExecution",
|
|
4899
|
+
"afterShellExecution",
|
|
4900
|
+
"beforeMCPExecution",
|
|
4901
|
+
"afterMCPExecution",
|
|
4902
|
+
"beforeReadFile",
|
|
4903
|
+
"afterFileEdit",
|
|
4904
|
+
"preCompact",
|
|
4905
|
+
"stop"
|
|
4906
|
+
];
|
|
4907
|
+
/**
|
|
4723
4908
|
* Hook events supported by Claude Code.
|
|
4724
4909
|
*
|
|
4725
4910
|
* Covers the full documented event surface.
|
|
@@ -5000,6 +5185,40 @@ const COPILOT_HOOK_EVENTS = [
|
|
|
5000
5185
|
"userPromptExpansion"
|
|
5001
5186
|
];
|
|
5002
5187
|
/**
|
|
5188
|
+
* The {@link COPILOT_HOOK_EVENTS} whose hook entries honor an optional
|
|
5189
|
+
* `matcher` regex. The hooks reference documents matcher filtering on
|
|
5190
|
+
* `preToolUse` / `postToolUse` / `permissionRequest` (tool name), `preCompact`
|
|
5191
|
+
* (trigger) and `subagentStart` (agent name), plus `notification`; the last
|
|
5192
|
+
* two are not authorable for `copilot`, so they are not listed here. VS Code's
|
|
5193
|
+
* migration guide confirms the Copilot harness "applies documented matcher
|
|
5194
|
+
* rules" (only the legacy Local harness ignores them).
|
|
5195
|
+
*
|
|
5196
|
+
* @see https://docs.github.com/en/copilot/reference/hooks-reference
|
|
5197
|
+
* @see https://code.visualstudio.com/docs/agent-customization/migrate-customizations
|
|
5198
|
+
*/
|
|
5199
|
+
const COPILOT_MATCHER_HOOK_EVENTS = [
|
|
5200
|
+
"preToolUse",
|
|
5201
|
+
"postToolUse",
|
|
5202
|
+
"preCompact",
|
|
5203
|
+
"subagentStart"
|
|
5204
|
+
];
|
|
5205
|
+
/**
|
|
5206
|
+
* The {@link COPILOTCLI_HOOK_EVENTS} whose hook entries honor an optional
|
|
5207
|
+
* `matcher` regex: the shared {@link COPILOT_MATCHER_HOOK_EVENTS} plus the two
|
|
5208
|
+
* CLI-only events, `permissionRequest` (tool name) and `notification`
|
|
5209
|
+
* (notification type).
|
|
5210
|
+
*
|
|
5211
|
+
* Confirmed by the upstream changelog: v1.0.36 (2026-04-24, `preToolUse.matcher`
|
|
5212
|
+
* honored) and v1.0.63 (2026-06-15, "PostToolUse hook matchers (e.g.
|
|
5213
|
+
* `Edit|Write`) are now honored instead of silently dropped").
|
|
5214
|
+
* @see https://docs.github.com/en/copilot/reference/hooks-reference
|
|
5215
|
+
*/
|
|
5216
|
+
const COPILOTCLI_MATCHER_HOOK_EVENTS = [
|
|
5217
|
+
...COPILOT_MATCHER_HOOK_EVENTS,
|
|
5218
|
+
"permissionRequest",
|
|
5219
|
+
"notification"
|
|
5220
|
+
];
|
|
5221
|
+
/**
|
|
5003
5222
|
* Hook events supported by the GitHub Copilot CLI (`copilotcli-hooks.ts`).
|
|
5004
5223
|
*
|
|
5005
5224
|
* The CLI documents a wider event surface than the shared cloud-agent set, so
|
|
@@ -5153,12 +5372,15 @@ const KIRO_IDE_HOOK_EVENTS = [
|
|
|
5153
5372
|
/**
|
|
5154
5373
|
* Hook events supported by Google Antigravity (both the IDE and the CLI).
|
|
5155
5374
|
*
|
|
5156
|
-
* Antigravity exposes a Claude-style hooks surface covering the
|
|
5157
|
-
* tool/model/turn lifecycle events it documents: `
|
|
5158
|
-
* `PreInvocation`, `PostInvocation`, and `Stop`.
|
|
5159
|
-
* (`PreInvocation`/`PostInvocation`)
|
|
5375
|
+
* Antigravity exposes a Claude-style hooks surface covering the six
|
|
5376
|
+
* session/tool/model/turn lifecycle events it documents: `SessionStart`,
|
|
5377
|
+
* `PreToolUse`, `PostToolUse`, `PreInvocation`, `PostInvocation`, and `Stop`.
|
|
5378
|
+
* `SessionStart`, the model-invocation events (`PreInvocation`/`PostInvocation`)
|
|
5379
|
+
* and `Stop` are matcher-less handler lists.
|
|
5380
|
+
* https://antigravity.google/docs/hooks
|
|
5160
5381
|
*/
|
|
5161
5382
|
const ANTIGRAVITY_HOOK_EVENTS = [
|
|
5383
|
+
"sessionStart",
|
|
5162
5384
|
"preToolUse",
|
|
5163
5385
|
"postToolUse",
|
|
5164
5386
|
"preModelInvocation",
|
|
@@ -5931,8 +6153,8 @@ const CONTINUE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANO
|
|
|
5931
6153
|
/**
|
|
5932
6154
|
* Hook events supported by Hermes Agent's native Shell Hooks system.
|
|
5933
6155
|
*
|
|
5934
|
-
* Hermes validates hook events against a fixed `VALID_HOOKS` set —
|
|
5935
|
-
* of v0.
|
|
6156
|
+
* Hermes validates hook events against a fixed `VALID_HOOKS` set — 46 entries as
|
|
6157
|
+
* of v0.22.3 (tag `v0.22.3`); see {@link HERMESAGENT_NATIVE_HOOK_EVENTS} for the
|
|
5936
6158
|
* full list. Only the events with a clean 1:1 canonical equivalent are mapped
|
|
5937
6159
|
* here. All other native events round-trip through `hermesagent.hooks`.
|
|
5938
6160
|
* @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
|
|
@@ -5956,15 +6178,16 @@ const HERMESAGENT_HOOK_EVENTS = [
|
|
|
5956
6178
|
* observers, `gateway_platform_event` and `transform_api_error_classification`.
|
|
5957
6179
|
* Shell hooks are gated on the same set.
|
|
5958
6180
|
*
|
|
5959
|
-
* Verified against the tag `v0.
|
|
6181
|
+
* Verified against the tag `v0.22.3`: `VALID_HOOKS` holds 46 entries, grown
|
|
5960
6182
|
* from 23 at `v2026.8.3` (v0.20.0) and 37 at `v2026.8.16` (v0.20.2) — v0.21
|
|
5961
6183
|
* added `agent_loop_stopped` and `on_room_member_activity`, v0.21.5 added
|
|
5962
|
-
* `pre_auxiliary_call` and `post_auxiliary_call`,
|
|
6184
|
+
* `pre_auxiliary_call` and `post_auxiliary_call`, v0.21.6 added
|
|
5963
6185
|
* `post_gateway_admission`, `on_human_input_request` and
|
|
5964
|
-
* `on_human_input_resolved
|
|
6186
|
+
* `on_human_input_resolved`, and v0.22.3 added `on_session_delete` and
|
|
6187
|
+
* `post_compaction`. This list holds 45 of them — see
|
|
5965
6188
|
* {@link HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS} for the exclusion.
|
|
5966
6189
|
*
|
|
5967
|
-
* @see https://github.com/NousResearch/hermes-agent/blob/v0.
|
|
6190
|
+
* @see https://github.com/NousResearch/hermes-agent/blob/v0.22.3/hermes_cli/plugins.py
|
|
5968
6191
|
*/
|
|
5969
6192
|
const HERMESAGENT_NATIVE_HOOK_EVENTS = [
|
|
5970
6193
|
"pre_tool_call",
|
|
@@ -5988,6 +6211,7 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
|
|
|
5988
6211
|
"on_session_end",
|
|
5989
6212
|
"on_session_finalize",
|
|
5990
6213
|
"on_session_reset",
|
|
6214
|
+
"on_session_delete",
|
|
5991
6215
|
"on_skill_lifecycle",
|
|
5992
6216
|
"subagent_start",
|
|
5993
6217
|
"subagent_stop",
|
|
@@ -6009,7 +6233,8 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
|
|
|
6009
6233
|
"on_kanban_task_updated",
|
|
6010
6234
|
"on_kanban_dispatch_tick",
|
|
6011
6235
|
"gateway_platform_event",
|
|
6012
|
-
"pre_command"
|
|
6236
|
+
"pre_command",
|
|
6237
|
+
"post_compaction"
|
|
6013
6238
|
];
|
|
6014
6239
|
/**
|
|
6015
6240
|
* `VALID_HOOKS` entries a SHELL hook may not register for. `VALID_HOOKS` doubles
|
|
@@ -6021,8 +6246,8 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
|
|
|
6021
6246
|
* rulesync only ever writes shell hooks, so these are excluded from
|
|
6022
6247
|
* {@link HERMESAGENT_NATIVE_HOOK_EVENTS} and warned about with their own message.
|
|
6023
6248
|
*
|
|
6024
|
-
* @see https://github.com/NousResearch/hermes-agent/blob/v0.
|
|
6025
|
-
* @see https://github.com/NousResearch/hermes-agent/blob/v0.
|
|
6249
|
+
* @see https://github.com/NousResearch/hermes-agent/blob/v0.22.3/hermes_cli/plugins.py — `SHELL_UNSUPPORTED_HOOKS`
|
|
6250
|
+
* @see https://github.com/NousResearch/hermes-agent/blob/v0.22.3/agent/shell_hooks.py — `_parse_hooks_block`
|
|
6026
6251
|
*/
|
|
6027
6252
|
const HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS = ["transform_api_error_classification"];
|
|
6028
6253
|
/**
|
|
@@ -6067,6 +6292,7 @@ const HooksConfigSchema = zod_mini.z.looseObject({
|
|
|
6067
6292
|
version: zod_mini.z.optional(zod_mini.z.number()),
|
|
6068
6293
|
hooks: canonicalHooksRecordSchema,
|
|
6069
6294
|
cursor: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6295
|
+
firebender: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6070
6296
|
claudecode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6071
6297
|
copilot: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6072
6298
|
copilotcli: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
@@ -6203,10 +6429,11 @@ const CANONICAL_TO_AUGMENTCODE_EVENT_NAMES = {
|
|
|
6203
6429
|
const AUGMENTCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_AUGMENTCODE_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
6204
6430
|
/**
|
|
6205
6431
|
* Map canonical camelCase event names to Antigravity PascalCase.
|
|
6206
|
-
* Antigravity uses the same PascalCase names as Claude for its tool/turn events,
|
|
6432
|
+
* Antigravity uses the same PascalCase names as Claude for its session/tool/turn events,
|
|
6207
6433
|
* plus `PreInvocation`/`PostInvocation` for the model-invocation lifecycle.
|
|
6208
6434
|
*/
|
|
6209
6435
|
const CANONICAL_TO_ANTIGRAVITY_EVENT_NAMES = {
|
|
6436
|
+
sessionStart: "SessionStart",
|
|
6210
6437
|
preToolUse: "PreToolUse",
|
|
6211
6438
|
postToolUse: "PostToolUse",
|
|
6212
6439
|
preModelInvocation: "PreInvocation",
|
|
@@ -7170,7 +7397,7 @@ const McpServerSchema = zod_mini.z.looseObject({
|
|
|
7170
7397
|
crushSessionless: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
7171
7398
|
crushChannelEnabled: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
7172
7399
|
crushChannelReply: zod_mini.z.optional(CrushChannelReplySchema),
|
|
7173
|
-
zcodeOauth: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
7400
|
+
zcodeOauth: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
7174
7401
|
poolAllow: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
7175
7402
|
headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
7176
7403
|
/**
|
|
@@ -7225,14 +7452,19 @@ const toolScopedMcpSchema = zod_mini.z.looseObject({ mcpServers: zod_mini.z.opti
|
|
|
7225
7452
|
/**
|
|
7226
7453
|
* Kimi Code's tool-scoped block also carries the `[mcp]` defaults from its
|
|
7227
7454
|
* shared user config: connect and per-tool-call timeouts that apply to *every*
|
|
7228
|
-
* MCP server, including ones rulesync did not write.
|
|
7229
|
-
* `
|
|
7455
|
+
* MCP server, including ones rulesync did not write, and (Kimi Code 2.3.0)
|
|
7456
|
+
* `defaultDeferred`, which loads every server's tools on demand unless the
|
|
7457
|
+
* server opts out. Per-server `startupTimeoutMs` / `toolTimeoutMs` / `deferred`
|
|
7458
|
+
* in `mcpServers` still win. `defaultDeferred` only takes effect when Kimi's
|
|
7459
|
+
* experimental `tool-select` flag is on, and is captured when a server connects
|
|
7460
|
+
* (MoonshotAI/kimi-code#4210).
|
|
7230
7461
|
*
|
|
7231
7462
|
* @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#mcp
|
|
7232
7463
|
*/
|
|
7233
7464
|
const kimiCodeScopedMcpSchema = zod_mini.z.extend(toolScopedMcpSchema, {
|
|
7234
7465
|
startupTimeoutMs: zod_mini.z.optional(zod_mini.z.number()),
|
|
7235
|
-
toolTimeoutMs: zod_mini.z.optional(zod_mini.z.number())
|
|
7466
|
+
toolTimeoutMs: zod_mini.z.optional(zod_mini.z.number()),
|
|
7467
|
+
defaultDeferred: zod_mini.z.optional(zod_mini.z.boolean())
|
|
7236
7468
|
});
|
|
7237
7469
|
/**
|
|
7238
7470
|
* Qwen Code's tool-scoped block also carries the server-level allow/deny lists
|
|
@@ -7272,6 +7504,7 @@ const RulesyncMcpFileSchema = zod_mini.z.looseObject({
|
|
|
7272
7504
|
devin: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7273
7505
|
dsh: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7274
7506
|
factorydroid: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7507
|
+
firebender: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7275
7508
|
gitlabduo: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7276
7509
|
goose: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7277
7510
|
grokcli: zod_mini.z.optional(toolScopedMcpSchema),
|
|
@@ -7296,6 +7529,7 @@ const RulesyncMcpFileSchema = zod_mini.z.looseObject({
|
|
|
7296
7529
|
vibe: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7297
7530
|
warp: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7298
7531
|
warpcli: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7532
|
+
xum: zod_mini.z.optional(toolScopedMcpSchema),
|
|
7299
7533
|
zed: zod_mini.z.optional(toolScopedMcpSchema)
|
|
7300
7534
|
});
|
|
7301
7535
|
/**
|
|
@@ -7999,13 +8233,16 @@ const CopilotPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
7999
8233
|
});
|
|
8000
8234
|
/**
|
|
8001
8235
|
* oh-my-pi's `tools.approvalMode` values: the tool tiers it auto-approves
|
|
8002
|
-
* before prompting. `always-ask` auto-approves only read-only tools,
|
|
8003
|
-
*
|
|
8236
|
+
* before prompting. `always-ask` auto-approves only read-only tools,
|
|
8237
|
+
* `always-ask-cached` (Ask and remember) also prompts before file reads and
|
|
8238
|
+
* lets a prompt remember the granted file or folder, `write` also
|
|
8239
|
+
* auto-approves workspace writes, and `yolo` (oh-my-pi's default) every tier.
|
|
8004
8240
|
*
|
|
8005
8241
|
* @see https://github.com/can1357/oh-my-pi/blob/main/docs/approval-mode.md
|
|
8006
8242
|
*/
|
|
8007
8243
|
const OMP_APPROVAL_MODES = [
|
|
8008
8244
|
"always-ask",
|
|
8245
|
+
"always-ask-cached",
|
|
8009
8246
|
"write",
|
|
8010
8247
|
"yolo"
|
|
8011
8248
|
];
|
|
@@ -8026,10 +8263,13 @@ const KimiCodePermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
8026
8263
|
])),
|
|
8027
8264
|
/**
|
|
8028
8265
|
* Kimi's `[permission] dangerous_command_guard` switch (Kimi Code 0.40.0,
|
|
8029
|
-
* default `true`). `false` turns off
|
|
8030
|
-
*
|
|
8266
|
+
* default `true`). Since Kimi Code 2.3.0, `false` turns off every built-in
|
|
8267
|
+
* guard confirmation policy: dangerous commands (`rm -rf`, `shutdown` and the
|
|
8268
|
+
* like), sensitive-file access (`.env`, `id_rsa`, ...) and git control-path
|
|
8269
|
+
* access. Before 2.3.0 it only covered dangerous commands.
|
|
8031
8270
|
*
|
|
8032
8271
|
* @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#permission
|
|
8272
|
+
* @see https://github.com/MoonshotAI/kimi-code/pull/4250
|
|
8033
8273
|
*/
|
|
8034
8274
|
dangerousCommandGuard: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
8035
8275
|
rules: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.looseObject({
|
|
@@ -8389,6 +8629,8 @@ const TabninePermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
8389
8629
|
* { "allowDynamicBash": true, "rawAllow": ["Bash=pnpm test"] }
|
|
8390
8630
|
* @example
|
|
8391
8631
|
* { "mode": "deny" }
|
|
8632
|
+
* @example
|
|
8633
|
+
* { "tools": { "disabled": ["web_fetch"] }, "askOnExternalOrigin": true }
|
|
8392
8634
|
*/
|
|
8393
8635
|
const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
8394
8636
|
permission: zod_mini.z.optional(ToolScopedPermissionSchema),
|
|
@@ -8402,7 +8644,9 @@ const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
8402
8644
|
"ask",
|
|
8403
8645
|
"allow",
|
|
8404
8646
|
"deny"
|
|
8405
|
-
]))
|
|
8647
|
+
])),
|
|
8648
|
+
askOnExternalOrigin: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
8649
|
+
tools: zod_mini.z.optional(zod_mini.z.strictObject({ disabled: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) }))
|
|
8406
8650
|
});
|
|
8407
8651
|
/**
|
|
8408
8652
|
* Tool-scoped override block for Qoder. `outputStyle` is written to the
|
|
@@ -11216,6 +11460,15 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
11216
11460
|
compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
|
|
11217
11461
|
metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
11218
11462
|
})),
|
|
11463
|
+
xum: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
11464
|
+
"disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
11465
|
+
advertise: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
11466
|
+
"user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
|
|
11467
|
+
"argument-hint": zod_mini.z.optional(zod_mini.z.string()),
|
|
11468
|
+
when_to_use: zod_mini.z.optional(zod_mini.z.string()),
|
|
11469
|
+
"when-to-use": zod_mini.z.optional(zod_mini.z.string()),
|
|
11470
|
+
paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
|
|
11471
|
+
})),
|
|
11219
11472
|
zcode: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
11220
11473
|
when_to_use: zod_mini.z.optional(zod_mini.z.string()),
|
|
11221
11474
|
license: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -11494,7 +11747,7 @@ async function getLocalSkillDirNames(sourceTree) {
|
|
|
11494
11747
|
* The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
|
|
11495
11748
|
* default that applies to every tool supporting the flag (claudecode, codebuddy, copilot,
|
|
11496
11749
|
* copilotcli, crush, cursor, zed, pi, omp, qwencode, vibe, grokcli, factorydroid, dsh, reasonix,
|
|
11497
|
-
* commandcode, lettacode, opencode, mimocode). Each tool's own section may override that
|
|
11750
|
+
* commandcode, lettacode, opencode, mimocode, xum). Each tool's own section may override that
|
|
11498
11751
|
* default with a per-target value. A defined section value (including `false`)
|
|
11499
11752
|
* always wins over the root default.
|
|
11500
11753
|
*
|
|
@@ -11514,7 +11767,7 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
|
|
|
11514
11767
|
* The rulesync skill frontmatter exposes a root-level `user-invocable` default
|
|
11515
11768
|
* that applies to every tool supporting the flag (claudecode, codebuddy, copilot,
|
|
11516
11769
|
* copilotcli, crush, cursor, qwencode, vibe, grokcli, factorydroid, dsh, reasonix, commandcode,
|
|
11517
|
-
* lettacode). Each tool's own section may override that default with a
|
|
11770
|
+
* lettacode, xum). Each tool's own section may override that default with a
|
|
11518
11771
|
* per-target value. A defined section value (including `false`) always wins
|
|
11519
11772
|
* over the root default.
|
|
11520
11773
|
*
|
|
@@ -13091,6 +13344,35 @@ const CRUSH_CONFIG_DECLARATION = {
|
|
|
13091
13344
|
}
|
|
13092
13345
|
};
|
|
13093
13346
|
/**
|
|
13347
|
+
* Firebender's `firebender.json`, shared by the project (`<project>/firebender.json`)
|
|
13348
|
+
* and personal (`~/.firebender/firebender.json`) spellings so a policy edit
|
|
13349
|
+
* lands on both. The file also carries `rules`, `commands`, `mcpEnvFile`,
|
|
13350
|
+
* `worktrees` and `useCursorRules` keys rulesync does not own, so every writer
|
|
13351
|
+
* refuses to read-modify-write a file it could not parse. Each feature owns
|
|
13352
|
+
* one whole top-level key: `ignore` (recomputed from `.rulesync/.aiignore`),
|
|
13353
|
+
* `mcpServers` (the complete server map) and `agents` (recomputed from the
|
|
13354
|
+
* existing list, with the entries for rulesync's own agent directory replaced).
|
|
13355
|
+
* https://docs.firebender.com/api-reference/syntax
|
|
13356
|
+
*/
|
|
13357
|
+
const FIREBENDER_CONFIG_DECLARATION = {
|
|
13358
|
+
format: "json",
|
|
13359
|
+
invalidRootPolicy: "error",
|
|
13360
|
+
features: {
|
|
13361
|
+
ignore: {
|
|
13362
|
+
kind: "replace-owned-keys",
|
|
13363
|
+
ownedKeys: ["ignore"]
|
|
13364
|
+
},
|
|
13365
|
+
subagents: {
|
|
13366
|
+
kind: "replace-owned-keys",
|
|
13367
|
+
ownedKeys: ["agents"]
|
|
13368
|
+
},
|
|
13369
|
+
mcp: {
|
|
13370
|
+
kind: "replace-owned-keys",
|
|
13371
|
+
ownedKeys: ["mcpServers"]
|
|
13372
|
+
}
|
|
13373
|
+
}
|
|
13374
|
+
};
|
|
13375
|
+
/**
|
|
13094
13376
|
* What the two Claude Code settings files have in common: both are plain JSON,
|
|
13095
13377
|
* and both validate against the one published schema, which the gateway
|
|
13096
13378
|
* points every file it writes at. `.claude/settings.local.json` is the same
|
|
@@ -13330,6 +13612,15 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13330
13612
|
ownedKeys: ["servers"]
|
|
13331
13613
|
} }
|
|
13332
13614
|
},
|
|
13615
|
+
".xum/mcp.jsonc": {
|
|
13616
|
+
format: "jsonc",
|
|
13617
|
+
invalidRootPolicy: "error",
|
|
13618
|
+
jsoncParseErrors: "error",
|
|
13619
|
+
features: { mcp: {
|
|
13620
|
+
kind: "replace-owned-keys",
|
|
13621
|
+
ownedKeys: ["servers"]
|
|
13622
|
+
} }
|
|
13623
|
+
},
|
|
13333
13624
|
".qwen/settings.json": {
|
|
13334
13625
|
format: "json",
|
|
13335
13626
|
features: {
|
|
@@ -13417,6 +13708,8 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13417
13708
|
".zcode/cli/config.json": ZCODE_CONFIG_DECLARATION,
|
|
13418
13709
|
"crush.json": CRUSH_CONFIG_DECLARATION,
|
|
13419
13710
|
".config/crush/crush.json": CRUSH_CONFIG_DECLARATION,
|
|
13711
|
+
"firebender.json": FIREBENDER_CONFIG_DECLARATION,
|
|
13712
|
+
".firebender/firebender.json": FIREBENDER_CONFIG_DECLARATION,
|
|
13420
13713
|
".bob/settings.json": {
|
|
13421
13714
|
format: "json",
|
|
13422
13715
|
invalidRootPolicy: "error",
|
|
@@ -13789,7 +14082,8 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13789
14082
|
ownedKeys: [
|
|
13790
14083
|
"permissions",
|
|
13791
14084
|
"sandbox",
|
|
13792
|
-
"agent"
|
|
14085
|
+
"agent",
|
|
14086
|
+
"tools"
|
|
13793
14087
|
]
|
|
13794
14088
|
}
|
|
13795
14089
|
}
|
|
@@ -13810,7 +14104,8 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13810
14104
|
ownedKeys: [
|
|
13811
14105
|
"permissions",
|
|
13812
14106
|
"sandbox",
|
|
13813
|
-
"agent"
|
|
14107
|
+
"agent",
|
|
14108
|
+
"tools"
|
|
13814
14109
|
]
|
|
13815
14110
|
}
|
|
13816
14111
|
}
|
|
@@ -20251,6 +20546,178 @@ var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
|
|
|
20251
20546
|
}
|
|
20252
20547
|
};
|
|
20253
20548
|
//#endregion
|
|
20549
|
+
//#region src/constants/firebender-paths.ts
|
|
20550
|
+
/**
|
|
20551
|
+
* Firebender (the coding agent plugin for Android Studio and other JetBrains
|
|
20552
|
+
* IDEs) keeps project configuration under `.firebender/` plus a project-root
|
|
20553
|
+
* `firebender.json`, and user configuration under `~/.firebender/` (including
|
|
20554
|
+
* `~/.firebender/firebender.json`).
|
|
20555
|
+
*
|
|
20556
|
+
* @see https://docs.firebender.com/api-reference/syntax
|
|
20557
|
+
* @see https://docs.firebender.com/api-reference/rules
|
|
20558
|
+
* @see https://docs.firebender.com/api-reference/commands
|
|
20559
|
+
* @see https://docs.firebender.com/api-reference/agents
|
|
20560
|
+
* @see https://docs.firebender.com/api-reference/skills
|
|
20561
|
+
* @see https://docs.firebender.com/api-reference/agents-md
|
|
20562
|
+
* @see https://docs.firebender.com/multi-agent/hooks
|
|
20563
|
+
*/
|
|
20564
|
+
const FIREBENDER_DIR = ".firebender";
|
|
20565
|
+
/**
|
|
20566
|
+
* `firebender.json`: at the project root for project scope, and under
|
|
20567
|
+
* `~/.firebender/` for the personal config. It is shared by the ignore
|
|
20568
|
+
* (`ignore`), MCP (`mcpServers`) and subagents (`agents`) features.
|
|
20569
|
+
*/
|
|
20570
|
+
const FIREBENDER_CONFIG_FILE_NAME = "firebender.json";
|
|
20571
|
+
/** `.firebender/AGENTS.md`, the first file Firebender looks for in both scopes. */
|
|
20572
|
+
const FIREBENDER_AGENTS_MD_FILE_NAME = "AGENTS.md";
|
|
20573
|
+
(0, node_path.join)(FIREBENDER_DIR, "rules");
|
|
20574
|
+
/** Commands: `.firebender/commands/*.mdc` (project) and `~/.firebender/commands/*.mdc` (global). */
|
|
20575
|
+
const FIREBENDER_COMMANDS_DIR_PATH = (0, node_path.join)(FIREBENDER_DIR, "commands");
|
|
20576
|
+
/** Agents: Markdown files registered through the `agents` array of `firebender.json`. */
|
|
20577
|
+
const FIREBENDER_AGENTS_DIR_NAME = "agents";
|
|
20578
|
+
const FIREBENDER_AGENTS_DIR_PATH = (0, node_path.join)(FIREBENDER_DIR, FIREBENDER_AGENTS_DIR_NAME);
|
|
20579
|
+
/** Skills: `.firebender/skills/<name>/SKILL.md` (project) and `~/.firebender/skills/` (global). */
|
|
20580
|
+
const FIREBENDER_SKILLS_DIR_PATH = (0, node_path.join)(FIREBENDER_DIR, "skills");
|
|
20581
|
+
/** Hooks: `.firebender/hooks.json` (project) and `~/.firebender/hooks.json` (global). */
|
|
20582
|
+
const FIREBENDER_HOOKS_FILE_NAME = "hooks.json";
|
|
20583
|
+
/**
|
|
20584
|
+
* The directory holding `firebender.json` for a scope: the project root, or
|
|
20585
|
+
* `~/.firebender/` for the personal config.
|
|
20586
|
+
*/
|
|
20587
|
+
function getFirebenderConfigDir({ global }) {
|
|
20588
|
+
return global ? FIREBENDER_DIR : ".";
|
|
20589
|
+
}
|
|
20590
|
+
//#endregion
|
|
20591
|
+
//#region src/features/commands/firebender-command.ts
|
|
20592
|
+
/**
|
|
20593
|
+
* Frontmatter of a Firebender command. Every field is optional: `name`
|
|
20594
|
+
* defaults to the file name, `model` is `default`, `quick` or a model ID, and
|
|
20595
|
+
* `mode` is one of `auto`, `read`, `write` or `composer`. Unknown keys are
|
|
20596
|
+
* preserved.
|
|
20597
|
+
*
|
|
20598
|
+
* @see https://docs.firebender.com/api-reference/commands
|
|
20599
|
+
*/
|
|
20600
|
+
const FirebenderCommandFrontmatterSchema = zod_mini.z.looseObject({
|
|
20601
|
+
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
20602
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
20603
|
+
model: zod_mini.z.optional(zod_mini.z.string()),
|
|
20604
|
+
mode: zod_mini.z.optional(zod_mini.z.string())
|
|
20605
|
+
});
|
|
20606
|
+
const FIREBENDER_COMMAND_EXTENSION = ".mdc";
|
|
20607
|
+
/**
|
|
20608
|
+
* Command generator for Firebender: one `.mdc` file per command under
|
|
20609
|
+
* `.firebender/commands/` (project) and `~/.firebender/commands/` (global),
|
|
20610
|
+
* with optional `name` / `description` / `model` / `mode` frontmatter. The
|
|
20611
|
+
* canonical `description` is written as is; `name`, `model` and `mode` come
|
|
20612
|
+
* from the `firebender:` section of the rulesync command.
|
|
20613
|
+
*
|
|
20614
|
+
* @see https://docs.firebender.com/api-reference/commands
|
|
20615
|
+
*/
|
|
20616
|
+
var FirebenderCommand = class FirebenderCommand extends ToolCommand {
|
|
20617
|
+
frontmatter;
|
|
20618
|
+
body;
|
|
20619
|
+
constructor({ frontmatter, body, ...rest }) {
|
|
20620
|
+
if (rest.validate) {
|
|
20621
|
+
const result = FirebenderCommandFrontmatterSchema.safeParse(frontmatter);
|
|
20622
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
20623
|
+
}
|
|
20624
|
+
super({
|
|
20625
|
+
...rest,
|
|
20626
|
+
fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true })
|
|
20627
|
+
});
|
|
20628
|
+
this.frontmatter = frontmatter;
|
|
20629
|
+
this.body = body;
|
|
20630
|
+
}
|
|
20631
|
+
static getSettablePaths(_options = {}) {
|
|
20632
|
+
return { relativeDirPath: FIREBENDER_COMMANDS_DIR_PATH };
|
|
20633
|
+
}
|
|
20634
|
+
getBody() {
|
|
20635
|
+
return this.body;
|
|
20636
|
+
}
|
|
20637
|
+
getFrontmatter() {
|
|
20638
|
+
return this.frontmatter;
|
|
20639
|
+
}
|
|
20640
|
+
toRulesyncCommand() {
|
|
20641
|
+
const { description, ...restFields } = this.frontmatter;
|
|
20642
|
+
const rulesyncFrontmatter = {
|
|
20643
|
+
targets: ["*"],
|
|
20644
|
+
description,
|
|
20645
|
+
...Object.keys(restFields).length > 0 && { firebender: restFields }
|
|
20646
|
+
};
|
|
20647
|
+
return new RulesyncCommand({
|
|
20648
|
+
outputRoot: ".",
|
|
20649
|
+
frontmatter: rulesyncFrontmatter,
|
|
20650
|
+
body: this.body,
|
|
20651
|
+
relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
|
|
20652
|
+
relativeFilePath: this.relativeFilePath.replace(/\.mdc$/, ".md"),
|
|
20653
|
+
fileContent: stringifyFrontmatter(this.body, rulesyncFrontmatter),
|
|
20654
|
+
validate: true
|
|
20655
|
+
});
|
|
20656
|
+
}
|
|
20657
|
+
static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
|
|
20658
|
+
const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
|
|
20659
|
+
const firebenderFields = rulesyncFrontmatter.firebender ?? {};
|
|
20660
|
+
const frontmatter = {
|
|
20661
|
+
...rulesyncFrontmatter.description && { description: rulesyncFrontmatter.description },
|
|
20662
|
+
...firebenderFields
|
|
20663
|
+
};
|
|
20664
|
+
return new FirebenderCommand({
|
|
20665
|
+
outputRoot,
|
|
20666
|
+
frontmatter,
|
|
20667
|
+
body: rulesyncCommand.getBody(),
|
|
20668
|
+
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
20669
|
+
relativeFilePath: rulesyncCommand.getRelativeFilePath().replace(/\.md$/, FIREBENDER_COMMAND_EXTENSION),
|
|
20670
|
+
validate
|
|
20671
|
+
});
|
|
20672
|
+
}
|
|
20673
|
+
validate() {
|
|
20674
|
+
if (!this.frontmatter) return {
|
|
20675
|
+
success: true,
|
|
20676
|
+
error: null
|
|
20677
|
+
};
|
|
20678
|
+
const result = FirebenderCommandFrontmatterSchema.safeParse(this.frontmatter);
|
|
20679
|
+
if (result.success) return {
|
|
20680
|
+
success: true,
|
|
20681
|
+
error: null
|
|
20682
|
+
};
|
|
20683
|
+
return {
|
|
20684
|
+
success: false,
|
|
20685
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
20686
|
+
};
|
|
20687
|
+
}
|
|
20688
|
+
static isTargetedByRulesyncCommand(rulesyncCommand) {
|
|
20689
|
+
return this.isTargetedByRulesyncCommandDefault({
|
|
20690
|
+
rulesyncCommand,
|
|
20691
|
+
toolTarget: "firebender"
|
|
20692
|
+
});
|
|
20693
|
+
}
|
|
20694
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
20695
|
+
const paths = this.getSettablePaths({ global });
|
|
20696
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
20697
|
+
const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
|
|
20698
|
+
const result = FirebenderCommandFrontmatterSchema.safeParse(frontmatter);
|
|
20699
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
20700
|
+
return new FirebenderCommand({
|
|
20701
|
+
outputRoot,
|
|
20702
|
+
relativeDirPath: paths.relativeDirPath,
|
|
20703
|
+
relativeFilePath,
|
|
20704
|
+
frontmatter: result.data,
|
|
20705
|
+
body: body.trim(),
|
|
20706
|
+
validate
|
|
20707
|
+
});
|
|
20708
|
+
}
|
|
20709
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
20710
|
+
return new FirebenderCommand({
|
|
20711
|
+
outputRoot,
|
|
20712
|
+
relativeDirPath,
|
|
20713
|
+
relativeFilePath,
|
|
20714
|
+
frontmatter: {},
|
|
20715
|
+
body: "",
|
|
20716
|
+
validate: false
|
|
20717
|
+
});
|
|
20718
|
+
}
|
|
20719
|
+
};
|
|
20720
|
+
//#endregion
|
|
20254
20721
|
//#region src/features/commands/gitlabduo-command.ts
|
|
20255
20722
|
/**
|
|
20256
20723
|
* GitLab Duo CLI custom slash commands are Markdown files named after the
|
|
@@ -20843,6 +21310,60 @@ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
|
|
|
20843
21310
|
}
|
|
20844
21311
|
};
|
|
20845
21312
|
//#endregion
|
|
21313
|
+
//#region src/features/shared/hermes-plugin-commands.ts
|
|
21314
|
+
/**
|
|
21315
|
+
* Normalize a name into a Hermes Agent plugin slash command name.
|
|
21316
|
+
*
|
|
21317
|
+
* `ctx.register_command()` lowercases the name, and Hermes' messaging gateway
|
|
21318
|
+
* looks plugin commands up with every `_` turned into `-`
|
|
21319
|
+
* (`gateway/run_inbound_hooks.py`), so only a lowercase, hyphenated name is
|
|
21320
|
+
* reachable from every surface.
|
|
21321
|
+
*/
|
|
21322
|
+
function hermesSlashName(value) {
|
|
21323
|
+
return value.toLowerCase().replace(/[ _]/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
|
|
21324
|
+
}
|
|
21325
|
+
/**
|
|
21326
|
+
* Python helpers shared by the generated Hermes command plugins.
|
|
21327
|
+
*
|
|
21328
|
+
* A plugin slash command runs outside any agent turn, so it cannot call
|
|
21329
|
+
* `delegate_task` (which requires a parent agent). Instead the handler queues
|
|
21330
|
+
* its prompt as a user message with `ctx.inject_message()`, passing the calling
|
|
21331
|
+
* session's key: a `session_key` handler keyword on newer Hermes, or
|
|
21332
|
+
* `HERMES_SESSION_KEY` from the session context the gateway binds around the
|
|
21333
|
+
* handler on older releases.
|
|
21334
|
+
*/
|
|
21335
|
+
function getHermesQueueUserMessagePython({ pluginName }) {
|
|
21336
|
+
return `def _session_key(kwargs):
|
|
21337
|
+
session_key = kwargs.get("session_key")
|
|
21338
|
+
if session_key:
|
|
21339
|
+
return session_key
|
|
21340
|
+
try:
|
|
21341
|
+
from gateway.session_context import get_session_env
|
|
21342
|
+
except Exception:
|
|
21343
|
+
return None
|
|
21344
|
+
return get_session_env("HERMES_SESSION_KEY") or None
|
|
21345
|
+
|
|
21346
|
+
|
|
21347
|
+
def _queue_user_message(ctx, message, kwargs, label):
|
|
21348
|
+
"""Queue *message* as the session's next user turn; return an error reply, or None."""
|
|
21349
|
+
session_key = _session_key(kwargs)
|
|
21350
|
+
try:
|
|
21351
|
+
if session_key:
|
|
21352
|
+
queued = ctx.inject_message(message, role="user", session_key=session_key)
|
|
21353
|
+
else:
|
|
21354
|
+
queued = ctx.inject_message(message, role="user")
|
|
21355
|
+
except Exception as error:
|
|
21356
|
+
return f"Could not queue {label}: {error}"
|
|
21357
|
+
if not queued:
|
|
21358
|
+
return (
|
|
21359
|
+
f"Hermes refused to queue {label}: there is no active session, or this plugin "
|
|
21360
|
+
"may not inject messages. In gateway sessions, set "
|
|
21361
|
+
"plugins.entries.${pluginName}.allow_gateway_injection: true in config.yaml."
|
|
21362
|
+
)
|
|
21363
|
+
return None
|
|
21364
|
+
`;
|
|
21365
|
+
}
|
|
21366
|
+
//#endregion
|
|
20846
21367
|
//#region src/features/skills/agentsskills-skill.ts
|
|
20847
21368
|
const AgentsSkillsSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
20848
21369
|
name: zod_mini.z.string(),
|
|
@@ -21325,9 +21846,6 @@ const HermesCommandSpecSchema = zod_mini.z.object({
|
|
|
21325
21846
|
description: zod_mini.z.string(),
|
|
21326
21847
|
prompt: zod_mini.z.string()
|
|
21327
21848
|
});
|
|
21328
|
-
function hermesSlashName(value) {
|
|
21329
|
-
return value.toLowerCase().replace(/[ _]/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
|
|
21330
|
-
}
|
|
21331
21849
|
function getPluginManifestContent$2() {
|
|
21332
21850
|
return [
|
|
21333
21851
|
"name: rulesync-commands",
|
|
@@ -21361,6 +21879,8 @@ def _load_commands():
|
|
|
21361
21879
|
return commands
|
|
21362
21880
|
|
|
21363
21881
|
|
|
21882
|
+
${getHermesQueueUserMessagePython({ pluginName: "rulesync-commands" })}
|
|
21883
|
+
|
|
21364
21884
|
def _register_command(ctx, command):
|
|
21365
21885
|
slug = command.get("slug")
|
|
21366
21886
|
if not slug:
|
|
@@ -21370,31 +21890,22 @@ def _register_command(ctx, command):
|
|
|
21370
21890
|
prompt = command.get("prompt") or ""
|
|
21371
21891
|
|
|
21372
21892
|
def handler(args=None, **kwargs):
|
|
21373
|
-
del kwargs
|
|
21374
21893
|
raw_args = ""
|
|
21375
21894
|
if isinstance(args, dict):
|
|
21376
21895
|
raw_args = args.get("args") or args.get("context") or args.get("prompt") or ""
|
|
21377
21896
|
elif args is not None:
|
|
21378
21897
|
raw_args = str(args)
|
|
21379
21898
|
|
|
21380
|
-
|
|
21899
|
+
parts = [prompt] if prompt else []
|
|
21381
21900
|
if raw_args:
|
|
21382
|
-
|
|
21901
|
+
parts.append(f"User arguments:\\n{raw_args}")
|
|
21383
21902
|
|
|
21384
|
-
#
|
|
21385
|
-
#
|
|
21386
|
-
#
|
|
21387
|
-
|
|
21388
|
-
|
|
21389
|
-
|
|
21390
|
-
"tasks": [
|
|
21391
|
-
{
|
|
21392
|
-
"goal": description,
|
|
21393
|
-
"context": "\\n\\n".join(context_parts),
|
|
21394
|
-
},
|
|
21395
|
-
],
|
|
21396
|
-
},
|
|
21397
|
-
)
|
|
21903
|
+
# A slash command runs outside any agent turn, so calling delegate_task
|
|
21904
|
+
# here fails with "delegate_task requires a parent agent context".
|
|
21905
|
+
# Queue the command prompt as the session's next user message instead.
|
|
21906
|
+
label = f"the /{slug} command"
|
|
21907
|
+
error = _queue_user_message(ctx, "\\n\\n".join(parts) or description, kwargs, label)
|
|
21908
|
+
return error or f"Queued {label}."
|
|
21398
21909
|
|
|
21399
21910
|
ctx.register_command(slug, handler, description)
|
|
21400
21911
|
|
|
@@ -23719,6 +24230,7 @@ var QwencodeCommand = class QwencodeCommand extends ToolCommand {
|
|
|
23719
24230
|
const REASONIX_PROJECT_MCP_FILE_NAME = "reasonix.toml";
|
|
23720
24231
|
const REASONIX_GLOBAL_DIR = ".reasonix";
|
|
23721
24232
|
const REASONIX_GLOBAL_MCP_FILE_NAME = "config.toml";
|
|
24233
|
+
const REASONIX_NESTED_PROJECT_CONFIG_RELATIVE_FILE_PATH = (0, node_path.join)(".reasonix", REASONIX_PROJECT_MCP_FILE_NAME);
|
|
23722
24234
|
const REASONIX_PROJECT_PERMISSIONS_FILE_NAME = REASONIX_PROJECT_MCP_FILE_NAME;
|
|
23723
24235
|
const REASONIX_GLOBAL_PERMISSIONS_FILE_NAME = REASONIX_GLOBAL_MCP_FILE_NAME;
|
|
23724
24236
|
const REASONIX_DIR = REASONIX_GLOBAL_DIR;
|
|
@@ -25266,6 +25778,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
|
|
|
25266
25778
|
supportsSubdirectory: false
|
|
25267
25779
|
}
|
|
25268
25780
|
}],
|
|
25781
|
+
["firebender", {
|
|
25782
|
+
class: FirebenderCommand,
|
|
25783
|
+
meta: {
|
|
25784
|
+
extension: "mdc",
|
|
25785
|
+
supportsProject: true,
|
|
25786
|
+
supportsGlobal: true,
|
|
25787
|
+
isSimulated: false,
|
|
25788
|
+
supportsSubdirectory: false
|
|
25789
|
+
}
|
|
25790
|
+
}],
|
|
25269
25791
|
["goose", {
|
|
25270
25792
|
class: GooseCommand,
|
|
25271
25793
|
meta: {
|
|
@@ -28056,25 +28578,65 @@ function toolHooksToCanonical({ hooks, converterConfig, logger }) {
|
|
|
28056
28578
|
//#endregion
|
|
28057
28579
|
//#region src/features/hooks/antigravity-hooks.ts
|
|
28058
28580
|
/**
|
|
28059
|
-
*
|
|
28060
|
-
* `
|
|
28061
|
-
|
|
28062
|
-
|
|
28063
|
-
|
|
28581
|
+
* Canonical events whose Antigravity counterparts (`SessionStart`,
|
|
28582
|
+
* `PreInvocation`, `PostInvocation`, `Stop`) take no matcher.
|
|
28583
|
+
*/
|
|
28584
|
+
const ANTIGRAVITY_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set([
|
|
28585
|
+
"sessionStart",
|
|
28586
|
+
"preModelInvocation",
|
|
28587
|
+
"postModelInvocation",
|
|
28588
|
+
"stop"
|
|
28589
|
+
]);
|
|
28590
|
+
/**
|
|
28591
|
+
* Antigravity event names whose value is a flat list of handlers
|
|
28592
|
+
* (`"Stop": [ { "type": "command", "command": "..." } ]`) rather than a list
|
|
28593
|
+
* of `{ "matcher", "hooks": [...] }` groups. Antigravity's loader rejects a
|
|
28594
|
+
* wrapped group under these events ("command hook must specify 'command'")
|
|
28595
|
+
* and then skips the whole `hooks.json`.
|
|
28596
|
+
* https://antigravity.google/docs/hooks
|
|
28597
|
+
*/
|
|
28598
|
+
const ANTIGRAVITY_FLAT_HANDLER_EVENTS = new Set([...ANTIGRAVITY_NO_MATCHER_EVENTS].map((event) => lookupOwn({
|
|
28599
|
+
record: CANONICAL_TO_ANTIGRAVITY_EVENT_NAMES,
|
|
28600
|
+
key: event
|
|
28601
|
+
}) ?? event));
|
|
28602
|
+
/**
|
|
28603
|
+
* Antigravity uses a Claude-Code-like `hooks.json`. The `PreToolUse` /
|
|
28604
|
+
* `PostToolUse` events use the matcher-based group shape, while `SessionStart`,
|
|
28605
|
+
* `PreInvocation`, `PostInvocation` and `Stop` are flat handler lists (see
|
|
28606
|
+
* {@link toAntigravityEventMap}). `projectDirVar` is empty because Antigravity
|
|
28607
|
+
* does not document a project-directory variable for commands.
|
|
28064
28608
|
*/
|
|
28065
28609
|
const ANTIGRAVITY_CONVERTER_CONFIG = {
|
|
28066
28610
|
supportedEvents: ANTIGRAVITY_HOOK_EVENTS,
|
|
28067
28611
|
canonicalToToolEventNames: CANONICAL_TO_ANTIGRAVITY_EVENT_NAMES,
|
|
28068
28612
|
toolToCanonicalEventNames: ANTIGRAVITY_TO_CANONICAL_EVENT_NAMES,
|
|
28069
28613
|
projectDirVar: "",
|
|
28070
|
-
noMatcherEvents:
|
|
28071
|
-
"preModelInvocation",
|
|
28072
|
-
"postModelInvocation",
|
|
28073
|
-
"stop"
|
|
28074
|
-
]),
|
|
28614
|
+
noMatcherEvents: ANTIGRAVITY_NO_MATCHER_EVENTS,
|
|
28075
28615
|
supportedHookTypes: /* @__PURE__ */ new Set(["command"])
|
|
28076
28616
|
};
|
|
28077
28617
|
/**
|
|
28618
|
+
* Turn the shared converter's matcher-group output into Antigravity's shape:
|
|
28619
|
+
* the matcher-less events get their handlers directly under the event key.
|
|
28620
|
+
*/
|
|
28621
|
+
function toAntigravityEventMap(eventMap) {
|
|
28622
|
+
return Object.fromEntries(Object.entries(eventMap).map(([event, entries]) => {
|
|
28623
|
+
if (!ANTIGRAVITY_FLAT_HANDLER_EVENTS.has(event)) return [event, entries];
|
|
28624
|
+
return [event, entries.flatMap((entry) => isPlainObject$1(entry) && Array.isArray(entry.hooks) ? entry.hooks : [])];
|
|
28625
|
+
}));
|
|
28626
|
+
}
|
|
28627
|
+
/**
|
|
28628
|
+
* Reverse of {@link toAntigravityEventMap} for import: wrap each flat handler
|
|
28629
|
+
* of a matcher-less event in a `{ hooks: [...] }` group so the shared converter
|
|
28630
|
+
* can read it. An entry that is already a group (`{ hooks: [...] }`, as older
|
|
28631
|
+
* rulesync versions wrote) is kept as is.
|
|
28632
|
+
*/
|
|
28633
|
+
function toMatcherGroupEventMap(eventMap) {
|
|
28634
|
+
return Object.fromEntries(Object.entries(eventMap).map(([event, entries]) => {
|
|
28635
|
+
if (!ANTIGRAVITY_FLAT_HANDLER_EVENTS.has(event) || !Array.isArray(entries)) return [event, entries];
|
|
28636
|
+
return [event, entries.map((entry) => isPlainObject$1(entry) && !("hooks" in entry) ? { hooks: [entry] } : entry)];
|
|
28637
|
+
}));
|
|
28638
|
+
}
|
|
28639
|
+
/**
|
|
28078
28640
|
* Antigravity's `hooks.json` is keyed by a named hook whose value holds the
|
|
28079
28641
|
* per-event map (plus an optional `enabled` flag). rulesync writes a single
|
|
28080
28642
|
* generated hook under this stable name.
|
|
@@ -28111,8 +28673,8 @@ function flattenAntigravityHooks(parsed) {
|
|
|
28111
28673
|
* Hooks generator for Google Antigravity (both the IDE and the CLI).
|
|
28112
28674
|
*
|
|
28113
28675
|
* Antigravity writes a dedicated `hooks.json` keyed by a named hook whose
|
|
28114
|
-
* value holds the
|
|
28115
|
-
*
|
|
28676
|
+
* value holds the event map (`{ "<name>": { "<Event>": [...] } }`): matcher
|
|
28677
|
+
* groups for the tool events, flat handler lists for the others. Project and global modes share the
|
|
28116
28678
|
* same shape; only the location differs (`.agents/hooks.json` vs
|
|
28117
28679
|
* `~/.gemini/config/hooks.json`).
|
|
28118
28680
|
*
|
|
@@ -28156,12 +28718,12 @@ var AntigravityHooks = class extends ToolHooks {
|
|
|
28156
28718
|
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
28157
28719
|
const paths = this.getSettablePaths({ global });
|
|
28158
28720
|
const config = rulesyncHooks.getJson();
|
|
28159
|
-
const eventMap = canonicalToToolHooks({
|
|
28721
|
+
const eventMap = toAntigravityEventMap(canonicalToToolHooks({
|
|
28160
28722
|
config,
|
|
28161
28723
|
toolOverrideHooks: config[this.getOverrideKey()]?.hooks,
|
|
28162
28724
|
converterConfig: ANTIGRAVITY_CONVERTER_CONFIG,
|
|
28163
28725
|
logger
|
|
28164
|
-
});
|
|
28726
|
+
}));
|
|
28165
28727
|
const antigravityHooks = Object.keys(eventMap).length > 0 ? { [ANTIGRAVITY_HOOK_NAME]: eventMap } : {};
|
|
28166
28728
|
const fileContent = JSON.stringify(antigravityHooks, null, 2);
|
|
28167
28729
|
return new this({
|
|
@@ -28180,7 +28742,7 @@ var AntigravityHooks = class extends ToolHooks {
|
|
|
28180
28742
|
throw new Error(`Failed to parse Antigravity hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
28181
28743
|
}
|
|
28182
28744
|
const hooks = toolHooksToCanonical({
|
|
28183
|
-
hooks: flattenAntigravityHooks(parsed),
|
|
28745
|
+
hooks: toMatcherGroupEventMap(flattenAntigravityHooks(parsed)),
|
|
28184
28746
|
converterConfig: ANTIGRAVITY_CONVERTER_CONFIG,
|
|
28185
28747
|
logger
|
|
28186
28748
|
});
|
|
@@ -30644,11 +31206,17 @@ var ContinueHooks = class extends SettingsJsonHooks {
|
|
|
30644
31206
|
* entry that uses them instead of a shell field is not mistaken for one
|
|
30645
31207
|
* without a command.
|
|
30646
31208
|
*
|
|
31209
|
+
* An optional `matcher` regex is honored on the events in
|
|
31210
|
+
* {@link COPILOT_MATCHER_HOOK_EVENTS}. The VS Code per-OS override fields come
|
|
31211
|
+
* from the legacy Local hooks reference and are kept for existing files.
|
|
31212
|
+
*
|
|
30647
31213
|
* @see https://docs.github.com/en/copilot/reference/hooks-reference
|
|
31214
|
+
* @see https://code.visualstudio.com/docs/agent-customization/hooks
|
|
30648
31215
|
* @see https://code.visualstudio.com/docs/agents/reference/hooks-reference
|
|
30649
31216
|
*/
|
|
30650
31217
|
const CopilotHookEntrySchema = zod_mini.z.looseObject({
|
|
30651
31218
|
type: zod_mini.z.string(),
|
|
31219
|
+
matcher: zod_mini.z.optional(zod_mini.z.string()),
|
|
30652
31220
|
bash: zod_mini.z.optional(zod_mini.z.string()),
|
|
30653
31221
|
powershell: zod_mini.z.optional(zod_mini.z.string()),
|
|
30654
31222
|
command: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -30682,8 +31250,10 @@ function canonicalToCopilotHooks(config) {
|
|
|
30682
31250
|
...sharedConfigHooks,
|
|
30683
31251
|
...config.copilot?.hooks
|
|
30684
31252
|
};
|
|
31253
|
+
const matcherEvents = new Set(COPILOT_MATCHER_HOOK_EVENTS);
|
|
30685
31254
|
const copilot = {};
|
|
30686
31255
|
for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
|
|
31256
|
+
const matcherSupported = matcherEvents.has(eventName);
|
|
30687
31257
|
const copilotEventName = lookupOwn({
|
|
30688
31258
|
record: CANONICAL_TO_COPILOT_EVENT_NAMES,
|
|
30689
31259
|
key: eventName
|
|
@@ -30691,7 +31261,7 @@ function canonicalToCopilotHooks(config) {
|
|
|
30691
31261
|
const entries = [];
|
|
30692
31262
|
for (const def of definitions) {
|
|
30693
31263
|
const hookType = def.type ?? "command";
|
|
30694
|
-
if (def.matcher) continue;
|
|
31264
|
+
if (def.matcher && !matcherSupported) continue;
|
|
30695
31265
|
if (hookType !== "command") continue;
|
|
30696
31266
|
const command = def.command;
|
|
30697
31267
|
const timeout = def.timeout;
|
|
@@ -30699,6 +31269,7 @@ function canonicalToCopilotHooks(config) {
|
|
|
30699
31269
|
const rest = Object.fromEntries(Object.entries(def).filter(([k]) => !canonicalSchemaKeys.includes(k)));
|
|
30700
31270
|
entries.push({
|
|
30701
31271
|
type: hookType,
|
|
31272
|
+
...def.matcher && { matcher: def.matcher },
|
|
30702
31273
|
...command !== void 0 && command !== null && { [commandField]: command },
|
|
30703
31274
|
...def.env !== void 0 && { env: def.env },
|
|
30704
31275
|
...timeout !== void 0 && timeout !== null && { timeoutSec: timeout },
|
|
@@ -30793,6 +31364,7 @@ function copilotHooksToCanonical(copilotHooks, logger) {
|
|
|
30793
31364
|
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.`);
|
|
30794
31365
|
defs.push({
|
|
30795
31366
|
type: "command",
|
|
31367
|
+
...entry.matcher !== void 0 && entry.matcher !== "" && { matcher: entry.matcher },
|
|
30796
31368
|
...command !== void 0 && { command },
|
|
30797
31369
|
...shell !== void 0 && { shell },
|
|
30798
31370
|
...timeout !== void 0 && { timeout },
|
|
@@ -30936,21 +31508,13 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
|
|
|
30936
31508
|
*/
|
|
30937
31509
|
/**
|
|
30938
31510
|
* Canonical events whose Copilot CLI hook entries honor an optional `matcher`
|
|
30939
|
-
* field (a regex compiled as `^(?:PATTERN)
|
|
30940
|
-
*
|
|
30941
|
-
*
|
|
30942
|
-
*
|
|
30943
|
-
*
|
|
30944
|
-
* @see https://docs.github.com/en/copilot/reference/hooks-reference
|
|
31511
|
+
* field (a regex compiled as `^(?:PATTERN)$`). The hooks reference filters each
|
|
31512
|
+
* on a different field: the tool name for `preToolUse` / `postToolUse` /
|
|
31513
|
+
* `permissionRequest`, `notification_type`, the `preCompact` trigger
|
|
31514
|
+
* (`manual` | `auto`) and the `subagentStart` agent name.
|
|
31515
|
+
* See {@link COPILOTCLI_MATCHER_HOOK_EVENTS}.
|
|
30945
31516
|
*/
|
|
30946
|
-
const COPILOTCLI_MATCHER_EVENTS =
|
|
30947
|
-
"preToolUse",
|
|
30948
|
-
"postToolUse",
|
|
30949
|
-
"notification",
|
|
30950
|
-
"permissionRequest",
|
|
30951
|
-
"preCompact",
|
|
30952
|
-
"subagentStart"
|
|
30953
|
-
]);
|
|
31517
|
+
const COPILOTCLI_MATCHER_EVENTS = new Set(COPILOTCLI_MATCHER_HOOK_EVENTS);
|
|
30954
31518
|
/** Human-readable list of the matcher-aware events, for the drop warning. */
|
|
30955
31519
|
const COPILOTCLI_MATCHER_EVENTS_LABEL = [...COPILOTCLI_MATCHER_EVENTS].join("/");
|
|
30956
31520
|
const CopilotCliHookEntrySchema = zod_mini.z.looseObject({
|
|
@@ -32549,6 +33113,116 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
|
|
|
32549
33113
|
}
|
|
32550
33114
|
};
|
|
32551
33115
|
//#endregion
|
|
33116
|
+
//#region src/features/hooks/firebender-hooks.ts
|
|
33117
|
+
/**
|
|
33118
|
+
* Hooks generator for Firebender.
|
|
33119
|
+
*
|
|
33120
|
+
* Firebender reads `.firebender/hooks.json` (project) and
|
|
33121
|
+
* `~/.firebender/hooks.json` (global). The file follows Cursor's layout — a
|
|
33122
|
+
* `version` plus a `hooks` map from camelCase event names to lists of
|
|
33123
|
+
* `{ command, timeout?, loop_limit?, matcher? }` entries — and every event is
|
|
33124
|
+
* spelled like its canonical counterpart. The per-hook `type` accepts
|
|
33125
|
+
* `"command" | "prompt"`, but the docs never say what a prompt hook does, so
|
|
33126
|
+
* only command hooks are generated (the HooksProcessor warns about the rest).
|
|
33127
|
+
*
|
|
33128
|
+
* @see https://docs.firebender.com/multi-agent/hooks
|
|
33129
|
+
*/
|
|
33130
|
+
var FirebenderHooks = class FirebenderHooks extends ToolHooks {
|
|
33131
|
+
constructor(params) {
|
|
33132
|
+
super({
|
|
33133
|
+
...params,
|
|
33134
|
+
fileContent: params.fileContent ?? "{}"
|
|
33135
|
+
});
|
|
33136
|
+
}
|
|
33137
|
+
static getSettablePaths(_options = {}) {
|
|
33138
|
+
return {
|
|
33139
|
+
relativeDirPath: FIREBENDER_DIR,
|
|
33140
|
+
relativeFilePath: FIREBENDER_HOOKS_FILE_NAME
|
|
33141
|
+
};
|
|
33142
|
+
}
|
|
33143
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
33144
|
+
const paths = FirebenderHooks.getSettablePaths({ global });
|
|
33145
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
|
|
33146
|
+
return new FirebenderHooks({
|
|
33147
|
+
outputRoot,
|
|
33148
|
+
relativeDirPath: paths.relativeDirPath,
|
|
33149
|
+
relativeFilePath: paths.relativeFilePath,
|
|
33150
|
+
fileContent,
|
|
33151
|
+
validate
|
|
33152
|
+
});
|
|
33153
|
+
}
|
|
33154
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
|
|
33155
|
+
const config = rulesyncHooks.getJson();
|
|
33156
|
+
const supportedEvents = new Set(FIREBENDER_HOOK_EVENTS);
|
|
33157
|
+
const sharedHooks = {};
|
|
33158
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (supportedEvents.has(event)) sharedHooks[event] = defs;
|
|
33159
|
+
const mergedHooks = {
|
|
33160
|
+
...sharedHooks,
|
|
33161
|
+
...config.firebender?.hooks
|
|
33162
|
+
};
|
|
33163
|
+
const firebenderHooks = {};
|
|
33164
|
+
for (const [eventName, defs] of Object.entries(mergedHooks)) {
|
|
33165
|
+
const mappedDefs = defs.filter((def) => (def.type ?? "command") === "command").filter((def) => typeof def.command === "string" && def.command.length > 0).map((def) => ({
|
|
33166
|
+
command: def.command,
|
|
33167
|
+
...def.timeout !== void 0 && def.timeout !== null && { timeout: def.timeout },
|
|
33168
|
+
...def.loop_limit !== void 0 && { loop_limit: def.loop_limit },
|
|
33169
|
+
...def.matcher !== void 0 && def.matcher !== null && { matcher: def.matcher }
|
|
33170
|
+
}));
|
|
33171
|
+
if (mappedDefs.length > 0) firebenderHooks[eventName] = mappedDefs;
|
|
33172
|
+
}
|
|
33173
|
+
const paths = FirebenderHooks.getSettablePaths({ global });
|
|
33174
|
+
const merged = mergeGeneratedHookLists({
|
|
33175
|
+
existingContent: await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "",
|
|
33176
|
+
generatedHooks: firebenderHooks,
|
|
33177
|
+
shape: "flat",
|
|
33178
|
+
preserveUnowned,
|
|
33179
|
+
previouslyOwned: preserveUnowned ? parseHooksOwnershipLock(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0,
|
|
33180
|
+
logger
|
|
33181
|
+
});
|
|
33182
|
+
const fileContent = JSON.stringify({
|
|
33183
|
+
version: config.version ?? 1,
|
|
33184
|
+
hooks: merged.hooks
|
|
33185
|
+
}, null, 2);
|
|
33186
|
+
return new FirebenderHooks({
|
|
33187
|
+
outputRoot,
|
|
33188
|
+
relativeDirPath: paths.relativeDirPath,
|
|
33189
|
+
relativeFilePath: paths.relativeFilePath,
|
|
33190
|
+
fileContent,
|
|
33191
|
+
ownedHookRefs: preserveUnowned ? merged.owned : void 0,
|
|
33192
|
+
validate
|
|
33193
|
+
});
|
|
33194
|
+
}
|
|
33195
|
+
static supportsPreserveUnowned() {
|
|
33196
|
+
return true;
|
|
33197
|
+
}
|
|
33198
|
+
static async getAuxiliaryFiles({ toolHooks } = {}) {
|
|
33199
|
+
return toolHooks instanceof FirebenderHooks ? toolHooks.getOwnershipLockFiles() : [];
|
|
33200
|
+
}
|
|
33201
|
+
toRulesyncHooks() {
|
|
33202
|
+
const parsed = JSON.parse(this.getFileContent());
|
|
33203
|
+
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
33204
|
+
hooks: parsed.hooks ?? {},
|
|
33205
|
+
overrideKey: "firebender",
|
|
33206
|
+
version: parsed.version ?? 1
|
|
33207
|
+
}), null, 2) });
|
|
33208
|
+
}
|
|
33209
|
+
validate() {
|
|
33210
|
+
return {
|
|
33211
|
+
success: true,
|
|
33212
|
+
error: null
|
|
33213
|
+
};
|
|
33214
|
+
}
|
|
33215
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
33216
|
+
return new FirebenderHooks({
|
|
33217
|
+
outputRoot,
|
|
33218
|
+
relativeDirPath,
|
|
33219
|
+
relativeFilePath,
|
|
33220
|
+
fileContent: "{}",
|
|
33221
|
+
validate: false
|
|
33222
|
+
});
|
|
33223
|
+
}
|
|
33224
|
+
};
|
|
33225
|
+
//#endregion
|
|
32552
33226
|
//#region src/features/hooks/gitlabduo-hooks.ts
|
|
32553
33227
|
const GITLABDUO_CONVERTER_CONFIG = {
|
|
32554
33228
|
supportedEvents: GITLABDUO_HOOK_EVENTS,
|
|
@@ -32976,7 +33650,7 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
|
|
|
32976
33650
|
for (const [nativeEvent, definitions] of Object.entries(toolOverrideHooks ?? {})) {
|
|
32977
33651
|
if (HERMESAGENT_CANONICAL_EVENTS.has(nativeEvent)) continue;
|
|
32978
33652
|
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.`);
|
|
32979
|
-
else if (!HERMESAGENT_NATIVE_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is not documented by Hermes Agent v0.
|
|
33653
|
+
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.`);
|
|
32980
33654
|
setHermesHookEntries({
|
|
32981
33655
|
result,
|
|
32982
33656
|
event: nativeEvent,
|
|
@@ -36959,6 +37633,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
36959
37633
|
supportedHookTypes: ["command", "prompt"],
|
|
36960
37634
|
supportsMatcher: true
|
|
36961
37635
|
}],
|
|
37636
|
+
["firebender", {
|
|
37637
|
+
class: FirebenderHooks,
|
|
37638
|
+
meta: {
|
|
37639
|
+
supportsProject: true,
|
|
37640
|
+
supportsGlobal: true,
|
|
37641
|
+
supportsImport: true
|
|
37642
|
+
},
|
|
37643
|
+
supportedEvents: FIREBENDER_HOOK_EVENTS,
|
|
37644
|
+
supportedHookTypes: ["command"],
|
|
37645
|
+
supportsMatcher: true
|
|
37646
|
+
}],
|
|
36962
37647
|
["claudecode", {
|
|
36963
37648
|
class: ClaudecodeHooks,
|
|
36964
37649
|
meta: {
|
|
@@ -37055,7 +37740,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
37055
37740
|
},
|
|
37056
37741
|
supportedEvents: COPILOT_HOOK_EVENTS,
|
|
37057
37742
|
supportedHookTypes: ["command"],
|
|
37058
|
-
supportsMatcher:
|
|
37743
|
+
supportsMatcher: true,
|
|
37744
|
+
matcherEvents: COPILOT_MATCHER_HOOK_EVENTS
|
|
37059
37745
|
}],
|
|
37060
37746
|
["copilotcli", {
|
|
37061
37747
|
class: CopilotcliHooks,
|
|
@@ -37070,7 +37756,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
37070
37756
|
"prompt",
|
|
37071
37757
|
"http"
|
|
37072
37758
|
],
|
|
37073
|
-
supportsMatcher:
|
|
37759
|
+
supportsMatcher: true
|
|
37074
37760
|
}],
|
|
37075
37761
|
["kilo", {
|
|
37076
37762
|
class: KiloHooks,
|
|
@@ -37830,6 +38516,67 @@ var AiassistantIgnore = class AiassistantIgnore extends ToolIgnore {
|
|
|
37830
38516
|
}
|
|
37831
38517
|
};
|
|
37832
38518
|
//#endregion
|
|
38519
|
+
//#region src/constants/androidstudio-paths.ts
|
|
38520
|
+
/**
|
|
38521
|
+
* Gemini in Android Studio configuration-layout conventions.
|
|
38522
|
+
*
|
|
38523
|
+
* Only the Android Studio-specific files are owned by the `androidstudio`
|
|
38524
|
+
* target. The shared `AGENTS.md` and `.agents/skills/` surfaces Gemini also
|
|
38525
|
+
* reads belong to the `agentsmd` and `agentsskills` targets.
|
|
38526
|
+
*/
|
|
38527
|
+
const ANDROIDSTUDIO_IGNORE_FILE_NAME = ".aiexclude";
|
|
38528
|
+
const ANDROIDSTUDIO_SKILLS_DIR_PATH = (0, node_path.join)(".android-studio", "skills");
|
|
38529
|
+
//#endregion
|
|
38530
|
+
//#region src/features/ignore/androidstudio-ignore.ts
|
|
38531
|
+
/**
|
|
38532
|
+
* Ignore generator for Gemini in Android Studio.
|
|
38533
|
+
*
|
|
38534
|
+
* Gemini reads `.aiexclude` files (gitignore syntax) anywhere in the project;
|
|
38535
|
+
* each one applies to the files at or below its directory and blocks both the
|
|
38536
|
+
* chat and the editor AI features such as code completion. Rulesync writes the
|
|
38537
|
+
* project-root file only. There is no user-level variant.
|
|
38538
|
+
*
|
|
38539
|
+
* @see https://developer.android.com/studio/gemini/aiexclude
|
|
38540
|
+
*/
|
|
38541
|
+
var AndroidstudioIgnore = class AndroidstudioIgnore extends ToolIgnore {
|
|
38542
|
+
static getSettablePaths() {
|
|
38543
|
+
return {
|
|
38544
|
+
relativeDirPath: ".",
|
|
38545
|
+
relativeFilePath: ANDROIDSTUDIO_IGNORE_FILE_NAME
|
|
38546
|
+
};
|
|
38547
|
+
}
|
|
38548
|
+
toRulesyncIgnore() {
|
|
38549
|
+
return this.toRulesyncIgnoreDefault();
|
|
38550
|
+
}
|
|
38551
|
+
static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
|
|
38552
|
+
return new AndroidstudioIgnore({
|
|
38553
|
+
outputRoot,
|
|
38554
|
+
relativeDirPath: this.getSettablePaths().relativeDirPath,
|
|
38555
|
+
relativeFilePath: this.getSettablePaths().relativeFilePath,
|
|
38556
|
+
fileContent: rulesyncIgnore.getFileContent()
|
|
38557
|
+
});
|
|
38558
|
+
}
|
|
38559
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true }) {
|
|
38560
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath));
|
|
38561
|
+
return new AndroidstudioIgnore({
|
|
38562
|
+
outputRoot,
|
|
38563
|
+
relativeDirPath: this.getSettablePaths().relativeDirPath,
|
|
38564
|
+
relativeFilePath: this.getSettablePaths().relativeFilePath,
|
|
38565
|
+
fileContent,
|
|
38566
|
+
validate
|
|
38567
|
+
});
|
|
38568
|
+
}
|
|
38569
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
38570
|
+
return new AndroidstudioIgnore({
|
|
38571
|
+
outputRoot,
|
|
38572
|
+
relativeDirPath,
|
|
38573
|
+
relativeFilePath,
|
|
38574
|
+
fileContent: "",
|
|
38575
|
+
validate: false
|
|
38576
|
+
});
|
|
38577
|
+
}
|
|
38578
|
+
};
|
|
38579
|
+
//#endregion
|
|
37833
38580
|
//#region src/features/ignore/antigravity-cli-ignore.ts
|
|
37834
38581
|
/**
|
|
37835
38582
|
* Antigravity CLI is the successor to Gemini CLI and is built on the same
|
|
@@ -38491,6 +39238,95 @@ var DevinIgnore = class DevinIgnore extends ToolIgnore {
|
|
|
38491
39238
|
}
|
|
38492
39239
|
};
|
|
38493
39240
|
//#endregion
|
|
39241
|
+
//#region src/features/ignore/firebender-ignore.ts
|
|
39242
|
+
function readIgnorePatterns(fileContent) {
|
|
39243
|
+
const ignore = parseSharedConfig({
|
|
39244
|
+
format: "json",
|
|
39245
|
+
fileContent
|
|
39246
|
+
}).ignore;
|
|
39247
|
+
if (!Array.isArray(ignore)) return [];
|
|
39248
|
+
return ignore.filter((pattern) => typeof pattern === "string");
|
|
39249
|
+
}
|
|
39250
|
+
/**
|
|
39251
|
+
* Ignore generator for Firebender.
|
|
39252
|
+
*
|
|
39253
|
+
* Firebender reads gitignore-style patterns from the `ignore` array of
|
|
39254
|
+
* `firebender.json` (project root) and `~/.firebender/firebender.json`
|
|
39255
|
+
* (personal); the two lists are appended together. The file is shared with the
|
|
39256
|
+
* MCP (`mcpServers`) and subagents (`agents`) features and carries settings
|
|
39257
|
+
* rulesync does not own, so only the `ignore` key is written and the file is
|
|
39258
|
+
* never deleted.
|
|
39259
|
+
*
|
|
39260
|
+
* @see https://docs.firebender.com/api-reference/syntax
|
|
39261
|
+
*/
|
|
39262
|
+
var FirebenderIgnore = class FirebenderIgnore extends ToolIgnore {
|
|
39263
|
+
constructor(params) {
|
|
39264
|
+
super(params);
|
|
39265
|
+
this.patterns = readIgnorePatterns(this.fileContent || "{}");
|
|
39266
|
+
}
|
|
39267
|
+
static getSettablePaths({ global = false } = {}) {
|
|
39268
|
+
return {
|
|
39269
|
+
relativeDirPath: getFirebenderConfigDir({ global }),
|
|
39270
|
+
relativeFilePath: FIREBENDER_CONFIG_FILE_NAME
|
|
39271
|
+
};
|
|
39272
|
+
}
|
|
39273
|
+
/** `firebender.json` is a user-managed config file, so rulesync never deletes it. */
|
|
39274
|
+
isDeletable() {
|
|
39275
|
+
return false;
|
|
39276
|
+
}
|
|
39277
|
+
toRulesyncIgnore() {
|
|
39278
|
+
return new RulesyncIgnore({
|
|
39279
|
+
outputRoot: ".",
|
|
39280
|
+
relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
|
|
39281
|
+
relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
|
|
39282
|
+
fileContent: this.patterns.filter((pattern) => pattern.length > 0).join("\n")
|
|
39283
|
+
});
|
|
39284
|
+
}
|
|
39285
|
+
static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
|
|
39286
|
+
const patterns = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
39287
|
+
const paths = this.getSettablePaths({ global });
|
|
39288
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
39289
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
39290
|
+
const managedPatterns = [...new Set(patterns)];
|
|
39291
|
+
return new FirebenderIgnore({
|
|
39292
|
+
outputRoot,
|
|
39293
|
+
relativeDirPath: paths.relativeDirPath,
|
|
39294
|
+
relativeFilePath: paths.relativeFilePath,
|
|
39295
|
+
fileContent: applySharedConfigPatch({
|
|
39296
|
+
fileKey: sharedConfigFileKey(paths),
|
|
39297
|
+
feature: "ignore",
|
|
39298
|
+
existingContent,
|
|
39299
|
+
patch: { ignore: managedPatterns.length > 0 ? managedPatterns : void 0 },
|
|
39300
|
+
filePath
|
|
39301
|
+
}),
|
|
39302
|
+
validate: true,
|
|
39303
|
+
global
|
|
39304
|
+
});
|
|
39305
|
+
}
|
|
39306
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
39307
|
+
const paths = this.getSettablePaths({ global });
|
|
39308
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
|
|
39309
|
+
return new FirebenderIgnore({
|
|
39310
|
+
outputRoot,
|
|
39311
|
+
relativeDirPath: paths.relativeDirPath,
|
|
39312
|
+
relativeFilePath: paths.relativeFilePath,
|
|
39313
|
+
fileContent,
|
|
39314
|
+
validate,
|
|
39315
|
+
global
|
|
39316
|
+
});
|
|
39317
|
+
}
|
|
39318
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
39319
|
+
return new FirebenderIgnore({
|
|
39320
|
+
outputRoot,
|
|
39321
|
+
relativeDirPath,
|
|
39322
|
+
relativeFilePath,
|
|
39323
|
+
fileContent: "{}",
|
|
39324
|
+
validate: false,
|
|
39325
|
+
global
|
|
39326
|
+
});
|
|
39327
|
+
}
|
|
39328
|
+
};
|
|
39329
|
+
//#endregion
|
|
38494
39330
|
//#region src/features/ignore/grokcli-ignore.ts
|
|
38495
39331
|
/** The base built-in profile the rulesync profile extends (Grok's own default). */
|
|
38496
39332
|
const GROKCLI_SANDBOX_BASE_PROFILE = "workspace";
|
|
@@ -39310,6 +40146,31 @@ const REASONIX_READ_TOOL_NAME = "read_file";
|
|
|
39310
40146
|
* treated as rulesync-owned on generate so the dormant copy is replaced.
|
|
39311
40147
|
*/
|
|
39312
40148
|
const REASONIX_LEGACY_READ_TOOL_NAME = "Read";
|
|
40149
|
+
async function isPathPresent(path) {
|
|
40150
|
+
return await fileExists(path) || await isPresentButUnresolvable(path);
|
|
40151
|
+
}
|
|
40152
|
+
/**
|
|
40153
|
+
* Warns when a project-scope generate is about to create `./reasonix.toml`
|
|
40154
|
+
* next to an existing `.reasonix/reasonix.toml`.
|
|
40155
|
+
*
|
|
40156
|
+
* Since v2.46.0 Reasonix reads `.reasonix/reasonix.toml` as the project config
|
|
40157
|
+
* when `./reasonix.toml` is absent, and never merges the two: once the root
|
|
40158
|
+
* file exists it wins, so every setting in the nested file (permissions,
|
|
40159
|
+
* sandbox, disabled tools, MCP servers, ...) silently stops applying. rulesync
|
|
40160
|
+
* writes only the root file, so it cannot keep the nested one in effect; this
|
|
40161
|
+
* at least names the problem, once per run however many of the features that
|
|
40162
|
+
* write the file (mcp, permissions, ignore) are generated. Like Reasonix, anything at the root path
|
|
40163
|
+
* (including a broken symlink) counts as the root file being there.
|
|
40164
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.47.1/internal/contract/config/paths.go
|
|
40165
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.47.1/docs/CONFIG_PATHS.md
|
|
40166
|
+
*/
|
|
40167
|
+
async function warnIfReasonixNestedProjectConfigShadowed({ outputRoot, global, logger }) {
|
|
40168
|
+
if (global) return;
|
|
40169
|
+
if (await isPathPresent((0, node_path.join)(outputRoot, "reasonix.toml"))) return;
|
|
40170
|
+
if (!await isPathPresent((0, node_path.join)(outputRoot, REASONIX_NESTED_PROJECT_CONFIG_RELATIVE_FILE_PATH))) return;
|
|
40171
|
+
const nested = toPosixPath(REASONIX_NESTED_PROJECT_CONFIG_RELATIVE_FILE_PATH);
|
|
40172
|
+
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.`);
|
|
40173
|
+
}
|
|
39313
40174
|
//#endregion
|
|
39314
40175
|
//#region src/features/ignore/reasonix-ignore.ts
|
|
39315
40176
|
const permissionsTableOf = (document) => toReasonixTable(document.permissions);
|
|
@@ -39393,7 +40254,12 @@ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
|
|
|
39393
40254
|
fileContent: rulesyncPatterns.join("\n")
|
|
39394
40255
|
});
|
|
39395
40256
|
}
|
|
39396
|
-
static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
|
|
40257
|
+
static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false, logger }) {
|
|
40258
|
+
await warnIfReasonixNestedProjectConfigShadowed({
|
|
40259
|
+
outputRoot,
|
|
40260
|
+
global,
|
|
40261
|
+
logger
|
|
40262
|
+
});
|
|
39397
40263
|
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));
|
|
39398
40264
|
const paths = this.getSettablePaths({ global });
|
|
39399
40265
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
@@ -39808,6 +40674,7 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
|
|
|
39808
40674
|
const IgnoreProcessorToolTargetSchema = zod_mini.z.enum(ignoreProcessorToolTargetTuple);
|
|
39809
40675
|
const toolIgnoreFactories = /* @__PURE__ */ new Map([
|
|
39810
40676
|
["aiassistant", { class: AiassistantIgnore }],
|
|
40677
|
+
["androidstudio", { class: AndroidstudioIgnore }],
|
|
39811
40678
|
["antigravity-cli", { class: AntigravityCliIgnore }],
|
|
39812
40679
|
["augmentcode", { class: AugmentcodeIgnore }],
|
|
39813
40680
|
["bob", { class: BobIgnore }],
|
|
@@ -39818,6 +40685,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
|
|
|
39818
40685
|
["continue", { class: ContinueIgnore }],
|
|
39819
40686
|
["crush", { class: CrushIgnore }],
|
|
39820
40687
|
["cursor", { class: CursorIgnore }],
|
|
40688
|
+
["firebender", { class: FirebenderIgnore }],
|
|
39821
40689
|
["grokcli", { class: GrokcliIgnore }],
|
|
39822
40690
|
["hermesagent", { class: HermesagentIgnore }],
|
|
39823
40691
|
["junie", { class: JunieIgnore }],
|
|
@@ -39842,6 +40710,7 @@ const ignoreProcessorToolTargets = [...toolIgnoreFactories.keys()];
|
|
|
39842
40710
|
const ignoreProcessorGlobalToolTargets = [
|
|
39843
40711
|
"continue",
|
|
39844
40712
|
"devin",
|
|
40713
|
+
"firebender",
|
|
39845
40714
|
"grokcli",
|
|
39846
40715
|
"kilo",
|
|
39847
40716
|
"kiro",
|
|
@@ -39968,7 +40837,8 @@ var IgnoreProcessor = class extends FeatureProcessor {
|
|
|
39968
40837
|
outputRoot: this.outputRoot,
|
|
39969
40838
|
rulesyncIgnore,
|
|
39970
40839
|
options: this.featureOptions,
|
|
39971
|
-
global: this.global
|
|
40840
|
+
global: this.global,
|
|
40841
|
+
logger: this.logger
|
|
39972
40842
|
});
|
|
39973
40843
|
const auxiliaryFiles = await factory.class.getAuxiliaryFiles?.({
|
|
39974
40844
|
toolIgnore,
|
|
@@ -40318,13 +41188,31 @@ function renameServerField(servers, from, to) {
|
|
|
40318
41188
|
return [name, Object.fromEntries(Object.entries(config).map(([key, value]) => key === from ? [to, value] : [key, value]))];
|
|
40319
41189
|
}));
|
|
40320
41190
|
}
|
|
41191
|
+
function isEmptyValue(value) {
|
|
41192
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
41193
|
+
return isPlainObject$1(value) && Object.keys(value).length === 0;
|
|
41194
|
+
}
|
|
40321
41195
|
/**
|
|
40322
41196
|
* Antigravity uses `serverUrl` (not the canonical `url`) for HTTP/SSE MCP
|
|
40323
41197
|
* servers. Convert the canonical shape into Antigravity's on the way out, and
|
|
40324
|
-
* back to canonical on the way in
|
|
41198
|
+
* back to canonical on the way in.
|
|
41199
|
+
*
|
|
41200
|
+
* Antigravity documents no `type` field: the transport follows from `command`
|
|
41201
|
+
* (stdio) or `serverUrl` (Streamable HTTP / SSE), and `agy plugin validate`
|
|
41202
|
+
* reports the canonical `type` as an unknown field, so it is not written. A
|
|
41203
|
+
* remote server that carries `command`, `args` or `env` was also observed to be
|
|
41204
|
+
* refused at startup (runtime behavior reported by `agy plugin validate` 1.4.4,
|
|
41205
|
+
* not documented), so an empty `args` / `env` left on a `serverUrl` server is
|
|
41206
|
+
* dropped.
|
|
41207
|
+
* https://antigravity.google/docs/mcp
|
|
40325
41208
|
*/
|
|
40326
41209
|
function toAntigravityMcpServers(servers) {
|
|
40327
|
-
|
|
41210
|
+
const renamed = renameServerField(servers, "url", "serverUrl");
|
|
41211
|
+
return Object.fromEntries(Object.entries(renamed).map(([name, config]) => {
|
|
41212
|
+
if (!isPlainObject$1(config)) return [name, config];
|
|
41213
|
+
const isRemote = typeof config.serverUrl === "string";
|
|
41214
|
+
return [name, Object.fromEntries(Object.entries(config).filter(([key, value]) => key !== "type" && !(isRemote && (key === "args" || key === "env") && isEmptyValue(value))))];
|
|
41215
|
+
}));
|
|
40328
41216
|
}
|
|
40329
41217
|
function toCanonicalMcpServers(servers) {
|
|
40330
41218
|
return renameServerField(servers, "serverUrl", "url");
|
|
@@ -44514,6 +45402,60 @@ function applyDeepagentsCommand({ name, server, converted, logger }) {
|
|
|
44514
45402
|
return true;
|
|
44515
45403
|
}
|
|
44516
45404
|
/**
|
|
45405
|
+
* Translate a canonical `oauth` block into dcode's custom Slack OAuth app
|
|
45406
|
+
* settings, or `undefined` (after warning) when it has to be left out.
|
|
45407
|
+
*
|
|
45408
|
+
* Since deepagents-code 0.1.87, `_validate_slack_oauth_config` accepts `oauth`
|
|
45409
|
+
* only as `{ client_id, client_secret? }` of non-empty strings, and only on a
|
|
45410
|
+
* `slack.com` / `*.slack.com` server with `auth: "oauth"`; anything else makes
|
|
45411
|
+
* dcode skip the whole server. The canonical block usually carries Claude
|
|
45412
|
+
* Code's `{ clientId, callbackPort, ... }` shape, so `clientId` becomes
|
|
45413
|
+
* `client_id` (a native `client_id` wins), `client_secret` passes through, and
|
|
45414
|
+
* every other key is dropped.
|
|
45415
|
+
*
|
|
45416
|
+
* @see https://github.com/langchain-ai/deepagents/blob/deepagents-code%3D%3D0.1.87/libs/code/deepagents_code/mcp_config.py
|
|
45417
|
+
*/
|
|
45418
|
+
function toDeepagentsOauth({ name, oauth, converted, logger }) {
|
|
45419
|
+
if (!isSlackOauthServer(converted)) {
|
|
45420
|
+
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.`);
|
|
45421
|
+
return;
|
|
45422
|
+
}
|
|
45423
|
+
const source = isRecord$1(oauth) ? oauth : {};
|
|
45424
|
+
const result = {};
|
|
45425
|
+
for (const [key, value] of Object.entries(source)) {
|
|
45426
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
45427
|
+
const nativeKey = key === "clientId" ? "client_id" : key;
|
|
45428
|
+
if (nativeKey !== "client_id" && nativeKey !== "client_secret") {
|
|
45429
|
+
logger?.warn(`${TOOL_NAME} MCP: dropping oauth.${key} on "${name}"; deepagents accepts only client_id and client_secret.`);
|
|
45430
|
+
continue;
|
|
45431
|
+
}
|
|
45432
|
+
if (nativeKey !== key && isDeepagentsOauthValue(source[nativeKey])) continue;
|
|
45433
|
+
if (!isDeepagentsOauthValue(value)) {
|
|
45434
|
+
logger?.warn(`${TOOL_NAME} MCP: dropping oauth.${key} on "${name}"; deepagents requires a non-empty string without control characters.`);
|
|
45435
|
+
continue;
|
|
45436
|
+
}
|
|
45437
|
+
result[nativeKey] = value;
|
|
45438
|
+
}
|
|
45439
|
+
if (result.client_id === void 0) {
|
|
45440
|
+
logger?.warn(`${TOOL_NAME} MCP: dropping oauth on "${name}"; deepagents requires oauth.client_id (canonical clientId) and skips a server without it.`);
|
|
45441
|
+
return;
|
|
45442
|
+
}
|
|
45443
|
+
return result;
|
|
45444
|
+
}
|
|
45445
|
+
function isDeepagentsOauthValue(value) {
|
|
45446
|
+
return typeof value === "string" && value.trim() !== "" && !/[\x00-\x1f\x7f]/.test(value);
|
|
45447
|
+
}
|
|
45448
|
+
function isSlackOauthServer(server) {
|
|
45449
|
+
if (server.auth !== "oauth" || typeof server.url !== "string") return false;
|
|
45450
|
+
let host;
|
|
45451
|
+
try {
|
|
45452
|
+
host = new URL(server.url).hostname.toLowerCase();
|
|
45453
|
+
} catch {
|
|
45454
|
+
return false;
|
|
45455
|
+
}
|
|
45456
|
+
return host === "slack.com" || host.endsWith(".slack.com");
|
|
45457
|
+
}
|
|
45458
|
+
/**
|
|
44517
45459
|
* Translate one canonical server into dcode's `.mcp.json` shape, or `null` to
|
|
44518
45460
|
* skip it.
|
|
44519
45461
|
*
|
|
@@ -44536,7 +45478,7 @@ function toDeepagentsServer({ name, server, logger }) {
|
|
|
44536
45478
|
reason: "the WebSocket transport, which deepagents does not support",
|
|
44537
45479
|
logger
|
|
44538
45480
|
});
|
|
44539
|
-
const { enabledTools, disabledTools, type: _type, transport, disabled: _disabled, httpUrl, ...rest } = server;
|
|
45481
|
+
const { enabledTools, disabledTools, type: _type, transport, disabled: _disabled, httpUrl, oauth, ...rest } = server;
|
|
44540
45482
|
const converted = { ...rest };
|
|
44541
45483
|
if (!applyDeepagentsCommand({
|
|
44542
45484
|
name,
|
|
@@ -44551,6 +45493,15 @@ function toDeepagentsServer({ name, server, logger }) {
|
|
|
44551
45493
|
if (transport !== void 0) converted.transport = normalized;
|
|
44552
45494
|
else converted.type = normalized;
|
|
44553
45495
|
}
|
|
45496
|
+
if (oauth !== void 0) {
|
|
45497
|
+
const deepagentsOauth = toDeepagentsOauth({
|
|
45498
|
+
name,
|
|
45499
|
+
oauth,
|
|
45500
|
+
converted,
|
|
45501
|
+
logger
|
|
45502
|
+
});
|
|
45503
|
+
if (deepagentsOauth !== void 0) converted.oauth = deepagentsOauth;
|
|
45504
|
+
}
|
|
44554
45505
|
if (enabledTools !== void 0 && disabledTools !== void 0) return warnAndSkipMcpServer({
|
|
44555
45506
|
toolName: TOOL_NAME,
|
|
44556
45507
|
serverName: name,
|
|
@@ -44577,6 +45528,13 @@ function toRulesyncServer(server) {
|
|
|
44577
45528
|
const converted = { ...rest };
|
|
44578
45529
|
for (const key of ["type", "transport"]) if (converted[key] === "streamable_http" || converted[key] === "streamable-http") converted[key] = "http";
|
|
44579
45530
|
if (Array.isArray(allowedTools)) converted.enabledTools = allowedTools;
|
|
45531
|
+
if (isRecord$1(converted.oauth) && Object.hasOwn(converted.oauth, "client_id")) {
|
|
45532
|
+
const { client_id: clientId, ...oauthRest } = converted.oauth;
|
|
45533
|
+
converted.oauth = Object.hasOwn(oauthRest, "clientId") ? oauthRest : {
|
|
45534
|
+
clientId,
|
|
45535
|
+
...oauthRest
|
|
45536
|
+
};
|
|
45537
|
+
}
|
|
44580
45538
|
return converted;
|
|
44581
45539
|
}
|
|
44582
45540
|
var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
|
|
@@ -45339,6 +46297,171 @@ var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
|
|
|
45339
46297
|
}
|
|
45340
46298
|
};
|
|
45341
46299
|
//#endregion
|
|
46300
|
+
//#region src/features/mcp/firebender-mcp.ts
|
|
46301
|
+
/**
|
|
46302
|
+
* The keys of the canonical server schema. Firebender reads only `command`,
|
|
46303
|
+
* `args`, `env`, `url` and `headers` of them, so the rest are dropped; a key
|
|
46304
|
+
* NOT on this list is unknown to rulesync and passed through untouched, so a
|
|
46305
|
+
* Firebender-native field such as `deferLoading` written in the tool-scoped
|
|
46306
|
+
* `firebender.mcpServers` block (or on a shared server) survives.
|
|
46307
|
+
*/
|
|
46308
|
+
const CANONICAL_MCP_SERVER_KEYS$1 = new Set(Object.keys(McpServerSchema.def.shape));
|
|
46309
|
+
function passthroughNonCanonicalFields$1(serverConfig) {
|
|
46310
|
+
return Object.fromEntries(Object.entries(serverConfig).filter(([key]) => !CANONICAL_MCP_SERVER_KEYS$1.has(key)));
|
|
46311
|
+
}
|
|
46312
|
+
/**
|
|
46313
|
+
* Translate one canonical server into Firebender's `mcpServers` entry, or
|
|
46314
|
+
* return `null` (with a warning) for a server Firebender cannot express. A
|
|
46315
|
+
* stdio server is `{ command, args?, env? }`, a remote one `{ url, headers? }`;
|
|
46316
|
+
* Firebender has no `type` field and no per-server disable switch.
|
|
46317
|
+
*
|
|
46318
|
+
* @see https://docs.firebender.com/api-reference/syntax
|
|
46319
|
+
*/
|
|
46320
|
+
function convertServerToFirebenderFormat({ serverName, serverConfig, logger }) {
|
|
46321
|
+
if (serverConfig.disabled === true) return warnAndSkipMcpServer({
|
|
46322
|
+
toolName: "Firebender",
|
|
46323
|
+
serverName,
|
|
46324
|
+
reason: "`disabled: true`, and Firebender has no per-server disable switch",
|
|
46325
|
+
logger
|
|
46326
|
+
});
|
|
46327
|
+
if (declaresNoTransport(serverConfig)) return warnAndSkipMcpServer({
|
|
46328
|
+
toolName: "Firebender",
|
|
46329
|
+
serverName,
|
|
46330
|
+
reason: "no command or url",
|
|
46331
|
+
logger
|
|
46332
|
+
});
|
|
46333
|
+
const passthrough = passthroughNonCanonicalFields$1(serverConfig);
|
|
46334
|
+
if (isRemoteMcpServer(serverConfig)) {
|
|
46335
|
+
const url = resolveRemoteMcpUrl(serverConfig);
|
|
46336
|
+
if (url === void 0) return warnAndSkipMcpServer({
|
|
46337
|
+
toolName: "Firebender",
|
|
46338
|
+
serverName,
|
|
46339
|
+
reason: "a remote transport but no url",
|
|
46340
|
+
logger
|
|
46341
|
+
});
|
|
46342
|
+
return {
|
|
46343
|
+
...passthrough,
|
|
46344
|
+
url,
|
|
46345
|
+
...serverConfig.headers !== void 0 && { headers: serverConfig.headers }
|
|
46346
|
+
};
|
|
46347
|
+
}
|
|
46348
|
+
const [command, ...args] = resolveLocalMcpCommand(serverConfig);
|
|
46349
|
+
if (command === void 0) return warnAndSkipMcpServer({
|
|
46350
|
+
toolName: "Firebender",
|
|
46351
|
+
serverName,
|
|
46352
|
+
reason: "a local transport but no command",
|
|
46353
|
+
logger
|
|
46354
|
+
});
|
|
46355
|
+
return {
|
|
46356
|
+
...passthrough,
|
|
46357
|
+
command,
|
|
46358
|
+
...args.length > 0 && { args },
|
|
46359
|
+
...serverConfig.env !== void 0 && { env: serverConfig.env }
|
|
46360
|
+
};
|
|
46361
|
+
}
|
|
46362
|
+
/**
|
|
46363
|
+
* MCP generator for Firebender.
|
|
46364
|
+
*
|
|
46365
|
+
* Firebender reads MCP servers from the `mcpServers` key of `firebender.json`
|
|
46366
|
+
* (project root) and `~/.firebender/firebender.json` (personal; it wins on
|
|
46367
|
+
* name clashes). The file is shared with the ignore (`ignore`) and subagents
|
|
46368
|
+
* (`agents`) features and carries settings rulesync does not own, so only the
|
|
46369
|
+
* `mcpServers` key is written and the file is never deleted.
|
|
46370
|
+
*
|
|
46371
|
+
* @see https://docs.firebender.com/api-reference/syntax
|
|
46372
|
+
*/
|
|
46373
|
+
var FirebenderMcp = class FirebenderMcp extends ToolMcp {
|
|
46374
|
+
json;
|
|
46375
|
+
constructor(params) {
|
|
46376
|
+
super(params);
|
|
46377
|
+
this.json = parseSharedConfig({
|
|
46378
|
+
format: "json",
|
|
46379
|
+
fileContent: this.fileContent || "{}"
|
|
46380
|
+
});
|
|
46381
|
+
}
|
|
46382
|
+
getJson() {
|
|
46383
|
+
return this.json;
|
|
46384
|
+
}
|
|
46385
|
+
static getSettablePaths({ global = false } = {}) {
|
|
46386
|
+
return {
|
|
46387
|
+
relativeDirPath: getFirebenderConfigDir({ global }),
|
|
46388
|
+
relativeFilePath: FIREBENDER_CONFIG_FILE_NAME
|
|
46389
|
+
};
|
|
46390
|
+
}
|
|
46391
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
46392
|
+
const paths = this.getSettablePaths({ global });
|
|
46393
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
|
|
46394
|
+
return new FirebenderMcp({
|
|
46395
|
+
outputRoot,
|
|
46396
|
+
relativeDirPath: paths.relativeDirPath,
|
|
46397
|
+
relativeFilePath: paths.relativeFilePath,
|
|
46398
|
+
fileContent,
|
|
46399
|
+
validate,
|
|
46400
|
+
global
|
|
46401
|
+
});
|
|
46402
|
+
}
|
|
46403
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
46404
|
+
const paths = this.getSettablePaths({ global });
|
|
46405
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
46406
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
46407
|
+
const mcpServers = {};
|
|
46408
|
+
for (const [serverName, serverConfig] of Object.entries(rulesyncMcp.getMcpServers())) {
|
|
46409
|
+
const converted = convertServerToFirebenderFormat({
|
|
46410
|
+
serverName,
|
|
46411
|
+
serverConfig,
|
|
46412
|
+
logger
|
|
46413
|
+
});
|
|
46414
|
+
if (converted !== null) mcpServers[serverName] = converted;
|
|
46415
|
+
}
|
|
46416
|
+
return new FirebenderMcp({
|
|
46417
|
+
outputRoot,
|
|
46418
|
+
relativeDirPath: paths.relativeDirPath,
|
|
46419
|
+
relativeFilePath: paths.relativeFilePath,
|
|
46420
|
+
fileContent: applySharedConfigPatch({
|
|
46421
|
+
fileKey: sharedConfigFileKey(paths),
|
|
46422
|
+
feature: "mcp",
|
|
46423
|
+
existingContent,
|
|
46424
|
+
patch: { mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : void 0 },
|
|
46425
|
+
filePath
|
|
46426
|
+
}),
|
|
46427
|
+
validate,
|
|
46428
|
+
global
|
|
46429
|
+
});
|
|
46430
|
+
}
|
|
46431
|
+
toRulesyncMcp() {
|
|
46432
|
+
const servers = this.json.mcpServers;
|
|
46433
|
+
const mcpServers = {};
|
|
46434
|
+
if (isMcpServers(servers)) {
|
|
46435
|
+
for (const [serverName, server] of Object.entries(servers)) if (server !== null && typeof server === "object" && !Array.isArray(server)) mcpServers[serverName] = server;
|
|
46436
|
+
}
|
|
46437
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
46438
|
+
}
|
|
46439
|
+
validate() {
|
|
46440
|
+
return {
|
|
46441
|
+
success: true,
|
|
46442
|
+
error: null
|
|
46443
|
+
};
|
|
46444
|
+
}
|
|
46445
|
+
/**
|
|
46446
|
+
* `firebender.json` is a user-managed file shared with other features (the
|
|
46447
|
+
* ignore feature's `ignore`, the subagents feature's `agents`), so it must
|
|
46448
|
+
* not be deleted.
|
|
46449
|
+
*/
|
|
46450
|
+
isDeletable() {
|
|
46451
|
+
return false;
|
|
46452
|
+
}
|
|
46453
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
46454
|
+
return new FirebenderMcp({
|
|
46455
|
+
outputRoot,
|
|
46456
|
+
relativeDirPath,
|
|
46457
|
+
relativeFilePath,
|
|
46458
|
+
fileContent: "{}",
|
|
46459
|
+
validate: false,
|
|
46460
|
+
global
|
|
46461
|
+
});
|
|
46462
|
+
}
|
|
46463
|
+
};
|
|
46464
|
+
//#endregion
|
|
45342
46465
|
//#region src/features/mcp/gitlabduo-mcp.ts
|
|
45343
46466
|
const CANONICAL_TO_GITLABDUO_TRANSPORT = {
|
|
45344
46467
|
local: "stdio",
|
|
@@ -46144,6 +47267,50 @@ function copyHermesOauth(source) {
|
|
|
46144
47267
|
if (typeof source.cimd === "boolean") oauth.cimd = source.cimd;
|
|
46145
47268
|
return Object.keys(oauth).length > 0 ? oauth : void 0;
|
|
46146
47269
|
}
|
|
47270
|
+
const HERMES_COST_UNIT_MAX_LENGTH = 32;
|
|
47271
|
+
function describeHermesValue(value) {
|
|
47272
|
+
return typeof value === "number" ? String(value) : JSON.stringify(value) ?? String(value);
|
|
47273
|
+
}
|
|
47274
|
+
/**
|
|
47275
|
+
* Copies the per-server MCP cost keys Hermes v0.22.3 reads in
|
|
47276
|
+
* `tools/mcp_tool_costs.py`: `tool_costs` (declared per-tool costs keyed by
|
|
47277
|
+
* MCP tool name; each a number, a `[min, max]` range or `{credits, note}`),
|
|
47278
|
+
* `spend_limit` (per-chat budget for the server's costed tools; `0`/absent =
|
|
47279
|
+
* no limit) and `cost_unit` (label shown with costs; default `credits`).
|
|
47280
|
+
*
|
|
47281
|
+
* A costed call that would exceed `spend_limit` asks first even on a
|
|
47282
|
+
* `trust: full` server, so dropping the key on regenerate would silently
|
|
47283
|
+
* remove the guard. Values Hermes would reject when loading the server are
|
|
47284
|
+
* dropped with a warning instead of being written into a config Hermes then
|
|
47285
|
+
* refuses: `spend_limit` must be a finite non-negative number, `cost_unit` a
|
|
47286
|
+
* non-blank string of at most 32 characters, and `tool_costs` a mapping. The
|
|
47287
|
+
* mapping is rebuilt as an opaque object (so no reference is shared with the
|
|
47288
|
+
* source), with prototype-pollution keys removed at every depth since its
|
|
47289
|
+
* values may themselves be objects.
|
|
47290
|
+
*/
|
|
47291
|
+
function copyHermesCostFields({ source, target, logger }) {
|
|
47292
|
+
let copied = false;
|
|
47293
|
+
const { spend_limit: spendLimit, cost_unit: costUnit, tool_costs: toolCosts } = source;
|
|
47294
|
+
if (spendLimit !== void 0) {
|
|
47295
|
+
if (typeof spendLimit === "number" && Number.isFinite(spendLimit) && spendLimit >= 0) {
|
|
47296
|
+
target.spend_limit = spendLimit;
|
|
47297
|
+
copied = true;
|
|
47298
|
+
} else logger?.warn(`Hermes MCP: dropping spend_limit ${describeHermesValue(spendLimit)}; Hermes requires a non-negative number.`);
|
|
47299
|
+
}
|
|
47300
|
+
if (costUnit !== void 0) {
|
|
47301
|
+
if (typeof costUnit === "string" && costUnit.trim() !== "" && costUnit.length <= HERMES_COST_UNIT_MAX_LENGTH) {
|
|
47302
|
+
target.cost_unit = costUnit;
|
|
47303
|
+
copied = true;
|
|
47304
|
+
} 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.`);
|
|
47305
|
+
}
|
|
47306
|
+
if (toolCosts !== void 0) {
|
|
47307
|
+
if (isPlainObject$1(toolCosts)) {
|
|
47308
|
+
target.tool_costs = omitPrototypePollutionKeysDeep(toolCosts);
|
|
47309
|
+
copied = true;
|
|
47310
|
+
} else logger?.warn("Hermes MCP: dropping tool_costs; Hermes requires a mapping of MCP tool names to costs.");
|
|
47311
|
+
}
|
|
47312
|
+
return copied;
|
|
47313
|
+
}
|
|
46147
47314
|
const HERMES_OPAQUE_MAPPING_KEYS = [
|
|
46148
47315
|
"sampling",
|
|
46149
47316
|
"elicitation",
|
|
@@ -46155,13 +47322,18 @@ const HERMES_OPAQUE_MAPPING_KEYS = [
|
|
|
46155
47322
|
* alias — `auth` (`oauth` for OAuth 2.1/PKCE), mTLS `client_cert` (string PEM
|
|
46156
47323
|
* path, or `[cert, key]`/`[cert, key, password]` list) and `client_key`,
|
|
46157
47324
|
* `connect_timeout` (seconds), `supports_parallel_tool_calls`, `protocol`,
|
|
46158
|
-
* `lazy`, `keepalive_interval`, `elicitation`, `trust`,
|
|
47325
|
+
* `lazy`, `keepalive_interval`, `elicitation`, `trust`, `identity_header`, and
|
|
47326
|
+
* the cost keys (see {@link copyHermesCostFields}) —
|
|
46159
47327
|
* verbatim from `source` to `target`. Field names are identical on both sides
|
|
46160
47328
|
* (the canonical `McpServerSchema` is a `looseObject`), so this serves export
|
|
46161
47329
|
* and import alike. See the Hermes mcp-config-reference.
|
|
46162
47330
|
*/
|
|
46163
|
-
function copyHermesAdvancedFields(source, target) {
|
|
46164
|
-
let copied =
|
|
47331
|
+
function copyHermesAdvancedFields(source, target, logger) {
|
|
47332
|
+
let copied = copyHermesCostFields({
|
|
47333
|
+
source,
|
|
47334
|
+
target,
|
|
47335
|
+
logger
|
|
47336
|
+
});
|
|
46165
47337
|
if (typeof source.auth === "string") {
|
|
46166
47338
|
target.auth = source.auth;
|
|
46167
47339
|
copied = true;
|
|
@@ -46270,7 +47442,7 @@ function applyHermesToolsBlock(hermesTools, server) {
|
|
|
46270
47442
|
* as Hermes's own `transport: sse`, without which Hermes would connect to it
|
|
46271
47443
|
* over Streamable HTTP.
|
|
46272
47444
|
*/
|
|
46273
|
-
function convertServerToHermes(config) {
|
|
47445
|
+
function convertServerToHermes(config, logger) {
|
|
46274
47446
|
const out = {};
|
|
46275
47447
|
const command = config.command;
|
|
46276
47448
|
const url = resolveHermesUrl(config);
|
|
@@ -46293,7 +47465,7 @@ function convertServerToHermes(config) {
|
|
|
46293
47465
|
if (config.disabled === true) out.enabled = false;
|
|
46294
47466
|
const timeout = resolveHermesTimeout(config);
|
|
46295
47467
|
if (timeout !== void 0) out.timeout = timeout;
|
|
46296
|
-
copyHermesAdvancedFields(config, out);
|
|
47468
|
+
copyHermesAdvancedFields(config, out, logger);
|
|
46297
47469
|
const tools = buildHermesToolsBlock(config);
|
|
46298
47470
|
if (Object.keys(tools).length > 0) out.tools = tools;
|
|
46299
47471
|
return out;
|
|
@@ -46301,11 +47473,11 @@ function convertServerToHermes(config) {
|
|
|
46301
47473
|
/**
|
|
46302
47474
|
* Converts rulesync canonical MCP servers into Hermes `mcp_servers:` entries.
|
|
46303
47475
|
*/
|
|
46304
|
-
function convertToHermesFormat(mcpServers) {
|
|
47476
|
+
function convertToHermesFormat(mcpServers, logger) {
|
|
46305
47477
|
const result = {};
|
|
46306
47478
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
46307
47479
|
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
46308
|
-
result[name] = convertServerToHermes(config);
|
|
47480
|
+
result[name] = convertServerToHermes(config, logger);
|
|
46309
47481
|
}
|
|
46310
47482
|
return result;
|
|
46311
47483
|
}
|
|
@@ -46325,7 +47497,7 @@ function mergeHermesMcpServers(config, mcpServers) {
|
|
|
46325
47497
|
* Mirrors {@link convertToHermesFormat}: `enabled: false` maps back to the
|
|
46326
47498
|
* canonical `disabled: true`, and only recognized fields are carried over.
|
|
46327
47499
|
*/
|
|
46328
|
-
function convertFromHermesFormat(mcpServers) {
|
|
47500
|
+
function convertFromHermesFormat(mcpServers, logger) {
|
|
46329
47501
|
const result = {};
|
|
46330
47502
|
const hermesOverrides = {};
|
|
46331
47503
|
for (const [name, config] of Object.entries(mcpServers)) {
|
|
@@ -46342,7 +47514,7 @@ function convertFromHermesFormat(mcpServers) {
|
|
|
46342
47514
|
if (isRecord$1(config.tools)) applyHermesToolsBlock(config.tools, server);
|
|
46343
47515
|
result[name] = server;
|
|
46344
47516
|
const hermesServer = { ...server };
|
|
46345
|
-
if (copyHermesAdvancedFields(config, hermesServer)) hermesOverrides[name] = hermesServer;
|
|
47517
|
+
if (copyHermesAdvancedFields(config, hermesServer, logger)) hermesOverrides[name] = hermesServer;
|
|
46346
47518
|
}
|
|
46347
47519
|
return {
|
|
46348
47520
|
mcpServers: result,
|
|
@@ -46361,8 +47533,10 @@ function convertFromHermesFormat(mcpServers) {
|
|
|
46361
47533
|
*/
|
|
46362
47534
|
var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
46363
47535
|
config;
|
|
46364
|
-
|
|
47536
|
+
logger;
|
|
47537
|
+
constructor({ logger, ...params }) {
|
|
46365
47538
|
super(params);
|
|
47539
|
+
this.logger = logger;
|
|
46366
47540
|
this.config = this.fileContent !== void 0 ? parseSharedConfig({
|
|
46367
47541
|
format: "yaml",
|
|
46368
47542
|
fileContent: this.fileContent
|
|
@@ -46406,7 +47580,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
46406
47580
|
static getExtraSharedWritePaths() {
|
|
46407
47581
|
return getHermesagentSharedConfigWritePaths();
|
|
46408
47582
|
}
|
|
46409
|
-
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
47583
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
|
|
46410
47584
|
if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
|
|
46411
47585
|
const paths = this.getSettablePaths({ global });
|
|
46412
47586
|
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
@@ -46416,17 +47590,18 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
46416
47590
|
relativeFilePath: paths.relativeFilePath,
|
|
46417
47591
|
fileContent,
|
|
46418
47592
|
validate,
|
|
46419
|
-
global
|
|
47593
|
+
global,
|
|
47594
|
+
logger
|
|
46420
47595
|
});
|
|
46421
47596
|
}
|
|
46422
|
-
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
47597
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
46423
47598
|
if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
|
|
46424
47599
|
const paths = this.getSettablePaths({ global });
|
|
46425
47600
|
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
46426
47601
|
const merged = mergeHermesMcpServers(parseSharedConfig({
|
|
46427
47602
|
format: "yaml",
|
|
46428
47603
|
fileContent
|
|
46429
|
-
}), convertToHermesFormat(rulesyncMcp.getMcpServers()));
|
|
47604
|
+
}), convertToHermesFormat(rulesyncMcp.getMcpServers(), logger));
|
|
46430
47605
|
return new HermesagentMcp({
|
|
46431
47606
|
outputRoot,
|
|
46432
47607
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -46438,11 +47613,12 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
|
|
|
46438
47613
|
patch: { mcp_servers: merged.mcp_servers }
|
|
46439
47614
|
}),
|
|
46440
47615
|
validate,
|
|
46441
|
-
global
|
|
47616
|
+
global,
|
|
47617
|
+
logger
|
|
46442
47618
|
});
|
|
46443
47619
|
}
|
|
46444
47620
|
toRulesyncMcp() {
|
|
46445
|
-
const { mcpServers: servers, hermesOverrides } = convertFromHermesFormat(isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
|
|
47621
|
+
const { mcpServers: servers, hermesOverrides } = convertFromHermesFormat(isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {}, this.logger);
|
|
46446
47622
|
return this.toRulesyncMcpDefault({
|
|
46447
47623
|
outputRoot: getHermesagentRulesyncOutputRoot({
|
|
46448
47624
|
nativeOutputRoot: this.outputRoot,
|
|
@@ -47326,14 +48502,15 @@ async function readKimiCodeMcpDefaults({ outputRoot }) {
|
|
|
47326
48502
|
if (!config.parsed) return {};
|
|
47327
48503
|
return {
|
|
47328
48504
|
...typeof config.mcp.startup_timeout_ms === "number" && { startupTimeoutMs: config.mcp.startup_timeout_ms },
|
|
47329
|
-
...typeof config.mcp.tool_timeout_ms === "number" && { toolTimeoutMs: config.mcp.tool_timeout_ms }
|
|
48505
|
+
...typeof config.mcp.tool_timeout_ms === "number" && { toolTimeoutMs: config.mcp.tool_timeout_ms },
|
|
48506
|
+
...typeof config.mcp.default_deferred === "boolean" && { defaultDeferred: config.mcp.default_deferred }
|
|
47330
48507
|
};
|
|
47331
48508
|
}
|
|
47332
48509
|
/**
|
|
47333
48510
|
* Kimi's `[mcp]` section in the shared user config sets the default connect and
|
|
47334
|
-
* per-tool-call timeouts
|
|
47335
|
-
* write. Per-server values in
|
|
47336
|
-
* the
|
|
48511
|
+
* per-tool-call timeouts, and whether tools load on demand, for *every* MCP
|
|
48512
|
+
* server, including ones rulesync did not write. Per-server values in
|
|
48513
|
+
* `mcp.json` still take precedence; this only moves the default.
|
|
47337
48514
|
*
|
|
47338
48515
|
* Written as an auxiliary file so it goes through the normal write phase and
|
|
47339
48516
|
* respects `--dry-run`, and merged through the shared-config gateway so the
|
|
@@ -47341,7 +48518,8 @@ async function readKimiCodeMcpDefaults({ outputRoot }) {
|
|
|
47341
48518
|
*
|
|
47342
48519
|
* The gateway replaces an owned key wholesale, and `mcp` is a table, so the
|
|
47343
48520
|
* section is recomputed from the existing file: authoring only
|
|
47344
|
-
* `startupTimeoutMs` must not delete a hand-written `tool_timeout_ms
|
|
48521
|
+
* `startupTimeoutMs` must not delete a hand-written `tool_timeout_ms` or
|
|
48522
|
+
* `default_deferred`. Same
|
|
47345
48523
|
* approach the `.vibe/config.toml` `tools` writer takes.
|
|
47346
48524
|
*
|
|
47347
48525
|
* @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#mcp
|
|
@@ -47368,7 +48546,8 @@ var KimiCodeMcpConfigToml = class KimiCodeMcpConfigToml extends ToolFile {
|
|
|
47368
48546
|
patch: { mcp: {
|
|
47369
48547
|
...existingSection,
|
|
47370
48548
|
...defaults.startupTimeoutMs !== void 0 && { startup_timeout_ms: defaults.startupTimeoutMs },
|
|
47371
|
-
...defaults.toolTimeoutMs !== void 0 && { tool_timeout_ms: defaults.toolTimeoutMs }
|
|
48549
|
+
...defaults.toolTimeoutMs !== void 0 && { tool_timeout_ms: defaults.toolTimeoutMs },
|
|
48550
|
+
...defaults.defaultDeferred !== void 0 && { default_deferred: defaults.defaultDeferred }
|
|
47372
48551
|
} },
|
|
47373
48552
|
filePath: relativeConfigPath
|
|
47374
48553
|
});
|
|
@@ -47466,18 +48645,20 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
|
|
|
47466
48645
|
if (!isRecord$1(block)) return [];
|
|
47467
48646
|
const startupTimeoutMs = typeof block.startupTimeoutMs === "number" ? block.startupTimeoutMs : void 0;
|
|
47468
48647
|
const toolTimeoutMs = typeof block.toolTimeoutMs === "number" ? block.toolTimeoutMs : void 0;
|
|
47469
|
-
|
|
48648
|
+
const defaultDeferred = typeof block.defaultDeferred === "boolean" ? block.defaultDeferred : void 0;
|
|
48649
|
+
if (startupTimeoutMs === void 0 && toolTimeoutMs === void 0 && defaultDeferred === void 0) return [];
|
|
47470
48650
|
const existing = await readKimiCodeConfig({ outputRoot });
|
|
47471
48651
|
if (!existing.parsed) {
|
|
47472
48652
|
const paths = kimiCodeConfigRelativePath();
|
|
47473
|
-
warnWithFallback(logger, `Skipping the Kimi Code MCP
|
|
48653
|
+
warnWithFallback(logger, `Skipping the Kimi Code MCP [mcp] defaults: ${(0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)} is not valid TOML.`);
|
|
47474
48654
|
return [];
|
|
47475
48655
|
}
|
|
47476
48656
|
return [KimiCodeMcpConfigToml.fromDefaults({
|
|
47477
48657
|
outputRoot,
|
|
47478
48658
|
defaults: {
|
|
47479
48659
|
startupTimeoutMs,
|
|
47480
|
-
toolTimeoutMs
|
|
48660
|
+
toolTimeoutMs,
|
|
48661
|
+
defaultDeferred
|
|
47481
48662
|
},
|
|
47482
48663
|
existing
|
|
47483
48664
|
})];
|
|
@@ -50053,6 +51234,11 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
|
|
|
50053
51234
|
});
|
|
50054
51235
|
}
|
|
50055
51236
|
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
51237
|
+
await warnIfReasonixNestedProjectConfigShadowed({
|
|
51238
|
+
outputRoot,
|
|
51239
|
+
global,
|
|
51240
|
+
logger
|
|
51241
|
+
});
|
|
50056
51242
|
const paths = this.getSettablePaths({ global });
|
|
50057
51243
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
50058
51244
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
@@ -52030,6 +53216,397 @@ var WarpcliMcp = class extends WarpMcp {
|
|
|
52030
53216
|
}
|
|
52031
53217
|
};
|
|
52032
53218
|
//#endregion
|
|
53219
|
+
//#region src/constants/xum-paths.ts
|
|
53220
|
+
/**
|
|
53221
|
+
* Xum configuration-layout conventions.
|
|
53222
|
+
*
|
|
53223
|
+
* Xum (Coder's desktop app and `xum run` CLI for running coding agents in
|
|
53224
|
+
* parallel workspaces, formerly Mux) keeps its project assets under a `.xum/`
|
|
53225
|
+
* directory at the project root and the matching user assets under `~/.xum/`.
|
|
53226
|
+
* Legacy `.mux/` files are still read when the matching `.xum/` file is
|
|
53227
|
+
* missing; rulesync writes only the `.xum/` paths.
|
|
53228
|
+
*
|
|
53229
|
+
* @see https://xum.cdr.dev/reference/mux-compatibility
|
|
53230
|
+
*/
|
|
53231
|
+
const XUM_DIR = ".xum";
|
|
53232
|
+
const XUM_RULE_FILE_NAME = "AGENTS.md";
|
|
53233
|
+
const XUM_LOCAL_RULE_FILE_NAME = "AGENTS.local.md";
|
|
53234
|
+
const XUM_MCP_FILE_NAME = "mcp.jsonc";
|
|
53235
|
+
const XUM_AGENTS_DIR_PATH = (0, node_path.join)(XUM_DIR, "agents");
|
|
53236
|
+
const XUM_SKILLS_DIR_PATH = (0, node_path.join)(XUM_DIR, "skills");
|
|
53237
|
+
//#endregion
|
|
53238
|
+
//#region src/features/mcp/xum-mcp.ts
|
|
53239
|
+
const EMPTY_XUM_MCP_CONFIG = JSON.stringify({ servers: {} }, null, 2);
|
|
53240
|
+
/** Characters that never need quoting in a POSIX shell word. */
|
|
53241
|
+
const SHELL_SAFE_WORD = /^[A-Za-z0-9_\-.,:/@%+=]+$/;
|
|
53242
|
+
/**
|
|
53243
|
+
* A command string free of quotes, escapes, expansions and operators, which
|
|
53244
|
+
* can be split on whitespace back into argv without changing its meaning.
|
|
53245
|
+
*/
|
|
53246
|
+
const SPLIT_SAFE_COMMAND = /^[^'"\\$`|&;<>(){}*?[\]~#!\n]+$/;
|
|
53247
|
+
/** Single-quote one argv element for the POSIX shell Xum runs commands in. */
|
|
53248
|
+
function quoteShellWord(value) {
|
|
53249
|
+
if (SHELL_SAFE_WORD.test(value)) return value;
|
|
53250
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
53251
|
+
}
|
|
53252
|
+
/**
|
|
53253
|
+
* Build the shell command string a Xum stdio server is launched with. A
|
|
53254
|
+
* `command` string is kept verbatim (Xum itself treats it as a raw shell
|
|
53255
|
+
* string, so `npx -y pkg` authored as one string keeps working); an explicit
|
|
53256
|
+
* argv (`command` as an array) and every `args` element are quoted where
|
|
53257
|
+
* needed, so `/opt/my tools/srv` stays one word.
|
|
53258
|
+
*/
|
|
53259
|
+
function buildCommandString(server) {
|
|
53260
|
+
const args = server.args ?? [];
|
|
53261
|
+
if (Array.isArray(server.command)) {
|
|
53262
|
+
const [executable] = server.command;
|
|
53263
|
+
if (executable === void 0 || executable.trim() === "") return void 0;
|
|
53264
|
+
return [...server.command, ...args].map(quoteShellWord).join(" ");
|
|
53265
|
+
}
|
|
53266
|
+
if (typeof server.command !== "string" || server.command.trim() === "") return void 0;
|
|
53267
|
+
return [server.command, ...args.map(quoteShellWord)].join(" ");
|
|
53268
|
+
}
|
|
53269
|
+
/** A name bash accepts on the left of a `NAME=value` assignment. */
|
|
53270
|
+
const SHELL_ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
53271
|
+
/** A canonical `${VAR}` reference whose name bash can expand. */
|
|
53272
|
+
const ENV_VAR_REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
53273
|
+
/**
|
|
53274
|
+
* Quote an env value for a `NAME=value` assignment. Literal text is
|
|
53275
|
+
* single-quoted, so nothing in it is expanded or run, while every canonical
|
|
53276
|
+
* `${VAR}` reference is emitted as `"${VAR}"` so bash still expands it from
|
|
53277
|
+
* the environment Xum starts the server with. The pieces are concatenated into
|
|
53278
|
+
* one shell word: `Bearer ${TOKEN}` becomes `'Bearer '"${TOKEN}"`.
|
|
53279
|
+
*/
|
|
53280
|
+
function quoteEnvValue(value) {
|
|
53281
|
+
if (value === "") return "''";
|
|
53282
|
+
const parts = [];
|
|
53283
|
+
let last = 0;
|
|
53284
|
+
for (const match of value.matchAll(ENV_VAR_REFERENCE)) {
|
|
53285
|
+
if (match.index > last) parts.push(quoteShellWord(value.slice(last, match.index)));
|
|
53286
|
+
parts.push(`"\${${match[1]}}"`);
|
|
53287
|
+
last = match.index + match[0].length;
|
|
53288
|
+
}
|
|
53289
|
+
if (last < value.length) parts.push(quoteShellWord(value.slice(last)));
|
|
53290
|
+
return parts.join("");
|
|
53291
|
+
}
|
|
53292
|
+
/**
|
|
53293
|
+
* Build the `NAME=value ` prefix that sets a stdio server's `env` for its
|
|
53294
|
+
* command only (Xum runs the command string with bash). Returns the prefix
|
|
53295
|
+
* plus the entries left out: names that cannot be bash assignment targets and
|
|
53296
|
+
* entries whose value is not a string.
|
|
53297
|
+
*/
|
|
53298
|
+
function buildEnvPrefix(env) {
|
|
53299
|
+
const assignments = [];
|
|
53300
|
+
const invalidNames = [];
|
|
53301
|
+
const nonStringNames = [];
|
|
53302
|
+
for (const [name, value] of Object.entries(env)) {
|
|
53303
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
53304
|
+
if (!SHELL_ENV_NAME.test(name)) {
|
|
53305
|
+
invalidNames.push(name);
|
|
53306
|
+
continue;
|
|
53307
|
+
}
|
|
53308
|
+
if (typeof value !== "string") {
|
|
53309
|
+
nonStringNames.push(name);
|
|
53310
|
+
continue;
|
|
53311
|
+
}
|
|
53312
|
+
assignments.push(`${name}=${quoteEnvValue(value)}`);
|
|
53313
|
+
}
|
|
53314
|
+
return {
|
|
53315
|
+
prefix: assignments.map((assignment) => `${assignment} `).join(""),
|
|
53316
|
+
invalidNames,
|
|
53317
|
+
nonStringNames
|
|
53318
|
+
};
|
|
53319
|
+
}
|
|
53320
|
+
/** Bash reserved words; a command starting with one cannot take an assignment prefix. */
|
|
53321
|
+
const BASH_RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
53322
|
+
"!",
|
|
53323
|
+
"[[",
|
|
53324
|
+
"]]",
|
|
53325
|
+
"{",
|
|
53326
|
+
"}",
|
|
53327
|
+
"case",
|
|
53328
|
+
"coproc",
|
|
53329
|
+
"do",
|
|
53330
|
+
"done",
|
|
53331
|
+
"elif",
|
|
53332
|
+
"else",
|
|
53333
|
+
"esac",
|
|
53334
|
+
"fi",
|
|
53335
|
+
"for",
|
|
53336
|
+
"function",
|
|
53337
|
+
"if",
|
|
53338
|
+
"in",
|
|
53339
|
+
"select",
|
|
53340
|
+
"then",
|
|
53341
|
+
"time",
|
|
53342
|
+
"until",
|
|
53343
|
+
"while"
|
|
53344
|
+
]);
|
|
53345
|
+
/**
|
|
53346
|
+
* Explain why a `NAME=value` prefix would not reach the server, or return
|
|
53347
|
+
* `undefined` when it would. Bash applies such assignments to the first simple
|
|
53348
|
+
* command only: with a list or pipeline (`cd x && srv`) the server would not
|
|
53349
|
+
* see them, and in front of a group, parenthesized list or reserved word (`( srv )`,
|
|
53350
|
+
* `{ srv; }`, `if ...`) they are a syntax error that keeps the server from
|
|
53351
|
+
* starting. Only the verbatim `command` string is checked for operators,
|
|
53352
|
+
* outside complete single-quoted words; argv elements are quoted by
|
|
53353
|
+
* {@link quoteShellWord}.
|
|
53354
|
+
*/
|
|
53355
|
+
function envPrefixBlocker(server, baseCommand) {
|
|
53356
|
+
if (typeof server.command === "string") {
|
|
53357
|
+
const unquoted = server.command.replace(/'[^']*'/g, "");
|
|
53358
|
+
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";
|
|
53359
|
+
}
|
|
53360
|
+
const [firstWord] = baseCommand.trimStart().split(/\s+/);
|
|
53361
|
+
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`;
|
|
53362
|
+
}
|
|
53363
|
+
/**
|
|
53364
|
+
* One leading `NAME=value` assignment in exactly the shape {@link quoteEnvValue}
|
|
53365
|
+
* writes: bare shell-safe characters, single-quoted text (with the `'\''`
|
|
53366
|
+
* escape) and `"${VAR}"` references, followed by spaces or tabs (a newline
|
|
53367
|
+
* would end the command, so the assignment would not apply to what follows). Bare characters
|
|
53368
|
+
* are matched one at a time, so every alternative starts with a distinct
|
|
53369
|
+
* character and the pattern cannot backtrack exponentially.
|
|
53370
|
+
*/
|
|
53371
|
+
const LEADING_ENV_ASSIGNMENT = /^([A-Za-z_][A-Za-z0-9_]*)=((?:[A-Za-z0-9_\-.,:/@%+=]|'[^']*'|\\'|"\$\{[A-Za-z_][A-Za-z0-9_]*\}")*)[ \t]+/;
|
|
53372
|
+
/** Decode a value written by {@link quoteEnvValue} back into its canonical form. */
|
|
53373
|
+
function unquoteEnvValue(quoted) {
|
|
53374
|
+
return quoted.replace(/'([^']*)'|\\'|"(\$\{[A-Za-z_][A-Za-z0-9_]*\})"/g, (_m, single, ref) => typeof single === "string" ? single : typeof ref === "string" ? ref : "'");
|
|
53375
|
+
}
|
|
53376
|
+
/**
|
|
53377
|
+
* Split leading `NAME=value` assignments off a command string, but only when
|
|
53378
|
+
* every one of them is in the shape rulesync writes and a plain command
|
|
53379
|
+
* follows (see {@link envPrefixBlocker}); anything else (for example a `$(...)` substitution) keeps the whole string as
|
|
53380
|
+
* the command.
|
|
53381
|
+
*/
|
|
53382
|
+
function splitEnvPrefix(command) {
|
|
53383
|
+
const env = {};
|
|
53384
|
+
let rest = command.trimStart();
|
|
53385
|
+
for (;;) {
|
|
53386
|
+
const match = LEADING_ENV_ASSIGNMENT.exec(rest);
|
|
53387
|
+
if (match === null) break;
|
|
53388
|
+
const [whole, name, quoted] = match;
|
|
53389
|
+
if (name === void 0 || quoted === void 0 || PROTOTYPE_POLLUTION_KEYS.has(name)) break;
|
|
53390
|
+
env[name] = unquoteEnvValue(quoted);
|
|
53391
|
+
rest = rest.slice(whole.length);
|
|
53392
|
+
}
|
|
53393
|
+
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 };
|
|
53394
|
+
return {
|
|
53395
|
+
env,
|
|
53396
|
+
rest
|
|
53397
|
+
};
|
|
53398
|
+
}
|
|
53399
|
+
/** Read a command string back into `env` + `command` + `args`. */
|
|
53400
|
+
function parseCommandString(command) {
|
|
53401
|
+
const { env, rest } = splitEnvPrefix(command);
|
|
53402
|
+
return {
|
|
53403
|
+
...env !== void 0 && { env },
|
|
53404
|
+
...splitCommandString(rest)
|
|
53405
|
+
};
|
|
53406
|
+
}
|
|
53407
|
+
/** Split a plain command string back into `command` + `args` when it is safe to. */
|
|
53408
|
+
function splitCommandString(command) {
|
|
53409
|
+
const trimmed = command.trim();
|
|
53410
|
+
if (!SPLIT_SAFE_COMMAND.test(trimmed)) return { command };
|
|
53411
|
+
const [head, ...rest] = trimmed.split(/\s+/);
|
|
53412
|
+
if (head === void 0) return { command };
|
|
53413
|
+
return rest.length > 0 ? {
|
|
53414
|
+
command: head,
|
|
53415
|
+
args: rest
|
|
53416
|
+
} : { command: head };
|
|
53417
|
+
}
|
|
53418
|
+
function resolveXumServerKind(server) {
|
|
53419
|
+
const candidate = server.type ?? server.transport;
|
|
53420
|
+
if (candidate === "stdio" || candidate === "local") return "stdio";
|
|
53421
|
+
if (candidate === "http" || candidate === "streamable-http") return "http";
|
|
53422
|
+
if (candidate === "sse") return "sse";
|
|
53423
|
+
if (candidate !== void 0) return { unsupported: candidate };
|
|
53424
|
+
if (server.command !== void 0) return "stdio";
|
|
53425
|
+
const url = server.url ?? server.httpUrl;
|
|
53426
|
+
if (typeof url === "string") return /^wss?:\/\//i.test(url) ? { unsupported: "ws" } : "auto";
|
|
53427
|
+
}
|
|
53428
|
+
/** Convert one canonical server into a Xum `servers` entry, or `null` to skip it. */
|
|
53429
|
+
function convertServerToXum({ name, server, logger }) {
|
|
53430
|
+
const skip = (reason) => {
|
|
53431
|
+
warnWithFallback(logger, `Xum MCP: skipping ${quoteValueForWarning(name)} because ${reason}.`);
|
|
53432
|
+
return null;
|
|
53433
|
+
};
|
|
53434
|
+
const kind = resolveXumServerKind(server);
|
|
53435
|
+
if (kind === void 0) return skip("it has neither a command nor a URL");
|
|
53436
|
+
if (typeof kind === "object") return skip(`it uses the "${kind.unsupported}" transport, which Xum does not support (only stdio, http and sse)`);
|
|
53437
|
+
const settings = {};
|
|
53438
|
+
if (server.disabled === true) settings.disabled = true;
|
|
53439
|
+
if (isStringArray$1(server.enabledTools)) settings.toolAllowlist = server.enabledTools;
|
|
53440
|
+
if (kind !== "stdio") {
|
|
53441
|
+
const url = server.url ?? server.httpUrl;
|
|
53442
|
+
if (typeof url !== "string" || url === "") return skip("it has no URL");
|
|
53443
|
+
return {
|
|
53444
|
+
...kind !== "auto" && { transport: kind },
|
|
53445
|
+
url,
|
|
53446
|
+
...isPlainObject$1(server.headers) && Object.keys(server.headers).length > 0 && { headers: server.headers },
|
|
53447
|
+
...settings
|
|
53448
|
+
};
|
|
53449
|
+
}
|
|
53450
|
+
const baseCommand = buildCommandString(server);
|
|
53451
|
+
if (baseCommand === void 0) return skip("it has no command");
|
|
53452
|
+
let command = baseCommand;
|
|
53453
|
+
if (isPlainObject$1(server.env) && Object.keys(server.env).length > 0) {
|
|
53454
|
+
const blocker = envPrefixBlocker(server, baseCommand);
|
|
53455
|
+
if (blocker !== void 0) warnWithFallback(logger, `Xum MCP: ${quoteValueForWarning(name)} sets env, but ${blocker}; the server is written without env.`);
|
|
53456
|
+
else {
|
|
53457
|
+
const { prefix, invalidNames, nonStringNames } = buildEnvPrefix(server.env);
|
|
53458
|
+
command = `${prefix}${baseCommand}`;
|
|
53459
|
+
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.`);
|
|
53460
|
+
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.`);
|
|
53461
|
+
}
|
|
53462
|
+
}
|
|
53463
|
+
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).`);
|
|
53464
|
+
return Object.keys(settings).length > 0 ? {
|
|
53465
|
+
command,
|
|
53466
|
+
...settings
|
|
53467
|
+
} : command;
|
|
53468
|
+
}
|
|
53469
|
+
/** Copy only the string values of a header map (secret references have no canonical form). */
|
|
53470
|
+
function copyStringHeaders(value) {
|
|
53471
|
+
if (!isPlainObject$1(value)) return void 0;
|
|
53472
|
+
const result = {};
|
|
53473
|
+
for (const [key, entry] of Object.entries(value)) if (!PROTOTYPE_POLLUTION_KEYS.has(key) && typeof entry === "string") result[key] = entry;
|
|
53474
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
53475
|
+
}
|
|
53476
|
+
/** Read a Xum `servers` map back into the canonical shape. */
|
|
53477
|
+
function convertFromXumFormat(servers) {
|
|
53478
|
+
if (!isPlainObject$1(servers)) return {};
|
|
53479
|
+
const result = {};
|
|
53480
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
53481
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
53482
|
+
if (typeof entry === "string") {
|
|
53483
|
+
result[name] = parseCommandString(entry);
|
|
53484
|
+
continue;
|
|
53485
|
+
}
|
|
53486
|
+
if (!isPlainObject$1(entry)) continue;
|
|
53487
|
+
const server = {};
|
|
53488
|
+
if (typeof entry.url === "string") {
|
|
53489
|
+
if (entry.transport === "http" || entry.transport === "sse") server.type = entry.transport;
|
|
53490
|
+
server.url = entry.url;
|
|
53491
|
+
const headers = copyStringHeaders(entry.headers);
|
|
53492
|
+
if (headers !== void 0) server.headers = headers;
|
|
53493
|
+
} else if (typeof entry.command === "string") Object.assign(server, parseCommandString(entry.command));
|
|
53494
|
+
else continue;
|
|
53495
|
+
if (entry.disabled === true) server.disabled = true;
|
|
53496
|
+
if (isStringArray$1(entry.toolAllowlist)) server.enabledTools = entry.toolAllowlist;
|
|
53497
|
+
result[name] = server;
|
|
53498
|
+
}
|
|
53499
|
+
return result;
|
|
53500
|
+
}
|
|
53501
|
+
/**
|
|
53502
|
+
* Xum MCP configuration.
|
|
53503
|
+
*
|
|
53504
|
+
* Xum reads `~/.xum/mcp.jsonc` (user) and the repo override `.xum/mcp.jsonc`
|
|
53505
|
+
* (project; a server there wins over the user one of the same name). Both are
|
|
53506
|
+
* a JSONC document with a `servers` map:
|
|
53507
|
+
*
|
|
53508
|
+
* - a stdio server is a shell command string run with bash (the object form
|
|
53509
|
+
* `{ command }` carries `disabled` / `toolAllowlist`); `env` is written as
|
|
53510
|
+
* `NAME=value` assignments in front of the command, and `cwd`, which has no
|
|
53511
|
+
* equivalent, is dropped with a warning;
|
|
53512
|
+
* - a remote server is `{ transport: "http" | "sse" | "auto", url, headers }`
|
|
53513
|
+
* (the transport defaults to auto-detection).
|
|
53514
|
+
*
|
|
53515
|
+
* rulesync owns only `servers`; other top-level keys (Xum writes
|
|
53516
|
+
* `enabledPluginServers` into the user file) are preserved. The project file
|
|
53517
|
+
* may be removed by `--delete`; the user file never is.
|
|
53518
|
+
*
|
|
53519
|
+
* @see https://xum.cdr.dev/config/mcp-servers
|
|
53520
|
+
* @see https://github.com/coder/xum/blob/main/src/node/services/mcpConfigService.ts
|
|
53521
|
+
*/
|
|
53522
|
+
var XumMcp = class XumMcp extends ToolMcp {
|
|
53523
|
+
json;
|
|
53524
|
+
constructor(params) {
|
|
53525
|
+
super(params);
|
|
53526
|
+
this.json = parseSharedConfig({
|
|
53527
|
+
format: "jsonc",
|
|
53528
|
+
fileContent: this.fileContent,
|
|
53529
|
+
filePath: (0, node_path.join)(this.relativeDirPath, this.relativeFilePath),
|
|
53530
|
+
invalidRootPolicy: "error",
|
|
53531
|
+
jsoncParseErrors: "error"
|
|
53532
|
+
});
|
|
53533
|
+
}
|
|
53534
|
+
getJson() {
|
|
53535
|
+
return this.json;
|
|
53536
|
+
}
|
|
53537
|
+
isDeletable() {
|
|
53538
|
+
return !this.global;
|
|
53539
|
+
}
|
|
53540
|
+
static getSettablePaths(_options = {}) {
|
|
53541
|
+
return {
|
|
53542
|
+
relativeDirPath: XUM_DIR,
|
|
53543
|
+
relativeFilePath: XUM_MCP_FILE_NAME
|
|
53544
|
+
};
|
|
53545
|
+
}
|
|
53546
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
53547
|
+
const paths = this.getSettablePaths({ global });
|
|
53548
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? EMPTY_XUM_MCP_CONFIG;
|
|
53549
|
+
return new XumMcp({
|
|
53550
|
+
outputRoot,
|
|
53551
|
+
relativeDirPath: paths.relativeDirPath,
|
|
53552
|
+
relativeFilePath: paths.relativeFilePath,
|
|
53553
|
+
fileContent,
|
|
53554
|
+
validate,
|
|
53555
|
+
global
|
|
53556
|
+
});
|
|
53557
|
+
}
|
|
53558
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
53559
|
+
const paths = this.getSettablePaths({ global });
|
|
53560
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
53561
|
+
const existingContent = await readFileContentOrNull(filePath) ?? EMPTY_XUM_MCP_CONFIG;
|
|
53562
|
+
const servers = {};
|
|
53563
|
+
for (const [name, server] of Object.entries(rulesyncMcp.getMcpServers())) {
|
|
53564
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
53565
|
+
const entry = convertServerToXum({
|
|
53566
|
+
name,
|
|
53567
|
+
server,
|
|
53568
|
+
logger
|
|
53569
|
+
});
|
|
53570
|
+
if (entry !== null) servers[name] = entry;
|
|
53571
|
+
}
|
|
53572
|
+
return new XumMcp({
|
|
53573
|
+
outputRoot,
|
|
53574
|
+
relativeDirPath: paths.relativeDirPath,
|
|
53575
|
+
relativeFilePath: paths.relativeFilePath,
|
|
53576
|
+
fileContent: applySharedConfigPatch({
|
|
53577
|
+
fileKey: sharedConfigFileKey(paths),
|
|
53578
|
+
feature: "mcp",
|
|
53579
|
+
existingContent,
|
|
53580
|
+
patch: { servers },
|
|
53581
|
+
filePath,
|
|
53582
|
+
logger
|
|
53583
|
+
}),
|
|
53584
|
+
validate,
|
|
53585
|
+
global
|
|
53586
|
+
});
|
|
53587
|
+
}
|
|
53588
|
+
toRulesyncMcp() {
|
|
53589
|
+
const mcpServers = convertFromXumFormat(this.json.servers);
|
|
53590
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
|
|
53591
|
+
}
|
|
53592
|
+
validate() {
|
|
53593
|
+
return {
|
|
53594
|
+
success: true,
|
|
53595
|
+
error: null
|
|
53596
|
+
};
|
|
53597
|
+
}
|
|
53598
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
53599
|
+
return new XumMcp({
|
|
53600
|
+
outputRoot,
|
|
53601
|
+
relativeDirPath,
|
|
53602
|
+
relativeFilePath,
|
|
53603
|
+
fileContent: "{}",
|
|
53604
|
+
validate: false,
|
|
53605
|
+
global
|
|
53606
|
+
});
|
|
53607
|
+
}
|
|
53608
|
+
};
|
|
53609
|
+
//#endregion
|
|
52033
53610
|
//#region src/features/mcp/zcode-mcp.ts
|
|
52034
53611
|
/**
|
|
52035
53612
|
* Single spelling of the config.json codec/policy, matching the
|
|
@@ -52063,7 +53640,7 @@ function asZcodeRemoteType(stated, url) {
|
|
|
52063
53640
|
}
|
|
52064
53641
|
/**
|
|
52065
53642
|
* The keys each OAuth flow of ZCode's per-server `oauth` union accepts on a
|
|
52066
|
-
* remote server, and which of them are required. Read off the 3.14.3
|
|
53643
|
+
* remote server, and which of them are required. Read off the 3.14.3–3.16.2
|
|
52067
53644
|
* runtime config schema, since ZCode's MCP page only says to "enable OAuth for
|
|
52068
53645
|
* the service in its configuration". Both variants are strict, so any other
|
|
52069
53646
|
* key would make ZCode skip the server.
|
|
@@ -52167,7 +53744,11 @@ function resolveZcodeTimeoutMs({ serverName, config, logger }) {
|
|
|
52167
53744
|
}
|
|
52168
53745
|
/**
|
|
52169
53746
|
* The per-server keys ZCode reads on every transport: `timeoutMs`,
|
|
52170
|
-
* `protocolVersion` and the enable toggle (`enableKey`).
|
|
53747
|
+
* `protocolVersion`, `alwaysLoad` and the enable toggle (`enableKey`).
|
|
53748
|
+
*
|
|
53749
|
+
* `alwaysLoad` (added in 3.16.2 alongside tool search) keeps a server's tools
|
|
53750
|
+
* loaded up front instead of deferring them behind tool search. ZCode's schema
|
|
53751
|
+
* types it as a boolean, so any other value is dropped with a warning.
|
|
52171
53752
|
*/
|
|
52172
53753
|
function toZcodeCommonKeys({ serverName, config, enableKey, logger }) {
|
|
52173
53754
|
const common = {};
|
|
@@ -52180,6 +53761,9 @@ function toZcodeCommonKeys({ serverName, config, enableKey, logger }) {
|
|
|
52180
53761
|
const { protocolVersion } = config;
|
|
52181
53762
|
if (ZCODE_PROTOCOL_VERSIONS.has(protocolVersion)) common.protocolVersion = protocolVersion;
|
|
52182
53763
|
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(", ")}.`);
|
|
53764
|
+
const { alwaysLoad } = config;
|
|
53765
|
+
if (typeof alwaysLoad === "boolean") common.alwaysLoad = alwaysLoad;
|
|
53766
|
+
else if (alwaysLoad !== void 0) logger?.warn(`ZCode MCP server "${serverName}": dropping alwaysLoad ${JSON.stringify(alwaysLoad)}, as it must be a boolean.`);
|
|
52183
53767
|
if (config.disabled === true) common[enableKey] = false;
|
|
52184
53768
|
return common;
|
|
52185
53769
|
}
|
|
@@ -52192,11 +53776,11 @@ function toZcodeCommonKeys({ serverName, config, enableKey, logger }) {
|
|
|
52192
53776
|
* absent.
|
|
52193
53777
|
*
|
|
52194
53778
|
* Every server also carries `timeoutMs` (from the canonical `timeout`, or an
|
|
52195
|
-
* explicit `timeoutMs`)
|
|
52196
|
-
* carry `cwd`, and remote servers an `oauth` built
|
|
52197
|
-
* `zcodeOauth`. That key is stripped by `RulesyncMcp.getMcpServers()`, so it
|
|
53779
|
+
* explicit `timeoutMs`), an allow-listed `protocolVersion` and a boolean
|
|
53780
|
+
* `alwaysLoad`; stdio servers carry `cwd`, and remote servers an `oauth` built
|
|
53781
|
+
* from the namespaced `zcodeOauth`. That key is stripped by `RulesyncMcp.getMcpServers()`, so it
|
|
52198
53782
|
* is read from the authored server through `rawServerOf`. ZCode's docs do not
|
|
52199
|
-
* name these keys, so they follow the 3.14.3
|
|
53783
|
+
* name these keys, so they follow the 3.14.3–3.16.2 runtime's per-server
|
|
52200
53784
|
* schema, which is strict: an unknown key makes ZCode skip the server, which
|
|
52201
53785
|
* is why the output stays a whitelist.
|
|
52202
53786
|
*
|
|
@@ -52291,8 +53875,8 @@ const ZCODE_ENABLE_KEYS = /* @__PURE__ */ new Set(["enable", "enabled"]);
|
|
|
52291
53875
|
* and unknown keys pass through untouched so an import keeps whatever a
|
|
52292
53876
|
* hand-authored entry declared. `timeoutMs` is lifted back into the canonical
|
|
52293
53877
|
* `timeout` (milliseconds); when the server also states a `timeout`, ZCode
|
|
52294
|
-
* has already dropped that one, so `timeoutMs` replaces it.
|
|
52295
|
-
* `oauth` is imported as the namespaced `zcodeOauth`, so
|
|
53878
|
+
* has already dropped that one, so `timeoutMs` replaces it. Any record
|
|
53879
|
+
* `oauth` is imported as the namespaced `zcodeOauth`, so a client secret is
|
|
52296
53880
|
* never emitted to another tool. Generation back out is a whitelist, so only
|
|
52297
53881
|
* the keys `convertToZcodeFormat` writes are re-emitted.
|
|
52298
53882
|
*
|
|
@@ -52318,7 +53902,7 @@ function convertFromZcodeFormat({ zcodeServers }) {
|
|
|
52318
53902
|
continue;
|
|
52319
53903
|
}
|
|
52320
53904
|
if (key === "timeout" && typeof config.timeoutMs === "number") continue;
|
|
52321
|
-
if (key === "oauth" &&
|
|
53905
|
+
if (key === "oauth" && isRecord$1(value)) {
|
|
52322
53906
|
converted[ZCODE_OAUTH_CANONICAL_KEY] = value;
|
|
52323
53907
|
continue;
|
|
52324
53908
|
}
|
|
@@ -52964,6 +54548,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
52964
54548
|
supportsDisabledTools: true
|
|
52965
54549
|
}
|
|
52966
54550
|
}],
|
|
54551
|
+
["firebender", {
|
|
54552
|
+
class: FirebenderMcp,
|
|
54553
|
+
meta: {
|
|
54554
|
+
supportsProject: true,
|
|
54555
|
+
supportsGlobal: true,
|
|
54556
|
+
supportsEnabledTools: false,
|
|
54557
|
+
supportsDisabledTools: false
|
|
54558
|
+
}
|
|
54559
|
+
}],
|
|
52967
54560
|
["goose", {
|
|
52968
54561
|
class: GooseMcp,
|
|
52969
54562
|
meta: {
|
|
@@ -53234,6 +54827,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
53234
54827
|
supportsDisabledTools: false
|
|
53235
54828
|
}
|
|
53236
54829
|
}],
|
|
54830
|
+
["xum", {
|
|
54831
|
+
class: XumMcp,
|
|
54832
|
+
meta: {
|
|
54833
|
+
supportsProject: true,
|
|
54834
|
+
supportsGlobal: true,
|
|
54835
|
+
supportsEnabledTools: true,
|
|
54836
|
+
supportsDisabledTools: false
|
|
54837
|
+
}
|
|
54838
|
+
}],
|
|
53237
54839
|
["devin", {
|
|
53238
54840
|
class: DevinMcp,
|
|
53239
54841
|
meta: {
|
|
@@ -57315,6 +58917,11 @@ const CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS = [
|
|
|
57315
58917
|
reason: "lets sandboxed commands send Apple Events, which removes code-execution isolation",
|
|
57316
58918
|
widens: isNotFalse
|
|
57317
58919
|
},
|
|
58920
|
+
{
|
|
58921
|
+
path: ["allowNestedUserNamespaces"],
|
|
58922
|
+
reason: "lets sandboxed Linux commands create user namespaces, which gets around the sandbox's file protections",
|
|
58923
|
+
widens: isNotFalse
|
|
58924
|
+
},
|
|
57318
58925
|
{
|
|
57319
58926
|
path: ["allowUnsandboxedCommands"],
|
|
57320
58927
|
reason: "controls whether Claude may retry a blocked command outside the sandbox",
|
|
@@ -67764,7 +69371,7 @@ const MANAGED_OMP_TOOLS = /* @__PURE__ */ new Set([
|
|
|
67764
69371
|
* catch-all one at its own strictness, a pattern-specific one as `prompt`).
|
|
67765
69372
|
*
|
|
67766
69373
|
* The `omp.approvalMode` override writes `tools.approvalMode` (`always-ask`,
|
|
67767
|
-
* `write` or `yolo`); without it the key is left as it is in the file, and on
|
|
69374
|
+
* `always-ask-cached`, `write` or `yolo`); without it the key is left as it is in the file, and on
|
|
67768
69375
|
* import a recognized value is read back into the override.
|
|
67769
69376
|
*
|
|
67770
69377
|
* `config.yml` carries every other oh-my-pi setting, so only `tools.approval`
|
|
@@ -70036,6 +71643,8 @@ function buildReasonixPermissionEntry(toolName, pattern) {
|
|
|
70036
71643
|
const REASONIX_TOOL_LABEL = "Reasonix";
|
|
70037
71644
|
/** The `[permissions]` key the override's `allowDynamicBash` writes and reads. */
|
|
70038
71645
|
const REASONIX_ALLOW_DYNAMIC_BASH_KEY = "allow_dynamic_bash";
|
|
71646
|
+
/** The `[permissions]` key the override's `askOnExternalOrigin` writes and reads. */
|
|
71647
|
+
const REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY = "ask_on_external_origin";
|
|
70039
71648
|
/**
|
|
70040
71649
|
* `[sandbox]` keys whose authored value loosens the enforcement layer beneath
|
|
70041
71650
|
* the permission policy: they take Bash out of its OS jail, open that jail to
|
|
@@ -70238,6 +71847,95 @@ function applyPermissionsMode({ permissions, authored, global, relativeFilePath,
|
|
|
70238
71847
|
reason: "lets every writer call that no rule matches run without a prompt"
|
|
70239
71848
|
}];
|
|
70240
71849
|
}
|
|
71850
|
+
/**
|
|
71851
|
+
* Writes the override's `askOnExternalOrigin` into `[permissions]`. Reasonix
|
|
71852
|
+
* honors the key only in the user config (`narrowPermissions` in
|
|
71853
|
+
* `project_scope.go`), so a project-scope generate skips it with a warning and
|
|
71854
|
+
* leaves whatever the file had. Turning it on only adds prompts, so that is
|
|
71855
|
+
* quiet; turning off a value the file had on is reported, since Auto and YOLO
|
|
71856
|
+
* then run calls that repeat text from a web page or tool result without asking.
|
|
71857
|
+
* As with `applyAllowDynamicBash`, only a literal `true` counts as quiet, so a
|
|
71858
|
+
* value a `--no-validate` run let through is never written in silence.
|
|
71859
|
+
*
|
|
71860
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.47.1/docs/GUIDE.md
|
|
71861
|
+
*/
|
|
71862
|
+
function applyAskOnExternalOrigin({ permissions, override, global, relativeFilePath, logger }) {
|
|
71863
|
+
const authored = override?.askOnExternalOrigin;
|
|
71864
|
+
if (authored === void 0) return [];
|
|
71865
|
+
if (!global) {
|
|
71866
|
+
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.`);
|
|
71867
|
+
return [];
|
|
71868
|
+
}
|
|
71869
|
+
const previous = permissions[REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY];
|
|
71870
|
+
permissions[REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY] = authored;
|
|
71871
|
+
if (authored === true || previous !== true && authored === false) return [];
|
|
71872
|
+
return [{
|
|
71873
|
+
label: `permissions.${REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY}`,
|
|
71874
|
+
reason: "lets Auto and YOLO run a call that repeats text from a web page or tool result without asking first"
|
|
71875
|
+
}];
|
|
71876
|
+
}
|
|
71877
|
+
/**
|
|
71878
|
+
* The `tools` patch entry — the `[tools]` table with the override's `disabled`
|
|
71879
|
+
* list laid over it, or nothing when the override does not author one — and the
|
|
71880
|
+
* trust rows it carries. Adding a name only removes a tool, and a project file
|
|
71881
|
+
* can only add to the user's list, so that stays quiet. The list is replaced
|
|
71882
|
+
* whole, though, so a name the file had turned off and the override leaves out
|
|
71883
|
+
* is turned back on, and that is reported, as are a `[tools]` or a `disabled`
|
|
71884
|
+
* the file holds in a shape that cannot be read. Only `disabled` is written:
|
|
71885
|
+
* the rest of `[tools]` (timeouts, search backends, ...) stays as it is.
|
|
71886
|
+
*/
|
|
71887
|
+
function applyToolsDisabled({ existing, override }) {
|
|
71888
|
+
const disabled = override?.tools?.disabled;
|
|
71889
|
+
if (disabled === void 0) return {
|
|
71890
|
+
patch: {},
|
|
71891
|
+
entries: []
|
|
71892
|
+
};
|
|
71893
|
+
const patch = { tools: {
|
|
71894
|
+
...asReasonixRecord(existing),
|
|
71895
|
+
disabled
|
|
71896
|
+
} };
|
|
71897
|
+
if (existing === void 0) return {
|
|
71898
|
+
patch,
|
|
71899
|
+
entries: []
|
|
71900
|
+
};
|
|
71901
|
+
if (!isRecord$1(existing)) return {
|
|
71902
|
+
patch,
|
|
71903
|
+
entries: [{
|
|
71904
|
+
label: "tools",
|
|
71905
|
+
reason: replacedUnreadableReason({
|
|
71906
|
+
shape: "object",
|
|
71907
|
+
toolLabel: REASONIX_TOOL_LABEL
|
|
71908
|
+
})
|
|
71909
|
+
}]
|
|
71910
|
+
};
|
|
71911
|
+
const before = existing.disabled;
|
|
71912
|
+
if (before === void 0) return {
|
|
71913
|
+
patch,
|
|
71914
|
+
entries: []
|
|
71915
|
+
};
|
|
71916
|
+
if (!Array.isArray(before)) return {
|
|
71917
|
+
patch,
|
|
71918
|
+
entries: [{
|
|
71919
|
+
label: "tools.disabled",
|
|
71920
|
+
reason: replacedUnreadableReason({
|
|
71921
|
+
shape: "list",
|
|
71922
|
+
toolLabel: REASONIX_TOOL_LABEL
|
|
71923
|
+
})
|
|
71924
|
+
}]
|
|
71925
|
+
};
|
|
71926
|
+
const reEnabled = (0, es_toolkit.uniq)(before.filter((name) => !disabled.includes(name)));
|
|
71927
|
+
if (reEnabled.length === 0) return {
|
|
71928
|
+
patch,
|
|
71929
|
+
entries: []
|
|
71930
|
+
};
|
|
71931
|
+
return {
|
|
71932
|
+
patch,
|
|
71933
|
+
entries: [{
|
|
71934
|
+
label: "tools.disabled",
|
|
71935
|
+
reason: `turns back on ${reEnabled.map((name) => quoteValueForWarning(name)).join(", ")}, which the file had turned off`
|
|
71936
|
+
}]
|
|
71937
|
+
};
|
|
71938
|
+
}
|
|
70241
71939
|
function parseReasonixConfig(fileContent) {
|
|
70242
71940
|
const parsed = smol_toml.parse(fileContent || smol_toml.stringify({}));
|
|
70243
71941
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
@@ -70293,6 +71991,11 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
70293
71991
|
});
|
|
70294
71992
|
}
|
|
70295
71993
|
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
|
|
71994
|
+
await warnIfReasonixNestedProjectConfigShadowed({
|
|
71995
|
+
outputRoot,
|
|
71996
|
+
global,
|
|
71997
|
+
logger
|
|
71998
|
+
});
|
|
70296
71999
|
const paths = this.getSettablePaths({ global });
|
|
70297
72000
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
70298
72001
|
const relativeFilePathForLog = toPosixPath((0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath));
|
|
@@ -70352,7 +72055,20 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
70352
72055
|
relativeFilePath: relativeFilePathForLog,
|
|
70353
72056
|
logger
|
|
70354
72057
|
}));
|
|
72058
|
+
trustAffecting.push(...applyAskOnExternalOrigin({
|
|
72059
|
+
permissions: mergedPermissions,
|
|
72060
|
+
override,
|
|
72061
|
+
global,
|
|
72062
|
+
relativeFilePath: relativeFilePathForLog,
|
|
72063
|
+
logger
|
|
72064
|
+
}));
|
|
70355
72065
|
const patch = { permissions: mergedPermissions };
|
|
72066
|
+
const toolsDisabled = applyToolsDisabled({
|
|
72067
|
+
existing: parsed.tools,
|
|
72068
|
+
override
|
|
72069
|
+
});
|
|
72070
|
+
Object.assign(patch, toolsDisabled.patch);
|
|
72071
|
+
trustAffecting.push(...toolsDisabled.entries);
|
|
70356
72072
|
if (override?.sandbox !== void 0) {
|
|
70357
72073
|
const authoredSandbox = dropUserOnlySandboxKeys({
|
|
70358
72074
|
sandbox: override.sandbox,
|
|
@@ -70426,6 +72142,14 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
70426
72142
|
const allowDynamicBash = permissions[REASONIX_ALLOW_DYNAMIC_BASH_KEY];
|
|
70427
72143
|
if (typeof allowDynamicBash === "boolean") reasonixOverride.allowDynamicBash = allowDynamicBash;
|
|
70428
72144
|
if (this.global && typeof permissions.mode === "string" && REASONIX_PERMISSION_MODES.has(permissions.mode)) reasonixOverride.mode = permissions.mode;
|
|
72145
|
+
const askOnExternalOrigin = permissions[REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY];
|
|
72146
|
+
if (this.global && typeof askOnExternalOrigin === "boolean") reasonixOverride.askOnExternalOrigin = askOnExternalOrigin;
|
|
72147
|
+
const toolsDisabled = asReasonixRecord(this.toml.tools).disabled;
|
|
72148
|
+
if (Array.isArray(toolsDisabled)) {
|
|
72149
|
+
const names = toReasonixStringArray(toolsDisabled);
|
|
72150
|
+
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.`);
|
|
72151
|
+
reasonixOverride.tools = { disabled: names };
|
|
72152
|
+
}
|
|
70429
72153
|
if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
|
|
70430
72154
|
if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
|
|
70431
72155
|
if (allowSplit.exact.length > 0) reasonixOverride.rawAllow = allowSplit.exact;
|
|
@@ -72007,7 +73731,12 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
|
|
|
72007
73731
|
const authoredProviderOptions = isPlainObject$1(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
|
|
72008
73732
|
const runtimeModeRefusesProviderOptions = authoredProviderOptions !== void 0 && runtime.active;
|
|
72009
73733
|
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.`);
|
|
72010
|
-
const overrideProviderOptions = runtimeModeRefusesProviderOptions ? void 0 :
|
|
73734
|
+
const overrideProviderOptions = runtimeModeRefusesProviderOptions ? void 0 : dropUntrustedProviderOptions({
|
|
73735
|
+
providerOptions: authoredProviderOptions,
|
|
73736
|
+
global,
|
|
73737
|
+
filePath,
|
|
73738
|
+
logger
|
|
73739
|
+
});
|
|
72011
73740
|
const authoredPolicies = pickSecurityPolicies(override, {
|
|
72012
73741
|
filePath,
|
|
72013
73742
|
logger,
|
|
@@ -72511,6 +74240,68 @@ function taktModeToRulesyncConfig(mode) {
|
|
|
72511
74240
|
default: return { permission: { bash: { [CATCH_ALL_PATTERN$1]: "deny" } } };
|
|
72512
74241
|
}
|
|
72513
74242
|
}
|
|
74243
|
+
const TAKT_BASE_URL_PROVIDERS = [
|
|
74244
|
+
"codex",
|
|
74245
|
+
"claude",
|
|
74246
|
+
"deepseek_harness"
|
|
74247
|
+
];
|
|
74248
|
+
const TAKT_DEEPSEEK_HARNESS_KEY = "deepseek_harness";
|
|
74249
|
+
/** Mirrors Takt's `isLoopbackBaseUrl`: localhost, *.localhost, ::1 or 127.x.x.x. */
|
|
74250
|
+
function isLoopbackBaseUrl(value) {
|
|
74251
|
+
let hostname;
|
|
74252
|
+
try {
|
|
74253
|
+
hostname = new URL(value).hostname.toLowerCase();
|
|
74254
|
+
} catch {
|
|
74255
|
+
return false;
|
|
74256
|
+
}
|
|
74257
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || hostname === "[::1]") return true;
|
|
74258
|
+
const octets = hostname.split(".");
|
|
74259
|
+
return octets.length === 4 && octets[0] === "127" && octets.every((octet) => /^\d+$/.test(octet) && Number(octet) <= 255);
|
|
74260
|
+
}
|
|
74261
|
+
/** The keys of one `provider_options.<provider>` entry Takt rejects in config.yaml. */
|
|
74262
|
+
function untrustedProviderOptionKeys({ provider, options, global }) {
|
|
74263
|
+
const keys = [];
|
|
74264
|
+
if (provider === TAKT_DEEPSEEK_HARNESS_KEY) {
|
|
74265
|
+
if (options.reasoning_effort !== void 0) keys.push("reasoning_effort");
|
|
74266
|
+
const { plugins } = options;
|
|
74267
|
+
if (!global && plugins !== void 0 && (!Array.isArray(plugins) || plugins.length > 0)) keys.push("plugins");
|
|
74268
|
+
}
|
|
74269
|
+
const baseUrl = options.base_url;
|
|
74270
|
+
if (!global && TAKT_BASE_URL_PROVIDERS.includes(provider) && baseUrl !== void 0 && (typeof baseUrl !== "string" || !isLoopbackBaseUrl(baseUrl))) keys.push("base_url");
|
|
74271
|
+
return keys;
|
|
74272
|
+
}
|
|
74273
|
+
/**
|
|
74274
|
+
* Drop the `provider_options` entries Takt refuses to load from `config.yaml`
|
|
74275
|
+
* (it fails at startup with a `Configuration error`), warning for each:
|
|
74276
|
+
* - `deepseek_harness.reasoning_effort` in either scope — Takt accepts it only
|
|
74277
|
+
* in runtime profile options or `TAKT_PROVIDER_OPTIONS_DEEPSEEK_HARNESS_REASONING_EFFORT`.
|
|
74278
|
+
* - at project scope, a non-empty `deepseek_harness.plugins` (Takt 0.71.0+,
|
|
74279
|
+
* global config only) and a non-loopback `{codex,claude,deepseek_harness}.base_url`.
|
|
74280
|
+
* https://github.com/nrslib/takt/blob/main/src/infra/config/providerOptions.ts
|
|
74281
|
+
* https://github.com/nrslib/takt/blob/main/docs/configuration.md
|
|
74282
|
+
*/
|
|
74283
|
+
function dropUntrustedProviderOptions({ providerOptions, global, filePath, logger }) {
|
|
74284
|
+
if (providerOptions === void 0) return;
|
|
74285
|
+
const dropped = [];
|
|
74286
|
+
const result = {};
|
|
74287
|
+
for (const [provider, options] of Object.entries(providerOptions)) {
|
|
74288
|
+
if (!isPlainObject$1(options)) {
|
|
74289
|
+
result[provider] = options;
|
|
74290
|
+
continue;
|
|
74291
|
+
}
|
|
74292
|
+
const untrusted = untrustedProviderOptionKeys({
|
|
74293
|
+
provider,
|
|
74294
|
+
options,
|
|
74295
|
+
global
|
|
74296
|
+
});
|
|
74297
|
+
const kept = Object.fromEntries(Object.entries(options).filter(([key]) => !untrusted.includes(key)));
|
|
74298
|
+
dropped.push(...untrusted.map((key) => `${TAKT_PROVIDER_OPTIONS_KEY}.${provider}.${key}`));
|
|
74299
|
+
if (Object.keys(kept).length > 0 || Object.keys(options).length === 0) result[provider] = kept;
|
|
74300
|
+
}
|
|
74301
|
+
if (dropped.length === 0) return providerOptions;
|
|
74302
|
+
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.`);
|
|
74303
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
74304
|
+
}
|
|
72514
74305
|
/**
|
|
72515
74306
|
* Lift Takt's security-policy keys out of a source object, keeping only the
|
|
72516
74307
|
* shapes Takt itself accepts (a boolean, or a table of booleans). Used in both
|
|
@@ -76445,6 +78236,11 @@ const TOOL_DISPLAY = [
|
|
|
76445
78236
|
label: "Factory Droid",
|
|
76446
78237
|
group: "ai"
|
|
76447
78238
|
},
|
|
78239
|
+
{
|
|
78240
|
+
key: "firebender",
|
|
78241
|
+
label: "Firebender",
|
|
78242
|
+
group: "ai"
|
|
78243
|
+
},
|
|
76448
78244
|
{
|
|
76449
78245
|
key: "opencode",
|
|
76450
78246
|
label: "OpenCode",
|
|
@@ -76600,6 +78396,11 @@ const TOOL_DISPLAY = [
|
|
|
76600
78396
|
label: "JetBrains AI Assistant",
|
|
76601
78397
|
group: "ai"
|
|
76602
78398
|
},
|
|
78399
|
+
{
|
|
78400
|
+
key: "androidstudio",
|
|
78401
|
+
label: "Gemini in Android Studio",
|
|
78402
|
+
group: "ai"
|
|
78403
|
+
},
|
|
76603
78404
|
{
|
|
76604
78405
|
key: "junie",
|
|
76605
78406
|
label: "JetBrains Junie",
|
|
@@ -76635,6 +78436,11 @@ const TOOL_DISPLAY = [
|
|
|
76635
78436
|
label: "Warp Agent CLI",
|
|
76636
78437
|
group: "ai"
|
|
76637
78438
|
},
|
|
78439
|
+
{
|
|
78440
|
+
key: "xum",
|
|
78441
|
+
label: "Xum",
|
|
78442
|
+
group: "ai"
|
|
78443
|
+
},
|
|
76638
78444
|
{
|
|
76639
78445
|
key: "replit",
|
|
76640
78446
|
label: "Replit",
|
|
@@ -77779,6 +79585,27 @@ var AmpSkill = class AmpSkill extends ToolSkill {
|
|
|
77779
79585
|
}
|
|
77780
79586
|
};
|
|
77781
79587
|
//#endregion
|
|
79588
|
+
//#region src/features/skills/androidstudio-skill.ts
|
|
79589
|
+
/**
|
|
79590
|
+
* Gemini in Android Studio skills follow the Agent Skills open standard
|
|
79591
|
+
* (`<name>/SKILL.md` with `name` and `description` frontmatter), so this target
|
|
79592
|
+
* reuses {@link AgentsSkillsSkill} with Android Studio's own location:
|
|
79593
|
+
* `.android-studio/skills/` at the project root (project) or in the home
|
|
79594
|
+
* directory (global). The shared `.agents/skills/` location Android Studio also
|
|
79595
|
+
* reads belongs to the `agentsskills` target.
|
|
79596
|
+
*
|
|
79597
|
+
* @see https://developer.android.com/studio/gemini/skills
|
|
79598
|
+
*/
|
|
79599
|
+
var AndroidstudioSkill = class extends AgentsSkillsSkill {
|
|
79600
|
+
static getSettablePaths(_options = {}) {
|
|
79601
|
+
return { relativeDirPath: ANDROIDSTUDIO_SKILLS_DIR_PATH };
|
|
79602
|
+
}
|
|
79603
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
79604
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
79605
|
+
return targets.includes("*") || targets.includes("androidstudio");
|
|
79606
|
+
}
|
|
79607
|
+
};
|
|
79608
|
+
//#endregion
|
|
77782
79609
|
//#region src/features/skills/antigravity-plugin-skill.ts
|
|
77783
79610
|
var AntigravityPluginSkill = class extends AntigravityIdeSkill {
|
|
77784
79611
|
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
@@ -81247,6 +83074,26 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
|
|
|
81247
83074
|
}
|
|
81248
83075
|
};
|
|
81249
83076
|
//#endregion
|
|
83077
|
+
//#region src/features/skills/firebender-skill.ts
|
|
83078
|
+
/**
|
|
83079
|
+
* Firebender skills follow the Agent Skills layout (`<name>/SKILL.md` with the
|
|
83080
|
+
* required `name` and `description` frontmatter, plus optional supporting
|
|
83081
|
+
* files), so this target reuses {@link AgentsSkillsSkill} with Firebender's
|
|
83082
|
+
* recommended locations: `.firebender/skills/` (project) and
|
|
83083
|
+
* `~/.firebender/skills/` (user).
|
|
83084
|
+
*
|
|
83085
|
+
* @see https://docs.firebender.com/api-reference/skills
|
|
83086
|
+
*/
|
|
83087
|
+
var FirebenderSkill = class extends AgentsSkillsSkill {
|
|
83088
|
+
static getSettablePaths(_options = {}) {
|
|
83089
|
+
return { relativeDirPath: FIREBENDER_SKILLS_DIR_PATH };
|
|
83090
|
+
}
|
|
83091
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
83092
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
83093
|
+
return targets.includes("*") || targets.includes("firebender");
|
|
83094
|
+
}
|
|
83095
|
+
};
|
|
83096
|
+
//#endregion
|
|
81250
83097
|
//#region src/features/skills/gitlabduo-skill.ts
|
|
81251
83098
|
/**
|
|
81252
83099
|
* GitLab Duo skills follow the Agent Skills standard (`name` and `description`
|
|
@@ -84782,6 +86629,110 @@ var WarpSkill = class WarpSkill extends ToolSkill {
|
|
|
84782
86629
|
}
|
|
84783
86630
|
};
|
|
84784
86631
|
//#endregion
|
|
86632
|
+
//#region src/features/skills/xum-skill.ts
|
|
86633
|
+
/**
|
|
86634
|
+
* The Agent Skills standard keys the `agentsskills` section carries. On import
|
|
86635
|
+
* they go back there, exactly as for the `agentsskills` target, and every other
|
|
86636
|
+
* key besides `name` / `description` goes into the `xum` section.
|
|
86637
|
+
*/
|
|
86638
|
+
const AGENT_SKILLS_STANDARD_KEYS = /* @__PURE__ */ new Set([
|
|
86639
|
+
"name",
|
|
86640
|
+
"description",
|
|
86641
|
+
"license",
|
|
86642
|
+
"compatibility",
|
|
86643
|
+
"metadata",
|
|
86644
|
+
"allowed-tools"
|
|
86645
|
+
]);
|
|
86646
|
+
/**
|
|
86647
|
+
* Xum skills follow the Agent Skills layout (`<name>/SKILL.md` with the
|
|
86648
|
+
* required `name` and `description` frontmatter; the directory name is
|
|
86649
|
+
* lowercase letters, digits and single hyphens up to 64 characters, and
|
|
86650
|
+
* `name` must match it), so this target reuses {@link AgentsSkillsSkill} with
|
|
86651
|
+
* Xum's locations: `.xum/skills/` (project) and `~/.xum/skills/` (user).
|
|
86652
|
+
* Within one directory `.xum/skills` wins over `.agents/skills`.
|
|
86653
|
+
*
|
|
86654
|
+
* On top of the standard fields, Xum reads `disable-model-invocation` (and its
|
|
86655
|
+
* own `advertise: false` alias), `user-invocable`, `argument-hint`,
|
|
86656
|
+
* `when_to_use` / `when-to-use` and `paths` (globs that make the skill
|
|
86657
|
+
* path-scoped). Those come from the `xum` section of the rulesync skill and are
|
|
86658
|
+
* written verbatim; the two invocation gates fall back to the root-level
|
|
86659
|
+
* rulesync values.
|
|
86660
|
+
*
|
|
86661
|
+
* @see https://xum.cdr.dev/agents/agent-skills
|
|
86662
|
+
*/
|
|
86663
|
+
var XumSkill = class XumSkill extends AgentsSkillsSkill {
|
|
86664
|
+
static getSettablePaths(_options = {}) {
|
|
86665
|
+
return { relativeDirPath: XUM_SKILLS_DIR_PATH };
|
|
86666
|
+
}
|
|
86667
|
+
toRulesyncSkill() {
|
|
86668
|
+
const base = super.toRulesyncSkill();
|
|
86669
|
+
const xumSection = Object.fromEntries(Object.entries(this.getFrontmatter()).filter(([key]) => !AGENT_SKILLS_STANDARD_KEYS.has(key)));
|
|
86670
|
+
if (Object.keys(xumSection).length === 0) return base;
|
|
86671
|
+
const rulesyncFrontmatter = {
|
|
86672
|
+
...base.getFrontmatter(),
|
|
86673
|
+
xum: xumSection
|
|
86674
|
+
};
|
|
86675
|
+
return new RulesyncSkill({
|
|
86676
|
+
outputRoot: base.getOutputRoot(),
|
|
86677
|
+
relativeDirPath: base.getRelativeDirPath(),
|
|
86678
|
+
dirName: base.getDirName(),
|
|
86679
|
+
frontmatter: rulesyncFrontmatter,
|
|
86680
|
+
body: base.getBody(),
|
|
86681
|
+
otherFiles: base.getOtherFiles(),
|
|
86682
|
+
validate: true,
|
|
86683
|
+
global: this.global
|
|
86684
|
+
});
|
|
86685
|
+
}
|
|
86686
|
+
static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false, logger }) {
|
|
86687
|
+
const settablePaths = XumSkill.getSettablePaths({ global });
|
|
86688
|
+
const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
|
|
86689
|
+
const dirName = rulesyncSkill.getDirName();
|
|
86690
|
+
const xumSection = {};
|
|
86691
|
+
const ignoredKeys = [];
|
|
86692
|
+
for (const [key, value] of Object.entries(rulesyncFrontmatter.xum ?? {})) if (AGENT_SKILLS_STANDARD_KEYS.has(key)) ignoredKeys.push(key);
|
|
86693
|
+
else xumSection[key] = value;
|
|
86694
|
+
if (ignoredKeys.length > 0) warnWithFallback(logger, `${(0, node_path.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.`);
|
|
86695
|
+
const userInvocable = resolveUserInvocable({
|
|
86696
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
86697
|
+
section: xumSection
|
|
86698
|
+
});
|
|
86699
|
+
const disableModelInvocation = resolveDisableModelInvocation({
|
|
86700
|
+
rootFrontmatter: rulesyncFrontmatter,
|
|
86701
|
+
section: xumSection
|
|
86702
|
+
});
|
|
86703
|
+
const frontmatter = {
|
|
86704
|
+
name: rulesyncFrontmatter.name,
|
|
86705
|
+
description: rulesyncFrontmatter.description,
|
|
86706
|
+
...toSpecConformantAgentSkillFields(resolveAgentsSkillsSection(rulesyncFrontmatter)),
|
|
86707
|
+
...xumSection,
|
|
86708
|
+
...userInvocable !== void 0 && { "user-invocable": userInvocable },
|
|
86709
|
+
...disableModelInvocation !== void 0 && { "disable-model-invocation": disableModelInvocation }
|
|
86710
|
+
};
|
|
86711
|
+
AgentsSkillsSkill.reportSpecViolations({
|
|
86712
|
+
outputRoot,
|
|
86713
|
+
relativeDirPath: settablePaths.relativeDirPath,
|
|
86714
|
+
dirName,
|
|
86715
|
+
frontmatter,
|
|
86716
|
+
sourceAllowedTools: rulesyncFrontmatter.agentsskills?.["allowed-tools"],
|
|
86717
|
+
logger
|
|
86718
|
+
});
|
|
86719
|
+
return new XumSkill({
|
|
86720
|
+
outputRoot,
|
|
86721
|
+
relativeDirPath: settablePaths.relativeDirPath,
|
|
86722
|
+
dirName,
|
|
86723
|
+
frontmatter,
|
|
86724
|
+
body: rulesyncSkill.getBody(),
|
|
86725
|
+
otherFiles: rulesyncSkill.getOtherFiles(),
|
|
86726
|
+
validate,
|
|
86727
|
+
global
|
|
86728
|
+
});
|
|
86729
|
+
}
|
|
86730
|
+
static isTargetedByRulesyncSkill(rulesyncSkill) {
|
|
86731
|
+
const targets = rulesyncSkill.getFrontmatter().targets;
|
|
86732
|
+
return targets.includes("*") || targets.includes("xum");
|
|
86733
|
+
}
|
|
86734
|
+
};
|
|
86735
|
+
//#endregion
|
|
84785
86736
|
//#region src/features/skills/zcode-skill.ts
|
|
84786
86737
|
const ZcodeSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
84787
86738
|
name: zod_mini.z.string(),
|
|
@@ -85197,6 +87148,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
85197
87148
|
supportsGlobal: false
|
|
85198
87149
|
}
|
|
85199
87150
|
}],
|
|
87151
|
+
["androidstudio", {
|
|
87152
|
+
class: AndroidstudioSkill,
|
|
87153
|
+
meta: {
|
|
87154
|
+
supportsProject: true,
|
|
87155
|
+
supportsSimulated: false,
|
|
87156
|
+
supportsGlobal: true
|
|
87157
|
+
}
|
|
87158
|
+
}],
|
|
85200
87159
|
["amp", {
|
|
85201
87160
|
class: AmpSkill,
|
|
85202
87161
|
meta: {
|
|
@@ -85382,6 +87341,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
85382
87341
|
supportsGlobal: true
|
|
85383
87342
|
}
|
|
85384
87343
|
}],
|
|
87344
|
+
["firebender", {
|
|
87345
|
+
class: FirebenderSkill,
|
|
87346
|
+
meta: {
|
|
87347
|
+
supportsProject: true,
|
|
87348
|
+
supportsSimulated: false,
|
|
87349
|
+
supportsGlobal: true
|
|
87350
|
+
}
|
|
87351
|
+
}],
|
|
85385
87352
|
["goose", {
|
|
85386
87353
|
class: GooseSkill,
|
|
85387
87354
|
meta: {
|
|
@@ -85632,6 +87599,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
|
|
|
85632
87599
|
supportsGlobal: true
|
|
85633
87600
|
}
|
|
85634
87601
|
}],
|
|
87602
|
+
["xum", {
|
|
87603
|
+
class: XumSkill,
|
|
87604
|
+
meta: {
|
|
87605
|
+
supportsProject: true,
|
|
87606
|
+
supportsSimulated: false,
|
|
87607
|
+
supportsGlobal: true
|
|
87608
|
+
}
|
|
87609
|
+
}],
|
|
85635
87610
|
["devin", {
|
|
85636
87611
|
class: DevinSkill,
|
|
85637
87612
|
meta: {
|
|
@@ -89478,6 +91453,338 @@ var FactorydroidSubagent = class FactorydroidSubagent extends ToolSubagent {
|
|
|
89478
91453
|
}
|
|
89479
91454
|
};
|
|
89480
91455
|
//#endregion
|
|
91456
|
+
//#region src/features/subagents/firebender-subagent.ts
|
|
91457
|
+
/**
|
|
91458
|
+
* Frontmatter of a Firebender agent file. `tools` is a comma-separated string
|
|
91459
|
+
* of tool groups (`read`, `edit`, `execution`, `other`) and MCP tools
|
|
91460
|
+
* (`mcp_<server>_<tool>`); `model` is `small`, `medium`, `large`, `inherit` or a
|
|
91461
|
+
* model ID; `callable` lets the main agent delegate to it. Unknown keys are
|
|
91462
|
+
* preserved.
|
|
91463
|
+
*
|
|
91464
|
+
* @see https://docs.firebender.com/api-reference/agents
|
|
91465
|
+
*/
|
|
91466
|
+
const FirebenderSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
91467
|
+
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
91468
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
91469
|
+
color: zod_mini.z.optional(zod_mini.z.string()),
|
|
91470
|
+
icon: zod_mini.z.optional(zod_mini.z.string()),
|
|
91471
|
+
tools: zod_mini.z.optional(zod_mini.z.string()),
|
|
91472
|
+
model: zod_mini.z.optional(zod_mini.z.string()),
|
|
91473
|
+
callable: zod_mini.z.optional(zod_mini.z.boolean())
|
|
91474
|
+
});
|
|
91475
|
+
/**
|
|
91476
|
+
* The `agents` entry Firebender resolves to an agent file rulesync writes:
|
|
91477
|
+
* relative to the project root for the project config, and to `~/.firebender`
|
|
91478
|
+
* for the personal one.
|
|
91479
|
+
*/
|
|
91480
|
+
function toAgentsEntry({ relativeFilePath, global }) {
|
|
91481
|
+
const fileName = toPosixPath(relativeFilePath);
|
|
91482
|
+
return global ? node_path.posix.join(FIREBENDER_AGENTS_DIR_NAME, fileName) : node_path.posix.join(toPosixPath(FIREBENDER_AGENTS_DIR_PATH), fileName);
|
|
91483
|
+
}
|
|
91484
|
+
/**
|
|
91485
|
+
* The agent file name an existing `agents` entry points at when it lies
|
|
91486
|
+
* directly inside the directory rulesync writes agents to
|
|
91487
|
+
* (`.firebender/agents/` in the project; `agents/` or `~/.firebender/agents/`
|
|
91488
|
+
* in the personal config), or `undefined` for any other entry
|
|
91489
|
+
* (`~/firebender-agents/...`, an absolute path, a nested path).
|
|
91490
|
+
*/
|
|
91491
|
+
function managedAgentFileName({ entry, global }) {
|
|
91492
|
+
const normalized = toPosixPath(entry).replace(/^\.\//, "");
|
|
91493
|
+
const prefix = global ? `${FIREBENDER_AGENTS_DIR_NAME}/` : `${toPosixPath(FIREBENDER_AGENTS_DIR_PATH)}/`;
|
|
91494
|
+
const homePrefix = `~/${toPosixPath(FIREBENDER_AGENTS_DIR_PATH)}/`;
|
|
91495
|
+
const rest = normalized.startsWith(prefix) ? normalized.slice(prefix.length) : global && normalized.startsWith(homePrefix) ? normalized.slice(homePrefix.length) : void 0;
|
|
91496
|
+
return rest !== void 0 && rest.endsWith(".md") && !rest.includes("/") ? rest : void 0;
|
|
91497
|
+
}
|
|
91498
|
+
/**
|
|
91499
|
+
* Subagent generator for Firebender.
|
|
91500
|
+
*
|
|
91501
|
+
* Each agent is a Markdown file with YAML frontmatter, written to
|
|
91502
|
+
* `.firebender/agents/<name>.md` (project) or `~/.firebender/agents/<name>.md`
|
|
91503
|
+
* (global). Firebender only loads agents listed in the `agents` array of
|
|
91504
|
+
* `firebender.json`, so generation also registers every file there — see
|
|
91505
|
+
* {@link FirebenderAgentsRegistry}. Rulesync subagents are meant to be
|
|
91506
|
+
* delegated to, so `callable: true` is written unless the `firebender:`
|
|
91507
|
+
* section says otherwise.
|
|
91508
|
+
*
|
|
91509
|
+
* @see https://docs.firebender.com/api-reference/agents
|
|
91510
|
+
*/
|
|
91511
|
+
var FirebenderSubagent = class FirebenderSubagent extends ToolSubagent {
|
|
91512
|
+
frontmatter;
|
|
91513
|
+
body;
|
|
91514
|
+
constructor({ frontmatter, body, ...rest }) {
|
|
91515
|
+
if (rest.validate !== false) {
|
|
91516
|
+
const result = FirebenderSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
91517
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
91518
|
+
}
|
|
91519
|
+
super({ ...rest });
|
|
91520
|
+
this.frontmatter = frontmatter;
|
|
91521
|
+
this.body = body;
|
|
91522
|
+
}
|
|
91523
|
+
static getSettablePaths(_options = {}) {
|
|
91524
|
+
return { relativeDirPath: FIREBENDER_AGENTS_DIR_PATH };
|
|
91525
|
+
}
|
|
91526
|
+
/**
|
|
91527
|
+
* Generation also read-modify-writes the `agents` key of `firebender.json`,
|
|
91528
|
+
* so the write is declared for the shared-file order derivation.
|
|
91529
|
+
*/
|
|
91530
|
+
static getExtraSharedWritePaths({ global = false } = {}) {
|
|
91531
|
+
return [{
|
|
91532
|
+
relativeDirPath: getFirebenderConfigDir({ global }),
|
|
91533
|
+
relativeFilePath: FIREBENDER_CONFIG_FILE_NAME
|
|
91534
|
+
}];
|
|
91535
|
+
}
|
|
91536
|
+
getFrontmatter() {
|
|
91537
|
+
return this.frontmatter;
|
|
91538
|
+
}
|
|
91539
|
+
getBody() {
|
|
91540
|
+
return this.body;
|
|
91541
|
+
}
|
|
91542
|
+
toRulesyncSubagent() {
|
|
91543
|
+
const { name, description, callable, ...rest } = this.frontmatter;
|
|
91544
|
+
const firebenderSection = {
|
|
91545
|
+
...rest,
|
|
91546
|
+
...callable !== true && { callable: callable ?? false }
|
|
91547
|
+
};
|
|
91548
|
+
return new RulesyncSubagent({
|
|
91549
|
+
outputRoot: ".",
|
|
91550
|
+
frontmatter: {
|
|
91551
|
+
targets: ["*"],
|
|
91552
|
+
name: name ?? (0, node_path.basename)(this.getRelativeFilePath(), ".md"),
|
|
91553
|
+
description,
|
|
91554
|
+
...Object.keys(firebenderSection).length > 0 && { firebender: firebenderSection }
|
|
91555
|
+
},
|
|
91556
|
+
body: this.body,
|
|
91557
|
+
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
|
|
91558
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
91559
|
+
validate: true
|
|
91560
|
+
});
|
|
91561
|
+
}
|
|
91562
|
+
/**
|
|
91563
|
+
* Every targeted subagent becomes its own agent file, plus one
|
|
91564
|
+
* {@link FirebenderAgentsRegistry} that lists them in `firebender.json`.
|
|
91565
|
+
* The registry is emitted even when no subagent is targeted, so agents a
|
|
91566
|
+
* previous run registered are retracted once their files are gone. The
|
|
91567
|
+
* agent directory is listed up front so the registry can tell a registered
|
|
91568
|
+
* file that still exists (kept, whoever wrote it) from a stale one.
|
|
91569
|
+
*/
|
|
91570
|
+
static async fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, global = false, logger, deleteOrphans = false }) {
|
|
91571
|
+
const agentsDirPath = this.getSettablePaths({ global }).relativeDirPath;
|
|
91572
|
+
const agentsDirEscapesRoot = await writablePathEscapesRoot({
|
|
91573
|
+
rootPath: outputRoot,
|
|
91574
|
+
targetPath: (0, node_path.join)(outputRoot, agentsDirPath, "agent.md")
|
|
91575
|
+
});
|
|
91576
|
+
const existingAgentFileNames = new Set(agentsDirEscapesRoot ? [] : await listDirectoryEntryNames((0, node_path.join)(outputRoot, agentsDirPath)));
|
|
91577
|
+
const subagents = rulesyncSubagents.map((rulesyncSubagent) => this.fromRulesyncSubagent({
|
|
91578
|
+
outputRoot,
|
|
91579
|
+
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
91580
|
+
rulesyncSubagent,
|
|
91581
|
+
global
|
|
91582
|
+
}));
|
|
91583
|
+
const registry = new FirebenderAgentsRegistry({
|
|
91584
|
+
outputRoot,
|
|
91585
|
+
relativeDirPath: getFirebenderConfigDir({ global }),
|
|
91586
|
+
relativeFilePath: FIREBENDER_CONFIG_FILE_NAME,
|
|
91587
|
+
entries: subagents.map((subagent) => toAgentsEntry({
|
|
91588
|
+
relativeFilePath: subagent.getRelativeFilePath(),
|
|
91589
|
+
global
|
|
91590
|
+
})),
|
|
91591
|
+
existingAgentFileNames,
|
|
91592
|
+
deleteOrphans,
|
|
91593
|
+
frozen: agentsDirEscapesRoot,
|
|
91594
|
+
global,
|
|
91595
|
+
logger
|
|
91596
|
+
});
|
|
91597
|
+
return [...subagents, registry];
|
|
91598
|
+
}
|
|
91599
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
|
|
91600
|
+
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
91601
|
+
const firebenderSection = rulesyncFrontmatter.firebender ?? {};
|
|
91602
|
+
const frontmatter = {
|
|
91603
|
+
name: rulesyncFrontmatter.name,
|
|
91604
|
+
...rulesyncFrontmatter.description !== void 0 && { description: rulesyncFrontmatter.description },
|
|
91605
|
+
callable: true,
|
|
91606
|
+
...firebenderSection
|
|
91607
|
+
};
|
|
91608
|
+
const body = rulesyncSubagent.getBody();
|
|
91609
|
+
return new FirebenderSubagent({
|
|
91610
|
+
outputRoot,
|
|
91611
|
+
frontmatter,
|
|
91612
|
+
body,
|
|
91613
|
+
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
91614
|
+
relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
|
|
91615
|
+
fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true }),
|
|
91616
|
+
validate,
|
|
91617
|
+
global
|
|
91618
|
+
});
|
|
91619
|
+
}
|
|
91620
|
+
validate() {
|
|
91621
|
+
if (!this.frontmatter) return {
|
|
91622
|
+
success: true,
|
|
91623
|
+
error: null
|
|
91624
|
+
};
|
|
91625
|
+
const result = FirebenderSubagentFrontmatterSchema.safeParse(this.frontmatter);
|
|
91626
|
+
if (result.success) return {
|
|
91627
|
+
success: true,
|
|
91628
|
+
error: null
|
|
91629
|
+
};
|
|
91630
|
+
return {
|
|
91631
|
+
success: false,
|
|
91632
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
91633
|
+
};
|
|
91634
|
+
}
|
|
91635
|
+
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
91636
|
+
return this.isTargetedByRulesyncSubagentDefault({
|
|
91637
|
+
rulesyncSubagent,
|
|
91638
|
+
toolTarget: "firebender"
|
|
91639
|
+
});
|
|
91640
|
+
}
|
|
91641
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
91642
|
+
const paths = this.getSettablePaths({ global });
|
|
91643
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
91644
|
+
const fileContent = await readFileContent(filePath);
|
|
91645
|
+
const { frontmatter, body } = parseFrontmatter(fileContent, filePath);
|
|
91646
|
+
const result = FirebenderSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
91647
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
91648
|
+
return new FirebenderSubagent({
|
|
91649
|
+
outputRoot,
|
|
91650
|
+
relativeDirPath: paths.relativeDirPath,
|
|
91651
|
+
relativeFilePath,
|
|
91652
|
+
frontmatter: result.data,
|
|
91653
|
+
body: body.trim(),
|
|
91654
|
+
fileContent,
|
|
91655
|
+
validate,
|
|
91656
|
+
global
|
|
91657
|
+
});
|
|
91658
|
+
}
|
|
91659
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
91660
|
+
return new FirebenderSubagent({
|
|
91661
|
+
outputRoot,
|
|
91662
|
+
relativeDirPath,
|
|
91663
|
+
relativeFilePath,
|
|
91664
|
+
frontmatter: {},
|
|
91665
|
+
body: "",
|
|
91666
|
+
fileContent: "",
|
|
91667
|
+
validate: false
|
|
91668
|
+
});
|
|
91669
|
+
}
|
|
91670
|
+
};
|
|
91671
|
+
/**
|
|
91672
|
+
* The `agents` array of `firebender.json`, which Firebender requires to load
|
|
91673
|
+
* an agent file. Every generated agent is registered. An existing entry that
|
|
91674
|
+
* points into rulesync's agent directory is kept while its file exists (a
|
|
91675
|
+
* hand-written agent there stays registered) and dropped, with a warning, once
|
|
91676
|
+
* the file is gone. Under `--delete` the orphan sweep removes every agent file
|
|
91677
|
+
* there that this run does not generate, but only after this file is written,
|
|
91678
|
+
* so in that mode such entries are dropped here too and a single run
|
|
91679
|
+
* converges. Every other entry and every other key of the file is kept.
|
|
91680
|
+
* The array is merged into the file on disk at write time and the file itself
|
|
91681
|
+
* is never deleted.
|
|
91682
|
+
*/
|
|
91683
|
+
var FirebenderAgentsRegistry = class extends ToolSubagent {
|
|
91684
|
+
entries;
|
|
91685
|
+
existingAgentFileNames;
|
|
91686
|
+
deleteOrphans;
|
|
91687
|
+
frozen;
|
|
91688
|
+
logger;
|
|
91689
|
+
mergedContent = null;
|
|
91690
|
+
constructor({ entries, existingAgentFileNames = /* @__PURE__ */ new Set(), deleteOrphans = false, frozen = false, logger, ...rest }) {
|
|
91691
|
+
super({
|
|
91692
|
+
...rest,
|
|
91693
|
+
fileContent: ""
|
|
91694
|
+
});
|
|
91695
|
+
this.entries = entries;
|
|
91696
|
+
this.existingAgentFileNames = existingAgentFileNames;
|
|
91697
|
+
this.deleteOrphans = deleteOrphans;
|
|
91698
|
+
this.frozen = frozen;
|
|
91699
|
+
this.logger = logger;
|
|
91700
|
+
}
|
|
91701
|
+
getEntries() {
|
|
91702
|
+
return this.entries;
|
|
91703
|
+
}
|
|
91704
|
+
isDeletable() {
|
|
91705
|
+
return false;
|
|
91706
|
+
}
|
|
91707
|
+
shouldMergeExistingFileContent() {
|
|
91708
|
+
return true;
|
|
91709
|
+
}
|
|
91710
|
+
setFileContent(newFileContent) {
|
|
91711
|
+
this.mergedContent = this.mergeInto(newFileContent);
|
|
91712
|
+
}
|
|
91713
|
+
getFileContent() {
|
|
91714
|
+
if (this.mergedContent === null) this.mergedContent = this.mergeInto("");
|
|
91715
|
+
return this.mergedContent;
|
|
91716
|
+
}
|
|
91717
|
+
mergeInto(existingContent) {
|
|
91718
|
+
const paths = {
|
|
91719
|
+
relativeDirPath: this.getRelativeDirPath(),
|
|
91720
|
+
relativeFilePath: this.getRelativeFilePath()
|
|
91721
|
+
};
|
|
91722
|
+
const filePath = (0, node_path.join)(this.getOutputRoot(), paths.relativeDirPath, paths.relativeFilePath);
|
|
91723
|
+
const existing = parseSharedConfig({
|
|
91724
|
+
format: "json",
|
|
91725
|
+
fileContent: existingContent,
|
|
91726
|
+
filePath,
|
|
91727
|
+
invalidRootPolicy: "error"
|
|
91728
|
+
});
|
|
91729
|
+
if (this.frozen) return applySharedConfigPatch({
|
|
91730
|
+
fileKey: sharedConfigFileKey(paths),
|
|
91731
|
+
feature: "subagents",
|
|
91732
|
+
existingContent,
|
|
91733
|
+
patch: { agents: existing.agents },
|
|
91734
|
+
filePath,
|
|
91735
|
+
logger: this.logger
|
|
91736
|
+
});
|
|
91737
|
+
const existingEntries = Array.isArray(existing.agents) ? existing.agents : [];
|
|
91738
|
+
const generatedByFileName = new Map(this.entries.map((entry) => [managedAgentFileName({
|
|
91739
|
+
entry,
|
|
91740
|
+
global: this.global
|
|
91741
|
+
}) ?? entry, entry]));
|
|
91742
|
+
const agents = [];
|
|
91743
|
+
const placed = /* @__PURE__ */ new Set();
|
|
91744
|
+
const dropped = [];
|
|
91745
|
+
for (const entry of existingEntries) {
|
|
91746
|
+
const fileName = typeof entry === "string" ? managedAgentFileName({
|
|
91747
|
+
entry,
|
|
91748
|
+
global: this.global
|
|
91749
|
+
}) : void 0;
|
|
91750
|
+
if (fileName === void 0) {
|
|
91751
|
+
agents.push(entry);
|
|
91752
|
+
continue;
|
|
91753
|
+
}
|
|
91754
|
+
const generated = generatedByFileName.get(fileName);
|
|
91755
|
+
if (generated !== void 0) {
|
|
91756
|
+
if (!placed.has(fileName)) {
|
|
91757
|
+
agents.push(generated);
|
|
91758
|
+
placed.add(fileName);
|
|
91759
|
+
}
|
|
91760
|
+
} else if (!this.deleteOrphans && this.existingAgentFileNames.has(fileName)) agents.push(entry);
|
|
91761
|
+
else dropped.push(String(entry));
|
|
91762
|
+
}
|
|
91763
|
+
for (const [fileName, entry] of generatedByFileName) if (!placed.has(fileName)) {
|
|
91764
|
+
agents.push(entry);
|
|
91765
|
+
placed.add(fileName);
|
|
91766
|
+
}
|
|
91767
|
+
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."));
|
|
91768
|
+
return applySharedConfigPatch({
|
|
91769
|
+
fileKey: sharedConfigFileKey(paths),
|
|
91770
|
+
feature: "subagents",
|
|
91771
|
+
existingContent,
|
|
91772
|
+
patch: { agents: agents.length > 0 ? agents : void 0 },
|
|
91773
|
+
filePath,
|
|
91774
|
+
logger: this.logger
|
|
91775
|
+
});
|
|
91776
|
+
}
|
|
91777
|
+
toRulesyncSubagent() {
|
|
91778
|
+
throw new Error(`${this.getRelativePathFromCwd()} registers Firebender agents and is not a subagent itself.`);
|
|
91779
|
+
}
|
|
91780
|
+
validate() {
|
|
91781
|
+
return {
|
|
91782
|
+
success: true,
|
|
91783
|
+
error: null
|
|
91784
|
+
};
|
|
91785
|
+
}
|
|
91786
|
+
};
|
|
91787
|
+
//#endregion
|
|
89481
91788
|
//#region src/features/subagents/goose-subagent.ts
|
|
89482
91789
|
/**
|
|
89483
91790
|
* Goose custom agents (v1.34.0+) are Markdown files with YAML frontmatter —
|
|
@@ -89765,13 +92072,13 @@ function subagentSlug(relativeFilePath) {
|
|
|
89765
92072
|
return (0, node_path.basename)(relativeFilePath, ".md").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
89766
92073
|
}
|
|
89767
92074
|
function hermesCommandName(slug) {
|
|
89768
|
-
return `
|
|
92075
|
+
return hermesSlashName(`rulesync-subagent-${slug}`);
|
|
89769
92076
|
}
|
|
89770
92077
|
function getPluginManifestContent() {
|
|
89771
92078
|
return [
|
|
89772
92079
|
"name: rulesync-subagents",
|
|
89773
92080
|
"version: \"1.0.0\"",
|
|
89774
|
-
"description: Exposes RuleSync subagents as Hermes
|
|
92081
|
+
"description: Exposes RuleSync subagents as Hermes slash commands that queue a delegation prompt.",
|
|
89775
92082
|
""
|
|
89776
92083
|
].join("\n");
|
|
89777
92084
|
}
|
|
@@ -89779,9 +92086,12 @@ function getPluginInitContent() {
|
|
|
89779
92086
|
return `"""RuleSync-generated Hermes subagent commands."""
|
|
89780
92087
|
|
|
89781
92088
|
import json
|
|
92089
|
+
import logging
|
|
92090
|
+
import re
|
|
89782
92091
|
from pathlib import Path
|
|
89783
92092
|
|
|
89784
92093
|
|
|
92094
|
+
logger = logging.getLogger(__name__)
|
|
89785
92095
|
SUBAGENTS_DIR = Path(__file__).resolve().parents[2] / "rulesync" / "subagents"
|
|
89786
92096
|
|
|
89787
92097
|
|
|
@@ -89801,52 +92111,71 @@ def _load_subagents():
|
|
|
89801
92111
|
return subagents
|
|
89802
92112
|
|
|
89803
92113
|
|
|
89804
|
-
def
|
|
92114
|
+
def _command_name(subagent, slug):
|
|
92115
|
+
# The spec carries the name Rulesync normalized; Hermes' messaging gateway
|
|
92116
|
+
# resolves plugin commands with "_" turned into "-", so keep it hyphenated.
|
|
92117
|
+
hermes = subagent.get("hermes")
|
|
92118
|
+
name = hermes.get("command") if isinstance(hermes, dict) else None
|
|
92119
|
+
name = name or f"rulesync-subagent-{slug}"
|
|
92120
|
+
# Mirrors hermesSlashName() in src/features/shared/hermes-plugin-commands.ts.
|
|
92121
|
+
name = re.sub(r"[ _]", "-", str(name).lower())
|
|
92122
|
+
name = re.sub(r"[^a-z0-9-]", "", name)
|
|
92123
|
+
return re.sub(r"-{2,}", "-", name).strip("-")
|
|
92124
|
+
|
|
92125
|
+
|
|
92126
|
+
${getHermesQueueUserMessagePython({ pluginName: "rulesync-subagents" })}
|
|
92127
|
+
|
|
92128
|
+
def _register_subagent(ctx, subagent, registered):
|
|
89805
92129
|
slug = subagent.get("slug")
|
|
89806
92130
|
if not slug:
|
|
89807
92131
|
return
|
|
89808
92132
|
|
|
89809
|
-
command_name =
|
|
92133
|
+
command_name = _command_name(subagent, slug)
|
|
92134
|
+
if command_name in registered:
|
|
92135
|
+
logger.warning(
|
|
92136
|
+
"rulesync-subagents: %s and %s both map to /%s; keeping the first.",
|
|
92137
|
+
registered[command_name],
|
|
92138
|
+
subagent.get("_path"),
|
|
92139
|
+
command_name,
|
|
92140
|
+
)
|
|
92141
|
+
return
|
|
92142
|
+
registered[command_name] = subagent.get("_path")
|
|
92143
|
+
|
|
89810
92144
|
name = subagent.get("name") or slug
|
|
89811
92145
|
description = subagent.get("description") or f"Delegate work to the {name} RuleSync subagent."
|
|
89812
92146
|
system_prompt = subagent.get("prompt") or ""
|
|
89813
92147
|
|
|
89814
92148
|
def handler(args=None, **kwargs):
|
|
89815
|
-
del kwargs
|
|
89816
92149
|
user_context = ""
|
|
89817
92150
|
if isinstance(args, dict):
|
|
89818
92151
|
user_context = args.get("context") or args.get("task") or args.get("prompt") or ""
|
|
89819
92152
|
elif args is not None:
|
|
89820
92153
|
user_context = str(args)
|
|
89821
92154
|
|
|
89822
|
-
|
|
92155
|
+
# A slash command runs outside any agent turn, so calling delegate_task
|
|
92156
|
+
# here fails with "delegate_task requires a parent agent context".
|
|
92157
|
+
# Queue the brief as a user message instead; the agent then delegates.
|
|
92158
|
+
parts = [
|
|
92159
|
+
f"Use the delegate_task tool to hand the following task to a subagent "
|
|
92160
|
+
f"acting as the {name} RuleSync subagent.",
|
|
92161
|
+
f"Goal: {description}",
|
|
92162
|
+
]
|
|
89823
92163
|
if system_prompt:
|
|
89824
|
-
|
|
92164
|
+
parts.append(f"Subagent instructions:\\n{system_prompt}")
|
|
89825
92165
|
if user_context:
|
|
89826
|
-
|
|
92166
|
+
parts.append(f"Task details:\\n{user_context}")
|
|
89827
92167
|
|
|
89828
|
-
|
|
89829
|
-
|
|
89830
|
-
|
|
89831
|
-
# advertises; a top-level "goal" is an unadvertised legacy path.
|
|
89832
|
-
return ctx.dispatch_tool(
|
|
89833
|
-
"delegate_task",
|
|
89834
|
-
{
|
|
89835
|
-
"tasks": [
|
|
89836
|
-
{
|
|
89837
|
-
"goal": description,
|
|
89838
|
-
"context": "\\n\\n".join(context_parts),
|
|
89839
|
-
},
|
|
89840
|
-
],
|
|
89841
|
-
},
|
|
89842
|
-
)
|
|
92168
|
+
label = f"the {name} subagent task"
|
|
92169
|
+
error = _queue_user_message(ctx, "\\n\\n".join(parts), kwargs, label)
|
|
92170
|
+
return error or f"Queued {label}; the agent will delegate it."
|
|
89843
92171
|
|
|
89844
92172
|
ctx.register_command(command_name, handler, description)
|
|
89845
92173
|
|
|
89846
92174
|
|
|
89847
92175
|
def register(ctx):
|
|
92176
|
+
registered = {}
|
|
89848
92177
|
for subagent in _load_subagents():
|
|
89849
|
-
_register_subagent(ctx, subagent)
|
|
92178
|
+
_register_subagent(ctx, subagent, registered)
|
|
89850
92179
|
`;
|
|
89851
92180
|
}
|
|
89852
92181
|
function getEnabledPluginConfigContent({ currentContent, global }) {
|
|
@@ -89877,7 +92206,7 @@ function getSubagentSpec(rulesyncSubagent) {
|
|
|
89877
92206
|
prompt: rulesyncSubagent.getBody(),
|
|
89878
92207
|
hermes: {
|
|
89879
92208
|
command: hermesCommandName(slug),
|
|
89880
|
-
dispatch: "
|
|
92209
|
+
dispatch: "inject_message"
|
|
89881
92210
|
}
|
|
89882
92211
|
};
|
|
89883
92212
|
}
|
|
@@ -90553,6 +92882,15 @@ const KiroCliSubagentJsonSchema = zod_mini.z.looseObject({
|
|
|
90553
92882
|
allowedTools: zod_mini.z.optional(zod_mini.z.nullable(zod_mini.z.array(zod_mini.z.string()))),
|
|
90554
92883
|
includeMcpJson: zod_mini.z.optional(zod_mini.z.nullable(zod_mini.z.boolean()))
|
|
90555
92884
|
});
|
|
92885
|
+
/**
|
|
92886
|
+
* The name Kiro gives an agent file: its path relative to the agents directory
|
|
92887
|
+
* without the extension, so a nested `team/planner.md` is `team/planner`.
|
|
92888
|
+
* https://kiro.dev/docs/custom-agents/
|
|
92889
|
+
*/
|
|
92890
|
+
function kiroAgentNameFromPath(relativeFilePath) {
|
|
92891
|
+
const posixPath = toPosixPath(relativeFilePath);
|
|
92892
|
+
return posixPath.slice(0, posixPath.length - (0, node_path.extname)(posixPath).length);
|
|
92893
|
+
}
|
|
90556
92894
|
var KiroSubagent = class extends ToolSubagent {
|
|
90557
92895
|
body;
|
|
90558
92896
|
constructor({ body, ...rest }) {
|
|
@@ -90588,7 +92926,7 @@ var KiroSubagent = class extends ToolSubagent {
|
|
|
90588
92926
|
outputRoot: ".",
|
|
90589
92927
|
frontmatter: {
|
|
90590
92928
|
targets: [toolTarget],
|
|
90591
|
-
name: name ?? (
|
|
92929
|
+
name: name ?? kiroAgentNameFromPath(this.getRelativeFilePath()),
|
|
90592
92930
|
description: description ?? void 0,
|
|
90593
92931
|
...Object.keys(kiroSection).length > 0 && { kiro: kiroSection }
|
|
90594
92932
|
},
|
|
@@ -90738,7 +93076,7 @@ var KiroIdeSubagent = class KiroIdeSubagent extends ToolSubagent {
|
|
|
90738
93076
|
outputRoot: ".",
|
|
90739
93077
|
frontmatter: {
|
|
90740
93078
|
targets: ["*"],
|
|
90741
|
-
name: name ?? this.getRelativeFilePath()
|
|
93079
|
+
name: name ?? kiroAgentNameFromPath(this.getRelativeFilePath()),
|
|
90742
93080
|
description: description ?? "",
|
|
90743
93081
|
...Object.keys(kiroIdeSection).length > 0 && { "kiro-ide": kiroIdeSection }
|
|
90744
93082
|
},
|
|
@@ -92669,6 +95007,166 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
|
|
|
92669
95007
|
}
|
|
92670
95008
|
};
|
|
92671
95009
|
//#endregion
|
|
95010
|
+
//#region src/features/subagents/xum-subagent.ts
|
|
95011
|
+
/**
|
|
95012
|
+
* The built-in agent a rulesync subagent inherits from when its `xum` block
|
|
95013
|
+
* names neither a `base` nor its own `tools`. Xum gives an agent no tools at
|
|
95014
|
+
* all unless some agent in its `base` chain declares them, so a definition
|
|
95015
|
+
* without either would be a prompt that cannot read or edit anything.
|
|
95016
|
+
*/
|
|
95017
|
+
const XUM_DEFAULT_BASE_AGENT = "exec";
|
|
95018
|
+
/**
|
|
95019
|
+
* Frontmatter of a Xum agent definition. `name` (the display name) is
|
|
95020
|
+
* required; the other documented fields are typed loosely and unknown keys are
|
|
95021
|
+
* kept (looseObject) so a round trip never drops them.
|
|
95022
|
+
*
|
|
95023
|
+
* @see https://xum.cdr.dev/agents
|
|
95024
|
+
*/
|
|
95025
|
+
const XumSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
95026
|
+
name: zod_mini.z.string(),
|
|
95027
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
95028
|
+
base: zod_mini.z.optional(zod_mini.z.string()),
|
|
95029
|
+
disabled: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
95030
|
+
ui: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
95031
|
+
prompt: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
95032
|
+
subagent: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
95033
|
+
ai: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
95034
|
+
tools: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
95035
|
+
});
|
|
95036
|
+
/**
|
|
95037
|
+
* Subagent generator for Xum (formerly Mux).
|
|
95038
|
+
*
|
|
95039
|
+
* Xum reads custom agent definitions — Markdown with YAML frontmatter — from
|
|
95040
|
+
* the top level of `.xum/agents/` (project) and `~/.xum/agents/` (user); the
|
|
95041
|
+
* body is the agent's instruction prompt. The file name (without `.md`) is the
|
|
95042
|
+
* agent id, so it must be lowercase letters and digits with single `-` / `_`
|
|
95043
|
+
* separators; Xum skips other files with a warning.
|
|
95044
|
+
*
|
|
95045
|
+
* rulesync writes `name` and `description`, spreads the `xum` section (`base`,
|
|
95046
|
+
* `ui`, `prompt`, `subagent`, `ai`, `tools`, ...) on top, and fills two
|
|
95047
|
+
* defaults so the definition works as a delegated subagent out of the box:
|
|
95048
|
+
* `subagent.runnable: true` (required for `task({ agentId })` to spawn it) and,
|
|
95049
|
+
* when the section sets neither `base` nor `tools`, `base: exec`.
|
|
95050
|
+
*
|
|
95051
|
+
* @see https://xum.cdr.dev/agents
|
|
95052
|
+
*/
|
|
95053
|
+
var XumSubagent = class XumSubagent extends ToolSubagent {
|
|
95054
|
+
frontmatter;
|
|
95055
|
+
body;
|
|
95056
|
+
constructor({ frontmatter, body, ...rest }) {
|
|
95057
|
+
if (rest.validate !== false) {
|
|
95058
|
+
const result = XumSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
95059
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
95060
|
+
}
|
|
95061
|
+
super({ ...rest });
|
|
95062
|
+
this.frontmatter = frontmatter;
|
|
95063
|
+
this.body = body;
|
|
95064
|
+
}
|
|
95065
|
+
static getSettablePaths(_options = {}) {
|
|
95066
|
+
return { relativeDirPath: XUM_AGENTS_DIR_PATH };
|
|
95067
|
+
}
|
|
95068
|
+
getFrontmatter() {
|
|
95069
|
+
return this.frontmatter;
|
|
95070
|
+
}
|
|
95071
|
+
getBody() {
|
|
95072
|
+
return this.body;
|
|
95073
|
+
}
|
|
95074
|
+
toRulesyncSubagent() {
|
|
95075
|
+
const { name, description, ...restFields } = this.frontmatter;
|
|
95076
|
+
return new RulesyncSubagent({
|
|
95077
|
+
outputRoot: ".",
|
|
95078
|
+
frontmatter: {
|
|
95079
|
+
targets: ["*"],
|
|
95080
|
+
name,
|
|
95081
|
+
description,
|
|
95082
|
+
...Object.keys(restFields).length > 0 && { xum: restFields }
|
|
95083
|
+
},
|
|
95084
|
+
body: this.body,
|
|
95085
|
+
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
|
|
95086
|
+
relativeFilePath: this.getRelativeFilePath(),
|
|
95087
|
+
validate: true
|
|
95088
|
+
});
|
|
95089
|
+
}
|
|
95090
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
|
|
95091
|
+
const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
|
|
95092
|
+
const xumSection = this.filterToolSpecificSection(rulesyncFrontmatter.xum ?? {}, ["name", "description"]);
|
|
95093
|
+
const subagentSection = xumSection.subagent;
|
|
95094
|
+
const result = XumSubagentFrontmatterSchema.safeParse({
|
|
95095
|
+
name: rulesyncFrontmatter.name,
|
|
95096
|
+
...rulesyncFrontmatter.description && { description: rulesyncFrontmatter.description },
|
|
95097
|
+
...xumSection.base === void 0 && xumSection.tools === void 0 && { base: XUM_DEFAULT_BASE_AGENT },
|
|
95098
|
+
...xumSection,
|
|
95099
|
+
subagent: {
|
|
95100
|
+
runnable: true,
|
|
95101
|
+
...isPlainObject$1(subagentSection) ? subagentSection : {}
|
|
95102
|
+
}
|
|
95103
|
+
});
|
|
95104
|
+
if (!result.success) throw new Error(`Invalid xum subagent frontmatter in ${rulesyncSubagent.getRelativeFilePath()}: ${formatError(result.error)}`);
|
|
95105
|
+
const body = rulesyncSubagent.getBody();
|
|
95106
|
+
return new XumSubagent({
|
|
95107
|
+
outputRoot,
|
|
95108
|
+
frontmatter: result.data,
|
|
95109
|
+
body,
|
|
95110
|
+
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
95111
|
+
relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
|
|
95112
|
+
fileContent: stringifyFrontmatter(body, result.data),
|
|
95113
|
+
validate
|
|
95114
|
+
});
|
|
95115
|
+
}
|
|
95116
|
+
validate() {
|
|
95117
|
+
if (!this.frontmatter) return {
|
|
95118
|
+
success: true,
|
|
95119
|
+
error: null
|
|
95120
|
+
};
|
|
95121
|
+
const result = XumSubagentFrontmatterSchema.safeParse(this.frontmatter);
|
|
95122
|
+
if (result.success) return {
|
|
95123
|
+
success: true,
|
|
95124
|
+
error: null
|
|
95125
|
+
};
|
|
95126
|
+
return {
|
|
95127
|
+
success: false,
|
|
95128
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
95129
|
+
};
|
|
95130
|
+
}
|
|
95131
|
+
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
95132
|
+
return this.isTargetedByRulesyncSubagentDefault({
|
|
95133
|
+
rulesyncSubagent,
|
|
95134
|
+
toolTarget: "xum"
|
|
95135
|
+
});
|
|
95136
|
+
}
|
|
95137
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
|
|
95138
|
+
const paths = this.getSettablePaths({ global });
|
|
95139
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
95140
|
+
const fileContent = await readFileContent(filePath);
|
|
95141
|
+
const { frontmatter, body } = parseFrontmatter(fileContent, filePath);
|
|
95142
|
+
const result = XumSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
95143
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
95144
|
+
return new XumSubagent({
|
|
95145
|
+
outputRoot,
|
|
95146
|
+
relativeDirPath: paths.relativeDirPath,
|
|
95147
|
+
relativeFilePath,
|
|
95148
|
+
frontmatter: result.data,
|
|
95149
|
+
body: body.trim(),
|
|
95150
|
+
fileContent,
|
|
95151
|
+
validate
|
|
95152
|
+
});
|
|
95153
|
+
}
|
|
95154
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
95155
|
+
return new XumSubagent({
|
|
95156
|
+
outputRoot,
|
|
95157
|
+
relativeDirPath,
|
|
95158
|
+
relativeFilePath,
|
|
95159
|
+
frontmatter: {
|
|
95160
|
+
name: "",
|
|
95161
|
+
description: ""
|
|
95162
|
+
},
|
|
95163
|
+
body: "",
|
|
95164
|
+
fileContent: "",
|
|
95165
|
+
validate: false
|
|
95166
|
+
});
|
|
95167
|
+
}
|
|
95168
|
+
};
|
|
95169
|
+
//#endregion
|
|
92672
95170
|
//#region src/features/subagents/zcode-subagent.ts
|
|
92673
95171
|
const ZcodeSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
92674
95172
|
name: zod_mini.z.string(),
|
|
@@ -93134,6 +95632,17 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
93134
95632
|
filePattern: "*.md"
|
|
93135
95633
|
}
|
|
93136
95634
|
}],
|
|
95635
|
+
["firebender", {
|
|
95636
|
+
class: FirebenderSubagent,
|
|
95637
|
+
meta: {
|
|
95638
|
+
supportsProject: true,
|
|
95639
|
+
supportsSimulated: false,
|
|
95640
|
+
supportsGlobal: true,
|
|
95641
|
+
emitsEmptyAggregate: true,
|
|
95642
|
+
emitsEmptyAggregateOnDelete: true,
|
|
95643
|
+
filePattern: "*.md"
|
|
95644
|
+
}
|
|
95645
|
+
}],
|
|
93137
95646
|
["goose", {
|
|
93138
95647
|
class: GooseSubagent,
|
|
93139
95648
|
meta: {
|
|
@@ -93185,7 +95694,8 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
93185
95694
|
supportsProject: true,
|
|
93186
95695
|
supportsSimulated: false,
|
|
93187
95696
|
supportsGlobal: true,
|
|
93188
|
-
filePattern: "
|
|
95697
|
+
filePattern: "**/*.json",
|
|
95698
|
+
supportsNestedPaths: true
|
|
93189
95699
|
}
|
|
93190
95700
|
}],
|
|
93191
95701
|
["kiro-ide", {
|
|
@@ -93194,7 +95704,8 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
93194
95704
|
supportsProject: true,
|
|
93195
95705
|
supportsSimulated: false,
|
|
93196
95706
|
supportsGlobal: true,
|
|
93197
|
-
filePattern: "
|
|
95707
|
+
filePattern: "**/*.md",
|
|
95708
|
+
supportsNestedPaths: true
|
|
93198
95709
|
}
|
|
93199
95710
|
}],
|
|
93200
95711
|
["lettacode", {
|
|
@@ -93361,6 +95872,15 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
93361
95872
|
filePattern: "*.toml"
|
|
93362
95873
|
}
|
|
93363
95874
|
}],
|
|
95875
|
+
["xum", {
|
|
95876
|
+
class: XumSubagent,
|
|
95877
|
+
meta: {
|
|
95878
|
+
supportsProject: true,
|
|
95879
|
+
supportsSimulated: false,
|
|
95880
|
+
supportsGlobal: true,
|
|
95881
|
+
filePattern: "*.md"
|
|
95882
|
+
}
|
|
95883
|
+
}],
|
|
93364
95884
|
["zcode", {
|
|
93365
95885
|
class: ZcodeSubagent,
|
|
93366
95886
|
meta: {
|
|
@@ -93415,6 +95935,7 @@ function compareRelativePathsShallowestFirst(left, right) {
|
|
|
93415
95935
|
var SubagentsProcessor = class extends FeatureProcessor {
|
|
93416
95936
|
toolTarget;
|
|
93417
95937
|
global;
|
|
95938
|
+
deleteOrphans;
|
|
93418
95939
|
getFactory;
|
|
93419
95940
|
/**
|
|
93420
95941
|
* Whether the last `loadRulesyncFiles` found a `subagents/` directory under
|
|
@@ -93422,13 +95943,14 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
93422
95943
|
* agents in its Pool settings are its own and must not be retracted.
|
|
93423
95944
|
*/
|
|
93424
95945
|
rulesyncSourceDirFound = false;
|
|
93425
|
-
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, logger }) {
|
|
95946
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, deleteOrphans = false, logger }) {
|
|
93426
95947
|
super({
|
|
93427
95948
|
outputRoot,
|
|
93428
95949
|
inputRoots,
|
|
93429
95950
|
dryRun,
|
|
93430
95951
|
logger
|
|
93431
95952
|
});
|
|
95953
|
+
this.deleteOrphans = deleteOrphans;
|
|
93432
95954
|
const result = SubagentsProcessorToolTargetSchema.safeParse(toolTarget);
|
|
93433
95955
|
if (!result.success) throw new Error(`Invalid tool target for SubagentsProcessor: ${toolTarget}. ${formatError(result.error)}`);
|
|
93434
95956
|
this.toolTarget = result.data;
|
|
@@ -93436,7 +95958,9 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
93436
95958
|
this.getFactory = getFactory;
|
|
93437
95959
|
}
|
|
93438
95960
|
emitsToolFilesForEmptySource() {
|
|
93439
|
-
|
|
95961
|
+
const { meta } = this.getFactory(this.toolTarget);
|
|
95962
|
+
if (meta.emitsEmptyAggregate !== true) return false;
|
|
95963
|
+
return this.rulesyncSourceDirFound || this.deleteOrphans && meta.emitsEmptyAggregateOnDelete === true;
|
|
93440
95964
|
}
|
|
93441
95965
|
async convertRulesyncFilesToToolFiles(rulesyncFiles) {
|
|
93442
95966
|
const rulesyncSubagents = rulesyncFiles.filter((file) => file instanceof RulesyncSubagent);
|
|
@@ -93452,11 +95976,12 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
93452
95976
|
});
|
|
93453
95977
|
if (factory.class.fromRulesyncSubagents) {
|
|
93454
95978
|
if (targeted.length === 0 && !this.emitsToolFilesForEmptySource()) return [];
|
|
93455
|
-
const toolSubagents = factory.class.fromRulesyncSubagents({
|
|
95979
|
+
const toolSubagents = await factory.class.fromRulesyncSubagents({
|
|
93456
95980
|
outputRoot: this.outputRoot,
|
|
93457
95981
|
rulesyncSubagents: targeted,
|
|
93458
95982
|
global: this.global,
|
|
93459
|
-
logger: this.logger
|
|
95983
|
+
logger: this.logger,
|
|
95984
|
+
deleteOrphans: this.deleteOrphans && !this.hasRulesyncSourceLoadFailure()
|
|
93460
95985
|
});
|
|
93461
95986
|
return Array.isArray(toolSubagents) ? toolSubagents : [toolSubagents];
|
|
93462
95987
|
}
|
|
@@ -94202,7 +96727,7 @@ const isAiassistantApply = (value) => AIASSISTANT_APPLY_VALUES.includes(value);
|
|
|
94202
96727
|
/**
|
|
94203
96728
|
* Globs that match the whole project and so say nothing beyond "always".
|
|
94204
96729
|
*/
|
|
94205
|
-
const UNIVERSAL_GLOBS$
|
|
96730
|
+
const UNIVERSAL_GLOBS$6 = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
94206
96731
|
const METADATA_DELIMITER = "---";
|
|
94207
96732
|
/**
|
|
94208
96733
|
* Splits text into lines that keep their own line terminator, so the body can
|
|
@@ -94327,7 +96852,7 @@ var AiassistantRule = class AiassistantRule extends ToolRule {
|
|
|
94327
96852
|
*/
|
|
94328
96853
|
static buildMetadata(frontmatter) {
|
|
94329
96854
|
const globs = (frontmatter.globs ?? []).map((glob) => flattenValue(glob)).filter((glob) => glob.length > 0).flatMap((glob) => expandBraceAlternations(glob));
|
|
94330
|
-
const hasUniversalGlob = globs.some((glob) => UNIVERSAL_GLOBS$
|
|
96855
|
+
const hasUniversalGlob = globs.some((glob) => UNIVERSAL_GLOBS$6.has(glob));
|
|
94331
96856
|
const instructions = frontmatter.description && flattenValue(frontmatter.description);
|
|
94332
96857
|
const explicitApply = frontmatter.aiassistant?.apply && flattenValue(frontmatter.aiassistant.apply);
|
|
94333
96858
|
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";
|
|
@@ -96981,7 +99506,7 @@ function normalizePatternList(value) {
|
|
|
96981
99506
|
* an always-on rule into a "when any file is in context" rule. Dropped on
|
|
96982
99507
|
* generate and mapped back on import.
|
|
96983
99508
|
*/
|
|
96984
|
-
const UNIVERSAL_GLOBS$
|
|
99509
|
+
const UNIVERSAL_GLOBS$5 = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
96985
99510
|
/**
|
|
96986
99511
|
* Rule generator for Continue (continuedev/continue), the open-source IDE
|
|
96987
99512
|
* extension and CLI (`cn`).
|
|
@@ -97095,7 +99620,7 @@ var ContinueRule = class ContinueRule extends ToolRule {
|
|
|
97095
99620
|
if (alwaysApply === true) return;
|
|
97096
99621
|
const targetGlobs = continueGlobs ?? parentGlobs;
|
|
97097
99622
|
if (!targetGlobs || targetGlobs.length === 0) return;
|
|
97098
|
-
if (targetGlobs.every((glob) => UNIVERSAL_GLOBS$
|
|
99623
|
+
if (targetGlobs.every((glob) => UNIVERSAL_GLOBS$5.has(glob.trim()))) return;
|
|
97099
99624
|
return targetGlobs;
|
|
97100
99625
|
}
|
|
97101
99626
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
@@ -97587,7 +100112,7 @@ const CursorRuleFrontmatterSchema = zod_mini.z.object({
|
|
|
97587
100112
|
* does not already say. Same pair the Cline and Qwen Code adapters treat as
|
|
97588
100113
|
* universal.
|
|
97589
100114
|
*/
|
|
97590
|
-
const UNIVERSAL_GLOBS$
|
|
100115
|
+
const UNIVERSAL_GLOBS$4 = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
97591
100116
|
var CursorRule = class CursorRule extends ToolRule {
|
|
97592
100117
|
frontmatter;
|
|
97593
100118
|
body;
|
|
@@ -97628,7 +100153,8 @@ var CursorRule = class CursorRule extends ToolRule {
|
|
|
97628
100153
|
}
|
|
97629
100154
|
/**
|
|
97630
100155
|
* Custom parse function for Cursor MDC files
|
|
97631
|
-
* MDC files don't support quotes in YAML, so we need to handle patterns like *.ts specially
|
|
100156
|
+
* MDC files don't support quotes in YAML, so we need to handle patterns like *.ts specially.
|
|
100157
|
+
* Also used by Firebender, whose `.firebender/rules/*.mdc` files carry the same frontmatter.
|
|
97632
100158
|
*/
|
|
97633
100159
|
static parseCursorFrontmatter(fileContent, filePath) {
|
|
97634
100160
|
const bounds = findFrontmatterBlockBounds(fileContent);
|
|
@@ -97691,7 +100217,7 @@ var CursorRule = class CursorRule extends ToolRule {
|
|
|
97691
100217
|
static resolveCursorGlobs({ cursorSpecificGlobs, parentGlobs, alwaysApply }) {
|
|
97692
100218
|
const targetGlobs = cursorSpecificGlobs !== void 0 ? cursorSpecificGlobs : parentGlobs;
|
|
97693
100219
|
if (!targetGlobs || targetGlobs.length === 0) return;
|
|
97694
|
-
if (alwaysApply && targetGlobs.every((glob) => UNIVERSAL_GLOBS$
|
|
100220
|
+
if (alwaysApply && targetGlobs.every((glob) => UNIVERSAL_GLOBS$4.has(glob.trim()))) return;
|
|
97695
100221
|
return targetGlobs.join(",");
|
|
97696
100222
|
}
|
|
97697
100223
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
|
|
@@ -98605,6 +101131,211 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
98605
101131
|
}
|
|
98606
101132
|
};
|
|
98607
101133
|
//#endregion
|
|
101134
|
+
//#region src/features/rules/firebender-rule.ts
|
|
101135
|
+
/**
|
|
101136
|
+
* Frontmatter of a Firebender `.mdc` rule. Every field is optional; `globs`
|
|
101137
|
+
* is one comma-separated string, and a YAML list is tolerated on import.
|
|
101138
|
+
*/
|
|
101139
|
+
const FirebenderRuleFrontmatterSchema = zod_mini.z.looseObject({
|
|
101140
|
+
description: zod_mini.z.optional(zod_mini.z.nullable(zod_mini.z.string())),
|
|
101141
|
+
globs: zod_mini.z.optional(zod_mini.z.nullable(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))),
|
|
101142
|
+
alwaysApply: zod_mini.z.optional(zod_mini.z.nullable(zod_mini.z.boolean()))
|
|
101143
|
+
});
|
|
101144
|
+
/** Globs that match every file, and so add nothing to an always-applied rule. */
|
|
101145
|
+
const UNIVERSAL_GLOBS$3 = /* @__PURE__ */ new Set(["**/*", "*"]);
|
|
101146
|
+
function normalizeGlobs$2(globs) {
|
|
101147
|
+
if (globs === null || globs === void 0) return [];
|
|
101148
|
+
return (typeof globs === "string" ? globs.split(",") : globs).map((glob) => glob.trim()).filter((glob) => glob.length > 0);
|
|
101149
|
+
}
|
|
101150
|
+
/**
|
|
101151
|
+
* Rule generator for Firebender.
|
|
101152
|
+
*
|
|
101153
|
+
* Root rule: Firebender looks for `.firebender/AGENTS.md` first (then the
|
|
101154
|
+
* project-root `AGENTS.md`, then the same pair in parent directories, then
|
|
101155
|
+
* `~/.firebender/AGENTS.md`) and uses the first file it finds, so the root rule
|
|
101156
|
+
* is written to `.firebender/AGENTS.md` in the project and to
|
|
101157
|
+
* `~/.firebender/AGENTS.md` globally, where it does not collide with the
|
|
101158
|
+
* project-root `AGENTS.md` the `agentsmd` target owns.
|
|
101159
|
+
*
|
|
101160
|
+
* Non-root rules: `.firebender/rules/*.mdc` (project) and
|
|
101161
|
+
* `~/.firebender/rules/*.mdc` (personal), each with optional Cursor-style
|
|
101162
|
+
* frontmatter: `description`, `globs` (one comma-separated string) and
|
|
101163
|
+
* `alwaysApply`. A rule with specific globs is written with
|
|
101164
|
+
* `alwaysApply: false` and those globs; every other rule is always applied.
|
|
101165
|
+
*
|
|
101166
|
+
* @see https://docs.firebender.com/api-reference/rules
|
|
101167
|
+
* @see https://docs.firebender.com/api-reference/agents-md
|
|
101168
|
+
*/
|
|
101169
|
+
var FirebenderRule = class FirebenderRule extends ToolRule {
|
|
101170
|
+
frontmatter;
|
|
101171
|
+
body;
|
|
101172
|
+
constructor({ frontmatter = {}, body, ...rest }) {
|
|
101173
|
+
if (rest.validate && !rest.root) {
|
|
101174
|
+
const result = FirebenderRuleFrontmatterSchema.safeParse(frontmatter);
|
|
101175
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
|
|
101176
|
+
}
|
|
101177
|
+
const resolvedBody = body ?? rest.fileContent;
|
|
101178
|
+
super({
|
|
101179
|
+
...rest,
|
|
101180
|
+
fileContent: rest.root ? resolvedBody : stringifyFrontmatter(resolvedBody, FirebenderRule.toOutputFrontmatter(frontmatter), { avoidBlockScalars: true })
|
|
101181
|
+
});
|
|
101182
|
+
this.frontmatter = frontmatter;
|
|
101183
|
+
this.body = resolvedBody;
|
|
101184
|
+
}
|
|
101185
|
+
static getSettablePaths({ excludeToolDir } = {}) {
|
|
101186
|
+
return {
|
|
101187
|
+
root: {
|
|
101188
|
+
relativeDirPath: buildToolPath(FIREBENDER_DIR, ".", excludeToolDir),
|
|
101189
|
+
relativeFilePath: FIREBENDER_AGENTS_MD_FILE_NAME
|
|
101190
|
+
},
|
|
101191
|
+
nonRoot: { relativeDirPath: buildToolPath(FIREBENDER_DIR, "rules", excludeToolDir) }
|
|
101192
|
+
};
|
|
101193
|
+
}
|
|
101194
|
+
/**
|
|
101195
|
+
* The frontmatter written to a `.mdc` file: `alwaysApply` first, then the
|
|
101196
|
+
* flattened `description`, then the globs joined the way Firebender's docs
|
|
101197
|
+
* spell them (`"*.kt, *.kts"`).
|
|
101198
|
+
*/
|
|
101199
|
+
static toOutputFrontmatter(frontmatter) {
|
|
101200
|
+
const description = typeof frontmatter.description === "string" ? frontmatter.description.replace(/[\r\n\u0085\u2028\u2029]+/g, " ").trim() : "";
|
|
101201
|
+
const globs = normalizeGlobs$2(frontmatter.globs);
|
|
101202
|
+
return {
|
|
101203
|
+
...typeof frontmatter.alwaysApply === "boolean" && { alwaysApply: frontmatter.alwaysApply },
|
|
101204
|
+
...description !== "" && { description },
|
|
101205
|
+
...globs.length > 0 && { globs: globs.join(", ") }
|
|
101206
|
+
};
|
|
101207
|
+
}
|
|
101208
|
+
/**
|
|
101209
|
+
* Derives the Firebender frontmatter from the canonical one: specific globs
|
|
101210
|
+
* make a file-scoped rule, everything else (no globs, universal globs) is
|
|
101211
|
+
* always applied. Firebender splits `globs` on every comma, so a brace
|
|
101212
|
+
* alternation such as `*.{ts,tsx}` is expanded into one glob per branch.
|
|
101213
|
+
*/
|
|
101214
|
+
static buildFrontmatter(frontmatter) {
|
|
101215
|
+
const globs = normalizeGlobs$2(frontmatter.globs ?? []).flatMap((glob) => expandBraceAlternations(glob));
|
|
101216
|
+
const specificGlobs = globs.filter((glob) => !UNIVERSAL_GLOBS$3.has(glob));
|
|
101217
|
+
const isFileScoped = specificGlobs.length > 0 && specificGlobs.length === globs.length;
|
|
101218
|
+
const description = frontmatter.description?.trim() || void 0;
|
|
101219
|
+
return {
|
|
101220
|
+
alwaysApply: !isFileScoped,
|
|
101221
|
+
...description !== void 0 && { description },
|
|
101222
|
+
...isFileScoped && { globs: specificGlobs.join(", ") }
|
|
101223
|
+
};
|
|
101224
|
+
}
|
|
101225
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
|
|
101226
|
+
const paths = this.getSettablePaths();
|
|
101227
|
+
const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
|
|
101228
|
+
if (rulesyncFrontmatter.root ?? false) return new FirebenderRule({
|
|
101229
|
+
outputRoot,
|
|
101230
|
+
relativeDirPath: paths.root.relativeDirPath,
|
|
101231
|
+
relativeFilePath: paths.root.relativeFilePath,
|
|
101232
|
+
fileContent: rulesyncRule.getBody(),
|
|
101233
|
+
validate,
|
|
101234
|
+
root: true,
|
|
101235
|
+
description: rulesyncFrontmatter.description,
|
|
101236
|
+
globs: rulesyncFrontmatter.globs
|
|
101237
|
+
});
|
|
101238
|
+
return new FirebenderRule({
|
|
101239
|
+
outputRoot,
|
|
101240
|
+
relativeDirPath: paths.nonRoot.relativeDirPath,
|
|
101241
|
+
relativeFilePath: rulesyncRule.getRelativeFilePath().replace(/\.md$/, ".mdc"),
|
|
101242
|
+
frontmatter: this.buildFrontmatter(rulesyncFrontmatter),
|
|
101243
|
+
body: rulesyncRule.getBody(),
|
|
101244
|
+
fileContent: "",
|
|
101245
|
+
validate,
|
|
101246
|
+
root: false
|
|
101247
|
+
});
|
|
101248
|
+
}
|
|
101249
|
+
static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true }) {
|
|
101250
|
+
const paths = this.getSettablePaths();
|
|
101251
|
+
if (relativeFilePath === paths.root.relativeFilePath && (relativeDirPath === void 0 || relativeDirPath === paths.root.relativeDirPath)) {
|
|
101252
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, paths.root.relativeDirPath, paths.root.relativeFilePath));
|
|
101253
|
+
return new FirebenderRule({
|
|
101254
|
+
outputRoot,
|
|
101255
|
+
relativeDirPath: paths.root.relativeDirPath,
|
|
101256
|
+
relativeFilePath: paths.root.relativeFilePath,
|
|
101257
|
+
fileContent,
|
|
101258
|
+
validate,
|
|
101259
|
+
root: true
|
|
101260
|
+
});
|
|
101261
|
+
}
|
|
101262
|
+
const filePath = (0, node_path.join)(outputRoot, paths.nonRoot.relativeDirPath, relativeFilePath);
|
|
101263
|
+
const fileContent = await readFileContent(filePath);
|
|
101264
|
+
const { frontmatter, body } = CursorRule.parseCursorFrontmatter(fileContent, filePath);
|
|
101265
|
+
const result = FirebenderRuleFrontmatterSchema.safeParse(frontmatter);
|
|
101266
|
+
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
101267
|
+
return new FirebenderRule({
|
|
101268
|
+
outputRoot,
|
|
101269
|
+
relativeDirPath: paths.nonRoot.relativeDirPath,
|
|
101270
|
+
relativeFilePath,
|
|
101271
|
+
frontmatter: result.data,
|
|
101272
|
+
body: body.trim(),
|
|
101273
|
+
fileContent: "",
|
|
101274
|
+
validate,
|
|
101275
|
+
root: false
|
|
101276
|
+
});
|
|
101277
|
+
}
|
|
101278
|
+
toRulesyncRule() {
|
|
101279
|
+
if (this.isRoot()) return this.toRulesyncRuleDefault();
|
|
101280
|
+
const sourceGlobs = normalizeGlobs$2(this.frontmatter.globs);
|
|
101281
|
+
const alwaysApply = this.frontmatter.alwaysApply ?? void 0;
|
|
101282
|
+
const description = this.frontmatter.description?.trim() || void 0;
|
|
101283
|
+
const globs = alwaysApply === true || sourceGlobs.length === 0 ? ["**/*"] : sourceGlobs;
|
|
101284
|
+
return new RulesyncRule({
|
|
101285
|
+
outputRoot: process.cwd(),
|
|
101286
|
+
frontmatter: {
|
|
101287
|
+
targets: ["*"],
|
|
101288
|
+
root: false,
|
|
101289
|
+
...description !== void 0 && { description },
|
|
101290
|
+
globs
|
|
101291
|
+
},
|
|
101292
|
+
body: this.body,
|
|
101293
|
+
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
101294
|
+
relativeFilePath: this.getRelativeFilePath().replace(/\.mdc$/, ".md"),
|
|
101295
|
+
validate: true
|
|
101296
|
+
});
|
|
101297
|
+
}
|
|
101298
|
+
validate() {
|
|
101299
|
+
if (this.isRoot() || !this.frontmatter) return {
|
|
101300
|
+
success: true,
|
|
101301
|
+
error: null
|
|
101302
|
+
};
|
|
101303
|
+
const result = FirebenderRuleFrontmatterSchema.safeParse(this.frontmatter);
|
|
101304
|
+
if (result.success) return {
|
|
101305
|
+
success: true,
|
|
101306
|
+
error: null
|
|
101307
|
+
};
|
|
101308
|
+
return {
|
|
101309
|
+
success: false,
|
|
101310
|
+
error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
|
|
101311
|
+
};
|
|
101312
|
+
}
|
|
101313
|
+
getFrontmatter() {
|
|
101314
|
+
return this.frontmatter;
|
|
101315
|
+
}
|
|
101316
|
+
getBody() {
|
|
101317
|
+
return this.body;
|
|
101318
|
+
}
|
|
101319
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
101320
|
+
const paths = this.getSettablePaths();
|
|
101321
|
+
const isRoot = relativeFilePath === paths.root.relativeFilePath && relativeDirPath === paths.root.relativeDirPath;
|
|
101322
|
+
return new FirebenderRule({
|
|
101323
|
+
outputRoot,
|
|
101324
|
+
relativeDirPath,
|
|
101325
|
+
relativeFilePath,
|
|
101326
|
+
fileContent: "",
|
|
101327
|
+
validate: false,
|
|
101328
|
+
root: isRoot
|
|
101329
|
+
});
|
|
101330
|
+
}
|
|
101331
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
101332
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
101333
|
+
rulesyncRule,
|
|
101334
|
+
toolTarget: "firebender"
|
|
101335
|
+
});
|
|
101336
|
+
}
|
|
101337
|
+
};
|
|
101338
|
+
//#endregion
|
|
98608
101339
|
//#region src/features/rules/gitlabduo-rule.ts
|
|
98609
101340
|
/**
|
|
98610
101341
|
* GitLab Duo CLI reads custom rules from a single plain-Markdown file:
|
|
@@ -101886,6 +104617,117 @@ var WarpRule = class extends NestedAgentsmdRule {
|
|
|
101886
104617
|
}
|
|
101887
104618
|
};
|
|
101888
104619
|
//#endregion
|
|
104620
|
+
//#region src/features/rules/xum-rule.ts
|
|
104621
|
+
/** An ATX level-1 heading line (`# Title`), which Xum's parser treats as an H1. */
|
|
104622
|
+
const ATX_H1_LINE = /^ {0,3}#(?:[ \t]|$)/;
|
|
104623
|
+
/**
|
|
104624
|
+
* Make `body` start at a level-1 heading, adding `# <heading>` when it does not
|
|
104625
|
+
* already open with one.
|
|
104626
|
+
*
|
|
104627
|
+
* Xum reads `.xum/AGENTS.md` (and appends `AGENTS.local.md`) as one Markdown
|
|
104628
|
+
* document and treats a `Mode:` / `Model:` / `Tool:` heading as a scoped
|
|
104629
|
+
* section that runs until the next heading of the same or a higher level.
|
|
104630
|
+
* Fragments rulesync concatenates into that document — folded topic rules and
|
|
104631
|
+
* the local overlay — would otherwise fall inside a scoped section the
|
|
104632
|
+
* preceding fragment ends with, and Xum would inject them only in that mode.
|
|
104633
|
+
* Only an H1 closes a section of any level, so each such fragment opens with
|
|
104634
|
+
* one.
|
|
104635
|
+
*
|
|
104636
|
+
* @see https://github.com/coder/xum/blob/main/src/node/utils/main/markdown.ts
|
|
104637
|
+
*/
|
|
104638
|
+
function ensureXumSectionBoundary({ body, heading }) {
|
|
104639
|
+
const firstLine = body.split(/\r?\n/).find((line) => line.trim() !== "");
|
|
104640
|
+
if (firstLine !== void 0 && ATX_H1_LINE.test(firstLine)) return body;
|
|
104641
|
+
const safeHeading = heading.replaceAll(/[\r\n:]/g, " ").trim() || "Instructions";
|
|
104642
|
+
return body.trim() === "" ? `# ${safeHeading}\n` : `# ${safeHeading}\n\n${body}`;
|
|
104643
|
+
}
|
|
104644
|
+
/** The heading a folded rule opens with: its file name without the extension. */
|
|
104645
|
+
function xumRuleHeading(rulesyncRule) {
|
|
104646
|
+
const fileName = (0, node_path.basename)(rulesyncRule.getRelativeFilePath());
|
|
104647
|
+
return fileName.slice(0, fileName.length - (0, node_path.extname)(fileName).length);
|
|
104648
|
+
}
|
|
104649
|
+
/**
|
|
104650
|
+
* Rule generator for Xum (Coder's parallel coding-agent app, formerly Mux).
|
|
104651
|
+
*
|
|
104652
|
+
* Xum layers its own `.xum/AGENTS.md` (project) and `~/.xum/AGENTS.md` (user)
|
|
104653
|
+
* on top of the shared root `AGENTS.md` / `AGENT.md` / `CLAUDE.md`, which the
|
|
104654
|
+
* `agentsmd` target already writes, so this target writes only the
|
|
104655
|
+
* Xum-dedicated file. Only these files honor the `## Model: <regex>` and
|
|
104656
|
+
* `## Mode: <mode>` scoped sections; rulesync passes the body through as-is,
|
|
104657
|
+
* so such sections authored in a rule keep working. The personal
|
|
104658
|
+
* `.xum/AGENTS.local.md` overlay is written for a `localRoot: true` rule.
|
|
104659
|
+
*
|
|
104660
|
+
* @see https://xum.cdr.dev/agents/instruction-files
|
|
104661
|
+
*/
|
|
104662
|
+
var XumRule = class XumRule extends ToolRule {
|
|
104663
|
+
constructor({ fileContent, root, ...rest }) {
|
|
104664
|
+
super({
|
|
104665
|
+
...rest,
|
|
104666
|
+
fileContent,
|
|
104667
|
+
root: root ?? false
|
|
104668
|
+
});
|
|
104669
|
+
}
|
|
104670
|
+
static getSettablePaths(_options = {}) {
|
|
104671
|
+
return { root: {
|
|
104672
|
+
relativeDirPath: XUM_DIR,
|
|
104673
|
+
relativeFilePath: XUM_RULE_FILE_NAME
|
|
104674
|
+
} };
|
|
104675
|
+
}
|
|
104676
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
|
|
104677
|
+
const { root } = this.getSettablePaths({ global });
|
|
104678
|
+
const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
|
|
104679
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
|
|
104680
|
+
return new XumRule({
|
|
104681
|
+
outputRoot,
|
|
104682
|
+
relativeDirPath: root.relativeDirPath,
|
|
104683
|
+
relativeFilePath: root.relativeFilePath,
|
|
104684
|
+
fileContent,
|
|
104685
|
+
validate,
|
|
104686
|
+
root: true
|
|
104687
|
+
});
|
|
104688
|
+
}
|
|
104689
|
+
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
104690
|
+
const { root } = this.getSettablePaths({ global });
|
|
104691
|
+
const isRoot = rulesyncRule.getFrontmatter().root ?? false;
|
|
104692
|
+
return new XumRule({
|
|
104693
|
+
outputRoot,
|
|
104694
|
+
relativeDirPath: root.relativeDirPath,
|
|
104695
|
+
relativeFilePath: root.relativeFilePath,
|
|
104696
|
+
fileContent: isRoot ? rulesyncRule.getBody() : ensureXumSectionBoundary({
|
|
104697
|
+
body: rulesyncRule.getBody(),
|
|
104698
|
+
heading: xumRuleHeading(rulesyncRule)
|
|
104699
|
+
}),
|
|
104700
|
+
validate,
|
|
104701
|
+
root: isRoot
|
|
104702
|
+
});
|
|
104703
|
+
}
|
|
104704
|
+
toRulesyncRule() {
|
|
104705
|
+
return this.toRulesyncRuleDefault();
|
|
104706
|
+
}
|
|
104707
|
+
validate() {
|
|
104708
|
+
return {
|
|
104709
|
+
success: true,
|
|
104710
|
+
error: null
|
|
104711
|
+
};
|
|
104712
|
+
}
|
|
104713
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
104714
|
+
return new XumRule({
|
|
104715
|
+
outputRoot,
|
|
104716
|
+
relativeDirPath,
|
|
104717
|
+
relativeFilePath,
|
|
104718
|
+
fileContent: "",
|
|
104719
|
+
validate: false,
|
|
104720
|
+
root: relativeFilePath === "AGENTS.md" && relativeDirPath === ".xum"
|
|
104721
|
+
});
|
|
104722
|
+
}
|
|
104723
|
+
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
104724
|
+
return this.isTargetedByRulesyncRuleDefault({
|
|
104725
|
+
rulesyncRule,
|
|
104726
|
+
toolTarget: "xum"
|
|
104727
|
+
});
|
|
104728
|
+
}
|
|
104729
|
+
};
|
|
104730
|
+
//#endregion
|
|
101889
104731
|
//#region src/features/rules/zcode-rule.ts
|
|
101890
104732
|
var ZcodeRule = class ZcodeRule extends ToolRule {
|
|
101891
104733
|
constructor({ fileContent, root, ...rest }) {
|
|
@@ -102374,6 +105216,14 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
102374
105216
|
collisionPolicy: "compose"
|
|
102375
105217
|
}
|
|
102376
105218
|
}],
|
|
105219
|
+
["firebender", {
|
|
105220
|
+
class: FirebenderRule,
|
|
105221
|
+
meta: {
|
|
105222
|
+
extension: "mdc",
|
|
105223
|
+
supportsGlobal: true,
|
|
105224
|
+
ruleDiscoveryMode: "auto"
|
|
105225
|
+
}
|
|
105226
|
+
}],
|
|
102377
105227
|
["goose", {
|
|
102378
105228
|
class: GooseRule,
|
|
102379
105229
|
meta: {
|
|
@@ -102657,6 +105507,17 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
|
|
|
102657
105507
|
collisionPolicy: "fold"
|
|
102658
105508
|
}
|
|
102659
105509
|
}],
|
|
105510
|
+
["xum", {
|
|
105511
|
+
class: XumRule,
|
|
105512
|
+
meta: {
|
|
105513
|
+
extension: "md",
|
|
105514
|
+
supportsGlobal: true,
|
|
105515
|
+
ruleDiscoveryMode: "auto",
|
|
105516
|
+
collisionPolicy: "fold",
|
|
105517
|
+
localRootMode: "separate-local-file",
|
|
105518
|
+
localRootFileName: XUM_LOCAL_RULE_FILE_NAME
|
|
105519
|
+
}
|
|
105520
|
+
}],
|
|
102660
105521
|
["devin", {
|
|
102661
105522
|
class: DevinRule,
|
|
102662
105523
|
meta: {
|
|
@@ -103427,18 +106288,39 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
103427
106288
|
root: true,
|
|
103428
106289
|
localRoot
|
|
103429
106290
|
});
|
|
103430
|
-
|
|
103431
|
-
|
|
103432
|
-
|
|
103433
|
-
|
|
103434
|
-
|
|
103435
|
-
relativeDirPath: relativeDirPath ?? ".qwen",
|
|
103436
|
-
relativeFilePath: fileName,
|
|
103437
|
-
fileContent: body,
|
|
103438
|
-
validate: true,
|
|
103439
|
-
root: true,
|
|
106291
|
+
return this.buildToolDirLocalRootFile({
|
|
106292
|
+
factory,
|
|
106293
|
+
fileName,
|
|
106294
|
+
body,
|
|
106295
|
+
relativeDirPath,
|
|
103440
106296
|
localRoot
|
|
103441
106297
|
});
|
|
106298
|
+
}
|
|
106299
|
+
/**
|
|
106300
|
+
* Qwen Code and Xum read the personal local file from their tool directory
|
|
106301
|
+
* (`.qwen/QWEN.local.md`, `.xum/AGENTS.local.md`), not the project root
|
|
106302
|
+
* (project scope only; global handling never reaches here).
|
|
106303
|
+
*/
|
|
106304
|
+
buildToolDirLocalRootFile({ factory, fileName, body, relativeDirPath, localRoot }) {
|
|
106305
|
+
const toolDirEntry = [[QwencodeRule, QWENCODE_DIR], [XumRule, XUM_DIR]].find(([base]) => isClassOrSubclassOf({
|
|
106306
|
+
candidate: factory.class,
|
|
106307
|
+
base
|
|
106308
|
+
}));
|
|
106309
|
+
if (toolDirEntry) {
|
|
106310
|
+
const [toolDirClass, toolDir] = toolDirEntry;
|
|
106311
|
+
return new toolDirClass({
|
|
106312
|
+
outputRoot: this.outputRoot,
|
|
106313
|
+
relativeDirPath: relativeDirPath ?? toolDir,
|
|
106314
|
+
relativeFilePath: fileName,
|
|
106315
|
+
fileContent: toolDirClass === XumRule && !localRoot ? ensureXumSectionBoundary({
|
|
106316
|
+
body,
|
|
106317
|
+
heading: "Local instructions"
|
|
106318
|
+
}) : body,
|
|
106319
|
+
validate: true,
|
|
106320
|
+
root: true,
|
|
106321
|
+
localRoot
|
|
106322
|
+
});
|
|
106323
|
+
}
|
|
103442
106324
|
return null;
|
|
103443
106325
|
}
|
|
103444
106326
|
/**
|
|
@@ -106190,6 +109072,7 @@ async function generateSubagentsCore(params) {
|
|
|
106190
109072
|
toolTarget,
|
|
106191
109073
|
global: config.getGlobal(),
|
|
106192
109074
|
dryRun: config.isPreviewMode(),
|
|
109075
|
+
deleteOrphans: config.getDelete(),
|
|
106193
109076
|
logger
|
|
106194
109077
|
});
|
|
106195
109078
|
const result = await processFeatureWithRulesyncFiles({
|
|
@@ -107064,6 +109947,12 @@ Object.defineProperty(exports, "PACKAGING_TOOL_TARGETS", {
|
|
|
107064
109947
|
return PACKAGING_TOOL_TARGETS;
|
|
107065
109948
|
}
|
|
107066
109949
|
});
|
|
109950
|
+
Object.defineProperty(exports, "PathOutsideRootError", {
|
|
109951
|
+
enumerable: true,
|
|
109952
|
+
get: function() {
|
|
109953
|
+
return PathOutsideRootError;
|
|
109954
|
+
}
|
|
109955
|
+
});
|
|
107067
109956
|
Object.defineProperty(exports, "QWENCODE_DIR", {
|
|
107068
109957
|
enumerable: true,
|
|
107069
109958
|
get: function() {
|
|
@@ -107364,6 +110253,12 @@ Object.defineProperty(exports, "assertDirectoryIfExists", {
|
|
|
107364
110253
|
return assertDirectoryIfExists;
|
|
107365
110254
|
}
|
|
107366
110255
|
});
|
|
110256
|
+
Object.defineProperty(exports, "assertPathResolvesInsideRoot", {
|
|
110257
|
+
enumerable: true,
|
|
110258
|
+
get: function() {
|
|
110259
|
+
return assertPathResolvesInsideRoot;
|
|
110260
|
+
}
|
|
110261
|
+
});
|
|
107367
110262
|
Object.defineProperty(exports, "assertTargetsFeaturesExclusive", {
|
|
107368
110263
|
enumerable: true,
|
|
107369
110264
|
get: function() {
|